Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
34 KiB
会员制 + 积分方案设计文档
Issue: #1895 版本: v1.0 状态: 方案设计(待确认) 创建时间: 2026-09-14
一、设计背景与目标
1.1 背景
当前系统采用纯订阅制计费模式(4 档:free/standard/pro/enterprise),存在以下问题:
- 灵活性不足:用户付费后只能按套餐配额使用,无法按需消费高频 AI 能力
- 前后端不一致:前端 Plans.tsx 写死 3 档,后端硬编码 4 档
- 积分能力缺失:
quota.py已预留AI_VOICE_CREDITS枚举,module_registry.py已注册消耗规则,但无实际账户、流水、扣费和充值链路 - 收入天花板:纯订阅制难以覆盖 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 模式:
# 新增模块注册示例
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
每个用户一个积分账户,记录余额和累计值。
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
每笔积分变动都记录一条流水,支持对账和审计。
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
记录用户充值积分的支付订单。
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
集中管理各场景的积分消耗规则,支持动态调整。
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 表新增字段
-- 会员等级字段保留现有 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
获取当前积分余额。
// 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
// 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
创建积分充值订单。
// 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
支付回调(内部接口 + 第三方支付通知)。
// Request (来自支付平台)
{
"order_id": "po_xxx",
"payment_method": "alipay",
"payment_id": "2026xxx",
"status": "paid"
}
回调处理流程:
- 验证支付签名
- 更新
points_orders.status = "paid" - 增加
points_accounts.balance += points_amount - 写入
points_transactions流水(type=earn, source=recharge)
6.3 积分消费 API
内部扣费接口(供各功能模块调用)
# 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
查询当前所有积分消耗规则(前端展示用)。
// 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(保持不变)
改动要点:
- 将 3 档对齐为 4 档,与后端一致
- 每档卡片增加"每月赠送 XXX 积分"标识
- 按钮文案按当前状态动态显示("当前方案"/"升级"/"降级"/"联系我们")
- 底部增加积分包购买入口
┌─────────────────────────────────────────────────────────────┐
│ 选择适合您的方案 │
├──────────┬──────────┬──────────┬──────────┐ │
│ 免费版 │ 标准版 │ 专业版 ★ │ 企业版 │ │
│ ¥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 迁移原则
- 向前兼容:迁移期间老用户权益不降低
- 平滑过渡:不需要用户手动操作,自动完成
- 灰度发布:按用户批次逐步迁移,降低风险
8.2 迁移步骤
Step 1:数据准备(后端 + DBA)
-- 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:代码兼容层
# 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
"""积分扣费中间件 - 用于 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 积分服务层
# 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 的集成
# 改造后的检查流程
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 各功能模块接入方式
# 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,
):
# 视频按秒计费,需要在中间件外动态计算
...
注意:对于视频生成等按量计费场景(时长不确定),中间件支持动态计算消耗量:
@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 年)?
- 退款策略:积分充值后是否支持退款?已消费的积分如何计算?
- 企业版定制:企业版是否需要支持自定义积分消耗规则?
- 支付渠道:第一期接入支付宝 + 微信支付,是否需要支持其他渠道?
- 发票需求:积分充值是否需要单独开发票?与订阅发票合并还是分开?
本文档为方案设计阶段产物,待确认后将按「实施计划」分阶段开发。