☰
Spring Boot校园闲置交易毕设:表结构、订单状态机与避坑指南
2026/9/25 4:59:30 网站建设 项目流程

简介:基于Spring Boot开发的校园闲置物品交易网站毕业设计项目,面向Java方向毕业生、需要课程设计参考的在校生及初学Spring Boot的开发者,提供从需求分析到系统测试的完整工程示范。系统围绕校园场景实现闲置商品发布、浏览检索、在线交易及后台数据管理,采用前后端分离架构,管理员端与用户端功能分明。资源包共785个文件,压缩体积约22.63MB,以java后端工程、vue与html前端视图、js交互脚本、css样式及svg图标为主要构成,另包含数据库初始化sql、Maven配置、jar依赖包和论文答辩PPT。论文分六章展开:绪论交代课题背景,技术选型讲解Spring Boot、MySQL与Tomcat,需求分析讨论可行性与系统功能,系统设计给出数据库ER图与表结构,系统实现展示管理员后台、前台首页与用户模块,系统测试验证运行稳定性;已有159人学习下载。完整源码配合章节清晰的论文,可帮助读者快速定位闲置交易系统的核心实现逻辑,尤其适合用于毕业设计参考、二次开发练习或答辩材料准备。

1. 校园闲置物品交易网站:Spring Boot毕设的“经典题”与它的三个真考点

校园闲置物品交易网站这个Spring Boot毕设题目,属于典型的“听起来不复杂,做起来全在细节里”的项目。二手交易没有商品SKU维度、没有复杂的促销规则,核心链路无非是用户发布闲置、浏览商品、发起购买、线下自提或校内当面交易。但真正让一届届学生熬夜翻车的,是三个点:一是订单状态怎么流转才不越界,二是商品图片上传后部署时总是404,三是答辩演示时打开页面发现订单数据对不上。这篇笔记会从表结构设计、后端核心代码、论文与PPT写法、常见踩坑四个方向,把整套方案完整拆开,照着做就能跑通。

2. 业务边界与表结构设计:先把订单状态机画清楚,再谈代码

2.1 校园场景的业务边界:回收站式发布、校内自提、站内信

校园闲置物品交易和闲鱼这类通用二手平台最大的区别在于:交易半径极小。买卖双方大概率在同一栋宿舍楼、同一个食堂圈子里,所以业务上不需要物流体系,不需要第三方担保支付,甚至不需要复杂的地址管理。常见做法是线下自提或者约在校内某个固定地点见面,订单流程只要做到“买家表达意向→卖家确认→线下成交→双方互相评价”就够了。

这意味着后端业务模型可以比想象中轻很多。如果你按淘宝的模型去设计,反而会给自己挖坑——支付回调、退款流程、物流轨迹这些模块一旦开做,一个月都收不了尾。做毕设题目时要把边界画清楚:网站只提供信息撮合和订单意向管理,不介入真实资金流转,论文里的“系统实现”也围绕这个边界展开。

适合这个题目的技术选型很明确:Spring Boot作为后端框架负责接口和业务逻辑,MyBatis-Plus操作数据库简化单表CRUD,MySQL存储业务数据,前端可以用JSP或者Thymeleaf配合Bootstrap,也可以完全分离做成Vue页面。如果想要论文里有“技术亮点”,就再加一个Redis做热点商品缓存,或者用RabbitMQ做下单通知,这两个点都会变成答辩加分项。

2.2 核心表结构:用户、商品、订单、收藏的字段设计与SQL实现

表结构设计是整个项目的骨架。按最常见的需求拆分,最少需要五张核心表:user用户表、product闲置商品表、orders订单表、favorite收藏表、message站内信表。下面给出可以直接复用的建表脚本:

