☰
没有需求文档的老系统怎么迁移?用代码考古还原真实业务,产出可执行PRD
2026/10/29 6:51:04 网站建设 项目流程

去年接了个老系统迁移项目,系统跑了大十年,需求文档、接口文档、设计文档早就不知道丢哪了。新团队要在一个季度内把核心模块迁到新平台,领导开口第一句话就是:先拿一份PRD出来评审。我在工位上翻遍了能翻的资料,能用的只有运维给的一堆jar包和一套刚搭好的测试环境。当时心里是真没底,但干完这一轮,我得说一句:代码是会讲真话的。文档可以停更在五年前,代码却一直跟着真实业务在跑。没有需求文档的老系统,本质上就是一份还活着的需求说明书,关键在于你会不会拆、会怎么读、能不能把它转写成一份能用的迁移PRD。

这篇就把我整个操作过程拆给你看。想接手老系统却苦于没文档的开发、要规划迁移的项目经理、还有想系统学一学“代码考古”思路的同学,应该都能从中找到几招能直接用的。

1. 先喝水再挖井:为什么代码才是最可靠的需求源

1.1 文档会过期,代码不会

老系统的文档普遍存在一个问题:停更时间永远比你想的更早。我见过最典型的例子是,需求文档里白纸黑字写着“订单只能取消一次”,但代码里已经有取消、恢复、再取消的完整闭环。为什么?因为2019年产品规则改成允许用户在被拒后重新提交,然后运营又加了一个“管理员可恢复订单”的后台入口。需求改了三轮,文档一次都没更新过。如果照着文档写迁移PRD,新系统上线第一天就会丢掉近两年的业务修正。

代码则不一样。线上系统只要还在跑,执行的逻辑就是当前真实业务,哪怕这段逻辑看起来有多不合理。代码里的每个if分支、每个状态字段、每段被注释掉的妥协方案,都是历史需求决策留下的化石。比如我后来在代码里看到大量“// 2020-06 兼容XX渠道”这类注释,这就是一份天然的需求变更日志。所以我的原则很简单:做迁移PRD时,需求来源的优先级是线上可执行代码大于数据库结构大于日志和监控大于运维手册,最后才是那堆不知道哪年写的二手文档。

1.2 迁移PRD要卖给谁:先搞清楚读者,再决定写法

写文档之前先想清楚一件事:这份迁移PRD是给谁看的?普通PRD是给产品和开发讲“我们要做一个新东西”,迁移PRD则是给一群完全没碰过老系统的人讲“老系统现在到底有什么、行为是什么样的”。读者不同,写法完全不同。

我这份PRD主要面对三类人。一类是目标平台的开发团队,他们需要知道每个功能模块的入口、角色、业务流程、业务规则,才能实现行为等价;一类是测试团队,他们要拿这份文档设计功能验证用例,确认迁移后的系统和老系统表现一致;还有一类是拍板评审的领导或外部团队,他们需要从概览层面快速理解迁移范围、工作量、风险点。所以迁移PRD的核心要求是行为等价,不是体验升级。千万不要在迁移PRD里顺手夹带产品改版的想法,今天想改个流程、明天想加个字段,评审永远过不了,迁移边界也会越扯越模糊。

1.3 动手前先自查三件事:授权、环境、工具

逆向老系统这件事,第一步不是打开IDE,而是确认授权。做代码梳理、数据字典整理、接口盘点,必须在公司内部有明确授权,通常就是运维和研发负责人给你的代码库及测试环境访问权限。整个过程只做读和分析,不修改任何运行逻辑,这是红线。我在项目启动第一天就把权限和范围跟相关方对齐清楚,后面才不会有人质疑。

环境上,最理想的是有一整套能启动的测试环境,连数据库都能连上。如果跑不起来,也要拿到完整的部署清单,包括jar包版本、配置文件、依赖的中间件清单。工具反而最简单:IntelliJ IDEA加反编译工具加数据库客户端就够了,我习惯再加一个ripgrep用来做全局搜索。不要迷信花哨的逆向平台,老项目代码考古,大多数体力活是在“读代码”和“查数据”之间来回切换,工具顺手比什么都重要。

