ARTICLE DETAIL

资讯详情

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

校园二手交易平台SpringBoot实战:数据库设计与索引优化

校园二手交易平台SpringBoot实战:数据库设计与索引优化 简介基于SpringBoot的校园二手交易平台项目是一套完整Java课程设计与毕业设计参考源码面向计算机专业高年级学生解决校园闲置物品交易系统从零搭建与二次开发问题。项目涵盖用户管理、商品展示、交易撮合、订单处理等核心模块数据库按第三范式设计并使用MySQL存储前端采用响应式布局支持多终端技术栈涉及SpringBoot 2.x、MyBatis Plus、Redis缓存及Shiro权限控制整体采用前后端分离架构。压缩包共262个文件约3.88MB文件类型以Java源码、SQL脚本、前端HTML/JS/CSS资源及gif演示图为主同时包含构建脚本、配置文件与项目文档可直接导入IDE运行并对照学习。资源附有系统设计说明书、数据库设计文档、部署指南及API接口文档代码注释完整、目录清晰方便二次扩展。已有81人学习浏览适合课程设计、项目实训及毕业设计参考。1. 校园二手交易平台该做什么不止是 CRUD而是 Java 源码与数据库设计的咬合做校园二手交易平台这类 SpringBoot 项目价值不在把 CRUD 写完而是搞明白一套 Java 项目源码里用户、商品、订单和数据库设计是怎么咬合在一起的。这类平台要解决的是真实痛点教材、耳机、电动车等闲置信息落在微信群就被闲聊淹没平台则用最朴素的商品记录把需求产品化。这篇笔记按复现这类项目的顺序展开理清模块边界落数据库设计接 SpringBoot 后端闭环再单独排查高频调试坑和搜索性能。目标是让做毕设、练手或备 Java 后端实习的读者能把源码跑通、能改也能说清楚每张表和每个接口为什么这么设计。2. 源码先拆模块用户、商品、订单和管理员的边界在哪里拿到基于 SpringBoot 的校园二手交易平台源码第一件事不是点启动而是先回答一个问题这个项目到底分几个功能域。模块边界决定了数据库表的数量、接口路径的划分、事务该加在哪个 Service 方法上。很多源码看起来文件几十个拆完之后核心其实就是三块用户、商品、订单外加一个管理员后台。2.1 用户、商品、订单三大域的职责划分用户域是入口。注册、登录、角色区分这三件事是必须做的校园场景还有一个特殊点学生身份验证。常见做法是注册时填学号后台按学号规则做格式校验不强制做实名认证。这个域在源码里通常对应 UserController、UserService、UserMapper 一组文件密码存 bcrypt 哈希而不是 MD5登录成功签发 JWT后续请求通过拦截器校验 token。Java 后端面试时经常问到的“密码为什么要加盐”“JWT 和 Session 有什么区别”都会落在这部分代码里。商品域是信息载体。卖家能发布商品、传图、设置分类和价格、主动下架买家能浏览、搜索、收藏。商品必须有一个状态位来控制生命周期这个状态驱动着整个列表页、详情页和下单逻辑。商品域最容易忽略的是图片处理。图片涉及存储和网络访问数据库里不能只存一个本地磁盘路径了事否则项目换个机器就全线 404。分类也值得想清楚固定枚举适合毕设可扩展的分类表更接近真实项目这个决定会在数据库设计阶段直接体现。订单域是交易的核心。二手交易的特点是线下见面交割所以不需要完整支付流程。订单只需要对准“买家要买、卖家同意、交易完成”这几个动作。这个域通常会生成业务订单号、保存下单时的金额快照同时把商品状态从“在售”改成“已预订”防止别人重复下单。我见过不少源码把订单写成简单的“插入一条记录”没有状态流转也没有并发保护这种代码跑演示可以答辩时一问就露馅。功能域必须包含可以砍掉用户域注册、登录、角色、找回密码第三方登录、邮箱验证商品域发布、分类、审核、上下架、搜索推荐、秒杀、竞价订单域下单、确认、取消、状态查询在线支付、退款、售后砍掉的部分不是不重要而是在这个业务模型里属于“复杂度大头但技术点重复”。二手交易的关键是信任和撮合不是支付通道。2.2 交易状态机与管理后台的必要性状态机是这个项目里最值得拿出来讲的设计点。商品状态我一般定义为 0 待审核、1 在售、2 已预订、3 已售、4 下架这五个。流转关系是0 到 1 走管理员审核1 到 2 是买家下单成功2 到 3 是卖家确认交易完成1 到 4 是卖家主动下架。这些状态如果散落在业务代码里用 if 到处判断很快就会乱。我在源码里面更倾向用枚举或常量类统一管理并且所有状态流转都收在 Service 层不允许 Controller 直接改商品状态字段。管理员审核这一步不是摆设。校园场景里有大量二手贩子和广告号管理员角色的意义就是把“待审核”的商品挡在列表外面。这也是区分“写着玩的 CRUD”和“能用起来的项目”的分界线。后台不需要单独做一套技术栈在源码里它就是一组要求 role2 才能访问的接口操作对象还是商品表和用户表。订单状态建议做成 0 待确认、1 已完成、2 已取消。这里有个容易翻车的并发点两个买家同时看到一件商品然后同时下单。常见做法是在下单事务里带着“status1”的条件去更新商品表如果受影响行数是 0说明商品已经被抢或下架直接抛业务异常。先查后改不行——两个请求都查到 status1然后都往下走就会生成两笔订单。2.3 拿到源码后的阅读顺序先看配置还是先看代码拿到这类 Java 项目源码后我习惯按五步走每一步都有明确目的。先看 application.yml确认数据源、端口、MyBatis-Plus 配置是否齐全。很多项目跑不起来都是数据库密码没改或时区配置缺失代码本身没问题。看 entity 目录数一数有几个实体类。实体类的数量基本等于数据库核心表的数量先对整体规模有概念。看 mapper 目录区分哪些是 BaseMapper 直接提供的 CRUD哪些是手写的自定义 SQL。手写 SQL 的地方往往是性能或业务的关键值得重点读。找带 Transactional 的方法把事务边界画出来。创建订单、修改商品状态这类写操作事务加在 Service 方法上才算对加在 Controller 上就很不合理。看 config 目录重点找拦截器和 MyBatis-Plus 配置类。JWT 校验、分页插件、自动填充全在这里属于“跑起来但行为不对”时最先怀疑的地方。这套顺序对 SpringBoot 项目结构不熟的读者尤其有用。先建立全局认知再钻到具体代码里不容易被零散的文件带偏。3. 把数据库设计写在前面五张核心表与索引落地的取舍数据库设计是整套源码的底盘。实体关系先理顺了SpringBoot 的代码写起来只是体力活实体关系如果乱后面每加一个功能都要回头改表。校园二手交易平台的表其实不多五张核心表加两张附属表足够覆盖全部业务关键是每张表的字段和索引对得上查询场景。3.1 建表之前先定约束保留字、逻辑外键与字符集建表之前有四个约束需要先定下来否则后面改起来很痛苦。第一表名避开 SQL 保留字。订单表不要叫 orderORDER 是 SQL 的标准保留字写 SELECT * FROM order 会直接语法报错。我一般用 t_order干净且通用。第二字符集统一 utf8mb4不要用 utf8。utf8mb4 才能存表情符号和生僻字二手商品描述里出现这类字符并不罕见。第三金额字段用 DECIMAL(10,2)不要用 FLOAT 或 DOUBLE。浮点数在 Java 里做运算会出现 0.10.2 不等于 0.3 的问题金额这种敏感数据必须精确。第四不建物理外键用逻辑外键由应用层保证一致性。物理外键在插入和删除时要额外做约束检查影响性能而且一旦业务要拆分表就不灵活。这个取舍在面试里是加分项能讲清楚理由比直接建外键强得多。时间字段也有一个通用约定created_at 默认 CURRENT_TIMESTAMPupdated_at 加上 ON UPDATE CURRENT_TIMESTAMP这样更新记录时不需要手动维护时间。这套约定的核心目的是让 SpringBoot 代码里少写重复的时间赋值逻辑。提示MySQL 5.7 的 utf8mb4 单字符最多占 4 字节VARCHAR(255) 建立普通索引没有问题如果字段超过 255 且需要索引记得控制长度或用前缀索引。3.2 用户表、商品表、订单表的建表语句与索引选择用户表是最基础的用户名做唯一索引密码字段留足长度。学号不做唯一索引因为管理员和游客也可能占用 user 表的记录。CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, student_no VARCHAR(32) DEFAULT NULL COMMENT 学号校园身份标识, username VARCHAR(64) NOT NULL COMMENT 登录名, password_hash VARCHAR(128) NOT NULL COMMENT bcrypt 加密后的密码串, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像存相对路径, role TINYINT NOT NULL DEFAULT 1 COMMENT 1 学生2 管理员, status TINYINT NOT NULL DEFAULT 1 COMMENT 1 正常0 封禁, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;password_hash 设成 128 是因为 bcrypt 输出固定 60 字符留出长度是为了未来换加密算法不用改表结构。username 唯一索引既保证业务上登录名不冲突又给登录查询提供快速检索。商品表是查询压力最大的一张表。这里要注意 status 字段的默认值新发布的商品默认进待审核而不是直接在售。CREATE TABLE product ( id BIGINT NOT NULL AUTO_INCREMENT, seller_id BIGINT NOT NULL COMMENT 卖家对应 user.id, title VARCHAR(128) NOT NULL COMMENT 标题, description TEXT COMMENT 描述选填, price DECIMAL(10,2) NOT NULL COMMENT 售价, original_price DECIMAL(10,2) DEFAULT NULL COMMENT 原价便于买家评估成色, category_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0 待审核1 在售2 已预订3 已售4 下架, view_count INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_seller (seller_id), KEY idx_category_status (category_id, status), KEY idx_status_created (status, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT二手商品表;三个索引各有用途。idx_seller 覆盖“我的商品列表”按卖家查是高频操作。idx_category_status 是组合索引覆盖“分类页按状态过滤”的查询。idx_status_created 覆盖首页场景“只查在售商品并按发布时间倒序”这个索引在最前面放 status 等值条件后面接排序字段过滤和排序都能走索引。订单表这里有一个关键字段amount 是下单时的金额快照。订单一旦创建这个值就不应该再随商品改价而变动。CREATE TABLE t_order ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号展示给用户, product_id BIGINT NOT NULL, buyer_id BIGINT NOT NULL, seller_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL COMMENT 下单时商品价格快照, status TINYINT NOT NULL DEFAULT 0 COMMENT 0 待确认1 已完成2 已取消, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, paid_at DATETIME DEFAULT NULL COMMENT 卖家确认交易完成的时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_buyer (buyer_id), KEY idx_seller (seller_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;product_id 在订单表里不做唯一索引因为同一商品会被取消再重新下单物理上会留下多条记录。业务上的“唯一在售”由下单时把商品状态改成已预订来保证而不是靠数据库约束。查询场景索引方案设计理由卖家查看我发布的商品idx_seller单列等值查询返回行数少分类列表按状态过滤idx_category_status两个等值条件联合命中首页在售商品按时间倒序idx_status_created过滤和排序都走索引树订单按买家查询idx_buyer个人中心高频查询索引不是越多越好。这张表设计到四个二级索引已经够用索引数量再往上增加写入和更新时的维护成本会明显上升。3.3 商品图片与收藏两张附属表的落地方案商品图片单独建表而不是在 product 表里存一个 JSON 数组。这个取舍的理由很直接图片要排序、要删除、要能查封面独立表里一行一条记录sort_order 字段控制展示顺序0 表示封面数字越小越靠前。CREATE TABLE product_image ( id BIGINT NOT NULL AUTO_INCREMENT, product_id BIGINT NOT NULL, url VARCHAR(255) NOT NULL COMMENT 相对路径如 /images/2024/10/xxx.jpg, sort_order INT NOT NULL DEFAULT 0 COMMENT 0 为封面数字越小越靠前, PRIMARY KEY (id), KEY idx_product (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品图片表;url 字段存相对路径不存绝对路径这条规则值得反复强调。开发机的 D:/upload 和服务器上的 /root/upload 完全不是一回事存相对路径让项目换环境之后只改一个上传目录配置就能继续用。后续如果接对象存储把 URL 换成 CDN 前缀拼接即可不需要重刷数据库。收藏表的唯一索引也要注意。同一个用户对同一件商品只能收藏一次这个约束放在数据库层最可靠应用层做判断总会有漏网之鱼。CREATE TABLE favorite ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, product_id BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_product (user_id, product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT收藏表;两张附属表都不建物理外键与核心表保持逻辑关联。删除商品时业务代码里手动删除图片记录和收藏记录这样数据流向是清晰的不会被数据库外键牵制。4. SpringBoot 后端怎么接上数据库从依赖配置到订单事务的最小闭环表结构定稿之后SpringBoot 这边就是三件事依赖配置、实体映射、服务接口。这也是源码里最值得一行行读的部分。实体类对应表Mapper 负责 SQLService 写事务Controller 只做参数接收和结果返回。4.1 项目结构与 Maven 依赖把 SpringBoot 骨架搭起来先看目录结构。一个可维护的 SpringBoot 项目应该有清晰的包划分config 放拦截器和 MyBatis-Plus 配置controller 放接口层service 放业务逻辑和事务mapper 放数据访问接口和 XMLentity 放实体类common 放统一返回结构和异常处理。如果源码里把全部类都堆在同一个包下运行没问题但扩展和维护会很吃力。Maven 依赖是整个骨架的地基。核心依赖就四个spring-boot-starter-web、mybatis-plus-boot-starter、mysql 连接器、lombok。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency这里有一个版本匹配问题需要特别注意。SpringBoot 3.x 下 mysql 连接器的坐标变成了 com.mysql:mysql-connector-j不再用 mysql:mysql-connector-java。JDK 版本直接决定 SpringBoot 大版本JDK 8 配 SpringBoot 2.7.xJDK 17 配 SpringBoot 3.x对应的 Maven 构建方法和依赖坐标都不一样。先确认环境再贴依赖避免依赖冲突。依赖配好之后是 application.yml这是 SpringBoot 启动时读取配置的入口也是排查问题时的第一个关注点。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/campus_trade?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 id-type: auto数据源 URL 里必须带上 serverTimezoneAsia/Shanghai否则 MySQL 8 驱动会报时区错误。map-underscore-to-camel-case 配置让表里的 created_at 自动映射到实体类的 createdAt 字段这是 SpringBoot 项目的常见配置省去手写大量 ResultMap。4.2 实体类与 MyBatis-Plus从 Product 表到 Java 对象的映射实体类是数据库表在 Java 世界的投影。用 MyBatis-Plus 时实体类通过注解映射到表名字段名走驼峰规则。下面以商品表为例。Data TableName(product) public class Product { TableId(type IdType.AUTO) private Long id; private Long sellerId; private String title; private BigDecimal price; private Long categoryId; private Integer status; TableField(fill FieldFill.INSERT) private LocalDateTime createdAt; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt; }Data 是 Lombok 提供的注解自动生成 getter 和 setter让实体类保持干净。TableName 指定表名TableId 配置主键生成策略AUTO 对应数据库自增主键。price 必须是 BigDecimal不能用 Double。TableField 的 fill 属性配合 MetaObjectHandler 实现字段自动填充这样插入和更新时不需要手动 set 时间。Mapper 层也简单。继承 BaseMapper 之后单表 CRUD 全部自带不需要手写 SQL。但业务里有些复杂查询还是要自定义比如关键词搜索。Mapper public interface ProductMapper extends BaseMapperProduct { Select(SELECT * FROM product WHERE status 1 AND title LIKE CONCAT(%, #{keyword}, %) ORDER BY created_at DESC) PageProduct searchByKeyword(PageProduct page, Param(keyword) String keyword); }这种 LIKE %keyword% 写法无法走索引数据量过万后性能会明显下降。对这个项目来说商品量级不会太大这种宽松搜索可以接受如果量上来了可以换成全文索引或者把搜索功能独立出去。MyBatis-Plus 的条件构造器也能实现大部分查询但像这种带了分页参数和动态条件的方法我还是习惯在 Mapper 里用注解或 XML 写清楚。4.3 Service 事务与 Controller 接口创建订单的最小闭环创建订单是整个项目里事务最核心的场景。它同时做了两件事插入一条订单记录把商品状态从在售改成已预订。这两件事必须在一个事务里要么都成功要么都回滚。Transactional(rollbackFor Exception.class) public Long createOrder(Long productId, Long buyerId) { Product product productMapper.selectById(productId); if (product null || !product.getStatus().equals(1)) { throw new BizException(商品不存在或已不在售); } Order order new Order(); order.setOrderNo(IdGenerator.generate()); order.setProductId(productId); order.setBuyerId(buyerId); order.setSellerId(product.getSellerId()); order.setAmount(product.getPrice()); order.setStatus(0); orderMapper.insert(order); product.setStatus(2); int updated productMapper.updateById(product); if (updated 0) { throw new BizException(商品已被预订请刷新后重试); } return order.getId(); }金额不下传服务端从数据库重新读取当前价格写入订单这是防止改价不一致的关键。Transactional 默认只回滚 RuntimeException所以这里必须显式写成 rollbackFor Exception.class否则抛出受检异常时事务不会回滚。updateById 返回受影响行数如果商品状态已经在其他地方被改掉这里更新行数就是 0直接抛异常比先查再判断更可靠。分页插件也要单独配置。没有它MyBatis-Plus 的 Page 对象只返回当前页数据total 字段永远是 0翻页功能会静默失效。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }Controller 层保持薄。它只做三件事接收参数、调用 Service、包装返回结果。统一返回 Result 结构前端只需要处理 code、message、data 三个字段而不是一接口一个返回格式。GetMapping(/api/products) public ResultPageProduct list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 20) Integer size, RequestParam(required false) Long categoryId) { PageProduct p new Page(page, size); LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.eq(Product::getStatus, 1) .eq(categoryId ! null, Product::getCategoryId, categoryId) .orderByDesc(Product::getCreatedAt); return Result.ok(productMapper.selectPage(p, wrapper)); }LambdaQueryWrapper 的好处是类型安全Product::getStatus 这种写法如果字段名拼错编译期就能发现而不是运行到 SQL 才报错。JWT 校验这块我一般用 HandlerInterceptor 实现在 preHandle 里从 Authorization 头取出 token 校验注册到 WebMvcConfigurer 后只拦截 /api/** 路径。密码加密用 BCryptPasswordEncoder不要用 MD5MD5 的哈希值可以在彩虹表里直接查出来。5. 复现时的常见坑与排查记录版本、SQL 保留字和事务边界源码复现最容易翻车的地方是环境和配置业务代码反而一般不会太折腾。以下五条是我跑这类项目时反复遇到的前两条属于“跑不起来”后三条属于“跑起来但结果不对”。每条都按现象、原因、解决三个步骤说清楚。5.1 SpringBoot 版本太高JDK 和依赖先对不上号现象导入源码后 Maven 一直报依赖错误或者点击启动后主类直接抛 UnsupportedClassVersionError控制台日志像天书一样。原因SpringBoot 3.x 强制要求 JDK17本地如果装的是 JDK8 必然失败反过来SpringBoot 2.7.x 在 JDK17 下如果 Lombok 版本太旧也会出现编译错误。解决先看 pom.xml 里的 parent 版本再在命令行执行 java -version 确认本地 JDK 版本。JDK8 配 SpringBoot 2.7.xJDK17 配 SpringBoot 3.xLombok 和 mybatis-plus 也同步换成对应版本。不要一上来就改代码环境对齐了再谈下一步。5.2 表名用了 orderSQL 一执行就报语法错现象建表脚本执行到一半报错或者订单表查询一直提示 SQL syntax error。原因order 是 SQL 标准保留字MySQL 会把它解析成 ORDER BY 的排序关键字“SELECT * FROM order” 必然语法错。解决表名一律用 t_order并在建表语句里加 COMMENT 注明这是一张订单表。不要依赖反引号去绕过反引号在 MySQL 里能跑但换数据库或动态拼接 SQL 时容易忘属于给自己埋雷。5.3 下单金额以客户端传值为准改价后对不上账现象买家下单页显示 100 元订单创建后却变成 80 元或者卖家改价之后历史订单金额也跟着变。原因创建订单接口接收了前端传来的 price 参数或者下单时读取的是缓存里的旧商品数据。解决金额快照只能在服务端做创建订单时从 product 表重新读取当前价格写入 amount而且创建之后不再更新。交易状态只改 status金额字段永远不动。这条也是二手交易平台与电商系统设计的核心差异点值得在源码注释里写清楚。5.4 图片存了本地绝对路径换机器就 404现象开发机上图片一切正常项目打成 jar 发给别人或者部署到服务器后所有图片都不显示。原因数据库里存的是 D:/upload/xxx.jpg 这类绝对路径换一台机器路径根本不存在。解决入库只存 /images/xxx.jpg 相对路径然后在 SpringBoot 里把 /images/** 映射到本机上传目录。Configuration public class WebConfig implements WebMvcConfigurer { Value(${upload.dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/images/**) .addResourceLocations(file: uploadDir /); } }这样项目只依赖一个 upload.dir 配置项换环境改配置即可。数据库里永远存相对路径后续接对象存储也只需改这一处映射。5.5 自动填充不生效created_at 插入后是空值现象实体类里加了 TableField(fill FieldFill.INSERT)但插入成功后 created_at 字段是 NULL。原因MyBatis-Plus 的自动填充依赖 MetaObjectHandler 实现类只加注解不会生效另一种常见情况是 application.yml 里 map-underscore-to-camel-case 被改成了 false导致 created_at 无法映射到 createdAt。解决实现 MetaObjectHandler在 insertFill 里调用 strictInsertFill 填充插入时间在 updateFill 里填充更新时间。Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createdAt, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updatedAt, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updatedAt, LocalDateTime.class, LocalDateTime.now()); } }这个坑之所以隐蔽是因为它在数据库里表现为“可空字段没填值”系统不会报错但列表页排序和后台统计会拿到一堆空时间。排查时先看启动日志里有没有 MetaObjectHandler 相关提示再检查实体字段上注解和 yml 配置是否同时到位。自动填充从来不是黑匣子它是配置类加注解的组合缺一个都不动。6. 商品搜索变慢怎么办用慢查询日志和组合索引做一次排查搜索性能是这类项目上线后最先暴露的问题。分类页、首页、关键词搜索每个请求都在扫描同一张 product 表。优化思路不是从代码层面硬调而是先看数据库是怎么执行的再决定索引怎么写。6.1 先看执行计划再谈优化一条典型的分类列表查询是这种形态按分类和状态过滤再按创建时间倒序。在没有索引的情况下MySQL 会全表扫描把每一行都读一遍再过滤。EXPLAIN SELECT * FROM product WHERE category_id 5 AND status 1 ORDER BY created_at DESC LIMIT 20;EXPLAIN 的输出里重点关注三个列type、key、rows。type 从 ALL 变成 ref 或 range说明语句开始走索引key 会显示实际命中的索引名rows 表示预估扫描的行数。rows 从几万降到几十这条 SQL 的优化就算到位了。第 3 章里设计的 idx_category_status 和 idx_status_created 两个组合索引就是为这类查询准备的。开启慢查询日志也很直接。MySQL 里执行两条命令超过阈值的 SQL 就会记录到日志文件之后再针对性 EXPLAIN。SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;long_query_time 是秒1 秒在开发环境可能偏敏感线上一般设 2 或 3。日志里的每条慢 SQL 都要和 EXPLAIN 结果对照着看单看执行时间很难定位是缺索引还是语句本身有问题。6.2 深分页的游标替代写法翻页翻到后面或者后台管理里查看第 5000 条之后的数据LIMIT 的偏移量会变得很大。-- 普通深分页数据库要扫描前 100000 行再丢弃 SELECT * FROM product WHERE status 1 ORDER BY created_at DESC LIMIT 100000, 20; -- 游标写法用上一页最后一条 id 作为条件 SELECT * FROM product WHERE status 1 AND id 100540 ORDER BY id DESC LIMIT 20;普通写法的问题是数据库要读完前 10 万行再扔掉其中 99980 行扫描量跟偏移量成正比。游标写法把“偏移”变成“条件”直接利用主键索引定位扫描多少行就返回多少行。前端配合“加载更多”而不是页码跳转体验也更顺滑。排序时 id 和 created_at 的增减方向要保持一致否则可能出现错漏。以前我拿到一套源码第一反应是先把所有字段都加上索引后来发现索引数量越多写入越慢优化器还容易选错索引。现在我的习惯是每条列表 SQL 先跑一遍 EXPLAIN再谈优化方案索引可以后加但顺序不能乱查询条件里等值字段放最前面排序字段放最后面。这是我折腾这类 SpringBoot 项目后最想留住的习惯如果你也准备用这套源码做点东西希望帮到你。本文还有配套的精品资源点击获取
返回列表