返回笔记首页

第 09 集:AI 辅助重构遗留代码

主题配置

一、什么是遗留代码

遗留代码(Legacy Code)不等于旧代码。Michael Feathers 在《修改代码的艺术》里给出了一个精准定义:

遗留代码 = 没有测试的代码

更宽泛的定义:难以理解、难以修改、不敢动的代码。

常见特征:

  • 函数很长,一个方法做了很多事情
  • 命名模糊,看不出变量/函数的用途
  • 大量 any 类型,类型信息丢失
  • 幻数(Magic Number),代码里出现没有解释的数字
  • 重复代码,同一段逻辑在多处出现
  • 没有错误处理,或者错误处理方式不对
  • 没有测试,改了不知道会不会破坏其他功能

二、代码坏味道(Code Smell)完整列表

Martin Fowler 在《重构》里总结了 20+ 种代码坏味道,以下是后端开发最常见的几种:

2.1 函数过长(Long Method)

表现:一个函数超过 30 行,做了多件事情

危害:难以理解,难以测试,改一处容易影响其他逻辑

重构方向:提取方法(Extract Method),把不同职责的逻辑拆成独立的私有方法

typescript
// 重构前:processOrder 方法做了 5 件事
async processOrder(data: any) {
  // 计算折扣(30 行)
  // 验证用户(20 行)
  // 创建数据库记录(10 行)
  // 发送通知(15 行)
  // 更新库存(10 行)
}

// 重构后:每件事是独立的私有方法
async processOrder(data: CreateOrderDto) {
  const discount = this.calculateDiscount(data.amount);
  const user = await this.validateUser(data.userId);
  const order = await this.createOrderRecord(data, discount);
  await this.sendNotification(user, order);
  await this.updateStock(data.items);
  return order;
}

2.2 幻数(Magic Number)

表现:代码里出现没有命名的数字或字符串

危害:6 个月后完全不知道这个数字从哪来的、为什么是这个值

typescript
// 重构前
if (data.amount > 10000) {
    discount = data.amount * 0.1
} else if (data.amount > 5000) {
    discount = data.amount * 0.05
}

// 重构后
const DISCOUNT_RULES = {
    HIGH_THRESHOLD: 10000,
    MID_THRESHOLD: 5000,
    HIGH_RATE: 0.1,
    MID_RATE: 0.05,
} as const

if (data.amount > DISCOUNT_RULES.HIGH_THRESHOLD) {
    discount = data.amount * DISCOUNT_RULES.HIGH_RATE
}

2.3 重复代码(Duplicated Code)

表现:同一段逻辑在多个地方出现

危害:需要修改时要改多处,容易漏改

typescript
// 重构前:两个方法里都有相同的用户验证逻辑
async processOrder(data) {
  const user = await this.prisma.user.findUnique({ where: { id: data.userId } });
  if (!user) throw new NotFoundException('用户不存在');
  if (!user.isActive) throw new ForbiddenException('用户已被禁用');
  // ...
}

async cancelOrder(data) {
  const user = await this.prisma.user.findUnique({ where: { id: data.userId } });
  if (!user) throw new NotFoundException('用户不存在');
  if (!user.isActive) throw new ForbiddenException('用户已被禁用');
  // ...
}

// 重构后:提取为私有方法
private async validateUser(userId: number): Promise<User> {
  const user = await this.prisma.user.findUnique({ where: { id: userId } });
  if (!user) throw new NotFoundException('用户不存在');
  if (!user.isActive) throw new ForbiddenException('用户已被禁用');
  return user;
}

2.4 any 类型滥用

表现:大量使用 any 类型,放弃了 TypeScript 的类型保护

危害:类型错误在运行时才发现,IDE 无法提供代码提示

typescript
// 重构前
async create(data: any) {
  const result: any = await this.prisma.user.create({ data });
  return result;
}

// 重构后
async create(createUserDto: CreateUserDto): Promise<Omit<User, 'password'>> {
  const { password, ...rest } = createUserDto;
  const hashedPassword = await bcrypt.hash(password, 10);
  const user = await this.prisma.user.create({
    data: { ...rest, password: hashedPassword },
    select: { id: true, username: true, email: true, createdAt: true },
  });
  return user;
}

2.5 不恰当的异常处理

