接手一个运行了七八年、连一份需求文档都找不到的老系统时,心态很容易崩。老板只丢下一句话:"下个季度要迁移到新平台,你先出个方案。"然后你打开代码仓库,发现注释比代码还少,数据库字段名是拼音缩写,接口文档早已随着前任架构师的离职一起消失在历史里。那时候你会明白一个道理:代码会说真话,只看你愿不愿意听。
我今天想聊的,就是如何在没有需求文档的情况下,从老系统的代码里逆向出一份能真正指导迁移的PRD(产品需求文档)。这套方法我踩过不少坑才整理出来,适用于企业内部老系统重构、跨平台迁移、技术栈升级等场景。只要你的系统还在跑、代码还能编译,这份"逆向PRD"就能做出来,而且比很多人凭记忆写的需求文档可靠得多。
1. 迁移前夜:为什么没有PRD我也敢开工
很多团队接到老系统迁移任务时,第一反应是找产品经理要PRD,第二反应是找以前的负责人问需求,两件事大概率都会碰壁。前者是因为PRD从来没存在过,后者是因为能说清楚需求的人早就离职了。就算问到一两个还在的老人,对系统理解也往往停留在"大概是个管理后台"的层面,真到字段级、状态机级、异常分支级的细节,谁也说不准。
1.1 迁移为什么必须有一份PRD
有人会说,迁移就是换个技术栈把代码重写一遍,要PRD干什么?这个想法是最大的坑。系统迁移的本质不是搬代码,而是把原来的一整套业务能力在另一个环境里完整复刻出来,包括用户看不到的边界条件、历史数据规则、报表口径、定时任务行为。没有PRD,你连"什么功能不能丢"都列不全,更别说验收了。
我见过太多团队栽在这个坎上:迁移后上线,表面功能全通了,结果每个月末对账发现金额对不上,原因是老系统里有一处只在特定日期执行的利息计算逻辑没人发现;或者年终报表取数口径不对,因为老系统里有个隐藏的"数据修正脚本"在每天凌晨跑。这些都不是靠产品经理回忆能发现的,只有代码里才有真相。
1.2 代码是唯一没撒谎的需求来源
人在描述需求时会美化、简化、遗漏,代码不会。老系统里每一行运行中的逻辑,不管它写得有多烂,都代表着一个真实发生过、被业务依赖过的行为。哪怕是那些看起来像历史遗留的"死代码",也可能在某个特定条件触发时被调用,迁移时漏了就会出事故。
所以,逆向PRD的核心思路非常简单:把代码当成需求文档来读,把每一个运行时的行为翻译成需求条目。这就好比考古学家面对一座没有文字记录的古城遗址,不会因为没找到碑文就说"这城没什么可研究的",而是从城墙布局、陶片纹样、灰烬层去还原当时的生活。代码就是老系统的"陶片与灰烬"。
2. 四步拆出系统的"骨架":从代码定位需求边界
正式开始做逆向PRD之前,先别急着逐行读代码,那会在细节里淹死。先把系统的骨架拆出来,画一张地图,然后再逐层填充细节。这个过程我一般分四步走:依赖分析、模块梳理、接口盘点、数据解构。每一步都有明确的目的和产出物。
2.1 先画地图:用依赖分析找出系统边界
打开一个老项目工程,你第一眼看到的是几十甚至上百个包、上千个类,如果直接开始读,很容易陷入"每个类都重要因此每个类都读"的泥潭。正确做法是先跑一遍静态依赖分析,把系统的边界画出来。Java项目用JDepend或ArchUnit,Python项目可以用pydeps,Node项目可以用dependency-cruiser,先把包与包、模块与模块之间的调用关系可视化。
这一步要回答的核心问题是:这个系统对外有哪些入口?它依赖了哪些外部系统?系统内部哪些模块是独立的,哪些是纠缠在一起的?边界画清楚之后,迁移的拆解边界也就自然浮现了。比如一个单体系统里明显有订单模块、用户模块、对账模块三条相对独立的依赖链,迁移PRD就可以分成三个子域去写,而不是一锅炖。
我在实际做的时候,习惯把依赖图导出成PDF,然后在旁边用不同颜色的笔标出"核心链路""边缘模块""疑似死代码区域"。做了这一步,后面跟业务方核对需求时,你拿着图说话会非常有说服力,因为图不会撒谎,任何人问"你说这个模块是核心,依据是什么",你可以直接指给他看调用链上有多少个上游在依赖它。
2.2 模块层梳理:从目录结构还原业务模块
依赖分析解决的是"系统长什么样"的问题,接下来要把代码目录映射成业务模块。老系统的目录结构往往已经透露出当初的领域划分,比如controller层、service层、dao层这种经典分层,或者按业务域划分的模块化结构。
这一步我常用的方法是"包名语义映射法",把代码包的命名翻译成业务语言。比如一个电商老系统里有个包叫com.oldcompany.trade,里面又有order、payment、refund三个子包,那基本可以判断这是交易域的订单、支付、退款三大模块。遇到命名很混乱的情况,比如包名叫util2、common3这种,就先看包里装的类都在处理什么业务概念,再给它人工打上业务标签。
梳理完之后,输出一张模块清单表,字段包括:模块名、代码路径、业务描述、依赖的上游模块、被哪些模块依赖、估计的代码行数、预估的迁移复杂度。这张表就是PRD里"功能范围"章节的原料。这里有个建议:迁移复杂度不要太纠结于代码量的精确统计,粗略分成高、中、低三档就行,因为迁移成本的决定性因素是业务复杂度和数据复杂度,不是代码行数。
2.3 接口层盘点:从Controller和事件监听器里捞需求
有了模块清单,下一层就是接口盘点。接口是系统的对外承诺,也是PRD里"功能需求"最直接的来源。我的做法是直接用工具扫描所有对外暴露的入口,Java看Controller层的RequestMapping注解,Python看Flask/Django的路由定义,Node看Express/Koa的路由注册,把它们全部列出来。
对于每个接口,至少要记录四项信息:HTTP方法、路径、入参结构、出参结构。能自动生成的接口文档工具(比如Swagger、Apipost)直接导出,但老系统多数没有这些注解,那就通过代码生成。这里有个经验:老系统的接口往往不止REST接口,还包括消息队列的消费者、定时任务、文件批处理入口、甚至数据库触发器里藏的逻辑。如果你只盯着HTTP接口,迁移时一定会漏东西。
所以我的接口盘点清单分四类:同步接口(HTTP/RPC)、异步消费者(MQ监听器)、定时任务(Spring Schedule/Cron/Quartz)、批处理入口(文件导入导出、Shell脚本调用)。每一类都要有明确的清单和触发条件。这就是PRD中"功能清单"的底稿,也是后续写用户故事的素材库。
2.4 数据层解构:从Schema倒推业务实体
UI和代码逻辑都可以重写,数据一旦丢了就真没了。所以数据层的梳理是逆向PRD里最不能马虎的环节。打开数据库,把所有表和字段过一遍,这项工作的产出包括三张清单:物理表清单、字段语义表、数据流图。
先看物理表清单,按业务域分组归类,比如用户域的表放一起,订单域的表放一起。这里有个技巧:可以通过表名前缀判断当初的领域划分,比如user_开头的表基本都是用户域的。再看字段语义表,这是最耗时的一步,因为老系统的字段名往往会让你怀疑人生,像a01、b02这种完全拼音首字母或者无意义命名的字段比比皆是。
遇到这种情况,我的办法是"三路并查":第一路查代码里对这个字段的所有读写位置,看它存了什么值、出现在什么业务场景;第二路查数据库里的注释——有些老库虽然名字没注释,但字段上挂的COMMENT还是能救命的;第三路查历史数据,直接SELECT几条真实的业务数据看看值长什么样,很多字段一看值就能猜出含义。这个办法确实笨,但足够可靠,我靠它啃下过不少"天书"级别的老库。
3. 需求细节的"翻译术":把代码语义还原成PRD条目
骨架搭好之后,接下来是最见功夫的部分:把代码里的具体业务逻辑翻译成PRD里的需求条目。这一步没有工具能全自动完成,需要你同时具备代码阅读能力和业务抽象能力。好消息是套路是固定的,按状态机、业务规则、非功能需求、用户故事四类去翻译,基本不会漏。
3.1 状态机怎么还原:从枚举和状态流转代码里提炼
老系统里最值钱的需求逻辑往往藏在状态机里。一个订单有哪些状态、什么条件下从待支付变成已支付、什么条件下变成已取消、哪些状态可以回退,这些逻辑散落在代码的各个角落:枚举类定义、状态流转的代码分支、数据库的状态字段。
我的做法是"状态机还原三步法"。第一步,把枚举类或常量类里所有状态定义找出来,列成一张状态清单。第二步,全局搜索所有对状态字段赋值的地方,把每一次赋值的条件上下文记录下来。第三步,把收集到的信息画成状态流转表——每一行是一次合法的状态迁移,包括当前状态、目标状态、触发条件、执行动作。这张表直接就是PRD里"核心业务流程"章节的一手素材。
实际项目中会遇到状态流转特别复杂的场景,比如一笔退款既有"退款中"又关联着"订单售后"和"财务审核"两个维度的状态,联动的条件散落在多个service方法里。这时候我会把每个方法的调用链打出来,标注清楚是哪条业务入口触发了这个状态迁移。PRD里可以用"状态机+前置条件+后置动作"的方式写清楚,这个表述方式对开发实现和测试验收都很有用。
3.2 业务规则怎么提炼:从校验逻辑和条件分支里挖
状态机解决的是"流程怎么走"的问题,业务规则解决的是"什么东西能走、什么不能走"的问题,也就是各种if-else、断言、校验、异常抛出。一个老系统里最隐藏需求的地方就是校验逻辑,这些逻辑恰恰是业务方最容易凭记忆出错的地方。
翻译业务规则时,我会按照三层结构去整理。第一层是入参校验规则,比如"金额必须大于0"、"手机号必须符合11位格式"、"会员等级不能为黑名单",这些在Controller入口或者Service方法开头通常都有。第二层是业务前提校验,比如"订单已支付才能申请退款"、"库存不足不允许下单",这些逻辑藏在更深层的业务判断里。第三层是异常分支处理,比如支付超时怎么办、调用外部接口失败重试几次、数据不满足条件时是报错还是静默跳过。
以我的经验来看,最容易在迁移时丢失的业务规则是"静默跳过"类型。代码里如果出现一个catch块里只写了日志没有抛异常,或者某个条件是if(order == null) { return; }直接返回,意味着老系统面对异常场景选择了"忽略"。这个行为有没有产品设计依据说不准,但业务方很可能已经依赖了它,比如"查不到数据就返回空数组"和"查不到数据就报错"在用户感知上完全不同。在PRD里,每一条这种规则都要明确写出来,并标注"原系统行为如此,是否保留需与业务确认"。
3.3 非功能需求怎么估算:从实现反推开销和瓶颈
PRD不能只写功能,非功能需求同样要在代码里找证据。老系统的性能瓶颈、可用性短板、依赖的外部服务,迁移时如果不重建或优化,原样搬过去迟早会出事。
逆向非功能需求有两个方法。一个是"日志统计法",去生产环境翻日志,统计每个核心接口的调用量、响应时间分布、错误率。另一个是"代码体检法",看代码里的连接池配置、超时时间设置、缓存策略、重试机制。代码里如果你看到setConnectTimeout(3000)和setReadTimeout(5000)这种配置,说明当初定过性能指标,这些参数本身就是需求的一部分。
还要列外部依赖清单:老系统调了哪些第三方服务、哪些内部公共服务、文件存储用的什么、消息队列用的什么。这些在代码里搜索HTTP client的调用地址、MQ的topic名称、配置文件的资源地址就能挖出来。迁移PRD里单独列一章"外部依赖与集成方案",写清楚每一项依赖的用途、调用量级、迁移后的替代方案,这对后续技术方案设计特别重要。
3.4 用户故事怎么写:从调用链反推业务流程
有了状态机、业务规则、非功能信息之后,最后一步是把这些碎片拼成完整的用户故事。PRD里的用户故事不是编出来的,而是从代码调用链中抽出来的完整业务流程。
我的做法是从一个入口接口开始,把它的方法调用链完整打出来,然后按时间顺序记录每一步发生了什么。比如"用户申请退款"这个故事,从Controller接收到退款请求开始,先校验订单状态,再检查退款金额是否符合规则,然后调用支付网关发起退款,同时更新退款单状态,最后写一条审计日志,整条链路就是一个完整的故事。
写用户故事时,我一般按"主流程+分支流程"结构来写。主流程写happy path,分支流程写各种条件和异常。比如退款主流程是"提交退款申请→审核通过→执行退款→退款完成",分支就有"审核驳回""退款失败""部分退款""重复退款请求"等等。每条分支在代码里都能找到对应的处理逻辑,这就保证了PRD的每个字都有据可查。
4. PRD的产出标准与输出物:一份能指导迁移的文件长什么样
你花了两三周时间做代码逆向,最终得拿出一份文档,这份文档要能指导开发团队动手写新代码,也要能指导测试团队写验收用例。所以它不能是代码分析的堆砌,而应该是一份结构完整、条目清晰、可直接执行的需求规格说明书。
4.1 逆向PRD必须包含的六大板块
我整理过很多次迁移项目PRD的模板,最后沉淀下来六个核心板块,积累了多次实战后基本稳定下来了。
第一板块是系统现状概览,写老系统的整体架构、技术栈、模块清单、外部依赖,相当于给不了解背景的读者一个全景图。第二板块是功能需求清单,按照业务模块拆分的所有功能点+详细规则,每条需求都标注来源代码的位置,方便追溯。第三板块是数据需求,包括数据实体清单、字段语义表、数据量估算、历史数据迁移规则。第四板块是非功能需求,性能指标、安全要求、可用性目标、监控需求。第五板块是合规与约束,有没有等保要求、数据合规要求、审计日志要求,老代码里如果有敏感信息处理逻辑,也要单独列出。第六板块是验收标准,包含功能验收用例、数据校验用例、性能验收指标。
这六个板块每个都不能省,我有一次因为嫌麻烦把"数据需求"写得很简略,结果新系统开发到一半才发现有个归档表带着文件存储路径,迁移时忘了把存储的数据一并处理,最后光追这批数据就多花了一周时间。
4.2 逆向PRD的模板与示例片段
直接说一下我常用的PRD模板长什么样,你可以按这个框架去填充自己的内容。以"订单状态管理"这个模块为例:
**需求编号:**MIG-ORD-001
**需求名称:**订单状态流转
来源代码:com.oldcompany.trade.order.service.OrderServiceImpl#updateOrderStatus
**需求描述:**系统支持订单在待支付、已支付、已发货、已完成、已取消五种状态间流转,各状态迁移条件如下表。
| 当前状态 | 目标状态 | 触发条件 | 后置动作 | 校验逻辑 |
|---|---|---|---|---|
| 待支付 | 已支付 | 支付网关回调成功 | 更新支付时间、发送通知 | 订单未被关闭、支付金额与订单金额一致 |
| 待支付 | 已取消 | 用户点击取消或超时30分钟 | 释放库存、记录取消原因 | 订单未被锁定 |
| 已支付 | 已发货 | 后台操作员发货 | 调用物流接口、更新发货时间 | 发货地址非空、库存已出库 |
| ... | ... | ... | ... | ... |
每一个需求条目都包括来源代码定位,这是在评审和后续追溯时特别有用的设计。开发会说"这条需求的依据是什么",你直接贴代码位置给他看;测试写用例时,也可以根据"校验逻辑"列直接生成边界值用例。
4.3 评审与确认:让代码当证人,让业务当裁判
逆向PRD初稿完成后,不要急着让开发照着去做,必须先过评审。评审会邀请两类人:一类是还熟悉系统的人,比如原来写过这个系统的老程序员、运维负责人;另一类是业务方,比如运营、财务这些最终用户。第一类人帮你校验"反向翻译"是否准确,第二类人帮你判断"业务现状"是否需要保留。
评审会的节奏也有讲究。我会把需求条目按模块分批过,每个模块先让老程序员确认功能逻辑没有遗漏,再让业务方确认每个行为是否是真实业务规则。遇到代码里有但业务方认为不存在的功能,以代码存在为准记录下来,标注"疑似废弃功能,建议迁移后隐藏但不删除",而不是直接丢掉。因为老系统的很多功能是低频但关键的,比如季末报表的取数逻辑,一年只用四次,业务操作人员不一定知道有这功能存在,但跑不到它系统会出大乱子。
5. 实操速查:我踩过的坑和可以抄的作业
最后这部分是实操层面的干货。逆向PRD这件事,理论上听起来不难,真正做起来处处是坑。我把常见的坑、和排查技巧整理成速查表,权当给你的一份踩坑指南。
5.1 六个典型坑,建议提前绕开
坑一:只看业务代码不看框架代码。老系统经常用旧版本框架,框架本身的行为也是系统行为的一部分。比如旧版Spring的事务默认回滚规则、旧版ORM的懒加载机制,都可能导致同一套业务代码在不同环境下行为不一致。迁移PRD里要把框架版本差异记录清楚,尤其要关注事务、序列化、时间处理这类容易被忽略的基础能力。
坑二:忽略定时任务的"隐性时间依赖"。我遇到过一个系统,定时任务里写死了new Date()配合Datetime字段做日期计算,跑批逻辑依赖服务器本地时间。新系统如果改用容器化部署,时区设置不一致直接导致跑批数据错位。所以逆向PRD时,所有时间处理逻辑都要单独梳理,包括时区、格式、闰年、时令切换这些边缘情况。
坑三: SQL里的逻辑没纳入需求范围。老系统很多业务规则藏在SQL的where条件、join关系、子查询里,而这些SQL既有MyBatis XML里的,也有数据库存储过程里的。很多代码是在Service层完成一部分逻辑、在SQL里完成另一部分逻辑的,迁移时如果只翻了Java代码没看SQL,需求一定是不完整的。建议把系统里所有的SQL语句导出,按业务域分组人工过一遍,重点看where条件中的业务含义。
坑四:对"历史数据兼容"考虑不足。老库里的数据不一定完全符合现有的代码约束,比如状态字段可能有代码里没定义的值——历史脏数据或曾经上线又下线过的功能留下的数据。PRD里要明确历史数据的清洗规则和兼容策略,不能想当然地假设数据都是合法的,我在实际操作中总会加一条通用规则:所有枚举字段的未知值禁止强校验,必须允许通过并记录告警日志,等确认后统一处理。
坑五:想一次性逆向完整再动手。老系统动辄上百万行代码,指望一个月全部逆向完毕再写PRD,PRD还没写完项目就被催死了。正确做法是"分模块逆向、分模块评审、分模块移交",把系统按依赖关系排优先级,核心交易先做,边缘模块后做。PRD也按模块输出,先交付一部分就能让开发先动起来。
坑六:逆向过程中丢掉了"数据流图"。单纯关注功能需求很容易忽视数据流转关系。我在第二个项目里就吃过亏,某个数据从A表写入到B表再到C表的完整链路没理清楚,结果新系统的数据同步设计漏了一环,上线后业务报表缺数据。后来我养成了强制习惯:每个业务域必须单独画一张数据流图,标注清楚数据产生、转换、落地、消费的每个环节,这张图也作为PRD的附录交付。
5.2 常见问题排查技巧速查表
遇到拿不准的情况,下面这组排查套路可以作为参考。
| 排查对象 | 首选方法 | 备选方法 | 兜底方案 |
|---|---|---|---|
| 字段含义不明 | 全局搜索字段在代码中的读写位置 | 查数据库COMMENT | 抽样SELECT历史数据观察值规律 |
| 接口调用关系不清 | 静态代码依赖分析工具 | 动态抓包(生产环境短时观察) | 查看API网关或Nginx访问日志 |
| 定时任务行为不明 | 查看调度平台配置 | 全局搜索Cron表达式和Schedule注解 | 翻运维的作业平台执行记录 |
| 业务规则定位不准 | 用Debugger打断点走一条真实流程 | 查看单元测试用例(如果存在的话) | 直接问回归测试人员平时测什么 |
| 判断功能是否废弃 | 查询Git提交历史和代码注释 | 看生产日志中该入口近期是否有真实调用 | 在PRD里标记"待确认",迁移时先保留再灰度验证 |
5.3 实操心得
根据我个人的经验,逆向PRD的过程中最需要警惕的心态是"带着预设去找证据"。做代码逆向,一定不要先入为主地认为某项需求应该是怎样的,然后再去找代码证明自己是对的,而是要老老实实跟着代码走,代码说有什么就记录什么。这中间,遇到"代码行为和业务理解不一致"的情况,以代码行为为准记录,把矛盾点单独标识,等到评审会让业务方做最终裁决。
还有一点值得养成习惯:逆向过程中每天写一条"发现日志",把当天发现的关键需求细节、代码位置和备注记录下来,两周攒下来的日志就是你PRD的素材库。真实项目里,我往往不是坐在电脑前"写"PRD,而是把零散记录汇总起来做拼接和归纳,写起来反而更高效。
单独的PRD文档也建议配一张"需求追溯矩阵",每条PRD条目都对应源代码位置,评审、开发、测试全程可以按图索骥。这个矩阵建议用Excel或在线表格管理,列字段设为需求编号、需求描述、来源代码、评审结论、开发状态、测试状态。项目推进中它就是你跟团队对齐的作战地图。
最后说几句
代码确实会说真话,但前提是你用心去听。逆向PRD的本质,不是一项工具活,而是一场与老系统开发者的隔空对话。试着去理解当初他们在想什么、为什么会写出这段略显古怪的逻辑,往往比你机械地照抄行为更能发现需求背后的意图。我在一次逆向过程中发现一段看起来很怪异的定时任务,研究之后发现它是在做"月末数据修正",而这个功能从来没在系统界面里出现过,也没有人知道它存在。如果不是看了代码,这个"幽灵逻辑"就会在系统迁移中无声消失。
如果你正在面对一个没有文档的老系统,不妨从现在开始,先把代码仓库克隆下来,跑一遍依赖分析,再打开那个你最熟悉的主业务Controller看一眼。你会发现,那条通往PRD的路,其实早就已经在代码里铺好了。