写一个养老金计算器,我被自己的"假数据"上了一课
这个项目的起点很朴素:做一个参数全透明、算法可查阅、结果可溯源的养老金计算工具。 但做到一半我才发现,最该被溯源的,是我自己拍脑袋写进去的数据。
起念
事情说起来不复杂。
去年有段时间,我爸妈频繁转发各种”养老金测算”小程序给我。我点开试了几个,体验大致相同:填几个数字,出来一个结论,但中间怎么算的——不知道。参数能不能调?不能。数据是哪一年的?不写。
这类工具我给它起了个名字:黑箱安慰剂。
我就在想,能不能做一个反过来的——参数全开放,算法可查阅,每一步都告诉你为什么是这个数。
于是有了”明数”——明数的明,数字明白的明。
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、重新部署,所有人就都能用上新数据。)
值得一说的几件事
回顾整个开发过程,有几件事我觉得值得记下来:
-
TDD 不只是测试手段,也是一种设计工具。 先写测试逼你想清楚输入和输出,代码写出来自然就是模块化的。
-
默认写法决定了性能天花板。 状态管理选什么、怎么选,不是”能用就行”的事。一行
useFormStore()和一行useFormStore(s => s.field)看起来差不多,但前者拖一次滑条重渲染整个页面,后者只影响一个组件。 -
数据可信度不是”可以补”的事,而是”一开始就不能错”的事。 我在算法上花了很大功夫,但数据是拍脑袋写的,再好的算法也是白搭。标注”已校准”的时候要有据可查,否则就是在透支信任。
-
透明是指别人能追究你。 光说”数据来自官方”不算透明,要让人点一下就能看到官方文件才算。
文章最开头那句 slogan——“算得明白,才敢信”——最后变成了项目的自我审视。
评论
加载中...