
2. 全文概览zhshop 重构到底做了什么在正式给出所有代码和步骤之前我想先交代这次重构的判断基础。因为只看代码改动你很难理解为什么某一段代码必须拆掉重写也很难判断自己项目里的“重构需求”是否真的成立。先说结论zhshop 的这次重构核心目标不是把代码写得更漂亮也不是为了引入某个新框架而是为了让这个电商项目能够支撑后续业务的持续变化。一个还在快速迭代业务的中小型商城系统最怕的不是代码写得“丑”而是每次改动都让团队多花半天时间梳理调用关系每次发布都牵一发动全身。重构的出发点永远应该是业务开发效率下降、系统稳定性不可控、测试成本越来越高而不是“看着不顺眼”。从外部看最近关于“重构”的话题并不少机房重构、工具箱重构版甚至有人问有没有负责代码重构的 skill。这些场景里的重构内容和电商项目完全不同但底层逻辑是一样的——上一代实现已经不适合当前的目标了。机房重构不是因为单台服务器坏了而是因为整网拓扑约束了后续的扩容和运维工具箱重构是为了形成更稳定的功能组织方式zhshop 这次面向商城领域的重构则把重点放在了模块边界、数据访问和状态流转上。本文会按下面的顺序展开先说明什么情况下你才应该启动一次重构再讲清楚重构的三个层面代码层、模块边界层、部署形态层然后用 zhshop 中的订单查询链路为例子从 Controller、Mapper、数据源、事务边界这几个维度拆解完整改造过程接着讲数据库表结构调整、双写、开关切换、灰度回滚这些容易翻车的环节最后给出常见问题排查表格和生产环境里的工程建议。如果你正在维护一个已经运行了一段时间的商城类系统或者准备对自己的项目做一次大规模重构这篇内容的实操链路应该能帮你在动手之前建立完整的风险清单。3. 判断一次重构是否值得先看这三个信号很多人把重构想得太简单以为就是“把代码重写一遍顺便升级一下框架版本”。但真实项目里重构从来不是技术部门单方面发起的通常是被业务端的卡点逼出来的。如果只是为了追赶新技术而重构大概率做到一半就失去动力最终留下一堆没迁移完的接口。我发现只有当下面三类信号频繁出现时重构才是真正必要且值得投入的。3.1 改动扩散一个状态变更牵扯五个模块电商系统的核心链路里最怕的就是一个小需求改动引发连锁反应。比如 zhshop 里调整一个订单状态你会发现订单实体类里有状态字段某个 XML 里有一段根据状态判断是否允许发货的 SQLController 里还有一段状态枚举到按钮文案的映射回调通知那边又有一套独立的状态判断。这时候你要小心因为隐藏的业务规则散落在很多地方状态机没有统一收敛。如果所有状态流转逻辑都集中在一个领域服务里调用方只关心“能不能从待支付改成已取消”那么以后新增退款中、售后关闭这类状态时就不需要四处寻找判断点。可如果没有一次重构每个新人都得先花一周时间搞清楚状态流转到底散落在哪里。3.2 发布风险改一个模块导致全服务重启单体商城最难受的是发布粒度。zhshop 在早期版本里商品、订单、会员、营销都部署在一个进程里。哪怕只是修改了一个促销活动的计算规则也要把整个应用打包发布。一个低风险的改动因为和老代码放在一起被迫承担了全链路回归的测试成本。这种问题不是说拆分部署就一定能解决但至少需要在模块边界上做出隔离。你可以暂时不拆成独立的微服务但必须先让商品模块、订单模块、会员模块在代码结构上相互独立编译期就不允许跨模块随意引用内部类。等将来并发量上来或者某个模块需要独立扩容时再把它从单体里剥离出来成本才会低很多。3.3 数据与业务不匹配无法支撑多渠道多端电商项目发展到一定阶段不再只有一个普通的前台商城可能会有商家后台、分销端、小程序端甚至是内部运营使用的管理端。早期 zhshop 的很多表结构是围绕 PC 网页时代的固定页面设计的比如订单表里有几个字段直接对应页面勾选项而不是模型层面的业务含义。一旦业务上要增加一个新的端团队就会被迫在原有字段上“硬扩”比如增加一个 channel_type 的字段来区分来源但代码里大量出现 if channelType 1 这样的判断。这种状况下重构不是可选项而是唯一能避免未来每个需求都要改判断逻辑的方案。前两个信号可能还能靠规范硬压下去但第三个信号已经说明数据库表设计和代码模型已经不再匹配业务域。这时候启动重构优先做的应该是梳理领域边界而不是简单地抽公共方法。4. 重构的三个层面代码、模块边界与部署形态我注意到一个很有意思的现象很多人讨论代码重构时只盯着某个类写得好不好看。但真正让 zhshop 这类系统“上线能跑跑久就乱”的原因通常不是代码风格问题而是模块间的依赖方向完全失控。从运维视角看机房重构的关键并不是换几台设备而是要重新设计网络拓扑、供电方案和资源池边界。商城项目的重构同样存在三个必须分开对待的层面如果只做第一个层面后面一定还会乱。4.1 代码层让每个模块内部的实现更清晰代码层重构包括变量命名、方法拆分、类职责界定、消灭重复代码、异常处理收敛等方面。这是大多数人最先想到的重构内容优点是成本低、风险小、见效快缺点是如果模块边界本身是乱的代码层的整理只能让“局部更清晰”整体依旧是一团缠绕的面条。4.2 模块边界层决定依赖方向和业务归属zhshop 在模块边界上的第一步是确定商品中心、订单中心、会员中心、营销中心各自应该拥有哪些数据和行为。你可以把模块想象成各部门部门内部的代码怎么组织是部门自己的事但部门之间不能随便调用彼此的私有方法更不能出现“订单模块修改了商品表的库存字段”这类跨越边界的行为。模块边界层重构时最需要做的一件事是明确依赖方向上层应用依赖领域层领域层依赖基础设施层但领域层绝不能反向依赖 Controller 或者某个 Mapper 的具体实现。4.3 部署形态层决定系统未来的弹性和交付链路不是说模块边界理清楚了就一定得拆成微服务。实际上很多中小型电商系统根本没有必要上全套微服务维护成本远超收益。zhshop 这次重构完成后的部署形态是订单、商品、会员等模块仍然可以打包成一个发布单元但代码层面已经为将来独立部署留好了接口。用电机控制领域的一个概念来解释这件事会更清楚。永磁同步电机的 FOC 控制里有一个“扇区重构”的概念当三相绕组接线从星形变成三角形时电流采样和 SVPWM 输出的相位基准会发生偏移不能沿用原来的扇区起点。否则电机转起来之后相序就错了轻则电流异常重则直接过流保护。把代码从一个模块迁移到另一个模块也是一样不是搬文件就能结束必须重新校准所有状态判断点和调用时序。zhshop 在重构中如果只是把接口方法从旧类复制到新类但订单状态机和数据库字段没有一起重新校准新代码也能编译通过运行几小时后才会在某个极端订单场景下暴露问题。所以我的判断是一次完整的重构代码层只是表面模块边界层和部署形态层的改动才是真正影响长期维护成本的重头戏。5. zhshop 重构需要掌握的基础概念在进入代码示例之前我先解释几个后面高频出现的概念。如果它们对你来说是老生常谈可以快速跳过。5.1 模块化与微服务并不是一回事模块化是在代码组织层面把业务按域拆开微服务是在运行进程层面把模块拆成独立部署单元。很多团队在重构时总喜欢讨论“要不要上微服务”其实应该先问自己的问题是模块之间的依赖是不是已经收敛了如果连代码层面都是商品 Controller 直接调用订单 Mapper 的内部查询方法那拆微服务的代价会非常大。更好的顺序是先做模块化重构让依赖方向变得清晰等某一块业务的资源需求和发布频率确实和其他部分不一致时再拆独立服务。5.2 防腐层防止外部系统污染内部模型防腐层是一种“翻译”的思想。zhshop 这样的商城系统会对接多个外部渠道比如第三方物流平台、支付网关、短信服务。外部系统的数据结构和内部领域模型经常不一致如果没有防腐层业务代码里就会到处出现“从第三方返回里取出某个字符串再转换成内部枚举”的逻辑一旦外部协议升级所有调用点都要跟着改。防腐层把这种转换集中到一个地方内部领域层永远只面向自己的模型。它的本质是给系统装上“适配器”让变化不会穿透整个架构。5.3 幂等重构中最容易被破坏的隐形规则幂等指同一个请求执行一次和执行多次最终业务结果保持一致。电商场景里支付回调、退款通知、库存扣减这些接口都必须具备幂等性。重构数据库表、调整代码结构时团队很容易把原先靠业务字段上的唯一索引实现幂等的逻辑漏掉只把代码搬走了索引没搬。看起来重构后所有接口都正常实际上同一个回调只要被重试一次可能出现重复发券或者重复加积分。重构完成后验证阶段一定要重新梳理所有涉及“外部重试”的接口。6. 重构准备先从现状盘点开始任何重构开始前都需要先花时间回答一个问题当前系统到底是什么样的如果这个问题没有想清楚就动手改代码很容易陷入两个极端一个是改得太浅只动了表面一个是改得太深把不该动的地方也重新发明了一遍。6.1 盘点接口与调用链首先是盘点对外接口和内部调用链。你不需要一次性把所有接口都列出来但至少要优先覆盖核心交易链路从用户浏览商品、加购物车、下单、支付回调、库存扣减到订单查询。把每一步涉及的服务类、数据表、缓存 key 和外部调用标记出来。这一步做得越细重构时就越清楚哪些调用点必须保持兼容哪些旧的内部调用可以顺手清理掉。6.2 梳理依赖关系接下来是确认模块之间的依赖关系。可以用一个简单的脚本扫描代码里 import 的包路径也可以用 IDE 的依赖分析图。重点寻找两类问题反向依赖和循环依赖。反向依赖指的是一个下层模块调用上层模块比如领域层的 Service 注入了 Controller 层面的类循环依赖则是 A 调用 B、B 又调用 A。这两种依赖会让模块边界完全失效。zhshop 重构前的旧代码里就存在过订单模块直接调用商品模块内部 Mapper 的情况而不是通过商品模块提供的服务接口。这种直查别的业务模块的数据库表是电商系统最需要警惕的坏味道。6.3 确定“重构完成”的验收标准重构开始前定义清楚“完成”的标准。如果只是说“代码改完了功能正常”最后很容易陷入没完没了的返工。更好的标准是所有核心服务在编译期不依赖其他模块的内部类依赖方向只能是从上层应用指向下层领域或基础设施数据库层面订单模块不允许直接修改商品表的字段核心交易链路具备自动化回归测试覆盖新老代码可以通过配置开关切换方便故障回滚。把这些标准写在项目文档第一页。后续每次 Pull Request 评审都应该拿它对照检查。7. 核心流程拆解以订单查询链路为例的分步重构下面用 zhshop 里最典型的一条链路来做演示——订单列表查询。这个接口几乎是商城系统的标配但它涉及的条件组合很多而且很容易踩坑。7.1 重构前的订单查询实现旧版本里订单列表查询通常是在 Controller 里直接接收查询参数然后传给一个 Mapper在 XML 中拼接大段动态 SQL。由于查询条件非常多包括时间范围、订单状态、用户 ID、商品名称、订单号等最终 XML 会变得极其复杂。来看一个简化版的问题代码文件路径是OrderController.java// 文件路径src/main/java/com/zhshop/order/controller/OrderController.java RestController RequestMapping(/order) public class OrderController { Autowired private OrderMapper orderMapper; PostMapping(/list) public ResultListOrderVO list( RequestParam(required false) Long userId, RequestParam(required false) String orderNo, RequestParam(required false) Integer status, RequestParam(required false) String startTime, RequestParam(required false) String endTime, RequestParam(required false) Long goodsId, RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size) { ListOrderDO orderList orderMapper.selectByCondition( userId, orderNo, status, startTime, endTime, goodsId, (page - 1) * size, size); return Result.ok(convertToVO(orderList)); } }这段代码的问题有几个方面一是 Controller 直接面向数据访问层业务规则无处存放以后如果订单列表需要同时过滤掉某些“逻辑删除”的订单或者按店铺权限限制可见范围就只能在 Controller 里继续加参数。二是所有查询都在一个大方法里拼 SQL路过这段代码的人不敢轻易改动因为不知道哪个前端页面传递了哪些参数组合。三是没有对“列表查询”和“详情查询”做合理区分所以后续做读写分离时很难判断哪些方法可以安全地走只读从库。7.2 引入 OrderQuery 参数对象重构第一步是把 Controller 的方法参数收敛为一个OrderQuery对象。这样新增查询字段时不需要修改方法签名也便于在服务层统一处理默认值、时间范围校正、权限过滤等逻辑。// 文件路径src/main/java/com/zhshop/order/application/query/OrderQuery.java public class OrderQuery { private Long userId; private String orderNo; private Integer orderStatus; private String startTime; private String endTime; private Long goodsId; private int pageNum 1; private int pageSize 20; public int getOffset() { return (pageNum - 1) * pageSize; } // getter setter 此处省略 }这个对象的目的并不是把所有参数都塞进去而是把一批强相关的查询条件组成一个完整的不变对象。后续如果增加渠道来源、店铺 ID、售后状态等条件只需要扩展这个 DTO。7.3 Controller 变薄Service 承接业务规则接着把 Controller 里的数据访问逻辑下沉到 Service。Controller 只负责参数绑定、简单校验和返回统一结构订单过滤的规则放在OrderQueryService里面。// 文件路径src/main/java/com/zhshop/order/controller/OrderController.java RestController RequestMapping(/order) public class OrderController { Autowired private OrderQueryService orderQueryService; PostMapping(/list) public ResultListOrderVO list(RequestBody OrderQuery query) { // 参数基本校验省略 PageResultOrderVO pageResult orderQueryService.pageQuery(query); return Result.ok(pageResult); } }Service 内部可以按业务需要做几种不同的事情填充默认排序规则、追加必要的权限过滤条件、组装 Redis 缓存 key、根据 userId 做分库分表路由等。这些逻辑放在 Controller 里会影响复用性放在 Mapper 里又会污染数据访问层。7.4 Mapper 层重构用动态 SQL 代替散落条件接下来看一下 Mapper 层的写法。重构前如果项目已经使用 MyBatis可以在 XML 里通过where标签动态生成查询条件避免where 11的形式。!-- 文件路径src/main/resources/mapper/OrderMainMapper.xml -- select idpageQuery resultTypecom.zhshop.order.infrastructure.dataobject.OrderDO SELECT id, order_no, user_id, shop_id, order_status, total_amount, create_time FROM order_main where if testquery.userId ! null AND user_id #{query.userId} /if if testquery.orderNo ! null and query.orderNo ! AND order_no #{query.orderNo} /if if testquery.orderStatus ! null AND order_status #{query.orderStatus} /if if testquery.startTime ! null and query.startTime ! AND create_time gt; #{query.startTime} /if if testquery.endTime ! null and query.endTime ! AND create_time lt; #{query.endTime} /if /where ORDER BY create_time DESC LIMIT #{query.offset}, #{query.pageSize} /select推荐用OrderQuery作为 Mapper 的入参传一个对象而非散落的多个参数。这样 Mapper 的接口可读性好也会让后续单元测试构造数据时更方便。这里真正值得注意的一点是动态 SQL 如果写得太宽会导致 MySQL 优化器无法有效使用索引。比如参数全部为空时SQL 变成了全表扫描查询几十万条订单时一次就可以把数据库打满。所以线上查询接口必须有“至少有一个必选条件”的规则比如必须传入 user_id 或者订单号管理员查询可以走另外一套接口避免全表范围查询。7.5 读写分离与只读事务重构期间另一个顺手要做的事情是让查询链路和写链路的数据源分离。因为商城系统订单查询频次很高如果所有列表查询都挤在主库上主库的压力会非常大进而影响下单这种关键写操作。推荐一种实现用Transactional(readOnly true)标记查询方法通过 AOP 或数据源路由组件把请求路由到只读从库。参考配置和代码如下。# 文件路径src/main/resources/application.yml spring: datasource: primary: jdbc-url: jdbc:mysql://127.0.0.1:3306/zhshop username: root password: change_me driver-class-name: com.mysql.cj.jdbc.Driver replica: jdbc-url: jdbc:mysql://127.0.0.1:3307/zhshop username: root password: change_me driver-class-name: com.mysql.cj.jdbc.Driver为了支持上面两个数据源需要自定义一个动态数据源在事务开启前根据readOnly标志选择目标数据源。这是一个常用的思路代码不复杂。// 文件路径src/main/java/com/zhshop/common/datasource/ReadOnlyRoutingDataSource.java import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.transaction.support.TransactionSynchronizationManager; public class ReadOnlyRoutingDataSource extends AbstractRoutingDataSource { Override protected Object determineCurrentLookupKey() { boolean readOnly TransactionSynchronizationManager.isCurrentTransactionReadOnly(); return readOnly ? replica : primary; } }使用这个方案时要避免几个常见错误Transactional(readOnly true)只对由 Spring 管理的事务方法生效同类内部调用会失效如果主从复制存在延迟刚下单后立刻查询订单列表可能会读不到最新记录。解决方案是强制走主库或者提供一个forceMaster标记在必须“先读后写”的接口中不要轻易使用只读事务否则查询到的数据可能不是最新的。7.6 用一个配置开关管理新老逻辑切换重构过程中最怕的是把老代码直接删除。更好的做法是保留一段时间的“开关切换期”通过配置中心或者 YAML 配置控制接口走新逻辑还是旧逻辑。# 文件路径src/main/resources/application.yml zhshop: refactor: order-query-mode: new # 可选值 old / new / dualold表示走旧代码逻辑new表示走重构后的代码dual表示新旧逻辑同时执行并把结果做日志比对方便灰度验证。等新逻辑稳定运行一段时间后再删掉旧逻辑分支。这一步是重构中最容易被低估的工程化能力。如果没有开关一旦重构后出现线上问题唯一回滚动作就是重新发布老版本的包。如果代码已经和数据表结构调整绑定在一起老包很可能连数据库表都不兼容回滚难度会成倍增加。8. 数据库结构调整从大订单表到主表加明细订单查询链路的重构结束之后zhshop 面临的另一个棘手问题是数据库表结构。旧系统很多模块为了查询方便把所有订单信息都塞在同一张大表里包括收货人、商品快照、优惠明细、支付信息。结果这张表字段超过 40 个每次查询都可能因为未命中的索引导致慢 SQL。数据库结构的重构目标通常是把核心领域的数据拆成主表和明细表。以订单为例可以拆成订单主表order_main保存订单号、用户 ID、店铺 ID、订单状态、实付金额、创建时间等订单级信息订单明细表order_item保存商品ID、商品名称、商品快照、单价、数量等行级信息订单扩展表order_ext保存不同渠道订单的特殊属性避免主表字段无限膨胀。一个常见的问题是拆分后查询列表需要重新 join 明细表性能反而变差了。解决方式是订单列表页只查主表不查明细如果要展示商品名称可以将主要商品信息冗余到主表中或者在详情页再查询明细。提供一段简化的建表示例-- 订单主表结构示例 CREATE TABLE order_main ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(64) NOT NULL COMMENT 订单号, user_id bigint NOT NULL COMMENT 用户ID, shop_id bigint NOT NULL COMMENT 店铺ID, order_status tinyint NOT NULL COMMENT 订单状态10待支付 20已支付 30已发货 40已完成 50已关闭, total_amount decimal(12,2) NOT NULL, create_time datetime NOT NULL, update_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_user_create (user_id, create_time), KEY idx_shop_create (shop_id, create_time), KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;这里需要注意一件事电商订单的索引设计要结合业务查询模式。不要为了“灵活”把每个字段都建索引因为索引写入成本也很高。优先保证用户查询订单列表、店铺查询订单列表、按订单号精确查询这三类高频路径的索引命中其余低频查询可以用组合条件或定时同步到查询数据库。另外当一张订单主表的数据量增长到几千万级别时任何索引都很难保证查询延迟此时要先考虑按 user_id 或者 shop_id 做水平分表而不是继续依赖数据库优化。zhshop 在这次重构中把订单表改成支持分表分库的结构但实际是否启用分表仍然取决于数据量而不是提前为不存在的问题过度设计。9. 数据迁移与兼容这是重构最容易翻车的阶段代码重构的难度低于数据迁移。原因是代码只要在测试环境跑通所有用例基本可以看到效果但数据迁移一旦有问题线上数据会处于“半新半旧”的状态追踪和修复的复杂度非常高。9.1 使用双写机制当订单表从旧结构迁移到新结构时推荐采用双写方式。写操作同时写入旧表和新表读操作优先从新表读取发现数据缺失时再读旧表。双写期间两套结构的字段映射关系要整理清楚最好有一张映射表或者写一个映射函数统一处理。双写比较大的坑在于新表结构往往更规范可能增加了一些旧表里没有的业务字段。这些字段在双写期间是空值导致新逻辑查询出来的展示不完整。解决方式是先跑批把旧数据补齐或者双写时同步从关联表读取字段补全。9.2 灰度切换策略数据表结构切换不适合一次性完成。更稳妥的节奏是第一阶段双写不切换读流量第二阶段按用户 ID 或者店铺 ID 灰度切换读流量比如先让 10% 的用户走新表查询第三阶段观察核心指标比如接口耗时、错误率、超时率第四阶段全量切换第五阶段取消双写清理旧表或旧字段。灰度切换需要一个控制层通常用 Apollo、Nacos 这类配置中心动态发布开关。如果没有配置中心也可以先放在数据库配置表或 YAML 文件里。9.3 回滚预案不能只写“重新发布”重构后一旦出问题团队第一个想法通常是改代码重新发布。但在同时存在旧表和新表的阶段回滚动作应该是动态切回旧逻辑而不是重新发布代码。所以数据迁移脚本前置到一个独立的变更目录里所有 SQL 都必须有对应的回滚 SQL。回滚 SQL 的价值在于数据库不是代码无法用版本控制直接回退。一旦新结构上线几天后发现问题旧结构的数据可能已经被新逻辑写入覆盖只有回滚 SQL 才能把结构恢复到可接受状态。10. 验证方案与结果判断重构完成后不能只说“测试环境好像没问题了”。在 zhshop 这样的商城系统里重构后的验证至少需要覆盖下面四层。10.1 接口维度的结果对比对核心查询接口在灰度阶段同时触发新老逻辑对比返回结果是否一致。这个方案可以做成一个本地小工具输入同一个订单号或用户 ID分别调用新老查询代码将返回的 JSON 做 diff。只要发现字段不一致就需要分析是数据迁移遗漏还是代码逻辑不同避免用户看到和以前不一样的页面内容。10.2 流量回放流量回放是指把线上真实请求记录下来在测试环境重新打给重构后的服务。这样做能覆盖手工测试难以构造的极端参数组合比如某个订单状态已经没有前端页面了但数据库里还残留着这种状态列表接口仍然必须能正确处理。回放结果重点看三个方面接口是否报错、耗时是否明显上升、返回数据量是否异常。10.3 监控与告警重构后的服务必须保留链路追踪日志。电商系统里一个订单查询可能跨订单中心、商品中心、会员中心如果监控只覆盖到应用层出现慢 SQL 时很难快速定位到具体是哪个模块出了问题。推荐在网关层记录完整的 traceId并透传到下游服务。一旦某个环节耗时突增能通过 traceId 直接找到完整调用链。10.4 故障演练不要等到线上出问题才测试回滚动作。可以在一台预发布机器上故意触发一次重构后异常然后执行开关切换到旧逻辑确认整个回滚过程能在几分钟内完成。否则你写的回滚预案只是纸面文档真到关键时刻手忙脚乱。11. zhshop 重构常见问题与排查方法下面把这次重构中最容易遇到的五类问题整理成表格供遇到相似情况时直接对照排查。问题现象可能原因排查方式解决方案启动失败提示 Bean 冲突新旧代码并存时存在多个相同类型的 Service 或 Mapper 实现查看启动日志中的 bean 定义冲突信息通过Primary指定默认实现或删除旧逻辑分支开关切换到 new 模式后部分订单查询不到新表数据不完整旧表同步任务延迟检查双写日志和同步任务耗时补跑数据同步或让读逻辑带兜底查询旧表开启读写分离后列表更新不生效主从复制存在延迟查询走了从库观察创建时间和查询时间的差值刚写入后的接口强制走主库或关闭这些方法上的只读事务重复触发支付回调后发放多次积分旧表的唯一索引在数据迁移时被遗漏检查支付回调表或积分流水表中是否存在唯一业务键约束增加 userId bizId 唯一索引并做幂等校验灰度期间接口耗时反而上升新逻辑中增加了多次查询或远程调用通过 traceId 查看调用链各阶段耗时合并查询批量加载关联数据减少循环调用重构后的订单状态机出现非法流转不同代码位置对状态机判断不一致搜索所有修改 orderStatus 的代码路径将状态流转收敛到领域服务禁止 Controller 直接改状态排查时有一条基本顺序先看配置开关是否正确再看数据库数据是否完整最后才怀疑代码逻辑。因为重构期间大量问题的根因是数据迁移滞后或配置错误而不是新代码写错了。12. 最佳实践与工程建议项目重构完成后我总结出几条值得长期坚持的工程习惯适用于所有准备做大型改造的团队。12.1 依赖方向必须用工具约束模块边界不是靠 Code Review 时人工提醒就能守住的。时间一长总有人图方便直接 import 其他模块的内部类。更稳妥的方式是在 CI 阶段加入依赖检查脚本禁止订单模块的代码出现商品模块内部包的 import。用工具而不是靠自觉是重构成果能维持下去的基础。12.2 每个查询方法都要有明确的默认排序和最大返回行数电商列表接口如果允许一次返回上万条数据迟早会成为慢 SQL 炸弹。重构时建议在 Service 层统一限制单页大小默认值给 20最大不超过 100。需要导出全量数据的场景单独走异步任务不要和页面查询混在一起。12.3 事务中不要执行远程调用下单过程中如果代码在事务里调用第三方支付接口或者发送短信数据库连接会被长时间占用。一次下单可能只需要几百毫秒但第三方接口超时拖到 3 秒时数据库连接池会迅速耗尽。重构时把外部调用放在事务提交之后通过事件或者消息队列异步处理。12.4 日志要输出 traceId 和业务单号排查线上问题时最怕看到一堆日志但不知道属于哪一次请求。运行日志里至少包含 traceId、orderNo、userId 三个字段。当用户反馈订单状态不对时只需要拿到其中的任意一个值就能串联起整条链路的日志。12.5 不要为了重构顺带“升级所有框架版本”一次重构的变量越少越容易控制结果。如果你想同时做模块拆分、数据库结构改造、Spring Boot 大版本升级、微服务化出问题时你将很难判断到底是哪一类改动引入的故障。建议一列一列地做每一列完整上线稳定后再启动下一列。12.6 保留业务对照用例重构时把所有核心业务场景整理成一组“黄金用例”。比如普通用户下单、未支付自动关闭、超卖保护、退款后库存回补、优惠券过期释放。每一组用例执行完后记录关键数据和页面结果。后续每次小版本迭代都先回归这一组用例比依赖测试人员临时点一遍要高效得多。13. 总结与后续方向zhshop 这次重构完成真正讲清楚的点其实是三个第一代码层的优化只是表面模块边界和部署形态才是长期维护的关键第二任何重构都需要一个可验证、可灰度、可回滚的工程路径单纯的重写代码不是合格的方案第三订单列表这样看起来最基础的接口背后涉及查询模型、只读事务、数据源路由、缓存和监控整套基础设施不能只以为是在“改一个方法”。如果你也是在维护类似的中小型商城项目下一步最值得做的是先梳理当前订单状态机和核心查询链路画出模块依赖图。不要急着把代码推到重构分支上先花一周时间把现有系统的依赖关系整理清楚。这一步做完你会对“重构成什么样子”有更具体的答案。重构后的代码也不是终点。接口幂等性需要继续压测验证读写分离后的主从延迟需要持续监控灰度开关在所有流量都切换到新逻辑之后要及时清理旧分支。否则半年后再看代码仓库里还会同时存在 new 和 old 两套实现下一次重构会变得更困难。对正准备对自己的项目动手的读者我有一个比较实际的建议第一次重构不要追求一步到位先选一条像“订单查询”这样足够核心、但边界还算清晰的链路把它完整跑通确认流程、配置、代码、验证方案都顺了再横向复制到其他模块。重构是个长期工程控制节奏比展示工作量更重要。