# LSFX Mock LogId Primary Binding Backend Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 让 `lsfx-mock-server` 为上传文件和拉取本行信息生成的每个 `logId` 绑定唯一且稳定的本方主体与本方账号,并在上传响应、上传状态和银行流水查询中保持一致。 **Architecture:** 在 `FileService` 中统一生成并维护 `logId -> 主体账号绑定`,以 `FileRecord` 作为单一事实来源;上传文件与拉取本行信息两条链路都创建完整记录;`StatementService` 通过只读查询能力按 `logId` 取回绑定并写入每条流水的 `leName/accountMaskNo`,不再自行随机本方主体或本方账号。 **Tech Stack:** Python 3, FastAPI, pytest, httpx TestClient ## 执行结果 - 2026-03-18 已按计划完成 Task 1 至 Task 5,功能实现、实施记录与最终验证均已落地。 - 代码提交顺序: - `0120d09` `收敛Mock文件记录主体账号绑定模型` - `6fb7287` `让拉取本行信息链路复用Mock主体账号绑定` - `0a85c09` `统一Mock上传状态主体账号绑定优先级` - `5195617` `让Mock流水查询复用logId主体账号绑定` - 最终验证已通过: - `python3 -m pytest tests/test_file_service.py -v` - `python3 -m pytest tests/test_statement_service.py -v` - `python3 -m pytest tests/test_api.py -v` - `python3 -m pytest tests/integration/test_full_workflow.py -v` - `python3 verify_implementation.py` - 实施细节见 `docs/reports/implementation/2026-03-18-lsfx-logid-primary-binding-implementation.md` --- ### Task 1: 收敛 FileRecord 为单一主体账号绑定模型 **Files:** - Modify: `lsfx-mock-server/services/file_service.py` - Create: `lsfx-mock-server/tests/test_file_service.py` - [ ] **Step 1: Write the failing test** 先新增 `test_file_service.py`,锁定 `FileRecord` 和上传响应的最小语义:一个 `logId` 只对应一个主体与一个账号。 ```python import pytest from fastapi import BackgroundTasks, UploadFile from io import BytesIO from services.file_service import FileService @pytest.mark.asyncio async def test_upload_file_should_create_single_primary_binding(): service = FileService() upload = UploadFile(filename="测试流水.csv", file=BytesIO(b"demo")) response = await service.upload_file(1000, upload, BackgroundTasks()) log = response["data"]["uploadLogList"][0] assert len(log["enterpriseNameList"]) == 1 assert len(log["accountNoList"]) == 1 assert response["data"]["accountsOfLog"][str(log["logId"])][0]["accountName"] == log["enterpriseNameList"][0] assert response["data"]["accountsOfLog"][str(log["logId"])][0]["accountNo"] == log["accountNoList"][0] ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd lsfx-mock-server pytest tests/test_file_service.py -k primary_binding -v ``` Expected: - `FAIL` - 原因是 `tests/test_file_service.py` 尚不存在,且 `FileRecord` 还没有明确的单值主体账号语义 - [ ] **Step 3: Write minimal implementation** 在 `file_service.py` 中增加单值字段和统一生成方法,例如: ```python @dataclass class FileRecord: primary_enterprise_name: str = "" primary_account_no: str = "" ``` ```python PRIMARY_ENTERPRISE_POOL = [ "测试主体A", "测试主体B", "测试主体C", "兰溪测试主体一部", "兰溪测试主体二部", ] def _generate_primary_account_binding(self) -> tuple[str, str]: enterprise_name = random.choice(PRIMARY_ENTERPRISE_POOL) account_no = f"{random.randint(100000000000000, 999999999999999)}" return enterprise_name, account_no ``` 并在 `upload_file()` 中回填: ```python enterprise_name, account_no = self._generate_primary_account_binding() file_record.primary_enterprise_name = enterprise_name file_record.primary_account_no = account_no file_record.enterprise_name_list = [enterprise_name] file_record.account_no_list = [account_no] ``` - [ ] **Step 4: Run test to verify it passes** Run: ```bash cd lsfx-mock-server pytest tests/test_file_service.py -k primary_binding -v ``` Expected: - `PASS` - 说明上传接口返回已经围绕单一主体账号绑定工作 - [ ] **Step 5: Commit** ```bash git add lsfx-mock-server/services/file_service.py lsfx-mock-server/tests/test_file_service.py git commit -m "收敛Mock文件记录主体账号绑定模型" ``` ### Task 2: 让拉取本行信息链路也创建并保存绑定记录 **Files:** - Modify: `lsfx-mock-server/services/file_service.py` - Modify: `lsfx-mock-server/tests/test_file_service.py` - Modify: `lsfx-mock-server/tests/test_api.py` - [ ] **Step 1: Write the failing test** 补两个测试,一个测服务层,一个测 API 层: ```python def test_fetch_inner_flow_should_persist_file_record_for_returned_log_id(): service = FileService() response = service.fetch_inner_flow({"groupId": 1000}) log_id = response["data"][0] assert log_id in service.file_records record = service.file_records[log_id] assert record.primary_enterprise_name assert record.primary_account_no ``` ```python def test_fetch_inner_flow_followed_by_upload_status_should_keep_same_binding(client, sample_inner_flow_request): flow_response = client.post("/watson/api/project/getJZFileOrZjrcuFile", data=sample_inner_flow_request) log_id = flow_response.json()["data"][0] status_response = client.get(f"/watson/api/project/bs/upload?groupId=1000&logId={log_id}") log = status_response.json()["data"]["logs"][0] assert len(log["enterpriseNameList"]) == 1 assert len(log["accountNoList"]) == 1 ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd lsfx-mock-server pytest tests/test_file_service.py -k fetch_inner_flow -v pytest tests/test_api.py -k "fetch_inner_flow_followed_by_upload_status" -v ``` Expected: - `FAIL` - 原因是 `fetch_inner_flow()` 当前只返回随机 `logId`,不会创建 `FileRecord` - [ ] **Step 3: Write minimal implementation** 把 `fetch_inner_flow()` 改成复用和上传相同的记录创建逻辑: ```python def _create_file_record(self, group_id: int, file_name: str, bank_name: str, template_name: str) -> FileRecord: ... ``` ```python def fetch_inner_flow(self, request: Union[Dict, object]) -> Dict: group_id = request.get("groupId", 1000) if isinstance(request, dict) else request.groupId self.log_counter += 1 log_id = self.log_counter file_record = self._create_file_record(group_id, f"inner_flow_{log_id}.csv", "ZJRCU", "ZJRCU_T251114") self.file_records[log_id] = file_record return {"code": "200", "data": [log_id], "status": "200", "successResponse": True} ``` - [ ] **Step 4: Run test to verify it passes** Run: ```bash cd lsfx-mock-server pytest tests/test_file_service.py -k fetch_inner_flow -v pytest tests/test_api.py -k "fetch_inner_flow_followed_by_upload_status" -v ``` Expected: - `PASS` - 说明拉取本行信息返回的 `logId` 已经能在后续链路中复用同一组主体账号 - [ ] **Step 5: Commit** ```bash git add lsfx-mock-server/services/file_service.py lsfx-mock-server/tests/test_file_service.py lsfx-mock-server/tests/test_api.py git commit -m "让拉取本行信息链路复用Mock主体账号绑定" ``` ### Task 3: 统一上传状态接口优先读取真实记录 **Files:** - Modify: `lsfx-mock-server/services/file_service.py` - Modify: `lsfx-mock-server/tests/test_file_service.py` - Modify: `lsfx-mock-server/tests/test_api.py` - [ ] **Step 1: Write the failing test** 补一组“真实记录优先级”测试,防止 `get_upload_status()` 继续走即时随机分支覆盖真实绑定: ```python import pytest @pytest.mark.asyncio async def test_get_upload_status_should_prefer_existing_file_record_binding(): service = FileService() upload = UploadFile(filename="测试流水.csv", file=BytesIO(b"demo")) response = await service.upload_file(1000, upload, BackgroundTasks()) log_id = response["data"]["uploadLogList"][0]["logId"] status = service.get_upload_status(1000, log_id) log = status["data"]["logs"][0] assert log["enterpriseNameList"] == response["data"]["uploadLogList"][0]["enterpriseNameList"] assert log["accountNoList"] == response["data"]["uploadLogList"][0]["accountNoList"] ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd lsfx-mock-server pytest tests/test_file_service.py -k "prefer_existing_file_record" -v ``` Expected: - `FAIL` - 原因是 `get_upload_status()` 当前只按 `logId` 做即时随机生成,不会优先读取 `self.file_records` - [ ] **Step 3: Write minimal implementation** 把 `get_upload_status()` 改成: ```python def get_upload_status(self, group_id: int, log_id: int = None) -> dict: logs = [] if log_id: if log_id in self.file_records: logs.append(self._build_log_detail(self.file_records[log_id])) else: random.seed(log_id) logs.append(self._generate_deterministic_record(log_id, group_id)) ``` 同时确保 `_generate_deterministic_record()` 也遵循单一绑定语义: - `enterpriseNameList` 长度为 1 - `accountNoList` 长度为 1 - [ ] **Step 4: Run test to verify it passes** Run: ```bash cd lsfx-mock-server pytest tests/test_file_service.py -v pytest tests/test_api.py -k "upload_status or deterministic_data_generation" -v ``` Expected: - `PASS` - 已有上传状态接口的确定性测试仍然成立 - 对真实记录会优先返回真实绑定 - [ ] **Step 5: Commit** ```bash git add lsfx-mock-server/services/file_service.py lsfx-mock-server/tests/test_file_service.py lsfx-mock-server/tests/test_api.py git commit -m "统一Mock上传状态主体账号绑定优先级" ``` ### Task 4: 让 StatementService 按 logId 注入本方主体和账号 **Files:** - Modify: `lsfx-mock-server/services/statement_service.py` - Modify: `lsfx-mock-server/services/file_service.py` - Modify: `lsfx-mock-server/routers/api.py` - Create: `lsfx-mock-server/tests/test_statement_service.py` - Modify: `lsfx-mock-server/tests/integration/test_full_workflow.py` - [ ] **Step 1: Write the failing test** 新增服务层测试,锁定同一 `logId` 的所有流水都要复用同一组 `leName/accountMaskNo`: ```python from services.file_service import FileService from services.statement_service import StatementService def test_bank_statement_should_reuse_file_record_primary_binding(): file_service = FileService() log_id = file_service.fetch_inner_flow({"groupId": 1000})["data"][0] record = file_service.file_records[log_id] service = StatementService(file_service=file_service) result = service.get_bank_statement({"groupId": 1000, "logId": log_id, "pageNow": 1, "pageSize": 20}) statements = result["data"]["bankStatementList"] assert all(item["leName"] == record.primary_enterprise_name for item in statements) assert all(item["accountMaskNo"] == record.primary_account_no for item in statements) ``` 再补一个集成断言: ```python def test_complete_workflow_should_keep_same_primary_binding_between_status_and_statement(client): ... ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd lsfx-mock-server pytest tests/test_statement_service.py -v pytest tests/integration/test_full_workflow.py -k primary_binding -v ``` Expected: - `FAIL` - 原因是 `StatementService` 目前仍写死 `leName = 张传伟` 并独立随机 `accountMaskNo` - [ ] **Step 3: Write minimal implementation** 给 `StatementService` 增加对 `FileService` 的只读依赖: ```python class StatementService: def __init__(self, file_service=None): self.file_service = file_service self._cache = {} ``` ```python def _resolve_primary_binding(self, group_id: int, log_id: int) -> tuple[str, str]: if self.file_service and log_id in self.file_service.file_records: record = self.file_service.file_records[log_id] return record.primary_enterprise_name, record.primary_account_no return "测试主体A", f"{random.randint(100000000000000, 999999999999999)}" ``` 在 `_generate_random_statement()` 中替换: ```python enterprise_name, account_no = self._resolve_primary_binding(group_id, log_id) ... "accountMaskNo": account_no, ... "leName": enterprise_name, ``` 并在 [`lsfx-mock-server/routers/api.py`](/Users/wkc/Desktop/ccdi/ccdi/lsfx-mock-server/routers/api.py) 中把服务实例装配改成共享同一份 `file_service`: ```python file_service = FileService() statement_service = StatementService(file_service=file_service) ``` - [ ] **Step 4: Run test to verify it passes** Run: ```bash cd lsfx-mock-server pytest tests/test_statement_service.py -v pytest tests/integration/test_full_workflow.py -v ``` Expected: - `PASS` - 同一 `logId` 的分页流水在 `leName/accountMaskNo` 上保持一致 - [ ] **Step 5: Commit** ```bash git add lsfx-mock-server/services/statement_service.py lsfx-mock-server/services/file_service.py lsfx-mock-server/tests/test_statement_service.py lsfx-mock-server/tests/integration/test_full_workflow.py git commit -m "让Mock流水查询复用logId主体账号绑定" ``` ### Task 5: 补实施记录并完成最终验证 **Files:** - Create: `docs/reports/implementation/2026-03-18-lsfx-logid-primary-binding-implementation.md` - Modify: `docs/plans/backend/2026-03-18-lsfx-logid-primary-binding-backend-implementation.md` - [ ] **Step 1: Write the implementation report skeleton** 记录以下内容: - `FileRecord` 新增的绑定字段 - 上传文件/拉取本行信息/上传状态/查流水四处的联动 - 实际执行过的 pytest 命令 - 兼容逻辑优先级说明 建议骨架: ```markdown # LSFX Mock LogId 主体账号绑定实施记录 ## 变更概述 - ... ## 验证记录 - `cd lsfx-mock-server && pytest tests/test_file_service.py -v` - `cd lsfx-mock-server && pytest tests/test_statement_service.py -v` - `cd lsfx-mock-server && pytest tests/test_api.py -v` ``` - [ ] **Step 2: Run final verification** Run: ```bash cd lsfx-mock-server pytest tests/test_file_service.py -v pytest tests/test_statement_service.py -v pytest tests/test_api.py -v pytest tests/integration/test_full_workflow.py -v python verify_implementation.py ``` Expected: - 所有测试通过 - 接口字段完整性检查仍然通过 - [ ] **Step 3: Stop any started services** 如果验证过程中启动了: - `python main.py` - `uvicorn main:app --reload --host 0.0.0.0 --port 8000` 结束时必须主动关闭进程,避免残留端口占用。 - [ ] **Step 4: Commit** ```bash git add docs/reports/implementation/2026-03-18-lsfx-logid-primary-binding-implementation.md docs/plans/backend/2026-03-18-lsfx-logid-primary-binding-backend-implementation.md git commit -m "补充Mock主体账号绑定实施记录" ```