一大波改动

这个提交包含在:
XuqmGroup 2026-05-15 22:11:03 +08:00
父节点 bc1165d22e
当前提交 0ed09a8229
共有 3 个文件被更改,包括 297 次插入27 次删除

查看文件

@ -114,18 +114,19 @@ public class AppStoreController {
List<String> storeTypes = body != null ? extractStringList(body, "storeTypes") : null;
String submitMode = body != null ? (String) body.get("submitMode") : null;
String scheduledAtText = body != null ? (String) body.get("scheduledPublishAt") : null;
String scheduledPublishAtText = body != null ? (String) body.get("scheduledPublishAt") : null;
Boolean autoPublishAfterReview = body != null && body.get("autoPublishAfterReview") != null
? Boolean.valueOf(body.get("autoPublishAfterReview").toString()) : null;
java.time.LocalDateTime scheduledAt = null;
if (scheduledAtText != null && !scheduledAtText.isBlank()) {
scheduledAt = java.time.LocalDateTime.parse(scheduledAtText);
java.time.LocalDateTime scheduledPublishAt = null;
if (scheduledPublishAtText != null && !scheduledPublishAtText.isBlank()) {
scheduledPublishAt = java.time.LocalDateTime.parse(scheduledPublishAtText);
}
AppVersionEntity v = storeService.markSubmitted(versionId,
storeTypes,
submitMode,
scheduledAt,
autoPublishAfterReview);
null,
autoPublishAfterReview,
scheduledPublishAt);
String normalizedMode = submitMode == null ? "MANUAL" : submitMode.trim().toUpperCase();
if (!"SCHEDULED".equals(normalizedMode)) {
submissionService.executeSubmitAsync(versionId);
@ -161,6 +162,41 @@ public class AppStoreController {
storeService.updateStoreReview(versionId, storeType, state, reason)));
}
// Cancel review (撤回审核)
/**
* Withdraw app from store review.
* Body: { "storeTypes": ["HUAWEI", "MI"] } optional, defaults to all active-review stores.
*/
@PostMapping("/app/{versionId}/cancel-review")
public ResponseEntity<ApiResponse<Void>> cancelReview(
@PathVariable String versionId,
@RequestBody(required = false) Map<String, Object> body) throws Exception {
List<String> storeTypes = body != null ? extractStringList(body, "storeTypes") : null;
submissionService.cancelStoreReview(versionId, storeTypes);
return ResponseEntity.ok(ApiResponse.success(null));
}
// Modify publish schedule
/**
* Change when an approved version goes live.
* Body: { "publishType": "IMMEDIATE" | "SCHEDULED", "scheduledAt": "2026-05-20T10:00:00" }
*/
@PutMapping("/app/{versionId}/publish-schedule")
public ResponseEntity<ApiResponse<AppVersionEntity>> updatePublishSchedule(
@PathVariable String versionId,
@RequestBody Map<String, Object> body) throws Exception {
String publishType = body.get("publishType") instanceof String s ? s : "IMMEDIATE";
String scheduledAtText = body.get("scheduledAt") instanceof String s ? s : null;
java.time.LocalDateTime scheduledAt = scheduledAtText != null && !scheduledAtText.isBlank()
? java.time.LocalDateTime.parse(scheduledAtText) : null;
return ResponseEntity.ok(ApiResponse.success(
submissionService.updatePublishSchedule(versionId, publishType, scheduledAt)));
}
private List<String> extractStringList(Map<String, Object> body, String key) {
Object value = body.get(key);
if (value instanceof List<?> list) {

查看文件

@ -115,6 +115,15 @@ public class AppStoreService {
String submitMode,
LocalDateTime scheduledAt,
Boolean autoPublishAfterReview) throws Exception {
return markSubmitted(versionId, storeTypes, submitMode, scheduledAt, autoPublishAfterReview, null);
}
public AppVersionEntity markSubmitted(String versionId,
List<String> storeTypes,
String submitMode,
LocalDateTime scheduledAt,
Boolean autoPublishAfterReview,
LocalDateTime scheduledPublishAt) throws Exception {
synchronized (lockFor(versionId)) {
AppVersionEntity v = versionRepo.findById(versionId).orElseThrow();
List<String> resolvedTargets = normalizeTargets(v.getAppKey(), storeTypes);
@ -130,7 +139,8 @@ public class AppStoreService {
// Withdraw lower versions' active reviews for the same stores
cancelSupersededVersionReviews(v.getAppKey(), v.getPlatform(), versionId, v.getVersionCode(), resolvedTargets);
Map<String, Object> reviewMap = new LinkedHashMap<>();
// Merge with existing store statuses instead of replacing preserves other stores' states
Map<String, Object> reviewMap = parseReviewStatus(v.getStoreReviewStatus());
for (String store : resolvedTargets) {
reviewMap.put(store, reviewPayload(
AppVersionEntity.StoreReviewState.PENDING.name(),
@ -144,6 +154,9 @@ public class AppStoreService {
v.setStoreReviewStatus(mapper.writeValueAsString(reviewMap));
v.setStoreSubmitMode(normalizedMode);
v.setStoreSubmitScheduledAt(scheduledAt);
if (scheduledPublishAt != null) {
v.setScheduledPublishAt(scheduledPublishAt);
}
if (autoPublishAfterReview != null) {
v.setAutoPublishAfterReview(autoPublishAfterReview && !"SCHEDULED".equals(normalizedMode));
}

查看文件

@ -324,12 +324,23 @@ public class StoreSubmissionService {
// 5. Bind APK
String pkgId = huaweiBindApk(clientId, token, hwAppId, file.getName(), objectId);
// 6. Wait for compile (poll up to 3 minutes)
huaweiWaitCompile(clientId, token, hwAppId, pkgId);
// 6. Brief compile check (up to 5 min) HUAWEI queues compile+review internally so
// we submit regardless of compile status after the grace period.
huaweiWaitCompileOrContinue(clientId, token, hwAppId, pkgId);
// 7. Update version description
huaweiUpdateDesc(clientId, token, hwAppId, v.getChangeLog());
// 7.5. Set planned release date if specified
if (v.getScheduledPublishAt() != null) {
try {
huaweiUpdateReleasePlan(v, creds, "SCHEDULED", v.getScheduledPublishAt());
log.info("Huawei release plan set to SCHEDULED at {}", v.getScheduledPublishAt());
} catch (Exception e) {
log.warn("Huawei release plan update failed (non-fatal): {}", e.getMessage());
}
}
// 8. Submit for review
huaweiSubmit(clientId, token, hwAppId);
}
@ -436,28 +447,50 @@ public class StoreSubmissionService {
return pkgId;
}
/**
* Polls HUAWEI compile status for up to 5 minutes.
* If compile finishes (status=2) we return early; status=3 (failed) throws.
* If the deadline passes without a clear result we log and continue anyway
* HUAWEI queues compile and review internally, so submitting before compile
* finishes is safe for large APKs that take longer than our grace period.
*/
@SuppressWarnings("unchecked")
private void huaweiWaitCompile(String clientId, String token, String hwAppId, String pkgId) throws InterruptedException {
private void huaweiWaitCompileOrContinue(String clientId, String token, String hwAppId, String pkgId) throws InterruptedException {
HttpHeaders headers = huaweiHeaders(clientId, token);
long deadline = System.currentTimeMillis() + 3 * 60_000L;
long deadline = System.currentTimeMillis() + 5 * 60_000L;
while (System.currentTimeMillis() < deadline) {
Thread.sleep(10_000);
Thread.sleep(15_000);
try {
ResponseEntity<Map> resp = rest.exchange(
HUAWEI_API + "/api/publish/v2/package/compile/status?appId=" + hwAppId + "&pkgIds=" + pkgId,
HttpMethod.GET, new HttpEntity<>(headers), Map.class);
Map<String, Object> respBody = requireBodyMap(resp.getBody(), "Huawei compile status");
log.info("Huawei compile status raw: {}", summarizeMap(respBody));
List<Map<String, Object>> states = asMapList(respBody.get("pkgStateList"));
if (states.isEmpty()) {
states = asMapList(respBody.get("data"));
}
if (states != null && !states.isEmpty()) {
Object compileStatus = states.get(0).get("compileStatus");
if ("2".equals(String.valueOf(compileStatus))) return; // 2 = success
if (states.isEmpty()) states = asMapList(respBody.get("data"));
if (!states.isEmpty()) {
Map<String, Object> s0 = states.get(0);
// HUAWEI compile status API uses successStatus (0=pending,1=success,2=failed)
// or legacy compileStatus (2=done,3=failed). Try both.
Object compileStatus = s0.get("compileStatus");
Object successStatus = s0.get("successStatus");
log.info("Huawei compile state: compileStatus={} successStatus={} aabCompileStatus={}",
compileStatus, successStatus, s0.get("aabCompileStatus"));
if ("2".equals(String.valueOf(compileStatus))) return; // legacy done
if ("3".equals(String.valueOf(compileStatus)))
throw new RuntimeException("Huawei compile failed: " + summarizeMap(states.get(0)));
throw new RuntimeException("Huawei compile failed: " + summarizeMap(s0));
if ("1".equals(String.valueOf(successStatus))) return; // new API done
if ("2".equals(String.valueOf(successStatus)))
throw new RuntimeException("Huawei compile failed (successStatus=2): " + summarizeMap(s0));
} else {
log.info("Huawei compile pkgStateList empty, keys={}", respBody.keySet());
}
} catch (RuntimeException e) {
if (e.getMessage() != null && e.getMessage().startsWith("Huawei compile failed")) throw e;
log.warn("Huawei compile status poll error (continuing): {}", e.getMessage());
}
}
throw new RuntimeException("Huawei compile timeout");
log.warn("Huawei compile grace period elapsed for pkgId={} — submitting anyway", pkgId);
}
private void huaweiUpdateDesc(String clientId, String token, String hwAppId, String changeLog) {
@ -469,10 +502,23 @@ public class StoreSubmissionService {
HttpMethod.PUT, new HttpEntity<>(body, headers), Map.class);
}
@SuppressWarnings("unchecked")
private void huaweiSubmit(String clientId, String token, String hwAppId) {
HttpHeaders headers = huaweiHeaders(clientId, token);
rest.postForEntity(HUAWEI_API + "/api/publish/v2/app-submit?appId=" + hwAppId,
ResponseEntity<Map> resp = rest.postForEntity(
HUAWEI_API + "/api/publish/v2/app-submit?appId=" + hwAppId,
new HttpEntity<>(headers), Map.class);
Map<String, Object> body = resp.getBody();
log.info("Huawei app-submit response: {}", body != null ? summarizeMap(body) : "null");
if (body != null) {
Object ret = body.get("ret");
if (ret instanceof Map<?, ?> retMap) {
Object code = retMap.get("code");
if (code != null && !"0".equals(String.valueOf(code))) {
throw new RuntimeException("Huawei submit failed: " + retMap.get("msg") + " (code=" + code + ")");
}
}
}
}
private HttpHeaders huaweiHeaders(String clientId, String token) {
@ -698,6 +744,181 @@ public class StoreSubmissionService {
log.info("Google Play submission is handled by the release script or Play Console; server-side submission service only records the market link and review tracking");
}
// Cancel review (撤回审核)
/**
* Cancel pending store review for the specified stores.
* Calls each store's withdrawal API (best-effort) then marks DB state as WITHDRAWN.
*/
public void cancelStoreReview(String versionId, List<String> storeTypes) throws Exception {
AppVersionEntity v = versionRepo.findById(versionId)
.orElseThrow(() -> new IllegalArgumentException("Version not found: " + versionId));
List<String> targets = (storeTypes != null && !storeTypes.isEmpty())
? storeTypes
: extractActiveReviewTargets(v);
for (String storeType : targets) {
try {
AppStoreConfigEntity cfg = configRepo
.findByAppKeyAndStoreType(v.getAppKey(), AppStoreConfigEntity.StoreType.valueOf(storeType))
.orElse(null);
if (cfg != null && cfg.isEnabled()) {
try {
Map<String, String> creds = parseConfig(cfg.getConfigJson());
cancelAtStore(storeType, v, creds);
} catch (Exception e) {
log.warn("Cancel API call failed for {}/{}: {}", versionId, storeType, e.getMessage());
}
}
storeService.updateStoreReview(versionId, storeType,
AppVersionEntity.StoreReviewState.WITHDRAWN, "用户手动撤回");
} catch (Exception e) {
log.error("Cancel review failed for {}/{}: {}", versionId, storeType, e.getMessage(), e);
}
}
}
private List<String> extractActiveReviewTargets(AppVersionEntity v) {
if (v.getStoreReviewStatus() == null || v.getStoreReviewStatus().isBlank()) return List.of();
try {
@SuppressWarnings("unchecked")
Map<String, Map<String, Object>> reviewMap =
mapper.readValue(v.getStoreReviewStatus(), new TypeReference<>() {});
return reviewMap.entrySet().stream()
.filter(e -> {
Object state = e.getValue().get("state");
String s = state == null ? "" : state.toString();
return "PENDING".equals(s) || "SUBMITTING".equals(s) || "UNDER_REVIEW".equals(s);
})
.map(Map.Entry::getKey)
.toList();
} catch (Exception e) {
return List.of();
}
}
private void cancelAtStore(String storeType, AppVersionEntity v, Map<String, String> creds) throws Exception {
switch (storeType) {
case "HUAWEI" -> cancelAtHuawei(v, creds);
case "HONOR" -> cancelAtHonor(v, creds);
case "OPPO" -> cancelAtOppo(v, creds);
case "VIVO" -> cancelAtVivo(v, creds);
// MI: no public cancel API
default -> log.info("No cancel API for store {}, DB-only withdrawal", storeType);
}
}
private void cancelAtHuawei(AppVersionEntity v, Map<String, String> creds) {
String clientId = require(creds, "clientId", "HUAWEI");
String clientSecret = require(creds, "clientSecret", "HUAWEI");
String packageName = requirePackageName(v);
String token = huaweiGetToken(clientId, clientSecret);
String hwAppId = huaweiGetAppId(clientId, token, packageName);
HttpHeaders headers = huaweiHeaders(clientId, token);
rest.postForEntity(HUAWEI_API + "/api/publish/v2/developer-service/unSubmit?appId=" + hwAppId,
new HttpEntity<>(headers), Map.class);
log.info("HUAWEI unSubmit called for appId={}", hwAppId);
}
private void cancelAtHonor(AppVersionEntity v, Map<String, String> creds) {
String clientId = require(creds, "clientId", "HONOR");
String clientSecret = require(creds, "clientSecret", "HONOR");
String packageName = requirePackageName(v);
String token = honorGetToken(clientId, clientSecret);
int appId = honorGetAppId(token, packageName);
HttpHeaders headers = honorHeaders(token);
headers.setContentType(MediaType.APPLICATION_JSON);
ResponseEntity<Map> resp = rest.postForEntity(
HONOR_API + "/openapi/v1/publish/cancel-audit?appId=" + appId,
new HttpEntity<>(headers), Map.class);
assertHonorSuccess(resp.getBody(), "cancel-audit");
log.info("HONOR cancel-audit called for appId={}", appId);
}
private void cancelAtOppo(AppVersionEntity v, Map<String, String> creds) throws Exception {
String clientId = require(creds, "clientId", "OPPO");
String clientSecret = require(creds, "clientSecret", "OPPO");
String packageName = requirePackageName(v);
String token = oppoGetToken(clientId, clientSecret);
Map<String, String> params = new LinkedHashMap<>();
params.put("pkg_name", packageName);
params.put("version_code", String.valueOf(parseVersionCode(v.getVersionCode())));
String url = oppoRequestUrl(
"https://oop-openapi-cn.heytapmobi.com/developer/v1/app/cancel-audit-info",
params, token, true, clientSecret);
ResponseEntity<String> resp = rest.getForEntity(url, String.class);
JsonNode root = mapper.readTree(Objects.requireNonNull(resp.getBody()));
oppoCheckSuccess(root, "cancel-audit");
log.info("OPPO cancel-audit called for pkg={}", packageName);
}
private void cancelAtVivo(AppVersionEntity v, Map<String, String> creds) throws Exception {
String accessKey = require(creds, "accessKey", "VIVO");
String accessSecret = require(creds, "accessSecret", "VIVO");
String packageName = requirePackageName(v);
Map<String, String> params = new LinkedHashMap<>();
params.put("packageName", packageName);
String url = vivoRequestUrl(accessKey, accessSecret, "app.sync.withdraw.app", params);
ResponseEntity<String> resp = rest.getForEntity(url, String.class);
JsonNode root = mapper.readTree(Objects.requireNonNull(resp.getBody()));
vivoCheckSuccess(root, "撤回");
log.info("VIVO withdraw called for pkg={}", packageName);
}
// Modify publish schedule
/**
* Change when the approved version goes live.
* publishType: "IMMEDIATE" or "SCHEDULED"
* scheduledAt: required when publishType == "SCHEDULED"
*/
public AppVersionEntity updatePublishSchedule(String versionId, String publishType,
java.time.LocalDateTime scheduledAt) {
AppVersionEntity v = versionRepo.findById(versionId)
.orElseThrow(() -> new IllegalArgumentException("Version not found: " + versionId));
if ("IMMEDIATE".equalsIgnoreCase(publishType)) {
v.setScheduledPublishAt(null);
} else if ("SCHEDULED".equalsIgnoreCase(publishType)) {
if (scheduledAt == null) throw new IllegalArgumentException("scheduledAt required for SCHEDULED publish");
v.setScheduledPublishAt(scheduledAt);
}
// Try to call HUAWEI API to update release plan if token available
try {
AppStoreConfigEntity cfg = configRepo
.findByAppKeyAndStoreType(v.getAppKey(), AppStoreConfigEntity.StoreType.HUAWEI)
.orElse(null);
if (cfg != null && cfg.isEnabled()) {
Map<String, String> creds = parseConfig(cfg.getConfigJson());
huaweiUpdateReleasePlan(v, creds, publishType, scheduledAt);
}
} catch (Exception e) {
log.warn("HUAWEI release plan update failed (non-fatal): {}", e.getMessage());
}
return versionRepo.save(v);
}
private void huaweiUpdateReleasePlan(AppVersionEntity v, Map<String, String> creds,
String publishType, java.time.LocalDateTime scheduledAt) throws Exception {
String clientId = require(creds, "clientId", "HUAWEI");
String clientSecret = require(creds, "clientSecret", "HUAWEI");
String token = huaweiGetToken(clientId, clientSecret);
String hwAppId = huaweiGetAppId(clientId, token, v.getPackageName());
HttpHeaders headers = huaweiHeaders(clientId, token);
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> body = new LinkedHashMap<>();
if ("IMMEDIATE".equalsIgnoreCase(publishType)) {
body.put("releaseType", 1);
} else {
body.put("releaseType", 3);
body.put("releaseTime", scheduledAt.format(
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
rest.exchange(HUAWEI_API + "/api/publish/v2/app-info?appId=" + hwAppId,
HttpMethod.PUT, new HttpEntity<>(body, headers), Map.class);
log.info("HUAWEI release plan updated: type={} for appId={}", publishType, hwAppId);
}
// Utilities
private JsonNode miGetAppInfo(String account,