1. 为什么选校园二手旧物交易这个方向先搞清楚项目要解决什么做毕设选课题这件事纠结程度不亚于选对象。我当时翻遍了身边同学在做的系统图书馆管理、实验室设备管理、在线商城、订餐平台……最后敲定的是这个基于Spring Boot的多鱼校园二手旧物交易平台。说实话二手交易这个方向被不少人低估了——它不像商城那样要接支付、搞库存但覆盖的需求场景特别完整用户认证、商品发布、多图上传、搜索分页、订单流转、收藏留言这些全是 Java Web 开发的核心基本功而且校园旧物这个业务背景本身有真实痛点论文和答辩都好讲。这项目的名字叫多鱼取自多余的谐音意思是让宿舍角落里吃灰的书本、自行车、台灯、吉他重新找到主人。闲鱼这种大众平台最大的问题是圈子太杂校园场景反而更垂直交易双方都在同一个校区见面方便、信任成本低、东西定位特别明确——教材教辅、二手电动车、宿舍小家电是绝对主力。所以多鱼从一开始就不是做一个闲鱼而是做一个限定在校园范围内的轻量闲置交易工具。1.1 多鱼的定位不是闲鱼是校园场景的轻量交易校园二手交易和普通 C2C 平台有几个本质上的区别这些区别直接决定了功能设计用户身份可约束。普通平台上用户是匿名的陌生人但校园场景里可以用学号、校园邮箱做认证卖家的身份相对可信可以不做复杂的信用体系。交易半径极短。买卖双方大概率在同一个校园所以物流不是核心诉求线下当面交易或校内自提是主要履约方式订单模型里不需要快递单号、配送地址这些重量级字段。商品生命周期短。教材每学期末集中释放毕业季是旧物爆发期商品不需要长期挂售所以一键重新上架批量下架这种操作比普通商城更常用。价格敏感但不复杂。二手商品不追求 SKU、规格、多级分类这种电商标准配置一个分类字段加一个可编辑描述就够了核心是快速发布、快速成交。基于这四点整个系统的业务边界就清楚了不需要购物车不需要支付对接不需要物流追踪。需要的是干净的商品信息流、顺畅的发布流程、简单的订单确认机制以及一个管理员能干预的兜底入口。1.2 核心用户与业务流程拆解系统里一共有三类角色普通用户同时承担卖家和买家两种身份、管理员、游客未登录浏览者。三类角色的关注点完全不同角色核心诉求对应功能游客先看一眼有什么好东西商品浏览、搜索、分类筛选买家找到需要的东西联系卖家完成购买收藏、留言、下单、确认收货卖家把闲置快速出手发布商品、上下架管理、订单处理管理员保证平台内容健康、交易有序商品审核、用户禁用、分类管理、数据统计最重要的业务流程是商品发布→浏览搜索→下单→履约→完成这条主链路。我在做需求分析时画过一个简单的用例清单核心用例包括注册登录、发布商品、编辑商品、上下架、浏览检索、收藏商品、发送留言、下单购买、卖家确认、买家确认收货、订单评价、管理员审核、用户管理。每个用例都不复杂但组合起来就是一个完整的交易闭环。1.3 功能清单哪些必须有哪些可以砍做毕设最容易犯的毛病是功能堆砌最后做不完又删功能。我的经验是先列必做清单再列加分项。第一梯队必须有注册登录、商品发布、商品列表分类关键词搜索、商品详情、个人中心我发布的/我买到的/我卖出的、订单状态流转、管理员后台商品管理、用户管理。第二梯队建议有收藏、留言/商品咨询、图片多图上传、浏览数统计、分类管理。这些功能实现成本不高但能显著提升系统的完整度答辩时也更有话讲。第三梯队量力而行消息通知、数据统计报表、商品违规举报、评论回复、模拟支付流程。这些属于锦上添花如果时间充裕再补。我最后实际实现的功能是第一梯队全部加第二梯队的大部分第三梯队只做了数据统计的雏形。事实证明这个规模对于单人毕设是合理的既能完整跑通主流程论文的功能设计章节也不会显得单薄。2. 技术栈确定与工程骨架搭建少踩版本坑多留扩展位技术选型这块网上说法很多但我的建议非常明确Spring Boot 2.x MyBatis Plus MySQL 8 Redis可选前端用 Thymeleaf 模板引擎加原生 JavaScript或者如果你对前端有信心上 Vue 3 做前后端分离也可以。这里我要重点说下版本的坑。2.1 Spring Boot 版本选型和依赖组合Spring Boot 3.x 已经是新主流了但 2023 年之前的大部分教程和博客都是 2.x 的。如果你是一个人对着一堆资料边查边做我强烈建议用Spring Boot 2.7.x。原因很简单Spring Boot 3 基于 Spring Framework 6 和 Java 17换了 Jakarta EE 命名空间很多老教程里javax.servlet的代码直接编译不过网上搜到的解决方案有一半不适用。另外2.7.x 的 MyBatis、PageHelper、Druid 这些整合方案都非常成熟遇到问题基本一搜就有答案。依赖组合方面我当时用的是parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent !-- 核心依赖 -- spring-boot-starter-web spring-boot-starter-thymeleaf mybatis-plus-boot-starter 3.5.3.1 mysql-connector-java 8.0.33 druid-spring-boot-starter 1.2.20 lombok jjwt 0.9.1 !-- 用于登录鉴权 --如果你选择前后端分离就把spring-boot-starter-thymeleaf换成spring-boot-starter-validation再引入一个springdoc-openapi自动生成接口文档方便联调。2.2 工程分层从 controller 到 mapper 的职责划分Spring Boot 项目的分层是老生常谈但很多人打一开始就没分干净后面越改越乱。我的工程结构是这样组织的com.duoyu ├── controller // 只做参数接收和结果包装不写业务逻辑 ├── service // 业务逻辑层事务标注在这里 │ └── impl ├── mapper // MyBatis Plus 的 Mapper 接口 ├── entity // 数据库实体 ├── dto // 接收前端参数的对象 ├── vo // 返回给前端展示的对象 ├── config // 配置类拦截器、跨域、静态资源映射 ├── common // 统一返回结果、异常、常量枚举 └── utils // JWT、文件存储等工具类这个分层对应到代码上的硬性要求是Controller 里不准出现if (xxx ! null)这种非空判断以外的业务判断Service 里不准出现HttpServletRequest这种 Web 层对象。例如用户下单这个方法Controller 只负责从 token 里解析出当前用户 ID 传给 Service所有的状态校验、金额计算、库存更新全在 Service 里完成。这样做的收益在写测试和答辩展示的时候特别明显——你能很清晰地说出业务逻辑层独立不依赖 Web 环境这句话而且是真的做到了。2.3 统一返回结果和全局异常处理从第一天就写好这个习惯是我踩了两次坑之后才养成的。刚开始写接口时我每个方法都直接返回MapString, Object代码里到处是result.put(code, 200)后来接口一多前端对接的人根本不知道你到底返回了什么格式改一个公共字段得全局搜一遍。正确做法是写一个ResultT统一返回体Data public class ResultT { private Integer code; // 200成功500业务异常401未登录 private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static T ResultT error(String message) { ResultT r new Result(); r.setCode(500); r.setMessage(message); return r; } }配合RestControllerAdvice做全局异常处理把业务异常、参数校验异常、未登录异常统一拦下来。这样 Controller 里的每个方法都只需要关心调用 Service 拿数据这一个动作返回格式混乱的问题直接从根上解决了。这个设计在论文里对应统一接口规范设计一节答辩时是加分点。3. 表结构设计交易平台能不能跑稳先看这几张核心表二手交易系统的表结构说实话不算复杂但有几个表的设计会影响后续所有业务逻辑。我按照用户→商品→交易→互动四个维度拆开讲。3.1 用户体系和认证状态设计用户表不只要存账号密码还要考虑校园认证这个点学号、学院、入学年份、校园认证状态、用户状态正常/禁用。我设计的基础字段如下user - id bigint 主键 - username varchar(30) 登录账号 - password varchar(100) 加密存储 - nickname varchar(30) 昵称 - avatar varchar(255) 头像地址 - student_no varchar(20) 学号 - college varchar(30) 学院手动填写或下拉选择 - grade varchar(10) 入学年份 - phone varchar(15) 手机号 - wechat varchar(30) 微信号方便线下交易联系 - status tinyint (0正常 1禁用) - auth_status tinyint (0未认证 1已认证) - create_time datetime - update_time datetime需要注意的两个细节一是密码必须加密存储我当时用的是BCryptPasswordEncoder成本因子默认 10比 MD5 可靠得多二是不要把明文密码以任何形式写入日志这属于基本职业素养。关于认证状态我做了个折中方案普通注册即可浏览和发布商品但发布商品超过 5 件时提示去认证认证方式很简单填学号加手机号管理员在后台核对。这样既保证毕业设计的核心流程能跑通又体现了一个校园平台应有的身份约束设计。3.2 商品、图片、分类信息展示的底层逻辑商品表是整个系统的信息中枢。每个字段都要问一句前端列表页需不需要这个字段需不需要索引。product - id bigint - user_id bigint 发布者 - category_id int 分类ID - title varchar(80) - description text - price decimal(10,2) 售价 - original_price decimal(10,2) 原价可选 - cover_image varchar(255) 封面图 - status tinyint (0在售 1已售出 2下架 3待审核) - view_count int 浏览数 - favorite_count int 收藏数 - create_time datetime - update_time datetime商品和图片是一对多关系单独建一张product_image表存图片地址和排序序号不要在商品表里存一个逗号分隔的图片链接字符串。那样短期省事后期要做删除某一张图或者拖动排序的时候会非常痛苦。分类表我建议设计成两级大类教材教辅、数码电器、生活用品、运动器材、其他 小类可选。校园场景的商品分类不需要像京东那样三级联动一个 select 下拉框就够但分类表独立成表是为了管理员能在后台增删改不至于每次改分类都要改代码重新部署。还有一个小细节cover_image建议单独存而不是从product_image表里取第一条。因为列表页要频繁展示封面图单独一个字段能少一次子查询在列表接口里写 SQL 也清爽很多。3.3 订单状态机与交易流程的字段设计订单表是整个系统里业务逻辑最重的部分。我先定义了状态枚举order_status 0 - 待卖家确认买家已下单等卖家接单 1 - 待买家收货卖家已确认线下交付中 2 - 已完成买家确认收货 3 - 已取消买家取消或超时未确认 4 - 已关闭管理员介入关闭对应的订单表orders - id bigint - order_no varchar(32) 订单号唯一 - product_id bigint - buyer_id bigint - seller_id bigint - amount decimal(10,2) 成交价 - status tinyint - remark varchar(255) 买家留言 - cancel_reason varchar(255) - create_time datetime - confirm_time datetime 卖家确认时间 - finish_time datetime 买家确认收货时间注意我用的是orders不是order因为order是 SQL 关键字建表时很容易踩这个坑。订单号生成规则我用的是yyyyMMddHHmmss 用户ID后四位 随机数保证同一秒并发下也不会重复。为什么要有卖家确认这一步这是和普通商城一个很关键的区别。普通商城下单后系统自动减库存、生成物流单但在校园二手交易里卖家不是库房商品可能已经被人线下约走了也可能临时不想卖了。所以订单状态机的核心是买家下单只是意向锁定卖家确认才生成有效交易。这个设计既贴合实际业务也避免了并发减库存的复杂问题——如果把库存设计成下单即减反而会导致大量无效订单。3.4 收藏、浏览记录、留言互动场景要考虑的边界这组表可以合并设计因为它们都是用户对商品的操作记录。我建了三张轻量表favoriteid, user_id, product_id, create_time唯一键(user_id, product_id)防止重复收藏。product_view_logid, user_id, product_id, view_time。浏览记录只保留最近 20 条首页展示所以不需要做太复杂。product_commentid, product_id, user_id, content, parent_id, create_time。这里parent_id支持卖家回复买家的场景但我实际只做了一层评论没有做嵌套楼中楼——嵌套评论看起来不难做起来全是边界问题毕设阶段没必要。所有互动表的共同原则是业务数据之外一律加create_time后面做最新上架猜你喜欢排序时都用得上。索引策略上product_id、user_id各自建普通索引就够了不要加联合索引——这个数据量级别普通索引已经完全能跑。4. 核心接口实现从发布商品到交易完成的全链路架构设计说得再多最后还是要落实到接口。这一节我把主链路上最关键的几个接口拉出来讲附带代码片段和一些实际的注意事项。4.1 发布商品多图上传与数据校验发布商品的第一步是处理图片。这里我强烈建议不要用 Base64 把图片塞进数据库这种写法在小项目里很常见但会导致数据库膨胀、接口响应变慢答辩时老师问一句为什么这么设计会很难堪。正确做法是把图片文件写到服务器磁盘指定目录数据库只存访问路径。我在application.yml里配置了上传路径和静态访问映射duoyu: upload-path: /data/duoyu/images/ spring: mvc: static-path-pattern: /images/** resources: static-locations: file:${duoyu.upload-path}后端上传接口PostMapping(/product/image) public ResultString uploadImage(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(文件不能为空); } // 校验文件大小限制在5MB以内 if (file.getSize() 5 * 1024 * 1024) { return Result.error(图片大小不能超过5MB); } // 校验文件后缀白名单防止上传可执行文件 String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)).toLowerCase(); if (!Arrays.asList(.jpg, .jpeg, .png, .gif, .webp).contains(ext)) { return Result.error(不支持的文件格式); } // 生成唯一文件名UUID 后缀避免中文名乱码和路径冲突 String fileName UUID.randomUUID().toString().replace(-, ) ext; File dest new File(uploadPath fileName); file.transferTo(dest); // 返回可访问路径给前端 return Result.ok(/images/ fileName); }注意三个细节文件名必须用 UUID 重命名不能直接用用户上传的文件名否则会出现中文乱码、路径穿越、同名覆盖问题文件格式校验用白名单而不是黑名单上传大小限制要同时在后端做不能只靠前端提示。商品保存核心逻辑先插入product表拿到自增 ID再循环保存product_image关联表最后设置商品状态为在售如果开启了管理员审核则设为待审核。这里要注意先保存商品主记录再保存图片关联这样才能拿到productId作为外键。4.2 商品搜索与分页PageHelper 的坑和正确用法列表页的搜索条件一般是关键词标题描述模糊匹配、分类ID、价格区间、排序方式最新/价格升序/价格降序/人气。MyBatis XML 里写一个动态 SQLselect idselectByCondition resultTypecom.duoyu.vo.ProductVO SELECT p.id, p.title, p.cover_image, p.price, p.original_price, p.view_count, p.favorite_count, p.create_time FROM product p where p.status 0 if testcategoryId ! null AND p.category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (p.title LIKE CONCAT(%, #{keyword}, %) OR p.description LIKE CONCAT(%, #{keyword}, %)) /if if testminPrice ! null AND p.price gt; #{minPrice} /if if testmaxPrice ! null AND p.price lt; #{maxPrice} /if /where choose when testsortType price_ascORDER BY p.price ASC/when when testsortType price_descORDER BY p.price DESC/when when testsortType hotORDER BY p.view_count DESC/when otherwiseORDER BY p.create_time DESC/otherwise /choose /select分页插件用的是PageHelper用法是PageHelper.startPage(pageNum, pageSize); ListProductVO list productMapper.selectByCondition(queryVO); PageInfoProductVO pageInfo new PageInfo(list);这里有一个必须知道的坑PageHelper.startPage()只对紧接着执行的第一次数据库查询生效。如果你在调用分页前去做了一次无关查询比如先查用户 ID、先统计某个参数分页就会作用到那次查询上导致返回的数据完全错乱。所以务必把startPage紧跟在你要分页的 mapper 调用之前。另一个常见问题是前端分页信息不完整。要返回total总条数、pageNum、pageSize、pages总页数这些全在PageInfo里直接转成 VO 返回给前端即可不要自己SELECT COUNT(*)再手动算麻烦且容易出错。4.3 下单与库存更新事务和状态流转的细节创建订单接口被问得最多的是超卖怎么办——但在校园二手场景下商品数量只有 1 件不存在库存递减问题核心反而在防止同一商品被重复下单。我的方案是在product表上加一个乐观锁字段version下单时执行UPDATE product SET status 1, version version 1 WHERE id #{productId} AND status 0在 Service 层判断影响行数如果返回 0说明商品已经被别人下单或已下架直接提示手慢了该商品已被下单。这段更新和订单插入必须在同一个事务里Transactional(rollbackFor Exception.class) public OrderVO createOrder(CreateOrderDTO dto, Long buyerId) { // 1. 查商品校验状态 Product product productService.getById(dto.getProductId()); if (product null || product.getStatus() ! 0) { throw new BizException(商品不存在或已下架); } if (product.getUserId().equals(buyerId)) { throw new BizException(不能购买自己发布的商品); } // 2. 乐观锁更新商品状态防止并发重复下单 boolean updated productMapper.lockProduct(product.getId()); if (!updated) { throw new BizException(手慢了商品已被下单); } // 3. 创建订单 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setProductId(product.getId()); order.setBuyerId(buyerId); order.setSellerId(product.getUserId()); order.setAmount(product.getPrice()); order.setStatus(0); orderMapper.insert(order); return orderVO; }Transactional的rollbackFor Exception.class一定要写上因为默认情况下 Spring 只在遇到 RuntimeException 时回滚如果业务代码抛的是自定义受检异常事务不会自动回滚商品状态改了而订单没建出来数据就脏了。至于买家下单→卖家确认→买家确认收货的后续状态流转我用一个updateStatus(orderNo, fromStatus, toStatus)方法统一处理每次流转都用WHERE status fromStatus做条件更新防止状态跳变。比如买家要取消订单前提是订单当前状态必须是待卖家确认如果卖家已经确认了就不能直接取消只能走申请关闭流程。4.4 个人中心与订单管理卖家和买家视角个人中心实际上就是几组列表查询但要注意交易双方看到的订单方向是相反的。我专门建了OrderVO视图对象包含商品封面、标题、对方昵称、金额、状态文本这样前端不需要自己拼数据。卖家视角的我卖出的列表查询条件是seller_id 当前用户买家视角的我买到的列表查询条件是buyer_id 当前用户。两个列表可以使用同一个 mapper 方法只不过传入的字段不同。不要在 service 层写两个几乎一样的查询方法那样以后改一个状态显示格式要改两处很容易漏。对于商品管理卖家可以在我的发布里对商品执行上架、下架、删除逻辑删除、编辑操作。这里有个隐藏需求商品下架后如果存在未完成的订单应该不能删除商品否则订单详情页查不到商品信息。我当时的做法是删除前先查一下是否有 status 为 0 或 1 的关联订单有则提示商品存在进行中的订单不可删除可先下架。5. 登录鉴权与安全防护没有安全答辩会被盯上登录鉴权在毕设里占的篇幅不大但它是老师最爱问的部分。这里我选了当下最主流的 JWT 方案讲解清楚也能体现你对无状态认证的理解。5.1 登录方案选择Session 还是 JWT传统 Session 方案的问题是登录状态存在服务器内存里多实例部署时 Session 不共享还要引入 Redis 做 Session 集群。对于单体项目来说 Session 完全够用但无状态支持前后端分离用 JWT 讲起来更顺畅。User 登录成功后生成一个包含用户基础信息的 Token// 登录成功后 String token JwtUtil.createToken(user.getId(), user.getUsername());Token 的有效期我设置成 24 小时用户在有效期内不需要重新登录。还需要考虑记住我场景的话可以拆成 accessToken refreshToken 两套但毕设不需要这么复杂一个 Token 搞定。5.2 基于拦截器的权限控制在WebMvcConfigurer里注册拦截器给需要登录的接口统一加上校验Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns( /user/login, /user/register, /product/list, /product/detail/**, /images/**, /error ); } }LoginInterceptor的核心逻辑public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (StringUtils.hasText(token) JwtUtil.validate(token)) { Long userId JwtUtil.getUserId(token); request.setAttribute(currentUserId, userId); return true; } // 未登录返回 401 response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return false; }管理员接口比如后台管理相关的可以再建一个AdminInterceptor或者简单地在 Controller 方法上用自定义注解RequireRole(ADMIN)配合 AOP 处理。我实际用的是后者因为 AOP 拦截注解一方面能精确到方法级别另一方面答辩时可以讲基于注解的权限控制设计比笼统的拦截器更有层次。5.3 常见的几个安全坑XSS、越权、密码存储这里必须提醒三个我实测踩到的坑。第一个是文本内容的 XSS 问题。商品标题、留言内容、用户昵称这些用户可控文本如果直接原样存库再原样渲染到页面攻击者可以插入script标签。最简单的解决方案在服务端做转义过滤注册一个全局过滤器把请求体里的、、等字符转义成 HTML 实体。这里重点提醒不仅普通的文本字段要处理上传的 PDF、Excel 等文件内容里也可能嵌入恶意脚本如果项目做了在线预览 PDF 的功能一定要对上传文件做类型校验和内容检测不能只靠文件后缀判断——搜索热词里就有人遇到了上传 PDF 触发 XSS 攻击的问题根源就是在文件预览时直接使用了原始文件没有做任何过滤。第二个是水平越权问题。修改商品这个接口如果只校验商品存在而不校验当前用户是不是商品发布者那任何登录用户都能改别人的商品。我在ProductServiceImpl里有一条固定规则所有对商品、订单的写操作第一步都是从 token 中取出currentUserId然后校验资源的userId/sellerId是否等于当前用户不相等直接抛异常。这个方法需要在每个写接口都调用是高压线。第三个是密码安全存储。上文说了用 BCrypt这里补充一点不同的密码即使相同BCrypt 每次生成的哈希串也不一样自带随机盐所以注册时直接用encode存库登录校验时用matches比对不要像以前那样去库里反查明文密码。6. 联调实测与答辩经验那些让我改了又改的地方最后这部分我想讲讲项目跑通之后发生的事情。代码写出来是一回事能稳定运行、能经得起老师提问是另一回事。6.1 本地联调环境测试数据要贴近真实很多人开发时用系统自带的 H2 数据库或者只在 MySQL 里插了三条测试数据结果演示的时候页面稀稀拉拉完全看不出效果。我建议开发环境用真实 MySQL并准备一批贴近校园场景的种子数据50 本不同的教材书名、作者、原价、售价比如《高等数学》原价 45 定价 2010 辆自行车定位是毕业季学生转手各种宿舍神器台灯、挂篮、小冰箱、床帘数码配件类键盘、鼠标、耳机、充电宝这批数据不能只写在 SQL 里一次性插入最好单独抽出一个DataInitializer类在应用启动时判断表为空才加载。这样另一个同学拉取代码跑起来也会自动有完整的演示数据不用手工配。6.2 实测高频 bug 和修复记录我整理一下整个开发过程中最值得记录的五个问题这些问题在答辩时反而成了项目深度的证据商品图片路径显示 404。原因是上传的文件目录不在 Spring Boot 的静态资源扫描路径里。解决方案就是前文写的在application.yml里配置file:开头的静态映射。注意 Windows 和 Linux 下的路径写法不同Linux 下/data/duoyu/images/的结尾斜杠不能丢否则拼出来的路径会出现双斜杠。分页参数错乱。PageHelper 和 MyBatis Plus 自带的分页功能混用导致当次查询的分页逻辑混乱。解决方案是二选一全套统一用 PageHelper或者统一用 MyBatis Plus 的Page对象。两种工具都很好用但混用就会出幺蛾子我在项目里统一留了 PageHelper。时间格式前后端不一致。Java 后端的 LocalDateTime 序列化成 JSON 时默认输出是2025-01-08T21:30:00这种带 T 的格式前端如果想显示2025-01-08 21:30还得额外处理。最好的方式是在application.yml里统一配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8Long 型主键 JS 精度丢失。MyBatis Plus 默认的雪花 ID 是 19 位 Long传到前端 JavaScript 会丢失后几位精度导致编辑某条数据时操作错了记录。这个问题的标准解法是在JacksonConfig里把 Long 序列化成 String。我当时的配置是对Long和long都转字符串防止主键精度出问题。如果你用的数据库自增 ID数据量不大这个坑可以绕过去但要跟老师讲清楚为什么自增 ID 在这里是够用的。取消订单后商品状态没有恢复。我最初实现买家取消订单时只改了订单状态忘了把product.status从 1已售出改回 0在售结果取消之后商品在列表页消失再也搜不到。修复方案就是在取消订单的事务里同时更新订单状态和商品状态并且都用条件更新确保幂等Transactional public void cancelOrder(String orderNo, Long userId) { int rows orderMapper.cancelOrder(orderNo, userId, OrderStatus.PENDING_CONFIRM.getCode()); if (rows 0) { throw new BizException(订单状态已变化无法取消); } // 恢复商品为在售 productMapper.restoreByOrderNo(orderNo); }6.3 答辩时如何把设计讲清楚很多人代码写得挺好一到答辩就变成我实现了增删改查。这里分享一个我觉得很有效的讲述框架每讲一个功能都按业务场景→设计决策→技术实现→异常处理四步来。举个例子讲下单功能业务场景校园二手交易中买家看到心仪商品后下单但这个商品不像电商有库存卖家可能已经线下约出必须有一个卖家确认环节。设计决策因此引入了买家意向单→卖家确认→买家收货三段式状态机并在商品表加 version 乐观锁防止同一个商品被并发下单。技术实现下单接口在 Service 层事务中先执行条件更新锁定商品再插入订单记录订单号由时间戳用户ID随机数生成保证高并发下不冲突。异常处理商品已被他人锁定、买家购买自己的商品、订单状态过期等业务异常统一由RestControllerAdvice拦截为友好提示。照着这个逻辑讲整体就是一个需求分析→设计→实现→保障的完整故事比干巴巴念 PPT 强太多。最后说一句关于测试的题外话毕设项目至少要把主链路的接口用 Postman 或 Apifox 过一遍包括正常流程和异常流程。我见过太多人演示时现场翻车——比如卖家确认订单和买家确认收货的按钮逻辑写反了这种低级错误一旦发生印象分会大打折扣。如果你也是从零开始做类似的项目建议把上面的表结构设计和状态机先画在纸上再动手写代码思路清晰了写代码的效率会高很多前期多花一小时的建模时间后期能省一整天的返工时间。
