基于Spring Boot的4S店维修管理系统实战:从工单到结算全链路解析 简介面向汽车4S店运营场景的维修管理工具基于C#与.NET框架开发适合汽车行业信息化从业者、计算机专业学生及需要参考完整业务系统的开发者学习借鉴。系统围绕车辆管理、维修服务、配件管理、客户关系管理与报表分析五大模块展开涵盖车辆注册与状态跟踪、维修历史与费用核算、库存监控与采购销售记录、客户满意度调查及销售与维修成本报表等业务环节并配套数据库架构设计与数据字典说明便于理解表间关系、索引与存储过程等优化思路。压缩包为rar格式约49.36MB文件总数与类型明细上游暂未提供从描述看应包含源码工程、数据库脚本及说明文档等核心内容。已有479人学习下载可作为C#桌面应用与关系型数据库结合的实践参考帮助读者梳理4S店业务数据流与界面交互设计快速把握系统整体结构与实现要点。1. 从一张手写工单说起这套 4S 店维修管理系统到底解决什么问题上个月去朋友的修理厂帮忙看系统前台桌上还压着一沓手写维修工单客户车牌、故障描述、配件领用全靠笔填月底对账时三个人翻单据翻了整整两天。这不是个例很多中小 4S 店和综合维修厂的信息化程度就停在这个阶段有 ERP 但用不起来或者干脆用 Excel 硬扛。这套「大型汽车 4S 店维修管理系统」要解决的就是这条链路上的信息断层——从车辆进厂接待、故障诊断、维修派工、配件领用、结算开单到客户回访把整条业务流装进一个后台里。它适合两类人一是想给修理厂做定制化管理系统的小团队二是拿它当 Java Web 综合项目练手、想看懂一个真实业务系统怎么分层落地的开发者。下面我按「能干什么 → 怎么跑起来 → 核心代码怎么写的 → 怎么改」的顺序拆一遍。2. 技术选型与工程结构一套典型的 Java Web 分层骨架2.1 为什么是这套技术栈这类管理系统对高并发没要求但对业务表关系复杂、权限粒度细、报表导出多这三点要求很高。所以技术选型上走的是稳字路线后端 Spring Boot MyBatis-Plus数据库 MySQL 8前端如果是分离版就是 Vue Element UI如果是传统版就是 Thymeleaf 或 JSP 渲染。权限用 Spring Security 或 Shiro 做 RBAC缓存用 Redis 存登录态和字典数据文件上传车辆照片、检测报告落本地磁盘或对象存储。这套组合的好处是生态成熟出问题一搜一大把坏处是配置项多新手容易在依赖版本上翻车。我一般会把项目按业务域拆成几个模块而不是一个大泥球模块职责关键表基础数据客户、车辆、车型、供应商customer、vehicle、car_model维修业务预约、接待、派工、工单repair_order、work_order、dispatch配件库存入库、出库、领用、盘点part、stock_in、stock_out结算财务工时费、配件费、折扣、发票settlement、invoice系统管理用户、角色、菜单、日志sys_user、sys_role、sys_menu2.2 目录结构长什么样拿到源码先别急着跑先看目录心里有个地图。典型的后端结构是这样auto-repair-system/ ├── src/main/java/com/example/repair/ │ ├── common/ # 通用返回、异常、工具类 │ ├── config/ # Security、MyBatis、Redis 配置 │ ├── controller/ # 接口层按业务域分子包 │ ├── service/ # 业务逻辑接口 impl │ ├── mapper/ # MyBatis Mapper 接口 │ ├── entity/ # 数据库实体 │ └── dto/vo/ # 入参出参对象 ├── src/main/resources/ │ ├── mapper/ # XML 映射文件 │ ├── application.yml # 主配置 │ └── db/init.sql # 建表 初始数据 └── pom.xml这个结构的关键在于 controller 只做参数校验和调用service 承担事务和业务规则mapper 只管 SQL。很多新手把业务逻辑写进 controller后面加个「维修单作废要回滚库存」的需求就得改三四个地方血泪经验就是一开始就守住分层。2.3 数据库设计里最容易被忽略的两张表维修系统的核心不是工单表而是工单和配件之间的关联表。一张repair_order记录车辆、客户、故障、状态一张order_part记录这张单子领了哪些配件、数量多少、单价多少。为什么单独拆因为同一张工单可能分多次领料而且配件价格会变必须在领用那一刻把单价快照下来否则月底结算时配件调价了历史单据金额就全乱了。这是我在实际项目里踩过的坑后来所有涉及金额的关联表都强制存快照字段。CREATE TABLE order_part ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL COMMENT 维修工单ID, part_id BIGINT NOT NULL COMMENT 配件ID, quantity INT NOT NULL COMMENT 领用数量, unit_price DECIMAL(10,2) NOT NULL COMMENT 领用时的单价快照, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_order (order_id) ) COMMENT 工单配件领用明细;unit_price这个字段就是后悔药没有它后面做毛利报表时只能拍脑袋。idx_order索引保证按工单查明细时不走全表扫描。3. 把项目跑起来环境、配置与启动验证3.1 环境准备与依赖版本跑之前先把环境对齐版本不一致是新手翻车重灾区。建议 JDK 17、Maven 3.8、MySQL 8.0、Redis 6、Node 18前端分离版才需要。MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver连接串要带时区和编码参数否则中文和日期会出玄学问题。spring: datasource: url: jdbc:mysql://localhost:3306/auto_repair?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0serverTimezone不写插入时间可能差 8 小时useSSLfalse是本地开发省掉证书握手生产环境要按实际改。Redis 如果只是存登录态database: 0够用别和别的项目共用同一个库键冲突排查起来很痛苦。3.2 初始化数据库与启动先执行src/main/resources/db/init.sql它会建库建表并插入管理员账号和基础字典。然后mvn clean package -DskipTests打包java -jar target/xxx.jar启动或者直接在 IDE 里跑主类。启动日志里看到 Tomcat started on port 8080 和 Started Application 就算起来了。mysql -uroot -p -e CREATE DATABASE auto_repair DEFAULT CHARSET utf8mb4; mysql -uroot -p auto_repair src/main/resources/db/init.sql mvn clean package -DskipTests java -jar target/auto-repair-system-1.0.0.jar建库时显式指定utf8mb4不然客户姓名里的生僻字、备注里的表情符号会存成问号。打包跳过测试是为了先跑通等环境稳定了再把测试打开。3.3 登录验证与接口自测默认管理员账号一般在 init.sql 里写死登录后先看菜单是否加载完整。如果菜单空白八成是角色菜单关联表没初始化或者 Security 的权限表达式把接口拦了。接口自测建议用 curl 或 Postman 先打一个不需要权限的字典接口确认数据库连通再打登录接口拿 token最后带 token 访问工单列表。curl -X POST http://localhost:8080/api/login \ -H Content-Type: application/json \ -d {username:admin,password:123456}返回体里应该有 token 和用户信息。如果返回 401先看密码是否被 BCrypt 加密过、数据库里存的是不是明文如果返回 500看日志里是不是 Redis 连不上导致 token 生成失败。这一步过了说明整条链路是通的后面改代码才有底。4. 核心模块拆解一张维修工单从进厂到结算的完整链路4.1 工单创建与状态机维修工单是整个系统的主动脉它的状态流转必须严格。常见状态是待接待 → 已接待 → 维修中 → 待结算 → 已完成 → 已作废。每次状态变更都要校验前置状态不能从「待接待」直接跳到「已完成」。我一般把状态流转规则集中在一个枚举或状态机类里而不是散落在各个 service 的 if-else 中。public enum OrderStatus { PENDING(0, 待接待), RECEIVED(1, 已接待), REPAIRING(2, 维修中), SETTLING(3, 待结算), FINISHED(4, 已完成), CANCELLED(9, 已作废); private final int code; private final String desc; // 构造、getter 省略 public static boolean canTransfer(int from, int to) { return switch (from) { case 0 - to 1 || to 9; case 1 - to 2 || to 9; case 2 - to 3 || to 9; case 3 - to 4; default - false; }; } }canTransfer把合法流转写死service 里调用前先判断非法流转直接抛业务异常。这样后面加「维修中可退回接待」的需求只改这一个方法不用满项目找 if。注意作废只能从前期状态发起已结算的单子作废要走冲红流程不能简单改状态。4.2 配件领用与库存扣减领用配件是并发最容易出问题的地方。两个人同时给不同工单领同一个配件如果先查库存再更新中间可能超卖。正确做法是用数据库行锁或乐观锁在一条 SQL 里完成「库存足够才扣减」。Transactional(rollbackFor Exception.class) public void deductStock(Long partId, int quantity, Long orderId) { int affected partMapper.deductStock(partId, quantity); if (affected 0) { throw new BizException(配件库存不足或不存在); } orderPartMapper.insert(buildOrderPart(partId, quantity, orderId)); }对应的 Mapper SQL 是UPDATE part SET stock stock - #{qty} WHERE id #{id} AND stock #{qty}。affected 0说明库存不够事务回滚明细也不会插入。这里Transactional的rollbackFor必须写 Exception否则受检异常不回滚库存扣了明细没进去账就对不上。这是我在生产环境真实翻过的车后来所有涉及多表写的 service 方法都强制加这个参数。4.3 结算金额计算与快照结算时金额由工时费 配件费 - 折扣组成。配件费直接读order_part里的单价快照乘数量工时费按维修项目对应的工时单价算。折扣要记录是谁批的、批了多少方便审计。public BigDecimal calculateTotal(Long orderId) { BigDecimal labor orderItemMapper.sumLaborFee(orderId); BigDecimal parts orderPartMapper.sumAmount(orderId); BigDecimal discount settlementMapper.getDiscount(orderId); return labor.add(parts).subtract(discount).max(BigDecimal.ZERO); }max(BigDecimal.ZERO)是兜底防止折扣配错导致负数。金额一律用 BigDecimal别用 double0.1 0.2 的问题在财务模块是致命的。所有汇总查询走 SQL 聚合而不是在 Java 里循环累加数据量大时差距很明显。4.4 权限控制落到接口粒度4S 店的角色分得很细前台只能建单不能改价维修师傅只能看自己派工的单财务才能结算。用 Spring Security 的PreAuthorize在方法上标注权限点比在拦截器里写 URL 匹配清晰得多。PreAuthorize(hasAuthority(order:settle)) PostMapping(/settle/{orderId}) public Result settle(PathVariable Long orderId) { settlementService.settle(orderId); return Result.ok(); }权限字符串要和菜单表里的 perms 字段一致否则会出现「菜单看得见、按钮点不动」的经典问题。角色和权限的关联建议做成可配置别写死在代码里不然每加一个岗位就要发一次版。5. 避坑与排查跑不起来时先看这几处5.1 启动报数据源找不到驱动现象是启动直接抛Failed to determine a suitable driver class。原因通常是 pom 里引了 MySQL 驱动但 scope 写成 provided或者连接串前缀写成了jdbc:mysql://却漏了驱动依赖。解决确认mysql-connector-j在 runtime scope连接串和驱动类名匹配 MySQL 8 的com.mysql.cj.jdbc.Driver。5.2 登录成功但接口一直 403现象是能登录、菜单也有但点任何业务按钮都提示无权限。原因多半是 token 里的权限集合没加载或者PreAuthorize的权限字符串和数据库 perms 大小写、冒号不一致。解决在登录返回里打印 authorities和 sys_menu.perms 逐条比对统一用小写加冒号分隔。5.3 中文乱码现象是客户姓名、故障描述存进库变成问号。原因在建库字符集或连接串编码。解决库表统一utf8mb4连接串带characterEncodingutf8前端请求头Content-Type: application/json;charsetUTF-8。三处缺一处都可能乱。5.4 库存扣成负数现象是盘点时发现某配件库存为负。原因是没有用条件更新或者扣减和明细插入不在同一事务。解决扣减 SQL 必须带stock qty条件service 方法加Transactional(rollbackFor Exception.class)并对affected 0显式抛异常。5.5 时间差 8 小时现象是新建工单的创建时间比实际早或晚 8 小时。原因在 JDBC 连接串没配serverTimezone或者实体字段用了java.util.Date而数据库是 datetime。解决连接串加serverTimezoneAsia/Shanghai实体统一用LocalDateTime序列化配置时区。6. 二次开发怎么改从加一个字段到接一条新业务线拿到这套系统后最常见的改造需求是「加字段」「加报表」「接支付」。加字段看着简单但要走完 entity → mapper XML → DTO → 前端表单 → 列表展示五处漏一处就出现「存进去了但列表不显示」。我的习惯是先改数据库和 entity再用 IDE 的全局搜索找所有引用该实体的地方逐个确认。加报表则优先在 mapper 里写聚合 SQL别在 Java 里拉全量再算工单量上万后内存和响应时间都会难看。接支付或开票这类外部能力建议单独抽一个integration包用接口隔离别把第三方 SDK 直接塞进 settlementService。这样以后换服务商只改实现类。下面是一个简单的扩展点写法public interface InvoiceProvider { InvoiceResult issue(InvoiceRequest request); } Service public class LocalInvoiceProvider implements InvoiceProvider { Override public InvoiceResult issue(InvoiceRequest request) { // 本地模拟开票返回票号 return InvoiceResult.success(LOCAL- System.currentTimeMillis()); } }业务层只依赖InvoiceProvider接口配置里决定注入哪个实现。验证改造是否成功我一般跑三条线单元测试覆盖金额计算、接口测试覆盖状态流转、手工走一遍「建单 → 领料 → 结算」全流程看库存和金额是否对得上。最后提醒一句改任何涉及金额和库存的代码前先在测试库造一批边界数据库存刚好等于领用量、折扣大于总额跑通了再上。从那以后我每次动结算和库存模块都强制先把这三条验证线走一遍省下来的返工时间远比写测试的时间多。希望帮到你。本文还有配套的精品资源点击获取