ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java二手电子产品维修系统:工单状态机与库存事务设计

Java二手电子产品维修系统:工单状态机与库存事务设计 简介基于Java语言的二手电子产品维修系统设计源码是一套面向高校学生、Java开发者和维修服务商的完整项目资源用于快速搭建涵盖维修任务、配件库存、客户信息与进度管理等核心模块的管理平台。压缩包共370个文件约784KB包括158个class字节码、142个java源文件、57个xml配置、6个yml配置、3个gitignore、2个iml、1个license及1个txt说明覆盖后端业务逻辑、Spring体系配置、IDE工程结构与版本控制规范体量紧凑但结构完整。目前已有255人学习/下载。源码采用模块化与面向对象设计后端基于Spring框架核心业务类覆盖用户、维修单、预约、支付、故障选项等典型模块分层清晰便于二次开发借助该资源可快速理解二手电子维修场景下的数据模型与业务流转适合作为毕业设计、课程项目或小型企业系统的参考实现也可作为学习Java工程结构与Spring配置的实战案例。对于正在寻找JavaWeb完整项目的开发者这份源码省去了从零搭建框架和设计表结构的时间可直接在其上扩展功能。1. 二手电子产品维修系统到底是什么一张维修工单背后要管多少数据先说结论基于 Java 的二手电子产品维修系统设计源码做出来不是让你真的去开一家维修铺而是让你用一套能跑的 Web 业务系统把“客户送修、技师检测、配件更换、结算取件”整个流程管起来。这个标题下的源码本质上是典型的 Java Web 课程设计或毕业设计项目核心是用 Spring Boot 这类框架实现工单管理、配件库存和客户档案三块业务。它解决的痛点是纸质登记单容易丢、配件账目对不上、维修进度靠吼。适合谁用正在做 Java 课程设计的学生、想攒一个完整项目经验的初级开发者以及小维修门店想低成本数字化的店主。所以下面从业务模型讲起落到可复现的源码结构和代码最后把坑一条条给你列清楚。2. 业务模型先立住维修工单、配件库存与客户档案的关系设计2.1 维修系统的核心数据对象工单、配件、客户与技师做系统之前先分清四个核心实体客户、工单、配件、技师。很多课程设计源码一上来就写用户表和登录把业务主体丢了。二手电子产品维修系统的业务主体是工单其他表都是围绕工单服务。工单表最少包含这些字段工单号、客户ID、设备类型手机/笔记本/平板、品牌型号、故障描述、接单状态、维修进度、收入金额、成本金额、创建时间、完成时间。配件要单独建表因为一张工单可能用到多个配件配件有自己的库存、进货价和销售价。技师也应独立建表不要用一张通用员工表硬凑维修系统里技师有擅长设备类型这个属性比如“主板维修”“屏幕更换”后续派单时按技能匹配是非常自然的扩展点。客户和工单是 1 对 N 关系一个客户可能送修多次每次生成一张新工单。工单和配件是多对多关系通过中间表“工单配件明细”关联明细里记录数量、结算单价。客户档案的关键字段是姓名、电话、微信号用于取件通知、常用设备类型。电话必须有唯一索引否则同一个客户第二次送修时又建一条记录统计客户数就虚高月底对账也对不上。2.2 状态机驱动维修进度从“待接单”到“已取件”的流转规则状态机是这套系统里最能体现设计能力的地方。一张工单从录入到结单至少要经历六个状态待接单、检测中、待用户确认、维修中、待取件、已取件外加一个“已关闭”用于用户放弃维修的场景。注意“待用户确认”不能省因为商家报价后用户可能嫌贵不修这是真实业务里的卡点。状态流转不能做成任意跳转。待接单的工单不能直接变成待取件检测中的工单也不能跳过“待用户确认”直接进入维修。常见做法是在代码里维护一个允许流转规则表用 Map 或枚举定义合法路径。把校验逻辑集中在一个类里而不是在每个 Service 方法里写 if 判断这样后续加状态时只改一处。状态变更日志也是一定要做的。记录工单号、旧状态、新状态、操作人、操作时间。维修行业最容易扯皮的是“顾客说手机之前还能开机技师说送来时就开不了机”有日志才能还原过程。这个细节在课程设计答辩时非常加分老师会认为你想到了真实交付才有的问题。2.3 用 E-R 关系把三个核心模块串起来整个系统的最小闭环需要五张表客户表、技师表、工单表、配件表、工单配件明细表。看关系关系类型说明客户 - 工单1 对 N一个客户可有多张工单一张工单只属于一个客户技师 - 工单1 对 N一个技师可处理多张工单一张工单只指派一个技师工单 - 配件N 对 N通过工单配件明细表关联明细中记录数量和结算价配件 - 库存1 对 1库存字段直接挂在配件表上不需要单独建库存流水表建表时外键约束和索引要一起加。很多源码在实体类里写了关联关系数据库里却没有外键删除客户时工单变成孤儿数据。代码里做逻辑校验是必要的但外键约束是底线不然别人导入你的建表脚本后数据随便删。还有一个容易被忽视的表维修记录表。它与工单是 1 对 N 关系一张工单可以追加多条检测记录比如“检测发现主板短路更换电容后正常”。把维修记录做成独立表而不是在工单表上堆字段以后统计高频故障类型就非常方便。这个细节就是源码设计里“可扩展性”的具体体现比空谈设计模式更有说服力。3. 技术选型与源码结构为什么用 Spring Boot MyBatis 而不是 SSH3.1 选型理由课程设计与真实交付之间的平衡点看到“基于 Java”老项目习惯用 Servlet JSP 或 SSH。我的建议是直接选 Spring Boot MyBatis MySQL Thymeleaf。原因很实际Spring Boot 内嵌 Tomcat别人拿到源码不用单独配 Tomcat双击启动类就能跑交作业和现场演示的成功率高很多MyBatis 的 SQL 直白适合业务逻辑清晰的管理系统一个维修系统本质上就是一堆 CRUD 加少量统计JPA 反而要把精力花在关联映射上对新手不友好Thymeleaf 做服务端渲染不用额外启动前端工程这对课程设计是最低门槛的组合。版本选择上有个血泪经验不要追最新版。Spring Boot 3.x 要求 JDK 17而很多学校的机器还停在 JDK 8跑不起来就是大翻车。用 Spring Boot 2.7.x JDK 8 是最稳的组合Java 环境变量配置也简单网上找资料全是现成的。等以后工作用到新版本再迁移成本也不高。3.2 源码包里的目录结构一个可复现的最小工程源码包用 Maven 标准目录结构一个可复现的最小工程长这样src/main/java/com/example/repair ├── controller │ ├── WorkOrderController.java │ ├── CustomerController.java │ ├── PartController.java │ └── StatisticController.java ├── service │ ├── WorkOrderService.java │ ├── CustomerService.java │ ├── PartService.java │ └── WorkOrderStatusService.java ├── mapper │ ├── WorkOrderMapper.java │ ├── CustomerMapper.java │ ├── PartMapper.java │ └── WorkOrderPartMapper.java ├── entity │ ├── WorkOrder.java │ ├── Customer.java │ ├── Part.java │ └── WorkOrderPart.java └── config └── WebConfig.java这个结构的核心是分层清晰。Controller 只负责接收参数和返回页面Service 层写业务判断Mapper 层操作数据库。不要在一个类里写完所有功能代码量看着大答辩时却没有一条清晰的主线可以讲。把状态校验放在 Service 层而不是 Controller 层这也是 Java 面试题里常问的“业务逻辑放哪一层”的标准答案。3.3 数据库建表脚本五张表把业务闭环串起来下面是可运行的最小建表脚本字段设计都是踩过坑之后的版本。CREATE TABLE customer ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL UNIQUE, wechat VARCHAR(50), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE technician ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, skill_tags VARCHAR(255) COMMENT 擅长维修的设备类型 ); CREATE TABLE work_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) UNIQUE NOT NULL, customer_id BIGINT NOT NULL, technician_id BIGINT, device_type VARCHAR(30) NOT NULL COMMENT 手机/笔记本/平板, brand_model VARCHAR(100), fault_desc VARCHAR(500), order_status TINYINT DEFAULT 0 COMMENT 0待接单 1检测中 2待确认 3维修中 4待取件 5已取件 6已关闭, income_amount DECIMAL(10,2) DEFAULT 0, cost_amount DECIMAL(10,2) DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, finish_time DATETIME, FOREIGN KEY (customer_id) REFERENCES customer(id), FOREIGN KEY (technician_id) REFERENCES technician(id) ); CREATE TABLE part ( id BIGINT PRIMARY KEY AUTO_INCREMENT, part_name VARCHAR(100) NOT NULL, stock INT NOT NULL DEFAULT 0, purchase_price DECIMAL(10,2) NOT NULL, sale_price DECIMAL(10,2) NOT NULL ); CREATE TABLE work_order_part ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, part_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 1, price DECIMAL(10,2) NOT NULL COMMENT 结算时实际使用的单价, FOREIGN KEY (order_id) REFERENCES work_order(id), FOREIGN KEY (part_id) REFERENCES part(id) );三个设计点值得展开。第一order_no 用字母加时间戳生成不要直接用自增主键当工单号它会暴露业务量而且后期做对外查询时要拼接固定位数的单号自增主键不够用。第二order_status 用数字存没问题但代码里必须对应枚举这个坑在第 5 章详细说。第三work_order_part 里的 price 单独保存一份不要直接查 part 表的 sale_price因为结算时可能打折明细表里保留历史快照改价不污染配件表。4. 把维修工单流程跑通从建单到结单的核心代码4.1 创建工单订单状态怎么定配件明细怎么拆先看创建工单的 Service 方法这是整套源码最核心的入口。Override Transactional public Long createOrder(WorkOrderCreateRequest request) { Customer customer customerMapper.findByPhone(request.getPhone()); if (customer null) { customer new Customer(request.getName(), request.getPhone()); customerMapper.insert(customer); } WorkOrder order new WorkOrder(); order.setOrderNo(generateOrderNo()); order.setCustomerId(customer.getId()); order.setDeviceType(request.getDeviceType()); order.setBrandModel(request.getBrandModel()); order.setFaultDesc(request.getFaultDesc()); order.setOrderStatus(0); workOrderMapper.insert(order); for (PartItem item : request.getParts()) { Part part partMapper.findById(item.getPartId()); if (part null || part.getStock() item.getQuantity()) { throw new BusinessException(配件库存不足: item.getPartName()); } workOrderPartMapper.insert(order.getId(), part.getId(), item.getQuantity(), part.getSalePrice()); } return order.getId(); }三个关键点必须讲明白。第一个是客户复用逻辑同一手机号不新建客户记录这是统计报表不虚高、不出现重复档案的前提。第二个是 Transactional 注解它保证“插入工单 插入多条配件明细”要么全部成功要么全部回滚否则会出现工单建了但明细缺失的脏数据。第三个是配件校验必须在插入明细前做把“库存不足”这个异常抛在事务内事务回滚就不会留下半截工单。generateOrderNo() 的常见实现是取当天日期加随机数private String generateOrderNo() { String datePart LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); String randomPart String.format(%04d, new Random().nextInt(10000)); return WO datePart randomPart; }orderStatus 初始值用 0对应状态机里的“待接单”。注意状态默认值和数据库默认值要保持一致不要约等于 1 却在建表时写 DEFAULT 0。4.2 维修进度流转状态机让状态不乱跳状态流转的规则写在一个专门的状态类里。public class WorkOrderStatus { public static final MapInteger, SetInteger ALLOWED_TRANSITIONS new HashMap(); static { ALLOWED_TRANSITIONS.put(0, new HashSet(Arrays.asList(1, 6))); ALLOWED_TRANSITIONS.put(1, new HashSet(Arrays.asList(2, 6))); ALLOWED_TRANSITIONS.put(2, new HashSet(Arrays.asList(3, 6))); ALLOWED_TRANSITIONS.put(3, new HashSet(Arrays.asList(4))); ALLOWED_TRANSITIONS.put(4, new HashSet(Arrays.asList(5))); } public static boolean canChange(int current, int target) { SetInteger targets ALLOWED_TRANSITIONS.get(current); return targets ! null targets.contains(target); } }状态机设计的好处是规则集中管理而不是散落在各个 Service 方法里。检测中1可以跳到待用户确认2用户不修就跳已关闭6但检测中不能直接跳到维修中3必须先等客户确认报价。把规则放在 Map 里后后续业务扩展出“返厂维修”状态只需要在这一个类里加规则业务代码不用动。实际调用的流程长这样public void updateStatus(Long orderId, int targetStatus, Long operatorId) { WorkOrder order workOrderMapper.findById(orderId); if (order null) { throw new BusinessException(工单不存在); } if (!WorkOrderStatus.canChange(order.getOrderStatus(), targetStatus)) { throw new BusinessException(非法状态流转: order.getOrderStatus() - targetStatus); } workOrderMapper.updateStatus(orderId, targetStatus); statusLogMapper.insert(orderId, order.getOrderStatus(), targetStatus, operatorId, LocalDateTime.now()); }向上流转时同时记录状态日志且日志插入和状态更新在同一个事务里。这个设计回答了 Java 基础面试题里“Map 比 switch 适合处理什么场景”规则多、会扩展、集中管理时用 Map固定且简单时用 switch。4.3 配件库存扣减事务边界不能让库存变负数扣库存的 SQL 不能先查再扣要写成一条原子更新语句。public boolean deductStock(Long partId, Integer quantity) { int rows partMapper.deductStockIfEnough(partId, quantity); if (rows 0) { throw new BusinessException(库存不足或配件已下架); } return true; }对应 Mapper 里的 SQLUPDATE part SET stock stock - #{quantity} WHERE id #{partId} AND stock #{quantity}这条 SQL 的精髓是把“库存是否够”和“扣减库存”合并成一个原子操作。数据库的行锁保证同一时刻只有一个线程能把这一行的 stock 扣减如果先 SELECT stock 再判断再 UPDATE两个并发请求都读到 stock1各自减 1最终库存变成 -1。在维修系统里表现为配件领走了但账上负数月底对账怎么都对不上。还有一个 MyBatis 的细节update 语句的返回值在 MySQL 里如果更新前后值相同会返回 0所以不要用“返回值是否等于 1”判断更新成功而是靠 WHERE 里 stock #{quantity} 是否命中。这一点在 java 八股文里经常被考到。扣库存和创建工单必须放在同一个事务里。扣库存成功但建单失败库存已经没法回滚建单成功但扣库存失败工单成了没有配件的空单。两者都用 Transactional 包住任何异常都会触发统一回滚。5. 落地踩坑记录这些坑不避开源码能跑但项目交不了5.1 时间字段变成 nullLocalDateTime 与 MySQL datetime 的映射坑现象工单创建后时间字段正常但更新完成时间后再次查询finish_time 变成 null。原因MyBatis 对 LocalDateTime 的映射依赖数据库驱动版本。老版本 mysql-connector-java 5.x 对 JSR310 时间类型的支持不完整写入时可能被忽略读取时类型转换出错被吞掉。更隐蔽的是有些源码里用 Date 类型接收页面传的是字符串格式化失败时字段直接变成 null。解决mysql-connector-j 升到 8.0.x实体类时间字段统一用 java.time.LocalDateTime不要混用 java.util.Date。数据库连接串里加上 serverTimezoneAsia/Shanghai 和 useSSLfalse避免时区差异把时间存偏查询出来又是另一个值。5.2 库存超卖先查后扣的并发问题现象明明库存剩余 1 个屏幕两个维修工单同时录入都显示库存足够最后库存变成 -1。原因先 SELECT stock 再判断再 UPDATE两个并发事务同时读到同一个库存值各自判断通过最终都执行扣减超卖就发生了。维修系统并发量虽然不如电商秒杀但门店连网后两台电脑同时开单就可能触发。解决用 UPDATE ... WHERE stock #{quantity} 的原子扣减。数据一致性靠数据库约束兜底不要只靠 Java 代码的逻辑判断。以后这个系统要去掉并发隐患把库存扣减放到独立事务或引入乐观锁那都是后话先用原子 SQL 保住不超卖。5.3 金额用 double 还是 BigDecimal账目对不上就出在这现象统计维修收入时换个屏幕 480 元换个电池 99.9 元加在一起显示 579.8999999。原因double 是二进制浮点计数0.1 0.2 算出来不是精确的 0.3。维修系统里每个字段都涉及钱int 和 double 看着省事累计报表一旦对不上账问题找起来最痛苦。解决实体类里用 BigDecimalMySQL 里用 DECIMAL(10,2)。BigDecimal 构造不要用 new BigDecimal(0.1)要用 BigDecimal.valueOf(0.1) 或字符串构造否则精度问题原样保留。前端展示统一调用 setScale(2, RoundingMode.HALF_UP) 后再输出JSON 序列化时如果用的是 fastjson注意它默认会把 BigDecimal 转成字符串类型就对不上了。5.4 状态字段用 int 还是 varchar改需求时最痛的后悔药现象原本 order_status 只有 0 到 5 六个值后来要加一个“待返厂维修”代码里所有 switch 判断都要翻一遍漏改一处状态就卡在中间位置。原因用数字魔法值表达状态阅读代码的人必须对照注释才能看懂每个数字的含义。新增状态时散落的 if-else 和 switch 分支都要同步改漏一处就是隐蔽 Bug。解决数据库里用数字存没问题但 Java 代码里定义枚举类所有状态判断用枚举的 name 或 code不写裸数字。加新状态时只需要改状态机的 ALLOWED_TRANSITIONS Map 和枚举定义业务代码基本不用动。这个后悔药值得备着因为维修系统的状态大概率会随着业务扩张而增加。5.5 有外键关联的表删除时莫名报错现象删除一条客户记录时弹出外键约束错误但代码里明明没有调用删除工单的接口。原因建表脚本里有 FOREIGN KEY 约束work_order 表引用了 customer 表的 id直接删除客户会触发数据库层的约束检查把删除操作挡住。解决不要关闭外键约束那等于自毁底线。正确的做法是业务上不允许物理删除有历史工单的客户改成逻辑删除给 customer 表加一个 is_deleted 字段删除时置 1查询时默认过滤掉已删除客户。这样既保留历史数据又解决外键报错。反过来如果某天想清理测试数据按父子关系先删子表再删父表工具里提供这个接口演示时不会尴尬。6. 这套源码的进阶玩法把统计报表和微信通知接进来6.1 维修收入与配件损耗统计一张 SQL 撑起答辩亮点如果源码只有增删改查答辩时会显得单薄。花一个晚上把统计报表加上价值立刻不一样。最有用的统计是按月份汇总维修毛利、按配件统计消耗数量、按故障类型统计频次。SELECT DATE_FORMAT(created_at, %Y-%m) AS month, SUM(income_amount - cost_amount) AS profit FROM work_order WHERE order_status IN (5, 6) GROUP BY DATE_FORMAT(created_at, %Y-%m) ORDER BY month;这条 SQL 的边界条件是只统计已取件和已关闭的工单检测中或维修中的工单还没产生真实毛利统计进去会误导决策。待用户确认后不修的工单要走已关闭状态此时成本金额为空建议用 COALESCE 兜底。把报表结果封装成一个 MonthProfitVOController 返回 Thymeleaf 模板前端用 Chart.js 画一条折线图工作量不大视觉效果好得多。6.2 从课程设计到简历项目给自己加一个亮点给正在做这个系统的同学一个具体建议别止步于把源码跑起来把自己当成真实店主用一遍。录一个客户建一张工单走完状态流转结单后看一眼统计报表完整闭环能走通项目才算交付。微信通知是性价比最高的进阶。在取件状态变更时调用企业微信机器人 Webhook用 Java 自带 HttpClient 发一条 JSON 消息内容是“您的设备已维修完毕请到店取件”。不需要申请公众号不需要服务器域名本地调试就能看到效果。这个功能在答辩现场展示比几十页 PPT 有用老师和面试官看到的不只是“我会调接口”而是“我能在业务里合理使用外部服务”。这么多年做 Java 项目我一个固定习惯是把建表脚本、初始化数据和源码目录放在同一个仓库里二手电子产品维修系统尤其如此因为换机器跑演示时只拷一份打了补丁的代码远远不够数据库脚本对不上启动必挂。再花二十分钟写一个 README把 JDK 版本、Java 环境变量配置、数据库创建步骤写清楚这是源码交付的基本素养。希望帮到你。本文还有配套的精品资源点击获取
返回列表