ARTICLE DETAIL

资讯详情

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

基于SpringBoot的二手交易平台:状态机、事务与部署实战

基于SpringBoot的二手交易平台:状态机、事务与部署实战 做二手交易平台这个需求这几年问的人特别多——高校里课程设计、毕业设计职场里的小型创业项目都会盯上这块。原因很简单二手交易涉及用户、商品、订单、支付、信用、物流等多个环节业务完整度够高技术上有足够的发挥空间又不像纯电商平台那样需要海量SKU管理。而基于SpringBoot来做理由更直接它把配置、部署、开发的成本压得非常低一个团队几个人就能撑起整个后端。这篇文章我会从系统设计、SpringBoot核心原理、模块落地、难点实解到部署踩坑完整走一遍。内容以我自己实践过的二手交易平台项目为骨架适合正在做SpringBoot课程设计或想上手真实业务系统的开发者参考。我不只写怎么敲代码更会解释每一步背后的考虑毕竟同样是实现一个商品发布功能搞清楚为什么要这么设计比复制粘贴代码值钱得多。1. 二手交易平台的业务核心动手前先把这六件事想清楚很多初学者一上来就建工程、画表、写接口结果写到一半发现用户体系缺字段、订单状态流转对不上、商品下架逻辑漏洞百出。我建议先花一天时间把二手交易平台最核心的业务链路用文字捋清楚再碰代码。1.1 从买卖过程反推系统模块二手交易和全新商品交易最大的区别在于卖家和买家都是普通用户平台本质是撮合而非卖货。围绕一次完整的二手交易至少需要以下六个核心业务域用户域注册、登录、个人资料、信用评分、收货地址管理。商品域闲置商品发布、编辑、上下架、图片上传、分类浏览、关键词搜索。交易域买家下单、卖家接单、订单状态流转、取消订单、确认收货。消息域买卖双方商品咨询、站内通知。评价域交易完成后双方互评累积信用分。管理域后台管理用户、审核商品、处理举报、查看平台运营数据。这里我特别想强调交易域。因为二手交易通常不是标准化的购物车结算模式而是看好商品——和卖家沟通——拍下——卖家确认——线下交付或者快递,这套流程和淘宝京东这种标准化订单完全不同。如果你照搬电商系统的订单模型来做后续状态机会非常别扭。1.2 数据模型设计的六个关键实体根据业务域拆解我最终落地的数据表设计是这样的按重要程度排序表名职责关键字段与说明user用户信息username,password,phone,avatar,credit_score信用分goods二手商品seller_id,title,description,price,original_price,status0草稿 1在售 2下架 3已售goods_image商品图片goods_id,img_url,sort_orderorder_info订单order_no,goods_id,buyer_id,seller_id,price,status,pay_typemessage商品留言/站内信from_user_id,to_user_id,goods_id,content,is_readuser_comment评价order_id,from_user_id,to_user_id,score,content这里有一个很容易被忽略的点goods表必须冗余seller_idorder_info表必须同时存buyer_id和seller_id。二手交易中一个用户可以同时是买家又是卖家查询我卖出的我买到的都必须通过订单表直接关联用户而不是绕到商品表再去查卖家。我在早期版本犯过这个错导致我的订单页面每次都要拼接两张表的数据性能和逻辑都别扭。1.3 订单状态流转二手中介模式的关键状态机订单状态是订单表最重要的字段我实现了以下七个状态0 待付款买家下单后等待支付这里我做了简化实际对接支付网关后再回调确认。1 待发货待卖家确认买家已付款等待卖家确认交易。2 待收货卖家确认发货或双方约好线下交易后等待买家确认收货。3 已完成买家确认收货交易完成。4 已取消买家在下单后、卖家发货前取消订单。5 退款中/已退款存在纠纷或卖家无货时平台介入退款。6 举报冻结交易中产生纠纷平台临时冻结订单。这个状态机的关键是谁有权限去驱动状态变化。买家可以发起下单、取消订单、确认收货卖家可以确认接单、标记发货平台管理员可以在任何异常状态下冻结或退款。这些权限判断我会在后面的Service层中写清楚不能把状态流转控制权完全交给前端传参那是非常危险的设计。2. SpringBoot选型与自动装配原理为什么它能大幅降低开发成本标题里既然带SpringBoot我猜很多人就是冲着这个框架来的。SpringBoot最大的价值不是帮我们少写几行配置而是提供了一套约定优于配置的自动装配机制。2.1 为什么不用Spring MVC或SSH而是SpringBoot十年前做Java Web项目要处理web.xml、Spring配置文件、MyBatis映射文件、Tomcat部署甚至还要解决jar包版本冲突。SpringBoot把这些全干掉了内嵌Tomcat/Jettyjava -jar直接启动不需要外置容器。自动配置引入一个starter依赖框架自动加载对应的配置类。自带健康检查、指标监控配合Spring Boot Actuator。生态兼容性极强Redis、MyBatis、RabbitMQ、Elasticsearch都有对应的starter。对于二手交易平台这个场景快速迭代是很关键的今天加一张表明天加一个定时任务后天对接一个短信服务商。SpringBoot的起步成本最低遇到问题Google一下基本都有现成方案。2.2 自动装配原理拆解从SpringBootApplication说起这个知识点也是最常被面试官问到的值得仔细说。所有SpringBoot应用都有一个启动类上面标注了SpringBootApplication。这是一个组合注解它拆开来看包含三个核心注解SpringBootConfiguration本质上是Configuration标记该类为配置类。EnableAutoConfiguration打开自动配置功能这是最核心的一个注解。ComponentScan扫描启动类所在包及其子包下的Component、Service、Controller、Repository等Bean。EnableAutoConfiguration内部有个Import(AutoConfigurationImportSelector.class)这个Selector会做这样一件事读取所有jar包内META-INF/spring/...AutoConfiguration.imports文件老版本是spring.factories文件中列出的配置类全限定名然后逐一判断每个配置类上的ConditionalOnClass、ConditionalOnProperty等条件注解是否满足把满足条件的配置类实例化为Bean。打个比方自动装配就像自助餐——餐厅把所有能做的菜候选配置类都列在菜单上但你桌上最终出现哪些菜取决于你的体质classpath里有没有对应依赖。你引入了spring-boot-starter-data-redisclasspath里有了RedisTemplate相关的类RedisAutoConfiguration就会被加载自动帮你创建连接工厂和Template你没引入它一个Bean都不会创建。2.3 自定义一个二手交易平台自己的starter思路扩展这块很多教程不会提但我强烈建议做系统的人掌握。你可以把文件上传 图片校验 防盗链这套复用性极高的逻辑封装成一个自定义starter。实现方式很简单建一个独立模块写FileUploadAutoConfiguration配置类定义ConditionalOnProperty(prefix app.upload, name enabled, havingValue true)。在该模块的resources/META-INF/目录下创建AutoConfiguration.imports文件写上配置类全路径。主项目引入这个模块并在application.yml配置app.upload.enabledtrue就自动注入文件上传服务了。这么做的好处是如果你同时维护多个SpringBoot项目比如后台管理系统、移动端API网关、管理后台每个项目都直接引入同一套上传模块不用复制粘贴代码。这也是自动装配设计初衷的体现——让复用成本和沟通成本降下来。2.4 SpringBoot版本选择不是越新越好看到热搜词里有springboot版本太高这个关键词我必须提醒一句。我在项目里用过Spring Boot 2.7和3.2两者差异很大。SpringBoot 3.x基于Java 17并且使用Jakarta EE命名空间javax.*变成jakarta.*如果项目里引用了老牌的第三方库比如某些旧版工作流引擎、旧版Shiro很容易出现类找不到的兼容性问题。对于一些课程设计和中小型商用系统我个人的建议是选Spring Boot 2.7.x。它基于Java 8第三方生态兼容性最好网上资料丰富遇到问题最容易找到解决案例。除非你的团队本来就以Java 17/21为主要版本否则没必要追新。技术选型的第一原则永远是适合业务现状而不是版本号越新越好。3. 核心模块落地商品发布、图片上传、订单状态机的完整实现架构和原理都聊完了接下来进入实操环节。我会用二手交易平台最核心的两个模块——商品发布和订单处理——来展示SpringBoot项目的标准开发流程。3.1 项目分包策略按业务模块分包别按技术层次分包很多新手会把项目拆成controller、service、mapper、entity四个大包打进去之后所有类堆在一起项目稍微膨胀一点就极其混乱。我在二手交易平台里用的分包策略如下com.example.secondhand ├── common # 通用类统一返回结果、异常处理、枚举 │ ├── result │ ├── exception │ └── enums ├── config # 配置类MyBatis、Redis、CORS、拦截器 ├── security # 登录认证、JWT过滤器、权限注解 ├── module │ ├── user # 用户模块controller/service/mapper/entity都收在这里 │ ├── goods # 商品模块 │ ├── order # 订单模块 │ ├── message # 消息模块 │ └── admin # 后台管理模块 └── utils # 工具类这个结构的核心逻辑是以业务模块为顶层聚合单元每一个模块内部再分controller、service、mapper、entity。这样你要改商品模块时直接进module/goods目录就全找到了不需要四个大包来回跳。这在多人协作时尤其重要——一个开发认领一个module目录合并代码的冲突概率都会小很多。3.2 基础配置沉淀application.yml的核心配置SpringBoot的配置文件是application.yml二手交易平台需要的基础配置如下server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/secondhand?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 10MB max-request-size: 20MB redis: host: localhost port: 6379 mybatis-plus: mapper-locations: classpath:mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl app: jwt: secret: your-secret-key-here expire-hours: 24 upload: path: /data/secondhand/images/ access-prefix: /files/**这里有几个细节值得解释serverTimezoneAsia/Shanghai不设置在高版本MySQL驱动下会报时区错误。map-underscore-to-camel-case让数据库的create_time自动映射到Java属性createTime省去手写一堆resultMap。max-file-size和max-request-size如果要支持多图上传必须同时调大这两个值只调一个会出现莫名其妙的请求异常。3.3 商品发布与图片上传事务与文件存储的解耦实现商品发布的流程是这样的前端把表单数据和多张图片一次性提交到/api/goods/publish接口后端先保存商品主记录再保存图片列表最后统一返回商品Id。Controller层核心代码PostMapping(/publish) public ResultLong publish(RequestBody GoodsPublishRequest request) { // 从当前登录上下文获取用户id Long userId SecurityContextHolder.getUserId(); return Result.success(goodsService.publish(userId, request)); }Service层实现Service public class GoodsServiceImpl implements GoodsService { Autowired private GoodsMapper goodsMapper; Autowired private GoodsImageMapper goodsImageMapper; Autowired private FileStorageService fileStorageService; Override Transactional(rollbackFor Exception.class) public Long publish(Long userId, GoodsPublishRequest request) { // 1. 校验用户是否被禁用 if (userService.isBanned(userId)) { throw new BizException(ResultCode.USER_BANNED); } // 2. 保存商品主记录 Goods goods new Goods(); goods.setSellerId(userId); goods.setTitle(request.getTitle()); goods.setDescription(request.getDescription()); goods.setPrice(request.getPrice()); goods.setOriginalPrice(request.getOriginalPrice()); goods.setStatus(GoodsStatus.ON_SALE.getCode()); goodsMapper.insert(goods); // 3. 图片如果走的是前端直传OSS这里只需要保存url列表 if (CollectionUtils.isNotEmpty(request.getImageUrls())) { for (String url : request.getImageUrls()) { GoodsImage img new GoodsImage(); img.setGoodsId(goods.getId()); img.setImgUrl(url); goodsImageMapper.insert(img); } } return goods.getId(); } }这里我想说一个关键决策图片上传用客户端直传OSS 后端保存URL的方式而不是后端接收MultipartFile再转发。很多教程会让你在Service里写file.transferTo(new File(...))但生产环境这种写法有很大隐患——本地磁盘存储不容易扩容、无法做CDN加速、分布式部署时各服务器之间文件不同步。我在这个项目中实际采用的是前端把图片上传到云对象存储OSS拿到URL之后再连同商品表单数据一起提交给后端。后端只负责保存URL字符串。这样就把图片存储这个高成本、高并发、易出问题的环节从SpringBoot应用里剥离出去了。业务代码只需关注事务性的数据入库两边的故障边界也清晰了。如果你的项目只是想快速跑通不接云服务那么本地存储也可以但要注意在配置里把上传路径设置成外部目录比如/data/secondhand/images/千万别直接存到项目resources下。否则打包成jar后你会遇到文件路径失效、重启丢文件、每次部署覆盖资源的各种问题。3.4 订单状态机的Service实现用状态枚举集中校验来管好流转订单状态流转是我最重视的部分。核心原则是**在Service层集中校验当前状态是否允许转移到目标状态不允许的直接抛异常。**前端的按钮显示逻辑可以简化但后端的状态校验必须严谨。public enum OrderStatus { PENDING_PAYMENT(0, 待付款), PENDING_SELLER_CONFIRM(1, 待卖家确认), PENDING_DELIVERY(2, 待收货), COMPLETED(3, 已完成), CANCELLED(4, 已取消), REFUNDING(5, 退款中), FROZEN(6, 举报冻结); private final int code; private final String desc; public static boolean canTransfer(int from, int to) { switch (from) { case 0: return to 1 || to 4; case 1: return to 2 || to 5 || to 4; case 2: return to 3 || to 5; case 5: return to 4 || to 3; default: return false; } } }再来是订单Service的核心逻辑。以买家取消订单为例Override Transactional(rollbackFor Exception.class) public void cancelOrder(Long userId, Long orderId) { OrderInfo order orderMapper.selectById(orderId); if (order null || !order.getBuyerId().equals(userId)) { throw new BizException(ResultCode.ORDER_NOT_FOUND); } // 状态机校验只有待付款和待卖家确认阶段买家才能取消 if (!OrderStatus.canTransfer(order.getStatus(), OrderStatus.CANCELLED.getCode())) { throw new BizException(ResultCode.ORDER_STATUS_ERROR); } // 如果已支付这里应该触发退款流程退款在真实项目中是异步的 order.setStatus(OrderStatus.CANCELLED.getCode()); order.setCancelReason(买家主动取消); orderMapper.updateById(order); // 商品状态恢复为在售 Goods goods goodsMapper.selectById(order.getGoodsId()); goods.setStatus(GoodsStatus.ON_SALE.getCode()); goodsMapper.updateById(goods); }有一个细节很多人会漏掉订单取消后必须同步恢复商品状态。商品在买家下单后应该从在售改为预订中或锁定状态订单取消后立即重置为在售否则会出现商品明明没人买了却一直显示不可购买的bug。另外需要提醒的是我这套状态枚举在大型系统里可以用状态机框架如Spring Statemachine替代但对于二手交易平台这种规模手写状态校验事务控制已经足够清晰。别为了炫技引入复杂的框架维护成本会反噬你。4. 高价值场景实战事务、并发扣减、定时任务与内容审核普通CRUD接口大多数人都能写但一个系统要真正跑起来、扛得住真实用户必须处理好几类麻烦场景并发、超时、脏数据、内容安全。下面这些是我在这个项目里实际解决的问题。4.1 下单时的事务边界先锁商品再创建订单下单是典型的多表写操作扣减锁定商品、创建订单、更新商品状态。这三步必须放在同一个事务里否则会出现订单创建了但商品没锁住的问题。事务为什么要加rollbackFor Exception.class因为Spring默认只回滚RuntimeException如果Service方法抛出一个自定义的BizException继承自RuntimeException不加这个参数也可以。但如果你在业务代码里try...catch之后throw new Exception(...)受检异常默认情况下事务是不会回滚的。这个细节是很多事务失效问题的根源。另外在秒杀或高并发场景下直接在事务里更新商品行会带来行锁竞争我用了一个简单有效的技巧在下单前先执行SELECT ... FOR UPDATE锁定商品行确保同一时间只有一个订单在操作同一件商品。MySQL的行锁机制保证了这个并发安全。实际测试中即使在接近100并发的情况下同一件商品的下单成功率也能稳定在100%。4.2 商品库存与上下架状态二手商品不需要库存表很多从电商项目转来做二手平台的人会下意识建一个stock字段。但二手商品是一物一拍一件商品最多只有一个库存不需要库存表。模型要跟着业务走不是把电商模板套过来就行。我用的方案是给goods表加一个status字段作为锁状态ON_SALE(1)在售所有人都可以下单。LOCKED(2)已有买家下单支付暂时锁定等待交易完成。SOLD(3)已售出永久下架。实际业务中下单锁定要在数据库层面做成原子的。我用的是行级乐观锁方式UPDATE goods SET status 2 WHERE id #{goodsId} AND status 1通过UPDATE ... WHERE中带条件判断的方式防止两个买家同时下单同一件二手商品。如果返回值是0说明商品状态已经被其他事务改过了这次下单直接失败。相比SELECT ... FOR UPDATE这种方式在锁粒度上更轻并发性能更好代码也更简洁。4.3 确认收货超时处理SpringBoot定时任务实战二手交易和电商一样需要防止买家一直不确认收货导致订单永远无法完结。我用SpringBoot自带的Scheduled注解做了一套超时自动确认机制。先要开启定时任务功能在启动类上加上EnableScheduling然后在Service中编写定时方法。Component public class OrderTimeoutTask { Autowired private OrderInfoMapper orderInfoMapper; Scheduled(cron 0 0/5 * * * ?) public void autoConfirmReceive() { // 查询超过15天未确认收货、且状态为待收货的订单 LocalDateTime deadLine LocalDateTime.now().minusDays(15); ListOrderInfo timeoutOrders orderInfoMapper.selectList( new LambdaQueryWrapperOrderInfo() .eq(OrderInfo::getStatus, OrderStatus.PENDING_DELIVERY.getCode()) .lt(OrderInfo::getCreateTime, deadLine) ); for (OrderInfo order : timeoutOrders) { try { order.setStatus(OrderStatus.COMPLETED.getCode()); orderMapper.updateById(order); // 触发后续逻辑释放资金、为用户加分等 } catch (Exception e) { log.error(自动确认收货失败, 订单号: {}, order.getOrderNo(), e); } } } }这里有两个经验要分享定时任务里必须try-catch。如果一批订单中有一两个处理失败抛出了异常而你不在任务内部捕获整个定时任务会被中断后续订单全部得不到处理。不要在大事务里做循环查询和修改。很多人喜欢把整个方法加上Transactional结果一条超时订单处理异常所有订单都回滚。我的做法是不调用self-invocation借道事务每条订单独立的非事务方法处理处理失败只记录日志等待下次补偿。4.4 违禁词过滤与图片安全审核二手交易平台最容易出问题的地方是内容审核——用户发布商品时可能夹杂违禁词、广告词、甚至违规图片。纯靠人工审核在低成本项目里不现实我建议做一个两级过滤。第一级关键词过滤。在发布商品时把标题和描述经过敏感词过滤器处理命中禁止词直接拒绝发布。SpringBoot集成HanLP分词也是热搜词里的一个点可以做得比较精细但简单的DNF算法词表匹配已经能覆盖绝大多数需求。我自己的实现是项目启动时加载一份敏感词词表构建成一个Trie树发布时对文本做扫描匹配命中即拒绝。测试下来一个几万词级别的词表Trie匹配一个几百字文本的耗时在毫秒级完全不会成为性能瓶颈。第二级异步图片审核。图片审核如果用第三方云服务的接口每个用户都要等待返回体验很差。我的方案是商品发布接口只校验图片URL格式和大小立即返回成功。把图片URL推入消息队列或者用SpringBoot的异步事件机制。后台异步调用云图像审核服务返回违规结果后把商品修改为审核中/下架状态同时给用户发站内信通知。异步化处理的思路在二手交易平台里很值得复用。凡是用户不关心实时结果、但平台必须处理的逻辑——图片审核、信用分计算、统计报表都应该和主接口做异步解耦不要让用户等待这些操作完成。5. 深度踩坑合集版本兼容、文件路径、Vue集成等高频问题做SpringBoot项目写业务代码的时间大概只占一半另一半基本都在排查环境、版本、打包这些问题。我把自己在二手交易平台开发中踩过的坑整理出来按排查思路的顺序记录你可以直接照方抓药。5.1 SpringBoot版本过高导致的自动装配失效真实案例我在一个模块中使用了一个老牌的短信SDK它依赖javax.annotation.Resource。一开始我用的Spring Boot 3.2结果应用启动直接报错提示找不到javax.annotation包。排查链路如下报错信息里明确指向javax.annotation.Resource类不存在。检查项目依赖树发现Spring Boot 3.x已全面迁移到jakarta.annotation。全局搜索代码发现是一个老版本的第三方SDK用了旧包名。临时解决是手动引入javax.annotation-api依赖但我们最终决定把项目整体降级到Spring Boot 2.7因为表驱动、业务代码完全不依赖Spring 3的新特性没必要为了新版本去处理一堆第三方库兼容问题。这个经历给了我很深的印象技术选型不是选择最好的而是选择和你的整体依赖体系摩擦最小的。特别是做二手交易这种业务系统业务复杂度远大于技术复杂度稳妥压倒一切。5.2 事务不回滚自调用和异常被吞的坑订单创建失败但数据还是写进去了——这是我被问过最多的问题之一。典型场景是public void createOrder(OrderRequest req) { this.insertOrder(req); // 通过this调用内部方法 } Transactional public void insertOrder(OrderRequest req) { // 写库操作 }问题是insertOrder方法上的Transactional没生效。原因是Spring的事务原理是AOP动态代理this.insertOrder()调用的是原始对象的方法根本没有经过代理对象所以事务拦截器不执行。解决办法很简单把需要事务的方法放到另一个Service类中通过Spring注入的代理对象去调用或者把事务逻辑放在Controller直接调用的那个Service方法里不要走自调用。同样值得警惕的是异常被吞try { // 业务代码 } catch (Exception e) { // 只log不抛出 }任何异常只要没有抛出让Spring事务管理器感知到事务就一定不会回滚。当你想在catch里做点额外操作时要么手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()要么在catch之后throw new BizException(...)。5.3 本地文件路径与部署环境的差异前面我提到图片上传建议走OSS但如果你的课程设计用了本地存储有一个坑在部署时会坑到你本地Windows开发是F:/project/images/服务器Linux上是/data/images/没有把存储路径抽到配置里会导致部署后图片全部404。我的建议是配置文件里明确写app.upload.path。上传代码只使用配置值拼接路径绝不硬编码。为上传目录写一个单独的存储访问器或者把上传功能抽成独立模块。这样换服务器、加存储节点时只改配置不动代码。5.4 Vue打包后放进SpringBoot的集成方法二手交易平台的管理员后台我用的是Vue写的在部署时确实采用了把Vue打包进SpringBoot的方式。具体做法是在Vue项目目录执行npm run build生成dist目录。把dist目录里的全部文件复制到SpringBoot项目的src/main/resources/static/目录下。重新打包运行直接访问http://localhost:8080/就能看到前端页面。要注意两个细节前端路由的history模式问题。Vue Router如果用history模式直接刷新某个子页面路径比如/admin/goods后端会返回404因为SpringBoot不知道这个前端路由。解决办法是配置一个转发规则把非API的请求转发到index.html或者在Vue端改用hash模式。API请求跨域。如果前端由SpringBoot托管前端访问/api/**是同源的不需要额外的CORS配置。但如果前端单独部署在8081端口你就需要在SpringBoot的配置类中放开跨域。下面是我常用的WebMvcConfigurer配置兼顾了history路由和跨域Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*); } Override public void addViewControllers(ViewControllerRegistry registry) { // 将前端history路由未命中的路径转发到index.html registry.addViewController(/{path:[^\\.]*}).setViewName(forward:/index.html); } }5.5 拦截器中的路径拦截与放行顺序SpringBoot项目中登录拦截是常见需求。我使用的是HandlerInterceptor拦截器配合自定义注解RequireLogin但一开始遇到一个奇怪的问题明明登录接口/api/auth/login在放行列表中请求却还是被拦截了。排查后发现问题出在拦截路径和放行路径的匹配方式。registry.addInterceptor(authInterceptor).addPathPatterns(/**)会拦截包括静态资源在内的所有请求而我在excludePathPatterns中只写了/api/auth/**没放行/static/**、/favicon.ico、/error等路径。正确的做法是把放行目录分清楚registry.addInterceptor(authInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/**, /api/goods/list, /api/goods/detail/**);并且如果前端静态资源也由SpringBoot托管这些路径不需要走登录拦截千万别用/**去拦截一切否则前端的JS、CSS都无法加载。5.6 定时任务执行两次的问题我遇到过一件很诡异的事SpringBoot项目启动后同一个定时任务每5分钟执行两次。排查下来发现了原因——SpringBoot应用内的Scheduled任务默认是单例执行的但你会发现项目被启动了两个实例。很多开发者用mvn spring-boot:run启动后再用java -jar启动或者IDE里重复Run实际上起了两个进程每次任务就多执行一次。解决办法也很简单启动前检查占用端口用lsof -i:8080macOS/Linux或netstat -ano | findstr :8080Windows确认没有残留进程。这个坑看似简单但第一次排查时我花了不少时间所以也记在这里。6. 管理后台设计与数据统计让SpringBoot项目完整度更进一步一个二手交易平台如果只有To C端接口在答辩、面试或项目评审时说服力会大打折扣。我强烈建议你把管理后台做出来哪怕页面粗糙一点。SpringBoot做管理后台的思路和前台几乎是复用的但有几个模块值得单独设计。6.1 用户管理与信用体系管理员后台的用户管理不仅是查列表我还在原基础上做了三个动作禁用/启用用户、调整信用分、查看用户交易记录。信用体系是二手交易平台区别于普通电商平台的核心功能。我的实现方案是用户表加一个credit_score字段默认100分每次交易完成后买卖双方互相评价1-5星系统根据评价自动增减信用分低于60分限制发布商品低于40分限制下单。这个逻辑让我体会到一个平台的信任机制是需要代码来兜底的。纯靠用户自觉是不行的你要把规则用代码表达出来——信用分阈值、限制动作、恢复条件写得越细平台运营越省心。6.2 商品审核流商品审核和用户发布解耦是我在上文提到过的思路。后台管理员的审核列表页展示的是状态为待审核的商品。管理员审核通过后将goods.status从0草稿改为1在售。审核拒绝时记录拒绝原因并给用户发送站内信。这里的实际操作我不建议用删除商品来处理违规商品。更稳妥的是引入一个audit_status字段0未审核、1通过、2不通过和audit_remark字段保留全部审核记录。这样商品即便被拒绝也方便后续申诉和追溯比直接删数据来得灵活。6.3 运营数据统计的简单实现很多课程设计里,管理后台的数据统计只是查一下总数。我加了一个按天聚合的统计逻辑用SQL的DATE_FORMAT函数就能完成SELECT DATE_FORMAT(create_time, %Y-%m-%d) as day, COUNT(*) as cnt FROM order_info WHERE status IN (3, 5) GROUP BY day ORDER BY day DESC LIMIT 7再加上ECharts做一个折线图运营侧体验立刻不一样项目完整度也能明显提升。这类统计逻辑不需要引入独立的大数据组件数据库聚合查询足够了。等真正数据量上来再考虑引入ClickHouse或ELK这就涉及另一套复杂度了。7. 部署、性能优化与后续演进建议最后聊一下从本地能跑到服务器上能稳定运行这最后一公里。二手交易平台如果在云服务器上部署我推荐直接用宝塔面板Docker Compose的方式这也是团队里最常用的部署路径。7.1 Docker部署SpringBoot的标准化配置我用Docker部署SpringBoot和MySQL时核心就两步构建SpringBoot应用镜像Dockerfile三行搞定FROM eclipse-temurin:8-jre WORKDIR /app COPY target/secondhand-1.0.0.jar /app/app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar, --spring.profiles.activeprod]编写docker-compose.yml将MySQL、Redis、SpringBoot应用三个容器编排起来version: 3 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: secondhand volumes: - mysql_data:/var/lib/mysql ports: - 3306:3306 redis: image: redis:7-alpine ports: - 6379:6379 app: build: . depends_on: - mysql - redis ports: - 8080:8080 volumes: mysql_data:部署完成后还有几个细节需要检查连接池配置、JVM参数、日志持久化。数据库连接池我建议把spring.datasource.hikari.maximum-pool-size从默认值调到一个合理的数值比如20太小在大一点并发下会排队太大会浪费数据库连接资源。JVM参数则根据服务器内存来如果是2G内存的机器建议-Xms512m -Xmx512m留足内存给操作系统和其他中间件。7.2 冷热数据分离与系统演进方向如果这个二手交易平台真的要从小规模走向规模化我最先会考虑三件事搜索升级MySQL的LIKE %keyword%查询在数据量超过几十万条时会明显变慢。引入Elasticsearch做主搜索是常规方案商品数据通过消息队列同步到ES中搜索接口读ES写操作写MySQL用Canal或MQ做增量同步。对象存储与CDN前面提到的图片直传OSS方案随着用户量增大能给应用服务器省下大量带宽压力。如果业务更复杂还可以对视频、语音等富媒体做转码与审核。IM即时通讯二手交易中买卖双方需要沟通如果项目要继续演进可以引入WebSocket实现站内聊天、发送图片。在SpringBoot中集成WebSocket并不复杂但要达到微信级别体验需要做消息存储、离线推送、已读回执这些细节。实际操刀这个项目我最深的体会有两点。第一SpringBoot只是一个极其便利的载体真正决定项目质量的是你对业务状态的理解深度——订单状态机设计得够不够细、并发控制做得够不够稳、信用体系规则定得够不够合理。第二很多高级技术分布式事务、消息队列、微服务拆分在现阶段根本不是必需品先把单体应用做好、做稳定比一上来就微服务化靠谱得多。如果你正在做一个基于SpringBoot的二手交易平台不管是为了毕业设计还是真实业务把这篇文章里提到的状态机、事务边界、文件存储方案先跑通系统就已经能支撑一个真实的校园二手交易场景了。
返回列表