目 录CONTENT

文章目录

记账软件

懿曲折扇情
2026-10-25 / 0 评论 / 0 点赞 / 3 阅读 / 3,236 字
温馨提示:
本文最后更新于 2026-10-25,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。
广告 广告

慧记账 SmartLedger:从 0 到 1 打造一款「能上线」的记账应用

一篇关于记账应用 慧记账 SmartLedger 的设计原理、架构连接、功能拆解与安全实现的工程博客。
它不是一份需求文档,而是把一个真实跑在 Vercel 上、打包成安卓 APK、并已生成 iOS 工程的应用,从数据模型讲到部署分发的完整复盘。


一、为什么做这个

市面上的记账软件很多——随手记、鲨鱼记账、一木记账、钱迹……它们各有所长,但也各有痛点:广告多、核心功能藏在付费墙后、数据封闭、隐私不透明。

我们想做一款「功能堆满、但每一处都真能用」的记账应用,定位是:美观权威、功能越全越好、隐私安全、能上线分发。于是有了慧记账 SmartLedger。

image


二、技术栈一览

层次 选型 用途
框架 React 19 + TypeScript 5.9 应用主体、类型安全
构建 Vite 8 开发与打包
样式 Tailwind CSS 4 + shadcn/ui(55 组件) 组件化、主题化
路由 react-router-dom 7 页面导航与路由守卫
状态 React Context + useReducer 风格不可变更新 全局账本状态
图表 recharts / echarts 报表、热力图、趋势图
日期 date-fns 周期计算、顺延、月份序列
动效 framer-motion / gsap 页面与交互动画
通知 sonner Toast 提示
表单 react-hook-form + zod 表单校验
原生封装 Capacitor 8(android / ios) 打包安卓 APK、生成 iOS 工程
部署 Vercel(SPA)/ 妙搭托管 线上访问

关键点:纯前端 + 浏览器本地存储(localStorage),无服务端运行时。因此整条数据链路、登录、安全都在浏览器内完成,能直接跑、能上线,代价是数据不跨设备自动同步(见文末边界说明)。


三、架构与连接:模块是怎么串起来的

3.1 Provider 嵌套(顶层连接关系)

应用入口 app.tsx 用一组 Provider 把各能力「插」成一颗树,顺序有讲究:

<AuthProvider>            // ① 登录态:决定你是谁
  <SecurityProvider>      // ② PIN 应用锁 + 隐私模式
    <Routes>
      <Route path="/login" element={<LoginPage />} />
      <Route element={<RequireAuth />}>   // ③ 路由守卫:未登录跳回登录页
        <LedgerProvider>                  // ④ 账本数据:按账号加载/持久化
          <ReminderProvider>              // ⑤ 记账提醒轮询
            <AddTxProvider>               // ⑥ 全局「记一笔」弹窗
              <Layout />                  // 侧栏 + 页面出口
            </AddTxProvider>
          </ReminderProvider>
        </LedgerProvider>
      </Route>
    </Routes>
  </SecurityProvider>
</AuthProvider>

连接逻辑:

  • 认证在最外层——任何模块都依赖「当前登录的是谁」,LedgerProvider 用 useAuth() 拿到 user.username 来决定数据存哪把锁。
  • 安全锁在路由之前——应用锁、隐私模式要挡在页面渲染前。
  • 数据层在登录之后——RequireAuth 保证只有登录用户才进入账本相关 Provider。
  • 共享状态通过 Context 单向流动:AuthContext → LedgerContext → 各页面。

3.2 数据流(一次记一笔,发生了什么)

用户在「记一笔」弹窗填写
   ↓
AddTxProvider 调用 ledger.addTransaction(tx)
   ↓
LedgerProvider.setState(不可变更新 activeLedger 的 transactions 数组)
   ↓
useEffect 监听 state 变化 → saveState(写入 localStorage)
   ↓
所有 useLedger() 的页面重新渲染(首页结余、报表、账户余额联动刷新)

核心:一份全局状态,驱动全部页面。增删改任何一笔,首页结余、账户余额、预算进度、报表、热力图全部同步重算,因为都从同一个 state 派生。


四、数据模型:记账应用的心脏

4.1 实体类型

