1
0
forked from erp-dev/erp
Files
erpnew/docs/mission_module_design.md
2026-04-13 12:30:33 +08:00

9.8 KiB
Raw Blame History

Mission 模块内部设计说明

本文档面向后端维护者,不作为对外 API 文档。

模块定位

mission 是一个中立任务及跟进模块,用于承载系统内任意业务对象上的任务、参与者和回应。

命名使用 Mission,不使用 Task,主要原因是避免和 Celery task 以及 Python/业务语义中的 task 混淆。

当前模型

核心模型:

  • Mission
  • MissionParticipant
  • MissionReply

Mission

职责:

  • 表示一个任务。
  • 可选关联任意业务对象。
  • 保存任务状态。
  • 保存创建人、取消人、所属商户。

关键设计:

  • merchant 是必填外键,用于严格多商户隔离。
  • creator 指向 basic_info.Employee,不直接关联 auth.User
  • cancelled_by 指向 basic_info.Employee,可空。
  • content_type + content_id 都可空,用 Django GenericForeignKey 表示可选业务关联。
  • has_ending_reply 是计算属性,只统计 ends_task=Trueis_rejected=False 的回应。
  • can_reply 是计算属性:任务未取消,且不存在有效结束回应。

自定义权限:

  • mission.reopen_mission
  • mission.reject_mission_reply

MissionParticipant

职责:

  • 表示任务参与者。

关键设计:

  • 通过中间表维护参与者,而不是裸 ManyToMany。
  • merchant 是冗余必填字段,用于隔离和后续高频查询。
  • (mission, employee) 有唯一约束,避免同一员工重复加入同一任务。

MissionReply

职责:

  • 表示任务回应。

关键设计:

  • responder 指向 basic_info.Employee,不可空。
  • ends_task=True 表示这条回应触发结束任务。
  • is_rejected/rejected_by/rejected_at 记录回应撤销行为。
  • merchant 是冗余必填字段,用于隔离和后续高频查询。

多商户隔离

当前策略是“显式 merchant 冗余 + service/API 双层限制”。

模型层:

  • Mission.merchant
  • MissionParticipant.merchant
  • MissionReply.merchant

API 层:

  • 所有 mission 查询都限制为 merchant=request.user.employee.merchant
  • 所有 reply 操作都限制为当前商户。

Service 层:

  • 校验操作员工属于任务所属商户。
  • 创建/更新关联业务对象时,如果目标对象存在 merchant_id 字段,则要求目标对象商户与任务商户一致。
  • 设置参与者时,所有参与者必须属于任务所属商户。

说明:

  • GenericForeignKey 本身没有数据库级外键约束,因此 service 层需要负责对象存在性和商户一致性校验。
  • MissionParticipant.merchantMissionReply.merchant 是有意冗余字段,后续代码应通过 service 创建和维护,避免绕过造成数据不一致。

Service 层现状

当前 service 函数位于 mission/services.py

公开业务函数:

  • create_mission(...)
  • update_mission(...)
  • set_mission_participants(...)
  • create_mission_reply(...)
  • reopen_mission(...)
  • reject_reply(...)
  • cancel_mission(...)
  • set_mission_urgent(...)

内部辅助函数:

  • _assert_employee_belongs_to_mission(...)
  • _validate_content_object_merchant(...)

create_mission

创建任务。

约定:

  • creator 必须是 Employee
  • merchant 来自 creator.merchant
  • is_urgent/is_completed/is_cancelled 不接受外部参数,按模型默认值创建。
  • 可选设置 content_type/content_id
  • 可选设置参与者列表。
  • 成功提交事务后发送 mission_created 信号。

update_mission

普通任务更新。

允许更新:

  • description
  • category
  • content_type/content_id
  • participant_ids

不负责更新状态字段。

set_mission_participants

重置任务参与者。

约定:

  • 参与者 ID 会去重。
  • 所有参与者必须属于任务所属商户。
  • 未在新列表中的旧参与者会被删除。

create_mission_reply

创建任务回应。

约定:

  • responder 必须属于任务所属商户。
  • 任务必须 can_reply=True
  • 如果 ends_task=True,同步设置 Mission.is_completed=True
  • 成功提交事务后发送 mission_replied 信号。
  • ends_task=True 且任务从未完成变为完成时,同时发送 mission_completed 信号。

reopen_mission

重新打开任务。

API 层需要先校验权限:

  • mission.reopen_mission

Service 层前置条件:

  • 操作员工属于任务所属商户。
  • 任务未取消。
  • 任务已完成。
  • 存在有效的 ends_task=True, is_rejected=False 回应。

执行效果:

  • Mission.is_completed=False
  • 相关结束回应设置为 ends_task=False
  • 相关结束回应记录 is_rejected=Truerejected_by=reopened_byrejected_at=now
  • 成功提交事务后发送 mission_reopened 信号。
  • 被 reopen 撤销的结束回应会发送 mission_reply_rejected 信号,reason="reopen"

reject_reply

撤销任务回应。

