Prompt 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] -
按时间戳排序重放:
-
add1 → 存在1
-
add2 → 存在2
-
delete1 → 删除1
-
add3 → 存在3
-
add4 → 存在4
-
add5 → 存在5
-
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,标签表独立,重命名只需改标签表,无需改交易。
六、开发优先级建议(迭代顺序)
-
操作日志基础设施:定义 Action 结构,实现日志追加、重放引擎(内存模式)。
-
基础记账 API:增删改查基于重放后的最终数据,记录对应的 Action。
-
Git 同步:将
actions.log和meta_actions.log作为同步单位,实现 push/pull 合并(Append-Only 无需冲突处理)。 -
标签与标签组:使用 Meta 操作维护标签定义,交易中引用标签 ID。
-
统计图表与搜索:基于重放后的最终数据进行聚合。
-
预算模块:同样基于最终数据计算。
-
前端五个页面:逐页实现,配合后端 API。
-
多设备测试 & 快照优化。
七、注意事项
-
安全性:GitHub Token 不能硬编码,使用环境变量。
-
移动端适配:所有页面需支持触摸操作,输入框调出合适的虚拟键盘。
-
记账效率:提供快捷选项(如长按“+”复制上一笔,常用标签置顶)。
-
预算“逃生舱”:需确保排除标签功能在统计和预算中一致生效。
-
离线支持:后端本地缓存仓库后,即使无网络也可查看近期数据,记账操作暂存队列,待网络恢复后 push。
-
操作日志的幂等性:为防止重复推送同一条 Action,可以在 Action 中增加全局唯一 ID,同步时去重。