-- 用户表 CREATE TABLE `user` ( `id` INT 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 '手机号', `avatar` VARCHAR(255) DEFAULT NULL COMMENT '头像路径', `role` TINYINT NOT NULL DEFAULT 0 COMMENT '0-普通用户 1-管理员', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1-正常 0-禁用', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 闲置商品表 CREATE TABLE `product` ( `id` INT NOT NULL AUTO_INCREMENT, `user_id` INT NOT NULL COMMENT '发布者ID', `title` VARCHAR(100) NOT NULL COMMENT '商品标题', `description` TEXT COMMENT '商品描述', `price` DECIMAL(10,2) NOT NULL DEFAULT 0 '价格,单位元', `original_price` DECIMAL(10,2) DEFAULT NULL COMMENT '原价/入手价,用于展示成色', `category` VARCHAR(30) DEFAULT NULL COMMENT '分类:教材/数码/生活用品等', `images` VARCHAR(1024) 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 NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='闲置商品表';

两张核心表的SQL注释里已经标清了每个字段的用途。password字段用BCrypt加密存储,论文里写安全设计时有话说;product.status用数字做状态,不直接用字符串,是为了后续查询走索引更快。商品图片存的是路径而非二进制内容,这也是行业里最稳的做法——MySQL存储大字段会让表体积膨胀,图片文件放在本地上传目录或对象存储里,数据库只存相对路径。

再看订单表和收藏表:

-- 订单表 CREATE TABLE `orders` ( `id` INT NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号,时间戳+随机数生成', `product_id` INT NOT NULL, `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-已完成', `note` VARCHAR(255) DEFAULT NULL COMMENT '买家留言,比如约见面时间地点', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_product_id` (`product_id`), KEY `idx_seller_id` (`seller_id`), KEY `idx_buyer_id` (`buyer_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表'; -- 收藏表 CREATE TABLE `favorite` ( `id` INT NOT NULL AUTO_INCREMENT, `user_id` INT NOT NULL, `product_id` INT NOT NULL, `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_product` (`user_id`, `product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收藏表';

订单表设计时有个容易犯的错误:只存product_id,不存冗余的seller_id和成交价price。这会在后续查询“我卖出的订单”时被迫去关联商品表,一旦商品被下架或删除,关联就断了,历史订单数据直接失联。所以宁可冗余存储,牺牲一点存储空间换查询鲁棒性,这是实战中很实用的思路。

favorite表建了联合唯一索引uk_user_product,实现了“同一用户对同一件商品只能收藏一次”的业务约束,同时让取消收藏的SQL可以直接用DELETE WHERE user_id=? AND product_id=?经纬定位。

2.3 订单状态机:闲置交易的5个状态与流转条件

状态机是整个订单模块的核心,也是论文里画E-R图、状态图时最出彩的部分。这套题目的订单流转通常是这样的:

  • 待确认(0):买家下单后生成,此时商品在页面上标记为“已被拍下”,但还未真正锁定;
  • 已确认(1):卖家在“我卖出的”列表里点击确认,商品从上架中变为已卖出,双方进入线下交易环节;
  • 已取消(2):买家在待确认状态下撤销订单,或卖家拒绝交易,商品恢复为上架中;
  • 已完成(3):双方完成线下交易后,买家点击“确认收货”。

这个状态机的核心约束是:只有待确认状态下允许取消,只有已确认状态下允许完成,任何状态都不能跳转。对应到代码上,就是在更新订单状态的Mapper里加上WHERE status = 前一状态条件,做乐观锁式的更新,而不是让前端传一个状态过来直接覆盖。

如果论文想写深一点,可以把这张状态表画成流程图放进第三章“系统设计”里,答辩时老师一定会顺着订单状态问并发问题。

3. 用Spring Boot把核心链路跑通:登录、发布、下单、确认收货

3.1 项目结构与最小启动配置

项目结构建议按功能模块分包:config(配置)、controller(接口层)、service(业务层)、mapper(数据访问层)、entity(实体类)、common(通用返回与异常处理)。这种分层结构不管写论文还是应付答辩都最标准,评委扫一眼源码目录就知道你有工程意识。

最小启动配置里有一个坑必须提前避开:数据库连接串的时区参数。

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/campus_trade?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 5MB max-request-size: 20MB mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true

serverTimezone=Asia/Shanghai不写的话,连接MySQL 8.x时会报时区错误。文件上传大小限制要显式配置,否则默认1MB,现在的手机照片一张就2MB起步,上传必崩。map-underscore-to-camel-case开启后,数据库的create_time能自动映射到实体类的createTime,少写一堆XML里的resultMap。

3.2 登录鉴权:JWT + 拦截器的实现

校园闲置交易网站不需要做多复杂的权限体系,但管理员和普通用户的接口必须区分开。这里用JWT做无状态登录是最常见也最好讲的方案:用户登录成功后服务端签发一个Token,前端后续请求带上Token,拦截器校验通过后放行,并把用户信息放进请求上下文。

先写一个简单的JWT工具类:

public class JwtUtil { // 密钥,实际项目中应放在配置文件中 private static final String SECRET = "campus-trade-secret"; private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000L; public static String createToken(Integer userId, String username) { return Jwts.builder() .setSubject(username) .claim("userId", userId) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }

这里用的是jjwt库的API,Token里只放了userId和username两个字段,没有放密码等敏感信息。过期时间设7天,是为了学生在答辩演示时不用频繁重新登录,如果你放到生产环境,这个时间通常缩到2小时左右,并配合RefreshToken机制。

接下来是拦截器,这是最容易写错的地方:

@Component public class LoginInterceptor 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 ")) { setUnauthorized(response); return false; } try { Claims claims = JwtUtil.parseToken(token.substring(7)); request.setAttribute("userId", claims.get("userId")); request.setAttribute("username", claims.get("subject")); return true; } catch (Exception e) { setUnauthorized(response); return false; } } private void setUnauthorized(HttpServletResponse response) throws IOException { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}"); } }

