service改造

This commit is contained in:
waner
2026-03-31 16:54:57 +08:00
parent 9b4203eacb
commit 1dec24a10c
32 changed files with 2122 additions and 2188 deletions
@@ -12,6 +12,8 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -20,18 +22,15 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
@Tag(name = "认证管理", description = "角色启用、密码重置与 UKey 绑定接口")
public class AuthAdminController {
private final AuthAdminService authAdminService;
public AuthAdminController(AuthAdminService authAdminService) {
this.authAdminService = authAdminService;
}
@PostMapping("/roles/{roleCode}/enable")
@Operation(summary = "启用角色", description = "仅允许 KEY_ADMIN FULL 会话启用目标角色。")
@RequireRole("KEY_ADMIN")
@Operation(summary = "启用角色", description = "仅允许 SUPER_ADMIN FULL 会话启用目标角色。")
@RequireRole("SUPER_ADMIN")
@RequireAuthLevel("FULL")
public ApiResponse<Void> enableRole(@PathVariable("roleCode") String roleCode, HttpServletRequest request) {
authAdminService.enableRole(
@@ -43,8 +42,8 @@ public class AuthAdminController {
}
@PostMapping("/roles/{roleCode}/reset-password")
@Operation(summary = "重置角色密码", description = "仅允许 KEY_ADMIN FULL 会话重置目标角色密码。")
@RequireRole("KEY_ADMIN")
@Operation(summary = "重置角色密码", description = "仅允许 SUPER_ADMIN FULL 会话重置目标角色密码。")
@RequireRole("SUPER_ADMIN")
@RequireAuthLevel("FULL")
public ApiResponse<Void> resetPassword(@PathVariable("roleCode") String roleCode, HttpServletRequest request) {
authAdminService.resetPassword(
@@ -56,8 +55,8 @@ public class AuthAdminController {
}
@PostMapping("/roles/{roleCode}/ukeys/bind")
@Operation(summary = "绑定角色 UKey", description = "仅允许 KEY_ADMIN FULL 会话登记目标角色的 UKey 绑定信息。")
@RequireRole("KEY_ADMIN")
@Operation(summary = "绑定角色 UKey", description = "仅允许 SUPER_ADMIN FULL 会话登记目标角色的 UKey 绑定信息。")
@RequireRole("SUPER_ADMIN")
@RequireAuthLevel("FULL")
public ApiResponse<Void> bindUkey(
@PathVariable("roleCode") String roleCode,
@@ -80,7 +79,7 @@ public class AuthAdminController {
@PostMapping("/roles/{roleCode}/ukeys/issue-sign")
@Operation(summary = "生成 UKey 发行签名", description = "按旧绑定流程为目标角色 UKey 材料生成发行签名。")
// @RequireRole("KEY_ADMIN")
// @RequireRole("SUPER_ADMIN")
@RequireAuthLevel("FULL")
public ApiResponse<UKeySignResult> issueUkeyBindingSign(
@PathVariable("roleCode") String roleCode,
@@ -15,6 +15,8 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -23,15 +25,12 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
@Tag(name = "认证鉴权", description = "管理端认证与当前会话接口")
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/password-login")
@Operation(summary = "口令登录", description = "按旧系统受限登录流程签发 LIMITED 会话。")
public ApiResponse<LoginResponse> passwordLogin(@Valid @RequestBody PasswordLoginRequest request) {
@@ -25,11 +25,11 @@ import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@AllArgsConstructor
@RequiredArgsConstructor
public class AuthAdminServiceImpl implements AuthAdminService {
static final String DEFAULT_PASSWORD = "12345678";
@@ -4,11 +4,11 @@ import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.AuthMethod;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.service.AuthPolicyService;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@AllArgsConstructor
@RequiredArgsConstructor
public class AuthPolicyServiceImpl implements AuthPolicyService {
/**
@@ -39,11 +39,11 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@AllArgsConstructor
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {
private static final int MAX_FAILED_ATTEMPTS = 5;
@@ -41,6 +41,8 @@ import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import java.util.Base64;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -52,15 +54,12 @@ import org.springframework.web.bind.annotation.RestController;
@Validated
@RestController
@RequestMapping("/api/v1/device/crypto")
@RequiredArgsConstructor
@Tag(name = "密码卡调试", description = "密码卡设备调试、密钥操作和文件管理接口")
public class CryptoCardController {
private final PcieCryptoService pcieCryptoService;
public CryptoCardController(PcieCryptoService pcieCryptoService) {
this.pcieCryptoService = pcieCryptoService;
}
@GetMapping("/device-count")
@Operation(summary = "查询密码卡数量", description = "返回当前环境可见的密码卡设备数量。")
public ApiResponse<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.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -20,6 +21,7 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/device")
@RequiredArgsConstructor
@Tag(name = "设备概览", description = "设备基础信息、状态和重启接口")
public class DeviceController {
@@ -27,16 +29,6 @@ public class DeviceController {
private final DeviceProfileService deviceProfileService;
private final DeviceRuntimeStatusService deviceRuntimeStatusService;
public DeviceController(
DeviceService deviceService,
DeviceProfileService deviceProfileService,
DeviceRuntimeStatusService deviceRuntimeStatusService
) {
this.deviceService = deviceService;
this.deviceProfileService = deviceProfileService;
this.deviceRuntimeStatusService = deviceRuntimeStatusService;
}
@GetMapping("/info")
@Operation(summary = "查询设备信息", description = "返回设备型号、设备名称、TMS版本、设备序列号、管理密钥状态和当前设备初始化状态。")
public ApiResponse<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.service.TimeConfigService;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -13,16 +14,12 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/device")
@RequiredArgsConstructor
@Tag(name = "device-time-config-controller")
public class TimeConfigController {
private final TimeConfigService timeConfigService;
public TimeConfigController(TimeConfigService timeConfigService) {
this.timeConfigService = timeConfigService;
}
@PostMapping("/time-config")
public ApiResponse<Void> configureTime(@RequestBody TimeConfigRequest request) {
//todo 鉴权
@@ -1,178 +1,8 @@
package com.cisd.tms.modules.device.service;
import com.cisd.tms.common.config.properties.CisdPresetProperties;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.integration.crypto.pcie.model.DeviceInfoResult;
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
import com.cisd.tms.modules.device.config.DeviceProfileProperties;
import com.cisd.tms.modules.device.dto.DeviceProfileDeviceSection;
import com.cisd.tms.modules.device.dto.DeviceProfileHardwareSection;
import com.cisd.tms.modules.device.dto.DeviceProfileResponse;
import com.cisd.tms.modules.device.dto.DeviceProfileSoftwareItem;
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository;
import com.cisd.tms.modules.device.support.SystemHardwareProbe;
import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe;
import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse;
import com.cisd.tms.modules.init.service.InitService;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
@Service
public class DeviceProfileService {
public interface DeviceProfileService {
private final InitService initService;
private final PcieCryptoService pcieCryptoService;
private final DeviceProfileProperties deviceProfileProperties;
private final SystemHardwareProbe systemHardwareProbe;
private final SystemSoftwareVersionProbe systemSoftwareVersionProbe;
private final DeviceSoftwareVersionRepository deviceSoftwareVersionRepository;
private final CisdPresetProperties cisdPresetProperties;
public DeviceProfileService(
InitService initService,
PcieCryptoService pcieCryptoService,
DeviceProfileProperties deviceProfileProperties,
SystemHardwareProbe systemHardwareProbe,
SystemSoftwareVersionProbe systemSoftwareVersionProbe,
DeviceSoftwareVersionRepository deviceSoftwareVersionRepository,
CisdPresetProperties cisdPresetProperties
) {
this.initService = initService;
this.pcieCryptoService = pcieCryptoService;
this.deviceProfileProperties = deviceProfileProperties;
this.systemHardwareProbe = systemHardwareProbe;
this.systemSoftwareVersionProbe = systemSoftwareVersionProbe;
this.deviceSoftwareVersionRepository = deviceSoftwareVersionRepository;
this.cisdPresetProperties = cisdPresetProperties;
}
public DeviceProfileResponse profile() {
DeviceInfoResult deviceInfo = safeDeviceInfo();
DeviceProfileResponse response = new DeviceProfileResponse();
response.setDevice(buildDeviceSection(deviceInfo));
response.setHardware(buildHardwareSection(deviceInfo));
response.setSoftware(buildSoftwareItems());
return response;
}
private DeviceProfileDeviceSection buildDeviceSection(DeviceInfoResult info) {
DeviceProfileDeviceSection section = new DeviceProfileDeviceSection();
section.setModel(deviceProfileProperties.getModel());
section.setName(deviceProfileProperties.getName());
section.setSerialNumber(trim(info.getDeviceSerial()));
section.setDeviceVersion(formatVersion(info.getStandardVersion()));
return section;
}
private DeviceProfileHardwareSection buildHardwareSection(DeviceInfoResult info) {
DeviceProfileHardwareSection section = new DeviceProfileHardwareSection();
section.setCryptoCardName(trim(info.getDeviceName()));
section.setCpuModel(firstNonBlank(systemHardwareProbe.cpuModel(), deviceProfileProperties.getCpuModelDefault()));
section.setMemoryTotal(firstNonBlank(systemHardwareProbe.memoryTotal(), deviceProfileProperties.getMemoryTotalDefault()));
section.setDiskTotal(firstNonBlank(systemHardwareProbe.diskTotal(), deviceProfileProperties.getDiskTotalDefault()));
return section;
}
private List<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();
}
DeviceProfileResponse profile();
}
@@ -1,239 +1,8 @@
package com.cisd.tms.modules.device.service;
import com.cisd.tms.common.config.properties.CisdPresetProperties;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
import com.cisd.tms.modules.device.dto.CryptoCardRuntimeStatus;
import com.cisd.tms.modules.device.dto.DeviceRuntimeStatusResponse;
import com.cisd.tms.modules.device.dto.HardwareRuntimeStatusSection;
import com.cisd.tms.modules.device.dto.SoftwareRuntimeStatusItem;
import com.cisd.tms.modules.device.dto.UsageMetricStatus;
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository;
import com.cisd.tms.modules.device.support.HardwareStatusProbe;
import com.cisd.tms.modules.device.support.SoftwareRuntimeProbe;
import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe;
import com.cisd.tms.modules.device.support.UsageSnapshot;
import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse;
import com.cisd.tms.modules.init.service.InitService;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.BooleanSupplier;
import org.springframework.stereotype.Service;
@Service
public class DeviceRuntimeStatusService {
public interface DeviceRuntimeStatusService {
private static final String STATUS_NORMAL = "NORMAL";
private static final String STATUS_ABNORMAL = "ABNORMAL";
private static final String STATUS_NOT_INSTALLED = "NOT_INSTALLED";
private static final String STATUS_UNKNOWN = "UNKNOWN";
private final InitService initService;
private final PcieCryptoService pcieCryptoService;
private final HardwareStatusProbe hardwareStatusProbe;
private final SystemSoftwareVersionProbe systemSoftwareVersionProbe;
private final SoftwareRuntimeProbe softwareRuntimeProbe;
private final DeviceSoftwareVersionRepository deviceSoftwareVersionRepository;
private final CisdPresetProperties cisdPresetProperties;
public DeviceRuntimeStatusService(
InitService initService,
PcieCryptoService pcieCryptoService,
HardwareStatusProbe hardwareStatusProbe,
SystemSoftwareVersionProbe systemSoftwareVersionProbe,
SoftwareRuntimeProbe softwareRuntimeProbe,
DeviceSoftwareVersionRepository deviceSoftwareVersionRepository,
CisdPresetProperties cisdPresetProperties
) {
this.initService = initService;
this.pcieCryptoService = pcieCryptoService;
this.hardwareStatusProbe = hardwareStatusProbe;
this.systemSoftwareVersionProbe = systemSoftwareVersionProbe;
this.softwareRuntimeProbe = softwareRuntimeProbe;
this.deviceSoftwareVersionRepository = deviceSoftwareVersionRepository;
this.cisdPresetProperties = cisdPresetProperties;
}
public DeviceRuntimeStatusResponse loadRuntimeStatus() {
DeviceRuntimeStatusResponse response = new DeviceRuntimeStatusResponse();
response.setHardware(buildHardwareStatus());
response.setSoftware(buildSoftwareStatuses());
response.setCollectedAt(OffsetDateTime.now().toString());
return response;
}
private HardwareRuntimeStatusSection buildHardwareStatus() {
HardwareRuntimeStatusSection section = new HardwareRuntimeStatusSection();
section.setCpu(buildCpuStatus());
section.setMemory(buildUsageStatus(() -> hardwareStatusProbe.memoryUsage()));
section.setDisk(buildUsageStatus(() -> hardwareStatusProbe.diskUsage()));
section.setCryptoCard(buildCryptoCardStatus());
return section;
}
private UsageMetricStatus buildCpuStatus() {
UsageMetricStatus status = new UsageMetricStatus();
try {
Double usage = hardwareStatusProbe.cpuUsagePercent();
status.setUsagePercent(round(usage));
status.setStatus(STATUS_NORMAL);
status.setMessage("正常");
} catch (RuntimeException ex) {
status.setStatus(STATUS_UNKNOWN);
status.setMessage("未知");
}
return status;
}
private UsageMetricStatus buildUsageStatus(UsageSupplier supplier) {
UsageMetricStatus status = new UsageMetricStatus();
try {
UsageSnapshot snapshot = supplier.get();
status.setUsedGb(round(snapshot.getUsedGb()));
status.setTotalGb(round(snapshot.getTotalGb()));
status.setUsagePercent(round(snapshot.usagePercent()));
status.setStatus(STATUS_NORMAL);
status.setMessage("正常");
} catch (RuntimeException ex) {
status.setStatus(STATUS_UNKNOWN);
status.setMessage("未知");
}
return status;
}
private CryptoCardRuntimeStatus buildCryptoCardStatus() {
CryptoCardRuntimeStatus status = new CryptoCardRuntimeStatus();
try {
pcieCryptoService.getDeviceInfo();
status.setStatus(STATUS_NORMAL);
status.setMessage("就绪状态");
} catch (RuntimeException ex) {
status.setStatus(STATUS_ABNORMAL);
status.setMessage("异常");
}
return status;
}
private List<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();
}
DeviceRuntimeStatusResponse loadRuntimeStatus();
}
@@ -1,83 +1,11 @@
package com.cisd.tms.modules.device.service;
import com.cisd.tms.integration.crypto.pcie.model.DeviceInfoResult;
import com.cisd.tms.integration.crypto.pcie.model.DeviceStatusResult;
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
import com.cisd.tms.modules.device.config.DeviceProfileProperties;
import com.cisd.tms.modules.device.dto.DeviceActionResponse;
import com.cisd.tms.modules.device.dto.DeviceInfoResponse;
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository;
import com.cisd.tms.modules.init.service.InitService;
import java.time.OffsetDateTime;
import java.util.Optional;
import org.springframework.stereotype.Service;
@Service
public class DeviceService {
public interface DeviceService {
private final DeviceProfileProperties deviceProfileProperties;
private final DeviceSoftwareVersionRepository deviceSoftwareVersionRepository;
private final PcieCryptoService pcieCryptoService;
private final InitService initService;
DeviceInfoResponse info();
public DeviceService(
DeviceProfileProperties deviceProfileProperties,
DeviceSoftwareVersionRepository deviceSoftwareVersionRepository,
PcieCryptoService pcieCryptoService,
InitService initService
) {
this.deviceProfileProperties = deviceProfileProperties;
this.deviceSoftwareVersionRepository = deviceSoftwareVersionRepository;
this.pcieCryptoService = pcieCryptoService;
this.initService = initService;
}
public DeviceInfoResponse info() {
DeviceInfoResponse response = new DeviceInfoResponse();
response.setDeviceModel(trim(deviceProfileProperties.getModel()));
response.setDeviceName(trim(deviceProfileProperties.getName()));
response.setVersion(loadTmsVersion());
response.setSerialNumber(loadDeviceSerial());
response.setMasterKeyStatus(loadManagementKeyReady());
InitService.DeviceInitStateSnapshot snapshot = initService.getDeviceInitStateSnapshot();
response.setDeviceStatus(snapshot.initState());
return response;
}
public DeviceActionResponse restart(String operator) {
DeviceActionResponse response = new DeviceActionResponse();
response.setAction("RESTART");
response.setAccepted(true);
response.setOperator(operator == null || operator.isBlank() ? "unknown" : operator);
response.setAcceptedAt(OffsetDateTime.now().toString());
return response;
}
private String loadTmsVersion() {
Optional<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();
}
DeviceActionResponse restart(String operator);
}
@@ -1,112 +1,8 @@
package com.cisd.tms.modules.device.service;
import com.cisd.tms.modules.device.dto.TimeConfigRequest;
import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
@Service
public class TimeConfigService {
public void processTimeConfig(TimeConfigRequest request) {
String mode = request.getMode();
if ("MANUAL".equalsIgnoreCase(mode)) {
String timezone = request.getTimezone();
if (timezone == null || !timezone.matches("^[A-Za-z0-9/_-]+$")) {
throw new IllegalArgumentException("Invalid or empty timezone format");
}
String datetime = request.getDatetime();
// 假设格式必须为 YYYY-MM-DD HH:MM
if (datetime == null || !datetime.matches("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}$")) {
throw new IllegalArgumentException("Invalid or empty datetime format. Expected: YYYY-MM-DD HH:MM");
}
executeCommand("timedatectl set-timezone " + request.getTimezone().trim());
executeCommand("systemctl stop chronyd");
executeCommand("date -s '" + request.getDatetime() + "'");
} else if ("NTP".equalsIgnoreCase(mode)) {
List<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);
}
}
public interface TimeConfigService {
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();
}
}
@@ -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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -17,15 +18,12 @@ import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api/v1/files")
@RequiredArgsConstructor
@Tag(name = "文件管理", description = "初始化文件上传与文件元数据查询接口")
public class FileController {
private final FileService fileService;
public FileController(FileService fileService) {
this.fileService = fileService;
}
// 内部上传入口,前端先拿到 fileId,再把 fileId 传给初始化相关接口。
@PostMapping("/upload")
@Operation(summary = "上传初始化文件", description = "上传许可证、配置包等初始化依赖文件,返回 fileId 供后续任务引用。")
@@ -1,118 +1,12 @@
package com.cisd.tms.modules.file.service;
import com.cisd.tms.common.config.properties.FileStorageProperties;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.file.dto.FileDetailResponse;
import com.cisd.tms.modules.file.dto.FileUploadResponse;
import com.cisd.tms.modules.file.entity.FileRecordEntity;
import com.cisd.tms.modules.file.repository.FileRecordRepository;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.UUID;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@Service
public class FileService {
public interface FileService {
private static final DateTimeFormatter FILE_ID_TS = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
FileUploadResponse upload(MultipartFile file);
private final FileRecordRepository fileRecordRepository;
private final FileStorageProperties fileStorageProperties;
public FileService(FileRecordRepository fileRecordRepository, FileStorageProperties fileStorageProperties) {
this.fileRecordRepository = fileRecordRepository;
this.fileStorageProperties = fileStorageProperties;
}
public FileUploadResponse upload(MultipartFile file) {
if (file == null || file.isEmpty() || file.getSize() <= 0) {
throw new IllegalArgumentException("upload file is empty");
}
if (file.getSize() > fileStorageProperties.getMaxFileSizeBytes()) {
throw new IllegalArgumentException("upload file size exceeds limit");
}
String originalFilename = sanitizeFilename(file.getOriginalFilename());
String fileId = generateFileId();
Path uploadBaseDir = Paths.get(normalize(fileStorageProperties.getUploadBaseDir())).normalize().toAbsolutePath();
// 固定为 <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();
}
FileDetailResponse getDetail(String fileId);
}
@@ -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;
import com.cisd.tms.common.config.properties.CisdPresetProperties;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.init.config.InitCommandProfileService;
import com.cisd.tms.modules.init.config.InitExecutorProperties;
import com.cisd.tms.modules.init.dto.InitCreateTaskResponse;
import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse;
import com.cisd.tms.modules.init.dto.InitCreateTaskResponse;
import com.cisd.tms.modules.init.dto.InitPlanTemplateResponse;
import com.cisd.tms.modules.init.dto.InitPreviewRequest;
import com.cisd.tms.modules.init.dto.InitPreviewResponse;
@@ -16,918 +11,35 @@ import com.cisd.tms.modules.init.dto.InitTaskStepLogResponse;
import com.cisd.tms.modules.init.dto.InitTaskStepResponse;
import com.cisd.tms.modules.init.dto.ResetCreateTaskResponse;
import com.cisd.tms.modules.init.dto.ResetPreviewResponse;
import com.cisd.tms.modules.init.entity.InitTaskEntity;
import com.cisd.tms.modules.init.entity.InitTaskStepEntity;
import com.cisd.tms.modules.init.executor.InitStepExecutionResult;
import com.cisd.tms.modules.init.executor.InitStepExecutor;
import com.cisd.tms.modules.init.repository.InitTaskRepository;
import com.cisd.tms.modules.init.repository.InitTaskStepRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class InitService {
public interface InitService {
private static final String TASK_TYPE_INIT = "INIT";
private static final String TASK_TYPE_RESET = "RESET";
private static final Set<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}$");
InitPlanTemplateResponse loadTemplate(String ignoredVersion);
private final InitTaskRepository initTaskRepository;
private final InitTaskStepRepository initTaskStepRepository;
private final CisdPresetProperties cisdPresetProperties;
private final ObjectMapper objectMapper;
private final InitStepExecutor initStepExecutor;
private final InitCommandProfileService initCommandProfileService;
private final InitExecutorProperties initExecutorProperties;
private final Executor backgroundExecutor;
private final ConcurrentMap<String, CompletableFuture<Void>> activeExecutions;
InitPreviewResponse preview(InitPreviewRequest request);
@Autowired
public InitService(
InitTaskRepository initTaskRepository,
InitTaskStepRepository initTaskStepRepository,
CisdPresetProperties cisdPresetProperties,
ObjectMapper objectMapper,
InitStepExecutor initStepExecutor,
InitCommandProfileService initCommandProfileService,
InitExecutorProperties initExecutorProperties
) {
this(
initTaskRepository,
initTaskStepRepository,
cisdPresetProperties,
objectMapper,
initStepExecutor,
initCommandProfileService,
initExecutorProperties,
Executors.newCachedThreadPool(),
new ConcurrentHashMap<>()
);
}
ResetPreviewResponse previewReset();
InitService(
InitTaskRepository initTaskRepository,
InitTaskStepRepository initTaskStepRepository,
CisdPresetProperties cisdPresetProperties,
ObjectMapper objectMapper,
InitStepExecutor initStepExecutor,
InitCommandProfileService initCommandProfileService,
InitExecutorProperties initExecutorProperties,
Executor backgroundExecutor,
ConcurrentMap<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;
}
InitCreateTaskResponse createTask(InitPreviewRequest request);
/**
* 读取后端预制产品类型和版本。
* 该接口是前端动态渲染“企业/间参/直参”初始化页的入口。
*/
public InitPlanTemplateResponse loadTemplate(String ignoredVersion) {
String productType = resolvePresetProductType();
ResetCreateTaskResponse createResetTask();
InitPlanTemplateResponse response = new InitPlanTemplateResponse();
response.setPresetProductType(productType);
response.setPresetVersion(cisdPresetProperties.getVersion());
response.setSupportedVersions(List.of("ENTERPRISE", "INDIRECT", "DIRECT"));
response.setTemplatePath("classpath:/initplan/");
return response;
}
DeviceInitStateSnapshot getDeviceInitStateSnapshot();
public InitPreviewResponse preview(InitPreviewRequest request) {
if (request == null) {
throw new IllegalArgumentException("request must not be null");
}
CurrentInitConfigResponse loadCurrentInitConfig();
String productType = resolvePresetProductType();
validateCommon(request);
validateByProductType(productType, request);
InitTaskDetailResponse getTask(String taskId);
String previewTaskId = "preview-" + productType + "-" + request.getOrgCode();
boolean existed = initTaskRepository.findByTaskId(previewTaskId).isPresent();
List<InitTaskStepResponse> getTaskSteps(String taskId);
InitPreviewResponse response = new InitPreviewResponse();
response.setResolvedProductType(productType);
response.setPresetVersion(cisdPresetProperties.getVersion());
response.setStepCount(estimateStepCount(productType, request));
response.setSummary(existed ? "init preview generated from existing task fingerprint" : "init preview generated");
return response;
}
InitTaskStepLogResponse getTaskStepLog(String taskId, int stepNo);
public ResetPreviewResponse previewReset() {
ResolvedResetContext context = resolveCurrentResetContext();
String productType = context.productType();
InitTaskExecuteResponse executeTask(String taskId);
ResetPreviewResponse response = new ResetPreviewResponse();
response.setResolvedProductType(productType);
response.setPresetVersion(cisdPresetProperties.getVersion());
response.setStepCount(resetSteps(productType, context.mqType()).size());
response.setSummary("reset preview generated from current initialized state");
return response;
}
InitTaskExecuteResponse executeResetTask(String taskId);
public InitCreateTaskResponse createTask(InitPreviewRequest request) {
if (request == null) {
throw new IllegalArgumentException("request must not be null");
}
String productType = resolvePresetProductType();
//todo 验证是否已经初始化
validateCommon(request);
validateByProductType(productType, request);
String taskId = buildTaskId(TASK_TYPE_INIT);
String normalizedMqType = normalizeUpper(request.getMq().getMqType());
String deployMode = normalizeUpper(request.getDeployMode());
LocalDateTime now = LocalDateTime.now();
// 1) 按产品类型 + MQ 类型生成标准步骤序列
List<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(
record DeviceInitStateSnapshot(
String initState,
String currentTaskType,
String currentTaskStatus,
@@ -935,13 +47,4 @@ public class InitService {
String initializedMqType
) {
}
private record ResolvedResetContext(
String productType,
String orgCode,
String mqType,
String channelUsername,
String sourceTaskId
) {
}
}
@@ -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.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@@ -19,15 +20,12 @@ import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
@Tag(name = "主密钥管理", description = "LMK 生成、恢复、状态查询和销毁接口")
public class LmkController {
private final LmkService lmkService;
public LmkController(LmkService lmkService, com.fasterxml.jackson.databind.ObjectMapper objectMapper) {
this.lmkService = lmkService;
}
@PostMapping("/masterKey/init")
@Operation(summary = "初始化主密钥", description = "执行密码卡初始化识别、主密钥生成加载、内部密钥生成,并返回主密钥状态与校验值。")
public ApiResponse<MasterKeyStateResult> initMasterKey() {
@@ -1,400 +1,35 @@
package com.cisd.tms.modules.mk.service;
import com.cisd.tms.common.util.DivisionUtils;
import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds;
import com.cisd.tms.integration.crypto.pcie.jna.EccRefPublicKey;
import com.cisd.tms.integration.crypto.pcie.jna.EccSignature;
import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult;
import com.cisd.tms.integration.crypto.pcie.model.DigestRequest;
import com.cisd.tms.integration.crypto.pcie.model.EccInternalSignRequest;
import com.cisd.tms.integration.crypto.pcie.model.EccInternalVerifyRequest;
import com.cisd.tms.integration.crypto.pcie.model.RecoverUserKeyRequest;
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
import com.cisd.tms.modules.mk.config.MasterKeyInitProperties;
import com.cisd.tms.modules.mk.common.LMKConstant;
import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket;
import com.cisd.tms.modules.mk.dto.MasterKeyBackupMaterial;
import com.cisd.tms.modules.mk.dto.MasterKeyPacketIkPart;
import com.cisd.tms.modules.mk.dto.MasterKeyPacketLmkPart;
import com.cisd.tms.modules.mk.dto.MasterKeyPacketUserKeyPart;
import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket;
import com.cisd.tms.modules.mk.dto.MasterKeyStateResult;
import com.cisd.tms.modules.mk.enums.IKEnums;
import com.cisd.tms.modules.mk.dto.RecoveryResult;
import com.cisd.tms.modules.mk.enums.MasterKeyStatus;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.util.encoders.Hex;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author liulu
* @since 2026/3/5
*/
@Slf4j
@Service
public class LmkService {
public interface LmkService {
private static final int AUTH_SIGN_KEY_INDEX = IKEnums.KEY_TYPE_AUTH.getCode();
private static final int LMK_MAC_BYTE_LENGTH = 8;
private static final int IK_COMPONENT_MAC_BYTE_LENGTH = 8;
private static final int IK_COMPONENT_DATA_LENGTH = 3 * 64;
private static final int USER_KEY_INDEX = 1;
private static final int USER_KEY_TYPE_SIGN = 0;
private static final int USER_KEY_TYPE_ENC = 1;
private static final int USER_KEY_BACKUP_BUFFER_SIZE = 120;
private final PcieCryptoService pcieCryptoService;
private final MasterKeyInitProperties masterKeyInitProperties;
MasterKeyStateResult initMasterKey();
public LmkService(PcieCryptoService pcieCryptoService, MasterKeyInitProperties masterKeyInitProperties) {
this.pcieCryptoService = pcieCryptoService;
this.masterKeyInitProperties = masterKeyInitProperties;
}
void destroyMasterKey();
MasterKeyStateResult queryMasterKeyStatus();
public MasterKeyStateResult initMasterKey() {
byte[] oldPin = decodeConfiguredPin("tms.mk.init-identify.old-pin-base64", masterKeyInitProperties.getOldPinBase64());
byte[] newPin = decodeConfiguredPin("tms.mk.init-identify.new-pin-base64", masterKeyInitProperties.getNewPinBase64());
String exportIkPublicKeyHex();
MasterKeyStateResult result = new MasterKeyStateResult();
result.setStatus(true);
result.setSeedMac(Hex.toHexString(pcieCryptoService.initializeMasterKey(oldPin, newPin)));
return result;
}
String signIk(String rawData);
public void destroyMasterKey() {
pcieCryptoService.destroyLmk();
}
void verifyIk(String rawData, String signatureHex);
public MasterKeyStateResult queryMasterKeyStatus() {
MasterKeyStateResult result = new MasterKeyStateResult();
boolean status = pcieCryptoService.checkLmk();
result.setStatus(status);
if (status) {
result.setSeedMac(Hex.toHexString(pcieCryptoService.exportLmkSeedMac()));
}
return result;
}
MasterKeyBackupPacket buildBackupPacket(int componentIndex);
public String exportIkPublicKeyHex() {
int outBufferSize = new EccRefPublicKey().size();
BackupDataResult result = pcieCryptoService.exportSignPublicKeyEcc(AUTH_SIGN_KEY_INDEX, outBufferSize);
return Hex.toHexString(result.getData());
}
MasterKeyBackupMaterial getBackupMaterial(int uid);
public String signIk(String rawData) {
byte[] digest = digestForInternalSign(rawData);
EccInternalSignRequest request = new EccInternalSignRequest();
request.setKeyIndex(AUTH_SIGN_KEY_INDEX);
request.setData(digest);
request.setSignatureBufferSize(new EccSignature().size());
BackupDataResult result = pcieCryptoService.eccInternalSign(request);
return Hex.toHexString(result.getData());
}
String getComponent(int uid);
public void verifyIk(String rawData, String signatureHex) {
byte[] digest = digestForInternalSign(rawData);
EccInternalVerifyRequest request = new EccInternalVerifyRequest();
request.setKeyIndex(AUTH_SIGN_KEY_INDEX);
request.setData(digest);
request.setSignature(Hex.decode(signatureHex));
pcieCryptoService.eccInternalVerify(request);
}
void recoverKeyPackets(List<MasterKeyBackupPacket> packets);
public MasterKeyBackupPacket buildBackupPacket(int componentIndex) {
int index = requireComponentIndex(componentIndex);
ParsedMasterKeyBackup lmkBackup = parseBackupMaterial();
ParsedIkBackup authIkBackup = parseIkBackupMaterial(IKEnums.KEY_TYPE_AUTH.getCode());
ParsedIkBackup deviceIkBackup = parseIkBackupMaterial(IKEnums.KEY_TYPE_DEVICE.getCode());
MasterKeyBackupPacket packet = new MasterKeyBackupPacket();
packet.setComponentIndex(index);
packet.setLmk(new MasterKeyPacketLmkPart(
lmkBackup.lmkMacHex(),
requiredComponent(lmkBackup.componentMap(), index, "lmk")
));
packet.setIkAuth(new MasterKeyPacketIkPart(
IKEnums.KEY_TYPE_AUTH.getCode(),
authIkBackup.macHex(),
requiredComponent(authIkBackup.componentMap(), index, "ikAuth")
));
packet.setIkDevice(new MasterKeyPacketIkPart(
IKEnums.KEY_TYPE_DEVICE.getCode(),
deviceIkBackup.macHex(),
requiredComponent(deviceIkBackup.componentMap(), index, "ikDevice")
));
packet.setUserKeys(List.of(
buildUserKeyPacket(USER_KEY_INDEX, USER_KEY_TYPE_SIGN, index),
buildUserKeyPacket(USER_KEY_INDEX, USER_KEY_TYPE_ENC, index)
));
return packet;
}
public MasterKeyBackupMaterial getBackupMaterial(int uid) {
MasterKeyBackupPacket packet = buildBackupPacket(uid);
return new MasterKeyBackupMaterial(packet.getLmk().getComponent(), packet.getLmk().getLmkMac());
}
public String getComponent(int uid) {
return getBackupMaterial(uid).getComponent();
}
public void recoverKeyPackets(List<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) {
}
void recoverLmkEx(String lmkMacHex, List<String> components);
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) {
}
}
+1 -1
View File
@@ -204,7 +204,7 @@ tms:
# 密码卡模式:MOCK(模拟)/JNA(动态库调用)等。
mode: ${TMS_CRYPTO_CARD_MODE:JNA}
# 动态库目录(.so 所在目录)。
vendor-lib-path: ${TMS_CRYPTO_VENDOR_LIB_PATH:/home/tms/libs/libsdf_ukey_syd1408-x64.so.1.0.0}
vendor-lib-path: ${TMS_CRYPTO_VENDOR_LIB_PATH:/home/tms/libs/libsdf_syd1408-x64.so.1.0.0}
# 动态库名(不含前缀/后缀或按加载器约定)。
vendor-lib-name: ${TMS_CRYPTO_VENDOR_LIB_NAME:sdf_ukey_syd1408}
# 是否严格校验算法标识(与 0018/厂商补充定义一致性校验)。
@@ -8,6 +8,7 @@ import com.cisd.tms.modules.device.dto.DeviceProfileResponse;
import com.cisd.tms.modules.device.dto.DeviceProfileSoftwareItem;
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository;
import com.cisd.tms.modules.device.service.impl.DeviceProfileServiceImpl;
import com.cisd.tms.modules.device.support.SystemHardwareProbe;
import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe;
import com.cisd.tms.modules.init.config.InitCommandProfileService;
@@ -21,6 +22,7 @@ import com.cisd.tms.modules.init.executor.InitStepExecutor;
import com.cisd.tms.modules.init.repository.InitTaskRepository;
import com.cisd.tms.modules.init.repository.InitTaskStepRepository;
import com.cisd.tms.modules.init.service.InitService;
import com.cisd.tms.modules.init.service.impl.InitServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -47,7 +49,7 @@ class DeviceProfileServiceTest {
versionRepository.save(component("NGINX", "Nginx", "Nginx V1.20"));
versionRepository.save(component("APP", "标准收发器间参版", "标准收发器间参版 V1.3.2"));
DeviceProfileService service = new DeviceProfileService(
DeviceProfileService service = new DeviceProfileServiceImpl(
initService,
cryptoService(),
profileProperties(),
@@ -78,7 +80,7 @@ class DeviceProfileServiceTest {
@Test
void shouldReturnOnlyBaseSoftwareWhenNoSuccessfulInitSnapshotExists() {
DeviceProfileService service = new DeviceProfileService(
DeviceProfileService service = new DeviceProfileServiceImpl(
newInitService("ENTERPRISE"),
cryptoService(),
profileProperties(),
@@ -111,7 +113,7 @@ class DeviceProfileServiceTest {
cached.setCurrentVersion("Nginx V1.20");
versionRepository.saveOrUpdate(cached);
DeviceProfileService service = new DeviceProfileService(
DeviceProfileService service = new DeviceProfileServiceImpl(
initService,
cryptoService(),
profileProperties(),
@@ -138,7 +140,7 @@ class DeviceProfileServiceTest {
InitService initService = newInitService("ENTERPRISE");
createSuccessfulInitTask(initService, validInitRequest("RABBITMQ"));
DeviceProfileService service = new DeviceProfileService(
DeviceProfileService service = new DeviceProfileServiceImpl(
initService,
cryptoService(),
profileProperties(),
@@ -171,7 +173,7 @@ class DeviceProfileServiceTest {
properties.setMemoryTotalDefault("16GB");
properties.setDiskTotalDefault("256G");
DeviceProfileService service = new DeviceProfileService(
DeviceProfileService service = new DeviceProfileServiceImpl(
newInitService("ENTERPRISE"),
cryptoService(),
properties,
@@ -308,7 +310,7 @@ class DeviceProfileServiceTest {
properties.setLogDir("/tmp/tms-init-logs");
ObjectMapper objectMapper = new ObjectMapper();
InitCommandProfileService commandProfileService = new InitCommandProfileService(objectMapper, properties);
return new InitService(
return new InitServiceImpl(
new InMemoryInitTaskRepository(),
new InMemoryInitTaskStepRepository(),
preset(productType),
@@ -7,6 +7,7 @@ import com.cisd.tms.modules.device.dto.DeviceRuntimeStatusResponse;
import com.cisd.tms.modules.device.dto.SoftwareRuntimeStatusItem;
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository;
import com.cisd.tms.modules.device.service.impl.DeviceRuntimeStatusServiceImpl;
import com.cisd.tms.modules.device.support.HardwareStatusProbe;
import com.cisd.tms.modules.device.support.SoftwareRuntimeProbe;
import com.cisd.tms.modules.device.support.UsageSnapshot;
@@ -22,6 +23,7 @@ import com.cisd.tms.modules.init.executor.InitStepExecutor;
import com.cisd.tms.modules.init.repository.InitTaskRepository;
import com.cisd.tms.modules.init.repository.InitTaskStepRepository;
import com.cisd.tms.modules.init.service.InitService;
import com.cisd.tms.modules.init.service.impl.InitServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -48,7 +50,7 @@ class DeviceRuntimeStatusServiceTest {
versions.save(component("NGINX", "Nginx", "Nginx V1.20"));
versions.save(component("APP", "标准收发器企业版", "标准收发器企业版 V1.3.2"));
DeviceRuntimeStatusService service = new DeviceRuntimeStatusService(
DeviceRuntimeStatusService service = new DeviceRuntimeStatusServiceImpl(
initService,
cryptoService(),
hardwareProbe(23.22, 23.55, 32.0, 500.32, 1024.0),
@@ -82,7 +84,7 @@ class DeviceRuntimeStatusServiceTest {
@Test
void shouldMarkNonInitializedSoftwareAsNotInstalled() {
DeviceRuntimeStatusService service = new DeviceRuntimeStatusService(
DeviceRuntimeStatusService service = new DeviceRuntimeStatusServiceImpl(
newInitService("ENTERPRISE"),
cryptoService(),
hardwareProbe(10.0, 8.0, 16.0, 100.0, 256.0),
@@ -107,7 +109,7 @@ class DeviceRuntimeStatusServiceTest {
InitService initService = newInitService("INDIRECT");
createSuccessfulInitTask(initService, validInitRequest("RABBITMQ_TLQ"));
DeviceRuntimeStatusService service = new DeviceRuntimeStatusService(
DeviceRuntimeStatusService service = new DeviceRuntimeStatusServiceImpl(
initService,
failingCryptoService(),
failingHardwareProbe(),
@@ -334,7 +336,7 @@ class DeviceRuntimeStatusServiceTest {
properties.setLogDir("/tmp/tms-init-logs");
ObjectMapper objectMapper = new ObjectMapper();
InitCommandProfileService commandProfileService = new InitCommandProfileService(objectMapper, properties);
return new InitService(
return new InitServiceImpl(
new InMemoryInitTaskRepository(),
new InMemoryInitTaskStepRepository(),
preset(productType),
@@ -8,6 +8,7 @@ import com.cisd.tms.modules.device.config.DeviceProfileProperties;
import com.cisd.tms.modules.device.dto.DeviceInfoResponse;
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository;
import com.cisd.tms.modules.device.service.impl.DeviceServiceImpl;
import com.cisd.tms.modules.init.config.InitCommandProfileService;
import com.cisd.tms.modules.init.config.InitExecutorProperties;
import com.cisd.tms.modules.init.dto.InitCreateTaskResponse;
@@ -19,6 +20,7 @@ import com.cisd.tms.modules.init.executor.InitStepExecutor;
import com.cisd.tms.modules.init.repository.InitTaskRepository;
import com.cisd.tms.modules.init.repository.InitTaskStepRepository;
import com.cisd.tms.modules.init.service.InitService;
import com.cisd.tms.modules.init.service.impl.InitServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -52,7 +54,7 @@ class DeviceServiceTest {
InMemoryInitTaskRepository taskRepository = new InMemoryInitTaskRepository();
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
InitService initService = newInitService(taskRepository, stepRepository, "ENTERPRISE");
DeviceService deviceService = new DeviceService(
DeviceService deviceService = new DeviceServiceImpl(
deviceProfile("MODEL-X", "设备A"),
fixedVersionRepository("TMS-V3.1.4"),
cryptoService("SERIAL-ABC", 0),
@@ -77,7 +79,7 @@ class DeviceServiceTest {
InMemoryInitTaskRepository taskRepository = new InMemoryInitTaskRepository();
InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository();
InitService initService = newInitService(taskRepository, stepRepository, productType);
return new DeviceService(
return new DeviceServiceImpl(
deviceProfile("SYD7108", "跨境支付终端一体机"),
fixedVersionRepository(tmsVersion),
cryptoService(serialNumber, fsmState),
@@ -95,7 +97,7 @@ class DeviceServiceTest {
properties.setLogDir("/tmp/tms-init-logs");
ObjectMapper objectMapper = new ObjectMapper();
InitCommandProfileService commandProfileService = new InitCommandProfileService(objectMapper, properties);
return new InitService(
return new InitServiceImpl(
taskRepository,
stepRepository,
preset(productType),
@@ -6,7 +6,11 @@ import com.cisd.tms.modules.file.dto.FileUploadResponse;
import com.cisd.tms.modules.file.entity.FileRecordEntity;
import com.cisd.tms.modules.file.repository.FileRecordRepository;
import com.cisd.tms.modules.file.service.FileService;
import com.cisd.tms.modules.file.service.impl.FileServiceImpl;
import java.io.IOException;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
@@ -18,6 +22,8 @@ import org.springframework.mock.web.MockMultipartFile;
class FileServiceTest {
private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-03-31T06:00:00Z"), ZoneOffset.UTC);
@Test
void shouldPersistUploadedFileMetadata() {
InMemoryFileRecordRepository repository = new InMemoryFileRecordRepository();
@@ -44,7 +50,7 @@ class FileServiceTest {
void shouldSaveUploadedFileUnderFileIdDirectory() throws IOException {
Path uploadBaseDir = Files.createTempDirectory("file-service-upload-test");
InMemoryFileRecordRepository repository = new InMemoryFileRecordRepository();
FileService service = new FileService(repository, properties(uploadBaseDir, 1024));
FileService service = new FileServiceImpl(repository, properties(uploadBaseDir, 1024), FIXED_CLOCK);
MockMultipartFile file = new MockMultipartFile(
"file",
"receiver-license.dat",
@@ -66,7 +72,7 @@ class FileServiceTest {
@Test
void shouldRejectBlankFileUpload() throws IOException {
Path uploadBaseDir = Files.createTempDirectory("file-service-upload-empty-test");
FileService service = new FileService(new InMemoryFileRecordRepository(), properties(uploadBaseDir, 1024));
FileService service = new FileServiceImpl(new InMemoryFileRecordRepository(), properties(uploadBaseDir, 1024), FIXED_CLOCK);
MockMultipartFile file = new MockMultipartFile("file", "empty.txt", "text/plain", new byte[0]);
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> service.upload(file));
@@ -77,7 +83,7 @@ class FileServiceTest {
@Test
void shouldRejectOversizeUpload() throws IOException {
Path uploadBaseDir = Files.createTempDirectory("file-service-upload-oversize-test");
FileService service = new FileService(new InMemoryFileRecordRepository(), properties(uploadBaseDir, 4));
FileService service = new FileServiceImpl(new InMemoryFileRecordRepository(), properties(uploadBaseDir, 4), FIXED_CLOCK);
MockMultipartFile file = new MockMultipartFile("file", "big.txt", "text/plain", "12345".getBytes());
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> service.upload(file));
@@ -88,7 +94,7 @@ class FileServiceTest {
@Test
void shouldReturnStoredFileDetailByFileId() throws IOException {
Path uploadBaseDir = Files.createTempDirectory("file-service-detail-test");
FileService service = new FileService(new InMemoryFileRecordRepository(), properties(uploadBaseDir, 1024));
FileService service = new FileServiceImpl(new InMemoryFileRecordRepository(), properties(uploadBaseDir, 1024), FIXED_CLOCK);
MockMultipartFile file = new MockMultipartFile("file", "front.zip", "application/zip", "zip-data".getBytes());
FileUploadResponse uploaded = service.upload(file);
@@ -20,6 +20,7 @@ import com.cisd.tms.modules.init.executor.InitStepExecutionResult;
import com.cisd.tms.modules.init.executor.InitStepExecutor;
import com.cisd.tms.modules.init.repository.InitTaskRepository;
import com.cisd.tms.modules.init.repository.InitTaskStepRepository;
import com.cisd.tms.modules.init.service.impl.InitServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.file.Files;
@@ -643,7 +644,7 @@ class InitServiceTest {
properties.setMode(InitExecutorProperties.Mode.SIMULATE);
ObjectMapper objectMapper = new ObjectMapper();
InitCommandProfileService commandProfileService = new InitCommandProfileService(objectMapper, properties);
return new InitService(taskRepository, stepRepository, preset, objectMapper, executor, commandProfileService, properties);
return new InitServiceImpl(taskRepository, stepRepository, preset, objectMapper, executor, commandProfileService, properties);
}
private static InitTaskRepository emptyRepository() {
@@ -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.MasterKeyPacketUserKeyPart;
import com.cisd.tms.modules.mk.dto.MasterKeyStateResult;
import com.cisd.tms.modules.mk.service.impl.LmkServiceImpl;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.junit.jupiter.api.Assertions;
@@ -29,7 +30,7 @@ class LmkServiceTest {
MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE=");
Mockito.when(pcieCryptoService.initializeMasterKey(Mockito.any(), Mockito.any())).thenReturn(new byte[] {0x01, 0x23, 0x45, 0x67});
LmkService service = new LmkService(pcieCryptoService, properties);
LmkService service = new LmkServiceImpl(pcieCryptoService, properties);
MasterKeyStateResult result = service.initMasterKey();
@@ -49,7 +50,7 @@ class LmkServiceTest {
Mockito.when(pcieCryptoService.initializeMasterKey(Mockito.any(), Mockito.any()))
.thenThrow(new IllegalStateException("master key initialization failed"));
LmkService service = new LmkService(pcieCryptoService, properties);
LmkService service = new LmkServiceImpl(pcieCryptoService, properties);
IllegalStateException exception = Assertions.assertThrows(IllegalStateException.class, service::initMasterKey);
@@ -62,7 +63,7 @@ class LmkServiceTest {
MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE=");
Mockito.when(pcieCryptoService.checkLmk()).thenReturn(false);
LmkService service = new LmkService(pcieCryptoService, properties);
LmkService service = new LmkServiceImpl(pcieCryptoService, properties);
MasterKeyStateResult result = service.queryMasterKeyStatus();
@@ -78,7 +79,7 @@ class LmkServiceTest {
Mockito.when(pcieCryptoService.checkLmk()).thenReturn(true);
Mockito.when(pcieCryptoService.exportLmkSeedMac()).thenReturn(new byte[] {(byte) 0xAA, (byte) 0xBB});
LmkService service = new LmkService(pcieCryptoService, properties);
LmkService service = new LmkServiceImpl(pcieCryptoService, properties);
MasterKeyStateResult result = service.queryMasterKeyStatus();
@@ -116,7 +117,7 @@ class LmkServiceTest {
Mockito.when(pcieCryptoService.backupUserKey(1, 0, 120)).thenReturn(userSignBackup);
Mockito.when(pcieCryptoService.backupUserKey(1, 1, 120)).thenReturn(userEncBackup);
LmkService service = new LmkService(pcieCryptoService, properties);
LmkService service = new LmkServiceImpl(pcieCryptoService, properties);
MasterKeyBackupPacket packet = service.buildBackupPacket(2);
@@ -142,7 +143,7 @@ class LmkServiceTest {
void shouldRecoverAllKeyPacketsInOrder() {
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE=");
LmkService service = new LmkService(pcieCryptoService, properties);
LmkService service = new LmkServiceImpl(pcieCryptoService, properties);
MasterKeyBackupPacket packet1 = new MasterKeyBackupPacket();
packet1.setComponentIndex(1);
@@ -220,7 +221,7 @@ class LmkServiceTest {
Mockito.when(pcieCryptoService.digest(Mockito.any())).thenReturn(digestResult);
Mockito.when(pcieCryptoService.eccInternalSign(Mockito.any())).thenReturn(signResult);
LmkService service = new LmkService(pcieCryptoService, properties);
LmkService service = new LmkServiceImpl(pcieCryptoService, properties);
String signatureHex = service.signIk("1223");
@@ -249,7 +250,7 @@ class LmkServiceTest {
});
Mockito.when(pcieCryptoService.digest(Mockito.any())).thenReturn(digestResult);
LmkService service = new LmkService(pcieCryptoService, properties);
LmkService service = new LmkServiceImpl(pcieCryptoService, properties);
service.verifyIk("1223", "aabb");