feat: 添加线索引擎、NLQ、场景检测、前端界面等核心功能模块
This commit is contained in:
@@ -43,7 +43,9 @@ def test_clue_full_lifecycle(session):
|
||||
assert clue.status == ClueStatus.ASSIGNED
|
||||
assert clue.assignee == "auditor_zhang"
|
||||
|
||||
paper = clue_svc.adjudicate(session, clue, confirmed=True, actor="auditor_zhang", note="属实,移交")
|
||||
paper = clue_svc.adjudicate(
|
||||
session, clue, confirmed=True, actor="auditor_zhang", note="属实,移交"
|
||||
)
|
||||
assert clue.status == ClueStatus.CONFIRMED
|
||||
assert clue.feedback == "confirmed"
|
||||
assert paper.conclusion == "confirmed"
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""线索/NLQ/看板 API 集成测试(需 PostgreSQL)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.db import get_session
|
||||
from app.engines import scan
|
||||
from app.main import app
|
||||
from app.scenarios.split_contract import ContractRecord
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(session):
|
||||
app.dependency_overrides[get_session] = lambda: session
|
||||
try:
|
||||
yield TestClient(app)
|
||||
finally:
|
||||
app.dependency_overrides.pop(get_session, None)
|
||||
|
||||
|
||||
def _seed_clue(session):
|
||||
contracts = [ContractRecord(f"C{i}", f"CUST{i}", 850000) for i in range(8)]
|
||||
return scan.run_split_contract_scan(
|
||||
session, contracts, approval_threshold=1_000_000, shared_controller=True
|
||||
).clue
|
||||
|
||||
|
||||
def test_list_and_get_clue(client, session):
|
||||
clue = _seed_clue(session)
|
||||
session.flush()
|
||||
resp = client.get("/clues")
|
||||
assert resp.status_code == 200
|
||||
assert any(c["id"] == str(clue.id) for c in resp.json())
|
||||
|
||||
resp2 = client.get(f"/clues/{clue.id}")
|
||||
assert resp2.status_code == 200
|
||||
assert resp2.json()["scenario_code"] == "R8"
|
||||
|
||||
|
||||
def test_assign_and_adjudicate_flow(client, session):
|
||||
clue = _seed_clue(session)
|
||||
session.flush()
|
||||
|
||||
r1 = client.post(
|
||||
f"/clues/{clue.id}/assign", json={"assignee": "auditor_w", "actor": "manager_l"}
|
||||
)
|
||||
assert r1.status_code == 200
|
||||
assert r1.json()["assignee"] == "auditor_w"
|
||||
assert r1.json()["status"] == "assigned"
|
||||
|
||||
r2 = client.post(
|
||||
f"/clues/{clue.id}/adjudicate",
|
||||
json={"confirmed": True, "actor": "auditor_w", "note": "属实"},
|
||||
)
|
||||
assert r2.status_code == 200
|
||||
assert r2.json()["status"] == "confirmed"
|
||||
assert r2.json()["feedback"] == "confirmed"
|
||||
|
||||
|
||||
def test_summary_endpoint(client, session):
|
||||
_seed_clue(session)
|
||||
session.flush()
|
||||
resp = client.get("/clues/summary")
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["total"] >= 1
|
||||
assert body["total_amount_involved"] > 0
|
||||
|
||||
|
||||
def test_no_delete_endpoint(client, session):
|
||||
"""R19:不存在删除线索的 API 端点。"""
|
||||
clue = _seed_clue(session)
|
||||
session.flush()
|
||||
resp = client.delete(f"/clues/{clue.id}")
|
||||
assert resp.status_code in (404, 405) # 方法不允许/路由不存在
|
||||
|
||||
|
||||
def test_nlq_endpoint_uses_local_provider(client):
|
||||
# 默认 .env 为 mock/dashscope;mock 不出域
|
||||
resp = client.post("/nlq", json={"question": "列出政企拆单线索"})
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert "answer" in body
|
||||
assert body["egress"] in (True, False)
|
||||
@@ -0,0 +1,37 @@
|
||||
"""NLQ 结构化检索集成测试(需 PostgreSQL)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.engines import scan
|
||||
from app.nlq import service as nlq
|
||||
from app.scenarios.split_contract import ContractRecord
|
||||
|
||||
|
||||
def _seed(session):
|
||||
contracts = [ContractRecord(f"C{i}", f"CUST{i}", 850000) for i in range(8)]
|
||||
scan.run_split_contract_scan(
|
||||
session, contracts, approval_threshold=1_000_000, shared_controller=True
|
||||
)
|
||||
session.flush()
|
||||
|
||||
|
||||
def test_nlq_retrieves_split_clues(session):
|
||||
_seed(session)
|
||||
ans = nlq.ask("列出高置信的政企拆单线索", session=session)
|
||||
assert ans.provider == "datahub"
|
||||
assert ans.egress is False
|
||||
assert "政企拆单" in ans.answer
|
||||
assert "共检索到" in ans.answer
|
||||
|
||||
|
||||
def test_nlq_no_match(session):
|
||||
ans = nlq.ask("列出养卡骗补线索", session=session)
|
||||
assert ans.egress is False
|
||||
assert "未检索到" in ans.answer or "共检索到" in ans.answer
|
||||
|
||||
|
||||
def test_nlq_open_question_falls_back_to_llm(session):
|
||||
# 不含检索关键词 → 走 LLM(mock)
|
||||
ans = nlq.ask("你好,请介绍一下你的能力", session=session)
|
||||
assert ans.provider in ("mock", "datahub")
|
||||
assert ans.egress is False
|
||||
@@ -0,0 +1,46 @@
|
||||
"""全量穿透扫描引擎集成测试(需 PostgreSQL)。
|
||||
|
||||
验证场景检测→线索生成→落库的端到端链路(R5+R7+R8/R9)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.clues.models import ClueStatus, ConfidenceTier
|
||||
from app.engines import scan
|
||||
from app.scenarios.churn_fraud import CohortPoint
|
||||
from app.scenarios.split_contract import ContractRecord
|
||||
|
||||
|
||||
def test_split_scan_creates_high_confidence_clue(session):
|
||||
contracts = [ContractRecord(f"C{i}", f"CUST{i}", 850000) for i in range(8)]
|
||||
result = scan.run_split_contract_scan(
|
||||
session, contracts, approval_threshold=1_000_000, shared_controller=True
|
||||
)
|
||||
assert result.scenario_code == "R8"
|
||||
assert result.scanned_count == 8
|
||||
assert result.clue is not None
|
||||
assert result.clue.confidence == ConfidenceTier.HIGH
|
||||
assert result.clue.status == ClueStatus.NEW
|
||||
assert result.clue.amount_involved > 0
|
||||
assert result.clue.model_version == scan.MODEL_VERSION
|
||||
|
||||
|
||||
def test_split_scan_no_clue_when_clean(session):
|
||||
contracts = [ContractRecord("C1", "A", 100000), ContractRecord("C2", "B", 3_000_000)]
|
||||
result = scan.run_split_contract_scan(session, contracts, approval_threshold=1_000_000)
|
||||
assert result.clue is None
|
||||
|
||||
|
||||
def test_churn_scan_creates_clue(session):
|
||||
curve = [CohortPoint(0, 1.0), CohortPoint(1, 0.95), CohortPoint(2, 0.1)]
|
||||
result = scan.run_churn_scan(
|
||||
session,
|
||||
retention_curve=curve,
|
||||
commission_paid=300000,
|
||||
active_ratio=0.05,
|
||||
zero_usage_ratio=0.9,
|
||||
channel_key="CH-001",
|
||||
)
|
||||
assert result.clue is not None
|
||||
assert result.clue.scenario_code == "R9"
|
||||
assert result.clue.subjects["channel"] == "CH-001"
|
||||
Reference in New Issue
Block a user