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

189
mission/models.py Normal file
View File

@@ -0,0 +1,189 @@
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils import timezone
from flower.common import ModelBase
class MissionCategory(ModelBase):
"""任务分类字典,按商户隔离。"""
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey(
"basic_info.Merchant",
on_delete=models.PROTECT,
related_name="mission_categories",
verbose_name="所属商户",
)
name = models.CharField(max_length=50, verbose_name="分类名称")
def __str__(self):
return self.name
class Meta:
verbose_name = "任务分类"
verbose_name_plural = "任务分类"
ordering = ["id"]
constraints = [
models.UniqueConstraint(fields=["merchant", "name"], name="unique_mission_category_name_per_merchant"),
]
class Mission(ModelBase):
"""中立任务,可选关联到系统中的任意业务对象。"""
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey(
"basic_info.Merchant",
on_delete=models.PROTECT,
related_name="missions",
verbose_name="所属商户",
)
description = models.TextField(verbose_name="任务描述")
category = models.ForeignKey(
MissionCategory,
on_delete=models.PROTECT,
related_name="missions",
verbose_name="任务类型",
)
is_urgent = models.BooleanField(default=False, db_index=True, verbose_name="是否紧急")
is_completed = models.BooleanField(default=False, db_index=True, verbose_name="是否完成")
is_cancelled = models.BooleanField(default=False, db_index=True, verbose_name="是否取消")
cancelled_at = models.DateTimeField(null=True, blank=True, verbose_name="取消时间")
creator = models.ForeignKey(
"basic_info.Employee",
on_delete=models.PROTECT,
related_name="created_missions",
verbose_name="任务创建者",
)
cancelled_by = models.ForeignKey(
"basic_info.Employee",
on_delete=models.PROTECT,
related_name="cancelled_missions",
null=True,
blank=True,
verbose_name="取消人",
)
content_type = models.ForeignKey(
ContentType,
on_delete=models.SET_NULL,
related_name="missions",
null=True,
blank=True,
verbose_name="关联对象类型",
)
content_id = models.PositiveBigIntegerField(null=True, blank=True, db_index=True, verbose_name="关联对象ID")
content_object = GenericForeignKey("content_type", "content_id")
@property
def has_ending_reply(self) -> bool:
return self.replies.filter(ends_task=True, is_rejected=False).exists()
@property
def can_reply(self) -> bool:
return not self.is_cancelled and not self.has_ending_reply
def get_participants(self):
return self.participants.select_related("employee")
def filter_participants(self, **filters):
return self.get_participants().filter(**filters)
@property
def category_name(self) -> str:
return self.category.name
def __str__(self):
return f"Mission #{self.id}"
class Meta:
verbose_name = "任务"
verbose_name_plural = "任务"
permissions = [
("reopen_mission", "Can reopen mission"),
("reject_mission_reply", "Can reject mission reply"),
]
indexes = [
models.Index(fields=["merchant", "content_type", "content_id"]),
models.Index(fields=["merchant", "is_completed", "is_cancelled"]),
]
class MissionParticipant(ModelBase):
"""任务参与员工。"""
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey(
"basic_info.Merchant",
on_delete=models.PROTECT,
related_name="mission_participants",
verbose_name="所属商户",
)
mission = models.ForeignKey(
Mission,
on_delete=models.CASCADE,
related_name="participants",
verbose_name="任务",
)
employee = models.ForeignKey(
"basic_info.Employee",
on_delete=models.PROTECT,
related_name="mission_participations",
verbose_name="参与者",
)
def __str__(self):
return f"{self.mission_id} - {self.employee_id}"
class Meta:
verbose_name = "任务参与者"
verbose_name_plural = "任务参与者"
constraints = [
models.UniqueConstraint(fields=["mission", "employee"], name="unique_mission_participant"),
]
class MissionReply(ModelBase):
"""任务回应记录。"""
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey(
"basic_info.Merchant",
on_delete=models.PROTECT,
related_name="mission_replies",
verbose_name="所属商户",
)
mission = models.ForeignKey(
Mission,
on_delete=models.CASCADE,
related_name="replies",
verbose_name="任务",
)
responder = models.ForeignKey(
"basic_info.Employee",
on_delete=models.PROTECT,
related_name="mission_replies",
verbose_name="回应者",
)
content = models.TextField(verbose_name="回应内容")
replied_at = models.DateTimeField(default=timezone.now, db_index=True, verbose_name="回应时间")
ends_task = models.BooleanField(default=False, db_index=True, verbose_name="是否结束任务")
is_rejected = models.BooleanField(default=False, db_index=True, verbose_name="是否被撤销")
rejected_by = models.ForeignKey(
"basic_info.Employee",
on_delete=models.PROTECT,
related_name="rejected_mission_replies",
null=True,
blank=True,
verbose_name="撤销人",
)
rejected_at = models.DateTimeField(null=True, blank=True, verbose_name="撤销时间")
def __str__(self):
return f"MissionReply #{self.id}"
class Meta:
verbose_name = "任务回应"
verbose_name_plural = "任务回应"
ordering = ["replied_at", "id"]