# 积分通知中心需求文档

更新时间：2026-05-22

## 目标

建立一份统一的创作者积分通知需求文档，避免积分提醒逻辑散落在验证码短信、人工发放、钱包查询或结算路径里。

v1 首个触达渠道是短信。但产品模型应按“通知中心”设计，使后续站内信、邮件、飞书等渠道可以复用同一套触发、去重、状态和运营审核规则。

本文档只描述需求，不直接引入 API、数据库表、任务、配置或前端类型变更。

## 现有参考

- `docs/billing-subscription-purchase-sms.md` 定义了当前 billing 异步短信模式：业务事实落库时记录通知意图，事务提交后入队，由 worker 调用供应商发送。
- `docs/operator-user-points-grant.md` 定义了人工积分发放语义，包括 `reward` 和 `compensation`。
- `docs/billing-subscription-design.md` 定义了钱包、余额桶、账本、余额桶过期和低余额提醒边界。

## 通知场景

### `credit_expiring`

在有效积分即将过期前通知创作者。

- 真相源：`credit_wallet_buckets`。
- 来源 ID：`wallet_bucket_bid`。
- 触发方式：定时扫描 active bucket，筛选 `effective_to` 落入配置提醒窗口的记录。
- v1 建议提醒窗口：`7d`、`3d`、`1d`、`0d`。
- 去重键：`credit_expiring:{wallet_bucket_bid}:{window}`。
- 内容变量：即将过期积分、过期时间、充值或订阅操作入口。
- 同一创作者在同一提醒窗口内有多个 bucket 到期时，应尽量合并成一条用户可见通知。

### `credit_granted`

积分成功发放后通知创作者。

- 真相源：`credit_ledger_entries`。
- 来源 ID：`ledger_bid`。
- 触发方式：grant ledger 和匹配 bucket 提交成功后，由事件触发。
- 覆盖发放来源：试用、付费套餐或充值包到账、人工奖励、人工补偿，以及未来活动发放。
- 去重键：`credit_granted:{ledger_bid}`。
- 内容变量：到账积分、发放来源标签、可用时的过期时间、积分详情或 billing 入口。
- 重复发放请求如果复用了既有 ledger，不得再次发送提醒。

### `low_balance`

可用积分较低时通知创作者。

- 真相源：`credit_wallets` 加当前可消费 bucket 状态。
- 来源 ID：`creator_bid`。
- 触发方式：定时扫描创作者钱包和 billing overview alert。
- 当前 billing v1 的低余额规则是 `available_credits <= BILL_LOW_BALANCE_THRESHOLD`，bootstrap 阈值为 `0`。
- v1 保留固定余额阈值，并新增“按日消耗估算可用天数”的自动阈值；未配置自动阈值时行为完全兼容固定阈值。
- 固定阈值去重键：`low_balance:{creator_bid}:{threshold}:{date}`。
- 自动阈值去重键：`low_balance:{creator_bid}:estimated_days:{days}:lookback:{lookback_days}:{date}`。
- 内容变量：当前可用积分、阈值类型、固定阈值、触发可用天数、回看天数、平均日消耗、预计剩余可用天数、checkout 或订阅操作入口。

按日消耗估算只用于余额提醒，不改变 softlimit、hardlimit、账务扣减或调试拦截语义。日消耗应以已结算账务聚合为准：使用 `bill_daily_ledger_summary` 中 `entry_type=consume` 的记录，只统计扫描日前的完整自然日，消耗额按绝对值计算。没有有效日消耗汇总或平均日消耗为 `0` 时，该自动规则应跳过，不触发兜底发送；有部分有效消耗历史但少于配置要求时，如果配置了固定余额兜底值，则按固定阈值兜底判断。

## 积分消耗提醒和提示

积分消耗提醒需要同时覆盖异步通知、页面提醒和 runtime 阻断提示。三者共享同一套余额状态判断，但职责不同：

- 异步通知负责短信、邮件等触达。
- 页面提醒负责老师端个人中心、积分页面等产品内提示。
- runtime 阻断提示负责 hardlimit 后停止生成并向学生端直接说明。

### 状态分层

