资源备份流程整理

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
@@ -0,0 +1,843 @@
# 资源备份恢复前端对接文档
## 1. 对接目标
本文说明当前 `tms-framework` 资源备份和资源恢复接口的前端调用方式、接口顺序、页面判断规则和错误处理建议。
当前前端只需要支持两个主流程:
- 创建资源备份包并下载 `.tmsbak`
- 上传 `.tmsbak`,预检通过后执行停机恢复,并在 TMS 重启后展示最近恢复结果
恢复侧已经收敛为轻量流程,不再提供恢复任务详情、恢复步骤和恢复步骤日志接口。恢复完成后的页面展示只依赖 `GET /api/v1/resource-restores/latest`。
## 2. 通用约定
### 2.1 基础路径
| 模块 | 基础路径 |
| --- | --- |
| 文件上传 | `/api/v1/files` |
| 资源备份 | `/api/v1/resource-backups` |
| 资源恢复 | `/api/v1/resource-restores` |
### 2.2 统一响应
除备份包下载接口外,其它接口返回 `ApiResponse<T>`:
```json
{
"success": true,
"code": 200,
"msg": "成功",
"data": {},
"timestamp": 1777459200000,
"traceId": "optional-trace-id"
}
```
前端统一处理规则:
- HTTP 非 2xx:请求失败,展示 HTTP 错误或后端 `msg`
- `success=false`:业务失败,展示 `msg`
- `success=true`:请求成功,继续按 `data` 中的业务字段判断流程
### 2.3 权限和防重放
资源备份/恢复接口要求内部登录态,角色为 `OPS_ADMIN` 或 `SUPER_ADMIN`,认证等级为 `FULL`。
需要防重放头的接口:
- `POST /api/v1/resource-backups`
- `POST /api/v1/resource-restores`
- `POST /api/v1/resource-restores/{taskId}/apply`
请求头示例:
```http
Authorization: Bearer <token>
X-Request-Timestamp: <unix秒时间戳>
X-Request-Nonce: <唯一随机串>
```
`GET` 查询类接口和 `POST /api/v1/resource-restores/precheck` 当前不需要防重放头,但仍需要登录态。
### 2.4 时间字段
时间字段前端按字符串展示即可:
- Java 任务时间:`2026-04-29T16:00:00`
- shell 状态时间:`2026-04-29T16:00:00+0800`
## 3. 页面流程总览
### 3.1 备份流程
1. 用户点击“创建资源备份”。
2. 前端调用 `POST /api/v1/resource-backups`。
3. 根据返回的 `data.status` 判断:
- `SUCCESS`:展示“下载备份包”
- `FAILED`:展示 `errorMessage`,可查看备份步骤/日志
- 其它状态:调用 `GET /api/v1/resource-backups/{taskId}` 轮询
4. 用户点击“下载”,调用 `GET /api/v1/resource-backups/{taskId}/download`。
5. 浏览器保存 `.tmsbak` 文件。
当前后端创建备份通常同步完成,`POST /api/v1/resource-backups` 可能直接返回 `SUCCESS` 或 `FAILED`。前端保留轮询逻辑即可。
### 3.2 恢复流程
1. 用户选择 `.tmsbak` 文件。
2. 前端调用 `POST /api/v1/files/upload` 上传文件,拿到 `fileId`。
3. 前端调用 `POST /api/v1/resource-restores/precheck`,传入 `fileId`。
4. 前端展示预检信息,并按 `data.precheckPassed` 判断是否允许下一步。
5. `precheckPassed=true` 时,前端展示一次“确认恢复”弹窗,弹窗中明确提示会进入停机恢复。
6. 用户确认后,前端调用 `POST /api/v1/resource-restores` 创建恢复准备任务。
7. 创建成功且 `data.status=READY_TO_APPLY` 时,前端不要再次弹确认框,直接调用 `POST /api/v1/resource-restores/{taskId}/apply`。
8. `apply` 接口返回后,TMS 可能很快停机、恢复、重启。前端应提示页面/API 短暂不可用。
9. TMS 重启后,恢复页面调用 `GET /api/v1/resource-restores/latest` 展示最近恢复结果。
### 3.3 恢复流程判断点
| 阶段 | 判断字段 | 前端动作 |
| --- | --- | --- |
| 上传备份包 | `data.fileId` 非空 | 保存 `fileId`,进入预检 |
| 预检 | `data.precheckPassed === true` | 允许点击“确认恢复” |
| 预检 | `data.precheckPassed !== true` | 禁止下一步,展示 `blockingIssues` / `warnings` |
| 创建恢复任务 | `data.status === "READY_TO_APPLY"` | 立即调用 apply,不再弹确认框 |
| 执行恢复任务 | `data.status === "APPLYING"` | 展示“恢复中,服务可能重启” |
| 重启后查询 latest | `data.status === "SUCCESS"` | 展示恢复成功 |
| 重启后查询 latest | `data.status === "FAILED"` | 展示恢复失败、`phase`、`message` |
| 重启后查询 latest | `data.status === "NONE"` | 展示暂无恢复记录 |
## 4. 接口明细
### 4.1 上传备份包
`POST /api/v1/files/upload`
请求类型:`multipart/form-data`
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `file` | `File` | 是 | 用户选择的 `.tmsbak` 文件 |
响应示例:
```json
{
"success": true,
"code": 200,
"msg": "成功",
"data": {
"fileId": "FILE-20260429-000001",
"filename": "RBKP-20260429-160000-ABCDEF.tmsbak",
"size": 1048576
}
}
```
前端只需要保存 `fileId`,不要依赖服务端文件路径。
### 4.2 创建资源备份
`POST /api/v1/resource-backups`
需要防重放头。
请求体可为空,也可以传备注:
```json
{
"remark": "硬件更换前备份"
}
```
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `remark` | `string` | 否 | 备份备注 |
响应示例:
```json
{
"success": true,
"code": 200,
"msg": "成功",
"data": {
"taskId": "RBKP-TASK-20260429-000001",
"backupId": "RBKP-20260429-160000-ABCDEF",
"status": "SUCCESS",
"createdAt": "2026-04-29T16:00:00"
}
}
```
前端判断:
- `status=SUCCESS`:允许下载
- `status=FAILED`:展示失败原因,可查询任务详情或步骤日志
- 其它状态:使用 `taskId` 查询任务详情
### 4.3 查询备份任务详情
`GET /api/v1/resource-backups/{taskId}`
常用响应字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `taskId` | `string` | 备份任务号 |
| `backupId` | `string` | 备份包身份号,恢复预检后会显示给用户确认 |
| `status` | `ResourceTaskStatus` | 任务状态 |
| `productType` | `ProductType` | 产品类型 |
| `mqType` | `MqType` | MQ 类型 |
| `orgCode` | `string` | 机构号 |
| `packageName` | `string` | 备份包文件名 |
| `packageSize` | `number` | 包大小,字节 |
| `packageHash` | `string` | 包摘要 |
| `remark` | `string` | 创建备份时传入的备注 |
| `errorMessage` | `string` | 失败原因 |
| `createdAt` | `string` | 创建时间 |
| `finishedAt` | `string` | 完成时间 |
### 4.4 下载备份包
`GET /api/v1/resource-backups/{taskId}/download`
该接口返回文件流,不是 `ApiResponse`。
响应头示例:
```http
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="RBKP-20260429-160000-ABCDEF.tmsbak"
```
前端建议:
- 仅当备份任务 `status=SUCCESS` 时展示下载按钮
- 使用浏览器下载能力或 `blob` 保存文件
### 4.5 查询备份步骤
`GET /api/v1/resource-backups/{taskId}/steps`
用途:备份失败时展示执行上下文。
响应示例:
```json
{
"success": true,
"code": 200,
"msg": "成功",
"data": [
{
"stepNo": 1,
"stepCode": "PACKAGE_CREATE",
"status": "SUCCESS",
"message": "generated backup package RBKP-20260429-160000-ABCDEF.tmsbak",
"startedAt": "2026-04-29T16:00:00",
"finishedAt": "2026-04-29T16:00:03",
"logAvailable": false
}
]
}
```
### 4.6 查询备份步骤日志
`GET /api/v1/resource-backups/{taskId}/steps/{stepNo}/log`
用途:备份失败或排障时展示详细日志。
响应示例:
```json
{
"success": true,
"code": 200,
"msg": "成功",
"data": {
"taskId": "RBKP-TASK-20260429-000001",
"stepNo": 1,
"stepCode": "PACKAGE_CREATE",
"content": "{\"packageName\":\"RBKP-20260429-160000-ABCDEF.tmsbak\"}",
"truncated": false
}
}
```
### 4.7 恢复预检
`POST /api/v1/resource-restores/precheck`
请求体:
```json
{
"fileId": "FILE-20260429-000001"
}
```
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `fileId` | `string` | 是 | 上传接口返回的文件 ID |
响应示例:
```json
{
"success": true,
"code": 200,
"msg": "成功",
"data": {
"precheckId": "RPRE-20260429-000001",
"backupId": "RBKP-20260429-160000-ABCDEF",
"sourceProductType": "ENTERPRISE",
"sourceMqType": "RABBITMQ",
"sourceOrgCode": "AAAABBBBXXX",
"sourceBackupTime": "2026-04-29T16:00:00",
"targetProductType": "ENTERPRISE",
"keysetMatched": true,
"signatureValid": true,
"compatible": true,
"precheckPassed": true,
"payloadDecrypted": false,
"warnings": [],
"blockingIssues": [],
"restorePlan": ["JAVA_EXTRACT", "SHELL_APPLY"],
"restoreScopeSummary": {
"resourceCount": 6,
"productType": "ENTERPRISE",
"mqType": "RABBITMQ",
"orgCode": "AAAABBBBXXX"
},
"expiresAt": "2026-04-29T16:30:00"
}
}
```
前端判断:
- 下一步只看 `precheckPassed`
- `precheckPassed=true`:允许创建恢复任务
- `precheckPassed=false`:禁止下一步,展示 `blockingIssues`;如只有 `warnings`,按页面需要提示用户
说明:
- `precheckPassed` 是后端聚合结果,当前等价于 `keysetMatched=true`、`signatureValid=true`、`compatible=true` 且 `blockingIssues` 为空
- `payloadDecrypted=false` 是正常值,预检阶段不解密完整 payload
- 真正解密、展开 payload、路径白名单校验发生在创建恢复任务阶段
### 4.8 创建恢复任务
`POST /api/v1/resource-restores`
需要防重放头。
请求体:
```json
{
"precheckId": "RPRE-20260429-000001",
"confirmBackupId": "RBKP-20260429-160000-ABCDEF",
"deployMode": "DUAL",
"nodes": {
"node01Ip": "192.168.1.11",
"node02Ip": "192.168.1.12"
}
}
```
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `precheckId` | `string` | 是 | 预检接口返回的预检号 |
| `confirmBackupId` | `string` | 是 | 用户确认的备份号,必须等于预检响应 `backupId` |
| `deployMode` | `DeployMode` | 是 | `SINGLE`、`DUAL`、`QUAD` |
| `nodes.node01Ip` | `string` | 是 | 节点 1 IPv4 |
| `nodes.node02Ip` | `string` | `DUAL` / `QUAD` 必填 | 节点 2 IPv4 |
| `nodes.node03Ip` | `string` | `QUAD` 必填 | 节点 3 IPv4 |
| `nodes.node04Ip` | `string` | `QUAD` 必填 | 节点 4 IPv4 |
响应示例:
```json
{
"success": true,
"code": 200,
"msg": "成功",
"data": {
"taskId": "RRST-TASK-20260429-000001",
"status": "READY_TO_APPLY",
"createdAt": "2026-04-29T16:05:00"
}
}
```
前端判断:
- `status=READY_TO_APPLY`:立即调用 apply,不再弹确认框
- 其它状态:展示异常提示,不调用 apply
后端在该阶段会完成验签、解密、payload 展开、用户密钥恢复、路径白名单校验和恢复计划文件生成,但不会停止 TMS。
### 4.9 执行恢复任务
`POST /api/v1/resource-restores/{taskId}/apply`
需要防重放头。
响应示例:
```json
{
"success": true,
"code": 200,
"msg": "成功",
"data": {
"taskId": "RRST-TASK-20260429-000001",
"status": "APPLYING",
"createdAt": "2026-04-29T16:05:00"
}
}
```
前端判断:
- `status=APPLYING`:提示用户恢复执行中,页面和接口可能短暂不可用
- 之后不要继续轮询恢复步骤,等待 TMS 重启后查 latest
后端在该阶段会启动后台脚本,执行停服务、文件覆盖、数据库导入、MQ replay、启动服务和健康检查。
### 4.10 查询最近恢复结果
`GET /api/v1/resource-restores/latest`
用途:TMS 重启后,恢复页面展示最近一次恢复结果。
响应示例:
```json
{
"success": true,
"code": 200,
"msg": "成功",
"data": {
"taskId": "RRST-TASK-20260429-000001",
"backupId": "RBKP-20260429-160000-ABCDEF",
"status": "FAILED",
"phase": "DATABASE",
"message": "数据库恢复失败",
"createdAt": "2026-04-29T16:05:00",
"finishedAt": "2026-04-29T16:12:00+0800"
}
}
```
前端判断:
- `status=SUCCESS`:展示最近恢复成功和 `finishedAt`
- `status=FAILED`:展示恢复失败、`phase`、`message`
- `status=APPLYING` 或 `RUNNING`:展示恢复仍在执行或等待刷新
- `status=NONE`:展示暂无恢复记录
## 5. 状态和枚举
### 5.1 ProductType
| 值 | 说明 |
| --- | --- |
| `ENTERPRISE` | 企参模式 |
| `INDIRECT` | 间参模式 |
| `DIRECT` | 直参模式 |
| `UNKNOWN` | 后端无法识别或未配置 |
### 5.2 MqType
| 值 | 说明 |
| --- | --- |
| `RABBITMQ` | 标准 RabbitMQ |
| `TLQ` | TLQ |
| `RABBITMQ_TLQ` | RabbitMQ + TLQ |
| `RABBITMQ_CFMQ` | RabbitMQ + CFMQ |
### 5.3 DeployMode
| 值 | 说明 | 节点要求 |
| --- | --- | --- |
| `SINGLE` | 单节点部署 | `node01Ip` |
| `DUAL` | 双节点部署 | `node01Ip`、`node02Ip` |
| `QUAD` | 四节点部署 | `node01Ip`、`node02Ip`、`node03Ip`、`node04Ip` |
第一版中,节点参数只写入恢复上下文用于审计和排障,不自动改写恢复后的配置文件。
### 5.4 ResourceTaskStatus
| 值 | 前端文案建议 | 说明 |
| --- | --- | --- |
| `RUNNING` | 执行中 | 备份或 shell 恢复执行中 |
| `SUCCESS` | 成功 | 任务成功 |
| `FAILED` | 失败 | 任务失败 |
| `READY_TO_APPLY` | 待停机恢复 | 恢复任务已准备好,前端可继续调用 apply |
| `APPLYING` | 恢复执行中 | 恢复脚本已启动或即将启动 |
| `NONE` | 暂无记录 | 仅 latest 接口可能返回 |
其它预留状态前端可以兜底展示原值。
### 5.5 RestorePhase
| 值 | 说明 |
| --- | --- |
| `APPLY_DELAY` | 后台脚本启动后短暂等待 |
| `SERVICE_STOP` | 停止服务 |
| `FILE` | 恢复文件 |
| `DATABASE` | 导入数据库 |
| `MQ_REPLAY` | 重放 MQ 配置 |
| `SERVICE_START` | 启动服务 |
| `HEALTH_CHECK` | 健康检查 |
| `DONE` | 完成 |
### 5.6 资源范围说明
当前备份/恢复资源包括:
- `TMS_CONFIG`
- `TMS_DB`
- `CMEP_DB`
- `ORG_DB`
- `CP_CONFIG`
- `RECEIVER_LICENSE`
- `STANDARD_CMSP_APP_CONFIG`
- `STANDARD_CMTP_APP_CONFIG`
- `USER_KEY`
- `MQ_REPLAY`
`TMS_DB` 默认排除 `tms.backup.tms-db-excluded-tables` 中声明的表。默认排除角色、授权、会话和审计表,避免恢复时覆盖新机器安全状态。
## 6. TypeScript 类型建议
```ts
export interface ApiResponse<T> {
success: boolean;
code: number;
msg: string;
data: T;
timestamp?: number;
traceId?: string;
}
export type ProductType = 'ENTERPRISE' | 'INDIRECT' | 'DIRECT' | 'UNKNOWN';
export type MqType = 'RABBITMQ' | 'TLQ' | 'RABBITMQ_TLQ' | 'RABBITMQ_CFMQ';
export type DeployMode = 'SINGLE' | 'DUAL' | 'QUAD';
export type ResourceTaskStatus =
| 'RUNNING'
| 'SUCCESS'
| 'FAILED'
| 'READY_TO_APPLY'
| 'APPLYING'
| 'NONE'
| string;
export interface CreateResourceBackupRequest {
remark?: string;
}
export interface CreateResourceBackupResponse {
taskId: string;
backupId?: string;
status: ResourceTaskStatus;
createdAt?: string;
}
export interface ResourceBackupTaskDetailResponse {
taskId: string;
backupId?: string;
status: ResourceTaskStatus;
productType?: ProductType;
mqType?: MqType;
orgCode?: string;
packageName?: string;
packageSize?: number;
packageHash?: string;
remark?: string;
errorMessage?: string;
createdAt?: string;
finishedAt?: string;
}
export interface ResourceRestorePrecheckRequest {
fileId: string;
}
export interface ResourceRestorePrecheckResponse {
precheckId: string;
backupId: string;
sourceProductType?: ProductType;
sourceMqType?: MqType;
sourceOrgCode?: string;
sourceBackupTime?: string;
targetProductType?: ProductType;
keysetMatched: boolean;
signatureValid: boolean;
compatible: boolean;
precheckPassed: boolean;
payloadDecrypted: boolean;
warnings: string[];
blockingIssues: string[];
restorePlan: string[];
restoreScopeSummary?: unknown;
expiresAt: string;
}
export interface CreateResourceRestoreRequest {
precheckId: string;
confirmBackupId: string;
deployMode: DeployMode;
nodes: {
node01Ip: string;
node02Ip?: string;
node03Ip?: string;
node04Ip?: string;
};
}
export interface CreateResourceRestoreResponse {
taskId: string;
status: ResourceTaskStatus;
createdAt?: string;
}
export interface ResourceRestoreLatestResponse {
taskId?: string;
backupId?: string;
status: ResourceTaskStatus;
phase?: string;
message?: string;
createdAt?: string;
finishedAt?: string;
}
```
## 7. Vue 3 对接示例
以下示例按 Vue 3 Composition API 编写。`request`、`message`、`confirmRestore`、`downloadByUrl` 可替换为项目内已有的 axios 封装、弹窗组件和下载工具。
### 7.1 防重放头
```ts
function replayHeaders() {
return {
'X-Request-Timestamp': String(Math.floor(Date.now() / 1000)),
'X-Request-Nonce': crypto.randomUUID()
};
}
```
### 7.2 备份 composable
```ts
import { computed, ref } from 'vue';
export function useResourceBackup() {
const backupTask = ref<ResourceBackupTaskDetailResponse | CreateResourceBackupResponse>();
const backupLoading = ref(false);
const canDownload = computed(() => backupTask.value?.status === 'SUCCESS');
const backupFailed = computed(() => backupTask.value?.status === 'FAILED');
async function createBackup(remark?: string) {
backupLoading.value = true;
try {
const res = await request.post<ApiResponse<CreateResourceBackupResponse>>(
'/api/v1/resource-backups',
remark ? { remark } : {},
{ headers: replayHeaders() }
);
if (!res.data.success) throw new Error(res.data.msg);
backupTask.value = res.data.data;
if (!['SUCCESS', 'FAILED'].includes(res.data.data.status)) {
await refreshBackup(res.data.data.taskId);
}
} finally {
backupLoading.value = false;
}
}
async function refreshBackup(taskId: string) {
const res = await request.get<ApiResponse<ResourceBackupTaskDetailResponse>>(
`/api/v1/resource-backups/${taskId}`
);
if (!res.data.success) throw new Error(res.data.msg);
backupTask.value = res.data.data;
}
function downloadBackup() {
if (!backupTask.value || backupTask.value.status !== 'SUCCESS') return;
downloadByUrl(`/api/v1/resource-backups/${backupTask.value.taskId}/download`);
}
return {
backupTask,
backupLoading,
canDownload,
backupFailed,
createBackup,
refreshBackup,
downloadBackup
};
}
```
页面判断:
- `canDownload=true` 时展示“下载备份包”。
- `backupFailed=true` 时展示 `backupTask.errorMessage`,并允许进入备份步骤/日志排查。
- 备份侧可以继续保留任务详情、步骤、日志查询。
### 7.3 恢复 composable
```ts
import { computed, ref } from 'vue';
export function useResourceRestore() {
const selectedFile = ref<File>();
const precheckResult = ref<ResourceRestorePrecheckResponse>();
const latestRestore = ref<ResourceRestoreLatestResponse>();
const restoreLoading = ref(false);
const canCreateRestore = computed(() => precheckResult.value?.precheckPassed === true);
const precheckBlocked = computed(() => precheckResult.value?.precheckPassed === false);
async function uploadAndPrecheck(file: File) {
selectedFile.value = file;
restoreLoading.value = true;
try {
const form = new FormData();
form.append('file', file);
const upload = await request.post<ApiResponse<{ fileId: string }>>(
'/api/v1/files/upload',
form
);
if (!upload.data.success) throw new Error(upload.data.msg);
const precheck = await request.post<ApiResponse<ResourceRestorePrecheckResponse>>(
'/api/v1/resource-restores/precheck',
{ fileId: upload.data.data.fileId }
);
if (!precheck.data.success) throw new Error(precheck.data.msg);
precheckResult.value = precheck.data.data;
} finally {
restoreLoading.value = false;
}
}
async function confirmAndRestore(deployMode: DeployMode, nodes: CreateResourceRestoreRequest['nodes']) {
if (!precheckResult.value || !canCreateRestore.value) return;
await confirmRestore({
title: '确认恢复资源',
message: `确认使用备份包 ${precheckResult.value.backupId} 进行恢复?恢复过程中服务会停启,页面可能短暂不可访问。`
});
restoreLoading.value = true;
try {
const created = await request.post<ApiResponse<CreateResourceRestoreResponse>>(
'/api/v1/resource-restores',
{
precheckId: precheckResult.value.precheckId,
confirmBackupId: precheckResult.value.backupId,
deployMode,
nodes
},
{ headers: replayHeaders() }
);
if (!created.data.success) throw new Error(created.data.msg);
if (created.data.data.status !== 'READY_TO_APPLY') {
throw new Error('恢复任务状态异常');
}
const applied = await request.post<ApiResponse<CreateResourceRestoreResponse>>(
`/api/v1/resource-restores/${created.data.data.taskId}/apply`,
{},
{ headers: replayHeaders() }
);
if (!applied.data.success) throw new Error(applied.data.msg);
message.info('恢复已开始,TMS 将停机并重启。重启后请返回恢复页面查看最近恢复结果。');
} finally {
restoreLoading.value = false;
}
}
async function loadLatestRestore() {
const res = await request.get<ApiResponse<ResourceRestoreLatestResponse>>(
'/api/v1/resource-restores/latest'
);
if (!res.data.success) throw new Error(res.data.msg);
latestRestore.value = res.data.data;
}
return {
selectedFile,
precheckResult,
latestRestore,
restoreLoading,
canCreateRestore,
precheckBlocked,
uploadAndPrecheck,
confirmAndRestore,
loadLatestRestore
};
}
```
页面判断:
- 文件选择后调用 `uploadAndPrecheck(file)`。
- “确认恢复”按钮只在 `canCreateRestore=true` 时启用。
- `precheckBlocked=true` 时展示 `blockingIssues`,不要允许用户继续。
- `confirmAndRestore` 内部只弹一次确认;用户确认后按顺序调用创建恢复任务和 apply。
- apply 返回后不要轮询恢复任务详情、恢复步骤或恢复日志。
### 7.4 重启后最近恢复结果展示
```vue
<script setup lang="ts">
import { computed, onMounted } from 'vue';
const { latestRestore, loadLatestRestore } = useResourceRestore();
onMounted(loadLatestRestore);
const latestText = computed(() => {
const latest = latestRestore.value;
if (!latest || latest.status === 'NONE') return '暂无资源恢复记录';
if (latest.status === 'SUCCESS') return `最近恢复成功:${latest.finishedAt ?? ''}`;
if (latest.status === 'FAILED') {
return `最近恢复失败:${latest.phase ?? ''} ${latest.message ?? ''}`;
}
return `最近恢复状态:${latest.status}`;
});
</script>
<template>
<div>{{ latestText }}</div>
</template>
```
## 8. 交互和展示建议
- 备份创建按钮点击后立即禁用,避免重复提交。
- 恢复预检通过后,必须展示备份号 `backupId`,让用户确认正在恢复哪个包。
- `precheckPassed=false` 时不要允许创建恢复任务。
- 恢复流程只做一次用户确认;确认后前端按顺序调用创建恢复任务和 apply。
- apply 返回后不要继续请求恢复步骤接口;恢复侧没有步骤接口。
- TMS 重启后只调用 latest 展示最近恢复结果。
- `payloadDecrypted=false` 不表示失败,它表示预检阶段没有解密完整 payload。
- `TMS_DB` 默认不包含角色、授权、会话和审计表,因此恢复不会覆盖新机器已有登录授权状态。
- 服务端路径类字段不展示给普通用户,只用于运维排查。