基于Spring Boot+Vue的家电维修系统:全流程设计与实现

发布时间:2026/11/5 7:29:03
基于Spring Boot+Vue的家电维修系统:全流程设计与实现

简介:这是一套面向Java全栈学习者与中小型维修服务企业开发者的家电家具维修系统完整项目源码,基于SpringBoot、Mybatis-Plus与Vue.js技术栈构建,覆盖用户管理、订单提交、维修工单调度、在线支付、评价反馈与统计报表等核心业务模块,可用于课程设计、毕业设计参考或二次开发实践。资源包共2009个文件,以1234个md文档、710个js脚本、47个json配置为主,另含sql建表脚本、html页面与项目介绍docx,压缩包约60.36MB,目录结构清晰,便于按前后端模块检索阅读。目前已有137人学习下载。项目将后端请求处理、数据持久化与前端组件化界面串联成完整数字化维修流程,读者可从中获取分层架构设计思路、接口与数据库组织方式以及业务模块落地细节,适合具备一定Java与前端基础、希望积累完整项目经验的学习者参考。

1. 家电维修系统为什么值得做成一套完整方案

家电家具维修这件事,线下门店最头疼的不是修不好,而是报修、派单、配件、结算全靠微信群和纸质单子,师傅跑一趟才发现配件不对,客户催三次也没人知道单子卡在谁手里。把这套流程搬进系统,本质是把「谁报修、谁接单、谁上门、换了什么件、收多少钱」五个问题用一条数据链串起来。基于 Spring Boot + MyBatis-Plus + Vue 的家电家具维修系统,正好是课程设计和毕设里既能讲清业务闭环、又能体现前后端分离工程能力的题目。它适合两类人:一类是要交完整设计文档和可运行代码的学生,另一类是想给小型维修门店搭一套能直接改的业务底座的开发者。下面按需求、架构、模块、避坑、验收的顺序,把这份方案拆到能照着做的程度。

2. 需求与角色建模:先把五类用户和三条主流程定死

2.1 五类角色各自要看到什么

维修系统最容易翻车的地方,是需求阶段只画了「用户下单、师傅接单」两条线,做到一半发现还有客服、仓库、财务三类人没有入口。我一般会把角色先定成五类,每类只给最小必要权限:

角色核心诉求首屏看到的内容
普通用户报修、查进度、评价我的报修单列表与状态
维修师傅接单、填维修记录、报配件待接单池与我的进行中工单
客服/派单员审核报修、指派师傅待派单列表与师傅负载
仓库管理员配件出入库、低库存预警库存列表与预警
系统管理员用户、品类、价格、统计数据看板与基础配置

这张表不是摆设,它直接决定后面数据库要建几张主表、前端要拆几个路由分组。角色定得越早,后面改权限的成本越低。通常的做法是权限用「角色-菜单」两级控制,不做细到按钮的权限,课程设计层面够用,也留了扩展口。

2.2 三条主流程串起整个业务闭环

角色定完,把流程收敛成三条,其余都是分支:

  1. 报修流程:用户提交报修(品类、故障描述、上门地址、期望时间)→ 客服审核 → 派单给师傅 → 师傅接单 → 上门 → 填写维修记录(故障原因、更换配件、工时)→ 用户确认 → 评价。
  2. 配件流程:师傅在维修记录里勾选更换的配件 → 系统扣减库存 → 库存低于阈值触发预警 → 仓库补货入库。
  3. 结算流程:维修记录确认后生成费用明细(工时费 + 配件费)→ 用户支付(课程设计里可用模拟支付)→ 生成结算记录供统计。

三条流程共用一张「工单」主表,状态机是核心。状态不要随手写字符串,我一般定义成枚举:待审核、待派单、待接单、维修中、待确认、已完成、已取消。每次状态流转都记一条操作日志,出问题时这就是黑匣子。

2.3 非功能需求别漏掉

学生方案最容易只写功能不写约束。这个场景下至少要写清三条:一是工单状态变更必须留痕,方便客服追溯;二是配件扣减要防并发超卖,两个人同时报同一个配件不能扣成负数;三是列表查询要分页,工单表数据量上来后不分页会直接拖垮首屏。这三条写进需求文档,答辩时就是加分项,因为它们说明你想过系统跑起来之后的事。

