
每年到了毕业设计季“报修管理系统”这类题目就会准时出现在导师给的选题清单上。我做这个校园宿舍报修平台之前其实先调研了一圈实际情况——学校里报修基本靠微信群接龙、宿管手写登记或者直接在楼栋群里口头喊一嗓子三个问题特别典型信息被聊天记录刷掉、维修进度完全靠猜、事后想统计都找不到数据。所以这个项目干脆用 SpringBoot Vue 做了一套前后端分离的维修工单系统把在线报修、后台派单、师傅接单、进度跟踪、完工验收整条链路串成一个闭环。这篇内容会把项目核心设计、技术选型思路、数据库字段、状态流转和踩坑记录都拆开讲适合正在准备 Java 方向毕业设计或者想练手完整前后端分离项目的同学参考。1. 这个项目到底在解决什么问题1.1 报修平台的本质是工单生命周期管理宿舍报修听起来简单无非是“学生报一下、师傅去修一下”但如果直接按这种直觉去设计数据库后面基本会翻车。换个角度想一条报修记录从创建到最后归档状态一直在变——刚提交是待处理管理员派给师傅是待接单师傅开始动手是维修中修好以后要等学生确认学生验收通过才算真正结束中间还可能出现师傅临时退回、材料不够挂起、学生撤销等情况。这其实就是一个典型的工单生命周期管理问题跟快递物流很像你下单、商家发货、快递运输、派送、签收每一步都有一个可查询的状态。工单系统要做的事情就是把这套状态流转和角色操作对应起来我这个项目里核心状态定义成六种每种状态谁可以操作、可以流向哪些状态都要提前定死。状态值中文含义谁可以触发可流向PENDING待派单学生提交后自动进入ASSIGNED、CANCELLEDASSIGNED已派单待接单管理员派单触发PROCESSING、REJECTEDPROCESSING维修中师傅接单后触发FINISHED、PENDINGFINISHED待验收师傅提交完成结果触发COMPLETED、REJECTEDCOMPLETED已完成学生确认验收触发无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核心业务逻辑状态流转、派单规则都写在这里。mapperMyBatis-Plus 的数据访问层。entity数据库表对应的实体类。dto接口入参对象避免直接用实体类接收前端数据。vo返回给前端的视图对象隐藏掉不必要字段。common统一返回结果、异常处理、工具类。configWebMvc 配置、跨域配置、文件存储配置。对比另一个常见的技术选择Spring Data JPA 直接操作对象更方便但复杂条件查询和分页反而不够透明。MyBatis-Plus 的优势在于既能通过 BaseMapper 快速实现单表 CRUD又保留了手写 SQL 的灵活性动态查询条件用 LambdaQueryWrapper 也很方便。毕设场景下数据量不大、查询逻辑也不复杂MyBatis-Plus 是最稳的选择。3. 数据库设计是成败的关键3.1 核心表设计逐字段说明这个项目的核心表我最终收敛成了六张左右业务主表是维修工单表另外有用户表、报修类型字典表、派工记录表、工单操作日志表、楼栋宿舍信息表。先说最重要的work_order工单表。字段名类型说明idbigint主键自增或雪花算法order_novarchar(32)工单编号对外展示用user_idbigint报修人 IDrepair_type_idbigint报修类型水、电、木工等dorm_buildingvarchar(32)宿舍楼栋dorm_roomvarchar(32)房间号descriptionvarchar(500)故障描述image_urlvarchar(200)现场图片路径statusvarchar(20)当前状态存字符串枚举assignee_idbigint当前处理师傅 IDprioritytinyint紧急程度finish_timedatetime用户确认完成时间deletedtinyint逻辑删除标记create_timedatetime创建时间update_timedatetime更新时间派工记录表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之前必须确认这个工单确实是派给自己的。所以接口入参要带上工单 IDService 里除了判断状态还要判断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 本身。