forked from erp-dev/erp
feat: notifier beta
This commit is contained in:
BIN
docs/plant/neutral-task-module-class-diagram.png
Normal file
BIN
docs/plant/neutral-task-module-class-diagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 235 KiB |
146
docs/plant/neutral-task-module-class-diagram.puml
Normal file
146
docs/plant/neutral-task-module-class-diagram.puml
Normal file
@@ -0,0 +1,146 @@
|
||||
@startuml
|
||||
title 中立 mission 及跟进模块类图
|
||||
|
||||
skinparam shadowing false
|
||||
skinparam class {
|
||||
BackgroundColor #F8FBFF
|
||||
BorderColor #4C6A92
|
||||
ArrowColor #4C6A92
|
||||
FontName Noto Sans CJK SC
|
||||
}
|
||||
skinparam note {
|
||||
BackgroundColor #FFF7E6
|
||||
BorderColor #B7791F
|
||||
FontName Noto Sans CJK SC
|
||||
}
|
||||
skinparam defaultFontName Noto Sans CJK SC
|
||||
|
||||
class "Mission\n任务" as Mission {
|
||||
+id: BigAutoField
|
||||
+merchant: Merchant
|
||||
+description: Text
|
||||
+category: CharField
|
||||
+created_at: DateTime
|
||||
+is_urgent: Boolean = false
|
||||
+is_completed: Boolean = false
|
||||
+is_cancelled: Boolean = false
|
||||
+cancelled_at: DateTime? = null
|
||||
+content_type: ContentType? = null
|
||||
+content_id: PositiveBigInteger? = null
|
||||
+content_object: GenericForeignKey?
|
||||
+creator: Employee
|
||||
+cancelled_by: Employee? = null
|
||||
--
|
||||
+participants: QuerySet<Employee>
|
||||
+has_ending_reply: Boolean <<computed>>
|
||||
+can_reply: Boolean <<computed>>
|
||||
+get_participants(): QuerySet<Employee>
|
||||
+filter_participants(...): QuerySet<Employee>
|
||||
+reopen(): Mission
|
||||
}
|
||||
|
||||
class "MissionParticipant\n任务参与者" as MissionParticipant {
|
||||
+id: BigAutoField
|
||||
+merchant: Merchant
|
||||
+mission: Mission
|
||||
+employee: Employee
|
||||
+created_at: DateTime
|
||||
}
|
||||
|
||||
class "MissionReply\n任务回应" as MissionReply {
|
||||
+id: BigAutoField
|
||||
+merchant: Merchant
|
||||
+mission: Mission
|
||||
+responder: Employee
|
||||
+content: Text
|
||||
+replied_at: DateTime
|
||||
+ends_task: Boolean = false
|
||||
+is_rejected: Boolean = false
|
||||
+rejected_by: Employee? = null
|
||||
+rejected_at: DateTime? = null
|
||||
}
|
||||
|
||||
class "Merchant\nbasic_info.Merchant" as Merchant {
|
||||
+id: BigAutoField
|
||||
+name: CharField
|
||||
}
|
||||
|
||||
class "Employee\nbasic_info.Employee" as Employee {
|
||||
+id: BigAutoField
|
||||
+name: CharField
|
||||
+merchant: Merchant
|
||||
}
|
||||
|
||||
class "ContentType\ndjango.contrib.contenttypes" as ContentType {
|
||||
+id: AutoField
|
||||
+app_label: CharField
|
||||
+model: CharField
|
||||
}
|
||||
|
||||
class "Any Business Model\n任意业务模型" as AnyBusinessModel {
|
||||
+id: ...
|
||||
}
|
||||
|
||||
Mission "1" o-- "0..*" MissionParticipant : participants
|
||||
Mission "0..*" --> "1" Merchant : merchant
|
||||
MissionParticipant "0..*" --> "1" Merchant : merchant
|
||||
MissionReply "0..*" --> "1" Merchant : merchant
|
||||
MissionParticipant "0..*" --> "1" Employee : employee
|
||||
Mission "1" --> "1" Employee : creator
|
||||
Mission "0..*" --> "0..1" Employee : cancelled_by
|
||||
Mission "1" o-- "0..*" MissionReply : replies
|
||||
MissionReply "0..*" --> "1" Employee : responder
|
||||
MissionReply "0..*" --> "0..1" Employee : rejected_by
|
||||
Mission "0..*" --> "0..1" ContentType : content_type
|
||||
Mission ..> AnyBusinessModel : content_object\nGenericForeignKey
|
||||
|
||||
note right of Mission
|
||||
content_type 与 content_id 均可为空:
|
||||
- 为空:任务不绑定具体业务对象
|
||||
- 非空:任务可关联系统中任意业务模型
|
||||
|
||||
所有人物字段均关联 basic_info.Employee,
|
||||
不直接关联 request.user / auth.User。
|
||||
|
||||
Mission / MissionParticipant / MissionReply
|
||||
都冗余保存 merchant,用于严格多商户隔离。
|
||||
end note
|
||||
|
||||
note bottom of Mission
|
||||
参与者通过 MissionParticipant 中间表维护。
|
||||
Mission 模型应提供便捷方法用于获取和筛选参与者,
|
||||
但关系本身不直接写成裸 ManyToMany 字段,
|
||||
方便后续扩展参与者状态、角色、加入时间等信息。
|
||||
end note
|
||||
|
||||
note right of MissionReply
|
||||
当任意关联回应 ends_task = true 时:
|
||||
- Mission.has_ending_reply = true
|
||||
- Mission.can_reply = false
|
||||
- 后续不允许继续创建 MissionReply
|
||||
|
||||
创建 ends_task = true 的回应时,
|
||||
本模块 service 同步设置:
|
||||
Mission.is_completed = true
|
||||
end note
|
||||
|
||||
note left of Mission
|
||||
is_cancelled 默认为 false。
|
||||
取消任务时写入:
|
||||
- cancelled_by
|
||||
- cancelled_at
|
||||
end note
|
||||
|
||||
note bottom of MissionReply
|
||||
reopen 独立业务函数的前置条件:
|
||||
- Mission.is_cancelled = false
|
||||
- Mission.is_completed = true
|
||||
- 能查询到 ends_task = true 的 MissionReply
|
||||
|
||||
reopen 执行效果:
|
||||
- Mission.is_completed = false
|
||||
- 对 ends_task = true 的回应设置 ends_task = false
|
||||
- 同时记录该回应 is_rejected = true、
|
||||
rejected_by = reopened_by、rejected_at = now
|
||||
end note
|
||||
@enduml
|
||||
BIN
docs/plant/notifier-module-class-diagram.png
Normal file
BIN
docs/plant/notifier-module-class-diagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 225 KiB |
155
docs/plant/notifier-module-class-diagram.puml
Normal file
155
docs/plant/notifier-module-class-diagram.puml
Normal file
@@ -0,0 +1,155 @@
|
||||
@startuml
|
||||
title Notifier 模块一期类图
|
||||
|
||||
skinparam shadowing false
|
||||
skinparam class {
|
||||
BackgroundColor #F8FBFF
|
||||
BorderColor #4C6A92
|
||||
ArrowColor #4C6A92
|
||||
FontName Noto Sans CJK SC
|
||||
}
|
||||
skinparam note {
|
||||
BackgroundColor #FFF7E6
|
||||
BorderColor #B7791F
|
||||
FontName Noto Sans CJK SC
|
||||
}
|
||||
skinparam defaultFontName Noto Sans CJK SC
|
||||
|
||||
class "Notifier\n通知器配置" as Notifier {
|
||||
+id: BigAutoField
|
||||
+merchant: Merchant
|
||||
+name: CharField
|
||||
+event_key: CharField
|
||||
+channel: CharField
|
||||
+template_key: CharField
|
||||
+is_enabled: Boolean = true
|
||||
+config: JSON
|
||||
+description: Text?
|
||||
+created_at: DateTime
|
||||
+updated_at: DateTime
|
||||
}
|
||||
|
||||
class "Merchant\nbasic_info.Merchant" as Merchant {
|
||||
+id: BigAutoField
|
||||
+name: CharField
|
||||
}
|
||||
|
||||
class "Mission Signals\n领域信号" as MissionSignals <<service>> {
|
||||
+mission_created
|
||||
+mission_replied
|
||||
+mission_completed
|
||||
+mission_reply_rejected
|
||||
+mission_reopened
|
||||
+mission_cancelled
|
||||
}
|
||||
|
||||
class "MissionHandlers\n信号处理器" as MissionHandlers <<service>> {
|
||||
+on_mission_created(...)
|
||||
+on_mission_replied(...)
|
||||
+on_mission_completed(...)
|
||||
+on_mission_reply_rejected(...)
|
||||
+on_mission_reopened(...)
|
||||
+on_mission_cancelled(...)
|
||||
}
|
||||
|
||||
class "NotifierDispatcher\n通知分发器" as NotifierDispatcher <<service>> {
|
||||
+dispatch(event_key, merchant_id, payload)
|
||||
}
|
||||
|
||||
class "TemplateRenderer\n模板渲染器" as TemplateRenderer <<service>> {
|
||||
+render(template_key, payload): dict
|
||||
}
|
||||
|
||||
abstract class "BaseNotifier\n渠道后端抽象" as BaseNotifier {
|
||||
+channel: str
|
||||
+notify(endpoint, event_key, payload, rendered): dict
|
||||
}
|
||||
|
||||
class "WeComWebhookNotifier\n企业微信通知器" as WeComWebhookNotifier {
|
||||
+channel = \"wecom_webhook\"
|
||||
+notify(endpoint, event_key, payload, rendered): dict
|
||||
}
|
||||
|
||||
class "EmailNotifier\n邮件通知器" as EmailNotifier {
|
||||
+channel = \"email\"
|
||||
+notify(endpoint, event_key, payload, rendered): dict
|
||||
}
|
||||
|
||||
class "NotifierRegistry\n通知器注册表" as NotifierRegistry <<service>> {
|
||||
+get(channel): BaseNotifier
|
||||
}
|
||||
|
||||
class "NotifierTask\n异步任务入口" as NotifierTask <<task>> {
|
||||
+dispatch_notification_event(...)
|
||||
}
|
||||
|
||||
MissionSignals ..> MissionHandlers : connect
|
||||
MissionHandlers ..> NotifierTask : on_commit + delay
|
||||
NotifierTask ..> NotifierDispatcher : dispatch(...)
|
||||
NotifierDispatcher --> "0..*" Notifier : query by\nmerchant + event_key + is_enabled
|
||||
NotifierDispatcher ..> TemplateRenderer : render(...)
|
||||
NotifierDispatcher ..> NotifierRegistry : get(channel)
|
||||
NotifierRegistry ..> BaseNotifier
|
||||
BaseNotifier <|-- WeComWebhookNotifier
|
||||
BaseNotifier <|-- EmailNotifier
|
||||
Notifier "0..*" --> "1" Merchant : merchant
|
||||
|
||||
note right of Notifier
|
||||
一期简化设计:
|
||||
- 不拆 Endpoint / Subscription
|
||||
- 直接把 event_key 绑定在 Notifier 对象上
|
||||
- 直接把 template_key 配置在 Notifier 对象上
|
||||
|
||||
适合通过 Django Admin 做:
|
||||
- 添加 / 删除
|
||||
- 启用 / 禁用
|
||||
- 编辑渠道参数
|
||||
end note
|
||||
|
||||
note bottom of Notifier
|
||||
config 为 JSON:
|
||||
企业微信示例可包含:
|
||||
- webhook_key
|
||||
- mentioned_list
|
||||
- mentioned_mobile_list
|
||||
|
||||
同一商户下可配置多个 Notifier,
|
||||
同一个 event_key 可命中多个 Notifier。
|
||||
end note
|
||||
|
||||
note right of TemplateRenderer
|
||||
模板文件不落数据库。
|
||||
Notifier 仅保存 template_key。
|
||||
|
||||
模板集中存放在固定目录,例如:
|
||||
notifier/templates/
|
||||
end note
|
||||
|
||||
note left of MissionHandlers
|
||||
handler 不直接拼企业微信内容,
|
||||
也不直接读取 settings.py 中的静态 key。
|
||||
|
||||
handler 只负责把领域事件
|
||||
转成 event_key + payload,
|
||||
再交给 notifier 模块。
|
||||
end note
|
||||
|
||||
note bottom of NotifierDispatcher
|
||||
一期暂不引入 NotificationDelivery 表。
|
||||
发送结果通过日志记录即可,
|
||||
不做数据库级发送审计。
|
||||
|
||||
风险:
|
||||
- 无法在数据库中重放 / 检索历史发送记录
|
||||
- 排查依赖日志系统
|
||||
end note
|
||||
|
||||
note left of Notifier
|
||||
设计取舍风险:
|
||||
把 template_key 配到 Notifier 对象上,
|
||||
会让“同一渠道目标用于多个事件”时产生配置重复。
|
||||
|
||||
但这能显著降低一期复杂度,
|
||||
且更贴合 Admin 直接维护。
|
||||
end note
|
||||
@enduml
|
||||
Reference in New Issue
Block a user