diff --git a/lsfx-mock-server/services/statement_service.py b/lsfx-mock-server/services/statement_service.py index e5aa1496..45eef399 100644 --- a/lsfx-mock-server/services/statement_service.py +++ b/lsfx-mock-server/services/statement_service.py @@ -5,7 +5,7 @@ import uuid from datetime import datetime, timedelta from services.statement_rule_samples import ( - build_large_transaction_seed_statements, + build_seed_statements_for_rule_plan, resolve_identity_cards, ) @@ -144,14 +144,21 @@ class StatementService: """生成指定数量的流水记录。""" primary_enterprise_name, primary_account_no = self._resolve_primary_binding(log_id) record = self.file_service.get_file_record(log_id) if self.file_service is not None else None + rule_plan = { + "large_transaction_hit_rules": ( + list(record.large_transaction_hit_rules) if record is not None else [] + ), + "phase1_hit_rules": list(record.phase1_hit_rules) if record is not None else [], + } if record is not None and record.staff_id_card: allowed_identity_cards = tuple([record.staff_id_card, *record.family_id_cards]) else: allowed_identity_cards = resolve_identity_cards(log_id) rng = random.Random(f"statement:{log_id}") - seeded_statements = build_large_transaction_seed_statements( + seeded_statements = build_seed_statements_for_rule_plan( group_id=group_id, log_id=log_id, + rule_plan=rule_plan, primary_enterprise_name=primary_enterprise_name, primary_account_no=primary_account_no, staff_id_card=record.staff_id_card if record is not None else None, diff --git a/lsfx-mock-server/tests/integration/test_full_workflow.py b/lsfx-mock-server/tests/integration/test_full_workflow.py index dc3774a6..7bafc204 100644 --- a/lsfx-mock-server/tests/integration/test_full_workflow.py +++ b/lsfx-mock-server/tests/integration/test_full_workflow.py @@ -170,3 +170,30 @@ def test_upload_status_and_bank_statement_share_same_primary_binding(client, mon assert statements assert all(item["leName"] == status_log["enterpriseNameList"][0] for item in statements) assert all(item["accountMaskNo"] == status_log["accountNoList"][0] for item in statements) + + +def test_inner_flow_bank_statement_should_keep_same_rule_subset(client): + fetch_response = client.post( + "/watson/api/project/getJZFileOrZjrcuFile", + data={ + "groupId": 1001, + "customerNo": "customer_subset", + "dataChannelCode": "test_code", + "requestDateId": 20240101, + "dataStartDateId": 20240101, + "dataEndDateId": 20240131, + "uploadUserId": 902001, + }, + ) + log_id = fetch_response.json()["data"][0] + + page1 = client.post( + "/watson/api/project/getBSByLogId", + data={"groupId": 1001, "logId": log_id, "pageNow": 1, "pageSize": 10}, + ).json() + page2 = client.post( + "/watson/api/project/getBSByLogId", + data={"groupId": 1001, "logId": log_id, "pageNow": 1, "pageSize": 10}, + ).json() + + assert page1["data"]["bankStatementList"] == page2["data"]["bankStatementList"] diff --git a/lsfx-mock-server/tests/test_statement_service.py b/lsfx-mock-server/tests/test_statement_service.py index a61d4ec3..01224378 100644 --- a/lsfx-mock-server/tests/test_statement_service.py +++ b/lsfx-mock-server/tests/test_statement_service.py @@ -22,11 +22,28 @@ class FakeStaffIdentityRepository: } -def test_generate_statements_should_include_seeded_samples_before_noise(): - """生成流水时必须先混入固定命中样本,而不是纯随机噪声。""" - service = StatementService() +def test_generate_statements_should_include_seeded_samples_before_noise_when_rule_plan_exists(): + """存在规则命中计划时,生成流水必须先混入被选中的命中样本。""" + file_service = FileService(staff_identity_repository=FakeStaffIdentityRepository()) + service = StatementService(file_service=file_service) - statements = service._generate_statements(group_id=1000, log_id=20001, count=30) + response = file_service.fetch_inner_flow( + { + "groupId": 1001, + "customerNo": "customer_seeded_samples", + "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 = ["HOUSE_OR_CAR_EXPENSE"] + record.phase1_hit_rules = [] + + statements = service._generate_statements(group_id=1000, log_id=log_id, count=30) assert len(statements) >= 30 assert any(item["userMemo"] == "购买房产首付款" for item in statements) @@ -137,6 +154,33 @@ def test_generate_statements_should_fill_noise_up_to_requested_count(): assert len(statements) == 80 +def test_generate_statements_should_follow_rule_hit_plan_from_file_record(): + file_service = FileService(staff_identity_repository=FakeStaffIdentityRepository()) + statement_service = StatementService(file_service=file_service) + + response = file_service.fetch_inner_flow( + { + "groupId": 1001, + "customerNo": "customer_rule_plan", + "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 = ["HOUSE_OR_CAR_EXPENSE"] + record.phase1_hit_rules = ["GAMBLING_SENSITIVE_KEYWORD"] + + statements = statement_service._generate_statements(group_id=1001, log_id=log_id, count=200) + + assert any("房产首付款" in item["userMemo"] for item in statements) + assert any("游戏" in item["userMemo"] for item in statements) + assert not any("购汇" in item["userMemo"] for item in statements) + + def test_generate_statements_should_stay_within_single_employee_scope_per_log_id(): """同一 logId 的流水只能落在 FileRecord 绑定的员工及亲属身份证内。""" file_service = FileService(staff_identity_repository=FakeStaffIdentityRepository()) @@ -267,6 +311,14 @@ def test_get_bank_statement_contains_large_transaction_hit_samples(monkeypatch): ) log_id = response["data"][0] record = file_service.file_records[log_id] + record.large_transaction_hit_rules = [ + "HOUSE_OR_CAR_EXPENSE", + "TAX_EXPENSE", + "CUMULATIVE_INCOME", + "FREQUENT_CASH_DEPOSIT", + "LARGE_TRANSFER", + ] + record.phase1_hit_rules = [] staff_id_card = record.staff_id_card family_id_card = record.family_id_cards[0]