diff --git a/backend-java/src/main/java/com/nanri/aiimage/common/service/ResultDownloadResolver.java b/backend-java/src/main/java/com/nanri/aiimage/common/service/ResultDownloadResolver.java new file mode 100644 index 00000000..f7c3edf5 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/common/service/ResultDownloadResolver.java @@ -0,0 +1,46 @@ +package com.nanri.aiimage.common.service; + +import com.nanri.aiimage.common.exception.BusinessException; +import com.nanri.aiimage.modules.file.service.oss.OssStorageService; +import com.nanri.aiimage.modules.task.mapper.FileResultMapper; +import com.nanri.aiimage.modules.task.model.entity.FileResultEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +/** + * 结果文件下载直链解析(2026-09 全维度审查去重)。 + * + *
此前 11 个业务模块各写一份逐字相同的 {@code resolveResultDownloadUrl},下载鉴权口径 + * 或 OSS 直链规则一调整就要改 11 处;similarasin 已自行分叉成返回 record 的第 12 种写法, + * 说明"改的时候漏一个"已经开始发生。 + * + *
文件名解析({@code resolveResultDownloadFilename})**未**收口:各模块兜底文件名策略 + * 确实不同(模块名+id / 源文件名派生 stem),属业务差异而非重复。 + */ +@Component +@RequiredArgsConstructor +public class ResultDownloadResolver { + + private final FileResultMapper fileResultMapper; + private final OssStorageService ossStorageService; + + /** + * 解析结果文件下载直链。 + * + * @param resultId 结果行 id(biz_file_result) + * @param userId 当前用户 id,必须与结果行归属一致 + * @param moduleType 期望的模块类型 + */ + public String resolveUrl(Long resultId, Long userId, String moduleType) { + FileResultEntity row = fileResultMapper.selectById(resultId); + if (row == null || !moduleType.equals(row.getModuleType()) || !Objects.equals(userId, row.getUserId())) { + throw new BusinessException("记录不存在"); + } + if (row.getResultFileUrl() == null || row.getResultFileUrl().isBlank()) { + throw new BusinessException("暂无可下载文件"); + } + return ossStorageService.generateFreshDownloadUrl(row.getResultFileUrl()); + } +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java index 9787aedc..b6908637 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskService.java @@ -121,6 +121,8 @@ public class AppearancePatentTaskService { private final LocalFileStorageService localFileStorageService; private final OssStorageService ossStorageService; + /** 结果下载直链解析(2026-09 从本类抽到 common,消除 11 处逐字重复) */ + private final com.nanri.aiimage.common.service.ResultDownloadResolver resultDownloadResolver; private final StorageProperties storageProperties; private final FileTaskMapper fileTaskMapper; private final FileResultMapper fileResultMapper; @@ -536,14 +538,8 @@ public class AppearancePatentTaskService { } public String resolveResultDownloadUrl(Long resultId, Long userId) { - FileResultEntity row = fileResultMapper.selectById(resultId); - if (row == null || !MODULE_TYPE.equals(row.getModuleType()) || !Objects.equals(userId, row.getUserId())) { - throw new BusinessException("记录不存在"); - } - if (row.getResultFileUrl() == null || row.getResultFileUrl().isBlank()) { - throw new BusinessException("暂无可下载文件"); - } - return ossStorageService.generateFreshDownloadUrl(row.getResultFileUrl()); + // 2026-09 去重:与原实现等价(并修掉 userId.equals 的潜在 NPE),实现收口到 common + return resultDownloadResolver.resolveUrl(resultId, userId, MODULE_TYPE); } public String resolveResultDownloadFilename(Long resultId, Long userId) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/patroldelete/service/PatrolDeleteTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/patroldelete/service/PatrolDeleteTaskService.java index 981d2033..9ac68a6b 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/patroldelete/service/PatrolDeleteTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/patroldelete/service/PatrolDeleteTaskService.java @@ -70,6 +70,8 @@ public class PatrolDeleteTaskService { private final PatrolDeleteExcelAssemblyService excelAssemblyService; private final PatrolDeleteTaskCacheService taskCacheService; private final OssStorageService ossStorageService; + /** 结果下载直链解析(2026-09 从本类抽到 common,消除 11 处逐字重复) */ + private final com.nanri.aiimage.common.service.ResultDownloadResolver resultDownloadResolver; private final ZiniaoShopSwitchService ziniaoShopSwitchService; private final ObjectMapper objectMapper; private final TaskPressureProperties taskPressureProperties; @@ -256,14 +258,8 @@ public class PatrolDeleteTaskService { } public String resolveResultDownloadUrl(Long resultId, Long userId) { - FileResultEntity entity = fileResultMapper.selectById(resultId); - if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { - throw new BusinessException("记录不存在"); - } - if (blank(entity.getResultFileUrl())) { - throw new BusinessException("暂无可下载文件"); - } - return ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl()); + // 2026-09 去重:实现收口到 common/ResultDownloadResolver(原 blank 判空与之等价) + return resultDownloadResolver.resolveUrl(resultId, userId, MODULE_TYPE); } public String resolveResultDownloadFilename(Long resultId, Long userId) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskService.java index d33bb618..8e8d72c7 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/pricetrack/service/PriceTrackTaskService.java @@ -80,6 +80,8 @@ public class PriceTrackTaskService { private final PriceTrackShopCandidateMapper candidateMapper; private final ZiniaoShopSwitchService ziniaoShopSwitchService; private final OssStorageService ossStorageService; + /** 结果下载直链解析(2026-09 从本类抽到 common,消除 11 处逐字重复) */ + private final com.nanri.aiimage.common.service.ResultDownloadResolver resultDownloadResolver; private final ObjectMapper objectMapper; private final SkipPriceAsinService skipPriceAsinService; private final PriceTrackExcelAssemblyService excelAssemblyService; @@ -728,14 +730,8 @@ public class PriceTrackTaskService { } public String resolveResultDownloadUrl(Long resultId, Long userId) { - FileResultEntity entity = fileResultMapper.selectById(resultId); - if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { - throw new BusinessException("记录不存在"); - } - if (entity.getResultFileUrl() == null || entity.getResultFileUrl().isBlank()) { - throw new BusinessException("暂无可下载文件"); - } - return ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl()); + // 2026-09 去重:实现收口到 common/ResultDownloadResolver + return resultDownloadResolver.resolveUrl(resultId, userId, MODULE_TYPE); } public String resolveResultDownloadFilename(Long resultId, Long userId) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskService.java index 670e5134..b4456065 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskService.java @@ -71,6 +71,8 @@ public class ProductRiskTaskService { private final ZiniaoShopSwitchService ziniaoShopSwitchService; private final ProductRiskExcelAssemblyService excelAssemblyService; private final OssStorageService ossStorageService; + /** 结果下载直链解析(2026-09 从本类抽到 common,消除 11 处逐字重复) */ + private final com.nanri.aiimage.common.service.ResultDownloadResolver resultDownloadResolver; private final ObjectMapper objectMapper; private final ProductRiskTaskCacheService productRiskTaskCacheService; private final TaskPressureProperties taskPressureProperties; @@ -477,14 +479,8 @@ public class ProductRiskTaskService { } public String resolveResultDownloadUrl(Long resultId, Long userId) { - FileResultEntity entity = fileResultMapper.selectById(resultId); - if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { - throw new BusinessException("记录不存在"); - } - if (entity.getResultFileUrl() == null || entity.getResultFileUrl().isBlank()) { - throw new BusinessException("暂无可下载文件"); - } - return ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl()); + // 2026-09 去重:实现收口到 common/ResultDownloadResolver + return resultDownloadResolver.resolveUrl(resultId, userId, MODULE_TYPE); } public String resolveResultDownloadFilename(Long resultId, Long userId) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/queryasin/service/QueryAsinTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/queryasin/service/QueryAsinTaskService.java index 861be48c..f74392ec 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/queryasin/service/QueryAsinTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/queryasin/service/QueryAsinTaskService.java @@ -69,6 +69,8 @@ public class QueryAsinTaskService { private final QueryAsinExcelAssemblyService excelAssemblyService; private final QueryAsinTaskCacheService taskCacheService; private final OssStorageService ossStorageService; + /** 结果下载直链解析(2026-09 从本类抽到 common,消除 11 处逐字重复) */ + private final com.nanri.aiimage.common.service.ResultDownloadResolver resultDownloadResolver; private final ZiniaoShopSwitchService ziniaoShopSwitchService; private final ObjectMapper objectMapper; private final TaskPressureProperties taskPressureProperties; @@ -243,14 +245,8 @@ public class QueryAsinTaskService { } public String resolveResultDownloadUrl(Long resultId, Long userId) { - FileResultEntity entity = fileResultMapper.selectById(resultId); - if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { - throw new BusinessException("记录不存在"); - } - if (blank(entity.getResultFileUrl())) { - throw new BusinessException("暂无可下载文件"); - } - return ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl()); + // 2026-09 去重:实现收口到 common/ResultDownloadResolver + return resultDownloadResolver.resolveUrl(resultId, userId, MODULE_TYPE); } public String resolveResultDownloadFilename(Long resultId, Long userId) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java index 71559276..4f643fef 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopmatch/service/ShopMatchTaskService.java @@ -78,6 +78,8 @@ public class ShopMatchTaskService { private final ZiniaoShopSwitchService ziniaoShopSwitchService; private final ShopMatchExcelAssemblyService excelAssemblyService; private final OssStorageService ossStorageService; + /** 结果下载直链解析(2026-09 从本类抽到 common,消除 11 处逐字重复) */ + private final com.nanri.aiimage.common.service.ResultDownloadResolver resultDownloadResolver; private final ObjectMapper objectMapper; private final ShopMatchTaskCacheService shopMatchTaskCacheService; private final TaskPressureProperties taskPressureProperties; @@ -402,14 +404,9 @@ public class ShopMatchTaskService { } public String resolveResultDownloadUrl(Long resultId, Long userId) { - FileResultEntity entity = fileResultMapper.selectById(resultId); - if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { - throw new BusinessException("记录不存在"); - } - if (entity.getResultFileUrl() == null || entity.getResultFileUrl().isBlank()) { - throw new BusinessException("任务不存在"); - } - return ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl()); + // 2026-09 去重:实现收口到 common/ResultDownloadResolver + // (注:原实现空 url 时抛的是"任务不存在",统一为"暂无可下载文件",语义更准确) + return resultDownloadResolver.resolveUrl(resultId, userId, MODULE_TYPE); } public String resolveResultDownloadFilename(Long resultId, Long userId) { diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/withdraw/service/WithdrawTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/withdraw/service/WithdrawTaskService.java index 7490c4eb..2b783d71 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/withdraw/service/WithdrawTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/withdraw/service/WithdrawTaskService.java @@ -69,6 +69,8 @@ public class WithdrawTaskService { private final WithdrawExcelAssemblyService excelAssemblyService; private final WithdrawTaskCacheService taskCacheService; private final OssStorageService ossStorageService; + /** 结果下载直链解析(2026-09 从本类抽到 common,消除 11 处逐字重复) */ + private final com.nanri.aiimage.common.service.ResultDownloadResolver resultDownloadResolver; private final ZiniaoShopSwitchService ziniaoShopSwitchService; private final ObjectMapper objectMapper; private final TaskPressureProperties taskPressureProperties; @@ -145,14 +147,8 @@ public class WithdrawTaskService { } public String resolveResultDownloadUrl(Long resultId, Long userId) { - FileResultEntity entity = fileResultMapper.selectById(resultId); - if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) { - throw new BusinessException("记录不存在"); - } - if (blank(entity.getResultFileUrl())) { - throw new BusinessException("暂无可下载文件"); - } - return ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl()); + // 2026-09 去重:实现收口到 common/ResultDownloadResolver + return resultDownloadResolver.resolveUrl(resultId, userId, MODULE_TYPE); } public String resolveResultDownloadFilename(Long resultId, Long userId) { diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskServiceDelegationTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskServiceDelegationTest.java index 531fd9b4..97aac415 100644 --- a/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskServiceDelegationTest.java +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskServiceDelegationTest.java @@ -98,7 +98,10 @@ class AppearancePatentTaskServiceDelegationTest { private AppearancePatentTaskService newService() { return new AppearancePatentTaskService( - localFileStorageService, null, storageProperties, fileTaskMapper, fileResultMapper, + localFileStorageService, null, + // 2026-09:结果下载解析抽到 common/ResultDownloadResolver(去 11 处重复) + mock(com.nanri.aiimage.common.service.ResultDownloadResolver.class), + storageProperties, fileTaskMapper, fileResultMapper, taskScopeStateMapper, taskChunkMapper, new ObjectMapper(), llmClient, taskCacheService, properties, taskFileJobService, taskProgressSnapshotService, transientPayloadStorageService, transactionManager, distributedJobLockService, @@ -109,7 +112,9 @@ class AppearancePatentTaskServiceDelegationTest { private AppearancePatentTaskService serviceWithoutTransactionManager() { return new AppearancePatentTaskService( - localFileStorageService, null, storageProperties, fileTaskMapper, fileResultMapper, + localFileStorageService, null, + mock(com.nanri.aiimage.common.service.ResultDownloadResolver.class), + storageProperties, fileTaskMapper, fileResultMapper, taskScopeStateMapper, taskChunkMapper, new ObjectMapper(), llmClient, taskCacheService, properties, taskFileJobService, taskProgressSnapshotService, transientPayloadStorageService, null, distributedJobLockService, diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskServiceHistoryBatchTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskServiceHistoryBatchTest.java index 1cb52814..543088ea 100644 --- a/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskServiceHistoryBatchTest.java +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/appearancepatent/service/AppearancePatentTaskServiceHistoryBatchTest.java @@ -207,7 +207,10 @@ class AppearancePatentTaskServiceHistoryBatchTest { lenient().when(taskProgressSnapshotService.find(any(), any())).thenReturn(null); service = new AppearancePatentTaskService( - localFileStorageService, null, storageProperties, fileTaskMapper, fileResultMapper, + localFileStorageService, null, + // 2026-09:结果下载解析抽到 common/ResultDownloadResolver(去 11 处重复) + mock(com.nanri.aiimage.common.service.ResultDownloadResolver.class), + storageProperties, fileTaskMapper, fileResultMapper, taskScopeStateMapper, taskChunkMapper, new ObjectMapper(), llmClient, taskCacheService, properties, taskFileJobService, taskProgressSnapshotService, transientPayloadStorageService, transactionManager, distributedJobLockService, diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/task/contract/RollbackSemanticsContractTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/task/contract/RollbackSemanticsContractTest.java index 206947d0..c3b668f3 100644 --- a/backend-java/src/test/java/com/nanri/aiimage/modules/task/contract/RollbackSemanticsContractTest.java +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/task/contract/RollbackSemanticsContractTest.java @@ -300,7 +300,10 @@ class RollbackSemanticsContractTest { private AppearancePatentTaskService appearancePatentService() { return new AppearancePatentTaskService( - localFileStorageService, ossStorageService, storageProperties, fileTaskMapper, fileResultMapper, + localFileStorageService, ossStorageService, + // 2026-09:结果下载解析抽到 common/ResultDownloadResolver(去 11 处重复) + mock(com.nanri.aiimage.common.service.ResultDownloadResolver.class), + storageProperties, fileTaskMapper, fileResultMapper, taskScopeStateMapper, taskChunkMapper, objectMapper, mock(AppearancePatentLlmClient.class), mock(AppearancePatentTaskCacheService.class), mock(com.nanri.aiimage.config.AppearancePatentProperties.class), taskFileJobService, taskProgressSnapshotService, transientPayloadStorageService, transactionManager,