service改造
This commit is contained in:
@@ -12,6 +12,8 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import 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();
|
||||
}
|
||||
}
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
package com.cisd.tms.modules.device.service.impl;
|
||||
|
||||
import com.cisd.tms.common.config.properties.CisdPresetProperties;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.device.dto.CryptoCardRuntimeStatus;
|
||||
import com.cisd.tms.modules.device.dto.DeviceRuntimeStatusResponse;
|
||||
import com.cisd.tms.modules.device.dto.HardwareRuntimeStatusSection;
|
||||
import com.cisd.tms.modules.device.dto.SoftwareRuntimeStatusItem;
|
||||
import com.cisd.tms.modules.device.dto.UsageMetricStatus;
|
||||
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
|
||||
import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository;
|
||||
import com.cisd.tms.modules.device.service.DeviceRuntimeStatusService;
|
||||
import com.cisd.tms.modules.device.support.HardwareStatusProbe;
|
||||
import com.cisd.tms.modules.device.support.SoftwareRuntimeProbe;
|
||||
import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe;
|
||||
import com.cisd.tms.modules.device.support.UsageSnapshot;
|
||||
import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse;
|
||||
import com.cisd.tms.modules.init.service.InitService;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DeviceRuntimeStatusServiceImpl implements DeviceRuntimeStatusService {
|
||||
|
||||
private static final String STATUS_NORMAL = "NORMAL";
|
||||
private static final String STATUS_ABNORMAL = "ABNORMAL";
|
||||
private static final String STATUS_NOT_INSTALLED = "NOT_INSTALLED";
|
||||
private static final String STATUS_UNKNOWN = "UNKNOWN";
|
||||
|
||||
private final InitService initService;
|
||||
private final PcieCryptoService pcieCryptoService;
|
||||
private final HardwareStatusProbe hardwareStatusProbe;
|
||||
private final SystemSoftwareVersionProbe systemSoftwareVersionProbe;
|
||||
private final SoftwareRuntimeProbe softwareRuntimeProbe;
|
||||
private final DeviceSoftwareVersionRepository deviceSoftwareVersionRepository;
|
||||
private final CisdPresetProperties cisdPresetProperties;
|
||||
|
||||
@Override
|
||||
public DeviceRuntimeStatusResponse loadRuntimeStatus() {
|
||||
DeviceRuntimeStatusResponse response = new DeviceRuntimeStatusResponse();
|
||||
response.setHardware(buildHardwareStatus());
|
||||
response.setSoftware(buildSoftwareStatuses());
|
||||
response.setCollectedAt(OffsetDateTime.now().toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
private HardwareRuntimeStatusSection buildHardwareStatus() {
|
||||
HardwareRuntimeStatusSection section = new HardwareRuntimeStatusSection();
|
||||
section.setCpu(buildCpuStatus());
|
||||
section.setMemory(buildUsageStatus(() -> hardwareStatusProbe.memoryUsage()));
|
||||
section.setDisk(buildUsageStatus(() -> hardwareStatusProbe.diskUsage()));
|
||||
section.setCryptoCard(buildCryptoCardStatus());
|
||||
return section;
|
||||
}
|
||||
|
||||
private UsageMetricStatus buildCpuStatus() {
|
||||
UsageMetricStatus status = new UsageMetricStatus();
|
||||
try {
|
||||
Double usage = hardwareStatusProbe.cpuUsagePercent();
|
||||
status.setUsagePercent(round(usage));
|
||||
status.setStatus(STATUS_NORMAL);
|
||||
status.setMessage("正常");
|
||||
} catch (RuntimeException ex) {
|
||||
status.setStatus(STATUS_UNKNOWN);
|
||||
status.setMessage("未知");
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private UsageMetricStatus buildUsageStatus(UsageSupplier supplier) {
|
||||
UsageMetricStatus status = new UsageMetricStatus();
|
||||
try {
|
||||
UsageSnapshot snapshot = supplier.get();
|
||||
status.setUsedGb(round(snapshot.getUsedGb()));
|
||||
status.setTotalGb(round(snapshot.getTotalGb()));
|
||||
status.setUsagePercent(round(snapshot.usagePercent()));
|
||||
status.setStatus(STATUS_NORMAL);
|
||||
status.setMessage("正常");
|
||||
} catch (RuntimeException ex) {
|
||||
status.setStatus(STATUS_UNKNOWN);
|
||||
status.setMessage("未知");
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private CryptoCardRuntimeStatus buildCryptoCardStatus() {
|
||||
CryptoCardRuntimeStatus status = new CryptoCardRuntimeStatus();
|
||||
try {
|
||||
pcieCryptoService.getDeviceInfo();
|
||||
status.setStatus(STATUS_NORMAL);
|
||||
status.setMessage("就绪状态");
|
||||
} catch (RuntimeException ex) {
|
||||
status.setStatus(STATUS_ABNORMAL);
|
||||
status.setMessage("异常");
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private List<SoftwareRuntimeStatusItem> buildSoftwareStatuses() {
|
||||
CurrentInitConfigResponse config = loadCurrentInitConfigIfPresent();
|
||||
boolean initialized = config != null;
|
||||
String productType = config == null ? normalize(cisdPresetProperties.getProductType()) : normalize(config.getProductType());
|
||||
String mqType = config == null ? "" : normalize(config.getMqType());
|
||||
|
||||
List<SoftwareRuntimeStatusItem> items = new ArrayList<>();
|
||||
items.add(buildSoftwareItem("RABBITMQ", "消息队列", cached("RABBITMQ"), initialized && isRabbitmqEnabled(mqType), softwareRuntimeProbe::isRabbitmqRunning));
|
||||
items.add(buildSoftwareItem("TLQ", "消息队列", cached("TLQ"), initialized && isTlqEnabled(mqType), softwareRuntimeProbe::isTlqRunning));
|
||||
items.add(buildSoftwareItem("TIDB", "数据库", cached("TIDB"), initialized && isTidbEnabled(productType), softwareRuntimeProbe::isTidbRunning));
|
||||
items.add(buildSoftwareItem("NGINX", "Nginx", cached("NGINX"), initialized && isNginxEnabled(productType), softwareRuntimeProbe::isNginxRunning));
|
||||
items.add(buildAppItem(productType, initialized));
|
||||
items.add(buildSoftwareItem("TMS", "设备管理软件", firstNonBlank(cached("TMS"), systemSoftwareVersionProbe.tmsVersion(), cisdPresetProperties.getVersion()), true, softwareRuntimeProbe::isTmsRunning));
|
||||
return items;
|
||||
}
|
||||
|
||||
private SoftwareRuntimeStatusItem buildAppItem(String productType, boolean initialized) {
|
||||
boolean standard = "ENTERPRISE".equals(productType) || "INDIRECT".equals(productType);
|
||||
BooleanSupplier supplier = standard ? softwareRuntimeProbe::isStandardAppRunning : softwareRuntimeProbe::isDirectAppRunning;
|
||||
String name = "应用软件";
|
||||
String displayName = firstNonBlank(cached("APP"), appName(productType));
|
||||
return buildSoftwareItem("APP", name, displayName, initialized, supplier);
|
||||
}
|
||||
|
||||
private SoftwareRuntimeStatusItem buildSoftwareItem(
|
||||
String code,
|
||||
String name,
|
||||
String displayName,
|
||||
boolean enabled,
|
||||
BooleanSupplier probe
|
||||
) {
|
||||
SoftwareRuntimeStatusItem item = new SoftwareRuntimeStatusItem();
|
||||
item.setComponentCode(code);
|
||||
item.setComponentName(name);
|
||||
item.setDisplayName(firstNonBlank(displayName, name));
|
||||
if (!enabled) {
|
||||
item.setStatus(STATUS_NOT_INSTALLED);
|
||||
item.setMessage("未安装");
|
||||
return item;
|
||||
}
|
||||
try {
|
||||
if (probe.getAsBoolean()) {
|
||||
item.setStatus(STATUS_NORMAL);
|
||||
item.setMessage("正常");
|
||||
} else {
|
||||
item.setStatus(STATUS_ABNORMAL);
|
||||
item.setMessage("异常");
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
item.setStatus(STATUS_UNKNOWN);
|
||||
item.setMessage("未知");
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private CurrentInitConfigResponse loadCurrentInitConfigIfPresent() {
|
||||
try {
|
||||
if (!"INITIALIZED".equals(initService.getDeviceInitStateSnapshot().initState())) {
|
||||
return null;
|
||||
}
|
||||
return initService.loadCurrentInitConfig();
|
||||
} catch (BizException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRabbitmqEnabled(String mqType) {
|
||||
return "RABBITMQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType) || "RABBITMQ_CFMQ".equals(mqType);
|
||||
}
|
||||
|
||||
private boolean isTlqEnabled(String mqType) {
|
||||
return "TLQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType);
|
||||
}
|
||||
|
||||
private boolean isTidbEnabled(String productType) {
|
||||
return "ENTERPRISE".equals(productType) || "INDIRECT".equals(productType);
|
||||
}
|
||||
|
||||
private boolean isNginxEnabled(String productType) {
|
||||
return "ENTERPRISE".equals(productType) || "INDIRECT".equals(productType);
|
||||
}
|
||||
|
||||
private String cached(String componentCode) {
|
||||
Optional<DeviceSoftwareVersionEntity> entity = deviceSoftwareVersionRepository.findByComponentCode(componentCode);
|
||||
return entity.map(DeviceSoftwareVersionEntity::getCurrentVersion).orElse("");
|
||||
}
|
||||
|
||||
private String appName(String productType) {
|
||||
return switch (productType) {
|
||||
case "ENTERPRISE" -> "标准收发器企业版";
|
||||
case "INDIRECT" -> "标准收发器间参版";
|
||||
case "DIRECT" -> "直参轻量化终端版";
|
||||
default -> "应用软件";
|
||||
};
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? "" : value.trim().toUpperCase();
|
||||
}
|
||||
|
||||
private double round(double value) {
|
||||
return Math.round(value * 100D) / 100D;
|
||||
}
|
||||
|
||||
private String firstNonBlank(String... values) {
|
||||
if (values == null) {
|
||||
return "";
|
||||
}
|
||||
for (String value : values) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface UsageSupplier {
|
||||
UsageSnapshot get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.cisd.tms.modules.device.service.impl;
|
||||
|
||||
import com.cisd.tms.integration.crypto.pcie.model.DeviceInfoResult;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.DeviceStatusResult;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.device.config.DeviceProfileProperties;
|
||||
import com.cisd.tms.modules.device.dto.DeviceActionResponse;
|
||||
import com.cisd.tms.modules.device.dto.DeviceInfoResponse;
|
||||
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
|
||||
import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository;
|
||||
import com.cisd.tms.modules.device.service.DeviceService;
|
||||
import com.cisd.tms.modules.init.service.InitService;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DeviceServiceImpl implements DeviceService {
|
||||
|
||||
private final DeviceProfileProperties deviceProfileProperties;
|
||||
private final DeviceSoftwareVersionRepository deviceSoftwareVersionRepository;
|
||||
private final PcieCryptoService pcieCryptoService;
|
||||
private final InitService initService;
|
||||
|
||||
@Override
|
||||
public DeviceInfoResponse info() {
|
||||
DeviceInfoResponse response = new DeviceInfoResponse();
|
||||
response.setDeviceModel(trim(deviceProfileProperties.getModel()));
|
||||
response.setDeviceName(trim(deviceProfileProperties.getName()));
|
||||
response.setVersion(loadTmsVersion());
|
||||
response.setSerialNumber(loadDeviceSerial());
|
||||
response.setMasterKeyStatus(loadManagementKeyReady());
|
||||
InitService.DeviceInitStateSnapshot snapshot = initService.getDeviceInitStateSnapshot();
|
||||
response.setDeviceStatus(snapshot.initState());
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceActionResponse restart(String operator) {
|
||||
DeviceActionResponse response = new DeviceActionResponse();
|
||||
response.setAction("RESTART");
|
||||
response.setAccepted(true);
|
||||
response.setOperator(operator == null || operator.isBlank() ? "unknown" : operator);
|
||||
response.setAcceptedAt(OffsetDateTime.now().toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
private String loadTmsVersion() {
|
||||
Optional<DeviceSoftwareVersionEntity> version = deviceSoftwareVersionRepository.findByComponentCode("TMS");
|
||||
return version.map(DeviceSoftwareVersionEntity::getCurrentVersion).map(DeviceServiceImpl::trim).orElse("");
|
||||
}
|
||||
|
||||
private String loadDeviceSerial() {
|
||||
try {
|
||||
DeviceInfoResult result = pcieCryptoService.getDeviceInfo();
|
||||
return result == null ? "" : trim(result.getDeviceSerial());
|
||||
} catch (RuntimeException ex) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private boolean loadManagementKeyReady() {
|
||||
try {
|
||||
DeviceStatusResult result = pcieCryptoService.getDeviceStatus();
|
||||
return result != null && result.getFsmState() == 0;
|
||||
} catch (RuntimeException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.cisd.tms.modules.device.service.impl;
|
||||
|
||||
import com.cisd.tms.modules.device.dto.TimeConfigRequest;
|
||||
import com.cisd.tms.modules.device.service.TimeConfigService;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TimeConfigServiceImpl implements TimeConfigService {
|
||||
|
||||
@Override
|
||||
public void processTimeConfig(TimeConfigRequest request) {
|
||||
String mode = request.getMode();
|
||||
|
||||
if ("MANUAL".equalsIgnoreCase(mode)) {
|
||||
|
||||
String timezone = request.getTimezone();
|
||||
if (timezone == null || !timezone.matches("^[A-Za-z0-9/_-]+$")) {
|
||||
throw new IllegalArgumentException("Invalid or empty timezone format");
|
||||
}
|
||||
|
||||
String datetime = request.getDatetime();
|
||||
if (datetime == null || !datetime.matches("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}$")) {
|
||||
throw new IllegalArgumentException("Invalid or empty datetime format. Expected: YYYY-MM-DD HH:MM");
|
||||
}
|
||||
|
||||
executeCommand("timedatectl set-timezone " + request.getTimezone().trim());
|
||||
executeCommand("systemctl stop chronyd");
|
||||
executeCommand("date -s '" + request.getDatetime() + "'");
|
||||
|
||||
} else if ("NTP".equalsIgnoreCase(mode)) {
|
||||
|
||||
List<String> servers = request.getNtpServers();
|
||||
if (servers == null || servers.isEmpty()) {
|
||||
throw new IllegalArgumentException("At least one NTP server IP must be provided in NTP mode");
|
||||
}
|
||||
|
||||
int interval = request.getSyncInterval() != null ? request.getSyncInterval() : 600;
|
||||
int pollExp = (int) Math.round(Math.log(interval) / Math.log(2));
|
||||
StringBuilder serverConfLines = new StringBuilder();
|
||||
for (String server : servers) {
|
||||
if (!server.matches("^[A-Za-z0-9.-]+$")) {
|
||||
throw new IllegalArgumentException("Invalid NTP server address format: " + server);
|
||||
}
|
||||
|
||||
String pingResult = executeCommand("ping -c 1 -W 1 " + server);
|
||||
if (!pingResult.contains("1 received")) {
|
||||
throw new RuntimeException("NTP server is unreachable via ping: " + server);
|
||||
}
|
||||
serverConfLines.append("server ").append(server)
|
||||
.append(" iburst minpoll ").append(pollExp)
|
||||
.append(" maxpoll ").append(pollExp).append("\\n");
|
||||
}
|
||||
String confContent = serverConfLines +
|
||||
"driftfile /var/lib/chrony/drift\\n" +
|
||||
"makestep 1.0 3\\n" +
|
||||
"rtcsync\\n";
|
||||
executeCommand("echo -e '" + confContent + "' > /etc/chrony.conf");
|
||||
executeCommand("systemctl restart chronyd");
|
||||
executeCommand("chronyc -a makestep");
|
||||
String status = executeCommand("chronyc tracking");
|
||||
if (!status.contains("Reference ID")) {
|
||||
throw new RuntimeException("NTP sync status check failed");
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown mode: " + mode);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private String executeCommand(String command) {
|
||||
ProcessBuilder processBuilder = new ProcessBuilder("bash", "-c", command);
|
||||
processBuilder.redirectErrorStream(true);
|
||||
|
||||
Process process = null;
|
||||
try {
|
||||
process = processBuilder.start();
|
||||
|
||||
StringBuilder output = new StringBuilder();
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
output.append(line).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
throw new RuntimeException("command execution failed: [" + command + "] ");
|
||||
}
|
||||
return output.toString();
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("I/O error executing command: [" + command + "]", e);
|
||||
} catch (InterruptedException e) {
|
||||
if (process != null) {
|
||||
process.destroy();
|
||||
}
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("process was interrupted while executing command: [" + command + "]", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import com.cisd.tms.modules.file.service.FileService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.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) {
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
+6
-4
@@ -7,6 +7,7 @@ import com.cisd.tms.modules.device.dto.DeviceRuntimeStatusResponse;
|
||||
import com.cisd.tms.modules.device.dto.SoftwareRuntimeStatusItem;
|
||||
import com.cisd.tms.modules.device.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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user