diff --git a/license-service/src/main/java/com/xuqm/license/controller/LicenseAdminController.java b/license-service/src/main/java/com/xuqm/license/controller/LicenseAdminController.java index e5ba75e..5b600a6 100644 --- a/license-service/src/main/java/com/xuqm/license/controller/LicenseAdminController.java +++ b/license-service/src/main/java/com/xuqm/license/controller/LicenseAdminController.java @@ -40,8 +40,18 @@ public class LicenseAdminController { if (req.maxDevices() != null && req.maxDevices() < 1) { throw new com.xuqm.common.exception.BusinessException(400, "最大设备数必须大于0"); } + java.time.LocalDateTime newExpiresAt = null; + boolean clearExpiresAt = false; + if (req.expiresAt() != null) { + if (req.expiresAt().isEmpty()) { + clearExpiresAt = true; + } else { + newExpiresAt = java.time.LocalDateTime.parse(req.expiresAt(), + java.time.format.DateTimeFormatter.ISO_DATE_TIME); + } + } AppLicenseEntity updated = appLicenseService.update( - appKey, null, req.maxDevices(), null, req.isActive(), req.remark()); + appKey, null, req.maxDevices(), newExpiresAt, clearExpiresAt, req.isActive(), req.remark()); return ResponseEntity.ok(ApiResponse.success(updated)); } @@ -60,7 +70,8 @@ public class LicenseAdminController { public record UpdateAppLicenseRequest( Integer maxDevices, Boolean isActive, - String remark + String remark, + String expiresAt ) {} } diff --git a/license-service/src/main/java/com/xuqm/license/service/AppLicenseService.java b/license-service/src/main/java/com/xuqm/license/service/AppLicenseService.java index 0cc9104..a35fe53 100644 --- a/license-service/src/main/java/com/xuqm/license/service/AppLicenseService.java +++ b/license-service/src/main/java/com/xuqm/license/service/AppLicenseService.java @@ -26,18 +26,21 @@ public class AppLicenseService { public AppLicenseEntity upsert(String appKey, String name, Integer maxDevices, LocalDateTime expiresAt, Boolean isActive, String remark) { return repository.findById(appKey) - .map(license -> update(appKey, name, maxDevices, expiresAt, isActive, remark)) + .map(license -> update(appKey, name, maxDevices, expiresAt, false, isActive, remark)) .orElseGet(() -> create(appKey, name, maxDevices, expiresAt, isActive, remark)); } @Transactional public AppLicenseEntity update(String appKey, String name, Integer maxDevices, - LocalDateTime expiresAt, Boolean isActive, String remark) { + LocalDateTime expiresAt, boolean clearExpiresAt, Boolean isActive, String remark) { AppLicenseEntity license = getByAppKey(appKey); if (name != null) license.setName(name); if (maxDevices != null) license.setMaxDevices(maxDevices); - // expiresAt 一旦设置不可修改 - if (expiresAt != null && license.getExpiresAt() == null) license.setExpiresAt(expiresAt); + if (clearExpiresAt) { + license.setExpiresAt(null); + } else if (expiresAt != null) { + license.setExpiresAt(expiresAt); + } if (isActive != null) license.setIsActive(isActive); if (remark != null) license.setRemark(remark); license.setUpdatedAt(LocalDateTime.now()); 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 46f7cfb..c04b089 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 @@ -7,6 +7,8 @@ import org.springframework.stereotype.Service; import java.io.BufferedReader; import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.List; import java.util.function.Consumer; @@ -25,34 +27,35 @@ 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) + dockerLogin(emit); + 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); + exec(emit, "docker", "compose", "-f", composeFile, "pull", "--quiet", svc); } } emit.accept(" pulling tenant-service ..."); - exec(emit, "docker-compose", "-f", composeFile, "-p", "xuqm", "pull", "--quiet", "tenant-service"); + exec(emit, "docker", "compose", "-f", composeFile, "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", + exec(emit, "docker", "compose", "-f", composeFile, "up", "-d", "--no-deps", "--force-recreate", svc); emit.accept(" " + svc + " ✓"); } } - // tenant-service 自身的重建:不能直接用 docker-compose,因为一旦发出 stop 指令, - // 当前容器(含 docker-compose 进程)会立即被杀死,后续的 rm/create/start 步骤不会执行。 + // tenant-service 自身的重建:不能直接用 docker compose,因为一旦发出 stop 指令, + // 当前容器(含 docker compose 进程)会立即被杀死,后续的 rm/create/start 步骤不会执行。 // 解决方案:先用 docker run -d 启动一个独立助手容器,它不依附于 tenant-service, // 能在 tenant-service 停止后继续完成重建。 emit.accept(">>> 启动自更新助手容器..."); - // Resolve the image by inspecting the running container via compose labels, - // so we don't depend on REGISTRY/IMAGE_TAG being present in the container env. String selfImage = getCurrentImage(); if (selfImage == null) { emit.accept(">>> [错误] 无法获取当前 tenant-service 镜像名,请检查容器标签或手动执行更新。"); @@ -66,11 +69,44 @@ public class SystemUpdateService { emit.accept("RESTART_SELF"); } else { emit.accept(">>> [警告] 助手容器启动失败,请手动执行:"); - emit.accept(">>> docker-compose -f " + composeFile + " -p xuqm up -d --no-deps --force-recreate tenant-service"); + emit.accept(">>> docker compose -f " + composeFile + " up -d --no-deps --force-recreate tenant-service"); emit.accept("DONE"); } } + /** + * Read REGISTRY / REGISTRY_USER / REGISTRY_PASSWORD from deployRoot/.env and + * run "docker login" so that subsequent pulls succeed on private registries. + */ + private void dockerLogin(Consumer emit) { + try { + String registry = null, user = null, password = null; + for (String line : Files.readAllLines(Paths.get(deployRoot + "/.env"))) { + if (line.startsWith("REGISTRY=")) registry = line.substring("REGISTRY=".length()).trim(); + else if (line.startsWith("REGISTRY_USER=")) user = line.substring("REGISTRY_USER=".length()).trim(); + 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); + Process p = pb.start(); + p.getOutputStream().write((password + "\n").getBytes()); + p.getOutputStream().flush(); + p.getOutputStream().close(); + String out = new String(p.getInputStream().readAllBytes()).trim(); + int code = p.waitFor(); + if (code == 0) { + emit.accept(" 已完成镜像仓库登录"); + } else { + emit.accept(" [警告] 镜像仓库登录失败,将使用本地缓存(" + out + ")"); + } + } catch (Exception e) { + emit.accept(" [警告] 读取仓库凭据失败: " + e.getMessage()); + } + } + /** * 启动一个独立的 detached 容器,在 tenant-service 被停止后重建它。 * 助手容器与 tenant-service 无父子关系,不会随 tenant-service 终止。 @@ -82,8 +118,8 @@ public class SystemUpdateService { .redirectErrorStream(true).start().waitFor(); // 等待 8 秒确保 tenant-service 已完全停止,然后执行 force-recreate - String shellCmd = "sleep 8 && docker-compose -f " + composeFile - + " -p xuqm up -d --no-deps --force-recreate tenant-service"; + String shellCmd = "sleep 8 && docker compose -f " + composeFile + + " up -d --no-deps --force-recreate tenant-service"; Process p = new ProcessBuilder( "docker", "run", "-d", "--rm", @@ -105,13 +141,11 @@ public class SystemUpdateService { } } - // Use compose labels instead of container name — works with both Compose v1 (xuqm_svc_1) - // and Compose v2 (xuqm-svc-1) naming conventions. + // 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( "docker", "ps", "-q", - "--filter", "label=com.docker.compose.project=xuqm", "--filter", "label=com.docker.compose.service=" + service ).redirectErrorStream(true).start(); String out = new String(p.getInputStream().readAllBytes()).trim(); @@ -126,7 +160,6 @@ public class SystemUpdateService { try { Process p = new ProcessBuilder( "docker", "ps", - "--filter", "label=com.docker.compose.project=xuqm", "--filter", "label=com.docker.compose.service=tenant-service", "--format", "{{.Image}}" ).redirectErrorStream(true).start();