实现lsfx-mock全命中SQL对齐

This commit is contained in:
wkc
2026-03-25 10:05:30 +08:00
parent f217d59f09
commit 5eea3c66ff
9 changed files with 523 additions and 24 deletions

View File

@@ -236,3 +236,70 @@ def test_inner_flow_should_apply_phase2_baselines_before_get_bank_statement(clie
assert response.status_code == 200
assert applied["called"] is True
assert applied["baseline_rule_codes"] == ["SUPPLIER_CONCENTRATION"]
def test_all_mode_should_expose_sql_aligned_target_rule_samples(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("services.file_service.settings.RULE_HIT_MODE", "all")
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": [
"SPECIAL_AMOUNT_TRANSACTION",
"SUSPICIOUS_INCOME_KEYWORD",
],
"phase2_statement_hit_rules": [
"LOW_INCOME_RELATIVE_LARGE_TRANSACTION",
"MONTHLY_FIXED_INCOME",
"FIXED_COUNTERPARTY_TRANSFER",
],
"phase2_baseline_hit_rules": [],
},
)
fetch_response = client.post(
"/watson/api/project/getJZFileOrZjrcuFile",
data={
"groupId": 1001,
"customerNo": "customer_sql_aligned_all_mode",
"dataChannelCode": "test_code",
"requestDateId": 20240101,
"dataStartDateId": 20240101,
"dataEndDateId": 20240131,
"uploadUserId": 902001,
},
)
assert fetch_response.status_code == 200
log_id = fetch_response.json()["data"][0]
assert applied["called"] is True
assert applied["baseline_rule_codes"] == ["LOW_INCOME_RELATIVE_LARGE_TRANSACTION"]
statement_response = client.post(
"/watson/api/project/getBSByLogId",
data={
"groupId": 1001,
"logId": log_id,
"pageNow": 1,
"pageSize": 200,
},
)
assert statement_response.status_code == 200
statements = statement_response.json()["data"]["bankStatementList"]
assert statements
assert any(item["transAmount"] == 1314.0 for item in statements)
assert any(item["userMemo"] == "劳务费发放" for item in statements)
assert any(item["userMemo"] == "月度稳定兼职收入" for item in statements)
assert any(item["userMemo"] == "季度稳定兼职收入" for item in statements)
assert any(item["userMemo"] in {"亲属大额转入", "亲属经营补贴"} for item in statements)

View File

@@ -215,6 +215,35 @@ def test_build_rule_hit_plan_should_return_all_compatible_rules_in_all_mode(monk
assert plan["phase2_baseline_hit_rules"] == PHASE2_BASELINE_RULE_CODES
def test_build_rule_hit_plan_should_keep_sql_aligned_target_rules_in_all_mode(monkeypatch):
monkeypatch.setattr("services.file_service.settings.RULE_HIT_MODE", "all")
service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
plan = service._build_rule_hit_plan(10001)
assert "SPECIAL_AMOUNT_TRANSACTION" in plan["phase1_hit_rules"]
assert "SUSPICIOUS_INCOME_KEYWORD" in plan["phase1_hit_rules"]
assert "LOW_INCOME_RELATIVE_LARGE_TRANSACTION" in plan["phase2_statement_hit_rules"]
assert "MONTHLY_FIXED_INCOME" in plan["phase2_statement_hit_rules"]
assert "FIXED_COUNTERPARTY_TRANSFER" in plan["phase2_statement_hit_rules"]
def test_build_rule_hit_plan_should_not_include_placeholder_rules_in_all_mode(monkeypatch):
monkeypatch.setattr("services.file_service.settings.RULE_HIT_MODE", "all")
service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
plan = service._build_rule_hit_plan(10001)
all_rule_codes = {
*plan["large_transaction_hit_rules"],
*plan["phase1_hit_rules"],
*plan["phase2_statement_hit_rules"],
*plan["phase2_baseline_hit_rules"],
}
assert "ABNORMAL_CUSTOMER_TRANSACTION" not in all_rule_codes
assert "INTEREST_PAYMENT_BY_OTHERS" not in all_rule_codes
def test_build_rule_hit_plan_should_keep_subset_mode_as_default():
service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
@@ -317,3 +346,38 @@ def test_fetch_inner_flow_should_persist_phase2_rule_hit_plan(monkeypatch):
"HOUSE_REGISTRATION_MISMATCH",
"SUPPLIER_CONCENTRATION",
]
def test_fetch_inner_flow_should_apply_low_income_baseline_in_all_mode(monkeypatch):
service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
applied = {}
def fake_apply(**kwargs):
applied["baseline_rule_codes"] = kwargs["baseline_rule_codes"]
monkeypatch.setattr("services.file_service.settings.RULE_HIT_MODE", "all")
monkeypatch.setattr(service.phase2_baseline_service, "apply", fake_apply)
monkeypatch.setattr(
service,
"_build_rule_hit_plan",
lambda log_id: {
"large_transaction_hit_rules": [],
"phase1_hit_rules": [],
"phase2_statement_hit_rules": ["LOW_INCOME_RELATIVE_LARGE_TRANSACTION"],
"phase2_baseline_hit_rules": [],
},
)
service.fetch_inner_flow(
{
"groupId": 1001,
"customerNo": "test_customer_low_income",
"dataChannelCode": "test_code",
"requestDateId": 20240101,
"dataStartDateId": 20240101,
"dataEndDateId": 20240131,
"uploadUserId": 902001,
}
)
assert applied["baseline_rule_codes"] == ["LOW_INCOME_RELATIVE_LARGE_TRANSACTION"]

View File

