refactor: 结果下载直链解析去重(11 处 → 1 处)

新增 common/service/ResultDownloadResolver,7 个模块的 resolveResultDownloadUrl
改为委托调用(appearancepatent/queryasin/withdraw/pricetrack/productrisk/
patroldelete/shopmatch)。

顺带修掉两处隐患:
- 原实现用 userId.equals(entity.getUserId()),userId 为 null 时 NPE
  (新实现用 Objects.equals)
- shopmatch 原实现在 url 为空时抛"任务不存在",与语义不符,统一为"暂无可下载文件"

其余 4 处**刻意保留**,因为它们本就不是重复:
- shopdatacrawl:有 validateUserId + requireResultEntity + ensureResultOwner 前置校验
- deletebrand:返回 null 而非抛异常(调用方依赖该语义)
- similarasin:已分叉为返回 record ResultDownloadInfo
- brand:走的是另一套下载路径
文件名解析 resolveResultDownloadFilename 同样没收口:各模块兜底文件名策略确有差异

配套更新 3 个显式构造 Service 的测试(DelegationTest/HistoryBatchTest/
RollbackSemanticsContractTest)注入新依赖的 mock。mvn test 2795 个全绿。
This commit is contained in:
2026-09-14 05:17:10 +08:00
parent ccce03b4d4
commit 24ada70997
11 changed files with 90 additions and 60 deletions
@@ -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 全维度审查去重)。
*
* <p>此前 11 个业务模块各写一份逐字相同的 {@code resolveResultDownloadUrl},下载鉴权口径
* 或 OSS 直链规则一调整就要改 11 处;similarasin 已自行分叉成返回 record 的第 12 种写法,
* 说明"改的时候漏一个"已经开始发生。
*
* <p>文件名解析({@code resolveResultDownloadFilename}**未**收口:各模块兜底文件名策略
* 确实不同(模块名+id / 源文件名派生 stem),属业务差异而非重复。
*/
@Component
@RequiredArgsConstructor
public class ResultDownloadResolver {
private final FileResultMapper fileResultMapper;
private final OssStorageService ossStorageService;
/**
* 解析结果文件下载直链。
*
* @param resultId 结果行 idbiz_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());
}
}
@@ -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) {
@@ -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) {
@@ -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) {
@@ -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) {
@@ -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) {
@@ -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) {
@@ -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) {
@@ -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,
@@ -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,
@@ -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,