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>
这个提交包含在:
XuqmGroup 2026-07-04 01:50:20 +08:00
父节点 032dd4be61
当前提交 7db07cc5fc
共有 3 个文件被更改,包括 123 次插入11 次删除

查看文件

@ -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) {
// 商店显示应用已上线但版本号不完全匹配
// 仍然标记为 APPROVEDpreExisting=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());
}