直接开写吧。这篇就从毕设项目的角度,把"基于SSM+Vue的橘子网上家教平台"从选题、建表、编码到写论文、过答辩的全链路经验捋一遍。
1. 为什么"家教平台"这类选题,是毕设里少有的稳妥牌
每年做计算机毕业设计,最怕的就是题目给自己挖坑。纯管理系统太单薄,答辩时被问两句底层逻辑就露馅;算法类又容易陷进数学细节,写代码的时间还没推公式的时间长。相比之下,SSM+Vue的网上家教平台属于典型的"业务型项目",它真正的价值不在于某个技术点有多深,而在于把一个线上线下结合的业务场景用一套完整的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项目里最容易让人崩溃的问题,很多同学卡在启动报错,一看全是版本冲突。我建议直接用下面这套组合,这是我在实际搭建中反复验证过、出问题最少的一套:
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 虽然现在新版本到17、21了,但SSM老牌生态还是1.8最稳 |
| Maven | 3.6.3 | 别用最新的3.9+,部分老私服和插件会有兼容问题 |
| Spring | 5.1.8.RELEASE | 5.x最后一个稳定序列,兼容性很好 |
| MyBatis | 3.5.3 | 搭配mybatis-spring 2.0.3 |
| MySQL | 5.7 | 不建议上8.0,驱动和时区问题容易折磨新手 |
| Vue | 2.6.x | 配Element UI 2.x,生态成熟资料多 |
| 前端构建 | vue-cli 4.x | 或者直接用Vite,但vue-cli对新手更友好 |
这里特别说一下为什么MySQL推荐5.7。并非8.0不好,而是SSM老项目里常用的useSSL=false等连接参数在8.0驱动下包名变了,com.mysql.jdbc.Driver要换成com.mysql.cj.jdbc.Driver,有些同学忘了改,直接报ClassNotFoundException。与其在环境上浪费时间,不如用5.7把精力放到业务实现上。当然你机器上已经装了8.0也不用卸载,把驱动改成com.mysql.cj.jdbc.Driver,并在连接串后面加上?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai,基本也能跑通。
2.3 SSM分层之后的代码组织
项目搭好之后,后端包结构我建议按功能竖切,而不是按技术层横切。很多教程会教你建controller、service、dao三个包,但实际业务里更推荐带业务名的竖切包,比如controller下细分成UserController、CourseController、OrderController,service下面对应UserServiceImpl之类。竖切的好处是每个功能模块的文件集中在一起,改某个业务时不会在十几个包之间来回跳。
前端Vue这边,我习惯把src下面分成api、views、components、router、store五个目录。一个常见的误解是觉得views就是写页面,其实页面里的核心逻辑应该放api目录下的JS文件里,每个后端接口对应一个前端api方法,这样以后后端接口路径变了,只需要改一个文件。
3. 数据库设计的六张核心表,以及表结构怎么定才不被答辩问倒
3.1 从业务链路推导表结构
网上家教平台听上去功能很多,但落到数据库层面,最核心的就六张表。你可以把整个业务抽象成一句话:用户发布家教信息,家长浏览后发起预约,老师确认后形成订单,完成之后双方评价。围绕这句话,表结构就很自然地推出来了:
| 表名 | 作用 | 关键字段 |
|---|---|---|
t_user | 存储家长、老师、管理员三类账号 | id,username,password,role,real_name,phone |
t_teacher_info | 老师的详细资料,头像、教龄、擅长科目 | id,user_id,subject,grade,experience,introduce |
t_course | 老师发布的课程/家教信息 | id,teacher_id,subject,price,timeslot,status |
t_order | 预约订单主表 | id,course_id,parent_id,teacher_id,status,create_time |
t_evaluation | 课程完成后的评价 | id,order_id,from_user,to_user,content,score |
t_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 class="com.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 id="searchCourses" resultType="CourseVO"> 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 test="subject != null and subject != ''"> AND c.subject = #{subject} </if> <if test="grade != null and grade != ''"> AND c.grade = #{grade} </if> <if test="maxPrice != null"> AND c.price <= #{maxPrice} </if> <if test="status != null"> AND c.status = #{status} </if> </where> ORDER BY c.create_time DESC </select>这段SQL看起来很基础,但里面有三个容易翻车的细节。第一是<if test>里判断空值必须同时判断null和空字符串,不然用户不填条件时也会拼接SQL,导致查不出来;第二是price比较时小于号<在XML里要转义成<,不转义直接写会报XML解析错误;第三是查询结果的映射,如果字段名和下划线命名不一致,一定要配好resultMap或者开启驼峰映射mapUnderscoreToCamelCase。
更推荐的做法是单独建一个CourseVO类来承接查询结果,而不是直接复用实体类。因为查询列表页展示的字段往往比表字段多,比如需要老师姓名、老师头像、平均评分这些关联数据,实体类塞不下,VO是更合理的选择。
4.3 预约下单:事务和并发控制是答辩高频问题
预约是整个项目里逻辑最重的模块,它的完整流程是:家长看到课程详情页,点击"立即预约"→ 后端校验课程状态是否可预约 → 生成订单记录 → 订单状态变为待确认 → 老师端收到预约提醒。
第一步的后端校验里隐藏着一个并发问题:两个家长同时点击同一个课程的最后可预约时段,如果后端不做任何防护,两个请求可能都通过校验,生成两条订单,超卖就出现了。解决这个问题最直接的手段是在t_course表里加一个stock字段(可预约名额),下单时用带条件的更新语句:
// 返回1说明更新成功,说明有人抢到了名额 int rows = courseMapper.decreaseStock(courseId); if (rows == 0) { throw new BusinessException("该课程已被预约完毕"); }<update id="decreaseStock"> 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这种三层套娃代码。
建议从一开始就封装一个统一的返回类Result:
public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 200; result.message = "success"; result.data = data; return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> 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,配合连接串里的serverTimezone=Asia/Shanghai。这样能规避绝大多数时区问题。
另一个时间相关的坑是创建时间要由后端生成,不要依赖前端传值。前端用户传入的时间是不可信的,后端统一用LocalDateTime.now()赋值,这里也是一个可以做全局处理的地方,比如在MyBatis插入时通过jdbcType=TIMESTAMP配合数据库的CURRENT_TIMESTAMP默认值自动填充,能省不少事。
写到这里,项目从技术选型到数据库设计、核心编码、论文写法和答辩准备的整套思路就都铺开了。最后再说一句我的个人体会:我见过太多人把毕设当成负担,但实际上选一个像"网上家教平台"这样业务完整、技术难度适中的题目,反而是系统性补全自己Web开发认知的好机会——SSM框架的分层思想、Vue的组件化、数据库的设计约束、前后端联调的工程化,这些东西是学校课程很难一次性串起来的。真正动手从零把它搭完,收获的绝对不只是那一纸成绩,而是你以后写任何业务系统都会用的底层框架。