每年六月毕业季宿舍楼下总是一片狼藉带不走的台灯、书架、考研资料直接扔进垃圾桶的大有人在。明明很多东西还有七八成新挂到群里卖却往往要面对两种结局——要么消息瞬间被刷屏淹没要么被砍价砍到怀疑人生。这就是我决定动手做一套springboot校园二手交易平台系统的直接原因。这个系统从表面看就是一个典型的管理业务项目但真正把它当作一个完整产品去做的时候你会发现里面要处理的东西远比想象中多用户身份认证怎么做、商品状态怎么流转、图片上传怎么处理、并发下单怎么保证不超卖甚至还有校园场景特有的信任体系设计。这些内容拼在一起才是这套系统的完整技术含量。这篇文章我会把整个设计和开发过程拆开来讲包括技术选型的取舍理由、数据库表结构设计、核心业务流程的实现细节以及那些踩过一遍才会长记性的坑。适合正在准备毕业设计、想拿校园项目练手或者正在做前后端分离项目但总觉得缺了点深度的同学参考。1. 为什么校园场景需要一套独立的二手交易系统1.1 校园二手交易的真实痛点校园里二手交易的体量其实非常大。一个三千人的学院每年毕业季大概会有上千件物品流转需求教材资料、小家电、自行车、吉他、显示器品类五花八门。但现实是这些需求一直靠QQ群、微信群、贴吧来解决体验非常糟糕。QQ群最大的问题是信息扁平化。群里每秒钟刷出十几条消息一条出九成新台式机1200自提的信息发出后十几秒就被淹没真正的买家根本看不到。而贴吧和论坛式的交易帖子存在找东西困难的问题——想买二手自行车的人得翻几十页帖子才能找到一个匹配的。更麻烦的是完全没有身份约束校外人员进群发广告、加好友骗定金的情况屡见不鲜。线下跳蚤市场倒是可以面对面交易但一年就办一两次时机通常集中在毕业季而实际上二手需求全年都有。大二学生上学期想买书架、下学期想买加湿器这些都是分散的、碎片化的需求必须有一个随时可用的平台来承接。我自己蹲了几个二手群观察了一周发现成交信息里大量涉及物品描述不完整、没有实物图、沟通成本高、缺乏交易记录。这些问题的本质不是信息不够而是没有一个结构化的信息模型来承载物品的多维度属性——图片、价格、成色、位置、联系方式、交易状态。1.2 平台要解决的核心问题清单做这个系统之前我先把问题拆成了以下几个核心目标信息结构化每件商品有明确的标题、描述、价格、图片、成色、分类而不是一句出一个东西见图。可检索支持按关键词、分类、价格区间组合搜索让买家快速找到想要的商品。身份可信只有通过学生认证的用户才能发布商品和下单解决校外人员混入的问题。交易可追踪从商品发布、下单、付款或约定线下交易到完成的整个生命周期都有状态记录。管理可介入管理员能处理举报、下架违规商品、封禁恶意用户而不是让平台秩序失控。这五个目标几乎是所有校园交易类项目的共同底座不管你是做的毕业设计还是想拿去参加比赛把这五点想清楚项目的骨架就立住了。1.3 这个系统的技术含量在哪里有人说校园二手交易平台不就是个CRUD吗商品增删改查、用户增删改查、订单增删改查框架半天就能搭出来。但做深了会发现真正的技术含量藏在那些看不太见的地方订单状态机商品从在售到待付款到交易完成状态流转的合法性校验怎么做并发控制两个人同时点立即购买同一件商品怎么保证不会都下单成功权限体系普通用户、学生认证用户、管理员三个角色接口权限怎么隔离文件处理用户上传的图片怎么存储、怎么设置访问权限、怎么防止恶意文件上传数据一致性下单后库存扣减和订单创建不是同时完成的中间出错了怎么回滚这些问题的答案只有真正动手写代码的时候才会浮出来。本文后面的章节就是逐个解决这些问题的完整记录。2. 技术选型的拆解与取舍逻辑2.1 为什么最终坚持用Spring Boot最初我曾经犹豫过要不要用SSH或SSM那套传统组合。但仔细算了一笔账决定还是用Spring Boot。原因很简单Spring Boot的自动配置机制把过去SSM项目里最大头的那部分工程量——beans.xml配置、springmvc-config.xml配置、mybatis配置、事务配置——几乎全部自动化了。我只需要引入对应的starter写好application.yml框架就能把数据源、SqlSessionFactory、事务管理器这些核心组件自动装配好。这里上面还有一个关键选择是版本。Spring Boot 3.x发布后很多教程都在推新版本但我在构建这个项目时选了2.7.x。原因有三JDK兼容性3.x强制要求JDK 17而2.7.x可以跑在JDK 8上。校园项目部署环境通常比较复杂可能是学校机房的旧电脑也可能是便宜的云服务器JDK 8的兼容面广得多。第三方组件兼容很多教程和开源项目还在用适配2.x的版本比如MyBatis-Plus旧版、某些安全框架的集成在3.x会有兼容风险。学习资料密度2.x的教程、踩坑文章、论坛讨论都是最丰富的遇到问题能更快搜索到解决方案。注意如果你是新做一个项目且目标环境明确是JDK 17用3.x没问题。但做毕设或长期维护的项目我会优先推荐2.7.x——稳定压倒一切这个判断在我后续开发过程中被反复验证是对的。2.2 后端组件搭配MyBatis-Plus、Redis、JWT一个都不能少分层技术栈最终定为Spring Boot 2.7.x底座MyBatis-Plus 3.5.xORM内置CRUD方法和分页插件省掉大量手写SQLRedis验证码存储、首页缓存、分布式锁处理并发下单JWT前后端分离场景下的无状态登录方案Lombok简化实体类代码Hutool工具库处理验证码生成、JWT工具、加密工具等有个容易犯的错误是觉得Redis是炫技用的、没这个必要。我一开始也觉得一个小项目搞Redis有点重但实际做下来发现Redis至少解决了两个真实问题短信/邮箱验证码的过期机制虽然校园平台通常不用短信验证码但注册发送邮箱验证码的场景很常见。验证码本来就是要过期自动销毁的数据Redis自带过期时间比存数据库然后定时清理优雅得多。热点商品的缓存首页爆款商品或者某个热门分类的商品如果每次请求都查数据库数据库压力会很明显。把热点数据放到Redis里读请求走缓存只有缓存过期才回源数据库。JWT选型的理由也很直接既然前端决定用Vue做前后端分离那Session机制就不合适了。JWT把用户身份信息加密后放在Token里后端不需要存储会话状态天然适配无状态API设计。2.3 前端方案Vue前后端分离还是服务端渲染前端我最终选了Vue 3 Vite Element Plus前后端分离架构。实际上很多校园项目的毕设版本用的是Thymeleaf模板引擎做服务端渲染好处是一个Spring Boot应用直接打包部署不需要单独部署前端省了跨域和联调的麻烦。但它的缺陷是整个界面交互都受限于后端渲染逻辑想做一个弹窗确认、动态筛选、实时状态更新代码会绕很多。前后端分离之后的开发模式舒服很多前端项目只需通过Axios调用后端接口接口返回JSON数据前端负责渲染交互。后端的注意力可以完全集中在业务逻辑、参数校验、权限控制上而不是纠结页面上的按钮点击事件。代价是需要额外处理三个问题跨域配置、Token传递、接口联调。跨域问题我会在后面的章节详细说这里先卖个关子。2.4 图片存哪里本地存储的利与弊用户上传的商品图片处理方案有三种本地磁盘存储、云存储OSS、MinIO自建对象存储。我第一版直接用了本地磁盘存储。理由很务实项目要跑起来给演示本地存储不需要额外配置也不依赖外网在任何一台机器上启动项目就能看到完整效果。工作原理是前端把图片文件通过multipart/form-data方式POST到后端。后端接收到MultipartFile后把文件写入服务器磁盘的指定目录比如/upload/2024/06/01/uuid.jpg。文件路径保存到数据库商品表。后端配置静态资源映射把/upload/**映射到磁盘目录前端通过http://ip:8080/upload/xxx.jpg直接访问图片。这套方案的坑在于重命名不要用文件名直接存否则不同的用户可以互相覆盖同名文件。我用UUID.randomUUID()加上原始文件的后缀名生成新文件名再按日期分目录存储能避免大量小文件堆在一个目录里导致目录索引性能下降。如果你在上线部署时追求高可用可以考虑换成MinIO接口设计和OSS类似但本地部署不需要开通云服务。这里先不展开后面踩坑章节再细说。3. 业务模块规划与数据模型设计3.1 功能模块全景图系统按照角色可以拆成三个端普通用户端未登录浏览首页、查看商品列表、搜索商品、查看商品详情。用户端已登录发布商品、编辑/下架商品、下单购买、查看自己的订单、收藏商品、留言咨询、举报商品、个人资料管理。管理员端用户管理认证审核、封禁/解封、商品管理下架违规商品、举报管理处理举报、查看处理记录、数据统计商品总量、成交量、活跃用户数。模块划分的原则是每个角色只看到和自己相关的东西商品列表必须有条件过滤——已登录用户不能看到自己发布的商品并点击购买管理员可以看到所有商品并一键下架普通用户只能看到状态为在售的商品。3.2 核心表结构详解下面这几张表是整个系统的核心建议在动手写代码之前就设计好后面返工的代价远大于提前多花一天想清楚。用户表 userCREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号登录账号, password VARCHAR(64) NOT NULL COMMENT BCrypt加密后的密码, nickname VARCHAR(30) COMMENT 昵称, avatar VARCHAR(255) COMMENT 头像图片路径, phone VARCHAR(11) COMMENT 手机号, college VARCHAR(50) COMMENT 学院, major VARCHAR(50) COMMENT 专业, auth_status TINYINT DEFAULT 0 COMMENT 认证状态 0未认证 1待审核 2已认证 3审核驳回, credit_score INT DEFAULT 100 COMMENT 信用分初始100, role TINYINT DEFAULT 0 COMMENT 角色 0普通用户 1管理员, status TINYINT DEFAULT 1 COMMENT 状态 1正常 0封禁, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT用户表;一个值得注意的设计细节学号直接作为登录账号。校园场景下学号是唯一且公开的天然适合做用户名而且后续做学生认证的时候学号和学生证、学校邮箱是绑定关系不需要额外建一张映射表。商品表 goodsCREATE TABLE goods ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 发布者ID, title VARCHAR(100) NOT NULL COMMENT 标题, description TEXT COMMENT 详细描述, category VARCHAR(20) NOT NULL COMMENT 分类教材/数码/生活/运动/其他, original_price DECIMAL(10,2) COMMENT 原价, price DECIMAL(10,2) NOT NULL COMMENT 出售价, quality TINYINT COMMENT 成色1全新 2九成新 3七成新 4五成新及以下, image_urls VARCHAR(1000) COMMENT 图片路径多张用逗号分隔, contact VARCHAR(50) COMMENT 联系方式, status TINYINT DEFAULT 0 COMMENT 状态 0在售 1已售出 2已下架 3违规下架, view_count INT DEFAULT 0 COMMENT 浏览量, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT二手商品表;image_urls用逗号分隔这种设计很常见虽然违反了第一范式但考虑到商品图片最多三五张查一次商品信息就能拿到全部图片路径省掉一次子查询性能上是划算的。大多数人不会把图集设计成独立表除非你的业务场景是每件商品可能有几十张详细展示图比如房源的户型图。订单表 ordersCREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 订单编号, goods_id BIGINT NOT NULL COMMENT 商品ID, buyer_id BIGINT NOT NULL COMMENT 买家ID, seller_id BIGINT NOT NULL COMMENT 卖家ID, price DECIMAL(10,2) NOT NULL COMMENT 成交价, status TINYINT DEFAULT 0 COMMENT 状态 0待付款 1已付款待收货 2交易完成 3已取消 4退款中, remark VARCHAR(255) COMMENT 买家留言, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME COMMENT 付款时间, finish_time DATETIME COMMENT 完成时间, cancel_time DATETIME COMMENT 取消时间 ) COMMENT订单表;order_no订单编号我用的是时间戳加随机数生成形如20240601103012001这样每个订单都有可读的业务编号方便用户对账和管理员排查。用雪花ID也可以但对校园项目说时间戳加随机数完全够用。3.3 订单状态机的数据库表达订单状态不是一个简单的字段它隐含了一组业务规则只有待付款的订单可以取消只有待付款的订单可以支付只有已付款待收货的订单可以确认收货变成交易完成已经完成的订单不能再取消。如果不在代码里做状态校验就会出现用户疯狂点击取消按钮把已完成的订单又取消掉数据库里一堆脏数据。我的做法是在Order实体里加一个状态枚举public enum OrderStatus { WAIT_PAYMENT(0, 待付款), PAID(1, 已付款待收货), FINISHED(2, 交易完成), CANCELED(3, 已取消); private final int value; private final String desc; // 构造方法、getter省略 }然后在订单状态变更的Service方法里统一通过一个方法判断当前状态能否流转到目标状态。这个方法本质上就是一张状态流转表private static final MapOrderStatus, SetOrderStatus ALLOWED_TRANSITIONS new HashMap(); static { ALLOWED_TRANSITIONS.put(OrderStatus.WAIT_PAYMENT, new HashSet(List.of(OrderStatus.PAID, OrderStatus.CANCELED))); ALLOWED_TRANSITIONS.put(OrderStatus.PAID, new HashSet(List.of(OrderStatus.FINISHED, OrderStatus.CANCELED))); }这种方式能把非法状态流转在业务层直接拦截掉比单纯改字段值安全很多。3.4 设计上的几个反直觉决定有几个设计我说出来可能有人要反对但实际开发下来是省心的。不建外键。用户表、商品表、订单表之间虽然有明确的关系但我故意没有在数据库层面加FOREIGN KEY约束。这是因为校园平台的数据量虽然不大但外键的存在会让删除操作变成牵一发动全身——删一个用户要检查所有关联商品、所有订单删一个商品要先处理所有锁定的订单。我在业务代码里通过Service层显式进行关联校验反而更好控制错误信息和处理顺序。逻辑删除代替物理删除。商品被用户删除时我只是把status字段改成某个标记值而不是直接DELETE。这样首页还能统计历史数据管理员还能在后台看到被删内容。配合一个deleted字段也是常见做法。不做购物车功能。很多人一听到二手交易系统就觉得应该有购物车但仔细想想——二手商品每一件都是唯一的不存在同一商品买多件的场景购物车在这里完全没有意义。过度设计是校园项目最普遍的通病少做没用的需求把下单流程做直反而是成熟的设计判断。4. 核心业务流程的实现细节4.1 商品发布从表单到上架的完整链路商品发布流程的代码逻辑不算复杂但有几个点必须处理到位。我把流程拆成下面几条用户提交表单数据前端把商品字段标题、描述、分类、原价、售价、成色、联系方式拼成JSON连同图片文件一起提交。后端先做登录校验和权限校验——未登录不能发布已认证的用户才能发布。参数校验标题长度限制、价格必须大于0且不能高于原价的离谱程度比如原价100卖500这种数据要直接拒绝。图片处理接收MultipartFile数组逐个保存返回图片路径数组。组装Goods实体初始状态设为在售写入数据库。核心的Service方法大致是Transactional(rollbackFor Exception.class) public Long publish(GoodsPublishDTO dto, ListMultipartFile images, Long userId) { // 1. 校验用户认证状态 User user userMapper.selectById(userId); if (user null || user.getAuthStatus() ! 2) { throw new BusinessException(请先完成学生认证后再发布商品); } // 2. 校验参数 if (dto.getPrice().compareTo(BigDecimal.ZERO) 0) { throw new BusinessException(出售价格必须大于0); } // 3. 处理图片 ListString imageUrls images.stream() .map(image - fileService.saveImage(image)) .collect(Collectors.toList()); // 4. 组装实体 Goods goods new Goods(); goods.setUserId(userId); goods.setTitle(dto.getTitle()); treats.setDescription(dto.getDescription()); goods.setPrice(dto.getPrice()); goods.setImageUrls(String.join(,, imageUrls)); goods.setStatus(GoodsStatus.ON_SALE.getValue()); // 5. 入库 goodsMapper.insert(goods); return goods.getId(); }Transactional(rollbackFor Exception.class)是必须写的如果图片保存后发现前面某一步抛错数据库不能留半截数据。4.2 图片上传最容易翻车的环节Spring Boot默认的multipart上传限制很小如果你不主动配置超过1MB的图片会被直接拒绝。用户的手机照片动辄2-3MB所以一定要在application.yml里显式调大spring: servlet: multipart: max-file-size: 10MB max-request-size: 30MB图片保存逻辑里有几个容易被忽略的细节文件名重命名不要用用户上传的原始文件名用UUID重新生成。否则可能遇到两个用户上传同名图片互相覆盖更危险的是文件名里带路径转义字符可能构成安全风险。扩展名白名单校验只允许.jpg、.jpeg、.png、.gif、.webp其他一律拒绝。不要只用Content-Type判断这个可以伪造最稳妥的是解析一下文件头或者用扩展名Content-Type双重校验。静态资源映射保存好文件之后还得让前端能访问到。在Spring Boot中配置Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadPath System.getProperty(user.dir) /upload/; registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath); } }这样浏览器就能通过http://localhost:8080/upload/xxx.jpg直接访问图片了。4.3 下单与库存扣减并发安全怎么做二手商品每件只有一个库存两个人同时点购买同一件商品应该只有一个人成功。刚开始我用的是典型的select-then-update逻辑查商品状态是否在售如果在售创建订单更新商品状态为已售出这个逻辑在低并发场景没问题但遇到并发请求时会出现两个人都查到了在售两个人都创建了订单然后商品被卖了两次。原因就是检查和更新不是原子操作。解法是乐观锁。在goods表增加一个version字段更新时用带上version来校验UPDATE goods SET status 1, version version 1 WHERE id #{goodsId} AND status 0 AND version #{version}如果UPDATE影响的行数为0说明商品状态已经被其他请求改掉了本次下单直接失败。结合事务下单逻辑变成Transactional(rollbackFor Exception.class) public Long createOrder(Long goodsId, Long buyerId) { // 1. 查商品 Goods goods goodsMapper.selectById(goodsId); // 2. 前置校验 if (goods null || goods.getStatus() ! GoodsStatus.ON_SALE.getValue()) { throw new BusinessException(商品已下架或已售出); } if (goods.getUserId().equals(buyerId)) { throw new BusinessException(不能购买自己发布的商品); } // 3. 乐观锁更新商品状态 int rows goodsMapper.updateStatusWithVersion( goodsId, goods.getVersion(), GoodsStatus.ON_SALE.getValue(), GoodsStatus.SOLD.getValue()); if (rows 0) { throw new BusinessException(手慢了商品已被别人买走); } // 4. 创建订单 Order order new Order(); order.setOrderNo(generateOrderNo()); // ... 设置订单字段 orderMapper.insert(order); return order.getId(); }这里有一个更极端的情况是数据库事务隔离级别默认是REPEATABLE_READMySQL InnoDB默认两个并发事务同时读同一行在售商品理论上一方更新后另一方再提交时会冲突但因为我们是先UPDATE后INSERTUPDATE的锁会串行化这个过程实测下来是安全的。如果你还担心可以再加一层Redis分布式锁以goods:lock:商品ID作为key下单前先尝试加锁。但考虑到校园平台的并发量——峰值大概每秒几十次下单——乐观锁已经绰绰有余分布式锁属于锦上添花。4.4 订单状态流转如何防止非法跳转状态机校验放在Service层统一出口。所有涉及订单状态变更的操作都必须通过OrderStateMachinepublic void ensureCanChange(OrderStatus current, OrderStatus target) { SetOrderStatus allowed ALLOWED_TRANSITIONS.get(current); if (allowed null || !allowed.contains(target)) { throw new BusinessException(订单状态非法流转 current.getDesc() - target.getDesc()); } }然后在每个Service业务方法里第一步调用这个校验。这样就让状态变更的唯一入口变得可控无论是用户页面的按钮、还是管理员的强制操作都必须经过规则校验。5. 校园场景特有的信任机制设计5.1 学生身份认证把骗子挡在门外线上交易最怕遇到不守信的人校园二手市场尤其明显。因为地点范围小一旦出现骗子影响会迅速波及整个学校。我设计了三层认证方式供不同情况下使用学校邮箱验证部分学校给每个学生分配了专属校园邮箱后缀一般是stu.xxx.edu.cn。用户填入学号和对应邮箱系统发验证码输入验证码即通过认证。这种方式防伪能力最强。学生证上传审核用户上传学生证照片管理员在后台人工审核。这种方式审核成本高但兼容了没有校园邮箱的学校。学号姓名匹配如果对接了学校的信息系统接口可以直接用学号姓名查询验证。但在毕设场景下工作量太大我把它列为可选方案。实践中我把1和2都做了优先邮箱验证验证不了就上传学生证等管理员审核。认证通过前发布商品功能直接不可用从入口处就挡住了大量潜在风险。5.2 信用分与举报处理体系信用分是校园平台的关键运营机制。初始分为100分规则如下交易成功且双方无投诉双方信用分各自加1分上限110分。卖家被举报且查实描述不符、拒不发货扣10分。买家被举报且查实恶意砍价、放鸽子扣5分。信用分低于60分禁止发布商品、限制下单。信用分低于30分封禁账号。这套规则对标的是主流电商平台的信用体系但做了简化强调高频低额的约束从违规到封禁是一个累积过程给用户改正空间也在流程上保留证据。举报处理流程买家/卖家 → 提交举报单选择商品、填写理由、上传截图 → 管理员后台审核 → 审核通过则下架商品并扣信用分审核驳回则关闭举报单 → 被举报方有申诉机会。5.3 交易安全提示与规则约束校园二手交易通常不走线上支付很多是当面交易平台不需要介入资金流。但为了减少纠纷我在系统中做了几个强制设计商品详情页显著位置展示建议校内当面交易仔细验货后再付款的提示。下单时弹出交易须知必须勾选已阅读并同意才能下单。订单完成后买家和卖家互相评价描述相符、沟通态度、交易速度三个维度打分。这些设计看似轻量但真的能降低平台方的责任风险同时让用户意识到交易平台只是撮合角色保护自己的权益还是要靠主动行为。6. 开发中的踩坑实录与优化手记6.1 Spring Boot版本选择引发的连锁反应最初我确实冲动过想直接用Spring Boot 3.x加JDK 17毕竟新版本性能更好而且代表未来的方向。但做了几天就开始后悔了。问题出在生态兼容上。我用到的MyBatis-Plus版本如果太旧直接不支持3.x的Spring Boot如果换新版和JDK 17 Spring Boot 3.x配合又需要额外配置。中间遇到的一个例子是MyBatis-Plus的分页插件在Spring Boot 3下需要新依赖mybatis-plus-spring-boot3-starter而网上的老教程全都是在讲mybatis-plus-boot-starter照着旧教程写了半天才发现不对。最后我把项目整体回退到Spring Boot 2.7.x JDK 8 MyBatis-Plus 3.5.3一天之内所有问题迎刃而解。后来想一想求稳定是项目开发的第一原则技术栈的新旧并没有想象中那么重要。6.2 文件上传限制和跨域那点事这两个坑几乎每个做前后端分离的人都会踩一遍我简单说下自己的解决过程。跨域问题排查了整整一个下午。现象是前端Vue启动在8081端口后端Spring Boot跑在8080端口浏览器里前端页面发起POST请求NetWork面板里显示请求发出去了但响应状态是CORS error。后来在Spring Boot的配置类里加上跨域配置就好了Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }让我注意的是allowedOriginPatterns(*)配合allowCredentials(true)必须用allowedOriginPatterns而不是allowedOrigins否则某些浏览器会拦截带Cookie的请求。校园项目开发环境下没有Cookie操作需求的话直接allowCredentials(false)更简单。文件上传限制前面已经说了不重复。排到一起说是因为它们经常一并出现——跨域导致图片上传失败调整完CORS又发现文件大小超限两个问题像是约好了一起来。6.3 时间格式化与全局异常处理的细节前端展示订单创建时间时发现格式一直是2024-06-01T10:30:00而不是期望的2024-06-01 10:30:00。原因是Java 8的LocalDateTime序列化成JSON时默认走了ISO8601格式。解决办法有两个在字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)在application.yml里配置全局格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8我建议用全局配置因为项目里的时间字段不止一个全局配置能一次解决所有的问题。再说Long类型精度丢失的坑。我把数据库自增主键设计为BIGINTJava里用Long接。如果主键值大于JavaScript的Number.MAX_SAFE_INTEGER9007199254740991前端拿到的ID会变成失真的数字。比如后端返回的订单号1752345678901234567前端可能变成1752345678901234500导致详情查询失败。解决办法是在返回给前端的VO里把ID转为String或者JsonSerialize(using ToStringSerializer.class)。全局异常处理也是一个被很多人低估的模块。我在项目里实现了RestControllerAdvice统一捕获业务异常、参数校验异常、系统异常返回统一的{code, message, data}结构。这样做的好处是前端只需要统一处理一种错误响应结构而不是每个接口各返回各的格式联调效率翻倍。6.4 部署上线后真实压出来的问题系统部署到服务器之后做了一轮最基础的并发测试果然暴露了问题。一是首页商品列表在没有缓存的情况下每次刷新都查数据库QPS到了50时数据库连接池占用就开始飙升。后来加了Redis缓存把首页热门商品和分类列表缓存5分钟压力立刻降下来了。需要注意缓存穿透和缓存雪崩——我做了空值缓存和随机过期时间防止热点key集中失效导致数据库被冲垮。二是数据库连接池用的是HikariCPSpring Boot默认默认最大连接数是10在并发测试里出现过connection is not available, request timed out的错误。后来把最大连接数调到了50并在连接池空闲时间、超时时间上做了调优spring: datasource: hikari: maximum-pool-size: 50 minimum-idle: 10 connection-timeout: 30000三是商品列表的大字段description和图片字段image_urls在列表页根本用不上但默认SELECT *会全部查出来导致响应体巨大。后来在Mapper里写了一个精简版VO查询只select列表展示需要的字段响应体直接从几KB降到几百字节。一些最后想说的话这个系统从最初的一时兴起到最后完整落地、部署上线前前后后用了大概三周。回头看最大的收获不在于学会了多少框架API而在于把一个想法变成一个可用的产品的过程中逼着自己做了大量教科书里不会讲的实操决策。比如版本选型时选择稳定而不是追新存储设计在规范化和性能之间的平衡信任机制从业务规则到代码落地的层层思考。如果你也要做类似的springboot主题项目我的建议是不要把注意力全放在写代码上先花两天把业务边界和表结构设计想清楚然后动手前想清楚每一个状态流转和并发场景这样后面代码写起来就是水到渠成而不是边写边改、边改边崩溃。最后分享一个实操小技巧给别人演示系统的时候先把测试数据准备得丰富一点——几十个用户的评论、上百件带真实图片的商品、几组不同状态的订单这些数据能让你的演示效果拉满。毕竟一个空荡荡的平台和一个热闹的二手市场站在评审老师的角度看完全是两个系统。