1. 项目概览与设计思路1.1 这个系统到底解决了什么问题先说结论这是一个典型的4S店车辆销售管理系统技术栈是 Spring Boot MyBatis MySQL前端用 Thymeleaf 或者 Vue 都能接我做的时候用的是前后端分离的思路后端只提供 RESTful 接口前端页面单独跑。整个项目适合课程设计、毕业设计也适合想完整走一遍企业级 CRUD 业务状态流转的初学者拿来练手。为什么说它“典型”因为4S店销售场景非常接地气。它不只是一个车辆信息增删改查而是围绕“车从入库到卖出”这条主线把库存、客户、订单、财务、售后回访全部串起来。很多同学做管理系统容易做成纯数据的堆砌但4S店销售管理系统最关键的一点是必须体现业务状态变化。举个例子一辆车从厂家运到店里状态是“在库”客户看中后下订单状态变成“已锁定”销售顾问开销售单客户付定金状态变成“订单中”财务确认收尾款、开票状态变成“已售出”。如果系统里只是改一个字段那没什么难度难的是怎么让这些状态之间转换得合理、可追踪、有权限约束。这就是这门课程设计最值得花时间的地方。1.2 技术选型为什么是 Spring Boot 而不是其他现在做 Java 课程设计Spring Boot 几乎是最稳妥的选择。原因很简单它把 Spring 的配置复杂度降下来了一个spring-boot-starter-web就能启动 Web 项目内置 Tomcat不需要额外部署。相比传统的 SSM 项目Spring Boot 省去了大量 XML 配置项目结构也更清晰答辩老师看着也舒服。我建议用 Spring Boot 2.7 版本JDK 用 8 或 11不要追求最新版本毕竟课程设计的稳定性比新特性更重要。数据库用 MySQL 5.7 或 8.0ORM 选择 MyBatis-Plus因为它的条件构造器能省下很多 XML 里重复的 SQL而且分页查询直接用Page对象写起来很顺手。这里有件事要提前想清楚如果你前后端分离前端用 Vue Element UI那么后端要处理跨域写一个CorsConfig配置类如果你用 Thymeleaf 服务端渲染就不需要考虑跨域但页面和后端逻辑耦合度更高。我的经验是毕业设计优先选前后端分离因为能展示的点更多答辨别的时候可以说“我设计了一套统一返回结果和异常处理机制”这会成为加分项。1.3 功能模块拆解别一上来就写代码很多同学拿到题目就建表、写代码结果写到一半发现业务流程没想清楚反复改表结构。我建议第一步先画功能结构图把系统拆成六大模块。模块核心功能对应表车辆管理车辆入库、库存查询、车辆信息编辑car_info销售管理创建订单、订单审核、销售单打印sale_order客户管理客户建档、跟进记录、客户分类customer员工管理销售人员、经理、财务角色划分sys_user统计报表日/月销售额、车辆销售排行、库存预警sale_order car_info系统管理登录、权限、操作日志sys_user sys_log这六块不是平均用力。车辆管理和销售管理是核心中的核心统计报表是加分项权限和日志是体现工程能力的地方。哪怕你只把前四块做扎实答辩也有90分以上。但如果你能加上“库存低于阈值自动提醒”这种小功能会明显比同组的人更有优势。2. 数据库设计与核心表结构2.1 车辆信息表状态字段是第一优先级车辆信息表是整个系统的地基字段设计直接影响后续所有模块。最基本的字段有车辆编号、品牌、车系、型号、颜色、配置版本、指导价、成交价、入库时间、仓库位置、车辆识别码VIN、发动机号、状态。重点说一下状态字段。我建议用status字段取值为1在库、2锁定、3已售、4调拨。不要用字符串直接存“在库”“已售”那样不利于统计和查询。正确做法是存数字在 Java 代码里用枚举或者常量类做映射。另外VIN 码一定要加唯一约束。车辆识别码是每台车的“身份证”如果系统里出现重复后续订单关联车辆就会乱。我在开发时就踩过一次坑测试数据里有两台车 VIN 码一样结果销售订单关联的时候怎么都对不上。所以建表时记得给vin_code加UNIQUE约束。下面是简化版建表语句供参考CREATE TABLE car_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, vin_code VARCHAR(64) NOT NULL UNIQUE, brand VARCHAR(32) NOT NULL, series VARCHAR(32) NOT NULL, model VARCHAR(32), color VARCHAR(16), config_version VARCHAR(32), guide_price DECIMAL(10,2), deal_price DECIMAL(10,2), status TINYINT DEFAULT 1 COMMENT 1在库 2锁定 3已售 4调拨, warehouse_location VARCHAR(32), create_time DATETIME, update_time DATETIME );deal_price为什么单独存因为销售时实际成交价有可能和指导价不一样也是为了报表统计实际营业额。如果不单独存后面销售订单还要回查车辆表逻辑就绕了。2.2 订单表主表和明细表分开销售订单不能只建一张表。标准做法是拆成主表sale_order和明细表sale_order_item。主表存订单编号、客户ID、销售人员ID、订单状态、总金额、优惠金额、实付金额、定金、支付方式、创建时间。明细表存该订单包含的车辆ID、车辆品牌、车型、单价、数量。拆开的好处是一个订单可以包含多辆车比如客户一次性买两台同款车。虽然实际销售中这种情况不多但设计上必须支持一对多。而且主表和明细表分离后查询“某订单有哪些车”和“某车属于哪个订单”都非常清晰。订单状态我设计为0待审核、1已审核、2已付定金、3已交尾款、4已完成、5已取消。这里注意审核逻辑普通销售员创建的订单需要销售经理审核审核通过后才能进入收款环节。这个审批流是答辩时的展示重点面试官最容易问“不同角色的操作权限怎么控制”。定金字段建议单独存因为4S店常见情况是客户先交定金锁定车辆几天后再交尾款提车。如果只设计一个“总金额”整个售车流程的真实感就弱了。2.3 用户表与角色权限最少要有三类角色用户表sys_user不需要太复杂字段包括用户ID、用户名、密码、真实姓名、角色ID、手机号、状态。密码不要用明文至少用 MD5 加盐或者更稳妥一点用 BCrypt。答辩老师看到你用 BCrypt哪怕只是简单调用也是一个可讲的亮点。角色表sys_role我建议设计三类管理员、销售经理、销售顾问。管理员管系统配置和员工账号销售经理可以审核订单、查看全部销售数据销售顾问只能创建订单、管理自己的客户。权限控制怎么做不要在 Controller 里写一堆if (role 1)那样太乱。我用的方案是 Spring AOP 自定义注解。定义一个RequireRole(value {manager})注解加在需要权限控制的接口方法上再用拦截器统一校验。这样代码很干净答辩时也能讲出“统一鉴权”的思路。如果嫌 AOP 复杂退而求其次用拦截器判断路径也能实现。比如/api/order/review/**路径只允许 manager 访问Interceptor 里取当前用户角色判断即可。重点是让角色权限成为一个独立关注点而不是散落在业务代码里。3. 后端核心实现与接口细节3.1 项目骨架和分层Controller 里不要写业务逻辑Spring Boot 项目的包结构直接影响后续维护和答辩观感。我推荐按功能分包而不是按技术层分包com.example.carsale ├── controller ├── service ├── mapper ├── entity ├── dto ├── config ├── common └── utilscontroller只负责接收参数、调用 service、返回结果service层处理业务mapper层或者用 MyBatis-Plus 的BaseMapper负责数据库交互。dto包放前端传参时用的对象不要直接用entity接收所有请求参数因为前端可能只传两三个字段直接接收实体类容易造成字段被有意或无意修改。这在真实项目里是个安全隐患。统一返回结果的类我也做了一个ResultT结构是public class ResultT { private Integer code; // 200成功500失败 private String message; private T data; }然后所有 Controller 都返回Result?。一个小习惯就能让前后端对接时不用反复讨论返回格式也给答辩加分。全局异常处理用RestControllerAdvice捕获BusinessException和Exception转成对应的Result。这样的话不管业务对不对前端拿到的 JSON 结构永远是统一的。3.2 销售订单流程的接口实现销售模块我定义了下面几个接口它们组成了完整的业务闭环POST /api/order/create创建订单POST /api/order/review经理审核POST /api/order/pay/deposit缴纳定金POST /api/order/pay/final缴纳尾款POST /api/order/complete确认交车完成POST /api/order/cancel取消订单这里最容易写错的是状态流转校验。比如客户还没交定金就不能直接交尾款订单已经在“已完成”状态就不能再取消。我的建议是在 service 层写一个checkStatus的私有方法根据当前状态和目标操作判断是否允许流转。创建订单时还有一个关键点事务控制。创建订单不仅要插入主表和明细表还要把对应车辆的状态从“在库”改成“锁定”。如果插入订单成功但车辆状态更新失败那就出大问题了。所以创建订单的 service 方法一定要加Transactional。代码大致是这样的Transactional public Long createOrder(OrderCreateDTO dto) { SaleOrder order new SaleOrder(); // 设置订单基本信息 saleOrderMapper.insert(order); for (Long carId : dto.getCarIds()) { // 校验车辆状态是否在库 // 插入订单明细 // 更新车辆状态为锁定 } return order.getId(); }还有一件事值得提车辆锁定后其他销售顾问在查询库存时不应该再看到这辆车所以要保证查询库存时自动过滤status 1。如果忽略这一点就会出现一辆车被两个客户同时订走的尴尬情况。3.3 库存统计与销售报表怎么算统计报表模块是很多人觉得难的地方其实只要用 SQL 聚合函数加时间筛选就能搞定。比如日销售额报表SELECT DATE(create_time) AS sale_date, COUNT(*) AS order_count, SUM(final_amount) AS total_amount FROM sale_order WHERE status 4 GROUP BY DATE(create_time) ORDER BY sale_date DESC;注意这里过滤了status 4也就是交易完成状态否则未审核、已取消的订单也会被统计进去。这个细节如果不注意数据会明显偏离实际。库存预警查询也不难就是查询某品牌下状态为“在库”的车辆数量是否小于设定的预警阈值。可以在car_info表加一个min_stock字段不一定是每个型号都要但至少每个品牌维护一个阈值。然后在车辆管理页面做一个提醒条比如“某品牌库存仅剩2台”。这个小功能实现的成本很低但很能体现你对业务的理解。4. 前端页面与联调经验4.1 页面结构最核心的四个页面如果你选择前后端分离前端页面我建议只做四张登录页、首页仪表盘、车辆管理页、销售订单页、客户管理页。还有一张可选的统计报表页。不要贪多页面数量少但交互完整比做了十个页面每个都是半成品要好得多。车辆管理页的核心是表格 条件查询 新增/编辑弹窗。表格要展示品牌、车型、颜色、指导价、状态、操作列。操作列放“编辑”“下架”“创建订单”按钮。状态字段在页面上用标签显示比如在库是绿色锁定是橙色已售是灰色前端的颜色映射能直观表达业务状态。销售订单页要有两个视图一个是普通销售员视角只能看到自己创建的订单另一个是经理视角能看到全部订单并操作“审核”。如果前后端权限做得好前端只需要根据角色显示或隐藏按钮真正的拦截逻辑还是在后端。前端隐藏只是体验优化不能作为安全手段。4.2 前后端对接中的几个实际坑前后端分离最常见的坑是跨域。我用 Spring Boot 后端的解决方案是写一个配置类Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }另一个坑是日期格式。后端返回LocalDateTime给前端默认是带 T 的 ISO 格式比如2025-06-01T10:30:00用户看起来很不舒服。我做了统一配置在application.yml里设置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这样接口返回的日期就是“2025-06-01 10:30:00”这样的格式。这个小细节页面整体会显得精致很多。还有分页参数传递。我用 MyBatis-Plus 的Page对象前端参数是current和size返回的结果里有total、records。前端拿到total后渲染分页器别把总数写死否则翻页时发现总条数不变会非常尴尬。4.3 两个能提升印象分的体验细节第一个是登录后记住角色并动态显示菜单。比如销售顾问登录后只显示“车辆库存、我的订单、我的客户”经理登录后多显示“订单审核、员工管理、统计报表”。这个功能前端实现很简单但会让答辩老师觉得你考虑了实际使用场景。第二个是操作确认弹窗。删除车辆、取消订单这类危险操作前端必须二次确认。我见过很多同学做的系统点一下删除按钮数据就没了没有任何提示。实际项目里这种设计是不可接受的。加一个ElMessageBox.confirm或者简单的confirm()成本只有几行代码但能体现出工程意识。5. 课程设计答辩准备与常见坑5.1 演示前一定要准备的真实数据很多人在自己电脑上开发好好的一到答辩演示就各种报错最常见的原因就是测试数据太假、太少。我建议你提前准备至少 10 台车的完整信息覆盖 3 个品牌、4 个车系、5 种颜色价格从十万到四十万不等。为什么强调“真实”因为演示时老师会现场点查询、筛选、详情。如果你的库存数据只有两三条查询功能根本看不出效果如果你所有车的状态都是“在库”那也没法现场演示创建订单、审核、交款这些流程。我的经验是把演示脚本提前排练三遍。脚本大概是这样的登录管理员 → 新增一台车 → 切换销售顾问账号 → 创建订单 → 切换经理账号 → 审核订单 → 财务收款 → 确认完成 → 回到报表页看销售数据变化。这一套流程走下来系统的主干功能全部覆盖时间控制在五分钟以内老师会觉得你思路非常清楚。5.2 最高频出现的几个问题及解法问题原因解决方法启动时端口被占用本地 Tomcat 端口冲突spring.application.port改为 8081或用lsof -i:8080查占用进程MySQL 时区报错JDBC 连接串缺少时区参数URL 加serverTimezoneAsia/ShanghaiuseSSLfalse前端接口 404后端路径和前端请求路径不一致统一在application.yml配context-path前端通过环境变量管理 baseURLMyBatis-Plus 查不到数据表名和实体类名映射不对在实体类上加TableName(car_info)显式指定密码登录失败MD5 加密后长度不匹配确认数据库密码字段长度至少 32并统一加密方式分页数据正常但 total 总是 0返回类型写了Page但前端取错字段前端取res.data.total不是res.data.records.total还有一个特别常见的隐藏坑MySQL 8.0 的驱动类名变成了com.mysql.cj.jdbc.Driver如果你用的是 MyBatis-Plus 3.5 以上版本一般没问题但手写配置时一定别用旧的com.mysql.jdbc.Driver。5.3 几个独家避坑技巧第一数据库脚本一定要给完整。不仅是建表语句还要有测试数据。有的老师会现场让你从零初始化数据库如果你的 SQL 脚本缺了外键关联顺序删表报错、插入报错那场面就很尴尬。我习惯在脚本开头写SET FOREIGN_KEY_CHECKS0;结尾写SET FOREIGN_KEY_CHECKS1;这样初始化顺序就随意了。第二代码里的业务校验要写在 service 层而不是只写在 controller 层。比如创建订单时判断车辆是否存在如果写在 controller换一种调用方式就能绕过校验。老师不一定真去绕过但这种严谨性在答辩时一句话就能说清楚。第三文档里要画好业务流程图和数据流图。源码、数据库、文档三者看起来多但真正拉开差距的是“订单审核流程”和“库存变化流程”。把这两张图画好文档质量立刻上一个档次。第四别忘了在application.yml里配置 MyBatis 的日志打印logging: level: com.example.carsale.mapper: debug这样控制台能看到每次执行的 SQL排查问题方便答辩演示时也能直接看出你操作后到底执行了什么语句非常加分。5.4 最后再分享一点个人体会我做完这个项目最大的感受是课程设计和真实项目最大的区别不是技术难度而是你有没有把业务逻辑当成一条线来思考。很多人拿到题目就拆成车辆表、订单表、客户表分别写增删改查结果做完发现各个模块根本串联不起来。而我这次先画出“一辆车从入库到售出”的完整流程再倒推需要哪些状态、哪些接口、哪些报表后面写代码就顺畅很多。如果你时间有限建议优先把“车辆入库 → 创建订单 → 审核 → 收款 → 完成”这条主链路跑通再补客户管理和报表。这条链路跑通了系统就能真正演示出一笔完整的销售业务远比十个零散页面更有说服力。