2. 搭骨架:接管老系统的一周内要摸清的底细

2.1 从部署入口识别系统边界

拿到代码后不要急着钻进业务逻辑,先搞清楚这个系统有哪些部分、边界在哪。我会先看技术栈和配置:项目里是pom.xml还是build.gradle,还是古老的lib目录直接塞jar包;主配置文件名是application.yml还是application.properties;有没有web.xml,有没有Nginx配置文件,有没有Dockerfile。这一圈下来基本能判断出系统的技术栈、部署方式、依赖的中间件。

第二步是找入口。Spring Boot项目就看启动类和Controller;老式SSH项目看web.xml里的Servlet配置和struts配置文件;如果连入口都找不到,就去翻部署脚本和启动日志,看它的class入口是什么。我习惯在这个过程中顺手画一张文字版的系统上下文图:左侧是哪些外部系统调它,右侧是它调了哪些外部系统,下面挂了哪些数据库、缓存、消息队列。这张图就是PRD里的系统概览部分,后面所有功能模块都要挂在这张地图上。

2.2 用一条命令把接口和调用链拉出来

接口是迁移PRD的核心线索,好在代码里接口跑不掉。我最常用的操作是全局搜索路由注解,如果是标准的Spring MVC项目,可以直接用ripgrep把映射注释捞出来:

rg "@(Get|Post|Put|Delete|Request)Mapping" src/main/java -N > api_mapping.txt

没有ripgrep就用grep -rn,效果一样。把结果清洗一下,能整理出一份基础接口清单,包含请求方法、请求路径、所在Controller文件。接下来再用IDEA的调用层次功能,从每个Controller方法往下追Service、Mapper,把一条调用链拉出来。这里有个很实用的技巧:光看代码还不够,配合浏览器开发者工具或抓包工具,在老系统的测试环境里真实点一遍功能,把请求出入参和响应抓下来,和代码比对。代码告诉你应该怎么走,抓包告诉你线上到底怎么走的,两者对上了,这条链路才算真正吃透。

2.3 数据库优先还是代码优先?我的排序是表结构

很多人做代码考古喜欢从入口顺序看,我自己的习惯反过来:先看数据库表结构,再看代码。原因是表结构通常是浓缩的业务语义,字段名、类型、注释往往比一长串Java代码直白得多。我拿到数据库连接信息后,先做四件事:把所有表列出来,按数据量级排序;把核心业务表和配置表、日志表区分开;导出每张表的字段、类型、长度、可空、注释、索引、主外键;然后统计几张核心表的数据分布。

这一步用基础的SQL就能搞定,比如:

SELECT table_name, table_comment FROM information_schema.tables WHERE table_schema = 'xxx'; SELECT column_name, column_type, is_nullable, column_comment FROM information_schema.columns WHERE table_schema = 'xxx' AND table_name = 't_order';

表结构看完再回代码,你会发现自己对字段的敏感度完全不同。比如看到t_order表里有个f_status字段,值域是0到5,再看代码里的OrderStatusConstant和switch分支,很快就能拼出状态图。我的整体顺序是:表结构、接口入口、服务层逻辑、定时任务和消息消费者、前端页面。走到前端那步,基本就是在印证前面的逻辑了。

3. 挖业务:从代码里还原流程、规则和数据字典

3.1 找入口的四种姿势:HTTP、定时任务、MQ、回调

老系统的业务入口不只有HTTP接口。如果你只盯着Controller,一定会漏掉一堆定时任务和消息处理逻辑。我建了一个入口清单,分四类逐个排查。第一类HTTP入口,通过路由注解和配置文件找Controller和Servlet;第二类定时任务,搜@Scheduled、Quartz配置、crontab脚本;第三类消息队列消费者,找@RabbitListener、@KafkaListener,老项目可能没有注解,直接找onMessage方法;第四类是外部回调,也就是第三方系统调过来的接口,这类最容易被忽略,但在迁移时往往是坑。

