package com.xuqm.tenant.controller; import com.xuqm.tenant.config.PrivateDeploymentProperties; import com.xuqm.tenant.service.SystemUpdateService; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import java.util.List; import java.util.Map; import com.xuqm.common.model.ApiResponse; @RestController @RequestMapping("/api/system") public class SystemUpdateController { private final PrivateDeploymentProperties deployProps; private final SystemUpdateService updateService; public SystemUpdateController(PrivateDeploymentProperties deployProps, SystemUpdateService updateService) { this.deployProps = deployProps; this.updateService = updateService; } /** 返回当前正在运行的服务列表。仅 PRIVATE 模式可用。 */ @GetMapping("/services") public ResponseEntity services() { if (!deployProps.isPrivate()) { return ResponseEntity.status(403).body(Map.of("message", "此接口仅在私有化部署可用")); } List svcList = updateService.getRunningServices(); return ResponseEntity.ok(Map.of("data", svcList)); } /** 返回指定服务最近 N 行日志。仅 PRIVATE 模式可用。 */ @GetMapping(value = "/logs/{service}", produces = MediaType.TEXT_PLAIN_VALUE) public ResponseEntity logs( @PathVariable String service, @RequestParam(defaultValue = "200") int lines) { if (!deployProps.isPrivate()) { return ResponseEntity.status(403).body("此接口仅在私有化部署可用"); } try { return ResponseEntity.ok(updateService.getServiceLogs(service, lines)); } catch (IllegalArgumentException e) { return ResponseEntity.status(400).body(e.getMessage()); } catch (Exception e) { return ResponseEntity.status(500).body(e.getMessage()); } } /** 返回当前部署版本号。仅 PRIVATE 模式可用。 */ @GetMapping("/version") public ResponseEntity version() { if (!deployProps.isPrivate()) { return ResponseEntity.status(403).body(Map.of("message", "此接口仅在私有化部署可用")); } String currentVersion = updateService.readCurrentVersion(); return ResponseEntity.ok(Map.of("data", Map.of("currentVersion", currentVersion))); } /** * 检查是否有可用更新。 * 对比本地版本与 versions.json 中的最新版本,返回各服务的版本差异。 * 仅 PRIVATE 模式可用。 */ @GetMapping("/check-update") public ResponseEntity checkUpdate() { if (!deployProps.isPrivate()) { return ResponseEntity.status(403).body(Map.of("message", "此接口仅在私有化部署可用")); } return ResponseEntity.ok(updateService.checkForUpdates()); } /** * 拉取最新镜像并重建所有容器。耗时较长(需 docker pull)。 * 仅 PRIVATE 模式可用。 */ @PostMapping(value = "/update", produces = MediaType.TEXT_PLAIN_VALUE) public ResponseEntity update() { if (!deployProps.isPrivate()) { return ResponseEntity.status(403) .contentType(MediaType.TEXT_PLAIN) .body(out -> out.write("此接口仅在私有化部署可用\n".getBytes())); } return stream(emit -> updateService.runUpdate(emit)); } /** * 选择性更新:只更新指定的服务。 * 仅 PRIVATE 模式可用。 */ @PostMapping(value = "/update-selective", produces = MediaType.TEXT_PLAIN_VALUE) public ResponseEntity updateSelective( @RequestParam(required = false) List services) { if (!deployProps.isPrivate()) { return ResponseEntity.status(403) .contentType(MediaType.TEXT_PLAIN) .body(out -> out.write("此接口仅在私有化部署可用\n".getBytes())); } return stream(emit -> updateService.runSelectiveUpdate(emit, services)); } /** * 保留数据,重置容器和数据库表结构。 * 流程:备份核心数据 → 删表 → 重建容器 → 恢复数据 → 执行迁移。 * 仅 PRIVATE 模式可用。 */ @PostMapping(value = "/reset", produces = MediaType.TEXT_PLAIN_VALUE) public ResponseEntity reset() { if (!deployProps.isPrivate()) { return ResponseEntity.status(403) .contentType(MediaType.TEXT_PLAIN) .body(out -> out.write("此接口仅在私有化部署可用\n".getBytes())); } return stream(emit -> updateService.runReset(emit)); } private ResponseEntity stream(java.util.function.Consumer> action) { StreamingResponseBody body = outputStream -> action.accept(line -> { try { outputStream.write((line + "\n").getBytes()); outputStream.flush(); } catch (Exception ignored) {} }); return ResponseEntity.ok() .contentType(MediaType.TEXT_PLAIN) .header("X-Accel-Buffering", "no") .header("Cache-Control", "no-cache, no-store") .body(body); } }