4S店维修管理系统核心实现:工单全流程、状态流转与库存扣减
简介一套面向大型汽车4s店运营场景的维修管理系统采用C#语言开发基于.NET框架实现适合汽修行业IT人员、管理系统开发者及4s店管理人员参考学习。系统覆盖车辆管理、维修服务、配件管理、客户关系、报表分析等核心模块可完成车辆注册与状态跟踪、维修预约与费用计算、配件库存监控与采购、客户满意度调查及各类业务报表输出。项目重点在于关系型数据库的设计与运用通过精心设计的表间关系、索引和数据字典确保车型、故障类型、维修工时、配件库存等字段的准确性与完整性。用户界面采用清晰的菜单结构和直观布局降低操作门槛便于维修人员与管理人员快速上手。资源包为RAR压缩格式大小约49.36MB文件总数暂未显示内含数据库脚本、界面设计相关文档及系统说明资料可辅助理解完整业务逻辑与数据表结构。已有479人学习浏览适合用于项目模仿、课程设计或实际管理系统二次开发。1. 从纸质工单到在线闭环这套维修管理系统到底管了什么一个服务顾问每天要处理几十个进店维修的客户最怕的不是车修不好而是工单找不到了。纸质工单在车间、前台、备件库之间转一圈少填一行工时费结算的时候就得对着单子查半天。这套大型汽车4S店维修管理系统就是把我所在的这类门店从“纸质流转Excel台账”里捞出来的全流程管理系统。它覆盖预约、开单、派工、领料、质检、结算、交车整条业务链服务顾问、车间技师、备件库管、收银员、店长各看各的界面。修车进度卡在哪个环节、备件有没有出库、这台车修了多少钱打开系统一眼就能看到。想要一套能跑起来、能改着用的管理系统参考这份代码值得下载下来过一遍。2. 系统模块全景六大模块怎么串起 4S 店维修业务这套系统不是简单的增删改查而是围绕「车进店到车离店」这条主线设计的。我拆完整个工程后发现它的模块划分非常贴近实体门店的组织架构每个岗位都有自己的工作台数据在岗位之间流转而不是各干各的。2.1 核心业务四条线工单、预约、备件、结算维修工单是整条链路的主动脉。工单记录的不只是“修什么车”还包括谁接待的、谁修的、用了哪些备件、花了多少工时、最后收了多少钱。系统里把它拆成主表和明细表主表存车辆、客户、服务顾问和整体状态明细表存具体的维修项目和工时费。预约管理是工单的前置入口。客户电话预约或者小程序预约后生成预约单到店后一键转化为维修工单不用二次录入。预约单状态从“已预约”变成“已到店”这步转化做完服务顾问马上能在工作台看到待确认的工单。备件管理走的是进销存加领料联动。备件入库、出库、盘点这些常规功能都有关键是领料环节和工单绑定技师在工单上勾选备件系统自动扣减库存同时把领料明细挂到工单下面最后结算时材料费直接汇总不需要另外算账。结算模块处理的是收钱那一下。结算单从工单拉取工时费和材料费生成应收金额收款后状态变成已支付工单流转到待交车。整个链路里结算单始终和工单一对一不会出现修完车对不上账的情况。2.2 基础支撑三件事客户档案、权限控制、统计报表客户和车辆档案是系统的地基。一个客户可以挂多辆车每辆车有自己的保养记录和维修历史。车辆档案里存了车牌号、VIN码、品牌车型、发动机号这些信息在开单时直接下拉选择不用手输减少录错数据的概率。权限控制按岗位切分。管理员、服务顾问、车间技师、备件库管、收银员、店长六种角色每种角色看到的菜单和可操作的按钮不一样。比如技师只能看到派给自己的工单和领料入口看不到结算金额店长则能看全店的统计报表。统计报表模块给店长和管理层看数。月度产值、工单完成量、备件出库排行、维修项目分布这些报表数据都来自业务表的汇总查询不需要单独的数据仓库。2.3 技术选型为什么是这套组合层级选型选它而不是别的原因后端框架Spring Boot 2.x起步快自动配置省掉大量 XML适合业务系统快速迭代ORMMyBatis Plus单表 CRUD 不用手写 SQL复杂查询还能用 XML兼顾效率与灵活权限认证Sa-Token比 Spring Security 轻量注解鉴权几分钟能接入小团队维护成本低前端Vue 2 Element UI中后台管理系统最成熟的组合组件丰富表单和表格开发效率高数据库MySQL 8.0业务数据量级完全够用InnoDB 行级锁能扛住门店并发权限模型RBAC基于角色的访问控制角色、权限、用户三者分离新增岗位时不用改代码这套选型放在 4S 店管理系统的场景里最明显的收益是开发速度。门店这类业务系统核心是理清业务状态和角色边界技术栈不需要追求新奇稳定、好招人、踩坑有答案才是关键。3. 数据库设计支撑维修主链路的 12 张核心表我打开 SQL 初始化脚本数了一下整个系统一共 12 张表分成三组5 张主链路表、5 张基础档案表、2 张辅助表。表数量不多但每张表的字段设计都踩在业务节点上。3.1 主链路五张表工单、项目、派工、领料、结算表名核心字段数据粒度work_orderorder_id、order_no、vehicle_id、customer_id、advisor_id、status、create_time一车一单一回维修work_order_itemitem_id、order_id、item_name、labor_hours、unit_price一张工单多行维修项目dispatch_recorddispatch_id、order_id、worker_id、dispatch_time一张工单多行派工记录material_usageusage_id、order_id、parts_id、qty、unit_price一张工单多行领料明细settlementsettlement_id、order_id、labor_fee、material_fee、total_fee、status一张工单只有一条结算单work_order 是主表work_order_item 存维修项目和工时费labor_hours 加 unit_price 就得到单项工时费。dispatch_record 记录技师派工信息一个工单可以分给多个技师每个技师一条记录。material_usage 是领料出库的流水unit_price 取的是出库那一刻的销售价这样做的好处是历史结算单不会因为备件后来涨价而对不上账。settlement 和 work_order 是一对一关系order_id 加了唯一约束防止重复结算。3.2 基础档案表客户、车辆、备件、员工、角色表名核心字段作用customercustomer_id、name、phone、address客户档案支持一客多车vehiclevehicle_id、customer_id、plate_no、vin_code、brand、model车辆档案VIN 码唯一索引partsparts_id、parts_code、parts_name、stock、purchase_price、sale_price备件库存与价格sys_useruser_id、username、password、real_name、role_id系统账号密码加密存储sys_rolerole_id、role_name、permission_codes角色定义权限码逗号分隔customer 和 vehicle 通过 customer_id 关联一个客户名下可以挂多辆车。vehicle 表的 vin_code 加了唯一索引防止同一辆车录入两遍。parts 表里做了最低库存字段报表页能直接预警哪些备件需要补货。sys_user 关联 sys_rolesys_role 里的 permission_codes 存的是一个权限码集合比如“work_order:create,work_order:dispatch,settlement:pay”后端接口鉴权时就校验这些权限码。3.3 辅助表与合作表预约单和数据字典appointment 表存预约记录关键字段是 appointment_no、customer_name、phone、appointment_time、status状态从“已预约”到“已到店”再流转到“已完成”。sys_dict 是数据字典表存工单类型、维修项目分类、结算方式等枚举值的显示名称改下拉框选项不用动代码改数据库就行。整条主链路的业务状态流转是这样设计的工单状态从 PENDING待确认到 CONFIRMED已确认再到 REPAIRING维修中、QUALITY_CHECKED质检完成、WAITING_PICKUP待交车、FINISHED已交车。每个状态之间的跳转都做了校验比如只有 QUALITY_CHECKED 状态才能生成结算单这个校验逻辑落在服务层不走数据库约束因为数据库管不了业务规则。4. 核心代码实现一个维修工单从开单到结算的完整旅程这一章我会按照一个工单从创建到收款的完整路径把关键代码拆开讲。代码是工程里最核心的三段创建工单、派工领料、结算。4.1 创建维修工单开单接口的前端联动先看后端 Service 的核心方法Service public class WorkOrderServiceImpl implements WorkOrderService { Autowired private WorkOrderMapper workOrderMapper; Autowired private AppointmentMapper appointmentMapper; Override Transactional(rollbackFor Exception.class) public Long createOrder(WorkOrderCreateDTO dto) { // 1. 生成工单号WO 年月日 Redis自增流水保证同一天不重复 String orderNo generateOrderNo(); // 2. 组装工单主表数据 WorkOrder order new WorkOrder(); order.setOrderNo(orderNo); order.setVehicleId(dto.getVehicleId()); order.setCustomerId(dto.getCustomerId()); order.setAdvisorId(dto.getAdvisorId()); // 服务顾问 order.setStatus(WorkOrderStatus.PENDING); // 新工单先进入待确认 order.setCreateTime(LocalDateTime.now()); workOrderMapper.insert(order); // 3. 如果来自预约单把预约状态改为已到店 if (dto.getAppointmentId() ! null) { Appointment apt appointmentMapper.selectById(dto.getAppointmentId()); apt.setStatus(AppointmentStatus.ARRIVED); appointmentMapper.updateById(apt); } return order.getId(); } }逻辑说明方法用Transactional保证“插入工单”和“更新预约状态”两步要么都成功要么都回滚。工单号生成规则是WO yyyyMMdd 流水号这种格式在门店场景够用还能在列表页直接用字符串排序。WorkOrderStatus是枚举类而不是硬编码数字这样状态流转逻辑在代码里可读性更高。前端调用时服务顾问选择客户车辆后表单会携带vehicleId、customerId、advisorId和可选的appointmentId。其中advisorId从当前登录用户信息中取值而不是由前端传避免员工替别人开单。4.2 派工与领料一个事务管住库存和工单派工和领料放在同一个事务里这是这套系统里我觉得最值得看的一段代码Transactional(rollbackFor Exception.class) public void assignWorkerAndMaterials(Long orderId, Long workerId, ListMaterialUsageDTO materials) { // 1. 校验工单状态只有已确认的工单才能派工 WorkOrder order workOrderMapper.selectById(orderId); if (!WorkOrderStatus.CONFIRMED.equals(order.getStatus())) { throw new BusinessException(当前工单状态不允许派工); } // 2. 写入派工记录 DispatchRecord record new DispatchRecord(); record.setOrderId(orderId); record.setWorkerId(workerId); record.setDispatchTime(LocalDateTime.now()); dispatchMapper.insert(record); // 3. 领料出库逐条扣减库存并写领料明细 for (MaterialUsageDTO material : materials) { Parts parts partsMapper.selectById(material.getPartsId()); if (parts.getStock() material.getQty()) { throw new BusinessException(备件库存不足 parts.getPartsName()); } parts.setStock(parts.getStock() - material.getQty()); partsMapper.updateById(parts); MaterialUsage usage new MaterialUsage(); usage.setOrderId(orderId); usage.setPartsId(material.getPartsId()); usage.setQty(material.getQty()); usage.setUnitPrice(parts.getSalePrice()); // 记出库时的销售价 materialUsageMapper.insert(usage); } // 4. 工单状态流转到维修中 order.setStatus(WorkOrderStatus.REPAIRING); workOrderMapper.updateById(order); }逻辑说明这个方法的重点在事务边界。库存扣减、领料明细写入、工单状态更新任何一个环节失败都会整体回滚不会出现“库存扣了但工单没记录”或者反过来。MaterialUsageDTO里只需要传partsId和qty单价不传从parts表现查防止前端改价格。并发场景下这段代码有优化空间比如用 SQL 条件更新实现原子扣减。门店场景并发量不高先查后改问题不大但如果以后要接线上预约入口建议把扣库存改成UPDATE parts SET stock stock - #{qty} WHERE part_id #{id} AND stock #{qty}这条 SQL 用行级锁保证扣减原子性返回 0 行说明库存不足。4.3 结算工时费加材料费的自动汇总Transactional(rollbackFor Exception.class) public Settlement settleOrder(Long orderId) { WorkOrder order workOrderMapper.selectById(orderId); if (!WorkOrderStatus.QUALITY_CHECKED.equals(order.getStatus())) { throw new BusinessException(质检未完成不能结算); } // 1. 汇总工时费所有维修项目工时 × 单价之和 BigDecimal laborFee workOrderItemMapper.sumLaborFee(orderId); // 2. 汇总材料费所有领料明细金额之和 BigDecimal materialFee materialUsageMapper.sumMaterialFee(orderId); // 3. 生成结算单 Settlement settlement new Settlement(); settlement.setOrderId(orderId); settlement.setLaborFee(laborFee); settlement.setMaterialFee(materialFee); settlement.setTotalFee(laborFee.add(materialFee)); settlement.setStatus(SettlementStatus.UNPAID); settlementMapper.insert(settlement); // 4. 工单流转到待交车 order.setStatus(WorkOrderStatus.WAITING_PICKUP); workOrderMapper.updateById(order); return settlement; }逻辑说明结算金额全部来自数据库汇总不从前端接收防止客户现场改价。sumLaborFee和sumMaterialFee是 MyBatis Plus 的 XML 自定义查询分别对work_order_item和material_usage表做求和。刚做完派工时领料明细还没审核的话这步校验就靠工单状态卡住所以在结算前必须走完质检流程。4.4 运行与验证把系统拉起来拿到源码后按三步走能跑通。先初始化数据库mysql -uroot -p sql/init.sqlSQL 脚本里建好了 12 张表并且写入了默认管理员账号和基础角色数据。然后改后端配置application.yml里的数据源地址、用户名、密码改成自己本地的启动后端服务mvn spring-boot:run前端是标准 Vue 工程装依赖、起开发服务器npm install npm run dev浏览器访问前端地址用默认管理员账号登录后先到「客户档案」里建一个客户和车辆再去「预约管理」创建一条预约单最后到「维修工单」点创建工单选择刚建的预约单这时候能看到工单状态是待确认后面每一步操作都会驱动状态往下走。验证整条链路是否通了最简单的办法是走完一遍结算到结算单页面看生成的金额是否等于工时费加材料费。5. 避坑指南这类管理系统开发中最容易踩的 5 个坑这几条坑是我在类似门店管理系统上实打实踩出来的每条都是先看现象、再找原因、最后定解决方案给正在改这套系统的你做个参考。5.1 事务注解写了但没生效数据写了一半现象调用开单接口时报错结果工单表插入了数据预约单状态却没有更新。重启系统后问题依旧。原因事务注解写在了 Controller 方法上而 Spring 的声明式事务基于 AOP 代理只有通过代理对象调用的 public 方法才会被拦截。更隐蔽的一种情况是 Service 内部this.method()自调用绕过了代理。解决把Transactional统一写在 Service 实现类的 public 方法上不要在 Controller 写事务Service 内部拆类调用避免同类自调用。5.2 用 double 算金额结算单出现一长串小数位现象一张 0.1 加 0.2 的结算单页面上显示 0.30000000000000004。原因二进制浮点数用 double 算十进制小数会产生精度误差结算金额这类数据不允许误差存在。解决所有金额字段用BigDecimal并且构造时用BigDecimal.valueOf()或者字符串构造不要用new BigDecimal(double)。前端传金额时传字符串而不是浮点数后端统一用字符串转BigDecimal。5.3 工单状态没做后端校验流程随便跳现象系统刚上线时有人直接在页面上把一张「待确认」工单点成了「已完成」。原因前端按钮只是按角色做了显示控制但后端接口没有校验当前状态是否允许目标状态。绕过前端页面的限制直接调用接口状态就被强制改了。解决在后端写一个统一的工单状态流转方法里面校验当前状态和下一个状态是否匹配所有状态变更全部走这个方法。类似这样public void changeStatus(WorkOrder order, WorkOrderStatus targetStatus) { if (!order.getStatus().canTransitTo(targetStatus)) { throw new BusinessException(非法状态流转 order.getStatus() - targetStatus); } order.setStatus(targetStatus); }业务规则放到后端前端只是体验层这个原则在管理系统里尤其重要。5.4 备件库存并发扣减出现负数现象两个工单同一秒领同一个备件库存最后显示负数。原因先查库存再更新库存两条线程同时查到了库存为 5各自扣 1结果数据库存的是 4 而不是 3。解决把扣减逻辑改成条件更新用数据库行级锁保证原子性UPDATE parts SET stock stock - #{qty} WHERE part_id #{id} AND stock #{qty}受影响行数为 0 就说明库存不足直接抛出业务异常。加了条件之后并发扣减就安全了。5.5 角色权限只管了菜单接口裸奔现象普通技师账号能通过浏览器开发者工具访问结算接口把收款状态改成已支付。原因前端只是隐藏了按钮和菜单接口层没有做权限校验。路由藏起来了不等于接口安全谁都能直接构造 HTTP 请求。解决在接口上加上基于权限码的鉴权注解比如 Sa-Token 的SaCheckPermission(settlement:pay)每个接口要求对应权限码才能访问。权限码在角色管理里给角色分配这样权限调整不用改代码。6. 二次开发方向从单机到联网的三个实战改造这套系统跑通之后真正要落地到门店还需要几个改造点这里挑三个我觉得优先级最高的讲。第一个改造是增加预约小程序入口。门店客户习惯在微信上预约保养改造方法是新增一个mini_customer表和预约接口小程序端提交客户姓名、电话、车牌、预约时间后端到点后生成预约记录。原有的appointment表不需要改动在新增的预约接口里校验手机号和车牌对应的客户档案没有档案的自动建档。小程序端用原生框架或 uni-app 都行后端接口复用现有的预约管理流程。第二个改造是局域网多端部署。门店前台、车间、备件库可能在不同电脑上操作把前端build后的静态文件丢给 Nginx后端服务跑在同一台局域网机器的 8080 端口。Nginx 里把/api/前缀反向代理到后端地址前端接口请求统一通过这个前缀走这样部署完成后不存在跨域问题所有电脑输入同一个 IP 就能访问系统。第三个改造是打印工单和结算单的模板。管理系统的打印不能靠浏览器默认打印需要在打印样式上单独适配。我的做法是在打印组件里加一层media print的 CSS用page size定义纸张尺寸表格、签名栏、金额合计全部用固定宽度布局然后调window.print()触发打印。打印内容单独放在一个隐藏的 div 里避免把页面按钮也打印出来。这三块做完这套系统就能从一个本地 Demo 变成门店日常在用的生产工具。说实话我第一次给类似门店定制这种系统时光应付维修工单的状态流转就改了三轮最早把状态流转散落在各个页面里后边维护时想死的心都有。后来学乖了每次动手改业务前先画一张状态流转图贴在工位边列出所有谁能在什么状态下做什么事然后才落代码。从那以后几乎没有因为状态问题返工过希望帮到你。本文还有配套的精品资源点击获取