项目实战Tally项目PromptPrompt v1.1

Cent 记账应用 – 开发 Prompt v1.1(含 CRDT 冲突避免)

一、项目定位

  • 名称:Cent(概念性记账工具)

  • 核心理念:用标签系统完全替代传统资产管理,提供高度灵活的记账体验;数据由用户通过 GitHub 仓库自托管,支持多端(手机 + 电脑)使用。

  • 目标用户:喜欢自定义分类、关注预算控制、注重数据隐私和长期可维护性的个人用户。

二、核心功能需求

1. 记账基础

  • 记录支出收入,字段包括:金额、类型、日期、备注、标签(可多选)、币种。

  • 支持按标签自动切换偏好币种(如选中“海外银行卡C”时,记账金额币种自动变为 USD)。

  • 支持转账场景:通过记录两笔带“不计收支”标签的交易实现,不干扰净收支统计。

2. 标签系统(替代资产管理)

  • 平铺标签 + 标签组

    • 标签组可定义:是否单选、是否必选、默认标签、显示顺序。

    • 例如:“资产”标签组(单选、必选,默认“银行卡A”)、“消费类别”标签组(多选、非必选)。

  • 通过筛选器实现资产管理

    • 用户可创建筛选器(如“银行卡A”),条件为 tags 包含 银行卡A,即可统计该资产的收支与结余。

    • 筛选器支持设置币种、排除标签(如“不计收支”)。

  • 多币种支持:每笔交易记录原始币种和金额;统计时可选择按历史汇率换算或直接显示原币种。

3. 预算模块

  • 总预算 + 子预算(按分类标签):

    • 可设置预算周期(月/年/自定义)、总金额、子预算(如餐饮1500、娱乐800)。

    • 支持排除标签(“逃生舱”),如“意外支出”不参与预算计算。

  • 首页进度条

    • 灰色背景;黄色 = 今日支出占比;绿色/红色 = 截止今日累计支出占比(超支变红);黑色竖线 = 理论时间进度对应的支出位置。

    • 理论进度 = (已过天数 / 周期总天数) × 总预算。

  • 历史达成视图

    • 每个预算周期显示两个圆点:总预算状态(绿=未超,红=超)、子预算状态(绿=所有子项未超,红=任一子项超)。

4. 页面结构(五个核心页面)

页面功能描述
首页(账单流)顶部今日支出快捷板块;预算卡片(进度条+预警);按时间倒序的账单列表。
记账页大号加号按钮进入;记录支出/收入;标签分组选择器;金额、日期、备注、币种。
搜索页多条件筛选:标签、时间范围、金额区间、类型;支持保存为自定义筛选器。
统计页年趋势图(月柱状)、月详情图(日折线+预算线)、标签占比饼图;预算历史圆点列表。
设置页GitHub 配置、标签/标签组管理、预算管理、筛选器管理、导入/导出、多币种偏好、主题等。

三、跨设备与数据同步(CRDT 操作日志方案)

为了避免多设备同时编辑导致的冲突,Cent 不直接存储最终数据,而是存储用户操作的日志(Action Log),通过“重放”操作得到最终状态。这借鉴了 CRDT 和 OT 的思想,但大幅简化,仅依赖每个元素的唯一 ID 和操作类型。

1. 核心数据结构(TypeScript 定义)


// 基础账单条目
type Item = {
  id: string;           // 唯一标识(UUID v4)
  time: number;         // 记账时间戳(毫秒)
  amount: number;
  categoryId: string;   // 标签ID(或标签名)
  // ... 其他字段:type, date, note, tags, currency, etc.
};
// 操作日志中的单条记录
type Action<T> = 
  | { type: 'add'; id: string; timestamp: number; content: T }
  | { type: 'update'; id: string; timestamp: number; value: T }
  | { type: 'delete'; id: string; timestamp: number; value: T['id'] }
  | { type: 'meta'; id?: string; timestamp: number; metaValue: any }; // 用于元数据覆盖
// 实际存储并参与合并的完整格式(含系统字段)
type FullAction<T> = {
  type: 'add' | 'update' | 'delete' | 'meta';
  id: string;               // 操作本身的唯一ID(可选,但建议有)
  timestamp: number;        // 操作发生的时间(用于确定最终顺序)
  content?: T;              // add 时携带
  value?: T | T['id'];      // update/delete 时携带
  metaValue?: any;          // meta 时携带
};
// 最终回放后得到的增强 Item(附加系统时间戳)
type FullItem<T> = T & {
  __create_at: number;      // 首次 add 操作的时间戳
  __update_at: number;      // 最后一次 update 的时间戳
  __delete_at?: number;     // 如果被删除,记录删除操作的时间戳
};

