修复后端BUG
This commit is contained in:
@@ -416,6 +416,15 @@ public class ImageVideoAsyncTaskService {
|
||||
|
||||
private String findDirectText(Object value, Set<String> names) {
|
||||
Object parsed = parsePossiblyJson(value);
|
||||
if (parsed instanceof Collection<?> collection) {
|
||||
for (Object child : collection) {
|
||||
String text = findDirectText(child, names);
|
||||
if (!text.isBlank()) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
if (!(parsed instanceof Map<?, ?> map)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskMatchShopsRequ
|
||||
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskTaskBatchRequest;
|
||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskCandidateVo;
|
||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskCountryPreferenceVo;
|
||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskCreateTaskVo;
|
||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskDashboardVo;
|
||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskHistoryVo;
|
||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskMatchShopsVo;
|
||||
@@ -16,6 +15,8 @@ import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskBatchVo;
|
||||
import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchCreateTaskRequest;
|
||||
import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchStageCompleteRequest;
|
||||
import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchSubmitResultRequest;
|
||||
import com.nanri.aiimage.modules.shopmatch.model.vo.ShopMatchCreateTaskVo;
|
||||
import com.nanri.aiimage.modules.shopmatch.model.vo.ShopMatchSkipAsinPageVo;
|
||||
import com.nanri.aiimage.modules.shopmatch.service.ShopMatchResolveService;
|
||||
import com.nanri.aiimage.modules.shopmatch.service.ShopMatchTaskService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -122,7 +123,7 @@ public class ShopMatchController {
|
||||
|
||||
@PostMapping("/tasks")
|
||||
@Operation(summary = "创建匹配店铺任务", description = "创建任务与各店铺占位结果,支持立即执行或多时间点定时执行。")
|
||||
public ApiResponse<ProductRiskCreateTaskVo> createTask(@Valid @RequestBody ShopMatchCreateTaskRequest request) {
|
||||
public ApiResponse<ShopMatchCreateTaskVo> createTask(@Valid @RequestBody ShopMatchCreateTaskRequest request) {
|
||||
return ApiResponse.success(shopMatchTaskService.createTask(request));
|
||||
}
|
||||
|
||||
@@ -146,6 +147,26 @@ public class ShopMatchController {
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@GetMapping("/tasks/{taskId}/skip-asins/paginated")
|
||||
@Operation(summary = "分页查询匹配店铺任务 ASIN 表数据", description = "Python 按任务、店铺和国家分页拉取从创建任务接口迁移出的 ASIN 表数据。")
|
||||
public ApiResponse<ShopMatchSkipAsinPageVo> getTaskSkipAsinsPaginated(
|
||||
@Parameter(description = "任务编号", 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,
|
||||
@Parameter(description = "店铺名称过滤条件,可重复传入,也可用英文逗号分隔", example = "测试店铺A")
|
||||
@RequestParam(value = "shop_name", required = false) List<String> shopNames,
|
||||
@Parameter(description = "国家代码过滤条件,可重复传入,也可用英文逗号分隔", example = "DE")
|
||||
@RequestParam(value = "country_code", required = false) List<String> countryCodes) {
|
||||
return ApiResponse.success(shopMatchTaskService.getTaskSkipAsinsPaginated(
|
||||
taskId,
|
||||
page,
|
||||
pageSize,
|
||||
shopNames,
|
||||
countryCodes));
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/stage-finished")
|
||||
@Operation(summary = "标记阶段完成", description = "某个定时阶段完成后推进到下一阶段,或等待最终收尾。")
|
||||
public ApiResponse<Void> completeStage(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.nanri.aiimage.modules.shopmatch.model.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskShopQueueItemVo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
@@ -24,7 +23,7 @@ public class ShopMatchCreateTaskRequest {
|
||||
@NotEmpty(message = "items cannot be empty")
|
||||
@Valid
|
||||
@Schema(description = "已匹配店铺列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private List<ProductRiskShopQueueItemVo> items = new ArrayList<>();
|
||||
private List<ShopMatchTaskItemDto> items = new ArrayList<>();
|
||||
|
||||
@NotEmpty(message = "country_codes cannot be empty")
|
||||
@JsonProperty("country_codes")
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.nanri.aiimage.modules.shopmatch.model.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Schema(description = "创建匹配店铺任务的单店任务数据")
|
||||
public class ShopMatchTaskItemDto {
|
||||
|
||||
@JsonProperty("shopName")
|
||||
@Schema(description = "店铺名称", example = "某某店")
|
||||
private String shopName;
|
||||
|
||||
@Schema(description = "是否已匹配到紫鸟索引")
|
||||
private boolean matched;
|
||||
|
||||
@JsonProperty("shopId")
|
||||
@Schema(description = "紫鸟侧店铺 ID")
|
||||
private String shopId;
|
||||
|
||||
@Schema(description = "平台")
|
||||
private String platform;
|
||||
|
||||
@JsonProperty("companyName")
|
||||
@Schema(description = "公司名称或主体标识")
|
||||
private String companyName;
|
||||
|
||||
@JsonProperty("openStoreUrl")
|
||||
@Schema(description = "打开店铺的地址,由 Python 侧消费")
|
||||
private String openStoreUrl;
|
||||
|
||||
@JsonProperty("matchedUserId")
|
||||
@Schema(description = "索引关联到的用户 ID")
|
||||
private Long matchedUserId;
|
||||
|
||||
@JsonProperty("matchStatus")
|
||||
@Schema(description = "匹配状态码,如 MATCHED、PENDING、CONFLICT、INDEX_STALE")
|
||||
private String matchStatus;
|
||||
|
||||
@JsonProperty("matchMessage")
|
||||
@Schema(description = "匹配说明")
|
||||
private String matchMessage;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.nanri.aiimage.modules.shopmatch.model.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "创建匹配店铺任务响应")
|
||||
public class ShopMatchCreateTaskVo {
|
||||
|
||||
@Schema(description = "新建任务主键 biz_file_task.id", example = "200")
|
||||
private Long taskId;
|
||||
|
||||
@Schema(description = "各店铺初始任务快照")
|
||||
private List<Item> items = new ArrayList<>();
|
||||
|
||||
@Data
|
||||
@Schema(description = "创建匹配店铺任务后的单店快照")
|
||||
public static class Item {
|
||||
|
||||
private Long resultId;
|
||||
|
||||
private Long taskId;
|
||||
|
||||
@JsonProperty("shopName")
|
||||
private String shopName;
|
||||
|
||||
@JsonProperty("shopId")
|
||||
private String shopId;
|
||||
|
||||
private String platform;
|
||||
|
||||
@JsonProperty("companyName")
|
||||
private String companyName;
|
||||
|
||||
private boolean matched;
|
||||
|
||||
@JsonProperty("matchStatus")
|
||||
private String matchStatus;
|
||||
|
||||
@JsonProperty("matchMessage")
|
||||
private String matchMessage;
|
||||
|
||||
@JsonProperty("taskStatus")
|
||||
private String taskStatus;
|
||||
|
||||
private Boolean success;
|
||||
|
||||
private String error;
|
||||
|
||||
@JsonProperty("outputFilename")
|
||||
private String outputFilename;
|
||||
|
||||
@JsonProperty("downloadUrl")
|
||||
private String downloadUrl;
|
||||
|
||||
private Long fileJobId;
|
||||
|
||||
private String fileStatus;
|
||||
|
||||
private String fileError;
|
||||
|
||||
private Boolean fileReady;
|
||||
|
||||
@JsonProperty("scheduledAt")
|
||||
private String scheduledAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.nanri.aiimage.modules.shopmatch.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinCountryAsinsDto;
|
||||
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinDetailDto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Schema(description = "分页查询匹配店铺任务 ASIN 表数据响应")
|
||||
public class ShopMatchSkipAsinPageVo {
|
||||
|
||||
@Schema(description = "当前页码,从 1 开始")
|
||||
private Integer page;
|
||||
|
||||
@Schema(description = "每页条数")
|
||||
private Integer pageSize;
|
||||
|
||||
@Schema(description = "总 ASIN 数")
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "总页数")
|
||||
private Integer totalPages;
|
||||
|
||||
@Schema(description = "原创建任务 items.queryAsins 字段迁移出的数据,按国家返回 ASIN 清单")
|
||||
private List<QueryAsinCountryAsinsDto> queryAsins = new ArrayList<>();
|
||||
|
||||
@Schema(description = "原创建任务 items.skipAsinsByCountry 字段迁移出的数据,按国家分组的跳过 ASIN 列表")
|
||||
private Map<String, List<String>> skipAsinsByCountry = new LinkedHashMap<>();
|
||||
|
||||
@Schema(description = "原创建任务 items.skipAsinDetailsByCountry 字段迁移出的数据,按国家分组的跳过 ASIN 明细")
|
||||
private Map<String, List<SkipPriceAsinDetailDto>> skipAsinDetailsByCountry = new LinkedHashMap<>();
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import com.nanri.aiimage.common.exception.BusinessException;
|
||||
import com.nanri.aiimage.config.TaskPressureProperties;
|
||||
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
|
||||
import com.nanri.aiimage.modules.productrisk.model.enums.ProductRiskCountryCode;
|
||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskCreateTaskVo;
|
||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskDashboardVo;
|
||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskHistoryVo;
|
||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskResultItemVo;
|
||||
@@ -16,15 +15,22 @@ import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskShopQueueItemVo
|
||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskBatchVo;
|
||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskDetailVo;
|
||||
import com.nanri.aiimage.modules.productrisk.model.vo.ProductRiskTaskItemVo;
|
||||
import com.nanri.aiimage.modules.shopmatch.model.vo.ShopMatchTaskStageVo;
|
||||
import com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo;
|
||||
import com.nanri.aiimage.modules.queryasin.model.dto.QueryAsinCountryAsinsDto;
|
||||
import com.nanri.aiimage.modules.shopmatch.mapper.ShopMatchShopCandidateMapper;
|
||||
import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchCreateTaskRequest;
|
||||
import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchRowDto;
|
||||
import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchShopPayloadDto;
|
||||
import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchStageCompleteRequest;
|
||||
import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchSubmitResultRequest;
|
||||
import com.nanri.aiimage.modules.shopmatch.model.dto.ShopMatchTaskItemDto;
|
||||
import com.nanri.aiimage.modules.shopmatch.model.entity.ShopMatchShopCandidateEntity;
|
||||
import com.nanri.aiimage.modules.shopmatch.model.vo.ShopMatchCreateTaskVo;
|
||||
import com.nanri.aiimage.modules.shopmatch.model.vo.ShopMatchSkipAsinPageVo;
|
||||
import com.nanri.aiimage.modules.shopmatch.model.vo.ShopMatchTaskStageVo;
|
||||
import com.nanri.aiimage.modules.shopkey.mapper.QueryAsinMapper;
|
||||
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinDetailDto;
|
||||
import com.nanri.aiimage.modules.shopkey.model.entity.QueryAsinEntity;
|
||||
import com.nanri.aiimage.modules.shopkey.service.SkipPriceAsinService;
|
||||
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
|
||||
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
||||
@@ -72,6 +78,7 @@ public class ShopMatchTaskService {
|
||||
private final TaskFileJobService taskFileJobService;
|
||||
private final TaskDistributedLockService taskDistributedLockService;
|
||||
private final SkipPriceAsinService skipPriceAsinService;
|
||||
private final QueryAsinMapper queryAsinMapper;
|
||||
|
||||
private FileTaskEntity loadTaskForExecution(Long taskId) {
|
||||
Map<Long, FileTaskEntity> cachedTasks = shopMatchTaskCacheService.getTaskCacheBatch(List.of(taskId));
|
||||
@@ -344,6 +351,30 @@ public class ShopMatchTaskService {
|
||||
return batch;
|
||||
}
|
||||
|
||||
public ShopMatchSkipAsinPageVo getTaskSkipAsinsPaginated(
|
||||
Long taskId, int page, int pageSize, List<String> requestedShopNames, List<String> requestedCountryCodes) {
|
||||
if (taskId == null || taskId <= 0) {
|
||||
throw new BusinessException("taskId 涓嶅悎娉?");
|
||||
}
|
||||
FileTaskEntity task = loadTaskForExecution(taskId);
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||
throw new BusinessException("浠诲姟涓嶅瓨鍦?");
|
||||
}
|
||||
|
||||
ShopMatchCreateTaskRequest request = parseTaskRequest(task);
|
||||
List<String> countryCodes = resolveSkipAsinCountryCodes(request, requestedCountryCodes);
|
||||
if (countryCodes.isEmpty()) {
|
||||
return emptySkipAsinPage(List.of(), page, pageSize);
|
||||
}
|
||||
List<String> shopNames = resolveSkipAsinShopNames(request, requestedShopNames);
|
||||
if (shopNames.isEmpty()) {
|
||||
return emptySkipAsinPage(countryCodes, page, pageSize);
|
||||
}
|
||||
SkipPriceAsinPageVo skipPage = skipPriceAsinService.listSkipAsinsPaginated(shopNames, countryCodes, page, pageSize);
|
||||
QueryAsinPage queryPage = listQueryAsinsPaginated(shopNames, countryCodes, page, pageSize);
|
||||
return toShopMatchAsinPage(skipPage, queryPage);
|
||||
}
|
||||
|
||||
public String resolveResultDownloadUrl(Long resultId, Long userId) {
|
||||
FileResultEntity entity = fileResultMapper.selectById(resultId);
|
||||
if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
|
||||
@@ -366,14 +397,14 @@ public class ShopMatchTaskService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ProductRiskCreateTaskVo createTask(ShopMatchCreateTaskRequest request) {
|
||||
public ShopMatchCreateTaskVo createTask(ShopMatchCreateTaskRequest request) {
|
||||
if (request.getUserId() == null || request.getUserId() <= 0) {
|
||||
throw new BusinessException("user_id 不合法");
|
||||
}
|
||||
if (request.getItems() == null || request.getItems().isEmpty()) {
|
||||
throw new BusinessException("items 不能为空");
|
||||
}
|
||||
List<ProductRiskShopQueueItemVo> uniqueItems = dedupeQueueItems(request.getItems());
|
||||
List<ProductRiskShopQueueItemVo> uniqueItems = dedupeQueueItems(toQueueItems(request.getItems()));
|
||||
if (uniqueItems.isEmpty()) {
|
||||
throw new BusinessException("items 不能为空");
|
||||
}
|
||||
@@ -384,13 +415,12 @@ public class ShopMatchTaskService {
|
||||
}
|
||||
|
||||
List<String> countryCodes = normalizeCountryCodes(request.getCountryCodes());
|
||||
enrichItemsWithSkipAsins(uniqueItems, countryCodes);
|
||||
List<LocalDateTime> scheduleTimes = normalizeScheduleTimes(request.getScheduleTimes());
|
||||
LocalDateTime now = now();
|
||||
|
||||
ShopMatchCreateTaskRequest persistedRequest = new ShopMatchCreateTaskRequest();
|
||||
persistedRequest.setUserId(request.getUserId());
|
||||
persistedRequest.setItems(uniqueItems);
|
||||
persistedRequest.setItems(toTaskItems(uniqueItems));
|
||||
persistedRequest.setCountryCodes(countryCodes);
|
||||
persistedRequest.setScheduleTimes(scheduleTimes);
|
||||
if (!scheduleTimes.isEmpty()) {
|
||||
@@ -441,9 +471,9 @@ public class ShopMatchTaskService {
|
||||
shopMatchTaskCacheService.touchTaskHeartbeat(task.getId());
|
||||
}
|
||||
|
||||
ProductRiskCreateTaskVo vo = new ProductRiskCreateTaskVo();
|
||||
ShopMatchCreateTaskVo vo = new ShopMatchCreateTaskVo();
|
||||
vo.setTaskId(task.getId());
|
||||
vo.setItems(snapshot);
|
||||
vo.setItems(snapshot.stream().map(this::toCreateTaskItem).toList());
|
||||
return vo;
|
||||
}
|
||||
|
||||
@@ -1073,7 +1103,7 @@ public class ShopMatchTaskService {
|
||||
return;
|
||||
}
|
||||
String key = ziniaoShopSwitchService.normalizeShopName(vo.getShopName());
|
||||
for (ProductRiskShopQueueItemVo item : request.getItems()) {
|
||||
for (ShopMatchTaskItemDto item : request.getItems()) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
@@ -1084,8 +1114,6 @@ public class ShopMatchTaskService {
|
||||
vo.setCompanyName(item.getCompanyName());
|
||||
vo.setMatchStatus(item.getMatchStatus());
|
||||
vo.setMatchMessage(item.getMatchMessage());
|
||||
vo.setSkipAsinsByCountry(item.getSkipAsinsByCountry());
|
||||
vo.setSkipAsinDetailsByCountry(item.getSkipAsinDetailsByCountry());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1158,6 +1186,78 @@ public class ShopMatchTaskService {
|
||||
return LocalDateTime.now(BUSINESS_ZONE);
|
||||
}
|
||||
|
||||
private List<ProductRiskShopQueueItemVo> toQueueItems(List<ShopMatchTaskItemDto> items) {
|
||||
List<ProductRiskShopQueueItemVo> out = new ArrayList<>();
|
||||
if (items == null) {
|
||||
return out;
|
||||
}
|
||||
for (ShopMatchTaskItemDto item : items) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
ProductRiskShopQueueItemVo vo = new ProductRiskShopQueueItemVo();
|
||||
vo.setShopName(item.getShopName());
|
||||
vo.setMatched(item.isMatched());
|
||||
vo.setShopId(item.getShopId());
|
||||
vo.setPlatform(item.getPlatform());
|
||||
vo.setCompanyName(item.getCompanyName());
|
||||
vo.setOpenStoreUrl(item.getOpenStoreUrl());
|
||||
vo.setMatchedUserId(item.getMatchedUserId());
|
||||
vo.setMatchStatus(item.getMatchStatus());
|
||||
vo.setMatchMessage(item.getMatchMessage());
|
||||
out.add(vo);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private List<ShopMatchTaskItemDto> toTaskItems(List<ProductRiskShopQueueItemVo> items) {
|
||||
List<ShopMatchTaskItemDto> out = new ArrayList<>();
|
||||
if (items == null) {
|
||||
return out;
|
||||
}
|
||||
for (ProductRiskShopQueueItemVo item : items) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
ShopMatchTaskItemDto dto = new ShopMatchTaskItemDto();
|
||||
dto.setShopName(item.getShopName());
|
||||
dto.setMatched(item.isMatched());
|
||||
dto.setShopId(item.getShopId());
|
||||
dto.setPlatform(item.getPlatform());
|
||||
dto.setCompanyName(item.getCompanyName());
|
||||
dto.setOpenStoreUrl(item.getOpenStoreUrl());
|
||||
dto.setMatchedUserId(item.getMatchedUserId());
|
||||
dto.setMatchStatus(item.getMatchStatus());
|
||||
dto.setMatchMessage(item.getMatchMessage());
|
||||
out.add(dto);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private ShopMatchCreateTaskVo.Item toCreateTaskItem(ProductRiskResultItemVo source) {
|
||||
ShopMatchCreateTaskVo.Item item = new ShopMatchCreateTaskVo.Item();
|
||||
item.setResultId(source.getResultId());
|
||||
item.setTaskId(source.getTaskId());
|
||||
item.setShopName(source.getShopName());
|
||||
item.setShopId(source.getShopId());
|
||||
item.setPlatform(source.getPlatform());
|
||||
item.setCompanyName(source.getCompanyName());
|
||||
item.setMatched(source.isMatched());
|
||||
item.setMatchStatus(source.getMatchStatus());
|
||||
item.setMatchMessage(source.getMatchMessage());
|
||||
item.setTaskStatus(source.getTaskStatus());
|
||||
item.setSuccess(source.getSuccess());
|
||||
item.setError(source.getError());
|
||||
item.setOutputFilename(source.getOutputFilename());
|
||||
item.setDownloadUrl(source.getDownloadUrl());
|
||||
item.setFileJobId(source.getFileJobId());
|
||||
item.setFileStatus(source.getFileStatus());
|
||||
item.setFileError(source.getFileError());
|
||||
item.setFileReady(source.getFileReady());
|
||||
item.setScheduledAt(source.getScheduledAt());
|
||||
return item;
|
||||
}
|
||||
|
||||
private List<ProductRiskShopQueueItemVo> dedupeQueueItems(List<ProductRiskShopQueueItemVo> raw) {
|
||||
Map<String, ProductRiskShopQueueItemVo> byNorm = new LinkedHashMap<>();
|
||||
for (ProductRiskShopQueueItemVo item : raw) {
|
||||
@@ -1172,34 +1272,269 @@ public class ShopMatchTaskService {
|
||||
return new ArrayList<>(byNorm.values());
|
||||
}
|
||||
|
||||
private void enrichItemsWithSkipAsins(List<ProductRiskShopQueueItemVo> items, List<String> countryCodes) {
|
||||
List<String> shopNames = items.stream()
|
||||
.map(ProductRiskShopQueueItemVo::getShopName)
|
||||
.filter(name -> name != null && !name.isBlank())
|
||||
.distinct()
|
||||
.toList();
|
||||
Map<String, Map<String, List<SkipPriceAsinDetailDto>>> detailsByShop =
|
||||
skipPriceAsinService.listSkipAsinDetailsByShopAndCountries(shopNames, countryCodes);
|
||||
for (ProductRiskShopQueueItemVo item : items) {
|
||||
Map<String, List<SkipPriceAsinDetailDto>> detailMap =
|
||||
detailsByShop.getOrDefault(item.getShopName(), Map.of());
|
||||
Map<String, List<SkipPriceAsinDetailDto>> normalizedDetails = new LinkedHashMap<>();
|
||||
Map<String, List<String>> asinsByCountry = new LinkedHashMap<>();
|
||||
for (String countryCode : countryCodes) {
|
||||
List<SkipPriceAsinDetailDto> details = detailMap.getOrDefault(countryCode, List.of());
|
||||
if (details.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
normalizedDetails.put(countryCode, details);
|
||||
asinsByCountry.put(countryCode, details.stream()
|
||||
.map(SkipPriceAsinDetailDto::getAsin)
|
||||
.filter(asin -> asin != null && !asin.isBlank())
|
||||
.distinct()
|
||||
.toList());
|
||||
}
|
||||
item.setSkipAsinDetailsByCountry(normalizedDetails);
|
||||
item.setSkipAsinsByCountry(asinsByCountry);
|
||||
private List<String> resolveSkipAsinCountryCodes(ShopMatchCreateTaskRequest request, List<String> requestedCountryCodes) {
|
||||
List<String> taskCountryCodes = normalizeTaskCountryCodes(request == null ? null : request.getCountryCodes());
|
||||
LinkedHashSet<String> requested = new LinkedHashSet<>();
|
||||
for (String country : splitQueryValues(requestedCountryCodes)) {
|
||||
requested.add(normalizeLookupCountry(country));
|
||||
}
|
||||
if (requested.isEmpty()) {
|
||||
return taskCountryCodes;
|
||||
}
|
||||
List<String> result = new ArrayList<>();
|
||||
for (String country : requested) {
|
||||
if (isTaskCountryEnabled(taskCountryCodes, country)) {
|
||||
result.add(country);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<String> normalizeTaskCountryCodes(List<String> countryCodes) {
|
||||
LinkedHashSet<String> normalized = new LinkedHashSet<>();
|
||||
if (countryCodes != null) {
|
||||
for (String country : countryCodes) {
|
||||
String value = country == null ? "" : country.trim().toUpperCase(Locale.ROOT);
|
||||
if (!value.isEmpty()) {
|
||||
normalized.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(normalized);
|
||||
}
|
||||
|
||||
private List<String> resolveSkipAsinShopNames(ShopMatchCreateTaskRequest request, List<String> requestedShopNames) {
|
||||
List<String> taskShopNames = resolveTaskShopNames(request);
|
||||
List<String> requested = splitQueryValues(requestedShopNames);
|
||||
LinkedHashSet<String> normalized = new LinkedHashSet<>();
|
||||
for (String shopName : requested) {
|
||||
String value = ziniaoShopSwitchService.normalizeShopName(shopName);
|
||||
if (!value.isBlank() && taskShopNames.contains(value)) {
|
||||
normalized.add(value);
|
||||
}
|
||||
}
|
||||
if (!normalized.isEmpty()) {
|
||||
return new ArrayList<>(normalized);
|
||||
}
|
||||
if (!requested.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return taskShopNames;
|
||||
}
|
||||
|
||||
private List<String> resolveTaskShopNames(ShopMatchCreateTaskRequest request) {
|
||||
List<String> shopNames = new ArrayList<>();
|
||||
if (request == null || request.getItems() == null) {
|
||||
return shopNames;
|
||||
}
|
||||
for (ShopMatchTaskItemDto item : request.getItems()) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
String normalized = ziniaoShopSwitchService.normalizeShopName(item.getShopName());
|
||||
if (!normalized.isBlank() && !shopNames.contains(normalized)) {
|
||||
shopNames.add(normalized);
|
||||
}
|
||||
}
|
||||
return shopNames;
|
||||
}
|
||||
|
||||
private List<String> splitQueryValues(List<String> values) {
|
||||
List<String> result = new ArrayList<>();
|
||||
if (values == null) {
|
||||
return result;
|
||||
}
|
||||
for (String value : values) {
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
for (String part : value.split(",")) {
|
||||
String trimmed = part.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
result.add(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isTaskCountryEnabled(List<String> countryCodes, String country) {
|
||||
if (countryCodes == null || countryCodes.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (String code : countryCodes) {
|
||||
if (country.equals(code == null ? "" : code.trim().toUpperCase(Locale.ROOT))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String normalizeLookupCountry(String country) {
|
||||
String normalized = country == null ? "" : country.trim().toUpperCase(Locale.ROOT);
|
||||
if (!List.of("DE", "UK", "FR", "IT", "ES").contains(normalized)) {
|
||||
throw new BusinessException("鍥藉鍙傛暟涓嶆敮鎸? " + country);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private ShopMatchSkipAsinPageVo emptySkipAsinPage(List<String> countryCodes, int page, int pageSize) {
|
||||
if (page < 1) {
|
||||
page = 1;
|
||||
}
|
||||
if (pageSize < 1 || pageSize > 2000) {
|
||||
pageSize = 1000;
|
||||
}
|
||||
List<String> targetCountries = (countryCodes == null || countryCodes.isEmpty())
|
||||
? List.of("DE", "UK", "FR", "IT", "ES")
|
||||
: countryCodes;
|
||||
Map<String, List<String>> skipAsinsByCountry = new LinkedHashMap<>();
|
||||
Map<String, List<SkipPriceAsinDetailDto>> skipDetailsByCountry = new LinkedHashMap<>();
|
||||
for (String country : targetCountries) {
|
||||
skipAsinsByCountry.put(country, new ArrayList<>());
|
||||
skipDetailsByCountry.put(country, new ArrayList<>());
|
||||
}
|
||||
ShopMatchSkipAsinPageVo vo = new ShopMatchSkipAsinPageVo();
|
||||
vo.setPage(page);
|
||||
vo.setPageSize(pageSize);
|
||||
vo.setTotal(0L);
|
||||
vo.setTotalPages(0);
|
||||
vo.setQueryAsins(toQueryAsinCountryList(skipAsinsByCountry));
|
||||
vo.setSkipAsinsByCountry(skipAsinsByCountry);
|
||||
vo.setSkipAsinDetailsByCountry(skipDetailsByCountry);
|
||||
return vo;
|
||||
}
|
||||
|
||||
private QueryAsinPage listQueryAsinsPaginated(List<String> shopNames, List<String> countryCodes, int page, int pageSize) {
|
||||
if (page < 1) {
|
||||
page = 1;
|
||||
}
|
||||
if (pageSize < 1 || pageSize > 2000) {
|
||||
pageSize = 1000;
|
||||
}
|
||||
List<String> targetCountries = (countryCodes == null || countryCodes.isEmpty())
|
||||
? List.of("DE", "UK", "FR", "IT", "ES")
|
||||
: countryCodes;
|
||||
QueryAsinPage result = new QueryAsinPage();
|
||||
result.page = page;
|
||||
result.pageSize = pageSize;
|
||||
|
||||
Map<String, List<String>> asinsByCountry = new LinkedHashMap<>();
|
||||
for (String country : targetCountries) {
|
||||
asinsByCountry.put(country, new ArrayList<>());
|
||||
}
|
||||
if (shopNames == null || shopNames.isEmpty()) {
|
||||
result.queryAsins = toQueryAsinCountryList(asinsByCountry);
|
||||
return result;
|
||||
}
|
||||
|
||||
List<QueryAsinEntity> rows = queryAsinMapper.selectList(new LambdaQueryWrapper<QueryAsinEntity>()
|
||||
.in(QueryAsinEntity::getShopName, shopNames)
|
||||
.orderByAsc(QueryAsinEntity::getId));
|
||||
List<QueryAsinItem> flattened = new ArrayList<>();
|
||||
for (QueryAsinEntity row : rows) {
|
||||
for (String country : targetCountries) {
|
||||
String asin = getQueryAsinByCountry(row, country);
|
||||
if (asin != null && !asin.isBlank()) {
|
||||
QueryAsinItem item = new QueryAsinItem();
|
||||
item.country = country;
|
||||
item.asin = normalizeAsin(asin);
|
||||
flattened.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.total = flattened.size();
|
||||
result.totalPages = (int) ((result.total + pageSize - 1) / pageSize);
|
||||
int startIndex = (page - 1) * pageSize;
|
||||
int endIndex = Math.min(startIndex + pageSize, flattened.size());
|
||||
for (int i = startIndex; i < endIndex; i++) {
|
||||
QueryAsinItem item = flattened.get(i);
|
||||
asinsByCountry.get(item.country).add(item.asin);
|
||||
}
|
||||
result.queryAsins = toQueryAsinCountryList(asinsByCountry);
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<QueryAsinCountryAsinsDto> toQueryAsinCountryList(Map<String, List<String>> asinsByCountry) {
|
||||
List<QueryAsinCountryAsinsDto> out = new ArrayList<>();
|
||||
for (Map.Entry<String, List<String>> entry : asinsByCountry.entrySet()) {
|
||||
QueryAsinCountryAsinsDto item = new QueryAsinCountryAsinsDto();
|
||||
item.setCountry(entry.getKey());
|
||||
item.setAsins(entry.getValue());
|
||||
out.add(item);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private String getQueryAsinByCountry(QueryAsinEntity row, String country) {
|
||||
if (row == null || country == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (country) {
|
||||
case "DE" -> row.getAsinDe();
|
||||
case "UK" -> row.getAsinUk();
|
||||
case "FR" -> row.getAsinFr();
|
||||
case "IT" -> row.getAsinIt();
|
||||
case "ES" -> row.getAsinEs();
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private Map<String, List<SkipPriceAsinDetailDto>> toSkipAsinDetailDtos(Map<String, List<Map<String, String>>> source) {
|
||||
Map<String, List<SkipPriceAsinDetailDto>> out = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, List<Map<String, String>>> entry : source.entrySet()) {
|
||||
List<SkipPriceAsinDetailDto> details = new ArrayList<>();
|
||||
if (entry.getValue() != null) {
|
||||
for (Map<String, String> row : entry.getValue()) {
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
SkipPriceAsinDetailDto detail = new SkipPriceAsinDetailDto();
|
||||
detail.setAsin(row.get("asin"));
|
||||
detail.setMinimumPrice(row.get("minimumPrice"));
|
||||
details.add(detail);
|
||||
}
|
||||
}
|
||||
out.put(entry.getKey(), details);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private ShopMatchSkipAsinPageVo toShopMatchAsinPage(SkipPriceAsinPageVo skipPage, QueryAsinPage queryPage) {
|
||||
ShopMatchSkipAsinPageVo vo = new ShopMatchSkipAsinPageVo();
|
||||
Map<String, List<String>> skipAsinsByCountry = skipPage == null || skipPage.getSkipAsinsByCountry() == null
|
||||
? new LinkedHashMap<>()
|
||||
: skipPage.getSkipAsinsByCountry();
|
||||
Map<String, List<SkipPriceAsinDetailDto>> skipDetailsByCountry = skipPage == null || skipPage.getSkipAsinDetailsByCountry() == null
|
||||
? new LinkedHashMap<>()
|
||||
: toSkipAsinDetailDtos(skipPage.getSkipAsinDetailsByCountry());
|
||||
long skipTotal = skipPage == null || skipPage.getTotal() == null ? 0L : skipPage.getTotal();
|
||||
int skipTotalPages = skipPage == null || skipPage.getTotalPages() == null ? 0 : skipPage.getTotalPages();
|
||||
long queryTotal = queryPage == null ? 0L : queryPage.total;
|
||||
int queryTotalPages = queryPage == null ? 0 : queryPage.totalPages;
|
||||
|
||||
vo.setPage(skipPage != null ? skipPage.getPage() : queryPage == null ? 1 : queryPage.page);
|
||||
vo.setPageSize(skipPage != null ? skipPage.getPageSize() : queryPage == null ? 1000 : queryPage.pageSize);
|
||||
vo.setTotal(Math.max(skipTotal, queryTotal));
|
||||
vo.setTotalPages(Math.max(skipTotalPages, queryTotalPages));
|
||||
vo.setQueryAsins(queryPage == null ? new ArrayList<>() : queryPage.queryAsins);
|
||||
vo.setSkipAsinsByCountry(skipAsinsByCountry);
|
||||
vo.setSkipAsinDetailsByCountry(skipDetailsByCountry);
|
||||
return vo;
|
||||
}
|
||||
|
||||
private static class QueryAsinPage {
|
||||
private int page;
|
||||
private int pageSize;
|
||||
private long total;
|
||||
private int totalPages;
|
||||
private List<QueryAsinCountryAsinsDto> queryAsins = new ArrayList<>();
|
||||
}
|
||||
|
||||
private static class QueryAsinItem {
|
||||
private String country;
|
||||
private String asin;
|
||||
}
|
||||
|
||||
private List<String> normalizeCountryCodes(List<String> raw) {
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.nanri.aiimage.modules.imagevideo.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nanri.aiimage.config.ImageVideoProperties;
|
||||
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
|
||||
import com.nanri.aiimage.modules.imagevideo.mapper.ImageVideoAsyncTaskMapper;
|
||||
import com.nanri.aiimage.modules.imagevideo.model.entity.ImageVideoAsyncTaskEntity;
|
||||
import com.nanri.aiimage.modules.imagevideo.model.vo.DouyinCopyVo;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.task.TaskExecutor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class ImageVideoAsyncTaskServiceTest {
|
||||
|
||||
@Test
|
||||
void parsesNestedDouyinCopyOutputIntoFrontendTextFields() {
|
||||
ImageVideoCozeService cozeService = new ImageVideoCozeService(
|
||||
new ImageVideoProperties(),
|
||||
mock(ImageVideoSecretService.class),
|
||||
mock(ImageVideoWorkflowConfigService.class),
|
||||
mock(OssStorageService.class),
|
||||
new ObjectMapper());
|
||||
Map<String, Object> cozeResult = Map.of("data", List.of(Map.of(
|
||||
"execute_status", "Success",
|
||||
"output", "{\"node_status\":\"{}\",\"Output\":\"{\\\"data\\\":{\\\"source\\\":\\\"这条裙裤绝了,遮肉显腿长,快拍!\\\",\\\"video_url\\\":\\\"https://example.com/video.mp4\\\"}}\"}"
|
||||
)));
|
||||
|
||||
DouyinCopyVo result = cozeService.parseDouyinCopyResult(cozeResult);
|
||||
|
||||
assertEquals("这条裙裤绝了,遮肉显腿长,快拍!", result.getRecognizedContent());
|
||||
assertEquals("这条裙裤绝了,遮肉显腿长,快拍!", result.getScriptDraft());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pollWaitingTaskCompletesWhenCozeStatusIsInsideDataArray() {
|
||||
ImageVideoAsyncTaskMapper taskMapper = mock(ImageVideoAsyncTaskMapper.class);
|
||||
ImageVideoCozeService cozeService = mock(ImageVideoCozeService.class);
|
||||
ImageVideoWorkflowConfigService workflowConfigService = mock(ImageVideoWorkflowConfigService.class);
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
TaskExecutor directExecutor = Runnable::run;
|
||||
ImageVideoAsyncTaskService service = new ImageVideoAsyncTaskService(
|
||||
taskMapper, cozeService, workflowConfigService, objectMapper, directExecutor);
|
||||
|
||||
ImageVideoAsyncTaskEntity task = waitingDouyinTask();
|
||||
Map<String, Object> cozeResult = Map.of("data", List.of(Map.of(
|
||||
"execute_status", "Success",
|
||||
"output", "{\"Output\":\"{\\\"data\\\":{\\\"source\\\":\\\"这条裙裤绝了,遮肉显腿长,快拍!\\\"}}\"}"
|
||||
)));
|
||||
DouyinCopyVo copyResult = new DouyinCopyVo();
|
||||
copyResult.setRecognizedContent("这条裙裤绝了,遮肉显腿长,快拍!");
|
||||
copyResult.setScriptDraft("这条裙裤绝了,遮肉显腿长,快拍!");
|
||||
|
||||
when(taskMapper.selectList(any())).thenReturn(List.of(task));
|
||||
when(taskMapper.claimWaiting(79L)).thenReturn(1);
|
||||
when(taskMapper.selectById(79L)).thenReturn(task);
|
||||
when(workflowConfigService.douyinCopyWorkflowId()).thenReturn("7652941112982798388");
|
||||
when(cozeService.getWorkflowResult(1L, "7652941112982798388", "7662364426301964329"))
|
||||
.thenReturn(cozeResult);
|
||||
when(cozeService.parseDouyinCopyResult(cozeResult)).thenReturn(copyResult);
|
||||
|
||||
service.pollWaitingTasks();
|
||||
|
||||
assertEquals("SUCCESS", task.getStatus());
|
||||
assertEquals("SUCCESS", task.getCozeStatus());
|
||||
assertNotNull(task.getCompletedAt());
|
||||
assertTrue(task.getResultJson().contains("这条裙裤绝了,遮肉显腿长,快拍!"));
|
||||
verify(cozeService).parseDouyinCopyResult(cozeResult);
|
||||
verify(taskMapper).updateById(task);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pollWaitingTaskRemainsWaitingWhenCozeStatusIsRunningInsideDataArray() {
|
||||
ImageVideoAsyncTaskMapper taskMapper = mock(ImageVideoAsyncTaskMapper.class);
|
||||
ImageVideoCozeService cozeService = mock(ImageVideoCozeService.class);
|
||||
ImageVideoWorkflowConfigService workflowConfigService = mock(ImageVideoWorkflowConfigService.class);
|
||||
ImageVideoAsyncTaskService service = new ImageVideoAsyncTaskService(
|
||||
taskMapper, cozeService, workflowConfigService, new ObjectMapper(), Runnable::run);
|
||||
|
||||
ImageVideoAsyncTaskEntity task = waitingDouyinTask();
|
||||
Map<String, Object> cozeResult = Map.of("data", List.of(Map.of("execute_status", "Running")));
|
||||
|
||||
when(taskMapper.selectList(any())).thenReturn(List.of(task));
|
||||
when(taskMapper.claimWaiting(79L)).thenReturn(1);
|
||||
when(taskMapper.selectById(79L)).thenReturn(task);
|
||||
when(workflowConfigService.douyinCopyWorkflowId()).thenReturn("7652941112982798388");
|
||||
when(cozeService.getWorkflowResult(1L, "7652941112982798388", "7662364426301964329"))
|
||||
.thenReturn(cozeResult);
|
||||
|
||||
service.pollWaitingTasks();
|
||||
|
||||
assertEquals("WAITING", task.getStatus());
|
||||
assertEquals("RUNNING", task.getCozeStatus());
|
||||
verify(cozeService, never()).parseDouyinCopyResult(any());
|
||||
verify(taskMapper).updateById(task);
|
||||
}
|
||||
|
||||
private ImageVideoAsyncTaskEntity waitingDouyinTask() {
|
||||
ImageVideoAsyncTaskEntity task = new ImageVideoAsyncTaskEntity();
|
||||
task.setId(79L);
|
||||
task.setUserId(1L);
|
||||
task.setTaskType("DOUYIN_COPY");
|
||||
task.setStatus("WAITING");
|
||||
task.setCozeExecuteId("7662364426301964329");
|
||||
task.setSubmittedAt(LocalDateTime.now());
|
||||
task.setCreatedAt(LocalDateTime.now());
|
||||
task.setUpdatedAt(LocalDateTime.now());
|
||||
return task;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user