From f2e126e2d0d40541e467cd556f2ff9a5a1b50f40 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Thu, 21 May 2026 14:46:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(tenant-service):=20=E4=B8=80=E9=94=AE?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=8E=A5=E5=8F=A3=20+=20Dockerfile=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20docker-compose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 SystemUpdateController POST /api/system/update(PRIVATE 模式) - SystemUpdateService 通过 docker-compose 拉镜像并逐服务重建容器 - Dockerfile 添加 docker-cli + docker-compose(用于容器内调用 Docker API) Co-Authored-By: Claude Sonnet 4.6 --- Dockerfile | 2 +- .../controller/SystemUpdateController.java | 50 ++++++++++ .../tenant/service/SystemUpdateService.java | 97 +++++++++++++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 tenant-service/src/main/java/com/xuqm/tenant/controller/SystemUpdateController.java create mode 100644 tenant-service/src/main/java/com/xuqm/tenant/service/SystemUpdateService.java diff --git a/Dockerfile b/Dockerfile index 86b5b19..96038c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,7 +43,7 @@ ARG SERVICE_MODULE # curl is required by update-service (MI store upload uses ProcessBuilder curl to handle # Expect:100-continue and force HTTP/1.1 for large multipart uploads) -RUN apk add --no-cache curl +RUN apk add --no-cache curl docker-cli docker-compose COPY --from=build /workspace/${SERVICE_MODULE}/target/${SERVICE_MODULE}-0.1.0-SNAPSHOT.jar /app/app.jar diff --git a/tenant-service/src/main/java/com/xuqm/tenant/controller/SystemUpdateController.java b/tenant-service/src/main/java/com/xuqm/tenant/controller/SystemUpdateController.java new file mode 100644 index 0000000..bf819b1 --- /dev/null +++ b/tenant-service/src/main/java/com/xuqm/tenant/controller/SystemUpdateController.java @@ -0,0 +1,50 @@ +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.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; + +@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 模式下可用;需要 JWT 认证(租户账号即可)。 + */ + @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())); + } + + StreamingResponseBody body = outputStream -> { + updateService.runUpdate(line -> { + try { + outputStream.write((line + "\n").getBytes()); + outputStream.flush(); + } catch (Exception ignored) {} + }); + }; + + return ResponseEntity.ok() + .contentType(MediaType.TEXT_PLAIN) + .body(body); + } +} diff --git a/tenant-service/src/main/java/com/xuqm/tenant/service/SystemUpdateService.java b/tenant-service/src/main/java/com/xuqm/tenant/service/SystemUpdateService.java new file mode 100644 index 0000000..ee2c9b9 --- /dev/null +++ b/tenant-service/src/main/java/com/xuqm/tenant/service/SystemUpdateService.java @@ -0,0 +1,97 @@ +package com.xuqm.tenant.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +@Service +public class SystemUpdateService { + + private static final Logger log = LoggerFactory.getLogger(SystemUpdateService.class); + + private static final List OTHER_SERVICES = List.of( + "file-service", "tenant-web", "im-service", "push-service", "update-service", "license-service" + ); + + @Value("${PRIVATE_DEPLOY_ROOT:/opt/xuqm-private}") + private String deployRoot; + + public void runUpdate(Consumer emit) { + String composeFile = deployRoot + "/docker-compose.yml"; + + emit.accept(">>> 拉取最新镜像..."); + for (String svc : OTHER_SERVICES) { + if (isRunning(svc)) { + emit.accept(" pulling " + svc + " ..."); + exec(emit, "docker-compose", "-f", composeFile, "-p", "xuqm", "pull", "--quiet", svc); + } + } + emit.accept(" pulling tenant-service ..."); + exec(emit, "docker-compose", "-f", composeFile, "-p", "xuqm", "pull", "--quiet", "tenant-service"); + emit.accept(">>> 镜像拉取完成"); + + emit.accept(">>> 重启各服务..."); + for (String svc : OTHER_SERVICES) { + if (isRunning(svc)) { + emit.accept(" restarting " + svc + " ..."); + exec(emit, "docker-compose", "-f", composeFile, "-p", "xuqm", + "up", "-d", "--no-deps", "--force-recreate", svc); + emit.accept(" " + svc + " ✓"); + } + } + + emit.accept(">>> 即将重启 tenant-service,连接将短暂中断..."); + emit.accept("RESTART_SELF"); + + // 延迟 3 秒后重启自身,确保 SSE 事件已发送到客户端 + CompletableFuture.runAsync(() -> { + try { + Thread.sleep(3000); + exec(msg -> log.info("[self-restart] {}", msg), + "docker-compose", "-f", composeFile, "-p", "xuqm", + "up", "-d", "--no-deps", "--force-recreate", "tenant-service"); + } catch (Exception e) { + log.error("self-restart failed", e); + } + }); + } + + private boolean isRunning(String service) { + String containerName = "xuqm-" + service; + try { + Process p = new ProcessBuilder("docker", "inspect", "--type=container", containerName) + .redirectErrorStream(true) + .start(); + p.getInputStream().transferTo(java.io.OutputStream.nullOutputStream()); + return p.waitFor() == 0; + } catch (Exception e) { + return false; + } + } + + private void exec(Consumer emit, String... cmd) { + try { + Process p = new ProcessBuilder(cmd) + .redirectErrorStream(true) + .start(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + if (!line.isBlank()) emit.accept(" " + line); + } + } + int code = p.waitFor(); + if (code != 0) emit.accept(" [warn] exit code " + code); + } catch (Exception e) { + emit.accept(" [error] " + e.getMessage()); + log.error("exec failed: {}", String.join(" ", cmd), e); + } + } +}