背景:一次"声称等价"的重写
2026 年 8 月,我把 dreamyouxi.com 的后端从 Python FastAPI 全量重写为 Rust(axum + tokio + sqlx)。重写提案里写着"对外行为与 Python 版逐项等价",切换前的验证门也全部通过:9 个读路由双版本对照 0 diff、审计聚合零差异、admin 页面渲染 200。
但切换当天我做了一次完整的功能对等性审计,结果发现:55 条路由里有 6 条没实现、3 处方法/路径偏差、微信推送整模块零移植——最严重的是,后台博客管理面板的主链路在 Rust 版上实际是不可用的。这篇文章记录这次审计的完整方法论,它适用于任何"重写/迁移后如何证明没丢功能"的场景。
方法论第一条:逐路由对照,以旧版为唯一真相
审计的第一步是把两个版本的 HTTP 路由表全部摊开对照:Python 版 55 条(方法 + 路径 + 请求格式),Rust 版 47 条。机械但有效——缺口立刻现形:GET /admin/api/blog/posts(后台文章列表)、POST /admin/api/blog/upload(multipart 上传)、GET /admin/api/blog/posts/{id}/content(编辑器取正文源)等 6 条在 Rust 路由表里不存在。
关键纪律:修复方向是 Rust 对齐 Python,而不是反过来。旧版是行为基准,回滚点也是旧版——保留新版的"自创契约"只会制造长期漂移源。
方法论第二条:前端 JS 才是事实契约
路由对照发现缺口后,最关键的一步来了:把 admin 页面的 JavaScript 逐行提取 fetch 调用清单。因为后台管理页是 2700 行 JS 的重前端,它调用的方法、路径、请求体格式、以及data.ok这类响应字段判定,才是真正的事实契约。
这一步交叉验证出的问题比路由对照多得多:
- 前端用
GET拉文章列表,Rust 版该路径只注册了 POST——列表加载直接 405; - 前端上传走
multipart form(file + meta 字段),Rust 版改成了纯 JSON——发布文章 404; - 前端成功判定全靠
data.ok,Rust 版响应体没这个字段——即使操作成功也提示"失败"; - 存档编辑弹窗提交的是
{action, id, patch},patch 是子对象——有一版实现直接读顶层字段,编辑保存静默无效,连报错都没有。
教训很清楚:"API 级冒烟通过"不等于"前端能用"。curl 测的是你自己声明的契约,浏览器跑的是前端实际依赖的契约。两者不一致时,一定是实现错了。
方法论第三条:多域并行,各域只回答一个问题
双版本对照天然可拆分。我把审计拆成五个互不重叠的域,每域只回答一个问题:
- 路由域:路由集合一一对应吗?
- admin 写路径域:每个写操作的字段校验、状态码、响应体逐项一致吗?
- 读路径域:缓存 key 构成、计数口径、正文处理链一致吗?
- 审计域:落盘格式、会话划分、聚合口径一致吗?
- 生命周期域:后台任务清单、启动校验、fail-fast 行为一致吗?
分域的收益是问题分级变得自然:按对生产的实际影响排 P0(功能破坏)/ P1(缺失降级)/ P2(口径细节),并把提案里声明过的有意差异单列(比如主动移除 markdown 上传)——三者不混,修复优先级一目了然。
方法论第四条:小心"验证过的部分"之外
切换前的验证门声称"聚合零差异",但那只是单日静态窗口的结论。审计发现多日聚合按天循环处理,跨午夜会话被切成两段——单日窗口根本测不出来。类似的还有:审计日志跨午夜批次整体写进"今天"的文件、限流参数整个缺失、无 XFF 时所有直连流量塌缩成同一个 0.0.0.0。
这一类问题的共性是:验证用例的形状决定了你能看到什么。修复后我把跨午夜双日志作为固定测试样例(23:58 → 00:01 应延续为同一会话),把 admin 前端的 fetch 清单生成为逐路由对照脚本——让"没测到的形状"不再依赖运气。
方法论第五条:修复也要有验证门
修复分两批落地:P0(后台契约 + 前台 bug)先行,P1(审计口径/限流/运维设施)跟上。每一批的验证门包含三层:
- 单元/契约测试:把 Router 装配抽成可测函数,oneshot 逐条打前端 fetch 清单,断言状态码与响应结构;
- 部署灰度验证:8 条公开路由 200(限流零误伤)、8 条修复的后台路由返回 401(鉴权拦截即路由存在)、搜索结果不再污染列表缓存;
- 真实端到端:用真实的 multipart 上传发一篇文章——也就是你正在读的这一篇。这篇文章的存在本身,就是发布链路修复完成的证明。
收尾:几条可复用的原则
- 重写声明"逐项等价"时,把对照清单本身做成 artifact,让"等价"可核对而不是可相信;
- 前后端分离的系统里,前端调用清单是契约的测不准底线——以它为准核对后端,而不是以文档为准;
- 验证门必须覆盖与声明同形状的场景:声称多日聚合等价,就必须有多日(含跨夜)样例;
- 有意差异(移除功能、语义变更)必须在提案里显式声明——审计时先分离声明项,剩下的才是真缺口。
11 年的个人站,从自研 C++ 服务器到 WordPress 到 FastAPI 再到 Rust,每一次迁移的底气都不是"我改得很小心",而是"我有一套能证明没丢东西的方法"。这次审计抓出的 30+ 个缺口,大部分在切换前的人工检查里是不可见的——方法比仔细更有用。
本文由 Claude Code 协作完成(审计、修复与发布链路验证),属于"AI Coding 实战"系列。