From 7db07cc5fc8526ebb399a094625172ca60ea06fd Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Sat, 4 Jul 2026 01:50:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(update-service):=20OPPO=20=E5=95=86?= =?UTF-8?q?=E5=BA=97=E7=8A=B6=E6=80=81=E5=AD=97=E6=AE=B5=E8=AF=BB=E5=8F=96?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E3=80=81=E9=81=BF=E5=85=8D=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8=E9=80=9A=E7=9F=A5=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E6=9B=B4=E6=96=B0=E8=AF=B4=E6=98=8E=E4=B8=8E?= =?UTF-8?q?=E5=85=AC=E5=BC=80=E4=B8=8B=E8=BD=BD=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 OPPO 状态查询读取 snake_case 字段名(version_code/version_name),解决版本已上线仍显示"审核中" - 手动刷新厂商状态时若已是干净的 APPROVED+live 状态则跳过重复通知,避免重复发送【应用审核通知】 - 各厂商提交分支统一走 effectiveChangeLog(),更新说明为空时兜底默认文案 - 新增 GET /api/v1/updates/public/download-info 公开接口,支撑新的应用下载页 Co-Authored-By: Claude Sonnet 5 --- .../xuqm/update/config/SecurityConfig.java | 1 + .../controller/PublicDownloadController.java | 88 +++++++++++++++++++ .../service/StoreSubmissionService.java | 45 +++++++--- 3 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 update-service/src/main/java/com/xuqm/update/controller/PublicDownloadController.java diff --git a/update-service/src/main/java/com/xuqm/update/config/SecurityConfig.java b/update-service/src/main/java/com/xuqm/update/config/SecurityConfig.java index ad47b20..90f4162 100644 --- a/update-service/src/main/java/com/xuqm/update/config/SecurityConfig.java +++ b/update-service/src/main/java/com/xuqm/update/config/SecurityConfig.java @@ -47,6 +47,7 @@ public class SecurityConfig { "/actuator/**", "/api/v1/updates/app/check", "/api/v1/updates/app/inspect", + "/api/v1/updates/public/**", "/api/v1/rn/update/check", "/api/v1/rn/inspect", "/api/v1/rn/files/**", diff --git a/update-service/src/main/java/com/xuqm/update/controller/PublicDownloadController.java b/update-service/src/main/java/com/xuqm/update/controller/PublicDownloadController.java new file mode 100644 index 0000000..c59be5f --- /dev/null +++ b/update-service/src/main/java/com/xuqm/update/controller/PublicDownloadController.java @@ -0,0 +1,88 @@ +package com.xuqm.update.controller; + +import com.xuqm.common.model.ApiResponse; +import com.xuqm.update.entity.AppStoreConfigEntity; +import com.xuqm.update.entity.AppVersionEntity; +import com.xuqm.update.repository.AppVersionRepository; +import com.xuqm.update.service.AppStoreService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +/** + * Public, unauthenticated endpoint backing the per-app download page. Returns only what a + * visitor needs to install the app — no admin data, no auth required. + */ +@RestController +@RequestMapping("/api/v1/updates/public") +public class PublicDownloadController { + + private final AppVersionRepository versionRepository; + private final AppStoreService appStoreService; + + public PublicDownloadController(AppVersionRepository versionRepository, AppStoreService appStoreService) { + this.versionRepository = versionRepository; + this.appStoreService = appStoreService; + } + + @GetMapping("/download-info") + public ResponseEntity>> downloadInfo(@RequestParam String appKey) { + Map response = new LinkedHashMap<>(); + response.put("android", androidInfo(appKey)); + response.put("ios", storeOnlyInfo(appKey, AppVersionEntity.Platform.IOS, AppStoreConfigEntity.StoreType.APP_STORE)); + response.put("harmony", storeOnlyInfo(appKey, AppVersionEntity.Platform.HARMONY, AppStoreConfigEntity.StoreType.HARMONY_APP)); + return ResponseEntity.ok(ApiResponse.success(response)); + } + + private Map androidInfo(String appKey) { + Optional latest = versionRepository.findTopByAppKeyAndPlatformAndPublishStatusOrderByVersionCodeDesc( + appKey, AppVersionEntity.Platform.ANDROID, AppVersionEntity.PublishStatus.PUBLISHED); + if (latest.isEmpty()) { + return null; + } + AppVersionEntity v = latest.get(); + Map info = new LinkedHashMap<>(); + info.put("versionName", v.getVersionName()); + info.put("versionCode", v.getVersionCode()); + info.put("downloadUrl", v.getDownloadUrl() != null ? v.getDownloadUrl() : ""); + info.put("changeLog", v.getChangeLog() != null ? v.getChangeLog() : ""); + info.put("publishedAt", v.getCreatedAt() != null ? v.getCreatedAt().toString() : ""); + + Map storeLinks = new LinkedHashMap<>(); + for (AppStoreConfigEntity.StoreType storeType : AppStoreConfigEntity.StoreType.values()) { + if (!storeType.isAndroid()) continue; + String url = appStoreService.getStoreJumpUrl(appKey, storeType); + if (url != null && !url.isBlank()) { + storeLinks.put(storeType.name(), url); + } + } + info.put("storeLinks", storeLinks); + return info; + } + + private Map storeOnlyInfo(String appKey, AppVersionEntity.Platform platform, + AppStoreConfigEntity.StoreType storeType) { + String storeUrl = appStoreService.getStoreJumpUrl(appKey, storeType); + if (storeUrl == null || storeUrl.isBlank()) { + return null; + } + Optional latest = versionRepository.findTopByAppKeyAndPlatformAndPublishStatusOrderByVersionCodeDesc( + appKey, platform, AppVersionEntity.PublishStatus.PUBLISHED); + if (latest.isEmpty()) { + return null; + } + AppVersionEntity v = latest.get(); + Map info = new LinkedHashMap<>(); + info.put("versionName", v.getVersionName()); + info.put("versionCode", v.getVersionCode()); + info.put("storeUrl", storeUrl); + info.put("publishedAt", v.getCreatedAt() != null ? v.getCreatedAt().toString() : ""); + return info; + } +} 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 256fa0f..51245a5 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 @@ -561,8 +561,10 @@ public class StoreSubmissionService { String clientSecret = require(creds, "clientSecret", "OPPO"); String token = oppoGetToken(clientId, clientSecret); JsonNode appData = oppoGetAppInfo(token, requirePackageName(v), clientId, clientSecret); - String onlineVersionCode = appData.path("versionCode").asText(""); - String onlineVersionName = appData.path("versionName").asText(""); + // OPPO's app/info API returns snake_case field names (version_code/version_name), + // not camelCase. Fall back to camelCase in case OPPO changes casing. + String onlineVersionCode = appData.path("version_code").asText(appData.path("versionCode").asText("")); + String onlineVersionName = appData.path("version_name").asText(appData.path("versionName").asText("")); int auditInt = appData.path("audit_status").asInt(appData.path("auditStatus").asInt(-1)); String submittedCode = String.valueOf(v.getVersionCode()); // OPPO audit_status: 2/3/4/111 = online variants, 444 = rejected aggregate. @@ -1067,6 +1069,12 @@ public class StoreSubmissionService { } else if (entry instanceof String s) { existingState = s; } + boolean existingLiveOnStore = false; + boolean existingPreExisting = false; + if (entry instanceof Map em) { + existingLiveOnStore = Boolean.TRUE.equals(em.get("liveOnStore")); + existingPreExisting = Boolean.TRUE.equals(em.get("preExisting")); + } boolean isUnderReview = "UNDER_REVIEW".equals(existingState); boolean isRejected = "REJECTED".equals(existingState); boolean isApproved = "APPROVED".equals(existingState); @@ -1109,10 +1117,18 @@ public class StoreSubmissionService { AppVersionEntity.StoreReviewState mappedState = mapToStoreReviewState(polled.getReviewState()); if (mappedState == AppVersionEntity.StoreReviewState.APPROVED) { if (polled.isCurrentSubmissionLive()) { - // 提交版本本身已上线 — 标记 APPROVED - log.info("Manual refresh: {}/{} submitted version now live — updating", v.getId(), storeType); - storeService.updateStoreReviewLive(v.getId(), storeType, false, - buildLiveReason(polled), buildExtra(polled)); + boolean alreadyCleanLive = isApproved && existingLiveOnStore && !existingPreExisting; + if (alreadyCleanLive) { + // Already recorded as APPROVED + live for this exact submission (e.g. by the + // scheduled poller or a previous manual refresh) — re-notifying on every click + // of "刷新厂商状态" would spam duplicate "【应用审核通知】" messages. + log.debug("Manual refresh: {}/{} already APPROVED+live — skipping duplicate notification", v.getId(), storeType); + } else { + // 提交版本本身已上线 — 标记 APPROVED + log.info("Manual refresh: {}/{} submitted version now live — updating", v.getId(), storeType); + storeService.updateStoreReviewLive(v.getId(), storeType, false, + buildLiveReason(polled), buildExtra(polled)); + } } else if (!isApproved) { // 商店显示应用已上线,但版本号不完全匹配。 // 仍然标记为 APPROVED(preExisting=true),避免卡在 UNDER_REVIEW。 @@ -1184,6 +1200,13 @@ public class StoreSubmissionService { }; } + private static final String DEFAULT_CHANGE_LOG = "部分线上已知问题修复"; + + private String effectiveChangeLog(AppVersionEntity v) { + String c = v.getChangeLog(); + return (c == null || c.isBlank()) ? DEFAULT_CHANGE_LOG : c; + } + private String buildLiveReason(StoreRemoteState result) { if (result.isCurrentSubmissionLive()) { return "厂商审核状态轮询检测:本次提交版本已上线"; @@ -1435,7 +1458,7 @@ public class StoreSubmissionService { huaweiWaitCompileOrContinue(clientId, token, hwAppId, pkgId); // 7. Update version description - huaweiUpdateDesc(clientId, token, hwAppId, v.getChangeLog()); + huaweiUpdateDesc(clientId, token, hwAppId, effectiveChangeLog(v)); // 7.5. Set planned release date if specified if (v.getScheduledPublishAt() != null) { @@ -1666,7 +1689,7 @@ public class StoreSubmissionService { // 6. Submit for review String submitToken = honorGetToken(clientId, clientSecret); - honorSubmit(submitToken, honorAppId, v.getChangeLog()); + honorSubmit(submitToken, honorAppId, effectiveChangeLog(v)); } private String honorGetToken(String clientId, String clientSecret) { @@ -1805,7 +1828,7 @@ public class StoreSubmissionService { throw new VersionAlreadyLiveException("MI", "MI 版本 " + v.getVersionName() + "(" + submittedCode + ") 已是小米在线版本,无需重复提交"); } - miUploadApk(file, account, appName, packageName, v.getChangeLog(), publicKey, privateKey); + miUploadApk(file, account, appName, packageName, effectiveChangeLog(v), publicKey, privateKey); } catch (VersionAlreadyLiveException e) { throw e; } catch (Exception e) { @@ -1846,7 +1869,7 @@ public class StoreSubmissionService { String packageName = requirePackageName(v); JsonNode appInfo = vivoGetAppInfo(accessKey, accessSecret, packageName); JsonNode apkResult = vivoUploadApk(accessKey, accessSecret, file, packageName); - vivoSubmit(accessKey, accessSecret, apkResult, v.getChangeLog(), appInfo); + vivoSubmit(accessKey, accessSecret, apkResult, effectiveChangeLog(v), appInfo); } catch (Exception e) { throw new IllegalStateException("VIVO store submission failed: " + e.getMessage(), e); } @@ -2373,7 +2396,7 @@ public class StoreSubmissionService { params.put("pkg_name", requirePackageName(v)); params.put("version_code", String.valueOf(parseVersionCode(v.getVersionCode()))); params.put("apk_url", apkUrl.toString()); - String oppoUpdateDesc = v.getChangeLog() == null ? "" : v.getChangeLog(); + String oppoUpdateDesc = effectiveChangeLog(v); if (oppoUpdateDesc.length() < 5) { oppoUpdateDesc = oppoUpdateDesc + " ".substring(0, 5 - oppoUpdateDesc.length()); }