From 501d7e09ab84209e4e7296b20ea1df45e3fd319b Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Tue, 19 May 2026 19:11:13 +0800 Subject: [PATCH] fix(update): fix OPPO token expiry, sign empty params, and MI already-live detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OPPO: re-obtain access token after APK upload (prevents expiry during long uploads) - OPPO: exclude empty-string params from api_sign computation per OPPO spec - OPPO: include errno in error message for easier debugging - MI: detect already-live version via packageInfo.versionCode before upload; throw VersionAlreadyLiveException → executor marks state APPROVED instead of REJECTED Co-Authored-By: Claude Sonnet 4.6 --- .../service/StoreSubmissionService.java | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) 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 c1620e6..8e1d8cb 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 @@ -988,6 +988,19 @@ public class StoreSubmissionService { "reviewState", AppVersionEntity.StoreReviewState.UNDER_REVIEW.name() ), null); log.info("Submitted version {} to {}", versionId, plan.storeType); + } catch (VersionAlreadyLiveException e) { + // Version is already the live version on this store — mark as APPROVED, not REJECTED + log.info("Store {} version {} already live, updating to APPROVED: {}", plan.storeType, versionId, e.getMessage()); + try { + storeService.updateStoreReview(versionId, plan.storeType, + AppVersionEntity.StoreReviewState.APPROVED); + recordStoreEvent(v, versionId, batchId, plan.storeType, "STORE_SUBMIT_ALREADY_LIVE", Map.of( + "durationMs", System.currentTimeMillis() - plan.storeStartedAt, + "reason", e.getMessage() + ), null); + } catch (Exception ex) { + log.warn("Failed to persist already-live state for {}/{}: {}", v.getAppKey(), plan.storeType, ex.getMessage()); + } } catch (Exception e) { rejectedCount.incrementAndGet(); String message = describeException(e); @@ -1443,7 +1456,16 @@ public class StoreSubmissionService { String packageName = requirePackageName(v); JsonNode appInfo = miGetAppInfo(account, packageName, publicKey, privateKey); String appName = appInfo.path("packageInfo").path("appName").asText(packageName); + // If Xiaomi's current live versionCode already matches this submission, skip upload + String onlineCode = appInfo.path("packageInfo").path("versionCode").asText(""); + String submittedCode = String.valueOf(v.getVersionCode()); + if (!submittedCode.isBlank() && submittedCode.equals(onlineCode)) { + throw new VersionAlreadyLiveException("MI", + "MI 版本 " + v.getVersionName() + "(" + submittedCode + ") 已是小米在线版本,无需重复提交"); + } miUploadApk(file, account, appName, packageName, v.getChangeLog(), publicKey, privateKey); + } catch (VersionAlreadyLiveException e) { + throw e; } catch (Exception e) { throw new IllegalStateException("MI store submission failed: " + e.getMessage(), e); } @@ -1464,6 +1486,8 @@ public class StoreSubmissionService { } Map uploadUrl = oppoGetUploadUrl(token, clientId, clientSecret); JsonNode apkResult = oppoUploadApk(uploadUrl, token, file, clientSecret); + // Re-obtain token: the upload can take minutes and the OPPO token may expire + token = oppoGetToken(clientId, clientSecret); oppoSubmit(v, file, token, appInfo, apkResult, clientSecret); } catch (Exception e) { throw new IllegalStateException("OPPO store submission failed: " + e.getMessage(), e); @@ -2066,9 +2090,9 @@ public class StoreSubmissionService { private void oppoCheckSuccess(JsonNode root, String action) { int code = root.path("errno").asInt(-1); - String message = root.path("data").path("message").asText(""); + String message = root.path("data").path("message").asText(root.path("errMsg").asText("未知")); if (code != 0) { - throw new IllegalStateException(action + " failed: " + message); + throw new IllegalStateException(action + " failed (errno=" + code + "): " + message); } } @@ -2078,7 +2102,8 @@ public class StoreSubmissionService { List parts = new ArrayList<>(); for (String key : keys) { String value = paramsMap.get(key); - if (value == null) continue; + // OPPO sign algorithm: skip null and empty-string params + if (value == null || value.isEmpty()) continue; parts.add(key + "=" + value); } return hmacSha256(String.join("&", parts), secret); @@ -2370,6 +2395,16 @@ public class StoreSubmissionService { return mapper.readValue(json, new TypeReference>() {}); } + /** Thrown when the version being submitted is already the live version on the target store. */ + private static final class VersionAlreadyLiveException extends RuntimeException { + private final String storeType; + VersionAlreadyLiveException(String storeType, String msg) { + super(msg); + this.storeType = storeType; + } + String getStoreType() { return storeType; } + } + private static final class SubmissionPlan { private final String storeType; private final Map creds;