最近有个师弟拿了个毕业设计题目来找我,题目是“基于SpringBoot的校园家教信息平台的设计开发”,代码仓库名还带着一长串随机尾号,典型的学校课题管理系统的自动编号。我帮他从头到尾梳理了一遍,从业务需求到数据库设计,再到SpringBoot后端实际写接口、接MinIO、前后端联调、最后Docker部署,前后折腾了差不多两个星期。这过程中坑也不少,主要是SpringBoot版本兼容、Vue打包进jar之后的404问题、MinIO容器内外网访问差异这几个,都是常规教程里基本不会写清楚的。
这个平台本身不复杂:学生、家长可以注册登录,发布家教需求或者直接搜索大学生家教老师;家教老师可以认证资料、接单授课;管理员负责审核认证、维护公告和用户管理。后端用SpringBoot,前端用Vue,数据库用MySQL,缓存用Redis,文件对象存储选MinIO,整体就是现在毕设项目和中小型系统最主流的那套组合。写这篇东西的初衷很简单——如果能把从零搭建这个平台的过程、选型理由、踩过的坑都讲明白,那你在做类似SpringBoot项目的时候能少走很多弯路。不管你是正在做毕设的本科生,还是想快速上手一套带文件上传、带部署的SpringBoot全栈项目的开发者,这篇文章都值得认真看一遍。
1. 先想清楚业务再动手:家教平台的需求边界与角色梳理
1.1 三种核心角色的动作闭环
很多同学拿到题目就急着建工程、写代码,结果做到一半发现表结构不对、接口设计也不符合业务逻辑,回头返工更浪费时间。做平台类系统,第一步永远是先把角色和业务流程走通。
校园家教信息平台最常见的角色有三类:学生/家长、家教老师、管理员。
学生/家长的诉求是:注册登录后,能按科目、年级、所在城市或学校筛选合适的家教老师,查看老师的资料、价格、评价,然后发起预约或者直接下单。下单之后可以和老师约定上课时间,上完课再评价打分。
家教老师的诉求是:注册登录后完善个人资料,包括学历、学校、擅长科目、家教经验、期望时薪等,提交认证材料等待管理员审核。认证通过后能收到学生订单提醒,接单、完成授课、总结订单。
管理员的工作是:审核老师认证资料,处理用户举报,发布平台公告,管理用户状态(封禁/解封),以及查看平台的整体订单数据。
这三类角色形成了完整的业务闭环:学生找老师 -> 下单 -> 老师接单 -> 线下授课 -> 线上互相评价 -> 管理员保障秩序。
1.2 平台的隐性价值:连接与信任
校园家教平台表面上是信息中介,本质上做的是连接和信任两件事。
连接很好理解:学生身边没有合适的家教资源,老师也不知道去哪儿找学生,平台把供需信息聚合起来,做筛选和匹配。但信任才是这类平台真正的门槛。校内一对一辅导涉及到人身安全、教学质量、费用纠纷,所以平台必须有认证机制(老师实名、学生实名),有评价机制(订单完成后才能评价),有订单状态跟踪(避免私下交易后平台失控)。
我从一开始就跟师弟强调:业务上的信任链路,比技术堆砌重要得多。你不能只做CRUD,必须在数据库里预留认证状态、评价关联、订单状态流转这些字段,否则功能看起来齐全,实际没法用。
1.3 非功能需求:校园场景下的取舍
校园家教平台属于典型的轻量级业务系统,并发量不大,但没有并发不代表可以乱写。需要考虑的非功能需求至少有三个:
第一是权限。学生、老师、管理员三种角色的接口要分开,不可能让学生去调管理员的审核接口。我选择用JWT里携带角色,配合SpringBoot拦截器做路径级权限校验。
第二是数据安全。用户密码必须加密存储,不能明文;用户上传的身份证照片、学生证照片属于敏感资料,不能公网永久直接访问,需要用对象存储的私有桶加临时URL。
第三是可维护性。项目要分模块写清楚,Controller、Service、Mapper分好层,配置文件和业务代码分离,环境准备文档要跟上。这些看起来简单,但在毕设项目的评分里往往是加分项,更重要的是以后你自己回看代码能少消耗脑细胞。
2. 技术选型为什么是这套组合:SpringBoot版本与依赖取舍
2.1 为什么坚持用SpringBoot而不是Spring MVC或Spring Cloud
SpringBoot现在已经成了Java后端开发的事实标准,它最大的贡献就是自动装配和内嵌服务器。以前用Spring MVC搭一个项目要写一堆XML配置文件,配置Tomcat、配置数据源、配置事务管理器,光这些前置工作就能耗掉一两天。SpringBoot把常规配置做成starter,你只需要引入依赖,框架自动帮你装配好。
有人会问,校园家教平台这种项目有必要用Spring Cloud吗?我的回答是:没必要,而且强烈不建议。Spring Cloud是一套微服务解决方案,包含注册中心、配置中心、网关、熔断等一堆组件,部署成本和学习成本都很高。对一个单体就能搞定的小平台,上了Spring Cloud只会让你陷入服务拆分和分布式事务的泥潭。合理的做法是:SpringBoot单体应用 + 合适的模块划分,以后真要做大,再按业务边界拆微服务也不迟。
2.2 版本选择的坑:SpringBoot 2.7还是3.x
SpringBoot的版本选择是整个项目里最值得重视的决定之一,因为这个坑直接决定你后面所有依赖是否好找。
SpringBoot 3.x发布之后,确实带来了很多新特性(Jakarta EE、更好响应式支持),但它有一个硬门槛:JDK版本必须17或更高。很多学校的毕设环境、实验室服务器、生产机器还停留在JDK8,部分老师对高版本JDK也很谨慎。如果用了SpringBoot 3.x,你的MyBatis、PageHelper、MinIO的SDK可能都要升级,Maven仓库里可能还会混入不兼容的版本,排查起来很痛苦。
我当时给师弟的建议是:用SpringBoot 2.7.18——这是2.x系列最后一个稳定版本,JDK8和JDK11都兼容,网上教程和依赖资料最全,跟主流毕设项目的匹配度也最高。实际开发中果然没让我失望,几乎所有第三方库都能直接支持。
2.3 持久层:为什么用MyBatis Plus而不是原生MyBatis
数据库操作我选了MyBatis Plus。这不是为了偷懒,而是因为它能让代码更清晰、维护成本更低。
原生MyBatis需要你为每一条SQL写Mapper接口和XML映射文件。用户表、老师表、订单表、评价表的基础增删改查,写起来极其无聊。MyBatis Plus内置了通用IService、BaseMapper,单表CRUD直接调用自带方法,不用写SQL。它还提供了分页插件(PaginationInnerInterceptor),后端分页查询非常方便;还有一个条件构造器QueryWrapper,动态查询条件写起来比拼字符串清爽多了。
当然,复杂查询(多表关联、统计报表)还是要自己写SQL。MyBatis Plus原生支持自定义Mapper方法,你可以在XML里面写关联查询,互不冲突。
2.4 缓存与中间件:Redis、MinIO、ActiveMQ的选型思考
项目里我用到了Redis、MinIO,还曾经考虑过ActiveMQ,这里说说我的考量。
Redis在这个项目里至少承担三个任务:缓存热点数据(比如科目列表、首页推荐老师)、存储短信验证码或者图形验证码(设置过期时间)、分布式锁(防止订单重复提交)。SpringBoot整合Redis很简单,引入spring-boot-starter-data-redis,配置连接参数,直接用RedisTemplate<String, Object>或者StringRedisTemplate。
MinIO是对象存储系统,负责保存用户头像、证件图片、老师资格证明等文件。为什么不用服务器本地目录存文件?因为本地文件系统在后续扩展、备份、迁移方面都是问题,而且和应用进程抢占磁盘IO。MinIO是开源软件,可以用Docker快速部署一套,接口跟AWS S3兼容,SpringBoot接入成本很低。关于MinIO整合的具体细节,后面单独拉一章讲。
ActiveMQ——我在选型时纠结了一下。热搜词里也常出现“SpringBoot整合ActiveMQ”,看起来是个加分项。但冷静分析:校园家教平台里“订单创建后发通知给老师”这种场景,用简单的异步任务或者Spring事件监听就够了;若引入消息中间件,就要额外维护一套ActiveMQ服务,还得处理消息持久化、重试、死信队列等一堆问题。对一个毕设项目而言,复杂度会明显超标。所以我最后的结论是:这个项目不引入ActiveMQ,如果后续需要,再单独集成也不迟。
3. 数据库设计:一张好表能省一半代码
3.1 用户与角色设计
用户表是整个系统的核心,所有业务都围绕用户展开。我的设计如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| username | varchar(50) | 登录账号,唯一 |
| password | varchar(100) | BCrypt加密后的密码 |
| phone | varchar(20) | 手机号 |
| nickname | varchar(50) | 昵称 |
| avatar | varchar(200) | 头像的访问URL或MinIO对象名 |
| role | tinyint | 0-学生/家长,1-家教老师,2-管理员 |
| status | tinyint | 0-正常,1-禁用 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间,MyBatis Plus中可以用@TableField(fill = FieldFill.INSERT_UPDATE)自动填充 |
角色字段我直接用tinyint存,不在用户表里纠结合RBAC那种复杂模型。校园家教平台的权限只有三级,完全够用。后续如果要更细的权限,可以再扩展角色表、菜单表,但那是另外一套复杂体系了,不建议在毕设阶段引入。
注意:密码必须加密存储,我这里选BCrypt。Spring Security中自带BCryptPasswordEncoder,也可以单独引入spring-security-crypto,它不依赖完整Security框架,比较轻量。
3.2 家教老师资料与认证
用户表只存通用信息,家教老师的专属资料要拆到独立表里,这就是典型的“垂直拆分”思想。
tutor_info表核心字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 关联用户表 |
| school | varchar(100) | 所在学校 |
| major | varchar(50) | 专业 |
| grade | varchar(50) | 大几/研几 |
| subject | varchar(50) | 主要教学科目 |
| teach_grade | varchar(50) | 能辅导的年级(如小学、初中、高中) |
| introduction | text | 个人简介/执教经验 |
| price | decimal(10,2) | 期望时薪 |
| verify_status | tinyint | 0-待审核,1-审核通过,2-审核驳回 |
| verify_remark | varchar(255) | 审核意见(驳回时填写) |
| verify_time | datetime | 审核时间 |
| id_card_url | varchar(200) | 证件照片(MinIO对象名) |
| student_card_url | varchar(200) | 学生证照片 |
这里最关键的是verify_status字段。老师填完资料、上传证件之后,管理员必须审核。审核通过的老师才能出现在前端检索列表里,审核驳回的要能修改资料重新提交。这个流程在开发时容易漏,导致所有人都能在列表里看到未认证老师,那就乱套了。
3.3 订单与评价的关系
订单表是交易关系的核心,我把状态机和关键金额字段都放在一张表里:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 订单编号(唯一) |
| student_id | bigint | 学生/家长用户ID |
| tutor_id | bigint | 家教老师用户ID |
| subject | varchar(50) | 辅导科目 |
| address | varchar(255) | 上课地点(可协商) |
| start_time | datetime | 预计开始时间 |
| end_time | datetime | 预计结束时间 |
| price | decimal(10,2) | 当时约定的时薪 |
| total_amount | decimal(10,2) | 总金额(按课时算) |
| status | tinyint | 0-待接单,1-已接单,2-授课中,3-已完成,4-已取消 |
| cancel_reason | varchar(255) | 取消原因 |
| create_time | datetime | 下单时间 |
订单状态流转如下:
0(待接单) -> 1(已接单) -> 2(授课中) -> 3(已完成)
任意状态(除非已完成)都可以 -> 4(已取消),取消时需要记录原因。
这里要注意,订单中有个order_no,虽然表里自增id也能唯一标识订单,但展示给用户时用自增id会泄露平台订单量,所以最好生成一个业务订单号,比如“时间戳+随机数字”,或者用雪花算法生成。
评价表和订单是一对一关系。每个订单完成后,学生可以对老师打分评价。评价表字段比较简单:id、order_id、tutor_id、student_id、score(1~5星)、content、create_time。核心约束是:一个订单只能评价一次,数据库里给order_id加唯一索引即可。
3.4 索引与状态机设计总结
数据库设计阶段最容易忽略索引,但项目数据量一大,慢查询立刻暴露。我在这些地方加索引:
- 用户表的
username、phone:唯一索引 - 家教老师表的
user_id:普通索引;subject+city作为组合查询条件也建议加索引,不过城市信息我放在用户表的address字段里,实际查询多用school、subject - 订单表的
student_id、tutor_id:普通索引;order_no:唯一索引 - 评价表的
tutor_id、order_id:索引
状态机这种东西,在代码里用常量类定义好,别在业务代码里直接写魔法数字。我习惯写一个OrderStatusEnum,把0/1/2/3/4和中文描述对应起来,这样SDK层不混乱,写出来的判断代码也更可读。
4. 后端核心模块的实现细节:从登录到下单的完整链路
4.1 基于JWT的登录与权限拦截
前后端分离项目里,Session做登录状态有两个麻烦:一是跨域时要处理Cookie的SameSite等问题;二是后端如果做集群部署,Session不能跨节点共享。用JWT可以完美解决这两个问题,它天然无状态,后端不存登录态,只要校验签名即可。
我的做法是这样:
引入jjwt依赖,创建JWT工具类,生成token时放入用户ID、用户名、角色,设置过期时间(一般设2小时)。登录接口验证用户名密码,通过后返回token给前端。前端每次请求在header里加Authorization: Bearer <token>。
后端写一个拦截器AuthInterceptor,在preHandle里解析token。
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String authHeader = request.getHeader("Authorization"); if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); Claims claims = JwtUtil.parseToken(token); if (claims != null) { // 将用户信息放入ThreadLocal,后续Controller直接取 UserContext.setUser(claims); return true; } } response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}"); return false; }同时写一个WebMvcConfigurer,注册拦截器,设置拦截路径/**,并放行登录接口、注册接口、老师列表接口等。
密码加密用BCrypt,在注册时加密入库,登录时用BCryptPasswordEncoder.matches(明文, 密文)校验。明文密码不能进入日志,不能传回前端,这是底线。
4.2 家教检索中的过滤排序与分词可选方案
家教老师列表是学生使用最频繁的页面,需要支持按科目、年级、学校、价格区间筛选,还要能按好评率排序。
最简单的方式是MyBatis Plus的QueryWrapper加like,比如subject字段直接匹配“数学”或“英语”。但如果支持“数学英语”这种关键字组合搜索,简单like就力不从心了。这时候热搜词里提到的HanLP就能派上用场。
HanLP是一个开源中文分词工具。你可以用它对搜索关键字做分词,例如“小学数学英语家教”分出来可能是“小学”、“数学”、“英语”、“家教”,然后拿这些词去数据库里做模糊匹配。但要注意:这会给项目增加不必要的复杂度,你需要在服务里集成HanLP,还得维护分词词典。我在这个项目中并没有默认使用HanLP,而是先做了多字段的like匹配,搜索时把用户输入的关键字拆成几个主要科目词去匹配。真正需要分词时再引入HanLP也不迟。
给同学的建议是:先跑通主流程,再考虑搜索优化。毕设的演示重点是核心业务链路的完整性和代码结构的清晰度,不是搜索引擎性能。
4.3 订单创建与状态机控制的坑
下单接口看起来简单:前端提交tutorId、科目、时间、价格,后端插入一条订单。但有两个坑一定要处理:
重复下单问题。用户连续点击两次“提交订单”,后端如果没做幂等控制,会生成两条一模一样的订单。解决方法是:前端按钮加loading,后端在插入前校验同一位学生是否已有待接单状态且指向同一位老师的订单;更稳妥是在订单表加一个唯一索引(比如student_id + tutor_id + time_slot),但这样会让业务不灵活。我采用的方式是前端生成一个请求唯一IDrequestId,后端用Redis的setIfAbsent做幂等键,5秒内相同请求直接拒绝。
超时未接单的订单处理。老师不是7x24小时在线,订单发出去后可能一直没人接。理想方案是用消息队列的延迟队列,ActiveMQ支持延迟投递,但前面说了这个项目不引MQ。我选择的方式是:在订单创建时写入一个“期望接单截止时间”,用一个定时任务每5分钟扫一遍订单表,把超过截止时间且状态还是0的订单自动改成“已取消”,取消原因写“超时未接单”。简单可靠,代码量不大。
4.4 配置与管理:用ConfigurationProperties代替散落的@Value
SpringBoot的配置管理是个容易写乱的地方。很多人喜欢在类里写@Value("${minio.endpoint}"),用得多了,配置文件里的key和代码里的引用散落得到处都是,改个配置都要全局搜索。
我习惯的做法是:为每一类配置创建配置类,用@ConfigurationProperties(prefix = "minio")把配置项映射成一个POJO,然后在需要使用的地方注入这个POJO。
@Component @ConfigurationProperties(prefix = "minio") @Data public class MinioProperties { private String endpoint; private String accessKey; private String secretKey; private String bucketName; }这样既清晰又安全,开发时IDE还能自动补全。SpringBoot 2.7在spring-boot-configuration-processor的配合下,还能在yaml里给出属性提示。
5. MinIO文件上传的整合过程:头像、资质图片、资料附件
5.1 为什么用MinIO而不是服务器本地路径
先说一个常见的误区:把文件保存在后端项目的resources/static/upload目录下。这样部署时如果打成jar包,运行期往jar包内部目录写文件会极其别扭;即使部署成war包,本地路径没有备份和权限管理,照片泄露风险也大。更关键的是,前后端分离后图片的访问URL如果写成本地绝对路径,前端就没办法用一个统一域名来访问。
MinIO解决的就是这个问题。你部署一套MinIO服务,创建好bucket,后端往MinIO上传文件,拿到一个文件对象名,然后拼接一个URL返回给前端。前端访问时走的全是HTTP,跟静态资源服务器一致。后续如果要做CDN加速、做迁移备份,也是在MinIO层面操作,跟业务代码无关了。
5.2 SpringBoot整合MinIO的关键步骤
先引入依赖:
<dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.5.7</version> </dependency>然后写配置类创建客户端:
@Configuration public class MinioConfig { @Resource private MinioProperties properties; @Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(properties.getEndpoint()) .credentials(properties.getAccessKey(), properties.getSecretKey()) .build(); } }接着封装一个上传Service:
public String upload(MultipartFile file, String folder) throws Exception { String originalFilename = file.getOriginalFilename(); String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); String objectName = folder + "/" + System.currentTimeMillis() + "_" + UUID.randomUUID() + extension; minioClient.putObject( PutObjectArgs.builder() .bucket(properties.getBucketName()) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build() ); return objectName; }上传时我习惯按业务类型分目录,比如avatar/、certificate/、announcement/,这样后续清理或者按类型设置权限都比较方便。文件名用时间戳加UUID,可以避免中文文件名乱码和重名覆盖。
Controller里接收MultipartFile时要注意,上传接口接收参数名一定要跟前端FormData的名称保持一致,我用的是file。
5.3 访问方式:公开读与预签名URL
头像这类不敏感的文件,可以设置bucket策略为公开读。这样前端拿到对象名后,可以拼出固定的URL直接访问:http://{minio_endpoint}/{bucket}/{objectName}。
但身份证、学生证这类敏感资料,绝对不能公开读。我的方案是:资质图片上传后只保存对象名,后端提供接口生成预签名URL返回给管理员或本人查看。
MinIO的SDK带来了一个方法:
public String createPresignedUrl(String objectName, int expiresSeconds) { GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(properties.getBucketName()) .object(objectName) .expiry(expiresSeconds) .build(); return minioClient.getPresignedObjectUrl(args); }先返回一个有时效的URL,浏览器打开后只能看一段时间,过期即失效,这样比永久公开安全得多。
5.4 上传过程中的异常与日志坑
MinIO最容易出现的问题是Endpoint配置不对。如果你用Docker部署MinIO,容器里访问MinIO应该用http://minio:9000,但外部浏览器访问要通过http://localhost:9000或者宿主机IP。很多同学只填一个endpoint,上传倒是成功,但前端从后端拿到的URL是内网地址,浏览器打不开。我的处理方式是:MinIO配置项拆成两个,internal-endpoint用于后端SDK连接,external-endpoint用于生成对外可见的URL。刚接手时这个配置很容易忽略,建议早点拆分。
上传文件时还要检查文件大小和类型。SpringBoot的spring.servlet.multipart.max-file-size默认只有1MB,我改成10MB,同时限制上传文件的扩展名,防止有人上传可执行脚本。
6. 前后端联调与打包部署:Vue如何优雅地住进SpringBoot
6.1 开发环境的代理与跨域处理
前后端分离开发时,前端跑在localhost:5173(Vite),后端跑在localhost:8080,跨域是必然的。
后端开跨域的简单方式是写一个CORS配置类:
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }但我更推荐前端用Vite的代理模式,配置server.proxy把/api开头的请求转发到http://localhost:8080,这样开发时浏览器请求走同源,减少CORS问题,还能躲避某些浏览器对非简单请求的限制。生产环境如果前后端分开部署,再用Nginx反向代理,这个后面会讲。
6.2 Vue打包后放进SpringBoot的静态资源目录
毕设项目最好部署成“一个jar包全搞定”,这样老师演示时不需要额外起前端服务,也方便拷贝到别的机器跑。做法很简单:前端npm run build后,把dist目录下的所有文件复制到后端src/main/resources/static里,再打包SpringBoot即可。
但这里藏了一个大坑:Vue如果用了history模式路由,部署在SpringBoot里刷新页面会404。原因是在history模式下,浏览器请求的是/student/orders这个路径,后端静态资源处理器找不到对应的文件,直接返回404。解决办法是在SpringBoot里写一个控制器,把所有非/api、非静态资源文件的路径都转发到index.html:
@Controller public class ForwardController { @GetMapping(value = {"/{path:[^\\.]*}", "/{path:[^\\.]*}/{subpath:[^\\.]*}"}) public String forward() { return "forward:/index.html"; } }注意这个控制器要放在静态资源处理之后才生效,而且它不能拦截/api接口,所以上面用正则排除了带点的路径。这个坑我刚开始也踩了,后来补了一个这个配置才算治本。
6.3 Docker Compose编排MySQL、Redis、MinIO和SpringBoot应用
部署阶段我选择了Docker Compose,一台Linux服务器上就能把全套中间件和应用跑起来。
先写一个docker-compose.yml:
version: '3.8' services: mysql: image: mysql:8.0 container_name: tutor-mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: tutor_campus ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql redis: image: redis:7-alpine container_name: tutor-redis ports: - "6379:6379" minio: image: minio/minio:latest container_name: tutor-minio environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin123 command: server /data ports: - "9000:9000" - "9001:9001" volumes: - minio-data:/data app: build: . container_name: tutor-app depends_on: - mysql - redis - minio ports: - "8080:8080" environment: SPRING_PROFILES_ACTIVE: prod DB_HOST: mysql REDIS_HOST: redis MINIO_ENDPOINT: http://minio:9000这里特别说明一下:SpringBoot应用里配置MySQL的地址不能写localhost,因为在Docker Compose网络中,应用容器要访问MySQL容器得用服务名mysql,同理Redis是redis、MinIO是minio。如果你在容器里还写localhost,连的其实是应用容器自己,肯定报Connection refused。
6.4 环境变量与配置分离:多Profile管理
配置写死是部署的大忌。我习惯在项目里建三个配置文件:
application.yml:公共配置,比如应用名、MyBatis Plus配置application-dev.yml:开发环境,localhost连接、日志debugapplication-prod.yml:生产环境,使用Docker Compose传入的环境变量
prod配置中可以使用${DB_HOST:localhost}这种占位符,默认值设为localhost,本机跑就用默认值,容器跑就注入环境变量。这样同一套代码在不同环境都能跑,不用改代码。
加上SPRING_PROFILES_ACTIVE=prod环境变量,启动时自动加载application-prod.yml。
7. 我在这套项目里踩过的坑:排错链路和解决办法
7.1 现象:SpringBoot版本太高导致MyBatis Plus自动配置失效
师弟一开始用了SpringBoot 3.2.1,结果MyBatis Plus官方当时还没完全适配,加上打包时总报ClassNotFoundException。排查时我先看了依赖树,发现mybatis-plus-boot-starter被解析到旧版本,而且它内部的mybatis-spring和新SpringBoot版本不兼容。
当时的排查链是这样的:
- 查看Maven依赖树:
mvn dependency:tree -Dincludes=com.baomidou - 发现
mybatis-plus-boot-starter版本停留在3.5.3 ,但SpringBoot 3.2需要3.5.4以上的适配版本 - 去MyBatis Plus官网查Release Notes,确认支持SpringBoot 3.x的版本是3.5.4+
- 用
mvn clean package重新打包,启动后仍然报错 - 干脆降级SpringBoot到2.7.18,并同步调整MyBatis Plus版本为3.5.3,问题消失
这个排查过程给我们的启发是:不要把版本升级想得理所当然,尤其是第三方starter的适配往往滞后。做毕设时优先选成熟的SpringBoot 2.7组合,不要赶潮流。如果你的课题明确要求SpringBoot 3.x,那就先确认所有依赖都有适配版本再做。
7.2 现象:MinIO的Endpoint在不同场景下访问不通
前面提到过,同一个MinIO地址在服务器内部访问和外部浏览器访问是不同的。还有另一个坑:如果MinIO启用了TLS或者配置了域名反代,getPresignedObjectUrl生成的URL可能不是你期望的那个对外域名。因为MinIO生成的预签名URL默认使用的是你SDK连接时的Endpoint。
我的处理办法是在生成预签名URL时,不直接使用SDK默认规则,而是用自定义域名拼:
String url = "http://你的域名/" + objectName + "?X-Amz-Algorithm=...";不过这样手动签名比较麻烦。更简单的方案是:在MinIO的config里设置MINIO_SERVER_URL或通过反代重写URL。开发环境我直接让MinIO对外暴露9000端口,用宿主机IP加端口拼地址。生产环境才考虑域名和HTTPS,毕设阶段不推荐加大难度。
7.3 现象:Vue打包后刷新页面404
这个刚才提过,我再补充一下排查过程。师弟第一次把dist文件放进SpringBoot,启动后访问首页没问题,但点进订单详情页后刷新,空白且浏览器控制台报404。
排查步骤:
- 确认使用的是Vue Router的
createWebHistory(),这个模式需要服务端配合 - 尝试改用
createWebHashHistory(),刷新不再404,但URL上多了#/,不好看 - 如果坚持用history模式,必须在后端加路由回退
最终我采用的做法是新增一个WebMvcConfigurer,把不带点的路径全部转发到index.html。注意顺序:要确保静态资源目录里已有的资源(如/assets/index.js)能正常访问,而不是被转发规则拦截。办法是只对没有.的路径做转发,也就是上面的正则方案。
7.4 现象:金仓数据库读写分离配置为什么比想象中复杂
热搜词里出现“SpringBoot金仓读写分离配置”,这里顺便说一下。有些学校要求国产化数据库,比如金仓KingbaseES。这个数据库兼容PostgreSQL模式,所以驱动包是kingbase8.jar,数据源配置里driver-class-name要写成com.kingbase8.Driver,方言用KingsbaseDialect。
读写分离更是另一套逻辑,通常需要借助中间件(如ShardingSphere)或者在数据源层配置动态数据源。这个复杂度对于校园家教平台来说严重超标,如果没有硬性要求,建议不要主动踩坑。如果学校确实要求国产数据库,优先考虑金仓的单机模式,读写分离放一边,先把功能跑通。
7.5 现象:ActiveMQ加了依赖但根本用不上
有段时间我看项目里塞了一堆ActiveMQ的配置类、监听器,但只在“老师接单通知”那里简单用了一下。结果因为ActiveMQ没启动,整个应用启动报JMSConnection异常,后来我们把ActiveMQ相关代码全部移除,改用Spring自带的ApplicationEventPublisher发布订单创建事件,再写监听器异步处理,发现完全能满足需求。
这里我的观点是:技术选型要控制复杂度。中间件是给真正高并发、需要削峰填谷的场景准备的,校园家教平台这个量级用不上。如果你在简历上写“整合ActiveMQ”,面试官问你怎么保证消息不丢、怎么处理重复消费,你得能真的答上来。不确定能不能讲清楚的东西,不如不写。
7.6 关于SpringBoot自动装配原理的简单理解
SpringBoot为什么能“零配置”跑起来?因为@SpringBootApplication注入了@EnableAutoConfiguration,框架启动时通过spring.factories或者AutoConfiguration.imports文件加载一批自动配置类,每个配置类上用@ConditionalOnClass、@ConditionalOnProperty等条件注解决定是否生效。
比如启动Redis的自动配置,RedisAutoConfiguration上就有@ConditionalOnClass(RedisOperations.class),如果你没引入Redis相关依赖,这个自动配置类就不会加载。这个机制是理解“为什么加了starter就有功能”的关键。做项目时如果发现某个自动配置没生效,第一时间要检查依赖是否引入、条件是否满足,而不是盲目改代码。
写在最后:一些做这类项目的心得
这个校园家教信息平台从零到一做完,我的体会是:技术选型最怕“贪多求全”,业务设计最怕“只懂CRUD”。把SpringBoot、MyBatis Plus、Redis、MinIO这几样吃透,足够撑起一个体验完整的系统。你可以在简历里自信地写“熟悉SpringBoot自动装配原理、熟悉MyBatis Plus常用流程、掌握MinIO对象存储整合、掌握Docker Compose部署”,这些表述里涉及的每一条,都是你能在这个项目里讲清楚细节的。
如果再让我做一次类似项目,我会把更多精力放在搜索匹配和订单状态流转上,因为那才是家教平台真正核心的差异化部分,而不是文件上传和页面长得好看。另外也会从一开始就写好部署脚本和启动文档,不然半个月后回头再看,连自己都得猜一遍配置的含义。
希望这篇实操笔记能帮到正在做SpringBoot毕设或者想上手全栈项目的同学。我踩过的版本、配置、部署这几点坑,都在上面写得足够具体了,如果你照着做还是遇到问题,优先看启动日志的第一屏异常,然后按“依赖对不对、配置串没串、网络通不通”这个顺序排查,大部分问题都能定位。