feat(models): 期1 模特库地基 — Model 顶级实体 + 模特库页 + 审核范围扩展

后端:
- 新增 Model(模特)团队级实体:形象图+三视图+声线(声线尾期),官方模板标签,软删
- Asset.Category 枚举一次加全(model_portrait/tri_view/voice/model_tryon/platform_kit/free_create/storyboard),逐期接线
- 送审范围 person → REVIEW_CATEGORIES{person,tri_view,storyboard}(review.py + adminpanel 队列),图片趴不送审;当前行为不变(forward-compat)
- /api/models/ ModelLibraryViewSet:本团队∪官方模板、官方/我的 tab、真人上传、软删(官方不可删)、跨团队资产引用防越权
- backfill_models 命令把历史成套模特(kind=model + 项目流命名配对)归集成 Model,dry-run 默认

前端:
- 左菜单新增「模特库」顶级入口(person 图标)+ 路由 /models
- 模特库页:全部/官方模板/我的模特 tab + 真人上传 + 模特卡(形象图+三视图缩略+官方标签),仅用 design token

测试:assets 10/10 绿;tsc+build 全绿;无头走查 0 console error(全部5/官方3/我的2,官方标签达标)
基线既有 7 失败(ai/projects provider mock 漂移)零新增

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
seaislee1209
2026-06-20 22:53:46 +08:00
co-authored by Claude Opus 4.8
parent 1c9143ce61
commit 44e90233ff
23 changed files with 1003 additions and 12 deletions
@@ -0,0 +1,46 @@
# Generated by Django 5.1.15 on 2026-06-20 14:32
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0005_invitation_kind_alter_invitation_team'),
('assets', '0005_asset_assets_asse_team_id_4b1b57_idx_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='asset',
name='category',
field=models.CharField(choices=[('person', 'Person'), ('scene', 'Scene'), ('product_image', 'Product Image'), ('video_clip', 'Video Clip'), ('final_video', 'Final Video'), ('upload', 'Upload'), ('uncategorized', 'Uncategorized'), ('model_portrait', 'Model Portrait'), ('tri_view', 'Tri View'), ('voice', 'Voice'), ('model_tryon', 'Model Try-on'), ('platform_kit', 'Platform Kit'), ('free_create', 'Free Create'), ('storyboard', 'Storyboard')], default='uncategorized', max_length=32),
),
migrations.CreateModel(
name='Model',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=255)),
('is_official', models.BooleanField(default=False)),
('source', models.CharField(choices=[('ai', 'AI Generated'), ('upload', 'Upload')], default='ai', max_length=16)),
('description', models.TextField(blank=True)),
('metadata', models.JSONField(blank=True, default=dict)),
('is_deleted', models.BooleanField(default=False)),
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_%(class)s_set', to=settings.AUTH_USER_MODEL)),
('portrait_asset', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='model_as_portrait', to='assets.asset')),
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='accounts.team')),
('triview_asset', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='model_as_triview', to='assets.asset')),
('voice_asset', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='model_as_voice', to='assets.asset')),
],
options={
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['team', 'is_official', '-created_at'], name='assets_mode_team_id_0caf77_idx')],
},
),
]