处理修复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
@@ -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(
@@ -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<>();
}
@@ -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));
}
@@ -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()) {