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

24 KiB
Raw Blame History

资源备份恢复前端对接文档

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>

{
  "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

请求头示例:

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 可能直接返回 SUCCESSFAILED。前端保留轮询逻辑即可。

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" 展示恢复失败、phasemessage
重启后查询 latest data.status === "NONE" 展示暂无恢复记录

4. 接口明细

4.1 上传备份包

POST /api/v1/files/upload

请求类型:multipart/form-data

字段 类型 必填 说明
file File 用户选择的 .tmsbak 文件

响应示例:

{
  "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

需要防重放头。

请求体可为空,也可以传备注:

{
  "remark": "硬件更换前备份"
}
字段 类型 必填 说明
remark string 备份备注

响应示例:

{
  "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

响应头示例:

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

用途:备份失败时展示执行上下文。

响应示例:

{
  "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

用途:备份失败或排障时展示详细日志。

响应示例:

{
  "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

请求体:

{
  "fileId": "FILE-20260429-000001"
}
字段 类型 必填 说明
fileId string 上传接口返回的文件 ID

响应示例:

{
  "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=truesignatureValid=truecompatible=trueblockingIssues 为空
  • payloadDecrypted=false 是正常值,预检阶段不解密完整 payload
  • 真正解密、展开 payload、路径白名单校验发生在创建恢复任务阶段
  • payload.enc 的实际加解密由后端通过 PCIe 密码卡 SDF 会话密钥流程完成,前端不需要传递 keyIndex/keyBits/algId

4.8 创建恢复任务

POST /api/v1/resource-restores

需要防重放头。

请求体:

{
  "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 SINGLEDUALQUAD
nodes.node01Ip string 节点 1 IPv4
nodes.node02Ip string DUAL / QUAD 必填 节点 2 IPv4
nodes.node03Ip string QUAD 必填 节点 3 IPv4
nodes.node04Ip string QUAD 必填 节点 4 IPv4

响应示例:

{
  "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

需要防重放头。

响应示例:

{
  "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 重启后,恢复页面展示最近一次恢复结果。

响应示例:

{
  "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:展示恢复失败、phasemessage
  • status=APPLYINGRUNNING:展示恢复仍在执行或等待刷新
  • 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 双节点部署 node01Ipnode02Ip
QUAD 四节点部署 node01Ipnode02Ipnode03Ipnode04Ip

第一版中,节点参数只写入恢复上下文用于审计和排障,不自动改写恢复后的配置文件。

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 类型建议

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 编写。requestmessageconfirmRestoredownloadByUrl 可替换为项目内已有的 axios 封装、弹窗组件和下载工具。

7.1 防重放头

function replayHeaders() {
  return {
    'X-Request-Timestamp': String(Math.floor(Date.now() / 1000)),
    'X-Request-Nonce': crypto.randomUUID()
  };
}

7.2 备份 composable

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

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 重启后最近恢复结果展示

<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 库备份。
  • 服务端路径类字段不展示给普通用户,只用于运维排查。