feat:CISD配置初始化

This commit is contained in:
waner
2026-03-11 14:23:11 +08:00
parent 1c254aaba9
commit d02c6530e8
58 changed files with 10699 additions and 1281 deletions
@@ -0,0 +1,26 @@
package com.cisd.tms.common.config.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "tms.file.storage")
public class FileStorageProperties {
private String uploadBaseDir = "/home/tms/uploads";
private long maxFileSizeBytes = 104857600L;
public String getUploadBaseDir() {
return uploadBaseDir;
}
public void setUploadBaseDir(String uploadBaseDir) {
this.uploadBaseDir = uploadBaseDir;
}
public long getMaxFileSizeBytes() {
return maxFileSizeBytes;
}
public void setMaxFileSizeBytes(long maxFileSizeBytes) {
this.maxFileSizeBytes = maxFileSizeBytes;
}
}
@@ -20,6 +20,7 @@ public class PcieErrorMapper {
lowByteErrorMap.put(0x25, "init status");
lowByteErrorMap.put(0x26, "already logined");
lowByteErrorMap.put(0x27, "timeout");
lowByteErrorMap.put(0x1D, "invalid argument");
}
public String toMessage(int retCode) {
@@ -3,6 +3,7 @@ package com.cisd.tms.integration.crypto.pcie;
import com.cisd.tms.integration.crypto.config.CryptoCardProperties;
import com.cisd.tms.integration.crypto.pcie.jna.PcieNativeLibrary;
import com.sun.jna.Library;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.PointerByReference;
@@ -61,6 +62,8 @@ public class PcieSessionTemplate {
public <T> T withSession(String apiName, SessionCallback<T> callback) {
return withDevice(apiName, (lib, deviceHandle) -> {
PointerByReference sessionRef = new PointerByReference();
Pointer ph = new Memory(8L);
sessionRef.setValue(ph);
int openSessionRc = lib.SDF_OpenSession(deviceHandle, sessionRef);
ensureSuccess(apiName + ":SDF_OpenSession", openSessionRc);
@@ -90,6 +90,10 @@ public class JnaPcieCryptoService implements PcieCryptoService {
private static final int ECC_KEY_BITS = 256;
private static final int LMK_SEED_MAC_LENGTH = 8;
private static final int SDFE_INIT_STATUS_LOW_BYTE = 0x25;
private static final int IK_KEY_TYPE_DEVICE = 1;
private static final int IK_KEY_TYPE_AUTH = 2;
private static final int ADMIN_INDEX_MIN = 1;
private static final int ADMIN_INDEX_MAX = 3;
private static final int HMAC_MAX_LENGTH = 64;
private static final int ECC_CIPHER_PREFIX_LENGTH = (EccRefPublicKey.MAX_LEN * 2) + 32 + Integer.BYTES;
@@ -15,11 +15,11 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/auth")
public class AuthInternalController {
public class AuthController {
private final AuthService authService;
public AuthInternalController(AuthService authService) {
public AuthController(AuthService authService) {
this.authService = authService;
}
@@ -42,11 +42,11 @@ import org.springframework.web.bind.annotation.RestController;
@Validated
@RestController
@RequestMapping("/api/v1/device/crypto")
public class CryptoCardInternalController {
public class CryptoCardController {
private final PcieCryptoService pcieCryptoService;
public CryptoCardInternalController(PcieCryptoService pcieCryptoService) {
public CryptoCardController(PcieCryptoService pcieCryptoService) {
this.pcieCryptoService = pcieCryptoService;
}
@@ -12,11 +12,11 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/device")
public class DeviceInternalController {
public class DeviceController {
private final DeviceService deviceService;
public DeviceInternalController(DeviceService deviceService) {
public DeviceController(DeviceService deviceService) {
this.deviceService = deviceService;
}
@@ -0,0 +1,36 @@
package com.cisd.tms.modules.file.controller;
import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.modules.file.dto.FileDetailResponse;
import com.cisd.tms.modules.file.dto.FileUploadResponse;
import com.cisd.tms.modules.file.service.FileService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api/v1/files")
public class FileController {
private final FileService fileService;
public FileController(FileService fileService) {
this.fileService = fileService;
}
// 内部上传入口,前端先拿到 fileId,再把 fileId 传给初始化相关接口。
@PostMapping("/upload")
public ApiResponse<FileUploadResponse> upload(@RequestParam("file") MultipartFile file) {
return ApiResponse.success(fileService.upload(file));
}
// 返回已存储的文件元数据,便于调用方确认 fileId 到实际文件的映射关系。
@GetMapping("/{fileId}")
public ApiResponse<FileDetailResponse> detail(@PathVariable String fileId) {
return ApiResponse.success(fileService.getDetail(fileId));
}
}
@@ -0,0 +1,50 @@
package com.cisd.tms.modules.file.dto;
public class FileDetailResponse {
private String fileId;
private String originalFilename;
private String storagePath;
private String status;
private long size;
public String getFileId() {
return fileId;
}
public void setFileId(String fileId) {
this.fileId = fileId;
}
public String getOriginalFilename() {
return originalFilename;
}
public void setOriginalFilename(String originalFilename) {
this.originalFilename = originalFilename;
}
public String getStoragePath() {
return storagePath;
}
public void setStoragePath(String storagePath) {
this.storagePath = storagePath;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public long getSize() {
return size;
}
public void setSize(long size) {
this.size = size;
}
}
@@ -0,0 +1,32 @@
package com.cisd.tms.modules.file.dto;
public class FileUploadResponse {
private String fileId;
private long size;
private String storagePath;
public String getFileId() {
return fileId;
}
public void setFileId(String fileId) {
this.fileId = fileId;
}
public long getSize() {
return size;
}
public void setSize(long size) {
this.size = size;
}
public String getStoragePath() {
return storagePath;
}
public void setStoragePath(String storagePath) {
this.storagePath = storagePath;
}
}
@@ -0,0 +1,45 @@
package com.cisd.tms.modules.file.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
@TableName("tms_file_record")
public class FileRecordEntity extends BaseEntity {
private String fileId;
private String originalFilename;
private String storagePath;
private String status;
public String getFileId() {
return fileId;
}
public void setFileId(String fileId) {
this.fileId = fileId;
}
public String getOriginalFilename() {
return originalFilename;
}
public void setOriginalFilename(String originalFilename) {
this.originalFilename = originalFilename;
}
public String getStoragePath() {
return storagePath;
}
public void setStoragePath(String storagePath) {
this.storagePath = storagePath;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}
@@ -0,0 +1,12 @@
package com.cisd.tms.modules.file.mapper;
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
import com.cisd.tms.modules.file.entity.FileRecordEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface FileRecordMapper extends BaseMapperX<FileRecordEntity> {
FileRecordEntity selectByFileId(@Param("fileId") String fileId);
}
@@ -0,0 +1,11 @@
package com.cisd.tms.modules.file.repository;
import com.cisd.tms.modules.file.entity.FileRecordEntity;
import java.util.Optional;
public interface FileRecordRepository {
Optional<FileRecordEntity> findByFileId(String fileId);
void save(FileRecordEntity entity);
}
@@ -0,0 +1,27 @@
package com.cisd.tms.modules.file.repository.impl;
import com.cisd.tms.modules.file.entity.FileRecordEntity;
import com.cisd.tms.modules.file.mapper.FileRecordMapper;
import com.cisd.tms.modules.file.repository.FileRecordRepository;
import java.util.Optional;
import org.springframework.stereotype.Repository;
@Repository
public class FileRecordRepositoryImpl implements FileRecordRepository {
private final FileRecordMapper fileRecordMapper;
public FileRecordRepositoryImpl(FileRecordMapper fileRecordMapper) {
this.fileRecordMapper = fileRecordMapper;
}
@Override
public Optional<FileRecordEntity> findByFileId(String fileId) {
return Optional.ofNullable(fileRecordMapper.selectByFileId(fileId));
}
@Override
public void save(FileRecordEntity entity) {
fileRecordMapper.insert(entity);
}
}
@@ -0,0 +1,118 @@
package com.cisd.tms.modules.file.service;
import com.cisd.tms.common.config.properties.FileStorageProperties;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.file.dto.FileDetailResponse;
import com.cisd.tms.modules.file.dto.FileUploadResponse;
import com.cisd.tms.modules.file.entity.FileRecordEntity;
import com.cisd.tms.modules.file.repository.FileRecordRepository;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.UUID;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@Service
public class FileService {
private static final DateTimeFormatter FILE_ID_TS = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private final FileRecordRepository fileRecordRepository;
private final FileStorageProperties fileStorageProperties;
public FileService(FileRecordRepository fileRecordRepository, FileStorageProperties fileStorageProperties) {
this.fileRecordRepository = fileRecordRepository;
this.fileStorageProperties = fileStorageProperties;
}
public FileUploadResponse upload(MultipartFile file) {
if (file == null || file.isEmpty() || file.getSize() <= 0) {
throw new IllegalArgumentException("upload file is empty");
}
if (file.getSize() > fileStorageProperties.getMaxFileSizeBytes()) {
throw new IllegalArgumentException("upload file size exceeds limit");
}
String originalFilename = sanitizeFilename(file.getOriginalFilename());
String fileId = generateFileId();
Path uploadBaseDir = Paths.get(normalize(fileStorageProperties.getUploadBaseDir())).normalize().toAbsolutePath();
// 固定为 <upload-base-dir>/<fileId>/<originalFilename>,这样初始化流程只靠 fileId 就能稳定反查文件。
Path target = uploadBaseDir.resolve(fileId).resolve(originalFilename).normalize().toAbsolutePath();
try {
Files.createDirectories(target.getParent());
file.transferTo(target);
} catch (IOException ex) {
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to save upload file");
}
// 直接落库存储绝对路径,避免后续再依赖可能变化的配置重新推导文件位置。
FileRecordEntity entity = new FileRecordEntity();
entity.setFileId(fileId);
entity.setOriginalFilename(originalFilename);
entity.setStoragePath(target.toString());
entity.setStatus("ACTIVE");
fileRecordRepository.save(entity);
FileUploadResponse response = new FileUploadResponse();
response.setFileId(fileId);
response.setSize(file.getSize());
response.setStoragePath(target.toString());
return response;
}
public FileDetailResponse getDetail(String fileId) {
FileRecordEntity entity = fileRecordRepository.findByFileId(normalize(fileId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "file not found: " + normalize(fileId)));
Path path = Paths.get(entity.getStoragePath()).normalize().toAbsolutePath();
FileDetailResponse response = new FileDetailResponse();
response.setFileId(entity.getFileId());
response.setOriginalFilename(entity.getOriginalFilename());
response.setStoragePath(entity.getStoragePath());
response.setStatus(entity.getStatus());
try {
// 文件大小以磁盘上的真实文件为准,保证查询结果反映当前实际落盘状态。
response.setSize(Files.exists(path) ? Files.size(path) : 0L);
} catch (IOException ex) {
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to read file detail");
}
return response;
}
private String generateFileId() {
String ts = LocalDateTime.now().format(FILE_ID_TS);
String suffix = UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase(Locale.ROOT);
return "FILE-" + ts + "-" + suffix;
}
private String sanitizeFilename(String originalFilename) {
String normalized = normalize(originalFilename);
if (normalized.isEmpty()) {
throw new IllegalArgumentException("original filename is blank");
}
try {
// 先剥离客户端传入文件名里的路径段,避免把目录信息带入服务端存储路径。
String fileName = Paths.get(normalized).getFileName().toString();
String sanitized = fileName.replaceAll("[^A-Za-z0-9._-]", "_");
if (sanitized.isBlank()) {
throw new IllegalArgumentException("original filename is invalid");
}
return sanitized;
} catch (InvalidPathException ex) {
throw new IllegalArgumentException("original filename is invalid");
}
}
private String normalize(String value) {
return value == null ? "" : value.trim();
}
}
@@ -65,9 +65,6 @@ public class InitCommandProfileService {
private String interpolate(String rawCommand) {
String standardHomeDir = normalize(initExecutorProperties.getStandardHomeDir());
if (standardHomeDir.isEmpty()) {
standardHomeDir = "/home/cemp4i";
}
return rawCommand.replace(STANDARD_HOME_DIR_PLACEHOLDER, standardHomeDir);
}
@@ -11,14 +11,49 @@ public class InitExecutorProperties {
LOCAL
}
private static final String DEFAULT_STANDARD_HOME_DIR = "/home/cemp4i";
private static final String DEFAULT_STANDARD_RUN_USER = "cmep4i";
private Mode mode = Mode.SIMULATE;
private int timeoutSec = 600;
// 所有步骤日志会写入该目录,读取日志接口会做同目录约束校验
private String logDir = "/tmp/tms-init-logs";
// 标准版(企业/间参)安装根目录(统一入口)
private String standardHomeDir = "/home/cemp4i";
private String standardHomeDir = DEFAULT_STANDARD_HOME_DIR;
// 标准版(企业/间参)默认 cpconfig.cfg 路径
private String standardCpconfigPath = "/home/cemp4i/cpconfig.cfg";
private String standardCpconfigPath = "";
// 标准版数据库脚本基目录(应包含 cpackage/sql_script)
private String standardDbScriptBaseDir = "";
// 标准版数据库加载目录(渲染后的 SQL 会复制到此目录)
private String standardDbLoadDir = "";
// 标准版数据库导库用户名;执行 DB_APPLY 时会注入 DB_USER
private String standardDbUsername = "";
// 标准版数据库导库密码;执行 DB_APPLY 时会注入 DB_PASSWORD
private String standardDbPassword = "";
// 标准版数据库导入命令;LOCAL 模式下不能为空,否则 DB_APPLY 直接失败
private String standardDbApplyCommand = "";
// 标准版数据库入口 SQL 文件名
private String standardDbEntrySqlFileName = "CMEP.sql";
// 标准版 nginx 模板路径
private String standardNginxTemplatePath = "";
// 标准版 nginx 目标配置路径
private String standardNginxTargetPath = "/usr/local/nginx/conf/nginx.conf";
// 标准版 WEB 前端压缩包路径
private String standardWebFrontZipPath = "";
// 标准版 WEB 工作目录
private String standardWebWorkDir = "";
// 标准版 WEB 发布目录
private String standardWebTargetDir = "/usr/local/nginx/html";
// 标准版 organization.json 输出路径
private String standardWebOrganizationJsonPath = "/usr/local/nginx/html/organization.json";
// 标准版 CMSP/CMTP 启动用户
private String standardRunUser = DEFAULT_STANDARD_RUN_USER;
// 标准版 CMSP/CMTP 启动命令
private String standardAppStartCommand = "";
// 标准版 nginx 启动命令
private String standardNginxStartCommand = "";
// 标准版启动后检查命令
private String standardPostCheckCommand = "";
// 直参版 cfgZipFileId 对应目录下的 cpconfig 文件名
private String directCpconfigFileName = "cpconfig.cfg";
// 上传文件基础目录(fileId->文件路径解析时使用)
@@ -26,7 +61,7 @@ public class InitExecutorProperties {
// 初始化阶段文件暂存目录(除固定落位文件外,均先暂存再使用)
private String stagingRootDir = "/tmp/tms-init-staging";
// 标准版收发器 license 固定落位
private String standardReceiverLicenseTargetPath = "/home/cemp4i/cmep/license/cmep.license";
private String standardReceiverLicenseTargetPath = "";
// 直参版收发器 license 固定落位
private String directReceiverLicenseTargetPath = "/home/cmep4d/cmep/license/cmep.license";
// 标准版 TLQ license(tlqconf.zip)固定落位
@@ -48,25 +83,21 @@ public class InitExecutorProperties {
private String rabbitmqCtlCommand = "rabbitmqctl";
private String rabbitmqAdminCommand = "/usr/sbin/rabbitmqadmin";
// 标准版 RabbitMQ 初始化脚本(创建admin/通道账号、替换队列脚本机构号)
private String standardRabbitSetupScriptPath = "/home/cemp4i/cpackage/rabq/setuprabq.sh";
private String standardRabbitSetupScriptPath = "";
// 标准版 RabbitMQ 队列初始化脚本(01节点)
private String standardRabbitQueueScriptPath = "/home/cemp4i/cpackage/rabq/addrabq_R_01.sh";
private String standardRabbitQueueScriptPath = "";
// 标准版 RabbitMQ 队列初始化脚本(02节点)
private String standardRabbitQueueScriptPath02 = "/home/cemp4i/cpackage/rabq/addrabq_R_02.sh";
private String standardRabbitQueueScriptPath02 = "";
// 队列和通道账号默认配置 vhost
private String rabbitVhost = "/RQ";
// 标准版应用 RabbitMQ 连接配置文件(支持文件名通配符)
private List<String> standardRabbitAppConfigPatterns = List.of(
"/home/cemp4i/cmsp/application-prd*.properties",
"/home/cemp4i/cmtp/application-prd*.properties"
);
private List<String> standardRabbitAppConfigPatterns = List.of();
// 标准版应用 RabbitMQ 连接账号/密码写回时是否加密
private boolean standardRabbitCredentialEncryptEnabled = true;
// 加密密钥,默认与官方 encrypt-decrypt.jar 保持一致
private String standardRabbitCredentialSecretKey = "ABCDEFGHIJKLMN1234567890";
// LOCAL 模式命令白名单前缀;仅命中前缀的命令允许执行
private List<String> whitelistPrefixes = List.of(
"bash /home/cemp4i/cpackage/",
"bash /opt/CAE_Install_",
"systemctl ",
"echo "
@@ -97,7 +128,7 @@ public class InitExecutorProperties {
}
public String getStandardHomeDir() {
return standardHomeDir;
return isBlank(standardHomeDir) ? DEFAULT_STANDARD_HOME_DIR : standardHomeDir.trim();
}
public void setStandardHomeDir(String standardHomeDir) {
@@ -105,7 +136,24 @@ public class InitExecutorProperties {
}
public List<String> getWhitelistPrefixes() {
return whitelistPrefixes;
if (whitelistPrefixes == null || whitelistPrefixes.isEmpty()) {
return List.of(
"bash " + getStandardHomeDir() + "/cpackage/",
"bash /opt/CAE_Install_",
"systemctl ",
"echo "
);
}
boolean hasStandardPrefix = whitelistPrefixes.stream()
.filter(item -> item != null && !item.isBlank())
.anyMatch(item -> item.startsWith("bash ") && item.contains("/cpackage/"));
if (hasStandardPrefix) {
return whitelistPrefixes;
}
java.util.ArrayList<String> merged = new java.util.ArrayList<>();
merged.add("bash " + getStandardHomeDir() + "/cpackage/");
merged.addAll(whitelistPrefixes);
return merged;
}
public void setWhitelistPrefixes(List<String> whitelistPrefixes) {
@@ -113,7 +161,7 @@ public class InitExecutorProperties {
}
public String getStandardCpconfigPath() {
return standardCpconfigPath;
return firstNonBlank(standardCpconfigPath, buildStandardHomePath("/cpconfig.cfg"));
}
public void setStandardCpconfigPath(String standardCpconfigPath) {
@@ -128,6 +176,134 @@ public class InitExecutorProperties {
this.directCpconfigFileName = directCpconfigFileName;
}
public String getStandardDbScriptBaseDir() {
return firstNonBlank(standardDbScriptBaseDir, buildStandardHomePath("/cpackage"));
}
public void setStandardDbScriptBaseDir(String standardDbScriptBaseDir) {
this.standardDbScriptBaseDir = standardDbScriptBaseDir;
}
public String getStandardDbLoadDir() {
return firstNonBlank(standardDbLoadDir, buildStandardHomePath("/mysql/loadfilepath"));
}
public void setStandardDbLoadDir(String standardDbLoadDir) {
this.standardDbLoadDir = standardDbLoadDir;
}
public String getStandardDbUsername() {
return standardDbUsername;
}
public void setStandardDbUsername(String standardDbUsername) {
this.standardDbUsername = standardDbUsername;
}
public String getStandardDbPassword() {
return standardDbPassword;
}
public void setStandardDbPassword(String standardDbPassword) {
this.standardDbPassword = standardDbPassword;
}
public String getStandardDbApplyCommand() {
return standardDbApplyCommand;
}
public void setStandardDbApplyCommand(String standardDbApplyCommand) {
this.standardDbApplyCommand = standardDbApplyCommand;
}
public String getStandardDbEntrySqlFileName() {
return standardDbEntrySqlFileName;
}
public void setStandardDbEntrySqlFileName(String standardDbEntrySqlFileName) {
this.standardDbEntrySqlFileName = standardDbEntrySqlFileName;
}
public String getStandardNginxTemplatePath() {
return firstNonBlank(standardNginxTemplatePath, buildStandardHomePath("/cpackage/software/nginx.conf"));
}
public void setStandardNginxTemplatePath(String standardNginxTemplatePath) {
this.standardNginxTemplatePath = standardNginxTemplatePath;
}
public String getStandardNginxTargetPath() {
return standardNginxTargetPath;
}
public void setStandardNginxTargetPath(String standardNginxTargetPath) {
this.standardNginxTargetPath = standardNginxTargetPath;
}
public String getStandardWebFrontZipPath() {
return firstNonBlank(standardWebFrontZipPath, buildStandardHomePath("/cpackage/front/front.zip"));
}
public void setStandardWebFrontZipPath(String standardWebFrontZipPath) {
this.standardWebFrontZipPath = standardWebFrontZipPath;
}
public String getStandardWebWorkDir() {
return firstNonBlank(standardWebWorkDir, buildStandardHomePath("/web"));
}
public void setStandardWebWorkDir(String standardWebWorkDir) {
this.standardWebWorkDir = standardWebWorkDir;
}
public String getStandardWebTargetDir() {
return standardWebTargetDir;
}
public void setStandardWebTargetDir(String standardWebTargetDir) {
this.standardWebTargetDir = standardWebTargetDir;
}
public String getStandardWebOrganizationJsonPath() {
return standardWebOrganizationJsonPath;
}
public void setStandardWebOrganizationJsonPath(String standardWebOrganizationJsonPath) {
this.standardWebOrganizationJsonPath = standardWebOrganizationJsonPath;
}
public String getStandardRunUser() {
return isBlank(standardRunUser) ? DEFAULT_STANDARD_RUN_USER : standardRunUser.trim();
}
public void setStandardRunUser(String standardRunUser) {
this.standardRunUser = standardRunUser;
}
public String getStandardAppStartCommand() {
return standardAppStartCommand;
}
public void setStandardAppStartCommand(String standardAppStartCommand) {
this.standardAppStartCommand = standardAppStartCommand;
}
public String getStandardNginxStartCommand() {
return standardNginxStartCommand;
}
public void setStandardNginxStartCommand(String standardNginxStartCommand) {
this.standardNginxStartCommand = standardNginxStartCommand;
}
public String getStandardPostCheckCommand() {
return standardPostCheckCommand;
}
public void setStandardPostCheckCommand(String standardPostCheckCommand) {
this.standardPostCheckCommand = standardPostCheckCommand;
}
public String getUploadBaseDir() {
return uploadBaseDir;
}
@@ -145,7 +321,7 @@ public class InitExecutorProperties {
}
public String getStandardReceiverLicenseTargetPath() {
return standardReceiverLicenseTargetPath;
return firstNonBlank(standardReceiverLicenseTargetPath, buildStandardHomePath("/cmep/license/cmep.license"));
}
public void setStandardReceiverLicenseTargetPath(String standardReceiverLicenseTargetPath) {
@@ -241,7 +417,7 @@ public class InitExecutorProperties {
}
public String getStandardRabbitSetupScriptPath() {
return standardRabbitSetupScriptPath;
return firstNonBlank(standardRabbitSetupScriptPath, buildStandardHomePath("/cpackage/rabq/setuprabq.sh"));
}
public void setStandardRabbitSetupScriptPath(String standardRabbitSetupScriptPath) {
@@ -249,7 +425,7 @@ public class InitExecutorProperties {
}
public String getStandardRabbitQueueScriptPath() {
return standardRabbitQueueScriptPath;
return firstNonBlank(standardRabbitQueueScriptPath, buildStandardHomePath("/cpackage/rabq/addrabq_R_01.sh"));
}
public void setStandardRabbitQueueScriptPath(String standardRabbitQueueScriptPath) {
@@ -257,7 +433,7 @@ public class InitExecutorProperties {
}
public String getStandardRabbitQueueScriptPath02() {
return standardRabbitQueueScriptPath02;
return firstNonBlank(standardRabbitQueueScriptPath02, buildStandardHomePath("/cpackage/rabq/addrabq_R_02.sh"));
}
public void setStandardRabbitQueueScriptPath02(String standardRabbitQueueScriptPath02) {
@@ -273,6 +449,13 @@ public class InitExecutorProperties {
}
public List<String> getStandardRabbitAppConfigPatterns() {
if (standardRabbitAppConfigPatterns == null || standardRabbitAppConfigPatterns.isEmpty()) {
String home = getStandardHomeDir();
return List.of(
home + "/cmsp/application-prd*.properties",
home + "/cmtp/application-prd*.properties"
);
}
return standardRabbitAppConfigPatterns;
}
@@ -295,4 +478,16 @@ public class InitExecutorProperties {
public void setStandardRabbitCredentialSecretKey(String standardRabbitCredentialSecretKey) {
this.standardRabbitCredentialSecretKey = standardRabbitCredentialSecretKey;
}
private String buildStandardHomePath(String suffix) {
return getStandardHomeDir() + suffix;
}
private static String firstNonBlank(String preferred, String fallback) {
return isBlank(preferred) ? fallback : preferred.trim();
}
private static boolean isBlank(String value) {
return value == null || value.isBlank();
}
}
@@ -22,11 +22,11 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/init")
public class InitInternalController {
public class InitController {
private final InitService initService;
public InitInternalController(InitService initService) {
public InitController(InitService initService) {
this.initService = initService;
}
@@ -74,7 +74,8 @@ public class InitInternalController {
}
/**
* 执行初始化任务:按步骤串行执行,失败即中断。
* 受理初始化任务执行请求。
* 接口只负责启动后台执行并立即返回,前端需继续轮询任务详情、步骤状态和步骤日志。
*/
@PostMapping("/tasks/{taskId}/execute")
public ApiResponse<InitTaskExecuteResponse> executeTask(@PathVariable String taskId) {
@@ -26,6 +26,9 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.stereotype.Component;
@@ -111,6 +114,13 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
case "mq_user_apply" -> executeMqUserApply(task, step);
case "mq_queue_apply" -> executeMqQueueApply(task, step);
case "app_mq_credentials_apply" -> executeAppMqCredentialsApply(task, step);
case "db_render" -> executeStandardDbRender(task, step);
case "db_apply" -> executeStandardDbApply(task, step);
case "nginx_config_apply" -> executeStandardNginxConfigApply(task, step);
case "web_config_apply" -> executeStandardWebConfigApply(task, step);
case "start_cmsp_cmtp" -> executeStandardStartCommand(task, step, "start_cmsp_cmtp", properties.getStandardAppStartCommand());
case "start_nginx" -> executeStandardStartCommand(task, step, "start_nginx", properties.getStandardNginxStartCommand());
case "post_check" -> executeStandardStartCommand(task, step, "post_check", properties.getStandardPostCheckCommand());
case "direct_db_init" -> executeDirectDbInit(task, step);
case "tlq_init_cfg" -> executeTlqInitCfg(task, step);
case "cfmq_apply" -> executeCfmqApply(task, step);
@@ -195,8 +205,8 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
String logPath = writeInternalLog(task, step, "mq_user_apply skipped for non-standard product type");
return InitStepExecutionResult.success("mq_user_apply skipped", 0, logPath);
}
if (!"RABBITMQ".equals(context.mqType())) {
String logPath = writeInternalLog(task, step, "mq_user_apply skipped for non-rabbitmq mode");
if (!shouldApplyStandardRabbitmq(context)) {
String logPath = writeInternalLog(task, step, "mq_user_apply skipped for current standard mode");
return InitStepExecutionResult.success("mq_user_apply skipped", 0, logPath);
}
@@ -237,8 +247,8 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
String logPath = writeInternalLog(task, step, "mq_queue_apply skipped for non-standard product type");
return InitStepExecutionResult.success("mq_queue_apply skipped", 0, logPath);
}
if (!"RABBITMQ".equals(context.mqType())) {
String logPath = writeInternalLog(task, step, "mq_queue_apply skipped for non-rabbitmq mode");
if (!shouldApplyStandardRabbitmq(context)) {
String logPath = writeInternalLog(task, step, "mq_queue_apply skipped for current standard mode");
return InitStepExecutionResult.success("mq_queue_apply skipped", 0, logPath);
}
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
@@ -343,7 +353,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
InitTaskEntity task,
PlanContext context
) throws IOException {
String standardHome = firstNonBlank(properties.getStandardHomeDir(), "/home/cemp4i");
String standardHome = properties.getStandardHomeDir();
Path sourceSetupScript = resolveRequiredScriptPath(
properties.getStandardRabbitSetupScriptPath(),
standardHome + "/cpackage/rabq/setuprabq.sh",
@@ -381,7 +391,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
InitTaskEntity task,
PlanContext context
) throws IOException {
String standardHome = firstNonBlank(properties.getStandardHomeDir(), "/home/cemp4i");
String standardHome = properties.getStandardHomeDir();
Path sourceQueueScript = resolveStandardRabbitQueueSourceScript(context, standardHome);
Path stageRoot = Paths.get(firstNonBlank(properties.getStagingRootDir(), "/tmp/tms-init-staging"))
@@ -416,7 +426,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
String permissionsLine = "rabbitmqctl set_permissions -p /RQ " + userQuoted + " '.*' '.*' '.*'";
String profileLine = "source /etc/profile >/dev/null 2>&1 || true";
String pathLine = "export PATH=$PATH:/usr/local/rabbitmq/sbin:/usr/local/rabbitmq-server/sbin:/usr/lib/rabbitmq/bin";
String sourceLine = "source " + firstNonBlank(properties.getStandardCpconfigPath(), "/home/cemp4i/cpconfig.cfg");
String sourceLine = "source " + properties.getStandardCpconfigPath();
String cdLine = "cd " + stagingScriptDir;
for (String line : lines) {
@@ -527,6 +537,16 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
return addresses;
}
private boolean shouldApplyStandardRabbitmq(PlanContext context) {
if ("ENTERPRISE".equals(context.productType())) {
return "RABBITMQ".equals(context.mqType());
}
if ("INDIRECT".equals(context.productType())) {
return "RABBITMQ".equals(context.mqType()) || "RABBITMQ_TLQ".equals(context.mqType());
}
return false;
}
private boolean waitRabbitmqReady(Path logFile, String ctl) throws IOException, InterruptedException {
for (int i = 1; i <= 3; i++) {
int code = runCommand(logFile, Arrays.asList(ctl, "status"), true);
@@ -701,6 +721,10 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
String logPath = writeInternalLog(task, step, "app_mq_credentials_apply skipped for non-standard product type");
return InitStepExecutionResult.success("app_mq_credentials_apply skipped", 0, logPath);
}
if (!shouldApplyStandardRabbitmq(context)) {
String logPath = writeInternalLog(task, step, "app_mq_credentials_apply skipped for current standard mode");
return InitStepExecutionResult.success("app_mq_credentials_apply skipped", 0, logPath);
}
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "app_mq_credentials_apply simulated");
return InitStepExecutionResult.success("app_mq_credentials_apply simulated", 0, logPath);
@@ -797,6 +821,237 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
}
}
private InitStepExecutionResult executeStandardDbRender(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if ("DIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, "db_render skipped for direct product type");
return InitStepExecutionResult.success("db_render skipped", 0, logPath);
}
Path scriptBaseDir = Paths.get(firstNonBlank(
properties.getStandardDbScriptBaseDir(),
properties.getStandardHomeDir() + "/cpackage"
)).normalize().toAbsolutePath();
Path sqlScriptDir = scriptBaseDir.resolve("sql_script");
Path sqlTmpDir = sqlScriptDir.resolve("sql_script_tmp");
Path cmtpTemplate = sqlTmpDir.resolve(resolveStandardDbCmtpTemplateName(context));
Path cmepDmlTemplate = sqlScriptDir.resolve("CMEP-DML.sql");
Path cmepEntryTemplate = sqlScriptDir.resolve(firstNonBlank(properties.getStandardDbEntrySqlFileName(), "CMEP.sql"));
Path schemaTemplate = sqlScriptDir.resolve("SCHEMA-DDL.sql");
Path upOrgTemplate = sqlScriptDir.resolve("UP-ORG-INFO.sql");
List<Path> requiredFiles = List.of(cmtpTemplate, cmepDmlTemplate, cmepEntryTemplate, schemaTemplate, upOrgTemplate);
for (Path requiredFile : requiredFiles) {
if (!Files.exists(requiredFile) || !Files.isRegularFile(requiredFile)) {
String logPath = writeInternalLog(task, step, "db_render failed: required sql template not found: " + requiredFile);
return InitStepExecutionResult.failure("db_render failed: sql template not found", -1, logPath);
}
}
Path stageSqlDir = resolveStandardDbStageSqlDir(task);
Files.createDirectories(stageSqlDir);
writeRenderedSql(stageSqlDir.resolve("CMTP-01.sql"), Files.readString(cmtpTemplate), buildStandardSqlReplacements(context));
writeRenderedSql(stageSqlDir.resolve("CMEP-DML.sql"), Files.readString(cmepDmlTemplate), buildStandardSqlReplacements(context));
Files.copy(cmepEntryTemplate, stageSqlDir.resolve("CMEP.sql"), StandardCopyOption.REPLACE_EXISTING);
Files.copy(schemaTemplate, stageSqlDir.resolve("SCHEMA-DDL.sql"), StandardCopyOption.REPLACE_EXISTING);
Files.copy(upOrgTemplate, stageSqlDir.resolve("UP-ORG-INFO.sql"), StandardCopyOption.REPLACE_EXISTING);
String message = "db sql rendered, dir=" + stageSqlDir;
String logPath = writeInternalLog(task, step, message);
return InitStepExecutionResult.success(message, 0, logPath);
}
private InitStepExecutionResult executeStandardDbApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if ("DIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, "db_apply skipped for direct product type");
return InitStepExecutionResult.success("db_apply skipped", 0, logPath);
}
Path stageSqlDir = resolveStandardDbStageSqlDir(task);
if (!Files.isDirectory(stageSqlDir)) {
String logPath = writeInternalLog(task, step, "db_apply failed: staged sql dir not found: " + stageSqlDir);
return InitStepExecutionResult.failure("db_apply failed: staged sql dir not found", -1, logPath);
}
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "db_apply simulated, staged dir=" + stageSqlDir);
return InitStepExecutionResult.success("db_apply simulated", 0, logPath);
}
Path loadDir = Paths.get(firstNonBlank(
properties.getStandardDbLoadDir(),
properties.getStandardHomeDir() + "/mysql/loadfilepath"
)).normalize().toAbsolutePath();
Files.createDirectories(loadDir);
try (DirectoryStream<Path> stream = Files.newDirectoryStream(stageSqlDir)) {
for (Path item : stream) {
if (Files.isRegularFile(item)) {
Files.copy(item, loadDir.resolve(item.getFileName()), StandardCopyOption.REPLACE_EXISTING);
}
}
}
Path logFile = prepareLogPath(task, step);
appendLog(logFile, "staged sql copied: " + stageSqlDir + " -> " + loadDir);
String applyCommand = normalize(properties.getStandardDbApplyCommand());
if (isBlank(applyCommand)) {
appendLog(logFile, "db_apply failed: standardDbApplyCommand is blank");
return InitStepExecutionResult.failure("db_apply failed: apply command is blank", -1, logFile.toString());
}
try {
int code = runCommand(
logFile,
Arrays.asList("/bin/zsh", "-lc", applyCommand),
false,
loadDir,
buildStandardDbApplyEnvironment(loadDir)
);
if (code != 0) {
return InitStepExecutionResult.failure("db_apply failed", code, logFile.toString());
}
return InitStepExecutionResult.success("db sql applied to load dir=" + loadDir + ", command executed", 0, logFile.toString());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return InitStepExecutionResult.failure("db_apply interrupted", -1, logFile.toString());
}
}
private InitStepExecutionResult executeStandardNginxConfigApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if ("DIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, "nginx_config_apply skipped for direct product type");
return InitStepExecutionResult.success("nginx_config_apply skipped", 0, logPath);
}
Path templatePath = Paths.get(firstNonBlank(
properties.getStandardNginxTemplatePath(),
properties.getStandardHomeDir() + "/cpackage/software/nginx.conf"
)).normalize().toAbsolutePath();
if (!Files.exists(templatePath) || !Files.isRegularFile(templatePath)) {
String logPath = writeInternalLog(task, step, "nginx_config_apply failed: template not found: " + templatePath);
return InitStepExecutionResult.failure("nginx_config_apply failed: template not found", -1, logPath);
}
Path stageFile = resolveStandardNginxStageFile(task);
Files.createDirectories(stageFile.getParent());
Map<String, String> replacements = buildStandardReplacements(context);
String rendered = Files.readString(templatePath)
.replace("sptp.01app.ip", normalize(replacements.get("sptp_01app_ip")))
.replace("sptp.02app.ip", normalize(replacements.get("sptp_02app_ip")));
Files.writeString(stageFile, rendered, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
Path logFile = prepareLogPath(task, step);
appendLog(logFile, "nginx config staged: " + templatePath + " -> " + stageFile);
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
return InitStepExecutionResult.success("nginx config staged", 0, logFile.toString());
}
Path targetPath = Paths.get(firstNonBlank(properties.getStandardNginxTargetPath(), "/usr/local/nginx/conf/nginx.conf"))
.normalize()
.toAbsolutePath();
copyToTarget(stageFile, targetPath);
appendLog(logFile, "nginx config applied: " + stageFile + " -> " + targetPath);
return InitStepExecutionResult.success("nginx config applied", 0, logFile.toString());
}
private InitStepExecutionResult executeStandardWebConfigApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if ("DIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, "web_config_apply skipped for direct product type");
return InitStepExecutionResult.success("web_config_apply skipped", 0, logPath);
}
Path frontZip = Paths.get(firstNonBlank(
properties.getStandardWebFrontZipPath(),
properties.getStandardHomeDir() + "/cpackage/front/front.zip"
)).normalize().toAbsolutePath();
if (!Files.exists(frontZip) || !Files.isRegularFile(frontZip)) {
String logPath = writeInternalLog(task, step, "web_config_apply failed: front zip not found: " + frontZip);
return InitStepExecutionResult.failure("web_config_apply failed: front zip not found", -1, logPath);
}
Path stagedWebDir = resolveStandardWebStageDir(task);
recreateDirectory(stagedWebDir);
unzipToDirectory(frontZip, stagedWebDir);
Path stagedOrganizationJson = stagedWebDir.resolve("organization.json");
String organizationName = normalize(context.orgNameCn());
Files.writeString(
stagedOrganizationJson,
"{\"organizationName\":\"" + escapeJson(organizationName) + "\"}",
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING
);
Path logFile = prepareLogPath(task, step);
appendLog(logFile, "web assets staged: " + frontZip + " -> " + stagedWebDir);
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
return InitStepExecutionResult.success("web assets staged", 0, logFile.toString());
}
Path targetDir = Paths.get(firstNonBlank(properties.getStandardWebTargetDir(), "/usr/local/nginx/html"))
.normalize()
.toAbsolutePath();
recreateDirectory(targetDir);
copyDirectoryContents(stagedWebDir, targetDir, "organization.json");
Path organizationJsonTarget = Paths.get(firstNonBlank(
properties.getStandardWebOrganizationJsonPath(),
"/usr/local/nginx/html/organization.json"
)).normalize().toAbsolutePath();
copyToTarget(stagedOrganizationJson, organizationJsonTarget);
appendLog(logFile, "web assets applied: " + stagedWebDir + " -> " + targetDir);
appendLog(logFile, "organization.json applied: " + stagedOrganizationJson + " -> " + organizationJsonTarget);
return InitStepExecutionResult.success("web assets applied", 0, logFile.toString());
}
private InitStepExecutionResult executeStandardStartCommand(
InitTaskEntity task,
InitTaskStepEntity step,
String stepName,
String command
) throws IOException {
PlanContext context = loadPlanContext(task);
if ("DIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, stepName + " skipped for direct product type");
return InitStepExecutionResult.success(stepName + " skipped", 0, logPath);
}
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, stepName + " simulated");
return InitStepExecutionResult.success(stepName + " simulated", 0, logPath);
}
if (isBlank(command)) {
String logPath = writeInternalLog(task, step, stepName + " failed: command is blank");
return InitStepExecutionResult.failure(stepName + " failed: command is blank", -1, logPath);
}
Path logFile = prepareLogPath(task, step);
Path workingDirectory = resolveExistingStandardWorkingDirectory();
if (workingDirectory == null) {
appendLog(logFile, "cwd skipped: standardHomeDir not found");
}
try {
int code = runCommand(
logFile,
Arrays.asList("/bin/zsh", "-lc", command),
false,
workingDirectory,
buildStandardStartEnvironment(stepName)
);
if (code != 0) {
return InitStepExecutionResult.failure(stepName + " failed", code, logFile.toString());
}
return InitStepExecutionResult.success(stepName + " success", 0, logFile.toString());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return InitStepExecutionResult.failure(stepName + " interrupted", -1, logFile.toString());
}
}
private InitStepExecutionResult executeTlqInitCfg(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!"DIRECT".equals(context.productType()) || !"RABBITMQ_TLQ".equals(context.mqType())) {
@@ -976,6 +1231,20 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
return replacements;
}
private Map<String, String> buildStandardSqlReplacements(PlanContext context) {
Map<String, String> replacements = new LinkedHashMap<>();
Map<String, String> configReplacements = buildStandardReplacements(context);
replacements.put("AAAABBBBXXX", normalize(context.orgCode()));
replacements.put("01tlq.ip", normalize(configReplacements.get("mq_01_ip")));
replacements.put("02tlq.ip", normalize(configReplacements.get("mq_02_ip")));
replacements.put("sptp.01app.ip", normalize(configReplacements.get("sptp_01app_ip")));
replacements.put("sptp.02app.ip", normalize(configReplacements.get("sptp_02app_ip")));
replacements.put("mybiccode", normalize(configReplacements.get("MY_BIC_CODE")));
replacements.put("mycipsid", normalize(configReplacements.get("MY_CIPS_ID")));
replacements.put("myleicode", normalize(configReplacements.get("MY_LEI_CODE")));
return replacements;
}
private Map<String, String> buildDirectReplacements(PlanContext context) {
Map<String, String> replacements = new LinkedHashMap<>();
String node01 = normalize(context.node01Ip());
@@ -1117,7 +1386,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
}
List<Path> candidates = new ArrayList<>();
addAbsoluteCandidate(candidates, firstNonBlank(properties.getStandardCpconfigPath(), "/home/cemp4i/cpconfig.cfg"));
addAbsoluteCandidate(candidates, properties.getStandardCpconfigPath());
return candidates;
}
@@ -1229,6 +1498,53 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
return target;
}
private Path resolveStandardDbStageSqlDir(InitTaskEntity task) {
Path stageRoot = Paths.get(firstNonBlank(properties.getStagingRootDir(), "/tmp/tms-init-staging"))
.normalize()
.toAbsolutePath();
return stageRoot.resolve(task.getTaskId()).resolve("db").resolve("sql");
}
private Path resolveStandardNginxStageFile(InitTaskEntity task) {
Path stageRoot = Paths.get(firstNonBlank(properties.getStagingRootDir(), "/tmp/tms-init-staging"))
.normalize()
.toAbsolutePath();
return stageRoot.resolve(task.getTaskId()).resolve("nginx").resolve("nginx.conf");
}
private Path resolveStandardWebStageDir(InitTaskEntity task) {
Path stageRoot = Paths.get(firstNonBlank(properties.getStagingRootDir(), "/tmp/tms-init-staging"))
.normalize()
.toAbsolutePath();
return stageRoot.resolve(task.getTaskId()).resolve("web");
}
private Path resolveExistingStandardWorkingDirectory() {
String configured = properties.getStandardHomeDir();
if (isBlank(configured)) {
return null;
}
Path path = Paths.get(configured).normalize().toAbsolutePath();
if (!Files.isDirectory(path)) {
return null;
}
return path;
}
private String resolveStandardDbCmtpTemplateName(PlanContext context) {
return ("TLQ".equals(context.mqType()) || "RABBITMQ_TLQ".equals(context.mqType()))
? "CMTP-01-tlq.sql"
: "CMTP-01-raq.sql";
}
private void writeRenderedSql(Path target, String templateContent, Map<String, String> replacements) throws IOException {
String rendered = templateContent;
for (Map.Entry<String, String> entry : replacements.entrySet()) {
rendered = rendered.replace(entry.getKey(), normalize(entry.getValue()));
}
Files.writeString(target, rendered, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
}
private void copyToTarget(Path source, Path target) throws IOException {
if (target.getParent() != null) {
Files.createDirectories(target.getParent());
@@ -1236,11 +1552,78 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
}
private void unzipToDirectory(Path zipFile, Path targetDir) throws IOException {
Files.createDirectories(targetDir);
try (ZipInputStream inputStream = new ZipInputStream(Files.newInputStream(zipFile))) {
ZipEntry entry;
while ((entry = inputStream.getNextEntry()) != null) {
Path output = targetDir.resolve(entry.getName()).normalize();
if (!output.startsWith(targetDir)) {
throw new IOException("zip entry escapes target dir: " + entry.getName());
}
if (entry.isDirectory()) {
Files.createDirectories(output);
} else {
if (output.getParent() != null) {
Files.createDirectories(output.getParent());
}
Files.copy(inputStream, output, StandardCopyOption.REPLACE_EXISTING);
}
inputStream.closeEntry();
}
}
}
private void recreateDirectory(Path directory) throws IOException {
if (Files.exists(directory)) {
deleteRecursively(directory);
}
Files.createDirectories(directory);
}
private void copyDirectoryContents(Path sourceDir, Path targetDir, String... excludes) throws IOException {
Set<String> excludeSet = Set.of(excludes);
try (Stream<Path> stream = Files.walk(sourceDir)) {
for (Path source : stream.toList()) {
if (source.equals(sourceDir)) {
continue;
}
Path relative = sourceDir.relativize(source);
if (!relative.toString().isEmpty() && excludeSet.contains(relative.toString())) {
continue;
}
Path target = targetDir.resolve(relative.toString()).normalize();
if (Files.isDirectory(source)) {
Files.createDirectories(target);
} else {
copyToTarget(source, target);
}
}
}
}
private void deleteRecursively(Path path) throws IOException {
if (!Files.exists(path)) {
return;
}
try (Stream<Path> stream = Files.walk(path)) {
for (Path item : stream.sorted((left, right) -> right.getNameCount() - left.getNameCount()).toList()) {
Files.deleteIfExists(item);
}
}
}
private String escapeJson(String raw) {
return normalize(raw)
.replace("\\", "\\\\")
.replace("\"", "\\\"");
}
private String resolveReceiverLicenseTargetPath(String productType) {
if ("DIRECT".equals(productType)) {
return firstNonBlank(properties.getDirectReceiverLicenseTargetPath(), "/home/cmep4d/cmep/license/cmep.license");
}
return firstNonBlank(properties.getStandardReceiverLicenseTargetPath(), "/home/cemp4i/cmep/license/cmep.license");
return properties.getStandardReceiverLicenseTargetPath();
}
private String resolveTlqLicenseTargetPath(String productType) {
@@ -1292,11 +1675,25 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
}
private int runCommand(Path logFile, List<String> command, boolean ignoreFailure, Path workingDirectory) throws IOException, InterruptedException {
return runCommand(logFile, command, ignoreFailure, workingDirectory, null);
}
private int runCommand(
Path logFile,
List<String> command,
boolean ignoreFailure,
Path workingDirectory,
Map<String, String> extraEnvironment
) throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder(command);
if (workingDirectory != null) {
pb.directory(workingDirectory.toFile());
appendLog(logFile, "cwd=" + workingDirectory);
}
if (extraEnvironment != null && !extraEnvironment.isEmpty()) {
pb.environment().putAll(extraEnvironment);
appendLog(logFile, "envKeys=" + extraEnvironment.keySet());
}
pb.redirectErrorStream(true);
pb.redirectOutput(ProcessBuilder.Redirect.appendTo(logFile.toFile()));
Process process = pb.start();
@@ -1313,6 +1710,29 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
return exitCode;
}
private Map<String, String> buildStandardDbApplyEnvironment(Path loadDir) {
Map<String, String> environment = new LinkedHashMap<>();
if (!isBlank(properties.getStandardDbUsername())) {
environment.put("DB_USER", normalize(properties.getStandardDbUsername()));
}
if (!isBlank(properties.getStandardDbPassword())) {
environment.put("DB_PASSWORD", normalize(properties.getStandardDbPassword()));
}
if (loadDir != null) {
environment.put("LOAD_DIR", loadDir.toAbsolutePath().normalize().toString());
}
return environment;
}
private Map<String, String> buildStandardStartEnvironment(String stepName) {
Map<String, String> environment = new LinkedHashMap<>();
if ("start_cmsp_cmtp".equals(stepName)) {
environment.put("STANDARD_HOME_DIR", normalize(properties.getStandardHomeDir()));
environment.put("STANDARD_RUN_USER", normalize(properties.getStandardRunUser()));
}
return environment;
}
private List<Path> resolveGlobFiles(List<String> patterns) throws IOException {
List<Path> result = new ArrayList<>();
if (patterns == null || patterns.isEmpty()) {
@@ -21,20 +21,27 @@ import com.cisd.tms.modules.init.repository.InitTaskRepository;
import com.cisd.tms.modules.init.repository.InitTaskStepRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@@ -43,7 +50,8 @@ public class InitService {
private static final Set<String> PRESET_PRODUCT_TYPES = Set.of("ENTERPRISE", "INDIRECT", "DIRECT");
private static final Set<String> DEPLOY_STANDARD = Set.of("SINGLE", "DUAL");
private static final Set<String> DEPLOY_DIRECT = Set.of("SINGLE", "DUAL", "QUAD");
private static final Set<String> MQ_STANDARD = Set.of("RABBITMQ", "TLQ");
private static final Set<String> MQ_ENTERPRISE = Set.of("RABBITMQ", "TLQ");
private static final Set<String> MQ_INDIRECT = Set.of("RABBITMQ", "RABBITMQ_TLQ");
private static final Set<String> MQ_DIRECT = Set.of("RABBITMQ_TLQ", "RABBITMQ_CFMQ");
private static final Pattern FILE_ID_PATTERN = Pattern.compile("^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$");
@@ -54,7 +62,10 @@ public class InitService {
private final InitStepExecutor initStepExecutor;
private final InitCommandProfileService initCommandProfileService;
private final InitExecutorProperties initExecutorProperties;
private final Executor backgroundExecutor;
private final ConcurrentMap<String, CompletableFuture<Void>> activeExecutions;
@Autowired
public InitService(
InitTaskRepository initTaskRepository,
InitTaskStepRepository initTaskStepRepository,
@@ -63,6 +74,30 @@ public class InitService {
InitStepExecutor initStepExecutor,
InitCommandProfileService initCommandProfileService,
InitExecutorProperties initExecutorProperties
) {
this(
initTaskRepository,
initTaskStepRepository,
cisdPresetProperties,
objectMapper,
initStepExecutor,
initCommandProfileService,
initExecutorProperties,
Executors.newCachedThreadPool(),
new ConcurrentHashMap<>()
);
}
InitService(
InitTaskRepository initTaskRepository,
InitTaskStepRepository initTaskStepRepository,
CisdPresetProperties cisdPresetProperties,
ObjectMapper objectMapper,
InitStepExecutor initStepExecutor,
InitCommandProfileService initCommandProfileService,
InitExecutorProperties initExecutorProperties,
Executor backgroundExecutor,
ConcurrentMap<String, CompletableFuture<Void>> activeExecutions
) {
this.initTaskRepository = initTaskRepository;
this.initTaskStepRepository = initTaskStepRepository;
@@ -71,6 +106,8 @@ public class InitService {
this.initStepExecutor = initStepExecutor;
this.initCommandProfileService = initCommandProfileService;
this.initExecutorProperties = initExecutorProperties;
this.backgroundExecutor = backgroundExecutor;
this.activeExecutions = activeExecutions;
}
/**
@@ -238,48 +275,97 @@ public class InitService {
if (steps.isEmpty()) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "init task has no steps: " + taskId);
}
if (steps.stream().allMatch(step -> "SUCCESS".equals(step.getStatus()))) {
task.setStatus("SUCCESS");
task.setUpdateTime(LocalDateTime.now());
initTaskRepository.update(task);
return buildExecuteResponse(task, steps);
}
// 任务进入运行态,后续每个步骤状态独立推进
task.setStatus("RUNNING");
task.setUpdateTime(LocalDateTime.now());
initTaskRepository.update(task);
synchronized (activeExecutions) {
CompletableFuture<Void> active = activeExecutions.get(taskId);
if (active != null && !active.isDone()) {
task.setStatus("RUNNING");
task.setUpdateTime(LocalDateTime.now());
initTaskRepository.update(task);
return buildExecuteResponse(task, steps);
}
task.setStatus("RUNNING");
task.setUpdateTime(LocalDateTime.now());
initTaskRepository.update(task);
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> executeTaskInBackground(taskId), backgroundExecutor)
.whenComplete((ignored, ex) -> activeExecutions.remove(taskId));
activeExecutions.put(taskId, future);
}
return buildExecuteResponse(task, steps);
}
private void executeTaskInBackground(String taskId) {
InitTaskEntity task = requireTask(taskId);
List<InitTaskStepEntity> steps = new ArrayList<>(initTaskStepRepository.findByTaskIdOrderByStepNo(taskId));
if (steps.isEmpty()) {
task.setStatus("FAILED");
task.setUpdateTime(LocalDateTime.now());
initTaskRepository.update(task);
return;
}
steps.sort(Comparator.comparing(item -> item.getStepNo() == null ? Integer.MAX_VALUE : item.getStepNo()));
resetResidualRunningSteps(steps);
initTaskStepRepository.updateBatch(steps);
int successSteps = 0;
boolean failed = false;
for (InitTaskStepEntity step : steps) {
if ("SUCCESS".equals(step.getStatus())) {
continue;
}
step.setStatus("RUNNING");
step.setMessage("running");
step.setUpdateTime(LocalDateTime.now());
initTaskStepRepository.updateBatch(steps);
InitStepExecutionResult result;
try {
result = initStepExecutor.execute(task, step);
} catch (RuntimeException ex) {
result = InitStepExecutionResult.failure(ex.getMessage(), -1, step.getLogPath());
}
// 串行执行:每步结束后立即持有 exitCode/logPath/message,便于失败诊断
InitStepExecutionResult result = initStepExecutor.execute(task, step);
step.setExitCode(result.getExitCode());
step.setLogPath(result.getLogPath());
step.setMessage(result.getMessage());
step.setUpdateTime(LocalDateTime.now());
if (result.isSuccess()) {
step.setStatus("SUCCESS");
successSteps++;
} else {
// 当前步骤失败后立刻中断,保持“失败即停”语义
step.setStatus("FAILED");
step.setStatus(result.isSuccess() ? "SUCCESS" : "FAILED");
initTaskStepRepository.updateBatch(steps);
if (!result.isSuccess()) {
failed = true;
break;
}
}
initTaskStepRepository.updateBatch(steps);
// 任务最终态由步骤执行结果汇总得出
task.setStatus(failed ? "FAILED" : "SUCCESS");
task.setUpdateTime(LocalDateTime.now());
initTaskRepository.update(task);
}
private void resetResidualRunningSteps(List<InitTaskStepEntity> steps) {
for (InitTaskStepEntity step : steps) {
if (!"RUNNING".equals(step.getStatus())) {
continue;
}
step.setStatus("FAILED");
step.setMessage(isBlank(step.getMessage()) ? "interrupted previously" : step.getMessage());
step.setUpdateTime(LocalDateTime.now());
}
}
private InitTaskExecuteResponse buildExecuteResponse(InitTaskEntity task, List<InitTaskStepEntity> steps) {
InitTaskExecuteResponse response = new InitTaskExecuteResponse();
response.setTaskId(taskId);
response.setTaskId(task.getTaskId());
response.setStatus(task.getStatus());
response.setTotalSteps(steps.size());
response.setSuccessSteps(successSteps);
response.setSuccessSteps((int) steps.stream().filter(step -> "SUCCESS".equals(step.getStatus())).count());
return response;
}
@@ -325,10 +411,60 @@ public class InitService {
}
return List.of("RENDER_CONFIG", "FILE_PREPARE", "MQ_USER_APPLY", "DB_INIT", "TLQ_INIT_CFG", "VERIFY");
}
if ("TLQ".equals(mqType)) {
return List.of("RENDER_CONFIG", "FILE_PREPARE", "SET_TLQ", "MQ_USER_APPLY", "MQ_QUEUE_APPLY", "DB_INIT", "DEPLOY_RECEIVER", "APP_MQ_CREDENTIALS_APPLY", "VERIFY");
if ("ENTERPRISE".equals(productType) && "TLQ".equals(mqType)) {
return List.of(
"RENDER_CONFIG",
"FILE_PREPARE",
"SET_TLQ",
"TLQ_CONF_APPLY",
"DB_RENDER",
"DB_APPLY",
"DEPLOY_RECEIVER",
"NGINX_CONFIG_APPLY",
"WEB_CONFIG_APPLY",
"START_CMSP_CMTP",
"START_NGINX",
"POST_CHECK",
"VERIFY"
);
}
return List.of("RENDER_CONFIG", "FILE_PREPARE", "SET_RABBITMQ", "MQ_USER_APPLY", "MQ_QUEUE_APPLY", "DB_INIT", "DEPLOY_RECEIVER", "APP_MQ_CREDENTIALS_APPLY", "VERIFY");
if ("INDIRECT".equals(productType) && "RABBITMQ_TLQ".equals(mqType)) {
return List.of(
"RENDER_CONFIG",
"FILE_PREPARE",
"SET_TLQ",
"TLQ_CONF_APPLY",
"MQ_USER_APPLY",
"MQ_QUEUE_APPLY",
"DB_RENDER",
"DB_APPLY",
"DEPLOY_RECEIVER",
"APP_MQ_CREDENTIALS_APPLY",
"NGINX_CONFIG_APPLY",
"WEB_CONFIG_APPLY",
"START_CMSP_CMTP",
"START_NGINX",
"POST_CHECK",
"VERIFY"
);
}
return List.of(
"RENDER_CONFIG",
"FILE_PREPARE",
"SET_RABBITMQ",
"MQ_USER_APPLY",
"MQ_QUEUE_APPLY",
"DB_RENDER",
"DB_APPLY",
"DEPLOY_RECEIVER",
"APP_MQ_CREDENTIALS_APPLY",
"NGINX_CONFIG_APPLY",
"WEB_CONFIG_APPLY",
"START_CMSP_CMTP",
"START_NGINX",
"POST_CHECK",
"VERIFY"
);
}
private String buildTaskId() {
@@ -387,23 +523,33 @@ public class InitService {
return;
}
validateStandard(request, deployMode, mqType);
validateStandard(productType, request, deployMode, mqType);
}
private void validateStandard(InitPreviewRequest request, String deployMode, String mqType) {
private void validateStandard(String productType, InitPreviewRequest request, String deployMode, String mqType) {
requireAllowed("deployMode", deployMode, DEPLOY_STANDARD);
requireAllowed("mq.mqType", mqType, MQ_STANDARD);
requireAllowed("mq.mqType", mqType, resolveStandardMqAllowed(productType));
if ("DUAL".equals(deployMode)) {
requireNotBlank("nodes.node02Ip", request.getNodes().getNode02Ip());
}
if ("TLQ".equals(mqType)) {
if ("TLQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType)) {
requireNotBlank("mq.tlqLicenseFileId", request.getMq().getTlqLicenseFileId());
requireFileId("mq.tlqLicenseFileId", request.getMq().getTlqLicenseFileId());
}
}
private Set<String> resolveStandardMqAllowed(String productType) {
if ("ENTERPRISE".equals(productType)) {
return MQ_ENTERPRISE;
}
if ("INDIRECT".equals(productType)) {
return MQ_INDIRECT;
}
throw new IllegalArgumentException("unsupported standard productType: " + productType);
}
private void validateDirect(InitPreviewRequest request, String deployMode, String mqType) {
requireAllowed("deployMode", deployMode, DEPLOY_DIRECT);
requireAllowed("mq.mqType", mqType, MQ_DIRECT);
@@ -1,4 +1,4 @@
package com.cisd.tms.modules.sign.controller.internal;
package com.cisd.tms.modules.sign.controller;
import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.modules.sign.dto.internal.InternalSignPreviewRequest;
@@ -14,11 +14,11 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/sign")
public class InternalSignController {
public class SignController {
private final SignService signService;
public InternalSignController(SignService signService) {
public SignController(SignService signService) {
this.signService = signService;
}
@@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/system")
public class HealthInternalController {
public class HealthController {
@GetMapping("/health")
public ApiResponse<Map<String, Object>> status() {
+38
View File
@@ -38,6 +38,12 @@ springdoc:
operations-sorter: alpha
tms:
file:
storage:
# 上传文件根目录;上传接口会将文件保存到 <upload-base-dir>/<fileId>/<originalFilename>。
upload-base-dir: ${TMS_FILE_STORAGE_UPLOAD_BASE_DIR:/home/tms/uploads}
# 上传文件大小上限(字节)。
max-file-size-bytes: ${TMS_FILE_STORAGE_MAX_FILE_SIZE_BYTES:104857600}
init:
executor:
# 初始化执行模式:SIMULATE(仅模拟,不执行外部命令) / LOCAL(本机执行脚本命令)。
@@ -50,6 +56,38 @@ tms:
standard-home-dir: ${TMS_INIT_STANDARD_HOME_DIR:/home/cmep4i}
# 标准版 cpconfig.cfg 路径;默认取 standard-home-dir 下文件。
standard-cpconfig-path: ${TMS_INIT_EXECUTOR_STANDARD_CPCONFIG_PATH:${tms.init.executor.standard-home-dir}/cpconfig.cfg}
# 标准版数据库脚本基目录;默认取 standard-home-dir/cpackage,用于读取 sql_script 模板。
standard-db-script-base-dir: ${TMS_INIT_EXECUTOR_STANDARD_DB_SCRIPT_BASE_DIR:${tms.init.executor.standard-home-dir}/cpackage}
# 标准版数据库 SQL 加载目录;DB_APPLY 会将渲染后 SQL 复制到该目录。
standard-db-load-dir: ${TMS_INIT_EXECUTOR_STANDARD_DB_LOAD_DIR:${tms.init.executor.standard-home-dir}/mysql/loadfilepath}
# 标准版数据库导库用户名;默认复用应用数据源账号。
standard-db-username: ${TMS_INIT_EXECUTOR_STANDARD_DB_USERNAME:root}
# 标准版数据库导库密码;默认复用应用数据源密码。
standard-db-password: ${TMS_INIT_EXECUTOR_STANDARD_DB_PASSWORD:Sunyard123}
# 标准版数据库导入命令;LOCAL 模式下必须配置,否则 DB_APPLY 会直接失败。
standard-db-apply-command: ${TMS_INIT_EXECUTOR_STANDARD_DB_APPLY_COMMAND:/home/tms/bin/apply_standard_db.sh}
# 标准版数据库入口 SQL 文件名;与导库命令配合使用。
standard-db-entry-sql-file-name: ${TMS_INIT_EXECUTOR_STANDARD_DB_ENTRY_SQL_FILE_NAME:CMEP.sql}
# 标准版 nginx 模板路径;用于生成最终 nginx.conf。
standard-nginx-template-path: ${TMS_INIT_EXECUTOR_STANDARD_NGINX_TEMPLATE_PATH:${tms.init.executor.standard-home-dir}/cpackage/software/nginx.conf}
# 标准版 nginx 目标配置路径。
standard-nginx-target-path: ${TMS_INIT_EXECUTOR_STANDARD_NGINX_TARGET_PATH:/usr/local/nginx/conf/nginx.conf}
# 标准版 WEB 前端压缩包路径;默认对齐厂商 cpackage/front/front.zip。
standard-web-front-zip-path: ${TMS_INIT_EXECUTOR_STANDARD_WEB_FRONT_ZIP_PATH:${tms.init.executor.standard-home-dir}/cpackage/front/front.zip}
# 标准版 WEB 工作目录;用于解压前端产物。
standard-web-work-dir: ${TMS_INIT_EXECUTOR_STANDARD_WEB_WORK_DIR:${tms.init.executor.standard-home-dir}/web}
# 标准版 WEB 发布目录。
standard-web-target-dir: ${TMS_INIT_EXECUTOR_STANDARD_WEB_TARGET_DIR:/usr/local/nginx/html}
# 标准版 organization.json 输出路径;默认对齐厂商脚本写入位置。
standard-web-organization-json-path: ${TMS_INIT_EXECUTOR_STANDARD_WEB_ORGANIZATION_JSON_PATH:${tms.init.executor.standard-web-target-dir}/organization.json}
# 标准版 CMSP/CMTP 启动用户;start_standard_apps.sh 会读取该值。
standard-run-user: ${TMS_INIT_EXECUTOR_STANDARD_RUN_USER:cmep4i}
# 标准版 CMSP/CMTP 启动命令。
standard-app-start-command: ${TMS_INIT_EXECUTOR_STANDARD_APP_START_COMMAND:/home/tms/bin/start_standard_apps.sh}
# 标准版 nginx 启动命令。
standard-nginx-start-command: ${TMS_INIT_EXECUTOR_STANDARD_NGINX_START_COMMAND:/home/tms/bin/start_standard_nginx.sh}
# 标准版启动后检查命令。
standard-post-check-command: ${TMS_INIT_EXECUTOR_STANDARD_POST_CHECK_COMMAND:/home/tms/bin/check_standard_runtime.sh}
# 直参版 cpconfig 文件名(按 upload-base-dir/<cfgZipFileId>/<fileName> 解析)。
direct-cpconfig-file-name: ${TMS_INIT_EXECUTOR_DIRECT_CPCONFIG_FILE_NAME:cpconfig.cfg}
# 上传文件根目录;所有 fileId 均从该目录解析。
@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS tms_file_record (
id BIGINT PRIMARY KEY,
file_id VARCHAR(64) NOT NULL,
original_filename VARCHAR(255) NOT NULL,
storage_path VARCHAR(1024) NOT NULL,
status VARCHAR(32) NOT NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_file_record_file_id (file_id)
);
@@ -6,20 +6,30 @@
"SET_RABBITMQ": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setfraq.sh",
"MQ_USER_APPLY": "internal::mq_user_apply",
"MQ_QUEUE_APPLY": "internal::mq_queue_apply",
"DB_INIT": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setsql.sh",
"DB_RENDER": "internal::db_render",
"DB_APPLY": "internal::db_apply",
"DEPLOY_RECEIVER": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setsptp.sh",
"APP_MQ_CREDENTIALS_APPLY": "internal::app_mq_credentials_apply",
"NGINX_CONFIG_APPLY": "internal::nginx_config_apply",
"WEB_CONFIG_APPLY": "internal::web_config_apply",
"START_CMSP_CMTP": "internal::start_cmsp_cmtp",
"START_NGINX": "internal::start_nginx",
"POST_CHECK": "internal::post_check",
"VERIFY": "internal::verify"
},
"TLQ": {
"RENDER_CONFIG": "internal::render_config",
"FILE_PREPARE": "internal::file_prepare",
"SET_TLQ": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setftq.sh",
"MQ_USER_APPLY": "internal::mq_user_apply",
"MQ_QUEUE_APPLY": "internal::mq_queue_apply",
"DB_INIT": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setsql.sh",
"TLQ_CONF_APPLY": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/settlq.sh",
"DB_RENDER": "internal::db_render",
"DB_APPLY": "internal::db_apply",
"DEPLOY_RECEIVER": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setsptp.sh",
"APP_MQ_CREDENTIALS_APPLY": "internal::app_mq_credentials_apply",
"NGINX_CONFIG_APPLY": "internal::nginx_config_apply",
"WEB_CONFIG_APPLY": "internal::web_config_apply",
"START_CMSP_CMTP": "internal::start_cmsp_cmtp",
"START_NGINX": "internal::start_nginx",
"POST_CHECK": "internal::post_check",
"VERIFY": "internal::verify"
}
},
@@ -30,20 +40,33 @@
"SET_RABBITMQ": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setfraq.sh",
"MQ_USER_APPLY": "internal::mq_user_apply",
"MQ_QUEUE_APPLY": "internal::mq_queue_apply",
"DB_INIT": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setsql.sh",
"DB_RENDER": "internal::db_render",
"DB_APPLY": "internal::db_apply",
"DEPLOY_RECEIVER": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setsptp.sh",
"APP_MQ_CREDENTIALS_APPLY": "internal::app_mq_credentials_apply",
"NGINX_CONFIG_APPLY": "internal::nginx_config_apply",
"WEB_CONFIG_APPLY": "internal::web_config_apply",
"START_CMSP_CMTP": "internal::start_cmsp_cmtp",
"START_NGINX": "internal::start_nginx",
"POST_CHECK": "internal::post_check",
"VERIFY": "internal::verify"
},
"TLQ": {
"RABBITMQ_TLQ": {
"RENDER_CONFIG": "internal::render_config",
"FILE_PREPARE": "internal::file_prepare",
"SET_TLQ": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setftq.sh",
"TLQ_CONF_APPLY": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/settlq.sh",
"MQ_USER_APPLY": "internal::mq_user_apply",
"MQ_QUEUE_APPLY": "internal::mq_queue_apply",
"DB_INIT": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setsql.sh",
"DB_RENDER": "internal::db_render",
"DB_APPLY": "internal::db_apply",
"DEPLOY_RECEIVER": "bash {{STANDARD_HOME_DIR}}/cpackage/toolsh/setsptp.sh",
"APP_MQ_CREDENTIALS_APPLY": "internal::app_mq_credentials_apply",
"NGINX_CONFIG_APPLY": "internal::nginx_config_apply",
"WEB_CONFIG_APPLY": "internal::web_config_apply",
"START_CMSP_CMTP": "internal::start_cmsp_cmtp",
"START_NGINX": "internal::start_nginx",
"POST_CHECK": "internal::post_check",
"VERIFY": "internal::verify"
}
},
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cisd.tms.modules.file.mapper.FileRecordMapper">
<resultMap id="FileRecordResultMap" type="com.cisd.tms.modules.file.entity.FileRecordEntity">
<id property="id" column="id"/>
<result property="fileId" column="file_id"/>
<result property="originalFilename" column="original_filename"/>
<result property="storagePath" column="storage_path"/>
<result property="status" column="status"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<select id="selectByFileId" parameterType="string" resultMap="FileRecordResultMap">
SELECT id, file_id, original_filename, storage_path, status, create_time, update_time
FROM tms_file_record
WHERE file_id = #{fileId}
LIMIT 1
</select>
</mapper>
@@ -17,6 +17,7 @@ import com.cisd.tms.integration.crypto.pcie.model.GenerateKeyWithIpkEccRequest;
import com.cisd.tms.integration.crypto.pcie.model.GenerateKeyWithIpkRsaRequest;
import com.cisd.tms.integration.crypto.pcie.model.ImportKeyWithIskEccRequest;
import com.cisd.tms.integration.crypto.pcie.model.ImportKeyWithIskRsaRequest;
import com.cisd.tms.integration.crypto.pcie.model.RecoverKekRequest;
import com.cisd.tms.integration.crypto.pcie.model.RsaExternalPrivateKeyOpRequest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -0,0 +1,96 @@
package com.cisd.tms.modules.file;
import com.cisd.tms.common.exception.GlobalExceptionHandler;
import com.cisd.tms.modules.file.controller.FileController;
import com.cisd.tms.modules.file.dto.FileDetailResponse;
import com.cisd.tms.modules.file.dto.FileUploadResponse;
import com.cisd.tms.modules.file.service.FileService;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class FileInternalControllerTest {
private static final Path OPENAPI_YAML = Path.of("docs/openapi/cisd-init.openapi.yaml");
private static final Path OPENAPI_JSON = Path.of("docs/openapi/cisd-init.openapi.json");
@Test
void shouldUploadFileViaInternalApi() throws Exception {
FileService fileService = Mockito.mock(FileService.class);
FileUploadResponse response = new FileUploadResponse();
response.setFileId("FILE-20260310-000001");
response.setSize(12L);
response.setStoragePath("/home/tms/uploads/FILE-20260310-000001/license.dat");
Mockito.when(fileService.upload(Mockito.any())).thenReturn(response);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new FileController(fileService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
MockMultipartFile file = new MockMultipartFile("file", "license.dat", MediaType.APPLICATION_OCTET_STREAM_VALUE, "hello-file".getBytes());
mockMvc.perform(multipart("/api/v1/files/upload").file(file))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.fileId").value("FILE-20260310-000001"))
.andExpect(jsonPath("$.data.size").value(12))
.andExpect(jsonPath("$.data.storagePath").value("/home/tms/uploads/FILE-20260310-000001/license.dat"));
}
@Test
void shouldQueryFileDetailViaInternalApi() throws Exception {
FileService fileService = Mockito.mock(FileService.class);
FileDetailResponse response = new FileDetailResponse();
response.setFileId("FILE-20260310-000001");
response.setOriginalFilename("license.dat");
response.setStoragePath("/home/tms/uploads/FILE-20260310-000001/license.dat");
response.setStatus("ACTIVE");
response.setSize(12L);
Mockito.when(fileService.getDetail("FILE-20260310-000001")).thenReturn(response);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new FileController(fileService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(get("/api/v1/files/FILE-20260310-000001"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.fileId").value("FILE-20260310-000001"))
.andExpect(jsonPath("$.data.originalFilename").value("license.dat"))
.andExpect(jsonPath("$.data.storagePath").value("/home/tms/uploads/FILE-20260310-000001/license.dat"))
.andExpect(jsonPath("$.data.status").value("ACTIVE"))
.andExpect(jsonPath("$.data.size").value(12));
}
@Test
void shouldDocumentUploadApi() throws Exception {
String yaml = Files.readString(OPENAPI_YAML);
String json = Files.readString(OPENAPI_JSON);
org.junit.jupiter.api.Assertions.assertTrue(yaml.contains("/api/v1/files/upload:"));
org.junit.jupiter.api.Assertions.assertTrue(yaml.contains("/api/v1/files/{fileId}:"));
org.junit.jupiter.api.Assertions.assertTrue(yaml.contains("multipart/form-data:"));
org.junit.jupiter.api.Assertions.assertTrue(yaml.contains("ApiResponseFileUploadResponse"));
org.junit.jupiter.api.Assertions.assertTrue(yaml.contains("ApiResponseFileDetailResponse"));
org.junit.jupiter.api.Assertions.assertTrue(yaml.contains("fileId"));
org.junit.jupiter.api.Assertions.assertTrue(yaml.contains("先上传文件"));
org.junit.jupiter.api.Assertions.assertTrue(json.contains("/api/v1/files/upload"));
org.junit.jupiter.api.Assertions.assertTrue(json.contains("/api/v1/files/{fileId}"));
org.junit.jupiter.api.Assertions.assertTrue(json.contains("multipart/form-data"));
org.junit.jupiter.api.Assertions.assertTrue(json.contains("ApiResponseFileUploadResponse"));
org.junit.jupiter.api.Assertions.assertTrue(json.contains("ApiResponseFileDetailResponse"));
org.junit.jupiter.api.Assertions.assertTrue(json.contains("先上传文件"));
}
}
@@ -0,0 +1,125 @@
package com.cisd.tms.modules.file;
import com.cisd.tms.common.config.properties.FileStorageProperties;
import com.cisd.tms.modules.file.dto.FileDetailResponse;
import com.cisd.tms.modules.file.dto.FileUploadResponse;
import com.cisd.tms.modules.file.entity.FileRecordEntity;
import com.cisd.tms.modules.file.repository.FileRecordRepository;
import com.cisd.tms.modules.file.service.FileService;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockMultipartFile;
class FileServiceTest {
@Test
void shouldPersistUploadedFileMetadata() {
InMemoryFileRecordRepository repository = new InMemoryFileRecordRepository();
String fileId = "FILE-20260310-000001";
String originalFilename = "receiver-license.dat";
String storagePath = "/home/tms/uploads/" + fileId + "/" + originalFilename;
FileRecordEntity entity = new FileRecordEntity();
entity.setFileId(fileId);
entity.setOriginalFilename(originalFilename);
entity.setStoragePath(storagePath);
entity.setStatus("ACTIVE");
repository.save(entity);
FileRecordEntity persisted = repository.findByFileId(fileId).orElseThrow();
Assertions.assertEquals(fileId, persisted.getFileId());
Assertions.assertEquals(originalFilename, persisted.getOriginalFilename());
Assertions.assertEquals(storagePath, persisted.getStoragePath());
Assertions.assertEquals("ACTIVE", persisted.getStatus());
}
@Test
void shouldSaveUploadedFileUnderFileIdDirectory() throws IOException {
Path uploadBaseDir = Files.createTempDirectory("file-service-upload-test");
InMemoryFileRecordRepository repository = new InMemoryFileRecordRepository();
FileService service = new FileService(repository, properties(uploadBaseDir, 1024));
MockMultipartFile file = new MockMultipartFile(
"file",
"receiver-license.dat",
"application/octet-stream",
"license-content".getBytes()
);
FileUploadResponse response = service.upload(file);
FileRecordEntity persisted = repository.findByFileId(response.getFileId()).orElseThrow();
Assertions.assertNotNull(response.getFileId());
Assertions.assertEquals(file.getSize(), response.getSize());
Assertions.assertTrue(response.getStoragePath().endsWith("/" + response.getFileId() + "/receiver-license.dat"));
Assertions.assertTrue(Files.exists(Path.of(response.getStoragePath())));
Assertions.assertEquals("ACTIVE", persisted.getStatus());
Assertions.assertEquals(response.getStoragePath(), persisted.getStoragePath());
}
@Test
void shouldRejectBlankFileUpload() throws IOException {
Path uploadBaseDir = Files.createTempDirectory("file-service-upload-empty-test");
FileService service = new FileService(new InMemoryFileRecordRepository(), properties(uploadBaseDir, 1024));
MockMultipartFile file = new MockMultipartFile("file", "empty.txt", "text/plain", new byte[0]);
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> service.upload(file));
Assertions.assertTrue(ex.getMessage().contains("empty"));
}
@Test
void shouldRejectOversizeUpload() throws IOException {
Path uploadBaseDir = Files.createTempDirectory("file-service-upload-oversize-test");
FileService service = new FileService(new InMemoryFileRecordRepository(), properties(uploadBaseDir, 4));
MockMultipartFile file = new MockMultipartFile("file", "big.txt", "text/plain", "12345".getBytes());
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> service.upload(file));
Assertions.assertTrue(ex.getMessage().contains("size"));
}
@Test
void shouldReturnStoredFileDetailByFileId() throws IOException {
Path uploadBaseDir = Files.createTempDirectory("file-service-detail-test");
FileService service = new FileService(new InMemoryFileRecordRepository(), properties(uploadBaseDir, 1024));
MockMultipartFile file = new MockMultipartFile("file", "front.zip", "application/zip", "zip-data".getBytes());
FileUploadResponse uploaded = service.upload(file);
FileDetailResponse detail = service.getDetail(uploaded.getFileId());
Assertions.assertEquals(uploaded.getFileId(), detail.getFileId());
Assertions.assertEquals(uploaded.getSize(), detail.getSize());
Assertions.assertEquals(uploaded.getStoragePath(), detail.getStoragePath());
Assertions.assertEquals("front.zip", detail.getOriginalFilename());
Assertions.assertEquals("ACTIVE", detail.getStatus());
}
private static FileStorageProperties properties(Path uploadBaseDir, long maxFileSizeBytes) {
FileStorageProperties properties = new FileStorageProperties();
properties.setUploadBaseDir(uploadBaseDir.toString());
properties.setMaxFileSizeBytes(maxFileSizeBytes);
return properties;
}
private static class InMemoryFileRecordRepository implements FileRecordRepository {
private final Map<String, FileRecordEntity> data = new LinkedHashMap<>();
@Override
public Optional<FileRecordEntity> findByFileId(String fileId) {
return Optional.ofNullable(data.get(fileId));
}
@Override
public void save(FileRecordEntity entity) {
data.put(entity.getFileId(), entity);
}
}
}
@@ -7,10 +7,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import org.junit.jupiter.api.Assertions;
@@ -375,6 +378,557 @@ class ConfigurableInitStepExecutorTest {
Assertions.assertTrue(Files.readString(cmtpCfg).contains("spring.rabbitmq.password=" + encryptedPass));
}
@Test
void shouldRenderStandardDatabaseSqlPackageForEnterpriseRabbitmq() throws IOException {
Path tempRoot = Files.createTempDirectory("init-executor-standard-db-render-test");
Path standardHome = prepareStandardSqlScriptBase(tempRoot);
Path cpconfig = standardHome.resolve("cpconfig.cfg");
Files.writeString(cpconfig, """
MY_CIPSID_OR_BIC=OLD
mq_01_ip=127.0.0.1
mq_02_ip=127.0.0.2
sptp_01app_ip=127.0.0.3
sptp_02app_ip=127.0.0.4
sptp_01db_ip=127.0.0.5
MY_BIC_CODE=OLD
MY_CIPS_ID=
MY_LEI_CODE=
""");
InitExecutorProperties properties = new InitExecutorProperties();
properties.setMode(InitExecutorProperties.Mode.SIMULATE);
properties.setLogDir(tempRoot.resolve("logs").toString());
properties.setStagingRootDir(tempRoot.resolve("staging").toString());
properties.setStandardHomeDir(standardHome.toString());
properties.setStandardCpconfigPath(cpconfig.toString());
properties.setStandardDbScriptBaseDir(standardHome.resolve("cpackage").toString());
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
InitTaskEntity task = new InitTaskEntity();
task.setTaskId("TASK-STANDARD-DB-RENDER-1");
task.setProductType("ENTERPRISE");
task.setInitPlanJson(buildInitPlanJson(
"ENTERPRISE",
Map.of(
"orgCodeType", "BIC",
"orgCode", "AAAABBBBXXX",
"orgNameCn", "测试机构",
"deployMode", "DUAL",
"nodes", Map.of("node01Ip", "10.0.0.1", "node02Ip", "10.0.0.2"),
"mq", Map.of("mqType", "RABBITMQ", "channelUsername", "bank_user", "channelPassword", "bank_pass"),
"licenses", Map.of("receiverLicenseFileId", "receiver-001")
)
));
InitTaskStepEntity renderConfigStep = new InitTaskStepEntity();
renderConfigStep.setStepNo(1);
renderConfigStep.setStepCode("RENDER_CONFIG");
renderConfigStep.setCommandLine("internal::render_config");
InitStepExecutionResult renderConfig = executor.execute(task, renderConfigStep);
Assertions.assertTrue(renderConfig.isSuccess());
InitTaskStepEntity dbRenderStep = new InitTaskStepEntity();
dbRenderStep.setStepNo(2);
dbRenderStep.setStepCode("DB_RENDER");
dbRenderStep.setCommandLine("internal::db_render");
InitStepExecutionResult result = executor.execute(task, dbRenderStep);
Assertions.assertTrue(result.isSuccess());
Path sqlDir = tempRoot.resolve("staging").resolve("TASK-STANDARD-DB-RENDER-1").resolve("db").resolve("sql");
Path cmtpSql = sqlDir.resolve("CMTP-01.sql");
Path cmepDmlSql = sqlDir.resolve("CMEP-DML.sql");
Assertions.assertTrue(Files.exists(cmtpSql));
Assertions.assertTrue(Files.exists(cmepDmlSql));
Assertions.assertTrue(Files.exists(sqlDir.resolve("CMEP.sql")));
Assertions.assertTrue(Files.exists(sqlDir.resolve("SCHEMA-DDL.sql")));
Assertions.assertTrue(Files.exists(sqlDir.resolve("UP-ORG-INFO.sql")));
String cmtpContent = Files.readString(cmtpSql);
String cmepDmlContent = Files.readString(cmepDmlSql);
Assertions.assertTrue(cmtpContent.contains("CI01_AAAABBBBXXX"));
Assertions.assertTrue(cmtpContent.contains("10.0.0.1"));
Assertions.assertTrue(cmtpContent.contains("10.0.0.2"));
Assertions.assertFalse(cmtpContent.contains("01tlq.ip"));
Assertions.assertFalse(cmtpContent.contains("sptp.01app.ip"));
Assertions.assertTrue(cmepDmlContent.contains("AAAABBBBXXX"));
Assertions.assertTrue(cmepDmlContent.contains("mybiccode") || cmepDmlContent.contains("AAAABBBBXXX"));
}
@Test
void shouldResolveStandardCpconfigFromConfiguredStandardHomeDir() throws IOException {
Path tempRoot = Files.createTempDirectory("init-executor-standard-home-cpconfig-test");
Path standardHome = tempRoot.resolve("custom-standard-home");
Files.createDirectories(standardHome);
Path cpconfig = standardHome.resolve("cpconfig.cfg");
Files.writeString(cpconfig, """
MY_CIPSID_OR_BIC=OLD
mq_01_ip=127.0.0.1
mq_02_ip=127.0.0.2
sptp_01app_ip=127.0.0.3
sptp_02app_ip=127.0.0.4
sptp_01db_ip=127.0.0.5
MY_BIC_CODE=OLD
MY_CIPS_ID=
MY_LEI_CODE=
""");
InitExecutorProperties properties = new InitExecutorProperties();
properties.setMode(InitExecutorProperties.Mode.SIMULATE);
properties.setLogDir(tempRoot.resolve("logs").toString());
properties.setStandardHomeDir(standardHome.toString());
properties.setStandardCpconfigPath("");
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-HOME-CPCONFIG-1", "RABBITMQ", "SINGLE");
InitTaskStepEntity step = new InitTaskStepEntity();
step.setStepNo(1);
step.setStepCode("RENDER_CONFIG");
step.setCommandLine("internal::render_config");
InitStepExecutionResult result = executor.execute(task, step);
Assertions.assertTrue(result.isSuccess());
Assertions.assertTrue(Files.readString(cpconfig).contains("MY_CIPSID_OR_BIC=AAAABBBBXXX"));
}
@Test
void shouldApplyStandardDatabaseSqlPackageToLoadDir() throws IOException {
Path tempRoot = Files.createTempDirectory("init-executor-standard-db-apply-test");
Path standardHome = prepareStandardSqlScriptBase(tempRoot);
Path cpconfig = standardHome.resolve("cpconfig.cfg");
Files.writeString(cpconfig, """
MY_CIPSID_OR_BIC=OLD
mq_01_ip=127.0.0.1
mq_02_ip=127.0.0.2
sptp_01app_ip=127.0.0.3
sptp_02app_ip=127.0.0.4
sptp_01db_ip=127.0.0.5
MY_BIC_CODE=OLD
MY_CIPS_ID=
MY_LEI_CODE=
""");
Path loadDir = tempRoot.resolve("loadfilepath");
Path marker = tempRoot.resolve("db-apply.done");
InitExecutorProperties properties = new InitExecutorProperties();
properties.setMode(InitExecutorProperties.Mode.LOCAL);
properties.setLogDir(tempRoot.resolve("logs").toString());
properties.setStagingRootDir(tempRoot.resolve("staging").toString());
properties.setStandardHomeDir(standardHome.toString());
properties.setStandardCpconfigPath(cpconfig.toString());
properties.setStandardDbScriptBaseDir(standardHome.resolve("cpackage").toString());
properties.setStandardDbLoadDir(loadDir.toString());
properties.setStandardDbApplyCommand("test -f CMEP.sql && echo applied > '" + marker + "'");
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
InitTaskEntity task = new InitTaskEntity();
task.setTaskId("TASK-STANDARD-DB-APPLY-1");
task.setProductType("ENTERPRISE");
task.setInitPlanJson(buildInitPlanJson(
"ENTERPRISE",
Map.of(
"orgCodeType", "BIC",
"orgCode", "AAAABBBBXXX",
"orgNameCn", "测试机构",
"deployMode", "DUAL",
"nodes", Map.of("node01Ip", "10.0.0.1", "node02Ip", "10.0.0.2"),
"mq", Map.of("mqType", "RABBITMQ", "channelUsername", "bank_user", "channelPassword", "bank_pass"),
"licenses", Map.of("receiverLicenseFileId", "receiver-001")
)
));
InitTaskStepEntity renderConfigStep = new InitTaskStepEntity();
renderConfigStep.setStepNo(1);
renderConfigStep.setStepCode("RENDER_CONFIG");
renderConfigStep.setCommandLine("internal::render_config");
Assertions.assertTrue(executor.execute(task, renderConfigStep).isSuccess());
InitTaskStepEntity dbRenderStep = new InitTaskStepEntity();
dbRenderStep.setStepNo(2);
dbRenderStep.setStepCode("DB_RENDER");
dbRenderStep.setCommandLine("internal::db_render");
Assertions.assertTrue(executor.execute(task, dbRenderStep).isSuccess());
InitTaskStepEntity dbApplyStep = new InitTaskStepEntity();
dbApplyStep.setStepNo(3);
dbApplyStep.setStepCode("DB_APPLY");
dbApplyStep.setCommandLine("internal::db_apply");
InitStepExecutionResult result = executor.execute(task, dbApplyStep);
Assertions.assertTrue(result.isSuccess());
Assertions.assertTrue(Files.exists(loadDir.resolve("CMTP-01.sql")));
Assertions.assertTrue(Files.exists(loadDir.resolve("CMEP-DML.sql")));
Assertions.assertTrue(Files.exists(loadDir.resolve("CMEP.sql")));
Assertions.assertTrue(Files.exists(marker));
}
@Test
void shouldFailDbApplyWhenStandardDbApplyCommandMissingInLocalMode() throws IOException {
Path tempRoot = Files.createTempDirectory("init-executor-standard-db-apply-missing-command-test");
Path standardHome = prepareStandardSqlScriptBase(tempRoot);
Path cpconfig = standardHome.resolve("cpconfig.cfg");
Files.writeString(cpconfig, """
MY_CIPSID_OR_BIC=OLD
mq_01_ip=127.0.0.1
mq_02_ip=127.0.0.2
sptp_01app_ip=127.0.0.3
sptp_02app_ip=127.0.0.4
sptp_01db_ip=127.0.0.5
MY_BIC_CODE=OLD
MY_CIPS_ID=
MY_LEI_CODE=
""");
Path loadDir = tempRoot.resolve("loadfilepath");
InitExecutorProperties properties = new InitExecutorProperties();
properties.setMode(InitExecutorProperties.Mode.LOCAL);
properties.setLogDir(tempRoot.resolve("logs").toString());
properties.setStagingRootDir(tempRoot.resolve("staging").toString());
properties.setStandardHomeDir(standardHome.toString());
properties.setStandardCpconfigPath(cpconfig.toString());
properties.setStandardDbScriptBaseDir(standardHome.resolve("cpackage").toString());
properties.setStandardDbLoadDir(loadDir.toString());
properties.setStandardDbApplyCommand("");
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-DB-APPLY-MISSING-CMD-1", "RABBITMQ", "DUAL");
InitTaskStepEntity renderConfigStep = new InitTaskStepEntity();
renderConfigStep.setStepNo(1);
renderConfigStep.setStepCode("RENDER_CONFIG");
renderConfigStep.setCommandLine("internal::render_config");
Assertions.assertTrue(executor.execute(task, renderConfigStep).isSuccess());
InitTaskStepEntity dbRenderStep = new InitTaskStepEntity();
dbRenderStep.setStepNo(2);
dbRenderStep.setStepCode("DB_RENDER");
dbRenderStep.setCommandLine("internal::db_render");
Assertions.assertTrue(executor.execute(task, dbRenderStep).isSuccess());
InitTaskStepEntity dbApplyStep = new InitTaskStepEntity();
dbApplyStep.setStepNo(3);
dbApplyStep.setStepCode("DB_APPLY");
dbApplyStep.setCommandLine("internal::db_apply");
InitStepExecutionResult result = executor.execute(task, dbApplyStep);
Assertions.assertFalse(result.isSuccess());
Assertions.assertTrue(result.getMessage().contains("apply command is blank"));
}
@Test
void shouldInjectDatabaseCredentialsWhenExecutingStandardDbApplyCommand() throws IOException {
Path tempRoot = Files.createTempDirectory("init-executor-standard-db-apply-env-test");
Path standardHome = prepareStandardSqlScriptBase(tempRoot);
Path cpconfig = standardHome.resolve("cpconfig.cfg");
Files.writeString(cpconfig, """
MY_CIPSID_OR_BIC=OLD
mq_01_ip=127.0.0.1
mq_02_ip=127.0.0.2
sptp_01app_ip=127.0.0.3
sptp_02app_ip=127.0.0.4
sptp_01db_ip=127.0.0.5
MY_BIC_CODE=OLD
MY_CIPS_ID=
MY_LEI_CODE=
""");
Path loadDir = tempRoot.resolve("loadfilepath");
Path marker = tempRoot.resolve("db-apply-env.done");
InitExecutorProperties properties = new InitExecutorProperties();
properties.setMode(InitExecutorProperties.Mode.LOCAL);
properties.setLogDir(tempRoot.resolve("logs").toString());
properties.setStagingRootDir(tempRoot.resolve("staging").toString());
properties.setStandardHomeDir(standardHome.toString());
properties.setStandardCpconfigPath(cpconfig.toString());
properties.setStandardDbScriptBaseDir(standardHome.resolve("cpackage").toString());
properties.setStandardDbLoadDir(loadDir.toString());
properties.setStandardDbUsername("db_user");
properties.setStandardDbPassword("db_password");
properties.setStandardDbApplyCommand(
"test \"$DB_USER\" = \"db_user\""
+ " && test \"$DB_PASSWORD\" = \"db_password\""
+ " && test \"$LOAD_DIR\" = '" + loadDir.toAbsolutePath() + "'"
+ " && echo applied > '" + marker + "'"
);
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-DB-APPLY-ENV-1", "RABBITMQ", "DUAL");
InitTaskStepEntity renderConfigStep = new InitTaskStepEntity();
renderConfigStep.setStepNo(1);
renderConfigStep.setStepCode("RENDER_CONFIG");
renderConfigStep.setCommandLine("internal::render_config");
Assertions.assertTrue(executor.execute(task, renderConfigStep).isSuccess());
InitTaskStepEntity dbRenderStep = new InitTaskStepEntity();
dbRenderStep.setStepNo(2);
dbRenderStep.setStepCode("DB_RENDER");
dbRenderStep.setCommandLine("internal::db_render");
Assertions.assertTrue(executor.execute(task, dbRenderStep).isSuccess());
InitTaskStepEntity dbApplyStep = new InitTaskStepEntity();
dbApplyStep.setStepNo(3);
dbApplyStep.setStepCode("DB_APPLY");
dbApplyStep.setCommandLine("internal::db_apply");
InitStepExecutionResult result = executor.execute(task, dbApplyStep);
Assertions.assertTrue(result.isSuccess());
Assertions.assertTrue(Files.exists(marker));
}
@Test
void shouldApplyStandardNginxConfigFromTemplate() throws IOException {
Path tempRoot = Files.createTempDirectory("init-executor-standard-nginx-config-test");
Path template = tempRoot.resolve("nginx.conf.template");
Path target = tempRoot.resolve("target/nginx.conf");
Files.writeString(template, "upstream a { server sptp.01app.ip; }\nupstream b { server sptp.02app.ip; }\n");
InitExecutorProperties properties = new InitExecutorProperties();
properties.setMode(InitExecutorProperties.Mode.LOCAL);
properties.setLogDir(tempRoot.resolve("logs").toString());
properties.setStagingRootDir(tempRoot.resolve("staging").toString());
properties.setStandardNginxTemplatePath(template.toString());
properties.setStandardNginxTargetPath(target.toString());
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-NGINX-1", "RABBITMQ", "DUAL");
InitTaskStepEntity step = new InitTaskStepEntity();
step.setStepNo(1);
step.setStepCode("NGINX_CONFIG_APPLY");
step.setCommandLine("internal::nginx_config_apply");
InitStepExecutionResult result = executor.execute(task, step);
Assertions.assertTrue(result.isSuccess());
Path staged = tempRoot.resolve("staging").resolve("TASK-STANDARD-NGINX-1").resolve("nginx").resolve("nginx.conf");
Assertions.assertTrue(Files.exists(staged));
Assertions.assertTrue(Files.exists(target));
String rendered = Files.readString(staged);
Assertions.assertTrue(rendered.contains("10.0.0.1"));
Assertions.assertTrue(rendered.contains("10.0.0.2"));
Assertions.assertFalse(rendered.contains("sptp.01app.ip"));
Assertions.assertFalse(rendered.contains("sptp.02app.ip"));
}
@Test
void shouldPublishStandardWebAssetsAndOrganizationJson() throws IOException {
Path tempRoot = Files.createTempDirectory("init-executor-standard-web-config-test");
Path frontZip = tempRoot.resolve("front.zip");
Path workDir = tempRoot.resolve("web-work");
Path targetDir = tempRoot.resolve("web-target");
Path organizationJson = tempRoot.resolve("config/organization.json");
writeZip(frontZip, Map.of("index.html", "<html>ok</html>", "assets/app.js", "console.log('ok');"));
InitExecutorProperties properties = new InitExecutorProperties();
properties.setMode(InitExecutorProperties.Mode.LOCAL);
properties.setLogDir(tempRoot.resolve("logs").toString());
properties.setStagingRootDir(tempRoot.resolve("staging").toString());
properties.setStandardWebFrontZipPath(frontZip.toString());
properties.setStandardWebWorkDir(workDir.toString());
properties.setStandardWebTargetDir(targetDir.toString());
properties.setStandardWebOrganizationJsonPath(organizationJson.toString());
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-WEB-1", "RABBITMQ", "SINGLE");
InitTaskStepEntity step = new InitTaskStepEntity();
step.setStepNo(1);
step.setStepCode("WEB_CONFIG_APPLY");
step.setCommandLine("internal::web_config_apply");
InitStepExecutionResult result = executor.execute(task, step);
Assertions.assertTrue(result.isSuccess());
Assertions.assertTrue(Files.exists(targetDir.resolve("index.html")));
Assertions.assertTrue(Files.exists(targetDir.resolve("assets/app.js")));
Assertions.assertTrue(Files.exists(organizationJson));
Assertions.assertTrue(Files.readString(organizationJson).contains("测试机构"));
}
@Test
void shouldStartStandardCmspCmtpWithConfiguredCommand() throws IOException {
Path tempRoot = Files.createTempDirectory("init-executor-standard-app-start-test");
Path marker = tempRoot.resolve("cmsp-cmtp.started");
InitExecutorProperties properties = new InitExecutorProperties();
properties.setMode(InitExecutorProperties.Mode.LOCAL);
properties.setLogDir(tempRoot.resolve("logs").toString());
properties.setStandardHomeDir("/home/custom-standard");
properties.setStandardRunUser("custom_user");
properties.setStandardAppStartCommand(
"test \"$STANDARD_HOME_DIR\" = \"/home/custom-standard\""
+ " && test \"$STANDARD_RUN_USER\" = \"custom_user\""
+ " && echo started > '" + marker + "'"
);
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-APP-START-1", "RABBITMQ", "SINGLE");
InitTaskStepEntity step = new InitTaskStepEntity();
step.setStepNo(1);
step.setStepCode("START_CMSP_CMTP");
step.setCommandLine("internal::start_cmsp_cmtp");
InitStepExecutionResult result = executor.execute(task, step);
Assertions.assertTrue(result.isSuccess());
Assertions.assertTrue(Files.exists(marker));
}
@Test
void shouldStartStandardNginxWithConfiguredCommand() throws IOException {
Path tempRoot = Files.createTempDirectory("init-executor-standard-nginx-start-test");
Path marker = tempRoot.resolve("nginx.started");
InitExecutorProperties properties = new InitExecutorProperties();
properties.setMode(InitExecutorProperties.Mode.LOCAL);
properties.setLogDir(tempRoot.resolve("logs").toString());
properties.setStandardNginxStartCommand("echo started > '" + marker + "'");
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-NGINX-START-1", "RABBITMQ", "SINGLE");
InitTaskStepEntity step = new InitTaskStepEntity();
step.setStepNo(1);
step.setStepCode("START_NGINX");
step.setCommandLine("internal::start_nginx");
InitStepExecutionResult result = executor.execute(task, step);
Assertions.assertTrue(result.isSuccess());
Assertions.assertTrue(Files.exists(marker));
}
@Test
void shouldRunStandardPostCheck() throws IOException {
Path tempRoot = Files.createTempDirectory("init-executor-standard-post-check-test");
Path marker = tempRoot.resolve("post-check.done");
InitExecutorProperties properties = new InitExecutorProperties();
properties.setMode(InitExecutorProperties.Mode.LOCAL);
properties.setLogDir(tempRoot.resolve("logs").toString());
properties.setStandardPostCheckCommand("echo checked > '" + marker + "'");
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-POST-CHECK-1", "RABBITMQ", "SINGLE");
InitTaskStepEntity step = new InitTaskStepEntity();
step.setStepNo(1);
step.setStepCode("POST_CHECK");
step.setCommandLine("internal::post_check");
InitStepExecutionResult result = executor.execute(task, step);
Assertions.assertTrue(result.isSuccess());
Assertions.assertTrue(Files.exists(marker));
}
@Test
void shouldExposeStandardInitScriptTemplates() throws IOException {
Path repoRoot = Paths.get("").toAbsolutePath().normalize();
Path scriptDir = repoRoot.resolve("scripts/standard-init");
Path dbScript = scriptDir.resolve("apply_standard_db.sh");
Path appScript = scriptDir.resolve("start_standard_apps.sh");
Path nginxScript = scriptDir.resolve("start_standard_nginx.sh");
Path checkScript = scriptDir.resolve("check_standard_runtime.sh");
Assertions.assertTrue(Files.exists(dbScript));
Assertions.assertTrue(Files.exists(appScript));
Assertions.assertTrue(Files.exists(nginxScript));
Assertions.assertTrue(Files.exists(checkScript));
String dbContent = Files.readString(dbScript);
Assertions.assertTrue(dbContent.contains("SCHEMA-DDL.sql"));
Assertions.assertTrue(dbContent.contains("CMEP.sql"));
Assertions.assertTrue(dbContent.contains("UP-ORG-INFO.sql"));
Assertions.assertTrue(dbContent.contains("LOAD_DIR"));
Assertions.assertTrue(dbContent.contains("--force"));
Assertions.assertFalse(dbContent.contains("/home/cemp4i/mysql/loadfilepath"));
String appContent = Files.readString(appScript);
Assertions.assertTrue(appContent.contains("STANDARD_HOME_DIR"));
Assertions.assertTrue(appContent.contains("STANDARD_RUN_USER"));
Assertions.assertTrue(appContent.contains("start_cmsp.sh"));
Assertions.assertTrue(appContent.contains("start_cmtp.sh"));
Assertions.assertFalse(appContent.contains("/home/cemp4i/cmsp/start_cmsp.sh"));
Assertions.assertFalse(appContent.contains("su - cmep4i"));
String nginxContent = Files.readString(nginxScript);
Assertions.assertTrue(nginxContent.contains("/usr/local/nginx/sbin/nginx"));
String checkContent = Files.readString(checkScript);
Assertions.assertTrue(checkContent.contains("CMEP-CMSP"));
Assertions.assertTrue(checkContent.contains("CMEP-CMTP"));
Assertions.assertTrue(checkContent.contains("organization.json"));
}
@Test
void shouldDocumentStandardInitScriptTemplateDeployment() throws IOException {
Path repoRoot = Paths.get("").toAbsolutePath().normalize();
Path exampleConfig = repoRoot.resolve("config/application.yml.example");
Path applicationConfig = repoRoot.resolve("src/main/resources/application.yml");
Path readme = repoRoot.resolve("README.md");
String exampleContent = Files.readString(exampleConfig);
Assertions.assertTrue(exampleContent.contains("standard-db-apply-command: /home/tms/bin/apply_standard_db.sh"));
Assertions.assertTrue(exampleContent.contains("standard-run-user: cmep4i"));
Assertions.assertTrue(exampleContent.contains("standard-app-start-command: /home/tms/bin/start_standard_apps.sh"));
Assertions.assertTrue(exampleContent.contains("standard-nginx-start-command: /home/tms/bin/start_standard_nginx.sh"));
Assertions.assertTrue(exampleContent.contains("standard-post-check-command: /home/tms/bin/check_standard_runtime.sh"));
String applicationContent = Files.readString(applicationConfig);
Assertions.assertTrue(applicationContent.contains("standard-app-start-command: ${TMS_INIT_EXECUTOR_STANDARD_APP_START_COMMAND:/home/tms/bin/start_standard_apps.sh}"));
Assertions.assertTrue(applicationContent.contains("standard-nginx-start-command: ${TMS_INIT_EXECUTOR_STANDARD_NGINX_START_COMMAND:/home/tms/bin/start_standard_nginx.sh}"));
Assertions.assertTrue(applicationContent.contains("standard-post-check-command: ${TMS_INIT_EXECUTOR_STANDARD_POST_CHECK_COMMAND:/home/tms/bin/check_standard_runtime.sh}"));
String readmeContent = Files.readString(readme);
Assertions.assertTrue(readmeContent.contains("scripts/standard-init/"));
Assertions.assertTrue(readmeContent.contains("/home/tms/bin"));
Assertions.assertTrue(readmeContent.contains("apply_standard_db.sh"));
Assertions.assertTrue(readmeContent.contains("Default module layout: `controller + service + repository + entity + dto`."));
Assertions.assertTrue(readmeContent.contains("`/api/**` controllers live under `controller`"));
Assertions.assertFalse(readmeContent.contains("Default module layout: `controller/internal + service + repository + entity + dto`."));
Assertions.assertFalse(readmeContent.contains("Internal API controllers only in `*/controller/internal`, path prefix `/api/**`."));
}
@Test
void shouldSkipStandardAppMqCredentialsForEnterpriseTlq() throws Exception {
Path tempRoot = Files.createTempDirectory("init-executor-standard-rabbit-cred-skip-test");
Path cmspDir = tempRoot.resolve("cmsp");
Files.createDirectories(cmspDir);
Path cmspCfg = cmspDir.resolve("application-prd-ci01.properties");
Files.writeString(cmspCfg, "spring.rabbitmq.username=OLD\nspring.rabbitmq.password=OLD\n");
InitExecutorProperties properties = new InitExecutorProperties();
properties.setMode(InitExecutorProperties.Mode.LOCAL);
properties.setLogDir(tempRoot.resolve("logs").toString());
properties.setStandardRabbitAppConfigPatterns(List.of(cmspDir.resolve("application-prd*.properties").toString()));
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
InitTaskEntity task = new InitTaskEntity();
task.setTaskId("TASK-APP-CRED-SKIP-1");
task.setProductType("ENTERPRISE");
task.setInitPlanJson(buildInitPlanJson(
"ENTERPRISE",
Map.of(
"orgCodeType", "BIC",
"orgCode", "AAAABBBBXXX",
"orgNameCn", "测试机构",
"deployMode", "SINGLE",
"nodes", Map.of("node01Ip", "10.0.0.1"),
"mq", Map.of("mqType", "TLQ", "channelUsername", "new_user", "channelPassword", "new_pass", "tlqLicenseFileId", "tlq-001"),
"licenses", Map.of("receiverLicenseFileId", "file-receiver-001")
)
));
InitTaskStepEntity step = new InitTaskStepEntity();
step.setStepNo(1);
step.setStepCode("APP_MQ_CREDENTIALS_APPLY");
step.setCommandLine("internal::app_mq_credentials_apply");
InitStepExecutionResult result = executor.execute(task, step);
Assertions.assertTrue(result.isSuccess());
Assertions.assertTrue(result.getMessage().contains("skipped"));
Assertions.assertEquals("spring.rabbitmq.username=OLD\nspring.rabbitmq.password=OLD\n", Files.readString(cmspCfg));
}
@Test
void shouldRewriteAndExecuteStandardRabbitSetupScript() throws IOException {
Path tempRoot = Files.createTempDirectory("init-executor-rabbit-setup-script-test");
@@ -445,6 +999,65 @@ class ConfigurableInitStepExecutorTest {
Assertions.assertTrue(stagedScriptContent.contains("rabbitmqctl set_permissions -p /RQ 'bank_user' '.*' '.*' '.*'"));
}
@Test
void shouldExecuteStandardRabbitSetupScriptForIndirectRabbitmqTlq() throws IOException {
Path tempRoot = Files.createTempDirectory("init-executor-rabbit-setup-indirect-tlq-test");
Path setupScript = tempRoot.resolve("setuprabq.sh");
Path queueScript = tempRoot.resolve("addrabq_R_01.sh");
Path cpconfig = tempRoot.resolve("cpconfig.cfg");
Path calls = tempRoot.resolve("calls.log");
Files.writeString(cpconfig, "MY_CIPSID_OR_BIC=AAAABBBBXXX\n");
Files.writeString(
setupScript,
"#!/bin/bash\n"
+ "source /home/cmep4i/cpconfig.cfg\n"
+ "cd /home/cmep4i/cpackage/rabq\n"
+ "rabbitmqctl(){ echo \"$*\" >> \"" + calls + "\"; return 0; }\n"
+ "rabbitmqctl add_user rConn rConn\n"
+ "rabbitmqctl set_permissions -p /RQ rConn '.*' '.*' '.*'\n"
);
Files.writeString(queueScript, "#!/bin/bash\necho queue > /dev/null\n");
InitExecutorProperties properties = new InitExecutorProperties();
properties.setMode(InitExecutorProperties.Mode.LOCAL);
properties.setLogDir(tempRoot.resolve("logs").toString());
properties.setStagingRootDir(tempRoot.resolve("staging").toString());
properties.setStandardCpconfigPath(cpconfig.toString());
properties.setStandardRabbitSetupScriptPath(setupScript.toString());
properties.setStandardRabbitQueueScriptPath(queueScript.toString());
ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper());
InitTaskEntity task = new InitTaskEntity();
task.setTaskId("TASK-RABBIT-SETUP-INDIRECT-TLQ-1");
task.setProductType("INDIRECT");
task.setInitPlanJson(buildInitPlanJson(
"INDIRECT",
Map.of(
"orgCodeType", "BIC",
"orgCode", "AAAABBBBXXX",
"orgNameCn", "测试机构",
"deployMode", "SINGLE",
"nodes", Map.of("node01Ip", "10.0.0.1"),
"mq", Map.of("mqType", "RABBITMQ_TLQ", "channelUsername", "bank_user", "channelPassword", "bank_pass", "tlqLicenseFileId", "tlq-001"),
"licenses", Map.of("receiverLicenseFileId", "receiver-001")
)
));
InitTaskStepEntity step = new InitTaskStepEntity();
step.setStepNo(1);
step.setStepCode("MQ_USER_APPLY");
step.setCommandLine("internal::mq_user_apply");
InitStepExecutionResult result = executor.execute(task, step);
Assertions.assertTrue(result.isSuccess());
Assertions.assertFalse(result.getMessage().contains("skipped"));
Assertions.assertTrue(Files.exists(tempRoot.resolve("staging")
.resolve("TASK-RABBIT-SETUP-INDIRECT-TLQ-1")
.resolve("scripts")
.resolve("rabq")
.resolve("setuprabq.sh")));
}
@Test
void shouldExecuteStandardRabbitQueueScript() throws IOException {
Path tempRoot = Files.createTempDirectory("init-executor-rabbit-queue-script-test");
@@ -546,6 +1159,54 @@ class ConfigurableInitStepExecutorTest {
return new ObjectMapper().writeValueAsString(root);
}
private static InitTaskEntity buildStandardEnterpriseTask(String taskId, String mqType, String deployMode) throws IOException {
InitTaskEntity task = new InitTaskEntity();
task.setTaskId(taskId);
task.setProductType("ENTERPRISE");
task.setInitPlanJson(buildInitPlanJson(
"ENTERPRISE",
Map.of(
"orgCodeType", "BIC",
"orgCode", "AAAABBBBXXX",
"orgNameCn", "测试机构",
"deployMode", deployMode,
"nodes", "DUAL".equals(deployMode)
? Map.of("node01Ip", "10.0.0.1", "node02Ip", "10.0.0.2")
: Map.of("node01Ip", "10.0.0.1"),
"mq", Map.of("mqType", mqType, "channelUsername", "bank_user", "channelPassword", "bank_pass"),
"licenses", Map.of("receiverLicenseFileId", "receiver-001")
)
));
return task;
}
private static Path prepareStandardSqlScriptBase(Path tempRoot) throws IOException {
Path standardHome = tempRoot.resolve("standard-home");
Path sqlBase = standardHome.resolve("cpackage").resolve("sql_script");
Path sqlTmp = sqlBase.resolve("sql_script_tmp");
Files.createDirectories(sqlTmp);
Files.writeString(sqlBase.resolve("CMEP-DML.sql"), "INSERT INTO TEST VALUES ('AAAABBBBXXX', 'mybiccode', 'mycipsid', 'myleicode');\n");
Files.writeString(sqlBase.resolve("CMEP.sql"), "source /home/cmep4i/mysql/loadfilepath/CMEP-DML.sql;\nsource /home/cmep4i/mysql/loadfilepath/CMTP-01.sql;\n");
Files.writeString(sqlBase.resolve("SCHEMA-DDL.sql"), "CREATE TABLE TEST(ID INT);\n");
Files.writeString(sqlBase.resolve("UP-ORG-INFO.sql"), "UPDATE ORG SET CODE='AAAABBBBXXX';\n");
Files.writeString(sqlTmp.resolve("CMTP-01-raq.sql"), "INSERT INTO X VALUES('CI01_AAAABBBBXXX','AAAABBBBXXX','01tlq.ip','02tlq.ip','sptp.01app.ip','sptp.02app.ip');\n");
Files.writeString(sqlTmp.resolve("CMTP-01-tlq.sql"), "INSERT INTO X VALUES('CI01_AAAABBBBXXX','AAAABBBBXXX','01tlq.ip','02tlq.ip','sptp.01app.ip','sptp.02app.ip');\n");
return standardHome;
}
private static void writeZip(Path zipPath, Map<String, String> entries) throws IOException {
if (zipPath.getParent() != null) {
Files.createDirectories(zipPath.getParent());
}
try (ZipOutputStream outputStream = new ZipOutputStream(Files.newOutputStream(zipPath))) {
for (Map.Entry<String, String> entry : entries.entrySet()) {
outputStream.putNextEntry(new ZipEntry(entry.getKey()));
outputStream.write(entry.getValue().getBytes());
outputStream.closeEntry();
}
}
}
private static Path parseRenderedCpconfigPath(String message) {
Assertions.assertNotNull(message);
int start = message.indexOf("path=");
@@ -21,12 +21,18 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -52,6 +58,39 @@ class InitServiceTest {
Assertions.assertTrue(ex.getMessage().contains("tlqLicenseFileId"));
}
@Test
void shouldRejectEnterpriseRabbitmqTlqMode() {
InitService service = newService(emptyRepository(), emptyStepRepository(), preset("ENTERPRISE", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs");
InitPreviewRequest request = validEnterpriseRequest();
request.getMq().setMqType("RABBITMQ_TLQ");
request.getMq().setTlqLicenseFileId("file-tlq-001");
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> service.preview(request));
Assertions.assertTrue(ex.getMessage().contains("mq.mqType"));
}
@Test
void shouldRejectIndirectPureTlqMode() {
InitService service = newService(emptyRepository(), emptyStepRepository(), preset("INDIRECT", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs");
InitPreviewRequest request = validEnterpriseRequest();
request.getMq().setMqType("TLQ");
request.getMq().setTlqLicenseFileId("file-tlq-001");
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> service.preview(request));
Assertions.assertTrue(ex.getMessage().contains("mq.mqType"));
}
@Test
void shouldRequireTlqLicenseForIndirectRabbitmqTlqMode() {
InitService service = newService(emptyRepository(), emptyStepRepository(), preset("INDIRECT", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs");
InitPreviewRequest request = validEnterpriseRequest();
request.getMq().setMqType("RABBITMQ_TLQ");
request.getMq().setTlqLicenseFileId(null);
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> service.preview(request));
Assertions.assertTrue(ex.getMessage().contains("tlqLicenseFileId"));
}
@Test
void shouldRejectPathLikeFileId() {
InitService service = newService(emptyRepository(), emptyStepRepository(), preset("ENTERPRISE", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs");
@@ -113,6 +152,106 @@ class InitServiceTest {
Assertions.assertNotNull(response.getSummary());
}
@Test
void shouldPlanEnterpriseTlqWithoutRabbitmqSteps() {
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
InitService service = newService(repository, stepRepository, preset("ENTERPRISE", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs");
InitPreviewRequest request = validEnterpriseRequest();
request.getMq().setMqType("TLQ");
request.getMq().setTlqLicenseFileId("file-tlq-001");
InitCreateTaskResponse created = service.createTask(request);
List<InitTaskStepResponse> steps = service.getTaskSteps(created.getTaskId());
List<String> stepCodes = steps.stream().map(InitTaskStepResponse::getStepCode).toList();
Assertions.assertTrue(stepCodes.contains("SET_TLQ"));
Assertions.assertTrue(stepCodes.contains("TLQ_CONF_APPLY"));
Assertions.assertTrue(stepCodes.contains("DB_RENDER"));
Assertions.assertTrue(stepCodes.contains("DB_APPLY"));
Assertions.assertFalse(stepCodes.contains("DB_INIT"));
Assertions.assertFalse(stepCodes.contains("MQ_USER_APPLY"));
Assertions.assertFalse(stepCodes.contains("MQ_QUEUE_APPLY"));
Assertions.assertFalse(stepCodes.contains("APP_MQ_CREDENTIALS_APPLY"));
assertContainsStandardPostDeploySteps(stepCodes);
}
@Test
void shouldPlanIndirectRabbitmqTlqWithTlqAndRabbitmqSteps() {
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
InitService service = newService(repository, stepRepository, preset("INDIRECT", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs");
InitPreviewRequest request = validEnterpriseRequest();
request.getMq().setMqType("RABBITMQ_TLQ");
request.getMq().setTlqLicenseFileId("file-tlq-001");
InitCreateTaskResponse created = service.createTask(request);
List<InitTaskStepResponse> steps = service.getTaskSteps(created.getTaskId());
List<String> stepCodes = steps.stream().map(InitTaskStepResponse::getStepCode).toList();
Assertions.assertTrue(stepCodes.contains("SET_TLQ"));
Assertions.assertTrue(stepCodes.contains("TLQ_CONF_APPLY"));
Assertions.assertTrue(stepCodes.contains("DB_RENDER"));
Assertions.assertTrue(stepCodes.contains("DB_APPLY"));
Assertions.assertFalse(stepCodes.contains("DB_INIT"));
Assertions.assertTrue(stepCodes.contains("MQ_USER_APPLY"));
Assertions.assertTrue(stepCodes.contains("MQ_QUEUE_APPLY"));
Assertions.assertTrue(stepCodes.contains("APP_MQ_CREDENTIALS_APPLY"));
assertContainsStandardPostDeploySteps(stepCodes);
}
@Test
void shouldPlanEnterpriseRabbitmqWithDbRenderAndApply() {
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
InitService service = newService(repository, stepRepository, preset("ENTERPRISE", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs");
InitCreateTaskResponse created = service.createTask(validEnterpriseRequest());
List<InitTaskStepResponse> steps = service.getTaskSteps(created.getTaskId());
List<String> stepCodes = steps.stream().map(InitTaskStepResponse::getStepCode).toList();
Assertions.assertTrue(stepCodes.contains("DB_RENDER"));
Assertions.assertTrue(stepCodes.contains("DB_APPLY"));
Assertions.assertFalse(stepCodes.contains("DB_INIT"));
assertContainsStandardPostDeploySteps(stepCodes);
}
@Test
void shouldPlanIndirectRabbitmqWithDbRenderAndApply() {
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
InitService service = newService(repository, stepRepository, preset("INDIRECT", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs");
InitCreateTaskResponse created = service.createTask(validEnterpriseRequest());
List<InitTaskStepResponse> steps = service.getTaskSteps(created.getTaskId());
List<String> stepCodes = steps.stream().map(InitTaskStepResponse::getStepCode).toList();
Assertions.assertTrue(stepCodes.contains("DB_RENDER"));
Assertions.assertTrue(stepCodes.contains("DB_APPLY"));
Assertions.assertFalse(stepCodes.contains("DB_INIT"));
assertContainsStandardPostDeploySteps(stepCodes);
}
@Test
void shouldNotPlanStandardPostDeployStepsForDirectMode() {
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
InitService service = newService(repository, stepRepository, preset("DIRECT", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs");
InitPreviewRequest request = validDirectRequest();
request.getMq().setMqType("RABBITMQ_TLQ");
request.getMq().setTlqLicenseFileId("file-tlq-001");
request.setDirectTlq(directTlqConfig());
InitCreateTaskResponse created = service.createTask(request);
List<InitTaskStepResponse> steps = service.getTaskSteps(created.getTaskId());
List<String> stepCodes = steps.stream().map(InitTaskStepResponse::getStepCode).toList();
assertNotContainsStandardPostDeploySteps(stepCodes);
}
@Test
void shouldCreateAndQueryInitTask() {
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
@@ -155,10 +294,11 @@ class InitServiceTest {
InitCreateTaskResponse created = service.createTask(validEnterpriseRequest());
InitTaskExecuteResponse executeResponse = service.executeTask(created.getTaskId());
awaitTaskStatus(service, created.getTaskId(), "SUCCESS");
InitTaskDetailResponse detail = service.getTask(created.getTaskId());
List<InitTaskStepResponse> steps = service.getTaskSteps(created.getTaskId());
Assertions.assertEquals("SUCCESS", executeResponse.getStatus());
Assertions.assertEquals("RUNNING", executeResponse.getStatus());
Assertions.assertEquals("SUCCESS", detail.getStatus());
Assertions.assertTrue(steps.stream().allMatch(s -> "SUCCESS".equals(s.getStatus())));
}
@@ -171,9 +311,10 @@ class InitServiceTest {
InitCreateTaskResponse created = service.createTask(validEnterpriseRequest());
InitTaskExecuteResponse executeResponse = service.executeTask(created.getTaskId());
awaitTaskStatus(service, created.getTaskId(), "FAILED");
List<InitTaskStepResponse> steps = service.getTaskSteps(created.getTaskId());
Assertions.assertEquals("FAILED", executeResponse.getStatus());
Assertions.assertEquals("RUNNING", executeResponse.getStatus());
Assertions.assertTrue(steps.stream().anyMatch(s -> "FAILED".equals(s.getStatus())));
}
@@ -186,6 +327,7 @@ class InitServiceTest {
InitCreateTaskResponse created = service.createTask(validEnterpriseRequest());
service.executeTask(created.getTaskId());
awaitTaskStatus(service, created.getTaskId(), "SUCCESS");
InitTaskStepLogResponse log = service.getTaskStepLog(created.getTaskId(), 1);
@@ -195,6 +337,95 @@ class InitServiceTest {
Assertions.assertTrue(log.getLogPath().startsWith(logDir.toString()));
}
@Test
void shouldAcceptTaskExecutionAsynchronously() throws Exception {
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
BlockingInitStepExecutor executor = new BlockingInitStepExecutor();
InitService service = newService(repository, stepRepository, preset("ENTERPRISE", "V1.3.2", "CONFIG"), executor, "/tmp/tms-init-logs");
InitCreateTaskResponse created = service.createTask(validEnterpriseRequest());
InitTaskExecuteResponse response = Assertions.assertTimeoutPreemptively(
Duration.ofMillis(300),
() -> service.executeTask(created.getTaskId())
);
Assertions.assertEquals(created.getTaskId(), response.getTaskId());
Assertions.assertTrue(executor.awaitFirstStepStarted());
executor.release();
awaitTaskStatus(service, created.getTaskId(), "SUCCESS");
}
@Test
void shouldResumeFromFirstFailedStepOnRetry() {
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
FailOnceOnStepExecutor executor = new FailOnceOnStepExecutor("DEPLOY_RECEIVER");
InitService service = newService(repository, stepRepository, preset("ENTERPRISE", "V1.3.2", "CONFIG"), executor, "/tmp/tms-init-logs");
InitCreateTaskResponse created = service.createTask(validEnterpriseRequest());
service.executeTask(created.getTaskId());
awaitTaskStatus(service, created.getTaskId(), "FAILED");
service.executeTask(created.getTaskId());
awaitTaskStatus(service, created.getTaskId(), "SUCCESS");
Assertions.assertEquals(1, executor.executionCount("RENDER_CONFIG"));
Assertions.assertEquals(1, executor.executionCount("DB_RENDER"));
Assertions.assertEquals(2, executor.executionCount("DEPLOY_RECEIVER"));
Assertions.assertEquals(1, executor.executionCount("VERIFY"));
}
@Test
void shouldResumeFromResidualRunningStep() {
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
RecordingSuccessExecutor executor = new RecordingSuccessExecutor();
InitService service = newService(repository, stepRepository, preset("ENTERPRISE", "V1.3.2", "CONFIG"), executor, "/tmp/tms-init-logs");
InitCreateTaskResponse created = service.createTask(validEnterpriseRequest());
List<InitTaskStepEntity> steps = stepRepository.findByTaskIdOrderByStepNo(created.getTaskId());
steps.get(0).setStatus("SUCCESS");
steps.get(0).setMessage("done");
steps.get(1).setStatus("RUNNING");
steps.get(1).setMessage("running");
stepRepository.updateBatch(steps);
service.executeTask(created.getTaskId());
awaitTaskStatus(service, created.getTaskId(), "SUCCESS");
Assertions.assertEquals(0, executor.executionCount("RENDER_CONFIG"));
Assertions.assertEquals(1, executor.executionCount("FILE_PREPARE"));
Assertions.assertEquals(1, executor.executionCount("VERIFY"));
}
@Test
void shouldNotDispatchDuplicateBackgroundExecutionForSameTask() throws Exception {
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
DuplicateDispatchProbeExecutor executor = new DuplicateDispatchProbeExecutor();
InitService service = newService(repository, stepRepository, preset("ENTERPRISE", "V1.3.2", "CONFIG"), executor, "/tmp/tms-init-logs");
InitCreateTaskResponse created = service.createTask(validEnterpriseRequest());
Thread firstCall = new Thread(() -> service.executeTask(created.getTaskId()));
firstCall.start();
Assertions.assertTrue(executor.awaitFirstStepStarted());
InitTaskExecuteResponse duplicateResponse = Assertions.assertTimeoutPreemptively(
Duration.ofMillis(300),
() -> service.executeTask(created.getTaskId())
);
Assertions.assertEquals("RUNNING", duplicateResponse.getStatus());
Assertions.assertEquals(1, executor.getInvocationCount());
executor.release();
firstCall.join(2000);
}
private static InitService newService(
InitTaskRepository taskRepository,
InitTaskStepRepository stepRepository,
@@ -254,6 +485,23 @@ class InitServiceTest {
};
}
private static void awaitTaskStatus(InitService service, String taskId, String expectedStatus) {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
while (System.nanoTime() < deadline) {
String current = service.getTask(taskId).getStatus();
if (expectedStatus.equals(current)) {
return;
}
try {
Thread.sleep(20L);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
Assertions.fail("interrupted while waiting for task status");
}
}
Assertions.fail("expected task status " + expectedStatus + " within timeout");
}
private static InitPreviewRequest validEnterpriseRequest() {
InitPreviewRequest request = new InitPreviewRequest();
request.setOrgCodeType("BIC");
@@ -308,6 +556,28 @@ class InitServiceTest {
return config;
}
private static void assertContainsStandardPostDeploySteps(List<String> stepCodes) {
Assertions.assertTrue(stepCodes.contains("DB_RENDER"));
Assertions.assertTrue(stepCodes.contains("DB_APPLY"));
Assertions.assertTrue(stepCodes.contains("DEPLOY_RECEIVER"));
Assertions.assertTrue(stepCodes.contains("NGINX_CONFIG_APPLY"));
Assertions.assertTrue(stepCodes.contains("WEB_CONFIG_APPLY"));
Assertions.assertTrue(stepCodes.contains("START_CMSP_CMTP"));
Assertions.assertTrue(stepCodes.contains("START_NGINX"));
Assertions.assertTrue(stepCodes.contains("POST_CHECK"));
}
private static void assertNotContainsStandardPostDeploySteps(List<String> stepCodes) {
Assertions.assertFalse(stepCodes.contains("DB_RENDER"));
Assertions.assertFalse(stepCodes.contains("DB_APPLY"));
Assertions.assertFalse(stepCodes.contains("DEPLOY_RECEIVER"));
Assertions.assertFalse(stepCodes.contains("NGINX_CONFIG_APPLY"));
Assertions.assertFalse(stepCodes.contains("WEB_CONFIG_APPLY"));
Assertions.assertFalse(stepCodes.contains("START_CMSP_CMTP"));
Assertions.assertFalse(stepCodes.contains("START_NGINX"));
Assertions.assertFalse(stepCodes.contains("POST_CHECK"));
}
private static class InMemoryInitTaskRepository implements InitTaskRepository {
private final Map<String, InitTaskEntity> data = new HashMap<>();
@@ -364,4 +634,117 @@ class InitServiceTest {
data.put(taskId, new ArrayList<>(entities));
}
}
private static class BlockingInitStepExecutor implements InitStepExecutor {
private final CountDownLatch started = new CountDownLatch(1);
private final CountDownLatch release = new CountDownLatch(1);
@Override
public InitStepExecutionResult execute(InitTaskEntity task, InitTaskStepEntity step) {
started.countDown();
try {
if (!release.await(5, TimeUnit.SECONDS)) {
return InitStepExecutionResult.failure("timed out waiting for release", -1, null);
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return InitStepExecutionResult.failure("interrupted", -1, null);
}
return InitStepExecutionResult.success("ok", 0, null);
}
boolean awaitFirstStepStarted() {
try {
return started.await(1, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return false;
}
}
void release() {
release.countDown();
}
}
private static class RecordingSuccessExecutor implements InitStepExecutor {
private final List<String> executedStepCodes = new CopyOnWriteArrayList<>();
@Override
public InitStepExecutionResult execute(InitTaskEntity task, InitTaskStepEntity step) {
executedStepCodes.add(step.getStepCode());
return InitStepExecutionResult.success("ok", 0, null);
}
int executionCount(String stepCode) {
return (int) executedStepCodes.stream().filter(stepCode::equals).count();
}
}
private static class FailOnceOnStepExecutor implements InitStepExecutor {
private final String failStepCode;
private final AtomicBoolean failed = new AtomicBoolean(false);
private final List<String> executedStepCodes = new CopyOnWriteArrayList<>();
private FailOnceOnStepExecutor(String failStepCode) {
this.failStepCode = failStepCode;
}
@Override
public InitStepExecutionResult execute(InitTaskEntity task, InitTaskStepEntity step) {
executedStepCodes.add(step.getStepCode());
if (failStepCode.equals(step.getStepCode()) && failed.compareAndSet(false, true)) {
return InitStepExecutionResult.failure("simulated failed", 1, "/tmp/fail.log");
}
return InitStepExecutionResult.success("ok", 0, "/tmp/success.log");
}
int executionCount(String stepCode) {
return (int) executedStepCodes.stream().filter(stepCode::equals).count();
}
}
private static class DuplicateDispatchProbeExecutor implements InitStepExecutor {
private final CountDownLatch started = new CountDownLatch(1);
private final CountDownLatch release = new CountDownLatch(1);
private final AtomicInteger invocationCount = new AtomicInteger();
@Override
public InitStepExecutionResult execute(InitTaskEntity task, InitTaskStepEntity step) {
int current = invocationCount.incrementAndGet();
if (current == 1) {
started.countDown();
try {
if (!release.await(5, TimeUnit.SECONDS)) {
return InitStepExecutionResult.failure("timed out waiting for release", -1, null);
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return InitStepExecutionResult.failure("interrupted", -1, null);
}
}
return InitStepExecutionResult.success("ok", 0, null);
}
boolean awaitFirstStepStarted() {
try {
return started.await(1, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return false;
}
}
int getInvocationCount() {
return invocationCount.get();
}
void release() {
release.countDown();
}
}
}