增加rufts

This commit is contained in:
super
2026-04-27 18:45:33 +08:00
parent 60a6494260
commit b96b136fa3
25 changed files with 1992 additions and 173 deletions

View File

@@ -13,8 +13,8 @@ client_name=ShuFuAI
# java_api_base=http://47.111.163.154:18080
# java_api_base=http://127.0.0.1:18080
java_api_base=http://8.136.19.173:18080
java_api_base=http://127.0.0.1:18080
# java_api_base=http://8.136.19.173:18080
# java_api_base=http://121.196.149.225:18080

Binary file not shown.

Binary file not shown.

View File

@@ -398,8 +398,9 @@ class SpiderTask:
url = f"{DELETE_BRAND_API_BASE}/api/appearance-patent/tasks/{task_id}/result"
submission_id = f"appearance-patent:{task_id}"
payload ={
"submissionId": f"{int(time.time())}",
"submissionId": submission_id,
"chunkIndex": chunkIndex,
"chunkTotal": chunkTotal,
"error": error,
@@ -412,6 +413,7 @@ class SpiderTask:
max_retries = 3
for retry in range(max_retries):
try:
request_timeout = 300 if is_done else 30
print("================【详情采集】=====================")
self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)")
self.log(f"回传URL: {url}")
@@ -420,7 +422,7 @@ class SpiderTask:
url,
json=payload,
headers={"Content-Type": "application/json"},
timeout=30,
timeout=request_timeout,
verify=False
)
self.log(f"回传结果: {response.text}")

1056
backend-java/jstack-6488.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,8 @@ public class AppearancePatentProperties {
private String cozeWorkflowId = "7632683471312355338";
private String cozeToken = "";
private int cozeBatchSize = 10;
private int cozeConnectTimeoutMillis = 10000;
private int cozeReadTimeoutMillis = 60000;
private int staleTimeoutMinutes = 20;
private String staleFinalizeCron = "0 */2 * * * *";
}

View File

