六周重写一套在跑的 ERP:484 次提交、132 万行代码,只借鉴功能

2026-09-27 Aryee 7

我手上正在跑的一单:把一套用了几年的 ERP 重写。

旧系统我是从里面改起的——二十个独立仓库、约 1.64 万次提交,我个人的 383 次提交分布在 19 个仓里,重心全在平台层(鉴权、网关、通用日志记录、公共库)。所以我很清楚哪一部分值得留、哪一部分不值得留:平台层那套做法我带走,业务实现层一行不搬。

这次的做法是只借鉴功能:旧系统做什么,新系统就做什么;类、方法、包结构、实现全部重新写。

先划一条线:这一单不写客户名、行业、内部地址,规模数字取约数。

一、为什么一行代码都不搬

上一单那套六层里,"调用封装"这一层在多数仓里只有一行继承——它换来的是二十多个仓长得一模一样。这次团队协作规模变了,我把这类只服务一致性的目录收进了包结构。复制代码最省一时,代价是把旧系统的形状一起带进来——那形状里有当时来不及整理的分层、有为了赶上线留下的写法、有一批只有最初那批人写得动的类。

重写的判断标准很简单:这段代码里装的是业务规则,还是当时那个人的习惯? 前者照着做,后者一个字不搬。

六周的量放在这里,因为这件事没有数字就不成立:

量 数
提交 484 次(八月中旬开工,九月 27 天里交了 300 多次)
Java 文件 约 12220 个
代码行 约 132 万行
控制器 672 个
表映射(@TableName) 540 张
生效模块 27 个(12 个域各一对 api / service)
跨服务调用声明(@FeignClient) 193 处,全部收在服务侧
架构红线 16 条,写进文档并按它评审
领域关系分析 7017 行(含实体关系、边界与服务间调用方向)

二、跑得快的真正原因:平台层不在这里写

这个仓里几乎没有平台代码——鉴权、权限模型、缓存、消息、数据库访问、Excel、审计日志、国际化、幂等,全都住在我自己的基础工程 aryee-foundation 里,业务仓只是一个依赖坐标。

底座是什么形状,全部可以核对:

底座给什么 实测
模块面 24 个顶层模块,251 份构建文件,约 26 万行主代码
测试 110 个测试文件、约 35000 行测试代码、3438 个测试方法,集中在公共核心模块,加密那批工具(AES / RSA / HMAC / TOTP / 口令散列 / 转义)逐个带测试
门禁 CI 跑 mvn verify -Pcoverage(JaCoCo 覆盖率门禁)+ 静态质量扫描,且等质量门禁结果才继续
分发 发布到 Maven Central,带 GPG 签名

"开箱即用"这四个字是可以验的,验法就是看下游到底引了多少。 这一单里:13 个域的 -api 模块直接依赖底座的安全契约,业务代码里出现 1690 处权限注解、分布在 371 个文件,另有 517 处从底座拿当前用户上下文;网关侧 13 个全局过滤器把限流、请求清洗、接口签名这些事在一个地方做完。

这意味着两件事,都是钱:

  1. 平台能力测一次,不是测十二次。 缓存切换、消息通道、鉴权链路这些行为在底座里已经有测试兜着,业务仓再各写一遍只会各写一遍不同的断言。给框架接线写重复测试,是这类项目里最典型的一种浪费。
  2. 业务开发者不需要懂权限模型就能写对。 他写的是 @RequiresAbacPermission(resource = ..., action = "access") 一行注解,鉴权分支、上下文取法、审计打点都在底座里。八十人协作的系统里,"每个人都能写对"比"少数人写得漂亮"值钱得多。

三、模块切法:从六层收到两层

上一套系统每个服务固定六个模块(契约、调用封装、传输对象、实现、控制器、常量)。这次每个域只有两个:<域>-api 放契约与传输对象,<域>-service 放实现,内部再分控制器、服务、仓储、实体。

六降到二不是把分层扔了,是把只靠目录表达的分层改成靠包结构表达。 那两个只为让二十多个仓保持一致而存在的模块,多数仓里各只有一行代码——模块数不产出约束,它产出的只是文件数;真正管住边界的是每个模块里只允许放哪四类东西。

收敛之后能核对到三件实际效果:

  • 新增一个业务域,只建两个模块,接线成本从"复制六个 pom 再改依赖"降到"复制一对";
  • 跨服务调用的声明 193 处全部留在服务侧,契约模块里一处都没有——调用方向因此是单向的,不会出现两个域的契约模块互相引用;
  • 旧目录结构残留清零,抽查每个域的旧包名路径都是 0 个文件。

