团队积分V1

This commit is contained in:
2026-08-14 15:23:46 +08:00
parent 516106c045
commit fac54b5667
70 changed files with 6670 additions and 4649 deletions
+3
View File
@@ -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",
]
+22 -1
View File
@@ -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)
+25 -10
View File
@@ -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)
+46 -15
View File
@@ -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"
)
+29 -8
View File
@@ -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)
+9 -13
View File
@@ -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)