3. 总体架构设计:前后端分离下的分层与数据模型

3.1 三层结构和技术选型理由

整体是经典的前后端分离三层结构:Vue 前端负责页面与交互,Spring Boot 提供 REST 接口,MyBatis-Plus 负责数据访问,MySQL 存业务数据。选型理由要能说出口:Spring Boot 把配置和依赖管理收拢,起步快;MyBatis-Plus 在 MyBatis 之上补了通用 CRUD 和分页插件,工单、配件这类表的增删改查不用手写一堆 XML;Vue 的组件化让「工单卡片」「状态标签」这类重复 UI 可以复用。三层职责划分如下:

  • 表现层:Vue 页面 + 路由 + 状态管理,只做展示和表单校验,不写业务规则。
  • 接口层:Controller 收参数、做权限校验、调 Service,返回统一响应体。
  • 业务层:Service 承载状态流转、库存扣减、费用计算等规则。
  • 数据层:Mapper 接口 + MyBatis-Plus,复杂统计查询再写自定义 SQL。

统一响应体建议固定成code / message / data三段,前端拦截器按 code 统一处理登录失效和错误提示,避免每个页面各写一套判断。

3.2 核心数据表怎么划

表不用多,六张主表能撑起整个闭环:

表名作用关键字段
user用户与角色id、username、role、phone
repair_order工单主表id、user_id、technician_id、status、address、create_time
repair_record维修记录id、order_id、fault_reason、work_hours、total_fee
part配件表id、name、stock、warn_threshold、price
part_record配件出入库流水id、part_id、change_num、type、order_id
order_log工单操作日志id、order_id、from_status、to_status、operator、create_time

工单主表只存「当前状态」,历史状态全部进 order_log,这样主表轻、追溯全。配件扣减不直接改 part.stock 了事,而是先写 part_record 流水再更新库存,账实能对上,这也是仓库场景的基本要求。

3.3 接口分层与统一约定

接口按资源划分前缀:/api/order、/api/part、/api/user,动词用 HTTP 方法表达。分页参数统一pageNum / pageSize,返回体里带total。下面是一个典型的工单分页查询接口骨架,MyBatis-Plus 的分页插件在这里省了大量手写代码:

// OrderController.java @RestController @RequestMapping("/api/order") public class OrderController { @Resource private RepairOrderService orderService; // 按状态和关键字分页查询工单,客服和师傅共用 @GetMapping("/page") public Result<Page<RepairOrder>> page( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String status, @RequestParam(required = false) String keyword) { Page<RepairOrder> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<RepairOrder> wrapper = new LambdaQueryWrapper<>(); // 状态为空表示查全部,避免前端传空串导致条件失效 wrapper.eq(StringUtils.hasText(status), RepairOrder::getStatus, status); wrapper.and(StringUtils.hasText(keyword), w -> w.like(RepairOrder::getAddress, keyword) .or().like(RepairOrder::getContactName, keyword)); wrapper.orderByDesc(RepairOrder::getCreateTime); return Result.ok(orderService.page(page, wrapper)); } }

这段代码有两个设计点值得说:一是条件构造用StringUtils.hasText做判空,前端不传或传空串都不会拼出错误 SQL;二是关键字查询用and(...)包一层,避免和状态条件用or拼在一起导致条件串味,这是 LambdaQueryWrapper 最常见的踩坑点。分页插件需要在配置类里注册MybatisPlusInterceptor并加PaginationInnerInterceptor,否则page方法返回的 total 永远是 0。

4. 核心模块实现:工单状态机、库存扣减与前端交互

4.1 工单状态流转与操作日志

状态流转必须收口在 Service 里,不能让 Controller 直接改 status 字段。我一般写一个changeStatus方法,校验「当前状态能否流转到目标状态」,再更新主表并写日志:

