diff --git a/src/main/java/com/cisd/tms/modules/auth/controller/AuthAdminController.java b/src/main/java/com/cisd/tms/modules/auth/controller/AuthAdminController.java index 6f1b577..4ea2145 100644 --- a/src/main/java/com/cisd/tms/modules/auth/controller/AuthAdminController.java +++ b/src/main/java/com/cisd/tms/modules/auth/controller/AuthAdminController.java @@ -12,6 +12,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -20,18 +22,15 @@ import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/v1/auth") +@RequiredArgsConstructor @Tag(name = "认证管理", description = "角色启用、密码重置与 UKey 绑定接口") public class AuthAdminController { private final AuthAdminService authAdminService; - public AuthAdminController(AuthAdminService authAdminService) { - this.authAdminService = authAdminService; - } - @PostMapping("/roles/{roleCode}/enable") - @Operation(summary = "启用角色", description = "仅允许 KEY_ADMIN FULL 会话启用目标角色。") - @RequireRole("KEY_ADMIN") + @Operation(summary = "启用角色", description = "仅允许 SUPER_ADMIN FULL 会话启用目标角色。") + @RequireRole("SUPER_ADMIN") @RequireAuthLevel("FULL") public ApiResponse enableRole(@PathVariable("roleCode") String roleCode, HttpServletRequest request) { authAdminService.enableRole( @@ -43,8 +42,8 @@ public class AuthAdminController { } @PostMapping("/roles/{roleCode}/reset-password") - @Operation(summary = "重置角色密码", description = "仅允许 KEY_ADMIN FULL 会话重置目标角色密码。") - @RequireRole("KEY_ADMIN") + @Operation(summary = "重置角色密码", description = "仅允许 SUPER_ADMIN FULL 会话重置目标角色密码。") + @RequireRole("SUPER_ADMIN") @RequireAuthLevel("FULL") public ApiResponse resetPassword(@PathVariable("roleCode") String roleCode, HttpServletRequest request) { authAdminService.resetPassword( @@ -56,8 +55,8 @@ public class AuthAdminController { } @PostMapping("/roles/{roleCode}/ukeys/bind") - @Operation(summary = "绑定角色 UKey", description = "仅允许 KEY_ADMIN FULL 会话登记目标角色的 UKey 绑定信息。") - @RequireRole("KEY_ADMIN") + @Operation(summary = "绑定角色 UKey", description = "仅允许 SUPER_ADMIN FULL 会话登记目标角色的 UKey 绑定信息。") + @RequireRole("SUPER_ADMIN") @RequireAuthLevel("FULL") public ApiResponse bindUkey( @PathVariable("roleCode") String roleCode, @@ -80,7 +79,7 @@ public class AuthAdminController { @PostMapping("/roles/{roleCode}/ukeys/issue-sign") @Operation(summary = "生成 UKey 发行签名", description = "按旧绑定流程为目标角色 UKey 材料生成发行签名。") -// @RequireRole("KEY_ADMIN") +// @RequireRole("SUPER_ADMIN") @RequireAuthLevel("FULL") public ApiResponse issueUkeyBindingSign( @PathVariable("roleCode") String roleCode, diff --git a/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java b/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java index 78628c5..6a28c3f 100644 --- a/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java +++ b/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java @@ -15,6 +15,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -23,15 +25,12 @@ import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/v1/auth") +@RequiredArgsConstructor @Tag(name = "认证鉴权", description = "管理端认证与当前会话接口") public class AuthController { private final AuthService authService; - public AuthController(AuthService authService) { - this.authService = authService; - } - @PostMapping("/password-login") @Operation(summary = "口令登录", description = "按旧系统受限登录流程签发 LIMITED 会话。") public ApiResponse passwordLogin(@Valid @RequestBody PasswordLoginRequest request) { diff --git a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthAdminServiceImpl.java b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthAdminServiceImpl.java index c1510c4..476f96a 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthAdminServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthAdminServiceImpl.java @@ -25,11 +25,11 @@ import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.List; import java.util.Objects; -import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service -@AllArgsConstructor +@RequiredArgsConstructor public class AuthAdminServiceImpl implements AuthAdminService { static final String DEFAULT_PASSWORD = "12345678"; diff --git a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthPolicyServiceImpl.java b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthPolicyServiceImpl.java index be3699d..759e7f5 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthPolicyServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthPolicyServiceImpl.java @@ -4,11 +4,11 @@ import com.cisd.tms.modules.auth.enums.AuthLevel; import com.cisd.tms.modules.auth.enums.AuthMethod; import com.cisd.tms.modules.auth.enums.RoleCode; import com.cisd.tms.modules.auth.service.AuthPolicyService; -import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service -@AllArgsConstructor +@RequiredArgsConstructor public class AuthPolicyServiceImpl implements AuthPolicyService { /** diff --git a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java index f57edbf..9f9beab 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/impl/AuthServiceImpl.java @@ -39,11 +39,11 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; -import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service -@AllArgsConstructor +@RequiredArgsConstructor public class AuthServiceImpl implements AuthService { private static final int MAX_FAILED_ATTEMPTS = 5; diff --git a/src/main/java/com/cisd/tms/modules/device/controller/CryptoCardController.java b/src/main/java/com/cisd/tms/modules/device/controller/CryptoCardController.java index 7b25299..54de185 100644 --- a/src/main/java/com/cisd/tms/modules/device/controller/CryptoCardController.java +++ b/src/main/java/com/cisd/tms/modules/device/controller/CryptoCardController.java @@ -41,6 +41,8 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.Min; import java.util.Base64; import java.util.Map; + +import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -52,15 +54,12 @@ import org.springframework.web.bind.annotation.RestController; @Validated @RestController @RequestMapping("/api/v1/device/crypto") +@RequiredArgsConstructor @Tag(name = "密码卡调试", description = "密码卡设备调试、密钥操作和文件管理接口") public class CryptoCardController { private final PcieCryptoService pcieCryptoService; - public CryptoCardController(PcieCryptoService pcieCryptoService) { - this.pcieCryptoService = pcieCryptoService; - } - @GetMapping("/device-count") @Operation(summary = "查询密码卡数量", description = "返回当前环境可见的密码卡设备数量。") public ApiResponse getDeviceCount() { diff --git a/src/main/java/com/cisd/tms/modules/device/controller/DeviceController.java b/src/main/java/com/cisd/tms/modules/device/controller/DeviceController.java index aaff2d3..3792b9c 100644 --- a/src/main/java/com/cisd/tms/modules/device/controller/DeviceController.java +++ b/src/main/java/com/cisd/tms/modules/device/controller/DeviceController.java @@ -13,6 +13,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -20,6 +21,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/v1/device") +@RequiredArgsConstructor @Tag(name = "设备概览", description = "设备基础信息、状态和重启接口") public class DeviceController { @@ -27,16 +29,6 @@ public class DeviceController { private final DeviceProfileService deviceProfileService; private final DeviceRuntimeStatusService deviceRuntimeStatusService; - public DeviceController( - DeviceService deviceService, - DeviceProfileService deviceProfileService, - DeviceRuntimeStatusService deviceRuntimeStatusService - ) { - this.deviceService = deviceService; - this.deviceProfileService = deviceProfileService; - this.deviceRuntimeStatusService = deviceRuntimeStatusService; - } - @GetMapping("/info") @Operation(summary = "查询设备信息", description = "返回设备型号、设备名称、TMS版本、设备序列号、管理密钥状态和当前设备初始化状态。") public ApiResponse info() { diff --git a/src/main/java/com/cisd/tms/modules/device/controller/TimeConfigController.java b/src/main/java/com/cisd/tms/modules/device/controller/TimeConfigController.java index 6f48707..0d546f6 100644 --- a/src/main/java/com/cisd/tms/modules/device/controller/TimeConfigController.java +++ b/src/main/java/com/cisd/tms/modules/device/controller/TimeConfigController.java @@ -6,6 +6,7 @@ import com.cisd.tms.common.enums.ErrorCode; import com.cisd.tms.modules.device.dto.TimeConfigRequest; import com.cisd.tms.modules.device.service.TimeConfigService; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -13,16 +14,12 @@ import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/v1/device") +@RequiredArgsConstructor @Tag(name = "device-time-config-controller") public class TimeConfigController { private final TimeConfigService timeConfigService; - public TimeConfigController(TimeConfigService timeConfigService) { - this.timeConfigService = timeConfigService; - } - - @PostMapping("/time-config") public ApiResponse configureTime(@RequestBody TimeConfigRequest request) { //todo 鉴权 diff --git a/src/main/java/com/cisd/tms/modules/device/service/DeviceProfileService.java b/src/main/java/com/cisd/tms/modules/device/service/DeviceProfileService.java index ec33a92..ff809da 100644 --- a/src/main/java/com/cisd/tms/modules/device/service/DeviceProfileService.java +++ b/src/main/java/com/cisd/tms/modules/device/service/DeviceProfileService.java @@ -1,178 +1,8 @@ package com.cisd.tms.modules.device.service; -import com.cisd.tms.common.config.properties.CisdPresetProperties; -import com.cisd.tms.common.exception.BizException; -import com.cisd.tms.integration.crypto.pcie.model.DeviceInfoResult; -import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; -import com.cisd.tms.modules.device.config.DeviceProfileProperties; -import com.cisd.tms.modules.device.dto.DeviceProfileDeviceSection; -import com.cisd.tms.modules.device.dto.DeviceProfileHardwareSection; import com.cisd.tms.modules.device.dto.DeviceProfileResponse; -import com.cisd.tms.modules.device.dto.DeviceProfileSoftwareItem; -import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; -import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository; -import com.cisd.tms.modules.device.support.SystemHardwareProbe; -import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe; -import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse; -import com.cisd.tms.modules.init.service.InitService; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import org.springframework.stereotype.Service; -@Service -public class DeviceProfileService { +public interface DeviceProfileService { - private final InitService initService; - private final PcieCryptoService pcieCryptoService; - private final DeviceProfileProperties deviceProfileProperties; - private final SystemHardwareProbe systemHardwareProbe; - private final SystemSoftwareVersionProbe systemSoftwareVersionProbe; - private final DeviceSoftwareVersionRepository deviceSoftwareVersionRepository; - private final CisdPresetProperties cisdPresetProperties; - - public DeviceProfileService( - InitService initService, - PcieCryptoService pcieCryptoService, - DeviceProfileProperties deviceProfileProperties, - SystemHardwareProbe systemHardwareProbe, - SystemSoftwareVersionProbe systemSoftwareVersionProbe, - DeviceSoftwareVersionRepository deviceSoftwareVersionRepository, - CisdPresetProperties cisdPresetProperties - ) { - this.initService = initService; - this.pcieCryptoService = pcieCryptoService; - this.deviceProfileProperties = deviceProfileProperties; - this.systemHardwareProbe = systemHardwareProbe; - this.systemSoftwareVersionProbe = systemSoftwareVersionProbe; - this.deviceSoftwareVersionRepository = deviceSoftwareVersionRepository; - this.cisdPresetProperties = cisdPresetProperties; - } - - public DeviceProfileResponse profile() { - DeviceInfoResult deviceInfo = safeDeviceInfo(); - DeviceProfileResponse response = new DeviceProfileResponse(); - response.setDevice(buildDeviceSection(deviceInfo)); - response.setHardware(buildHardwareSection(deviceInfo)); - response.setSoftware(buildSoftwareItems()); - return response; - } - - private DeviceProfileDeviceSection buildDeviceSection(DeviceInfoResult info) { - DeviceProfileDeviceSection section = new DeviceProfileDeviceSection(); - section.setModel(deviceProfileProperties.getModel()); - section.setName(deviceProfileProperties.getName()); - section.setSerialNumber(trim(info.getDeviceSerial())); - section.setDeviceVersion(formatVersion(info.getStandardVersion())); - return section; - } - - private DeviceProfileHardwareSection buildHardwareSection(DeviceInfoResult info) { - DeviceProfileHardwareSection section = new DeviceProfileHardwareSection(); - section.setCryptoCardName(trim(info.getDeviceName())); - section.setCpuModel(firstNonBlank(systemHardwareProbe.cpuModel(), deviceProfileProperties.getCpuModelDefault())); - section.setMemoryTotal(firstNonBlank(systemHardwareProbe.memoryTotal(), deviceProfileProperties.getMemoryTotalDefault())); - section.setDiskTotal(firstNonBlank(systemHardwareProbe.diskTotal(), deviceProfileProperties.getDiskTotalDefault())); - return section; - } - - private List buildSoftwareItems() { - List items = new ArrayList<>(); - addSoftwareItem(items, "OS", "操作系统", firstNonBlank(systemSoftwareVersionProbe.osVersion(), cached("OS"))); - addSoftwareItem(items, "TMS", "设备管理软件", firstNonBlank(cached("TMS"), systemSoftwareVersionProbe.tmsVersion(), cisdPresetProperties.getVersion())); - - CurrentInitConfigResponse currentConfig = loadCurrentInitConfigIfPresent(); - if (currentConfig == null) { - return items; - } - - String productType = trim(currentConfig.getProductType()); - String mqType = trim(currentConfig.getMqType()); - if ("RABBITMQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType)) { - addSoftwareItem(items, "RABBITMQ", "消息队列", cached("RABBITMQ")); - } - if ("TLQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType)) { - addSoftwareItem(items, "TLQ", "消息队列", cached("TLQ")); - } - if ("ENTERPRISE".equals(productType) || "INDIRECT".equals(productType)) { - addSoftwareItem(items, "TIDB", "数据库", cached("TIDB")); - addSoftwareItem(items, "NGINX", "Nginx", cached("NGINX")); - addSoftwareItem(items, "APP", appName(productType), firstNonBlank(cached("APP"), cisdPresetProperties.getVersion())); - } else if ("DIRECT".equals(productType)) { - addSoftwareItem(items, "APP", appName(productType), firstNonBlank(cached("APP"), cisdPresetProperties.getVersion())); - } - return items; - } - - private CurrentInitConfigResponse loadCurrentInitConfigIfPresent() { - try { - if (!"INITIALIZED".equals(initService.getDeviceInitStateSnapshot().initState())) { - return null; - } - return initService.loadCurrentInitConfig(); - } catch (BizException ex) { - return null; - } - } - - private DeviceInfoResult safeDeviceInfo() { - try { - DeviceInfoResult result = pcieCryptoService.getDeviceInfo(); - return result == null ? new DeviceInfoResult() : result; - } catch (RuntimeException ex) { - return new DeviceInfoResult(); - } - } - - private void addSoftwareItem(List items, String code, String name, String version) { - DeviceProfileSoftwareItem item = new DeviceProfileSoftwareItem(); - item.setComponentCode(code); - item.setComponentName(name); - item.setVersion(trim(version)); - items.add(item); - } - - private String cached(String componentCode) { - Optional cached = deviceSoftwareVersionRepository.findByComponentCode(componentCode); - return cached.map(DeviceSoftwareVersionEntity::getCurrentVersion).orElse(""); - } - - private String appName(String productType) { - return switch (productType) { - case "ENTERPRISE" -> "标准收发器企业版"; - case "INDIRECT" -> "标准收发器间参版"; - case "DIRECT" -> "直参轻量化终端版"; - default -> "应用软件"; - }; - } - - private String formatVersion(int version) { - if (version <= 0) { - return ""; - } - if (version >= 100) { - return "V" + (version / 100) + "." + String.format("%02d", version % 100); - } - return "V" + version; - } - - private static String firstNonBlank(String... values) { - if (values == null) { - return ""; - } - for (String value : values) { - if (!isBlank(value)) { - return trim(value); - } - } - return ""; - } - - private static String trim(String value) { - return value == null ? "" : value.trim(); - } - - private static boolean isBlank(String value) { - return value == null || value.isBlank(); - } + DeviceProfileResponse profile(); } diff --git a/src/main/java/com/cisd/tms/modules/device/service/DeviceRuntimeStatusService.java b/src/main/java/com/cisd/tms/modules/device/service/DeviceRuntimeStatusService.java index 6064248..c131737 100644 --- a/src/main/java/com/cisd/tms/modules/device/service/DeviceRuntimeStatusService.java +++ b/src/main/java/com/cisd/tms/modules/device/service/DeviceRuntimeStatusService.java @@ -1,239 +1,8 @@ package com.cisd.tms.modules.device.service; -import com.cisd.tms.common.config.properties.CisdPresetProperties; -import com.cisd.tms.common.exception.BizException; -import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; -import com.cisd.tms.modules.device.dto.CryptoCardRuntimeStatus; import com.cisd.tms.modules.device.dto.DeviceRuntimeStatusResponse; -import com.cisd.tms.modules.device.dto.HardwareRuntimeStatusSection; -import com.cisd.tms.modules.device.dto.SoftwareRuntimeStatusItem; -import com.cisd.tms.modules.device.dto.UsageMetricStatus; -import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; -import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository; -import com.cisd.tms.modules.device.support.HardwareStatusProbe; -import com.cisd.tms.modules.device.support.SoftwareRuntimeProbe; -import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe; -import com.cisd.tms.modules.device.support.UsageSnapshot; -import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse; -import com.cisd.tms.modules.init.service.InitService; -import java.time.OffsetDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.function.BooleanSupplier; -import org.springframework.stereotype.Service; -@Service -public class DeviceRuntimeStatusService { +public interface DeviceRuntimeStatusService { - private static final String STATUS_NORMAL = "NORMAL"; - private static final String STATUS_ABNORMAL = "ABNORMAL"; - private static final String STATUS_NOT_INSTALLED = "NOT_INSTALLED"; - private static final String STATUS_UNKNOWN = "UNKNOWN"; - - private final InitService initService; - private final PcieCryptoService pcieCryptoService; - private final HardwareStatusProbe hardwareStatusProbe; - private final SystemSoftwareVersionProbe systemSoftwareVersionProbe; - private final SoftwareRuntimeProbe softwareRuntimeProbe; - private final DeviceSoftwareVersionRepository deviceSoftwareVersionRepository; - private final CisdPresetProperties cisdPresetProperties; - - public DeviceRuntimeStatusService( - InitService initService, - PcieCryptoService pcieCryptoService, - HardwareStatusProbe hardwareStatusProbe, - SystemSoftwareVersionProbe systemSoftwareVersionProbe, - SoftwareRuntimeProbe softwareRuntimeProbe, - DeviceSoftwareVersionRepository deviceSoftwareVersionRepository, - CisdPresetProperties cisdPresetProperties - ) { - this.initService = initService; - this.pcieCryptoService = pcieCryptoService; - this.hardwareStatusProbe = hardwareStatusProbe; - this.systemSoftwareVersionProbe = systemSoftwareVersionProbe; - this.softwareRuntimeProbe = softwareRuntimeProbe; - this.deviceSoftwareVersionRepository = deviceSoftwareVersionRepository; - this.cisdPresetProperties = cisdPresetProperties; - } - - public DeviceRuntimeStatusResponse loadRuntimeStatus() { - DeviceRuntimeStatusResponse response = new DeviceRuntimeStatusResponse(); - response.setHardware(buildHardwareStatus()); - response.setSoftware(buildSoftwareStatuses()); - response.setCollectedAt(OffsetDateTime.now().toString()); - return response; - } - - private HardwareRuntimeStatusSection buildHardwareStatus() { - HardwareRuntimeStatusSection section = new HardwareRuntimeStatusSection(); - section.setCpu(buildCpuStatus()); - section.setMemory(buildUsageStatus(() -> hardwareStatusProbe.memoryUsage())); - section.setDisk(buildUsageStatus(() -> hardwareStatusProbe.diskUsage())); - section.setCryptoCard(buildCryptoCardStatus()); - return section; - } - - private UsageMetricStatus buildCpuStatus() { - UsageMetricStatus status = new UsageMetricStatus(); - try { - Double usage = hardwareStatusProbe.cpuUsagePercent(); - status.setUsagePercent(round(usage)); - status.setStatus(STATUS_NORMAL); - status.setMessage("正常"); - } catch (RuntimeException ex) { - status.setStatus(STATUS_UNKNOWN); - status.setMessage("未知"); - } - return status; - } - - private UsageMetricStatus buildUsageStatus(UsageSupplier supplier) { - UsageMetricStatus status = new UsageMetricStatus(); - try { - UsageSnapshot snapshot = supplier.get(); - status.setUsedGb(round(snapshot.getUsedGb())); - status.setTotalGb(round(snapshot.getTotalGb())); - status.setUsagePercent(round(snapshot.usagePercent())); - status.setStatus(STATUS_NORMAL); - status.setMessage("正常"); - } catch (RuntimeException ex) { - status.setStatus(STATUS_UNKNOWN); - status.setMessage("未知"); - } - return status; - } - - private CryptoCardRuntimeStatus buildCryptoCardStatus() { - CryptoCardRuntimeStatus status = new CryptoCardRuntimeStatus(); - try { - pcieCryptoService.getDeviceInfo(); - status.setStatus(STATUS_NORMAL); - status.setMessage("就绪状态"); - } catch (RuntimeException ex) { - status.setStatus(STATUS_ABNORMAL); - status.setMessage("异常"); - } - return status; - } - - private List buildSoftwareStatuses() { - CurrentInitConfigResponse config = loadCurrentInitConfigIfPresent(); - String productType = config == null ? normalize(cisdPresetProperties.getProductType()) : normalize(config.getProductType()); - String mqType = config == null ? "" : normalize(config.getMqType()); - - List items = new ArrayList<>(); - items.add(buildSoftwareItem("RABBITMQ", "消息队列", cached("RABBITMQ"), isRabbitmqEnabled(mqType), softwareRuntimeProbe::isRabbitmqRunning)); - items.add(buildSoftwareItem("TLQ", "消息队列", cached("TLQ"), isTlqEnabled(mqType), softwareRuntimeProbe::isTlqRunning)); - items.add(buildSoftwareItem("TIDB", "数据库", cached("TIDB"), isTidbEnabled(productType), softwareRuntimeProbe::isTidbRunning)); - items.add(buildSoftwareItem("NGINX", "Nginx", cached("NGINX"), isNginxEnabled(productType), softwareRuntimeProbe::isNginxRunning)); - items.add(buildAppItem(productType, config != null)); - items.add(buildSoftwareItem("TMS", "设备管理软件", firstNonBlank(cached("TMS"), systemSoftwareVersionProbe.tmsVersion(), cisdPresetProperties.getVersion()), true, softwareRuntimeProbe::isTmsRunning)); - return items; - } - - private SoftwareRuntimeStatusItem buildAppItem(String productType, boolean initialized) { - boolean standard = "ENTERPRISE".equals(productType) || "INDIRECT".equals(productType); - BooleanSupplier supplier = standard ? softwareRuntimeProbe::isStandardAppRunning : softwareRuntimeProbe::isDirectAppRunning; - String name = "应用软件"; - String displayName = firstNonBlank(cached("APP"), appName(productType)); - return buildSoftwareItem("APP", name, displayName, initialized, supplier); - } - - private SoftwareRuntimeStatusItem buildSoftwareItem( - String code, - String name, - String displayName, - boolean enabled, - BooleanSupplier probe - ) { - SoftwareRuntimeStatusItem item = new SoftwareRuntimeStatusItem(); - item.setComponentCode(code); - item.setComponentName(name); - item.setDisplayName(firstNonBlank(displayName, name)); - if (!enabled) { - item.setStatus(STATUS_NOT_INSTALLED); - item.setMessage("未安装"); - return item; - } - try { - if (probe.getAsBoolean()) { - item.setStatus(STATUS_NORMAL); - item.setMessage("正常"); - } else { - item.setStatus(STATUS_ABNORMAL); - item.setMessage("异常"); - } - } catch (RuntimeException ex) { - item.setStatus(STATUS_UNKNOWN); - item.setMessage("未知"); - } - return item; - } - - private CurrentInitConfigResponse loadCurrentInitConfigIfPresent() { - try { - if (!"INITIALIZED".equals(initService.getDeviceInitStateSnapshot().initState())) { - return null; - } - return initService.loadCurrentInitConfig(); - } catch (BizException ex) { - return null; - } - } - - private boolean isRabbitmqEnabled(String mqType) { - return "RABBITMQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType) || "RABBITMQ_CFMQ".equals(mqType); - } - - private boolean isTlqEnabled(String mqType) { - return "TLQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType); - } - - private boolean isTidbEnabled(String productType) { - return "ENTERPRISE".equals(productType) || "INDIRECT".equals(productType); - } - - private boolean isNginxEnabled(String productType) { - return "ENTERPRISE".equals(productType) || "INDIRECT".equals(productType); - } - - private String cached(String componentCode) { - Optional entity = deviceSoftwareVersionRepository.findByComponentCode(componentCode); - return entity.map(DeviceSoftwareVersionEntity::getCurrentVersion).orElse(""); - } - - private String appName(String productType) { - return switch (productType) { - case "ENTERPRISE" -> "标准收发器企业版"; - case "INDIRECT" -> "标准收发器间参版"; - case "DIRECT" -> "直参轻量化终端版"; - default -> "应用软件"; - }; - } - - private String normalize(String value) { - return value == null ? "" : value.trim().toUpperCase(); - } - - private double round(double value) { - return Math.round(value * 100D) / 100D; - } - - private String firstNonBlank(String... values) { - if (values == null) { - return ""; - } - for (String value : values) { - if (value != null && !value.isBlank()) { - return value.trim(); - } - } - return ""; - } - - @FunctionalInterface - private interface UsageSupplier { - UsageSnapshot get(); - } + DeviceRuntimeStatusResponse loadRuntimeStatus(); } diff --git a/src/main/java/com/cisd/tms/modules/device/service/DeviceService.java b/src/main/java/com/cisd/tms/modules/device/service/DeviceService.java index 279cdd8..d9af263 100644 --- a/src/main/java/com/cisd/tms/modules/device/service/DeviceService.java +++ b/src/main/java/com/cisd/tms/modules/device/service/DeviceService.java @@ -1,83 +1,11 @@ package com.cisd.tms.modules.device.service; -import com.cisd.tms.integration.crypto.pcie.model.DeviceInfoResult; -import com.cisd.tms.integration.crypto.pcie.model.DeviceStatusResult; -import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; -import com.cisd.tms.modules.device.config.DeviceProfileProperties; import com.cisd.tms.modules.device.dto.DeviceActionResponse; import com.cisd.tms.modules.device.dto.DeviceInfoResponse; -import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; -import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository; -import com.cisd.tms.modules.init.service.InitService; -import java.time.OffsetDateTime; -import java.util.Optional; -import org.springframework.stereotype.Service; -@Service -public class DeviceService { +public interface DeviceService { - private final DeviceProfileProperties deviceProfileProperties; - private final DeviceSoftwareVersionRepository deviceSoftwareVersionRepository; - private final PcieCryptoService pcieCryptoService; - private final InitService initService; + DeviceInfoResponse info(); - public DeviceService( - DeviceProfileProperties deviceProfileProperties, - DeviceSoftwareVersionRepository deviceSoftwareVersionRepository, - PcieCryptoService pcieCryptoService, - InitService initService - ) { - this.deviceProfileProperties = deviceProfileProperties; - this.deviceSoftwareVersionRepository = deviceSoftwareVersionRepository; - this.pcieCryptoService = pcieCryptoService; - this.initService = initService; - } - - public DeviceInfoResponse info() { - DeviceInfoResponse response = new DeviceInfoResponse(); - response.setDeviceModel(trim(deviceProfileProperties.getModel())); - response.setDeviceName(trim(deviceProfileProperties.getName())); - response.setVersion(loadTmsVersion()); - response.setSerialNumber(loadDeviceSerial()); - response.setMasterKeyStatus(loadManagementKeyReady()); - InitService.DeviceInitStateSnapshot snapshot = initService.getDeviceInitStateSnapshot(); - response.setDeviceStatus(snapshot.initState()); - return response; - } - - public DeviceActionResponse restart(String operator) { - DeviceActionResponse response = new DeviceActionResponse(); - response.setAction("RESTART"); - response.setAccepted(true); - response.setOperator(operator == null || operator.isBlank() ? "unknown" : operator); - response.setAcceptedAt(OffsetDateTime.now().toString()); - return response; - } - - private String loadTmsVersion() { - Optional version = deviceSoftwareVersionRepository.findByComponentCode("TMS"); - return version.map(DeviceSoftwareVersionEntity::getCurrentVersion).map(DeviceService::trim).orElse(""); - } - - private String loadDeviceSerial() { - try { - DeviceInfoResult result = pcieCryptoService.getDeviceInfo(); - return result == null ? "" : trim(result.getDeviceSerial()); - } catch (RuntimeException ex) { - return ""; - } - } - - private boolean loadManagementKeyReady() { - try { - DeviceStatusResult result = pcieCryptoService.getDeviceStatus(); - return result != null && result.getFsmState() == 0; - } catch (RuntimeException ex) { - return false; - } - } - - private static String trim(String value) { - return value == null ? "" : value.trim(); - } + DeviceActionResponse restart(String operator); } diff --git a/src/main/java/com/cisd/tms/modules/device/service/TimeConfigService.java b/src/main/java/com/cisd/tms/modules/device/service/TimeConfigService.java index 3ab3170..dacfdcc 100644 --- a/src/main/java/com/cisd/tms/modules/device/service/TimeConfigService.java +++ b/src/main/java/com/cisd/tms/modules/device/service/TimeConfigService.java @@ -1,112 +1,8 @@ package com.cisd.tms.modules.device.service; import com.cisd.tms.modules.device.dto.TimeConfigRequest; -import org.springframework.stereotype.Service; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.List; - -@Service -public class TimeConfigService { - - - public void processTimeConfig(TimeConfigRequest request) { - String mode = request.getMode(); - - if ("MANUAL".equalsIgnoreCase(mode)) { - - String timezone = request.getTimezone(); - if (timezone == null || !timezone.matches("^[A-Za-z0-9/_-]+$")) { - throw new IllegalArgumentException("Invalid or empty timezone format"); - } - - String datetime = request.getDatetime(); - // 假设格式必须为 YYYY-MM-DD HH:MM - if (datetime == null || !datetime.matches("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}$")) { - throw new IllegalArgumentException("Invalid or empty datetime format. Expected: YYYY-MM-DD HH:MM"); - } - - executeCommand("timedatectl set-timezone " + request.getTimezone().trim()); - executeCommand("systemctl stop chronyd"); - executeCommand("date -s '" + request.getDatetime() + "'"); - - } else if ("NTP".equalsIgnoreCase(mode)) { - - List servers = request.getNtpServers(); - if (servers == null || servers.isEmpty()) { - throw new IllegalArgumentException("At least one NTP server IP must be provided in NTP mode"); - } - - - int interval = request.getSyncInterval() != null ? request.getSyncInterval() : 600; - int pollExp = (int) Math.round(Math.log(interval) / Math.log(2)); - StringBuilder serverConfLines = new StringBuilder(); - for (String server : servers) { - if (!server.matches("^[A-Za-z0-9.-]+$")) { - throw new IllegalArgumentException("Invalid NTP server address format: " + server); - } - - String pingResult = executeCommand("ping -c 1 -W 1 " + server); - if (!pingResult.contains("1 received")) { - throw new RuntimeException("NTP server is unreachable via ping: " + server); - } - serverConfLines.append("server ").append(server) - .append(" iburst minpoll ").append(pollExp) - .append(" maxpoll ").append(pollExp).append("\\n"); - } - String confContent = serverConfLines + - "driftfile /var/lib/chrony/drift\\n" + - "makestep 1.0 3\\n" + - "rtcsync\\n"; - executeCommand("echo -e '" + confContent + "' > /etc/chrony.conf"); - executeCommand("systemctl restart chronyd"); - executeCommand("chronyc -a makestep"); - String status = executeCommand("chronyc tracking"); - if (!status.contains("Reference ID")) { - throw new RuntimeException("NTP sync status check failed"); - } - - } else { - throw new IllegalArgumentException("Unknown mode: " + mode); - } - - } - - - private String executeCommand(String command){ - ProcessBuilder processBuilder = new ProcessBuilder("bash", "-c", command); - processBuilder.redirectErrorStream(true); - - Process process = null; - try { - process = processBuilder.start(); - - StringBuilder output = new StringBuilder(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) { - output.append(line).append("\n"); - } - } - - - int exitCode = process.waitFor(); - if (exitCode != 0) { - throw new RuntimeException("command execution failed: [" + command + "] "); - } - return output.toString(); - - } catch (IOException e){ - throw new RuntimeException("I/O error executing command: [" + command + "]", e); - } catch (InterruptedException e) { - if (process != null) { - process.destroy(); - } - Thread.currentThread().interrupt(); - throw new RuntimeException("process was interrupted while executing command: [" + command + "]", e); - } - } +public interface TimeConfigService { + void processTimeConfig(TimeConfigRequest request); } diff --git a/src/main/java/com/cisd/tms/modules/device/service/impl/DeviceProfileServiceImpl.java b/src/main/java/com/cisd/tms/modules/device/service/impl/DeviceProfileServiceImpl.java new file mode 100644 index 0000000..a26bae3 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/service/impl/DeviceProfileServiceImpl.java @@ -0,0 +1,164 @@ +package com.cisd.tms.modules.device.service.impl; + +import com.cisd.tms.common.config.properties.CisdPresetProperties; +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.integration.crypto.pcie.model.DeviceInfoResult; +import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; +import com.cisd.tms.modules.device.config.DeviceProfileProperties; +import com.cisd.tms.modules.device.dto.DeviceProfileDeviceSection; +import com.cisd.tms.modules.device.dto.DeviceProfileHardwareSection; +import com.cisd.tms.modules.device.dto.DeviceProfileResponse; +import com.cisd.tms.modules.device.dto.DeviceProfileSoftwareItem; +import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; +import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository; +import com.cisd.tms.modules.device.service.DeviceProfileService; +import com.cisd.tms.modules.device.support.SystemHardwareProbe; +import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe; +import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse; +import com.cisd.tms.modules.init.service.InitService; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DeviceProfileServiceImpl implements DeviceProfileService { + + private final InitService initService; + private final PcieCryptoService pcieCryptoService; + private final DeviceProfileProperties deviceProfileProperties; + private final SystemHardwareProbe systemHardwareProbe; + private final SystemSoftwareVersionProbe systemSoftwareVersionProbe; + private final DeviceSoftwareVersionRepository deviceSoftwareVersionRepository; + private final CisdPresetProperties cisdPresetProperties; + + @Override + public DeviceProfileResponse profile() { + DeviceInfoResult deviceInfo = safeDeviceInfo(); + DeviceProfileResponse response = new DeviceProfileResponse(); + response.setDevice(buildDeviceSection(deviceInfo)); + response.setHardware(buildHardwareSection(deviceInfo)); + response.setSoftware(buildSoftwareItems()); + return response; + } + + private DeviceProfileDeviceSection buildDeviceSection(DeviceInfoResult info) { + DeviceProfileDeviceSection section = new DeviceProfileDeviceSection(); + section.setModel(deviceProfileProperties.getModel()); + section.setName(deviceProfileProperties.getName()); + section.setSerialNumber(trim(info.getDeviceSerial())); + section.setDeviceVersion(formatVersion(info.getStandardVersion())); + return section; + } + + private DeviceProfileHardwareSection buildHardwareSection(DeviceInfoResult info) { + DeviceProfileHardwareSection section = new DeviceProfileHardwareSection(); + section.setCryptoCardName(trim(info.getDeviceName())); + section.setCpuModel(firstNonBlank(systemHardwareProbe.cpuModel(), deviceProfileProperties.getCpuModelDefault())); + section.setMemoryTotal(firstNonBlank(systemHardwareProbe.memoryTotal(), deviceProfileProperties.getMemoryTotalDefault())); + section.setDiskTotal(firstNonBlank(systemHardwareProbe.diskTotal(), deviceProfileProperties.getDiskTotalDefault())); + return section; + } + + private List buildSoftwareItems() { + List items = new ArrayList<>(); + addSoftwareItem(items, "OS", "操作系统", firstNonBlank(systemSoftwareVersionProbe.osVersion(), cached("OS"))); + addSoftwareItem(items, "TMS", "设备管理软件", firstNonBlank(cached("TMS"), systemSoftwareVersionProbe.tmsVersion(), cisdPresetProperties.getVersion())); + + CurrentInitConfigResponse currentConfig = loadCurrentInitConfigIfPresent(); + if (currentConfig == null) { + return items; + } + + String productType = trim(currentConfig.getProductType()); + String mqType = trim(currentConfig.getMqType()); + if ("RABBITMQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType)) { + addSoftwareItem(items, "RABBITMQ", "消息队列", cached("RABBITMQ")); + } + if ("TLQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType)) { + addSoftwareItem(items, "TLQ", "消息队列", cached("TLQ")); + } + if ("ENTERPRISE".equals(productType) || "INDIRECT".equals(productType)) { + addSoftwareItem(items, "TIDB", "数据库", cached("TIDB")); + addSoftwareItem(items, "NGINX", "Nginx", cached("NGINX")); + addSoftwareItem(items, "APP", appName(productType), firstNonBlank(cached("APP"), cisdPresetProperties.getVersion())); + } else if ("DIRECT".equals(productType)) { + addSoftwareItem(items, "APP", appName(productType), firstNonBlank(cached("APP"), cisdPresetProperties.getVersion())); + } + return items; + } + + private CurrentInitConfigResponse loadCurrentInitConfigIfPresent() { + try { + if (!"INITIALIZED".equals(initService.getDeviceInitStateSnapshot().initState())) { + return null; + } + return initService.loadCurrentInitConfig(); + } catch (BizException ex) { + return null; + } + } + + private DeviceInfoResult safeDeviceInfo() { + try { + DeviceInfoResult result = pcieCryptoService.getDeviceInfo(); + return result == null ? new DeviceInfoResult() : result; + } catch (RuntimeException ex) { + return new DeviceInfoResult(); + } + } + + private void addSoftwareItem(List items, String code, String name, String version) { + DeviceProfileSoftwareItem item = new DeviceProfileSoftwareItem(); + item.setComponentCode(code); + item.setComponentName(name); + item.setVersion(trim(version)); + items.add(item); + } + + private String cached(String componentCode) { + Optional cached = deviceSoftwareVersionRepository.findByComponentCode(componentCode); + return cached.map(DeviceSoftwareVersionEntity::getCurrentVersion).orElse(""); + } + + private String appName(String productType) { + return switch (productType) { + case "ENTERPRISE" -> "标准收发器企业版"; + case "INDIRECT" -> "标准收发器间参版"; + case "DIRECT" -> "直参轻量化终端版"; + default -> "应用软件"; + }; + } + + private String formatVersion(int version) { + if (version <= 0) { + return ""; + } + if (version >= 100) { + return "V" + (version / 100) + "." + String.format("%02d", version % 100); + } + return "V" + version; + } + + private static String firstNonBlank(String... values) { + if (values == null) { + return ""; + } + for (String value : values) { + if (!isBlank(value)) { + return trim(value); + } + } + return ""; + } + + private static String trim(String value) { + return value == null ? "" : value.trim(); + } + + private static boolean isBlank(String value) { + return value == null || value.isBlank(); + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/service/impl/DeviceRuntimeStatusServiceImpl.java b/src/main/java/com/cisd/tms/modules/device/service/impl/DeviceRuntimeStatusServiceImpl.java new file mode 100644 index 0000000..7164abf --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/service/impl/DeviceRuntimeStatusServiceImpl.java @@ -0,0 +1,226 @@ +package com.cisd.tms.modules.device.service.impl; + +import com.cisd.tms.common.config.properties.CisdPresetProperties; +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; +import com.cisd.tms.modules.device.dto.CryptoCardRuntimeStatus; +import com.cisd.tms.modules.device.dto.DeviceRuntimeStatusResponse; +import com.cisd.tms.modules.device.dto.HardwareRuntimeStatusSection; +import com.cisd.tms.modules.device.dto.SoftwareRuntimeStatusItem; +import com.cisd.tms.modules.device.dto.UsageMetricStatus; +import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; +import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository; +import com.cisd.tms.modules.device.service.DeviceRuntimeStatusService; +import com.cisd.tms.modules.device.support.HardwareStatusProbe; +import com.cisd.tms.modules.device.support.SoftwareRuntimeProbe; +import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe; +import com.cisd.tms.modules.device.support.UsageSnapshot; +import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse; +import com.cisd.tms.modules.init.service.InitService; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.BooleanSupplier; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DeviceRuntimeStatusServiceImpl implements DeviceRuntimeStatusService { + + private static final String STATUS_NORMAL = "NORMAL"; + private static final String STATUS_ABNORMAL = "ABNORMAL"; + private static final String STATUS_NOT_INSTALLED = "NOT_INSTALLED"; + private static final String STATUS_UNKNOWN = "UNKNOWN"; + + private final InitService initService; + private final PcieCryptoService pcieCryptoService; + private final HardwareStatusProbe hardwareStatusProbe; + private final SystemSoftwareVersionProbe systemSoftwareVersionProbe; + private final SoftwareRuntimeProbe softwareRuntimeProbe; + private final DeviceSoftwareVersionRepository deviceSoftwareVersionRepository; + private final CisdPresetProperties cisdPresetProperties; + + @Override + public DeviceRuntimeStatusResponse loadRuntimeStatus() { + DeviceRuntimeStatusResponse response = new DeviceRuntimeStatusResponse(); + response.setHardware(buildHardwareStatus()); + response.setSoftware(buildSoftwareStatuses()); + response.setCollectedAt(OffsetDateTime.now().toString()); + return response; + } + + private HardwareRuntimeStatusSection buildHardwareStatus() { + HardwareRuntimeStatusSection section = new HardwareRuntimeStatusSection(); + section.setCpu(buildCpuStatus()); + section.setMemory(buildUsageStatus(() -> hardwareStatusProbe.memoryUsage())); + section.setDisk(buildUsageStatus(() -> hardwareStatusProbe.diskUsage())); + section.setCryptoCard(buildCryptoCardStatus()); + return section; + } + + private UsageMetricStatus buildCpuStatus() { + UsageMetricStatus status = new UsageMetricStatus(); + try { + Double usage = hardwareStatusProbe.cpuUsagePercent(); + status.setUsagePercent(round(usage)); + status.setStatus(STATUS_NORMAL); + status.setMessage("正常"); + } catch (RuntimeException ex) { + status.setStatus(STATUS_UNKNOWN); + status.setMessage("未知"); + } + return status; + } + + private UsageMetricStatus buildUsageStatus(UsageSupplier supplier) { + UsageMetricStatus status = new UsageMetricStatus(); + try { + UsageSnapshot snapshot = supplier.get(); + status.setUsedGb(round(snapshot.getUsedGb())); + status.setTotalGb(round(snapshot.getTotalGb())); + status.setUsagePercent(round(snapshot.usagePercent())); + status.setStatus(STATUS_NORMAL); + status.setMessage("正常"); + } catch (RuntimeException ex) { + status.setStatus(STATUS_UNKNOWN); + status.setMessage("未知"); + } + return status; + } + + private CryptoCardRuntimeStatus buildCryptoCardStatus() { + CryptoCardRuntimeStatus status = new CryptoCardRuntimeStatus(); + try { + pcieCryptoService.getDeviceInfo(); + status.setStatus(STATUS_NORMAL); + status.setMessage("就绪状态"); + } catch (RuntimeException ex) { + status.setStatus(STATUS_ABNORMAL); + status.setMessage("异常"); + } + return status; + } + + private List buildSoftwareStatuses() { + CurrentInitConfigResponse config = loadCurrentInitConfigIfPresent(); + boolean initialized = config != null; + String productType = config == null ? normalize(cisdPresetProperties.getProductType()) : normalize(config.getProductType()); + String mqType = config == null ? "" : normalize(config.getMqType()); + + List items = new ArrayList<>(); + items.add(buildSoftwareItem("RABBITMQ", "消息队列", cached("RABBITMQ"), initialized && isRabbitmqEnabled(mqType), softwareRuntimeProbe::isRabbitmqRunning)); + items.add(buildSoftwareItem("TLQ", "消息队列", cached("TLQ"), initialized && isTlqEnabled(mqType), softwareRuntimeProbe::isTlqRunning)); + items.add(buildSoftwareItem("TIDB", "数据库", cached("TIDB"), initialized && isTidbEnabled(productType), softwareRuntimeProbe::isTidbRunning)); + items.add(buildSoftwareItem("NGINX", "Nginx", cached("NGINX"), initialized && isNginxEnabled(productType), softwareRuntimeProbe::isNginxRunning)); + items.add(buildAppItem(productType, initialized)); + items.add(buildSoftwareItem("TMS", "设备管理软件", firstNonBlank(cached("TMS"), systemSoftwareVersionProbe.tmsVersion(), cisdPresetProperties.getVersion()), true, softwareRuntimeProbe::isTmsRunning)); + return items; + } + + private SoftwareRuntimeStatusItem buildAppItem(String productType, boolean initialized) { + boolean standard = "ENTERPRISE".equals(productType) || "INDIRECT".equals(productType); + BooleanSupplier supplier = standard ? softwareRuntimeProbe::isStandardAppRunning : softwareRuntimeProbe::isDirectAppRunning; + String name = "应用软件"; + String displayName = firstNonBlank(cached("APP"), appName(productType)); + return buildSoftwareItem("APP", name, displayName, initialized, supplier); + } + + private SoftwareRuntimeStatusItem buildSoftwareItem( + String code, + String name, + String displayName, + boolean enabled, + BooleanSupplier probe + ) { + SoftwareRuntimeStatusItem item = new SoftwareRuntimeStatusItem(); + item.setComponentCode(code); + item.setComponentName(name); + item.setDisplayName(firstNonBlank(displayName, name)); + if (!enabled) { + item.setStatus(STATUS_NOT_INSTALLED); + item.setMessage("未安装"); + return item; + } + try { + if (probe.getAsBoolean()) { + item.setStatus(STATUS_NORMAL); + item.setMessage("正常"); + } else { + item.setStatus(STATUS_ABNORMAL); + item.setMessage("异常"); + } + } catch (RuntimeException ex) { + item.setStatus(STATUS_UNKNOWN); + item.setMessage("未知"); + } + return item; + } + + private CurrentInitConfigResponse loadCurrentInitConfigIfPresent() { + try { + if (!"INITIALIZED".equals(initService.getDeviceInitStateSnapshot().initState())) { + return null; + } + return initService.loadCurrentInitConfig(); + } catch (BizException ex) { + return null; + } + } + + private boolean isRabbitmqEnabled(String mqType) { + return "RABBITMQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType) || "RABBITMQ_CFMQ".equals(mqType); + } + + private boolean isTlqEnabled(String mqType) { + return "TLQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType); + } + + private boolean isTidbEnabled(String productType) { + return "ENTERPRISE".equals(productType) || "INDIRECT".equals(productType); + } + + private boolean isNginxEnabled(String productType) { + return "ENTERPRISE".equals(productType) || "INDIRECT".equals(productType); + } + + private String cached(String componentCode) { + Optional entity = deviceSoftwareVersionRepository.findByComponentCode(componentCode); + return entity.map(DeviceSoftwareVersionEntity::getCurrentVersion).orElse(""); + } + + private String appName(String productType) { + return switch (productType) { + case "ENTERPRISE" -> "标准收发器企业版"; + case "INDIRECT" -> "标准收发器间参版"; + case "DIRECT" -> "直参轻量化终端版"; + default -> "应用软件"; + }; + } + + private String normalize(String value) { + return value == null ? "" : value.trim().toUpperCase(); + } + + private double round(double value) { + return Math.round(value * 100D) / 100D; + } + + private String firstNonBlank(String... values) { + if (values == null) { + return ""; + } + for (String value : values) { + if (value != null && !value.isBlank()) { + return value.trim(); + } + } + return ""; + } + + @FunctionalInterface + private interface UsageSupplier { + UsageSnapshot get(); + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/service/impl/DeviceServiceImpl.java b/src/main/java/com/cisd/tms/modules/device/service/impl/DeviceServiceImpl.java new file mode 100644 index 0000000..4ceeca6 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/service/impl/DeviceServiceImpl.java @@ -0,0 +1,76 @@ +package com.cisd.tms.modules.device.service.impl; + +import com.cisd.tms.integration.crypto.pcie.model.DeviceInfoResult; +import com.cisd.tms.integration.crypto.pcie.model.DeviceStatusResult; +import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; +import com.cisd.tms.modules.device.config.DeviceProfileProperties; +import com.cisd.tms.modules.device.dto.DeviceActionResponse; +import com.cisd.tms.modules.device.dto.DeviceInfoResponse; +import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; +import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository; +import com.cisd.tms.modules.device.service.DeviceService; +import com.cisd.tms.modules.init.service.InitService; +import java.time.OffsetDateTime; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DeviceServiceImpl implements DeviceService { + + private final DeviceProfileProperties deviceProfileProperties; + private final DeviceSoftwareVersionRepository deviceSoftwareVersionRepository; + private final PcieCryptoService pcieCryptoService; + private final InitService initService; + + @Override + public DeviceInfoResponse info() { + DeviceInfoResponse response = new DeviceInfoResponse(); + response.setDeviceModel(trim(deviceProfileProperties.getModel())); + response.setDeviceName(trim(deviceProfileProperties.getName())); + response.setVersion(loadTmsVersion()); + response.setSerialNumber(loadDeviceSerial()); + response.setMasterKeyStatus(loadManagementKeyReady()); + InitService.DeviceInitStateSnapshot snapshot = initService.getDeviceInitStateSnapshot(); + response.setDeviceStatus(snapshot.initState()); + return response; + } + + @Override + public DeviceActionResponse restart(String operator) { + DeviceActionResponse response = new DeviceActionResponse(); + response.setAction("RESTART"); + response.setAccepted(true); + response.setOperator(operator == null || operator.isBlank() ? "unknown" : operator); + response.setAcceptedAt(OffsetDateTime.now().toString()); + return response; + } + + private String loadTmsVersion() { + Optional version = deviceSoftwareVersionRepository.findByComponentCode("TMS"); + return version.map(DeviceSoftwareVersionEntity::getCurrentVersion).map(DeviceServiceImpl::trim).orElse(""); + } + + private String loadDeviceSerial() { + try { + DeviceInfoResult result = pcieCryptoService.getDeviceInfo(); + return result == null ? "" : trim(result.getDeviceSerial()); + } catch (RuntimeException ex) { + return ""; + } + } + + private boolean loadManagementKeyReady() { + try { + DeviceStatusResult result = pcieCryptoService.getDeviceStatus(); + return result != null && result.getFsmState() == 0; + } catch (RuntimeException ex) { + return false; + } + } + + private static String trim(String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/service/impl/TimeConfigServiceImpl.java b/src/main/java/com/cisd/tms/modules/device/service/impl/TimeConfigServiceImpl.java new file mode 100644 index 0000000..4b94f48 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/service/impl/TimeConfigServiceImpl.java @@ -0,0 +1,109 @@ +package com.cisd.tms.modules.device.service.impl; + +import com.cisd.tms.modules.device.dto.TimeConfigRequest; +import com.cisd.tms.modules.device.service.TimeConfigService; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class TimeConfigServiceImpl implements TimeConfigService { + + @Override + public void processTimeConfig(TimeConfigRequest request) { + String mode = request.getMode(); + + if ("MANUAL".equalsIgnoreCase(mode)) { + + String timezone = request.getTimezone(); + if (timezone == null || !timezone.matches("^[A-Za-z0-9/_-]+$")) { + throw new IllegalArgumentException("Invalid or empty timezone format"); + } + + String datetime = request.getDatetime(); + if (datetime == null || !datetime.matches("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}$")) { + throw new IllegalArgumentException("Invalid or empty datetime format. Expected: YYYY-MM-DD HH:MM"); + } + + executeCommand("timedatectl set-timezone " + request.getTimezone().trim()); + executeCommand("systemctl stop chronyd"); + executeCommand("date -s '" + request.getDatetime() + "'"); + + } else if ("NTP".equalsIgnoreCase(mode)) { + + List servers = request.getNtpServers(); + if (servers == null || servers.isEmpty()) { + throw new IllegalArgumentException("At least one NTP server IP must be provided in NTP mode"); + } + + int interval = request.getSyncInterval() != null ? request.getSyncInterval() : 600; + int pollExp = (int) Math.round(Math.log(interval) / Math.log(2)); + StringBuilder serverConfLines = new StringBuilder(); + for (String server : servers) { + if (!server.matches("^[A-Za-z0-9.-]+$")) { + throw new IllegalArgumentException("Invalid NTP server address format: " + server); + } + + String pingResult = executeCommand("ping -c 1 -W 1 " + server); + if (!pingResult.contains("1 received")) { + throw new RuntimeException("NTP server is unreachable via ping: " + server); + } + serverConfLines.append("server ").append(server) + .append(" iburst minpoll ").append(pollExp) + .append(" maxpoll ").append(pollExp).append("\\n"); + } + String confContent = serverConfLines + + "driftfile /var/lib/chrony/drift\\n" + + "makestep 1.0 3\\n" + + "rtcsync\\n"; + executeCommand("echo -e '" + confContent + "' > /etc/chrony.conf"); + executeCommand("systemctl restart chronyd"); + executeCommand("chronyc -a makestep"); + String status = executeCommand("chronyc tracking"); + if (!status.contains("Reference ID")) { + throw new RuntimeException("NTP sync status check failed"); + } + + } else { + throw new IllegalArgumentException("Unknown mode: " + mode); + } + + } + + private String executeCommand(String command) { + ProcessBuilder processBuilder = new ProcessBuilder("bash", "-c", command); + processBuilder.redirectErrorStream(true); + + Process process = null; + try { + process = processBuilder.start(); + + StringBuilder output = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + } + + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new RuntimeException("command execution failed: [" + command + "] "); + } + return output.toString(); + + } catch (IOException e) { + throw new RuntimeException("I/O error executing command: [" + command + "]", e); + } catch (InterruptedException e) { + if (process != null) { + process.destroy(); + } + Thread.currentThread().interrupt(); + throw new RuntimeException("process was interrupted while executing command: [" + command + "]", e); + } + } +} diff --git a/src/main/java/com/cisd/tms/modules/file/controller/FileController.java b/src/main/java/com/cisd/tms/modules/file/controller/FileController.java index 7ee866e..c98fd2a 100644 --- a/src/main/java/com/cisd/tms/modules/file/controller/FileController.java +++ b/src/main/java/com/cisd/tms/modules/file/controller/FileController.java @@ -7,6 +7,7 @@ import com.cisd.tms.modules.file.service.FileService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -17,15 +18,12 @@ import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/api/v1/files") +@RequiredArgsConstructor @Tag(name = "文件管理", description = "初始化文件上传与文件元数据查询接口") public class FileController { private final FileService fileService; - public FileController(FileService fileService) { - this.fileService = fileService; - } - // 内部上传入口,前端先拿到 fileId,再把 fileId 传给初始化相关接口。 @PostMapping("/upload") @Operation(summary = "上传初始化文件", description = "上传许可证、配置包等初始化依赖文件,返回 fileId 供后续任务引用。") diff --git a/src/main/java/com/cisd/tms/modules/file/service/FileService.java b/src/main/java/com/cisd/tms/modules/file/service/FileService.java index a816d55..3703017 100644 --- a/src/main/java/com/cisd/tms/modules/file/service/FileService.java +++ b/src/main/java/com/cisd/tms/modules/file/service/FileService.java @@ -1,118 +1,12 @@ 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 { +public interface FileService { - private static final DateTimeFormatter FILE_ID_TS = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + FileUploadResponse upload(MultipartFile file); - 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(); - // 固定为 //,这样初始化流程只靠 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(); - } + FileDetailResponse getDetail(String fileId); } diff --git a/src/main/java/com/cisd/tms/modules/file/service/impl/FileServiceImpl.java b/src/main/java/com/cisd/tms/modules/file/service/impl/FileServiceImpl.java new file mode 100644 index 0000000..8a34cb3 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/file/service/impl/FileServiceImpl.java @@ -0,0 +1,119 @@ +package com.cisd.tms.modules.file.service.impl; + +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 com.cisd.tms.modules.file.service.FileService; +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.time.Clock; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +@RequiredArgsConstructor +public class FileServiceImpl implements FileService { + + private static final DateTimeFormatter FILE_ID_TS = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + + private final FileRecordRepository fileRecordRepository; + private final FileStorageProperties fileStorageProperties; + private final Clock clock; + + @Override + 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(); + // 固定为 //,这样初始化流程只靠 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; + } + + @Override + 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(clock).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(); + } +} diff --git a/src/main/java/com/cisd/tms/modules/init/service/InitService.java b/src/main/java/com/cisd/tms/modules/init/service/InitService.java index 78d3a6e..83f62fd 100644 --- a/src/main/java/com/cisd/tms/modules/init/service/InitService.java +++ b/src/main/java/com/cisd/tms/modules/init/service/InitService.java @@ -1,12 +1,7 @@ package com.cisd.tms.modules.init.service; -import com.cisd.tms.common.config.properties.CisdPresetProperties; -import com.cisd.tms.common.enums.ErrorCode; -import com.cisd.tms.common.exception.BizException; -import com.cisd.tms.modules.init.config.InitCommandProfileService; -import com.cisd.tms.modules.init.config.InitExecutorProperties; -import com.cisd.tms.modules.init.dto.InitCreateTaskResponse; import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse; +import com.cisd.tms.modules.init.dto.InitCreateTaskResponse; import com.cisd.tms.modules.init.dto.InitPlanTemplateResponse; import com.cisd.tms.modules.init.dto.InitPreviewRequest; import com.cisd.tms.modules.init.dto.InitPreviewResponse; @@ -16,918 +11,35 @@ import com.cisd.tms.modules.init.dto.InitTaskStepLogResponse; import com.cisd.tms.modules.init.dto.InitTaskStepResponse; import com.cisd.tms.modules.init.dto.ResetCreateTaskResponse; import com.cisd.tms.modules.init.dto.ResetPreviewResponse; -import com.cisd.tms.modules.init.entity.InitTaskEntity; -import com.cisd.tms.modules.init.entity.InitTaskStepEntity; -import com.cisd.tms.modules.init.executor.InitStepExecutionResult; -import com.cisd.tms.modules.init.executor.InitStepExecutor; -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.JsonNode; -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.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; -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; +import java.util.List; -@Service -public class InitService { +public interface InitService { - private static final String TASK_TYPE_INIT = "INIT"; - private static final String TASK_TYPE_RESET = "RESET"; - private static final Set PRESET_PRODUCT_TYPES = Set.of("ENTERPRISE", "INDIRECT", "DIRECT"); - private static final Set DEPLOY_STANDARD = Set.of("SINGLE", "DUAL"); - private static final Set DEPLOY_DIRECT = Set.of("SINGLE", "DUAL", "QUAD"); - private static final Set MQ_ENTERPRISE = Set.of("RABBITMQ", "TLQ"); - private static final Set MQ_INDIRECT = Set.of("RABBITMQ", "RABBITMQ_TLQ"); - private static final Set 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}$"); + InitPlanTemplateResponse loadTemplate(String ignoredVersion); - private final InitTaskRepository initTaskRepository; - private final InitTaskStepRepository initTaskStepRepository; - private final CisdPresetProperties cisdPresetProperties; - private final ObjectMapper objectMapper; - private final InitStepExecutor initStepExecutor; - private final InitCommandProfileService initCommandProfileService; - private final InitExecutorProperties initExecutorProperties; - private final Executor backgroundExecutor; - private final ConcurrentMap> activeExecutions; + InitPreviewResponse preview(InitPreviewRequest request); - @Autowired - public InitService( - InitTaskRepository initTaskRepository, - InitTaskStepRepository initTaskStepRepository, - CisdPresetProperties cisdPresetProperties, - ObjectMapper objectMapper, - InitStepExecutor initStepExecutor, - InitCommandProfileService initCommandProfileService, - InitExecutorProperties initExecutorProperties - ) { - this( - initTaskRepository, - initTaskStepRepository, - cisdPresetProperties, - objectMapper, - initStepExecutor, - initCommandProfileService, - initExecutorProperties, - Executors.newCachedThreadPool(), - new ConcurrentHashMap<>() - ); - } + ResetPreviewResponse previewReset(); - InitService( - InitTaskRepository initTaskRepository, - InitTaskStepRepository initTaskStepRepository, - CisdPresetProperties cisdPresetProperties, - ObjectMapper objectMapper, - InitStepExecutor initStepExecutor, - InitCommandProfileService initCommandProfileService, - InitExecutorProperties initExecutorProperties, - Executor backgroundExecutor, - ConcurrentMap> activeExecutions - ) { - this.initTaskRepository = initTaskRepository; - this.initTaskStepRepository = initTaskStepRepository; - this.cisdPresetProperties = cisdPresetProperties; - this.objectMapper = objectMapper; - this.initStepExecutor = initStepExecutor; - this.initCommandProfileService = initCommandProfileService; - this.initExecutorProperties = initExecutorProperties; - this.backgroundExecutor = backgroundExecutor; - this.activeExecutions = activeExecutions; - } + InitCreateTaskResponse createTask(InitPreviewRequest request); - /** - * 读取后端预制产品类型和版本。 - * 该接口是前端动态渲染“企业/间参/直参”初始化页的入口。 - */ - public InitPlanTemplateResponse loadTemplate(String ignoredVersion) { - String productType = resolvePresetProductType(); + ResetCreateTaskResponse createResetTask(); - InitPlanTemplateResponse response = new InitPlanTemplateResponse(); - response.setPresetProductType(productType); - response.setPresetVersion(cisdPresetProperties.getVersion()); - response.setSupportedVersions(List.of("ENTERPRISE", "INDIRECT", "DIRECT")); - response.setTemplatePath("classpath:/initplan/"); - return response; - } + DeviceInitStateSnapshot getDeviceInitStateSnapshot(); - public InitPreviewResponse preview(InitPreviewRequest request) { - if (request == null) { - throw new IllegalArgumentException("request must not be null"); - } + CurrentInitConfigResponse loadCurrentInitConfig(); - String productType = resolvePresetProductType(); - validateCommon(request); - validateByProductType(productType, request); + InitTaskDetailResponse getTask(String taskId); - String previewTaskId = "preview-" + productType + "-" + request.getOrgCode(); - boolean existed = initTaskRepository.findByTaskId(previewTaskId).isPresent(); + List getTaskSteps(String taskId); - InitPreviewResponse response = new InitPreviewResponse(); - response.setResolvedProductType(productType); - response.setPresetVersion(cisdPresetProperties.getVersion()); - response.setStepCount(estimateStepCount(productType, request)); - response.setSummary(existed ? "init preview generated from existing task fingerprint" : "init preview generated"); - return response; - } + InitTaskStepLogResponse getTaskStepLog(String taskId, int stepNo); - public ResetPreviewResponse previewReset() { - ResolvedResetContext context = resolveCurrentResetContext(); - String productType = context.productType(); + InitTaskExecuteResponse executeTask(String taskId); - ResetPreviewResponse response = new ResetPreviewResponse(); - response.setResolvedProductType(productType); - response.setPresetVersion(cisdPresetProperties.getVersion()); - response.setStepCount(resetSteps(productType, context.mqType()).size()); - response.setSummary("reset preview generated from current initialized state"); - return response; - } + InitTaskExecuteResponse executeResetTask(String taskId); - public InitCreateTaskResponse createTask(InitPreviewRequest request) { - if (request == null) { - throw new IllegalArgumentException("request must not be null"); - } - - String productType = resolvePresetProductType(); - //todo 验证是否已经初始化 - validateCommon(request); - validateByProductType(productType, request); - - String taskId = buildTaskId(TASK_TYPE_INIT); - String normalizedMqType = normalizeUpper(request.getMq().getMqType()); - String deployMode = normalizeUpper(request.getDeployMode()); - LocalDateTime now = LocalDateTime.now(); - - // 1) 按产品类型 + MQ 类型生成标准步骤序列 - List stepCodes = defaultSteps(productType, normalizedMqType); - - InitTaskEntity entity = new InitTaskEntity(); - entity.setTaskId(taskId); - entity.setTaskType(TASK_TYPE_INIT); - entity.setVersion(cisdPresetProperties.getVersion()); - entity.setStatus("PENDING"); - entity.setProductType(productType); - entity.setOrgCode(request.getOrgCode()); - entity.setMqType(normalizedMqType); - entity.setDeployMode(deployMode); - entity.setCreateTime(now); - entity.setUpdateTime(now); - entity.setInitPlanJson(generateInitPlanJson(productType, request, stepCodes)); - - // 2) 先落任务主表,确保 taskId 可作为步骤外键归属 - initTaskRepository.save(entity); - - List stepEntities = new ArrayList<>(); - for (int i = 0; i < stepCodes.size(); i++) { - String stepCode = stepCodes.get(i); - InitTaskStepEntity step = new InitTaskStepEntity(); - step.setTaskId(taskId); - step.setStepNo(i + 1); - step.setStepCode(stepCode); - step.setStatus("PENDING"); - step.setMessage("waiting"); - // 3) 每个步骤的执行命令由 profile 配置驱动,避免硬编码在业务代码中 - step.setCommandLine(initCommandProfileService.resolveCommand(productType, normalizedMqType, stepCode)); - step.setCreateTime(now); - step.setUpdateTime(now); - stepEntities.add(step); - } - // 4) 批量落步骤表,初始状态统一为 PENDING - initTaskStepRepository.saveBatch(stepEntities); - - InitCreateTaskResponse response = new InitCreateTaskResponse(); - response.setTaskId(taskId); - response.setStatus(entity.getStatus()); - response.setTaskType(entity.getTaskType()); - response.setResolvedProductType(productType); - response.setPresetVersion(cisdPresetProperties.getVersion()); - return response; - } - - public ResetCreateTaskResponse createResetTask() { - ResolvedResetContext context = resolveCurrentResetContext(); - String productType = context.productType(); - String taskId = buildTaskId(TASK_TYPE_RESET); - String normalizedMqType = context.mqType(); - LocalDateTime now = LocalDateTime.now(); - List stepCodes = resetSteps(productType, normalizedMqType); - - InitTaskEntity entity = new InitTaskEntity(); - entity.setTaskId(taskId); - entity.setTaskType(TASK_TYPE_RESET); - entity.setVersion(cisdPresetProperties.getVersion()); - entity.setStatus("PENDING"); - entity.setProductType(productType); - entity.setOrgCode(context.orgCode()); - entity.setMqType(normalizedMqType); - entity.setDeployMode(""); - entity.setCreateTime(now); - entity.setUpdateTime(now); - entity.setInitPlanJson(generateResetPlanJson(context, stepCodes)); - initTaskRepository.save(entity); - - List stepEntities = new ArrayList<>(); - for (int i = 0; i < stepCodes.size(); i++) { - String stepCode = stepCodes.get(i); - InitTaskStepEntity step = new InitTaskStepEntity(); - step.setTaskId(taskId); - step.setStepNo(i + 1); - step.setStepCode(stepCode); - step.setStatus("PENDING"); - step.setMessage("waiting"); - step.setCommandLine(initCommandProfileService.resolveCommand(productType, normalizedMqType, stepCode)); - step.setCreateTime(now); - step.setUpdateTime(now); - stepEntities.add(step); - } - initTaskStepRepository.saveBatch(stepEntities); - - ResetCreateTaskResponse response = new ResetCreateTaskResponse(); - response.setTaskId(taskId); - response.setStatus(entity.getStatus()); - response.setTaskType(entity.getTaskType()); - response.setResolvedProductType(productType); - response.setPresetVersion(cisdPresetProperties.getVersion()); - return response; - } - - public DeviceInitStateSnapshot getDeviceInitStateSnapshot() { - Optional running = initTaskRepository.findLatestByStatus("RUNNING"); - if (running.isPresent()) { - InitTaskEntity task = running.get(); - String taskType = normalize(task.getTaskType()); - if (TASK_TYPE_RESET.equals(taskType)) { - return new DeviceInitStateSnapshot("RESETTING", TASK_TYPE_RESET, task.getStatus(), null, null); - } - return new DeviceInitStateSnapshot("INITIALIZING", TASK_TYPE_INIT, task.getStatus(), null, null); - } - - Optional latestInit = initTaskRepository.findLatestByTaskTypeAndStatus(TASK_TYPE_INIT, "SUCCESS"); - Optional latestReset = initTaskRepository.findLatestByTaskTypeAndStatus(TASK_TYPE_RESET, "SUCCESS"); - if (latestInit.isEmpty()) { - return new DeviceInitStateSnapshot("UNINITIALIZED", null, null, null, null); - } - if (latestReset.isPresent() && compareTaskRecency(latestReset.get(), latestInit.get()) >= 0) { - return new DeviceInitStateSnapshot("UNINITIALIZED", TASK_TYPE_RESET, latestReset.get().getStatus(), null, null); - } - - ResolvedResetContext context = resolveResetContextFromInitTask(latestInit.get()); - return new DeviceInitStateSnapshot("INITIALIZED", TASK_TYPE_INIT, latestInit.get().getStatus(), context.orgCode(), context.mqType()); - } - - public CurrentInitConfigResponse loadCurrentInitConfig() { - Optional latestInit = initTaskRepository.findLatestByTaskTypeAndStatus(TASK_TYPE_INIT, "SUCCESS"); - if (latestInit.isEmpty()) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "device is not initialized"); - } - return resolveCurrentInitConfigFromTask(latestInit.get()); - } - - public InitTaskDetailResponse getTask(String taskId) { - InitTaskEntity entity = requireTask(taskId); - - InitTaskDetailResponse response = new InitTaskDetailResponse(); - response.setTaskId(entity.getTaskId()); - response.setStatus(entity.getStatus()); - response.setTaskType(normalize(entity.getTaskType()).isBlank() ? TASK_TYPE_INIT : entity.getTaskType()); - response.setResolvedProductType(entity.getProductType()); - response.setPresetVersion(entity.getVersion()); - response.setOrgCode(entity.getOrgCode()); - response.setMqType(entity.getMqType()); - response.setDeployMode(entity.getDeployMode()); - response.setInitPlanJson(entity.getInitPlanJson()); - return response; - } - - public List getTaskSteps(String taskId) { - requireTask(taskId); - - return initTaskStepRepository.findByTaskIdOrderByStepNo(taskId).stream().map(entity -> { - InitTaskStepResponse response = new InitTaskStepResponse(); - response.setStepNo(entity.getStepNo() == null ? 0 : entity.getStepNo()); - response.setStepCode(entity.getStepCode()); - response.setStatus(entity.getStatus()); - response.setMessage(entity.getMessage()); - response.setCommandLine(entity.getCommandLine()); - response.setLogPath(entity.getLogPath()); - response.setExitCode(entity.getExitCode()); - return response; - }).toList(); - } - - public InitTaskStepLogResponse getTaskStepLog(String taskId, int stepNo) { - if (stepNo <= 0) { - throw new IllegalArgumentException("stepNo must be > 0"); - } - requireTask(taskId); - InitTaskStepEntity step = requireStep(taskId, stepNo); - - if (isBlank(step.getLogPath())) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "log file not generated for task step"); - } - - Path logPath = Paths.get(step.getLogPath()).normalize().toAbsolutePath(); - Path logDir = Paths.get(initExecutorProperties.getLogDir()).normalize().toAbsolutePath(); - // 只允许读取配置日志目录下的文件,防止路径穿越读取任意系统文件 - if (!logPath.startsWith(logDir)) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "log path is out of allowed directory"); - } - if (!Files.exists(logPath) || !Files.isRegularFile(logPath)) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "log file does not exist"); - } - - try { - String content = Files.readString(logPath); - InitTaskStepLogResponse response = new InitTaskStepLogResponse(); - response.setTaskId(taskId); - response.setStepNo(stepNo); - response.setStepCode(step.getStepCode()); - response.setLogPath(logPath.toString()); - response.setContent(content); - return response; - } catch (IOException ex) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "failed to read log file"); - } - } - - public InitTaskExecuteResponse executeTask(String taskId) { - return executeTaskInternal(taskId); - } - - public InitTaskExecuteResponse executeResetTask(String taskId) { - InitTaskEntity task = requireTask(taskId); - String taskType = normalize(task.getTaskType()); - if (!TASK_TYPE_RESET.equals(taskType)) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "reset task not found: " + taskId); - } - return executeTaskInternal(taskId); - } - - private InitTaskExecuteResponse executeTaskInternal(String taskId) { - InitTaskEntity task = requireTask(taskId); - List steps = initTaskStepRepository.findByTaskIdOrderByStepNo(taskId); - 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); - } - - synchronized (activeExecutions) { - CompletableFuture 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 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 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); - - 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()); - } - - step.setExitCode(result.getExitCode()); - step.setLogPath(result.getLogPath()); - step.setMessage(result.getMessage()); - step.setUpdateTime(LocalDateTime.now()); - step.setStatus(result.isSuccess() ? "SUCCESS" : "FAILED"); - initTaskStepRepository.updateBatch(steps); - if (!result.isSuccess()) { - failed = true; - break; - } - } - - task.setStatus(failed ? "FAILED" : "SUCCESS"); - task.setUpdateTime(LocalDateTime.now()); - initTaskRepository.update(task); - } - - private void resetResidualRunningSteps(List 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 steps) { - InitTaskExecuteResponse response = new InitTaskExecuteResponse(); - response.setTaskId(task.getTaskId()); - response.setStatus(task.getStatus()); - response.setTotalSteps(steps.size()); - response.setSuccessSteps((int) steps.stream().filter(step -> "SUCCESS".equals(step.getStatus())).count()); - return response; - } - - private InitTaskEntity requireTask(String taskId) { - return initTaskRepository.findByTaskId(taskId) - .orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "init task not found: " + taskId)); - } - - private InitTaskStepEntity requireStep(String taskId, int stepNo) { - return initTaskStepRepository.findByTaskIdOrderByStepNo(taskId).stream() - .filter(item -> item.getStepNo() != null && item.getStepNo() == stepNo) - .findFirst() - .orElseThrow(() -> new BizException( - ErrorCode.BIZ_ERROR.getCode(), - "init task step not found: taskId=" + taskId + ", stepNo=" + stepNo - )); - } - - private String generateInitPlanJson(String productType, InitPreviewRequest request, List steps) { - Map root = new LinkedHashMap<>(); - root.put("schemaVersion", "1.0"); - root.put("productType", productType); - root.put("orgCode", request.getOrgCode()); - root.put("deployMode", normalizeUpper(request.getDeployMode())); - root.put("mqType", normalizeUpper(request.getMq().getMqType())); - // 执行 internal::render_config / internal::cfmq_apply 时需要读取原始请求参数进行文件渲染与校验。 - root.put("request", request); - root.put("steps", steps); - root.put("generatedAt", LocalDateTime.now().toString()); - - try { - return objectMapper.writeValueAsString(root); - } catch (JsonProcessingException ex) { - throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to serialize init plan"); - } - } - - private String generateResetPlanJson(ResolvedResetContext context, List steps) { - Map root = new LinkedHashMap<>(); - root.put("schemaVersion", "1.0"); - root.put("taskType", TASK_TYPE_RESET); - root.put("productType", context.productType()); - root.put("orgCode", context.orgCode()); - root.put("mqType", context.mqType()); - root.put("sourceTaskId", context.sourceTaskId()); - Map request = new LinkedHashMap<>(); - request.put("orgCode", context.orgCode()); - request.put("mqType", context.mqType()); - request.put("channelUsername", context.channelUsername()); - root.put("request", request); - root.put("steps", steps); - root.put("generatedAt", LocalDateTime.now().toString()); - - try { - return objectMapper.writeValueAsString(root); - } catch (JsonProcessingException ex) { - throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to serialize reset plan"); - } - } - - private List defaultSteps(String productType, String mqType) { - // 该步骤序列只描述“逻辑编排”,具体脚本命令在 step-command-profiles.json 中定义 - if ("DIRECT".equals(productType)) { - if ("RABBITMQ_CFMQ".equals(mqType)) { - return List.of("RENDER_CONFIG", "FILE_PREPARE", "MQ_USER_APPLY", "DB_INIT", "CFMQ_APPLY", "VERIFY"); - } - return List.of("RENDER_CONFIG", "FILE_PREPARE", "MQ_USER_APPLY", "DB_INIT", "TLQ_INIT_CFG", "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" - ); - } - 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 List resetSteps(String productType, String mqType) { - if ("DIRECT".equals(productType)) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "reset is not supported for current product type: " + productType); - } - return List.of( - "STOP_APPS", - "DB_DROP_CMEP", - "DB_DROP_ORG", - "MQ_CLEANUP", - "STOP_RABBITMQ", - "TLQ_LICENSE_CLEANUP", - "CLEAN_STAGING", - "VERIFY_RESET" - ); - } - - private String buildTaskId(String taskType) { - String ts = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); - String suffix = UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase(Locale.ROOT); - String prefix = TASK_TYPE_RESET.equals(taskType) ? TASK_TYPE_RESET : TASK_TYPE_INIT; - return prefix + "-" + ts + "-" + suffix; - } - - private String resolvePresetProductType() { - String raw = normalize(cisdPresetProperties.getProductType()); - if (isBlank(raw)) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "device preset product type is missing, initialization is forbidden"); - } - - String productType = raw.toUpperCase(Locale.ROOT); - if (!PRESET_PRODUCT_TYPES.contains(productType)) { - throw new BizException( - ErrorCode.BIZ_ERROR.getCode(), - "device preset product type is invalid: " + raw + ", initialization is forbidden" - ); - } - return productType; - } - - private String resolveSupportedResetProductType() { - String productType = resolvePresetProductType(); - if ("DIRECT".equals(productType)) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "reset is not supported for current product type: " + productType); - } - return productType; - } - - private void validateCommon(InitPreviewRequest request) { - if (request.getNodes() == null) { - throw new IllegalArgumentException("nodes is required"); - } - if (request.getMq() == null) { - throw new IllegalArgumentException("mq is required"); - } - if (request.getLicenses() == null) { - throw new IllegalArgumentException("licenses is required"); - } - - requireNotBlank("orgCode", request.getOrgCode()); - requireNotBlank("deployMode", request.getDeployMode()); - requireNotBlank("nodes.node01Ip", request.getNodes().getNode01Ip()); - requireNotBlank("mq.mqType", request.getMq().getMqType()); - requireNotBlank("mq.channelUsername", request.getMq().getChannelUsername()); - requireNotBlank("mq.channelPassword", request.getMq().getChannelPassword()); - requireNotBlank("licenses.receiverLicenseFileId", request.getLicenses().getReceiverLicenseFileId()); - requireFileId("licenses.receiverLicenseFileId", request.getLicenses().getReceiverLicenseFileId()); - - requireOptionalFileId("mq.tlqLicenseFileId", request.getMq().getTlqLicenseFileId()); - requireOptionalFileId("mq.cfmqConfigFileId", request.getMq().getCfmqConfigFileId()); - requireOptionalFileId("licenses.cfgZipFileId", request.getLicenses().getCfgZipFileId()); - } - - private void validateByProductType(String productType, InitPreviewRequest request) { - String deployMode = normalizeUpper(request.getDeployMode()); - String mqType = normalizeUpper(request.getMq().getMqType()); - - if ("DIRECT".equals(productType)) { - validateDirect(request, deployMode, mqType); - return; - } - - validateStandard(productType, request, deployMode, mqType); - } - - private ResolvedResetContext resolveCurrentResetContext() { - String productType = resolveSupportedResetProductType(); - Optional latestInit = initTaskRepository.findLatestByTaskTypeAndStatus(TASK_TYPE_INIT, "SUCCESS"); - if (latestInit.isEmpty()) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "device is not initialized, reset is forbidden"); - } - Optional latestReset = initTaskRepository.findLatestByTaskTypeAndStatus(TASK_TYPE_RESET, "SUCCESS"); - if (latestReset.isPresent() && compareTaskRecency(latestReset.get(), latestInit.get()) >= 0) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "device is not initialized, reset is forbidden"); - } - - ResolvedResetContext context = resolveResetContextFromInitTask(latestInit.get()); - if (!productType.equals(context.productType())) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "device preset product type changed, reset context is invalid"); - } - requireAllowed("mqType", context.mqType(), resolveStandardMqAllowed(productType)); - if (("RABBITMQ".equals(context.mqType()) || "RABBITMQ_TLQ".equals(context.mqType())) && isBlank(context.channelUsername())) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "current init context missing rabbitmq channel username"); - } - return context; - } - - private ResolvedResetContext resolveResetContextFromInitTask(InitTaskEntity task) { - if (task == null) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task is missing"); - } - if (isBlank(task.getInitPlanJson())) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task snapshot is missing"); - } - try { - JsonNode root = objectMapper.readTree(task.getInitPlanJson()); - JsonNode request = root.path("request"); - JsonNode mq = request.path("mq"); - String productType = firstNonBlank(normalizeUpper(text(root, "productType")), normalizeUpper(task.getProductType())); - String orgCode = firstNonBlank(text(request, "orgCode"), text(root, "orgCode"), task.getOrgCode()); - String mqType = firstNonBlank(normalizeUpper(text(mq, "mqType")), normalizeUpper(text(root, "mqType")), normalizeUpper(task.getMqType())); - String channelUsername = firstNonBlank(text(mq, "channelUsername"), text(request, "channelUsername")); - - if (isBlank(productType) || isBlank(orgCode) || isBlank(mqType)) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task snapshot is incomplete"); - } - return new ResolvedResetContext(productType, normalize(orgCode), normalizeUpper(mqType), normalize(channelUsername), task.getTaskId()); - } catch (JsonProcessingException ex) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "failed to parse successful init task snapshot"); - } - } - - private CurrentInitConfigResponse resolveCurrentInitConfigFromTask(InitTaskEntity task) { - if (task == null) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task is missing"); - } - if (isBlank(task.getInitPlanJson())) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task snapshot is missing"); - } - try { - JsonNode root = objectMapper.readTree(task.getInitPlanJson()); - JsonNode request = root.path("request"); - JsonNode mq = request.path("mq"); - JsonNode nodes = request.path("nodes"); - JsonNode signServer = request.path("signServer"); - - CurrentInitConfigResponse response = new CurrentInitConfigResponse(); - response.setOrgCodeType(firstNonBlank(normalizeUpper(text(request, "orgCodeType")), normalizeUpper(text(root, "orgCodeType")))); - response.setOrgCode(firstNonBlank(text(request, "orgCode"), text(root, "orgCode"), task.getOrgCode())); - response.setOrgNameCn(firstNonBlank(text(request, "orgNameCn"), text(root, "orgNameCn"))); - response.setOrgNameEn(firstNonBlank(text(request, "orgNameEn"), text(root, "orgNameEn"))); - response.setProductType(firstNonBlank(normalizeUpper(text(root, "productType")), normalizeUpper(task.getProductType()))); - response.setDeployMode(firstNonBlank(normalizeUpper(text(request, "deployMode")), normalizeUpper(task.getDeployMode()))); - response.setMqType(firstNonBlank(normalizeUpper(text(mq, "mqType")), normalizeUpper(text(root, "mqType")), normalizeUpper(task.getMqType()))); - response.setChannelUsername(firstNonBlank(text(mq, "channelUsername"), text(request, "channelUsername"))); - response.setNode01Ip(firstNonBlank(text(nodes, "node01Ip"), text(request, "node01Ip"))); - response.setNode02Ip(firstNonBlank(text(nodes, "node02Ip"), text(request, "node02Ip"))); -// response.setSignHost(firstNonBlank(text(signServer, "signHost"), text(request, "signHost"))); -// response.setSignPort(integerValue(firstNonBlank(text(signServer, "signPort"), text(request, "signPort")))); -// response.setSignType(firstNonBlank(text(signServer, "signType"), text(request, "signType"))); - return response; - } catch (JsonProcessingException ex) { - throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "failed to parse successful init task snapshot"); - } - } - - private int compareTaskRecency(InitTaskEntity left, InitTaskEntity right) { - Comparator comparator = Comparator.nullsFirst(LocalDateTime::compareTo); - int byUpdate = comparator.compare(left.getUpdateTime(), right.getUpdateTime()); - if (byUpdate != 0) { - return byUpdate; - } - int byCreate = comparator.compare(left.getCreateTime(), right.getCreateTime()); - if (byCreate != 0) { - return byCreate; - } - return normalize(left.getTaskId()).compareTo(normalize(right.getTaskId())); - } - - private void validateStandard(String productType, InitPreviewRequest request, String deployMode, String mqType) { - requireAllowed("deployMode", deployMode, DEPLOY_STANDARD); - requireAllowed("mq.mqType", mqType, resolveStandardMqAllowed(productType)); - - if ("DUAL".equals(deployMode)) { - requireNotBlank("nodes.node02Ip", request.getNodes().getNode02Ip()); - } - - if ("TLQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType)) { - requireNotBlank("mq.tlqLicenseFileId", request.getMq().getTlqLicenseFileId()); - requireFileId("mq.tlqLicenseFileId", request.getMq().getTlqLicenseFileId()); - } - } - - private Set 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); - - if ("DUAL".equals(deployMode) || "QUAD".equals(deployMode)) { - requireNotBlank("nodes.node02Ip", request.getNodes().getNode02Ip()); - } - if ("QUAD".equals(deployMode)) { - requireNotBlank("nodes.node03Ip", request.getNodes().getNode03Ip()); - requireNotBlank("nodes.node04Ip", request.getNodes().getNode04Ip()); - } - - requireNotBlank("licenses.cfgZipFileId", request.getLicenses().getCfgZipFileId()); - requireFileId("licenses.cfgZipFileId", request.getLicenses().getCfgZipFileId()); - - InitPreviewRequest.SignServer signServer = request.getSignServer(); - if (signServer == null) { - throw new IllegalArgumentException("signServer is required for DIRECT"); - } - requireNotBlank("signServer.signHost", signServer.getSignHost()); - if (signServer.getSignPort() == null || signServer.getSignPort() <= 0 || signServer.getSignPort() > 65535) { - throw new IllegalArgumentException("signServer.signPort must be in [1,65535]"); - } - requireNotBlank("signServer.signType", signServer.getSignType()); - requireNotBlank("signServer.signCertDn", signServer.getSignCertDn()); - requireNotBlank("signServer.signCertSn", signServer.getSignCertSn()); - requireNotBlank("signServer.signPassword", signServer.getSignPassword()); - - if ("RABBITMQ_TLQ".equals(mqType)) { - requireNotBlank("mq.tlqLicenseFileId", request.getMq().getTlqLicenseFileId()); - requireFileId("mq.tlqLicenseFileId", request.getMq().getTlqLicenseFileId()); - InitPreviewRequest.DirectTlqConfig directTlq = request.getDirectTlq(); - if (directTlq == null) { - throw new IllegalArgumentException("directTlq is required for RABBITMQ_TLQ"); - } - requireNotBlank("directTlq.localNodeSeq", directTlq.getLocalNodeSeq()); - requireNotBlank("directTlq.upstreamInstitutions", directTlq.getUpstreamInstitutions()); - if (directTlq.getUpstreamCocCount() == null - || directTlq.getUpstreamCocCount() < 1 - || directTlq.getUpstreamCocCount() > 2) { - throw new IllegalArgumentException("directTlq.upstreamCocCount must be in [1,2]"); - } - } - if ("RABBITMQ_CFMQ".equals(mqType)) { - requireNotBlank("mq.cfmqConfigFileId", request.getMq().getCfmqConfigFileId()); - requireFileId("mq.cfmqConfigFileId", request.getMq().getCfmqConfigFileId()); - } - } - - private int estimateStepCount(String productType, InitPreviewRequest request) { - String mqType = normalizeUpper(request.getMq().getMqType()); - return defaultSteps(productType, mqType).size(); - } - - private static void requireAllowed(String field, String value, Set allowed) { - if (!allowed.contains(value)) { - throw new IllegalArgumentException(field + " must be one of " + allowed); - } - } - - private static void requireNotBlank(String field, String value) { - if (isBlank(value)) { - throw new IllegalArgumentException(field + " is required"); - } - } - - private static void requireOptionalFileId(String field, String value) { - if (!isBlank(value)) { - requireFileId(field, value); - } - } - - private static void requireFileId(String field, String value) { - if (isBlank(value) || !FILE_ID_PATTERN.matcher(normalize(value)).matches()) { - throw new IllegalArgumentException(field + " must be a fileId"); - } - } - - private static String firstNonBlank(String... values) { - if (values == null) { - return ""; - } - for (String value : values) { - if (!isBlank(value)) { - return normalize(value); - } - } - return ""; - } - - private static String text(JsonNode node, String fieldName) { - if (node == null || fieldName == null || node.isMissingNode() || node.isNull()) { - return ""; - } - JsonNode child = node.path(fieldName); - if (child.isMissingNode() || child.isNull()) { - return ""; - } - if (child.isTextual()) { - return normalize(child.asText()); - } - return normalize(child.toString()); - } - - private static String normalizeUpper(String value) { - return normalize(value).toUpperCase(Locale.ROOT); - } - - private static String normalize(String value) { - return value == null ? "" : value.trim(); - } - - private static Integer integerValue(String value) { - String normalized = normalize(value); - if (isBlank(normalized)) { - return null; - } - try { - return Integer.valueOf(normalized); - } catch (NumberFormatException ex) { - return null; - } - } - - private static boolean isBlank(String value) { - return value == null || value.isBlank(); - } - - public record DeviceInitStateSnapshot( + record DeviceInitStateSnapshot( String initState, String currentTaskType, String currentTaskStatus, @@ -935,13 +47,4 @@ public class InitService { String initializedMqType ) { } - - private record ResolvedResetContext( - String productType, - String orgCode, - String mqType, - String channelUsername, - String sourceTaskId - ) { - } } diff --git a/src/main/java/com/cisd/tms/modules/init/service/impl/InitServiceImpl.java b/src/main/java/com/cisd/tms/modules/init/service/impl/InitServiceImpl.java new file mode 100644 index 0000000..8933611 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/init/service/impl/InitServiceImpl.java @@ -0,0 +1,919 @@ +package com.cisd.tms.modules.init.service.impl; + +import com.cisd.tms.common.config.properties.CisdPresetProperties; +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.modules.init.config.InitCommandProfileService; +import com.cisd.tms.modules.init.config.InitExecutorProperties; +import com.cisd.tms.modules.init.dto.InitCreateTaskResponse; +import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse; +import com.cisd.tms.modules.init.dto.InitPlanTemplateResponse; +import com.cisd.tms.modules.init.dto.InitPreviewRequest; +import com.cisd.tms.modules.init.dto.InitPreviewResponse; +import com.cisd.tms.modules.init.dto.InitTaskDetailResponse; +import com.cisd.tms.modules.init.dto.InitTaskExecuteResponse; +import com.cisd.tms.modules.init.dto.InitTaskStepLogResponse; +import com.cisd.tms.modules.init.dto.InitTaskStepResponse; +import com.cisd.tms.modules.init.dto.ResetCreateTaskResponse; +import com.cisd.tms.modules.init.dto.ResetPreviewResponse; +import com.cisd.tms.modules.init.entity.InitTaskEntity; +import com.cisd.tms.modules.init.entity.InitTaskStepEntity; +import com.cisd.tms.modules.init.executor.InitStepExecutionResult; +import com.cisd.tms.modules.init.executor.InitStepExecutor; +import com.cisd.tms.modules.init.repository.InitTaskRepository; +import com.cisd.tms.modules.init.repository.InitTaskStepRepository; +import com.cisd.tms.modules.init.service.InitService; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +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.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +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 lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class InitServiceImpl implements InitService { + + private static final String TASK_TYPE_INIT = "INIT"; + private static final String TASK_TYPE_RESET = "RESET"; + private static final Set PRESET_PRODUCT_TYPES = Set.of("ENTERPRISE", "INDIRECT", "DIRECT"); + private static final Set DEPLOY_STANDARD = Set.of("SINGLE", "DUAL"); + private static final Set DEPLOY_DIRECT = Set.of("SINGLE", "DUAL", "QUAD"); + private static final Set MQ_ENTERPRISE = Set.of("RABBITMQ", "TLQ"); + private static final Set MQ_INDIRECT = Set.of("RABBITMQ", "RABBITMQ_TLQ"); + private static final Set 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}$"); + + private final InitTaskRepository initTaskRepository; + private final InitTaskStepRepository initTaskStepRepository; + private final CisdPresetProperties cisdPresetProperties; + private final ObjectMapper objectMapper; + private final InitStepExecutor initStepExecutor; + private final InitCommandProfileService initCommandProfileService; + private final InitExecutorProperties initExecutorProperties; + private Executor backgroundExecutor = Executors.newCachedThreadPool(); + private ConcurrentMap> activeExecutions = new ConcurrentHashMap<>(); + + InitServiceImpl( + InitTaskRepository initTaskRepository, + InitTaskStepRepository initTaskStepRepository, + CisdPresetProperties cisdPresetProperties, + ObjectMapper objectMapper, + InitStepExecutor initStepExecutor, + InitCommandProfileService initCommandProfileService, + InitExecutorProperties initExecutorProperties, + Executor backgroundExecutor, + ConcurrentMap> activeExecutions + ) { + this.initTaskRepository = initTaskRepository; + this.initTaskStepRepository = initTaskStepRepository; + this.cisdPresetProperties = cisdPresetProperties; + this.objectMapper = objectMapper; + this.initStepExecutor = initStepExecutor; + this.initCommandProfileService = initCommandProfileService; + this.initExecutorProperties = initExecutorProperties; + this.backgroundExecutor = backgroundExecutor; + this.activeExecutions = activeExecutions; + } + + /** + * 读取后端预制产品类型和版本。 + * 该接口是前端动态渲染“企业/间参/直参”初始化页的入口。 + */ + public InitPlanTemplateResponse loadTemplate(String ignoredVersion) { + String productType = resolvePresetProductType(); + + InitPlanTemplateResponse response = new InitPlanTemplateResponse(); + response.setPresetProductType(productType); + response.setPresetVersion(cisdPresetProperties.getVersion()); + response.setSupportedVersions(List.of("ENTERPRISE", "INDIRECT", "DIRECT")); + response.setTemplatePath("classpath:/initplan/"); + return response; + } + + public InitPreviewResponse preview(InitPreviewRequest request) { + if (request == null) { + throw new IllegalArgumentException("request must not be null"); + } + + String productType = resolvePresetProductType(); + validateCommon(request); + validateByProductType(productType, request); + + String previewTaskId = "preview-" + productType + "-" + request.getOrgCode(); + boolean existed = initTaskRepository.findByTaskId(previewTaskId).isPresent(); + + InitPreviewResponse response = new InitPreviewResponse(); + response.setResolvedProductType(productType); + response.setPresetVersion(cisdPresetProperties.getVersion()); + response.setStepCount(estimateStepCount(productType, request)); + response.setSummary(existed ? "init preview generated from existing task fingerprint" : "init preview generated"); + return response; + } + + public ResetPreviewResponse previewReset() { + ResolvedResetContext context = resolveCurrentResetContext(); + String productType = context.productType(); + + ResetPreviewResponse response = new ResetPreviewResponse(); + response.setResolvedProductType(productType); + response.setPresetVersion(cisdPresetProperties.getVersion()); + response.setStepCount(resetSteps(productType, context.mqType()).size()); + response.setSummary("reset preview generated from current initialized state"); + return response; + } + + public InitCreateTaskResponse createTask(InitPreviewRequest request) { + if (request == null) { + throw new IllegalArgumentException("request must not be null"); + } + + String productType = resolvePresetProductType(); + //todo 验证是否已经初始化 + validateCommon(request); + validateByProductType(productType, request); + + String taskId = buildTaskId(TASK_TYPE_INIT); + String normalizedMqType = normalizeUpper(request.getMq().getMqType()); + String deployMode = normalizeUpper(request.getDeployMode()); + LocalDateTime now = LocalDateTime.now(); + + // 1) 按产品类型 + MQ 类型生成标准步骤序列 + List stepCodes = defaultSteps(productType, normalizedMqType); + + InitTaskEntity entity = new InitTaskEntity(); + entity.setTaskId(taskId); + entity.setTaskType(TASK_TYPE_INIT); + entity.setVersion(cisdPresetProperties.getVersion()); + entity.setStatus("PENDING"); + entity.setProductType(productType); + entity.setOrgCode(request.getOrgCode()); + entity.setMqType(normalizedMqType); + entity.setDeployMode(deployMode); + entity.setCreateTime(now); + entity.setUpdateTime(now); + entity.setInitPlanJson(generateInitPlanJson(productType, request, stepCodes)); + + // 2) 先落任务主表,确保 taskId 可作为步骤外键归属 + initTaskRepository.save(entity); + + List stepEntities = new ArrayList<>(); + for (int i = 0; i < stepCodes.size(); i++) { + String stepCode = stepCodes.get(i); + InitTaskStepEntity step = new InitTaskStepEntity(); + step.setTaskId(taskId); + step.setStepNo(i + 1); + step.setStepCode(stepCode); + step.setStatus("PENDING"); + step.setMessage("waiting"); + // 3) 每个步骤的执行命令由 profile 配置驱动,避免硬编码在业务代码中 + step.setCommandLine(initCommandProfileService.resolveCommand(productType, normalizedMqType, stepCode)); + step.setCreateTime(now); + step.setUpdateTime(now); + stepEntities.add(step); + } + // 4) 批量落步骤表,初始状态统一为 PENDING + initTaskStepRepository.saveBatch(stepEntities); + + InitCreateTaskResponse response = new InitCreateTaskResponse(); + response.setTaskId(taskId); + response.setStatus(entity.getStatus()); + response.setTaskType(entity.getTaskType()); + response.setResolvedProductType(productType); + response.setPresetVersion(cisdPresetProperties.getVersion()); + return response; + } + + public ResetCreateTaskResponse createResetTask() { + ResolvedResetContext context = resolveCurrentResetContext(); + String productType = context.productType(); + String taskId = buildTaskId(TASK_TYPE_RESET); + String normalizedMqType = context.mqType(); + LocalDateTime now = LocalDateTime.now(); + List stepCodes = resetSteps(productType, normalizedMqType); + + InitTaskEntity entity = new InitTaskEntity(); + entity.setTaskId(taskId); + entity.setTaskType(TASK_TYPE_RESET); + entity.setVersion(cisdPresetProperties.getVersion()); + entity.setStatus("PENDING"); + entity.setProductType(productType); + entity.setOrgCode(context.orgCode()); + entity.setMqType(normalizedMqType); + entity.setDeployMode(""); + entity.setCreateTime(now); + entity.setUpdateTime(now); + entity.setInitPlanJson(generateResetPlanJson(context, stepCodes)); + initTaskRepository.save(entity); + + List stepEntities = new ArrayList<>(); + for (int i = 0; i < stepCodes.size(); i++) { + String stepCode = stepCodes.get(i); + InitTaskStepEntity step = new InitTaskStepEntity(); + step.setTaskId(taskId); + step.setStepNo(i + 1); + step.setStepCode(stepCode); + step.setStatus("PENDING"); + step.setMessage("waiting"); + step.setCommandLine(initCommandProfileService.resolveCommand(productType, normalizedMqType, stepCode)); + step.setCreateTime(now); + step.setUpdateTime(now); + stepEntities.add(step); + } + initTaskStepRepository.saveBatch(stepEntities); + + ResetCreateTaskResponse response = new ResetCreateTaskResponse(); + response.setTaskId(taskId); + response.setStatus(entity.getStatus()); + response.setTaskType(entity.getTaskType()); + response.setResolvedProductType(productType); + response.setPresetVersion(cisdPresetProperties.getVersion()); + return response; + } + + public DeviceInitStateSnapshot getDeviceInitStateSnapshot() { + Optional running = initTaskRepository.findLatestByStatus("RUNNING"); + if (running.isPresent()) { + InitTaskEntity task = running.get(); + String taskType = normalize(task.getTaskType()); + if (TASK_TYPE_RESET.equals(taskType)) { + return new DeviceInitStateSnapshot("RESETTING", TASK_TYPE_RESET, task.getStatus(), null, null); + } + return new DeviceInitStateSnapshot("INITIALIZING", TASK_TYPE_INIT, task.getStatus(), null, null); + } + + Optional latestInit = initTaskRepository.findLatestByTaskTypeAndStatus(TASK_TYPE_INIT, "SUCCESS"); + Optional latestReset = initTaskRepository.findLatestByTaskTypeAndStatus(TASK_TYPE_RESET, "SUCCESS"); + if (latestInit.isEmpty()) { + return new DeviceInitStateSnapshot("UNINITIALIZED", null, null, null, null); + } + if (latestReset.isPresent() && compareTaskRecency(latestReset.get(), latestInit.get()) >= 0) { + return new DeviceInitStateSnapshot("UNINITIALIZED", TASK_TYPE_RESET, latestReset.get().getStatus(), null, null); + } + + ResolvedResetContext context = resolveResetContextFromInitTask(latestInit.get()); + return new DeviceInitStateSnapshot("INITIALIZED", TASK_TYPE_INIT, latestInit.get().getStatus(), context.orgCode(), context.mqType()); + } + + public CurrentInitConfigResponse loadCurrentInitConfig() { + Optional latestInit = initTaskRepository.findLatestByTaskTypeAndStatus(TASK_TYPE_INIT, "SUCCESS"); + if (latestInit.isEmpty()) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "device is not initialized"); + } + return resolveCurrentInitConfigFromTask(latestInit.get()); + } + + public InitTaskDetailResponse getTask(String taskId) { + InitTaskEntity entity = requireTask(taskId); + + InitTaskDetailResponse response = new InitTaskDetailResponse(); + response.setTaskId(entity.getTaskId()); + response.setStatus(entity.getStatus()); + response.setTaskType(normalize(entity.getTaskType()).isBlank() ? TASK_TYPE_INIT : entity.getTaskType()); + response.setResolvedProductType(entity.getProductType()); + response.setPresetVersion(entity.getVersion()); + response.setOrgCode(entity.getOrgCode()); + response.setMqType(entity.getMqType()); + response.setDeployMode(entity.getDeployMode()); + response.setInitPlanJson(entity.getInitPlanJson()); + return response; + } + + public List getTaskSteps(String taskId) { + requireTask(taskId); + + return initTaskStepRepository.findByTaskIdOrderByStepNo(taskId).stream().map(entity -> { + InitTaskStepResponse response = new InitTaskStepResponse(); + response.setStepNo(entity.getStepNo() == null ? 0 : entity.getStepNo()); + response.setStepCode(entity.getStepCode()); + response.setStatus(entity.getStatus()); + response.setMessage(entity.getMessage()); + response.setCommandLine(entity.getCommandLine()); + response.setLogPath(entity.getLogPath()); + response.setExitCode(entity.getExitCode()); + return response; + }).toList(); + } + + public InitTaskStepLogResponse getTaskStepLog(String taskId, int stepNo) { + if (stepNo <= 0) { + throw new IllegalArgumentException("stepNo must be > 0"); + } + requireTask(taskId); + InitTaskStepEntity step = requireStep(taskId, stepNo); + + if (isBlank(step.getLogPath())) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "log file not generated for task step"); + } + + Path logPath = Paths.get(step.getLogPath()).normalize().toAbsolutePath(); + Path logDir = Paths.get(initExecutorProperties.getLogDir()).normalize().toAbsolutePath(); + // 只允许读取配置日志目录下的文件,防止路径穿越读取任意系统文件 + if (!logPath.startsWith(logDir)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "log path is out of allowed directory"); + } + if (!Files.exists(logPath) || !Files.isRegularFile(logPath)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "log file does not exist"); + } + + try { + String content = Files.readString(logPath); + InitTaskStepLogResponse response = new InitTaskStepLogResponse(); + response.setTaskId(taskId); + response.setStepNo(stepNo); + response.setStepCode(step.getStepCode()); + response.setLogPath(logPath.toString()); + response.setContent(content); + return response; + } catch (IOException ex) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "failed to read log file"); + } + } + + public InitTaskExecuteResponse executeTask(String taskId) { + return executeTaskInternal(taskId); + } + + public InitTaskExecuteResponse executeResetTask(String taskId) { + InitTaskEntity task = requireTask(taskId); + String taskType = normalize(task.getTaskType()); + if (!TASK_TYPE_RESET.equals(taskType)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "reset task not found: " + taskId); + } + return executeTaskInternal(taskId); + } + + private InitTaskExecuteResponse executeTaskInternal(String taskId) { + InitTaskEntity task = requireTask(taskId); + List steps = initTaskStepRepository.findByTaskIdOrderByStepNo(taskId); + 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); + } + + synchronized (activeExecutions) { + CompletableFuture 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 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 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); + + 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()); + } + + step.setExitCode(result.getExitCode()); + step.setLogPath(result.getLogPath()); + step.setMessage(result.getMessage()); + step.setUpdateTime(LocalDateTime.now()); + step.setStatus(result.isSuccess() ? "SUCCESS" : "FAILED"); + initTaskStepRepository.updateBatch(steps); + if (!result.isSuccess()) { + failed = true; + break; + } + } + + task.setStatus(failed ? "FAILED" : "SUCCESS"); + task.setUpdateTime(LocalDateTime.now()); + initTaskRepository.update(task); + } + + private void resetResidualRunningSteps(List 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 steps) { + InitTaskExecuteResponse response = new InitTaskExecuteResponse(); + response.setTaskId(task.getTaskId()); + response.setStatus(task.getStatus()); + response.setTotalSteps(steps.size()); + response.setSuccessSteps((int) steps.stream().filter(step -> "SUCCESS".equals(step.getStatus())).count()); + return response; + } + + private InitTaskEntity requireTask(String taskId) { + return initTaskRepository.findByTaskId(taskId) + .orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "init task not found: " + taskId)); + } + + private InitTaskStepEntity requireStep(String taskId, int stepNo) { + return initTaskStepRepository.findByTaskIdOrderByStepNo(taskId).stream() + .filter(item -> item.getStepNo() != null && item.getStepNo() == stepNo) + .findFirst() + .orElseThrow(() -> new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "init task step not found: taskId=" + taskId + ", stepNo=" + stepNo + )); + } + + private String generateInitPlanJson(String productType, InitPreviewRequest request, List steps) { + Map root = new LinkedHashMap<>(); + root.put("schemaVersion", "1.0"); + root.put("productType", productType); + root.put("orgCode", request.getOrgCode()); + root.put("deployMode", normalizeUpper(request.getDeployMode())); + root.put("mqType", normalizeUpper(request.getMq().getMqType())); + // 执行 internal::render_config / internal::cfmq_apply 时需要读取原始请求参数进行文件渲染与校验。 + root.put("request", request); + root.put("steps", steps); + root.put("generatedAt", LocalDateTime.now().toString()); + + try { + return objectMapper.writeValueAsString(root); + } catch (JsonProcessingException ex) { + throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to serialize init plan"); + } + } + + private String generateResetPlanJson(ResolvedResetContext context, List steps) { + Map root = new LinkedHashMap<>(); + root.put("schemaVersion", "1.0"); + root.put("taskType", TASK_TYPE_RESET); + root.put("productType", context.productType()); + root.put("orgCode", context.orgCode()); + root.put("mqType", context.mqType()); + root.put("sourceTaskId", context.sourceTaskId()); + Map request = new LinkedHashMap<>(); + request.put("orgCode", context.orgCode()); + request.put("mqType", context.mqType()); + request.put("channelUsername", context.channelUsername()); + root.put("request", request); + root.put("steps", steps); + root.put("generatedAt", LocalDateTime.now().toString()); + + try { + return objectMapper.writeValueAsString(root); + } catch (JsonProcessingException ex) { + throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to serialize reset plan"); + } + } + + private List defaultSteps(String productType, String mqType) { + // 该步骤序列只描述“逻辑编排”,具体脚本命令在 step-command-profiles.json 中定义 + if ("DIRECT".equals(productType)) { + if ("RABBITMQ_CFMQ".equals(mqType)) { + return List.of("RENDER_CONFIG", "FILE_PREPARE", "MQ_USER_APPLY", "DB_INIT", "CFMQ_APPLY", "VERIFY"); + } + return List.of("RENDER_CONFIG", "FILE_PREPARE", "MQ_USER_APPLY", "DB_INIT", "TLQ_INIT_CFG", "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" + ); + } + 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 List resetSteps(String productType, String mqType) { + if ("DIRECT".equals(productType)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "reset is not supported for current product type: " + productType); + } + return List.of( + "STOP_APPS", + "DB_DROP_CMEP", + "DB_DROP_ORG", + "MQ_CLEANUP", + "STOP_RABBITMQ", + "TLQ_LICENSE_CLEANUP", + "CLEAN_STAGING", + "VERIFY_RESET" + ); + } + + private String buildTaskId(String taskType) { + String ts = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); + String suffix = UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase(Locale.ROOT); + String prefix = TASK_TYPE_RESET.equals(taskType) ? TASK_TYPE_RESET : TASK_TYPE_INIT; + return prefix + "-" + ts + "-" + suffix; + } + + private String resolvePresetProductType() { + String raw = normalize(cisdPresetProperties.getProductType()); + if (isBlank(raw)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "device preset product type is missing, initialization is forbidden"); + } + + String productType = raw.toUpperCase(Locale.ROOT); + if (!PRESET_PRODUCT_TYPES.contains(productType)) { + throw new BizException( + ErrorCode.BIZ_ERROR.getCode(), + "device preset product type is invalid: " + raw + ", initialization is forbidden" + ); + } + return productType; + } + + private String resolveSupportedResetProductType() { + String productType = resolvePresetProductType(); + if ("DIRECT".equals(productType)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "reset is not supported for current product type: " + productType); + } + return productType; + } + + private void validateCommon(InitPreviewRequest request) { + if (request.getNodes() == null) { + throw new IllegalArgumentException("nodes is required"); + } + if (request.getMq() == null) { + throw new IllegalArgumentException("mq is required"); + } + if (request.getLicenses() == null) { + throw new IllegalArgumentException("licenses is required"); + } + + requireNotBlank("orgCode", request.getOrgCode()); + requireNotBlank("deployMode", request.getDeployMode()); + requireNotBlank("nodes.node01Ip", request.getNodes().getNode01Ip()); + requireNotBlank("mq.mqType", request.getMq().getMqType()); + requireNotBlank("mq.channelUsername", request.getMq().getChannelUsername()); + requireNotBlank("mq.channelPassword", request.getMq().getChannelPassword()); + requireNotBlank("licenses.receiverLicenseFileId", request.getLicenses().getReceiverLicenseFileId()); + requireFileId("licenses.receiverLicenseFileId", request.getLicenses().getReceiverLicenseFileId()); + + requireOptionalFileId("mq.tlqLicenseFileId", request.getMq().getTlqLicenseFileId()); + requireOptionalFileId("mq.cfmqConfigFileId", request.getMq().getCfmqConfigFileId()); + requireOptionalFileId("licenses.cfgZipFileId", request.getLicenses().getCfgZipFileId()); + } + + private void validateByProductType(String productType, InitPreviewRequest request) { + String deployMode = normalizeUpper(request.getDeployMode()); + String mqType = normalizeUpper(request.getMq().getMqType()); + + if ("DIRECT".equals(productType)) { + validateDirect(request, deployMode, mqType); + return; + } + + validateStandard(productType, request, deployMode, mqType); + } + + private ResolvedResetContext resolveCurrentResetContext() { + String productType = resolveSupportedResetProductType(); + Optional latestInit = initTaskRepository.findLatestByTaskTypeAndStatus(TASK_TYPE_INIT, "SUCCESS"); + if (latestInit.isEmpty()) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "device is not initialized, reset is forbidden"); + } + Optional latestReset = initTaskRepository.findLatestByTaskTypeAndStatus(TASK_TYPE_RESET, "SUCCESS"); + if (latestReset.isPresent() && compareTaskRecency(latestReset.get(), latestInit.get()) >= 0) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "device is not initialized, reset is forbidden"); + } + + ResolvedResetContext context = resolveResetContextFromInitTask(latestInit.get()); + if (!productType.equals(context.productType())) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "device preset product type changed, reset context is invalid"); + } + requireAllowed("mqType", context.mqType(), resolveStandardMqAllowed(productType)); + if (("RABBITMQ".equals(context.mqType()) || "RABBITMQ_TLQ".equals(context.mqType())) && isBlank(context.channelUsername())) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "current init context missing rabbitmq channel username"); + } + return context; + } + + private ResolvedResetContext resolveResetContextFromInitTask(InitTaskEntity task) { + if (task == null) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task is missing"); + } + if (isBlank(task.getInitPlanJson())) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task snapshot is missing"); + } + try { + JsonNode root = objectMapper.readTree(task.getInitPlanJson()); + JsonNode request = root.path("request"); + JsonNode mq = request.path("mq"); + String productType = firstNonBlank(normalizeUpper(text(root, "productType")), normalizeUpper(task.getProductType())); + String orgCode = firstNonBlank(text(request, "orgCode"), text(root, "orgCode"), task.getOrgCode()); + String mqType = firstNonBlank(normalizeUpper(text(mq, "mqType")), normalizeUpper(text(root, "mqType")), normalizeUpper(task.getMqType())); + String channelUsername = firstNonBlank(text(mq, "channelUsername"), text(request, "channelUsername")); + + if (isBlank(productType) || isBlank(orgCode) || isBlank(mqType)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task snapshot is incomplete"); + } + return new ResolvedResetContext(productType, normalize(orgCode), normalizeUpper(mqType), normalize(channelUsername), task.getTaskId()); + } catch (JsonProcessingException ex) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "failed to parse successful init task snapshot"); + } + } + + private CurrentInitConfigResponse resolveCurrentInitConfigFromTask(InitTaskEntity task) { + if (task == null) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task is missing"); + } + if (isBlank(task.getInitPlanJson())) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task snapshot is missing"); + } + try { + JsonNode root = objectMapper.readTree(task.getInitPlanJson()); + JsonNode request = root.path("request"); + JsonNode mq = request.path("mq"); + JsonNode nodes = request.path("nodes"); + JsonNode signServer = request.path("signServer"); + + CurrentInitConfigResponse response = new CurrentInitConfigResponse(); + response.setOrgCodeType(firstNonBlank(normalizeUpper(text(request, "orgCodeType")), normalizeUpper(text(root, "orgCodeType")))); + response.setOrgCode(firstNonBlank(text(request, "orgCode"), text(root, "orgCode"), task.getOrgCode())); + response.setOrgNameCn(firstNonBlank(text(request, "orgNameCn"), text(root, "orgNameCn"))); + response.setOrgNameEn(firstNonBlank(text(request, "orgNameEn"), text(root, "orgNameEn"))); + response.setProductType(firstNonBlank(normalizeUpper(text(root, "productType")), normalizeUpper(task.getProductType()))); + response.setDeployMode(firstNonBlank(normalizeUpper(text(request, "deployMode")), normalizeUpper(task.getDeployMode()))); + response.setMqType(firstNonBlank(normalizeUpper(text(mq, "mqType")), normalizeUpper(text(root, "mqType")), normalizeUpper(task.getMqType()))); + response.setChannelUsername(firstNonBlank(text(mq, "channelUsername"), text(request, "channelUsername"))); + response.setNode01Ip(firstNonBlank(text(nodes, "node01Ip"), text(request, "node01Ip"))); + response.setNode02Ip(firstNonBlank(text(nodes, "node02Ip"), text(request, "node02Ip"))); + // 保留初始化快照中的签名服务配置,便于设备画像和联调页面直接复用。 + response.setSignHost(firstNonBlank(text(signServer, "signHost"), text(request, "signHost"))); + response.setSignPort(integerValue(firstNonBlank(text(signServer, "signPort"), text(request, "signPort")))); + response.setSignType(firstNonBlank(text(signServer, "signType"), text(request, "signType"))); + return response; + } catch (JsonProcessingException ex) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "failed to parse successful init task snapshot"); + } + } + + private int compareTaskRecency(InitTaskEntity left, InitTaskEntity right) { + Comparator comparator = Comparator.nullsFirst(LocalDateTime::compareTo); + int byUpdate = comparator.compare(left.getUpdateTime(), right.getUpdateTime()); + if (byUpdate != 0) { + return byUpdate; + } + int byCreate = comparator.compare(left.getCreateTime(), right.getCreateTime()); + if (byCreate != 0) { + return byCreate; + } + return normalize(left.getTaskId()).compareTo(normalize(right.getTaskId())); + } + + private void validateStandard(String productType, InitPreviewRequest request, String deployMode, String mqType) { + requireAllowed("deployMode", deployMode, DEPLOY_STANDARD); + requireAllowed("mq.mqType", mqType, resolveStandardMqAllowed(productType)); + + if ("DUAL".equals(deployMode)) { + requireNotBlank("nodes.node02Ip", request.getNodes().getNode02Ip()); + } + + if ("TLQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType)) { + requireNotBlank("mq.tlqLicenseFileId", request.getMq().getTlqLicenseFileId()); + requireFileId("mq.tlqLicenseFileId", request.getMq().getTlqLicenseFileId()); + } + } + + private Set 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); + + if ("DUAL".equals(deployMode) || "QUAD".equals(deployMode)) { + requireNotBlank("nodes.node02Ip", request.getNodes().getNode02Ip()); + } + if ("QUAD".equals(deployMode)) { + requireNotBlank("nodes.node03Ip", request.getNodes().getNode03Ip()); + requireNotBlank("nodes.node04Ip", request.getNodes().getNode04Ip()); + } + + requireNotBlank("licenses.cfgZipFileId", request.getLicenses().getCfgZipFileId()); + requireFileId("licenses.cfgZipFileId", request.getLicenses().getCfgZipFileId()); + + InitPreviewRequest.SignServer signServer = request.getSignServer(); + if (signServer == null) { + throw new IllegalArgumentException("signServer is required for DIRECT"); + } + requireNotBlank("signServer.signHost", signServer.getSignHost()); + if (signServer.getSignPort() == null || signServer.getSignPort() <= 0 || signServer.getSignPort() > 65535) { + throw new IllegalArgumentException("signServer.signPort must be in [1,65535]"); + } + requireNotBlank("signServer.signType", signServer.getSignType()); + requireNotBlank("signServer.signCertDn", signServer.getSignCertDn()); + requireNotBlank("signServer.signCertSn", signServer.getSignCertSn()); + requireNotBlank("signServer.signPassword", signServer.getSignPassword()); + + if ("RABBITMQ_TLQ".equals(mqType)) { + requireNotBlank("mq.tlqLicenseFileId", request.getMq().getTlqLicenseFileId()); + requireFileId("mq.tlqLicenseFileId", request.getMq().getTlqLicenseFileId()); + InitPreviewRequest.DirectTlqConfig directTlq = request.getDirectTlq(); + if (directTlq == null) { + throw new IllegalArgumentException("directTlq is required for RABBITMQ_TLQ"); + } + requireNotBlank("directTlq.localNodeSeq", directTlq.getLocalNodeSeq()); + requireNotBlank("directTlq.upstreamInstitutions", directTlq.getUpstreamInstitutions()); + if (directTlq.getUpstreamCocCount() == null + || directTlq.getUpstreamCocCount() < 1 + || directTlq.getUpstreamCocCount() > 2) { + throw new IllegalArgumentException("directTlq.upstreamCocCount must be in [1,2]"); + } + } + if ("RABBITMQ_CFMQ".equals(mqType)) { + requireNotBlank("mq.cfmqConfigFileId", request.getMq().getCfmqConfigFileId()); + requireFileId("mq.cfmqConfigFileId", request.getMq().getCfmqConfigFileId()); + } + } + + private int estimateStepCount(String productType, InitPreviewRequest request) { + String mqType = normalizeUpper(request.getMq().getMqType()); + return defaultSteps(productType, mqType).size(); + } + + private static void requireAllowed(String field, String value, Set allowed) { + if (!allowed.contains(value)) { + throw new IllegalArgumentException(field + " must be one of " + allowed); + } + } + + private static void requireNotBlank(String field, String value) { + if (isBlank(value)) { + throw new IllegalArgumentException(field + " is required"); + } + } + + private static void requireOptionalFileId(String field, String value) { + if (!isBlank(value)) { + requireFileId(field, value); + } + } + + private static void requireFileId(String field, String value) { + if (isBlank(value) || !FILE_ID_PATTERN.matcher(normalize(value)).matches()) { + throw new IllegalArgumentException(field + " must be a fileId"); + } + } + + private static String firstNonBlank(String... values) { + if (values == null) { + return ""; + } + for (String value : values) { + if (!isBlank(value)) { + return normalize(value); + } + } + return ""; + } + + private static String text(JsonNode node, String fieldName) { + if (node == null || fieldName == null || node.isMissingNode() || node.isNull()) { + return ""; + } + JsonNode child = node.path(fieldName); + if (child.isMissingNode() || child.isNull()) { + return ""; + } + if (child.isTextual()) { + return normalize(child.asText()); + } + return normalize(child.toString()); + } + + private static String normalizeUpper(String value) { + return normalize(value).toUpperCase(Locale.ROOT); + } + + private static String normalize(String value) { + return value == null ? "" : value.trim(); + } + + private static Integer integerValue(String value) { + String normalized = normalize(value); + if (isBlank(normalized)) { + return null; + } + try { + return Integer.valueOf(normalized); + } catch (NumberFormatException ex) { + return null; + } + } + + private static boolean isBlank(String value) { + return value == null || value.isBlank(); + } + + private record ResolvedResetContext( + String productType, + String orgCode, + String mqType, + String channelUsername, + String sourceTaskId + ) { + } +} diff --git a/src/main/java/com/cisd/tms/modules/mk/controller/LmkController.java b/src/main/java/com/cisd/tms/modules/mk/controller/LmkController.java index 2af8a50..42e20a9 100644 --- a/src/main/java/com/cisd/tms/modules/mk/controller/LmkController.java +++ b/src/main/java/com/cisd/tms/modules/mk/controller/LmkController.java @@ -9,6 +9,7 @@ import com.cisd.tms.modules.mk.service.LmkService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; @@ -19,15 +20,12 @@ import org.springframework.web.bind.annotation.*; @Slf4j @RestController @RequestMapping("/api/v1") +@RequiredArgsConstructor @Tag(name = "主密钥管理", description = "LMK 生成、恢复、状态查询和销毁接口") public class LmkController { private final LmkService lmkService; - public LmkController(LmkService lmkService, com.fasterxml.jackson.databind.ObjectMapper objectMapper) { - this.lmkService = lmkService; - } - @PostMapping("/masterKey/init") @Operation(summary = "初始化主密钥", description = "执行密码卡初始化识别、主密钥生成加载、内部密钥生成,并返回主密钥状态与校验值。") public ApiResponse initMasterKey() { diff --git a/src/main/java/com/cisd/tms/modules/mk/service/LmkService.java b/src/main/java/com/cisd/tms/modules/mk/service/LmkService.java index e16ee8d..ba9b3cf 100644 --- a/src/main/java/com/cisd/tms/modules/mk/service/LmkService.java +++ b/src/main/java/com/cisd/tms/modules/mk/service/LmkService.java @@ -1,400 +1,35 @@ package com.cisd.tms.modules.mk.service; -import com.cisd.tms.common.util.DivisionUtils; -import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds; -import com.cisd.tms.integration.crypto.pcie.jna.EccRefPublicKey; -import com.cisd.tms.integration.crypto.pcie.jna.EccSignature; -import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult; -import com.cisd.tms.integration.crypto.pcie.model.DigestRequest; -import com.cisd.tms.integration.crypto.pcie.model.EccInternalSignRequest; -import com.cisd.tms.integration.crypto.pcie.model.EccInternalVerifyRequest; -import com.cisd.tms.integration.crypto.pcie.model.RecoverUserKeyRequest; -import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; -import com.cisd.tms.modules.mk.config.MasterKeyInitProperties; -import com.cisd.tms.modules.mk.common.LMKConstant; -import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket; import com.cisd.tms.modules.mk.dto.MasterKeyBackupMaterial; -import com.cisd.tms.modules.mk.dto.MasterKeyPacketIkPart; -import com.cisd.tms.modules.mk.dto.MasterKeyPacketLmkPart; -import com.cisd.tms.modules.mk.dto.MasterKeyPacketUserKeyPart; +import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket; import com.cisd.tms.modules.mk.dto.MasterKeyStateResult; -import com.cisd.tms.modules.mk.enums.IKEnums; +import com.cisd.tms.modules.mk.dto.RecoveryResult; import com.cisd.tms.modules.mk.enums.MasterKeyStatus; -import lombok.extern.slf4j.Slf4j; -import org.bouncycastle.util.encoders.Hex; -import org.springframework.stereotype.Service; - -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Base64; -import java.util.HashMap; import java.util.List; -import java.util.Map; -/** - * @author liulu - * @since 2026/3/5 - */ -@Slf4j -@Service -public class LmkService { +public interface LmkService { - private static final int AUTH_SIGN_KEY_INDEX = IKEnums.KEY_TYPE_AUTH.getCode(); - private static final int LMK_MAC_BYTE_LENGTH = 8; - private static final int IK_COMPONENT_MAC_BYTE_LENGTH = 8; - private static final int IK_COMPONENT_DATA_LENGTH = 3 * 64; - private static final int USER_KEY_INDEX = 1; - private static final int USER_KEY_TYPE_SIGN = 0; - private static final int USER_KEY_TYPE_ENC = 1; - private static final int USER_KEY_BACKUP_BUFFER_SIZE = 120; - private final PcieCryptoService pcieCryptoService; - private final MasterKeyInitProperties masterKeyInitProperties; + MasterKeyStateResult initMasterKey(); - public LmkService(PcieCryptoService pcieCryptoService, MasterKeyInitProperties masterKeyInitProperties) { - this.pcieCryptoService = pcieCryptoService; - this.masterKeyInitProperties = masterKeyInitProperties; - } + void destroyMasterKey(); + MasterKeyStateResult queryMasterKeyStatus(); - public MasterKeyStateResult initMasterKey() { - byte[] oldPin = decodeConfiguredPin("tms.mk.init-identify.old-pin-base64", masterKeyInitProperties.getOldPinBase64()); - byte[] newPin = decodeConfiguredPin("tms.mk.init-identify.new-pin-base64", masterKeyInitProperties.getNewPinBase64()); + String exportIkPublicKeyHex(); - MasterKeyStateResult result = new MasterKeyStateResult(); - result.setStatus(true); - result.setSeedMac(Hex.toHexString(pcieCryptoService.initializeMasterKey(oldPin, newPin))); - return result; - } + String signIk(String rawData); - public void destroyMasterKey() { - pcieCryptoService.destroyLmk(); - } + void verifyIk(String rawData, String signatureHex); - public MasterKeyStateResult queryMasterKeyStatus() { - MasterKeyStateResult result = new MasterKeyStateResult(); - boolean status = pcieCryptoService.checkLmk(); - result.setStatus(status); - if (status) { - result.setSeedMac(Hex.toHexString(pcieCryptoService.exportLmkSeedMac())); - } - return result; - } + MasterKeyBackupPacket buildBackupPacket(int componentIndex); - public String exportIkPublicKeyHex() { - int outBufferSize = new EccRefPublicKey().size(); - BackupDataResult result = pcieCryptoService.exportSignPublicKeyEcc(AUTH_SIGN_KEY_INDEX, outBufferSize); - return Hex.toHexString(result.getData()); - } + MasterKeyBackupMaterial getBackupMaterial(int uid); - public String signIk(String rawData) { - byte[] digest = digestForInternalSign(rawData); - EccInternalSignRequest request = new EccInternalSignRequest(); - request.setKeyIndex(AUTH_SIGN_KEY_INDEX); - request.setData(digest); - request.setSignatureBufferSize(new EccSignature().size()); - BackupDataResult result = pcieCryptoService.eccInternalSign(request); - return Hex.toHexString(result.getData()); - } + String getComponent(int uid); - public void verifyIk(String rawData, String signatureHex) { - byte[] digest = digestForInternalSign(rawData); - EccInternalVerifyRequest request = new EccInternalVerifyRequest(); - request.setKeyIndex(AUTH_SIGN_KEY_INDEX); - request.setData(digest); - request.setSignature(Hex.decode(signatureHex)); - pcieCryptoService.eccInternalVerify(request); - } + void recoverKeyPackets(List packets); - public MasterKeyBackupPacket buildBackupPacket(int componentIndex) { - int index = requireComponentIndex(componentIndex); - ParsedMasterKeyBackup lmkBackup = parseBackupMaterial(); - ParsedIkBackup authIkBackup = parseIkBackupMaterial(IKEnums.KEY_TYPE_AUTH.getCode()); - ParsedIkBackup deviceIkBackup = parseIkBackupMaterial(IKEnums.KEY_TYPE_DEVICE.getCode()); - - MasterKeyBackupPacket packet = new MasterKeyBackupPacket(); - packet.setComponentIndex(index); - packet.setLmk(new MasterKeyPacketLmkPart( - lmkBackup.lmkMacHex(), - requiredComponent(lmkBackup.componentMap(), index, "lmk") - )); - packet.setIkAuth(new MasterKeyPacketIkPart( - IKEnums.KEY_TYPE_AUTH.getCode(), - authIkBackup.macHex(), - requiredComponent(authIkBackup.componentMap(), index, "ikAuth") - )); - packet.setIkDevice(new MasterKeyPacketIkPart( - IKEnums.KEY_TYPE_DEVICE.getCode(), - deviceIkBackup.macHex(), - requiredComponent(deviceIkBackup.componentMap(), index, "ikDevice") - )); - packet.setUserKeys(List.of( - buildUserKeyPacket(USER_KEY_INDEX, USER_KEY_TYPE_SIGN, index), - buildUserKeyPacket(USER_KEY_INDEX, USER_KEY_TYPE_ENC, index) - )); - return packet; - } - - public MasterKeyBackupMaterial getBackupMaterial(int uid) { - MasterKeyBackupPacket packet = buildBackupPacket(uid); - return new MasterKeyBackupMaterial(packet.getLmk().getComponent(), packet.getLmk().getLmkMac()); - } - - public String getComponent(int uid) { - return getBackupMaterial(uid).getComponent(); - } - - public void recoverKeyPackets(List packets) { - List orderedPackets = sortPackets(packets); - byte[] oldPin = decodeConfiguredPin("tms.mk.init-identify.old-pin-base64", masterKeyInitProperties.getOldPinBase64()); - byte[] newPin = decodeConfiguredPin("tms.mk.init-identify.new-pin-base64", masterKeyInitProperties.getNewPinBase64()); - - pcieCryptoService.initIdentify(oldPin, newPin); - pcieCryptoService.recoverLmkEx(Hex.decode(composeLmkHex(orderedPackets))); - pcieCryptoService.loadLmk(); - pcieCryptoService.recoverIkComponent(IKEnums.KEY_TYPE_AUTH.getCode(), Hex.decode(composeIkHex(orderedPackets, true))); - pcieCryptoService.recoverIkComponent(IKEnums.KEY_TYPE_DEVICE.getCode(), Hex.decode(composeIkHex(orderedPackets, false))); - pcieCryptoService.recoverUserKey(toRecoverUserKeyRequest(orderedPackets, USER_KEY_INDEX, USER_KEY_TYPE_SIGN)); - pcieCryptoService.recoverUserKey(toRecoverUserKeyRequest(orderedPackets, USER_KEY_INDEX, USER_KEY_TYPE_ENC)); - } - - public void recoverLmkEx(String lmkMacHex, List components) { - byte[] oldPin = decodeConfiguredPin("tms.mk.init-identify.old-pin-base64", masterKeyInitProperties.getOldPinBase64()); - byte[] newPin = decodeConfiguredPin("tms.mk.init-identify.new-pin-base64", masterKeyInitProperties.getNewPinBase64()); - String fullLmkHex = masterKeyCompose(lmkMacHex, components); - pcieCryptoService.initIdentify(oldPin, newPin); - pcieCryptoService.recoverLmkEx(Hex.decode(fullLmkHex)); - pcieCryptoService.loadLmk(); - } - - - public MasterKeyStatus.StatusDetail getMasterKeyStatus() { - boolean ok = pcieCryptoService.checkLmk(); - MasterKeyStatus status = ok ? MasterKeyStatus.NORMAL : MasterKeyStatus.ABNORMAL; - if (ok) { - String mac = Hex.toHexString(pcieCryptoService.exportLmkSeedMac()); - return status.getDetail(mac); - } - return status.getDetail(); - } - - private String masterKeyCompose(String lmkMacHex, List components) { - if (lmkMacHex == null || lmkMacHex.isBlank()) { - throw new IllegalArgumentException("lmkMac must not be blank"); - } - if (components == null || components.size() != LMKConstant.COMPONENT_NUM) { - throw new IllegalArgumentException("master key components size must be " + LMKConstant.COMPONENT_NUM); - } - StringBuilder lmk = new StringBuilder(lmkMacHex); - for (int index = 0; index < LMKConstant.COMPONENT_NUM; index++) { - String component = components.get(index); - if (component == null || component.isBlank()) { - throw new IllegalArgumentException("master key component missing: " + (index + 1)); - } - lmk.append(component); - } - return lmk.toString(); - } - - private String composeLmkHex(List packets) { - String lmkMac = requireConsistentLmkMac(packets); - StringBuilder builder = new StringBuilder(lmkMac); - for (MasterKeyBackupPacket packet : packets) { - builder.append(packet.getLmk().getComponent()); - } - return builder.toString(); - } - - private String composeIkHex(List packets, boolean authKey) { - int keyType = authKey ? IKEnums.KEY_TYPE_AUTH.getCode() : IKEnums.KEY_TYPE_DEVICE.getCode(); - String mac = requireConsistentIkMac(packets, keyType); - StringBuilder builder = new StringBuilder(); - for (MasterKeyBackupPacket packet : packets) { - MasterKeyPacketIkPart part = authKey ? packet.getIkAuth() : packet.getIkDevice(); - if (part == null || part.getComponent() == null || part.getComponent().isBlank()) { - throw new IllegalArgumentException((authKey ? "ikAuth" : "ikDevice") + " component missing in packet " + packet.getComponentIndex()); - } - builder.append(part.getComponent()); - } - builder.append(mac); - return builder.toString(); - } - - private RecoverUserKeyRequest toRecoverUserKeyRequest(List packets, int keyIndex, int keyType) { - RecoverUserKeyRequest request = new RecoverUserKeyRequest(); - request.setKeyIndex(keyIndex); - request.setKeyType(keyType); - request.setEncryptedKey(Hex.decode(composeUserKeyHex(packets, keyIndex, keyType))); - return request; - } - - private String composeUserKeyHex(List packets, int keyIndex, int keyType) { - StringBuilder builder = new StringBuilder(); - for (MasterKeyBackupPacket packet : packets) { - MasterKeyPacketUserKeyPart part = findUserKeyPart(packet, keyIndex, keyType); - builder.append(part.getComponent()); - } - return builder.toString(); - } - - private ParsedMasterKeyBackup parseBackupMaterial() { - BackupDataResult backupData = pcieCryptoService.backupLmkEx(); - byte[] normalized = normalizeBackupData(backupData); - if (normalized.length < LMK_MAC_BYTE_LENGTH) { - throw new IllegalStateException("backup lmk data length is too short: " + normalized.length); - } - byte[] lmkMac = Arrays.copyOfRange(normalized, 0, LMK_MAC_BYTE_LENGTH); - byte[] componentBytes = Arrays.copyOfRange(normalized, LMK_MAC_BYTE_LENGTH, normalized.length); - String[] splitComponents = DivisionUtils.equalDivision(Hex.toHexString(componentBytes), LMKConstant.COMPONENT_NUM); - Map componentMap = new HashMap<>(); - for (int i = 0; i < LMKConstant.COMPONENT_NUM; i++) { - componentMap.put(LMKConstant.KEY_ORDER[i], splitComponents[i]); - } - return new ParsedMasterKeyBackup(Hex.toHexString(lmkMac), componentMap); - } - - private ParsedIkBackup parseIkBackupMaterial(int keyType) { - BackupDataResult backupData = pcieCryptoService.backupIkComponent(keyType); - byte[] normalized = normalizeBackupData(backupData); - if (normalized.length < IK_COMPONENT_DATA_LENGTH + IK_COMPONENT_MAC_BYTE_LENGTH) { - throw new IllegalStateException("backup ik data length is too short: " + normalized.length); - } - byte[] componentBytes = Arrays.copyOfRange(normalized, 0, IK_COMPONENT_DATA_LENGTH); - byte[] mac = Arrays.copyOfRange(normalized, IK_COMPONENT_DATA_LENGTH, IK_COMPONENT_DATA_LENGTH + IK_COMPONENT_MAC_BYTE_LENGTH); - String[] splitComponents = DivisionUtils.equalDivision(Hex.toHexString(componentBytes), LMKConstant.COMPONENT_NUM); - Map componentMap = new HashMap<>(); - for (int i = 0; i < LMKConstant.COMPONENT_NUM; i++) { - componentMap.put(LMKConstant.KEY_ORDER[i], splitComponents[i]); - } - return new ParsedIkBackup(Hex.toHexString(mac), componentMap); - } - - private MasterKeyPacketUserKeyPart buildUserKeyPacket(int keyIndex, int keyType, int componentIndex) { - BackupDataResult backupData = pcieCryptoService.backupUserKey(keyIndex, keyType, USER_KEY_BACKUP_BUFFER_SIZE); - byte[] normalized = normalizeBackupData(backupData); - String[] components = DivisionUtils.equalDivision(Hex.toHexString(normalized), LMKConstant.COMPONENT_NUM); - return new MasterKeyPacketUserKeyPart(keyIndex, keyType, components[componentIndex - 1]); - } - - private byte[] normalizeBackupData(BackupDataResult backupData) { - byte[] data = backupData.getData(); - if (data == null || data.length == 0) { - return new byte[0]; - } - int length = backupData.getLength(); - int safeLength = length > 0 && length <= data.length ? length : data.length; - return Arrays.copyOf(data, safeLength); - } - - private byte[] decodeConfiguredPin(String fieldName, String base64Value) { - if (base64Value == null || base64Value.isBlank()) { - throw new IllegalStateException(fieldName + " is not configured"); - } - try { - return Base64.getDecoder().decode(base64Value); - } catch (IllegalArgumentException e) { - throw new IllegalStateException(fieldName + " is not valid Base64", e); - } - } - - private byte[] digestForInternalSign(String rawData) { - DigestRequest request = new DigestRequest(); - request.setAlgId(Gm0018AlgorithmIds.SM3); - request.setData(rawData.getBytes(StandardCharsets.UTF_8)); - BackupDataResult result = pcieCryptoService.digest(request); - return normalizeBackupData(result); - } - - private int requireComponentIndex(int componentIndex) { - if (componentIndex < 1 || componentIndex > LMKConstant.COMPONENT_NUM) { - throw new IllegalArgumentException("componentIndex must be 1, 2 or 3"); - } - return componentIndex; - } - - private String requiredComponent(Map componentMap, int componentIndex, String name) { - String component = componentMap.get(componentIndex); - if (component == null || component.isBlank()) { - throw new IllegalArgumentException(name + " component missing: " + componentIndex); - } - return component; - } - - private List sortPackets(List packets) { - if (packets == null || packets.size() != LMKConstant.COMPONENT_NUM) { - throw new IllegalArgumentException("packets size must be " + LMKConstant.COMPONENT_NUM); - } - Map packetMap = new HashMap<>(); - for (MasterKeyBackupPacket packet : packets) { - if (packet == null) { - throw new IllegalArgumentException("packet must not be null"); - } - int componentIndex = requireComponentIndex(packet.getComponentIndex()); - if (packetMap.put(componentIndex, packet) != null) { - throw new IllegalArgumentException("duplicate packet for componentIndex " + componentIndex); - } - } - List ordered = new ArrayList<>(); - for (int componentIndex : LMKConstant.KEY_ORDER) { - MasterKeyBackupPacket packet = packetMap.get(componentIndex); - if (packet == null) { - throw new IllegalArgumentException("packet missing for componentIndex " + componentIndex); - } - ordered.add(packet); - } - return ordered; - } - - private String requireConsistentLmkMac(List packets) { - String expected = null; - for (MasterKeyBackupPacket packet : packets) { - if (packet.getLmk() == null || packet.getLmk().getLmkMac() == null || packet.getLmk().getLmkMac().isBlank()) { - throw new IllegalArgumentException("lmk mac missing in packet " + packet.getComponentIndex()); - } - String current = packet.getLmk().getLmkMac(); - if (expected == null) { - expected = current; - continue; - } - if (!expected.equalsIgnoreCase(current)) { - throw new IllegalArgumentException("lmk mac is inconsistent across packets"); - } - } - return expected; - } - - private String requireConsistentIkMac(List packets, int keyType) { - String expected = null; - for (MasterKeyBackupPacket packet : packets) { - MasterKeyPacketIkPart part = keyType == IKEnums.KEY_TYPE_AUTH.getCode() ? packet.getIkAuth() : packet.getIkDevice(); - if (part == null || part.getMac() == null || part.getMac().isBlank()) { - throw new IllegalArgumentException("ik mac missing for keyType " + keyType); - } - if (expected == null) { - expected = part.getMac(); - continue; - } - if (!expected.equalsIgnoreCase(part.getMac())) { - throw new IllegalArgumentException("ik mac is inconsistent across packets for keyType " + keyType); - } - } - return expected; - } - - private MasterKeyPacketUserKeyPart findUserKeyPart(MasterKeyBackupPacket packet, int keyIndex, int keyType) { - if (packet.getUserKeys() == null) { - throw new IllegalArgumentException("userKeys missing in packet " + packet.getComponentIndex()); - } - return packet.getUserKeys().stream() - .filter(part -> part.getKeyIndex() == keyIndex && part.getKeyType() == keyType) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("user key component missing in packet " + packet.getComponentIndex())); - } - - private record ParsedMasterKeyBackup(String lmkMacHex, Map componentMap) { - } - - private record ParsedIkBackup(String macHex, Map componentMap) { - } + void recoverLmkEx(String lmkMacHex, List components); + MasterKeyStatus.StatusDetail getMasterKeyStatus(); } diff --git a/src/main/java/com/cisd/tms/modules/mk/service/impl/LmkServiceImpl.java b/src/main/java/com/cisd/tms/modules/mk/service/impl/LmkServiceImpl.java new file mode 100644 index 0000000..a50fa6b --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/mk/service/impl/LmkServiceImpl.java @@ -0,0 +1,395 @@ +package com.cisd.tms.modules.mk.service.impl; + +import com.cisd.tms.common.util.DivisionUtils; +import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds; +import com.cisd.tms.integration.crypto.pcie.jna.EccRefPublicKey; +import com.cisd.tms.integration.crypto.pcie.jna.EccSignature; +import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult; +import com.cisd.tms.integration.crypto.pcie.model.DigestRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccInternalSignRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccInternalVerifyRequest; +import com.cisd.tms.integration.crypto.pcie.model.RecoverUserKeyRequest; +import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; +import com.cisd.tms.modules.mk.common.LMKConstant; +import com.cisd.tms.modules.mk.config.MasterKeyInitProperties; +import com.cisd.tms.modules.mk.dto.MasterKeyBackupMaterial; +import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket; +import com.cisd.tms.modules.mk.dto.MasterKeyPacketIkPart; +import com.cisd.tms.modules.mk.dto.MasterKeyPacketLmkPart; +import com.cisd.tms.modules.mk.dto.MasterKeyPacketUserKeyPart; +import com.cisd.tms.modules.mk.dto.MasterKeyStateResult; +import com.cisd.tms.modules.mk.enums.IKEnums; +import com.cisd.tms.modules.mk.enums.MasterKeyStatus; +import com.cisd.tms.modules.mk.service.LmkService; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.util.encoders.Hex; +import org.springframework.stereotype.Service; + +/** + * 主密钥生命周期服务实现。 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class LmkServiceImpl implements LmkService { + + private static final int AUTH_SIGN_KEY_INDEX = IKEnums.KEY_TYPE_AUTH.getCode(); + private static final int LMK_MAC_BYTE_LENGTH = 8; + private static final int IK_COMPONENT_MAC_BYTE_LENGTH = 8; + private static final int IK_COMPONENT_DATA_LENGTH = 3 * 64; + private static final int USER_KEY_INDEX = 1; + private static final int USER_KEY_TYPE_SIGN = 0; + private static final int USER_KEY_TYPE_ENC = 1; + private static final int USER_KEY_BACKUP_BUFFER_SIZE = 120; + + private final PcieCryptoService pcieCryptoService; + private final MasterKeyInitProperties masterKeyInitProperties; + + @Override + public MasterKeyStateResult initMasterKey() { + byte[] oldPin = decodeConfiguredPin("tms.mk.init-identify.old-pin-base64", masterKeyInitProperties.getOldPinBase64()); + byte[] newPin = decodeConfiguredPin("tms.mk.init-identify.new-pin-base64", masterKeyInitProperties.getNewPinBase64()); + + MasterKeyStateResult result = new MasterKeyStateResult(); + result.setStatus(true); + result.setSeedMac(Hex.toHexString(pcieCryptoService.initializeMasterKey(oldPin, newPin))); + return result; + } + + @Override + public void destroyMasterKey() { + pcieCryptoService.destroyLmk(); + } + + @Override + public MasterKeyStateResult queryMasterKeyStatus() { + MasterKeyStateResult result = new MasterKeyStateResult(); + boolean status = pcieCryptoService.checkLmk(); + result.setStatus(status); + if (status) { + result.setSeedMac(Hex.toHexString(pcieCryptoService.exportLmkSeedMac())); + } + return result; + } + + @Override + public String exportIkPublicKeyHex() { + int outBufferSize = new EccRefPublicKey().size(); + BackupDataResult result = pcieCryptoService.exportSignPublicKeyEcc(AUTH_SIGN_KEY_INDEX, outBufferSize); + return Hex.toHexString(result.getData()); + } + + @Override + public String signIk(String rawData) { + byte[] digest = digestForInternalSign(rawData); + EccInternalSignRequest request = new EccInternalSignRequest(); + request.setKeyIndex(AUTH_SIGN_KEY_INDEX); + request.setData(digest); + request.setSignatureBufferSize(new EccSignature().size()); + BackupDataResult result = pcieCryptoService.eccInternalSign(request); + return Hex.toHexString(result.getData()); + } + + @Override + public void verifyIk(String rawData, String signatureHex) { + byte[] digest = digestForInternalSign(rawData); + EccInternalVerifyRequest request = new EccInternalVerifyRequest(); + request.setKeyIndex(AUTH_SIGN_KEY_INDEX); + request.setData(digest); + request.setSignature(Hex.decode(signatureHex)); + pcieCryptoService.eccInternalVerify(request); + } + + @Override + public MasterKeyBackupPacket buildBackupPacket(int componentIndex) { + int index = requireComponentIndex(componentIndex); + ParsedMasterKeyBackup lmkBackup = parseBackupMaterial(); + ParsedIkBackup authIkBackup = parseIkBackupMaterial(IKEnums.KEY_TYPE_AUTH.getCode()); + ParsedIkBackup deviceIkBackup = parseIkBackupMaterial(IKEnums.KEY_TYPE_DEVICE.getCode()); + + MasterKeyBackupPacket packet = new MasterKeyBackupPacket(); + packet.setComponentIndex(index); + packet.setLmk(new MasterKeyPacketLmkPart( + lmkBackup.lmkMacHex(), + requiredComponent(lmkBackup.componentMap(), index, "lmk") + )); + packet.setIkAuth(new MasterKeyPacketIkPart( + IKEnums.KEY_TYPE_AUTH.getCode(), + authIkBackup.macHex(), + requiredComponent(authIkBackup.componentMap(), index, "ikAuth") + )); + packet.setIkDevice(new MasterKeyPacketIkPart( + IKEnums.KEY_TYPE_DEVICE.getCode(), + deviceIkBackup.macHex(), + requiredComponent(deviceIkBackup.componentMap(), index, "ikDevice") + )); + packet.setUserKeys(List.of( + buildUserKeyPacket(USER_KEY_INDEX, USER_KEY_TYPE_SIGN, index), + buildUserKeyPacket(USER_KEY_INDEX, USER_KEY_TYPE_ENC, index) + )); + return packet; + } + + @Override + public MasterKeyBackupMaterial getBackupMaterial(int uid) { + MasterKeyBackupPacket packet = buildBackupPacket(uid); + return new MasterKeyBackupMaterial(packet.getLmk().getComponent(), packet.getLmk().getLmkMac()); + } + + @Override + public String getComponent(int uid) { + return getBackupMaterial(uid).getComponent(); + } + + @Override + public void recoverKeyPackets(List packets) { + List orderedPackets = sortPackets(packets); + byte[] oldPin = decodeConfiguredPin("tms.mk.init-identify.old-pin-base64", masterKeyInitProperties.getOldPinBase64()); + byte[] newPin = decodeConfiguredPin("tms.mk.init-identify.new-pin-base64", masterKeyInitProperties.getNewPinBase64()); + + pcieCryptoService.initIdentify(oldPin, newPin); + pcieCryptoService.recoverLmkEx(Hex.decode(composeLmkHex(orderedPackets))); + pcieCryptoService.loadLmk(); + pcieCryptoService.recoverIkComponent(IKEnums.KEY_TYPE_AUTH.getCode(), Hex.decode(composeIkHex(orderedPackets, true))); + pcieCryptoService.recoverIkComponent(IKEnums.KEY_TYPE_DEVICE.getCode(), Hex.decode(composeIkHex(orderedPackets, false))); + pcieCryptoService.recoverUserKey(toRecoverUserKeyRequest(orderedPackets, USER_KEY_INDEX, USER_KEY_TYPE_SIGN)); + pcieCryptoService.recoverUserKey(toRecoverUserKeyRequest(orderedPackets, USER_KEY_INDEX, USER_KEY_TYPE_ENC)); + } + + @Override + public void recoverLmkEx(String lmkMacHex, List components) { + byte[] oldPin = decodeConfiguredPin("tms.mk.init-identify.old-pin-base64", masterKeyInitProperties.getOldPinBase64()); + byte[] newPin = decodeConfiguredPin("tms.mk.init-identify.new-pin-base64", masterKeyInitProperties.getNewPinBase64()); + String fullLmkHex = masterKeyCompose(lmkMacHex, components); + pcieCryptoService.initIdentify(oldPin, newPin); + pcieCryptoService.recoverLmkEx(Hex.decode(fullLmkHex)); + pcieCryptoService.loadLmk(); + } + + @Override + public MasterKeyStatus.StatusDetail getMasterKeyStatus() { + boolean ok = pcieCryptoService.checkLmk(); + MasterKeyStatus status = ok ? MasterKeyStatus.NORMAL : MasterKeyStatus.ABNORMAL; + if (ok) { + String mac = Hex.toHexString(pcieCryptoService.exportLmkSeedMac()); + return status.getDetail(mac); + } + return status.getDetail(); + } + + private String masterKeyCompose(String lmkMacHex, List components) { + if (lmkMacHex == null || lmkMacHex.isBlank()) { + throw new IllegalArgumentException("lmkMac must not be blank"); + } + if (components == null || components.size() != LMKConstant.COMPONENT_NUM) { + throw new IllegalArgumentException("master key components size must be " + LMKConstant.COMPONENT_NUM); + } + StringBuilder lmk = new StringBuilder(lmkMacHex); + for (int index = 0; index < LMKConstant.COMPONENT_NUM; index++) { + String component = components.get(index); + if (component == null || component.isBlank()) { + throw new IllegalArgumentException("master key component missing: " + (index + 1)); + } + lmk.append(component); + } + return lmk.toString(); + } + + private String composeLmkHex(List packets) { + String lmkMac = requireConsistentLmkMac(packets); + StringBuilder builder = new StringBuilder(lmkMac); + for (MasterKeyBackupPacket packet : packets) { + builder.append(packet.getLmk().getComponent()); + } + return builder.toString(); + } + + private String composeIkHex(List packets, boolean authKey) { + int keyType = authKey ? IKEnums.KEY_TYPE_AUTH.getCode() : IKEnums.KEY_TYPE_DEVICE.getCode(); + String mac = requireConsistentIkMac(packets, keyType); + StringBuilder builder = new StringBuilder(); + for (MasterKeyBackupPacket packet : packets) { + MasterKeyPacketIkPart part = authKey ? packet.getIkAuth() : packet.getIkDevice(); + if (part == null || part.getComponent() == null || part.getComponent().isBlank()) { + throw new IllegalArgumentException((authKey ? "ikAuth" : "ikDevice") + " component missing in packet " + packet.getComponentIndex()); + } + builder.append(part.getComponent()); + } + builder.append(mac); + return builder.toString(); + } + + private RecoverUserKeyRequest toRecoverUserKeyRequest(List packets, int keyIndex, int keyType) { + RecoverUserKeyRequest request = new RecoverUserKeyRequest(); + request.setKeyIndex(keyIndex); + request.setKeyType(keyType); + request.setEncryptedKey(Hex.decode(composeUserKeyHex(packets, keyIndex, keyType))); + return request; + } + + private String composeUserKeyHex(List packets, int keyIndex, int keyType) { + StringBuilder builder = new StringBuilder(); + for (MasterKeyBackupPacket packet : packets) { + MasterKeyPacketUserKeyPart part = findUserKeyPart(packet, keyIndex, keyType); + builder.append(part.getComponent()); + } + return builder.toString(); + } + + private ParsedMasterKeyBackup parseBackupMaterial() { + BackupDataResult backupData = pcieCryptoService.backupLmkEx(); + byte[] normalized = normalizeBackupData(backupData); + if (normalized.length < LMK_MAC_BYTE_LENGTH) { + throw new IllegalStateException("backup lmk data length is too short: " + normalized.length); + } + byte[] lmkMac = Arrays.copyOfRange(normalized, 0, LMK_MAC_BYTE_LENGTH); + byte[] componentBytes = Arrays.copyOfRange(normalized, LMK_MAC_BYTE_LENGTH, normalized.length); + String[] splitComponents = DivisionUtils.equalDivision(Hex.toHexString(componentBytes), LMKConstant.COMPONENT_NUM); + Map componentMap = new HashMap<>(); + for (int i = 0; i < LMKConstant.COMPONENT_NUM; i++) { + componentMap.put(LMKConstant.KEY_ORDER[i], splitComponents[i]); + } + return new ParsedMasterKeyBackup(Hex.toHexString(lmkMac), componentMap); + } + + private ParsedIkBackup parseIkBackupMaterial(int keyType) { + BackupDataResult backupData = pcieCryptoService.backupIkComponent(keyType); + byte[] normalized = normalizeBackupData(backupData); + if (normalized.length < IK_COMPONENT_DATA_LENGTH + IK_COMPONENT_MAC_BYTE_LENGTH) { + throw new IllegalStateException("backup ik data length is too short: " + normalized.length); + } + byte[] componentBytes = Arrays.copyOfRange(normalized, 0, IK_COMPONENT_DATA_LENGTH); + byte[] mac = Arrays.copyOfRange(normalized, IK_COMPONENT_DATA_LENGTH, IK_COMPONENT_DATA_LENGTH + IK_COMPONENT_MAC_BYTE_LENGTH); + String[] splitComponents = DivisionUtils.equalDivision(Hex.toHexString(componentBytes), LMKConstant.COMPONENT_NUM); + Map componentMap = new HashMap<>(); + for (int i = 0; i < LMKConstant.COMPONENT_NUM; i++) { + componentMap.put(LMKConstant.KEY_ORDER[i], splitComponents[i]); + } + return new ParsedIkBackup(Hex.toHexString(mac), componentMap); + } + + private MasterKeyPacketUserKeyPart buildUserKeyPacket(int keyIndex, int keyType, int componentIndex) { + BackupDataResult backupData = pcieCryptoService.backupUserKey(keyIndex, keyType, USER_KEY_BACKUP_BUFFER_SIZE); + byte[] normalized = normalizeBackupData(backupData); + String[] components = DivisionUtils.equalDivision(Hex.toHexString(normalized), LMKConstant.COMPONENT_NUM); + return new MasterKeyPacketUserKeyPart(keyIndex, keyType, components[componentIndex - 1]); + } + + private byte[] normalizeBackupData(BackupDataResult backupData) { + byte[] data = backupData.getData(); + if (data == null || data.length == 0) { + return new byte[0]; + } + int length = backupData.getLength(); + int safeLength = length > 0 && length <= data.length ? length : data.length; + return Arrays.copyOf(data, safeLength); + } + + private byte[] decodeConfiguredPin(String fieldName, String base64Value) { + if (base64Value == null || base64Value.isBlank()) { + throw new IllegalStateException(fieldName + " is not configured"); + } + try { + return Base64.getDecoder().decode(base64Value); + } catch (IllegalArgumentException e) { + throw new IllegalStateException(fieldName + " is not valid Base64", e); + } + } + + private byte[] digestForInternalSign(String rawData) { + DigestRequest request = new DigestRequest(); + request.setAlgId(Gm0018AlgorithmIds.SM3); + request.setData(rawData.getBytes(StandardCharsets.UTF_8)); + BackupDataResult result = pcieCryptoService.digest(request); + return normalizeBackupData(result); + } + + private int requireComponentIndex(int componentIndex) { + if (componentIndex < 1 || componentIndex > LMKConstant.COMPONENT_NUM) { + throw new IllegalArgumentException("componentIndex must be 1, 2 or 3"); + } + return componentIndex; + } + + private String requiredComponent(Map componentMap, int componentIndex, String name) { + String component = componentMap.get(componentIndex); + if (component == null || component.isBlank()) { + throw new IllegalArgumentException(name + " component missing: " + componentIndex); + } + return component; + } + + private String requireConsistentLmkMac(List packets) { + if (packets == null || packets.size() != LMKConstant.COMPONENT_NUM) { + throw new IllegalArgumentException("packets size must be " + LMKConstant.COMPONENT_NUM); + } + String lmkMac = null; + for (MasterKeyBackupPacket packet : packets) { + if (packet == null || packet.getLmk() == null || packet.getLmk().getLmkMac() == null || packet.getLmk().getLmkMac().isBlank()) { + throw new IllegalArgumentException("lmk packet missing"); + } + if (lmkMac == null) { + lmkMac = packet.getLmk().getLmkMac(); + continue; + } + if (!lmkMac.equals(packet.getLmk().getLmkMac())) { + throw new IllegalArgumentException("lmk mac is inconsistent"); + } + } + return lmkMac; + } + + private String requireConsistentIkMac(List packets, int keyType) { + String mac = null; + for (MasterKeyBackupPacket packet : packets) { + MasterKeyPacketIkPart part = keyType == IKEnums.KEY_TYPE_AUTH.getCode() ? packet.getIkAuth() : packet.getIkDevice(); + if (part == null || part.getMac() == null || part.getMac().isBlank()) { + throw new IllegalArgumentException("ik mac missing"); + } + if (mac == null) { + mac = part.getMac(); + continue; + } + if (!mac.equals(part.getMac())) { + throw new IllegalArgumentException("ik mac is inconsistent"); + } + } + return mac; + } + + private MasterKeyPacketUserKeyPart findUserKeyPart(MasterKeyBackupPacket packet, int keyIndex, int keyType) { + return packet.getUserKeys().stream() + .filter(item -> item.getKeyIndex() == keyIndex && item.getKeyType() == keyType) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("user key component missing in packet " + packet.getComponentIndex())); + } + + private List sortPackets(List packets) { + if (packets == null || packets.size() != LMKConstant.COMPONENT_NUM) { + throw new IllegalArgumentException("packets size must be " + LMKConstant.COMPONENT_NUM); + } + List sorted = new ArrayList<>(packets); + sorted.sort(java.util.Comparator.comparingInt(MasterKeyBackupPacket::getComponentIndex)); + for (int index = 0; index < sorted.size(); index++) { + int expected = index + 1; + if (sorted.get(index).getComponentIndex() != expected) { + throw new IllegalArgumentException("packet componentIndex must be 1,2,3"); + } + } + return sorted; + } + + private record ParsedMasterKeyBackup(String lmkMacHex, Map componentMap) { + } + + private record ParsedIkBackup(String macHex, Map componentMap) { + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 15c0bd7..6008e39 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -204,7 +204,7 @@ tms: # 密码卡模式:MOCK(模拟)/JNA(动态库调用)等。 mode: ${TMS_CRYPTO_CARD_MODE:JNA} # 动态库目录(.so 所在目录)。 - vendor-lib-path: ${TMS_CRYPTO_VENDOR_LIB_PATH:/home/tms/libs/libsdf_ukey_syd1408-x64.so.1.0.0} + vendor-lib-path: ${TMS_CRYPTO_VENDOR_LIB_PATH:/home/tms/libs/libsdf_syd1408-x64.so.1.0.0} # 动态库名(不含前缀/后缀或按加载器约定)。 vendor-lib-name: ${TMS_CRYPTO_VENDOR_LIB_NAME:sdf_ukey_syd1408} # 是否严格校验算法标识(与 0018/厂商补充定义一致性校验)。 diff --git a/src/test/java/com/cisd/tms/modules/device/service/DeviceProfileServiceTest.java b/src/test/java/com/cisd/tms/modules/device/service/DeviceProfileServiceTest.java index e4f862e..5c33f8c 100644 --- a/src/test/java/com/cisd/tms/modules/device/service/DeviceProfileServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/device/service/DeviceProfileServiceTest.java @@ -8,6 +8,7 @@ import com.cisd.tms.modules.device.dto.DeviceProfileResponse; import com.cisd.tms.modules.device.dto.DeviceProfileSoftwareItem; import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository; +import com.cisd.tms.modules.device.service.impl.DeviceProfileServiceImpl; import com.cisd.tms.modules.device.support.SystemHardwareProbe; import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe; import com.cisd.tms.modules.init.config.InitCommandProfileService; @@ -21,6 +22,7 @@ import com.cisd.tms.modules.init.executor.InitStepExecutor; import com.cisd.tms.modules.init.repository.InitTaskRepository; import com.cisd.tms.modules.init.repository.InitTaskStepRepository; import com.cisd.tms.modules.init.service.InitService; +import com.cisd.tms.modules.init.service.impl.InitServiceImpl; import com.fasterxml.jackson.databind.ObjectMapper; import java.time.LocalDateTime; import java.util.ArrayList; @@ -47,7 +49,7 @@ class DeviceProfileServiceTest { versionRepository.save(component("NGINX", "Nginx", "Nginx V1.20")); versionRepository.save(component("APP", "标准收发器间参版", "标准收发器间参版 V1.3.2")); - DeviceProfileService service = new DeviceProfileService( + DeviceProfileService service = new DeviceProfileServiceImpl( initService, cryptoService(), profileProperties(), @@ -78,7 +80,7 @@ class DeviceProfileServiceTest { @Test void shouldReturnOnlyBaseSoftwareWhenNoSuccessfulInitSnapshotExists() { - DeviceProfileService service = new DeviceProfileService( + DeviceProfileService service = new DeviceProfileServiceImpl( newInitService("ENTERPRISE"), cryptoService(), profileProperties(), @@ -111,7 +113,7 @@ class DeviceProfileServiceTest { cached.setCurrentVersion("Nginx V1.20"); versionRepository.saveOrUpdate(cached); - DeviceProfileService service = new DeviceProfileService( + DeviceProfileService service = new DeviceProfileServiceImpl( initService, cryptoService(), profileProperties(), @@ -138,7 +140,7 @@ class DeviceProfileServiceTest { InitService initService = newInitService("ENTERPRISE"); createSuccessfulInitTask(initService, validInitRequest("RABBITMQ")); - DeviceProfileService service = new DeviceProfileService( + DeviceProfileService service = new DeviceProfileServiceImpl( initService, cryptoService(), profileProperties(), @@ -171,7 +173,7 @@ class DeviceProfileServiceTest { properties.setMemoryTotalDefault("16GB"); properties.setDiskTotalDefault("256G"); - DeviceProfileService service = new DeviceProfileService( + DeviceProfileService service = new DeviceProfileServiceImpl( newInitService("ENTERPRISE"), cryptoService(), properties, @@ -308,7 +310,7 @@ class DeviceProfileServiceTest { properties.setLogDir("/tmp/tms-init-logs"); ObjectMapper objectMapper = new ObjectMapper(); InitCommandProfileService commandProfileService = new InitCommandProfileService(objectMapper, properties); - return new InitService( + return new InitServiceImpl( new InMemoryInitTaskRepository(), new InMemoryInitTaskStepRepository(), preset(productType), diff --git a/src/test/java/com/cisd/tms/modules/device/service/DeviceRuntimeStatusServiceTest.java b/src/test/java/com/cisd/tms/modules/device/service/DeviceRuntimeStatusServiceTest.java index 1fa5403..c941df9 100644 --- a/src/test/java/com/cisd/tms/modules/device/service/DeviceRuntimeStatusServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/device/service/DeviceRuntimeStatusServiceTest.java @@ -7,6 +7,7 @@ import com.cisd.tms.modules.device.dto.DeviceRuntimeStatusResponse; import com.cisd.tms.modules.device.dto.SoftwareRuntimeStatusItem; import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository; +import com.cisd.tms.modules.device.service.impl.DeviceRuntimeStatusServiceImpl; import com.cisd.tms.modules.device.support.HardwareStatusProbe; import com.cisd.tms.modules.device.support.SoftwareRuntimeProbe; import com.cisd.tms.modules.device.support.UsageSnapshot; @@ -22,6 +23,7 @@ import com.cisd.tms.modules.init.executor.InitStepExecutor; import com.cisd.tms.modules.init.repository.InitTaskRepository; import com.cisd.tms.modules.init.repository.InitTaskStepRepository; import com.cisd.tms.modules.init.service.InitService; +import com.cisd.tms.modules.init.service.impl.InitServiceImpl; import com.fasterxml.jackson.databind.ObjectMapper; import java.time.LocalDateTime; import java.util.ArrayList; @@ -48,7 +50,7 @@ class DeviceRuntimeStatusServiceTest { versions.save(component("NGINX", "Nginx", "Nginx V1.20")); versions.save(component("APP", "标准收发器企业版", "标准收发器企业版 V1.3.2")); - DeviceRuntimeStatusService service = new DeviceRuntimeStatusService( + DeviceRuntimeStatusService service = new DeviceRuntimeStatusServiceImpl( initService, cryptoService(), hardwareProbe(23.22, 23.55, 32.0, 500.32, 1024.0), @@ -82,7 +84,7 @@ class DeviceRuntimeStatusServiceTest { @Test void shouldMarkNonInitializedSoftwareAsNotInstalled() { - DeviceRuntimeStatusService service = new DeviceRuntimeStatusService( + DeviceRuntimeStatusService service = new DeviceRuntimeStatusServiceImpl( newInitService("ENTERPRISE"), cryptoService(), hardwareProbe(10.0, 8.0, 16.0, 100.0, 256.0), @@ -107,7 +109,7 @@ class DeviceRuntimeStatusServiceTest { InitService initService = newInitService("INDIRECT"); createSuccessfulInitTask(initService, validInitRequest("RABBITMQ_TLQ")); - DeviceRuntimeStatusService service = new DeviceRuntimeStatusService( + DeviceRuntimeStatusService service = new DeviceRuntimeStatusServiceImpl( initService, failingCryptoService(), failingHardwareProbe(), @@ -334,7 +336,7 @@ class DeviceRuntimeStatusServiceTest { properties.setLogDir("/tmp/tms-init-logs"); ObjectMapper objectMapper = new ObjectMapper(); InitCommandProfileService commandProfileService = new InitCommandProfileService(objectMapper, properties); - return new InitService( + return new InitServiceImpl( new InMemoryInitTaskRepository(), new InMemoryInitTaskStepRepository(), preset(productType), diff --git a/src/test/java/com/cisd/tms/modules/device/service/DeviceServiceTest.java b/src/test/java/com/cisd/tms/modules/device/service/DeviceServiceTest.java index e723559..c8490d9 100644 --- a/src/test/java/com/cisd/tms/modules/device/service/DeviceServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/device/service/DeviceServiceTest.java @@ -8,6 +8,7 @@ import com.cisd.tms.modules.device.config.DeviceProfileProperties; import com.cisd.tms.modules.device.dto.DeviceInfoResponse; import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository; +import com.cisd.tms.modules.device.service.impl.DeviceServiceImpl; import com.cisd.tms.modules.init.config.InitCommandProfileService; import com.cisd.tms.modules.init.config.InitExecutorProperties; import com.cisd.tms.modules.init.dto.InitCreateTaskResponse; @@ -19,6 +20,7 @@ import com.cisd.tms.modules.init.executor.InitStepExecutor; import com.cisd.tms.modules.init.repository.InitTaskRepository; import com.cisd.tms.modules.init.repository.InitTaskStepRepository; import com.cisd.tms.modules.init.service.InitService; +import com.cisd.tms.modules.init.service.impl.InitServiceImpl; import com.fasterxml.jackson.databind.ObjectMapper; import java.time.LocalDateTime; import java.util.ArrayList; @@ -52,7 +54,7 @@ class DeviceServiceTest { InMemoryInitTaskRepository taskRepository = new InMemoryInitTaskRepository(); InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository(); InitService initService = newInitService(taskRepository, stepRepository, "ENTERPRISE"); - DeviceService deviceService = new DeviceService( + DeviceService deviceService = new DeviceServiceImpl( deviceProfile("MODEL-X", "设备A"), fixedVersionRepository("TMS-V3.1.4"), cryptoService("SERIAL-ABC", 0), @@ -77,7 +79,7 @@ class DeviceServiceTest { InMemoryInitTaskRepository taskRepository = new InMemoryInitTaskRepository(); InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository(); InitService initService = newInitService(taskRepository, stepRepository, productType); - return new DeviceService( + return new DeviceServiceImpl( deviceProfile("SYD7108", "跨境支付终端一体机"), fixedVersionRepository(tmsVersion), cryptoService(serialNumber, fsmState), @@ -95,7 +97,7 @@ class DeviceServiceTest { properties.setLogDir("/tmp/tms-init-logs"); ObjectMapper objectMapper = new ObjectMapper(); InitCommandProfileService commandProfileService = new InitCommandProfileService(objectMapper, properties); - return new InitService( + return new InitServiceImpl( taskRepository, stepRepository, preset(productType), diff --git a/src/test/java/com/cisd/tms/modules/file/FileServiceTest.java b/src/test/java/com/cisd/tms/modules/file/FileServiceTest.java index 7d99160..f32133e 100644 --- a/src/test/java/com/cisd/tms/modules/file/FileServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/file/FileServiceTest.java @@ -6,7 +6,11 @@ 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 com.cisd.tms.modules.file.service.impl.FileServiceImpl; import java.io.IOException; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; import java.nio.file.Files; import java.nio.file.Path; import java.util.LinkedHashMap; @@ -18,6 +22,8 @@ import org.springframework.mock.web.MockMultipartFile; class FileServiceTest { + private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-03-31T06:00:00Z"), ZoneOffset.UTC); + @Test void shouldPersistUploadedFileMetadata() { InMemoryFileRecordRepository repository = new InMemoryFileRecordRepository(); @@ -44,7 +50,7 @@ class FileServiceTest { void shouldSaveUploadedFileUnderFileIdDirectory() throws IOException { Path uploadBaseDir = Files.createTempDirectory("file-service-upload-test"); InMemoryFileRecordRepository repository = new InMemoryFileRecordRepository(); - FileService service = new FileService(repository, properties(uploadBaseDir, 1024)); + FileService service = new FileServiceImpl(repository, properties(uploadBaseDir, 1024), FIXED_CLOCK); MockMultipartFile file = new MockMultipartFile( "file", "receiver-license.dat", @@ -66,7 +72,7 @@ class FileServiceTest { @Test void shouldRejectBlankFileUpload() throws IOException { Path uploadBaseDir = Files.createTempDirectory("file-service-upload-empty-test"); - FileService service = new FileService(new InMemoryFileRecordRepository(), properties(uploadBaseDir, 1024)); + FileService service = new FileServiceImpl(new InMemoryFileRecordRepository(), properties(uploadBaseDir, 1024), FIXED_CLOCK); MockMultipartFile file = new MockMultipartFile("file", "empty.txt", "text/plain", new byte[0]); IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> service.upload(file)); @@ -77,7 +83,7 @@ class FileServiceTest { @Test void shouldRejectOversizeUpload() throws IOException { Path uploadBaseDir = Files.createTempDirectory("file-service-upload-oversize-test"); - FileService service = new FileService(new InMemoryFileRecordRepository(), properties(uploadBaseDir, 4)); + FileService service = new FileServiceImpl(new InMemoryFileRecordRepository(), properties(uploadBaseDir, 4), FIXED_CLOCK); MockMultipartFile file = new MockMultipartFile("file", "big.txt", "text/plain", "12345".getBytes()); IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> service.upload(file)); @@ -88,7 +94,7 @@ class FileServiceTest { @Test void shouldReturnStoredFileDetailByFileId() throws IOException { Path uploadBaseDir = Files.createTempDirectory("file-service-detail-test"); - FileService service = new FileService(new InMemoryFileRecordRepository(), properties(uploadBaseDir, 1024)); + FileService service = new FileServiceImpl(new InMemoryFileRecordRepository(), properties(uploadBaseDir, 1024), FIXED_CLOCK); MockMultipartFile file = new MockMultipartFile("file", "front.zip", "application/zip", "zip-data".getBytes()); FileUploadResponse uploaded = service.upload(file); diff --git a/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java b/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java index 65f5c5d..6512196 100644 --- a/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java @@ -20,6 +20,7 @@ import com.cisd.tms.modules.init.executor.InitStepExecutionResult; import com.cisd.tms.modules.init.executor.InitStepExecutor; import com.cisd.tms.modules.init.repository.InitTaskRepository; import com.cisd.tms.modules.init.repository.InitTaskStepRepository; +import com.cisd.tms.modules.init.service.impl.InitServiceImpl; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.nio.file.Files; @@ -643,7 +644,7 @@ class InitServiceTest { properties.setMode(InitExecutorProperties.Mode.SIMULATE); ObjectMapper objectMapper = new ObjectMapper(); InitCommandProfileService commandProfileService = new InitCommandProfileService(objectMapper, properties); - return new InitService(taskRepository, stepRepository, preset, objectMapper, executor, commandProfileService, properties); + return new InitServiceImpl(taskRepository, stepRepository, preset, objectMapper, executor, commandProfileService, properties); } private static InitTaskRepository emptyRepository() { diff --git a/src/test/java/com/cisd/tms/modules/mk/controller/LmkControllerTest.java b/src/test/java/com/cisd/tms/modules/mk/controller/LmkControllerTest.java deleted file mode 100644 index 6996939..0000000 --- a/src/test/java/com/cisd/tms/modules/mk/controller/LmkControllerTest.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.cisd.tms.modules.mk.controller; - -import com.cisd.tms.common.exception.GlobalExceptionHandler; -import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket; -import com.cisd.tms.modules.mk.dto.MasterKeyPacketIkPart; -import com.cisd.tms.modules.mk.dto.MasterKeyPacketLmkPart; -import com.cisd.tms.modules.mk.dto.MasterKeyPacketUserKeyPart; -import com.cisd.tms.modules.mk.dto.MasterKeyStateResult; -import com.cisd.tms.modules.mk.service.LmkService; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatchers; -import org.mockito.Mockito; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; - -import static org.hamcrest.Matchers.containsString; -import static org.mockito.ArgumentMatchers.eq; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -class LmkControllerTest { - - @Test - void shouldRecoverKeyPacketsUsingOrderedPackets() throws Exception { - LmkService lmkService = Mockito.mock(LmkService.class); - - MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new LmkController(lmkService, new ObjectMapper())) - .setControllerAdvice(new GlobalExceptionHandler()) - .build(); - - mockMvc.perform(post("/api/v1/masterKey/recover") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - { - "packets": [ - { - "componentIndex": 1, - "lmk": { - "lmkMac": "a1a2a3a4a5a6a7a8", - "component": "0102" - }, - "ikAuth": { - "mac": "1111111111111111", - "component": "0304" - }, - "ikDevice": { - "mac": "2222222222222222", - "component": "0506" - }, - "userKeys": [ - { - "keyIndex": 1, - "keyType": 0, - "component": "0708" - } - ] - }, - { - "componentIndex": 2 - }, - { - "componentIndex": 3 - } - ] - } - """)) - .andExpect(status().isOk()); - - Mockito.verify(lmkService).recoverKeyPackets(ArgumentMatchers.argThat(packets -> packets != null && packets.size() == 3)); - } - - @Test - void shouldInitMasterKeyThroughNewLifecycleEndpoint() throws Exception { - LmkService lmkService = Mockito.mock(LmkService.class); - MasterKeyStateResult result = new MasterKeyStateResult(); - result.setStatus(true); - result.setSeedMac("a1b2c3d4"); - Mockito.when(lmkService.initMasterKey()).thenReturn(result); - - MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new LmkController(lmkService, new ObjectMapper())) - .setControllerAdvice(new GlobalExceptionHandler()) - .build(); - - mockMvc.perform(post("/api/v1/masterKey/init")) - .andExpect(status().isOk()) - .andExpect(content().string(containsString("\"status\":true"))) - .andExpect(content().string(containsString("\"seedMac\":\"a1b2c3d4\""))); - } - - @Test - void shouldDestroyMasterKeyThroughNewLifecycleEndpoint() throws Exception { - LmkService lmkService = Mockito.mock(LmkService.class); - - MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new LmkController(lmkService, new ObjectMapper())) - .setControllerAdvice(new GlobalExceptionHandler()) - .build(); - - mockMvc.perform(post("/api/v1/masterKey/destroy")) - .andExpect(status().isOk()); - - Mockito.verify(lmkService).destroyMasterKey(); - } - - @Test - void shouldReturnMasterKeyLifecycleStatus() throws Exception { - LmkService lmkService = Mockito.mock(LmkService.class); - MasterKeyStateResult result = new MasterKeyStateResult(); - result.setStatus(true); - result.setSeedMac("00112233"); - Mockito.when(lmkService.queryMasterKeyStatus()).thenReturn(result); - - MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new LmkController(lmkService, new ObjectMapper())) - .setControllerAdvice(new GlobalExceptionHandler()) - .build(); - - mockMvc.perform(get("/api/v1/masterKey/status")) - .andExpect(status().isOk()) - .andExpect(content().string(containsString("\"status\":true"))) - .andExpect(content().string(containsString("\"seedMac\":\"00112233\""))); - } -} diff --git a/src/test/java/com/cisd/tms/modules/mk/service/LmkServiceTest.java b/src/test/java/com/cisd/tms/modules/mk/service/LmkServiceTest.java index 3ba0034..261a87e 100644 --- a/src/test/java/com/cisd/tms/modules/mk/service/LmkServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/mk/service/LmkServiceTest.java @@ -13,6 +13,7 @@ import com.cisd.tms.modules.mk.dto.MasterKeyPacketIkPart; import com.cisd.tms.modules.mk.dto.MasterKeyPacketLmkPart; import com.cisd.tms.modules.mk.dto.MasterKeyPacketUserKeyPart; import com.cisd.tms.modules.mk.dto.MasterKeyStateResult; +import com.cisd.tms.modules.mk.service.impl.LmkServiceImpl; import java.nio.charset.StandardCharsets; import java.util.List; import org.junit.jupiter.api.Assertions; @@ -29,7 +30,7 @@ class LmkServiceTest { MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE="); Mockito.when(pcieCryptoService.initializeMasterKey(Mockito.any(), Mockito.any())).thenReturn(new byte[] {0x01, 0x23, 0x45, 0x67}); - LmkService service = new LmkService(pcieCryptoService, properties); + LmkService service = new LmkServiceImpl(pcieCryptoService, properties); MasterKeyStateResult result = service.initMasterKey(); @@ -49,7 +50,7 @@ class LmkServiceTest { Mockito.when(pcieCryptoService.initializeMasterKey(Mockito.any(), Mockito.any())) .thenThrow(new IllegalStateException("master key initialization failed")); - LmkService service = new LmkService(pcieCryptoService, properties); + LmkService service = new LmkServiceImpl(pcieCryptoService, properties); IllegalStateException exception = Assertions.assertThrows(IllegalStateException.class, service::initMasterKey); @@ -62,7 +63,7 @@ class LmkServiceTest { MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE="); Mockito.when(pcieCryptoService.checkLmk()).thenReturn(false); - LmkService service = new LmkService(pcieCryptoService, properties); + LmkService service = new LmkServiceImpl(pcieCryptoService, properties); MasterKeyStateResult result = service.queryMasterKeyStatus(); @@ -78,7 +79,7 @@ class LmkServiceTest { Mockito.when(pcieCryptoService.checkLmk()).thenReturn(true); Mockito.when(pcieCryptoService.exportLmkSeedMac()).thenReturn(new byte[] {(byte) 0xAA, (byte) 0xBB}); - LmkService service = new LmkService(pcieCryptoService, properties); + LmkService service = new LmkServiceImpl(pcieCryptoService, properties); MasterKeyStateResult result = service.queryMasterKeyStatus(); @@ -116,7 +117,7 @@ class LmkServiceTest { Mockito.when(pcieCryptoService.backupUserKey(1, 0, 120)).thenReturn(userSignBackup); Mockito.when(pcieCryptoService.backupUserKey(1, 1, 120)).thenReturn(userEncBackup); - LmkService service = new LmkService(pcieCryptoService, properties); + LmkService service = new LmkServiceImpl(pcieCryptoService, properties); MasterKeyBackupPacket packet = service.buildBackupPacket(2); @@ -142,7 +143,7 @@ class LmkServiceTest { void shouldRecoverAllKeyPacketsInOrder() { PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE="); - LmkService service = new LmkService(pcieCryptoService, properties); + LmkService service = new LmkServiceImpl(pcieCryptoService, properties); MasterKeyBackupPacket packet1 = new MasterKeyBackupPacket(); packet1.setComponentIndex(1); @@ -220,7 +221,7 @@ class LmkServiceTest { Mockito.when(pcieCryptoService.digest(Mockito.any())).thenReturn(digestResult); Mockito.when(pcieCryptoService.eccInternalSign(Mockito.any())).thenReturn(signResult); - LmkService service = new LmkService(pcieCryptoService, properties); + LmkService service = new LmkServiceImpl(pcieCryptoService, properties); String signatureHex = service.signIk("1223"); @@ -249,7 +250,7 @@ class LmkServiceTest { }); Mockito.when(pcieCryptoService.digest(Mockito.any())).thenReturn(digestResult); - LmkService service = new LmkService(pcieCryptoService, properties); + LmkService service = new LmkServiceImpl(pcieCryptoService, properties); service.verifyIk("1223", "aabb");