"""消息通知与预警 ViewSets: 1. NotificationViewSet: 查询、标记已读、全部已读 2. AlertRuleViewSet: 预警规则维护、触发全量检测 """ from rest_framework import status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.exceptions import ValidationError from django.db.models import Q from django.utils import timezone from asgiref.sync import sync_to_async from apps.core.viewset import BaseTenantViewSet from .models import Notification, AlertRule from .serializers import NotificationSerializer, AlertRuleSerializer from . import services as notify_services class NotificationViewSet(BaseTenantViewSet): model = Notification serializer_class = NotificationSerializer search_fields = ["title", "content"] async def get_queryset(self): qs = await super().get_queryset() # 仅查全员广播或当前用户的通知 user = self.request.user if user.is_authenticated: qs = qs.filter(Q(recipient__isnull=True) | Q(recipient=user)) else: # Keep the queryset safe even if this view is reused without the # default IsAuthenticated permission. qs = qs.none() return qs @action(detail=True, methods=["post"], url_path="mark-read") async def mark_read(self, request, pk=None): """标为已读。""" # Use the same tenant and recipient scope as list/retrieve. This # makes another user's notification indistinguishable from a missing # notification instead of allowing its state to be changed. queryset = await self.get_queryset() try: notification = await sync_to_async(queryset.get)(pk=pk) except self.model.DoesNotExist: return Response({"detail": "通知不存在"}, status=status.HTTP_404_NOT_FOUND) notification.is_read = True notification.read_at = timezone.now() await sync_to_async(notification.save)(update_fields=["is_read", "read_at"]) return Response({"ok": True, "id": notification.id, "is_read": True}) @action(detail=False, methods=["post"], url_path="mark-all-read") async def mark_all_read(self, request): """全部标为已读。""" tenant = await self.get_tenant() if not tenant: raise ValidationError({"tenant": "无法识别租户"}) user = request.user if not user.is_authenticated: return Response({"ok": True, "updated_count": 0}) updated = await sync_to_async( self.model.objects.filter(tenant=tenant, is_read=False) .filter(Q(recipient__isnull=True) | Q(recipient=user)) .update )(is_read=True, read_at=timezone.now()) return Response({"ok": True, "updated_count": updated}) class AlertRuleViewSet(BaseTenantViewSet): model = AlertRule serializer_class = AlertRuleSerializer search_fields = ["code", "name"] @action(detail=False, methods=["post"], url_path="init-default") async def init_default(self, request): """初始化默认预警规则。""" tenant = await self.get_tenant() if not tenant: raise ValidationError({"tenant": "无法识别租户"}) n = await sync_to_async(notify_services.init_default_alert_rules)(tenant) return Response({"ok": True, "created_count": n}) @action(detail=False, methods=["post"], url_path="run-checks") async def run_checks(self, request): """主动触发全量业务智能预警扫描(库存低限、应收逾期)。""" tenant = await self.get_tenant() if not tenant: raise ValidationError({"tenant": "无法识别租户"}) result = await sync_to_async(notify_services.run_all_alert_checks)(tenant) return Response({"ok": True, "result": result})