Files
yingqing/core/backend/apps/adminpanel/serializers.py
T
2026-08-28 14:45:38 +08:00

267 lines
11 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.model_routing import model_metadata_errors, provider_metadata_errors
from apps.ai.models import AIModelAttempt, 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": ["场景描述"],
"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()
# 可手动回收:卡在 RESERVED 超过 10 分钟(与 admin_task_reap 的服务端闸完全同口径,前端据此显示按钮)
reapable = 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", "reapable", "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_reapable(self, obj) -> bool:
from datetime import timedelta
from django.utils import timezone
return bool(
obj.status == AITask.Status.RESERVED
and obj.updated_at is not None
and obj.updated_at < timezone.now() - timedelta(minutes=10)
)
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 AdminModelAttemptSerializer(serializers.ModelSerializer):
class Meta:
model = AIModelAttempt
fields = [
"id", "sequence", "provider_name", "provider_display_name", "model_name", "model_display_name",
"public_model_name", "capability", "operation", "status", "is_retry", "is_fallback",
"previous_attempt", "provider_task_id", "started_at", "finished_at", "duration_ms", "error_type",
"provider_error_code", "raw_error", "safe_error_summary", "usage", "platform_cost", "request_summary",
"response_summary",
]
read_only_fields = fields
class AdminTaskDetailSerializer(AdminTaskSerializer):
attempts = AdminModelAttemptSerializer(source="model_attempts", many=True, read_only=True)
class Meta(AdminTaskSerializer.Meta):
fields = AdminTaskSerializer.Meta.fields + [
"project", "idempotency_key", "request_payload", "response_payload",
"error_message", "submitted_at", "completed_at", "attempts",
]
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()
def validate_metadata(self, value):
errors = provider_metadata_errors(value)
if errors:
raise serializers.ValidationError(list(errors))
return value
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"]
def validate(self, attrs):
attrs = super().validate(attrs)
if self.instance is None or "metadata" in attrs or "capability" in attrs:
capability = attrs.get("capability", getattr(self.instance, "capability", ""))
metadata = attrs.get("metadata", getattr(self.instance, "metadata", {}))
errors = model_metadata_errors(capability, metadata)
if errors:
raise serializers.ValidationError({"metadata": list(errors)})
return attrs
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