feat: 平台规则从硬编码改为品牌方上传文档 + AI 解析

- 新增 PlatformRule 模型 (draft/active/inactive 状态流转)
- 新增文档解析服务 (PDF/Word/Excel → 纯文本)
- 新增 4 个 API: 解析/确认/查询/删除平台规则
- 脚本审核优先从 DB 读取 active 规则,硬编码兜底
- 视频审核合并平台规则违禁词到检测列表
- Alembic 迁移 006: platform_rules 表

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-02-10 13:23:11 +08:00
co-authored by Claude Opus 4.6
parent a2f6f82e15
commit fed361b9b3
10 changed files with 790 additions and 34 deletions
+58 -8
View File
@@ -14,7 +14,7 @@ from sqlalchemy.orm import sessionmaker
from app.config import settings
from app.models.review import ReviewTask, TaskStatus as DBTaskStatus
from app.models.rule import ForbiddenWord, Competitor
from app.models.rule import ForbiddenWord, Competitor, PlatformRule, RuleStatus
from app.models.ai_config import AIConfig
from app.services.video_download import VideoDownloadService, DownloadResult
from app.services.keyframe import KeyFrameExtractor, ExtractionResult
@@ -81,6 +81,7 @@ async def complete_review(
summary: str,
violations: list[dict],
status: DBTaskStatus = DBTaskStatus.COMPLETED,
soft_warnings: Optional[list[dict]] = None,
):
"""完成审核"""
result = await db.execute(
@@ -94,6 +95,8 @@ async def complete_review(
task.score = score
task.summary = summary
task.violations = violations
if soft_warnings is not None:
task.soft_warnings = soft_warnings
task.completed_at = datetime.now(timezone.utc)
await db.commit()
@@ -153,6 +156,24 @@ async def get_competitors(db: AsyncSession, tenant_id: str, brand_id: str) -> li
return [row[0] for row in result.fetchall()]
async def get_platform_forbidden_words(
db: AsyncSession, tenant_id: str, brand_id: str, platform: str,
) -> list[str]:
"""从 DB 获取品牌方在该平台的 active 规则中的违禁词"""
result = await db.execute(
select(PlatformRule).where(
PlatformRule.tenant_id == tenant_id,
PlatformRule.brand_id == brand_id,
PlatformRule.platform == platform,
PlatformRule.status == RuleStatus.ACTIVE.value,
)
)
rule = result.scalar_one_or_none()
if not rule or not rule.parsed_rules:
return []
return rule.parsed_rules.get("forbidden_words", [])
async def process_video_review(
review_id: str,
tenant_id: str,
@@ -199,6 +220,13 @@ async def process_video_review(
# 获取规则
forbidden_words = await get_forbidden_words(db, tenant_id)
# 合并平台规则中的违禁词
platform_fw = await get_platform_forbidden_words(db, tenant_id, brand_id, platform)
existing_set = set(forbidden_words)
for w in platform_fw:
if w not in existing_set:
forbidden_words.append(w)
existing_set.add(w)
competitors = await get_competitors(db, tenant_id, brand_id)
# 初始化 AI 服务
@@ -281,16 +309,37 @@ async def process_video_review(
)
all_violations.extend(subtitle_violations)
# 6. 计算分数和生成报告
# 6. 分流 violations / soft_warnings
await update_review_progress(db, review_id, 90, "生成报告")
score = review_service.calculate_score(all_violations)
if not all_violations:
hard_violations = []
soft_warnings_data = []
for v in all_violations:
v_type = v.get("type", "")
if v_type in ("forbidden_word", "efficacy_claim", "competitor_logo", "brand_safety"):
hard_violations.append(v)
elif v_type in ("duration_short", "mention_missing"):
soft_warnings_data.append({
"code": f"video_{v_type}",
"message": v.get("content", ""),
"action_required": "note",
"blocking": False,
"context": {"suggestion": v.get("suggestion", "")},
})
else:
hard_violations.append(v) # 默认当硬性违规
# 计算分数(仅硬性违规影响分数)
score = review_service.calculate_score(hard_violations)
if not hard_violations:
summary = "视频内容合规,未发现违规项"
if soft_warnings_data:
summary += f"{len(soft_warnings_data)} 条提醒)"
else:
high_count = sum(1 for v in all_violations if v.get("risk_level") == "high")
medium_count = sum(1 for v in all_violations if v.get("risk_level") == "medium")
summary = f"发现 {len(all_violations)} 处违规"
high_count = sum(1 for v in hard_violations if v.get("risk_level") == "high")
summary = f"发现 {len(hard_violations)} 处违规"
if high_count > 0:
summary += f"{high_count} 处高风险)"
@@ -300,7 +349,8 @@ async def process_video_review(
review_id,
score=score,
summary=summary,
violations=all_violations,
violations=hard_violations,
soft_warnings=soft_warnings_data if soft_warnings_data else None,
)
except Exception as e: