更新带货视频工作流接口和页面

This commit is contained in:
super
2026-07-09 00:27:49 +08:00
parent 0ecf5cd45a
commit aab4ce942c
51 changed files with 4777 additions and 820 deletions
@@ -1,35 +1,55 @@
package com.nanri.aiimage.modules.pricetrack.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.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 跟价跳过 ASIN 分页返回
* Price-track skipped ASIN page response.
*/
@Data
@Schema(description = "跟价跳过 ASIN 分页返回")
@Schema(description = "Price-track skipped ASIN page response")
public class SkipPriceAsinPageVo {
@Schema(description = "当前页码,从 1 开始")
@Schema(description = "Current page, starting from 1")
private Integer page;
@Schema(description = "每页条数")
@Schema(description = "Page size")
private Integer pageSize;
@Schema(description = "总记录数")
@Schema(description = "Total records")
private Long total;
@Schema(description = "总页数")
@Schema(description = "Total pages")
private Integer totalPages;
@Schema(description = "按国家分组的 ASIN 列表(简化版,仅 ASIN)")
@Schema(description = "ASIN list grouped by country")
private Map<String, List<String>> skipAsinsByCountry = new LinkedHashMap<>();
@Schema(description = "按国家分组的 ASIN 详情列表(包含 asin minimumPrice")
@Schema(description = "ASIN detail list grouped by country, including asin and minimumPrice")
private Map<String, List<Map<String, String>>> skipAsinDetailsByCountry = new LinkedHashMap<>();
@JsonProperty("skip_asins")
@Schema(description = "Legacy Python queue field: ASIN list grouped by country")
private Map<String, List<String>> skipAsins = new LinkedHashMap<>();
@JsonProperty("skip_asins_by_country")
@Schema(description = "Legacy Python queue field: ASIN list grouped by country")
private Map<String, List<String>> skipAsinsByCountryLegacy = new LinkedHashMap<>();
@JsonProperty("skip_asin_details_by_country")
@Schema(description = "Legacy Python queue field: ASIN detail list grouped by country")
private Map<String, List<Map<String, String>>> skipAsinDetailsByCountryLegacy = new LinkedHashMap<>();
@JsonProperty("asin_rows_by_country")
@Schema(description = "Legacy Python queue field: appointed ASIN rows grouped by country")
private Map<String, List<Map<String, String>>> asinRowsByCountry = new LinkedHashMap<>();
@JsonProperty("minimum_price_by_country_and_asin")
@Schema(description = "Legacy Python queue field: minimum price map grouped by country and ASIN")
private Map<String, Map<String, String>> minimumPriceByCountryAndAsin = new LinkedHashMap<>();
}
@@ -61,6 +61,7 @@ public class PriceTrackTaskService {
private static final String MODULE_TYPE = "PRICE_TRACK";
private static final String CONTENT_TYPE_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
private static final String ASIN_ROWS_PAYLOAD_SCOPE = "price-track-asin-rows";
private final FileTaskMapper fileTaskMapper;
private final FileResultMapper fileResultMapper;
@@ -333,7 +334,7 @@ public class PriceTrackTaskService {
if (taskIds == null || taskIds.isEmpty()) {
return batch;
}
Map<Long, FileTaskEntity> taskMap = loadTaskMapByIds(taskIds);
Map<Long, FileTaskEntity> taskMap = loadTaskProgressMapByIds(taskIds);
for (Long taskId : taskIds) {
if (taskId == null || taskId <= 0) {
continue;
@@ -350,6 +351,41 @@ public class PriceTrackTaskService {
return batch;
}
private Map<Long, FileTaskEntity> loadTaskProgressMapByIds(List<Long> taskIds) {
Map<Long, FileTaskEntity> result = new LinkedHashMap<>();
if (taskIds == null || taskIds.isEmpty()) {
return result;
}
List<Long> normalizedTaskIds = taskIds.stream()
.filter(taskId -> taskId != null && taskId > 0)
.distinct()
.toList();
if (normalizedTaskIds.isEmpty()) {
return result;
}
int batchSize = Math.max(1, taskPressureProperties.getDbSelectBatchSize());
for (int start = 0; start < normalizedTaskIds.size(); start += batchSize) {
int end = Math.min(start + batchSize, normalizedTaskIds.size());
List<FileTaskEntity> tasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
.select(FileTaskEntity::getId,
FileTaskEntity::getTaskNo,
FileTaskEntity::getStatus,
FileTaskEntity::getErrorMessage,
FileTaskEntity::getCreatedAt,
FileTaskEntity::getUpdatedAt,
FileTaskEntity::getFinishedAt,
FileTaskEntity::getRequestJson)
.eq(FileTaskEntity::getModuleType, MODULE_TYPE)
.in(FileTaskEntity::getId, normalizedTaskIds.subList(start, end)));
for (FileTaskEntity task : tasks) {
if (task != null && task.getId() != null) {
result.put(task.getId(), task);
}
}
}
return result;
}
@Transactional
public PriceTrackCreateTaskVo createTask(PriceTrackCreateTaskRequest request) {
if (request.isStatusMode() == request.isAsinMode()) throw new BusinessException("跟价模式必须二选一");
@@ -372,7 +408,11 @@ public class PriceTrackTaskService {
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<>();
Map<String, List<Map<String, String>>> asinRowsForPayload = new LinkedHashMap<>();
Map<String, Map<String, String>> minimumPriceByCountryAndAsin = new LinkedHashMap<>();
if (request.isAsinMode()) {
asinRowsForPayload = parseAsinRowsByCountry(request.getAsinFiles(), request.getCountryCodes());
}
log.info("[price-track] createTask skipAsins countries={} asinMode={}",
skipAsinsByCountry.keySet(), request.isAsinMode());
@@ -391,6 +431,9 @@ public class PriceTrackTaskService {
task.setUpdatedAt(LocalDateTime.now());
fileTaskMapper.insert(task);
priceTrackTaskCacheService.touchTaskHeartbeat(task.getId());
if (request.isAsinMode()) {
saveTaskAsinRowsPayload(task.getId(), asinRowsForPayload);
}
if (request.getLoopRunId() != null) {
priceTrackLoopRunService.bindChildTask(request.getLoopRunId(), task.getId(), request.getRoundIndex(), request.getShopIndex());
}
@@ -507,7 +550,7 @@ public class PriceTrackTaskService {
continue;
}
if (countPayloadRows(merged) <= 0) {
markResultFailed(fr, "no usable price-track rows received");
markResultFailed(fr, buildNoUsableRowsMessage(payload));
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
continue;
}
@@ -584,7 +627,7 @@ public class PriceTrackTaskService {
continue;
}
if (countPayloadRows(cachedPayload) <= 0) {
markResultFailed(fr, "no usable price-track rows received before interruption");
markResultFailed(fr, buildNoUsableRowsMessage(cachedPayload));
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
changed = true;
log.warn("[price-track] stale finalize finished without data taskId={} shop={} fromCompensation={}",
@@ -784,21 +827,42 @@ public class PriceTrackTaskService {
PriceTrackCreateTaskRequest request = parseTaskRequest(task);
// 从任务中获取国家代码
// Resolve country codes from task request.
List<String> countryCodes = request.getCountryCodes();
// 判断是全量模式还是文件模式
// Choose full mode or uploaded-ASIN file mode.
boolean isAsinMode = request.isAsinMode();
if (isAsinMode) {
// 文件模式:从任务 requestJson 中的上传文件解析数据并分页返回
Map<String, List<Map<String, String>>> storedRows = loadTaskAsinRowsPayload(taskId);
if (storedRows != null) {
return getTaskAsinRowsPaginated(storedRows, countryCodes, page, pageSize);
}
// File mode reads parsed uploaded ASIN rows.
return getTaskAsinFilePaginated(request, countryCodes, page, pageSize);
} else {
// 全量模式:从 biz_skip_price_asin 表查询
return skipPriceAsinService.listSkipAsinsPaginated(null, countryCodes, page, pageSize);
// Full mode uses all ASINs and minimum prices from biz_skip_price_asin.
return skipPriceAsinService.listSkipAsinsPaginated(List.of(), countryCodes, page, pageSize);
}
}
private List<String> resolveTaskShopNames(PriceTrackCreateTaskRequest request) {
List<String> shopNames = new ArrayList<>();
if (request == null || request.getItems() == null) {
return shopNames;
}
for (PriceTrackMatchShopsVo.PriceTrackShopQueueItem item : request.getItems()) {
if (item == null) {
continue;
}
String normalized = ziniaoShopSwitchService.normalizeShopName(item.getShopName());
if (!normalized.isBlank() && !shopNames.contains(normalized)) {
shopNames.add(normalized);
}
}
return shopNames;
}
public SkipPriceAsinCheckVo checkTaskSkipAsin(Long taskId, String country, String asin) {
if (taskId == null || taskId <= 0) {
throw new BusinessException("taskId 不合法");
@@ -909,11 +973,13 @@ public class PriceTrackTaskService {
// 提取当前页的数据(跨国家顺序提取)
Map<String, List<String>> skipAsinsByCountry = new LinkedHashMap<>();
Map<String, List<Map<String, String>>> skipDetailsByCountry = new LinkedHashMap<>();
Map<String, List<Map<String, String>>> asinRowsByCountry = new LinkedHashMap<>();
// 初始化所有目标国家
for (String country : targetCountries) {
skipAsinsByCountry.put(country, new ArrayList<>());
skipDetailsByCountry.put(country, new ArrayList<>());
asinRowsByCountry.put(country, new ArrayList<>());
}
int currentIndex = 0;
@@ -938,6 +1004,7 @@ public class PriceTrackTaskService {
detail.put("asin", asin);
detail.put("minimumPrice", minimumPrice == null ? "" : minimumPrice);
skipDetailsByCountry.get(country).add(detail);
asinRowsByCountry.get(country).add(new LinkedHashMap<>(row));
}
}
@@ -960,10 +1027,168 @@ public class PriceTrackTaskService {
vo.setTotalPages(totalPages);
vo.setSkipAsinsByCountry(skipAsinsByCountry);
vo.setSkipAsinDetailsByCountry(skipDetailsByCountry);
vo.setSkipAsins(skipAsinsByCountry);
vo.setSkipAsinsByCountryLegacy(skipAsinsByCountry);
vo.setSkipAsinDetailsByCountryLegacy(skipDetailsByCountry);
vo.setAsinRowsByCountry(asinRowsByCountry);
vo.setMinimumPriceByCountryAndAsin(buildMinimumPriceByCountryAndAsin(asinRowsByCountry));
return vo;
}
private com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo getTaskAsinRowsPaginated(
Map<String, List<Map<String, String>>> allAsinRows, List<String> countryCodes, int page, int pageSize) {
if (page < 1) {
page = 1;
}
if (pageSize < 1 || pageSize > 2000) {
pageSize = 1000;
}
if (allAsinRows == null) {
allAsinRows = new LinkedHashMap<>();
}
List<String> targetCountries = (countryCodes == null || countryCodes.isEmpty())
? new ArrayList<>(allAsinRows.keySet())
: countryCodes;
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<>();
Map<String, List<Map<String, String>>> asinRowsByCountry = new LinkedHashMap<>();
for (String country : targetCountries) {
skipAsinsByCountry.put(country, new ArrayList<>());
skipDetailsByCountry.put(country, new ArrayList<>());
asinRowsByCountry.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);
asinRowsByCountry.get(country).add(new LinkedHashMap<>(row));
}
}
currentIndex++;
if (currentIndex >= endIndex) {
shouldBreak = true;
break;
}
}
}
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);
vo.setSkipAsins(skipAsinsByCountry);
vo.setSkipAsinsByCountryLegacy(skipAsinsByCountry);
vo.setSkipAsinDetailsByCountryLegacy(skipDetailsByCountry);
vo.setAsinRowsByCountry(asinRowsByCountry);
vo.setMinimumPriceByCountryAndAsin(buildMinimumPriceByCountryAndAsin(asinRowsByCountry));
return vo;
}
private void saveTaskAsinRowsPayload(Long taskId, Map<String, List<Map<String, String>>> asinRowsByCountry) {
if (asinRowsByCountry == null) {
return;
}
taskResultPayloadService.saveLatest(taskId, MODULE_TYPE, ASIN_ROWS_PAYLOAD_SCOPE, asinRowsByCountry);
log.info("[price-track] saved asin rows payload taskId={} countries={} rows={}",
taskId, asinRowsByCountry.keySet(), countAsinRows(asinRowsByCountry));
}
private Map<String, List<Map<String, String>>> loadTaskAsinRowsPayload(Long taskId) {
try {
Object payload = taskResultPayloadService.getLatest(taskId, MODULE_TYPE, ASIN_ROWS_PAYLOAD_SCOPE, Map.class);
if (payload == null) {
return null;
}
Map<String, List<Map<String, String>>> rows = normalizeAsinRowsPayload(payload);
if (!rows.isEmpty()) {
log.info("[price-track] loaded asin rows payload taskId={} countries={} rows={}",
taskId, rows.keySet(), countAsinRows(rows));
}
return rows;
} catch (Exception ex) {
log.warn("[price-track] load asin rows payload failed taskId={} err={}", taskId, ex.getMessage());
return null;
}
}
private Map<String, List<Map<String, String>>> normalizeAsinRowsPayload(Object payload) {
Map<String, List<Map<String, String>>> out = new LinkedHashMap<>();
if (!(payload instanceof Map<?, ?> rawMap)) {
return out;
}
for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
String country = Objects.toString(entry.getKey(), "").trim().toUpperCase(Locale.ROOT);
if (country.isBlank()) {
continue;
}
List<Map<String, String>> rows = new ArrayList<>();
if (entry.getValue() instanceof List<?> rawRows) {
for (Object rawRow : rawRows) {
if (!(rawRow instanceof Map<?, ?> rawRowMap)) {
continue;
}
Map<String, String> row = new LinkedHashMap<>();
for (Map.Entry<?, ?> cell : rawRowMap.entrySet()) {
String key = Objects.toString(cell.getKey(), "").trim();
if (key.isBlank()) {
continue;
}
row.put(key, Objects.toString(cell.getValue(), ""));
}
if (!row.isEmpty()) {
rows.add(row);
}
}
}
out.put(country, rows);
}
return out;
}
private long countAsinRows(Map<String, List<Map<String, String>>> asinRowsByCountry) {
if (asinRowsByCountry == null || asinRowsByCountry.isEmpty()) {
return 0L;
}
long total = 0L;
for (List<Map<String, String>> rows : asinRowsByCountry.values()) {
if (rows != null) {
total += rows.size();
}
}
return total;
}
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()) {
@@ -1149,7 +1374,7 @@ public class PriceTrackTaskService {
}
private boolean isEmptyAsinRow(Map<String, String> row) {
return row.values().stream().allMatch(value -> value == null || value.isBlank());
return row == null || normalizeCellText(row.get("asin")).isBlank();
}
private String mapHeaderKey(String rawHeader) {
@@ -1159,7 +1384,9 @@ public class PriceTrackTaskService {
}
return switch (header) {
case "店铺商城名称", "shopmallname" -> "shopMallName";
case "asin" -> "asin";
case "asin", "asin码", "asin编码", "商品asin", "商品编码", "商品编号",
"子asin", "父asin", "amazonasin", "amazon_asin", "amazon asin",
"sellerasin", "seller_asin", "seller asin" -> "asin";
case "价格", "price" -> "price";
case "推荐价", "recommendedprice" -> "recommendedPrice";
case "最低价", "minimumprice" -> "minimumPrice";
@@ -1498,7 +1725,30 @@ public class PriceTrackTaskService {
if (payload == null) {
return 0;
}
return excelAssemblyService.countRows(excelAssemblyService.normalizeCountriesMap(payload.getCountries()));
Map<String, List<PriceTrackSubmitResultRequest.AsinResult>> countries =
excelAssemblyService.normalizeCountriesMap(payload.getCountries());
if (countries.isEmpty()) {
return 0;
}
int count = 0;
for (List<PriceTrackSubmitResultRequest.AsinResult> rows : countries.values()) {
if (rows == null || rows.isEmpty()) {
continue;
}
for (PriceTrackSubmitResultRequest.AsinResult row : rows) {
if (row != null && hasText(row.getAsin())) {
count++;
}
}
}
return count;
}
private String buildNoUsableRowsMessage(PriceTrackSubmitResultRequest.ShopResult payload) {
if (payload != null && hasText(payload.getError())) {
return payload.getError().trim();
}
return "no usable price-track rows received";
}
private String firstNonBlank(String preferred, String fallback) {
if (preferred != null && !preferred.isBlank()) {