修复流水上传身份证号回填

This commit is contained in:
wjj
2026-07-20 11:03:05 +08:00
parent b70f967153
commit 4a75d68bf6
4 changed files with 144 additions and 3 deletions

View File

@@ -46,6 +46,7 @@ import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
@@ -819,7 +820,9 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
enterpriseNamesStr, accountNosStr);
log.info("【文件上传】步骤7: 获取流水数据");
FetchBankStatementResult fetchResult = fetchAndSaveBankStatements(projectId, lsfxProjectId, logId);
String fallbackCretNo = extractIdCardFromFileName(record.getFileName());
FetchBankStatementResult fetchResult = fetchAndSaveBankStatements(projectId, lsfxProjectId, logId,
fallbackCretNo);
if (!fetchResult.isSuccess()) {
updateFailedRecord(record, fetchResult.getErrorMessage());
return;
@@ -892,7 +895,8 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
* @param logId 文件ID
*/
private FetchBankStatementResult fetchAndSaveBankStatements(Long projectId, Integer groupId,
Integer logId) {
Integer logId,
String fallbackCretNo) {
log.info("【文件上传】开始获取流水数据: projectId={}, groupId={}, logId={}",
projectId, groupId, logId);
@@ -957,6 +961,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
if (statement != null) {
statement.setBatchId(logId);
statement.setProjectId(projectId);
fillMissingCretNo(statement, fallbackCretNo);
normalizeDedupFields(statement);
batchList.add(statement);
@@ -1000,6 +1005,22 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
}
}
private String extractIdCardFromFileName(String fileName) {
if (!StringUtils.hasText(fileName)) {
return null;
}
String mainFileName = getMainFileName(fileName);
Matcher matcher = UPLOAD_FILE_NAME_ID_CARD_PATTERN.matcher(mainFileName);
return matcher.find() ? matcher.group() : null;
}
private void fillMissingCretNo(CcdiBankStatement statement, String fallbackCretNo) {
if (statement == null || StringUtils.hasText(statement.getCretNo()) || !StringUtils.hasText(fallbackCretNo)) {
return;
}
statement.setCretNo(fallbackCretNo);
}
private void cleanupBankStatements(Long projectId, Integer logId) {
bankStatementMapper.deleteByProjectIdAndBatchId(projectId, logId);
}

View File

@@ -6,6 +6,7 @@ import ch.qos.logback.core.read.ListAppender;
import com.alibaba.excel.EasyExcel;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.enums.TriggerType;
import com.ruoyi.ccdi.project.domain.entity.CcdiBankStatement;
import com.ruoyi.ccdi.project.domain.vo.CcdiFileUploadStatisticsVO;
import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord;
import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
@@ -470,6 +471,44 @@ class CcdiFileUploadServiceImplTest {
), eq("原始流水.xlsx"));
}
@Test
void processFileAsync_shouldBackfillMissingCretNoFromUploadFileName() throws IOException {
AtomicReference<List<CcdiBankStatement>> insertedStatements = new AtomicReference<>();
doAnswer(invocation -> {
List<CcdiBankStatement> statements = invocation.getArgument(0);
insertedStatements.set(new ArrayList<>(statements));
return statements.size();
}).when(bankStatementMapper).insertBatch(any());
CcdiProject project = new CcdiProject();
project.setProjectId(PROJECT_ID);
when(projectMapper.selectById(PROJECT_ID)).thenReturn(project);
when(bankStatementMapper.countMatchedStaffCountByProjectId(PROJECT_ID)).thenReturn(1);
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), eq("张三_330101199001010011_流水.xlsx")))
.thenReturn(buildUploadResponse());
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
.thenReturn(buildCheckParseStatusResponse(false));
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
.thenAnswer(invocation -> {
GetBankStatementRequest request = invocation.getArgument(0);
if (Integer.valueOf(1).equals(request.getPageSize())) {
return buildBankStatementCountResponse(1);
}
return buildBankStatementResponseWithBlankCretNo();
});
CcdiFileUploadRecord record = buildRecord();
record.setFileName("张三_330101199001010011_流水.xlsx");
Path tempFile = createTempFile();
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
assertNotNull(insertedStatements.get());
assertEquals(1, insertedStatements.get().size());
assertEquals("330101199001010011", insertedStatements.get().get(0).getCretNo());
}
@Test
void processFileAsync_shouldKeepOriginalFileNameWhenStatusReturnsDifferentName() throws IOException {
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
@@ -717,7 +756,8 @@ class CcdiFileUploadServiceImplTest {
"fetchAndSaveBankStatements",
PROJECT_ID,
LSFX_PROJECT_ID,
LOG_ID
LOG_ID,
null
);
assertTrue(Boolean.TRUE.equals(ReflectionTestUtils.getField(result, "success")));
@@ -929,6 +969,27 @@ class CcdiFileUploadServiceImplTest {
return response;
}
private GetBankStatementResponse buildBankStatementCountResponse(int totalCount) {
GetBankStatementResponse.BankStatementData data = new GetBankStatementResponse.BankStatementData();
data.setTotalCount(totalCount);
GetBankStatementResponse response = new GetBankStatementResponse();
response.setData(data);
return response;
}
private GetBankStatementResponse buildBankStatementResponseWithBlankCretNo() {
GetBankStatementResponse.BankStatementItem item = new GetBankStatementResponse.BankStatementItem();
item.setBankStatementId(1L);
item.setLeName("测试主体");
item.setAccountNo("62220001");
item.setCustomerName("交易对手");
item.setCustomerAccountNo("62220002");
item.setDrAmount(BigDecimal.TEN);
item.setCretNo(null);
return buildBankStatementResponseWithItems(1, List.of(item));
}
private void invokeSubmitTasksAsync(List<String> tempFilePaths,
List<CcdiFileUploadRecord> records,
String batchId) throws Exception {

View File

@@ -0,0 +1,23 @@
# 上传流水身份证号本地回填后端实施计划
## 目标
修复上传行外流水文件后,流水平台返回明细中 `cretNo` 为空导致 `ccdi_bank_statement.cret_no` 为空、结果总览和打标无法按身份证号关联的问题。
## 范围
- 仅调整后端批量上传流水文件链路。
- 不调整拉取本行信息链路。
- 不调整前端页面、数据库表结构、结果总览查询和打标 SQL。
## 实施方案
1. 复用现有文件名空白清理与身份证号识别规则,从归一化后的上传记录文件名中提取 18 位身份证号。
2. 保持转传流水平台的文件名不变,继续使用当前上传记录文件名。
3. 保存平台返回的流水明细时,若 `CcdiBankStatement.cretNo` 为空,则使用当前上传记录文件名中提取到的身份证号回填。
4. 补充单元测试覆盖平台 `cretNo` 为空时本地回填。
## 验证
- 运行 `mvn -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test`
- 必要时运行 `mvn -pl ccdi-project -am -DskipTests compile`

View File

@@ -0,0 +1,36 @@
# 上传流水身份证号本地回填实施记录
## 背景
上传行外流水文件时,文件名已经会校验必须包含 18 位身份证号,但流水平台返回明细中的 `cretNo` 可能为空。当前结果总览、流水打标和专项分析主要依赖 `ccdi_bank_statement.cret_no` 关联员工或关系人,空值会导致上传后有流水但结果出不来。
## 修改内容
- 仅调整后端上传流水文件链路,不修改前端、不修改拉取本行信息链路、不修改数据库结构。
- `CcdiFileUploadServiceImpl`
- 在上传记录文件名中复用现有身份证号识别规则提取 18 位身份证号。
- 保存流水平台返回的流水明细前,若 `CcdiBankStatement.cretNo` 为空,则使用上传记录文件名中的身份证号回填。
- 继续使用原有上传记录文件名转传流水平台,不新增 `身份证号-原文件名` 前缀改写。
- `CcdiFileUploadServiceImplTest`
- 保留“转传流水平台使用记录文件名”的测试。
- 新增平台返回 `cretNo` 为空时,本地入库实体回填上传文件名身份证号的测试。
## 影响范围
- 对平台正常返回 `cretNo` 的流水无影响,仍优先使用平台返回值。
- 对平台未返回 `cretNo`、但上传文件名包含身份证号的流水,入库时会补齐 `ccdi_bank_statement.cret_no`
- 对文件名不含身份证号的上传仍在上传前拦截,不进入落库阶段。
## 验证
```bash
mvn -pl ccdi-project -am "-Dtest=CcdiFileUploadServiceImplTest" "-Dsurefire.failIfNoSpecifiedTests=false" test
```
- 结果BUILD SUCCESS
- 覆盖:上传链路文件名校验、上传平台文件名传递、解析完成后流水入库、`cretNo` 为空时本地回填。
## 备注
- PowerShell 下 Maven `-D` 参数需要加引号;未加引号时命令会被解析为错误生命周期阶段。
- 本次未启动前端或后端服务,因此无测试进程需要关闭。