1
This commit is contained in:
28
alembic/versions/014_add_user_points.py
Normal file
28
alembic/versions/014_add_user_points.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""add users.points
|
||||
|
||||
Revision ID: 014
|
||||
Revises: 013
|
||||
Create Date: 2026-05-22
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "014"
|
||||
down_revision: Union[str, None] = "013"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"users",
|
||||
sa.Column("points", sa.Float(), nullable=False, server_default="0"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("users", "points")
|
||||
38
alembic/versions/015_add_card_key_type.py
Normal file
38
alembic/versions/015_add_card_key_type.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""add card_keys.card_type and points_amount
|
||||
|
||||
Revision ID: 015
|
||||
Revises: 014
|
||||
Create Date: 2026-05-22
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "015"
|
||||
down_revision: Union[str, None] = "014"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"card_keys",
|
||||
sa.Column(
|
||||
"card_type",
|
||||
sa.String(length=16),
|
||||
nullable=False,
|
||||
server_default="时长",
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"card_keys",
|
||||
sa.Column("points_amount", sa.Float(), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("card_keys", "points_amount")
|
||||
op.drop_column("card_keys", "card_type")
|
||||
6
app/core/card_key_types.py
Normal file
6
app/core/card_key_types.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""卡密类型常量。"""
|
||||
|
||||
CARD_TYPE_DURATION = "时长"
|
||||
CARD_TYPE_POINTS = "点数"
|
||||
|
||||
CARD_KEY_TYPES = frozenset({CARD_TYPE_DURATION, CARD_TYPE_POINTS})
|
||||
@@ -1,8 +1,9 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.card_key_types import CARD_TYPE_DURATION
|
||||
from app.database import Base
|
||||
|
||||
|
||||
@@ -11,7 +12,11 @@ class CardKey(Base):
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
serial_number: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
||||
card_type: Mapped[str] = mapped_column(
|
||||
String(16), default=CARD_TYPE_DURATION, server_default=CARD_TYPE_DURATION
|
||||
)
|
||||
duration_days: Mapped[int] = mapped_column(Integer)
|
||||
points_amount: Mapped[float | None] = mapped_column(Float, nullable=True, default=None)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.database import Base
|
||||
@@ -26,3 +26,4 @@ class User(Base):
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
device_serial: Mapped[str] = mapped_column(String(255), default="", server_default="",index=True)
|
||||
points: Mapped[float] = mapped_column(Float, default=0.0, server_default="0")
|
||||
@@ -3,13 +3,19 @@ from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
from app.core.card_key_types import CARD_TYPE_DURATION, CARD_TYPE_POINTS
|
||||
|
||||
CardKeyType = Literal["时长", "点数"]
|
||||
|
||||
|
||||
class CardKeyOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
serial_number: str
|
||||
card_type: CardKeyType
|
||||
duration_days: int
|
||||
points_amount: float | None = None
|
||||
created_at: datetime
|
||||
activated_at: datetime | None = None
|
||||
user_id: int | None = None
|
||||
@@ -22,7 +28,13 @@ class CardKeyOut(BaseModel):
|
||||
|
||||
|
||||
class CardKeyCreate(BaseModel):
|
||||
duration_days: int = Field(ge=1, le=3650, description="有效天数")
|
||||
card_type: CardKeyType = Field(default=CARD_TYPE_DURATION, description="卡密类型")
|
||||
duration_days: int | None = Field(
|
||||
default=None, ge=1, le=3650, description="时长卡有效天数"
|
||||
)
|
||||
points_amount: float | None = Field(
|
||||
default=None, gt=0, description="点数卡点数面值"
|
||||
)
|
||||
remark: str | None = Field(default=None, max_length=255)
|
||||
count: int = Field(default=1, ge=1, le=100, description="批量生成数量")
|
||||
serial_number: str | None = Field(default=None, max_length=64)
|
||||
@@ -46,16 +58,24 @@ class CardKeyCreate(BaseModel):
|
||||
return stripped or None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def serial_only_for_single(self) -> "CardKeyCreate":
|
||||
def validate_type_and_value(self) -> "CardKeyCreate":
|
||||
if self.serial_number and self.count != 1:
|
||||
raise ValueError("指定序列号时仅可生成 1 张卡密")
|
||||
if self.oem_id is not None and self.agent_id is not None:
|
||||
raise ValueError("OEM 与代理只能指定其一")
|
||||
if self.card_type == CARD_TYPE_DURATION:
|
||||
if self.duration_days is None:
|
||||
raise ValueError("时长卡密须填写有效天数")
|
||||
elif self.card_type == CARD_TYPE_POINTS:
|
||||
if self.points_amount is None:
|
||||
raise ValueError("点数卡密须填写点数")
|
||||
return self
|
||||
|
||||
|
||||
class CardKeyUpdate(BaseModel):
|
||||
card_type: CardKeyType | None = None
|
||||
duration_days: int | None = Field(default=None, ge=1, le=3650)
|
||||
points_amount: float | None = Field(default=None, gt=0)
|
||||
remark: str | None = Field(default=None, max_length=255)
|
||||
|
||||
@field_validator("remark")
|
||||
@@ -66,6 +86,14 @@ class CardKeyUpdate(BaseModel):
|
||||
stripped = value.strip()
|
||||
return stripped or None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_type_and_value(self) -> "CardKeyUpdate":
|
||||
if self.card_type == CARD_TYPE_DURATION and self.points_amount is not None:
|
||||
raise ValueError("时长卡密不能设置点数")
|
||||
if self.card_type == CARD_TYPE_POINTS and self.duration_days is not None:
|
||||
raise ValueError("点数卡密不能设置有效天数")
|
||||
return self
|
||||
|
||||
|
||||
class CardKeyBatchCreateResult(BaseModel):
|
||||
items: list[CardKeyOut]
|
||||
|
||||
@@ -5,6 +5,7 @@ from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.roles import ROLE_AGENT, ROLE_OEM
|
||||
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.admin_card_key import CardKeyCreate, CardKeyUpdate
|
||||
@@ -129,7 +130,9 @@ async def create_card_keys(db: AsyncSession, body: CardKeyCreate) -> list[CardKe
|
||||
)
|
||||
card = CardKey(
|
||||
serial_number=serial,
|
||||
duration_days=body.duration_days,
|
||||
card_type=body.card_type,
|
||||
duration_days=body.duration_days if body.card_type == CARD_TYPE_DURATION else 0,
|
||||
points_amount=body.points_amount if body.card_type == CARD_TYPE_POINTS else None,
|
||||
remark=body.remark,
|
||||
oem_id=body.oem_id,
|
||||
agent_id=body.agent_id,
|
||||
@@ -153,10 +156,30 @@ async def update_card_key(
|
||||
raise AdminCardKeyError("卡密不存在")
|
||||
|
||||
if card.activated_at is not None:
|
||||
if (
|
||||
body.duration_days is not None
|
||||
or body.points_amount is not None
|
||||
or body.card_type is not None
|
||||
):
|
||||
raise AdminCardKeyError("已激活卡密不能修改类型或面值")
|
||||
else:
|
||||
if body.card_type is not None:
|
||||
card.card_type = body.card_type
|
||||
if body.card_type == CARD_TYPE_DURATION:
|
||||
card.points_amount = None
|
||||
if body.duration_days is None and card.duration_days <= 0:
|
||||
raise AdminCardKeyError("时长卡密须填写有效天数")
|
||||
elif body.card_type == CARD_TYPE_POINTS:
|
||||
card.duration_days = 0
|
||||
if body.points_amount is None and card.points_amount is None:
|
||||
raise AdminCardKeyError("点数卡密须填写点数")
|
||||
|
||||
effective_type = card.card_type
|
||||
if effective_type == CARD_TYPE_DURATION:
|
||||
if body.duration_days is not None:
|
||||
raise AdminCardKeyError("已激活卡密不能修改时长")
|
||||
elif body.duration_days is not None:
|
||||
card.duration_days = body.duration_days
|
||||
elif body.points_amount is not None:
|
||||
card.points_amount = body.points_amount
|
||||
|
||||
if body.remark is not None:
|
||||
card.remark = body.remark
|
||||
|
||||
Reference in New Issue
Block a user