2026-04-21 22:07:29 +08:00
|
|
|
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;
|
2026-04-24 16:16:33 +08:00
|
|
|
import org.springframework.web.bind.annotation.*;
|
2026-04-21 22:07:29 +08:00
|
|
|
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;
|
2026-04-24 16:16:33 +08:00
|
|
|
import java.util.List;
|
2026-04-21 22:07:29 +08:00
|
|
|
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<ApiResponse<Map<String, Object>>> checkUpdate(
|
|
|
|
|
@RequestParam String appId,
|
|
|
|
|
@RequestParam String moduleId,
|
|
|
|
|
@RequestParam String platform,
|
|
|
|
|
@RequestParam String currentVersion) {
|
|
|
|
|
|
|
|
|
|
RnBundleEntity.Platform p = RnBundleEntity.Platform.valueOf(platform.toUpperCase());
|
|
|
|
|
Optional<RnBundleEntity> 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(),
|
2026-04-24 10:42:11 +08:00
|
|
|
"downloadUrl", resolvePublicBaseUrl() + "/api/v1/rn/files/" + appId + "/" + platform.toLowerCase() + "/" + moduleId,
|
2026-04-21 22:07:29 +08:00
|
|
|
"md5", b.getMd5(),
|
|
|
|
|
"minCommonVersion", b.getMinCommonVersion() != null ? b.getMinCommonVersion() : "0.0.0",
|
|
|
|
|
"note", b.getNote() != null ? b.getNote() : ""
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/upload")
|
|
|
|
|
public ResponseEntity<ApiResponse<RnBundleEntity>> 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)));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 16:16:33 +08:00
|
|
|
@GetMapping("/list")
|
|
|
|
|
public ResponseEntity<ApiResponse<List<RnBundleEntity>>> list(
|
|
|
|
|
@RequestParam String appId,
|
|
|
|
|
@RequestParam(required = false) String moduleId,
|
|
|
|
|
@RequestParam(required = false) String platform) {
|
|
|
|
|
List<RnBundleEntity> 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));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 22:07:29 +08:00
|
|
|
@PostMapping("/{id}/publish")
|
|
|
|
|
public ResponseEntity<ApiResponse<RnBundleEntity>> publish(@PathVariable String id) {
|
|
|
|
|
RnBundleEntity entity = bundleRepository.findById(id).orElseThrow();
|
|
|
|
|
entity.setPublishStatus(RnBundleEntity.PublishStatus.PUBLISHED);
|
2026-04-24 16:16:33 +08:00
|
|
|
entity.setGrayEnabled(false);
|
|
|
|
|
entity.setGrayPercent(0);
|
|
|
|
|
return ResponseEntity.ok(ApiResponse.success(bundleRepository.save(entity)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@PostMapping("/{id}/unpublish")
|
|
|
|
|
public ResponseEntity<ApiResponse<RnBundleEntity>> 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<ApiResponse<RnBundleEntity>> gray(
|
|
|
|
|
@PathVariable String id,
|
|
|
|
|
@RequestBody Map<String, Object> 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);
|
2026-04-21 22:07:29 +08:00
|
|
|
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());
|
|
|
|
|
}
|
2026-04-24 10:42:11 +08:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2026-04-21 22:07:29 +08:00
|
|
|
}
|