From 4a38147cb95041df0b845d61927d8e5be6ca2781 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Thu, 21 May 2026 17:08:01 +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=E8=87=AA=E5=8A=A8=E4=BF=AE=E5=A4=8D=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新前执行幂等配置修复:nginx location /file/ → /api/file/, docker-compose.yml 补齐 FILE_UPLOAD_DIR 和 FILE_BASE_URL - nginx 移至 OTHER_SERVICES 末尾,最后重启以应用修复后的配置 - docker login 读取 .env 中的仓库凭据,解决私有镜像拉取 403 Co-Authored-By: Claude Sonnet 4.6 --- .../tenant/service/SystemUpdateService.java | 92 ++++++++++++++++--- 1 file changed, 81 insertions(+), 11 deletions(-) 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 index c04b089..7e613d8 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/service/SystemUpdateService.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/service/SystemUpdateService.java @@ -6,9 +6,12 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.io.BufferedReader; +import java.io.IOException; import java.io.InputStreamReader; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import java.util.List; import java.util.function.Consumer; @@ -17,8 +20,9 @@ public class SystemUpdateService { private static final Logger log = LoggerFactory.getLogger(SystemUpdateService.class); + // nginx is restarted last so it picks up any patched config files. private static final List OTHER_SERVICES = List.of( - "file-service", "tenant-web", "im-service", "push-service", "update-service", "license-service" + "file-service", "tenant-web", "im-service", "push-service", "update-service", "license-service", "nginx" ); @Value("${PRIVATE_DEPLOY_ROOT:/opt/xuqm-private}") @@ -27,9 +31,13 @@ public class SystemUpdateService { public void runUpdate(Consumer emit) { String composeFile = deployRoot + "/docker-compose.yml"; - // Authenticate to the registry before pulling (credentials stored in .env by deploy.sh) + // Step 1: authenticate to registry dockerLogin(emit); + // Step 2: apply any pending config patches (idempotent) + patchConfigs(emit); + + // Step 3: pull images emit.accept(">>> 拉取最新镜像..."); for (String svc : OTHER_SERVICES) { if (isRunning(svc)) { @@ -41,6 +49,7 @@ public class SystemUpdateService { exec(emit, "docker", "compose", "-f", composeFile, "pull", "--quiet", "tenant-service"); emit.accept(">>> 镜像拉取完成"); + // Step 4: restart other services (nginx last so patched conf is applied) emit.accept(">>> 重启各服务..."); for (String svc : OTHER_SERVICES) { if (isRunning(svc)) { @@ -51,10 +60,7 @@ public class SystemUpdateService { } } - // tenant-service 自身的重建:不能直接用 docker compose,因为一旦发出 stop 指令, - // 当前容器(含 docker compose 进程)会立即被杀死,后续的 rm/create/start 步骤不会执行。 - // 解决方案:先用 docker run -d 启动一个独立助手容器,它不依附于 tenant-service, - // 能在 tenant-service 停止后继续完成重建。 + // Step 5: self-update tenant-service via detached helper container emit.accept(">>> 启动自更新助手容器..."); String selfImage = getCurrentImage(); if (selfImage == null) { @@ -74,6 +80,75 @@ public class SystemUpdateService { } } + /** + * Idempotent config patches — fixes known misconfigurations introduced in earlier deploy versions. + * Each patch checks before writing so running this multiple times is safe. + */ + private void patchConfigs(Consumer emit) { + emit.accept(">>> 检查并修复配置文件..."); + patchNginxFileRoute(emit); + patchDockerComposeFileService(emit); + } + + /** nginx: location /file/ must be location /api/file/ to match the file controller path. */ + private void patchNginxFileRoute(Consumer emit) { + Path conf = Paths.get(deployRoot, "config", "nginx", "conf.d", "xuqm.conf"); + if (!Files.exists(conf)) return; + try { + String content = Files.readString(conf); + if (!content.contains("location /file/")) return; + String patched = content.replace("location /file/", "location /api/file/"); + Files.writeString(conf, patched, StandardOpenOption.TRUNCATE_EXISTING); + emit.accept(" [已修复] nginx: location /file/ → /api/file/"); + } catch (IOException e) { + emit.accept(" [警告] nginx 配置修复失败: " + e.getMessage()); + } + } + + /** + * docker-compose: ensure FILE_UPLOAD_DIR and FILE_BASE_URL are set for file-service. + * Reads CONSOLE_DOMAIN from xuqm.env to determine the correct base URL. + */ + private void patchDockerComposeFileService(Consumer emit) { + Path composeFile = Paths.get(deployRoot, "docker-compose.yml"); + if (!Files.exists(composeFile)) return; + try { + String content = Files.readString(composeFile); + if (content.contains("FILE_UPLOAD_DIR") && content.contains("FILE_BASE_URL")) return; + + String consoleDomain = readEnvValue(Paths.get(deployRoot, "config", "xuqm.env"), "CONSOLE_DOMAIN"); + if (consoleDomain == null) consoleDomain = ""; + + // Inject the missing env vars directly after SPRING_DATA_REDIS_DATABASE line in file-service block. + // This pattern is stable — the line is unique in the file-service environment section. + String anchor = " SPRING_DATA_REDIS_DATABASE: \"${REDIS_DATABASE:-0}\"\n"; + if (!content.contains(anchor)) { + emit.accept(" [跳过] docker-compose 文件-服务补丁锚点未找到,请手动检查"); + return; + } + String injection = anchor + + " FILE_UPLOAD_DIR: \"/data/uploads\"\n" + + " FILE_BASE_URL: \"" + consoleDomain + "\"\n"; + String patched = content.replace(anchor, injection); + Files.writeString(composeFile, patched, StandardOpenOption.TRUNCATE_EXISTING); + emit.accept(" [已修复] docker-compose: 补齐 FILE_UPLOAD_DIR 和 FILE_BASE_URL"); + } catch (IOException e) { + emit.accept(" [警告] docker-compose 修复失败: " + e.getMessage()); + } + } + + private String readEnvValue(Path envFile, String key) { + if (!Files.exists(envFile)) return null; + try { + for (String line : Files.readAllLines(envFile)) { + if (line.startsWith(key + "=")) { + return line.substring(key.length() + 1).trim(); + } + } + } catch (IOException ignored) {} + return null; + } + /** * Read REGISTRY / REGISTRY_USER / REGISTRY_PASSWORD from deployRoot/.env and * run "docker login" so that subsequent pulls succeed on private registries. @@ -87,7 +162,6 @@ public class SystemUpdateService { else if (line.startsWith("REGISTRY_PASSWORD=")) password = line.substring("REGISTRY_PASSWORD=".length()).trim(); } if (registry == null || user == null || password == null || password.isEmpty()) return; - // REGISTRY format: host/namespace → extract host String host = registry.contains("/") ? registry.substring(0, registry.indexOf('/')) : registry; ProcessBuilder pb = new ProcessBuilder("docker", "login", host, "-u", user, "--password-stdin") .redirectErrorStream(true); @@ -109,15 +183,12 @@ public class SystemUpdateService { /** * 启动一个独立的 detached 容器,在 tenant-service 被停止后重建它。 - * 助手容器与 tenant-service 无父子关系,不会随 tenant-service 终止。 */ private boolean spawnSelfUpdater(String composeFile, String image) { try { - // 清理上次残留(若有) new ProcessBuilder("docker", "rm", "-f", "xuqm-self-updater") .redirectErrorStream(true).start().waitFor(); - // 等待 8 秒确保 tenant-service 已完全停止,然后执行 force-recreate String shellCmd = "sleep 8 && docker compose -f " + composeFile + " up -d --no-deps --force-recreate tenant-service"; @@ -141,7 +212,6 @@ public class SystemUpdateService { } } - // Filter only by service label — avoids dependency on project name which varies by deploy root dir. private boolean isRunning(String service) { try { Process p = new ProcessBuilder(