恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
SpringBoot+Vue在线拍卖系统实战:并发控制与前后端分离
首页
资讯中心
/
SpringBoot+Vue在线拍卖系统实战:并发控制与前后端分离
SpringBoot+Vue在线拍卖系统实战:并发控制与前后端分离
发布时间:2026/9/18 0:25:40
做了这么多年Java后端也带过不少实习生和毕业生我越来越觉得“拍卖系统”是毕设选题里被低估的一个方向。市面上大量“学生管理系统”“图书管理系统”同质化严重答辩时老师一眼看穿很难有亮点。而SpringBootVue的在线拍卖系统业务闭环完整、有并发场景、有状态流转、有前后端分离不管是课设、毕设还是自己练手学习都能让你真正学到东西而不是停留在“增删改查”表面。这篇文章就围绕这个项目的完整实现展开包含技术选型逻辑、数据库设计、后端核心模块尤其出价并发控制、前端关键交互、部署上线以及问题排查。内容会比较多但每一步都是可落地、可复现的适合有一定Java基础、正在找全栈项目练手的朋友。1. 项目定位与技术选型思路1.1 为什么是SpringBootVueMySQL这套组合先说结论这套技术栈是目前国内中小型项目、企业面试题和校园毕设里出现频率最高的组合之一。SpringBoot简化了SSM时代的配置地狱内嵌Tomcat让部署不再是痛苦事Vue作为渐进式前端框架学习曲线平缓、生态丰富MySQL则是最经典的开源关系型数据库资料多、排错容易。三者组合既主流又有深度答辩时老师不会觉得你在炫技但你又能在业务里展示并发控制、状态机设计这些硬功夫。SpringBoot真正让我觉得“省心”的是它的自动装配机制。你引入spring-boot-starter-web内嵌容器、DispatcherServlet这些就被自动配置好了引入spring-boot-starter-data-redisRedisTemplate就能直接注入使用。原理上SpringBootApplication里的EnableAutoConfiguration会扫描META-INF/spring.factories新版本是AutoConfiguration.imports中声明的自动配置类再配合ConditionalOnClass、ConditionalOnMissingBean等条件注解按需生效。面试时被问到“SpringBoot自动装配原理”其实就是这句话展开说清楚这也是我在后面设计项目时一直遵循的思路全局配置简化但核心逻辑自己掌控。Vue这边我选的是Vue2 Element UI组合。很多新手一上来就追Vue3 Element Plus不是说不行而是网上大量毕设参考代码、踩坑文章都基于Vue2遇到问题更容易找到答案。如果你是自学者先把一套组合吃透比频繁换技术栈更重要。前端管用户交互后端管业务逻辑前后端分离通过JSON交互这种协作模式也是目前企业的标准做法。1.2 在线拍卖系统的业务闭环与技术亮点拍卖系统和普通电商系统最大的区别在于“价格是动态变化的”。普通商城是明码标价用户下单支付就行拍卖系统则是起拍价加价幅度出价记录截拍时间这导致业务上有几个真正值得下功夫的点第一是出价并发控制多个用户同时出价必须保证最终只有一个成功而且系统里记录的当前最高价不能乱。这个场景能用来讲清楚“锁”“事务”“隔离级别”这些概念而不是停留在背面试题。第二是拍卖状态流转一个拍品要经历“待审核 - 竞拍中 - 已结束成交/流拍”状态的推进可以由定时任务扫描触发也可以由用户操作触发。把这个理顺你就理解了状态机设计在业务系统里的作用。第三是前后端高频交互前端要展示倒计时、刷新最新出价、在最后几秒防止“截拍狙击”这里就涉及轮询、接口幂等、组件生命周期管理等细节。第四是管理后台闭环管理员审核拍品、管理场次、处理成交订单这个部分能让你的系统从“demo”变成“完整平台”也是答辩时展示系统完备性的重要模块。这套项目的整体架构可以概括为三个端用户前端、管理后台、后端服务一个数据库MySQL。用户前端用Vue搭建管理后台可以和用户端合并成一个工程划分路由即可后端按标准分层结构拆成controller / service / mapper / entity数据库以商品表和出价记录表为核心向外辐射出订单、用户、公告等表。2. 系统功能模块与数据库设计2.1 角色权限与功能拆分在设计系统前先把角色理清楚。一个在线拍卖平台必须有普通用户和管理员两种角色否则“管理平台”名不副实。普通用户端核心功能注册登录、个人信息维护浏览拍卖中的拍品列表、查看拍品详情对竞拍中的拍品出价查看自己的出价记录竞拍成功后生成待支付订单进入个人订单列表查看平台公告管理后台核心功能用户管理查看用户列表、禁用异常账号拍品管理审核用户提交的拍品、上架/下架、设置拍卖场次场次管理创建拍卖场次设定开始时间、结束时间订单管理查看成交订单、处理发货公告管理发布平台公告数据看板统计成交额、成交数量、热门拍品等功能清单看着多但落到数据库上其实只有六张核心表就够用户表、拍品表、出价记录表、订单表、拍卖场次表、公告表。加上一个操作日志表可选就可以支撑完整业务。2.2 数据库表结构设计要点数据库设计是毕设答辩的高频提问区域设计得好不好内行人一眼就能看出来。这里先说几个关键原则再给核心建表SQL。金额字段用Decimal绝不用Float/Doublefloat和double在MySQL里是近似存储做价格计算会出“0.10.2不等于0.3”这种诡异问题。金额用DECIMAL(10,2)精确到分稳妥又专业。状态字段用TinyInt存储代码里用枚举对应数据库里存0、1、2、3这样的数字不要直接存中文。后端定义枚举类统一管理避免代码里到处写魔法数字。比如拍品状态Getter AllArgsConstructor public enum AuctionStatus { PENDING(0, 待审核), ONGOING(1, 竞拍中), FINISHED(2, 已结束), SOLD(3, 已成交), FLOW(4, 流拍); private final int code; private final String desc; }出价记录表要建联合索引一次拍卖可能产生几百上千条出价记录需要经常按“拍品ID 出价金额”查询最高价和出价历史所以表结构里要提前考虑索引。这也是答题時可以讲的细节“MySQL索引为什么用B树”“联合索引最左前缀原则”这些基础知识正好在这个表上做文章。用户表CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, phone varchar(20) DEFAULT NULL COMMENT 手机号, role tinyint(4) NOT NULL DEFAULT 1 COMMENT 1普通用户 2管理员, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;拍品表CREATE TABLE goods ( id bigint(20) NOT NULL AUTO_INCREMENT, goods_name varchar(100) NOT NULL COMMENT 拍品名称, description text COMMENT 拍品描述, cover_img varchar(255) DEFAULT NULL COMMENT 封面图URL, start_price decimal(10,2) NOT NULL COMMENT 起拍价, current_price decimal(10,2) DEFAULT NULL COMMENT 当前最高出价, bid_increment decimal(10,2) NOT NULL DEFAULT 100.00 COMMENT 最低加价幅度, auction_id bigint(20) DEFAULT NULL COMMENT 所属场次ID, seller_id bigint(20) DEFAULT NULL COMMENT 委托用户ID, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待审核 1竞拍中 2已结束 3已成交 4流拍, start_time datetime DEFAULT NULL COMMENT 开拍时间, end_time datetime DEFAULT NULL COMMENT 截拍时间, version int(11) NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_auction_id (auction_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT拍品表;出价记录表CREATE TABLE bid_record ( id bigint(20) NOT NULL AUTO_INCREMENT, goods_id bigint(20) NOT NULL COMMENT 拍品ID, user_id bigint(20) NOT NULL COMMENT 出价用户ID, price decimal(10,2) NOT NULL COMMENT 出价金额, create_time datetime DEFAULT NULL COMMENT 出价时间, PRIMARY KEY (id), KEY idx_goods_price (goods_id, price) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT出价记录表;这里有个细节值得展开current_price字段被冗余到了拍品表里。为什么不直接每次从出价记录表里取MAX(price)因为在“拍卖进行中”这个高频查询场景下每次聚合一张越来越大的记录表性能会随着数据量增长明显下降。把当前最高价冗余在拍品表上查询详情页时只需要带走一条记录效率最高。代价是出价时需要在一个事务里同时更新拍品表并插入出价记录保证数据一致。这个“空间换时间”的思路恰恰是面试中常考的“反范式设计”的典型例子。2.3 订单表与关联关系竞拍结束后系统会根据出价记录生成待支付订单。订单表建议单独拆出来而不是把订单信息挂在拍品表上原因很简单订单有自己独立的生命周期待支付、已支付、已发货、已完成也有独立的金额快照需求。比如拍品当前价格可能因为后续操作变化但订单里的成交金额必须冻结在成交那一刻。订单表核心字段CREATE TABLE order_info ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, goods_id bigint(20) NOT NULL COMMENT 拍品ID, buyer_id bigint(20) NOT NULL COMMENT 买家ID, seller_id bigint(20) NOT NULL COMMENT 卖家ID, amount decimal(10,2) NOT NULL COMMENT 成交金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成, pay_time datetime DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_buyer_id (buyer_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;表与表之间的关系不复杂一个用户可以委托多个拍品1对多一个拍品属于一个场次多对1一个拍品有N条出价记录1对N一个成交拍品对应一条订单1对1。把这些关系画清楚做前端页面和写接口的时候思路会非常清晰。3. 后端核心实现SpringBoot关键模块3.1 项目结构与基础设施搭建后端我习惯按“controller - service - mapper - entity”四层拆分再补上config、common等辅助包。完整结构如下com.auction.system ├── controller # 接口层 │ ├── AuthController │ ├── GoodsController │ ├── BidController │ ├── OrderController │ └── AdminController ├── service # 业务层 │ ├── AuthService │ ├── GoodsService │ ├── BidService │ └── OrderService ├── mapper # 数据访问层 │ ├── UserMapper │ ├── GoodsMapper │ ├── BidRecordMapper │ └── OrderMapper ├── entity # 数据库实体 │ ├── User │ ├── Goods │ ├── BidRecord │ └── OrderInfo ├── config # 配置类 │ ├── CorsConfig │ ├── WebMvcConfig │ └── MybatisPlusConfig └── common # 封装的通用类 ├── Result ├── BusinessException └── GlobalExceptionHandler项目依赖在pom.xml里加这几组就够SpringBoot Web、MyBatis-Plus、MySQL驱动、Lombok、JWT工具包、Hutool工具集。mybatis-plus 是我比较推荐的持久层框架它的内置CRUD方法能省掉大量Java代码。普通单表操作你甚至不需要写XML只用LambdaQueryWrapper就能搞定。它不是把MyBatis的灵活能力弄丢而是把“动态SQL拼装”这个麻烦事藏了起来。对于毕设级别的项目完全够用而且代码可读性比手写一堆XML好得多。配置方面application.yml里最核心的是数据源配置。MySQL 8.0以上版本要注意驱动类变化和时区问题server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/auction_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: your_password mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplserverTimezoneAsia/Shanghai这个参数非常关键缺失时数据库连接会直接报The server time zone value异常。这是我见过出现频率最高的启动报错之一。3.2 登录鉴权与JWT实践管理平台必须做登录鉴权不可能让游客直接调用管理接口。这里我采用JWT无状态鉴权方案原因是它实现简单、前后端分离天然友好不需要在服务端维护Session。注册时密码用BCrypt加密。BCrypt和MD5、SHA这种摘要算法最大的不同是自带随机盐每次加密结果都不同同一个密码存到数据库里也是不同的密文这样即使数据库泄露彩虹表攻击也很难生效。// 注册时加密 String encodedPwd BCrypt.hashpw(user.getPassword(), BCrypt.gensalt()); user.setPassword(encodedPwd); // 登录时校验 if (BCrypt.checkpw(rawPassword, user.getPassword())) { // 密码正确生成token String token JwtUtil.generateToken(user.getId(), user.getUsername(), user.getRole()); }登录成功后将用户ID和角色写入JWT返回给前端。前端把Token存在localStorage里后续每个请求在拦截器中带上Authorization: Bearer token头。后端做一个拦截器统一校验public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { Claims claims JwtUtil.parseToken(token.replace(Bearer , )); if (claims ! null) { request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } } response.setStatus(401); return false; } }在WebMvcConfig里注册拦截器时要设计好放行路径。登录、注册、拍品列表、拍品详情这些公开接口放行出价、订单、管理后台相关接口都要拦截。管理后台接口还要再校验role 2防止普通用户越权访问。3.3 拍卖业务核心出价并发控制的三种方案这是整个系统的灵魂也是你答辩时最有技术含量的谈资。先说问题当两个用户同时出价都读到当前价格是1000元一个报1200一个报1300怎么保证最终数据库里的结果不是乱的方案一数据库悲观锁FOR UPDATE在事务中查询拍品时使用SELECT ... FOR UPDATE把这一行锁住直到事务提交或回滚才释放。并发场景下第二个用户的查询会阻塞等第一个事务完成后再读取到最新数据。Transactional(rollbackFor Exception.class) public BidResult placeBid(BidRequest request) { Goods goods goodsMapper.selectForUpdate(request.getGoodsId()); if (goods null || goods.getStatus() ! 1) { throw new BizException(拍品不存在或不在竞拍期); } BigDecimal currentPrice goods.getCurrentPrice() null ? goods.getStartPrice() : goods.getCurrentPrice(); if (request.getPrice().compareTo(currentPrice.add(goods.getBidIncrement())) 0) { throw new BizException(出价不能低于当前价加最低加价幅度); } // 更新当前价并插入出价记录 goods.setCurrentPrice(request.getPrice()); goodsMapper.updateById(goods); BidRecord record new BidRecord(); record.setGoodsId(goods.getId()); record.setUserId(request.getUserId()); record.setPrice(request.getPrice()); bidRecordMapper.insert(record); // 自动延时逻辑距结束不足1分钟则延长1分钟 return BidResult.success(goods.getCurrentPrice()); }这个方案实现简单、思路清晰对毕设项目来说完全够用。缺点是并发高时数据库行锁竞争会成为瓶颈但拍卖这种低频高价值场景完全不需要担心。方案二乐观锁CAS不锁行而是在更新时带上版本号或旧状态作为条件。更新的影响行数为0说明数据已经被别人改了本次操作失败。UPDATE goods SET current_price #{newPrice}, version version 1 WHERE id #{goodsId} AND current_price #{oldPrice}代码里对应的是boolean success goodsMapper.updatePriceWithVersion(goodsId, newPrice, oldPrice); if (!success) { throw new BizException(手慢了价格已被刷新请重新出价); }乐观锁的优点是并发性能更好缺点是冲突频繁时用户体验差用户可能反复刷新重试。适合“读多写少、冲突概率低”的业务拍卖出价其实也算适配。方案三RedisLua脚本把当前价格、结束时间等数据放到Redis里用Lua脚本原子性地执行“读价格 - 判断是否满足加价幅度 - 更新价格 - 记录出价人”这一串操作。这是电商秒杀场景的经典方案性能极高但实现复杂需要额外部署Redis而且最终数据还是要异步落库。我的建议是项目里主推“悲观锁事务”同时把乐观锁代码也写在同一个Service里注释说明两套方案的适用场景。答辩时老师问你“并发问题怎么解决”你不仅能回答还能对比分析三个方案的优劣这就是加分项。3.4 定时任务与拍卖状态流转拍卖系统的另一个核心问题是状态流转一场拍卖从“未开始”到“竞拍中”再到“已结束”以及结束时判断成交还是流拍必须有可靠机制触发。我采用Spring自带的Scheduled定时任务单机环境下足够稳定。业务逻辑是每分钟扫描一次拍品表把start_time 当前时间 end_time且状态为“待审核实际上架后”的拍品置为“竞拍中”把end_time 当前时间且状态为“竞拍中”的拍品置为“已结束”对“已结束”的拍品检查是否有出价记录有则生成订单并置为“已成交”没有则置为“流拍”Component Slf4j public class AuctionStatusTask { Scheduled(fixedDelay 60000) public void processAuctionStatus() { // 1. 扫描开拍状态为待审核/已上架start_time已到置为竞拍中 // 2. 扫描截拍状态为竞拍中end_time已过置为已结束 // 3. 生成订单对已结束的拍品判断最高出价并生成订单 } }细心的人会发现光有每分钟扫描还不够。如果用户在结束前30秒出价这个出价是有效的但下一分钟扫描时拍品已经结束这个出价就“来不及”了。所以出价接口里要有自动延时逻辑当出价时间距离end_time不足1分钟时将end_time顺延1分钟。这就是很多拍卖平台“最后1分钟有人出价拍卖延长1分钟”的实现原理本质上是为了防止最后时刻的“狙击出价”。状态流转统一用AuctionStatus枚举维护每次状态变更都写日志这样出了问题可以追溯。把这条链路跑通你的系统就不再是简单的CRUD了而是一个有“业务节奏”的平台。4. 前端核心实现Vue与管理后台4.1 前端工程化与路由设计前端建议用Vue CLI 4或5创建项目。如果你是跟着教程安装遇到过ERESOLVE之类依赖报错可以手动把npm源切成淘宝镜像再安装依赖基本能解决大部分环境问题。npm config set registry https://registry.npmmirror.com vue create auction-web npm install vue-router3 axios element-ui2路由设计上分成两大块用户端和管理端。用户端路由包括首页、拍品列表、拍品详情、用户中心、登录注册管理端路由包括仪表盘、拍品管理、用户管理、订单管理、场次管理、公告管理。管理端的路由组件全部放在views/admin目录下并且用路由守卫做权限控制router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else if (to.path.startsWith(/admin)) { const role localStorage.getItem(role) if (role ! 2) { next(/) } else { next() } } else { next() } })接口请求统一封装在utils/request.js里创建axios实例、设置baseURL、添加请求拦截器注入Token、响应拦截器统一处理错误码。这样业务代码里不需要重复处理登录过期、接口报错这些公共逻辑。4.2 核心页面交互竞拍出价与倒计时拍品详情页是用户端最核心的页面。要展示的信息包括拍品图片、名称、描述、当前最高价、最低加价幅度、倒计时、出价记录列表、出价输入框。这里有两个关键技术点倒计时组件倒计时不能用前端生成的时间来计算必须根据服务端返回的endTime和本地时间做差值。很多新手直接setInterval每秒减一页面刷新或短暂卡顿后数据就错乱了。正确写法是每次计算new Date(endTime).getTime() - Date.now()。template span{{ formatTime }}/span /template script export default { name: CountDown, props: { endTime: { type: String, required: true } }, data() { return { remain: 0, timer: null } }, mounted() { this.calc() this.timer setInterval(this.calc, 1000) }, beforeDestroy() { clearInterval(this.timer) }, methods: { calc() { const diff new Date(this.endTime).getTime() - Date.now() this.remain Math.max(0, parseInt(diff / 1000)) if (this.remain 0) { clearInterval(this.timer) this.$emit(timeup) } } }, computed: { formatTime() { const h parseInt(this.remain / 3600) const m parseInt((this.remain % 3600) / 60) const s this.remain % 60 return ${h}时${m}分${s}秒 } } } /script大于24小时不用管组件会自动计算。出价交互出价框里预填“当前价 加价幅度”前端先做一轮校验不满足条件直接提示减少无效请求。用户点“出价”按钮后调用后端接口。成功则更新当前价格、刷新出价记录列表、弹窗提示“出价成功”失败则弹出“出价已被超过最新价为xxx请重新出价”同时刷新最新价格。由于拍卖页需要展示最新价格我又不想为了一个小项目上WebSocket就采用简单轮询方案页面挂载后每3秒调一次“查询拍品详情”接口更新价格和倒计时。组件销毁时清掉定时器避免内存泄漏。答辩时可以提一句“生产级别可以升级为WebSocket或SSE推送”体现你考虑过这个问题。4.3 管理后台商品审核与订单管理管理后台在技术上和用户端没有本质区别更多的是业务功能的设计。拍品管理页面是一张表格列出所有拍品状态列用标签颜色区分待审核用黄色、竞拍中用绿色、已结束用灰色、已成交用蓝色。管理员可以对“待审核”的拍品点击“通过”或“驳回”。通过后拍品自动关联到正在进行的场次等待定时任务或管理员手动将其置为“竞拍中”。订单管理页面展示所有成交订单包括订单编号、拍品名称、买卖双方、成交金额、订单状态。管理员可以执行“发货”操作订单状态从“已支付”变为“已发货”。上传图片这个功能建议使用Element UI的el-upload组件上传接口接收文件后保存到服务器本地目录数据库里只存URL。服务端要做文件大小和格式校验防止用户传超大文件或恶意脚本。跨域配置要放开这个上传接口。4.4 前后端联调与接口约定前后端分离项目最容易在联调阶段出问题提前约定好规范能省很多时间。我的习惯是统一返回结构{ code: 200, message: success, data: {} }错误时返回非200的code前端拦截器统一弹出message。所有分页接口入参统一用pageNum和pageSize返回结构统一是{ records, total }这样前端写表格组件时可以封装一套通用逻辑。跨域问题的处理我推荐在后端加全局CORS配置简单又不容易漏Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }有一个坑必须提醒如果后端返回的实体类主键用的是Long而数据库主键用了雪花ID或类似超过2^53的值前端JavaScript解析会丢失精度。你可能会看到两条记录的ID一模一样排查半天才发现是精度问题。解决办法是给主键字段加上JsonSerialize(using ToStringSerializer.class)让后端返回字符串类型public class Goods { JsonSerialize(using ToStringSerializer.class) private Long id; }这个坑几乎每个做全栈项目的人都会遇到提前处理好能省一晚上的排查时间。5. 部署上线与常见问题排查5.1 本地运行环境配置项目想在自己电脑上跑起来环境至少要满足JDK 8或JDK 11这个项目的SpringBoot版本建议用2.7.x不要盲目追3.xMaven 3.6以上MySQL 5.7或8.0Node.js 14以上npm 6以上启动步骤其实很固定创建数据库CREATE DATABASE auction_db DEFAULT CHARACTER SET utf8mb4;导入init.sql脚本生成所有表结构修改后端application.yml中的数据库用户密码启动后端mvn spring-boot:run确认8080端口启动成功进入前端目录npm install然后npm run serve浏览器访问http://localhost:8081Vue默认端口5173或8080冲突就改一个这里特别提两句“SpringBoot版本太高”的问题。很多人喜欢下载最新版SpringBoot比如3.x但3.x最低要求JDK17而且很多老教程里的starter坐标变了。如果你用的还是JDK8老老实实用SpringBoot 2.7.18它是2.x的最后一个支持版本资料最多、最稳定。5.2 部署方案与性能优化到了展示阶段可以把项目部署到服务器上。前端打包后是一个dist目录里面全是静态文件后端打包成可执行JAR包。用Nginx托管前端静态文件同时把/api请求反向代理到后端服务这是目前最主流的前后端分离部署方式server { listen 80; server_name auction.example.com; root /opt/auction/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 前端history路由模式必须加这个刷新页面才不会404 location / { try_files $uri $uri/ /index.html; } }后端打包部署命令mvn clean package -DskipTests java -jar auction-server.jar --spring.profiles.activeprodvue 打包后布局异常这个问题我见过很多次通常是因为前端资源用了绝对路径/js/app.js部署到子目录后全部404导致页面样式全乱。解决办法是修改vue.config.js。module.exports { publicPath: ./ }让打包后的资源都变成相对路径这样部署到任何子目录都能正常加载。性能优化方面毕设项目不需要做到极致但要知道方向。第一层是加Redis缓存热门拍品信息减少数据库压力第二层是给常用查询加索引如出价记录表的(goods_id, price)联合索引第三层是把定时任务、订单生成这些逻辑尽量异步化。真把这些做好你的项目已经远超一般毕设水平。5.3 常见问题速查表问题现象可能原因解决方案后端启动报“The server time zone value”MySQL连接字符串缺少时区参数URL末尾加serverTimezoneAsia/Shanghai前端请求接口提示跨域后端未配置CORS或配置覆盖不全添加全局CORS配置类出价后当前价格不更新事务未提交或用了乐观锁但没处理失败检查事务注解乐观锁失败时给用户明确提示图片上传后访问404静态资源映射路径未配置配置WebMvcConfigurer的addResourceHandlers打包部署后页面空白或样式丢失前端资源路径是绝对路径vue.config.js里设置publicPath: ./Long类型ID前端显示精度错误JavaScript超出2^53精度给Long字段加JsonSerialize(using ToStringSerializer.class)定时任务到点没执行主类缺少EnableScheduling注解在启动类上添加该注解页面刷新404History模式Nginx未配置try_files添加try_files $uri $uri/ /index.html;npm install安装报错npm版本或依赖树冲突切换淘宝镜像源或使用--legacy-peer-deps6. 进阶扩展建议与个人心得项目跑通后不要急着写在简历上了事有几个方向可以继续深挖既能加深理解又能变成答辩时的加分项方向一接入WebSocket实时推送现在的轮询方案每3秒请求一次体验一般。把它升级成SpringBoot的WebSocket后后端在有新出价时主动推送最新价格给所有在线客户端既能省流量又能做到“秒级”更新还能引出高并发下WebSocket集群如何共享Session的话题。方向二引入Redis缓存和分布式锁把拍品详情、出价记录这些读多写少的数据放到Redis里热点数据查询压力立刻降下来。出价接口可以用Redis的分布式锁替换数据库行锁你会更有体感地理解“锁为什么不能乱用”这也是面试里被问到的分布式场景。方向三对接第三方支付接口订单生成后停留在“待支付”状态如果能在沙箱环境接入一个支付流程整个业务闭环就更完整了。做这个项目时我自己最大的感受是越早把“用户角色”想清楚后面写代码越顺手。第二点心得是状态字段一定要一开始就用枚举别图省事写数字。写数字一时爽等加了定时任务、状态流转、前端标签颜色映射你会发现到处是“3代表什么来着”的困惑。第三也是最重要的一点——这个项目真正让你值回票价的地方并不是“会写登录、会写列表”而是你亲手处理过并发出价、状态流转、延时截拍、精度丢失这些真实问题。带着这些问题去答辩哪怕老师问得再深你也能从容接住。