2. 操作日志的存储与合并流程

  • 所有用户操作(记账、修改、删除)都不直接修改最终数据,而是以 Append-Only 方式追加到 actions.log 文件(或按月份拆分,如 actions_2026-04.log)。

  • 每个操作必须包含:

    • timestamp:操作发生时的本地时间(用于排序)。

    • id:操作的唯一 ID(可选,但用于去重)。

    • type 及对应的负载。

  • 同步时:设备将本地的 actions.log 增量部分上传到 GitHub,同时拉取远程新增的操作。

  • 合并规则

    • 将所有操作按 timestamp 升序排列。

    • 顺序重放操作,生成最终状态(类似 Event Sourcing)。

    • 对同一个 Item.id

      • 遇到 add:如果该 id 已存在,则忽略(除非设计了版本向量,简化起见先到先得)。

      • 遇到 update:更新该 id 对应的内容,并记录 __update_at = max(__update_at, action.timestamp)

      • 遇到 delete:标记 __delete_at = action.timestamp(逻辑删除,不物理移除,以便其它设备恢复或统计时排除)。

  • 最终 API 返回数据时:过滤掉 __delete_at 不为空且 __delete_at 小于当前时间的条目。

3. 示例

初始云端操作日志:[add1, add2, delete1] → 重放后实际数据 [2]

  • 设备 A 离线操作:[add3, add4] → 本地重放为 [2,3,4]

  • 设备 B 离线操作:[add5, delete2] → 本地重放为 [5]

同步后(无论上传顺序):

  • 云端操作日志变为:[add1, add2, delete1, add3, add4, add5, delete2]

  • 按时间戳排序重放:

    1. add1 → 存在1

    2. add2 → 存在2

    3. delete1 → 删除1

    4. add3 → 存在3

    5. add4 → 存在4

    6. add5 → 存在5

    7. delete2 → 删除2

  • 最终结果:[3,4,5](所有设备一致)

4. 元数据(Meta)的冲突处理

对于标签定义、预算设置等简单 JSON 对象,采用“最后写入获胜”(Last Write Wins)策略,因为这类数据体积小,冲突概率低,且覆盖后影响可控。使用 meta 类型的 Action 记录每次变更,同步时取 timestamp 最大的作为最终值。

5. 性能与实现注意事项

  • 操作日志文件大小:会随时间增长,但个人记账每日操作数十条,一年约 1 万条,完全可接受。可定期(如每年)做一次 快照(Snapshot),将重放结果固化,并截断旧日志。

  • 重放性能:每次请求都需要重放全量日志?不需要。服务启动时加载一次日志到内存中,后续增量追加并更新内存中的最终状态。写操作时同时追加日志和更新内存状态。

  • 前端展示:前端不直接处理日志,后端 API 返回的是重放后的最终数据。前端无需关心冲突。

四、技术选型

  • 后端:Java + Spring Boot + JGit + Jackson (JSON) + 操作日志重放引擎

  • 数据存储:本地 Git 仓库缓存 + GitHub 远程,存储 actions.log(或按月份分片)以及 meta_actions.log

  • 前端:响应式 Web UI(Vue 3 + Vant 或纯 HTML/CSS/JS),适配手机与电脑

  • 统计图表:ECharts 或 Chart.js

  • 部署:电脑本地运行(推荐初期)或免费云服务(Railway/Vercel + 后端独立)

五、关键业务逻辑要点

1. 标签组规则校验(后端)

  • 记账时校验:若标签组 required=true,则交易中必须包含该组的至少一个标签;若 singleSelect=true,则交易中不能包含该组的两个及以上标签。

2. 预算进度计算

  • 实际支出 = 周期内所有支出交易金额之和(排除 excludedTags 中的标签)。

  • 理论支出 = 总预算 × (已过天数 / 总天数)。

  • 今日支出单独提取,用于进度条黄色部分。

3. 统计优化

  • 可定期生成聚合快照(如每月汇总文件),避免每次重放全部日志。

4. 标签名称变更的迁移(在操作日志模型下)

  • 标签重命名会生成一个特殊的 meta 操作,记录“标签 oldName -> newName”。重放时,需要将历史交易中的旧标签名映射为新名(或直接在查询时动态替换)。更简单的做法:交易中存储的 categoryId 是标签的 ID,标签表独立,重命名只需改标签表,无需改交易。

六、开发优先级建议(迭代顺序)

  1. 操作日志基础设施:定义 Action 结构,实现日志追加、重放引擎(内存模式)。

  2. 基础记账 API:增删改查基于重放后的最终数据,记录对应的 Action。

  3. Git 同步:将 actions.log 和 meta_actions.log 作为同步单位,实现 push/pull 合并(Append-Only 无需冲突处理)。

  4. 标签与标签组:使用 Meta 操作维护标签定义,交易中引用标签 ID。

  5. 统计图表与搜索:基于重放后的最终数据进行聚合。

  6. 预算模块:同样基于最终数据计算。

  7. 前端五个页面:逐页实现,配合后端 API。

  8. 多设备测试 & 快照优化

七、注意事项

  • 安全性:GitHub Token 不能硬编码,使用环境变量。

  • 移动端适配:所有页面需支持触摸操作,输入框调出合适的虚拟键盘。

  • 记账效率:提供快捷选项(如长按“+”复制上一笔,常用标签置顶)。

  • 预算“逃生舱”:需确保排除标签功能在统计和预算中一致生效。

  • 离线支持:后端本地缓存仓库后,即使无网络也可查看近期数据,记账操作暂存队列,待网络恢复后 push。

  • 操作日志的幂等性:为防止重复推送同一条 Action,可以在 Action 中增加全局唯一 ID,同步时去重。

Built with LogoFlowershow