Files
ccdi/docs/plans/2026-03-11-project-detail-pull-bank-info-backend-implementation.md

18 KiB
Raw Blame History

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

新增“提交拉取任务时先插入上传记录”的测试,验证 accountNosuploadUser 初始化正确:

@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.xmlinsertBatch 中补上 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 中拆分两段逻辑:

  1. 文件来源阶段
  • processFileAsync(...) 中只负责上传文件并拿到 logId
  • processPullBankInfoAsync(...) 中只负责调用 fetchInnerFlow 并拿到 logId
  1. 公共处理阶段
  • 新增 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 回写逻辑遗漏空值判断
  • FetchInnerFlowResponselogId 时未处理空列表
  • Long userIdInteger 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 "完成拉取本行信息后端实现与校验"