from fastapi import BackgroundTasks, UploadFile from utils.response_builder import ResponseBuilder from config.settings import settings from services.phase2_baseline_service import Phase2BaselineService from services.staff_identity_repository import StaffIdentityRepository from typing import Dict, List, Union from dataclasses import dataclass, field import time from datetime import datetime, timedelta import random 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", ] PHASE2_STATEMENT_RULE_CODES = [ "LOW_INCOME_RELATIVE_LARGE_TRANSACTION", "MULTI_PARTY_GAMBLING_TRANSFER", "MONTHLY_FIXED_INCOME", "FIXED_COUNTERPARTY_TRANSFER", "SALARY_QUICK_TRANSFER", "SALARY_UNUSED", ] PHASE2_BASELINE_RULE_CODES = [ "HOUSE_REGISTRATION_MISMATCH", "PROPERTY_FEE_REGISTRATION_MISMATCH", "TAX_ASSET_REGISTRATION_MISMATCH", "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", "ANNUAL_TURNOVER", "LARGE_CASH_DEPOSIT", "FREQUENT_CASH_DEPOSIT", } MONTHLY_FIXED_INCOME_ISOLATED_PHASE1_RULE_CODES = { "SUSPICIOUS_INCOME_KEYWORD", "FOREX_SELL_AMT", } MONTHLY_FIXED_INCOME_ISOLATED_PHASE2_RULE_CODES = { "FIXED_COUNTERPARTY_TRANSFER", } RULE_CONFLICT_GROUPS = [] ALL_MODE_STATEMENT_BASELINE_RULE_CODES = { "LOW_INCOME_RELATIVE_LARGE_TRANSACTION", } @dataclass class FileRecord: """文件记录模型(扩展版)""" # 原有字段 log_id: int group_id: int file_name: str status: int = -5 # -5 表示解析成功 upload_status_desc: str = "data.wait.confirm.newaccount" parsing: bool = True # True表示正在解析 # 新增字段 - 账号和主体信息 primary_enterprise_name: str = "" primary_account_no: str = "" account_no_list: List[str] = field(default_factory=list) enterprise_name_list: List[str] = field(default_factory=list) # 新增字段 - 银行和模板信息 bank_name: str = "ZJRCU" real_bank_name: str = "ZJRCU" template_name: str = "ZJRCU_T251114" data_type_info: List[str] = field(default_factory=lambda: ["CSV", ","]) # 新增字段 - 文件元数据 file_size: int = 50000 download_file_name: str = "" file_package_id: str = field(default_factory=lambda: str(uuid.uuid4()).replace('-', '')) # 新增字段 - 上传用户信息 file_upload_by: int = 448 file_upload_by_user_name: str = "admin@support.com" file_upload_time: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S")) # 新增字段 - 法律实体信息 le_id: int = 10000 login_le_id: int = 10000 log_type: str = "bankstatement" log_meta: str = "{\"lostHeader\":[],\"balanceAmount\":\"-1\"}" lost_header: List[str] = field(default_factory=list) # 新增字段 - 记录统计 rows: int = 0 source: str = "http" total_records: int = 150 is_split: int = 0 # 新增字段 - 交易日期范围 trx_date_start_id: int = 20240101 trx_date_end_id: int = 20241231 # 新增字段 - 身份绑定 staff_name: str = "" staff_id_card: str = "" 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) 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 _NoopAbnormalAccountBaselineService: def apply(self, staff_id_card: str, abnormal_accounts: List[dict]) -> None: return None class FileService: """文件上传和解析服务""" INNER_FLOW_TOTAL_RECORDS = 200 LOG_ID_MIN = settings.INITIAL_LOG_ID LOG_ID_MAX = 99999 def __init__( self, staff_identity_repository=None, phase2_baseline_service=None, abnormal_account_baseline_service=None, ): self.file_records: Dict[int, FileRecord] = {} # logId -> FileRecord self.log_counter = settings.INITIAL_LOG_ID self.staff_identity_repository = staff_identity_repository or StaffIdentityRepository() self.phase2_baseline_service = phase2_baseline_service or Phase2BaselineService() self.abnormal_account_baseline_service = ( abnormal_account_baseline_service or _NoopAbnormalAccountBaselineService() ) def get_file_record(self, log_id: int) -> FileRecord: """按 logId 获取已存在的文件记录。""" return self.file_records.get(log_id) def _generate_log_id(self) -> int: """生成当前进程内未占用的随机 logId。""" available_capacity = self.LOG_ID_MAX - self.LOG_ID_MIN + 1 if len(self.file_records) >= available_capacity: raise RuntimeError("可用 logId 已耗尽") while True: candidate = random.randint(self.LOG_ID_MIN, self.LOG_ID_MAX) if candidate not in self.file_records: self.log_counter = candidate return candidate def _infer_bank_name(self, filename: str) -> tuple: """根据文件名推断银行名称和模板名称""" if "支付宝" in filename or "alipay" in filename.lower(): return "ALIPAY", "ALIPAY_T220708" elif "绍兴银行" in filename or "BSX" in filename: return "BSX", "BSX_T240925" else: return "ZJRCU", "ZJRCU_T251114" def _generate_primary_binding(self) -> tuple: """生成单一稳定的本方主体/本方账号绑定。""" primary_account_no = f"{random.randint(10000000000, 99999999999)}" primary_enterprise_name = "测试主体" return primary_enterprise_name, primary_account_no def _generate_primary_binding_from_rng(self, rng: random.Random) -> tuple: """使用局部随机源生成单一稳定的本方主体/本方账号绑定。""" primary_account_no = f"{rng.randint(10000000000, 99999999999)}" primary_enterprise_name = "测试主体" return primary_enterprise_name, primary_account_no def _build_primary_binding_lists( self, primary_enterprise_name: str, primary_account_no: str ) -> dict: """基于主绑定事实源构建列表字段。""" return { "accountNoList": [primary_account_no], "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_subset_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), "phase2_statement_hit_rules": self._pick_rule_subset( rng, PHASE2_STATEMENT_RULE_CODES, 2, 4 ), "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: """生成全部兼容规则命中计划。""" 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), "abnormal_account_hit_rules": list(ABNORMAL_ACCOUNT_RULE_CODES), } def _build_monthly_fixed_income_isolated_rule_hit_plan(self) -> dict: """为月固定收入准备不受正向流入污染的 all 模式计划。""" full_plan = self._build_all_compatible_rule_hit_plan() return { "large_transaction_hit_rules": [ rule_code for rule_code in full_plan["large_transaction_hit_rules"] if rule_code not in MONTHLY_FIXED_INCOME_ISOLATED_LARGE_TRANSACTION_RULE_CODES ], "phase1_hit_rules": [ rule_code for rule_code in full_plan["phase1_hit_rules"] if rule_code not in MONTHLY_FIXED_INCOME_ISOLATED_PHASE1_RULE_CODES ], "phase2_statement_hit_rules": [ rule_code for rule_code in full_plan["phase2_statement_hit_rules"] if rule_code not in MONTHLY_FIXED_INCOME_ISOLATED_PHASE2_RULE_CODES ], "phase2_baseline_hit_rules": list(full_plan["phase2_baseline_hit_rules"]), } 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 _apply_rule_hit_plan_to_record(self, file_record: FileRecord, rule_hit_plan: dict) -> None: """将规则命中计划回填到指定文件记录。""" file_record.large_transaction_hit_rules = list( rule_hit_plan.get("large_transaction_hit_rules", []) ) file_record.phase1_hit_rules = list(rule_hit_plan.get("phase1_hit_rules", [])) file_record.phase2_statement_hit_rules = list( rule_hit_plan.get("phase2_statement_hit_rules", []) ) 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: """同项目存在多文件时,隔离月固定收入样本,避免被其他正向流入规则污染。""" if settings.RULE_HIT_MODE != "all": return group_records = [ record for record in self.file_records.values() if record.group_id == group_id ] if not group_records: return full_plan = self._apply_conflict_groups(self._build_all_compatible_rule_hit_plan()) if len(group_records) == 1: return monthly_safe_plan = self._apply_conflict_groups( self._build_monthly_fixed_income_isolated_rule_hit_plan() ) self._apply_rule_hit_plan_to_record(group_records[0], monthly_safe_plan) for record in group_records[1:]: self._apply_rule_hit_plan_to_record(record, full_plan) def _create_file_record( self, *, log_id: int, group_id: int, file_name: str, download_file_name: str, bank_name: str, template_name: str, primary_enterprise_name: str, primary_account_no: str, file_size: int, total_records: int, trx_date_start_id: int, trx_date_end_id: int, le_id: int, login_le_id: int, staff_name: str = "", staff_id_card: str = "", family_id_cards: List[str] = None, large_transaction_hit_rules: List[str] = None, 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: """创建文件记录并写入主绑定信息。""" binding_lists = self._build_primary_binding_lists( primary_enterprise_name, primary_account_no, ) return FileRecord( log_id=log_id, group_id=group_id, file_name=file_name, download_file_name=download_file_name, bank_name=bank_name, real_bank_name=bank_name, template_name=template_name, primary_enterprise_name=primary_enterprise_name, primary_account_no=primary_account_no, account_no_list=binding_lists["accountNoList"], enterprise_name_list=binding_lists["enterpriseNameList"], le_id=le_id, login_le_id=login_le_id, file_size=file_size, total_records=total_records, trx_date_start_id=trx_date_start_id, trx_date_end_id=trx_date_end_id, staff_name=staff_name, staff_id_card=staff_id_card, 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 []), 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, ) def _select_staff_identity_scope(self) -> dict: """读取一个员工及其亲属身份范围。""" return self.staff_identity_repository.select_random_staff_with_families() def _apply_phase2_baselines(self, file_record: FileRecord) -> None: """按当前记录命中的第二期基线规则幂等补齐外部事实。""" baseline_rule_codes = list(file_record.phase2_baseline_hit_rules) if settings.RULE_HIT_MODE == "all": for rule_code in file_record.phase2_statement_hit_rules: if ( rule_code in ALL_MODE_STATEMENT_BASELINE_RULE_CODES and rule_code not in baseline_rule_codes ): baseline_rule_codes.append(rule_code) self.phase2_baseline_service.apply( staff_id_card=file_record.staff_id_card, family_id_cards=file_record.family_id_cards, baseline_rule_codes=baseline_rule_codes, ) def _apply_abnormal_account_baselines(self, file_record: FileRecord) -> None: """按当前记录命中的异常账户规则幂等补齐账户事实。""" if not file_record.abnormal_account_hit_rules: return if not file_record.abnormal_accounts: raise RuntimeError("异常账户命中计划存在,但未生成账户事实") self.abnormal_account_baseline_service.apply( staff_id_card=file_record.staff_id_card, abnormal_accounts=file_record.abnormal_accounts, ) async def upload_file( self, group_id: int, file: UploadFile, background_tasks: BackgroundTasks ) -> Dict: """上传文件并启动后台解析任务 Args: group_id: 项目ID file: 上传的文件 background_tasks: FastAPI后台任务 Returns: 上传响应字典 """ # 生成唯一 logId log_id = self._generate_log_id() # 推断银行信息 bank_name, template_name = self._infer_bank_name(file.filename) rule_hit_plan = self._build_rule_hit_plan(log_id) # 生成合理的交易日期范围 end_date = datetime.now() start_date = end_date - timedelta(days=random.randint(90, 365)) trx_date_start_id = int(start_date.strftime("%Y%m%d")) trx_date_end_id = int(end_date.strftime("%Y%m%d")) # 生成单一主绑定 primary_enterprise_name, primary_account_no = self._generate_primary_binding() identity_scope = self._select_staff_identity_scope() # 创建完整的文件记录 file_record = self._create_file_record( log_id=log_id, group_id=group_id, file_name=file.filename, download_file_name=file.filename, bank_name=bank_name, template_name=template_name, primary_enterprise_name=primary_enterprise_name, primary_account_no=primary_account_no, file_size=random.randint(10000, 100000), total_records=random.randint(150, 200), trx_date_start_id=trx_date_start_id, trx_date_end_id=trx_date_end_id, le_id=10000 + random.randint(0, 9999), login_le_id=10000 + random.randint(0, 9999), staff_name=identity_scope["staff_name"], staff_id_card=identity_scope["staff_id_card"], family_id_cards=identity_scope["family_id_cards"], large_transaction_hit_rules=rule_hit_plan.get("large_transaction_hit_rules", []), 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", []), ), ) self._apply_abnormal_account_baselines(file_record) self.file_records[log_id] = file_record self._rebalance_all_mode_group_rule_plans(group_id) self._apply_phase2_baselines(file_record) # 添加后台任务(延迟解析) background_tasks.add_task(self._delayed_parse, log_id) # 构建响应 return self._build_upload_response(file_record) def _build_upload_response(self, file_record: FileRecord) -> dict: """构建上传接口的完整响应""" return { "code": "200", "data": { "accountsOfLog": { str(file_record.log_id): [ { "bank": file_record.bank_name, "accountName": file_record.primary_enterprise_name, "accountNo": file_record.primary_account_no, "currency": "CNY" } ] }, "uploadLogList": [ { **self._build_primary_binding_lists( file_record.primary_enterprise_name, file_record.primary_account_no, ), "bankName": file_record.bank_name, "dataTypeInfo": file_record.data_type_info, "downloadFileName": file_record.download_file_name, "filePackageId": file_record.file_package_id, "fileSize": file_record.file_size, "fileUploadBy": file_record.file_upload_by, "fileUploadByUserName": file_record.file_upload_by_user_name, "fileUploadTime": file_record.file_upload_time, "leId": file_record.le_id, "logId": file_record.log_id, "logMeta": file_record.log_meta, "logType": file_record.log_type, "loginLeId": file_record.login_le_id, "lostHeader": file_record.lost_header, "realBankName": file_record.real_bank_name, "rows": file_record.rows, "source": file_record.source, "status": file_record.status, "templateName": file_record.template_name, "totalRecords": file_record.total_records, "trxDateEndId": file_record.trx_date_end_id, "trxDateStartId": file_record.trx_date_start_id, "uploadFileName": file_record.file_name, "uploadStatusDesc": file_record.upload_status_desc } ], "uploadStatus": 1 }, "status": "200", "successResponse": True } def _delayed_parse(self, log_id: int): """后台任务:模拟文件解析过程 Args: log_id: 日志ID """ time.sleep(settings.PARSE_DELAY_SECONDS) # 解析完成,更新状态 if log_id in self.file_records: self.file_records[log_id].parsing = False def _generate_deterministic_record( self, log_id: int, group_id: int, rng: random.Random ) -> dict: """ 基于 logId 生成确定性的文件记录 Args: log_id: 文件ID(用作随机种子) group_id: 项目ID Returns: 文件记录字典(26个字段) """ # 银行类型选项 bank_options = [ ("ALIPAY", "ALIPAY_T220708"), ("BSX", "BSX_T240925"), ("ZJRCU", "ZJRCU_T251114") ] bank_name, template_name = rng.choice(bank_options) # 生成基于种子的稳定时间范围,确保同一 logId 重复查询完全一致 base_datetime = datetime(2024, 1, 1, 8, 0, 0) end_date = base_datetime + timedelta(days=rng.randint(180, 540)) start_date = end_date - timedelta(days=rng.randint(90, 365)) file_upload_time = ( base_datetime + timedelta( days=rng.randint(0, 540), hours=rng.randint(0, 23), minutes=rng.randint(0, 59), seconds=rng.randint(0, 59), ) ) # 生成账号和主体 primary_enterprise_name, primary_account_no = self._generate_primary_binding_from_rng(rng) binding_lists = self._build_primary_binding_lists( primary_enterprise_name, primary_account_no ) return { **binding_lists, "bankName": bank_name, "dataTypeInfo": ["CSV", ","], "downloadFileName": f"测试文件_{log_id}.csv", "fileSize": rng.randint(10000, 100000), "fileUploadBy": 448, "fileUploadByUserName": "admin@support.com", "fileUploadTime": file_upload_time.strftime("%Y-%m-%d %H:%M:%S"), "isSplit": 0, "leId": 10000 + rng.randint(0, 9999), "logId": log_id, "logMeta": "{\"lostHeader\":[],\"balanceAmount\":\"-1\"}", "logType": "bankstatement", "loginLeId": 10000 + rng.randint(0, 9999), "lostHeader": [], "realBankName": bank_name, "rows": 0, "source": "http", "status": -5, "templateName": template_name, "totalRecords": rng.randint(100, 300), "trxDateEndId": int(end_date.strftime("%Y%m%d")), "trxDateStartId": int(start_date.strftime("%Y%m%d")), "uploadFileName": f"测试文件_{log_id}.pdf", "uploadStatusDesc": "data.wait.confirm.newaccount" } def _build_deterministic_log_detail(self, log_id: int, group_id: int) -> dict: """构建 deterministic 回退的单条日志详情。""" rng = random.Random(log_id) return self._generate_deterministic_record(log_id, group_id, rng) def _build_log_detail(self, record: FileRecord) -> dict: """构建日志详情对象""" return { **self._build_primary_binding_lists( record.primary_enterprise_name, record.primary_account_no, ), "bankName": record.bank_name, "dataTypeInfo": record.data_type_info, "downloadFileName": record.download_file_name, "fileSize": record.file_size, "fileUploadBy": record.file_upload_by, "fileUploadByUserName": record.file_upload_by_user_name, "fileUploadTime": record.file_upload_time, "isSplit": record.is_split, "leId": record.le_id, "logId": record.log_id, "logMeta": record.log_meta, "logType": record.log_type, "loginLeId": record.login_le_id, "lostHeader": record.lost_header, "realBankName": record.real_bank_name, "rows": record.rows, "source": record.source, "status": record.status, "templateName": record.template_name, "totalRecords": record.total_records, "trxDateEndId": record.trx_date_end_id, "trxDateStartId": record.trx_date_start_id, "uploadFileName": record.file_name, "uploadStatusDesc": record.upload_status_desc } def check_parse_status(self, group_id: int, inprogress_list: str) -> Dict: """检查文件解析状态 Args: group_id: 项目ID inprogress_list: 文件ID列表(逗号分隔) Returns: 解析状态响应字典 """ # 解析logId列表 log_ids = [int(x.strip()) for x in inprogress_list.split(",") if x.strip()] pending_list = [] all_parsing_complete = True for log_id in log_ids: if log_id in self.file_records: record = self.file_records[log_id] if record.parsing: all_parsing_complete = False pending_list.append(self._build_log_detail(record)) return { "code": "200", "data": { "parsing": not all_parsing_complete, "pendingList": pending_list }, "status": "200", "successResponse": True } def get_upload_status(self, group_id: int, log_id: int = None) -> dict: """ 获取文件上传状态(基于 logId 生成确定性数据) Args: group_id: 项目ID log_id: 文件ID(可选) Returns: 上传状态响应字典 """ logs = [] if log_id is not None: if log_id in self.file_records: log_detail = self._build_log_detail(self.file_records[log_id]) else: log_detail = self._build_deterministic_log_detail(log_id, group_id) logs.append(log_detail) # 返回响应 return { "code": "200", "data": { "logs": logs, "status": "", "accountId": 8954, "currency": "CNY" }, "status": "200", "successResponse": True } def delete_files(self, group_id: int, log_ids: List[int], user_id: int) -> Dict: """删除文件 Args: group_id: 项目ID log_ids: 文件ID列表 user_id: 用户ID Returns: 删除响应字典 """ # 删除文件记录 deleted_count = 0 for log_id in log_ids: if log_id in self.file_records: del self.file_records[log_id] deleted_count += 1 return { "code": "200 OK", # 注意:这里是 "200 OK" 不是 "200" "data": { "message": "delete.files.success" }, "message": "delete.files.success", "status": "200", "successResponse": True } def fetch_inner_flow(self, request: Union[Dict, object]) -> Dict: """拉取行内流水(创建并保存绑定记录) Args: request: 拉取流水请求(可以是字典或对象) Returns: 流水响应字典,包含创建并保存的logId数组 """ # 支持 dict 或对象 if isinstance(request, dict): group_id = request.get("groupId", 1000) customer_no = request.get("customerNo", "") data_start_date_id = request.get("dataStartDateId", 20240101) data_end_date_id = request.get("dataEndDateId", 20241231) else: group_id = request.groupId customer_no = request.customerNo data_start_date_id = request.dataStartDateId data_end_date_id = request.dataEndDateId # 使用随机 logId,确保与上传链路一致且不覆盖现有记录 log_id = self._generate_log_id() rule_hit_plan = self._build_rule_hit_plan(log_id) primary_enterprise_name, primary_account_no = self._generate_primary_binding() identity_scope = self._select_staff_identity_scope() file_record = self._create_file_record( log_id=log_id, group_id=group_id, file_name=f"{customer_no or 'inner_flow'}_{log_id}.csv", download_file_name=f"{customer_no or 'inner_flow'}_{log_id}.csv", bank_name="ZJRCU", template_name="ZJRCU_T251114", primary_enterprise_name=primary_enterprise_name, primary_account_no=primary_account_no, file_size=random.randint(10000, 100000), total_records=self.INNER_FLOW_TOTAL_RECORDS, trx_date_start_id=data_start_date_id, trx_date_end_id=data_end_date_id, le_id=10000 + random.randint(0, 9999), login_le_id=10000 + random.randint(0, 9999), staff_name=identity_scope["staff_name"], staff_id_card=identity_scope["staff_id_card"], family_id_cards=identity_scope["family_id_cards"], large_transaction_hit_rules=rule_hit_plan.get("large_transaction_hit_rules", []), 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, ) self._apply_abnormal_account_baselines(file_record) self.file_records[log_id] = file_record self._rebalance_all_mode_group_rule_plans(group_id) self._apply_phase2_baselines(file_record) # 返回成功的响应,包含logId数组 return { "code": "200", "data": [log_id], "status": "200", "successResponse": True, }