1. 接手一个没有文档的老系统,到底难在哪
很多做企业级开发的朋友都遇到过这种局面:领导拍着你的肩膀说,这套系统跑了五六年了,现在要迁移到新架构,你先把需求文档整理出来。你打开代码仓库一看,别说需求文档了,连像样的注释都没几行,README 还是三年前某位已经离职的同事随手写的两句话。数据库里几十张表,字段命名风格从拼音缩写到英文全称混着来,前端页面点进去功能倒是能跑,但没人说得清每个按钮背后的业务规则。
这就是典型的“代码即文档”状态。系统还活着,业务还在跑,但知识已经随着人员流动散掉了。你要做的不是重新发明需求,而是把已经存在于代码里的隐性知识,逆向提取成一份能指导迁移的 PRD。这件事的核心逻辑其实很朴素:代码是系统行为的唯一真相来源,数据库结构、接口定义、前端交互、定时任务、消息消费,这些加起来就是一份比任何文档都精确的需求说明书,只不过它用的是机器语言。
我接手过一套基于 Spring Boot 加 Vue 的多商户商城系统,前后端分离,后端大概四十多个 Controller,前端三十多个路由页面,数据库 MySQL 里一百多张表。原计划两周出迁移 PRD,实际花了将近一个月。踩过的坑、总结的方法,下面一点点拆开讲。
这篇文章适合三类人看:一是正在做老系统迁移、需要补文档的后端或全栈工程师;二是技术负责人,想了解逆向梳理的可行路径和工期预估;三是对 Spring Boot 和 Vue 技术栈有一定了解、想学习如何从代码反推业务的学习者。不需要你精通逆向工程,但需要你能读懂基本的 Java 和 JavaScript 代码。
2. 逆向迁移 PRD 的整体思路与工具选型
2.1 为什么不能直接照着代码翻译成文档
最容易想到的做法是打开每个 Controller,把接口路径和参数抄下来,整理成表格。我一开始也这么干,做了三天就发现行不通。原因有三个:第一,接口数量太多,纯手工抄写效率极低且容易遗漏;第二,接口只是入口,真正的业务规则藏在 Service 层的条件判断里,比如“订单金额大于 500 且用户等级为 VIP 时才免运费”,这种规则不读透代码根本提取不出来;第三,前端页面上的交互逻辑,比如某个字段在什么条件下才显示、某个按钮点击后触发几个接口调用,这些信息分散在 Vue 组件里,光看后端接口是拼不完整的。
所以正确的思路不是“翻译代码”,而是“重建业务模型”。具体来说,分四层来拆:数据层看表结构和字段约束,服务层看业务规则和状态流转,接口层看对外契约和调用关系,交互层看用户操作路径和页面流转。这四层信息交叉验证,才能拼出一份完整的需求画像。
2.2 工具链的选择与理由
工欲善其事,必先利其器。我实际用到的工具组合如下:
| 工具 | 用途 | 选择理由 |
|---|---|---|
| IntelliJ IDEA 社区版 | 阅读 Spring Boot 源码 | 免费,对 Java 生态支持好,全局搜索和调用链追踪够用 |
| MySQL Workbench | 导出表结构、查看索引和外键 | 可视化直观,能直接导出 DDL |
| Swagger 或 SpringDoc | 自动提取接口文档 | 如果项目集成了就直接用,没集成可以临时加依赖 |
| Vue DevTools | 查看组件树和路由结构 | 浏览器插件,能快速理清前端页面层级 |
| PlantUML | 画状态流转图和时序图 | 文本化绘图,方便版本管理和修改 |
| 数据库逆向工具 | 生成 ER 图 | 比如 MySQL Workbench 自带的 Reverse Engineer |
这里重点说一下 IntelliJ IDEA 社区版。很多人觉得社区版功能不够,但对于读代码这件事,它的全局搜索(Ctrl+Shift+F)、查找用法(Alt+F7)、调用层级(Ctrl+Alt+H)完全够用。我通常的做法是先用全局搜索找到所有 Controller 类,然后逐个追踪每个接口的调用链,一直追到 Mapper 层的 SQL 语句。这个过程虽然笨,但最可靠。
提示:如果项目用了 MyBatis,建议把 Mapper XML 文件全部打开,因为很多业务查询逻辑直接写在 SQL 里,光看 Java 代码会漏掉关键过滤条件。
2.3 逆向 PRD 的文档结构设计
一份能指导迁移的 PRD,和写给新产品用的 PRD 不一样。新产品 PRD 侧重“要做什么”,迁移 PRD 侧重“现在是什么”和“迁移后必须保持什么”。我的文档结构是这样的:
第一部分是系统概览,包括技术栈、部署架构、模块划分。第二部分是数据模型,包括核心表结构、字段含义、表间关系。第三部分是功能清单,按模块列出所有功能点,每个功能点标注对应的接口、页面和业务规则。第四部分是状态流转,把订单、支付、退款等核心业务的状态机画出来。第五部分是迁移风险点,标注哪些逻辑是隐性的、容易在迁移中丢失的。
这个结构的好处是,开发人员拿到后可以直接对照着写新代码,测试人员可以对照着设计测试用例,产品经理可以对照着确认业务完整性。
3. 从数据库和代码中提取核心业务规则
3.1 数据库表结构逆向:从字段名读出业务含义
数据库是系统的骨架。我拿到一套陌生系统,第一件事就是导出所有表的 DDL,然后逐张表分析。以那套多商户商城为例,一百多张表里,核心表大概二十张,包括用户表、商户表、商品表、订单表、订单明细表、支付记录表、退款表、物流表、优惠券表、评价表等。
看表结构的时候,重点关注几类信息。第一类是字段命名,比如is_deleted、status、type这种字段,往往对应着业务上的软删除、状态机和分类逻辑。第二类是索引,唯一索引通常意味着业务上的唯一性约束,比如order_no唯一索引说明订单号不能重复。第三类是外键关系,虽然很多互联网项目不建物理外键,但通过字段名也能推断出关联关系,比如order_id关联订单表。
我遇到过一个坑:订单表里有个source字段,类型是 tinyint,注释写的是“来源”。光看这个根本不知道有哪些取值、分别代表什么。后来在前端代码里找到了一个下拉框的选项列表,才确认 1 代表 App、2 代表小程序、3 代表 H5。这种信息如果不从前端反查,光看数据库是猜不出来的。
3.2 Service 层业务规则提取:条件判断里的隐藏需求
Service 层是业务规则最密集的地方。我通常的做法是,从 Controller 入口开始,顺着调用链往下读,把每个 if-else、switch-case 都记下来,因为这些条件判断就是业务规则。
举个例子,那套商城的订单创建逻辑里,有一段代码是这样的:先检查用户是否被禁用,再检查商品是否下架,然后检查库存是否充足,接着计算优惠券抵扣,最后判断是否需要拆单。这一连串判断,在代码里可能分散在五六个方法里,但从前端用户视角看,就是“点击下单按钮后发生的事”。逆向 PRD 要做的,就是把这些分散的逻辑重新组织成一条完整的业务规则描述。
我习惯用一张表格来记录这类规则:
| 规则编号 | 触发条件 | 业务动作 | 代码位置 | 备注 |
|---|---|---|---|---|
| R001 | 用户状态为禁用 | 拒绝下单 | UserService.checkStatus | 返回错误码 1001 |
| R002 | 商品状态为下架 | 拒绝下单 | ProductService.checkOnSale | 返回错误码 2001 |
| R003 | 库存不足 | 拒绝下单 | StockService.checkStock | 返回错误码 3001 |
| R004 | 优惠券已过期 | 忽略优惠券 | CouponService.calc | 不报错,直接按原价 |
这种表格在迁移时特别有用,因为新系统开发时可以逐条对照实现,测试时也可以逐条验证。
3.3 接口契约梳理:给第三方用的接口要单独标记
那套系统有一部分接口是给外部合作伙伴调用的,比如商户系统对接、物流回调等。这些接口和内部接口混在一起,如果不仔细区分,迁移时很容易漏掉或者改错。
我的做法是在梳理接口时,给每个接口打上标签:内部接口、外部接口、回调接口。外部接口通常有签名验证、IP 白名单、频率限制等额外逻辑,这些在迁移时必须保留。回调接口则要特别注意幂等性处理,因为外部系统可能会重复调用。
注意:有些接口虽然路径看起来是内部的,但实际上被前端跨域调用了,这种也要标记出来。判断方法是看 Controller 上有没有
@CrossOrigin注解,或者看前端代码里的请求地址。
4. 前端交互与状态流转的还原方法
4.1 Vue 路由与页面结构还原
前端是用户直接接触的部分,也是需求最直观的体现。那套系统用的是 Vue 2 加 Element UI,路由配置在router/index.js里。我先把路由表导出来,整理成页面清单,然后逐个页面去看组件代码。
看 Vue 组件的时候,重点关注几个东西:data里定义了哪些字段(对应页面上的表单项和展示项)、methods里有哪些方法(对应页面上的操作)、watch里监听了什么(对应字段联动逻辑)、computed里计算了什么(对应派生数据)。把这些串起来,就是一个页面的完整功能描述。
比如商品编辑页面,data里有商品名称、价格、库存、分类、图片列表等字段,methods里有保存、上架、下架、删除等操作,watch里监听了分类变化去加载对应的属性模板。这些信息组合起来,就能写出“商品编辑页面支持修改商品基本信息、控制上下架状态、根据分类动态展示扩展属性”这样的需求描述。
4.2 状态流转图:订单从创建到完成的全链路
状态流转是迁移中最容易出问题的部分。因为状态机往往分散在多个 Service 方法里,而且可能还有定时任务在后台修改状态。那套商城的订单状态有:待支付、已支付、待发货、已发货、已完成、已取消、退款中、已退款。这些状态之间的流转条件,我花了整整两天才理清楚。
理清之后,我用 PlantUML 画了一张状态图,标注了每个流转的触发条件和对应的代码位置。这张图后来成了迁移 PRD 里最有价值的部分,因为新系统开发时,订单模块的负责人直接照着这张图实现,省了大量沟通成本。
除了订单,还有退款单、优惠券、商户入驻申请等也有状态流转。我的建议是,凡是带status字段的表,都要画一张状态图,不要嫌麻烦。
4.3 定时任务与异步逻辑的排查
这是最容易被遗漏的部分。老系统里往往有一些定时任务,比如每天凌晨自动关闭超时未支付的订单、自动确认收货、自动结算商户货款等。这些逻辑不在用户操作路径上,光看接口和页面是发现不了的。
排查方法是在代码里全局搜索@Scheduled注解,或者搜索quartz、xxl-job等定时任务框架的关键字。找到之后,逐个分析任务的执行逻辑、执行频率、依赖条件。那套系统里有七个定时任务,其中三个和订单相关,两个和结算相关,还有两个是数据同步任务。这些在迁移 PRD 里都必须明确列出,否则新系统上线后会出现订单状态不流转、货款不结算等严重问题。
异步逻辑也是类似,搜索@Async、MQ、消息队列等关键字,把异步处理的业务场景找出来。比如订单创建后发送通知、支付成功后更新库存等,这些在迁移时都要考虑。
5. 实操过程:从零到一份完整 PRD 的完整记录
5.1 第一阶段:环境搭建与代码通读
我拿到代码后的第一周,主要做三件事。第一,把项目跑起来。那套系统依赖 MySQL、Redis、RabbitMQ,我用了半天时间配好本地环境,确保能正常启动和访问。第二,通读项目结构,把 Maven 模块划分、包结构、配置文件都看一遍,心里有个大概的模块地图。第三,导出数据库 DDL 和前端路由表,作为后续梳理的基础材料。
这个阶段不要急着写文档,先把整体感觉建立起来。我通常会画一张粗略的模块关系图,标注哪些模块之间有调用关系,哪些模块是独立的。
5.2 第二阶段:逐模块深挖与规则记录
第二周到第三周,我按模块逐个深挖。顺序是先核心后边缘:用户模块、商品模块、订单模块、支付模块、商户模块、营销模块、物流模块。每个模块的梳理流程是固定的:先看数据库表,再看 Controller 接口,然后追 Service 逻辑,最后对照前端页面验证。
每梳理完一个模块,我就写一份模块级的逆向文档,包括功能清单、业务规则表、状态流转图、接口列表。这些模块文档最后汇总成完整的迁移 PRD。
这个阶段最耗时间的是订单模块,因为涉及拆单、优惠分摊、退款计算等复杂逻辑。我光读订单相关的代码就花了三天,画了四张状态图,记录了三十多条业务规则。
5.3 第三阶段:交叉验证与遗漏排查
第四周主要是查漏补缺。我把前端所有页面都点了一遍,对照后端接口列表,看有没有页面调用了但接口列表里没有的接口,或者接口列表里有但前端没用的接口。前者说明遗漏了功能,后者可能是废弃接口或者给外部用的接口。
同时,我把定时任务、消息消费、缓存更新这些隐性逻辑再排查一遍,确保没有遗漏。最后,我找了一位还在职的老员工,花了两个小时对着文档过了一遍,他补充了几个我漏掉的业务规则,比如某个优惠券在特定节日会自动翻倍,这个逻辑藏在配置表里,光看代码不容易发现。
5.4 第四阶段:文档整理与评审
最后一周把前面所有材料整理成正式 PRD,按之前设计的五部分结构组织。整理过程中,我把所有业务规则都编了号,方便后续追溯。文档完成后,组织了一次评审会,邀请开发、测试、产品三方参加,逐模块确认。评审会上又发现了几处理解偏差,修正后定稿。
整个流程下来,实际耗时四周多一点,产出了一份约八十页的迁移 PRD,包含一百多张表结构说明、两百多个接口定义、四十多条业务规则、八张状态流转图。
6. 常见问题与避坑经验实录
6.1 代码里的“僵尸逻辑”怎么识别
老系统里经常有一些代码,看起来是业务逻辑,但实际上已经废弃了。比如某个接口还在,但前端早就不调用了;某个配置项还在,但实际不会生效。这些“僵尸逻辑”如果写进 PRD,会误导新系统开发。
识别方法有几个:看代码提交记录,如果某个方法最近两年都没改过,而且没有测试覆盖,大概率是废弃的;看前端调用,如果全局搜索接口路径在前端代码里找不到,可能是废弃的;看数据库数据,如果某个状态值在数据表里从来没有出现过,对应的逻辑可能是废弃的。
我遇到过一个例子:订单表里有个is_urgent字段,代码里有加急处理的逻辑,但查了数据库发现这个字段全是 0,前端也没有设置入口,确认是废弃功能,没有写入 PRD。
6.2 隐式业务规则:配置表里的秘密
有些业务规则不在代码里,而在配置表里。比如那套系统有一张sys_config表,里面存了各种参数:订单超时时间、优惠券最大抵扣比例、商户结算周期等。这些参数在代码里是读取配置表的值,而不是硬编码,所以光看代码只能看到“读取配置”,看不到具体数值。
迁移时,这些配置值必须一并迁移,否则新系统行为会和老系统不一致。我的做法是把配置表里所有和业务相关的配置项都列出来,标注含义、当前值、影响范围。
6.3 迁移 PRD 的验收标准
一份迁移 PRD 写得好不好,有一个简单的验收标准:拿给一个没接触过老系统的开发,他能不能照着这份文档把新系统开发出来,而且行为和老系统一致。如果能,说明文档合格;如果不能,说明还有遗漏。
我通常会在文档完成后,找一个同事做“盲测”,让他只看文档不看代码,描述某个功能应该怎么实现,然后对照代码看是否一致。这个方法很有效,能发现很多自己没意识到的表述不清或逻辑缺失。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决思路 |
|---|---|---|---|
| 接口列表和前端调用对不上 | 存在废弃接口或外部接口 | 全局搜索接口路径 | 标记接口类型,区分处理 |
| 状态流转理不清 | 状态修改分散在多个地方 | 搜索所有修改 status 的代码 | 画状态图,标注触发条件 |
| 业务规则遗漏 | 规则藏在配置表或定时任务里 | 检查配置表和 @Scheduled | 补充配置项清单和任务清单 |
| 字段含义不明 | 命名不规范或注释缺失 | 查前端选项列表或问老员工 | 结合前后端推断,标注确认状态 |
| 迁移后行为不一致 | 隐式规则未迁移 | 对比新旧系统测试结果 | 补充隐式规则到 PRD |
提示:逆向 PRD 不是一次性的工作,建议在迁移开发过程中持续维护,发现遗漏随时补充。我通常会在文档里留一个“待确认”章节,把不确定的点记下来,后续逐个确认。
7. 迁移 PRD 的后续维护与扩展思路
文档写完不是终点。新系统开发过程中,开发人员会不断提出疑问,这些疑问往往指向文档的薄弱环节。我的做法是,每次答疑后都把答案补充到文档里,让文档保持“活”的状态。
另外,迁移完成后,这份逆向 PRD 还有别的用途。比如新同事入职时,可以用它快速了解老系统的业务逻辑;比如做系统重构时,可以用它评估影响范围;比如做数据迁移时,可以用它核对字段映射关系。
如果后续还要做国产化迁移,比如从 MySQL 换到国产数据库、从 Spring Boot 2.x 升级到 3.x,这份 PRD 同样是基础材料。因为国产化迁移最大的风险不是技术本身,而是业务逻辑在迁移过程中丢失或变形。有了这份文档,至少业务层面是有保障的。
我个人在实际操作中的体会是,逆向梳理这件事,技术难度不高,但耐心和细致程度要求很高。不要指望有什么工具能一键生成 PRD,工具只能帮你提取接口和表结构,真正的业务规则还是要靠人一行行读代码、一个个点页面、一条条问老员工。这个过程很枯燥,但做完之后,你对系统的理解会超过任何一个还在职的人。