845 lines
24 KiB
Markdown
845 lines
24 KiB
Markdown
# 资源备份恢复前端对接文档
|
||
|
||
## 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 库备份。
|
||
- 服务端路径类字段不展示给普通用户,只用于运维排查。
|