// RepairOrderServiceImpl.java private static final Map<String, List<String>> FLOW = Map.of( "待审核", List.of("待派单", "已取消"), "待派单", List.of("待接单", "已取消"), "待接单", List.of("维修中", "已取消"), "维修中", List.of("待确认"), "待确认", List.of("已完成") ); @Transactional(rollbackFor = Exception.class) public void changeStatus(Long orderId, String toStatus, Long operatorId) { RepairOrder order = getById(orderId); if (order == null) { throw new BizException("工单不存在"); } List<String> allowed = FLOW.getOrDefault(order.getStatus(), List.of()); if (!allowed.contains(toStatus)) { // 非法流转直接拒绝,防止前端传任意状态 throw new BizException("当前状态不允许流转到:" + toStatus); } String fromStatus = order.getStatus(); order.setStatus(toStatus); updateById(order); // 每次流转都落日志,作为追溯依据 OrderLog log = new OrderLog(); log.setOrderId(orderId); log.setFromStatus(fromStatus); log.setToStatus(toStatus); log.setOperatorId(operatorId); orderLogService.save(log); }

FLOW这张流转表是整个状态机的规则来源,改流程只改这一处,不用满项目找 if-else。@Transactional保证「改状态 + 写日志」要么都成功要么都回滚,否则会出现状态变了但日志丢了的情况,排查时非常难受。参数operatorId从登录态里取,不要信前端传的值。

4.2 配件库存扣减防超卖

库存扣减是这类系统里最容易出问题的地方。正确做法是用带条件的 UPDATE,把「库存够不够」的判断交给数据库:

<!-- PartMapper.xml --> <update id="deductStock"> UPDATE part SET stock = stock - #{num} WHERE id = #{partId} AND stock >= #{num} </update>

Service 里判断影响行数,等于 0 就说明库存不足,抛异常回滚整个维修记录提交:

