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()); }