feat(admin): Phase 9 收尾治理 — 全局项目监控 + 数据完整性体检

后端:adminpanel projects(全局项目流水线监控+状态筛)+ integrity(数据体检:无账户团队/无图商品/过期邀请码/
无文件资产/失败项目/卡 reserved 计数 + totals);AdminProjectSerializer;IsPlatformAdmin。
前端:adminApi adminProjects/integrity;Admin 治理页(体检卡片超0高亮 + 项目监控表状态筛)。
测试:adminpanel 53 单测过(项目列/筛/体检报告);无头 e2e _admin-p9.mjs 4 断言过 + 0 console error(全只读);tsc+build 绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
seaislee1209
2026-06-19 22:51:49 +08:00
co-authored by Claude Opus 4.8
parent 4ce7e957d2
commit cdfcffcecd
11 changed files with 362 additions and 0 deletions
+40
View File
@@ -18,12 +18,15 @@ from apps.assets.review import poll_asset_review, submit_asset_for_review
from apps.billing.models import CreditLedger, QuotaPolicy
from apps.billing.services.ledger import adjust_credit
from apps.common.pagination import DefaultPagination
from apps.products.models import Product
from apps.projects.models import Project
from .serializers import (
COST_ANOMALY_RATIO,
AdminLedgerSerializer,
AdminModelConfigSerializer,
AdminModelProviderSerializer,
AdminProjectSerializer,
AdminQuotaPolicySerializer,
AdminReviewAssetSerializer,
AdminTaskDetailSerializer,
@@ -585,3 +588,40 @@ def admin_model_set_default(request, model_id):
obj.save(update_fields=["is_default", "updated_at"])
log_admin_action(request, "model.set_default", target_type="model_config", target_id=obj.id, target_name=f"{obj.capability}:{obj.name}")
return Response(AdminModelConfigSerializer(ModelConfig.objects.select_related("provider").get(id=obj.id)).data)
# ─────────────────────────── 收尾治理(项目监控 + 数据完整性)───────────────────────────
@api_view(["GET"])
@permission_classes([IsPlatformAdmin])
def admin_projects(request):
"""全局项目流水线监控(?status= / ?search= 筛 + 分页)。"""
qs = Project.objects.select_related("team", "product").order_by("-created_at")
st = request.query_params.get("status")
if st in dict(Project.Status.choices):
qs = qs.filter(status=st)
search = (request.query_params.get("search") or "").strip()
if search:
qs = qs.filter(Q(name__icontains=search) | Q(team__name__icontains=search))
paginator = DefaultPagination()
page = paginator.paginate_queryset(qs, request)
return paginator.get_paginated_response(AdminProjectSerializer(page, many=True).data)
@api_view(["GET"])
@permission_classes([IsPlatformAdmin])
def admin_integrity(request):
"""数据完整性 / 治理体检(只读):统计潜在孤儿 / 异常记录数,供平台超管巡检。"""
from django.utils import timezone
now = timezone.now()
checks = [
{"key": "teams_without_account", "label": "无信用账户的团队", "count": Team.objects.filter(credit_account__isnull=True).count()},
{"key": "products_without_image", "label": "无图商品(无主图且无图册)", "count": Product.objects.filter(cover_asset__isnull=True, images__isnull=True).distinct().count()},
{"key": "expired_pending_invites", "label": "已过期但未标记的邀请码", "count": Invitation.objects.filter(status=Invitation.Status.PENDING, expires_at__lt=now).count()},
{"key": "assets_without_files", "label": "无文件的资产(未删除)", "count": Asset.objects.filter(is_deleted=False, files__isnull=True).distinct().count()},
{"key": "failed_projects", "label": "失败的项目", "count": Project.objects.filter(status=Project.Status.FAILED).count()},
{"key": "stale_reserved_tasks", "label": "卡在 reserved 的任务", "count": AITask.objects.filter(status=AITask.Status.RESERVED).count()},
]
return Response({"checks": checks, "totals": {"teams": Team.objects.count(), "users": User.objects.count(), "projects": Project.objects.count(), "products": Product.objects.count()}})