Files
xingrin/backend/apps/scan/notifications/views.py
yyhuni cd5c2b9f11 chore(notifications): remove test notification endpoint
- Remove test notification route from URL patterns
- Delete notifications_test view function and associated logic
- Clean up unused test endpoint that was used for development purposes
- Simplify notification API surface by removing non-production code
2026-01-06 16:57:29 +08:00

234 lines
6.8 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
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 apps.common.response_helpers import success_response, error_response
from apps.common.error_codes import ErrorCodes
from .models import Notification
from .serializers import NotificationSerializer
from .types import NotificationLevel
from .services import NotificationService, NotificationSettingsService
logger = logging.getLogger(__name__)
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 success_response(data={'count': count})
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 success_response(data={'updated': updated})
class NotificationSettingsView(APIView):
"""通知设置 API
URL: /api/settings/notifications/
支持的方法:
- GET: 获取当前通知设置
- PUT: 更新通知设置
"""
def get(self, request: Request) -> Response:
"""获取通知设置"""
service = NotificationSettingsService()
settings = service.get_settings()
return success_response(data=settings)
def put(self, request: Request) -> Response:
"""更新通知设置"""
service = NotificationSettingsService()
settings = service.update_settings(request.data)
return success_response(data=settings)
# ============================================
# Worker 回调 API
# ============================================
@api_view(['POST'])
# 权限由全局 IsAuthenticatedOrPublic 处理,/api/callbacks/* 需要 Worker API Key 认证
def notification_callback(request):
"""
接收 Worker 的通知推送请求
Worker 容器无法直接访问 Redis通过此 API 回调让 Server 推送 WebSocket。
需要 Worker API Key 认证X-Worker-API-Key Header
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 error_response(
code=ErrorCodes.VALIDATION_ERROR,
message=f'Missing field: {field}',
status_code=status.HTTP_400_BAD_REQUEST
)
# 推送到 WebSocket
_push_notification_to_websocket(data)
logger.debug(f"回调通知推送成功 - ID: {data['id']}, Title: {data['title']}")
return success_response(data={'status': 'ok'})
except Exception as e:
logger.error(f"回调通知处理失败: {e}", exc_info=True)
return error_response(
code=ErrorCodes.SERVER_ERROR,
message=str(e),
status_code=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
)