from django.core.exceptions import ValidationError from django.db import models from django.db.models import Q from flower.common import ModelBase class NotifierChannelEnum(models.TextChoices): WECOM_WEBHOOK = "wecom_webhook", "企业微信机器人" MESSAGE_API = "message_api", "消息发送 API" JPUSH = "jpush", "极光推送" class NotificationEventKeyEnum(models.TextChoices): MISSION_CREATED = "mission.created", "任务已创建" MISSION_REPLIED = "mission.replied", "任务有新回应" MISSION_COMPLETED = "mission.completed", "任务已完成" MISSION_UNREPLIED = "mission.unreplied", "任务未回复提醒" MISSION_REPLY_REJECTED = "mission.reply_rejected", "任务回应已撤销" MISSION_REOPENED = "mission.reopened", "任务已重新打开" MISSION_CANCELLED = "mission.cancelled", "任务已取消" class Notifier(ModelBase): id = models.BigAutoField(primary_key=True) merchant = models.ForeignKey( "basic_info.Merchant", on_delete=models.PROTECT, related_name="notifiers", verbose_name="所属商户", ) name = models.CharField(max_length=100, verbose_name="通知器名称") channel = models.CharField( max_length=50, choices=NotifierChannelEnum.choices, default=NotifierChannelEnum.WECOM_WEBHOOK, verbose_name="通知渠道", ) template_key = models.CharField(max_length=100, verbose_name="模板标识") is_enabled = models.BooleanField(default=True, db_index=True, verbose_name="是否启用") config = models.JSONField(default=dict, blank=True, verbose_name="渠道配置") description = models.TextField(blank=True, null=True, verbose_name="备注描述") def get_template_name(self) -> str: suffix = "json" if self.channel in {NotifierChannelEnum.MESSAGE_API, NotifierChannelEnum.JPUSH} else "md" return f"notifier/events/{self.template_key}.{suffix}" def get_config_value(self, key: str, default=None): config = self.config or {} return config.get(key, default) def __str__(self): return self.name class Meta: verbose_name = "通知器" verbose_name_plural = "通知器" constraints = [ models.UniqueConstraint(fields=["merchant", "name"], name="unique_notifier_name_per_merchant"), ] indexes = [ models.Index(fields=["merchant", "channel", "is_enabled"], name="notifier_no_merchan_532b51_idx"), ] class NotifierRoute(ModelBase): id = models.BigAutoField(primary_key=True) merchant = models.ForeignKey( "basic_info.Merchant", on_delete=models.PROTECT, related_name="notifier_routes", verbose_name="所属商户", ) notifier = models.ForeignKey( Notifier, on_delete=models.CASCADE, related_name="routes", verbose_name="通知器", ) event_key = models.CharField( max_length=100, choices=NotificationEventKeyEnum.choices, db_index=True, verbose_name="事件标识", ) mission_category = models.ForeignKey( "mission.MissionCategory", on_delete=models.PROTECT, related_name="notifier_routes", null=True, blank=True, verbose_name="任务分类路由", help_text="为空时表示该事件的通配路由", ) is_enabled = models.BooleanField(default=True, db_index=True, verbose_name="是否启用") description = models.TextField(blank=True, null=True, verbose_name="备注描述") def clean(self): errors = {} if self.notifier_id and self.merchant_id and self.notifier.merchant_id != self.merchant_id: errors["merchant"] = "路由所属商户必须与通知器所属商户一致" if self.mission_category_id and self.merchant_id and self.mission_category.merchant_id != self.merchant_id: errors["mission_category"] = "任务分类必须属于当前路由商户" if errors: raise ValidationError(errors) def __str__(self): category_name = self.mission_category.name if self.mission_category_id else "全部分类" return f"{self.notifier.name} -> {self.event_key} [{category_name}]" class Meta: verbose_name = "通知路由" verbose_name_plural = "通知路由" constraints = [ models.UniqueConstraint( fields=["notifier", "event_key", "mission_category"], name="unique_notifier_route_per_scope", ), models.UniqueConstraint( fields=["notifier", "event_key"], condition=Q(mission_category__isnull=True), name="unique_notifier_route_global_scope", ), ] indexes = [ models.Index(fields=["merchant", "event_key", "is_enabled"], name="notifier_no_merchan_58d463_idx"), models.Index(fields=["merchant", "mission_category", "is_enabled"], name="notifier_no_merchan_14b7f6_idx"), ]