又到毕业季了,不少同学在选题上反复纠结。要我说,如果你想要一个业务逻辑清晰、技术栈主流、演示效果直观、又不容易撞车的选题,校园自助洗衣管理系统是个成熟度很高且发挥空间很大的选择:它同时涉及用户端小程序/Web、设备端状态管理、订单履约、支付对账、运营后台、统计报表等多个维度,几乎把SpringBoot生态里最常用的组件都能融合进来。这套源码我已经完整整理好,借着这篇博文把设计思路、核心数据结构和关键代码细节都拆开讲清楚,不管你是拿来直接跑通交差,还是要在答辩时讲出深度,都值得先通读一遍。
1. 为什么选"校园自助洗衣"做毕设:选题逻辑与需求拆解
1.1 一个毕设选题值不值得做,先看它的业务完整性
很多同学选毕设题目只看技术新不新、框架炫不炫,结果做到一半发现业务场景撑不起来——就CRUD两三个表,答辩时老师问一句"你解决了一个什么实际问题"就卡住了。
校园自助洗衣管理系统是我的首选推荐,原因很简单:这个场景的业务链条天然齐全、闭环完整。
- 用户角色分明:学生用户、前台管理者、设备运维人员三类角色,权限边界清晰;
- 核心业务闭环:用户扫码预约 → 洗衣下单 → 支付 → 设备启动 → 履约完成 → 评价/售后,这是非常完整的"交易+履约"链路;
- 管理需求真实:设备故障报修、洗衣高峰期错峰、订单退款、营收统计,每一样都是现实中洗衣房运营团队真实存在的痛点;
- 可扩展性强:业务确定后,你可以随意往上面加模块。加了定时任务就是"订单超时自动取消",加了消息队列就是"设备状态大屏推送",加了权限框架就是"多角色操作审计",这些扩展点都是答辩加分项。
用一句话概括:这个选题的后期上限,取决于你愿意花多少时间给它"加料"。
1.2 核心业务场景梳理
我建议把系统拆成三个核心业务子域来设计和讲解,答辩时按子域来讲,逻辑比"用户管理、订单管理、设备管理"这种老套分类清楚得多。
场景一:学生用户侧的预约下单流程
学生打开小程序/页面,能看到宿舍楼下每一台洗衣机的实时状态:空闲、使用中、故障、维护中。空闲状态下可在线预约(锁定设备)、选择洗涤模式(标准洗/快洗/大件洗/烘干)、提交订单、在线支付。支付完成后生成取衣码,在洗衣结束后凭码取衣。
这个场景的技术核心在于:设备状态如何保持实时同步、订单状态如何流转、支付回调如何保证一致性。
场景二:设备与运维管理侧
运维人员通过后台查看所有设备信息,处理故障报修工单,更新设备维护记录,以及配置每台设备的收费标准。
这个场景的技术核心在于:设备状态机的变更边界(例如"运行中"不能直接改成"空闲",必须先"停止",再确认取衣后才会释放设备)。
场景三:运营数据侧
管理员查看今日订单数、营收金额、设备使用率、高峰时段统计,为合理安排洗衣资源提供数据支撑。
这个场景的技术核心在于:定时统计任务、数据聚合查询、报表可视化。
1.3 角色权限矩阵
| 角色 | 核心功能 | 数据权限 |
|---|---|---|
| 学生用户 | 扫码预约、下单支付、查看订单、评价售后 | 仅限本人订单数据 |
| 设备运维 | 设备管理、故障处理、维护记录 | 设备及工单模块 |
| 系统管理员 | 用户管理、订单管理、营收统计、系统配置 | 全模块 |
权限设计在毕设里是加分项,建议用Spring Boot + Sa-Token(或Spring Security + JWT)实现基于角色的权限控制。登录校验、接口放行、动态菜单这套东西真正跑通了,比单纯写一堆接口要拿得出手得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型思路:先明白为什么,再选什么
技术选型这块我想多聊几句,因为很多毕业生简历上写着"熟悉SpringBoot",但一问为什么选它、为什么用某个依赖、版本号是多少,就答不上来。这不是背题,而是真的需要理解选型背后的逻辑。
2.1 核心框架:SpringBoot版本选型(注意版本别追太高)
这是第一个要重点强调的坑。打开Spring Initializr时默认拉到的通常是当前最新稳定版,有可能是Spring Boot 3.x,但对毕业设计来说,我强烈建议使用 Spring Boot 2.7.x。
原因有三点:
- 目前大多数教学资料、CSDN博客、源码参考停留在2.x,遇到问题能搜到的解决方案更多;
- Spring Boot 3.x要求JDK 17及以上,很多同学本机是JDK 8,为了跑通还要重装环境;
- 最重要的,Spring Boot 3.x把 javax 包名迁移到了 jakarta,很多第三方组件的兼容版本还没跟上,你引入一个2.x的老依赖就可能直接启动报错。
我在部署这套源码时发现,后台引入的MyBatis-Plus、Druid、FastJSON这些组件,在2.7.x下完全兼容,跑起来非常顺滑。如果你的学校对技术版本有强制要求(比如指定Spring Boot 3),那另说;否则选2.7.x,能帮你省掉大量折腾环境的时间。
2.2 持久层:为什么建议用MyBatis-Plus而不是JPA
作为毕设项目,数据库操作要兼顾两件事:开发效率和讲解复杂度低。MyBatis-Plus在两者之间取得了最好的平衡。
- 简单的单表CRUD,不用手写SQL,继承
BaseMapper<T>就能用增删改查和分页; - 复杂的多表关联查询、统计报表,需要写SQL时仍然可以走自定义Mapper XML;
- 条件构造器
QueryWrapper/LambdaQueryWrapper非常直观,代码量少,排查方便。
JPA虽然也是好选择,但它的懒加载、级联、持久化上下文等概念对不熟悉JVM生态的读者来说理解成本偏高。MyBatis-Plus的SqlSession概念我们做Java开发的多多少少都碰过,答辩时更容易讲清楚。
2.3 前端与接口协作:Vue + Axios + JWT
如果整套系统只有后端接口,视觉效果上会吃大亏。毕设评审的时候,有页面展示和纯Swagger接口是两种观感。
建议前端采用Vue 2 + Element UI(或Vue 3 + Element Plus),用Axios请求后端接口。登录鉴权统一采用JWT Token方案:用户登录成功后,后端签发一个Token,前端存储在本地,并在每次请求的 Authorization 请求头中携带。
这里有一个很多同学常踩的坑:前端发起请求时会先有预检请求(OPTIONS),如果后端没有正确配置跨域和放行,就会出现"前端能看到请求发出去了,但后端报错403"的情况。后面第5章我会给出完整的跨域和JWT过滤器配置。
2.4 加分项选型:Flowable工作流与Quartz定时任务
热搜词里出现了"springboot使用flowable"和"springboot quartz",这其实是两个被高频关注的技术结合点,用在这套系统里也很有说服力。
- Flowable工作流:可以用在"故障报修工单审批流程"和"退费申请流程"上。比如学生提交退费申请后,需要设备管理员审核、运营主管复核、财务确认退款,这是一个标准的多级审批流,用Flowable的BPMN流程定义可以很优雅地实现;
- Quartz定时任务:可以用在"订单超时未支付自动关闭"、"每日凌晨生成运营日报"等场景上。在毕设答辩时,能主动说出"我用Quartz定期扫描超过30分钟未支付的订单并关闭,释放洗衣设备",比说"我写了定时任务"有说服力得多。
3. 核心功能模块落地拆解:从下单洗衣到设备运维
3.1 用户端完整操作链路的设计
整套系统里最核心的业务链路就是"预约下单-支付-洗衣-取衣"。我梳理一下每个环节的职责划分:
预约阶段
用户选择空闲设备 → 设置洗涤模式 → 选择预计开始时间 → 生成待支付订单。这一步的关键是防止设备被多人同时预约。虽然在实际项目里可以引入Redis分布式锁,但为了兼顾可读性,我在源码中采用的是数据库乐观锁:设备表中加一个 version 字段,更新设备状态时用 UPDATE ... SET status = 1, version = version + 1 WHERE id = ? AND version = #{version},如果影响行数为0则说明设备已被别人抢先操作,提示"该设备刚刚被预约,请重新选择"。
支付阶段
学生支付成功后会进入"已支付待使用"状态。这里要特别注意支付回调的幂等性:如果支付平台回调了两次,系统不能生成两次支付流水。我在实现时用 payment_no 唯一索引 + 状态判断双重保证,只有当订单状态为"待支付"时才允许更新为"已支付"。
履约阶段
到预约时间后,设备状态变为"运行中"。前端可以实时轮询显示剩余时间(实际项目可用WebSocket或SSE推送,毕设用轮询也说得通,但如果你时间充裕,强烈建议用WebSocket做设备状态实时推送,这是一个很好的答辩亮点)。洗衣结束时状态变为"待取衣",用户凭取衣码取走衣物后,设备释放为"空闲"。
售后阶段
用户可以对订单发起"故障投诉"或"退费申请",并填写原因描述。提交后自动创建一条工单,对应的审批流程由Flowable驱动流转。
3.2 管理端设备与运维工单的处理逻辑
设备管理不能只是一个增删改查页面,要体现"设备生命周期"的概念。
我在系统里给每台设备设计了几个核心字段:
- 设备编码(每一台洗衣机有唯一编码,用于扫码识别);
- 所在宿舍楼栋、楼层;
- 设备状态(空闲、使用中、故障、维护中、已下线);
- 累计使用次数、累计营收金额;
- 最近维护时间、下次保养提醒时间。
设备状态的变更路径不是随意跳转的。比如一台"运行中"的设备如果没走完"洗衣完成"的流程是不能直接改为"空闲"的;一台"故障"设备需要维修人员提交维修记录后才可重新上线。这个逻辑看似简单,但能让答辩老师看到你对状态机设计的重视。
3.3 引入Flowable工作流后的状态流转设计
在毕设中引入工作流引擎是一个很能体现技术深度的地方。Flowable是业界成熟的开源工作流引擎,它为"退费审批"、"故障报修"这类需要多方协作审批的业务提供了标准化的流程定义和执行机制。
我把"退费申请"流程设计为:
code复制用户提交退费申请(启动流程实例)
→ 设备管理员审批(校验订单是否已完成、设备是否有异常)
→ 运营主管复核(确认退款金额、审核原因)
→ 财务执行退款(更新订单状态为已退款)
Flowable会维护一张 ACT_RU_TASK 运行时任务表和 ACT_HI_* 历史表,所有待办任务、已办任务、审批记录都有迹可循,不需要自己手动建表存审批状态。答辩时你能说清楚Flowable的流程定义(BPMN)和核心数据表结构,这已经达到优秀毕业设计的深度了。
我第一次跑通Flowable的时候,最大的感受是:工作流引擎的价值不是"帮我省了写CRUD的功夫",而是让业务过程中的"谁审批、下一步去哪、审批历史"完全可追溯。这种认知差异,在开发者和学生之间非常明显。
4. 数据库设计:这些表是这套系统的心脏
4.1 核心表结构总览
我按第三范式设计了一套覆盖系统所有业务模块的表结构,总共 11 张核心表,在答辩时可以按"用户域、订单域、设备域、运营域"四组来讲解。
| 业务域 | 表名 | 说明 |
|---|---|---|
| 用户域 | sys_user |
系统用户表(学生/管理员/运维人员),含账号、密码、手机号、角色 |
| 用户域 | sys_role/sys_user_role |
角色表与用户角色关联表 |
| 订单域 | wash_order |
洗衣订单主表,状态机核心表 |
| 订单域 | payment_record |
支付流水表,记录支付时间、金额、支付渠道、交易号 |
| 订单域 | refund_apply |
退费申请表,关联Flowable流程实例ID |
| 设备域 | device_info |
洗衣设备信息表,含设备编码、地理位置、状态 |
| 设备域 | device_maintenance |
设备维护/维修记录表 |
| 设备域 | device_fault_report |
故障报修表,学生端可提交 |
| 运营域 | recharge_record |
用户钱包充值记录表 |
| 运营域 | statistics_daily |
每日运营统计数据表 |
| 运营域 | banner_info |
首页轮播/公告配置表 |
这11张表足够撑起一个像模像样的管理信息系统,而且每张表都有内容可讲。如果你后面想扩展,可以再加优惠券表、消息通知表、操作日志表,随时有扩展空间。
4.2 订单表的状态机设计:这是毕设的核心
wash_order 表是整套系统里逻辑最复杂的表,也是答辩时最容易被追问的表。建议把状态字段用 tinyint 存储,并在代码里用枚举类统一管理。
| 状态值 | 含义 | 触发动作 |
|---|---|---|
| 0 | 待支付 | 用户提交订单 |
| 1 | 已支付待使用 | 支付成功回调 |
| 2 | 运行中 | 设备开始洗衣 |
| 3 | 待取衣 | 洗衣完成,等待用户取衣 |
| 4 | 已完成 | 用户确认取衣或系统超时自动确认 |
| 5 | 已取消 | 超时未支付或用户主动取消 |
| 6 | 退款中 | 用户申请退费,流程审批中 |
| 7 | 已退款 | 退费流程完结,退款到账 |
| 8 | 异常 | 设备运行中出现故障 |
这张状态机表值得你花时间画一张图(用Word或Visio都可以),答辩时PPT里放上这张图,不用多说话,老师就知道你认真设计了业务逻辑。
我特别想提醒一点:订单状态字段一旦更新,必须关联操作日志表或订单流水表。比如"待支付" -> "已支付"这步,要记录下支付流水号;"运行中" -> "待取衣"这步,要记录设备返回的结果。这样做的好处是,排查问题时不用靠猜,直接看状态流转记录就能定位到哪一步出了问题。很多同学做到这一步会偷懒,等到做数据回查时才知道后悔。
4.3 设备表与状态管理
device_info 表设计时请留意几个容易被忽略但实际很有用的字段:
last_heartbeat_time:设备最近一次心跳上报时间。如果超过一定时间没有上报,说明设备离线,需要运维介入;total_usage_count和total_income:累计使用次数和累计营收,统计报表直接聚合这个字段,不用每天全表扫描订单;sort_order:列表排序值,方便把使用频率高的设备排在前面。
数据库设计上还有一个细节:金额字段一律用 decimal(10,2),绝不要用 float 或 double。这是大学课堂里讲得很少但工作中非常重要的常识,浮点数会产生精度丢失,涉及钱的地方必须用定点数。
5. 关键技术点的实战代码细节
这几块代码是我从完整源码里抽出来的核心片段,每一个都可以直接复制到你的项目里跑通。我尽量还原我最初跑通它们时的真实场景和踩坑过程。
5.1 JWT登录态校验与Swagger白名单:冲突解决思路
JWT登录态是这套系统的地基。用户登录成功后,后端使用 jjwt 生成Token,并把用户ID、角色信息封装在Token里。后续每次请求,前端在 Authorization 头中携带Token,后端通过一个拦截器统一校验。
配置拦截器时有一个经典坑:因为引入了Swagger做接口文档,如果没把Swagger相关的接口路径加入白名单,会导致无法访问 /swagger-ui.html 和 /v3/api-docs。我在源码中这样处理:
java复制@Configuration
public class JwtInterceptor implements HandlerInterceptor {
private static final List<String> WHITE_LIST = Arrays.asList(
"/auth/login",
"/auth/register",
"/swagger-ui.html",
"/webjars/**",
"/v2/api-docs",
"/v3/api-docs",
"/swagger-resources/**",
"/doc.html",
"/favicon.ico"
);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
// 静态资源和白名单直接放行
for (String pattern : WHITE_LIST) {
if (uri.startsWith(pattern) || uri.matches(pattern.replace("**", ".*"))) {
return true;
}
}
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
try {
Claims claims = JwtUtil.parseToken(token);
request.setAttribute("userId", claims.get("userId"));
request.setAttribute("role", claims.get("role"));
return true;
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"code\":401,\"msg\":\"登录状态已失效,请重新登录\"}");
return false;
}
}
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"code\":401,\"msg\":\"请先登录\"}");
return false;
}
}
配置跨域时要额外注意,allowedOriginPatterns 不要写成 allowedOrigins("*"),否则在携带 Authorization 头时某些浏览器版本会出现跨域失效问题。推荐这样写:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedHeaders("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.maxAge(3600);
}
}
5.2 订单超时未支付自动关闭:Quartz定时任务的正确用法
这是源码里最有代表性的一个定时任务场景。订单生成后30分钟内未支付,就要自动关闭并释放设备,避免占用资源。
我采用Quartz + Spring Boot的方式,写一个Job类,每1分钟扫描一次"待支付"订单:
java复制@Component
public class CloseTimeoutOrderJob implements Job {
@Autowired
private WashOrderMapper washOrderMapper;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 查询所有待支付且创建时间超过30分钟的订单
List<WashOrder> timeoutOrders = washOrderMapper.selectList(
new LambdaQueryWrapper<WashOrder>()
.eq(WashOrder::getStatus, 0)
.lt(WashOrder::getCreateTime, LocalDateTime.now().minusMinutes(30))
.last("LIMIT 200")
);
for (WashOrder order : timeoutOrders) {
// 关闭订单
washOrderMapper.updateStatusById(order.getId(), 5);
// 释放设备:设备状态从"预约锁定"恢复为"空闲"
deviceInfoMapper.updateStatusByOrderId(order.getDeviceId(), 0);
log.info("订单【{}】超时未支付,已自动关闭", order.getOrderNo());
}
}
}
这里有一个细节值得注意:如果你在真实项目里用 @Scheduled 注解就能实现,为什么还要引入Quartz?答辩时如果被问到,可以说 @Scheduled 适合单机固定周期任务,Quartz支持任务持久化、分布式部署、Cron表达式的动态调度,扩展性更强。这是技术上"加分"的合理话术。
5.3 设备状态并发控制:乐观锁和唯一索引缺一不可
设备并发问题是这套系统里最容易出事故的地方。想象一下:A和B两个用户同时点开了同一台空闲洗衣机的页面,A先提交了预约,然后B也提交了预约。如果没有并发控制,B的后端会拿着脏数据去更新设备状态,结果就是设备台账显示"被B锁定",但订单是A创建的,数据库里就出现脏数据了。
我在设备状态更新时使用的乐观锁方案是:
java复制@Update("UPDATE device_info SET status = #{newStatus}, version = version + 1, " +
"update_time = NOW() " +
"WHERE id = #{deviceId} AND status = #{expectStatus} AND version = #{version}")
int compareAndSetStatus(Long deviceId, Integer expectStatus, Integer newStatus, Integer version);
每次更新前,先读取当前 version,更新时把 version 当作条件,如果影响行数为0说明设备状态已被其他事务修改,需要重试或提示用户。这套逻辑你可以类比成"抢购时用乐观锁扣减库存",也是电商系统里非常经典的做法。
5.4 MyBatis-Plus分页与条件组合查询
管理后台的"订单列表"基本都长一个样:顶上是筛选条件(状态、时间范围、设备编号),下面是一个分页表格。MyBatis-Plus让整段代码非常简洁:
java复制public PageResult<WashOrderVO> pageOrders(OrderPageQuery query) {
Page<WashOrder> page = new Page<>(query.getPageNum(), query.getPageSize());
LambdaQueryWrapper<WashOrder> wrapper = new LambdaQueryWrapper<>();
// 状态筛选
if (query.getStatus() != null) {
wrapper.eq(WashOrder::getStatus, query.getStatus());
}
// 时间范围筛选
if (query.getStartTime() != null && query.getEndTime() != null) {
wrapper.between(WashOrder::getCreateTime, query.getStartTime(), query.getEndTime());
}
// 设备编号模糊查询
if (StringUtils.hasText(query.getDeviceCode())) {
wrapper.like(WashOrder::getDeviceCode, query.getDeviceCode());
}
wrapper.orderByDesc(WashOrder::getCreateTime);
Page<WashOrder> result = washOrderMapper.selectPage(page, wrapper);
return PageResult.of(result);
}
注意:like 查询在字段值非常大时性能会下降,但在毕设数据量级别下完全没有问题。答辩时如果有老师问到,可以说"生产环境会使用全文检索或搜索引擎,毕设场景用MySQL的like就能满足",既显示你思考过,也不会给自己挖坑。
6. 让项目跑起来:从源码到本地部署
源码拿到手后,很多人第一件事就是直接启动,结果报错一堆。别慌,这很正常。我按正常顺序把部署过程捋一遍,照着做能省一半时间。
6.1 环境准备与参数配置
| 依赖项 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 对应Spring Boot 2.7.x |
| Maven | 3.6.3+ | 管理项目依赖 |
| MySQL | 5.7 / 8.0 | 建议8.0,连接串加 serverTimezone=Asia/Shanghai |
| Redis | 5.0+ | 用于Token黑名单和热点缓存 |
| Node.js(可选) | 14+ | Vue前端开发环境 |
拿到源码后首先要改的是 application.yml 里的数据库连接配置,尤其是 jdbc:mysql://localhost:3306/laundry_system?useUnicode=true&characterEncoding utf8&useSSL=false&serverTimezone=Asia/Shanghai,同时设置 username 和 password。
然后执行项目里附带 sql/laundry_system.sql 建库脚本,注意先建立数据库 laundry_system,再运行SQL文件。
6.2 常见启动报错清单
-
启动时报
Address already in use: bind——端口占用。打开application.yml修改server.port为8081或8082。 -
启动时报
Unable to start EmbeddedWebApplicationContext——Redis连不上。如果没装Redis,先在本地启动一个;或者在配置里把Redis相关配置注释掉,并删除代码里的Redis操作类。 -
前端页面接口报404——检查是不是后端启动端口跟前端 axios 配置里的
baseURL不一致。Vue项目里的.env.development文件改成VUE_APP_BASE_URL = 'http://localhost:8080'。 -
Flowable 创建的表前缀不同:如果项目配置了Flowable,默认会创建
ACT_*前缀的35张表,首次启动会自动建表,不用手动导入。
6.3 基于Docker的部署方案(加分项)
如果你希望代码能在答辩环境快速演示,推荐用Docker把MySQL + Redis + 后端 + 前端全部编排起来。
后端镜像打包时,我踩过一个坑:JDK 8环境下用Maven打出的jar需要基础镜像,不要用 openjdk:11,要用 openjdk:8:
dockerfile复制FROM openjdk:8-jdk-alpine
VOLUME /tmp
ADD target/laundry-system.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
前端Nginx部署时,需要将代理指向后端容器:
nginx复制server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:8080/;
}
}
docker-compose.yml把MySQL、Redis、后端、前端四个服务编排起来,答辩时一键 docker-compose up -d 就能全盘拉起,这个操作在现场演示时效果相当加分。
7. 毕设过程中那些坑与答辩加分细节
7.1 最容易踩的坑,我这里逐个说一遍
坑一:数据库建表顺序导致外键报错
如果你使用了外键约束(虽然我建议尽量不用物理外键,逻辑上维护外键关系就好),导入SQL时要注意先建主表(用户表)再建子表(订单表),否则会报外键约束失败。如果你导入SQL时报错,先看看建表语句顺序,把有外键关联的表放到最后。
坑二:JPA / MyBatis-Plus字段映射不一致
数据库字段是下划线命名(create_time),Java实体类是驼峰命名(createTime),如果MyBatis-Plus没有开启驼峰映射,查询结果会出现 createTime 始终为null。在 application.yml 中确认:
yaml复制mybatis-plus:
configuration:
map-underscore-to-camel-case: true
这是我见过最多人踩的坑,排查时容易让人怀疑人生,实际就是一行配置的事。
坑三:上传的图片/头像访问404
如果做用户头像或设备照片上传功能,本地上传路径最好放在项目外的独立目录(如 D:/upload/),然后在WebMvcConfig里配置虚拟路径映射:
java复制@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/upload/**")
.addResourceLocations("file:D:/upload/");
}
如果文件存在但浏览器访问 404,多半是路径映射没配好,而不是文件没传成功。
坑四:Spring Boot 2.6以后 spring.mvc.pathmatch.matching-strategy
这个问题特别隐蔽。如果你在项目中使用Springfox(springfox-swagger2 3.0.0)并搭配 Spring Boot 2.6+,启动时会直接报空指针异常,原因是Spring MVC的路径匹配策略从 AntPathMatcher 被更改为 PathPatternParser,Springfox 还没适配。解决办法是在 application.yml 中加一行:
yaml复制spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
7.2 答辩时老师普遍会问的技术点
根据我带过的毕业生答辩经验,这套系统里老师最常问的5个问题是:
-
"订单超时未支付为什么不用Redis的过期键?" —— 正面回答:Redis过期键在键过期时才会清除,且不保证实时性;Quartz定时扫描能更准确地控制"超过30分钟"这个业务阈值,同时可以批量处理并留下日志。这样的回答更有说服力。
-
"你怎么保证支付回调的幂等性?" —— 回答要点:唯一索引 + 状态更新前先查询/更新状态,重复回调时状态已不是"待支付",直接忽略或返回成功。
-
"Flowable的表为什么要那么多?" —— 回答要点:
ACT_RU_*是运行时的任务和流程实例,ACT_HI_*是历史数据,ACT_ID_*是身份管理,ACT_GE_*是通用数据。这种表分类本身就是工作流引擎的设计精髓。 -
"并发场景怎么处理?" —— 回答要点:数据库乐观锁控制设备状态;后续可以引入Redis分布式锁升级。提到基于数据库的乐观锁时,顺手把SQL片段一说,老师就知道你是真做过。
-
"为什么选Spring Boot而不是SSH?" —— 回答要点:Spring Boot简化了整合配置,内嵌Tomcat开箱即用,自动装配机制降低了上手成本,同时国内企业现在的主流技术栈就是Spring Boot生态。
7.3 我的一点点个人建议
这套源码我陆陆续续调整过很多遍,从最初只有后端接口,到后来补全前端页面、引入Flowable、加上Quartz定时任务、打包Docker镜像,一路走下来最大的感触是:毕设项目的加分项不是某一个"炫酷"的技术,而是一条完整通畅的技术链路。同一个系统,把"用户登录 → 下单 → 支付 → 设备状态变更 → 定时关单 → 退费流程审批"这条链路完整跑通,比堆砌十个孤立的技术点更能打动评委。
如果你时间紧张,我建议优先保证三件事:一是系统能正常启动演示,二是把订单状态机的核心流程讲清楚,三是把JWT登录和数据库设计说透。这三样稳了,答辩拿个不错的成绩基本没问题。后面如果还有富余时间,再逐项打磨Flowable流程和中台统计报表,每加一个模块就是多一个加分亮点。
这套源码本身就是按照上面这些思路完整实现的,把前后端跑通之后,建议你逐行看一看订单状态机的Service实现和Flowable的BPMN流程文件,再对照自己的理解重构一遍,收获会比直接拿去交差大得多。
