feat: 解耦角色与模特并完善模特库
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""审计“我的演员”收口为模特前的历史数据;只读,不写库。"""
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Q
|
||||
|
||||
from apps.assets.models import Asset, Model
|
||||
from apps.projects.models import BaseAssetGroup
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "只读审计旧 person 资产,输出可安全迁移为 Model 的候选和排除原因。"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--team-id", help="仅审计指定团队 UUID")
|
||||
parser.add_argument("--limit", type=int, default=50, help="最多打印多少条候选(默认 50)")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
team_id = options.get("team_id")
|
||||
limit = max(0, options["limit"])
|
||||
|
||||
active_people = Asset.objects.filter(
|
||||
category=Asset.Category.PERSON,
|
||||
is_deleted=False,
|
||||
purged_at__isnull=True,
|
||||
)
|
||||
if team_id:
|
||||
active_people = active_people.filter(team_id=team_id)
|
||||
|
||||
# 项目基础资产组引用的 person(包含采用版和候选版)一律视为项目角色,不自动迁移。
|
||||
project_role_asset_ids = set(
|
||||
Asset.objects.filter(
|
||||
Q(adopted_base_groups__kind=BaseAssetGroup.Kind.PERSON)
|
||||
| Q(candidate_base_groups__kind=BaseAssetGroup.Kind.PERSON)
|
||||
)
|
||||
.values_list("id", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
if team_id:
|
||||
project_role_asset_ids &= set(active_people.values_list("id", flat=True))
|
||||
|
||||
existing_model_portraits = set(
|
||||
Model.objects.filter(
|
||||
is_deleted=False,
|
||||
purged_at__isnull=True,
|
||||
portrait_asset__isnull=False,
|
||||
)
|
||||
.values_list("portrait_asset_id", flat=True)
|
||||
)
|
||||
if team_id:
|
||||
existing_model_portraits &= set(active_people.values_list("id", flat=True))
|
||||
|
||||
saved_people = active_people.filter(in_library=True)
|
||||
project_generated_ids = set(
|
||||
active_people.filter(origin_task__project__isnull=False).values_list("id", flat=True)
|
||||
)
|
||||
eligible = (
|
||||
saved_people.exclude(id__in=project_role_asset_ids)
|
||||
.exclude(id__in=project_generated_ids)
|
||||
.exclude(id__in=existing_model_portraits)
|
||||
.order_by("created_at")
|
||||
)
|
||||
|
||||
trash_people = Asset.objects.filter(
|
||||
category=Asset.Category.PERSON,
|
||||
is_deleted=True,
|
||||
purged_at__isnull=True,
|
||||
)
|
||||
purged_people = Asset.objects.filter(category=Asset.Category.PERSON, purged_at__isnull=False)
|
||||
if team_id:
|
||||
trash_people = trash_people.filter(team_id=team_id)
|
||||
purged_people = purged_people.filter(team_id=team_id)
|
||||
|
||||
self.stdout.write(self.style.WARNING("=== audit_actor_model_migration · DRY-RUN / READ-ONLY ==="))
|
||||
self.stdout.write(f"正常 person 资产: {active_people.count()}")
|
||||
self.stdout.write(f" 已有 Model 形象图: {len(existing_model_portraits)}")
|
||||
self.stdout.write(f" 项目角色引用: {len(project_role_asset_ids)}")
|
||||
self.stdout.write(f" 项目生成角色: {len(project_generated_ids)}")
|
||||
self.stdout.write(f" 旧已保存人物候选: {eligible.count()}")
|
||||
self.stdout.write(f"垃圾桶 person(不迁移): {trash_people.count()}")
|
||||
self.stdout.write(f"彻底隐藏 person(不迁移): {purged_people.count()}")
|
||||
self.stdout.write("\n可迁移候选(只打印,不创建 Model):")
|
||||
for asset in eligible[:limit]:
|
||||
self.stdout.write(f" {asset.id} | team={asset.team_id} | {asset.name}")
|
||||
if eligible.count() > limit:
|
||||
self.stdout.write(f" … 其余 {eligible.count() - limit} 条未打印(可用 --limit 调整)")
|
||||
self.stdout.write(self.style.WARNING("审计结束:未创建、更新、删除任何记录或文件。"))
|
||||
@@ -0,0 +1,80 @@
|
||||
"""把符合规则的历史“已保存演员”补为 Model;默认 dry-run。"""
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
|
||||
from apps.assets.models import Asset, Model
|
||||
from apps.projects.models import BaseAssetGroup
|
||||
|
||||
|
||||
def eligible_legacy_actor_assets(team_id=None):
|
||||
"""只返回正常状态、已保存、非项目角色且尚未拥有 Model 的旧 person 资产。"""
|
||||
people = Asset.objects.filter(
|
||||
category=Asset.Category.PERSON,
|
||||
in_library=True,
|
||||
is_deleted=False,
|
||||
purged_at__isnull=True,
|
||||
)
|
||||
if team_id:
|
||||
people = people.filter(team_id=team_id)
|
||||
|
||||
project_role_ids = Asset.objects.filter(
|
||||
Q(adopted_base_groups__kind=BaseAssetGroup.Kind.PERSON)
|
||||
| Q(candidate_base_groups__kind=BaseAssetGroup.Kind.PERSON)
|
||||
).values_list("id", flat=True)
|
||||
existing_model_ids = Model.objects.filter(
|
||||
is_deleted=False,
|
||||
purged_at__isnull=True,
|
||||
portrait_asset__isnull=False,
|
||||
).values_list("portrait_asset_id", flat=True)
|
||||
return (
|
||||
people.exclude(origin_task__project__isnull=False)
|
||||
.exclude(id__in=project_role_ids)
|
||||
.exclude(id__in=existing_model_ids)
|
||||
.order_by("created_at")
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "迁移正常状态的历史已保存演员为 Model;默认 dry-run,--apply 才写库。"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--team-id", help="仅迁移指定团队 UUID")
|
||||
parser.add_argument("--apply", action="store_true", help="真正创建 Model(默认只打印)")
|
||||
parser.add_argument("--limit", type=int, default=50, help="最多打印多少条候选(默认 50)")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
apply = options["apply"]
|
||||
candidates = eligible_legacy_actor_assets(options.get("team_id"))
|
||||
count = candidates.count()
|
||||
limit = max(0, options["limit"])
|
||||
self.stdout.write(self.style.WARNING(f"=== migrate_saved_actors_to_models · {'APPLY' if apply else 'DRY-RUN'} ==="))
|
||||
self.stdout.write(f"可迁移旧保存人物: {count}")
|
||||
for asset in candidates[:limit]:
|
||||
self.stdout.write(f" {asset.id} | team={asset.team_id} | {asset.name}")
|
||||
|
||||
if not apply:
|
||||
self.stdout.write(self.style.WARNING("DRY-RUN 结束,未写库。加 --apply 才会创建 Model。"))
|
||||
return
|
||||
|
||||
created = 0
|
||||
with transaction.atomic():
|
||||
for asset in candidates.select_for_update():
|
||||
# select_for_update 后再次确认,保证并发/重复执行都不会造成重复 Model。
|
||||
if Model.objects.filter(
|
||||
portrait_asset=asset,
|
||||
is_deleted=False,
|
||||
purged_at__isnull=True,
|
||||
).exists():
|
||||
continue
|
||||
Model.objects.create(
|
||||
team_id=asset.team_id,
|
||||
created_by_id=asset.created_by_id,
|
||||
name=(asset.name or "模特")[:255],
|
||||
source=Model.Source.UPLOAD if asset.source == Asset.Source.UPLOAD else Model.Source.AI,
|
||||
portrait_asset=asset,
|
||||
metadata={"migrated_from_legacy_actor": True},
|
||||
)
|
||||
created += 1
|
||||
self.stdout.write(self.style.SUCCESS(f"完成:创建 {created} 个 Model;未修改任何 Asset、项目、垃圾桶或文件。"))
|
||||
Reference in New Issue
Block a user