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:
co-authored by
Claude Opus 4.6
parent
c17c64cd11
commit
2f24dcfd34
@@ -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 解析) ====================
|
||||
|
||||
|
||||
Reference in New Issue
Block a user