先说我做完这个项目的整体感受:电脑维修行业的工单系统,看着简单,实际做起来比想象中复杂得多。最核心的不是CRUD,而是工单状态怎么流转、角色权限怎么隔离、一个工单从报修到完结怎么做到全程留痕。这篇博文我不打算讲那种"从零搭建"的流水账,而是把它们按业务问题拆开讲——包括状态机的设计思路、数据库表结构的取舍、接单和完结环节里容易踩的坑,以及最后部署演示时怎么做到让现场演示不翻车。整个项目源码、数据库脚本和部署文档我放在文末,可以直接拉下来跑。
1. 先想清楚:电脑维修工单系统到底在解决什么问题
很多同学一看到"电脑维修工单接单完结管理系统"这个题目,第一反应就是"不就是个增删改查吗"。这话对了一半,但恰恰是另一半决定了这个项目能不能在答辩时站得住脚。
1.1 维修行业线下流程的真实痛点
我们模拟一个很常见的场景:某公司的IT部门或者一家电脑维修门店,每天会接到大量报修请求——可能是同事的电脑开不了机,也可能是客户的笔记本需要换屏幕。如果没有系统,整个流程是这样的:客户打电话或者发微信描述故障,客服手写登记在一个本子上,然后喊一声"谁有空去处理一下",工程师接单后可能忘了记录维修结果,最后客户也不知道找谁确认、怎么验收付款。整个过程里至少有四个问题:
- 状态不可见:工单派给谁了、修到哪一步了、有没有完结,全靠人脑记忆。
- 责任难追溯:同一台设备多次维修,谁修的、什么时候修的、怎么修的,没有任何记录。
- 数据无法沉淀:哪些故障类型最多、哪个工程师处理效率高、平均修复时长多少,全是空白。
- 接单/完结容易扯皮:客户说没修好,工程师说已经修完了,两边各说各话。
所以这个系统的核心价值,不是"把纸上的记录挪到电脑里",而是把一整条业务链路——报修、派单、接单、维修、验收、完结——用状态和数据串起来,让每一笔工单都有迹可循。
1.2 从用户视角梳理角色与核心流程
搭建系统之前,我建议先画一张最原始的业务流程图,不要直接画表结构。这个系统的角色非常清晰,一共三类:
| 角色 | 核心操作 | 典型场景 |
|---|---|---|
| 客服/管理员 | 创建工单、指派工程师、调整状态 | 接到报修电话,录入设备故障信息,派给指定工程师 |
| 维修工程师 | 接单、填写维修反馈、提交完工 | 收到待接单列表,接下工单,上门或取件维修 |
| 客户/送修人 | 查看进度、确认验收、评价 | 报修后等待处理,维修完成确认无误并评价 |
核心流程闭环是这样的:客户报修 → 客服创建工单 → 派单(或由工程师抢单) → 工程师接单 → 维修中 → 工程师提交结果 → 客户/客服验收 → 工单完结 → 评价归档。这里面最关键的设计决策是:工单状态不能只搞一个"进行中/已完成",必须把每个环节都变成可追踪的状态节点。原因很简单——状态越多,越能定位问题出在哪个环节,这也是答辩时一个很拿得出手的设计亮点。
2. 技术选型:为什么是SpringBoot + MyBatis Plus这套组合
项目题目里已经指定了SpringBoot,这是目前Java后端开发事实上的标准框架,没什么好纠结的。但具体到持久层、鉴权方案、前端模板,还是有几个关键选择想说一下。
2.1 后端框架与版本选择
我用的是SpringBoot 2.7.x,配合MyBatis Plus 3.5.x。为什么不选SpringBoot 3.x呢?主要是JDK版本兼容问题。SpringBoot 3要求JDK 17,而很多学校的教学环境和学生本机装的还是JDK 8,部署到服务器也容易出现各种依赖不兼容的问题。SpringBoot 2.7 + JDK 8的组合非常成熟,网上资料多、报错少,对毕业设计这种"稳定压倒一切"的项目来说是更稳妥的选择。
MyBatis Plus和原生MyBatis的区别,一句话总结:单表CRUD不用写SQL,复杂查询还是得写XML。工单管理里涉及大量分页查询、条件检索、多表联查,MyBatis Plus的Wrapper机制能省掉至少一半的样板代码。比如工程师端查看"我的待接单列表",一个LambdaQueryWrapper就搞定了:
// 查询状态为待接单、且没有被指派的工单(供抢单), // 或者是指派给当前工程师的工单 LambdaQueryWrapper<WorkOrder> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(WorkOrder::getStatus, WorkOrderStatus.PENDING_ACCEPT.getCode()) .and(w -> w.eq(WorkOrder::getAssignEngineerId, currentUserId) .or() .eq(WorkOrder::getAssignType, AssignType.GRAB.getCode()));2.2 前端方案:服务端渲染还是前后端分离
这个题目下有同学用Thymeleaf做服务端渲染,也有同学做Vue + SpringBoot前后端分离。我个人建议:如果时间充裕且前端基础还行,选前后端分离;如果只想把核心业务逻辑做扎实,Thymeleaf更稳。
原因在于,工单系统的核心价值在后端业务规则,不在前端的视觉效果。前后端分离会引入跨域配置、Token鉴权联调、Vue打包部署等一系列额外问题,这些问题很容易在答辩时成为减分项。我自己这次用的是Vue 2 + Element UI + Axios的前后端分离方案,因为演示时页面确实更好看,交互也更流畅。如果你选择这条路,有几个点提前处理好:
- 跨域配置:在SpringBoot里写一个CorsConfig,允许指定Origin跨域请求,别在这一步卡太久。
- Token传递:Axios请求拦截器里统一从localStorage取Token,加到请求头。
- 部署形态:前端
npm run build后把dist目录放到Nginx下,或者直接扔进SpringBoot的static目录里做一个合并部署。演示环境建议合并部署,少一套Nginx配置,少一堆麻烦。
2.3 为什么不用Shiro而选Spring Security + JWT
做权限控制的时候,我纠结过Shiro还是Spring Security。很多Java毕设用的都是Shiro,因为配置简单、资料多。但我最后还是选了Spring Security + JWT的无状态鉴权方案。理由有两个:一是Spring Security毕竟Spring家族官方生态,面试时讲起来更有含金量;二是工单系统的权限控制其实不算复杂,走JWT过滤器链路,代码量也就那么几个类。
安全这块要覆盖的链路包括:登录认证、Token签发、接口访问控制、角色权限校验。具体的实现方案在第五节我会展开讲,这里先给一个完整的依赖清单:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>3. 数据库设计:工单流转的核心表结构与字段规划
我的习惯是先想清楚业务表之间的关系,再动手写代码。工单系统核心其实就五张表:用户表、角色表、工单表、工单日志表(状态变更记录)、评价表。再加上一些辅助表比如故障类型字典表。这里重点讲工单表和工单日志表,因为这两个表的设计水平直接决定了系统上限。
3.1 工单主表的核心字段设计
工单表是整个系统的核心,字段设计要围绕"这一个工单从哪来、到哪去、谁来处理、处理结果如何"展开。我设计的表结构大致如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| order_no | varchar(32) | 工单编号,格式如WO20250101001 |
| device_type | varchar(64) | 设备类型(笔记本、台式机、显示器、打印机等) |
| device_brand | varchar(64) | 设备品牌 |
| device_model | varchar(64) | 型号 |
| fault_desc | varchar(500) | 故障描述(客服录入) |
| fault_type | varchar(32) | 故障类型(硬件、软件、网络、外设等) |
| urgency_level | tinyint | 紧急程度(1普通、2加急、3特急) |
| customer_name | varchar(32) | 报修人姓名 |
| customer_phone | varchar(20) | 联系电话 |
| customer_address | varchar(255) | 上门地址 |
| assign_type | tinyint | 派单方式(1管理员指派、2工程师抢单) |
| assign_engineer_id | bigint | 指派的工程师ID,可空 |
| status | tinyint | 工单状态(见下方状态机) |
| total_fee | decimal(10,2) | 维修费用 |
| start_time | datetime | 接单时间 |
| finish_time | datetime | 完工时间 |
| complete_time | datetime | 完结时间 |
| remark | varchar(500) | 备注 |
| deleted | tinyint | 逻辑删除标记 |
重点说几个细节。order_no工单编号这个字段,不要用自增ID直接展示给客户看,因为客户会看到你的单量,这不专业。应该用"WO + 年月日 + 三位流水号"这种格式,比如WO20250607001。生成逻辑用数据库查询当天的最大单号再加一,或者用Redis自增,我这边因为不想多引入Redis依赖,用的就是数据库查询方式,单量不大的场景完全够用。
deleted逻辑删除字段是MyBatis Plus的标配,工单数据很重要不能物理删除,但默认的@TableLogic要注意一点——它会自动给所有查询加上deleted=0的条件,如果你写了自定义XML联表查询,一定要记得手动带上这个条件,否则会查出已删除的数据。
urgency_level紧急程度这个字段容易被忽略,但它是后续列表排序的重要依据。工程师端接单列表默认就按"紧急程度降序 + 创建时间升序"排序,这样最紧急的单子最先被看到。一个字段就能体现你的业务思考深度,答辩时值得主动提一句。
3.2 工单日志表:全程留痕的底气
一个完整的工单系统,光有最终状态是不够的。客户问"我这个单子现在到哪一步了",工程师说"我记不清了",客服想查"上次是谁改的状态",这些查询需求都指向同一个答案:日志表。
工单日志表设计大概是这样:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_id | bigint | 工单ID |
| operator_id | bigint | 操作人ID |
| operator_name | varchar(32) | 操作人姓名(做一个冗余,方便展示) |
| action | varchar(32) | 动作(CREATE、ASSIGN、ACCEPT、FINISH、VERIFY、COMPLETE、CANCEL) |
| from_status | tinyint | 操作前状态 |
| to_status | tinyint | 操作后状态 |
| content | varchar(500) | 操作内容描述 |
| create_time | datetime | 操作时间 |
这个表的逻辑很简单:每次工单状态发生变更,就往日志表里插一条记录。它带来的直接收益是:工单详情页可以完整展示时间线,从"创建工单"到"派单给张三"到"张三接单"再到"完工验收",每一步都有操作人和时间。这在答辩演示时是非常有视觉冲击力的功能,而且代码实现并不复杂——只需要在Service层做一个统一的状态变更方法,在这个方法里完成"更新状态 + 写日志"两个动作。
我实际写代码时把这两个操作封装在一个事务方法里:
@Transactional(rollbackFor = Exception.class) public void changeStatus(WorkOrder order, int toStatus, String action, String content) { int fromStatus = order.getStatus(); // 状态流转校验 WorkOrderStateMachine.checkTransition(fromStatus, toStatus); // 更新工单状态 order.setStatus(toStatus); workOrderMapper.updateById(order); // 插入日志 WorkOrderLog log = new WorkOrderLog(); log.setOrderId(order.getId()); log.setOperatorId(SecurityUtils.getCurrentUserId()); log.setOperatorName(SecurityUtils.getCurrentUserName()); log.setAction(action); log.setFromStatus(fromStatus); log.setToStatus(toStatus); log.setContent(content); workOrderLogMapper.insert(log); }这个changeStatus方法后来成了整个系统的核心,所有状态流转都走它,日志一条不漏。这是我在项目里最满意的一个设计,没有之一。
4. 核心流程实现:从报修到完结的状态机设计
工单系统的灵魂,就是那套状态机。如果只做CRUD,状态字段随便写写就行;但要把业务做严谨,必须先定义状态、再定义事件、最后限定每个状态下能触发哪些事件。
4.1 状态定义与流转规则
我把工单状态定义为七个节点:
- 待派单(PENDING_ASSIGN):客服已创建工单,但还没有指派工程师或进入抢单池。
- 待接单(PENDING_ACCEPT):工单已派给某个工程师,或者进入抢单池等待工程师认领。
- 维修中(REPAIRING):工程师已接单,正在进行维修或上门途中。
- 待验收(PENDING_VERIFY):工程师提交完工结果,等待客户或客服确认。
- 已完结(COMPLETED):验收通过,工单正式关闭。
- 已取消(CANCELED):客户取消报修或超时未处理。
- 已退回(RETURNED):验收不通过,退回给工程师重新处理。
状态之间的流转关系,用文字描述会比较繁琐,但代码里可以用一个Map来做合法性校验。我在实体类里定义了一个状态机的工具类,本质上就是一个合法的状态转移表:
public class WorkOrderStateMachine { private static final Map<Integer, Set<Integer>> TRANSITIONS = new HashMap<>(); static { // 待派单可以派单(进入待接单)、可以取消 TRANSITIONS.put(WorkOrderStatus.PENDING_ASSIGN.getCode(), Set.of(WorkOrderStatus.PENDING_ACCEPT.getCode(), WorkOrderStatus.CANCELED.getCode())); // 待接单可以接单(进入维修中)、可以取消 TRANSITIONS.put(WorkOrderStatus.PENDING_ACCEPT.getCode(), Set.of(WorkOrderStatus.REPAIRING.getCode(), WorkOrderStatus.CANCELED.getCode())); // 维修中可以提交完工(进入待验收) TRANSITIONS.put(WorkOrderStatus.REPAIRING.getCode(), Set.of(WorkOrderStatus.PENDING_VERIFY.getCode())); // 待验收可以通过(进入已完结)、可以退回(回到维修中) TRANSITIONS.put(WorkOrderStatus.PENDING_VERIFY.getCode(), Set.of(WorkOrderStatus.COMPLETED.getCode(), WorkOrderStatus.REPAIRING.getCode())); } public static void checkTransition(int from, int to) { Set<Integer> allowed = TRANSITIONS.get(from); if (allowed == null || !allowed.contains(to)) { throw new BusinessException("非法状态流转:" + from + " -> " + to); } } }这套状态机的价值在于,它把状态流转规则集中在一处,无论前端怎么调用接口、无论用户点击多少次按钮,后端都能保证状态不会出现非法跳转。比如一个"待派单"的工单,任何人都不可能跳过接单直接把它改成"维修中"。这种设计体现了对业务边界的思考,在答辩时效果非常好。
4.2 接单环节的两种模式:指派与抢单
接单环节我设计成了两种模式,也是考虑到实际维修业务的不同场景。
指派模式:客服在创建工单时直接选择某个工程师,工单进入该工程师的"待接单"列表,只有这个工程师能接单。这种模式适合"客户指定要某人来修"或者"某个工程师恰好熟悉这类设备"的场景。
抢单模式:客服只录入故障信息,不指定工程师,工单进入公共抢单池,所有工程师都能看到,先接单者得。这种模式适合业务量大的维修团队,能最大化利用工程师的空闲时间。
抢单最怕的并发问题是两个工程师同时抢同一张单。我用的方案是数据库乐观锁:接单时用一条带条件更新的SQL,只有当status = 待接单时才能更新为维修中,影响行数为1表示抢单成功,为0表示已被别人抢走。
UPDATE work_order SET status = 3, assign_engineer_id = #{engineerId}, take_time = NOW() WHERE id = #{orderId} AND status = 2 AND (assign_engineer_id = #{engineerId} OR assign_type = 2)这个SQL里status = 2是条件,如果两条线程同时执行,数据库行锁保证只有一个更新成功,另一个影响行数为0,我就提示"手慢了,工单已被其他工程师接走"。
4.3 完结前的验收环节:防止扯皮的关键
完结环节是整个系统最容易被忽略,但实际最重要的地方。如果工程师提交完工后直接就把工单状态改成"已完结",会出现一个问题:客户可能根本没确认过维修效果。
所以在我的流程里,工程师提交完工后,工单进入"待验收"状态,不会直接完结。此时客户或客服可以在系统里看到维修详情和费用,确认无误后点击"验收通过",工单才正式变成"已完结"。如果客户觉得没修好,可以点"退回"让工程师重新处理,工单回到"维修中"。
这个"先验收、后完结"的设计,真正意义上解决了1.1节里说的"客户说没修好,工程师说修完了"的扯皮问题。而且验收环节顺理成章地引出了评价表——工单完结之后,客户可以对本次维修进行打分和评价,所有评价数据沉淀下来就是工程师的服务质量画像。
5. 权限与角色隔离:三种用户各管一摊的权限模型
权限设计这件事,很多毕设项目都是"做个登录、判断一下角色就完事"。但工单系统里有一个很容易被忽视的点:光有角色还不够,还要做数据隔离。
5.1 基于RBAC的菜单与接口权限
RBAC(基于角色的访问控制)是最经典的权限模型,工单系统用这个模型非常合适。我设计了三个角色:管理员(admin)、客服(service)、工程师(engineer)。其中管理员拥有全部权限,客服拥有工单创建、指派、验收等权限,工程师只有接单、维修反馈、提交完工等权限。
实现层面我走的是Spring Security的过滤器链 +@PreAuthorize注解。核心流程是用户登录后签发JWT,JWT里带上用户的角色标识和用户ID;后续每个请求都经过JWT过滤器解析出用户信息,放到SecurityContext里;接口层面再用注解做二次校验,比如:
@PreAuthorize("hasRole('ENGINEER')") @PostMapping("/accept") public Result acceptOrder(@RequestBody AcceptOrderDTO dto) { ... }菜单权限这一层,我是从数据库里查角色对应的菜单列表,返回给前端动态渲染侧边栏。Element UI的侧边栏是根据路由表生成的,但不同角色看到的菜单项不一样,前端需要在路由守卫里根据后端返回的菜单权限做过滤。这个也不算复杂,花半天时间就能调通。
5.2 数据权限的双层过滤:工程师只能看自己的工单
说个很多项目都容易翻车的细节。假设一个工程师登录系统后,通过直接访问接口的方式,把他的assignEngineerId参数改成别人的ID,会发生什么?如果后端只校验了"你是否登录、是否工程师",但没有进一步校验"这个工单是否属于你",就会出现越权访问——工程师能看到别人的工单,甚至修改别人的工单状态。
我的解决方案是在Service层做数据级权限过滤,核心思路是:当前登录用户的信息不从请求参数里拿,而是从SecurityContext里拿。工程师接单、查看工单详情、提交完工这些操作,我不会相信前端传的"engineerId",而是用SecurityUtils.getCurrentUserId()获取真实登录者的ID:
public OrderDetailVO getOrderDetail(Long orderId) { WorkOrder order = workOrderMapper.selectById(orderId); // 校验当前登录用户是否有权查看此工单 boolean canView = false; if (SecurityUtils.isAdmin()) { canView = true; } else if (SecurityUtils.isEngineer()) { canView = order.getAssignEngineerId().equals(SecurityUtils.getCurrentUserId()); } else if (SecurityUtils.isCustomerService()) { canView = true; } if (!canView) { throw new BusinessException("无权查看该工单"); } ... }这种"从登录态取身份,而不是从前端参数取身份"的习惯,是从真实企业级开发里学来的。虽然毕设项目不会真的有人攻击,但这个设计是你从"学生代码"迈向"工程代码"的一个明显标志,建议在答辩时主动展示。
5.3 密码加密与Token刷新
密码存储我用的BCrypt加密,这是Spring Security内置支持的方案,永远不要用MD5存密码——MD5加盐都挡不住彩虹表。注册或新增用户时用BCryptPasswordEncoder加密后再入库,登录校验时用它自带的matches方法比对。
Token过期时间我设置了2小时,并在拦截器里做了一个简单的滑动续期:如果Token剩余有效时间不足30分钟且还在访问系统,就签发一个新Token返回给前端。前端Axios响应拦截器里检测到新Token就替换本地存储。这个机制不复杂,但能显著改善演示体验——演示现场不会出现"操作到一半突然要求重新登录"的尴尬情况。
6. 开发过程中踩过的坑与排查实录
这部分说说我在实际写代码、跑测试的过程中踩过的比较有代表性的坑。有些坑在网上一搜就有答案,有些是查了好久才定位到的,这里一并整理出来。
6.1 订单编号生成并发丢失
最开始我生成工单编号的代码是"取当天最大单号 + 1",在并发测试时发现同一个毫秒内两个线程可能取到同一个最大单号,导致生成重复的单号,数据库唯一的索引直接抛异常。后来我改成了在插入时使用数据库的原子操作:用一个专门的流水表记录每天的编号增长,靠数据库行锁保证并发安全。具体做法是创建一张order_sequence表,每次生成单号时执行:
UPDATE order_sequence SET current_value = current_value + 1 WHERE biz_date = '2025-06-07' AND biz_type = 'WO';然后查询更新后的current_value,拼上日期前缀和WO。因为UPDATE语句本身带行锁,并发请求会排队执行,从根本上杜绝了重复单号的问题。
6.2 MyBatis Plus分页插件失效
MyBatis Plus的分页需要手动注入分页插件,这一点很多教程里提到过,但我在实际配置时发现自己把PaginationInnerInterceptor放到代码里了,但忘了设置数据库类型,导致分页查询时LIMIT语句的方言不对。正确写法是这样的:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }不写DbType.MYSQL的时候,默认是H2还是什么类型的数据库,出来的SQL在某些情况下就是不对,页面列表一直查不全,非常坑。
6.3 时间字段的时区问题
这个坑几乎每个做SpringBoot项目的人都会遇到。数据库连接的serverTimezone参数如果不设置,在MySQL 8里查出来的时间会跟本地时间差了8个小时。我用的是:
spring.datasource.url=jdbc:mysql://localhost:3306/repair_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai另外Java实体类的LocalDateTime和前端交互时,默认的JSON序列化格式是"2025-06-07T10:30:00"这种带T的格式,不好看。我加了一个Jackson全局配置,统一格式化为yyyy-MM-dd HH:mm:ss:
@Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder -> { builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); }; }6.4 演示前才发现的上传路径问题
工单系统里有个功能是维修前后上传设备照片,本地测试时我直接把文件存到了项目的static/upload目录下,一切正常。但打包部署后图片一直显示不出来,排查了半天才知道问题:SpringBoot打成JAR后,不能在运行期往JAR包内的目录写文件。我的解决办法是将上传路径改成外部绝对路径,并通过配置类映射成虚拟路径访问:
# 配置文件里指定外部存储路径 upload.dir=/data/repair-system/upload/@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${upload.dir}") private String uploadDir; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadDir); } }这个坑在本地IDE里运行永远遇不到,但恰恰是很多部署环节翻车的重灾区。如果你的系统涉及文件上传,建议从第一天就按"外部存储 + 映射访问"的姿势来写,不要图省事写到项目目录里。
7. 从能跑到能答辩:部署、演示与交付经验
项目写完了,代码能跑,数据库有数据,但这不代表可以上演示现场了。从"本地能跑"到"现场演示不翻车",中间还有几个关键步骤要做。
7.1 打包部署的标准化流程
我的部署方式是直接打JAR包扔到服务器上,用java -jar启动,配合一个外部配置文件。这样做的优点是部署路径和配置文件完全在外部可控,不用每次改配置都重新打包。打包时要注意:
# 前端构建 cd frontend && npm run build # 将前端dist目录复制到SpringBoot的static目录下 cp -r dist/* ../src/main/resources/static/ # 后端打包 cd .. && mvn clean package -DskipTests # 启动 java -jar target/repair-system-1.0.0.jar --spring.config.location=file:/data/repair-system/application.yml我把数据库连接信息、上传路径这些环境相关的配置都放到了外部配置文件里,这样同一个JAR包在任意环境都能跑,只需要改外部YAML文件就行。这个做法在企业里叫"配置外部化",听起来高级,做起来其实就是加一个启动参数而已。
7.2 演示数据的造数技巧
演示最怕的是现场打开数据库发现全是test001、123456这种乱起八糟的数据,观感很差。我在交付前专门写了一个造数SQL脚本,生成一批看起来真实的演示数据。比如工单的客户姓名用"李女士""王先生",故障描述用"笔记本开机黑屏,电源指示灯不亮""打印机卡纸,无法正常出纸"这种真实场景描述,送修地址也用规范写法。
这批演示数据的核心逻辑是覆盖状态机的所有状态:有几张待派单、有几张待接单、有几张维修中、有几张待验收、有几张已完结带评价。这样在现场演示时,无论操作哪个环节,点开列表都有对应的数据,状态流转的效果非常直观。建议你也可以做一张包含完整生命周期数据的工单,从创建到完结的日志时间线都齐全,演示时直接打开详情页拉时间线,这个效果比任何口头讲解都有说服力。
7.3 答辩时最容易被问到的几个问题与回答思路
结合我自己和身边同学的经验,答辩老师对这类系统最感兴趣的问题一般集中在三个方向:
- 为什么这样设计状态机?回答思路:从业务闭环出发,强调"每个状态都有明确归属、每次流转都有日志留痕、非法跳转会被拦截",顺带可以举一个没有状态机导致扯皮的业务反例。
- 并发抢单怎么处理?回答思路:讲清楚乐观锁和行锁的原理,再补充说明数据库在这条SQL上是如何保证只有一个线程能更新成功的。
- 如果让你上线生产环境,还缺什么?回答思路:这里是加分题,可以说消息队列削峰、Redis缓存热点数据、分布式锁保证抢单一致性、监控和告警等,主要展示你知道工程化方向是什么,不完全等于"没做过"。
7.4 交付物清单:源码之外还要准备什么
最后说一下交付整理。这个项目的交付物,我按"能不能让别人拿到手就跑起来"为标准来准备,一共五件套:
- 完整源码:后端Java代码 + 前端Vue代码,目录清晰有注释,关键业务代码要有简单的注释说明。
- 数据库脚本:建库建表SQL + 初始化数据SQL + 演示数据SQL,三个脚本分开。
- 部署说明文档:环境要求、JDK版本、MySQL配置、前端构建步骤、启动命令,图文并茂。
- 演示视频:按照"登录 → 创建工单 → 派单/抢单 → 工程师接单 → 维修反馈 → 验收 → 完结评价"主流程录屏,配简短解说字幕。
- 项目设计文档(LW):需求分析、功能模块、数据库设计、核心代码说明、系统测试,覆盖论文/报告需要的所有章节。
我在整理数据库脚本的时候特别注意了:生产库、测试库、演示库分开;所有SQL文件都设置utf8mb4字符集,避免插入数据时出现乱码;演示SQL里所有密码字段都是BCrypt加密后的密文,配合README里提供的明文密码说明,别人拿过去可以直接登录"管理员/123456"。
写在最后:再做这类管理系统时我会怎么优化
做完这个电脑维修工单管理系统,我最大的体会是:一个业务系统好不好,不看功能多不多,看的是核心业务链路是否严密。工单系统的价值不在于"能增删改查",而在于每一张工单从出生到消亡的全过程,都有条理清晰的记录、合法无歧义的状态流转、以及严格的权限边界。
如果后续还有时间和精力,我会在三个方向继续扩展:一是消息通知渠道,工单状态变化通过短信或公众号模板消息推送给客户,形成闭环触达;二是数据看板,按日/周/月统计工单量、完结率、平均维修时长、工程师工作量等关键指标,让管理者一眼看清业务执行情况;三是备件库存联动,维修过程中使用的配件自动扣减库存,低于阈值自动生成采购提醒。
源码、数据库脚本、部署文档和演示视频,我都打包放在网盘里了,有需要可以直接拉下来跑。如果在部署环节或者写论文时遇到问题,欢迎留言交流,我看到都会回。