2026-03-18 16:39:09 +08:00
|
|
|
|
from typing import Dict, List, Union
|
|
|
|
|
|
import logging
|
2026-03-13 16:38:07 +08:00
|
|
|
|
import random
|
|
|
|
|
|
import uuid
|
2026-03-18 16:39:09 +08:00
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
|
|
|
|
|
|
from services.statement_rule_samples import (
|
2026-03-20 14:48:02 +08:00
|
|
|
|
build_seed_statements_for_rule_plan,
|
2026-03-19 16:07:28 +08:00
|
|
|
|
resolve_identity_cards,
|
2026-03-18 16:39:09 +08:00
|
|
|
|
)
|
2026-03-13 16:38:07 +08:00
|
|
|
|
|
|
|
|
|
|
# 配置日志
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
2026-03-13 15:13:18 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class StatementService:
|
|
|
|
|
|
"""流水数据服务"""
|
|
|
|
|
|
|
2026-03-19 17:18:02 +08:00
|
|
|
|
FIXED_TOTAL_COUNT = 200
|
|
|
|
|
|
|
2026-03-18 15:54:11 +08:00
|
|
|
|
def __init__(self, file_service=None):
|
2026-03-13 16:38:07 +08:00
|
|
|
|
# 缓存:logId -> (statements_list, total_count)
|
|
|
|
|
|
self._cache: Dict[int, tuple] = {}
|
2026-03-18 15:54:11 +08:00
|
|
|
|
self.file_service = file_service
|
2026-03-18 16:39:09 +08:00
|
|
|
|
logger.info("StatementService initialized with empty cache")
|
2026-03-13 16:38:07 +08:00
|
|
|
|
|
2026-03-18 15:54:11 +08:00
|
|
|
|
def _resolve_primary_binding(self, log_id: int) -> tuple:
|
2026-03-18 16:39:09 +08:00
|
|
|
|
"""优先从 FileService 读取真实主绑定,不存在时再走 deterministic fallback。"""
|
2026-03-18 15:54:11 +08:00
|
|
|
|
if self.file_service is not None:
|
|
|
|
|
|
record = self.file_service.get_file_record(log_id)
|
|
|
|
|
|
if record is not None:
|
|
|
|
|
|
return record.primary_enterprise_name, record.primary_account_no
|
|
|
|
|
|
|
2026-03-18 16:39:09 +08:00
|
|
|
|
rng = random.Random(f"binding:{log_id}")
|
|
|
|
|
|
return "张传伟", f"{rng.randint(100000000000000, 999999999999999)}"
|
2026-03-18 15:54:11 +08:00
|
|
|
|
|
|
|
|
|
|
def _generate_random_statement(
|
|
|
|
|
|
self,
|
|
|
|
|
|
group_id: int,
|
|
|
|
|
|
log_id: int,
|
|
|
|
|
|
primary_enterprise_name: str,
|
|
|
|
|
|
primary_account_no: str,
|
2026-03-19 16:07:28 +08:00
|
|
|
|
allowed_identity_cards: tuple,
|
2026-03-18 16:39:09 +08:00
|
|
|
|
rng: random.Random,
|
2026-03-18 15:54:11 +08:00
|
|
|
|
) -> Dict:
|
2026-03-18 16:39:09 +08:00
|
|
|
|
"""生成单条随机噪声流水记录。"""
|
|
|
|
|
|
reference_now = datetime(2026, 3, 18, 9, 0, 0)
|
|
|
|
|
|
days_ago = rng.randint(0, 365)
|
|
|
|
|
|
trx_datetime = reference_now - timedelta(days=days_ago, minutes=rng.randint(0, 1439))
|
|
|
|
|
|
trans_amount = round(rng.uniform(10, 10000), 2)
|
|
|
|
|
|
|
|
|
|
|
|
if rng.random() > 0.5:
|
2026-03-13 16:38:07 +08:00
|
|
|
|
dr_amount = trans_amount
|
2026-03-18 16:39:09 +08:00
|
|
|
|
cr_amount = 0.0
|
2026-03-13 16:38:07 +08:00
|
|
|
|
trans_flag = "P"
|
|
|
|
|
|
else:
|
|
|
|
|
|
cr_amount = trans_amount
|
2026-03-18 16:39:09 +08:00
|
|
|
|
dr_amount = 0.0
|
2026-03-13 16:38:07 +08:00
|
|
|
|
trans_flag = "R"
|
|
|
|
|
|
|
2026-03-18 16:39:09 +08:00
|
|
|
|
customer_name = rng.choice(
|
|
|
|
|
|
["小店", "支付宝", "微信支付", "财付通", "美团", "京东", "淘宝", "银行转账"]
|
|
|
|
|
|
)
|
|
|
|
|
|
user_memo = rng.choice(
|
|
|
|
|
|
[
|
|
|
|
|
|
f"消费_{customer_name}",
|
|
|
|
|
|
f"转账_{customer_name}",
|
|
|
|
|
|
f"收款_{customer_name}",
|
|
|
|
|
|
f"支付_{customer_name}",
|
|
|
|
|
|
f"退款_{customer_name}",
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
2026-03-13 16:38:07 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"accountId": 0,
|
2026-03-18 15:54:11 +08:00
|
|
|
|
"accountMaskNo": primary_account_no,
|
2026-03-18 16:39:09 +08:00
|
|
|
|
"accountingDate": trx_datetime.strftime("%Y-%m-%d"),
|
|
|
|
|
|
"accountingDateId": int(trx_datetime.strftime("%Y%m%d")),
|
2026-03-13 16:38:07 +08:00
|
|
|
|
"archivingFlag": 0,
|
|
|
|
|
|
"attachments": 0,
|
2026-03-18 16:39:09 +08:00
|
|
|
|
"balanceAmount": round(rng.uniform(1000, 50000), 2),
|
2026-03-13 16:38:07 +08:00
|
|
|
|
"bank": "ZJRCU",
|
|
|
|
|
|
"bankComments": "",
|
2026-03-18 16:39:09 +08:00
|
|
|
|
"bankStatementId": 0,
|
|
|
|
|
|
"bankTrxNumber": "",
|
2026-03-13 16:38:07 +08:00
|
|
|
|
"batchId": log_id,
|
|
|
|
|
|
"cashType": "1",
|
|
|
|
|
|
"commentsNum": 0,
|
|
|
|
|
|
"crAmount": cr_amount,
|
2026-03-18 16:39:09 +08:00
|
|
|
|
"createDate": reference_now.strftime("%Y-%m-%d %H:%M:%S"),
|
2026-03-13 16:38:07 +08:00
|
|
|
|
"createdBy": "902001",
|
2026-03-19 16:07:28 +08:00
|
|
|
|
"cretNo": rng.choice(allowed_identity_cards),
|
2026-03-13 16:38:07 +08:00
|
|
|
|
"currency": "CNY",
|
2026-03-18 16:39:09 +08:00
|
|
|
|
"customerAccountMaskNo": str(rng.randint(100000000, 999999999)),
|
2026-03-13 16:38:07 +08:00
|
|
|
|
"customerBank": "",
|
|
|
|
|
|
"customerId": -1,
|
|
|
|
|
|
"customerName": customer_name,
|
|
|
|
|
|
"customerReference": "",
|
|
|
|
|
|
"downPaymentFlag": 0,
|
|
|
|
|
|
"drAmount": dr_amount,
|
|
|
|
|
|
"exceptionType": "",
|
|
|
|
|
|
"groupId": group_id,
|
|
|
|
|
|
"internalFlag": 0,
|
|
|
|
|
|
"leId": 16308,
|
2026-03-18 15:54:11 +08:00
|
|
|
|
"leName": primary_enterprise_name,
|
2026-03-13 16:38:07 +08:00
|
|
|
|
"overrideBsId": 0,
|
|
|
|
|
|
"paymentMethod": "",
|
|
|
|
|
|
"sourceCatalogId": 0,
|
|
|
|
|
|
"split": 0,
|
|
|
|
|
|
"subBankstatementId": 0,
|
|
|
|
|
|
"toDoFlag": 0,
|
|
|
|
|
|
"transAmount": trans_amount,
|
|
|
|
|
|
"transFlag": trans_flag,
|
|
|
|
|
|
"transTypeId": 0,
|
|
|
|
|
|
"transformAmount": 0,
|
|
|
|
|
|
"transformCrAmount": 0,
|
|
|
|
|
|
"transformDrAmount": 0,
|
|
|
|
|
|
"transfromBalanceAmount": 0,
|
|
|
|
|
|
"trxBalance": 0,
|
2026-03-18 16:39:09 +08:00
|
|
|
|
"trxDate": trx_datetime.strftime("%Y-%m-%d %H:%M:%S"),
|
|
|
|
|
|
"uploadSequnceNumber": 0,
|
|
|
|
|
|
"userMemo": user_memo,
|
2026-03-13 16:38:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 16:39:09 +08:00
|
|
|
|
def _assign_statement_ids(self, statements: List[Dict], group_id: int, log_id: int) -> List[Dict]:
|
|
|
|
|
|
"""为样本与噪声流水统一补齐稳定的流水标识。"""
|
|
|
|
|
|
assigned: List[Dict] = []
|
|
|
|
|
|
base_id = log_id * 100000
|
|
|
|
|
|
for index, statement in enumerate(statements, start=1):
|
|
|
|
|
|
item = dict(statement)
|
|
|
|
|
|
item["groupId"] = group_id
|
|
|
|
|
|
item["batchId"] = log_id
|
|
|
|
|
|
item["bankStatementId"] = base_id + index
|
|
|
|
|
|
item["bankTrxNumber"] = uuid.uuid5(
|
|
|
|
|
|
uuid.NAMESPACE_DNS, f"lsfx-mock-{log_id}-{index}"
|
|
|
|
|
|
).hex
|
|
|
|
|
|
item["uploadSequnceNumber"] = index
|
|
|
|
|
|
item["transAmount"] = round(item.get("drAmount", 0) + item.get("crAmount", 0), 2)
|
|
|
|
|
|
assigned.append(item)
|
|
|
|
|
|
return assigned
|
2026-03-13 16:38:07 +08:00
|
|
|
|
|
|
|
|
|
|
def _generate_statements(self, group_id: int, log_id: int, count: int) -> List[Dict]:
|
2026-03-18 16:39:09 +08:00
|
|
|
|
"""生成指定数量的流水记录。"""
|
2026-03-18 15:54:11 +08:00
|
|
|
|
primary_enterprise_name, primary_account_no = self._resolve_primary_binding(log_id)
|
2026-03-19 16:07:28 +08:00
|
|
|
|
record = self.file_service.get_file_record(log_id) if self.file_service is not None else None
|
2026-03-20 14:48:02 +08:00
|
|
|
|
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 [],
|
|
|
|
|
|
}
|
2026-03-19 16:07:28 +08:00
|
|
|
|
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)
|
2026-03-18 16:39:09 +08:00
|
|
|
|
rng = random.Random(f"statement:{log_id}")
|
2026-03-20 14:48:02 +08:00
|
|
|
|
seeded_statements = build_seed_statements_for_rule_plan(
|
2026-03-18 16:39:09 +08:00
|
|
|
|
group_id=group_id,
|
|
|
|
|
|
log_id=log_id,
|
2026-03-20 14:48:02 +08:00
|
|
|
|
rule_plan=rule_plan,
|
2026-03-18 16:39:09 +08:00
|
|
|
|
primary_enterprise_name=primary_enterprise_name,
|
|
|
|
|
|
primary_account_no=primary_account_no,
|
2026-03-19 16:07:28 +08:00
|
|
|
|
staff_id_card=record.staff_id_card if record is not None else None,
|
|
|
|
|
|
family_id_cards=record.family_id_cards if record is not None else None,
|
2026-03-18 16:39:09 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
total_count = max(count, len(seeded_statements))
|
|
|
|
|
|
statements = list(seeded_statements)
|
|
|
|
|
|
for _ in range(total_count - len(seeded_statements)):
|
2026-03-18 15:54:11 +08:00
|
|
|
|
statements.append(
|
|
|
|
|
|
self._generate_random_statement(
|
|
|
|
|
|
group_id,
|
|
|
|
|
|
log_id,
|
|
|
|
|
|
primary_enterprise_name,
|
|
|
|
|
|
primary_account_no,
|
2026-03-19 16:07:28 +08:00
|
|
|
|
allowed_identity_cards,
|
2026-03-18 16:39:09 +08:00
|
|
|
|
rng,
|
2026-03-18 15:54:11 +08:00
|
|
|
|
)
|
|
|
|
|
|
)
|
2026-03-18 16:39:09 +08:00
|
|
|
|
|
|
|
|
|
|
statements = self._assign_statement_ids(statements, group_id, log_id)
|
|
|
|
|
|
rng.shuffle(statements)
|
2026-03-13 16:38:07 +08:00
|
|
|
|
return statements
|
|
|
|
|
|
|
2026-03-18 15:54:11 +08:00
|
|
|
|
def _apply_primary_binding(
|
|
|
|
|
|
self,
|
|
|
|
|
|
statements: List[Dict],
|
|
|
|
|
|
primary_enterprise_name: str,
|
|
|
|
|
|
primary_account_no: str,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""将解析出的主绑定统一回填到已有流水记录。"""
|
|
|
|
|
|
for statement in statements:
|
|
|
|
|
|
statement["leName"] = primary_enterprise_name
|
|
|
|
|
|
statement["accountMaskNo"] = primary_account_no
|
|
|
|
|
|
|
2026-03-13 15:13:18 +08:00
|
|
|
|
def get_bank_statement(self, request: Union[Dict, object]) -> Dict:
|
2026-03-18 16:39:09 +08:00
|
|
|
|
"""获取银行流水列表。"""
|
2026-03-13 15:13:18 +08:00
|
|
|
|
if isinstance(request, dict):
|
2026-03-13 16:38:07 +08:00
|
|
|
|
group_id = request.get("groupId", 1000)
|
|
|
|
|
|
log_id = request.get("logId", 10000)
|
2026-03-13 15:13:18 +08:00
|
|
|
|
page_now = request.get("pageNow", 1)
|
|
|
|
|
|
page_size = request.get("pageSize", 10)
|
|
|
|
|
|
else:
|
2026-03-13 16:38:07 +08:00
|
|
|
|
group_id = request.groupId
|
|
|
|
|
|
log_id = request.logId
|
2026-03-13 15:13:18 +08:00
|
|
|
|
page_now = request.pageNow
|
|
|
|
|
|
page_size = request.pageSize
|
|
|
|
|
|
|
2026-03-13 16:38:07 +08:00
|
|
|
|
if log_id not in self._cache:
|
2026-03-19 17:18:02 +08:00
|
|
|
|
total_count = self.FIXED_TOTAL_COUNT
|
2026-03-13 16:38:07 +08:00
|
|
|
|
all_statements = self._generate_statements(group_id, log_id, total_count)
|
|
|
|
|
|
self._cache[log_id] = (all_statements, total_count)
|
|
|
|
|
|
|
|
|
|
|
|
all_statements, total_count = self._cache[log_id]
|
2026-03-18 15:54:11 +08:00
|
|
|
|
primary_enterprise_name, primary_account_no = self._resolve_primary_binding(log_id)
|
2026-03-18 16:39:09 +08:00
|
|
|
|
self._apply_primary_binding(all_statements, primary_enterprise_name, primary_account_no)
|
2026-03-13 15:13:18 +08:00
|
|
|
|
|
|
|
|
|
|
start = (page_now - 1) * page_size
|
|
|
|
|
|
end = start + page_size
|
2026-03-13 16:38:07 +08:00
|
|
|
|
page_data = all_statements[start:end]
|
2026-03-13 15:13:18 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"code": "200",
|
|
|
|
|
|
"data": {"bankStatementList": page_data, "totalCount": total_count},
|
|
|
|
|
|
"status": "200",
|
|
|
|
|
|
"successResponse": True,
|
|
|
|
|
|
}
|