新需求更新 同步更新
This commit is contained in:
+1
-1
@@ -28,7 +28,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/publish")
|
||||
@Tag(name = "上架", description = "上架 Excel 多文件任务接口。前端创建批次并严格串行派发文件;Python 按页拉取数据、发送统一任务心跳并回传当前店铺完整结果。")
|
||||
@Tag(name = "上架", description = "上架 Excel 多文件任务接口。前端创建批次并严格串行派发文件;Python 按页拉取数据、通过结果分片回传进度和当前店铺完整结果,任务心跳用于保活。")
|
||||
public class PublishController {
|
||||
|
||||
private final PublishTaskService publishTaskService;
|
||||
|
||||
+2
-2
@@ -38,9 +38,9 @@ public class PublishFileVo {
|
||||
private Integer percent;
|
||||
@Schema(description = "与 percent 相同,供公共进度组件使用", example = "50")
|
||||
private Integer progressPercent;
|
||||
@Schema(description = "心跳上报的当前处理数量;未上报时使用 processedRows", example = "341")
|
||||
@Schema(description = "结果分片累计接收的当前处理数量;兼容心跳上报", example = "341")
|
||||
private Integer progressCurrent;
|
||||
@Schema(description = "心跳上报的总处理数量;未上报时使用 totalRows", example = "682")
|
||||
@Schema(description = "解析得到的总处理数量;兼容心跳上报", example = "682")
|
||||
private Integer progressTotal;
|
||||
@Schema(description = "文件失败原因的进度展示副本;无错误时为空")
|
||||
private String progressMessage;
|
||||
|
||||
+118
-15
@@ -345,7 +345,11 @@ public class PublishTaskService {
|
||||
.eq(FileTaskEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(FileTaskEntity::getStatus, STATUS_RUNNING)
|
||||
.set(FileTaskEntity::getUpdatedAt, now));
|
||||
if (request == null || (request.getCurrent() == null && request.getTotal() == null)) {
|
||||
// Generic desktop heartbeats use 0/0 when they only need to keep the task alive.
|
||||
// Treat that as no progress update so result chunks cannot be reset to zero.
|
||||
if (request == null
|
||||
|| ((request.getCurrent() == null || request.getCurrent() <= 0)
|
||||
&& (request.getTotal() == null || request.getTotal() <= 0))) {
|
||||
return;
|
||||
}
|
||||
PublishFileEntity active = publishFileMapper.selectOne(new LambdaQueryWrapper<PublishFileEntity>()
|
||||
@@ -360,12 +364,12 @@ public class PublishTaskService {
|
||||
.eq(PublishFileEntity::getId, active.getId())
|
||||
.eq(PublishFileEntity::getStatus, STATUS_RUNNING)
|
||||
.set(PublishFileEntity::getUpdatedAt, now);
|
||||
if (request.getTotal() != null && request.getTotal() >= 0) {
|
||||
if (request.getTotal() != null && request.getTotal() > 0) {
|
||||
update.set(PublishFileEntity::getTotalRows, request.getTotal());
|
||||
}
|
||||
if (request.getCurrent() != null && request.getCurrent() >= 0) {
|
||||
if (request.getCurrent() != null && request.getCurrent() > 0) {
|
||||
int current = request.getCurrent();
|
||||
if (request.getTotal() != null && request.getTotal() >= 0) {
|
||||
if (request.getTotal() != null && request.getTotal() > 0) {
|
||||
current = Math.min(current, request.getTotal());
|
||||
}
|
||||
update.set(PublishFileEntity::getProcessedRows, current);
|
||||
@@ -432,6 +436,7 @@ public class PublishTaskService {
|
||||
throw new BusinessException("no successful publish files");
|
||||
}
|
||||
|
||||
TaskOptions options = readTaskOptions(task);
|
||||
List<PublishWorkbookService.WorkbookInput> inputs = new ArrayList<>();
|
||||
int rowCount = 0;
|
||||
for (PublishFileEntity file : successfulFiles) {
|
||||
@@ -443,7 +448,7 @@ public class PublishTaskService {
|
||||
List<PublishRowDto> rows = items.stream().map(this::toRowDto).toList();
|
||||
rowCount += rows.size();
|
||||
inputs.add(new PublishWorkbookService.WorkbookInput(
|
||||
file.getSourceFilename(), file.getShopName(), rows));
|
||||
file.getSourceFilename(), file.getShopName(), options.publishCountry(), rows));
|
||||
}
|
||||
|
||||
PublishWorkbookService.PackagedResult packaged = workbookService.packageTaskResult(
|
||||
@@ -737,6 +742,7 @@ public class PublishTaskService {
|
||||
} else {
|
||||
ResultChunkReceipt receipt = persistResultChunk(taskId, file, incoming, storedPayloads);
|
||||
if (!receipt.completed()) {
|
||||
updateReceivedProgress(taskId, file, receipt.receivedRowCount());
|
||||
file.setStatus(STATUS_RUNNING);
|
||||
file.setErrorMessage(null);
|
||||
file.setUpdatedAt(LocalDateTime.now());
|
||||
@@ -1025,9 +1031,11 @@ public class PublishTaskService {
|
||||
if (existing != null) {
|
||||
validateExistingChunk(existing, chunkTotal, payloadHash);
|
||||
int receivedChunkCount = countResultChunks(taskId, scopeHash);
|
||||
persistResultScope(taskId, scopeKey, scopeHash, chunkTotal, receivedChunkCount);
|
||||
int receivedRowCount = resolveReceivedRowCount(taskId, scopeHash, scope, 0, false);
|
||||
persistResultScope(taskId, scopeKey, scopeHash, chunkTotal,
|
||||
receivedChunkCount, receivedRowCount);
|
||||
return new ResultChunkReceipt(scopeHash, chunkTotal,
|
||||
receivedChunkCount >= chunkTotal);
|
||||
receivedChunkCount >= chunkTotal, receivedRowCount);
|
||||
}
|
||||
|
||||
ensureRustfsPayloadStorageEnabled();
|
||||
@@ -1046,9 +1054,11 @@ public class PublishTaskService {
|
||||
chunk.setPayloadHash(payloadHash);
|
||||
chunk.setCreatedAt(LocalDateTime.now());
|
||||
chunk.setUpdatedAt(LocalDateTime.now());
|
||||
boolean inserted = false;
|
||||
try {
|
||||
taskChunkMapper.insert(chunk);
|
||||
storedPayloads.add(storedPayload);
|
||||
inserted = true;
|
||||
} catch (DuplicateKeyException ex) {
|
||||
transientPayloadStorageService.deletePayloadIfPresent(storedPayload);
|
||||
TaskChunkEntity winner = findResultChunk(taskId, scopeHash, chunkIndex);
|
||||
@@ -1062,11 +1072,13 @@ public class PublishTaskService {
|
||||
}
|
||||
|
||||
int receivedChunkCount = countResultChunks(taskId, scopeHash);
|
||||
persistResultScope(taskId, scopeKey, scopeHash, chunkTotal, receivedChunkCount);
|
||||
log.info("[publish] result chunk received taskId={} fileId={} chunk={}/{} received={}",
|
||||
taskId, file.getId(), chunkIndex, chunkTotal, receivedChunkCount);
|
||||
int receivedRowCount = resolveReceivedRowCount(taskId, scopeHash, scope, rows.size(), inserted);
|
||||
persistResultScope(taskId, scopeKey, scopeHash, chunkTotal,
|
||||
receivedChunkCount, receivedRowCount);
|
||||
log.info("[publish] result chunk received taskId={} fileId={} chunk={}/{} receivedChunks={} receivedRows={}",
|
||||
taskId, file.getId(), chunkIndex, chunkTotal, receivedChunkCount, receivedRowCount);
|
||||
return new ResultChunkReceipt(scopeHash, chunkTotal,
|
||||
receivedChunkCount >= chunkTotal);
|
||||
receivedChunkCount >= chunkTotal, receivedRowCount);
|
||||
}
|
||||
|
||||
private void validateChunkMetadata(int chunkIndex, int chunkTotal) {
|
||||
@@ -1119,11 +1131,70 @@ public class PublishTaskService {
|
||||
return count == null ? 0 : count.intValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the number of result rows received for a file without re-reading
|
||||
* every payload on every callback. New callbacks keep the count in the
|
||||
* existing scope state JSON; scopes created by older versions are repaired
|
||||
* once by counting their stored chunks.
|
||||
*/
|
||||
private int resolveReceivedRowCount(Long taskId,
|
||||
String scopeHash,
|
||||
TaskScopeStateEntity scope,
|
||||
int currentChunkRows,
|
||||
boolean inserted) {
|
||||
Integer persisted = readReceivedRowCount(scope);
|
||||
if (persisted != null) {
|
||||
long next = (long) persisted + (inserted ? Math.max(0, currentChunkRows) : 0);
|
||||
return next > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) Math.max(0, next);
|
||||
}
|
||||
return countReceivedResultRows(taskId, scopeHash);
|
||||
}
|
||||
|
||||
private Integer readReceivedRowCount(TaskScopeStateEntity scope) {
|
||||
if (scope == null || scope.getStateJson() == null || scope.getStateJson().isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(scope.getStateJson());
|
||||
JsonNode receivedRows = root == null ? null : root.get("receivedRows");
|
||||
if (receivedRows == null || !receivedRows.isIntegralNumber()) {
|
||||
return null;
|
||||
}
|
||||
return Math.max(0, receivedRows.asInt(0));
|
||||
} catch (Exception ex) {
|
||||
log.warn("[publish] failed to read received row count from result scope taskId={} scopeHash={} msg={}",
|
||||
scope.getTaskId(), scope.getScopeHash(), safeMessage(ex));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private int countReceivedResultRows(Long taskId, String scopeHash) {
|
||||
List<TaskChunkEntity> chunks = taskChunkMapper.selectList(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getTaskId, taskId)
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
|
||||
.eq(TaskChunkEntity::getScopeHash, scopeHash)
|
||||
.orderByAsc(TaskChunkEntity::getChunkIndex));
|
||||
if (chunks == null || chunks.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
TypeReference<List<PublishRowDto>> listType = new TypeReference<>() {
|
||||
};
|
||||
long count = 0;
|
||||
for (TaskChunkEntity chunk : chunks) {
|
||||
count += readResultChunkRows(chunk, listType).size();
|
||||
if (count >= Integer.MAX_VALUE) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
return (int) count;
|
||||
}
|
||||
|
||||
private void persistResultScope(Long taskId,
|
||||
String scopeKey,
|
||||
String scopeHash,
|
||||
int chunkTotal,
|
||||
int receivedChunkCount) {
|
||||
int receivedChunkCount,
|
||||
int receivedRowCount) {
|
||||
TaskScopeStateEntity scope = findResultScope(taskId, scopeHash);
|
||||
validateChunkTotal(scope == null ? null : scope.getChunkTotal(), chunkTotal);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -1141,7 +1212,7 @@ public class PublishTaskService {
|
||||
scope.setCompleted(completed ? 1 : 0);
|
||||
scope.setLastChunkAt(now);
|
||||
scope.setLastError(null);
|
||||
scope.setStateJson(completed ? "{\"phase\":\"COMPLETE\"}" : "{\"phase\":\"RECEIVING\"}");
|
||||
scope.setStateJson(resultScopeStateJson(completed, receivedRowCount));
|
||||
scope.setUpdatedAt(now);
|
||||
if (scope.getId() != null) {
|
||||
taskScopeStateMapper.updateById(scope);
|
||||
@@ -1160,12 +1231,43 @@ public class PublishTaskService {
|
||||
winner.setCompleted(completed ? 1 : 0);
|
||||
winner.setLastChunkAt(now);
|
||||
winner.setLastError(null);
|
||||
winner.setStateJson(scope.getStateJson());
|
||||
Integer winnerRowCount = readReceivedRowCount(winner);
|
||||
winner.setStateJson(resultScopeStateJson(completed,
|
||||
Math.max(receivedRowCount, winnerRowCount == null ? 0 : winnerRowCount)));
|
||||
winner.setUpdatedAt(now);
|
||||
taskScopeStateMapper.updateById(winner);
|
||||
}
|
||||
}
|
||||
|
||||
private String resultScopeStateJson(boolean completed, int receivedRowCount) {
|
||||
Map<String, Object> state = new LinkedHashMap<>();
|
||||
state.put("phase", completed ? "COMPLETE" : "RECEIVING");
|
||||
state.put("receivedRows", Math.max(0, receivedRowCount));
|
||||
return writeJson(state, "保存上架结果分片进度失败");
|
||||
}
|
||||
|
||||
private void updateReceivedProgress(Long taskId, PublishFileEntity file, int receivedRowCount) {
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
int total = safeInt(file.getTotalRows());
|
||||
if (total <= 0) {
|
||||
long parsedRows = Objects.requireNonNullElse(publishItemMapper.selectCount(
|
||||
new LambdaQueryWrapper<PublishItemEntity>()
|
||||
.eq(PublishItemEntity::getTaskId, taskId)
|
||||
.eq(PublishItemEntity::getFileId, file.getId())), 0L);
|
||||
total = parsedRows >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) Math.max(0L, parsedRows);
|
||||
if (total > 0) {
|
||||
file.setTotalRows(total);
|
||||
}
|
||||
}
|
||||
int progress = Math.max(safeInt(file.getProcessedRows()), Math.max(0, receivedRowCount));
|
||||
if (total > 0) {
|
||||
progress = Math.min(total, progress);
|
||||
}
|
||||
file.setProcessedRows(progress);
|
||||
}
|
||||
|
||||
private List<PublishRowDto> loadCompleteResultRows(Long taskId, ResultChunkReceipt receipt) {
|
||||
List<TaskChunkEntity> chunks = taskChunkMapper.selectList(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getTaskId, taskId)
|
||||
@@ -1637,6 +1739,7 @@ public class PublishTaskService {
|
||||
|
||||
private record ResultChunkReceipt(String scopeHash,
|
||||
int chunkTotal,
|
||||
boolean completed) {
|
||||
boolean completed,
|
||||
int receivedRowCount) {
|
||||
}
|
||||
}
|
||||
|
||||
+18
-13
@@ -91,7 +91,7 @@ public class PublishWorkbookService {
|
||||
}
|
||||
}
|
||||
|
||||
public File writeWorkbook(File outputFile, List<PublishRowDto> rows) {
|
||||
public File writeWorkbook(File outputFile, List<PublishRowDto> rows, String publishCountry) {
|
||||
File parent = outputFile.getParentFile();
|
||||
if (parent != null) {
|
||||
parent.mkdirs();
|
||||
@@ -100,13 +100,9 @@ public class PublishWorkbookService {
|
||||
workbook.setCompressTempFiles(true);
|
||||
try (FileOutputStream output = new FileOutputStream(outputFile)) {
|
||||
CellStyle headerStyle = createHeaderStyle(workbook);
|
||||
Map<String, List<PublishRowDto>> rowsByCountry = groupByCountry(rows);
|
||||
Set<String> usedSheetNames = new LinkedHashSet<>();
|
||||
for (Map.Entry<String, List<PublishRowDto>> entry : rowsByCountry.entrySet()) {
|
||||
String sheetName = uniqueSheetName(entry.getKey(), usedSheetNames);
|
||||
Sheet sheet = workbook.createSheet(sheetName);
|
||||
writeSheet(sheet, entry.getValue(), headerStyle);
|
||||
}
|
||||
String country = countrySheetName(publishCountry);
|
||||
Sheet sheet = workbook.createSheet(country);
|
||||
writeSheet(sheet, rows, headerStyle, country);
|
||||
workbook.write(output);
|
||||
return outputFile;
|
||||
} catch (Exception ex) {
|
||||
@@ -135,7 +131,7 @@ public class PublishWorkbookService {
|
||||
+ "_上架结果.xlsx";
|
||||
String filename = uniqueFilename(desired, usedFilenames);
|
||||
File workbook = new File(workDirectory, filename);
|
||||
writeWorkbook(workbook, input.rows());
|
||||
writeWorkbook(workbook, input.rows(), input.publishCountry());
|
||||
workbooks.add(workbook);
|
||||
}
|
||||
|
||||
@@ -202,7 +198,10 @@ public class PublishWorkbookService {
|
||||
return grouped;
|
||||
}
|
||||
|
||||
private void writeSheet(Sheet sheet, List<PublishRowDto> rows, CellStyle headerStyle) {
|
||||
private void writeSheet(Sheet sheet,
|
||||
List<PublishRowDto> rows,
|
||||
CellStyle headerStyle,
|
||||
String publishCountry) {
|
||||
Row header = sheet.createRow(0);
|
||||
for (int index = 0; index < RESULT_HEADERS.size(); index++) {
|
||||
Cell cell = header.createCell(index);
|
||||
@@ -210,11 +209,14 @@ public class PublishWorkbookService {
|
||||
cell.setCellStyle(headerStyle);
|
||||
}
|
||||
int rowIndex = 1;
|
||||
for (PublishRowDto value : rows) {
|
||||
for (PublishRowDto value : rows == null ? List.<PublishRowDto>of() : rows) {
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
Row row = sheet.createRow(rowIndex++);
|
||||
setText(row, 0, value.getSourceId());
|
||||
setText(row, 1, value.getAsin());
|
||||
setText(row, 2, value.getCountry());
|
||||
setText(row, 2, publishCountry);
|
||||
setText(row, 3, value.getBrand());
|
||||
setPrice(row, 4, value.getPrice());
|
||||
setText(row, 5, value.getStatus());
|
||||
@@ -337,7 +339,10 @@ public class PublishWorkbookService {
|
||||
public record ParsedWorkbook(List<PublishRowDto> rows) {
|
||||
}
|
||||
|
||||
public record WorkbookInput(String sourceFilename, String shopName, List<PublishRowDto> rows) {
|
||||
public record WorkbookInput(String sourceFilename,
|
||||
String shopName,
|
||||
String publishCountry,
|
||||
List<PublishRowDto> rows) {
|
||||
}
|
||||
|
||||
public record PackagedResult(File file, String filename, String contentType) {
|
||||
|
||||
Reference in New Issue
Block a user