每年毕业设计季,总有人问我同一个问题:题目怎么选?既想体现技术水平,又怕做不完,最好还能在答辩时讲出点自己的东西。我的回答通常很直接——如果你学的是Java方向,基于Spring Boot的二手汽车交易系统是一个非常稳的选择。别急着把它当成又一个"管理系统",这个题目的价值在于:它要处理的不是简单的增删改查,而是一条包含车源发布、平台审核、买家检索、买卖双方在线议价、系统撮合建议、预约看车、定金尾款履约的真实交易链路。换句话说,它逼着你把一套Web后端该有的东西都走一遍。
这篇文章我想从项目设计的角度,把这个题目的骨架、关键模块、核心代码思路和实测踩坑记录完整拆给你。适合已经把Java基础、Spring Boot基本用法学完,但还没想清楚毕设整体怎么搭的同学参考;如果你准备做类似方向的选题,也可以直接把它当需求说明书和技术方案用。文章里不会贴完整的几百行代码,而是把我认为是"题目灵魂"的部分拿出来细讲:数据模型、状态流转、协商撮合逻辑、并发控制,以及答辩时怎么把话说漂亮。
1. 为什么"二手汽车交易"在毕业设计选题里属于性价比之王
1.1 一个题目覆盖了Web后端的全部核心能力
我见过太多同学选题时纠结:做电商系统,样式和支付是坑;做图书管理,又被老师嫌弃"没有业务深度"。二手汽车交易系统刚好站在两者中间。它本质上是一个多角色、多状态、强交互的撮合交易系统,技术栈覆盖面非常完整。
简单列一下你在这个项目里一定会碰到的点:
- 用户体系:注册登录、JWT认证、角色权限区分(普通用户、管理员)
- 业务实体管理:车源信息、图片、收藏、浏览记录、留言咨询
- 多条件检索:品牌、价格区间、城市、里程、排放标准组合查询
- 文件上传与访问:车辆实拍图、验车报告、证件材料
- 复杂业务状态机:车源状态、订单状态、协商状态之间的转换
- 事务与并发控制:同一辆车不能被两个人重复下单,议价过程中车源状态要锁定
- 前后端交互接口设计:返回结构统一、异常处理、分页参数设计
这套东西做完,你去看其他任何业务系统(租赁、家政、培训报名)的架构都会觉得很轻松。很多公司面试应届生,问来问去其实也就是这些能力,所以它不只是为了过答辩,对找工作也有直接的参考价值。
1.2 "协商交易"和"标准商城"的天然后发优势
如果只是做一个"二手车信息展示+管理员维护"的静态网站,那确实没意思。但这个题目的标题里有两个关键词被我特别看重:在线协商交易和撮合服务。这正是二手车区别于普通标品电商的地方。
卖一辆手机,标价5999,买家直接付款就完事了。但卖一辆车完全不一样:车况非标、一车一况、价格弹性极大,买卖双方大概率要来回沟通几轮才能达成一致。所以这个系统里必须有"卖家期望售价""买家出价""卖家还价""系统估值参考区间""撮合成交提示"这一套机制。要做这些,你就得去设计出价表、议价会话、价格状态判断,而不仅仅是在订单表里塞一个price字段。
这种业务复杂度是选题的天然加分项。答辩的时候你可以直接说:"我的系统不是一个简单展示平台,而是一个通过价格撮合算法连接买卖双方的双边交易平台。"这句话一说出来,和旁边那些"某某管理系统"就不在一个层次上了。另外,这个题目还有很强的延展空间:二手新能源车电池健康度展示、车商入驻、委托代卖、金融分期入口、车辆维保记录查询,后续想往深做,每个方向都能开出一个独立的章节。
2. 交易系统边界怎么划:角色清单、功能地图与"撮合链路"推导
2.1 先定角色,再定权限
很多同学拿到题目喜欢直接建表,我建议反过来:先把角色和每类角色能干什么划清楚。二手车交易平台里,最基础的角色是三类。
买家:浏览车源、收藏、咨询、发起议价、预约看车、下单支付定金、确认尾款、查看车辆交付状态。
卖家:发布车源、维护车辆资料、回复议价、接受或拒绝出价、确认预约、处理订单、提交过户资料。
管理员:审核车源、管理用户状态、下架违规车辆、处理举报、查看平台交易统计数据。
如果题目要求扩展,可以再加一个"车商"角色,本质上是卖家+批量管理;还有"平台审核员"角色,和卖家内容审核隔离。但毕业设计阶段我不建议把角色拆得太碎,否则光配权限权限就要耗掉大量时间。三个角色足够撑起整个演示流程。
认证方案上,我用的是Spring Security + JWT,接口层通过注解做权限控制。比如车源发布接口标注@PreAuthorize("hasRole('SELLER')"),管理员审核接口标注@PreAuthorize("hasRole('ADMIN')")。这比单纯的拦截器判断登录状态规范得多,答辩时也能解释清楚"基于RBAC模型的权限控制"。
2.2 功能清单要能讲出一个完整故事
功能模块不需要贪多,但一定要能串成一条完整的交易故事线。我按主流程把功能拆成下面这些模块:
| 模块 | 核心功能 | 说明 |
|---|---|---|
| 用户认证 | 注册、登录、Token刷新、密码重置 | 买家卖家共用一套账号体系 |
| 车源管理 | 发布、编辑、上下架、审核状态查询 | 卖家侧核心,车辆图片批量上传 |
| 车源检索 | 多条件筛选、排序、关键词搜索、分页 | 买家进入系统的第一屏 |
| 车源详情 | 车辆参数、多图展示、车况等级、浏览计数 | 详情页要展示所有历史协商入口 |
| 协商议价 | 发起报价、回复还价、出价记录、撮合提示 | 全系统最有特色的模块,后面重点讲 |
| 预约看车 | 时间段选择、卖家确认、状态跟踪 | 体现撮合平台的线下履约衔接 |
| 订单履约 | 生成订单、定金/尾款状态、过户资料上传 | 与车源状态联动 |
| 用户中心 | 我的发布、我的出价、我的预约、我的订单 | 买卖双方不同视角 |
| 后台管理 | 车源审核、用户管理、车辆下架、数据统计 | 管理员独立界面 |
每个模块做完以后,最起码要保证从"卖家发布一辆车"到"买家最终提车"这条主线上的每一个功能都是连通的。很多毕设烂尾,不是因为功能少,而是因为功能之间是割裂的:车源下架了但订单还能继续支付、车已经被卖掉了但还在搜索结果里出现。这种问题在演示时一眼就会被看穿。
2.3 从标题关键词反推业务设计
这个题目的副标题里有"存量汽车资产数字化管理与撮合服务系统构建"两个关键词,里面的信息量很值得玩味。
"数字化管理"意味着车辆不能只当作一条文本记录,它需要有结构化的参数体系:品牌、车系、年款、上牌时间、行驶里程、变速箱类型、排放标准、车况评分、维保记录等。这些字段既支撑前端精细化筛选,也支撑系统计算估值。"撮合服务"意味着平台不仅提供一个信息展示窗口,还要主动参与交易促成——最直观的做法就是给出"市场估值区间"和"出价命中提示"。
我当时的做法是:卖家发布车辆时可以设置两个价格,一个是对外展示的期望售价,另一个是仅自己可见的心理底价。系统在买家出价时,如果出价大于心理底价,马上给双方发送"当前报价已进入可成交区间"的提示;如果出价明显低于底价,系统则结合车辆残值模型给出一个建议区间,引导理性出价。这套逻辑不复杂,但它让平台从"被动陈列"变成了"主动撮合",整个项目的立意立刻不一样了。
3. 数据库建模:车辆主表、议价记录、订单表的字段设计与状态流转
3.1 车辆主表是整条交易链的源头
数据库设计是这个项目最值得花时间的地方,因为后面所有业务逻辑都建立在表结构上。车辆主表的字段,我强烈建议按"结构化参数"来设计,而不是把描述塞进一个大文本字段。下面是我认为比较合理的一张表结构,你可以直接参考:
CREATE TABLE `vehicle` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '车辆ID', `seller_id` bigint(20) NOT NULL COMMENT '卖家用户ID', `brand` varchar(30) NOT NULL COMMENT '品牌', `series` varchar(50) NOT NULL COMMENT '车系', `model_year` int(11) DEFAULT NULL COMMENT '年款', `plate_time` date DEFAULT NULL COMMENT '首次上牌日期', `mileage` decimal(10,1) DEFAULT NULL COMMENT '行驶里程,万公里', `gearbox` tinyint(4) DEFAULT NULL COMMENT '1自动 2手动', `displacement` varchar(20) DEFAULT NULL COMMENT '排量,如1.5T/2.0L', `emission_standard` varchar(20) DEFAULT NULL COMMENT '排放标准,国V/国VI', `color` varchar(20) DEFAULT NULL COMMENT '外观颜色', `city` varchar(50) DEFAULT NULL COMMENT '车辆所在地', `guide_price` decimal(12,0) DEFAULT NULL COMMENT '新车指导价', `expect_price` decimal(12,0) NOT NULL COMMENT '期望售价', `floor_price` decimal(12,0) NOT NULL COMMENT '心理底价,仅卖家可见', `condition_grade` tinyint(4) DEFAULT NULL COMMENT '车况等级 1-5', `description` text COMMENT '车况描述', `cover_image` varchar(255) DEFAULT NULL COMMENT '封面图', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0待审核 1在售 2议价中 3已预订 4已售 5下架 6审核驳回', `view_count` int(11) DEFAULT '0' COMMENT '浏览数', `version` int(11) DEFAULT '0' COMMENT '乐观锁版本号', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_brand` (`brand`), KEY `idx_city_status` (`city`, `status`), KEY `idx_price` (`expect_price`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆信息表';两个容易忽略的设计点我单独说一下:一是floor_price字段,它绝对不能被普通的查询接口返回。我当时的做法是写一个VO对象,把所有和"底价""审核备注"相关的敏感字段单独放出来,查询接口永远只返回VO,不返回实体类,从源头避免误泄露。二是version字段,这个字段是后面做并发控制的关键,先留着,第4章会详细讲它的用法。
3.2 议价协商不能只塞一条消息记录
在线协商是这个题目最有区分度的部分,表设计也最有讲究。很多同学用一张"留言表"搞协商,结果买家出价和卖家回复混在一起,想统计"谁还过价""成交价怎么来的"根本查不清楚。我的方案是拆成两张表:一张负责会话主信息,一张负责每一轮出价和还价。
CREATE TABLE `negotiation` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `vehicle_id` bigint(20) NOT NULL COMMENT '车辆ID', `buyer_id` bigint(20) NOT NULL COMMENT '买家ID', `seller_id` bigint(20) NOT NULL COMMENT '卖家ID', `status` tinyint(4) DEFAULT '0' COMMENT '协商状态 0进行中 1买家已还价 2卖家已还价 3成交 4关闭', `last_quote` decimal(12,0) DEFAULT NULL COMMENT '最近一次报价', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_vehicle_buyer` (`vehicle_id`, `buyer_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='议价会话表'; CREATE TABLE `negotiation_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `negotiation_id` bigint(20) NOT NULL COMMENT '会话ID', `from_user_id` bigint(20) NOT NULL COMMENT '发起方用户ID', `quote_price` decimal(12,0) NOT NULL COMMENT '本次报价金额', `message` varchar(500) DEFAULT NULL COMMENT '留言内容', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出价还价记录表';这里设置了一个唯一约束uk_vehicle_buyer,表示同一辆车、同一个买家只能建立一个协商会话,后续的讨价还价都在一个会话里追加记录。这符合真实交易习惯:购车咨询是有连续性的,不应该每次刷新产生一条新会话。每次撮合判断时,只需要取出会话内最新一条quote_price和卖家的floor_price做比较即可。
3.3 订单与车源状态必须联动
订单表相对常规,但一定要想清楚它和车源状态的关系。我的设计是订单表里冗余保存车辆的关键信息快照——品牌、车系、年款、成交价、卖家ID、买家ID——这样即使车辆后来被下架或修改,订单的历史信息也不会受影响。订单状态我设计为:待付定金、已付定金、待过户、已完成、已取消。
真正的难点在联动逻辑。比如买家发起下单时,车辆状态必须是"在售"或者"议价中";下单成功后,车源状态立刻变为"已预订",其他买家再点击下单就要被拦截;如果订单取消,车源状态又要回滚为"在售"。这套状态机建议画清楚再动手,不用画得很复杂,但每个转换条件和触发动作必须明确。状态间的流转关系我在第4章核心模块里会用一个表格列出来,大家照着这个思路实现就不会乱。
4. 后端业务闭环实现:发布审核、协商撮合、并发下单到交付状态
4.1 车源发布与审核的状态机
二手车平台必须要有审核环节,否则管理员角色毫无存在感。卖家提交车辆信息后,状态从"待审核"开始,管理员在后台可以审核通过或驳回,驳回时必须填写原因。这里我用一个枚举类来管理车辆状态,避免代码里到处都是魔法数字:
public enum VehicleStatus { PENDING(0, "待审核"), ON_SALE(1, "在售"), NEGOTIATING(2, "议价中"), RESERVED(3, "已预订"), SOLD(4, "已售"), OFFLINE(5, "下架"), REJECTED(6, "审核驳回"); private final int code; private final String desc; // 构造方法、getter省略 }状态流转的核心原则是"任何状态变更都必须走同一个入口方法",我在Service层封装了changeVehicleStatus(Long vehicleId, int fromStatus, int toStatus),方法内部用乐观锁更新的方式来保证状态不会乱跳。比如审核通过时:UPDATE vehicle SET status=1, version=version+1 WHERE id=? AND status=0 AND version=?。这样就算管理员连续点击两次"通过"按钮,第二次更新也因为状态不是待审核而失败。
4.2 协商撮合逻辑:一个不复杂但很有说头的估价模型
下面这段代码是我整个项目里最满意的一部分,也是答辩时主要讲的点。当买家发起一个报价,系统不仅要记录,还要做两件事:判断报价是否命中卖家的心理底价区间,如果没命中则给出一个参考估值区间引导协商。
public NegotiationResult submitQuote(Long vehicleId, Long buyerId, BigDecimal quotePrice) { Vehicle vehicle = vehicleMapper.selectById(vehicleId); if (vehicle.getStatus() != VehicleStatus.ON_SALE.getCode() && vehicle.getStatus() != VehicleStatus.NEGOTIATING.getCode()) { throw new BizException("该车辆当前不可议价"); } // 生成或获取唯一会话 Negotiation negotiation = findOrCreateNegotiation(vehicleId, buyerId); // 判断是否命中底价 if (quotePrice.compareTo(vehicle.getFloorPrice()) >= 0) { // 命中底价:提示双方可加速成交 return NegotiationResult.hitFloor(negotiation, buildEstimateRange(vehicle)); } else { // 未命中:给出估值区间和当前差值 PriceRange range = buildEstimateRange(vehicle); return NegotiationResult.missFloor(negotiation, range); } } private PriceRange buildEstimateRange(Vehicle vehicle) { // 简化版残值模型:首年折旧15%,后续每年递减8%,里程每超1万公里修正0.5% int years = Years.yearsBetween(vehicle.getPlateTime(), LocalDate.now()).getYears(); BigDecimal base = vehicle.getGuidePrice() .multiply(BigDecimal.valueOf(Math.pow(0.92, years))) .multiply(BigDecimal.valueOf(0.85)); BigDecimal mileageAdjust = BigDecimal.valueOf(1.0 - vehicle.getMileage().doubleValue() * 0.005); BigDecimal estimate = base.multiply(mileageAdjust).max(vehicle.getFloorPrice()); return new PriceRange(estimate.multiply(BigDecimal.valueOf(0.92)), estimate.multiply(BigDecimal.valueOf(1.05))); }这套残值模型肯定比不上真正二手车评估师的算法,但作为毕业设计里面"平台提供估值参考"的功能落点已经足够合理。答辩时老师问"你的估值依据是什么",你就如实讲:基于年限折旧、里程修正和车况等级的简化评估模型,并说明真实商业平台还会接入第三方车况数据,保留扩展接口即可。这样既诚实,又展示了扩展思维。
4.3 并发下单:乐观锁是毕业设计最安全的解法
"同一辆车被两个买家同时下单"是实际业务里一定会发生的情况,也是老师最爱拷问的场景。解决思路不只一种,悲观锁、Redis分布式锁都可以,但毕业设计阶段我推荐乐观锁+状态校验,理由很简单:实现成本低,逻辑清晰,不需要额外引入中间件。
-- 下单事务中执行的第一条更新语句 UPDATE vehicle SET status = 3, version = version + 1 WHERE id = #{vehicleId} AND status = 1 AND version = #{oldVersion};如果这条SQL的影响行数为0,说明车辆已经被别人抢先一步预订,当前下单请求立即抛出业务异常,前端弹出"车辆已被预订"。这个方案有几个天然优点:一是把"车辆是否可售"的判断和"抢占车辆"的更新合并成一条原子SQL,不需要编程式锁;二是version字段每次修改都会变化,重复提交也只会成功一次;三是不用额外学习分布式锁组件,答辩也能把原理讲透。
需要注意的一点是,下单和支付定金是两个步骤,我在下单接口里只做"锁定车辆+生成订单",订单状态是"待付定金",然后给买家一个15分钟的支付窗口,如果窗口内未支付,可以写个定时任务把超时订单取消并把车源状态回滚为"在售"。这个设计在真实平台里很常见,也是答辩时很不错的加分点。
4.4 预约看车与防冲突
预约看车模块看似简单,其实有一个经典坑:同一辆车在同一时段可能被多个买家预约。我在表设计上给预约记录加了一个唯一约束:
CREATE TABLE `appointment` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `vehicle_id` bigint(20) NOT NULL, `buyer_id` bigint(20) NOT NULL, `appointment_time` datetime NOT NULL, `status` tinyint(4) DEFAULT '0' COMMENT '0待确认 1已确认 2已完成 3已取消', PRIMARY KEY (`id`), UNIQUE KEY `uk_vehicle_time` (`vehicle_id`, `appointment_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='看车预约表';这样一个时间段内一辆车只能有一条预约记录,数据库层直接挡住重复预约。卖家收到预约消息后可以在后台确认,确认后买家会收到站内通知,到了预约时间自动把状态改为已完成。这个模块做起来很快,但它在演示时很出效果——因为它展示了平台对线下履约环节的考虑,而不只是线上信息展示。
5. 技术选型、打包部署与实测踩坑记录
5.1 版本与组件怎么搭配最稳
毕业设计不追求技术最新,追求的是"别人跑得通你也能跑通"。我实际推荐一套非常稳定的组合:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 8 或 17 | 看学校实验环境,稳妥优先 |
| Spring Boot | 2.7.x | 兼容JDK8,资料多,坑少 |
| MyBatis-Plus | 3.5.x | 分页、条件构造器、代码生成器都有 |
| MySQL | 8.0 | 注意驱动名和时区配置 |
| Redis | 有则加,无则不加 | 用于车源热度缓存、验证码存储 |
| 前端 | Vue3 + Element Plus 或 纯Thymeleaf | 看个人时间,模板引擎更省事 |
Spring Boot 3.x 也很好,但它要求JDK17,如果学校机房还是JDK8环境,光环境适配就要折腾很久。毕业设计的核心是业务闭环,不是版本追新,这是我的真实体会。数据访问层我选MyBatis-Plus,因为它内置的分页插件和LambdaQueryWrapper能让条件检索代码非常干净:
LambdaQueryWrapper<Vehicle> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StringUtils.hasText(brand), Vehicle::getBrand, brand) .between(minPrice != null && maxPrice != null, Vehicle::getExpectPrice, minPrice, maxPrice) .eq(StringUtils.hasText(city), Vehicle::getCity, city) .eq(emission != null, Vehicle::getEmissionStandard, emission) .eq(Vehicle::getStatus, VehicleStatus.ON_SALE.getCode()) .orderByDesc(Vehicle::getCreateTime); Page<Vehicle> page = vehicleMapper.selectPage(new Page<>(current, size), wrapper);这段代码就是一个完整的车源列表多条件查询,没有拼接SQL,也没有繁琐的XML映射,清晰度非常高。答辩时老师问"怎么实现多条件组合查询的",指一下这段代码就能讲明白。
5.2 文件上传:本地存储比OSS更省心
车辆图片上传是绕不开的。很多同学第一时间想到接入云对象存储,我建议毕业设计阶段慎重——如果只是平时练习没有正式备案的域名,调用云服务还要处理鉴权、回调、防盗链,纯属给自己加戏。本地磁盘存储完全够用:上传接口把文件写入服务器的一个固定目录,文件名用UUID重命名避免中文乱码,然后把文件相对路径存到数据库。前端访问时用一个/upload/**映射到本地磁盘路径。
配置示例:
file: upload-dir: /data/car-images # 绝对路径,放在jar包目录之外 access-pattern: /api/files/**然后写一个配置类把本地目录映射为静态资源:这样可以保证服务器重启后图片还在,也不会因为重新打包jar而丢失图片。如果论文里想写"支持后续接入OSS",只需要抽象一个FileStorageService接口,本地实现和OOS实现类替换即可。
上赛季我就见过某同学把图片存到了项目resources目录下,打包上线后一访问详情页图片就404,后台一看就剩一堆请求日志和一行报错。原因就是resources下的文件会被打进jar包里,运行时根本没法向jar内写入新图片。这个坑非常经典,提前说清楚,希望大家避过去。
5.3 部署和高频踩坑清单
项目做完之后,真正折磨人的往往是部署环节。我把踩过的和看别人踩过的高频问题汇总成了一张表:
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 数据库连接报错 | MySQL 8驱动类名变了 | 驱动用com.mysql.cj.jdbc.Driver |
| 时间字段相差8小时 | 未配置时区 | 连接串加serverTimezone=Asia/Shanghai |
| 运行后首页白屏 | 前端页面没打进入jar | dist目录放对位置,或直接用Thymeleaf模板 |
| 上传图片浏览器打不开 | 静态资源映射缺失 | 确认映射路径和前端请求路径一致 |
| 端口被占用 | 服务器已有服务 | 换端口或杀进程,用netstat -anp查 |
| 字体/图片中文乱码 | 系统编码问题 | Linux上检查LANG环境变量,文件路径避免中文 |
打包命令直接使用Maven即可:
mvn clean package -DskipTests java -jar target/car-trading-system.jar --spring.profiles.active=prod &建议在服务器上用一个独立的/data/app目录放jar,配置文件放在jar外,升级时只需要替换jar,配置不动。这个小细节虽然简单,但能让你的项目在演示时显得很规范。
5.4 列表查询性能:一次索引和联表就能解决
车辆列表页如果一次查出几百辆车,每辆车再逐条查图片,性能就会很难看。这个问题叫N+1查询,就是我查出N条车辆记录,结果额外执行了N次图片查询。正确做法是先查车辆列表,拿到车辆ID集合,再用IN一次性查图片,然后在内存里按车辆ID分组。
另外,车辆表的索引建议不要在status、expect_price这些单列上各搞一个普通索引,而是针对最常用的组合场景建立联合索引,比如(city, status, expect_price)。这个我在建表里已经给了示例。虽然毕设数据量不大,索引优劣不明显,但写出"我为高频查询设计了联合索引"这句话在论文的优化章节里仍然是一个实打实的技术点。
6. 答辩演示的叙事线:把"撮合+协商"讲清楚并展示异常处理
6.1 演示主线建议
答辩演示环节,我不建议打开系统就随便点。提前设计一条"交易主线"会让评委的注意力一直跟着你的设计逻辑走,我当时的演示顺序,你可以直接模仿:
- 用卖家账号登录,发布一辆车,展示图片上传和字段校验,提交后车源状态是"待审核"。
- 切换到管理员账号,审核通过,顺便提一嘴管理员还可以驳回并填写原因。
- 切换到买家账号,通过多条件搜索找到这辆车,点击进入详情页。
- 发起出价,把价格压到低于心理底价,展示系统弹出的"估值参考区间"。
- 切换卖家账号,看到协商消息,还价一次。
- 再切回买家账号,按卖家的还价接近或达到底价,系统提示"已进入可成交区间"。
- 发起预约看车,卖家确认预约。
- 下单支付定金,展示车源状态变为"已预订"。
- 最后在订单模块里走完过户交付,车源状态变成"已售"。
这一套走下来,状态机的每一步都清晰可见,评委不需要你解释太多就能感受到系统的完整性。比你东点一下西点一下强得多。
6.2 主动演示异常分支,比遮盖问题更有说服力
答辩时很多同学害怕暴露错误,我的经验恰好相反:主动演示一两个"异常被正确处理"的场景,是提升印象分最快的办法。你可以准备两个小脚本:
第一个:买家A和买家B同时打开同一辆车详情页,A先提交订单锁定车辆,B再点击下单,系统提示"车辆已被预订"。这里你可以顺势讲出乐观锁的原理,评委基本都会被这个吸引。
第二个:卖家编辑一辆已售车辆,系统提示"当前状态不可编辑",同时按钮置灰。这展示了你在状态机设计上确实考虑到了非法操作的拦截。
还有越权访问的展示:普通买家直接请求管理员接口,返回403 JSON。这些场景非常加分,因为它们说明你的代码有防御性设计,而不只是功能能跑通。
6.3 常见答辩追问与回答思路
提前把几个高频问题想好答案,答辩就不会慌:
- 问:为什么用Spring Boot而不用传统的SSH?答:Spring Boot内置Tomcat、自动配置、Starter生态,开发效率高;同时仍然基于Spring生态,IOC和AOP机制没有变。
- 问:你的车辆搜索如何保证数据一致?答:查询走MySQL索引,热度数据如果需要可以缓存到Redis,但下单和状态修改永远走数据库事务。
- 问:如果车辆下架时有人正在议价,怎么处理?答:下架意味着协商会话强制关闭,通过状态机约束,所有涉及车源的业务操作都会先校验当前状态。
- 问:多条件检索时怎么做索引优化?答:基于最常用的组合查询建立联合索引,利用最左前缀原则,避免全表扫描。
回答这些问题的时候语气要笃定,引用自己代码里的具体类名和方法名会让人产生"他是真做了"的信任感。这比任何花哨的PPT都有效。
最后说点我自己的实操体会。做这种偏业务系统的毕业设计,最忌讳的是把大量时间花在平平无奇的CRUD页面上,最后做出一个"管理员后台大全"。你真正要做的,是选一条交易闭环,比如"发布到成交",把它每个状态流转、每个边界条件都理清楚,然后再往外扩展。实测下来,只要核心闭环立得住,老师问到任何场景你都能接得住。答辩前我强烈建议准备一套有层次感的演示数据,不同品牌、不同价位、不同城市的车都要有,现场临时填数据不仅浪费时间,还会暴露页面空状态没做好的问题。一个能让评委跟着你的节奏走完整个故事的项目,拿高分就差不了太多。