后端架构更新
This commit is contained in:
+1
-1
@@ -215,7 +215,7 @@ public class PriceTrackController {
|
||||
+ "未完成/处理中:本次只传增量行数据,shop.success 不传或传 null,后端仅合并缓存继续等待。"
|
||||
+ "已完成:传最后一批数据并设置 shop.success=true,后端会使用该店铺累计后的完整数据出结果。"
|
||||
+ "失败:传 shop.success=false 或传非空 shop.error。"
|
||||
+ "行级数据只保留表头字段:shopMallName、asin、price、recommendedPrice、minimumPrice、firstPlace、secondPlace、cartShopName、priceChangeStatus、modifyCount、status。"
|
||||
+ "行级数据只保留表头字段:shopMallName、asin、price、recommendedPrice、shippingFee、minimumPrice、firstPlace、secondPlace、cartShopName、priceChangeStatus、modifyCount、status。"
|
||||
)
|
||||
public ApiResponse<Void> submitResult(
|
||||
@Parameter(
|
||||
|
||||
+3
@@ -73,6 +73,9 @@ public class PriceTrackSubmitResultRequest {
|
||||
@Schema(description = "推荐价", example = "18.99")
|
||||
private String recommendedPrice;
|
||||
|
||||
@Schema(description = "运费", example = "2.50")
|
||||
private String shippingFee;
|
||||
|
||||
@Schema(description = "最低价", example = "18.50")
|
||||
private String minimumPrice;
|
||||
|
||||
|
||||
+4
@@ -52,6 +52,10 @@ public class PriceTrackResultItemVo {
|
||||
|
||||
@Schema(description = "预签名下载 URL,列表可能带回,也可通过下载接口获取")
|
||||
private String downloadUrl;
|
||||
private Long fileJobId;
|
||||
private String fileStatus;
|
||||
private String fileError;
|
||||
private Boolean fileReady;
|
||||
|
||||
@Schema(description = "所属循环任务 ID", example = "1")
|
||||
private Long loopRunId;
|
||||
|
||||
+10
-8
@@ -24,6 +24,7 @@ public class PriceTrackExcelAssemblyService {
|
||||
"ASIN",
|
||||
"价格",
|
||||
"推荐价",
|
||||
"运费",
|
||||
"最低价",
|
||||
"第一名",
|
||||
"第二名",
|
||||
@@ -56,13 +57,14 @@ public class PriceTrackExcelAssemblyService {
|
||||
row.createCell(1).setCellValue(valueOf(item.getAsin()));
|
||||
row.createCell(2).setCellValue(valueOf(item.getPrice()));
|
||||
row.createCell(3).setCellValue(valueOf(item.getRecommendedPrice()));
|
||||
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(valueOf(item.getPriceChangeStatus()));
|
||||
row.createCell(9).setCellValue(valueOf(item.getModifyCount()));
|
||||
row.createCell(10).setCellValue(valueOf(item.getStatus()));
|
||||
row.createCell(4).setCellValue(valueOf(item.getShippingFee()));
|
||||
row.createCell(5).setCellValue(valueOf(item.getMinimumPrice()));
|
||||
row.createCell(6).setCellValue(valueOf(item.getFirstPlace()));
|
||||
row.createCell(7).setCellValue(valueOf(item.getSecondPlace()));
|
||||
row.createCell(8).setCellValue(valueOf(item.getCartShopName()));
|
||||
row.createCell(9).setCellValue(valueOf(item.getPriceChangeStatus()));
|
||||
row.createCell(10).setCellValue(valueOf(item.getModifyCount()));
|
||||
row.createCell(11).setCellValue(valueOf(item.getStatus()));
|
||||
}
|
||||
applyDefaultColumnWidths(sheet);
|
||||
}
|
||||
@@ -112,7 +114,7 @@ public class PriceTrackExcelAssemblyService {
|
||||
}
|
||||
|
||||
private void applyDefaultColumnWidths(Sheet sheet) {
|
||||
int[] widths = {22, 18, 12, 12, 12, 20, 20, 22, 18, 12, 12};
|
||||
int[] widths = {22, 18, 12, 12, 12, 12, 20, 20, 22, 18, 12, 12};
|
||||
for (int i = 0; i < widths.length; i++) {
|
||||
sheet.setColumnWidth(i, widths[i] * 256);
|
||||
}
|
||||
|
||||
+66
-8
@@ -6,6 +6,7 @@ import com.nanri.aiimage.modules.pricetrack.model.dto.PriceTrackSubmitResultRequ
|
||||
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
||||
import com.nanri.aiimage.modules.task.service.TaskScopePayloadStorageService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -13,11 +14,13 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class PriceTrackTaskCacheService {
|
||||
|
||||
private static final String MODULE_TYPE = "PRICE_TRACK";
|
||||
@@ -32,17 +35,27 @@ public class PriceTrackTaskCacheService {
|
||||
if (taskId == null || taskId <= 0) {
|
||||
return;
|
||||
}
|
||||
stringRedisTemplate.opsForValue().set(
|
||||
buildTaskHeartbeatKey(taskId),
|
||||
String.valueOf(Instant.now().toEpochMilli()),
|
||||
Duration.ofHours(HEARTBEAT_TTL_HOURS));
|
||||
try {
|
||||
stringRedisTemplate.opsForValue().set(
|
||||
buildTaskHeartbeatKey(taskId),
|
||||
String.valueOf(Instant.now().toEpochMilli()),
|
||||
Duration.ofHours(HEARTBEAT_TTL_HOURS));
|
||||
} catch (Exception ex) {
|
||||
log.warn("[price-track-cache] touch heartbeat degraded taskId={} msg={}", taskId, ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public long getTaskHeartbeatMillis(Long taskId) {
|
||||
if (taskId == null || taskId <= 0) {
|
||||
return 0L;
|
||||
}
|
||||
String raw = stringRedisTemplate.opsForValue().get(buildTaskHeartbeatKey(taskId));
|
||||
String raw;
|
||||
try {
|
||||
raw = stringRedisTemplate.opsForValue().get(buildTaskHeartbeatKey(taskId));
|
||||
} catch (Exception ex) {
|
||||
log.warn("[price-track-cache] get heartbeat degraded taskId={} msg={}", taskId, ex.getMessage());
|
||||
return 0L;
|
||||
}
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return 0L;
|
||||
}
|
||||
@@ -53,6 +66,41 @@ public class PriceTrackTaskCacheService {
|
||||
}
|
||||
}
|
||||
|
||||
public Map<Long, Long> getTaskHeartbeatMillisBatch(List<Long> taskIds) {
|
||||
Map<Long, Long> result = new LinkedHashMap<>();
|
||||
if (taskIds == null || taskIds.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
List<Long> normalized = taskIds.stream()
|
||||
.filter(id -> id != null && id > 0)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (normalized.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
List<String> keys = normalized.stream().map(this::buildTaskHeartbeatKey).toList();
|
||||
List<String> values;
|
||||
try {
|
||||
values = stringRedisTemplate.opsForValue().multiGet(keys);
|
||||
} catch (Exception ex) {
|
||||
log.warn("[price-track-cache] batch get heartbeat degraded taskIds={} msg={}", normalized, ex.getMessage());
|
||||
return result;
|
||||
}
|
||||
for (int i = 0; i < normalized.size(); i++) {
|
||||
String raw = values != null && i < values.size() ? values.get(i) : null;
|
||||
if (raw == null || raw.isBlank()) {
|
||||
result.put(normalized.get(i), 0L);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
result.put(normalized.get(i), Long.parseLong(raw));
|
||||
} catch (NumberFormatException ignored) {
|
||||
result.put(normalized.get(i), 0L);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public PriceTrackSubmitResultRequest.ShopResult getShopMergedPayload(Long taskId, String shopKey) {
|
||||
return taskScopePayloadStorageService.getScopePayload(taskId, MODULE_TYPE, shopKey, PriceTrackSubmitResultRequest.ShopResult.class);
|
||||
}
|
||||
@@ -82,8 +130,12 @@ public class PriceTrackTaskCacheService {
|
||||
return;
|
||||
}
|
||||
taskEntityLocalCache.remove(taskId);
|
||||
stringRedisTemplate.delete(buildTaskHeartbeatKey(taskId));
|
||||
stringRedisTemplate.delete(buildTaskEntityKey(taskId));
|
||||
try {
|
||||
stringRedisTemplate.delete(buildTaskHeartbeatKey(taskId));
|
||||
stringRedisTemplate.delete(buildTaskEntityKey(taskId));
|
||||
} catch (Exception ex) {
|
||||
log.warn("[price-track-cache] delete cache degraded taskId={} msg={}", taskId, ex.getMessage());
|
||||
}
|
||||
taskScopePayloadStorageService.deleteTaskScopePayloads(taskId, MODULE_TYPE);
|
||||
}
|
||||
|
||||
@@ -132,7 +184,13 @@ public class PriceTrackTaskCacheService {
|
||||
return result;
|
||||
}
|
||||
java.util.List<String> keys = missingIds.stream().map(this::buildTaskEntityKey).toList();
|
||||
java.util.List<String> values = stringRedisTemplate.opsForValue().multiGet(keys);
|
||||
java.util.List<String> values;
|
||||
try {
|
||||
values = stringRedisTemplate.opsForValue().multiGet(keys);
|
||||
} catch (Exception ex) {
|
||||
log.warn("[price-track-cache] batch get task cache degraded taskIds={} msg={}", missingIds, ex.getMessage());
|
||||
return result;
|
||||
}
|
||||
for (int i = 0; i < missingIds.size(); i++) {
|
||||
Long taskId = missingIds.get(i);
|
||||
String val = values != null && i < values.size() ? values.get(i) : null;
|
||||
|
||||
+66
-4
@@ -26,6 +26,9 @@ import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
|
||||
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
||||
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
||||
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
||||
import com.nanri.aiimage.modules.task.model.entity.TaskFileJobEntity;
|
||||
import com.nanri.aiimage.modules.task.service.TaskFileJobService;
|
||||
import com.nanri.aiimage.modules.task.service.TaskResultPayloadService;
|
||||
import com.nanri.aiimage.modules.ziniao.service.ZiniaoShopSwitchService;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -66,6 +69,8 @@ public class PriceTrackTaskService {
|
||||
private final PriceTrackTaskCacheService priceTrackTaskCacheService;
|
||||
private final PriceTrackLoopRunService priceTrackLoopRunService;
|
||||
private final TaskPressureProperties taskPressureProperties;
|
||||
private final TaskResultPayloadService taskResultPayloadService;
|
||||
private final TaskFileJobService taskFileJobService;
|
||||
|
||||
private FileTaskEntity loadTaskForExecution(Long taskId) {
|
||||
Map<Long, FileTaskEntity> cachedTasks = priceTrackTaskCacheService.getTaskCacheBatch(List.of(taskId));
|
||||
@@ -435,7 +440,7 @@ public class PriceTrackTaskService {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
assembleShopResult(fr, shopKey, merged);
|
||||
enqueueResultFileAssembly(fr, shopKey, merged);
|
||||
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||
} catch (Exception ex) {
|
||||
markResultFailed(fr, ex.getMessage() == null ? "assemble price-track result failed" : ex.getMessage());
|
||||
@@ -509,7 +514,7 @@ public class PriceTrackTaskService {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
assembleShopResult(fr, shopKey, cachedPayload);
|
||||
enqueueResultFileAssembly(fr, shopKey, cachedPayload);
|
||||
priceTrackTaskCacheService.removeShopMergedPayload(taskId, shopKey);
|
||||
changed = true;
|
||||
log.warn("[price-track] stale finalize assembled taskId={} shop={} fromCompensation={} completedFlag={} rowCount={}",
|
||||
@@ -618,6 +623,49 @@ public class PriceTrackTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
public void processResultFileJob(TaskFileJobEntity job) {
|
||||
if (job == null || job.getTaskId() == null || job.getResultId() == null || job.getScopeKey() == null) {
|
||||
throw new BusinessException("结果文件任务参数不完整");
|
||||
}
|
||||
FileResultEntity result = fileResultMapper.selectById(job.getResultId());
|
||||
if (result == null || !MODULE_TYPE.equals(result.getModuleType())) {
|
||||
throw new BusinessException("结果记录不存在");
|
||||
}
|
||||
PriceTrackSubmitResultRequest.ShopResult payload = taskResultPayloadService.getLatest(
|
||||
job.getTaskId(), MODULE_TYPE, job.getScopeKey(), PriceTrackSubmitResultRequest.ShopResult.class);
|
||||
if (payload == null) {
|
||||
throw new BusinessException("结果文件载荷不存在");
|
||||
}
|
||||
assembleShopResult(result, job.getScopeKey(), payload);
|
||||
}
|
||||
|
||||
private void enqueueResultFileAssembly(FileResultEntity result,
|
||||
String shopKey,
|
||||
PriceTrackSubmitResultRequest.ShopResult payload) {
|
||||
taskResultPayloadService.saveLatest(result.getTaskId(), MODULE_TYPE, shopKey, payload);
|
||||
markResultFilePending(result, shopKey, payload);
|
||||
taskFileJobService.enqueueAssembleResult(result.getTaskId(), MODULE_TYPE, result.getId(), shopKey);
|
||||
}
|
||||
|
||||
private void markResultFilePending(FileResultEntity result,
|
||||
String shopKey,
|
||||
PriceTrackSubmitResultRequest.ShopResult payload) {
|
||||
Map<String, List<PriceTrackSubmitResultRequest.AsinResult>> countries =
|
||||
excelAssemblyService.normalizeCountriesMap(payload.getCountries());
|
||||
String displayName = payload.getShopName() != null && !payload.getShopName().isBlank()
|
||||
? payload.getShopName().trim()
|
||||
: shopKey;
|
||||
String stem = safeFileStem(displayName);
|
||||
result.setSuccess(1);
|
||||
result.setErrorMessage(null);
|
||||
result.setResultFilename(stem + ".xlsx");
|
||||
result.setResultFileUrl(null);
|
||||
result.setResultFileSize(0L);
|
||||
result.setResultContentType(CONTENT_TYPE_XLSX);
|
||||
result.setRowCount(excelAssemblyService.countRows(countries));
|
||||
fileResultMapper.updateById(result);
|
||||
}
|
||||
|
||||
public Map<String, List<Map<String, String>>> parseMatchAsinRowsByCountry(List<String> asinFiles, List<String> countryCodes) {
|
||||
return parseAsinRowsByCountry(asinFiles, countryCodes);
|
||||
}
|
||||
@@ -901,8 +949,8 @@ public class PriceTrackTaskService {
|
||||
vo.setSuccess(ok);
|
||||
vo.setError(entity.getErrorMessage());
|
||||
vo.setOutputFilename(entity.getResultFilename());
|
||||
vo.setDownloadUrl(entity.getResultFileUrl() == null || entity.getResultFileUrl().isBlank()
|
||||
? null : ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl()));
|
||||
vo.setDownloadUrl(null);
|
||||
attachFileJobState(vo, entity);
|
||||
vo.setMatched(true);
|
||||
if (entity.getTaskId() != null) {
|
||||
mergeQueueFieldsFromRequest(vo, entity.getTaskId());
|
||||
@@ -910,6 +958,18 @@ public class PriceTrackTaskService {
|
||||
return vo;
|
||||
}
|
||||
|
||||
private void attachFileJobState(PriceTrackResultItemVo vo, FileResultEntity entity) {
|
||||
TaskFileJobEntity job = taskFileJobService.findAssembleJob(entity.getTaskId(), MODULE_TYPE, entity.getId());
|
||||
vo.setFileReady(entity.getResultFileUrl() != null && !entity.getResultFileUrl().isBlank());
|
||||
if (job == null) {
|
||||
vo.setFileStatus(Boolean.TRUE.equals(vo.getFileReady()) ? "SUCCESS" : null);
|
||||
return;
|
||||
}
|
||||
vo.setFileJobId(job.getId());
|
||||
vo.setFileStatus(job.getStatus());
|
||||
vo.setFileError(job.getErrorMessage());
|
||||
}
|
||||
|
||||
private PriceTrackTaskDetailVo buildTaskDetail(FileTaskEntity task) {
|
||||
PriceTrackTaskDetailVo detail = new PriceTrackTaskDetailVo();
|
||||
detail.setTask(toTaskItemVo(task));
|
||||
@@ -1074,6 +1134,7 @@ public class PriceTrackTaskService {
|
||||
merged.setAsin(firstNonBlank(incoming.getAsin(), merged.getAsin()));
|
||||
merged.setPrice(firstNonBlank(incoming.getPrice(), merged.getPrice()));
|
||||
merged.setRecommendedPrice(firstNonBlank(incoming.getRecommendedPrice(), merged.getRecommendedPrice()));
|
||||
merged.setShippingFee(firstNonBlank(incoming.getShippingFee(), merged.getShippingFee()));
|
||||
merged.setMinimumPrice(firstNonBlank(incoming.getMinimumPrice(), merged.getMinimumPrice()));
|
||||
merged.setFirstPlace(firstNonBlank(incoming.getFirstPlace(), merged.getFirstPlace()));
|
||||
merged.setSecondPlace(firstNonBlank(incoming.getSecondPlace(), merged.getSecondPlace()));
|
||||
@@ -1097,6 +1158,7 @@ public class PriceTrackTaskService {
|
||||
out.setAsin(row.getAsin());
|
||||
out.setPrice(row.getPrice());
|
||||
out.setRecommendedPrice(row.getRecommendedPrice());
|
||||
out.setShippingFee(row.getShippingFee());
|
||||
out.setMinimumPrice(row.getMinimumPrice());
|
||||
out.setFirstPlace(row.getFirstPlace());
|
||||
out.setSecondPlace(row.getSecondPlace());
|
||||
|
||||
Reference in New Issue
Block a user