news 2026/10/27 7:48:25

Spring Boot校园二手市场平台:表结构设计与登录鉴权实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot校园二手市场平台:表结构设计与登录鉴权实战

简介: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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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.java

controller 里只做三件事:接收参数、调用 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(".") ); List<String> 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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true 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 里的 characterEncoding=utf8 确保中文不乱码,serverTimezone=Asia/Shanghai 避免 MySQL 8 时报时区错误,useSSL=false 和 allowPublicKeyRetrieval=true 是 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 设置的时区与中国时区不一致,而连接串里又没有 serverTimezone=Asia/Shanghai,查询出来的时间会比实际快或慢 8 小时。订单列表里的下单时间一旦对不上,演示时被追问「为什么时间不对」就很被动。serverTimezone 参数加上之后,还要确认 MySQL 实例本身的时区设置,两步都对了才稳妥。

还有一个小概率但很影响体验的事:MySQL 8 默认的 caching_sha2_password 认证插件在旧版驱动下会报 Public Key Retrieval is not allowed。连接串里加上 allowPublicKeyRetrieval=true 就能解决,新版驱动基本不会遇到,但如果你照着网上的老配置抄,就容易撞上。

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); } // 缓存未命中,查数据库 IPage<Goods> goodsPage = goodsMapper.selectPage( new Page<>(page, size), new QueryWrapper<Goods>().eq("status", 1) ); redisTemplate.opsForValue().set( cacheKey, goodsPage, 5, TimeUnit.MINUTES ); return Result.success(goodsPage); }

这里有两个注意点:缓存 key 要把分页参数带上,否则第二页拿到的是第一页的数据;设置失效时间而不是永久缓存,因为商品数据会变化,5 分钟过期是一个折中方案。Redis 不是必选项,但如果你在简历里写「用 Redis 做了热点缓存」,面试官通常是认可的。

第二个是在订单状态变化时加站内消息。订单从下单到确认收货,状态变化有好几次,全让买家自己刷新页面看体验不好。做法是加一张 message 表,在订单状态变更的 service 方法里插入一条消息记录,用户端做一个「消息中心」入口,显示未读数即可。不需要引入消息队列,一个表加一个查询接口就够。

第三个是给管理端加一个销售统计接口。管理端只有审核功能的话,答辩展示会显得单薄。加一个统计接口,一条 SQL 返回今日订单数、总销售额、在售商品数、待审核商品数,前端用卡片或柱状图展示。这个改动涉及的知识点不多,但能让你在讲解管理端时多一个业务亮点。

从那以后我每次拿到一个 Spring Boot 毕设项目,都会先跑通数据库脚本,确认登录、发商品、下单这条主链路没有问题了,再考虑往哪个方向加东西。改完一个功能就实测一次,不要攒到最后一起验证。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 7:47:43

回调函数:从基础概念到现代编程实践

1. 回调的本质&#xff1a;从生活场景到编程范式第一次接触"回调"这个概念时&#xff0c;我正盯着一段前端代码发愣。那是个简单的按钮点击事件处理&#xff0c;同事写了行button.addEventListener(click, () > { ... })。箭头函数里的逻辑我能看懂&#xff0c;但…

作者头像 李华
网站建设 2026/10/27 7:47:04

基于C#的编组站调车系统:从钩计划到现车台账的信息化实践

简介&#xff1a;基于C#的编组站调车系统是一份面向铁路运输信息化与C#开发学习者的完整项目资源&#xff0c;适合用于理解编组站调车业务流程及Windows窗体程序的设计方法。资源包含完整Visual Studio工程&#xff0c;共47个文件&#xff0c;以16个.cs源码文件为核心&#xff…

作者头像 李华
网站建设 2026/10/27 7:46:54

LVS负载均衡架构与DR模式部署实战

1. LVS&#xff08;Linux Virtual Server&#xff09;核心架构解析LVS是构建在Linux内核上的负载均衡解决方案&#xff0c;其核心架构由三部分组成&#xff1a;负载调度器&#xff08;Load Balancer&#xff09;、服务器池&#xff08;Server Pool&#xff09;和后端存储&#…

作者头像 李华
网站建设 2026/10/27 7:46:53

Serverless与Node.js实战:高可用架构与性能优化

1. 为什么选择Serverless与Node.js组合&#xff1f;三年前我第一次接触Serverless架构时&#xff0c;被它"按需付费"的特性所吸引。当时我们团队正在开发一个电商促销活动页面&#xff0c;流量高峰和低谷相差近百倍。传统服务器要么在高峰期崩溃&#xff0c;要么在平…

作者头像 李华
网站建设 2026/10/27 7:46:48

Harness Engineering实战:五步流水线让AI编程稳定交付

1. Harness Engineering 到底是什么&#xff0c;值得花一篇文章讲清楚先聊个大家都有过的经历。第一次用 AI 写代码&#xff0c;很多人是惊艳的&#xff1a;让它写个排序函数、生成一段正则、补个单元测试&#xff0c;又快又准&#xff0c;感觉“编程要被终结了”。但等它去改一…

作者头像 李华
网站建设 2026/10/27 7:46:47

Java高并发支付网关架构设计与Netty实践

1. 为什么我们需要重新思考支付网关架构&#xff1f;支付网关作为金融交易的"高速公路"&#xff0c;其稳定性直接关系到企业的命脉。去年双十一期间&#xff0c;某头部电商平台的支付系统崩溃30分钟&#xff0c;直接损失超过2.4亿元——这个数字让我深刻意识到&#…

作者头像 李华