# Project Import History 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:** 为历史项目导入能力补齐后端链路,支持创建新项目后异步复制历史流水、复制成功文件记录、禁止删除历史导入记录,并自动触发现有打标与结果刷新流程。 **Architecture:** 后端采用最短路径实现,在现有 `CcdiProjectController -> ICcdiProjectService -> CcdiProjectServiceImpl` 创建项目主链路外新增“历史导入”专用接口与异步编排服务。流水复制不复用文件上传接口语义,而是直接在项目维度重建批次号、批量写入 `ccdi_bank_statement` 和 `ccdi_file_upload_record`,完成后复用 `bankTagService.submitAutoRebuild(...)` 与现有详情页状态轮询。 **Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, MyBatis XML, MySQL 8, JUnit 5, Mockito, Maven ## 后端验收清单 - `GET /ccdi/project/history` 仅返回已完成、已归档项目,支持项目名搜索 - `POST /ccdi/project/import` 能先创建新项目,再异步提交历史导入任务 - 历史导入按 `trxDate` 过滤流水,并按现有唯一性口径去重 - 新项目中的历史导入文件记录带来源标识,且不允许删除 - 历史导入完成后自动触发打标、结果表刷新和统计刷新 --- ### Task 1: 先用 SQL/Mapper 测试锁定历史导入所需数据结构 **Files:** - Create: `sql/migration/2026-03-29-add-file-upload-history-import-source-fields.sql` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiFileUploadRecord.java` - Modify: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFileUploadRecordMapper.xml` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiFileUploadRecordMapper.java` - Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapperXmlTest.java` - [ ] **Step 1: Write the failing mapper/XML assertions** 在 `CcdiBankStatementMapperXmlTest.java` 或新增紧邻的 XML 测试中,先锁定以下点: ```java assertTrue(xml.contains("source_type")); assertTrue(xml.contains("source_project_id")); assertTrue(xml.contains("source_project_name")); assertTrue(xml.contains("fur.source_project_name")); ``` 同时确认 `CcdiFileUploadRecord` 实体新增字段后有对应 getter/setter。 - [ ] **Step 2: Run test to verify it fails** Run: ```bash mvn -pl ccdi-project -Dtest=CcdiBankStatementMapperXmlTest test ``` Expected: - `FAIL` - 原因是当前文件记录表和 Mapper 还没有历史导入来源字段 - [ ] **Step 3: Add the minimal schema and mapper support** 在 SQL 脚本中新增: ```sql ALTER TABLE ccdi_file_upload_record ADD COLUMN source_type varchar(32) DEFAULT 'UPLOAD' COMMENT '来源类型', ADD COLUMN source_project_id bigint NULL COMMENT '来源项目ID', ADD COLUMN source_project_name varchar(100) NULL COMMENT '来源项目名称'; ``` 在 `CcdiFileUploadRecord.java` 增加: ```java private String sourceType; private Long sourceProjectId; private String sourceProjectName; ``` 在 `CcdiFileUploadRecordMapper.xml` 的 `resultMap`、`select`、`insertBatch` 中补齐字段映射。 - [ ] **Step 4: Run test to verify it passes** Run: ```bash mvn -pl ccdi-project -Dtest=CcdiBankStatementMapperXmlTest test ``` Expected: - `PASS` - [ ] **Step 5: Commit** ```bash git add sql/migration/2026-03-29-add-file-upload-history-import-source-fields.sql ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiFileUploadRecord.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiFileUploadRecordMapper.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFileUploadRecordMapper.xml ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapperXmlTest.java git commit -m "补充历史导入文件记录字段" ``` ### Task 2: 锁定项目历史列表接口与导入接口契约 **Files:** - Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectImportHistoryDTO.java` - Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectHistoryListItemVO.java` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java` - Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectControllerContractTest.java` - Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectControllerTest.java` - [ ] **Step 1: Write the failing controller contract test** 在 `CcdiProjectControllerContractTest.java` 中新增断言: ```java Method history = CcdiProjectController.class.getMethod("listHistoryProjects", CcdiProjectQueryDTO.class); assertEquals("/history", history.getAnnotation(GetMapping.class).value()[0]); Method importing = CcdiProjectController.class.getMethod("importFromHistory", CcdiProjectImportHistoryDTO.class); assertEquals("/import", importing.getAnnotation(PostMapping.class).value()[0]); ``` 并在控制器测试中锁定返回结构至少包含“项目创建成功后返回新项目数据”。 - [ ] **Step 2: Run test to verify it fails** Run: ```bash mvn -pl ccdi-project -Dtest=CcdiProjectControllerContractTest,CcdiProjectControllerTest test ``` Expected: - `FAIL` - 原因是当前控制器没有历史项目列表和导入提交两个真实接口 - [ ] **Step 3: Add DTO/VO and minimal controller signatures** 在 DTO 中定义: ```java private String projectName; private String description; private List sourceProjectIds; private String startDate; private String endDate; ``` 在 `ICcdiProjectService.java` 中补充: ```java List listHistoryProjects(CcdiProjectQueryDTO queryDTO); CcdiProjectVO importFromHistory(CcdiProjectImportHistoryDTO dto, String operator); ``` 在 `CcdiProjectController.java` 中增加: ```java @GetMapping("/history") public AjaxResult listHistoryProjects(CcdiProjectQueryDTO queryDTO) { ... } @PostMapping("/import") public AjaxResult importFromHistory(@Validated @RequestBody CcdiProjectImportHistoryDTO dto) { ... } ``` - [ ] **Step 4: Run tests to verify the contract passes** Run: ```bash mvn -pl ccdi-project -Dtest=CcdiProjectControllerContractTest,CcdiProjectControllerTest test ``` Expected: - `PASS` - [ ] **Step 5: Commit** ```bash git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectImportHistoryDTO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectHistoryListItemVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectControllerContractTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectControllerTest.java git commit -m "补充历史项目导入接口契约" ``` ### Task 3: 实现历史项目列表查询与项目创建后任务提交流程 **Files:** - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectMapper.java` - Modify: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectMapper.xml` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java` - Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectHistoryImportService.java` - Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectHistoryImportServiceImpl.java` - Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImplTest.java` - [ ] **Step 1: Write the failing service tests** 在 `CcdiProjectServiceImplTest.java` 中新增至少两个场景: ```java @Test void shouldOnlyReturnCompletedAndArchivedHistoryProjects() { ... } @Test void shouldCreateProjectThenSubmitHistoryImportAfterCommit() { ... } ``` 第二个测试至少锁定: - 先调用 `createProject` 生成本地项目 - 仅在事务提交后委托 `historyImportService.submitImport(...)` - [ ] **Step 2: Run test to verify it fails** Run: ```bash mvn -pl ccdi-project -Dtest=CcdiProjectServiceImplTest test ``` Expected: - `FAIL` - [ ] **Step 3: Implement the minimal query and submit flow** 在 `CcdiProjectMapper.xml` 中新增历史项目查询 SQL,条件固定: ```sql where p.status in ('1', '2') ``` 并支持项目名模糊搜索。 在 `CcdiProjectServiceImpl.java` 中实现: ```java public List listHistoryProjects(CcdiProjectQueryDTO queryDTO) { ... } public CcdiProjectVO importFromHistory(CcdiProjectImportHistoryDTO dto, String operator) { CcdiProjectSaveDTO saveDTO = new CcdiProjectSaveDTO(); saveDTO.setProjectName(dto.getProjectName()); saveDTO.setDescription(dto.getDescription()); saveDTO.setConfigType("default"); CcdiProjectVO project = createProject(saveDTO); TransactionSynchronizationManager.registerSynchronization(... historyImportService.submitImport(...)); return project; } ``` 约束: - 不新增导入专用项目状态 - 不在接口内同步执行大批量复制 - 只在事务提交后启动异步导入 - [ ] **Step 4: Run tests to verify it passes** Run: ```bash mvn -pl ccdi-project -Dtest=CcdiProjectServiceImplTest,CcdiProjectControllerTest test ``` Expected: - `PASS` - [ ] **Step 5: Commit** ```bash git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectMapper.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectMapper.xml ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectHistoryImportService.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectHistoryImportServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImplTest.java git commit -m "实现历史项目导入任务提交流程" ``` ### Task 4: 实现流水复制、批次号重映射与来源文件记录生成 **Files:** - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java` - Modify: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiFileUploadRecordMapper.java` - Modify: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFileUploadRecordMapper.xml` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectHistoryImportServiceImpl.java` - Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java` - Create: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectHistoryImportServiceImplTest.java` - [ ] **Step 1: Write the failing history import service tests** 新增 `CcdiProjectHistoryImportServiceImplTest.java`,锁定以下行为: ```java @Test void shouldFilterStatementsByTrxDateAndDeduplicateAcrossSourceProjects() { ... } @Test void shouldGenerateNewBatchIdsAndHistoryImportFileRecordsOnlyForSuccessfulSourceBatches() { ... } ``` 断言重点: - 仅复制命中时间范围的流水 - 新项目 `batchId/logId` 不等于来源批次号 - 文件记录 `sourceType = HISTORY_IMPORT` - 文件记录携带 `sourceProjectId/sourceProjectName` - [ ] **Step 2: Run test to verify it fails** Run: ```bash mvn -pl ccdi-project -Dtest=CcdiProjectHistoryImportServiceImplTest,CcdiFileUploadServiceImplTest test ``` Expected: - `FAIL` - [ ] **Step 3: Implement the minimal copy pipeline** 在 Mapper 中新增只读查询: - 查询来源项目成功文件记录 - 按来源批次和日期区间查询流水 在 `CcdiProjectHistoryImportServiceImpl.java` 中实现: ```java Map newBatchIdMapping = new HashMap<>(); for (SourceBatch batch : matchedBatches) { int newBatchId = nextBatchId(); newBatchIdMapping.put(batch.getSourceLogId(), newBatchId); } ``` 并在复制流水时写入: ```java statement.setProjectId(targetProjectId); statement.setGroupId(targetLsfxProjectId); statement.setBatchId(newBatchId); ``` 在生成文件记录时写入: ```java record.setSourceType("HISTORY_IMPORT"); record.setSourceProjectId(sourceProjectId); record.setSourceProjectName(sourceProjectName); record.setFileStatus("parsed_success"); ``` - [ ] **Step 4: Run tests to verify it passes** Run: ```bash mvn -pl ccdi-project -Dtest=CcdiProjectHistoryImportServiceImplTest,CcdiFileUploadServiceImplTest,CcdiBankStatementMapperXmlTest test ``` Expected: - `PASS` - [ ] **Step 5: Commit** ```bash git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiFileUploadRecordMapper.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFileUploadRecordMapper.xml ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectHistoryImportServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectHistoryImportServiceImplTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java git commit -m "实现历史项目流水复制后端逻辑" ``` ### Task 5: 接入自动打标、删除拦截与回归验证 **Files:** - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectHistoryImportServiceImpl.java` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java` - Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectHistoryImportServiceImplTest.java` - Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java` - Modify: `docs/reports/implementation/2026-03-29-project-import-history-plan-record.md` - [ ] **Step 1: Write the failing tests for auto rebuild and delete guard** 在 `CcdiProjectHistoryImportServiceImplTest.java` 中新增: ```java verify(bankTagService).submitAutoRebuild(targetProjectId, TriggerType.AUTO_BATCH_UPLOAD); ``` 在 `CcdiFileUploadServiceImplTest.java` 中新增: ```java assertThrows(ServiceException.class, () -> service.deleteFileUploadRecord(historyRecordId, 1L)); ``` 并锁定错误信息包含“历史导入文件不支持删除”。 - [ ] **Step 2: Run test to verify it fails** Run: ```bash mvn -pl ccdi-project -Dtest=CcdiProjectHistoryImportServiceImplTest,CcdiFileUploadServiceImplTest test ``` Expected: - `FAIL` - [ ] **Step 3: Implement the minimal rebuild and delete guard** 在历史导入服务复制完成后补充: ```java refreshProjectTargetCount(targetProjectId); bankTagService.submitAutoRebuild(targetProjectId, TriggerType.AUTO_BATCH_UPLOAD); ``` 在 `deleteFileUploadRecord(...)` 中补充: ```java if ("HISTORY_IMPORT".equals(record.getSourceType())) { throw new ServiceException("历史导入文件不支持删除"); } ``` - [ ] **Step 4: Run the backend regression suite** Run: ```bash mvn -pl ccdi-project -Dtest=CcdiProjectControllerTest,CcdiProjectControllerContractTest,CcdiProjectServiceImplTest,CcdiProjectHistoryImportServiceImplTest,CcdiFileUploadServiceImplTest,CcdiBankStatementMapperXmlTest test ``` Expected: - `PASS` - [ ] **Step 5: Commit** ```bash git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectHistoryImportServiceImpl.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectHistoryImportServiceImplTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java docs/reports/implementation/2026-03-29-project-import-history-plan-record.md git commit -m "接通历史项目导入后端闭环" ```