diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCreditInfoServiceImpl.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCreditInfoServiceImpl.java index 6bc04706..964bb816 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCreditInfoServiceImpl.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCreditInfoServiceImpl.java @@ -21,6 +21,7 @@ import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse; import com.ruoyi.lsfx.domain.response.CreditParsePayload; import com.ruoyi.lsfx.domain.response.CreditParseResponse; import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -33,6 +34,7 @@ import java.util.Map; /** * 征信维护服务实现 */ +@Slf4j @Service public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService { @@ -150,7 +152,7 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService { private void handleSingleFile(MultipartFile multipartFile, String userName, CallerContext caller) throws Exception { CreditHtmlStorageService.StoredCreditHtml storedHtml = creditHtmlStorageService.save(multipartFile); - CreditParseInvokeResponse response = creditParseClient.parse(caller, storedHtml.remotePath()); + CreditParseInvokeResponse response = parseAndDeleteLocalFile(caller, storedHtml); CreditParsePayload payload = requireResponse(response).getPayload(); Map header = requireHeader(payload); String personId = stringValue(header.get("query_cert_no")); @@ -164,6 +166,25 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService { replaceEmployeeCredit(personId, debts, negative, userName); } + private CreditParseInvokeResponse parseAndDeleteLocalFile( + CallerContext caller, CreditHtmlStorageService.StoredCreditHtml storedHtml) throws Exception { + CreditParseInvokeResponse response; + try { + response = creditParseClient.parse(caller, storedHtml.remotePath()); + } catch (Exception parseException) { + try { + creditHtmlStorageService.delete(storedHtml); + } catch (Exception deleteException) { + parseException.addSuppressed(deleteException); + log.error("征信解析失败后清理本地文件失败", deleteException); + } + throw parseException; + } + + creditHtmlStorageService.delete(storedHtml); + return response; + } + private void validateHtmlFile(MultipartFile file) { String originalFilename = file == null ? null : file.getOriginalFilename(); if (originalFilename == null) { diff --git a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/CreditHtmlStorageService.java b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/CreditHtmlStorageService.java index 9d0a02d0..bcaade35 100644 --- a/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/CreditHtmlStorageService.java +++ b/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/CreditHtmlStorageService.java @@ -8,6 +8,10 @@ import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; /** * 征信 HTML 服务器落盘与远程访问地址生成。 @@ -16,6 +20,7 @@ import java.io.File; public class CreditHtmlStorageService { private static final String CREDIT_HTML_DIR = "credit-html"; + private static final String CREDIT_HTML_PROFILE_PREFIX = "/profile/" + CREDIT_HTML_DIR + "/"; private static final String[] HTML_EXTENSIONS = {"html", "htm"}; @Value("${credit-parse.api.file-public-base-url}") @@ -26,6 +31,25 @@ public class CreditHtmlStorageService { return new StoredCreditHtml(profilePath, buildRemotePath(profilePath)); } + public void delete(StoredCreditHtml storedHtml) throws IOException { + String profilePath = storedHtml == null ? null : storedHtml.profilePath(); + if (StringUtils.isBlank(profilePath) || !profilePath.startsWith(CREDIT_HTML_PROFILE_PREFIX)) { + throw new IllegalArgumentException("征信HTML本地文件路径非法"); + } + + String relativePath = profilePath.substring(CREDIT_HTML_PROFILE_PREFIX.length()); + if (StringUtils.isBlank(relativePath)) { + throw new IllegalArgumentException("征信HTML本地文件路径非法"); + } + + Path baseDir = Paths.get(getCreditHtmlBaseDir()).toAbsolutePath().normalize(); + Path targetFile = baseDir.resolve(relativePath).normalize(); + if (!targetFile.startsWith(baseDir)) { + throw new IllegalArgumentException("征信HTML本地文件路径越界"); + } + Files.deleteIfExists(targetFile); + } + private String getCreditHtmlBaseDir() { return RuoYiConfig.getProfile() + File.separator + CREDIT_HTML_DIR; } diff --git a/docs/plans/backend/2026-07-20-credit-html-local-cleanup-backend-plan.md b/docs/plans/backend/2026-07-20-credit-html-local-cleanup-backend-plan.md new file mode 100644 index 00000000..2b930f7d --- /dev/null +++ b/docs/plans/backend/2026-07-20-credit-html-local-cleanup-backend-plan.md @@ -0,0 +1,24 @@ +# 征信本地文件清理后端实施计划 + +## 目标 + +征信 HTML 上传后仅作为外部解析接口的临时输入。解析调用结束后立即删除本地文件,避免成功或失败流程残留敏感征信文件。 + +## 实施内容 + +1. 在 `CreditHtmlStorageService` 增加受限删除能力,仅允许删除 `ruoyi.profile/credit-html` 目录内的文件。 +2. 在征信上传单文件流程中,解析成功后先删除本地文件,再校验解析结果并入库。 +3. 解析失败时仍执行删除;解析与删除同时失败时保留原始解析错误,并记录删除异常。 +4. 删除失败时终止当前文件后续处理,由现有批量上传逻辑记录失败并继续处理其他文件。 + +## 影响范围 + +- 仅修改征信维护上传的后端处理流程。 +- 不修改上传接口、返回结构、数据库和前端页面。 +- `/lsfx/credit/parse` 外部地址测试接口不管理本地上传文件,保持不变。 + +## 验证 + +- 覆盖成功、解析失败、结果校验失败、入库失败和删除失败场景。 +- 验证删除路径不能逃逸 `ruoyi.profile/credit-html`。 +- 运行征信维护定向测试及 `ccdi-info-collection` 模块测试。 diff --git a/docs/reports/implementation/2026-07-20-credit-html-local-cleanup-backend-implementation.md b/docs/reports/implementation/2026-07-20-credit-html-local-cleanup-backend-implementation.md new file mode 100644 index 00000000..cd5c680f --- /dev/null +++ b/docs/reports/implementation/2026-07-20-credit-html-local-cleanup-backend-implementation.md @@ -0,0 +1,41 @@ +# 征信本地文件清理后端实施记录 + +## 修改内容 + +- 为征信 HTML 存储服务增加本地文件删除能力,并限制删除范围为 `ruoyi.profile/credit-html`。 +- 征信上传调用外部解析接口后立即删除本地文件,再继续解析结果校验与入库。 +- 解析失败时同样清理文件;清理异常不覆盖原始解析错误。 +- 删除失败时当前文件不再入库,批量上传继续处理后续文件。 + +## 影响范围 + +- 后端模块:`ccdi-info-collection` +- 前端、接口协议、数据库结构均无变更。 +- 外部 `remotePath` 测试接口无变更。 + +## 验证情况 + +### 定向测试 + +执行命令: + +```bash +mvn -pl ccdi-info-collection -am -Dtest=CcdiCreditInfoServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test +``` + +结果:通过,共执行 9 个用例,失败 0、错误 0。已覆盖解析成功、解析失败、结果校验失败、入库失败、删除失败、批量继续处理、真实文件删除、重复删除和越界路径拒绝。 + +### 模块测试 + +执行命令: + +```bash +mvn -pl ccdi-info-collection -am test +``` + +结果:共执行 178 个用例,本次修改相关测试全部通过;模块整体存在 1 个失败和 1 个错误: + +- `CcdiBaseStaffMapperTest.mapperXml_shouldUseStableOrderForBaseStaffPagination`:员工 Mapper 排序契约断言失败。 +- `CcdiBaseStaffServiceImplTest.selectBaseStaffById_shouldReturnSelfOwnedAssetInfoList`:员工详情测试返回空对象并触发空指针。 + +上述失败位于未修改的员工信息代码与测试中,与本次征信本地文件清理无关,本次未扩大范围处理。