上个月,我干了一件所有人都劝我别干的事。
把一个3年没人动过的核心服务,用AI做了一次完整重构。
我的同事老刘知道之后,发来一条消息:
"你疯了吗?那个服务每天处理几十万笔流水,出了事你担得起吗?"
我没回他。
因为我其实也怕。
但我更怕的是,这块代码再这样烂下去,下一个踩坑的人,是我自己。
一、为什么非要动它
这个服务是3年前一个离职同事写的,核心逻辑是处理商户结算和账单生成。
没有人完整读过它。
不是因为代码量大,总共也就2000行。
是因为它太重要了,没人敢随便碰。
每次有新需求,大家的做法都一样:找一个最不重要的角落,加一段if-else,测通了就上线,绝对不碰核心逻辑。
3年下来,这个服务变成了这个样子:
BillService.java -- 1个类,823行
BillService.java里有:
- 账单生成逻辑
- 手续费计算逻辑
- 商户信息查询逻辑
- 短信通知逻辑
- 报表数据组装逻辑
- 定时任务触发逻辑
...共17个方法,最长的一个方法214行
17个方法,6种职责,全在一个类里。
上个月产品提了一个新需求:支持账单按日汇总和按周汇总两种模式,商户可以自己切换。
我打开BillService,看了20分钟,关掉了。
不是看不懂,是找不到从哪里下手。
每改一行,都不知道会影响哪里。
那天晚上我想了很久,做了一个决定:与其继续往这个泥潭里加代码,不如先把坑填了。
二、重构之前,我做了一件很重要的事
很多人一拿到要重构的代码,第一件事是打开Cursor,开始让AI分析。
我没有。
我做的第一件事,是给这个服务补单元测试。
原来的单元测试覆盖率:11%。
是的,11%。三年了,只有11%。
我花了两天时间,把核心逻辑的单元测试补到了68%。
补测试前:
- 核心方法覆盖率:11%
- 重构后怎么知道没有破坏现有逻辑?
- 靠感觉?
补测试后:
- 核心方法覆盖率:68%
- 重构后跑一遍测试
- 通过了,说明行为没变
- 这是底线,不是锦上添花
这两天,是整个重构里最关键的两天。
后来翻车,也和这件事有关。等我讲到翻车那部分你就明白了。
三、把代码喂给AI,让它先"读懂"
测试补好之后,我开始让AI介入。
第一步不是让AI重构,而是让AI读懂这个服务。
我的Prompt:
这是一个Java后端服务,核心是商户账单生成和结算逻辑。
我把主要的类和方法都贴出来了。
请帮我做一件事:
1. 用你自己的语言,描述这个服务现在在做什么
2. 找出职责划分有问题的地方(一个类/方法做了不止一件事)
3. 找出你认为重构风险最高的部分(改了最容易出问题)
4. 给出重构的优先级建议
[粘贴完整代码]
AI的分析,超出了我的预期:
服务现在在做什么: BillService承担了账单生命周期的全部管理职责:创建、计算、通知、归档。 这是一个典型的"上帝类"(God Class)反模式。
职责划分问题(3处主要):
generateBill()方法同时负责数据查询、费用计算和账单持久化, 这3件事应该拆开,当前任何一环出错都会导致整个方法失败且难以定位sendNotification()嵌套在账单生成的主流程里, 通知失败会导致账单生成事务回滚,这不合理- 定时任务的触发逻辑和业务逻辑在同一个类里, 导致单元测试需要mock时钟,增加了测试难度
重构风险最高的部分: 第87-143行的手续费计算逻辑。 这段代码包含多个嵌套条件,涉及不同商户类型的差异化处理, 注释几乎为零,改错了直接影响资金计算。 建议:这段逻辑在有充分测试覆盖之前,不要动。
重构优先级:
- 先把通知逻辑抽出来(低风险,影响面小)
- 再把定时任务触发层分离(中风险)
- 最后拆分核心账单生成流程(高风险,需要充分测试)
- 手续费计算逻辑,建议最后处理,且单独做一次专项测试
我把这份分析打印出来,钉在显示器旁边。
后来证明,AI给的这个优先级顺序,救了我一命。
四、重构过程:哪些顺了,哪些翻了
顺利的部分
第一步:把通知逻辑抽出来
按照AI的建议,先从风险最低的部分开始。
我给Cursor的Prompt:
把BillService里所有和通知相关的逻辑(短信、站内信、webhook)
抽取成一个独立的BillNotificationService。
要求:
1. BillService通过依赖注入调用BillNotificationService
2. 通知失败不影响账单生成主流程(用try-catch隔离)
3. 通知记录异步处理,不阻塞主流程
4. 保持原有的方法签名不变,只是移动代码
项目技术栈:Spring Boot 3, MyBatis-Plus
AI生成了BillNotificationService,我review了一遍,改了2处细节,跑单元测试,全部通过。
耗时:40分钟。
原来我估计要半天。
第二步:分离定时任务层
同样顺利。AI生成了BillScheduleService,把触发逻辑从业务逻辑里剥离出来。
测试通过,上灰度,监控正常。
耗时:1小时。
这两步完成之后,BillService从823行缩减到了571行,职责清晰多了。
翻车的部分
第三步,我开始动核心账单生成流程。
AI建议的是把generateBill()这个214行的大方法,按照"数据查询 → 费用计算 → 持久化"拆成3个方法。
我觉得这个方向对,让AI来做:
把generateBill()方法按照职责拆分:
1. queryBillData():负责所有数据查询
2. calculateBillAmount():负责费用计算
3. saveBill():负责持久化和事务
保持原有的@Transactional注解在主方法上
AI生成的代码,看起来很干净。
我review了一遍,没发现问题,跑单元测试,通过了。
提交,上灰度。
然后,账单金额开始对不上。
五、翻车的原因,让我沉默了很久
灰度上线后的第二个小时,风控那边发来消息:
"你们结算服务有问题,有几笔账单金额和流水对不上,差了几块到几十块不等。"
我第一反应是:不可能,单元测试全通过了。
然后我想起来了。
11%。
原来的测试覆盖率是11%。
我补到了68%,但还有32%没有覆盖。
而翻车的那段逻辑,正好在那32%里。
具体是什么问题?
AI在拆分方法的时候,把queryBillData()和calculateBillAmount()拆成了两个独立的方法调用。
但原来的generateBill()里,有一段逻辑是这样的:
// 原代码(214行大方法里的一段)
List<Transaction> transactions = queryTransactions(merchantId, startDate, endDate);
// 查完之后,要过滤掉已经计入上期账单的流水
transactions = transactions.stream()
.filter(t -> t.getBillId() == null) // 关键:只统计未出账的流水
.collect(Collectors.toList());
BigDecimal totalAmount = calculateAmount(transactions);
AI拆分之后变成了:
// AI拆分后
List<Transaction> transactions = queryBillData(merchantId, startDate, endDate);
// ❌ filter逻辑被AI"优化"掉了——它认为这个filter应该在query层做
// 但实际上,AI没有意识到这个filter依赖一个业务规定:
// "同一笔流水不能重复出账"
BigDecimal totalAmount = calculateBillAmount(transactions);
AI不知道t.getBillId() == null这个条件背后有一条业务规定。
它看到一个filter,觉得应该下沉到queryBillData里,但查询层没有加这个条件,于是这段逻辑就消失了。
结果:部分已经出过账的流水被重复计算,账单金额偏高。
这个逻辑,在原来的11%单元测试里没有覆盖。
我补的68%,恰好也没有覆盖这个边界条件。
六、处理过程
发现问题之后,我做了3件事。
第一,立刻回滚。
灰度只影响了5%的流量,受影响的商户不多,但涉及资金,回滚是第一优先级。
15分钟内完成回滚,错误金额暂停结算,等修复后重新核算。
第二,复盘根因,不是修bug。
那个消失的filter不是bug,是我和AI都没有意识到的隐性业务规则。
这条规则没有写在任何注释里,没有写在任何文档里,只存在于当年写代码的人的脑子里,而那个人已经离职了。
我用了半天,把这个服务里所有"隐性规则"全部翻出来,写成了注释,加到代码里。
一共找到了7条。
/**
* 注意:此处filter是核心业务规则:
* 同一笔流水(transaction)只能归入一期账单。
* billId不为null表示该流水已被归入历史账单,当期不得重复计算。
* 这条规则来源于结算协议第4.2条,不可删除或修改。
*
* 如需修改此逻辑,必须同时更新:
* 1. BillServiceTest.testNoDuplicateTransaction()
* 2. 通知财务团队
* ——2026年3月,重构时补充
*/
transactions = transactions.stream()
.filter(t -> t.getBillId() == null)
.collect(Collectors.toList());
第三,补测试,再重构。
把这7条隐性规则全部写成单元测试,覆盖率从68%提到了91%。
然后重新让AI重构,这次给了更详细的上下文:
重构generateBill()方法时注意以下业务规则(绝对不能丢失):
1. filter(t -> t.getBillId() == null) 是核心约束,不能移到query层
2. 手续费计算依赖商户类型,不同类型走不同分支
3. [其他6条...]
请在拆分时确保这些规则在重构后的代码中有明确体现,
并说明每条规则在新代码的哪个位置得到了保留。
这次AI生成的代码,我逐条对照规则清单做了review。
单元测试全部通过。
上线,监控正常,没有再出问题。
七、复盘:翻车教会了我什么
重构完成后,我坐下来想了很久。
翻车的根本原因,不是AI出错了。
AI做的事是对的:把一个大方法按职责拆分。这个方向完全正确。
翻车的原因是:我没有把"业务规则"告诉AI。
AI是一个技术能力很强的工程师,但它是第一天入职的那种。
它不知道结算协议第4.2条,不知道"同一笔流水不能重复出账",不知道这段filter背后有一个财务合规要求。
这些知识,不在代码里,不在注释里,不在文档里。
只存在于人的经验里。
而我,没有把这些经验传递给AI。
这次经历,让我总结出了3条重构的铁律:
铁律1:先测试,后重构。覆盖率低于80%,不要动核心逻辑。
不是80%就足够安全,而是80%以下根本不够。
我补到68%以为差不多了,结果那32%里藏着最核心的业务规则。
现在我的标准是:核心业务逻辑,覆盖率不到80%不动刀,动刀之前先补到90%+。
铁律2:让AI重构之前,先把"隐性规则"显性化。
每一段"莫名其妙"的代码,背后可能都有一个业务原因。
在让AI重构之前,把你知道的所有"不能动的理由"写成注释,写进Prompt。
AI不会帮你发现你不知道的东西,但它能帮你保留你告诉它的东西。
铁律3:分步骤重构,每步上灰度验证,不要一次性大改。
通知逻辑、定时任务层、核心流程,分3次上线,每次只动一块,出了问题影响面有限,回滚成本低。
如果我一次性把整个服务全改了再上线,出问题的时候根本不知道是哪里出了问题。
八、最终结果
整个重构从开始到稳定上线,花了3周时间。
重构前:
- BillService.java:823行,6种职责,17个方法
- 单元测试覆盖率:11%
- 最长方法:214行
- 注释密度:几乎为零
重构后:
- BillService.java:主流程,187行
- BillNotificationService.java:156行
- BillScheduleService.java:89行
- BillCalculationService.java:203行
- 单元测试覆盖率:91%
- 最长方法:67行
- 核心业务规则全部有注释说明
线上表现:
- 功能:完全一致
- 性能:账单生成耗时降低了23%(拆分后可以并行查询)
- 新需求(按日/按周汇总):在新结构上,2小时做完
原来预估2天做不完的新需求,在重构之后2小时做完了。
这才是重构真正的价值:不是让代码好看,是让后面的工作变得可能。
写在最后
那个3年没人敢碰的老服务,现在有人敢碰了。
那个人是我,但也可以是任何一个新来的同事。
AI在这次重构里帮了很大的忙——分析代码结构、生成拆分方案、写单元测试、执行重构。
但它救不了我自己没做好的事:测试覆盖不够,隐性规则没有显性化,上线步骤太激进。
翻车,是我的问题,不是AI的问题。
AI是一把很好用的刀。
但刀不会告诉你,你在切什么。
如果你的项目里也有一个"3年没人敢碰的老服务",我的建议是:
先别急着重构,先把它的测试补上去。
那个过程会让你重新读懂这段代码。
读懂了,再动刀。
带着AI一起。
你有没有类似的经历?动过"没人敢碰"的老代码吗?结果怎么样?
欢迎评论区聊聊。
后端AI实验室 不讲概念,只谈实战 代码开源,每周更新