每年到了毕业设计季,“报修管理系统”这类题目就会准时出现在导师给的选题清单上。我做这个校园宿舍报修平台之前,其实先调研了一圈实际情况——学校里报修基本靠微信群接龙、宿管手写登记,或者直接在楼栋群里口头喊一嗓子,三个问题特别典型:信息被聊天记录刷掉、维修进度完全靠猜、事后想统计都找不到数据。所以这个项目干脆用 SpringBoot + Vue 做了一套前后端分离的维修工单系统,把在线报修、后台派单、师傅接单、进度跟踪、完工验收整条链路串成一个闭环。这篇内容会把项目核心设计、技术选型思路、数据库字段、状态流转和踩坑记录都拆开讲,适合正在准备 Java 方向毕业设计,或者想练手完整前后端分离项目的同学参考。
1. 这个项目到底在解决什么问题
1.1 报修平台的本质是工单生命周期管理
宿舍报修听起来简单,无非是“学生报一下、师傅去修一下”,但如果直接按这种直觉去设计数据库,后面基本会翻车。换个角度想:一条报修记录从创建到最后归档,状态一直在变——刚提交是待处理,管理员派给师傅是待接单,师傅开始动手是维修中,修好以后要等学生确认,学生验收通过才算真正结束,中间还可能出现师傅临时退回、材料不够挂起、学生撤销等情况。
这其实就是一个典型的工单生命周期管理问题,跟快递物流很像:你下单、商家发货、快递运输、派送、签收,每一步都有一个可查询的状态。工单系统要做的事情,就是把这套状态流转和角色操作对应起来:我这个项目里,核心状态定义成六种,每种状态谁可以操作、可以流向哪些状态,都要提前定死。
| 状态值 | 中文含义 | 谁可以触发 | 可流向 |
|---|---|---|---|
| PENDING | 待派单 | 学生提交后自动进入 | ASSIGNED、CANCELLED |
| ASSIGNED | 已派单待接单 | 管理员派单触发 | PROCESSING、REJECTED |
| PROCESSING | 维修中 | 师傅接单后触发 | FINISHED、PENDING |
| FINISHED | 待验收 | 师傅提交完成结果触发 | COMPLETED、REJECTED |
| COMPLETED | 已完成 | 学生确认验收触发 | 无 |
| CANCELLED | 已取消 | 学生撤销或管理员关闭 | 无 |
设计状态机的时候,我的建议是先别急着写 CRUD,拿一张纸把每个状态画出来,标清楚“谁在什么条件下能把状态改成什么”。这一张状态图想清楚了,后面写 Controller 和 Service 都是顺水推舟的事,答辩的时候导师问业务流程,你也能当场画出来。
1.2 角色、权限与业务边界
工单系统不是给一个人用的,角色至少分成四类,每一类能看到和操作的资源都不一样:
- 普通学生用户:创建报修单、查询自己的工单列表、查看维修进度、确认完工、评价、取消未受理的工单。
- 维修师傅:查看派给自己的工单、接单或退回、填写维修记录和补充材料费用、提交完工结果。
- 后勤管理员(调度员):查看全部工单、给工单指派师傅、处理异常单、导出统计报表。
- 系统管理员:维护楼栋宿舍信息、维护用户和角色、管理报修类型字典、查看操作日志。
这四个角色基本就是后台权限管理的最小完整闭环。权限控制不用做得很复杂,一个用户表带 role 字段,后端接口做角色校验就够了。但如果你希望答辩加分,也可以引入 JWT 中间件里校验角色的设计,后面专门讲。
需求边界也要控制住。毕设项目最怕什么?怕功能铺太开。有人一上来就想做多校区、多维修队排班、消息推送、工单自动分单算法,结果数据库一塌糊涂,前端页面堆了一堆半成品。我给自己定的边界就是:以工单为主线,把报修、派单、维修、验收、统计这五件事做扎实,其他都是选做,全部做完了还有时间再往上加。
2. 技术选型:为什么是 SpringBoot + 前后端分离
2.1 SpringBoot 凭什么是毕设主力
Java 生态里做后端,现在基本绕不开 SpringBoot。它不是新发明了一个框架,而是把 Spring 那一套繁琐的 XML 配置、依赖注入、组件装配工作简化成了“自动配置 + 约定大于配置”。我最早用 SSM 写项目,光搭一个能跑起来的环境就要折腾一两天,SpringBoot 把内嵌 Tomcat、数据源、MyBatis 等常用组件全自动装配好,启动就是一个 main 方法,这对毕设这种周期短、需要快速出东西的场景非常友好。
另一个重要原因是答辩和面试都看重 SpringBoot。导师看到“基于 SpringBoot + Vue 的报修平台”这个题目就能理解你在做什么,面试官也默认应届生应该会这个技术栈。加上 SpringBoot 本身生态成熟,Redis 缓存、定时任务、邮件通知、文件上传这些扩展能力都有现成的 starter,后面想加亮点模块很容易。
2.2 前后端分离到底怎么落地
这个项目说的是“前后端分离”,很多人以为只要把 Vue 工程和后端工程分开放就叫分离了,其实核心是接口化:前端通过 HTTP 请求访问后端提供的 RESTful API,数据以 JSON 格式传输,前后端不再共享页面模板和 Session,各自独立开发、独立部署。
我用的组合是 Vue 3 + Element Plus 做管理端界面,学生在手机上也能访问报修页面。前端工程通过 Axios 统一封装请求,带上 Token,后端提供/api/user/**、/api/order/**、/api/admin/**等接口前缀区分权限。开发环境下前端通过 Vite 代理解决跨域,生产环境可以部署到同一台服务器的不同端口,再用 Nginx 做反向代理把/api转发给后端。这样设计的好处很明显:小程序端、移动端、管理后台都可以复用同一套接口,以后要做扩展不用重写后端。
2.3 认证方案:JWT 无状态登录
Session 登录在前后端分离项目里会遇到跨域带 Cookie 的问题,所以这个项目我直接用了 JWT 方案。登录成功后后端签发一个 Token,前端存到 localStorage 或内存里,每次请求在 Header 里带上Authorization: Bearer <token>,后端拦截器解析 Token 拿到用户 ID 和角色。
具体链路是:用户登录 -> 后端校验账号密码 -> 用 secret 生成 Token -> 返回给前端 -> 前端请求接口时携带 -> 后端自定义拦截器或 Spring Security 过滤器校验 -> 放行或返回 401。我记得很多同学第一次写的时候容易直接在 Controller 里每次手动解析 Token,太啰嗦,更好的做法是写一个LoginUser工具类或配套 AuthInterceptor,一段逻辑全局生效。
2.4 项目整体模块划分
后端按业务模块分包,而不是按技术层次扔一堆类,这一点新手特别容易乱。我划分成了这些模块:
controller:对外暴露接口,只做参数接收和简单校验。service:核心业务逻辑,状态流转、派单规则都写在这里。mapper:MyBatis-Plus 的数据访问层。entity:数据库表对应的实体类。dto:接口入参对象,避免直接用实体类接收前端数据。vo:返回给前端的视图对象,隐藏掉不必要字段。common:统一返回结果、异常处理、工具类。config:WebMvc 配置、跨域配置、文件存储配置。
对比另一个常见的技术选择,Spring Data JPA 直接操作对象更方便,但复杂条件查询和分页反而不够透明。MyBatis-Plus 的优势在于既能通过 BaseMapper 快速实现单表 CRUD,又保留了手写 SQL 的灵活性,动态查询条件用 LambdaQueryWrapper 也很方便。毕设场景下数据量不大、查询逻辑也不复杂,MyBatis-Plus 是最稳的选择。
3. 数据库设计是成败的关键
3.1 核心表设计逐字段说明
这个项目的核心表我最终收敛成了六张左右,业务主表是维修工单表,另外有用户表、报修类型字典表、派工记录表、工单操作日志表、楼栋宿舍信息表。先说最重要的work_order工单表。
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增或雪花算法 |
| order_no | varchar(32) | 工单编号,对外展示用 |
| user_id | bigint | 报修人 ID |
| repair_type_id | bigint | 报修类型(水、电、木工等) |
| dorm_building | varchar(32) | 宿舍楼栋 |
| dorm_room | varchar(32) | 房间号 |
| description | varchar(500) | 故障描述 |
| image_url | varchar(200) | 现场图片路径 |
| status | varchar(20) | 当前状态,存字符串枚举 |
| assignee_id | bigint | 当前处理师傅 ID |
| priority | tinyint | 紧急程度 |
| finish_time | datetime | 用户确认完成时间 |
| deleted | tinyint | 逻辑删除标记 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
派工记录表repair_dispatch记录每一次派工行为,因为同一个工单可能会因为师傅退回而二次派单,不能只保留一个字段。操作日志表work_order_log记录每个状态变更的时间、操作人、操作前后的状态,这一步是后面做“进度时间线”功能的数据来源,强烈建议每个工单系统都要有。
为什么要有日志表?我排坑时发现,如果没有日志表,一旦状态交替出现错误,你想排查是哪个环节改的,所有线索都没有。有了日志表,前端随便展示,后端排查也方便,属于性价比极高的一张表。
3.2 工单号生成规则与状态字段选型
工单号不要直接用数据库自增 ID,原因很简单:懂行的人看到订单号是 1、2、3 就能猜出你的业务量,而且自增 ID 拼接在 URL 里容易被遍历抓取。我采用的规则是:业务前缀BX+ 年月日 + 当日随机流水号,例如BX202506120003,含义就是维修报修、2025年6月12日、当天第 3 单。
生成方式是每天凌晨把当天的计数 Redis 重置,或者直接查当天已有工单数再加一。如果不想引 Redis,数据库表里记一个每日序号字段也可以,并发不高的情况下完全够用。这个细节能在答辩时加分,因为它体现了你会考虑业务可读性和数据安全,而不是只会自增。
状态字段这里也多说一句:别用0/1/2/3这种魔法数字,数据库里直接存PENDING、PROCESSING这样的可读字符串。魔法数字的坏处是,代码里到处是if(status == 2),过两周你根本想不起来 2 代表什么,前端更是看得一头雾水。字符串枚举虽然稍微占一点存储,但可读性带来的维护收益非常大——唯一要注意的是代码里要维护一个枚举类,别散落各处写死字符串。
3.3 索引和通用审计字段
数据库设计最容易被人忽略的就是索引,但这是后期查询性能的分水岭。工单常见的查询场景包括:按报修人查自己的工单、按状态查待处理工单、按楼栋筛选、按时间排序,所以至少要有这几个索引:
(user_id):学生查“我的报修单”。(status, create_time):管理员按状态筛选并按时间倒序。(assignee_id, status):师傅看自己名下待接单/维修中的工单。order_no:唯一索引,用于精确查询工单编号。
除了索引,每张表都应该加上create_time、update_time、deleted三个字段。前两个做排序和统计,后一个做逻辑删除。很多毕设直接物理删除数据,演示时点一下删一条,看起来没问题,但到答辩演示统计数据时会发现数据稀碎,逻辑删除至少让你能保留历史记录。MyBatis-Plus 里配置@TableLogic就能实现全局逻辑删除,成本极低,收益明显。
4. 核心流程实现:从报修到完工
4.1 提交报修:校验、入库与图片上传
先看学生提交报修这一段。前端页面把表单提交到/api/order/create,后端要做的事情不只是 insert 一条记录,而是包括权限校验(当前登录用户必须是普通学生账号)、参数校验(楼栋房间不能空、故障描述至少 5 个字)、图片处理、生成工单号、组装状态和初始日志。
图片上传是这里面的一个细节。我最初把图片直接存到数据库 base64,结果一条带图的记录能把接口响应拖慢一大截,这是新手最容易犯的错误。正确做法是前端先调用文件上传接口,把图片放到服务器指定目录或者 MINIO/OSS,数据库只存访问 URL。本地存储时记得配置静态资源映射,比如把/upload/**映射到磁盘目录,否则图片存了也访问不到,这个问题我后面排障部分还会专门讲。
核心代码逻辑大概是这样:
@PostMapping("/create") public Result createOrder(@RequestBody @Valid OrderCreateDTO dto) { // 1. 从当前登录上下文获取用户ID Long userId = LoginUser.get().getId(); // 2. 生成工单号 String orderNo = orderNumberGenerator.generate(); // 3. 构造工单实体,状态初始化为 PENDING WorkOrder order = new WorkOrder(); order.setOrderNo(orderNo); order.setUserId(userId); order.setStatus(OrderStatus.PENDING.name()); // 4. 保存并写日志 orderService.save(order); orderLogService.record(order.getId(), null, OrderStatus.PENDING.name(), "学生提交报修"); return Result.success(order.getOrderNo()); }注意这里虽然代码看起来简单,但save和recordLog两步必须放在同一个事务里,否则会出现工单创建成功但日志丢失的情况。事务要加在 Service 层的公开方法上,Controller 里加注解是无效的,这个知识点面试和答辩常问。
4.2 派单与师傅接单:并发状态控制
工单创建后进入待派单状态,管理员可以在管理后台看到所有待派单的记录,选择一名维修师傅进行指派。这个环节我设计了两种模式可供选择,一是管理员直接指定师傅,二是师傅在待接单列表里“抢单”或“接单”。校园后勤场景里,直接指派更可控,所以我以指派为主,抢单作为扩展。
派单动作会发生一个关键问题——并发更新。设想一个场景:学生同时提交了两条报修,管理员在页面上快速操作;或者两个管理员都看到了同一个待派单工单,同时点了指派不同的师傅。如果不加控制,后写的把先写的覆盖掉,状态和师傅就对不上了。解决思路有两种:
第一种是乐观锁思路,在更新语句里把“当前状态等于期望状态”作为条件:
UPDATE work_order SET status = 'ASSIGNED', assignee_id = #{assigneeId} WHERE id = #{orderId} AND status = 'PENDING'如果影响行数为 0,说明已经被别人处理过了,接口直接提示“工单状态已变化,请刷新页面”。这是工单系统里非常实用的写法,比在代码里先 select 再 update 靠谱多了。
师傅端接单的本质也是状态流转:他把ASSIGNED改成PROCESSING之前,必须确认这个工单确实是派给自己的。所以接口入参要带上工单 ID,Service 里除了判断状态,还要判断assignee_id == 当前登录用户ID,防止越权操作别人名下的工单。
4.3 维修、验收与评价的完整闭环
师傅维修完成以后,需要填写维修结果,包括维修方式、是否更换配件、材料费用等,然后提交完工。此时工单状态变成FINISHED(待验收)。这一步的信息属于关键业务数据,要一次性录入完整,所以我设置了非空校验,前端也做了相应的表单约束。
待验收状态下,学生登录系统会看到“师傅已完成维修,请验收”的提醒。学生可以确认完成,也可以选择不满意退回,退回后工单回到PENDING或者ASSIGNED,具体看设计——我这里是让工单回到待派单状态并附带一条退回原因,管理员重新处理。
验收完成之后,可以顺手做一个小评价功能,让学生对维修速度、服务态度打分。这个功能虽然简单,但很有价值:一是完整闭环了“报修 - 派单 - 维修 - 验收 - 评价”全流程,二是评价数据能直接用于统计模块。要注意评价表和工单表是一对一关系,不要设计成一对多。
4.4 通知与跟踪页面设计
跟踪功能依赖日志表。前端工单详情页可以拉取/api/order/{id}/log接口,按时间倒序展示“谁在什么时间做了什么事”,这就是全链路时间线。后续如果要做站内信通知,比如派单后给师傅发消息、完工后给学生发消息,也可以基于日志表和未读消息表来做。
前端有几个细节值得注意。一是按钮要根据状态和角色显隐,比如学生只能在PENDING状态看到“取消工单”、在FINISHED状态看到“确认验收”,师傅只能在ASSIGNED状态看到“接单”,要封装一个权限判断方法统一处理,不要在模板里写一堆v-if嵌套逻辑难维护。二是列表页要配合后端的分页参数,用页码和每页条数来请求数据,不要一次拉全量。
5. 高频问题与排坑记录
5.1 并发更新把状态覆盖了
这个坑非常典型,我在测试阶段反复遇到过:两个请求同时到达,第一个请求把工单从PENDING改成ASSIGNED,第二个请求基于旧的PENDING状态又做了一次更新,最终数据变成错误状态。后来统一改成“条件更新 + 判断影响行数”的方案,所有状态流转都带上前置状态条件,问题迎刃而解。而且这个方案不需要引入分布式锁,毕设场景里完全够用。
5.2 上传图片/文件访问 404
本地存储图片后访问 404 的原因基本就是没配置静态资源映射。SpringBoot 默认只映射classpath:/static/,你上传到磁盘/data/upload/目录后,浏览器访问http://localhost:8080/upload/xxx.jpg自然找不到。解法是在 WebMvc 配置类里加一段映射:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:D:/project/upload/"); }注意 Windows 和 Linux 路径写法不一样,Linux 要用file:/开头,这样打包部署后也不容易踩坑。如果你的项目用 MINIO 这类对象存储,相当于天然解决了这个问题,因为访问路径是 OSS 提供的 URL,不需要走后端静态资源映射,这个方案在毕设上也是很好的加分解法。
5.3 跨域、事务、分页三个经典坑
跨域报错是前后端分离初体验的必经之路。Vite 开发环境下一般配代理最省心,但如果后端接口已部署到独立地址,前端直连就要后端开启 CORS。注意配置allowedOriginPatterns时,不要和allowCredentials(true)组合使用通配符*,浏览器会直接拒绝。正确做法是明确允许的前端地址,比如http://localhost:5173。
事务不生效也很隐蔽。同类中一个方法调用另一个事务方法时,事务会失效,因为 Spring 的声明式事务默认通过代理对象生效,类内部调用不走代理。解决方式有三种:拆到不同的 Service、注入自身代理、或者用TransactionTemplate手动控制事务。新手最容易在这种地方查半天。
MyBatis-Plus 分页失效也是个高频问题。低版本可以直接用Page对象,但高版本必须先把PaginationInnerInterceptor注册进MybatisPlusInterceptor,而且拦截器顺序有讲究,别把多个拦截器顺序搞错了。我之前遇到分页 SQL 不拼LIMIT,就是这个原因。
5.4 答辩演示数据的准备
这个坑不在代码里,在演示环节。很多人的系统跑起来列表空空荡荡,导师一看没有真实场景感,印象分直接减半。我建议初始化脚本里造一批带真实感的演示数据:20 个学生账号、6 个维修师傅、多个不同宿舍楼栋、覆盖各状态的工单,再配上一些已经完成的评价记录。这样打开系统的第一眼就是“有数据”的系统,统计图表也不至于空白。
封装一个数据初始化 CommandLineRunner,项目启动时检测到库为空就自动执行数据填充脚本,非常实用。造数据的时候注意楼栋、房间、报修类型的关联要合理,别出现 3 号楼的人报了 12 号楼的故障这种一眼假的情况。
6. 加分的功能设计与后续扩展
6.1 低成本亮点功能
毕设想要在答辩时让导师眼前一亮,不一定要上特别前沿的技术,把已有功能做到位更能体现工程思维。我用三个低成本功能提升了系统的完整度:
- 统计报表页:用 ECharts 展示每周报修数量趋势、报修类型占比饼图、师傅完工数量 Top 榜、平均响应耗时。后端只需要提供几个聚合查询接口,难度不大,但视觉效果和说服力很强。
- 操作日志记录:自定义注解 + AOP 切面统一记录关键操作,哪里都能看到“谁改了什么”。
- 定时任务催办:用
@Scheduled每天扫描超过 48 小时未处理的待派单工单,自动发送站内提醒,逻辑很少,但体现你会用系统化手段处理业务风险。
这三个功能背后都对应真实系统的常见诉求,答辩时每个都能展开讲故事。特别是统计报表,它把“工单闭环”的价值直接可视化——导师看了自然明白这个平台的业务价值。
6.2 如果还想再往前走一步
项目做完以后,如果学有余力,有两个方向可以考虑。一个是性能优化方向,引入 Redis 做热点数据缓存,例如报修类型字典、待处理工单列表,把数据库压力降下来;引入消息队列做异步通知,比如派单后推送提醒,不一定能在毕设里大规模用上,但在简历项目经验里写上“了解如何用消息中间件解耦核心链路”,会让技术深度上一个台阶。
另一个是流程抽象方向,当前状态流转逻辑分散在 Service 里,如果以后业务变复杂可以引入状态机模式或者 Flowable 工作流引擎来解决。对毕设来说这是过度设计,但面试官问“你这个工单系统怎么优化”时,你有这个备选答案,和没准备是完全不同的状态。
踩过不少坑之后的一点体会
项目做完回头看,最给我省时间的决定是在开工前把角色和状态机画明白了,后面所有代码都是顺着这个骨架长出来的。如果刚拿到题目就急着写增删改查,大概率中期改库改到崩溃。给准备拿这个题做毕设的同学一个建议:数据库表设计第一版尽量把日志表、逻辑删除、审计字段都加上,这些事后补的代价永远比一开始设计好要贵得多。另外,项目做到能跑通主干流程后,尽早造一批测试数据反复演示,把状态流转的边界情况多测几遍——工单系统最容易出问题的环节,永远是状态和时间,而不是 CRUD 本身。