新增拉取本行信息前后端实施计划文档
This commit is contained in:
@@ -0,0 +1,493 @@
|
|||||||
|
# Project Detail Pull Bank Info Backend Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Build the backend parse-and-pull workflow for the project detail “拉取本行信息” modal, including ID-card Excel parsing, task submission, thread-pool scheduling, file-upload record updates, and bank-statement ingestion reuse.
|
||||||
|
|
||||||
|
**Architecture:** Extend the existing `CcdiFileUploadController` and `CcdiFileUploadServiceImpl` instead of creating a second task subsystem. Parse身份证文件 on demand, persist one `ccdi_file_upload_record` per身份证 with `accountNos` initialized to the身份证号, then reuse the existing `fileUploadExecutor` plus a shared “logId ready” pipeline to update file name, polling status, and bank statements consistently for both uploaded files and pulled inner-flow tasks.
|
||||||
|
|
||||||
|
**Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, MyBatis XML, EasyExcel 3.3.4, JUnit 5, Mockito
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add parse/submit contracts and write the first failing parse test
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiPullBankInfoSubmitDTO.java`
|
||||||
|
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiIdCardParseVO.java`
|
||||||
|
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiIdCardExcelRow.java`
|
||||||
|
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java`
|
||||||
|
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
在 `CcdiFileUploadServiceImplTest` 中新增身份证文件解析测试,验证“首个 sheet 第一列、忽略表头、空行、重复值”:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Test
|
||||||
|
void parseIdCardFile_shouldReadFirstSheetFirstColumnAndDeduplicate() throws Exception {
|
||||||
|
MultipartFile file = createIdCardExcel(
|
||||||
|
"身份证号",
|
||||||
|
"110101199001018888",
|
||||||
|
"",
|
||||||
|
"110101199001018888",
|
||||||
|
"110101199001019999"
|
||||||
|
);
|
||||||
|
|
||||||
|
List<String> result = service.parseIdCardFile(file);
|
||||||
|
|
||||||
|
assertEquals(List.of("110101199001018888", "110101199001019999"), result);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
再补一个非法身份证失败测试:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Test
|
||||||
|
void parseIdCardFile_shouldRejectInvalidIdCard() throws Exception {
|
||||||
|
MultipartFile file = createIdCardExcel("身份证号", "123456");
|
||||||
|
|
||||||
|
RuntimeException exception = assertThrows(RuntimeException.class, () -> service.parseIdCardFile(file));
|
||||||
|
|
||||||
|
assertTrue(exception.getMessage().contains("身份证"));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest`
|
||||||
|
|
||||||
|
Expected: FAIL because `parseIdCardFile` and the new DTO / VO / Excel row model do not exist yet.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
在 `ICcdiFileUploadService` 中新增方法:
|
||||||
|
|
||||||
|
```java
|
||||||
|
List<String> parseIdCardFile(MultipartFile file);
|
||||||
|
```
|
||||||
|
|
||||||
|
创建 `CcdiIdCardExcelRow`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Data
|
||||||
|
public class CcdiIdCardExcelRow {
|
||||||
|
|
||||||
|
@ExcelProperty(index = 0)
|
||||||
|
private String idCard;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `CcdiFileUploadServiceImpl` 中用 EasyExcel 实现最小可用解析逻辑:
|
||||||
|
|
||||||
|
```java
|
||||||
|
List<CcdiIdCardExcelRow> rows = EasyExcel.read(file.getInputStream(), CcdiIdCardExcelRow.class)
|
||||||
|
.sheet(0)
|
||||||
|
.headRowNumber(1)
|
||||||
|
.doReadSync();
|
||||||
|
```
|
||||||
|
|
||||||
|
然后:
|
||||||
|
|
||||||
|
- `trim()` 去空白
|
||||||
|
- 过滤空值
|
||||||
|
- 用 `LinkedHashSet` 去重保序
|
||||||
|
- 使用 18 位身份证正则校验
|
||||||
|
- 当无有效身份证时抛出 `RuntimeException("首个sheet第一列未解析到有效身份证号")`
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest`
|
||||||
|
|
||||||
|
Expected: PASS for the new parse tests; existing tests remain green.
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiPullBankInfoSubmitDTO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiIdCardParseVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiIdCardExcelRow.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java
|
||||||
|
git commit -m "新增拉取本行信息参数模型与身份证解析能力"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: Add pull-bank-info submission logic and make record initialization fail first
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java`
|
||||||
|
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
|
||||||
|
- Modify: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFileUploadRecordMapper.xml`
|
||||||
|
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
新增“提交拉取任务时先插入上传记录”的测试,验证 `accountNos` 和 `uploadUser` 初始化正确:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Test
|
||||||
|
void submitPullBankInfo_shouldInsertUploadingRecordsWithIdCardAsAccountNo() {
|
||||||
|
CcdiProject project = new CcdiProject();
|
||||||
|
project.setProjectId(PROJECT_ID);
|
||||||
|
project.setLsfxProjectId(LSFX_PROJECT_ID);
|
||||||
|
when(projectMapper.selectById(PROJECT_ID)).thenReturn(project);
|
||||||
|
|
||||||
|
AtomicReference<List<CcdiFileUploadRecord>> inserted = new AtomicReference<>();
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
List<CcdiFileUploadRecord> records = invocation.getArgument(0);
|
||||||
|
for (int i = 0; i < records.size(); i++) {
|
||||||
|
records.get(i).setId((long) (i + 1));
|
||||||
|
}
|
||||||
|
inserted.set(new ArrayList<>(records));
|
||||||
|
return records.size();
|
||||||
|
}).when(recordMapper).insertBatch(any());
|
||||||
|
|
||||||
|
TransactionSynchronizationManager.initSynchronization();
|
||||||
|
try {
|
||||||
|
String batchId = service.submitPullBankInfo(
|
||||||
|
PROJECT_ID,
|
||||||
|
List.of("110101199001018888", "110101199001019999"),
|
||||||
|
"2026-03-01",
|
||||||
|
"2026-03-10",
|
||||||
|
9527L,
|
||||||
|
"admin"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertNotNull(batchId);
|
||||||
|
assertEquals("110101199001018888", inserted.get().get(0).getAccountNos());
|
||||||
|
assertEquals("admin", inserted.get().get(0).getUploadUser());
|
||||||
|
assertEquals("uploading", inserted.get().get(0).getFileStatus());
|
||||||
|
assertEquals(1, TransactionSynchronizationManager.getSynchronizations().size());
|
||||||
|
} finally {
|
||||||
|
TransactionSynchronizationManager.clearSynchronization();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest`
|
||||||
|
|
||||||
|
Expected: FAIL because `submitPullBankInfo` does not exist and `insertBatch` SQL does not persist `accountNos`.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
在 `ICcdiFileUploadService` 中新增方法:
|
||||||
|
|
||||||
|
```java
|
||||||
|
String submitPullBankInfo(Long projectId,
|
||||||
|
List<String> idCards,
|
||||||
|
String startDate,
|
||||||
|
String endDate,
|
||||||
|
Long userId,
|
||||||
|
String username);
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `CcdiFileUploadServiceImpl` 中实现:
|
||||||
|
|
||||||
|
- 校验项目存在且带有 `lsfxProjectId`
|
||||||
|
- 校验日期非空、开始日期不大于结束日期
|
||||||
|
- 校验身份证集合非空
|
||||||
|
- 为每个身份证创建一条 `CcdiFileUploadRecord`
|
||||||
|
- `record.setAccountNos(idCard);`
|
||||||
|
- `record.setFileName(idCard);`
|
||||||
|
- `record.setUploadUser(username);`
|
||||||
|
- `record.setFileStatus("uploading");`
|
||||||
|
- `record.setUploadTime(new Date());`
|
||||||
|
|
||||||
|
在 `CcdiFileUploadRecordMapper.xml` 的 `insertBatch` 中补上 `account_nos`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
insert into ccdi_file_upload_record (
|
||||||
|
project_id, lsfx_project_id, file_name, file_size, file_status,
|
||||||
|
enterprise_names, account_nos, upload_time, upload_user
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
注册事务提交后的异步调度:
|
||||||
|
|
||||||
|
```java
|
||||||
|
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||||
|
@Override
|
||||||
|
public void afterCommit() {
|
||||||
|
CompletableFuture.runAsync(() -> submitPullBankInfoTasks(...));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest`
|
||||||
|
|
||||||
|
Expected: PASS for the new submission test and no regression in existing upload tests.
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFileUploadRecordMapper.xml ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java
|
||||||
|
git commit -m "实现拉取本行信息任务提交与记录初始化"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: Refactor the shared logId pipeline and add the first failing pull-flow test
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- 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/CcdiFileUploadServiceImplTest.java`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
新增“拉取行内流水拿到 `logId` 后复用公共流水线”的测试,先验证文件名回写和最终成功:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Test
|
||||||
|
void processPullBankInfoAsync_shouldUpdateFileNameFromStatusResponse() {
|
||||||
|
when(lsfxClient.fetchInnerFlow(any())).thenReturn(buildFetchInnerFlowResponse(LOG_ID));
|
||||||
|
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||||
|
.thenReturn(buildCheckParseStatusResponse(false));
|
||||||
|
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse("XX身份证.xlsx"));
|
||||||
|
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||||
|
.thenReturn(buildEmptyBankStatementResponse());
|
||||||
|
|
||||||
|
CcdiFileUploadRecord record = buildRecord();
|
||||||
|
service.processPullBankInfoAsync(
|
||||||
|
PROJECT_ID,
|
||||||
|
LSFX_PROJECT_ID,
|
||||||
|
record,
|
||||||
|
"110101199001018888",
|
||||||
|
"2026-03-01",
|
||||||
|
"2026-03-10",
|
||||||
|
9527L
|
||||||
|
);
|
||||||
|
|
||||||
|
verify(recordMapper, atLeastOnce()).updateById(argThat(item ->
|
||||||
|
"XX身份证.xlsx".equals(item.getFileName())));
|
||||||
|
verify(recordMapper, atLeastOnce()).updateById(argThat(item ->
|
||||||
|
"parsed_success".equals(item.getFileStatus())));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
再补一个失败测试,验证 `fetchInnerFlow` 异常只影响当前记录:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Test
|
||||||
|
void processPullBankInfoAsync_shouldMarkParsedFailedWhenFetchInnerFlowThrows() {
|
||||||
|
when(lsfxClient.fetchInnerFlow(any())).thenThrow(new RuntimeException("fetch inner flow failed"));
|
||||||
|
|
||||||
|
CcdiFileUploadRecord record = buildRecord();
|
||||||
|
service.processPullBankInfoAsync(
|
||||||
|
PROJECT_ID,
|
||||||
|
LSFX_PROJECT_ID,
|
||||||
|
record,
|
||||||
|
"110101199001018888",
|
||||||
|
"2026-03-01",
|
||||||
|
"2026-03-10",
|
||||||
|
9527L
|
||||||
|
);
|
||||||
|
|
||||||
|
verify(recordMapper, atLeastOnce()).updateById(argThat(item ->
|
||||||
|
"parsed_failed".equals(item.getFileStatus())));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest`
|
||||||
|
|
||||||
|
Expected: FAIL because `processPullBankInfoAsync` and the shared “logId ready” pipeline do not exist yet.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
在 `CcdiFileUploadServiceImpl` 中拆分两段逻辑:
|
||||||
|
|
||||||
|
1. 文件来源阶段
|
||||||
|
- `processFileAsync(...)` 中只负责上传文件并拿到 `logId`
|
||||||
|
- `processPullBankInfoAsync(...)` 中只负责调用 `fetchInnerFlow` 并拿到 `logId`
|
||||||
|
|
||||||
|
2. 公共处理阶段
|
||||||
|
- 新增 `processRecordAfterLogIdReady(...)`
|
||||||
|
|
||||||
|
公共处理方法至少负责:
|
||||||
|
|
||||||
|
```java
|
||||||
|
private void processRecordAfterLogIdReady(Long projectId,
|
||||||
|
Integer lsfxProjectId,
|
||||||
|
CcdiFileUploadRecord record,
|
||||||
|
Integer logId) {
|
||||||
|
record.setLogId(logId);
|
||||||
|
record.setFileStatus("parsing");
|
||||||
|
recordMapper.updateById(record);
|
||||||
|
boolean parsingComplete = waitForParsingComplete(lsfxProjectId, logId.toString());
|
||||||
|
GetFileUploadStatusResponse statusResponse = lsfxClient.getFileUploadStatus(statusRequest);
|
||||||
|
String fileName = StringUtils.hasText(logItem.getUploadFileName())
|
||||||
|
? logItem.getUploadFileName()
|
||||||
|
: logItem.getDownloadFileName();
|
||||||
|
record.setFileName(fileName);
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`processPullBankInfoAsync(...)` 最小实现:
|
||||||
|
|
||||||
|
```java
|
||||||
|
FetchInnerFlowRequest request = new FetchInnerFlowRequest();
|
||||||
|
request.setGroupId(lsfxProjectId);
|
||||||
|
request.setCustomerNo(idCard);
|
||||||
|
request.setDataChannelCode("ZJRCU");
|
||||||
|
request.setRequestDateId(Integer.parseInt(LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)));
|
||||||
|
request.setDataStartDateId(Integer.parseInt(startDate.replace("-", "")));
|
||||||
|
request.setDataEndDateId(Integer.parseInt(endDate.replace("-", "")));
|
||||||
|
request.setUploadUserId(userId.intValue());
|
||||||
|
```
|
||||||
|
|
||||||
|
从 `FetchInnerFlowResponse.getData()` 里取第一个 `logId` 后进入公共处理方法。
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest`
|
||||||
|
|
||||||
|
Expected: PASS for the new pull-flow tests and the existing file-upload tests.
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java
|
||||||
|
git commit -m "抽取logId后公共处理流水线并接入本行拉取"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 4: Add controller endpoints and fail on controller tests first
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java`
|
||||||
|
- Create: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadControllerTest.java`
|
||||||
|
|
||||||
|
**Step 1: Write the failing test**
|
||||||
|
|
||||||
|
为解析接口和提交接口各写一个控制器测试:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Test
|
||||||
|
void parseIdCardFile_shouldReturnAjaxResultSuccess() {
|
||||||
|
MockMultipartFile file = new MockMultipartFile(
|
||||||
|
"file",
|
||||||
|
"ids.xlsx",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"test".getBytes(StandardCharsets.UTF_8)
|
||||||
|
);
|
||||||
|
when(fileUploadService.parseIdCardFile(file)).thenReturn(List.of("110101199001018888"));
|
||||||
|
|
||||||
|
AjaxResult result = controller.parseIdCardFile(file);
|
||||||
|
|
||||||
|
assertEquals(200, result.get("code"));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Test
|
||||||
|
void pullBankInfo_shouldUseCurrentLoginUserInfo() {
|
||||||
|
CcdiPullBankInfoSubmitDTO dto = new CcdiPullBankInfoSubmitDTO();
|
||||||
|
dto.setProjectId(PROJECT_ID);
|
||||||
|
dto.setIdCards(List.of("110101199001018888"));
|
||||||
|
dto.setStartDate("2026-03-01");
|
||||||
|
dto.setEndDate("2026-03-10");
|
||||||
|
|
||||||
|
try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class)) {
|
||||||
|
mocked.when(SecurityUtils::getUserId).thenReturn(9527L);
|
||||||
|
mocked.when(SecurityUtils::getUserName).thenReturn("admin");
|
||||||
|
when(fileUploadService.submitPullBankInfo(PROJECT_ID, dto.getIdCards(), "2026-03-01", "2026-03-10", 9527L, "admin"))
|
||||||
|
.thenReturn("batch-1");
|
||||||
|
|
||||||
|
AjaxResult result = controller.pullBankInfo(dto);
|
||||||
|
|
||||||
|
assertEquals(200, result.get("code"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `mvn test -pl ccdi-project -Dtest=CcdiFileUploadControllerTest`
|
||||||
|
|
||||||
|
Expected: FAIL because the new controller methods do not exist yet.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
在 `CcdiFileUploadController` 中新增接口:
|
||||||
|
|
||||||
|
```java
|
||||||
|
@PostMapping("/parse-id-card-file")
|
||||||
|
public AjaxResult parseIdCardFile(@RequestParam MultipartFile file) {
|
||||||
|
List<String> idCards = fileUploadService.parseIdCardFile(file);
|
||||||
|
return AjaxResult.success("解析成功", new CcdiIdCardParseVO(idCards, idCards.size()));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
@PostMapping("/pull-bank-info")
|
||||||
|
public AjaxResult pullBankInfo(@RequestBody CcdiPullBankInfoSubmitDTO dto) {
|
||||||
|
Long userId = SecurityUtils.getUserId();
|
||||||
|
String username = SecurityUtils.getUserName();
|
||||||
|
String batchId = fileUploadService.submitPullBankInfo(
|
||||||
|
dto.getProjectId(),
|
||||||
|
dto.getIdCards(),
|
||||||
|
dto.getStartDate(),
|
||||||
|
dto.getEndDate(),
|
||||||
|
userId,
|
||||||
|
username
|
||||||
|
);
|
||||||
|
return AjaxResult.success("拉取任务已提交", batchId);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
补上参数校验和 Swagger 注释。
|
||||||
|
|
||||||
|
**Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `mvn test -pl ccdi-project -Dtest=CcdiFileUploadControllerTest`
|
||||||
|
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadControllerTest.java
|
||||||
|
git commit -m "新增拉取本行信息后端接口"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 5: Verify the backend end-to-end inside the module
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify if needed after failures: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java`
|
||||||
|
- Modify if needed after failures: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
|
||||||
|
- Modify if needed after failures: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFileUploadRecordMapper.xml`
|
||||||
|
- Modify if needed after failures: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
|
||||||
|
- Modify if needed after failures: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadControllerTest.java`
|
||||||
|
|
||||||
|
**Step 1: Run focused backend tests**
|
||||||
|
|
||||||
|
Run: `mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest,CcdiFileUploadControllerTest`
|
||||||
|
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
**Step 2: Run module compile**
|
||||||
|
|
||||||
|
Run: `mvn clean compile -pl ccdi-project -am`
|
||||||
|
|
||||||
|
Expected: BUILD SUCCESS
|
||||||
|
|
||||||
|
**Step 3: Run the existing upload regression tests**
|
||||||
|
|
||||||
|
Run: `mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest`
|
||||||
|
|
||||||
|
Expected: PASS with no regression on file-upload flow, no MyBatis binding error, and no missing mapper statements.
|
||||||
|
|
||||||
|
**Step 4: Fix the smallest failing point if verification breaks**
|
||||||
|
|
||||||
|
优先排查:
|
||||||
|
|
||||||
|
- `insertBatch` 的 XML 列顺序与实体字段不一致
|
||||||
|
- `uploadFileName` / `downloadFileName` 回写逻辑遗漏空值判断
|
||||||
|
- `FetchInnerFlowResponse` 取 `logId` 时未处理空列表
|
||||||
|
- `Long userId` 转 `Integer uploadUserId` 时的空值或溢出保护
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFileUploadRecordMapper.xml ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadControllerTest.java
|
||||||
|
git commit -m "完成拉取本行信息后端实现与校验"
|
||||||
|
```
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
# Project Detail Pull Bank Info Frontend Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Build the “拉取本行信息” modal on the project detail upload page, including ID-card Excel auto-parse and backfill, date-range submission, and reuse of the existing upload-record polling refresh flow.
|
||||||
|
|
||||||
|
**Architecture:** Keep the implementation inside the existing `UploadData.vue` page and `ccdiProjectUpload.js` API module instead of introducing a new page or a new API file. Replace the current confirm-only placeholder with a real dialog, call a dedicated parse endpoint as soon as the user chooses an Excel file, merge the returned身份证集合 back into the textarea, then submit the final list and reuse the existing statistics, record list, and polling behavior.
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 2.6, Element UI 2.15, Axios request wrapper, existing polling/list refresh logic, `npm run build:prod`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add API contracts and make the build fail first
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `ruoyi-ui/src/api/ccdiProjectUpload.js`
|
||||||
|
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
|
||||||
|
|
||||||
|
**Step 1: Write the failing verification**
|
||||||
|
|
||||||
|
先在 `UploadData.vue` 中把原来的简单确认流程替换成新的 API 引用,但暂时不创建 API 方法:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import {
|
||||||
|
getImportStatus,
|
||||||
|
getNameListOptions,
|
||||||
|
getUploadStatus,
|
||||||
|
pullBankInfo,
|
||||||
|
parseIdCardFile,
|
||||||
|
updateNameListSelection,
|
||||||
|
uploadFile,
|
||||||
|
batchUploadFiles,
|
||||||
|
getFileUploadList,
|
||||||
|
getFileUploadStatistics,
|
||||||
|
} from "@/api/ccdiProjectUpload";
|
||||||
|
```
|
||||||
|
|
||||||
|
并把 `handleFetchBankInfo` 改成只打开弹窗:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
handleFetchBankInfo() {
|
||||||
|
this.pullBankInfoDialogVisible = true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run build to verify it fails**
|
||||||
|
|
||||||
|
Run: `cd ruoyi-ui; npm run build:prod`
|
||||||
|
|
||||||
|
Expected: FAIL because `parseIdCardFile` does not exist in `ccdiProjectUpload.js`, and the new dialog state has not been defined.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
在 `ccdiProjectUpload.js` 中补两个接口:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export function parseIdCardFile(file) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
return request({
|
||||||
|
url: "/ccdi/file-upload/parse-id-card-file",
|
||||||
|
method: "post",
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export function pullBankInfo(data) {
|
||||||
|
return request({
|
||||||
|
url: "/ccdi/file-upload/pull-bank-info",
|
||||||
|
method: "post",
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:把原来 `pullBankInfo(projectId)` 的签名改成 JSON 提交,不再走 `/ccdi/project/{projectId}/pull-bank-info` 占位接口。
|
||||||
|
|
||||||
|
**Step 4: Run build to verify it still only fails on missing dialog state**
|
||||||
|
|
||||||
|
Run: `cd ruoyi-ui; npm run build:prod`
|
||||||
|
|
||||||
|
Expected: FAIL only because `UploadData.vue` 还没有新增弹窗数据和模板绑定。
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ruoyi-ui/src/api/ccdiProjectUpload.js ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue
|
||||||
|
git commit -m "补充拉取本行信息前端接口契约"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: Build the modal shell and page state
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
|
||||||
|
|
||||||
|
**Step 1: Write the failing verification**
|
||||||
|
|
||||||
|
在模板里新增弹窗骨架,但先不实现方法:
|
||||||
|
|
||||||
|
- `el-dialog` 标题:`拉取本行信息`
|
||||||
|
- `el-input type="textarea"` 用于证件号码输入
|
||||||
|
- `el-upload` 用于身份证文件上传
|
||||||
|
- `el-date-picker type="daterange"` 用于时间跨度
|
||||||
|
- 底部按钮:`取消`、`确认拉取`
|
||||||
|
|
||||||
|
使用以下数据字段:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
pullBankInfoDialogVisible: false,
|
||||||
|
pullBankInfoLoading: false,
|
||||||
|
parsingIdCardFile: false,
|
||||||
|
idCardFileList: [],
|
||||||
|
pullBankInfoForm: {
|
||||||
|
idCardText: "",
|
||||||
|
dateRange: []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run build to verify it fails**
|
||||||
|
|
||||||
|
Run: `cd ruoyi-ui; npm run build:prod`
|
||||||
|
|
||||||
|
Expected: FAIL because the template references `pullBankInfoDialogVisible`, `pullBankInfoForm`, and upload handlers that are not implemented yet.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
在 `data()` 中补齐新状态,并实现基础弹窗方法:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
openPullBankInfoDialog() {
|
||||||
|
this.pullBankInfoDialogVisible = true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
resetPullBankInfoForm() {
|
||||||
|
this.pullBankInfoForm = {
|
||||||
|
idCardText: "",
|
||||||
|
dateRange: []
|
||||||
|
};
|
||||||
|
this.idCardFileList = [];
|
||||||
|
this.parsingIdCardFile = false;
|
||||||
|
this.pullBankInfoLoading = false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
同时调整 `handleFetchBankInfo()` 改为:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
handleFetchBankInfo() {
|
||||||
|
this.resetPullBankInfoForm();
|
||||||
|
this.openPullBankInfoDialog();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Run build to verify it passes**
|
||||||
|
|
||||||
|
Run: `cd ruoyi-ui; npm run build:prod`
|
||||||
|
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue
|
||||||
|
git commit -m "搭建拉取本行信息弹窗骨架"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: Implement instant Excel parsing and textarea backfill
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
|
||||||
|
|
||||||
|
**Step 1: Write the failing verification**
|
||||||
|
|
||||||
|
把文件上传控件接到实际事件,但先不写实现:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<el-upload
|
||||||
|
action="#"
|
||||||
|
:auto-upload="false"
|
||||||
|
:limit="1"
|
||||||
|
:file-list="idCardFileList"
|
||||||
|
:on-change="handleIdCardFileChange"
|
||||||
|
:on-remove="handleIdCardFileRemove">
|
||||||
|
</el-upload>
|
||||||
|
```
|
||||||
|
|
||||||
|
并在文件列表下面显示解析提示:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div v-if="parsingIdCardFile">正在解析身份证文件...</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run build to verify it fails**
|
||||||
|
|
||||||
|
Run: `cd ruoyi-ui; npm run build:prod`
|
||||||
|
|
||||||
|
Expected: FAIL because `handleIdCardFileChange` and `handleIdCardFileRemove` do not exist yet.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
实现 4 个前端辅助方法:
|
||||||
|
|
||||||
|
1. `parseIdCardText(text)`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
parseIdCardText(text) {
|
||||||
|
return Array.from(new Set(
|
||||||
|
(text || "")
|
||||||
|
.split(/[\n,,]+/)
|
||||||
|
.map(item => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. `mergeIdCards(currentText, parsedIdCards)`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
mergeIdCards(currentText, parsedIdCards) {
|
||||||
|
const merged = [
|
||||||
|
...this.parseIdCardText(currentText),
|
||||||
|
...(parsedIdCards || [])
|
||||||
|
];
|
||||||
|
return Array.from(new Set(merged)).join(", ");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. `handleIdCardFileChange(file, fileList)`
|
||||||
|
- 只保留一个文件
|
||||||
|
- 校验扩展名为 `.xls` / `.xlsx`
|
||||||
|
- 设置 `parsingIdCardFile = true`
|
||||||
|
- 调用 `parseIdCardFile(file.raw)`
|
||||||
|
- 成功后把返回的 `idCards` 合并回填到 `pullBankInfoForm.idCardText`
|
||||||
|
- 失败后提示错误并清空文件列表
|
||||||
|
|
||||||
|
4. `handleIdCardFileRemove()`
|
||||||
|
- 清空 `idCardFileList`
|
||||||
|
|
||||||
|
解析成功后的关键回填逻辑:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
this.pullBankInfoForm.idCardText = this.mergeIdCards(
|
||||||
|
this.pullBankInfoForm.idCardText,
|
||||||
|
res.data.idCards || []
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 4: Run build to verify it passes**
|
||||||
|
|
||||||
|
Run: `cd ruoyi-ui; npm run build:prod`
|
||||||
|
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue
|
||||||
|
git commit -m "实现身份证文件自动解析与输入框回填"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 4: Submit the final pull request and reuse the existing polling refresh flow
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
|
||||||
|
|
||||||
|
**Step 1: Write the failing verification**
|
||||||
|
|
||||||
|
先把“确认拉取”按钮接到真正的方法名,但先不写逻辑:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<el-button type="primary" :loading="pullBankInfoLoading" @click="handleConfirmPullBankInfo">
|
||||||
|
确认拉取
|
||||||
|
</el-button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Run build to verify it fails**
|
||||||
|
|
||||||
|
Run: `cd ruoyi-ui; npm run build:prod`
|
||||||
|
|
||||||
|
Expected: FAIL because `handleConfirmPullBankInfo` does not exist yet.
|
||||||
|
|
||||||
|
**Step 3: Write minimal implementation**
|
||||||
|
|
||||||
|
实现提交前的最终整理与校验:
|
||||||
|
|
||||||
|
1. `buildFinalIdCardList()`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
buildFinalIdCardList() {
|
||||||
|
return this.parseIdCardText(this.pullBankInfoForm.idCardText);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. `handleConfirmPullBankInfo()`
|
||||||
|
- 校验证件号码非空
|
||||||
|
- 校验 `dateRange` 长度为 2
|
||||||
|
- 组装请求体:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const [startDate, endDate] = this.pullBankInfoForm.dateRange || [];
|
||||||
|
const payload = {
|
||||||
|
projectId: this.projectId,
|
||||||
|
idCards: this.buildFinalIdCardList(),
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
- 调用 `pullBankInfo(payload)`
|
||||||
|
- 成功后:
|
||||||
|
- 关闭弹窗
|
||||||
|
- 提示“拉取任务已提交”
|
||||||
|
- `await Promise.all([this.loadStatistics(), this.loadFileList()])`
|
||||||
|
- 若有 `uploading` / `parsing` 记录则执行 `this.startPolling()`
|
||||||
|
|
||||||
|
失败后:
|
||||||
|
- 保留弹窗内容
|
||||||
|
- 显示后端错误信息
|
||||||
|
|
||||||
|
**Step 4: Run build to verify it passes**
|
||||||
|
|
||||||
|
Run: `cd ruoyi-ui; npm run build:prod`
|
||||||
|
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue
|
||||||
|
git commit -m "接通拉取本行信息提交流程与列表刷新"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 5: Final verification and manual smoke-check
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify if needed after failures: `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
|
||||||
|
- Modify if needed after failures: `ruoyi-ui/src/api/ccdiProjectUpload.js`
|
||||||
|
|
||||||
|
**Step 1: Run production build**
|
||||||
|
|
||||||
|
Run: `cd ruoyi-ui; npm run build:prod`
|
||||||
|
|
||||||
|
Expected: BUILD SUCCESS
|
||||||
|
|
||||||
|
**Step 2: Manual smoke in the browser**
|
||||||
|
|
||||||
|
手工验证以下场景:
|
||||||
|
|
||||||
|
1. 打开项目详情页 `上传数据`
|
||||||
|
2. 点击“拉取本行信息”,确认弹窗打开
|
||||||
|
3. 手工输入两个身份证,确认文本框保留原值
|
||||||
|
4. 上传身份证 Excel,确认自动解析并把去重后的身份证回填到文本框
|
||||||
|
5. 不选日期时点击“确认拉取”,确认拦截
|
||||||
|
6. 选择日期后提交,确认弹窗关闭、提示提交成功
|
||||||
|
7. 确认上传记录列表新增记录并进入 `上传中 / 解析中`
|
||||||
|
8. 确认已有轮询逻辑能自动刷新状态
|
||||||
|
|
||||||
|
**Step 3: Fix the smallest UI or data-binding issue**
|
||||||
|
|
||||||
|
优先排查:
|
||||||
|
|
||||||
|
- 日期控件 `value-format` 是否返回 `yyyy-MM-dd`
|
||||||
|
- 文件移除后是否错误保留旧文件列表
|
||||||
|
- 文本框合并去重后是否出现多余逗号或空白
|
||||||
|
- 提交成功后是否忘记重置弹窗状态
|
||||||
|
|
||||||
|
**Step 4: Run final build again**
|
||||||
|
|
||||||
|
Run: `cd ruoyi-ui; npm run build:prod`
|
||||||
|
|
||||||
|
Expected: BUILD SUCCESS
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add ruoyi-ui/src/api/ccdiProjectUpload.js ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue
|
||||||
|
git commit -m "完成拉取本行信息前端弹窗与自动解析"
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user