2026-01-23 14:02:36 +08:00
|
|
|
|
"""
|
|
|
|
|
|
用户相关 API 路由
|
|
|
|
|
|
"""
|
2026-02-10 14:23:40 +08:00
|
|
|
|
import os
|
|
|
|
|
|
from datetime import timedelta
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
2026-01-23 14:02:36 +08:00
|
|
|
|
from pydantic import BaseModel
|
2026-02-10 14:23:40 +08:00
|
|
|
|
from typing import Optional, Literal
|
2026-01-23 14:02:36 +08:00
|
|
|
|
|
|
|
|
|
|
from middleware.auth import get_current_user
|
2026-02-10 14:23:40 +08:00
|
|
|
|
from database.models import User, utc_now
|
|
|
|
|
|
from database import get_async_db
|
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
2026-01-23 14:02:36 +08:00
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/api/user", tags=["user"])
|
|
|
|
|
|
|
2026-02-10 14:23:40 +08:00
|
|
|
|
# 是否开启测试订阅(仅用于微信支付审核未通过前的测试)
|
|
|
|
|
|
ENABLE_TEST_SUBSCRIPTION = os.getenv("ENABLE_TEST_SUBSCRIPTION", "").lower() in ("1", "true", "yes")
|
|
|
|
|
|
|
2026-01-23 14:02:36 +08:00
|
|
|
|
|
|
|
|
|
|
class UserProfileResponse(BaseModel):
|
|
|
|
|
|
"""用户资料响应"""
|
|
|
|
|
|
id: str
|
|
|
|
|
|
phone: str
|
|
|
|
|
|
email: Optional[str]
|
|
|
|
|
|
nickname: str
|
|
|
|
|
|
avatar_url: Optional[str]
|
|
|
|
|
|
subscription_type: str
|
|
|
|
|
|
created_at: str
|
2026-03-01 10:12:23 +01:00
|
|
|
|
birth_year: Optional[int] = None
|
|
|
|
|
|
birth_place: Optional[str] = None
|
|
|
|
|
|
grew_up_place: Optional[str] = None
|
|
|
|
|
|
occupation: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UpdateUserProfileRequest(BaseModel):
|
|
|
|
|
|
"""更新用户基础资料请求"""
|
|
|
|
|
|
birth_year: Optional[int] = None
|
|
|
|
|
|
birth_place: Optional[str] = None
|
|
|
|
|
|
grew_up_place: Optional[str] = None
|
|
|
|
|
|
occupation: Optional[str] = None
|
2026-01-23 14:02:36 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-02-10 14:23:40 +08:00
|
|
|
|
class TestSubscriptionRequest(BaseModel):
|
|
|
|
|
|
"""测试订阅请求"""
|
|
|
|
|
|
action: Literal["activate", "deactivate"]
|
|
|
|
|
|
plan_id: Optional[str] = "pro" # activate 时生效,仅支持 pro / pro_plus
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestSubscriptionResponse(BaseModel):
|
|
|
|
|
|
"""测试订阅响应"""
|
|
|
|
|
|
success: bool
|
|
|
|
|
|
message: str
|
|
|
|
|
|
subscription_type: str
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-23 14:02:36 +08:00
|
|
|
|
@router.get("/profile", response_model=UserProfileResponse)
|
|
|
|
|
|
async def get_user_profile(
|
|
|
|
|
|
current_user: User = Depends(get_current_user)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取当前用户资料
|
2026-02-10 14:23:40 +08:00
|
|
|
|
|
2026-01-23 14:02:36 +08:00
|
|
|
|
与 /api/auth/me 功能相同,但路径不同以满足前端需求
|
|
|
|
|
|
"""
|
|
|
|
|
|
return UserProfileResponse(
|
|
|
|
|
|
id=current_user.id,
|
|
|
|
|
|
phone=current_user.phone,
|
|
|
|
|
|
email=current_user.email,
|
|
|
|
|
|
nickname=current_user.nickname,
|
|
|
|
|
|
avatar_url=current_user.avatar_url,
|
|
|
|
|
|
subscription_type=current_user.subscription_type,
|
2026-03-01 10:12:23 +01:00
|
|
|
|
created_at=current_user.created_at.isoformat(),
|
|
|
|
|
|
birth_year=current_user.birth_year,
|
|
|
|
|
|
birth_place=current_user.birth_place,
|
|
|
|
|
|
grew_up_place=current_user.grew_up_place,
|
|
|
|
|
|
occupation=current_user.occupation,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/profile", response_model=UserProfileResponse)
|
|
|
|
|
|
async def update_user_profile(
|
|
|
|
|
|
body: UpdateUserProfileRequest,
|
|
|
|
|
|
current_user: User = Depends(get_current_user),
|
|
|
|
|
|
db: AsyncSession = Depends(get_async_db),
|
|
|
|
|
|
):
|
|
|
|
|
|
"""更新用户基础资料(出生年份、出生地、成长地、职业)"""
|
|
|
|
|
|
if body.birth_year is not None:
|
|
|
|
|
|
current_user.birth_year = body.birth_year
|
|
|
|
|
|
if body.birth_place is not None:
|
|
|
|
|
|
current_user.birth_place = body.birth_place
|
|
|
|
|
|
if body.grew_up_place is not None:
|
|
|
|
|
|
current_user.grew_up_place = body.grew_up_place
|
|
|
|
|
|
if body.occupation is not None:
|
|
|
|
|
|
current_user.occupation = body.occupation
|
|
|
|
|
|
await db.commit()
|
|
|
|
|
|
await db.refresh(current_user)
|
|
|
|
|
|
return UserProfileResponse(
|
|
|
|
|
|
id=current_user.id,
|
|
|
|
|
|
phone=current_user.phone,
|
|
|
|
|
|
email=current_user.email,
|
|
|
|
|
|
nickname=current_user.nickname,
|
|
|
|
|
|
avatar_url=current_user.avatar_url,
|
|
|
|
|
|
subscription_type=current_user.subscription_type,
|
|
|
|
|
|
created_at=current_user.created_at.isoformat(),
|
|
|
|
|
|
birth_year=current_user.birth_year,
|
|
|
|
|
|
birth_place=current_user.birth_place,
|
|
|
|
|
|
grew_up_place=current_user.grew_up_place,
|
|
|
|
|
|
occupation=current_user.occupation,
|
2026-01-23 14:02:36 +08:00
|
|
|
|
)
|
2026-02-10 14:23:40 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/test-subscription", response_model=TestSubscriptionResponse)
|
|
|
|
|
|
async def test_subscription(
|
|
|
|
|
|
body: TestSubscriptionRequest,
|
|
|
|
|
|
current_user: User = Depends(get_current_user),
|
|
|
|
|
|
db: AsyncSession = Depends(get_async_db),
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
测试订阅开关(仅当 ENABLE_TEST_SUBSCRIPTION=1 时可用)。
|
|
|
|
|
|
|
|
|
|
|
|
- activate:将当前用户设为付费套餐(pro 或 pro_plus),用于在微信支付审核通过前测试付费后额度。
|
|
|
|
|
|
- deactivate:恢复为免费体验版。
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not ENABLE_TEST_SUBSCRIPTION:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="测试订阅功能未开放")
|
|
|
|
|
|
|
|
|
|
|
|
now = utc_now()
|
|
|
|
|
|
|
|
|
|
|
|
if body.action == "activate":
|
|
|
|
|
|
if body.plan_id not in ("pro", "pro_plus"):
|
|
|
|
|
|
raise HTTPException(status_code=400, detail="plan_id 仅支持 pro 或 pro_plus")
|
|
|
|
|
|
current_user.subscription_type = body.plan_id
|
|
|
|
|
|
current_user.subscription_expires_at = now + timedelta(days=365)
|
|
|
|
|
|
await db.flush()
|
|
|
|
|
|
return TestSubscriptionResponse(
|
|
|
|
|
|
success=True,
|
|
|
|
|
|
message=f"已开启测试订阅:{body.plan_id}",
|
|
|
|
|
|
subscription_type=body.plan_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# deactivate
|
|
|
|
|
|
current_user.subscription_type = "free"
|
|
|
|
|
|
current_user.subscription_expires_at = None
|
|
|
|
|
|
await db.flush()
|
|
|
|
|
|
return TestSubscriptionResponse(
|
|
|
|
|
|
success=True,
|
|
|
|
|
|
message="已关闭测试订阅,恢复免费体验版",
|
|
|
|
|
|
subscription_type="free",
|
|
|
|
|
|
)
|