From fc74720dd92a1ae9f6b7972b747ffbbf29edc04a Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 14 Sep 2026 17:39:09 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20#1895=20=E4=BC=9A=E5=91=98+=E7=A7=AF?= =?UTF-8?q?=E5=88=86=E6=9B=BF=E4=BB=A3=E8=AE=A2=E9=98=85=E5=88=B6=E6=96=B9?= =?UTF-8?q?=E6=A1=88=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/membership-points-design.md | 907 +++++++++++++++++++++++++++++++ 1 file changed, 907 insertions(+) create mode 100644 docs/membership-points-design.md diff --git a/docs/membership-points-design.md b/docs/membership-points-design.md new file mode 100644 index 000000000..21f505f2e --- /dev/null +++ b/docs/membership-points-design.md @@ -0,0 +1,907 @@ +# 会员制 + 积分方案设计文档 + +> **Issue**: [#1895](https://git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas/issues/1895) +> **版本**: v1.0 +> **状态**: 方案设计(待确认) +> **创建时间**: 2026-09-14 + +--- + +## 一、设计背景与目标 + +### 1.1 背景 + +当前系统采用**纯订阅制**计费模式(4 档:free/standard/pro/enterprise),存在以下问题: + +1. **灵活性不足**:用户付费后只能按套餐配额使用,无法按需消费高频 AI 能力 +2. **前后端不一致**:前端 Plans.tsx 写死 3 档,后端硬编码 4 档 +3. **积分能力缺失**:`quota.py` 已预留 `AI_VOICE_CREDITS` 枚举,`module_registry.py` 已注册消耗规则,但无实际账户、流水、扣费和充值链路 +4. **收入天花板**:纯订阅制难以覆盖 AI 调用成本波动,高频用户和低频用户无法差异化变现 + +### 1.2 目标 + +- **会员制**保留基础权益(存储、项目数、并发数等),提供稳定收入 +- **积分制**覆盖 AI 消耗型功能(配音、数字人、视频生成等),按需付费、用多少扣多少 +- 两者结合,既降低轻度用户入门门槛,又提升重度用户 ARPU +- 在现有 `QuotaChecker` + `ModuleRegistry` 架构上扩展,不推倒重来 + +--- + +## 二、会员等级设计 + +### 2.1 等级定义 + +保留 4 档,与现有后端 `PLAN_QUOTAS` 对齐,重新定义权益: + +| 等级 | 月付价格 | 年付价格 | 定位 | +|------|----------|----------|------| +| **免费版(free)** | ¥0 | ¥0 | 体验用户,验证产品价值 | +| **标准版(standard)** | ¥99/月 | ¥999/年(约 ¥83/月) | 个人创作者 | +| **专业版(pro)** | ¥299/月 | ¥2,999/年(约 ¥250/月) | 专业团队 | +| **企业版(enterprise)** | ¥999/月 | ¥9,999/年(约 ¥833/月) | 企业 / 工作室 | + +### 2.2 会员权益对照表 + +| 权益维度 | 免费版 | 标准版 | 专业版 | 企业版 | +|----------|--------|--------|--------|--------| +| 项目数量 | 3 个 | 10 个 | 不限 | 不限 | +| 存储空间 | 10 GB | 50 GB | 100 GB | 1 TB | +| 并发任务数 | 3 | 10 | 20 | 50 | +| 导出分辨率 | 720p | 1080p | 4K | 4K | +| 模板数量 | 3 个 | 15 个 | 不限 | 不限 | +| 标题库 | 50 条 | 500 条 | 500 条 | 不限 | +| 配音库 | 10 条 | 100 条 | 100 条 | 不限 | +| AI 配音 | ✗ | ✓ | ✓ | ✓ | +| 批量导出 | ✗ | ✓ | ✓ | ✓ | +| 多平台发布 | ✗ | ✗ | ✓ | ✓ | +| 去重检测报告 | ✗ | ✗ | ✓ | ✓ | +| **每月赠送积分** | 0 | 200 | 800 | 3,000 | +| 技术支持 | 社区 | 邮件 | 优先响应 | 专属客服 | + +> 注:`AI_VOICE_ENABLED`、`BATCH_EXPORT_ENABLED`、`MULTI_PLATFORM_ENABLED`、`DEDUP_REPORT_ENABLED` 等开关类权益由现有 `QuotaRegistry` 直接控制,无需积分参与。 + +### 2.3 与现有代码的映射 + +- 后端 `PLAN_QUOTAS` 字典扩展,新增 `monthly_credits` 字段 +- 前端 `Plans.tsx` 对齐后端 4 档,与后端保持一致 +- 现有 `QuotaTier.limits` 中扩展 `monthly_credits` 维度,由 `QuotaRegistry` 统一管理 + +--- + +## 三、积分体系设计 + +### 3.1 积分获取方式 + +| 获取方式 | 说明 | 频率 | +|----------|------|------| +| **会员每月赠送** | 各等级每月自动到账(见上表) | 每月 1 日 00:00 自动发放 | +| **单独充值** | 用户按需购买积分包 | 随时可买 | +| **任务奖励** | 完成指定任务赠送(新手引导、邀请好友、反馈 Bug 等) | 一次性 | + +### 3.2 积分包定价 + +| 积分包 | 积分数量 | 价格 | 单价 | 备注 | +|--------|----------|------|------|------| +| 体验包 | 50 积分 | ¥9.9 | ¥0.198/积分 | 首次购买限购 1 次 | +| 基础包 | 200 积分 | ¥36 | ¥0.18/积分 | | +| 标准包 | 500 积分 | ¥80 | ¥0.16/积分 | | +| 专业包 | 1,500 积分 | ¥210 | ¥0.14/积分 | 热门 | +| 企业包 | 5,000 积分 | ¥600 | ¥0.12/积分 | | + +> 积分永久有效,不随会员过期清零。会员过期后停止每月赠送,但已有积分不受影响。 + +### 3.3 任务奖励规则 + +| 任务 | 奖励积分 | 次数限制 | +|------|----------|----------| +| 新用户注册 | 50 | 1 次 | +| 完善个人信息 | 20 | 1 次 | +| 邀请新用户注册 | 30/人 | 每月限 10 人 | +| 首次完成视频生成 | 20 | 1 次 | +| 提交有效 Bug 反馈 | 50 | 不限(审核通过后发放) | + +--- + +## 四、积分消耗场景 + +### 4.1 消耗清单 + +| 功能模块 | 消耗场景 | 每次消耗积分 | 说明 | +|----------|----------|-------------|------| +| **AI 配音** | 生成一条配音 | 1 | 已存在于 `module_registry.py` | +| **AI 数字人** | 生成一段数字人视频片段 | 5 | 新场景 | +| **视频生成** | 生成一段 AI 视频(≤10s) | 10 | 新场景 | +| **视频生成(长时长)** | 每增加 10s | +5 | 累进计费 | +| **抖音文案提取** | 提取一次 | 1 | 新场景 | +| **AI 改写** | 改写一段文案 | 1 | 新场景 | +| **AI 标题生成** | 批量生成一次(≤10 条) | 1 | 新场景 | +| **AI 封面生成** | 生成一张封面 | 2 | 新场景 | +| **AI 字幕翻译** | 翻译一条字幕(≤50 字) | 1 | 新场景 | + +### 4.2 消耗规则扩展机制 + +复用现有 `ModuleRegistry` + `QuotaRule` 模式: + +```python +# 新增模块注册示例 +module_registry.register(Module( + name="ai_digital_human", + version="1.0.0", + description="AI 数字人生成模块", + capabilities=[ + ModuleCapability( + name="generate_digital_human", + description="生成数字人视频片段", + quota_rules=[QuotaRule("points", 5.0, "每段数字人视频消耗 5 积分")], + ), + ], +)) +``` + +新增 `QuotaDimension.POINTS = "points"` 作为通用积分维度,所有消耗型功能统一通过积分维度扣费。 + +### 4.3 消费折扣(预留) + +未来可扩展按会员等级设置折扣: + +| 等级 | 积分消耗折扣 | +|------|-------------| +| 免费版 | 无折扣 | +| 标准版 | 9.5 折 | +| 专业版 | 9 折 | +| 企业版 | 8 折 | + +> 折扣仅在会员有效期内生效,过期后恢复原价。实现时通过 `discount_rate` 配置字段支持。 + +--- + +## 五、数据库表设计 + +### 5.1 新增表结构 + +#### 5.1.1 积分账户表 `points_accounts` + +每个用户一个积分账户,记录余额和累计值。 + +```sql +CREATE TABLE IF NOT EXISTS points_accounts ( + id VARCHAR(36) PRIMARY KEY, + user_id VARCHAR(36) NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE, + balance INTEGER NOT NULL DEFAULT 0, -- 当前可用积分 + total_earned INTEGER NOT NULL DEFAULT 0, -- 累计获得积分 + total_spent INTEGER NOT NULL DEFAULT 0, -- 累计消耗积分 + total_recharged INTEGER NOT NULL DEFAULT 0, -- 累计充值积分 + total_gifted INTEGER NOT NULL DEFAULT 0, -- 累计赠送积分(会员赠送 + 任务奖励) + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_points_accounts_user ON points_accounts(user_id); +``` + +**设计说明**: +- `user_id` 设为 UNIQUE,每个用户只有一个积分账户 +- 余额通过 `total_earned - total_spent` 可交叉校验 `balance`,保证数据一致性 +- 不使用悲观锁,而是通过事务 + 乐观锁(`updated_at`)保证并发安全 + +#### 5.1.2 积分流水表 `points_transactions` + +每笔积分变动都记录一条流水,支持对账和审计。 + +```sql +CREATE TABLE IF NOT EXISTS points_transactions ( + id VARCHAR(36) PRIMARY KEY, + user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + account_id VARCHAR(36) NOT NULL REFERENCES points_accounts(id) ON DELETE CASCADE, + type VARCHAR(20) NOT NULL, -- earn(获得) / spend(消耗) / refund(退还) / expire(过期) + source VARCHAR(50) NOT NULL, -- recharge(充值) / membership_gift(会员赠送) / task_reward(任务奖励) / ai_voice / ai_digital_human / ai_video / ... + amount INTEGER NOT NULL, -- 变动数量(正数) + balance_after INTEGER NOT NULL, -- 变动后余额 + description VARCHAR(255) DEFAULT '', -- 描述 + ref_id VARCHAR(100) DEFAULT '', -- 关联业务 ID(订单号、任务 ID 等) + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_points_tx_user ON points_transactions(user_id); +CREATE INDEX idx_points_tx_type ON points_transactions(type); +CREATE INDEX idx_points_tx_source ON points_transactions(source); +CREATE INDEX idx_points_tx_created ON points_transactions(created_at); +``` + +**设计说明**: +- `source` 字段标识具体来源/场景,新增消耗场景时只需新增 source 值,不需要改表结构 +- `ref_id` 关联具体业务,方便追溯(如充值关联订单号,消费关联生成任务 ID) +- 流水只追加不修改,保证审计完整性 + +#### 5.1.3 积分订单表 `points_orders` + +记录用户充值积分的支付订单。 + +```sql +CREATE TABLE IF NOT EXISTS points_orders ( + id VARCHAR(36) PRIMARY KEY, + user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + package_name VARCHAR(50) NOT NULL, -- 积分包名称 + points_amount INTEGER NOT NULL, -- 积分数量 + price_cents INTEGER NOT NULL, -- 支付金额(分),避免浮点精度问题 + currency VARCHAR(10) NOT NULL DEFAULT 'CNY', + status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending / paid / failed / refunded + payment_method VARCHAR(50), -- alipay / wechat_pay / ... + payment_id VARCHAR(100), -- 第三方支付流水号 + paid_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + expire_at TIMESTAMP -- 订单过期时间(未支付自动关闭) +); + +CREATE INDEX idx_points_orders_user ON points_orders(user_id); +CREATE INDEX idx_points_orders_status ON points_orders(status); +``` + +#### 5.1.4 积分消耗配置表 `points_consumption_config` + +集中管理各场景的积分消耗规则,支持动态调整。 + +```sql +CREATE TABLE IF NOT EXISTS points_consumption_config ( + id VARCHAR(36) PRIMARY KEY, + scene_key VARCHAR(50) NOT NULL UNIQUE, -- 场景标识,如 ai_voice, ai_video + scene_name VARCHAR(100) NOT NULL, -- 场景显示名称 + points_per_use INTEGER NOT NULL DEFAULT 1, -- 每次消耗积分数 + is_active BOOLEAN NOT NULL DEFAULT TRUE, -- 是否启用 + description VARCHAR(255) DEFAULT '', + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); +``` + +### 5.2 现有表变更 + +#### 5.2.1 users 表新增字段 + +```sql +-- 会员等级字段保留现有 subscription_plan,无需改动 +-- 无需在 users 表加 credits 字段,积分独立在 points_accounts 表管理 +``` + +### 5.3 表关系 + +``` +users (1) ──── (1) points_accounts + │ + └── (1:N) points_transactions + │ + └── ref_id ──> generation_tasks / points_orders / ... + +users (1) ──── (1:N) points_orders +``` + +### 5.4 与现有模型的映射 + +| 现有模型 | 改动 | +|----------|------| +| `UserModel` | 不新增积分字段,积分由独立表管理 | +| `BillingRecordModel` | 保留,用于订阅支付记录;积分充值走新的 `points_orders` | +| `QuotaDimension` | 新增 `POINTS = "points"` 维度 | +| `QuotaTier` | 各等级新增 `monthly_credits` 配额 | +| `QuotaRule` | `dimension` 支持 `"points"` 值 | + +--- + +## 六、API 设计 + +### 6.1 积分账户 API + +#### `GET /api/v1/points/balance` + +获取当前积分余额。 + +```json +// Response +{ + "balance": 580, + "total_earned": 1200, + "total_spent": 620, + "membership_monthly_gift": 200, + "membership_expires_at": "2027-01-15T00:00:00Z" +} +``` + +#### `GET /api/v1/points/transactions` + +查询积分流水,支持分页和筛选。 + +``` +?page=1&page_size=20&type=spend&source=ai_voice&start_date=2026-09-01&end_date=2026-09-30 +``` + +```json +// Response +{ + "total": 156, + "page": 1, + "page_size": 20, + "items": [ + { + "id": "tx_xxx", + "type": "spend", + "source": "ai_voice", + "amount": 1, + "balance_after": 579, + "description": "AI 配音 - 温柔女声", + "ref_id": "task_xxx", + "created_at": "2026-09-14T10:30:00Z" + } + ] +} +``` + +### 6.2 积分充值 API + +#### `POST /api/v1/points/recharge` + +创建积分充值订单。 + +```json +// Request +{ + "package_id": "standard_pack" // 或自定义 amount +} + +// Response +{ + "order_id": "po_xxx", + "package_name": "标准包", + "points_amount": 500, + "price_cents": 8000, + "payment_url": "https://pay.alipay.com/...", + "expire_at": "2026-09-14T16:30:00Z" +} +``` + +#### `POST /api/v1/points/payment-callback` + +支付回调(内部接口 + 第三方支付通知)。 + +```json +// Request (来自支付平台) +{ + "order_id": "po_xxx", + "payment_method": "alipay", + "payment_id": "2026xxx", + "status": "paid" +} +``` + +回调处理流程: +1. 验证支付签名 +2. 更新 `points_orders.status = "paid"` +3. 增加 `points_accounts.balance += points_amount` +4. 写入 `points_transactions` 流水(type=earn, source=recharge) + +### 6.3 积分消费 API + +#### 内部扣费接口(供各功能模块调用) + +```python +# packages/domain/points_service.py +class PointsService: + def deduct(self, user_id: str, scene_key: str, amount: int, ref_id: str = "") -> DeductResult: + """ + 扣减积分 + 1. 检查积分余额是否充足 + 2. 在事务中扣减余额、写入流水 + 3. 返回扣减结果 + """ + pass + + def check_balance(self, user_id: str, scene_key: str) -> CheckResult: + """ + 检查余额是否充足某场景消耗 + """ + pass +``` + +#### `GET /api/v1/points/consumption-rules` + +查询当前所有积分消耗规则(前端展示用)。 + +```json +// Response +{ + "rules": [ + { "scene_key": "ai_voice", "scene_name": "AI 配音", "points_per_use": 1 }, + { "scene_key": "ai_digital_human", "scene_name": "AI 数字人", "points_per_use": 5 }, + { "scene_key": "ai_video", "scene_name": "视频生成", "points_per_use": 10 }, + ... + ] +} +``` + +### 6.4 会员订阅 API(改造) + +保留现有 `subscription.py` 路由结构,新增以下逻辑: + +- `POST /subscription/change-plan`:变更套餐时,自动创建/更新积分账户,发放当月赠送积分 +- `GET /subscription/current`:返回中增加 `monthly_credits` 和 `points_balance` 字段 +- 新增 `POST /subscription/claim-monthly-credits`:手动领取每月赠送积分(兜底入口) + +### 6.5 API 路由汇总 + +| 方法 | 路径 | 说明 | 类型 | +|------|------|------|------| +| GET | `/api/v1/points/balance` | 查询积分余额 | 用户 | +| GET | `/api/v1/points/transactions` | 积分流水查询 | 用户 | +| POST | `/api/v1/points/recharge` | 创建充值订单 | 用户 | +| POST | `/api/v1/points/payment-callback` | 支付回调 | 内部 | +| GET | `/api/v1/points/consumption-rules` | 消耗规则查询 | 用户 | +| POST | `/api/v1/points/check` | 消费前余额检查 | 内部 | +| POST | `/api/v1/points/deduct` | 消费扣减 | 内部 | +| POST | `/api/v1/points/refund` | 消费退还 | 内部 | + +--- + +## 七、前端页面设计 + +### 7.1 会员购买页(改造 Plans.tsx) + +**路由**: `/app/subscription`(保持不变) + +**改动要点**: +1. 将 3 档对齐为 4 档,与后端一致 +2. 每档卡片增加"每月赠送 XXX 积分"标识 +3. 按钮文案按当前状态动态显示("当前方案"/"升级"/"降级"/"联系我们") +4. 底部增加积分包购买入口 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 选择适合您的方案 │ +├──────────┬──────────┬──────────┬──────────┐ │ +│ 免费版 │ 标准版 │ 专业版 ★ │ 企业版 │ │ +│ ¥0/月 │ ¥99/月 │ ¥299/月 │ ¥999/月 │ │ +│ │ │ │ │ │ +│ 3 项目 │ 10 项目 │ 无限项目 │ 无限项目 │ │ +│ 10GB │ 50GB │ 100GB │ 1TB │ │ +│ 0 积分 │ 200积分 │ 800积分 │ 3000积分 │ ← 新增 │ +│ │ │ │ │ │ +│ [当前] │ [升级] │ [升级] │ [联系] │ │ +├──────────┴──────────┴──────────┴──────────┘ │ +│ │ +│ 💰 积分充值 [查看全部积分包] │ +│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ +│ │50积分 │ │200积分│ │500积分│ │1500积分│ │5000积分│ │ +│ │ ¥9.9 │ │ ¥36 │ │ ¥80 │ │ ¥210 │ │ ¥600 │ │ +│ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 7.2 积分余额展示位置 + +#### 顶部导航栏(Header 组件) + +在用户头像旁边增加积分余额徽章: + +``` +┌─────────────────────────────────────────────────┐ +│ Logo 工作台 模板 素材 🔔 💎 580 👤 │ +└─────────────────────────────────────────────────┘ +``` + +- 点击积分徽章展开快捷面板,显示余额、本月已用、充值入口 +- 积分不足时徽章变为警告色(橙色) + +#### 功能入口处的消耗提示 + +在每个 AI 功能的操作按钮旁,显示本次操作将消耗的积分: + +``` +[生成配音] 💎 -1 积分 +[生成视频] 💎 -10 积分 +``` + +### 7.3 积分中心页面 + +**路由**: `/app/points`(新增) + +**页面结构**: + +``` +┌─────────────────────────────────────────────────┐ +│ 积分中心 │ +├─────────────────────────────────────────────────┤ +│ │ +│ 当前余额 本月获得 本月消耗 │ +│ 💎 580 +200 -120 │ +│ │ +├─────────────────────────────────────────────────┤ +│ │ +│ [积分明细] [充值记录] [消耗规则] │ +│ ───────── │ +│ │ +│ 时间 类型 场景 数量 余额 │ +│ 09-14 10:30 消耗 AI配音 -1 579 │ +│ 09-14 09:15 消耗 AI视频 -10 580 │ +│ 09-01 00:00 获得 会员赠送 +200 590 │ +│ 08-28 14:20 获得 充值+500 +500 390 │ +│ ... │ +│ │ +└─────────────────────────────────────────────────┘ +``` + +### 7.4 前端组件清单 + +| 组件 | 路径 | 说明 | +|------|------|------| +| `PointsBadge` | `components/common/PointsBadge/` | 顶部积分余额徽章 | +| `PointsPanel` | `components/common/PointsPanel/` | 点击徽章展开的快捷面板 | +| `PointsCost` | `components/common/PointsCost/` | 功能入口的消耗提示标签 | +| `PointsCenter` | `pages/points/Center.tsx` | 积分中心主页面 | +| `PointsTransactions` | `pages/points/Transactions.tsx` | 积分明细子页 | +| `PointsPackages` | `pages/points/Packages.tsx` | 充值积分包子页 | +| `Plans` (改造) | `pages/subscription/Plans.tsx` | 对齐 4 档 + 积分展示 | + +--- + +## 八、与现有订阅制的迁移方案 + +### 8.1 迁移原则 + +1. **向前兼容**:迁移期间老用户权益不降低 +2. **平滑过渡**:不需要用户手动操作,自动完成 +3. **灰度发布**:按用户批次逐步迁移,降低风险 + +### 8.2 迁移步骤 + +#### Step 1:数据准备(后端 + DBA) + +```sql +-- 1. 创建新表(points_accounts, points_transactions, points_orders, points_consumption_config) +-- 2. 为所有现有用户创建积分账户 +INSERT INTO points_accounts (id, user_id, balance, total_earned, total_gifted) +SELECT + uuid(), id, + CASE subscription_plan + WHEN 'standard' THEN 200 + WHEN 'pro' THEN 800 + WHEN 'enterprise' THEN 3000 + ELSE 0 + END, + CASE subscription_plan + WHEN 'standard' THEN 200 + WHEN 'pro' THEN 800 + WHEN 'enterprise' THEN 3000 + ELSE 0 + END, + CASE subscription_plan + WHEN 'standard' THEN 200 + WHEN 'pro' THEN 800 + WHEN 'enterprise' THEN 3000 + ELSE 0 + END +FROM users WHERE subscription_status = 'active'; +``` + +#### Step 2:代码兼容层 + +```python +# packages/domain/quota.py 扩展 +class QuotaDimension(str, Enum): + # ... 现有维度保持不变 + POINTS = "points" # 新增:通用积分维度 + +# QUOTA_TIERS 扩展 +"standard": QuotaTier( + name="standard", + limits={ + # ... 现有配额保持不变 + QuotaDimension.MONTHLY_CREDITS: 200, # 新增 + }, +), +``` + +#### Step 3:双轨运行期(1 个月) + +- 订阅制功能不变,老用户正常续费 +- 积分系统上线后,所有 AI 消耗型功能改为积分扣费 +- 会员权益中的"基础配额"(存储、项目数等)继续由订阅制控制 +- 在用户首次登录后,弹出迁移通知弹窗,说明变更内容 + +#### Step 4:完全切换 + +- 停止订阅制的 AI 配额逻辑(`AI_VOICE_CREDITS` 等旧维度废弃) +- 所有 AI 功能统一使用积分扣费 +- 订阅制仅控制基础权益(存储、项目数、并发数、导出分辨率等) + +### 8.3 老用户过渡策略 + +| 用户类型 | 过渡方案 | +|----------|----------| +| 当前 free 用户 | 不变,积分余额为 0,可充值 | +| 当前 standard 用户 | 赠送 200 积分作为过渡礼包,当前周期内权益不变 | +| 当前 pro 用户 | 赠送 800 积分 + 延长 1 个月有效期 | +| 当前 enterprise 用户 | 赠送 3000 积分 + 延长 1 个月有效期 + 专属客户经理通知 | +| 年付用户 | 按剩余月数比例折算赠送积分 | + +### 8.4 前端迁移 + +- `Plans.tsx` 从 3 档改为 4 档,增加积分信息展示 +- 新增 `/app/points` 积分中心页面 +- 顶部 Header 增加积分余额徽章 +- 各 AI 功能页增加积分消耗提示 + +--- + +## 九、配额检查中间件设计 + +### 9.1 整体架构 + +``` +用户请求 → API Route → 积分检查中间件 → 业务逻辑 → 返回结果 + │ + ├─ 检查会员权益(QuotaChecker) + ├─ 检查积分余额(PointsChecker) + └─ 扣减积分(PointsService.deduct) +``` + +### 9.2 中间件设计 + +#### `apps/api/app/middleware/points_check.py` + +```python +"""积分扣费中间件 - 用于 AI 功能入口的统一检查与扣费""" + +from functools import wraps +from fastapi import HTTPException + +def require_points(scene_key: str, amount: int = None): + """ + 装饰器:在 AI 功能入口检查积分余额并扣费 + + Args: + scene_key: 消耗场景标识,如 "ai_voice", "ai_video" + amount: 指定消耗数量,为 None 时从 points_consumption_config 读取 + + 使用方式: + @router.post("/generate-voice") + @require_points(scene_key="ai_voice") + async def generate_voice(request: VoiceRequest, current_user = Depends(get_current_user)): + # 到这里积分已扣减成功 + ... + """ + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + # 1. 从 kwargs 或 args 中提取 current_user + current_user = kwargs.get("current_user") or next( + (a for a in args if isinstance(a, AuthenticatedUser)), None + ) + if not current_user: + raise HTTPException(status_code=401, detail="未登录") + + # 2. 获取消耗数量 + consume_amount = amount or get_consumption_config(scene_key) + + # 3. 检查会员权益(原有 QuotaChecker 逻辑) + user = current_user.user + tier = quota_registry.get_tier(user.subscription_plan or "free") + # ... 检查存储、并发等基础权益 + + # 4. 检查并扣减积分 + points_service = get_points_service() + result = points_service.check_and_deduct( + user_id=user.id, + scene_key=scene_key, + amount=consume_amount, + ) + + if not result.success: + raise HTTPException( + status_code=402, # Payment Required + detail={ + "code": "INSUFFICIENT_POINTS", + "message": f"积分不足,需要 {consume_amount} 积分,当前余额 {result.balance}", + "recharge_url": "/app/points/recharge" + } + ) + + # 5. 将扣减信息注入请求上下文,供业务层使用 + kwargs["points_deduct_id"] = result.transaction_id + + try: + # 6. 执行业务逻辑 + return await func(*args, **kwargs) + except Exception as e: + # 7. 业务失败时退还积分 + points_service.refund( + user_id=user.id, + transaction_id=result.transaction_id, + reason=f"业务执行失败: {scene_key}" + ) + raise + + return wrapper + return decorator +``` + +### 9.3 积分服务层 + +```python +# packages/domain/points_service.py + +class PointsService: + """积分服务 - 核心扣费逻辑""" + + def __init__(self, account_repo, transaction_repo, config_repo): + self.account_repo = account_repo + self.transaction_repo = transaction_repo + self.config_repo = config_repo + + def check_and_deduct(self, user_id: str, scene_key: str, amount: int, ref_id: str = "") -> DeductResult: + """ + 检查余额并扣减积分(事务操作) + + 流程: + 1. 查询积分账户 + 2. 检查余额是否 >= amount + 3. 在事务中:扣减余额 + 写入流水 + 4. 返回扣减结果 + """ + pass + + def refund(self, user_id: str, transaction_id: str, reason: str = "") -> bool: + """退还积分(业务失败时调用)""" + pass + + def gift(self, user_id: str, amount: int, source: str, ref_id: str = ""): + """赠送积分(会员赠送 / 任务奖励)""" + pass + + def get_balance(self, user_id: str) -> int: + """查询余额""" + pass + + def get_transactions(self, user_id: str, page: int = 1, page_size: int = 20, + type: str = None, source: str = None) -> list: + """查询流水""" + pass +``` + +### 9.4 与现有 QuotaChecker 的集成 + +```python +# 改造后的检查流程 +async def check_all_quotas(user, scene_key: str, consume_amount: int): + """统一配额检查入口""" + + # 1. 基础配额检查(存储空间、项目数、并发数等) + # 复用现有 QuotaChecker + plan = user.subscription_plan or "free" + quota_results = quota_checker.check_multiple(plan, { + QuotaDimension.STORAGE_GB.value: get_used_storage(user.id), + QuotaDimension.VIDEOS_PER_MONTH.value: get_monthly_video_count(user.id), + QuotaDimension.MAX_CONCURRENT.value: get_concurrent_count(user.id), + }) + for result in quota_results: + if not result.allowed: + raise QuotaExceededError(result) + + # 2. 积分检查 + # 新增 PointsChecker + balance = points_service.get_balance(user.id) + if balance < consume_amount: + raise InsufficientPointsError( + required=consume_amount, + balance=balance + ) + + return True +``` + +### 9.5 各功能模块接入方式 + +```python +# apps/api/app/api/routes/ai_voice.py(示例) +@router.post("/generate") +@require_points(scene_key="ai_voice") +async def generate_voice( + request: VoiceGenerateRequest, + current_user: AuthenticatedUser = Depends(get_current_user), + points_deduct_id: str = None, # 由中间件注入 +): + # 积分已扣减,直接执行业务逻辑 + result = await voice_service.generate(request, current_user.user.id) + return VoiceGenerateResponse( + voice_id=result.id, + points_consumed=1, + ) + +# apps/api/app/api/routes/ai_video.py(示例) +@router.post("/generate") +@require_points(scene_key="ai_video") +async def generate_video( + request: VideoGenerateRequest, + current_user: AuthenticatedUser = Depends(get_current_user), + points_deduct_id: str = None, +): + # 视频按秒计费,需要在中间件外动态计算 + ... +``` + +> **注意**:对于视频生成等按量计费场景(时长不确定),中间件支持动态计算消耗量: +> ```python +> @require_points(scene_key="ai_video", dynamic=True) +> # dynamic=True 时,中间件只检查余额 > 0,实际扣费由业务层调用 points_service.deduct() +> ``` + +--- + +## 十、实施计划 + +### 10.1 开发阶段拆分 + +| 阶段 | 内容 | 预估工时 | +|------|------|----------| +| **P1 - 基础框架** | 数据库迁移脚本、积分账户/流水/订单 Model、Repository | 3 天 | +| **P2 - 核心服务** | PointsService 核心逻辑(扣费/退还/赠送/查询) | 3 天 | +| **P3 - API 层** | 积分 API 路由、中间件、与现有订阅 API 集成 | 3 天 | +| **P4 - 前端页面** | Plans.tsx 改造、积分中心页面、Header 积分徽章、消耗提示 | 5 天 | +| **P5 - 功能接入** | 各 AI 功能模块接入 `@require_points` 中间件 | 3 天 | +| **P6 - 迁移与测试** | 数据迁移脚本、灰度方案、集成测试、端到端测试 | 3 天 | + +### 10.2 文件改动清单(预览) + +| 类型 | 文件路径 | 改动说明 | +|------|----------|----------| +| 新增 | `migrations/007_membership_points.sql` | 新建积分相关 4 张表 | +| 新增 | `packages/domain/points_service.py` | 积分核心服务 | +| 新增 | `packages/domain/points_models.py` 或追加到 `models.py` | 积分相关 ORM Model | +| 新增 | `packages/adapters/sqlalchemy_impl/points_repository.py` | 积分 Repository | +| 新增 | `apps/api/app/middleware/points_check.py` | 积分扣费中间件 | +| 新增 | `apps/api/app/api/routes/points.py` | 积分 API 路由 | +| 新增 | `apps/api/app/schemas/points.py` | 积分 Schema | +| 新增 | `apps/web/src/pages/points/Center.tsx` | 积分中心页面 | +| 新增 | `apps/web/src/pages/points/Transactions.tsx` | 积分明细页面 | +| 新增 | `apps/web/src/pages/points/Packages.tsx` | 积分包充值页面 | +| 新增 | `apps/web/src/components/common/PointsBadge/` | 积分徽章组件 | +| 新增 | `apps/web/src/components/common/PointsCost/` | 消耗提示组件 | +| 修改 | `packages/domain/quota.py` | 新增 POINTS 维度和 monthly_credits 配额 | +| 修改 | `packages/adapters/sqlalchemy_impl/models.py` | 新增积分相关 Model | +| 修改 | `apps/api/app/api/routes/subscription.py` | 集成积分逻辑 | +| 修改 | `apps/api/app/schemas/subscription.py` | 返回中增加积分信息 | +| 修改 | `apps/web/src/pages/subscription/Plans.tsx` | 对齐 4 档 + 积分展示 | +| 修改 | 各 AI 功能路由文件 | 添加 `@require_points` 装饰器 | + +### 10.3 测试计划 + +| 测试类型 | 覆盖范围 | +|----------|----------| +| 单元测试 | PointsService 扣费/退还/赠送逻辑、PointsChecker 余额检查 | +| 集成测试 | API 端到端:充值→到账→消费→扣减→流水查询 | +| 并发测试 | 同一用户多请求并发扣费的余额一致性 | +| 前端测试 | Plans 页面渲染、积分中心交互、Header 徽章实时更新 | +| 迁移测试 | 老用户数据迁移正确性验证 | + +--- + +## 十一、风险与注意事项 + +| 风险 | 应对方案 | +|------|----------| +| 并发扣费导致余额不一致 | 数据库事务 + 行锁,`points_accounts` 使用 `SELECT ... FOR UPDATE` | +| 支付回调延迟导致积分未到账 | 订单创建后 30 分钟未支付自动关闭;回调支持幂等重试 | +| 积分消耗规则变更影响用户 | 变更前 7 天公告通知;已购买的服务按旧价格执行 | +| 前后端积分展示不一致 | 统一从 `GET /api/v1/points/balance` 获取,前端不本地缓存余额 | +| 老用户迁移产生不满 | 过渡期权益不降低 + 额外赠送积分礼包 | + +--- + +## 十二、开放问题(待确认) + +1. **积分有效期**:当前设计为永久有效。是否需要设置有效期(如 1 年)? +2. **退款策略**:积分充值后是否支持退款?已消费的积分如何计算? +3. **企业版定制**:企业版是否需要支持自定义积分消耗规则? +4. **支付渠道**:第一期接入支付宝 + 微信支付,是否需要支持其他渠道? +5. **发票需求**:积分充值是否需要单独开发票?与订阅发票合并还是分开? + +--- + +*本文档为方案设计阶段产物,待确认后将按「实施计划」分阶段开发。*