From b8f88ca06eeb3361cc643fe56f4a0f4d4e4b1f46 Mon Sep 17 00:00:00 2001 From: super <2903208875@qq.com> Date: Mon, 30 Mar 2026 20:39:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9A=82=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/DeleteBrandRunService.java | 61 ++- .../service/DeleteBrandStaleTaskService.java | 21 +- .../ziniao/service/ZiniaoAuthService.java | 2 +- .../resources/application-local.example.yml | 2 +- .../src/main/resources/application.yml | 2 +- .../brand/components/BrandDeleteBrandTab.vue | 353 ++++++++++++------ frontend-vue/src/shared/api/java-modules.ts | 7 +- 7 files changed, 319 insertions(+), 129 deletions(-) diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandRunService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandRunService.java index 4274841..684a806 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandRunService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandRunService.java @@ -40,6 +40,8 @@ import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + import java.io.File; import java.io.FileInputStream; @@ -125,6 +127,8 @@ public class DeleteBrandRunService { item.setShopId(matchResult.shopId()); item.setPlatform(matchResult.platform()); item.setOpenStoreUrl(matchResult.openStoreUrl()); + } else { + throw new BusinessException("未匹配到紫鸟店铺,跳过处理"); } } catch (BusinessException ex) { String message = ex.getMessage(); @@ -191,9 +195,10 @@ public class DeleteBrandRunService { task.setFailedFileCount(parsedFailedCount); task.setResultJson(JSONUtil.toJsonStr(items)); task.setUpdatedAt(LocalDateTime.now()); + task.setSourceFileCount(parsedSuccessCount); if (parsedSuccessCount <= 0) { task.setStatus("FAILED"); - task.setErrorMessage(parsedFailedCount > 0 ? "删除品牌文件解析失败" : null); + task.setErrorMessage(parsedFailedCount > 0 ? "全部文件解析或匹配失败" : null); task.setFinishedAt(LocalDateTime.now()); } fileTaskMapper.updateById(task); @@ -269,11 +274,20 @@ public class DeleteBrandRunService { } if (entity.getSourceFilename().equals(candidate.getSourceFilename())) { item.setMatched(candidate.isMatched()); + // 如果物理结果已经是成功了,说明已经处理过了,强制置为匹配成功 + if (item.isSuccess()) { + item.setMatched(true); + } + item.setShopId(candidate.getShopId()); item.setPlatform(candidate.getPlatform()); item.setOpenStoreUrl(candidate.getOpenStoreUrl()); + // 历史表可能只有通用 errorMessage;优先返回任务当时的真实 error - if ((item.getError() == null || item.getError().isBlank()) && candidate.getError() != null && !candidate.getError().isBlank()) { + // 但是,如果物理结果是成功的,就不应该显示所谓的“未匹配”报错 + if (item.isSuccess()) { + item.setError(null); + } else if ((item.getError() == null || item.getError().isBlank()) && candidate.getError() != null && !candidate.getError().isBlank()) { item.setError(candidate.getError()); } break; @@ -636,7 +650,10 @@ public class DeleteBrandRunService { } } + @Transactional public void submitResult(Long taskId, DeleteBrandSubmitResultRequest request) { + log.info("[DeleteBrand] Received submitResult for taskId: {}, files count: {}", taskId, request == null || request.getFiles() == null ? 0 : request.getFiles().size()); + if (taskId == null || taskId <= 0) { throw new BusinessException("taskId 不合法"); } @@ -659,24 +676,33 @@ public class DeleteBrandRunService { throw new BusinessException("任务原始数据已过期,请重新执行解析"); } - task.setStatus("RUNNING"); - task.setErrorMessage(null); - task.setUpdatedAt(LocalDateTime.now()); - task.setFinishedAt(null); - fileTaskMapper.updateById(task); + if (!"RUNNING".equals(task.getStatus())) { + task.setStatus("RUNNING"); + task.setErrorMessage(null); + task.setUpdatedAt(LocalDateTime.now()); + task.setFinishedAt(null); + fileTaskMapper.updateById(task); + } + for (DeleteBrandResultFileDto fileDto : request.getFiles()) { + log.info("[DeleteBrand] submitResult processing chunk -> taskId: {}, file: {}, chunkIndex: {}/{}, processedRows: {}/{}", + taskId, fileDto.getSourceFilename(), fileDto.getChunkIndex(), fileDto.getChunkTotal(), + fileDto.getProcessedRows(), fileDto.getTotalRows()); + String fileIdentity = resolveFileIdentity(fileDto); if (fileIdentity.isBlank()) { throw new BusinessException("fileKey/sourceFilename 不能为空"); } DeleteBrandParsedFileCacheDto parsedFile = parsedPayload.get(fileIdentity); if (parsedFile == null) { + log.warn("[DeleteBrand] parsed payload not found for identity: {}", fileIdentity); throw new BusinessException("文件标识不匹配: " + fileIdentity); } // 更早的 duplicate fast-path:尽量在较重的校验/进度写入之前直接短路 if (deleteBrandTaskCacheService.hasSameResultChunk(taskId, fileIdentity, fileDto.getChunkIndex(), fileDto)) { + log.info("[DeleteBrand] Duplicate chunk ignored for taskId: {}, file: {}, chunkIndex: {}", taskId, fileIdentity, fileDto.getChunkIndex()); continue; } @@ -699,6 +725,16 @@ public class DeleteBrandRunService { progress.put("updated_at", String.valueOf(System.currentTimeMillis())); progress.put("last_heartbeat_at", String.valueOf(System.currentTimeMillis())); deleteBrandTaskCacheService.saveProgress(taskId, progress); + + // 如果该文件已完成,立即更新成品计数的 Redis 缓存「提示」,让后续 tryFinalizeTask 能更精确 + if (isFileCompleted(fileDto)) { + log.info("[DeleteBrand] File chunk transmission fully reached via chunkTotal for taskId: {}, file: {}", taskId, fileDto.getSourceFilename()); + java.util.Map currentProgress = deleteBrandTaskCacheService.getProgress(taskId); + int finishedCount = parseInt(currentProgress.get("finished_files")) == null ? 0 : parseInt(currentProgress.get("finished_files")); + // 这里我们暂不直接 increment,而是标记需要重算或在 tryFinalizeTask 中重新计算 + // 为了保险,我们直接在 tryFinalizeTask 里重新遍历分片状态 + } + } tryFinalizeTask(taskId, false); @@ -724,12 +760,10 @@ public class DeleteBrandRunService { return; } - Map progressSnapshot = deleteBrandTaskCacheService.getProgress(taskId); int expectedFiles = parsedPayload.size(); - Integer finishedFilesHint = parseInt(progressSnapshot == null ? null : progressSnapshot.get("finished_files")); - if (finishedFilesHint != null && finishedFilesHint < expectedFiles) { - return; - } + // 移除 finishedFilesHint 强行返回逻辑,因为 Redis 里的 finished_files 可能是旧的, + // 既然进入了 tryFinalizeTask,就说明有新片到达,应该以 loadMergedChunks 为准 + Map> mergedByFile = loadMergedChunks(taskId); int finishedFiles = countCompletedFiles(mergedByFile); @@ -747,8 +781,11 @@ public class DeleteBrandRunService { fileTaskMapper.updateById(task); if (finishedFiles < expectedFiles) { + log.info("[DeleteBrand] tryFinalizeTask skipping -> finishedFiles ({}) < expectedFiles ({})", finishedFiles, expectedFiles); return; } + + log.info("[DeleteBrand] tryFinalizeTask calling finalizeTask -> finishedFiles: {}, expectedFiles: {}", finishedFiles, expectedFiles); finalizeTask(task, parsedPayload, mergedByFile); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandStaleTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandStaleTaskService.java index 495875b..e9968c9 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandStaleTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/deletebrand/service/DeleteBrandStaleTaskService.java @@ -39,6 +39,16 @@ public class DeleteBrandStaleTaskService { Map 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) { try { @@ -48,18 +58,19 @@ public class DeleteBrandStaleTaskService { } LocalDateTime lastActivityTime; - if (lastHeartbeatAt > 0) { - // 有心跳,用最后心跳时间对比 15 分钟 + // 只有当 has_progress 为 true 且超过 15 分钟没心跳,才认为是异常卡死 + if (lastHeartbeatAt > 0 && isActive) { + // 有心跳,用最后心跳时间对比 15 分钟 (threshold 是 now - 15) lastActivityTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastHeartbeatAt), ZoneId.systemDefault()); if (lastActivityTime.isAfter(threshold)) { continue; // 没超时 } } else { - // 没有心跳,说明任务一直没被 Python 取出来执行(处在排队状态中) - // 排队的宽限期我们设为 12 个小时,如果建了 12 个小时都没执行,再杀死 + // 没有心跳,或者当前处在 has_progress: false (等待用户点击下一个任务推送的静默期) + // 静默期和排队期的宽限期一样,设为 12 个小时 LocalDateTime queuedThreshold = LocalDateTime.now().minusHours(12); if (task.getCreatedAt() != null && task.getCreatedAt().isAfter(queuedThreshold)) { - continue; // 排队中,不要杀它! + continue; // 等待下个队列推送中,不要杀它! } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoAuthService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoAuthService.java index cfb3e0e..036bd65 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoAuthService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/ziniao/service/ZiniaoAuthService.java @@ -294,7 +294,7 @@ public class ZiniaoAuthService { .queryParam("storeId", storeId) .queryParam("openapiToken", loginToken) .queryParam("userId", userId) - .queryParam("lanuchUrl", encodedLaunchUrl) + .queryParam("launchUrl", encodedLaunchUrl) .queryParam("autoopen", Boolean.TRUE.equals(ziniaoProperties.getOpenStoreAutoOpen())) .queryParam("debuggPort", ziniaoProperties.getOpenStoreDebugPort()) .queryParam("notPromptForDownload", Boolean.TRUE.equals(ziniaoProperties.getOpenStoreNotPromptForDownload()) ? 1 : 0); diff --git a/backend-java/src/main/resources/application-local.example.yml b/backend-java/src/main/resources/application-local.example.yml index 8b41c59..d76c3a9 100644 --- a/backend-java/src/main/resources/application-local.example.yml +++ b/backend-java/src/main/resources/application-local.example.yml @@ -29,7 +29,7 @@ AIIMAGE_ZINIAO_APP_TOKEN_PATH=/auth/get_app_token AIIMAGE_ZINIAO_STAFF_LIST_PATH=/superbrowser/rest/v1/erp/staff/list AIIMAGE_ZINIAO_USER_STORES_PATH=/superbrowser/rest/v1/erp/user/stores AIIMAGE_ZINIAO_USER_LOGIN_TOKEN_PATH=/superbrowser/rest/v1/token/user-login -AIIMAGE_ZINIAO_OPEN_STORE_SCHEME=superbrowser://OpenStrore +AIIMAGE_ZINIAO_OPEN_STORE_SCHEME=superbrowser://OpenStore AIIMAGE_ZINIAO_OPEN_STORE_USER_ID= AIIMAGE_ZINIAO_OPEN_STORE_LAUNCH_URL=https://www.baidu.com AIIMAGE_ZINIAO_OPEN_STORE_AUTO_OPEN=true diff --git a/backend-java/src/main/resources/application.yml b/backend-java/src/main/resources/application.yml index 7ce15a9..25abfb6 100644 --- a/backend-java/src/main/resources/application.yml +++ b/backend-java/src/main/resources/application.yml @@ -96,7 +96,7 @@ aiimage: staff-list-path: ${AIIMAGE_ZINIAO_STAFF_LIST_PATH:/superbrowser/rest/v1/erp/staff/list} user-stores-path: ${AIIMAGE_ZINIAO_USER_STORES_PATH:/superbrowser/rest/v1/erp/user/stores} user-login-token-path: ${AIIMAGE_ZINIAO_USER_LOGIN_TOKEN_PATH:/superbrowser/rest/v1/token/user-login} - open-store-scheme: ${AIIMAGE_ZINIAO_OPEN_STORE_SCHEME:superbrowser://OpenStrore} + open-store-scheme: ${AIIMAGE_ZINIAO_OPEN_STORE_SCHEME:superbrowser://OpenStore} open-store-user-id: ${AIIMAGE_ZINIAO_OPEN_STORE_USER_ID:} open-store-launch-url: ${AIIMAGE_ZINIAO_OPEN_STORE_LAUNCH_URL:https://www.baidu.com} open-store-auto-open: ${AIIMAGE_ZINIAO_OPEN_STORE_AUTO_OPEN:true} diff --git a/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue b/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue index 153b572..b07ab72 100644 --- a/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandDeleteBrandTab.vue @@ -101,7 +101,8 @@
  • @@ -109,6 +110,7 @@
    店铺名:{{ item.shopName || '-' }}
    匹配结果:{{ item.matched ? `已匹配 ${item.shopId || ''}` : '未匹配到紫鸟店铺' }}
    平台:{{ item.platform }}
    +
    队列状态:{{ getQueueStatus(item) }}
    国家数:{{ item.countryCount }}
    去重后 {{ item.totalRows }} 条
    @@ -135,7 +137,7 @@ v-if="item.openStoreUrl" type="button" class="download" - @click="openShop(item.openStoreUrl)" + @click="triggerItemRun(item)" > 打开紫鸟 @@ -165,7 +167,8 @@
    • @@ -195,14 +198,7 @@ {{ getTaskStatusInfo(item).text }} - +