拦截器里要先放行OPTIONS预检请求,否则前端用axios发跨域请求时,浏览器预检直接挂在半路上,接口看着没问题但就是调不通。Token解析失败要统一返回401并写明原因,而不是抛个500让前端瞎猜。拿到userId后放进request上下文,后续所有Controller里通过request.getAttribute("userId")就能拿到当前登录人,不需要每次查数据库。

拦截器注册的时候,注意排除登录和注册接口:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns("/api/user/login", "/api/user/register", "/api/product/list", "/api/product/detail/**"); } }

商品列表和详情接口是游客也能看的,所以必须放行。管理员校验单独做一个AdminInterceptor,或者直接在Controller方法里用注解判断role字段,看你的时间充裕程度而定。

3.3 商品发布:参数校验、图片上传与状态字段的初值

商品发布是卖家端的核心操作。一个容易漏掉的设计是:新发布的商品默认status=0(待审核),要等管理员在后台审核通过后才会变成status=1(上架中)。如果没有这个审核环节,两边接口都对不上,论文里的“管理员模块”也少了一整块功能没写。

Controller层接收表单时,用Spring Validation做参数校验:

@PostMapping("/api/product/publish") public Result publish(@Valid @RequestBody ProductDTO dto, HttpServletRequest request) { Integer userId = (Integer) request.getAttribute("userId"); return productService.publish(userId, dto); }

ProductDTO里给关键字段打注解:

public class ProductDTO { @NotBlank(message = "标题不能为空") @Size(max = 100, message = "标题不能超过100字") private String title; @NotNull(message = "价格不能为空") @DecimalMin(value = "0.01", message = "价格必须大于0") private BigDecimal price; @NotBlank(message = "商品描述不能为空") private String description; }

注意这里用的是@Valid触发校验,如果校验失败,Spring Boot会抛MethodArgumentNotValidException,在全局异常处理器里统一转成JSON返回给前端。一定不要在Controller里自己写一堆if判断,时间不够写不完还漏得一塌糊涂。

图片上传的路径常见做法是存在本地某个目录,然后做一个虚拟路径映射让外部能访问。上传接口的写法:

@PostMapping("/api/upload") public Result upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.error("文件不能为空"); } String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String fileName = UUID.randomUUID().toString() + ext; String datePath = new SimpleDateFormat("yyyy/MM/dd").format(new Date()); File dir = new File(uploadPath + "/" + datePath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, fileName)); return Result.ok("/upload/" + datePath + "/" + fileName); }

用UUID重命名文件,避免中文文件名和重名文件互相覆盖;按日期分子目录,防止单个目录下文件过多。这里生成的访问路径是/upload/2024/05/20/uuid.jpg,接下来必须在配置里加资源映射:

