fix(update-service): OPPO 商店状态字段读取修复、避免重复审核通知,新增默认更新说明与公开下载接口
- 修复 OPPO 状态查询读取 snake_case 字段名(version_code/version_name),解决版本已上线仍显示"审核中" - 手动刷新厂商状态时若已是干净的 APPROVED+live 状态则跳过重复通知,避免重复发送【应用审核通知】 - 各厂商提交分支统一走 effectiveChangeLog(),更新说明为空时兜底默认文案 - 新增 GET /api/v1/updates/public/download-info 公开接口,支撑新的应用下载页 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
这个提交包含在:
父节点
032dd4be61
当前提交
7db07cc5fc
@ -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/**",
|
||||
|
||||
@ -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<ApiResponse<Map<String, Object>>> downloadInfo(@RequestParam String appKey) {
|
||||
Map<String, Object> 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<String, Object> androidInfo(String appKey) {
|
||||
Optional<AppVersionEntity> latest = versionRepository.findTopByAppKeyAndPlatformAndPublishStatusOrderByVersionCodeDesc(
|
||||
appKey, AppVersionEntity.Platform.ANDROID, AppVersionEntity.PublishStatus.PUBLISHED);
|
||||
if (latest.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
AppVersionEntity v = latest.get();
|
||||
Map<String, Object> 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<String, String> 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<String, Object> storeOnlyInfo(String appKey, AppVersionEntity.Platform platform,
|
||||
AppStoreConfigEntity.StoreType storeType) {
|
||||
String storeUrl = appStoreService.getStoreJumpUrl(appKey, storeType);
|
||||
if (storeUrl == null || storeUrl.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
Optional<AppVersionEntity> latest = versionRepository.findTopByAppKeyAndPlatformAndPublishStatusOrderByVersionCodeDesc(
|
||||
appKey, platform, AppVersionEntity.PublishStatus.PUBLISHED);
|
||||
if (latest.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
AppVersionEntity v = latest.get();
|
||||
Map<String, Object> 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;
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户