
简介一套基于SpringBoot与Vue的维修工单系统完整源码面向毕业设计、课程设计及Java全栈学习者。系统实现工单创建、分配、执行、完成与反馈的全周期管理并集成权限控制、数据统计与报表生成功能后端提供接口服务前端通过Vue渲染动态交互界面还支持常用关系型数据库的配置与切换。压缩包共55个文件核心是33个Java后端源码和11个HTML前端页面另含4个properties配置、XML配置、mvnw构建脚本、CSS样式及说明文档整体约50KB结构清晰易读。目前已有61人学习下载适合希望通过实际项目快速掌握前后端分离开发与工单业务建模的读者。借助源码可学习接口编写、数据库操作、版本管理与项目构建等关键实践也可作为毕业设计或课程设计的直接参考。1. 一套 SpringBoot 维修工单系统解决的不只是“记一笔”维修报修这件事在很多公司还停留在“前台收电话、填一张 Excel、微信群里维修师傅”的阶段。工单状态全凭记忆领导问进度只能翻聊天记录月底统计耗时耗力。这套基于 SpringBoot 的维修工单系统解压 zip 后就是一套可以直接跑的 Web 应用核心是把报修、派单、接单、维修、验收这条链路从人治变成状态机驱动。它适合中小团队做内部运维工具、物业后勤、设备维保也适合开发者拿来做 SpringBoot 的练手和二次开发起步。接下来我从表设计讲到部署避坑全程按“能复现”的标准写你照着做就能把它跑起来。2. 先立业务骨架工单状态机与表设计2.1 状态机设计一张图看懂工单从出生到关闭维修工单系统最容易翻车的地方不是代码写不出来而是状态没设计好。常见做法是把工单生命周期拆成六个状态待派单、待接单、维修中、待验收、已完工、已取消。如果业务里还有返修就在“待验收”增加一条回到“维修中”的回退分支。待派单 - 待接单 - 维修中 - 待验收 - 已完工 | | | | v v v v 已取消 已取消 已取消 维修中(返修)这套流转规则决定了系统的所有业务逻辑所以必须写在代码里而不是散落在页面上。状态机的好处是任何人操作工单系统都知道当前在哪一步、下一步能去哪不会出现“订单还在维修中却被删了”这种低级事故。SpringBoot 工单系统的所有查询、统计、看板都依赖这一套状态字段。2.2 建表 SQL三张表把工单立起来核心业务围绕三张表工单主表、工单操作日志表、附件表。下面是我整理过的最小可用建表语句直接复制到 MySQL 8.0 以上即可-- 工单主表 CREATE TABLE work_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL COMMENT 工单编号格式如 WO20250101001, title VARCHAR(128) NOT NULL COMMENT 报修标题, description TEXT COMMENT 问题详细描述, category VARCHAR(32) COMMENT 报修分类水电/网络/办公设备/其他, priority TINYINT NOT NULL DEFAULT 2 COMMENT 1紧急 2普通 3低, current_status TINYINT NOT NULL DEFAULT 0 COMMENT 0待派单 1待接单 2维修中 3待验收 4已完工 5已取消, reporter_id BIGINT NOT NULL COMMENT 报修人ID, assignee_id BIGINT COMMENT 维修人ID派单时写入, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号防止并发重复操作, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_status_created (current_status, created_at), KEY idx_assignee_status (assignee_id, current_status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT维修工单主表; -- 工单操作日志表 CREATE TABLE work_order_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_id BIGINT NOT NULL, from_status TINYINT COMMENT 操作前状态, to_status TINYINT NOT NULL COMMENT 操作后状态, operator_id BIGINT NOT NULL COMMENT 操作人ID, remark VARCHAR(255) COMMENT 操作备注, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT工单流转日志; -- 工单附件表 CREATE TABLE work_order_attachment ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_id BIGINT NOT NULL, file_name VARCHAR(128) NOT NULL, file_path VARCHAR(256) NOT NULL COMMENT 相对路径不建议存绝对路径, uploader_id BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT工单附件;几个字段我特意加了注释解释一下设计考量。current_status用 TINYINT 而不是字符串是为了查询快、存储小同时配合 Java 里的枚举常量类避免魔法数字散落。version字段是乐观锁后面第 4 章写状态流转时会用到这个字段能直接拦住“两个维修工同时接同一张单”的并发问题。work_order_log是审计表每次状态变化都留痕这是工单系统跟普通 CRUD 最不一样的地方——出了问题要能追溯谁在什么时候把单子改了。2.3 为什么不用 update 直接改状态很多新手会写update work_order set current_status 4 where id ?看起来没问题但两个隐蔽问题会咬人。第一没有任何流转校验一个“已取消”的工单可以直接被改成“已完工”数据就脏了。第二缺少日志业务方来问“这单怎么突然完工了”你无从查起。所以正确的做法是所有状态变更必须经过同一个服务方法由状态机判断这次流转是否合法校验通过后才执行 update同时插入一条日志。我在实际项目里还见过一种偷懒写法直接把状态逻辑写在 Controller 里结果一个系统三个入口改状态的代码散落在四处后期维护非常痛苦。提示如果工单系统后续要做“超时未接单自动取消”“部门间转派”状态机设计得当这些功能只是加一条流转边而不是改一堆 if-else。3. 把 zip 里的 SpringBoot 工程跑起来项目结构与配置3.1 解压后先看结构两种形态一眼分清拿到 zip 先别急着mvn spring-boot:run先看项目根目录。市面上流传的基于 SpringBoot 的维修工单系统源码包绝大多数是 Maven 工程结构大致长这样work-order-system/ ├── pom.xml ├── src/main/java/ │ └── com/example/workorder/ │ ├── WorkOrderApplication.java │ ├── config/ # 跨域、拦截器、异步配置 │ ├── controller/ # 接口层 │ ├── service/ # 业务层 │ ├── mapper/ # MyBatis-Plus 或 MyBatis 的 Mapper 接口 │ ├── entity/ # 数据库实体 │ └── common/ # 统一返回、异常处理、常量 ├── src/main/resources/ │ ├── application.yml │ ├── mapper/ # XML 文件如果用纯注解可以没有 │ └── static/ 或 templates/ │ └── index.html └── sql/ └── init.sql这里有个关键分水岭resources下面是statictemplates说明是前后端不分离的 Thymeleaf 模板项目如果有一个独立的dist目录或者前端工程Vue 项目一起打包说明是前后端分离项目Vue 打包后的文件会被放进 SpringBoot 的static目录来托管。看 SpringBoot web 项目结构目录里的pom.xml依赖能确认它属于哪一种。我见过不少“基于 SpringBoot Vue 的项目”的 zip解压后有两个目录一个后端 SpringBoot 工程一个前端 Vue 工程。如果你拿到的是这种分离结构本地跑起来需要先npm install再npm run build把产物拷到后端的static目录或者通过后端配置的静态资源映射指向前端 dist 目录。3.2 运行前必改的配置数据源、端口、文件路径不管哪种形态application.yml是最先要动刀的地方。下面是常见配置模板加了对应的中文说明server: port: 8080 servlet: context-path: / spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/work_order?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 servlet: multipart: max-file-size: 20MB max-request-size: 200MB mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 自定义配置附件存储路径Linux 部署必改 upload: path: /data/work-order/uploadspring.datasource里的serverTimezoneAsia/Shanghai一定不要删否则 Java 8 时间类型和 MySQL 的 datetime 会差 8 小时。MyBatis-Plus 的map-underscore-to-camel-case打开后数据库的order_no才能自动映射到实体的orderNo。upload.path是自定义配置项很多 zip 包默认写死成D:/upload或/tmp/upload在 Linux 服务器上会导致上传瞬间成功、重启后文件消失。这里顺带提一下 SpringBoot 配置的加载优先级命令行参数 application-{profile}.yml application.yml。所以部署时可以用--spring.profiles.activeprod切换环境而不用改主配置。把数据库密码这类敏感信息放进 prod 配置里并让.gitignore忽略它是团队协作的基本习惯。3.3 启动与验证别只看“启动成功”四个字配置改完命令行进入项目根目录执行mvn spring-boot:run如果不想占用终端也可以打包后运行mvn clean package -DskipTests java -jar target/work-order-system.jar第一次启动建议用前一种方便看实时日志。看到Started WorkOrderApplication还不够你要确认三件事才能算真正起来了。第一日志里有没有Tomcat started on port 8080端口没被占用。第二MySQL 连接没报Access denied或Unknown database先去sql/init.sql建库。第三/api/health或登录接口能返回 JSON而不是 404。提示SpringBoot 自动装配原理在启动日志里会显示成一个很长的Positive matches列表。排查问题版本时用--debug参数启动能看到哪些自动配置被激活、哪些被排除这不只是面试常客实操排查也靠它。4. 工单流转关键实现状态机 Service、定时任务与权限4.1 状态机 Service把流转规则收敛成一个方法把状态流转规则收敛到一个 Service 方法是这个系统里最值得抄的一段代码。下面的实现用 Map 定义了合法流转路径配合乐观锁防止并发重复操作Service public class WorkOrderStateMachine { private final WorkOrderMapper workOrderMapper; private final WorkOrderLogMapper logMapper; public WorkOrderStateMachine(WorkOrderMapper workOrderMapper, WorkOrderLogMapper logMapper) { this.workOrderMapper workOrderMapper; this.logMapper logMapper; } /** * 定义每个状态允许流转到的目标状态 * 0待派单 1待接单 2维修中 3待验收 4已完工 5已取消 */ private static final MapInteger, SetInteger ALLOWED Map.of( 0, Set.of(1, 5), 1, Set.of(2, 5), 2, Set.of(3, 5), 3, Set.of(4, 2) // 验收不通过退回维修中 ); Transactional(rollbackFor Exception.class) public void transition(WorkOrder order, int targetStatus, Long operatorId, String remark) { SetInteger allowed ALLOWED.get(order.getCurrentStatus()); if (allowed null || !allowed.contains(targetStatus)) { throw new IllegalStateException(非法的工单状态流转 order.getCurrentStatus() - targetStatus); } WorkOrder updated workOrderMapper.updateStatusAndVersion( order.getId(), order.getCurrentStatus(), targetStatus, order.getVersion()); if (updated null) { throw new ConflictException(工单已被其他人处理请刷新后重试); } logMapper.insertLog(order.getId(), order.getCurrentStatus(), targetStatus, operatorId, remark); } }对应 Mapper 里的更新语句用version做乐观锁条件update idupdateStatusAndVersion UPDATE work_order SET current_status #{targetStatus}, version version 1 WHERE id #{id} AND current_status #{fromStatus} AND version #{version} /update这段代码的逻辑说明先校验状态路径是否合法再执行条件更新最后写日志。updateStatusAndVersion返回受影响行数如果返回 0说明工单状态或版本号在本次操作前已经被别人改过直接抛异常拒绝操作。这个机制能把“两个维修工同时点接单”变成只有一个赢家。我一般要求团队所有变更操作必须走这个 Service禁止在 Controller 里直接调 Mapper 改状态。4.2 超时提醒SpringBoot 定时任务扫描待处理工单维修工单系统必然要面对“没人接单”“超时未处理”的场景。SpringBoot 定时任务实现起来非常轻在启动类加EnableScheduling然后写一个扫描任务。下面这个例子每 5 分钟扫一次超时 30 分钟还没人接的单子发站内通知Component public class TimeoutReminderTask { private final WorkOrderMapper workOrderMapper; private final NoticeService noticeService; // 每 5 分钟执行一次0 0/5 * * * ? Scheduled(cron 0 0/5 * * * ?) public void remindUnassignedOrders() { ListWorkOrder timeoutOrders workOrderMapper.selectTimeoutOrders( LocalDateTime.now().minusMinutes(30), 1); // 1: 待接单 for (WorkOrder order : timeoutOrders) { noticeService.sendToManager(order, 工单 order.getOrderNo() 超时未接单); } } }cron表达式里0 0/5 * * * ?比0 */5 * * * ?更稳妥前半段精确到秒避免整点合并执行。selectTimeoutOrders的 SQL 条件要写assignee_id IS NULL AND current_status 1 AND created_at #{deadline}只扫待接单状态别把“维修中”的也拉出来重复提醒。这个定时任务有个多实例部署的坑如果系统上了两台服务器同一时间会执行两次用户收到两条重复通知。解决常见做法是引入 ShedLock 做分布式锁或者先只部署单实例。内部工具系统单实例完全够用别为了高可用提前背上分布式复杂度。4.3 权限边界防止维修工自己验收自己的单工单系统的权限不是简单的“管理员/普通用户”两级至少要拆成四种角色报修人、派单员、维修工、管理员。关键权限规则我用一张表列出来操作报修人派单员维修工管理员创建工单允许允许允许允许派单/改派否允许否允许接单否否允许允许标记完工否否允许允许验收允许允许否允许取消工单仅待派单任意未完成否允许验收这一步要特别留意如果维修工自己既能“完工”又能“验收”他就能绕过把关把单子直接关掉。所以即使代码里状态机允许“维修中 - 待验收 - 已完工”权限层也必须卡住“验收人不能是当前指派维修工”。在 Controller 里加一个PreAuthorize(hasRole(APPROVER))只解决了一半我一般在 Service 方法里额外校验当前用户和assignee_id不一致才放行因为权限注解在某些内部调用的场景会跳过。5. 避坑排查把 zip 变成稳定服务的实战笔记5.1 五个高频坑从现象到解决坑一SpringBoot 版本太高Thymeleaf 页面样式全丢。现象解压后项目用 Spring Boot 3.2 运行登录页能出来但 CSS/JS 全部 404。 原因Spring Boot 3.x 把静态资源拦截规则改了WebSecurityConfigurerAdapter被废弃部分老工程的安全配置没有放行/static/**和/css/**。 解决升级安全配置为SecurityFilterChain方式显式放行静态资源如果只是内部工具不想搞安全框架直接把spring-boot-starter-security从 pom 里去掉反而更省事。坑二LocalDateTime 序列化报错前端时间显示成“数字”或直接接口 500。现象工单列表接口有时成功有时失败报错Cannot deserialize value of type java.time.LocalDateTime。 原因工程里同时用了 FastJson 和 Jackson或者 MySQL 驱动版本和 Java 时间类型不匹配。 解决统一用 Jackson在application.yml里加spring.jackson.date-formatyyyy-MM-dd HH:mm:ss和spring.jackson.time-zoneGMT8。如果代码里用了JSONField注解把它全改成JsonFormat。坑三定时任务不触发日志里安静得像没这回事。现象Scheduled方法写好了启动也没报错但就是不在预期时间执行。 原因80% 是漏了EnableScheduling或者定时任务类没被 Spring 扫描到。 解决启动类上补EnableScheduling确认任务类在启动类的子包下。还有一种是 cron 表达式写错了Spring 的 cron 是 6 段不是 Quartz 的 7 段多写一位秒位会导致启动时直接报错。如果连报错都没有用/actuator/scheduledtasks端点检查任务注册状态。坑四上传图片显示 404重启后文件全丢。现象Windows 本地上传正常Linux 部署后上传成功但访问图片 404服务器重启后上传的文件全部消失。 原因我把这个叫“玄学”其实一点都不玄——配置里写的是相对路径/upload/Linux 下被解析到临时目录/tmp重启清空。 解决upload.path配成绝对路径如/data/work-order/upload并确保目录存在且有写权限。在config里注册ResourceHandler把/upload/**映射到这个物理路径。坑五并发接单两个人同时操作一张单没人报错。现象两个维修工同时点击接单都显示成功工单状态被后提交的人覆盖。 原因update 语句没有带状态条件最后一个写库的人赢。 解决用第 4 章的乐观锁方案。这是我在这个项目里吃得最亏的一课最初没加version上线两周后就出现一次抢单事故然后老老实实补了条件更新。注意凡是直接抄来的 zip 项目上线前必须检查所有 update 语句是否带状态条件。一个没有并发保护的工单系统平时没事一忙就翻车。5.2 排查手段先看自动装配和 SQL 日志遇到问题不要瞎猜按顺序推进。第一层看启动日志的Positive matches和Negative matches确认数据库、定时任务、文件上传相关的自动配置有没有被激活。第二层看 MyBatis 的 SQL 日志zip 包里如果没开StdOutImpl在application.yml里加上再运行看真实执行的 SQL 和参数。第三层看异常堆栈的首行绝大多数坑在“Caused by”之后的第一个原因里别被外层大类报错带偏。举个例子如果报Invalid bound statement (not found)那不是 SpringBoot 的锅是 Mapper XML 没编译到 target 目录或者mapper-locations路径配错。用mvn clean package清理再启动90% 能解决。5.3 上线前必走的回归路径我给这套系统做上线检查时会照着下面这条路完整走一遍创建工单 - 派单员派单 - 维修工接单 - 维修工填维修记录并标记完工 - 报修人验收通过 - 工单变成已完工。然后反向测试创建工单 - 派单 - 维修工接单 - 尝试从“维修中”直接改成“已完工”。最后测试异常分支维修工点击接单时另一个浏览器先改状态看后点击的人是否收到友好提示。这条路径走完系统正常与否基本一目了然。其中“从维修中直接改成已完工”是状态机最容易漏掉的口子如果代码里ALLOWEDMap 定义完整服务端会直接拒绝。6. 二次开发前先做验证状态机覆盖与 Vue 打包部署技巧拿到这种 zip 项目先别急着加功能花一个小时把状态机覆盖度验证一遍。我的习惯是写一段 JUnit 集成测试把上节 5.3 的路径固化成用例用SpringBootTest拉起真实 Spring 容器。测试用例里专门断言非法流转会抛IllegalStateException这样以后任何人改状态机代码跑一遍测试就能发现哪里漏了。部署层面前后端分离的工程经常用到 “Vue 打包放进 SpringBoot 中” 的技巧。前端npm run build后把dist目录下的文件复制到后端src/main/resources/static重新打 jar 包即可。Vue 的路由要改成history模式的兼容写法否则刷新页面会 404。更省心的做法是后端加一个WebMvcConfigurer把非/api开头的请求都转发到index.html这样前端路由在 SpringBoot 内嵌 Tomcat 下也能正常工作。提示如果工程很大不要每改一次前端就复制一次 dist。开发阶段把 Vue dev server 跑在 8081后端配CrossOrigin或代理联调通过后再打包合并。最后说一点我个人的教训最早接手这种 zip 工程时我第一件事是打开业务代码找炫技点结果在部署配置上耗了一整天。现在拿到任何 SpringBoot 项目我会先花十分钟看pom.xml版本、application.yml配置、数据库脚本这三样东西决定了系统能不能在当前环境活下来。技术从来没那么多玄学坑都在配置和边界里。希望这套从状态机设计到部署避坑的路径能帮你把这个维修工单系统稳稳跑起来少走我走过的弯路。本文还有配套的精品资源点击获取