Your Name 0ef7650c09 feat: 审核体系全面改造 — 多维度评分 + 卖点优先级 + AI 语义匹配 + 品牌方 AI 状态通知
后端:
- 审核结果拆分为 4 个独立维度 (法规合规/平台规则/品牌安全/Brief匹配度)
- 卖点优先级从 required:bool 改为三级 (core/recommended/reference)
- AI 语义匹配卖点覆盖 + AI 整体 Brief 匹配度分析
- BriefMatchDetail 评分详情 (覆盖率+亮点+问题点)
- min_selling_points 代理商可配置最少卖点数 + Alembic 迁移
- AI 语境复核过滤误报
- Brief AI 解析 + 规则 AI 解析
- AI 未配置/异常时通知品牌方
- 种子数据更新 (新格式审核结果+brief_match_detail)

前端:
- 三端审核页面展示四维度评分卡片
- 卖点编辑改为三级优先级选择器
- BriefMatchDetail 展示 (覆盖率进度条+亮点+问题)
- min_selling_points 配置 UI
- AI 配置页未配置时静默处理
- 文件预览/下载/签名 URL 优化

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 19:11:54 +08:00

538 lines
22 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.

"""
种子数据脚本
创建 demo 用户、组织关系、项目、Brief、任务、规则数据
支持幂等运行:已存在则跳过
用法:
cd backend && python -m scripts.seed
"""
import asyncio
import sys
from datetime import datetime, timedelta, timezone
from sqlalchemy import select, insert, text
from sqlalchemy.ext.asyncio import AsyncSession
# 确保能找到 app 模块
sys.path.insert(0, ".")
from app.database import AsyncSessionLocal
from app.models import (
User, UserRole, Brand, Agency, Creator,
Project, Task, TaskStage, TaskStatus, Brief,
ForbiddenWord, WhitelistItem, Competitor, AIConfig, Tenant,
Message,
brand_agency_association, agency_creator_association,
project_agency_association,
)
from app.services.auth import hash_password
# ============================================================
# 固定 ID方便前端 mock 数据对齐和反复运行幂等检查
# ============================================================
BRAND_USER_ID = "U100001"
AGENCY_USER_ID = "U100002"
CREATOR_USER_ID = "U100003"
BRAND_ID = "BR100001"
AGENCY_ID = "AG100001"
CREATOR_ID = "CR100001"
TENANT_ID = BRAND_ID # 品牌方 = 租户
PROJECT_ID = "PJ100001"
BRIEF_ID = "BF100001"
TASK_IDS = ["TK100001", "TK100002", "TK100003", "TK100004"]
PASSWORD_HASH = hash_password("demo123")
NOW = datetime.now(timezone.utc)
async def seed_data() -> None:
async with AsyncSessionLocal() as db:
# ========== 幂等检查 ==========
result = await db.execute(
select(User).where(User.email == "brand@demo.com")
)
if result.scalar_one_or_none():
print("✅ 种子数据已存在,跳过创建")
return
print("🌱 开始创建种子数据...")
# ========== 1. Demo 用户 ==========
brand_user = User(
id=BRAND_USER_ID,
email="brand@demo.com",
password_hash=PASSWORD_HASH,
name="秒思科技",
role=UserRole.BRAND,
is_active=True,
is_verified=True,
)
agency_user = User(
id=AGENCY_USER_ID,
email="agency@demo.com",
password_hash=PASSWORD_HASH,
name="星辰传媒",
role=UserRole.AGENCY,
is_active=True,
is_verified=True,
)
creator_user = User(
id=CREATOR_USER_ID,
email="creator@demo.com",
password_hash=PASSWORD_HASH,
name="李小红",
role=UserRole.CREATOR,
is_active=True,
is_verified=True,
)
db.add_all([brand_user, agency_user, creator_user])
await db.flush()
print(" ✓ 用户已创建: brand@demo.com / agency@demo.com / creator@demo.com")
# ========== 2. 组织实体 ==========
brand = Brand(
id=BRAND_ID,
user_id=BRAND_USER_ID,
name="秒思科技",
description="秒思科技是一家专注于 AI 内容合规的科技公司",
contact_name="张经理",
contact_phone="13800138000",
contact_email="brand@demo.com",
final_review_enabled=True,
is_active=True,
)
agency = Agency(
id=AGENCY_ID,
user_id=AGENCY_USER_ID,
name="星辰传媒",
description="星辰传媒是一家专业的内容营销代理商",
contact_name="王总监",
contact_phone="13900139000",
contact_email="agency@demo.com",
force_pass_enabled=True,
is_active=True,
)
creator = Creator(
id=CREATOR_ID,
user_id=CREATOR_USER_ID,
name="李小红",
bio="美妆博主,专注护肤分享,全网粉丝 50 万+",
douyin_account="lixiaohong_dy",
xiaohongshu_account="lixiaohong_xhs",
is_active=True,
)
db.add_all([brand, agency, creator])
await db.flush()
print(" ✓ 组织已创建: 秒思科技 / 星辰传媒 / 李小红")
# ========== 3. 租户(兼容旧表) ==========
tenant = Tenant(
id=TENANT_ID,
name="秒思科技",
is_active=True,
)
db.add(tenant)
await db.flush()
print(" ✓ 租户已创建: 秒思科技")
# ========== 4. 组织关联关系 ==========
await db.execute(
insert(brand_agency_association).values(
brand_id=BRAND_ID,
agency_id=AGENCY_ID,
is_active=True,
)
)
await db.execute(
insert(agency_creator_association).values(
agency_id=AGENCY_ID,
creator_id=CREATOR_ID,
is_active=True,
)
)
await db.flush()
print(" ✓ 组织关系已建立: 品牌方 → 代理商 → 达人")
# ========== 5. 项目 ==========
project = Project(
id=PROJECT_ID,
brand_id=BRAND_ID,
name="2026春季新品推广",
description="春季新品防晒霜推广活动,面向 18-35 岁女性用户,重点投放抖音和小红书平台",
platform="douyin",
start_date=NOW,
deadline=NOW + timedelta(days=30),
status="active",
)
db.add(project)
await db.flush()
# 项目 → 代理商关联
await db.execute(
insert(project_agency_association).values(
project_id=PROJECT_ID,
agency_id=AGENCY_ID,
is_active=True,
)
)
await db.flush()
print(" ✓ 项目已创建: 2026春季新品推广")
# ========== 6. Brief ==========
brief = Brief(
id=BRIEF_ID,
project_id=PROJECT_ID,
selling_points=[
{"content": "SPF50+ PA++++,超强防晒", "priority": "core"},
{"content": "轻薄不油腻,适合日常通勤", "priority": "core"},
{"content": "添加玻尿酸成分,防晒同时保湿", "priority": "recommended"},
{"content": "获得皮肤科医生推荐", "priority": "reference"},
],
blacklist_words=[
{"word": "最好", "reason": "绝对化用语"},
{"word": "第一", "reason": "绝对化用语"},
{"word": "纯天然", "reason": "虚假宣传"},
],
competitors=["安耐晒", "怡思丁", "薇诺娜"],
brand_tone="年轻、活力、专业、可信赖",
min_selling_points=2,
min_duration=30,
max_duration=60,
other_requirements="请在视频中展示产品实际使用效果,包含户外场景拍摄",
)
db.add(brief)
await db.flush()
print(" ✓ Brief 已创建")
# ========== 7. 示例任务4 种阶段) ==========
tasks = [
# TK-001: 等待上传脚本
Task(
id=TASK_IDS[0],
project_id=PROJECT_ID,
agency_id=AGENCY_ID,
creator_id=CREATOR_ID,
name="春季防晒霜种草视频(1)",
sequence=1,
stage=TaskStage.SCRIPT_UPLOAD,
),
# TK-002: 脚本等待代理商审核
Task(
id=TASK_IDS[1],
project_id=PROJECT_ID,
agency_id=AGENCY_ID,
creator_id=CREATOR_ID,
name="春季防晒霜种草视频(2)",
sequence=2,
stage=TaskStage.SCRIPT_AGENCY_REVIEW,
script_file_url="https://example.com/scripts/demo-script.pdf",
script_file_name="防晒霜种草脚本v2.pdf",
script_uploaded_at=NOW - timedelta(hours=2),
script_ai_score=85,
script_ai_result={
"score": 85,
"summary": "脚本整体符合要求,卖点覆盖充分",
"dimensions": {
"legal": {"score": 100, "passed": True, "issue_count": 0},
"platform": {"score": 85, "passed": True, "issue_count": 1},
"brand_safety": {"score": 100, "passed": True, "issue_count": 0},
"brief_match": {"score": 80, "passed": True, "issue_count": 1},
},
"selling_point_matches": [
{"content": "SPF50+ PA++++,超强防晒", "priority": "core", "matched": True, "evidence": "脚本中提到了SPF50+防晒参数"},
{"content": "轻薄不油腻,适合日常通勤", "priority": "core", "matched": True, "evidence": "提到了轻薄质地不油腻"},
{"content": "添加玻尿酸成分,防晒同时保湿", "priority": "recommended", "matched": False, "evidence": "未提及玻尿酸成分"},
],
"brief_match_detail": {
"total_points": 3,
"matched_points": 2,
"required_points": 2,
"coverage_score": 100,
"overall_score": 75,
"highlights": [
"防晒参数描述准确SPF50+ PA++++完整提及",
"产品使用场景贴合Brief要求的日常通勤场景",
],
"issues": [
"缺少玻尿酸保湿成分的说明,建议补充产品成分亮点",
"脚本中使用了\"神器\"等夸张用语,需替换为更客观的表述",
],
"explanation": "脚本覆盖了2/2条要求卖点核心卖点全部匹配。整体内容方向正确但部分细节可优化。",
},
"violations": [
{"type": "forbidden_word", "content": "神器", "severity": "medium", "suggestion": "建议替换为\"好物\"", "dimension": "platform"},
],
"soft_warnings": [
{"type": "suggestion", "content": "建议增加产品成分说明", "suggestion": "可提及玻尿酸等核心成分"},
],
},
script_ai_reviewed_at=NOW - timedelta(hours=1),
),
# TK-003: 脚本已通过,等待上传视频
Task(
id=TASK_IDS[2],
project_id=PROJECT_ID,
agency_id=AGENCY_ID,
creator_id=CREATOR_ID,
name="春季防晒霜种草视频(3)",
sequence=3,
stage=TaskStage.VIDEO_UPLOAD,
script_file_url="https://example.com/scripts/demo-script-3.pdf",
script_file_name="防晒霜种草脚本v3.pdf",
script_uploaded_at=NOW - timedelta(days=2),
script_ai_score=92,
script_ai_result={
"score": 92,
"summary": "脚本质量优秀,完全符合 Brief 要求",
"dimensions": {
"legal": {"score": 100, "passed": True, "issue_count": 0},
"platform": {"score": 100, "passed": True, "issue_count": 0},
"brand_safety": {"score": 100, "passed": True, "issue_count": 0},
"brief_match": {"score": 90, "passed": True, "issue_count": 0},
},
"selling_point_matches": [
{"content": "SPF50+ PA++++,超强防晒", "priority": "core", "matched": True, "evidence": "脚本完整提及防晒参数"},
{"content": "轻薄不油腻,适合日常通勤", "priority": "core", "matched": True, "evidence": "详细描述了质地体验"},
{"content": "添加玻尿酸成分,防晒同时保湿", "priority": "recommended", "matched": True, "evidence": "提及了玻尿酸保湿功能"},
],
"brief_match_detail": {
"total_points": 3,
"matched_points": 3,
"required_points": 2,
"coverage_score": 100,
"overall_score": 90,
"highlights": [
"所有核心和推荐卖点均完整覆盖",
"产品使用场景自然与Brief要求高度一致",
"成分说明准确,玻尿酸保湿功能表述清晰",
],
"issues": [],
"explanation": "脚本覆盖了3/2条要求卖点超出要求与Brief整体匹配度优秀。",
},
"violations": [],
"soft_warnings": [],
},
script_ai_reviewed_at=NOW - timedelta(days=2),
script_agency_status=TaskStatus.PASSED,
script_agency_comment="脚本内容不错,可以进入拍摄",
script_agency_reviewer_id=AGENCY_USER_ID,
script_agency_reviewed_at=NOW - timedelta(days=1),
script_brand_status=TaskStatus.PASSED,
script_brand_comment="同意",
script_brand_reviewer_id=BRAND_USER_ID,
script_brand_reviewed_at=NOW - timedelta(days=1),
),
# TK-004: 已完成
Task(
id=TASK_IDS[3],
project_id=PROJECT_ID,
agency_id=AGENCY_ID,
creator_id=CREATOR_ID,
name="春季防晒霜种草视频(4)",
sequence=4,
stage=TaskStage.COMPLETED,
script_file_url="https://example.com/scripts/demo-script-4.pdf",
script_file_name="防晒霜种草脚本v4.pdf",
script_uploaded_at=NOW - timedelta(days=7),
script_ai_score=90,
script_ai_result={
"score": 90,
"summary": "符合要求",
"dimensions": {
"legal": {"score": 100, "passed": True, "issue_count": 0},
"platform": {"score": 100, "passed": True, "issue_count": 0},
"brand_safety": {"score": 100, "passed": True, "issue_count": 0},
"brief_match": {"score": 85, "passed": True, "issue_count": 0},
},
"selling_point_matches": [],
"violations": [],
"soft_warnings": [],
},
script_ai_reviewed_at=NOW - timedelta(days=7),
script_agency_status=TaskStatus.PASSED,
script_agency_comment="通过",
script_agency_reviewer_id=AGENCY_USER_ID,
script_agency_reviewed_at=NOW - timedelta(days=6),
script_brand_status=TaskStatus.PASSED,
script_brand_comment="通过",
script_brand_reviewer_id=BRAND_USER_ID,
script_brand_reviewed_at=NOW - timedelta(days=6),
video_file_url="https://example.com/videos/demo-video-4.mp4",
video_file_name="防晒霜种草视频v4.mp4",
video_duration=45,
video_uploaded_at=NOW - timedelta(days=5),
video_ai_score=88,
video_ai_result={
"score": 88,
"summary": "视频质量良好",
"dimensions": {
"legal": {"score": 100, "passed": True, "issue_count": 0},
"platform": {"score": 100, "passed": True, "issue_count": 0},
"brand_safety": {"score": 85, "passed": True, "issue_count": 0},
"brief_match": {"score": 80, "passed": True, "issue_count": 0},
},
"selling_point_matches": [],
"violations": [],
"soft_warnings": [],
},
video_ai_reviewed_at=NOW - timedelta(days=5),
video_agency_status=TaskStatus.PASSED,
video_agency_comment="视频效果好",
video_agency_reviewer_id=AGENCY_USER_ID,
video_agency_reviewed_at=NOW - timedelta(days=4),
video_brand_status=TaskStatus.PASSED,
video_brand_comment="终审通过",
video_brand_reviewer_id=BRAND_USER_ID,
video_brand_reviewed_at=NOW - timedelta(days=3),
),
]
db.add_all(tasks)
await db.flush()
print(" ✓ 任务已创建: TK100001~TK100004 (4种阶段)")
# ========== 8. 规则数据 ==========
forbidden_words = [
ForbiddenWord(id="FW100001", tenant_id=TENANT_ID, word="假药", category="法规违禁", severity="high"),
ForbiddenWord(id="FW100002", tenant_id=TENANT_ID, word="虚假宣传", category="法规违禁", severity="high"),
ForbiddenWord(id="FW100003", tenant_id=TENANT_ID, word="最好", category="绝对化用语", severity="medium"),
ForbiddenWord(id="FW100004", tenant_id=TENANT_ID, word="第一", category="绝对化用语", severity="medium"),
ForbiddenWord(id="FW100005", tenant_id=TENANT_ID, word="纯天然", category="虚假宣传", severity="medium"),
]
db.add_all(forbidden_words)
await db.flush()
print(" ✓ 违禁词已创建: 5 条")
competitors = [
Competitor(id="CP100001", tenant_id=TENANT_ID, brand_id=BRAND_ID, name="安耐晒", keywords=["安耐晒", "ANESSA", "资生堂防晒"]),
Competitor(id="CP100002", tenant_id=TENANT_ID, brand_id=BRAND_ID, name="怡思丁", keywords=["怡思丁", "ISDIN"]),
Competitor(id="CP100003", tenant_id=TENANT_ID, brand_id=BRAND_ID, name="薇诺娜", keywords=["薇诺娜", "WINONA"]),
]
db.add_all(competitors)
await db.flush()
print(" ✓ 竞品已创建: 3 条")
whitelist_items = [
WhitelistItem(id="WL100001", tenant_id=TENANT_ID, brand_id=BRAND_ID, term="SPF50+", reason="产品实际参数,非夸大宣传"),
WhitelistItem(id="WL100002", tenant_id=TENANT_ID, brand_id=BRAND_ID, term="PA++++", reason="产品实际参数,非夸大宣传"),
]
db.add_all(whitelist_items)
await db.flush()
print(" ✓ 白名单已创建: 2 条")
# ========== 9. AI 配置(模板) ==========
ai_config = AIConfig(
tenant_id=TENANT_ID,
provider="oneapi",
base_url="https://api.example.com/v1",
api_key_encrypted="demo-placeholder-key",
models={"text": "gpt-4o", "vision": "gpt-4o", "audio": "whisper-1"},
temperature=0.7,
max_tokens=2000,
is_configured=False,
)
db.add(ai_config)
await db.flush()
print(" ✓ AI 配置模板已创建")
# ========== 10. 示例消息 ==========
messages = [
# 达人消息
Message(
id="MSG100001",
user_id=CREATOR_USER_ID,
type="new_task",
title="新任务分配",
content="您有新的任务「春季防晒霜种草视频(1)」来自项目「2026春季新品推广」",
is_read=False,
related_task_id=TASK_IDS[0],
related_project_id=PROJECT_ID,
sender_name="星辰传媒",
),
Message(
id="MSG100002",
user_id=CREATOR_USER_ID,
type="pass",
title="脚本审核通过",
content="您的任务「春季防晒霜种草视频(3)」脚本已被通过",
is_read=True,
related_task_id=TASK_IDS[2],
sender_name="星辰传媒",
),
Message(
id="MSG100003",
user_id=CREATOR_USER_ID,
type="system_notice",
title="系统通知",
content="平台违禁词库已更新,请在创作时注意避免使用新增的违禁词",
is_read=True,
),
# 代理商消息
Message(
id="MSG100004",
user_id=AGENCY_USER_ID,
type="new_task",
title="新脚本提交",
content="达人「李小红」提交了「春季防晒霜种草视频(2)」脚本,请及时审核",
is_read=False,
related_task_id=TASK_IDS[1],
sender_name="李小红",
),
Message(
id="MSG100005",
user_id=AGENCY_USER_ID,
type="pass",
title="品牌终审通过",
content="任务「春季防晒霜种草视频(4)」已通过品牌方终审",
is_read=True,
related_task_id=TASK_IDS[3],
sender_name="秒思科技",
),
# 品牌方消息
Message(
id="MSG100006",
user_id=BRAND_USER_ID,
type="new_task",
title="脚本待终审",
content="「星辰传媒」的达人「李小红」脚本已通过代理商审核,请进行终审",
is_read=False,
related_task_id=TASK_IDS[1],
sender_name="星辰传媒",
),
Message(
id="MSG100007",
user_id=BRAND_USER_ID,
type="system_notice",
title="项目创建成功",
content="您的项目「2026春季新品推广」已创建成功",
is_read=True,
related_project_id=PROJECT_ID,
),
]
db.add_all(messages)
await db.flush()
print(" ✓ 示例消息已创建: 7 条 (达人3 + 代理商2 + 品牌方2)")
# ========== 提交 ==========
await db.commit()
print("\n🎉 种子数据创建完成!")
print("=" * 50)
print("Demo 账号:")
print(" 品牌方: brand@demo.com / demo123")
print(" 代理商: agency@demo.com / demo123")
print(" 达人: creator@demo.com / demo123")
print("=" * 50)
def main():
asyncio.run(seed_data())
if __name__ == "__main__":
main()