视频任务管理更新

This commit is contained in:
super
2026-07-19 22:23:26 +08:00
parent 69784b8d32
commit cbde22ec79
62 changed files with 2164 additions and 280 deletions
@@ -5,6 +5,7 @@ import com.nanri.aiimage.common.util.DownloadHeaderUtil;
import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackCandidateAddRequest;
import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackCountryPreferenceSaveRequest;
import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackCreateTaskRequest;
import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackDispatchFailedRequest;
import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackLoopRunChildFinishedRequest;
import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackLoopRunCreateRequest;
import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackMatchShopsRequest;
@@ -145,7 +146,7 @@ public class PriceTrackController {
@PathVariable Long taskId,
@Parameter(description = "国家代码", required = true, example = "DE")
@RequestParam("country") String country,
@Parameter(description = "ASIN", required = true, example = "B0XXXX")
@Parameter(description = "商品 ASIN", required = true, example = "B0XXXX")
@RequestParam("asin") String asin) {
return ApiResponse.success(priceTrackTaskService.checkTaskSkipAsin(taskId, country, asin));
}
@@ -182,6 +183,16 @@ public class PriceTrackController {
return ApiResponse.success(priceTrackTaskService.createTask(request));
}
@PostMapping("/tasks/{taskId}/dispatch-failed")
@Operation(summary = "标记 Python 入队失败", description = "前端创建任务后若 Python 队列拒绝接收,立即将任务及占位店铺标记为失败。")
public ApiResponse<Void> markDispatchFailed(
@PathVariable Long taskId,
@RequestParam("user_id") Long userId,
@Valid @RequestBody PriceTrackDispatchFailedRequest request) {
priceTrackTaskService.markDispatchFailed(taskId, userId, request.getErrorMessage());
return ApiResponse.success(null);
}
@PostMapping("/loop-runs")
@Operation(summary = "创建跟价循环任务", description = "创建整批店铺的多轮循环主任务。")
public ApiResponse<PriceTrackLoopRunVo> createLoopRun(@Valid @RequestBody PriceTrackLoopRunCreateRequest request) {
@@ -33,12 +33,12 @@ public class PriceTrackCreateTaskRequest {
@NotEmpty(message = "国家列表不能为空")
@Schema(description = "国家代码列表,顺序即跟价处理顺序", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"DE\",\"UK\",\"FR\"]")
private List<String> countryCodes;
@Schema(description = "optional loop run id", example = "1")
@Schema(description = "可选的循环运行 ID", example = "1")
private Long loopRunId;
@Schema(description = "optional round index, 1-based", example = "2")
@Schema(description = "可选的轮次序号,从 1 开始", example = "2")
private Integer roundIndex;
@Schema(description = "optional shop index, 0-based", example = "0")
@Schema(description = "可选的店铺序号,从 0 开始", example = "0")
private Integer shopIndex;
}
@@ -0,0 +1,14 @@
package com.nanri.aiimage.modules.pricetrack.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "跟价任务推送 Python 队列失败请求")
public class PriceTrackDispatchFailedRequest {
@NotBlank(message = "errorMessage不能为空")
@Schema(description = "Python 队列拒绝或调用失败的原始原因", example = "当前店铺正在执行中")
private String errorMessage;
}
@@ -32,7 +32,7 @@ public class PriceTrackLoopRunCreateRequest {
private List<String> countryCodes = new ArrayList<>();
@NotBlank(message = "executionMode不能为空")
@Schema(description = "FINITE or INFINITE", example = "FINITE")
@Schema(description = "循环模式:FINITE 表示有限次数,INFINITE 表示无限循环", example = "FINITE")
private String executionMode;
@Schema(description = "固定轮次时必填", example = "3")
@@ -8,20 +8,20 @@ import lombok.Data;
import java.util.List;
@Data
@Schema(description = "Price track match shops request")
@Schema(description = "跟价店铺匹配请求")
public class PriceTrackMatchShopsRequest {
@NotNull(message = "userId is required")
@Schema(description = "Current user id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@Schema(description = "当前用户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long userId;
@NotEmpty(message = "shopNames cannot be empty")
@Schema(description = "Shop names to match", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(description = "需要匹配的店铺名称", requiredMode = Schema.RequiredMode.REQUIRED)
private List<String> shopNames;
@Schema(description = "Selected ASIN file local paths")
@Schema(description = "已选择的 ASIN 文件本地路径")
private List<String> asinFiles;
@Schema(description = "Selected country codes")
@Schema(description = "已选择的国家代码")
private List<String> countryCodes;
}
@@ -65,7 +65,7 @@ public class PriceTrackSubmitResultRequest {
@Schema(description = "店铺商城名称", example = "Amazon.de")
private String shopMallName;
@Schema(description = "ASIN", example = "B0ABCDE123")
@Schema(description = "商品 ASIN", example = "B0ABCDE123")
private String asin;
@Schema(description = "价格", example = "19.99")
@@ -7,23 +7,23 @@ import java.util.List;
import java.util.Map;
@Data
@Schema(description = "price track create task response")
@Schema(description = "跟价任务创建响应")
public class PriceTrackCreateTaskVo {
@Schema(description = "task id", example = "200")
@Schema(description = "任务 ID", example = "200")
private Long taskId;
@Schema(description = "initial task items")
@Schema(description = "初始任务项")
private List<PriceTrackResultItemVo> items;
@Schema(description = "all skip asins grouped by country")
@Schema(description = "按国家分组的全部跳过跟价 ASIN")
private Map<String, List<String>> skipAsinsByCountry;
@Schema(description = "all skip asin details grouped by country code, each item includes asin and minimumPrice")
@Schema(description = "按国家代码分组的全部跳过跟价 ASIN 明细,每项包含 asin minimumPrice")
private Map<String, List<Map<String, String>>> skipAsinDetailsByCountry;
@Schema(description = "parsed asin rows by country")
@Schema(description = "按国家分组的已解析 ASIN 行")
private Map<String, List<Map<String, String>>> asinRowsByCountry;
@Schema(description = "minimum price grouped by country code and asin")
@Schema(description = "按国家代码和 ASIN 分组的最低价格")
private Map<String, Map<String, String>> minimumPriceByCountryAndAsin;
}
@@ -8,53 +8,53 @@ import java.util.List;
import java.util.Map;
@Data
@Schema(description = "Price track shop match response")
@Schema(description = "跟价店铺匹配响应")
public class PriceTrackMatchShopsVo {
@Schema(description = "Match results in the same order as requested shop names")
@Schema(description = "匹配结果,顺序与请求中的店铺名称一致")
private List<PriceTrackShopQueueItem> items = new ArrayList<>();
@Schema(description = "All skip ASINs grouped by country code")
@Schema(description = "按国家代码分组的全部跳过跟价 ASIN")
private Map<String, List<String>> skipAsinsByCountry;
@Schema(description = "All skip ASIN details grouped by country code, each item includes asin and minimumPrice")
@Schema(description = "按国家代码分组的全部跳过跟价 ASIN 明细,每项包含 asin minimumPrice")
private Map<String, List<Map<String, String>>> skipAsinDetailsByCountry;
@Schema(description = "Parsed asin rows grouped by country code")
@Schema(description = "按国家代码分组的已解析 ASIN 行")
private Map<String, List<Map<String, String>>> asinRowsByCountry;
@Schema(description = "Minimum price grouped by country code and asin")
@Schema(description = "按国家代码和 ASIN 分组的最低价格")
private Map<String, Map<String, String>> minimumPriceByCountryAndAsin;
@Data
@Schema(description = "Single matched shop item")
@Schema(description = "单个店铺匹配结果")
public static class PriceTrackShopQueueItem {
@Schema(description = "Shop name", example = "Demo Shop")
@Schema(description = "店铺名称", example = "Demo Shop")
private String shopName;
@Schema(description = "Shop mall name", example = "Amazon.de")
@Schema(description = "店铺商城名称", example = "Amazon.de")
private String shopMallName;
@Schema(description = "Matched shop id")
@Schema(description = "匹配到的店铺 ID")
private Object shopId;
@Schema(description = "Platform", example = "Amazon")
@Schema(description = "平台", example = "Amazon")
private String platform;
@Schema(description = "Company name")
@Schema(description = "公司名称")
private String companyName;
@Schema(description = "Whether the shop matched")
@Schema(description = "店铺是否匹配成功")
private Boolean matched;
@Schema(description = "Match status, such as MATCHED or PENDING")
@Schema(description = "匹配状态,例如 MATCHED PENDING")
private String matchStatus;
@Schema(description = "Readable match message")
@Schema(description = "可读的匹配结果说明")
private String matchMessage;
@Schema(description = "All skip ASINs grouped by country code")
@Schema(description = "按国家代码分组的全部跳过跟价 ASIN")
private Map<String, List<String>> skipAsins;
}
}
@@ -17,7 +17,7 @@ public class PriceTrackResultItemVo {
@Schema(description = "店铺名称,详情页会尽量恢复创建任务时的展示名")
private String shopName;
@Schema(description = "Shop mall name")
@Schema(description = "店铺商城名称")
private String shopMallName;
@Schema(description = "店铺 ID(紫鸟)")
@@ -4,18 +4,18 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "single skip price asin lookup response")
@Schema(description = "单个跳过跟价 ASIN 查询响应")
public class SkipPriceAsinCheckVo {
@Schema(description = "country code", example = "DE")
@Schema(description = "国家代码", example = "DE")
private String country;
@Schema(description = "normalized asin", example = "B0XXXX")
@Schema(description = "规范化后的 ASIN", example = "B0XXXX")
private String asin;
@Schema(description = "whether the asin exists in skip price asin table")
@Schema(description = "该 ASIN 是否存在于跳过跟价 ASIN 表中")
private boolean exists;
@Schema(description = "configured minimum price, empty when not configured or not found")
@Schema(description = "已配置的最低价格;未配置或未找到时为空")
private String minimumPrice;
}
@@ -277,6 +277,46 @@ public class PriceTrackTaskService {
}
}
@Transactional
public void markDispatchFailed(Long taskId, Long userId, String errorMessage) {
if (userId == null || userId <= 0) throw new BusinessException("user_id 不合法");
if (errorMessage == null || errorMessage.isBlank()) throw new BusinessException("errorMessage 不能为空");
String normalizedError = errorMessage.trim();
try (TaskDistributedLockService.LockHandle ignored = acquireTaskLockOrThrow(taskId)) {
FileTaskEntity task = loadTaskForExecution(taskId);
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !userId.equals(task.getUserId())) {
throw new BusinessException("任务不存在");
}
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
return;
}
List<FileResultEntity> results = fileResultMapper.selectList(
new LambdaQueryWrapper<FileResultEntity>()
.eq(FileResultEntity::getTaskId, taskId)
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
.orderByAsc(FileResultEntity::getId));
if (results.isEmpty()) {
task.setStatus("FAILED");
task.setErrorMessage(normalizedError);
task.setUpdatedAt(LocalDateTime.now());
task.setFinishedAt(LocalDateTime.now());
fileTaskMapper.updateById(task);
cleanupTaskCacheIfTerminal(taskId, task.getStatus());
priceTrackLoopRunService.syncLoopRunAfterChildTerminal(taskId);
return;
}
for (FileResultEntity result : results) {
boolean succeeded = result.getSuccess() != null && result.getSuccess() == 1;
boolean failed = result.getErrorMessage() != null && !result.getErrorMessage().isBlank();
if (!succeeded && !failed) {
markResultFailed(result, normalizedError);
}
}
updateTaskStatusFromLatestRows(task, results);
log.warn("[price-track] Python dispatch failed taskId={} userId={} error={}", taskId, userId, normalizedError);
}
}
@Transactional
public PriceTrackPendingDeleteVo deletePendingShopResult(Long userId, String shopName) {
if (userId == null || userId <= 0) throw new BusinessException("user_id 不合法");