每找到一个入口,我就往清单里记一条:触发方式、入口地址或方法、调了哪个Service、涉及哪几张表。这样边摸边记,功能全景图会快速变完整。等清单盘点完,你会发现原来系统里至少有三分之一的行为是从定时任务和MQ消费者出发的,这些行为如果漏掉,迁移后的对账、报表、推送功能会集体失效。

3.2 状态机不是靠猜,是靠枚举和if-else

状态字段是业务规则的主心骨。还原状态机时,我先找代码里的枚举类和状态常量类,常见命名是OrderStatus、StatusEnum、XxxConstant,里面通常会定义每个状态值的含义。如果没有枚举,只有魔法数字,就结合数据库里的真实数据分布来反推,再用日志里的关键字确认。

下一步是沿着状态字段的读写位置找状态流转。在IDEA里直接点击状态常量,用Find Usages找出所有读取和赋值的代码,看每一处switch或if判断,就能理出什么动作让状态从A变成B。我整理状态机时习惯用一张表,每一行对应一次流转:

当前状态触发动作条件或角色目标状态代码位置
0 待提交submit用户本人1 待审核OrderServiceImpl:123
1 待审核approve管理员2 已通过OrderServiceImpl:180
1 待审核reject管理员3 已驳回OrderServiceImpl:200

整理完状态表,业务流程基本就清晰了。这比看一叠业务描述文档有用得多,因为代码里只有真实存在的流转,没有那些“规划中”“也许以后会做”的水分。

3.3 数据字典双向拼图:建表SQL、Mapper、日志文案

迁移PRD里必须有数据字典,否则开发团队拿到PRD也不知道字段怎么填。数据字典我不会只抄数据库注释,因为很多老库的注释是空的。我的做法是三方对拼:第一方是数据库表结构信息,包括字段名、类型、长度、默认值;第二方是代码里的Mapper SQL和实体类注解,看这个字段到底被怎么赋值、怎么查询;第三方是日志文案,例如代码里log.info写的“订单提交成功”“审核驳回”往往就是业务事件最直观的描述。

举个小例子,t_order里的f_status字段数据库注释是空的,类型是tinyint。但我在OrderStatusConstant.java里看到3这个值被定义为REJECTED,又在ServiceImpl里搜到log.info("订单被驳回,原因:{}", reason),这样这个字段的含义和取值就全部串起来了。我最终会做成一张数据字典表,每行一个小栏位,同时带上来源,是建表SQL、实体类还是哪段日志,方便评审时回溯。

3.4 那些藏得很深的业务规则:校验、权限、并发、灰度

很多业务规则不在业务流程里,而是在代码的边角里。头一个就是校验逻辑,比如上传文件的大小限制、手机号格式、金额上限,这些往往零散地写在Validator、正则表达式或者参数拦截器里。我在PRD里会专门留一个“业务规则”小节,把这类硬编码规则逐条列出来。

第二个是权限控制。如果代码里有登录拦截器、@PreAuthorize、角色判断的代码块,要高亮标出来。因为迁移后的权限模型很可能和老系统不一样,你得先知道每个操作原来要求什么角色,才能设计权限映射。

第三个是并发控制。看到version字段和select for update语句、Redis锁的代码,要提醒迁移团队这里存在并发控制,新系统必须保持同等语义。第四个是灰度开关。老系统里经常有一堆配置项控制某些功能是否开启,代码里表现为if(featureEnabled)之类的分支。这些开关在迁移后默认值要重新评审,不梳理清楚就会出现“线上功能在新系统里怎么不见了”的诡异问题。

4. 写迁移PRD:把代码考古结果变成可执行的文档

4.1 迁移版PRD的结构和普通PRD差在哪

普通PRD结构一般是背景、用户画像、功能说明、交互细节,迁移PRD完全不照这个来。我总结了自己常用的一套迁移PRD目录,可以直接抄作业:

  1. 背景与目标:为什么迁、迁移边界、目标平台
  2. 现状系统概览:系统上下文、模块清单、外部依赖
  3. 功能需求清单:每个功能的入口、角色、流程、业务规则
  4. 数据需求:核心表、数据字典、数据量、保留策略
  5. 接口需求:外部接口清单、协议、字段、调用频率
  6. 非功能需求:性能、并发、备份、日志、监控、安全
  7. 迁移约束与风险:停服窗口、数据迁移方案、兼容性清单
  8. 验收标准:功能等价用例、数据核对规则

差别在于,不写用户痛点,不写交互细节,不写“以后可以考虑的优化方向”。只写现状是什么、规则是什么、迁移时要注意什么。老系统的历史包袱已经够重了,迁移PRD再夹带私货,目标团队根本没法落地。

4.2 用四张表串起整个功能全景

写迁移PRD正文之前,我先做四张表,它们基本撑起整个文档的骨架。

第一张是模块清单表,列模块名、入口、主要页面或接口、迁移优先级。第二张是功能点清单表,每条功能记录编号、功能名、入口路径、操作角色、前置条件、涉及表、状态流转。第三张是数据字典表,就是前面整理出来的字段级清单。第四张是风险清单表,记录风险描述、证据来源、影响范围、建议方案。

这四张表我会放进PRD附录,并在正文里按模块引用。评审时最有用的其实是风险清单表,比如“原系统凌晨2点定时任务执行对账,依赖服务器本地时间,新平台需改调度”,这种问题如果不提前写清楚,测试阶段一定会炸。每张表的每一行都要带“来源”列,要么是文件路径加行号,要么是日志样本。来源列是我和产品经理、测试团队沟通时的底气。

4.3 实战回放:一个导入审核模块的逆向与转写

拿我手头一个“对账文件导入与审核”模块当例子。原文档只有一句话:“财务可导入对账文件。”但实际上这个模块包含上传、解析、核销、审核、差异报告五个环节。我是这么逆向的:

先在Controller里找到两个接口,一个是文件上传,一个是审核。顺着上传接口往下看,发现它先校验文件后缀和大小,再解析Excel逐行校验商户号、金额、日期,然后写入明细表并生成一个批次记录。继续看定时任务,每天凌晨2点有一个任务处理前一天的批次,逐条核销订单并更新明细状态。批次状态分为0新导入、1校验中、2待审核、3已通过、4已驳回。异常规则我也找到了,Excel超过5000行会拆批;某行数据格式错误只记录错误行不会中断整个文件;核销失败时批次回滚到待审核并给负责人发邮件。

最终我在PRD里把这个模块转写成一个标准功能描述,流程、前置条件、异常分支、结果输出都写清楚。如果你没有这个逆向过程,凭文档里那一句话去迁移,新系统很可能只做一个文件上传功能,后面整个对账链路全丢了。

4.4 看不懂和不存在是两回事:学会写“待确认”

代码考古一定会遇到读不懂或解释不了的地方。这时候千万别硬编,也别当看不见。我的处理原则是先标记、再求证、最后决定迁移还是不迁移。

比如遇到一段没有任何注释、字段名也不是直译的魔法逻辑,我就在PRD里写成“该规则语义待确认,疑似与XX渠道相关”,然后放到问题清单里,每周和业务方对一次。再比如代码里有一段接口地址存在但调用量几乎为零的死代码,我会先找运维拿日志确认,确实没流量,就标注“建议新系统不迁移,待最终确认”。

“待确认”一点都不可怕,可怕的是把猜的当事实写进PRD。我见过有人因为想当然补了一段状态流转,结果新系统上线后一笔历史数据怎么都查不出来,最后排查到就是因为老系统里根本没那套流程。写“不知道”不会挨骂,写“确定的错误”才会让整个迁移项目翻车。

5. 现场实录:常见坑、排查技巧和我的收尾习惯

5.1 反编译和代码阅读工具的真实用法

有源码的情况最简单,直接用IDEA全局搜索、调用层次、Find Usages三件套,按着调用链往下跳。但老系统经常没有源码,只有部署目录里的一堆class或jar。我的做法是用IDEA直接打开jar包,它会自动反编译,虽然生成的代码偶尔有点乱,但跳转和搜索功能完整,比单独开一个jd-gui强得多。jd-gui更适合看单个小文件,大项目一打开就卡。

如果老系统是客户端或者前端工程,那思路其实一样,只是工具换成浏览器开发者工具和抓包工具,重点不要放在还原全部前端逻辑上,而是抓请求、响应、关键校验规则,把这些整理成接口清单和规则表。记住,逆向老系统的目的是迁移等价,不是把所有代码重新推演一遍。读不懂的地方,配合数据库真实数据分布去看,逻辑会清晰很多。

5.2 老系统的隐性依赖:别让Nginx和定时任务坑你一整轮

代码本身的逻辑再复杂都有迹可循,隐性依赖才是真正的大坑。我踩过最典型的一次是,代码里所有定时任务在配置文件里都有,但跑了一圈发现还有一个每天凌晨同步数据的逻辑是从服务器crontab里调的一段脚本,脚本再调用本地服务端口。代码里完全没有定时任务配置,只有运维的crontab才知道。后来我把这个写进环境依赖清单,迁移到新平台时才没有把这段逻辑弄丢。

另一个常见隐性依赖是配置中心或环境变量。代码里写了@Value("${xxx.url}"),本地启动直接报错,必须从发布脚本和配置中心里把实际值捞出来。还有硬编码的IP和端口,老系统里很多地方直接写死了旧环境的地址,迁移时这些地址要么改成配置,要么在新的网络环境里重新映射。我的建议是动手第一周就把部署脚本、配置文件、Nginx配置、计划任务全部归档,建立一份环境依赖清单,每一项都标记来源和用途。

5.3 常见问题速查表

代码考古过程中有些问题反复出现,我直接整理成了一张速查表,丢给团队用。

现象可能原因排查方法
反编译代码和线上行为不一致代码版本和部署版本不一致对比jar包MD5,找运维要部署清单
数据库字段无注释,看不懂命名缩写或历史遗留结合Mapper SQL、日志文案、真实数据分布反推
找不到接口文档文档确实没有全局扫描路由,再用抓包和日志关联
代码里有大量反射或动态代理老框架或插件机制运行时打印调用链,用arthas现场观察关键方法
定时任务在代码里找不到在操作系统层或配置中心查crontab、systemd、计划任务、Quartz持久化表
状态字段全是魔法数字没有枚举注释统计真实数据分布,找业务方确认语义

5.4 收尾习惯:给每个结论留下证据链

写迁移PRD这件事,做到最后拼的不是谁的文笔好,而是谁的结论能追溯。我习惯在PRD正文之外,单独维护一份考古证据库:每条业务规则后面标注来源于哪个文件的哪一行,哪条日志样例,哪张表的哪个字段。关键结论还会附上截图或关键日志片段。

这样在评审会上,不管领导还是业务方问“你凭什么说这个功能必须保留”,我都能当场翻到证据,而不是说一句“我觉得”。这个习惯也帮我避免了不少来回扯皮。最实在的一个动作是,每从代码里挖出一个结论,就顺手把文件路径、行号、数据样本记到表格里。过程看着笨,越到项目后期越觉得值。

最后再分享一个收尾习惯:PRD里的“待确认问题清单”要每周和业务方过一遍,不要拖到评审前一夜才去问。迁移项目最大的风险不是代码难读,而是业务规则确认得太晚。早一周把问题抛出去,业务方有足够时间回忆和查证,你也有足够时间改文档。代码会说真话,前提是你得真的去听,还要让听来的每个字都能拿得出依据。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询