package com.xuqm.update.controller; import com.xuqm.common.model.ApiResponse; import com.xuqm.update.entity.RnBundleEntity; import com.xuqm.update.repository.RnBundleRepository; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.DigestInputStream; import java.security.MessageDigest; import java.time.LocalDateTime; import java.util.HexFormat; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; @RestController @RequestMapping("/api/v1/rn") public class RnBundleController { private final RnBundleRepository bundleRepository; @Value("${update.upload-dir:/tmp/xuqm-update}") private String uploadDir; @Value("${update.base-url:http://localhost:8084}") private String baseUrl; public RnBundleController(RnBundleRepository bundleRepository) { this.bundleRepository = bundleRepository; } @GetMapping("/update/check") public ResponseEntity>> checkUpdate( @RequestParam String appId, @RequestParam String moduleId, @RequestParam String platform, @RequestParam String currentVersion) { RnBundleEntity.Platform p = RnBundleEntity.Platform.valueOf(platform.toUpperCase()); Optional latest = bundleRepository .findTopByAppIdAndModuleIdAndPlatformAndPublishStatusOrderByCreatedAtDesc( appId, moduleId, p, RnBundleEntity.PublishStatus.PUBLISHED); if (latest.isEmpty()) { return ResponseEntity.ok(ApiResponse.success(Map.of("needsUpdate", false))); } RnBundleEntity b = latest.get(); boolean needsUpdate = !b.getVersion().equals(currentVersion); return ResponseEntity.ok(ApiResponse.success(Map.of( "needsUpdate", needsUpdate, "latestVersion", b.getVersion(), "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() : "" ))); } @PostMapping("/upload") public ResponseEntity> upload( @RequestParam String appId, @RequestParam String moduleId, @RequestParam RnBundleEntity.Platform platform, @RequestParam String version, @RequestParam(required = false) String minCommonVersion, @RequestParam(required = false) String note, @RequestParam MultipartFile bundle) throws Exception { String filename = moduleId + "." + platform.name().toLowerCase() + ".bundle"; Path dir = Paths.get(uploadDir, "rn", appId, platform.name().toLowerCase(), moduleId); Files.createDirectories(dir); Path dest = dir.resolve(filename); String md5 = computeMd5(bundle); bundle.transferTo(dest.toFile()); RnBundleEntity entity = new RnBundleEntity(); entity.setId(UUID.randomUUID().toString()); entity.setAppId(appId); entity.setModuleId(moduleId); entity.setPlatform(platform); entity.setVersion(version); entity.setBundleUrl(dest.toAbsolutePath().toString()); entity.setMd5(md5); entity.setMinCommonVersion(minCommonVersion); entity.setNote(note); entity.setPublishStatus(RnBundleEntity.PublishStatus.DRAFT); entity.setCreatedAt(LocalDateTime.now()); return ResponseEntity.ok(ApiResponse.success(bundleRepository.save(entity))); } @GetMapping("/list") public ResponseEntity>> list( @RequestParam String appId, @RequestParam(required = false) String moduleId, @RequestParam(required = false) String platform) { List result; if (moduleId != null && platform != null) { RnBundleEntity.Platform p = RnBundleEntity.Platform.valueOf(platform.toUpperCase()); result = bundleRepository.findByAppIdAndModuleIdAndPlatformOrderByCreatedAtDesc(appId, moduleId, p); } else if (moduleId != null) { result = bundleRepository.findByAppIdAndModuleIdOrderByCreatedAtDesc(appId, moduleId); } else { result = bundleRepository.findByAppIdOrderByCreatedAtDesc(appId); } return ResponseEntity.ok(ApiResponse.success(result)); } @PostMapping("/{id}/publish") public ResponseEntity> publish(@PathVariable String id) { RnBundleEntity entity = bundleRepository.findById(id).orElseThrow(); entity.setPublishStatus(RnBundleEntity.PublishStatus.PUBLISHED); entity.setGrayEnabled(false); entity.setGrayPercent(0); return ResponseEntity.ok(ApiResponse.success(bundleRepository.save(entity))); } @PostMapping("/{id}/unpublish") public ResponseEntity> unpublish(@PathVariable String id) { RnBundleEntity entity = bundleRepository.findById(id).orElseThrow(); entity.setPublishStatus(RnBundleEntity.PublishStatus.DEPRECATED); return ResponseEntity.ok(ApiResponse.success(bundleRepository.save(entity))); } @PostMapping("/{id}/gray") public ResponseEntity> gray( @PathVariable String id, @RequestBody Map body) { RnBundleEntity entity = bundleRepository.findById(id).orElseThrow(); entity.setGrayEnabled(Boolean.TRUE.equals(body.get("enabled"))); entity.setGrayPercent(body.get("percent") instanceof Number n ? n.intValue() : 0); entity.setPublishStatus(RnBundleEntity.PublishStatus.PUBLISHED); return ResponseEntity.ok(ApiResponse.success(bundleRepository.save(entity))); } private String computeMd5(MultipartFile file) throws Exception { MessageDigest digest = MessageDigest.getInstance("MD5"); try (DigestInputStream dis = new DigestInputStream(file.getInputStream(), digest)) { byte[] buf = new byte[8192]; while (dis.read(buf) != -1) {} } return HexFormat.of().formatHex(digest.digest()); } private String resolvePublicBaseUrl() { String normalized = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; String suffix = "/api/v1/updates"; if (normalized.endsWith(suffix)) { return normalized.substring(0, normalized.length() - suffix.length()); } return normalized; } }