# Mission 任务模块 API 文档

更新日期：2026-07-03

本文档面向前端，描述 `mission` 中立任务模块的对外 API。当前 mission API 仅在 `api_v2` 提供；已检查 `api_v1/urls.py`，`api_v1` 暂无 mission 相关接口。

## 1. 基本约定

- Base URL: `/api/v2`
- 认证：所有 mission 接口均需要登录。
- 当前员工：后端使用 `request.user.employee` 作为当前业务员工。
- 商户隔离：任务、任务分类、参与者、回应均按当前员工所属 `merchant` 隔离。
- 参与者对象：任务参与者是 `basic_info.Employee`，不是 Django `User`。
- 删除任务：不提供物理删除任务接口；业务结束请使用取消接口。
- 普通创建/更新接口不允许直接修改状态字段，状态变化必须走独立状态接口。

普通创建/更新禁止提交字段：

| 字段 | 说明 |
|---|---|
| `is_urgent` | 是否紧急，使用 `set-urgent` 接口修改 |
| `is_completed` | 是否完成，由结束回应、reopen、reject 流程维护 |
| `is_cancelled` | 是否取消，使用 `cancel` 接口修改 |
| `cancelled_by` | 取消人，由后端写入 |
| `cancelled_at` | 取消时间，由后端写入 |
| `rejected_by` | 回应撤销人，由后端写入 |
| `rejected_at` | 回应撤销时间，由后端写入 |

## 2. API v1 状态

已检查 `api_v1/urls.py` 和 `api_v1/views`，当前没有 mission 模块接口。

如前端需要任务模块，请使用本文档中的 `/api/v2/...` 接口。

## 3. 核心数据结构

### 3.1 EmployeePayload

任务模块中所有人员字段均使用员工对象摘要：

```json
{
  "id": 21,
  "name": "李四",
  "merchant_id": 10
}
```

### 3.2 MissionCategory

```json
{
  "id": 1,
  "merchant": 10,
  "name": "通用",
  "payload_processor": "",
  "speech_enabled": false,
  "created_at": "2026-07-03T10:00:00+08:00",
  "updated_at": "2026-07-03T10:00:00+08:00"
}
```

字段说明：

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | number | 任务分类 ID |
| `merchant` | number | 所属商户 ID |
| `name` | string | 分类名称，同商户内唯一 |
| `payload_processor` | string | payload 增强器，可为空；当前可选 `structured_description_v1` |
| `speech_enabled` | boolean | 是否启用任务播报 |
| `created_at` | string | 创建时间 |
| `updated_at` | string | 更新时间 |

### 3.3 Mission

```json
{
  "id": 1,
  "merchant": 10,
  "description": "请跟进这张生产单",
  "category": 1,
  "category_name": "通用",
  "is_urgent": false,
  "is_completed": false,
  "is_cancelled": false,
  "notify_if_unreplied": true,
  "unreplied_notify_interval_minutes": 30,
  "unreplied_notify_max_count": 5,
  "unreplied_notify_sent_count": 0,
  "unreplied_last_notified_at": null,
  "cancelled_at": null,
  "creator": {
    "id": 20,
    "name": "张三",
    "merchant_id": 10
  },
  "cancelled_by": null,
  "participants": [
    {
      "id": 21,
      "name": "李四",
      "merchant_id": 10
    }
  ],
  "content_type": 33,
  "content_type_label": "printing.printingorder",
  "content_id": 123,
  "extra": {
    "source": "printing-order"
  },
  "has_ending_reply": false,
  "can_reply": true,
  "created_at": "2026-07-03T10:00:00+08:00",
  "updated_at": "2026-07-03T10:00:00+08:00"
}
```

