@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
