说说博主开源 AI 记账系统看全栈项目设计:Smart Ledger

刷到一个挺有意思的话题,结合自己之前的经验,整理了一下核心要点。

个人记账看似只是一个“收入减支出”的简单应用,但真正把它做成可持续用的产品后,开发者很快会遇到一系列工程问题:账户余额如何避免并发错乱,多账本数据如何隔离,账单导入怎样防止重复,AI 能力如何与核心业务解耦,PC 和移动端又该共享代码还是独立维护。

Smart Ledger 是一个开源的个人和家庭记账系统。它采用 Node.js、Express 和 MySQL 构建后端服务,用原生 HTML、CSS 和 JavaScript 实现前端页面,并提供 H5 移动端与 PC 桌面端两套界面。项目还集成了自然语言记账、小票识别、语音输入、账单分析、预算管理、借贷跟踪、多人协同和还款提醒等功能。

本文以该项目为例,从系统定位、整体架构、数据模型、核心业务流程、AI 能力集成、前后端设计、工程化建设和后续演进等方面,探讨一个记账系统背后的技术取舍。

一、项目定位:记账系统的繁琐度被低估了

许多人第一次设计记账系统时,会先建立一张“账单表”,其中包含金额、类型、时间和备注。这个模型足以应对单机笔记本,却很难承载真实家庭场景。

一个成熟的记账系统至少需要考虑以下需求:

1. 一笔钱从哪个账户流出,进入哪个账户;
2. 转账时如何保证两个账户余额同时变化;
3. 用户修改或删除账单后,账户余额怎样回溯;
4. 一个家庭账本中有哪些成员,各自拥有什么权限;
5. 分类如何支持多级结构和用户自定义;
6. 微信和支付宝账单如何导入;
7. 如何分析消费趋势、预测月末结余;
8. 如何向用户推送还款和信用卡账单提醒;
9. 数据导出、备份和删除如何满足安全要求;
10. 同一套业务如何同时适配移动端和桌面端。

因此,记账系统本质上不是一个简单的表单应用,而是一个具有账户一致性、权限边界、数据迁移和业务提醒要求的轻量级财务系统。

Smart Ledger 选择将前后端放在同一个代码仓库中维护,后端负责业务规则、数据一致性和外部接口,前端负责页面交互和数据展示。项目没有引入 Vue、React 等前端框架,也没有用 Chart.js 等图表库,而是用原生技术实现页面与图表。这种选择降低了依赖数量和部署繁琐度,格外适合希望自托管、二次开发或学习全栈开发的用户。

二、整体架构:一套 API,两套独立前端

项目采用典型的前后端不分离部署、但前后端职责分离的多页面架构。

浏览器
  │
  ├── H5 移动端:底部导航、移动页面
  ├── PC 桌面端:侧边栏、顶部导航、桌面页面
  │
  ▼
Node.js + Express 后端
  │
  ├── 认证与权限中间件
  ├── 业务路由
  ├── 数据库访问层
  ├── 定时任务
  ├── 文件上传
  └── AI 服务适配层
  │
  ▼
MySQL 数据库

PC 端和 H5 端虽然共享同一套后端 API,但页面结构、导航方式、样式和交互脚本相互隔离。PC 端采用侧边栏加顶栏的桌面布局,适合展示账户卡片、报表图表和管理功能;移动端采用底部导航和单列流式布局,强调快速记账和随手查看。

这种架构有两种明显优势。

首先,两套界面可以根据设备特点分别优化。桌面端可以并排展示预算进度、支出排行和近期记录,移动端则更适合“点击悬浮按钮、快速记账、立即返回”的短流程。若强行共用同一套响应式布局,组件很容易因为同时兼容键盘、鼠标、触摸和大屏而变得繁琐。

其次,业务规则不会因端侧差异而重复。无论用户在 PC 还是手机上发起转账,账户锁定、余额变更和审计流水都由后端统一处理。这样可以避免不同端出现计算口径不一致的问题。

