# 积分审计与注册赠分策略改造计划(gstack / plan-eng-review) ## 1. 目标与结论 本计划解决三个问题: 1. 用户删除账号后,积分与成本审计数据不能随业务数据一起丢失。 2. 同邮箱重复注册时,不应再次拿到注册赠分。 3. 积分消耗审计必须记录真实 `input_tokens` / `output_tokens` / `cost`,不能再写占位值。 4. LLM 失败/取消时若平台已产生真实成本,该成本不转嫁用户积分,但必须进入审计账本。 结论:采用 **双账本 + 资格账本**。 - 保留业务账本:`user_points`、`points_ledger`(在线业务能力) - 新增审计账本:`points_audit_ledger`(不可变审计) - 新增资格账本:`register_bonus_claims`(注册奖励去重) - 注册赠分策略从 DB trigger 移出,改为应用层策略(配置驱动) --- ## 2. 系统边界 ### 2.1 业务域(可删除) - `user_points`:余额视图 - `points_ledger`:业务流水 - `messages` / `sessions`:会话与消息 ### 2.2 审计域(不可级联删除) - `points_audit_ledger`:审计流水,保留用户快照和成本快照(含用户承担/平台承担归属) - `register_bonus_claims`:注册奖励领取资格记录 ### 2.3 策略域(应用层) - `register_bonus_points` 配置项(默认 60) - `register_bonus_hmac_key` 配置项(环境变量注入) - 首登赠分是否发放由服务层决定,不写死在数据库 trigger --- ## 3. 现状问题(基于当前代码) 1. 注册赠分写死在 DB trigger。 当前函数:`public.initialize_profile_and_invite_code_on_signup()`,历史上出现过 100/60 改动漂移。 2. 积分消费审计写占位值。 在 `backend/src/v1/points/service.py` 中,`consume_successful_run_points` 写入 `input_tokens=0`、`output_tokens=0`、`cost=0`。 3. 删号会丢审计线索。 当前业务删除路径会清理业务数据,缺少独立审计账本保留策略。 --- ## 4. 数据模型(按项目风格精简命名) 说明:不引入陌生“模板字段”,沿用当前 `points_ledger` 命名风格。 ### 4.1 新表:`points_audit_ledger` - `id` UUID PK - `event_id` VARCHAR(64) UNIQUE NOT NULL - `user_id_snapshot` UUID NULL - `user_email_snapshot` TEXT NULL - `change_type` VARCHAR(16) NOT NULL - `biz_type` VARCHAR(16) NULL - `biz_id` UUID NULL - `direction` SMALLINT NOT NULL - `amount` BIGINT NOT NULL - `balance_after` BIGINT NOT NULL - `billed_to` VARCHAR(16) NOT NULL -- `user` | `platform` - `run_id` VARCHAR(128) NULL - `request_id` VARCHAR(128) NULL - `input_tokens` INTEGER NOT NULL DEFAULT 0 - `output_tokens` INTEGER NOT NULL DEFAULT 0 - `cost` NUMERIC(12,6) NOT NULL DEFAULT 0 - `metadata` JSONB NOT NULL DEFAULT '{}' - `created_at` TIMESTAMPTZ NOT NULL DEFAULT now() - `updated_at` TIMESTAMPTZ NOT NULL DEFAULT now() 索引建议: - `uq_points_audit_ledger_event_id` - `ix_points_audit_ledger_user_id_created_at` (`user_id_snapshot`, `created_at DESC`) - `ix_points_audit_ledger_change_type_created_at` (`change_type`, `created_at DESC`) ### 4.2 新表:`register_bonus_claims` - `id` UUID PK - `email_hash` VARCHAR(64) UNIQUE NOT NULL - `user_email_snapshot` TEXT NOT NULL - `first_user_id` UUID NULL - `grant_event_id` VARCHAR(64) UNIQUE NOT NULL - `created_at` TIMESTAMPTZ NOT NULL DEFAULT now() - `updated_at` TIMESTAMPTZ NOT NULL DEFAULT now() 注:`email_hash` 由标准化邮箱(trim + lower)计算(HMAC-SHA256,key 来自 `register_bonus_hmac_key`)。 --- ## 5. 数据流设计 ### 5.1 注册赠分流程(应用层,非 trigger) ```text [用户首登/注册完成] -> PointsPolicyService.load(register_bonus_points) -> normalize(email) -> email_hash -> INSERT register_bonus_claims(email_hash, ...) - 成功: 继续发放积分 - 唯一冲突: 说明历史已领取,跳过发放 -> 更新 user_points -> 写 points_ledger -> 写 points_audit_ledger -> commit ``` ### 5.2 运行消耗积分流程(写真实成本) ```text [run completed] -> 从持久化消息/会话聚合真实 usage (input_tokens, output_tokens, cost) -> PointsService.consume_successful_run_points(...) -> 更新 user_points -> 写 points_ledger -> 写 points_audit_ledger(真实 usage) -> commit ``` ### 5.3 运行失败/取消但平台发生成本流程(不扣用户,记平台账) ```text [run failed/canceled] -> 从持久化消息/事件聚合真实 usage -> 若 cost > 0: - 不调用用户扣分 - 写 points_audit_ledger( direction=0, amount=0, billed_to='platform', input_tokens/output_tokens/cost=真实值, metadata.reason='run_failed_or_canceled_platform_billed' ) -> commit ``` ### 5.4 删除账号流程 ```text [delete account] -> 删除 user_points / points_ledger / sessions / messages / profile / auth -> 保留 points_audit_ledger / register_bonus_claims ``` --- ## 6. 失败模式与处理 ### 6.1 双写不一致(P0) - 场景:`points_ledger` 写成功,`points_audit_ledger` 写失败。 - 策略:同事务写入,任一失败全部回滚。 ### 6.2 并发重复注册(P0) - 场景:同邮箱并发首登,发放多次。 - 策略:`register_bonus_claims.email_hash` 唯一约束 + 冲突即跳过。 ### 6.3 邮箱规范化不一致(P1) - 场景:`User@A.com` 与 `user@a.com` 被当成不同人。 - 策略:统一 normalize(trim + lower)后再 hash。 ### 6.4 成本快照缺失(P1) - 场景:run 成功但 usage 聚合取不到,写入 0。 - 策略: - 业务是否扣分与成本写入解耦:允许扣分,但审计需标记 `metadata.usage_missing=true` - 记录 warning 日志并纳入告警指标 ### 6.5 失败/取消真实成本归属(P0) - 场景:LLM 回调失败或用户取消,但上游已计费。 - 策略: - 不扣用户积分(`user_points`、`points_ledger`不变) - 审计账本强制落一条平台承担记录(`billed_to='platform'`) - 该记录必须包含真实 `input_tokens` / `output_tokens` / `cost` --- ## 7. 信任边界与安全 1. `user_email_snapshot` 必须来自服务端认证上下文,不接受客户端传入。 2. `input_tokens/output_tokens/cost` 必须来自服务端持久化记录,不接受客户端上报。 3. 审计表只允许后端 service-role 写入,不暴露客户端写接口。 4. `register_bonus_claims` 不应被普通业务接口更新/删除。 5. `register_bonus_hmac_key` 仅后端可读,不下发客户端,不写日志。 --- ## 8. 实施步骤(最小改动优先) ### Phase 1: 协议与配置 - 更新协议文档: - `docs/protocols/common/user-points-chat-data-protocol.md` - 新增“审计留存与注册奖励策略”章节 - 新增配置:`register_bonus_points`(默认 60) ### Phase 2: 数据库迁移 - 新增表:`points_audit_ledger` - 新增表:`register_bonus_claims` - 不改现有 `points_ledger`、`user_points` 结构 ### Phase 3: 服务层改造 - 移除 trigger 中注册送分逻辑(trigger 只保留 profile/invite 初始化) - 在应用层增加注册奖励发放逻辑(带资格检查) - 在积分消费路径改造为真实 usage 写审计 - 在失败/取消路径增加平台承担成本审计(不扣用户) ### Phase 4: 删除链路校验 - 删除账号后验证业务表清理 - 验证审计表与资格表仍可查 --- ## 9. 测试覆盖计划 ### 9.0 P0 测试门槛(实现前锁定) 以下测试为上线前阻断项,任一缺失不得合并: 1. **幂等回放**:同一 `event_id` 重放不重复写 `points_audit_ledger`。 2. **注册送分去重**:同邮箱(normalize 后)重复注册不重复发放积分。 3. **事务一致性**:业务账本写入成功但审计写入失败时,整体回滚。 4. **删除后重注册**:删号后同邮箱重注册仍不再发放首登奖励。 5. **失败/取消审计**:run 失败与取消场景写审计但不扣积分。 6. **成本归属**:失败/取消且 `cost>0` 的记录必须为 `billed_to='platform'`。 ### 9.1 单元测试 - 邮箱 normalize/hash 一致性 - 注册奖励配置读取与默认值 - usage 聚合函数(含空值和异常值) ### 9.2 集成测试 - 首次注册发放奖励成功 - 同邮箱重复注册不再发放 - 并发注册仅一次成功发放 - 消费积分写入真实 tokens/cost 审计 - 失败/取消且平台发生成本时,写平台承担审计且不扣用户积分 - 删号后审计数据保留 ### 9.3 回归测试 - 现有积分余额查询和扣分逻辑不回归 - 邀请码流程不回归 --- ## 10. 文件级改造清单 ### 数据库 / 模型 - `backend/alembic/versions/*` 新增迁移:创建两张新表 - `backend/src/models/points_audit_ledger.py` 新增 - `backend/src/models/register_bonus_claims.py` 新增 ### 积分服务与仓储 - `backend/src/v1/points/repository.py`:新增审计写入、资格检查方法 - `backend/src/v1/points/service.py`: - 新增注册奖励发放入口(配置驱动) - 消费路径写真实 usage 审计 - 失败/取消路径写平台承担成本审计 ### 运行时调用链 - `backend/src/core/agentscope/runtime/tasks.py`: - 在扣分点传入真实 usage(或可计算上下文) - 在 run 异常/取消路径传入 usage 并落平台承担审计 ### 协议文档 - `docs/protocols/common/user-points-chat-data-protocol.md` 更新 --- ## 11. 取舍说明 ### 为什么不直接改 `points_ledger` 为审计表 - 会把在线业务与审计诉求耦合在一张表,后续权限和迁移风险高。 - 当前最小改动方案是新增审计表,保持业务链路稳定。 ### 为什么保留 `event_id` - `id` 是技术主键,只保证行唯一。 - `event_id` 是业务幂等键,防重放、防重试重复记账、支持跨表对账。 --- ## 12. 未决事项 1. `user_email_snapshot` 是否明文存储,还是仅内部可解密存储。 2. 审计数据保留时长(默认建议至少 1 年)。 3. 成本单位与精度是否统一沿用 `NUMERIC(12,6)`。 --- ## 13. PR 拆分与执行顺序(可直接实现) ### PR1:数据库与协议落地(不改业务行为) 目标:先建立新数据边界,不改变线上积分逻辑。 改动范围: - `backend/alembic/versions/*`:新增迁移,创建 `points_audit_ledger`、`register_bonus_claims` - `backend/src/models/points_audit_ledger.py`:新增模型 - `backend/src/models/register_bonus_claims.py`:新增模型 - `docs/protocols/common/user-points-chat-data-protocol.md`:补审计与注册送分策略契约 验收标准: - 迁移可执行、可回滚 - 新表索引与唯一约束生效 - 协议文档与表结构一致 测试要求: - 迁移 smoke test - 约束与索引存在性校验 ### PR2:注册送分策略迁移到应用层(去 trigger 固化) 目标:把注册送分从 DB trigger 移到应用层唯一触发点(注册回调)。 改动范围: - `backend/src/v1/points/service.py`:新增注册奖励发放入口与资格校验 - `backend/src/v1/points/repository.py`:新增 `register_bonus_claims` 检查/写入 - `backend/src/core/config/settings.py`(或等价配置入口):新增 `register_bonus_points` - `backend/src/core/config/settings.py`(或等价配置入口):新增 `register_bonus_hmac_key` - 相关注册回调调用链文件:接入 `grant_register_bonus_if_eligible(...)` - 迁移调整:更新 trigger,移除注册送分写入逻辑,仅保留 profile/invite 初始化 验收标准: - 新注册触发一次赠分 - 同邮箱重复注册不再赠分 - 配置变更可控制赠分值(默认 60) - 邮箱哈希稳定且不可逆(同邮箱同哈希,不暴露明文) 测试要求: - 并发注册去重测试(唯一约束 + 冲突路径) - 删除账号后同邮箱重注册不赠分 - event_id 幂等回放不重复发放 - 缺失 `register_bonus_hmac_key` 时服务启动失败(fail fast) ### PR3:真实成本审计与删除链路联调 目标:将 run 真实 usage 写入审计,并覆盖成功/失败/取消三种对话轮次;失败/取消场景发生真实成本时记平台承担。 改动范围: - `backend/src/v1/points/service.py`:消费路径审计写入(真实 tokens/cost) - `backend/src/v1/points/repository.py`:新增 `append_audit_ledger(...)` - `backend/src/core/agentscope/runtime/tasks.py`:传递该轮次必要上下文 - 账号删除服务链路:确认保留 `points_audit_ledger/register_bonus_claims` 验收标准: - 成功对话:扣分 + 审计 - 失败/取消对话:不扣分 + 审计(若有成本则 `billed_to='platform'`) - 审计中的 `input_tokens/output_tokens/cost` 为真实值,不再占位 0 测试要求: - 成功/失败/取消三路径集成测试 - 事务一致性测试(业务写成功 + 审计写失败 -> 回滚) - 删除后审计保留验证 - 失败/取消 + `cost>0` 平台承担场景回归测试 ### PR4:观测与运维保障(建议同迭代完成) 目标:避免审计静默失真。 改动范围: - 指标与日志: - `points_audit_write_failed_total` - `points_usage_missing_total` - 告警阈值:连续失败或短时突增告警 - 运维文档:异常重放与人工核对流程 验收标准: - 审计写入失败可被监控发现 - usage 缺失可被监控发现并可追溯到事件 --- ## 14. 实施完成定义(DoD) 满足以下全部条件才算完成: 1. 计划中的 P0 测试门槛全部通过。 2. 注册赠分不再依赖 DB trigger 写死值。 3. `points_audit_ledger` 记录真实 usage,覆盖成功/失败/取消。 3. `points_audit_ledger` 记录真实 usage,覆盖成功/失败/取消;失败/取消有真实成本时归属 `platform`。 4. 删除账号后业务数据清理,审计与资格数据保留。 5. 关键失败有指标与告警,不允许静默失败。