fix(update): fix OPPO token expiry, sign empty params, and MI already-live detection

- 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 <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-05-19 19:11:13 +08:00
父节点 450a44de68
当前提交 501d7e09ab

查看文件

@ -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<String, String> 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<String> 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<Map<String, String>>() {});
}
/** 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<String, String> creds;