# 明道云开版暂存开版图上传七牛命令

日期：2026-06-28

## 目标

将 `api_v1.MDYPlateOrderStaging.raw` 中明道云“开版图”字段的原始图片 URL 上传到七牛云，并把上传结果记录到独立审计表和任务状态表：

- 暂存表：`api_mdy_plate_order_staging`
- 审计表：`api_mdy_plate_order_staging_qiniu_image_upload_audit`
- 状态表：`api_mdy_plate_order_staging_qiniu_image_upload_state`
- 处理字段：`raw["62d52f4b8d2972284492dd27"]`
- URL 优先级：`original_file_full_path`、`original_file_path`、`DownloadUrl`、`download_url`、`downloadUrl`
- 目标域名：`image.yuwen.cloud`

命令不会回填或修改 `MDYPlateOrderStaging.raw`。

## 命令

```bash
python manage.py upload_mdy_plate_order_staging_images_to_qiniu
```

建议先在生产/测试副本环境做只读扫描：

```bash
python manage.py upload_mdy_plate_order_staging_images_to_qiniu --dry-run
```

`--dry-run` 不写数据库、不扫描 staging、不下载、不上传，只输出当前 cursor 和运行配置。

## 推荐发布后执行方式

默认命令会持续运行到 staging cursor 扫描完成，并且审计表中没有可处理记录为止：

```bash
python manage.py upload_mdy_plate_order_staging_images_to_qiniu --qps 2
```

命令在 PostgreSQL 上会使用 advisory lock，同一数据库同一时间只允许一个实例运行该命令。

如果需要更保守：

```bash
python manage.py upload_mdy_plate_order_staging_images_to_qiniu --qps 1 --scan-batch-size 10 --upload-batch-size 2 --loop-sleep-seconds 0.5
```

## 重要参数

- `--scan-batch-size N`：每轮最多扫描 N 条 staging，默认 `20`。
- `--upload-batch-size N`：每轮最多上传 N 条审计记录，默认 `5`。
- `--max-upload-attempts N`：最多尝试处理 N 条审计上传记录，达到后退出；用于本地测试/灰度，不传则持续运行到结束。
- `--loop-sleep-seconds N`：每轮结束后的短暂休眠，默认 `0.2` 秒。
- `--qps N`：限制下载/上传速率；默认读取 `MDY_PLATE_ORDER_STAGING_QINIU_UPLOAD_QPS`，没有配置时为 `2`。传 `0` 表示不限速。
- `--reset-cursor`：重置 staging 扫描 cursor，从头重新发现审计记录；已有审计记录不会重复创建。
- `--retry-failed`：重试已经明确失败的审计记录。
- `--retry-stale-processing`：重试长时间停在 `processing` 的审计记录。
- `--stale-after-minutes N`：配合 `--retry-stale-processing`，默认 `120` 分钟。
- `--max-attempts N`：单张图最多尝试次数，默认 `3`；传 `0` 表示不限制。
- `--max-image-size-mb N`：单张图最大下载大小，默认 `100MB`。
- `--spool-max-size-mb N`：单张图内存缓冲上限，默认 `8MB`；超过后自动使用临时文件。
- `--download-chunk-size-mb N`：流式下载块大小，默认 `1MB`。
- `--timeout N`：单次下载超时时间，默认 `30` 秒。
- `--progress-every-loops N`：每多少轮输出一次心跳，默认每轮输出。

## 中断与恢复

状态表保存 staging 扫描游标：

- `last_staging_id`：已经扫描到的 staging id。
- `staging_finished`：staging 是否已经扫描完成。
- `last_heartbeat_at`：命令最近一次心跳。

审计表保存逐图上传状态：

- `success`：已成功，有 `qiniu_key` 和 `qiniu_url`，后续默认跳过。
- `failed`：明确失败，默认跳过；需要传 `--retry-failed` 才会重试。
- `processing`：命令中断可能留下的处理中状态，默认跳过；需要传 `--retry-stale-processing` 才会按陈旧时间重试。
- `skipped`：例如原始 URL 已经是七牛域名，默认跳过。

重试所有明确失败：

```bash
python manage.py upload_mdy_plate_order_staging_images_to_qiniu --retry-failed --qps 1
```

重试中断留下的陈旧 `processing`：

```bash
python manage.py upload_mdy_plate_order_staging_images_to_qiniu --retry-stale-processing --stale-after-minutes 120 --qps 1
```

## 资源占用策略

命令不会对 staging 开长时间全表 iterator。它使用持久 cursor 做 keyset 分批：

1. 每轮只查询 `id > last_staging_id` 的少量 staging，默认 20 条。
2. 从这批 staging 中发现图片并创建 `pending` 审计。
3. 更新状态表 `last_staging_id`。
4. 每轮只消费少量审计记录，默认 5 条。
5. 输出心跳，再短暂 sleep。

每张图片独立处理：

1. 创建或读取一条图片审计记录。
2. 将状态置为 `processing` 并递增 `attempts`。
3. 先查找同 `original_url` 的成功审计；如存在，直接复用已有七牛 URL，不重复下载上传。
4. 如无可复用记录，则流式下载原图。
5. 使用 `SpooledTemporaryFile`：小文件留在内存，超过 `--spool-max-size-mb` 自动落临时文件。
6. 上传到七牛后关闭响应和临时文件。
7. 单张图成功或失败都立即落库，不开启长事务。

因此命令的内存占用主要受 `--scan-batch-size`、`--upload-batch-size`、`--spool-max-size-mb`、`--download-chunk-size-mb` 和单张图片大小影响，不随总记录量线性增长。

## 数据核查 SQL

查看状态分布：

```sql
select status, count(*)
from api_mdy_plate_order_staging_qiniu_image_upload_audit
group by status
order by status;
```

查看最近失败：

```sql
select id, staging_id, mdy_rowid, attachment_index, attempts, original_url, error, updated_at
from api_mdy_plate_order_staging_qiniu_image_upload_audit
where status = 'failed'
order by updated_at desc
limit 50;
```

查看成功但 URL 非目标域名的异常记录：

```sql
select id, staging_id, mdy_rowid, qiniu_url
from api_mdy_plate_order_staging_qiniu_image_upload_audit
where status = 'success'
  and qiniu_url not like 'http://image.yuwen.cloud/%'
  and qiniu_url not like 'https://image.yuwen.cloud/%'
limit 50;
```