@Configuration public class UploadPathConfig implements WebMvcConfigurer { @Value("${upload.path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadPath + "/"); } }

这个配置不写,上传成功但前端<img>标签里的图片永远显示裂图,属于最高频翻车点之一。

3.4 下单与确认收货:事务边界和乐观锁的应用

下单动作涉及两张表的写操作:插入订单记录、把商品状态从上架中改成已被拍下。这两步必须在一个事务里,否则会出现订单生成了但商品状态还是上架中的脏数据。在Spring Boot里,直接在Service方法上标注@Transactional即可:

@Transactional(rollbackFor = Exception.class) public Long createOrder(Integer buyerId, Integer productId, String note) { // 1. 查商品,判断状态必须是上架中 Product product = productMapper.selectById(productId); if (product == null || product.getStatus() != 1) { throw new BizException("商品不存在或已被拍下"); } // 2. 同一件商品不允许自己买自己的 if (product.getUserId().equals(buyerId)) { throw new BizException("不能购买自己发布的商品"); } // 3. 生成订单号并插入订单表 Orders order = new Orders(); order.setOrderNo(generateOrderNo()); order.setProductId(productId); order.setSellerId(product.getUserId()); order.setBuyerId(buyerId); order.setPrice(product.getPrice()); order.setStatus(0); order.setNote(note); ordersMapper.insert(order); // 4. 更新商品状态为已被拍下 Product update = new Product(); update.setId(productId); update.setStatus(3); productMapper.updateById(update); return order.getId(); }

这个方法已经考虑了三个边界条件:商品不存在或已被拍下抛异常、自买自抛异常、事务回滚保证一致性。但高并发场景下还有更深一层的问题:两个买家同时下单同一件商品,前一个事务还没提交,后一个查到的商品状态也是上架中。解决办法是在查询商品时加FOR UPDATE行锁:

SELECT * FROM product WHERE id = #{id} AND status = 1 FOR UPDATE

或者用乐观锁方式:更新商品状态时带上WHERE status = 1条件,如果影响行数为0,说明商品状态已被改动,直接抛“手慢了,宝贝被拍走了”的错误。实践中用乐观锁的体验更好,不会长时间锁行。放在论文里这就是并发控制小节的内容,答辩时零压力。

确认收货的接口同理,订单状态从已确认变成已完成,必须加上WHERE id=? AND status=1条件。前端在点击“确认收货”之前,页面一直显示当前状态,后端更新成功返回最新状态即可。

3.5 用Redis缓存商品列表:论文技术亮点与实战收益

给这个项目加一个Redis做热点商品缓存,是让论文“技术选型”章节立刻饱满的最快方式。核心思路:首页的商品列表和搜索列表访问频率最高,但数据变化不频繁,第一次查数据库后把结果写入Redis,后续请求直接命中缓存。

public List<ProductVO> getProductList(int page, int size, String keyword) { String cacheKey = "product:list:" + page + ":" + size + ":" + keyword; // 1. 先查缓存 String json = redisUtil.get(cacheKey); if (StringUtils.isNotBlank(json)) { return JSON.parseArray(json, ProductVO.class); } // 2. 缓存未命中,查数据库 List<ProductVO> list = productMapper.selectList(page, size, keyword); // 3. 写回缓存,设置过期时间5分钟防止数据长期不一致 redisUtil.set(cacheKey, JSON.toJSONString(list), 300); return list; }

这里有个细节:商品列表的Redis缓存过期时间不要设太长,5分钟比较合适。因为二手商品的状态变化很频繁,一件商品被拍下后如果缓存还是“上架中”,用户点进去会报错。加了缓存之后,论文里“性能设计”章节的400字就有了,答辩时也能回答“缓存与数据库一致性怎么保证”这类必问题。

4. 论文与PPT答辩:把代码讲成“设计与实现”

4.1 论文的章节结构与每章字数参考

这套项目交付物里,论文是不可或缺的部分,它本质上是对代码的二次包装。常见的结构安排是:

章节内容要点参考字数
第一章 绪论校园闲置交易的背景、国内外研究现状、论文结构安排2000字左右
第二章 相关技术介绍Spring Boot框架、MyBatis-Plus、MySQL、Redis、JWT2000字左右
第三章 系统分析可行性分析(技术/经济/操作)、功能性需求、非功能性需求、用例图2500字左右
第四章 系统设计总体架构图、功能模块划分、数据库E-R图、核心表结构、状态图3000字左右
第五章 系统实现每个模块的界面截图+核心代码+逻辑说明,按“用户模块/商品模块/订单模块/后台管理模块”展开3500字左右
第六章 系统测试测试环境、功能测试用例表格、性能测试(用JMeter截图一张)、测试结论1500字左右
第七章 总结与展望完成的工作、不足之处、后续改进方向800字左右

系统实现那一章最容易写成代码堆砌大杂烩,评审最不喜欢看大段大段贴代码的论文。正确姿势是每个功能小节先写一段业务逻辑描述,再放一个关键方法的核心代码片段(控制在15行左右),最后用1~2句话说明这段代码实现了一个什么业务约束。比如订单模块就重点贴状态更新的乐观锁条件,而不是把整个Service类几十行全盘复制。

4.2 PPT的10页结构与演示顺序

答辩PPT一般控制在10页左右,每页讲1~2分钟,配上现场系统演示正好10分钟。结构建议是:

  • 第1页:题目、答辩人、指导教师——名字不念串就行;
  • 第2页:项目背景与研究意义——两句话带过,不要念长篇;
  • 第3页:技术栈图谱——Spring Boot、MyBatis-Plus、MySQL、Redis,用表格列出每个技术解决什么问题;
  • 第4页:系统功能模块图——画一个树状图,用户端/管理员端两条分支;
  • 第5页:系统架构图——浏览器→Controller→Service→Mapper→MySQL,标注Redis在哪一层;
  • 第6页:数据库设计——只放核心的E-R图或订单状态流转图;
  • 第7页:系统演示——直接切到运行环境,这是全场核心,前面页面不要拖太久;
  • 第8页:核心难点与解决方案——讲乐观锁控制商品并发下单、JWT无状态登录、图片本地存储与路径映射这三点;
  • 第9页:系统测试结果——放用例表格截图;
  • 第10页:总结与展望——客气收尾,感谢老师提问。

演示的时候,一定要按角色切换账号演示。先切普通卖家账号发布一件商品,再切管理员账号在后台审核通过,然后切另一个买家账号去搜索并下单,最后回到卖家账号确认订单。这条完整链路走完,评审对系统的信任度会高很多。

4.3 答辩被问概率最高的5个问题与应答思路

第一个必问题是“为什么选Spring Boot而不是SSH或SSM”。应答要点:Spring Boot简化了配置,内嵌Tomcat不用单独部署,自动装配特性让开发专注于业务逻辑,同时社区生态成熟,适合快速交付项目。

第二个必问题是“订单并发情况下怎么防止一件商品被多人买走”。应答思路是:先讲数据库乐观锁,更新商品状态时带条件WHERE status=1,影响行数为0说明被抢;再讲Redis缓存商品状态,但最终以数据库更新结果为准。

第三个问题是“JWT和传统Session登录有什么区别”。应答要点:Session存储在服务端,集群部署时要考虑Session共享;JWT无状态,Token存在客户端,服务端只需要验签,天然支持分布式环境,更适合前后端分离架构。

第四个问题是“一张表大概有多少条数据,性能如何”。这个必须提前造数,往商品表插个几百条测试数据,论文测试章节放JMeter压测结果,比如并发100时接口平均响应时间120ms,吞吐量每秒80个请求。别被问到的时候答“测试环境没有数据”。

第五个问题是“做过哪些安全性考虑”。应答思路:密码BCrypt加密存储、JWT拦截器保护非公开接口、上传文件格式限制、SQL语句全部使用预编译避免注入。

5. Spring Boot交易网站避坑清单:5个让你当场翻车的细节

5.1 图裂404:上传成功但页面加载不出图片

现象:文件上传接口返回成功,数据库里也存了路径,但前端<img>标签的src为404。刷新、重启都没有用。

原因:Spring Boot默认的静态资源映射只覆盖classpath:/static/等目录,你上传到本地磁盘绝对路径下的文件,并没有被映射到URL上。

解决:在WebMvcConfigurer里添加资源映射,把/upload/**这个虚拟路径指向磁盘的物理路径,也就是3.3节里贴的那段配置。上传文件的保存路径用配置文件维护,不要硬编码在代码里,更不要放到项目打包后的目录下。

5.2 商品超卖:两个买家同时下单同一本书

现象:测试阶段两个账号同时抢一件商品,两个订单都创建成功了,商品状态却是已卖出。

原因:查询商品状态和更新商品状态不是原子操作。事务A查到状态为1,事务B也查到状态为1,A更新成已卖出,B再更新时虽然覆盖了,但订单已经插入了。

解决:更新商品状态时,在update set status=3 where id=? and status=1这个SQL上做版本控制,updateById的返回值是0就说明已经被别人抢了,直接抛异常回滚。或者用select ... for update锁行。千万不要只用代码里的if判断来挡并发,那个只能骗骗自己。

5.3 拦截器把登录接口拦截了,登录变成死循环

现象:启动项目后调用登录接口,返回401未授权,但明明登录接口是不需要鉴权的。

原因:拦截器注册时addPathPatterns("/**")通配了所有路径,excludePathPatterns里的路径写错了,常见的是后端接口统一带/api前缀,但排除路径漏写了,或者排除的是/user/login而接口实际是/api/user/login。

解决:把注册拦截器的配置放在一个独立的WebConfig类里,排除路径写绝对路径,启动时在控制台打印出拦截器匹配的路径清单,肉眼确认一遍。前端联调时遇到401优先查这里,不要怀疑前端代码。

5.4 修改商品时未填的字段被更新成null

现象:前端只传了标题和价格,没有传描述和分类,保存后数据库里的description字段变成null,页面详情只剩残缺信息。

原因:MyBatis-Plus的updateById默认策略是字段为null也更新,你用实体类做更新时,没有从前端带过来的字段自然就是null,覆盖掉了原有的值。

解决:给实体类的字段加配置策略,在MyBatis-Plus全局配置里设置:

mybatis-plus: global-config: db-config: update-strategy: not_null

这样更新SQL只会拼接非空字段,没传过来的字段保持原值不变。这是MyBatis-Plus非常经典的一个坑,答辩时你自己主动提出来,老师会觉得你有实战经验。

5.5 演示现场数据库连不上:时区、端口、防火墙三连问

现象:评委老师让你现场跑一下系统,结果启动报错,Communications link failure。

原因:第一层是MySQL没启动;第二层是serverTimezone配置不对,高版本MySQL驱动强制要求显式时区;第三层是你笔记本连了实验室WiFi,防火墙拦了3306端口。

解决:答辩前一天做一次完整预演,把IDEA启动Spring Boot、Navicat连接数据库、前端页面三个流程在自己电脑上各跑两遍。所有配置不要依赖网络上的共享数据库,就用本机MySQL,连接串里的时区写成Asia/Shanghai。手机开热点也连不上时,检查MySQL的bind-address是不是127.0.0.1,改成本机局域网IP或者直接注释掉重启服务。

6. 把系统做成“能讲的完整项目”:最后一天值得做的五件事

很多学生代码写完了,但演示时只能干巴巴地点几个页面,没有数据、没有对比、没有细节。答辩前如果还剩一天的时间,按优先级做这几件事:第一是写一个数据初始化脚本,往用户表里插两个普通用户和一个管理员,商品表里插20件分布在各个分类的商品,模拟不同成色和价格区间,订单一到两笔,这样演示时不用现场造数据对抗尴尬。第二是准备两个浏览器窗口,一个登录卖家,一个登录买家,演示下单时两个窗口切换,比反复退出登录更流畅。

第三件事是检查所有前端页面的空状态处理,比如商品列表没数据时显示“暂无闲置物品”,订单列表为空时显示“还没有相关订单”,这比空白页面让评委舒服得多。第四件事是把项目打包部署一次,执行mvn clean package构建出jar包,用java -jar命令跑起来,确认没有依赖本机IDEA环境。很多学生平时都是在IDEA里点运行的,打包后才发现配置文件里的路径写死导致启动失败,提前做一遍相当于买了后悔药。

最后一件事是给核心接口写一份简单的README测试清单,把每个接口的请求方式和预期响应贴进去,答辩时如果老师要现场看你接口,直接按清单演示。

这几年陆陆续续帮人看过不少同类毕设,说句实在话,校园闲置物品交易网站这个题目本身不卡人,卡人的永远是那些你以为不是问题的问题:图片路径没映射、输入框没做校验、商品状态更新没加并发控制、答辩PPT念了十分钟没演示系统。把这些细节按上面这套流程走一遍,这个项目就不只是能跑,而且是能讲出设计亮点的完整作品。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询