不过,两套独立页面也会带来维护成本。公共交互组件、请求封装、弹窗和通知逻辑需要在两个端分别维护;如果后期缺少统一规范,可能出现同一功能在两套页面中表现不一致的情况。因此,更合理的做法不是机械地共享所有代码,而是抽取请求层、校验规则、状态文案和通用样式变量,同时保留界面布局与交互的独立性。

三、数据模型:围绕账本建立数据边界

记账系统的核心并不是“账单”一个实体,而是用户、账本、账户、分类、账单、借贷和通知等多个实体之间的协作。

可以简化为核心实体关系:

用户
  │
  ├── 账本成员
  │
账本
  ├── 账户
  ├── 分类
  ├── 账单
  ├── 借贷
  ├── 预算
  └── 成员权限

其中,账本是最重要的数据边界。家庭账本、旅行账本和个人账本应相互隔离;一个用户可以通过成员关系加入多个账本,同一个账本也可以拥有多个成员。

项目采用逻辑删除字段 del_flag,而不是物理删除业务数据。这种设计在记账和财务场景中挺重要:已删除的账单仍然保留在数据库中,管理员可以在必要时恢复数据、检查操作历史或排查对账问题。

3.1 账户与账单的关系

账户可以分为现金、微信、支付宝、银行卡、信用卡、投资理财等类型。账单则至少包含以下信息:

  • 收支类型:收入、支出或转账;
  • 金额与方向;
  • 所属账本;
  • 记账账户;
  • 目标账户,用于转账场景;
  • 一、二级分类;
  • 交易日期;
  • 商家和备注;
  • 图片凭证;
  • 外部账单来源与唯一编号。

转账与普通收支的区别是:普通收支通常只影响一个账户,而转账同时影响转出账户和转入账户。后端务必把它当作一个整体事务,不能先更新账户 A,再在网络中断或程序异常后留下未更新的账户 B。

3.2 分类体系

项目预置了多组一级和二级分类。一级分类负责宏观统计,例如餐饮、交通和居住;二级分类负责细化场景,例如餐饮可以继续分为早餐、午餐、晚餐、外卖和奶茶咖啡。

系统需要处理好三件事:

1. 预置分类与用户自定义分类共存;
2. 用户删除分类后,历史账单仍然可以显示原分类名称;
3. 旧数据中的分类结构升级后可以平滑迁移。

这意味着分类不能被简单删除,至少应在业务层区分“停用”和“物理删除”。历史账单保存的该是分类快照或稳定标识,不能因为用户修改了当前分类名称,就改变过去的统计结果。

四、并发记账:金融场景务必解决的一致性问题

转账接口是记账系统中最典型的一致性场景。假设账户 A 有 1000 元,账户 B 有 500 元,两个并发请求分别执行:

  • 从 A 向 B 转账 200 元;
  • 从 B 向 A 转账 100 元。

如果程序先查询余额,再在应用层计算,最后执行更新,就可能出现两个请求同时读到旧余额,最终金额取决于哪一次写入后发生。即使金额计算在单个 Node.js 进程中没有出现竞态,只要未来增加集群、定时任务或批量导入,风险仍然存在。

项目使用数据库事务和 SELECT ... FOR UPDATE 锁定账户记录,这是一个合理的基础方案。其思路是:

1. 开启数据库事务;
2. 按固定顺序锁定相关账户;
3. 查询当前余额;
4. 校验余额和账户状态;
5. 更新账户余额;
6. 写入账单和审计流水;
7. 提交事务。

多账户按 ID 升序锁定,可以降低两个事务以相反顺序申请锁而形成死锁的概率。MySQL 官方也将事务、行锁和一致性访问列为 InnoDB 表类型的重要特性。

4.1 建议用的事务伪代码

START TRANSACTION;

SELECT balance
FROM sl_biz_account
WHERE id = 1001
FOR UPDATE;

SELECT balance
FROM sl_biz_account
WHERE id = 2002
FOR UPDATE;