字段说明：

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | number | 任务 ID |
| `merchant` | number | 所属商户 ID |
| `description` | string | 任务描述 |
| `category` | number | MissionCategory.id |
| `category_name` | string | 分类名称 |
| `is_urgent` | boolean | 是否紧急 |
| `is_completed` | boolean | 是否完成 |
| `is_cancelled` | boolean | 是否取消 |
| `notify_if_unreplied` | boolean | 是否开启未回复提醒 |
| `unreplied_notify_interval_minutes` | number/null | 未回复提醒间隔，单位分钟 |
| `unreplied_notify_max_count` | number | 未回复最大提醒次数，默认 5 |
| `unreplied_notify_sent_count` | number | 已发送未回复提醒次数 |
| `unreplied_last_notified_at` | string/null | 上一次未回复提醒时间 |
| `cancelled_at` | string/null | 取消时间 |
| `creator` | EmployeePayload | 创建员工 |
| `cancelled_by` | EmployeePayload/null | 取消员工 |
| `participants` | EmployeePayload[] | 最终展开后的任务参与员工列表 |
| `content_type` | number/null | django_content_type.id |
| `content_type_label` | string/null | 可读模型标识，例如 `printing.printingorder` |
| `content_id` | number/null | 关联对象 ID |
| `extra` | object/null | 扩展字段 |
| `has_ending_reply` | boolean | 是否已有结束任务的有效回应 |
| `can_reply` | boolean | 当前是否允许继续回应 |
| `created_at` | string | 创建时间 |
| `updated_at` | string | 更新时间 |

### 3.4 MissionLite

`MissionLite` 用于轻量查询，字段与 `Mission` 基本一致，但不返回 `participants`、`has_ending_reply`、`can_reply`。

### 3.5 MissionReply

```json
{
  "id": 100,
  "mission": 1,
  "merchant": 10,
  "responder": {
    "id": 20,
    "name": "张三",
    "merchant_id": 10
  },
  "content": "已处理",
  "extra": {
    "attachment_ids": [1001, 1002]
  },
  "replied_at": "2026-07-03T10:10:00+08:00",
  "ends_task": true,
  "is_rejected": false,
  "rejected_by": null,
  "rejected_at": null,
  "created_at": "2026-07-03T10:10:00+08:00",
  "updated_at": "2026-07-03T10:10:00+08:00"
}
```

字段说明：

| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | number | 回应 ID |
| `mission` | number | Mission.id |
| `merchant` | number | 所属商户 ID |
| `responder` | EmployeePayload | 回应员工 |
| `content` | string | 回应内容 |
| `extra` | object/null | 扩展字段 |
| `replied_at` | string | 回应时间 |
| `ends_task` | boolean | 是否结束任务 |
| `is_rejected` | boolean | 是否已被撤销 |
| `rejected_by` | EmployeePayload/null | 撤销员工 |
| `rejected_at` | string/null | 撤销时间 |
| `created_at` | string | 创建时间 |
| `updated_at` | string | 更新时间 |

### 3.6 MissionWithReplies

在 `Mission` 结构基础上额外返回 `replies`：

```json
{
  "id": 1,
  "description": "请跟进这张生产单",
  "participants": [],
  "replies": [
    {
      "id": 100,
      "mission": 1,
      "content": "已处理",
      "ends_task": true,
      "is_rejected": false
    }
  ]
}
```

## 4. 参与者参数规则

任务创建和任务更新支持两类参与者参数：

| 参数 | 类型 | 说明 |
|---|---|---|
| `participant_ids` | number[] | 显式参与员工列表，传 `Employee.id` |
| `employee_type_ids` | number[] | 职位列表，传 `EmployeeType.id`，后端会展开为该职位下的在职员工 |

最终参与者计算规则：

```text
最终参与者 = participant_ids 中的员工 + employee_type_ids 对应职位下的在职员工
```

详细规则：

- `participant_ids` 传的是 `Employee.id`。
- `employee_type_ids` 传的是 `EmployeeType.id`。
- `employee_type_ids` 只允许传当前商户下的职位。
- 职位展开时只加入 `status=ACTIVE` 的员工。
- 重复员工会自动去重。
- 职位下没有员工不会报错。
- 传入不存在或跨商户的 `EmployeeType.id` 会返回 400。
- `PATCH /missions/{mission_id}/` 中，只要传了 `participant_ids` 或 `employee_type_ids`，即表示重新设置最终参与者列表，不是增量追加。
- `PATCH /missions/{mission_id}/` 中，如果两个参数都不传，则不修改原参与者。

清空参与者：

```json
{
  "participant_ids": [],
  "employee_type_ids": []
}
```

## 5. content_type 规则

`content_type` 和 `content_id` 用于把任务挂靠到业务对象上。

- `content_type` 是 `django_content_type.id`。
- `content_id` 是对应业务对象主键。
- 两者必须同时提交或同时省略。
- 前端不应硬编码 `content_type`，不同环境的 ID 可能不同。
- 推荐通过 `GET /api/v2/content-types/` 获取可用值。

