Files
life-echo/api/routers/plans.py
penghanyuan 141dd33901 feat: 更新免费版订阅计划以提供更多对话轮次和无章节限制
- 将免费版的对话轮次从 50 增加至 500,并移除章节限制,提升用户体验。
- 更新相关注释以反映新的订阅计划内容。
2026-02-14 13:24:30 +01:00

181 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
订阅计划相关 API 路由
"""
import os
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from middleware.auth import get_current_user
from database.models import User
from database import get_async_db
router = APIRouter(prefix="/api/plans", tags=["plans"])
class PlanResponse(BaseModel):
"""订阅计划响应"""
id: str
name: str
display_name: str
price: float
currency: str
features: List[str]
max_conversations: Optional[int] = None # None表示无限制
max_chapters: Optional[int] = None
max_words: Optional[int] = None
is_popular: bool = False
class CurrentPlanResponse(BaseModel):
"""当前订阅计划响应"""
plan_id: str
plan_name: str
subscription_type: str
expires_at: Optional[str] = None # 过期时间None表示永久
features: List[str]
usage: dict # 使用情况统计
# 预定义的订阅计划
# 免费版500 轮对话 + 无章节限制
# Pro88 元2000 轮对话,无章节限制
# Pro+288 元10000 轮对话,无章节限制
AVAILABLE_PLANS = [
PlanResponse(
id="free",
name="free",
display_name="免费体验版",
price=0.0,
currency="CNY",
features=[
"500 轮对话",
"无章节限制",
"完整回忆录生成流程"
],
max_conversations=500,
max_chapters=None,
max_words=None,
is_popular=False
),
PlanResponse(
id="pro",
name="pro",
display_name="Pro 版",
price=88.0,
currency="CNY",
features=[
"2000 轮对话",
"无章节限制",
"完整回忆录生成"
],
max_conversations=2000,
max_chapters=None,
max_words=None,
is_popular=True
),
PlanResponse(
id="pro_plus",
name="pro_plus",
display_name="Pro+ 版",
price=288.0,
currency="CNY",
features=[
"10000 轮对话",
"无章节限制",
"完整回忆录生成",
"长期创作无忧"
],
max_conversations=10000,
max_chapters=None,
max_words=None,
is_popular=False
)
]
# 一分钱测试套餐:仅当 ENABLE_TEST_PLAN=1 时开放,用于开发环境反复测试支付
ENABLE_TEST_PLAN = os.getenv("ENABLE_TEST_PLAN", "").lower() in ("1", "true", "yes")
TEST_PLAN = PlanResponse(
id="test",
name="test",
display_name="一分钱测试版",
price=0.01,
currency="CNY",
features=[
"无限对话",
"无限章节整理",
"仅用于开发环境测试支付"
],
max_conversations=None,
max_chapters=None,
max_words=None,
is_popular=False,
)
def get_plans_for_api() -> List[PlanResponse]:
"""返回对外暴露的套餐列表(含测试套餐当且仅当 ENABLE_TEST_PLAN 开启)。"""
if ENABLE_TEST_PLAN:
return AVAILABLE_PLANS + [TEST_PLAN]
return list(AVAILABLE_PLANS)
def get_plan_by_type(subscription_type: str) -> Optional[PlanResponse]:
"""根据订阅类型获取计划信息。旧字段 premium 按 pro 展示test 仅当 ENABLE_TEST_PLAN 时有效。"""
if subscription_type == "premium":
subscription_type = "pro"
if subscription_type == "test":
return TEST_PLAN if ENABLE_TEST_PLAN else AVAILABLE_PLANS[0]
for plan in AVAILABLE_PLANS:
if plan.id == subscription_type:
return plan
return AVAILABLE_PLANS[0] # 默认返回免费版
@router.get("", response_model=List[PlanResponse])
async def get_plans():
"""
获取所有可用的订阅计划(开发环境 ENABLE_TEST_PLAN=1 时包含「一分钱测试版」)。
"""
return get_plans_for_api()
@router.get("/current", response_model=CurrentPlanResponse)
async def get_current_plan(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""
获取当前用户的订阅计划信息
"""
plan = get_plan_by_type(current_user.subscription_type)
# 计算使用情况(对话轮数 = Segment 数量)
from routers.quota import get_segment_count, get_chapter_count
segment_count = await get_segment_count(current_user.id, db)
chapter_count = await get_chapter_count(current_user.id, db)
usage = {
"conversations": segment_count, # 已用对话轮数
"chapters": chapter_count,
"max_conversations": plan.max_conversations,
"max_chapters": plan.max_chapters,
}
expires_at = None
if current_user.subscription_expires_at:
expires_at = current_user.subscription_expires_at.isoformat()
return CurrentPlanResponse(
plan_id=plan.id,
plan_name=plan.display_name,
subscription_type=current_user.subscription_type,
expires_at=expires_at,
features=plan.features,
usage=usage
)