# 本地持久化模块 ## 1. 定位 持久化模块负责把 [数据模块](data.md) 的事实安全地保存到本地,并提供事务、查询、迁移、备份、归档和重建能力。它不解释 CPA payload、不计算价格、不决定 Key 是否放行,也不包含 UI 逻辑。 第一版采用单文件 SQLite。它符合插件单机、低运维、随 CPA 一起分发的目标,又能提供计费账本需要的事务与唯一约束。不能沿用 `cpa-plugin-key-billing` 把整个状态写成一个 JSON 文档的方式。 ## 2. 存储分层 ### 2.1 权威事实:不可随意修改 - Request 与 Execution; - 标准 Usage; - BillingRecord 与 LedgerEntry; - 管理员余额调整; - 请求准入使用的 plan/cycle/price 快照。 事实只允许追加、幂等补全或显式冲正。历史价格变化不能重算并覆盖已经入账金额。 ### 2.2 当前目录与配置事实 - 下游 account、credential、自管 Key public ID/HMAC digest/preview/alias,以及兼容模式的 CPA scope; - 上游 auth/provider identity; - Key→plan 与 Key→upstream binding; - plan、route policy、price catalog/rule; - plugin settings 和 schema migrations。 同步删除采用 soft delete,历史请求不能因当前 Key 或上游账号被删除而失去解释能力。 ### 2.3 时间点快照 - 上游 quota/subscription snapshot; - 模型与价格目录版本; - credential health; - 同步运行结果。 快照按时间追加,当前值由最新有效快照投影,不覆盖历史。 ### 2.4 可重建派生数据 - 小时/日 overview; - activity; - latency 分布; - identity totals; - ranking、Top 和 heatmap; - realtime cache。 这些数据可以删除后从事实表重建,不能作为余额或账本真相。 ## 3. 建议表组 字段细节由数据模块契约决定,持久化初版至少需要以下表组: | 表组 | 建议表 | 说明 | | --- | --- | --- | | 下游身份 | `accounts`、`billing_accounts`、`credentials`、`credential_secret_versions` | 用户、金额账户、逻辑 Credential 和可轮换 secret version 分开;只存 public ID、HMAC digest/key ID、preview,不存可恢复明文 | | 上游身份 | `upstream_identities` | auth ID/index、provider、账户展示、状态、订阅元数据 | | 绑定与策略 | `billing_account_plans`、`credential_routes` | 金额套餐属于 BillingAccount,上游账户路由属于 Credential/Account | | 请求事实 | `requests`、`executions`、`usage_records` | 一次 Request 可有多个 Execution/Usage | | Usage 关联 | `usage_correlations`、`usage_observations` | response ID→Request/Execution 最小映射、canonical vector/hash/revision 与迟到观察 | | 价格 | `price_catalogs`、`price_rules`、`pricing_snapshots` | 带版本、来源、有效期和审批状态 | | 计费 | `billing_records`、`ledger_entries`、`billing_cycles` | 定点整数金额,事务提交 | | 上游配额 | `quota_snapshots`、`subscription_snapshots` | 与下游余额严格分离 | | 同步诊断 | `ingest_inbox`、`sync_runs`、`diagnostic_events` | 失败重试、来源与处理状态 | | 统一投影流 | `projection_events` | 全库单调 event_seq,领域事实/修订对应的确定性统计变化 | | 聚合 | `overview_hourly`、`overview_daily`、`activity_stats`、`latency_stats`、`aggregation_checkpoints` | 可以重建 | | 系统 | `schema_migrations`、`app_settings` | 数据库版本和插件设置;只有引入 sidecar/已认证用户路由后才增加 `auth_sessions` | `billing_records` 对 Usage 使用稳定逻辑 ID/revision,`ledger_entries.billing_record_id`、各 EventID/IdempotencyKey 必须有唯一约束。`usage_observations` 至少唯一约束 `(execution_id, canonical_hash)`;相同累计 vector 重放不能产生新 revision。 ## 4. SQLite 运行策略 参考 `cpa-usage-keeper/internal/repository/db.go`: - 文件库启用 WAL; - `busy_timeout=5000`; - `foreign_keys=ON`; - 单 writer connection,所有写事务串行; - 独立只读 pool,可允许少量并发查询; - 内存数据库测试时复用单连接; - 数据库路径使用插件专属 data directory,不放入动态库目录或当前工作目录猜测位置。 插件在 CPA 进程内,任何数据库操作都不能长期阻塞请求线程。准入 pending 和 response hook 取得的可靠 canonical Usage 必须在回调返回前耐久化;completion 不是 Usage 唯一落库点。使用有界队列时,队列满不能静默丢弃,应同步落库、进入本地 inbox,或对新请求 fail closed。 Go c-shared 内长期 goroutine/timer 的安全性尚未通过验证,MVP 持久化先采用 host callback/管理请求驱动的同步短事务,不自行启动后台 flusher。连接池、WAL checkpoint、聚合和 backup worker 只有通过 [dev.md](dev.md) 的双 Go runtime soak gate 后才能开启;否则长期任务移到 sidecar。 ## 5. 原子事务 一次正常结算至少在一个事务内完成: 1. 幂等插入/确认 Request、Execution、Usage; 2. 读取准入时锁定的 cycle 与 pricing snapshot; 3. 插入 BillingRecord; 4. 插入不可变 LedgerEntry; 5. 更新 cycle/account 的缓存余额投影; 6. 在 `projection_events` 插入可供聚合追赶的单调 `event_seq`,事务后再发送轻量通知;结算事务不提前推进任何聚合 checkpoint。 唯一约束是最终防线。进程内去重缓存只能提高性能,不能替代数据库幂等。 管理员充值、扣减、重置和冲正同样必须写 LedgerEntry,禁止直接修改 `spent` 或 `balance` 而不留原因。 ## 6. 金额、时间和未知值 - 金额统一保存为 `int64 micros` 和 currency,不使用 float 作为余额或账本字段; - Token 使用非负 `int64`; - 时间保存 UTC 或一个固定的规范化格式,展示时再转时区; - `NULL` 表示未知,数值 0 表示确认观察到零; - bool 若来源可能缺失则使用 nullable; - 枚举按稳定字符串保存,并由 schema version 管理扩展。 ## 7. Hot、Archive 与保留策略 参考 usage-keeper 的 `usage_events` / `usage_events_archive`,但不能直接照搬其单表 `INSERT SELECT + DELETE`:本项目存在 Request→Execution→Usage→Billing→Ledger 永久审计关系。 MVP 决断:不物理移动或删除 Request/Execution/Usage 最小事实行;只清理原始大响应引用、临时诊断和已过期内存缓存。BillingRecord、LedgerEntry 和管理员调整永久保留。待真实容量达到阈值后再设计统一逻辑 ID + hot/cold view,或只归档大字段/诊断明细;任何方案都不能打断账本引用。 - 最小 `usage_correlations` 默认随 hot Request 保留至少 90 天;24 小时只表示内存 cache TTL/awaiting 快速扫描窗口,不立即删除 response-ID 映射; - 超过关联保留期后,只有自带 RequestID/ExecutionID 的迟到事实仍可可靠补记,无 ID 观察不得猜配; - 小时级高分辨率聚合可以短期保留,日级金额/请求聚合长期保留; - quota snapshot 可以按策略降采样,保留重置边界和异常快照。 归档不是删除历史。用户要求清除日志时,必须区分“隐藏/清理诊断日志”“删除请求内容引用”和“不可删除的金额账本”。 ## 8. 聚合与 checkpoint `projection_events.event_seq INTEGER PRIMARY KEY AUTOINCREMENT` 是全库唯一聚合游标;同一事务还写 `event_id UNIQUE`、`event_kind`、`fact_id`、`fact_revision`、`occurred_at`、`booked_at`、`supersedes_event_id` 和确定性 delta payload。状态从 unmeasured 变为 measured 时,事件明确携带 `unmeasured_count=-1`、`measured_count=+1`、`spend_delta=...`,不能只追加一个“已结算”状态造成双计数。 每类聚合使用独立 checkpoint:overview、activity、latency、identity、ranking 互不阻塞。checkpoint 保存已处理的最大单调 `event_seq`,而不是 UUID `event_id` 或只保存时间;`event_id` 负责全局身份/幂等,不能拿来比较处理先后。 - 新事件提交后发送轻量通知; - callback-driven runner、sidecar 或 soak 通过后的后台 runner 批量追赶; - 启动时检查 lag 并恢复; - 单个聚合失败只保留自己的旧 checkpoint; - 聚合 upsert 与 checkpoint 推进处于同一事务; - 支持从 0 或指定 `event_seq` 重建; - UI 能展示各 checkpoint lag 和最后错误。 ## 9. Inbox、崩溃恢复与备份 如果存在 Redis/HTTP 导入或异步采集,应先把原始消息写入 `ingest_inbox`,再解码为事实,状态至少包含 pending、processed、process_failed、discarded。失败重试必须有次数和最后错误。 原生回调的 Request 临时状态不能完整依赖内存。至少在准入/执行开始处留下轻量 pending 事实,使进程崩溃后可以将未完成请求标记为 abandoned/unknown,而不是永久占用并发或额度预留。 恢复扫描还必须包括:已保存 canonical Usage 但尚未结算的 Execution、awaiting usage、未发布 projection/outbox。即使 completion 永久丢失,Request 可以转为 abandoned/unknown,可靠 Usage 仍必须结算。 备份使用 SQLite online backup API,不能只复制正在 WAL 模式运行的 `.db` 文件。建议: - 每日自动备份; - 默认保留 7~30 天; - 管理员可立即创建和下载备份; - 恢复必须校验 schema version、integrity check 和账本一致性; - SQLite 备份不包含外部 HMAC secret keyring;运维必须分开加密备份、成对恢复,并用非秘密 fingerprint/key ID 验证匹配;缺失或不匹配时认证 fail closed,禁止自动生成替代 secret; - shutdown 时停止 runner、checkpoint WAL、关闭 reader/writer,且整个过程幂等有超时。 ## 10. 索引原则 索引围绕真实查询建立: - request/event ID 唯一索引; - timestamp + ID 游标; - downstream credential/account + timestamp; - upstream identity + timestamp; - model + timestamp; - outcome/status + timestamp; - cycle、ledger account + occurred_at; - archive 只保留主键和必要的少数索引。 不要为 UI 的每个筛选组合创建索引。使用真实数据容量测试查询计划,再调整复合索引。 ## 11. 迁移策略 - 新库可创建当前完整 schema; - 已存在数据库必须执行有序、带版本的显式 migration; - migration 在插件开始接收业务请求前完成; - 破坏性迁移先备份; - migration 失败时插件进入不可计费/不放行的安全状态,不能打开空数据库继续免费运行; - 数据契约 schema version 与 SQLite migration version 分开管理。 ## 12. 参考路径与验收 主要参考: - `cpa-usage-keeper/internal/repository/db.go`; - `internal/entities/`; - `internal/repository/migration/`; - `internal/repository/usage_event_archive.go`; - `internal/backup/`; - `internal/repository/usage*.go`。 验收必须覆盖事务回滚、重复事件、并发读写、WAL、数据库锁、崩溃恢复、migration、备份恢复、archive 前 checkpoint 检查、账本金额一致性和 `go test -race`。