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"]