ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Java的校园二手交易系统实战:Spring Boot+MyBatis-Plus开发解析

基于Java的校园二手交易系统实战:Spring Boot+MyBatis-Plus开发解析 简介《基于Java的校园二手交易市场系统设计与实现》是一份完整的Java毕业设计项目包面向计算机专业学生、毕业设计开发者以及希望学习Java Web开发的初学者可帮助理解校园二手交易场景下的系统设计、前后端编码与数据库实现。压缩包共4个文件整体约73.27MB包含2个mp4演示录像、1个zip源码工程和1个sql数据库脚本其中录像展示系统整体运行与操作流程源码提供可直接导入IDE的Java工程sql文件用于初始化数据库表结构与示例数据三者配合即可复现可运行项目。目前已有116人浏览学习项目已通过验收运行状态可靠适合用于毕业设计或课程设计的参考。通过对照源码、录屏与数据脚本读者可以快速掌握Java Web开发流程、数据库建模思路以及交易平台的典型业务逻辑从而在现有基础上进行功能扩展或二次开发。1. 校园二手交易Java系统从散落在QQ群的信息到可运行的闭环大学里的二手交易信息八成还散在QQ群、年级群和朋友圈里想买台二手打印机翻几百条聊天记录好不容易联系上卖家对方一句“已经出了”把你打回原点。有人干脆建了个“跳蚤市场”群结果群里全是广告没有检索、没有状态管理更没有交易闭环。基于Java的校园二手交易市场系统就是把这些散落的供需信息收进一个结构化系统里注册登录、发布商品、分类检索、下单交易、订单管理整套流程都由数据库和接口支撑而不是靠人肉和聊天记录。这篇笔记要做的就是把它拆开先讲技术选型和库表设计再讲发布、检索、下单三段核心代码最后把图片上传、登录态和那些容易翻车的点一次说清。适合正在做Java方向课程设计或毕业设计的同学也适合想完整走一遍Java Web后端工程的初级工程师。2. 技术选型与库表建模为什么是Spring Boot MyBatis-Plus六张表怎么设计2.1 选型理由Spring Boot 2.7 MyBatis-Plus 3.5 的分工边界标题只写了“基于Java”具体落到工程上可选的路线有好几条JSP Servlet、SSHStruts Spring Hibernate、Spring Boot MyBatis-Plus。我的建议很直接除非教学大纲强制要求否则别碰JSP Servlet。它能把“请求怎么进来、页面怎么出去”讲清楚但工程化太弱代码全堆在Servlet里后面加一个订单状态机就能把人写崩溃。SSH更不用考虑Struts 2的配置和XWork的兼容性属于“老工程师的噩梦”。Spring Boot 2.7.x Java 8是当前最稳的组合JDK 11和JDK 17也能跑但部分老版本插件会报模块访问错误处理起来花时间。MyBatis-Plus 3.5.x解决的是单表增删改查的体力活——BaseMapper里已经内置了insert、updateById、selectPage、selectById连SQL都不用写。它的分页插件、LambdaQueryWrapper条件构造器都让“数据库增删改查”这件事从写XML变成写Java方法。这套组合对一个人短时间做完一个毕设项目来说边界非常清晰MyBatis-Plus管数据访问Spring Boot管接口和Bean装配前端只要对接JSON即可。pom.xml里最核心的依赖就这几项dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependencystarter-web负责把接口层搭起来mybatis-plus-boot-starter接数据库lombok用来省掉实体类里的getter/setter。数据库连接池直接用Spring Boot默认的HikariCP不需要额外引druid除非你后面要做慢SQL监控。版本上注意一点mysql-connector-java 8.x的驱动类名是com.mysql.cj.jdbc.Driver别再用老写法com.mysql.jdbc.Driver不然启动直接报ClassNotFound。数据库连接配置里要特别留意时区和编码参数spring: datasource: url: jdbc:mysql://localhost:3306/campus_market?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your-password driver-class-name: com.mysql.cj.jdbc.DriversueUnicodetruecharacterEncodingutf8保证写入的中文不乱码serverTimezoneAsia/Shanghai解决MySQL 8.x默认时区和中国本地时间差8小时的问题。如果这条配置漏了createTime字段存进数据库的时间会比当前时间晚8个小时排查起来非常像玄学。2.2 库表设计用户、商品、订单、收藏、留言六张核心表的DDL与索引二手交易系统的表结构不用追求大而全连购物车表都不建议建——校园二手是单件直拍逻辑用户看中一件商品直接下单不存在“多件合并结算”的场景。核心就是六张表用户表、分类表、商品表、订单表、收藏表、留言表。先看DDLCREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT BCrypt密文, nickname VARCHAR(50) DEFAULT NULL COMMENT 昵称, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像URL, phone VARCHAR(20) DEFAULT NULL COMMENT 联系方式, role TINYINT DEFAULT 0 COMMENT 0普通用户 1管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE category ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(30) NOT NULL, sort INT DEFAULT 0 COMMENT 排序权重, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品分类表; CREATE TABLE goods ( id BIGINT NOT NULL AUTO_INCREMENT, seller_id BIGINT NOT NULL COMMENT 发布者, category_id BIGINT NOT NULL, title VARCHAR(100) NOT NULL, description TEXT COMMENT 商品描述, price DECIMAL(10,2) NOT NULL COMMENT 价格单位元, images VARCHAR(1000) DEFAULT NULL COMMENT 逗号分隔的图片URL, status TINYINT DEFAULT 0 COMMENT 0在售 1已售 2下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_seller (seller_id), KEY idx_category (category_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表; CREATE TABLE orders ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号, goods_id BIGINT NOT NULL, buyer_id BIGINT NOT NULL COMMENT 买家, seller_id BIGINT NOT NULL COMMENT 卖家冗余字段, amount DECIMAL(10,2) NOT NULL, state TINYINT DEFAULT 0 COMMENT 0待确认 1待收货 2已完成 3已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_buyer (buyer_id), KEY idx_seller (seller_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表; CREATE TABLE favorite ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, goods_id BIGINT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_goods (user_id, goods_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT收藏表; CREATE TABLE message ( id BIGINT NOT NULL AUTO_INCREMENT, goods_id BIGINT NOT NULL, from_user_id BIGINT NOT NULL COMMENT 提问者, to_user_id BIGINT NOT NULL COMMENT 卖家, content VARCHAR(500) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_goods (goods_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品留言表;几个设计决策值得展开。第一goods.status用TINYINT而不是字符串查询走索引更快也不容易被前端拼出脏数据0/1/2三个状态正好对应“在售/已售/下架”。第二orders.seller_id是故意冗余的因为下单那一刻商品可能已经改过名字或者卖家改了昵称订单里保存一份快照方便后续查询。第三favorite表加了UNIQUE KEY uk_user_goods从数据库层面保证一个用户对同一商品只能收藏一次防止前端连点两次按钮插出重复数据。为什么不建外键不是不会是没必要。这个规模的项目事务都控制在Service层外键带来的插入顺序约束和级联删除反而会让初始化数据时处处受制。删商品就删商品留言和收藏不跟着物理删除直接用status2做软下架数据留底比删干净更有价值。分类表在启动时手工插入几行就够了书籍教材、数码产品、生活用品、运动器材、其他。别搞太细用户根本不会选。3. 核心交易流程实现发布、检索、下单三段代码怎么落地3.1 发布商品参数校验、价格精度与图片字段的约定商品发布是整个系统第一个真正有业务含量的接口。Controller层只负责接收和校验具体落库逻辑放到Service里。DTO用Validated注解把必填项挡在业务代码之前Data public class GoodsPublishDTO { NotBlank(message 标题不能为空) Size(max 100, message 标题不能超过100字) private String title; NotBlank(message 描述不能为空) private String description; NotNull(message 价格不能为空) DecimalMin(value 0.01, message 价格必须大于0) private BigDecimal price; NotNull(message 请选择分类) private Long categoryId; private String images; // 逗号分隔的图片URL可空 }接口层写成这样PostMapping(/api/goods) public ResultLong publish(Validated RequestBody GoodsPublishDTO dto, RequestAttribute(userId) Long userId) { return Result.success(goodsService.publish(dto, userId)); }Service里把DTO转成实体手动设定初始状态Transactional(rollbackFor Exception.class) public Long publish(GoodsPublishDTO dto, Long userId) { Goods goods new Goods(); goods.setSellerId(userId); goods.setCategoryId(dto.getCategoryId()); goods.setTitle(dto.getTitle()); goods.setDescription(dto.getDescription()); goods.setPrice(dto.getPrice()); goods.setImages(dto.getImages()); goods.setStatus(0); // 新发布的商品默认在售 goodsMapper.insert(goods); return goods.getId(); }价格字段必须用BigDecimal而不是double原因很直接double在计算0.10.2时会得到0.30000000000000004金额数据一旦出现这类精度问题后面订单金额、统计报表全是黑洞。DecimalMin注解保证用户不能传0元或负数这是业务底线。images字段用逗号分隔的字符串存多个图片URL前端上传后返回URL列表提交时join成一个字符串。有人会问为什么不用一对多子表存图片——一张图片一行记录列表查询就得join校园项目商品图片通常不超过5张一个字段存完全够用。3.2 商品检索分页插件配置与关键字搜索的查询条件商品列表页是访问量最大的接口必须做分页否则数据一多页面直接卡死。MyBatis-Plus的分页需要先注册插件很多新手直接写selectPage发现没效果就是因为漏了这一步Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }插件注册好之后查询接口这样写public PageResultGoods query(Integer pageNum, Integer pageSize, Long categoryId, String keyword) { PageGoods page new Page(pageNum, pageSize); LambdaQueryWrapperGoods wrapper new LambdaQueryWrapper(); wrapper.eq(Goods::getStatus, 0) // 只查在售商品 .eq(categoryId ! null, Goods::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), Goods::getTitle, keyword) .orderByDesc(Goods::getCreateTime); IPageGoods result goodsMapper.selectPage(page, wrapper); return new PageResult(result.getTotal(), result.getRecords()); }eq和like方法的重载都支持第一个参数作为条件开关categoryId ! null为false时这个查询条件自动跳过不用手动拼SQL。orderByDesc(Goods::getCreateTime)相当于SQL里的ORDER BY create_time DESC让新发布的商品排在前面这也算一种最基础的“Java排序”实践。关键字搜索用like %xxx%就够了校园项目的商品量撑死几千条全表扫一遍也就几毫秒完全不需要上Elasticsearch或全文索引——那种复杂度对这个体量是负资产。MyBatis的#{}预编译能防SQL注入但如果用户输入的关键词里带%或_会被当成通配符搜索结果会比预期多。想要严谨可以在拼接前把这两个字符转义掉不转义也不会出安全问题只是搜索结果会宽松一点。3.3 下单闭环事务、状态机与防重复下单的边界下单是整个系统里最需要谨慎的接口它涉及两条数据的变化插入订单、修改商品状态。这两步必须在一个事务里否则会出现“订单创建了但商品还是已售状态”的脏数据。核心代码Transactional(rollbackFor Exception.class) public Long createOrder(Long goodsId, Long buyerId) { Goods goods goodsMapper.selectById(goodsId); if (goods null || goods.getStatus() ! 0) { throw new BizException(商品不存在或已下架); } if (goods.getSellerId().equals(buyerId)) { throw new BizException(不能购买自己发布的商品); } Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setGoodsId(goodsId); order.setBuyerId(buyerId); order.setSellerId(goods.getSellerId()); order.setAmount(goods.getPrice()); order.setState(0); ordersMapper.insert(order); Goods update new Goods(); update.setId(goodsId); update.setStatus(1); // 商品从在售变为已售 goodsMapper.updateById(update); return order.getId(); }Transactional(rollbackFor Exception.class)声明让这个方法里任何一步抛异常前面的数据库操作都回滚。这里要强调事务默认只在RuntimeException上回滚如果业务异常是受检异常必须显式声明rollbackFor所以统一用继承RuntimeException的BizException最省心。generateOrderNo()生成业务订单号常见做法是时间戳加随机数再加点业务标记类似202501011200001234。这个版本的下单逻辑有一个并发隐患两个请求同时查到status0同时通过校验就能对同一件商品插入两条订单。事务保证的是“出错回滚”不是“并发隔离”这个问题在第5章会给出补丁方案。订单状态机简单直接0待买家确认、1待买家收货、2已完成、3已取消。校园二手交易多是当面交接不需要物流状态买家点击确认收货后这笔交易就算闭环了。4. 登录态与上传JWT拦截器、本地图片存储和静态资源映射4.1 用JWT做无状态登录生成、校验、放行路径前端页面和后端接口分离部署时Session方案天然不好用——跨端口共享Cookie要配一堆CORS和跨域策略演示环境还容易出幺蛾子。更常见的做法是用JWT做无状态登录令牌由后端签发前端存在localStorage里每次请求在Header带Authorization: Bearer token。JWT工具类核心代码Component public class JwtUtil { private static final SecretKey KEY Keys.hmacShaKeyFor( campus-market-2024-secret-key-change-me.getBytes(StandardCharsets.UTF_8)); public String generate(Long userId, String username) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 2)) .signWith(KEY) .compact(); } public Claims parse(String token) { return Jwts.parserBuilder() .setSigningKey(KEY) .build() .parseClaimsJws(token) .getBody(); } }过期时间设为2小时演示场景足够如果要上线建议改成15分钟有效期加Redis刷新机制但那是后话。密钥字符串在生产环境要放到配置中心或环境变量别硬编码在代码里否则仓库泄露等于所有令牌都能被伪造。拦截器负责统一校验Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; // 放行静态资源 } String auth request.getHeader(Authorization); if (auth ! null auth.startsWith(Bearer )) { try { Claims claims jwtUtil.parse(auth.substring(7)); request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { // token无效继续往下走返回401 } } response.setStatus(401); return false; } }拦截器里解析出的userId写进Request属性后续Controller方法通过RequestAttribute(userId)直接取省得每个接口都重复解析token。注意检查细节静态资源的HandlerMapping不是HandlerMethod不拦截放行否则会拦住上传图片的访问导致图片全挂。4.2 商品图片上传磁盘存储、URL返回与资源映射配置图片上传是演示系统里最容易做但最容易做砸的模块。上传接口写起来很短PostMapping(/api/upload) public ResultString upload(RequestParam(file) MultipartFile file) throws IOException { String original file.getOriginalFilename(); String ext Objects.requireNonNull(original).substring(original.lastIndexOf(.)); if (!ALLOWED_EXT.contains(ext.toLowerCase())) { throw new BizException(仅支持jpg/png/webp格式); } String filename UUID.randomUUID().toString().replace(-, ) ext; File dir new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, filename)); return Result.success(/upload/ filename); }UUID.randomUUID()重命名文件是为了避免两个问题中文文件名在跨平台传输时乱码同名文件互相覆盖。上传目录uploadDir在application.yml里配置为upload: dir: D:/campus-market-upload。Windows和Linux的路径分隔符不一样别在代码里写死用File构造器自动适配。文件保存到磁盘只是第一步前端要能通过URL访问到它。需要配置静态资源映射Configuration public class WebConfig implements WebMvcConfigurer { Value(${upload.dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir File.separator); } }addResourceHandler(/upload/**)定义URL前缀addResourceLocations(file: uploadDir)把磁盘目录映射上去。注意file:协议后面要跟绝对路径相对路径在java -jar启动时会有不同工作目录图片会写到意想不到的位置这也是“启动失败”或“图片找不到”类问题的高发区。上传接口还要限制文件大小在application.yml里配spring: servlet: multipart: max-file-size: 5MB max-request-size: 20MB不给限制的话Tomcat默认允许1MB文件图片稍大一点就报FileSizeLimitExceededException前端只看到一个500根本不知道发生了什么。这套方案只承接演示级流量如果系统将来要部署在云服务器上本地磁盘不够用到时候再换对象存储但接口含义可以保持一致——上传返回的就是URL字符串前端无感知。4.3 权限边界哪些接口必须拦哪些接口必须放JWT拦截器注册后得明确哪些路径需要登录哪些放行。我的设计是路径是否拦截说明POST /api/user/login放行登录本身不需要令牌POST /api/user/register放行注册不需要令牌GET /api/goods/detail放行游客可以看商品详情GET /api/goods/list放行游客可以浏览商品列表GET /upload/**放行图片资源不能拦POST /api/goods拦截发布商品必须登录POST /api/orders拦截下单必须登录POST /api/upload拦截只有登录用户能传图POST /api/orders/confirm拦截确认收货必须登录注册拦截器时把这些规则维护在WebConfig里路径清晰又集中。怕漏拦的话用最省事的原则兜底默认全部拦截只放行上面表格里的明示路径。这样后加的接口如果没有主动配置会自动被登录校验拦住反倒不容易漏。管理员功能在系统里不必做复杂权限模型给user表加个role字段管理端接口在Service层判断一下role 1即可字段到位后面对接后台管理页面时不用改表。5. 避坑排查校园二手交易系统最常见的五个翻车现场5.1 图片上传成功却刷新即404现象上传接口返回了一个类似/upload/xxx.jpg的路径浏览器直接打开403或者404但磁盘上文件明明存在。原因这是静态资源映射没配置。Spring Boot默认只映射classpath:/static/目录上传到自定义磁盘路径的文件不在这个范围内请求自然找不到资源。另外如果你用EnableWebMvc注解它会把Spring Boot的自动资源映射整个关掉导致问题更隐蔽。解决参考4.2的配置用addResourceHandlers把/upload/**映射到磁盘目录。真遇到403还有一个可能Linux服务器上上传目录的权限不对确保运行Java进程的用户对目录有读权限。排查时先看磁盘文件是否存在再看资源映射是否生效别一上来就去改前端路径。5.2 分页失效page(2,10) 返回了全表数据现象前端传了pageNum2pageSize10接口返回了全表所有记录total字段也变成0。原因MyBatis-Plus的分页插件没有真正注册。3.5.x版本必须通过MybatisPlusInterceptor装载PaginationInnerInterceptor很多人照抄旧博客用的PaginationInterceptor在3.5.x里已经废弃不生效。还有一种情况是引了插件但没指定DbType.MYSQL分页方言识别失败。解决回到3.2的配置代码检查是否加了Bean、是否指定数据库类型。配置完重启后把goodsMapper.selectPage的SQL日志打开确认日志里有LIMIT字样有就是生效了。这条问题属于最典型的“黑匣子故障”——接口不报错就是数据不对唯一的排查手段是看SQL日志。5.3 买家买到了自己的商品现象用户登录后在商品列表页看到了自己发布的商品点“立即购买”居然也能下单成功。原因下单接口只校验了“商品存在且状态在售”没校验“当前登录用户是不是商品卖家”。自己买自己的东西在校园场景没意义而且会让商品状态变成已售把自己挂在商品列表里。解决下单前拿RequestAttribute里的userId跟商品sellerId比对相同就抛业务异常。前端也可以在商品卡片上对当前用户发布的商品隐藏购买按钮但前端隐藏只是体验优化后端校验才是兜底。这里的关键是不要把“用户是否登录”和“用户是否有权操作”混为一谈登录只是第一道门业务归属校验是第二道门。5.4 订单并发提交一件商品被两个人同时买走现象两个用户同时对同一件商品下单系统里出现两条订单商品状态都是已售。原因退回3.3的代码看下单流程是先selectById查到status0通过校验后插入订单、更新状态。两个请求并发执行时都读到了status0都通过校验事务隔离级别默认的REPEATABLE READ只能保证同一事务内两次读一致阻止不了这种“先读后写”的竞态。解决给包装一层行锁用数据库的悲观锁串行化并发请求。把selectById换成下面的写法Goods goods goodsMapper.selectByIdForUpdate(goodsId);对应SQL是SELECT * FROM goods WHERE id ? FOR UPDATE在MySQL InnoDB下这个查询会对该行加排他锁第二个事务必须等第一个事务提交才能读到数据自然就拿到最新的status1了。另一种方案是乐观锁给goods表加version字段更新时SET status1 WHERE id? AND status0影响行数为0说明已经被别人抢购。悲观锁实现简单校园项目没有超高峰流量选它最省心。5.5 商品标题带emoji数据库直接报错现象用户在标题里加了一个表情符号保存时报SQL异常Incorrect string value: \xF0\x9F\x98\x80。原因MySQL的utf8字符集实际是utf8mb3最多支持3字节字符emoji是4字节超出范围直接被拒绝。建表时写了DEFAULT CHARSETutf8标题字段就吃不下emoji了。解决建库建表统一用utf8mb4DDL里我已经全部写成CHARSETutf8mb4。数据库连接串也要配套修改把characterEncodingutf8改成characterEncodingutf8mb4MySQL驱动8.x支持这个写法。改完重启后重新插入emoji测试。如果项目已经上线且库里已有数据要先ALTER TABLE goods CONVERT TO CHARACTER SET utf8mb4但字段长度会扩大索引和字段长度的边界要提前算好尤其注意unique索引长度不能超过限制。6. 验证与进阶把“能跑”推到“能演示、能答辩”6.1 订单状态流转的自动化测试与事务回滚验证系统做完了能不能演示是一回事有没有验证过是另一回事。最简单的验证手段是写一个SpringBootTest把最核心的下单、确认收货流程跑通SpringBootTest Transactional class OrderFlowTest { Autowired private GoodsMapper goodsMapper; Autowired private OrderService orderService; Test void orderShouldTransitToFinished() { Goods goods new Goods(); goods.setSellerId(1L); goods.setTitle(二手路由器); goods.setPrice(new BigDecimal(50.00)); goods.setStatus(0); goodsMapper.insert(goods); Long orderId orderService.createOrder(goods.getId(), 2L); orderService.confirm(orderId, 2L); Orders order orderService.getById(orderId); assertEquals(2, order.getState().intValue()); } }测试方法上的Transactional让测试结束自动回滚不会污染演示数据库——这是最实用的“后悔药”。跑通这套测试说明下单、确认订单和事务回滚这些核心链路正常工作。想验证事务回滚有没有生效就在createOrder里手动抛一个RuntimeException看测试结束后的订单表里没有新增记录就是回滚成功的标志。6.2 再往前一步让系统能接住线上流量演示和答辩不是终点。如果还想往上走有一个优先级很明确的清单先给下单接口加行锁补丁见5.4这是数据正确性的底线再用Redis缓存热门商品Top10减少数据库压力把登录Token同时放进Redis实现退出即失效图片存储换对象存储磁盘方案撑不住大规模部署。更实际的一步是把全套东西写成Docker Compose编排MySQL和Java应用各一个容器新环境一键启动比手动装JDK、配MySQL环境变量省太多事。我做这类校园项目时也栽过跟头印象最深的就是那一次并发下单的现场翻车演示台上两个人同时点购买一件商品凭空生成了两条订单场面非常尴尬。后来才明白能跑通接口和能扛住边界是两个世界系统做完了不算完把状态机、并发、异常路径逐条验证过才敢说这套系统是能拿出来见人的。希望今天这些坑位和补丁能帮你把同样的事情一次做对。本文还有配套的精品资源点击获取
返回列表