Files
ccdi/docs/plans/backend/2026-03-18-lsfx-logid-primary-binding-backend-implementation.md

484 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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主体账号绑定实施记录"
```