团队积分V1
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from app.models.base import Base, TimestampMixin, SoftDeleteMixin, engine, async_session, init_database, close_database
|
||||
from app.models.user import User
|
||||
from app.models.team import Team
|
||||
from app.models.team_manager_history import TeamManagerHistory
|
||||
from app.models.team_invitation import TeamInvitation
|
||||
from app.models.team_join_request import TeamJoinRequest
|
||||
from app.models.project import Project
|
||||
@@ -20,6 +21,7 @@ from app.models.recharge_package import RechargePackage
|
||||
from app.models.credit import (
|
||||
CreditProduct, CreditRecordAllocation, UserCreditBalance,
|
||||
UserCreditSubscription, UserCreditSubscriptionPeriod,
|
||||
TeamSubscriptionSeat, TeamSubscriptionSeatUsage,
|
||||
)
|
||||
from app.models.llm_billing import LlmBillingPolicyModel, LlmBillingExecution, LlmCallAttempt
|
||||
from app.models.operation_log import OperationLog
|
||||
@@ -56,6 +58,7 @@ __all__ = [
|
||||
"TokenUsage", "IndustryConfig", "VideoEngine", "CreditRatio",
|
||||
"MenuConfig", "RechargePackage", "CreditProduct", "CreditRecordAllocation", "UserCreditBalance",
|
||||
"UserCreditSubscription", "UserCreditSubscriptionPeriod",
|
||||
"TeamSubscriptionSeat", "TeamSubscriptionSeatUsage", "TeamManagerHistory",
|
||||
"LlmBillingPolicyModel", "LlmBillingExecution", "LlmCallAttempt",
|
||||
"OperationLog", "ContactRequest",
|
||||
"ChatGenerationTask", "ChatGenerationTaskEvent", "ChatProviderCallLog", "VideoUpscaleTask",
|
||||
|
||||
@@ -3,6 +3,8 @@ from app.models.credit.balance import UserCreditBalance
|
||||
from app.models.credit.product import CreditProduct
|
||||
from app.models.credit.subscription import UserCreditSubscription
|
||||
from app.models.credit.subscription_period import UserCreditSubscriptionPeriod
|
||||
from app.models.credit.team_seat import TeamSubscriptionSeat
|
||||
from app.models.credit.team_seat_usage import TeamSubscriptionSeatUsage
|
||||
|
||||
__all__ = [
|
||||
"CreditRecordAllocation",
|
||||
@@ -10,4 +12,6 @@ __all__ = [
|
||||
"CreditProduct",
|
||||
"UserCreditSubscription",
|
||||
"UserCreditSubscriptionPeriod",
|
||||
"TeamSubscriptionSeat",
|
||||
"TeamSubscriptionSeatUsage",
|
||||
]
|
||||
|
||||
@@ -6,6 +6,7 @@ from decimal import Decimal
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, Numeric, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.enums.credit_balance import CreditScope
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
@@ -16,6 +17,15 @@ class CreditRecordAllocation(Base, TimestampMixin):
|
||||
Index("ix_credit_record_allocations_balance", "credit_balance_id", "created_at"),
|
||||
Index("ix_credit_record_allocations_user_time", "user_id", "created_at"),
|
||||
Index("ix_credit_record_allocations_source_allocation", "source_allocation_id"),
|
||||
Index("ix_credit_record_allocations_team_time", "team_id_snapshot", "created_at", "id"),
|
||||
Index(
|
||||
"ix_credit_record_allocations_team_manager_time",
|
||||
"team_id_snapshot", "team_manager_id_snapshot", "created_at", "id",
|
||||
),
|
||||
Index(
|
||||
"ix_credit_record_allocations_team_period_user",
|
||||
"subscription_period_id_snapshot", "user_id", "allocation_action",
|
||||
),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
@@ -26,7 +36,8 @@ class CreditRecordAllocation(Base, TimestampMixin):
|
||||
String(32), ForeignKey("user_credit_balances.id", ondelete="RESTRICT"), nullable=False
|
||||
)
|
||||
user_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
|
||||
String(32), ForeignKey("users.id", ondelete="CASCADE"), nullable=False,
|
||||
comment="真实业务消费者;发放类记录为资金所有人",
|
||||
)
|
||||
source_allocation_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("credit_record_allocations.id", ondelete="SET NULL"), nullable=True
|
||||
@@ -36,8 +47,18 @@ class CreditRecordAllocation(Base, TimestampMixin):
|
||||
request_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
|
||||
credit_level_snapshot: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
credit_scope_snapshot: Mapped[str] = mapped_column(
|
||||
String(16), nullable=False, default=CreditScope.PERSONAL.value,
|
||||
server_default=CreditScope.PERSONAL.value,
|
||||
)
|
||||
source_type_snapshot: Mapped[str] = mapped_column(String(48), nullable=False)
|
||||
source_id_snapshot: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
team_id_snapshot: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
team_manager_id_snapshot: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
subscription_id_snapshot: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
subscription_period_id_snapshot: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
seat_id_snapshot: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
|
||||
valid_from_snapshot: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
expires_at_snapshot: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from decimal import Decimal
|
||||
from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Index, JSON, Numeric, String, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.enums.credit_balance import CreditBalanceStatus
|
||||
from app.enums.credit_balance import CreditBalanceStatus, CreditScope
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
@@ -23,19 +23,25 @@ class UserCreditBalance(Base, TimestampMixin):
|
||||
name="ck_user_credit_balances_amount_reconciled",
|
||||
),
|
||||
CheckConstraint("expires_at > valid_from", name="ck_user_credit_balances_valid_window"),
|
||||
CheckConstraint(
|
||||
"(credit_scope = 'personal' AND team_id IS NULL) OR "
|
||||
"(credit_scope = 'team' AND team_id IS NOT NULL AND subscription_id IS NOT NULL "
|
||||
"AND subscription_period_id IS NOT NULL)",
|
||||
name="ck_user_credit_balances_scope_fields",
|
||||
),
|
||||
Index(
|
||||
"ix_user_credit_balances_spendable",
|
||||
"user_id",
|
||||
"credit_level_rank",
|
||||
"expires_at",
|
||||
"valid_from",
|
||||
"id",
|
||||
"user_id", "credit_scope", "credit_level_rank", "expires_at", "valid_from", "id",
|
||||
postgresql_where=text("unspent_amount > 0 AND revoked_at IS NULL"),
|
||||
),
|
||||
Index(
|
||||
"ix_user_credit_balances_team_spendable",
|
||||
"team_id", "subscription_id", "subscription_period_id", "credit_level_rank", "expires_at", "id",
|
||||
postgresql_where=text("credit_scope = 'team' AND unspent_amount > 0 AND revoked_at IS NULL"),
|
||||
),
|
||||
Index(
|
||||
"ix_user_credit_balances_expire_due",
|
||||
"expires_at",
|
||||
"id",
|
||||
"expires_at", "id",
|
||||
postgresql_where=text(
|
||||
"unspent_amount > 0 AND expired_processed_at IS NULL AND revoked_at IS NULL"
|
||||
),
|
||||
@@ -48,7 +54,15 @@ class UserCreditBalance(Base, TimestampMixin):
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
user_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
String(32), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True,
|
||||
comment="资金所有人;团队积分为成交时队长",
|
||||
)
|
||||
credit_scope: Mapped[str] = mapped_column(
|
||||
String(16), nullable=False, default=CreditScope.PERSONAL.value,
|
||||
server_default=CreditScope.PERSONAL.value,
|
||||
)
|
||||
team_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("teams.id", ondelete="RESTRICT"), nullable=True
|
||||
)
|
||||
credit_level: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
credit_level_rank: Mapped[int] = mapped_column(nullable=False, default=20, server_default="20")
|
||||
@@ -86,7 +100,8 @@ class UserCreditBalance(Base, TimestampMixin):
|
||||
valid_from: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
|
||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(24), nullable=False, default=CreditBalanceStatus.ACTIVE.value, server_default=CreditBalanceStatus.ACTIVE.value
|
||||
String(24), nullable=False, default=CreditBalanceStatus.ACTIVE.value,
|
||||
server_default=CreditBalanceStatus.ACTIVE.value,
|
||||
)
|
||||
expired_processed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
@@ -8,22 +8,38 @@ from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.enums.credit_balance import CreditLevel
|
||||
from app.enums.credit_product import CreditProductType
|
||||
from app.models.base import Base, TimestampMixin
|
||||
from app.models.base import Base, SoftDeleteMixin, TimestampMixin
|
||||
|
||||
|
||||
class CreditProduct(Base, TimestampMixin):
|
||||
class CreditProduct(Base, TimestampMixin, SoftDeleteMixin):
|
||||
__tablename__ = "credit_products"
|
||||
__table_args__ = (
|
||||
Index("uq_credit_products_code", "product_code", unique=True),
|
||||
Index("ix_credit_products_public", "product_type", "is_active", "sort_order"),
|
||||
Index("ix_credit_products_public", "product_type", "deleted_at", "is_active", "sort_order"),
|
||||
CheckConstraint("price >= 0", name="ck_credit_products_price_nonnegative"),
|
||||
CheckConstraint("first_purchase_price IS NULL OR first_purchase_price >= 0", name="ck_credit_products_first_price_nonnegative"),
|
||||
CheckConstraint("regular_price IS NULL OR regular_price >= 0", name="ck_credit_products_regular_price_nonnegative"),
|
||||
CheckConstraint("activity_price IS NULL OR activity_price >= 0", name="ck_credit_products_activity_price_nonnegative"),
|
||||
CheckConstraint("monthly_grant_credits IS NULL OR monthly_grant_credits > 0", name="ck_credit_products_monthly_grant_positive"),
|
||||
CheckConstraint("grant_credits IS NULL OR grant_credits > 0", name="ck_credit_products_grant_positive"),
|
||||
CheckConstraint(
|
||||
"(product_type = 'subscription' AND tier_code IS NOT NULL AND tier_rank IS NOT NULL "
|
||||
"first_purchase_price IS NULL OR first_purchase_price >= 0",
|
||||
name="ck_credit_products_first_price_nonnegative",
|
||||
),
|
||||
CheckConstraint(
|
||||
"regular_price IS NULL OR regular_price >= 0",
|
||||
name="ck_credit_products_regular_price_nonnegative",
|
||||
),
|
||||
CheckConstraint(
|
||||
"activity_price IS NULL OR activity_price >= 0",
|
||||
name="ck_credit_products_activity_price_nonnegative",
|
||||
),
|
||||
CheckConstraint(
|
||||
"monthly_grant_credits IS NULL OR monthly_grant_credits > 0",
|
||||
name="ck_credit_products_monthly_grant_positive",
|
||||
),
|
||||
CheckConstraint(
|
||||
"grant_credits IS NULL OR grant_credits > 0",
|
||||
name="ck_credit_products_grant_positive",
|
||||
),
|
||||
CheckConstraint(
|
||||
"(product_type IN ('subscription', 'team_subscription') "
|
||||
"AND tier_code IS NOT NULL AND tier_rank IS NOT NULL "
|
||||
"AND billing_cycle IS NOT NULL AND monthly_grant_credits IS NOT NULL "
|
||||
"AND first_purchase_price IS NOT NULL AND regular_price IS NOT NULL "
|
||||
"AND grant_credits IS NULL AND validity_months IS NULL) "
|
||||
@@ -49,7 +65,7 @@ class CreditProduct(Base, TimestampMixin):
|
||||
description: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
features_json: Mapped[list | None] = mapped_column(JSON, nullable=True)
|
||||
|
||||
# 订阅套餐字段
|
||||
# 个人/团队订阅套餐字段
|
||||
tier_code: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
tier_rank: Mapped[int | None] = mapped_column(nullable=True)
|
||||
billing_cycle: Mapped[str | None] = mapped_column(String(24), nullable=True)
|
||||
@@ -59,6 +75,7 @@ class CreditProduct(Base, TimestampMixin):
|
||||
activity_price: Mapped[Decimal | None] = mapped_column(Numeric(20, 2), nullable=True)
|
||||
activity_start_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
activity_end_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
# 是否允许失去首购资格后的再次购买。不是自动续费开关,不会自动创建订单或扣款。
|
||||
renewal_enabled: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, default=True, server_default="true"
|
||||
)
|
||||
@@ -67,17 +84,31 @@ class CreditProduct(Base, TimestampMixin):
|
||||
grant_credits: Mapped[Decimal | None] = mapped_column(Numeric(20, 2), nullable=True)
|
||||
validity_months: Mapped[int | None] = mapped_column(nullable=True)
|
||||
|
||||
price: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False, default=Decimal("0.00"), server_default="0")
|
||||
price: Mapped[Decimal] = mapped_column(
|
||||
Numeric(20, 2), nullable=False, default=Decimal("0.00"), server_default="0"
|
||||
)
|
||||
credit_level: Mapped[str] = mapped_column(
|
||||
String(32), nullable=False, default=CreditLevel.GENERAL.value, server_default=CreditLevel.GENERAL.value
|
||||
String(32), nullable=False, default=CreditLevel.GENERAL.value,
|
||||
server_default=CreditLevel.GENERAL.value,
|
||||
)
|
||||
currency: Mapped[str] = mapped_column(
|
||||
String(8), nullable=False, default="CNY", server_default="CNY"
|
||||
)
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, default=True, server_default="true"
|
||||
)
|
||||
currency: Mapped[str] = mapped_column(String(8), nullable=False, default="CNY", server_default="CNY")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default="true")
|
||||
sort_order: Mapped[int] = mapped_column(nullable=False, default=0, server_default="0")
|
||||
|
||||
@property
|
||||
def is_subscription(self) -> bool:
|
||||
return self.product_type == CreditProductType.SUBSCRIPTION.value
|
||||
return self.product_type in {
|
||||
CreditProductType.SUBSCRIPTION.value,
|
||||
CreditProductType.TEAM_SUBSCRIPTION.value,
|
||||
}
|
||||
|
||||
@property
|
||||
def is_team_subscription(self) -> bool:
|
||||
return self.product_type == CreditProductType.TEAM_SUBSCRIPTION.value
|
||||
|
||||
@property
|
||||
def is_credit_addon(self) -> bool:
|
||||
|
||||
@@ -13,16 +13,26 @@ from app.models.base import Base, TimestampMixin
|
||||
class UserCreditSubscription(Base, TimestampMixin):
|
||||
__tablename__ = "user_credit_subscriptions"
|
||||
__table_args__ = (
|
||||
Index("ix_user_credit_subscriptions_current", "user_id", "status", "expires_at"),
|
||||
Index("ix_user_credit_subscriptions_user_active", "user_id", "status", "expires_at"),
|
||||
Index("ix_user_credit_subscriptions_team_active", "team_id", "status", "expires_at"),
|
||||
Index("ix_user_credit_subscriptions_expire_due", "status", "expires_at", "id"),
|
||||
Index("ix_user_credit_subscriptions_grant_due", "status", "next_grant_at", "id"),
|
||||
Index("uq_user_credit_subscriptions_payment", "payment_order_id", unique=True),
|
||||
Index("uq_user_credit_subscriptions_no", "subscription_no", unique=True),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
user_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
subscription_no: Mapped[str] = mapped_column(
|
||||
String(32), nullable=False, comment="订阅业务实例编号,供用户/客服/开发定位"
|
||||
)
|
||||
user_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True,
|
||||
comment="成交时的个人用户;团队订阅为成交时队长",
|
||||
)
|
||||
team_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("teams.id", ondelete="RESTRICT"), nullable=True
|
||||
)
|
||||
team_manager_id_snapshot: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
product_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("credit_products.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
@@ -34,21 +44,27 @@ class UserCreditSubscription(Base, TimestampMixin):
|
||||
server_default=CreditSubscriptionStatus.PENDING.value,
|
||||
)
|
||||
purchase_scene: Mapped[str] = mapped_column(String(24), nullable=False)
|
||||
|
||||
product_type_snapshot: Mapped[str] = mapped_column(String(24), nullable=False)
|
||||
product_name_snapshot: Mapped[str] = mapped_column(String(96), nullable=False)
|
||||
tier_code: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
tier_rank: Mapped[int] = mapped_column(nullable=False)
|
||||
billing_cycle: Mapped[str] = mapped_column(String(24), nullable=False)
|
||||
|
||||
anchor_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
start_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
next_grant_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
monthly_grant_credits_snapshot: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
monthly_total_credits_snapshot: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
quantity_snapshot: Mapped[int] = mapped_column(nullable=False, default=1, server_default="1")
|
||||
grant_count: Mapped[int] = mapped_column(nullable=False)
|
||||
granted_count: Mapped[int] = mapped_column(nullable=False, default=0, server_default="0")
|
||||
|
||||
first_purchase_price_snapshot: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
regular_price_snapshot: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
activity_price_snapshot: Mapped[Decimal | None] = mapped_column(Numeric(20, 2), nullable=True)
|
||||
actual_unit_price_snapshot: Mapped[Decimal] = mapped_column(Numeric(20, 6), nullable=False)
|
||||
paid_amount_snapshot: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
product_snapshot_json: Mapped[dict] = mapped_column(JSON, nullable=False)
|
||||
source_subscription_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("user_credit_subscriptions.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
upgrade_order_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("payment_orders.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
|
||||
@@ -15,8 +15,8 @@ class UserCreditSubscriptionPeriod(Base, TimestampMixin):
|
||||
__table_args__ = (
|
||||
Index("uq_user_credit_subscription_periods_sequence", "subscription_id", "sequence", unique=True),
|
||||
Index("ix_user_credit_subscription_periods_due", "status", "scheduled_at", "id"),
|
||||
Index("ix_user_credit_subscription_periods_upgrade", "upgrade_order_id", "status"),
|
||||
Index("uq_user_credit_subscription_periods_balance", "issued_balance_id", unique=True),
|
||||
Index("ix_user_credit_subscription_periods_window", "subscription_id", "valid_from", "expires_at"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
@@ -37,9 +37,4 @@ class UserCreditSubscriptionPeriod(Base, TimestampMixin):
|
||||
String(32), ForeignKey("user_credit_balances.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
issued_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
upgrade_order_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("payment_orders.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
reserved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
cancelled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Index, Numeric, String, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, SoftDeleteMixin, TimestampMixin
|
||||
|
||||
|
||||
class TeamSubscriptionSeat(Base, TimestampMixin, SoftDeleteMixin):
|
||||
__tablename__ = "team_subscription_seats"
|
||||
__table_args__ = (
|
||||
Index("ix_team_subscription_seats_subscription", "subscription_id", "created_at"),
|
||||
Index("ix_team_subscription_seats_team_id", "team_id"),
|
||||
CheckConstraint("monthly_allocated_credits > 0", name="ck_team_subscription_seat_allocation_positive"),
|
||||
Index("ix_team_subscription_seats_user", "user_id", "subscription_id"),
|
||||
Index(
|
||||
"uq_team_subscription_seats_active_user",
|
||||
"subscription_id", "user_id",
|
||||
unique=True,
|
||||
postgresql_where=text("deleted_at IS NULL AND cancelled_at IS NULL"),
|
||||
),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
team_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("teams.id", ondelete="RESTRICT"), nullable=False
|
||||
)
|
||||
subscription_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("user_credit_subscriptions.id", ondelete="RESTRICT"), nullable=False
|
||||
)
|
||||
user_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("users.id", ondelete="RESTRICT"), nullable=False
|
||||
)
|
||||
monthly_allocated_credits: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
created_by_user_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("users.id", ondelete="RESTRICT"), nullable=False
|
||||
)
|
||||
cancelled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
@@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import CheckConstraint, ForeignKey, Index, Numeric, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class TeamSubscriptionSeatUsage(Base, TimestampMixin):
|
||||
__tablename__ = "team_subscription_seat_usages"
|
||||
__table_args__ = (
|
||||
Index("uq_team_subscription_seat_usage_period", "seat_id", "subscription_period_id", unique=True),
|
||||
CheckConstraint("used_credits >= 0", name="ck_team_subscription_seat_usage_nonnegative"),
|
||||
Index("ix_team_subscription_seat_usage_member", "subscription_period_id", "user_id"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
seat_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("team_subscription_seats.id", ondelete="RESTRICT"), nullable=False
|
||||
)
|
||||
subscription_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("user_credit_subscriptions.id", ondelete="RESTRICT"), nullable=False
|
||||
)
|
||||
subscription_period_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("user_credit_subscription_periods.id", ondelete="RESTRICT"), nullable=False
|
||||
)
|
||||
user_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("users.id", ondelete="RESTRICT"), nullable=False
|
||||
)
|
||||
used_credits: Mapped[Decimal] = mapped_column(
|
||||
Numeric(20, 2), nullable=False, default=Decimal("0.00"), server_default="0"
|
||||
)
|
||||
@@ -3,9 +3,10 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, JSON, Numeric, String
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, Integer, JSON, Numeric, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.enums.common import PaymentOrderSourceEnum
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
@@ -14,6 +15,8 @@ class PaymentOrder(Base, TimestampMixin):
|
||||
__table_args__ = (
|
||||
Index("idx_payorder_user_status_created", "user_id", "status", "created_at"),
|
||||
Index("idx_payorder_status_created", "status", "created_at"),
|
||||
Index("ix_payorder_source_status_created", "order_source", "status", "created_at"),
|
||||
Index("ix_payorder_team_status_created", "team_id_snapshot", "status", "created_at"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
@@ -21,15 +24,22 @@ class PaymentOrder(Base, TimestampMixin):
|
||||
String(32), ForeignKey("users.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
order_no: Mapped[str] = mapped_column(String(64), unique=True)
|
||||
amount: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
credits: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False, default=Decimal("0.00"), server_default="0")
|
||||
amount: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False, comment="实际整单实收金额")
|
||||
credits: Mapped[Decimal] = mapped_column(
|
||||
Numeric(20, 2), nullable=False, default=Decimal("0.00"), server_default="0"
|
||||
)
|
||||
payment_method: Mapped[str] = mapped_column(String(16))
|
||||
order_source: Mapped[str] = mapped_column(
|
||||
String(24), nullable=False, default=PaymentOrderSourceEnum.ONLINE_PAYMENT.value,
|
||||
server_default=PaymentOrderSourceEnum.ONLINE_PAYMENT.value,
|
||||
)
|
||||
status: Mapped[str] = mapped_column(String(32), default="pending")
|
||||
paid_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
trade_no: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
refund_trade_no: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
refunded_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
refund_amount: Mapped[Decimal | None] = mapped_column(Numeric(20, 2), nullable=True)
|
||||
refund_entitlement_status: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
|
||||
product_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("credit_products.id", ondelete="SET NULL"), nullable=True, index=True
|
||||
@@ -40,11 +50,22 @@ class PaymentOrder(Base, TimestampMixin):
|
||||
product_code_snapshot: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
product_name_snapshot: Mapped[str | None] = mapped_column(String(96), nullable=True)
|
||||
product_snapshot_json: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
|
||||
quantity: Mapped[int] = mapped_column(Integer, nullable=False, default=1, server_default="1")
|
||||
quoted_unit_price_snapshot: Mapped[Decimal | None] = mapped_column(Numeric(20, 2), nullable=True)
|
||||
quoted_amount_snapshot: Mapped[Decimal | None] = mapped_column(Numeric(20, 2), nullable=True)
|
||||
actual_unit_price_snapshot: Mapped[Decimal | None] = mapped_column(Numeric(20, 6), nullable=True)
|
||||
team_id_snapshot: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("teams.id", ondelete="RESTRICT"), nullable=True
|
||||
)
|
||||
|
||||
operator_admin_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
offline_trade_no: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
offline_payment_detail: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
remark: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
|
||||
subscription_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
|
||||
source_subscription_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
|
||||
upgrade_period_ids_json: Mapped[list | None] = mapped_column(JSON, nullable=True)
|
||||
target_price_snapshot: Mapped[Decimal | None] = mapped_column(Numeric(20, 2), nullable=True)
|
||||
deduction_amount_snapshot: Mapped[Decimal | None] = mapped_column(Numeric(20, 2), nullable=True)
|
||||
payable_amount_snapshot: Mapped[Decimal | None] = mapped_column(Numeric(20, 2), nullable=True)
|
||||
fulfillment_status: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
|
||||
fulfilled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from sqlalchemy import ForeignKey, Index, Integer, String
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.enums.team import TeamStatus
|
||||
@@ -17,21 +19,15 @@ class Team(Base, TimestampMixin, SoftDeleteMixin):
|
||||
code: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="团队编码")
|
||||
description: Mapped[str | None] = mapped_column(String(512), nullable=True, comment="团队备注")
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(16),
|
||||
default=TeamStatus.ACTIVE.value,
|
||||
server_default=TeamStatus.ACTIVE.value,
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="团队状态:active启用,disabled禁用",
|
||||
String(16), default=TeamStatus.ACTIVE.value, server_default=TeamStatus.ACTIVE.value,
|
||||
nullable=False, index=True, comment="团队状态:active启用,disabled禁用",
|
||||
)
|
||||
sort_order: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
default=0,
|
||||
server_default="0",
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="排序值,越小越靠前",
|
||||
Integer, default=0, server_default="0", nullable=False, index=True, comment="排序值,越小越靠前",
|
||||
)
|
||||
manager_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("users.id"), nullable=True, index=True, comment="团队管理人ID"
|
||||
)
|
||||
first_subscription_paid_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, index=True, comment="团队首次真实订阅成交时间"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, String, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class TeamManagerHistory(Base, TimestampMixin):
|
||||
__tablename__ = "team_manager_history"
|
||||
__table_args__ = (
|
||||
Index("ix_team_manager_history_team_time", "team_id", "started_at", "ended_at"),
|
||||
Index("ix_team_manager_history_manager_time", "manager_user_id", "started_at", "ended_at"),
|
||||
Index(
|
||||
"uq_team_manager_history_current",
|
||||
"team_id",
|
||||
unique=True,
|
||||
postgresql_where=text("ended_at IS NULL"),
|
||||
),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
team_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("teams.id", ondelete="RESTRICT"), nullable=False
|
||||
)
|
||||
manager_user_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("users.id", ondelete="RESTRICT"), nullable=False
|
||||
)
|
||||
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
Reference in New Issue
Block a user