# 数据库备份与恢复

生产数据库(PostgreSQL,Compose 容器)每日备份到阿里云 OSS,与图片存储共用 OSS 基础设施,但使用独立的 `backups/` 前缀与(建议)独立的 RAM 凭证。

## 备份内容与存放

| 内容 | 方式 | OSS key 前缀 |
| --- | --- | --- |
| PostgreSQL 全库 | `pg_dump -Fc`(custom 格式,自带压缩,MVCC 快照无需停机) | `backups/postgres/` |
| 平台 JSON 配置(`apps/backend/data`,排除日志/缓存) | `tar -czf` | `backups/configs/` |

- 每个备份对象附带 `<key>.manifest.json` 清单:大小、sha256、创建时间、数据库版本、已应用迁移列表。没有有效清单的备份默认不会被 `latest` 选择,恢复也会拒绝;只有明确使用 `--allow-unverified` 才兼容历史备份。
- 不备份的内容:图片(已在 OSS)、Caddy 证书(自动重新签发)、日志(`apps/backend/data/logs`)。
- PostgreSQL dump 是数据库内部一致快照,但数据库与 OSS 不共享事务;备份时刻附近的新图片可能只存在一侧。恢复后应按 `file_assets.object_key` 抽查 OSS 对象,并为图片桶开启版本控制/保留策略。
- 服务器本地在 `backups/` 目录保留最近 `DB_BACKUP_KEEP` 份(默认 7),OSS 端过期由桶生命周期规则管理。
- 涉及脚本:`deploy/backup-db.sh`(备份)、`deploy/restore-db.sh`(恢复/演练)、后端镜像内 `dist/db-backup.js`(OSS 传输与校验)。

## 首次开通

以下步骤在阿里云控制台与服务器上各执行一次。

### 1. RAM 授权(建议独立,可直接复用现有凭证)

现有 `STORAGE_OSS_*` 的 RAM 用户拥有整个桶的权限,也能直接跑备份。但备份包含全部订单与用户数据,是最敏感的资产,建议单独创建一个 RAM 用户 `order-site-backup`,只授予 `backups/` 前缀,自定义权限策略:

```json
{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["oss:PutObject", "oss:GetObject"],
      "Resource": ["acs:oss:*:*:你的桶名/backups/*"]
    },
    {
      "Effect": "Allow",
      "Action": ["oss:HeadBucket"],
      "Resource": ["acs:oss:*:*:你的桶名"]
    },
    {
      "Effect": "Allow",
      "Action": ["oss:ListObjects"],
      "Resource": ["acs:oss:*:*:你的桶名"],
      "Condition": {
        "StringLike": { "oss:Prefix": ["backups/", "backups/*"] }
      }
    }
  ]
}
```

为该用户创建 AccessKey,填入服务器 `.env` 的 `DB_BACKUP_OSS_ACCESS_KEY_ID` / `DB_BACKUP_OSS_SECRET_ACCESS_KEY`(不填则回退 `STORAGE_OSS_*`)。若用独立备份桶,同时覆盖 `DB_BACKUP_OSS_ENDPOINT` / `DB_BACKUP_OSS_BUCKET` / `DB_BACKUP_OSS_REGION`。

### 2. 桶生命周期规则(控制存储成本)

在 OSS 控制台为目标桶添加生命周期规则:

- 前缀 `backups/`:最后一次访问 90 天后删除(按需调整,数据库每日一份,90 份足够覆盖一个季度)。
- 建议同时开启版本控制:误删备份时可找回旧版本;给非当前版本配置 7 天过期,避免版本堆积。

### 3. 手动执行一次,验证链路

```bash
bash deploy/backup-db.sh
bash deploy/restore-db.sh --list
bash deploy/restore-db.sh --verify
```

`--verify` 会把最新带有效清单的备份恢复到一个隔离的临时 PostgreSQL 容器(不接入业务网络,不影响线上库),并打印迁移列表与各表行数。建议之后每月执行一次演练——没验证过的备份等于没有备份。

### 4. 配置每日定时备份

```bash
crontab -e
```

```cron
# 每天 03:00 备份数据库与配置到 OSS(目录需先存在,首次开通时脚本已创建)
0 3 * * * cd /srv/order_site && bash deploy/backup-db.sh >> backups/backup.log 2>&1
```