| 状态 | 余额条件 | 老师端行为 | 学生端行为 |
| --- | --- | --- | --- |
| 正常使用 | 可用积分高于 softlimit | 正常显示 | 无额外提示 |
| 达到 softlimit | 剩余额度小于配置阈值，且仍可使用 | 页面提醒、个人中心提醒；可触发短信或邮件提醒；禁止调试 | 无额外提示 |
| 达到 hardlimit | 已用完，剩余额度为 `0`，禁止继续使用 | 页面提醒、个人中心提醒；可触发短信或邮件提醒 | 新内容生成和追问停止生成，直接提示 |

### 行为要求

- softlimit 阈值应由运营平台配置；v1 只使用固定积分阈值，避免按日消耗估算影响调试禁用。
- hardlimit 由 billing 可消费余额判断固定触发，不应由运营平台随意配置。
- 老师端页面提醒应优先出现在个人中心、积分页、调试入口等与充值或订阅操作相关的位置。
- softlimit 下老师端调试入口应被禁止或降级，避免继续消耗接近耗尽的积分。
- hardlimit 下学生端不再继续 LLM/TTS 等新内容生成，也不再继续追问生成；前端应展示明确的停止原因和后续操作提示。
- 学生端在正常使用和 softlimit 阶段不展示积分不足提醒，避免把创作者侧账务压力暴露给学习用户。

## 触发规则

- 事件触发型通知只能在业务状态提交成功后创建。v1 主要适用于 `credit_granted`。
- 扫描触发型通知由定时 worker 生成。v1 适用于 `credit_expiring` 和 `low_balance`。
- 路由 handler 不得直接发送短信。
- 支付供应商 adapter、验证码 helper、钱包只读 API 不应承载积分通知业务规则。
- 供应商失败可以重试。无手机号、退订、黑名单、重复抑制等跳过状态默认为终态，除非运营策略明确改变。

## 规则配置归属

通知规则应优先由运营平台配置，而不是写死在业务代码里。运营平台负责“何时提醒、用哪个模板、提醒谁、频率多少”；billing 代码负责“积分事实是否成立、通知是否幂等、投递状态是否可靠”。

运营平台应支持配置：

- 每类通知的启用状态：`credit_expiring`、`credit_granted`、`low_balance`。
- 每类通知的短信模板绑定，包括阿里云模板 code 和模板变量映射。
- 到期提醒窗口，例如 `7d`、`3d`、`1d`、`0d`。
- 低余额阈值，例如余额为 `0`、低于固定积分数，或按最近完整自然日消耗估算预计 N 天内耗尽。
- 自动低余额阈值参数：触发可用天数、日消耗回看天数、最少有效消耗天数、数据不足时的固定余额兜底值。
- softlimit 固定阈值，以及达到 softlimit 后老师端页面提醒、短信或邮件提醒、禁止调试等策略开关。
- 频控规则，例如同一手机号每天或每周最多发送几条积分通知。
- 免打扰时间，例如夜间不发送，延迟到下一个可发送窗口。
- 黑名单、退订、禁发策略。
- dry-run、审批、预算上限和批量启用前的人群预估。
- 失败重发策略，仅允许对 `failed_provider` 等可重试状态重新入队。

不应由运营平台随意配置：

- 阿里云 AccessKey、Secret 等供应商密钥；这些仍应使用环境变量或 Secret 管理。
- 验证码短信模板；验证码短信和积分通知短信必须分开。
- 积分发放、扣减、过期等账务规则；这些仍以 billing ledger、bucket、wallet 逻辑为准。
- hardlimit 判定和 runtime 阻断语义；这些应由 billing admission 和前端运行时固定实现。
- 幂等键生成规则；运营平台可以查看去重结果，但生成规则应由实现固定。
- runtime admission 错误码；通知规则不应扩展 runtime 阻断语义。
- softlimit 的自动日消耗规则；v1 只允许固定阈值控制 softlimit。

v1 可先用 billing 动态配置承载运营策略，例如 `BILL_CREDIT_NOTIFICATION_SMS_CONFIG` JSON。该 JSON 是底层存储格式，运营平台不得直接暴露 JSON 编辑器；页面应提供结构化表单来维护开关、模板、窗口、阈值、频控、名单和预算。若后续需要多活动、多模板版本、审批流和操作审计，应演进为独立的 notification policy 与 notification record 表。

