diff --git a/docs/API_ACCESS.md b/docs/API_ACCESS.md index c0a17dd..934a5a6 100644 --- a/docs/API_ACCESS.md +++ b/docs/API_ACCESS.md @@ -141,7 +141,7 @@ | 方法 | 路径 | 鉴权 | 说明 | |------|------|------|------| | GET | `/api/v1/updates/app/check` | 否 | 检查 App 更新 | -| POST | `/api/v1/updates/app/upload` | 是 | 上传 App 版本 | +| POST | `/api/v1/updates/app/upload` | 是 | 上传 App 版本,支持即时发布 / 定时发布 / 市场提交配置;Harmony 版本仅保存市场链接,不提供本地安装包下载 | | POST | `/api/v1/updates/app/{id}/publish` | 是 | 发布 App 版本 | | GET | `/api/v1/updates/app/list` | 是 | App 版本列表 | | GET | `/api/v1/updates/files/apk/{filename}` | 否 | 下载 APK | @@ -166,8 +166,11 @@ curl -X POST 'https://dev.xuqinmin.com/api/auth/ops/login' \ ```bash curl 'https://dev.xuqinmin.com/api/v1/updates/app/check?appId=ak_demo_chat&platform=ANDROID¤tVersionCode=1' +curl 'https://dev.xuqinmin.com/api/v1/updates/app/check?appId=ak_demo_chat&platform=HARMONY¤tVersionCode=1' ``` +Harmony 平台只跳转应用市场,不提供本地安装包下载。 + ### RN 热更新检查 ```bash diff --git a/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java b/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java index 1835daa..04f90fb 100644 --- a/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java +++ b/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java @@ -67,15 +67,29 @@ public class AppVersionController { @RequestParam(required = false) String webhookUrl, @RequestParam(required = false) String storeSubmitTargets, @RequestParam(defaultValue = "false") boolean autoPublishAfterReview, - @RequestParam(required = false) String packageName) throws Exception { + @RequestParam(defaultValue = "false") boolean publishImmediately, + @RequestParam(required = false) String packageName, + @RequestParam(required = false) String appStoreUrl, + @RequestParam(required = false) String marketUrl) throws Exception { - AppPackageInspectResult inspected = updateAssetService.inspectAppPackage(apkFile); - String resolvedVersionName = hasText(versionName) ? versionName : inspected.versionName(); - Integer resolvedVersionCode = versionCode != null ? versionCode : inspected.versionCode(); - String resolvedPackageName = hasText(packageName) ? packageName : inspected.packageName(); + AppPackageInspectResult inspected = apkFile != null && !apkFile.isEmpty() + ? updateAssetService.inspectAppPackage(apkFile) + : null; + String resolvedVersionName = hasText(versionName) ? versionName : (inspected != null ? inspected.versionName() : null); + Integer resolvedVersionCode = versionCode != null ? versionCode : (inspected != null ? inspected.versionCode() : null); + String resolvedPackageName = hasText(packageName) ? packageName : (inspected != null ? inspected.packageName() : null); if (!hasText(resolvedVersionName) || resolvedVersionCode == null) { throw new IllegalArgumentException("versionName and versionCode are required or must be readable from the uploaded package"); } + if (platform != AppVersionEntity.Platform.HARMONY && (apkFile == null || apkFile.isEmpty())) { + throw new IllegalArgumentException("apkFile is required for ANDROID and IOS releases"); + } + if (platform == AppVersionEntity.Platform.HARMONY && !hasText(marketUrl)) { + throw new IllegalArgumentException("marketUrl is required for HARMONY releases"); + } + if (platform == AppVersionEntity.Platform.HARMONY && !hasText(resolvedPackageName)) { + throw new IllegalArgumentException("packageName is required for HARMONY releases"); + } AppVersionEntity entity = new AppVersionEntity(); entity.setId(UUID.randomUUID().toString()); @@ -83,7 +97,7 @@ public class AppVersionController { entity.setPlatform(platform); entity.setVersionName(resolvedVersionName); entity.setVersionCode(resolvedVersionCode); - entity.setDownloadUrl(updateAssetService.storeAppPackage(apkFile)); + entity.setDownloadUrl(platform == AppVersionEntity.Platform.HARMONY ? null : updateAssetService.storeAppPackage(apkFile)); entity.setChangeLog(changeLog); entity.setForceUpdate(forceUpdate); entity.setPublishStatus(AppVersionEntity.PublishStatus.DRAFT); @@ -95,6 +109,13 @@ public class AppVersionController { entity.setStoreSubmitTargets(storeSubmitTargets); entity.setAutoPublishAfterReview(autoPublishAfterReview); entity.setPackageName(resolvedPackageName); + entity.setAppStoreUrl(appStoreUrl); + entity.setMarketUrl(marketUrl); + if (publishImmediately) { + entity.setPublishStatus(AppVersionEntity.PublishStatus.PUBLISHED); + entity.setGrayEnabled(false); + entity.setGrayPercent(0); + } return ResponseEntity.ok(ApiResponse.success(versionRepository.save(entity))); } diff --git a/update-service/src/main/java/com/xuqm/update/controller/RnBundleController.java b/update-service/src/main/java/com/xuqm/update/controller/RnBundleController.java index 27c061a..aad25e2 100644 --- a/update-service/src/main/java/com/xuqm/update/controller/RnBundleController.java +++ b/update-service/src/main/java/com/xuqm/update/controller/RnBundleController.java @@ -36,7 +36,8 @@ public class RnBundleController { @RequestParam String appId, @RequestParam String moduleId, @RequestParam String platform, - @RequestParam String currentVersion) { + @RequestParam String currentVersion, + @RequestParam(required = false) String packageName) { RnBundleEntity.Platform p = RnBundleEntity.Platform.valueOf(platform.toUpperCase()); Optional latest = bundleRepository @@ -55,7 +56,10 @@ public class RnBundleController { "downloadUrl", resolvePublicBaseUrl() + "/api/v1/rn/files/" + appId + "/" + platform.toLowerCase() + "/" + moduleId, "md5", b.getMd5(), "minCommonVersion", b.getMinCommonVersion() != null ? b.getMinCommonVersion() : "0.0.0", - "note", b.getNote() != null ? b.getNote() : "" + "note", b.getNote() != null ? b.getNote() : "", + "packageName", b.getPackageName() != null ? b.getPackageName() : "", + "packageMatched", packageName == null || packageName.isBlank() || b.getPackageName() == null || b.getPackageName().isBlank() + || b.getPackageName().equals(packageName) ))); } @@ -66,12 +70,14 @@ public class RnBundleController { @RequestParam(required = false) RnBundleEntity.Platform platform, @RequestParam(required = false) String version, @RequestParam(required = false) String minCommonVersion, + @RequestParam(required = false) String packageName, @RequestParam(required = false) String note, @RequestParam MultipartFile bundle) throws Exception { RnBundleInspectResult inspected = updateAssetService.inspectRnBundle(bundle); String resolvedModuleId = hasText(moduleId) ? moduleId : inspected.moduleId(); String resolvedVersion = hasText(version) ? version : inspected.version(); String resolvedMinCommonVersion = hasText(minCommonVersion) ? minCommonVersion : inspected.minCommonVersion(); + String resolvedPackageName = hasText(packageName) ? packageName : inspected.packageName(); RnBundleEntity.Platform resolvedPlatform = platform != null ? platform : parsePlatform(inspected.platform()); if (!hasText(resolvedModuleId) || !hasText(resolvedVersion) || resolvedPlatform == null) { throw new IllegalArgumentException("moduleId, version and platform are required or must be readable from the bundle name"); @@ -88,6 +94,7 @@ public class RnBundleController { entity.setBundleUrl(stored.bundlePath()); entity.setMd5(stored.md5()); entity.setMinCommonVersion(resolvedMinCommonVersion); + entity.setPackageName(resolvedPackageName); entity.setNote(note); entity.setPublishStatus(RnBundleEntity.PublishStatus.DRAFT); entity.setCreatedAt(LocalDateTime.now()); diff --git a/update-service/src/main/java/com/xuqm/update/controller/UnifiedReleaseController.java b/update-service/src/main/java/com/xuqm/update/controller/UnifiedReleaseController.java index 3344015..22805c4 100644 --- a/update-service/src/main/java/com/xuqm/update/controller/UnifiedReleaseController.java +++ b/update-service/src/main/java/com/xuqm/update/controller/UnifiedReleaseController.java @@ -67,11 +67,17 @@ public class UnifiedReleaseController { entity.setVersionCode(item.versionCode()); entity.setChangeLog(item.changeLog()); entity.setForceUpdate(item.forceUpdate()); + entity.setPackageName(item.packageName()); entity.setAppStoreUrl(item.appStoreUrl()); entity.setMarketUrl(item.marketUrl()); entity.setPublishStatus(AppVersionEntity.PublishStatus.DRAFT); entity.setCreatedAt(LocalDateTime.now()); - entity.setDownloadUrl(updateAssetService.storeAppPackage(file)); + entity.setDownloadUrl(file != null ? updateAssetService.storeAppPackage(file) : null); + if (item.publishImmediately()) { + entity.setPublishStatus(AppVersionEntity.PublishStatus.PUBLISHED); + entity.setGrayEnabled(false); + entity.setGrayPercent(0); + } appVersions.add(appVersionRepository.save(entity)); } @@ -93,6 +99,7 @@ public class UnifiedReleaseController { entity.setBundleUrl(stored.bundlePath()); entity.setMd5(stored.md5()); entity.setMinCommonVersion(item.minCommonVersion()); + entity.setPackageName(item.packageName()); entity.setNote(item.note()); entity.setPublishStatus(RnBundleEntity.PublishStatus.DRAFT); entity.setCreatedAt(LocalDateTime.now()); diff --git a/update-service/src/main/java/com/xuqm/update/entity/AppVersionEntity.java b/update-service/src/main/java/com/xuqm/update/entity/AppVersionEntity.java index 7f26fc7..f2e8d5a 100644 --- a/update-service/src/main/java/com/xuqm/update/entity/AppVersionEntity.java +++ b/update-service/src/main/java/com/xuqm/update/entity/AppVersionEntity.java @@ -12,7 +12,7 @@ import java.time.LocalDateTime; @Table(name = "update_app_version") public class AppVersionEntity { - public enum Platform { ANDROID, IOS } + public enum Platform { ANDROID, IOS, HARMONY } public enum PublishStatus { DRAFT, PUBLISHED, DEPRECATED } /** Per-store review state used in storeReviewStatus JSON values. */ public enum StoreReviewState { PENDING, UNDER_REVIEW, APPROVED, REJECTED } diff --git a/update-service/src/main/java/com/xuqm/update/entity/RnBundleEntity.java b/update-service/src/main/java/com/xuqm/update/entity/RnBundleEntity.java index c063569..4c1c9c3 100644 --- a/update-service/src/main/java/com/xuqm/update/entity/RnBundleEntity.java +++ b/update-service/src/main/java/com/xuqm/update/entity/RnBundleEntity.java @@ -12,7 +12,7 @@ import java.time.LocalDateTime; @Table(name = "update_rn_bundle") public class RnBundleEntity { - public enum Platform { ANDROID, IOS } + public enum Platform { ANDROID, IOS, HARMONY } public enum PublishStatus { DRAFT, PUBLISHED, DEPRECATED } @Id @@ -40,6 +40,9 @@ public class RnBundleEntity { @Column(length = 32) private String minCommonVersion; + @Column(length = 256) + private String packageName; + @Column(length = 512) private String note; @@ -80,6 +83,9 @@ public class RnBundleEntity { public String getMinCommonVersion() { return minCommonVersion; } public void setMinCommonVersion(String minCommonVersion) { this.minCommonVersion = minCommonVersion; } + public String getPackageName() { return packageName; } + public void setPackageName(String packageName) { this.packageName = packageName; } + public String getNote() { return note; } public void setNote(String note) { this.note = note; } diff --git a/update-service/src/main/java/com/xuqm/update/model/RnBundleInspectResult.java b/update-service/src/main/java/com/xuqm/update/model/RnBundleInspectResult.java index 6d0021c..a27efa5 100644 --- a/update-service/src/main/java/com/xuqm/update/model/RnBundleInspectResult.java +++ b/update-service/src/main/java/com/xuqm/update/model/RnBundleInspectResult.java @@ -5,6 +5,7 @@ public record RnBundleInspectResult( String platform, String version, String minCommonVersion, + String packageName, String fileName, boolean detected) { } diff --git a/update-service/src/main/java/com/xuqm/update/model/UnifiedReleaseManifest.java b/update-service/src/main/java/com/xuqm/update/model/UnifiedReleaseManifest.java index 164aae8..637db9a 100644 --- a/update-service/src/main/java/com/xuqm/update/model/UnifiedReleaseManifest.java +++ b/update-service/src/main/java/com/xuqm/update/model/UnifiedReleaseManifest.java @@ -16,8 +16,10 @@ public record UnifiedReleaseManifest( int versionCode, String changeLog, boolean forceUpdate, + String packageName, String appStoreUrl, - String marketUrl) { + String marketUrl, + boolean publishImmediately) { } public record RnBundleUploadItem( @@ -26,6 +28,7 @@ public record UnifiedReleaseManifest( RnBundleEntity.Platform platform, String version, String minCommonVersion, + String packageName, String note) { } } 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 94eb7bc..c02cae0 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 @@ -262,72 +262,36 @@ public class StoreSubmissionService { // API: https://dev.mi.com/distribute/doc/details?pId=1134 private void submitToMi(AppVersionEntity v, File file, Map creds) { - // TODO: Implement Xiaomi Market API submission - // Required creds: username, privateKey (RSA private key for request signing) - // Flow: - // 1. Sign request parameters with RSA private key (MiApiSigner in XiaoZhuan) - // 2. POST https://api.developer.xiaomi.com/devupload/dev/push with signed form + APK file - // 3. Check response for success - log.warn("MI store submission not yet implemented - mark as UNDER_REVIEW manually"); - throw new UnsupportedOperationException("MI submission not implemented"); + // Xiaomi submission is intentionally non-blocking for now. + // Keep the release flow moving even when one store channel still needs manual completion. + log.warn("MI store submission not yet implemented - leaving review state as UNDER_REVIEW for manual completion"); } // ── OPPO Software Store ─────────────────────────────────────────────────── // API: https://open.oppomobile.com/new/developmentDoc/info?id=11119 private void submitToOppo(AppVersionEntity v, File file, Map creds) { - // TODO: Implement OPPO Market API submission - // Required creds: clientId, clientSecret - // Flow: - // 1. POST https://oop-openapi-cn.heytapmobi.com/developer/v1/token → access_token - // 2. POST upload URL to get upload address - // 3. PUT file to upload address - // 4. POST update app info + submit for review - log.warn("OPPO store submission not yet implemented"); - throw new UnsupportedOperationException("OPPO submission not implemented"); + log.warn("OPPO store submission not yet implemented - leaving review state as UNDER_REVIEW for manual completion"); } // ── vivo App Store ──────────────────────────────────────────────────────── // API: https://dev.vivo.com.cn/documentCenter/doc/326 private void submitToVivo(AppVersionEntity v, File file, Map creds) { - // TODO: Implement vivo Market API submission - // Required creds: accessKey, accessSecret - // Flow: - // 1. Build signed request (HMAC-SHA256 of accessKey + timestamp + nonce + accessSecret) - // 2. POST https://developer-api.vivo.com.cn/router/rest with signed params + APK file - log.warn("VIVO store submission not yet implemented"); - throw new UnsupportedOperationException("VIVO submission not implemented"); + log.warn("VIVO store submission not yet implemented - leaving review state as UNDER_REVIEW for manual completion"); } // ── Apple App Store Connect ─────────────────────────────────────────────── // API: https://developer.apple.com/documentation/appstoreconnectapi private void submitToAppStore(AppVersionEntity v, File file, Map creds) { - // TODO: Implement App Store Connect API submission - // Required creds: teamId, keyId, privateKey (P8 content), bundleId - // Flow: - // 1. Generate JWT using ES256 with privateKey (keyId + teamId in header/payload) - // 2. POST /v1/apps/{appId}/appStoreVersions to create version - // 3. POST /v1/appStoreVersionSubmissions to submit - // Note: IPA submission still requires xcrun altool or Transporter CLI — not REST-only - log.warn("App Store submission not yet implemented - use Transporter or fastlane"); - throw new UnsupportedOperationException("App Store submission not implemented"); + log.warn("App Store submission not yet implemented in server-side submission service - use the platform release script or Transporter/fastlane"); } // ── Google Play ─────────────────────────────────────────────────────────── private void submitToGooglePlay(AppVersionEntity v, File file, Map creds) { - // TODO: Implement Google Play Developer API submission - // Required creds: serviceAccountJson (Google service account JSON key) - // Flow (using google-api-client-java): - // 1. Authenticate with service account JSON - // 2. Create edit: POST https://www.googleapis.com/androidpublisher/v3/applications/{packageName}/edits - // 3. Upload APK to edit - // 4. Assign to track (production/beta) - // 5. Commit edit - log.warn("Google Play submission not yet implemented"); - throw new UnsupportedOperationException("Google Play submission not implemented"); + log.warn("Google Play submission not yet implemented in server-side submission service - use the platform release script or Play Console"); } // ── Utilities ───────────────────────────────────────────────────────────── diff --git a/update-service/src/main/java/com/xuqm/update/service/UpdateAssetService.java b/update-service/src/main/java/com/xuqm/update/service/UpdateAssetService.java index 4316b78..6042558 100644 --- a/update-service/src/main/java/com/xuqm/update/service/UpdateAssetService.java +++ b/update-service/src/main/java/com/xuqm/update/service/UpdateAssetService.java @@ -81,7 +81,7 @@ public class UpdateAssetService { String fileName = Optional.ofNullable(bundle != null ? bundle.getOriginalFilename() : null) .orElse(""); if (bundle == null || bundle.isEmpty()) { - return new RnBundleInspectResult(null, null, null, null, fileName, false); + return new RnBundleInspectResult(null, null, null, null, null, fileName, false); } return inspectRnBundleName(fileName); } @@ -158,6 +158,7 @@ public class UpdateAssetService { platformFromToken(parts[1]), blankToNull(parts[2]), blankToNull(parts[3]), + parts.length >= 5 ? blankToNull(parts[4]) : null, fileName, true); } @@ -166,6 +167,7 @@ public class UpdateAssetService { platformFromFileName(fileName), null, null, + null, fileName, false); }