把 `/srv/order_site` 换成实际项目目录。日志追加在 `backups/backup.log`,可用 logrotate 管理;备份失败时 cron 不通知,请定期查看该文件,或后续接入告警 webhook。

## 日常运维

- **部署自动备份**:`bash deploy/ubuntu-deploy.sh` 在每次执行数据库迁移前会自动备份一次;`--reset-db` 会在删除数据卷前抓最后一份旧库备份,并跳过重置后空库备份,避免空库被误选为 latest。备份失败会中止部署,确认为可接受后可用 `--skip-backup` 继续。
- **手动备份**:`bash deploy/backup-db.sh`(可加 `--keep N` 覆盖本地保留份数)。
- **查看备份列表**:`bash deploy/restore-db.sh --list`。
- **恢复演练**:`bash deploy/restore-db.sh --verify`(可加 `--key` 指定备份)。
- **兼容历史备份**:仅在确认对象来源可信时加 `--allow-unverified`,不要作为生产默认参数。

## 恢复

### 场景 A:整库恢复到最新备份(最常用)

```bash
bash deploy/restore-db.sh
```

流程:下载最新备份并强制校验 sha256 → 停 backend/web 并确认已停止 → `pg_restore --clean --if-exists` → 执行迁移补差 → 重启并等待健康检查。任一步失败会尝试恢复原先运行的服务,并明确报告数据库可能处于部分恢复状态。执行前需交互输入 `RESTORE` 确认(非交互终端用 `RESTORE_CONFIRM=1` 或 `--yes`)。

如需同时恢复 `apps/backend/data` 配置:

```bash
bash deploy/restore-db.sh --restore-configs
```

也可以用 `--config-key` 指定配置归档。配置归档会先校验路径,再解包到 data 目录。

### 场景 B:恢复到指定时间点

```bash
bash deploy/restore-db.sh --list
bash deploy/restore-db.sh --key backups/postgres/order_site-20260823-030000.dump
```

### 场景 C:服务器全损(数据卷丢失/换机)

在新服务器完成部署(`docs/部署启动与数据库重建.md`)后,把旧服务器 `.env`(含 OSS 凭证)恢复到新服务器,再执行场景 A。图片不受影响(在 OSS),平台 JSON 配置可从 `backups/configs/order_site-<时间戳>.tar.gz` 解回 `apps/backend/data/`。

### 场景 D:外部数据库(DATABASE_URL 非 postgres 服务)

`backup-db.sh` 与 `restore-db.sh` 会自动识别 `.env` 的 `DATABASE_URL`:指向外部库时,从 postgres 容器经网络直连执行 pg_dump/pg_restore,因此服务器仍需有可用的 postgres 镜像/容器作为客户端。恢复外部库前会额外告警确认目标。

### 恢复后检查

- 打开后台登录、近期订单列表,抽查数据到备份时间点为止。
- `bash deploy/restore-db.sh --list` 确认下一次定时备份已恢复产生新对象。

## 故障排查

| 现象 | 处理 |
| --- | --- |
| `[db-backup] HeadBucket 403` | RAM 策略缺 `oss:HeadBucket`,按上文策略补齐 |
| `ListObjects 403` | 策略缺 `oss:ListObjects` 或 `oss:Prefix` 条件与实际前缀不匹配 |
| `PutObject 403` | `Resource` 里的桶名/前缀与 `.env` 的 `DB_BACKUP_OSS_BUCKET` / `DB_BACKUP_OSS_PREFIX` 不一致 |
| `缺少 OSS 配置` | `.env` 里 `DB_BACKUP_OSS_*` 与 `STORAGE_OSS_*` 都为空;改完后备份脚本用的是镜像内程序,若同时改了代码需重新构建 |
| 下载校验 `sha256 校验失败` | 备份对象被篡改或截断,换更早的备份重试 |
| `pg_restore` 报版本错误 | 不会发生在本编排内(pg_dump/pg_restore 与库同镜像);仅外部库场景需保证客户端 ≥ 服务端大版本 |

## 安全注意

- 备份 RAM 凭证只进服务器 `.env`(已 gitignore),不要提交到仓库。
- 备份桶/前缀保持私有,不要开公共读写;`backups/` 下的对象不要通过前端暴露。
- 若备份桶与图片桶相同,确认图片访问逻辑只读图片前缀,不会枚举 `backups/`。