## 接收人规则

- 默认接收人是积分被发放、即将过期或低余额的创作者。
- 手机号从创作者用户账号资料中解析。
- 手机号标识必须沿用 auth 和用户查询流程里的统一规范化规则。
- 创作者没有可用手机号时，通知状态标记为 `skipped_no_mobile`。
- 创作者已退订、在黑名单中，或被运营策略禁发时，通知状态标记为跳过，不调用短信供应商。
- 国际化和时区格式化应优先使用用户语言和应用时区。

## 通知状态

每次通知尝试都应有持久状态记录，或等价的 metadata 记录，至少包含：

- 通知类型：`credit_expiring`、`credit_granted` 或 `low_balance`
- 触达渠道：v1 使用 `sms`
- creator BID 和目标 user BID
- 本次发送使用的手机号快照
- 来源类型和来源 ID
- 去重键
- 状态
- 请求、发送、更新时间
- 供应商响应摘要和错误码

必须支持的状态：

- `pending`：通知意图已创建，可以被处理
- `sent`：短信供应商已接受发送请求
- `skipped_no_mobile`：没有可用的接收手机号
- `skipped_opt_out`：接收人或运营策略阻止发送
- `suppressed_duplicate`：同一去重键已有已处理通知
- `failed_provider`：短信供应商失败，通知可以重试

## 运营后台需求

v1 需要一个只读为主的运营管理面：

- 按创作者、手机号、通知类型、渠道、状态、来源 ID、时间范围查询通知记录。
- 查看失败通知的供应商响应和错误码。
- 对 `failed_provider` 状态的通知执行重新入队。
- 查看通知被跳过的原因。
- 配置每类通知的启用状态、短信模板、提醒窗口和低余额阈值。
- 低余额阈值配置必须是结构化字段，支持固定积分阈值和按可用天数提醒，不提供 raw JSON 编辑入口。
- 配置频控、免打扰、黑名单、退订和预算上限等运营规则。
- 配置 softlimit 阈值和老师端提醒策略，并展示 hardlimit 的固定阻断规则。
- 启用扫描型通知策略前，可以先 dry-run 统计预计触达人群数量。

运营操作不得修改 wallet、bucket 或 ledger 事实。重新入队只改变通知投递状态。

## 指标

按通知类型和渠道统计产品与运营指标：

- 生成通知数
- 发送成功数
- 供应商失败数
- 按原因分组的跳过数
- 重复抑制数
- 短信成本估算
- 通知后的转化，例如登录、checkout、充值、续费、课程创建或继续消耗积分

指标只用于报表分析。积分余额和消耗真相仍以 billing ledger 和 bucket 表为准。

## 边界

- 积分通知不负责发放、消耗、过期或调整积分。
- 积分事实仍以 `credit_ledger_entries`、`credit_wallet_buckets` 和 `credit_wallets` 为准。
- 余额桶过期仍由 `billing.expire_wallet_buckets` 负责。
- 低余额检测仍属于 billing 范畴，不应新增 runtime admission 错误码。
- 不复用 `/api/user/send_sms_code`、`/api/user/console_send_sms_code` 或验证码短信模板发送积分通知。
- 来源 billing 事实提交前，不得发送通知短信。
- 运营平台配置通知策略，但不能改变积分账务事实或幂等规则。
- hardlimit 后的学生端停止生成属于 runtime 行为，不应通过异步通知任务补偿。

## 验收标准

- 三类 v1 通知类型均明确了触发真相源、来源 ID 和去重键。
- 文档明确 softlimit 与 hardlimit 下老师端和学生端的提示、通知、禁止调试和停止生成行为。
- 文档清楚区分通知投递和积分账本变更。
- 文档明确区分 auth 验证码短信和积分通知短信。
- 文档明确通知规则应由运营平台配置，并列出可配置与不可配置边界。
- 文档在需求层定义跳过、失败、重复和重试行为。
- 文档引用现有 billing 短信、人工发放、过期和低余额设计，但不宣称新能力已经实现。