@Transactional(rollbackFor = Exception.class) public void submitRecord(RepairRecord record, List<PartUse> parts) { // 先扣库存,任何一件不足都整体回滚 for (PartUse use : parts) { int rows = partMapper.deductStock(use.getPartId(), use.getNum()); if (rows == 0) { throw new BizException("配件库存不足:" + use.getPartName()); } partRecordService.saveFlow(use, record.getOrderId(), "出库"); } repairRecordService.save(record); orderService.changeStatus(record.getOrderId(), "待确认", record.getOperatorId()); }

这里的关键是「先扣库存再落记录」,且整个方法在一个事务里。如果先查库存再更新,两个请求同时查到库存为 1,就会都扣成功变成 -1,这就是典型的超卖。用stock >= #{num}作为更新条件,数据库行锁会保证只有一个请求扣成功。这个点在答辩时讲出来,比讲十个页面都管用。

4.3 Vue 前端的工单列表与状态标签

前端按角色拆路由,工单列表用一张卡片列表渲染,状态用统一组件映射颜色,避免每个页面各写一套:

// OrderList.vue 片段 const statusMap = { '待审核': { color: '#909399', text: '待审核' }, '待派单': { color: '#e6a23c', text: '待派单' }, '待接单': { color: '#409eff', text: '待接单' }, '维修中': { color: '#67c23a', text: '维修中' }, '待确认': { color: '#f56c6c', text: '待确认' }, '已完成': { color: '#67c23a', text: '已完成' } } // 根据当前登录角色决定显示哪些操作按钮 function actionsOf(order, role) { if (role === '客服' && order.status === '待审核') return ['派单', '取消'] if (role === '师傅' && order.status === '待接单') return ['接单'] if (role === '师傅' && order.status === '维修中') return ['提交维修记录'] if (role === '用户' && order.status === '待确认') return ['确认完成'] return [] }

statusMap把状态和展示解耦,后端加状态时前端只改这一处。actionsOf用角色加状态两个维度算按钮,比在后端返回一堆布尔字段更直观,也避免前端硬编码判断散落各处。列表请求带上pageNum / pageSize / status,切换状态标签时只改查询参数重新拉数据,不做本地过滤,保证分页总数准确。

4.4 统计看板与费用计算

管理端看板通常要三个数:今日新增工单、待派单数量、本月配件消耗金额。前两个用 MyBatis-Plus 的count加条件即可,第三个需要联表统计,写自定义 SQL 更直接:

-- 本月配件消耗金额,按配件分组 SELECT p.name AS partName, SUM(r.change_num) AS totalNum, SUM(r.change_num * p.price) AS totalAmount FROM part_record r JOIN part p ON p.id = r.part_id WHERE r.type = '出库' AND r.create_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01') GROUP BY p.id, p.name ORDER BY totalAmount DESC;

费用计算放在 Service 里,工时费按品类单价乘工时,配件费按出库流水汇总,两者相加写入维修记录。金额字段统一用BigDecimal,不要用 double,否则结算时会出现 0.1 + 0.2 这类精度问题,对账时很难解释。

5. 避坑与常见问题:上线前必须排查的五件事

5.1 分页 total 为 0

现象:列表数据能出来,但分页组件显示总数为 0,翻页失效。原因:MyBatis-Plus 分页插件没注册,或者注册了但没加PaginationInnerInterceptor。解决:在配置类里显式注册拦截器,并确认DbType与数据库一致;如果用了多数据源,拦截器要挂在正确的 SqlSessionFactory 上。

5.2 状态被前端随意篡改

现象:测试时发现工单能从「待审核」直接跳到「已完成」。原因:更新接口直接接收前端传来的整个实体并updateById,没有校验流转规则。解决:更新状态一律走changeStatus方法,实体更新时用LambdaUpdateWrapper只允许改白名单字段,status 字段不接受前端直传。

5.3 配件库存扣成负数

现象:并发提交维修记录后库存出现负数。原因:先查后改,判断和更新不在一条 SQL 里。解决:改成UPDATE ... WHERE stock >= num并判断影响行数,配合事务回滚。压测时用两个线程同时提交同一配件,验证只有一个成功。

5.4 跨域和登录态丢失

现象:前端本地调试接口全部 403 或 CORS 报错,登录后刷新页面又变回未登录。原因:后端没配跨域,或者 token 只存在内存里没持久化。解决:后端统一配 CORS 允许前端域名,前端 token 存 localStorage 并在请求拦截器里统一带上;后端拦截器放行登录和静态资源路径,其余接口校验 token。

5.5 时间字段时区错乱

现象:工单创建时间比实际早或晚 8 小时,统计按天分组时数据跑到前一天。原因:数据库、JDBC 连接、应用三处时区不一致。解决:连接串里显式指定时区,实体时间字段统一用LocalDateTime,统计 SQL 里的日期函数与数据库时区对齐,改完用一条跨零点的测试数据验证。

6. 验收与答辩:用一条工单跑通全流程

验收不要按页面一个个点,那样容易漏。我一般准备一条「全链路用例」,从用户提交报修开始,依次切换五个角色账号,把工单从待审核推到已完成,中间故意触发一次库存不足,观察是否回滚。具体步骤:用户提交报修并记下工单号;客服登录审核并派单;师傅登录接单、提交维修记录并勾选一个库存为 1 的配件;仓库登录确认库存扣减和流水;用户确认完成并评价;管理员看板核对今日工单数和配件消耗金额。每一步都截图存档,答辩时按这条线讲,比念 PPT 有说服力。

验收标准可以列成一张对照表,逐项打勾:

验收项通过标准
状态流转非法流转被拒绝并提示原因
操作日志每次流转都有 from/to/操作人记录
库存扣减并发提交不出现负库存,流水与库存一致
权限隔离师傅看不到派单按钮,用户看不到他人工单
分页查询总数正确,翻页数据不重复不丢失
统计看板今日工单数、配件金额与手工核对一致

答辩时最容易被问的三个问题提前准备好答案:为什么用 MyBatis-Plus 而不是 JPA(通用 CRUD 省事、复杂 SQL 可控);库存并发怎么保证(条件更新加事务);状态机为什么不用工作流引擎(业务简单,枚举加流转表足够,引入引擎反而增加维护成本)。回答时落到具体代码位置,比空谈架构更可信。

血泪经验是:我第一次做这类系统时,验收只点了页面没跑并发,结果演示当天两个账号同时提交,库存直接变负,当场被问住。从那以后我每次交付前都强制走一遍「全链路用例 + 一次并发提交」,哪怕时间再紧也不省这一步。希望这套拆法能帮到你,照着把需求、表结构、状态机和验收用例落下来,这份方案就能直接改成你自己的设计文档和可运行代码。

本文还有配套的精品资源,点击获取