简介本资源为基于JavaJSP的汽车维修保养管理系统完整项目包面向计算机相关专业毕业设计学生及Java Web初学者帮助解决课程设计或毕设选题中缺乏完整可运行系统的难题。系统前端采用JSP与jQuery后端基于Servlet与JDBC实现区分管理员与用户两种角色集成车主信息管理、保养管理、维修管理、年检保险管理及预约信息管理等功能模块业务逻辑贴近真实汽修门店场景。压缩包共166个文件包含72个JSP页面、17个JS脚本、7个Java源文件、7个CSS样式表及1个SQL数据库脚本另附PNG、JPG等图片素材与依赖jar包整体约4.29MB目录结构清晰便于按模块查阅与二次开发。目前已有41人学习下载。读者可获得完整源码、数据库脚本与前端资源直接导入IDEA并配置MySQL、Tomcat即可运行适合作为毕设参考方案也可用于学习ServletJDBC的经典开发模式与JSP页面组织方式。1. 汽车维修保养管理系统从工单到库存的一条完整链路一家综合修理厂最怕的不是没车修而是车修完了账对不上。前台手写接车单师傅凭记忆领配件月底盘点发现机油少了两箱、刹车片多出一套客户回访时又说不清上次换了什么。这类问题在单店月进场量超过 300 台次之后会集中爆发靠 Excel 和微信群已经压不住。汽车维修保养管理系统要解决的就是这条链路上的信息断层接车登记、派工、领料、结算、回访、库存预警每个环节都落到数据库里谁在什么时间动了哪条记录都能查。这套基于 Java JSP 的汽车维修保养管理系统技术栈是典型的 Servlet/JSP JDBC MySQL适合课程设计、小型修理厂内部工具、以及想练手 Java Web 全链路增删改查的开发者。它不追求微服务和高并发胜在结构直白、部署门槛低一台 2 核 4G 的云主机就能跑起来。下面按「先立住数据模型、再跑通核心流程、最后处理坑」的顺序拆开讲源码和数据库脚本按常见做法组织你照着改就能落地。2. 数据模型先立住七张表撑起维修保养业务2.1 为什么表结构决定这套系统能不能用很多人拿到汽车维修保养管理系统源码第一反应是打开登录页看界面这是典型的翻车起点。维修业务的核心不是界面而是「一辆车对应多个工单、一个工单对应多个配件、一个配件对应多次出入库」这三层关系。如果表结构设计成工单直接存配件名称字符串那库存永远对不上历史查询也没法按配件维度统计。我一般会先把 ER 关系画清楚再写代码七张核心表分别是用户表、客户表、车辆表、工单表、工单明细表、配件表、库存流水表。客户和车辆必须分开。一个客户可能有多台车一台车也可能换车主把车主信息冗余进车辆表后期过户就会产生脏数据。工单表只存工单头信息工单号、车辆 ID、接待人、状态、创建时间具体换了什么、修了什么全部放工单明细表。配件表存当前库存和预警阈值库存流水表记录每一次入库、出库、退料工单明细和库存流水通过工单号关联。这样设计之后任何一笔库存变动都能追溯到具体工单月底盘点直接对流水求和即可。2.2 建表 SQL 与字段说明下面这段 SQL 是 MySQL 8.0 可直接执行的建表脚本字符集用 utf8mb4避免客户姓名里的生僻字变成问号。字段命名统一小写下划线主键用自增 bigint金额字段用 decimal 而不是 float这是血泪经验——float 做累加会出现 0.01 的误差对账时能让人崩溃。-- 客户表一个客户可对应多台车 CREATE TABLE customer ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 客户姓名, phone VARCHAR(20) NOT NULL COMMENT 手机号登录和回访用, level TINYINT DEFAULT 1 COMMENT 会员等级 1普通 2银卡 3金卡, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 车辆表车牌唯一关联客户 CREATE TABLE vehicle ( id BIGINT NOT NULL AUTO_INCREMENT, customer_id BIGINT NOT NULL, plate_no VARCHAR(10) NOT NULL COMMENT 车牌号, brand VARCHAR(30) COMMENT 品牌, model VARCHAR(50) COMMENT 车型, mileage INT DEFAULT 0 COMMENT 当前里程, PRIMARY KEY (id), UNIQUE KEY uk_plate (plate_no), KEY idx_customer (customer_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 工单主表状态 0待派工 1施工中 2待结算 3已结算 CREATE TABLE work_order ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 工单号业务唯一, vehicle_id BIGINT NOT NULL, receiver VARCHAR(30) COMMENT 接待人, status TINYINT DEFAULT 0, total_amount DECIMAL(10,2) DEFAULT 0.00, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_vehicle (vehicle_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 工单明细记录每个维修/保养项目及用料 CREATE TABLE work_order_item ( id BIGINT NOT NULL AUTO_INCREMENT, order_id BIGINT NOT NULL, item_type TINYINT COMMENT 1工时 2配件, item_name VARCHAR(100), part_id BIGINT DEFAULT NULL COMMENT 配件ID工时项为空, quantity DECIMAL(10,2) DEFAULT 1.00, price DECIMAL(10,2) DEFAULT 0.00, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 配件表stock 为当前库存warn_stock 为预警线 CREATE TABLE part ( id BIGINT NOT NULL AUTO_INCREMENT, part_no VARCHAR(40) NOT NULL COMMENT 配件编码, part_name VARCHAR(100) NOT NULL, stock DECIMAL(10,2) DEFAULT 0.00, warn_stock DECIMAL(10,2) DEFAULT 5.00, unit VARCHAR(10) DEFAULT 个, price DECIMAL(10,2) DEFAULT 0.00, PRIMARY KEY (id), UNIQUE KEY uk_part_no (part_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 库存流水每次出入库都插一条type 1入库 2出库 3退料 CREATE TABLE stock_log ( id BIGINT NOT NULL AUTO_INCREMENT, part_id BIGINT NOT NULL, change_qty DECIMAL(10,2) NOT NULL COMMENT 正数入库负数出库, type TINYINT NOT NULL, order_no VARCHAR(32) DEFAULT NULL COMMENT 关联工单号, operator VARCHAR(30), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_part (part_id), KEY idx_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户表系统登录账号role 1管理员 2接待 3技师 CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(30) NOT NULL, password VARCHAR(64) NOT NULL COMMENT 存MD5加盐值, real_name VARCHAR(30), role TINYINT DEFAULT 2, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时最容易忽略的是索引。vehicle表的plate_no必须唯一否则同一辆车会被重复录入工单历史就散了。stock_log的order_no加索引是为了退料和结算时能快速按工单反查流水。金额字段统一 decimal(10,2)表示最大 8 位整数加 2 位小数对修理厂单笔几千块的业务完全够用。字符集选 utf8mb4 而不是 utf8是因为 MySQL 的 utf8 实际只支持 3 字节遇到 emoji 或部分生僻字会报错这个坑在客户姓名里带「」这类字时必现。2.3 初始化数据与连接配置建完表要插一条管理员账号和几条测试配件方便跑通流程。密码不要明文存用 MD5 加固定盐值虽然强度一般但比明文强课程设计场景够用。-- 初始管理员密码为 123456 的 MD5 加盐结果盐值 fix_salt_ INSERT INTO sys_user (username, password, real_name, role) VALUES (admin, MD5(CONCAT(123456, fix_salt_)), 系统管理员, 1); -- 测试配件 INSERT INTO part (part_no, part_name, stock, warn_stock, unit, price) VALUES (P001, 全合成机油 5W-30, 20, 5, 桶, 320.00), (P002, 机油滤清器, 30, 10, 个, 45.00), (P003, 前刹车片, 8, 4, 套, 260.00);数据库连接建议单独抽一个工具类不要在每个 JSP 里写 DriverManager。常见做法是用一个DBUtil静态类加载驱动、提供getConnection()配合 try-with-resources 自动关闭。连接串里加上useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai否则中文乱码和时区差 8 小时这两个问题会轮流出现。连接池在小型项目里可以用 Druid 或 HikariCP但纯 JSP 项目直接 DriverManager 也能跑只是并发上来后每次新建连接开销大建议至少加一个简易连接池。3. 核心流程跑通接车、派工、领料、结算3.1 接车登记一次插入两张表的顺序问题接车是整个系统的入口前台拿到行驶证先查客户手机号是否存在不存在就插客户再查车牌是否存在不存在就插车辆最后生成工单。这里有个顺序陷阱必须先插客户拿到自增 ID再插车辆时把 customer_id 填进去最后插工单时填 vehicle_id。如果顺序反了外键关联不上工单就成了孤儿数据。// 接车登记核心逻辑放在 service 层 public String createWorkOrder(String phone, String name, String plateNo, String brand, String model, int mileage) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务三步要么全成要么全滚 // 1. 查客户没有则插入 long customerId queryOrInsertCustomer(conn, phone, name); // 2. 查车辆没有则插入关联 customerId long vehicleId queryOrInsertVehicle(conn, customerId, plateNo, brand, model, mileage); // 3. 生成工单号日期 4位随机业务唯一 String orderNo WO new SimpleDateFormat(yyyyMMdd).format(new Date()) String.format(%04d, new Random().nextInt(10000)); String sql INSERT INTO work_order(order_no, vehicle_id, receiver, status) VALUES(?,?,?,0); try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, orderNo); ps.setLong(2, vehicleId); ps.setString(3, 前台); ps.executeUpdate(); } conn.commit(); return orderNo; } catch (Exception e) { if (conn ! null) try { conn.rollback(); } catch (SQLException ex) { } throw new RuntimeException(接车失败, e); } finally { if (conn ! null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ex) { } } }这段代码的关键在事务。接车涉及三张表的写操作如果客户插进去了、车辆插失败了不回滚就会留下一个没有车的客户下次同手机号再进来会查到这条脏数据。setAutoCommit(false)之后必须显式 commit 或 rollback很多新手忘了在 finally 里恢复自动提交导致连接归还池子后还带着未提交事务下一个请求拿到这条连接就会出现玄学锁等待。工单号用日期加随机数简单但有小概率重复生产环境建议用数据库序列或 Redis 自增课程设计场景加个唯一索引兜底即可。3.2 派工与领料库存扣减必须和流水同时写派工是把工单状态从 0 改成 1并指定技师。领料是维修业务里最容易出问题的环节因为涉及库存扣减。正确做法是先在工单明细表插入一条配件记录再更新配件表库存最后往库存流水表插一条负数记录三步在同一个事务里。任何一步失败全部回滚否则会出现「明细有了、库存没扣」或者「库存扣了、流水没记」的对账灾难。// 领料出库扣库存 写明细 记流水三合一事务 public void pickPart(long orderId, long partId, double qty, String operator) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 校验库存是否充足用行锁防止并发超卖 String checkSql SELECT stock, part_name, price FROM part WHERE id? FOR UPDATE; double stock; double price; String partName; try (PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setLong(1, partId); try (ResultSet rs ps.executeQuery()) { if (!rs.next()) throw new RuntimeException(配件不存在); stock rs.getDouble(stock); price rs.getDouble(price); partName rs.getString(part_name); } } if (stock qty) throw new RuntimeException(库存不足当前仅 stock); // 2. 扣减库存 try (PreparedStatement ps conn.prepareStatement( UPDATE part SET stock stock - ? WHERE id ?)) { ps.setDouble(1, qty); ps.setLong(2, partId); ps.executeUpdate(); } // 3. 写工单明细 try (PreparedStatement ps conn.prepareStatement( INSERT INTO work_order_item(order_id, item_type, item_name, part_id, quantity, price) VALUES(?,2,?,?,?,?))) { ps.setLong(1, orderId); ps.setString(2, partName); ps.setLong(3, partId); ps.setDouble(4, qty); ps.setDouble(5, price); ps.executeUpdate(); } // 4. 记库存流水出库为负数 try (PreparedStatement ps conn.prepareStatement( INSERT INTO stock_log(part_id, change_qty, type, order_no, operator) VALUES(?,?,2,(SELECT order_no FROM work_order WHERE id?),?))) { ps.setLong(1, partId); ps.setDouble(2, -qty); ps.setLong(3, orderId); ps.setString(4, operator); ps.executeUpdate(); } conn.commit(); } catch (Exception e) { if (conn ! null) try { conn.rollback(); } catch (SQLException ex) { } throw new RuntimeException(领料失败 e.getMessage(), e); } finally { if (conn ! null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ex) { } } }SELECT ... FOR UPDATE这行是防超卖的关键。两个技师同时给不同工单领同一个配件如果没有行锁两人都读到库存 8都判断充足都扣 5最后库存变成 -2。加了 FOR UPDATE 之后第二个事务会等第一个提交后再读读到的是扣减后的值库存不足直接抛异常。这个锁只在事务内有效commit 或 rollback 后自动释放不用担心死锁——除非两个事务以相反顺序锁多行所以领料时建议按 part_id 升序处理多个配件。3.3 结算与状态流转金额汇总和状态机结算时把工单下所有明细的quantity * price求和写入工单主表的total_amount状态从 2 改成 3。状态流转要有约束0 只能到 11 只能到 22 只能到 3不能跳级也不能回退。常见做法是在 service 层用 if 判断当前状态不满足就抛异常而不是在 SQL 里直接 update因为 SQL 无法给出清晰的错误提示。// 结算汇总明细金额校验状态后更新 public void settle(long orderId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 校验当前状态必须是 2 待结算 int status; try (PreparedStatement ps conn.prepareStatement( SELECT status FROM work_order WHERE id? FOR UPDATE)) { ps.setLong(1, orderId); try (ResultSet rs ps.executeQuery()) { if (!rs.next()) throw new RuntimeException(工单不存在); status rs.getInt(status); } } if (status ! 2) throw new RuntimeException(工单状态不是待结算无法结算); // 汇总明细金额 double total 0; try (PreparedStatement ps conn.prepareStatement( SELECT SUM(quantity * price) AS total FROM work_order_item WHERE order_id?)) { ps.setLong(1, orderId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) total rs.getDouble(total); } } try (PreparedStatement ps conn.prepareStatement( UPDATE work_order SET status3, total_amount? WHERE id?)) { ps.setDouble(1, total); ps.setLong(2, orderId); ps.executeUpdate(); } conn.commit(); } catch (Exception e) { if (conn ! null) try { conn.rollback(); } catch (SQLException ex) { } throw new RuntimeException(结算失败 e.getMessage(), e); } finally { if (conn ! null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ex) { } } }状态机用 FOR UPDATE 锁住工单行防止两个前台同时点结算导致重复汇总。金额汇总用 SQL 的 SUM 而不是在 Java 里循环累加一是少一次数据传输二是避免 Java 浮点误差。如果明细里有折扣或优惠建议在工单主表加discount字段结算时total sum - discount不要把折扣摊到每条明细里否则退单时算不清。4. 避坑与排查JSP 项目最容易翻车的五个地方4.1 中文乱码从请求到响应要设三道编码现象是前台输入「张三」数据库里存成「å¼ ä¸」或者问号。原因通常是三处编码不一致JSP 页面 pageEncoding、请求体编码、数据库连接编码。解决方法是 JSP 顶部写% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %在 Servlet 或过滤器的 doPost 第一行写request.setCharacterEncoding(UTF-8)连接串加characterEncodingutf8mb4。GET 请求的参数编码取决于服务器配置Tomcat 8 以后默认 UTF-8老版本需要在 server.xml 的 Connector 上加URIEncodingUTF-8。三处都对齐后乱码基本消失。4.2 数据库连接泄漏连接池耗尽后整个系统假死现象是系统跑一段时间后所有页面都转圈日志里出现Too many connections。原因是 JSP 里直接DriverManager.getConnection()之后忘了 close或者 ResultSet、Statement 没关。解决方法是统一用 try-with-resources把 Connection、PreparedStatement、ResultSet 都放在 try 的括号里JVM 会自动关闭。如果用了连接池还要确保在 finally 里归还连接而不是物理关闭。排查时可以在 MySQL 执行SHOW PROCESSLIST看有多少连接处于 Sleep 状态如果数量接近 max_connections基本就是泄漏。4.3 工单号重复并发下的唯一性冲突现象是偶尔插入工单时报Duplicate entry for key uk_order_no。原因是工单号用日期加随机数生成高并发下随机数碰撞。解决方法是把随机数换成数据库自增序列或者用日期 自增ID后四位再或者用 UUID 截取。最稳妥的是在数据库建一个序列表每次UPDATE seq SET val val 1后取回配合事务保证唯一。如果不想改表结构至少把随机数范围从 10000 扩大到 1000000碰撞概率降到可接受。4.4 库存对不上流水和库存表不同步现象是配件表 stock 字段和 stock_log 流水求和结果不一致。原因通常是某次操作只更新了库存没写流水或者只写了流水没更新库存也可能是手工在数据库里直接改了 stock。解决方法是所有库存变动必须走同一个 service 方法禁止在 JSP 里直接写 UPDATE part。另外加一个对账定时任务每天凌晨跑一次SELECT part_id, SUM(change_qty) FROM stock_log GROUP BY part_id和 part 表的 stock 对比不一致就告警。初期数据已经乱了的话以流水为准重算一次 stock 即可。4.5 JSP 里写业务逻辑后期改不动现象是 JSP 页面里既有 HTML 又有 JDBC 代码改一个字段要翻好几个文件。原因是图省事把查询逻辑直接写在 JSP 的% %脚本里。解决方法是分层JSP 只负责展示Servlet 接收请求和转发Service 处理业务DAO 操作数据库。哪怕项目小也至少把 JDBC 代码抽到 DAO 类里。JSP 里用 JSTL 的c:forEach遍历数据不要用 Java 脚本片段。这样后期加一个字段只改实体类和 DAO 的 SQLJSP 基本不动。5. 进阶技巧用一条 SQL 查出库存预警和工单统计系统跑起来之后老板最关心的两个数字是「哪些配件快没了」和「这个月修了多少台车、产值多少」。这两个需求不需要写复杂代码各一条 SQL 就能出结果配合 JSP 的 JSTL 直接渲染成表格。库存预警查询找出 stock 小于等于 warn_stock 的配件按缺口大小排序。-- 库存预警缺口 预警线 - 当前库存缺口越大越紧急 SELECT p.part_no, p.part_name, p.stock, p.warn_stock, (p.warn_stock - p.stock) AS gap FROM part p WHERE p.stock p.warn_stock ORDER BY gap DESC;工单月度统计按月份分组统计工单数和总产值只算已结算的工单。-- 月度产值统计status3 表示已结算 SELECT DATE_FORMAT(create_time, %Y-%m) AS month, COUNT(*) AS order_count, SUM(total_amount) AS total_revenue FROM work_order WHERE status 3 GROUP BY DATE_FORMAT(create_time, %Y-%m) ORDER BY month DESC;这两条 SQL 可以直接放在 DAO 里返回 ListMapString,ObjectJSP 用c:forEach遍历。注意DATE_FORMAT的%Y-%m在 MySQL 里是区分大小写的写成%y会变成两位年份跨世纪时排序会乱。另外SUM(total_amount)如果某月没有已结算工单返回的是 NULL 而不是 0JSP 渲染时要用c:if判断或者 SQL 里套IFNULL(SUM(total_amount), 0)。再进一步可以把库存预警做成登录后的首页提醒用一条 SQL 查出预警数量大于 0 就在页面顶部显示红条。这个功能实现成本极低但能让库管每天主动来看系统而不是等月底盘点才发现缺货。我自己的习惯是每接一个维修管理类项目先把这两条统计 SQL 跑通再回头补界面因为数据对了界面怎么调都不会错。这套系统的边界也很清楚单店、几十个并发、数据量在十万级工单以内用 JSP JDBC 完全撑得住。如果要做连锁多门店、要对接微信小程序、要做实时库存同步那就该换 Spring Boot MyBatis-Plus 加缓存了。但在那之前把这条从接车到结算的链路在 JSP 里跑通一遍你会对事务、锁、编码这些基础问题有肌肉记忆后面换框架只是换写法思路不变。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?