33 KiB
会员制 + 积分方案设计文档(v2)
Issue: #1895 版本: v2.0 状态: 方案设计(待确认) 创建时间: 2026-09-14 更新: 2026-09-14 — 基于用户确认方案重写,简化为两档会员 + 基于真实 AI 调用成本定价
一、方案概述
1.1 核心思路
两档会员 + 积分消耗制:
- 免费会员:每天可免费混剪 2 条视频;其他 AI 功能需购买积分包
- 付费会员:解锁全部功能 + 每月赠送积分 + 积分购买折扣
1.2 设计原则
| 原则 | 说明 |
|---|---|
| 极简 | 只分免费/付费两档,不搞多梯度会员 |
| 成本驱动 | 积分定价严格基于真实 AI 调用成本,毛利率 60-75% |
| 可扩展 | 新增消耗场景只需注册,不改动核心逻辑 |
| 平滑迁移 | 老用户统一转为年卡会员,无感知过渡 |
二、真实成本基准
数据来源:《小虾 SaaS AI 能力成本核算报告》(生产实测 + API 官方定价)
| 能力 | 调用方式 | 真实成本 |
|---|---|---|
| AI 配音(TTS) | 阿里云 cosyvoice-v3-flash | ¥0.024/分钟 |
| AI 数字人(口型同步) | 火山 MediaKit lip-sync | ¥1.028/分钟(含 TTS) |
| 智能混剪渲染 | 本地 FFmpeg(CPU) | ¥0.001/分钟 |
| 声音克隆训练 | 阿里云 voice-enrollment | 免费(合成才收费) |
| 声音克隆合成 | 同 cosyvoice TTS | ¥0.024/分钟 |
| AI 文案改写 / 标题生成 | 火山方舟 doubao-seed-1-6 | <¥0.001/条 |
| 抖音链接提取文案 | yt-dlp + 火山 ASR | ¥0.03/分钟 |
| AI 封面生成 | 本地 ffmpeg 抽帧 + PIL | <¥0.001/张 |
| OSS 存储 | 阿里云杭州标准 | ¥0.001/条/月 |
典型场景成本:
- 1 分钟基础混剪(TTS + FFmpeg + 封面 + 存储 1 月):≈ ¥0.03/条
- 1 分钟数字人口播(TTS + lip-sync + 封装):≈ ¥1.03/条(是混剪的 35 倍)
- 免费用户每天 2 条混剪:单活跃用户月成本 ≈ ¥2.3(完全可承受)
三、会员等级设计
3.1 两档会员对照
| 权益项 | 免费会员 | 付费会员 |
|---|---|---|
| 每日免费混剪 | 2 条/天(含 TTS + 渲染) | 不限次数 |
| AI 数字人 | ✗ 不可用 | ✓ |
| 声音克隆 | ✗ 不可用 | ✓ |
| AI 文案改写 | ✗ 不可用 | ✓ |
| AI 标题生成 | ✗ 不可用 | ✓ |
| 抖音链接提取 | ✗ 不可用 | ✓ |
| AI 封面生成 | ✗ 不可用 | ✓ |
| 导出分辨率 | 720p | 最高 4K |
| 每月赠送积分 | 0 | 见下表 |
| 积分购买折扣 | 无 | 见下表 |
| 批量导出 | ✗ | ✓ |
| 多平台发布 | ✗ | ✓ |
| 去重检测报告 | ✗ | ✓ |
3.2 付费会员定价
| 套餐 | 价格 | 折合月价 | 积分购买折扣 | 每月赠送积分 |
|---|---|---|---|---|
| 月卡 | ¥19.9/月 | ¥19.9 | 9 折 | 200 |
| 季卡 | ¥39.9/季 | ≈ ¥13.3 | 8.7 折 | 600(200×3) |
| 年卡 | ¥159/年 | ≈ ¥13.25 | 8 折 | 2,400(200×12) |
年卡和季卡的核心优惠体现在积分折扣更低,而不是赠送更多积分。
3.3 与现有代码的映射
废弃 subscription.py 中的 4 档 PLAN_QUOTAS(free/standard/pro/enterprise),替换为:
MEMBER_TIERS = {
"free": {
"name": "免费会员",
"daily_free_clips": 2, # 每日免费混剪数
"max_resolution": "720p",
"ai_features_enabled": False, # AI 高级功能锁定
"monthly_credits": 0,
"points_discount": 1.0, # 无折扣
"free_multiplier": 1.15, # 积分消耗上浮 15%(非免费混剪场景不适用,因 AI 功能被锁定)
},
"paid": {
"name": "付费会员",
"daily_free_clips": -1, # 不限
"max_resolution": "4K",
"ai_features_enabled": True,
"monthly_credits": 200,
"points_discount": 1.0, # 默认月卡 9 折通过购买折扣体现
"free_multiplier": 1.0,
},
}
付费会员的积分购买折扣通过订单系统实现,不在 MEMBER_TIERS 中硬编码:
MEMBER_DISCOUNT = {
"monthly": 0.9, # 月卡 9 折
"quarterly": 0.87, # 季卡 8.7 折
"yearly": 0.8, # 年卡 8 折
}
四、积分体系设计
4.1 积分获取
| 获取方式 | 说明 | 频率 |
|---|---|---|
| 会员每月赠送 | 开通/续费时到账 200 积分 | 每月 1 次 |
| 积分包充值 | 用户按需购买,付费会员享受折扣 | 随时 |
| 任务奖励 | 注册送 50、首次生成视频送 20、邀请好友送 30/人(月上限 10 人) | 一次性/周期性 |
4.2 积分包定价
积分定价核心:1 积分 ≈ ¥0.10(会员价),免费用户同操作消耗 ×1.15 向上取整。
| 积分包 | 积分数量 | 会员价 | 折合单价 | 免费用户等量价格 |
|---|---|---|---|---|
| 体验包 | 100 | ¥9.9 | ¥0.099/积分 | ¥10.89 |
| 基础包 | 500 | ¥39 | ¥0.078/积分 | ¥42.9 |
| 专业包 | 2,000 | ¥129 | ¥0.065/积分 | ¥141.9 |
简化为 3 档,覆盖轻度、中度、重度用户。
4.3 积分消耗场景清单
| 功能模块 | 消耗场景 | 会员消耗 | 免费用户消耗(×1.15↑) | 真实成本 | 毛利率 |
|---|---|---|---|---|---|
| AI 配音 | 生成 1 分钟配音 | 1 积分 | 2 积分 | ¥0.024 | ~76% |
| 智能混剪 | 生成 1 条视频(≤30s) | 3 积分 | 4 积分 | ¥0.03 | >85% |
| 智能混剪(加长) | 每增加 30s | +1 积分 | +2 积分 | — | — |
| AI 数字人 | 生成 1 分钟口播视频 | 15 积分 | — (不可用) | ¥1.03 | ~30% |
| 声音克隆训练 | 训练一个声音模型 | 免费 | — (不可用) | ¥0 | — |
| 声音克隆合成 | 用克隆声音生成 1 分钟配音 | 1 积分 | — (不可用) | ¥0.024 | ~76% |
| 抖音链接提取 | 提取 1 次文案 | 1 积分 | — (不可用) | ¥0.03 | ~70% |
| AI 改写文案 | 改写 1 次 | 1 积分 | — (不可用) | <¥0.001 | ~99% |
| AI 标题生成 | 生成 1 次(≤10 条) | 1 积分 | — (不可用) | <¥0.001 | ~99% |
| AI 封面生成 | 生成 1 张 | 1 积分 | — (不可用) | <¥0.001 | ~99% |
免费用户专属规则:每天可免费混剪 2 条视频(含 TTS),不消耗积分。但其他所有 AI 功能对免费用户不可用,需先开通付费会员。
4.4 积分有效期
- 用户付费购买的积分:永久有效,不清零
- 会员每月赠送的积分:有效期 12 个月(到期未使用则清零)
- 任务奖励赠送的积分:永久有效
五、数据库设计
5.1 users 表新增字段
-- 在现有 users 表上新增字段
ALTER TABLE users ADD COLUMN is_member BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE users ADD COLUMN member_type VARCHAR(20) DEFAULT NULL; -- monthly / quarterly / yearly
ALTER TABLE users ADD COLUMN member_expires_at TIMESTAMP DEFAULT NULL;
ALTER TABLE users ADD COLUMN points_balance INTEGER NOT NULL DEFAULT 0;
-- 保留但可废弃的字段(兼容期)
-- subscription_plan → 迁移期间映射为 is_member + member_type
-- subscription_status → 迁移期间保留
-- max_projects / max_storage_gb → 不再限制,保留字段但不再使用
5.2 积分账户表 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_purchased 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);
5.3 积分流水表 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(退还)
source VARCHAR(50) NOT NULL, -- recharge / membership_gift / task_reward / ai_voice / ai_digital_human / ai_video / ai_rewrite / ai_title / ai_cover / douyin_extract / voice_clone
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);
5.4 积分订单表 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',
discount REAL NOT NULL DEFAULT 1.0, -- 享受的折扣
original_price_cents INTEGER NOT NULL, -- 原价(分)
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending / paid / failed / refunded
payment_method VARCHAR(50), -- wechat_pay / alipay
payment_id VARCHAR(100), -- 第三方支付流水号
paid_at TIMESTAMP,
expire_at TIMESTAMP, -- 未支付自动过期时间
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_points_orders_user ON points_orders(user_id);
CREATE INDEX idx_points_orders_status ON points_orders(status);
5.5 每日免费混剪计数 daily_usage_records
CREATE TABLE IF NOT EXISTS daily_usage_records (
id VARCHAR(36) PRIMARY KEY,
user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
usage_date DATE NOT NULL, -- 使用日期(UTC+8)
usage_type VARCHAR(50) NOT NULL, -- free_clip(免费混剪)
count INTEGER NOT NULL DEFAULT 0, -- 当日已使用次数
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(user_id, usage_date, usage_type)
);
CREATE INDEX idx_daily_usage_user_date ON daily_usage_records(user_id, usage_date);
Redis 辅助:用 Redis
INCR做实时计数(key:daily_usage:{user_id}:{date}:{usage_type}),TTL 48 小时自动过期;DB 作为持久化兜底,定期同步。
5.6 表关系图
users (1) ──── (1) points_accounts
│
└── (1:N) points_transactions
│
└── ref_id → generation_tasks / points_orders / ...
users (1) ──── (1:N) points_orders
users (1) ──── (1:N) daily_usage_records
六、API 设计
6.1 积分账户
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/v1/points/balance |
查询积分余额 + 会员状态 |
| GET | /api/v1/points/transactions |
积分流水(分页 + 筛选) |
GET /api/v1/points/balance 响应
{
"balance": 580,
"total_earned": 1200,
"total_spent": 620,
"is_member": true,
"member_type": "yearly",
"member_expires_at": "2027-09-14T00:00:00+08:00",
"monthly_gift_remaining": 180,
"next_gift_date": "2026-10-01T00:00:00+08:00"
}
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
6.2 积分充值
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/v1/points/packages |
查询可购买的积分包列表 |
| POST | /api/v1/points/recharge |
创建充值订单 |
| POST | /api/v1/points/payment-callback |
支付回调(内部) |
GET /api/v1/points/packages 响应
{
"packages": [
{
"id": "starter_pack",
"name": "体验包",
"points": 100,
"price": 990,
"discounted_price_for_monthly": 891,
"discounted_price_for_quarterly": 861,
"discounted_price_for_yearly": 792
},
{
"id": "basic_pack",
"name": "基础包",
"points": 500,
"price": 3900,
"discounted_price_for_monthly": 3510,
"discounted_price_for_quarterly": 3393,
"discounted_price_for_yearly": 3120
},
{
"id": "pro_pack",
"name": "专业包",
"points": 2000,
"price": 12900,
"discounted_price_for_monthly": 11610,
"discounted_price_for_quarterly": 11223,
"discounted_price_for_yearly": 10320
}
],
"user_discount": "yearly",
"unit_price_yuan": 0.10
}
价格单位为分(避免浮点精度问题)。折扣根据用户当前会员类型自动计算。
6.3 积分消耗(内部服务接口)
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/v1/points/check |
消费前余额检查 |
| POST | /api/v1/points/deduct |
消费扣减(事务性) |
| POST | /api/v1/points/refund |
消费退还(业务失败时) |
POST /api/v1/points/check 请求
{
"scene_key": "ai_digital_human",
"duration_minutes": 1,
"user_id": "xxx"
}
POST /api/v1/points/check 响应
{
"allowed": true,
"required_points": 15,
"current_balance": 580,
"remaining_after": 565,
"is_free_quota": false
}
6.4 每日免费额度
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/v1/usage/daily |
查询今日免费额度使用情况 |
GET /api/v1/usage/daily 响应
{
"free_clips_used": 1,
"free_clips_limit": 2,
"free_clips_remaining": 1,
"reset_at": "2026-09-15T00:00:00+08:00"
}
6.5 会员订阅(改造现有接口)
| 方法 | 路径 | 说明 | 改动 |
|---|---|---|---|
| GET | /api/v1/subscription/current |
当前会员信息 | 增加积分余额、会员类型 |
| POST | /api/v1/subscription/subscribe |
开通/续费会员 | 替换原 change-plan |
| POST | /api/v1/subscription/cancel |
取消自动续费 | 保留 |
| POST | /api/v1/subscription/payment-callback |
支付回调 | 改造 |
POST /api/v1/subscription/subscribe 请求
{
"member_type": "yearly",
"payment_method": "wechat_pay"
}
6.6 消耗规则查询
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/v1/points/rules |
查询所有积分消耗规则 |
{
"rules": [
{ "scene_key": "ai_voice", "scene_name": "AI 配音", "points_per_use": 1, "unit": "分钟" },
{ "scene_key": "ai_video", "scene_name": "智能混剪", "points_per_use": 3, "unit": "条", "extra_per_30s": 1 },
{ "scene_key": "ai_digital_human", "scene_name": "AI 数字人", "points_per_use": 15, "unit": "分钟", "member_only": true },
{ "scene_key": "voice_clone_train", "scene_name": "声音克隆训练", "points_per_use": 0, "member_only": true },
{ "scene_key": "voice_clone_synth", "scene_name": "声音克隆合成", "points_per_use": 1, "unit": "分钟", "member_only": true },
{ "scene_key": "douyin_extract", "scene_name": "抖音链接提取", "points_per_use": 1, "unit": "次", "member_only": true },
{ "scene_key": "ai_rewrite", "scene_name": "AI 改写文案", "points_per_use": 1, "unit": "次", "member_only": true },
{ "scene_key": "ai_title", "scene_name": "AI 标题生成", "points_per_use": 1, "unit": "次", "member_only": true },
{ "scene_key": "ai_cover", "scene_name": "AI 封面生成", "points_per_use": 1, "unit": "张", "member_only": true }
],
"free_user_multiplier": 1.15,
"note": "免费用户消耗 = 会员消耗 × 1.15,向上取整"
}
七、前端页面改造要点
7.1 会员购买页(改造 Plans.tsx)
现状:写死 4 档(free/standard/pro/enterprise),与后端不一致。
改造为:
┌──────────────────────────────────────────────┐
│ 选择适合您的方案 │
├────────────────────┬─────────────────────────┤
│ 免费会员 │ 付费会员 ★ 推荐 │
│ ¥0 │ ¥19.9/月 │
│ │ ¥39.9/季(省33%) │
│ ✓ 每日2条免费混剪 │ ¥159/年(省33%) │
│ ✓ 720p 导出 │ │
│ ✗ AI 功能不可用 │ ✓ 无限混剪 │
│ │ ✓ 全部 AI 功能 │
│ │ ✓ 每月赠送 200 积分 │
│ │ ✓ 积分购买最高 8 折 │
│ │ ✓ 最高 4K 导出 │
│ │ │
│ [当前方案] │ [立即开通] │
├────────────────────┴─────────────────────────┤
│ 💰 积分充值(付费会员专享折扣) │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │100积分 │ │500积分 │ │2000积分│ │
│ │ ¥9.9 │ │ ¥39 │ │ ¥129 │ │
│ └────────┘ └────────┘ └────────┘ │
└──────────────────────────────────────────────┘
改动要点:
- 移除 4 档卡片,改为左右对比布局
- 付费会员区增加月卡/季卡/年卡切换
- 底部增加积分包快捷购买区
7.2 新增积分中心页 /app/points
┌──────────────────────────────────────────────┐
│ 积分中心 │
├──────────────────────────────────────────────┤
│ │
│ 💎 580 积分 会员到期: 2027-09-14 │
│ 本月获得 +200 本月消耗 -120 │
│ │
├──────────────────────────────────────────────┤
│ [积分明细] [充值记录] [消耗规则] │
│ ───────── │
│ 时间 类型 场景 数量 余额 │
│ 09-14 10:30 消耗 AI配音 -1 579 │
│ 09-14 09:15 消耗 智能混剪 -5 580 │
│ 09-01 00:00 获得 会员赠送 +200 585 │
│ 08-28 14:20 获得 充值+500 +500 385 │
│ ... │
│ │
└──────────────────────────────────────────────┘
7.3 Header 积分徽章
- 位置:顶部导航栏用户头像左侧
- 显示:💎 + 积分余额
- 余额 < 10 时变橙色警告
- 点击展开快捷面板:余额、本月消耗、充值入口
7.4 功能入口消耗提示
在每个 AI 功能操作按钮旁显示:
[生成配音] 💎 -1 积分
[生成视频] 💎 -5 积分(约1分钟)
[数字人口播] 💎 -15 积分/分钟
免费用户看到 AI 功能时显示 🔒 锁定图标 + "开通会员解锁"。
7.5 免费额度提示
免费用户在工作台顶部显示:
今日免费混剪:已用 1/2 条 [升级会员享无限混剪 →]
7.6 前端文件改动清单
| 文件 | 改动类型 | 说明 |
|---|---|---|
Plans.tsx |
重写 | 两档会员 + 积分包购买 |
pages/points/Center.tsx |
新增 | 积分中心主页 |
pages/points/Transactions.tsx |
新增 | 积分明细 |
pages/points/Packages.tsx |
新增 | 积分包充值 |
components/common/PointsBadge/ |
新增 | Header 积分徽章 |
components/common/PointsCost/ |
新增 | 消耗提示组件 |
api/points.ts |
新增 | 积分相关 API 封装 |
api/subscription.ts |
改造 | 对接新会员接口 |
store/pointsStore.ts |
新增 | 积分状态管理 |
八、配额检查中间件设计
8.1 整体流程
用户请求 → API Route
│
├─ 1. 检查会员状态(is_member + member_expires_at)
│
├─ 2. 免费用户专属检查
│ ├─ 是否为"混剪"功能?→ 检查每日免费额度(Redis)
│ │ ├─ 未超限 → 放行,不扣积分
│ │ └─ 已超限 → 返回 402,提示开通会员或购买积分
│ └─ 非混剪功能 → 返回 403,提示需开通会员
│
├─ 3. 付费用户检查
│ ├─ 查询积分余额
│ ├─ 计算本次消耗(考虑折扣?不,折扣只影响购买价格)
│ ├─ 余额充足 → 事务扣减 → 放行
│ └─ 余额不足 → 返回 402,提示充值
│
└─ 4. 业务执行失败 → 自动退还积分
8.2 装饰器实现
# packages/middleware/points_gate.py
from functools import wraps
from fastapi import HTTPException
def points_gate(scene_key: str, per_unit: int = None, unit_field: str = None):
"""
AI 功能入口的积分扣费装饰器
Args:
scene_key: 消耗场景标识
per_unit: 每单位消耗积分(固定值时直接传)
unit_field: 动态计算时,从 request 中取数量的字段名
如 "duration_minutes" 表示按时长计费
"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# 提取用户
current_user = _extract_user(args, kwargs)
request = _extract_request(args, kwargs)
# 计算消耗量
if per_unit is not None:
total_points = per_unit
elif unit_field is not None:
units = getattr(request, unit_field, 1)
# 查配置表获取每单位消耗
base_points = get_scene_points(scene_key)
total_points = base_points * units
else:
total_points = get_scene_points(scene_key)
# 免费用户检查
if not current_user.user.is_member:
if scene_key == "free_clip":
# 检查每日免费额度
if not check_and_incr_daily_usage(current_user.user.id, "free_clip"):
raise HTTPException(
status_code=402,
detail={
"code": "DAILY_LIMIT_EXCEEDED",
"message": "今日免费混剪次数已用完,开通会员享无限混剪",
"upgrade_url": "/app/subscription"
}
)
# 免费混剪不扣积分
kwargs["_points_deducted"] = 0
return await func(*args, **kwargs)
else:
raise HTTPException(
status_code=403,
detail={
"code": "MEMBER_REQUIRED",
"message": "此功能需要开通付费会员",
"upgrade_url": "/app/subscription"
}
)
# 付费用户:检查并扣减积分
points_service = get_points_service()
result = points_service.check_and_deduct(
user_id=current_user.user.id,
scene_key=scene_key,
amount=total_points,
)
if not result.success:
raise HTTPException(
status_code=402,
detail={
"code": "INSUFFICIENT_POINTS",
"message": f"积分不足,需要 {total_points} 积分,当前余额 {result.balance}",
"recharge_url": "/app/points/recharge"
}
)
kwargs["_points_deduct_id"] = result.transaction_id
kwargs["_points_deducted"] = total_points
try:
return await func(*args, **kwargs)
except Exception as e:
# 业务失败,退还积分
points_service.refund(
user_id=current_user.user.id,
transaction_id=result.transaction_id,
reason=f"业务执行失败: {scene_key}"
)
raise
return wrapper
return decorator
8.3 每日免费额度检查(Redis)
# packages/middleware/daily_usage.py
import redis
from datetime import datetime, timezone, timedelta
# Redis key: daily_usage:{user_id}:{date}:{usage_type}
# TTL: 48 小时自动过期
def check_and_incr_daily_usage(user_id: str, usage_type: str, limit: int = 2) -> bool:
"""
检查并递增每日使用次数
返回 True 表示允许,False 表示已超限
"""
tz = timezone(timedelta(hours=8))
today = datetime.now(tz).strftime("%Y%m%d")
key = f"daily_usage:{user_id}:{today}:{usage_type}"
r = get_redis_client()
current = r.get(key)
if current and int(current) >= limit:
return False
pipe = r.pipeline()
pipe.incr(key)
pipe.expire(key, 48 * 3600) # 48 小时过期
results = pipe.execute()
# 同步到 DB(异步或定时任务)
sync_daily_usage_to_db(user_id, today, usage_type)
return True
8.4 与现有 QuotaChecker 的集成
现有 QuotaChecker 保留,负责检查存储、并发等基础配额。新增 PointsGate 负责积分扣费。两者在中间件层协作:
# 统一中间件入口
async def unified_quota_check(user, scene_key, **kwargs):
# Step 1: 基础配额(存储、并发等)
plan = "paid" if user.is_member else "free"
base_check = quota_checker.check_multiple(plan, {
QuotaDimension.STORAGE_GB.value: get_used_storage(user.id),
QuotaDimension.MAX_CONCURRENT.value: get_concurrent_count(user.id),
})
for result in base_check:
if not result.allowed:
raise QuotaExceededError(result)
# Step 2: 积分扣费
await points_gate_check(user, scene_key, **kwargs)
九、支付接入
9.1 支付渠道
| 渠道 | 优先级 | 说明 |
|---|---|---|
| 微信支付 | P0(首期) | 覆盖绝大多数用户 |
| 支付宝 | P1(后续) | 二期接入 |
9.2 支付流程
用户选择套餐/积分包
→ POST /api/v1/subscription/subscribe 或 /api/v1/points/recharge
→ 后端创建订单,调用微信支付统一下单 API
→ 返回支付参数(prepay_id / code_url)
→ 前端展示二维码 or 调起支付
→ 用户支付完成
→ 微信回调 POST /api/v1/subscription/payment-callback
→ 验签 → 更新订单状态 → 发放积分/开通会员
9.3 发票
- 初期不做自助开票
- 用户有需求时通过客服人工处理
9.4 退款策略
- 虚拟商品原则上不退
- 特殊情况(如系统故障导致积分异常消耗)由客服人工处理
十、老用户迁移方案
10.1 迁移策略
| 现有用户类型 | 迁移方案 |
|---|---|
| free 用户 | 直接标记 is_member = false,享受每日 2 条免费混剪 |
| standard 用户(¥99/月) | 自动转为年卡付费会员,有效期 12 个月(从迁移日起算),到期后按新价格续费 |
| pro 用户(¥299/月) | 同上,转年卡,赠送 500 额外积分 |
| enterprise 用户(¥999/月) | 同上,转年卡,赠送 2000 额外积分 + 客服 1v1 通知 |
10.2 迁移执行
-- Step 1: 新增字段
ALTER TABLE users ADD COLUMN IF NOT EXISTS is_member BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE users ADD COLUMN IF NOT EXISTS member_type VARCHAR(20) DEFAULT NULL;
ALTER TABLE users ADD COLUMN IF NOT EXISTS member_expires_at TIMESTAMP DEFAULT NULL;
ALTER TABLE users ADD COLUMN IF NOT EXISTS points_balance INTEGER NOT NULL DEFAULT 0;
-- Step 2: 迁移现有付费用户为年卡会员
UPDATE users SET
is_member = TRUE,
member_type = 'yearly',
member_expires_at = NOW() + INTERVAL '12 months',
points_balance = CASE
WHEN subscription_plan = 'standard' THEN 200
WHEN subscription_plan IN ('pro', 'enterprise') THEN 200
ELSE 0
END
WHERE subscription_plan IN ('standard', 'pro', 'enterprise')
AND subscription_status = 'active';
-- Step 3: 额外赠送积分
UPDATE users SET points_balance = points_balance + 500
WHERE subscription_plan = 'pro' AND subscription_status = 'active';
UPDATE users SET points_balance = points_balance + 2000
WHERE subscription_plan = 'enterprise' AND subscription_status = 'active';
-- Step 4: 创建积分账户
INSERT INTO points_accounts (id, user_id, balance, total_earned, total_gifted)
SELECT uuid(), id, points_balance, points_balance, points_balance
FROM users
WHERE points_balance > 0;
-- Step 5: 免费用户标记
UPDATE users SET is_member = FALSE WHERE subscription_plan = 'free' OR subscription_plan IS NULL;
10.3 迁移通知
- 迁移前 7 天:站内信 + 邮件通知
- 迁移当天:弹窗说明变更内容
- 重点说明:
- "您的权益不会减少"
- "已为您升级为年卡会员,有效期至 XXXX"
- "赠送 XX 积分,可用于 AI 配音、数字人等高级功能"
十一、文件改动总览
| 类型 | 文件路径 | 说明 |
|---|---|---|
| 新增 | migrations/007_membership_points.sql |
新建积分相关表 |
| 新增 | packages/domain/points_service.py |
积分核心服务 |
| 新增 | packages/middleware/points_gate.py |
积分扣费中间件 |
| 新增 | packages/middleware/daily_usage.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/ |
Header 积分徽章 |
| 新增 | apps/web/src/components/common/PointsCost/ |
消耗提示组件 |
| 新增 | apps/web/src/api/points.ts |
积分 API 封装 |
| 新增 | apps/web/src/store/pointsStore.ts |
积分状态管理 |
| 修改 | packages/domain/quota.py |
简化配额维度 |
| 修改 | packages/adapters/sqlalchemy_impl/models.py |
新增积分 Model + users 字段 |
| 修改 | apps/api/app/api/routes/subscription.py |
改造为两档会员 |
| 修改 | apps/api/app/schemas/subscription.py |
简化 Schema |
| 修改 | apps/web/src/pages/subscription/Plans.tsx |
两档会员 UI |
| 修改 | 各 AI 功能路由 | 添加 @points_gate 装饰器 |
十二、实施排期
| 阶段 | 内容 | 预估工时 |
|---|---|---|
| P1 | 数据库迁移 + Model + Repository | 2 天 |
| P2 | PointsService 核心逻辑 + 每日额度 | 2 天 |
| P3 | 积分 API + 微信支付接入 | 3 天 |
| P4 | 中间件改造(各 AI 功能接入) | 2 天 |
| P5 | 前端页面改造 + 新增页面 | 4 天 |
| P6 | 老用户迁移脚本 + 通知 | 1 天 |
| P7 | 集成测试 + 端到端测试 | 2 天 |
| 合计 | ~16 天 |
十三、开放问题(待后续讨论)
| # | 问题 | 当前决策 |
|---|---|---|
| 1 | 积分是否需要有效期? | 永久有效(已确认) |
| 2 | 退款策略? | 虚拟商品不退,特殊情况人工处理(已确认) |
| 3 | 企业版定制需求? | 暂不做,后续按需(已确认) |
| 4 | 支付渠道优先级? | 微信优先,支付宝二期(已确认) |
| 5 | 发票需求? | 暂不做,后续按需(已确认) |
| 6 | 免费用户每日 2 条混剪是否限制分辨率? | 建议限制 720p |
| 7 | 会员过期后未使用的积分是否清零? | 购买的不清零,赠送的 12 个月后清零 |
本方案基于真实 AI 调用成本核算,经用户确认后制定。待确认后即可进入开发阶段。