表现

  • throw new Error(...) 代替 NestJS 异常类
  • 空的 catch 块(catch(e) {}
  • 捕获了异常但没有处理
typescript
// 重构前
if (!user) {
    throw new Error('user not found') // 会变成 500,而不是 404
}

// 重构后
if (!user) {
    throw new NotFoundException('用户不存在') // 正确返回 404
}

2.6 副作用(Side Effects)

表现:函数修改了传入的参数,调用方不知道参数被改了

typescript
// 重构前(直接修改入参,有副作用)
async processOrder(data: any) {
  data.discount = this.calculateDiscount(data.amount);  // 修改了 data
  data.finalAmount = data.amount - data.discount;        // 修改了 data
  data.status = 'PENDING';                               // 修改了 data
  return await this.prisma.order.create({ data });
}

// 重构后(用新对象,无副作用)
async processOrder(createOrderDto: CreateOrderDto) {
  const discount = this.calculateDiscount(createOrderDto.amount);
  const orderData = {
    ...createOrderDto,
    discount,
    finalAmount: createOrderDto.amount - discount,
    status: OrderStatus.PENDING,
  };
  return await this.prisma.order.create({ data: orderData });
}

三、安全重构的核心原则

原则一:先有测试,再动代码

没有测试就重构 = 在没有安全网的情况下走钢丝。

重构流程:

plain
1. 为现有代码(有坏味道的代码)写测试
2. 确认测试全绿(验证现有行为)
3. 开始重构
4. 每改一步,运行一次测试
5. 测试保持全绿,重构安全

原则二:小步重构,频繁测试

每次只改一个坏味道,改完就跑测试,而不是一次改很多再测试。

原因:如果一次改了 5 个地方,测试失败了,不知道是哪个改动导致的。

原则三:重构不改业务逻辑

重构的定义:在不改变代码外部行为的前提下,改善代码的内部结构

如果在重构的同时修改了业务逻辑,出了问题分不清是重构导致的还是逻辑变更导致的。


四、演示用的遗留代码

在项目里新建 src/legacy/order-processor.service.ts,写入以下内容:

typescript
import { Injectable } from '@nestjs/common'
import { PrismaService } from '../prisma/prisma.service'

// 这是演示用的遗留代码文件,包含多种典型坏味道

@Injectable()
export class OrderProcessorService {
    constructor(private p: PrismaService) {} // 坏味道1:单字母变量名

    // 坏味道2:函数过长,做了太多事情(超过 50 行)
    // 坏味道3:any 类型
    // 坏味道4:没有 JSDoc 注释
    async processOrder(data: any) {
        // 坏味道5:幻数
        if (data.amount > 10000) {
            data.discount = data.amount * 0.1
        } else if (data.amount > 5000) {
            data.discount = data.amount * 0.05
        } else if (data.amount > 1000) {
            data.discount = data.amount * 0.02
        } else {
            data.discount = 0
        }

        // 坏味道6:直接修改入参(副作用)
        data.finalAmount = data.amount - data.discount
        data.status = 'PENDING'
        data.orderNo = 'ORD' + Date.now()

        // 坏味道7:数据库操作和业务逻辑混在一起
        const user = await this.p.user.findUnique({
            where: { id: data.userId },
        })

        // 坏味道8:用 Error 而不是 NestJS 异常类
        if (!user) {
            throw new Error('user not found')
        }

        // 坏味道9:重复代码(cancelOrder 里也有这段)
        if (!user.isActive) {
            throw new Error('user is inactive')
        }

        const order = await this.p.order.create({ data })

        // 坏味道10:console.log 在生产代码里
        console.log('order created', order.id)

        return order
    }

    // 坏味道11:和 processOrder 高度重复的用户验证逻辑
    async cancelOrder(data: any) {
        const user = await this.p.user.findUnique({
            where: { id: data.userId },
        })
        if (!user) {
            throw new Error('user not found')
        }
        if (!user.isActive) {
            throw new Error('user is inactive')
        }

        const order = await this.p.order.findUnique({
            where: { id: data.orderId },
        })
        if (!order) {
            throw new Error('order not found')
        }

        // 坏味道12:幻字符串
        if (order.status !== 'PENDING') {
            throw new Error('only pending orders can be cancelled')
        }

        return await this.p.order.update({
            where: { id: data.orderId },
            data: { status: 'CANCELLED' },
        })
    }
}

五、演示操作步骤

第一步:创建遗留代码文件

bash
mkdir -p src/legacy

把上面第四章的代码写入 src/legacy/order-processor.service.ts

第二步:为遗留代码生成测试(安全网)

在 Cursor Chat 里输入:

plain
@src/legacy/order-processor.service.ts

为 OrderProcessorService 生成单元测试文件 src/legacy/order-processor.service.spec.ts。

目的:在重构前先验证当前行为,作为重构的安全网。

覆盖场景:
- processOrder:正常流程(amount = 6000,用户存在且激活)
- processOrder:用户不存在时抛出 Error('user not found')
- processOrder:用户未激活时抛出 Error('user is inactive')
- cancelOrder:正常取消 PENDING 订单
- cancelOrder:订单不是 PENDING 状态时抛出 Error

注意:Mock 要覆盖 this.p(PrismaService),字段是 user 和 order

运行测试,确认全绿:

bash
npm test -- order-processor.service.spec.ts --verbose

必须在测试全绿之后才能开始重构

第三步:AI 分析坏味道

在 Cursor Chat 里输入:

plain
@src/legacy/order-processor.service.ts

分析这个文件里所有的代码坏味道,按以下格式输出:

### [编号]. [坏味道名称]
- 位置:第几行,哪个方法
- 问题描述:具体是什么问题
- 危害:这个问题会带来什么后果
- 重构方向:应该怎么改

按危害程度从高到低排序

记录 AI 给出的坏味道清单,用于后续逐步重构。

第四步:制定重构计划

在 Chat 里输入:

plain
根据上面的坏味道分析,帮我制定重构计划。

要求:
1. 从风险最低的改动开始(不改逻辑,只改形式的改动放最前面)
2. 每步改动范围要小,改完就运行测试
3. 用步骤编号列出,每步说明:改什么、为什么先改这个

AI 给出的计划通常是以下顺序(从低风险到高风险):

plain
第一步:提取常量(幻数 → 有名字的常量)
第二步:修复异常类型(Error → NestJS 异常类)
第三步:重命名变量(单字母 → 有意义的名字)
第四步:提取重复的用户验证逻辑(→ 私有方法)
第五步:拆分 processOrder 函数(→ 多个小方法)
第六步:修复 any 类型(→ 具体的 TypeScript 类型)
第七步:删除 console.log

第五步:执行重构(逐步进行)

重构第一步:提取常量

在 Chat 里输入:

plain
执行重构第一步:提取 processOrder 方法里的所有幻数为命名常量。

在类定义上方创建常量对象:
const DISCOUNT_RULES = {
  HIGH_THRESHOLD: 10000,
  MID_THRESHOLD: 5000,
  LOW_THRESHOLD: 1000,
  HIGH_RATE: 0.1,
  MID_RATE: 0.05,
  LOW_RATE: 0.02,
} as const;

用这些常量替换方法里的幻数。不要改其他任何东西。

改完立即运行测试:

bash
npm test -- order-processor.service.spec.ts --verbose

确认全绿后继续。

重构第二步:修复异常类型

在 Chat 里输入:

plain
执行重构第二步:把所有的 throw new Error(...) 替换成 NestJS 异常类。

替换规则:
- 'user not found' → NotFoundException('用户不存在')
- 'user is inactive' → ForbiddenException('用户已被禁用')
- 'order not found' → NotFoundException('订单不存在')
- 'only pending orders can be cancelled' → BadRequestException('只有待处理的订单可以取消')

记得在文件顶部 import 这些异常类。不要改其他任何东西。

改完运行测试。

注意:这步改了抛出的异常类型,测试文件里的断言也需要更新(原来断言抛 Error,现在要改成断言抛 NotFoundException 等)。

在 Chat 里说:

plain
测试文件里的断言用的是 Error,但代码已经改成了 NestJS 异常类,
请同步更新测试文件里对应的断言

重新运行测试,确认全绿。

重构第三步:重命名变量

在 Chat 里输入:

plain
执行重构第三步:把构造函数里的 private p: PrismaService 改成 private prisma: PrismaService,
并把文件里所有 this.p. 替换为 this.prisma.。不要改其他任何东西。

运行测试,确认全绿。

重构第四步:提取重复的用户验证逻辑

在 Chat 里输入:

plain
执行重构第四步:把 processOrder 和 cancelOrder 里重复的用户验证逻辑提取成私有方法。

私有方法签名:
private async validateUser(userId: number): Promise<User>

方法逻辑:
1. 查找用户
2. 用户不存在抛 NotFoundException
3. 用户未激活抛 ForbiddenException
4. 返回用户对象

然后在 processOrder 和 cancelOrder 里调用这个方法,删除重复代码。

运行测试,确认全绿。

重构第五步:删除 console.log

在 Chat 里输入:

plain
执行重构第五步:删除所有 console.log 语句。
如果需要日志,注入 NestJS Logger 并替换为 this.logger.log。
不要改其他任何东西。

运行测试,确认全绿。

第六步:对比重构前后

查看 git diff,对比重构前后的差异:

bash
git diff src/legacy/order-processor.service.ts

统计变化:

bash
# 重构前行数
git show HEAD:src/legacy/order-processor.service.ts | wc -l

# 重构后行数
wc -l src/legacy/order-processor.service.ts

重构后的代码应该具备:

  • 无幻数,所有数值都有名字
  • 无重复代码,用户验证逻辑只在一处
  • Error,全部是 NestJS 异常类
  • console.log
  • 变量名有意义

测试依然全绿,行为没有改变。


六、重构常见问题

问题 原因 处理方式
重构后测试失败 不小心改了业务逻辑 用 git diff 找出改了什么,回退那部分改动
测试本身需要更新 异常类型或返回值结构变了 同步更新测试文件的断言,这是正常的
AI 一次改了太多地方 Prompt 没有写"不要改其他任何东西" 每次重构 Prompt 结尾加"不要改其他任何东西"
不知道从哪里开始 没有制定重构计划 先让 AI 分析坏味道,再制定计划,按计划执行

Spec Coding 实战补充:09 AI 辅助重构遗留代码

来源:Spec Coding实战/09 AI 辅助重构遗留代码.md,已合并到本章节。

1. 遗留代码的真实面目

"遗留代码"不一定是很久以前写的代码。上个月写的代码,如果没有测试、没有文档、耦合严重,它就已经是遗留代码了。

Michael Feathers 在《修改代码的艺术》里给遗留代码的定义很直接:没有测试的代码

遗留代码的典型特征:

  • 一个函数 200 行,干了十件事
  • 数据库查询和业务逻辑混在一起
  • 魔法数字满天飞(if (status === 2),2 是什么没人知道)
  • 错误处理要么没有,要么是 catch (e) { console.log(e) }
  • 函数名和它做的事情不一致
  • 全局状态,函数互相依赖

AI 在重构遗留代码这件事上非常有用,但有一个前提:你要先让 AI 理解代码,再让它提建议,最后才动手改。 不能直接说"帮我重构这个文件"就完事了。


2. 第一步:理解遗留代码

在改之前,先让 AI 读懂它。

plain
读取 @src/legacy/order.service.ts,告诉我:
1. 这个文件总共做了哪些事情(功能清单)
2. 主要的数据流是什么(数据从哪来,怎么处理,返回什么)
3. 有哪些外部依赖(数据库、第三方服务、全局变量等)
4. 有没有明显的副作用(发邮件、写文件、修改全局状态等)

这一步不是为了让 AI 去改,而是为了建立"共同理解"。如果 AI 对代码的描述和你的预期不符,说明代码本身写得就很模糊,需要先搞清楚它实际在做什么。


3. 第二步:识别坏味道

plain
继续分析 @src/legacy/order.service.ts,识别以下代码坏味道:

1. 过长函数(超过 30 行的函数)
2. 过大的类(承担了超过一个职责的 Service)
3. 重复代码(相似逻辑出现了多次)
4. 魔法数字/魔法字符串(硬编码的数字、状态值)
5. 深层嵌套(超过 3 层的 if/for 嵌套)
6. 注释掉的死代码
7. 不恰当的命名(变量名 a、b、temp 等)
8. 没有错误处理的异步操作

按照严重程度排序,优先列出影响最大的问题。

典型的遗留代码示例

typescript
// ❌ 重构前:典型的遗留代码
export class OrderService {
    constructor(
        private db: any,
        private mailer: any
    ) {}

    async processOrder(data: any) {
        // 校验
        if (!data.userId) throw new Error('no user')
        if (!data.items || data.items.length === 0) throw new Error('no items')

        let total = 0
        for (let i = 0; i < data.items.length; i++) {
            const item = await this.db.query(
                `SELECT * FROM products WHERE id = ${data.items[i].productId}`
            )
            if (!item) throw new Error('product not found')
            if (item.stock < data.items[i].quantity) throw new Error('no stock')
            total += item.price * data.items[i].quantity
        }

        // 打折
        if (total > 1000) {
            total = total * 0.9
        } else if (total > 500) {
            total = total * 0.95
        }

        // 创建订单
        const orderId = Math.random().toString(36).substr(2, 9)
        await this.db.query(
            `INSERT INTO orders (id, user_id, total, status) VALUES ('${orderId}', '${data.userId}', ${total}, 1)`
        )

        for (let i = 0; i < data.items.length; i++) {
            await this.db.query(
                `INSERT INTO order_items (order_id, product_id, quantity) VALUES ('${orderId}', '${data.items[i].productId}', ${data.items[i].quantity})`
            )
            await this.db.query(
                `UPDATE products SET stock = stock - ${data.items[i].quantity} WHERE id = '${data.items[i].productId}'`
            )
        }

        // 发邮件
        const user = await this.db.query(
            `SELECT * FROM users WHERE id = '${data.userId}'`
        )
        await this.mailer.send(
            user.email,
            'Order Confirmed',
            `Your order ${orderId} total: ${total}`
        )

        return { orderId, total }
    }
}

这段代码有什么问题:

问题类型 具体表现
SQL 注入 直接拼接用户输入到 SQL 字符串
N+1 查询 循环里每次都查数据库
无事务保护 插入订单成功但扣库存失败,数据不一致
魔法数字 status = 1
是什么状态?折扣 0.9/0.95 是什么规则?
职责混乱 一个方法干了:校验、计价、创建订单、扣库存、发邮件
随机 ID Math.random()
生成的 ID,可能重复
类型不安全 参数是 any
,返回也不声明类型

4. 第三步:制定重构计划

重构不是一次性的,要分步走。让 AI 帮你制定计划:

plain
基于上面识别的问题,帮我制定一个安全的重构计划。

原则:
1. 每一步都保持代码可运行
2. 优先处理安全风险(SQL 注入)
3. 其次处理数据一致性风险(无事务)
4. 最后做代码结构优化

给出具体的步骤顺序,每步的目标是什么,改完后如何验证没有引入新问题。

典型的重构计划:

plain
Step 1: 消除 SQL 注入(用参数化查询或 ORM 替换拼接 SQL)
        验证:接口功能不变,无 SQL 注入漏洞

Step 2: 引入事务(用数据库事务包裹插入订单+扣库存操作)
        验证:任一步骤失败时整体回滚,不产生脏数据

Step 3: 消灭魔法数字(提取常量和枚举)
        验证:代码逻辑不变,status/折扣规则有明确命名

Step 4: 拆分函数(每个函数只做一件事)
        验证:单元测试覆盖每个子函数

Step 5: 加类型声明(消灭 any)
        验证:TypeScript 编译通过,无类型错误

5. 第四步:AI 辅助执行重构

重构示例:拆分过长函数

plain
@src/legacy/order.service.ts 的 processOrder 方法承担了太多职责。

帮我按以下方式拆分,不改变业务逻辑,只做结构重组:

1. validateOrderInput(data):校验输入参数
2. calculateOrderItems(items):查询商品信息,返回商品详情+价格小计
   (同时返回 insufficientStock 的商品列表)
3. calculateDiscount(subtotal):根据总价计算折扣,返回最终金额
4. createOrderWithItems(userId, items, total):在事务中创建订单+扣库存
5. sendOrderConfirmation(userId, orderId, total):发送确认邮件(异步)
6. processOrder(data):调用上面各步骤,组织完整流程

每个方法要有明确的参数类型和返回类型,不能用 any。

重构后的代码

typescript
// ✅ 重构后:清晰的 NestJS Service
import {
    Injectable,
    BadRequestException,
    ConflictException,
} from '@nestjs/common'
import { PrismaService } from '../../prisma/prisma.service'
import { MailService } from '../mail/mail.service'
import { CreateOrderDto, OrderItemDto } from './dto/create-order.dto'
import { BusinessException } from '../../common/exceptions/business.exception'
import { ErrorCode } from '../../common/constants/error-code'

// 折扣规则常量
const DISCOUNT_RULES = [
    { threshold: 1000, rate: 0.9 },
    { threshold: 500, rate: 0.95 },
] as const

// 订单状态枚举
enum OrderStatus {
    PENDING = 'PENDING',
    PAID = 'PAID',
    CANCELLED = 'CANCELLED',
}

interface OrderItemDetail {
    productId: string
    productName: string
    price: number
    quantity: number
    subtotal: number
}

@Injectable()
export class OrderService {
    constructor(
        private readonly prisma: PrismaService,
        private readonly mailService: MailService
    ) {}

    async createOrder(
        dto: CreateOrderDto
    ): Promise<{ orderId: string; total: number }> {
        const itemDetails = await this.resolveOrderItems(dto.items)
        const subtotal = itemDetails.reduce(
            (sum, item) => sum + item.subtotal,
            0
        )
        const total = this.applyDiscount(subtotal)
        const orderId = await this.persistOrder(dto.userId, itemDetails, total)

        // 异步发送邮件,不阻塞响应
        this.mailService
            .sendOrderConfirmation(dto.userId, orderId, total)
            .catch((err) => {
                // 邮件失败不影响订单创建,只记录日志
                this.logger.error(
                    { orderId, error: err.message },
                    'Failed to send order confirmation'
                )
            })

        return { orderId, total }
    }

    private async resolveOrderItems(
        items: OrderItemDto[]
    ): Promise<OrderItemDetail[]> {
        const productIds = items.map((item) => item.productId)

        const products = await this.prisma.product.findMany({
            where: { id: { in: productIds }, isDeleted: false },
        })

        const productMap = new Map(products.map((p) => [p.id, p]))

        return items.map((item) => {
            const product = productMap.get(item.productId)
            if (!product) {
                throw new BusinessException(ErrorCode.PRODUCT_NOT_FOUND)
            }
            if (product.stock < item.quantity) {
                throw new BusinessException(ErrorCode.INSUFFICIENT_STOCK)
            }
            return {
                productId: item.productId,
                productName: product.name,
                price: product.price,
                quantity: item.quantity,
                subtotal: product.price * item.quantity,
            }
        })
    }

    private applyDiscount(subtotal: number): number {
        const rule = DISCOUNT_RULES.find((r) => subtotal >= r.threshold)
        return rule ? subtotal * rule.rate : subtotal
    }

    private async persistOrder(
        userId: string,
        items: OrderItemDetail[],
        total: number
    ): Promise<string> {
        return this.prisma.$transaction(async (tx) => {
            const order = await tx.order.create({
                data: {
                    userId,
                    total,
                    status: OrderStatus.PENDING,
                    items: {
                        create: items.map((item) => ({
                            productId: item.productId,
                            quantity: item.quantity,
                            price: item.price,
                        })),
                    },
                },
            })

            // 批量扣减库存
            await Promise.all(
                items.map((item) =>
                    tx.product.update({
                        where: { id: item.productId },
                        data: { stock: { decrement: item.quantity } },
                    })
                )
            )

            return order.id
        })
    }
}

6. 重构前后对比

维度 重构前 重构后
函数长度 50+ 行,一个函数 5 个方法,每个 10-20 行
SQL 注入 存在(字符串拼接) 消除(Prisma 参数化)
数据一致性 无事务保护 事务包裹订单+库存操作
N+1 查询 循环查数据库 一次批量查询
类型安全 any
参数
完整 DTO + 返回类型
可测试性 极难 Mock 每个私有方法可独立测试
可读性 需要逐行读懂 函数名即文档

7. 安全演进:保障回归不出问题

重构遗留代码最大的风险是:改着改着,原来能跑的功能不能跑了。

防止回归的策略

策略一:先加测试,再重构
plain
在重构 processOrder 之前,先帮我为现有的代码写集成测试,
覆盖正常下单流程,用内存数据库模拟,这样重构后可以跑测试验证行为没变。
策略二:小步提交

每完成一个重构步骤就提交一次:

bash
git commit -m "refactor: extract resolveOrderItems from processOrder"
git commit -m "refactor: wrap order creation in transaction"
git commit -m "refactor: replace magic numbers with constants"

这样如果某步出了问题,可以精准回滚,不影响其他改动。

策略三:让 AI 检查重构前后的语义等价性
plain
对比 @src/legacy/order.service.ts(重构前)和
@src/modules/order/order.service.ts(重构后),

确认以下方面的语义是否完全一致:
1. 输入校验的条件
2. 折扣计算规则
3. 数据库操作的内容(哪些表、哪些字段)
4. 邮件发送的时机和参数

找出任何可能的行为差异。

8. 小结

AI 辅助重构遗留代码的正确节奏:

plain
理解代码 → 识别坏味道 → 制定计划 → 小步执行 → 测试验证

不要跳步。特别是"理解代码"这一步,很多开发者会跳过,直接让 AI 重构,结果 AI 改了但逻辑不对,因为连 AI 都没弄清楚原来的代码在干什么。

重构的目的不是让代码"看起来更好",而是让它更容易被修改、更容易被理解、更难产生 bug。AI 是这个过程的加速器,但方向还是你来把握。