简介一套大型汽车4S店维修管理系统的完整项目文件采用C#语言开发适合软件专业学生作为课程设计、毕业设计参考也可供初入.NET方向的开发者学习典型分层管理系统的实现思路。项目覆盖车辆管理、维修服务、配件库存、客户关系与报表分析等核心模块附带SQL建库脚本与数据字典说明能帮助理解字段含义、表间关系及业务数据流。RAR压缩包约49.36MB内容以C#源码、SQL数据库脚本、界面配置和工程文件为主结构清晰便于按模块查阅。目前已有479人学习下载。通过阅读源码与数据字典可掌握C#连接SQL Server、基础增删改查、常用控件布局及数据库表设计等实用技能也可结合实际场景二次扩展使用。1. 大型汽车4S店维修管理系统一张工单管到底的全链路实践车间主管老李每天早上最头疼的不是技师不够用而是昨天哪台车修到哪一步、哪个配件欠料、哪位客户该回访——这些信息散落在纸质工单、Excel 表格和微信聊天里。这套大型汽车4S店维修管理系统的核心价值就是把「客户预约—接待开单—派工维修—配件出库—质检结算—回访跟踪」这条售后链路全部落到一套系统里让管理者随时能看清每一张工单的状态、每一位技师的负荷、每一个配件的流向。它不是单纯的教学 Demo而是一个能支撑几十个工位、日均上百张工单的完整业务系统适合正在做进销存或企业级管理系统二次开发的工程师也适合4S店售后信息化的产品经理用来梳理业务模型。2. 技术选型与整体架构单体应用如何支撑4S店全流程2.1 技术栈为什么是 Spring Boot MySQL Vue这套系统采用了目前企业级管理系统里最主流的组合后端用 Spring Boot 做单体应用前端用 Vue 2 Element UI 搭管理后台数据库选 MySQL部署时打成一个 jar 包扔到服务器上就能跑。这个选型在 4S 店维修管理这个场景里很务实——业务复杂度没有高到需要微服务拆分单体应用反而让部署、调试、备份都简单得多。后端分层上Controller 只做参数接收和结果返回Service 层处理业务规则Mapper 层用 MyBatis-Plus 操作数据库省去了大量手写 SQL 的重复工作。权限认证集成的是 Spring Security JWT登录后前端把 token 存在本地每次请求带上拦截器统一校验。这套组合在同类系统里算是「标准答案」好处是遇到的坑网上都有现成解法团队招人也好上手。前端这边没有用重型脚手架就是 Vue CLI 创建的标准项目配合 Vue Router 做页面跳转、Vuex 存全局状态。表格用 Element UI 的 el-table 渲染工单列表表单用 el-form 做校验日期范围、下拉选择、弹窗确认这些组件都是现成的。整个前端没有复杂的自定义组件维护成本很低。2.2 目录结构从启动类到业务模块的划分拿到源码后先把整体结构看清楚才知道改哪里。repair-system ├── src/main/java/com/repair │ ├── RepairApplication.java # 启动类 │ ├── common/ # 通用配置类 │ │ ├── Result.java # 统一返回体 │ │ ├── JwtInterceptor.java # token 拦截器 │ │ └── GlobalExceptionHandler.java # 全局异常处理 │ ├── controller/ # 接口层 │ │ ├── WorkOrderController.java # 工单模块 │ │ ├── CustomerController.java # 客户模块 │ │ ├── PartsController.java # 配件模块 │ │ ├── TechnicianController.java # 技师模块 │ │ └── SettlementController.java # 结算模块 │ ├── service/ # 业务逻辑层 │ │ ├── WorkOrderService.java │ │ └── impl/ │ ├── mapper/ # MyBatis-Plus 数据访问层 │ ├── entity/ # 数据库实体类 │ └── config/ # 跨域、安全等配置 ├── src/main/resources │ ├── application.yml # 数据源/Redis/日志配置 │ └── mapper/ # 自定义 SQL 映射文件 └── frontend/ # Vue 前端工程 ├── src/api/ # 接口封装 ├── src/views/ # 页面组件 └── src/router/index.js # 路由表这个划分遵循的是最常规的单体分层实体类对应数据库表结构Mapper 负责查数据Service 里写业务规则Controller 暴露 HTTP 接口。比较值得留意的一点是项目把所有模块的通用逻辑统一返回体、异常处理抽到了 common 包里前端拿到接口返回时先判断code字段再处理data格式统一之后联调省了很多事我一般新建项目时也会先把这个骨架搭好。2.3 部署架构与关键配置部署架构走的是「上传 jar 静态文件分离」的方案。前端工程构建后生成的 dist 目录扔到 Nginx 里做静态托管后端 jar 包单独跑在 8080 端口Nginx 配置一条/api开头的反向代理把请求转给后端服务。这样做的好处是前后端彻底分离开前端挂了后端不受影响而且静态资源由 Nginx 负责传输Tomcat 的压力小很多。application.yml里有几个关键配置数据源要改成自己环境的数据库地址和账号密码文件上传路径配置了绝对路径用于保存客户驾驶证照片和维修单据扫描件MyBatis-Plus 开启了驼峰命名自动映射这样数据库里create_time字段能直接映射到实体的createTime属性上不用每张表都写一堆columnMap。3. 核心模块拆解一张维修工单从进入到完结的完整链路3.1 工单创建VIN 码查询与客户信息自动带出维修工单是这个系统最核心的实体。前台接待接到客户车辆后第一件事是扫描行驶证上的 VIN 码车辆识别码系统拿这个唯一标识去客户表里查历史记录。老客户直接带出车主姓名、手机号、车辆型号和上次保养里程新客户则进入建档流程。工单创建接口的核心逻辑如下Override Transactional(rollbackFor Exception.class) public Result createWorkOrder(WorkOrderCreateDTO dto) { // 1. 根据 VIN 码查客户不存在则自动建档 Customer customer customerMapper.selectOne( new LambdaQueryWrapperCustomer() .eq(Customer::getVinCode, dto.getVinCode())); if (customer null) { customer new Customer(); customer.setVinCode(dto.getVinCode()); customer.setName(dto.getCustomerName()); customer.setPhone(dto.getPhone()); customer.setCarModel(dto.getCarModel()); customerMapper.insert(customer); } // 2. 创建工单主表记录状态初始为待接待 WorkOrder workOrder new WorkOrder(); workOrder.setOrderNo(generateOrderNo()); // 生成单号日期流水号 workOrder.setCustomerId(customer.getId()); workOrder.setCarMileage(dto.getCarMileage()); workOrder.setStatus(WorkOrderStatus.PENDING_RECEPTION.getValue()); workOrder.setCreateTime(LocalDateTime.now()); workOrderMapper.insert(workOrder); // 3. 保存客户报修的项目描述一条条写入子表 ListWorkOrderItem items dto.getItems().stream().map(item - { WorkOrderItem detail new WorkOrderItem(); detail.setOrderId(workOrder.getId()); detail.setItemName(item.getItemName()); detail.setItemType(item.getItemType()); // 维修/保养/钣喷 detail.setCustomerDesc(item.getCustomerDesc()); return detail; }).collect(Collectors.toList()); workOrderItemMapper.insertBatch(items); return Result.success(workOrder.getId()); }这段代码里有几个处理值得注意。Transactional保证了客户建档、工单创建、报修项目写入是原子操作任何一个环节失败就整体回滚不会出现客户建好了但工单没生成的情况。生成单号的方法用的是yyyyMMdd加当天流水号比如202501150013表示 1 月 15 日的第 13 张单排除了数据库自增 ID 被猜测的风险。参数 DTO 里前端传入的items是数组对应的 JSON 结构是[{ itemName: 更换机油, itemType: 保养 }]一次前端请求完成主表和子表的双重写入。3.2 维修派工技师池与工位状态的动态分配接待建单之后车间主任在派工看板上能看到所有待派工的工单。系统支持两种派工方式手工指定技师或者按技能标签自动推荐。自动推荐的规则是先筛选具备该维修项目技能标签的技师再按当前待办工单数量排序取最空闲的那个。派工完成后技师在手机端或车间大屏端能看到自己的工单队列点击「开始维修」后工单状态变为维修中同时该技师的负载 1工位状态同步变为占用。关键点在于技师操作时能实时推进状态整个链路是待接待 - 待派工 - 维修中 - 待质检 - 待结算 - 已完结这个状态机在代码里是一个枚举类WorkOrderStatus每个状态变更都做了前置校验不允许跳过状态。比如工单还在待接待时直接点结算后端会返回「当前状态不允许该操作」。这种防御式写法的好处是即使前端按钮被绕过后端的数据也不会变脏。3.3 配件出库库存扣减与锁定机制维修过程中需要更换配件时技师在工单详情里选择配件并填写用量。这个操作会走一条「检查库存 → 锁定库存 → 实际出库」的流程。先看代码public Result pickParts(PartPickDTO dto) { // 检查是否有足够库存多个配件要逐一校验 for (PartPickItem item : dto.getItems()) { PartsStock stock partsStockMapper.findByPartId(item.getPartId()); if (stock.getAvailableQty() item.getQty()) { return Result.error(配件[ stock.getPartName() ]库存不足); } } // 回写工单子表记录配件消耗明细 // 生成配件出库单 PartOutbound outbound new PartOutbound(); outbound.setOrderId(dto.getOrderId()); outbound.setOperatorId(CurrentUserHolder.getUserId()); outbound.setTotalAmount(calcTotalAmount(dto.getItems())); partOutboundMapper.insert(outbound); // 扣减可用库存同时写一条流水 for (PartPickItem item : dto.getItems()) { partsStockMapper.deductAvailableQty(item.getPartId(), item.getQty()); partStockLogMapper.insertLog(dto.getOrderId(), item, OUTBOUND); } return Result.success(); }这里「可用库存」和「物理库存」是区分开来的。物理库存是仓库里实际有的数量可用库存是扣掉了已被其他工单预占的数量。如果一个技师操作到一半没提交可用库存不变化不会错误扣减。实际项目中这条逻辑很容易翻车——不加锁的扣减在高并发下会导致超卖但在 4S 店这种日均几十单的场景下数据库行锁配合UPDATE ... SET available_qty available_qty - #{qty} WHERE available_qty #{qty}这种乐观扣减写法就够了没必要引入 Redis 分布式锁。3.4 结算与回访费用聚合和满意度跟踪车辆维修完成后进入结算环节。结算单的费用由三部分聚合而来配件费取自出库单明细、工时费按维修项目乘以工位费率、其他费用如外加工费、拖车费。系统支持预结算和正式结算预结算给客户确认报价客户点头后生成正式结算单并打印。结算完成后系统会自动在回访任务表里生成一条记录到期后客服跟进。回访结果记录客户满意度评分和意见如果评分低于 3 分系统自动推送消息给售后经理。这块业务逻辑不复杂但它是 4S 店客户留存的关键闭环很多管理系统都会做但做得不完整这套系统把这个环节落到了实处。4. 数据库设计读懂这几张核心表就掌握了业务骨架4.1 核心表清单和关系整个系统一共十几张表但真正核心的就六张理解了这六张表之间的关系整个系统的数据流向就清楚了。表名作用关键字段customer客户车辆档案id, vin_code, name, phone, car_modelwork_order维修工单主表id, order_no, customer_id, status, create_timework_order_item工单维修项目子表id, order_id, item_name, item_typeparts_stock配件库存表id, part_no, part_name, available_qtypart_outbound配件出库单id, order_id, total_amount, operator_idsettlement结算单id, order_id, part_fee, labor_fee, total_fee表间关系也比较直接customer与work_order是一对多一个客户可能有多张工单work_order与work_order_item是一对多一张工单有多个维修项目part_outbound挂在work_order下同时通过明细表关联多个配件settlement与work_order是一对一一张工单最终只有一张有效结算单。业务上一个容易混淆的细节是如果客户修完后返工是否需要新建工单这套系统的做法是不新建直接在原工单下追加维修项目状态回退到维修中。这样财务对账时能看到一张工单的完整花费而不是拆成两张导致成本核算失真。4.2 工单主表与子表的设计工单表最重要的设计决策是状态字段用int存储而不是用varchar。这个选择的好处是排序和过滤效率高代码里用枚举来映射避免在多个地方散落魔法数字。看建表 SQLCREATE TABLE work_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 工单号日期流水, customer_id bigint(20) NOT NULL COMMENT 客户ID, car_plate varchar(10) DEFAULT NULL COMMENT 车牌号, car_mileage int(11) DEFAULT NULL COMMENT 进店里程, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0待接待 1待派工 2维修中 3待质检 4待结算 5已完结, appointment_time datetime DEFAULT NULL COMMENT 预约时间, receive_time datetime DEFAULT NULL COMMENT 实际接待时间, finish_time datetime DEFAULT NULL COMMENT 完工时间, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_customer (customer_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时有几个细节是实战中踩过坑才补上的order_no加了唯一索引防止并发下生成重复单号customer_id和status分别建索引因为列表查询基本上都是按客户或按状态过滤所有时间字段都用了datetime没有用timestamp避免 2038 年问题。update_time用了ON UPDATE CURRENT_TIMESTAMP这样每次行记录更新时自动刷新不需要在应用代码里手动赋值。4.3 库存表与出入库流水配件库存表单独说是因为它有一个容易忽略的业务点同一配件要区分「原厂件」「副厂件」「再制造件」三种类型价格和质保周期都不同。所以主键不是配件编号而是配件编号加配件类型的联合标识。CREATE TABLE parts_stock ( id bigint(20) NOT NULL AUTO_INCREMENT, part_no varchar(32) NOT NULL COMMENT 配件编号, part_name varchar(64) NOT NULL COMMENT 配件名称, part_type tinyint(4) NOT NULL COMMENT 1原厂 2副厂 3再制造, available_qty int(11) NOT NULL DEFAULT 0 COMMENT 可用库存, physical_qty int(11) NOT NULL DEFAULT 0 COMMENT 物理库存, safe_qty int(11) NOT NULL DEFAULT 0 COMMENT 安全库存警戒线, purchase_price decimal(10,2) DEFAULT NULL, sale_price decimal(10,2) DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_parts (part_no, part_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;每次入库、出库、盘点调整都往part_stock_log里写一条流水记录操作类型、关联单号、操作前后数量、操作人和操作时间。库存流水表只增不改做审计追踪时能还原任意时刻的库存快照。实际项目里只要能坚持「有变动必有流水」财务对账的时候就不会一头雾水。5. 部署运行与避坑指南从拿到代码到跑起来5.1 环境准备与数据库初始化运行这套系统需要 JDK 1.8、MySQL 5.7、Maven 3.6前端构建需要 Node.js 14。数据库初始化不需要手动建表项目里带了一个db/init.sql脚本包含全部建表语句和基础数据比如管理员账号、默认部门、默认配件分类。执行方式mysql -u root -p db/init.sql初始化脚本里包含了一个管理员账号用户名admin初始密码admin123。第一次登录后要在员工管理里强制改密否则如果部署在公网服务器上很容易被扫描器爆破——这是我见过的管理系统最常出现的低级安全问题。后端配置文件application.yml需要改三个地方数据库地址、数据库账号密码、文件上传路径。文件上传路径要指定为绝对路径比如/data/repair-system/upload/确保目录存在且有写权限否则上传客户证件时会报「系统找不到指定路径」。前端启动则比较简单# 进入前端目录 cd frontend npm install npm run dev开发模式下前端默认跑在 9527 端口通过 Vite 或 Webpack 的代理把/api请求转发到后端的 8080 端口。这里有个细节npm install如果报错多半是 Node 版本太高或太低导致的项目里package.json的 engines 字段声明了版本区间尽量保持一致。5.2 后端启动与接口验证后端直接用 Maven 启动mvn spring-boot:run看到日志里出现Started RepairApplication in x seconds就说明启动成功。验证接口是否正常可以访问http://localhost:8080/api/login用管理员账号登录拿到 token 后请求工单列表接口。也可以直接访问http://localhost:8080/doc.html如果项目集成了 Knife4j 的话在 Swagger 文档页面里调试所有接口比用 Postman 手动构造请求要方便很多。启动过程如果报端口被占用改application.yml里的server.port换一个端口同时记得同步修改前端代理配置。这里最容易踩的坑是前端代理写的是target: http://localhost:8080而后端跑在 8081结果前端页面能打开但所有接口都请求失败看浏览器控制台才发现是 404。排查这类问题第一步永远是在浏览器 Network 面板里确认请求实际打到了哪个端口。5.3 常见问题与排查五个高频故障的完整记录现象一前端登录后跳转到首页但列表数据加载不出来控制台报 401。原因token 过期或根本没有被正确保存。JWT 是有有效期的脚手架里默认设置了 24 小时过期。用户隔天打开系统本地存的 token 已失效拦截器直接返回 401。解决在前端封装的 axios 实例里加响应拦截器检测到 401 时清除本地 token 并跳回登录页重新登录。这个处理逻辑应该放在前端最外层而不是每个请求单独写。现象二上传客户资料图片报错提示「系统找不到指定的路径」。原因文件上传配置的目录不存在或者应用没有创建目录的权限。Windows 下常见的是写了C:\upload但该目录没创建Linux 下常见的是目录存在但运行 jar 的用户没有写权限。解决启动前手动创建目录并赋权或者在代码里用FileUtil.mkdir()在启动时确保目录存在。Linux 上执行mkdir -p /data/repair-system/upload chown -R appuser:appuser /data/repair-system。现象三并发创建工单时出现「单号重复」错误。原因工单号生成逻辑是「日期 内存中的自增数字」但项目重启后自增数字重置或者两个请求在同一毫秒内各生成了一样的流水号触发了唯一索引。解决改成「日期 Redis INCR」自增或者直接查数据库当天最大流水号加一。不引入 Redis 的话用「日期 随机四位」也能大大降低冲突概率同时保留唯一索引兜底。现象四结算时配件费金额与实际出库配件成本对不上。原因出库单是在派工阶段生成的但结算时配件价格可能已经调整过供应商调价而结算单读取的是当前价格而不是出库时快照价格。解决在出库单明细上冗余保存「当时单价」结算时直接取快照金额而非实时单价。这提醒了一个经验凡是涉及钱的数据都要在业务发生那一刻做快照不要事后去查主数据表。现象五数据库连接池报错Too many connections。原因默认的 HikariCP 最大连接数是 10如果后台上传了几个耗时较久的导出任务占用连接不释放其他请求就全排队等连接。解决在application.yml里调大maximum-pool-size到 30~50取决于 MySQL 实例的max_connections同时排查是否有事务没提交或 SQL 慢查询导致连接长时间被占用。6. 二次开发思路加一个预约提醒模块的完整路径如果要把这套系统落地成自己的产品我建议从「预约提醒」这个模块入手练手。它不涉及复杂的表结构变更但能覆盖一条完整的开发链路建表、写接口、改页面、联调验证。在customer表旁新增一张appointment表字段包括预约人、联系电话、预约时间、服务类型、备注和状态。后端写一个定时任务每小时扫描一次预约表把预约时间在 4 小时内的记录查出来然后对接短信网关或微信模板消息推送提醒。接口设计上预约管理一组 RESTful API 就够了POST /appointment新增预约、GET /appointment/today查询今日预约、PUT /appointment/{id}/status调整预约状态待确认/已确认/已到店/爽约。前端在首页顶部加一个「今日预约」的卡片区域显示未来几个时段的预约列表数据轮询 30 秒刷新一次。接短信通道时注意把验证码发送和业务短信分开业务短信用模板 ID 去匹配省去自己拼接内容的尴尬。联调时的验证方法是造一条预约时间是 10 分钟后的数据改系统时间或直接改数据库时间戳触发定时任务确认短信能收到且内容没有乱码。做完这个模块你会把工单、预约、客户三块数据串起来理解再往后加「保养到期提醒」根据车型和上次保养里程推荐下次保养时间就顺理成章了。这套系统给我的教训是管理类系统不在乎技术多炫重点在于状态流转严谨、数据有据可查、异常能正常兜底。从那以后我每做一个模块都会写一份「字段变更记录」哪怕只改了一个字段类型也记录下来几个月后回头查问题能少掉一半头发。希望帮到你。本文还有配套的精品资源点击获取