UPDATE sl_biz_account
SET balance = balance - 200
WHERE id = 1001;

UPDATE sl_biz_account
SET balance = balance + 200
WHERE id = 2002;

COMMIT;

在实际应用中,不能只执行“余额加金额”,还应保存变化前金额、变化金额、变化后金额和操作人。这样,即使某次余额异常,也能通过审计流水追溯原因。

4.2 事务设计原则

第一,事务应尽量短。文件上传、调用 AI、发送通知等操作不应放在数据库事务内部。这些外部操作可能耗时较长,会延长锁持有时间,并增加死锁概率。

第二,先验证再锁账。对参数、权限和账本归属的检查应提前做好,只有真正需要修改余额时才开启锁和事务。

第三,转账方向要统一。建议明确“正数表示流入、负数表示流出”,由统一函数计算账户金额变化,避免不同接口采用不同符号规则。

第四,失败务必整体回滚。账单写入成功后如果审计流水失败,系统不能继续提交事务;否则余额正确,审计记录却会丢失。

第五,重试要有边界。发生死锁或锁等待超时后,可以结合指数退避有限重试,但不能把外部 AI 请求、短信发送等非事务操作重复执行。

五、余额审计:让每一笔变化都可以追溯

记账系统不仅要了解账户当前有多少钱,还要回答“为什么变成这个金额”。

项目为每次余额变化记录流水,包括收入、支出、转账、期初金额和手动调整。完整的审计记录通常包含:

  • 账户编号;
  • 关联账单或借贷编号;
  • 操作类型;
  • 变化前金额;
  • 变化金额;
  • 变化后金额;
  • 操作人;
  • 操作时间;
  • 账本编号。

这种设计与银行流水类似。当前余额是账户的“状态”,流水则是账户变化的“事实”。如果账户余额与流水合计不一致,系统可以自动扫描并生成对账异常。

5.1 状态与事实要分开

一个常见错误是让账单表同时承担余额计算职责。例如,每次查询账户余额时都汇总所有账单。这样做虽然实现简单,但随着账单增加,性能会持续下降,而且难以处理期初余额、人工调整和撤销操作。

更好的方式是:

  • 账户表保存当前余额;
  • 审计流水保存每次变化;
  • 账单表保存交易事实;
  • 定时任务校验三者的关系。
查询余额时直接读取账户表,对账时再通过审计流水进行校验。这样做兼顾性能与可追踪性。

六、账单导入:真实数据往往比接口更复杂

微信和支付宝账单导入是记账系统最有价值、也最容易出错的功能之一。它处理的不是结构稳定的内部 API,而是用户从第三方应用导出的 CSV、TXT 或 Excel 文件。

这些文件可能存在:

  • UTF-8 或 GBK 编码;
  • 文件头说明和汇总行;
  • 金额中的逗号;
  • 支出与收入符号差异;
  • Excel 日期序列号;
  • 合并单元格;
  • 交易状态字段;
  • 信用卡还款、充值提现等中性交易;
  • 相同账单重复导出。
因此,账单导入不能简单使用 split(',') 解析,也不能直接把第一行当作表头。项目通过独立解析模块处理编码、日期和文件格式,并在导入前展示预览结果。

6.1 导入流程

完整的导入流程可以分成六步:

1. 上传原始文件;
2. 检测编码并转换为统一文本;
3. 跳过说明行、汇总行和空行;
4. 识别表头与字段映射;
5. 逐行解析交易;
6. 返回可导入记录、跳过记录和分类建议。

用户确认后再执行第二步落库:

1. 开启数据库事务;
2. 根据交易单号判断是否已经导入;
3. 创建账单记录;
4. 更新相关账户余额;
5. 写入余额审计流水;
6. 保存导入批次;
7. 提交事务。

通过交易单号进行幂等判断,可以避免用户重复上传同一份文件时产生重复账单。

6.2 分类匹配不能影响用户确认

