From 8dea96ef5e7d812fe20af810279e3275fcdab726 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Sat, 16 May 2026 15:58:27 +0800 Subject: [PATCH] fix: skip already-reviewing stores on resubmit; clean up orphaned SUBMITTING states - StoreSubmissionService: skip UNDER_REVIEW/APPROVED stores in preflight loop to prevent duplicate submissions - StoreSubmissionService: post-batch sweep marks any still-SUBMITTING stores in same batch as REJECTED - StoreSubmissionService: @EventListener(ApplicationReadyEvent) clears orphaned SUBMITTING states on startup - AppVersionRepository: add findAllWithSubmittingStores() native query for startup sweep Co-Authored-By: Claude Sonnet 4.6 --- .../repository/AppVersionRepository.java | 4 + .../service/StoreSubmissionService.java | 85 +++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/update-service/src/main/java/com/xuqm/update/repository/AppVersionRepository.java b/update-service/src/main/java/com/xuqm/update/repository/AppVersionRepository.java index 393f77c..a045cb6 100644 --- a/update-service/src/main/java/com/xuqm/update/repository/AppVersionRepository.java +++ b/update-service/src/main/java/com/xuqm/update/repository/AppVersionRepository.java @@ -2,6 +2,7 @@ package com.xuqm.update.repository; import com.xuqm.update.entity.AppVersionEntity; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import java.time.LocalDateTime; import java.util.List; @@ -28,4 +29,7 @@ public interface AppVersionRepository extends JpaRepository findByStoreSubmitModeAndStoreSubmitScheduledAtBefore( String storeSubmitMode, LocalDateTime before); + + @Query(value = "SELECT * FROM update_app_version WHERE store_review_status LIKE '%SUBMITTING%'", nativeQuery = true) + List findAllWithSubmittingStores(); } diff --git a/update-service/src/main/java/com/xuqm/update/service/StoreSubmissionService.java b/update-service/src/main/java/com/xuqm/update/service/StoreSubmissionService.java index 7869e6f..6c2a002 100644 --- a/update-service/src/main/java/com/xuqm/update/service/StoreSubmissionService.java +++ b/update-service/src/main/java/com/xuqm/update/service/StoreSubmissionService.java @@ -12,6 +12,8 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.FileSystemResource; import org.springframework.http.*; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -147,6 +149,19 @@ public class StoreSubmissionService { "packageName", v.getPackageName() == null ? "" : v.getPackageName() ), null); try { + // Skip stores already in active review to avoid duplicate submissions + String activeState = currentStoreState(v.getStoreReviewStatus(), storeType); + if ("UNDER_REVIEW".equals(activeState) || "APPROVED".equals(activeState)) { + skippedCount.incrementAndGet(); + log.info("Skipping {} for version={} batchId={} — already in state {}", + storeType, versionId, batchId, activeState); + recordStoreEvent(v, versionId, batchId, storeType, "STORE_SUBMIT_STORE_SKIPPED", Map.of( + "reason", "already in " + activeState, + "currentState", activeState + ), null); + continue; + } + AppStoreConfigEntity cfg = configRepo .findByAppKeyAndStoreType(v.getAppKey(), AppStoreConfigEntity.StoreType.valueOf(storeType)) .orElse(null); @@ -222,6 +237,8 @@ public class StoreSubmissionService { executeSinglePlan(plan, v, apkFile, versionId, batchId, successCount, rejectedCount); } } + sweepStuckSubmittingForBatch(versionId, batchId); + recordBatchEvent(v, versionId, batchId, "STORE_SUBMIT_BATCH_END", startedAt, Map.of( "targets", targets, "successCount", successCount.get(), @@ -242,6 +259,74 @@ public class StoreSubmissionService { } } + /** + * On startup, mark any stores stuck in SUBMITTING as FAILED. + * A SUBMITTING state with no corresponding active thread means the service was + * restarted mid-batch — the submission did not complete. + */ + @EventListener(ApplicationReadyEvent.class) + public void sweepOrphanedSubmittingOnStartup() { + List candidates = versionRepo.findAllWithSubmittingStores(); + if (candidates.isEmpty()) return; + log.info("Startup sweep: found {} version(s) with orphaned SUBMITTING states", candidates.size()); + for (AppVersionEntity v : candidates) { + sweepStuckSubmittingForVersion(v.getId(), null, "服务重启,提交中断"); + } + } + + /** + * After a batch finishes, sweep any stores in this batch that are still SUBMITTING. + * This catches cases where a thread was cancelled (timeout) or threw an unchecked + * exception that bypassed the normal failure path in executeSinglePlan. + */ + private void sweepStuckSubmittingForBatch(String versionId, String batchId) { + sweepStuckSubmittingForVersion(versionId, batchId, "提交中断:未收到厂商确认响应"); + } + + private String currentStoreState(String reviewStatusJson, String storeType) { + if (reviewStatusJson == null || reviewStatusJson.isBlank()) return ""; + try { + Map reviewMap = mapper.readValue(reviewStatusJson, new TypeReference<>() {}); + Object entry = reviewMap.get(storeType); + if (entry instanceof Map m) { + Object state = m.get("state"); + return state instanceof String s ? s : ""; + } + return entry instanceof String s ? s : ""; + } catch (Exception e) { + return ""; + } + } + + @SuppressWarnings("unchecked") + private void sweepStuckSubmittingForVersion(String versionId, String matchBatchId, String failReason) { + try { + AppVersionEntity v = versionRepo.findById(versionId).orElse(null); + if (v == null || v.getStoreReviewStatus() == null) return; + Map reviewMap = mapper.readValue(v.getStoreReviewStatus(), + new TypeReference>() {}); + for (Map.Entry entry : new ArrayList<>(reviewMap.entrySet())) { + String store = entry.getKey(); + if (!(entry.getValue() instanceof Map rawInfo)) continue; + Map info = (Map) rawInfo; + String state = info.get("state") instanceof String s ? s : ""; + if (!"SUBMITTING".equals(state)) continue; + String entryBatchId = info.get("batchId") instanceof String s ? s : ""; + if (matchBatchId != null && !matchBatchId.equals(entryBatchId)) continue; + log.warn("Sweeping stuck SUBMITTING store={} version={} batchId={} reason={}", + store, versionId, entryBatchId, failReason); + try { + storeService.updateStoreReview(versionId, store, + AppVersionEntity.StoreReviewState.REJECTED, failReason); + } catch (Exception ex) { + log.error("Failed to sweep stuck store={} version={}: {}", store, versionId, ex.getMessage()); + } + } + } catch (Exception ex) { + log.error("sweepStuckSubmitting failed for version={}: {}", versionId, ex.getMessage()); + } + } + private void executeSinglePlan(SubmissionPlan plan, AppVersionEntity v, File apkFile, String versionId, String batchId, AtomicInteger successCount, AtomicInteger rejectedCount) {