service改造
This commit is contained in:
@@ -12,6 +12,8 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
@@ -20,18 +22,15 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/auth")
|
@RequestMapping("/api/v1/auth")
|
||||||
|
@RequiredArgsConstructor
|
||||||
@Tag(name = "认证管理", description = "角色启用、密码重置与 UKey 绑定接口")
|
@Tag(name = "认证管理", description = "角色启用、密码重置与 UKey 绑定接口")
|
||||||
public class AuthAdminController {
|
public class AuthAdminController {
|
||||||
|
|
||||||
private final AuthAdminService authAdminService;
|
private final AuthAdminService authAdminService;
|
||||||
|
|
||||||
public AuthAdminController(AuthAdminService authAdminService) {
|
|
||||||
this.authAdminService = authAdminService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/roles/{roleCode}/enable")
|
@PostMapping("/roles/{roleCode}/enable")
|
||||||
@Operation(summary = "启用角色", description = "仅允许 KEY_ADMIN FULL 会话启用目标角色。")
|
@Operation(summary = "启用角色", description = "仅允许 SUPER_ADMIN FULL 会话启用目标角色。")
|
||||||
@RequireRole("KEY_ADMIN")
|
@RequireRole("SUPER_ADMIN")
|
||||||
@RequireAuthLevel("FULL")
|
@RequireAuthLevel("FULL")
|
||||||
public ApiResponse<Void> enableRole(@PathVariable("roleCode") String roleCode, HttpServletRequest request) {
|
public ApiResponse<Void> enableRole(@PathVariable("roleCode") String roleCode, HttpServletRequest request) {
|
||||||
authAdminService.enableRole(
|
authAdminService.enableRole(
|
||||||
@@ -43,8 +42,8 @@ public class AuthAdminController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/roles/{roleCode}/reset-password")
|
@PostMapping("/roles/{roleCode}/reset-password")
|
||||||
@Operation(summary = "重置角色密码", description = "仅允许 KEY_ADMIN FULL 会话重置目标角色密码。")
|
@Operation(summary = "重置角色密码", description = "仅允许 SUPER_ADMIN FULL 会话重置目标角色密码。")
|
||||||
@RequireRole("KEY_ADMIN")
|
@RequireRole("SUPER_ADMIN")
|
||||||
@RequireAuthLevel("FULL")
|
@RequireAuthLevel("FULL")
|
||||||
public ApiResponse<Void> resetPassword(@PathVariable("roleCode") String roleCode, HttpServletRequest request) {
|
public ApiResponse<Void> resetPassword(@PathVariable("roleCode") String roleCode, HttpServletRequest request) {
|
||||||
authAdminService.resetPassword(
|
authAdminService.resetPassword(
|
||||||
@@ -56,8 +55,8 @@ public class AuthAdminController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/roles/{roleCode}/ukeys/bind")
|
@PostMapping("/roles/{roleCode}/ukeys/bind")
|
||||||
@Operation(summary = "绑定角色 UKey", description = "仅允许 KEY_ADMIN FULL 会话登记目标角色的 UKey 绑定信息。")
|
@Operation(summary = "绑定角色 UKey", description = "仅允许 SUPER_ADMIN FULL 会话登记目标角色的 UKey 绑定信息。")
|
||||||
@RequireRole("KEY_ADMIN")
|
@RequireRole("SUPER_ADMIN")
|
||||||
@RequireAuthLevel("FULL")
|
@RequireAuthLevel("FULL")
|
||||||
public ApiResponse<Void> bindUkey(
|
public ApiResponse<Void> bindUkey(
|
||||||
@PathVariable("roleCode") String roleCode,
|
@PathVariable("roleCode") String roleCode,
|
||||||
@@ -80,7 +79,7 @@ public class AuthAdminController {
|
|||||||
|
|
||||||
@PostMapping("/roles/{roleCode}/ukeys/issue-sign")
|
@PostMapping("/roles/{roleCode}/ukeys/issue-sign")
|
||||||
@Operation(summary = "生成 UKey 发行签名", description = "按旧绑定流程为目标角色 UKey 材料生成发行签名。")
|
@Operation(summary = "生成 UKey 发行签名", description = "按旧绑定流程为目标角色 UKey 材料生成发行签名。")
|
||||||
// @RequireRole("KEY_ADMIN")
|
// @RequireRole("SUPER_ADMIN")
|
||||||
@RequireAuthLevel("FULL")
|
@RequireAuthLevel("FULL")
|
||||||
public ApiResponse<UKeySignResult> issueUkeyBindingSign(
|
public ApiResponse<UKeySignResult> issueUkeyBindingSign(
|
||||||
@PathVariable("roleCode") String roleCode,
|
@PathVariable("roleCode") String roleCode,
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
@@ -23,15 +25,12 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/auth")
|
@RequestMapping("/api/v1/auth")
|
||||||
|
@RequiredArgsConstructor
|
||||||
@Tag(name = "认证鉴权", description = "管理端认证与当前会话接口")
|
@Tag(name = "认证鉴权", description = "管理端认证与当前会话接口")
|
||||||
public class AuthController {
|
public class AuthController {
|
||||||
|
|
||||||
private final AuthService authService;
|
private final AuthService authService;
|
||||||
|
|
||||||
public AuthController(AuthService authService) {
|
|
||||||
this.authService = authService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/password-login")
|
@PostMapping("/password-login")
|
||||||
@Operation(summary = "口令登录", description = "按旧系统受限登录流程签发 LIMITED 会话。")
|
@Operation(summary = "口令登录", description = "按旧系统受限登录流程签发 LIMITED 会话。")
|
||||||
public ApiResponse<LoginResponse> passwordLogin(@Valid @RequestBody PasswordLoginRequest request) {
|
public ApiResponse<LoginResponse> passwordLogin(@Valid @RequestBody PasswordLoginRequest request) {
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ import java.time.LocalDateTime;
|
|||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@AllArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AuthAdminServiceImpl implements AuthAdminService {
|
public class AuthAdminServiceImpl implements AuthAdminService {
|
||||||
|
|
||||||
static final String DEFAULT_PASSWORD = "12345678";
|
static final String DEFAULT_PASSWORD = "12345678";
|
||||||
|
|||||||
@@ -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.AuthMethod;
|
||||||
import com.cisd.tms.modules.auth.enums.RoleCode;
|
import com.cisd.tms.modules.auth.enums.RoleCode;
|
||||||
import com.cisd.tms.modules.auth.service.AuthPolicyService;
|
import com.cisd.tms.modules.auth.service.AuthPolicyService;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@AllArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AuthPolicyServiceImpl implements AuthPolicyService {
|
public class AuthPolicyServiceImpl implements AuthPolicyService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -39,11 +39,11 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@AllArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AuthServiceImpl implements AuthService {
|
public class AuthServiceImpl implements AuthService {
|
||||||
|
|
||||||
private static final int MAX_FAILED_ATTEMPTS = 5;
|
private static final int MAX_FAILED_ATTEMPTS = 5;
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ import jakarta.validation.Valid;
|
|||||||
import jakarta.validation.constraints.Min;
|
import jakarta.validation.constraints.Min;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -52,15 +54,12 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
@Validated
|
@Validated
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/device/crypto")
|
@RequestMapping("/api/v1/device/crypto")
|
||||||
|
@RequiredArgsConstructor
|
||||||
@Tag(name = "密码卡调试", description = "密码卡设备调试、密钥操作和文件管理接口")
|
@Tag(name = "密码卡调试", description = "密码卡设备调试、密钥操作和文件管理接口")
|
||||||
public class CryptoCardController {
|
public class CryptoCardController {
|
||||||
|
|
||||||
private final PcieCryptoService pcieCryptoService;
|
private final PcieCryptoService pcieCryptoService;
|
||||||
|
|
||||||
public CryptoCardController(PcieCryptoService pcieCryptoService) {
|
|
||||||
this.pcieCryptoService = pcieCryptoService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/device-count")
|
@GetMapping("/device-count")
|
||||||
@Operation(summary = "查询密码卡数量", description = "返回当前环境可见的密码卡设备数量。")
|
@Operation(summary = "查询密码卡数量", description = "返回当前环境可见的密码卡设备数量。")
|
||||||
public ApiResponse<DeviceCountResult> getDeviceCount() {
|
public ApiResponse<DeviceCountResult> getDeviceCount() {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -20,6 +21,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/device")
|
@RequestMapping("/api/v1/device")
|
||||||
|
@RequiredArgsConstructor
|
||||||
@Tag(name = "设备概览", description = "设备基础信息、状态和重启接口")
|
@Tag(name = "设备概览", description = "设备基础信息、状态和重启接口")
|
||||||
public class DeviceController {
|
public class DeviceController {
|
||||||
|
|
||||||
@@ -27,16 +29,6 @@ public class DeviceController {
|
|||||||
private final DeviceProfileService deviceProfileService;
|
private final DeviceProfileService deviceProfileService;
|
||||||
private final DeviceRuntimeStatusService deviceRuntimeStatusService;
|
private final DeviceRuntimeStatusService deviceRuntimeStatusService;
|
||||||
|
|
||||||
public DeviceController(
|
|
||||||
DeviceService deviceService,
|
|
||||||
DeviceProfileService deviceProfileService,
|
|
||||||
DeviceRuntimeStatusService deviceRuntimeStatusService
|
|
||||||
) {
|
|
||||||
this.deviceService = deviceService;
|
|
||||||
this.deviceProfileService = deviceProfileService;
|
|
||||||
this.deviceRuntimeStatusService = deviceRuntimeStatusService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/info")
|
@GetMapping("/info")
|
||||||
@Operation(summary = "查询设备信息", description = "返回设备型号、设备名称、TMS版本、设备序列号、管理密钥状态和当前设备初始化状态。")
|
@Operation(summary = "查询设备信息", description = "返回设备型号、设备名称、TMS版本、设备序列号、管理密钥状态和当前设备初始化状态。")
|
||||||
public ApiResponse<DeviceInfoResponse> info() {
|
public ApiResponse<DeviceInfoResponse> info() {
|
||||||
|
|||||||
@@ -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.dto.TimeConfigRequest;
|
||||||
import com.cisd.tms.modules.device.service.TimeConfigService;
|
import com.cisd.tms.modules.device.service.TimeConfigService;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -13,16 +14,12 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/device")
|
@RequestMapping("/api/v1/device")
|
||||||
|
@RequiredArgsConstructor
|
||||||
@Tag(name = "device-time-config-controller")
|
@Tag(name = "device-time-config-controller")
|
||||||
public class TimeConfigController {
|
public class TimeConfigController {
|
||||||
|
|
||||||
private final TimeConfigService timeConfigService;
|
private final TimeConfigService timeConfigService;
|
||||||
|
|
||||||
public TimeConfigController(TimeConfigService timeConfigService) {
|
|
||||||
this.timeConfigService = timeConfigService;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("/time-config")
|
@PostMapping("/time-config")
|
||||||
public ApiResponse<Void> configureTime(@RequestBody TimeConfigRequest request) {
|
public ApiResponse<Void> configureTime(@RequestBody TimeConfigRequest request) {
|
||||||
//todo 鉴权
|
//todo 鉴权
|
||||||
|
|||||||
@@ -1,178 +1,8 @@
|
|||||||
package com.cisd.tms.modules.device.service;
|
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.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 interface DeviceProfileService {
|
||||||
public class DeviceProfileService {
|
|
||||||
|
|
||||||
private final InitService initService;
|
DeviceProfileResponse profile();
|
||||||
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<DeviceProfileSoftwareItem> buildSoftwareItems() {
|
|
||||||
List<DeviceProfileSoftwareItem> 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<DeviceProfileSoftwareItem> 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<DeviceSoftwareVersionEntity> 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,239 +1,8 @@
|
|||||||
package com.cisd.tms.modules.device.service;
|
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.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 interface DeviceRuntimeStatusService {
|
||||||
public class DeviceRuntimeStatusService {
|
|
||||||
|
|
||||||
private static final String STATUS_NORMAL = "NORMAL";
|
DeviceRuntimeStatusResponse loadRuntimeStatus();
|
||||||
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<SoftwareRuntimeStatusItem> buildSoftwareStatuses() {
|
|
||||||
CurrentInitConfigResponse config = loadCurrentInitConfigIfPresent();
|
|
||||||
String productType = config == null ? normalize(cisdPresetProperties.getProductType()) : normalize(config.getProductType());
|
|
||||||
String mqType = config == null ? "" : normalize(config.getMqType());
|
|
||||||
|
|
||||||
List<SoftwareRuntimeStatusItem> 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<DeviceSoftwareVersionEntity> 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,83 +1,11 @@
|
|||||||
package com.cisd.tms.modules.device.service;
|
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.DeviceActionResponse;
|
||||||
import com.cisd.tms.modules.device.dto.DeviceInfoResponse;
|
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 interface DeviceService {
|
||||||
public class DeviceService {
|
|
||||||
|
|
||||||
private final DeviceProfileProperties deviceProfileProperties;
|
DeviceInfoResponse info();
|
||||||
private final DeviceSoftwareVersionRepository deviceSoftwareVersionRepository;
|
|
||||||
private final PcieCryptoService pcieCryptoService;
|
|
||||||
private final InitService initService;
|
|
||||||
|
|
||||||
public DeviceService(
|
DeviceActionResponse restart(String operator);
|
||||||
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<DeviceSoftwareVersionEntity> 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,112 +1,8 @@
|
|||||||
package com.cisd.tms.modules.device.service;
|
package com.cisd.tms.modules.device.service;
|
||||||
|
|
||||||
import com.cisd.tms.modules.device.dto.TimeConfigRequest;
|
import com.cisd.tms.modules.device.dto.TimeConfigRequest;
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
public interface TimeConfigService {
|
||||||
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<String> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
void processTimeConfig(TimeConfigRequest request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<DeviceProfileSoftwareItem> buildSoftwareItems() {
|
||||||
|
List<DeviceProfileSoftwareItem> 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<DeviceProfileSoftwareItem> 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<DeviceSoftwareVersionEntity> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+226
@@ -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<SoftwareRuntimeStatusItem> 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<SoftwareRuntimeStatusItem> 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<DeviceSoftwareVersionEntity> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<DeviceSoftwareVersionEntity> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -17,15 +18,12 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/files")
|
@RequestMapping("/api/v1/files")
|
||||||
|
@RequiredArgsConstructor
|
||||||
@Tag(name = "文件管理", description = "初始化文件上传与文件元数据查询接口")
|
@Tag(name = "文件管理", description = "初始化文件上传与文件元数据查询接口")
|
||||||
public class FileController {
|
public class FileController {
|
||||||
|
|
||||||
private final FileService fileService;
|
private final FileService fileService;
|
||||||
|
|
||||||
public FileController(FileService fileService) {
|
|
||||||
this.fileService = fileService;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 内部上传入口,前端先拿到 fileId,再把 fileId 传给初始化相关接口。
|
// 内部上传入口,前端先拿到 fileId,再把 fileId 传给初始化相关接口。
|
||||||
@PostMapping("/upload")
|
@PostMapping("/upload")
|
||||||
@Operation(summary = "上传初始化文件", description = "上传许可证、配置包等初始化依赖文件,返回 fileId 供后续任务引用。")
|
@Operation(summary = "上传初始化文件", description = "上传许可证、配置包等初始化依赖文件,返回 fileId 供后续任务引用。")
|
||||||
|
|||||||
@@ -1,118 +1,12 @@
|
|||||||
package com.cisd.tms.modules.file.service;
|
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.FileDetailResponse;
|
||||||
import com.cisd.tms.modules.file.dto.FileUploadResponse;
|
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;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@Service
|
public interface FileService {
|
||||||
public class FileService {
|
|
||||||
|
|
||||||
private static final DateTimeFormatter FILE_ID_TS = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
FileUploadResponse upload(MultipartFile file);
|
||||||
|
|
||||||
private final FileRecordRepository fileRecordRepository;
|
FileDetailResponse getDetail(String fileId);
|
||||||
private final FileStorageProperties fileStorageProperties;
|
|
||||||
|
|
||||||
public FileService(FileRecordRepository fileRecordRepository, FileStorageProperties fileStorageProperties) {
|
|
||||||
this.fileRecordRepository = fileRecordRepository;
|
|
||||||
this.fileStorageProperties = fileStorageProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FileUploadResponse upload(MultipartFile file) {
|
|
||||||
if (file == null || file.isEmpty() || file.getSize() <= 0) {
|
|
||||||
throw new IllegalArgumentException("upload file is empty");
|
|
||||||
}
|
|
||||||
if (file.getSize() > fileStorageProperties.getMaxFileSizeBytes()) {
|
|
||||||
throw new IllegalArgumentException("upload file size exceeds limit");
|
|
||||||
}
|
|
||||||
|
|
||||||
String originalFilename = sanitizeFilename(file.getOriginalFilename());
|
|
||||||
String fileId = generateFileId();
|
|
||||||
Path uploadBaseDir = Paths.get(normalize(fileStorageProperties.getUploadBaseDir())).normalize().toAbsolutePath();
|
|
||||||
// 固定为 <upload-base-dir>/<fileId>/<originalFilename>,这样初始化流程只靠 fileId 就能稳定反查文件。
|
|
||||||
Path target = uploadBaseDir.resolve(fileId).resolve(originalFilename).normalize().toAbsolutePath();
|
|
||||||
|
|
||||||
try {
|
|
||||||
Files.createDirectories(target.getParent());
|
|
||||||
file.transferTo(target);
|
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to save upload file");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 直接落库存储绝对路径,避免后续再依赖可能变化的配置重新推导文件位置。
|
|
||||||
FileRecordEntity entity = new FileRecordEntity();
|
|
||||||
entity.setFileId(fileId);
|
|
||||||
entity.setOriginalFilename(originalFilename);
|
|
||||||
entity.setStoragePath(target.toString());
|
|
||||||
entity.setStatus("ACTIVE");
|
|
||||||
fileRecordRepository.save(entity);
|
|
||||||
|
|
||||||
FileUploadResponse response = new FileUploadResponse();
|
|
||||||
response.setFileId(fileId);
|
|
||||||
response.setSize(file.getSize());
|
|
||||||
response.setStoragePath(target.toString());
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FileDetailResponse getDetail(String fileId) {
|
|
||||||
FileRecordEntity entity = fileRecordRepository.findByFileId(normalize(fileId))
|
|
||||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "file not found: " + normalize(fileId)));
|
|
||||||
Path path = Paths.get(entity.getStoragePath()).normalize().toAbsolutePath();
|
|
||||||
|
|
||||||
FileDetailResponse response = new FileDetailResponse();
|
|
||||||
response.setFileId(entity.getFileId());
|
|
||||||
response.setOriginalFilename(entity.getOriginalFilename());
|
|
||||||
response.setStoragePath(entity.getStoragePath());
|
|
||||||
response.setStatus(entity.getStatus());
|
|
||||||
try {
|
|
||||||
// 文件大小以磁盘上的真实文件为准,保证查询结果反映当前实际落盘状态。
|
|
||||||
response.setSize(Files.exists(path) ? Files.size(path) : 0L);
|
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to read file detail");
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String generateFileId() {
|
|
||||||
String ts = LocalDateTime.now().format(FILE_ID_TS);
|
|
||||||
String suffix = UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase(Locale.ROOT);
|
|
||||||
return "FILE-" + ts + "-" + suffix;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String sanitizeFilename(String originalFilename) {
|
|
||||||
String normalized = normalize(originalFilename);
|
|
||||||
if (normalized.isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("original filename is blank");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// 先剥离客户端传入文件名里的路径段,避免把目录信息带入服务端存储路径。
|
|
||||||
String fileName = Paths.get(normalized).getFileName().toString();
|
|
||||||
String sanitized = fileName.replaceAll("[^A-Za-z0-9._-]", "_");
|
|
||||||
if (sanitized.isBlank()) {
|
|
||||||
throw new IllegalArgumentException("original filename is invalid");
|
|
||||||
}
|
|
||||||
return sanitized;
|
|
||||||
} catch (InvalidPathException ex) {
|
|
||||||
throw new IllegalArgumentException("original filename is invalid");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String normalize(String value) {
|
|
||||||
return value == null ? "" : value.trim();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
// 固定为 <upload-base-dir>/<fileId>/<originalFilename>,这样初始化流程只靠 fileId 就能稳定反查文件。
|
||||||
|
Path target = uploadBaseDir.resolve(fileId).resolve(originalFilename).normalize().toAbsolutePath();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Files.createDirectories(target.getParent());
|
||||||
|
file.transferTo(target);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to save upload file");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接落库存储绝对路径,避免后续再依赖可能变化的配置重新推导文件位置。
|
||||||
|
FileRecordEntity entity = new FileRecordEntity();
|
||||||
|
entity.setFileId(fileId);
|
||||||
|
entity.setOriginalFilename(originalFilename);
|
||||||
|
entity.setStoragePath(target.toString());
|
||||||
|
entity.setStatus("ACTIVE");
|
||||||
|
fileRecordRepository.save(entity);
|
||||||
|
|
||||||
|
FileUploadResponse response = new FileUploadResponse();
|
||||||
|
response.setFileId(fileId);
|
||||||
|
response.setSize(file.getSize());
|
||||||
|
response.setStoragePath(target.toString());
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
package com.cisd.tms.modules.init.service;
|
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.CurrentInitConfigResponse;
|
||||||
|
import com.cisd.tms.modules.init.dto.InitCreateTaskResponse;
|
||||||
import com.cisd.tms.modules.init.dto.InitPlanTemplateResponse;
|
import com.cisd.tms.modules.init.dto.InitPlanTemplateResponse;
|
||||||
import com.cisd.tms.modules.init.dto.InitPreviewRequest;
|
import com.cisd.tms.modules.init.dto.InitPreviewRequest;
|
||||||
import com.cisd.tms.modules.init.dto.InitPreviewResponse;
|
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.InitTaskStepResponse;
|
||||||
import com.cisd.tms.modules.init.dto.ResetCreateTaskResponse;
|
import com.cisd.tms.modules.init.dto.ResetCreateTaskResponse;
|
||||||
import com.cisd.tms.modules.init.dto.ResetPreviewResponse;
|
import com.cisd.tms.modules.init.dto.ResetPreviewResponse;
|
||||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
import java.util.List;
|
||||||
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;
|
|
||||||
|
|
||||||
@Service
|
public interface InitService {
|
||||||
public class InitService {
|
|
||||||
|
|
||||||
private static final String TASK_TYPE_INIT = "INIT";
|
InitPlanTemplateResponse loadTemplate(String ignoredVersion);
|
||||||
private static final String TASK_TYPE_RESET = "RESET";
|
|
||||||
private static final Set<String> PRESET_PRODUCT_TYPES = Set.of("ENTERPRISE", "INDIRECT", "DIRECT");
|
|
||||||
private static final Set<String> DEPLOY_STANDARD = Set.of("SINGLE", "DUAL");
|
|
||||||
private static final Set<String> DEPLOY_DIRECT = Set.of("SINGLE", "DUAL", "QUAD");
|
|
||||||
private static final Set<String> MQ_ENTERPRISE = Set.of("RABBITMQ", "TLQ");
|
|
||||||
private static final Set<String> MQ_INDIRECT = Set.of("RABBITMQ", "RABBITMQ_TLQ");
|
|
||||||
private static final Set<String> MQ_DIRECT = Set.of("RABBITMQ_TLQ", "RABBITMQ_CFMQ");
|
|
||||||
private static final Pattern FILE_ID_PATTERN = Pattern.compile("^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$");
|
|
||||||
|
|
||||||
private final InitTaskRepository initTaskRepository;
|
InitPreviewResponse preview(InitPreviewRequest request);
|
||||||
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<String, CompletableFuture<Void>> activeExecutions;
|
|
||||||
|
|
||||||
@Autowired
|
ResetPreviewResponse previewReset();
|
||||||
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<>()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
InitService(
|
InitCreateTaskResponse createTask(InitPreviewRequest request);
|
||||||
InitTaskRepository initTaskRepository,
|
|
||||||
InitTaskStepRepository initTaskStepRepository,
|
|
||||||
CisdPresetProperties cisdPresetProperties,
|
|
||||||
ObjectMapper objectMapper,
|
|
||||||
InitStepExecutor initStepExecutor,
|
|
||||||
InitCommandProfileService initCommandProfileService,
|
|
||||||
InitExecutorProperties initExecutorProperties,
|
|
||||||
Executor backgroundExecutor,
|
|
||||||
ConcurrentMap<String, CompletableFuture<Void>> activeExecutions
|
|
||||||
) {
|
|
||||||
this.initTaskRepository = initTaskRepository;
|
|
||||||
this.initTaskStepRepository = initTaskStepRepository;
|
|
||||||
this.cisdPresetProperties = cisdPresetProperties;
|
|
||||||
this.objectMapper = objectMapper;
|
|
||||||
this.initStepExecutor = initStepExecutor;
|
|
||||||
this.initCommandProfileService = initCommandProfileService;
|
|
||||||
this.initExecutorProperties = initExecutorProperties;
|
|
||||||
this.backgroundExecutor = backgroundExecutor;
|
|
||||||
this.activeExecutions = activeExecutions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
ResetCreateTaskResponse createResetTask();
|
||||||
* 读取后端预制产品类型和版本。
|
|
||||||
* 该接口是前端动态渲染“企业/间参/直参”初始化页的入口。
|
|
||||||
*/
|
|
||||||
public InitPlanTemplateResponse loadTemplate(String ignoredVersion) {
|
|
||||||
String productType = resolvePresetProductType();
|
|
||||||
|
|
||||||
InitPlanTemplateResponse response = new InitPlanTemplateResponse();
|
DeviceInitStateSnapshot getDeviceInitStateSnapshot();
|
||||||
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) {
|
CurrentInitConfigResponse loadCurrentInitConfig();
|
||||||
if (request == null) {
|
|
||||||
throw new IllegalArgumentException("request must not be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
String productType = resolvePresetProductType();
|
InitTaskDetailResponse getTask(String taskId);
|
||||||
validateCommon(request);
|
|
||||||
validateByProductType(productType, request);
|
|
||||||
|
|
||||||
String previewTaskId = "preview-" + productType + "-" + request.getOrgCode();
|
List<InitTaskStepResponse> getTaskSteps(String taskId);
|
||||||
boolean existed = initTaskRepository.findByTaskId(previewTaskId).isPresent();
|
|
||||||
|
|
||||||
InitPreviewResponse response = new InitPreviewResponse();
|
InitTaskStepLogResponse getTaskStepLog(String taskId, int stepNo);
|
||||||
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() {
|
InitTaskExecuteResponse executeTask(String taskId);
|
||||||
ResolvedResetContext context = resolveCurrentResetContext();
|
|
||||||
String productType = context.productType();
|
|
||||||
|
|
||||||
ResetPreviewResponse response = new ResetPreviewResponse();
|
InitTaskExecuteResponse executeResetTask(String taskId);
|
||||||
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) {
|
record DeviceInitStateSnapshot(
|
||||||
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<String> 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<InitTaskStepEntity> 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<String> 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<InitTaskStepEntity> 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<InitTaskEntity> 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<InitTaskEntity> latestInit = initTaskRepository.findLatestByTaskTypeAndStatus(TASK_TYPE_INIT, "SUCCESS");
|
|
||||||
Optional<InitTaskEntity> 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<InitTaskEntity> 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<InitTaskStepResponse> 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<InitTaskStepEntity> 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<Void> active = activeExecutions.get(taskId);
|
|
||||||
if (active != null && !active.isDone()) {
|
|
||||||
task.setStatus("RUNNING");
|
|
||||||
task.setUpdateTime(LocalDateTime.now());
|
|
||||||
initTaskRepository.update(task);
|
|
||||||
return buildExecuteResponse(task, steps);
|
|
||||||
}
|
|
||||||
|
|
||||||
task.setStatus("RUNNING");
|
|
||||||
task.setUpdateTime(LocalDateTime.now());
|
|
||||||
initTaskRepository.update(task);
|
|
||||||
|
|
||||||
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> executeTaskInBackground(taskId), backgroundExecutor)
|
|
||||||
.whenComplete((ignored, ex) -> activeExecutions.remove(taskId));
|
|
||||||
activeExecutions.put(taskId, future);
|
|
||||||
}
|
|
||||||
return buildExecuteResponse(task, steps);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void executeTaskInBackground(String taskId) {
|
|
||||||
InitTaskEntity task = requireTask(taskId);
|
|
||||||
List<InitTaskStepEntity> steps = new ArrayList<>(initTaskStepRepository.findByTaskIdOrderByStepNo(taskId));
|
|
||||||
if (steps.isEmpty()) {
|
|
||||||
task.setStatus("FAILED");
|
|
||||||
task.setUpdateTime(LocalDateTime.now());
|
|
||||||
initTaskRepository.update(task);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
steps.sort(Comparator.comparing(item -> item.getStepNo() == null ? Integer.MAX_VALUE : item.getStepNo()));
|
|
||||||
resetResidualRunningSteps(steps);
|
|
||||||
initTaskStepRepository.updateBatch(steps);
|
|
||||||
|
|
||||||
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<InitTaskStepEntity> steps) {
|
|
||||||
for (InitTaskStepEntity step : steps) {
|
|
||||||
if (!"RUNNING".equals(step.getStatus())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
step.setStatus("FAILED");
|
|
||||||
step.setMessage(isBlank(step.getMessage()) ? "interrupted previously" : step.getMessage());
|
|
||||||
step.setUpdateTime(LocalDateTime.now());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private InitTaskExecuteResponse buildExecuteResponse(InitTaskEntity task, List<InitTaskStepEntity> steps) {
|
|
||||||
InitTaskExecuteResponse response = new InitTaskExecuteResponse();
|
|
||||||
response.setTaskId(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<String> steps) {
|
|
||||||
Map<String, Object> 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<String> steps) {
|
|
||||||
Map<String, Object> 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<String, Object> 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<String> 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<String> 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<InitTaskEntity> 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<InitTaskEntity> 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<LocalDateTime> 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<String> resolveStandardMqAllowed(String productType) {
|
|
||||||
if ("ENTERPRISE".equals(productType)) {
|
|
||||||
return MQ_ENTERPRISE;
|
|
||||||
}
|
|
||||||
if ("INDIRECT".equals(productType)) {
|
|
||||||
return MQ_INDIRECT;
|
|
||||||
}
|
|
||||||
throw new IllegalArgumentException("unsupported standard productType: " + productType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateDirect(InitPreviewRequest request, String deployMode, String mqType) {
|
|
||||||
requireAllowed("deployMode", deployMode, DEPLOY_DIRECT);
|
|
||||||
requireAllowed("mq.mqType", mqType, MQ_DIRECT);
|
|
||||||
|
|
||||||
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<String> 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(
|
|
||||||
String initState,
|
String initState,
|
||||||
String currentTaskType,
|
String currentTaskType,
|
||||||
String currentTaskStatus,
|
String currentTaskStatus,
|
||||||
@@ -935,13 +47,4 @@ public class InitService {
|
|||||||
String initializedMqType
|
String initializedMqType
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ResolvedResetContext(
|
|
||||||
String productType,
|
|
||||||
String orgCode,
|
|
||||||
String mqType,
|
|
||||||
String channelUsername,
|
|
||||||
String sourceTaskId
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<String> PRESET_PRODUCT_TYPES = Set.of("ENTERPRISE", "INDIRECT", "DIRECT");
|
||||||
|
private static final Set<String> DEPLOY_STANDARD = Set.of("SINGLE", "DUAL");
|
||||||
|
private static final Set<String> DEPLOY_DIRECT = Set.of("SINGLE", "DUAL", "QUAD");
|
||||||
|
private static final Set<String> MQ_ENTERPRISE = Set.of("RABBITMQ", "TLQ");
|
||||||
|
private static final Set<String> MQ_INDIRECT = Set.of("RABBITMQ", "RABBITMQ_TLQ");
|
||||||
|
private static final Set<String> MQ_DIRECT = Set.of("RABBITMQ_TLQ", "RABBITMQ_CFMQ");
|
||||||
|
private static final Pattern FILE_ID_PATTERN = Pattern.compile("^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$");
|
||||||
|
|
||||||
|
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<String, CompletableFuture<Void>> activeExecutions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
InitServiceImpl(
|
||||||
|
InitTaskRepository initTaskRepository,
|
||||||
|
InitTaskStepRepository initTaskStepRepository,
|
||||||
|
CisdPresetProperties cisdPresetProperties,
|
||||||
|
ObjectMapper objectMapper,
|
||||||
|
InitStepExecutor initStepExecutor,
|
||||||
|
InitCommandProfileService initCommandProfileService,
|
||||||
|
InitExecutorProperties initExecutorProperties,
|
||||||
|
Executor backgroundExecutor,
|
||||||
|
ConcurrentMap<String, CompletableFuture<Void>> activeExecutions
|
||||||
|
) {
|
||||||
|
this.initTaskRepository = initTaskRepository;
|
||||||
|
this.initTaskStepRepository = initTaskStepRepository;
|
||||||
|
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<String> 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<InitTaskStepEntity> 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<String> 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<InitTaskStepEntity> 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<InitTaskEntity> 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<InitTaskEntity> latestInit = initTaskRepository.findLatestByTaskTypeAndStatus(TASK_TYPE_INIT, "SUCCESS");
|
||||||
|
Optional<InitTaskEntity> 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<InitTaskEntity> 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<InitTaskStepResponse> 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<InitTaskStepEntity> 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<Void> active = activeExecutions.get(taskId);
|
||||||
|
if (active != null && !active.isDone()) {
|
||||||
|
task.setStatus("RUNNING");
|
||||||
|
task.setUpdateTime(LocalDateTime.now());
|
||||||
|
initTaskRepository.update(task);
|
||||||
|
return buildExecuteResponse(task, steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
task.setStatus("RUNNING");
|
||||||
|
task.setUpdateTime(LocalDateTime.now());
|
||||||
|
initTaskRepository.update(task);
|
||||||
|
|
||||||
|
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> executeTaskInBackground(taskId), backgroundExecutor)
|
||||||
|
.whenComplete((ignored, ex) -> activeExecutions.remove(taskId));
|
||||||
|
activeExecutions.put(taskId, future);
|
||||||
|
}
|
||||||
|
return buildExecuteResponse(task, steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeTaskInBackground(String taskId) {
|
||||||
|
InitTaskEntity task = requireTask(taskId);
|
||||||
|
List<InitTaskStepEntity> steps = new ArrayList<>(initTaskStepRepository.findByTaskIdOrderByStepNo(taskId));
|
||||||
|
if (steps.isEmpty()) {
|
||||||
|
task.setStatus("FAILED");
|
||||||
|
task.setUpdateTime(LocalDateTime.now());
|
||||||
|
initTaskRepository.update(task);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
steps.sort(Comparator.comparing(item -> item.getStepNo() == null ? Integer.MAX_VALUE : item.getStepNo()));
|
||||||
|
resetResidualRunningSteps(steps);
|
||||||
|
initTaskStepRepository.updateBatch(steps);
|
||||||
|
|
||||||
|
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<InitTaskStepEntity> steps) {
|
||||||
|
for (InitTaskStepEntity step : steps) {
|
||||||
|
if (!"RUNNING".equals(step.getStatus())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
step.setStatus("FAILED");
|
||||||
|
step.setMessage(isBlank(step.getMessage()) ? "interrupted previously" : step.getMessage());
|
||||||
|
step.setUpdateTime(LocalDateTime.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private InitTaskExecuteResponse buildExecuteResponse(InitTaskEntity task, List<InitTaskStepEntity> steps) {
|
||||||
|
InitTaskExecuteResponse response = new InitTaskExecuteResponse();
|
||||||
|
response.setTaskId(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<String> steps) {
|
||||||
|
Map<String, Object> 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<String> steps) {
|
||||||
|
Map<String, Object> 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<String, Object> 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<String> 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<String> 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<InitTaskEntity> 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<InitTaskEntity> 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<LocalDateTime> 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<String> resolveStandardMqAllowed(String productType) {
|
||||||
|
if ("ENTERPRISE".equals(productType)) {
|
||||||
|
return MQ_ENTERPRISE;
|
||||||
|
}
|
||||||
|
if ("INDIRECT".equals(productType)) {
|
||||||
|
return MQ_INDIRECT;
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("unsupported standard productType: " + productType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateDirect(InitPreviewRequest request, String deployMode, String mqType) {
|
||||||
|
requireAllowed("deployMode", deployMode, DEPLOY_DIRECT);
|
||||||
|
requireAllowed("mq.mqType", mqType, MQ_DIRECT);
|
||||||
|
|
||||||
|
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<String> 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
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@@ -19,15 +20,12 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1")
|
@RequestMapping("/api/v1")
|
||||||
|
@RequiredArgsConstructor
|
||||||
@Tag(name = "主密钥管理", description = "LMK 生成、恢复、状态查询和销毁接口")
|
@Tag(name = "主密钥管理", description = "LMK 生成、恢复、状态查询和销毁接口")
|
||||||
public class LmkController {
|
public class LmkController {
|
||||||
|
|
||||||
private final LmkService lmkService;
|
private final LmkService lmkService;
|
||||||
|
|
||||||
public LmkController(LmkService lmkService, com.fasterxml.jackson.databind.ObjectMapper objectMapper) {
|
|
||||||
this.lmkService = lmkService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/masterKey/init")
|
@PostMapping("/masterKey/init")
|
||||||
@Operation(summary = "初始化主密钥", description = "执行密码卡初始化识别、主密钥生成加载、内部密钥生成,并返回主密钥状态与校验值。")
|
@Operation(summary = "初始化主密钥", description = "执行密码卡初始化识别、主密钥生成加载、内部密钥生成,并返回主密钥状态与校验值。")
|
||||||
public ApiResponse<MasterKeyStateResult> initMasterKey() {
|
public ApiResponse<MasterKeyStateResult> initMasterKey() {
|
||||||
|
|||||||
@@ -1,400 +1,35 @@
|
|||||||
package com.cisd.tms.modules.mk.service;
|
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.MasterKeyBackupMaterial;
|
||||||
import com.cisd.tms.modules.mk.dto.MasterKeyPacketIkPart;
|
import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket;
|
||||||
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.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 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.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
public interface LmkService {
|
||||||
* @author liulu
|
|
||||||
* @since 2026/3/5
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
public class LmkService {
|
|
||||||
|
|
||||||
private static final int AUTH_SIGN_KEY_INDEX = IKEnums.KEY_TYPE_AUTH.getCode();
|
MasterKeyStateResult initMasterKey();
|
||||||
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;
|
|
||||||
|
|
||||||
public LmkService(PcieCryptoService pcieCryptoService, MasterKeyInitProperties masterKeyInitProperties) {
|
void destroyMasterKey();
|
||||||
this.pcieCryptoService = pcieCryptoService;
|
|
||||||
this.masterKeyInitProperties = masterKeyInitProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
MasterKeyStateResult queryMasterKeyStatus();
|
||||||
|
|
||||||
public MasterKeyStateResult initMasterKey() {
|
String exportIkPublicKeyHex();
|
||||||
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();
|
String signIk(String rawData);
|
||||||
result.setStatus(true);
|
|
||||||
result.setSeedMac(Hex.toHexString(pcieCryptoService.initializeMasterKey(oldPin, newPin)));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void destroyMasterKey() {
|
void verifyIk(String rawData, String signatureHex);
|
||||||
pcieCryptoService.destroyLmk();
|
|
||||||
}
|
|
||||||
|
|
||||||
public MasterKeyStateResult queryMasterKeyStatus() {
|
MasterKeyBackupPacket buildBackupPacket(int componentIndex);
|
||||||
MasterKeyStateResult result = new MasterKeyStateResult();
|
|
||||||
boolean status = pcieCryptoService.checkLmk();
|
|
||||||
result.setStatus(status);
|
|
||||||
if (status) {
|
|
||||||
result.setSeedMac(Hex.toHexString(pcieCryptoService.exportLmkSeedMac()));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String exportIkPublicKeyHex() {
|
MasterKeyBackupMaterial getBackupMaterial(int uid);
|
||||||
int outBufferSize = new EccRefPublicKey().size();
|
|
||||||
BackupDataResult result = pcieCryptoService.exportSignPublicKeyEcc(AUTH_SIGN_KEY_INDEX, outBufferSize);
|
|
||||||
return Hex.toHexString(result.getData());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String signIk(String rawData) {
|
String getComponent(int uid);
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void verifyIk(String rawData, String signatureHex) {
|
void recoverKeyPackets(List<MasterKeyBackupPacket> packets);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MasterKeyBackupPacket buildBackupPacket(int componentIndex) {
|
void recoverLmkEx(String lmkMacHex, List<String> components);
|
||||||
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<MasterKeyBackupPacket> packets) {
|
|
||||||
List<MasterKeyBackupPacket> 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<String> 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<String> 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<MasterKeyBackupPacket> 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<MasterKeyBackupPacket> 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<MasterKeyBackupPacket> 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<MasterKeyBackupPacket> 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<Integer, String> 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<Integer, String> 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<Integer, String> 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<MasterKeyBackupPacket> sortPackets(List<MasterKeyBackupPacket> packets) {
|
|
||||||
if (packets == null || packets.size() != LMKConstant.COMPONENT_NUM) {
|
|
||||||
throw new IllegalArgumentException("packets size must be " + LMKConstant.COMPONENT_NUM);
|
|
||||||
}
|
|
||||||
Map<Integer, MasterKeyBackupPacket> 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<MasterKeyBackupPacket> 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<MasterKeyBackupPacket> 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<MasterKeyBackupPacket> 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<Integer, String> componentMap) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private record ParsedIkBackup(String macHex, Map<Integer, String> componentMap) {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
MasterKeyStatus.StatusDetail getMasterKeyStatus();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<MasterKeyBackupPacket> packets) {
|
||||||
|
List<MasterKeyBackupPacket> 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<String> 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<String> 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<MasterKeyBackupPacket> 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<MasterKeyBackupPacket> 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<MasterKeyBackupPacket> 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<MasterKeyBackupPacket> 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<Integer, String> 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<Integer, String> 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<Integer, String> 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<MasterKeyBackupPacket> 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<MasterKeyBackupPacket> 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<MasterKeyBackupPacket> sortPackets(List<MasterKeyBackupPacket> packets) {
|
||||||
|
if (packets == null || packets.size() != LMKConstant.COMPONENT_NUM) {
|
||||||
|
throw new IllegalArgumentException("packets size must be " + LMKConstant.COMPONENT_NUM);
|
||||||
|
}
|
||||||
|
List<MasterKeyBackupPacket> 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<Integer, String> componentMap) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ParsedIkBackup(String macHex, Map<Integer, String> componentMap) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -204,7 +204,7 @@ tms:
|
|||||||
# 密码卡模式:MOCK(模拟)/JNA(动态库调用)等。
|
# 密码卡模式:MOCK(模拟)/JNA(动态库调用)等。
|
||||||
mode: ${TMS_CRYPTO_CARD_MODE:JNA}
|
mode: ${TMS_CRYPTO_CARD_MODE:JNA}
|
||||||
# 动态库目录(.so 所在目录)。
|
# 动态库目录(.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}
|
vendor-lib-name: ${TMS_CRYPTO_VENDOR_LIB_NAME:sdf_ukey_syd1408}
|
||||||
# 是否严格校验算法标识(与 0018/厂商补充定义一致性校验)。
|
# 是否严格校验算法标识(与 0018/厂商补充定义一致性校验)。
|
||||||
|
|||||||
@@ -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.dto.DeviceProfileSoftwareItem;
|
||||||
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
|
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
|
||||||
import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository;
|
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.SystemHardwareProbe;
|
||||||
import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe;
|
import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe;
|
||||||
import com.cisd.tms.modules.init.config.InitCommandProfileService;
|
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.InitTaskRepository;
|
||||||
import com.cisd.tms.modules.init.repository.InitTaskStepRepository;
|
import com.cisd.tms.modules.init.repository.InitTaskStepRepository;
|
||||||
import com.cisd.tms.modules.init.service.InitService;
|
import com.cisd.tms.modules.init.service.InitService;
|
||||||
|
import com.cisd.tms.modules.init.service.impl.InitServiceImpl;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -47,7 +49,7 @@ class DeviceProfileServiceTest {
|
|||||||
versionRepository.save(component("NGINX", "Nginx", "Nginx V1.20"));
|
versionRepository.save(component("NGINX", "Nginx", "Nginx V1.20"));
|
||||||
versionRepository.save(component("APP", "标准收发器间参版", "标准收发器间参版 V1.3.2"));
|
versionRepository.save(component("APP", "标准收发器间参版", "标准收发器间参版 V1.3.2"));
|
||||||
|
|
||||||
DeviceProfileService service = new DeviceProfileService(
|
DeviceProfileService service = new DeviceProfileServiceImpl(
|
||||||
initService,
|
initService,
|
||||||
cryptoService(),
|
cryptoService(),
|
||||||
profileProperties(),
|
profileProperties(),
|
||||||
@@ -78,7 +80,7 @@ class DeviceProfileServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnOnlyBaseSoftwareWhenNoSuccessfulInitSnapshotExists() {
|
void shouldReturnOnlyBaseSoftwareWhenNoSuccessfulInitSnapshotExists() {
|
||||||
DeviceProfileService service = new DeviceProfileService(
|
DeviceProfileService service = new DeviceProfileServiceImpl(
|
||||||
newInitService("ENTERPRISE"),
|
newInitService("ENTERPRISE"),
|
||||||
cryptoService(),
|
cryptoService(),
|
||||||
profileProperties(),
|
profileProperties(),
|
||||||
@@ -111,7 +113,7 @@ class DeviceProfileServiceTest {
|
|||||||
cached.setCurrentVersion("Nginx V1.20");
|
cached.setCurrentVersion("Nginx V1.20");
|
||||||
versionRepository.saveOrUpdate(cached);
|
versionRepository.saveOrUpdate(cached);
|
||||||
|
|
||||||
DeviceProfileService service = new DeviceProfileService(
|
DeviceProfileService service = new DeviceProfileServiceImpl(
|
||||||
initService,
|
initService,
|
||||||
cryptoService(),
|
cryptoService(),
|
||||||
profileProperties(),
|
profileProperties(),
|
||||||
@@ -138,7 +140,7 @@ class DeviceProfileServiceTest {
|
|||||||
InitService initService = newInitService("ENTERPRISE");
|
InitService initService = newInitService("ENTERPRISE");
|
||||||
createSuccessfulInitTask(initService, validInitRequest("RABBITMQ"));
|
createSuccessfulInitTask(initService, validInitRequest("RABBITMQ"));
|
||||||
|
|
||||||
DeviceProfileService service = new DeviceProfileService(
|
DeviceProfileService service = new DeviceProfileServiceImpl(
|
||||||
initService,
|
initService,
|
||||||
cryptoService(),
|
cryptoService(),
|
||||||
profileProperties(),
|
profileProperties(),
|
||||||
@@ -171,7 +173,7 @@ class DeviceProfileServiceTest {
|
|||||||
properties.setMemoryTotalDefault("16GB");
|
properties.setMemoryTotalDefault("16GB");
|
||||||
properties.setDiskTotalDefault("256G");
|
properties.setDiskTotalDefault("256G");
|
||||||
|
|
||||||
DeviceProfileService service = new DeviceProfileService(
|
DeviceProfileService service = new DeviceProfileServiceImpl(
|
||||||
newInitService("ENTERPRISE"),
|
newInitService("ENTERPRISE"),
|
||||||
cryptoService(),
|
cryptoService(),
|
||||||
properties,
|
properties,
|
||||||
@@ -308,7 +310,7 @@ class DeviceProfileServiceTest {
|
|||||||
properties.setLogDir("/tmp/tms-init-logs");
|
properties.setLogDir("/tmp/tms-init-logs");
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
InitCommandProfileService commandProfileService = new InitCommandProfileService(objectMapper, properties);
|
InitCommandProfileService commandProfileService = new InitCommandProfileService(objectMapper, properties);
|
||||||
return new InitService(
|
return new InitServiceImpl(
|
||||||
new InMemoryInitTaskRepository(),
|
new InMemoryInitTaskRepository(),
|
||||||
new InMemoryInitTaskStepRepository(),
|
new InMemoryInitTaskStepRepository(),
|
||||||
preset(productType),
|
preset(productType),
|
||||||
|
|||||||
+6
-4
@@ -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.dto.SoftwareRuntimeStatusItem;
|
||||||
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
|
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
|
||||||
import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository;
|
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.HardwareStatusProbe;
|
||||||
import com.cisd.tms.modules.device.support.SoftwareRuntimeProbe;
|
import com.cisd.tms.modules.device.support.SoftwareRuntimeProbe;
|
||||||
import com.cisd.tms.modules.device.support.UsageSnapshot;
|
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.InitTaskRepository;
|
||||||
import com.cisd.tms.modules.init.repository.InitTaskStepRepository;
|
import com.cisd.tms.modules.init.repository.InitTaskStepRepository;
|
||||||
import com.cisd.tms.modules.init.service.InitService;
|
import com.cisd.tms.modules.init.service.InitService;
|
||||||
|
import com.cisd.tms.modules.init.service.impl.InitServiceImpl;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -48,7 +50,7 @@ class DeviceRuntimeStatusServiceTest {
|
|||||||
versions.save(component("NGINX", "Nginx", "Nginx V1.20"));
|
versions.save(component("NGINX", "Nginx", "Nginx V1.20"));
|
||||||
versions.save(component("APP", "标准收发器企业版", "标准收发器企业版 V1.3.2"));
|
versions.save(component("APP", "标准收发器企业版", "标准收发器企业版 V1.3.2"));
|
||||||
|
|
||||||
DeviceRuntimeStatusService service = new DeviceRuntimeStatusService(
|
DeviceRuntimeStatusService service = new DeviceRuntimeStatusServiceImpl(
|
||||||
initService,
|
initService,
|
||||||
cryptoService(),
|
cryptoService(),
|
||||||
hardwareProbe(23.22, 23.55, 32.0, 500.32, 1024.0),
|
hardwareProbe(23.22, 23.55, 32.0, 500.32, 1024.0),
|
||||||
@@ -82,7 +84,7 @@ class DeviceRuntimeStatusServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldMarkNonInitializedSoftwareAsNotInstalled() {
|
void shouldMarkNonInitializedSoftwareAsNotInstalled() {
|
||||||
DeviceRuntimeStatusService service = new DeviceRuntimeStatusService(
|
DeviceRuntimeStatusService service = new DeviceRuntimeStatusServiceImpl(
|
||||||
newInitService("ENTERPRISE"),
|
newInitService("ENTERPRISE"),
|
||||||
cryptoService(),
|
cryptoService(),
|
||||||
hardwareProbe(10.0, 8.0, 16.0, 100.0, 256.0),
|
hardwareProbe(10.0, 8.0, 16.0, 100.0, 256.0),
|
||||||
@@ -107,7 +109,7 @@ class DeviceRuntimeStatusServiceTest {
|
|||||||
InitService initService = newInitService("INDIRECT");
|
InitService initService = newInitService("INDIRECT");
|
||||||
createSuccessfulInitTask(initService, validInitRequest("RABBITMQ_TLQ"));
|
createSuccessfulInitTask(initService, validInitRequest("RABBITMQ_TLQ"));
|
||||||
|
|
||||||
DeviceRuntimeStatusService service = new DeviceRuntimeStatusService(
|
DeviceRuntimeStatusService service = new DeviceRuntimeStatusServiceImpl(
|
||||||
initService,
|
initService,
|
||||||
failingCryptoService(),
|
failingCryptoService(),
|
||||||
failingHardwareProbe(),
|
failingHardwareProbe(),
|
||||||
@@ -334,7 +336,7 @@ class DeviceRuntimeStatusServiceTest {
|
|||||||
properties.setLogDir("/tmp/tms-init-logs");
|
properties.setLogDir("/tmp/tms-init-logs");
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
InitCommandProfileService commandProfileService = new InitCommandProfileService(objectMapper, properties);
|
InitCommandProfileService commandProfileService = new InitCommandProfileService(objectMapper, properties);
|
||||||
return new InitService(
|
return new InitServiceImpl(
|
||||||
new InMemoryInitTaskRepository(),
|
new InMemoryInitTaskRepository(),
|
||||||
new InMemoryInitTaskStepRepository(),
|
new InMemoryInitTaskStepRepository(),
|
||||||
preset(productType),
|
preset(productType),
|
||||||
|
|||||||
@@ -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.dto.DeviceInfoResponse;
|
||||||
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
|
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
|
||||||
import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository;
|
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.InitCommandProfileService;
|
||||||
import com.cisd.tms.modules.init.config.InitExecutorProperties;
|
import com.cisd.tms.modules.init.config.InitExecutorProperties;
|
||||||
import com.cisd.tms.modules.init.dto.InitCreateTaskResponse;
|
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.InitTaskRepository;
|
||||||
import com.cisd.tms.modules.init.repository.InitTaskStepRepository;
|
import com.cisd.tms.modules.init.repository.InitTaskStepRepository;
|
||||||
import com.cisd.tms.modules.init.service.InitService;
|
import com.cisd.tms.modules.init.service.InitService;
|
||||||
|
import com.cisd.tms.modules.init.service.impl.InitServiceImpl;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -52,7 +54,7 @@ class DeviceServiceTest {
|
|||||||
InMemoryInitTaskRepository taskRepository = new InMemoryInitTaskRepository();
|
InMemoryInitTaskRepository taskRepository = new InMemoryInitTaskRepository();
|
||||||
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
|
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
|
||||||
InitService initService = newInitService(taskRepository, stepRepository, "ENTERPRISE");
|
InitService initService = newInitService(taskRepository, stepRepository, "ENTERPRISE");
|
||||||
DeviceService deviceService = new DeviceService(
|
DeviceService deviceService = new DeviceServiceImpl(
|
||||||
deviceProfile("MODEL-X", "设备A"),
|
deviceProfile("MODEL-X", "设备A"),
|
||||||
fixedVersionRepository("TMS-V3.1.4"),
|
fixedVersionRepository("TMS-V3.1.4"),
|
||||||
cryptoService("SERIAL-ABC", 0),
|
cryptoService("SERIAL-ABC", 0),
|
||||||
@@ -77,7 +79,7 @@ class DeviceServiceTest {
|
|||||||
InMemoryInitTaskRepository taskRepository = new InMemoryInitTaskRepository();
|
InMemoryInitTaskRepository taskRepository = new InMemoryInitTaskRepository();
|
||||||
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
|
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
|
||||||
InitService initService = newInitService(taskRepository, stepRepository, productType);
|
InitService initService = newInitService(taskRepository, stepRepository, productType);
|
||||||
return new DeviceService(
|
return new DeviceServiceImpl(
|
||||||
deviceProfile("SYD7108", "跨境支付终端一体机"),
|
deviceProfile("SYD7108", "跨境支付终端一体机"),
|
||||||
fixedVersionRepository(tmsVersion),
|
fixedVersionRepository(tmsVersion),
|
||||||
cryptoService(serialNumber, fsmState),
|
cryptoService(serialNumber, fsmState),
|
||||||
@@ -95,7 +97,7 @@ class DeviceServiceTest {
|
|||||||
properties.setLogDir("/tmp/tms-init-logs");
|
properties.setLogDir("/tmp/tms-init-logs");
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
InitCommandProfileService commandProfileService = new InitCommandProfileService(objectMapper, properties);
|
InitCommandProfileService commandProfileService = new InitCommandProfileService(objectMapper, properties);
|
||||||
return new InitService(
|
return new InitServiceImpl(
|
||||||
taskRepository,
|
taskRepository,
|
||||||
stepRepository,
|
stepRepository,
|
||||||
preset(productType),
|
preset(productType),
|
||||||
|
|||||||
@@ -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.entity.FileRecordEntity;
|
||||||
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||||
import com.cisd.tms.modules.file.service.FileService;
|
import com.cisd.tms.modules.file.service.FileService;
|
||||||
|
import com.cisd.tms.modules.file.service.impl.FileServiceImpl;
|
||||||
import java.io.IOException;
|
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.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
@@ -18,6 +22,8 @@ import org.springframework.mock.web.MockMultipartFile;
|
|||||||
|
|
||||||
class FileServiceTest {
|
class FileServiceTest {
|
||||||
|
|
||||||
|
private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-03-31T06:00:00Z"), ZoneOffset.UTC);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldPersistUploadedFileMetadata() {
|
void shouldPersistUploadedFileMetadata() {
|
||||||
InMemoryFileRecordRepository repository = new InMemoryFileRecordRepository();
|
InMemoryFileRecordRepository repository = new InMemoryFileRecordRepository();
|
||||||
@@ -44,7 +50,7 @@ class FileServiceTest {
|
|||||||
void shouldSaveUploadedFileUnderFileIdDirectory() throws IOException {
|
void shouldSaveUploadedFileUnderFileIdDirectory() throws IOException {
|
||||||
Path uploadBaseDir = Files.createTempDirectory("file-service-upload-test");
|
Path uploadBaseDir = Files.createTempDirectory("file-service-upload-test");
|
||||||
InMemoryFileRecordRepository repository = new InMemoryFileRecordRepository();
|
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(
|
MockMultipartFile file = new MockMultipartFile(
|
||||||
"file",
|
"file",
|
||||||
"receiver-license.dat",
|
"receiver-license.dat",
|
||||||
@@ -66,7 +72,7 @@ class FileServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldRejectBlankFileUpload() throws IOException {
|
void shouldRejectBlankFileUpload() throws IOException {
|
||||||
Path uploadBaseDir = Files.createTempDirectory("file-service-upload-empty-test");
|
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]);
|
MockMultipartFile file = new MockMultipartFile("file", "empty.txt", "text/plain", new byte[0]);
|
||||||
|
|
||||||
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> service.upload(file));
|
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> service.upload(file));
|
||||||
@@ -77,7 +83,7 @@ class FileServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldRejectOversizeUpload() throws IOException {
|
void shouldRejectOversizeUpload() throws IOException {
|
||||||
Path uploadBaseDir = Files.createTempDirectory("file-service-upload-oversize-test");
|
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());
|
MockMultipartFile file = new MockMultipartFile("file", "big.txt", "text/plain", "12345".getBytes());
|
||||||
|
|
||||||
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> service.upload(file));
|
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> service.upload(file));
|
||||||
@@ -88,7 +94,7 @@ class FileServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldReturnStoredFileDetailByFileId() throws IOException {
|
void shouldReturnStoredFileDetailByFileId() throws IOException {
|
||||||
Path uploadBaseDir = Files.createTempDirectory("file-service-detail-test");
|
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());
|
MockMultipartFile file = new MockMultipartFile("file", "front.zip", "application/zip", "zip-data".getBytes());
|
||||||
|
|
||||||
FileUploadResponse uploaded = service.upload(file);
|
FileUploadResponse uploaded = service.upload(file);
|
||||||
|
|||||||
@@ -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.executor.InitStepExecutor;
|
||||||
import com.cisd.tms.modules.init.repository.InitTaskRepository;
|
import com.cisd.tms.modules.init.repository.InitTaskRepository;
|
||||||
import com.cisd.tms.modules.init.repository.InitTaskStepRepository;
|
import com.cisd.tms.modules.init.repository.InitTaskStepRepository;
|
||||||
|
import com.cisd.tms.modules.init.service.impl.InitServiceImpl;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -643,7 +644,7 @@ class InitServiceTest {
|
|||||||
properties.setMode(InitExecutorProperties.Mode.SIMULATE);
|
properties.setMode(InitExecutorProperties.Mode.SIMULATE);
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
InitCommandProfileService commandProfileService = new InitCommandProfileService(objectMapper, properties);
|
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() {
|
private static InitTaskRepository emptyRepository() {
|
||||||
|
|||||||
@@ -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\"")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.MasterKeyPacketLmkPart;
|
||||||
import com.cisd.tms.modules.mk.dto.MasterKeyPacketUserKeyPart;
|
import com.cisd.tms.modules.mk.dto.MasterKeyPacketUserKeyPart;
|
||||||
import com.cisd.tms.modules.mk.dto.MasterKeyStateResult;
|
import com.cisd.tms.modules.mk.dto.MasterKeyStateResult;
|
||||||
|
import com.cisd.tms.modules.mk.service.impl.LmkServiceImpl;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
@@ -29,7 +30,7 @@ class LmkServiceTest {
|
|||||||
MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE=");
|
MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE=");
|
||||||
Mockito.when(pcieCryptoService.initializeMasterKey(Mockito.any(), Mockito.any())).thenReturn(new byte[] {0x01, 0x23, 0x45, 0x67});
|
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();
|
MasterKeyStateResult result = service.initMasterKey();
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ class LmkServiceTest {
|
|||||||
Mockito.when(pcieCryptoService.initializeMasterKey(Mockito.any(), Mockito.any()))
|
Mockito.when(pcieCryptoService.initializeMasterKey(Mockito.any(), Mockito.any()))
|
||||||
.thenThrow(new IllegalStateException("master key initialization failed"));
|
.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);
|
IllegalStateException exception = Assertions.assertThrows(IllegalStateException.class, service::initMasterKey);
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ class LmkServiceTest {
|
|||||||
MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE=");
|
MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE=");
|
||||||
Mockito.when(pcieCryptoService.checkLmk()).thenReturn(false);
|
Mockito.when(pcieCryptoService.checkLmk()).thenReturn(false);
|
||||||
|
|
||||||
LmkService service = new LmkService(pcieCryptoService, properties);
|
LmkService service = new LmkServiceImpl(pcieCryptoService, properties);
|
||||||
|
|
||||||
MasterKeyStateResult result = service.queryMasterKeyStatus();
|
MasterKeyStateResult result = service.queryMasterKeyStatus();
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ class LmkServiceTest {
|
|||||||
Mockito.when(pcieCryptoService.checkLmk()).thenReturn(true);
|
Mockito.when(pcieCryptoService.checkLmk()).thenReturn(true);
|
||||||
Mockito.when(pcieCryptoService.exportLmkSeedMac()).thenReturn(new byte[] {(byte) 0xAA, (byte) 0xBB});
|
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();
|
MasterKeyStateResult result = service.queryMasterKeyStatus();
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ class LmkServiceTest {
|
|||||||
Mockito.when(pcieCryptoService.backupUserKey(1, 0, 120)).thenReturn(userSignBackup);
|
Mockito.when(pcieCryptoService.backupUserKey(1, 0, 120)).thenReturn(userSignBackup);
|
||||||
Mockito.when(pcieCryptoService.backupUserKey(1, 1, 120)).thenReturn(userEncBackup);
|
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);
|
MasterKeyBackupPacket packet = service.buildBackupPacket(2);
|
||||||
|
|
||||||
@@ -142,7 +143,7 @@ class LmkServiceTest {
|
|||||||
void shouldRecoverAllKeyPacketsInOrder() {
|
void shouldRecoverAllKeyPacketsInOrder() {
|
||||||
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
|
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
|
||||||
MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE=");
|
MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE=");
|
||||||
LmkService service = new LmkService(pcieCryptoService, properties);
|
LmkService service = new LmkServiceImpl(pcieCryptoService, properties);
|
||||||
|
|
||||||
MasterKeyBackupPacket packet1 = new MasterKeyBackupPacket();
|
MasterKeyBackupPacket packet1 = new MasterKeyBackupPacket();
|
||||||
packet1.setComponentIndex(1);
|
packet1.setComponentIndex(1);
|
||||||
@@ -220,7 +221,7 @@ class LmkServiceTest {
|
|||||||
Mockito.when(pcieCryptoService.digest(Mockito.any())).thenReturn(digestResult);
|
Mockito.when(pcieCryptoService.digest(Mockito.any())).thenReturn(digestResult);
|
||||||
Mockito.when(pcieCryptoService.eccInternalSign(Mockito.any())).thenReturn(signResult);
|
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");
|
String signatureHex = service.signIk("1223");
|
||||||
|
|
||||||
@@ -249,7 +250,7 @@ class LmkServiceTest {
|
|||||||
});
|
});
|
||||||
Mockito.when(pcieCryptoService.digest(Mockito.any())).thenReturn(digestResult);
|
Mockito.when(pcieCryptoService.digest(Mockito.any())).thenReturn(digestResult);
|
||||||
|
|
||||||
LmkService service = new LmkService(pcieCryptoService, properties);
|
LmkService service = new LmkServiceImpl(pcieCryptoService, properties);
|
||||||
|
|
||||||
service.verifyIk("1223", "aabb");
|
service.verifyIk("1223", "aabb");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user