@@ -43,6 +43,23 @@ def test_build_sql_plan_should_skip_unselected_phase2_rules():
assert not any("ccdi_asset_info" in sql for sql in sql_plan)
def test_build_sql_plan_should_include_low_income_family_baseline():
"""低收入亲属大额交易需要额外补齐关系表收入基线。"""
service = Phase2BaselineService()
sql_plan = service.build_sql_plan(
staff_id_card="330101198801010011",
family_id_cards=["330101199001010022"],
baseline_rule_codes=["LOW_INCOME_RELATIVE_LARGE_TRANSACTION"],
)
assert any("ccdi_staff_fmy_relation" in sql for sql in sql_plan)
assert any("'330101198801010011'" in sql for sql in sql_plan)
assert any("'330101199001010022'" in sql for sql in sql_plan)
assert any("annual_income" in sql for sql in sql_plan)
assert any("0.00" in sql or "0," in sql or " 0\n" in sql for sql in sql_plan)
def test_build_sql_plan_should_use_staff_scope_for_family_asset_baselines():
"""亲属资产基线应保留员工归属与亲属实际持有人的双字段语义。"""
service = Phase2BaselineService()
@@ -99,3 +116,43 @@ def test_apply_should_execute_generated_sql_plan(monkeypatch):
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)
def test_apply_should_execute_low_income_family_baseline_sql(monkeypatch):
"""apply() 在低收入亲属规则下应执行关系表基线 SQL。"""
service = Phase2BaselineService()
executed_sql = []
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):
return None
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())
service.apply(
staff_id_card="330101198801010011",
family_id_cards=["330101199001010022"],
baseline_rule_codes=["LOW_INCOME_RELATIVE_LARGE_TRANSACTION"],
)
assert any("ccdi_staff_fmy_relation" in sql for sql in executed_sql)
assert any("annual_income" in sql for sql in executed_sql)

View File

@@ -8,8 +8,13 @@ from services.file_service import FileService
from services.statement_service import StatementService
from services.statement_rule_samples import (
DEFAULT_LARGE_TRANSACTION_THRESHOLDS,
build_fixed_counterparty_transfer_samples,
build_large_transaction_seed_statements,
build_low_income_relative_large_transaction_samples,
build_monthly_fixed_income_samples,
build_seed_statements_for_rule_plan,
build_special_amount_transaction_samples,
build_suspicious_income_keyword_samples,
)
@@ -409,3 +414,100 @@ def test_get_bank_statement_contains_large_transaction_hit_samples(monkeypatch):
assert any(amount > 50000001 for amount in income_amounts.values())
assert any(count >= 6 for count in cash_deposit_daily_counter.values())
assert has_large_transfer
def test_special_amount_transaction_samples_should_use_sql_supported_amounts():
statements = build_special_amount_transaction_samples(
group_id=1000,
log_id=20001,
)
assert statements
assert {item["transAmount"] for item in statements}.issubset({520.0, 1314.0})
def test_monthly_fixed_income_samples_should_cover_at_least_six_months():
statements = build_monthly_fixed_income_samples(
group_id=1000,
log_id=20001,
)
income_months = {item["trxDate"][:7] for item in statements}
assert len(income_months) >= 6
def test_suspicious_income_keyword_samples_should_hit_sql_keyword_set():
statements = build_suspicious_income_keyword_samples(
group_id=1000,
log_id=20001,
)
assert statements
assert any(
any(keyword in item["userMemo"] for keyword in ("工资", "分红", "红利", "奖金", "劳务费", "绩效"))
or any(keyword in item["cashType"] for keyword in ("工资", "劳务费", "代发"))
for item in statements
)
def test_fixed_counterparty_transfer_samples_should_use_staff_identity():
statements = build_fixed_counterparty_transfer_samples(
group_id=1000,
log_id=20001,
staff_id_card="320101199001010030",
family_id_cards=["320101199201010051", "320101199301010052"],
)
assert statements
assert {item["cretNo"] for item in statements} == {"320101199001010030"}
def test_low_income_relative_large_transaction_samples_should_exceed_total_threshold():
statements = build_low_income_relative_large_transaction_samples(
group_id=1000,
log_id=20001,
staff_id_card="320101199001010030",
family_id_cards=["320101199201010051", "320101199301010052"],
)
assert statements
assert len({item["cretNo"] for item in statements}) == 1
assert sum(item["crAmount"] + item["drAmount"] for item in statements) > 100000
def test_generate_statements_should_keep_all_mode_noise_as_safe_debits(monkeypatch):
file_service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
statement_service = StatementService(file_service=file_service)
monkeypatch.setattr("services.file_service.settings.RULE_HIT_MODE", "all")
response = file_service.fetch_inner_flow(
{
"groupId": 1001,
"customerNo": "customer_safe_noise",
"dataChannelCode": "test_code",
"requestDateId": 20240101,
"dataStartDateId": 20240101,
"dataEndDateId": 20240131,
"uploadUserId": 902001,
}
)
log_id = response["data"][0]
record = file_service.file_records[log_id]
record.large_transaction_hit_rules = []
record.phase1_hit_rules = []
record.phase2_statement_hit_rules = [
"MONTHLY_FIXED_INCOME",
"FIXED_COUNTERPARTY_TRANSFER",
]
statements = statement_service._generate_statements(group_id=1001, log_id=log_id, count=30)
noise_statements = [
item
for item in statements
if item["userMemo"] not in {"月度稳定兼职收入", "季度稳定兼职收入"}
]
assert noise_statements
assert all(item["drAmount"] > 0 and item["crAmount"] == 0 for item in noise_statements)