XuqmGroup-Server/update-service/src/main/java/com/xuqm/update/controller/AppVersionController.java
XuqmGroup f5a1eb4470 docs(sdk): 添加 React Native SDK 文档和 Android/HarmonyOS 发版脚本
- 新增 XuqmGroup React Native SDK 使用文档,包含安装、初始化、HTTP客户端、IM模块、推送模块、版本管理等功能说明
- 添加 Android Gradle 发版任务脚本,支持构建发布 APK 并上传到更新服务
- 添加 HarmonyOS hvigorw 发版任务脚本,支持 HAP 包构建和上传功能
- 实现多平台版本检查、自动重连、灰度发布等发版流程自动化
- 集成商店提交、定时发布、Webhook 回调等发布后处理功能
2026-04-29 17:35:52 +08:00

183 行
9.2 KiB
Java

package com.xuqm.update.controller;
import com.xuqm.common.model.ApiResponse;
import com.xuqm.update.entity.AppVersionEntity;
import com.xuqm.update.repository.AppVersionRepository;
import com.xuqm.update.model.AppPackageInspectResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import com.xuqm.update.service.UpdateAssetService;
@RestController
@RequestMapping("/api/v1/updates")
public class AppVersionController {
private static final Logger log = LoggerFactory.getLogger(AppVersionController.class);
private final AppVersionRepository versionRepository;
private final UpdateAssetService updateAssetService;
public AppVersionController(AppVersionRepository versionRepository, UpdateAssetService updateAssetService) {
this.versionRepository = versionRepository;
this.updateAssetService = updateAssetService;
}
@GetMapping("/app/check")
public ResponseEntity<ApiResponse<Map<String, Object>>> checkUpdate(
@RequestParam String appId,
@RequestParam AppVersionEntity.Platform platform,
@RequestParam int currentVersionCode) {
Optional<AppVersionEntity> latest = versionRepository
.findTopByAppIdAndPlatformAndPublishStatusOrderByVersionCodeDesc(
appId, platform, AppVersionEntity.PublishStatus.PUBLISHED);
if (latest.isEmpty() || latest.get().getVersionCode() <= currentVersionCode) {
return ResponseEntity.ok(ApiResponse.success(Map.of("needsUpdate", false)));
}
AppVersionEntity v = latest.get();
return ResponseEntity.ok(ApiResponse.success(Map.of(
"needsUpdate", true,
"versionName", v.getVersionName(),
"versionCode", v.getVersionCode(),
"downloadUrl", v.getDownloadUrl() != null ? v.getDownloadUrl() : "",
"changeLog", v.getChangeLog() != null ? v.getChangeLog() : "",
"forceUpdate", v.isForceUpdate(),
"appStoreUrl", v.getAppStoreUrl() != null ? v.getAppStoreUrl() : "",
"marketUrl", v.getMarketUrl() != null ? v.getMarketUrl() : ""
)));
}
@PostMapping("/app/upload")
public ResponseEntity<ApiResponse<AppVersionEntity>> upload(
@RequestParam String appId,
@RequestParam AppVersionEntity.Platform platform,
@RequestParam(required = false) String versionName,
@RequestParam(required = false) Integer versionCode,
@RequestParam(required = false) String changeLog,
@RequestParam(defaultValue = "false") boolean forceUpdate,
@RequestParam(required = false) String apkUrl,
@RequestParam(required = false) MultipartFile apkFile,
@RequestParam(required = false) String scheduledPublishAt,
@RequestParam(required = false) String webhookUrl,
@RequestParam(required = false) String storeSubmitTargets,
@RequestParam(defaultValue = "false") boolean autoPublishAfterReview,
@RequestParam(defaultValue = "false") boolean publishImmediately,
@RequestParam(required = false) String packageName,
@RequestParam(required = false) String appStoreUrl,
@RequestParam(required = false) String marketUrl) throws Exception {
AppPackageInspectResult inspected = null;
try {
inspected = hasText(apkUrl)
? updateAssetService.inspectAppPackage(apkUrl)
: (apkFile != null && !apkFile.isEmpty() ? updateAssetService.inspectAppPackage(apkFile) : null);
} catch (Exception ex) {
log.warn("Unable to inspect upload package for appId={}, platform={}, source={}, fallback to manual version fields: {}",
appId, platform, hasText(apkUrl) ? apkUrl : (apkFile != null ? apkFile.getOriginalFilename() : null), ex.getMessage());
}
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 && !hasText(apkUrl) && (apkFile == null || apkFile.isEmpty())) {
throw new IllegalArgumentException("apkUrl or 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());
entity.setAppId(appId);
entity.setPlatform(platform);
entity.setVersionName(resolvedVersionName);
entity.setVersionCode(resolvedVersionCode);
entity.setDownloadUrl(platform == AppVersionEntity.Platform.HARMONY
? null
: (hasText(apkUrl) ? apkUrl : updateAssetService.storeAppPackage(apkFile)));
entity.setChangeLog(changeLog);
entity.setForceUpdate(forceUpdate);
entity.setPublishStatus(AppVersionEntity.PublishStatus.DRAFT);
entity.setCreatedAt(LocalDateTime.now());
if (scheduledPublishAt != null && !scheduledPublishAt.isBlank()) {
entity.setScheduledPublishAt(LocalDateTime.parse(scheduledPublishAt));
}
entity.setWebhookUrl(webhookUrl);
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)));
}
@RequestMapping(value = "/app/inspect", method = {RequestMethod.GET, RequestMethod.POST})
public ResponseEntity<ApiResponse<AppPackageInspectResult>> inspect(
@RequestParam(required = false) String apkUrl,
@RequestParam(required = false) MultipartFile apkFile) throws Exception {
if (hasText(apkUrl)) {
return ResponseEntity.ok(ApiResponse.success(updateAssetService.inspectAppPackage(apkUrl)));
}
return ResponseEntity.ok(ApiResponse.success(updateAssetService.inspectAppPackage(apkFile)));
}
@PostMapping("/app/{id}/publish")
public ResponseEntity<ApiResponse<AppVersionEntity>> publish(@PathVariable String id) {
AppVersionEntity entity = versionRepository.findById(id).orElseThrow();
entity.setPublishStatus(AppVersionEntity.PublishStatus.PUBLISHED);
entity.setGrayEnabled(false);
entity.setGrayPercent(0);
return ResponseEntity.ok(ApiResponse.success(versionRepository.save(entity)));
}
@PostMapping("/app/{id}/unpublish")
public ResponseEntity<ApiResponse<AppVersionEntity>> unpublish(@PathVariable String id) {
AppVersionEntity entity = versionRepository.findById(id).orElseThrow();
entity.setPublishStatus(AppVersionEntity.PublishStatus.DEPRECATED);
return ResponseEntity.ok(ApiResponse.success(versionRepository.save(entity)));
}
@PostMapping("/app/{id}/gray")
public ResponseEntity<ApiResponse<AppVersionEntity>> gray(
@PathVariable String id,
@RequestBody Map<String, Object> body) {
AppVersionEntity entity = versionRepository.findById(id).orElseThrow();
entity.setGrayEnabled(Boolean.TRUE.equals(body.get("enabled")));
entity.setGrayPercent(body.get("percent") instanceof Number n ? n.intValue() : 0);
entity.setPublishStatus(AppVersionEntity.PublishStatus.PUBLISHED);
return ResponseEntity.ok(ApiResponse.success(versionRepository.save(entity)));
}
@GetMapping("/app/list")
public ResponseEntity<ApiResponse<List<AppVersionEntity>>> list(
@RequestParam String appId, @RequestParam AppVersionEntity.Platform platform) {
return ResponseEntity.ok(ApiResponse.success(
versionRepository.findByAppIdAndPlatformOrderByVersionCodeDesc(appId, platform)));
}
private boolean hasText(String value) {
return value != null && !value.isBlank();
}
}