feat: 规则冲突检测增强 — 后端接入 DB 规则 + 前端集成检查按钮

后端 validate_rules 端点改为 async,合并 DB active 平台规则与硬编码兜底规则,
新增 selling_points 字段支持和时长冲突检测。前端品牌方/代理商 Brief 页面
添加"检查规则冲突"按钮,支持选择平台后展示冲突详情弹窗。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-02-10 14:12:49 +08:00
co-authored by Claude Opus 4.6
parent c17c64cd11
commit 2f24dcfd34
4 changed files with 580 additions and 37 deletions
+186 -2
View File
@@ -345,7 +345,7 @@ class TestRuleConflictDetection:
@pytest.mark.asyncio
async def test_detect_brief_platform_conflict(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""检测 Brief 与平台规则冲突"""
"""检测 Brief 与平台规则冲突required_phrases"""
response = await client.post(
"/api/v1/rules/validate",
headers={"X-Tenant-ID": tenant_id},
@@ -353,7 +353,7 @@ class TestRuleConflictDetection:
"brand_id": brand_id,
"platform": "douyin",
"brief_rules": {
"required_phrases": ["绝对有效"], # 可能违反平台规则
"required_phrases": ["绝对有效"],
}
}
)
@@ -386,6 +386,190 @@ class TestRuleConflictDetection:
assert "platform_rule" in conflict
assert "suggestion" in conflict
@pytest.mark.asyncio
async def test_selling_points_conflict_detection(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""selling_points 字段也参与冲突检测"""
response = await client.post(
"/api/v1/rules/validate",
headers={"X-Tenant-ID": tenant_id},
json={
"brand_id": brand_id,
"platform": "douyin",
"brief_rules": {
"selling_points": ["100%纯天然成分", "绝对安全"],
}
}
)
data = response.json()
assert len(data["conflicts"]) >= 2 # "100%" 和 "绝对" 都命中
@pytest.mark.asyncio
async def test_no_conflict_returns_empty(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""无冲突时返回空列表"""
response = await client.post(
"/api/v1/rules/validate",
headers={"X-Tenant-ID": tenant_id},
json={
"brand_id": brand_id,
"platform": "douyin",
"brief_rules": {
"selling_points": ["温和护肤", "适合敏感肌"],
}
}
)
data = response.json()
assert data["conflicts"] == []
@pytest.mark.asyncio
async def test_duration_conflict_brief_max_below_platform_min(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""Brief 最长时长低于平台最短要求"""
response = await client.post(
"/api/v1/rules/validate",
headers={"X-Tenant-ID": tenant_id},
json={
"brand_id": brand_id,
"platform": "douyin", # 硬编码 min_seconds=7
"brief_rules": {
"max_duration": 5,
}
}
)
data = response.json()
assert len(data["conflicts"]) >= 1
assert any("时长" in c["brief_rule"] for c in data["conflicts"])
@pytest.mark.asyncio
async def test_db_rules_participate_in_conflict_detection(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""DB 中 active 的规则参与冲突检测"""
headers = {"X-Tenant-ID": tenant_id}
# 创建并确认一条包含自定义违禁词的 DB 平台规则
create_resp = await _create_platform_rule(client, tenant_id, brand_id, platform="douyin")
rule_id = create_resp.json()["id"]
custom_rules = {
"forbidden_words": ["自定义违禁词ABC"],
"restricted_words": [],
"duration": {"min_seconds": 15, "max_seconds": 120},
"content_requirements": [],
"other_rules": [],
}
await client.put(
f"/api/v1/rules/platform-rules/{rule_id}/confirm",
headers=headers,
json={"parsed_rules": custom_rules},
)
# 验证 DB 违禁词参与检测
response = await client.post(
"/api/v1/rules/validate",
headers=headers,
json={
"brand_id": brand_id,
"platform": "douyin",
"brief_rules": {
"selling_points": ["这个自定义违禁词ABC很好"],
}
}
)
data = response.json()
assert len(data["conflicts"]) >= 1
assert any("自定义违禁词ABC" in c["suggestion"] for c in data["conflicts"])
@pytest.mark.asyncio
async def test_db_duration_conflict(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""DB 规则中的时长限制参与检测"""
headers = {"X-Tenant-ID": tenant_id}
# 创建 DB 规则:max_seconds=60
create_resp = await _create_platform_rule(client, tenant_id, brand_id, platform="xiaohongshu")
rule_id = create_resp.json()["id"]
custom_rules = {
"forbidden_words": [],
"restricted_words": [],
"duration": {"min_seconds": 10, "max_seconds": 60},
"content_requirements": [],
"other_rules": [],
}
await client.put(
f"/api/v1/rules/platform-rules/{rule_id}/confirm",
headers=headers,
json={"parsed_rules": custom_rules},
)
# Brief 最短时长 90s > 平台最长 60s → 冲突
response = await client.post(
"/api/v1/rules/validate",
headers=headers,
json={
"brand_id": brand_id,
"platform": "xiaohongshu",
"brief_rules": {
"min_duration": 90,
}
}
)
data = response.json()
assert len(data["conflicts"]) >= 1
assert any("最长限制" in c["platform_rule"] for c in data["conflicts"])
@pytest.mark.asyncio
async def test_db_and_hardcoded_rules_merge(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""DB 规则与硬编码规则合并检测"""
headers = {"X-Tenant-ID": tenant_id}
# DB 规则只包含自定义违禁词
create_resp = await _create_platform_rule(client, tenant_id, brand_id, platform="douyin")
rule_id = create_resp.json()["id"]
await client.put(
f"/api/v1/rules/platform-rules/{rule_id}/confirm",
headers=headers,
json={"parsed_rules": {
"forbidden_words": ["DB专属词"],
"restricted_words": [],
"duration": None,
"content_requirements": [],
"other_rules": [],
}},
)
# selling_points 同时包含 DB 违禁词和硬编码违禁词
response = await client.post(
"/api/v1/rules/validate",
headers=headers,
json={
"brand_id": brand_id,
"platform": "douyin",
"brief_rules": {
"selling_points": ["这是DB专属词内容", "最好的选择"],
}
}
)
data = response.json()
# 应同时检出 DB 违禁词和硬编码违禁词
suggestions = [c["suggestion"] for c in data["conflicts"]]
assert any("DB专属词" in s for s in suggestions)
assert any("最好" in s for s in suggestions)
@pytest.mark.asyncio
async def test_unknown_platform_returns_empty(self, client: AsyncClient, tenant_id: str, brand_id: str):
"""未知平台返回空冲突(无硬编码规则,无 DB 规则)"""
response = await client.post(
"/api/v1/rules/validate",
headers={"X-Tenant-ID": tenant_id},
json={
"brand_id": brand_id,
"platform": "unknown_platform",
"brief_rules": {
"selling_points": ["最好的产品"],
}
}
)
data = response.json()
assert data["conflicts"] == []
# ==================== 品牌方平台规则(文档上传 + AI 解析) ====================