Files
eryao/docs/plans/points-audit-and-register-bonus-plan.md
T
2026-04-10 10:40:44 +08:00

420 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 积分审计与注册赠分策略改造计划(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-SHA256key 来自 `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` 被当成不同人。
- 策略:统一 normalizetrim + 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. 关键失败有指标与告警,不允许静默失败。