响应中的 `content_type_label` 是只读辅助字段，格式为：

```text
app_label.model
```

例如：

```text
printing.printingorder
business.salesorder
```

当前允许前端用于 mission 关联的业务对象：

| content_type_label | 中文说明 |
|---|---|
| `printing.plateorder` | 开版单 |
| `printing.printingorder` | 印刷单 |
| `printing.printingjob` | 印刷任务 |
| `business.purchaseorder` | 采购单 |
| `business.presalesorder` | 预销售单 |
| `business.salesorder` | 销售单 |
| `business.prepurchaseorder` | 预采购单 |
| `business.purchasereturnorder` | 采购退货单 |
| `business.salesreturnorder` | 销售退货单 |
| `business.paymentorder` | 付款单 |
| `business.receiptorder` | 收款单 |
| `stock.transferorder` | 调拨单 |
| `shipment.shipment` | 发货单 |
| `stateflow.process` | 工艺流程实例 |

## 6. 接口列表

### 6.1 查询可用 content_type

```http
GET /api/v2/content-types/
```

响应：`200 OK`

```json
[
  {
    "id": 33,
    "app_label": "printing",
    "model": "printingorder",
    "label": "printing.printingorder",
    "name": "印刷单"
  }
]
```

### 6.2 查询任务分类列表

```http
GET /api/v2/mission-categories/
```

响应：`200 OK`

```json
[
  {
    "id": 1,
    "merchant": 10,
    "name": "通用",
    "payload_processor": "",
    "speech_enabled": false,
    "created_at": "2026-07-03T10:00:00+08:00",
    "updated_at": "2026-07-03T10:00:00+08:00"
  }
]
```

### 6.3 创建任务分类

```http
POST /api/v2/mission-categories/
```

请求体：

```json
{
  "name": "生产跟进",
  "speech_enabled": true,
  "payload_processor": "structured_description_v1"
}
```

请求字段：

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `name` | string | 是 | 分类名称，同商户内唯一 |
| `speech_enabled` | boolean | 否 | 是否启用播报，默认 false |
| `payload_processor` | string | 否 | 可为空；可选 `structured_description_v1` |

响应：`201 Created`，返回 `MissionCategory`。

常见错误：

```json
{"name": ["分类名称已存在"]}
```

### 6.4 查询任务分类详情

```http
GET /api/v2/mission-categories/{category_id}/
```

响应：`200 OK`，返回 `MissionCategory`。

### 6.5 更新任务分类

```http
PATCH /api/v2/mission-categories/{category_id}/
```

请求体：

```json
{
  "name": "售后跟进",
  "speech_enabled": false,
  "payload_processor": ""
}
```

响应：`200 OK`，返回 `MissionCategory`。

### 6.6 删除任务分类

```http
DELETE /api/v2/mission-categories/{category_id}/
```

响应：

- `204 No Content`：删除成功。
- `400 Bad Request`：分类已被任务使用，不能删除。

错误示例：

```json
{"detail": "任务分类已被使用，不能删除"}
```

### 6.7 查询任务列表

```http
GET /api/v2/missions/
```

查询参数：

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `category` | number | 否 | 按任务分类 ID 过滤 |
| `is_urgent` | boolean | 否 | 按是否紧急过滤，支持 `1/0/true/false/yes/no` |
| `is_completed` | boolean | 否 | 按是否完成过滤 |
| `is_cancelled` | boolean | 否 | 按是否取消过滤 |
| `content_type` | number | 否 | 按 content_type ID 过滤 |
| `content_id` | number | 否 | 按关联对象 ID 过滤 |

响应：`200 OK`，返回 `Mission[]`。

```json
[
  {
    "id": 1,
    "description": "请跟进这张生产单",
    "category": 1,
    "category_name": "通用",
    "participants": [],
    "can_reply": true
  }
]
```

### 6.8 创建任务

```http
POST /api/v2/missions/
```

请求体：

```json
{
  "description": "请跟进这张生产单",
  "category": 1,
  "content_type": 33,
  "content_id": 123,
  "extra": {
    "source": "printing-order"
  },
  "participant_ids": [101, 102],
  "employee_type_ids": [3, 4],
  "notify_if_unreplied": true,
  "unreplied_notify_interval_minutes": 30,
  "unreplied_notify_max_count": 5
}
```

