ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

汽车修理管理系统实战:Spring Boot+Vue全栈开发与业务闭环解析

汽车修理管理系统实战:Spring Boot+Vue全栈开发与业务闭环解析

简介:这是一套面向汽车修理厂日常运营场景的数字化管理软件源码,适合计算机专业学生、中小型汽修企业管理者及希望学习桌面端信息管理系统的开发者参考。系统围绕客户档案、预约排程、维修订单、配件库存、财务收支、员工绩效与统计报表等模块展开,并配有数据备份与权限控制思路,可帮助读者理解汽修业务从接车到结算的完整数据流转。资源包共53个文件,约2.66MB,以h头文件与cpp源文件为主体,辅以obj编译产物、tlh/tli类型库文件、mdb数据库、rc资源脚本及exe可执行程序,构成一套可编译运行的VC工程骨架。目前已有678人学习下载。通过阅读源码,读者可掌握各功能对话框与业务逻辑的对应关系,借鉴数据库表结构设计与模块划分方式,并在此基础上进行二次开发或课程设计改造。

1. 汽修厂老板的Excel该换了:一个能跑起来的修理管理系统长什么样

如果你去问一个开汽修厂的老板,店里最头疼的事是什么,十有八九不是修不好车,而是“账对不上、件找不到、人管不住”。接车靠手写单子,配件靠脑子记库存,月底算工时全靠Excel拉公式,一旦师傅离职或者客户扯皮,连三个月前换过什么件都查不出来。这套“汽车修理管理系统”就是冲着这个场景来的——它把接车登记、维修派工、配件出入库、结算对账这几件事串成一条线,让前台、仓库、财务看到的是同一份数据。技术栈上,它走的是典型的Java Web路线:Spring Boot做后端,MyBatis-Plus管数据,MySQL存业务,前端用Vue加Element UI搭管理台。适合谁?一是中小型修理厂想摆脱纸质单据的,二是计算机专业做毕设或课程设计需要完整业务闭环的,三是想拿一个真实业务系统练手二次开发的。下面我按“能干什么、怎么跑、怎么改”的顺序,把这份源码拆开讲。

2. 系统骨架与业务闭环:从接车到结算的数据怎么流转

2.1 技术选型与分层结构

这套系统没有走微服务那套重装备,而是标准的单体分层架构,对中小项目来说这是最务实的选择——部署简单,调试直观,一个人也能维护。后端按Controller、Service、Mapper三层切分,实体类用MyBatis-Plus的注解直接映射表字段,省掉大量XML。前端是独立的前后端分离项目,通过Axios调REST接口,登录态用JWT令牌维持。

常见做法是后端端口跑8080,前端devServer跑8081并配置代理转发,这样本地开发不用处理跨域。数据库连接池用HikariCP,Spring Boot 2.x默认自带,配置里改改url和账号密码就能连上自己的库。

# application.yml 关键配置片段 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/repair_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true

这段配置里有两个参数值得注意:serverTimezone必须写对,否则插入时间会差8小时,这是MySQL 8.x的经典坑;map-underscore-to-camel-case开启后,数据库的create_time会自动映射到Java的createTime,不用手写ResultMap。

2.2 核心业务表与状态流转

系统的业务闭环围绕四张主表展开:车辆表(vehicle)、维修工单表(repair_order)、配件表(parts)、结算表(settlement)。工单表里有个status字段是整条链路的灵魂,它控制着一条维修记录能走到哪一步。

状态值含义可执行操作
0待派工前台可修改,可指派师傅
1维修中师傅可登记用料,不可删除
2待结算仓库已出库,财务可生成账单
3已结算只读,仅可查看和打印
4已取消只读,保留记录用于追溯

这个状态机是防止业务混乱的关键。我见过不少毕设项目把状态做成自由编辑,结果出现“已结算的工单又被改了配件”这种数据打架的情况。这里用状态值卡住操作入口,前端根据status渲染不同按钮,后端在Service层再做一次校验,双保险。

2.3 一个工单从进厂到出厂的完整链路

