fix: withdraw previously-approved store reviews before re-submitting same version
When re-submitting a version to stores where the previous review was APPROVED: 1. markSubmitted now sets those stores to WITHDRAWN (not PENDING), preserving the old batchId/submittedAt for traceability. This signals to executeSubmitAsync that the store cancel API must be called before a new submission is attempted. 2. executeSubmitAsync detects the WITHDRAWN state and calls cancelAtStore first, then falls through to the normal submission path. This revokes the old approval on the store's side so no stale webhook or poll cycle can fire APPROVED for the old review after re-submission. 3. updateStoreReview now rejects APPROVED transitions from PENDING or WITHDRAWN states (stale webhook guard). A valid approval can only arrive after the store has seen the new submission (i.e. current state must be SUBMITTING or UNDER_REVIEW). This prevents autoPublishAfterReview from triggering before the new review cycle. Operation log includes `approvedWithdrawn` list when any store was withdrawn on re-submit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
ab7f029960
当前提交
8de0338b93
@ -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<String, Object> reviewMap = parseReviewStatus(v.getStoreReviewStatus());
|
||||
List<String> approvedWithdrawn = new ArrayList<>();
|
||||
for (String store : resolvedTargets) {
|
||||
String existingState = readReviewState(reviewMap.get(store));
|
||||
if (AppVersionEntity.StoreReviewState.APPROVED.name().equals(existingState)) {
|
||||
Map<String, Object> 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()));
|
||||
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<String, Object> 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<String, Object> 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<String, Object> current = asReviewPayload(reviewMap.get(storeType));
|
||||
String batchId = readText(current.get("batchId"));
|
||||
String submittedAt = readText(current.get("submittedAt"));
|
||||
|
||||
@ -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<String, String> 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);
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户