## API v2：PlateOrder 批量更新（batch-update）

### 1. 概述
用于把**同一份更新数据**（`data`）批量应用到多条开版订单（`PlateOrder`）。

- **一致性**：全成功 / 全失败（任意 id 不存在或校验失败则全部不更新）
- **字段控制**：仅允许更新“白名单字段”（服务端控制，前端不可越权更新）
- **不触发流程副作用**：内部使用 `queryset.update()` 批量写库，不会触发 `save()` 逻辑（例如自动创建/更新流程实例等）
- **支持 dry_run**：只校验与预览，不实际写库

### 2. Endpoint
- **Method**：POST
- **Path**：`/api/v2/plate-orders/batch-update/`
- **Content-Type**：`application/json`

### 3. 权限要求
- 需要登录（`IsAuthenticated`）
- 需要工厂用户（`IsPrintingFactory`）
- 需要权限：`printing.change_plateorder`
- 若 `data` 包含 `is_invalid`：
  - `is_invalid=true` 额外需要 `printing.can_invalidate_plateorder`
  - `is_invalid=false` 额外需要 `printing.can_activate_plateorder`

### 4. 请求参数（Body）

#### 4.1 JSON Schema
```json
{
  "plate_order_ids": [1, 2, 3],
  "data": {
    "urgency_level": "加急",
    "designer": 10
  },
  "dry_run": false
}
```

#### 4.2 字段说明
- **plate_order_ids**：`int[]`（必填）
  - 需要更新的 PlateOrder id 列表
  - 会自动去重（保持首个出现顺序）
  - 任意一个 id 不存在：直接 400（不做部分更新）
- **data**：`object`（必填）
  - 仅包含需要更新的字段键值对（字段必须在“白名单字段”中）
  - 只要 `data` 校验失败：直接 400，且不会更新任何订单
- **dry_run**：`boolean`（可选，默认 `false`）
  - `true`：仅校验、返回匹配到的 id 与原始 data，不写库

### 5. 允许批量更新的字段（白名单）
> 只允许更新以下字段；其它字段（例如 `process`/`business_object`/`created_by`/`plate_image` 等）都会被拒绝。

| 字段名 | 类型 | 示例 | 备注 |
|---|---|---|---|
| original_id | int \| null | 12345 | 克隆来源订单ID |
| design_code | string \| null | "D20251218" | 设计编号 |
| plate_type | string \| null | "首版" | 起版情况 |
| plate_date | datetime \| null | "2025-12-18T10:00:00Z" | 下版时间 |
| plate_method | string \| null | "圆网" | 开版方式 |
| image_name | string \| null | "图A" | 图片名称 |
| plate_notes | string \| null | "注意事项" | 打版注意事项 |
| reprint_reason | string \| null | "复版原因" | 复版原因 |
| urgency_level | string | "正常"/"加急" | 紧急程度 |
| is_invalid | boolean | true/false | 作废/恢复（有额外权限要求） |
| customer | int | 100 | 客户ID（不可为 null） |
| area | string \| null | "广州" | 区域 |
| default_address | string \| null | "XX路" | 默认地址 |
| salesperson | int \| null | 11 | 销售员 Employee ID |
| merchandiser | int \| null | 12 | 跟单员 Employee ID |
| designer | int \| null | 13 | 设计师 Employee ID |
| style_name | string \| null | "S1" | 款号名称 |
| fabric | string \| null | "棉" | 布料 |
| fabric_source | string \| null | "现货" | 布料来源 |
| width | string \| null | "150cm" | 幅宽 |
| production_method | string \| null | "现货" | 做货方式 |
| is_mark_frame | boolean | true/false | 是否套唛架 |
| drawing_rating | string \| null | "A" | 画图评级 |
| color_matching_rating | string \| null | "B" | 调色评级 |
| sample_rating | string \| null | "C" | 套样评级 |
| difficulty_rating | string \| null | "简单" | 难度评级 |
| sample_meter | string \| null | "5米" | 米样（文本） |
| required_sample_meters | number \| null | 12.5 | 客户要求米样米数（Decimal） |
| required_completion_date | datetime \| null | "2025-12-20T00:00:00Z" | 要求完成时间 |
| completion_date | datetime \| null | "2025-12-19T12:00:00Z" | 完成时间 |
| approval_result | string \| null | "通过" | 审批结果 |
| is_ordered | boolean | true/false | 是否已下单 |
| customer_feedback | string \| null | "请改色" | 客户修改意见 |

> datetime 建议使用 ISO 8601 字符串（Django/DRF 默认可解析）。

### 6. 响应

#### 6.1 成功（dry_run=false）
- **HTTP 200**
```json
{
  "detail": "批量更新成功",
  "updated_count": 3,
  "plate_order_ids": [1, 2, 3]
}
```

#### 6.2 成功（dry_run=true）
- **HTTP 200**
```json
{
  "dry_run": true,
  "plate_order_ids": [1, 2, 3],
  "matched_count": 3,
  "data": {
    "urgency_level": "加急",
    "designer": 10
  }
}
```

### 7. 错误返回

#### 7.1 字段不在白名单
- **HTTP 400**
```json
{
  "detail": "不支持批量更新字段: process",
  "allowed_fields": ["..." ]
}
```

#### 7.2 部分 id 不存在
- **HTTP 400**
```json
{
  "detail": "以下 PlateOrder 不存在: 999999"
}
```

#### 7.3 权限不足
- **HTTP 403**
```json
{ "detail": "您没有权限批量更新开版订单" }
```
或（作废/恢复权限不足）：
```json
{ "detail": "您没有权限作废开版订单" }
```

#### 7.4 字段类型/外键校验失败（DRF 校验错误）
- **HTTP 400**（典型结构示例）
```json
{
  "data": {
    "designer": ["Invalid pk \"999\" - object does not exist."]
  }
}
```

### 8. 使用建议
- 批量更新是“同一份 data 应用到多个订单”。如果每个订单需要不同字段值，请拆成多次调用或使用单条更新接口。
