11
This commit is contained in:
33
app/api/v1/card_keys.py
Normal file
33
app/api/v1/card_keys.py
Normal file
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
26
app/schemas/card_key.py
Normal file
26
app/schemas/card_key.py
Normal file
@@ -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
|
||||
87
app/services/card_key_activate.py
Normal file
87
app/services/card_key_activate.py
Normal file
@@ -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("卡密类型无效")
|
||||
Reference in New Issue
Block a user