资源备份流程整理
This commit is contained in:
@@ -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` 默认不包含角色、授权、会话和审计表,因此恢复不会覆盖新机器已有登录授权状态。
|
||||
- 服务端路径类字段不展示给普通用户,只用于运维排查。
|
||||
Reference in New Issue
Block a user