完成LSFX Mock第二期稳定随机命中后端实施
This commit is contained in:
@@ -197,3 +197,42 @@ def test_inner_flow_bank_statement_should_keep_same_rule_subset(client):
|
||||
).json()
|
||||
|
||||
assert page1["data"]["bankStatementList"] == page2["data"]["bankStatementList"]
|
||||
|
||||
|
||||
def test_inner_flow_should_apply_phase2_baselines_before_get_bank_statement(client, monkeypatch):
|
||||
from routers.api import file_service
|
||||
|
||||
applied = {}
|
||||
|
||||
def fake_apply(**kwargs):
|
||||
applied["called"] = True
|
||||
applied["baseline_rule_codes"] = kwargs["baseline_rule_codes"]
|
||||
|
||||
monkeypatch.setattr(file_service.phase2_baseline_service, "apply", fake_apply)
|
||||
monkeypatch.setattr(
|
||||
file_service,
|
||||
"_build_rule_hit_plan",
|
||||
lambda log_id: {
|
||||
"large_transaction_hit_rules": [],
|
||||
"phase1_hit_rules": [],
|
||||
"phase2_statement_hit_rules": ["MONTHLY_FIXED_INCOME"],
|
||||
"phase2_baseline_hit_rules": ["SUPPLIER_CONCENTRATION"],
|
||||
},
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/watson/api/project/getJZFileOrZjrcuFile",
|
||||
data={
|
||||
"groupId": 1001,
|
||||
"customerNo": "phase2_customer",
|
||||
"dataChannelCode": "channel_code",
|
||||
"requestDateId": 20240101,
|
||||
"dataStartDateId": 20240101,
|
||||
"dataEndDateId": 20240131,
|
||||
"uploadUserId": 902001,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert applied["called"] is True
|
||||
assert applied["baseline_rule_codes"] == ["SUPPLIER_CONCENTRATION"]
|
||||
|
||||
@@ -185,6 +185,17 @@ def test_build_rule_hit_plan_should_be_deterministic_for_same_log_id():
|
||||
assert 2 <= len(plan1["phase1_hit_rules"]) <= 4
|
||||
|
||||
|
||||
def test_phase2_rule_hit_plan_should_be_deterministic_for_same_log_id():
|
||||
service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
|
||||
|
||||
plan1 = service._build_rule_hit_plan(10001)
|
||||
plan2 = service._build_rule_hit_plan(10001)
|
||||
|
||||
assert plan1 == plan2
|
||||
assert 2 <= len(plan1["phase2_statement_hit_rules"]) <= 4
|
||||
assert 2 <= len(plan1["phase2_baseline_hit_rules"]) <= 4
|
||||
|
||||
|
||||
def test_fetch_inner_flow_should_persist_rule_hit_plan(monkeypatch):
|
||||
service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
|
||||
monkeypatch.setattr(
|
||||
@@ -218,3 +229,46 @@ def test_fetch_inner_flow_should_persist_rule_hit_plan(monkeypatch):
|
||||
"GAMBLING_SENSITIVE_KEYWORD",
|
||||
"FOREX_BUY_AMT",
|
||||
]
|
||||
|
||||
|
||||
def test_fetch_inner_flow_should_persist_phase2_rule_hit_plan(monkeypatch):
|
||||
service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"_build_rule_hit_plan",
|
||||
lambda log_id: {
|
||||
"large_transaction_hit_rules": ["HOUSE_OR_CAR_EXPENSE", "TAX_EXPENSE"],
|
||||
"phase1_hit_rules": ["GAMBLING_SENSITIVE_KEYWORD", "FOREX_BUY_AMT"],
|
||||
"phase2_statement_hit_rules": [
|
||||
"LOW_INCOME_RELATIVE_LARGE_TRANSACTION",
|
||||
"SALARY_QUICK_TRANSFER",
|
||||
],
|
||||
"phase2_baseline_hit_rules": [
|
||||
"HOUSE_REGISTRATION_MISMATCH",
|
||||
"SUPPLIER_CONCENTRATION",
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
response = service.fetch_inner_flow(
|
||||
{
|
||||
"groupId": 1001,
|
||||
"customerNo": "test_customer_001",
|
||||
"dataChannelCode": "test_code",
|
||||
"requestDateId": 20240101,
|
||||
"dataStartDateId": 20240101,
|
||||
"dataEndDateId": 20240131,
|
||||
"uploadUserId": 902001,
|
||||
}
|
||||
)
|
||||
log_id = response["data"][0]
|
||||
record = service.file_records[log_id]
|
||||
|
||||
assert record.phase2_statement_hit_rules == [
|
||||
"LOW_INCOME_RELATIVE_LARGE_TRANSACTION",
|
||||
"SALARY_QUICK_TRANSFER",
|
||||
]
|
||||
assert record.phase2_baseline_hit_rules == [
|
||||
"HOUSE_REGISTRATION_MISMATCH",
|
||||
"SUPPLIER_CONCENTRATION",
|
||||
]
|
||||
|
||||
101
lsfx-mock-server/tests/test_phase2_baseline_service.py
Normal file
101
lsfx-mock-server/tests/test_phase2_baseline_service.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
第二期数据库基线服务测试
|
||||
"""
|
||||
|
||||
from contextlib import nullcontext
|
||||
|
||||
from services.phase2_baseline_service import Phase2BaselineService
|
||||
|
||||
|
||||
def test_build_sql_plan_should_return_idempotent_sql_plan_for_selected_phase2_baselines():
|
||||
"""抽中第二期基线规则时,应生成幂等 SQL 计划。"""
|
||||
service = Phase2BaselineService()
|
||||
|
||||
sql_plan = service.build_sql_plan(
|
||||
staff_id_card="330101198801010011",
|
||||
family_id_cards=["330101199001010022"],
|
||||
baseline_rule_codes=[
|
||||
"SUPPLIER_CONCENTRATION",
|
||||
"HOUSE_REGISTRATION_MISMATCH",
|
||||
],
|
||||
)
|
||||
|
||||
assert any("LSFXMOCKP2PUR001" in sql for sql in sql_plan)
|
||||
assert any("LSFX Mock P2 HOUSE" in sql for sql in sql_plan)
|
||||
assert any("'房产'" in sql for sql in sql_plan)
|
||||
assert any("'正常'" in sql for sql in sql_plan)
|
||||
assert any(sql.strip().startswith("DELETE") for sql in sql_plan)
|
||||
assert any(sql.strip().startswith("INSERT") for sql in sql_plan)
|
||||
|
||||
|
||||
def test_build_sql_plan_should_skip_unselected_phase2_rules():
|
||||
"""未选中的规则不应写入无关 SQL。"""
|
||||
service = Phase2BaselineService()
|
||||
|
||||
sql_plan = service.build_sql_plan(
|
||||
staff_id_card="330101198801010011",
|
||||
family_id_cards=[],
|
||||
baseline_rule_codes=["SUPPLIER_CONCENTRATION"],
|
||||
)
|
||||
|
||||
assert any("LSFXMOCKP2PUR001" in sql for sql in sql_plan)
|
||||
assert not any("LSFX Mock P2 HOUSE" in sql for sql in sql_plan)
|
||||
assert not any("ccdi_asset_info" in sql for sql in sql_plan)
|
||||
|
||||
|
||||
def test_build_sql_plan_should_use_staff_scope_for_family_asset_baselines():
|
||||
"""亲属资产基线应保留员工归属与亲属实际持有人的双字段语义。"""
|
||||
service = Phase2BaselineService()
|
||||
|
||||
sql_plan = service.build_sql_plan(
|
||||
staff_id_card="330101198801010011",
|
||||
family_id_cards=["330101199001010022"],
|
||||
baseline_rule_codes=["PROPERTY_FEE_REGISTRATION_MISMATCH"],
|
||||
)
|
||||
|
||||
assert any("'330101198801010011'" in sql for sql in sql_plan)
|
||||
assert any("'330101199001010022'" in sql for sql in sql_plan)
|
||||
assert not any("'REAL_ESTATE'" in sql for sql in sql_plan)
|
||||
|
||||
|
||||
def test_apply_should_execute_generated_sql_plan(monkeypatch):
|
||||
"""apply() 应执行生成出的 SQL 计划,而不是只返回字符串。"""
|
||||
service = Phase2BaselineService()
|
||||
executed_sql = []
|
||||
committed = {"value": False}
|
||||
|
||||
class FakeCursor:
|
||||
def execute(self, sql):
|
||||
executed_sql.append(sql.strip())
|
||||
|
||||
class FakeConnection:
|
||||
def __init__(self):
|
||||
self.cursor_instance = FakeCursor()
|
||||
|
||||
def cursor(self):
|
||||
return nullcontext(self.cursor_instance)
|
||||
|
||||
def commit(self):
|
||||
committed["value"] = True
|
||||
|
||||
def rollback(self):
|
||||
raise AssertionError("测试路径不应触发回滚")
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(service, "_connect", lambda: FakeConnection())
|
||||
|
||||
result = service.apply(
|
||||
staff_id_card="330101198801010011",
|
||||
family_id_cards=["330101199001010022"],
|
||||
baseline_rule_codes=["SUPPLIER_CONCENTRATION"],
|
||||
)
|
||||
|
||||
assert result is None
|
||||
assert committed["value"] is True
|
||||
assert any("DELETE FROM ccdi_purchase_transaction" in sql for sql in executed_sql)
|
||||
assert any("INSERT INTO ccdi_purchase_transaction" in sql for sql in executed_sql)
|
||||
@@ -87,6 +87,54 @@ def test_build_seed_statements_for_rule_plan_should_generate_withdraw_cnt_sample
|
||||
) >= 4
|
||||
|
||||
|
||||
def test_build_seed_statements_for_rule_plan_should_only_include_requested_phase2_rules():
|
||||
plan = {
|
||||
"large_transaction_hit_rules": [],
|
||||
"phase1_hit_rules": [],
|
||||
"phase2_statement_hit_rules": [
|
||||
"MULTI_PARTY_GAMBLING_TRANSFER",
|
||||
"SALARY_QUICK_TRANSFER",
|
||||
],
|
||||
"phase2_baseline_hit_rules": [],
|
||||
}
|
||||
|
||||
statements = build_seed_statements_for_rule_plan(
|
||||
group_id=1000,
|
||||
log_id=30001,
|
||||
rule_plan=plan,
|
||||
)
|
||||
|
||||
assert any(item["userMemo"] == "工资入账" for item in statements)
|
||||
assert any(item["customerName"] == "欢乐游戏科技有限公司" for item in statements)
|
||||
assert not any(item["userMemo"] == "季度稳定兼职收入" for item in statements)
|
||||
|
||||
|
||||
def test_salary_quick_transfer_and_salary_unused_should_use_different_identity_groups():
|
||||
plan = {
|
||||
"large_transaction_hit_rules": [],
|
||||
"phase1_hit_rules": [],
|
||||
"phase2_statement_hit_rules": [
|
||||
"SALARY_QUICK_TRANSFER",
|
||||
"SALARY_UNUSED",
|
||||
],
|
||||
"phase2_baseline_hit_rules": [],
|
||||
}
|
||||
|
||||
statements = build_seed_statements_for_rule_plan(
|
||||
group_id=1000,
|
||||
log_id=30001,
|
||||
rule_plan=plan,
|
||||
)
|
||||
|
||||
salary_id_cards = {
|
||||
item["cretNo"]
|
||||
for item in statements
|
||||
if item["userMemo"] == "工资入账"
|
||||
}
|
||||
|
||||
assert len(salary_id_cards) >= 2
|
||||
|
||||
|
||||
def test_large_transaction_seed_should_cover_all_eight_rules():
|
||||
"""大额交易样本生成器必须覆盖 8 条已实现规则的关键口径。"""
|
||||
statements = build_large_transaction_seed_statements(group_id=1000, log_id=20001)
|
||||
|
||||
Reference in New Issue
Block a user