docs(sdk): 添加 Android SDK 文档和 API 设计规范

- 新增 Android SDK 使用文档,包含模块结构、集成方式和快速开始指南
- 添加 SDK API 重设计规范,统一初始化和登录接口设计
- 补充安全设计规范,完善 UserSig 鉴权和敏感数据处理方案
- 创建平台 REST API 规范,定义服务端到服务端的调用接口
- 添加离线推送架构设计,集成各大厂商推送服务与 IM 联动方案
这个提交包含在:
XuqmGroup 2026-04-29 15:46:40 +08:00
父节点 d7f5fd02c2
当前提交 c3968e808d
共有 10 个文件被更改,包括 71 次插入57 次删除

查看文件

@ -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&currentVersionCode=1'
curl 'https://dev.xuqinmin.com/api/v1/updates/app/check?appId=ak_demo_chat&platform=HARMONY&currentVersionCode=1'
```
Harmony 平台只跳转应用市场,不提供本地安装包下载。
### RN 热更新检查
```bash

查看文件

@ -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)));
}

查看文件

@ -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<RnBundleEntity> 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());

查看文件

@ -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());

查看文件

@ -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 }

查看文件

@ -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; }

查看文件

@ -5,6 +5,7 @@ public record RnBundleInspectResult(
String platform,
String version,
String minCommonVersion,
String packageName,
String fileName,
boolean detected) {
}

查看文件

@ -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) {
}
}

查看文件

@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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

查看文件

@ -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);
}