@@ -7,6 +7,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "aiimage.delete-brand-progress")
public class DeleteBrandProgressProperties {
private long heartbeatTimeoutMinutes = 15;
private long deleteBrandInitialTimeoutMinutes = 20;
private String staleCheckCron = "*/30 * * * * *";
/**

View File

@@ -9,11 +9,14 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import org.springframework.web.client.RestClient;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@Component
@@ -33,29 +36,72 @@ public class AppearancePatentCozeClient {
return rows.stream().map(this::copy).toList();
}
try {
return inspectWithFallback(rows, prompt);
} catch (Exception ex) {
String failureMessage = failureMessage(ex);
log.warn("[appearance-patent] coze batch failed size={} err={}", rows.size(), failureMessage);
return rows.stream().map(this::copy).map(row -> markFailed(row, failureMessage)).toList();
}
}
private List<AppearancePatentResultRowDto> inspectWithFallback(List<AppearancePatentResultRowDto> rows, String prompt) {
try {
if (rows.size() == 1) {
return inspectSingleRowWithRetry(rows, prompt);
}
InspectAttempt attempt = inspectOnce(rows, prompt);
if (attempt.resolvedCount() < rows.size()) {
throw new PartialCozeResultException(attempt.resolvedCount(), rows.size(), attempt.rawResultCount());
}
return attempt.mergedRows();
} catch (Exception ex) {
if (shouldSplitBatch(rows, ex)) {
int middle = rows.size() / 2;
log.warn("[appearance-patent] coze batch fallback split size={} left={} right={} err={}",
rows.size(), middle, rows.size() - middle, failureMessage(ex));
List<AppearancePatentResultRowDto> merged = new ArrayList<>(rows.size());
merged.addAll(inspectWithFallback(rows.subList(0, middle), prompt));
merged.addAll(inspectWithFallback(rows.subList(middle, rows.size()), prompt));
return merged;
}
throw propagate(ex);
}
}
private List<AppearancePatentResultRowDto> inspectSingleRowWithRetry(List<AppearancePatentResultRowDto> rows, String prompt) throws Exception {
AppearancePatentResultRowDto row = rows.getFirst();
PartialCozeResultException lastFailure = null;
for (int attemptIndex = 1; attemptIndex <= 3; attemptIndex++) {
InspectAttempt attempt = inspectOnce(rows, prompt);
if (attempt.resolvedCount() == rows.size()) {
return attempt.mergedRows();
}
log.warn("[appearance-patent] coze single unresolved attempt={} rowId={} asin={} country={} title={} url={} raw={}",
attemptIndex,
row.getId(),
row.getAsin(),
row.getCountry(),
abbreviate(row.getTitle(), 120),
abbreviate(row.getUrl(), 120),
abbreviate(attempt.raw(), 500));
lastFailure = new PartialCozeResultException(attempt.resolvedCount(), rows.size(), attempt.rawResultCount());
}
throw lastFailure == null ? new PartialCozeResultException(0, rows.size(), 0) : lastFailure;
}
private InspectAttempt inspectOnce(List<AppearancePatentResultRowDto> rows, String prompt) throws Exception {
String raw = postWorkflow(rows, prompt);
List<CozeResult> results = parseResults(raw);
List<AppearancePatentResultRowDto> merged = new ArrayList<>();
for (int i = 0; i < rows.size(); i++) {
AppearancePatentResultRowDto row = copy(rows.get(i));
if (i < results.size()) {
CozeResult result = results.get(i);
row.setTitleRisk(result.title());
row.setAppearanceRisk(result.appearance());
row.setPatentRisk(result.patent());
row.setConclusion(result.result());
}
merged.add(row);
}
return merged;
} catch (Exception ex) {
log.warn("[appearance-patent] coze batch failed size={} err={}", rows.size(), ex.getMessage());
return rows.stream().map(this::copy).toList();
}
List<AppearancePatentResultRowDto> merged = mergeRows(rows, results);
return new InspectAttempt(raw, merged, resolvedCount(merged), results.size());
}
private String postWorkflow(List<AppearancePatentResultRowDto> rows, String prompt) {
Map<String, Object> parameters = new LinkedHashMap<>();
parameters.put("row_id_list", rows.stream().map(row -> nonBlank(row.getId(), "")).toList());
parameters.put("asin_list", rows.stream().map(row -> nonBlank(row.getAsin(), "")).toList());
parameters.put("country_list", rows.stream().map(row -> nonBlank(row.getCountry(), "")).toList());
parameters.put("row_key_list", rows.stream().map(this::rowKey).toList());
parameters.put("title_list", rows.stream().map(row -> nonBlank(row.getTitle(), row.getAsin())).toList());
parameters.put("url_list", rows.stream().map(row -> nonBlank(row.getUrl(), "")).toList());
parameters.put("prompt", prompt == null ? "" : prompt);
@@ -71,7 +117,10 @@ public class AppearancePatentCozeClient {
headers.setContentType(MediaType.APPLICATION_JSON);
});
request.body(body);
return request.retrieve().body(String.class);
return request.exchange((clientRequest, clientResponse) -> {
byte[] responseBytes = StreamUtils.copyToByteArray(clientResponse.getBody());
return responseBytes.length == 0 ? "" : new String(responseBytes, StandardCharsets.UTF_8);
});
}
private List<CozeResult> parseResults(String raw) throws Exception {
@@ -89,6 +138,9 @@ public class AppearancePatentCozeClient {
if (array.isArray()) {
for (JsonNode node : array) {
results.add(new CozeResult(
text(firstNonNull(node.get("row_id"), firstNonNull(node.get("rowId"), node.get("id")))),
text(node.get("asin")),
text(firstNonNull(node.get("country"), node.get("site"))),
text(node.get("title")),
text(node.get("appearance")),
text(firstNonNull(node.get("patent"), node.get("patent "))),
@@ -99,10 +151,51 @@ public class AppearancePatentCozeClient {
return results;
}
private List<AppearancePatentResultRowDto> mergeRows(List<AppearancePatentResultRowDto> rows, List<CozeResult> results) {
Map<String, CozeResult> resultByCompositeKey = new LinkedHashMap<>();
Map<String, CozeResult> resultByRowId = new LinkedHashMap<>();
for (CozeResult result : results) {
String compositeKey = rowKey(result.rowId(), result.asin(), result.country());
if (!compositeKey.isBlank()) {
resultByCompositeKey.putIfAbsent(compositeKey, result);
}
String rowIdKey = normalize(result.rowId());
if (!rowIdKey.isBlank()) {
resultByRowId.putIfAbsent(rowIdKey, result);
}
}
List<AppearancePatentResultRowDto> merged = new ArrayList<>(rows.size());
boolean allowIndexFallback = results.size() == rows.size();
for (int i = 0; i < rows.size(); i++) {
AppearancePatentResultRowDto row = copy(rows.get(i));
CozeResult result = resultByCompositeKey.get(rowKey(row.getId(), row.getAsin(), row.getCountry()));
if (result == null) {
result = resultByRowId.get(normalize(row.getId()));
}
if (result == null && allowIndexFallback && i < results.size()) {
result = results.get(i);
}
applyResult(row, result);
merged.add(row);
}
return merged;
}
private void applyResult(AppearancePatentResultRowDto row, CozeResult result) {
if (row == null || result == null) {
return;
}
row.setTitleRisk(result.title());
row.setAppearanceRisk(result.appearance());
row.setPatentRisk(result.patent());
row.setConclusion(result.result());
}
private RestClient restClient() {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(10000);
requestFactory.setReadTimeout(60000);
requestFactory.setConnectTimeout(properties.getCozeConnectTimeoutMillis());
requestFactory.setReadTimeout(properties.getCozeReadTimeoutMillis());
return RestClient.builder().requestFactory(requestFactory).build();
}
@@ -122,6 +215,67 @@ public class AppearancePatentCozeClient {
return row;
}
private AppearancePatentResultRowDto markFailed(AppearancePatentResultRowDto row, String failureMessage) {
if (row.getError() == null || row.getError().isBlank()) {
row.setError(failureMessage);
}
if (row.getConclusion() == null || row.getConclusion().isBlank()) {
row.setConclusion(failureMessage);
}
return row;
}
private boolean shouldSplitBatch(List<AppearancePatentResultRowDto> rows, Exception ex) {
return rows != null && rows.size() > 1 && isRetryableBatchFailure(ex);
}
private boolean isRetryableBatchFailure(Exception ex) {
if (ex instanceof PartialCozeResultException) {
return true;
}
String message = ex == null ? "" : nonBlank(ex.getMessage(), "");
return message.contains("Workflow node execution limit exceeded")
|| message.contains("Read timed out")
|| message.contains("Connection reset")
|| message.contains("I/O error on POST request")
|| message.toLowerCase(Locale.ROOT).contains("timeout");
}
private int resolvedCount(List<AppearancePatentResultRowDto> rows) {
int resolved = 0;
for (AppearancePatentResultRowDto row : rows) {
if (hasResolvedCozeFields(row)) {
resolved++;
}
}
return resolved;
}
private boolean hasResolvedCozeFields(AppearancePatentResultRowDto row) {
if (row == null) {
return false;
}
return !normalize(row.getTitleRisk()).isBlank()
|| !normalize(row.getAppearanceRisk()).isBlank()
|| !normalize(row.getPatentRisk()).isBlank()
|| !normalize(row.getConclusion()).isBlank();
}
private String abbreviate(String value, int maxLength) {
String normalized = value == null ? "" : value.trim();
if (normalized.length() <= maxLength) {
return normalized;
}
return normalized.substring(0, Math.max(0, maxLength - 3)) + "...";
}
private RuntimeException propagate(Exception ex) {
if (ex instanceof RuntimeException runtimeException) {
return runtimeException;
}
return new IllegalStateException(nonBlank(ex.getMessage(), "Coze call failed"), ex);
}
private JsonNode firstNonNull(JsonNode left, JsonNode right) {
return left == null || left.isNull() ? right : left;
}
@@ -134,6 +288,38 @@ public class AppearancePatentCozeClient {
return value == null || value.isBlank() ? fallback : value;
}
private String normalize(String value) {
return value == null ? "" : value.replace("\ufeff", "").replace("\u3000", " ").trim();
}
private String rowKey(AppearancePatentResultRowDto row) {
if (row == null) {
return "";
}
return rowKey(row.getId(), row.getAsin(), row.getCountry());
}
private String rowKey(String rowId, String asin, String country) {
return normalize(rowId) + "::" + normalize(asin).toUpperCase(Locale.ROOT) + "::" + normalize(country);
}
private String failureMessage(Exception ex) {
if (ex instanceof PartialCozeResultException partial) {
return "Coze返回结果不完整(" + partial.resolvedCount() + "/" + partial.expectedCount() + ")";
}
String message = ex == null ? null : ex.getMessage();
if (message == null || message.isBlank()) {
return "Coze调用失败";
}
if (message.contains("Workflow node execution limit exceeded")) {
return "Coze工作流节点执行超限请检查工作流配置";
}
if (message.contains("Read timed out")) {
return "Coze调用超时请稍后重试";
}
return "Coze调用失败: " + message;
}
private String stripBearer(String token) {
String normalized = token == null ? "" : token.trim();
return normalized.regionMatches(true, 0, "Bearer ", 0, 7) ? normalized.substring(7).trim() : normalized;
@@ -151,6 +337,49 @@ public class AppearancePatentCozeClient {
return base + suffix;
}
private record CozeResult(String title, String appearance, String patent, String result) {
private record CozeResult(
String rowId,
String asin,
String country,
String title,
String appearance,
String patent,
String result
) {
}
private record InspectAttempt(
String raw,
List<AppearancePatentResultRowDto> mergedRows,
int resolvedCount,
int rawResultCount
) {
}
private static final class PartialCozeResultException extends RuntimeException {
private final int resolvedCount;
private final int expectedCount;
private final int rawResultCount;
private PartialCozeResultException(int resolvedCount, int expectedCount, int rawResultCount) {
super("partial-result resolved=" + resolvedCount + "/" + expectedCount + " raw=" + rawResultCount);
this.resolvedCount = resolvedCount;
this.expectedCount = expectedCount;
this.rawResultCount = rawResultCount;
}
private int resolvedCount() {
return resolvedCount;
}
private int expectedCount() {
return expectedCount;
}
@SuppressWarnings("unused")
private int rawResultCount() {
return rawResultCount;
}
}
}

View File

@@ -16,6 +16,7 @@ public class AppearancePatentParsedRowVo {
@Schema(description = "前端展示和 Python 回传使用的 id。整数 id 原样保留,子数据第一条如 2_1 原样保留。", example = "2_1")
private String displayId;
@Schema(description = "亚马逊 ASIN。", example = "B0CJ8SNXXV")
private String groupKey;
private String asin;
@Schema(description = "国家或站点。", example = "英国")
private String country;

View File

@@ -22,14 +22,14 @@ public class AppearancePatentTaskCacheService {
private final StringRedisTemplate stringRedisTemplate;
private final ObjectMapper objectMapper;
public void appendPendingRow(Long taskId, Integer chunkIndex, AppearancePatentResultRowDto row) {
if (taskId == null || taskId <= 0 || chunkIndex == null || row == null) {
public void appendPendingRow(Long taskId, String scopeHash, Integer chunkIndex, AppearancePatentResultRowDto row) {
if (taskId == null || taskId <= 0 || scopeHash == null || scopeHash.isBlank() || chunkIndex == null || row == null) {
return;
}
try {
stringRedisTemplate.opsForList().rightPush(
pendingRowsKey(taskId),
objectMapper.writeValueAsString(new PendingRow(chunkIndex, row))
objectMapper.writeValueAsString(new PendingRow(scopeHash, chunkIndex, row))
);
stringRedisTemplate.expire(pendingRowsKey(taskId), Duration.ofHours(TTL_HOURS));
touchTaskHeartbeat(taskId);
@@ -139,6 +139,6 @@ public class AppearancePatentTaskCacheService {
return "appearance-patent:task:heartbeat:" + taskId;
}
public record PendingRow(Integer chunkIndex, AppearancePatentResultRowDto row) {
public record PendingRow(String scopeHash, Integer chunkIndex, AppearancePatentResultRowDto row) {
}
}

View File

@@ -49,7 +49,10 @@ import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import java.io.File;
import java.io.FileInputStream;
@@ -65,6 +68,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
@Service
@RequiredArgsConstructor
@@ -101,6 +105,7 @@ public class AppearancePatentTaskService {
private final AppearancePatentProperties properties;
private final TaskFileJobService taskFileJobService;
private final TransientPayloadStorageService transientPayloadStorageService;
private final PlatformTransactionManager transactionManager;
@Transactional
public AppearancePatentParseVo parseAndCreateTask(AppearancePatentParseRequest request) {
@@ -259,8 +264,16 @@ public class AppearancePatentTaskService {
return vo;
}
@Transactional
public void submitResult(Long taskId, AppearancePatentSubmitResultRequest request) {
if (transactionManager != null) {
SubmitContext context = inNewTransaction(() -> persistSubmittedChunk(taskId, request));
flushPendingRowsIfNeeded(context.task(), context.forceFlush());
inNewTransaction(() -> {
completeSubmittedChunk(context);
return null;
});
return;
}
FileTaskEntity task = fileTaskMapper.selectById(taskId);
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
throw new BusinessException("任务不存在");
@@ -302,19 +315,21 @@ public class AppearancePatentTaskService {
taskChunkMapper.insert(chunk);
} catch (DuplicateKeyException ex) {
inserted = false;
transientPayloadStorageService.deletePayloadIfPresent(storedPayload);
// storeChunkPayload uses a deterministic key like chunk-{index}. When two
// concurrent callbacks submit the same chunk, deleting the loser payload here
// can also remove the winner's shared object and break later assembly.
log.info("[appearance-patent] duplicate chunk inserted concurrently taskId={} scope={} chunk={}", taskId, scopeKey, chunkIndex);
}
if (inserted) {
for (AppearancePatentResultRowDto rawRow : rawRows) {
taskCacheService.appendPendingRow(taskId, chunkIndex, rawRow);
taskCacheService.appendPendingRow(taskId, scopeHash, chunkIndex, rawRow);
}
}
} else {
log.info("[appearance-patent] duplicate chunk ignored taskId={} scope={} chunk={}", taskId, scopeKey, chunkIndex);
}
flushPendingRowsIfNeeded(task, scopeHash, Boolean.TRUE.equals(request.getDone()));
flushPendingRowsIfNeeded(task, Boolean.TRUE.equals(request.getDone()));
TaskScopeStateEntity scope = taskScopeStateMapper.selectOne(new LambdaQueryWrapper<TaskScopeStateEntity>()
.eq(TaskScopeStateEntity::getTaskId, taskId)
@@ -394,8 +409,8 @@ public class AppearancePatentTaskService {
}
@Scheduled(cron = "${aiimage.appearance-patent.stale-finalize-cron:0 */2 * * * *}")
@Transactional
public void finalizeStaleTasks() {
if (transactionManager != null) {
LocalDateTime threshold = LocalDateTime.now().minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes()));
long thresholdMillis = threshold.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
List<FileTaskEntity> tasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
@@ -408,11 +423,188 @@ public class AppearancePatentTaskService {
if (heartbeatMillis > thresholdMillis) {
continue;
}
flushPendingRowsIfNeeded(task, DigestUtil.sha256Hex("appearance-patent-" + task.getId()), true);
flushPendingRowsIfNeeded(task, true);
inNewTransaction(() -> {
finalizeStaleTask(task.getId(), "Python interrupted before uploading final appearance patent result");
return null;
});
}
return;
}
LocalDateTime threshold = LocalDateTime.now().minusMinutes(Math.max(1, properties.getStaleTimeoutMinutes()));
long thresholdMillis = threshold.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
List<FileTaskEntity> tasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
.eq(FileTaskEntity::getModuleType, MODULE_TYPE)
.eq(FileTaskEntity::getStatus, STATUS_RUNNING)
.lt(FileTaskEntity::getUpdatedAt, threshold)
.last("limit 50"));
for (FileTaskEntity task : tasks) {
long heartbeatMillis = taskCacheService.getTaskHeartbeatMillis(task.getId());
if (heartbeatMillis > thresholdMillis) {
continue;
}
flushPendingRowsIfNeeded(task, true);
finalizeTask(task, "Python interrupted before uploading final appearance patent result", allRowCount(task), true);
}
}
private SubmitContext persistSubmittedChunk(Long taskId, AppearancePatentSubmitResultRequest request) {
FileTaskEntity task = fileTaskMapper.selectById(taskId);
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
throw new BusinessException("浠诲姟涓嶅瓨鍦?");
}
if (!STATUS_RUNNING.equals(task.getStatus())) {
throw new BusinessException("浠诲姟涓嶆槸杩愯涓姸鎬?");
}
int chunkIndex = request.getChunkIndex() == null ? 0 : request.getChunkIndex();
int chunkTotal = request.getChunkTotal() == null ? 1 : request.getChunkTotal();
boolean done = Boolean.TRUE.equals(request.getDone());
String scopeKey = firstNonBlank(request.getSubmissionId(), "task:" + taskId);
String scopeHash = DigestUtil.sha256Hex(scopeKey);
taskCacheService.touchTaskHeartbeat(taskId);
TaskChunkEntity existing = taskChunkMapper.selectOne(new LambdaQueryWrapper<TaskChunkEntity>()
.eq(TaskChunkEntity::getTaskId, taskId)
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
.eq(TaskChunkEntity::getScopeHash, scopeHash)
.eq(TaskChunkEntity::getChunkIndex, chunkIndex)
.last("limit 1"));
if (existing == null) {
List<AppearancePatentResultRowDto> rawRows = request.getItems() == null ? List.of() : request.getItems();
String payloadJson = writeJson(rawRows, "缁撴灉搴忓垪鍖栧け璐?");
TaskChunkEntity chunk = new TaskChunkEntity();
chunk.setTaskId(taskId);
chunk.setModuleType(MODULE_TYPE);
chunk.setScopeKey(scopeKey);
chunk.setScopeHash(scopeHash);
chunk.setChunkIndex(chunkIndex);
chunk.setChunkTotal(chunkTotal);
String storedPayload = transientPayloadStorageService.storeChunkPayload(MODULE_TYPE, taskId, scopeHash, chunkIndex, payloadJson);
chunk.setPayloadJson(storedPayload);
chunk.setPayloadHash(DigestUtil.sha256Hex(payloadJson));
chunk.setCreatedAt(LocalDateTime.now());
chunk.setUpdatedAt(LocalDateTime.now());
boolean inserted = true;
try {
taskChunkMapper.insert(chunk);
} catch (DuplicateKeyException ex) {
inserted = false;
log.info("[appearance-patent] duplicate chunk inserted concurrently taskId={} scope={} chunk={}", taskId, scopeKey, chunkIndex);
}
if (inserted) {
for (AppearancePatentResultRowDto rawRow : rawRows) {
taskCacheService.appendPendingRow(taskId, scopeHash, chunkIndex, rawRow);
}
}
} else {
log.info("[appearance-patent] duplicate chunk ignored taskId={} scope={} chunk={}", taskId, scopeKey, chunkIndex);
}
upsertScopeState(taskId, scopeKey, scopeHash, chunkTotal, request.getError(), done, false);
task.setUpdatedAt(LocalDateTime.now());
fileTaskMapper.updateById(task);
return new SubmitContext(task, scopeKey, scopeHash, done, request.getError());
}
private void completeSubmittedChunk(SubmitContext context) {
FileTaskEntity task = fileTaskMapper.selectById(context.task().getId());
if (task == null || !MODULE_TYPE.equals(task.getModuleType())) {
throw new BusinessException("浠诲姟涓嶅瓨鍦?");
}
if (!STATUS_RUNNING.equals(task.getStatus())) {
log.info("[appearance-patent] skip completion because task already finalized taskId={} status={}",
task.getId(), task.getStatus());
return;
}
upsertScopeState(task.getId(), context.scopeKey(), context.scopeHash(), null, context.error(), context.forceFlush(), true);
if (context.forceFlush() || context.error() != null && !context.error().isBlank()) {
finalizeTask(task, context.error(), allRowCount(task), true);
return;
}
task.setUpdatedAt(LocalDateTime.now());
fileTaskMapper.updateById(task);
}
private void finalizeStaleTask(Long taskId, String error) {
FileTaskEntity task = fileTaskMapper.selectById(taskId);
if (task == null || !MODULE_TYPE.equals(task.getModuleType()) || !STATUS_RUNNING.equals(task.getStatus())) {
return;
}
finalizeTask(task, error, allRowCount(task), true);
}
private void upsertScopeState(Long taskId,
String scopeKey,
String scopeHash,
Integer chunkTotal,
String error,
boolean completed,
boolean cozeDone) {
TaskScopeStateEntity scope = taskScopeStateMapper.selectOne(new LambdaQueryWrapper<TaskScopeStateEntity>()
.eq(TaskScopeStateEntity::getTaskId, taskId)
.eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE)
.eq(TaskScopeStateEntity::getScopeHash, scopeHash)
.last("limit 1"));
LocalDateTime now = LocalDateTime.now();
if (scope == null) {
scope = new TaskScopeStateEntity();
scope.setTaskId(taskId);
scope.setModuleType(MODULE_TYPE);
scope.setScopeKey(scopeKey);
scope.setScopeHash(scopeHash);
scope.setCreatedAt(now);
}
if (chunkTotal != null) {
scope.setChunkTotal(chunkTotal);
}
scope.setReceivedChunkCount(countChunks(taskId, scopeHash));
scope.setLastChunkAt(now);
scope.setLastError(error);
scope.setCompleted(completed ? 1 : 0);
scope.setUpdatedAt(now);
scope.setStateJson(cozeDone
? "{\"phase\":\"RECEIVED\",\"coze\":\"DONE\"}"
: "{\"phase\":\"RECEIVED\",\"coze\":\"PENDING\"}");
if (scope.getId() == null) {
try {
taskScopeStateMapper.insert(scope);
return;
} catch (DuplicateKeyException ex) {
log.info("[appearance-patent] duplicate scope state inserted concurrently taskId={} scope={}", taskId, scopeKey);
scope = taskScopeStateMapper.selectOne(new LambdaQueryWrapper<TaskScopeStateEntity>()
.eq(TaskScopeStateEntity::getTaskId, taskId)
.eq(TaskScopeStateEntity::getModuleType, MODULE_TYPE)
.eq(TaskScopeStateEntity::getScopeHash, scopeHash)
.last("limit 1"));
if (scope == null) {
log.info("[appearance-patent] scope state winner not committed yet, skip duplicate updater taskId={} scope={}",
taskId, scopeKey);
return;
}
if (chunkTotal != null) {
scope.setChunkTotal(chunkTotal);
}
scope.setReceivedChunkCount(countChunks(taskId, scopeHash));
scope.setLastChunkAt(now);
scope.setLastError(error);
scope.setCompleted(completed ? 1 : 0);
scope.setUpdatedAt(now);
scope.setStateJson(cozeDone
? "{\"phase\":\"RECEIVED\",\"coze\":\"DONE\"}"
: "{\"phase\":\"RECEIVED\",\"coze\":\"PENDING\"}");
}
}
taskScopeStateMapper.updateById(scope);
}
private <T> T inNewTransaction(Supplier<T> action) {
TransactionTemplate template = new TransactionTemplate(transactionManager);
template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
return template.execute(status -> action.get());
}
private List<AppearancePatentResultRowDto> applyCozeInBatches(List<AppearancePatentResultRowDto> items, FileTaskEntity task) {
if (items == null || items.isEmpty()) {
return List.of();
@@ -426,8 +618,10 @@ public class AppearancePatentTaskService {
return result;
}
private void flushPendingRowsIfNeeded(FileTaskEntity task, String scopeHash, boolean force) {
private void flushPendingRowsIfNeeded(FileTaskEntity task, boolean force) {
int batchSize = Math.max(1, properties.getCozeBatchSize());
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
String prompt = readAiPrompt(task);
while (force ? taskCacheService.pendingRowCount(task.getId()) > 0 : taskCacheService.pendingRowCount(task.getId()) >= batchSize) {
int drainSize = force ? batchSize : Math.max(1, batchSize);
List<AppearancePatentTaskCacheService.PendingRow> pendingRows = taskCacheService.drainPendingRows(task.getId(), drainSize);
@@ -437,27 +631,47 @@ public class AppearancePatentTaskService {
List<AppearancePatentResultRowDto> rawRows = pendingRows.stream()
.map(AppearancePatentTaskCacheService.PendingRow::row)
.toList();
List<AppearancePatentResultRowDto> cozeRows = cozeClient.inspect(rawRows, readAiPrompt(task));
List<AppearancePatentResultRowDto> cozeRows = cozeClient.inspect(rawRows, prompt);
Map<String, Map<String, AppearancePatentResultRowDto>> chunkRows = new LinkedHashMap<>();
for (int i = 0; i < pendingRows.size(); i++) {
AppearancePatentTaskCacheService.PendingRow pending = pendingRows.get(i);
AppearancePatentResultRowDto resultRow = i < cozeRows.size() ? cozeRows.get(i) : pending.row();
List<AppearancePatentResultRowDto> persistedRows = expandRows(task, List.of(resultRow));
updateChunkPayload(task.getId(), scopeHash, pending.chunkIndex(), persistedRows);
List<AppearancePatentResultRowDto> expandedRows = expandRows(List.of(resultRow), allRowsByBaseId);
String chunkKey = pending.scopeHash() + "::" + pending.chunkIndex();
Map<String, AppearancePatentResultRowDto> mergedRows =
chunkRows.computeIfAbsent(chunkKey, key -> new LinkedHashMap<>());
for (AppearancePatentResultRowDto expandedRow : expandedRows) {
mergedRows.put(rowKey(expandedRow.getId(), expandedRow.getAsin(), expandedRow.getCountry()), expandedRow);
}
}
for (Map.Entry<String, Map<String, AppearancePatentResultRowDto>> entry : chunkRows.entrySet()) {
int delimiter = entry.getKey().lastIndexOf("::");
if (delimiter <= 0) {
continue;
}
String scopeHash = entry.getKey().substring(0, delimiter);
Integer chunkIndex = Integer.parseInt(entry.getKey().substring(delimiter + 2));
mergeChunkPayload(task.getId(), scopeHash, chunkIndex, new ArrayList<>(entry.getValue().values()));
}
}
}
private void updateChunkPayload(Long taskId, String scopeHash, Integer chunkIndex, List<AppearancePatentResultRowDto> rows) {
private void mergeChunkPayload(Long taskId, String scopeHash, Integer chunkIndex, List<AppearancePatentResultRowDto> rows) {
TaskChunkEntity chunk = taskChunkMapper.selectOne(new LambdaQueryWrapper<TaskChunkEntity>()
.eq(TaskChunkEntity::getTaskId, taskId)
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
.eq(TaskChunkEntity::getScopeHash, scopeHash)
.eq(TaskChunkEntity::getChunkIndex, chunkIndex)
.last("limit 1"));
if (chunk == null) {
if (chunk == null || rows == null || rows.isEmpty()) {
return;
}
Map<String, AppearancePatentResultRowDto> persistedRows = readChunkRows(chunk);
for (AppearancePatentResultRowDto row : rows) {
persistedRows.put(rowKey(row.getId(), row.getAsin(), row.getCountry()), row);
}
String payloadJson = writeJson(rows == null ? List.of() : rows, "结果序列化失败");
payloadJson = writeJson(new ArrayList<>(persistedRows.values()), "appearance patent chunk payload merge failed");
String storedPayload = transientPayloadStorageService.storeChunkPayload(MODULE_TYPE, taskId, scopeHash, chunkIndex, payloadJson);
transientPayloadStorageService.deleteReplacedPayloadIfNeeded(chunk.getPayloadJson(), storedPayload);
chunk.setPayloadJson(storedPayload);
@@ -466,16 +680,15 @@ public class AppearancePatentTaskService {
taskChunkMapper.updateById(chunk);
}
private List<AppearancePatentResultRowDto> expandRows(FileTaskEntity task, List<AppearancePatentResultRowDto> representatives) {
private List<AppearancePatentResultRowDto> expandRows(List<AppearancePatentResultRowDto> representatives,
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId) {
if (representatives == null || representatives.isEmpty()) {
return List.of();
}
Map<String, List<AppearancePatentParsedRowVo>> allRowsByBaseId = loadAllRowsByBaseId(task);
List<AppearancePatentResultRowDto> result = new ArrayList<>();
Set<String> emittedKeys = new LinkedHashSet<>();
for (AppearancePatentResultRowDto representative : representatives) {
String baseId = baseId(representative.getId());
List<AppearancePatentParsedRowVo> siblings = allRowsByBaseId.get(baseId);
List<AppearancePatentParsedRowVo> siblings = resolveSiblingRows(representative, allRowsByBaseId);
if (siblings == null || siblings.isEmpty()) {
result.add(representative);
continue;
@@ -491,14 +704,40 @@ public class AppearancePatentTaskService {
return result;
}
private Map<String, List<AppearancePatentParsedRowVo>> loadAllRowsByBaseId(FileTaskEntity task) {
Map<String, List<AppearancePatentParsedRowVo>> result = new LinkedHashMap<>();
try {
for (AppearancePatentParsedRowVo row : readParsedPayload(task).getAllItems()) {
result.computeIfAbsent(baseId(row.getDisplayId()), key -> new ArrayList<>()).add(row);
private List<AppearancePatentParsedRowVo> resolveSiblingRows(AppearancePatentResultRowDto representative,
Map<String, List<AppearancePatentParsedRowVo>> groupedRows) {
if (representative == null || groupedRows == null || groupedRows.isEmpty()) {
return List.of();
}
String representativeKey = rowKey(representative.getId(), representative.getAsin(), representative.getCountry());
for (List<AppearancePatentParsedRowVo> rows : groupedRows.values()) {
for (AppearancePatentParsedRowVo row : rows) {
String rowKey = rowKey(row.getDisplayId(), row.getAsin(), row.getCountry());
if (Objects.equals(representativeKey, rowKey)) {
return rows;
}
}
}
return List.of();
}
private Map<String, List<AppearancePatentParsedRowVo>> loadAllRowsByBaseId(FileTaskEntity task) {
try {
return groupRowsByBaseId(readParsedPayload(task).getAllItems());
} catch (Exception ex) {
log.warn("[appearance-patent] read all rows failed taskId={} err={}", task.getId(), ex.getMessage());
return new LinkedHashMap<>();
}
}
private Map<String, List<AppearancePatentParsedRowVo>> groupRowsByBaseId(List<AppearancePatentParsedRowVo> rows) {
Map<String, List<AppearancePatentParsedRowVo>> result = new LinkedHashMap<>();
if (rows == null) {
return result;
}
for (AppearancePatentParsedRowVo row : rows) {
String key = firstNonBlank(normalize(row.getGroupKey()), baseId(row.getDisplayId()));
result.computeIfAbsent(key, ignored -> new ArrayList<>()).add(row);
}
return result;
}
@@ -517,8 +756,8 @@ public class AppearancePatentTaskService {
row.setId(sibling.getDisplayId());
row.setAsin(sibling.getAsin());
row.setCountry(sibling.getCountry());
row.setUrl(sibling.getUrl());
row.setTitle(sibling.getTitle());
row.setUrl(firstNonBlank(representative.getUrl(), sibling.getUrl()));
row.setTitle(firstNonBlank(representative.getTitle(), sibling.getTitle()));
row.setError(representative.getError());
row.setTitleRisk(representative.getTitleRisk());
row.setAppearanceRisk(representative.getAppearanceRisk());
@@ -644,22 +883,7 @@ public class AppearancePatentTaskService {
.eq(TaskChunkEntity::getModuleType, MODULE_TYPE)
.orderByAsc(TaskChunkEntity::getChunkIndex));
for (TaskChunkEntity chunk : chunks) {
if (chunk.getPayloadJson() == null || chunk.getPayloadJson().isBlank()) {
continue;
}
try {
String payloadJson = transientPayloadStorageService.resolvePayload(chunk.getPayloadJson(), "read appearance patent chunk failed");
JsonNode array = objectMapper.readTree(payloadJson);
if (!array.isArray()) {
continue;
}
for (JsonNode node : array) {
AppearancePatentResultRowDto row = objectMapper.treeToValue(node, AppearancePatentResultRowDto.class);
result.put(rowKey(row.getId(), row.getAsin(), row.getCountry()), row);
}
} catch (Exception ex) {
log.warn("[appearance-patent] read chunk payload failed taskId={} chunk={} err={}", taskId, chunk.getChunkIndex(), ex.getMessage());
}
result.putAll(readChunkRows(chunk));
}
return result;
}
@@ -672,26 +896,35 @@ public class AppearancePatentTaskService {
font.setBold(true);
headerStyle.setFont(font);
List<String> resultHeaders = new ArrayList<>(RESULT_HEADERS);
resultHeaders.add(4, "title");
resultHeaders.add(5, "url");
Row header = sheet.createRow(0);
for (int i = 0; i < RESULT_HEADERS.size(); i++) {
for (int i = 0; i < resultHeaders.size(); i++) {
Cell cell = header.createCell(i);
cell.setCellValue(RESULT_HEADERS.get(i));
cell.setCellValue(resultHeaders.get(i));
cell.setCellStyle(headerStyle);
}
int rowIndex = 1;
for (AppearancePatentParsedRowVo parsedRow : parsed.getAllItems()) {
AppearancePatentResultRowDto resultRow = resultMap.get(rowKey(parsedRow.getDisplayId(), parsedRow.getAsin(), parsedRow.getCountry()));
String missingReason = "";
if (resultRow == null) {
missingReason = hasPromptFields(parsedRow) ? "未匹配到检测结果" : "未送检:缺少标题或图片";
}
Row row = sheet.createRow(rowIndex++);
int col = 0;
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getDisplayId(), parsedRow.getSourceId()));
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getAsin(), ""));
row.createCell(col++).setCellValue(firstNonBlank(parsedRow.getCountry(), ""));
row.createCell(col++).setCellValue(readValueByHeader(parsedRow, "价格", "price"));
row.createCell(col++).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getTitleRisk(), ""));
row.createCell(col++).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getAppearanceRisk(), ""));
row.createCell(col++).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getPatentRisk(), ""));
row.createCell(col).setCellValue(resultRow == null ? "" : firstNonBlank(resultRow.getConclusion(), ""));
row.createCell(col++).setCellValue(resultRow == null ? firstNonBlank(parsedRow.getTitle(), "") : firstNonBlank(resultRow.getTitle(), parsedRow.getTitle()));
row.createCell(col++).setCellValue(resultRow == null ? firstNonBlank(parsedRow.getUrl(), "") : firstNonBlank(resultRow.getUrl(), parsedRow.getUrl()));
row.createCell(col++).setCellValue(resultRow == null ? missingReason : firstNonBlank(resultRow.getTitleRisk(), ""));
row.createCell(col++).setCellValue(resultRow == null ? missingReason : firstNonBlank(resultRow.getAppearanceRisk(), ""));
row.createCell(col++).setCellValue(resultRow == null ? missingReason : firstNonBlank(resultRow.getPatentRisk(), ""));
row.createCell(col).setCellValue(resultRow == null ? "未送检" : firstNonBlank(resultRow.getConclusion(), ""));
}
workbook.write(fos);
workbook.dispose();
@@ -713,13 +946,18 @@ public class AppearancePatentTaskService {
int idCol = findRequiredHeader(headerMap, "id");
int asinCol = findRequiredHeader(headerMap, "asin");
int countryCol = findRequiredHeader(headerMap, "国家", "country");
int urlCol = findOptionalHeader(headerMap, "url", "rul", "链接", "商品链接");
int titleCol = findOptionalHeader(headerMap, "标题", "title", "商品标题");
int urlCol = findOptionalHeaderExact(headerMap,
"url", "rul", "link", "image", "img", "pic", "picture",
"链接", "商品链接", "图片", "商品图片", "主图", "商品主图", "图片链接", "主图链接");
int titleCol = findOptionalHeaderExact(headerMap,
"标题", "title", "listing title", "product title", "商品标题", "商品名称", "产品名称");
List<AppearancePatentParsedRowVo> accepted = new ArrayList<>();
List<AppearancePatentParsedRowVo> allRows = new ArrayList<>();
int total = 0;
int dropped = 0;
String currentBlockBaseId = "";
String currentGroupKey = "";
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (row == null) {
@@ -740,6 +978,12 @@ public class AppearancePatentTaskService {
vo.setRowIndex(i + 1);
vo.setSourceId(id);
vo.setDisplayId(normalizeDisplayId(id));
String rowBaseId = baseId(vo.getDisplayId());
if (!Objects.equals(currentBlockBaseId, rowBaseId)) {
currentBlockBaseId = rowBaseId;
currentGroupKey = rowBaseId + "@" + vo.getRowIndex();
}
vo.setGroupKey(currentGroupKey);
vo.setAsin(asin);
vo.setCountry(country);
vo.setUrl(urlCol >= 0 ? cell(row, urlCol, formatter) : "");
@@ -752,6 +996,10 @@ public class AppearancePatentTaskService {
dropped++;
}
}
hydratePromptFields(allRows);
if (accepted.isEmpty()) {
throw new BusinessException("no valid appearance patent rows");
}
return new ParsedWorkbook(total, dropped, headers, accepted, allRows);
} catch (BusinessException ex) {
throw ex;
@@ -761,6 +1009,33 @@ public class AppearancePatentTaskService {
}
}
private void hydratePromptFields(List<AppearancePatentParsedRowVo> rows) {
if (rows == null || rows.isEmpty()) {
return;
}
Map<String, List<AppearancePatentParsedRowVo>> rowsByBaseId = groupRowsByBaseId(rows);
for (List<AppearancePatentParsedRowVo> siblings : rowsByBaseId.values()) {
String title = "";
String url = "";
for (AppearancePatentParsedRowVo sibling : siblings) {
title = firstNonBlank(title, sibling.getTitle());
url = firstNonBlank(url, sibling.getUrl());
}
for (AppearancePatentParsedRowVo sibling : siblings) {
if (normalize(sibling.getTitle()).isBlank()) {
sibling.setTitle(title);
}
if (normalize(sibling.getUrl()).isBlank()) {
sibling.setUrl(url);
}
}
}
}
private boolean hasPromptFields(AppearancePatentParsedRowVo row) {
return row != null && (!normalize(row.getTitle()).isBlank() || !normalize(row.getUrl()).isBlank());
}
private Map<String, Integer> buildHeaderMap(Row header, DataFormatter formatter) {
Map<String, Integer> map = new LinkedHashMap<>();
for (int i = 0; i < header.getLastCellNum(); i++) {
@@ -808,6 +1083,23 @@ public class AppearancePatentTaskService {
return -1;
}
private int findOptionalHeaderExact(Map<String, Integer> map, String... names) {
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String normalizedHeader = normalizeHeaderAlias(entry.getKey());
for (String name : names) {
if (normalizedHeader.equals(normalizeHeaderAlias(name))) {
return entry.getValue();
}
}
}
return -1;
}
private String normalizeHeaderAlias(String value) {
String normalized = normalize(value).toLowerCase(Locale.ROOT);
return normalized.replaceAll("[\\s_\\-()\\[\\]{}:/\\\\]+", "");
}
private String cell(Row row, int col, DataFormatter formatter) {
return col < 0 ? "" : normalize(formatter.formatCellValue(row.getCell(col)));
}
@@ -933,6 +1225,28 @@ public class AppearancePatentTaskService {
return new AppearancePatentParsedPayloadDto();
}
private Map<String, AppearancePatentResultRowDto> readChunkRows(TaskChunkEntity chunk) {
Map<String, AppearancePatentResultRowDto> rows = new LinkedHashMap<>();
if (chunk == null || chunk.getPayloadJson() == null || chunk.getPayloadJson().isBlank()) {
return rows;
}
try {
String payloadJson = transientPayloadStorageService.resolvePayload(chunk.getPayloadJson(), "read appearance patent chunk failed");
JsonNode array = objectMapper.readTree(payloadJson);
if (!array.isArray()) {
return rows;
}
for (JsonNode node : array) {
AppearancePatentResultRowDto row = objectMapper.treeToValue(node, AppearancePatentResultRowDto.class);
rows.put(rowKey(row.getId(), row.getAsin(), row.getCountry()), row);
}
} catch (Exception ex) {
log.warn("[appearance-patent] read chunk payload failed taskId={} chunk={} err={}",
chunk.getTaskId(), chunk.getChunkIndex(), ex.getMessage());
}
return rows;
}
private String rowKey(String id, String asin, String country) {
return normalize(id) + "::" + normalize(asin).toUpperCase(Locale.ROOT) + "::" + normalize(country);
}
@@ -996,6 +1310,13 @@ public class AppearancePatentTaskService {
}
}
private record SubmitContext(FileTaskEntity task,
String scopeKey,
String scopeHash,
boolean forceFlush,
String error) {
}
private record ParsedWorkbook(int totalRows, int droppedRows, List<String> headers, List<AppearancePatentParsedRowVo> acceptedRows, List<AppearancePatentParsedRowVo> allRows) {
}
}

View File

@@ -27,7 +27,6 @@ import java.nio.file.attribute.FileTime;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
import java.util.Map;
@@ -47,6 +46,7 @@ public class DeleteBrandStaleTaskService {
private final FileTaskMapper fileTaskMapper;
private final DeleteBrandTaskCacheService deleteBrandTaskCacheService;
private final DeleteBrandTaskStorageService deleteBrandTaskStorageService;
private final DeleteBrandRunService deleteBrandRunService;
private final ProductRiskTaskService productRiskTaskService;
private final ProductRiskTaskCacheService productRiskTaskCacheService;
@@ -110,7 +110,13 @@ public class DeleteBrandStaleTaskService {
}
private void failStaleDeleteBrandTasks() {
LocalDateTime threshold = LocalDateTime.now().minusMinutes(deleteBrandProgressProperties.getHeartbeatTimeoutMinutes());
long minutes = Math.max(1L, deleteBrandProgressProperties.getHeartbeatTimeoutMinutes());
long initialMinutes = Math.max(minutes, deleteBrandProgressProperties.getDeleteBrandInitialTimeoutMinutes());
long staleTimeoutMillis = Duration.ofMinutes(minutes).toMillis();
long nowMillis = System.currentTimeMillis();
LocalDateTime now = LocalDateTime.now();
LocalDateTime threshold = now.minusMinutes(minutes);
LocalDateTime initialThreshold = now.minusMinutes(initialMinutes);
java.util.List<FileTaskEntity> runningTasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
.eq(FileTaskEntity::getModuleType, MODULE_TYPE_DELETE_BRAND)
@@ -120,36 +126,35 @@ public class DeleteBrandStaleTaskService {
for (FileTaskEntity task : runningTasks) {
Map<Object, Object> progress = deleteBrandTaskCacheService.getProgress(task.getId());
boolean hasProgress = progress != null && !progress.isEmpty() && progress.containsKey("last_heartbeat_at");
boolean isActive = false;
if (progress != null && progress.containsKey("has_progress")) {
Object hpObj = progress.get("has_progress");
if (hpObj instanceof Boolean) {
isActive = (Boolean) hpObj;
} else if (hpObj instanceof String) {
isActive = Boolean.parseBoolean((String) hpObj);
}
}
long lastHeartbeatAt = 0L;
if (hasProgress) {
if (progress != null && !progress.isEmpty() && progress.containsKey("last_heartbeat_at")) {
try {
lastHeartbeatAt = Long.parseLong(String.valueOf(progress.get("last_heartbeat_at")));
} catch (Exception ignored) {
}
}
boolean hasStartedProgress = lastHeartbeatAt > 0L;
if (!hasStartedProgress) {
hasStartedProgress = deleteBrandTaskStorageService.countCompletedScopes(task.getId()) > 0
|| deleteBrandTaskStorageService.countParsedPayloadScopes(task.getId()) > 0;
}
if (lastHeartbeatAt > 0 && isActive) {
LocalDateTime lastActivityTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastHeartbeatAt), ZoneId.systemDefault());
if (lastActivityTime.isAfter(threshold)) {
if (lastHeartbeatAt > 0L && nowMillis - lastHeartbeatAt < staleTimeoutMillis) {
continue;
}
} else {
LocalDateTime queuedThreshold = LocalDateTime.now().minusHours(12);
if (task.getCreatedAt() != null && task.getCreatedAt().isAfter(queuedThreshold)) {
if (!hasStartedProgress && task.getCreatedAt() != null && task.getCreatedAt().isAfter(initialThreshold)) {
continue;
}
try {
deleteBrandRunService.tryFinalizeTask(task.getId(), true);
FileTaskEntity refreshed = fileTaskMapper.selectById(task.getId());
if (refreshed != null && !"RUNNING".equals(refreshed.getStatus())) {
deleteBrandTaskCacheService.saveTaskCache(refreshed);
continue;
}
} catch (Exception ex) {
log.warn("[stale-check] delete-brand finalize threw taskId={} msg={}", task.getId(), ex.getMessage());
}
int updated = fileTaskMapper.update(null, new LambdaUpdateWrapper<FileTaskEntity>()

View File

@@ -49,6 +49,9 @@ public class PatrolDeleteTaskService {
private static final String MODULE_TYPE = "PATROL_DELETE";
private static final String CONTENT_TYPE_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
private static final int RESULT_PENDING = -1;
private static final int RESULT_FAILED = 0;
private static final int RESULT_SUCCESS = 1;
private static final String INTERRUPTED_MESSAGE = "Python 在该店铺结果提交完成前中断";
private final FileTaskMapper fileTaskMapper;
@@ -264,12 +267,13 @@ public class PatrolDeleteTaskService {
result.setSourceFilename(normalizedShopName);
result.setSourceFileUrl(item.getShopId());
result.setUserId(request.getUserId());
result.setSuccess(null);
result.setSuccess(RESULT_PENDING);
result.setCreatedAt(now);
fileResultMapper.insert(result);
snapshots.add(toSnapshotVo(result, item, task.getStatus(), null));
}
persistTaskJson(task, uniqueItems, snapshots);
taskCacheService.saveTaskCache(task);
PatrolDeleteCreateTaskVo vo = new PatrolDeleteCreateTaskVo();
vo.setTaskId(task.getId());
@@ -326,6 +330,7 @@ public class PatrolDeleteTaskService {
persistSnapshotJson(task, snapshots);
fileTaskMapper.updateById(task);
taskCacheService.saveTaskCache(task);
tryFinalizeTask(taskId, false);
}
@@ -402,6 +407,7 @@ public class PatrolDeleteTaskService {
persistSnapshotJson(task, snapshots);
fileTaskMapper.updateById(task);
taskCacheService.saveTaskCache(task);
return changed;
}
@@ -448,6 +454,7 @@ public class PatrolDeleteTaskService {
updateTaskStatusFromRows(task, rows);
persistSnapshotJson(task, buildSnapshotFromDb(task, rows));
fileTaskMapper.updateById(task);
taskCacheService.saveTaskCache(task);
}
private long countTasks(Long userId, List<String> statuses) {
@@ -502,9 +509,9 @@ public class PatrolDeleteTaskService {
private Map<Long, Map<Long, PatrolDeleteResultItemVo>> buildSnapshotMap(Map<Long, FileTaskEntity> taskMap) {
Map<Long, Map<Long, PatrolDeleteResultItemVo>> out = new LinkedHashMap<>();
for (Map.Entry<Long, FileTaskEntity> entry : taskMap.entrySet()) {
List<PatrolDeleteResultItemVo> snapshots = taskResultItemService.listResultSnapshots(entry.getKey(), MODULE_TYPE, PatrolDeleteResultItemVo.class);
List<PatrolDeleteResultItemVo> snapshots = parseTaskSnapshots(entry.getValue().getResultJson());
if (snapshots.isEmpty()) {
snapshots = parseTaskSnapshots(entry.getValue().getResultJson());
snapshots = taskResultItemService.listResultSnapshots(entry.getKey(), MODULE_TYPE, PatrolDeleteResultItemVo.class);
}
out.put(entry.getKey(), indexSnapshotByResultId(snapshots));
}
@@ -518,7 +525,7 @@ public class PatrolDeleteTaskService {
item.setShopName(firstNonBlank(item.getShopName(), entity.getSourceFilename()));
item.setShopId(firstNonBlank(item.getShopId(), entity.getSourceFileUrl()));
item.setTaskStatus(task != null ? task.getStatus() : item.getTaskStatus());
item.setSuccess(entity.getSuccess() == null ? item.getSuccess() : entity.getSuccess() == 1);
item.setSuccess(toSuccessFlag(entity.getSuccess(), item.getSuccess()));
item.setError(!blank(entity.getErrorMessage()) ? entity.getErrorMessage() : item.getError());
item.setCreatedAt(entity.getCreatedAt());
item.setFinishedAt(task != null ? task.getFinishedAt() : item.getFinishedAt());
@@ -575,7 +582,7 @@ public class PatrolDeleteTaskService {
vo.setMatchStatus(item.getMatchStatus());
vo.setMatchMessage(item.getMatchMessage());
vo.setTaskStatus(taskStatus);
vo.setSuccess(result.getSuccess() == null ? null : result.getSuccess() == 1);
vo.setSuccess(toSuccessFlag(result.getSuccess(), null));
vo.setError(result.getErrorMessage());
vo.setCreatedAt(result.getCreatedAt());
vo.setFinishedAt(finishedAt);
@@ -616,7 +623,7 @@ public class PatrolDeleteTaskService {
item.setShopName(row.getSourceFilename());
item.setShopId(row.getSourceFileUrl());
item.setTaskStatus(task == null ? null : task.getStatus());
item.setSuccess(row.getSuccess() == null ? null : row.getSuccess() == 1);
item.setSuccess(toSuccessFlag(row.getSuccess(), null));
item.setError(row.getErrorMessage());
item.setCreatedAt(row.getCreatedAt());
item.setFinishedAt(task == null ? null : task.getFinishedAt());
@@ -642,8 +649,8 @@ public class PatrolDeleteTaskService {
}
private void updateTaskStatusFromRows(FileTaskEntity task, List<FileResultEntity> rows) {
long successCount = rows.stream().filter(row -> Integer.valueOf(1).equals(row.getSuccess())).count();
long failedCount = rows.stream().filter(row -> Integer.valueOf(0).equals(row.getSuccess())).count();
long successCount = rows.stream().filter(row -> Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess())).count();
long failedCount = rows.stream().filter(row -> Integer.valueOf(RESULT_FAILED).equals(row.getSuccess())).count();
long pendingCount = rows.stream().filter(row -> !isResultFinished(row)).count();
task.setSuccessFileCount((int) successCount);
task.setFailedFileCount((int) failedCount);
@@ -796,11 +803,11 @@ public class PatrolDeleteTaskService {
.filter(item -> Boolean.TRUE.equals(item.getSuccess()))
.toList();
if (!successItems.isEmpty()) {
String filename = safeFileStem("patrol-delete-" + task.getId()) + ".xlsx";
String filename = buildTaskWorkbookFilename(task);
int rowCount = excelAssemblyService.countRows(successItems);
FileResultEntity firstSuccessRow = null;
for (FileResultEntity row : rows) {
if (Integer.valueOf(1).equals(row.getSuccess())) {
if (Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess())) {
row.setResultFilename(filename);
row.setResultFileUrl(null);
row.setResultFileSize(0L);
@@ -843,7 +850,7 @@ public class PatrolDeleteTaskService {
throw new BusinessException("没有可生成的巡店删除结果");
}
File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "patrol-delete-result", String.valueOf(task.getId())));
String filename = safeFileStem("patrol-delete-" + task.getId()) + ".xlsx";
String filename = buildTaskWorkbookFilename(task);
File xlsx = FileUtil.file(workRoot, filename);
try {
excelAssemblyService.writeWorkbook(xlsx, successItems);
@@ -851,7 +858,7 @@ public class PatrolDeleteTaskService {
long fileSize = xlsx.length();
int rowCount = excelAssemblyService.countRows(successItems);
for (FileResultEntity row : rows) {
if (Integer.valueOf(1).equals(row.getSuccess())) {
if (Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess())) {
row.setResultFilename(filename);
row.setResultFileUrl(objectKey);
row.setResultFileSize(fileSize);
@@ -866,13 +873,13 @@ public class PatrolDeleteTaskService {
}
private void markResultSuccess(FileResultEntity row) {
row.setSuccess(1);
row.setSuccess(RESULT_SUCCESS);
row.setErrorMessage(null);
fileResultMapper.updateById(row);
}
private void markResultFailed(FileResultEntity row, String message) {
row.setSuccess(0);
row.setSuccess(RESULT_FAILED);
row.setErrorMessage(blankToNull(message));
row.setResultFilename(null);
row.setResultFileUrl(null);
@@ -883,7 +890,14 @@ public class PatrolDeleteTaskService {
}
private boolean isResultFinished(FileResultEntity row) {
return Integer.valueOf(1).equals(row.getSuccess()) || Integer.valueOf(0).equals(row.getSuccess());
return Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess()) || Integer.valueOf(RESULT_FAILED).equals(row.getSuccess());
}
private Boolean toSuccessFlag(Integer dbValue, Boolean fallback) {
if (dbValue == null || Integer.valueOf(RESULT_PENDING).equals(dbValue)) {
return fallback;
}
return Integer.valueOf(RESULT_SUCCESS).equals(dbValue);
}
private List<PatrolDeleteResultItemVo> parseTaskSnapshots(String json) {
@@ -972,6 +986,11 @@ public class PatrolDeleteTaskService {
return !blank(first) ? first : second;
}
private String buildTaskWorkbookFilename(FileTaskEntity task) {
Long taskId = task == null ? null : task.getId();
return safeFileStem("巡店删除-" + (taskId == null ? "result" : taskId)) + ".xlsx";
}
private String safeFileStem(String value) {
String raw = value == null ? "result" : value.trim();
String safe = raw.replaceAll("[\\\\/:*?\"<>|]+", "_");

View File

@@ -49,6 +49,9 @@ public class QueryAsinTaskService {
private static final String MODULE_TYPE = "QUERY_ASIN";
private static final String CONTENT_TYPE_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
private static final int RESULT_PENDING = -1;
private static final int RESULT_FAILED = 0;
private static final int RESULT_SUCCESS = 1;
private static final String INTERRUPTED_MESSAGE = "Python 在该店铺结果提交完成前中断";
private final FileTaskMapper fileTaskMapper;
@@ -270,12 +273,13 @@ public class QueryAsinTaskService {
result.setSourceFilename(normalizedShopName);
result.setSourceFileUrl(item.getShopId());
result.setUserId(request.getUserId());
result.setSuccess(null);
result.setSuccess(RESULT_PENDING);
result.setCreatedAt(now);
fileResultMapper.insert(result);
snapshots.add(toSnapshotVo(result, item, task.getStatus(), null));
}
persistTaskJson(task, uniqueItems, snapshots);
taskCacheService.saveTaskCache(task);
QueryAsinCreateTaskVo vo = new QueryAsinCreateTaskVo();
vo.setTaskId(task.getId());
@@ -332,6 +336,7 @@ public class QueryAsinTaskService {
persistSnapshotJson(task, snapshots);
fileTaskMapper.updateById(task);
taskCacheService.saveTaskCache(task);
tryFinalizeTask(taskId, false);
}
@@ -408,6 +413,7 @@ public class QueryAsinTaskService {
persistSnapshotJson(task, snapshots);
fileTaskMapper.updateById(task);
taskCacheService.saveTaskCache(task);
return changed;
}
@@ -454,6 +460,7 @@ public class QueryAsinTaskService {
updateTaskStatusFromRows(task, rows);
persistSnapshotJson(task, buildSnapshotFromDb(task, rows));
fileTaskMapper.updateById(task);
taskCacheService.saveTaskCache(task);
}
private long countTasks(Long userId, List<String> statuses) {
@@ -508,9 +515,9 @@ public class QueryAsinTaskService {
private Map<Long, Map<Long, QueryAsinResultItemVo>> buildSnapshotMap(Map<Long, FileTaskEntity> taskMap) {
Map<Long, Map<Long, QueryAsinResultItemVo>> out = new LinkedHashMap<>();
for (Map.Entry<Long, FileTaskEntity> entry : taskMap.entrySet()) {
List<QueryAsinResultItemVo> snapshots = taskResultItemService.listResultSnapshots(entry.getKey(), MODULE_TYPE, QueryAsinResultItemVo.class);
List<QueryAsinResultItemVo> snapshots = parseTaskSnapshots(entry.getValue().getResultJson());
if (snapshots.isEmpty()) {
snapshots = parseTaskSnapshots(entry.getValue().getResultJson());
snapshots = taskResultItemService.listResultSnapshots(entry.getKey(), MODULE_TYPE, QueryAsinResultItemVo.class);
}
out.put(entry.getKey(), indexSnapshotByResultId(snapshots));
}
@@ -524,7 +531,7 @@ public class QueryAsinTaskService {
item.setShopName(firstNonBlank(item.getShopName(), entity.getSourceFilename()));
item.setShopId(firstNonBlank(item.getShopId(), entity.getSourceFileUrl()));
item.setTaskStatus(task != null ? task.getStatus() : item.getTaskStatus());
item.setSuccess(entity.getSuccess() == null ? item.getSuccess() : entity.getSuccess() == 1);
item.setSuccess(toSuccessFlag(entity.getSuccess(), item.getSuccess()));
item.setError(!blank(entity.getErrorMessage()) ? entity.getErrorMessage() : item.getError());
item.setCreatedAt(entity.getCreatedAt());
item.setFinishedAt(task != null ? task.getFinishedAt() : item.getFinishedAt());
@@ -581,7 +588,7 @@ public class QueryAsinTaskService {
vo.setMatchStatus(item.getMatchStatus());
vo.setMatchMessage(item.getMatchMessage());
vo.setTaskStatus(taskStatus);
vo.setSuccess(result.getSuccess() == null ? null : result.getSuccess() == 1);
vo.setSuccess(toSuccessFlag(result.getSuccess(), null));
vo.setError(result.getErrorMessage());
vo.setCreatedAt(result.getCreatedAt());
vo.setFinishedAt(finishedAt);
@@ -622,7 +629,7 @@ public class QueryAsinTaskService {
item.setShopName(row.getSourceFilename());
item.setShopId(row.getSourceFileUrl());
item.setTaskStatus(task == null ? null : task.getStatus());
item.setSuccess(row.getSuccess() == null ? null : row.getSuccess() == 1);
item.setSuccess(toSuccessFlag(row.getSuccess(), null));
item.setError(row.getErrorMessage());
item.setCreatedAt(row.getCreatedAt());
item.setFinishedAt(task == null ? null : task.getFinishedAt());
@@ -648,8 +655,8 @@ public class QueryAsinTaskService {
}
private void updateTaskStatusFromRows(FileTaskEntity task, List<FileResultEntity> rows) {
long successCount = rows.stream().filter(row -> Integer.valueOf(1).equals(row.getSuccess())).count();
long failedCount = rows.stream().filter(row -> Integer.valueOf(0).equals(row.getSuccess())).count();
long successCount = rows.stream().filter(row -> Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess())).count();
long failedCount = rows.stream().filter(row -> Integer.valueOf(RESULT_FAILED).equals(row.getSuccess())).count();
long pendingCount = rows.stream().filter(row -> !isResultFinished(row)).count();
task.setSuccessFileCount((int) successCount);
task.setFailedFileCount((int) failedCount);
@@ -816,11 +823,11 @@ public class QueryAsinTaskService {
.filter(item -> Boolean.TRUE.equals(item.getSuccess()))
.toList();
if (!successItems.isEmpty()) {
String filename = safeFileStem("query-asin-" + task.getId()) + ".xlsx";
String filename = buildTaskWorkbookFilename(task);
int rowCount = excelAssemblyService.countRows(successItems);
FileResultEntity firstSuccessRow = null;
for (FileResultEntity row : rows) {
if (Integer.valueOf(1).equals(row.getSuccess())) {
if (Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess())) {
row.setResultFilename(filename);
row.setResultFileUrl(null);
row.setResultFileSize(0L);
@@ -863,7 +870,7 @@ public class QueryAsinTaskService {
throw new BusinessException("没有可生成的查询ASIN结果");
}
File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "query-asin-result", String.valueOf(task.getId())));
String filename = safeFileStem("query-asin-" + task.getId()) + ".xlsx";
String filename = buildTaskWorkbookFilename(task);
File xlsx = FileUtil.file(workRoot, filename);
try {
excelAssemblyService.writeWorkbook(xlsx, successItems);
@@ -871,7 +878,7 @@ public class QueryAsinTaskService {
long fileSize = xlsx.length();
int rowCount = excelAssemblyService.countRows(successItems);
for (FileResultEntity row : rows) {
if (Integer.valueOf(1).equals(row.getSuccess())) {
if (Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess())) {
row.setResultFilename(filename);
row.setResultFileUrl(objectKey);
row.setResultFileSize(fileSize);
@@ -886,13 +893,13 @@ public class QueryAsinTaskService {
}
private void markResultSuccess(FileResultEntity row) {
row.setSuccess(1);
row.setSuccess(RESULT_SUCCESS);
row.setErrorMessage(null);
fileResultMapper.updateById(row);
}
private void markResultFailed(FileResultEntity row, String message) {
row.setSuccess(0);
row.setSuccess(RESULT_FAILED);
row.setErrorMessage(blankToNull(message));
row.setResultFilename(null);
row.setResultFileUrl(null);
@@ -903,7 +910,14 @@ public class QueryAsinTaskService {
}
private boolean isResultFinished(FileResultEntity row) {
return Integer.valueOf(1).equals(row.getSuccess()) || Integer.valueOf(0).equals(row.getSuccess());
return Integer.valueOf(RESULT_SUCCESS).equals(row.getSuccess()) || Integer.valueOf(RESULT_FAILED).equals(row.getSuccess());
}
private Boolean toSuccessFlag(Integer dbValue, Boolean fallback) {
if (dbValue == null || Integer.valueOf(RESULT_PENDING).equals(dbValue)) {
return fallback;
}
return Integer.valueOf(RESULT_SUCCESS).equals(dbValue);
}
private List<QueryAsinResultItemVo> parseTaskSnapshots(String json) {
@@ -1034,6 +1048,11 @@ public class QueryAsinTaskService {
return asin == null ? "" : asin.trim().toUpperCase();
}
private String buildTaskWorkbookFilename(FileTaskEntity task) {
Long taskId = task == null ? null : task.getId();
return safeFileStem("查询ASIN-" + (taskId == null ? "result" : taskId)) + ".xlsx";
}
private String safeFileStem(String value) {
String raw = value == null ? "result" : value.trim();
String safe = raw.replaceAll("[\\\\/:*?\"<>|]+", "_");

View File

@@ -15,6 +15,7 @@ import com.nanri.aiimage.modules.shopkey.model.vo.QueryAsinItemVo;
import com.nanri.aiimage.modules.shopkey.model.vo.QueryAsinPageVo;
import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageGroupItemVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
@@ -30,9 +31,11 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
@Service
@RequiredArgsConstructor
@Slf4j
public class QueryAsinService {
private static final List<String> SUPPORTED_COUNTRIES = List.of("DE", "UK", "FR", "IT", "ES");
@@ -364,13 +367,32 @@ public class QueryAsinService {
}
private String normalizeCountry(String country) {
String normalized = normalizeBlank(country).toUpperCase(Locale.ROOT);
String normalized = normalizeCountryAlias(country);
if (!SUPPORTED_COUNTRIES.contains(normalized)) {
throw new BusinessException("国家参数不支持: " + country);
}
return normalized;
}
private String normalizeCountryAlias(String country) {
String normalized = normalizeBlank(country);
if (normalized.isEmpty()) {
return "";
}
String upper = normalized.toUpperCase(Locale.ROOT);
if (SUPPORTED_COUNTRIES.contains(upper)) {
return upper;
}
return switch (normalizeHeaderKey(normalized)) {
case "\u5fb7\u56fd", "\u5fb7", "de", "germany", "german", "deutschland" -> "DE";
case "\u82f1\u56fd", "\u82f1", "uk", "gb", "greatbritain", "britain", "unitedkingdom" -> "UK";
case "\u6cd5\u56fd", "\u6cd5", "fr", "france", "french" -> "FR";
case "\u610f\u5927\u5229", "\u610f", "it", "italy", "italian" -> "IT";
case "\u897f\u73ed\u7259", "\u897f", "es", "spain", "spanish", "espana" -> "ES";
default -> "";
};
}
private String normalizeAsin(String asin) {
String normalized = normalizeRequired(asin, "ASIN 不能为空").toUpperCase(Locale.ROOT);
if (normalized.length() > 64) {
@@ -505,13 +527,19 @@ public class QueryAsinService {
}
}
headerLoaded = true;
if (findGroupIndex() == null && (fallbackGroupId == null || fallbackGroupId <= 0)) {
Integer resolvedGroupIndex = resolveGroupHeaderIndex();
Integer resolvedShopIndex = resolveShopHeaderIndex();
boolean hasWideCountries = hasWideCountryColumns();
boolean hasSingleCountryAsin = hasSingleCountryAsinColumns();
log.info("[query-asin-import] headerMap={} normalizedKeys={} groupIndex={} shopIndex={} hasWideCountries={} hasSingleCountryAsin={}",
headerMap, headerIndexByKey.keySet(), resolvedGroupIndex, resolvedShopIndex, hasWideCountries, hasSingleCountryAsin);
if (resolvedGroupIndex == null && (fallbackGroupId == null || fallbackGroupId <= 0)) {
throw new BusinessException("Excel 缺少分组列,请在页面选择分组或提供分组列");
}
if (findShopIndex() == null) {
if (resolvedShopIndex == null) {
throw new BusinessException("Excel 缺少店铺名列");
}
if (SUPPORTED_COUNTRIES.stream().noneMatch(country -> findCountryIndex(country) != null)) {
if (!hasWideCountries && !hasSingleCountryAsin) {
throw new BusinessException("Excel 至少需要包含一个国家 ASIN 列");
}
}
@@ -524,7 +552,7 @@ public class QueryAsinService {
progress.setProcessedRows(rowIndex);
Long groupId = resolveGroupId(rowMap);
String shopName = cell(rowMap, findShopIndex());
String shopName = cell(rowMap, resolveShopHeaderIndex());
if (groupId == null || shopName.isEmpty()) {
skippedCount++;
updateProgress();
@@ -532,27 +560,8 @@ public class QueryAsinService {
}
QueryAsinEntity row = newImportEntity(groupId, shopName);
int rowAsinCount = 0;
int rowAcceptedCount = 0;
for (String country : SUPPORTED_COUNTRIES) {
Integer index = findCountryIndex(country);
if (index == null) {
continue;
}
String asin = normalizeBlank(cell(rowMap, index)).toUpperCase(Locale.ROOT);
if (asin.isEmpty()) {
continue;
}
if (asin.length() > 64) {
skippedCount++;
continue;
}
asinCount++;
rowAsinCount++;
setCountryAsin(row, country, asin);
rowAcceptedCount++;
}
if (rowAsinCount == 0) {
int rowAcceptedCount = collectRowCountryAsins(row, rowMap);
if (countCountryCells(row) == 0) {
skippedCount++;
}
if (rowAcceptedCount > 0) {
@@ -754,7 +763,7 @@ public class QueryAsinService {
}
private Long resolveGroupId(Map<Integer, String> rowMap) {
Integer groupIndex = findGroupIndex();
Integer groupIndex = resolveGroupHeaderIndex();
String groupText = cell(rowMap, groupIndex);
if (groupText.isEmpty()) {
return fallbackGroupId != null && fallbackGroupId > 0 ? fallbackGroupId : null;
@@ -767,6 +776,51 @@ public class QueryAsinService {
return id;
}
private int collectRowCountryAsins(QueryAsinEntity row, Map<Integer, String> rowMap) {
int acceptedCount = 0;
for (String country : SUPPORTED_COUNTRIES) {
Integer index = resolveCountryHeaderIndex(country);
if (index == null) {
continue;
}
acceptedCount += acceptCountryAsin(row, country, cell(rowMap, index));
}
Integer singleCountryIndex = findSingleCountryIndex();
Integer singleAsinIndex = findSingleAsinIndex();
if (singleCountryIndex != null && singleAsinIndex != null) {
acceptedCount += acceptCountryAsin(
row,
normalizeCountryAlias(cell(rowMap, singleCountryIndex)),
cell(rowMap, singleAsinIndex));
}
return acceptedCount;
}
private int acceptCountryAsin(QueryAsinEntity row, String country, String asin) {
String normalizedAsin = normalizeBlank(asin).toUpperCase(Locale.ROOT);
if (normalizedAsin.isEmpty()) {
return 0;
}
asinCount++;
if (country == null || country.isEmpty() || !SUPPORTED_COUNTRIES.contains(country)) {
skippedCount++;
return 0;
}
if (normalizedAsin.length() > 64) {
skippedCount++;
return 0;
}
String existing = getCountryAsin(row, country);
if (!existing.isEmpty()) {
if (!existing.equals(normalizedAsin)) {
skippedCount++;
}
return 0;
}
setCountryAsin(row, country, normalizedAsin);
return 1;
}
private void loadGroups() {
if (groupsLoaded) {
return;
@@ -801,6 +855,88 @@ public class QueryAsinService {
};
}
private Integer resolveGroupHeaderIndex() {
Integer index = findGroupIndex();
if (index != null) {
return index;
}
index = firstHeader("groupname", "\u5206\u7ec4id", "\u5206\u7ec4");
if (index != null) {
return index;
}
return findHeaderByPredicate(key -> key.contains("\u5206\u7ec4") || key.contains("group"));
}
private Integer resolveShopHeaderIndex() {
Integer index = findShopIndex();
if (index != null) {
return index;
}
index = firstHeader(
"\u5e97\u94fa\u540d", "\u5e97\u94fa\u540d\u79f0", "\u5e97\u94fa\u8d26\u53f7", "\u8d26\u53f7",
"account", "selleraccount", "storeaccount", "storename");
if (index != null) {
return index;
}
return findHeaderByPredicate(key ->
(key.contains("\u5e97\u94fa") && (key.contains("\u540d") || key.contains("\u8d26\u53f7")))
|| (key.contains("shop") && (key.contains("name") || key.contains("account")))
|| key.contains("selleraccount")
|| key.contains("storeaccount"));
}
private Integer resolveCountryHeaderIndex(String country) {
Integer index = findCountryIndex(country);
if (index != null) {
return index;
}
return switch (country) {
case "DE" -> firstHeader("\u5fb7\u56fd", "\u5fb7", "\u5fb7\u56fdasin");
case "UK" -> firstHeader("\u82f1\u56fd", "\u82f1", "\u82f1\u56fdasin");
case "FR" -> firstHeader("\u6cd5\u56fd", "\u6cd5", "\u6cd5\u56fdasin");
case "IT" -> firstHeader("\u610f\u5927\u5229", "\u610f", "\u610f\u5927\u5229asin");
case "ES" -> firstHeader("\u897f\u73ed\u7259", "\u897f", "\u897f\u73ed\u7259asin");
default -> null;
};
}
private Integer findSingleCountryIndex() {
Integer index = firstHeader(
"\u4e2d\u6587\u56fd\u5bb6\u540d", "\u56fd\u5bb6", "\u56fd\u5bb6\u540d", "\u56fd\u5bb6\u7ad9\u70b9",
"\u7ad9\u70b9", "country", "countryname", "marketplace", "site");
if (index != null) {
return index;
}
return findHeaderByPredicate(key ->
key.contains("\u56fd\u5bb6")
|| key.contains("\u7ad9\u70b9")
|| key.contains("country")
|| key.contains("marketplace")
|| key.equals("site"));
}
private Integer findSingleAsinIndex() {
return firstHeader("asin", "\u67e5\u8be2asin", "\u4ea7\u54c1asin", "\u94fe\u63a5asin");
}
private boolean hasWideCountryColumns() {
return SUPPORTED_COUNTRIES.stream().anyMatch(country -> resolveCountryHeaderIndex(country) != null);
}
private boolean hasSingleCountryAsinColumns() {
return findSingleCountryIndex() != null && findSingleAsinIndex() != null;
}
private Integer findHeaderByPredicate(Predicate<String> predicate) {
for (Map.Entry<String, Integer> entry : headerIndexByKey.entrySet()) {
String key = entry.getKey();
if (key != null && predicate.test(key)) {
return entry.getValue();
}
}
return null;
}
private Integer firstHeader(String... names) {
for (String name : names) {
Integer index = headerIndexByKey.get(normalizeHeaderKey(name));

View File

@@ -125,15 +125,18 @@ public class TaskResultItemService {
String status,
Object payload) {
String payloadJson = writeJson(payload);
String payloadHash = hash(payloadJson);
String normalizedScopeKey = firstNonBlank(scopeKey, "task:" + taskId).trim();
String normalizedItemKey = firstNonBlank(itemKey, "result:" + resultId).trim();
String scopeHash = hash(normalizedScopeKey);
String storedPayload = transientPayloadStorageService.storeResultItemPayload(
moduleType, taskId, scopeHash, normalizedItemKey, payloadJson);
String payloadHash = hash(payloadJson);
LocalDateTime now = LocalDateTime.now();
TaskResultItemEntity existing = find(taskId, moduleType, scopeHash, normalizedItemKey);
if (isUnchanged(existing, resultId, normalizedScopeKey, countryCode, asin, status, payloadHash)) {
return;
}
String storedPayload = transientPayloadStorageService.storeResultItemPayload(
moduleType, taskId, scopeHash, normalizedItemKey, payloadJson);
if (existing == null) {
TaskResultItemEntity entity = new TaskResultItemEntity();
entity.setTaskId(taskId);
@@ -159,6 +162,10 @@ public class TaskResultItemService {
if (existing == null) {
throw new BusinessException("保存任务结果明细失败");
}
if (isUnchanged(existing, resultId, normalizedScopeKey, countryCode, asin, status, payloadHash)) {
transientPayloadStorageService.deleteReplacedPayloadIfNeeded(storedPayload, existing.getPayloadJson());
return;
}
transientPayloadStorageService.deleteReplacedPayloadIfNeeded(existing.getPayloadJson(), storedPayload);
taskResultItemMapper.update(null, new LambdaUpdateWrapper<TaskResultItemEntity>()
.eq(TaskResultItemEntity::getId, existing.getId())
@@ -172,6 +179,24 @@ public class TaskResultItemService {
.set(TaskResultItemEntity::getUpdatedAt, now));
}
private boolean isUnchanged(TaskResultItemEntity existing,
Long resultId,
String scopeKey,
String countryCode,
String asin,
String status,
String payloadHash) {
if (existing == null) {
return false;
}
return java.util.Objects.equals(existing.getResultId(), resultId)
&& java.util.Objects.equals(existing.getScopeKey(), scopeKey)
&& java.util.Objects.equals(existing.getCountryCode(), countryCode)
&& java.util.Objects.equals(existing.getAsin(), asin)
&& java.util.Objects.equals(existing.getStatus(), status)
&& java.util.Objects.equals(existing.getPayloadHash(), payloadHash);
}
private TaskResultItemEntity find(Long taskId, String moduleType, String scopeHash, String itemKey) {
return taskResultItemMapper.selectOne(new LambdaQueryWrapper<TaskResultItemEntity>()
.eq(TaskResultItemEntity::getTaskId, taskId)

View File

@@ -31,6 +31,7 @@ AIIMAGE_APPEARANCE_PATENT_COZE_WORKFLOW_PATH=/v1/workflow/run
AIIMAGE_APPEARANCE_PATENT_COZE_WORKFLOW_ID=7632683471312355338
AIIMAGE_APPEARANCE_PATENT_COZE_TOKEN=
AIIMAGE_APPEARANCE_PATENT_COZE_BATCH_SIZE=10
AIIMAGE_APPEARANCE_PATENT_COZE_READ_TIMEOUT_MILLIS=60000
AIIMAGE_APPEARANCE_PATENT_STALE_TIMEOUT_MINUTES=20
AIIMAGE_ROCKETMQ_NAME_SERVER=121.196.149.225:9876

View File

@@ -136,6 +136,8 @@ aiimage:
coze-workflow-id: ${AIIMAGE_APPEARANCE_PATENT_COZE_WORKFLOW_ID:7632683471312355338}
coze-token: ${AIIMAGE_APPEARANCE_PATENT_COZE_TOKEN:}
coze-batch-size: ${AIIMAGE_APPEARANCE_PATENT_COZE_BATCH_SIZE:10}
coze-connect-timeout-millis: ${AIIMAGE_APPEARANCE_PATENT_COZE_CONNECT_TIMEOUT_MILLIS:10000}
coze-read-timeout-millis: ${AIIMAGE_APPEARANCE_PATENT_COZE_READ_TIMEOUT_MILLIS:60000}
stale-timeout-minutes: ${AIIMAGE_APPEARANCE_PATENT_STALE_TIMEOUT_MINUTES:20}
stale-finalize-cron: ${AIIMAGE_APPEARANCE_PATENT_STALE_FINALIZE_CRON:0 */2 * * * *}
security:

View File

@@ -23,8 +23,8 @@ bucket_path = "nanri-image/"
file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/"
import os
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/')
backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://8.136.19.173:18080').rstrip('/')
backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/')
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://8.136.19.173:18080').rstrip('/')
os.environ['OSS_ACCESS_KEY_ID'] = accessKeyId
os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret
os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6"

View File

@@ -862,7 +862,7 @@
<h3 style="margin-bottom:12px;font-size:14px;">导入添加</h3>
<div class="form-row">
<div class="form-group" style="min-width:320px;">
<label>上传 Excel列:分组店铺名、德国/英国/法国/意大利/西班牙</label>
<label>上传 Excel支持两种格式1. 分组/店铺名/德国-西班牙2. 店铺账号/ASIN/中文国家名。文件无分组列时取页面当前分组</label>
<input type="file" id="queryAsinImportFile" accept=".xlsx,.xls">
</div>
<button class="btn" id="btnImportQueryAsin" type="button">上传并添加</button>
@@ -879,7 +879,7 @@
<h3 style="margin-bottom:12px;font-size:14px;">导入删除</h3>
<div class="form-row">
<div class="form-group" style="min-width:320px;">
<label>上传 Excel匹配分组 + 店铺名 + 国家 ASIN 后删除</label>
<label>上传 Excel支持按 分组/店铺名/国家列 或 店铺账号/ASIN/中文国家名 删除;文件无分组列时取页面当前分组</label>
<input type="file" id="queryAsinDeleteImportFile" accept=".xlsx,.xls">
</div>
<button class="btn btn-danger" id="btnDeleteImportQueryAsin" type="button">上传并删除</button>