补充异常账户命中计划与账户事实

This commit is contained in:
wkc
2026-03-31 20:40:38 +08:00
parent 37e0c231a7
commit f0e2595a2b
2 changed files with 96 additions and 0 deletions

View File

@@ -48,6 +48,11 @@ PHASE2_BASELINE_RULE_CODES = [
"SUPPLIER_CONCENTRATION",
]
ABNORMAL_ACCOUNT_RULE_CODES = [
"SUDDEN_ACCOUNT_CLOSURE",
"DORMANT_ACCOUNT_LARGE_ACTIVATION",
]
MONTHLY_FIXED_INCOME_ISOLATED_LARGE_TRANSACTION_RULE_CODES = {
"SINGLE_LARGE_INCOME",
"CUMULATIVE_INCOME",
@@ -127,6 +132,8 @@ class FileRecord:
phase1_hit_rules: List[str] = field(default_factory=list)
phase2_statement_hit_rules: List[str] = field(default_factory=list)
phase2_baseline_hit_rules: List[str] = field(default_factory=list)
abnormal_account_hit_rules: List[str] = field(default_factory=list)
abnormal_accounts: List[dict] = field(default_factory=list)
class FileService:
@@ -213,6 +220,9 @@ class FileService:
"phase2_baseline_hit_rules": self._pick_rule_subset(
rng, PHASE2_BASELINE_RULE_CODES, 2, 4
),
"abnormal_account_hit_rules": self._pick_rule_subset(
rng, ABNORMAL_ACCOUNT_RULE_CODES, 1, len(ABNORMAL_ACCOUNT_RULE_CODES)
),
}
def _build_all_compatible_rule_hit_plan(self) -> dict:
@@ -222,6 +232,7 @@ class FileService:
"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),
"abnormal_account_hit_rules": list(ABNORMAL_ACCOUNT_RULE_CODES),
}
def _build_monthly_fixed_income_isolated_rule_hit_plan(self) -> dict:
@@ -284,6 +295,52 @@ class FileService:
file_record.phase2_baseline_hit_rules = list(
rule_hit_plan.get("phase2_baseline_hit_rules", [])
)
file_record.abnormal_account_hit_rules = list(
rule_hit_plan.get("abnormal_account_hit_rules", [])
)
file_record.abnormal_accounts = self._build_abnormal_accounts(
log_id=file_record.log_id,
staff_id_card=file_record.staff_id_card,
abnormal_account_hit_rules=file_record.abnormal_account_hit_rules,
)
def _build_abnormal_accounts(
self,
*,
log_id: int,
staff_id_card: str,
abnormal_account_hit_rules: List[str],
) -> List[dict]:
"""按命中规则生成最小异常账户事实。"""
if not abnormal_account_hit_rules:
return []
rng = random.Random(f"abnormal-account:{log_id}")
accounts = []
for index, rule_code in enumerate(abnormal_account_hit_rules, start=1):
account_no = f"622200{rng.randint(10**9, 10**10 - 1)}"
account_fact = {
"account_no": account_no,
"owner_id_card": staff_id_card,
"account_name": "测试员工工资卡",
"status": 1,
"effective_date": "2025-01-01",
"invalid_date": None,
"rule_code": rule_code,
}
if rule_code == "SUDDEN_ACCOUNT_CLOSURE":
account_fact["status"] = 2
account_fact["effective_date"] = "2024-01-01"
account_fact["invalid_date"] = "2026-03-20"
elif rule_code == "DORMANT_ACCOUNT_LARGE_ACTIVATION":
account_fact["status"] = 1
account_fact["effective_date"] = "2025-01-01"
account_fact["invalid_date"] = None
account_fact["account_no"] = f"{account_no[:-2]}{index:02d}"
accounts.append(account_fact)
return accounts
def _rebalance_all_mode_group_rule_plans(self, group_id: int) -> None:
"""同项目存在多文件时,隔离月固定收入样本,避免被其他正向流入规则污染。"""
@@ -332,6 +389,8 @@ class FileService:
phase1_hit_rules: List[str] = None,
phase2_statement_hit_rules: List[str] = None,
phase2_baseline_hit_rules: List[str] = None,
abnormal_account_hit_rules: List[str] = None,
abnormal_accounts: List[dict] = None,
parsing: bool = True,
status: int = -5,
) -> FileRecord:
@@ -366,6 +425,8 @@ class FileService:
phase1_hit_rules=list(phase1_hit_rules or []),
phase2_statement_hit_rules=list(phase2_statement_hit_rules or []),
phase2_baseline_hit_rules=list(phase2_baseline_hit_rules or []),
abnormal_account_hit_rules=list(abnormal_account_hit_rules or []),
abnormal_accounts=[dict(account) for account in (abnormal_accounts or [])],
parsing=parsing,
status=status,
)
@@ -444,6 +505,12 @@ class FileService:
phase1_hit_rules=rule_hit_plan.get("phase1_hit_rules", []),
phase2_statement_hit_rules=rule_hit_plan.get("phase2_statement_hit_rules", []),
phase2_baseline_hit_rules=rule_hit_plan.get("phase2_baseline_hit_rules", []),
abnormal_account_hit_rules=rule_hit_plan.get("abnormal_account_hit_rules", []),
abnormal_accounts=self._build_abnormal_accounts(
log_id=log_id,
staff_id_card=identity_scope["staff_id_card"],
abnormal_account_hit_rules=rule_hit_plan.get("abnormal_account_hit_rules", []),
),
)
# 存储记录
@@ -775,6 +842,12 @@ class FileService:
phase1_hit_rules=rule_hit_plan.get("phase1_hit_rules", []),
phase2_statement_hit_rules=rule_hit_plan.get("phase2_statement_hit_rules", []),
phase2_baseline_hit_rules=rule_hit_plan.get("phase2_baseline_hit_rules", []),
abnormal_account_hit_rules=rule_hit_plan.get("abnormal_account_hit_rules", []),
abnormal_accounts=self._build_abnormal_accounts(
log_id=log_id,
staff_id_card=identity_scope["staff_id_card"],
abnormal_account_hit_rules=rule_hit_plan.get("abnormal_account_hit_rules", []),
),
parsing=False,
)

View File

@@ -163,6 +163,29 @@ def test_fetch_inner_flow_persists_primary_binding_record(monkeypatch):
assert record.total_records == 200
def test_fetch_inner_flow_should_attach_abnormal_account_rule_plan():
service = FileService(staff_identity_repository=FakeStaffIdentityRepository())
response = service.fetch_inner_flow(
{
"groupId": 1001,
"customerNo": "customer_abnormal_account",
"dataChannelCode": "test_code",
"requestDateId": 20240101,
"dataStartDateId": 20240101,
"dataEndDateId": 20240131,
"uploadUserId": 902001,
}
)
log_id = response["data"][0]
record = service.file_records[log_id]
assert hasattr(record, "abnormal_account_hit_rules")
assert hasattr(record, "abnormal_accounts")
assert isinstance(record.abnormal_account_hit_rules, list)
assert isinstance(record.abnormal_accounts, list)
def test_generate_log_id_should_retry_when_random_value_conflicts(monkeypatch):
"""随机 logId 命中已存在记录时必须重试并返回未占用值。"""
service = FileService(staff_identity_repository=FakeStaffIdentityRepository())