资讯动态

Java二手车拍卖系统:Spring Boot高并发出价与定时结算

发布时间:2026/10/24 0:42:40 来源:尧图企业网站定制
简介这套Java二手车交易拍卖系统源码工程面向高校计算机相关专业的毕业设计场景适合正在准备毕设、需要可运行项目作参考的本科生也适合想借小型管理系统走完需求分析到测试全流程的初学者。压缩包约10.96MB上游未细分文件清单实际提供程序源代码一份与配套数据库一份按配置环境说明操作即可正常部署运行。工程按软件工程思路组织先通过需求分析确定系统主要功能再完成功能设计、总体结构设计、数据结构设计与安全设计随后落地数据库访问实现与各主要功能模块的具体编码并保留关键代码说明末尾附功能测试与结果分析。读者可据此理解二手车拍卖业务的建模方式参考模块划分与数据表设计对照关键代码做二次开发或答辩演示也能借鉴测试用例的整理思路。已有2281人学习。1. 二手车拍卖系统的难点不在页面在出价那一秒某开发同学拿着毕业设计题目来找我说页面都画好了结果一模拟两个人同时出价价格就乱了。二手车拍卖和普通商品下单不一样普通电商是库存减一拍卖是价格往上顶而且有截止时间。这个项目就是一个基于Java的二手车交易拍卖系统Spring Boot 起服务MySQL 存车辆、场次、出价和订单Redis 处理竞价并发和场次倒计时缓存。它适合两类人一类是拿它做毕业设计、需要完整业务流程和可演示后台的另一类是想练手高并发出价、定时结算、状态机这些后端常见问题的。下面从目录结构一路拆到出价接口和二次开发能跑起来的部分我给命令能改的地方我标参数。2. 技术选型与目录结构一个 Spring Boot 单应用怎么装下拍卖流程2.1 为什么这个场景用单应用而不是微服务二手车拍卖的业务边界其实很清楚用户、车辆、场次、出价、订单、后台。毕业设计或者中小型项目我一般不建议上来就拆微服务。原因很实际出价要校验场次状态、保证金、加价幅度、当前最高价这些校验放在一个事务里最省心拆成服务后分布式事务和跨服务查询会把简单问题复杂化。这个项目用 Spring Boot 单应用按 controller / service / mapper 分层MyBatis-Plus 做持久层Redis 只承担两个职责缓存热门场次的当前价和做竞价短锁。前端常见做法是 Thymeleaf 服务端渲染加少量 Ajax也有用 Vue Element UI 前后端分离的版本目录里一般会多一个front或admin静态工程。层次选型用途后端框架Spring Boot 2.x统一配置、内嵌 Tomcat、快速启动持久层MyBatis-Plus单表 CRUD、条件构造器、分页数据库MySQL 8车辆、场次、出价、订单、用户缓存/并发Redis场次当前价缓存、出价短锁模板/前端Thymeleaf Bootstrap 或 Vue用户端和后台页面定时任务Spring Task拍卖结束结算、流拍处理文件存储本地磁盘 静态映射车辆图片、检测报告提示如果下载到的工程里没有 Redis 依赖先看pom.xml里有没有spring-boot-starter-data-redis没有的话出价并发那块可能是用数据库乐观锁兜底的跑之前要确认。2.2 目录结构从 controller 到 mapper 的一条线单模块工程的典型目录长这样car-auction/ ├── src/main/java/com/example/auction/ │ ├── AuctionApplication.java # 启动类 │ ├── config/ # Web、Redis、MyBatis-Plus、拦截器配置 │ ├── controller/ # 用户端和后台接口 │ ├── service/ # 业务接口 │ │ └── impl/ # 拍卖、出价、订单实现 │ ├── mapper/ # MyBatis-Plus Mapper │ ├── entity/ # 数据库实体 │ ├── dto/ # 请求/响应对象 │ ├── vo/ # 页面展示对象 │ ├── task/ # 定时结算任务 │ ├── common/ # 统一返回、异常、常量 │ └── utils/ # 日期、金额、文件工具 ├── src/main/resources/ │ ├── application.yml # 数据源、Redis、上传路径 │ ├── mapper/ # XML复杂查询才用 │ ├── static/ # css、js、图片 │ └── templates/ # Thymeleaf 页面 └── pom.xml这个结构不新鲜但对拍卖系统够用。我一般会先看controller里有哪些入口再看service/impl里哪个类最长通常最长那个就是出价或者结算。车辆发布、审核、上架这些是普通 CRUD真正容易翻车的是出价和关闸。后台管理一般单独一个AdminController路径带/admin登录拦截器会区分角色。2.3 依赖和配置跑起来之前先对这三处pom.xml里关键依赖通常包括dependencies !-- Web 和模板 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency !-- MyBatis-Plus -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- MySQL 驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- Redis出价锁和缓存用 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- 参数校验 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependenciesapplication.yml里要改的主要是数据源、Redis 和上传目录server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/car_auction?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 servlet: multipart: max-file-size: 5MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: id-type: auto logic-delete-field: deleted auction: upload-path: D:/car-auction/upload/ deposit-rate: 0.05 # 保证金比例 bid-step: 100 # 默认加价幅度 delay-seconds: 120 # 最后两分钟出价自动延时参数说明serverTimezone必须写不然时间字段容易差 8 小时deposit-rate是保证金占起拍价比例改这个会影响出价资格delay-seconds是防狙击的延时窗口很多拍卖系统都有没写的话最后几秒出价会直接卡死。上传路径要提前建好目录Windows 下注意结尾斜杠。注意MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver如果下载到的工程里写的是com.mysql.jdbc.Driver启动会报弃用警告改成前者即可。配置对完后按三步启动建库、导 SQL、跑起来。命令贴在下面执行前确认本地 MySQL 和 Redis 都起来了。# 1. 建库 mysql -uroot -p -e create database car_auction default charset utf8mb4; # 2. 导入表结构和初始数据 mysql -uroot -p car_auction sql/car_auction.sql # 3. 启动 mvn spring-boot:run启动后访问http://localhost:8080看首页/admin看后台。如果首页空白先看控制台有没有模板解析异常再看templates下文件名和 controller 返回值是否一致。3. 核心模块拆解拍卖场次、出价、结算的三条链路3.1 场次状态机从草稿到流拍只有五步拍卖场次是整条业务线的骨架。常见做法是用一个status字段控制0 草稿、1 待审核、2 进行中、3 已结束、4 流拍、5 已成交。状态流转不是随便改的要在 service 里收口。比如车辆审核通过后才能发布场次场次开始时间到了才能出价结束时间到了由定时任务关闸。下面是一段状态流转的常量定义和校验public class AuctionStatus { public static final int DRAFT 0; // 草稿仅卖家可见 public static final int PENDING 1; // 待审核后台可看 public static final int RUNNING 2; // 进行中可出价 public static final int FINISHED 3; // 已结束等待结算 public static final int FAILED 4; // 流拍无人出价或未达底价 public static final int DEAL 5; // 已成交生成订单 } // 出价前的状态校验放在 service 里不要只靠前端按钮置灰 public void checkBiddable(AuctionSession session) { if (session.getStatus() ! AuctionStatus.RUNNING) { throw new BizException(当前场次不在竞价中); } LocalDateTime now LocalDateTime.now(); if (now.isBefore(session.getStartTime())) { throw new BizException(拍卖尚未开始); } if (now.isAfter(session.getEndTime())) { throw new BizException(拍卖已结束); } }逻辑说明checkBiddable做了三道校验状态、开始时间、结束时间。参数session是从数据库查出来的场次不要信前端传的状态。很多翻车现场是只在前端把按钮置灰接口照样能调最后出现结束后还能出价的数据。时间比较用LocalDateTime和数据库datetime字段对应不要用Date混着来。3.2 出价接口一次有效出价要过五道校验出价是整个系统最不能松的接口。我一般会把校验顺序排成登录和实名 → 场次状态 → 保证金 → 加价幅度 → 当前最高价。任何一步不满足都直接返回不往下走。下面是一个简化版的 service 方法Transactional(rollbackFor Exception.class) public BidResult placeBid(Long sessionId, Long userId, BigDecimal bidPrice) { // 1. 场次校验状态、时间窗口 AuctionSession session sessionMapper.selectById(sessionId); checkBiddable(session); // 2. 保证金校验没交保证金的用户不能出价 Deposit deposit depositMapper.selectByUserAndSession(userId, sessionId); if (deposit null || deposit.getStatus() ! 1) { throw new BizException(请先缴纳保证金); } // 3. 加价幅度校验出价必须 当前价 最小加价 BigDecimal current session.getCurrentPrice() null ? session.getStartPrice() : session.getCurrentPrice(); BigDecimal minNext current.add(session.getBidStep()); if (bidPrice.compareTo(minNext) 0) { throw new BizException(出价不能低于 minNext); } // 4. 防重复同一用户连续出价当前价已经是自己顶上去的不允许自己抬自己 Bid lastBid bidMapper.selectLastBid(sessionId); if (lastBid ! null lastBid.getUserId().equals(userId)) { throw new BizException(您已是当前最高出价人); } // 5. 写出一条出价记录并更新场次当前价 Bid bid new Bid(); bid.setSessionId(sessionId); bid.setUserId(userId); bid.setBidPrice(bidPrice); bid.setBidTime(LocalDateTime.now()); bidMapper.insert(bid); // 这里用乐观锁更新避免并发覆盖下一章展开 int rows sessionMapper.updateCurrentPrice(sessionId, bidPrice, session.getVersion()); if (rows 0) { throw new BizException(出价冲突请重试); } // 6. 最后两分钟出价顺延结束时间防止最后一秒狙击 if (Duration.between(LocalDateTime.now(), session.getEndTime()).getSeconds() 120) { sessionMapper.delayEndTime(sessionId, 120); } return new BidResult(true, bidPrice, 出价成功); }逻辑说明这段代码把“能不能出价”和“出价后怎么落库”放在一个事务里。第 4 步的防重复不是必须的有的拍卖允许同一用户多次加价但毕业设计演示时经常要避免自己顶自己所以加了这个限制。参数bidPrice是用户输入金额session.getBidStep()是场次配置的最小加价幅度version是乐观锁版本号。最后两分钟延时是常见拍卖规则delayEndTime直接改数据库结束时间前端倒计时下一轮刷新就能看到。提示金额字段统一用BigDecimal不要用double。数据库对应decimal(12,2)比较用compareTo不要用equals1.0和1.00在equals下不相等。3.3 结算任务到点关闸、生成订单、处理流拍拍卖结束后不能靠用户手动点常见做法是 Spring Task 每分钟扫一次到期的场次Component public class AuctionSettleTask { Resource private AuctionSessionMapper sessionMapper; Resource private BidMapper bidMapper; Resource private OrderService orderService; // 每分钟执行一次扫描已到结束时间但还没结算的场次 Scheduled(cron 0 * * * * ?) public void settle() { ListAuctionSession list sessionMapper.selectExpiredRunning(); for (AuctionSession session : list) { // 先查最高出价没有出价就是流拍 Bid topBid bidMapper.selectTopBid(session.getId()); if (topBid null) { sessionMapper.updateStatus(session.getId(), AuctionStatus.FAILED); continue; } // 有最高价就生成订单状态置为已成交 orderService.createAuctionOrder(session, topBid); sessionMapper.updateStatus(session.getId(), AuctionStatus.DEAL); } } }逻辑说明selectExpiredRunning的 SQL 条件一般是status 2 and end_time now()只查进行中且已到期的。selectTopBid按bid_price desc, bid_time asc排序取第一条价格相同先出价的人赢。参数cron是每分钟第 0 秒执行毕业设计演示够用生产环境要考虑分布式锁不然多实例会重复生成订单第 5 章会讲这个坑。orderService.createAuctionOrder里要写订单号、买家、卖家、成交价、保证金抵扣这些字段在订单表里都要有。4. 数据库与并发控制出价不打架靠的是这三张表和一个版本号4.1 四张主表车辆、场次、出价、订单拍卖系统的表不用太多核心就四张。车辆表存基本信息场次表存拍卖规则和当前价出价表存每一次出价流水订单表存成交结果。下面是我一般会用的字段CREATE TABLE car ( id BIGINT PRIMARY KEY AUTO_INCREMENT, seller_id BIGINT NOT NULL COMMENT 卖家用户ID, brand VARCHAR(50) NOT NULL COMMENT 品牌, model VARCHAR(80) NOT NULL COMMENT 车型, reg_date DATE COMMENT 上牌日期, mileage INT COMMENT 行驶里程公里, guide_price DECIMAL(12,2) COMMENT 新车指导价, images VARCHAR(500) COMMENT 图片路径逗号分隔, audit_status TINYINT DEFAULT 0 COMMENT 0待审 1通过 2驳回, deleted TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 车辆信息; CREATE TABLE auction_session ( id BIGINT PRIMARY KEY AUTO_INCREMENT, car_id BIGINT NOT NULL, seller_id BIGINT NOT NULL, start_price DECIMAL(12,2) NOT NULL COMMENT 起拍价, bid_step DECIMAL(12,2) NOT NULL DEFAULT 100 COMMENT 最小加价幅度, current_price DECIMAL(12,2) COMMENT 当前最高价, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT DEFAULT 0 COMMENT 0草稿 1待审 2进行中 3已结束 4流拍 5成交, version INT DEFAULT 0 COMMENT 乐观锁版本号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 拍卖场次; CREATE TABLE bid ( id BIGINT PRIMARY KEY AUTO_INCREMENT, session_id BIGINT NOT NULL, user_id BIGINT NOT NULL, bid_price DECIMAL(12,2) NOT NULL, bid_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_session_price (session_id, bid_price DESC) ) COMMENT 出价记录; CREATE TABLE auction_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, session_id BIGINT NOT NULL, buyer_id BIGINT NOT NULL, seller_id BIGINT NOT NULL, deal_price DECIMAL(12,2) NOT NULL, deposit_amount DECIMAL(12,2) COMMENT 抵扣的保证金, pay_status TINYINT DEFAULT 0 COMMENT 0待付 1已付 2已退, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 成交订单;字段说明auction_session.version是乐观锁字段出价更新当前价时必须带上bid表的联合索引(session_id, bid_price DESC)是为了快速查最高价current_price允许为空表示还没人出价起拍价单独存。订单表里deposit_amount用来记录保证金抵扣退款逻辑要和支付状态一起看。4.2 出价并发乐观锁更新比锁表更适合这个场景两个人同时出价如果直接update auction_session set current_price ?后写的会覆盖先写的出现“出价成功但价格变低”的玄学现象。常见做法有两种数据库行锁select ... for update和乐观锁版本号。拍卖出价冲突概率不算特别高我一般用乐观锁代码轻失败让用户重试就行// AuctionSessionMapper.xml update idupdateCurrentPrice update auction_session set current_price #{price}, version version 1 where id #{sessionId} and version #{version} and status 2 /update// service 里判断影响行数 int rows sessionMapper.updateCurrentPrice(sessionId, bidPrice, session.getVersion()); if (rows 0) { // 版本号被别的请求改了说明有人抢先出价让前端刷新后重试 throw new BizException(当前价格已更新请刷新后重新出价); }逻辑说明where version #{version}保证只有版本号没变才更新version version 1每次加一。参数sessionId是场次主键price是新出价version是从上一次查询里带出来的。影响行数为 0 不代表系统坏了只代表这一轮没抢到返回提示让用户重试即可。如果业务要求必须成功可以在 service 外层加两到三次重试重新查版本号再更新。注意乐观锁要和事务一起用Transactional加在 service 方法上。如果方法里先查再更新中间不要做远程调用或者长耗时操作不然版本号冲突会变多。4.3 Redis 短锁和缓存只做辅助不替代数据库有的版本会在出价前加 Redis 锁key 用bid:lock:{sessionId}过期时间 3 到 5 秒防止同一场次被大量请求同时打到数据库。我一般把 Redis 定位成“削峰”不把它当唯一正确性保证public boolean tryLock(Long sessionId) { String key bid:lock: sessionId; // SET key value NX PX 3000原子加锁3秒自动过期 Boolean ok redisTemplate.opsForValue() .setIfAbsent(key, 1, Duration.ofSeconds(3)); return Boolean.TRUE.equals(ok); } public void unlock(Long sessionId) { redisTemplate.delete(bid:lock: sessionId); }逻辑说明setIfAbsent对应 Redis 的SET NX只有 key 不存在才设置成功Duration.ofSeconds(3)是过期时间防止宕机后锁不释放。参数sessionId用来隔离不同场次同一场次串行不同场次互不影响。这个锁的粒度不能太大不要用全局锁不然所有拍卖场次都会排队。释放锁直接delete在毕业设计里够用严格来说要用 Lua 脚本校验持有者第 6 章会提。缓存那块常见做法是把场次当前价和结束时间放进 Redis前端轮询读缓存减少数据库压力。但写路径仍然以数据库为准缓存只做展示。不要先改缓存再改数据库不然数据库写失败会出现缓存和库不一致排查起来很麻烦。5. 避坑与排查本地启动、出价异常和定时任务的五个现场记录5.1 启动报数据源连接失败先看库名和时区现象mvn spring-boot:run启动直接抛Communications link failure或者Unknown database car_auction。原因application.yml里的库名和实际建的库不一致或者 MySQL 没启动还有一种常见情况是url里没写serverTimezoneMySQL 8 驱动连接时报时区错误。解决先用命令行确认库存在再对配置mysql -uroot -p -e show databases like car_auction;spring: datasource: url: jdbc:mysql://localhost:3306/car_auction?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse如果库名对、MySQL 也启动就看端口是不是 3306有没有被本地另一个实例占用。密码里如果有特殊字符和#要转义不然 YAML 解析会截断。5.2 出价成功但页面价格不刷新接口返回和前端轮询对不上现象接口返回“出价成功”数据库里current_price也变了但页面停在那不动刷新后才正常。原因前端轮询间隔太长或者轮询接口本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