一大波改动
这个提交包含在:
父节点
bc1165d22e
当前提交
0ed09a8229
@ -114,18 +114,19 @@ public class AppStoreController {
|
|||||||
|
|
||||||
List<String> storeTypes = body != null ? extractStringList(body, "storeTypes") : null;
|
List<String> storeTypes = body != null ? extractStringList(body, "storeTypes") : null;
|
||||||
String submitMode = body != null ? (String) body.get("submitMode") : 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 autoPublishAfterReview = body != null && body.get("autoPublishAfterReview") != null
|
||||||
? Boolean.valueOf(body.get("autoPublishAfterReview").toString()) : null;
|
? Boolean.valueOf(body.get("autoPublishAfterReview").toString()) : null;
|
||||||
java.time.LocalDateTime scheduledAt = null;
|
java.time.LocalDateTime scheduledPublishAt = null;
|
||||||
if (scheduledAtText != null && !scheduledAtText.isBlank()) {
|
if (scheduledPublishAtText != null && !scheduledPublishAtText.isBlank()) {
|
||||||
scheduledAt = java.time.LocalDateTime.parse(scheduledAtText);
|
scheduledPublishAt = java.time.LocalDateTime.parse(scheduledPublishAtText);
|
||||||
}
|
}
|
||||||
AppVersionEntity v = storeService.markSubmitted(versionId,
|
AppVersionEntity v = storeService.markSubmitted(versionId,
|
||||||
storeTypes,
|
storeTypes,
|
||||||
submitMode,
|
submitMode,
|
||||||
scheduledAt,
|
null,
|
||||||
autoPublishAfterReview);
|
autoPublishAfterReview,
|
||||||
|
scheduledPublishAt);
|
||||||
String normalizedMode = submitMode == null ? "MANUAL" : submitMode.trim().toUpperCase();
|
String normalizedMode = submitMode == null ? "MANUAL" : submitMode.trim().toUpperCase();
|
||||||
if (!"SCHEDULED".equals(normalizedMode)) {
|
if (!"SCHEDULED".equals(normalizedMode)) {
|
||||||
submissionService.executeSubmitAsync(versionId);
|
submissionService.executeSubmitAsync(versionId);
|
||||||
@ -161,6 +162,41 @@ public class AppStoreController {
|
|||||||
storeService.updateStoreReview(versionId, storeType, state, reason)));
|
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) {
|
private List<String> extractStringList(Map<String, Object> body, String key) {
|
||||||
Object value = body.get(key);
|
Object value = body.get(key);
|
||||||
if (value instanceof List<?> list) {
|
if (value instanceof List<?> list) {
|
||||||
|
|||||||
@ -115,6 +115,15 @@ public class AppStoreService {
|
|||||||
String submitMode,
|
String submitMode,
|
||||||
LocalDateTime scheduledAt,
|
LocalDateTime scheduledAt,
|
||||||
Boolean autoPublishAfterReview) throws Exception {
|
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)) {
|
synchronized (lockFor(versionId)) {
|
||||||
AppVersionEntity v = versionRepo.findById(versionId).orElseThrow();
|
AppVersionEntity v = versionRepo.findById(versionId).orElseThrow();
|
||||||
List<String> resolvedTargets = normalizeTargets(v.getAppKey(), storeTypes);
|
List<String> resolvedTargets = normalizeTargets(v.getAppKey(), storeTypes);
|
||||||
@ -130,7 +139,8 @@ public class AppStoreService {
|
|||||||
// Withdraw lower versions' active reviews for the same stores
|
// Withdraw lower versions' active reviews for the same stores
|
||||||
cancelSupersededVersionReviews(v.getAppKey(), v.getPlatform(), versionId, v.getVersionCode(), resolvedTargets);
|
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) {
|
for (String store : resolvedTargets) {
|
||||||
reviewMap.put(store, reviewPayload(
|
reviewMap.put(store, reviewPayload(
|
||||||
AppVersionEntity.StoreReviewState.PENDING.name(),
|
AppVersionEntity.StoreReviewState.PENDING.name(),
|
||||||
@ -144,6 +154,9 @@ public class AppStoreService {
|
|||||||
v.setStoreReviewStatus(mapper.writeValueAsString(reviewMap));
|
v.setStoreReviewStatus(mapper.writeValueAsString(reviewMap));
|
||||||
v.setStoreSubmitMode(normalizedMode);
|
v.setStoreSubmitMode(normalizedMode);
|
||||||
v.setStoreSubmitScheduledAt(scheduledAt);
|
v.setStoreSubmitScheduledAt(scheduledAt);
|
||||||
|
if (scheduledPublishAt != null) {
|
||||||
|
v.setScheduledPublishAt(scheduledPublishAt);
|
||||||
|
}
|
||||||
if (autoPublishAfterReview != null) {
|
if (autoPublishAfterReview != null) {
|
||||||
v.setAutoPublishAfterReview(autoPublishAfterReview && !"SCHEDULED".equals(normalizedMode));
|
v.setAutoPublishAfterReview(autoPublishAfterReview && !"SCHEDULED".equals(normalizedMode));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -324,12 +324,23 @@ public class StoreSubmissionService {
|
|||||||
// 5. Bind APK
|
// 5. Bind APK
|
||||||
String pkgId = huaweiBindApk(clientId, token, hwAppId, file.getName(), objectId);
|
String pkgId = huaweiBindApk(clientId, token, hwAppId, file.getName(), objectId);
|
||||||
|
|
||||||
// 6. Wait for compile (poll up to 3 minutes)
|
// 6. Brief compile check (up to 5 min) — HUAWEI queues compile+review internally so
|
||||||
huaweiWaitCompile(clientId, token, hwAppId, pkgId);
|
// we submit regardless of compile status after the grace period.
|
||||||
|
huaweiWaitCompileOrContinue(clientId, token, hwAppId, pkgId);
|
||||||
|
|
||||||
// 7. Update version description
|
// 7. Update version description
|
||||||
huaweiUpdateDesc(clientId, token, hwAppId, v.getChangeLog());
|
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
|
// 8. Submit for review
|
||||||
huaweiSubmit(clientId, token, hwAppId);
|
huaweiSubmit(clientId, token, hwAppId);
|
||||||
}
|
}
|
||||||
@ -436,28 +447,50 @@ public class StoreSubmissionService {
|
|||||||
return pkgId;
|
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")
|
@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);
|
HttpHeaders headers = huaweiHeaders(clientId, token);
|
||||||
long deadline = System.currentTimeMillis() + 3 * 60_000L;
|
long deadline = System.currentTimeMillis() + 5 * 60_000L;
|
||||||
while (System.currentTimeMillis() < deadline) {
|
while (System.currentTimeMillis() < deadline) {
|
||||||
Thread.sleep(10_000);
|
Thread.sleep(15_000);
|
||||||
ResponseEntity<Map> resp = rest.exchange(
|
try {
|
||||||
HUAWEI_API + "/api/publish/v2/package/compile/status?appId=" + hwAppId + "&pkgIds=" + pkgId,
|
ResponseEntity<Map> resp = rest.exchange(
|
||||||
HttpMethod.GET, new HttpEntity<>(headers), Map.class);
|
HUAWEI_API + "/api/publish/v2/package/compile/status?appId=" + hwAppId + "&pkgIds=" + pkgId,
|
||||||
Map<String, Object> respBody = requireBodyMap(resp.getBody(), "Huawei compile status");
|
HttpMethod.GET, new HttpEntity<>(headers), Map.class);
|
||||||
List<Map<String, Object>> states = asMapList(respBody.get("pkgStateList"));
|
Map<String, Object> respBody = requireBodyMap(resp.getBody(), "Huawei compile status");
|
||||||
if (states.isEmpty()) {
|
log.info("Huawei compile status raw: {}", summarizeMap(respBody));
|
||||||
states = asMapList(respBody.get("data"));
|
List<Map<String, Object>> states = asMapList(respBody.get("pkgStateList"));
|
||||||
}
|
if (states.isEmpty()) states = asMapList(respBody.get("data"));
|
||||||
if (states != null && !states.isEmpty()) {
|
if (!states.isEmpty()) {
|
||||||
Object compileStatus = states.get(0).get("compileStatus");
|
Map<String, Object> s0 = states.get(0);
|
||||||
if ("2".equals(String.valueOf(compileStatus))) return; // 2 = success
|
// HUAWEI compile status API uses successStatus (0=pending,1=success,2=failed)
|
||||||
if ("3".equals(String.valueOf(compileStatus)))
|
// or legacy compileStatus (2=done,3=failed). Try both.
|
||||||
throw new RuntimeException("Huawei compile failed: " + summarizeMap(states.get(0)));
|
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(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) {
|
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);
|
HttpMethod.PUT, new HttpEntity<>(body, headers), Map.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
private void huaweiSubmit(String clientId, String token, String hwAppId) {
|
private void huaweiSubmit(String clientId, String token, String hwAppId) {
|
||||||
HttpHeaders headers = huaweiHeaders(clientId, token);
|
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);
|
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) {
|
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");
|
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 ─────────────────────────────────────────────────────────────
|
// ── Utilities ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private JsonNode miGetAppInfo(String account,
|
private JsonNode miGetAppInfo(String account,
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户