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.0,ORM 选择 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接收所有请求参数,因为前端可能只传两三个字段,直接接收实体类容易造成字段被有意或无意修改。这在真实项目里是个安全隐患。
统一返回结果的类我也做了一个Result<T>,结构是:
public class Result<T> { 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: GMT+8这样接口返回的日期就是“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 加serverTimezone=Asia/Shanghai&useSSL=false |
| 前端接口 404 | 后端路径和前端请求路径不一致 | 统一在application.yml配context-path,前端通过环境变量管理 baseURL |
| MyBatis-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_CHECKS=0;,结尾写SET FOREIGN_KEY_CHECKS=1;,这样初始化顺序就随意了。
第二,代码里的业务校验要写在 service 层,而不是只写在 controller 层。比如创建订单时判断车辆是否存在,如果写在 controller,换一种调用方式就能绕过校验。老师不一定真去绕过,但这种严谨性在答辩时一句话就能说清楚。
第三,文档里要画好业务流程图和数据流图。源码、数据库、文档三者看起来多,但真正拉开差距的是“订单审核流程”和“库存变化流程”。把这两张图画好,文档质量立刻上一个档次。
第四,别忘了在application.yml里配置 MyBatis 的日志打印:
logging: level: com.example.carsale.mapper: debug这样控制台能看到每次执行的 SQL,排查问题方便,答辩演示时也能直接看出你操作后到底执行了什么语句,非常加分。
5.4 最后再分享一点个人体会
我做完这个项目最大的感受是:课程设计和真实项目最大的区别,不是技术难度,而是你有没有把业务逻辑当成一条线来思考。很多人拿到题目就拆成车辆表、订单表、客户表,分别写增删改查,结果做完发现各个模块根本串联不起来。而我这次先画出“一辆车从入库到售出”的完整流程,再倒推需要哪些状态、哪些接口、哪些报表,后面写代码就顺畅很多。
如果你时间有限,建议优先把“车辆入库 → 创建订单 → 审核 → 收款 → 完成”这条主链路跑通,再补客户管理和报表。这条链路跑通了,系统就能真正演示出一笔完整的销售业务,远比十个零散页面更有说服力。