13c302c037
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 54s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 55s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m5s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m2s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 54s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m24s
CI/CD Pipeline / Integration Tests (push) Successful in 3m32s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m42s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m7s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m6s
CI/CD Pipeline / Validate - Style (push) Successful in 5m29s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m23s
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Failing after 6h5m2s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 6h2m58s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 6h5m9s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 6h2m58s
CI/CD Pipeline / PR Build API Image (push) Failing after 6h4m9s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 6h2m58s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 6h4m8s
CI/CD Pipeline / Frontend Lint (push) Failing after 6h4m10s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
139 lines
4.6 KiB
Python
139 lines
4.6 KiB
Python
"""积分消耗规则配置 (#1895)"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
# ============ 场景定义 ============
|
|
# 每个场景: base_points(基础积分), unit(计费单位), name(显示名称)
|
|
|
|
POINTS_SCENES: dict[str, dict] = {
|
|
"ai_voice": {
|
|
"base_points": 1,
|
|
"unit": "分钟",
|
|
"name": "AI 配音",
|
|
"description": "AI 配音每分钟消耗 1 积分(免费用户上浮 15%,会员 8~9 折)",
|
|
},
|
|
"ai_video": {
|
|
"base_points": 3,
|
|
"unit": "条",
|
|
"name": "智能混剪",
|
|
"extra_per_30s": 1,
|
|
"description": "智能混剪每条 3 积分起,视频超过 30 秒后每 30 秒加 1 积分;免费用户每日 2 条免费额度",
|
|
},
|
|
"ai_digital_human": {
|
|
"base_points": 15,
|
|
"unit": "分钟",
|
|
"name": "AI 数字人",
|
|
"description": "AI 数字人每分钟消耗 15 积分",
|
|
},
|
|
"voice_clone_train": {
|
|
"base_points": 0,
|
|
"unit": "次",
|
|
"name": "声音克隆训练",
|
|
"description": "声音克隆训练免费(每用户限 1 个声音)",
|
|
},
|
|
"voice_clone_synth": {
|
|
"base_points": 1,
|
|
"unit": "分钟",
|
|
"name": "声音克隆合成",
|
|
"description": "克隆音色合成每分钟消耗 1 积分",
|
|
},
|
|
"douyin_extract": {
|
|
"base_points": 1,
|
|
"unit": "次",
|
|
"name": "抖音链接提取",
|
|
"description": "抖音文案提取每次 1 积分",
|
|
},
|
|
"ai_rewrite": {"base_points": 1, "unit": "次", "name": "AI 改写文案", "description": "AI 改写文案每次 1 积分"},
|
|
"ai_title": {
|
|
"base_points": 1,
|
|
"unit": "次",
|
|
"name": "AI 标题生成",
|
|
"description": "AI 生成标题每次 1 积分(免费用户实际上浮后 2 积分/次)",
|
|
},
|
|
"ai_cover": {"base_points": 1, "unit": "张", "name": "AI 封面生成", "description": "AI 封面生成每张 1 积分"},
|
|
}
|
|
|
|
# 免费用户积分消耗上浮系数
|
|
FREE_USER_MULTIPLIER = 1.15
|
|
|
|
# ============ 积分包定义 ============
|
|
|
|
POINTS_PACKAGES: dict[str, dict] = {
|
|
"starter_pack": {"name": "体验包", "points": 100, "price_cents": 990},
|
|
"basic_pack": {"name": "基础包", "points": 500, "price_cents": 3900},
|
|
"pro_pack": {"name": "专业包", "points": 2000, "price_cents": 12900},
|
|
}
|
|
|
|
# ============ 会员定价 ============
|
|
|
|
MEMBERSHIP_PRICES: dict[str, dict] = {
|
|
"monthly": {"name": "月卡", "price_cents": 1990, "duration_days": 30},
|
|
"quarterly": {"name": "季卡", "price_cents": 3990, "duration_days": 90},
|
|
"yearly": {"name": "年卡", "price_cents": 15900, "duration_days": 365},
|
|
}
|
|
|
|
# 会员积分折扣(付费会员按此系数打折)
|
|
MEMBER_DISCOUNT: dict[str, float] = {
|
|
"monthly": 0.9,
|
|
"quarterly": 0.87,
|
|
"yearly": 0.8,
|
|
}
|
|
|
|
# 每日免费混剪次数(免费用户)
|
|
DAILY_FREE_CLIP_LIMIT = 2
|
|
|
|
|
|
def calculate_points_cost(
|
|
scene_key: str,
|
|
is_member: bool,
|
|
quantity: int = 1,
|
|
duration_minutes: float = 0,
|
|
member_type: str | None = None,
|
|
) -> int:
|
|
"""计算指定场景的积分消耗。
|
|
|
|
Args:
|
|
scene_key: 场景标识,如 "ai_voice"、"ai_video"
|
|
is_member: 是否付费会员
|
|
quantity: 数量(按次计费场景)
|
|
duration_minutes: 时长分钟数(按时长计费场景)
|
|
member_type: 会员类型 (monthly/quarterly/yearly),用于折扣
|
|
|
|
Returns:
|
|
实际消耗积分(已含免费用户 ×1.15 上浮或会员折扣)
|
|
|
|
Raises:
|
|
ValueError: 未知场景标识
|
|
"""
|
|
scene = POINTS_SCENES.get(scene_key)
|
|
if not scene:
|
|
raise ValueError(f"Unknown points scene: {scene_key}")
|
|
|
|
base = scene["base_points"]
|
|
if base == 0:
|
|
return 0
|
|
|
|
# —— 计算基础消耗 ——
|
|
unit = scene["unit"]
|
|
if unit == "分钟":
|
|
total_base = base * max(1, math.ceil(duration_minutes))
|
|
elif unit in ("条", "次", "张"):
|
|
total_base = base * quantity
|
|
# 混剪特殊逻辑:视频超过 30s 后每 +30s 额外加 1 积分
|
|
if scene_key == "ai_video" and duration_minutes > 0.5:
|
|
extra_segments = math.ceil((duration_minutes * 60 - 30) / 30)
|
|
if extra_segments > 0:
|
|
total_base += scene.get("extra_per_30s", 1) * extra_segments
|
|
else:
|
|
total_base = base
|
|
|
|
# —— 会员折扣 / 免费用户上浮 ——
|
|
if is_member and member_type and member_type in MEMBER_DISCOUNT:
|
|
total_base = max(1, math.floor(total_base * MEMBER_DISCOUNT[member_type]))
|
|
elif not is_member:
|
|
total_base = math.ceil(total_base * FREE_USER_MULTIPLIER)
|
|
|
|
return total_base
|