mirror of
https://github.com/yyhuni/xingrin.git
synced 2026-01-31 19:53:11 +08:00
- 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
234 lines
6.8 KiB
Python
234 lines
6.8 KiB
Python
"""
|
||
通知系统视图 - 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):
|
||
"""推送通知到 WebSocket(Server 端使用)"""
|
||
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
|
||
)
|