处理一些合并冲突
This commit is contained in:
+7
-2
@@ -6,6 +6,7 @@ import com.nanri.aiimage.config.AppearancePatentProperties;
|
||||
import com.nanri.aiimage.modules.appearancepatent.model.dto.AppearancePatentResultRowDto;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -24,6 +25,8 @@ import java.util.Map;
|
||||
@Slf4j
|
||||
public class AppearancePatentCozeClient {
|
||||
|
||||
private static final MediaType APPLICATION_JSON_UTF8 = new MediaType("application", "json", StandardCharsets.UTF_8);
|
||||
|
||||
private final AppearancePatentProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@@ -216,7 +219,8 @@ public class AppearancePatentCozeClient {
|
||||
.uri(joinUrl(properties.getCozeBaseUrl(), properties.getCozeWorkflowPath()))
|
||||
.headers(headers -> {
|
||||
headers.setBearerAuth(stripBearer(properties.getCozeToken()));
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setContentType(APPLICATION_JSON_UTF8);
|
||||
headers.set(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());
|
||||
});
|
||||
request.body(body);
|
||||
return request.exchange((clientRequest, clientResponse) -> {
|
||||
@@ -237,7 +241,8 @@ public class AppearancePatentCozeClient {
|
||||
.uri(joinUrl(properties.getCozeBaseUrl(), path))
|
||||
.headers(headers -> {
|
||||
headers.setBearerAuth(stripBearer(properties.getCozeToken()));
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setContentType(APPLICATION_JSON_UTF8);
|
||||
headers.set(HttpHeaders.ACCEPT_CHARSET, StandardCharsets.UTF_8.name());
|
||||
})
|
||||
.exchange((clientRequest, clientResponse) -> {
|
||||
byte[] responseBytes = StreamUtils.copyToByteArray(clientResponse.getBody());
|
||||
|
||||
+345
-74
@@ -38,6 +38,7 @@ import com.nanri.aiimage.modules.task.model.entity.TaskFileJobEntity;
|
||||
import com.nanri.aiimage.modules.task.model.entity.TaskChunkEntity;
|
||||
import com.nanri.aiimage.modules.task.model.entity.TaskProgressSnapshotEntity;
|
||||
import com.nanri.aiimage.modules.task.model.entity.TaskScopeStateEntity;
|
||||
import com.nanri.aiimage.modules.task.service.TaskDistributedLockService;
|
||||
import com.nanri.aiimage.modules.task.service.TaskFileJobService;
|
||||
import com.nanri.aiimage.modules.task.service.TaskProgressSnapshotService;
|
||||
import com.nanri.aiimage.modules.task.service.TransientPayloadStorageService;
|
||||
@@ -71,6 +72,7 @@ import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
@@ -98,6 +100,9 @@ public class AppearancePatentTaskService {
|
||||
private static final String CONTENT_TYPE_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||
private static final int RESULT_ROWS_READ_RETRY_LIMIT = 3;
|
||||
private static final long RESULT_ROWS_READ_RETRY_DELAY_MS = 500L;
|
||||
private static final Duration TASK_LOCK_TTL = Duration.ofMinutes(5);
|
||||
private static final long TASK_LOCK_WAIT_MILLIS = 10000L;
|
||||
private static final long TASK_LOCK_RETRY_DELAY_MILLIS = 200L;
|
||||
private static final List<String> RESULT_HEADERS = List.of(
|
||||
"id",
|
||||
"asin",
|
||||
@@ -125,6 +130,7 @@ public class AppearancePatentTaskService {
|
||||
private final TransientPayloadStorageService transientPayloadStorageService;
|
||||
private final PlatformTransactionManager transactionManager;
|
||||
private final DistributedJobLockService distributedJobLockService;
|
||||
private final TaskDistributedLockService taskDistributedLockService;
|
||||
@Autowired
|
||||
@Qualifier("cozeTaskExecutor")
|
||||
private TaskExecutor cozeTaskExecutor;
|
||||
@@ -296,24 +302,41 @@ public class AppearancePatentTaskService {
|
||||
.eq(FileResultEntity::getUserId, userId)
|
||||
.orderByDesc(FileResultEntity::getCreatedAt)
|
||||
.last("limit " + safeLimit));
|
||||
Map<Long, String> statusMap = new LinkedHashMap<>();
|
||||
Map<Long, FileTaskEntity> taskMap = new LinkedHashMap<>();
|
||||
List<Long> taskIds = rows.stream().map(FileResultEntity::getTaskId).filter(Objects::nonNull).distinct().toList();
|
||||
if (!taskIds.isEmpty()) {
|
||||
for (FileTaskEntity task : fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||
.select(FileTaskEntity::getId, FileTaskEntity::getStatus)
|
||||
.select(FileTaskEntity::getId,
|
||||
FileTaskEntity::getStatus,
|
||||
FileTaskEntity::getCreatedAt,
|
||||
FileTaskEntity::getUpdatedAt,
|
||||
FileTaskEntity::getFinishedAt)
|
||||
.in(FileTaskEntity::getId, taskIds))) {
|
||||
statusMap.put(task.getId(), task.getStatus());
|
||||
taskMap.put(task.getId(), task);
|
||||
}
|
||||
}
|
||||
Map<Long, TaskFileJobEntity> jobMap = taskFileJobService.findAssembleJobsByResultIds(MODULE_TYPE, rows.stream()
|
||||
.map(FileResultEntity::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.toList());
|
||||
List<FileResultEntity> sortedRows = new ArrayList<>();
|
||||
for (FileResultEntity row : rows) {
|
||||
String taskStatus = statusMap.get(row.getTaskId());
|
||||
FileTaskEntity task = taskMap.get(row.getTaskId());
|
||||
String taskStatus = task == null ? null : task.getStatus();
|
||||
if (STATUS_PENDING.equals(taskStatus)) {
|
||||
continue;
|
||||
}
|
||||
sortedRows.add(row);
|
||||
}
|
||||
sortedRows.sort(Comparator
|
||||
.comparingInt((FileResultEntity row) -> historyPriority(row, taskMap.get(row.getTaskId()), jobMap.get(row.getId())))
|
||||
.thenComparing((FileResultEntity row) -> historyActivityTime(row, taskMap.get(row.getTaskId()), jobMap.get(row.getId())),
|
||||
Comparator.nullsLast(Comparator.reverseOrder()))
|
||||
.thenComparing(FileResultEntity::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder()))
|
||||
.thenComparing(FileResultEntity::getId, Comparator.nullsLast(Comparator.reverseOrder())));
|
||||
for (FileResultEntity row : sortedRows) {
|
||||
FileTaskEntity task = taskMap.get(row.getTaskId());
|
||||
String taskStatus = task == null ? null : task.getStatus();
|
||||
vo.getItems().add(toHistoryItem(row, taskStatus, jobMap.get(row.getId())));
|
||||
}
|
||||
return vo;
|
||||
@@ -384,6 +407,16 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
|
||||
public void submitResult(Long taskId, AppearancePatentSubmitResultRequest request) {
|
||||
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskId, TASK_LOCK_WAIT_MILLIS);
|
||||
if (lockHandle == null) {
|
||||
throw new BusinessException(40902, "任务正在处理上一批结果,请稍后重试");
|
||||
}
|
||||
try (lockHandle) {
|
||||
submitResultLocked(taskId, request);
|
||||
}
|
||||
}
|
||||
|
||||
private void submitResultLocked(Long taskId, AppearancePatentSubmitResultRequest request) {
|
||||
if (transactionManager != null) {
|
||||
SubmitContext context = inNewTransaction(() -> persistSubmittedChunk(taskId, request));
|
||||
inNewTransaction(() -> {
|
||||
@@ -423,7 +456,7 @@ public class AppearancePatentTaskService {
|
||||
chunk.setScopeHash(scopeHash);
|
||||
chunk.setChunkIndex(chunkIndex);
|
||||
chunk.setChunkTotal(chunkTotal);
|
||||
String storedPayload = transientPayloadStorageService.storeChunkPayload(MODULE_TYPE, taskId, scopeHash, chunkIndex, payloadJson);
|
||||
String storedPayload = storeSharedChunkPayload(taskId, scopeHash, chunkIndex, payloadJson);
|
||||
chunk.setPayloadJson(storedPayload);
|
||||
chunk.setPayloadHash(DigestUtil.sha256Hex(payloadJson));
|
||||
chunk.setCreatedAt(LocalDateTime.now());
|
||||
@@ -536,10 +569,16 @@ public class AppearancePatentTaskService {
|
||||
if (heartbeatMillis > thresholdMillis) {
|
||||
continue;
|
||||
}
|
||||
inNewTransaction(() -> {
|
||||
finalizeStaleTask(task.getId(), "Python interrupted before uploading final appearance patent result");
|
||||
return null;
|
||||
});
|
||||
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(task.getId(), 0L);
|
||||
if (lockHandle == null) {
|
||||
continue;
|
||||
}
|
||||
try (lockHandle) {
|
||||
inNewTransaction(() -> {
|
||||
finalizeStaleTask(task.getId(), "Python interrupted before uploading final appearance patent result");
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -559,7 +598,16 @@ public class AppearancePatentTaskService {
|
||||
if (heartbeatMillis > thresholdMillis) {
|
||||
continue;
|
||||
}
|
||||
finalizeTask(task, "Python interrupted before uploading final appearance patent result", allRowCount(task), true);
|
||||
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(task.getId(), 0L);
|
||||
if (lockHandle == null) {
|
||||
continue;
|
||||
}
|
||||
try (lockHandle) {
|
||||
FileTaskEntity latestTask = fileTaskMapper.selectById(task.getId());
|
||||
if (latestTask != null) {
|
||||
finalizeTask(latestTask, "Python interrupted before uploading final appearance patent result", allRowCount(latestTask), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,7 +644,7 @@ public class AppearancePatentTaskService {
|
||||
chunk.setScopeHash(scopeHash);
|
||||
chunk.setChunkIndex(chunkIndex);
|
||||
chunk.setChunkTotal(chunkTotal);
|
||||
String storedPayload = transientPayloadStorageService.storeChunkPayload(MODULE_TYPE, taskId, scopeHash, chunkIndex, payloadJson);
|
||||
String storedPayload = storeSharedChunkPayload(taskId, scopeHash, chunkIndex, payloadJson);
|
||||
chunk.setPayloadJson(storedPayload);
|
||||
chunk.setPayloadHash(DigestUtil.sha256Hex(payloadJson));
|
||||
chunk.setCreatedAt(LocalDateTime.now());
|
||||
@@ -784,7 +832,7 @@ public class AppearancePatentTaskService {
|
||||
String payloadJson = writeJson(rows == null ? List.of() : rows, "结果序列化失败");
|
||||
payloadJson = writeJson(new ArrayList<>(persistedRows.values()), "appearance patent chunk payload merge failed");
|
||||
String oldPayload = chunk.getPayloadJson();
|
||||
String storedPayload = transientPayloadStorageService.storeChunkPayloadVersioned(MODULE_TYPE, taskId, scopeHash, chunkIndex, payloadJson);
|
||||
String storedPayload = storeSharedChunkPayloadVersioned(taskId, scopeHash, chunkIndex, payloadJson);
|
||||
chunk.setPayloadJson(storedPayload);
|
||||
chunk.setPayloadHash(DigestUtil.sha256Hex(payloadJson));
|
||||
chunk.setUpdatedAt(LocalDateTime.now());
|
||||
@@ -876,7 +924,7 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
List<AppearancePatentResultRowDto> representatives = new ArrayList<>();
|
||||
for (List<AppearancePatentResultRowDto> siblings : groupedRows.values()) {
|
||||
boolean alreadyResolved = siblings.stream().anyMatch(this::hasResolvedCozeFields);
|
||||
boolean alreadyResolved = siblings.stream().anyMatch(this::hasCompleteCozeResult);
|
||||
if (alreadyResolved) {
|
||||
continue;
|
||||
}
|
||||
@@ -1064,6 +1112,18 @@ public class AppearancePatentTaskService {
|
||||
if (job == null || job.getTaskId() == null || job.getResultId() == null) {
|
||||
throw new BusinessException("result file job arguments are incomplete");
|
||||
}
|
||||
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(job.getTaskId(), TASK_LOCK_WAIT_MILLIS);
|
||||
if (lockHandle == null) {
|
||||
taskFileJobService.requeue(job.getId(), "Task is busy, waiting for appearance patent result merge");
|
||||
touchJavaSideTaskActivity(job.getTaskId());
|
||||
return false;
|
||||
}
|
||||
try (lockHandle) {
|
||||
return processResultFileJobLocked(job);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean processResultFileJobLocked(TaskFileJobEntity job) {
|
||||
FileTaskEntity task = fileTaskMapper.selectById(job.getTaskId());
|
||||
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
|
||||
throw new BusinessException("task not found");
|
||||
@@ -1076,9 +1136,15 @@ public class AppearancePatentTaskService {
|
||||
.eq(TaskChunkEntity::getTaskId, task.getId())
|
||||
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
|
||||
.orderByAsc(TaskChunkEntity::getChunkIndex));
|
||||
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
|
||||
int cozeWorkUnits = countCozeWorkUnits(chunks, Math.max(1, properties.getCozeBatchSize()));
|
||||
int totalProgressUnits = Math.max(3, cozeWorkUnits + 3);
|
||||
if (countPendingCozeStates(task.getId()) > 0) {
|
||||
taskFileJobService.touchRunning(job.getId());
|
||||
touchJavaSideTaskActivity(task.getId());
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, 1, "Coze submitted, waiting for result");
|
||||
return false;
|
||||
}
|
||||
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, 0, "Submitting Coze");
|
||||
boolean pendingCoze = submitCozeBatches(task, result, job, chunks, allRowsByBaseId);
|
||||
if (pendingCoze) {
|
||||
@@ -1113,7 +1179,18 @@ public class AppearancePatentTaskService {
|
||||
if (state == null || state.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
cozeTaskExecutor.execute(() -> pollPendingCozeState(state.getId()));
|
||||
Long stateId = state.getId();
|
||||
try {
|
||||
cozeTaskExecutor.execute(() -> {
|
||||
log.info("[appearance-patent] coze poll worker entered stateId={} taskId={} executeId={}",
|
||||
stateId, state.getTaskId(), state.getCozeExecuteId());
|
||||
pollPendingCozeState(stateId);
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
log.warn("[appearance-patent] coze poll dispatch failed stateId={} taskId={} executeId={} err={}",
|
||||
stateId, state.getTaskId(), state.getCozeExecuteId(),
|
||||
firstNonBlank(ex.getMessage(), ex.getClass().getSimpleName()), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1231,8 +1308,7 @@ public class AppearancePatentTaskService {
|
||||
batchTotal
|
||||
);
|
||||
String batchPayload = writeJson(batchRows, "serialize coze batch payload failed");
|
||||
String storedBatchPayload = transientPayloadStorageService.storeParsedPayloadFast(
|
||||
MODULE_TYPE, task.getId(), batchScopeHash, batchPayload, true);
|
||||
String storedBatchPayload = storeSharedCozeBatchPayload(task.getId(), batchScopeHash, batchPayload);
|
||||
TaskScopeStateEntity state = new TaskScopeStateEntity();
|
||||
state.setTaskId(task.getId());
|
||||
state.setModuleType(MODULE_TYPE);
|
||||
@@ -1260,25 +1336,53 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
|
||||
private void pollPendingCozeState(Long stateId) {
|
||||
TaskScopeStateEntity state = taskScopeStateMapper.selectById(stateId);
|
||||
if (state == null || state.getCozeExecuteId() == null || state.getCozeExecuteId().isBlank()) {
|
||||
Long taskIdForLock = null;
|
||||
TaskScopeStateEntity lockState = taskScopeStateMapper.selectById(stateId);
|
||||
if (lockState != null) {
|
||||
taskIdForLock = lockState.getTaskId();
|
||||
}
|
||||
TaskDistributedLockService.LockHandle lockHandle = acquireTaskLock(taskIdForLock, 0L);
|
||||
if (lockHandle == null) {
|
||||
if (taskIdForLock != null) {
|
||||
log.info("[appearance-patent] coze poll skipped because task is locked taskId={} stateId={}",
|
||||
taskIdForLock, stateId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!(COZE_STATUS_SUBMITTED.equals(state.getCozeStatus()) || COZE_STATUS_RUNNING.equals(state.getCozeStatus()))) {
|
||||
return;
|
||||
try (lockHandle) {
|
||||
pollPendingCozeStateLocked(stateId);
|
||||
}
|
||||
if (!tryClaimCozeStateForPoll(state)) {
|
||||
return;
|
||||
}
|
||||
CozeBatchContext context = readCozeBatchContext(state);
|
||||
if (context == null || context.jobId() == null || context.resultId() == null) {
|
||||
markCozeStateTerminal(state, COZE_STATUS_FAILED, "Coze batch context missing");
|
||||
return;
|
||||
}
|
||||
taskFileJobService.touchRunning(context.jobId());
|
||||
}
|
||||
|
||||
private void pollPendingCozeStateLocked(Long stateId) {
|
||||
try {
|
||||
TaskScopeStateEntity state = taskScopeStateMapper.selectById(stateId);
|
||||
if (state == null || state.getCozeExecuteId() == null || state.getCozeExecuteId().isBlank()) {
|
||||
return;
|
||||
}
|
||||
if (!(COZE_STATUS_SUBMITTED.equals(state.getCozeStatus()) || COZE_STATUS_RUNNING.equals(state.getCozeStatus()))) {
|
||||
return;
|
||||
}
|
||||
if (!tryClaimCozeStateForPoll(state)) {
|
||||
log.info("[appearance-patent] coze poll skipped by claim guard taskId={} stateId={} executeId={} lastPolledAt={}",
|
||||
state.getTaskId(), state.getId(), state.getCozeExecuteId(), state.getCozeLastPolledAt());
|
||||
return;
|
||||
}
|
||||
CozeBatchContext context = readCozeBatchContext(state);
|
||||
if (context == null || context.jobId() == null || context.resultId() == null) {
|
||||
log.warn("[appearance-patent] coze poll aborted because batch context is missing taskId={} stateId={} executeId={} stateJson={}",
|
||||
state.getTaskId(), state.getId(), state.getCozeExecuteId(), abbreviate(state.getStateJson(), 300));
|
||||
markCozeStateTerminal(state, COZE_STATUS_FAILED, "Coze batch context missing");
|
||||
return;
|
||||
}
|
||||
taskFileJobService.touchRunning(context.jobId());
|
||||
log.info("[appearance-patent] coze poll start taskId={} stateId={} executeId={} jobId={} chunk={} batch={}/{}",
|
||||
state.getTaskId(), state.getId(), state.getCozeExecuteId(),
|
||||
context.jobId(), context.chunkIndex(), context.batchIndex(), context.batchTotal());
|
||||
AppearancePatentCozeClient.CozePollResponse poll = cozeClient.pollWorkflow(state.getCozeExecuteId());
|
||||
if (!poll.hasPayload() && !poll.isFinished() && !isCozeStateTimedOut(state)) {
|
||||
log.info("[appearance-patent] coze poll pending taskId={} stateId={} executeId={} status={}",
|
||||
state.getTaskId(), state.getId(), state.getCozeExecuteId(), poll.status());
|
||||
updateCozeStateRunning(state, null);
|
||||
return;
|
||||
}
|
||||
@@ -1291,6 +1395,9 @@ public class AppearancePatentTaskService {
|
||||
: "Coze async workflow completed without output";
|
||||
}
|
||||
List<AppearancePatentResultRowDto> batchRows = readCozeBatchRows(state);
|
||||
if (batchRows.isEmpty() && failureMessage.isBlank()) {
|
||||
failureMessage = "Coze batch payload missing";
|
||||
}
|
||||
List<AppearancePatentResultRowDto> cozeRows = failureMessage.isBlank()
|
||||
? cozeClient.mergeRowsFromDataText(batchRows, poll.resolvedPayloadText())
|
||||
: cozeClient.markRowsFailed(batchRows, failureMessage);
|
||||
@@ -1302,27 +1409,38 @@ public class AppearancePatentTaskService {
|
||||
markCozeStateTerminal(state,
|
||||
failureMessage.isBlank() ? COZE_STATUS_DONE : COZE_STATUS_FAILED,
|
||||
failureMessage.isBlank() ? null : failureMessage);
|
||||
maybeFinalizeCozeJob(state.getTaskId(), context);
|
||||
log.info("[appearance-patent] coze poll completed taskId={} stateId={} executeId={} status={} batchRows={} mergedRows={} failure={}",
|
||||
state.getTaskId(), state.getId(), state.getCozeExecuteId(),
|
||||
failureMessage.isBlank() ? COZE_STATUS_DONE : COZE_STATUS_FAILED,
|
||||
batchRows.size(), cozeRows.size(), firstNonBlank(failureMessage, "-"));
|
||||
maybeFinalizeCozeJobLocked(state.getTaskId(), context);
|
||||
} catch (Exception ex) {
|
||||
TaskScopeStateEntity state = taskScopeStateMapper.selectById(stateId);
|
||||
String message = firstNonBlank(ex.getMessage(), "Coze poll failed");
|
||||
if (isCozeStateTimedOut(state)) {
|
||||
List<AppearancePatentResultRowDto> batchRows = readCozeBatchRows(state);
|
||||
FileTaskEntity task = fileTaskMapper.selectById(state.getTaskId());
|
||||
if (task != null) {
|
||||
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
|
||||
mergeCozeRowsIntoChunk(task,
|
||||
context.chunkScopeHash(),
|
||||
context.chunkIndex(),
|
||||
cozeClient.markRowsFailed(batchRows, message),
|
||||
allRowsByBaseId);
|
||||
if (state != null) {
|
||||
CozeBatchContext context = readCozeBatchContext(state);
|
||||
if (isCozeStateTimedOut(state) && context != null) {
|
||||
List<AppearancePatentResultRowDto> batchRows = readCozeBatchRows(state);
|
||||
FileTaskEntity task = fileTaskMapper.selectById(state.getTaskId());
|
||||
if (task != null) {
|
||||
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
|
||||
mergeCozeRowsIntoChunk(task,
|
||||
context.chunkScopeHash(),
|
||||
context.chunkIndex(),
|
||||
cozeClient.markRowsFailed(batchRows, message),
|
||||
allRowsByBaseId);
|
||||
}
|
||||
markCozeStateTerminal(state, COZE_STATUS_FAILED, message);
|
||||
maybeFinalizeCozeJobLocked(state.getTaskId(), context);
|
||||
return;
|
||||
}
|
||||
markCozeStateTerminal(state, COZE_STATUS_FAILED, message);
|
||||
maybeFinalizeCozeJob(state.getTaskId(), context);
|
||||
log.warn("[appearance-patent] coze poll failed taskId={} stateId={} executeId={} err={}",
|
||||
state.getTaskId(), state.getId(), state.getCozeExecuteId(), message);
|
||||
updateCozeStateRunning(state, message);
|
||||
return;
|
||||
}
|
||||
log.warn("[appearance-patent] coze poll failed taskId={} stateId={} executeId={} err={}",
|
||||
state.getTaskId(), state.getId(), state.getCozeExecuteId(), message);
|
||||
updateCozeStateRunning(state, message);
|
||||
log.warn("[appearance-patent] coze poll crashed before state refresh stateId={} err={}",
|
||||
stateId, message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1376,6 +1494,19 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
|
||||
private void maybeFinalizeCozeJob(Long taskId, CozeBatchContext context) {
|
||||
if (taskId == null || context == null || countPendingCozeStates(taskId) > 0) {
|
||||
return;
|
||||
}
|
||||
TaskDistributedLockService.LockHandle taskLockHandle = acquireTaskLock(taskId, 0L);
|
||||
if (taskLockHandle == null) {
|
||||
return;
|
||||
}
|
||||
try (taskLockHandle) {
|
||||
maybeFinalizeCozeJobLocked(taskId, context);
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeFinalizeCozeJobLocked(Long taskId, CozeBatchContext context) {
|
||||
if (taskId == null || context == null || countPendingCozeStates(taskId) > 0) {
|
||||
return;
|
||||
}
|
||||
@@ -1388,19 +1519,15 @@ public class AppearancePatentTaskService {
|
||||
if (countPendingCozeStates(taskId) > 0) {
|
||||
return;
|
||||
}
|
||||
FileTaskEntity task = fileTaskMapper.selectById(taskId);
|
||||
FileResultEntity result = fileResultMapper.selectById(context.resultId());
|
||||
TaskFileJobEntity job = taskFileJobService.findAssembleJob(taskId, MODULE_TYPE, context.resultId());
|
||||
if (task == null || result == null || job == null || "SUCCESS".equals(job.getStatus())) {
|
||||
if (job == null || "SUCCESS".equals(job.getStatus())) {
|
||||
return;
|
||||
}
|
||||
int cozeWorkUnits = countCompletedCozeStates(taskId);
|
||||
int totalProgressUnits = Math.max(3, cozeWorkUnits + 3);
|
||||
completeCozeFileJob(task, result, job, totalProgressUnits, cozeWorkUnits);
|
||||
taskFileJobService.markSuccess(job, result.getResultFileUrl());
|
||||
cleanupResultFileJob(job);
|
||||
log.info("[appearance-patent] coze async job finalized taskId={} jobId={} resultId={} resultFileUrl={}",
|
||||
taskId, job.getId(), result.getId(), result.getResultFileUrl());
|
||||
boolean requeued = taskFileJobService.requeue(job.getId(), "Coze results ready, assembling xlsx");
|
||||
if (requeued) {
|
||||
log.info("[appearance-patent] coze async results ready, result file job requeued taskId={} jobId={} resultId={}",
|
||||
taskId, job.getId(), context.resultId());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
TaskFileJobEntity job = taskFileJobService.findAssembleJob(taskId, MODULE_TYPE, context.resultId());
|
||||
if (job != null) {
|
||||
@@ -1416,6 +1543,9 @@ public class AppearancePatentTaskService {
|
||||
TaskFileJobEntity job,
|
||||
int totalProgressUnits,
|
||||
int cozeWorkUnits) {
|
||||
if (countPendingCozeStates(task.getId()) > 0) {
|
||||
throw new BusinessException("Coze 结果仍在处理中,暂不能生成结果文件");
|
||||
}
|
||||
int assembleProgress = Math.max(1, Math.min(totalProgressUnits - 2, cozeWorkUnits));
|
||||
saveFileBuildProgress(task, job, totalProgressUnits, assembleProgress, "Assembling xlsx");
|
||||
assembleResultWorkbook(task, result);
|
||||
@@ -1534,6 +1664,20 @@ public class AppearancePatentTaskService {
|
||||
+ ":batch:" + batchIndex;
|
||||
}
|
||||
|
||||
private String abbreviate(String value, int maxLength) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
int normalizedMaxLength = Math.max(0, maxLength);
|
||||
if (value.length() <= normalizedMaxLength) {
|
||||
return value;
|
||||
}
|
||||
if (normalizedMaxLength <= 3) {
|
||||
return value.substring(0, normalizedMaxLength);
|
||||
}
|
||||
return value.substring(0, normalizedMaxLength - 3) + "...";
|
||||
}
|
||||
|
||||
private int countCozeWorkUnits(List<TaskChunkEntity> chunks, int batchSize) {
|
||||
if (chunks == null || chunks.isEmpty()) {
|
||||
return 0;
|
||||
@@ -1579,6 +1723,11 @@ public class AppearancePatentTaskService {
|
||||
if (job == null || job.getTaskId() == null) {
|
||||
return;
|
||||
}
|
||||
if (countPendingCozeStates(job.getTaskId()) > 0) {
|
||||
log.warn("[appearance-patent] skip cleanup because coze is still pending taskId={} jobId={}",
|
||||
job.getTaskId(), job.getId());
|
||||
return;
|
||||
}
|
||||
deleteTransientTaskPayloads(job.getTaskId());
|
||||
taskChunkMapper.delete(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
.eq(TaskChunkEntity::getTaskId, job.getTaskId())
|
||||
@@ -1591,17 +1740,19 @@ public class AppearancePatentTaskService {
|
||||
private void assembleResultWorkbook(FileTaskEntity task, FileResultEntity result) {
|
||||
AppearancePatentParsedPayloadDto parsed = readParsedPayload(task);
|
||||
Map<String, AppearancePatentResultRowDto> resultMap = loadPersistedResultRowsWithRetry(task.getId(), parsed.getAllItems().size());
|
||||
long resolvedRows = parsed.getAllItems().stream()
|
||||
.filter(row -> findResultRow(row, resultMap) != null || findResultRowByAsin(row.getAsin(), resultMap) != null)
|
||||
List<AppearancePatentParsedRowVo> receivedRows = filterReceivedParsedRows(parsed.getAllItems(), resultMap);
|
||||
long resolvedRows = receivedRows.stream()
|
||||
.filter(row -> findResultRow(row, resultMap) != null)
|
||||
.count();
|
||||
long reasonRows = resultMap.values().stream()
|
||||
.filter(this::hasReasonFields)
|
||||
.count();
|
||||
log.info("[appearance-patent] assemble workbook taskId={} parsedRows={} resultRows={} resolvedRows={} reasonRows={}",
|
||||
task.getId(), parsed.getAllItems().size(), resultMap.size(), resolvedRows, reasonRows);
|
||||
if (!parsed.getAllItems().isEmpty() && resultMap.isEmpty()) {
|
||||
log.info("[appearance-patent] assemble workbook taskId={} parsedRows={} receivedRows={} resultRows={} resolvedRows={} reasonRows={}",
|
||||
task.getId(), parsed.getAllItems().size(), receivedRows.size(), resultMap.size(), resolvedRows, reasonRows);
|
||||
if (!parsed.getAllItems().isEmpty() && receivedRows.isEmpty()) {
|
||||
throw new BusinessException("外观专利检测结果为空,请稍后重试生成结果文件");
|
||||
}
|
||||
validateCompleteCozeCoverage(task.getId(), receivedRows, resultMap);
|
||||
File outputDir = new File(storageProperties.getLocalTempDir(), "appearance-patent-result");
|
||||
if (!outputDir.exists() && !outputDir.mkdirs()) {
|
||||
throw new BusinessException("创建结果目录失败");
|
||||
@@ -1614,13 +1765,13 @@ public class AppearancePatentTaskService {
|
||||
+ "-result.xlsx";
|
||||
File xlsx = new File(outputDir, tempFilename);
|
||||
try {
|
||||
writeResultWorkbook(xlsx, parsed, resultMap);
|
||||
writeResultWorkbook(xlsx, parsed, receivedRows, resultMap);
|
||||
String objectKey = ossStorageService.uploadResultFile(xlsx, MODULE_TYPE);
|
||||
result.setResultFilename(filename);
|
||||
result.setResultFileUrl(objectKey);
|
||||
result.setResultFileSize(xlsx.length());
|
||||
result.setResultContentType(CONTENT_TYPE_XLSX);
|
||||
result.setRowCount(parsed.getAllItems().size());
|
||||
result.setRowCount(receivedRows.size());
|
||||
} finally {
|
||||
if (xlsx.exists() && !xlsx.delete()) {
|
||||
log.warn("[appearance-patent] delete temp xlsx failed file={}", xlsx);
|
||||
@@ -1628,6 +1779,56 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private List<AppearancePatentParsedRowVo> filterReceivedParsedRows(List<AppearancePatentParsedRowVo> parsedRows,
|
||||
Map<String, AppearancePatentResultRowDto> resultMap) {
|
||||
if (parsedRows == null || parsedRows.isEmpty() || resultMap == null || resultMap.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<AppearancePatentParsedRowVo> receivedRows = new ArrayList<>();
|
||||
for (AppearancePatentParsedRowVo parsedRow : parsedRows) {
|
||||
if (findResultRow(parsedRow, resultMap) != null) {
|
||||
receivedRows.add(parsedRow);
|
||||
}
|
||||
}
|
||||
return receivedRows;
|
||||
}
|
||||
|
||||
private void validateCompleteCozeCoverage(Long taskId,
|
||||
List<AppearancePatentParsedRowVo> receivedRows,
|
||||
Map<String, AppearancePatentResultRowDto> resultMap) {
|
||||
if (receivedRows == null || receivedRows.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int expectedRows = 0;
|
||||
int missingRows = 0;
|
||||
List<String> sampleAsins = new ArrayList<>();
|
||||
for (AppearancePatentParsedRowVo parsedRow : receivedRows) {
|
||||
if (!hasPromptFields(parsedRow)) {
|
||||
continue;
|
||||
}
|
||||
expectedRows++;
|
||||
AppearancePatentResultRowDto resultRow = findResultRow(parsedRow, resultMap);
|
||||
if (resultRow == null || !hasCompleteCozeResult(resultRow)) {
|
||||
missingRows++;
|
||||
if (sampleAsins.size() < 5) {
|
||||
sampleAsins.add(firstNonBlank(parsedRow.getAsin(), firstNonBlank(parsedRow.getDisplayId(), "")));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (expectedRows > 0 && missingRows > 0) {
|
||||
log.warn("[appearance-patent] incomplete coze coverage taskId={} expectedRows={} missingRows={} samples={}",
|
||||
taskId, expectedRows, missingRows, sampleAsins);
|
||||
throw new BusinessException("Coze 结果不完整:缺少 " + missingRows + "/" + expectedRows + " 条检测结果,请等待重试或重新运行任务");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasCompleteCozeResult(AppearancePatentResultRowDto row) {
|
||||
return hasUsableCozeField(row.getTitleRisk())
|
||||
&& hasUsableCozeField(row.getAppearanceRisk())
|
||||
&& hasUsableCozeField(row.getPatentRisk())
|
||||
&& hasUsableCozeField(row.getConclusion());
|
||||
}
|
||||
|
||||
private Map<String, AppearancePatentResultRowDto> loadPersistedResultRows(Long taskId) {
|
||||
Map<String, AppearancePatentResultRowDto> result = new LinkedHashMap<>();
|
||||
List<TaskChunkEntity> chunks = taskChunkMapper.selectList(new LambdaQueryWrapper<TaskChunkEntity>()
|
||||
@@ -1667,7 +1868,10 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
}
|
||||
|
||||
private void writeResultWorkbook(File xlsx, AppearancePatentParsedPayloadDto parsed, Map<String, AppearancePatentResultRowDto> resultMap) {
|
||||
private void writeResultWorkbook(File xlsx,
|
||||
AppearancePatentParsedPayloadDto parsed,
|
||||
List<AppearancePatentParsedRowVo> receivedRows,
|
||||
Map<String, AppearancePatentResultRowDto> resultMap) {
|
||||
try (SXSSFWorkbook workbook = new SXSSFWorkbook(200); FileOutputStream fos = new FileOutputStream(xlsx)) {
|
||||
Sheet sheet = workbook.createSheet("外观专利检测结果");
|
||||
CellStyle headerStyle = workbook.createCellStyle();
|
||||
@@ -1686,11 +1890,9 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
|
||||
int rowIndex = 1;
|
||||
for (AppearancePatentParsedRowVo parsedRow : parsed.getAllItems()) {
|
||||
List<AppearancePatentParsedRowVo> rowsToWrite = receivedRows == null ? List.of() : receivedRows;
|
||||
for (AppearancePatentParsedRowVo parsedRow : rowsToWrite) {
|
||||
AppearancePatentResultRowDto resultRow = findResultRow(parsedRow, resultMap);
|
||||
if (resultRow == null) {
|
||||
resultRow = findResultRowByAsin(parsedRow.getAsin(), resultMap);
|
||||
}
|
||||
String missingReason = "";
|
||||
if (resultRow == null) {
|
||||
missingReason = hasPromptFields(parsedRow) ? "未匹配到检测结果" : "未送检:缺少标题或图片";
|
||||
@@ -1708,7 +1910,7 @@ public class AppearancePatentTaskService {
|
||||
row.createCell(col++).setCellValue(resultRow == null ? missingReason : userFacingCozeCellValue(resultRow, resultRow.getPatentRisk()));
|
||||
row.createCell(col).setCellValue(resultRow == null ? "未送检" : userFacingConclusion(resultRow));
|
||||
}
|
||||
writeReasonSheet(workbook, headerStyle, parsed, resultMap);
|
||||
writeReasonSheet(workbook, headerStyle, rowsToWrite, resultMap);
|
||||
workbook.write(fos);
|
||||
workbook.dispose();
|
||||
} catch (Exception ex) {
|
||||
@@ -1718,7 +1920,7 @@ public class AppearancePatentTaskService {
|
||||
|
||||
private void writeReasonSheet(SXSSFWorkbook workbook,
|
||||
CellStyle headerStyle,
|
||||
AppearancePatentParsedPayloadDto parsed,
|
||||
List<AppearancePatentParsedRowVo> rowsToWrite,
|
||||
Map<String, AppearancePatentResultRowDto> resultMap) {
|
||||
Sheet sheet = workbook.createSheet("原因");
|
||||
Row header = sheet.createRow(0);
|
||||
@@ -1731,15 +1933,12 @@ public class AppearancePatentTaskService {
|
||||
|
||||
Set<String> writtenAsins = new LinkedHashSet<>();
|
||||
int rowIndex = 1;
|
||||
for (AppearancePatentParsedRowVo parsedRow : parsed.getAllItems()) {
|
||||
for (AppearancePatentParsedRowVo parsedRow : rowsToWrite == null ? List.<AppearancePatentParsedRowVo>of() : rowsToWrite) {
|
||||
String asin = normalize(parsedRow.getAsin()).toUpperCase(Locale.ROOT);
|
||||
if (asin.isBlank() || !writtenAsins.add(asin)) {
|
||||
continue;
|
||||
}
|
||||
AppearancePatentResultRowDto resultRow = findResultRow(parsedRow, resultMap);
|
||||
if (resultRow == null) {
|
||||
resultRow = findResultRowByAsin(asin, resultMap);
|
||||
}
|
||||
Row row = sheet.createRow(rowIndex++);
|
||||
row.createCell(0).setCellValue(asin);
|
||||
row.createCell(1).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getAppearanceReason(), ""));
|
||||
@@ -2072,6 +2271,36 @@ public class AppearancePatentTaskService {
|
||||
return vo;
|
||||
}
|
||||
|
||||
private int historyPriority(FileResultEntity row, FileTaskEntity task, TaskFileJobEntity job) {
|
||||
String taskStatus = task == null ? null : task.getStatus();
|
||||
if (STATUS_RUNNING.equals(taskStatus) || isHistoryFileBuilding(row, taskStatus, job)) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private LocalDateTime historyActivityTime(FileResultEntity row, FileTaskEntity task, TaskFileJobEntity job) {
|
||||
LocalDateTime latest = latestTime(
|
||||
task == null ? null : task.getUpdatedAt(),
|
||||
job == null ? null : job.getUpdatedAt(),
|
||||
task == null ? null : task.getFinishedAt(),
|
||||
row == null ? null : row.getCreatedAt(),
|
||||
task == null ? null : task.getCreatedAt());
|
||||
return latest == null && row != null ? row.getCreatedAt() : latest;
|
||||
}
|
||||
|
||||
private boolean isHistoryFileBuilding(FileResultEntity row, String taskStatus, TaskFileJobEntity job) {
|
||||
if (!STATUS_SUCCESS.equals(taskStatus)) {
|
||||
return false;
|
||||
}
|
||||
boolean fileReady = row != null && row.getResultFileUrl() != null && !row.getResultFileUrl().isBlank();
|
||||
if (fileReady) {
|
||||
return false;
|
||||
}
|
||||
String fileStatus = job == null ? null : job.getStatus();
|
||||
return !STATUS_SUCCESS.equals(fileStatus) && !STATUS_FAILED.equals(fileStatus);
|
||||
}
|
||||
|
||||
private void attachFileJobState(AppearancePatentHistoryItemVo vo, FileResultEntity row, TaskFileJobEntity job) {
|
||||
vo.setFileReady(row.getResultFileUrl() != null && !row.getResultFileUrl().isBlank());
|
||||
if (job == null) {
|
||||
@@ -2126,6 +2355,22 @@ public class AppearancePatentTaskService {
|
||||
return t == null ? null : t.toString();
|
||||
}
|
||||
|
||||
private LocalDateTime latestTime(LocalDateTime... values) {
|
||||
LocalDateTime latest = null;
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
for (LocalDateTime value : values) {
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
if (latest == null || value.isAfter(latest)) {
|
||||
latest = value;
|
||||
}
|
||||
}
|
||||
return latest;
|
||||
}
|
||||
|
||||
private String firstNonBlank(String preferred, String fallback) {
|
||||
return preferred == null || preferred.isBlank() ? fallback : preferred.trim();
|
||||
}
|
||||
@@ -2157,9 +2402,35 @@ public class AppearancePatentTaskService {
|
||||
}
|
||||
|
||||
private String storeParsedPayload(Long taskId, String scopeHash, String parsedPayloadJson) {
|
||||
requireSharedTransientPayloadStorage("parsed payload");
|
||||
return transientPayloadStorageService.storeParsedPayloadFast(MODULE_TYPE, taskId, scopeHash, parsedPayloadJson, false);
|
||||
}
|
||||
|
||||
private TaskDistributedLockService.LockHandle acquireTaskLock(Long taskId, long waitMillis) {
|
||||
return taskDistributedLockService.acquire(MODULE_TYPE, taskId, TASK_LOCK_TTL, waitMillis);
|
||||
}
|
||||
|
||||
private String storeSharedChunkPayload(Long taskId, String scopeHash, Integer chunkIndex, String payloadJson) {
|
||||
requireSharedTransientPayloadStorage("chunk payload");
|
||||
return transientPayloadStorageService.storeChunkPayload(MODULE_TYPE, taskId, scopeHash, chunkIndex, payloadJson);
|
||||
}
|
||||
|
||||
private String storeSharedChunkPayloadVersioned(Long taskId, String scopeHash, Integer chunkIndex, String payloadJson) {
|
||||
requireSharedTransientPayloadStorage("merged chunk payload");
|
||||
return transientPayloadStorageService.storeChunkPayloadVersioned(MODULE_TYPE, taskId, scopeHash, chunkIndex, payloadJson);
|
||||
}
|
||||
|
||||
private String storeSharedCozeBatchPayload(Long taskId, String scopeHash, String payloadJson) {
|
||||
requireSharedTransientPayloadStorage("coze batch payload");
|
||||
return transientPayloadStorageService.storeParsedPayloadFast(MODULE_TYPE, taskId, scopeHash, payloadJson, true);
|
||||
}
|
||||
|
||||
private void requireSharedTransientPayloadStorage(String payloadType) {
|
||||
if (!transientPayloadStorageService.isSharedWriteEnabled()) {
|
||||
throw new BusinessException("外观专利" + payloadType + "必须写入共享临时存储 RustFS,请检查 aiimage.transient-storage 配置");
|
||||
}
|
||||
}
|
||||
|
||||
private long elapsedMs(long start, long end) {
|
||||
return (end - start) / 1_000_000L;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user