假设一辆车进厂,前台先查车辆表——如果是老客户,直接带出历史记录;新客户则先建档。然后创建工单,选维修项目(比如换机油、补漆),系统自动带出项目工时费和预估配件。派工给师傅后,状态从0变1。师傅在移动端或后台登记实际用料,仓库那边同步扣减库存。修完后状态变2,财务拉出结算单,算上工时、配件、折扣,生成应收金额。收款后状态变3,整个链路闭合。

// RepairOrderServiceImpl.java 状态流转核心方法 @Transactional(rollbackFor = Exception.class) public void updateStatus(Long orderId, Integer targetStatus) { RepairOrder order = this.getById(orderId); // 校验状态跃迁是否合法,防止跳步 if (!isValidTransition(order.getStatus(), targetStatus)) { throw new BusinessException("当前状态不允许此操作"); } // 进入结算前必须确认配件已全部出库 if (targetStatus == 2 && hasUnfinishedParts(orderId)) { throw new BusinessException("存在未出库配件,无法结算"); } order.setStatus(targetStatus); order.setUpdateTime(new Date()); this.updateById(order); }

这段代码的@Transactional注解保证了状态变更和后续操作的原子性,isValidTransition是个私有方法,内部用Map定义了允许的跃迁路径。参数targetStatus由前端传入,但后端不信任前端,必须重新校验。hasUnfinishedParts会去查配件出库记录表,这是很多简化版系统漏掉的环节,导致结算金额和实际用料对不上。

3. 本地跑起来:环境准备、建库与启动排错

3.1 环境清单与版本要求

在动手之前,先把环境对齐,版本不匹配是新手翻车最多的地方。这套系统对版本不算挑剔,但有几个底线:

  • JDK 1.8或11,别用17以上,MyBatis-Plus老版本在模块化下有反射问题
  • MySQL 5.7或8.0,8.0要改驱动类为com.mysql.cj.jdbc.Driver
  • Maven 3.6+,用来拉依赖和打包
  • Node.js 14或16,前端Vue 2项目对高版本Node兼容性一般
  • IDE用IDEA或Eclipse都行,IDEA对Spring Boot支持更顺

提示:如果你机器上已经装了多个JDK,记得在IDEA的Project Structure里把SDK和Language Level都设成8,只改环境变量有时候不生效。

3.2 建库与初始化数据

源码包里一般会带一个sql目录,里面是建表脚本和初始数据。操作顺序是先建库再导表:

# 登录MySQL后执行 CREATE DATABASE repair_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE repair_db; SOURCE /你的路径/repair_db.sql;

字符集一定用utf8mb4,不然客户姓名里的生僻字或者备注里的特殊符号会变问号。导入完成后用SHOW TABLES;确认表都建出来了,通常会有十几张表,除了前面说的四张主表,还有用户表、角色表、菜单表这些权限相关的。

初始数据里一般会预置一个管理员账号,密码是加密存的。如果你不知道密码,可以看sql文件里的insert语句,或者直接用工具生成一个MD5或BCrypt密文替换进去。常见做法是密码用BCrypt加密,登录时用BCryptPasswordEncoder.matches()比对。

3.3 后端启动与接口验证

后端启动就是标准的Spring Boot流程,但有几个地方容易卡住:

# 在项目根目录执行 mvn clean package -DskipTests java -jar target/repair-system-1.0.0.jar # 或者直接在IDEA里运行Application主类

启动日志里看到Started Application in x.x seconds就算成功了。然后打开浏览器或Postman访问http://localhost:8080/api/login,用POST方式传JSON:

{ "username": "admin", "password": "123456" }

如果返回的JSON里带token字段,说明后端和数据库通了。如果报Access denied for user,检查application.yml里的账号密码;如果报Unknown database,说明库没建对;如果报Table doesn't exist,说明sql没导全。这三个错误覆盖了90%的启动失败。

3.4 前端启动与联调

前端目录通常叫repair-web或frontend,进去先装依赖再跑:

npm install npm run serve

npm install如果卡住,换成淘宝镜像npm config set registry https://registry.npmmirror.com再试。启动成功后控制台会打印本地访问地址,一般是http://localhost:8081。这时候如果登录报404或跨域,检查vue.config.js里的proxy配置:

// vue.config.js 代理配置 module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', // 后端地址 changeOrigin: true, pathRewrite: { '^/api': '' } // 去掉前缀再转发 } } } }

