Files
ccdi/docs/plans/backend/2026-03-29-project-import-history-backend-implementation.md

438 lines
16 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.
# 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<Long> sourceProjectIds;
private String startDate;
private String endDate;
```
`ICcdiProjectService.java` 中补充:
```java
List<CcdiProjectHistoryListItemVO> 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<CcdiProjectHistoryListItemVO> 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<Integer, Integer> 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 "接通历史项目导入后端闭环"
```