diff --git a/alembic/versions/014_add_user_points.py b/alembic/versions/014_add_user_points.py new file mode 100644 index 0000000..a646210 --- /dev/null +++ b/alembic/versions/014_add_user_points.py @@ -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") diff --git a/alembic/versions/015_add_card_key_type.py b/alembic/versions/015_add_card_key_type.py new file mode 100644 index 0000000..b6d7012 --- /dev/null +++ b/alembic/versions/015_add_card_key_type.py @@ -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") diff --git a/app/core/card_key_types.py b/app/core/card_key_types.py new file mode 100644 index 0000000..774e4e2 --- /dev/null +++ b/app/core/card_key_types.py @@ -0,0 +1,6 @@ +"""卡密类型常量。""" + +CARD_TYPE_DURATION = "时长" +CARD_TYPE_POINTS = "点数" + +CARD_KEY_TYPES = frozenset({CARD_TYPE_DURATION, CARD_TYPE_POINTS}) diff --git a/app/models/card_key.py b/app/models/card_key.py index b280ff7..c49e714 100644 --- a/app/models/card_key.py +++ b/app/models/card_key.py @@ -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() ) diff --git a/app/models/user.py b/app/models/user.py index f67f178..85c8ec7 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -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 @@ -25,4 +25,5 @@ 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) \ No newline at end of file + 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") \ No newline at end of file diff --git a/app/schemas/admin_card_key.py b/app/schemas/admin_card_key.py index 85941fd..a3f54f3 100644 --- a/app/schemas/admin_card_key.py +++ b/app/schemas/admin_card_key.py @@ -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] diff --git a/app/services/admin_card_key.py b/app/services/admin_card_key.py index 89f882e..58c0f10 100644 --- a/app/services/admin_card_key.py +++ b/app/services/admin_card_key.py @@ -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: - raise AdminCardKeyError("已激活卡密不能修改时长") - elif body.duration_days is not None: - card.duration_days = body.duration_days + 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: + 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