直接开写吧。这篇就从毕设项目的角度把基于SSMVue的橘子网上家教平台从选题、建表、编码到写论文、过答辩的全链路经验捋一遍。1. 为什么家教平台这类选题是毕设里少有的稳妥牌每年做计算机毕业设计最怕的就是题目给自己挖坑。纯管理系统太单薄答辩时被问两句底层逻辑就露馅算法类又容易陷进数学细节写代码的时间还没推公式的时间长。相比之下SSMVue的网上家教平台属于典型的业务型项目它真正的价值不在于某个技术点有多深而在于把一个线上线下结合的业务场景用一套完整的Web系统表达出来——有用户、有商品家教服务、有交易预约下单、有评价整个链路是闭合的。这个题目听起来像个APP创业项目但定位成毕业设计其实非常合适。它的核心业务主线和电商平台高度相似家长找老师、老师接单、双方约时间、上完课互相评价。你不需要真去实现视频通话、在线支付这些烧钱又容易引入风控问题的功能只需要把信息撮合订单管理评价体系这三件事做扎实就能覆盖题目里网上平台的核心含义。答辩老师最愿意看到的就是这种麻雀虽小五脏俱全的完整闭环他能顺着角色问你权限、问状态流转、问数据表之间的关系你也能答得上来。我实际带过不少类似项目坦白讲学弟学妹们翻车最多的反而不是写代码而是前期把需求想得太大。有的同学一上来就要做在线课堂、课件共享、小时级排课算法结果数据库画了三十多张表写到第三个模块就开始摆烂。如果你选的也是家教平台这类题目我的建议非常直接以订单为核心往前管好信息发布往后管好评价结算其他都是锦上添花。围绕这个核心整个项目会自动拆成几个天然模块——面向老师的课程/简历管理、面向家长/学生的搜索与预约、面向管理员的后台审核与数据统计。这三个角色划分清楚之后你的SSM后端和Vue前端做出来都会显得非常有条理。2. SSM三件套和Vue的搭配逻辑以及版本怎么选不踩坑2.1 为什么是SSM而不是Spring Boot先说一句容易挨喷但很现实的话毕业设计用SSM不代表SSM是当前最佳实践而是因为它是最适合展示基础功的经典组合。Spring管理Bean、SpringMVC处理请求分发、MyBatis做持久层映射这三层结构条理清晰论文里框架原理那一章可以写得很扎实答辩被问到Spring的IoC是什么MyBatis怎么解决JDBC硬编码问题这类问题时也都有标准答案可讲。我用Spring Boot当然更省事自动配置能省掉一大半XML配置。但毕设的核心目的是让老师看到你理解了框架而不是使用了框架。SSM的XML配置虽然繁琐但每一行都是有教学意义的比如applicationContext.xml里配数据源、配事务管理器、配Mapper扫描写一遍下来你对Spring容器的工作流程会有非常直观的体感。当然如果你导师明确允许Spring Boot也可以把Spring Boot理解成SSM的升级封装换上去之后Controller、Service、Mapper的分层结构完全不用变只是配置从XML挪到了注解和yml里。这也是接下来所有代码示例仍然适用的原因。2.2 版本组合怎么选才不折腾版本匹配是SSM项目里最容易让人崩溃的问题很多同学卡在启动报错一看全是版本冲突。我建议直接用下面这套组合这是我在实际搭建中反复验证过、出问题最少的一套组件版本说明JDK1.8虽然现在新版本到17、21了但SSM老牌生态还是1.8最稳Maven3.6.3别用最新的3.9部分老私服和插件会有兼容问题Spring5.1.8.RELEASE5.x最后一个稳定序列兼容性很好MyBatis3.5.3搭配mybatis-spring 2.0.3MySQL5.7不建议上8.0驱动和时区问题容易折磨新手Vue2.6.x配Element UI 2.x生态成熟资料多前端构建vue-cli 4.x或者直接用Vite但vue-cli对新手更友好这里特别说一下为什么MySQL推荐5.7。并非8.0不好而是SSM老项目里常用的useSSLfalse等连接参数在8.0驱动下包名变了com.mysql.jdbc.Driver要换成com.mysql.cj.jdbc.Driver有些同学忘了改直接报ClassNotFoundException。与其在环境上浪费时间不如用5.7把精力放到业务实现上。当然你机器上已经装了8.0也不用卸载把驱动改成com.mysql.cj.jdbc.Driver并在连接串后面加上?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai基本也能跑通。2.3 SSM分层之后的代码组织项目搭好之后后端包结构我建议按功能竖切而不是按技术层横切。很多教程会教你建controller、service、dao三个包但实际业务里更推荐带业务名的竖切包比如controller下细分成UserController、CourseController、OrderControllerservice下面对应UserServiceImpl之类。竖切的好处是每个功能模块的文件集中在一起改某个业务时不会在十几个包之间来回跳。前端Vue这边我习惯把src下面分成api、views、components、router、store五个目录。一个常见的误解是觉得views就是写页面其实页面里的核心逻辑应该放api目录下的JS文件里每个后端接口对应一个前端api方法这样以后后端接口路径变了只需要改一个文件。3. 数据库设计的六张核心表以及表结构怎么定才不被答辩问倒3.1 从业务链路推导表结构网上家教平台听上去功能很多但落到数据库层面最核心的就六张表。你可以把整个业务抽象成一句话用户发布家教信息家长浏览后发起预约老师确认后形成订单完成之后双方评价。围绕这句话表结构就很自然地推出来了表名作用关键字段t_user存储家长、老师、管理员三类账号id,username,password,role,real_name,phonet_teacher_info老师的详细资料头像、教龄、擅长科目id,user_id,subject,grade,experience,introducet_course老师发布的课程/家教信息id,teacher_id,subject,price,timeslot,statust_order预约订单主表id,course_id,parent_id,teacher_id,status,create_timet_evaluation课程完成后的评价id,order_id,from_user,to_user,content,scoret_feedback用户反馈或管理员处理记录id,user_id,content,reply,status为什么需要t_teacher_info和t_course两张分开而不是合在一起因为一个老师可以发布多条课程信息比如同时教小学数学和初中英语如果合并在一张表里字段就会大量冗余。分开之后t_user管账号登录t_teacher_info管老师个人展示页t_course管具体可预约的家教服务层次非常清晰。这套表结构最大的好处是可解释。答辩时老师顺着订单表问下去你能讲清楚每一个外键来源比如t_order里的course_id从哪来、parent_id为什么指向t_user.id而不是单独建家长表——因为家长本质上也是用户只是role字段不同。如果你为了省事给每个角色单独建表反而会落进表和表之间冗余太多的坑里。3.2 状态字段怎么设计是订单表的重头戏订单表的status字段是整个系统里最值得花心思设计的地方。很多人写订单功能时用简单的字符串存状态比如已预约已完成,虽然能跑但答辩时容易被问到状态之间怎么流转如果家长取消预约怎么办这类问题。我推荐用数字枚举来存状态再配合一张状态说明表或者在代码里定义常量。比如订单状态可以定义为状态值含义流转方向0待确认家长预约后等待老师确认1已确认老师同意接单等待上课2授课中可以选做需要签到记录时用3已完成课程结束可发起评价4已取消家长或老师主动取消5已关闭超时未确认系统自动关闭做这个设计的核心思路是让状态机可追溯。每次状态变更都在t_order表里更新status字段有精力的话还可以加一张t_order_log表记录谁在什么时间把状态改成了什么。这在答辩时是一个很大的加分点因为它展示了你对真实业务复杂度的理解而不只是完成了一个简单的增删改查。另外price字段一定要用decimal(10,2)而不是float或double。很多新手在这里踩坑用浮点型存金额算总价的时候出现9.999999这样的值。凡是涉及钱的字段都要用精确的定点数。3.3 评价表和课程表之间为什么要冗余一个订单号t_evaluation表里有个容易被忽略的细节除了记录评价人和被评价人之外最好把order_id也存进去并对它设置唯一索引。这样设计的效果是——一个订单只能产生一条评价从数据库层面挡住了重复评价的问题。如果不加这个唯一索引你就需要在Service层写大量判断逻辑先查订单状态是不是已完成、再查有没有评过。这些逻辑当然要写但数据库约束作为底线会让代码更健壮。我在项目里就是先判断再插入但即便代码漏了数据库也会挡住重复数据这在答辩时是非常好的健壮性设计论据。课程表t_course里的status字段也同样重要。老师的家教信息挂了之后需要审核审核通过才能被家长搜索到这又是一个独立的状态字段。可以把审核状态分成0待审核、1已通过、2已拒绝、3已下架。这里多花十分钟设计状态后面写后台管理模块时会轻松很多。4. 核心功能模块的代码实现顺序和最容易暴露问题的地方4.1 先做权限登录还是先做业务功能我的建议是先做登录和权限再做具体业务这和很多教程的课程顺序正好相反。原因很简单没有登录态之前你每个接口都要临时加参数传用户信息做完登录之后通过Token或Session拿当前用户信息就成了固定套路后面的所有代码都会更干净。SSM项目里我推荐用最经典的Session方案不需要额外引JWT依赖。登录成功后把用户对象放进Session拦截器里判断Session是否为空没有就重定向到登录页再加一个简单的角色判断就够了。拦截器配置的代码大致长这样public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object user session.getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login); return false; } return true; } }然后在SpringMVC配置文件里注册拦截路径mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/api/register/ mvc:exclude-mapping path/static/**/ bean classcom.example.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors前端Vue侧用vue-router的全局前置守卫做配合每次路由跳转前先发一个请求验证登录态或者简单点直接检查本地存储的Token。这里要注意的是前后端必须统一约定好未登录的返回值格式比如后端的表示未登录时返回的状态码为401前端Axios响应拦截器看到401就统一跳转登录页而不是在几十个页面的then回调里各写一遍判断。常见的失败案例是后端拦截器拦截了所有请求但前端没有统一处理401导致用户登录过期后点击任何按钮都没反应也不跳转。这种问题我看过太多次了就属于代码看着都写了但整体跑起来体验很差的典型。4.2 家教检索Vue前端搜索 MyBatis动态SQL平台最核心的浏览功能就是按科目、按年级、按价格范围筛选家教老师。这个功能的技术含量不高但非常考察MyBatis动态SQL的掌握程度。前端页面用Element UI的表单组件收集筛选条件点击查询后通过Axios把条件对象传给后端后端Controller接收后传给Mapper接口。Mapper.xml里的SQL需要写成where配合if标签的形式select idsearchCourses resultTypeCourseVO SELECT c.*, t.real_name, t.subject, t.experience FROM t_course c LEFT JOIN t_teacher_info t ON c.teacher_id t.id where if testsubject ! null and subject ! AND c.subject #{subject} /if if testgrade ! null and grade ! AND c.grade #{grade} /if if testmaxPrice ! null AND c.price lt; #{maxPrice} /if if teststatus ! null AND c.status #{status} /if /where ORDER BY c.create_time DESC /select这段SQL看起来很基础但里面有三个容易翻车的细节。第一是if test里判断空值必须同时判断null和空字符串不然用户不填条件时也会拼接SQL导致查不出来第二是price比较时小于号在XML里要转义成lt;不转义直接写会报XML解析错误第三是查询结果的映射如果字段名和下划线命名不一致一定要配好resultMap或者开启驼峰映射mapUnderscoreToCamelCase。更推荐的做法是单独建一个CourseVO类来承接查询结果而不是直接复用实体类。因为查询列表页展示的字段往往比表字段多比如需要老师姓名、老师头像、平均评分这些关联数据实体类塞不下VO是更合理的选择。4.3 预约下单事务和并发控制是答辩高频问题预约是整个项目里逻辑最重的模块它的完整流程是家长看到课程详情页点击立即预约→ 后端校验课程状态是否可预约 → 生成订单记录 → 订单状态变为待确认 → 老师端收到预约提醒。第一步的后端校验里隐藏着一个并发问题两个家长同时点击同一个课程的最后可预约时段如果后端不做任何防护两个请求可能都通过校验生成两条订单超卖就出现了。解决这个问题最直接的手段是在t_course表里加一个stock字段可预约名额下单时用带条件的更新语句// 返回1说明更新成功说明有人抢到了名额 int rows courseMapper.decreaseStock(courseId); if (rows 0) { throw new BusinessException(该课程已被预约完毕); }update iddecreaseStock UPDATE t_course SET stock stock - 1 WHERE id #{courseId} AND stock 0 /update这里的关键点在于不只是查询出来判断再更新而是把判断和扣减放到一条SQL里通过影响行数来判断是否成功从根上避免了先查后改带来的并发窗口期。项目里如果没有这个概念接口写好就完事但答辩时被问到多个用户同时预约怎么办很容易答不上来。扣减库存之后就要生成订单了生成订单和扣减库存这两个操作必须在同一个事务里。在SSM里你只需要在Service实现类的方法上加上Transactional注解它就会自动开启事务中途任何一步抛异常前面已经执行的SQL都会回滚。这个注解的理解深度也是答辩老师特别爱考察的点你可以顺手查一下Transactional默认的rollbackFor是哪几种异常类型提前把这个知识点吃透。4.4 Element UI表单校验和Vue组件拆分前端页面比较多的情况下如果所有代码都堆在views里后期维护会非常痛苦。我的习惯是所有用到的UI组件比如课程卡片、评价列表、老师信息栏都拆到components目录下页面路由只负责组合这些组件。表单校验是前端看起来简单但特别容易做漏的地方。比如家长注册时要填手机号Element UI里用rules属性加校验规则但很多人只写了required: true没有写格式校验。手机号这种固定格式的字段建议用pattern正则做二次校验rules: { phone: [ { required: true, message: 请输入手机号, trigger: blur }, { pattern: /^1[3-9]\d{9}$/, message: 手机号格式不正确, trigger: blur } ] }后端Controller也要做同样的参数校验绝不能因为前端已经校验了就省略后端。这个说法虽然听着有点啰嗦但很多同学的代码真的是前端卡了一道、后端完全不设防用Postman直接调接口就能插非法数据。至少要保证非空、长度、格式三层检查后端的这次校验也算你在论文里能写进系统安全性设计的一笔素材。5. 课堂上不会教的前后端联调技巧和数据脱敏防坑5.1 接口返回格式建议全局统一项目里最让我头疼的不是某个接口写不出来而是返回格式不统一导致前端到处做判断。后端有些人返回Map有些人直接返回实体类有些人返回String前端Axios里就充满了res.data.data.data这种三层套娃代码。建议从一开始就封装一个统一的返回类Resultpublic class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message success; result.data data; return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.code code; result.message message; return result; } }这样Controller里所有接口统一返回Result前端在Axios拦截器里统一处理service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { Message.error(res.message); return Promise.reject(new Error(res.message)); } return res; }, error { Message.error(网络异常请稍后重试); return Promise.reject(error); } );这个习惯会在联调阶段省下大量时间也是我特别想提醒初学者的一件事先定接口规范再写代码比写完接口再统一封装要轻松太多。毕设项目虽然是自己一个人写但这套规范放到论文里的系统设计章节同样能加分。5.2 密码不能明文存储很多毕设项目的用户表里密码就是明文躺在数据库里说句不好听的一眼看到就会让答辩印象分下降。正确做法是使用MD5加盐或者BCrypt做哈希存储。SSM项目里我比较推荐Shiro自带的SimpleHash工具类先做一个可用的处理String salt random_salt; String hashedPassword new SimpleHash(MD5, rawPassword, salt, 2).toHex();登录时把用户输入的密码拼上同样的盐做MD5再和数据库里存的值比较。答辩时被问密码怎么保证安全就能从不存明文 加盐 多次哈希三个层面去解释。如果想让方案更硬核一点也可以提一下加盐哈希是单向的即使数据库泄露拿到哈希值也无法直接逆推出原密码。5.3 联调阶段最常用的调试手段前后端分离的项目联调问题是老大难。最常见的错误是前端页面访问/api/xxx返回404但后端用Postman测试接口又是通的。这绝大多数是跨域配置的问题。SSM后端解决跨域最简单的办法是写一个CORS过滤器public class CorsFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletResponse response (HttpServletResponse) res; HttpServletRequest request (HttpServletRequest) req; response.setHeader(Access-Control-Allow-Origin, request.getHeader(Origin)); response.setHeader(Access-Control-Allow-Methods, GET, POST, PUT, DELETE, OPTIONS); response.setHeader(Access-Control-Allow-Headers, Content-Type, Authorization); response.setHeader(Access-Control-Allow-Credentials, true); chain.doFilter(req, res); } }Access-Control-Allow-Origin这里不要简单粗暴地写*因为后面一旦需要携带Cookie或Session就必须设置成具体的来源而且要配合Access-Control-Allow-Credentials为true。从开发第一天就按这个思路配后面就不会反复改。另外一个调试神器是后端启动类里加一个全局异常处理器用ControllerAdvice把未捕获异常统一包装成Result.error返回。这样前端看到的是友好的提示信息而不是一大堆带堆栈的报错页自己排查问题也方便得多。这个处理器看起来只是个小工具类但对于答辩演示时的稳定性非常有价值——最怕的就是现场演示时突然白屏全局异常兜底至少能把错误转化成提示。6. LW文档和毕业论文的写作策略关键是把做了什么讲成为什么这么做6.1 论文目录怎么架构更像一个真实项目很多同学拿到题目之后第一件事就是上网找模板然后照着模板搭目录结果写出来的论文千篇一律老师看一眼目录就觉得没新意。论文结构当然要符合基本规范但章节内部的设计要围绕你自己的功能亮点来写。比如核心章节我推荐按系统总体设计 → 数据库设计 → 各功能模块详细设计与实现 → 系统测试的顺序来。容易被人忽略的是数据库设计这一章很多论文只贴建表语句却不解释为什么这样设计。你在3.1节里设计订单状态机、在3.2节里对评价表加唯一索引这些都是可以在论文里大段展开的内容——把表的关系画成实体关系图把每个状态的跳转逻辑用文字讲清楚这篇论文的深度立刻就不一样了。6.2 需求分析写不好后面全白搭需求分析是论文第一章的重头戏但也是很多人写得最敷衍的部分。常见的写法是本系统分为前台和后台前台供普通用户使用后台供管理员使用然后就没有然后了。真正好的需求分析应该回答三个问题谁会使用这个系统他们各自想完成什么任务这些任务之间有什么顺序和约束就以橘子家教平台为例如果你是家长你要能注册登录、搜索老师、查看老师主页、发起预约、取消预约、对完成的课程评价如果你是老师你要能完善个人资料、发布课程、确认或拒绝预约、查看评价如果你是管理员你要能审核老师资料、审核课程信息、查看订单统计、处理用户反馈。把这些角色任务写清楚了后面所有功能模块的设计都是有源之水而不是凭空想象出来的功能清单。很多同学的论文被批没有工作量根源不在于代码写得少而在于需求分析阶段就没有把功能边界划清楚导致后面章节写起来东拼西凑。我建议你写需求分析时花一天时间列出每个角色的用例图这个图放进论文里非常直观老师看到的第一印象就是这个学生是认真做过前期调研的。6.3 测试章节不要只写功能正常论文里的测试章节普遍写得像走过场比如点击登录按钮跳转成功测试通过这种描述不会让任何人觉得你有工程素养。有分量的测试部分要围绕边界条件和异常场景来写。我当时带项目时会给学生列一张测试用例表专门覆盖这些容易出问题的地方测试项操作步骤预期结果实际结果重复预约拦截家长A已预约课程X再次点击预约提示请勿重复预约与预期一致库存为0时预约课程X显示剩余0家长点击预约提示课程已约满与预期一致未登录访问个人中心直接输入后台页面网址跳转登录页与预期一致评价分数越界提交评分为6提示评分范围1-5与预期一致表里的每一行都能呼应你在代码里做的某个防护操作。这样测试章节就不再是凑字数的流水账而是对系统健壮性的系统论证。把上面这些用例加起来再加上性能方面的基本描述比如页面响应时间在可接受范围内论文的测试章节就能写得非常扎实。6.4 把系统演示录屏存好能让你少熬几个夜这里提一个很实用但常被忽略的建议每完成一个阶段功能就顺手用录屏工具把操作流程录下来整理成一个短视频。尤其是家长下单→老师确认→评价这条核心链路一定要完整录一套。很多同学到了答辩前几天才发现某个联调环境起不来了或者数据库服务崩了现场演示只能干讲PPT。有了一份提前录制好的演示视频就算现场环境炸了你也能从容播放并且边播边解释每一步的技术实现。这个习惯不只是为了应对突发状况在做项目报告时也是很好的素材算是我见过性价比最高的一个备考建议。7. 从部署到答辩Tomcat打包、环境还原和几个能救命的兜底方案7.1 Maven打包和部署时的常见弯路后端是SSM项目最终通常以War包形式部署到Tomcat。很多开发时一切正常的项目一打包部署就各种报错最常见的原因是本地依赖没有完整打进去。建议在pom.xml里配好maven-war-plugin打包前先执行clean确认target目录下的War包大于几兆再往Tomcat的webapps里丢。如果本地开发时用的Tomcat自带的和部署环境版本不同还可能遇到Servlet版本相关的报错。这种问题排查起来特别费时所以我通常建议毕设答辩就固定用同一个Tomcat版本尽量8.5系列兼容性和稳定性都兼顾。Vue前端构建之后生成的是dist目录里面有index.html和static资源可以选择放进Tomcat的webapps下作为一个静态站点也可以单独用Nginx托管前端请求时通过代理转发到后端的8080端口。本地演示其实不需要特别规范的部署但那套简单的Nginx配置我建议还是写进论文里哪怕只有一个截图加一段配置说明也能体现部署能力。7.2 答辩常见的十连问提前准备好接招答辩环节老师喜欢问的问题其实高度集中把下面这些问题准备好基本就稳了一半系统的角色权限是如何实现的对应拦截器和Session的角色判断订单状态是如何流转的对应状态机设计两个用户同时预约同一个课程怎么办对应库存扣减SQL密码加密方式是什么对应加盐MD5或BCrypt数据库表为什么这样设计对应主外键关系和order_id唯一索引前端框架为什么选Vue?对应组件化和响应式原理项目中遇到过什么困难怎么解决的——这个问题需要提前准备一个真实发生过的技术问题比如跨域配置、数据库驱动版本冲突、动态SQL拼接Bug讲清楚现象→排查→解决即可。一个小技巧是你自己亲手把项目从零搭一遍尤其是那些踩过的坑比如环境的版本冲突不光是你踩坑的记录也是答辩时最好的素材。因为你踩坑的细节老师一听就知道是你自己做过的而不是抄来的这种细节比任何大而空的我通过本次项目提升了自己都有说服力。7.3 如果时间不够功能优先级怎么砍项目做不完的情况太常见了这个时候最考验的是砍需求的功力。我给的建议是保主干、砍枝节。主干就是登录注册→浏览老师→预约下单→确认订单→完成评价→后台管理这条链路砍掉任何一段系统都不完整。可以砍掉的是消息通知、数据可视化大屏、忘记密码邮箱验证这类辅助功能。如果砍完之后还是紧张那就要在完成度上做一个看起来完整的方案。比如后台统计这块如果做不了ECharts大屏只做一个简单的表格统计页面数据从数据库里查出来展示就行工作量能减少七成但功能闭环依然成立。答辩老师看到的是有统计功能而不是统计图表多好看。8. 几个被问烂了的细节问题以及我在实际项目中总结的教训8.1 修改密码后Session会失效吗系统里有个很容易被忽略的Bug用户修改密码之后Session里存的是旧密码或者旧用户对象下次访问时老Session仍然有效导致改密码形同虚设。处理方式很简单修改密码成功后把当前Session销毁强制用户重新登录。这个小细节虽然代码量很少但在答辩演示时如果你恰好操作到这个功能就能让老师看到你的工程意识。登录逻辑里还有一个小陷阱就是用户输入错误密码后不要直接把用户不存在和密码错误分成两个提示统一返回用户名或密码错误更安全。这既是防御恶意试探的常识也是在论文系统安全设计里可以写的一句话。8.2 图片上传别把文件存进数据库家教平台一定会涉及头像上传、课程图片上传。有些新手喜欢把图片转成Base64字符串存到MySQL的Text字段里这么做前端展示方便但数据库会迅速膨胀备份和迁移都非常痛苦。正确做法是文件落到服务器的某个目录数据库只存相对路径前端通过一个静态资源映射来访问。SSM里配置静态资源映射的方式是mvc:resources mapping/upload/** location/WEB-INF/upload//或者更简单一点在SpringMVC配置里加上mvc:default-servlet-handler/让默认的Servlet处理静态资源。上传文件时不建议直接信任前端传过来的文件名最好用UUID.randomUUID()重新生成文件名防止路径穿越和重名覆盖问题。这个问题虽然小在毕设中也很少被人深挖但真被问到的时候答得上来就是亮点。8.3 时间字段统一用DateTime还是Timestamp表面上看是选数据类型实际上指向的是时区问题的坑。如果数据库和Tomcat部署在不同时区云服务器上尤其容易遇到时间显示会出现八小时偏差。为了避免这种问题我建议所有时间字段统一使用datetime类型并在Java实体类里使用LocalDateTime配合连接串里的serverTimezoneAsia/Shanghai。这样能规避绝大多数时区问题。另一个时间相关的坑是创建时间要由后端生成不要依赖前端传值。前端用户传入的时间是不可信的后端统一用LocalDateTime.now()赋值这里也是一个可以做全局处理的地方比如在MyBatis插入时通过jdbcTypeTIMESTAMP配合数据库的CURRENT_TIMESTAMP默认值自动填充能省不少事。写到这里项目从技术选型到数据库设计、核心编码、论文写法和答辩准备的整套思路就都铺开了。最后再说一句我的个人体会我见过太多人把毕设当成负担但实际上选一个像网上家教平台这样业务完整、技术难度适中的题目反而是系统性补全自己Web开发认知的好机会——SSM框架的分层思想、Vue的组件化、数据库的设计约束、前后端联调的工程化这些东西是学校课程很难一次性串起来的。真正动手从零把它搭完收获的绝对不只是那一纸成绩而是你以后写任何业务系统都会用的底层框架。