18 KiB
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 第一列、忽略表头、空行、重复值”:
@Test
void parseIdCardFile_shouldReadFirstSheetFirstColumnAndDeduplicate() throws Exception {
MultipartFile file = createIdCardExcel(
"身份证号",
"110101199001018888",
"",
"110101199001018888",
"110101199001019999"
);
List<String> result = service.parseIdCardFile(file);
assertEquals(List.of("110101199001018888", "110101199001019999"), result);
}
再补一个非法身份证失败测试:
@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 中新增方法:
List<String> parseIdCardFile(MultipartFile file);
创建 CcdiIdCardExcelRow:
@Data
public class CcdiIdCardExcelRow {
@ExcelProperty(index = 0)
private String idCard;
}
在 CcdiFileUploadServiceImpl 中用 EasyExcel 实现最小可用解析逻辑:
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
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 初始化正确:
@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 中新增方法:
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:
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
)
注册事务提交后的异步调度:
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
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 后复用公共流水线”的测试,先验证文件名回写和最终成功:
@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 异常只影响当前记录:
@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 中拆分两段逻辑:
- 文件来源阶段
processFileAsync(...)中只负责上传文件并拿到logIdprocessPullBankInfoAsync(...)中只负责调用fetchInnerFlow并拿到logId
- 公共处理阶段
- 新增
processRecordAfterLogIdReady(...)
公共处理方法至少负责:
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(...) 最小实现:
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
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
为解析接口和提交接口各写一个控制器测试:
@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"));
}
@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 中新增接口:
@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()));
}
@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
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
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 "完成拉取本行信息后端实现与校验"