1
0
forked from erp-dev/erp

feat: notifier beta

This commit is contained in:
2026-04-13 12:30:33 +08:00
parent 646dbe7f18
commit 9512132bb9
50 changed files with 17830 additions and 2 deletions

437
docs/api_v2_mission_api.md Normal file
View File

@@ -0,0 +1,437 @@
# 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": "通用",
"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,
"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,
"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": "已处理",
"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"
}
```
## 任务分类列表
- URL: `/api/v2/mission-categories/`
- Method: `GET`
响应:`MissionCategory[]`
## 创建任务分类
- URL: `/api/v2/mission-categories/`
- Method: `POST`
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `name` | string | 是 | 分类名称,同商户下唯一 |
请求示例:
```json
{
"name": "售后"
}
```
成功响应:`201 Created`,返回 `MissionCategory`
## 任务分类详情
- URL: `/api/v2/mission-categories/<category_id>/`
- Method: `GET`
成功响应:`MissionCategory`
## 更新任务分类
- URL: `/api/v2/mission-categories/<category_id>/`
- Method: `PATCH`
允许更新:
| 参数 | 类型 | 说明 |
|------|------|------|
| `name` | string | 分类名称,同商户下唯一 |
成功响应:`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` 同时提供或同时省略 |
| `participant_ids` | int[] | 否 | 参与者员工 ID 列表,必须属于当前商户 |
说明:
- `creator``merchant` 由当前登录用户的 employee 自动写入
- `category_name` 为只读字段,由后端根据分类表返回
- `is_urgent``is_completed``is_cancelled` 均按默认值创建,不接受请求参数
- 若关联对象存在 `merchant_id` 字段,后端会校验它必须属于当前商户
请求示例:
```json
{
"description": "跟进客户问题",
"category": 1,
"participant_ids": [21, 22]
}
```
成功响应:`201 Created`,返回 `Mission`
## 任务详情
- URL: `/api/v2/missions/<mission_id>/`
- Method: `GET`
成功响应:`Mission`
跨商户访问返回 `404`
## 更新任务
- 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` 同时提供 |
| `participant_ids` | int[] | 重置参与者列表 |
禁止更新状态字段,见“基本约定”
成功响应:`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` |
说明:
- `responder` 使用当前登录用户的 employee
- 如果 `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`