持久化Mock随机命中规则计划
This commit is contained in:
@@ -9,6 +9,28 @@ from datetime import datetime, timedelta
|
|||||||
import random
|
import random
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
LARGE_TRANSACTION_RULE_CODES = [
|
||||||
|
"HOUSE_OR_CAR_EXPENSE",
|
||||||
|
"TAX_EXPENSE",
|
||||||
|
"SINGLE_LARGE_INCOME",
|
||||||
|
"CUMULATIVE_INCOME",
|
||||||
|
"ANNUAL_TURNOVER",
|
||||||
|
"LARGE_CASH_DEPOSIT",
|
||||||
|
"FREQUENT_CASH_DEPOSIT",
|
||||||
|
"LARGE_TRANSFER",
|
||||||
|
]
|
||||||
|
|
||||||
|
PHASE1_RULE_CODES = [
|
||||||
|
"GAMBLING_SENSITIVE_KEYWORD",
|
||||||
|
"SPECIAL_AMOUNT_TRANSACTION",
|
||||||
|
"SUSPICIOUS_INCOME_KEYWORD",
|
||||||
|
"FOREX_BUY_AMT",
|
||||||
|
"FOREX_SELL_AMT",
|
||||||
|
"STOCK_TFR_LARGE",
|
||||||
|
"LARGE_STOCK_TRADING",
|
||||||
|
"WITHDRAW_CNT",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FileRecord:
|
class FileRecord:
|
||||||
@@ -64,6 +86,8 @@ class FileRecord:
|
|||||||
staff_name: str = ""
|
staff_name: str = ""
|
||||||
staff_id_card: str = ""
|
staff_id_card: str = ""
|
||||||
family_id_cards: List[str] = field(default_factory=list)
|
family_id_cards: List[str] = field(default_factory=list)
|
||||||
|
large_transaction_hit_rules: List[str] = field(default_factory=list)
|
||||||
|
phase1_hit_rules: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class FileService:
|
class FileService:
|
||||||
@@ -110,6 +134,27 @@ class FileService:
|
|||||||
"enterpriseNameList": [primary_enterprise_name],
|
"enterpriseNameList": [primary_enterprise_name],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _pick_rule_subset(
|
||||||
|
self,
|
||||||
|
rng: random.Random,
|
||||||
|
rule_codes: List[str],
|
||||||
|
min_count: int,
|
||||||
|
max_count: int,
|
||||||
|
) -> List[str]:
|
||||||
|
"""按固定随机源选择稳定规则子集,并保留规则池原始顺序。"""
|
||||||
|
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]
|
||||||
|
|
||||||
|
def _build_rule_hit_plan(self, log_id: int) -> dict:
|
||||||
|
"""基于 logId 生成稳定的规则命中计划。"""
|
||||||
|
rng = random.Random(f"rule-plan:{log_id}")
|
||||||
|
return {
|
||||||
|
"large_transaction_hit_rules": self._pick_rule_subset(
|
||||||
|
rng, LARGE_TRANSACTION_RULE_CODES, 2, 4
|
||||||
|
),
|
||||||
|
"phase1_hit_rules": self._pick_rule_subset(rng, PHASE1_RULE_CODES, 2, 4),
|
||||||
|
}
|
||||||
|
|
||||||
def _create_file_record(
|
def _create_file_record(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -130,6 +175,8 @@ class FileService:
|
|||||||
staff_name: str = "",
|
staff_name: str = "",
|
||||||
staff_id_card: str = "",
|
staff_id_card: str = "",
|
||||||
family_id_cards: List[str] = None,
|
family_id_cards: List[str] = None,
|
||||||
|
large_transaction_hit_rules: List[str] = None,
|
||||||
|
phase1_hit_rules: List[str] = None,
|
||||||
parsing: bool = True,
|
parsing: bool = True,
|
||||||
status: int = -5,
|
status: int = -5,
|
||||||
) -> FileRecord:
|
) -> FileRecord:
|
||||||
@@ -160,6 +207,8 @@ class FileService:
|
|||||||
staff_name=staff_name,
|
staff_name=staff_name,
|
||||||
staff_id_card=staff_id_card,
|
staff_id_card=staff_id_card,
|
||||||
family_id_cards=list(family_id_cards or []),
|
family_id_cards=list(family_id_cards or []),
|
||||||
|
large_transaction_hit_rules=list(large_transaction_hit_rules or []),
|
||||||
|
phase1_hit_rules=list(phase1_hit_rules or []),
|
||||||
parsing=parsing,
|
parsing=parsing,
|
||||||
status=status,
|
status=status,
|
||||||
)
|
)
|
||||||
@@ -187,6 +236,7 @@ class FileService:
|
|||||||
|
|
||||||
# 推断银行信息
|
# 推断银行信息
|
||||||
bank_name, template_name = self._infer_bank_name(file.filename)
|
bank_name, template_name = self._infer_bank_name(file.filename)
|
||||||
|
rule_hit_plan = self._build_rule_hit_plan(log_id)
|
||||||
|
|
||||||
# 生成合理的交易日期范围
|
# 生成合理的交易日期范围
|
||||||
end_date = datetime.now()
|
end_date = datetime.now()
|
||||||
@@ -217,6 +267,8 @@ class FileService:
|
|||||||
staff_name=identity_scope["staff_name"],
|
staff_name=identity_scope["staff_name"],
|
||||||
staff_id_card=identity_scope["staff_id_card"],
|
staff_id_card=identity_scope["staff_id_card"],
|
||||||
family_id_cards=identity_scope["family_id_cards"],
|
family_id_cards=identity_scope["family_id_cards"],
|
||||||
|
large_transaction_hit_rules=rule_hit_plan["large_transaction_hit_rules"],
|
||||||
|
phase1_hit_rules=rule_hit_plan["phase1_hit_rules"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# 存储记录
|
# 存储记录
|
||||||
@@ -521,6 +573,7 @@ class FileService:
|
|||||||
# 使用递增 logId,确保与上传链路一致
|
# 使用递增 logId,确保与上传链路一致
|
||||||
self.log_counter += 1
|
self.log_counter += 1
|
||||||
log_id = self.log_counter
|
log_id = self.log_counter
|
||||||
|
rule_hit_plan = self._build_rule_hit_plan(log_id)
|
||||||
|
|
||||||
primary_enterprise_name, primary_account_no = self._generate_primary_binding()
|
primary_enterprise_name, primary_account_no = self._generate_primary_binding()
|
||||||
identity_scope = self._select_staff_identity_scope()
|
identity_scope = self._select_staff_identity_scope()
|
||||||
@@ -542,6 +595,8 @@ class FileService:
|
|||||||
staff_name=identity_scope["staff_name"],
|
staff_name=identity_scope["staff_name"],
|
||||||
staff_id_card=identity_scope["staff_id_card"],
|
staff_id_card=identity_scope["staff_id_card"],
|
||||||
family_id_cards=identity_scope["family_id_cards"],
|
family_id_cards=identity_scope["family_id_cards"],
|
||||||
|
large_transaction_hit_rules=rule_hit_plan["large_transaction_hit_rules"],
|
||||||
|
phase1_hit_rules=rule_hit_plan["phase1_hit_rules"],
|
||||||
parsing=False,
|
parsing=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -154,3 +154,49 @@ def test_fetch_inner_flow_persists_primary_binding_record(monkeypatch):
|
|||||||
assert record.enterprise_name_list == ["行内主体"]
|
assert record.enterprise_name_list == ["行内主体"]
|
||||||
assert record.account_no_list == ["6210987654321098"]
|
assert record.account_no_list == ["6210987654321098"]
|
||||||
assert record.total_records == 200
|
assert record.total_records == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_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["large_transaction_hit_rules"]) <= 4
|
||||||
|
assert 2 <= len(plan1["phase1_hit_rules"]) <= 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_inner_flow_should_persist_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"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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.large_transaction_hit_rules == [
|
||||||
|
"HOUSE_OR_CAR_EXPENSE",
|
||||||
|
"TAX_EXPENSE",
|
||||||
|
]
|
||||||
|
assert record.phase1_hit_rules == [
|
||||||
|
"GAMBLING_SENSITIVE_KEYWORD",
|
||||||
|
"FOREX_BUY_AMT",
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user