系统可以根据交易对方、商品名称和金额自动匹配分类,但自动匹配只能作为建议。最终分类和排除规则必须经过用户确认,因为“美团”可能对应餐饮、超市或电影,“转账”也可能属于人情、还款或账户内部划转。

如果文件过大,还该引入批次、进度、失败原因和继续导入机制。导入失败不能只提示“处理失败”,而应明确告诉用户:第几行金额格式错误、哪个字段为空、哪条记录已经导入。

七、AI 能力:把不确定性的模型放在业务边界之外

Smart Ledger 的 AI 功能不是简单地把用户输入发送给大模型,而是把 AI 作为业务辅助层。

项目接入了文本和视觉模型,用于自然语言记账、小票识别、消费分析、异常检测、月末预测、预算建议用和饮食建议。不同能力承担的职责并不相同。

AI 能力

输入

核心输出

业务风险

自然语言记账

文本

类型、金额、分类、账户、日期和备注

字段解析错误

小票识别

图片

商户、金额和日期

图片模糊或模型幻觉

账单分析

统计后的账单数据

消费结构与建议

过度解读数据

异常检测

两个周期统计数据

增幅与异常分类

周期数据不足

月末预测

当前支出和天数

月末支出或结余预测

线性假设失真

预算建议用

历史分类数据

分类预算建议

特殊月份干扰

7.1 自然语言记账

用户输入“今天午餐外卖30元”后,后端需要将其转换为结构化字段:

{
  "type": "expense",
  "amount": 30,
  "category": "餐饮",
  "subCategory": "外卖",
  "account": "微信钱包",
  "date": "2026-10-22",
  "remark": "午餐外卖"
}

但 AI 输出不能直接写入数据库。后端仍要做好:

  • 校验金额是否为正数;
  • 判断账单类型是否合法;
  • 校验账户是否属于当前账本;
  • 校验分类是否存在;
  • 对日期和时区进行标准化;
  • 对低置信度结果要求用户确认。
AI 负责搞懂语义,业务规则负责保证安全。即使模型没有识别出金额,也不能让应用层默认填入 0 或直接报错,而应提示用户补充。

7.2 OCR 小票识别

小票识别比文本记账更复杂,因为图片可能模糊、倾斜、缺角或包含多件商品。系统可以先让视觉模型提取总金额和商户,再由用户确认后生成账单。

金额属于高风险字段。即使 OCR 准确率较高,也应优先使用小票中的“合计”或“应收金额”,并对折扣、服务费和支付金额进行区分。OCR 结果只应作为表单预填,不应绕过人工确认直接记账。

7.3 AI 分析不是实时统计

账单分析、异常检测、预测和预算推荐都依赖聚合数据。正确顺序是先在数据库中进行 SQL 聚合,再把少量统计结果发送给模型。

不应把成千上万条原始账单直接发送给大模型。这样做会浪费令牌、暴露用户隐私,并降低响应速度。更合理的数据结构是:

{
  "period": "2026-10",
  "income": 12000,
  "expense": 6800,
  "categories": [
    {"name": "餐饮", "amount": 1800},
    {"name": "交通", "amount": 600}
  ],
  "dailyAverage": 220,
  "daysPassed": 22,
  "totalDays": 31
}

模型只负责解释和提出建议,收支总额、分类占比等准确数字仍应由数据库计算。

八、定时任务与通知:提醒系统也要避免重复

借贷和信用卡提醒是典型的时间触发业务。系统需要在每天固定时间检查即将到期、已经逾期的记录,然后为相关用户生成通知。

项目采用个人通知与系统公告分离的设计:

  • 系统公告面向全部或指定用户;
  • 个人通知仅对接收人可见;
  • 还款提醒根据到期日自动生成;
  • 服务启动后可以补检漏掉的任务;
  • 通知使用唯一索引防止重复。
这种设计的关键是“检查任务”与“用户通知”分离。定时任务只负责发现符合条件的借贷和信用卡记录,不直接承担消息展示逻辑;通知表负责记录接收人、关联业务、是否已读和通知类型。

8.1 防止重复提醒

