1
0
forked from erp-dev/erp
Files
erpnew/docs/api_v2_mission_api.md
2026-07-01 11:51:13 +08:00

667 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# API v2 Mission 任务模块接口文档
本文档面向前端,描述 `mission` 中立任务模块的 API。
## 基本约定
- Base URL: `/api/v2`
- 认证:所有接口都需要登录。
- 人员身份:后端使用 `request.user.employee` 作为当前员工身份。
- 多商户隔离:所有任务、任务分类、参与者、回应都只允许访问当前员工所属商户的数据。
- 删除能力:不提供任务删除接口;需要结束业务时使用 `cancel` 取消任务。
- 状态字段:普通创建/更新接口不允许修改状态字段,状态变化统一走独立接口。
普通 CRUD 禁止提交这些字段:
| 字段 | 说明 |
|------|------|
| `is_urgent` | 是否紧急,使用 `set-urgent` 接口修改 |
| `is_completed` | 是否完成,由结束回应或 reopen/reject 流程维护 |
| `is_cancelled` | 是否取消,使用 `cancel` 接口修改 |
| `cancelled_by` | 取消人,由后端写入 |
| `cancelled_at` | 取消时间,由后端写入 |
| `rejected_by` | 撤销人,由后端写入 |
| `rejected_at` | 撤销时间,由后端写入 |
说明:
- 本次新增的“未回复提醒配置字段”不属于状态字段,允许通过普通创建/更新接口维护
## content_type 说明
`content_type``content_id` 是可选的"关联业务对象"字段,用于把任务挂靠到系统中的某个具体业务单据或对象上。
### content_type 是什么
`content_type` 是一个整数,对应后端数据库 `django_content_type` 表的主键(`id`)。
该表记录了系统中所有 Django 模型的元信息,每一行代表一个模型,格式为 `app_label` + `model`
**前端不应硬编码 content_type 整数 ID**,因为这个 ID 在不同部署环境中可能不同。
正确做法:调用 `/api/v2/content-types/` 接口(见下方"查询可用 content_type"一节)获取当前环境的实际 ID。
### content_type_label 字段
响应体中的 `content_type_label` 字段是人类可读的模型标识,格式为 `"app_label.model"`,例如:
- `"printing.plateorder"` — 开版单
- `"printing.printingjob"` — 印刷任务
- `"business.purchaseorder"` — 采购单
该字段**只读**,用于前端展示或调试,不作为提交 content_type 时的值。
### 当前系统中可关联的主要业务对象
| `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` | 工艺流程实例 |
> 上述列表是"有意义的"业务关联对象;系统本身不限制可关联的模型类型,技术上任何有效 ContentType ID 都被接受。
### 查询可用 content_type
- URL: `/api/v2/content-types/`
- Method: `GET`
响应示例:
```json
[
{"id": 7, "app_label": "business", "model": "paymentorder", "label": "business.paymentorder", "name": "付款单"},
{"id": 8, "app_label": "business", "model": "presalesorder", "label": "business.presalesorder", "name": "预销售单"},
...
]
```
前端应在运行时调用此接口获取 `id`,不应硬编码,因为不同部署环境的 ID 可能不同。
## 数据结构
### MissionCategory
```json
{
"id": 1,
"merchant": 10,
"name": "通用",
"payload_processor": "",
"speech_enabled": false,
"created_at": "2026-04-10T12:00:00+08:00",
"updated_at": "2026-04-10T12:00:00+08:00"
}
```
### Mission
```json
{
"id": 1,
"merchant": 10,
"description": "跟进客户问题",
"category": 1,
"category_name": "通用",
"is_urgent": false,
"is_completed": false,
"is_cancelled": false,
"notify_if_unreplied": false,
"unreplied_notify_interval_minutes": null,
"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": null,
"content_type_label": null,
"content_id": null,
"extra": {
"source": "wechat",
"ticket_no": "TK-001"
},
"has_ending_reply": false,
"can_reply": true,
"created_at": "2026-04-10T12:00:00+08:00",
"updated_at": "2026-04-10T12:00:00+08:00"
}
```
### MissionReply
```json
{
"id": 100,
"mission": 1,
"merchant": 10,
"responder": {
"id": 20,
"name": "张三",
"merchant_id": 10
},
"content": "已处理",
"extra": {
"attachment_ids": [1001, 1002]
},
"replied_at": "2026-04-10T12:10:00+08:00",
"ends_task": true,
"is_rejected": false,
"rejected_by": null,
"rejected_at": null,
"created_at": "2026-04-10T12:10:00+08:00",
"updated_at": "2026-04-10T12:10:00+08:00"
}
```
### MissionWithReplies
`Mission` 结构基础上,额外返回 `replies` 字段:
```json
{
"id": 1,
"merchant": 10,
"description": "跟进客户问题",
"category": 1,
"category_name": "通用",
"is_urgent": false,
"is_completed": false,
"is_cancelled": false,
"notify_if_unreplied": false,
"unreplied_notify_interval_minutes": null,
"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": [],
"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-04-10T12:00:00+08:00",
"updated_at": "2026-04-10T12:00:00+08:00",
"replies": [
{
"id": 100,
"mission": 1,
"merchant": 10,
"responder": {
"id": 20,
"name": "张三",
"merchant_id": 10
},
"content": "已处理",
"extra": {
"attachment_ids": [1001, 1002]
},
"replied_at": "2026-04-10T12:10:00+08:00",
"ends_task": false,
"is_rejected": false,
"rejected_by": null,
"rejected_at": null,
"created_at": "2026-04-10T12:10:00+08:00",
"updated_at": "2026-04-10T12:10:00+08:00"
}
]
}
```
### MissionLite
用于按印花订单查询任务时的轻量响应结构,不返回参与者、结束回应状态和回复列表:
```json
{
"id": 1,
"merchant": 10,
"description": "跟进客户问题",
"category": 1,
"category_name": "通用",
"is_urgent": false,
"is_completed": false,
"is_cancelled": false,
"notify_if_unreplied": false,
"unreplied_notify_interval_minutes": null,
"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,
"content_type": 33,
"content_type_label": "printing.printingorder",
"content_id": 123,
"extra": {
"source": "printing-order"
},
"created_at": "2026-04-10T12:00:00+08:00",
"updated_at": "2026-04-10T12:00:00+08:00"
}
```
## 任务分类列表
- URL: `/api/v2/mission-categories/`
- Method: `GET`
响应:`MissionCategory[]`
`MissionCategory` 当前包含以下字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | int | 分类 ID |
| `merchant` | int | 所属商户 ID |
| `name` | string | 分类名称 |
| `payload_processor` | string | payload 增强器标识,未启用时为空字符串 |
| `speech_enabled` | boolean | 该分类创建任务时是否触发语音播报;默认 `false` |
| `created_at` | datetime | 创建时间 |
| `updated_at` | datetime | 更新时间 |
## 创建任务分类
- URL: `/api/v2/mission-categories/`
- Method: `POST`
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `name` | string | 是 | 分类名称,同商户下唯一 |
| `payload_processor` | string | 否 | payload 增强器标识;当前可选值:`structured_description_v1` |
| `speech_enabled` | boolean | 否 | 是否启用该分类的任务创建语音播报;默认 `false` |
请求示例:
```json
{
"name": "售后",
"payload_processor": "structured_description_v1",
"speech_enabled": true
}
```
成功响应:`201 Created`,返回 `MissionCategory`
## 任务分类详情
- URL: `/api/v2/mission-categories/<category_id>/`
- Method: `GET`
成功响应:`MissionCategory`
## 更新任务分类
- URL: `/api/v2/mission-categories/<category_id>/`
- Method: `PATCH`
允许更新:
| 参数 | 类型 | 说明 |
|------|------|------|
| `name` | string | 分类名称,同商户下唯一 |
| `payload_processor` | string | payload 增强器标识;传空字符串表示清空 |
| `speech_enabled` | boolean | 是否启用该分类的任务创建语音播报 |
成功响应:`MissionCategory`
## 删除任务分类
- URL: `/api/v2/mission-categories/<category_id>/`
- Method: `DELETE`
说明:
- 如果该分类已经被任务使用,则删除会被拒绝,返回 `400`
成功响应:`204 No Content`
## 任务列表
- URL: `/api/v2/missions/`
- Method: `GET`
查询参数:
| 参数 | 类型 | 说明 |
|------|------|------|
| `category` | int | 任务分类 ID |
| `is_urgent` | boolean | `true` / `false` |
| `is_completed` | boolean | `true` / `false` |
| `is_cancelled` | boolean | `true` / `false` |
| `content_type` | int | Django ContentType ID |
| `content_id` | int | 关联业务对象 ID |
响应:`Mission[]`
## 创建任务
- URL: `/api/v2/missions/`
- Method: `POST`
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `description` | string | 是 | 任务描述 |
| `category` | int | 否 | 任务分类 ID不传时默认使用当前商户下名称为“通用”的分类不存在则自动创建 |
| `content_type` | int/null | 否 | Django ContentType ID必须与 `content_id` 同时提供或同时省略 |
| `content_id` | int/null | 否 | 关联业务对象 ID必须与 `content_type` 同时提供或同时省略 |
| `extra` | object/null | 否 | 任务扩展字段,原样保存为 JSON可传 `null` |
| `notify_if_unreplied` | boolean | 否 | 是否开启“未回复持续提醒”,默认 `false` |
| `unreplied_notify_interval_minutes` | int/null | 否 | 未回复提醒间隔(分钟);开启未回复提醒时必填 |
| `unreplied_notify_max_count` | int | 否 | 最大提醒次数,默认 `5` |
| `participant_ids` | int[] | 否 | 参与者员工 ID 列表,必须属于当前商户 |
说明:
- `creator``merchant` 由当前登录用户的 employee 自动写入
- `category_name` 为只读字段,由后端根据分类表返回
- `is_urgent``is_completed``is_cancelled` 均按默认值创建,不接受请求参数
- 若关联对象存在 `merchant_id` 字段,后端会校验它必须属于当前商户
-`notify_if_unreplied=true`,则必须同时传入 `unreplied_notify_interval_minutes`
- `unreplied_notify_sent_count``unreplied_last_notified_at` 为只读运行时字段,由后端维护
请求示例:
```json
{
"description": "跟进客户问题",
"category": 1,
"extra": {
"source": "wechat",
"ticket_no": "TK-001"
},
"notify_if_unreplied": true,
"unreplied_notify_interval_minutes": 30,
"unreplied_notify_max_count": 5,
"participant_ids": [21, 22]
}
```
成功响应:`201 Created`,返回 `Mission`
## 任务详情
- URL: `/api/v2/missions/<mission_id>/`
- Method: `GET`
成功响应:`Mission`
跨商户访问返回 `404`
## 按印花订单查询任务及回复
- URL: `/api/v2/missions/by-printing-order/<printing_order_id>/`
- Method: `GET`
查询参数:
| 参数 | 类型 | 说明 |
|------|------|------|
| `category_ids` | int[] / comma-separated string | 可选任务分类筛选。支持重复参数 `?category_ids=1&category_ids=2`,也支持逗号分隔 `?category_ids=1,2` |
| `include_details` | boolean | 是否返回完整详情,默认 `true`。传 `false` 时返回轻量结构,不带 `participants``has_ending_reply``can_reply``replies` |
说明:
- 只查询 `content_type=printing.printingorder``content_id=<printing_order_id>` 的任务
- 只返回当前登录员工所属商户下的任务
- `include_details=true` 时返回 `MissionWithReplies[]`
- `include_details=false` 时返回 `MissionLite[]`
- 完整模式下每个任务会内嵌其全部回复,按 `replied_at``id` 升序返回
- `Mission.extra``MissionReply.extra` 都会原样返回
- 该接口启用了 180 秒的低层缓存;缓存键会区分当前商户、`printing_order_id``category_ids``include_details`
- 在缓存有效期内,如果任务、参与者或回复刚发生变化,接口结果最多可能延迟约 3 分钟刷新
- 若该 `printing_order_id` 不属于当前商户,返回 `404`
请求示例:
```http
GET /api/v2/missions/by-printing-order/123/
GET /api/v2/missions/by-printing-order/123/?category_ids=1,2
GET /api/v2/missions/by-printing-order/123/?include_details=false
GET /api/v2/missions/by-printing-order/123/?category_ids=2&include_details=false
```
成功响应:
- `include_details=true``MissionWithReplies[]`
- `include_details=false``MissionLite[]`
## 更新任务
- URL: `/api/v2/missions/<mission_id>/`
- Method: `PATCH`
允许更新:
| 参数 | 类型 | 说明 |
|------|------|------|
| `description` | string | 任务描述 |
| `category` | int | 任务分类 ID |
| `content_type` | int/null | 关联对象类型;必须与 `content_id` 同时提供 |
| `content_id` | int/null | 关联对象 ID必须与 `content_type` 同时提供 |
| `extra` | object/null | 任务扩展字段,原样保存为 JSON可传 `null` |
| `notify_if_unreplied` | boolean | 是否开启“未回复持续提醒” |
| `unreplied_notify_interval_minutes` | int/null | 未回复提醒间隔(分钟) |
| `unreplied_notify_max_count` | int | 最大提醒次数 |
| `participant_ids` | int[] | 重置参与者列表 |
禁止更新状态字段,见“基本约定”
补充说明:
- 更新 `notify_if_unreplied``unreplied_notify_interval_minutes` 时,后端会重置当前任务的未回复提醒计数与上次提醒时间
- 更新 `unreplied_notify_max_count` 不会重置已提醒次数
成功响应:`Mission`
## 删除任务
- URL: `/api/v2/missions/<mission_id>/`
- Method: `DELETE`
当前不提供删除能力,固定返回:
```json
{
"detail": "Mission 删除接口未提供,请使用 cancel 接口取消任务"
}
```
HTTP 状态码:`405 Method Not Allowed`
## 获取任务回应列表
- URL: `/api/v2/missions/<mission_id>/replies/`
- Method: `GET`
查询参数:
| 参数 | 类型 | 说明 |
|------|------|------|
| `ends_task` | boolean | `true` / `false`,筛选是否为结束回应 |
| `is_rejected` | boolean | `true` / `false`,筛选是否已被撤销 |
响应:`MissionReply[]`,按 `replied_at` 升序排列
## 创建任务回应
- URL: `/api/v2/missions/<mission_id>/replies/`
- Method: `POST`
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `content` | string | 是 | 回应内容 |
| `ends_task` | boolean | 否 | 是否结束任务,默认 `false` |
| `extra` | object/null | 否 | 回应扩展字段,原样保存为 JSON可传 `null` |
说明:
- `responder` 使用当前登录用户的 employee
- `extra` 由后端原样存储并原样返回,不做结构校验
- 如果 `ends_task=true`,后端会同步设置 `Mission.is_completed=true`
- 已取消任务、或已有有效结束回应的任务不允许继续回应
成功响应:`201 Created`,返回 `MissionReply`
## 重新打开任务
- URL: `/api/v2/missions/<mission_id>/reopen/`
- Method: `POST`
- 额外权限:`mission.reopen_mission`
请求体可为空:
```json
{}
```
前置条件:
- 当前用户必须具备 `mission.reopen_mission`
- 任务未取消
- 任务已完成
- 任务存在有效的 `ends_task=true` 回应
执行效果:
- `Mission.is_completed=false`
- 相关结束回应被标记为 `ends_task=false`
- 相关结束回应记录 `is_rejected=true``rejected_by=当前员工``rejected_at=当前时间`
成功响应:`Mission`
无权限响应:`403 Forbidden`
## 取消任务
- URL: `/api/v2/missions/<mission_id>/cancel/`
- Method: `POST`
请求体可为空:
```json
{}
```
执行效果:
- `Mission.is_cancelled=true`
- `Mission.cancelled_by=当前员工`
- `Mission.cancelled_at=当前时间`
- 不强行修改 `Mission.is_completed`
- 后端会停止当前任务后续的未回复提醒,并保留任务上的提醒历史字段供查看
成功响应:`Mission`
## 设置紧急状态
- URL: `/api/v2/missions/<mission_id>/set-urgent/`
- Method: `POST`
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `is_urgent` | boolean | 是 | 新的紧急状态 |
请求示例:
```json
{
"is_urgent": true
}
```
成功响应:`Mission`
## 撤销任务回应
- URL: `/api/v2/mission-replies/<reply_id>/reject/`
- Method: `POST`
- 额外权限:`mission.reject_mission_reply`
请求体可为空:
```json
{}
```
执行效果:
- 当前回应标记为 `is_rejected=true`
- 当前回应的 `rejected_by``rejected_at` 由后端写入
- 如果当前回应原本是唯一有效的结束回应,则对应任务会被重新置为未完成
- 如果任务重新回到“没有任何有效回复”的状态,后端会重置未回复提醒计数,并从新的空窗期重新开始计算后续提醒
成功响应:`MissionReply`
无权限响应:`403 Forbidden`
## 未回复提醒规则
当任务开启 `notify_if_unreplied=true` 时,系统会通过每分钟一次的后台定时任务检查是否需要发送“未回复提醒”。
判定规则:
1. 任务开启了未回复提醒
2. 任务未完成
3. 任务未取消
4. 任务当前没有任何有效回复(`is_rejected=false` 的回复)
5. `unreplied_notify_sent_count < unreplied_notify_max_count`
6. 到达提醒时间:
- 从未提醒过:`created_at + unreplied_notify_interval_minutes`
- 已提醒过:`unreplied_last_notified_at + unreplied_notify_interval_minutes`
发送成功后:
- `unreplied_notify_sent_count` 自增 1
- `unreplied_last_notified_at` 更新为本次成功发送时间
收到有效回复后:
- 当前未回复提醒周期会被停止
- `unreplied_notify_sent_count``unreplied_last_notified_at` 会被重置
如果后续因为撤销回复或 reopen 重新回到“无有效回复”状态:
- 系统会把该任务视为一个新的未回复周期重新开始计时