baseline: 批次A-D 成果 + membership 半成品(测试红)

This commit is contained in:
agent
2026-09-11 23:11:35 +08:00
commit b3f3095d53
311 changed files with 40540 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
"""消息通知与预警 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})