交易流水对账明细 | AI引擎计价规则

This commit is contained in:
2026-07-10 16:57:23 +08:00
parent ad2e40bc67
commit 468184d352
50 changed files with 6998 additions and 506 deletions
+2 -1
View File
@@ -7,6 +7,7 @@ from app.models.project import Project
from app.models.generation_record import GenerationRecord
from app.models.credit_record import CreditRecord
from app.models.model_config import ModelConfig
from app.models.model_pricing_rule import ModelPricingRule
from app.models.system_config import SystemConfig
from app.models.notification import Notification
from app.models.notification_read import NotificationRead
@@ -41,7 +42,7 @@ __all__ = [
"Base", "TimestampMixin", "SoftDeleteMixin", "engine", "async_session",
"init_database", "close_database",
"User", "Team", "TeamInvitation", "TeamJoinRequest", "Project", "GenerationRecord", "CreditRecord",
"ModelConfig", "SystemConfig", "Notification", "PaymentOrder",
"ModelConfig", "ModelPricingRule", "SystemConfig", "Notification", "PaymentOrder",
"TokenUsage", "IndustryConfig", "VideoEngine", "CreditRatio",
"MenuConfig", "RechargePackage", "OperationLog", "ContactRequest",
"ChatGenerationTask", "ChatGenerationTaskEvent", "ChatProviderCallLog",
@@ -73,6 +73,8 @@ class ChatGenerationTask(Base, TimestampMixin, SoftDeleteMixin):
engine_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
engine_snapshot_json: Mapped[str | None] = mapped_column(Text, nullable=True)
provider_response_json: Mapped[str | None] = mapped_column(Text, nullable=True)
# 当前媒体扣费尝试号;Provider 回调必须按 owner + attempt_no 精确回填。
current_billing_attempt_no: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
credits_cost: Mapped[float] = mapped_column(Float, default=0.0)
text_credits_cost: Mapped[float] = mapped_column(Float, default=0.0)
+55 -8
View File
@@ -1,14 +1,22 @@
from sqlalchemy import Float, ForeignKey, Index, Integer, String
from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from typing import Any
from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Index, Integer, JSON, Numeric, String
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, TimestampMixin
JsonType = JSON().with_variant(JSONB, "postgresql")
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("ix_credit_records_related_type", "related_id", "type"),
@@ -18,12 +26,13 @@ class CreditRecord(Base, TimestampMixin):
Index("ix_credit_records_user_kind_time", "user_type_snapshot", "frontend_user_kind_snapshot", "created_at"),
Index("ix_credit_records_team_time", "team_id_snapshot", "created_at"),
Index("ix_credit_records_user_kind_team_time", "user_type_snapshot", "frontend_user_kind_snapshot", "team_id_snapshot", "created_at"),
Index("ix_credit_records_pricing_status_time", "provider_cost_status", "created_at"),
Index("ix_credit_records_pricing_model_time", "engine_provider", "engine_model_name", "pricing_reference_at"),
Index("ix_credit_records_pricing_version", "pricing_version_code", "pricing_rule_id"),
)
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
)
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)
@@ -59,15 +68,53 @@ class CreditRecord(Base, TimestampMixin):
output_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
total_tokens: Mapped[int | None] = mapped_column(Integer, nullable=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)
engine_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
engine_provider: Mapped[str | None] = mapped_column(String(64), nullable=True)
engine_model_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
# 不可变计价版本快照。后续调价不得基于规则表重新计算历史流水。
pricing_rule_id: Mapped[str | None] = mapped_column(
String(32), ForeignKey("model_pricing_rules.id", ondelete="RESTRICT"), nullable=True, index=True
)
pricing_version_code: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
pricing_billing_mode: Mapped[str | None] = mapped_column(String(48), nullable=True)
pricing_calculator_version: Mapped[str | None] = mapped_column(String(64), nullable=True)
pricing_usage_source: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
pricing_reference_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
pricing_effective_from: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
pricing_effective_to: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
pricing_snapshot_schema_version: Mapped[int | None] = mapped_column(Integer, nullable=True)
pricing_snapshot_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
provider_cost_currency: Mapped[str | None] = mapped_column(String(8), nullable=True)
provider_cost_amount: Mapped[Decimal | None] = mapped_column(Numeric(20, 8), nullable=True)
provider_cost_status: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
provider_cost_calculated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
provider_cost_finalized_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
provider_usage_primary: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default="true")
provider_cost_is_estimated: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default="false")
# 财务高频聚合字段平铺,避免列表/导出时逐条解析 JSON 或回查任务链。
attachment_image_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
attachment_video_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
attachment_audio_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
attachment_total_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
attachment_video_duration_seconds: Mapped[Decimal] = mapped_column(Numeric(20, 6), nullable=False, default=0, server_default="0")
attachment_audio_duration_seconds: Mapped[Decimal] = mapped_column(Numeric(20, 6), nullable=False, default=0, server_default="0")
requested_output_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
generated_image_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
generated_video_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
generated_total_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
pricing_snapshot_json: Mapped[dict[str, Any] | None] = mapped_column(JsonType, nullable=True)
usage_snapshot_json: Mapped[dict[str, Any] | None] = mapped_column(JsonType, nullable=True)
attachment_snapshot_json: Mapped[dict[str, Any] | None] = mapped_column(JsonType, nullable=True)
generation_snapshot_json: Mapped[dict[str, Any] | None] = mapped_column(JsonType, nullable=True)
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)
@@ -35,6 +35,11 @@ class GenerationRecord(Base, TimestampMixin, SoftDeleteMixin):
DateTime(timezone=True), nullable=True
)
seedance_task_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
# 生成开始时锁定实际引擎,后续提交、轮询和计价均使用同一快照。
engine_id: Mapped[str | None] = mapped_column(String(32), nullable=True, index=True)
engine_snapshot_json: Mapped[str | None] = mapped_column(Text, nullable=True)
provider_response_json: Mapped[str | None] = mapped_column(Text, nullable=True)
current_billing_attempt_no: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
credits_cost: Mapped[float] = mapped_column(Float, default=0.0)
text_credits_cost: Mapped[float] = mapped_column(Float, default=0.0)
text_tokens_used: Mapped[int] = mapped_column(Integer, default=0)
@@ -0,0 +1,63 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
from sqlalchemy import CheckConstraint, DateTime, Index, Integer, JSON, String, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, TimestampMixin
JsonType = JSON().with_variant(JSONB, "postgresql")
class ModelPricingRule(Base, TimestampMixin):
__tablename__ = "model_pricing_rules"
__table_args__ = (
Index(
"uq_model_pricing_rules_provider_model_version",
"provider",
"model_name",
"version_code",
unique=True,
),
Index(
"ix_model_pricing_rules_resolve",
"provider",
"model_name",
"publish_status",
"effective_from",
"effective_to",
),
Index("ix_model_pricing_rules_category_status", "model_category", "publish_status"),
CheckConstraint(
"effective_to IS NULL OR effective_to >= effective_from",
name="ck_model_pricing_rules_effective_range",
),
)
id: Mapped[str] = mapped_column(String(32), primary_key=True)
provider: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
model_name: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
model_category: Mapped[str] = mapped_column(String(16), nullable=False, index=True)
billing_mode: Mapped[str] = mapped_column(String(48), nullable=False, index=True)
calculator_version: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
version_code: Mapped[str] = mapped_column(String(64), nullable=False)
effective_from: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
effective_to: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
publish_status: Mapped[str] = mapped_column(String(16), nullable=False, default="draft", index=True)
currency: Mapped[str] = mapped_column(String(8), nullable=False, default="CNY")
rule_schema_version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
# 规则 JSON 统一使用“构建新 dict 后整体赋值”,禁止嵌套原地修改。
rule_json: Mapped[dict[str, Any]] = mapped_column(JsonType, nullable=False, default=dict)
rule_content_hash: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
source_url: Mapped[str | None] = mapped_column(Text, nullable=True)
source_updated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
remark: Mapped[str | None] = mapped_column(Text, nullable=True)
created_by: Mapped[str | None] = mapped_column(String(32), nullable=True)
updated_by: Mapped[str | None] = mapped_column(String(32), nullable=True)