changeOrigin设为true是为了让后端收到的Host头是目标地址,有些安全校验会检查这个。pathRewrite看后端接口有没有统一前缀,有就去掉,没有就删掉这行。联调通了之后,登录进首页应该能看到左侧菜单和顶部的用户信息。

4. 核心模块拆解:工单、库存、结算三块怎么写的

4.1 工单模块:分页查询与多条件筛选

工单列表是使用频率最高的页面,前台要根据车牌、状态、时间段来查。后端用MyBatis-Plus的QueryWrapper拼条件,比手写XML灵活。

// RepairOrderController.java 分页查询接口 @GetMapping("/page") public Result<IPage<RepairOrder>> page( @RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "10") Integer size, String plateNo, Integer status, String startDate, String endDate) { QueryWrapper<RepairOrder> wrapper = new QueryWrapper<>(); // 车牌模糊匹配,用户可能只记得部分 wrapper.like(StringUtils.isNotBlank(plateNo), "plate_no", plateNo); // 状态精确匹配 wrapper.eq(status != null, "status", status); // 时间范围,前端传的是字符串,直接比较 wrapper.ge(StringUtils.isNotBlank(startDate), "create_time", startDate); wrapper.le(StringUtils.isNotBlank(endDate), "create_time", endDate); wrapper.orderByDesc("create_time"); return Result.success(repairOrderService.page(new Page<>(current, size), wrapper)); }

like和eq的第一个参数是条件布尔值,只有为true时才拼进SQL,这样避免了写一堆if判断。orderByDesc保证最新的工单排最前面。参数current和size由前端分页组件传入,默认第一页每页10条。返回的IPage对象里包含总记录数和当前页数据,前端Element UI的table直接绑定就行。

4.2 配件库存:出库扣减与并发控制

库存模块最怕的是超卖——两个工单同时领同一个配件,库存只剩1个,结果都扣成功了。这里用乐观锁或者数据库行锁来解决。

// PartsServiceImpl.java 出库扣减 @Transactional(rollbackFor = Exception.class) public void outbound(Long partsId, Integer quantity, Long orderId) { // 先查当前库存 Parts parts = this.getById(partsId); if (parts.getStock() < quantity) { throw new BusinessException("库存不足,当前剩余:" + parts.getStock()); } // 用update语句带条件扣减,防止并发 boolean success = this.update() .setSql("stock = stock - " + quantity) .eq("id", partsId) .ge("stock", quantity) // 关键:库存足够才更新 .update(); if (!success) { throw new BusinessException("库存扣减失败,请重试"); } // 记录出库流水,用于追溯 PartsRecord record = new PartsRecord(); record.setPartsId(partsId); record.setQuantity(quantity); record.setOrderId(orderId); record.setType(1); // 1表示出库 partsRecordMapper.insert(record); }

关键在.ge("stock", quantity)这个条件,它让UPDATE语句变成UPDATE parts SET stock = stock - N WHERE id = ? AND stock >= N,数据库层面保证不会扣成负数。如果更新影响行数为0,说明并发时被别人抢先扣了,直接抛异常让前端重试。出库流水表一定要记,不然月底盘点时对不上账,你连查都没处查。

4.3 结算模块:工时费与配件费的合并计算

结算单的金额由三部分组成:工时费、配件费、其他费用(比如外加工),再减去折扣。工时费按维修项目的标准工时乘以费率,配件费按实际出库数量和售价算。

// SettlementServiceImpl.java 生成结算单 public Settlement generate(Long orderId) { RepairOrder order = repairOrderService.getById(orderId); // 汇总工时费 BigDecimal laborFee = orderItemMapper.sumLaborFee(orderId); // 汇总配件费,从出库流水里算 BigDecimal partsFee = partsRecordMapper.sumPartsFee(orderId); // 其他费用从工单附加项取 BigDecimal otherFee = order.getOtherFee() == null ? BigDecimal.ZERO : order.getOtherFee(); BigDecimal total = laborFee.add(partsFee).add(otherFee); // 折扣按百分比,比如打九折传90 if (order.getDiscount() != null && order.getDiscount() < 100) { total = total.multiply(new BigDecimal(order.getDiscount())) .divide(new BigDecimal(100), 2, RoundingMode.HALF_UP); } Settlement settlement = new Settlement(); settlement.setOrderId(orderId); settlement.setLaborFee(laborFee); settlement.setPartsFee(partsFee); settlement.setOtherFee(otherFee); settlement.setTotalAmount(total); settlement.setCreateTime(new Date()); this.save(settlement); return settlement; }

