
简介基于SpringBoot的维修工单系统是一份面向毕业设计、课程设计及Java全栈学习者的完整项目源码资源。系统覆盖工单创建、分配、执行、完成到反馈的全周期流程后端采用SpringBoot提供RESTful API与数据库交互前端使用Vue.js构建动态交互界面并包含权限控制、数据统计与报表生成等进阶功能适合用于理解前后端分离开发模式及企业级工单管理场景。资源包共55个文件主要包括33个Java源码文件、11个HTML页面、4个properties配置、2个XML配置以及README说明文档、Maven构建脚本等压缩包仅50KB整体结构清晰便于快速导入IDE运行与二次开发。目前已有61人学习下载。通过该资源可以获得完整的前后端工程代码、数据库配置说明、项目构建与部署指引帮助快速掌握SpringBootVue项目的实战开发技巧。1. 维修工单系统最难的从来不是增删改查SpringBoot在这里到底承担什么一个小团队或后勤部门最常用的内部工具可能就是报修和派单。用Excel登记、微信群喊一嗓子工单一旦多了漏处理、没人接、超时没人管几乎是必然结果。基于SpringBoot的维修工单系统要解决的不是把报修信息存进数据库这么简单而是把“谁提交、谁接单、处理到哪一步、卡在谁手里”这条状态链钉死让每一单都有据可查、有责可追。这也是这类系统最有价值的部分查得清、催得动、统计得出。对正在做毕业设计、课程设计或者打算在公司内部搭一套轻量报修系统的开发者来说这套SpringBoot方案是最常见也最容易上手的选择单体内聚、启动快、部署不挑环境配一个MySQL就能把整个业务跑起来。真正需要花心思的是状态机的设计、接口的权限边界以及上线之后那些“本地没事、部署就挂”的问题。接下来的内容会按照做这套系统的顺序从表结构讲到部署避坑全部是可复现的细节。2. 先把工单状态机画对表结构、角色权限与SpringBoot项目结构2.1 状态机与角色权限先定规则再写代码维修工单系统最容易出现的返工就是状态字段失控。有人用int随机填有人用字符串存中文还有人把状态变化直接写死在十几个Controller方法里。结果做到一半需求要加一个“待验收”状态几乎要把所有接口翻一遍。我现在的做法是在写任何代码之前先画一张状态流转表把可能出现的状态和允许的跳转路径固定下来。以最常见的维修流程为例状态可以定义成0待接单、1处理中、2待验收、3已结束、4已驳回、5已取消。合法流转是0→1接单、0→5用户取消、1→2维修工提交完成、1→4驳回、2→3用户确认结束、2→1验收不通过退回。这张表的价值在于Service层的updateStatus方法只需要一张Map或switch做合法性校验非法跳转直接抛业务异常绝不让Controller直接去setStatus。这样即使后面加了管理员改派、重新指派状态流的边界依然是清晰的。角色权限上内部系统不建议一上来就上Spring Security全家桶。工单系统的角色就三类普通用户提交工单、确认验收、维修工接单、处理、填写结果、管理员分派、驳回、看统计报表。用拦截器加一个简单的角色注解就能撑住。前后端分离的话在SpringBoot侧写一个HandlerInterceptor从请求头里拿token解析出userId和role放到ThreadLocal里Service层直接从上下文取当前操作人。等业务真的复杂到需要动态权限、细粒度数据权限的时候再迁移到Spring Security也不迟。2.2 三张核心表与SpringBoot项目结构工单系统的表可以做得很多但最小可用集是两张主表加一张日志表工单主表work_order、流程记录表work_order_log、附件表work_order_attachment。很多教程喜欢把所有字段塞进work_order一张表流程记录用备注字段存这在工单量小的时候没问题可一旦要统计“每个维修工平均处理时长”“驳回率”“超时率”没有独立的log表会非常痛苦。work_order表的核心字段包括id、order_no工单编号、title、description、reporter_id提交人、assignee_id指派人/维修工、status当前状态、urgent_level紧急程度、version乐观锁版本号、created_at、updated_at。索引上status和created_at联合索引基本够用因为列表页最常见的查询是“某状态下的工单按时间倒序”。work_order_log表记的是每次状态跳转order_id、operator_id、from_status、to_status、action、remark、created_at。这张表不参与业务写操作只做追加是后续统计和排障的底牌。CREATE TABLE work_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 工单编号, title varchar(100) NOT NULL COMMENT 报修标题, description text COMMENT 报修描述, reporter_id bigint NOT NULL COMMENT 提交人ID, assignee_id bigint DEFAULT NULL COMMENT 维修工ID, status tinyint NOT NULL DEFAULT 0 COMMENT 0待接单 1处理中 2待验收 3已结束 4已驳回 5已取消, urgent_level tinyint NOT NULL DEFAULT 0 COMMENT 0普通 1加急, 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, PRIMARY KEY (id), KEY idx_status_created (status, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT维修工单主表; CREATE TABLE work_order_log ( id bigint NOT NULL AUTO_INCREMENT, order_id bigint NOT NULL COMMENT 工单ID, operator_id bigint NOT NULL COMMENT 操作人ID, from_status tinyint DEFAULT NULL, to_status tinyint NOT NULL, action varchar(50) NOT NULL COMMENT 动作描述, remark varchar(255) DEFAULT NULL, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT工单流程记录表;这两张表创建之后对应的SpringBoot项目结构就清晰了。常见的做法是按业务模块分包而不是按技术层往死里分层。比如com.company.workorder下面直接分controller、service、mapper、entity、dto其中entity对应数据库表dto对应前端入参。这样单人维护或交接到下一任手里都只需要打开一个包就能看懂整个业务链路。项目里还要注意MyBatis-Plus的全局配置默认开启了驼峰映射也就是说order_no会自动映射到orderNo字段不用手写一堆resultMap。如果沿用我上面的表结构实体类里对应写Long id、String orderNo、Integer status、Integer version就行。写updateById的时候MyBatis-Plus会自动带上id条件但状态更新必须用更新的UpdateWrapper显式加上status条件避免把并发场景下的脏写带进系统这个在后面的避坑章节会展开说。3. 用SpringBoot把核心接口写出来自动装配、MyBatis-Plus与定时任务3.1 SpringBoot自动装配原理为什么加了starter就能跑很多同学第一次接触SpringBoot项目时最大的困惑是我只加了一个spring-boot-starter-web依赖为什么Tomcat就能起来、RestController就能用这背后是SpringBoot的自动装配机制也正是面试里高频的“springboot自动装配原理”。SpringBoot在启动时会扫描依赖jar包里的自动配置类根据当前classpath下有没有对应的类、有没有用户自定义的Bean来决定要不要把这个配置生效。SpringBoot 2.7及之前自动配置类声明在META-INF/spring.factories里从SpringBoot 3.0开始改成了META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。这个差异在升级版本的时候很容易踩坑如果你在别人的项目里看到spring.factories里有一大串AutoConfiguration但项目用的是SpringBoot 3.x那这个自动配置大概率不会生效。自定义自动配置类的模样核心就是下面这个框架AutoConfiguration ConditionalOnClass(WorkOrderService.class) EnableConfigurationProperties(WorkOrderProperties.class) public class WorkOrderAutoConfiguration { Bean ConditionalOnMissingBean public WorkOrderService workOrderService() { return new WorkOrderService(); } }这段代码的逻辑是当classpath下存在WorkOrderService时这个配置才有资格生效同时如果容器里已经有一个WorkOrderService的Bean就以用户自定义的为准不再重复创建。ConditionalOnClass和ConditionalOnMissingBean是自动装配里最常见的一对条件注解前者保证“依赖没到就不启动”后者保证“用户有自定义就不覆盖”。理解了这套机制再用到各种starter时就不会再觉得是黑匣子了。3.2 创建工单接口从Controller到Mapper的完整写法工单最核心的接口就是“提交工单”。这里要特别注意Controller只负责收参数和返回结果真正的状态初始化和流程记录必须放在Service层。常见的错误做法是Controller里直接new一个WorkOrder然后save还会漏写log后面查“这个单子谁什么时候提交的”就找不着北了。RestController RequestMapping(/api/order) public class WorkOrderController { PostMapping public ResultString create(RequestBody Validated CreateOrderDTO dto) { return Result.ok(orderService.create(dto)); } }Transactional(rollbackFor Exception.class) public String create(CreateOrderDTO dto) { WorkOrder order new WorkOrder(); order.setOrderNo(generateOrderNo()); order.setTitle(dto.getTitle()); order.setDescription(dto.getDescription()); order.setUrgentLevel(dto.getUrgentLevel()); order.setStatus(WorkOrderStatus.PENDING); order.setReporterId(SecurityUtils.getUserId()); this.save(order); OrderLog log new OrderLog(); log.setOrderId(order.getId()); log.setOperatorId(order.getReporterId()); log.setFromStatus(null); log.setToStatus(WorkOrderStatus.PENDING); log.setAction(提交工单); orderLogService.save(log); return order.getOrderNo(); }这里有两个参数值得细看。第一个是Transactional(rollbackFor Exception.class)默认情况下Spring事务只在RuntimeException时回滚如果业务方法里抛的是自定义的CheckedException不加rollbackFor会导致工单保存成功、日志写入失败两边数据不一致。第二个是generateOrderNo()工单编号我习惯用WO加年月日再加四位随机数比如WO202501150037避免直接用自增id暴露工单量同时排序也直观。CreateOrderDTO里放的是前端传过来的title、description、urgentLeveluserId不从前端拿从登录上下文取。这一点务必遵守否则用户改一下请求体就能冒充别人提交工单属于最基本的接口安全红线。3.3 定时任务与超时提醒让工单自动流转起来维修工单系统里一定要有定时任务不然“超时未处理”就永远只能靠人肉催。SpringBoot里做定时任务只需要两步启动类或配置类上加上EnableScheduling然后在方法上标注Scheduled。下面是一个典型的“超时未接单自动报警”任务扫描所有待接单且创建时间超过24小时的工单把状态标记为超时同时给管理员推送一条提醒。Component public class OrderTimeoutTask { Scheduled(fixedDelayString ${task.timeout.delay:60000}) public void scanTimeoutOrders() { ListWorkOrder list workOrderService.list( new LambdaQueryWrapperWorkOrder() .eq(WorkOrder::getStatus, WorkOrderStatus.PENDING) .lt(WorkOrder::getCreatedAt, LocalDateTime.now().minusHours(24)) .last(limit 200)); for (WorkOrder order : list) { order.setStatus(WorkOrderStatus.TIMEOUT); workOrderService.updateById(order); orderLogService.record(order.getId(), system, WorkOrderStatus.PENDING, WorkOrderStatus.TIMEOUT, 超时未接单); } } }这里fixedDelayString ${task.timeout.delay:60000}的意思是从配置文件读取task.timeout.delay读不到就用默认值60000毫秒。注意fixedDelay是上一次执行完成后再等60秒fixedRate是每60秒执行一次开始cron则按表达式走。对于扫描型任务fixedDelay更安全因为如果某一次扫描耗时较长不会出现两个任务重叠执行的情况。但要注意Scheduled默认只在单线程池里跑如果有多个定时任务互相阻塞就必须给SchedulingConfigurer单独配置线程池这个坑在避坑章节会展开讲。4. 从本地到服务器Vue打包放进SpringBoot与Docker部署的完整路径4.1 Vue打包放进SpringBoot前端刷新404怎么解决很多基于SpringBoot的维修工单系统前端用Vue做页面后端用SpringBoot中间通过接口通信。部署时最省事的方式是前端执行npm run build然后把dist目录里的文件全部复制到src/main/resources/static目录下和SpringBoot打成一个jar包。这样一个Tomcat端口同时服务前端页面和接口不用单独为前端开Nginx小项目完全够用。但这里有一个高频翻车点Vue如果开了history路由即URL里没有#号用户直接访问/order/list并刷新页面SpringBoot会认为这是个服务端请求结果返回404。原因是前端路由是浏览器端的跳转后端根本没有这个静态资源。常见做法是加一个视图控制器兜底把不包含点号的所有路径转发到index.htmlConfiguration public class WebMvcConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{path:[^\\.]*}) .setViewName(forward:/index.html); } }这段配置的含义是只要URL路径里不含点号排除静态文件就一律转发到index.html由Vue前端路由去解析。加了这段之后刷新页面不会再404同时接口路径如/api/order不受影响因为/api/order是Controller映射优先级高于视图控制器。接口和页面的路由冲突问题归纳起来就是一句话后端路由管/api前端路由管页面静态资源兜底转发。4.2 application.yml配置端口、数据库、文件上传这些最容易翻车的点application.yml是SpringBoot项目运维最需要抠细节的地方。维修工单系统的典型配置长这样server: port: 8080 servlet: context-path: / spring: datasource: url: jdbc:mysql://localhost:3306/work_order?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: ${DB_PASSWORD} servlet: multipart: max-file-size: 20MB max-request-size: 50MB mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这个配置里有几个细节值得较真。第一是数据库连接串里的serverTimezoneAsia/Shanghai不写这个参数MySQL驱动和JVM本地时区不一致会出现“存进数据库的时间比实际慢了/快了8小时”的玄学问题。第二是multipart配置不限制的话默认上传文件大小只有1MB用户传一张维修现场照片就报错。第三是password用${DB_PASSWORD}读取环境变量这个习惯比把明文密码写死在yml里强太多尤其项目要交给别人维护的时候。还要注意context-path这个参数。如果配了/server那么所有接口都要变成/api/order加上/server前缀前端打包进static后页面里的相对路径很容易因为前缀问题白屏。我的建议是小项目一律不配context-path保持根路径少一个变量就少一个坑。4.3 用宝塔Docker部署SpringBoot镜像构建与版本选择部署阶段最常见的落地做法是宝塔面板里的Docker管理器来构建和运行。先把项目打成jar包或者直接在服务器上用多阶段Docker构建Dockerfile写法如下FROM maven:3.8-jdk-11 AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests FROM openjdk:11-jre-slim WORKDIR /app COPY --frombuild /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app.jar]这个Dockerfile把构建和运行拆成了两个阶段第一阶段用Maven镜像编译出jar包第二阶段只拷贝jar到精简的JRE镜像里。这样最终镜像里没有Maven和源码体积能小一半以上。如果是SpringBoot 3.x的项目基础镜像要换成maven:3.9-jdk-17和eclipse-temurin:17-jreJDK版本对不上启动直接报UnsupportedClassVersionError。说到版本就一定要提“springboot版本太高”这个话题。SpringBoot 2.x和3.x之间不只是JDK版本要求不同包名也变了。3.x把javax.servlet换成了jakarta.servlet所有依赖这个坐标的老版本第三方库都会编译失败。MyBatis-Plus在SpringBoot 3.x下必须用3.5.3以上的版本否则启动报错。用宝塔部署时如果镜像里的JDK版本不对症状是启动一两秒就退出docker logs一查基本上都是版本问题这类问题排查起来最省时间的办法就是先确认SpringBoot版本、JDK版本、MyBatis-Plus版本三方匹配。5. 维修工单系统常见坑排查与避坑指南5.1 并发更新为什么两个接单操作会把工单状态改乱现象两个维修工同时点击“接单”两个请求都查到status0都执行setStatus(1)最后都返回成功。但实际只有一个应该成功另一个必须提示“工单已被接走”。原因updateById默认只以id作为更新条件。第一次更新成功后status已经变成1但第二个请求在点击时已经读到了旧状态发出的update语句因为没有status条件依然会把status覆盖为1并向日志里插入一条“接单”记录看起来两边都成功了。解决更新时显式带上status条件配合MyBatis-Plus的乐观锁Version。实体类的version字段加上Version注解后updateById会自动生成UPDATE work_order SET status1, versionversion1 WHERE id? AND version旧值。两个并发请求只有一个能命中version条件另一个影响行数为0在Service里检查int值即可判断是否抢单成功。这是维修工单系统上并发场景下最值得做的一道防护。5.2 定时任务不执行与文件路径消失现象本地IDE里启动定时任务每隔一分钟正常执行。打成jar包部署到服务器后到点不跑了或者跑了一次就再也不动。原因常见的有两种情况。第一种是启动类没有加EnableScheduling本地可能因为IDE里其他配置类间接开启了定时但jar包环境没有。第二种是多个Scheduled任务共用默认单线程池某个任务执行时间过长或抛出异常把调度线程阻塞了后面的任务全部卡死。要注意Scheduled方法如果抛出未捕获异常这个任务的调度不会恢复日志里可能什么都没留下。解决明确在启动类或独立配置类上写EnableScheduling同时把定时任务线程池调整到4个以上见前面第3章的配置代码。更稳妥的做法是每个定时方法内部用try/catch包住业务逻辑异常打到独立日志里绝不让异常往外穿透。文件上传路径消失则是另一个经典问题本地运行时把文件写到项目的uploads目录jar包部署后这个相对路径实际上指向了java命令运行的工作目录一旦用systemd或Docker启动这个目录的位置非常难找有时还在临时目录里重启后文件全没了。解决方式是文件存储路径不写相对路径读取配置项file.upload-dirDocker部署时通过挂载卷把宿主机的/data/work-order-files映射到容器内目录数据才留得下来。5.3 SpringBoot版本迁移与前端路由白屏现象项目从SpringBoot 2.x升级到3.x启动时报ClassNotFoundException报错信息里是javax.servlet.*相关类。原因SpringBoot 3.x使用了Jakarta EE 9规范原来的javax包名整体迁移到jakarta。项目里如果手动依赖了老版本的PageHelper、Druid、或某些工具库这些库编译时写死了javax坐标在3.x运行时就会找不到类。解决先升级这些第三方依赖。Druid要1.2.9PageHelper要5.3.2MyBatis-Plus要3.5.3。如果某个库已经不维护就果断换掉。还有一个实际经验SpringBoot 3.x最低要求JDK17如果服务器上只有JDK8想跑新版SpringBoot就需要先升级JDK这是一条硬性门槛不要试图绕过。前端路由白屏的坑也一样常见做完第4.1节的视图控制器转发之后要记得把static目录下的index.html里的静态资源路径改成相对路径或绝对路径。有些Vue项目默认的publicPath是/打出来的包在根路径访问没问题一旦部署在子路径或通过nginx转发到子目录页面会是白屏。把vue.config.js里的publicPath改成./最省心这样打包出来的引用路径都是相对路径放进SpringBoot的static目录后不管context-path怎么变都不会白屏。6. 给定时任务加上分布式锁一个必会的小技巧6.1 实现与验证如果工单系统只部署一个实例Scheduled完全够用。可一旦上了两台服务器做负载均衡同一个定时任务会在两个实例上各跑一遍超时工单会被重复扫描、重复记录日志用户会收到两遍一模一样的催办通知。这个问题的标准解法是给定时任务加分布式锁最常见的实现是Redis的SETNX。Component public class OrderTimeoutTask { Scheduled(fixedDelayString ${task.timeout.delay:60000}) public void scanTimeoutOrders() { String lockKey work_order:timeout_lock; String requestId UUID.randomUUID().toString(); Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, Duration.ofMinutes(1)); if (!Boolean.TRUE.equals(locked)) { return; } try { // 执行真正的超时扫描逻辑 doScan(); } finally { releaseLock(lockKey, requestId); } } }setIfAbsent是SETNX语义如果key不存在就设置成功并加过期时间存在则不做任何操作。requestId用来标识当前线程释放锁时不能直接del(lockKey)而是要用requestId做对校再删除防止“锁过期之后误删别人刚拿到的锁”。严谨的做法是用Lua脚本保证“取值、比较、删除”三步原子执行如果项目里没引入Redis也可以用数据库行锁SELECT * FROM work_order FOR UPDATE或者ShedLock框架但Redis方案是维护成本最低的。这个技巧我从翻车里学到的教训是单机环境下永远测不出分布式锁的问题。我当时用两个实例做验证一个实例正常跑另一个实例的日志里没有任何扫描输出说明锁的竞争逻辑生效了。如果你在测试环境没有条件起两个实例也可以在本地改两个端口同时启动同一份打包产物来观察看到只有一份日志打印就说明锁没有白写。这套逻辑往后不管做订单系统、库存系统还是审批系统凡是定时任务写状态变更我都会默认加一道锁。希望帮到你。本文还有配套的精品资源点击获取