处理修复BUG

This commit is contained in:
super
2026-06-14 16:48:20 +08:00
parent 8672668c33
commit ac19637ad6
18 changed files with 908 additions and 283 deletions

View File

@@ -11,5 +11,6 @@ import java.util.List;
public class ModuleCleanupProperties {
private boolean enabled = true;
private String cron = "0 0 0 * * *";
private List<String> moduleTypes = new ArrayList<>(List.of("DEDUPE", "SPLIT", "CONVERT", "DELETE_BRAND", "PRODUCT_RISK_RESOLVE", "PRICE_TRACK", "SHOP_MATCH", "PATROL_DELETE", "QUERY_ASIN"));
private long retentionDays = 7;
private List<String> moduleTypes = new ArrayList<>(List.of("DEDUPE", "SPLIT", "CONVERT", "DELETE_BRAND", "PRODUCT_RISK_RESOLVE", "PRICE_TRACK", "SHOP_MATCH", "PATROL_DELETE", "QUERY_ASIN", "APPEARANCE_PATENT", "SIMILAR_ASIN", "COLLECT_DATA"));
}

View File

@@ -20,9 +20,11 @@ import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackLoopRunVo;
import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackMatchShopsVo;
import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackPendingDeleteVo;
import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackTaskBatchVo;
import com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo;
import com.nanri.aiimage.modules.pricetrack.service.PriceTrackLoopRunService;
import com.nanri.aiimage.modules.pricetrack.service.PriceTrackService;
import com.nanri.aiimage.modules.pricetrack.service.PriceTrackTaskService;
import com.nanri.aiimage.modules.shopkey.service.SkipPriceAsinService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
@@ -58,6 +60,7 @@ public class PriceTrackController {
private final PriceTrackService priceTrackService;
private final PriceTrackTaskService priceTrackTaskService;
private final PriceTrackLoopRunService priceTrackLoopRunService;
private final SkipPriceAsinService skipPriceAsinService;
@GetMapping("/candidates")
@Operation(summary = "查询备选店铺列表", description = "返回当前用户在跟价模块中保存的备选店铺。")
@@ -104,6 +107,24 @@ public class PriceTrackController {
return ApiResponse.success(priceTrackService.matchShops(request));
}
@GetMapping("/tasks/{taskId}/skip-asins/paginated")
@Operation(
summary = "分页查询任务的跳过 ASIN 数据",
description = "根据任务 ID 分页拉取该任务关联的 ASIN 数据。"
+ "全量模式:从 biz_skip_price_asin 表查询;"
+ "文件模式:从任务 requestJson 中存储的上传文件数据查询。"
+ "国家代码从任务的 requestJson.countryCodes 中获取。"
)
public ApiResponse<SkipPriceAsinPageVo> getTaskSkipAsinsPaginated(
@Parameter(description = "任务 ID", required = true, example = "200")
@PathVariable Long taskId,
@Parameter(description = "页码,从 1 开始", example = "1")
@RequestParam(value = "page", defaultValue = "1") Integer page,
@Parameter(description = "每页条数,默认 1000最大 2000", example = "1000")
@RequestParam(value = "page_size", defaultValue = "1000") Integer pageSize) {
return ApiResponse.success(priceTrackTaskService.getTaskSkipAsinsPaginated(taskId, page, pageSize));
}
@GetMapping("/dashboard")
@Operation(summary = "统计看板", description = "返回备选店铺数、已结束任务数、成功任务数和失败任务数。")
public ApiResponse<PriceTrackDashboardVo> dashboard(

View File

@@ -0,0 +1,35 @@
package com.nanri.aiimage.modules.pricetrack.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 跟价跳过 ASIN 分页返回
*/
@Data
@Schema(description = "跟价跳过 ASIN 分页返回")
public class SkipPriceAsinPageVo {
@Schema(description = "当前页码,从 1 开始")
private Integer page;
@Schema(description = "每页条数")
private Integer pageSize;
@Schema(description = "总记录数")
private Long total;
@Schema(description = "总页数")
private Integer totalPages;
@Schema(description = "按国家分组的 ASIN 列表(简化版,仅 ASIN")
private Map<String, List<String>> skipAsinsByCountry = new LinkedHashMap<>();
@Schema(description = "按国家分组的 ASIN 详情列表(包含 asin 和 minimumPrice")
private Map<String, List<Map<String, String>>> skipAsinDetailsByCountry = new LinkedHashMap<>();
}

View File

@@ -145,24 +145,16 @@ public class PriceTrackService {
}
boolean asinMode = request.getAsinFiles() != null && !request.getAsinFiles().isEmpty();
Map<String, List<String>> skipAsinsByCountry = asinMode
? new LinkedHashMap<>()
: skipPriceAsinService.listAllSkipAsinsByCountry();
Map<String, List<Map<String, String>>> skipAsinDetailsByCountry = asinMode
? new LinkedHashMap<>()
: skipPriceAsinService.listAllSkipAsinDetailsByCountry();
Map<String, List<Map<String, String>>> asinRowsByCountry =
!asinMode
? new LinkedHashMap<>()
: priceTrackTaskService.parseMatchAsinRowsByCountry(
request.getAsinFiles(),
request.getCountryCodes());
// 不再在 matchShops 时加载全量数据,改由 Python 端分页拉取
Map<String, List<String>> skipAsinsByCountry = new LinkedHashMap<>();
Map<String, List<Map<String, String>>> skipAsinDetailsByCountry = new LinkedHashMap<>();
Map<String, List<Map<String, String>>> asinRowsByCountry = new LinkedHashMap<>();
PriceTrackMatchShopsVo vo = new PriceTrackMatchShopsVo();
vo.setSkipAsinsByCountry(skipAsinsByCountry);
vo.setSkipAsinDetailsByCountry(skipAsinDetailsByCountry);
vo.setAsinRowsByCountry(asinRowsByCountry);
vo.setMinimumPriceByCountryAndAsin(priceTrackTaskService.buildMinimumPriceLookupForMatch(asinRowsByCountry));
vo.setMinimumPriceByCountryAndAsin(new LinkedHashMap<>());
for (String shopName : ordered) {
vo.getItems().add(matchOneShop(shopName, skipAsinsByCountry));
}

View File

@@ -428,10 +428,15 @@ public class PriceTrackTaskService {
ctx.put("statusMode", request.isStatusMode());
ctx.put("asinMode", request.isAsinMode());
ctx.put("countryCodes", request.getCountryCodes());
ctx.put("skipAsinsByCountry", skipAsinsByCountry);
ctx.put("skipAsinDetailsByCountry", skipAsinDetailsByCountry);
ctx.put("asinRowsByCountry", asinRowsByCountry);
ctx.put("minimumPriceByCountryAndAsin", minimumPriceByCountryAndAsin);
// 不再保存全量 ASIN 数据到 requestJson
// ctx.put("skipAsinsByCountry", skipAsinsByCountry);
// ctx.put("skipAsinDetailsByCountry", skipAsinDetailsByCountry);
// ctx.put("asinRowsByCountry", asinRowsByCountry);
// ctx.put("minimumPriceByCountryAndAsin", minimumPriceByCountryAndAsin);
// 文件模式:保存文件路径,供后续分页读取
if (request.isAsinMode()) {
ctx.put("asinFiles", request.getAsinFiles());
}
ctx.put("items", uniqueItems);
ctx.put("loopRunId", request.getLoopRunId());
ctx.put("roundIndex", request.getRoundIndex());
@@ -764,6 +769,144 @@ public class PriceTrackTaskService {
return buildMinimumPriceByCountryAndAsin(asinRowsByCountry);
}
/**
* 分页查询任务关联的跳过 ASIN 数据
*
* @param taskId 任务 ID
* @param page 页码,从 1 开始
* @param pageSize 每页条数
* @return 分页结果
*/
public com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo getTaskSkipAsinsPaginated(
Long taskId, int page, int pageSize) {
if (taskId == null || taskId <= 0) {
throw new BusinessException("taskId 不合法");
}
FileTaskEntity task = loadTaskForExecution(taskId);
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
throw new BusinessException("任务不存在");
}
// 解析任务请求参数
PriceTrackCreateTaskRequest request;
try {
if (task.getRequestJson() == null || task.getRequestJson().isBlank()) {
throw new BusinessException("任务请求参数为空");
}
request = objectMapper.readValue(task.getRequestJson(), PriceTrackCreateTaskRequest.class);
} catch (Exception e) {
throw new BusinessException("任务请求参数解析失败: " + e.getMessage());
}
// 从任务中获取国家代码
List<String> countryCodes = request.getCountryCodes();
// 判断是全量模式还是文件模式
boolean isAsinMode = request.isAsinMode();
if (isAsinMode) {
// 文件模式:从任务 requestJson 中的上传文件解析数据并分页返回
return getTaskAsinFilePaginated(request, countryCodes, page, pageSize);
} else {
// 全量模式:从 biz_skip_price_asin 表查询
return skipPriceAsinService.listSkipAsinsPaginated(null, countryCodes, page, pageSize);
}
}
/**
* 文件模式:从任务上传的文件中分页返回 ASIN 数据
*/
private com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo getTaskAsinFilePaginated(
PriceTrackCreateTaskRequest request, List<String> countryCodes, int page, int pageSize) {
if (page < 1) {
page = 1;
}
if (pageSize < 1 || pageSize > 2000) {
pageSize = 1000;
}
// 解析上传文件中的所有 ASIN 数据
Map<String, List<Map<String, String>>> allAsinRows = parseAsinRowsByCountry(
request.getAsinFiles(),
request.getCountryCodes()
);
// 按国家分组并分页
List<String> targetCountries = (countryCodes == null || countryCodes.isEmpty())
? new ArrayList<>(allAsinRows.keySet())
: countryCodes;
// 计算总记录数(所有国家的 ASIN 行数总和)
long totalRows = 0;
for (String country : targetCountries) {
List<Map<String, String>> rows = allAsinRows.getOrDefault(country, List.of());
totalRows += rows.size();
}
// 计算分页
int totalPages = (int) ((totalRows + pageSize - 1) / pageSize);
int startIndex = (page - 1) * pageSize;
int endIndex = Math.min(startIndex + pageSize, (int) totalRows);
// 提取当前页的数据(跨国家顺序提取)
Map<String, List<String>> skipAsinsByCountry = new LinkedHashMap<>();
Map<String, List<Map<String, String>>> skipDetailsByCountry = new LinkedHashMap<>();
// 初始化所有目标国家
for (String country : targetCountries) {
skipAsinsByCountry.put(country, new ArrayList<>());
skipDetailsByCountry.put(country, new ArrayList<>());
}
int currentIndex = 0;
boolean shouldBreak = false;
for (String country : targetCountries) {
if (shouldBreak) {
break;
}
List<Map<String, String>> countryRows = allAsinRows.getOrDefault(country, List.of());
for (Map<String, String> row : countryRows) {
// 判断当前行是否在分页范围内
if (currentIndex >= startIndex && currentIndex < endIndex) {
String asin = row.get("asin");
String minimumPrice = row.get("minimumPrice");
if (asin != null && !asin.isBlank()) {
skipAsinsByCountry.get(country).add(asin);
Map<String, String> detail = new LinkedHashMap<>();
detail.put("asin", asin);
detail.put("minimumPrice", minimumPrice == null ? "" : minimumPrice);
skipDetailsByCountry.get(country).add(detail);
}
}
currentIndex++;
// 已达到本页结束位置
if (currentIndex >= endIndex) {
shouldBreak = true;
break;
}
}
}
// 构造返回 VO
com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo vo =
new com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo();
vo.setPage(page);
vo.setPageSize(pageSize);
vo.setTotal(totalRows);
vo.setTotalPages(totalPages);
vo.setSkipAsinsByCountry(skipAsinsByCountry);
vo.setSkipAsinDetailsByCountry(skipDetailsByCountry);
return vo;
}
private Map<String, List<Map<String, String>>> parseAsinRowsByCountry(List<String> asinFiles, List<String> countryCodes) {
Map<String, List<Map<String, String>>> merged = new LinkedHashMap<>();
if (asinFiles == null || asinFiles.isEmpty()) {

View File

@@ -51,7 +51,7 @@ public class ProductRiskExcelAssemblyService {
createTextCell(row, 0, firstNonBlank(dto == null ? null : dto.getShopName(), shopDisplayName));
createTextCell(row, 1, dto == null ? null : dto.getProductAsinSku());
createTextCell(row, 2, dto == null ? null : dto.getStatus());
createTextCell(row, 3, dto == null || dto.getDone() == null ? null : (dto.getDone() ? "" : ""));
createTextCell(row, 3, hasBusinessData(dto) ? "" : "");
createTextCell(row, 4, dto == null ? null : dto.getRemoveAsin());
createTextCell(row, 5, dto == null ? null : dto.getRemoveStatus());
}
@@ -111,6 +111,18 @@ public class ProductRiskExcelAssemblyService {
return fallback == null ? "" : fallback;
}
private static boolean hasBusinessData(ProductRiskRowDto dto) {
return dto != null
&& (hasText(dto.getProductAsinSku())
|| hasText(dto.getStatus())
|| hasText(dto.getRemoveAsin())
|| hasText(dto.getRemoveStatus()));
}
private static boolean hasText(String value) {
return value != null && !value.isBlank();
}
private void applyDefaultColumnWidths(Sheet sheet) {
int[] widths = {20, 22, 18, 10, 18, 18};
for (int i = 0; i < widths.length; i++) {

View File

@@ -1,6 +1,7 @@
package com.nanri.aiimage.modules.shopkey.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import cn.hutool.core.util.IdUtil;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.modules.shopkey.mapper.SkipPriceAsinMapper;
@@ -35,6 +36,7 @@ import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -1055,6 +1057,104 @@ public class SkipPriceAsinService {
return grouped;
}
/**
* 分页查询跳过 ASIN 数据(按商品状态全量模式使用)
* 按 ASIN 个数精确分页(而非表记录数)
*
* @param shopName 店铺名称(可选,为空则查询所有)
* @param countryCodes 国家代码列表
* @param page 页码,从 1 开始
* @param pageSize 每页条数
* @return 分页结果
*/
public com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo listSkipAsinsPaginated(
String shopName, List<String> countryCodes, int page, int pageSize) {
if (page < 1) {
page = 1;
}
if (pageSize < 1 || pageSize > 2000) {
pageSize = 1000;
}
// 构建查询条件
LambdaQueryWrapper<SkipPriceAsinEntity> queryWrapper = new LambdaQueryWrapper<SkipPriceAsinEntity>()
.orderByDesc(SkipPriceAsinEntity::getId);
// 如果指定了店铺名,按店铺过滤
if (shopName != null && !shopName.trim().isEmpty()) {
String normalizedShopName = normalizeBlank(shopName);
queryWrapper.eq(SkipPriceAsinEntity::getShopName, normalizedShopName);
}
// 查询所有记录(用于内存分页)
List<SkipPriceAsinEntity> allRecords = skipPriceAsinMapper.selectList(queryWrapper);
List<String> targetCountries = (countryCodes == null || countryCodes.isEmpty())
? List.of("DE", "UK", "FR", "IT", "ES")
: countryCodes;
// 展开所有 ASIN 为扁平列表
List<AsinItem> flattenedAsins = new ArrayList<>();
for (SkipPriceAsinEntity row : allRecords) {
for (String country : targetCountries) {
String asin = getCountryAsin(row, country);
BigDecimal minimumPrice = getCountryMinimumPrice(row, country);
if (asin != null && !asin.isBlank()) {
AsinItem item = new AsinItem();
item.country = country;
item.asin = asin;
item.minimumPrice = minimumPrice;
flattenedAsins.add(item);
}
}
}
// 计算总数和分页
long totalAsins = flattenedAsins.size();
int totalPages = (int) ((totalAsins + pageSize - 1) / pageSize);
int startIndex = (page - 1) * pageSize;
int endIndex = Math.min(startIndex + pageSize, flattenedAsins.size());
// 提取当前页的 ASIN
Map<String, List<String>> skipAsinsByCountry = new LinkedHashMap<>();
Map<String, List<Map<String, String>>> skipDetailsByCountry = new LinkedHashMap<>();
for (String country : targetCountries) {
skipAsinsByCountry.put(country, new ArrayList<>());
skipDetailsByCountry.put(country, new ArrayList<>());
}
for (int i = startIndex; i < endIndex; i++) {
AsinItem item = flattenedAsins.get(i);
skipAsinsByCountry.get(item.country).add(item.asin);
Map<String, String> detail = new LinkedHashMap<>();
detail.put("asin", item.asin);
detail.put("minimumPrice", item.minimumPrice == null ? "" : item.minimumPrice.toPlainString());
skipDetailsByCountry.get(item.country).add(detail);
}
// 构造返回 VO
com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo vo =
new com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo();
vo.setPage(page);
vo.setPageSize(pageSize);
vo.setTotal(totalAsins);
vo.setTotalPages(totalPages);
vo.setSkipAsinsByCountry(skipAsinsByCountry);
vo.setSkipAsinDetailsByCountry(skipDetailsByCountry);
return vo;
}
// 内部辅助类
private static class AsinItem {
String country;
String asin;
BigDecimal minimumPrice;
}
@Transactional
public boolean removeByShopCountryAndAsin(String shopName, String country, String asin) {
String normalizedShopName = normalizeBlank(shopName);

View File

@@ -4,10 +4,24 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.nanri.aiimage.common.service.DistributedJobLockService;
import com.nanri.aiimage.config.ModuleCleanupProperties;
import com.nanri.aiimage.modules.collectdata.mapper.CollectDataItemMapper;
import com.nanri.aiimage.modules.collectdata.model.entity.CollectDataItemEntity;
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
import com.nanri.aiimage.modules.task.mapper.TaskChunkMapper;
import com.nanri.aiimage.modules.task.mapper.TaskFileJobMapper;
import com.nanri.aiimage.modules.task.mapper.TaskProgressSnapshotMapper;
import com.nanri.aiimage.modules.task.mapper.TaskResultItemMapper;
import com.nanri.aiimage.modules.task.mapper.TaskResultPayloadMapper;
import com.nanri.aiimage.modules.task.mapper.TaskScopeStateMapper;
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
import com.nanri.aiimage.modules.task.model.entity.TaskChunkEntity;
import com.nanri.aiimage.modules.task.model.entity.TaskFileJobEntity;
import com.nanri.aiimage.modules.task.model.entity.TaskProgressSnapshotEntity;
import com.nanri.aiimage.modules.task.model.entity.TaskResultItemEntity;
import com.nanri.aiimage.modules.task.model.entity.TaskResultPayloadEntity;
import com.nanri.aiimage.modules.task.model.entity.TaskScopeStateEntity;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
@@ -15,6 +29,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@@ -27,10 +42,18 @@ public class ModuleHistoryCleanupService {
private static final Set<String> TERMINAL_STATUSES = Set.of("SUCCESS", "FAILED", "CANCELLED", "CANCELED");
private static final Duration CLEANUP_LOCK_TTL = Duration.ofHours(2);
private static final String COLLECT_DATA_MODULE_TYPE = "COLLECT_DATA";
private final ModuleCleanupProperties moduleCleanupProperties;
private final FileTaskMapper fileTaskMapper;
private final FileResultMapper fileResultMapper;
private final TaskFileJobMapper taskFileJobMapper;
private final TaskResultItemMapper taskResultItemMapper;
private final TaskProgressSnapshotMapper taskProgressSnapshotMapper;
private final TaskResultPayloadMapper taskResultPayloadMapper;
private final TaskScopeStateMapper taskScopeStateMapper;
private final TaskChunkMapper taskChunkMapper;
private final CollectDataItemMapper collectDataItemMapper;
private final DistributedJobLockService distributedJobLockService;
@Transactional
@@ -50,30 +73,71 @@ public class ModuleHistoryCleanupService {
if (moduleTypes == null || moduleTypes.isEmpty()) {
return;
}
LocalDateTime cutoff = LocalDateTime.now().minusDays(Math.max(0, moduleCleanupProperties.getRetentionDays()));
List<FileTaskEntity> moduleTasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
.in(FileTaskEntity::getModuleType, moduleTypes)
.select(FileTaskEntity::getId, FileTaskEntity::getModuleType, FileTaskEntity::getStatus));
.select(FileTaskEntity::getId, FileTaskEntity::getModuleType, FileTaskEntity::getStatus,
FileTaskEntity::getUpdatedAt, FileTaskEntity::getFinishedAt));
List<Long> cleanupTaskIds = new ArrayList<>();
List<Long> cleanupCollectDataTaskIds = new ArrayList<>();
List<Long> skippedActiveTaskIds = new ArrayList<>();
List<Long> skippedRetainedTaskIds = new ArrayList<>();
for (FileTaskEntity task : moduleTasks) {
if (task == null || task.getId() == null) {
continue;
}
if (isTerminalStatus(task.getStatus())) {
cleanupTaskIds.add(task.getId());
} else {
if (!isTerminalStatus(task.getStatus())) {
skippedActiveTaskIds.add(task.getId());
continue;
}
if (isExpired(task, cutoff)) {
cleanupTaskIds.add(task.getId());
if (COLLECT_DATA_MODULE_TYPE.equals(task.getModuleType())) {
cleanupCollectDataTaskIds.add(task.getId());
}
} else {
skippedRetainedTaskIds.add(task.getId());
}
}
if (cleanupTaskIds.isEmpty()) {
log.info("模块历史清理跳过: moduleTypes={}, activeTaskIds={}, reason=no-terminal-tasks",
moduleTypes, skippedActiveTaskIds);
log.info("[module-cleanup] skipped: moduleTypes={}, retentionDays={}, cutoff={}, activeTaskIds={}, retainedTaskIds={}, reason=no-expired-terminal-tasks",
moduleTypes, moduleCleanupProperties.getRetentionDays(), cutoff, skippedActiveTaskIds, skippedRetainedTaskIds);
return;
}
int deletedFileJobs = taskFileJobMapper.delete(new LambdaQueryWrapper<TaskFileJobEntity>()
.in(TaskFileJobEntity::getModuleType, moduleTypes)
.in(TaskFileJobEntity::getTaskId, cleanupTaskIds));
int deletedResultItems = taskResultItemMapper.delete(new LambdaQueryWrapper<TaskResultItemEntity>()
.in(TaskResultItemEntity::getModuleType, moduleTypes)
.in(TaskResultItemEntity::getTaskId, cleanupTaskIds));
int deletedProgressSnapshots = taskProgressSnapshotMapper.delete(new LambdaQueryWrapper<TaskProgressSnapshotEntity>()
.in(TaskProgressSnapshotEntity::getModuleType, moduleTypes)
.in(TaskProgressSnapshotEntity::getTaskId, cleanupTaskIds));
int deletedResultPayloads = taskResultPayloadMapper.delete(new LambdaQueryWrapper<TaskResultPayloadEntity>()
.in(TaskResultPayloadEntity::getModuleType, moduleTypes)
.in(TaskResultPayloadEntity::getTaskId, cleanupTaskIds));
int deletedScopeStates = taskScopeStateMapper.delete(new LambdaQueryWrapper<TaskScopeStateEntity>()
.in(TaskScopeStateEntity::getModuleType, moduleTypes)
.in(TaskScopeStateEntity::getTaskId, cleanupTaskIds));
int deletedChunks = taskChunkMapper.delete(new LambdaQueryWrapper<TaskChunkEntity>()
.in(TaskChunkEntity::getModuleType, moduleTypes)
.in(TaskChunkEntity::getTaskId, cleanupTaskIds));
int deletedCollectDataItems = 0;
if (!cleanupCollectDataTaskIds.isEmpty()) {
deletedCollectDataItems = collectDataItemMapper.delete(new LambdaQueryWrapper<CollectDataItemEntity>()
.in(CollectDataItemEntity::getTaskId, cleanupCollectDataTaskIds));
}
int deletedResults = fileResultMapper.delete(new LambdaQueryWrapper<FileResultEntity>()
.in(FileResultEntity::getModuleType, moduleTypes)
.in(FileResultEntity::getTaskId, cleanupTaskIds));
@@ -87,11 +151,19 @@ public class ModuleHistoryCleanupService {
int deletedTasks = fileTaskMapper.delete(new LambdaQueryWrapper<FileTaskEntity>()
.in(FileTaskEntity::getId, cleanupTaskIds));
log.info("模块历史清理完成: moduleTypes={}, deletedResults={}, resetTasks={}, deletedTasks={}, skippedActiveTaskIds={}",
moduleTypes, deletedResults, resetTasks, deletedTasks, skippedActiveTaskIds);
log.info("[module-cleanup] completed: moduleTypes={}, retentionDays={}, cutoff={}, deletedFileJobs={}, deletedResultItems={}, deletedProgressSnapshots={}, deletedResultPayloads={}, deletedScopeStates={}, deletedChunks={}, deletedCollectDataItems={}, deletedResults={}, resetTasks={}, deletedTasks={}, skippedActiveTaskIds={}, retainedTaskIds={}",
moduleTypes, moduleCleanupProperties.getRetentionDays(), cutoff,
deletedFileJobs, deletedResultItems, deletedProgressSnapshots, deletedResultPayloads,
deletedScopeStates, deletedChunks, deletedCollectDataItems, deletedResults, resetTasks, deletedTasks,
skippedActiveTaskIds, skippedRetainedTaskIds);
}
}
private boolean isExpired(FileTaskEntity task, LocalDateTime cutoff) {
LocalDateTime completedAt = task.getFinishedAt() != null ? task.getFinishedAt() : task.getUpdatedAt();
return completedAt != null && !completedAt.isAfter(cutoff);
}
private boolean isTerminalStatus(String status) {
if (status == null || status.isBlank()) {
return false;

View File

@@ -148,7 +148,8 @@ aiimage:
module-cleanup:
enabled: ${AIIMAGE_MODULE_CLEANUP_ENABLED:true}
cron: ${AIIMAGE_MODULE_CLEANUP_CRON:0 0 0 * * *}
module-types: ${AIIMAGE_MODULE_CLEANUP_MODULE_TYPES:DEDUPE,SPLIT,CONVERT,DELETE_BRAND,PRODUCT_RISK_RESOLVE,PRICE_TRACK,SHOP_MATCH,PATROL_DELETE}
retention-days: ${AIIMAGE_MODULE_CLEANUP_RETENTION_DAYS:7}
module-types: ${AIIMAGE_MODULE_CLEANUP_MODULE_TYPES:DEDUPE,SPLIT,CONVERT,DELETE_BRAND,PRODUCT_RISK_RESOLVE,PRICE_TRACK,SHOP_MATCH,PATROL_DELETE,QUERY_ASIN,APPEARANCE_PATENT,SIMILAR_ASIN,COLLECT_DATA}
permission-schema-init:
enabled: ${AIIMAGE_PERMISSION_SCHEMA_INIT_ENABLED:false}
task-pressure: