diff --git a/docs/plans/backend/2026-03-18-lsfx-logid-primary-binding-backend-implementation.md b/docs/plans/backend/2026-03-18-lsfx-logid-primary-binding-backend-implementation.md new file mode 100644 index 00000000..ee868cd5 --- /dev/null +++ b/docs/plans/backend/2026-03-18-lsfx-logid-primary-binding-backend-implementation.md @@ -0,0 +1,467 @@ +# 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 + +--- + +### 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主体账号绑定实施记录" +``` diff --git a/docs/plans/frontend/2026-03-18-lsfx-logid-primary-binding-frontend-implementation.md b/docs/plans/frontend/2026-03-18-lsfx-logid-primary-binding-frontend-implementation.md new file mode 100644 index 00000000..57f578a8 --- /dev/null +++ b/docs/plans/frontend/2026-03-18-lsfx-logid-primary-binding-frontend-implementation.md @@ -0,0 +1,191 @@ +# LSFX Mock LogId Primary Binding Frontend 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 后,能够稳定展示同一 `logId` 下统一的本方主体与本方账号。 + +**Architecture:** 本期前端仍以联调验证为主,不预设 Vue 代码改造;通过联调记录、构建验证和页面手工检查,确认上传链路返回的主体账号与流水明细展示保持一致;只有在联调中暴露真实展示问题时,才拆出单独前端修复需求。 + +**Tech Stack:** Vue 2, Element UI, Axios request wrapper, npm, Node + +--- + +### Task 1: 建立主体账号联调记录模板 + +**Files:** +- Verify: `ruoyi-ui/src/views/ccdiProject/detail.vue` +- Verify: `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue` +- Verify: `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue` +- Verify: `ruoyi-ui/src/api/ccdiProjectUpload.js` +- Create: `docs/tests/records/2026-03-18-lsfx-logid-primary-binding-frontend-verification.md` + +- [ ] **Step 1: Write the verification record skeleton** + +先创建联调记录文件,明确本次验证重点是“同一 `logId` 主体账号一致性”: + +```markdown +# LSFX Mock LogId 主体账号前端联调记录 + +## 验证范围 +- 上传返回的主体与账号 +- 流水明细中的本方主体与账号 +- 同一 logId 跨分页一致性 + +## 验证结果 +- [ ] 上传返回存在主体账号 +- [ ] 流水明细可见统一主体账号 +- [ ] 翻页后主体账号不变化 +``` + +- [ ] **Step 2: Run path existence smoke check** + +Run: + +```bash +test -f ruoyi-ui/src/views/ccdiProject/detail.vue +test -f ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue +test -f ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue +test -f ruoyi-ui/src/api/ccdiProjectUpload.js +``` + +Expected: + +- 四条命令都成功 + +- [ ] **Step 3: Keep implementation minimal** + +本任务只建立联调记录,不修改前端业务代码。 + +- [ ] **Step 4: Commit** + +```bash +git add docs/tests/records/2026-03-18-lsfx-logid-primary-binding-frontend-verification.md +git commit -m "新增Mock主体账号绑定前端联调记录模板" +``` + +### Task 2: 完成前端构建与入口冒烟 + +**Files:** +- Modify: `docs/tests/records/2026-03-18-lsfx-logid-primary-binding-frontend-verification.md` + +- [ ] **Step 1: Run frontend build smoke check** + +Run: + +```bash +cd ruoyi-ui +npm run build:prod +``` + +Expected: + +- 构建成功 +- 说明本次 Mock 变化不会强制前端源码同步改动 + +- [ ] **Step 2: Run route-level smoke check** + +手工验证以下入口: + +- 项目详情页可打开 +- “上传数据”页签可打开 +- “流水明细查询”页签可打开 +- “拉取本行信息”相关交互仍可进入 + +- [ ] **Step 3: Update verification notes** + +把构建结果和入口冒烟结果写入联调记录。 + +- [ ] **Step 4: Commit** + +```bash +git add docs/tests/records/2026-03-18-lsfx-logid-primary-binding-frontend-verification.md +git commit -m "记录Mock主体账号绑定前端构建与入口验证" +``` + +### Task 3: 验证上传结果与流水明细中的主体账号一致性 + +**Files:** +- Modify: `docs/tests/records/2026-03-18-lsfx-logid-primary-binding-frontend-verification.md` +- Verify: `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue` +- Verify: `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue` + +- [ ] **Step 1: Prepare the manual verification checklist** + +围绕同一 `logId`,记录以下检查点: + +- 上传返回中是否能看到本方主体和本方账号 +- 进入流水明细后,本方主体是否与上传返回一致 +- 本方账号是否与上传返回一致 +- 翻页后本方主体和账号是否仍一致 + +- [ ] **Step 2: Run manual verification against updated Mock** + +建议联调步骤: + +1. 启动前端、后端、`lsfx-mock-server` +2. 在项目详情页执行上传文件或拉取本行信息 +3. 记下返回的 `logId`、本方主体、本方账号 +4. 打开同一 `logId` 对应的流水明细 +5. 验证列表首屏和翻页后的主体账号是否一致 + +- [ ] **Step 3: Record actual UI results** + +在联调记录中按以下格式记录: + +```markdown +- `logId=xxxxx` + - 上传返回主体:`测试主体A` + - 上传返回账号:`6222...` + - 明细页主体:`测试主体A` + - 明细页账号:`6222...` + - 翻页后是否一致:是 / 否 +``` + +- [ ] **Step 4: Commit** + +```bash +git add docs/tests/records/2026-03-18-lsfx-logid-primary-binding-frontend-verification.md +git commit -m "记录Mock主体账号绑定前端联调结果" +``` + +### Task 4: 收敛前端结论并清理验证进程 + +**Files:** +- Modify: `docs/tests/records/2026-03-18-lsfx-logid-primary-binding-frontend-verification.md` +- Modify: `docs/plans/frontend/2026-03-18-lsfx-logid-primary-binding-frontend-implementation.md` + +- [ ] **Step 1: Summarize whether frontend code changes are required** + +在联调记录末尾输出最终结论: + +- 若上传返回和流水明细中的主体账号一致,则结论为“本期前端零代码改动” +- 若存在显示缺口,则明确是接口字段问题还是页面渲染问题 + +- [ ] **Step 2: Stop any started services** + +若本任务启动过: + +- `npm run dev` +- 后端 `mvn -pl ruoyi-admin spring-boot:run` +- Mock `python main.py` 或 `uvicorn main:app --reload --host 0.0.0.0 --port 8000` + +验证完成后必须关闭进程,避免残留占用端口。 + +- [ ] **Step 3: Keep implementation minimal** + +除非联调明确暴露前端显示缺陷,否则不提前修改 `ruoyi-ui/src` 下页面代码。 + +- [ ] **Step 4: Commit** + +```bash +git add docs/tests/records/2026-03-18-lsfx-logid-primary-binding-frontend-verification.md docs/plans/frontend/2026-03-18-lsfx-logid-primary-binding-frontend-implementation.md +git commit -m "补充Mock主体账号绑定前端联调结论" +``` + +## Frontend Integration Conclusion + +本计划的默认结论是“先联调验证,再决定是否需要前端修复”: + +- 这次主改动仍在 `lsfx-mock-server` +- 前端重点验证同一 `logId` 主体账号的一致性展示 +- 若页面已能正确展示,则本期不需要前端代码改造 diff --git a/docs/reports/implementation/2026-03-18-lsfx-logid-primary-binding-plan-record.md b/docs/reports/implementation/2026-03-18-lsfx-logid-primary-binding-plan-record.md new file mode 100644 index 00000000..ef34d006 --- /dev/null +++ b/docs/reports/implementation/2026-03-18-lsfx-logid-primary-binding-plan-record.md @@ -0,0 +1,17 @@ +# LSFX Mock LogId 主体账号绑定实施计划产出记录 + +## 变更概述 + +- 基于已确认的主体账号绑定设计文档,新增后端实施计划与前端实施计划各一份。 +- 后端计划聚焦 `FileService` 与 `StatementService` 围绕 `logId` 复用主体账号绑定。 +- 前端计划聚焦现有项目详情页对同一 `logId` 主体账号一致性的联调验证,默认不做 Vue 代码改造。 + +## 新增文件 + +- `docs/plans/backend/2026-03-18-lsfx-logid-primary-binding-backend-implementation.md` +- `docs/plans/frontend/2026-03-18-lsfx-logid-primary-binding-frontend-implementation.md` + +## 说明 + +- 计划已按仓库约定拆分为前后端两份。 +- 后续按计划执行时,仍需继续补实施记录与测试记录。