ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot + MyBatis-Plus 打造二手交易平台:从建模到防超卖实战

Spring Boot + MyBatis-Plus 打造二手交易平台:从建模到防超卖实战 简介基于Java Spring Boot的二手交易平台完整源码面向毕业设计、课程设计及需要快速上手全栈项目的初学者解决从零搭建可运行交易系统的难题。围绕用户注册登录、商品发布与浏览、多条件搜索、购物车下单、订单管理等主要环节设计覆盖了典型电商业务闭环。资源共809个文件以Java后端源码、Vue前端组件、JavaScript脚本、HTML页面、CSS样式为主同时包含SVG图标、GIF效果图、SQL数据库脚本和项目说明文档压缩包整体16.84MB目录按控制层、服务层、持久层与视图模块划分结构清楚便于按需研读或替换功能。目前已有41人学习或下载属于可正常运行的课程设计/毕业设计方案适合毕业设计或课设演示使用。项目自带MySQL建表与初始化脚本、启动/安装批处理命令以及可参考的备份文件配置好开发环境后即可运行。读者既能从完整代码了解Spring BootVue前后端联调思路也能直接将其作为课程设计展示、论文支撑或二次开发基础。1. 为什么二手交易平台是Java后端最稳的毕业设计选题每年毕业设计选题季总有一批人抱着“做个商城”的心态去找导师结果被一句“满大街都是”怼回来。而二手交易平台不同——它看似也是买卖但比普通商城多了一整条“人 → 物 → 钱 → 信任”的闭环。买家要注册登录、卖家要发布闲置、双方要讨价还价、下单后订单状态要一路流转、交易完成要留凭证。这些环节正好把Java后端开发里最常考的实体建模、关联查询、事务一致性、鉴权拦截、文件上传全部串起来课程设计能交差毕业设计能扩写成论文。更关键的是二手平台的业务规则比标准商城更灵活商品可下架重上架、价格可面议、订单可取消可退款。这些“非标流程”恰好是答辩时展示设计能力的素材。这篇笔记按我实际做完这类项目的顺序来讲——先定技术选型再建表建模然后逐段落地核心代码最后是花了两晚才解决的坑。读者如果正卡在“不知道从哪里开始”的阶段可以直接照着章节顺序把工程搭起来每一步都会解释为什么这么做而不是只贴代码。2. 技术选型与工程骨架Spring Boot MyBatis-Plus MySQL 怎么搭最省事2.1 为什么不用 SSM / SSH 而用 Spring Boot很多学校课程还在教 SSM 整合但到了做课程设计和毕业设计的节点时间比什么都值钱。SSM 要手动配置 DataSource、SqlSessionFactory、MapperScanner、事务管理器Spring MVC 还要配视图解析器和拦截器这一套下来至少占掉两三天而且配置错了报错信息又长又绕。Spring Boot 用自动配置和起步依赖把这些全干了一个spring-boot-starter-web就内嵌 Tomcat 并注册好 DispatcherServlet数据库连接池、事务管理器、MyBatis 的 Mapper 扫描都能通过注解或配置项打开。对于项目周期只有 8 到 12 周的课题来说把时间省下来写业务代码、画 E-R 图、准备答辩收益高得多。另一个考量是 Spring Boot 的生态太成熟了。做二手交易平台要用的分页插件、代码生成器、参数校验框架社区都有现成的 starter。遇到问题搜解决方案十篇里有八篇是 Spring Boot 环境下的答案这对新手非常重要。SSM 年代遇到一个NoMyBatisMapperException可能要查半天Spring Boot 下往往是漏了MapperScan或者依赖没引全错误信息也更友好。2.2 具体搭建步骤和依赖清单创建工程我一般用 Spring Initializr选 Java 8 或 11对应学校环境最稳依赖选Spring Web、MySQL Driver、MyBatis Framework。然后手动在pom.xml里补上 MyBatis-Plus 的依赖因为很多课程设计环境里默认只有 MyBatis而 MyBatis-Plus 能让单表 CRUD 连 SQL 都不用写dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency这段依赖解决的是最繁琐的通用 Mapper 问题。MyBatis-Plus 提供BaseMapperT里面预置了selectById、selectPage、updateById、deleteById等常用方法二手交易平台里的用户表、商品表、订单表大部分操作都是单表增删改查直接继承就能用只有复杂统计查询才需要写 XML。版本号建议用 3.5.x太老的 3.3.x 在 Spring Boot 3 下会冲突。application.yml里最关键的几个配置项如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/second_hand?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0参数说明serverTimezoneAsia/Shanghai必须加MySQL 8.0 默认时区与驱动不一致时会报The server time zone value йʱ is unrecognized。useSSLfalse是本地开发固定写法避免 MySQL 8 默认开启 SSL 导致握手警告。multipart的两个参数分别控制单文件和单次请求的最大体积二手交易的商品实拍图一般手机拍的都大于 1MB这里给了 10MB 的冗余。map-underscore-to-camel-case让数据库的seller_id自动映射到实体的sellerId能少写大量结果集映射。logic-delete-field打开逻辑删除后调用deleteById实际执行的是UPDATE ... SET deleted 1对二手平台来说商品删除应该是隐藏而不是物理消失这个配置后面会专门讲。启动类就是标准的 Spring Boot 入口加一个MapperScanSpringBootApplication MapperScan(com.secondhand.mapper) public class SecondHandApplication { public static void main(String[] args) { SpringApplication.run(SecondHandApplication.class, args); } }MapperScan的包路径必须和你的 mapper 接口所在包完全一致否则启动时所有 mapper 都不会被注册调用任何查询方法都会报Invalid bound statement。工程内部按controller、service、mapper、entity、common、config六个包组织这不仅是整洁问题——答辩时老师通常会看包结构来快速判断你是否有分层意识。2.3 为什么轻量级项目坚持用单体而不用微服务一定要警惕把课设做成微服务的冲动。所有要部署多模块、引入 Nacos 和 OpenFeign 的方案在这个场景下都是自找麻烦。二手交易平台的用户量在答辩演示时可能就几百个用单体架构连一百行并发配置都不用写性能完全够。更重要的是微服务的服务拆分、注册发现、配置中心概念写进论文里如果自己讲不清楚答辩老师三句话就能问穿。单体工程里体现服务层接口、控制层参数校验、事务控制已经足够展示工程能力。3. 数据库建模与交易核心四张表如何支撑买卖闭环3.1 用户表、商品表、订单表、交易流水表的设计二手交易平台业务上可以拆成“人、物、单、账”四大部分对应四张核心表。很多没做过交易系统的同学会漏掉交易流水表只靠订单表里的状态字段打天下做到后面发现退款记录、申诉凭证无处安放只能临时加字段。我一般把表结构建为四个实体CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 用户ID, 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 头像访问路径, credit_score INT DEFAULT 100 COMMENT 信用分默认100, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE goods ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 商品ID, seller_id BIGINT NOT NULL COMMENT 卖家ID关联user表, title VARCHAR(100) NOT NULL COMMENT 标题, description TEXT COMMENT 描述, category VARCHAR(30) NOT NULL COMMENT 分类手机数码/家具/书籍..., price DECIMAL(10,2) NOT NULL COMMENT 售价, original_price DECIMAL(10,2) DEFAULT NULL COMMENT 买入价用于显示折扣, status TINYINT NOT NULL DEFAULT 0 COMMENT 商品状态0上架 1下架 2已售出, view_count INT DEFAULT 0 COMMENT 浏览次数, cover_image VARCHAR(255) COMMENT 封面图路径, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted TINYINT DEFAULT 0, PRIMARY KEY (id), KEY idx_seller_status (seller_id, status), KEY idx_category_created (category, created_at) ) 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 COMMENT 商品ID, seller_id BIGINT NOT NULL COMMENT 卖家ID, buyer_id BIGINT NOT NULL COMMENT 买家ID, amount DECIMAL(10,2) NOT NULL COMMENT 成交金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 订单状态0待付款 1已付款 2已发货 3已完成 4已取消, message VARCHAR(255) DEFAULT NULL COMMENT 买家留言, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_buyer_status (buyer_id, status), KEY idx_seller_status (seller_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表; CREATE TABLE trade_flow ( id BIGINT NOT NULL AUTO_INCREMENT, order_id BIGINT NOT NULL COMMENT 关联订单表主键, flow_no VARCHAR(32) NOT NULL COMMENT 流水号, buyer_id BIGINT NOT NULL, seller_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL COMMENT 金额, direction TINYINT NOT NULL COMMENT 方向1付款 2退款 3平台佣金, remark VARCHAR(255) COMMENT 备注如支付模拟、退款原因, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT交易流水表;表设计里有几个点答辩时一定要能讲清楚。用户密码字段设成VARCHAR(100)是因为 BCrypt 加密结果固定 60 字符留出余量给未来升级算法。商品表里刻意区分了price和original_price这对应二手交易里“几乎全新”的展示逻辑——用户看到买入价和卖价对比才有购买欲望。订单表单独设计order_no而不用自增主键给用户看一是避免暴露平台真实订单量二是业务单号以后接支付回调时必须用幂等键自增 ID 不适合对外暴露。索引方面idx_seller_status解决卖家“我发布的商品”列表页——这是每个卖家的高频操作idx_category_created对应首页分类浏览和时间排序。为什么不给description加全文索引因为 MySQL 全文索引在中文分词上效果一般而且课设规模用LIKE %关键词%糊一层已经够用等答辩老师问“搜索怎么做的”再实事求是说这是改进方向。3.2 为什么价格字段必须用 DECIMAL 不能用 Double浮点数精度问题是 Java 开发里最经典的血泪经验。float和double在二进制存储中无法精确表示 0.1两个浮点数相减会出现0.30000000000000004这样的结果。交易系统里金额一旦失之毫厘订单金额和支付模拟金额对不上就会被老师当场发现。数据库层用DECIMAL(10,2)Java 实体里用BigDecimal两层都精确表示十进制小数加减乘除不会丢精度。还有一个细节BigDecimal 构造时要用new BigDecimal(19.9)或者BigDecimal.valueOf(19.9)绝对不能new BigDecimal(19.9)后者是用 double 的二进制值转进去的精度已经丢了。3.3 逻辑删除与数据保留的取舍二手交易平台的商品删除有特殊性。用户卖出一件闲置如果删除后所有评论、交易记录都物理消失后期产生纠纷就无从追溯。所以商品表、用户表都加了deleted字段MyBatis-Plus 配置里打开了逻辑删除后框架会在所有查询里自动追加AND deleted 0。有个坑必须提前说逻辑删除字段只对 MyBatis-Plus 自动生成的 SQL 生效如果你手写了 XML 里的自定义查询框架不会帮你改 SQL必须要自己在where条件里显式加deleted 0。我第一版上线就因为这个漏了很多条数据出来排查方法很简单——打开log-impl的 SQL 输出看执行的 SQL 里有没有带上逻辑删除条件。4. 关键功能落地订单状态机、防超卖与文件上传的代码实现4.1 订单状态流转用枚举和状态机把业务规则写死订单状态如果只靠 if-else 判断初期功能少看不出问题一旦加上取消、退款、超时关闭代码就变成一坨剪不断理还乱的判断。正确做法是定义订单状态枚举把合法的状态流转路径集中管理public enum OrderStatus { PENDING_PAYMENT(0, 待付款), PAID(1, 已付款), SHIPPED(2, 已发货), COMPLETED(3, 已完成), CANCELLED(4, 已取消); private final Integer code; private final String desc; OrderStatus(Integer code, String desc) { this.code code; this.desc desc; } public static OrderStatus fromCode(Integer code) { for (OrderStatus status : values()) { if (status.code.equals(code)) { return status; } } throw new IllegalArgumentException(未知订单状态: code); } public boolean canTransitTo(OrderStatus target) { return switch (this) { case PENDING_PAYMENT - target PAID || target CANCELLED; case PAID - target SHIPPED || target CANCELLED; case SHIPPED - target COMPLETED; default - false; }; } }这段代码的意图很明确状态流转规则全部集中到canTransitTo方法里。待付款订单能去已付款或已取消已付款订单能去已发货或已取消已发货只能去已完成。这样在 Service 层做订单更新时不需要散落的 if 判断调用方只需要执行OrderStatus current OrderStatus.fromCode(order.getStatus()); if (!current.canTransitTo(target)) { throw new BizException(订单状态不允许从 current.getDesc() 流转到 target.getDesc()); }状态机带来三个直接好处第一非法流转在业务入口就被拦截不会污染数据库第二答辩时老师问“订单状态你怎么设计的”可以直接把枚举类展示出来比口述“我用数字 0 到 4 表示状态”强得多第三后续就算要加“退款中”状态只需要在枚举里加一个值并修改一条流转规则不影响其他代码。顺带说一句这个枚举的switch语法已经是 Java 14 的增强版大部分学校机房装的 JDK 8 跑不了用的时候改成传统的if (this PENDING_PAYMENT target PAID) return true;写法即可别因为这个翻车。4.2 秒杀式的商品扣减防止两个人同时买到同一件闲置二手平台虽然不像电商秒杀那样高并发但答辩时老师一定会问“两个买家同时下单同一件商品怎么处理”。如果你用“先查询库存是否大于0再更新库存”的方式两个线程读到的库存都是 1然后都执行减一库存变成 -1商品超卖。原因在于查询和更新之间有时间窗口这不是玄学是典型的并发竞态问题。解决办法是条件更新把判断和扣减合并到一条 SQL 里Update(UPDATE goods SET stock stock - 1 WHERE id #{goodsId} AND stock 0) int deductStock(Long goodsId);这条 SQL 的含义是“在确保库存大于 0 的前提下把库存减一”。MySQL 的行锁保证同一时刻只有一个线程能对同一行执行更新第二个线程执行时stock 0不成立影响行数为 0。在 Service 层调用后判断返回值int rows goodsMapper.deductStock(goods.getGoodsId()); if (rows 0) { throw new BizException(商品已售罄); } // 行数大于0才允许创建订单参数说明stock字段在上一章的表结构里没有展示这里补充一下goods表还需要加stock INT NOT NULL DEFAULT 1因为二手商品大部分只卖一件但允许卖家发布多件。deductStock方法返回int是 MyBatis 的执行行数这个返回值非常关键它的语义不是“扣减了多少数量”而是“更新影响了几行数据”。条件更新比乐观锁version字段的写法更简单——乐观锁要先查出 version更新时对 version 做比对失败还要重试而条件更新一条 SQL 就完成了。它的局限在于只适用于“库存扣减”这种单字段原子操作的场景如果复杂状态流转还是要配合乐观锁。4.3 图片上传与访问路径设计本地存储的完整方案课设阶段不推荐引入 OSS 对象存储因为涉及云厂商账号和计费问题答辩环境依赖网络可能有风险。本地文件存储足够但要注意路径设计的坑。我最初的做法是把图片放到项目static目录下结果每次mvn clean后上传的图片全没了因为target目录被清理重建。血泪经验得出的结论是上传目录要放在项目外部比如D:/secondhand-upload/Linux 上就放/home/secondhand-upload/再通过一个 WebMvc 配置把这个目录映射成 URL 访问。上传接口的核心代码PostMapping(/api/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(文件不能为空); } String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String month LocalDate.now().format(DateTimeFormatter.ofPattern(yyyyMM)); String filename UUID.randomUUID().toString().replace(-, ) ext; String relativeDir month /; File dir new File(uploadDir relativeDir); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, filename)); return Result.success(/upload/ relativeDir filename); }代码逻辑按四步走校验文件非空 → 提取扩展名防止有些人传无后缀文件→ 按月份建子目录并生成 UUID 文件名 → 保存文件到磁盘并返回访问路径。uploadDir是配置在application.yml里的绝对路径file: upload-dir: D:/secondhand-upload注意扩展名提取用的是lastIndexOf(.)而不是indexOf(.)因为文件名可能叫“毕业答辩.最终版.png”点不止一个。UUID 文件名的作用是防止不同用户上传同名图片相互覆盖同时避免中文文件名在 URL 传递时产生编码问题。返回的路径要把系统绝对路径藏起来只暴露/upload/202511/xxx.jpg这样的相对路径真正的磁盘映射由配置类完成Configuration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(uploadDir /); } Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/api/login, /api/register, /upload/**, /static/**, /error); } }这段代码同时处理了两件事把/upload/**映射到磁盘目录、配置登录拦截器放行哪些地址。拦截器是课设项目另一个高频踩坑点。addPathPatterns(/**)会拦截一切路径如果你不给/upload/**和/static/**放行页面里的 CSS、JS、商品图片全部会被 302 重定向到登录页浏览器控制台刷出一片红色报错。开发阶段排查这类问题观察响应状态码是 200、302 还是 404比你反复刷新页面有用得多。5. 避坑记录代码能跑但结果不对问题多数出在这 5 处5.1 登录状态突然失效Session 过期时间没配现象用着用着页面就跳回登录页尤其是上传图片或者提交订单后操作被拦截器拦截。原因默认 Session 有效期是 30 分钟但文件上传等耗时操作会拉长会话间隔服务器端 Session 已经过期。另一个隐蔽原因是应用重启导致 Session 全部清空因为 Tomcat 默认把 Session 存在内存里。解决在application.yml里显式配置会话超时server: servlet: session: timeout: 60mtimeout单位是分钟60m表示 60 分钟。如果是前后端分离用 JWT这种情况不会发生但 JWT 有它自己的过期时间问题需要前端在 401 时自动跳转登录页。课设项目用 Session Cookie 最简单不要自己造 Token 轮子。5.2 订单金额变成 19.900000000000002现象数据库里金额明明是 19.90Java 里计算完税费或者折扣后打印出来一长串浮点数。原因实体类里amount字段用了Double虽然数据库是 DECIMAL但 JDBC 驱动读出来转换成了 Double 精度丢失。解决实体类金额字段一律用BigDecimalDTO 里的传输对象也用BigDecimal。前端传金额时用字符串而不是数字因为 JSON 的19.9解析到 JS 的 Number 再传回后端精度又丢一次。Controller 接收时用RequestBody配合BigDecimal类型参数字段写成amount: 19.90。这套链路非常关键数据库 DECIMAL → 实体 BigDecimal → 前端字符串。5.3 更新订单状态时把别人的修改覆盖了现象买家取消订单成功后卖家那边看到的订单状态还是“待付款”再点发货把已取消的订单改成了“已发货”。原因取消订单和发货操作同时发生时两个请求都读取到了订单状态0待付款各自执行了自己的状态更新后执行的把先执行的覆盖了。这就是典型的并发覆盖问题。解决状态更新 SQL 要带上当前状态条件Update(UPDATE orders SET status #{newStatus} WHERE id #{id} AND status #{oldStatus}) int updateStatusByIdAndOldStatus(Long id, Integer oldStatus, Integer newStatus);只有WHERE status 旧状态成立才会更新两个并发请求只有一个能成功。拿到返回行数为 0 时说明状态已被别人改过直接提示“订单状态已变化请刷新后重试”。这个写法比在 Service 层加synchronized靠谱因为课设项目经常部署在多实例环境synchronized只对本机有效。5.4 图片上传成功但页面打不开现象文件明明传到了磁盘返回的地址http://localhost:8080/upload/202511/xxx.jpg却 404。原因没有配置资源映射。Spring Boot 默认只会把classpath:/static/下的文件当静态资源服务你上传到的是D:/secondhand-upload或者 Linux 的/home/upload它不是类路径里的目录自然无法访问。解决用前面 4.3 节写的WebConfig通过addResourceHandlers把/upload/**映射到磁盘目录。还有一个变体坑如果忘了加file:前缀或者路径最后没加/映射也会失败。Linux 路径正确写法是file:/home/secondhand-upload/Windows 是file:D:/secondhand-upload/。5.5 列表分页后前端拿不到数据现象selectPage查询后接口返回的 JSON 里始终没有 records 里的内容或者直接报ClassCastException。原因MyBatis-Plus 的selectPage(page, wrapper)返回的是IPageT但很多人习惯当List用前端拿到的对象里records是空的而真正的数据其实在records字段里。另外一个常见原因是分页插件没配置selectPage执行时 LIMIT 没拼上去返回的是全量数据再在前端做假分页。解决分页插件只能在 MyBatis-Plus 里通过配置类注册Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }前端取数时用page.getRecords()而不是把page直接当列表。这个配置类只加一次加完后selectPage才会生成真正的LIMIT ? OFFSET ?SQL。我见过有人把整个Page对象转成 JSON 返回给前端然后前端怎么都解析不出数据排查半小时发现前端代码没错是后端少了分页拦截器。6. 答辩前的最后一步把单体工程演进成能讲出亮点的架构做到这里工程已经能完整跑通“注册 → 发布商品 → 浏览 → 下单 → 发货确认”这条主链路。但课设和毕设的评分差异往往体现在最后这一步——不是代码量多寡而是你能否讲清“如果用户量大了怎么演进”。我一般会在论文里留出专门一节写架构演进方向不需要全部实现但至少要讲明白设计意图。这里给几条经过验证的切入点。第一引入 Redis 做首页热点商品缓存。现在的实现里每个用户打开首页都会查询数据库用户量到上千后首页响应会明显变慢。用 Redis 把 top 20 的热门商品缓存起来TTL 设 5 分钟浏览计数先在 Redis 里累加定时落库能把数据库查询压力降一个量级。答辩时画出“请求先查缓存、未命中再查库”的流程图比写一百行代码更有说明力。第二把商品搜索从LIKE %关键词%升级为 MySQL 全文索引或 Elasticsearch。课程设计用 LIKE 完全够但论文里可以说这是系统下一步优化方向因为 LIKE 前置通配符会放弃索引全表扫描数据量过万后体验会断崖式下跌。如果愿意多花一天时间可以引入ngram全文解析器实现中文分词搜索——这算是一个稳妥的加分项。第三订单状态机的扩展性。当前的枚举虽然把流转规则收拢了但“超时自动取消”“买家申请退款后卖家拒绝”这类逆向流程还没有完全覆盖。回答这类问题时可以用状态机的经典理论——每个状态定义合法事件所有状态转移都在触发器里校验跟 4.2 节一样。我再补一句血泪经验我当年做的时候没在商品表加version字段结果两个人同时编辑商品信息时后保存的人覆盖了先保存的人答辩老师一眼就看出来了。如果不想在表里加版本号至少应该在 Service 层做字段级更新只更新真正变化的列。回头看整个方案技术难度都控制在课设范围内但因为建模完整、并发处理有意识、状态设计可扩展整体完成度会明显高于“纯 CRUD 系统”的平均水平。希望这篇笔记能帮你少走几个我当年绕过的弯路也祝你的答辩一切顺利。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进