Your Name e4959d584f feat: 完善代理商端业务逻辑与前后端框架
主要更新:
- 更新代理商端文档,明确项目由品牌方分配流程
- 新增Brief配置详情页(已配置)设计稿
- 完善工作台紧急待办中品牌新任务功能
- 整理Pencil设计文件中代理商端页面顺序
- 新增后端FastAPI框架及核心API
- 新增前端Next.js页面和组件库
- 添加.gitignore排除构建和缓存文件

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 19:27:31 +08:00

319 lines
9.7 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.

"""
脚本预审 API
"""
import re
from typing import Optional
from fastapi import APIRouter, Depends, Header
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.schemas.review import (
ScriptReviewRequest,
ScriptReviewResponse,
Violation,
ViolationType,
RiskLevel,
Position,
SoftRiskWarning,
)
from app.api.rules import (
get_whitelist_for_brand,
get_other_brands_whitelist_terms,
get_forbidden_words_for_tenant,
)
from app.services.soft_risk import evaluate_soft_risk
from app.services.ai_service import AIServiceFactory
router = APIRouter(prefix="/scripts", tags=["scripts"])
# 内置违禁词库(广告极限词)
ABSOLUTE_WORDS = ["最好", "第一", "最佳", "绝对", "100%"]
# 功效词库(医疗/功效宣称)
EFFICACY_WORDS = ["根治", "治愈", "治疗", "药效", "疗效", "特效"]
# 广告语境关键词(用于判断是否为广告场景)
AD_CONTEXT_KEYWORDS = ["产品", "购买", "销量", "品质", "推荐", "价格", "优惠", "促销"]
def _is_ad_context(content: str, word: str) -> bool:
"""
判断是否为广告语境
规则:
- 如果内容中包含广告关键词,认为是广告语境
- 如果违禁词出现在明显的非广告句式中,不是广告语境
"""
# 非广告语境模式
non_ad_patterns = [
r"他是第一[个名位]", # 他是第一个/名
r"[是为]第一[个名位]", # 是第一个
r"最开心|最高兴|最难忘", # 情感表达
r"第一[次个].*[到来抵达]", # 第一次到达
]
for pattern in non_ad_patterns:
if re.search(pattern, content):
return False
# 检查是否包含广告关键词
return any(kw in content for kw in AD_CONTEXT_KEYWORDS)
def _check_selling_point_coverage(content: str, required_points: list[str]) -> list[str]:
"""
检查卖点覆盖情况
使用语义匹配而非精确匹配
"""
missing = []
# 卖点关键词映射
point_keywords = {
"品牌名称": ["品牌", "牌子", "品牌A", "品牌B"],
"使用方法": ["使用", "用法", "早晚", "每天", "一次", "涂抹", "喷洒"],
"功效说明": ["功效", "效果", "水润", "美白", "保湿", "滋润", "改善"],
}
for point in required_points:
# 精确匹配
if point in content:
continue
# 关键词匹配
keywords = point_keywords.get(point, [])
if any(kw in content for kw in keywords):
continue
missing.append(point)
return missing
@router.post("/review", response_model=ScriptReviewResponse)
async def review_script(
request: ScriptReviewRequest,
x_tenant_id: str = Header(..., alias="X-Tenant-ID"),
db: AsyncSession = Depends(get_db),
) -> ScriptReviewResponse:
"""
脚本预审
- 检测违禁词(支持语境感知)
- 检测功效词
- 检查必要卖点
- 应用白名单
- 可选 AI 深度分析
- 返回合规分数和修改建议
"""
violations = []
content = request.content
# 获取品牌白名单
whitelist = await get_whitelist_for_brand(x_tenant_id, request.brand_id, db)
# 获取租户自定义违禁词
tenant_forbidden_words = await get_forbidden_words_for_tenant(x_tenant_id, db)
# 1. 违禁词检测(广告极限词)
all_forbidden_words = ABSOLUTE_WORDS + [w["word"] for w in tenant_forbidden_words]
for word in all_forbidden_words:
# 白名单跳过
if word in whitelist:
continue
start = 0
while True:
pos = content.find(word, start)
if pos == -1:
break
# 语境感知:非广告语境跳过
if not _is_ad_context(content, word):
start = pos + 1
continue
violations.append(Violation(
type=ViolationType.FORBIDDEN_WORD,
content=word,
severity=RiskLevel.HIGH,
suggestion=f"建议删除或替换违禁词:{word}",
position=Position(start=pos, end=pos + len(word)),
))
start = pos + 1
# 2. 功效词检测
for word in EFFICACY_WORDS:
if word in whitelist:
continue
start = 0
while True:
pos = content.find(word, start)
if pos == -1:
break
violations.append(Violation(
type=ViolationType.EFFICACY_CLAIM,
content=word,
severity=RiskLevel.HIGH,
suggestion=f"功效宣称词违反广告法,建议删除:{word}",
position=Position(start=pos, end=pos + len(word)),
))
start = pos + 1
# 3. 检测其他品牌专属词(品牌安全风险)
other_brand_terms = await get_other_brands_whitelist_terms(x_tenant_id, request.brand_id, db)
for term, owner_brand in other_brand_terms:
if term in content:
violations.append(Violation(
type=ViolationType.BRAND_SAFETY,
content=term,
severity=RiskLevel.MEDIUM,
suggestion=f"使用了其他品牌的专属词汇:{term}",
position=Position(start=content.find(term), end=content.find(term) + len(term)),
))
# 4. 检查遗漏卖点
missing_points: list[str] | None = None
if request.required_points:
missing = _check_selling_point_coverage(content, request.required_points)
missing_points = missing if missing else []
# 5. 可选AI 深度分析
ai_violations = await _ai_deep_analysis(x_tenant_id, content, db)
if ai_violations:
violations.extend(ai_violations)
# 6. 计算分数
score = 100 - len(violations) * 25
if missing_points:
score -= len(missing_points) * 5
score = max(0, score)
# 7. 生成摘要
parts = []
if violations:
parts.append(f"发现 {len(violations)} 处违规")
if missing_points:
parts.append(f"遗漏 {len(missing_points)} 个卖点")
if not parts:
summary = "脚本内容合规,未发现问题"
else:
summary = "".join(parts)
# 8. 软性风控评估
soft_warnings: list[SoftRiskWarning] = []
if request.soft_risk_context:
soft_warnings = evaluate_soft_risk(request.soft_risk_context)
return ScriptReviewResponse(
score=score,
summary=summary,
violations=violations,
missing_points=missing_points,
soft_warnings=soft_warnings,
)
async def _ai_deep_analysis(
tenant_id: str,
content: str,
db: AsyncSession,
) -> list[Violation]:
"""
使用 AI 进行深度分析
AI 分析失败时返回空列表,降级到规则检测
"""
try:
# 获取 AI 客户端
ai_client = await AIServiceFactory.get_client(tenant_id, db)
if not ai_client:
return []
# 获取模型配置
config = await AIServiceFactory.get_config(tenant_id, db)
if not config:
return []
text_model = config.models.get("text", "gpt-4o")
# 构建分析提示
analysis_prompt = f"""作为广告合规审核专家,请分析以下广告脚本内容,检测潜在的合规风险:
脚本内容:
{content}
请检查以下方面:
1. 是否存在隐性的虚假宣传(如暗示疗效但不直接说明)
2. 是否存在容易引起误解的表述
3. 是否存在夸大描述
4. 是否存在可能违反广告法的其他内容
如果发现问题,请以 JSON 数组格式返回,每项包含:
- type: 违规类型 (forbidden_word/efficacy_claim/brand_safety)
- content: 违规内容
- severity: 严重程度 (high/medium/low)
- suggestion: 修改建议
如果未发现问题,返回空数组 []
请只返回 JSON 数组,不要包含其他内容。"""
response = await ai_client.chat_completion(
messages=[{"role": "user", "content": analysis_prompt}],
model=text_model,
temperature=0.3,
max_tokens=1000,
)
# 解析 AI 响应
import json
try:
# 清理响应内容(移除可能的 markdown 标记)
response_content = response.content.strip()
if response_content.startswith("```"):
response_content = response_content.split("\n", 1)[1]
if response_content.endswith("```"):
response_content = response_content.rsplit("\n", 1)[0]
ai_results = json.loads(response_content)
violations = []
for item in ai_results:
violation_type = item.get("type", "forbidden_word")
if violation_type == "forbidden_word":
vtype = ViolationType.FORBIDDEN_WORD
elif violation_type == "efficacy_claim":
vtype = ViolationType.EFFICACY_CLAIM
else:
vtype = ViolationType.BRAND_SAFETY
severity = item.get("severity", "medium")
if severity == "high":
slevel = RiskLevel.HIGH
elif severity == "low":
slevel = RiskLevel.LOW
else:
slevel = RiskLevel.MEDIUM
violations.append(Violation(
type=vtype,
content=item.get("content", ""),
severity=slevel,
suggestion=item.get("suggestion", "建议修改"),
))
return violations
except json.JSONDecodeError:
# JSON 解析失败,返回空列表
return []
except Exception:
# AI 调用失败,降级到规则检测
return []