Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ AI_IMAGE_MODEL=gemini-2.5-flash-image
AI_VIDEO_MODEL=kling-v2-5-turbo

# ── 积分定价 ──
QUOTA_REGISTER_GIFT_AMOUNT=100
QUOTA_INVITE_REWARD_AMOUNT=50
QUOTA_REGISTER_GIFT_AMOUNT=300
QUOTA_INVITE_REWARD_AMOUNT=200
QUOTA_INVITE_REWARD_DAILY_LIMIT=3
QUOTA_INVITE_CODE_TTL_DAYS=30
QUOTA_GENERATE_IMAGE_COST=10
QUOTA_GENERATE_ACTION_COST=50

Expand Down
3 changes: 1 addition & 2 deletions backend/packages/app/src/windup_app/bootstrap/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@
from windup_app.server.character.service import service as character_service
from windup_app.server.orchestrator.dispatcher import GenerationDispatcher
from windup_app.server.project.model import Project # noqa: F401
from windup_app.server.quota.model import CreditAccount, CreditTransaction # noqa: F401
# InviteCode, InviteRecord, TokenUsage 暂不实现
from windup_app.server.quota.model import CreditAccount, CreditTransaction, InviteCode, InviteRecord # noqa: F401
from windup_app.server.user.model import User # noqa: F401
from windup_app.server.workflow_run.model import WorkflowRun # noqa: F401
from windup_app.web.api.auth import router as auth_router
Expand Down
50 changes: 33 additions & 17 deletions backend/packages/app/src/windup_app/server/quota/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from windup_app.server.quota.model import (
CreditAccountView,
CreditTransactionView,
InviteCode,
InviteCodeView,
)


Expand All @@ -35,7 +37,12 @@ def reserve_credit(

@abstractmethod
def capture_credit(
self, session: Session, user_id: int, actual_amount: int, ref_id: str, frozen_amount: int
self,
session: Session,
user_id: int,
actual_amount: int,
ref_id: str,
frozen_amount: int,
) -> None:
"""预付费扣减:冻结转消耗。

Expand Down Expand Up @@ -65,7 +72,12 @@ def release_credit(

@abstractmethod
def credit(
self, session: Session, user_id: int, amount: int, reason: int, ref_id: str | None = None
self,
session: Session,
user_id: int,
amount: int,
reason: int,
ref_id: str | None = None,
) -> None:
"""入账:增加可用余额与累计获得。"""

Expand All @@ -78,18 +90,22 @@ def list_transactions(
"""分页查询积分流水,返回 (列表, 总数)。"""

# -- 邀请码 -----------------------------------------------------------
# TODO 目前先不实现。
# @abstractmethod
# def get_invite_code(self, session: Session, user_id: int) -> InviteCodeView | None:
# """获取用户当前邀请码。"""
#
# @abstractmethod
# def generate_invite_code(self, session: Session, user_id: int) -> InviteCodeView:
# """生成新邀请码(替换旧码)。"""
#
# @abstractmethod
# def redeem_invite_code(self, session: Session, user_id: int, code: str) -> None:
# """兑换邀请码,双方各得积分。
#
# :raises BizException: 邀请码无效 / 已达上限 / 已填过码。
# """

@abstractmethod
def get_invite_code(self, session: Session, user_id: int) -> InviteCodeView:
"""获取当前未过期邀请码;没有或已过期则签发新行。"""

@abstractmethod
def generate_invite_code(self, session: Session, user_id: int) -> InviteCodeView:
"""签发新邀请码:插入新行,仍有效的旧码立即过期但保留。"""

@abstractmethod
def require_active_invite(self, session: Session, code: str) -> InviteCode:
"""注册前校验邀请码存在且未过期。非法返回「邀请码无效」,过期返回「邀请码已过期」。"""

@abstractmethod
def redeem_invite_code(self, session: Session, user_id: int, code: str) -> None:
"""注册时兑换邀请码。被邀请人始终得邀请奖励;邀请人受每日人数上限。

:raises BizException: 邀请码无效 / 已过期 / 已填过码 / 不能填自己的码。
"""
105 changes: 84 additions & 21 deletions backend/packages/app/src/windup_app/server/quota/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,19 @@
"""

from dataclasses import dataclass, field
from datetime import datetime, timezone

from sqlalchemy import BigInteger, DateTime, Integer, SmallInteger, String, UniqueConstraint
from datetime import datetime, timedelta, timezone

from sqlalchemy import (
BigInteger,
DateTime,
Integer,
SmallInteger,
String,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column

from windup_framework.config.quota import settings as quota_settings
from windup_framework.db import Base


Expand Down Expand Up @@ -107,18 +115,70 @@ class CreditTransaction(Base):
)


# -- 以下 ORM 暂不实现(枚举 / 接口已预留)----------------------------------
#
# class InviteCode(Base):
# """邀请码。"""
# __tablename__ = "windup_invite_code"
# ...
#
# class InviteRecord(Base):
# """邀请记录。"""
# __tablename__ = "windup_invite_record"
# ...
#
class InviteCode(Base):
"""用户邀请码。只增不删;轮换插入新行,旧行保留。"""

__tablename__ = "windup_invite_code"

id: Mapped[int] = mapped_column(
BigInteger().with_variant(Integer, "sqlite"),
primary_key=True,
autoincrement=True,
)
user_id: Mapped[int] = mapped_column(
BigInteger().with_variant(Integer, "sqlite"),
index=True,
nullable=False,
)
code: Mapped[str] = mapped_column(String(16), unique=True, nullable=False)
used_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
expires_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
default=lambda: datetime.now(timezone.utc)
+ timedelta(days=quota_settings.invite_code_ttl_days),
)
create_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
default=lambda: datetime.now(timezone.utc),
)
update_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
)


class InviteRecord(Base):
"""一次成功的邀请关系。被邀请人只能出现一次。"""

__tablename__ = "windup_invite_record"

id: Mapped[int] = mapped_column(
BigInteger().with_variant(Integer, "sqlite"),
primary_key=True,
autoincrement=True,
)
inviter_id: Mapped[int] = mapped_column(
BigInteger().with_variant(Integer, "sqlite"),
nullable=False,
index=True,
)
invitee_id: Mapped[int] = mapped_column(
BigInteger().with_variant(Integer, "sqlite"),
unique=True,
nullable=False,
)
code: Mapped[str] = mapped_column(String(16), nullable=False)
create_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
default=lambda: datetime.now(timezone.utc),
)


# class TokenUsage(Base):
# """Token 用量记录。"""
# __tablename__ = "windup_token_usage"
Expand Down Expand Up @@ -156,9 +216,12 @@ class CreditTransactionView:
create_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))


# -- 暂不实现 --
#
# @dataclass
# class InviteCodeView:
# """邀请码视图。"""
# ...
@dataclass
class InviteCodeView:
"""邀请码视图。"""

code: str
used_count: int = 0
expires_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
create_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
update_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
Loading
Loading