补充Mock全部兼容规则命中计划
This commit is contained in:
@@ -48,6 +48,8 @@ PHASE2_BASELINE_RULE_CODES = [
|
|||||||
"SUPPLIER_CONCENTRATION",
|
"SUPPLIER_CONCENTRATION",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
RULE_CONFLICT_GROUPS = []
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FileRecord:
|
class FileRecord:
|
||||||
@@ -179,8 +181,8 @@ class FileService:
|
|||||||
selected_codes = set(rng.sample(rule_codes, rng.randint(min_count, max_count)))
|
selected_codes = set(rng.sample(rule_codes, rng.randint(min_count, max_count)))
|
||||||
return [rule_code for rule_code in rule_codes if rule_code in selected_codes]
|
return [rule_code for rule_code in rule_codes if rule_code in selected_codes]
|
||||||
|
|
||||||
def _build_rule_hit_plan(self, log_id: int) -> dict:
|
def _build_subset_rule_hit_plan(self, log_id: int) -> dict:
|
||||||
"""基于 logId 生成稳定的规则命中计划。"""
|
"""基于 logId 生成稳定的规则子集命中计划。"""
|
||||||
rng = random.Random(f"rule-plan:{log_id}")
|
rng = random.Random(f"rule-plan:{log_id}")
|
||||||
return {
|
return {
|
||||||
"large_transaction_hit_rules": self._pick_rule_subset(
|
"large_transaction_hit_rules": self._pick_rule_subset(
|
||||||
@@ -195,6 +197,41 @@ class FileService:
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _build_all_compatible_rule_hit_plan(self) -> dict:
|
||||||
|
"""生成全部兼容规则命中计划。"""
|
||||||
|
return {
|
||||||
|
"large_transaction_hit_rules": list(LARGE_TRANSACTION_RULE_CODES),
|
||||||
|
"phase1_hit_rules": list(PHASE1_RULE_CODES),
|
||||||
|
"phase2_statement_hit_rules": list(PHASE2_STATEMENT_RULE_CODES),
|
||||||
|
"phase2_baseline_hit_rules": list(PHASE2_BASELINE_RULE_CODES),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _apply_conflict_groups(self, rule_plan: dict) -> dict:
|
||||||
|
"""按显式互斥组裁剪规则计划,同组仅保留固定优先级的首个规则。"""
|
||||||
|
resolved_plan = {plan_key: list(rule_codes) for plan_key, rule_codes in rule_plan.items()}
|
||||||
|
for plan_key, rule_codes in resolved_plan.items():
|
||||||
|
filtered_codes = list(rule_codes)
|
||||||
|
for conflict_group in RULE_CONFLICT_GROUPS:
|
||||||
|
kept_rule_code = next(
|
||||||
|
(rule_code for rule_code in conflict_group if rule_code in filtered_codes),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if kept_rule_code is None:
|
||||||
|
continue
|
||||||
|
filtered_codes = [
|
||||||
|
rule_code
|
||||||
|
for rule_code in filtered_codes
|
||||||
|
if rule_code == kept_rule_code or rule_code not in conflict_group
|
||||||
|
]
|
||||||
|
resolved_plan[plan_key] = filtered_codes
|
||||||
|
return resolved_plan
|
||||||
|
|
||||||
|
def _build_rule_hit_plan(self, log_id: int) -> dict:
|
||||||
|
"""按配置模式生成规则命中计划。"""
|
||||||
|
if settings.RULE_HIT_MODE == "all":
|
||||||
|
return self._apply_conflict_groups(self._build_all_compatible_rule_hit_plan())
|
||||||
|
return self._build_subset_rule_hit_plan(log_id)
|
||||||
|
|
||||||
def _create_file_record(
|
def _create_file_record(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -8,7 +8,14 @@ import io
|
|||||||
from fastapi import BackgroundTasks
|
from fastapi import BackgroundTasks
|
||||||
from fastapi.datastructures import UploadFile
|
from fastapi.datastructures import UploadFile
|
||||||
|
|
||||||
from services.file_service import FileRecord, FileService
|
from services.file_service import (
|
||||||
|
LARGE_TRANSACTION_RULE_CODES,
|
||||||
|
PHASE1_RULE_CODES,
|
||||||
|
PHASE2_BASELINE_RULE_CODES,
|
||||||
|
PHASE2_STATEMENT_RULE_CODES,
|
||||||
|
FileRecord,
|
||||||
|
FileService,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FakeStaffIdentityRepository:
|
class FakeStaffIdentityRepository:
|
||||||
@@ -196,6 +203,44 @@ def test_phase2_rule_hit_plan_should_be_deterministic_for_same_log_id():
|
|||||||
assert 2 <= len(plan1["phase2_baseline_hit_rules"]) <= 4
|
assert 2 <= len(plan1["phase2_baseline_hit_rules"]) <= 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_rule_hit_plan_should_return_all_compatible_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 plan["large_transaction_hit_rules"] == LARGE_TRANSACTION_RULE_CODES
|
||||||
|
assert plan["phase1_hit_rules"] == PHASE1_RULE_CODES
|
||||||
|
assert plan["phase2_statement_hit_rules"] == PHASE2_STATEMENT_RULE_CODES
|
||||||
|
assert plan["phase2_baseline_hit_rules"] == PHASE2_BASELINE_RULE_CODES
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_rule_hit_plan_should_keep_subset_mode_as_default():
|
||||||
|
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["large_transaction_hit_rules"]) <= 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_rule_hit_plan_should_drop_conflicting_rules_from_all_mode(monkeypatch):
|
||||||
|
monkeypatch.setattr("services.file_service.settings.RULE_HIT_MODE", "all")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"services.file_service.RULE_CONFLICT_GROUPS",
|
||||||
|
[["SALARY_QUICK_TRANSFER", "SALARY_UNUSED"]],
|
||||||
|
)
|
||||||
|
service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
|
||||||
|
|
||||||
|
plan = service._build_rule_hit_plan(10001)
|
||||||
|
|
||||||
|
assert not (
|
||||||
|
"SALARY_QUICK_TRANSFER" in plan["phase2_statement_hit_rules"]
|
||||||
|
and "SALARY_UNUSED" in plan["phase2_statement_hit_rules"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_inner_flow_should_persist_rule_hit_plan(monkeypatch):
|
def test_fetch_inner_flow_should_persist_rule_hit_plan(monkeypatch):
|
||||||
service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
|
service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
|
|||||||
Reference in New Issue
Block a user