# 外部订单产品补图

本文档说明“外部印染订单产品补图”的手动命令、共享逻辑与定时任务配置。

## 背景

系统中有一类 `PrintingJob` 来自外部印染订单，同步后：

- `printing_order.external_order_id` 有值
- `product.image` 为空
- `job.external_raw.YanSe` 或 `job.external_product_name` 中包含可用于外部图片接口查询的名称

为了让产品图片逐步补齐，项目提供：

1. 手动 management command
2. 可复用的共享函数
3. 每小时一次的 Celery 定时任务

## 处理规则

补图任务只处理满足以下条件的数据：

1. `printing_order.external_order_id` 非空
2. `product.image` 为空
3. 能从以下字段提取请求名：
   - 优先：`external_raw.YanSe`
   - 回退：`external_product_name`

并且同一个 `product` 在单次运行中只会处理一次，避免重复请求外部图片接口。

## 共享逻辑

共享逻辑已抽到：

- [api_v1/external_product_image_backfill.py](/home/f/coding/flower/api_v1/external_product_image_backfill.py)

其中：

- `extract_request_name(job)`
- `run_external_product_image_backfill(...)`

management command 与 Celery task 都复用这套逻辑，避免出现双份实现。

## 手动运行

命令位置：

- [api_v1/management/commands/backfill_external_product_images.py](/home/f/coding/flower/api_v1/management/commands/backfill_external_product_images.py)

### 示例

处理指定商户：

```bash
python manage.py backfill_external_product_images --merchant-id 1
```

限制本次最多处理 50 条：

```bash
python manage.py backfill_external_product_images --merchant-id 1 --limit 50
```

只预览、不实际写库：

```bash
python manage.py backfill_external_product_images --merchant-id 1 --dry-run
```

仅处理指定产品：

```bash
python manage.py backfill_external_product_images --product-id 123
```

### 参数

| 参数 | 说明 |
|------|------|
| `--merchant-id` | 仅处理指定商户的产品 |
| `--product-id` | 仅处理指定产品 |
| `--limit` | 最多处理多少条符合条件的产品 |
| `--dry-run` | 仅输出将处理的产品，不实际请求外部 API 或写库 |

## Celery Task

Task 位置：

- [api_v1/tasks.py](/home/f/coding/flower/api_v1/tasks.py)

Task 名称：

- `api_v1.tasks.backfill_external_product_images`

支持参数：

- `merchant_id`
- `product_id`
- `limit`
- `dry_run`

默认建议：

- `limit=500`
- `dry_run=False`

## 定时任务

已接入 `CELERY_BEAT_SCHEDULE`：

- key: `backfill_external_product_images_hourly`
- schedule: `crontab(minute=17)`
- kwargs:
  - `limit=500`
  - `dry_run=False`

也就是说：

- 每小时第 `17` 分钟运行一次
- 采用错峰策略，避免和若干整点/五分钟任务撞在一起

配置位置：

- [flower/settings.py](/home/f/coding/flower/flower/settings.py)

## 输出与结果

无论是 command 还是 task，最终都会产出统一统计结果，包含：

- `scanned`
- `selected`
- `success`
- `failed`
- `skipped_with_image`
- `skipped_without_request_name`
- `skipped_duplicate_product`

Celery task 还会额外带：

- `task_id`

## 风险与建议

1. 这是外部接口依赖任务，若外部图片接口不稳定，`failed` 可能升高
2. 每小时一次已足够，`limit=500` 对“补漏”场景比较合适
3. 如果后续发现积压较多，可以只调大 `limit`，不必提高调度频率
4. 若未来需要追踪失败明细，可考虑像外部印染同步那样补失败落库；当前版本仅记录日志