API 层需要先校验权限:

  • mission.reject_mission_reply

Service 层前置条件:

  • 操作员工属于任务所属商户。
  • 任务未取消。
  • 回应未被撤销。

执行效果:

  • MissionReply.ends_task=False
  • MissionReply.is_rejected=True
  • MissionReply.rejected_by=rejected_by
  • MissionReply.rejected_at=now
  • 如果被撤销的是结束回应,且任务没有其他有效结束回应,则同步 Mission.is_completed=False
  • 成功提交事务后发送 mission_reply_rejected 信号,reason="reject_reply"

cancel_mission

取消任务。

约定:

  • 操作员工必须属于任务所属商户。
  • 已取消任务不能重复取消。
  • 只设置 is_cancelled/cancelled_by/cancelled_at
  • 不强行修改 is_completed,展示层应让取消状态优先于完成状态。
  • 成功提交事务后发送 mission_cancelled 信号。

set_mission_urgent

设置紧急状态。

约定:

  • 操作员工必须属于任务所属商户。
  • is_urgent 不允许通过普通 CRUD 更新,只能通过独立状态接口更新。

领域信号

信号定义位于 mission/signals.py,空 handler 接入口位于 mission/handlers.py,注册逻辑位于 mission/apps.py

发送原则:

  • 只从 service 层发送,不从 model post_save 自动发送。
  • 使用 transaction.on_commit(...),确保事务提交成功后 handler 才运行。
  • signal 发送异常只记录日志,不反向影响主业务流程。

当前信号:

信号 sender 触发时机 主要 payload
mission_created Mission 任务创建成功 instance, created_by
mission_replied MissionReply 任务回应创建成功 instance, mission, responder
mission_completed Mission 结束回应使任务变为完成 instance, completed_by, reply
mission_reply_rejected MissionReply 回应被撤销 instance, mission, rejected_by, reason
mission_reopened Mission 任务被 reopen instance, reopened_by, rejected_reply_ids
mission_cancelled Mission 任务被取消 instance, cancelled_by

mission_reply_rejected.reason 当前取值:

  • reject_reply:显式调用 reject_reply(...) 撤销回应。
  • reopenreopen_mission(...) 内部撤销结束回应。

API 层现状

API 位于 api_v2/views/mission.py,路由位于 api_v2/urls.py

对外接口文档见:

  • docs/api_v2_mission_api.md

当前 API 风格沿用 api_v2 现有结构:显式 APIView + path(...),未引入 ViewSet/Router。

普通 CRUD

  • GET /api/v2/mission-categories/
  • POST /api/v2/mission-categories/
  • GET /api/v2/mission-categories/<category_id>/
  • PATCH /api/v2/mission-categories/<category_id>/
  • DELETE /api/v2/mission-categories/<category_id>/
  • GET /api/v2/missions/
  • POST /api/v2/missions/
  • GET /api/v2/missions/<mission_id>/
  • PATCH /api/v2/missions/<mission_id>/
  • DELETE /api/v2/missions/<mission_id>/ 返回 405不提供删除能力

状态和 service 对应接口:

  • POST /api/v2/missions/<mission_id>/replies/
  • POST /api/v2/missions/<mission_id>/reopen/
  • POST /api/v2/missions/<mission_id>/cancel/
  • POST /api/v2/missions/<mission_id>/set-urgent/
  • POST /api/v2/mission-replies/<reply_id>/reject/

当前测试

测试文件:

  • mission/tests.py
  • api_v2/test_mission_api.py

覆盖重点:

  • 参与者便捷方法。
  • 创建结束回应时任务自动完成。
  • reopen 撤销结束回应。
  • cancel 记录取消人和取消时间。
  • reject reply 撤销回应并按需要恢复任务未完成。
  • 普通 CRUD 禁止状态字段。
  • 多商户隔离。
  • reopen/reject 权限校验。

当前覆盖率

最近一次统计命令:

docker compose exec -T -e DB_HOST=postgres -e DB_PORT=5432 web \
  uv run coverage run --source=mission manage.py test --keepdb --noinput \
  api_v2.test_mission_api mission

docker compose exec -T web uv run coverage report -m

统计结果:

范围 覆盖率
mission 总体 96%
mission/models.py 95%
mission/services.py 99%
mission/admin.py 100%
mission/tests.py 100%

说明:

  • 当前按 --source=mission 统计,只计算 mission 模块本身,不包含 api_v2/views/mission.py
  • 当前 mission/services.py 覆盖率为 99%,剩余未覆盖为 signal 发送异常保护分支。
  • mission 总体剩余未覆盖主要来自迁移回填函数分支和空的 mission/views.py

已知边界

  • 当前没有 Mission 的前端分页;列表直接返回数组。若任务量增大,应补分页。
  • 当前 MissionCategory 已独立成表Mission 对外返回 category=idcategory_name
  • 当前没有删除能力,取消是唯一业务关闭入口之一。
  • 当前 GenericForeignKey 只做对象存在性与可选商户一致性校验,不做目标对象的复杂业务权限校验。