Merge branch 'main' of https://gitee.com/wg123/video-gen
This commit is contained in:
@@ -17,6 +17,11 @@ from app.models.video_engine import VideoEngine
|
||||
from app.models.credit_ratio import CreditRatio
|
||||
from app.models.menu_config import MenuConfig
|
||||
from app.models.recharge_package import RechargePackage
|
||||
from app.models.credit import (
|
||||
CreditProduct, CreditRecordAllocation, UserCreditBalance,
|
||||
UserCreditSubscription, UserCreditSubscriptionPeriod,
|
||||
)
|
||||
from app.models.llm_billing import LlmBillingPolicyModel, LlmBillingExecution, LlmCallAttempt
|
||||
from app.models.operation_log import OperationLog
|
||||
from app.models.chat_generation_task import ChatGenerationTask
|
||||
from app.models.chat_generation_task_event import ChatGenerationTaskEvent
|
||||
@@ -47,7 +52,10 @@ __all__ = [
|
||||
"User", "Team", "TeamInvitation", "TeamJoinRequest", "Project", "GenerationRecord", "CreditRecord",
|
||||
"ModelConfig", "SystemConfig", "Notification", "PaymentOrder",
|
||||
"TokenUsage", "IndustryConfig", "VideoEngine", "CreditRatio",
|
||||
"MenuConfig", "RechargePackage", "OperationLog", "ContactRequest",
|
||||
"MenuConfig", "RechargePackage", "CreditProduct", "CreditRecordAllocation", "UserCreditBalance",
|
||||
"UserCreditSubscription", "UserCreditSubscriptionPeriod",
|
||||
"LlmBillingPolicyModel", "LlmBillingExecution", "LlmCallAttempt",
|
||||
"OperationLog", "ContactRequest",
|
||||
"ChatGenerationTask", "ChatGenerationTaskEvent", "ChatProviderCallLog", "VideoUpscaleTask",
|
||||
"GeneratedResource", "UploadResource", "UserResourceMonthStat", "UserResourceTotalStat",
|
||||
"UserResourceCapacityConfig",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from app.models.credit.allocation import CreditRecordAllocation
|
||||
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
|
||||
|
||||
__all__ = [
|
||||
"CreditRecordAllocation",
|
||||
"UserCreditBalance",
|
||||
"CreditProduct",
|
||||
"UserCreditSubscription",
|
||||
"UserCreditSubscriptionPeriod",
|
||||
]
|
||||
@@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, Numeric, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class CreditRecordAllocation(Base, TimestampMixin):
|
||||
__tablename__ = "credit_record_allocations"
|
||||
__table_args__ = (
|
||||
Index("ix_credit_record_allocations_record", "credit_record_id", "id"),
|
||||
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"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
credit_record_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("credit_records.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
credit_balance_id: Mapped[str] = mapped_column(
|
||||
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
|
||||
)
|
||||
source_allocation_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("credit_record_allocations.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
allocation_action: Mapped[str] = mapped_column(String(48), nullable=False, index=True)
|
||||
amount: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
request_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
|
||||
credit_level_snapshot: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
source_type_snapshot: Mapped[str] = mapped_column(String(48), nullable=False)
|
||||
source_id_snapshot: Mapped[str | None] = mapped_column(String(64), 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)
|
||||
|
||||
unspent_before: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
unspent_after: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
consumed_before: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
consumed_after: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
@@ -0,0 +1,94 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
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.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class UserCreditBalance(Base, TimestampMixin):
|
||||
__tablename__ = "user_credit_balances"
|
||||
__table_args__ = (
|
||||
CheckConstraint("grant_amount >= 0", name="ck_user_credit_balances_grant_nonnegative"),
|
||||
CheckConstraint("unspent_amount >= 0", name="ck_user_credit_balances_unspent_nonnegative"),
|
||||
CheckConstraint("consumed_amount >= 0", name="ck_user_credit_balances_consumed_nonnegative"),
|
||||
CheckConstraint("expired_amount >= 0", name="ck_user_credit_balances_expired_nonnegative"),
|
||||
CheckConstraint("revoked_amount >= 0", name="ck_user_credit_balances_revoked_nonnegative"),
|
||||
CheckConstraint(
|
||||
"grant_amount = unspent_amount + consumed_amount + expired_amount + revoked_amount",
|
||||
name="ck_user_credit_balances_amount_reconciled",
|
||||
),
|
||||
CheckConstraint("expires_at > valid_from", name="ck_user_credit_balances_valid_window"),
|
||||
Index(
|
||||
"ix_user_credit_balances_spendable",
|
||||
"user_id",
|
||||
"credit_level_rank",
|
||||
"expires_at",
|
||||
"valid_from",
|
||||
"id",
|
||||
postgresql_where=text("unspent_amount > 0 AND revoked_at IS NULL"),
|
||||
),
|
||||
Index(
|
||||
"ix_user_credit_balances_expire_due",
|
||||
"expires_at",
|
||||
"id",
|
||||
postgresql_where=text(
|
||||
"unspent_amount > 0 AND expired_processed_at IS NULL AND revoked_at IS NULL"
|
||||
),
|
||||
),
|
||||
Index("ix_user_credit_balances_source", "source_type", "source_id"),
|
||||
Index("ix_user_credit_balances_payment", "payment_order_id"),
|
||||
Index("ix_user_credit_balances_subscription", "subscription_id", "subscription_period_id"),
|
||||
Index("uq_user_credit_balances_user_biz_key", "user_id", "biz_key", 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
|
||||
)
|
||||
credit_level: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||
credit_level_rank: Mapped[int] = mapped_column(nullable=False, default=20, server_default="20")
|
||||
|
||||
source_type: Mapped[str] = mapped_column(String(48), nullable=False, index=True)
|
||||
source_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
||||
product_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("credit_products.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
payment_order_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("payment_orders.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
subscription_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("user_credit_subscriptions.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
subscription_period_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("user_credit_subscription_periods.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
grant_record_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("credit_records.id", ondelete="SET NULL"), nullable=True, index=True
|
||||
)
|
||||
|
||||
grant_amount: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
unspent_amount: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
consumed_amount: Mapped[Decimal] = mapped_column(
|
||||
Numeric(20, 2), nullable=False, default=Decimal("0.00"), server_default="0"
|
||||
)
|
||||
expired_amount: Mapped[Decimal] = mapped_column(
|
||||
Numeric(20, 2), nullable=False, default=Decimal("0.00"), server_default="0"
|
||||
)
|
||||
revoked_amount: Mapped[Decimal] = mapped_column(
|
||||
Numeric(20, 2), nullable=False, default=Decimal("0.00"), server_default="0"
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
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)
|
||||
biz_key: Mapped[str] = mapped_column(String(180), nullable=False)
|
||||
metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import Boolean, CheckConstraint, DateTime, Index, JSON, Numeric, String
|
||||
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
|
||||
|
||||
|
||||
class CreditProduct(Base, TimestampMixin):
|
||||
__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"),
|
||||
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 "
|
||||
"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) "
|
||||
"OR (product_type = 'credit_addon' AND grant_credits IS NOT NULL "
|
||||
"AND validity_months BETWEEN 1 AND 36 AND tier_code IS NULL AND tier_rank IS NULL "
|
||||
"AND billing_cycle IS NULL AND monthly_grant_credits IS NULL "
|
||||
"AND first_purchase_price IS NULL AND regular_price IS NULL "
|
||||
"AND activity_price IS NULL AND activity_start_at IS NULL AND activity_end_at IS NULL)",
|
||||
name="ck_credit_products_type_required_fields",
|
||||
),
|
||||
CheckConstraint(
|
||||
"(activity_price IS NULL AND activity_start_at IS NULL AND activity_end_at IS NULL) "
|
||||
"OR (activity_price IS NOT NULL AND activity_start_at IS NOT NULL "
|
||||
"AND activity_end_at IS NOT NULL AND activity_end_at > activity_start_at)",
|
||||
name="ck_credit_products_activity_window",
|
||||
),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
product_code: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
product_type: Mapped[str] = mapped_column(String(24), nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(96), nullable=False)
|
||||
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)
|
||||
monthly_grant_credits: Mapped[Decimal | None] = mapped_column(Numeric(20, 2), nullable=True)
|
||||
first_purchase_price: Mapped[Decimal | None] = mapped_column(Numeric(20, 2), nullable=True)
|
||||
regular_price: Mapped[Decimal | None] = mapped_column(Numeric(20, 2), nullable=True)
|
||||
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"
|
||||
)
|
||||
|
||||
# 积分增值包字段;有效期按自然月配置,范围1-36个月。
|
||||
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")
|
||||
credit_level: Mapped[str] = mapped_column(
|
||||
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")
|
||||
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
|
||||
|
||||
@property
|
||||
def is_credit_addon(self) -> bool:
|
||||
return self.product_type == CreditProductType.CREDIT_ADDON.value
|
||||
@@ -0,0 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, JSON, Numeric, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.enums.credit_subscription import CreditSubscriptionStatus
|
||||
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_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),
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
product_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("credit_products.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
payment_order_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("payment_orders.id", ondelete="RESTRICT"), nullable=False
|
||||
)
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(32), nullable=False, default=CreditSubscriptionStatus.PENDING.value,
|
||||
server_default=CreditSubscriptionStatus.PENDING.value,
|
||||
)
|
||||
purchase_scene: Mapped[str] = mapped_column(String(24), 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)
|
||||
grant_count: Mapped[int] = mapped_column(nullable=False)
|
||||
granted_count: Mapped[int] = mapped_column(nullable=False, default=0, server_default="0")
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, Numeric, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.enums.credit_subscription import CreditSubscriptionPeriodStatus
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class UserCreditSubscriptionPeriod(Base, TimestampMixin):
|
||||
__tablename__ = "user_credit_subscription_periods"
|
||||
__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),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
subscription_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("user_credit_subscriptions.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
sequence: Mapped[int] = mapped_column(nullable=False)
|
||||
scheduled_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
valid_from: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
grant_credits: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
allocated_paid_amount: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(32), nullable=False, default=CreditSubscriptionPeriodStatus.SCHEDULED.value,
|
||||
server_default=CreditSubscriptionPeriodStatus.SCHEDULED.value,
|
||||
)
|
||||
issued_balance_id: Mapped[str | None] = mapped_column(
|
||||
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)
|
||||
@@ -1,4 +1,9 @@
|
||||
from sqlalchemy import Float, ForeignKey, Index, Integer, String, text
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, Integer, Numeric, String, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
@@ -7,14 +12,11 @@ from app.models.base import Base, TimestampMixin
|
||||
class CreditRecord(Base, TimestampMixin):
|
||||
__tablename__ = "credit_records"
|
||||
__table_args__ = (
|
||||
# 正式计费幂等键:同一用户同一个业务流水只能写入一次。
|
||||
# PostgreSQL/MySQL/SQLite 对 nullable unique 的处理都允许多条 NULL,兼容历史数据。
|
||||
Index("uq_credit_records_user_biz_key", "user_id", "biz_key", unique=True),
|
||||
Index("ix_credit_records_user_refund_for_biz_key", "user_id", "refund_for_biz_key"),
|
||||
Index(
|
||||
"uq_credit_records_user_refund_target",
|
||||
"user_id",
|
||||
"refund_for_biz_key",
|
||||
"uq_credit_records_user_refund_target_kind",
|
||||
"user_id", "refund_for_biz_key", "refund_kind",
|
||||
unique=True,
|
||||
postgresql_where=text("type = 'refund' AND refund_for_biz_key IS NOT NULL"),
|
||||
),
|
||||
@@ -31,30 +33,30 @@ class CreditRecord(Base, TimestampMixin):
|
||||
user_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("users.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
type: Mapped[str] = mapped_column(String(16), index=True)
|
||||
amount: Mapped[float] = mapped_column(Float)
|
||||
balance_after: Mapped[float] = mapped_column(Float)
|
||||
description: Mapped[str] = mapped_column(String(256))
|
||||
type: Mapped[str] = mapped_column(String(24), index=True)
|
||||
amount: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
balance_delta: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False, default=Decimal("0.00"), server_default="0")
|
||||
expired_amount: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False, default=Decimal("0.00"), server_default="0")
|
||||
balance_after: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
description: Mapped[str] = mapped_column(String(512))
|
||||
related_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
request_time: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
|
||||
|
||||
# 当前积分流水自己的业务幂等键。
|
||||
# 例如:generation_record:{record_id}:attempt:1:media:charge
|
||||
biz_key: Mapped[str | None] = mapped_column(String(160), nullable=True, index=True)
|
||||
biz_key: Mapped[str | None] = mapped_column(String(180), nullable=True, index=True)
|
||||
refund_for_biz_key: Mapped[str | None] = mapped_column(String(180), nullable=True, index=True)
|
||||
refund_kind: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
|
||||
|
||||
# 如果当前流水是退款,记录它退的是哪一次扣费。
|
||||
# 例如:generation_record:{record_id}:attempt:1:media:charge
|
||||
refund_for_biz_key: Mapped[str | None] = mapped_column(String(160), nullable=True, index=True)
|
||||
|
||||
# 账务快照字段:保证业务步骤/资源软删后,流水仍可独立展示。
|
||||
owner_type: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
||||
owner_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
||||
attempt_no: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
|
||||
charge_kind: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
|
||||
charge_action: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||||
charge_action: Mapped[str | None] = mapped_column(String(24), nullable=True)
|
||||
credit_subject: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
|
||||
media_type: Mapped[str | None] = mapped_column(String(16), nullable=True, index=True)
|
||||
billing_scene: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
||||
scene_name_snapshot: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
credit_level_snapshot: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
|
||||
source_module: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
||||
source_project_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
|
||||
@@ -65,7 +67,10 @@ class CreditRecord(Base, TimestampMixin):
|
||||
input_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
output_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
total_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
|
||||
llm_call_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
llm_success_call_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
llm_failed_call_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
llm_billing_execution_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
|
||||
|
||||
engine_type: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||||
engine_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
|
||||
@@ -75,6 +80,5 @@ class CreditRecord(Base, TimestampMixin):
|
||||
|
||||
user_type_snapshot: Mapped[str | None] = mapped_column(String(16), nullable=True, index=True)
|
||||
frontend_user_kind_snapshot: Mapped[str | None] = mapped_column(String(16), nullable=True, index=True)
|
||||
# 交易流水发生时的团队归属冷备快照;用户后续改团队不影响历史流水展示与筛选。
|
||||
team_id_snapshot: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
|
||||
team_name_snapshot: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from app.models.llm_billing.call_attempt import LlmCallAttempt
|
||||
from app.models.llm_billing.execution import LlmBillingExecution
|
||||
from app.models.llm_billing.policy import LlmBillingPolicyModel
|
||||
|
||||
__all__ = ["LlmCallAttempt", "LlmBillingExecution", "LlmBillingPolicyModel"]
|
||||
@@ -0,0 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.enums.llm_billing import LlmCallAttemptStatus
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class LlmCallAttempt(Base, TimestampMixin):
|
||||
__tablename__ = "llm_call_attempts"
|
||||
__table_args__ = (
|
||||
Index("uq_llm_call_attempts_sequence", "billing_execution_id", "call_sequence", unique=True),
|
||||
Index("ix_llm_call_attempts_execution_time", "billing_execution_id", "created_at"),
|
||||
Index("ix_llm_call_attempts_provider_request", "provider_request_id"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
billing_execution_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("llm_billing_executions.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
call_sequence: Mapped[int] = mapped_column(nullable=False)
|
||||
retry_sequence: Mapped[int] = mapped_column(nullable=False, default=0, server_default="0")
|
||||
model_config_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("model_configs.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
model_name_snapshot: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
provider_snapshot: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
provider_request_id: Mapped[str | None] = mapped_column(String(160), nullable=True)
|
||||
request_started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
response_received_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
duration_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(24), nullable=False, default=LlmCallAttemptStatus.STARTED.value,
|
||||
server_default=LlmCallAttemptStatus.STARTED.value,
|
||||
)
|
||||
input_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
output_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
total_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
token_usage_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("token_usage.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
http_status: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
provider_error_code: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
error_message: Mapped[str | None] = mapped_column(String(1000), nullable=True)
|
||||
token_unavailable_reason: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||||
postprocess_status: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
postprocess_error: Mapped[str | None] = mapped_column(String(1000), nullable=True)
|
||||
@@ -0,0 +1,71 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, JSON, Numeric, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.enums.llm_billing import LlmBillingExecutionStatus
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class LlmBillingExecution(Base, TimestampMixin):
|
||||
__tablename__ = "llm_billing_executions"
|
||||
__table_args__ = (
|
||||
Index(
|
||||
"uq_llm_billing_executions_business_attempt",
|
||||
"user_id", "scene_code", "owner_type", "owner_id", "business_attempt_no",
|
||||
unique=True,
|
||||
),
|
||||
Index("uq_llm_billing_executions_credit_record", "credit_record_id", unique=True),
|
||||
Index("ix_llm_billing_executions_status_time", "status", "created_at"),
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
scene_code: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||||
scene_name_snapshot: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
owner_type: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
owner_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
business_attempt_no: Mapped[int] = mapped_column(nullable=False)
|
||||
|
||||
model_config_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("model_configs.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
model_name_snapshot: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
provider_snapshot: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
model_parameters_snapshot: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
|
||||
billing_policy_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("llm_billing_policies.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
billing_policy_version: Mapped[int | None] = mapped_column(nullable=True)
|
||||
request_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
pre_deduct_credits: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
credit_record_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("credit_records.id", ondelete="RESTRICT"), nullable=False
|
||||
)
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(32), nullable=False, default=LlmBillingExecutionStatus.PRE_DEDUCTED.value,
|
||||
server_default=LlmBillingExecutionStatus.PRE_DEDUCTED.value,
|
||||
)
|
||||
|
||||
total_call_count: Mapped[int] = mapped_column(nullable=False, default=0, server_default="0")
|
||||
successful_call_count: Mapped[int] = mapped_column(nullable=False, default=0, server_default="0")
|
||||
failed_call_count: Mapped[int] = mapped_column(nullable=False, default=0, server_default="0")
|
||||
total_input_tokens: Mapped[int] = mapped_column(nullable=False, default=0, server_default="0")
|
||||
total_output_tokens: Mapped[int] = mapped_column(nullable=False, default=0, server_default="0")
|
||||
total_tokens: Mapped[int] = mapped_column(nullable=False, default=0, server_default="0")
|
||||
|
||||
refund_available_credits: Mapped[Decimal] = mapped_column(
|
||||
Numeric(20, 2), nullable=False, default=Decimal("0.00"), server_default="0"
|
||||
)
|
||||
refund_expired_credits: Mapped[Decimal] = mapped_column(
|
||||
Numeric(20, 2), nullable=False, default=Decimal("0.00"), server_default="0"
|
||||
)
|
||||
final_error_message: Mapped[str | None] = mapped_column(String(1000), nullable=True)
|
||||
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
refunded_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import Boolean, Index, Numeric, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class LlmBillingPolicyModel(Base, TimestampMixin):
|
||||
__tablename__ = "llm_billing_policies"
|
||||
__table_args__ = (Index("uq_llm_billing_policies_scene", "scene_code", unique=True),)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
scene_code: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
scene_name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
pre_deduct_credits: Mapped[Decimal] = mapped_column(Numeric(20, 2), nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default="true")
|
||||
version: Mapped[int] = mapped_column(nullable=False, default=1, server_default="1")
|
||||
created_by: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
updated_by: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
@@ -1,6 +1,9 @@
|
||||
from datetime import datetime
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, Index
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, JSON, Numeric, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
@@ -8,27 +11,40 @@ from app.models.base import Base, TimestampMixin
|
||||
|
||||
class PaymentOrder(Base, TimestampMixin):
|
||||
__tablename__ = "payment_orders"
|
||||
__table_args__ = (
|
||||
Index("idx_payorder_user_status_created", "user_id", "status", "created_at"),
|
||||
Index("idx_payorder_status_created", "status", "created_at"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
user_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("users.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
order_no: Mapped[str] = mapped_column(String(64), unique=True)
|
||||
amount: Mapped[float] = mapped_column(Float)
|
||||
credits: Mapped[float] = mapped_column(Float)
|
||||
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")
|
||||
payment_method: Mapped[str] = mapped_column(String(16))
|
||||
status: Mapped[str] = mapped_column(String(16), default="pending")
|
||||
paid_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
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[float | None] = mapped_column(Float, 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)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_payorder_user_status_created', 'user_id', 'status', 'created_at'),
|
||||
Index('idx_payorder_status_created', 'status', 'created_at'),
|
||||
product_id: Mapped[str | None] = mapped_column(
|
||||
String(32), ForeignKey("credit_products.id", ondelete="SET NULL"), nullable=True, index=True
|
||||
)
|
||||
product_type: Mapped[str | None] = mapped_column(String(24), nullable=True, index=True)
|
||||
purchase_scene: Mapped[str | None] = mapped_column(String(24), nullable=True)
|
||||
price_type: Mapped[str | None] = mapped_column(String(24), nullable=True)
|
||||
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)
|
||||
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,19 +1,9 @@
|
||||
from sqlalchemy import Boolean, Integer, String, Float
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
"""兼容旧导入路径。
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
充值产品已统一迁移为 credit_products 表;新代码应直接导入 CreditProduct。
|
||||
"""
|
||||
from app.models.credit.product import CreditProduct
|
||||
|
||||
RechargePackage = CreditProduct
|
||||
|
||||
class RechargePackage(Base, TimestampMixin):
|
||||
__tablename__ = "recharge_packages"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(64))
|
||||
credits: Mapped[float] = mapped_column(Float)
|
||||
price: Mapped[float] = mapped_column(Float)
|
||||
bonus_credits: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
description: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||||
package_type: Mapped[str] = mapped_column(String(32), default="normal")
|
||||
is_gift: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
sort_order: Mapped[int] = mapped_column(Integer, default=0)
|
||||
__all__ = ["CreditProduct", "RechargePackage"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, Float, Integer, String, JSON
|
||||
from sqlalchemy import Boolean, DateTime, Integer, String, JSON
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.enums.user import FrontendUserKind
|
||||
@@ -9,19 +9,17 @@ from app.models.base import Base, TimestampMixin
|
||||
|
||||
class User(Base, TimestampMixin):
|
||||
__tablename__ = "users"
|
||||
__allow_unmapped__ = True
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
username: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
||||
email: Mapped[str | None] = mapped_column(String(255), unique=True, nullable=True)
|
||||
phone: Mapped[str | None] = mapped_column(String(20), unique=True, nullable=True)
|
||||
# 短信注册用户允许先没有密码,后续通过 /auth/set-password 设置。
|
||||
hashed_password: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
avatar: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
credits: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
user_type: Mapped[str] = mapped_column(String(16), default="frontend", index=True)
|
||||
# 仅前台用户有业务意义;默认外部用户。取消内部标记时也设置回 external。
|
||||
frontend_user_kind: Mapped[str] = mapped_column(
|
||||
String(16),
|
||||
default=FrontendUserKind.EXTERNAL.value,
|
||||
@@ -29,17 +27,13 @@ class User(Base, TimestampMixin):
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
# 当前归属团队,仅前台用户有业务意义;不影响 frontend_user_kind 内部/外部设置。
|
||||
team_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
|
||||
last_login_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
password_set_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
password_set_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
first_membership_paid_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, index=True
|
||||
)
|
||||
allowed_menus: Mapped[list | None] = mapped_column(JSON, nullable=True)
|
||||
|
||||
# 私域人像素材总量限制。0 表示关闭模块;>0 表示启用并限制真人/虚拟、图片/视频素材总量。
|
||||
private_portrait_asset_limit: Mapped[int] = mapped_column(
|
||||
Integer, default=50, server_default="50", nullable=False
|
||||
)
|
||||
@@ -49,6 +43,14 @@ class User(Base, TimestampMixin):
|
||||
Integer, default=0, server_default="0", nullable=False
|
||||
)
|
||||
|
||||
@property
|
||||
def credits(self) -> float:
|
||||
return float(getattr(self, "_credits_snapshot", 0.0) or 0.0)
|
||||
|
||||
@credits.setter
|
||||
def credits(self, value: float | int) -> None:
|
||||
self._credits_snapshot = round(float(value or 0.0), 2)
|
||||
|
||||
@property
|
||||
def must_set_password(self) -> bool:
|
||||
return self.user_type == "frontend" and not self.hashed_password
|
||||
|
||||
Reference in New Issue
Block a user