Files
yingqing/core/backend/apps/adminpanel/serializers.py
T
zycandClaude Sonnet 5 bf20de956c feat(billing): 计费商业化重构(积分制)+ 团队差异化调价
统一计价引擎 apps/billing/pricing.py:1积分=¥0.1,平台成本(¥)与用户价(积分)双记账,
视频按火山真实 usage.total_tokens 结算(true-up,终结¥1/段倒贴)+ 首发×1.5毛利系数。
Team.price_multiplier 差异化调价(jimeng同款):挂牌价×系数两步HALF_UP取整,视频类
按下单时的价格/汇率快照结算(中途改配不影响在途任务)。

BillingConfig 单例(汇率/毛利系数/预留buffer,admin可调即刻生效)。存量数据 ×10 rescale
迁移(RunPython+atomic,MySQL 迁移不可中断重跑)。开户赠送归零(DEFAULT_TRIAL_CREDITS=0,
商业决策)。audit_billing 加 I9(卖亏审计)。271 条测试 + tsc/build 全绿。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 10:04:00 +08:00

223 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from decimal import Decimal
from rest_framework import serializers
from apps.accounts.models import Team, TeamMember, User
from apps.ai.models import AITask, ModelConfig, ModelProvider, PromptTemplate, QualityWord
from apps.assets.models import Asset
from apps.billing.models import CreditLedger, QuotaPolicy
from apps.projects.models import Project
# 成本异常阈值:实际成本 > 预估 × 此倍数(且预估 > 0)即标异常
COST_ANOMALY_RATIO = Decimal("1.5")
def is_cost_anomaly(estimated, actual) -> bool:
est = estimated or Decimal("0")
act = actual or Decimal("0")
return bool(est > 0 and act > est * COST_ANOMALY_RATIO)
class QualityWordSerializer(serializers.ModelSerializer):
class Meta:
model = QualityWord
fields = ["id", "stage", "slot", "text", "sort", "enabled", "created_at"]
read_only_fields = ["id", "created_at"]
# 每条提示词可用的 {占位符}(给 admin 提示;运行时由对应 builder 注入,改错会原样保留不致命)
PROMPT_PLACEHOLDERS = {
"person_portrait": ["描述"],
"person_triview": [],
"product_triview": ["商品", "补充"],
"scene": ["场景描述"],
"storyboard_frame": ["设定", "场景上下文", "时长", "脚本", "补充"],
"video_segment": ["设定", "分镜", "脚本", "时长"],
}
class PromptTemplateSerializer(serializers.ModelSerializer):
label = serializers.CharField(source="get_key_display", read_only=True)
placeholders = serializers.SerializerMethodField()
class Meta:
model = PromptTemplate
fields = ["id", "key", "label", "template", "ratio", "enabled", "placeholders", "updated_at"]
read_only_fields = ["id", "key", "label", "placeholders", "updated_at"] # key 固定,只改正文/比例/启用
def get_placeholders(self, obj) -> list:
return PROMPT_PLACEHOLDERS.get(obj.key, [])
class AdminReviewAssetSerializer(serializers.ModelSerializer):
"""跨团队人像审核队列行:复用 AssetFileSerializer 的首图 preview_url。"""
team_name = serializers.CharField(source="team.name", read_only=True, default=None)
preview_url = serializers.SerializerMethodField()
class Meta:
model = Asset
fields = ["id", "name", "category", "review_status", "review_error", "team", "team_name", "preview_url", "created_at"]
read_only_fields = fields
def get_preview_url(self, obj):
from apps.assets.serializers import AssetFileSerializer
# 用 prefetch 缓存(list(...))而非 .first():后者会另发查询,列表页 N+1 拖慢到数秒
files = list(obj.files.all())
return AssetFileSerializer(files[0]).data.get("preview_url", "") if files else ""
class AdminTeamSerializer(serializers.ModelSerializer):
owner_username = serializers.CharField(source="owner.username", read_only=True, default=None)
member_count = serializers.SerializerMethodField()
balance = serializers.SerializerMethodField()
class Meta:
model = Team
fields = ["id", "name", "status", "owner", "owner_username", "member_count", "balance", "price_multiplier", "created_at"]
read_only_fields = fields
def get_member_count(self, obj):
anno = getattr(obj, "member_count_anno", None)
return anno if anno is not None else obj.members.count()
def get_balance(self, obj):
acct = getattr(obj, "credit_account", None)
return str(acct.balance) if acct is not None else "0"
class AdminTeamMemberSerializer(serializers.ModelSerializer):
username = serializers.CharField(source="user.username", read_only=True)
user_status = serializers.CharField(source="user.status", read_only=True)
class Meta:
model = TeamMember
fields = ["id", "username", "role", "status", "user_status", "monthly_credit_limit"]
read_only_fields = fields
class AdminUserSerializer(serializers.ModelSerializer):
teams = serializers.SerializerMethodField()
class Meta:
model = User
fields = ["id", "username", "status", "is_platform_admin", "date_joined", "teams"]
read_only_fields = fields
def get_teams(self, obj):
return [
{"team_id": str(m.team_id), "team_name": m.team.name, "role": m.role}
for m in obj.team_memberships.select_related("team").all()
]
class AdminTaskSerializer(serializers.ModelSerializer):
team_name = serializers.CharField(source="team.name", read_only=True, default=None)
model_name = serializers.CharField(source="model_config.name", read_only=True, default=None)
cost_anomaly = serializers.SerializerMethodField()
# 单任务毛利(¥):actual_cost(积分)÷汇率 base_cost。base_cost=0(成本未知)时 None,报表侧过滤
margin_yuan = serializers.SerializerMethodField()
class Meta:
model = AITask
fields = [
"id", "task_type", "status", "team", "team_name", "model_name",
"estimated_cost", "actual_cost", "base_cost", "margin_yuan", "cost_anomaly", "error_code", "created_at",
]
read_only_fields = fields
def get_cost_anomaly(self, obj) -> bool:
return is_cost_anomaly(obj.estimated_cost, obj.actual_cost)
def get_margin_yuan(self, obj) -> str | None:
base = obj.base_cost or Decimal("0")
actual = obj.actual_cost or Decimal("0")
if base <= 0 or actual <= 0:
return None
from apps.billing.pricing import get_billing_config
# 优先用任务计价当时的汇率快照:汇率调整不追溯历史,毛利报表不整体漂移(review 确认)。
# __dict__ 直取避免触发 deferred 列加载(有的列表 queryset 会 defer payload)。
payload = obj.__dict__.get("request_payload") or {}
try:
rate = Decimal(str(payload.get("points_per_yuan_snapshot") or "")) if payload.get("points_per_yuan_snapshot") else get_billing_config().points_per_yuan
except Exception: # noqa: BLE001
rate = get_billing_config().points_per_yuan
if rate <= 0:
return None
return str((actual / rate - base).quantize(Decimal("0.01")))
class AdminTaskDetailSerializer(AdminTaskSerializer):
class Meta(AdminTaskSerializer.Meta):
fields = AdminTaskSerializer.Meta.fields + [
"project", "idempotency_key", "request_payload", "response_payload",
"error_message", "submitted_at", "completed_at",
]
read_only_fields = fields
class AdminLedgerSerializer(serializers.ModelSerializer):
team_name = serializers.CharField(source="team.name", read_only=True, default=None)
username = serializers.CharField(source="user.username", read_only=True, default=None)
class Meta:
model = CreditLedger
fields = ["id", "team", "team_name", "username", "ledger_type", "amount", "balance_after", "reason", "created_at"]
read_only_fields = fields
class AdminQuotaPolicySerializer(serializers.ModelSerializer):
team_name = serializers.CharField(source="team.name", read_only=True, default=None)
class Meta:
model = QuotaPolicy
fields = [
"id", "team", "team_name", "user", "project",
"monthly_limit", "project_limit", "per_task_limit", "is_active", "created_at",
]
read_only_fields = ["id", "team_name", "created_at"]
class AdminModelProviderSerializer(serializers.ModelSerializer):
has_api_key = serializers.SerializerMethodField()
model_count = serializers.SerializerMethodField()
class Meta:
model = ModelProvider
fields = ["id", "name", "display_name", "status", "base_url", "api_key", "metadata", "has_api_key", "model_count", "created_at"]
# api_key 只写不读(密钥不回传前端)
extra_kwargs = {"api_key": {"write_only": True, "required": False}}
read_only_fields = ["id", "has_api_key", "model_count", "created_at"]
def get_has_api_key(self, obj) -> bool:
return bool(obj.api_key)
def get_model_count(self, obj) -> int:
anno = getattr(obj, "model_count_anno", None)
return anno if anno is not None else obj.models.count()
class AdminModelConfigSerializer(serializers.ModelSerializer):
provider_name = serializers.CharField(source="provider.name", read_only=True, default=None)
class Meta:
model = ModelConfig
fields = [
"id", "provider", "provider_name", "name", "display_name", "capability",
"endpoint", "unit_price", "status", "is_default", "rate_limit_per_minute", "metadata", "created_at",
]
# is_default 只经 set-default 端点改,不在普通编辑里直接写
read_only_fields = ["id", "provider_name", "is_default", "created_at"]
class AdminProjectSerializer(serializers.ModelSerializer):
team_name = serializers.CharField(source="team.name", read_only=True, default=None)
product_title = serializers.CharField(source="product.title", read_only=True, default=None)
class Meta:
model = Project
fields = ["id", "name", "team", "team_name", "product_title", "status", "current_stage", "created_at"]
read_only_fields = fields