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:
co-authored by
Claude Opus 4.6
parent
a2f6f82e15
commit
fed361b9b3
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user