
简介Spring Boot作为Java后端的主流框架常被用来快速搭建Web服务。理解分层架构、数据库表结构设计与登录鉴权是开发交易系统的关键能力。以一个校园二手市场平台为实例拆解用户、商品、订单三大核心表的设计要领分析基于JWT的token鉴权流程并展示MyBatis-Plus在业务层的应用方式。这种项目既适合作为毕业设计参考也能帮助开发者掌握从SQL初始化到工程落地的完整思路。内容同时覆盖状态流转、金额精度、文件上传等高频踩坑点为构建中小型C2C交易系统提供实用指南。1. 校园二手市场平台毕业设计里的经典款落地能用才算数每年毕业季Java 方向的毕设选题总绕不开几个经典款校园二手市场平台算一个。这套基于 Spring Boot 的源码不是单个 Demo而是一套完整可用的小型交易系统后端接口、管理端页面、数据库脚本、初始数据都打包好了。拿到手之后不用再从头拼业务逻辑改改数据库连接、跑一下初始化脚本登录、发商品、下单、管理审核这条链路就能完整走通。它适合两类人一类是手里缺一个能现场演示的完整项目的应届生另一类是刚学完 Spring Boot 基础、想看看真实工程结构和模块怎么划分的同学。下面从表结构讲到踩坑记录把这个项目完整拆一遍。2. 把业务拆成表结构用户、商品、订单三个核心模块怎么设计在动手跑任何代码之前我习惯先看数据库脚本。这个项目的表结构决定了它后续所有接口的写法也决定了你答辩时能不能把业务逻辑讲圆。二手交易平台本质上就三件事谁在卖、卖什么、怎么交易。对应到数据库里就是用户表、商品表、订单表再加上几张支撑互动的辅助表。2.1 用户模块学生身份与登录权限的落点用户表是整个系统的地基。校园场景和普通电商最大的区别在于「学生」这个身份约束平台要对卖家做身份校验所以用户表里通常会有学号字段和审核状态字段。CREATE TABLE user ( id int NOT NULL AUTO_INCREMENT COMMENT 用户ID, username varchar(50) NOT NULL COMMENT 登录用户名, password varchar(100) NOT NULL COMMENT 登录密码MD5存储, nickname varchar(50) DEFAULT NULL COMMENT 显示昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, phone varchar(20) DEFAULT NULL COMMENT 手机号, student_no varchar(20) DEFAULT NULL COMMENT 学号用于校园身份认证, role tinyint NOT NULL DEFAULT 1 COMMENT 角色1普通用户 2管理员, status tinyint NOT NULL DEFAULT 0 COMMENT 状态0待审核 1正常 2禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;这段建表脚本里几个字段值得留意。password 用 MD5 存储而不是明文这是毕业设计里经常被问到的问题不要在答辩时说「密码没加密」。student_no 和 status 字段是配套的普通用户注册后默认 0 待审核管理员在后台审核学生身份审核通过才能发布商品。role 字段把管理员和普通用户放在同一张表里省去单独建管理员表的开销登录时根据 role 决定跳转后台还是前台。create_time 用 DEFAULT CURRENT_TIMESTAMP 自动填充insert 时不用手动赋值。2.2 商品模块上下架状态机与浏览计数商品表是信息展示的核心。二手商品的特点是「一件商品只属于一个卖家」所以不需要像电商那样设计库存 SKU一个商品对应一条记录就够了。重点在于状态字段的设计它要覆盖从发布到成交的完整生命周期。CREATE TABLE goods ( id int NOT NULL AUTO_INCREMENT COMMENT 商品ID, user_id int NOT NULL COMMENT 卖家用户ID, title varchar(100) NOT NULL COMMENT 商品标题, description text COMMENT 商品描述, price decimal(10,2) NOT NULL COMMENT 售价, original_price decimal(10,2) DEFAULT NULL COMMENT 原价用于展示成色, category varchar(50) DEFAULT NULL COMMENT 分类教材/数码/生活用品等, images varchar(1000) DEFAULT NULL COMMENT 图片地址多张用逗号分隔, status tinyint NOT NULL DEFAULT 0 COMMENT 状态0待审核 1在售 2已售出 3下架, view_count int NOT NULL DEFAULT 0 COMMENT 浏览次数, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_category (category) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表;status 字段是商品模块的业务核心发布进入待审核管理端审核通过变成在售买家下单并完成交易后变成已售出卖家主动下架则变成下架。四个状态对应四个接口动作答辩时可以拿状态流转当主线讲业务流程。images 字段用逗号分隔多张图片路径在这个项目规模下比单独建图片表简单得多前端按逗号 split 就能渲染多图。price 用 decimal(10,2)这是金额字段的基本要求千万别用 double 或 float否则累计计算时会出现精度偏差。view_count 配合更新时间可以做简单的热门排序。2.3 订单模块一个简洁但完整的交易状态流订单表连接买卖双方也连接商品。这个项目不需要购物车和退款流程所以订单状态可以走精简路线但几个关键状态不能少。CREATE TABLE orders ( id int NOT NULL AUTO_INCREMENT COMMENT 订单ID, order_no varchar(32) NOT NULL COMMENT 订单编号全局唯一, goods_id int NOT NULL COMMENT 商品ID, seller_id int NOT NULL COMMENT 卖家ID冗余存储, buyer_id int NOT NULL COMMENT 买家ID, price decimal(10,2) NOT NULL COMMENT 成交价, status tinyint NOT NULL DEFAULT 0 COMMENT 状态0待付款 1已付款 2已发货 3已确认 4已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间, pay_time datetime DEFAULT NULL COMMENT 付款时间, finish_time datetime DEFAULT NULL COMMENT 完成时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_buyer_id (buyer_id), KEY idx_goods_id (goods_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;order_no 用唯一索引约束生成方式常见的是「时间戳 随机数」或者 UUID 去掉横线要保证并发下不重复。seller_id 单独存一份而不是通过商品表关联是为了防止商品被删除或下架后订单依然能还原交易双方的完整信息这是一个可以在答辩中主动说出来的设计理由。订单状态从 0 到 4 依次流转只有状态 0 可以取消已付款之后需要经过发货、确认收货才能结束这套流程足够支撑现场演示。2.4 辅助表收藏与留言撑起互动场景除了三张主表项目里通常还带着收藏表和留言表作用是让「用户之间能互动」这个需求有地方落。收藏表核心字段是 user_id、goods_id 和一个创建时间加一个联合唯一索引防止重复收藏。留言表则需要记录留言内容、发送者、接收者和对应商品展示在商品详情页的下方。CREATE TABLE favorite ( id int NOT NULL AUTO_INCREMENT, user_id int NOT NULL COMMENT 用户ID, goods_id int NOT NULL COMMENT 商品ID, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_goods (user_id, goods_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT收藏表;收藏表这个联合唯一索引 uk_user_goods 很关键。如果没有它同一个人可以反复收藏同一件商品绕过页面直接调接口就会重复插入。有了唯一索引重复收藏时数据库直接拒绝代码里捕获一下异常即可。几组表之间的关系不复杂商品表通过 user_id 关联用户表订单表通过 goods_id、seller_id、buyer_id 关联商品表和用户表收藏与留言都是用户对商品的多对一关系。看源码时对照 E-R 图去理解Mapper 里的 SQL 就不会觉得乱。字段类型上这个项目用 tinyint 表示状态、decimal 表示金额、datetime 表示时间都是 Java 后端项目里比较稳妥的选型。3. Spring Boot 后端工程落地分层架构与登录鉴权实现数据库脚本看完接着过一遍后端代码的结构。这个项目用的是标准的 Spring Boot 分层架构controller 接请求、service 处理业务、mapper 操作数据库、entity 对应表结构。分层不是为了好看而是为了改东西的时候知道该去哪个目录下找。3.1 项目目录结构与分层约定拿到源码先别急着点运行把目录结构过一遍再动手。这个项目的包结构通常长这样src/main/java/com/example/campusmarket/ ├── controller/ # 接收前端请求只做参数接收和结果返回 │ ├── UserController.java │ ├── GoodsController.java │ └── OrderController.java ├── service/ # 业务逻辑层事务注解加在这一层 │ ├── UserService.java │ ├── GoodsService.java │ └── OrderService.java ├── mapper/ # MyBatis-Plus 的 Mapper 接口继承 BaseMapper │ ├── UserMapper.java │ ├── GoodsMapper.java │ └── OrderMapper.java ├── entity/ # 实体类字段与数据库表一一对应 │ ├── User.java │ ├── Goods.java │ └── Order.java ├── common/ # 统一返回结果、异常处理、工具类 │ ├── Result.java │ └── JwtUtil.java └── config/ # 配置类跨域、拦截器、静态资源映射 ├── WebConfig.java └── InterceptorConfig.javacontroller 里只做三件事接收参数、调用 service、包装返回结果不要在里面写 SQL 或复杂业务判断。service 层是业务逻辑的核心Transactional 事务注解加在这一层的方法上。mapper 层用 MyBatis-Plus 的话绝大多数方法只需要继承 BaseMapper 就能拿到单表增删改查复杂查询再在接口里声明方法并配 XML 或注解 SQL。entity 类的字段命名用驼峰MyBatis-Plus 默认开启驼峰转下划线所以studentNo会自动映射到student_no。common 里的 Result 是统一返回体常见格式是 code、message、data 三个字段前端根据 code 判断成功失败。3.2 基于 JWT 的登录鉴权流程登录鉴权是这个项目里技术含量较高的部分。平台有两种角色——普通用户和管理员不能把所有接口都放开也不能每次请求都去数据库查密码。这个项目采用 JWT 方案登录成功后后端签发一个 token前端后续请求带上它后端拦截器校验通过就放行。// controller/UserController.java 登录接口 PostMapping(/login) public Result login(RequestBody User loginUser) { // 1.按用户名查库 User user userService.getByUsername(loginUser.getUsername()); if (user null) { return Result.error(用户名不存在); } // 2.密码比对先做 MD5 再比较 String md5Pwd DigestUtils.md5DigestAsHex( loginUser.getPassword().getBytes() ); if (!md5Pwd.equals(user.getPassword())) { return Result.error(密码错误); } // 3.生成 token保存用户ID和角色 String token JwtUtil.createToken(user.getId(), user.getRole()); return Result.success(token); }这段登录代码有三个细节。密码比对不是拿前端传来的明文直接和数据库比而是先做一次 MD5 再比因为数据库里存的就是密文。JwtUtil.createToken 会传入用户 ID 和角色生成带过期时间的 token常见做法是设置 24 小时或 7 天有效期。loginUser 的密码字段在真正项目里要先判空再继续否则空指针会直接 500这种细节在排查接口报错时经常遇到。拦截器校验是鉴权的另一半它决定哪些请求需要登录、哪些可以匿名访问。// config/InterceptorConfig.java Configuration public class InterceptorConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JwtInterceptor()) .addPathPatterns(/**) // 拦截所有请求 .excludePathPatterns( /api/user/login, // 登录放行 /api/user/register, // 注册放行 /api/goods/list, // 商品列表放行 /api/goods/detail/**, // 商品详情放行 /file/** // 静态图片放行 ); } }拦截器配置的放行路径需要逐条核对。如果漏放了商品详情路径游客就看不到商品详情页如果多放了需要身份的接口比如发布商品任何人都能绕过登录直接发商品。项目里的放行清单不一定和上面完全一致但原则一样只放行不需要登录身份的接口涉及用户身份的操作一律拦截。3.3 商品发布接口参数校验与状态流转商品发布是卖家端最核心的接口它的实现能体现对 MyBatis-Plus 的使用熟练度。// service/GoodsService.java 新增商品 public Result addGoods(Goods goods, Integer userId) { // 1.基础校验 if (goods.getTitle() null || goods.getTitle().trim().isEmpty()) { return Result.error(商品标题不能为空); } if (goods.getPrice() null || goods.getPrice().compareTo(BigDecimal.ZERO) 0) { return Result.error(商品价格必须大于0); } // 2.补充卖家信息强制使用登录用户ID不允许前端传 goods.setUserId(userId); goods.setStatus(0); // 待审核 goods.setViewCount(0); // 3.调用 MyBatis-Plus 内置 insert 方法 goodsMapper.insert(goods); return Result.success(发布成功等待管理员审核); }这里有一个答辩时经常被追问的点为什么商品的 userId 不从前端传因为前端传的用户 ID 可被篡改攻击者把 user_id 改成别人的 ID商品就归到别人名下了。正确做法是从 token 里解析出当前登录用户的 ID后端强制覆盖。价格校验用 BigDecimal 的 compareTo 而不是直接大于号避免 null 导致的空指针也是金额比较的正确姿势。insert 是 MyBatis-Plus 自带的方法不需要手写 SQL这一点在讲代码时可以主动提一句。3.4 文件上传与图片访问路径配置商品图片上传是这个项目里影响体验但容易被忽视的功能。常见做法是用 MultipartFile 接收文件保存到服务端某个目录同时把访问路径写进数据库。// controller/FileController.java 图片上传 PostMapping(/upload) public Result upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(请选择文件); } // 1.校验文件后缀只允许常见图片格式 String originalName file.getOriginalFilename(); String suffix originalName.substring( originalName.lastIndexOf(.) ); ListString allowSuffix Arrays.asList( .jpg, .jpeg, .png, .gif ); if (!allowSuffix.contains(suffix.toLowerCase())) { return Result.error(不支持的图片格式); } // 2.生成新文件名避免重名覆盖 String fileName System.currentTimeMillis() _ originalName; // 3.保存到配置的本地目录 String savePath uploadDir / fileName; file.transferTo(new File(savePath)); // 4.返回访问路径 return Result.success(/file/ fileName); }文件名用时间戳拼接原始名能避免多人上传同名文件互相覆盖。uploadDir 变量对应 application.yml 里配置的上传目录不能写死。返回给前端的是访问路径 /file/xxx而不是本地磁盘路径这个映射关系由 WebConfig 的静态资源映射完成。如果返回了本地路径或者映射配错前端图片就会加载不出来。# application.yml 中相关配置 file: upload-dir: D:/campus-market/upload// config/WebConfig.java 静态资源映射 Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/file/**) .addResourceLocations(file:D:/campus-market/upload/); }这两段要配合看。upload-dir 表示文件实际写到哪个目录addResourceHandler 表示请求 /file/xxx 时去哪个目录找。配置里的绝对路径要改成自己机器的实际路径用相对路径也能跑但部署到服务器时容易因当前目录不同而找不到文件。这是一个典型的配置与环境耦合的问题改完配置一定要重启并实测上传一次。4. 数据库脚本与初始化数据一键跑起来的临门一脚后端代码再完整数据库连不上也白搭。这个项目自带的 SQL 脚本里包含建库、建表、初始化数据三个部分。其中初始化数据是很多同学容易忽略的地方——没有测试数据登录、列表、详情页看起来都像黑匣子演示效果会大打折扣。4.1 数据库脚本结构与初始化数据打开 SQL 脚本一般能看到这样的结构-- 1.建库 CREATE DATABASE IF NOT EXISTS campus_market DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE campus_market; -- 2.建表内容同上一章这里省略 -- 3.初始化管理员账号 INSERT INTO user (username, password, nickname, role, status) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, 管理员, 2, 1); -- e10adc3949ba59abbe56e057f20f883e 是 123456 的 MD5管理员账号的初始化记录非常关键没有它你就进不了管理端。默认账号通常是 admin密码 123456这串 e10adc3949ba59abbe56e057f20f883e 就是 123456 的 MD5 值。后续的初始化数据还包括几个测试用户、十几件测试商品和少量订单记录让商品列表页不至于一片空白。-- 初始化几条测试商品 INSERT INTO goods (user_id, title, description, price, original_price, category, status, view_count) VALUES (1, 高等数学 第七版 上册, 九成新扉页有名字不影响阅读, 15.00, 42.00, 教材书籍, 1, 126), (2, 罗技无线鼠标 M275, 去年双十一买的接收器还在, 25.00, 79.00, 数码产品, 1, 233), (3, 宿舍USB台灯, 三档调光毕业离校出, 12.00, 39.00, 生活用品, 1, 87);测试数据的价值主要有三点。第一列表页和搜索功能演示时不是空页面第一印象就好。第二多商品数据能顺手验证关键词搜索、分类筛选这些 SQL 写得对不对。第三订单数据能让管理端的统计有值得展示的数字。我见过有的项目把初始化数据砍到只剩一条结果分页和搜索完全没法演示临到答辩才发现来不及补。status 为 1 表示在售如果想要管理端有待审核的数据可以再插两条 status 为 0 的商品。4.2 application.yml 里的连接配置与参数数据库脚本准备好之后Spring Boot 要能连上它靠的是 application.yml 里的数据源配置。spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/campus_market?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 servlet: multipart: max-file-size: 5MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里每一行都有讲究。url 里的 characterEncodingutf8 确保中文不乱码serverTimezoneAsia/Shanghai 避免 MySQL 8 时报时区错误useSSLfalse 和 allowPublicKeyRetrievaltrue 是 MySQL 8.0 连接时常见的要求。driver-class-name 用 com.mysql.cj.jdbc.Driver在 MySQL 5.7 和 8.x 下都可以用。MyBatis-Plus 的驼峰映射默认开启但显式写出来更清晰。multipart 限制 5MB前端如果限制了 2MB这里配 5MB 就够用。逻辑删除相关的配置要留意如果表结构里没有 deleted 字段这里配置了也不会报错但会存在隐患最好对照实体类检查一遍。4.3 数据库版本与字符集的兼容问题SQL 脚本在低版本 MySQL 机器上运行大概率没问题但在 MySQL 8.x 上有时会碰到排序规则不匹配的情况。常见原因有两种一是表定义里写了 utf8mb4_general_ci而数据库默认排序规则是 utf8mb4_0900_ai_ci表和库的排序规则不一致二是 CREATE TABLE 语句里用了当前 MySQL 实例不支持的字符集或语法。解决方式是把建库语句里的 COLLATE 显式指定成与 MySQL 实例一致的排序规则或者先建好对应排序规则的库再导入表。数据库时区也是一个隐藏坑。如果服务器或本地 MySQL 设置的时区与中国时区不一致而连接串里又没有 serverTimezoneAsia/Shanghai查询出来的时间会比实际快或慢 8 小时。订单列表里的下单时间一旦对不上演示时被追问「为什么时间不对」就很被动。serverTimezone 参数加上之后还要确认 MySQL 实例本身的时区设置两步都对了才稳妥。还有一个小概率但很影响体验的事MySQL 8 默认的 caching_sha2_password 认证插件在旧版驱动下会报 Public Key Retrieval is not allowed。连接串里加上 allowPublicKeyRetrievaltrue 就能解决新版驱动基本不会遇到但如果你照着网上的老配置抄就容易撞上。5. 避坑与常见问题跑通这套源码最容易踩的五个坑这一章写五条我在跑类似 Spring Boot 毕设项目时真实遇到过、而且大概率会踩的坑。每一条都按「现象 → 原因 → 解决」三步讲清楚照着排查基本能定位。5.1 跨域配置失效接口通了但浏览器一直报 CORS现象前端页面单独用 8081 端口跑后端在 8080登录请求发出去浏览器控制台报跨域错误接口根本没到 controller。原因项目里 WebConfig 虽然实现了 WebMvcConfigurer 并加了跨域映射但自定义拦截器先于跨域处理执行。请求被拦截器挡掉CORS 响应头没加上浏览器就判定跨域失败。解决在拦截器的 preHandle 里先判断请求是否为 OPTIONS 请求放行预检请求再走正常校验。// JwtInterceptor.java 中的 preHandle public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 放行 OPTIONS 预检请求否则跨域配置会被拦截器挡住 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } // 正常 token 校验逻辑 String token request.getHeader(token); if (token null || !JwtUtil.verify(token)) { // 返回未登录提示 return false; } return true; }这个坑的典型性在于原因很明确跨域预检请求先飞过来探路后端拦截器先把它挡了真正处理跨域的过滤器根本没机会工作。以后只要是「加了 CrossOrigin 或跨域配置仍然 CORS 报错」的项目第一反应就查拦截器。5.2 列表正常但详情数据错乱大整数精度丢失现象商品列表接口返回数据正常点击进入详情页时前端拿到的 ID 和列表里显示的不一样或者根据 ID 查不到对应商品。原因如果主键用的是 MyBatis-Plus 默认的雪花 ID很长的 Long 型数字前端 JavaScript 的 Number 类型超过 2^53 时会丢失精度。后端返回 139 开头的一长串数字到前端变成结尾全是 0 的值再拿这个变了的值去查详情自然查不到。解决在主键字段上增加字符串序列化注解让后端输出为字符串。// entity/Goods.java public class Goods { // 使用字符串序列化避免前端 JS 精度丢失 JsonFormat(shape JsonFormat.Shape.STRING) private Long id; }这行注解改动很小效果立竿见影。如果项目里除了主键还有其他 Long 类型的字段比如雪花订单号也要一并处理。用自增 int 主键的项目不会遇到这个问题但一旦升级为雪花 ID 就必然踩值得记住这个解法。5.3 图片上传提示成功页面却显示不出图片现象上传接口返回成功数据库也写入了路径但 img 标签的 src 打不开浏览器访问该路径返回 404。原因上传保存的本地目录和静态资源映射的目录不一致。比如保存路径配置的是 D:/campus-market/upload而 WebConfig 里 addResourceLocations 写的是 D:/campus-market/image两边对不上请求 /file/xx 时后端找不到文件。解决把两个路径统一到同一个绝对目录。最稳妥的做法是把目录路径写进 application.yml通过 Value 注入给保存逻辑和静态资源映射共同使用。// config/WebConfig.java Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/file/**) .addResourceLocations(file: uploadDir /); }修改后记得重启服务并重新上传一张图验证。这个问题的隐蔽性在于接口不报错只有图片显示不出来排查时容易在代码层面兜圈子而实际上只是两处配置没对齐。5.4 启动直接报错Spring Boot 或 JDK 版本不兼容现象按说明配好环境和数据库启动 main 方法直接报错或者刚启动几秒就崩溃日志里的错误各不相同。原因项目里的 Spring Boot 版本和你本地的 JDK 不兼容。比如 Spring Boot 2.x 配 JDK 17 在个别版本上会报 module 相关错误或者 Lombok 版本过低不支持当前 JDK 的编译方式。解决先看 pom.xml 里 Spring Boot parent 的版本再对照本地 JDK 版本。如果打包阶段报 Lombok 相关错误把 Lombok 升级到和 JDK 匹配的版本同时检查 IDEA 里是否开启了注解处理。这个坑我的血泪经验是不要急于升级 Spring Boot 版本。毕业设计项目的依赖版本是作者保证过的组合原样跑通最稳。改版本前先备份改完至少走一遍「登录 → 发商品 → 下单」主链路不要只看到启动成功就收工。5.5 修改后端端口后前端全部请求失败现象后端默认端口 8080 被占用把 server.port 改成 8081重新启动前端页面所有接口都请求失败。原因前端项目里写死了后端地址比如 http://localhost:8080/api/xxx。后端改了端口前端没跟着改自然全部失敗。解决检查前端 api 配置文件把基础地址改成新端口。更稳妥的做法是把基础地址提到独立配置文件里改一处即可。常见做法是把 /api 前缀做成全局常量或直接在 axios 实例里配置 baseURL。如果你用的是项目自带的静态页面里面大概率有固定写死的路径搜索 8080 就能找到所有需要改的位置。改完之后用浏览器开发者工具看网络请求标签页确认实际请求的地址是不是新端口。6. 从「能跑」到「答辩加分」三个值得动手改一改的进阶点如果你的时间允许在保证主链路能跑通的前提下可以从下面三个方向里挑一个做改进。难度由低到高改动量都不大答辩时容易讲清楚。第一个是给商品列表加 Redis 缓存。商品列表是访问频率最高的接口每次都查数据库有点浪费。在 service 层加一个缓存逻辑查询时先查 Redis命中直接返回未命中查数据库再把结果写入缓存并设置失效时间。// GoodsService.java 加缓存后的列表查询 public Result getGoodsList(int page, int size) { String cacheKey goods:list: page : size; Object cached redisTemplate.opsForValue().get(cacheKey); if (cached ! null) { return Result.success(cached); } // 缓存未命中查数据库 IPageGoods goodsPage goodsMapper.selectPage( new Page(page, size), new QueryWrapperGoods().eq(status, 1) ); redisTemplate.opsForValue().set( cacheKey, goodsPage, 5, TimeUnit.MINUTES ); return Result.success(goodsPage); }这里有两个注意点缓存 key 要把分页参数带上否则第二页拿到的是第一页的数据设置失效时间而不是永久缓存因为商品数据会变化5 分钟过期是一个折中方案。Redis 不是必选项但如果你在简历里写「用 Redis 做了热点缓存」面试官通常是认可的。第二个是在订单状态变化时加站内消息。订单从下单到确认收货状态变化有好几次全让买家自己刷新页面看体验不好。做法是加一张 message 表在订单状态变更的 service 方法里插入一条消息记录用户端做一个「消息中心」入口显示未读数即可。不需要引入消息队列一个表加一个查询接口就够。第三个是给管理端加一个销售统计接口。管理端只有审核功能的话答辩展示会显得单薄。加一个统计接口一条 SQL 返回今日订单数、总销售额、在售商品数、待审核商品数前端用卡片或柱状图展示。这个改动涉及的知识点不多但能让你在讲解管理端时多一个业务亮点。从那以后我每次拿到一个 Spring Boot 毕设项目都会先跑通数据库脚本确认登录、发商品、下单这条主链路没有问题了再考虑往哪个方向加东西。改完一个功能就实测一次不要攒到最后一起验证。希望帮到你。本文还有配套的精品资源点击获取