更新跟价相关内容修改

This commit is contained in:
super
2026-04-20 00:41:49 +08:00
parent 9b1138c83e
commit 72c8167472
22 changed files with 598 additions and 147 deletions

View File

@@ -238,9 +238,10 @@ public class DeleteBrandStaleTaskService {
runningTasks.size(), threshold, minutes, initialThreshold, initialMinutes);
for (FileTaskEntity task : runningTasks) {
long lastHeartbeatMillis = priceTrackTaskCacheService.getTaskHeartbeatMillis(task.getId());
boolean hasUploadedPayload = priceTrackTaskCacheService.hasAnyShopMergedPayload(task.getId());
boolean hasFinishedRows = (task.getSuccessFileCount() != null && task.getSuccessFileCount() > 0)
|| (task.getFailedFileCount() != null && task.getFailedFileCount() > 0);
boolean hasStartedProgress = lastHeartbeatMillis > 0L || hasFinishedRows;
boolean hasStartedProgress = lastHeartbeatMillis > 0L || hasUploadedPayload || hasFinishedRows;
if (lastHeartbeatMillis > 0L && nowMillis - lastHeartbeatMillis < staleTimeoutMillis) {
stats.skippedTaskCount++;
continue;

View File

@@ -209,9 +209,20 @@ public class PriceTrackController {
}
@PostMapping("/tasks/{taskId}/result")
@Operation(summary = "Python 回传处理结果", description = "同一 taskId 可多次回传。成功时按 countries 生成结果文件,所有店铺进入终态后任务会变为 SUCCESS 或 FAILED。")
@Operation(
summary = "Python 回传处理结果",
description = "同一 taskId 支持多次回传。后端会先按店铺合并缓存,再在该店铺 success=true 时组装最终结果文件。"
+ "未完成/处理中本次只传增量行数据shop.success 不传或传 null后端仅合并缓存继续等待。"
+ "已完成:传最后一批数据并设置 shop.success=true后端会使用该店铺累计后的完整数据出结果。"
+ "失败:传 shop.success=false 或传非空 shop.error。"
+ "行级数据只保留表头字段shopMallName、asin、price、recommendedPrice、minimumPrice、firstPlace、secondPlace、cartShopName、priceChangeStatus、modifyCount、status。"
)
public ApiResponse<Void> submitResult(
@Parameter(description = "任务主键,必须处于 RUNNING 状态", example = "200") @PathVariable Long taskId,
@Parameter(
description = "任务主键,必须处于 RUNNING 状态",
example = "200"
)
@PathVariable Long taskId,
@Valid @RequestBody PriceTrackSubmitResultRequest request) {
priceTrackTaskService.submitResult(taskId, request);
return ApiResponse.success(null);

View File

@@ -7,73 +7,91 @@ import java.util.List;
import java.util.Map;
@Data
@Schema(description = "Python submit payload for price track task results")
@Schema(
description = "跟价任务结果回传请求体。"
+ "同一 taskId 支持多次回传。"
+ "未完成的店铺可分批回传shop.success 不传或传 null 即可;"
+ "店铺最终完成时传 shop.success=true后端会基于该店铺累计数据组装结果文件"
+ "店铺失败时传 shop.success=false 或传非空的 shop.error。"
)
public class PriceTrackSubmitResultRequest {
@Schema(description = "Shop result list", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(
description = "店铺结果列表。一次请求可以提交一个或多个店铺的数据。",
requiredMode = Schema.RequiredMode.REQUIRED
)
private List<ShopResult> shops;
@Data
@Schema(description = "Per-shop submit result")
@Schema(
description = "单个店铺的回传结果。"
+ "处理中/未完成时success 不传或传 null"
+ "店铺最终完成时传 success=true"
+ "店铺失败时传 success=false 或 error 非空。"
)
public static class ShopResult {
@Schema(description = "Shop name used to match task rows", example = "Test Shop A")
@Schema(description = "店铺名称,用于和任务中的店铺结果行进行匹配", example = "测试店铺A")
private String shopName;
@Schema(description = "Rows grouped by country code, for example DE/UK/FR/IT/ES")
@Schema(
description = "按国家代码分组的结果行,例如 DE、UK、FR、IT、ES。"
+ "每一行只允许保留最终表头中的字段。"
)
private Map<String, List<AsinResult>> countries;
@Schema(description = "Failure message")
@Schema(
description = "失败信息。非空时后端会将该店铺标记为失败,并停止等待该店铺后续回传。",
example = ""
)
private String error;
@Schema(description = "Whether the shop finished successfully")
@Schema(
description = "店铺完成标记。"
+ "不传或传 null 表示本次只是部分回传,后端仅合并缓存并继续等待;"
+ "true 表示该店铺已全部完成,后端会组装最终结果文件;"
+ "false 表示该店铺处理失败。",
example = "true"
)
private Boolean success;
}
@Data
@Schema(description = "Per-ASIN price track result row")
@Schema(
description = "单条 ASIN 跟价结果行。"
+ "只保留最终表头字段。"
+ "不要再传旧字段,如 currentPrice、competitorPrice、action、done。"
)
public static class AsinResult {
@Schema(description = "Shop mall name", example = "Amazon.de")
@Schema(description = "店铺商城名称", example = "Amazon.de")
private String shopMallName;
@Schema(description = "ASIN", example = "B0ABCDE123")
private String asin;
@Schema(description = "Price", example = "19.99")
@Schema(description = "价格", example = "19.99")
private String price;
@Schema(description = "Recommended price", example = "18.99")
@Schema(description = "推荐价", example = "18.99")
private String recommendedPrice;
@Schema(description = "Lowest price", example = "18.50")
@Schema(description = "最低价", example = "18.50")
private String minimumPrice;
@Schema(description = "First place", example = "Competitor A")
@Schema(description = "第一名", example = "竞对A")
private String firstPlace;
@Schema(description = "Second place", example = "Competitor B")
@Schema(description = "第二名", example = "竞对B")
private String secondPlace;
@Schema(description = "Buy box shop name", example = "Shop A")
@Schema(description = "购物车店铺名", example = "店铺A")
private String cartShopName;
@Schema(description = "Price change status", example = "UPDATED")
@Schema(description = "改价情况", example = "UPDATED")
private String priceChangeStatus;
@Schema(description = "Modify count", example = "2")
@Schema(description = "修改次数", example = "2")
private String modifyCount;
@Schema(description = "Status", example = "SUCCESS")
@Schema(description = "状态", example = "SUCCESS")
private String status;
@Schema(description = "Legacy current price", example = "19.99")
private String currentPrice;
@Schema(description = "Legacy competitor price", example = "18.50")
private String competitorPrice;
@Schema(description = "Legacy action", example = "LOWER_PRICE")
private String action;
@Schema(description = "Legacy done flag")
private Boolean done;
}
}

View File

@@ -27,6 +27,9 @@ public class PriceTrackMatchShopsVo {
@Schema(description = "Shop name", example = "Demo Shop")
private String shopName;
@Schema(description = "Shop mall name", example = "Amazon.de")
private String shopMallName;
@Schema(description = "Matched shop id")
private Object shopId;

View File

@@ -17,6 +17,9 @@ public class PriceTrackResultItemVo {
@Schema(description = "店铺名称,详情页会尽量恢复创建任务时的展示名")
private String shopName;
@Schema(description = "Shop mall name")
private String shopMallName;
@Schema(description = "店铺 ID紫鸟")
private Object shopId;

View File

@@ -52,15 +52,15 @@ public class PriceTrackExcelAssemblyService {
Row row = sheet.createRow(rowIdx++);
row.createCell(0).setCellValue(valueOf(item.getShopMallName()));
row.createCell(1).setCellValue(valueOf(item.getAsin()));
row.createCell(2).setCellValue(firstNonBlank(item.getPrice(), item.getCurrentPrice()));
row.createCell(2).setCellValue(valueOf(item.getPrice()));
row.createCell(3).setCellValue(valueOf(item.getRecommendedPrice()));
row.createCell(4).setCellValue(firstNonBlank(item.getMinimumPrice(), item.getCompetitorPrice()));
row.createCell(4).setCellValue(valueOf(item.getMinimumPrice()));
row.createCell(5).setCellValue(valueOf(item.getFirstPlace()));
row.createCell(6).setCellValue(valueOf(item.getSecondPlace()));
row.createCell(7).setCellValue(valueOf(item.getCartShopName()));
row.createCell(8).setCellValue(firstNonBlank(item.getPriceChangeStatus(), item.getAction()));
row.createCell(8).setCellValue(valueOf(item.getPriceChangeStatus()));
row.createCell(9).setCellValue(valueOf(item.getModifyCount()));
row.createCell(10).setCellValue(firstNonBlank(item.getStatus(), item.getDone() == null ? null : String.valueOf(item.getDone())));
row.createCell(10).setCellValue(valueOf(item.getStatus()));
}
for (int i = 0; i < RESULT_HEADER.length; i++) {
sheet.autoSizeColumn(i);
@@ -105,15 +105,4 @@ public class PriceTrackExcelAssemblyService {
return value == null ? "" : value;
}
private static String firstNonBlank(String... values) {
if (values == null || values.length == 0) {
return "";
}
for (String value : values) {
if (value != null && !value.isBlank()) {
return value;
}
}
return "";
}
}

View File

@@ -15,6 +15,7 @@ import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackCandidateVo;
import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackCountryPreferenceVo;
import com.nanri.aiimage.modules.pricetrack.model.vo.PriceTrackMatchShopsVo;
import com.nanri.aiimage.modules.shopkey.service.SkipPriceAsinService;
import com.nanri.aiimage.modules.shopkey.service.ShopManageService;
import com.nanri.aiimage.modules.ziniao.model.vo.ZiniaoShopMatchResultVo;
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopIndexService;
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopSwitchService;
@@ -43,6 +44,7 @@ public class PriceTrackService {
private final ZiniaoShopSwitchService ziniaoShopSwitchService;
private final SkipPriceAsinService skipPriceAsinService;
private final PriceTrackTaskService priceTrackTaskService;
private final ShopManageService shopManageService;
public List<PriceTrackCandidateVo> listCandidates(Long userId) {
if (userId == null || userId <= 0) {
@@ -220,6 +222,7 @@ public class PriceTrackService {
) {
PriceTrackMatchShopsVo.PriceTrackShopQueueItem item = new PriceTrackMatchShopsVo.PriceTrackShopQueueItem();
item.setShopName(shopName);
item.setShopMallName(resolveShopMallName(shopName));
item.setSkipAsins(skipAsins == null ? new LinkedHashMap<>() : new LinkedHashMap<>(skipAsins));
try {
ZiniaoShopMatchResultVo match = ziniaoShopSwitchService.findIndexedStoreByName(shopName, false);
@@ -247,6 +250,14 @@ public class PriceTrackService {
return item;
}
private String resolveShopMallName(String shopName) {
try {
return shopManageService.findMallNameByShopName(shopName);
} catch (Exception ignored) {
return null;
}
}
private static List<String> sanitizeStoredCodes(List<String> raw) {
List<String> parsed = parseValidCountryCodes(raw);
if (parsed.isEmpty()) {

View File

@@ -1,11 +1,16 @@
package com.nanri.aiimage.modules.pricetrack.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackSubmitResultRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
@Service
@RequiredArgsConstructor
@@ -14,6 +19,7 @@ public class PriceTrackTaskCacheService {
private static final long HEARTBEAT_TTL_HOURS = 24;
private final StringRedisTemplate stringRedisTemplate;
private final ObjectMapper objectMapper;
public void touchTaskHeartbeat(Long taskId) {
if (taskId == null || taskId <= 0) {
@@ -40,13 +46,80 @@ public class PriceTrackTaskCacheService {
}
}
public PriceTrackSubmitResultRequest.ShopResult getShopMergedPayload(Long taskId, String shopKey) {
if (taskId == null || taskId <= 0 || shopKey == null || shopKey.isBlank()) {
return null;
}
Object raw = stringRedisTemplate.opsForHash().get(buildShopPayloadKey(taskId), shopKey);
if (!(raw instanceof String json) || json.isBlank()) {
return null;
}
try {
return objectMapper.readValue(json, PriceTrackSubmitResultRequest.ShopResult.class);
} catch (Exception ex) {
throw new BusinessException("read price-track cache failed");
}
}
public void saveShopMergedPayload(Long taskId, String shopKey, PriceTrackSubmitResultRequest.ShopResult payload) {
if (taskId == null || taskId <= 0 || shopKey == null || shopKey.isBlank() || payload == null) {
return;
}
try {
stringRedisTemplate.opsForHash().put(buildShopPayloadKey(taskId), shopKey, objectMapper.writeValueAsString(payload));
stringRedisTemplate.expire(buildShopPayloadKey(taskId), Duration.ofHours(HEARTBEAT_TTL_HOURS));
touchTaskHeartbeat(taskId);
} catch (Exception ex) {
throw new BusinessException("save price-track cache failed");
}
}
public void removeShopMergedPayload(Long taskId, String shopKey) {
if (taskId == null || taskId <= 0 || shopKey == null || shopKey.isBlank()) {
return;
}
stringRedisTemplate.opsForHash().delete(buildShopPayloadKey(taskId), shopKey);
}
public Map<String, PriceTrackSubmitResultRequest.ShopResult> getAllShopMergedPayload(Long taskId) {
try {
Map<Object, Object> raw = stringRedisTemplate.opsForHash().entries(buildShopPayloadKey(taskId));
if (raw == null || raw.isEmpty()) {
return Map.of();
}
LinkedHashMap<String, PriceTrackSubmitResultRequest.ShopResult> out = new LinkedHashMap<>();
for (Map.Entry<Object, Object> entry : raw.entrySet()) {
if (!(entry.getKey() instanceof String key) || !(entry.getValue() instanceof String val) || val.isBlank()) {
continue;
}
out.put(key, objectMapper.readValue(val, PriceTrackSubmitResultRequest.ShopResult.class));
}
return out;
} catch (Exception ex) {
throw new BusinessException("read price-track cache failed");
}
}
public boolean hasAnyShopMergedPayload(Long taskId) {
if (taskId == null || taskId <= 0) {
return false;
}
Long size = stringRedisTemplate.opsForHash().size(buildShopPayloadKey(taskId));
return size != null && size > 0;
}
public void deleteTaskCache(Long taskId) {
if (taskId == null || taskId <= 0) {
return;
}
stringRedisTemplate.delete(buildShopPayloadKey(taskId));
stringRedisTemplate.delete(buildTaskHeartbeatKey(taskId));
}
private String buildShopPayloadKey(Long taskId) {
return "price-track:task:shop-payload:" + taskId;
}
private String buildTaskHeartbeatKey(Long taskId) {
return "price-track:task:heartbeat:" + taskId;
}

View File

@@ -323,49 +323,63 @@ public class PriceTrackTaskService {
log.warn("[price-track] submitResult rejected taskId={} status={}", taskId, task.getStatus());
throw new BusinessException(40901, "任务已结束,拒绝重复提交");
}
priceTrackTaskCacheService.touchTaskHeartbeat(taskId);
List<FileResultEntity> resultRows = fileResultMapper.selectList(
new LambdaQueryWrapper<FileResultEntity>()
.eq(FileResultEntity::getTaskId, taskId)
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
.orderByAsc(FileResultEntity::getId));
Map<String, PriceTrackSubmitResultRequest.ShopResult> payloadByShop = new LinkedHashMap<>();
for (PriceTrackSubmitResultRequest.ShopResult sr : request.getShops()) {
if (sr == null) continue;
String key = ziniaoShopSwitchService.normalizeShopName(sr.getShopName());
if (!key.isBlank()) payloadByShop.put(key, sr);
}
log.info("[price-track] submitResult received taskId={} payloadShopCount={} payloadKeys={}",
taskId, payloadByShop.size(), payloadByShop.keySet());
boolean changed = false;
for (FileResultEntity fr : resultRows) {
String shopKey = fr.getSourceFilename();
PriceTrackSubmitResultRequest.ShopResult payload = shopKey == null ? null : payloadByShop.get(shopKey);
if (payload == null) {
// 当前批次没有该店铺的 payload保持待处理
continue;
}
changed = true;
if (payload.getError() != null && !payload.getError().isBlank()) {
markResultFailed(fr, payload.getError());
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
continue;
}
if (Boolean.FALSE.equals(payload.getSuccess())) {
markResultFailed(fr, "店铺处理失败");
markResultFailed(fr, "shop processing failed");
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
continue;
}
PriceTrackSubmitResultRequest.ShopResult merged = mergeShopPayload(taskId, shopKey, payload);
if (!Boolean.TRUE.equals(merged.getSuccess())) {
continue;
}
if (countPayloadRows(merged) <= 0) {
markResultFailed(fr, "no usable price-track rows received");
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
continue;
}
try {
assembleShopResult(fr, shopKey, payload);
assembleShopResult(fr, shopKey, merged);
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
} catch (Exception ex) {
markResultFailed(fr, ex.getMessage() == null ? "组装结果失败" : ex.getMessage());
markResultFailed(fr, ex.getMessage() == null ? "assemble price-track result failed" : ex.getMessage());
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
}
}
if (!changed) {
return;
}
List<FileResultEntity> latest = fileResultMapper.selectList(
new LambdaQueryWrapper<FileResultEntity>()
.eq(FileResultEntity::getTaskId, taskId)
.eq(FileResultEntity::getModuleType, MODULE_TYPE)
.orderByAsc(FileResultEntity::getId));
updateTaskStatusFromLatestRows(task, latest);
}
@@ -380,7 +394,6 @@ public class PriceTrackTaskService {
cleanupTaskCacheIfTerminal(taskId, task.getStatus());
return false;
}
List<FileResultEntity> latest = fileResultMapper.selectList(
new LambdaQueryWrapper<FileResultEntity>()
.eq(FileResultEntity::getTaskId, taskId)
@@ -388,28 +401,61 @@ public class PriceTrackTaskService {
.orderByAsc(FileResultEntity::getId));
if (latest.isEmpty()) {
task.setStatus("FAILED");
task.setErrorMessage("任务明细缺失,无法自动收尾");
task.setErrorMessage("task details missing, cannot finalize automatically");
task.setUpdatedAt(LocalDateTime.now());
task.setFinishedAt(LocalDateTime.now());
fileTaskMapper.updateById(task);
cleanupTaskCacheIfTerminal(taskId, task.getStatus());
return true;
}
Map<String, PriceTrackSubmitResultRequest.ShopResult> cachedPayloadByShop =
priceTrackTaskCacheService.getAllShopMergedPayload(taskId);
boolean changed = false;
for (FileResultEntity fr : latest) {
boolean success = fr.getSuccess() != null && fr.getSuccess() == 1;
boolean failed = fr.getErrorMessage() != null && !fr.getErrorMessage().isBlank();
if (success || failed) continue;
markResultFailed(fr, "Python interrupted before this shop finished uploading results");
changed = true;
log.warn("[price-track] stale finalize failed taskId={} shop={} fromCompensation={}",
taskId, fr.getSourceFilename(), fromCompensation);
String shopKey = fr.getSourceFilename();
PriceTrackSubmitResultRequest.ShopResult cachedPayload = shopKey == null ? null : cachedPayloadByShop.get(shopKey);
if (cachedPayload == null) {
markResultFailed(fr, "Python interrupted before this shop finished uploading results");
changed = true;
log.warn("[price-track] stale finalize failed without cache taskId={} shop={} fromCompensation={}",
taskId, shopKey, fromCompensation);
continue;
}
if (cachedPayload.getError() != null && !cachedPayload.getError().isBlank()) {
markResultFailed(fr, cachedPayload.getError());
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
changed = true;
continue;
}
if (countPayloadRows(cachedPayload) <= 0) {
markResultFailed(fr, "no usable price-track rows received before interruption");
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
changed = true;
log.warn("[price-track] stale finalize finished without data taskId={} shop={} fromCompensation={}",
taskId, shopKey, fromCompensation);
continue;
}
try {
assembleShopResult(fr, shopKey, cachedPayload);
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
changed = true;
log.warn("[price-track] stale finalize assembled taskId={} shop={} fromCompensation={} completedFlag={} rowCount={}",
taskId, shopKey, fromCompensation, Boolean.TRUE.equals(cachedPayload.getSuccess()), countPayloadRows(cachedPayload));
} catch (Exception ex) {
String message = ex.getMessage() == null ? "assemble price-track result failed" : ex.getMessage();
markResultFailed(fr, message);
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
changed = true;
log.warn("[price-track] stale finalize assemble failed taskId={} shop={} msg={}",
taskId, shopKey, message);
}
}
if (!changed) {
if (!changed && cachedPayloadByShop.isEmpty()) {
return false;
}
List<FileResultEntity> refreshed = fileResultMapper.selectList(
new LambdaQueryWrapper<FileResultEntity>()
.eq(FileResultEntity::getTaskId, taskId)
@@ -723,6 +769,7 @@ public class PriceTrackTaskService {
vo.setResultId(fr.getId());
vo.setTaskId(fr.getTaskId());
vo.setShopName(item.getShopName());
vo.setShopMallName(item.getShopMallName());
vo.setShopId(item.getShopId());
vo.setPlatform(item.getPlatform());
vo.setCompanyName(item.getCompanyName());
@@ -799,6 +846,7 @@ public class PriceTrackTaskService {
if (item == null) continue;
if (key.equals(ziniaoShopSwitchService.normalizeShopName(item.getShopName()))) {
vo.setShopName(item.getShopName());
vo.setShopMallName(item.getShopMallName());
vo.setShopId(item.getShopId());
vo.setPlatform(item.getPlatform());
vo.setCompanyName(item.getCompanyName());
@@ -852,6 +900,113 @@ public class PriceTrackTaskService {
return s.replaceAll("[\\\\/:*?\"<>|]", "_");
}
private PriceTrackSubmitResultRequest.ShopResult mergeShopPayload(Long taskId,
String shopKey,
PriceTrackSubmitResultRequest.ShopResult incoming) {
PriceTrackSubmitResultRequest.ShopResult merged = priceTrackTaskCacheService.getShopMergedPayload(taskId, shopKey);
if (merged == null) {
merged = new PriceTrackSubmitResultRequest.ShopResult();
}
if (incoming.getShopName() != null && !incoming.getShopName().isBlank()) {
merged.setShopName(incoming.getShopName().trim());
} else if (merged.getShopName() == null || merged.getShopName().isBlank()) {
merged.setShopName(shopKey);
}
if (incoming.getError() != null && !incoming.getError().isBlank()) {
merged.setError(incoming.getError().trim());
}
if (incoming.getSuccess() != null) {
merged.setSuccess(incoming.getSuccess());
}
Map<String, List<PriceTrackSubmitResultRequest.AsinResult>> nextCountries =
merged.getCountries() == null ? new LinkedHashMap<>() : new LinkedHashMap<>(merged.getCountries());
if (incoming.getCountries() != null && !incoming.getCountries().isEmpty()) {
for (Map.Entry<String, List<PriceTrackSubmitResultRequest.AsinResult>> entry : incoming.getCountries().entrySet()) {
String countryCode = entry.getKey();
List<PriceTrackSubmitResultRequest.AsinResult> incomingRows = entry.getValue();
if (countryCode == null || countryCode.isBlank() || incomingRows == null || incomingRows.isEmpty()) {
continue;
}
nextCountries.put(countryCode, mergeCountryRows(nextCountries.get(countryCode), incomingRows));
}
}
merged.setCountries(nextCountries);
priceTrackTaskCacheService.saveShopMergedPayload(taskId, shopKey, merged);
return merged;
}
private List<PriceTrackSubmitResultRequest.AsinResult> mergeCountryRows(
List<PriceTrackSubmitResultRequest.AsinResult> existingRows,
List<PriceTrackSubmitResultRequest.AsinResult> incomingRows) {
LinkedHashMap<String, PriceTrackSubmitResultRequest.AsinResult> byKey = new LinkedHashMap<>();
if (existingRows != null) {
for (PriceTrackSubmitResultRequest.AsinResult row : existingRows) {
if (row != null) {
byKey.put(buildRowKey(row), cloneRow(row));
}
}
}
for (PriceTrackSubmitResultRequest.AsinResult row : incomingRows) {
if (row == null) {
continue;
}
String key = buildRowKey(row);
byKey.put(key, mergeRow(byKey.get(key), row));
}
return new ArrayList<>(byKey.values());
}
private String buildRowKey(PriceTrackSubmitResultRequest.AsinResult row) {
String asin = row == null || row.getAsin() == null ? "" : row.getAsin().trim().toUpperCase(Locale.ROOT);
return asin.isBlank() ? "row:" + java.util.UUID.randomUUID() : "asin:" + asin;
}
private PriceTrackSubmitResultRequest.AsinResult mergeRow(PriceTrackSubmitResultRequest.AsinResult previous,
PriceTrackSubmitResultRequest.AsinResult incoming) {
if (previous == null) {
return cloneRow(incoming);
}
PriceTrackSubmitResultRequest.AsinResult merged = cloneRow(previous);
merged.setShopMallName(firstNonBlank(incoming.getShopMallName(), merged.getShopMallName()));
merged.setAsin(firstNonBlank(incoming.getAsin(), merged.getAsin()));
merged.setPrice(firstNonBlank(incoming.getPrice(), merged.getPrice()));
merged.setRecommendedPrice(firstNonBlank(incoming.getRecommendedPrice(), merged.getRecommendedPrice()));
merged.setMinimumPrice(firstNonBlank(incoming.getMinimumPrice(), merged.getMinimumPrice()));
merged.setFirstPlace(firstNonBlank(incoming.getFirstPlace(), merged.getFirstPlace()));
merged.setSecondPlace(firstNonBlank(incoming.getSecondPlace(), merged.getSecondPlace()));
merged.setCartShopName(firstNonBlank(incoming.getCartShopName(), merged.getCartShopName()));
merged.setPriceChangeStatus(firstNonBlank(incoming.getPriceChangeStatus(), merged.getPriceChangeStatus()));
merged.setModifyCount(firstNonBlank(incoming.getModifyCount(), merged.getModifyCount()));
merged.setStatus(firstNonBlank(incoming.getStatus(), merged.getStatus()));
return merged;
}
private PriceTrackSubmitResultRequest.AsinResult cloneRow(PriceTrackSubmitResultRequest.AsinResult row) {
PriceTrackSubmitResultRequest.AsinResult out = new PriceTrackSubmitResultRequest.AsinResult();
if (row == null) {
return out;
}
out.setShopMallName(row.getShopMallName());
out.setAsin(row.getAsin());
out.setPrice(row.getPrice());
out.setRecommendedPrice(row.getRecommendedPrice());
out.setMinimumPrice(row.getMinimumPrice());
out.setFirstPlace(row.getFirstPlace());
out.setSecondPlace(row.getSecondPlace());
out.setCartShopName(row.getCartShopName());
out.setPriceChangeStatus(row.getPriceChangeStatus());
out.setModifyCount(row.getModifyCount());
out.setStatus(row.getStatus());
return out;
}
private int countPayloadRows(PriceTrackSubmitResultRequest.ShopResult payload) {
if (payload == null) {
return 0;
}
return excelAssemblyService.countRows(excelAssemblyService.normalizeCountriesMap(payload.getCountries()));
}
private String firstNonBlank(String preferred, String fallback) {
if (preferred != null && !preferred.isBlank()) {
return preferred;
}
return fallback;
}
private static String fmt(LocalDateTime t) {
return t == null ? null : t.toString();
}

View File

@@ -142,6 +142,15 @@ public class ShopManageService {
return vo;
}
public String findMallNameByShopName(String shopName) {
String normalizedShopName = normalizeRequired(shopName, "shopName required");
ShopManageEntity entity = shopManageMapper.selectOne(new LambdaQueryWrapper<ShopManageEntity>()
.select(ShopManageEntity::getMallName)
.eq(ShopManageEntity::getShopName, normalizedShopName)
.last("limit 1"));
return entity == null ? null : entity.getMallName();
}
private ShopManageEntity getById(Long id) {
ShopManageEntity entity = shopManageMapper.selectById(id);
if (entity == null) {