请求字段：

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `description` | string | 是 | 任务描述，不能为空 |
| `category` | number | 否 | MissionCategory.id；不传则使用默认“通用”分类 |
| `content_type` | number/null | 否 | django_content_type.id；与 `content_id` 必须同时提供或同时省略 |
| `content_id` | number/null | 否 | 关联业务对象 ID；与 `content_type` 必须同时提供或同时省略 |
| `extra` | object/null | 否 | 扩展字段 |
| `participant_ids` | number[] | 否 | 显式参与员工 ID 列表，传 Employee.id |
| `employee_type_ids` | number[] | 否 | 参与职位 ID 列表，传 EmployeeType.id；展开为在职员工 |
| `notify_if_unreplied` | boolean | 否 | 是否开启未回复提醒，默认 false |
| `unreplied_notify_interval_minutes` | number/null | 否 | 未回复提醒间隔；开启提醒时必填 |
| `unreplied_notify_max_count` | number | 否 | 最大提醒次数，默认 5 |

响应：`201 Created`，返回 `Mission`。

常见错误：

```json
{"detail": "参与者不存在或不属于任务所属商户"}
```

```json
{"detail": "员工职位不存在或不属于任务所属商户"}
```

```json
{"unreplied_notify_interval_minutes": ["开启未回复提醒时必须设置提醒间隔"]}
```

### 6.9 查询任务详情

```http
GET /api/v2/missions/{mission_id}/
```

响应：`200 OK`，返回 `Mission`。

### 6.10 更新任务

```http
PATCH /api/v2/missions/{mission_id}/
```

请求体示例：

```json
{
  "description": "更新后的任务描述",
  "category": 2,
  "extra": {
    "channel": "wechat"
  },
  "participant_ids": [101],
  "employee_type_ids": [3],
  "notify_if_unreplied": true,
  "unreplied_notify_interval_minutes": 20,
  "unreplied_notify_max_count": 8
}
```

可更新字段：

| 字段 | 类型 | 说明 |
|---|---|---|
| `description` | string | 任务描述，不能为空 |
| `category` | number | MissionCategory.id |
| `content_type` | number/null | 与 `content_id` 必须同时提交；可提交 null 清空关联 |
| `content_id` | number/null | 与 `content_type` 必须同时提交；可提交 null 清空关联 |
| `extra` | object/null | 扩展字段；提交 null 可清空 |
| `participant_ids` | number[] | 显式员工参与者列表 |
| `employee_type_ids` | number[] | 按职位展开的参与者列表 |
| `notify_if_unreplied` | boolean | 是否开启未回复提醒 |
| `unreplied_notify_interval_minutes` | number/null | 未回复提醒间隔 |
| `unreplied_notify_max_count` | number | 未回复最大提醒次数 |

响应：`200 OK`，返回 `Mission`。

注意：

- `participant_ids` 和 `employee_type_ids` 表示重新设置最终参与者列表，不是增量追加。
- 如果两个字段都不传，则不修改参与者。
- 若提交 `content_type` 或 `content_id`，两者必须同时出现在请求体中。
- 状态字段不能通过该接口更新。

清空任务关联对象：

```json
{
  "content_type": null,
  "content_id": null
}
```

清空参与者：

```json
{
  "participant_ids": [],
  "employee_type_ids": []
}
```

### 6.11 删除任务

```http
DELETE /api/v2/missions/{mission_id}/
```

不支持。

响应：`405 Method Not Allowed`

```json
{"detail": "Mission 删除接口未提供，请使用 cancel 接口取消任务"}
```

### 6.12 按印刷单查询任务

```http
GET /api/v2/missions/by-printing-order/{printing_order_id}/
```

查询指定印刷单关联的任务。

查询参数：

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `category_ids` | number[]/string | 否 | 分类过滤。支持重复参数或逗号分隔，例如 `?category_ids=1,2` 或 `?category_ids=1&category_ids=2` |
| `include_details` | boolean | 否 | 是否返回详情。默认 true；false 时返回 MissionLite[] |

响应：

- `include_details=true` 或未传：`200 OK`，返回 `MissionWithReplies[]`。
- `include_details=false`：`200 OK`，返回 `MissionLite[]`。

