feat(tenant): split update/reset ops, remove bootstrap app auto-creation
- SystemUpdateService: split runUpdate() (pull+recreate) and runReset() (recreate only) - SystemUpdateController: add POST /api/system/reset endpoint - SdkAppProvisioningService: remove ensureBootstrapApp/ensureApp/ensureFeatureDefaults; resolveApp now throws 404 instead of auto-creating - SdkAppInitializer: remove ensureBootstrapApp call; only runs one-time migration marking existing system apps as isDefault=true - PrivateTenantBootstrapInitializer: remove bootstrap app creation; only ensures admin tenant account exists Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
9728dbb002
当前提交
32aa3c0eef
@ -1,10 +1,7 @@
|
||||
package com.xuqm.tenant.config;
|
||||
|
||||
import com.xuqm.tenant.entity.AppEntity;
|
||||
import com.xuqm.tenant.entity.TenantEntity;
|
||||
import com.xuqm.tenant.repository.AppRepository;
|
||||
import com.xuqm.tenant.repository.TenantRepository;
|
||||
import com.xuqm.tenant.service.SdkAppProvisioningService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@ -30,8 +27,6 @@ public class PrivateTenantBootstrapInitializer implements ApplicationRunner {
|
||||
|
||||
private final PrivateDeploymentProperties deployProps;
|
||||
private final TenantRepository tenantRepository;
|
||||
private final AppRepository appRepository;
|
||||
private final SdkAppProvisioningService provisioningService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Value("${tenant.bootstrap.email:admin@customer.com}")
|
||||
@ -43,18 +38,11 @@ public class PrivateTenantBootstrapInitializer implements ApplicationRunner {
|
||||
@Value("${tenant.bootstrap.username:admin}")
|
||||
private String bootstrapUsername;
|
||||
|
||||
@Value("${sdk.bootstrap-app-key:ak_private_default}")
|
||||
private String bootstrapAppKey;
|
||||
|
||||
public PrivateTenantBootstrapInitializer(PrivateDeploymentProperties deployProps,
|
||||
TenantRepository tenantRepository,
|
||||
AppRepository appRepository,
|
||||
SdkAppProvisioningService provisioningService,
|
||||
PasswordEncoder passwordEncoder) {
|
||||
this.deployProps = deployProps;
|
||||
this.tenantRepository = tenantRepository;
|
||||
this.appRepository = appRepository;
|
||||
this.provisioningService = provisioningService;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@ -84,16 +72,5 @@ public class PrivateTenantBootstrapInitializer implements ApplicationRunner {
|
||||
} else {
|
||||
log.info("[PRIVATE] Bootstrap tenant already exists: {}", bootstrapEmail);
|
||||
}
|
||||
|
||||
AppEntity app = appRepository.findByAppKey(bootstrapAppKey).orElse(null);
|
||||
if (app == null) {
|
||||
provisioningService.ensureApp(bootstrapAppKey, true);
|
||||
log.info("[PRIVATE] Bootstrap app created: {}", bootstrapAppKey);
|
||||
} else if (!app.isDefault() || app.isDeletable()) {
|
||||
app.setDefault(true);
|
||||
app.setDeletable(false);
|
||||
appRepository.save(app);
|
||||
log.info("[PRIVATE] Bootstrap app marked as system app: {}", bootstrapAppKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ package com.xuqm.tenant.config;
|
||||
|
||||
import com.xuqm.tenant.entity.AppEntity;
|
||||
import com.xuqm.tenant.repository.AppRepository;
|
||||
import com.xuqm.tenant.service.SdkAppProvisioningService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
@ -11,8 +10,6 @@ import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
public class SdkAppInitializer implements ApplicationRunner {
|
||||
|
||||
private final SdkAppProvisioningService provisioningService;
|
||||
private final PrivateDeploymentProperties deployProps;
|
||||
private final AppRepository appRepository;
|
||||
|
||||
@Value("${sdk.bootstrap-app-key:ak_demo_chat}")
|
||||
@ -21,20 +18,13 @@ public class SdkAppInitializer implements ApplicationRunner {
|
||||
@Value("${sdk.im-platform-app-key:ak_409e217e4aa14254ad73ad3c}")
|
||||
private String imPlatformAppKey;
|
||||
|
||||
public SdkAppInitializer(SdkAppProvisioningService provisioningService,
|
||||
PrivateDeploymentProperties deployProps,
|
||||
AppRepository appRepository) {
|
||||
this.provisioningService = provisioningService;
|
||||
this.deployProps = deployProps;
|
||||
public SdkAppInitializer(AppRepository appRepository) {
|
||||
this.appRepository = appRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
migrateExistingSystemApps();
|
||||
if (!deployProps.isPrivate()) {
|
||||
provisioningService.ensureBootstrapApp();
|
||||
}
|
||||
}
|
||||
|
||||
private void migrateExistingSystemApps() {
|
||||
|
||||
@ -23,8 +23,8 @@ public class SystemUpdateController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发私有化部署一键升级,以流式文本返回进度日志。
|
||||
* 仅在 PRIVATE 模式下可用;需要 JWT 认证(租户账号即可)。
|
||||
* 拉取最新镜像并重建所有容器。耗时较长(需 docker pull)。
|
||||
* 仅 PRIVATE 模式可用。
|
||||
*/
|
||||
@PostMapping(value = "/update", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
public ResponseEntity<StreamingResponseBody> update() {
|
||||
@ -33,16 +33,30 @@ public class SystemUpdateController {
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.body(out -> out.write("此接口仅在私有化部署可用\n".getBytes()));
|
||||
}
|
||||
return stream(emit -> updateService.runUpdate(emit));
|
||||
}
|
||||
|
||||
StreamingResponseBody body = outputStream -> {
|
||||
updateService.runUpdate(line -> {
|
||||
/**
|
||||
* 不拉取新镜像,直接用当前本地镜像重建所有容器。速度快,适合修复异常服务。
|
||||
* 仅 PRIVATE 模式可用。
|
||||
*/
|
||||
@PostMapping(value = "/reset", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
public ResponseEntity<StreamingResponseBody> 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<StreamingResponseBody> stream(java.util.function.Consumer<java.util.function.Consumer<String>> 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)
|
||||
.body(body);
|
||||
|
||||
@ -2,181 +2,23 @@ package com.xuqm.tenant.service;
|
||||
|
||||
import com.xuqm.common.exception.BusinessException;
|
||||
import com.xuqm.tenant.entity.AppEntity;
|
||||
import com.xuqm.tenant.entity.FeatureServiceEntity;
|
||||
import com.xuqm.tenant.entity.TenantEntity;
|
||||
import com.xuqm.tenant.repository.AppRepository;
|
||||
import com.xuqm.tenant.repository.FeatureServiceRepository;
|
||||
import com.xuqm.tenant.repository.TenantRepository;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class SdkAppProvisioningService {
|
||||
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
|
||||
private final AppRepository appRepository;
|
||||
private final TenantRepository tenantRepository;
|
||||
private final FeatureServiceRepository featureServiceRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Value("${sdk.bootstrap-app-key:ak_demo_chat}")
|
||||
private String bootstrapAppKey;
|
||||
|
||||
@Value("${sdk.bootstrap-app-name:Demo Chat}")
|
||||
private String bootstrapAppName;
|
||||
|
||||
@Value("${sdk.bootstrap-app-package:com.xuqm.demo}")
|
||||
private String bootstrapAppPackage;
|
||||
|
||||
@Value("${sdk.bootstrap-app-description:XuqmGroup demo app}")
|
||||
private String bootstrapAppDescription;
|
||||
|
||||
public SdkAppProvisioningService(AppRepository appRepository,
|
||||
TenantRepository tenantRepository,
|
||||
FeatureServiceRepository featureServiceRepository,
|
||||
PasswordEncoder passwordEncoder) {
|
||||
public SdkAppProvisioningService(AppRepository appRepository) {
|
||||
this.appRepository = appRepository;
|
||||
this.tenantRepository = tenantRepository;
|
||||
this.featureServiceRepository = featureServiceRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AppEntity ensureBootstrapApp() {
|
||||
return ensureApp(bootstrapAppKey, true);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AppEntity resolveApp(String appKey) {
|
||||
return appRepository.findByAppKey(appKey)
|
||||
.or(() -> appRepository.findById(appKey))
|
||||
.orElseGet(() -> {
|
||||
if (!bootstrapAppKey.equals(appKey)) {
|
||||
throw new BusinessException(404, "App not found: " + appKey);
|
||||
}
|
||||
return ensureApp(appKey, true);
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AppEntity ensureApp(String appKey, boolean bootstrapDefaults) {
|
||||
AppEntity existing = appRepository.findByAppKey(appKey).orElse(null);
|
||||
if (existing != null) {
|
||||
if (bootstrapDefaults) {
|
||||
ensureFeatureDefaults(existing);
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
TenantEntity owner = resolveOwnerTenant();
|
||||
AppEntity app = new AppEntity();
|
||||
app.setId(UUID.randomUUID().toString());
|
||||
app.setTenantId(owner.getId());
|
||||
app.setPackageName(bootstrapAppPackage);
|
||||
app.setName(bootstrapAppName);
|
||||
app.setDescription(bootstrapAppDescription);
|
||||
app.setIconUrl(null);
|
||||
app.setAppKey(appKey);
|
||||
app.setAppSecret(generateSecret());
|
||||
app.setCreatedAt(LocalDateTime.now());
|
||||
app.setDefault(true);
|
||||
app.setDeletable(false);
|
||||
app = appRepository.save(app);
|
||||
|
||||
if (bootstrapDefaults) {
|
||||
ensureFeatureDefaults(app);
|
||||
}
|
||||
return app;
|
||||
}
|
||||
|
||||
private TenantEntity resolveOwnerTenant() {
|
||||
return tenantRepository.findFirstByOrderByCreatedAtAsc()
|
||||
.orElseGet(this::createBootstrapTenant);
|
||||
}
|
||||
|
||||
private TenantEntity createBootstrapTenant() {
|
||||
TenantEntity tenant = new TenantEntity();
|
||||
tenant.setId(UUID.randomUUID().toString());
|
||||
tenant.setUsername("system");
|
||||
tenant.setPassword(passwordEncoder.encode(generateSecret()));
|
||||
tenant.setEmail("system@xuqinmin.com");
|
||||
tenant.setNickname("System");
|
||||
tenant.setPhone(null);
|
||||
tenant.setType(TenantEntity.Type.MAIN);
|
||||
tenant.setStatus(TenantEntity.Status.ACTIVE);
|
||||
tenant.setParentId(null);
|
||||
tenant.setCreatedAt(LocalDateTime.now());
|
||||
return tenantRepository.save(tenant);
|
||||
}
|
||||
|
||||
private void ensureFeatureDefaults(AppEntity app) {
|
||||
featureServiceRepository.findByAppKeyAndServiceType(app.getAppKey(), FeatureServiceEntity.ServiceType.IM)
|
||||
.stream()
|
||||
.findFirst()
|
||||
.orElseGet(() -> {
|
||||
FeatureServiceEntity feature = new FeatureServiceEntity();
|
||||
feature.setId(UUID.randomUUID().toString());
|
||||
feature.setAppKey(app.getAppKey());
|
||||
feature.setPlatform(FeatureServiceEntity.Platform.ANDROID);
|
||||
feature.setServiceType(FeatureServiceEntity.ServiceType.IM);
|
||||
feature.setEnabled(true);
|
||||
feature.setConfig("""
|
||||
{"allowStrangerMessage":false,"allowFriendRequest":true,"friendRequestMode":"REQUIRE_CONFIRM","allowGroupJoinRequest":true,"blacklistSendSuccess":true,"messageRecallMinutes":2,"historyRetentionDays":7,"conversationPullLimit":100,"allowMultiDeviceLogin":true,"multiClientConversationDeleteSync":false}
|
||||
""".trim());
|
||||
feature.setCreatedAt(LocalDateTime.now());
|
||||
return featureServiceRepository.save(feature);
|
||||
});
|
||||
|
||||
for (FeatureServiceEntity.Platform platform : List.of(
|
||||
FeatureServiceEntity.Platform.ANDROID,
|
||||
FeatureServiceEntity.Platform.IOS,
|
||||
FeatureServiceEntity.Platform.HARMONY)) {
|
||||
for (FeatureServiceEntity.ServiceType serviceType : List.of(
|
||||
FeatureServiceEntity.ServiceType.PUSH,
|
||||
FeatureServiceEntity.ServiceType.UPDATE,
|
||||
FeatureServiceEntity.ServiceType.LICENSE)) {
|
||||
featureServiceRepository.findByAppKeyAndPlatformAndServiceType(app.getAppKey(), platform, serviceType)
|
||||
.orElseGet(() -> {
|
||||
FeatureServiceEntity feature = new FeatureServiceEntity();
|
||||
feature.setId(UUID.randomUUID().toString());
|
||||
feature.setAppKey(app.getAppKey());
|
||||
feature.setPlatform(platform);
|
||||
feature.setServiceType(serviceType);
|
||||
feature.setEnabled(true);
|
||||
feature.setConfig(switch (serviceType) {
|
||||
case UPDATE -> switch (platform) {
|
||||
case ANDROID -> """
|
||||
{"defaultStoreTargets":[],"defaultPublishMode":"MANUAL","defaultPublishImmediately":false,"defaultScheduledPublishAt":"","defaultAutoPublishAfterReview":false,"defaultWebhookUrl":"","defaultForceUpdate":false,"defaultGrayEnabled":false,"defaultGrayPercent":0,"defaultPackageName":"","defaultAppStoreUrl":"","defaultMarketUrl":""}
|
||||
""".trim();
|
||||
case IOS -> """
|
||||
{"defaultStoreTargets":["APP_STORE"],"defaultPublishMode":"MANUAL","defaultPublishImmediately":false,"defaultScheduledPublishAt":"","defaultAutoPublishAfterReview":false,"defaultWebhookUrl":"","defaultForceUpdate":false,"defaultGrayEnabled":false,"defaultGrayPercent":0,"defaultPackageName":"","defaultAppStoreUrl":"","defaultMarketUrl":""}
|
||||
""".trim();
|
||||
case HARMONY -> """
|
||||
{"defaultStoreTargets":[],"defaultPublishMode":"MANUAL","defaultPublishImmediately":false,"defaultScheduledPublishAt":"","defaultAutoPublishAfterReview":false,"defaultWebhookUrl":"","defaultForceUpdate":false,"defaultGrayEnabled":false,"defaultGrayPercent":0,"defaultPackageName":"","defaultAppStoreUrl":"","defaultMarketUrl":""}
|
||||
""".trim();
|
||||
};
|
||||
case LICENSE -> "{\"maxDevices\":1}";
|
||||
default -> null;
|
||||
});
|
||||
feature.setCreatedAt(LocalDateTime.now());
|
||||
return featureServiceRepository.save(feature);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String generateSecret() {
|
||||
byte[] bytes = new byte[32];
|
||||
RANDOM.nextBytes(bytes);
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
|
||||
.orElseThrow(() -> new BusinessException(404, "App not found: " + appKey));
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,43 +28,49 @@ public class SystemUpdateService {
|
||||
@Value("${PRIVATE_DEPLOY_ROOT:/opt/xuqm-private}")
|
||||
private String deployRoot;
|
||||
|
||||
/** 拉取最新镜像并重建所有容器。 */
|
||||
public void runUpdate(Consumer<String> emit) {
|
||||
String composeFile = deployRoot + "/docker-compose.yml";
|
||||
|
||||
// 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)) {
|
||||
emit.accept(" pulling " + svc + " ...");
|
||||
exec(emit, "docker", "compose", "-f", composeFile, "pull", "--quiet", svc);
|
||||
}
|
||||
}
|
||||
emit.accept(" pulling tenant-service ...");
|
||||
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(">>> 重启各服务...");
|
||||
restartAndSelfUpdate(emit, composeFile);
|
||||
}
|
||||
|
||||
/** 不拉取新镜像,直接用当前本地镜像重建所有容器。 */
|
||||
public void runReset(Consumer<String> emit) {
|
||||
String composeFile = deployRoot + "/docker-compose.yml";
|
||||
|
||||
patchConfigs(emit);
|
||||
restartAndSelfUpdate(emit, composeFile);
|
||||
}
|
||||
|
||||
// ── Shared core ───────────────────────────────────────────────────────────
|
||||
|
||||
private void restartAndSelfUpdate(Consumer<String> emit, String composeFile) {
|
||||
emit.accept(">>> 重建各服务容器...");
|
||||
for (String svc : OTHER_SERVICES) {
|
||||
if (isRunning(svc)) {
|
||||
emit.accept(" restarting " + svc + " ...");
|
||||
exec(emit, "docker", "compose", "-f", composeFile,
|
||||
"up", "-d", "--no-deps", "--force-recreate", svc);
|
||||
emit.accept(" " + svc + " ✓");
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: self-update tenant-service via detached helper container
|
||||
emit.accept(">>> 启动自更新助手容器...");
|
||||
String selfImage = getCurrentImage();
|
||||
if (selfImage == null) {
|
||||
emit.accept(">>> [错误] 无法获取当前 tenant-service 镜像名,请检查容器标签或手动执行更新。");
|
||||
emit.accept(">>> [错误] 无法获取当前 tenant-service 镜像名,请手动执行:");
|
||||
emit.accept(">>> docker compose -f " + composeFile + " up -d --no-deps --force-recreate tenant-service");
|
||||
emit.accept("DONE");
|
||||
return;
|
||||
}
|
||||
@ -80,10 +86,8 @@ 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.
|
||||
*/
|
||||
// ── Config patchers ───────────────────────────────────────────────────────
|
||||
|
||||
private void patchConfigs(Consumer<String> emit) {
|
||||
emit.accept(">>> 检查并修复配置文件...");
|
||||
patchNginxFileRoute(emit);
|
||||
@ -91,24 +95,20 @@ public class SystemUpdateService {
|
||||
patchDockerComposeFileService(emit);
|
||||
}
|
||||
|
||||
/**
|
||||
* nginx: add exact-match location for /api/system/update with 600s timeout so that
|
||||
* docker pull (which may be silent for minutes) doesn't hit the 60s proxy_read_timeout
|
||||
* on the generic /api/ block and cause ERR_INCOMPLETE_CHUNKED_ENCODING.
|
||||
*/
|
||||
private void patchNginxUpdateTimeout(Consumer<String> 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 = /api/system/update")) return;
|
||||
// Already patched with regex location (new format) or exact-match (old format)
|
||||
if (content.contains("location ~ ^/api/system/") || content.contains("location = /api/system/update")) return;
|
||||
String anchor = " # 核心 API(兜底,在所有具体 /api/xxx/ 之后)\n location /api/ {";
|
||||
if (!content.contains(anchor)) {
|
||||
emit.accept(" [跳过] nginx 更新超时补丁锚点未找到,请手动检查");
|
||||
return;
|
||||
}
|
||||
String injection = " # 一键更新:docker pull 可能耗时数分钟,需要更长超时(精确匹配,优先于 /api/ 前缀)\n"
|
||||
+ " location = /api/system/update {\n"
|
||||
String injection = " # 一键更新/重置:操作耗时较长,需要更长超时(精确匹配,优先于 /api/ 前缀)\n"
|
||||
+ " location ~ ^/api/system/(update|reset)$ {\n"
|
||||
+ " set $svc tenant-service;\n"
|
||||
+ " proxy_pass http://$svc:9001;\n"
|
||||
+ " proxy_set_header Host $host;\n"
|
||||
@ -120,13 +120,12 @@ public class SystemUpdateService {
|
||||
+ anchor;
|
||||
String patched = content.replace(anchor, injection);
|
||||
Files.writeString(conf, patched, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
emit.accept(" [已修复] nginx: 补齐 /api/system/update 600s 超时");
|
||||
emit.accept(" [已修复] nginx: 补齐 /api/system/(update|reset) 600s 超时");
|
||||
} catch (IOException e) {
|
||||
emit.accept(" [警告] nginx 更新超时修复失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** nginx: location /file/ must be location /api/file/ to match the file controller path. */
|
||||
private void patchNginxFileRoute(Consumer<String> emit) {
|
||||
Path conf = Paths.get(deployRoot, "config", "nginx", "conf.d", "xuqm.conf");
|
||||
if (!Files.exists(conf)) return;
|
||||
@ -141,10 +140,6 @@ public class SystemUpdateService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<String> emit) {
|
||||
Path composeFile = Paths.get(deployRoot, "docker-compose.yml");
|
||||
if (!Files.exists(composeFile)) return;
|
||||
@ -155,8 +150,6 @@ public class SystemUpdateService {
|
||||
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 文件-服务补丁锚点未找到,请手动检查");
|
||||
@ -173,22 +166,8 @@ public class SystemUpdateService {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
// ── Docker helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 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<String> emit) {
|
||||
try {
|
||||
String registry = null, user = null, password = null;
|
||||
@ -217,9 +196,6 @@ public class SystemUpdateService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动一个独立的 detached 容器,在 tenant-service 被停止后重建它。
|
||||
*/
|
||||
private boolean spawnSelfUpdater(String composeFile, String image) {
|
||||
try {
|
||||
new ProcessBuilder("docker", "rm", "-f", "xuqm-self-updater")
|
||||
@ -248,20 +224,6 @@ public class SystemUpdateService {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRunning(String service) {
|
||||
try {
|
||||
Process p = new ProcessBuilder(
|
||||
"docker", "ps", "-q",
|
||||
"--filter", "label=com.docker.compose.service=" + service
|
||||
).redirectErrorStream(true).start();
|
||||
String out = new String(p.getInputStream().readAllBytes()).trim();
|
||||
p.waitFor();
|
||||
return !out.isEmpty();
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String getCurrentImage() {
|
||||
try {
|
||||
Process p = new ProcessBuilder(
|
||||
@ -277,6 +239,18 @@ public class SystemUpdateService {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private void exec(Consumer<String> emit, String... cmd) {
|
||||
try {
|
||||
Process p = new ProcessBuilder(cmd)
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户