资源备份流程整理

This commit is contained in:
waner
2026-05-09 11:45:12 +08:00
parent 8229b3e44e
commit 524824bf02
62 changed files with 2856 additions and 1178 deletions
@@ -1,7 +1,7 @@
package com.cisd.tms.modules.backup.config;
import com.cisd.tms.modules.backup.dto.support.BackupResourceKind;
import com.cisd.tms.modules.backup.dto.support.ConfiguredFileBackupResource;
import com.cisd.tms.modules.backup.dto.support.ConfiguredRestoreCommand;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -17,12 +17,21 @@ public class ResourceBackupProperties {
private String restoreDbScriptPath = "/home/tms/bin/resource-restore/restore-db.sh";
private String mqReplayScriptPath = "/home/tms/bin/resource-restore/replay-mq.sh";
private String tmsDatabaseName = "";
private List<String> tmsDbExcludedTables = List.of(
"tms_role_account",
"tms_role_ukey_binding",
"tms_auth_full_account",
"tms_auth_session",
"tms_auth_challenge",
"tms_auth_audit_log",
"tms_operation_audit_log"
);
private String cmepDatabaseName = "CMEP";
private String healthCheckUrl = "http://127.0.0.1:8080/actuator/health";
private int healthCheckTimeoutSeconds = 5;
private int storageEncKeyIndex = 1;
private int authenticitySignKeyIndex = 1;
private String restoreApplyScriptPath = "/home/tms/bin/resource-restore/apply-resource-backup.sh";
private List<ConfiguredRestoreCommand> restoreStopCommands = List.of();
private List<ConfiguredRestoreCommand> restoreStartCommands = List.of();
private List<String> allowedRestoreRoots = List.of(
"/home/tms/config",
"/home/cmep4i",
@@ -103,6 +112,14 @@ public class ResourceBackupProperties {
this.tmsDatabaseName = tmsDatabaseName;
}
public List<String> getTmsDbExcludedTables() {
return tmsDbExcludedTables;
}
public void setTmsDbExcludedTables(List<String> tmsDbExcludedTables) {
this.tmsDbExcludedTables = tmsDbExcludedTables;
}
public String getCmepDatabaseName() {
return cmepDatabaseName;
}
@@ -127,22 +144,6 @@ public class ResourceBackupProperties {
this.healthCheckTimeoutSeconds = healthCheckTimeoutSeconds;
}
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;
}
public String getRestoreApplyScriptPath() {
return restoreApplyScriptPath;
}
@@ -151,6 +152,22 @@ public class ResourceBackupProperties {
this.restoreApplyScriptPath = restoreApplyScriptPath;
}
public List<ConfiguredRestoreCommand> getRestoreStopCommands() {
return restoreStopCommands;
}
public void setRestoreStopCommands(List<ConfiguredRestoreCommand> restoreStopCommands) {
this.restoreStopCommands = restoreStopCommands;
}
public List<ConfiguredRestoreCommand> getRestoreStartCommands() {
return restoreStartCommands;
}
public void setRestoreStartCommands(List<ConfiguredRestoreCommand> restoreStartCommands) {
this.restoreStartCommands = restoreStartCommands;
}
public List<String> getAllowedRestoreRoots() {
return allowedRestoreRoots;
}
@@ -167,33 +184,4 @@ public class ResourceBackupProperties {
this.fileResources = fileResources;
}
public List<ConfiguredFileBackupResource> getDefaultFileResources() {
ConfiguredFileBackupResource tmsConfig = fileResource(
"TMS_CONFIG",
"DIR",
"/home/tms/config",
"/home/tms/config",
true
);
ConfiguredFileBackupResource cpConfig = fileResource("CP_CONFIG", "FILE", "", "", true);
ConfiguredFileBackupResource receiverLicense = fileResource("RECEIVER_LICENSE", "FILE", "", "", true);
return List.of(tmsConfig, cpConfig, receiverLicense);
}
private static ConfiguredFileBackupResource fileResource(
String code,
String mode,
String path,
String restorePath,
boolean required
) {
ConfiguredFileBackupResource resource = new ConfiguredFileBackupResource();
resource.setCode(code);
resource.setKind(BackupResourceKind.FILE.name());
resource.setMode(mode);
resource.setPath(path);
resource.setRestorePath(restorePath);
resource.setRequired(required);
return resource;
}
}
@@ -49,7 +49,7 @@ public class ResourceBackupController {
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, authLevel = AuthLevel.FULL)
@AuditedOperation(module = ModuleCode.BACKUP, action = ActionType.BACKUP, summary = "创建资源备份任务")
@Operation(summary = "创建资源备份任务", description = "受理资源备份请求并返回备份任务号。")
public ApiResponse<CreateResourceBackupResponse> createBackupTask(@Valid @RequestBody CreateResourceBackupRequest request) {
public ApiResponse<CreateResourceBackupResponse> createBackupTask(@Valid @RequestBody(required = false) CreateResourceBackupRequest request) {
return ApiResponse.success(resourceBackupService.createBackupTask(request));
}
@@ -7,10 +7,8 @@ import com.cisd.tms.modules.auth.security.RequireInternalAuth;
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.ResourceRestoreLatestResponse;
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;
@@ -22,7 +20,6 @@ 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 lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
@@ -34,7 +31,7 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/resource-restores")
@Tag(name = "资源恢复", description = "资源恢复预检、任务创建、详情和步骤日志接口")
@Tag(name = "资源恢复", description = "资源恢复预检、任务创建、执行和最近结果接口")
@RequiredArgsConstructor
public class ResourceRestoreController {
@@ -59,35 +56,23 @@ public class ResourceRestoreController {
return ApiResponse.success(resourceRestoreService.createRestoreTask(request));
}
@GetMapping("/{taskId}")
@PostMapping("/{taskId}/apply")
@ReplayProtected
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, authLevel = AuthLevel.FULL)
@Operation(summary = "查询资源恢复任务详情", description = "返回资源恢复任务详情和当前控制阶段。")
public ApiResponse<ResourceRestoreTaskDetailResponse> taskDetail(
@AuditedOperation(module = ModuleCode.BACKUP, action = ActionType.RECOVER, summary = "执行资源恢复任务")
@Operation(summary = "执行资源恢复任务", description = "用户确认停机恢复后,启动后台恢复脚本。")
public ApiResponse<CreateResourceRestoreResponse> applyRestoreTask(
@Parameter(description = "恢复任务号")
@PathVariable String taskId
) {
return ApiResponse.success(resourceTaskQueryService.getRestoreTask(taskId));
return ApiResponse.success(resourceRestoreService.applyRestoreTask(taskId));
}
@GetMapping("/{taskId}/steps")
@GetMapping("/latest")
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, authLevel = AuthLevel.FULL)
@Operation(summary = "查询资源恢复任务步骤", description = "返回资源恢复任务步骤列表。")
public ApiResponse<List<ResourceTaskStepItemResponse>> taskSteps(
@Parameter(description = "恢复任务号")
@PathVariable String taskId
) {
return ApiResponse.success(resourceTaskQueryService.getRestoreTaskSteps(taskId));
@Operation(summary = "查询最近资源恢复结果", description = "TMS 重启后用于恢复页面展示最近一次恢复时间、结果和失败原因。")
public ApiResponse<ResourceRestoreLatestResponse> latestRestore() {
return ApiResponse.success(resourceTaskQueryService.getLatestRestore());
}
@GetMapping("/{taskId}/steps/{stepNo}/log")
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, authLevel = AuthLevel.FULL)
@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,12 @@
package com.cisd.tms.modules.backup.crypto;
public final class ResourceCryptoKeyConstants {
public static final int STORAGE_ENC_KEY_INDEX = 1;
public static final int AUTHENTICITY_SIGN_KEY_INDEX = 1;
public static final String STORAGE_ENC_KEY_FINGERPRINT = "USER_KEY_ENC:1";
public static final String AUTHENTICITY_KEY_FINGERPRINT = "USER_KEY_SIGN:1";
private ResourceCryptoKeyConstants() {
}
}
@@ -1,12 +1,7 @@
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.model.*;
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
import java.security.Security;
import java.util.Arrays;
import java.util.Base64;
@@ -24,7 +19,6 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
private static final String PROVIDER = "BC";
private final PcieCryptoService pcieCryptoService;
private final ResourceBackupProperties resourceBackupProperties;
static {
if (Security.getProvider(PROVIDER) == null) {
@@ -32,12 +26,8 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
}
}
public ResourceEnvelopeCryptoServiceImpl(
PcieCryptoService pcieCryptoService,
ResourceBackupProperties resourceBackupProperties
) {
public ResourceEnvelopeCryptoServiceImpl(PcieCryptoService pcieCryptoService) {
this.pcieCryptoService = pcieCryptoService;
this.resourceBackupProperties = resourceBackupProperties;
}
@Override
@@ -55,7 +45,7 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
// 用户密钥只负责封装随机 DEK;真正的大 payload 仍走对称加密,避免直接把大文件送进 ECC 接口。
EccInternalEncryptRequest encryptRequest = new EccInternalEncryptRequest();
encryptRequest.setKeyIndex(resourceBackupProperties.getStorageEncKeyIndex());
encryptRequest.setKeyIndex(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_INDEX);
encryptRequest.setData(sm4Key);
BackupDataResult wrappedDek = pcieCryptoService.sdfeInternalEncryptEcc(encryptRequest);
@@ -70,8 +60,8 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
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()));
sealedPayload.setStorageEncKeyFingerprint(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_FINGERPRINT);
sealedPayload.setAuthenticityKeyFingerprint(ResourceCryptoKeyConstants.AUTHENTICITY_KEY_FINGERPRINT);
return sealedPayload;
} catch (Exception ex) {
throw new IllegalStateException("资源备份载荷加封失败", ex);
@@ -85,11 +75,11 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
if (dataToSign == null) {
throw new IllegalArgumentException("dataToSign不能为空");
}
EccInternalSignRequest request = new EccInternalSignRequest();
request.setKeyIndex(resourceBackupProperties.getAuthenticitySignKeyIndex());
// 内部签名接口这里统一签 SM3 摘要,避免把整段原文直接交给密码卡并固定后续验签输入语义。
request.setData(sm3(dataToSign));
return normalize(pcieCryptoService.eccInternalSign(request));
UserKeySignRequest request = new UserKeySignRequest();
request.setData(dataToSign);
request.setKeyIndex(ResourceCryptoKeyConstants.AUTHENTICITY_SIGN_KEY_INDEX);
return normalize(pcieCryptoService.userKeySignWithSm3(request));
}
@Override
@@ -101,7 +91,7 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
throw new IllegalArgumentException("signature不能为空");
}
EccInternalVerifyRequest verifyRequest = new EccInternalVerifyRequest();
verifyRequest.setKeyIndex(resourceBackupProperties.getAuthenticitySignKeyIndex());
verifyRequest.setKeyIndex(ResourceCryptoKeyConstants.AUTHENTICITY_SIGN_KEY_INDEX);
verifyRequest.setData(sm3(dataToVerify));
verifyRequest.setSignature(signature);
pcieCryptoService.eccInternalVerify(verifyRequest);
@@ -127,7 +117,7 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
int dekLength = parseInt(envelope.get("dekLength"), 16);
// 先验签,再解数字信封,再解 payload;顺序固定,避免篡改包先进入明文处理路径。
EccInternalDecryptRequest decryptRequest = new EccInternalDecryptRequest();
decryptRequest.setKeyIndex(resourceBackupProperties.getStorageEncKeyIndex());
decryptRequest.setKeyIndex(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_INDEX);
decryptRequest.setCipherBlob(wrappedDek);
decryptRequest.setOutBufferSize(dekLength);
byte[] sm4Key = normalizeAndWipe(pcieCryptoService.sdfeInternalDecryptEcc(decryptRequest));
@@ -193,10 +183,6 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
}
}
private static String buildFingerprint(String prefix, int keyIndex) {
return prefix + ":" + keyIndex;
}
private static void wipe(byte[] bytes) {
if (bytes != null) {
Arrays.fill(bytes, (byte) 0);
@@ -1,19 +1,22 @@
package com.cisd.tms.modules.backup.discovery;
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
import com.cisd.tms.modules.backup.dto.support.ConfiguredFileBackupResource;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
private final InitExecutorProperties initExecutorProperties;
private final ResourceBackupProperties resourceBackupProperties;
public ResourceDiscoveryServiceImpl(InitExecutorProperties initExecutorProperties) {
this.initExecutorProperties = initExecutorProperties;
@Autowired
public ResourceDiscoveryServiceImpl(ResourceBackupProperties resourceBackupProperties) {
this.resourceBackupProperties = resourceBackupProperties == null ? new ResourceBackupProperties() : resourceBackupProperties;
}
@Override
@@ -30,16 +33,15 @@ public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
// 资源发现只决定“应该备份什么”,不在这里触碰文件系统、数据库或密码卡。
// 真实采集放在 ResourcePackageServiceImpl 中完成,便于统一处理必选资源缺失和包内 manifest。
List<DiscoveredResourceItem> items = new ArrayList<>();
// TMS 自身配置目录是恢复后服务能否按原部署参数启动的基础资源,按目录递归打包。
items.add(file("TMS_CONFIG", initExecutorProperties.getTmsConfigDir(), initExecutorProperties.getTmsConfigDir(), "DIR", true));
// 收发器配置和 license 按产品形态切换路径:标准版走 cmep4i,直连版走 direct 目标路径。
items.add(file("CP_CONFIG", resolveCpConfigPath(productType), resolveCpConfigPath(productType), "FILE", true));
items.add(file("RECEIVER_LICENSE", resolveReceiverLicensePath(productType), resolveReceiverLicensePath(productType), "FILE", true));
appendConfiguredFileResources(items, productType, mqType);
// 数据库资源在包内固定成 db/*.sql,恢复脚本通过 restore-databases.tsv 再决定导入目标库名。
items.add(database("TMS_DB", "TMS", "db/TMS.sql", true));
if (!"DIRECT".equals(productType)) {
// 第一版标准版迁移需要把收发器业务库一起带走,DIRECT 暂时不要求这份库。
items.add(database("CMEP_DB", "CMEP", "db/CMEP.sql", true));
// 标准收发器初始化还会创建一个以机构号命名的业务库;这份库和 CMEP 一样属于不可重建业务数据。
String orgCode = requireDatabaseIdentifier(initTask.getOrgCode(), "orgCode");
items.add(database("ORG_DB", orgCode, "db/" + orgCode + ".sql", true));
}
// 实体证书表记录的 keyIdx 对应密码卡内用户签名密钥;备份时会按这些索引逐个调用 backupUserKey。
items.add(userKey(true));
@@ -48,18 +50,35 @@ public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
return items;
}
private String resolveCpConfigPath(String productType) {
if ("DIRECT".equals(productType)) {
return initExecutorProperties.getDirectCpconfigTargetPath();
private void appendConfiguredFileResources(List<DiscoveredResourceItem> items, String productType, String mqType) {
List<ConfiguredFileBackupResource> resources = resourceBackupProperties.getFileResources();
if (resources == null || resources.isEmpty()) {
return;
}
return initExecutorProperties.getStandardCpconfigPath();
}
private String resolveReceiverLicensePath(String productType) {
if ("DIRECT".equals(productType)) {
return initExecutorProperties.getDirectReceiverLicenseTargetPath();
for (ConfiguredFileBackupResource resource : resources) {
if (!matches(resource.getProductTypes(), productType) || !matches(resource.getMqTypes(), mqType)) {
continue;
}
String code = trim(resource.getCode());
String path = trim(resource.getPath());
if (code.isEmpty() || path.isEmpty()) {
continue;
}
String mode = trim(resource.getMode()).isEmpty() ? "FILE" : trim(resource.getMode()).toUpperCase();
String restorePath = trim(resource.getRestorePath());
if (restorePath.isEmpty()) {
restorePath = "GLOB".equals(mode) ? globRestoreDir(path) : path;
}
DiscoveredResourceItem item = file(
code,
path,
restorePath,
mode,
resource.isRequired()
);
removeExistingCode(items, code);
items.add(item);
}
return initExecutorProperties.getStandardReceiverLicenseTargetPath();
}
private static DiscoveredResourceItem file(String code, String path, String restorePath, String mode, boolean required) {
@@ -73,6 +92,29 @@ public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
return item;
}
private static String globRestoreDir(String pattern) {
String value = trim(pattern);
int slash = Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\'));
if (slash <= 0) {
return ".";
}
return value.substring(0, slash);
}
private static void removeExistingCode(List<DiscoveredResourceItem> items, String code) {
items.removeIf(item -> code.equals(item.getResourceCode()));
}
private static boolean matches(List<String> configuredValues, String actualValue) {
if (configuredValues == null || configuredValues.isEmpty()) {
return true;
}
String actual = upper(actualValue);
return configuredValues.stream()
.map(ResourceDiscoveryServiceImpl::upper)
.anyMatch(actual::equals);
}
private static DiscoveredResourceItem database(String code, String databaseName, String entryPath, boolean required) {
DiscoveredResourceItem item = new DiscoveredResourceItem();
item.setResourceType("DATABASE");
@@ -104,4 +146,19 @@ public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
private static String upper(String value) {
return value == null ? "" : value.trim().toUpperCase();
}
private static String requireDatabaseIdentifier(String value, String label) {
String identifier = trim(value);
if (identifier.isEmpty()) {
throw new IllegalArgumentException(label + "不能为空");
}
if (!identifier.matches("[A-Za-z0-9_]+")) {
throw new IllegalArgumentException(label + "数据库名无效:" + identifier);
}
return identifier;
}
private static String trim(String value) {
return value == null ? "" : value.trim();
}
}
@@ -2,34 +2,12 @@ package com.cisd.tms.modules.backup.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "创建资源备份任务请求")
@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;
}
@@ -1,12 +1,17 @@
package com.cisd.tms.modules.backup.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
@Schema(description = "创建资源恢复任务请求")
public class CreateResourceRestoreRequest {
private static final String IPV4_REGEX =
"^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}$";
@NotBlank
@Schema(description = "预检号", example = "RPRE-20260408-000001", requiredMode = Schema.RequiredMode.REQUIRED)
private String precheckId;
@@ -15,9 +20,15 @@ public class CreateResourceRestoreRequest {
@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;
@NotBlank(message = "deployMode不能为空")
@Pattern(regexp = "SINGLE|DUAL|QUAD", message = "deployMode必须为SINGLE/DUAL/QUAD")
@Schema(description = "目标部署方式", example = "DUAL", requiredMode = Schema.RequiredMode.REQUIRED)
private String deployMode;
@Valid
@NotNull(message = "nodes不能为空")
@Schema(description = "目标节点 IP 配置", requiredMode = Schema.RequiredMode.REQUIRED)
private Nodes nodes;
public String getPrecheckId() {
return precheckId;
@@ -35,11 +46,72 @@ public class CreateResourceRestoreRequest {
this.confirmBackupId = confirmBackupId;
}
public Boolean getConfirmRiskAccepted() {
return confirmRiskAccepted;
public String getDeployMode() {
return deployMode;
}
public void setConfirmRiskAccepted(Boolean confirmRiskAccepted) {
this.confirmRiskAccepted = confirmRiskAccepted;
public void setDeployMode(String deployMode) {
this.deployMode = deployMode;
}
public Nodes getNodes() {
return nodes;
}
public void setNodes(Nodes nodes) {
this.nodes = nodes;
}
@Schema(description = "目标节点 IP 配置")
public static class Nodes {
@NotBlank(message = "nodes.node01Ip不能为空")
@Pattern(regexp = IPV4_REGEX, message = "nodes.node01Ip必须是有效IPv4地址")
@Schema(description = "节点 1 IP", example = "192.168.1.11", requiredMode = Schema.RequiredMode.REQUIRED)
private String node01Ip;
@Pattern(regexp = IPV4_REGEX, message = "nodes.node02Ip必须是有效IPv4地址")
@Schema(description = "节点 2 IP", example = "192.168.1.12")
private String node02Ip;
@Pattern(regexp = IPV4_REGEX, message = "nodes.node03Ip必须是有效IPv4地址")
@Schema(description = "节点 3 IP", example = "192.168.1.13")
private String node03Ip;
@Pattern(regexp = IPV4_REGEX, message = "nodes.node04Ip必须是有效IPv4地址")
@Schema(description = "节点 4 IP", example = "192.168.1.14")
private String node04Ip;
public String getNode01Ip() {
return node01Ip;
}
public void setNode01Ip(String node01Ip) {
this.node01Ip = node01Ip;
}
public String getNode02Ip() {
return node02Ip;
}
public void setNode02Ip(String node02Ip) {
this.node02Ip = node02Ip;
}
public String getNode03Ip() {
return node03Ip;
}
public void setNode03Ip(String node03Ip) {
this.node03Ip = node03Ip;
}
public String getNode04Ip() {
return node04Ip;
}
public void setNode04Ip(String node04Ip) {
this.node04Ip = node04Ip;
}
}
}
@@ -0,0 +1,71 @@
package com.cisd.tms.modules.backup.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "最近一次资源恢复结果响应")
public class ResourceRestoreLatestResponse {
private String taskId;
private String backupId;
private String status;
private String phase;
private String message;
private String createdAt;
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 getPhase() {
return phase;
}
public void setPhase(String phase) {
this.phase = phase;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public String getFinishedAt() {
return finishedAt;
}
public void setFinishedAt(String finishedAt) {
this.finishedAt = finishedAt;
}
}
@@ -16,6 +16,7 @@ public class ResourceRestorePrecheckResponse {
private Boolean keysetMatched;
private Boolean signatureValid;
private Boolean compatible;
private Boolean precheckPassed;
private Boolean payloadDecrypted;
private List<String> warnings;
private List<String> blockingIssues;
@@ -103,6 +104,14 @@ public class ResourceRestorePrecheckResponse {
this.compatible = compatible;
}
public Boolean getPrecheckPassed() {
return precheckPassed;
}
public void setPrecheckPassed(Boolean precheckPassed) {
this.precheckPassed = precheckPassed;
}
public Boolean getPayloadDecrypted() {
return payloadDecrypted;
}
@@ -1,261 +0,0 @@
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 statusFilePath;
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 getStatusFilePath() {
return statusFilePath;
}
public void setStatusFilePath(String statusFilePath) {
this.statusFilePath = statusFilePath;
}
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;
}
}
@@ -1,5 +1,7 @@
package com.cisd.tms.modules.backup.dto.support;
import java.util.List;
public class ConfiguredFileBackupResource {
private String code;
@@ -7,6 +9,8 @@ public class ConfiguredFileBackupResource {
private String mode = "FILE";
private String path;
private String restorePath;
private List<String> productTypes = List.of();
private List<String> mqTypes = List.of();
private boolean required = true;
public String getCode() {
@@ -49,6 +53,22 @@ public class ConfiguredFileBackupResource {
this.restorePath = restorePath;
}
public java.util.List<String> getProductTypes() {
return productTypes;
}
public void setProductTypes(java.util.List<String> productTypes) {
this.productTypes = productTypes;
}
public java.util.List<String> getMqTypes() {
return mqTypes;
}
public void setMqTypes(java.util.List<String> mqTypes) {
this.mqTypes = mqTypes;
}
public boolean isRequired() {
return required;
}
@@ -0,0 +1,43 @@
package com.cisd.tms.modules.backup.dto.support;
import java.util.List;
public class ConfiguredRestoreCommand {
private String code;
private String command;
private boolean required = true;
private List<String> productTypes = List.of();
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getCommand() {
return command;
}
public void setCommand(String command) {
this.command = command;
}
public boolean isRequired() {
return required;
}
public void setRequired(boolean required) {
this.required = required;
}
public List<String> getProductTypes() {
return productTypes;
}
public void setProductTypes(List<String> productTypes) {
this.productTypes = productTypes;
}
}
@@ -1,100 +0,0 @@
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;
}
}
@@ -1,11 +0,0 @@
package com.cisd.tms.modules.backup.enums;
/**
* 恢复任务控制阶段。
*/
public enum ResourceControlPhase {
TMS_PRECHECK,
JAVA_EXTRACT,
SHELL_APPLY,
DONE
}
@@ -1,13 +0,0 @@
package com.cisd.tms.modules.backup.enums;
/**
* 资源任务步骤状态。
*/
public enum ResourceStepStatus {
PENDING,
RUNNING,
SUCCESS,
SUCCESS_WITH_WARNINGS,
FAILED,
SKIPPED
}
@@ -1,16 +0,0 @@
package com.cisd.tms.modules.backup.enums;
/**
* 资源备份/恢复任务主状态。
*/
public enum ResourceTaskStatus {
PENDING,
PRECHECKING,
PRECHECK_FAILED,
READY,
APPLYING,
VERIFYING,
SUCCESS,
FAILED,
CANCELLED
}
@@ -1,9 +0,0 @@
package com.cisd.tms.modules.backup.enums;
/**
* 资源任务类型。
*/
public enum ResourceTaskType {
RESOURCE_BACKUP,
RESOURCE_RESTORE
}
@@ -1,19 +0,0 @@
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);
}
@@ -6,6 +6,8 @@ public interface DatabaseBackupCollector {
CollectedDatabaseDump collectCmepDatabaseDump();
CollectedDatabaseDump collectDatabaseDump(String databaseName, String fileName);
class CollectedDatabaseDump {
private String databaseName;
@@ -34,7 +34,7 @@ public class MysqlDatabaseBackupCollector implements DatabaseBackupCollector {
@Override
public CollectedDatabaseDump collectTmsDatabaseDump() {
MysqlConnectionTarget target = resolveTarget(resourceBackupProperties.getTmsDatabaseName(), "TMS");
return collectDatabaseDump(target, "TMS.sql");
return collectDatabaseDump(target, "TMS.sql", resourceBackupProperties.getTmsDbExcludedTables());
}
@Override
@@ -43,7 +43,21 @@ public class MysqlDatabaseBackupCollector implements DatabaseBackupCollector {
return collectDatabaseDump(target, "CMEP.sql");
}
@Override
public CollectedDatabaseDump collectDatabaseDump(String databaseName, String fileName) {
MysqlConnectionTarget target = resolveTarget(databaseName, databaseName);
return collectDatabaseDump(target, fileName);
}
private CollectedDatabaseDump collectDatabaseDump(MysqlConnectionTarget target, String fileName) {
return collectDatabaseDump(target, fileName, List.of());
}
private CollectedDatabaseDump collectDatabaseDump(
MysqlConnectionTarget target,
String fileName,
List<String> excludedTables
) {
List<String> command = new ArrayList<>();
command.add(resourceBackupProperties.getMysqldumpPath());
command.add("--host=" + target.host());
@@ -52,8 +66,14 @@ public class MysqlDatabaseBackupCollector implements DatabaseBackupCollector {
if (!target.password().isEmpty()) {
command.add("--password=" + target.password());
}
command.add("--single-transaction");
// command.add("--single-transaction");
command.add("--skip-lock-tables");
for (String table : excludedTables == null ? List.<String>of() : excludedTables) {
String ignoreTable = normalizeIgnoreTable(target.databaseName(), table);
if (!ignoreTable.isEmpty()) {
command.add("--ignore-table=" + ignoreTable);
}
}
command.add("--databases");
command.add(target.databaseName());
@@ -126,6 +146,17 @@ public class MysqlDatabaseBackupCollector implements DatabaseBackupCollector {
return value == null ? "" : value.trim();
}
private static String normalizeIgnoreTable(String databaseName, String table) {
String value = trim(table);
if (value.isEmpty()) {
return "";
}
if (value.contains(".")) {
return value;
}
return trim(databaseName) + "." + value;
}
private record MysqlConnectionTarget(String host, int port, String username, String password, String databaseName) {
}
}
@@ -14,8 +14,10 @@ import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.LinkedHashMap;
@@ -271,8 +273,6 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
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;
@@ -332,6 +332,9 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
resourceEntry.put("mode", trim(item.getMode()).isEmpty() ? "FILE" : trim(item.getMode()));
resourceEntry.put("sourcePath", item.getResolvedPath());
resourceEntry.put("restorePath", trim(item.getRestorePath()).isEmpty() ? item.getResolvedPath() : item.getRestorePath());
if ("GLOB".equalsIgnoreCase(trim(item.getMode()))) {
return collectGlobFileItem(zipOutputStream, item, resourceEntry);
}
// 文件类资源在备份端只允许采集真实存在的文件/目录;恢复端还会再次校验 restorePath 白名单。
Path resolvedPath = toResolvedPath(item.getResolvedPath());
if (resolvedPath == null || !Files.exists(resolvedPath)) {
@@ -376,6 +379,51 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
return archivedFiles.size();
}
private int collectGlobFileItem(
ZipOutputStream zipOutputStream,
DiscoveredResourceItem item,
Map<String, Object> resourceEntry
) throws IOException {
Path globParent = globParent(item.getResolvedPath());
if (globParent == null || !Files.isDirectory(globParent)) {
assertOptionalResource(item, "PATH_NOT_FOUND");
resourceEntry.put("collected", Boolean.FALSE);
resourceEntry.put("skipReason", "PATH_NOT_FOUND");
return 0;
}
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + trim(item.getResolvedPath()));
List<Path> matchedFiles;
try (Stream<Path> pathStream = Files.list(globParent)) {
matchedFiles = pathStream
.filter(Files::isRegularFile)
.filter(path -> matcher.matches(path.normalize()))
.sorted(Comparator.naturalOrder())
.toList();
}
if (matchedFiles.isEmpty()) {
assertOptionalResource(item, "GLOB_NOT_MATCHED");
resourceEntry.put("collected", Boolean.FALSE);
resourceEntry.put("skipReason", "GLOB_NOT_MATCHED");
return 0;
}
List<Map<String, Object>> archivedFiles = new ArrayList<>();
for (Path matchedFile : matchedFiles) {
Map<String, Object> fileEntry = writeRegularFileEntry(
zipOutputStream,
item,
matchedFile,
matchedFile.getFileName().toString()
);
String restoreDir = trim(item.getRestorePath()).isEmpty() ? globParent.toString() : trim(item.getRestorePath());
fileEntry.put("restorePath", Path.of(restoreDir).resolve(matchedFile.getFileName()).normalize().toString());
archivedFiles.add(fileEntry);
}
resourceEntry.put("collected", Boolean.TRUE);
resourceEntry.put("fileCount", archivedFiles.size());
resourceEntry.put("files", archivedFiles);
return archivedFiles.size();
}
private int collectDatabaseItem(
ZipOutputStream zipOutputStream,
DiscoveredResourceItem item,
@@ -466,6 +514,16 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
if ("CMEP_DB".equalsIgnoreCase(resourceCode) || "CMEP_DB_DUMP".equalsIgnoreCase(resourceCode)) {
return databaseBackupCollector.collectCmepDatabaseDump();
}
if ("ORG_DB".equalsIgnoreCase(resourceCode) || "ORG_DB_DUMP".equalsIgnoreCase(resourceCode)) {
String databaseName = trim(item.getDatabaseName()).isEmpty() ? trim(item.getResolvedPath()) : trim(item.getDatabaseName());
if (databaseName.isEmpty()) {
return null;
}
String fileName = trim(item.getEntryPath()).isEmpty()
? databaseName + ".sql"
: Path.of(trim(item.getEntryPath())).getFileName().toString();
return databaseBackupCollector.collectDatabaseDump(databaseName, fileName);
}
return null;
}
@@ -531,6 +589,18 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
return Path.of(value).normalize();
}
private static Path globParent(String pattern) {
String value = trim(pattern);
if (value.isEmpty()) {
return null;
}
int slash = Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\'));
if (slash < 0) {
return Path.of(".").normalize();
}
return Path.of(value.substring(0, slash)).normalize();
}
private static <T> T writeUnchecked(CheckedSupplier<T> supplier) {
try {
return supplier.get();
@@ -1,11 +0,0 @@
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);
}
@@ -1,28 +0,0 @@
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,288 @@
package com.cisd.tms.modules.backup.restore;
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
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.Optional;
import org.springframework.stereotype.Component;
@Component
public class ActiveRestoreStateStore {
private static final String ACTIVE_FILE_NAME = "active-restore.json";
private final ResourceBackupProperties properties;
private final ObjectMapper objectMapper;
public ActiveRestoreStateStore(ResourceBackupProperties properties, ObjectMapper objectMapper) {
this.properties = properties;
this.objectMapper = objectMapper;
}
public void write(ResourceRestoreTaskEntity entity, String status) {
ActiveRestoreSnapshot snapshot = fromEntity(entity, status);
Path path = activeFilePath();
try {
Files.createDirectories(path.getParent());
objectMapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), snapshot);
} catch (IOException ex) {
throw new IllegalStateException("写入当前资源恢复状态失败:" + path, ex);
}
}
public Optional<ActiveRestoreSnapshot> read() {
Path path = activeFilePath();
if (!Files.isRegularFile(path)) {
return Optional.empty();
}
try {
return Optional.of(objectMapper.readValue(path.toFile(), ActiveRestoreSnapshot.class));
} catch (IOException ex) {
throw new IllegalStateException("读取当前资源恢复状态失败:" + path, ex);
}
}
private Path activeFilePath() {
return Path.of(properties.getRestoreTaskRootDir()).resolve(ACTIVE_FILE_NAME).normalize();
}
private static ActiveRestoreSnapshot fromEntity(ResourceRestoreTaskEntity entity, String status) {
return new ActiveRestoreSnapshot(
entity.getTaskId(),
entity.getPrecheckId(),
entity.getBackupId(),
status,
entity.getSourceDeviceId(),
entity.getSourceProductType(),
entity.getTargetProductType(),
entity.getPackageFileId(),
entity.getPackagePath(),
entity.getStagingDir(),
entity.getRunnerStatePath(),
entity.getStorageEncKeyFingerprint(),
entity.getAuthenticityKeyFingerprint(),
entity.getRestorePlanJson(),
entity.getCreateBy(),
text(entity.getCreateTime()),
text(entity.getStartTime()),
text(entity.getFinishTime())
);
}
private static String text(LocalDateTime value) {
return value == null ? null : value.toString();
}
public static class ActiveRestoreSnapshot {
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 createBy;
private String createdAt;
private String startedAt;
private String finishedAt;
public ActiveRestoreSnapshot() {
}
public ActiveRestoreSnapshot(
String taskId,
String precheckId,
String backupId,
String status,
String sourceDeviceId,
String sourceProductType,
String targetProductType,
String packageFileId,
String packagePath,
String stagingDir,
String runnerStatePath,
String storageEncKeyFingerprint,
String authenticityKeyFingerprint,
String restorePlanJson,
String createBy,
String createdAt,
String startedAt,
String finishedAt
) {
this.taskId = taskId;
this.precheckId = precheckId;
this.backupId = backupId;
this.status = status;
this.sourceDeviceId = sourceDeviceId;
this.sourceProductType = sourceProductType;
this.targetProductType = targetProductType;
this.packageFileId = packageFileId;
this.packagePath = packagePath;
this.stagingDir = stagingDir;
this.runnerStatePath = runnerStatePath;
this.storageEncKeyFingerprint = storageEncKeyFingerprint;
this.authenticityKeyFingerprint = authenticityKeyFingerprint;
this.restorePlanJson = restorePlanJson;
this.createBy = createBy;
this.createdAt = createdAt;
this.startedAt = startedAt;
this.finishedAt = 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 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 getCreateBy() {
return createBy;
}
public void setCreateBy(String createBy) {
this.createBy = createBy;
}
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;
}
}
}
@@ -1,6 +1,7 @@
package com.cisd.tms.modules.backup.restore;
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
import com.cisd.tms.modules.backup.dto.support.ConfiguredRestoreCommand;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
@@ -35,10 +36,24 @@ public class LightweightRestoreApplierLauncher {
}
public void launch(Path workDir) {
launch(workDir, "");
}
public void launch(Path workDir, String targetProductType) {
Path normalizedWorkDir = workDir.toAbsolutePath().normalize();
// shell 脚本需要数据库连接、恢复脚本路径、健康检查地址等运行时参数。
// 这些信息来自当前 TMS 配置,写入任务目录下的 restore-env.sh,避免在命令行暴露复杂参数。
writeRestoreEnv(normalizedWorkDir);
writeCommandPlan(
normalizedWorkDir.resolve("restore-stop-commands.tsv"),
properties.getRestoreStopCommands(),
targetProductType
);
writeCommandPlan(
normalizedWorkDir.resolve("restore-start-commands.tsv"),
properties.getRestoreStartCommands(),
targetProductType
);
Path logFile = normalizedWorkDir.resolve("restore-launch.log");
// Java 阶段到这里已经完成包验签、解密、payload 展开和用户密钥恢复。
// 后续进入停机恢复脚本,当前 TMS 服务可能被脚本停止。
@@ -54,6 +69,35 @@ public class LightweightRestoreApplierLauncher {
}
}
private void writeCommandPlan(Path planFile, List<ConfiguredRestoreCommand> commands, String targetProductType) {
try {
StringBuilder builder = new StringBuilder();
for (ConfiguredRestoreCommand command : commands == null ? List.<ConfiguredRestoreCommand>of() : commands) {
String shellCommand = trim(command.getCommand());
if (shellCommand.isEmpty() || !matchesProductType(command.getProductTypes(), targetProductType)) {
continue;
}
builder.append(defaultIfBlank(command.getCode(), "COMMAND"))
.append('\t')
.append(command.isRequired())
.append('\t')
.append(shellCommand.replace('\t', ' '))
.append(System.lineSeparator());
}
Files.writeString(planFile, builder.toString());
} catch (IOException ex) {
throw new IllegalStateException("写入恢复停启命令计划失败:" + planFile, ex);
}
}
private static boolean matchesProductType(List<String> productTypes, String targetProductType) {
if (productTypes == null || productTypes.isEmpty()) {
return true;
}
String target = trim(targetProductType);
return productTypes.stream().anyMatch(item -> trim(item).equalsIgnoreCase(target));
}
private void writeRestoreEnv(Path workDir) {
try {
Files.createDirectories(workDir);
@@ -71,6 +115,7 @@ public class LightweightRestoreApplierLauncher {
values.put("DB_PASSWORD", trim(datasourcePassword));
values.put("HEALTH_CHECK_URL", trim(properties.getHealthCheckUrl()));
values.put("HEALTH_CHECK_TIMEOUT_SECONDS", String.valueOf(properties.getHealthCheckTimeoutSeconds()));
values.put("RESTORE_APPLY_DELAY_SECONDS", "3");
StringBuilder builder = new StringBuilder("# generated by TMS resource restore\n");
for (Map.Entry<String, String> entry : values.entrySet()) {
@@ -105,6 +150,10 @@ public class LightweightRestoreApplierLauncher {
return "'" + trim(value).replace("'", "'\\''") + "'";
}
private static String defaultIfBlank(String candidate, String fallback) {
return trim(candidate).isEmpty() ? fallback : candidate;
}
private static String trim(String value) {
return value == null ? "" : value.trim();
}
@@ -24,13 +24,10 @@ public class ProcessBuilderRestoreCommandInvoker implements RestoreCommandInvoke
if (logFile != null) {
pb.redirectOutput(ProcessBuilder.Redirect.appendTo(logFile.toFile()));
}
Process process = pb.start();
return process.waitFor();
pb.start();
return 0;
} catch (IOException ex) {
throw new IllegalStateException("启动恢复命令失败:" + String.join(" ", command), ex);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException("restore command 被中断: " + String.join(" ", command), ex);
}
}
}
@@ -6,4 +6,6 @@ import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
public interface ResourceRestoreService {
CreateResourceRestoreResponse createRestoreTask(CreateResourceRestoreRequest request);
CreateResourceRestoreResponse applyRestoreTask(String taskId);
}
@@ -1,15 +1,8 @@
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;
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreLatestResponse;
public interface ResourceTaskQueryService {
ResourceRestoreTaskDetailResponse getRestoreTask(String taskId);
List<ResourceTaskStepItemResponse> getRestoreTaskSteps(String taskId);
ResourceTaskStepLogResponse getRestoreTaskStepLog(String taskId, int stepNo);
ResourceRestoreLatestResponse getLatestRestore();
}
@@ -10,6 +10,7 @@ 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.crypto.ResourceCryptoKeyConstants;
import com.cisd.tms.modules.backup.packagex.RequiredBackupResourceMissingException;
import com.cisd.tms.modules.backup.packagex.ResourcePackageResult;
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
@@ -39,7 +40,6 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
private final ResourcePackageService resourcePackageService;
private final ResourceBackupTaskRepository resourceBackupTaskRepository;
private final ResourceTaskIdGenerator resourceTaskIdGenerator;
private final com.cisd.tms.modules.backup.config.ResourceBackupProperties resourceBackupProperties;
private final ResourceTaskAdmissionGuard resourceTaskAdmissionGuard;
@Override
@@ -50,9 +50,9 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
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");
response.setStorageEncKeyFingerprint(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_FINGERPRINT);
response.setAuthenticityKeyFingerprint(ResourceCryptoKeyConstants.AUTHENTICITY_KEY_FINGERPRINT);
response.setSource("FIXED_KEY_INDEX");
return response;
}
@@ -80,6 +80,8 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
entity.setSourceDeviceId(resolveSourceDeviceId());
entity.setRemark(request == null ? null : request.getRemark());
entity.setCreateBy("SYSTEM");
entity.setStorageEncKeyFingerprint(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_FINGERPRINT);
entity.setAuthenticityKeyFingerprint(ResourceCryptoKeyConstants.AUTHENTICITY_KEY_FINGERPRINT);
entity.setCreateTime(now);
entity.setUpdateTime(now);
entity.setStartTime(now);
@@ -5,7 +5,7 @@ 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.crypto.ResourceCryptoKeyConstants;
import com.cisd.tms.modules.backup.packagex.ResourcePackageInspectResult;
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
import com.cisd.tms.modules.backup.service.ResourcePrecheckService;
@@ -30,7 +30,6 @@ public class ResourcePrecheckServiceImpl implements ResourcePrecheckService {
private final ResourcePrecheckStateStore resourcePrecheckStateStore;
private final ResourceTaskIdGenerator resourceTaskIdGenerator;
private final CisdPresetProperties cisdPresetProperties;
private final ResourceBackupProperties resourceBackupProperties;
@Override
public ResourceRestorePrecheckResponse precheck(ResourceRestorePrecheckRequest request) {
@@ -61,6 +60,7 @@ public class ResourcePrecheckServiceImpl implements ResourcePrecheckService {
response.setPayloadDecrypted(false);
response.setWarnings(List.of());
response.setBlockingIssues(List.of());
response.setPrecheckPassed(isPrecheckPassed(response));
// 第一版恢复计划固定为两段:Java 负责验签/解密/展开/用户密钥恢复,shell 负责停服后的落盘恢复。
response.setRestorePlan(List.of("JAVA_EXTRACT", "SHELL_APPLY"));
response.setRestoreScopeSummary(buildScopeSummary(inspectResult));
@@ -84,11 +84,11 @@ public class ResourcePrecheckServiceImpl implements ResourcePrecheckService {
}
private String expectedStorageFingerprint() {
return "USER_KEY_ENC:" + resourceBackupProperties.getStorageEncKeyIndex();
return ResourceCryptoKeyConstants.STORAGE_ENC_KEY_FINGERPRINT;
}
private String expectedAuthenticityFingerprint() {
return "USER_KEY_SIGN:" + resourceBackupProperties.getAuthenticitySignKeyIndex();
return ResourceCryptoKeyConstants.AUTHENTICITY_KEY_FINGERPRINT;
}
private static java.util.Map<String, Object> buildScopeSummary(ResourcePackageInspectResult inspectResult) {
@@ -106,4 +106,11 @@ public class ResourcePrecheckServiceImpl implements ResourcePrecheckService {
}
return targetProductType.equalsIgnoreCase(sourceProductType);
}
private static boolean isPrecheckPassed(ResourceRestorePrecheckResponse response) {
return Boolean.TRUE.equals(response.getKeysetMatched())
&& Boolean.TRUE.equals(response.getSignatureValid())
&& Boolean.TRUE.equals(response.getCompatible())
&& (response.getBlockingIssues() == null || response.getBlockingIssues().isEmpty());
}
}
@@ -10,19 +10,25 @@ import com.cisd.tms.modules.backup.packagex.ResourcePayloadExtractor;
import com.cisd.tms.modules.backup.packagex.ResourcePayloadExtractionResult;
import com.cisd.tms.modules.backup.packagex.ResourceUserKeyRestoreService;
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
import com.cisd.tms.modules.backup.restore.ActiveRestoreStateStore;
import com.cisd.tms.modules.backup.restore.LightweightRestoreApplierLauncher;
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.crypto.ResourceCryptoKeyConstants;
import com.cisd.tms.modules.file.entity.FileRecordEntity;
import com.cisd.tms.modules.file.repository.FileRecordRepository;
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.time.DateTimeException;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -40,6 +46,8 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
private final ResourceUserKeyRestoreService resourceUserKeyRestoreService;
private final LightweightRestoreApplierLauncher lightweightRestoreApplierLauncher;
private final ResourceTaskAdmissionGuard resourceTaskAdmissionGuard;
private final ObjectMapper objectMapper;
private final ActiveRestoreStateStore activeRestoreStateStore;
@Override
@@ -47,9 +55,7 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
// 恢复会导入用户密钥、覆盖配置文件、导入数据库并停启服务,
// 必须在入口处和其它高风险任务互斥。
resourceTaskAdmissionGuard.assertCanStartRestore();
if (!Boolean.TRUE.equals(request.getConfirmRiskAccepted())) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "必须确认恢复风险");
}
validateTargetNodes(request);
// 恢复创建必须消费预检结果,确保“用户确认的 backupId”与预检看到的是同一个包。
ResourcePrecheckContext context = resourcePrecheckStateStore.find(request.getPrecheckId())
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复预检不存在"));
@@ -71,6 +77,7 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
createDirectories(taskDir);
// Java 阶段完成包的完整验签、解密和 payload 展开,并生成 shell 可消费的 TSV 计划文件。
ResourcePayloadExtractionResult extractionResult = resourcePayloadExtractor.extract(Path.of(packagePath), taskDir);
writeTargetNodeContext(taskDir, request);
// 用户密钥恢复必须放在 shell 停服/导库之前:数据库和证书数据恢复后会引用这些 keyIdx。
// 如果这里失败,不保存 APPLYING 任务、不启动 shell,避免出现“数据已覆盖但密钥不可用”的半恢复状态。
resourceUserKeyRestoreService.recover(extractionResult.getPayloadDir());
@@ -79,8 +86,8 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
entity.setTaskId(taskId);
entity.setPrecheckId(precheck.getPrecheckId());
entity.setBackupId(precheck.getBackupId());
// 轻量恢复模型下,Java 已完成包验签/解密/展开,后续由 shell apply 脚本接管实际覆盖/导入。
entity.setStatus("APPLYING");
// 创建接口只准备恢复并返回任务号,避免在 HTTP 响应返回前停掉当前 TMS。
entity.setStatus("READY_TO_APPLY");
entity.setSourceDeviceId("UNKNOWN");
entity.setSourceProductType(precheck.getSourceProductType());
entity.setTargetProductType(precheck.getTargetProductType());
@@ -88,17 +95,17 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
entity.setPackagePath(packagePath);
entity.setStagingDir(taskDir.resolve("payload").toString());
entity.setRunnerStatePath(taskDir.resolve("status.json").toString());
entity.setStorageEncKeyFingerprint("USER_KEY_ENC:" + resourceBackupProperties.getStorageEncKeyIndex());
entity.setAuthenticityKeyFingerprint("USER_KEY_SIGN:" + resourceBackupProperties.getAuthenticitySignKeyIndex());
entity.setStorageEncKeyFingerprint(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_FINGERPRINT);
entity.setAuthenticityKeyFingerprint(ResourceCryptoKeyConstants.AUTHENTICITY_KEY_FINGERPRINT);
entity.setRestorePlanJson("[\"JAVA_EXTRACT\",\"SHELL_APPLY\"]");
entity.setCreateBy("SYSTEM");
entity.setCreateTime(now);
entity.setUpdateTime(now);
// 只有 Java 解包和用户密钥恢复都成功后,任务才进入 APPLYING。
// 后续状态由 shell 写 status.json,查询接口会读取该状态文件展示实时恢复进度。
// 只有 Java 解包和用户密钥恢复都成功后,任务才进入 READY_TO_APPLY。
// 前端一次确认后可连续调用 create 和 apply;shell 只在 apply 阶段启动,
// 避免创建接口返回前停掉当前 TMS。
resourceRestoreTaskRepository.save(entity);
// shell apply 是停机恢复的执行点:停服务、覆盖文件、导 SQL、MQ replay、起服务、健康检查。
lightweightRestoreApplierLauncher.launch(taskDir);
activeRestoreStateStore.write(entity, entity.getStatus());
CreateResourceRestoreResponse response = new CreateResourceRestoreResponse();
response.setTaskId(taskId);
@@ -107,6 +114,36 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
return response;
}
@Override
public CreateResourceRestoreResponse applyRestoreTask(String taskId) {
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务不存在:" + trim(taskId)));
String status = trim(entity.getStatus());
if ("SUCCESS".equalsIgnoreCase(status) || "FAILED".equalsIgnoreCase(status)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务已结束:" + trim(taskId));
}
if (!"READY_TO_APPLY".equalsIgnoreCase(status) && !"APPLYING".equalsIgnoreCase(status)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务状态不允许执行:" + status);
}
LocalDateTime now = LocalDateTime.now();
entity.setStatus("APPLYING");
if (entity.getStartTime() == null) {
entity.setStartTime(now);
}
entity.setUpdateTime(now);
resourceRestoreTaskRepository.update(entity);
activeRestoreStateStore.write(entity, entity.getStatus());
Path taskDir = resolveTaskDir(entity);
// shell apply 是停机恢复的执行点:停服务、覆盖文件、导 SQL、MQ replay、起服务、健康检查。
lightweightRestoreApplierLauncher.launch(taskDir, entity.getTargetProductType());
CreateResourceRestoreResponse response = new CreateResourceRestoreResponse();
response.setTaskId(entity.getTaskId());
response.setStatus(entity.getStatus());
response.setCreatedAt(entity.getCreateTime() == null ? null : entity.getCreateTime().toString());
return response;
}
private static void createDirectories(Path taskDir) {
try {
Files.createDirectories(taskDir);
@@ -115,10 +152,71 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
}
}
private void writeTargetNodeContext(Path taskDir, CreateResourceRestoreRequest request) {
Path contextPath = taskDir.resolve("restore-context.json").normalize();
try {
Map<String, Object> context = Files.isRegularFile(contextPath)
? objectMapper.readValue(contextPath.toFile(), new TypeReference<Map<String, Object>>() {
})
: new LinkedHashMap<>();
context.put("targetDeployMode", trim(request.getDeployMode()).toUpperCase());
context.put("targetNodes", targetNodes(request.getNodes()));
objectMapper.writerWithDefaultPrettyPrinter().writeValue(contextPath.toFile(), context);
} catch (IOException ex) {
throw new IllegalStateException("写入恢复目标节点上下文失败:" + contextPath, ex);
}
}
private static Map<String, Object> targetNodes(CreateResourceRestoreRequest.Nodes nodes) {
Map<String, Object> values = new LinkedHashMap<>();
putIfNotBlank(values, "node01Ip", nodes.getNode01Ip());
putIfNotBlank(values, "node02Ip", nodes.getNode02Ip());
putIfNotBlank(values, "node03Ip", nodes.getNode03Ip());
putIfNotBlank(values, "node04Ip", nodes.getNode04Ip());
return values;
}
private static void putIfNotBlank(Map<String, Object> values, String key, String value) {
String text = trim(value);
if (!text.isEmpty()) {
values.put(key, text);
}
}
private static String trim(String value) {
return value == null ? "" : value.trim();
}
private static Path resolveTaskDir(ResourceRestoreTaskEntity entity) {
String runnerStatePath = trim(entity.getRunnerStatePath());
if (runnerStatePath.isEmpty()) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务状态文件路径为空");
}
return Path.of(runnerStatePath).normalize().getParent();
}
private static void validateTargetNodes(CreateResourceRestoreRequest request) {
String deployMode = trim(request.getDeployMode()).toUpperCase();
CreateResourceRestoreRequest.Nodes nodes = request.getNodes();
if (nodes == null) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "nodes不能为空");
}
if (trim(nodes.getNode01Ip()).isEmpty()) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "nodes.node01Ip不能为空");
}
if (("DUAL".equals(deployMode) || "QUAD".equals(deployMode)) && trim(nodes.getNode02Ip()).isEmpty()) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "nodes.node02Ip不能为空");
}
if ("QUAD".equals(deployMode)) {
if (trim(nodes.getNode03Ip()).isEmpty()) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "nodes.node03Ip不能为空");
}
if (trim(nodes.getNode04Ip()).isEmpty()) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "nodes.node04Ip不能为空");
}
}
}
private static void validatePrecheck(ResourceRestorePrecheckResponse precheck) {
if (precheck == null) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复预检不存在");
@@ -18,7 +18,7 @@ import org.springframework.stereotype.Component;
public class ResourceTaskAdmissionGuard {
private static final Set<String> ACTIVE_BACKUP_STATUSES = Set.of("RUNNING");
private static final Set<String> ACTIVE_RESTORE_STATUSES = Set.of("APPLYING", "RUNNING", "HANDOFF", "PENDING");
private static final Set<String> ACTIVE_RESTORE_STATUSES = Set.of("READY_TO_APPLY", "APPLYING", "RUNNING", "HANDOFF", "PENDING");
private final ResourceBackupTaskRepository resourceBackupTaskRepository;
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
@@ -1,20 +1,17 @@
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.dto.response.ResourceRestoreLatestResponse;
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
import com.cisd.tms.modules.backup.restore.ActiveRestoreStateStore;
import com.cisd.tms.modules.backup.restore.RestoreStatusFileReader;
import com.cisd.tms.modules.backup.restore.RestoreStatusFileReader.RestoreStatusSnapshot;
import com.cisd.tms.modules.backup.service.ResourceTaskQueryService;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -24,160 +21,144 @@ import org.springframework.stereotype.Service;
public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
private final ObjectMapper objectMapper;
private final RestoreStatusFileReader restoreStatusFileReader;
private final ActiveRestoreStateStore activeRestoreStateStore;
@Override
public ResourceRestoreTaskDetailResponse getRestoreTask(String taskId) {
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务不存在:" + 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.setStatusFilePath(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());
mergeLightweightStatus(response, entity);
return response;
}
@Override
public List<ResourceTaskStepItemResponse> getRestoreTaskSteps(String taskId) {
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务不存在:" + trim(taskId)));
List<String> restorePlan = readStringList(entity.getRestorePlanJson());
Path taskDir = resolveTaskDir(entity);
RestoreStatusSnapshot snapshot = restoreStatusFileReader.read(taskDir);
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);
applyLightweightStepState(item, snapshot, stepCode);
responses.add(item);
public ResourceRestoreLatestResponse getLatestRestore() {
Optional<ActiveRestoreStateStore.ActiveRestoreSnapshot> active = activeRestoreStateStore.read();
if (active.isPresent()) {
return latestFromActive(active.get());
}
return responses;
}
@Override
public ResourceTaskStepLogResponse getRestoreTaskStepLog(String taskId, int stepNo) {
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务不存在:" + trim(taskId)));
List<String> restorePlan = readStringList(entity.getRestorePlanJson());
String stepCode = stepNo > 0 && stepNo <= restorePlan.size() ? restorePlan.get(stepNo - 1) : "";
RestoreStatusSnapshot snapshot = restoreStatusFileReader.read(resolveTaskDir(entity));
ResourceTaskStepLogResponse response = new ResourceTaskStepLogResponse();
response.setTaskId(trim(taskId));
response.setStepNo(stepNo);
response.setStepCode(stepCode);
response.setContent(snapshot.log());
response.setTruncated(false);
return response;
return latestFromDatabase();
}
private static String trim(String value) {
return value == null ? "" : value.trim();
}
private void mergeLightweightStatus(ResourceRestoreTaskDetailResponse response, ResourceRestoreTaskEntity entity) {
RestoreStatusSnapshot snapshot = restoreStatusFileReader.read(resolveTaskDir(entity));
if (trim(snapshot.status()).isEmpty()) {
private ResourceRestoreLatestResponse latestFromActive(ActiveRestoreStateStore.ActiveRestoreSnapshot active) {
RestoreStatusSnapshot status = restoreStatusFileReader.read(resolveTaskDir(active.getRunnerStatePath()));
String latestStatus = defaultIfBlank(status.status(), active.getStatus());
repairMissingTask(active, status, latestStatus);
ResourceRestoreLatestResponse response = new ResourceRestoreLatestResponse();
response.setTaskId(active.getTaskId());
response.setBackupId(active.getBackupId());
response.setStatus(latestStatus);
response.setPhase(status.phase());
response.setMessage(displayMessage(status.message(), latestStatus));
response.setCreatedAt(active.getCreatedAt());
response.setFinishedAt(isTerminal(latestStatus) ? defaultIfBlank(status.updatedAt(), active.getFinishedAt()) : null);
return response;
}
private ResourceRestoreLatestResponse latestFromDatabase() {
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findAll().stream()
.filter(item -> !trim(item.getTaskId()).isEmpty())
.max(Comparator.comparing(item -> item.getCreateTime() == null ? LocalDateTime.MIN : item.getCreateTime()))
.orElse(null);
ResourceRestoreLatestResponse response = new ResourceRestoreLatestResponse();
if (entity == null) {
response.setStatus("NONE");
response.setMessage("暂无资源恢复记录");
return response;
}
RestoreStatusSnapshot status = restoreStatusFileReader.read(resolveTaskDir(entity));
String latestStatus = defaultIfBlank(status.status(), entity.getStatus());
response.setTaskId(entity.getTaskId());
response.setBackupId(entity.getBackupId());
response.setStatus(latestStatus);
response.setPhase(status.phase());
response.setMessage(displayMessage(status.message(), latestStatus));
response.setCreatedAt(entity.getCreateTime() == null ? null : entity.getCreateTime().toString());
response.setFinishedAt(isTerminal(latestStatus)
? defaultIfBlank(status.updatedAt(), entity.getFinishTime() == null ? null : entity.getFinishTime().toString())
: null);
return response;
}
private void repairMissingTask(
ActiveRestoreStateStore.ActiveRestoreSnapshot active,
RestoreStatusSnapshot status,
String latestStatus
) {
if (trim(active.getTaskId()).isEmpty() || resourceRestoreTaskRepository.findByTaskId(active.getTaskId()).isPresent()) {
return;
}
response.setStatus(defaultIfBlank(snapshot.status(), response.getStatus()));
response.setControlPhase(defaultIfBlank(snapshot.phase(), response.getControlPhase()));
response.setStateSource("SHELL_STATUS");
response.setCurrentStepCode(defaultIfBlank(snapshot.phase(), response.getCurrentStepCode()));
response.setCurrentStepNo(currentStepNo(snapshot.phase(), response.getRestorePlan()));
response.setLastHeartbeatAt(defaultIfBlank(snapshot.updatedAt(), response.getLastHeartbeatAt()));
response.setErrorMessage(defaultIfBlank(snapshot.message(), response.getErrorMessage()));
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
entity.setTaskId(active.getTaskId());
entity.setPrecheckId(defaultIfBlank(active.getPrecheckId(), active.getTaskId()));
entity.setBackupId(defaultIfBlank(active.getBackupId(), "UNKNOWN"));
entity.setStatus(defaultIfBlank(latestStatus, "UNKNOWN"));
entity.setSourceDeviceId(active.getSourceDeviceId());
entity.setSourceProductType(defaultIfBlank(active.getSourceProductType(), "UNKNOWN"));
entity.setTargetProductType(defaultIfBlank(active.getTargetProductType(), "UNKNOWN"));
entity.setPackageFileId(defaultIfBlank(active.getPackageFileId(), "UNKNOWN"));
entity.setPackagePath(active.getPackagePath());
entity.setStagingDir(active.getStagingDir());
entity.setRunnerStatePath(active.getRunnerStatePath());
entity.setStorageEncKeyFingerprint(defaultIfBlank(active.getStorageEncKeyFingerprint(), "UNKNOWN"));
entity.setAuthenticityKeyFingerprint(defaultIfBlank(active.getAuthenticityKeyFingerprint(), "UNKNOWN"));
entity.setRestorePlanJson(defaultIfBlank(active.getRestorePlanJson(), "[\"JAVA_EXTRACT\",\"SHELL_APPLY\"]"));
entity.setErrorMessage(status.message());
entity.setCreateBy(defaultIfBlank(active.getCreateBy(), "SYSTEM"));
entity.setCreateTime(parseLocalDateTime(active.getCreatedAt(), LocalDateTime.now()));
entity.setStartTime(parseLocalDateTime(active.getStartedAt(), null));
entity.setFinishTime(isTerminal(latestStatus) ? parseLocalDateTime(status.updatedAt(), null) : null);
entity.setUpdateTime(LocalDateTime.now());
resourceRestoreTaskRepository.save(entity);
}
private Path resolveStatePath(String statusFilePath) {
return trim(statusFilePath).isEmpty() ? null : Path.of(statusFilePath).normalize();
}
private Path resolveTaskDir(String statusFilePath) {
Path statePath = resolveStatePath(statusFilePath);
return statePath == null ? null : statePath.getParent();
}
private Path resolveTaskDir(ResourceRestoreTaskEntity entity) {
Path statePath = resolveStatePath(entity.getRunnerStatePath());
return statePath == null ? null : statePath.getParent();
}
private void applyLightweightStepState(ResourceTaskStepItemResponse item, RestoreStatusSnapshot snapshot, String stepCode) {
item.setLogAvailable(!trim(snapshot.log()).isEmpty());
if (trim(snapshot.status()).isEmpty()) {
item.setStatus("PENDING");
return;
}
if ("JAVA_EXTRACT".equals(stepCode)) {
item.setStatus("SUCCESS");
return;
}
if ("SHELL_APPLY".equals(stepCode)) {
item.setStatus(snapshot.status());
item.setMessage(snapshot.message());
return;
}
if (stepCode.equals(snapshot.phase())) {
item.setStatus(snapshot.status());
item.setMessage(snapshot.message());
return;
}
item.setStatus("PENDING");
}
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("解析恢复计划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("解析一致性报告JSON失败", ex);
}
}
private static String defaultIfBlank(String candidate, String fallback) {
return trim(candidate).isEmpty() ? fallback : candidate;
}
private static Integer currentStepNo(String phase, List<String> restorePlan) {
if (restorePlan == null || restorePlan.isEmpty()) {
return null;
}
for (int i = 0; i < restorePlan.size(); i++) {
if (restorePlan.get(i).equals(phase)) {
return i + 1;
}
}
return restorePlan.contains("SHELL_APPLY") ? restorePlan.indexOf("SHELL_APPLY") + 1 : null;
private static boolean isTerminal(String status) {
String value = trim(status);
return "SUCCESS".equalsIgnoreCase(value) || "FAILED".equalsIgnoreCase(value);
}
private static String displayMessage(String message, String status) {
String text = trim(message);
if (!text.isEmpty()) {
return text;
}
if ("SUCCESS".equalsIgnoreCase(trim(status))) {
return "恢复成功";
}
if ("FAILED".equalsIgnoreCase(trim(status))) {
return "恢复失败";
}
if ("NONE".equalsIgnoreCase(trim(status))) {
return "暂无资源恢复记录";
}
return "恢复执行中";
}
private static LocalDateTime parseLocalDateTime(String value, LocalDateTime fallback) {
String text = trim(value);
if (text.isEmpty()) {
return fallback;
}
try {
return LocalDateTime.parse(text);
} catch (DateTimeException ex) {
return fallback;
}
}
}
@@ -1,9 +0,0 @@
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);
}
@@ -1,37 +0,0 @@
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("读取资源任务步骤日志失败", ex);
}
}
}
+99 -2
View File
@@ -194,6 +194,41 @@ tms:
output-dir: ${TMS_BACKUP_OUTPUT_DIR:/home/tms/tmp/resource-backup-packages}
# 轻量恢复 apply 脚本启停 TMS 时使用的脚本路径。
tms-script-path: ${TMS_BACKUP_TMS_SCRIPT_PATH:/home/tms/scripts/tms.sh}
# 轻量恢复停服务命令。未配置时 apply 脚本会回退到 tms-script-path stop。
# command 支持现场脚本路径或完整 shell 命令;required=false 表示失败只记录 warning。
restore-stop-commands:
- code: STANDARD_NGINX
command: ${TMS_BACKUP_RESTORE_STOP_STANDARD_NGINX_COMMAND:${tms.init.executor.standard-nginx-stop-command}}
required: ${TMS_BACKUP_RESTORE_STOP_STANDARD_NGINX_REQUIRED:false}
product-types:
- ENTERPRISE
- INDIRECT
- code: STANDARD_APPS
command: ${TMS_BACKUP_RESTORE_STOP_STANDARD_APPS_COMMAND:${tms.init.executor.standard-app-stop-command}}
required: ${TMS_BACKUP_RESTORE_STOP_STANDARD_APPS_REQUIRED:false}
product-types:
- ENTERPRISE
- INDIRECT
- code: TMS
command: ${TMS_BACKUP_RESTORE_STOP_TMS_COMMAND:${tms.backup.tms-script-path} stop}
required: ${TMS_BACKUP_RESTORE_STOP_TMS_REQUIRED:false}
# 轻量恢复启动服务命令。标准/间参默认启动 TMS、CMSP/CMTP 和 nginx。
restore-start-commands:
- code: TMS
command: ${TMS_BACKUP_RESTORE_START_TMS_COMMAND:${tms.backup.tms-script-path} start}
required: ${TMS_BACKUP_RESTORE_START_TMS_REQUIRED:true}
- code: STANDARD_APPS
command: ${TMS_BACKUP_RESTORE_START_STANDARD_APPS_COMMAND:${tms.init.executor.standard-app-start-command}}
required: ${TMS_BACKUP_RESTORE_START_STANDARD_APPS_REQUIRED:true}
product-types:
- ENTERPRISE
- INDIRECT
- code: STANDARD_NGINX
command: ${TMS_BACKUP_RESTORE_START_STANDARD_NGINX_COMMAND:${tms.init.executor.standard-nginx-start-command}}
required: ${TMS_BACKUP_RESTORE_START_STANDARD_NGINX_REQUIRED:true}
product-types:
- ENTERPRISE
- INDIRECT
# 轻量恢复 apply 脚本路径;Java 完成验签/解密/展开后调用该脚本执行覆盖、SQL、MQ replay。
restore-apply-script-path: ${TMS_BACKUP_RESTORE_APPLY_SCRIPT_PATH:/home/tms/bin/resource-restore/apply-resource-backup.sh}
# 文件恢复白名单根路径;manifest 中 FILE 恢复路径必须落在这些目录下。
@@ -202,6 +237,61 @@ tms:
- ${TMS_BACKUP_ALLOWED_RESTORE_ROOT_CMEP4I:/home/cmep4i}
- ${TMS_BACKUP_ALLOWED_RESTORE_ROOT_CMEP4D:/home/cmep4d}
- ${TMS_BACKUP_ALLOWED_RESTORE_ROOT_WORK:/home/tms/tmp/resource-restore}
# 现场可追加或覆盖直接复制恢复的文件类资源。
# mode=FILE 表示单文件,mode=DIR 表示目录递归,mode=GLOB 表示按通配符匹配同一目录下的一批文件。
# restore-path 对 FILE 是目标文件路径,对 DIR/GLOB 是目标目录;目标仍必须落在 allowed-restore-roots 下。
file-resources:
- code: TMS_CONFIG
mode: DIR
path: ${TMS_BACKUP_TMS_CONFIG_PATH:${tms.init.executor.tms-config-dir}}
restore-path: ${TMS_BACKUP_TMS_CONFIG_RESTORE_PATH:${tms.init.executor.tms-config-dir}}
required: ${TMS_BACKUP_TMS_CONFIG_REQUIRED:true}
- code: CP_CONFIG
mode: FILE
path: ${TMS_BACKUP_STANDARD_CP_CONFIG_PATH:${tms.init.executor.standard-cpconfig-path}}
restore-path: ${TMS_BACKUP_STANDARD_CP_CONFIG_RESTORE_PATH:${tms.init.executor.standard-cpconfig-path}}
product-types:
- ENTERPRISE
- INDIRECT
required: ${TMS_BACKUP_STANDARD_CP_CONFIG_REQUIRED:true}
- code: CP_CONFIG
mode: FILE
path: ${TMS_BACKUP_DIRECT_CP_CONFIG_PATH:${tms.init.executor.direct-cpconfig-target-path}}
restore-path: ${TMS_BACKUP_DIRECT_CP_CONFIG_RESTORE_PATH:${tms.init.executor.direct-cpconfig-target-path}}
product-types:
- DIRECT
required: ${TMS_BACKUP_DIRECT_CP_CONFIG_REQUIRED:true}
- code: RECEIVER_LICENSE
mode: FILE
path: ${TMS_BACKUP_STANDARD_RECEIVER_LICENSE_PATH:${tms.init.executor.standard-receiver-license-target-path}}
restore-path: ${TMS_BACKUP_STANDARD_RECEIVER_LICENSE_RESTORE_PATH:${tms.init.executor.standard-receiver-license-target-path}}
product-types:
- ENTERPRISE
- INDIRECT
required: ${TMS_BACKUP_STANDARD_RECEIVER_LICENSE_REQUIRED:true}
- code: RECEIVER_LICENSE
mode: FILE
path: ${TMS_BACKUP_DIRECT_RECEIVER_LICENSE_PATH:${tms.init.executor.direct-receiver-license-target-path}}
restore-path: ${TMS_BACKUP_DIRECT_RECEIVER_LICENSE_RESTORE_PATH:${tms.init.executor.direct-receiver-license-target-path}}
product-types:
- DIRECT
required: ${TMS_BACKUP_DIRECT_RECEIVER_LICENSE_REQUIRED:true}
- code: STANDARD_CMSP_APP_CONFIG
mode: GLOB
path: ${TMS_BACKUP_STANDARD_CMSP_APP_CONFIG_PATH:${tms.init.executor.standard-home-dir}/cmsp/application-prd*.properties}
restore-path: ${TMS_BACKUP_STANDARD_CMSP_APP_CONFIG_RESTORE_PATH:${tms.init.executor.standard-home-dir}/cmsp}
product-types:
- ENTERPRISE
- INDIRECT
required: ${TMS_BACKUP_STANDARD_CMSP_APP_CONFIG_REQUIRED:true}
- code: STANDARD_CMTP_APP_CONFIG
mode: GLOB
path: ${TMS_BACKUP_STANDARD_CMTP_APP_CONFIG_PATH:${tms.init.executor.standard-home-dir}/cmtp/application-prd*.properties}
restore-path: ${TMS_BACKUP_STANDARD_CMTP_APP_CONFIG_RESTORE_PATH:${tms.init.executor.standard-home-dir}/cmtp}
product-types:
- ENTERPRISE
- INDIRECT
required: ${TMS_BACKUP_STANDARD_CMTP_APP_CONFIG_REQUIRED:true}
# 导出 TMS 库时使用的 mysqldump 命令;现场可按实际安装路径覆盖。
mysqldump-path: ${TMS_BACKUP_MYSQLDUMP_PATH:mysqldump}
# 恢复 TMS 库时使用的 mysql 客户端命令。
@@ -212,6 +302,13 @@ tms:
mq-replay-script-path: ${TMS_BACKUP_MQ_REPLAY_SCRIPT_PATH:/home/tms/bin/resource-restore/replay-mq.sh}
# 备份 TMS 库名;为空时默认从 spring.datasource.url 中解析。
tms-database-name: ${TMS_BACKUP_TMS_DATABASE_NAME:}
# TMS 库备份排除表。默认不备份角色、授权、会话和审计表,避免恢复时覆盖新机器安全状态。
tms-db-excluded-tables:
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_ROLE_ACCOUNT:tms_role_account}
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_ROLE_UKEY:tms_role_ukey_binding}
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_AUTH_FULL:tms_auth_full_account}
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_AUTH_SESSION:tms_auth_session}
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_AUTH_CHALLENGE:tms_auth_challenge}
# 备份/恢复标准收发器业务库名;第一版默认使用 CMEP。
cmep-database-name: ${TMS_BACKUP_CMEP_DATABASE_NAME:CMEP}
# 恢复完成后的最小健康检查地址。
@@ -229,9 +326,9 @@ tms:
internal-token: ${TMS_INTERNAL_TOKEN:change-me-internal-token}
internal-auth:
# 是否启用 /api/** 的内部登录鉴权;开发联调时可临时关闭。
enabled: ${TMS_SECURITY_INTERNAL_AUTH_ENABLED:true}
enabled: ${TMS_SECURITY_INTERNAL_AUTH_ENABLED:false}
# 关闭内部鉴权后,注入请求上下文的调试角色。
debug-role-code: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_ROLE_CODE:KEY_ADMIN}
debug-role-code: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_ROLE_CODE:OPS_ADMIN}
# 关闭内部鉴权后,注入请求上下文的调试认证等级。
debug-auth-level: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_AUTH_LEVEL:FULL}
# 关闭内部鉴权后,注入请求上下文的调试 session token。
@@ -115,26 +115,6 @@ CREATE TABLE IF NOT EXISTS tms_resource_restore_task (
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)
);
-- -----------------------------------------------------------------------------
-- 认证与设备基础表
-- -----------------------------------------------------------------------------
@@ -62,22 +62,3 @@ CREATE TABLE IF NOT EXISTS tms_resource_restore_task (
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)
);