示例：

```http
GET /api/v2/missions/by-printing-order/123/?category_ids=1,2&include_details=true
```

常见错误：

```json
{"include_details": "必须是布尔值"}
```

```json
{"category_ids": "必须是整数 ID 列表"}
```

### 6.13 查询任务回应列表

```http
GET /api/v2/missions/{mission_id}/replies/
```

查询参数：

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ends_task` | boolean | 否 | 按是否结束任务过滤 |
| `is_rejected` | boolean | 否 | 按是否已撤销过滤 |

响应：`200 OK`，返回 `MissionReply[]`。

### 6.14 创建任务回应

```http
POST /api/v2/missions/{mission_id}/replies/
```

请求体：

```json
{
  "content": "已处理完成",
  "ends_task": true,
  "extra": {
    "attachment_ids": [1001, 1002]
  }
}
```

请求字段：

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `content` | string | 是 | 回应内容，不能为空 |
| `ends_task` | boolean | 否 | 是否结束任务，默认 false |
| `extra` | object/null | 否 | 扩展字段 |

响应：`201 Created`，返回 `MissionReply`。

行为说明：

- 回应人固定为当前登录用户关联的 Employee。
- 如果 `ends_task=true`，任务会被标记为完成。
- 已取消任务或已有未撤销结束回应的任务不可继续回应。

### 6.15 重新打开任务

```http
POST /api/v2/missions/{mission_id}/reopen/
```

权限要求：当前用户需要 Django 权限 `mission.reopen_mission`。

响应：`200 OK`，返回 `Mission`。

行为说明：

- 用于重新打开已完成任务。
- 会撤销已有结束任务效果，使任务回到未完成状态。

常见错误：

```json
{"detail": "缺少重新打开任务权限"}
```

### 6.16 取消任务

```http
POST /api/v2/missions/{mission_id}/cancel/
```

响应：`200 OK`，返回 `Mission`。

行为说明：

- 取消人固定为当前登录用户关联的 Employee。
- 取消后 `is_cancelled=true`，`cancelled_by` 和 `cancelled_at` 由后端写入。

### 6.17 设置任务紧急状态

```http
POST /api/v2/missions/{mission_id}/set-urgent/
```

请求体：

```json
{
  "is_urgent": true
}
```

请求字段：

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `is_urgent` | boolean | 是 | 是否紧急 |

响应：`200 OK`，返回 `Mission`。

### 6.18 撤销任务回应

```http
POST /api/v2/mission-replies/{reply_id}/reject/
```

权限要求：当前用户需要 Django 权限 `mission.reject_mission_reply`。

响应：`200 OK`，返回 `MissionReply`。

行为说明：

- 撤销人固定为当前登录用户关联的 Employee。
- 如果撤销的是结束任务回应，任务可能会被重新打开。

常见错误：

```json
{"detail": "缺少撤销任务回应权限"}
```

## 7. 常见状态码

| 状态码 | 场景 |
|---|---|
| `200 OK` | 查询、更新、状态操作成功 |
| `201 Created` | 创建任务、分类、回应成功 |
| `204 No Content` | 删除任务分类成功 |
| `400 Bad Request` | 参数非法、跨商户、业务状态不允许 |
| `401 Unauthorized` | 未登录 |
| `403 Forbidden` | 缺少 `reopen_mission` 或 `reject_mission_reply` 等权限 |
| `404 Not Found` | 对象不存在或不属于当前商户 |
| `405 Method Not Allowed` | 不支持的删除任务操作 |

## 8. 前端接入建议

- 创建/更新任务时，`participant_ids` 使用 `Employee.id`，`employee_type_ids` 使用 `EmployeeType.id`。
- 如果只想按职位添加参与者，可以只传 `employee_type_ids`。
- 如果 PATCH 不想改变参与者，不要传 `participant_ids` 和 `employee_type_ids`。
- 如果 PATCH 想清空参与者，两个字段都传空数组。
- `content_type` 不要硬编码，应从 `/api/v2/content-types/` 获取。
- 普通任务编辑页不要提交状态字段，紧急、取消、重新打开、撤销回应都走独立接口。
- 响应中的 `participants` 是后端最终展开后的员工列表，前端展示以该字段为准。

更新日期：2026-07-03
