Files
xingrin/backend/apps/scan/notifications/views.py
2025-12-12 18:04:57 +08:00

295 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
通知系统视图 - REST API 和测试接口
"""
import logging
from typing import Any
from django.http import JsonResponse
from django.utils import timezone
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from apps.common.pagination import BasePagination
from .models import Notification
from .serializers import NotificationSerializer
from .types import NotificationLevel
from .services import NotificationService, NotificationSettingsService
logger = logging.getLogger(__name__)
def notifications_test(request):
"""
测试通知推送
"""
try:
from .services import create_notification
from django.http import JsonResponse
level_param = request.GET.get('level', NotificationLevel.LOW)
try:
level_choice = NotificationLevel(level_param)
except ValueError:
level_choice = NotificationLevel.LOW
title = request.GET.get('title') or "测试通知"
message = request.GET.get('message') or "这是一条测试通知消息"
# 创建测试通知
notification = create_notification(
title=title,
message=message,
level=level_choice
)
return JsonResponse({
'success': True,
'message': '测试通知已发送',
'notification_id': notification.id
})
except Exception as e:
logger.error(f"发送测试通知失败: {e}")
return JsonResponse({
'success': False,
'error': str(e)
}, status=500)
def build_api_response(
data: Any = None,
*,
message: str = '操作成功',
code: str = '200',
state: str = 'success',
status_code: int = status.HTTP_200_OK
) -> Response:
"""构建统一的 API 响应格式
Args:
data: 响应数据体(可选)
message: 响应消息
code: 响应代码
state: 响应状态success/error
status_code: HTTP 状态码
Returns:
DRF Response 对象
"""
payload = {
'code': code,
'state': state,
'message': message,
}
if data is not None:
payload['data'] = data
return Response(payload, status=status_code)
def _parse_bool(value: str | None) -> bool | None:
"""解析字符串为布尔值
Args:
value: 字符串值,支持 '1', 'true', 'yes' 为 True'0', 'false', 'no' 为 False
Returns:
布尔值,或 None如果无法解析
"""
if value is None:
return None
value = str(value).strip().lower()
if value in {'1', 'true', 'yes'}:
return True
if value in {'0', 'false', 'no'}:
return False
return None
class NotificationCollectionView(APIView):
"""通知列表
支持的方法:
- GET: 获取通知列表(支持分页和过滤)
"""
pagination_class = BasePagination
def get(self, request: Request) -> Response:
"""
获取通知列表
URL: GET /api/notifications/?page=1&pageSize=20&level=info&unread=true
查询参数:
- page: 页码(默认 1
- pageSize: 每页数量(默认 10最大 1000
- level: 通知级别过滤low/medium/high
- unread: 是否未读true/false
返回:
- results: 通知列表
- total: 总记录数
- page: 当前页码
- page_size: 每页大小
- total_pages: 总页数
"""
service = NotificationService()
# 按级别过滤
level_param = request.query_params.get('level')
level_filter = level_param if level_param in NotificationLevel.values else None
# 按已读状态过滤
# unread=true: 仅未读 unread=false: 仅已读 unread=None: 全部
unread_param = _parse_bool(request.query_params.get('unread'))
queryset = service.get_notifications(level=level_filter, unread=unread_param)
# 使用通用分页器
paginator = self.pagination_class()
page_obj = paginator.paginate_queryset(queryset, request)
serializer = NotificationSerializer(page_obj, many=True)
return paginator.get_paginated_response(serializer.data)
class NotificationUnreadCountView(APIView):
"""获取未读通知数量
URL: GET /api/notifications/unread-count/
功能:
- 返回当前未读通知的数量
返回:
- count: 未读通知数量
"""
def get(self, request: Request) -> Response:
"""获取未读通知数量"""
service = NotificationService()
count = service.get_unread_count()
return build_api_response({'count': count}, message='获取未读数量成功')
class NotificationMarkAllAsReadView(APIView):
"""标记全部通知为已读
URL: POST /api/notifications/mark-all-as-read/
功能:
- 将所有未读通知标记为已读
- 更新 read_at 时间戳
返回:
- updated: 更新的通知数量
"""
def post(self, request: Request) -> Response:
"""标记全部通知为已读"""
service = NotificationService()
updated = service.mark_all_as_read()
return build_api_response({'updated': updated}, message='全部标记已读成功')
class NotificationSettingsView(APIView):
"""通知设置 API
URL: /api/settings/notifications/
支持的方法:
- GET: 获取当前通知设置
- PUT: 更新通知设置
"""
def get(self, request: Request) -> Response:
"""获取通知设置"""
service = NotificationSettingsService()
settings = service.get_settings()
return Response(settings)
def put(self, request: Request) -> Response:
"""更新通知设置"""
service = NotificationSettingsService()
settings = service.update_settings(request.data)
return Response({'message': '已保存通知设置', **settings})
# ============================================
# Worker 回调 API
# ============================================
@api_view(['POST'])
@permission_classes([AllowAny]) # Worker 容器无认证,可考虑添加 Token 验证
def notification_callback(request):
"""
接收 Worker 的通知推送请求
Worker 容器无法直接访问 Redis通过此 API 回调让 Server 推送 WebSocket。
POST /api/callbacks/notification/
{
"id": 1,
"category": "scan",
"title": "扫描开始",
"message": "...",
"level": "info",
"created_at": "2025-01-01T00:00:00"
}
"""
try:
data = request.data
# 验证必要字段
required_fields = ['id', 'category', 'title', 'message', 'level', 'created_at']
for field in required_fields:
if field not in data:
return Response(
{'error': f'缺少字段: {field}'},
status=status.HTTP_400_BAD_REQUEST
)
# 推送到 WebSocket
_push_notification_to_websocket(data)
logger.debug(f"回调通知推送成功 - ID: {data['id']}, Title: {data['title']}")
return Response({'status': 'ok'})
except Exception as e:
logger.error(f"回调通知处理失败: {e}", exc_info=True)
return Response(
{'error': str(e)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
def _push_notification_to_websocket(data: dict):
"""推送通知到 WebSocketServer 端使用)"""
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
channel_layer = get_channel_layer()
if channel_layer is None:
logger.warning("Channel Layer 未配置,跳过 WebSocket 推送")
return
# 构造通知数据
ws_data = {
'type': 'notification.message',
'id': data['id'],
'category': data['category'],
'title': data['title'],
'message': data['message'],
'level': data['level'],
'created_at': data['created_at']
}
# 发送到通知组
async_to_sync(channel_layer.group_send)(
'notifications',
ws_data
)