ARTICLE DETAIL

资讯详情

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

SpringBoot校园二手交易平台实战:数据库设计与核心模块全解析

SpringBoot校园二手交易平台实战:数据库设计与核心模块全解析 简介在Java后端开发中SpringBoot已成为快速构建业务系统的首选框架。一个完整的业务系统往往涉及用户认证、商品管理、交易流程等核心模块而数据库表结构设计与状态机业务流转是决定系统稳定性的关键。通过MyBatis-Plus持久层框架可以高效实现单表CRUD、分页查询与条件构造降低重复编码。校园二手交易平台正是将SpringBoot与MyBatis-Plus落地实践的典型案例其业务边界清晰覆盖用户注册登录、商品发布搜索、订单流转等场景。本文以该项目为背景深入拆解六张核心表的设计逻辑、JWT鉴权实现、商品搜索分页及订单状态机设计帮助开发者掌握从数据库建模到接口实现的全链路技能。 提到校园二手交易平台这个题目做过Java方向课设或毕设的同学应该都有印象——它是SpringBoot项目里出现频率最高的练手题目之一。原因是它的业务边界非常清晰用户注册登录、商品发布、浏览搜索、收藏留言、订单流转刚好把SpringBoot后端开发的核心知识点全覆盖了同时规模又足够小一个人完全能在两周内从零写完源码和数据库设计。这套系统解决的是校园里二手教材、电子产品、生活用品流转效率低的问题适合用来做课程设计、毕业设计或者作为你接触第一个完整SpringBoot项目的实战起点。我拿这个题目带学生做过好几次每次发现大家卡住的点都差不多不是代码写不出来而是从一开始就没想清楚数据库表怎么设计、模块怎么划分、接口怎么定。这套思路一旦理顺后面就是体力活。这篇文章我会把整个项目的源码结构和数据库设计思路完整拆开讲包括每张表为什么这么建、每个核心接口怎么实现、以及实际开发中容易踩的坑。代码部分我会直接给出可以拿去用的关键实现不是那种只截一段伪代码的教程。1. 项目概述与技术选型为什么这套系统值得做1.1 校园二手交易平台到底要解决什么问题校内二手交易的需求其实很朴素毕业生离校时一堆教材、台灯、自行车带不走新生入学又需要低价购入平时想买 Kindle、相机这种低频使用的物品没必要买全新的。传统做法是在QQ群、表白墙、校内论坛发消息效率低、信息分散、没有交易闭环。这套平台要做的就是把发布闲置和查找闲置这两个动作产品化。用户注册登录后可以发布商品填写标题、描述、价格、成色、图片买家通过分类、关键词、价格区间筛选商品看中了可以收藏、留言询问最后走一个简化的订单流程在线下完成交易。注意校园二手场景和电商平台有个本质区别它不需要在线支付和物流交易核心是信息撮合 状态同步这直接决定了数据库表结构和订单状态机的设计方式。很多新手一开始就把这套系统往电商方向设计搞出购物车、支付流水、物流单号纯属给自己加戏。1.2 技术选型SpringBoot MyBatis-Plus 的缘分后端框架选 SpringBoot 没什么好犹豫的。它内置Tomcat、自动配置、起步依赖这套机制让开发者从繁琐的XML配置里解放出来一个main方法就能把服务跑起来。这正好匹配校园二手平台这种业务复杂度中等、需要快速交付的项目。ORM框架我推荐 MyBatis-PlusMP不是原生的 MyBatis。原因很实在单表 CRUD 是 MP 的强项BaseMapper直接提供selectById、insert、updateById这些现成方法省掉大量重复的 XML 映射文件它自带分页插件配合Page对象几行代码就能完成分页查询还可以用LambdaQueryWrapper写出类型安全的查询条件。这套组合在中小型项目里非常能打也是目前国内SpringBoot实战项目用得最多的一套。至于 JPA我个人觉得在这个场景里没有 MyBatis-Plus 顺手——MP 的 SQL 可控性更强对新手来说也更好理解每条语句在干什么。数据库使用 MySQL 5.7 或 8.0 都行前者兼容性稳后者性能更好。如果实在不想本地装数据库用 Docker 拉一个 MySQL 镜像也很简单一条docker run就解决了。1.3 项目整体结构与模块划分项目用标准的 Maven 单模块结构就行不要一上来就搞多模块聚合那种结构更适合中大型团队协作。核心包结构如下com.campus.secondhand ├── config // 配置类跨域、拦截器、静态资源映射 ├── controller // 控制层接收请求、返回结果 ├── service // 业务层核心业务逻辑 │ └── impl ├── mapper // 数据访问层MyBatis-Plus的Mapper接口 ├── entity // 实体类与数据库表对应 ├── dto // 数据传输对象接收前端参数 ├── vo // 视图对象返回给前端的数据结构 ├── common // 通用类统一返回结果、异常处理、常量 ├── utils // 工具类JWT工具、图片存储工具 └── interceptor // 拦截器登录鉴权模块划分上用户、商品、订单、收藏、留言五个业务模块是核心。管理员端可以合并进用户模块通过role字段区分普通用户和管理员不用单独拆一个后台服务。整个项目按 用户、商品、订单 三大主线推进功能开发用户线注册、登录、个人信息、我的发布商品线发布、编辑、上下架、搜索、分页、详情交易线下单、取消、完成、我买到的、我卖出的收藏和留言作为辅助功能挂在商品模块下面逻辑简单但很能体现细节建议做进去。2. 数据库设计一张干净的表结构胜过十次返工2.1 核心表结构拆解数据库是整个系统的地基。表结构设计得好后面的查询和业务扩展都会很顺设计得潦草到处都是冗余字段和诡异的多对多关系写代码的时候就想骂人。校园二手交易平台我建议设计以下六张核心表表名作用核心字段user用户表id, username, password, nickname, avatar, role, statusproduct商品表id, user_id, category_id, title, description, price, original_price, grade, statuscategory分类表id, name, sortorders订单表id, order_no, product_id, seller_id, buyer_id, price, statusfavorite收藏表id, user_id, product_idcomment留言表id, product_id, user_id, content用户和商品是一对多一个用户能发布多个商品。用户和收藏是多对多一个用户可以收藏多个商品一个商品能被多个用户收藏中间通过 favorite 表关联。商品和订单是一对一一个商品在同一时间只属于一个有效订单。留言表是商品的从属信息一个商品下面有多条留言。2.2 关键字段的设计逻辑先看 user 表。username必须加唯一索引这是登录凭证。password字段不要用明文存 BCrypt 加密后的哈希值长度建议 60 到 64 位别用 20 长度的VARCHAR数据写入会直接截断报错。role用TINYINT0 代表普通用户1 代表管理员不要用字符串查询和判断都麻烦。status字段用 0 表示正常、1 表示封禁封禁用户时直接改这个状态就行不用物理删除。product 表是设计的重头戏。price和original_price都用DECIMAL(10,2)而不是FLOAT。原因很简单浮点数有精度损失0.1 加 0.2 在二进制里都表示不精确价格这种需要精确计算的数据不能开玩笑。grade字段表示商品成色1 几乎全新、2 良好、3 一般、4 较旧这是二手交易独有的维度能给买家提供重要参考。status字段我习惯设计四个值0 审核中、1 在售、2 已下架、3 已售出。审核中这个状态如果有管理员功能就用得上没有也可以直接用 1 替代。orders 表要特别注意。很多新手会把订单表设计成只存product_id和buyer_id看起来好像没问题但商品价格是会变的卖家也可能会修改商品信息。订单是交易当时的快照必须把price、seller_id都冗余进来这样即使后续商品下架或改价历史订单数据依然完整可追溯。order_no生成唯一订单号可以用时间戳加随机数也可以用数据库自增ID加前缀自己选一个顺手的方案。2.3 建表SQL实战直接给出一套可以跑的建表脚本关键表和字段都带上注释CREATE DATABASE IF NOT EXISTS campus_secondhand DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE campus_secondhand; CREATE 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, email VARCHAR(100) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, role TINYINT DEFAULT 0 COMMENT 0普通用户 1管理员, status TINYINT DEFAULT 0 COMMENT 0正常 1封禁, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE category ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, sort INT DEFAULT 0 COMMENT 排序权重, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品分类表; CREATE TABLE product ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 发布者ID, category_id INT NOT NULL COMMENT 分类ID, title VARCHAR(100) NOT NULL COMMENT 标题, description TEXT COMMENT 详细描述, price DECIMAL(10,2) NOT NULL COMMENT 售价, original_price DECIMAL(10,2) DEFAULT NULL COMMENT 原价, grade TINYINT DEFAULT 1 COMMENT 成色 1全新 2良好 3一般 4较旧, cover_image VARCHAR(255) DEFAULT NULL COMMENT 封面图, images TEXT COMMENT 多图URLJSON数组, view_count INT DEFAULT 0 COMMENT 浏览次数, status TINYINT DEFAULT 1 COMMENT 0审核中 1在售 2已下架 3已售出, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_category_id (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 订单号, product_id BIGINT NOT NULL, seller_id BIGINT NOT NULL COMMENT 卖家ID冗余, buyer_id BIGINT NOT NULL COMMENT 买家ID, price DECIMAL(10,2) NOT NULL COMMENT 成交价冗余, status TINYINT DEFAULT 1 COMMENT 1待交易 2已成交 3已取消, remark VARCHAR(255) DEFAULT NULL COMMENT 买家留言, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, finish_time DATETIME DEFAULT NULL COMMENT 成交时间, 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表; CREATE TABLE favorite ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, product_id BIGINT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_product (user_id, product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT收藏表; CREATE TABLE comment ( id BIGINT NOT NULL AUTO_INCREMENT, product_id BIGINT NOT NULL, user_id BIGINT NOT NULL, content VARCHAR(500) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_product_id (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT留言表;建表脚本直接用就行。有一点要提醒设置字符集时务必统一使用utf8mb4不是utf8。MySQL 的utf8是阉割版存不了 emoji 和生僻字现在写新项目基本都是utf8mb4。user表名是 MySQL 的保留字建表时如果不加反引号会直接报语法错误我上面的脚本已经处理好了。2.4 关于冗余字段和范式的取舍设计这套表结构时我故意违反了部分范式规则orders 表里冗余了 seller_id 和 priceproduct 表里冗余了 original_price 作为对比参考。很多教科书强调数据一致性恨不得一个字段只在一个表出现但在实际业务里完全遵循第三范式会让查询变得极其痛苦。比如你要查我买到的订单列表走范式设计得先查 orders 表拿到 product_id再 join product 表拿商品标题和图片每页 10 条数据就要多好几次关联查询。冗余的核心目的是业务快照。订单一旦生成它记录的就是当时的商品信息和价格这是交易凭证的一部分不该随商品表联动变化。这不是设计失误而是对业务场景理解后的主动选择。当然冗余也分场合商品表里的view_count这个冗余字段就是纯粹为了性能考虑直接在列表页展示浏览量不需要单独建一张统计表。3. 核心功能从零实现注册登录到商品交易闭环3.1 用户模块JWT登录与密码安全用户模块是整个系统的入口也是一开始就要写好的基础能力。注册流程不复杂前端提交用户名、密码、邮箱、手机号后端校验用户名是否已存在密码 BCrypt 加密后插入数据库。校验逻辑放在 service 层做Controller 只负责接收参数和调用 service这个分层规范从第一个接口就要养成。登录成功后用 JWT 签发 token 返回给前端。JWT 的好处是服务端不需要保存会话状态token 本身携带用户标识和过期时间后续请求在拦截器里解析 token 就能拿到当前用户。核心工具类如下public class JwtUtil { private static final String SECRET your-secret-key-change-in-production; private static final long EXPIRE_TIME 24 * 60 * 60 * 1000; public static String generateToken(Long userId, String username) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .setIssuedAt(new Date()) .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(); } }生成 token 后写一个拦截器统一处理登录鉴权。拦截器里从请求头Authorization取 token解析失败直接返回 401成功就把 userId 放到request的 attribute 里后续 Controller 直接通过RequestAttribute拿当前用户。这个方案在前后端分离的项目里非常常见一定要弄明白。密码安全这块我要多说一句。很多网上流传的代码用 MD5 加盐存密码现在这种方案已经不推荐了。MD5 计算速度太快暴力破解成本低BCrypt 内部自带随机盐同样的密码每次加密结果都不一样安全性高一个数量级。Spring Security 里的BCryptPasswordEncoder可以直接拿来用或者引入jbcrypt依赖也行。3.2 商品模块发布、编辑、上下架与图片上传商品发布是平台的核心业务前端表单字段包括标题、分类、成色、描述、价格、原价、图片。后端接收时用 DTO 对象接收不要直接把 Entity 暴露给前端接收数据。原因很简单前端提交的字段和后端表结构是对不上的直接用 Entity 接收容易出现参数覆盖的安全问题也破坏了分层设计。发布接口的核心代码大概是这样的PostMapping(/publish) public ResultString publish(RequestBody ProductDTO dto, RequestAttribute Long userId) { Product product new Product(); BeanUtils.copyProperties(dto, product, id, userId, status); product.setUserId(userId); product.setStatus(1); // 直接上架 product.setViewCount(0); productService.save(product); return Result.success(发布成功); }BeanUtils.copyProperties指定忽略id、userId、status这几个字段防止前端传入恶意数据覆盖掉关键信息这是一个容易被忽略的安全细节。商品编辑和上下架的逻辑必须加权限校验只有商品发布者本人才能操作。具体实现是查出商品后比较product.userId和当前登录用户是否一致不一致直接抛业务异常。这个校验不能只在前端做后端必须重复校验因为接口是可以被直接调用的。图片上传是商品模块很容易出错的地方。本地存储方案里保存文件后要把访问 URL 存到数据库URL 用/upload/2024/xx.jpg这种相对路径不要存http://localhost:8080/upload/xx.jpg这种写死域名的完整路径否则以后换域名或者服务器 IP 变了旧数据全部失效。配置层面要写一个静态资源映射把/upload/**映射到本地磁盘目录Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: System.getProperty(user.dir) /upload/); } }3.3 搜索筛选与分页商品列表页是整个系统流量最大的接口也是开发中和前端联调时最容易出问题的点。它需要同时支持关键词搜索、分类筛选、价格区间、成色筛选、状态过滤、排序、分页。这些条件组合起来用 MyBatis-Plus 的 LambdaQueryWrapper 写会比较清晰public IPageProduct pageProducts(ProductQueryDTO dto) { PageProduct page new Page(dto.getPageNum(), dto.getPageSize()); LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.eq(Product::getStatus, 1) // 只在售 .eq(dto.getCategoryId() ! null, Product::getCategoryId, dto.getCategoryId()) .eq(dto.getGrade() ! null, Product::getGrade, dto.getGrade()) .between(dto.getMinPrice() ! null dto.getMaxPrice() ! null, Product::getPrice, dto.getMinPrice(), dto.getMaxPrice()) .and(StringUtils.hasText(dto.getKeyword()), w - w.like(Product::getTitle, dto.getKeyword()) .or().like(Product::getDescription, dto.getKeyword())) .orderByDesc(dto.getSort() null ? Product::getCreateTime : Product::getPrice); return productMapper.selectPage(page, wrapper); }这里有几个细节值得注意。第一eq方法第一个参数是条件判断为false时这个查询条件不会拼到 SQL 里这样可以避免手动拼字符串的麻烦。第二关键词搜索把 title 和 description 都查一遍用and包裹or防止条件逻辑错乱。第三排序字段存在前端传入时的安全风险不能直接用前端传的字段名拼 SQL要么用白名单映射要么像我这样只允许在创建时间和价格之间选择。分页插件配置在 MyBatis-Plus 里是必须的Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }不配这个插件selectPage查出来的总条数永远是 0这是新手最容易踩的坑。3.4 订单流程模拟线下二手交易的完整状态机订单模块是这个平台和普通商品展示系统的核心区别。因为没有在线支付所以订单状态机可以设计得简单一些待交易、已成交、已取消三个状态。买家对在售商品点击我想要后端生成订单状态为 1待交易。此时商品状态应从 1在售改成 3已售出防止其他人重复下单。这一步一定要放在同一个事务里否则会出现商品被两个人同时下单的并发问题。卖家看到订单后和买家线下联系交易完成后卖家确认订单状态变为 2已成交整个流程结束。订单状态变更的接口需要针对不同角色做权限控制买家可以下单、取消自己的订单卖家可以确认完成、取消订单普通用户不能操作别人的订单。判断逻辑写在 service 层不能依赖前端只展示按钮来限制。我用一个状态流转的枚举来管理这些状态变更public enum OrderStatus { PENDING(1, 待交易), FINISHED(2, 已成交), CANCELLED(3, 已取消); }每个状态变更接口的核心逻辑都是查订单 → 校验操作人是否为订单关联方 → 校验当前状态是否允许流转 → 更新订单状态 → 同步更新商品状态 → 事务提交。把这一步拆清楚订单模块就算拿下了。3.5 收藏与留言收藏功能本质是一张中间表。用户点击收藏时后端先查 favorite 表是否已有记录有就删除表示取消收藏没有就插入表示收藏一个接口搞定两种动作。商品详情页需要显示当前用户是否已收藏查询时带上user_id和product_id查一下即可。留言功能类似评论区。发布留言时校验用户登录状态商品下架或已售出时可以正常查看留言但禁止发布新留言。列表查询按创建时间倒序配合用户表的昵称和头像一起返回。这个功能对二手交易很重要因为买家通常会询问成色细节、是否可刀、交易地点等信息留言就是买卖双方的第一轮沟通。4. 实操过程记录IDEA从0搭建到接口联调4.1 用Spring Initializr快速初始化项目打开 IDEA新建项目时选择 Spring Initializr这一步是标准操作。需要注意三个选项的配合Java 版本选 8 或 11 比较稳对应 SpringBoot 2.7.x如果你选 Java 17SpringBoot 可以上 3.x但 3.x 要求javax包名换成jakarta很多老教程的代码会报错新手不建议直接上 3.x。依赖这里勾选Spring Web、MySQL Driver、LombokMyBatis-Plus 不在初始化的依赖列表里需要手动往pom.xml里加dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency如果你选择的 SpringBoot 版本是 3.x那对应要引入mybatis-plus-spring-boot3-starter这个坐标别搞混了。Lombok 装好后记得确认 IDEA 里安装了 Lombok 插件并且开启了Enable annotation processing否则实体类的一堆注解通通不生效。4.2 application.yml配置的六个关键项配置是启动项目的第一道门槛我见过太多人在这一步卡住。核心配置如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/campus_secondhand?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: your-password driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 5MB max-request-size: 20MB jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto这里六个关键点逐个说清楚。数据源 URL 里的characterEncodingutf8解决中文乱码serverTimezoneAsia/Shanghai解决时间差 8 小时问题useSSLfalse避免 MySQL 8.0 在连接时报 SSL 警告。max-file-size限制单张图片大小防止用户传超大文件把磁盘塞爆。map-underscore-to-camel-case开启后数据库的下划线字段自动映射到 Java 的驼峰属性这是 MyBatis-Plus 能省事的根基。log-impl设为 StdOutImpl 后控制台能打印完整 SQL排查问题非常方便上线前记得删掉。4.3 分层结构如何组织Controller别写业务逻辑很多同学的代码走到第三种程度就是 Controller 里堆了一两百行业务代码。这种写法短时间看很爽但到联调阶段就痛苦了接口稍微复杂一点Controller 就膨胀成一坨难以维护的代码。我的建议是严格执行 Controller → Service → Mapper 三层。Controller 层只做三件事接收参数、调用 Service、返回统一结果。Service 层写核心业务逻辑包括参数校验、权限判断、事务控制。Mapper 层就是数据访问接口不写业务。实体类的创建和转换放在 Service 里完成Controller 不直接操作 Entity。统一返回结果这个类非常关键所有接口都返回同一个结构前端才能统一处理Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }配合RestControllerAdvice全局异常处理器业务异常统一抛给全局处理Controller 里就不用每个方法都 try-catch 了。4.4 接口自测与联调后端写完后我习惯先用 Apifox 或 Postman 把核心接口全部跑一遍再交给前端。测试顺序有个讲究先跑注册登录拿到 token再把 token 配置到全局请求头里然后依次测商品、收藏、订单接口。自测时重点关注三个问题状态码是否统一、返回数据结构和前端约定是否一致、异常路径是否返回了友好提示。比如登录时密码错误不能直接把 500 和异常堆栈抛给前端而是返回业务异常用户名或密码错误。前端只需要根据 code 判断成功失败message 直接弹给用户看。前后端分离项目里接口联调最怕的就是两边对返回结构理解不一致这种前提下的自测能省掉大量沟通成本。5. 常见问题与排查技巧实录5.1 高频问题速查表这几类问题在实战中出现频率最高我直接整理成表症状原因解决方案数据库中文变成???JDBC连接没设置编码URL加 characterEncodingutf8时间比实际少8小时serverTimezone设置为UTC改为 serverTimezoneAsia/Shanghai分页返回total恒为0没配MyBatis-Plus分页插件添加MybatisPlusInterceptor图片上传后访问404静态资源映射没配置实现addResourceHandlers前端请求跨域没开启CORS配置全局CorsFilterIDEA里Lombok注解报红插件未安装或未开启注解处理安装插件并打开annotation processing8080端口被占用其他程序占用了端口server.port换端口或杀掉占用进程上传大图片内存溢出没限制multipart大小配置 max-file-sizeJVM调大堆内存最后一条内存溢出是个经典问题。SpringBoot 默认的max-file-size是 1MB如果你不配置传大图直接报文件大小超限如果配了很大的值又不限制 JVM 堆内存并发上传时可能出现OutOfMemoryError。我一般设置单图不超过 5MB同时启动参数加-Xmx512m对这个小项目完全够用。5.2 图片上传与访问的坑图片这个问题几乎每个做这个项目的同学都会踩一遍。第一个坑是保存路径。直接用new File(upload/)存相对路径项目以 jar 包形式部署后就抓瞎了路径会跑到临时目录里去。我现在的做法是固定使用用户目录或项目运行目录下的某个绝对路径比如String uploadDir System.getProperty(user.dir) /upload;第二个坑是文件名。直接用用户上传的原始文件名会导致路径穿越攻击和重名覆盖。路径穿越攻击的原理是文件名里包含../拼接路径时就可以跳出上传目录这是实际存在的安全漏洞。我一般用 UUID 重命名String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String newFilename UUID.randomUUID().toString().replace(-, ) suffix;第三个坑是访问权限。有些人配了静态资源映射但总是不生效排查时先看 URL 是否和映射配置的路径一致再看file:后面的路径是不是以/结尾。这个/漏了映射就拼出一个错误的磁盘路径。5.3 数据库中文乱码与时区问题中文乱码是个老生常谈但每隔一阵子就会遇到的问题。排查步骤要按顺序走先看 MySQL 数据库和表的字符集SHOW CREATE TABLE user查表的编码再确认 JDBC URL 带了characterEncodingutf8最后看代码里是不是用InputStreamReader读文件时指定了错误的编码。一层一层查大多数情况都是 JDBC URL 没设置编码导致的。时区问题则是因为 MySQL 8.0 默认时区是 UTC而国内是东八区。如果你在配置文件里没指定serverTimezone程序启动时通常会直接报错或者在查询DATETIME类型字段时比北京时间少 8 小时。解决办法就是 URL 里加serverTimezoneAsia/Shanghai同时在 Jackson 配置里也把时区设为GMT8双保险。5.4 项目做完后怎么应对面试提问这个项目如果作为简历上的项目经验面试官大概率会问这几个问题JWT 和 Session 的区别是什么MyBatis-Plus 的分页插件底层是怎么实现的商品下单时如何防止超卖数据库表设计为什么这么建前两个问题可以靠背第三个问题要真正理解事务和状态变更的逻辑。下单防止超卖本质上是通过数据库的行锁实现的UPDATE product SET status 3 WHERE id ? AND status 1这条语句自带乐观锁效果更新影响行数为 0 说明商品已经被人抢先下单了。这也是我刚才在订单模块强调状态判断要在 SQL 层面完成而不是先查出来再手动判断的原因。最后一个问题就是这篇文章的数据库设计章节把你对冗余字段、业务快照、状态机的理解讲清楚面试官一般都会认可。这个项目虽然技术栈不算前沿但胜在业务完整、逻辑闭环把表设计和状态机讲明白就足够体现你的后端设计能力了。我在实操中发现最划算的事情是在动手写业务代码之前先把那六张表和接口文档定了。表结构一改后面所有 Service 和 Mapper 全要跟着动代价极高。如果你正准备动手做这个项目建议先花一天时间把建表脚本跑通把注册和商品发布这两个接口调通剩下的模块就是顺着这条路往下走越写越顺手。本文还有配套的精品资源点击获取
返回列表