# 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//` - Method: `GET` 成功响应:`MissionCategory` ## 更新任务分类 - URL: `/api/v2/mission-categories//` - Method: `PATCH` 允许更新: | 参数 | 类型 | 说明 | |------|------|------| | `name` | string | 分类名称,同商户下唯一 | | `payload_processor` | string | payload 增强器标识;传空字符串表示清空 | | `speech_enabled` | boolean | 是否启用该分类的任务创建语音播报 | 成功响应:`MissionCategory` ## 删除任务分类 - URL: `/api/v2/mission-categories//` - 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//` - Method: `GET` 成功响应:`Mission` 跨商户访问返回 `404` ## 按印花订单查询任务及回复 - URL: `/api/v2/missions/by-printing-order//` - 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=` 的任务 - 只返回当前登录员工所属商户下的任务 - `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//` - 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//` - Method: `DELETE` 当前不提供删除能力,固定返回: ```json { "detail": "Mission 删除接口未提供,请使用 cancel 接口取消任务" } ``` HTTP 状态码:`405 Method Not Allowed` ## 获取任务回应列表 - URL: `/api/v2/missions//replies/` - Method: `GET` 查询参数: | 参数 | 类型 | 说明 | |------|------|------| | `ends_task` | boolean | `true` / `false`,筛选是否为结束回应 | | `is_rejected` | boolean | `true` / `false`,筛选是否已被撤销 | 响应:`MissionReply[]`,按 `replied_at` 升序排列 ## 创建任务回应 - URL: `/api/v2/missions//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//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//cancel/` - Method: `POST` 请求体可为空: ```json {} ``` 执行效果: - `Mission.is_cancelled=true` - `Mission.cancelled_by=当前员工` - `Mission.cancelled_at=当前时间` - 不强行修改 `Mission.is_completed` - 后端会停止当前任务后续的未回复提醒,并保留任务上的提醒历史字段供查看 成功响应:`Mission` ## 设置紧急状态 - URL: `/api/v2/missions//set-urgent/` - Method: `POST` 请求参数: | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `is_urgent` | boolean | 是 | 新的紧急状态 | 请求示例: ```json { "is_urgent": true } ``` 成功响应:`Mission` ## 撤销任务回应 - URL: `/api/v2/mission-replies//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 重新回到“无有效回复”状态: - 系统会把该任务视为一个新的未回复周期重新开始计时