ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于SpringBoot的校园家教平台开发实战:从数据库设计到Docker部署

基于SpringBoot的校园家教平台开发实战:从数据库设计到Docker部署 最近有个师弟拿了个毕业设计题目来找我题目是“基于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 CloudSpringBoot现在已经成了Java后端开发的事实标准它最大的贡献就是自动装配和内嵌服务器。以前用Spring MVC搭一个项目要写一堆XML配置文件配置Tomcat、配置数据源、配置事务管理器光这些前置工作就能耗掉一两天。SpringBoot把常规配置做成starter你只需要引入依赖框架自动帮你装配好。有人会问校园家教平台这种项目有必要用Spring Cloud吗我的回答是没必要而且强烈不建议。Spring Cloud是一套微服务解决方案包含注册中心、配置中心、网关、熔断等一堆组件部署成本和学习成本都很高。对一个单体就能搞定的小平台上了Spring Cloud只会让你陷入服务拆分和分布式事务的泥潭。合理的做法是SpringBoot单体应用 合适的模块划分以后真要做大再按业务边界拆微服务也不迟。2.2 版本选择的坑SpringBoot 2.7还是3.xSpringBoot的版本选择是整个项目里最值得重视的决定之一因为这个坑直接决定你后面所有依赖是否好找。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配置连接参数直接用RedisTemplateString, Object或者StringRedisTemplate。MinIO是对象存储系统负责保存用户头像、证件图片、老师资格证明等文件。为什么不用服务器本地目录存文件因为本地文件系统在后续扩展、备份、迁移方面都是问题而且和应用进程抢占磁盘IO。MinIO是开源软件可以用Docker快速部署一套接口跟AWS S3兼容SpringBoot接入成本很低。关于MinIO整合的具体细节后面单独拉一章讲。ActiveMQ——我在选型时纠结了一下。热搜词里也常出现“SpringBoot整合ActiveMQ”看起来是个加分项。但冷静分析校园家教平台里“订单创建后发通知给老师”这种场景用简单的异步任务或者Spring事件监听就够了若引入消息中间件就要额外维护一套ActiveMQ服务还得处理消息持久化、重试、死信队列等一堆问题。对一个毕设项目而言复杂度会明显超标。所以我最后的结论是这个项目不引入ActiveMQ如果后续需要再单独集成也不迟。3. 数据库设计一张好表能省一半代码3.1 用户与角色设计用户表是整个系统的核心所有业务都围绕用户展开。我的设计如下字段类型说明idbigint主键自增usernamevarchar(50)登录账号唯一passwordvarchar(100)BCrypt加密后的密码phonevarchar(20)手机号nicknamevarchar(50)昵称avatarvarchar(200)头像的访问URL或MinIO对象名roletinyint0-学生/家长1-家教老师2-管理员statustinyint0-正常1-禁用create_timedatetime创建时间update_timedatetime更新时间MyBatis Plus中可以用TableField(fill FieldFill.INSERT_UPDATE)自动填充角色字段我直接用tinyint存不在用户表里纠结合RBAC那种复杂模型。校园家教平台的权限只有三级完全够用。后续如果要更细的权限可以再扩展角色表、菜单表但那是另外一套复杂体系了不建议在毕设阶段引入。注意密码必须加密存储我这里选BCrypt。Spring Security中自带BCryptPasswordEncoder也可以单独引入spring-security-crypto它不依赖完整Security框架比较轻量。3.2 家教老师资料与认证用户表只存通用信息家教老师的专属资料要拆到独立表里这就是典型的“垂直拆分”思想。tutor_info表核心字段字段类型说明idbigint主键user_idbigint关联用户表schoolvarchar(100)所在学校majorvarchar(50)专业gradevarchar(50)大几/研几subjectvarchar(50)主要教学科目teach_gradevarchar(50)能辅导的年级如小学、初中、高中introductiontext个人简介/执教经验pricedecimal(10,2)期望时薪verify_statustinyint0-待审核1-审核通过2-审核驳回verify_remarkvarchar(255)审核意见驳回时填写verify_timedatetime审核时间id_card_urlvarchar(200)证件照片MinIO对象名student_card_urlvarchar(200)学生证照片这里最关键的是verify_status字段。老师填完资料、上传证件之后管理员必须审核。审核通过的老师才能出现在前端检索列表里审核驳回的要能修改资料重新提交。这个流程在开发时容易漏导致所有人都能在列表里看到未认证老师那就乱套了。3.3 订单与评价的关系订单表是交易关系的核心我把状态机和关键金额字段都放在一张表里字段类型说明idbigint主键order_novarchar(32)订单编号唯一student_idbigint学生/家长用户IDtutor_idbigint家教老师用户IDsubjectvarchar(50)辅导科目addressvarchar(255)上课地点可协商start_timedatetime预计开始时间end_timedatetime预计结束时间pricedecimal(10,2)当时约定的时薪total_amountdecimal(10,2)总金额按课时算statustinyint0-待接单1-已接单2-授课中3-已完成4-已取消cancel_reasonvarchar(255)取消原因create_timedatetime下单时间订单状态流转如下0待接单 - 1已接单 - 2授课中 - 3已完成任意状态除非已完成都可以 - 4已取消取消时需要记录原因。这里要注意订单中有个order_no虽然表里自增id也能唯一标识订单但展示给用户时用自增id会泄露平台订单量所以最好生成一个业务订单号比如“时间戳随机数字”或者用雪花算法生成。评价表和订单是一对一关系。每个订单完成后学生可以对老师打分评价。评价表字段比较简单id、order_id、tutor_id、student_id、score1~5星、content、create_time。核心约束是一个订单只能评价一次数据库里给order_id加唯一索引即可。3.4 索引与状态机设计总结数据库设计阶段最容易忽略索引但项目数据量一大慢查询立刻暴露。我在这些地方加索引用户表的username、phone唯一索引家教老师表的user_id普通索引subjectcity作为组合查询条件也建议加索引不过城市信息我放在用户表的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;charsetUTF-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代替散落的ValueSpringBoot的配置管理是个容易写乱的地方。很多人喜欢在类里写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 groupIdio.minio/groupId artifactIdminio/artifactId version8.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(); } }接着封装一个上传Servicepublic 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如何优雅地住进SpringBoot6.1 开发环境的代理与跨域处理前后端分离开发时前端跑在localhost:5173Vite后端跑在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.htmlController 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.ymlversion: 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_ACTIVEprod环境变量启动时自动加载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 -Dincludescom.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毕设或者想上手全栈项目的同学。我踩过的版本、配置、部署这几点坑都在上面写得足够具体了如果你照着做还是遇到问题优先看启动日志的第一屏异常然后按“依赖对不对、配置串没串、网络通不通”这个顺序排查大部分问题都能定位。
返回列表