实体 字段要点 作用
ITransaction type(支出/收入/转账)、amount、categoryId、accountId、date、note、tags、reimbursable/reimbursed 每一笔流水
IAccount name、type(现金/储蓄卡/信用卡/支付宝/微信/投资理财/虚拟)、baseBalance、color 账户与初始余额
ICategory name、type、color、icon 收支分类
IBudget categoryId(null=总预算)、month、limit 月度预算
IDebt person、type(lend=借出/borrow=欠款)、amount、settled 债务
ISubscription name、amount、cycle(月/年)、nextDate、autoBook 周期性订阅

4.2 状态顶层结构

interface ILedgerState {
  ledgers: Record<string, ILedgerData>; // 多账本
  activeLedgerId: string;
  ledgerMeta: ILedgerMeta[];
  categories: ICategory[];
  profile: IProfile;       // 主题/币种/会员/隐私/提醒
  debts: IDebt[];
  subscriptions: ISubscription[];
}

设计要点:

  • 多账本用 Record<string, ILedgerData> 表达,每个账本内部自含 transactions/accounts/budgets,互不干扰;activeLedgerId 决定当前操作哪个账本。
  • ILedgerData(账务)+ debts/subscriptions(跨账本)分层,既满足「每本账独立」,又支持「债务/订阅全局可见」。

4.3 种子与空账本

  • buildSeedState():返回 17 笔演示流水、5 个账户、3 个预算、2 笔债务、2 个订阅,用于演示。
  • buildEmptyState():全零空账本,内置授权账号 jasper 首次登录走这条——从零开始记账,无任何演示数据干扰。
  • migrateState(parsed):版本迁移器。旧版本是「单账本」扁平结构,新版是多账本。加载时若发现旧结构,自动包装成默认账本并保留原流水/账户/预算,保证老用户数据不丢。

五、持久化:数据存在哪、怎么隔离

5.1 localStorage 命名空间

Key 内容
ledger-app:auth 登录态、失败计数与锁定时间
ledger-app:security PIN 锁设置
ledger-app:state:<username> 按账号隔离的账本数据(如 ledger-app:state:jasper)

5.2 加载策略(关键:账号隔离 + 兼容迁移)

function loadState(key) {
  const tryLoad = (k) => { /* 读 + JSON.parse + migrateState */ };
  if (key === 'ledger-app:state:jasper') return tryLoad(key) ?? buildEmptyState();
  return tryLoad(key) ?? tryLoad('ledger-app:state') ?? buildSeedState();
}
  • 每个账号的数据锁在自己的 key 里,不同账号互不可见。
  • 内置授权账号从零开始(不回退旧全局演示数据)。
  • 每次任何状态变更,useEffect 自动写回 localStorage —— 改了就存,刷新不丢。

边界:这些数据只在当前这台设备、这个浏览器里。换浏览器/换设备就是独立存储;跨设备自动同步需要云后端(见文末)。


六、统计原理:所有数字怎么算出来的

统计全部收敛在 stats.ts 的纯函数里(不碰 UI、不写状态,可测试):

  • monthBalance(txns, key):本月结余 = 收入 − 支出(按月 key 过滤)。
  • monthSeries(txns, keys):最近 N 个月的收入/支出/结余序列 → 喂给折线图。
  • categoryBreakdown(...):分类汇总 → 饼图/排行。
  • accountBalance(account, txns):账户余额 = 初始余额 + 收入 − 支出 ± 转账(转出减、转入加)。
  • totalAsset(accounts, txns):总资产 = 所有账户余额之和。
  • budgetSpent(...):预算已花 = 该分类当月支出(总预算则不计分类)。

「名下净资产」卡片 = 总资产 + 借出 − 欠款,由 totalAsset 与债务联动得出——改动任一笔,净资产实时重算。


七、安全设计:密码、PIN、隐私、防爆破

用户要求「安全」,这里做得比较扎实:

7.1 密码:PBKDF2-SHA256 + 随机盐,明文不落盘

const ITERATIONS = 100000; // 10 万次迭代
async function derive(password, salt) {
  const key = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', ...);
  return crypto.subtle.deriveBits({ name: 'PBKDF2', salt, iterations: ITERATIONS, hash: 'SHA-256' }, key, 256);
}
  • 存储的是 salt + hash(16 字节随机盐 + 32 字节散列),任何地方都不存明文密码。
  • 内置授权账号 jasper 的散列由 pbkdf2Sync(100000, SHA-256, 32B) 预置在代码里,登录时用同一算法 verifyPassword 比对。

7.2 单授权账号模式

  • register 已禁用(返回「本应用为授权账号专属,不支持自助注册」)。
  • 登录只认 jasper,其他用户名统一返回「账号不存在或无权登录」——不泄露账号是否存在,避免被探测。
  • 路由 /register 与注册页已删除,登录页标注「仅预置账号可登录」。

