提供基本前后端骨架

This commit is contained in:
hisatri
2026-01-06 23:49:23 +08:00
parent 84d4ccc226
commit 06f8176e23
89 changed files with 19293 additions and 2 deletions

346
app/schemas/redeem_code.py Normal file
View File

@@ -0,0 +1,346 @@
"""
兑换码相关 Schema
定义兑换码数据的验证和序列化规则。
"""
from datetime import datetime
from typing import Annotated
from pydantic import Field, field_validator
from app.schemas.base import BaseSchema, PaginatedResponse
from app.schemas.balance import UNITS_PER_DISPLAY, display_to_units, format_display
from app.models.redeem_code import RedeemCodeStatus
# ============================================================
# 用户兑换 Schema
# ============================================================
class RedeemRequest(BaseSchema):
"""兑换请求"""
code: Annotated[
str,
Field(
min_length=1,
max_length=32,
description="兑换码",
examples=["ABCD-EFGH-JKLM-NPQR"],
),
]
@field_validator("code")
@classmethod
def normalize_code(cls, v: str) -> str:
"""标准化兑换码格式"""
# 移除空格,转大写
return v.strip().upper().replace(" ", "")
class RedeemResponse(BaseSchema):
"""兑换响应"""
success: bool = True
message: str = "兑换成功"
face_value: str = Field(description="兑换金额")
balance_before: str = Field(description="兑换前余额")
balance_after: str = Field(description="兑换后余额")
# ============================================================
# 兑换码信息 Schema
# ============================================================
class RedeemCodeResponse(BaseSchema):
"""兑换码信息响应"""
id: str
code: str
face_value_units: int = Field(description="面值(单位额度)")
status: RedeemCodeStatus
max_uses: int
used_count: int
expires_at: datetime | None
used_at: datetime | None
created_at: datetime
@property
def face_value(self) -> str:
"""显示面值2 位小数)"""
return format_display(self.face_value_units)
@property
def is_valid(self) -> bool:
"""是否有效"""
if self.status != RedeemCodeStatus.ACTIVE:
return False
if self.used_count >= self.max_uses:
return False
if self.expires_at and self.expires_at < datetime.now(self.expires_at.tzinfo):
return False
return True
class RedeemCodeDetailResponse(RedeemCodeResponse):
"""兑换码详细信息响应(管理员用)"""
batch_id: str | None
batch_name: str | None = None
remark: str | None
created_by: str | None
used_by: str | None
class RedeemCodeListResponse(PaginatedResponse[RedeemCodeDetailResponse]):
"""兑换码列表响应"""
pass
# ============================================================
# 批次 Schema
# ============================================================
class BatchCreateRequest(BaseSchema):
"""创建兑换码批次请求"""
name: Annotated[
str,
Field(
min_length=1,
max_length=128,
description="批次名称",
examples=["2024年1月活动"],
),
]
description: Annotated[
str | None,
Field(
default=None,
max_length=500,
description="批次描述",
),
]
face_value: Annotated[
float,
Field(
gt=0,
description="面值(显示金额)",
examples=[10.00, 50.00],
),
]
count: Annotated[
int,
Field(
gt=0,
le=10000,
description="生成数量",
examples=[100],
),
]
max_uses: Annotated[
int,
Field(
default=1,
gt=0,
le=100,
description="每个兑换码最大使用次数",
),
]
expires_at: Annotated[
datetime | None,
Field(
default=None,
description="过期时间",
),
]
@field_validator("face_value")
@classmethod
def validate_face_value(cls, v: float) -> float:
"""验证面值精度"""
if round(v, 3) != v:
raise ValueError("面值精度不能超过 3 位小数")
return v
@property
def face_value_units(self) -> int:
"""转换为单位额度"""
return display_to_units(self.face_value)
class BatchResponse(BaseSchema):
"""批次信息响应"""
id: str
name: str
description: str | None
face_value_units: int
total_count: int
used_count: int
created_by: str | None
created_at: datetime
@property
def face_value(self) -> str:
"""显示面值2 位小数)"""
return format_display(self.face_value_units)
@property
def unused_count(self) -> int:
"""未使用数量"""
return self.total_count - self.used_count
class BatchDetailResponse(BatchResponse):
"""批次详细信息响应"""
codes: list[RedeemCodeResponse] = []
class BatchListResponse(PaginatedResponse[BatchResponse]):
"""批次列表响应"""
pass
# ============================================================
# 导入导出 Schema
# ============================================================
class ImportCodeRequest(BaseSchema):
"""导入兑换码请求"""
code: Annotated[
str,
Field(
min_length=1,
max_length=32,
description="兑换码",
),
]
face_value: Annotated[
float,
Field(
gt=0,
description="面值",
),
]
max_uses: Annotated[
int,
Field(
default=1,
gt=0,
),
]
expires_at: datetime | None = None
remark: str | None = None
@field_validator("code")
@classmethod
def normalize_code(cls, v: str) -> str:
"""标准化兑换码"""
return v.strip().upper()
@property
def face_value_units(self) -> int:
"""转换为单位额度"""
return display_to_units(self.face_value)
class BulkImportRequest(BaseSchema):
"""批量导入兑换码请求"""
codes: Annotated[
list[ImportCodeRequest],
Field(
min_length=1,
max_length=1000,
description="兑换码列表",
),
]
batch_name: Annotated[
str | None,
Field(
default=None,
max_length=128,
description="批次名称(可选)",
),
]
class BulkImportResponse(BaseSchema):
"""批量导入响应"""
success_count: int
failed_count: int
failed_codes: list[str] = []
batch_id: str | None = None
class ExportCodeItem(BaseSchema):
"""导出兑换码条目"""
code: str
face_value: str
status: str
max_uses: int
used_count: int
expires_at: str | None
created_at: str
used_at: str | None
used_by: str | None
class ExportResponse(BaseSchema):
"""导出响应"""
total: int
codes: list[ExportCodeItem]
# ============================================================
# 使用日志 Schema
# ============================================================
class UsageLogResponse(BaseSchema):
"""兑换码使用日志响应"""
id: str
redeem_code_id: str
code_snapshot: str
user_id: str
username: str | None = None
face_value: str
ip_address: str | None
created_at: datetime
class UsageLogListResponse(PaginatedResponse[UsageLogResponse]):
"""使用日志列表响应"""
pass
# ============================================================
# 查询参数 Schema
# ============================================================
class RedeemCodeQueryParams(BaseSchema):
"""兑换码查询参数"""
status: RedeemCodeStatus | None = None
batch_id: str | None = None
code: str | None = None
created_after: datetime | None = None
created_before: datetime | None = None
class UsageLogQueryParams(BaseSchema):
"""使用日志查询参数"""
redeem_code_id: str | None = None
user_id: str | None = None
code: str | None = None
created_after: datetime | None = None
created_before: datetime | None = None