feat: 添加 points_audit_ledger 及 JSON 字段 Pydantic Schema 约束
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
# iOS 新人包支付接入与一次性权益计划
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
当前前端充值页为静态套餐展示,购买按钮未接入真实支付链路。现需新增 iOS 新人包:
|
||||
|
||||
- 价格:`$0.99`
|
||||
- 积分:`60`
|
||||
- 资格:同邮箱只能购买一次
|
||||
- 删除账号后同邮箱重新注册,不刷新新人包资格
|
||||
|
||||
同时补齐后端真实支付路由与订单审计能力,前端不再硬编码套餐。
|
||||
|
||||
## 2. 本次范围
|
||||
|
||||
### 2.1 In Scope
|
||||
|
||||
1. 后端新增 iOS 支付相关路由(下单/验单/查询/回调)。
|
||||
2. 新建支付订单主表与支付事件审计表。
|
||||
3. 改造 `register_bonus_claims` 为可承载“权益唯一占用”能力。
|
||||
4. 前端套餐由后端接口驱动,不再硬编码三档固定套餐。
|
||||
5. 新人包资格前后端联动(展示、购买、验单、入账)。
|
||||
|
||||
### 2.2 Out of Scope
|
||||
|
||||
1. Android 支付渠道接入。
|
||||
2. Apple 开发者账号正式联调(当前账号未就绪)。
|
||||
3. 财务对账后台页面。
|
||||
|
||||
## 3. 数据模型设计
|
||||
|
||||
## 3.1 新建表:`payment_orders`
|
||||
|
||||
用途:订单当前态,支持幂等验单与退款状态跟踪。
|
||||
|
||||
建议字段:
|
||||
|
||||
- `id` UUID PK
|
||||
- `order_no` VARCHAR(64) UNIQUE
|
||||
- `user_id` UUID NOT NULL (`auth.users.id`)
|
||||
- `channel` VARCHAR(16) NOT NULL (`ios_iap`)
|
||||
- `product_code` VARCHAR(64) NOT NULL(例:`new_user_pack_099_60`)
|
||||
- `price_usd` NUMERIC(12,6) NOT NULL
|
||||
- `credits` BIGINT NOT NULL
|
||||
- `currency` VARCHAR(8) NOT NULL DEFAULT `USD`
|
||||
- `status` VARCHAR(24) NOT NULL
|
||||
- `created|receipt_submitted|verified|credited|refund_pending|refunded|revoked|failed`
|
||||
- `apple_transaction_id` VARCHAR(128) NULL UNIQUE
|
||||
- `apple_original_transaction_id` VARCHAR(128) NULL
|
||||
- `app_account_token` UUID NULL
|
||||
- `idempotency_key` VARCHAR(128) NULL UNIQUE
|
||||
- `error_code` VARCHAR(64) NULL
|
||||
- `error_message` TEXT NULL
|
||||
- `created_at` / `updated_at`
|
||||
|
||||
关键约束:
|
||||
|
||||
- `credits > 0`
|
||||
- `price_usd >= 0`
|
||||
- `status` check
|
||||
- `channel='ios_iap'`(本期)
|
||||
|
||||
## 3.2 新建表:`payment_order_events`
|
||||
|
||||
用途:支付事件不可变审计流水(验单结果、回调、退款、冲正)。
|
||||
|
||||
建议字段:
|
||||
|
||||
- `id` UUID PK
|
||||
- `order_id` UUID NOT NULL FK `payment_orders.id`
|
||||
- `event_type` VARCHAR(32) NOT NULL
|
||||
- `order_created|receipt_submitted|verify_success|verify_failed|credited|refund_notified|refunded|revoke_notified|reversed`
|
||||
- `event_source` VARCHAR(24) NOT NULL
|
||||
- `api|apple_server_notification|job`
|
||||
- `event_idempotency_key` VARCHAR(128) NULL UNIQUE
|
||||
- `payload` JSONB NOT NULL
|
||||
- `operator_id` UUID NULL
|
||||
- `created_at`
|
||||
|
||||
## 3.3 改造表:`register_bonus_claims`
|
||||
|
||||
目标:从“注册送分去重”升级为“权益唯一占用”。
|
||||
|
||||
新增字段建议:
|
||||
|
||||
- `offer_code` VARCHAR(64) NOT NULL(例:`register_bonus_20`、`new_user_pack_099_60`)
|
||||
- `claim_source` VARCHAR(24) NOT NULL(`register_bonus|ios_purchase`)
|
||||
- `claim_order_id` UUID NULL FK `payment_orders.id`
|
||||
|
||||
新增唯一约束:
|
||||
|
||||
- `UNIQUE(offer_code, email_hash)`
|
||||
|
||||
保留行为:
|
||||
|
||||
- `first_user_id` 允许 `ON DELETE SET NULL`,保证删号后资格仍占用。
|
||||
|
||||
## 4. 路由与服务边界
|
||||
|
||||
## 4.1 后端新增路由(v1)
|
||||
|
||||
1. `GET /api/v1/payments/packages`
|
||||
- 返回可购买套餐列表与用户资格(是否可买新人包)。
|
||||
2. `POST /api/v1/payments/orders`
|
||||
- 创建订单,返回 `orderNo` 与客户端支付所需参数。
|
||||
3. `POST /api/v1/payments/orders/{orderNo}/verify-ios-receipt`
|
||||
- 提交 iOS 收据,后端调用 Apple 校验。
|
||||
4. `GET /api/v1/payments/orders/{orderNo}`
|
||||
- 查询订单状态与入账结果。
|
||||
5. `POST /api/v1/payments/webhooks/apple`
|
||||
- 接收 App Store Server Notifications V2,处理退款/撤销。
|
||||
|
||||
## 4.2 分层职责
|
||||
|
||||
- Router:鉴权、请求校验、RFC7807 错误映射。
|
||||
- Service:
|
||||
- 资格判断(新人包是否可买)
|
||||
- 下单与验单业务编排
|
||||
- 入账积分与冲正
|
||||
- 幂等控制
|
||||
- Repository:
|
||||
- `payment_orders`/`payment_order_events`/`register_bonus_claims` 读写
|
||||
- 订单状态流转条件更新
|
||||
|
||||
## 5. 核心流程
|
||||
|
||||
## 5.1 下单与资格检查
|
||||
|
||||
```text
|
||||
客户端请求套餐 -> GET /payments/packages
|
||||
-> 后端按 email_hash 检查 offer_code='new_user_pack_099_60' 是否已占用
|
||||
-> 返回 eligible=true/false
|
||||
|
||||
客户端创建订单 -> POST /payments/orders
|
||||
-> 再次做资格校验(防并发)
|
||||
-> 创建 payment_orders(status=created)
|
||||
-> 写 payment_order_events(order_created)
|
||||
```
|
||||
|
||||
## 5.2 iOS 验单与积分入账
|
||||
|
||||
```text
|
||||
客户端支付后提交 receipt -> POST /orders/{orderNo}/verify-ios-receipt
|
||||
-> 后端调用 Apple 验单(可切 sandbox)
|
||||
-> 验证 transaction_id 幂等
|
||||
-> 状态 verified
|
||||
-> 原子事务:
|
||||
1) 占用权益 register_bonus_claims(offer_code,email_hash)
|
||||
2) 写 points_ledger(grant)
|
||||
3) 写 points_audit_ledger(direction=1,billed_to='user')
|
||||
4) 订单置 credited
|
||||
5) 写 payment_order_events(credited)
|
||||
```
|
||||
|
||||
## 5.3 退款与冲正
|
||||
|
||||
```text
|
||||
Apple 回调退款 -> POST /payments/webhooks/apple
|
||||
-> 定位 order(transaction_id / original_transaction_id)
|
||||
-> 幂等处理通知
|
||||
-> 状态 refunded/revoked
|
||||
-> 原子事务:
|
||||
1) 写 points_ledger(adjust/consume reverse)
|
||||
2) 写 points_audit_ledger(direction=-1,billed_to='platform',metadata.reason='refund')
|
||||
3) 写 payment_order_events(refunded/reversed)
|
||||
```
|
||||
|
||||
## 6. 信任边界与风控
|
||||
|
||||
1. 客户端价格、积分、product_code 全部不可信,按后端配置为准。
|
||||
2. 不信任客户端“支付成功”标记,必须后端验单通过才入账。
|
||||
3. Apple 回调需验签(JWS)并做 `notificationUUID` 幂等。
|
||||
4. 订单与入账使用数据库事务,失败不允许半成功。
|
||||
5. `offer_code + email_hash` 唯一约束是最终防线。
|
||||
|
||||
## 7. 前端改造
|
||||
|
||||
当前 `CoinCenterScreen` 中套餐硬编码,需改为 API 驱动:
|
||||
|
||||
- 页面加载调用 `GET /api/v1/payments/packages`
|
||||
- 渲染返回的套餐列表
|
||||
- 新人包 `eligible=false` 时展示“已购买/不可购买”态
|
||||
- 点击购买后走真实支付流(创建订单 -> 拉起 IAP -> 提交 receipt)
|
||||
|
||||
## 8. 无 Apple 账号阶段的交付策略
|
||||
|
||||
在无开发者账号前,先做可替换的验单适配层:
|
||||
|
||||
- `IOSReceiptVerifier` 接口(生产实现 + mock 实现)
|
||||
- 通过配置开关使用 mock 结果跑通后端链路与前端状态
|
||||
- 后续只替换 verifier 实现,不改订单主流程
|
||||
|
||||
## 9. 测试计划
|
||||
|
||||
## 9.1 后端单元测试
|
||||
|
||||
1. 新人包资格判定(首次可买、重复不可买、删号重注册不可买)
|
||||
2. 验单幂等(同 transaction_id 不重复入账)
|
||||
3. 退款冲正幂等(同通知不重复冲正)
|
||||
|
||||
## 9.2 后端集成测试
|
||||
|
||||
1. 首次注册 -> 下单 -> 验单 -> 入账 60
|
||||
2. 删除账号 -> 同邮箱重注册 -> 新人包不可买
|
||||
3. 退款通知 -> 积分冲正 -> 订单状态更新
|
||||
|
||||
## 9.3 前端集成测试
|
||||
|
||||
1. 套餐接口渲染(替代硬编码)
|
||||
2. 新人包可买/不可买状态切换
|
||||
3. 支付中/成功/失败/退款状态展示
|
||||
|
||||
## 10. 里程碑拆分
|
||||
|
||||
### PR1(数据层)
|
||||
|
||||
- 迁移:新建 `payment_orders`、`payment_order_events`
|
||||
- 迁移:改造 `register_bonus_claims`
|
||||
- 模型与 repository
|
||||
|
||||
### PR2(后端业务)
|
||||
|
||||
- 支付路由 + service
|
||||
- iOS 验单适配层(先 mock)
|
||||
- 订单与积分入账/冲正
|
||||
|
||||
### PR3(前端)
|
||||
|
||||
- 套餐改 API 驱动
|
||||
- 新人包购买态与禁用态
|
||||
- 下单/验单交互链路
|
||||
|
||||
### PR4(联调与验证)
|
||||
|
||||
- 使用集成测试回归全流程
|
||||
- Apple 账号就绪后切换真实 verifier
|
||||
|
||||
## 11. 变更类型判定
|
||||
|
||||
这是 **新 Feature**,不是现有功能的小修补。
|
||||
|
||||
理由:
|
||||
|
||||
1. 引入了新的支付域模型和事件审计。
|
||||
2. 引入了新的后端支付路由与验单流程。
|
||||
3. 前端从静态展示升级为可交易流程。
|
||||
4. 增加了退款冲正与 iOS 回调处理能力。
|
||||
@@ -0,0 +1,784 @@
|
||||
# 通知系统实现方案
|
||||
|
||||
> 创建时间:2026-04-10
|
||||
> 状态:已评审
|
||||
|
||||
## 1. 需求理解
|
||||
|
||||
### 1.1 核心问题
|
||||
当前项目(觅爻签问 App)没有通知系统,需要从零建设。核心需求包括:
|
||||
- 通知中心(通知列表/收件箱)
|
||||
- 用户通知存储与读取
|
||||
- 已读/已看状态追踪
|
||||
- 系统推送触达(APNs/FCM)
|
||||
- 前台实时通知同步
|
||||
- 新版本通知、活动通知等业务通知类型
|
||||
|
||||
### 1.2 通知类型区分
|
||||
|
||||
| 类型 | 说明 | 当前状态 |
|
||||
|------|------|----------|
|
||||
| 应用内通知记录 | 存储在 DB 的通知数据 | **不存在** |
|
||||
| 系统推送通知 | 通过 APNs/FCM 发送 | **不存在** |
|
||||
| 前台实时同步 | App 打开时实时拉取 | **不存在** |
|
||||
| 本地通知 | App 本地触发的通知 | **不存在** |
|
||||
| 已看状态 | 用户是否打开过通知详情 | **未设计** |
|
||||
| 已读状态 | 用户是否标记为已读 | **未设计** |
|
||||
| 推送触达 | 消息是否成功送达设备 | **未设计** |
|
||||
|
||||
### 1.3 系统边界
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Flutter App │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
|
||||
│ │ 本地通知 │ │ 推送接收 │ │ 通知中心 │ │ Badge │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │
|
||||
└───────────────┬─────────────────────┬───────────────────┘
|
||||
│ │
|
||||
│ REST API │ Supabase Realtime
|
||||
▼ ▼
|
||||
┌───────────────────────────────────────────────────────────┐
|
||||
│ Backend (FastAPI) │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌────────────────────┐ │
|
||||
│ │ 通知写入 │ │ Fanout │ │ 推送发送 (APNs/FCM)│ │
|
||||
│ └────────────┘ └────────────┘ └────────────────────┘ │
|
||||
└───────────────┬─────────────────────┬───────────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────────┐ ┌─────────────────────────────┐
|
||||
│ PostgreSQL (Supabase) │ │ Redis / Supabase │
|
||||
│ ┌──────────────────┐ │ │ Realtime │
|
||||
│ │ notifications │ │ │ ┌─────────────────────┐ │
|
||||
│ │ notification_ │ │ │ │ user:{id}:notif:new │ │
|
||||
│ │ receipts │ │ │ └─────────────────────┘ │
|
||||
│ │ user_push_ │ │ └─────────────────────────────┘
|
||||
│ │ devices │ │
|
||||
│ └──────────────────┘ │
|
||||
└─────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ Push Provider │
|
||||
│ (APNs / FCM / 备用) │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 现有代码调研结果
|
||||
|
||||
### 2.1 已发现的相关模块
|
||||
|
||||
#### Flutter 端
|
||||
|
||||
| 路径 | 说明 |
|
||||
|------|------|
|
||||
| `apps/lib/features/settings/presentation/screens/privacy_notification_settings_screen.dart` | 通知设置页面(占位 UI) |
|
||||
| `apps/lib/features/settings/data/models/profile_settings.dart` | `NotificationSettings`(`allowNotifications`, `allowVibration`) |
|
||||
| `apps/lib/features/home/presentation/screens/home_screen.dart:204` | 通知图标(点击显示 `featurePending`) |
|
||||
|
||||
#### 后端
|
||||
|
||||
| 路径 | 说明 |
|
||||
|------|------|
|
||||
| `backend/src/schemas/shared/user.py:44` | `NotificationSettings` schema |
|
||||
| `backend/src/v1/users/service.py` | 用户服务(含 settings 更新) |
|
||||
| `backend/src/v1/users/router.py` | 用户路由 |
|
||||
| `backend/src/services/base/supabase.py` | Supabase 服务封装 |
|
||||
| `backend/src/core/config/settings.py` | 配置管理 |
|
||||
|
||||
#### 数据库迁移
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `backend/alembic/versions/20260403_0002_user_points_chat_schema.py` | profiles, sessions, messages 表 |
|
||||
| `backend/alembic/versions/20260407_0001_update_notification_settings.py` | 通知设置默认值 |
|
||||
|
||||
### 2.2 已有能力
|
||||
|
||||
1. **Profile + Settings 系统**:`profiles.settings` 是 JSONB,可扩展存储通知偏好
|
||||
2. **用户认证**:Supabase Auth 完整
|
||||
3. **数据库迁移框架**:Alembic 已建立
|
||||
4. **后台任务**:Taskiq (基于 Redis)
|
||||
5. **日志系统**:已集成 structlog
|
||||
6. **API 模式**:Pydantic schemas, RFC7807 错误格式
|
||||
|
||||
### 2.3 当前缺口
|
||||
|
||||
| 缺口 | 说明 |
|
||||
|------|------|
|
||||
| 通知数据模型 | 无 `notifications` 表 |
|
||||
| 推送设备管理 | 无 `user_push_devices` 表 |
|
||||
| 通知状态追踪 | 无 `user_notifications` 状态字段体系 |
|
||||
| 推送服务集成 | 无 APNs/FCM/备用推送集成 |
|
||||
| Flutter 通知中心 | 无通知列表页面 |
|
||||
| Flutter 推送接收 | 无 firebase_messaging 等依赖 |
|
||||
| Supabase Realtime | 未用于通知同步 |
|
||||
| 未读数 Badge | 未实现 |
|
||||
|
||||
### 2.4 潜在冲突或风险
|
||||
|
||||
1. **AGENTS.md 约束**:`apps/AGENTS.md` 明确要求通知相关代码放在 `core/notification/` 和 `shared/widgets/notification/`,需要遵循
|
||||
2. **现有 Settings 结构**:`NotificationSettings` 只有两个布尔字段,未来需要扩展
|
||||
3. **数据库 JSONB 查询**:`profiles.settings` 使用 GIN 索引,但通知列表不适合放 JSONB
|
||||
|
||||
---
|
||||
|
||||
## 3. 当前架构判断
|
||||
|
||||
### 3.1 推荐架构:混合推送 + 实时同步(Broadcast 主通道)
|
||||
|
||||
**推荐方案:Supabase Realtime Broadcast(前台)+ APNs/FCM(后台/离线)**
|
||||
|
||||
**原因**:
|
||||
1. App 打开时(前台):使用 Broadcast 推送状态变化,避免直接依赖 `postgres_changes` 在高并发下的 RLS 扫描压力
|
||||
2. App 关闭时(后台/离线):通过 APNs/FCM 触达设备
|
||||
3. 两条链路统一写入 `user_notifications`,状态由服务端收敛,客户端只上报回执
|
||||
|
||||
**不适合当前项目的方案**:
|
||||
|
||||
| 方案 | 不适合原因 |
|
||||
|------|------------|
|
||||
| 纯轮询 | 电量/服务器压力大,不优雅 |
|
||||
| WebSocket 直连 | 需要自己维护连接,复杂度高 |
|
||||
| 仅本地通知 | 无法触达离线用户 |
|
||||
| 仅 APNs/FCM | 无法查看历史通知列表 |
|
||||
|
||||
### 3.2 职责划分
|
||||
|
||||
| 组件 | 职责 |
|
||||
|------|------|
|
||||
| `notifications` 表 | 存储通知内容,永久记录 |
|
||||
| `user_notifications` 表 | 追踪每个用户对每条通知的状态 |
|
||||
| `notification_push_attempts` 表 | 追踪推送每次尝试和失败原因 |
|
||||
| `user_push_devices` 表 | 存储设备 token |
|
||||
| Supabase Realtime | 前台实时通知同步 |
|
||||
| APNs/FCM | 后台/离线推送 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 推荐实现方案
|
||||
|
||||
### 4.1 数据模型设计
|
||||
|
||||
#### 4.1.1 `notifications` 表
|
||||
|
||||
```sql
|
||||
CREATE TABLE notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
type VARCHAR(32) NOT NULL, -- 'system'|'activity'|'version'|'social'
|
||||
priority SMALLINT DEFAULT 0, -- 0=normal, 1=high
|
||||
title TEXT NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
data JSONB, -- 透传数据(deeplink 等)
|
||||
action_url TEXT,
|
||||
expires_at TIMESTAMPTZ, -- 过期时间,NULL=永不过期
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX ix_notifications_type ON notifications(type);
|
||||
CREATE INDEX ix_notifications_created_at ON notifications(created_at DESC);
|
||||
```
|
||||
|
||||
#### 4.1.2 `user_notifications` 表(用户通知记录 + 状态)
|
||||
|
||||
```sql
|
||||
CREATE TABLE user_notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
notification_id UUID NOT NULL REFERENCES notifications(id) ON DELETE CASCADE,
|
||||
dedupe_key TEXT NOT NULL, -- 幂等键(建议: campaign_id:user_id 或业务事件ID)
|
||||
|
||||
-- 状态字段
|
||||
is_read BOOLEAN DEFAULT FALSE, -- 已读(点击进入详情)
|
||||
is_seen BOOLEAN DEFAULT FALSE, -- 已看(列表中曝光)
|
||||
is_opened BOOLEAN DEFAULT FALSE, -- 从推送打开(客户端回执)
|
||||
read_at TIMESTAMPTZ,
|
||||
seen_at TIMESTAMPTZ,
|
||||
opened_at TIMESTAMPTZ,
|
||||
|
||||
-- 推送状态(服务端可验证状态,不直接使用 delivered 语义)
|
||||
push_state SMALLINT DEFAULT 0, -- 0=queued, 1=sent, 2=provider_ack, 3=failed
|
||||
push_provider VARCHAR(16), -- apns|fcm
|
||||
push_error_code TEXT,
|
||||
push_sent_at TIMESTAMPTZ,
|
||||
push_provider_ack_at TIMESTAMPTZ,
|
||||
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
|
||||
UNIQUE(user_id, dedupe_key)
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX ix_user_notifications_user_id ON user_notifications(user_id);
|
||||
CREATE INDEX ix_user_notifications_user_unread ON user_notifications(user_id, is_read) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX ix_user_notifications_created_at ON user_notifications(created_at DESC);
|
||||
CREATE INDEX ix_user_notifications_user_seen ON user_notifications(user_id, is_seen) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX ix_user_notifications_push_state ON user_notifications(push_state) WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
#### 4.1.3 `user_push_devices` 表
|
||||
|
||||
```sql
|
||||
CREATE TABLE user_push_devices (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
device_token TEXT NOT NULL, -- APNs/FCM token
|
||||
device_type VARCHAR(16) NOT NULL, -- 'ios'|'android'
|
||||
push_provider VARCHAR(32) NOT NULL, -- 'apns'|'fcm'|'huawei'|'xiaomi'
|
||||
app_version TEXT,
|
||||
locale VARCHAR(16),
|
||||
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
last_used_at TIMESTAMPTZ DEFAULT now(),
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
|
||||
UNIQUE(user_id, device_token, push_provider)
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX ix_user_push_devices_user_id ON user_push_devices(user_id);
|
||||
CREATE INDEX ix_user_push_devices_token ON user_push_devices(device_token);
|
||||
CREATE INDEX ix_user_push_devices_active ON user_push_devices(user_id, is_active) WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
#### 4.1.4 `notification_push_attempts` 表(推送尝试日志)
|
||||
|
||||
```sql
|
||||
CREATE TABLE notification_push_attempts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_notification_id UUID NOT NULL REFERENCES user_notifications(id) ON DELETE CASCADE,
|
||||
provider VARCHAR(16) NOT NULL, -- apns|fcm
|
||||
attempt_no SMALLINT NOT NULL,
|
||||
request_id TEXT,
|
||||
result SMALLINT NOT NULL, -- 0=sent, 1=provider_ack, 2=failed, 3=timeout
|
||||
error_code TEXT,
|
||||
error_detail TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX ix_notification_push_attempts_un_id ON notification_push_attempts(user_notification_id, created_at DESC);
|
||||
```
|
||||
|
||||
#### 4.1.5 状态字段说明
|
||||
|
||||
| 字段 | 含义 | 触发条件 |
|
||||
|------|------|----------|
|
||||
| `is_seen` | 用户在列表中看到该通知 | 列表滚动曝光(停留 > 1秒)或拉取 |
|
||||
| `is_read` | 用户点击进入详情 | 点击通知项 |
|
||||
| `is_opened` | 用户从系统通知打开 App | 客户端上报 open 回执 |
|
||||
| `push_state=sent` | 推送请求已发出 | 服务端调用 APNs/FCM 成功返回 |
|
||||
| `push_state=provider_ack` | 平台确认接收 | APNs/FCM Provider 级确认 |
|
||||
| `push_state=failed` | 推送失败 | 服务端重试耗尽或不可恢复错误 |
|
||||
|
||||
> 注意:`provider_ack` 不等价于“用户已看到通知”。用户可见性必须以 `is_opened`/`is_seen`/`is_read` 为准。
|
||||
|
||||
### 4.2 后端 API 设计
|
||||
|
||||
#### 4.2.1 通知路由 (`backend/src/v1/notifications/`)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/v1/notifications` | 获取当前用户通知列表 |
|
||||
| GET | `/api/v1/notifications/unread-count` | 获取未读数 |
|
||||
| PATCH | `/api/v1/notifications/{id}/seen` | 标记为已看 |
|
||||
| PATCH | `/api/v1/notifications/{id}/read` | 标记为已读 |
|
||||
| PATCH | `/api/v1/notifications/mark-all-read` | 全部已读 |
|
||||
| DELETE | `/api/v1/notifications/{id}` | 删除通知 |
|
||||
|
||||
#### 4.2.2 设备路由 (`backend/src/v1/push/`)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | `/api/v1/push/devices` | 注册/更新设备 token |
|
||||
| DELETE | `/api/v1/push/devices/{id}` | 删除设备 |
|
||||
| GET | `/api/v1/push/devices` | 获取用户设备列表 |
|
||||
|
||||
### 4.3 Flutter 端设计
|
||||
|
||||
#### 4.3.1 目录结构(遵循 AGENTS.md)
|
||||
|
||||
```
|
||||
apps/lib/
|
||||
├── core/
|
||||
│ └── notification/ # NEW
|
||||
│ ├── models/
|
||||
│ │ ├── notification.dart
|
||||
│ │ └── push_device.dart
|
||||
│ ├── services/
|
||||
│ │ ├── notification_service.dart
|
||||
│ │ └── push_service.dart
|
||||
│ └── repositories/
|
||||
│ └── notification_repository.dart
|
||||
├── features/
|
||||
│ └── notifications/ # NEW
|
||||
│ ├── data/
|
||||
│ │ ├── apis/notification_api.dart
|
||||
│ │ └── repositories/notification_repository_impl.dart
|
||||
│ ├── presentation/
|
||||
│ │ ├── screens/notification_center_screen.dart
|
||||
│ │ ├── widgets/notification_item.dart
|
||||
│ │ └── bloc/notification_bloc.dart
|
||||
├── shared/
|
||||
│ └── widgets/
|
||||
│ └── notification/ # NEW
|
||||
│ ├── notification_badge.dart
|
||||
│ └── notification_toast.dart
|
||||
```
|
||||
|
||||
#### 4.3.2 通知中心流程
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ App 打开 / HomeScreen │
|
||||
│ 1. 初始化时连接 Supabase Realtime 私有频道 │
|
||||
│ 2. 监听 Broadcast 通知事件 │
|
||||
│ 3. 实时更新本地列表和未读数 Badge │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ NotificationCenterScreen │
|
||||
│ 1. 首次进入 → 调用 GET /api/v1/notifications │
|
||||
│ 2. 滚动曝光 → 调用 PATCH /api/v1/notifications/{id}/seen │
|
||||
│ 3. 点击通知 → 调用 PATCH /api/v1/notifications/{id}/read │
|
||||
│ 4. 下拉刷新 → 重新拉取列表 │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.4 实时同步方案
|
||||
|
||||
**Supabase Realtime 职责**:
|
||||
- 当用户在另一设备读取通知时,当前设备实时更新 `is_read` / `is_seen` / `is_opened` 状态
|
||||
- 仅做前台状态分发,不承担离线触达
|
||||
|
||||
**推荐通道:Broadcast(私有频道)**
|
||||
|
||||
```text
|
||||
DB update(user_notifications)
|
||||
-> trigger 调用 realtime.broadcast_changes(...)
|
||||
-> channel: user:{user_id}:notifications
|
||||
-> Flutter 收到事件并更新本地缓存
|
||||
```
|
||||
|
||||
**为什么不用 postgres_changes 作为主通道**:
|
||||
- 在订阅规模扩大时,`postgres_changes` 会放大 RLS 评估开销
|
||||
- Broadcast 在通知场景下更可控,可按用户私有频道精准下发
|
||||
|
||||
```dart
|
||||
final channel = supabase.channel(
|
||||
'user:${userId}:notifications',
|
||||
opts: const RealtimeChannelConfig(private: true),
|
||||
);
|
||||
|
||||
channel.onBroadcast(
|
||||
event: 'notification_changed',
|
||||
callback: (payload) {
|
||||
// 更新本地状态和 Badge
|
||||
},
|
||||
).subscribe();
|
||||
```
|
||||
|
||||
### 4.5 推送发送流程(Outbox + Worker)
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||
│ 业务触发 │────▶│ DB 事务写入 │────▶│ push_outbox │
|
||||
│ (后台任务) │ │ notifications + │ │ pending 事件 │
|
||||
└─────────────┘ │ user_notifications│ └────────┬────────┘
|
||||
└──────────────────┘ │
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Fanout Worker │
|
||||
│ 读取 outbox │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌─────────────────────────────────┼─────────────────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Broadcast │ │ APNs / FCM │ │ 重试 / DLQ │
|
||||
│ 前台增量同步 │ │ 离线触达 │ │ 指数退避 │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
### 4.6 幂等、重试与失败处理
|
||||
|
||||
#### 4.6.1 幂等策略
|
||||
|
||||
- 每次业务发通知必须携带 `dedupe_key`
|
||||
- `UNIQUE(user_id, dedupe_key)` 保证重复投递不会生成多条记录
|
||||
- `PATCH /read`、`PATCH /seen` 必须幂等,重复调用返回 200 且不重复更新统计
|
||||
|
||||
#### 4.6.2 重试策略
|
||||
|
||||
- 可重试错误:超时、5xx、429
|
||||
- 不可重试错误:token invalid、payload invalid、认证失败
|
||||
- 退避策略:`30s -> 2m -> 10m -> 1h`,最多 4 次
|
||||
- 超过上限进入 DLQ,由运维任务定时重放或人工处理
|
||||
|
||||
#### 4.6.3 失败模式清单
|
||||
|
||||
| 场景 | 风险 | 方案 |
|
||||
|------|------|------|
|
||||
| 推送平台短时不可用 | 批量失败 | Outbox + 指数退避 + DLQ |
|
||||
| 客户端重复上报 read | 状态抖动/统计偏差 | 幂等更新 + 仅首次写时间戳 |
|
||||
| 多设备并发 read/seen | 最终状态不一致 | 单条记录原子更新 + `updated_at` 冲突检测 |
|
||||
| Realtime 断连 | UI 未及时同步 | 前台重连后触发增量拉取(按 `updated_at`) |
|
||||
|
||||
### 4.7 版本通知与活动通知
|
||||
|
||||
#### 4.7.1 版本通知
|
||||
|
||||
- 触发条件:用户登录时检测到 App 版本低于最新版本
|
||||
- 写入方式:后台任务扫描所有用户,批量写入 `user_notifications`
|
||||
- `notification.type = 'version'`
|
||||
|
||||
#### 4.7.2 活动通知
|
||||
|
||||
- 触发条件:运营后台或定时任务触发
|
||||
- 目标用户:可按标签/行为筛选
|
||||
- `notification.type = 'activity'`
|
||||
|
||||
### 4.8 权限与安全策略
|
||||
|
||||
#### RLS 设计
|
||||
|
||||
```sql
|
||||
-- user_notifications
|
||||
ALTER TABLE user_notifications ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY auth_select ON user_notifications FOR SELECT
|
||||
USING (auth.uid() = user_id AND deleted_at IS NULL);
|
||||
|
||||
CREATE POLICY auth_update ON user_notifications FOR UPDATE
|
||||
USING (auth.uid() = user_id)
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- user_push_devices
|
||||
ALTER TABLE user_push_devices ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY auth_all ON user_push_devices FOR ALL
|
||||
USING (auth.uid() = user_id AND deleted_at IS NULL)
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
```
|
||||
|
||||
#### 信任边界
|
||||
|
||||
- 客户端不能创建通知主记录,通知内容仅由服务端写入
|
||||
- 客户端只能操作自己的 `seen/read/open` 回执和设备 token
|
||||
- `owner_id/user_id` 必须来自 JWT `sub`,禁止客户端传入
|
||||
- 管理后台发通知接口必须走服务角色鉴权,不暴露给普通用户 token
|
||||
|
||||
### 4.9 可观测性与运行指标
|
||||
|
||||
| 指标 | 目标/SLO | 备注 |
|
||||
|------|----------|------|
|
||||
| 通知写入成功率 | >= 99.9% | 5 分钟窗口 |
|
||||
| 推送发送成功率(sent) | >= 99.5% | 按 provider 分维度 |
|
||||
| 推送 provider_ack 延迟 P95 | < 3s | 仅平台确认链路 |
|
||||
| 前台实时同步延迟 P95 | < 1s | Broadcast 收包到 UI 更新 |
|
||||
| read API P95 | < 150ms | 排除网络抖动 |
|
||||
|
||||
日志要求:每次通知发送链路打通 `trace_id`(创建 -> outbox -> provider -> 回执)。
|
||||
|
||||
### 4.10 NOT in scope(本阶段不做)
|
||||
|
||||
- 厂商通道(小米/华为)深度集成
|
||||
- 通知模板多语言管理后台
|
||||
- 通知 A/B 实验平台
|
||||
- 全量历史归档/冷热分层
|
||||
|
||||
---
|
||||
|
||||
## 5. 分阶段落地计划
|
||||
|
||||
### 第一阶段:最小可用通知中心(MVP 收敛)
|
||||
|
||||
**目标**:在 App 内显示通知列表,支持标记已读
|
||||
|
||||
**改动范围**:
|
||||
- Backend: `notifications` 表 + API
|
||||
- Flutter: 通知中心页面 + Repository
|
||||
|
||||
**主要任务**:
|
||||
1. 创建 `notifications` 和 `user_notifications` 表
|
||||
2. 实现 `GET /api/v1/notifications` API
|
||||
3. 实现 `PATCH /api/v1/notifications/{id}/read` API
|
||||
4. Flutter 通知中心页面(列表 + 标记已读)
|
||||
5. 未读数 Badge(在 HomeScreen 通知图标上显示红点)
|
||||
6. 完成协议文档:`docs/protocols/notification/notification-protocol.md`
|
||||
7. 完成基础测试(见第 7 节)
|
||||
|
||||
**依赖项**:无
|
||||
|
||||
**风险点**:
|
||||
- 无推送,通知需要后台手动写入 DB
|
||||
- 不支持实时同步,多设备体验差
|
||||
|
||||
**验收标准**:
|
||||
- [ ] 能看到通知列表
|
||||
- [ ] 点击通知能标记为已读
|
||||
- [ ] 未读通知有 Badge 提示
|
||||
- [ ] 已读/未读状态在下拉刷新后正确
|
||||
- [ ] 越权访问被拒绝(用户 A 无法读写用户 B 通知)
|
||||
- [ ] read 接口重复调用幂等
|
||||
|
||||
---
|
||||
|
||||
### 第二阶段:接入系统推送
|
||||
|
||||
**目标**:支持离线推送通知
|
||||
|
||||
**改动范围**:
|
||||
- Backend: `user_push_devices` 表 + Push Service
|
||||
- Flutter: 推送接收 + 设备注册
|
||||
|
||||
**主要任务**:
|
||||
1. 创建 `user_push_devices` 表
|
||||
2. 实现 `POST /api/v1/push/devices` API(注册 token)
|
||||
3. 集成 firebase_messaging(Android)/ apns(iOS)
|
||||
4. 实现 Push Sending Service(APNs/FCM SDK)+ Outbox Worker
|
||||
5. Flutter 端:获取 FCM/APNs token 并注册
|
||||
6. 服务端发送后更新 `push_state`,客户端仅上报 open/read/seen 回执
|
||||
7. 落地重试与 DLQ
|
||||
|
||||
**依赖项**:
|
||||
- 第一阶段完成
|
||||
- Firebase 项目配置(Android)
|
||||
- APNs 证书/Key(iOS)
|
||||
|
||||
**风险点**:
|
||||
- APNs/FCM 配置复杂
|
||||
- 推送送达率不稳定(特别是国内 Android)
|
||||
|
||||
**验收标准**:
|
||||
- [ ] 离线设备能收到推送
|
||||
- [ ] 点击推送能打开对应通知详情
|
||||
- [ ] 推送状态(sent/provider_ack/failed)正确回写
|
||||
- [ ] provider 短时故障时消息可自动重试并可追踪
|
||||
|
||||
---
|
||||
|
||||
### 第三阶段:实时同步与统计
|
||||
|
||||
**目标**:多设备实时同步 + 完整状态追踪
|
||||
|
||||
**改动范围**:
|
||||
- Backend: Supabase Realtime + 状态统计
|
||||
- Flutter: Realtime 订阅 + 曝光追踪
|
||||
|
||||
**主要任务**:
|
||||
1. Supabase Realtime Broadcast 私有频道接入
|
||||
2. 实现 `seen` 状态(曝光追踪)
|
||||
3. 添加统计接口(送达率、点击率)
|
||||
4. Flutter 端:列表滚动时标记 `seen`
|
||||
5. 版本通知、活动通知的后台写入逻辑
|
||||
6. 断线重连后的增量拉取机制(按 `updated_at`)
|
||||
|
||||
**依赖项**:
|
||||
- 第二阶段完成
|
||||
- Supabase Realtime 已启用
|
||||
|
||||
**风险点**:
|
||||
- Realtime 连接数限制(根据 plan)
|
||||
- 曝光追踪可能影响性能
|
||||
|
||||
**验收标准**:
|
||||
- [ ] 一设备读取通知,另一设备实时更新
|
||||
- [ ] 列表曝光能正确标记 `seen`
|
||||
- [ ] 能查看送达/点击统计
|
||||
- [ ] Realtime 断连恢复后不丢状态更新
|
||||
|
||||
---
|
||||
|
||||
## 6. 建议改动清单
|
||||
|
||||
### 6.1 新增/修改的表
|
||||
|
||||
| 表名 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `notifications` | 新增 | 通知模板表 |
|
||||
| `user_notifications` | 新增 | 用户通知记录+状态 |
|
||||
| `user_push_devices` | 新增 | 设备 token 存储 |
|
||||
| `notification_push_attempts` | 新增 | 推送尝试日志 |
|
||||
| `profiles` | 修改 | 暂不改,优先在线计算 unread_count |
|
||||
|
||||
### 6.2 新增后端模块
|
||||
|
||||
| 路径 | 说明 |
|
||||
|------|------|
|
||||
| `backend/src/v1/notifications/` | 通知路由+服务+仓库 |
|
||||
| `backend/src/v1/push/` | 推送设备路由+服务 |
|
||||
| `backend/src/services/push/` | APNs/FCM 发送服务 |
|
||||
| `backend/src/services/push/outbox_worker.py` | 推送 outbox 消费与重试 |
|
||||
| `backend/src/models/notification.py` | 通知 ORM 模型 |
|
||||
| `backend/src/models/notification_push_attempt.py` | 推送尝试日志模型 |
|
||||
| `backend/src/schemas/notification.py` | Pydantic schemas |
|
||||
|
||||
### 6.3 新增 Flutter 模块
|
||||
|
||||
| 路径 | 说明 |
|
||||
|------|------|
|
||||
| `apps/lib/core/notification/` | 通知核心逻辑 |
|
||||
| `apps/lib/features/notifications/` | 通知功能模块 |
|
||||
| `apps/lib/shared/widgets/notification/` | 通知 UI 组件 |
|
||||
|
||||
### 6.4 新增接口
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/v1/notifications` | 通知列表 |
|
||||
| GET | `/api/v1/notifications/unread-count` | 未读数 |
|
||||
| PATCH | `/api/v1/notifications/{id}/seen` | 标记已看 |
|
||||
| PATCH | `/api/v1/notifications/{id}/read` | 标记已读 |
|
||||
| PATCH | `/api/v1/notifications/mark-all-read` | 全部已读 |
|
||||
| DELETE | `/api/v1/notifications/{id}` | 删除通知 |
|
||||
| POST | `/api/v1/notifications/{id}/opened` | 上报从推送打开 |
|
||||
| POST | `/api/v1/push/devices` | 注册设备 |
|
||||
| DELETE | `/api/v1/push/devices/{id}` | 删除设备 |
|
||||
|
||||
### 6.5 新增配置项
|
||||
|
||||
```env
|
||||
# .env
|
||||
ERYAO_PUSH__APNS_KEY_ID=xxx
|
||||
ERYAO_PUSH__APNS_TEAM_ID=xxx
|
||||
ERYAO_PUSH__APNS_KEY_PATH=/path/to/key.p8
|
||||
ERYAO_PUSH__FCM_SERVER_KEY=xxx
|
||||
ERYAO_PUSH__ENABLED=true
|
||||
```
|
||||
|
||||
### 6.6 Flutter 依赖(pubspec.yaml)
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
firebase_messaging: ^15.0.0 # FCM
|
||||
flutter_local_notifications: ^18.0.0 # 本地通知
|
||||
supabase_flutter: ^2.5.0 # Supabase 客户端(含 Realtime)
|
||||
```
|
||||
|
||||
### 6.7 协议文档(先于实现)
|
||||
|
||||
```text
|
||||
docs/protocols/notification/
|
||||
├── notification-protocol.md # 数据模型、字段语义、兼容策略
|
||||
├── notification-api-protocol.md # API 请求/响应、错误码
|
||||
└── notification-realtime-protocol.md # Broadcast payload 与版本约束
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试策略(必须随阶段落地)
|
||||
|
||||
### 7.1 测试框架
|
||||
|
||||
- 后端:`pytest` + `pytest-asyncio`(见 `pyproject.toml`)
|
||||
- Flutter:单测 + Widget 测试 + 关键链路集成测试
|
||||
|
||||
### 7.2 覆盖图(关键分支)
|
||||
|
||||
```text
|
||||
Create Notification
|
||||
-> validate payload
|
||||
-> invalid type [unit]
|
||||
-> expires_at in past [unit]
|
||||
-> fanout target users
|
||||
-> empty target set [unit]
|
||||
-> partial user write failed [integration]
|
||||
-> write user_notifications (dedupe)
|
||||
-> first write success [unit]
|
||||
-> duplicate dedupe_key [unit]
|
||||
-> enqueue outbox
|
||||
-> enqueue success [integration]
|
||||
-> enqueue failed rollback [integration]
|
||||
|
||||
Push Worker
|
||||
-> send provider
|
||||
-> sent/provider_ack [integration]
|
||||
-> timeout retry [integration]
|
||||
-> permanent failure to DLQ [integration]
|
||||
|
||||
User Callback
|
||||
-> PATCH seen/read/opened
|
||||
-> own record success [api]
|
||||
-> cross-user forbidden [api/security]
|
||||
-> repeat call idempotent [api]
|
||||
|
||||
Realtime
|
||||
-> broadcast receive update [integration]
|
||||
-> reconnect + incremental pull [e2e]
|
||||
```
|
||||
|
||||
### 7.3 最低覆盖要求
|
||||
|
||||
- 新增后端核心分支(通知创建、状态更新、重试)语句覆盖率 >= 90%
|
||||
- 安全相关路径(越权、伪造 user_id、非法 token)必须 100% 有测试
|
||||
- 每个阶段必须包含至少 1 条回归测试,防止旧行为被破坏
|
||||
|
||||
### 7.4 回归测试强制项
|
||||
|
||||
1. 同一通知重复发送不应生成重复 `user_notifications`
|
||||
2. `mark-all-read` 与单条 `read` 并发时,最终状态一致
|
||||
3. Realtime 中断后恢复,未读数与服务端一致
|
||||
4. 无效/过期 token 不应导致消息丢失(进入失败态并可重试)
|
||||
|
||||
---
|
||||
|
||||
## 8. 最终推荐
|
||||
|
||||
### 8.1 推荐总体方案
|
||||
|
||||
**采用 Supabase Realtime Broadcast(前台)+ APNs/FCM(后台/离线)的混合方案**
|
||||
|
||||
### 8.2 推荐原因
|
||||
|
||||
1. **用户体验最优**:前台实时同步,多设备状态一致
|
||||
2. **离线可达**:通过 APNs/FCM 触达离线用户
|
||||
3. **扩展性更稳**:Broadcast 规避高并发下 `postgres_changes` 的权限扫描放大
|
||||
4. **状态语义更准确**:`provider_ack` 与用户可见状态分离,统计更可信
|
||||
5. **项目适配**:Supabase 已在用,Realtime 只是扩展使用
|
||||
|
||||
### 8.3 不确定点
|
||||
|
||||
| 问题 | 推断依据 |
|
||||
|------|----------|
|
||||
| 是否已有 Firebase 项目 | 未在代码库中找到 `google-services.json` 或 Firebase 配置 |
|
||||
| 是否已有 APNs 证书 | 未在代码库中找到证书文件 |
|
||||
| 国内 Android 推送需求 | 国内 Android ROM 需要厂商通道(如小米、华为),建议预留 |
|
||||
|
||||
### 8.4 实施优先级排序
|
||||
|
||||
| 优先级 | 阶段 | 说明 |
|
||||
|--------|------|------|
|
||||
| **P0** | 第一阶段 | MVP:通知列表 + 标记已读 |
|
||||
| **P1** | 第一阶段 | 未读数 Badge |
|
||||
| **P2** | 第二阶段 | 推送接入(Android FCM 先,iOS APNs 后) |
|
||||
| **P3** | 第三阶段 | Realtime 同步 |
|
||||
| **P4** | 第三阶段 | 曝光追踪 + 统计 |
|
||||
|
||||
### 8.5 关键约束提醒
|
||||
|
||||
1. **遵循 AGENTS.md**:通知代码放在 `core/notification/` 和 `shared/widgets/notification/`
|
||||
2. **Error Swallowing**:所有异常必须传播,禁止静默捕获
|
||||
3. **Protocol 先行**:新建通知相关协议文档在 `docs/protocols/notification/`
|
||||
4. **渐进演进**:不废弃现有 `NotificationSettings`,而是扩展它
|
||||
|
||||
---
|
||||
|
||||
## 附录:推断依据
|
||||
|
||||
| 信息点 | 推断依据 |
|
||||
|--------|----------|
|
||||
| 无通知表 | 所有 migration 文件中均未发现 `notifications` 相关表 |
|
||||
| 无推送集成 | `pubspec.yaml` 无 firebase_messaging / apns 相关依赖 |
|
||||
| 无 Realtime 通知 | Supabase Service 仅用于 Storage,未配置 Realtime |
|
||||
| AGENTS.md 约束 | `apps/AGENTS.md` 明确提到 "Reminder/Notification Rewrite Boundary" |
|
||||
| 推送服务配置 | `settings.py` 无任何 APNs/FCM 相关配置 |
|
||||
@@ -1,131 +0,0 @@
|
||||
你是一个资深系统设计与代码分析助手。你的任务不是立刻编写代码,而是先深入理解当前项目中与“通知系统”相关的现有实现,然后基于现有代码结构输出一份可靠、可落地的实现方案。
|
||||
|
||||
本次任务聚焦于 App 通知系统设计,重点包括但不限于:
|
||||
- 通知中心(notification list / inbox)
|
||||
- 用户通知存储
|
||||
- 已读 / 已看状态
|
||||
- 推送触达状态
|
||||
- 前台实时通知
|
||||
- 后台/离线系统推送
|
||||
- 新版本通知、活动通知等业务通知类型
|
||||
- Flutter 客户端、后端服务、Supabase 之间的协作方式
|
||||
|
||||
你的首要目标是“理解现状并制定方案”,而不是直接进入编码。
|
||||
|
||||
请严格遵循以下工作方式:
|
||||
|
||||
1. 先理解现有代码,再做设计
|
||||
- 主动查找项目中与通知相关的现有代码、模块、接口、表结构、状态管理、服务封装和配置文件。
|
||||
- 特别关注以下内容:
|
||||
- Flutter 端是否已有本地通知、消息中心、badge、页面入口、深链跳转
|
||||
- 后端是否已有 notification / message / event / push / reminder 等相关模型、接口或 service
|
||||
- Supabase 中是否已有相关表、RLS、Realtime、设备 token 存储
|
||||
- 是否已有 APNs / FCM / flutter_local_notifications / Firebase Messaging / Supabase Realtime 等集成痕迹
|
||||
- 是否已有版本检查机制、活动通知机制、用户收件箱机制
|
||||
- 不要假设项目是空白的。必须优先复用现有架构与已有能力。
|
||||
|
||||
2. 基于现有架构设计,而不是脱离项目另起炉灶
|
||||
- 方案必须尽量贴合当前项目的技术栈、目录结构、分层方式、命名风格和已有约束。
|
||||
- 优先考虑如何在现有模块上扩展,而不是重新设计一整套无关架构。
|
||||
- 如果当前实现存在明显缺陷或冲突,可以指出问题,但仍要给出“在现有基础上渐进演进”的方案。
|
||||
|
||||
3. 明确区分几个概念
|
||||
你在分析和设计时,必须区分以下概念,避免混淆:
|
||||
- 应用内通知记录
|
||||
- 系统推送通知
|
||||
- 前台实时同步
|
||||
- 本地通知
|
||||
- 已看状态
|
||||
- 已读状态
|
||||
- 推送是否成功
|
||||
- 用户是否真正查看
|
||||
|
||||
4. 方案输出要覆盖的核心问题
|
||||
在最终方案中,至少要回答以下问题:
|
||||
- 现有代码里已经有什么,缺什么
|
||||
- 通知数据应该如何建模
|
||||
- 是否需要 notifications / notification_receipts / user_push_devices 等表
|
||||
- 已读、已看、点击、删除等状态如何设计
|
||||
- Flutter 端如何读取通知列表、显示未读数、更新已读状态
|
||||
- Supabase Realtime 在这个项目里适合承担什么职责
|
||||
- APNs / FCM 或其他推送通道应该如何接入
|
||||
- 后端应该如何组织通知写入、fanout、推送发送、状态回写
|
||||
- 新版本通知与活动通知如何落地
|
||||
- 如何保证权限安全,例如 RLS、用户只能访问自己的通知
|
||||
- 如何分阶段实施,避免一次性改动过大
|
||||
|
||||
5. 输出必须先分析,后给建议
|
||||
不要一上来直接写“建议这样做”。
|
||||
你必须先给出:
|
||||
- 当前代码现状梳理
|
||||
- 已有能力
|
||||
- 缺失点
|
||||
- 架构约束
|
||||
然后再给出推荐方案。
|
||||
|
||||
6. 不直接修改代码
|
||||
- 本轮目标是产出实现方案,而不是直接提交代码。
|
||||
- 除非我明确要求,否则不要直接创建文件、修改代码或生成迁移。
|
||||
- 可以提出建议的文件改动点,但不要直接实现。
|
||||
|
||||
请按以下结构输出:
|
||||
|
||||
# 1. 需求理解
|
||||
- 这次通知系统要解决的核心问题
|
||||
- 涉及的通知类型
|
||||
- 系统边界(Flutter / Backend / Supabase / Push Provider)
|
||||
|
||||
# 2. 现有代码调研结果
|
||||
- 已发现的相关模块
|
||||
- 已有能力
|
||||
- 可复用部分
|
||||
- 当前缺口
|
||||
- 潜在冲突或风险
|
||||
|
||||
# 3. 当前架构判断
|
||||
- 当前项目更适合采用什么通知架构
|
||||
- 为什么
|
||||
- 哪些方案不适合当前项目
|
||||
|
||||
# 4. 推荐实现方案
|
||||
至少包括:
|
||||
- 数据模型设计
|
||||
- 状态字段设计
|
||||
- 客户端交互流程
|
||||
- 服务端处理流程
|
||||
- 实时通知与系统推送的职责划分
|
||||
- 已读/已看/触达状态方案
|
||||
- 版本通知与活动通知方案
|
||||
- 权限与安全策略
|
||||
|
||||
# 5. 分阶段落地计划
|
||||
请拆分为多个阶段,例如:
|
||||
- 第一阶段:最小可用通知中心
|
||||
- 第二阶段:接入系统推送
|
||||
- 第三阶段:完善版本通知/活动通知/统计能力
|
||||
每个阶段说明:
|
||||
- 目标
|
||||
- 改动范围
|
||||
- 主要任务
|
||||
- 依赖项
|
||||
- 风险点
|
||||
- 验收标准
|
||||
|
||||
# 6. 建议改动清单
|
||||
- 建议新增或修改的表
|
||||
- 建议新增或修改的后端模块
|
||||
- 建议新增或修改的 Flutter 模块
|
||||
- 建议新增的接口 / RPC / service
|
||||
- 建议新增的配置项
|
||||
|
||||
# 7. 最终推荐
|
||||
- 推荐采用的总体方案
|
||||
- 推荐原因
|
||||
- 不确定点
|
||||
- 实施优先级排序
|
||||
|
||||
额外要求:
|
||||
- 如果代码库中已经存在通知、提醒、消息、推送等相近实现,优先尝试整合,而不是重复建设。
|
||||
- 如果某些信息无法从当前代码中确认,要明确写出“不确定项”和“推断依据”。
|
||||
- 方案必须可执行、可渐进落地,避免空泛。
|
||||
- 优先给出最贴合当前代码库的设计,不要输出与项目现状脱节的理想化架构。
|
||||
@@ -1,419 +0,0 @@
|
||||
# 积分审计与注册赠分策略改造计划(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. 关键失败有指标与告警,不允许静默失败。
|
||||
Reference in New Issue
Block a user