边界本身写在 16 条红线里,逐条可评审。红线不是装饰:这次每个域的对外契约都从红线里的调用规则生成,谁越界在评审上一次就能定位。

四、「只借鉴功能」里最难的一条:外部契约算功能

有一批对外接口的路径,是上一家甲方按自己的名字定的。这次重写,这些路径、HTTP 方法、请求体字段名、响应结构、错误码原样保留。

为什么值得为此停下重写——因为前端和第三方系统里那些地址是接死的,动一个 URL 就是动别人的系统。所以这条重写的验收标准第一条就是:现有调用方不改一行代码,能跑通同样的业务流程。

这条判据对甲方的价值很直白:重写这件事对下游系统是不可见的。 没有联调窗口、没有第三方改造排期、不需要一次全员培训。

同样保留的还有领域词汇。这个仓里一百多个 Java 文件带着客户那边一直在用的业务词,一些枚举的类名里就是那个词。这不是没洗干净,是故意的:那个业务对象本身就是那个东西,换成中性词,读代码的人反而要多查一层映射。词汇是功能的一部分,客户每天早上在系统里看到的字段名,就是他们自己的语言。

所以我给"功能"这个词的定义是三格:外部契约、数据所有权、领域词汇。 三格都保留,只有实现全部重写。这么定义之后,重写的风险面一下就清楚了——凡是保留的都有一份回归判据,凡是重造的都有一次变更评审。

五、推进量怎么留证据

重写最怕的是"看起来在做,其实说不清做到哪儿"。这一单每个阶段都留了一个能对外核对的量:

  • 端点全量对账:迁移前后 HTTP 端点数 178 → 178,丢失 0、新增 0。入口数量这条线管住了"没搬丢";行为层的等价性用黄金样本比对来管——同一批真实输入,新旧两份输出逐字段比,这一层排在下一批搬迁的最前面(做法在《没有测试的老系统,先让它自己交代行为》里)。
  • 大文件在建仓第一天就有清单和顺序:48 个两千行以上的实现类全部登记在拆分计划里,试点挑最狠的那个(单文件 9424 行、控制器 97 个公开方法),拆完编译通过、端点对账不变,这份顺序就成了后面 47 项的排期表。重写项目里的大文件不该是写完之后再治的病。
  • 一致性边界随业务长:跨服务写入口上的全局事务标注目前有 146 处,每一处都对应"这个动作的中间态不能被人读到"的一次判定。
  • 文档先行:架构总说明、16 条红线、迁移计划、领域重构计划、领域关系分析、网关与鉴权与权限分析、开发规范共十二份,先有边界再落代码。同一套文档我给下一个项目的交付物清单也会照搬——文档是这个项目的产出之一,不是过程消耗。

一句实话放在这儿比藏在注释里好:这套做法的产出量不来自加班,来自该写的代码少了一半——平台层不用写,契约不用重新设计,词汇不用重新解释。

六、四条你可以直接抄走的

  1. 重写先分"规则"和"习惯"。 旧代码里装业务规则的照着做,装当时那个人写法的重写掉。不做这个区分,重写就是一次昂贵的复制。
  2. 平台层沉到独立底座,并且把它当成产品维护。 覆盖率门禁、静态质量扫描、发到公共仓库去——只有底座是被独立验证过的,业务仓才敢不重复写那些代码。
  3. 把"功能"定义成外部契约 + 数据所有权 + 领域词汇三格,开工第一天逐格签字。前两格一般会被想到,第三格一定被漏掉,然后以"零历史包袱"的名义悄悄背回来。
  4. 每个阶段留一个可对外核对的量。 端点对账、契约模块里的越界引用数、旧目录残留文件数——这三个数不需要测试框架就能算,但它们让"进度"这个词第一次有了可验证的形态。

收口

六周这个点上,这个仓有 27 个生效模块、约 132 万行新写代码、16 条红线、12 份设计文档、178 个端点对齐、1690 处权限注解,业务代码里零行鉴权实现——权限是注解,上下文是从底座取的。

这套做法我已经用在上一单的架构上(《新系统从 0 到 1 怎么搭:一套上线几年的微服务架构,逐层拆开》),现在正在把它用在重写这一单上。方法论是同一套,只是这次先量的是我自己。

如果你手上也有一套"还在赚钱但没人敢动"的系统,可以带着它的模块清单来聊一次:我给它一份保留 / 重写的分档判断,和一条按域的推进顺序。

你手上那套系统,卡在哪一步?

留个手机号和方便的时间,我打过来先听你说现状与约束,再给可执行的判断:这套系统是该继续修、该动哪里,还是干脆重做一版设计更省。

约一次沟通不接纯 UI 外包,只做系统层面的问题。