cisd/docs/openapi/resource-backup-restore-frontend-integration.md
2026-05-13 15:33:29 +08:00

845 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 资源备份恢复前端对接文档
## 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`,认证等级为 `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、路径白名单校验发生在创建恢复任务阶段
- `payload.enc` 的实际加解密由后端通过 PCIe 密码卡 SDF 会话密钥流程完成,前端不需要传递 `keyIndex/keyBits/algId`
### 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
后端在该阶段会完成验签、PCIe/SDF 解密、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` 中声明的表。默认排除角色、授权、会话和资源任务表,避免恢复时覆盖新机器安全登录状态或带回旧的资源任务状态;`tms_operation_audit_log` 默认随 TMS 库一起备份。
## 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` 默认不包含角色、授权、会话和资源任务表,因此恢复不会覆盖新机器已有登录授权状态;操作审计日志默认随 TMS 库备份。
- 服务端路径类字段不展示给普通用户,只用于运维排查。