如果服务在 09:00 前重启,或者定时任务重复执行,系统可能生成多条相同提醒。解决方式是在通知表中建立业务唯一键:

用户编号 + 业务类型 + 业务编号 + 提醒日期

例如,同一笔借贷在 2026-10-22 的“到期提醒”只能生成一次。即使定时任务多次执行,数据库也会通过唯一索引拒绝重复记录。

8.2 提醒规则要可解释

到期前三天、当天和逾期通知应生成不同的消息模板。例如:

  • 距离还款日还有 3 天;
  • 今天为还款日;
  • 已经逾期,请尽快处理。
用户点击通知后,应跳转到对应的借贷或信用卡详情,而不能只展示一条无法关联业务的文字。

九、前端设计:为什么原生技术也能支撑复杂交互

项目前端没有使用主流框架,而是采用原生 JavaScript 实现页面。其核心优势是部署简单、运行环境透明、没有构建链依赖;缺点则是状态管理、组件复用和跨页面通信需要自行约束。

在记账场景中,页面可以围绕业务拆分:

  • login.html:登录和注册;
  • index.html:首页概览;
  • bills.html:账单列表和日历;
  • accounts.html:账户管理;
  • reports.html:统计报表;
  • loans.html:借贷中心;
  • profile.html:个人中心和系统设置。
每张页面维护自己的 HTML、CSS 和 JavaScript,可以减少单文件膨胀,也便于搞懂页面边界。

9.1 请求层需要统一封装

即使不使用框架,也应统一处理:

  • 接口地址;
  • 请求头;
  • Token 注入;
  • 登录失效;
  • 业务错误码;
  • 网络异常;
  • 加载状态;
  • 参数序列化。
统一封装可以避免每个页面重复编写 Token 读取和错误提示。

9.2 弹窗与表单校验

记账弹窗、分类选择、筛选条件和确认删除都属于高频交互。建议建立统一的弹窗组件和表单校验函数,至少支持:

  • 必填校验;
  • 金额格式校验;
  • 日期范围校验;
  • 账户归属校验;
  • 分类是否存在;
  • 操作确认;
  • 键盘与点击关闭。
移动端要格外注意点击区域、软键盘弹起和滚动穿透;PC 端则应支持键盘操作、表单回车和焦点管理。

十、Canvas 图表:手写绘制也要兼顾清晰度与性能

项目使用 Canvas 绘制饼图、折线图、柱状图和趋势图,而没有引入图表库。这种实现可以减少依赖,并根据记账业务定制交互细节,但需要自行处理响应式、高清显示和坐标计算。

在高分屏上,Canvas 模糊的常见原因是绘图缓冲区尺寸与 CSS 显示尺寸不一致。正确做法是按照设备像素比放大画布,再对绘图上下文进行缩放。

function resizeCanvas(canvas) {
  const dpr = window.devicePixelRatio || 1;
  const rect = canvas.getBoundingClientRect();

  canvas.width = Math.round(rect.width * dpr);
  canvas.height = Math.round(rect.height * dpr);

  const ctx = canvas.getContext('2d');
  ctx.scale(dpr, dpr);

  return ctx;
}

报表页面还需要处理:

  • 窗口尺寸变化;
  • 深色模式切换;
  • 数值为 0 时的空状态;
  • 分类过多时的聚合;
  • 日期不足一个完整周期;
  • 图表与统计数字口径一致;
  • 长分类名称截断;
  • 高分屏下的内存占用。
当分类数量较多时,不应把几十个二级分类同时展示在饼图中,而应先聚合到一级分类,再提供点击展开或明细列表。

十一、语音记账:能力增强不能替代手动输入

语音记账的流程是:

1. 浏览器获取麦克风权限;
2. 将语音转换为文字;
3. 对文字进行自然语言解析;
4. 预填记账表单;
5. 用户确认后保存。

Web Speech API 的浏览器支持并不均衡。Chrome 和 Edge 的支持相对较好,Firefox 默认未启用,iOS Safari 则对 HTTPS 和版本存在要求。 因此,语音只能作为快捷入口,不能成为唯一记账方式。

