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

19 KiB
Raw Blame History

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_typecontent_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

响应示例:

[
  {"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

{
  "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

{
  "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

{
  "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 字段:

{
  "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

用于按印花订单查询任务时的轻量响应结构,不返回参与者、结束回应状态和回复列表:

{
  "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

请求示例:

{
  "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 列表,必须属于当前商户

说明:

  • creatormerchant 由当前登录用户的 employee 自动写入
  • category_name 为只读字段,由后端根据分类表返回
  • is_urgentis_completedis_cancelled 均按默认值创建,不接受请求参数
  • 若关联对象存在 merchant_id 字段,后端会校验它必须属于当前商户
  • notify_if_unreplied=true,则必须同时传入 unreplied_notify_interval_minutes
  • unreplied_notify_sent_countunreplied_last_notified_at 为只读运行时字段,由后端维护

请求示例:

{
  "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 时返回轻量结构,不带 participantshas_ending_replycan_replyreplies

说明:

  • 只查询 content_type=printing.printingordercontent_id=<printing_order_id> 的任务
  • 只返回当前登录员工所属商户下的任务
  • include_details=true 时返回 MissionWithReplies[]
  • include_details=false 时返回 MissionLite[]
  • 完整模式下每个任务会内嵌其全部回复,按 replied_atid 升序返回
  • Mission.extraMissionReply.extra 都会原样返回
  • 该接口启用了 180 秒的低层缓存;缓存键会区分当前商户、printing_order_idcategory_idsinclude_details
  • 在缓存有效期内,如果任务、参与者或回复刚发生变化,接口结果最多可能延迟约 3 分钟刷新
  • 若该 printing_order_id 不属于当前商户,返回 404

请求示例:

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=trueMissionWithReplies[]
  • include_details=falseMissionLite[]

更新任务

  • 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_unrepliedunreplied_notify_interval_minutes 时,后端会重置当前任务的未回复提醒计数与上次提醒时间
  • 更新 unreplied_notify_max_count 不会重置已提醒次数

成功响应:Mission

删除任务

  • URL: /api/v2/missions/<mission_id>/
  • Method: DELETE

当前不提供删除能力,固定返回:

{
  "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

请求体可为空:

{}

前置条件:

  • 当前用户必须具备 mission.reopen_mission
  • 任务未取消
  • 任务已完成
  • 任务存在有效的 ends_task=true 回应

执行效果:

  • Mission.is_completed=false
  • 相关结束回应被标记为 ends_task=false
  • 相关结束回应记录 is_rejected=truerejected_by=当前员工rejected_at=当前时间

成功响应:Mission

无权限响应:403 Forbidden

取消任务

  • URL: /api/v2/missions/<mission_id>/cancel/
  • Method: POST

请求体可为空:

{}

执行效果:

  • 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 新的紧急状态

请求示例:

{
  "is_urgent": true
}

成功响应:Mission

撤销任务回应

  • URL: /api/v2/mission-replies/<reply_id>/reject/
  • Method: POST
  • 额外权限:mission.reject_mission_reply

请求体可为空:

{}

执行效果:

  • 当前回应标记为 is_rejected=true
  • 当前回应的 rejected_byrejected_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_countunreplied_last_notified_at 会被重置

如果后续因为撤销回复或 reopen 重新回到“无有效回复”状态:

  • 系统会把该任务视为一个新的未回复周期重新开始计时