feat:备份恢复

This commit is contained in:
waner
2026-04-14 14:57:25 +08:00
parent f60bef7142
commit 622702b0b0
103 changed files with 7786 additions and 13 deletions
@@ -0,0 +1,80 @@
package com.cisd.tms.modules.backup.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "tms.backup")
public class ResourceBackupProperties {
private String outputDir = "/home/tms/tmp/resource-backup-packages";
private String precheckStoreDir = "/home/tms/tmp/resource-restore-precheck";
private String restoreTaskRootDir = "/home/tms/tmp/resource-restore";
private String tmsScriptPath = "/home/tms/scripts/tms.sh";
private boolean runnerAutoLaunchEnabled = false;
private int runnerHeartbeatTimeoutSeconds = 300;
private int storageEncKeyIndex = 1;
private int authenticitySignKeyIndex = 1;
public String getOutputDir() {
return outputDir;
}
public void setOutputDir(String outputDir) {
this.outputDir = outputDir;
}
public String getPrecheckStoreDir() {
return precheckStoreDir;
}
public void setPrecheckStoreDir(String precheckStoreDir) {
this.precheckStoreDir = precheckStoreDir;
}
public String getRestoreTaskRootDir() {
return restoreTaskRootDir;
}
public void setRestoreTaskRootDir(String restoreTaskRootDir) {
this.restoreTaskRootDir = restoreTaskRootDir;
}
public String getTmsScriptPath() {
return tmsScriptPath;
}
public void setTmsScriptPath(String tmsScriptPath) {
this.tmsScriptPath = tmsScriptPath;
}
public boolean getRunnerAutoLaunchEnabled() {
return runnerAutoLaunchEnabled;
}
public void setRunnerAutoLaunchEnabled(boolean runnerAutoLaunchEnabled) {
this.runnerAutoLaunchEnabled = runnerAutoLaunchEnabled;
}
public int getRunnerHeartbeatTimeoutSeconds() {
return runnerHeartbeatTimeoutSeconds;
}
public void setRunnerHeartbeatTimeoutSeconds(int runnerHeartbeatTimeoutSeconds) {
this.runnerHeartbeatTimeoutSeconds = runnerHeartbeatTimeoutSeconds;
}
public int getStorageEncKeyIndex() {
return storageEncKeyIndex;
}
public void setStorageEncKeyIndex(int storageEncKeyIndex) {
this.storageEncKeyIndex = storageEncKeyIndex;
}
public int getAuthenticitySignKeyIndex() {
return authenticitySignKeyIndex;
}
public void setAuthenticitySignKeyIndex(int authenticitySignKeyIndex) {
this.authenticitySignKeyIndex = authenticitySignKeyIndex;
}
}
@@ -0,0 +1,98 @@
package com.cisd.tms.modules.backup.controller;
import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
import com.cisd.tms.modules.backup.dto.response.CreateResourceBackupResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceBackupTaskDetailResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceKeysetStatusResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
import com.cisd.tms.modules.backup.service.ResourceBackupService;
import com.cisd.tms.modules.backup.support.ResourceBackupDownloadResult;
import com.cisd.tms.security.internal.ReplayProtected;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/resource-backups")
@Tag(name = "资源备份", description = "资源备份创建、状态查询和步骤日志接口")
public class ResourceBackupController {
private final ResourceBackupService resourceBackupService;
public ResourceBackupController(ResourceBackupService resourceBackupService) {
this.resourceBackupService = resourceBackupService;
}
@GetMapping("/keyset/status")
@Operation(summary = "查询资源备份密钥状态", description = "返回资源备份恢复前置密钥状态和公钥指纹。")
public ApiResponse<ResourceKeysetStatusResponse> keysetStatus() {
return ApiResponse.success(resourceBackupService.getKeysetStatus());
}
@PostMapping
@ReplayProtected
@Operation(summary = "创建资源备份任务", description = "受理资源备份请求并返回备份任务号。")
public ApiResponse<CreateResourceBackupResponse> createBackupTask(@Valid @RequestBody CreateResourceBackupRequest request) {
return ApiResponse.success(resourceBackupService.createBackupTask(request));
}
@GetMapping("/{taskId}")
@Operation(summary = "查询资源备份任务详情", description = "返回资源备份任务详情。")
public ApiResponse<ResourceBackupTaskDetailResponse> taskDetail(
@Parameter(description = "备份任务号")
@PathVariable String taskId
) {
return ApiResponse.success(resourceBackupService.getBackupTask(taskId));
}
@GetMapping("/{taskId}/download")
@Operation(summary = "下载资源备份包", description = "以浏览器附件方式下载资源备份包。")
public ResponseEntity<Resource> downloadPackage(
@Parameter(description = "备份任务号")
@PathVariable String taskId
) {
ResourceBackupDownloadResult result = resourceBackupService.downloadBackupPackage(taskId);
FileSystemResource resource = new FileSystemResource(result.getPackagePath());
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(result.getContentLength())
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename(result.getFilename()).build().toString())
.body(resource);
}
@GetMapping("/{taskId}/steps")
@Operation(summary = "查询资源备份任务步骤", description = "返回资源备份任务步骤列表。")
public ApiResponse<List<ResourceTaskStepItemResponse>> taskSteps(
@Parameter(description = "备份任务号")
@PathVariable String taskId
) {
return ApiResponse.success(resourceBackupService.getBackupTaskSteps(taskId));
}
@GetMapping("/{taskId}/steps/{stepNo}/log")
@Operation(summary = "查询资源备份步骤日志", description = "返回资源备份任务步骤日志。")
public ApiResponse<ResourceTaskStepLogResponse> taskStepLog(
@Parameter(description = "备份任务号")
@PathVariable String taskId,
@Parameter(description = "步骤序号")
@PathVariable int stepNo
) {
return ApiResponse.success(resourceBackupService.getBackupTaskStepLog(taskId, stepNo));
}
}
@@ -0,0 +1,87 @@
package com.cisd.tms.modules.backup.controller;
import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest;
import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest;
import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
import com.cisd.tms.modules.backup.service.ResourcePrecheckService;
import com.cisd.tms.modules.backup.service.ResourceRestoreService;
import com.cisd.tms.modules.backup.service.ResourceTaskQueryService;
import com.cisd.tms.security.internal.ReplayProtected;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/resource-restores")
@Tag(name = "资源恢复", description = "资源恢复预检、任务创建、详情和步骤日志接口")
public class ResourceRestoreController {
private final ResourcePrecheckService resourcePrecheckService;
private final ResourceRestoreService resourceRestoreService;
private final ResourceTaskQueryService resourceTaskQueryService;
public ResourceRestoreController(
ResourcePrecheckService resourcePrecheckService,
ResourceRestoreService resourceRestoreService,
ResourceTaskQueryService resourceTaskQueryService
) {
this.resourcePrecheckService = resourcePrecheckService;
this.resourceRestoreService = resourceRestoreService;
this.resourceTaskQueryService = resourceTaskQueryService;
}
@PostMapping("/precheck")
@Operation(summary = "预检资源恢复请求", description = "根据 fileId 校验备份包头、签名和恢复范围摘要。")
public ApiResponse<ResourceRestorePrecheckResponse> precheck(@Valid @RequestBody ResourceRestorePrecheckRequest request) {
return ApiResponse.success(resourcePrecheckService.precheck(request));
}
@PostMapping
@ReplayProtected
@Operation(summary = "创建资源恢复任务", description = "根据预检号创建资源恢复任务。")
public ApiResponse<CreateResourceRestoreResponse> createRestoreTask(@Valid @RequestBody CreateResourceRestoreRequest request) {
return ApiResponse.success(resourceRestoreService.createRestoreTask(request));
}
@GetMapping("/{taskId}")
@Operation(summary = "查询资源恢复任务详情", description = "返回资源恢复任务详情和当前控制阶段。")
public ApiResponse<ResourceRestoreTaskDetailResponse> taskDetail(
@Parameter(description = "恢复任务号")
@PathVariable String taskId
) {
return ApiResponse.success(resourceTaskQueryService.getRestoreTask(taskId));
}
@GetMapping("/{taskId}/steps")
@Operation(summary = "查询资源恢复任务步骤", description = "返回资源恢复任务步骤列表。")
public ApiResponse<List<ResourceTaskStepItemResponse>> taskSteps(
@Parameter(description = "恢复任务号")
@PathVariable String taskId
) {
return ApiResponse.success(resourceTaskQueryService.getRestoreTaskSteps(taskId));
}
@GetMapping("/{taskId}/steps/{stepNo}/log")
@Operation(summary = "查询资源恢复步骤日志", description = "返回资源恢复步骤日志。")
public ApiResponse<ResourceTaskStepLogResponse> taskStepLog(
@Parameter(description = "恢复任务号")
@PathVariable String taskId,
@Parameter(description = "步骤序号")
@PathVariable int stepNo
) {
return ApiResponse.success(resourceTaskQueryService.getRestoreTaskStepLog(taskId, stepNo));
}
}
@@ -0,0 +1,17 @@
package com.cisd.tms.modules.backup.crypto;
public interface ResourceEnvelopeCryptoService {
ResourceSealedPayload seal(byte[] payloadBytes);
byte[] sign(byte[] dataToSign);
void verify(byte[] dataToVerify, byte[] signature);
byte[] unsealAndVerify(
java.util.Map<String, Object> envelope,
byte[] cipherBytes,
byte[] dataToVerify,
byte[] signature
);
}
@@ -0,0 +1,185 @@
package com.cisd.tms.modules.backup.crypto;
import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult;
import com.cisd.tms.integration.crypto.pcie.model.EccInternalDecryptRequest;
import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest;
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.service.PcieCryptoService;
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.stereotype.Service;
@Service
public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCryptoService {
private static final String PROVIDER = "BC";
private final PcieCryptoService pcieCryptoService;
private final ResourceBackupProperties resourceBackupProperties;
static {
if (Security.getProvider(PROVIDER) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
public ResourceEnvelopeCryptoServiceImpl(
PcieCryptoService pcieCryptoService,
ResourceBackupProperties resourceBackupProperties
) {
this.pcieCryptoService = pcieCryptoService;
this.resourceBackupProperties = resourceBackupProperties;
}
@Override
public ResourceSealedPayload seal(byte[] payloadBytes) {
if (payloadBytes == null) {
throw new IllegalArgumentException("payloadBytes is required");
}
// 资源包正文使用随机 SM4 DEK 加密,再由用户密钥把 DEK 封装成 envelope。
// 这样大 payload 仍走对称加密,ECC 只负责保护一次性数据密钥。
byte[] sm4Key = randomBytes(16);
byte[] iv = randomBytes(16);
try {
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(sm4Key, "SM4"), new IvParameterSpec(iv));
// 用户密钥只负责封装随机 DEK;真正的大 payload 仍走对称加密,避免直接把大文件送进 ECC 接口。
EccInternalEncryptRequest encryptRequest = new EccInternalEncryptRequest();
encryptRequest.setKeyIndex(resourceBackupProperties.getStorageEncKeyIndex());
encryptRequest.setData(sm4Key);
BackupDataResult wrappedDek = pcieCryptoService.sdfeInternalEncryptEcc(encryptRequest);
Map<String, Object> envelope = new LinkedHashMap<>();
envelope.put("version", 1);
envelope.put("payloadAlg", "SM4-CBC");
envelope.put("keyWrapAlg", "SDFE_InternalEncrypt_ECC");
envelope.put("ivBase64", Base64.getEncoder().encodeToString(iv));
envelope.put("wrappedDekBase64", Base64.getEncoder().encodeToString(normalize(wrappedDek)));
envelope.put("dekLength", sm4Key.length);
ResourceSealedPayload sealedPayload = new ResourceSealedPayload();
sealedPayload.setCipherBytes(cipher.doFinal(payloadBytes));
sealedPayload.setEnvelope(envelope);
sealedPayload.setStorageEncKeyFingerprint(buildFingerprint("USER_KEY_ENC", resourceBackupProperties.getStorageEncKeyIndex()));
sealedPayload.setAuthenticityKeyFingerprint(buildFingerprint("USER_KEY_SIGN", resourceBackupProperties.getAuthenticitySignKeyIndex()));
return sealedPayload;
} catch (Exception ex) {
throw new IllegalStateException("failed to seal resource payload", ex);
}
}
@Override
public byte[] sign(byte[] dataToSign) {
if (dataToSign == null) {
throw new IllegalArgumentException("dataToSign is required");
}
EccInternalSignRequest request = new EccInternalSignRequest();
request.setKeyIndex(resourceBackupProperties.getAuthenticitySignKeyIndex());
// 内部签名接口这里统一签 SM3 摘要,避免把整段原文直接交给密码卡并固定后续验签输入语义。
request.setData(sm3(dataToSign));
return normalize(pcieCryptoService.eccInternalSign(request));
}
@Override
public void verify(byte[] dataToVerify, byte[] signature) {
if (dataToVerify == null) {
throw new IllegalArgumentException("dataToVerify is required");
}
if (signature == null || signature.length == 0) {
throw new IllegalArgumentException("signature is required");
}
EccInternalVerifyRequest verifyRequest = new EccInternalVerifyRequest();
verifyRequest.setKeyIndex(resourceBackupProperties.getAuthenticitySignKeyIndex());
verifyRequest.setData(sm3(dataToVerify));
verifyRequest.setSignature(signature);
pcieCryptoService.eccInternalVerify(verifyRequest);
}
@Override
public byte[] unsealAndVerify(Map<String, Object> envelope, byte[] cipherBytes, byte[] dataToVerify, byte[] signature) {
if (envelope == null) {
throw new IllegalArgumentException("envelope is required");
}
if (cipherBytes == null) {
throw new IllegalArgumentException("cipherBytes is required");
}
if (dataToVerify == null) {
throw new IllegalArgumentException("dataToVerify is required");
}
if (signature == null || signature.length == 0) {
throw new IllegalArgumentException("signature is required");
}
verify(dataToVerify, signature);
byte[] wrappedDek = Base64.getDecoder().decode(String.valueOf(envelope.get("wrappedDekBase64")));
int dekLength = parseInt(envelope.get("dekLength"), 16);
// 先验签,再解数字信封,再解 payload;顺序固定,避免篡改包先进入明文处理路径。
EccInternalDecryptRequest decryptRequest = new EccInternalDecryptRequest();
decryptRequest.setKeyIndex(resourceBackupProperties.getStorageEncKeyIndex());
decryptRequest.setCipherBlob(wrappedDek);
decryptRequest.setOutBufferSize(dekLength);
byte[] sm4Key = normalize(pcieCryptoService.sdfeInternalDecryptEcc(decryptRequest));
byte[] iv = Base64.getDecoder().decode(String.valueOf(envelope.get("ivBase64")));
try {
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", PROVIDER);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(sm4Key, "SM4"), new IvParameterSpec(iv));
return cipher.doFinal(cipherBytes);
} catch (Exception ex) {
throw new IllegalStateException("failed to unseal resource payload", ex);
}
}
private static byte[] randomBytes(int size) {
byte[] bytes = new byte[size];
new java.security.SecureRandom().nextBytes(bytes);
return bytes;
}
private static byte[] sm3(byte[] data) {
SM3Digest digest = new SM3Digest();
digest.update(data, 0, data.length);
byte[] out = new byte[digest.getDigestSize()];
digest.doFinal(out, 0);
return out;
}
private static byte[] normalize(BackupDataResult result) {
if (result == null || result.getData() == null) {
return new byte[0];
}
int safeLength = result.getLength() > 0 && result.getLength() <= result.getData().length
? result.getLength()
: result.getData().length;
byte[] normalized = new byte[safeLength];
System.arraycopy(result.getData(), 0, normalized, 0, safeLength);
return normalized;
}
private static int parseInt(Object value, int defaultValue) {
if (value == null) {
return defaultValue;
}
if (value instanceof Number number) {
return number.intValue();
}
try {
return Integer.parseInt(String.valueOf(value));
} catch (NumberFormatException ex) {
return defaultValue;
}
}
private static String buildFingerprint(String prefix, int keyIndex) {
return prefix + ":" + keyIndex;
}
}
@@ -0,0 +1,43 @@
package com.cisd.tms.modules.backup.crypto;
import java.util.Map;
public class ResourceSealedPayload {
private byte[] cipherBytes;
private Map<String, Object> envelope;
private String storageEncKeyFingerprint;
private String authenticityKeyFingerprint;
public byte[] getCipherBytes() {
return cipherBytes;
}
public void setCipherBytes(byte[] cipherBytes) {
this.cipherBytes = cipherBytes;
}
public Map<String, Object> getEnvelope() {
return envelope;
}
public void setEnvelope(Map<String, Object> envelope) {
this.envelope = envelope;
}
public String getStorageEncKeyFingerprint() {
return storageEncKeyFingerprint;
}
public void setStorageEncKeyFingerprint(String storageEncKeyFingerprint) {
this.storageEncKeyFingerprint = storageEncKeyFingerprint;
}
public String getAuthenticityKeyFingerprint() {
return authenticityKeyFingerprint;
}
public void setAuthenticityKeyFingerprint(String authenticityKeyFingerprint) {
this.authenticityKeyFingerprint = authenticityKeyFingerprint;
}
}
@@ -0,0 +1,10 @@
package com.cisd.tms.modules.backup.discovery;
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
import com.cisd.tms.modules.init.entity.InitTaskEntity;
import java.util.List;
public interface ResourceDiscoveryService {
List<DiscoveredResourceItem> discover(InitTaskEntity initTask);
}
@@ -0,0 +1,80 @@
package com.cisd.tms.modules.backup.discovery;
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
import com.cisd.tms.modules.init.config.InitExecutorProperties;
import com.cisd.tms.modules.init.entity.InitTaskEntity;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
private final InitExecutorProperties initExecutorProperties;
public ResourceDiscoveryServiceImpl(InitExecutorProperties initExecutorProperties) {
this.initExecutorProperties = initExecutorProperties;
}
@Override
public List<DiscoveredResourceItem> discover(InitTaskEntity initTask) {
if (initTask == null) {
throw new IllegalArgumentException("initTask is required");
}
String productType = upper(initTask.getProductType());
String mqType = upper(initTask.getMqType());
if (productType.isEmpty() || mqType.isEmpty()) {
throw new IllegalArgumentException("productType and mqType are required");
}
List<DiscoveredResourceItem> items = new ArrayList<>();
items.add(item("FILE", "TMS_CONFIG", initExecutorProperties.getTmsConfigDir(), true));
items.add(item("FILE", "CP_CONFIG", resolveCpConfigPath(productType), true));
items.add(item("FILE", "RECEIVER_LICENSE", resolveReceiverLicensePath(productType), true));
if (mqType.startsWith("RABBITMQ")) {
items.add(item("MQ", "RABBITMQ_DEFINITIONS", initExecutorProperties.getRabbitmqAdminCommand(), true));
}
if ("TLQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType)) {
items.add(item("FILE", "TLQ_LICENSE", resolveTlqLicensePath(productType), true));
}
if ("RABBITMQ_CFMQ".equals(mqType)) {
items.add(item("FILE", "CFMQ_CONFIG", initExecutorProperties.getCfmqTargetDir(), true));
}
return items;
}
private String resolveCpConfigPath(String productType) {
if ("DIRECT".equals(productType)) {
return initExecutorProperties.getDirectCpconfigTargetPath();
}
return initExecutorProperties.getStandardCpconfigPath();
}
private String resolveReceiverLicensePath(String productType) {
if ("DIRECT".equals(productType)) {
return initExecutorProperties.getDirectReceiverLicenseTargetPath();
}
return initExecutorProperties.getStandardReceiverLicenseTargetPath();
}
private String resolveTlqLicensePath(String productType) {
if ("DIRECT".equals(productType)) {
return initExecutorProperties.getDirectTlqLicenseTargetPath();
}
return initExecutorProperties.getStandardTlqLicenseTargetPath();
}
private static DiscoveredResourceItem item(String type, String code, String path, boolean required) {
DiscoveredResourceItem item = new DiscoveredResourceItem();
item.setResourceType(type);
item.setResourceCode(code);
item.setResolvedPath(path);
item.setRequired(required);
return item;
}
private static String upper(String value) {
return value == null ? "" : value.trim().toUpperCase();
}
}
@@ -0,0 +1,40 @@
package com.cisd.tms.modules.backup.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "创建资源备份任务请求")
public class CreateResourceBackupRequest {
@Schema(description = "是否包含升级历史", example = "true")
private Boolean includeUpgradeHistory;
@Schema(description = "是否包含审计备份", example = "false")
private Boolean includeAuditBackups;
@Schema(description = "备注", example = "before hardware replacement")
private String remark;
public Boolean getIncludeUpgradeHistory() {
return includeUpgradeHistory;
}
public void setIncludeUpgradeHistory(Boolean includeUpgradeHistory) {
this.includeUpgradeHistory = includeUpgradeHistory;
}
public Boolean getIncludeAuditBackups() {
return includeAuditBackups;
}
public void setIncludeAuditBackups(Boolean includeAuditBackups) {
this.includeAuditBackups = includeAuditBackups;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}
@@ -0,0 +1,45 @@
package com.cisd.tms.modules.backup.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@Schema(description = "创建资源恢复任务请求")
public class CreateResourceRestoreRequest {
@NotBlank
@Schema(description = "预检号", example = "RPRE-20260408-000001", requiredMode = Schema.RequiredMode.REQUIRED)
private String precheckId;
@NotBlank
@Schema(description = "确认的备份号", example = "RBKP-20260408-153000-000001", requiredMode = Schema.RequiredMode.REQUIRED)
private String confirmBackupId;
@NotNull
@Schema(description = "是否确认接受恢复风险", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean confirmRiskAccepted;
public String getPrecheckId() {
return precheckId;
}
public void setPrecheckId(String precheckId) {
this.precheckId = precheckId;
}
public String getConfirmBackupId() {
return confirmBackupId;
}
public void setConfirmBackupId(String confirmBackupId) {
this.confirmBackupId = confirmBackupId;
}
public Boolean getConfirmRiskAccepted() {
return confirmRiskAccepted;
}
public void setConfirmRiskAccepted(Boolean confirmRiskAccepted) {
this.confirmRiskAccepted = confirmRiskAccepted;
}
}
@@ -0,0 +1,20 @@
package com.cisd.tms.modules.backup.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@Schema(description = "资源恢复预检请求")
public class ResourceRestorePrecheckRequest {
@NotBlank
@Schema(description = "备份包 fileId", example = "FILE-20260408-000001", requiredMode = Schema.RequiredMode.REQUIRED)
private String fileId;
public String getFileId() {
return fileId;
}
public void setFileId(String fileId) {
this.fileId = fileId;
}
}
@@ -0,0 +1,48 @@
package com.cisd.tms.modules.backup.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "创建资源备份任务响应")
public class CreateResourceBackupResponse {
@Schema(description = "备份任务号", example = "RBKP-TASK-20260408-000001")
private String taskId;
@Schema(description = "备份号", example = "RBKP-20260408-153000-000001")
private String backupId;
@Schema(description = "任务状态", example = "PENDING")
private String status;
@Schema(description = "创建时间", example = "2026-04-08T15:30:00+08:00")
private String createdAt;
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public String getBackupId() {
return backupId;
}
public void setBackupId(String backupId) {
this.backupId = backupId;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,35 @@
package com.cisd.tms.modules.backup.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "创建资源恢复任务响应")
public class CreateResourceRestoreResponse {
private String taskId;
private String status;
private String createdAt;
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
}
@@ -0,0 +1,206 @@
package com.cisd.tms.modules.backup.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "资源备份任务详情响应")
public class ResourceBackupTaskDetailResponse {
private String taskId;
private String backupId;
private String status;
private String productType;
private String mqType;
private String orgCode;
private String sourceDeviceId;
private String packageName;
private Long packageSize;
private String packageHash;
private String storageEncKeyFingerprint;
private String authenticityKeyFingerprint;
private Object manifestSummary;
private String currentStepCode;
private Integer currentStepNo;
private String remark;
private String errorCode;
private String errorMessage;
private String createdBy;
private String createdAt;
private String startedAt;
private String finishedAt;
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public String getBackupId() {
return backupId;
}
public void setBackupId(String backupId) {
this.backupId = backupId;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getProductType() {
return productType;
}
public void setProductType(String productType) {
this.productType = productType;
}
public String getMqType() {
return mqType;
}
public void setMqType(String mqType) {
this.mqType = mqType;
}
public String getOrgCode() {
return orgCode;
}
public void setOrgCode(String orgCode) {
this.orgCode = orgCode;
}
public String getSourceDeviceId() {
return sourceDeviceId;
}
public void setSourceDeviceId(String sourceDeviceId) {
this.sourceDeviceId = sourceDeviceId;
}
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public Long getPackageSize() {
return packageSize;
}
public void setPackageSize(Long packageSize) {
this.packageSize = packageSize;
}
public String getPackageHash() {
return packageHash;
}
public void setPackageHash(String packageHash) {
this.packageHash = packageHash;
}
public String getStorageEncKeyFingerprint() {
return storageEncKeyFingerprint;
}
public void setStorageEncKeyFingerprint(String storageEncKeyFingerprint) {
this.storageEncKeyFingerprint = storageEncKeyFingerprint;
}
public String getAuthenticityKeyFingerprint() {
return authenticityKeyFingerprint;
}
public void setAuthenticityKeyFingerprint(String authenticityKeyFingerprint) {
this.authenticityKeyFingerprint = authenticityKeyFingerprint;
}
public Object getManifestSummary() {
return manifestSummary;
}
public void setManifestSummary(Object manifestSummary) {
this.manifestSummary = manifestSummary;
}
public String getCurrentStepCode() {
return currentStepCode;
}
public void setCurrentStepCode(String currentStepCode) {
this.currentStepCode = currentStepCode;
}
public Integer getCurrentStepNo() {
return currentStepNo;
}
public void setCurrentStepNo(Integer currentStepNo) {
this.currentStepNo = currentStepNo;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public String getStartedAt() {
return startedAt;
}
public void setStartedAt(String startedAt) {
this.startedAt = startedAt;
}
public String getFinishedAt() {
return finishedAt;
}
public void setFinishedAt(String finishedAt) {
this.finishedAt = finishedAt;
}
}
@@ -0,0 +1,88 @@
package com.cisd.tms.modules.backup.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "资源备份密钥状态响应")
public class ResourceKeysetStatusResponse {
@Schema(description = "LMK 是否就绪")
private Boolean lmkReady;
@Schema(description = "IK 是否就绪")
private Boolean ikReady;
@Schema(description = "数据存储加密密钥对是否就绪")
private Boolean storageEncKeyReady;
@Schema(description = "数据真实性保护密钥对是否就绪")
private Boolean authenticityKeyReady;
@Schema(description = "数据存储加密公钥指纹")
private String storageEncKeyFingerprint;
@Schema(description = "真实性保护公钥指纹")
private String authenticityKeyFingerprint;
@Schema(description = "设备产品类型")
private String productType;
@Schema(description = "来源")
private String source;
public Boolean getLmkReady() {
return lmkReady;
}
public void setLmkReady(Boolean lmkReady) {
this.lmkReady = lmkReady;
}
public Boolean getIkReady() {
return ikReady;
}
public void setIkReady(Boolean ikReady) {
this.ikReady = ikReady;
}
public Boolean getStorageEncKeyReady() {
return storageEncKeyReady;
}
public void setStorageEncKeyReady(Boolean storageEncKeyReady) {
this.storageEncKeyReady = storageEncKeyReady;
}
public Boolean getAuthenticityKeyReady() {
return authenticityKeyReady;
}
public void setAuthenticityKeyReady(Boolean authenticityKeyReady) {
this.authenticityKeyReady = authenticityKeyReady;
}
public String getStorageEncKeyFingerprint() {
return storageEncKeyFingerprint;
}
public void setStorageEncKeyFingerprint(String storageEncKeyFingerprint) {
this.storageEncKeyFingerprint = storageEncKeyFingerprint;
}
public String getAuthenticityKeyFingerprint() {
return authenticityKeyFingerprint;
}
public void setAuthenticityKeyFingerprint(String authenticityKeyFingerprint) {
this.authenticityKeyFingerprint = authenticityKeyFingerprint;
}
public String getProductType() {
return productType;
}
public void setProductType(String productType) {
this.productType = productType;
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
}
@@ -0,0 +1,153 @@
package com.cisd.tms.modules.backup.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
@Schema(description = "资源恢复预检响应")
public class ResourceRestorePrecheckResponse {
private String precheckId;
private String backupId;
private String sourceProductType;
private String sourceMqType;
private String sourceOrgCode;
private String sourceBackupTime;
private String targetProductType;
private Boolean keysetMatched;
private Boolean signatureValid;
private Boolean compatible;
private Boolean payloadDecrypted;
private List<String> warnings;
private List<String> blockingIssues;
private List<String> restorePlan;
private Object restoreScopeSummary;
private String expiresAt;
public String getPrecheckId() {
return precheckId;
}
public void setPrecheckId(String precheckId) {
this.precheckId = precheckId;
}
public String getBackupId() {
return backupId;
}
public void setBackupId(String backupId) {
this.backupId = backupId;
}
public String getSourceProductType() {
return sourceProductType;
}
public void setSourceProductType(String sourceProductType) {
this.sourceProductType = sourceProductType;
}
public String getSourceMqType() {
return sourceMqType;
}
public void setSourceMqType(String sourceMqType) {
this.sourceMqType = sourceMqType;
}
public String getSourceOrgCode() {
return sourceOrgCode;
}
public void setSourceOrgCode(String sourceOrgCode) {
this.sourceOrgCode = sourceOrgCode;
}
public String getSourceBackupTime() {
return sourceBackupTime;
}
public void setSourceBackupTime(String sourceBackupTime) {
this.sourceBackupTime = sourceBackupTime;
}
public String getTargetProductType() {
return targetProductType;
}
public void setTargetProductType(String targetProductType) {
this.targetProductType = targetProductType;
}
public Boolean getKeysetMatched() {
return keysetMatched;
}
public void setKeysetMatched(Boolean keysetMatched) {
this.keysetMatched = keysetMatched;
}
public Boolean getSignatureValid() {
return signatureValid;
}
public void setSignatureValid(Boolean signatureValid) {
this.signatureValid = signatureValid;
}
public Boolean getCompatible() {
return compatible;
}
public void setCompatible(Boolean compatible) {
this.compatible = compatible;
}
public Boolean getPayloadDecrypted() {
return payloadDecrypted;
}
public void setPayloadDecrypted(Boolean payloadDecrypted) {
this.payloadDecrypted = payloadDecrypted;
}
public List<String> getWarnings() {
return warnings;
}
public void setWarnings(List<String> warnings) {
this.warnings = warnings;
}
public List<String> getBlockingIssues() {
return blockingIssues;
}
public void setBlockingIssues(List<String> blockingIssues) {
this.blockingIssues = blockingIssues;
}
public List<String> getRestorePlan() {
return restorePlan;
}
public void setRestorePlan(List<String> restorePlan) {
this.restorePlan = restorePlan;
}
public Object getRestoreScopeSummary() {
return restoreScopeSummary;
}
public void setRestoreScopeSummary(Object restoreScopeSummary) {
this.restoreScopeSummary = restoreScopeSummary;
}
public String getExpiresAt() {
return expiresAt;
}
public void setExpiresAt(String expiresAt) {
this.expiresAt = expiresAt;
}
}
@@ -0,0 +1,261 @@
package com.cisd.tms.modules.backup.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
@Schema(description = "资源恢复任务详情响应")
public class ResourceRestoreTaskDetailResponse {
private String taskId;
private String precheckId;
private String backupId;
private String status;
private String controlPhase;
private String stateSource;
private String sourceDeviceId;
private String sourceProductType;
private String targetProductType;
private String packageFileId;
private String packageName;
private Boolean packagePathVisible;
private String currentStepCode;
private Integer currentStepNo;
private String lastHeartbeatAt;
private String runnerStatePath;
private Boolean orphaned;
private List<String> cleanupWarnings;
private List<String> warnings;
private List<String> blockingIssues;
private List<String> restorePlan;
private Object consistencyReport;
private String errorCode;
private String errorMessage;
private String createdBy;
private String createdAt;
private String startedAt;
private String finishedAt;
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public String getPrecheckId() {
return precheckId;
}
public void setPrecheckId(String precheckId) {
this.precheckId = precheckId;
}
public String getBackupId() {
return backupId;
}
public void setBackupId(String backupId) {
this.backupId = backupId;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getControlPhase() {
return controlPhase;
}
public void setControlPhase(String controlPhase) {
this.controlPhase = controlPhase;
}
public String getStateSource() {
return stateSource;
}
public void setStateSource(String stateSource) {
this.stateSource = stateSource;
}
public String getSourceDeviceId() {
return sourceDeviceId;
}
public void setSourceDeviceId(String sourceDeviceId) {
this.sourceDeviceId = sourceDeviceId;
}
public String getSourceProductType() {
return sourceProductType;
}
public void setSourceProductType(String sourceProductType) {
this.sourceProductType = sourceProductType;
}
public String getTargetProductType() {
return targetProductType;
}
public void setTargetProductType(String targetProductType) {
this.targetProductType = targetProductType;
}
public String getPackageFileId() {
return packageFileId;
}
public void setPackageFileId(String packageFileId) {
this.packageFileId = packageFileId;
}
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public Boolean getPackagePathVisible() {
return packagePathVisible;
}
public void setPackagePathVisible(Boolean packagePathVisible) {
this.packagePathVisible = packagePathVisible;
}
public String getCurrentStepCode() {
return currentStepCode;
}
public void setCurrentStepCode(String currentStepCode) {
this.currentStepCode = currentStepCode;
}
public Integer getCurrentStepNo() {
return currentStepNo;
}
public void setCurrentStepNo(Integer currentStepNo) {
this.currentStepNo = currentStepNo;
}
public String getLastHeartbeatAt() {
return lastHeartbeatAt;
}
public void setLastHeartbeatAt(String lastHeartbeatAt) {
this.lastHeartbeatAt = lastHeartbeatAt;
}
public String getRunnerStatePath() {
return runnerStatePath;
}
public void setRunnerStatePath(String runnerStatePath) {
this.runnerStatePath = runnerStatePath;
}
public Boolean getOrphaned() {
return orphaned;
}
public void setOrphaned(Boolean orphaned) {
this.orphaned = orphaned;
}
public List<String> getCleanupWarnings() {
return cleanupWarnings;
}
public void setCleanupWarnings(List<String> cleanupWarnings) {
this.cleanupWarnings = cleanupWarnings;
}
public List<String> getWarnings() {
return warnings;
}
public void setWarnings(List<String> warnings) {
this.warnings = warnings;
}
public List<String> getBlockingIssues() {
return blockingIssues;
}
public void setBlockingIssues(List<String> blockingIssues) {
this.blockingIssues = blockingIssues;
}
public List<String> getRestorePlan() {
return restorePlan;
}
public void setRestorePlan(List<String> restorePlan) {
this.restorePlan = restorePlan;
}
public Object getConsistencyReport() {
return consistencyReport;
}
public void setConsistencyReport(Object consistencyReport) {
this.consistencyReport = consistencyReport;
}
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public String getStartedAt() {
return startedAt;
}
public void setStartedAt(String startedAt) {
this.startedAt = startedAt;
}
public String getFinishedAt() {
return finishedAt;
}
public void setFinishedAt(String finishedAt) {
this.finishedAt = finishedAt;
}
}
@@ -0,0 +1,80 @@
package com.cisd.tms.modules.backup.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "资源任务步骤项响应")
public class ResourceTaskStepItemResponse {
private Integer stepNo;
private String stepCode;
private String status;
private String message;
private Integer exitCode;
private String startedAt;
private String finishedAt;
private Boolean logAvailable;
public Integer getStepNo() {
return stepNo;
}
public void setStepNo(Integer stepNo) {
this.stepNo = stepNo;
}
public String getStepCode() {
return stepCode;
}
public void setStepCode(String stepCode) {
this.stepCode = stepCode;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Integer getExitCode() {
return exitCode;
}
public void setExitCode(Integer exitCode) {
this.exitCode = exitCode;
}
public String getStartedAt() {
return startedAt;
}
public void setStartedAt(String startedAt) {
this.startedAt = startedAt;
}
public String getFinishedAt() {
return finishedAt;
}
public void setFinishedAt(String finishedAt) {
this.finishedAt = finishedAt;
}
public Boolean getLogAvailable() {
return logAvailable;
}
public void setLogAvailable(Boolean logAvailable) {
this.logAvailable = logAvailable;
}
}
@@ -0,0 +1,53 @@
package com.cisd.tms.modules.backup.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "资源任务步骤日志响应")
public class ResourceTaskStepLogResponse {
private String taskId;
private Integer stepNo;
private String stepCode;
private String content;
private Boolean truncated;
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public Integer getStepNo() {
return stepNo;
}
public void setStepNo(Integer stepNo) {
this.stepNo = stepNo;
}
public String getStepCode() {
return stepCode;
}
public void setStepCode(String stepCode) {
this.stepCode = stepCode;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Boolean getTruncated() {
return truncated;
}
public void setTruncated(Boolean truncated) {
this.truncated = truncated;
}
}
@@ -0,0 +1,41 @@
package com.cisd.tms.modules.backup.dto.support;
public class DiscoveredResourceItem {
private String resourceType;
private String resourceCode;
private String resolvedPath;
private boolean required;
public String getResourceType() {
return resourceType;
}
public void setResourceType(String resourceType) {
this.resourceType = resourceType;
}
public String getResourceCode() {
return resourceCode;
}
public void setResourceCode(String resourceCode) {
this.resourceCode = resourceCode;
}
public String getResolvedPath() {
return resolvedPath;
}
public void setResolvedPath(String resolvedPath) {
this.resolvedPath = resolvedPath;
}
public boolean isRequired() {
return required;
}
public void setRequired(boolean required) {
this.required = required;
}
}
@@ -0,0 +1,190 @@
package com.cisd.tms.modules.backup.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
import java.time.LocalDateTime;
@TableName("tms_resource_backup_task")
public class ResourceBackupTaskEntity extends BaseEntity {
private String taskId;
private String backupId;
private String status;
private String productType;
private String mqType;
private String orgCode;
private String sourceDeviceId;
private String packageName;
private String packagePath;
private Long packageSize;
private String packageHash;
private String storageEncKeyFingerprint;
private String authenticityKeyFingerprint;
private String manifestSummaryJson;
private String errorCode;
private String errorMessage;
private String remark;
private String createBy;
private LocalDateTime startTime;
private LocalDateTime finishTime;
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public String getBackupId() {
return backupId;
}
public void setBackupId(String backupId) {
this.backupId = backupId;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getProductType() {
return productType;
}
public void setProductType(String productType) {
this.productType = productType;
}
public String getMqType() {
return mqType;
}
public void setMqType(String mqType) {
this.mqType = mqType;
}
public String getOrgCode() {
return orgCode;
}
public void setOrgCode(String orgCode) {
this.orgCode = orgCode;
}
public String getSourceDeviceId() {
return sourceDeviceId;
}
public void setSourceDeviceId(String sourceDeviceId) {
this.sourceDeviceId = sourceDeviceId;
}
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public String getPackagePath() {
return packagePath;
}
public void setPackagePath(String packagePath) {
this.packagePath = packagePath;
}
public Long getPackageSize() {
return packageSize;
}
public void setPackageSize(Long packageSize) {
this.packageSize = packageSize;
}
public String getPackageHash() {
return packageHash;
}
public void setPackageHash(String packageHash) {
this.packageHash = packageHash;
}
public String getStorageEncKeyFingerprint() {
return storageEncKeyFingerprint;
}
public void setStorageEncKeyFingerprint(String storageEncKeyFingerprint) {
this.storageEncKeyFingerprint = storageEncKeyFingerprint;
}
public String getAuthenticityKeyFingerprint() {
return authenticityKeyFingerprint;
}
public void setAuthenticityKeyFingerprint(String authenticityKeyFingerprint) {
this.authenticityKeyFingerprint = authenticityKeyFingerprint;
}
public String getManifestSummaryJson() {
return manifestSummaryJson;
}
public void setManifestSummaryJson(String manifestSummaryJson) {
this.manifestSummaryJson = manifestSummaryJson;
}
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public String getCreateBy() {
return createBy;
}
public void setCreateBy(String createBy) {
this.createBy = createBy;
}
public LocalDateTime getStartTime() {
return startTime;
}
public void setStartTime(LocalDateTime startTime) {
this.startTime = startTime;
}
public LocalDateTime getFinishTime() {
return finishTime;
}
public void setFinishTime(LocalDateTime finishTime) {
this.finishTime = finishTime;
}
}
@@ -0,0 +1,199 @@
package com.cisd.tms.modules.backup.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
import java.time.LocalDateTime;
@TableName("tms_resource_restore_task")
public class ResourceRestoreTaskEntity extends BaseEntity {
private String taskId;
private String precheckId;
private String backupId;
private String status;
private String sourceDeviceId;
private String sourceProductType;
private String targetProductType;
private String packageFileId;
private String packagePath;
private String stagingDir;
private String runnerStatePath;
private String storageEncKeyFingerprint;
private String authenticityKeyFingerprint;
private String restorePlanJson;
private String consistencyReportJson;
private String errorCode;
private String errorMessage;
private String remark;
private String createBy;
private LocalDateTime startTime;
private LocalDateTime finishTime;
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public String getPrecheckId() {
return precheckId;
}
public void setPrecheckId(String precheckId) {
this.precheckId = precheckId;
}
public String getBackupId() {
return backupId;
}
public void setBackupId(String backupId) {
this.backupId = backupId;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getSourceDeviceId() {
return sourceDeviceId;
}
public void setSourceDeviceId(String sourceDeviceId) {
this.sourceDeviceId = sourceDeviceId;
}
public String getSourceProductType() {
return sourceProductType;
}
public void setSourceProductType(String sourceProductType) {
this.sourceProductType = sourceProductType;
}
public String getTargetProductType() {
return targetProductType;
}
public void setTargetProductType(String targetProductType) {
this.targetProductType = targetProductType;
}
public String getPackageFileId() {
return packageFileId;
}
public void setPackageFileId(String packageFileId) {
this.packageFileId = packageFileId;
}
public String getPackagePath() {
return packagePath;
}
public void setPackagePath(String packagePath) {
this.packagePath = packagePath;
}
public String getStagingDir() {
return stagingDir;
}
public void setStagingDir(String stagingDir) {
this.stagingDir = stagingDir;
}
public String getRunnerStatePath() {
return runnerStatePath;
}
public void setRunnerStatePath(String runnerStatePath) {
this.runnerStatePath = runnerStatePath;
}
public String getStorageEncKeyFingerprint() {
return storageEncKeyFingerprint;
}
public void setStorageEncKeyFingerprint(String storageEncKeyFingerprint) {
this.storageEncKeyFingerprint = storageEncKeyFingerprint;
}
public String getAuthenticityKeyFingerprint() {
return authenticityKeyFingerprint;
}
public void setAuthenticityKeyFingerprint(String authenticityKeyFingerprint) {
this.authenticityKeyFingerprint = authenticityKeyFingerprint;
}
public String getRestorePlanJson() {
return restorePlanJson;
}
public void setRestorePlanJson(String restorePlanJson) {
this.restorePlanJson = restorePlanJson;
}
public String getConsistencyReportJson() {
return consistencyReportJson;
}
public void setConsistencyReportJson(String consistencyReportJson) {
this.consistencyReportJson = consistencyReportJson;
}
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public String getCreateBy() {
return createBy;
}
public void setCreateBy(String createBy) {
this.createBy = createBy;
}
public LocalDateTime getStartTime() {
return startTime;
}
public void setStartTime(LocalDateTime startTime) {
this.startTime = startTime;
}
public LocalDateTime getFinishTime() {
return finishTime;
}
public void setFinishTime(LocalDateTime finishTime) {
this.finishTime = finishTime;
}
}
@@ -0,0 +1,100 @@
package com.cisd.tms.modules.backup.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
import java.time.LocalDateTime;
@TableName("tms_resource_task_step")
public class ResourceTaskStepEntity extends BaseEntity {
private String taskId;
private String taskType;
private Integer stepNo;
private String stepCode;
private String status;
private String message;
private String logPath;
private Integer exitCode;
private LocalDateTime startTime;
private LocalDateTime finishTime;
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public String getTaskType() {
return taskType;
}
public void setTaskType(String taskType) {
this.taskType = taskType;
}
public Integer getStepNo() {
return stepNo;
}
public void setStepNo(Integer stepNo) {
this.stepNo = stepNo;
}
public String getStepCode() {
return stepCode;
}
public void setStepCode(String stepCode) {
this.stepCode = stepCode;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getLogPath() {
return logPath;
}
public void setLogPath(String logPath) {
this.logPath = logPath;
}
public Integer getExitCode() {
return exitCode;
}
public void setExitCode(Integer exitCode) {
this.exitCode = exitCode;
}
public LocalDateTime getStartTime() {
return startTime;
}
public void setStartTime(LocalDateTime startTime) {
this.startTime = startTime;
}
public LocalDateTime getFinishTime() {
return finishTime;
}
public void setFinishTime(LocalDateTime finishTime) {
this.finishTime = finishTime;
}
}
@@ -0,0 +1,12 @@
package com.cisd.tms.modules.backup.enums;
/**
* 恢复任务控制阶段。
*/
public enum ResourceControlPhase {
TMS_PRECHECK,
HANDOFF_READY,
RUNNER_OWNED,
RECONCILE_PENDING,
DONE
}
@@ -0,0 +1,13 @@
package com.cisd.tms.modules.backup.enums;
/**
* 资源任务步骤状态。
*/
public enum ResourceStepStatus {
PENDING,
RUNNING,
SUCCESS,
SUCCESS_WITH_WARNINGS,
FAILED,
SKIPPED
}
@@ -0,0 +1,17 @@
package com.cisd.tms.modules.backup.enums;
/**
* 资源备份/恢复任务主状态。
*/
public enum ResourceTaskStatus {
PENDING,
PRECHECKING,
PRECHECK_FAILED,
READY,
HANDOFF,
RUNNING,
VERIFYING,
SUCCESS,
FAILED,
CANCELLED
}
@@ -0,0 +1,9 @@
package com.cisd.tms.modules.backup.enums;
/**
* 资源任务类型。
*/
public enum ResourceTaskType {
RESOURCE_BACKUP,
RESOURCE_RESTORE
}
@@ -0,0 +1,20 @@
package com.cisd.tms.modules.backup.mapper;
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface ResourceBackupTaskMapper extends BaseMapperX<ResourceBackupTaskEntity> {
@Select(
"SELECT id, task_id, backup_id, status, product_type, mq_type, org_code, source_device_id, "
+ "package_name, package_path, package_size, package_hash, storage_enc_key_fingerprint, "
+ "authenticity_key_fingerprint, manifest_summary_json, error_code, error_message, remark, "
+ "create_by, create_time, update_time, start_time, finish_time "
+ "FROM tms_resource_backup_task WHERE task_id = #{taskId}"
)
ResourceBackupTaskEntity selectByTaskId(@Param("taskId") String taskId);
}
@@ -0,0 +1,21 @@
package com.cisd.tms.modules.backup.mapper;
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface ResourceRestoreTaskMapper extends BaseMapperX<ResourceRestoreTaskEntity> {
@Select(
"SELECT id, task_id, precheck_id, backup_id, status, source_device_id, source_product_type, "
+ "target_product_type, package_file_id, package_path, staging_dir, runner_state_path, "
+ "storage_enc_key_fingerprint, authenticity_key_fingerprint, restore_plan_json, "
+ "consistency_report_json, error_code, error_message, remark, create_by, create_time, "
+ "update_time, start_time, finish_time "
+ "FROM tms_resource_restore_task WHERE task_id = #{taskId}"
)
ResourceRestoreTaskEntity selectByTaskId(@Param("taskId") String taskId);
}
@@ -0,0 +1,19 @@
package com.cisd.tms.modules.backup.mapper;
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
import com.cisd.tms.modules.backup.entity.ResourceTaskStepEntity;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface ResourceTaskStepMapper extends BaseMapperX<ResourceTaskStepEntity> {
@Select(
"SELECT id, task_id, task_type, step_no, step_code, status, message, log_path, exit_code, "
+ "create_time, update_time, start_time, finish_time "
+ "FROM tms_resource_task_step WHERE task_id = #{taskId} ORDER BY step_no ASC"
)
List<ResourceTaskStepEntity> selectByTaskIdOrderByStepNo(@Param("taskId") String taskId);
}
@@ -0,0 +1,29 @@
package com.cisd.tms.modules.backup.packagex;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
final class ResourcePackageFormatSupport {
private ResourcePackageFormatSupport() {
}
static byte[] buildSignatureSource(byte[] manifestBytes, byte[] envelopeBytes, byte[] cipherBytes) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
writePart(outputStream, manifestBytes);
writePart(outputStream, envelopeBytes);
writePart(outputStream, cipherBytes);
return outputStream.toByteArray();
} catch (IOException ex) {
throw new IllegalStateException("failed to build package signature source", ex);
}
}
private static void writePart(ByteArrayOutputStream outputStream, byte[] data) throws IOException {
byte[] safeData = data == null ? new byte[0] : data;
outputStream.write(ByteBuffer.allocate(4).putInt(safeData.length).array());
outputStream.write(safeData);
}
}
@@ -0,0 +1,52 @@
package com.cisd.tms.modules.backup.packagex;
import java.util.Map;
public class ResourcePackageInspectResult {
private Map<String, Object> manifest;
private Map<String, Object> envelope;
private Map<String, Object> payload;
private byte[] payloadArchiveBytes;
private byte[] signature;
public Map<String, Object> getManifest() {
return manifest;
}
public void setManifest(Map<String, Object> manifest) {
this.manifest = manifest;
}
public Map<String, Object> getEnvelope() {
return envelope;
}
public void setEnvelope(Map<String, Object> envelope) {
this.envelope = envelope;
}
public Map<String, Object> getPayload() {
return payload;
}
public void setPayload(Map<String, Object> payload) {
this.payload = payload;
}
public byte[] getPayloadArchiveBytes() {
return payloadArchiveBytes;
}
public void setPayloadArchiveBytes(byte[] payloadArchiveBytes) {
this.payloadArchiveBytes = payloadArchiveBytes;
}
public byte[] getSignature() {
return signature;
}
public void setSignature(byte[] signature) {
this.signature = signature;
}
}
@@ -0,0 +1,68 @@
package com.cisd.tms.modules.backup.packagex;
public class ResourcePackageResult {
private String packageName;
private String packagePath;
private Long packageSize;
private String packageHash;
private String storageEncKeyFingerprint;
private String authenticityKeyFingerprint;
private String manifestSummaryJson;
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public String getPackagePath() {
return packagePath;
}
public void setPackagePath(String packagePath) {
this.packagePath = packagePath;
}
public Long getPackageSize() {
return packageSize;
}
public void setPackageSize(Long packageSize) {
this.packageSize = packageSize;
}
public String getPackageHash() {
return packageHash;
}
public void setPackageHash(String packageHash) {
this.packageHash = packageHash;
}
public String getStorageEncKeyFingerprint() {
return storageEncKeyFingerprint;
}
public void setStorageEncKeyFingerprint(String storageEncKeyFingerprint) {
this.storageEncKeyFingerprint = storageEncKeyFingerprint;
}
public String getAuthenticityKeyFingerprint() {
return authenticityKeyFingerprint;
}
public void setAuthenticityKeyFingerprint(String authenticityKeyFingerprint) {
this.authenticityKeyFingerprint = authenticityKeyFingerprint;
}
public String getManifestSummaryJson() {
return manifestSummaryJson;
}
public void setManifestSummaryJson(String manifestSummaryJson) {
this.manifestSummaryJson = manifestSummaryJson;
}
}
@@ -0,0 +1,22 @@
package com.cisd.tms.modules.backup.packagex;
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
import com.cisd.tms.modules.init.entity.InitTaskEntity;
import java.nio.file.Path;
import java.util.List;
public interface ResourcePackageService {
ResourcePackageResult createPackage(
String taskId,
String backupId,
InitTaskEntity initTask,
List<DiscoveredResourceItem> discoveredItems,
CreateResourceBackupRequest request
);
ResourcePackageInspectResult inspectPackageHeader(Path packagePath);
ResourcePackageInspectResult inspectPackage(Path packagePath);
}
@@ -0,0 +1,392 @@
package com.cisd.tms.modules.backup.packagex;
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
import com.cisd.tms.modules.backup.crypto.ResourceEnvelopeCryptoService;
import com.cisd.tms.modules.backup.crypto.ResourceSealedPayload;
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
import com.cisd.tms.modules.init.entity.InitTaskEntity;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.springframework.stereotype.Service;
@Service
public class ResourcePackageServiceImpl implements ResourcePackageService {
private final ResourceBackupProperties resourceBackupProperties;
private final ResourceEnvelopeCryptoService resourceEnvelopeCryptoService;
private final ObjectMapper objectMapper;
public ResourcePackageServiceImpl(
ResourceBackupProperties resourceBackupProperties,
ResourceEnvelopeCryptoService resourceEnvelopeCryptoService,
ObjectMapper objectMapper
) {
this.resourceBackupProperties = resourceBackupProperties;
this.resourceEnvelopeCryptoService = resourceEnvelopeCryptoService;
this.objectMapper = objectMapper;
}
@Override
public ResourcePackageResult createPackage(
String taskId,
String backupId,
InitTaskEntity initTask,
List<DiscoveredResourceItem> discoveredItems,
CreateResourceBackupRequest request
) {
if (isBlank(taskId) || isBlank(backupId)) {
throw new IllegalArgumentException("taskId and backupId are required");
}
if (initTask == null) {
throw new IllegalArgumentException("initTask is required");
}
if (discoveredItems == null) {
throw new IllegalArgumentException("discoveredItems is required");
}
try {
Path taskDir = Path.of(resourceBackupProperties.getOutputDir()).resolve(taskId).normalize();
Files.createDirectories(taskDir);
// 资源恢复真正消费的是“受保护的内部归档”,而不是一段摘要 JSON。
// 这里先把可采集的文件资源打成 payload.zip,再统一交给数字信封加密,
// 后续即使补数据库导出/MQ definitions,也仍然沿着这个边界扩展。
PayloadArchiveBuildResult payloadArchive = buildPayloadArchive(
taskId,
backupId,
initTask,
discoveredItems,
request
);
ResourceSealedPayload sealedPayload = resourceEnvelopeCryptoService.seal(payloadArchive.archiveBytes());
// manifest 只保留恢复前需要快速识别的元数据,不直接展开敏感资源内容。
Map<String, Object> manifest = new LinkedHashMap<>();
manifest.put("backupId", backupId);
manifest.put("taskId", taskId);
manifest.put("backupTime", LocalDateTime.now().toString());
manifest.put("productType", initTask.getProductType());
manifest.put("mqType", initTask.getMqType());
manifest.put("orgCode", initTask.getOrgCode());
manifest.put("resourceCount", discoveredItems.size());
manifest.put("collectedResourceCount", payloadArchive.collectedResourceCount());
manifest.put("storageEncKeyFingerprint", sealedPayload.getStorageEncKeyFingerprint());
manifest.put("authenticityKeyFingerprint", sealedPayload.getAuthenticityKeyFingerprint());
byte[] manifestBytes = objectMapper.writeValueAsBytes(manifest);
byte[] envelopeBytes = objectMapper.writeValueAsBytes(sealedPayload.getEnvelope());
// 签名覆盖 manifest + envelope + ciphertext 三段,恢复端按同一字节拼装规则重建验签输入。
byte[] signatureBytes = resourceEnvelopeCryptoService.sign(
ResourcePackageFormatSupport.buildSignatureSource(manifestBytes, envelopeBytes, sealedPayload.getCipherBytes())
);
String packageName = backupId + ".tmsbak";
Path packagePath = taskDir.resolve(packageName);
try (OutputStream outputStream = Files.newOutputStream(packagePath);
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
// 当前包结构已经固定成 manifest + envelope + ciphertext,后续即使替换加密实现也不必重做外层格式。
addZipEntry(zipOutputStream, "manifest.json", manifestBytes);
addZipEntry(zipOutputStream, "envelope.json", envelopeBytes);
addZipEntry(zipOutputStream, "payload.enc", sealedPayload.getCipherBytes());
addZipEntry(zipOutputStream, "signature.sig", signatureBytes);
}
Map<String, Object> manifestSummary = new LinkedHashMap<>();
manifestSummary.put("resourceCount", discoveredItems.size());
manifestSummary.put("collectedResourceCount", payloadArchive.collectedResourceCount());
manifestSummary.put("productType", initTask.getProductType());
manifestSummary.put("mqType", initTask.getMqType());
manifestSummary.put("orgCode", initTask.getOrgCode());
manifestSummary.put("packageName", packageName);
ResourcePackageResult result = new ResourcePackageResult();
result.setPackageName(packageName);
result.setPackagePath(packagePath.toString());
result.setPackageSize(Files.size(packagePath));
// 这里记录的是整个备份包文件的 SM3,方便后续下载校验和审计追踪。
result.setPackageHash(sm3Hex(Files.readAllBytes(packagePath)));
result.setStorageEncKeyFingerprint(sealedPayload.getStorageEncKeyFingerprint());
result.setAuthenticityKeyFingerprint(sealedPayload.getAuthenticityKeyFingerprint());
result.setManifestSummaryJson(objectMapper.writeValueAsString(manifestSummary));
return result;
} catch (IOException ex) {
throw new IllegalStateException("failed to create backup package", ex);
}
}
@Override
public ResourcePackageInspectResult inspectPackageHeader(Path packagePath) {
if (packagePath == null) {
throw new IllegalArgumentException("packagePath is required");
}
try (ZipFile zipFile = new ZipFile(packagePath.toFile())) {
byte[] manifestBytes = readRequiredEntry(zipFile, "manifest.json");
byte[] envelopeBytes = readRequiredEntry(zipFile, "envelope.json");
byte[] cipherBytes = readRequiredEntry(zipFile, "payload.enc");
byte[] signatureBytes = readRequiredEntry(zipFile, "signature.sig");
resourceEnvelopeCryptoService.verify(
ResourcePackageFormatSupport.buildSignatureSource(manifestBytes, envelopeBytes, cipherBytes),
signatureBytes
);
ResourcePackageInspectResult result = new ResourcePackageInspectResult();
result.setManifest(objectMapper.readValue(manifestBytes, new TypeReference<HashMap<String, Object>>() {
}));
result.setEnvelope(objectMapper.readValue(envelopeBytes, new TypeReference<HashMap<String, Object>>() {
}));
result.setSignature(signatureBytes);
return result;
} catch (IOException ex) {
throw new IllegalStateException("failed to inspect backup package header", ex);
}
}
@Override
public ResourcePackageInspectResult inspectPackage(Path packagePath) {
if (packagePath == null) {
throw new IllegalArgumentException("packagePath is required");
}
try (ZipFile zipFile = new ZipFile(packagePath.toFile())) {
byte[] manifestBytes = readRequiredEntry(zipFile, "manifest.json");
byte[] envelopeBytes = readRequiredEntry(zipFile, "envelope.json");
byte[] cipherBytes = readRequiredEntry(zipFile, "payload.enc");
byte[] signatureBytes = readRequiredEntry(zipFile, "signature.sig");
byte[] payloadBytes = resourceEnvelopeCryptoService.unsealAndVerify(
objectMapper.readValue(envelopeBytes, new TypeReference<Map<String, Object>>() {
}),
cipherBytes,
ResourcePackageFormatSupport.buildSignatureSource(manifestBytes, envelopeBytes, cipherBytes),
signatureBytes
);
Map<String, Object> payloadManifest = readPayloadManifest(payloadBytes);
ResourcePackageInspectResult result = new ResourcePackageInspectResult();
result.setManifest(objectMapper.readValue(manifestBytes, new TypeReference<HashMap<String, Object>>() {
}));
result.setEnvelope(objectMapper.readValue(envelopeBytes, new TypeReference<HashMap<String, Object>>() {
}));
result.setPayload(payloadManifest);
result.setPayloadArchiveBytes(payloadBytes);
result.setSignature(signatureBytes);
return result;
} catch (IOException ex) {
throw new IllegalStateException("failed to inspect backup package", ex);
}
}
private PayloadArchiveBuildResult buildPayloadArchive(
String taskId,
String backupId,
InitTaskEntity initTask,
List<DiscoveredResourceItem> discoveredItems,
CreateResourceBackupRequest request
) throws IOException {
Map<String, Object> payloadManifest = new LinkedHashMap<>();
payloadManifest.put("archiveFormat", "ZIP");
payloadManifest.put("backupId", backupId);
payloadManifest.put("taskId", taskId);
payloadManifest.put("productType", initTask.getProductType());
payloadManifest.put("mqType", initTask.getMqType());
payloadManifest.put("orgCode", initTask.getOrgCode());
payloadManifest.put("remark", request == null ? null : request.getRemark());
payloadManifest.put("includeUpgradeHistory", request != null && Boolean.TRUE.equals(request.getIncludeUpgradeHistory()));
payloadManifest.put("includeAuditBackups", request != null && Boolean.TRUE.equals(request.getIncludeAuditBackups()));
List<Map<String, Object>> resourceEntries = new ArrayList<>();
int collectedCount = 0;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
for (DiscoveredResourceItem item : discoveredItems) {
Map<String, Object> resourceEntry = new LinkedHashMap<>();
resourceEntry.put("resourceType", item.getResourceType());
resourceEntry.put("resourceCode", item.getResourceCode());
resourceEntry.put("resolvedPath", item.getResolvedPath());
resourceEntry.put("required", item.isRequired());
int collectedFiles = collectResourceItem(zipOutputStream, item, resourceEntry);
if (collectedFiles > 0) {
collectedCount++;
}
resourceEntries.add(resourceEntry);
}
payloadManifest.put("resources", resourceEntries);
addZipEntry(
zipOutputStream,
"payload-manifest.json",
objectMapper.writeValueAsBytes(payloadManifest)
);
}
return new PayloadArchiveBuildResult(outputStream.toByteArray(), collectedCount);
}
private int collectResourceItem(
ZipOutputStream zipOutputStream,
DiscoveredResourceItem item,
Map<String, Object> resourceEntry
) throws IOException {
if (!"FILE".equalsIgnoreCase(trim(item.getResourceType()))) {
resourceEntry.put("collected", Boolean.FALSE);
resourceEntry.put("skipReason", "RESOURCE_TYPE_NOT_SUPPORTED_YET");
return 0;
}
Path resolvedPath = toResolvedPath(item.getResolvedPath());
if (resolvedPath == null || !Files.exists(resolvedPath)) {
resourceEntry.put("collected", Boolean.FALSE);
resourceEntry.put("skipReason", "PATH_NOT_FOUND");
return 0;
}
List<Map<String, Object>> archivedFiles = new ArrayList<>();
if (Files.isRegularFile(resolvedPath)) {
archivedFiles.add(writeRegularFileEntry(zipOutputStream, item, resolvedPath, resolvedPath.getFileName().toString()));
} else if (Files.isDirectory(resolvedPath)) {
// 目录资源恢复时真正需要的是内部文件集合,而不是目录本身的绝对路径。
// 这里把目录拍平成 zip entry,并把相对路径写进 payload-manifest,后续恢复端按 manifest 还原。
try (Stream<Path> pathStream = Files.walk(resolvedPath)) {
pathStream
.filter(Files::isRegularFile)
.sorted(Comparator.naturalOrder())
.forEach(path -> archivedFiles.add(writeUnchecked(
() -> writeRegularFileEntry(
zipOutputStream,
item,
path,
resolvedPath.relativize(path).toString()
)
)));
}
} else {
resourceEntry.put("collected", Boolean.FALSE);
resourceEntry.put("skipReason", "UNSUPPORTED_FILE_TYPE");
return 0;
}
resourceEntry.put("collected", !archivedFiles.isEmpty());
resourceEntry.put("fileCount", archivedFiles.size());
resourceEntry.put("files", archivedFiles);
if (archivedFiles.size() == 1) {
resourceEntry.put("entryPath", archivedFiles.get(0).get("entryPath"));
}
return archivedFiles.size();
}
private Map<String, Object> writeRegularFileEntry(
ZipOutputStream zipOutputStream,
DiscoveredResourceItem item,
Path file,
String relativeName
) throws IOException {
byte[] content = Files.readAllBytes(file);
String entryPath = buildArchiveEntryPath(item, relativeName);
addZipEntry(zipOutputStream, entryPath, content);
Map<String, Object> fileEntry = new LinkedHashMap<>();
fileEntry.put("entryPath", entryPath);
fileEntry.put("sourcePath", file.toString());
fileEntry.put("relativeName", relativeName.replace('\\', '/'));
fileEntry.put("size", content.length);
fileEntry.put("contentSm3", sm3Hex(content));
return fileEntry;
}
private Map<String, Object> readPayloadManifest(byte[] payloadArchiveBytes) throws IOException {
try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(payloadArchiveBytes))) {
ZipEntry entry;
while ((entry = zipInputStream.getNextEntry()) != null) {
if ("payload-manifest.json".equals(entry.getName())) {
return objectMapper.readValue(zipInputStream.readAllBytes(), new TypeReference<HashMap<String, Object>>() {
});
}
}
}
throw new IllegalStateException("payload archive entry missing: payload-manifest.json");
}
private static String buildArchiveEntryPath(DiscoveredResourceItem item, String relativeName) {
String code = trim(item.getResourceCode()).isEmpty() ? "UNKNOWN" : trim(item.getResourceCode());
String normalizedRelativeName = trim(relativeName).replace('\\', '/');
if (normalizedRelativeName.isEmpty()) {
normalizedRelativeName = "content.bin";
}
return "resources/" + code + "/" + normalizedRelativeName;
}
private static Path toResolvedPath(String path) {
String value = trim(path);
if (value.isEmpty()) {
return null;
}
return Path.of(value).normalize();
}
private static <T> T writeUnchecked(CheckedSupplier<T> supplier) {
try {
return supplier.get();
} catch (IOException ex) {
throw new IllegalStateException("failed to write payload archive entry", ex);
}
}
private static void addZipEntry(ZipOutputStream zipOutputStream, String entryName, byte[] content) throws IOException {
zipOutputStream.putNextEntry(new ZipEntry(entryName));
zipOutputStream.write(content);
zipOutputStream.closeEntry();
}
private static byte[] readRequiredEntry(ZipFile zipFile, String entryName) throws IOException {
ZipEntry entry = zipFile.getEntry(entryName);
if (entry == null) {
throw new IllegalStateException("backup package entry missing: " + entryName);
}
try (InputStream inputStream = zipFile.getInputStream(entry)) {
return inputStream.readAllBytes();
}
}
private static String sm3Hex(byte[] data) {
SM3Digest digest = new SM3Digest();
digest.update(data, 0, data.length);
byte[] out = new byte[digest.getDigestSize()];
digest.doFinal(out, 0);
StringBuilder builder = new StringBuilder(out.length * 2);
for (byte item : out) {
builder.append(String.format("%02x", item));
}
return builder.toString();
}
private static boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
private static String trim(String value) {
return value == null ? "" : value.trim();
}
@FunctionalInterface
private interface CheckedSupplier<T> {
T get() throws IOException;
}
private record PayloadArchiveBuildResult(byte[] archiveBytes, int collectedResourceCount) {
}
}
@@ -0,0 +1,11 @@
package com.cisd.tms.modules.backup.repository;
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
import java.util.Optional;
public interface ResourceBackupTaskRepository {
void save(ResourceBackupTaskEntity entity);
Optional<ResourceBackupTaskEntity> findByTaskId(String taskId);
}
@@ -0,0 +1,16 @@
package com.cisd.tms.modules.backup.repository;
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
import java.util.List;
import java.util.Optional;
public interface ResourceRestoreTaskRepository {
void save(ResourceRestoreTaskEntity entity);
void update(ResourceRestoreTaskEntity entity);
Optional<ResourceRestoreTaskEntity> findByTaskId(String taskId);
List<ResourceRestoreTaskEntity> findAll();
}
@@ -0,0 +1,11 @@
package com.cisd.tms.modules.backup.repository;
import com.cisd.tms.modules.backup.entity.ResourceTaskStepEntity;
import java.util.List;
public interface ResourceTaskStepRepository {
void save(ResourceTaskStepEntity entity);
List<ResourceTaskStepEntity> findByTaskIdOrderByStepNo(String taskId);
}
@@ -0,0 +1,27 @@
package com.cisd.tms.modules.backup.repository.impl;
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
import com.cisd.tms.modules.backup.mapper.ResourceBackupTaskMapper;
import com.cisd.tms.modules.backup.repository.ResourceBackupTaskRepository;
import java.util.Optional;
import org.springframework.stereotype.Repository;
@Repository
public class ResourceBackupTaskRepositoryImpl implements ResourceBackupTaskRepository {
private final ResourceBackupTaskMapper resourceBackupTaskMapper;
public ResourceBackupTaskRepositoryImpl(ResourceBackupTaskMapper resourceBackupTaskMapper) {
this.resourceBackupTaskMapper = resourceBackupTaskMapper;
}
@Override
public void save(ResourceBackupTaskEntity entity) {
resourceBackupTaskMapper.insert(entity);
}
@Override
public Optional<ResourceBackupTaskEntity> findByTaskId(String taskId) {
return Optional.ofNullable(resourceBackupTaskMapper.selectByTaskId(taskId));
}
}
@@ -0,0 +1,39 @@
package com.cisd.tms.modules.backup.repository.impl;
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
import com.cisd.tms.modules.backup.mapper.ResourceRestoreTaskMapper;
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
@Repository
public class ResourceRestoreTaskRepositoryImpl implements ResourceRestoreTaskRepository {
private final ResourceRestoreTaskMapper resourceRestoreTaskMapper;
public ResourceRestoreTaskRepositoryImpl(ResourceRestoreTaskMapper resourceRestoreTaskMapper) {
this.resourceRestoreTaskMapper = resourceRestoreTaskMapper;
}
@Override
public void save(ResourceRestoreTaskEntity entity) {
resourceRestoreTaskMapper.insert(entity);
}
@Override
public void update(ResourceRestoreTaskEntity entity) {
resourceRestoreTaskMapper.updateById(entity);
}
@Override
public Optional<ResourceRestoreTaskEntity> findByTaskId(String taskId) {
return Optional.ofNullable(resourceRestoreTaskMapper.selectByTaskId(taskId));
}
@Override
public List<ResourceRestoreTaskEntity> findAll() {
List<ResourceRestoreTaskEntity> tasks = resourceRestoreTaskMapper.selectList(null);
return tasks == null ? List.of() : tasks;
}
}
@@ -0,0 +1,28 @@
package com.cisd.tms.modules.backup.repository.impl;
import com.cisd.tms.modules.backup.entity.ResourceTaskStepEntity;
import com.cisd.tms.modules.backup.mapper.ResourceTaskStepMapper;
import com.cisd.tms.modules.backup.repository.ResourceTaskStepRepository;
import java.util.List;
import org.springframework.stereotype.Repository;
@Repository
public class ResourceTaskStepRepositoryImpl implements ResourceTaskStepRepository {
private final ResourceTaskStepMapper resourceTaskStepMapper;
public ResourceTaskStepRepositoryImpl(ResourceTaskStepMapper resourceTaskStepMapper) {
this.resourceTaskStepMapper = resourceTaskStepMapper;
}
@Override
public void save(ResourceTaskStepEntity entity) {
resourceTaskStepMapper.insert(entity);
}
@Override
public List<ResourceTaskStepEntity> findByTaskIdOrderByStepNo(String taskId) {
List<ResourceTaskStepEntity> steps = resourceTaskStepMapper.selectByTaskIdOrderByStepNo(taskId);
return steps == null ? List.of() : steps;
}
}
@@ -0,0 +1,30 @@
package com.cisd.tms.modules.backup.runner;
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
import java.nio.file.Path;
import org.springframework.stereotype.Component;
@Component
public class AsyncResourceRestoreRunnerLauncher implements ResourceRestoreRunnerLauncher {
private final ResourceRestoreRunner resourceRestoreRunner;
private final ResourceBackupProperties resourceBackupProperties;
public AsyncResourceRestoreRunnerLauncher(
ResourceRestoreRunner resourceRestoreRunner,
ResourceBackupProperties resourceBackupProperties
) {
this.resourceRestoreRunner = resourceRestoreRunner;
this.resourceBackupProperties = resourceBackupProperties;
}
@Override
public void launch(Path handoffFile) {
if (!resourceBackupProperties.getRunnerAutoLaunchEnabled()) {
return;
}
Thread thread = new Thread(() -> resourceRestoreRunner.run(handoffFile), "resource-restore-runner");
thread.setDaemon(true);
thread.start();
}
}
@@ -0,0 +1,143 @@
package com.cisd.tms.modules.backup.runner;
import com.cisd.tms.modules.backup.packagex.ResourcePackageInspectResult;
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
import com.cisd.tms.modules.backup.runner.model.ResourceRestoreHandoffSnapshot;
import com.cisd.tms.modules.backup.support.file.HandoffFileStore;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.springframework.stereotype.Component;
@Component
public class PayloadArchiveRunnerStepExecutor implements RunnerStepExecutor {
private final HandoffFileStore handoffFileStore;
private final ResourcePackageService resourcePackageService;
private final ObjectMapper objectMapper;
public PayloadArchiveRunnerStepExecutor(
HandoffFileStore handoffFileStore,
ResourcePackageService resourcePackageService,
ObjectMapper objectMapper
) {
this.handoffFileStore = handoffFileStore;
this.resourcePackageService = resourcePackageService;
this.objectMapper = objectMapper;
}
@Override
public boolean supports(String stepCode) {
return "RESTORE_FILES".equals(stepCode) || "CONSISTENCY_VERIFY".equals(stepCode);
}
@Override
public RunnerStepExecutionResult execute(RunnerStepContext context) {
if ("RESTORE_FILES".equals(context.stepCode())) {
return restoreFiles(context);
}
if ("CONSISTENCY_VERIFY".equals(context.stepCode())) {
return verifyExtractedPayload(context);
}
throw new IllegalArgumentException("unsupported runner step: " + context.stepCode());
}
private RunnerStepExecutionResult restoreFiles(RunnerStepContext context) {
ResourceRestoreHandoffSnapshot handoff = readRequiredHandoff(context.handoffFile());
Path packagePath = resolveRequiredPackagePath(handoff);
ResourcePackageInspectResult inspectResult = resourcePackageService.inspectPackage(packagePath);
byte[] payloadArchiveBytes = Optional.ofNullable(inspectResult.getPayloadArchiveBytes())
.orElseThrow(() -> new IllegalStateException("payload archive bytes missing"));
Path payloadDir = context.taskDir().resolve("staging").resolve("payload").normalize();
extractPayloadArchive(payloadArchiveBytes, payloadDir);
int resourceCount = readResourceCount(payloadDir.resolve("payload-manifest.json"));
return RunnerStepExecutionResult.success("payload extracted to staging, resourceCount=" + resourceCount);
}
private RunnerStepExecutionResult verifyExtractedPayload(RunnerStepContext context) {
Path manifestFile = context.taskDir().resolve("staging").resolve("payload").resolve("payload-manifest.json").normalize();
if (!Files.isRegularFile(manifestFile)) {
throw new IllegalStateException("payload manifest missing: " + manifestFile);
}
int resourceCount = readResourceCount(manifestFile);
return RunnerStepExecutionResult.success("payload manifest verified, resourceCount=" + resourceCount);
}
private ResourceRestoreHandoffSnapshot readRequiredHandoff(Path handoffFile) {
return handoffFileStore.read(handoffFile)
.orElseThrow(() -> new IllegalStateException("restore handoff file not found: " + handoffFile));
}
private Path resolveRequiredPackagePath(ResourceRestoreHandoffSnapshot handoff) {
String packagePath = trim(handoff.getPackagePath());
if (packagePath.isEmpty()) {
throw new IllegalStateException("restore handoff packagePath is empty");
}
Path resolved = Path.of(packagePath).normalize();
if (!Files.isRegularFile(resolved)) {
throw new IllegalStateException("restore package file not found: " + resolved);
}
return resolved;
}
private void extractPayloadArchive(byte[] payloadArchiveBytes, Path payloadDir) {
try {
Files.createDirectories(payloadDir);
try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(payloadArchiveBytes))) {
ZipEntry entry;
while ((entry = zipInputStream.getNextEntry()) != null) {
Path target = resolveSafePayloadTarget(payloadDir, entry.getName());
if (entry.isDirectory()) {
Files.createDirectories(target);
continue;
}
Files.createDirectories(target.getParent());
// 备份包来自外部输入,即使已经验签,也要防御 zip-slip,确保只落到 staging/payload 下。
Files.write(target, readAllBytes(zipInputStream));
}
}
} catch (IOException ex) {
throw new IllegalStateException("failed to extract payload archive", ex);
}
}
private Path resolveSafePayloadTarget(Path payloadDir, String entryName) {
Path target = payloadDir.resolve(entryName).normalize();
if (!target.startsWith(payloadDir)) {
throw new IllegalStateException("payload archive entry escapes staging dir: " + entryName);
}
return target;
}
private int readResourceCount(Path manifestFile) {
try {
Map<String, Object> manifest = objectMapper.readValue(manifestFile.toFile(), new TypeReference<Map<String, Object>>() {
});
Object resourcesValue = manifest.get("resources");
if (resourcesValue instanceof List<?> resources) {
return resources.size();
}
return 0;
} catch (IOException ex) {
throw new IllegalStateException("failed to read payload manifest: " + manifestFile, ex);
}
}
private static byte[] readAllBytes(InputStream inputStream) throws IOException {
return inputStream.readAllBytes();
}
private static String trim(String value) {
return value == null ? "" : value.trim();
}
}
@@ -0,0 +1,36 @@
package com.cisd.tms.modules.backup.runner;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.springframework.stereotype.Component;
@Component
public class ProcessBuilderRunnerCommandInvoker implements RunnerCommandInvoker {
@Override
public int run(List<String> command, Path logFile, Path workingDirectory) {
try {
if (logFile != null && logFile.getParent() != null) {
Files.createDirectories(logFile.getParent());
}
ProcessBuilder pb = new ProcessBuilder(command);
if (workingDirectory != null) {
Files.createDirectories(workingDirectory);
pb.directory(workingDirectory.toFile());
}
pb.redirectErrorStream(true);
if (logFile != null) {
pb.redirectOutput(ProcessBuilder.Redirect.appendTo(logFile.toFile()));
}
Process process = pb.start();
return process.waitFor();
} catch (IOException ex) {
throw new IllegalStateException("failed to start runner command: " + String.join(" ", command), ex);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException("runner command interrupted: " + String.join(" ", command), ex);
}
}
}
@@ -0,0 +1,156 @@
package com.cisd.tms.modules.backup.runner;
import com.cisd.tms.modules.backup.runner.model.ResourceRestoreHandoffSnapshot;
import com.cisd.tms.modules.backup.support.file.HandoffFileStore;
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.springframework.stereotype.Component;
@Component
public class ResourceRestoreRunner {
private static final String INITIAL_STEP_CODE = "RESTORE_LOCK";
private static final int INITIAL_STEP_NO = 1;
private final HandoffFileStore handoffFileStore;
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
private final ObjectMapper objectMapper;
private final List<RunnerStepExecutor> runnerStepExecutors;
public ResourceRestoreRunner(
HandoffFileStore handoffFileStore,
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
ObjectMapper objectMapper,
List<RunnerStepExecutor> runnerStepExecutors
) {
this.handoffFileStore = handoffFileStore;
this.resourceRestoreStateFileStore = resourceRestoreStateFileStore;
this.objectMapper = objectMapper;
this.runnerStepExecutors = runnerStepExecutors;
}
public void run(Path handoffFile) {
ResourceRestoreHandoffSnapshot handoff = handoffFileStore.read(handoffFile)
.orElseThrow(() -> new IllegalStateException("restore handoff file not found: " + handoffFile));
Path stateFile = Path.of(handoff.getRunnerStatePath()).normalize();
String runnerInstanceId = nextRunnerInstanceId();
long runnerPid = ProcessHandle.current().pid();
resourceRestoreStateFileStore.claimOwnership(stateFile, runnerInstanceId, runnerPid, INITIAL_STEP_CODE, INITIAL_STEP_NO);
List<String> restorePlan = readRestorePlan(handoff.getRestorePlanJson());
try {
for (int i = 0; i < restorePlan.size(); i++) {
String stepCode = restorePlan.get(i);
int stepNo = i + 1;
if (INITIAL_STEP_CODE.equals(stepCode)) {
continue;
}
resourceRestoreStateFileStore.markStepRunning(stateFile, stepCode, stepNo, runnerInstanceId);
RunnerStepExecutor executor = resolveExecutor(stepCode);
RunnerStepContext context = new RunnerStepContext(
handoff.getTaskId(),
stepCode,
stepNo,
handoffFile.getParent(),
handoffFile,
stateFile
);
RunnerStepExecutionResult result = executor.execute(context);
writeStepOutput(context, result);
resourceRestoreStateFileStore.touchHeartbeat(stateFile, runnerInstanceId);
}
String lastStepCode = restorePlan.isEmpty() ? INITIAL_STEP_CODE : restorePlan.get(restorePlan.size() - 1);
int lastStepNo = restorePlan.isEmpty() ? INITIAL_STEP_NO : restorePlan.size();
resourceRestoreStateFileStore.markReconcilePending(stateFile, "SUCCESS", null, null, lastStepCode, lastStepNo, runnerInstanceId);
} catch (RuntimeException ex) {
int failedStepNo = detectFailedStepNo(handoff, stateFile);
String failedStepCode = detectFailedStepCode(handoff, stateFile);
writeStepFailureOutput(handoffFile.getParent(), failedStepCode, failedStepNo, ex);
resourceRestoreStateFileStore.markReconcilePending(
stateFile,
"FAILED",
"RUNNER_EXECUTION_FAILED",
ex.getMessage(),
failedStepCode,
failedStepNo,
runnerInstanceId
);
throw ex;
}
}
private String nextRunnerInstanceId() {
return "RRUN-" + UUID.randomUUID();
}
private List<String> readRestorePlan(String restorePlanJson) {
if (restorePlanJson == null || restorePlanJson.isBlank()) {
return List.of(INITIAL_STEP_CODE);
}
try {
return objectMapper.readValue(restorePlanJson, new TypeReference<List<String>>() {
});
} catch (Exception ex) {
throw new IllegalStateException("failed to parse restore plan json", ex);
}
}
private RunnerStepExecutor resolveExecutor(String stepCode) {
return runnerStepExecutors.stream()
.filter(executor -> executor.supports(stepCode))
.findFirst()
.orElseThrow(() -> new IllegalStateException("no runner step executor for step: " + stepCode));
}
private int detectFailedStepNo(ResourceRestoreHandoffSnapshot handoff, Path stateFile) {
return resourceRestoreStateFileStore.read(stateFile)
.map(snapshot -> snapshot.getCurrentStepNo() == null ? INITIAL_STEP_NO : snapshot.getCurrentStepNo())
.orElse(INITIAL_STEP_NO);
}
private String detectFailedStepCode(ResourceRestoreHandoffSnapshot handoff, Path stateFile) {
return resourceRestoreStateFileStore.read(stateFile)
.map(snapshot -> snapshot.getCurrentStepCode() == null || snapshot.getCurrentStepCode().isBlank()
? INITIAL_STEP_CODE
: snapshot.getCurrentStepCode())
.orElse(INITIAL_STEP_CODE);
}
private void writeStepOutput(RunnerStepContext context, RunnerStepExecutionResult result) {
try {
Path outputDir = context.taskDir().resolve("step-output");
Files.createDirectories(outputDir);
Path outputFile = outputDir.resolve(String.format("%02d-%s.json", context.stepNo(), context.stepCode()));
Map<String, Object> json = new LinkedHashMap<>();
json.put("stepCode", context.stepCode());
json.put("result", result.getResult());
json.put("summary", result.getSummary());
objectMapper.writeValue(outputFile.toFile(), json);
} catch (IOException ex) {
throw new IllegalStateException("failed to write step output for " + context.stepCode(), ex);
}
}
private void writeStepFailureOutput(Path taskDir, String stepCode, int stepNo, RuntimeException ex) {
try {
Path outputDir = taskDir.resolve("step-output");
Files.createDirectories(outputDir);
Path outputFile = outputDir.resolve(String.format("%02d-%s.json", stepNo, stepCode));
Map<String, Object> json = new LinkedHashMap<>();
json.put("stepCode", stepCode);
json.put("result", "FAILED");
json.put("summary", ex.getMessage() == null ? ex.getClass().getSimpleName() : ex.getMessage());
objectMapper.writeValue(outputFile.toFile(), json);
} catch (IOException ioException) {
throw new IllegalStateException("failed to write failed step output for " + stepCode, ioException);
}
}
}
@@ -0,0 +1,8 @@
package com.cisd.tms.modules.backup.runner;
import java.nio.file.Path;
public interface ResourceRestoreRunnerLauncher {
void launch(Path handoffFile);
}
@@ -0,0 +1,58 @@
package com.cisd.tms.modules.backup.runner;
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
import java.time.LocalDateTime;
import java.util.function.Supplier;
import org.springframework.stereotype.Component;
@Component
public class RestoreStateReconciler {
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
private final Supplier<LocalDateTime> nowSupplier;
public RestoreStateReconciler(
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
ResourceRestoreStateFileStore resourceRestoreStateFileStore
) {
this(resourceRestoreTaskRepository, resourceRestoreStateFileStore, LocalDateTime::now);
}
RestoreStateReconciler(
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
Supplier<LocalDateTime> nowSupplier
) {
this.resourceRestoreTaskRepository = resourceRestoreTaskRepository;
this.resourceRestoreStateFileStore = resourceRestoreStateFileStore;
this.nowSupplier = nowSupplier;
}
public void reconcileOnce() {
for (ResourceRestoreTaskEntity entity : resourceRestoreTaskRepository.findAll()) {
if (entity.getRunnerStatePath() == null || entity.getRunnerStatePath().isBlank()) {
continue;
}
java.nio.file.Path stateFile = java.nio.file.Path.of(entity.getRunnerStatePath()).normalize();
resourceRestoreStateFileStore.read(stateFile).ifPresent(snapshot -> {
if (!"RECONCILE_PENDING".equals(snapshot.getControlPhase())) {
return;
}
entity.setStatus(snapshot.getStatus());
entity.setErrorCode(blankToNull(snapshot.getErrorCode()));
entity.setErrorMessage(blankToNull(snapshot.getErrorMessage()));
entity.setFinishTime(nowSupplier.get());
entity.setUpdateTime(nowSupplier.get());
resourceRestoreTaskRepository.update(entity);
resourceRestoreStateFileStore.markDone(stateFile);
});
}
}
private static String blankToNull(String value) {
return value == null || value.isBlank() ? null : value;
}
}
@@ -0,0 +1,9 @@
package com.cisd.tms.modules.backup.runner;
import java.nio.file.Path;
import java.util.List;
public interface RunnerCommandInvoker {
int run(List<String> command, Path logFile, Path workingDirectory);
}
@@ -0,0 +1,62 @@
package com.cisd.tms.modules.backup.runner;
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
import java.nio.file.Path;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.function.Supplier;
import org.springframework.stereotype.Component;
@Component
public class RunnerStallDetector {
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
private final ResourceBackupProperties resourceBackupProperties;
private final Supplier<LocalDateTime> nowSupplier;
public RunnerStallDetector(
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
ResourceBackupProperties resourceBackupProperties
) {
this(resourceRestoreTaskRepository, resourceRestoreStateFileStore, resourceBackupProperties, LocalDateTime::now);
}
RunnerStallDetector(
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
ResourceBackupProperties resourceBackupProperties,
Supplier<LocalDateTime> nowSupplier
) {
this.resourceRestoreTaskRepository = resourceRestoreTaskRepository;
this.resourceRestoreStateFileStore = resourceRestoreStateFileStore;
this.resourceBackupProperties = resourceBackupProperties;
this.nowSupplier = nowSupplier;
}
public void detectOnce() {
for (ResourceRestoreTaskEntity entity : resourceRestoreTaskRepository.findAll()) {
if (entity.getRunnerStatePath() == null || entity.getRunnerStatePath().isBlank()) {
continue;
}
Path stateFile = Path.of(entity.getRunnerStatePath()).normalize();
resourceRestoreStateFileStore.read(stateFile).ifPresent(snapshot -> {
if (!"RUNNER_OWNED".equals(snapshot.getControlPhase())) {
return;
}
if (snapshot.getLastHeartbeatAt() == null || snapshot.getLastHeartbeatAt().isBlank()) {
return;
}
LocalDateTime heartbeat = LocalDateTime.parse(snapshot.getLastHeartbeatAt());
if (Duration.between(heartbeat, nowSupplier.get()).getSeconds() <= resourceBackupProperties.getRunnerHeartbeatTimeoutSeconds()) {
return;
}
resourceRestoreStateFileStore.markStalled(stateFile, "RUNNER_STALLED", "restore runner heartbeat timed out");
});
}
}
}
@@ -0,0 +1,13 @@
package com.cisd.tms.modules.backup.runner;
import java.nio.file.Path;
public record RunnerStepContext(
String taskId,
String stepCode,
int stepNo,
Path taskDir,
Path handoffFile,
Path stateFile
) {
}
@@ -0,0 +1,37 @@
package com.cisd.tms.modules.backup.runner;
public class RunnerStepExecutionResult {
private String result;
private String summary;
public static RunnerStepExecutionResult success(String summary) {
RunnerStepExecutionResult result = new RunnerStepExecutionResult();
result.setResult("SUCCESS");
result.setSummary(summary);
return result;
}
public static RunnerStepExecutionResult failed(String summary) {
RunnerStepExecutionResult result = new RunnerStepExecutionResult();
result.setResult("FAILED");
result.setSummary(summary);
return result;
}
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
public String getSummary() {
return summary;
}
public void setSummary(String summary) {
this.summary = summary;
}
}
@@ -0,0 +1,8 @@
package com.cisd.tms.modules.backup.runner;
public interface RunnerStepExecutor {
boolean supports(String stepCode);
RunnerStepExecutionResult execute(RunnerStepContext context);
}
@@ -0,0 +1,82 @@
package com.cisd.tms.modules.backup.runner;
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
import com.cisd.tms.modules.init.config.InitExecutorProperties;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Component;
@Component
public class ServiceCommandRunnerStepExecutor implements RunnerStepExecutor {
private final ResourceBackupProperties resourceBackupProperties;
private final InitExecutorProperties initExecutorProperties;
private final RunnerCommandInvoker runnerCommandInvoker;
public ServiceCommandRunnerStepExecutor(
ResourceBackupProperties resourceBackupProperties,
InitExecutorProperties initExecutorProperties,
RunnerCommandInvoker runnerCommandInvoker
) {
this.resourceBackupProperties = resourceBackupProperties;
this.initExecutorProperties = initExecutorProperties;
this.runnerCommandInvoker = runnerCommandInvoker;
}
@Override
public boolean supports(String stepCode) {
return "STOP_SERVICES".equals(stepCode) || "START_SERVICES".equals(stepCode);
}
@Override
public RunnerStepExecutionResult execute(RunnerStepContext context) {
if (!supports(context.stepCode())) {
throw new IllegalArgumentException("unsupported runner step: " + context.stepCode());
}
List<List<String>> commands = resolveCommands(context.stepCode());
int executedCount = 0;
for (List<String> command : commands) {
int exitCode = runnerCommandInvoker.run(command, resolveLogFile(context), context.taskDir());
if (exitCode != 0) {
throw new IllegalStateException("runner step " + context.stepCode() + " failed, exitCode=" + exitCode);
}
executedCount++;
}
return RunnerStepExecutionResult.success("executed " + executedCount + " command(s)");
}
private List<List<String>> resolveCommands(String stepCode) {
return switch (stepCode) {
case "STOP_SERVICES" -> buildStopCommands();
case "START_SERVICES" -> buildStartCommands();
default -> throw new IllegalArgumentException("unsupported runner step: " + stepCode);
};
}
private List<List<String>> buildStopCommands() {
List<List<String>> commands = new ArrayList<>();
commands.add(List.of("bash", resourceBackupProperties.getTmsScriptPath(), "stop"));
appendShellCommand(commands, initExecutorProperties.getStandardAppStopCommand());
appendShellCommand(commands, initExecutorProperties.getStandardRabbitStopCommand());
return commands;
}
private List<List<String>> buildStartCommands() {
List<List<String>> commands = new ArrayList<>();
appendShellCommand(commands, initExecutorProperties.getStandardAppStartCommand());
commands.add(List.of("bash", resourceBackupProperties.getTmsScriptPath(), "start"));
return commands;
}
private void appendShellCommand(List<List<String>> commands, String shellCommand) {
if (shellCommand == null || shellCommand.isBlank()) {
return;
}
commands.add(List.of("/bin/zsh", "-lc", shellCommand.trim()));
}
private Path resolveLogFile(RunnerStepContext context) {
return context.taskDir().resolve("steps").resolve(String.format("%02d-%s.log", context.stepNo(), context.stepCode()));
}
}
@@ -0,0 +1,77 @@
package com.cisd.tms.modules.backup.runner.model;
public class ResourceRestoreHandoffSnapshot {
private String taskId;
private String precheckId;
private String backupId;
private String packageFileId;
private String packagePath;
private String stagingDir;
private String runnerStatePath;
private String restorePlanJson;
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public String getPrecheckId() {
return precheckId;
}
public void setPrecheckId(String precheckId) {
this.precheckId = precheckId;
}
public String getBackupId() {
return backupId;
}
public void setBackupId(String backupId) {
this.backupId = backupId;
}
public String getPackageFileId() {
return packageFileId;
}
public void setPackageFileId(String packageFileId) {
this.packageFileId = packageFileId;
}
public String getPackagePath() {
return packagePath;
}
public void setPackagePath(String packagePath) {
this.packagePath = packagePath;
}
public String getStagingDir() {
return stagingDir;
}
public void setStagingDir(String stagingDir) {
this.stagingDir = stagingDir;
}
public String getRunnerStatePath() {
return runnerStatePath;
}
public void setRunnerStatePath(String runnerStatePath) {
this.runnerStatePath = runnerStatePath;
}
public String getRestorePlanJson() {
return restorePlanJson;
}
public void setRestorePlanJson(String restorePlanJson) {
this.restorePlanJson = restorePlanJson;
}
}
@@ -0,0 +1,25 @@
package com.cisd.tms.modules.backup.service;
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
import com.cisd.tms.modules.backup.dto.response.CreateResourceBackupResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceBackupTaskDetailResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceKeysetStatusResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
import com.cisd.tms.modules.backup.support.ResourceBackupDownloadResult;
import java.util.List;
public interface ResourceBackupService {
ResourceKeysetStatusResponse getKeysetStatus();
CreateResourceBackupResponse createBackupTask(CreateResourceBackupRequest request);
ResourceBackupTaskDetailResponse getBackupTask(String taskId);
ResourceBackupDownloadResult downloadBackupPackage(String taskId);
List<ResourceTaskStepItemResponse> getBackupTaskSteps(String taskId);
ResourceTaskStepLogResponse getBackupTaskStepLog(String taskId, int stepNo);
}
@@ -0,0 +1,9 @@
package com.cisd.tms.modules.backup.service;
import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest;
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
public interface ResourcePrecheckService {
ResourceRestorePrecheckResponse precheck(ResourceRestorePrecheckRequest request);
}
@@ -0,0 +1,9 @@
package com.cisd.tms.modules.backup.service;
import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest;
import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
public interface ResourceRestoreService {
CreateResourceRestoreResponse createRestoreTask(CreateResourceRestoreRequest request);
}
@@ -0,0 +1,15 @@
package com.cisd.tms.modules.backup.service;
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
import java.util.List;
public interface ResourceTaskQueryService {
ResourceRestoreTaskDetailResponse getRestoreTask(String taskId);
List<ResourceTaskStepItemResponse> getRestoreTaskSteps(String taskId);
ResourceTaskStepLogResponse getRestoreTaskStepLog(String taskId, int stepNo);
}
@@ -0,0 +1,218 @@
package com.cisd.tms.modules.backup.service.impl;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.backup.discovery.ResourceDiscoveryService;
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
import com.cisd.tms.modules.backup.dto.response.CreateResourceBackupResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceBackupTaskDetailResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceKeysetStatusResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
import com.cisd.tms.modules.backup.packagex.ResourcePackageResult;
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
import com.cisd.tms.modules.backup.repository.ResourceBackupTaskRepository;
import com.cisd.tms.modules.backup.service.ResourceBackupService;
import com.cisd.tms.modules.backup.support.ResourceBackupDownloadResult;
import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
import com.cisd.tms.modules.init.entity.InitTaskEntity;
import com.cisd.tms.modules.init.repository.InitTaskRepository;
import java.net.InetAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
@Service
public class ResourceBackupServiceImpl implements ResourceBackupService {
private final InitTaskRepository initTaskRepository;
private final ResourceDiscoveryService resourceDiscoveryService;
private final ResourcePackageService resourcePackageService;
private final ResourceBackupTaskRepository resourceBackupTaskRepository;
private final ResourceTaskIdGenerator resourceTaskIdGenerator;
private final com.cisd.tms.modules.backup.config.ResourceBackupProperties resourceBackupProperties;
public ResourceBackupServiceImpl(
InitTaskRepository initTaskRepository,
ResourceDiscoveryService resourceDiscoveryService,
ResourcePackageService resourcePackageService,
ResourceBackupTaskRepository resourceBackupTaskRepository,
ResourceTaskIdGenerator resourceTaskIdGenerator,
com.cisd.tms.modules.backup.config.ResourceBackupProperties resourceBackupProperties
) {
this.initTaskRepository = initTaskRepository;
this.resourceDiscoveryService = resourceDiscoveryService;
this.resourcePackageService = resourcePackageService;
this.resourceBackupTaskRepository = resourceBackupTaskRepository;
this.resourceTaskIdGenerator = resourceTaskIdGenerator;
this.resourceBackupProperties = resourceBackupProperties;
}
@Override
public ResourceKeysetStatusResponse getKeysetStatus() {
ResourceKeysetStatusResponse response = new ResourceKeysetStatusResponse();
// 第一版先把接口骨架和本地备份主链打通;真实密钥状态后续接入密码卡/UKey恢复结果。
response.setLmkReady(false);
response.setIkReady(false);
response.setStorageEncKeyReady(false);
response.setAuthenticityKeyReady(false);
response.setStorageEncKeyFingerprint("USER_KEY_ENC:" + resourceBackupProperties.getStorageEncKeyIndex());
response.setAuthenticityKeyFingerprint("USER_KEY_SIGN:" + resourceBackupProperties.getAuthenticitySignKeyIndex());
response.setSource("CONFIGURED_KEY_INDEX");
return response;
}
@Override
public CreateResourceBackupResponse createBackupTask(CreateResourceBackupRequest request) {
// 资源备份必须基于“最近一次成功初始化”快照收集上下文,避免前端重新传一套可能失真的参数。
InitTaskEntity initTask = initTaskRepository.findLatestByTaskTypeAndStatus("INIT", "SUCCESS")
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task is missing"));
String taskId = resourceTaskIdGenerator.nextBackupTaskId();
String backupId = resourceTaskIdGenerator.nextBackupId();
// 先根据初始化快照收敛资源范围,再交给打包服务生成本地可见的 .tmsbak 文件。
ResourcePackageResult packageResult = resourcePackageService.createPackage(
taskId,
backupId,
initTask,
resourceDiscoveryService.discover(initTask),
request
);
LocalDateTime now = LocalDateTime.now();
ResourceBackupTaskEntity entity = new ResourceBackupTaskEntity();
// 第一版先按同步成功模型落库,后续切换为真正异步任务时继续复用这张任务表。
entity.setTaskId(taskId);
entity.setBackupId(backupId);
entity.setStatus("SUCCESS");
entity.setProductType(initTask.getProductType());
entity.setMqType(initTask.getMqType());
entity.setOrgCode(initTask.getOrgCode());
entity.setSourceDeviceId(resolveSourceDeviceId());
entity.setPackageName(packageResult.getPackageName());
entity.setPackagePath(packageResult.getPackagePath());
entity.setPackageSize(packageResult.getPackageSize());
entity.setPackageHash(packageResult.getPackageHash());
entity.setStorageEncKeyFingerprint(packageResult.getStorageEncKeyFingerprint());
entity.setAuthenticityKeyFingerprint(packageResult.getAuthenticityKeyFingerprint());
entity.setManifestSummaryJson(packageResult.getManifestSummaryJson());
entity.setRemark(request == null ? null : request.getRemark());
entity.setCreateBy("SYSTEM");
entity.setCreateTime(now);
entity.setUpdateTime(now);
entity.setStartTime(now);
entity.setFinishTime(now);
resourceBackupTaskRepository.save(entity);
CreateResourceBackupResponse response = new CreateResourceBackupResponse();
response.setTaskId(taskId);
response.setBackupId(backupId);
response.setStatus(entity.getStatus());
response.setCreatedAt(now.toString());
return response;
}
@Override
public ResourceBackupTaskDetailResponse getBackupTask(String taskId) {
ResourceBackupTaskEntity entity = resourceBackupTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup task not found: " + trim(taskId)));
ResourceBackupTaskDetailResponse response = new ResourceBackupTaskDetailResponse();
response.setTaskId(entity.getTaskId());
response.setBackupId(entity.getBackupId());
response.setStatus(entity.getStatus());
response.setProductType(entity.getProductType());
response.setMqType(entity.getMqType());
response.setOrgCode(entity.getOrgCode());
response.setSourceDeviceId(entity.getSourceDeviceId());
response.setPackageName(entity.getPackageName());
response.setPackageSize(entity.getPackageSize());
response.setPackageHash(entity.getPackageHash());
response.setStorageEncKeyFingerprint(entity.getStorageEncKeyFingerprint());
response.setAuthenticityKeyFingerprint(entity.getAuthenticityKeyFingerprint());
response.setManifestSummary(entity.getManifestSummaryJson());
response.setRemark(entity.getRemark());
response.setErrorCode(entity.getErrorCode());
response.setErrorMessage(entity.getErrorMessage());
response.setCreatedBy(entity.getCreateBy());
response.setCreatedAt(entity.getCreateTime() == null ? null : entity.getCreateTime().toString());
response.setStartedAt(entity.getStartTime() == null ? null : entity.getStartTime().toString());
response.setFinishedAt(entity.getFinishTime() == null ? null : entity.getFinishTime().toString());
return response;
}
@Override
public ResourceBackupDownloadResult downloadBackupPackage(String taskId) {
ResourceBackupTaskEntity entity = resourceBackupTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup task not found: " + trim(taskId)));
Path packagePath = Path.of(trim(entity.getPackagePath())).normalize().toAbsolutePath();
if (trim(entity.getPackagePath()).isEmpty() || !Files.exists(packagePath) || !Files.isRegularFile(packagePath)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup package file not found");
}
try {
ResourceBackupDownloadResult result = new ResourceBackupDownloadResult();
result.setFilename(trim(entity.getPackageName()).isEmpty() ? packagePath.getFileName().toString() : entity.getPackageName());
result.setPackagePath(packagePath);
result.setContentLength(Files.size(packagePath));
return result;
} catch (Exception ex) {
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to read resource backup package");
}
}
@Override
public List<ResourceTaskStepItemResponse> getBackupTaskSteps(String taskId) {
ResourceBackupTaskEntity entity = resourceBackupTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup task not found: " + trim(taskId)));
ResourceTaskStepItemResponse item = new ResourceTaskStepItemResponse();
item.setStepNo(1);
item.setStepCode("PACKAGE_CREATE");
item.setStatus(entity.getStatus());
item.setMessage("generated backup package " + trim(entity.getPackageName()));
item.setStartedAt(entity.getStartTime() == null ? null : entity.getStartTime().toString());
item.setFinishedAt(entity.getFinishTime() == null ? null : entity.getFinishTime().toString());
item.setLogAvailable(Boolean.FALSE);
return List.of(item);
}
@Override
public ResourceTaskStepLogResponse getBackupTaskStepLog(String taskId, int stepNo) {
ResourceBackupTaskEntity entity = resourceBackupTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup task not found: " + trim(taskId)));
ResourceTaskStepLogResponse response = new ResourceTaskStepLogResponse();
response.setTaskId(trim(taskId));
response.setStepNo(stepNo);
response.setStepCode("PACKAGE_CREATE");
response.setContent(stepNo == 1 ? objectToJsonLine(Map.of(
"packageName", trim(entity.getPackageName()),
"packagePath", trim(entity.getPackagePath()),
"packageHash", trim(entity.getPackageHash())
)) : "");
response.setTruncated(false);
return response;
}
private static String resolveSourceDeviceId() {
try {
// 先用主机名兜底,后续如有更稳定的设备唯一标识,可在这里统一替换。
return InetAddress.getLocalHost().getHostName();
} catch (Exception ex) {
return "UNKNOWN";
}
}
private static String trim(String value) {
return value == null ? "" : value.trim();
}
private static String objectToJsonLine(Map<String, Object> value) {
try {
return new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(value);
} catch (Exception ex) {
return "{}";
}
}
}
@@ -0,0 +1,124 @@
package com.cisd.tms.modules.backup.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.backup.dto.request.ResourceRestorePrecheckRequest;
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
import com.cisd.tms.modules.backup.packagex.ResourcePackageInspectResult;
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
import com.cisd.tms.modules.backup.service.ResourcePrecheckService;
import com.cisd.tms.modules.backup.support.ResourcePrecheckContext;
import com.cisd.tms.modules.backup.support.ResourcePrecheckStateStore;
import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
import com.cisd.tms.modules.file.entity.FileRecordEntity;
import com.cisd.tms.modules.file.repository.FileRecordRepository;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class ResourcePrecheckServiceImpl implements ResourcePrecheckService {
private final FileRecordRepository fileRecordRepository;
private final ResourcePackageService resourcePackageService;
private final ResourcePrecheckStateStore resourcePrecheckStateStore;
private final ResourceTaskIdGenerator resourceTaskIdGenerator;
private final CisdPresetProperties cisdPresetProperties;
private final ResourceBackupProperties resourceBackupProperties;
public ResourcePrecheckServiceImpl(
FileRecordRepository fileRecordRepository,
ResourcePackageService resourcePackageService,
ResourcePrecheckStateStore resourcePrecheckStateStore,
ResourceTaskIdGenerator resourceTaskIdGenerator,
CisdPresetProperties cisdPresetProperties,
ResourceBackupProperties resourceBackupProperties
) {
this.fileRecordRepository = fileRecordRepository;
this.resourcePackageService = resourcePackageService;
this.resourcePrecheckStateStore = resourcePrecheckStateStore;
this.resourceTaskIdGenerator = resourceTaskIdGenerator;
this.cisdPresetProperties = cisdPresetProperties;
this.resourceBackupProperties = resourceBackupProperties;
}
@Override
public ResourceRestorePrecheckResponse precheck(ResourceRestorePrecheckRequest request) {
FileRecordEntity fileRecord = fileRecordRepository.findByFileId(trim(request.getFileId()))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "backup package file not found: " + trim(request.getFileId())));
// 预检阶段只解析包头、验签和比对指纹,不提前解密完整 payload。
ResourcePackageInspectResult inspectResult = resourcePackageService.inspectPackageHeader(Path.of(fileRecord.getStoragePath()));
String storageFingerprint = stringValue(inspectResult.getManifest().get("storageEncKeyFingerprint"));
String authenticityFingerprint = stringValue(inspectResult.getManifest().get("authenticityKeyFingerprint"));
if (!expectedStorageFingerprint().equals(storageFingerprint) || !expectedAuthenticityFingerprint().equals(authenticityFingerprint)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源备份包密钥指纹不匹配");
}
ResourceRestorePrecheckResponse response = new ResourceRestorePrecheckResponse();
response.setPrecheckId(resourceTaskIdGenerator.nextPrecheckId());
response.setBackupId(stringValue(inspectResult.getManifest().get("backupId")));
response.setSourceProductType(stringValue(inspectResult.getManifest().get("productType")));
response.setSourceMqType(stringValue(inspectResult.getManifest().get("mqType")));
response.setSourceOrgCode(stringValue(inspectResult.getManifest().get("orgCode")));
response.setSourceBackupTime(stringValue(inspectResult.getManifest().get("backupTime")));
response.setTargetProductType(defaultProductType());
response.setKeysetMatched(true);
response.setSignatureValid(true);
response.setCompatible(isCompatible(response.getSourceProductType(), response.getTargetProductType()));
response.setPayloadDecrypted(false);
response.setWarnings(List.of());
response.setBlockingIssues(List.of());
response.setRestorePlan(List.of(
"RESTORE_LOCK",
"STOP_SERVICES",
"RESTORE_FILES",
"RESTORE_DATABASES",
"START_SERVICES",
"CONSISTENCY_VERIFY"
));
response.setRestoreScopeSummary(buildScopeSummary(inspectResult));
response.setExpiresAt(LocalDateTime.now().plusMinutes(30).toString());
resourcePrecheckStateStore.save(new ResourcePrecheckContext(request.getFileId(), response));
return response;
}
private String defaultProductType() {
return cisdPresetProperties.getProductType() == null || cisdPresetProperties.getProductType().isBlank()
? "UNKNOWN"
: cisdPresetProperties.getProductType().trim();
}
private static String trim(String value) {
return value == null ? "" : value.trim();
}
private static String stringValue(Object value) {
return value == null ? "" : String.valueOf(value);
}
private String expectedStorageFingerprint() {
return "USER_KEY_ENC:" + resourceBackupProperties.getStorageEncKeyIndex();
}
private String expectedAuthenticityFingerprint() {
return "USER_KEY_SIGN:" + resourceBackupProperties.getAuthenticitySignKeyIndex();
}
private static java.util.Map<String, Object> buildScopeSummary(ResourcePackageInspectResult inspectResult) {
java.util.LinkedHashMap<String, Object> summary = new java.util.LinkedHashMap<>();
summary.put("resourceCount", inspectResult.getManifest().get("resourceCount"));
summary.put("productType", inspectResult.getManifest().get("productType"));
summary.put("mqType", inspectResult.getManifest().get("mqType"));
summary.put("orgCode", inspectResult.getManifest().get("orgCode"));
return summary;
}
private static boolean isCompatible(String sourceProductType, String targetProductType) {
if (targetProductType == null || targetProductType.isBlank() || "UNKNOWN".equalsIgnoreCase(targetProductType)) {
return true;
}
return targetProductType.equalsIgnoreCase(sourceProductType);
}
}
@@ -0,0 +1,157 @@
package com.cisd.tms.modules.backup.service.impl;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest;
import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
import com.cisd.tms.modules.backup.runner.ResourceRestoreRunnerLauncher;
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
import com.cisd.tms.modules.backup.service.ResourceRestoreService;
import com.cisd.tms.modules.backup.support.ResourcePrecheckContext;
import com.cisd.tms.modules.backup.support.ResourcePrecheckStateStore;
import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
import com.cisd.tms.modules.backup.support.file.HandoffFileStore;
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
import com.cisd.tms.modules.file.entity.FileRecordEntity;
import com.cisd.tms.modules.file.repository.FileRecordRepository;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class ResourceRestoreServiceImpl implements ResourceRestoreService {
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
private final FileRecordRepository fileRecordRepository;
private final ResourcePrecheckStateStore resourcePrecheckStateStore;
private final ResourceTaskIdGenerator resourceTaskIdGenerator;
private final HandoffFileStore handoffFileStore;
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
private final ResourceBackupProperties resourceBackupProperties;
private final ResourceRestoreRunnerLauncher resourceRestoreRunnerLauncher;
public ResourceRestoreServiceImpl(
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
FileRecordRepository fileRecordRepository,
ResourcePrecheckStateStore resourcePrecheckStateStore,
ResourceTaskIdGenerator resourceTaskIdGenerator,
HandoffFileStore handoffFileStore,
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
ResourceBackupProperties resourceBackupProperties,
ResourceRestoreRunnerLauncher resourceRestoreRunnerLauncher
) {
this.resourceRestoreTaskRepository = resourceRestoreTaskRepository;
this.fileRecordRepository = fileRecordRepository;
this.resourcePrecheckStateStore = resourcePrecheckStateStore;
this.resourceTaskIdGenerator = resourceTaskIdGenerator;
this.handoffFileStore = handoffFileStore;
this.resourceRestoreStateFileStore = resourceRestoreStateFileStore;
this.resourceBackupProperties = resourceBackupProperties;
this.resourceRestoreRunnerLauncher = resourceRestoreRunnerLauncher;
}
public ResourceRestoreServiceImpl(
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
FileRecordRepository fileRecordRepository,
ResourcePrecheckStateStore resourcePrecheckStateStore,
ResourceTaskIdGenerator resourceTaskIdGenerator,
HandoffFileStore handoffFileStore,
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
ResourceBackupProperties resourceBackupProperties
) {
this(
resourceRestoreTaskRepository,
fileRecordRepository,
resourcePrecheckStateStore,
resourceTaskIdGenerator,
handoffFileStore,
resourceRestoreStateFileStore,
resourceBackupProperties,
handoffFile -> {
}
);
}
@Override
public CreateResourceRestoreResponse createRestoreTask(CreateResourceRestoreRequest request) {
if (!Boolean.TRUE.equals(request.getConfirmRiskAccepted())) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "restore risk must be accepted");
}
// 恢复创建必须消费预检结果,确保“用户确认的 backupId”与预检看到的是同一个包。
ResourcePrecheckContext context = resourcePrecheckStateStore.find(request.getPrecheckId())
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore precheck not found"));
ResourceRestorePrecheckResponse precheck = context.getResponse();
if (!trim(request.getConfirmBackupId()).equals(trim(precheck.getBackupId()))) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "confirmed backup id does not match precheck result");
}
FileRecordEntity packageFile = fileRecordRepository.findByFileId(context.getFileId())
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup package file not found: " + trim(context.getFileId())));
String packagePath = trim(packageFile.getStoragePath());
if (packagePath.isEmpty()) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup package path is empty");
}
String taskId = resourceTaskIdGenerator.nextRestoreTaskId();
LocalDateTime now = LocalDateTime.now();
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
entity.setTaskId(taskId);
entity.setPrecheckId(precheck.getPrecheckId());
entity.setBackupId(precheck.getBackupId());
// 恢复任务创建完成后,就应该已经进入“等待 runner 接管”的交接态,
// 前端和运维看到的状态不能还停留在 PRECHECKED。
entity.setStatus("HANDOFF");
entity.setSourceDeviceId("UNKNOWN");
entity.setSourceProductType(precheck.getSourceProductType());
entity.setTargetProductType(precheck.getTargetProductType());
entity.setPackageFileId(context.getFileId());
Path taskDir = Path.of(resourceBackupProperties.getRestoreTaskRootDir()).resolve(taskId).normalize();
// 恢复 runner 的工作目录和状态文件路径在任务创建时就固定下来,
// 这样后续即使 TMS 重启,控制面路径也不会漂移。
// 这里必须把真实备份包路径写进任务和 handoff,后续 runner 才能真正解包,
// 否则它只能拿到 fileId,却不知道恢复时去哪里取浏览器上传/下载回来的备份文件。
entity.setPackagePath(packagePath);
entity.setStagingDir(taskDir.resolve("staging").toString());
entity.setRunnerStatePath(taskDir.resolve("restore-state.json").toString());
entity.setStorageEncKeyFingerprint("USER_KEY_ENC:" + resourceBackupProperties.getStorageEncKeyIndex());
entity.setAuthenticityKeyFingerprint("USER_KEY_SIGN:" + resourceBackupProperties.getAuthenticitySignKeyIndex());
entity.setRestorePlanJson(joinAsJsonArray(precheck.getRestorePlan()));
entity.setCreateBy("SYSTEM");
entity.setCreateTime(now);
entity.setUpdateTime(now);
resourceRestoreTaskRepository.save(entity);
handoffFileStore.write(taskDir, entity);
resourceRestoreStateFileStore.initialize(taskDir, entity);
resourceRestoreStateFileStore.markHandoffReady(taskDir.resolve("restore-state.json"), "RESTORE_LOCK", 1);
resourceRestoreRunnerLauncher.launch(taskDir.resolve("handoff.json"));
CreateResourceRestoreResponse response = new CreateResourceRestoreResponse();
response.setTaskId(taskId);
response.setStatus(entity.getStatus());
response.setCreatedAt(now.toString());
return response;
}
private static String joinAsJsonArray(List<String> items) {
if (items == null || items.isEmpty()) {
return "[]";
}
// 这里先用最轻量的 JSON 数组序列化,避免为了一个占位字段引入额外 DTO。
StringBuilder builder = new StringBuilder("[");
for (int i = 0; i < items.size(); i++) {
if (i > 0) {
builder.append(',');
}
builder.append('"').append(items.get(i)).append('"');
}
builder.append(']');
return builder.toString();
}
private static String trim(String value) {
return value == null ? "" : value.trim();
}
}
@@ -0,0 +1,228 @@
package com.cisd.tms.modules.backup.service.impl;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
import com.cisd.tms.modules.backup.service.ResourceTaskQueryService;
import com.cisd.tms.modules.backup.support.ResourceTaskStepLogService;
import com.cisd.tms.modules.backup.support.file.FileSystemResourceTaskStepLogService;
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateSnapshot;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Map;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
private final ObjectMapper objectMapper;
private final ResourceTaskStepLogService resourceTaskStepLogService;
public ResourceTaskQueryServiceImpl(
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
ObjectMapper objectMapper
) {
this(resourceRestoreTaskRepository, resourceRestoreStateFileStore, objectMapper, new FileSystemResourceTaskStepLogService());
}
public ResourceTaskQueryServiceImpl(
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
ObjectMapper objectMapper,
ResourceTaskStepLogService resourceTaskStepLogService
) {
this.resourceRestoreTaskRepository = resourceRestoreTaskRepository;
this.resourceRestoreStateFileStore = resourceRestoreStateFileStore;
this.objectMapper = objectMapper;
this.resourceTaskStepLogService = resourceTaskStepLogService;
}
@Override
public ResourceRestoreTaskDetailResponse getRestoreTask(String taskId) {
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore task not found: " + trim(taskId)));
ResourceRestoreTaskDetailResponse response = new ResourceRestoreTaskDetailResponse();
response.setTaskId(entity.getTaskId());
response.setPrecheckId(entity.getPrecheckId());
response.setBackupId(entity.getBackupId());
response.setStatus(entity.getStatus());
response.setControlPhase("PRECHECKED");
response.setStateSource("DB");
response.setSourceDeviceId(entity.getSourceDeviceId());
response.setSourceProductType(entity.getSourceProductType());
response.setTargetProductType(entity.getTargetProductType());
response.setPackageFileId(entity.getPackageFileId());
response.setPackagePathVisible(Boolean.FALSE);
response.setRunnerStatePath(entity.getRunnerStatePath());
response.setRestorePlan(readStringList(entity.getRestorePlanJson()));
response.setConsistencyReport(readObject(entity.getConsistencyReportJson()));
response.setErrorCode(entity.getErrorCode());
response.setErrorMessage(entity.getErrorMessage());
response.setCreatedBy(entity.getCreateBy());
response.setCreatedAt(entity.getCreateTime() == null ? null : entity.getCreateTime().toString());
response.setStartedAt(entity.getStartTime() == null ? null : entity.getStartTime().toString());
response.setFinishedAt(entity.getFinishTime() == null ? null : entity.getFinishTime().toString());
mergeRunnerState(response, entity);
return response;
}
@Override
public List<ResourceTaskStepItemResponse> getRestoreTaskSteps(String taskId) {
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore task not found: " + trim(taskId)));
List<String> restorePlan = readStringList(entity.getRestorePlanJson());
ResourceRestoreStateSnapshot snapshot = resourceRestoreStateFileStore.read(resolveStatePath(entity.getRunnerStatePath())).orElse(null);
Path taskDir = resolveTaskDir(entity);
List<ResourceTaskStepItemResponse> responses = new ArrayList<>();
for (int i = 0; i < restorePlan.size(); i++) {
String stepCode = restorePlan.get(i);
int stepNo = i + 1;
ResourceTaskStepItemResponse item = new ResourceTaskStepItemResponse();
item.setStepNo(stepNo);
item.setStepCode(stepCode);
applyStepState(item, taskDir, snapshot, stepCode, stepNo);
responses.add(item);
}
return responses;
}
@Override
public ResourceTaskStepLogResponse getRestoreTaskStepLog(String taskId, int stepNo) {
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore task not found: " + trim(taskId)));
List<String> restorePlan = readStringList(entity.getRestorePlanJson());
String stepCode = stepNo > 0 && stepNo <= restorePlan.size() ? restorePlan.get(stepNo - 1) : "";
return resourceTaskStepLogService.read(resolveTaskDir(entity), trim(taskId), stepNo, stepCode);
}
private static String trim(String value) {
return value == null ? "" : value.trim();
}
private void mergeRunnerState(ResourceRestoreTaskDetailResponse response, ResourceRestoreTaskEntity entity) {
// 恢复任务详情以数据库记录为基线,再用 runner 状态文件覆盖实时字段。
// 这样即使 runner 尚未接管,前端也能看到稳定的任务基本信息。
resourceRestoreStateFileStore.read(resolveStatePath(entity.getRunnerStatePath()))
.ifPresent(snapshot -> applySnapshot(response, snapshot));
}
private void applySnapshot(ResourceRestoreTaskDetailResponse response, ResourceRestoreStateSnapshot snapshot) {
response.setStatus(defaultIfBlank(snapshot.getStatus(), response.getStatus()));
response.setControlPhase(defaultIfBlank(snapshot.getControlPhase(), response.getControlPhase()));
response.setStateSource(defaultIfBlank(snapshot.getStateSource(), response.getStateSource()));
response.setCurrentStepCode(defaultIfBlank(snapshot.getCurrentStepCode(), response.getCurrentStepCode()));
response.setCurrentStepNo(snapshot.getCurrentStepNo() == null ? response.getCurrentStepNo() : snapshot.getCurrentStepNo());
response.setLastHeartbeatAt(defaultIfBlank(snapshot.getLastHeartbeatAt(), response.getLastHeartbeatAt()));
response.setOrphaned(snapshot.getOrphaned() == null ? response.getOrphaned() : snapshot.getOrphaned());
response.setCleanupWarnings(defaultIfNull(snapshot.getCleanupWarnings(), response.getCleanupWarnings()));
response.setWarnings(defaultIfNull(snapshot.getWarnings(), response.getWarnings()));
response.setBlockingIssues(defaultIfNull(snapshot.getBlockingIssues(), response.getBlockingIssues()));
response.setConsistencyReport(snapshot.getConsistencyReport() == null ? response.getConsistencyReport() : snapshot.getConsistencyReport());
response.setErrorCode(defaultIfBlank(snapshot.getErrorCode(), response.getErrorCode()));
response.setErrorMessage(defaultIfBlank(snapshot.getErrorMessage(), response.getErrorMessage()));
}
private Path resolveStatePath(String runnerStatePath) {
return trim(runnerStatePath).isEmpty() ? null : Path.of(runnerStatePath).normalize();
}
private Path resolveTaskDir(ResourceRestoreTaskEntity entity) {
Path statePath = resolveStatePath(entity.getRunnerStatePath());
return statePath == null ? null : statePath.getParent();
}
private void applyStepState(
ResourceTaskStepItemResponse item,
Path taskDir,
ResourceRestoreStateSnapshot snapshot,
String stepCode,
int stepNo
) {
Map<String, Object> stepOutput = readStepOutput(taskDir, stepNo, stepCode);
item.setLogAvailable(taskDir != null && Files.isRegularFile(taskDir.resolve("steps").resolve(String.format("%02d-%s.log", stepNo, stepCode))));
if (stepOutput != null) {
item.setStatus(stringValue(stepOutput.get("result"), "UNKNOWN"));
item.setMessage(stringValue(stepOutput.get("summary"), ""));
return;
}
if (snapshot != null && stepNo == safeInt(snapshot.getCurrentStepNo())) {
if ("RUNNER_OWNED".equals(snapshot.getControlPhase())) {
item.setStatus("RUNNING");
} else if ("RECONCILE_PENDING".equals(snapshot.getControlPhase()) && "FAILED".equals(snapshot.getStatus())) {
item.setStatus("FAILED");
item.setMessage(defaultIfBlank(snapshot.getErrorMessage(), ""));
} else {
item.setStatus("PENDING");
}
return;
}
item.setStatus("PENDING");
}
private Map<String, Object> readStepOutput(Path taskDir, int stepNo, String stepCode) {
if (taskDir == null) {
return null;
}
Path outputFile = taskDir.resolve("step-output").resolve(String.format("%02d-%s.json", stepNo, stepCode));
if (!Files.isRegularFile(outputFile)) {
return null;
}
try {
return objectMapper.readValue(outputFile.toFile(), new TypeReference<Map<String, Object>>() {
});
} catch (Exception ex) {
throw new IllegalStateException("failed to parse resource task step output", ex);
}
}
private static int safeInt(Integer value) {
return value == null ? -1 : value;
}
private static String stringValue(Object value, String defaultValue) {
return value == null ? defaultValue : String.valueOf(value);
}
private List<String> readStringList(String json) {
if (trim(json).isEmpty()) {
return List.of();
}
try {
return objectMapper.readValue(json, new TypeReference<List<String>>() {
});
} catch (Exception ex) {
throw new IllegalStateException("failed to parse restore plan json", ex);
}
}
private Object readObject(String json) {
if (trim(json).isEmpty()) {
return null;
}
try {
return objectMapper.readValue(json, Object.class);
} catch (Exception ex) {
throw new IllegalStateException("failed to parse consistency report json", ex);
}
}
private static String defaultIfBlank(String candidate, String fallback) {
return trim(candidate).isEmpty() ? fallback : candidate;
}
private static <T> T defaultIfNull(T candidate, T fallback) {
return candidate == null ? fallback : candidate;
}
}
@@ -0,0 +1,25 @@
package com.cisd.tms.modules.backup.support;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.springframework.stereotype.Component;
@Component
public class InMemoryResourcePrecheckStateStore implements ResourcePrecheckStateStore {
private final ConcurrentMap<String, ResourcePrecheckContext> contexts = new ConcurrentHashMap<>();
@Override
public void save(ResourcePrecheckContext context) {
if (context == null || context.getResponse() == null || context.getResponse().getPrecheckId() == null) {
throw new IllegalArgumentException("context with precheckId is required");
}
contexts.put(context.getResponse().getPrecheckId(), context);
}
@Override
public Optional<ResourcePrecheckContext> find(String precheckId) {
return Optional.ofNullable(contexts.get(precheckId));
}
}
@@ -0,0 +1,34 @@
package com.cisd.tms.modules.backup.support;
import java.nio.file.Path;
public class ResourceBackupDownloadResult {
private String filename;
private Path packagePath;
private long contentLength;
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
public Path getPackagePath() {
return packagePath;
}
public void setPackagePath(Path packagePath) {
this.packagePath = packagePath;
}
public long getContentLength() {
return contentLength;
}
public void setContentLength(long contentLength) {
this.contentLength = contentLength;
}
}
@@ -0,0 +1,22 @@
package com.cisd.tms.modules.backup.support;
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
public class ResourcePrecheckContext {
private final String fileId;
private final ResourceRestorePrecheckResponse response;
public ResourcePrecheckContext(String fileId, ResourceRestorePrecheckResponse response) {
this.fileId = fileId;
this.response = response;
}
public String getFileId() {
return fileId;
}
public ResourceRestorePrecheckResponse getResponse() {
return response;
}
}
@@ -0,0 +1,10 @@
package com.cisd.tms.modules.backup.support;
import java.util.Optional;
public interface ResourcePrecheckStateStore {
void save(ResourcePrecheckContext context);
Optional<ResourcePrecheckContext> find(String precheckId);
}
@@ -0,0 +1,34 @@
package com.cisd.tms.modules.backup.support;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.UUID;
import org.springframework.stereotype.Component;
@Component
public class ResourceTaskIdGenerator {
private static final DateTimeFormatter TASK_ID_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private static final DateTimeFormatter BACKUP_ID_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
public String nextBackupTaskId() {
return "RBKP-TASK-" + LocalDateTime.now().format(TASK_ID_FORMATTER) + "-" + randomSuffix();
}
public String nextBackupId() {
return "RBKP-" + LocalDateTime.now().format(BACKUP_ID_FORMATTER) + "-" + randomSuffix();
}
public String nextPrecheckId() {
return "RPRE-" + LocalDateTime.now().format(TASK_ID_FORMATTER) + "-" + randomSuffix();
}
public String nextRestoreTaskId() {
return "RRST-TASK-" + LocalDateTime.now().format(TASK_ID_FORMATTER) + "-" + randomSuffix();
}
private static String randomSuffix() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase(Locale.ROOT);
}
}
@@ -0,0 +1,9 @@
package com.cisd.tms.modules.backup.support;
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
import java.nio.file.Path;
public interface ResourceTaskStepLogService {
ResourceTaskStepLogResponse read(Path taskDir, String taskId, int stepNo, String stepCode);
}
@@ -0,0 +1,67 @@
package com.cisd.tms.modules.backup.support.file;
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
import com.cisd.tms.modules.backup.support.ResourcePrecheckContext;
import com.cisd.tms.modules.backup.support.ResourcePrecheckStateStore;
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.util.Optional;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
@Component
@Primary
public class FileSystemResourcePrecheckStateStore implements ResourcePrecheckStateStore {
private final ResourceBackupProperties resourceBackupProperties;
private final ObjectMapper objectMapper;
public FileSystemResourcePrecheckStateStore(
ResourceBackupProperties resourceBackupProperties,
ObjectMapper objectMapper
) {
this.resourceBackupProperties = resourceBackupProperties;
this.objectMapper = objectMapper;
}
@Override
public void save(ResourcePrecheckContext context) {
if (context == null || context.getResponse() == null || context.getResponse().getPrecheckId() == null) {
throw new IllegalArgumentException("context with precheckId is required");
}
try {
Path file = resolvePath(context.getResponse().getPrecheckId());
Files.createDirectories(file.getParent());
objectMapper.writeValue(file.toFile(), new StoredResourcePrecheckContext(context.getFileId(), context.getResponse()));
} catch (IOException ex) {
throw new IllegalStateException("failed to persist resource precheck context", ex);
}
}
@Override
public Optional<ResourcePrecheckContext> find(String precheckId) {
Path file = resolvePath(precheckId);
if (!Files.exists(file)) {
return Optional.empty();
}
try {
JsonNode root = objectMapper.readTree(file.toFile());
String fileId = root.path("fileId").asText();
ResourceRestorePrecheckResponse response = objectMapper.treeToValue(root.path("response"), ResourceRestorePrecheckResponse.class);
return Optional.of(new ResourcePrecheckContext(fileId, response));
} catch (IOException ex) {
throw new IllegalStateException("failed to load resource precheck context", ex);
}
}
private Path resolvePath(String precheckId) {
return Path.of(resourceBackupProperties.getPrecheckStoreDir()).resolve(precheckId + ".json").normalize();
}
private record StoredResourcePrecheckContext(String fileId, ResourceRestorePrecheckResponse response) {
}
}
@@ -0,0 +1,37 @@
package com.cisd.tms.modules.backup.support.file;
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
import com.cisd.tms.modules.backup.support.ResourceTaskStepLogService;
import java.nio.file.Files;
import java.nio.file.Path;
import org.springframework.stereotype.Component;
@Component
public class FileSystemResourceTaskStepLogService implements ResourceTaskStepLogService {
@Override
public ResourceTaskStepLogResponse read(Path taskDir, String taskId, int stepNo, String stepCode) {
ResourceTaskStepLogResponse response = new ResourceTaskStepLogResponse();
response.setTaskId(taskId);
response.setStepNo(stepNo);
response.setStepCode(stepCode);
if (taskDir == null || stepCode == null || stepCode.isBlank()) {
response.setContent("");
response.setTruncated(false);
return response;
}
Path logFile = taskDir.resolve("steps").resolve(String.format("%02d-%s.log", stepNo, stepCode));
try {
if (!Files.isRegularFile(logFile)) {
response.setContent("");
response.setTruncated(false);
return response;
}
response.setContent(Files.readString(logFile));
response.setTruncated(false);
return response;
} catch (Exception ex) {
throw new IllegalStateException("failed to read resource task step log", ex);
}
}
}
@@ -0,0 +1,52 @@
package com.cisd.tms.modules.backup.support.file;
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
import com.cisd.tms.modules.backup.runner.model.ResourceRestoreHandoffSnapshot;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import org.springframework.stereotype.Component;
@Component
public class HandoffFileStore {
private final ObjectMapper objectMapper;
public HandoffFileStore(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public void write(Path taskDir, ResourceRestoreTaskEntity entity) {
try {
Files.createDirectories(taskDir);
Path file = taskDir.resolve("handoff.json");
Map<String, Object> json = new LinkedHashMap<>();
json.put("taskId", entity.getTaskId());
json.put("precheckId", entity.getPrecheckId());
json.put("backupId", entity.getBackupId());
json.put("packageFileId", entity.getPackageFileId());
json.put("packagePath", entity.getPackagePath());
json.put("stagingDir", entity.getStagingDir());
json.put("runnerStatePath", entity.getRunnerStatePath());
json.put("restorePlanJson", entity.getRestorePlanJson());
objectMapper.writeValue(file.toFile(), json);
} catch (IOException ex) {
throw new IllegalStateException("failed to write restore handoff file", ex);
}
}
public Optional<ResourceRestoreHandoffSnapshot> read(Path handoffFile) {
if (handoffFile == null || !Files.isRegularFile(handoffFile)) {
return Optional.empty();
}
try {
return Optional.ofNullable(objectMapper.readValue(handoffFile.toFile(), ResourceRestoreHandoffSnapshot.class));
} catch (IOException ex) {
throw new IllegalStateException("failed to read restore handoff file", ex);
}
}
}
@@ -0,0 +1,190 @@
package com.cisd.tms.modules.backup.support.file;
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import org.springframework.stereotype.Component;
@Component
public class ResourceRestoreStateFileStore {
private final ObjectMapper objectMapper;
public ResourceRestoreStateFileStore(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public void initialize(Path taskDir, ResourceRestoreTaskEntity entity) {
try {
Files.createDirectories(taskDir);
Map<String, Object> json = new LinkedHashMap<>();
json.put("taskId", entity.getTaskId());
json.put("status", entity.getStatus());
json.put("controlPhase", "PRECHECKED");
json.put("stateSource", "DB");
json.put("runnerInstanceId", "");
json.put("runnerPid", 0);
json.put("ownershipToken", UUID.randomUUID().toString());
json.put("orphaned", false);
objectMapper.writeValue(taskDir.resolve("restore-state.json").toFile(), json);
} catch (IOException ex) {
throw new IllegalStateException("failed to initialize restore state file", ex);
}
}
public java.util.Optional<ResourceRestoreStateSnapshot> read(Path stateFile) {
if (stateFile == null || !Files.isRegularFile(stateFile)) {
return java.util.Optional.empty();
}
try {
return java.util.Optional.ofNullable(objectMapper.readValue(stateFile.toFile(), ResourceRestoreStateSnapshot.class));
} catch (IOException ex) {
throw new IllegalStateException("failed to read restore state file", ex);
}
}
public void markHandoffReady(Path stateFile, String currentStepCode, Integer currentStepNo) {
try {
Map<String, Object> json = readJson(stateFile);
json.put("status", "HANDOFF");
json.put("controlPhase", "HANDOFF_READY");
json.put("stateSource", "DB");
json.put("currentStepCode", currentStepCode);
json.put("currentStepNo", currentStepNo);
json.put("orphaned", false);
objectMapper.writeValue(stateFile.toFile(), json);
} catch (IOException ex) {
throw new IllegalStateException("failed to mark restore state as handoff ready", ex);
}
}
public void claimOwnership(Path stateFile, String runnerInstanceId, long runnerPid, String currentStepCode, Integer currentStepNo) {
try {
Map<String, Object> json = readJson(stateFile);
String controlPhase = String.valueOf(json.getOrDefault("controlPhase", ""));
if (!"HANDOFF_READY".equals(controlPhase)) {
throw new IllegalStateException("restore state is not handoff ready: " + controlPhase);
}
json.put("status", "RUNNING");
json.put("controlPhase", "RUNNER_OWNED");
json.put("stateSource", "RUNNER_FILE");
json.put("runnerInstanceId", runnerInstanceId);
json.put("runnerPid", runnerPid);
json.put("currentStepCode", currentStepCode);
json.put("currentStepNo", currentStepNo);
json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString());
json.put("orphaned", false);
objectMapper.writeValue(stateFile.toFile(), json);
} catch (IOException ex) {
throw new IllegalStateException("failed to claim restore runner ownership", ex);
}
}
public void markStepRunning(Path stateFile, String currentStepCode, Integer currentStepNo, String runnerInstanceId) {
updateOwnedState(stateFile, runnerInstanceId, json -> {
json.put("status", "RUNNING");
json.put("controlPhase", "RUNNER_OWNED");
json.put("stateSource", "RUNNER_FILE");
json.put("currentStepCode", currentStepCode);
json.put("currentStepNo", currentStepNo);
json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString());
}, "failed to mark restore step running");
}
public void touchHeartbeat(Path stateFile, String runnerInstanceId) {
updateOwnedState(stateFile, runnerInstanceId, json ->
json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString()), "failed to touch restore heartbeat");
}
public void markReconcilePending(
Path stateFile,
String status,
String errorCode,
String errorMessage,
String currentStepCode,
Integer currentStepNo,
String runnerInstanceId
) {
updateOwnedState(stateFile, runnerInstanceId, json -> {
json.put("status", status);
json.put("controlPhase", "RECONCILE_PENDING");
json.put("stateSource", "RUNNER_FILE");
json.put("currentStepCode", currentStepCode);
json.put("currentStepNo", currentStepNo);
json.put("errorCode", errorCode == null ? "" : errorCode);
json.put("errorMessage", errorMessage == null ? "" : errorMessage);
json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString());
}, "failed to mark restore reconcile pending");
}
private void updateOwnedState(
Path stateFile,
String runnerInstanceId,
JsonMutator mutator,
String errorMessage
) {
try {
Map<String, Object> json = readJson(stateFile);
String currentRunnerInstanceId = String.valueOf(json.getOrDefault("runnerInstanceId", ""));
if (!runnerInstanceId.equals(currentRunnerInstanceId)) {
throw new IllegalStateException("restore runner ownership changed: " + currentRunnerInstanceId);
}
mutator.accept(json);
objectMapper.writeValue(stateFile.toFile(), json);
} catch (IOException ex) {
throw new IllegalStateException(errorMessage, ex);
}
}
public void markDone(Path stateFile) {
try {
Map<String, Object> json = readJson(stateFile);
json.put("controlPhase", "DONE");
json.put("stateSource", "DB");
json.put("lastHeartbeatAt", LocalDateTime.now().toString());
objectMapper.writeValue(stateFile.toFile(), json);
} catch (IOException ex) {
throw new IllegalStateException("failed to mark restore state done", ex);
}
}
public void markStalled(Path stateFile, String errorCode, String errorMessage) {
try {
Map<String, Object> json = readJson(stateFile);
if (!"RUNNER_OWNED".equals(String.valueOf(json.getOrDefault("controlPhase", "")))) {
return;
}
json.put("status", "FAILED");
json.put("controlPhase", "RECONCILE_PENDING");
json.put("stateSource", "RUNNER_FILE");
json.put("orphaned", true);
json.put("errorCode", errorCode);
json.put("errorMessage", errorMessage);
json.put("lastHeartbeatAt", LocalDateTime.now().toString());
objectMapper.writeValue(stateFile.toFile(), json);
} catch (IOException ex) {
throw new IllegalStateException("failed to mark stalled restore state", ex);
}
}
@FunctionalInterface
private interface JsonMutator {
void accept(Map<String, Object> json);
}
private Map<String, Object> readJson(Path stateFile) throws IOException {
if (stateFile == null || !Files.isRegularFile(stateFile)) {
throw new IllegalStateException("restore state file not found: " + stateFile);
}
return objectMapper.readValue(
stateFile.toFile(),
objectMapper.getTypeFactory().constructMapType(LinkedHashMap.class, String.class, Object.class)
);
}
}
@@ -0,0 +1,160 @@
package com.cisd.tms.modules.backup.support.file;
import java.util.List;
public class ResourceRestoreStateSnapshot {
private String taskId;
private String status;
private String controlPhase;
private String stateSource;
private String runnerInstanceId;
private Long runnerPid;
private String ownershipToken;
private String currentStepCode;
private Integer currentStepNo;
private String lastHeartbeatAt;
private Boolean orphaned;
private List<String> cleanupWarnings;
private List<String> warnings;
private List<String> blockingIssues;
private Object consistencyReport;
private String errorCode;
private String errorMessage;
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getControlPhase() {
return controlPhase;
}
public void setControlPhase(String controlPhase) {
this.controlPhase = controlPhase;
}
public String getStateSource() {
return stateSource;
}
public void setStateSource(String stateSource) {
this.stateSource = stateSource;
}
public String getRunnerInstanceId() {
return runnerInstanceId;
}
public void setRunnerInstanceId(String runnerInstanceId) {
this.runnerInstanceId = runnerInstanceId;
}
public Long getRunnerPid() {
return runnerPid;
}
public void setRunnerPid(Long runnerPid) {
this.runnerPid = runnerPid;
}
public String getOwnershipToken() {
return ownershipToken;
}
public void setOwnershipToken(String ownershipToken) {
this.ownershipToken = ownershipToken;
}
public String getCurrentStepCode() {
return currentStepCode;
}
public void setCurrentStepCode(String currentStepCode) {
this.currentStepCode = currentStepCode;
}
public Integer getCurrentStepNo() {
return currentStepNo;
}
public void setCurrentStepNo(Integer currentStepNo) {
this.currentStepNo = currentStepNo;
}
public String getLastHeartbeatAt() {
return lastHeartbeatAt;
}
public void setLastHeartbeatAt(String lastHeartbeatAt) {
this.lastHeartbeatAt = lastHeartbeatAt;
}
public Boolean getOrphaned() {
return orphaned;
}
public void setOrphaned(Boolean orphaned) {
this.orphaned = orphaned;
}
public List<String> getCleanupWarnings() {
return cleanupWarnings;
}
public void setCleanupWarnings(List<String> cleanupWarnings) {
this.cleanupWarnings = cleanupWarnings;
}
public List<String> getWarnings() {
return warnings;
}
public void setWarnings(List<String> warnings) {
this.warnings = warnings;
}
public List<String> getBlockingIssues() {
return blockingIssues;
}
public void setBlockingIssues(List<String> blockingIssues) {
this.blockingIssues = blockingIssues;
}
public Object getConsistencyReport() {
return consistencyReport;
}
public void setConsistencyReport(Object consistencyReport) {
this.consistencyReport = consistencyReport;
}
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
}
@@ -18,6 +18,8 @@ public class InitExecutorProperties {
private int timeoutSec = 600;
// 所有步骤日志会写入该目录,读取日志接口会做同目录约束校验
private String logDir = "/tmp/tms-init-logs";
// TMS 外置配置目录(资源备份恢复时使用)
private String tmsConfigDir = "/home/tms/config";
// 标准版(企业/间参)安装根目录(统一入口)
private String standardHomeDir = DEFAULT_STANDARD_HOME_DIR;
// 标准版(企业/间参)默认 cpconfig.cfg 路径
@@ -133,6 +135,14 @@ public class InitExecutorProperties {
this.logDir = logDir;
}
public String getTmsConfigDir() {
return tmsConfigDir;
}
public void setTmsConfigDir(String tmsConfigDir) {
this.tmsConfigDir = tmsConfigDir;
}
public String getStandardHomeDir() {
return isBlank(standardHomeDir) ? DEFAULT_STANDARD_HOME_DIR : standardHomeDir.trim();
}
+11
View File
@@ -57,6 +57,8 @@ tms:
timeout-sec: ${TMS_INIT_EXECUTOR_TIMEOUT_SEC:600}
# 初始化步骤日志目录;按 taskId/stepNo 生成日志文件。
log-dir: ${TMS_INIT_EXECUTOR_LOG_DIR:/home/tms/tmp/tms-init-logs}
# TMS 外置配置目录;资源备份恢复会按此目录收集配置。
tms-config-dir: ${TMS_INIT_EXECUTOR_TMS_CONFIG_DIR:/home/tms/config}
# 标准版(企业/间参)安装根目录统一变量。
standard-home-dir: ${TMS_INIT_STANDARD_HOME_DIR:/home/cmep4i}
# 标准版 cpconfig.cfg 路径;默认取 standard-home-dir 下文件。
@@ -179,6 +181,15 @@ tms:
log-dir: ${TMS_UPGRADE_LOG_DIR:/home/tms/tmp/tms-upgrade-logs}
# 升级包验签使用的公钥 PEM 文件路径;为空时升级预检会拒绝通过。
signature-public-key-pem-path: ${TMS_UPGRADE_SIGNATURE_PUBLIC_KEY_PEM_PATH:/home/tms/cert/public_key.pem}
backup:
# 资源备份包输出目录;第一版先在本机生成可见的 .tmsbak 文件。
output-dir: ${TMS_BACKUP_OUTPUT_DIR:/home/tms/tmp/resource-backup-packages}
# 恢复 runner 启停 TMS 时使用的脚本路径。
tms-script-path: ${TMS_BACKUP_TMS_SCRIPT_PATH:/home/tms/scripts/tms.sh}
# 是否在创建恢复任务后自动拉起本机 runner;默认关闭,避免开发环境误触停机恢复链。
runner-auto-launch-enabled: ${TMS_BACKUP_RUNNER_AUTO_LAUNCH_ENABLED:false}
# 恢复 runner 心跳失联超时(秒)。
runner-heartbeat-timeout-seconds: ${TMS_BACKUP_RUNNER_HEARTBEAT_TIMEOUT_SECONDS:300}
mk:
init-identify:
# SDFE_InitIdentify 旧 PIN(Base64 编码);主密钥初始化接口会读取该值。
@@ -51,6 +51,90 @@ CREATE TABLE IF NOT EXISTS tms_init_task_step (
KEY idx_tms_init_task_step_task_id (task_id)
);
-- -----------------------------------------------------------------------------
-- 资源备份与恢复
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_resource_backup_task (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
backup_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
product_type VARCHAR(16) NOT NULL,
mq_type VARCHAR(32) NULL,
org_code VARCHAR(32) NULL,
source_device_id VARCHAR(128) NULL,
package_name VARCHAR(255) NULL,
package_path VARCHAR(1024) NULL,
package_size BIGINT NULL,
package_hash VARCHAR(128) NULL,
storage_enc_key_fingerprint VARCHAR(128) NOT NULL,
authenticity_key_fingerprint VARCHAR(128) NOT NULL,
manifest_summary_json JSON NULL,
error_code VARCHAR(64) NULL,
error_message VARCHAR(1024) NULL,
remark VARCHAR(1024) NULL,
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
start_time DATETIME(3) NULL,
finish_time DATETIME(3) NULL,
UNIQUE KEY uk_tms_resource_backup_task_task_id (task_id),
UNIQUE KEY uk_tms_resource_backup_task_backup_id (backup_id),
KEY idx_tms_resource_backup_task_status (status),
KEY idx_tms_resource_backup_task_create_time (create_time)
);
CREATE TABLE IF NOT EXISTS tms_resource_restore_task (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
precheck_id VARCHAR(64) NOT NULL,
backup_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
source_device_id VARCHAR(128) NULL,
source_product_type VARCHAR(16) NOT NULL,
target_product_type VARCHAR(16) NOT NULL,
package_file_id VARCHAR(64) NOT NULL,
package_path VARCHAR(1024) NULL,
staging_dir VARCHAR(1024) NULL,
runner_state_path VARCHAR(1024) NULL,
storage_enc_key_fingerprint VARCHAR(128) NOT NULL,
authenticity_key_fingerprint VARCHAR(128) NOT NULL,
restore_plan_json JSON NULL,
consistency_report_json JSON NULL,
error_code VARCHAR(64) NULL,
error_message VARCHAR(1024) NULL,
remark VARCHAR(1024) NULL,
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
start_time DATETIME(3) NULL,
finish_time DATETIME(3) NULL,
UNIQUE KEY uk_tms_resource_restore_task_task_id (task_id),
UNIQUE KEY uk_tms_resource_restore_task_precheck_id (precheck_id),
KEY idx_tms_resource_restore_task_status (status),
KEY idx_tms_resource_restore_task_create_time (create_time)
);
CREATE TABLE IF NOT EXISTS tms_resource_task_step (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
task_type VARCHAR(32) NOT NULL COMMENT 'RESOURCE_BACKUP / RESOURCE_RESTORE',
step_no INT NOT NULL,
step_code VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
message VARCHAR(512) NULL,
log_path VARCHAR(1024) NULL,
exit_code INT NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
start_time DATETIME(3) NULL,
finish_time DATETIME(3) NULL,
UNIQUE KEY uk_tms_resource_task_step_task_no (task_id, step_no),
KEY idx_tms_resource_task_step_task_id (task_id),
KEY idx_tms_resource_task_step_task_type (task_type)
);
-- -----------------------------------------------------------------------------
-- 认证与设备基础表
-- -----------------------------------------------------------------------------
@@ -365,4 +449,4 @@ CREATE TABLE IF NOT EXISTS tms_access_whitelist (
mask VARCHAR(10) NOT NULL,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
);
);
@@ -0,0 +1,83 @@
-- -----------------------------------------------------------------------------
-- Resource backup and restore tables
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_resource_backup_task (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
backup_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
product_type VARCHAR(16) NOT NULL,
mq_type VARCHAR(32) NULL,
org_code VARCHAR(32) NULL,
source_device_id VARCHAR(128) NULL,
package_name VARCHAR(255) NULL,
package_path VARCHAR(1024) NULL,
package_size BIGINT NULL,
package_hash VARCHAR(128) NULL,
storage_enc_key_fingerprint VARCHAR(128) NOT NULL,
authenticity_key_fingerprint VARCHAR(128) NOT NULL,
manifest_summary_json JSON NULL,
error_code VARCHAR(64) NULL,
error_message VARCHAR(1024) NULL,
remark VARCHAR(1024) NULL,
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
start_time DATETIME(3) NULL,
finish_time DATETIME(3) NULL,
UNIQUE KEY uk_tms_resource_backup_task_task_id (task_id),
UNIQUE KEY uk_tms_resource_backup_task_backup_id (backup_id),
KEY idx_tms_resource_backup_task_status (status),
KEY idx_tms_resource_backup_task_create_time (create_time)
);
CREATE TABLE IF NOT EXISTS tms_resource_restore_task (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
precheck_id VARCHAR(64) NOT NULL,
backup_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
source_device_id VARCHAR(128) NULL,
source_product_type VARCHAR(16) NOT NULL,
target_product_type VARCHAR(16) NOT NULL,
package_file_id VARCHAR(64) NOT NULL,
package_path VARCHAR(1024) NULL,
staging_dir VARCHAR(1024) NULL,
runner_state_path VARCHAR(1024) NULL,
storage_enc_key_fingerprint VARCHAR(128) NOT NULL,
authenticity_key_fingerprint VARCHAR(128) NOT NULL,
restore_plan_json JSON NULL,
consistency_report_json JSON NULL,
error_code VARCHAR(64) NULL,
error_message VARCHAR(1024) NULL,
remark VARCHAR(1024) NULL,
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
start_time DATETIME(3) NULL,
finish_time DATETIME(3) NULL,
UNIQUE KEY uk_tms_resource_restore_task_task_id (task_id),
UNIQUE KEY uk_tms_resource_restore_task_precheck_id (precheck_id),
KEY idx_tms_resource_restore_task_status (status),
KEY idx_tms_resource_restore_task_create_time (create_time)
);
CREATE TABLE IF NOT EXISTS tms_resource_task_step (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
task_type VARCHAR(32) NOT NULL COMMENT 'RESOURCE_BACKUP / RESOURCE_RESTORE',
step_no INT NOT NULL,
step_code VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
message VARCHAR(512) NULL,
log_path VARCHAR(1024) NULL,
exit_code INT NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
start_time DATETIME(3) NULL,
finish_time DATETIME(3) NULL,
UNIQUE KEY uk_tms_resource_task_step_task_no (task_id, step_no),
KEY idx_tms_resource_task_step_task_id (task_id),
KEY idx_tms_resource_task_step_task_type (task_type)
);