系统应在页面加载时进行能力检测:

const SpeechRecognition =
  window.SpeechRecognition ||
  window.webkitSpeechRecognition;

if (!SpeechRecognition) {
  disableVoiceButton('当前浏览器不支持语音识别');
}

同时提供清晰的状态提示:

  • 等待说话;
  • 正在识别;
  • 识别结果;
  • 网络异常;
  • 用户取消;
  • 浏览器不支持。
识别失败时,用户可以继续手动输入或点击分类快速记账。语音转文字与 AI 解析都应设置超时时间,避免请求长时间占用界面。

十二、权限与数据安全:账本不是简单的用户数据

多人协同使权限模型从“用户拥有数据”变成“用户通过成员关系访问账本”。

系统至少需要区分:

  • 账本所有者;
  • 编辑者;
  • 查看者。
不同角色对应不同能力:

角色

查看账单

新建账单

修改账单

删除账单

管理成员

所有者

是

是

是

是

是

编辑者

是

是

是

视规则

否

查看者

是

否

否

否

否

所有账单、账户、借贷和预算查询都必须先校验当前用户是否属于目标账本。不能只验证用户是否登录,否则可能出现水平越权。

密码应使用 bcrypt 等慢哈希算法处理,JWT 只用于无状态认证。生产环境还应:

  • 使用 HTTPS;
  • 设置合理的 Token 有效期;
  • 对登录失败进行限流;
  • 校验文件上传类型和大小;
  • 防止 SQL 注入;
  • 对敏感配置使用环境变量;
  • 记录关键操作和异常请求。
AI 接口尤其需要限流。自然语言解析和小票识别会调用外部模型,如果缺少频率限制,容易被恶意请求消耗 API 额度。

十三、工程化与部署:让系统可以持续运行

项目通过环境变量区分开发和生产配置,使用 PM2 管理 Node.js 进程,并可配合 Nginx 进行反向代理。对于个人项目而言,这是一套成本低、可维护性较高的部署方案。

部署时需要重点关注:

1. 数据库定时备份;
2. 上传文件异地备份;
3. 环境变量与代码分离;
4. 日志自动轮转;
5. 服务异常重启;
6. 数据库迁移版本管理;
7. 接口请求日志;
8. 定时任务执行记录;
9. 上传目录权限;
10. 安全更新。

13.1 数据库迁移

项目将初始化脚本和增量脚本分开管理,这是一种值得坚持的实践。全新安装可以执行初始化脚本,已有系统则按日期顺序执行增量脚本。

升级前应备份数据库,升级后要检查:

  • 表结构是否创建成功;
  • 索引是否正常;
  • 默认值是否生效;
  • 历史数据是否兼容;
  • 上传文件路径是否需要迁移。
数据库升级不能只执行 SQL,还应验证业务接口。例如新增“外部账单编号”字段后,需要测试导入重复检测;新增信用卡账单日字段后,需要测试提醒任务。

13.2 日志要同时服务运维和审计

后端日志至少应区分:

  • 请求日志;
  • 业务日志;
  • 数据库错误;
  • AI 调用日志;
  • 文件导入日志;
  • 登录日志;
  • 定时任务日志。
日志中不能记录完整密码、Token 或 API Key。涉及金额的操作应记录操作人、账本、业务编号和结果,但敏感字段需要脱敏。

十四、可观测性与测试:家庭项目也需要质量底线

开源项目初期通常以功能实现为主,但随着用户增加,缺乏测试和日志会显著增加维护成本。

至少需要补充以下测试:

14.1 接口测试

  • 正常创建收支记录;
  • 同一账户重复转账;
  • 余额不足;
  • 跨账本操作;
  • 非账本成员访问;
  • 删除分类;
  • 修改已结算借贷;
  • 重复导入账单;
  • AI 返回异常字段;
  • 文件编码异常。

14.2 并发测试

