初始化配置完善

This commit is contained in:
waner
2026-04-17 15:29:20 +08:00
parent 8989c25aed
commit a5c22222e9
25 changed files with 1234 additions and 442 deletions
@@ -14,6 +14,7 @@ import com.cisd.tms.modules.device.support.HardwareStatusProbe;
import com.cisd.tms.modules.device.support.SoftwareRuntimeProbe;
import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe;
import com.cisd.tms.modules.device.support.UsageSnapshot;
import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse;
import com.cisd.tms.modules.init.service.InitService;
import java.time.OffsetDateTime;
import java.util.ArrayList;
@@ -177,10 +178,26 @@ public class DeviceRuntimeStatusServiceImpl implements DeviceRuntimeStatusServic
}
private BooleanSupplier receiverProbe() {
String productType = normalize(cisdPresetProperties.getProductType());
String productType = resolveReceiverProductType();
return "DIRECT".equals(productType) ? softwareRuntimeProbe::isDirectAppRunning : softwareRuntimeProbe::isStandardAppRunning;
}
private String resolveReceiverProductType() {
try {
String initState = normalize(initService.getDeviceInitStateSnapshot().initState());
if ("INITIALIZED".equals(initState)) {
CurrentInitConfigResponse current = initService.loadCurrentInitConfig();
String currentProductType = normalize(current == null ? null : current.getProductType());
if (!currentProductType.isBlank()) {
return currentProductType;
}
}
} catch (RuntimeException ignored) {
// Fallback to the preset product type when runtime init context is unavailable.
}
return normalize(cisdPresetProperties.getProductType());
}
@FunctionalInterface
private interface UsageSupplier {
UsageSnapshot get();
@@ -18,67 +18,128 @@ public class DefaultSoftwareRuntimeProbe implements SoftwareRuntimeProbe {
private final Supplier<List<String>> processSnapshotLoader;
private final Duration cacheTtl;
private final LongSupplier nanoTimeSupplier;
private final CommandExitCodeSupplier commandExitCodeSupplier;
private final Object lock = new Object();
private volatile CachedProcessSnapshot cachedSnapshot;
public DefaultSoftwareRuntimeProbe() {
this(DefaultSoftwareRuntimeProbe::loadProcessSnapshot, DEFAULT_CACHE_TTL, System::nanoTime);
this(DefaultSoftwareRuntimeProbe::loadProcessSnapshot, DEFAULT_CACHE_TTL, System::nanoTime, DefaultSoftwareRuntimeProbe::runCommand);
}
DefaultSoftwareRuntimeProbe(
Supplier<List<String>> processSnapshotLoader,
Duration cacheTtl,
LongSupplier nanoTimeSupplier
) {
this(processSnapshotLoader, cacheTtl, nanoTimeSupplier, DefaultSoftwareRuntimeProbe::runCommand);
}
DefaultSoftwareRuntimeProbe(
Supplier<List<String>> processSnapshotLoader,
Duration cacheTtl,
LongSupplier nanoTimeSupplier,
CommandExitCodeSupplier commandExitCodeSupplier
) {
this.processSnapshotLoader = Objects.requireNonNull(processSnapshotLoader, "processSnapshotLoader");
this.cacheTtl = Objects.requireNonNull(cacheTtl, "cacheTtl");
this.nanoTimeSupplier = Objects.requireNonNull(nanoTimeSupplier, "nanoTimeSupplier");
this.commandExitCodeSupplier = Objects.requireNonNull(commandExitCodeSupplier, "commandExitCodeSupplier");
}
@Override
public boolean isRabbitmqRunning() {
return isRunning("rabbitmq-server");
return commandSucceeds("bash", "-lc", "rabbitmqctl status >/dev/null 2>&1")
|| commandSucceeds("bash", "-lc", "systemctl is-active --quiet rabbitmq.service")
|| isRunningAny("beam.smp", "rabbitmq-server", "epmd");
}
@Override
public boolean isTlqRunning() {
return isRunning("tonglink|TLQ");
return isRunningAny("tonglink", "tlq", "tlqremote");
}
@Override
public boolean isTidbRunning() {
return isRunning("tidb-server");
return isRunningAny("tidb-server");
}
@Override
public boolean isNginxRunning() {
return isRunning("nginx");
return isRunningAny("nginx");
}
@Override
public boolean isStandardAppRunning() {
return isRunning("cmsp") && isRunning("cmtp");
return (commandSucceeds("bash", "-lc", "systemctl is-active --quiet cmsp.service") || isRunningAny("cmsp"))
&& (commandSucceeds("bash", "-lc", "systemctl is-active --quiet cmtp.service") || isRunningAny("cmtp"));
}
@Override
public boolean isDirectAppRunning() {
return isRunning("cmep4d|CAE");
boolean pmts = commandSucceeds("bash", "-lc", "systemctl is-active --quiet pmts.service")
|| isRunningAny("pmts", "pmtsstat");
boolean cmcp = commandSucceeds("bash", "-lc", "systemctl is-active --quiet cmcp.service")
|| isRunningAny("cmcp");
boolean cmsp = commandSucceeds("bash", "-lc", "systemctl is-active --quiet cmsp.service")
|| isRunningAny("cmsp");
boolean cmtp = commandSucceeds("bash", "-lc", "systemctl is-active --quiet cmtp.service")
|| isRunningAny("cmtp");
boolean cmtp4cae = commandSucceeds("bash", "-lc", "systemctl is-active --quiet cmtp4cae.service")
|| isRunningAny("cmtp4cae");
return pmts && cmcp && cmsp && cmtp && cmtp4cae;
}
@Override
public boolean isTmsRunning() {
return isRunning("tms-framework.jar");
return isRunningAny("tms-framework.jar");
}
private boolean isRunning(String pattern) {
private boolean isRunningAll(String... tokens) {
for (String token : tokens) {
if (!isRunningAny(token)) {
return false;
}
}
return true;
}
private boolean isRunningAny(String... tokens) {
try {
Pattern compiled = Pattern.compile(pattern);
return processSnapshot().stream().anyMatch(commandLine -> compiled.matcher(commandLine).find());
List<Pattern> patterns = compilePatterns(tokens);
return processSnapshot().stream().anyMatch(commandLine -> matchesAny(commandLine, patterns));
} catch (PatternSyntaxException ex) {
throw new IllegalStateException("invalid process probe pattern: " + pattern, ex);
throw new IllegalStateException("invalid process probe tokens: " + String.join(",", tokens), ex);
} catch (RuntimeException ex) {
throw new IllegalStateException("process probe failed: " + pattern, ex);
throw new IllegalStateException("process probe failed: " + String.join(",", tokens), ex);
}
}
private List<Pattern> compilePatterns(String... tokens) {
return List.of(tokens).stream()
.filter(token -> token != null && !token.isBlank())
.map(this::compileTokenPattern)
.toList();
}
private Pattern compileTokenPattern(String token) {
return Pattern.compile("(?i)(?<![a-z0-9])" + Pattern.quote(token.trim()) + "(?![a-z0-9])");
}
private boolean matchesAny(String commandLine, List<Pattern> patterns) {
for (Pattern pattern : patterns) {
if (pattern.matcher(commandLine).find()) {
return true;
}
}
return false;
}
private boolean commandSucceeds(String... command) {
try {
return commandExitCodeSupplier.get(command) == 0;
} catch (RuntimeException ex) {
return false;
}
}
@@ -130,6 +191,23 @@ public class DefaultSoftwareRuntimeProbe implements SoftwareRuntimeProbe {
}
}
private static int runCommand(String... command) {
try {
Process process = new ProcessBuilder(command).start();
return process.waitFor();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException("command interrupted", ex);
} catch (IOException ex) {
throw new IllegalStateException("command failed", ex);
}
}
@FunctionalInterface
interface CommandExitCodeSupplier {
int get(String... command);
}
private record CachedProcessSnapshot(List<String> commandLines, long loadedAtNanos) {
}
}
@@ -82,7 +82,19 @@ public class InitExecutorProperties {
private String directDbScriptDir = "/opt/CAE_Install_Base_Resource/DB";
// 直参版 TLQ init_cfg.sh 脚本目录
private String directTlqInitCfgDir = "/opt/CAE_Install_App_Resource/CAE";
// CFMQ 配置落地目录(为空表示只做文件存在校验,不做拷贝)
// 直参版 CAE 客户化配置目标目录
private String directCaeTargetDir = "/pmts/cfg";
// 直参版 CAE 更新脚本目录
private String directCaeUpdateScriptDir = "/opt/CAE_Install_App_Resource/CAE";
// 直参版 CAE 更新命令
private String directCaeUpdateCommand = "bash ./cae_update.sh";
// TLQ 网关脚本目标目录
private String directTlqGatewayScriptTargetDir = "/home/tlq/bin";
// TLQ 重启命令
private String directTlqRestartCommand = "systemctl restart tonglinkmq.service";
// CFMQ 运行目录
private String directCfmqInstallPath = "/home/cfmq/dev/cfmq";
// CFMQ 配置落地目录(兼容旧实现)
private String cfmqTargetDir = "/pmts/cfg";
// RabbitMQ 管理员账号(预制)
private String rabbitAdminUsername = "admin";
@@ -392,6 +404,54 @@ public class InitExecutorProperties {
this.directTlqLicenseTargetPath = directTlqLicenseTargetPath;
}
public String getDirectCaeTargetDir() {
return directCaeTargetDir;
}
public void setDirectCaeTargetDir(String directCaeTargetDir) {
this.directCaeTargetDir = directCaeTargetDir;
}
public String getDirectCaeUpdateScriptDir() {
return directCaeUpdateScriptDir;
}
public void setDirectCaeUpdateScriptDir(String directCaeUpdateScriptDir) {
this.directCaeUpdateScriptDir = directCaeUpdateScriptDir;
}
public String getDirectCaeUpdateCommand() {
return directCaeUpdateCommand;
}
public void setDirectCaeUpdateCommand(String directCaeUpdateCommand) {
this.directCaeUpdateCommand = directCaeUpdateCommand;
}
public String getDirectTlqGatewayScriptTargetDir() {
return directTlqGatewayScriptTargetDir;
}
public void setDirectTlqGatewayScriptTargetDir(String directTlqGatewayScriptTargetDir) {
this.directTlqGatewayScriptTargetDir = directTlqGatewayScriptTargetDir;
}
public String getDirectTlqRestartCommand() {
return directTlqRestartCommand;
}
public void setDirectTlqRestartCommand(String directTlqRestartCommand) {
this.directTlqRestartCommand = directTlqRestartCommand;
}
public String getDirectCfmqInstallPath() {
return directCfmqInstallPath;
}
public void setDirectCfmqInstallPath(String directCfmqInstallPath) {
this.directCfmqInstallPath = directCfmqInstallPath;
}
public String getCfmqTargetDir() {
return cfmqTargetDir;
}
@@ -1,6 +1,8 @@
package com.cisd.tms.modules.init.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.ArrayList;
import java.util.List;
@Schema(description = "当前已初始化的 CISD 核心配置响应")
public class CurrentInitConfigResponse {
@@ -27,6 +29,12 @@ public class CurrentInitConfigResponse {
private String node02Ip;
@Schema(description = "CIPS 中心网关地址", example = "192.168.10.20")
private String cipsGatewayHost;
@Schema(description = "TLQ 相关文件名列表,包含 TLQ License、TLQ 配置包等")
private List<String> tlqFileNames = new ArrayList<>();
@Schema(description = "CFMQ 相关文件名列表,包含 CFMQ 配置包、CFMQ 配置脚本等")
private List<String> cfmqFileNames = new ArrayList<>();
@Schema(description = "收发器 LICENSE 文件名", example = "receiver-license.dat")
private String receiverLicenseFileName;
public String getOrgCodeType() {
return orgCodeType;
@@ -115,4 +123,28 @@ public class CurrentInitConfigResponse {
public void setCipsGatewayHost(String cipsGatewayHost) {
this.cipsGatewayHost = cipsGatewayHost;
}
public List<String> getTlqFileNames() {
return tlqFileNames;
}
public void setTlqFileNames(List<String> tlqFileNames) {
this.tlqFileNames = tlqFileNames;
}
public List<String> getCfmqFileNames() {
return cfmqFileNames;
}
public void setCfmqFileNames(List<String> cfmqFileNames) {
this.cfmqFileNames = cfmqFileNames;
}
public String getReceiverLicenseFileName() {
return receiverLicenseFileName;
}
public void setReceiverLicenseFileName(String receiverLicenseFileName) {
this.receiverLicenseFileName = receiverLicenseFileName;
}
}
@@ -2,8 +2,6 @@ package com.cisd.tms.modules.init.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
@@ -51,14 +49,9 @@ public class InitPreviewRequest {
private Mq mq;
@Valid
@NotNull(message = "licenses is required")
@Schema(description = "初始化许可证与配置包引用")
private Licenses licenses;
@Valid
@Schema(description = "直连 TLQ 配置,直参版按需填写")
private DirectTlqConfig directTlq;
public String getOrgCodeType() {
return orgCodeType;
}
@@ -123,14 +116,6 @@ public class InitPreviewRequest {
this.licenses = licenses;
}
public DirectTlqConfig getDirectTlq() {
return directTlq;
}
public void setDirectTlq(DirectTlqConfig directTlq) {
this.directTlq = directTlq;
}
@Schema(description = "节点 IP 配置")
public static class Nodes {
@@ -193,28 +178,34 @@ public class InitPreviewRequest {
@NotBlank(message = "mq.channelUsername is required")
@Size(max = 32, message = "mq.channelUsername length must be <= 32")
@Schema(description = "通道用户名", example = "tms")
@Schema(description = "RABBITMQ通道用户名", example = "tms")
private String channelUsername;
@NotBlank(message = "mq.channelPassword is required")
@Size(max = 128, message = "mq.channelPassword length must be <= 128")
@Schema(description = "通道密码")
@Schema(description = "RABBITMQ通道密码")
private String channelPassword;
@Size(max = 128, message = "mq.tlqLicenseFileId length must be <= 128")
@Pattern(regexp = FILE_ID_REGEX, message = "mq.tlqLicenseFileId must be a fileId")
@Schema(description = "TLQ 许可证文件 fileId", example = "FILE-20260310-000001")
@Schema(description = "TLQ 许可证文件 fileId。作用:用于 TLQ 相关模式的许可证下发。使用条件:`TLQ` 或 `RABBITMQ_TLQ` 场景必填", example = "FILE-20260310-000001")
private String tlqLicenseFileId;
@Size(max = 64, message = "mq.cipsGatewayHost length must be <= 64")
@Schema(description = "CIPS 中心网关地址。作用:回写到 TLQ 网关脚本中的中心网关主机地址。使用条件:仅 `DIRECT + RABBITMQ_TLQ` 必填", example = "192.168.10.20")
private String cipsGatewayHost;
@Size(max = 128, message = "mq.cfgZipFileId length must be <= 128")
@Pattern(regexp = FILE_ID_REGEX, message = "mq.cfgZipFileId must be a fileId")
@Schema(description = "直参 PMTS-CAE 客户化配置包 fileId。使用条件:仅 `DIRECT` 使用;`RABBITMQ_TLQ`必填", example = "FILE-20260310-000201")
private String cfgZipFileId;
@Size(max = 128, message = "mq.cfmqConfigFileId length must be <= 128")
@Pattern(regexp = FILE_ID_REGEX, message = "mq.cfmqConfigFileId must be a fileId")
@Schema(description = "CFMQ 配置文件 fileId", example = "FILE-20260310-000002")
@Schema(description = "直参 CFMQ 客户化脚本/配置 fileId。使用条件:仅 `DIRECT + RABBITMQ_CFMQ` 必填", example = "FILE-20260310-000002")
private String cfmqConfigFileId;
@Size(max = 64, message = "mq.cipsGatewayHost length must be <= 64")
@Schema(description = "CIPS 中心网关地址,仅 DIRECT + RABBITMQ_TLQ 使用", example = "192.168.10.20")
private String cipsGatewayHost;
public String getMqType() {
return mqType;
}
@@ -262,6 +253,15 @@ public class InitPreviewRequest {
public void setCipsGatewayHost(String cipsGatewayHost) {
this.cipsGatewayHost = cipsGatewayHost;
}
public String getCfgZipFileId() {
return cfgZipFileId;
}
public void setCfgZipFileId(String cfgZipFileId) {
this.cfgZipFileId = cfgZipFileId;
}
}
@Schema(description = "许可证和配置包引用")
@@ -275,7 +275,7 @@ public class InitPreviewRequest {
@Size(max = 128, message = "licenses.cfgZipFileId length must be <= 128")
@Pattern(regexp = FILE_ID_REGEX, message = "licenses.cfgZipFileId must be a fileId")
@Schema(description = "标准版配置包 zip 文件 fileId", example = "FILE-20260310-000004")
@Schema(description = "标准版配置包 zip 文件 fileId。作用:用于标准版/间参版初始化时引用配置包。使用条件:`ENTERPRISE`、`INDIRECT` 场景按对应流程使用,`DIRECT` 不使用此字段", example = "FILE-20260310-000004")
private String cfgZipFileId;
public String getReceiverLicenseFileId() {
@@ -294,45 +294,4 @@ public class InitPreviewRequest {
this.cfgZipFileId = cfgZipFileId;
}
}
@Schema(description = "直连 TLQ 配置")
public static class DirectTlqConfig {
@Pattern(regexp = "A|B", message = "directTlq.localNodeSeq must be A/B")
@Schema(description = "本节点序号", example = "A")
private String localNodeSeq;
@Size(max = 256, message = "directTlq.upstreamInstitutions length must be <= 256")
@Schema(description = "上游机构列表", example = "BANKA,BANKB")
private String upstreamInstitutions;
@Min(value = 1, message = "directTlq.upstreamCocCount must be >= 1")
@Max(value = 2, message = "directTlq.upstreamCocCount must be <= 2")
@Schema(description = "上游 COC 数量", example = "2")
private Integer upstreamCocCount;
public String getLocalNodeSeq() {
return localNodeSeq;
}
public void setLocalNodeSeq(String localNodeSeq) {
this.localNodeSeq = localNodeSeq;
}
public String getUpstreamInstitutions() {
return upstreamInstitutions;
}
public void setUpstreamInstitutions(String upstreamInstitutions) {
this.upstreamInstitutions = upstreamInstitutions;
}
public Integer getUpstreamCocCount() {
return upstreamCocCount;
}
public void setUpstreamCocCount(Integer upstreamCocCount) {
this.upstreamCocCount = upstreamCocCount;
}
}
}
@@ -149,7 +149,12 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
case "verify_reset" -> executeVerifyReset(task, step);
case "direct_db_init" -> executeDirectDbInit(task, step);
case "tlq_init_cfg" -> executeTlqInitCfg(task, step);
case "cfmq_apply" -> executeCfmqApply(task, step);
case "cae_package_apply" -> executeCaePackageApply(task, step);
case "cae_update" -> executeCaeUpdate(task, step);
case "tlq_gateway_apply" -> executeTlqGatewayApply(task, step);
case "restart_tlq" -> executeRestartTlq(task, step);
case "cfmq_link_apply", "cfmq_apply" -> executeCfmqLinkApply(task, step);
case "cfmq_route_apply" -> executeCfmqRouteApply(task, step);
case "verify" -> executeVerify(task, step);
default -> {
String logPath = writeInternalLog(task, step, "skip unsupported internal step: " + internalCode);
@@ -177,6 +182,18 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
: buildStandardReplacements(context);
int changed = replaceConfigValues(cpconfigPath, replacements);
String message = "cpconfig rendered, path=" + cpconfigPath + ", changedKeys=" + changed;
if ("DIRECT".equals(context.productType())) {
Path targetCpconfigPath = Paths.get(firstNonBlank(properties.getDirectCpconfigTargetPath(), "/opt/cpconfig.cfg"))
.normalize()
.toAbsolutePath();
if (targetCpconfigPath.getParent() != null) {
Files.createDirectories(targetCpconfigPath.getParent());
}
if (!cpconfigPath.equals(targetCpconfigPath)) {
Files.copy(cpconfigPath, targetCpconfigPath, StandardCopyOption.REPLACE_EXISTING);
message = message + ", syncedTarget=" + targetCpconfigPath;
}
}
String logPath = writeInternalLog(task, step, message);
return InitStepExecutionResult.success(message, 0, logPath);
}
@@ -794,57 +811,8 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
String logPath = writeInternalLog(task, step, "direct_db_init skipped for non-direct product type");
return InitStepExecutionResult.success("direct_db_init skipped", 0, logPath);
}
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "direct_db_init simulated");
return InitStepExecutionResult.success("direct_db_init simulated", 0, logPath);
}
Path scriptDir = Paths.get(firstNonBlank(properties.getDirectDbScriptDir(), "/opt/CAE_Install_Base_Resource/DB"))
.normalize()
.toAbsolutePath();
if (!Files.isDirectory(scriptDir)) {
String logPath = writeInternalLog(task, step, "direct_db_init failed: script dir not found: " + scriptDir);
return InitStepExecutionResult.failure("direct_db_init failed: script dir not found", -1, logPath);
}
ResolvedCpconfig resolvedCpconfig = resolveCpconfigPath(task, context);
Path cpconfigPath = resolvedCpconfig.path();
if (!Files.exists(cpconfigPath) || !Files.isRegularFile(cpconfigPath)) {
String logPath = writeInternalLog(
task,
step,
"direct_db_init failed: cpconfig not found: " + cpconfigPath
+ ", candidates=" + formatPathList(resolvedCpconfig.candidates())
);
return InitStepExecutionResult.failure("direct_db_init failed: cpconfig not found", -1, logPath);
}
Path targetCpconfigPath = Paths.get(firstNonBlank(properties.getDirectCpconfigTargetPath(), "/opt/cpconfig.cfg"))
.normalize()
.toAbsolutePath();
try {
Path logFile = prepareLogPath(task, step);
if (targetCpconfigPath.getParent() != null) {
Files.createDirectories(targetCpconfigPath.getParent());
}
Files.copy(cpconfigPath, targetCpconfigPath, StandardCopyOption.REPLACE_EXISTING);
appendLog(logFile, "cpconfig prepared: " + cpconfigPath + " -> " + targetCpconfigPath);
int code = runCommand(
logFile,
Arrays.asList("bash", "./setsql.sh"),
false,
scriptDir
);
if (code != 0) {
return InitStepExecutionResult.failure("direct_db_init failed", code, logFile.toString());
}
return InitStepExecutionResult.success("direct db init applied", 0, logFile.toString());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
String logPath = writeInternalLog(task, step, "direct_db_init interrupted");
return InitStepExecutionResult.failure("direct_db_init interrupted", -1, logPath);
}
String logPath = writeInternalLog(task, step, "direct_db_init skipped: out of direct customer-config scope");
return InitStepExecutionResult.success("direct_db_init skipped", 0, logPath);
}
private InitStepExecutionResult executeStandardDbRender(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
@@ -1229,83 +1197,197 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
String logPath = writeInternalLog(task, step, "tlq_init_cfg skipped for non-direct TLQ mode");
return InitStepExecutionResult.success("tlq_init_cfg skipped", 0, logPath);
}
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "tlq_init_cfg simulated");
return InitStepExecutionResult.success("tlq_init_cfg simulated", 0, logPath);
String logPath = writeInternalLog(task, step, "tlq_init_cfg skipped: no longer part of direct customer-config scope");
return InitStepExecutionResult.success("tlq_init_cfg skipped", 0, logPath);
}
private InitStepExecutionResult executeCaePackageApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!"DIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, "cae_package_apply skipped for non-direct product type");
return InitStepExecutionResult.success("cae_package_apply skipped", 0, logPath);
}
String localNodeSeq = normalizeUpper(context.directTlqLocalNodeSeq());
String upstreamInstitutions = normalize(context.directTlqUpstreamInstitutions());
Integer upstreamCocCount = context.directTlqUpstreamCocCount();
if (!"A".equals(localNodeSeq) && !"B".equals(localNodeSeq)) {
String logPath = writeInternalLog(task, step, "tlq_init_cfg failed: localNodeSeq must be A/B");
return InitStepExecutionResult.failure("tlq_init_cfg failed: invalid localNodeSeq", -1, logPath);
}
if (isBlank(upstreamInstitutions)) {
String logPath = writeInternalLog(task, step, "tlq_init_cfg failed: upstreamInstitutions is blank");
return InitStepExecutionResult.failure("tlq_init_cfg failed: upstreamInstitutions is blank", -1, logPath);
}
if (upstreamCocCount == null || upstreamCocCount < 1 || upstreamCocCount > 2) {
String logPath = writeInternalLog(task, step, "tlq_init_cfg failed: upstreamCocCount must be in [1,2]");
return InitStepExecutionResult.failure("tlq_init_cfg failed: invalid upstreamCocCount", -1, logPath);
}
Path zipFile = resolveAndStageInputFile(task, context.cfgZipFileId(), "cae_package", null);
Path unpackDir = resolveDirectStageDir(task, "cae/unpacked");
recreateDirectory(unpackDir);
unzipToDirectory(zipFile, unpackDir);
Path scriptDir = Paths.get(firstNonBlank(properties.getDirectTlqInitCfgDir(), "/opt/CAE_Install_App_Resource/CAE"))
Path targetDir = Paths.get(firstNonBlank(properties.getDirectCaeTargetDir(), properties.getCfmqTargetDir()))
.normalize()
.toAbsolutePath();
if (!Files.isDirectory(scriptDir)) {
String logPath = writeInternalLog(task, step, "tlq_init_cfg failed: script dir not found: " + scriptDir);
return InitStepExecutionResult.failure("tlq_init_cfg failed: script dir not found", -1, logPath);
Files.createDirectories(targetDir);
copyDirectoryContents(unpackDir, targetDir);
String message = "cae package applied: " + zipFile + " -> " + targetDir;
String logPath = writeInternalLog(task, step, message);
return InitStepExecutionResult.success(message, 0, logPath);
}
private InitStepExecutionResult executeCaeUpdate(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!"DIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, "cae_update skipped for non-direct product type");
return InitStepExecutionResult.success("cae_update skipped", 0, logPath);
}
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "cae_update simulated");
return InitStepExecutionResult.success("cae_update simulated", 0, logPath);
}
String command = normalize(properties.getDirectCaeUpdateCommand());
if (isBlank(command)) {
String logPath = writeInternalLog(task, step, "cae_update validated only: command is blank");
return InitStepExecutionResult.success("cae_update validated", 0, logPath);
}
Path scriptDir = Paths.get(firstNonBlank(properties.getDirectCaeUpdateScriptDir(), "/opt/CAE_Install_App_Resource/CAE"))
.normalize()
.toAbsolutePath();
Path logFile = prepareLogPath(task, step);
try {
Path logFile = prepareLogPath(task, step);
int code = runCommand(
logFile,
Arrays.asList(
"bash",
"./init_cfg.sh",
normalize(context.orgCode()),
localNodeSeq,
"TLQ",
upstreamInstitutions,
String.valueOf(upstreamCocCount)
),
false,
scriptDir
);
int code = runCommand(logFile, Arrays.asList("/bin/zsh", "-lc", command), false, scriptDir);
if (code != 0) {
return InitStepExecutionResult.failure("tlq_init_cfg failed", code, logFile.toString());
return InitStepExecutionResult.failure("cae_update failed", code, logFile.toString());
}
return InitStepExecutionResult.success("tlq_init_cfg applied", 0, logFile.toString());
return InitStepExecutionResult.success("cae_update applied", 0, logFile.toString());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
String logPath = writeInternalLog(task, step, "tlq_init_cfg interrupted");
return InitStepExecutionResult.failure("tlq_init_cfg interrupted", -1, logPath);
return InitStepExecutionResult.failure("cae_update interrupted", -1, logFile.toString());
}
}
private InitStepExecutionResult executeCfmqApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
private InitStepExecutionResult executeTlqGatewayApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!"DIRECT".equals(context.productType()) || !"RABBITMQ_TLQ".equals(context.mqType())) {
String logPath = writeInternalLog(task, step, "tlq_gateway_apply skipped for non-direct TLQ mode");
return InitStepExecutionResult.success("tlq_gateway_apply skipped", 0, logPath);
}
Path sourceScript = resolveTlqGatewaySourceScript(task);
if (!Files.exists(sourceScript) || !Files.isRegularFile(sourceScript)) {
String logPath = writeInternalLog(task, step, "tlq_gateway_apply failed: source script not found: " + sourceScript);
return InitStepExecutionResult.failure("tlq_gateway_apply failed: source script not found", -1, logPath);
}
String rewritten = rewriteGatewayHost(Files.readString(sourceScript), context.cipsGatewayHost());
Path targetDir = Paths.get(firstNonBlank(properties.getDirectTlqGatewayScriptTargetDir(), "/home/tlq/bin"))
.normalize()
.toAbsolutePath();
Files.createDirectories(targetDir);
Path targetScript = targetDir.resolve(sourceScript.getFileName());
Files.writeString(targetScript, rewritten, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "tlq_gateway_apply simulated: " + targetScript);
return InitStepExecutionResult.success("tlq_gateway_apply simulated", 0, logPath);
}
Path logFile = prepareLogPath(task, step);
try {
int code = runCommand(logFile, Arrays.asList("bash", targetScript.toString()), false);
if (code != 0) {
return InitStepExecutionResult.failure("tlq_gateway_apply failed", code, logFile.toString());
}
return InitStepExecutionResult.success("tlq gateway applied: " + targetScript, 0, logFile.toString());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return InitStepExecutionResult.failure("tlq_gateway_apply interrupted", -1, logFile.toString());
}
}
private InitStepExecutionResult executeRestartTlq(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!"DIRECT".equals(context.productType()) || !"RABBITMQ_TLQ".equals(context.mqType())) {
String logPath = writeInternalLog(task, step, "restart_tlq skipped for non-direct TLQ mode");
return InitStepExecutionResult.success("restart_tlq skipped", 0, logPath);
}
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "restart_tlq simulated");
return InitStepExecutionResult.success("restart_tlq simulated", 0, logPath);
}
String command = normalize(properties.getDirectTlqRestartCommand());
if (isBlank(command)) {
String logPath = writeInternalLog(task, step, "restart_tlq validated only: command is blank");
return InitStepExecutionResult.success("restart_tlq validated", 0, logPath);
}
Path logFile = prepareLogPath(task, step);
try {
int code = runCommand(logFile, Arrays.asList("/bin/zsh", "-lc", command), false);
if (code != 0) {
return InitStepExecutionResult.failure("restart_tlq failed", code, logFile.toString());
}
return InitStepExecutionResult.success("restart_tlq applied", 0, logFile.toString());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return InitStepExecutionResult.failure("restart_tlq interrupted", -1, logFile.toString());
}
}
private InitStepExecutionResult executeCfmqLinkApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!"DIRECT".equals(context.productType()) || !"RABBITMQ_CFMQ".equals(context.mqType())) {
String logPath = writeInternalLog(task, step, "mq is not RABBITMQ_CFMQ, cfmq_apply skipped");
return InitStepExecutionResult.success("cfmq_apply skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "cfmq_link_apply skipped for non-direct CFMQ mode");
return InitStepExecutionResult.success("cfmq_link_apply skipped", 0, logPath);
}
Path source = resolveAndStageInputFile(task, context.cfmqConfigFileId(), "cfmq_config", null);
String targetDirValue = normalize(properties.getCfmqTargetDir());
if (isBlank(targetDirValue)) {
String logPath = writeInternalLog(task, step, "cfmq config validated only: " + source);
return InitStepExecutionResult.success("cfmq config validated", 0, logPath);
Path sourceScript = resolveAndStageInputFile(task, context.cfmqConfigFileId(), "cfmq_link_script", null);
String rewritten = Files.readString(sourceScript)
.replace("/cfmq/cfmq-server", normalize(properties.getDirectCfmqInstallPath()));
Path stagedScript = resolveDirectStageDir(task, "cfmq/link").resolve(sourceScript.getFileName());
Files.createDirectories(stagedScript.getParent());
Files.writeString(stagedScript, rewritten, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "cfmq_link_apply simulated: " + stagedScript);
return InitStepExecutionResult.success("cfmq_link_apply simulated", 0, logPath);
}
Path targetDir = Paths.get(targetDirValue).normalize().toAbsolutePath();
Files.createDirectories(targetDir);
Path target = targetDir.resolve(source.getFileName());
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
String message = "cfmq config applied: " + source + " -> " + target;
String logPath = writeInternalLog(task, step, message);
return InitStepExecutionResult.success(message, 0, logPath);
Path logFile = prepareLogPath(task, step);
try {
int code = runCommand(logFile, Arrays.asList("bash", stagedScript.toString()), false);
if (code != 0) {
return InitStepExecutionResult.failure("cfmq_link_apply failed", code, logFile.toString());
}
return InitStepExecutionResult.success("cfmq_link_apply applied", 0, logFile.toString());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return InitStepExecutionResult.failure("cfmq_link_apply interrupted", -1, logFile.toString());
}
}
private InitStepExecutionResult executeCfmqRouteApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!"DIRECT".equals(context.productType()) || !"RABBITMQ_CFMQ".equals(context.mqType())) {
String logPath = writeInternalLog(task, step, "cfmq_route_apply skipped for non-direct CFMQ mode");
return InitStepExecutionResult.success("cfmq_route_apply skipped", 0, logPath);
}
Path targetDir = Paths.get(firstNonBlank(properties.getDirectCaeTargetDir(), properties.getCfmqTargetDir()))
.normalize()
.toAbsolutePath();
Path logFile = prepareLogPath(task, step);
int routeChanged = 0;
try (Stream<Path> stream = Files.walk(targetDir)) {
for (Path item : stream.filter(Files::isRegularFile).toList()) {
String fileName = item.getFileName() == null ? "" : item.getFileName().toString();
if (fileName.startsWith("route_") && fileName.endsWith(".ini")) {
String content = Files.readString(item);
String rewritten = content.replace("MSGMB_1", "MSGTOMB_1");
if (!rewritten.equals(content)) {
Files.writeString(item, rewritten, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
routeChanged++;
}
}
}
}
Path pmtsMsgHdl = targetDir.resolve("pmtsmsghdl.cfg");
Files.deleteIfExists(pmtsMsgHdl);
appendLog(logFile, "cfmq route files changed=" + routeChanged + ", pmtsmsghdl deleted if exists=" + pmtsMsgHdl);
return InitStepExecutionResult.success("cfmq_route_apply applied", 0, logFile.toString());
}
private InitStepExecutionResult executeVerify(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
@@ -1363,12 +1445,6 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
private void verifyDirect(Map<String, String> configMap, PlanContext context, List<String> failures) {
requireNotBlank(configMap, "MQ_TYPE", failures);
requireNotBlank(configMap, "node_01_ip", failures);
requireNotBlank(configMap, "db_ip", failures);
requireNotBlank(configMap, "sign_host", failures);
requireNotBlank(configMap, "sign_port", failures);
requireNotBlank(configMap, "sign_type", failures);
requireNotBlank(configMap, "sign_cert_dn", failures);
requireNotBlank(configMap, "sign_cert_sn", failures);
if ("DUAL".equals(context.deployMode()) || "QUAD".equals(context.deployMode())) {
requireNotBlank(configMap, "node_02_ip", failures);
@@ -1432,13 +1508,6 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
replacements.put("node_02_ip", node02);
replacements.put("node_03_ip", node03);
replacements.put("node_04_ip", node04);
replacements.put("db_ip", node01);
replacements.put("sign_host", context.signHost());
replacements.put("sign_port", context.signPort() == null ? "" : String.valueOf(context.signPort()));
replacements.put("sign_password", quoteShellValue(context.signPassword()));
replacements.put("sign_type", context.signType());
replacements.put("sign_cert_dn", quoteShellValue(context.signCertDn()));
replacements.put("sign_cert_sn", context.signCertSn());
fillOrgCodeMappings(context, replacements);
return replacements;
}
@@ -1531,6 +1600,12 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
}
private ResolvedCpconfig resolveCpconfigPath(InitTaskEntity task, PlanContext context) throws IOException {
if ("DIRECT".equals(context.productType())) {
Path stagedCpconfig = resolveDirectStageCpconfigPath(task);
if (Files.exists(stagedCpconfig) && Files.isRegularFile(stagedCpconfig)) {
return new ResolvedCpconfig(stagedCpconfig, List.of(stagedCpconfig));
}
}
List<Path> candidates = buildCpconfigCandidates(context);
if (candidates.isEmpty()) {
Path uploadBase = Paths.get(firstNonBlank(properties.getUploadBaseDir(), "/home/tms/uploads"))
@@ -1541,7 +1616,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
for (Path candidate : candidates) {
if (Files.exists(candidate) && Files.isRegularFile(candidate)) {
Path cpconfig = candidate;
if ("DIRECT".equals(context.productType()) && !isBlank(context.cfgZipFileId())) {
if ("DIRECT".equals(context.productType())) {
cpconfig = stageFile(task, "cpconfig", firstNonBlank(properties.getDirectCpconfigFileName(), "cpconfig.cfg"), candidate);
}
return new ResolvedCpconfig(cpconfig, candidates);
@@ -1550,10 +1625,27 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
return new ResolvedCpconfig(candidates.get(0), candidates);
}
private Path resolveDirectStageCpconfigPath(InitTaskEntity task) {
Path stageRoot = Paths.get(firstNonBlank(properties.getStagingRootDir(), "/tmp/tms-init-staging"))
.normalize()
.toAbsolutePath();
return stageRoot.resolve(task.getTaskId())
.resolve("inputs")
.resolve("cpconfig")
.resolve(firstNonBlank(properties.getDirectCpconfigFileName(), "cpconfig.cfg"))
.normalize()
.toAbsolutePath();
}
private List<Path> buildCpconfigCandidates(PlanContext context) {
String cpconfigFileName = firstNonBlank(properties.getDirectCpconfigFileName(), "cpconfig.cfg");
if ("DIRECT".equals(context.productType())) {
return buildFileIdCandidates(context.cfgZipFileId(), cpconfigFileName);
List<Path> candidates = new ArrayList<>();
addAbsoluteCandidate(candidates, properties.getDirectCpconfigTargetPath());
addAbsoluteCandidate(candidates, Paths.get(firstNonBlank(properties.getDirectCpconfigTargetPath(), "/opt/cpconfig.cfg"))
.resolveSibling(cpconfigFileName)
.toString());
return deduplicatePaths(candidates);
}
List<Path> candidates = new ArrayList<>();
@@ -2024,18 +2116,6 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
JsonNode nodes = request.path("nodes");
JsonNode mq = request.path("mq");
JsonNode licenses = request.path("licenses");
JsonNode signServer = request.path("signServer");
JsonNode directTlq = request.path("directTlq");
Integer signPort = null;
if (!signServer.path("signPort").isMissingNode() && !signServer.path("signPort").isNull()) {
signPort = signServer.path("signPort").asInt();
}
Integer directTlqUpstreamCocCount = null;
if (!directTlq.path("upstreamCocCount").isMissingNode() && !directTlq.path("upstreamCocCount").isNull()) {
directTlqUpstreamCocCount = directTlq.path("upstreamCocCount").asInt();
}
return new PlanContext(
firstNonBlank(normalizeUpper(text(root, "productType")), normalizeUpper(task.getProductType())),
normalizeUpper(text(request, "orgCodeType")),
@@ -2051,18 +2131,10 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
text(nodes, "node03Ip"),
text(nodes, "node04Ip"),
text(mq, "tlqLicenseFileId"),
text(mq, "cfmqConfigFileId"),
text(licenses, "receiverLicenseFileId"),
text(licenses, "cfgZipFileId"),
text(signServer, "signHost"),
signPort,
text(signServer, "signType"),
text(signServer, "signCertDn"),
text(signServer, "signCertSn"),
text(signServer, "signPassword"),
text(directTlq, "localNodeSeq"),
text(directTlq, "upstreamInstitutions"),
directTlqUpstreamCocCount
text(mq, "cipsGatewayHost"),
text(mq, "cfgZipFileId"),
text(mq, "cfmqConfigFileId")
);
}
@@ -2130,6 +2202,46 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
return "'" + escaped + "'";
}
private Path resolveDirectStageDir(InitTaskEntity task, String relative) {
Path stageRoot = Paths.get(firstNonBlank(properties.getStagingRootDir(), "/tmp/tms-init-staging"))
.normalize()
.toAbsolutePath();
return stageRoot.resolve(task.getTaskId()).resolve("direct").resolve(relative).normalize().toAbsolutePath();
}
private Path resolveTlqGatewaySourceScript(InitTaskEntity task) throws IOException {
Path caeTargetDir = Paths.get(firstNonBlank(properties.getDirectCaeTargetDir(), properties.getCfmqTargetDir()))
.normalize()
.toAbsolutePath();
Path preferred = caeTargetDir.resolve("bin").resolve("tlq_gateway.sh").normalize().toAbsolutePath();
if (Files.exists(preferred) && Files.isRegularFile(preferred)) {
return preferred;
}
try (Stream<Path> stream = Files.walk(caeTargetDir)) {
for (Path item : stream.filter(Files::isRegularFile).toList()) {
String name = item.getFileName() == null ? "" : item.getFileName().toString().toLowerCase(Locale.ROOT);
if (name.contains("gateway") && name.endsWith(".sh")) {
return item;
}
}
}
return preferred;
}
private String rewriteGatewayHost(String content, String gatewayHost) {
String normalizedHost = normalize(gatewayHost);
String rewritten = normalize(content)
.replaceAll("(?m)^(\\s*gateway\\s*=\\s*).*$", "$1" + normalizedHost)
.replaceAll("(?m)^(\\s*host\\s*=\\s*).*$", "$1" + normalizedHost);
if (rewritten.equals(normalize(content))) {
rewritten = normalize(content).replace("127.0.0.1", normalizedHost);
}
if (!rewritten.endsWith(System.lineSeparator())) {
rewritten = rewritten + System.lineSeparator();
}
return rewritten;
}
private void verifyProcessAbsent(Path logFile, String pattern, String label, List<String> failures) throws IOException {
try {
int code = runCommand(
@@ -2249,18 +2361,10 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
String node03Ip,
String node04Ip,
String tlqLicenseFileId,
String cfmqConfigFileId,
String receiverLicenseFileId,
String cipsGatewayHost,
String cfgZipFileId,
String signHost,
Integer signPort,
String signType,
String signCertDn,
String signCertSn,
String signPassword,
String directTlqLocalNodeSeq,
String directTlqUpstreamInstitutions,
Integer directTlqUpstreamCocCount
String cfmqConfigFileId
) {
}
@@ -3,6 +3,7 @@ package com.cisd.tms.modules.init.service.impl;
import com.cisd.tms.common.config.properties.CisdPresetProperties;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.file.repository.FileRecordRepository;
import com.cisd.tms.modules.init.config.InitCommandProfileService;
import com.cisd.tms.modules.init.config.InitExecutorProperties;
import com.cisd.tms.modules.init.dto.InitCreateTaskResponse;
@@ -59,6 +60,7 @@ public class InitServiceImpl implements InitService {
private final InitTaskRepository initTaskRepository;
private final InitTaskStepRepository initTaskStepRepository;
private final FileRecordRepository fileRecordRepository;
private final CisdPresetProperties cisdPresetProperties;
private final ObjectMapper objectMapper;
private final InitStepExecutor initStepExecutor;
@@ -507,9 +509,25 @@ public class InitServiceImpl implements InitService {
// 该步骤序列只描述“逻辑编排”,具体脚本命令在 step-command-profiles.json 中定义
if ("DIRECT".equals(productType)) {
if ("RABBITMQ_CFMQ".equals(mqType)) {
return List.of("RENDER_CONFIG", "FILE_PREPARE", "MQ_USER_APPLY", "DB_INIT", "CFMQ_APPLY", "VERIFY");
return List.of(
"RENDER_CONFIG",
"MQ_USER_APPLY",
"CFMQ_LINK_APPLY",
"CAE_PACKAGE_APPLY",
"CFMQ_ROUTE_APPLY",
"CAE_UPDATE",
"VERIFY"
);
}
return List.of("RENDER_CONFIG", "FILE_PREPARE", "MQ_USER_APPLY", "DB_INIT", "TLQ_INIT_CFG", "VERIFY");
return List.of(
"RENDER_CONFIG",
"MQ_USER_APPLY",
"CAE_PACKAGE_APPLY",
"TLQ_GATEWAY_APPLY",
"CAE_UPDATE",
"RESTART_TLQ",
"VERIFY"
);
}
if ("ENTERPRISE".equals(productType) && "TLQ".equals(mqType)) {
return List.of(
@@ -621,9 +639,6 @@ public class InitServiceImpl implements InitService {
if (request.getMq() == null) {
throw new IllegalArgumentException("mq is required");
}
if (request.getLicenses() == null) {
throw new IllegalArgumentException("licenses is required");
}
requireNotBlank("orgCode", request.getOrgCode());
requireNotBlank("deployMode", request.getDeployMode());
@@ -631,12 +646,14 @@ public class InitServiceImpl implements InitService {
requireNotBlank("mq.mqType", request.getMq().getMqType());
requireNotBlank("mq.channelUsername", request.getMq().getChannelUsername());
requireNotBlank("mq.channelPassword", request.getMq().getChannelPassword());
requireNotBlank("licenses.receiverLicenseFileId", request.getLicenses().getReceiverLicenseFileId());
requireFileId("licenses.receiverLicenseFileId", request.getLicenses().getReceiverLicenseFileId());
if (request.getLicenses() != null) {
requireOptionalFileId("licenses.receiverLicenseFileId", request.getLicenses().getReceiverLicenseFileId());
requireOptionalFileId("licenses.cfgZipFileId", request.getLicenses().getCfgZipFileId());
}
requireOptionalFileId("mq.tlqLicenseFileId", request.getMq().getTlqLicenseFileId());
requireOptionalFileId("mq.cfmqConfigFileId", request.getMq().getCfmqConfigFileId());
requireOptionalFileId("licenses.cfgZipFileId", request.getLicenses().getCfgZipFileId());
requireOptionalFileId("mq.cfgZipFileId", request.getMq().getCfgZipFileId());
}
private void validateByProductType(String productType, InitPreviewRequest request) {
@@ -709,8 +726,8 @@ public class InitServiceImpl implements InitService {
JsonNode root = objectMapper.readTree(task.getInitPlanJson());
JsonNode request = root.path("request");
JsonNode mq = request.path("mq");
JsonNode licenses = request.path("licenses");
JsonNode nodes = request.path("nodes");
JsonNode directTlq = request.path("directTlq");
CurrentInitConfigResponse response = new CurrentInitConfigResponse();
response.setOrgCodeType(firstNonBlank(normalizeUpper(text(request, "orgCodeType")), normalizeUpper(text(root, "orgCodeType"))));
@@ -724,12 +741,60 @@ public class InitServiceImpl implements InitService {
response.setNode01Ip(firstNonBlank(text(nodes, "node01Ip"), text(request, "node01Ip")));
response.setNode02Ip(firstNonBlank(text(nodes, "node02Ip"), text(request, "node02Ip")));
response.setCipsGatewayHost(firstNonBlank(text(mq, "cipsGatewayHost"), text(request, "cipsGatewayHost")));
response.setReceiverLicenseFileName(resolveUploadedFileName(text(licenses, "receiverLicenseFileId")));
String mqType = firstNonBlank(normalizeUpper(text(mq, "mqType")), normalizeUpper(text(root, "mqType")), normalizeUpper(task.getMqType()));
response.setTlqFileNames(buildTlqFileNames(mq, mqType));
response.setCfmqFileNames(buildCfmqFileNames(mq, mqType));
return response;
} catch (JsonProcessingException ex) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "failed to parse successful init task snapshot");
}
}
private List<String> buildTlqFileNames(
JsonNode mq,
String mqType
) {
List<String> fileNames = new ArrayList<>();
if (!Set.of("TLQ", "RABBITMQ_TLQ").contains(normalizeUpper(mqType))) {
return fileNames;
}
addUploadedFileName(fileNames, text(mq, "tlqLicenseFileId"));
addUploadedFileName(fileNames, text(mq, "cfgZipFileId"));
return fileNames;
}
private List<String> buildCfmqFileNames(
JsonNode mq,
String mqType
) {
List<String> fileNames = new ArrayList<>();
if (!"RABBITMQ_CFMQ".equals(normalizeUpper(mqType))) {
return fileNames;
}
addUploadedFileName(fileNames, text(mq, "cfgZipFileId"));
addUploadedFileName(fileNames, text(mq, "cfmqConfigFileId"));
return fileNames;
}
private void addUploadedFileName(List<String> fileNames, String fileId) {
String fileName = resolveUploadedFileName(fileId);
if (isBlank(fileName)) {
return;
}
fileNames.add(fileName);
}
private String resolveUploadedFileName(String fileId) {
if (isBlank(fileId)) {
return null;
}
return fileRecordRepository.findByFileId(normalize(fileId))
.map(record -> normalize(record.getOriginalFilename()))
.filter(name -> !name.isEmpty())
.orElse(null);
}
private int compareTaskRecency(InitTaskEntity left, InitTaskEntity right) {
Comparator<LocalDateTime> comparator = Comparator.nullsFirst(LocalDateTime::compareTo);
int byUpdate = comparator.compare(left.getUpdateTime(), right.getUpdateTime());
@@ -746,6 +811,11 @@ public class InitServiceImpl implements InitService {
private void validateStandard(String productType, InitPreviewRequest request, String deployMode, String mqType) {
requireAllowed("deployMode", deployMode, DEPLOY_STANDARD);
requireAllowed("mq.mqType", mqType, resolveStandardMqAllowed(productType));
if (request.getLicenses() == null) {
throw new IllegalArgumentException("licenses is required");
}
requireNotBlank("licenses.receiverLicenseFileId", request.getLicenses().getReceiverLicenseFileId());
requireFileId("licenses.receiverLicenseFileId", request.getLicenses().getReceiverLicenseFileId());
if ("DUAL".equals(deployMode)) {
requireNotBlank("nodes.node02Ip", request.getNodes().getNode02Ip());
@@ -779,26 +849,14 @@ public class InitServiceImpl implements InitService {
requireNotBlank("nodes.node04Ip", request.getNodes().getNode04Ip());
}
requireNotBlank("licenses.cfgZipFileId", request.getLicenses().getCfgZipFileId());
requireFileId("licenses.cfgZipFileId", request.getLicenses().getCfgZipFileId());
if ("RABBITMQ_TLQ".equals(mqType)) {
requireNotBlank("mq.tlqLicenseFileId", request.getMq().getTlqLicenseFileId());
requireFileId("mq.tlqLicenseFileId", request.getMq().getTlqLicenseFileId());
InitPreviewRequest.DirectTlqConfig directTlq = request.getDirectTlq();
if (directTlq == null) {
throw new IllegalArgumentException("directTlq is required for RABBITMQ_TLQ");
}
requireNotBlank("directTlq.localNodeSeq", directTlq.getLocalNodeSeq());
requireNotBlank("directTlq.upstreamInstitutions", directTlq.getUpstreamInstitutions());
requireNotBlank("mq.cfgZipFileId", request.getMq().getCfgZipFileId());
requireFileId("mq.cfgZipFileId", request.getMq().getCfgZipFileId());
requireNotBlank("mq.cipsGatewayHost", request.getMq().getCipsGatewayHost());
if (directTlq.getUpstreamCocCount() == null
|| directTlq.getUpstreamCocCount() < 1
|| directTlq.getUpstreamCocCount() > 2) {
throw new IllegalArgumentException("directTlq.upstreamCocCount must be in [1,2]");
}
}
if ("RABBITMQ_CFMQ".equals(mqType)) {
requireNotBlank("mq.cfgZipFileId", request.getMq().getCfgZipFileId());
requireFileId("mq.cfgZipFileId", request.getMq().getCfgZipFileId());
requireNotBlank("mq.cfmqConfigFileId", request.getMq().getCfmqConfigFileId());
requireFileId("mq.cfmqConfigFileId", request.getMq().getCfmqConfigFileId());
}
@@ -859,6 +917,7 @@ public class InitServiceImpl implements InitService {
return normalize(child.toString());
}
private static String normalizeUpper(String value) {
return normalize(value).toUpperCase(Locale.ROOT);
}
@@ -73,14 +73,16 @@ public class UpgradeController {
}
@GetMapping("/upgrades")
@Operation(summary = "查询升级记录列表", description = "分页查询升级任务列表,支持按升级对象和状态筛选。")
@Operation(summary = "查询升级记录列表", description = "分页查询升级任务列表,支持按升级对象、状态和升级日期筛选。")
public ApiResponse<UpgradeTaskPageResponse> list(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int pageSize,
@RequestParam(required = false) String taskType,
@RequestParam(required = false) String status
@RequestParam(required = false) String status,
@Parameter(description = "升级日期,格式 yyyy-MM-dd,按 startedAt 所在自然日筛选")
@RequestParam(required = false) String upgradeDate
) {
return ApiResponse.success(upgradeService.listTasks(page, pageSize, taskType, status));
return ApiResponse.success(upgradeService.listTasks(page, pageSize, taskType, status, upgradeDate));
}
@GetMapping("/upgrades/{taskId}")
@@ -7,6 +7,8 @@ public class UpgradeCreateTaskRequest {
@Schema(description = "升级包文件标识", example = "FILE-20260319-000001", requiredMode = Schema.RequiredMode.REQUIRED)
private String fileId;
@Schema(description = "用户选择的升级任务类型,必须与升级包解析结果一致", example = "TMS", requiredMode = Schema.RequiredMode.REQUIRED)
private String taskType;
public String getFileId() {
return fileId;
@@ -15,4 +17,12 @@ public class UpgradeCreateTaskRequest {
public void setFileId(String fileId) {
this.fileId = fileId;
}
public String getTaskType() {
return taskType;
}
public void setTaskType(String taskType) {
this.taskType = taskType;
}
}
@@ -17,13 +17,17 @@ public interface UpgradeTaskMapper extends BaseMapperX<UpgradeTaskEntity> {
List<UpgradeTaskEntity> selectPage(
@Param("taskType") String taskType,
@Param("status") String status,
@Param("startedAtBegin") LocalDateTime startedAtBegin,
@Param("startedAtEndExclusive") LocalDateTime startedAtEndExclusive,
@Param("offset") int offset,
@Param("limit") int limit
);
long countPage(
@Param("taskType") String taskType,
@Param("status") String status
@Param("status") String status,
@Param("startedAtBegin") LocalDateTime startedAtBegin,
@Param("startedAtEndExclusive") LocalDateTime startedAtEndExclusive
);
int updateStatusByTaskId(
@@ -13,9 +13,16 @@ public interface UpgradeTaskRepository {
Optional<UpgradeTaskEntity> findRunningTask();
List<UpgradeTaskEntity> findPage(String taskType, String status, int offset, int limit);
List<UpgradeTaskEntity> findPage(
String taskType,
String status,
LocalDateTime startedAtBegin,
LocalDateTime startedAtEndExclusive,
int offset,
int limit
);
long countPage(String taskType, String status);
long countPage(String taskType, String status, LocalDateTime startedAtBegin, LocalDateTime startedAtEndExclusive);
void updateStatus(
String taskId,
@@ -33,13 +33,20 @@ public class UpgradeTaskRepositoryImpl implements UpgradeTaskRepository {
}
@Override
public List<UpgradeTaskEntity> findPage(String taskType, String status, int offset, int limit) {
return upgradeTaskMapper.selectPage(taskType, status, offset, limit);
public List<UpgradeTaskEntity> findPage(
String taskType,
String status,
LocalDateTime startedAtBegin,
LocalDateTime startedAtEndExclusive,
int offset,
int limit
) {
return upgradeTaskMapper.selectPage(taskType, status, startedAtBegin, startedAtEndExclusive, offset, limit);
}
@Override
public long countPage(String taskType, String status) {
return upgradeTaskMapper.countPage(taskType, status);
public long countPage(String taskType, String status, LocalDateTime startedAtBegin, LocalDateTime startedAtEndExclusive) {
return upgradeTaskMapper.countPage(taskType, status, startedAtBegin, startedAtEndExclusive);
}
@Override
@@ -19,8 +19,10 @@ import com.cisd.tms.modules.upgrade.repository.UpgradeTaskRepository;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -36,6 +38,7 @@ import org.springframework.stereotype.Service;
public class UpgradeService {
private static final DateTimeFormatter TASK_ID_TS = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private static final DateTimeFormatter UPGRADE_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private final UpgradeTaskRepository upgradeTaskRepository;
private final FileRecordRepository fileRecordRepository;
@@ -97,11 +100,17 @@ public class UpgradeService {
if (request == null || request.getFileId() == null || request.getFileId().isBlank()) {
throw new IllegalArgumentException("fileId must not be blank");
}
if (request.getTaskType() == null || request.getTaskType().isBlank()) {
throw new IllegalArgumentException("taskType must not be blank");
}
// 创建任务前重新预检一次,避免用户上传后替换文件或设备版本状态发生变化。
UpgradePreviewResponse preview = upgradePackageService.preview(request.getFileId());
if (!preview.isAllowed()) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), blankAs(preview.getBlockReason(), "upgrade preview not allowed"));
}
if (!trim(request.getTaskType()).equals(trim(preview.getTaskType()))) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "selected taskType does not match package taskType");
}
FileRecordEntity fileRecord = fileRecordRepository.findByFileId(request.getFileId().trim())
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + request.getFileId().trim()));
@@ -186,12 +195,27 @@ public class UpgradeService {
return toResponse(loadTask(taskId));
}
public UpgradeTaskPageResponse listTasks(int page, int pageSize, String taskType, String status) {
public UpgradeTaskPageResponse listTasks(int page, int pageSize, String taskType, String status, String upgradeDate) {
int normalizedPage = Math.max(1, page);
int normalizedPageSize = Math.max(1, pageSize);
int offset = (normalizedPage - 1) * normalizedPageSize;
List<UpgradeTaskEntity> entities = upgradeTaskRepository.findPage(trim(taskType), trim(status), offset, normalizedPageSize);
long total = upgradeTaskRepository.countPage(trim(taskType), trim(status));
LocalDateTime startedAtBegin = null;
LocalDateTime startedAtEndExclusive = null;
String normalizedUpgradeDate = trim(upgradeDate);
if (!normalizedUpgradeDate.isEmpty()) {
LocalDate filterDate = parseUpgradeDate(normalizedUpgradeDate);
startedAtBegin = filterDate.atStartOfDay();
startedAtEndExclusive = filterDate.plusDays(1).atStartOfDay();
}
List<UpgradeTaskEntity> entities = upgradeTaskRepository.findPage(
trim(taskType),
trim(status),
startedAtBegin,
startedAtEndExclusive,
offset,
normalizedPageSize
);
long total = upgradeTaskRepository.countPage(trim(taskType), trim(status), startedAtBegin, startedAtEndExclusive);
UpgradeTaskPageResponse response = new UpgradeTaskPageResponse();
response.setPage(normalizedPage);
@@ -267,4 +291,12 @@ public class UpgradeService {
private String blankAs(String value, String fallback) {
return trim(value).isEmpty() ? fallback : trim(value);
}
private LocalDate parseUpgradeDate(String upgradeDate) {
try {
return LocalDate.parse(upgradeDate, UPGRADE_DATE_FORMAT);
} catch (DateTimeParseException ex) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "upgradeDate must be yyyy-MM-dd");
}
}
}
@@ -59,6 +59,12 @@
<if test="status != null and status != ''">
AND status = #{status}
</if>
<if test="startedAtBegin != null">
AND started_at <![CDATA[ >= ]]> #{startedAtBegin}
</if>
<if test="startedAtEndExclusive != null">
AND started_at <![CDATA[ < ]]> #{startedAtEndExclusive}
</if>
</where>
ORDER BY create_time DESC, id DESC
LIMIT #{limit} OFFSET #{offset}
@@ -74,6 +80,12 @@
<if test="status != null and status != ''">
AND status = #{status}
</if>
<if test="startedAtBegin != null">
AND started_at <![CDATA[ >= ]]> #{startedAtBegin}
</if>
<if test="startedAtEndExclusive != null">
AND started_at <![CDATA[ < ]]> #{startedAtEndExclusive}
</if>
</where>
</select>