我花 600 元用 AI 做了一个本地优先的个人财务管理 App——技术选型与核心实现
文章目录
- 我花 600 元用 AI 做了一个本地优先的个人财务管理 App——技术选型与核心实现
- 背景:为什么做这个 App
- 产品长这样:
- 花了多少钱?
- 技术选型
- Vibe Coding:我的角色从"写代码"变成了"指挥 AI + 审核"
- 核心功能实现
- 1. 持有成本计算:4 种分摊策略
- 2. 净资产趋势:降采样 + 线性回归预测
- 3. 自动备份:App 退到后台就存一份
- 遇到的技术难点
- 图表手势冲突
- 数据导入的 ID 重映射
- 测试
- 开源地址
我花 600 元用 AI 做了一个本地优先的个人财务管理 App——技术选型与核心实现
不联网、不注册、不上云。你的财务数据只存在你手机上。
背景:为什么做这个 App
市面上的记账 App 都有一个共同点:要求你注册账号,数据上传到云端。
但我不想把自己的财务状况交给任何第三方服务器。我要的是一个能回答三个问题的工具:
- 我有多少钱— 多账户余额汇总
- 我的东西值多少— 实物资产估值 + 持有成本计算
- 净资产趋势如何— 历史折线图 + 目标预测
于是我做了家底 WorthBase:一款隐私优先、本地存储的个人财务状态管理 App。
产品长这样:
花了多少钱?
整个项目我只为 AI token 付了费。成本分解:
| 项目 | 费用 |
|---|---|
| Claude + Qwen API(编程辅助) | ~¥600 |
| EAS Build 构建 | 免费额度内 |
| Apple/Google 开发者账号 | 未购买(开源分发) |
| 总计 | ~¥600 |
没有上架应用商店。因为还不确定是否真的有人用,先放GitHub 下载。
技术选型
┌─────────────────────────────────────────────────┐ │ UI Layer │ │ Expo Router (Tab Navigator) │ │ react-native-paper (Material Design 3) │ │ react-native-svg (图表渲染) │ │ react-native-gesture-handler (缩放/拖拽) │ ├─────────────────────────────────────────────────┤ │ State Layer │ │ Zustand (轻量状态管理) │ ├─────────────────────────────────────────────────┤ │ Engine Layer │ │ HoldingCostCalculator (持有成本,4种策略) │ │ NetWorthCalculator (净资产 = 余额 + 估值) │ │ ProjectionCalculator (线性回归预测目标达成) │ │ SettlementCalculator (卖出结算) │ ├─────────────────────────────────────────────────┤ │ Data Layer │ │ SQLite (expo-sqlite) │ │ Repository 模式 (7 张表, 无 ORM) │ └─────────────────────────────────────────────────┘选择Expo SDK 55而不是裸 React Native,是因为 Expo 的开发体验(热更新、EAS 构建)对独立开发者来说效率更高。选择SQLite而不是 AsyncStorage,是因为财务数据需要事务支持和复杂查询。选择Zustand而不是 Redux,是因为 75 个文件的项目不需要那么重的状态管理。
Vibe Coding:我的角色从"写代码"变成了"指挥 AI + 审核"
这个项目 99% 的代码是 AI 写的。我的工作方式变成了:
- 描述需求:用自然语言告诉 Claude 我要什么功能
- AI 生成代码:Claude 产出完整实现
- 跑起来验证:在设备上运行,看效果
- 描述 bug 让 AI 改:发现问题后用自然语言描述,AI 修复
我的核心价值从"编码能力"变成了"需求定义能力 + 代码审核能力"。AI 不懂"持有成本应该怎么算才符合直觉",但我懂。我负责做产品决策,AI 负责把它们变成代码。
这个流程让整个项目在5 天内从零到可用。
核心功能实现
1. 持有成本计算:4 种分摊策略
这是整个 App 最核心的功能。你买一台 8000 块的 MacBook,用了 3 年,它每个月"花"你多少钱?
答案取决于你怎么算。我设计了 4 种分摊方式,用Strategy 模式实现:
// src/engine/strategies/index.tsconststrategyMap:Record<AmortizationType,AmortizationStrategy>={[AmortizationType.SIMPLE_LINEAR]:SimpleLinearStrategy,[AmortizationType.EXPECTED_LIFESPAN]:ExpectedLifespanStrategy,[AmortizationType.RESIDUAL_VALUE]:ResidualValueStrategy,[AmortizationType.NO_AMORTIZATION]:NoAmortizationStrategy,};exportfunctiongetStrategy(asset:Asset):AmortizationStrategy{returnstrategyMap[asset.amortizationType]??NoAmortizationStrategy;}每种策略实现同一个接口的三个方法:
// src/engine/strategies/SimpleLinearStrategy.tsexportconstSimpleLinearStrategy:AmortizationStrategy={calculateMonthlyCost(asset,currentDate){constmonthsHeld=monthsBetween(asset.purchaseDate,currentDate);returnasset.purchasePrice/monthsHeld;// 持有越久,月均越低},calculateAccumulated(asset){returnasset.purchasePrice;},calculateRemaining(){return0;},};四种方式的对比:
| 方式 | 公式 | 月均变化 | 适用场景 |
|---|---|---|---|
| 简单线性 | 购入价 ÷ 持有月数 | 递减 | 电子产品、数码 |
| 预期寿命 | 购入价 ÷ 预期月数 | 固定 | 房产(360月)、家居(108月) |
| 残值分摊 | (购入价-残值) ÷ 预期月数 | 固定 | 车辆(60月) |
| 不分摊 | 月摊销 = 0 | 零 | 奢侈品、贵金属 |
为了让用户不需要理解这些算法,我做了一个智能推荐器,根据资产类型自动选择最合适的方式:
// src/engine/AmortizationRecommender.tsconstRECOMMENDATION_TABLE={[AssetCategory.ELECTRONICS]:{type:SIMPLE_LINEAR,hint:'电子产品贬值快,按已持有时间递减'},[AssetCategory.REAL_ESTATE]:{type:EXPECTED_LIFESPAN,defaultLifespanMonths:360,...},[AssetCategory.VEHICLE]:{type:RESIDUAL_VALUE,defaultLifespanMonths:60,...},[AssetCategory.LUXURY]:{type:NO_AMORTIZATION,hint:'奢侈品可能增值'},};用户添加一台 MacBook,系统自动选"简单线性";添加一辆车,自动选"残值分摊"并默认 60 个月。技术为产品体验服务,而不是让用户做技术决策。
2. 净资产趋势:降采样 + 线性回归预测
净资产 = 账户余额 + 资产估值。公式只有 68 行代码:
// src/engine/NetWorthCalculator.tsconstnetWorth=liquidAssets+assetValuations;但趋势图的数据处理更有意思。当数据点超过 24 个时,需要降采样。我没有用简单的等距取样,而是写了一个保留极值的降采样算法——确保趋势图上的峰值和谷值不会在降采样中丢失:
// app/index.tsx:644functiondownsamplePreservingExtrema(points,maxPoints){if(points.length<=maxPoints)returnpoints;constresult=[points[0]];// 始终保留第一个点constbucketCount=maxPoints-2;constinteriorPoints=points.slice(1,-1);constbucketSize=interiorPoints.length/bucketCount;for(letb=0;b<bucketCount;b++){constbucket=interiorPoints.slice(Math.floor(b*bucketSize),Math.floor((b+1)*bucketSize));// 在每个桶中找偏离趋势线最远的点constavg=(result[result.length-1].value+points[points.length-1].value)/2;letbestIdx=0,bestDist=-1;for(letj=0;j<bucket.length;j++){constdist=Math.abs(bucket[j].value-avg);if(dist>bestDist){bestDist=dist;bestIdx=j;}}result.push(bucket[bestIdx]);}result.push(points[points.length-1]);// 始终保留最后一个点returnresult;}另外,如果你设了净资产目标,系统会用最近 6 个数据点做线性回归,预测你什么时候能达成目标:
// src/engine/ProjectionCalculator.tsconstpoints=historicalPoints.slice(-6);// 最小二乘法求斜率(月均增长额)constslope=(n*sumXY-sumX*sumY)/(n*sumX2-sumX*sumX);// 预计达成日期 = 最后数据点 + (目标差距 / 月增长)constmonthsNeeded=(goal-currentValue)/slope;3. 自动备份:App 退到后台就存一份
本地优先 App 最大的风险是数据丢失。我的解决方案极其简单——App 退到后台时自动备份 SQLite 文件:
// app/_layout.tsx:64if(prev==='active'&&(nextAppState==='background'||nextAppState==='inactive')){BackupService.createBackup();}就这 5 行代码。没有后台任务、没有定时器、没有推送。AppState监听状态变化,退后台就复制.db文件到备份目录,最多保留 3 份。
此外还支持 JSON 格式的手动导出/导入(带 ID 重映射和事务安全),以及 CSV 格式的数据导出(方便在 Excel 里查看)。
遇到的技术难点
图表手势冲突
净资产趋势图支持缩放和拖拽,但它嵌套在 ScrollView 里。三个手势(图表缩放、图表拖拽、页面滚动)互相冲突。最终通过 Gesture Handler 的状态管理解决了:图表区域内优先响应缩放/拖拽手势,只有手势结束后才允许外层 ScrollView 滚动。InteractiveTrendChart组件因此有 540 行,是整个项目最复杂的单个文件。
数据导入的 ID 重映射
从 JSON 文件导入数据时,不能直接用原来的 ID(可能和目标设备的已有数据冲突)。解决方案是在一个事务内:清空所有表 → 重建账户和资产(记录 old ID → new ID 的映射)→ 用新 ID 重建子记录(余额快照、经常性支出等)。整个流程在一个数据库事务里,任何一步失败都会回滚。
测试
作为独立开发者,我用三层策略保证质量:
- Jest 单元测试:2,800+ 行测试覆盖核心引擎、Repository、Store、UI 组件和边界场景
- 设备手动测试:Expo Go 扫码即测,热更新快速迭代
- 真实用户反馈:小红书发了一篇帖子,18 条评论帮我发现了 2 个 bug
开源地址
🔗GitHub: github.com/callmebg/worthbase
如果你觉得这个项目有意思,给个 Star 就是最大的支持。欢迎提 Issue 和 PR。
下一篇:一个人从 0 到 1 做一个 App,我是怎么用 vibe coding 在 5 天内管理这个项目的(项目管理角度复盘)
技术深度系列(想深入了解某个模块的实现?):
- 1 WorthBase(家底)技术架构全解析:一款本地优先的个人财务 App
- 2 持有成本计算的 4 种分摊策略详解
- 3 本地优先架构:SQLite + Repository 模式 + 自动备份
- 4 净资产趋势可视化:SVG 图表 + 手势交互 + 降采样算法
- 5 资产生命周期管理:状态机 + 卖出结算
- 6 多账户余额管理:8 种账户类型 + 负债处理
- 7 React Native / Expo 开发体验与可复用模式
- 8 UI 组件库与设计系统