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

14 KiB
Raw Blame History

积分审计与注册赠分策略改造计划(gstack / plan-eng-review

1. 目标与结论

本计划解决三个问题:

  1. 用户删除账号后,积分与成本审计数据不能随业务数据一起丢失。
  2. 同邮箱重复注册时,不应再次拿到注册赠分。
  3. 积分消耗审计必须记录真实 input_tokens / output_tokens / cost,不能再写占位值。
  4. LLM 失败/取消时若平台已产生真实成本,该成本不转嫁用户积分,但必须进入审计账本。

结论:采用 双账本 + 资格账本

  • 保留业务账本:user_pointspoints_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=0output_tokens=0cost=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)

[用户首登/注册完成]
  -> 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 运行消耗积分流程(写真实成本)

[run completed]
  -> 从持久化消息/会话聚合真实 usage
     (input_tokens, output_tokens, cost)
  -> PointsService.consume_successful_run_points(...)
  -> 更新 user_points
  -> 写 points_ledger
  -> 写 points_audit_ledger(真实 usage)
  -> commit

5.3 运行失败/取消但平台发生成本流程(不扣用户,记平台账)

[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 删除账号流程

[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.comuser@a.com 被当成不同人。
  • 策略:统一 normalizetrim + lower)后再 hash。

6.4 成本快照缺失(P1

  • 场景:run 成功但 usage 聚合取不到,写入 0。
  • 策略:
    • 业务是否扣分与成本写入解耦:允许扣分,但审计需标记 metadata.usage_missing=true
    • 记录 warning 日志并纳入告警指标

6.5 失败/取消真实成本归属(P0

  • 场景:LLM 回调失败或用户取消,但上游已计费。
  • 策略:
    • 不扣用户积分(user_pointspoints_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_ledgeruser_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_ledgerregister_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,覆盖成功/失败/取消。
  4. points_audit_ledger 记录真实 usage,覆盖成功/失败/取消;失败/取消有真实成本时归属 platform
  5. 删除账号后业务数据清理,审计与资格数据保留。
  6. 关键失败有指标与告警,不允许静默失败。