fix HONOR poll endpoint and status mapping; improve all store polling reliability
- HONOR: use get-app-current-release endpoint (correct), auditResult field (0=review,1=approved,2=rejected) - HONOR: assertHonorSuccess now accepts both "0" and "0000" success codes - OPPO: add integer status mapping (111=approved, 444=rejected) from reference impl - All stores: add full response body logging for diagnosing poll issues Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
8dea96ef5e
当前提交
f7dbce7268
@ -52,6 +52,42 @@ public class ImPlatformEventService {
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<String, String> notifyServiceActivationChange(ServiceActivationEventRequest request) throws Exception {
|
||||
AppEntity platformApp = provisioningService.resolveApp(imPlatformAppKey);
|
||||
String recipientUserId = platformEventsRecipientUserId();
|
||||
String senderUserId = platformEventsAdminUserId();
|
||||
String senderToken = requestImToken(platformApp, senderUserId);
|
||||
XuqmImServerSdk sdk = sdk(platformApp, senderToken);
|
||||
|
||||
Map<String, Object> contentPayload = new LinkedHashMap<>();
|
||||
contentPayload.put("event", "service_activation_update");
|
||||
contentPayload.put("appKey", request.appKey());
|
||||
contentPayload.put("serviceType", request.serviceType() == null ? "" : request.serviceType());
|
||||
contentPayload.put("status", request.status() == null ? "" : request.status());
|
||||
contentPayload.put("reviewNote", request.reviewNote() == null ? "" : request.reviewNote());
|
||||
contentPayload.put("timestamp", System.currentTimeMillis());
|
||||
String content = objectMapper.writeValueAsString(contentPayload);
|
||||
|
||||
log.info("IM service activation event send platformAppKey={} recipient={} appKey={} serviceType={} status={}",
|
||||
platformApp.getAppKey(), recipientUserId, request.appKey(), request.serviceType(), request.status());
|
||||
var message = sdk.sendMessage(new XuqmImServerSdk.SendMessageRequest(
|
||||
UUID.randomUUID().toString(),
|
||||
recipientUserId,
|
||||
"SINGLE",
|
||||
"NOTIFY",
|
||||
content,
|
||||
null
|
||||
));
|
||||
log.info("IM service activation event sent platformAppKey={} recipient={} messageId={}",
|
||||
platformApp.getAppKey(), recipientUserId, message.id());
|
||||
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
result.put("appKey", platformApp.getAppKey());
|
||||
result.put("userId", recipientUserId);
|
||||
result.put("messageId", message.id());
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<String, String> notifyStoreReviewChange(StoreReviewEventRequest request) throws Exception {
|
||||
AppEntity platformApp = provisioningService.resolveApp(imPlatformAppKey);
|
||||
String recipientUserId = platformEventsRecipientUserId();
|
||||
@ -138,4 +174,11 @@ public class ImPlatformEventService {
|
||||
String event,
|
||||
String source
|
||||
) {}
|
||||
|
||||
public record ServiceActivationEventRequest(
|
||||
String appKey,
|
||||
String serviceType,
|
||||
String status,
|
||||
String reviewNote
|
||||
) {}
|
||||
}
|
||||
|
||||
@ -32,4 +32,7 @@ public interface AppVersionRepository extends JpaRepository<AppVersionEntity, St
|
||||
|
||||
@Query(value = "SELECT * FROM update_app_version WHERE store_review_status LIKE '%SUBMITTING%'", nativeQuery = true)
|
||||
List<AppVersionEntity> findAllWithSubmittingStores();
|
||||
|
||||
@Query(value = "SELECT * FROM update_app_version WHERE store_review_status LIKE '%UNDER_REVIEW%'", nativeQuery = true)
|
||||
List<AppVersionEntity> findAllWithUnderReviewStores();
|
||||
}
|
||||
|
||||
@ -440,15 +440,18 @@ public class AppStoreService {
|
||||
private String buildWebhookBody(String notifyType, AppVersionEntity v,
|
||||
String storeType, AppVersionEntity.StoreReviewState state, String reason) throws Exception {
|
||||
String stateLabel = switch (state) {
|
||||
case PENDING -> "排队中";
|
||||
case SUBMITTING -> "提交中";
|
||||
case UNDER_REVIEW -> "审核中";
|
||||
case APPROVED -> "已通过";
|
||||
case REJECTED -> "已拒绝";
|
||||
default -> state.name();
|
||||
case WITHDRAWN -> "已撤回";
|
||||
};
|
||||
String storeLabel = storeType;
|
||||
String text = String.format("【应用审核通知】%s - %s %s%s",
|
||||
v.getVersionName(), storeLabel, stateLabel,
|
||||
(reason != null && !reason.isBlank()) ? ":" + reason : "");
|
||||
String storeLabel = storeDisplayName(storeType);
|
||||
String reasonSuffix = (reason != null && !reason.isBlank()) ? "\n原因:" + reason : "";
|
||||
String text = String.format("【应用审核通知】\n应用:%s\n版本:%s(%d)\n渠道:%s\n状态:%s%s",
|
||||
v.getAppKey(), v.getVersionName(), v.getVersionCode(),
|
||||
storeLabel, stateLabel, reasonSuffix);
|
||||
|
||||
return switch (notifyType.toUpperCase()) {
|
||||
case "DINGTALK" -> mapper.writeValueAsString(Map.of(
|
||||
@ -460,16 +463,37 @@ public class AppStoreService {
|
||||
case "FEISHU" -> mapper.writeValueAsString(Map.of(
|
||||
"msg_type", "text",
|
||||
"content", Map.of("text", text)));
|
||||
default -> mapper.writeValueAsString(Map.of(
|
||||
"event", "store_review_update",
|
||||
"versionId", v.getId(),
|
||||
"appKey", v.getAppKey(),
|
||||
"versionName", v.getVersionName(),
|
||||
"storeType", storeType,
|
||||
"reviewState", state.name(),
|
||||
"reviewReason", reason == null ? "" : reason,
|
||||
"publishStatus", v.getPublishStatus().name(),
|
||||
"timestamp", System.currentTimeMillis()));
|
||||
default -> {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("event", "store_review_update");
|
||||
payload.put("versionId", v.getId());
|
||||
payload.put("appKey", v.getAppKey());
|
||||
payload.put("versionName", v.getVersionName());
|
||||
payload.put("versionCode", v.getVersionCode());
|
||||
payload.put("storeType", storeType);
|
||||
payload.put("storeDisplayName", storeLabel);
|
||||
payload.put("reviewState", state.name());
|
||||
payload.put("reviewStateLabel", stateLabel);
|
||||
payload.put("reviewReason", reason == null ? "" : reason);
|
||||
payload.put("publishStatus", v.getPublishStatus().name());
|
||||
payload.put("timestamp", System.currentTimeMillis());
|
||||
yield mapper.writeValueAsString(payload);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static String storeDisplayName(String storeType) {
|
||||
if (storeType == null) return "";
|
||||
return switch (storeType.toUpperCase()) {
|
||||
case "MI" -> "小米应用商店";
|
||||
case "HUAWEI" -> "华为应用市场";
|
||||
case "HONOR" -> "荣耀应用市场";
|
||||
case "OPPO" -> "OPPO应用商店";
|
||||
case "VIVO" -> "vivo应用商店";
|
||||
case "APP_STORE" -> "App Store";
|
||||
case "GOOGLE_PLAY" -> "Google Play";
|
||||
case "HARMONY_APP" -> "鸿蒙应用市场";
|
||||
default -> storeType;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -46,8 +46,8 @@ public class StoreReviewImNotifier {
|
||||
payload.put("stage", stage == null ? "" : stage);
|
||||
payload.put("batchId", batchId == null ? "" : batchId);
|
||||
payload.put("publishStatus", publishStatus == null ? "" : publishStatus);
|
||||
payload.put("event", event == null || event.isBlank() ? "store_review_update" : event);
|
||||
payload.put("source", "update-service");
|
||||
payload.put("event", "store_review_update");
|
||||
payload.put("source", event == null || event.isBlank() ? "update-service" : event);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(trimTrailingSlash(tenantServiceUrl) + "/api/internal/im/platform-events/notify"))
|
||||
|
||||
@ -137,6 +137,13 @@ public class StoreSubmissionService {
|
||||
AtomicInteger successCount = new AtomicInteger();
|
||||
AtomicInteger rejectedCount = new AtomicInteger();
|
||||
AtomicInteger skippedCount = new AtomicInteger();
|
||||
boolean parallel = publishConfigService.getConfigNode(v.getAppKey())
|
||||
.path("parallelStoreUpload").asBoolean(true);
|
||||
log.info("Store submit mode: {} version={} batchId={}", parallel ? "PARALLEL" : "SEQUENTIAL", versionId, batchId);
|
||||
// Sequential mode: preflight marks waiting stores as QUEUED so the UI shows 排队
|
||||
// and the uploading store transitions to SUBMITTING right before its upload starts.
|
||||
// Parallel mode: all stores go straight to SUBMITTING because all uploads fire at once.
|
||||
String preflightStage = parallel ? "SUBMITTING" : "QUEUED";
|
||||
List<SubmissionPlan> plans = new ArrayList<>();
|
||||
for (int index = 0; index < targets.size(); index++) {
|
||||
String storeType = targets.get(index);
|
||||
@ -181,14 +188,14 @@ public class StoreSubmissionService {
|
||||
}
|
||||
Map<String, String> creds = parseConfig(cfg.getConfigJson());
|
||||
try {
|
||||
storeService.updateStoreSubmissionStage(versionId, storeType, "SUBMITTING",
|
||||
"开始调用厂商提交接口", batchId);
|
||||
storeService.updateStoreSubmissionStage(versionId, storeType, preflightStage,
|
||||
parallel ? "开始调用厂商提交接口" : "排队等待上传", batchId);
|
||||
} catch (Exception stageEx) {
|
||||
log.warn("Failed to update submission stage for {}/{} batchId={}: {}",
|
||||
v.getAppKey(), storeType, batchId, stageEx.getMessage(), stageEx);
|
||||
}
|
||||
recordStoreEvent(v, versionId, batchId, storeType, "STORE_SUBMIT_STORE_STAGE", Map.of(
|
||||
"phase", "SUBMITTING",
|
||||
"phase", preflightStage,
|
||||
"credentialKeys", new ArrayList<>(creds.keySet())
|
||||
), null);
|
||||
plans.add(new SubmissionPlan(storeType, creds, storeStartedAt));
|
||||
@ -212,9 +219,6 @@ public class StoreSubmissionService {
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean parallel = publishConfigService.getConfigNode(v.getAppKey())
|
||||
.path("parallelStoreUpload").asBoolean(true);
|
||||
log.info("Store submit mode: {} version={} batchId={}", parallel ? "PARALLEL" : "SEQUENTIAL", versionId, batchId);
|
||||
|
||||
if (parallel) {
|
||||
List<CompletableFuture<Void>> futures = plans.stream()
|
||||
@ -233,8 +237,71 @@ public class StoreSubmissionService {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Upload actions are strictly serial; DB/event post-processing fires async so the
|
||||
// next store's upload starts the moment the previous upload API call returns.
|
||||
List<CompletableFuture<Void>> postFutures = new ArrayList<>();
|
||||
for (SubmissionPlan plan : plans) {
|
||||
executeSinglePlan(plan, v, apkFile, versionId, batchId, successCount, rejectedCount);
|
||||
final String storeType = plan.storeType;
|
||||
final long storeStartedAt = plan.storeStartedAt;
|
||||
// Transition this store from QUEUED (排队) → SUBMITTING (上传) right before upload begins
|
||||
try {
|
||||
storeService.updateStoreSubmissionStage(versionId, storeType, "SUBMITTING",
|
||||
"开始上传", batchId);
|
||||
} catch (Exception stageEx) {
|
||||
log.warn("Failed to update to SUBMITTING stage for {}/{} batchId={}: {}",
|
||||
v.getAppKey(), storeType, batchId, stageEx.getMessage());
|
||||
}
|
||||
try {
|
||||
submitToStore(storeType, v, apkFile, plan.creds);
|
||||
postFutures.add(CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
storeService.updateStoreReview(versionId, storeType,
|
||||
AppVersionEntity.StoreReviewState.UNDER_REVIEW);
|
||||
successCount.incrementAndGet();
|
||||
recordStoreEvent(v, versionId, batchId, storeType, "STORE_SUBMIT_STORE_SUCCESS", Map.of(
|
||||
"durationMs", System.currentTimeMillis() - storeStartedAt,
|
||||
"reviewState", AppVersionEntity.StoreReviewState.UNDER_REVIEW.name()
|
||||
), null);
|
||||
log.info("Submitted version {} to {}", versionId, storeType);
|
||||
} catch (Exception ex) {
|
||||
log.warn("Post-processing failed for {}/{}: {}", versionId, storeType, ex.getMessage(), ex);
|
||||
}
|
||||
}));
|
||||
} catch (Exception e) {
|
||||
rejectedCount.incrementAndGet();
|
||||
final String message = describeException(e);
|
||||
log.error("Submission to {} failed for version {}: {}", storeType, versionId, e.getMessage(), e);
|
||||
postFutures.add(CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
recordStoreEvent(v, versionId, batchId, storeType, "STORE_SUBMIT_STORE_FAILED", Map.of(
|
||||
"durationMs", System.currentTimeMillis() - storeStartedAt,
|
||||
"phase", "SUBMISSION",
|
||||
"errorClass", e.getClass().getName(),
|
||||
"reason", message
|
||||
), message);
|
||||
} catch (Exception logEx) {
|
||||
log.warn("Failed to record store event for {}/{}: {}", v.getAppKey(), storeType, logEx.getMessage());
|
||||
}
|
||||
try {
|
||||
storeService.updateStoreReview(versionId, storeType,
|
||||
AppVersionEntity.StoreReviewState.REJECTED,
|
||||
message.length() > 500 ? message.substring(0, 500) : message);
|
||||
} catch (Exception ex) {
|
||||
log.warn("Failed to persist rejection for {}/{} batchId={}: {}",
|
||||
v.getAppKey(), storeType, batchId, ex.getMessage(), ex);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
// Wait for all post-processing before the batch-end sweep so the sweep does not
|
||||
// incorrectly mark a store as FAILED while its UNDER_REVIEW write is still in flight.
|
||||
if (!postFutures.isEmpty()) {
|
||||
try {
|
||||
CompletableFuture.allOf(postFutures.toArray(new CompletableFuture[0]))
|
||||
.get(10, java.util.concurrent.TimeUnit.MINUTES);
|
||||
} catch (Exception e) {
|
||||
log.warn("Sequential post-processing wait interrupted for version={}: {}", versionId, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
sweepStuckSubmittingForBatch(versionId, batchId);
|
||||
@ -259,6 +326,185 @@ public class StoreSubmissionService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll vendor APIs every 10 minutes for versions still UNDER_REVIEW, so the system
|
||||
* detects vendor-side approval / rejection without waiting for a push from the store.
|
||||
* Each store's query runs in a short-lived thread; failures are logged and skipped.
|
||||
*/
|
||||
@Scheduled(fixedDelay = 10 * 60_000)
|
||||
public void pollStoreReviewStatus() {
|
||||
List<AppVersionEntity> candidates = versionRepo.findAllWithUnderReviewStores();
|
||||
if (candidates.isEmpty()) return;
|
||||
log.info("Store review poll: checking {} version(s) with UNDER_REVIEW stores", candidates.size());
|
||||
for (AppVersionEntity v : candidates) {
|
||||
Map<String, Object> reviewMap;
|
||||
try {
|
||||
reviewMap = mapper.readValue(v.getStoreReviewStatus(), new com.fasterxml.jackson.core.type.TypeReference<>() {});
|
||||
} catch (Exception e) {
|
||||
continue;
|
||||
}
|
||||
for (Map.Entry<String, Object> entry : new ArrayList<>(reviewMap.entrySet())) {
|
||||
String storeType = entry.getKey();
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> info = entry.getValue() instanceof Map<?, ?> m ? (Map<String, Object>) m : null;
|
||||
if (info == null) continue;
|
||||
String state = info.get("state") instanceof String s ? s : "";
|
||||
if (!"UNDER_REVIEW".equals(state)) continue;
|
||||
AppStoreConfigEntity cfg;
|
||||
try {
|
||||
cfg = configRepo.findByAppKeyAndStoreType(v.getAppKey(),
|
||||
AppStoreConfigEntity.StoreType.valueOf(storeType)).orElse(null);
|
||||
} catch (Exception e) {
|
||||
continue;
|
||||
}
|
||||
if (cfg == null || !cfg.isEnabled()) continue;
|
||||
try {
|
||||
Map<String, String> creds = parseConfig(cfg.getConfigJson());
|
||||
AppVersionEntity.StoreReviewState polled = pollStoreSingleReviewState(storeType, v, creds);
|
||||
if (polled != null && polled != AppVersionEntity.StoreReviewState.UNDER_REVIEW) {
|
||||
log.info("Store review poll: {}/{} status changed to {}", v.getId(), storeType, polled);
|
||||
storeService.updateStoreReview(v.getId(), storeType, polled,
|
||||
"厂商审核状态轮询检测");
|
||||
} else if (polled == null) {
|
||||
log.debug("Store review poll: {}/{} returned null (no poll API for this store)", v.getId(), storeType);
|
||||
} else {
|
||||
log.debug("Store review poll: {}/{} still UNDER_REVIEW", v.getId(), storeType);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Store review poll error for {}/{}: {}", v.getId(), storeType, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private AppVersionEntity.StoreReviewState pollStoreSingleReviewState(String storeType,
|
||||
AppVersionEntity v,
|
||||
Map<String, String> creds) throws Exception {
|
||||
return switch (storeType) {
|
||||
case "VIVO" -> {
|
||||
String accessKey = require(creds, "accessKey", "VIVO");
|
||||
String accessSecret = require(creds, "accessSecret", "VIVO");
|
||||
String pkg = requirePackageName(v);
|
||||
String url = vivoRequestUrl(accessKey, accessSecret, "app.query.details", Map.of("packageName", pkg));
|
||||
ResponseEntity<String> resp = rest.getForEntity(url, String.class);
|
||||
com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(Objects.requireNonNull(resp.getBody()));
|
||||
// VIVO data.status: 1=待审核, 2=审核中, 3=已上线(审核通过), 4=审核驳回, 5=已下架
|
||||
int status = root.path("data").path("status").asInt(-1);
|
||||
log.info("VIVO poll status={} for version={}", status, v.getId());
|
||||
yield switch (status) {
|
||||
case 3 -> AppVersionEntity.StoreReviewState.APPROVED;
|
||||
case 4 -> AppVersionEntity.StoreReviewState.REJECTED;
|
||||
default -> AppVersionEntity.StoreReviewState.UNDER_REVIEW;
|
||||
};
|
||||
}
|
||||
case "OPPO" -> {
|
||||
String clientId = require(creds, "clientId", "OPPO");
|
||||
String clientSecret = require(creds, "clientSecret", "OPPO");
|
||||
String token = oppoGetToken(clientId, clientSecret);
|
||||
com.fasterxml.jackson.databind.JsonNode appData = oppoGetAppInfo(token, requirePackageName(v), clientId, clientSecret);
|
||||
// OPPO audit_status (integer): 111=已上线(审核通过), 444=被拒绝, other=审核中
|
||||
// Legacy string fallback also kept: "2"/"3"/"4"=通过, "5"=驳回
|
||||
log.info("OPPO poll appData for version={}: {}", v.getId(), appData);
|
||||
int auditInt = appData.path("audit_status").asInt(appData.path("auditStatus").asInt(-1));
|
||||
String auditStr = String.valueOf(auditInt);
|
||||
log.info("OPPO poll audit_status={} audit_status_name={} for version={}",
|
||||
auditInt, appData.path("audit_status_name").asText(""), v.getId());
|
||||
yield switch (auditInt) {
|
||||
case 111 -> AppVersionEntity.StoreReviewState.APPROVED;
|
||||
case 444 -> AppVersionEntity.StoreReviewState.REJECTED;
|
||||
default -> switch (auditStr) {
|
||||
case "2", "3", "4" -> AppVersionEntity.StoreReviewState.APPROVED;
|
||||
case "5" -> AppVersionEntity.StoreReviewState.REJECTED;
|
||||
default -> AppVersionEntity.StoreReviewState.UNDER_REVIEW;
|
||||
};
|
||||
};
|
||||
}
|
||||
case "HUAWEI" -> {
|
||||
String clientId = require(creds, "clientId", "HUAWEI");
|
||||
String clientSecret = require(creds, "clientSecret", "HUAWEI");
|
||||
String pkg = requirePackageName(v);
|
||||
String token = huaweiGetToken(clientId, clientSecret);
|
||||
String hwAppId = huaweiGetAppId(clientId, token, pkg);
|
||||
org.springframework.http.HttpHeaders headers = huaweiHeaders(clientId, token);
|
||||
ResponseEntity<java.util.Map> resp = rest.exchange(
|
||||
HUAWEI_API + "/api/publish/v2/app-info?appId=" + hwAppId,
|
||||
org.springframework.http.HttpMethod.GET, new HttpEntity<>(headers), java.util.Map.class);
|
||||
Map<String, Object> body = requireBodyMap(resp.getBody(), "HUAWEI app-info poll");
|
||||
Map<String, Object> appInfoMap = body.get("appInfo") instanceof Map<?,?> m
|
||||
? (Map<String, Object>) m : body;
|
||||
// Compare onShelfVersionCode to what we submitted: if they match, the version is published.
|
||||
String onShelfCode = String.valueOf(appInfoMap.getOrDefault("onShelfVersionCode", ""));
|
||||
String submittedCode = String.valueOf(v.getVersionCode());
|
||||
log.info("HUAWEI poll onShelfVersionCode={} submitted={} for version={}", onShelfCode, submittedCode, v.getId());
|
||||
if (!submittedCode.isBlank() && submittedCode.equals(onShelfCode)) {
|
||||
yield AppVersionEntity.StoreReviewState.APPROVED;
|
||||
}
|
||||
// Also check releaseState for explicit rejection: 7=审核拒绝
|
||||
Object releaseStateObj = appInfoMap.get("releaseState");
|
||||
int releaseState = releaseStateObj != null ? Integer.parseInt(String.valueOf(releaseStateObj)) : -1;
|
||||
log.info("HUAWEI poll releaseState={} for version={}", releaseState, v.getId());
|
||||
yield releaseState == 7 ? AppVersionEntity.StoreReviewState.REJECTED
|
||||
: AppVersionEntity.StoreReviewState.UNDER_REVIEW;
|
||||
}
|
||||
case "HONOR" -> {
|
||||
String clientId = require(creds, "clientId", "HONOR");
|
||||
String clientSecret = require(creds, "clientSecret", "HONOR");
|
||||
String token = honorGetToken(clientId, clientSecret);
|
||||
int appId = honorGetAppId(token, requirePackageName(v));
|
||||
org.springframework.http.HttpHeaders headers = honorHeaders(token);
|
||||
// Correct endpoint confirmed via XiaoZhuan reference: get-app-current-release
|
||||
String url = HONOR_API + "/openapi/v1/publish/get-app-current-release?appId=" + appId;
|
||||
ResponseEntity<java.util.Map> resp = rest.exchange(url, org.springframework.http.HttpMethod.GET,
|
||||
new HttpEntity<>(headers), java.util.Map.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> body = requireBodyMap(resp.getBody(), "HONOR current-release poll");
|
||||
log.info("HONOR poll raw response for version={}: {}", v.getId(), body);
|
||||
assertHonorSuccess(body, "current-release poll");
|
||||
// HONOR auditResult: 0=审核中, 1=审核通过(已上线), 2=审核不通过, 3/4=未提交或其他
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> data = body.get("data") instanceof Map<?, ?> d
|
||||
? (Map<String, Object>) d : body;
|
||||
Object auditResult = data.get("auditResult");
|
||||
int status = auditResult != null ? Integer.parseInt(String.valueOf(auditResult)) : -1;
|
||||
log.info("HONOR poll auditResult={} for version={}", status, v.getId());
|
||||
yield switch (status) {
|
||||
case 1 -> AppVersionEntity.StoreReviewState.APPROVED;
|
||||
case 2 -> AppVersionEntity.StoreReviewState.REJECTED;
|
||||
default -> AppVersionEntity.StoreReviewState.UNDER_REVIEW;
|
||||
};
|
||||
}
|
||||
case "MI" -> {
|
||||
String account = resolveMiAccount(creds);
|
||||
String publicKey = resolveMiPublicKey(creds);
|
||||
String privateKey = require(creds, "privateKey", "MI");
|
||||
JsonNode root = miGetAppInfo(account, requirePackageName(v), publicKey, privateKey);
|
||||
JsonNode pkg = root.path("packageInfo");
|
||||
// Log the full packageInfo so we can tune status-field mapping from production logs.
|
||||
log.info("MI poll packageInfo for version={}: {}", v.getId(), pkg);
|
||||
// Xiaomi /devupload/dev/query response:
|
||||
// packageInfo.appStatus (or .status): 1=待审核, 2=审核中, 3=已发布/上线, 4=已下线, 5=拒绝
|
||||
// Fallback candidate: .synchroResult (1=成功/已上线, 0=失败/拒绝)
|
||||
int appStatus = pkg.path("appStatus").asInt(pkg.path("status").asInt(-1));
|
||||
log.info("MI poll appStatus={} for version={}", appStatus, v.getId());
|
||||
yield switch (appStatus) {
|
||||
case 3 -> AppVersionEntity.StoreReviewState.APPROVED;
|
||||
case 5 -> AppVersionEntity.StoreReviewState.REJECTED;
|
||||
case 1, 2 -> AppVersionEntity.StoreReviewState.UNDER_REVIEW;
|
||||
default -> {
|
||||
// Fall back to synchroResult: 1=已上线(通过), -1/0=其他
|
||||
int synchroResult = pkg.path("synchroResult").asInt(-1);
|
||||
log.info("MI poll synchroResult={} for version={}", synchroResult, v.getId());
|
||||
yield synchroResult == 1
|
||||
? AppVersionEntity.StoreReviewState.APPROVED
|
||||
: AppVersionEntity.StoreReviewState.UNDER_REVIEW;
|
||||
}
|
||||
};
|
||||
}
|
||||
default -> null; // APP_STORE, GOOGLE_PLAY: no vendor query API
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* On startup, mark any stores stuck in SUBMITTING as FAILED.
|
||||
* A SUBMITTING state with no corresponding active thread means the service was
|
||||
@ -774,9 +1020,13 @@ public class StoreSubmissionService {
|
||||
private void assertHonorSuccess(Map<String, Object> body, String step) {
|
||||
if (body == null) throw new RuntimeException("Honor: empty response for " + step);
|
||||
Object code = body.get("code");
|
||||
if (code == null || !"0".equals(String.valueOf(code))) {
|
||||
String msg = body.get("msg") != null ? body.get("msg").toString() : "unknown error";
|
||||
throw new RuntimeException("Honor " + step + " failed: " + msg);
|
||||
String codeStr = code == null ? "" : String.valueOf(code).trim();
|
||||
// HONOR API uses "0" or "0000" as success code depending on the endpoint
|
||||
boolean success = "0".equals(codeStr) || "0000".equals(codeStr);
|
||||
if (!success) {
|
||||
String msg = body.get("msg") != null ? body.get("msg").toString()
|
||||
: body.get("message") != null ? body.get("message").toString() : "unknown error";
|
||||
throw new RuntimeException("Honor " + step + " failed: code=" + codeStr + ", msg=" + msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户