可以使用多个并发请求模拟同一账户同时记账。重点观察:

  • 最终余额是否正确;
  • 是否出现死锁;
  • 是否生成重复流水;
  • 失败请求是否回滚;
  • 审计流水是否与余额一致。
并发测试不需要复杂压测工具。只要构造几十个并发请求,就足以暴露“先查后改”和事务边界错误。

14.3 业务监控

建议监控:

  • 登录失败次数;
  • AI 调用成功率;
  • 平均响应时间;
  • 账单导入失败率;
  • 定时任务最后执行时间;
  • 数据库慢查询;
  • 磁盘剩余空间;
  • 上传目录增长情况。
个人项目虽然不需要企业级可观测平台,但至少要能收到服务异常和定时任务失败的提醒。

十五、后续演进:从可用走向可维护

如果该项目继续演进,可以重点考虑以下几个方向。

15.1 抽取共享前端能力

PC 和移动端可以继续保持布局独立,但请求封装、错误码、日期工具、金额格式、权限判断和主题变量可以抽取为共享模块。这样既能保持端侧体验差异,又能减少重复逻辑。

15.2 引入领域服务层

目前后端路由承担了较多职责。后续可以将代码拆分为:

  • 控制器:处理 HTTP;
  • 服务层:编写业务规则;
  • 仓储层:访问数据库;
  • 领域模型:表达账户、账单和账本;
  • 任务层:处理导入和提醒;
  • 适配器:对接 AI 服务。
例如,转账、余额调整和审计流水应放入统一的账户服务,避免多个路由分别实现金额计算。

15.3 支持可插拔 AI 提供商

当前 AI 配置已经支持模型地址、文本模型和视觉模型。下一步可以进一步定义统一的 AI 请求和响应结构,让系统同时适配不同模型供应商。

业务层只关心:

parseBill(text): BillDraft
recognizeReceipt(image): ReceiptResult
analyzeReport(data): AnalysisResult

底层模型可以替换。这样既能降低供应商锁定,也方便用户使用自有模型或本地服务。

15.4 强化数据迁移能力

账单导入可以增加:

  • 字段映射配置;
  • 分类规则管理;
  • 导入批次回滚;
  • 失败记录重试;
  • 大额交易二次确认;
  • 导入前后余额校验。
对于家庭用户来说,导入结果比“支持多少种格式”更重要。用户必须可以清楚看到哪些记录被导入、哪些被跳过、哪些发生错误。

15.5 增加端到端测试

PC 和移动端页面可以采用不同的测试策略。移动端重点测试触摸、弹窗和快速记账,PC 端重点测试筛选、表格和报表。核心转账、账单导入和提醒任务则适合使用接口自动化测试长期守护。

十六、总结

Smart Ledger 的价值不仅在于提供了一套开源记账应用,更在于它展示了个人项目如何逐步面对真实软件工程问题。

从技术上看,它用相对朴素的技术栈覆盖了复杂业务:

  • 使用数据库事务和行级锁保证账户一致性;
  • 使用审计流水让余额变化可追溯;
  • 使用账本和成员关系实现多人协同;
  • 使用统一 API 支撑 PC 和移动端;
  • 使用预览与幂等机制提高账单导入可靠性;
  • 将 AI 置于业务边界之外,使其负责搞懂与建议;
  • 使用定时任务和个人通知建立提醒体系;
  • 通过环境变量、PM2 和反向代理支持自托管部署。
这个项目也说明,框架并不是衡量工程质量的唯一标准。原生 JavaScript 同样可以构建完整应用,但前提是开发者主动补足状态管理、组件复用、测试、日志和部署能力。

记账系统最核心的原则始终是:界面可以简单,交互可以快捷,但金额必须准确、权限必须清晰、操作必须可追踪。只要守住这三条底线,再逐步增加 AI、协同和数据分析,系统才会在功能变多的同时保持可信和可持续维护。


这篇笔记就先到这里,后面用到新的思路或者发现有问题再补充。

评论 (0)

暂无评论