金额计算全部用BigDecimal,别用double,浮点误差在财务场景是致命的。divide的时候指定保留两位小数和舍入模式,HALF_UP是四舍五入,符合日常习惯。折扣字段存的是整数百分比,比如九折存90,这样前端传参简单,后端算的时候除以100。

5. 避坑与排查:启动、数据、权限的常见翻车现场

5.1 启动报错“找不到符号”或“程序包不存在”

现象:IDEA里一片红,Maven依赖下载了但编译不过。 原因:多半是Lombok插件没装或没启用。实体类上用了@Data注解,编译期需要Lombok生成getter/setter。 解决:IDEA里装Lombok插件并重启,同时在Settings里开启Annotation Processors。如果还不行,检查pom.xml里Lombok的scope是不是provided,是的话打包时不会带进去,但编译期需要。

5.2 登录成功但刷新页面就退出

现象:登录后能进首页,按F5刷新一下又跳回登录页。 原因:token存在了Vuex里,刷新后Vuex状态丢失,但没从localStorage或cookie里恢复。 解决:在路由守卫里加判断,如果Vuex没token但localStorage有,就重新拉用户信息并放行。常见做法是把token同时存localStorage,Vuex只做内存缓存。

5.3 工单列表查不到刚建的数据

现象:明明保存成功了,列表里却没有。 原因:查询条件里带了默认的时间范围,比如默认查当月,而测试数据的时间是上个月。 解决:检查前端查询表单的默认值,把时间范围清空再查。或者在后端把时间条件的ge和le改成只有传了值才拼。

5.4 配件出库后库存没变

现象:出库操作提示成功,但配件表里的stock字段没减。 原因:事务没生效,可能是@Transactional加在了private方法上,或者同类内部调用绕过了代理。 解决:确保注解加在public方法上,且调用方是通过Spring注入的Bean调用的,不是new出来的。另外检查数据库引擎是不是InnoDB,MyISAM不支持事务。

5.5 结算金额和手工算的对不上

现象:系统算出来的总额比财务手算的少几块钱。 原因:配件费汇总时漏掉了某些出库记录,比如退料后重新出库的,或者折扣计算时舍入方式不一致。 解决:把出库流水全部拉出来逐条核对,确认sumPartsFee的SQL有没有过滤条件。折扣计算统一用HALF_UP,别一处四舍五入一处直接截断。

6. 二次开发怎么改:从加字段到换架构的实操建议

拿到这套源码,最常做的改造是加字段和加模块。比如老板说“我要记录每辆车的保险到期日”,这就涉及车辆表加字段、实体类加属性、前端表单加输入框、列表加列,一条线改下来。我的习惯是先改数据库,再改实体,再改Mapper,最后改前端,顺序反了容易编译报错找不到字段。

-- 车辆表加保险到期日 ALTER TABLE vehicle ADD COLUMN insurance_expire DATE COMMENT '保险到期日' AFTER plate_no;

加完字段后,Vehicle实体类加private Date insuranceExpire;,MyBatis-Plus会自动映射。前端在车辆表单里加一个日期选择器,列表里加一列显示。如果要做“保险到期提醒”,可以在Service里加个定时任务,每天扫一遍快到期且没提醒过的车,往消息表插一条记录。

另一个常见改造是换权限模型。原系统如果是简单的角色判断,想改成细粒度的按钮级权限,可以引入Spring Security或Shiro。但我的血泪经验是:别一上来就重构,先把现有登录逻辑摸透,在原有User表基础上加角色和权限表,逐步替换。直接换框架容易把登录态搞崩,到时候连后台都进不去。

验证改造是否成功,我一般走三步:先单元测试Service层的新方法,再用Postman调接口确认返回结构没变,最后前端点一遍完整流程。特别是涉及金额和库存的改动,必须手工对一遍账。从那以后我每次改结算相关的代码,都强制走一遍“建单-派工-出库-结算-收款”的全链路,不敢跳步。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进