From 04b9b1bb967822b29f83849b3dad3d484f50bb9e Mon Sep 17 00:00:00 2001 From: let5sne Date: Tue, 14 Jul 2026 21:43:27 +0800 Subject: [PATCH] fix(aihr): clean up orphaned knowledge uploads --- .../aihr/service/AihrSopSeedService.java | 34 +++++++++++++++---- .../aihr/service/AihrSopSeedServiceTest.java | 13 +++++++ docs/BRD_IMPLEMENTATION_AUDIT.md | 1 + scripts/demo-check.sh | 1 + 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java index f3e0551e..cfde93f4 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java @@ -763,9 +763,14 @@ public class AihrSopSeedService { docId = reuseDuplicateAttach(hit, config.knowledgeId(), fileName); remark = "duplicate:" + hit.matchType() + ":" + insight.classifiedBy(); } else { - SysOssVo oss = ossUploader.get(); - ossId = oss.getOssId(); - docId = upsertAttach(config.knowledgeId(), ossId, fileName); + SysOssVo uploadedOss = ossUploader.get(); + ossId = uploadedOss.getOssId(); + try { + docId = upsertAttach(config.knowledgeId(), ossId, fileName); + } catch (RuntimeException error) { + deleteUploadedOssQuietly(uploadedOss); + throw error; + } remark = "oss-upload:" + insight.classifiedBy(); } try { @@ -804,8 +809,14 @@ public class AihrSopSeedService { String defaultName = video ? "培训视频" : "图片素材"; String knowledgeName = isAutoCategory(category) || category == null || category.isBlank() ? defaultName : cleanCategory(category); KnowledgeConfig config = knowledgeConfig(knowledgeName); - SysOssVo oss = ossUploader.get(); - String docId = upsertAttach(config.knowledgeId(), oss.getOssId(), fileName); + SysOssVo uploadedOss = ossUploader.get(); + String docId; + try { + docId = upsertAttach(config.knowledgeId(), uploadedOss.getOssId(), fileName); + } catch (RuntimeException error) { + deleteUploadedOssQuietly(uploadedOss); + throw error; + } markAttachStatus(config.knowledgeId(), docId, 0, video ? "pending-transcribe" : "pending-ocr"); String hint; if (video) { @@ -816,7 +827,18 @@ public class AihrSopSeedService { : "未配置可用视觉模型,图片已入库标记待处理;在模型管理启用 vision/多模态 chat 模型后重新上传即可解析"; } List tags = video ? List.of("视频", "待转写") : List.of("图片", "待OCR"); - return new UploadResponse(docId, oss.getOssId(), fileName, knowledgeName, 0, hint, tags, List.of()); + return new UploadResponse(docId, uploadedOss.getOssId(), fileName, knowledgeName, 0, hint, tags, List.of()); + } + + private void deleteUploadedOssQuietly(SysOssVo oss) { + if (oss == null || oss.getOssId() == null || ossService == null) { + return; + } + try { + ossService.deleteWithValidByIds(List.of(oss.getOssId()), false); + } catch (Exception cleanupError) { + log.warn("knowledge document OSS cleanup failed ossId={}", oss.getOssId(), cleanupError); + } } private DocumentInsight documentInsight(String fileName, String content, String requestedCategory) { diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java index 17c5f900..1916cfe3 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrSopSeedServiceTest.java @@ -143,6 +143,19 @@ public class AihrSopSeedServiceTest { assertTrue(code.contains("!modelService.visionAllowed()")); } + @Test + @Tag("dev") + public void documentUploadCleansNewOssWhenAttachWriteFails() throws Exception { + Path source = Path.of("src/main/java/org/dromara/aihr/service/AihrSopSeedService.java"); + if (!Files.exists(source)) { + source = Path.of("ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java"); + } + String code = Files.readString(source); + + assertTrue(code.contains("deleteUploadedOssQuietly(uploadedOss)")); + assertTrue(code.contains("private void deleteUploadedOssQuietly(SysOssVo oss)")); + } + @Test @Tag("dev") public void disabledVisionGateStopsBeforeDatabaseLookup() throws Exception { diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index f11deb00..35b22e5a 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -1,5 +1,6 @@ # BRD 功能审查与未完成项记录 +- 2026-07-14 BRD G3 SOP 附件绑定补偿:同步/待处理图片文档此前在 `sys_oss` 上传成功后再写 `aihr_knowledge_attach`,附件写入失败时可能留下无业务引用对象;现仅对“附件尚未成功绑定”的异常路径做 OSS 补偿删除,解析失败后已存在的失败/待处理附件仍保留用于重试,不改变现有文档处理状态机。 - 2026-07-14 BRD G3 OSS 生命周期补偿:候选资料和案例音频此前先写 `sys_oss`、再写业务记录,业务数据库写入失败时可能留下没有业务引用的对象;现对“本次上传新建且尚未绑定”的 `oss_id` 增加失败补偿删除,案例同时移除内存态,补充两条回归测试和 `demo-check` marker。该修复不触碰已有业务资源;SOP 分片/ASR 等复杂异步链路仍需按各自状态机单独核查,不能用本次补偿覆盖全部媒体生命周期。 - 2026-07-14 BRD G3 文件名边界复核:候选资料、案例音频、同步 SOP 文档和 ASR 音频此前虽然分别校验或清理文件名,但仍有路径把原始 `MultipartFile` 直接交给 `sys_oss`,清理后的名字没有贯通到 OSS 元数据;现新增共用 `AihrMultipartFiles` 包装器,四条链路均以清理后的文件名上传/外发,并补单测与 `demo-check` marker。该修复不改变文件类型、大小、权限和业务状态,仍需正式环境抽查 OSS 原始文件名与下载响应头。 diff --git a/scripts/demo-check.sh b/scripts/demo-check.sh index 89996d2c..6eb165ae 100755 --- a/scripts/demo-check.sh +++ b/scripts/demo-check.sh @@ -631,6 +631,7 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "AihrMultipartFiles.withOriginalFilename" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "cleanupFailedUpload" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "AihrMultipartFiles.withOriginalFilename" +contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "deleteUploadedOssQuietly" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSpeechController.java "AihrMultipartFiles.withOriginalFilename" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "o.tenant_id = a.tenant_id" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "where tenant_id = ? and oss_id = ? limit 1"