diff --git a/app/api/v1/card_keys.py b/app/api/v1/card_keys.py new file mode 100644 index 0000000..032abe7 --- /dev/null +++ b/app/api/v1/card_keys.py @@ -0,0 +1,33 @@ +"""用户卡密激活。""" + +from fastapi import APIRouter + +from app.dependencies import CurrentUser, DbSession +from app.schemas.card_key import CardKeyActivateRequest, CardKeyActivateResponse +from app.schemas.common import ApiResponse +from app.services import card_key_activate as activate_service + +router = APIRouter(prefix="/card-keys", tags=["卡密"]) + + +@router.post("/activate", response_model=ApiResponse[CardKeyActivateResponse]) +async def activate_card_key( + body: CardKeyActivateRequest, + current_user: CurrentUser, + db: DbSession, +) -> ApiResponse[CardKeyActivateResponse]: + try: + data = await activate_service.activate_card_key( + db, + user=current_user, + serial_number=body.serial_number, + ) + except activate_service.CardKeyActivateError as exc: + return ApiResponse(ok=False, message=exc.message) + + if data.card_type == "时长": + message = f"激活成功,会员已延长 {data.added_days} 天" + else: + message = f"激活成功,已获得 {data.added_points} 点数" + + return ApiResponse(ok=True, message=message, data=data) diff --git a/app/api/v1/router.py b/app/api/v1/router.py index 8da0a63..95898a2 100644 --- a/app/api/v1/router.py +++ b/app/api/v1/router.py @@ -4,6 +4,7 @@ from app.api.v1 import ( admin_card_keys, oem_card_keys, agent_card_keys, + card_keys, admin_desktop_configs, oem_desktop_configs, @@ -23,6 +24,7 @@ api_router = APIRouter() api_router.include_router(oem_users.router) api_router.include_router(oem_public.router) api_router.include_router(auth.router) +api_router.include_router(card_keys.router) api_router.include_router(admin_users.router) api_router.include_router(agent_users.router) api_router.include_router(admin_card_keys.router) diff --git a/app/schemas/auth.py b/app/schemas/auth.py index 38d2ed2..245f793 100644 --- a/app/schemas/auth.py +++ b/app/schemas/auth.py @@ -41,6 +41,7 @@ class UserPublic(BaseModel): username: str role_id: int vip_end_time: datetime | None = None + points: float = 0.0 class TokenResponse(BaseModel): diff --git a/app/schemas/card_key.py b/app/schemas/card_key.py new file mode 100644 index 0000000..4d92ff0 --- /dev/null +++ b/app/schemas/card_key.py @@ -0,0 +1,26 @@ +from datetime import datetime +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field, field_validator + +CardKeyType = Literal["时长", "点数"] + + +class CardKeyActivateRequest(BaseModel): + serial_number: str = Field(min_length=1, max_length=64) + + @field_validator("serial_number") + @classmethod + def normalize_serial(cls, value: str) -> str: + stripped = value.strip().upper() + if not stripped: + raise ValueError("请输入卡密") + return stripped + + +class CardKeyActivateResponse(BaseModel): + card_type: CardKeyType + vip_end_time: datetime | None = None + points: float | None = None + added_days: int | None = None + added_points: float | None = None diff --git a/app/services/card_key_activate.py b/app/services/card_key_activate.py new file mode 100644 index 0000000..3197699 --- /dev/null +++ b/app/services/card_key_activate.py @@ -0,0 +1,87 @@ +from datetime import datetime, timedelta, timezone + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.card_key_types import CARD_TYPE_DURATION, CARD_TYPE_POINTS +from app.models.card_key import CardKey +from app.models.user import User +from app.schemas.card_key import CardKeyActivateResponse + + +class CardKeyActivateError(Exception): + def __init__(self, message: str) -> None: + self.message = message + super().__init__(message) + + +def _utc_now() -> datetime: + return datetime.now(timezone.utc) + + +def _ensure_utc(value: datetime) -> datetime: + if value.tzinfo is None: + return value.replace(tzinfo=timezone.utc) + return value + + +def _extend_vip_end_time(current: datetime | None, days: int) -> datetime: + now = _utc_now() + if current is not None: + end = _ensure_utc(current) + base = end if end > now else now + else: + base = now + return base + timedelta(days=days) + + +async def activate_card_key( + db: AsyncSession, + *, + user: User, + serial_number: str, +) -> CardKeyActivateResponse: + result = await db.execute( + select(CardKey) + .where(CardKey.serial_number == serial_number) + .with_for_update() + ) + card = result.scalar_one_or_none() + if card is None: + raise CardKeyActivateError("卡密不存在") + + if card.activated_at is not None: + raise CardKeyActivateError("卡密已被使用") + + now = _utc_now() + card.activated_at = now + card.user_id = user.id + card.username = user.username + + if card.card_type == CARD_TYPE_DURATION: + if card.duration_days < 1: + raise CardKeyActivateError("卡密配置无效") + user.vip_end_time = _extend_vip_end_time(user.vip_end_time, card.duration_days) + await db.flush() + await db.refresh(user) + return CardKeyActivateResponse( + card_type=card.card_type, + vip_end_time=user.vip_end_time, + points=user.points, + added_days=card.duration_days, + ) + + if card.card_type == CARD_TYPE_POINTS: + if card.points_amount is None or card.points_amount <= 0: + raise CardKeyActivateError("卡密配置无效") + user.points = (user.points or 0.0) + card.points_amount + await db.flush() + await db.refresh(user) + return CardKeyActivateResponse( + card_type=card.card_type, + vip_end_time=user.vip_end_time, + points=user.points, + added_points=card.points_amount, + ) + + raise CardKeyActivateError("卡密类型无效")