写一个养老金计算器,我被自己的"假数据"上了一课

实验室主理人 ·

这个项目的起点很朴素:做一个参数全透明、算法可查阅、结果可溯源的养老金计算工具。 但做到一半我才发现,最该被溯源的,是我自己拍脑袋写进去的数据。


起念

事情说起来不复杂。

去年有段时间,我爸妈频繁转发各种”养老金测算”小程序给我。我点开试了几个,体验大致相同:填几个数字,出来一个结论,但中间怎么算的——不知道。参数能不能调?不能。数据是哪一年的?不写。

这类工具我给它起了个名字:黑箱安慰剂

我就在想,能不能做一个反过来的——参数全开放,算法可查阅,每一步都告诉你为什么是这个数

于是有了”明数”——明数的明,数字明白的明。

TDD 开局:先写测试,再写代码

这个项目我做了个决定:全程测试驱动开发。

先写计算引擎的测试,再写实现。比如基础养老金的公式是:

当地社平工资 × (1 + 本人平均缴费指数) ÷ 2 × 缴费年限 × 1%

我先写测试用例:

it('指数1.0、缴费30年,应返回3000', () => {
  const result = calculateBasePension({
    averageSalary: 10000,
    averageIndex: 1.0,
    totalYears: 30,
  })
  expect(result).toBe(3000)
})

然后才写实现。一个模块一个模块地红绿循环。基础养老金 → 个人账户 → 过渡性养老金 → 方案对比 → 情景模拟,七个模块,46 个测试,全部通过。

这种写法的好处是:你不可能偷偷改了一个地方而不知道后果。

一个性能陷阱:Zustand 的隐形成本

项目做到一半,用户说”浏览器 CPU 很高”。我一看,是输入滑条的问题。

代码里有个”预期退休年龄”的滑条,拖一下 onChange 就触发一次状态更新。问题是——我用 Zustand 管理状态,每个组件都是这么拿数据的:

const { retirementAge, futureLevel, salaryGrowthRate } = useFormStore()

这个写法会订阅整个 store。拖一下滑条、改一个字段,所有组件都跟着重渲染。App、四个步骤组件、结果页、对比卡片——全部跑一遍。

修复方案很简单:

const retirementAge = useFormStore((s) => s.retirementAge)

精确选择,只订阅你关心的字段。 这样拖滑条的时候,只有滑条组件自己重渲染。

改动很小,效果很明显。这件事给我的启发是:状态管理不是”能用就行”,默认写法决定了性能天花板。

最打脸的一刻:我给用户灌了”假数据”

产品做好,功能跑通,测试全绿。用户问了我一个问题:

“数据准确性根本得不到保证,这还怎么可信?”

我觉得冤枉——8 个核心省市的数据我都写了啊,每个省还有”✓ 已校准”的标记。

然后我仔细看了看自己写的数字。比如北京,我写了15,701 元/月

用户问:这数字哪里来的?

我:嗯……我……”大概”记得是这个数。

我根本没有查证。 我只是根据零散的印象,写了几个看起来”差不多”的数字。而我标了”✓ 已校准”。

这就好比开了一家称,号称”童叟无欺”,然后随手画了个刻度就开始给人称重。

我开始搜真实数据。结果是这样的:

省份我猜的数官方计发基数差距
北京¥15,701¥12,049多了 30%
上海¥14,964¥12,434多了 20%
广东¥10,421¥9,493多了 10%
山东¥8,833¥7,831多了 13%

没有一个是对的。

真实数据来自各省人社厅每年公布的文件,比如北京的”京人社发〔2025〕13号”、广东的”粤人社发〔2025〕32号”。数据是公开的,只是我没去查。

可点击的信任

修正数据之后,我又在每个省的参数里加了一个字段:sourceLink

现在结果页里,每个省的数据来源都是一个可点击的链接,直接打开官方发文:

当地社平工资:¥12,049/月(2025年) 数据来源:京人社发〔2025〕13号 [↗]

用户点击就能看到官方文件原文。

这件事让我想明白一个道理:“透明”不是把数字列出来,而是让人有办法核实你的数字。

隐藏的管理页

另外我还做了一个”隐藏功能”:双击页面标题,会进入一个参数管理页。在这里可以直接修改各省的社平工资、数据年份、来源链接,修改后存到浏览器本地存储。

下次测算自动用新值。参数每年更新后,不用等代码发布,自己改就行。

(如果把改好的数据同步到代码文件、提交 git、重新部署,所有人就都能用上新数据。)

值得一说的几件事

回顾整个开发过程,有几件事我觉得值得记下来:

  1. TDD 不只是测试手段,也是一种设计工具。 先写测试逼你想清楚输入和输出,代码写出来自然就是模块化的。

  2. 默认写法决定了性能天花板。 状态管理选什么、怎么选,不是”能用就行”的事。一行 useFormStore() 和一行 useFormStore(s => s.field) 看起来差不多,但前者拖一次滑条重渲染整个页面,后者只影响一个组件。

  3. 数据可信度不是”可以补”的事,而是”一开始就不能错”的事。 我在算法上花了很大功夫,但数据是拍脑袋写的,再好的算法也是白搭。标注”已校准”的时候要有据可查,否则就是在透支信任。

  4. 透明是指别人能追究你。 光说”数据来自官方”不算透明,要让人点一下就能看到官方文件才算。

文章最开头那句 slogan——“算得明白,才敢信”——最后变成了项目的自我审视。

📖 本文已被阅读 --

评论

加载中...

支持 Markdown 语法