zhshop 重构完成。先交代结论:这不是一次推倒重来的“新写一遍”,而是把商城类业务系统从“能跑就行”捋到了“能长期改不炸”的状态。如果你手头也维护着类似商品、订单、会员、营销交织在一起的后台工程,这篇复盘可以直接当检查清单用。
这次重构的复盘按四个维度展开:工程骨架、模块边界、数据库迁移、接口兼容。最终落地的核心目标不是“代码更漂亮”,而是三个字:可维护、可回滚、可灰度。文章中会出现的目录结构和接口示例,属于通用的商城系统重构模板,直接复制到你的项目时,记得把工程名、包名、端口和部署路径替换成实际值。
文章后面会依次给出重构前的能力拆解、环境准备、多模块工程结构、数据库版本迁移、统一响应体、接口兼容方案、回归验证方法、性能观察手段和常见问题排查清单。适合正在准备重构的 Java 后端开发、技术负责人,以及刚开始接手遗留商城系统的同学。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目定位 | 商城/交易类业务系统的重构过程复盘,本文以 zhshop 为代号 |
| 重构目标 | 可维护性、可观测性、可回滚性、可灰度能力 |
| 核心改造点 | 工程结构分层、配置外置、数据库迁移受管、统一响应、接口版本化 |
| 是否需要全新硬件 | 不需要,主要依赖常规研发环境 |
| 推荐技术栈 | 以 Spring Boot / MyBatis / MySQL / Redis 为例,实际项目按现状替换 |
| 推荐 JDK | JDK 17 或项目当前稳定版本,不强制升级 |
| 启动方式 | 本地微服务/单体多模块启动,或 Docker Compose 一键拉起 |
| 是否支持接口 API | 支持,原接口保留兼容,新增接口统一走 v2 风格 |
| 是否支持批量迁移 | 支持分批次、分模块灰度迁移 |
| 交付物 | 模块化工程骨架、版本化数据库脚本、统一返回对象、回归验证脚本 |
| 适合场景 | 遗留商城系统维护、历史代码结构混乱、需要快速接新需求的任务 |
上面这些不是理论上的“最佳架构”,而是这次重构过程中真正产生收益的部分。任何一条如果和当前团队技术栈不一致,都可以先保留现状,只引入对应的规范和验证手段。
2. 适用场景与使用边界
2.1 什么时候适合做这种重构
不是所有系统都需要立刻重构。判断标准很简单:代码修改频率、新增需求难度、线上问题定位速度。
如果系统已经出现以下现象,重构优先级就很高:
- 每加一个商品属性要改动 5 个以上的 Service 文件,且经常改漏。
- 订单详情查询 SQL 超过 500 行,一个慢查询拖垮整个数据库连接池。
- 配置文件分散在多个环境,测试环境的连接串能被研发不小心推到公开仓库。
- 接口返回格式不统一,前端对接每个页面都要写一套兼容逻辑。
- 数据库字段命名混乱,同一业务含义在 3 张表里有 3 种写法。
- 新同学入职一个月,看完代码仍然不敢动营销模块。
2.2 什么时候不应该做重构
反过来,下面这些情况建议先保留现状,不要为了“技术情怀”重写:
- 系统已经停止迭代,只做日常运维,重构投入无法回收。
- 团队没有人能完整解释核心交易链路,贸然拆分容易引入资金风险。
- 没有自动化回归手段,纯靠人工测试,重构完成后很难验证。
- 业务正在爆发式增长,新功能优先级远高于技术债务清理。
2.3 使用边界与合规提醒
重构过程中会接触大量线上数据。涉及真实用户信息、订单数据、支付凭据、优惠券策略时,需要严格遵循最小化原则:不要因为本地调试方便就导出完整生产库。测试环境使用的数据要做脱敏处理,手机号、身份证、地址等敏感字段必须打码。不要把包含真实数据的日志提交到 Git 仓库。
同时,重构商城系统时要留意业务边界。商品图片、品牌素材、营销文案等可能存在版权约定,内部重构与功能演示时注意授权范围。支付、退款、会员积分等资金相关模块,不管代码怎么重构,原有的事务边界和财务对账逻辑不能随意改变。
3. 重构到底重构什么
讨论重构之前,先明确一个容易被混淆的点:不同领域说的“重构”不是一回事。
机房重构,是物理基础设施的拓扑重组;图吧工具箱这类工具软件重构版,更多是前端形态、打包方式和功能组织方式的重新设计;永磁同步电机 FOC+SVPWM 控制里的相位偏移与扇区重构,则是算法运行时的计算分段被重新划分。它们和代码重构共享一个逻辑:让原有组件以新的边界重新组合,对外表现不变或更稳定。
zhshop 这次重构关注的是软件工程层面的边界重组。商城的核心业务是商品、库存、订单、支付、会员、营销,彼此之间又有大量交叉。重构不是把模块拆得越细越好,而是把变化频率不同的代码分开。
拆解时可以按下面六个维度盘点:
| 维度 | 关注点 | 常见问题 |
|---|---|---|
| 接入层 | 对外接口格式、鉴权、限流 | 响应结构不统一,错误码混乱 |
| 应用层 | 业务用例编排、事务边界 | Service 方法过长,事务粒度错误 |
| 领域层 | 商品、订单、会员等核心逻辑 | 业务规则散落在 Controller 和 SQL 里 |
| 基础设施层 | Redis、MQ、文件存储、第三方 SDK | 客户端初始化代码重复 |
| 数据层 | 表结构、索引、迁移脚本 | 缺少版本管理,改表靠手工执行 |
| 运维部署 | 启动脚本、环境配置、日志采集 | 配置文件不统一,环境难以复制 |
4. 环境准备与前置条件
开始重构前,先把“地基”对齐,否则后面每一步都可能返工。
4.1 研发环境检查清单
| 项目 | 建议 |
|---|---|
| 操作系统 | Windows / Linux / macOS 均可 |
| JDK | 项目当前稳定版本,示例使用 JDK 17 |
| 构建工具 | Maven 3.8+ 或 Gradle,按项目原工具链选择 |
| 数据库 | MySQL 8.x 或项目现有版本 |
| 缓存 | Redis 6+,如项目未使用可跳过 |
| Git | 必须,重构前确认所有代码已提交 |
| 本地数据库 | 准备独立的重构验证库,不要直接连生产 |
| 接口调试工具 | curl、Postman 或 Apifox |
| 远程调试工具 | 按需准备 Arthas,排查线上问题时有用 |
4.2 重构前的代码准备工作
第一步,提交基线。在新建重构分支前,确保主干分支可构建、可部署。用当前主干打一个 tag,例如release-pre-refactor,方便随时回退。
第二步,盘点模块。把所有功能模块整理成一张表格,至少包含模块名、负责人、对外接口数量、核心表、当前状态。没有这张盘点表,重构过程中很容易漏模块。
第三步,统一包名。如果历史代码里存在多个根包名混用,例如com.old.shop、com.test.shop、com.demo.zhshop,先统一成一个根包。包名统一可以在 IDEA 里做批量重命名,但要注意检查配置文件、MyBatis mapper 扫描路径、Spring 包扫描配置和日志配置。
第四步,确认外部依赖。部分功能依赖短信服务、对象存储、支付网关等第三方 SDK,先确认测试环境的密钥和回调白名单是否可用,避免重构后无法完整跑通支付回调。
5. 重构实施步骤
5.1 建立多模块工程骨架
单体应用重构不一定要一步拆成微服务。把单模块 Maven 工程拆成多模块,已经能解决大量依赖混乱和编译时间问题。下面是一个典型的商城工程结构:
zhshop-root ├── zhshop-common # 通用工具、响应体、异常定义 ├── zhshop-framework # 安全、日志、Redis、MyBatis 配置类 ├── zhshop-admin # 管理后台接口入口 ├── zhshop-api # 面向小程序/App的接口入口 ├── zhshop-modules │ ├── zhshop-product # 商品模块 │ ├── zhshop-order # 订单模块 │ ├── zhshop-member # 会员模块 │ └── zhshop-marketing # 营销模块 └── zhshop-job # 定时任务父工程的pom.xml可以按照下面的方式组织。多模块只是第一步,更重要的是让模块间的依赖方向一致:admin和api依赖modules,modules依赖framework,framework依赖common,避免出现订单模块反向依赖商品模块内部实现的情况。
<project> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>zhshop-root</artifactId> <version>2.0.0-SNAPSHOT</version> <packaging>pom</packaging> <modules> <module>zhshop-common</module> <module>zhshop-framework</module> <module>zhshop-modules</module> <module>zhshop-admin</module> <module>zhshop-api</module> <module>zhshop-job</module> </modules> </project>5.2 配置外置与环境隔离
重构前容易踩的一个坑是配置硬编码在代码里。改造时至少要把以下内容全部挪到配置文件:
- 数据源地址、用户名、密码。
- Redis 地址与密码。
- 第三方服务的 appId、secret、回调地址。
- 文件存储的 bucket 和访问域名。
- 日志级别。
约定每个环境一个配置文件,启动时通过spring.profiles.active激活。
# application.yml server: port: 8080 spring: application: name: zhshop-admin profiles: active: dev datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/zhshop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root redis: host: 127.0.0.1 port: 6379 database: 0 # 自定义配置,使用 `${}` 占位符,不把密钥硬编码进代码 zhshop: upload: endpoint: ${OSS_ENDPOINT:http://127.0.0.1:9000} access-key: ${OSS_ACCESS_KEY:minioadmin} secret-key: ${OSS_SECRET_KEY:minioadmin}生产环境通过环境变量注入密码,效果更好。例如启动时传入:
SPRING_PROFILES_ACTIVE=prod OSS_ACCESS_KEY=真实密钥 \ java -jar zhshop-admin.jar --server.port=80805.3 统一返回响应体
商城接口数量多,如果没有统一响应结构,前端每个请求都要自己判断data、message、code的格式。重构阶段定义一个全局返回对象,让所有接口返回统一结构。下面是使用 Java Record 的写法,JDK 8 项目可以改成普通类。
public class Result<T> { private int code; private String message; private T data; private long timestamp; public static <T> Result<T> ok(T data) { Result<T> result = new Result<>(); result.code = 200; result.message = "success"; result.data = data; result.timestamp = System.currentTimeMillis(); return result; } public static <T> Result<T> fail(int code, String message) { Result<T> result = new Result<>(); result.code = code; result.message = message; result.timestamp = System.currentTimeMillis(); return result; } // getter/setter 省略 }Controller 基本只负责参数接收和返回,不写大量业务逻辑。统一格式后还方便在网关层做日志记录和异常拦截。
5.4 数据库迁移纳入版本管理
很多历史商城系统的库表结构没有脚本管理,测试环境和线上环境经常“差几张表”。重构期间,所有建表和字段变更都要纳入迁移脚本。
推荐使用 Flyway 管理,也可以使用 Liquibase。下面是 Flyway 的依赖配置思路。
<dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-mysql</artifactId> </dependency>启动时如果发现迁移脚本有变更,Flyway 会自动执行未执行过的脚本。脚本放在src/main/resources/db/migration下,命名规则建议如下:
V1__init_schema.sql V2__product_optimize_index.sql V3__order_add_snapshot_fields.sql V4__member_split_nickname.sql这类脚本一旦执行成功,不要回头修改。如果需要调整表结构,新建一个版本号更大的脚本。第一次整理时,可以先从当前线上库生成一份 baseline 脚本,并设置 Flyway 的baseline-on-migrate=true。
如果不想引入 Flyway,至少要建立一个sql/migration目录,并约定所有变更脚本必须有版本号、执行时间、执行人和回滚说明。
5.5 数据库表结构调整示例
商城重构中比较典型的表结构改造,是订单表逐渐膨胀后做字段拆分。下面的 SQL 只是示例,实际字段以业务为准。
-- 订单主表只保留高频查询字段 CREATE TABLE `order_main` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号', `member_id` BIGINT NOT NULL COMMENT '会员ID', `order_status` TINYINT NOT NULL COMMENT '订单状态', `pay_amount` DECIMAL(10, 2) NOT NULL DEFAULT 0 COMMENT '支付金额', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_member_status` (`member_id`, `order_status`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '订单主表'; -- 低频详情字段单独存储,避免大字段拖慢列表查询 CREATE TABLE `order_ext` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `order_id` BIGINT NOT NULL, `receiver_name` VARCHAR(64) DEFAULT NULL, `receiver_phone` VARCHAR(32) DEFAULT NULL, `receiver_address` VARCHAR(512) DEFAULT NULL, `buyer_remark` VARCHAR(512) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '订单扩展信息表';表拆分过程中最重要的是保持查询可回滚。第一步先建新表,业务读写双写并做数据对比,确认没有问题后再切换查询逻辑,最后再删除旧表字段。这种“先建后切”的思路比一次性在大表上 DROP 字段安全得多。
5.6 前端联调与跨域配置
如果系统包含前端管理后台,重构后需要同步调整接口地址。开发阶段遇到跨域问题,可以在前端工程中配置代理,而不是在浏览器里临时关闭跨域校验。以 Vite 为例:
// vite.config.js export default { server: { port: 3000, proxy: { '/api': { target: 'http://127.0.0.1:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } }如果使用 Nginx 部署,可以按下面的方式配置反向代理。实际路径需要根据服务端路由确认。
server { listen 80; server_name shop.example.com; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }配置完成后,通过浏览器访问http://127.0.0.1:3000/api/product/list,如果接口返回 JSON,说明代理链路已经打通。
5.7 旧接口兼容与版本化
重构期间最怕业务方还在依赖旧接口,后端新代码字段命名却调整了。应对方法是保留一层兼容适配,而不是直接删除旧接口。
接口路径规划时建议统一带上版本前缀。管理后台接口使用/admin/api/v1/xxx,面向 C 端的接口使用/api/v1/xxx。新增的不兼容接口放到 v2 下,旧客户端仍访问 v1。对外文档和前端代码中明确标注版本号。
@RestController @RequestMapping("/api/v1/product") public class ProductV1Controller { @GetMapping("/list") public Result<List<ProductVO>> list() { return Result.ok(new ArrayList<>()); } }如果某些内部模块之间调用了其他模块的 Mapper 或 Service 实现类,重构时要优先改成接口调用,而不是让商品模块直接操作订单模块的数据库表。短期看多写了一层 Adapter,长期看模块拆分后才不会变成“拆了目录,但没拆依赖”。
6. 功能测试与效果验证
重构完成后,功能验证是最高优先级的事项。没有跑通商品、订单、会员、营销四条主链路的测试,代码结构再好看也不能发布。
6.1 验证环境与数据准备
准备一套独立测试环境,库表结构用迁移脚本重建,不要直接把生产库复制到本地。准备测试账号时,使用虚拟手机号,不要使用真实手机号。准备几组典型商品,至少覆盖上架、下架、无库存、限购等场景。
6.2 分层回归测试
推荐按下面的顺序执行验证:
| 验证步骤 | 操作 | 判断标准 |
|---|---|---|
| 1. 实例启动 | 执行启动命令,观察日志 | 无异常堆栈,端口正常监听 |
| 2. 健康检查 | 调用健康检查接口 | 返回 UP 或 200 |
| 3. 数据库迁移检查 | 查看flyway_schema_history | 脚本全部 success |
| 4. 登录鉴权 | 调用登录接口获取 token | 返回 token,失效逻辑正常 |
| 5. 商品查询 | 调用商品列表与详情 | 分页正常,价格与库存字段不缺失 |
| 6. 购物车与下单 | 加入购物车并下单 | 库存扣减,订单状态正确 |
| 7. 支付回调模拟 | 用测试回调地址模拟支付通知 | 订单状态流转为已支付 |
| 8. 售后流程 | 发起退款/退货申请 | 状态正确,金额核对无误 |
| 9. 营销优惠 | 使用优惠券或满减活动 | 优惠金额与订单金额匹配 |
| 10. 定时任务 | 执行关闭超时订单任务 | 日志无异常,数据逻辑正确 |
6.3 使用 curl 做冒烟验证
服务启动后,可以使用 curl 做最基本的接口冒烟。下面的命令是通用示例,路径、参数、鉴权头按实际项目调整。
# 1. 检查服务健康状态 curl -s http://127.0.0.1:8080/actuator/health # 2. 模拟管理员登录,获取 token curl -s -X POST http://127.0.0.1:8080/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"test123"}' # 3. 携带 token 查询商品列表 curl -s http://127.0.0.1:8080/api/v1/product/list?page=1&size=10 \ -H "Authorization: Bearer 替换成真实token"如果系统没有引入 Spring Boot Actuator,健康检查接口路径会不一样。可以换成项目里已有的登录接口或预留 ping 接口。
6.4 验证失败时的定位顺序
测试过程中遇到接口报错,优先按下面的顺序排查:
- 服务日志中是否出现异常堆栈。
- 请求是否到了 Controller,如果没到,检查网关路由、Nginx 代理、拦截器。
- 接口是否报 404,检查
@RequestMapping路径和项目上下文路径。 - 是否报 SQL 错误,把日志中的 SQL 复制到测试库执行,确认字段和表名是否存在。
- 是否报参数错误,用 Swagger 或接口文档核对参数名和类型。
7. 接口 API 与批量任务设计
7.1 对外 API 与内部 API 分离
重构商城系统时,对外 API 与内部 API 必须分层。
对外 API 面向小程序、App、H5,请求可能需要登录鉴权、频率限制和参数签名校验。内部 API 面向后台管理员,鉴权方式不同,危险操作需要记录操作日志。把两类接口放在不同 Controller 下,并通过拦截器配置不同鉴权路径,是重构后期避免“后台接口被脚本刷”的基础手段。
7.2 批量任务与异步处理
商城系统中常见的批量任务有:批量上下架、批量改价、批量发放优惠券、超时订单关闭、对账文件生成。重构前很多批量操作是同步 for 循环执行,数据量大时容易阻塞请求线程。
重构时建议把批量任务改为独立线程池或消息队列处理。下面以 Java 的ThreadPoolExecutor为例,实际项目中可以替换为 MQ 消费逻辑。
import java.util.concurrent.*; public class BatchTaskExecutor { private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor( 4, 16, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(500), new ThreadPoolExecutor.CallerRunsPolicy() ); public static void execute(Runnable task) { EXECUTOR.execute(task); } }使用线程池要注意:不要在循环里 new 线程,线程池必须设置有界队列,拒绝策略使用CallerRunsPolicy,避免任务在无界队列中堆积导致内存溢出。更重要的是,每个批量任务都要有独立的任务记录表,至少记录任务批次号、执行状态、成功数量、失败数量和失败原因。
如果业务量很大,优先考虑引入 RocketMQ 或 RabbitMQ,任务发送成功后立刻返回,再由消费者处理。消息队列不能替代事务,数据库主状态仍然要保证一致。
7.3 通用批量任务表设计
| 字段 | 说明 |
|---|---|
| batch_no | 批次号,每次任务执行生成 |
| task_type | 任务类型,例如上架、改价、发券 |
| total_count | 总数 |
| success_count | 成功数 |
| fail_count | 失败数 |
| status | 待处理、执行中、成功、失败 |
| error_log | 错误摘要 |
| create_time | 创建时间 |
| finish_time | 完成时间 |
批量任务跑完后,生成一份结果文件或结果页,方便运营人员查看失败明细。
8. 资源占用与性能观察
商城系统做重构后,最容易出现的问题不是代码报错,而是资源占用异常。上线前和上线后都要观察服务端指标。
8.1 观察哪些指标
| 指标 | 观察方式 | 判断标准 |
|---|---|---|
| CPU 占用率 | top -Hp 进程PID | 长期超过 80% 需要排查 |
| 堆内存 | 使用jmap -heap PID或 JDK 自带工具 | 老年代持续增长需要关注 |
| GC 频率 | 查看 GC 日志或使用jstat -gcutil PID 1000 | 频繁 Full GC 说明内存有问题 |
| 线程数 | jstack PID检查线程状态 | 大量 BLOCKED 线程说明锁竞争严重 |
| 数据库连接数 | 查询连接池监控 | 打到上限时排查慢 SQL |
| 慢 SQL | 开启 MySQL 慢查询日志 | 超过 1 秒的 SQL 列入待优化清单 |
8.2 降低数据库压力的常见手段
重构阶段建议同步优化一批高频 SQL:
- 商品列表查询尽量只查主表,详情再查扩展表。
- 订单查询按会员ID和时间范围建联合索引,避免全表扫描。
- 不要在大表上直接
SELECT *。 - 分页查询不要使用深度
OFFSET,可以用WHERE id > 上次最大值的方式代替。 - 库存扣减使用乐观锁或原子更新,不要把库存先查回应用层再写回。
下面是一个改进后的库存扣减 SQL 示例,通过条件stock >= 扣减数量保证并发安全。
UPDATE sku_stock SET stock = stock - #{count}, update_time = NOW() WHERE sku_id = #{skuId} AND stock >= #{count};如果更新行数为 0,说明库存不足或商品已下架,业务层需要提示用户。
8.3 一台机器还是多台机器部署
重构初期推荐先使用单机多模块部署,减少运维层面的变量。等模块依赖稳定后,再把admin和api拆成两个独立进程,通过 Nginx 做同机或跨机转发。只有确实需要独立扩缩容的模块,才考虑拆成微服务。
9. 常见问题与排查方法
下面整理的是商城系统重构过程中最容易遇到的问题,按经验优先级排列。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 项目无法启动 | 包扫描路径配置错误 | 查看启动日志,检查 Spring 扫描根路径 | 统一包名,修改@SpringBootApplication扫描范围 |
| 接口返回 404 | Controller 路径未匹配 | 查看项目 context-path 和接口 mapping | 统一增加版本前缀,如/api/v1 |
| MyBatis 报找不到 statement | mapper XML 路径未扫描 | 检查mybatis.mapper-locations配置 | 指定classpath*:mapper/**/*.xml |
| 启动时循环依赖 | 模块间互相注入 | 查看BeanCurrentlyInCreationException堆栈 | 抽出中间层服务,或改用事件解耦 |
| 登录后 session 失效 | Redis 连接异常或序列化方式不一致 | 查看 Redis 日志与 key 前缀 | 统一使用 StringRedisTemplate 或 JSON 序列化 |
| 接口数据字段缺失 | 新旧 VO 转换遗漏字段 | 对比接口返回 JSON 和旧接口文档 | 补充字段映射测试 |
| 数据库连接池被打满 | 慢 SQL 或连接未释放 | 查看连接池活跃数、慢查询日志 | 优化 SQL、调大队列、关闭长期事务 |
| 新接口能用旧接口不正常 | 兼容层未做参数转换 | 对比 v1 和 v2 入参与出参 | 保留旧接口并独立适配 |
| 定时任务重复执行 | 多实例部署重复消费 | 检查任务注册方式和数据库锁 | 引入分布式锁或使用 xxl-job |
| 迁移脚本执行失败 | 字段顺序或权限问题 | 查看 Flyway 失败 SQL | 新建版本修复脚本,不修改已执行脚本 |
| 前端访问接口跨域 | 反向代理或网关未配置 | 使用浏览器 F12 查看网络请求 | 按上文方式配置代理或允许跨域预检 |
| 部署后数据出现不一致 | 新旧逻辑双写未对账 | 对比新旧两张表数据量 | 增加对账任务,写定期 diff 脚本 |
遇到问题时,先看日志,再复现,不要盲改。尤其是订单和库存相关逻辑,没有完整理解现有事务边界前,不要轻易把synchronized、分布式锁或事务注解添加进去。
10. 最佳实践与使用建议
重构不是一次性的“冲刺”,更像是把整个系统的修改方式从“不可控”变成“可控”。下面是这次 zhshop 重构完成后沉淀下来的几条建议。
10.1 先做可回滚,再做性能优化
重构时优先保证回滚路径:数据库变更脚本可回滚,代码版本可通过 Git 回退,接口保留旧版本。性能优化放到功能验证完成后单独做,不要和结构调整混在同一个发布窗口。
10.2 配置一个最小的可运行模板
准备一个脚本,能够用一条命令完成编译、迁移、启动。下面是一个简化版启动脚本模板,实际项目可能需要调整 Maven 命令和 Java 启动参数。
#!/bin/bash # 一键启动 zhshop-admin 示例 mvn clean package -DskipTests -pl zhshop-admin -am JAVA_OPTS="-Xms512m -Xmx512m -Dspring.profiles.active=dev" java ${JAVA_OPTS} -jar zhshop-admin/target/zhshop-admin.jar保存为start.sh后,每次修改代码都能用固定方式验证,减少“在我本地能跑”的问题。
10.3 加一层“重构门禁”
在 CI 流程中增加三个基础检查:编译检查、单元测试、代码规范检查。不需要很复杂,先把能自动化的部分自动化。
10.4 给每次发布留一个回滚关键词
发布前在部署文档里写好回滚步骤,回滚时只需要切回上一个 tag 并执行旧版本的数据库回滚脚本,而不是靠人肉记。
10.5 正确处理辅助工具
有同学问过“有没有负责代码重构的 skill”,这类工具可以在依赖分析、批量改包名、生成单元测试、扫描重复代码方面提升效率。但目前它们更适合当“侦察兵”和“执行者”,不适合让它独立决定架构边界。架构拆分一旦切错,影响的是整个交易链路。
10.6 在资金相关模块保持克制
重构中离用户的钱越近的代码,越要保持克制。支付回调、退款、对账、优惠券分摊这类逻辑,即使看上去有设计问题,也不要顺手改掉。正确做法是先写针对性的测试用例,把当前逻辑锁定下来,再讨论优化方案。
11. 总结与下一步
zhshop 重构完成,最值得保留的成果不是某个文件写得多优雅,而是这套系统终于可以做到:数据库变更有人管、接口版本能兼容、依赖方向不混乱、重启后能快速验证。如果下一次要扩展新的营销玩法,团队可以直接把精力放在业务规则上,而不是花三天时间梳理老代码到底改了哪几个地方。
拿到这套代码后,建议先做一件事:把商品列表接口、订单创建接口和库存扣减 SQL 分别核对一遍,确认基线和线上保持一致。最容易踩的坑,是重构过程中只关注模块结构,忘了核对最核心的金额和库存逻辑。
后续可以继续扩展的方向有三个:第一,把定时任务从业务进程中独立出去,避免任务高峰期影响接口响应;第二,为订单和营销模块补充分布式事务方案,解决后续拆服务时的数据一致性问题;第三,把这次整理的迁移脚本和回归检查项接入 CI/CD 流水线,让下一次迭代不需要重复靠人肉检查。