后端架构更新

This commit is contained in:
super
2026-04-27 09:18:10 +08:00
parent 0caf62c3d2
commit 995e2ee65a
141 changed files with 6957 additions and 772 deletions
@@ -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(
@@ -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;
@@ -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;
@@ -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);
}
@@ -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;
@@ -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());