diff --git a/update-service/src/main/java/com/xuqm/update/service/AppStoreService.java b/update-service/src/main/java/com/xuqm/update/service/AppStoreService.java index 90c5a80..7544d55 100644 --- a/update-service/src/main/java/com/xuqm/update/service/AppStoreService.java +++ b/update-service/src/main/java/com/xuqm/update/service/AppStoreService.java @@ -148,16 +148,29 @@ public class AppStoreService { // Withdraw lower versions' active reviews for the same stores cancelSupersededVersionReviews(v.getAppKey(), v.getPlatform(), versionId, v.getVersionCode(), resolvedTargets); - // Merge with existing store statuses instead of replacing — preserves other stores' states + // Merge with existing store statuses instead of replacing — preserves other stores' states. + // If a store was previously APPROVED, mark it WITHDRAWN so executeSubmitAsync knows to + // call the store's cancel API before re-submitting. This prevents stale APPROVED webhooks + // or poll cycles from triggering auto-publish before the new review cycle completes. Map reviewMap = parseReviewStatus(v.getStoreReviewStatus()); + List approvedWithdrawn = new ArrayList<>(); for (String store : resolvedTargets) { - reviewMap.put(store, reviewPayload( - AppVersionEntity.StoreReviewState.PENDING.name(), - null, - "QUEUED", - null, - null, - LocalDateTime.now().toString())); + String existingState = readReviewState(reviewMap.get(store)); + if (AppVersionEntity.StoreReviewState.APPROVED.name().equals(existingState)) { + Map prev = asReviewPayload(reviewMap.get(store)); + reviewMap.put(store, reviewPayload( + AppVersionEntity.StoreReviewState.WITHDRAWN.name(), + "重新提审,待撤回原有审核通过状态", + "PENDING_CANCEL", + readText(prev.get("batchId")).isBlank() ? null : readText(prev.get("batchId")), + readText(prev.get("submittedAt")).isBlank() ? null : readText(prev.get("submittedAt")), + LocalDateTime.now().toString())); + approvedWithdrawn.add(store); + } else { + reviewMap.put(store, reviewPayload( + AppVersionEntity.StoreReviewState.PENDING.name(), + null, "QUEUED", null, null, LocalDateTime.now().toString())); + } } v.setStoreSubmitTargets(mapper.writeValueAsString(resolvedTargets)); v.setStoreReviewStatus(mapper.writeValueAsString(reviewMap)); @@ -170,18 +183,21 @@ public class AppStoreService { v.setAutoPublishAfterReview(autoPublishAfterReview && !"SCHEDULED".equals(normalizedMode)); } AppVersionEntity saved = versionRepo.save(v); + Map submitLogDetails = new LinkedHashMap<>(); + submitLogDetails.put("storeTypes", resolvedTargets); + submitLogDetails.put("submitMode", saved.getStoreSubmitMode()); + submitLogDetails.put("scheduledAt", saved.getStoreSubmitScheduledAt() == null ? "" : saved.getStoreSubmitScheduledAt().toString()); + submitLogDetails.put("autoPublishAfterReview", saved.isAutoPublishAfterReview()); + if (!approvedWithdrawn.isEmpty()) { + submitLogDetails.put("approvedWithdrawn", approvedWithdrawn); + } operationLogService.record( saved.getAppKey(), "APP_VERSION", saved.getId(), "STORE_SUBMIT_REQUEST", null, - Map.of( - "storeTypes", resolvedTargets, - "submitMode", saved.getStoreSubmitMode(), - "scheduledAt", saved.getStoreSubmitScheduledAt() == null ? "" : saved.getStoreSubmitScheduledAt().toString(), - "autoPublishAfterReview", saved.isAutoPublishAfterReview() - )); + submitLogDetails); storeReviewImNotifier.notifyStoreReviewChange( saved.getAppKey(), saved.getId(), @@ -268,6 +284,21 @@ public class AppStoreService { AppVersionEntity v = versionRepo.findById(versionId).orElseThrow(); Map reviewMap = parseReviewStatus(v.getStoreReviewStatus()); + + // Guard: reject APPROVED transitions from PENDING or WITHDRAWN states. + // Those states mean a re-submission is queued or in progress; accepting APPROVED + // here would be a stale webhook from the previous review cycle, which would + // incorrectly trigger auto-publish before the new review completes. + if (state == AppVersionEntity.StoreReviewState.APPROVED) { + String existingState = readReviewState(reviewMap.get(storeType)); + if (AppVersionEntity.StoreReviewState.PENDING.name().equals(existingState) + || AppVersionEntity.StoreReviewState.WITHDRAWN.name().equals(existingState)) { + log.warn("Ignored stale APPROVED event for {}/{} (current: {}); re-submission is pending", + versionId, storeType, existingState); + return v; + } + } + Map current = asReviewPayload(reviewMap.get(storeType)); String batchId = readText(current.get("batchId")); String submittedAt = readText(current.get("submittedAt")); 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 f548e07..c30d24f 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 @@ -171,6 +171,30 @@ public class StoreSubmissionService { continue; } + // WITHDRAWN means this store had a previous APPROVED state that markSubmitted reset. + // Call the store's cancel API to revoke the old approval before re-submitting, so + // no stale webhooks or poll cycles can trigger auto-publish from the old review. + if ("WITHDRAWN".equals(activeState)) { + AppStoreConfigEntity withdrawCfg = configRepo + .findByAppKeyAndStoreType(v.getAppKey(), AppStoreConfigEntity.StoreType.valueOf(storeType)) + .orElse(null); + if (withdrawCfg != null && withdrawCfg.isEnabled()) { + try { + Map withdrawCreds = parseConfig(withdrawCfg.getConfigJson()); + cancelAtStore(storeType, v, withdrawCreds); + log.info("Withdrew previously-approved store {} for version {} before re-submitting", + storeType, versionId); + recordStoreEvent(v, versionId, batchId, storeType, "STORE_WITHDRAW_BEFORE_RESUBMIT", Map.of( + "reason", "重新提审,撤回原有审核通过状态" + ), null); + } catch (Exception e) { + log.warn("Cancel-before-resubmit failed for {}/{} batchId={}: {}", + storeType, versionId, batchId, e.getMessage()); + } + } + // Fall through to the normal submission path below + } + AppStoreConfigEntity cfg = configRepo .findByAppKeyAndStoreType(v.getAppKey(), AppStoreConfigEntity.StoreType.valueOf(storeType)) .orElse(null);