7.3 防爆破锁定

连续失败 5 次,锁定 30 秒(count >= 5 → lockUntil = now + 30_000),期间即使密码正确也拒绝。

7.4 PIN 应用锁 + 隐私模式

  • PIN 应用锁:为应用再加一道锁(独立于密码)。
  • 隐私模式:开启后全站金额显示为 •••,防旁人偷看。

八、功能清单(对标竞品,越全越好)

模块 功能
首页 本月结余、收支、预算进度、总资产、最近账单
账单 增删改、支出/收入/转账、报销标记、分类、标签
账户 多类型账户、初始余额、名下净资产、资产构成占比
多账本 多本独立账本、切换、增删
预算 月度总预算 + 分类预算、超支预警
报表 月度折线/饼图、日历热力(月)、年度热力矩阵(365 天 GitHub 风格)、同比洞察、资产趋势
年报 全年收支/结余率/记账天数/12 月柱状图/年度排行/年度关键词
债务 借出/欠款/结清,联动净资产
订阅 月/年周期、7 天待扣提醒、到期自动记一笔并顺延、已续费
会员 免费版 / 专业版分层权益
安全中心 PIN 锁、自动锁定、隐私模式、完整 JSON 备份/恢复、CSV 导出(专业版)
设置 主题(深浅)、币种、跨设备同步(导出/导入)、清空账本数据、记账提醒、数据存储说明

8.1 亮点机制:周期订阅「自动入账」

LedgerProvider 挂载时检查所有开启 autoBook 的订阅:

if (due <= now) {
  newTxns.unshift(支出账单);
  nextDate = cycle === 'monthly' ? addMonths(due, 1) : addYears(due, 1);
}

到期自动生成一笔支出并顺延下次扣款日期,无需手动——这是「一木记账/钱迹」一类竞品的核心体验。


九、分发与上线:网页、APK、iOS 是怎么连起来的

9.1 双部署链路(同一份代码)

  • Vercel:vite.config.ts 用 isVercel=!!process.env.VERCEL 切分支,VERCEL=1 时产出标准 SPA(dist/),vercel.json 做 SPA 回退(除静态资源外全部 rewrite 到 index.html)。稳定线上:https://ledger-app-lilac-kappa.vercel.app。
  • 妙搭托管:走 miaodaOutputPlugin,产出 dist/output(同源资源)+ dist/output_resource(CDN 资源),另生成 routes.json。

9.2 Capacitor 打包原生应用

npm i @capacitor/core @capacitor/cli @capacitor/android @capacitor/ios
capacitor.config.ts: { appId:'com.smartledger.app', appName:'慧记账 SmartLedger', webDir:'dist', server:{ androidScheme:'https' } }
VERCEL=1 npm run build   → 产出标准 SPA 到 dist
npx cap add android / ios → 生成原生工程
npx cap sync             → 把 web 产物拷入原生 assets
./gradlew assembleDebug  → 产出 app-debug.apk
  • 安卓:已产出 app-debug.apk(约 4.3MB,debug 签名可直接侧载安装,Android 7.0+),web 界面完整打入。
  • iOS:已生成 ios/App/App.xcodeproj(Xcode 工程,包名 com.smartledger.app)。真机安装 .ipa 需要 Apple 开发者账号签名(年费约 ¥688),工程就绪,只差签名一步。

9.3 PWA:无需安装包即可「添加主屏幕」

  • public/manifest.webmanifest + service worker + 图标,PROD 下注册(Capacitor 原生壳内自动跳过)。
  • 安卓 Chrome / iPhone Safari「添加到主屏幕」即生成全屏应用图标。

十、诚实的边界与下一步

当前已知边界(如实说明):

  • 数据存在浏览器/设备本地,换设备不自动互通。
  • 会员、PIN、到期自动记账均为本地逻辑,无真实支付/推送。
  • iOS 真机安装需付费开发者账号签名。

下一步(可做):

  • 接入云端后端(如 Vercel KV / Supabase)实现跨设备自动云同步,按 jasper 账号隔离——安卓记的账,苹果也能完整看到。
  • 拿到 Apple 开发者账号后,用现有 iOS 工程签名发布到 App Store / TestFlight。

写于 2026-10 · 慧记账 SmartLedger 工程:/Users/apple/code/doubao_jizhang/ledger-app

0

评论区