毕业设计选这个题的人每年都有不少,但真正能把“校园闲置物品租售管理系统”做出花来的没几个。Spring Boot作为当前后端开发最主流的框架,配合Vue或者Thymeleaf做前后端,再加MySQL存数据、Redis扛缓存、Minio存图片,这套组合基本就是校园类管理系统的标准配置了。我自己前前后后带过几届毕设小组,也独立做过类似的项目,今天把这些经验完整梳理一遍,从需求拆解、表结构设计、核心模块实现到最后的部署上线,每一步的坑和细节都摆出来。这篇内容主要面向正在做Spring Boot毕设的同学,也适合想独立完成一个完整项目来提升实战能力的后端初学者。
1. 需求拆解与整体设计思路
1.1 先从真实的校园场景理解业务
很多同学拿到这个题目,第一反应就是“做个商城”,然后把淘宝那套搬过来。这其实是认知上的偏差。校园闲置物品租售和普通电商有本质区别,它的人群固定(都是本校学生)、物品流通快(学期末集中爆发)、交易距离近(同校甚至同楼当面交付),而且它天然包含“租”和“售”两种完全不同的交易模式。
先说说“租”。校园里哪些东西适合租?单反相机、COS服、正装、教材、代步车,这类物品使用频率低、单价偏高,买新的不划算,闲置又占地方。毕业后一届人走了,下一届人接着用,这个流转逻辑在校园里非常成熟。再说“售”,毕业季的清仓甩卖、换季闲置的衣物、考研结束的复习资料,这些东西就是纯买卖,一手交钱一手交货。
所以在设计系统之前,你要先想清楚一件事:这个系统的核心用户是本校学生,核心价值是让闲置物品在同校熟人圈子里低成本流转。订单、支付、物流这些东西不是重点,信息展示的清晰度、沟通的便捷性、交易的信任感才是重点。
1.2 功能模块与数据库设计的合理取舍
基于上面的业务理解,我建议功能模块这样划分:
| 功能模块 | 核心功能 | 备注 |
|---|---|---|
| 用户模块 | 注册登录、个人信息、信用评价 | 学生认证是校园系统的关键 |
| 物品模块 | 发布闲置、浏览搜索、分类筛选、收藏 | 支持图片上传、多条件查询 |
| 订单模块 | 购买订单、租借订单、状态流转 | 租借需记录租期与押金 |
| 消息模块 | 私信沟通、催还提醒、系统通知 | 站内信即可,不需要实时聊天 |
| 管理后台 | 用户管理、物品审核、订单监管、数据统计 | 管理员独立端,权限分离 |
数据库设计方面,建议核心表控制在10张以内,表字段不要贪多。很多同学喜欢在一个表里塞几十个字段,后面自己写CRUD都分不清。合理的做法是字段宁少勿多、能拆就拆。比如用户表就放基础信息和信用分,地址、学校、学号这种如果不需要就砍掉;物品表把图片单独拆一张表也好,用逗号拼接字符串也罢,只要你能自圆其说,答辩时逻辑能讲通就行。
这是我认为最稳妥的一套核心表结构:
-- 用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50), avatar VARCHAR(255), phone VARCHAR(20), credit_score INT DEFAULT 100, -- 信用分 status TINYINT DEFAULT 1, -- 1正常 0禁用 create_time DATETIME ); -- 闲置物品表 CREATE TABLE idle_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, -- 发布人 title VARCHAR(100) NOT NULL, description TEXT, category_id BIGINT, price DECIMAL(10,2) NOT NULL, -- 售价 rent_price DECIMAL(10,2), -- 租金,为空表示不可租 deposit DECIMAL(10,2), -- 押金 images VARCHAR(1000), -- 图片URL,逗号分隔 status TINYINT DEFAULT 1, -- 1在售/可租 2已下架 3已售出/已出租 4审核中 view_count INT DEFAULT 0, create_time DATETIME ); -- 订单表 CREATE TABLE trade_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, item_id BIGINT NOT NULL, seller_id BIGINT NOT NULL, buyer_id BIGINT NOT NULL, order_type TINYINT, -- 1购买 2租借 amount DECIMAL(10,2), -- 实付金额 deposit DECIMAL(10,2), -- 租借押金 rent_days INT, -- 租期 status TINYINT, -- 状态机见下文 create_time DATETIME );这套表设计的核心思路是订单不冗余快照。这里我特意说明一下,商城的订单一般会把商品名称、价格、图片冗余一份进来,防止商家改价后订单数据不对。但在校园项目里,物品状态相对稳定,而且你查订单时大概率要关联查询物品信息,冗余反而容易造成数据不一致。毕业设计答辩时,老师问“为什么订单表不冗余商品快照”,你回答“校园场景物品信息变动少,冗余成本大于收益”,这个答案是完全立得住的。
1.3 租售一体的混合模式怎么设计才不翻车
这是整个系统设计的灵魂,也是最容易做崩的地方。很多人贪图省事,把“租”和“售”做成两个模块,各写各的。但实际业务中,一个物品往往既能出售也能出租,比如一台相机,别人可以买走,也可以租三天。如果你拆成两个物品来处理,库存管理和数据一致性就是灾难。
我的做法是:物品表和下单逻辑统一,通过订单类型字段区分租和售。
idle_item.price是出售价,rent_price是每天的租金,deposit是押金- 用户下单时选择“购买”或“租借”,生成不同type的订单
- 物品状态根据订单类型进入不同的流转分支
这里有个业务细节需要注意:当一个物品同时具备“可买”和“可租”属性时,要怎么处理?我推荐一个比较简单的方案——用status字段限定:
- 1 = 在售(可买不可租)
- 2 = 在租(可租不可买)
- 3 = 在售且可租
- 4 = 已下架
这样查询列表时,购买入口判断price != null且status in (1,3),租借入口判断rent_price != null且status in (2,3),逻辑清晰,也不容易出状态错乱的问题。
还有单物品同时被两个人租借的问题,尤其是每一届做这个题目的同学都会碰到。我的处理是:同一时间一个物品只能有一个人持有,所以租借订单完成之前,物品状态必须立即从“可租”切到“已出租”。这要求你在用户点击“立即租借”提交订单时,用UPDATE ... WHERE status IN (2,3)做条件更新,更新影响行数为0直接提示“手慢了,物品已被租走”。这种乐观锁思路在校园项目里完全够用,不用引入复杂的分布式锁。
2. 技术选型与核心组件整合
2.1 Spring Boot版本别盲目追新
项目构建是第一步,也是最容易卡住的地方。现在很多教程上来就让你用Spring Boot 3.x,如果你是跟着网上的老教程做,十有八九会栽在配置上。Spring Boot 3.x 的基线是 Java 17,很多老版本依赖库没跟上,比如 MyBatis 和 Druid 的某些版本就直接用不了。
我做这个项目时选的是Spring Boot 2.7.x + Java 8。选择理由非常现实:
- 大量教程、开源项目都基于这套组合,遇到问题搜索引擎能直接给出答案
- 2.7.x 是 2.x 的最后一个稳定主线,功能完整且坑最少
- 兼容性强,无论你本机装的是 JDK 8 还是 JDK 11 都没问题
如果你确实想用 3.x,那我建议你同时把 JDK 升到 17,然后把依赖版本全部换上支持 Jakarta 的新版本。尤其是javax.servlet到jakarta.servlet的包名变化,很多老代码会报ClassNotFoundException,不要被这种环境问题耽误了核心逻辑的时间。
在 IDEA 里创建项目的路径是:New Project → Spring Initializr → 选好 JDK 和 Spring Boot 版本 → 勾选 Web、MyBatis、MySQL Driver 等依赖。Maven 构建的话,项目根目录下执行mvn clean install即可,IDEA 右侧 Maven 面板里的package双击也是同样效果。
番茄 说句实在话,用 2.7 + JDK 8 这个组合做完整个项目,对绝大部分毕设场景已经完全够用了。你要是将来工作去了新项目组,自然会接触到 3.x 甚至更新的版本,那是另一套方法论。
2.2 数据访问层选 MyBatis-Plus 而不是裸 MyBatis
现在做这类项目,我强烈推荐 MyBatis-Plus,它不是替代 MyBatis,而是在 MyBatis 之上做了一套增强工具。核心价值是:单表CRUD不用写XML。用户表的增删改查、分页查询、条件构造器,它内置的方法全都覆盖了。
举个例子,你要查“价格在100-200之间、状态在售、按浏览量倒序”的物品列表,原生 MyBatis 你要写动态SQL,但 MyBatis-Plus 的 LambdaQueryWrapper 几行搞定:
List<IdleItem> items = idleItemMapper.selectList( new LambdaQueryWrapper<IdleItem>() .between(IdleItem::getPrice, 100, 200) .in(IdleItem::getStatus, 1, 3) .orderByDesc(IdleItem::getViewCount) );分页插件也简单,配置一个MybatisPlusInterceptor就行:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(50L); interceptor.addInnerInterceptor(pagination); return interceptor; } }这里有个细节:maxLimit一定要设置。如果不设上限,前端传一个pageSize=999999,一次查询就能把全表拉出来,数据量大了之后服务直接卡死,这也是一个安全漏洞。
2.3 图片存储:为什么必须把 Minio 整合进来
校园闲置物品最核心的展示就是图片,没有图片的物品基本无人问津。那图片存在哪?最常见的错误做法是把图片丢到项目 static 目录,或者用本地磁盘路径。这个方案的致命缺陷是:重启或者重新部署时文件就丢了,而且前后端分离部署时,前端服务器根本访问不到后端机器的本地路径。
正确解法是引入 Minio。Minio 是一个开源的对象存储服务,兼容 S3 协议,部署非常简单,一个 Docker 命令就能跑起来。核心思路是:图片等二进制文件统一上传到 Minio,数据库里只存访问 URL,应用服务器不保存任何文件。
Minio 的部署方式:
docker run -d --name minio \ -p 9000:9000 -p 9001:9001 \ -e MINIO_ROOT_USER=admin \ -e MINIO_ROOT_PASSWORD=admin123456 \ -v /data/minio:/data \ minio/minio server /data --console-address ":9001"然后在 Spring Boot 里配置连接信息:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 50MB minio: endpoint: http://localhost:9000 access-key: admin secret-key: admin123456 bucket-name: idle-items关键代码是上传逻辑,有一个容易踩的坑:获取文件后缀时不能直接用getOriginalFilename()去截取。有些浏览器上传的文件名是带路径的,比如C:\Users\test\photo.jpg,你直接截后缀很可能得到photo.jpg这种完整文件名,拼接出来的对象名都是乱的。正确做法是用工具类从 MIME 类型或者原始文件名里剥离出真正的纯后缀。
@Service public class FileStorageService { @Autowired private MinioClient minioClient; @Value("${minio.bucket-name}") private String bucketName; public String upload(MultipartFile file) { // 先确保桶存在 try { boolean exists = minioClient.bucketExists( BucketExistsArgs.builder().bucket(bucketName).build()); if (!exists) { minioClient.makeBucket( MakeBucketArgs.builder().bucket(bucketName).build()); } } catch (Exception e) { throw new RuntimeException("初始化存储桶失败"); } String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".") + 1); String objectName = System.currentTimeMillis() + "_" + UUID.randomUUID().toString().substring(0, 8) + "." + ext; try { minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); } catch (Exception e) { throw new RuntimeException("文件上传失败", e); } return "/" + bucketName + "/" + objectName; } }注意返回值,我存的是相对路径而不是完整的http://ip:9000开头的地址。这么做的原因很简单:你本地开发时 IP 是localhost,打包部署之后要换成服务器 IP,如果你把完整地址存进数据库,换环境就是一场灾难。存相对路径,前端展示时拼上 Minio 的实际访问前缀就行,一条配置文件解决所有问题。
2.4 Redis + JWT 做登录会话,而不是 Session
校园系统也是多用户系统,用户的登录状态管理是绕不开的问题。用 Session 虽然简单,但有跨域和集群部署的隐患。我建议用JWT + Redis的方案:JWT 存用户身份信息,Redis 管 token 的过期和注销。
登录流程大致是这样:
- 用户提交用户名密码,后端校验通过后生成 JWT token
- 把 token 存入 Redis,key 是
login:token:{userId},value 是 token,过期时间设为 2 小时 - 前端每次请求把 token 放到请求头
Authorization里 - 后端写一个拦截器,从请求头取 token,解析出 userId,查 Redis 对比是否一致
- 用户退出时删除 Redis 中的 key,token 立即失效
Redis 在这里的核心作用是解决“JWT 本身无法主动失效”的问题。JWT 是无状态的,签发之后在有效期内永远有效,万一用户退出或者被盗你没法让 token 作废。加一层 Redis 之后,服务端就能主动控制会话状态了。
拦截器里的一段核心代码:
@Component public class LoginInterceptor implements HandlerInterceptor { @Autowired private StringRedisTemplate redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(401); return false; } token = token.substring(7); // 解析JWT得到userId Long userId = JwtUtil.parseToken(token); if (userId == null) { response.setStatus(401); return false; } // 校验Redis里的token是否一致 String redisKey = "login:token:" + userId; String cachedToken = redisTemplate.opsForValue().get(redisKey); if (cachedToken == null || !cachedToken.equals(token)) { response.setStatus(401); return false; } // 把userId放入request上下文,后续Controller直接用 request.setAttribute("userId", userId); return true; } }这里的小技巧是:让 Redis 里的 key 包含 userId,这样踢人、续期都方便。如果你把 token 作为 key,想知道“这个用户当前有哪些会话”就非常困难,管理端想要强制下线某个用户也做不到。
3. 核心业务模块的实现细节
3.1 用户注册登录与校园身份验证
用户模块看似简单,但有两个容易被问倒的点。第一个是密码加密,明文存库是毕设答辩中的大忌。用 BCrypt 加盐哈希,Spring Security 自带BCryptPasswordEncoder,或者你喜欢的话只用它的核心类不加整个 Spring Security 框架也行。加密逻辑很简单,注册时加密存入,登录时匹配校验。
第二个点是校园身份验证。很多同学不知道怎么设计这块,干脆不做,这其实是个减分项。不需要对接真实的学号库,最简单的做法是:注册时填学校、学号、姓名,管理员在后台审核通过后账号才能正常发布物品。这样既体现了系统的完整业务逻辑,又避免了对接外部接口的复杂度,答辩时完全说得通。
登录接口还有一个细节:失败次数限制。用 Redis 记一下login:fail:{username}的 PV,5次失败后锁定10分钟。别小看这个功能,它是面试官和高分答辩时非常喜欢的点,体现了你的安全意识。
// 登录失败时 String failKey = "login:fail:" + username; Long count = redisTemplate.opsForValue().increment(failKey); if (count != null && count == 1) { redisTemplate.expire(failKey, 10, TimeUnit.MINUTES); } if (count > 5) { throw new BusinessException("失败次数过多,请10分钟后再试"); }3.2 物品发布与状态流转的完整逻辑
物品发布是整个系统的重头戏。这部分有几个容易踩坑的地方:
图片上传要先于表单提交。很多新手把图片和表单一起提交,一个接口接收 multipart 文件,同时还要解析一堆文本字段,前端代码和后端代码都很难维护。正确的做法是分成两个接口:
POST /api/item/image先传图片,返回图片的相对路径POST /api/item提交物品信息,图片字段放的是第一步返回的URL列表
前端用一个chooseImage选择后立即上传,预览区展示缩略图,最后点“发布”时,把图片URL数组跟着其他字段一起提交。
物品状态不能只有“上架/下架”两个。我见过不少项目把物品状态做成布尔值,然后发现订单和物品状态纠缠不清,最后只能打补丁。建议用 int 类型,设计几个清晰的语义状态:
| 状态值 | 含义 | 触发条件 |
|---|---|---|
| 0 | 待审核 | 用户提交发布,等待管理员审核 |
| 1 | 在售 | 审核通过,可下单购买 |
| 2 | 在租 | 租借中,不可被再次下单 |
| 3 | 可租可售 | 两种交易都开放 |
| 4 | 已完成 | 已售出或租借归还后进入完成态 |
| 5 | 已下架 | 用户手动下架或管理员强制下架 |
发布接口的校验逻辑也值得写一下:标题长度限制(5-30字)、价格必须大于0、图片至少一张、描述不建议超过500字。这些限制不是憋出来的,而是为了避免数据垃圾。你想想,一个标题只有一个字的物品出现在列表里,其他用户点进去发现啥信息都没有,整个平台的体验都被拉低了。
3.3 交易订单的状态机设计
如果说物品模块是这个系统的手脚,那订单模块就是心脏。订单状态设计得好不好,直接决定你的系统是否专业。
购买订单的状态流转建议这样:
待付款 -> 待发货(可选) -> 待收货(可选) -> 已完成 | | | v v v 已取消 已取消 已取消校园场景里买卖双方基本都是校内当面交易,物流环节其实可以弱化甚至去掉。我做过的最简方案是:下单即锁定物品、支付(或约定线下支付)后进入“待交付”,双方确认后进入“已完成”。有些同学在系统里把电商那套完整物流状态都做了,最后发现根本没人用,增加了大量无效工作。
租借订单的状态流转更复杂一点:
待付款 -> 租借中 -> 待归还 -> 已完成(已归还) | | | v v v 已取消 逾期中 已申请退还押金这中间有两个核心业务逻辑需要代码实现:
第一个是租金计算。租期按天算,下单时计算总租金 = 每天租金 × 租期天数 + 押金。归还时如果逾期,按超出的天数补收租金。这个计算的精度要注意,BigDecimal是必须的,不能用double,否则金额会出现浮点数误差。
第二个是到期提醒。租借订单到期前1天,用定时任务扫描订单表,查出所有status=2且end_time在明天的订单,给买家发一条站内信提醒归还。Spring Boot 里直接用@Scheduled注解就行,记得在启动类上加@EnableScheduling。
@Component public class RentReminderJob { @Autowired private OrderMapper orderMapper; @Autowired private MessageService messageService; // 每天上午9点执行一次 @Scheduled(cron = "0 0 9 * * ?") public void remindExpiringRents() { LocalDate tomorrow = LocalDate.now().plusDays(1); List<TradeOrder> dueOrders = orderMapper.selectList( new LambdaQueryWrapper<TradeOrder>() .eq(TradeOrder::getStatus, 2) .eq(TradeOrder::getOrderType, 2) .apply("DATE(end_time) = {0}", tomorrow) ); for (TradeOrder order : dueOrders) { messageService.sendSystemMessage( order.getBuyerId(), "您租借的物品即将到期,请尽快归还" ); } } }3.4 管理后台:别只做花架子
管理后台是答辩时的加分项,但也最容易被做成“花架子”。很多同学后台就做几个列表页,管理员啥也干不了。我的建议是后台重点做这四件事:
物品审核:列表展示所有待审核物品,管理员可以一键通过或驳回,驳回时填写原因通知用户。这条流程让“校园管理”的元素落地了。
用户管理:列表展示所有用户,能搜索、能禁用。禁用后 Redis 里这个用户的 token 直接删掉,强制下线。
订单监管:查看所有订单,异常订单(比如投诉纠纷)管理员有权限把订单置为“已关闭”。
数据统计:这是最容易出彩的一块。用几张图表展示每日新增物品数、订单成交量TOP10分类、用户活跃度等。后端只需要提供几个统计接口,前端用 ECharts 画图。SQL 也不难,比如:
// 统计最近7天每天的订单数 SELECT DATE(create_time) AS day, COUNT(*) AS cnt FROM trade_order WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY DATE(create_time)后台的前端页面不需要多精美,布局清晰、操作流畅就够了。如果精力有限,后台直接用 Thymeleaf 做服务端渲染,减少一个前端的构建成本,也是一种合理取舍。
4. 从 IDEA 启动到 Docker 部署
4.1 IDEA 中的启动配置细节
开发阶段的启动配置是个小坑。很多同学从模板创建项目后不管端口,直接启动,结果发现 8080 端口被占用,或者两个服务同时跑在 8080 上报错。
在application.yml里明确指定端口是最稳妥的:
server: port: 8081 servlet: context-path: /api把context-path设为/api有一个好处:所有后端接口统一以/api开头,前端联调时代理配置只写一个前缀就行。而且将来如果要部署到 Nginx 反代,路径转发规则也简单。
IDEA 里的 Run Configuration 还要注意:Active profiles要选对。如果你有application-dev.yml和application-prod.yml两个环境的配置,本地启动时必须指定dev,不然可能加载了生产环境的数据库连接。这个问题看似低级,但我见过不止一个同学本地启动一直报数据库连接失败,折腾半天发现是 profile 选错了。
4.2 用 Docker 做一键部署
项目做完要部署演示,最省心的是 Docker。Dockerfile 写起来很简单,核心就几个步骤:构建 jar 包 → 拉一个 JDK 镜像 → 把 jar 放进去 → 指定启动命令。
FROM openjdk:8-jdk-alpine LABEL maintainer="yourname" WORKDIR /app COPY target/idle-market-0.0.1-SNAPSHOT.jar app.jar EXPOSE 8081 ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=prod"]这里有一个部署细节:MySQL、Redis、Minio 都要用能互通的内网地址。比如 MySQL 不能用localhost连接,因为 MySQL 跑在另一个容器里。最省事的方案是写一个docker-compose.yml,把所有服务编排在一起:
version: "3" services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: idle_market ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql redis: image: redis:7-alpine ports: - "6379:6379" minio: image: minio/minio environment: MINIO_ROOT_USER: admin MINIO_ROOT_PASSWORD: admin123456 command: server /data --console-address ":9001" ports: - "9000:9000" - "9001:9001" app: build: . depends_on: - mysql - redis - minio ports: - "8081:8081"注意depends_on只保证容器启动顺序,不保证 MySQL 已经初始化完成。这个没有完美的通用解,最简单的方法是在 Spring Boot 的启动类里加一个启动等待逻辑,或者用restart: always让应用启动失败后自动重启几次,通常等 MySQL 起来了自然就连接成功了。
4.3 前后端联调时的跨域与代理
如果你用 Vue 做前端,开发时的跨域问题是躲不过的。前后端分离架构下,前端跑在 5173(Vite 默认端口),后端跑在 8081,浏览器直接请求会报 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); } }前端开发环境用 Vite 代理:
// vite.config.js server: { proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } }产线部署时,更推荐 Nginx 反代统一入口,前端静态资源由 Nginx 托管,/api前缀的请求转发给后端服务,这样浏览器层面就没有跨域问题了。
5. 常见问题与排查技巧实录
做这个项目过程中,最耗时间的往往不是业务逻辑本身,而是各种环境问题和隐蔽 Bug。我挑几个最高频的写下来,这些坑几乎每个做 Spring Boot 毕设的人都会碰到。
问题1:Druid 连接池启动报错,检查 MySQL 时区设置
MySQL 8.x 默认时区和系统时区不一致,连接时会出现Server returns invalid timezone的报错。在连接 URL 上加参数解决:
jdbc:mysql://localhost:3306/idle_market?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true问题2:Maven 依赖下载缓慢或者拉取超时
国内网络环境下,Maven 中央仓库很不稳定。在settings.xml里配置阿里云镜像:
<mirror> <id>aliyun</id> <mirrorOf>central</mirrorOf> <url>https://maven.aliyun.com/repository/public</url> </mirror>问题3:前端请求接口返回 401,但登录状态明明有效
大概率是拦截器里没有放行登录、注册、图片访问这几个公开接口。在配置拦截器时明确排除掉白名单路径,这是最容易被忽略的:
registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns("/api/user/login", "/api/user/register") .excludePathPatterns("/api/item/list", "/api/item/detail/**");问题4:上传图片后页面不显示
先检查 Minio 的访问地址是否能直接打开(访问 URL 应该能看到图片内容)。如果打不开,大概率是存储桶的访问策略没有设置为公开读,Minio Web 控制台里把access policy改成public即可。
问题5:JVM 内存不够,打包或启动直接 OOM
IDEA 构建 Maven 项目时内存不足,在Maven Runner的 VM Options 里加:
-Xmx1024m -XX:MaxMetaspaceSize=512m问题6:租借订单归还后,物品状态没有恢复
这是状态机设计漏了一环。归还确认操作里,除了更新订单状态,必须同时更新物品状态。写个事务方法,把“订单状态更新”和“物品状态回写”放在同一个事务里,杜绝数据不一致:
@Transactional public void confirmReturn(Long orderId) { TradeOrder order = orderMapper.selectById(orderId); // 归还后,物品状态回写为可租可售 IdleItem item = idleItemMapper.selectById(order.getItemId()); item.setStatus(3); idleItemMapper.updateById(item); // 更新订单状态 order.setStatus(4); orderMapper.updateById(order); }我花了不少篇幅在那些不起眼的细节上,因为整个项目从 0 到 1 顺利走下来,真正的胜负手就是这些细节。MySQL 连接串多一个参数或少一个参数,Docker 容器能不能互通,拦截器白名单有没有覆盖所有公开接口,这些看着都是小问题,但任何一个爆出来都能卡你好几天。
最后分享一个我个人非常推荐的做法:整个项目完成之后,把所有接口用 Postman 或者 Apifox 挨个过一遍,把核心流程(注册→登录→发布物品→下单→租借→归还→后台审核)做成一个自动化测试集。这样一方面能在答辩演示时不慌,不用担心现场出 Bug;另一方面这套接口文档本身就是你毕业设计论文里“系统测试”章节的最佳素材。校园闲置物品租售系统这个题目做了这么多年,本质考验的就是你能否老老实实把每一个环节打通,不投机取巧。把上面这些内容消化透,这个项目你不仅能做出来,还能在答辩时讲出设计逻辑,这比代码本身更能让你拿高分。