diff --git a/backend/application/asgi.py b/backend/application/asgi.py index 14aacecf519444552820f61a288c5323f57c0669..37e9f35951344b2b6485b57c7c6623b84bfaea51 100644 --- a/backend/application/asgi.py +++ b/backend/application/asgi.py @@ -8,9 +8,7 @@ https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ """ import os -from channels.auth import AuthMiddlewareStack -from channels.security.websocket import AllowedHostsOriginValidator -from channels.routing import ProtocolTypeRouter, URLRouter +from channels.routing import ProtocolTypeRouter from django.core.asgi import get_asgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') @@ -18,15 +16,6 @@ os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" http_application = get_asgi_application() -from application.routing import websocket_urlpatterns - application = ProtocolTypeRouter({ "http": http_application, - 'websocket': AllowedHostsOriginValidator( - AuthMiddlewareStack( - URLRouter( - websocket_urlpatterns # 指明路由文件是devops/routing.py - ) - ) - ), }) diff --git a/backend/application/routing.py b/backend/application/routing.py deleted file mode 100644 index d4df9f8883c13ef080dfccade1565d7411741772..0000000000000000000000000000000000000000 --- a/backend/application/routing.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- -from django.urls import path -from application.websocketConfig import MegCenter - -websocket_urlpatterns = [ - path('ws//', MegCenter.as_asgi()), # consumers.DvadminWebSocket 是该路由的消费者 -] diff --git a/backend/application/sse_views.py b/backend/application/sse_views.py new file mode 100644 index 0000000000000000000000000000000000000000..f1cbe014c5a2c01773d72ce09ba6e89b72314eb8 --- /dev/null +++ b/backend/application/sse_views.py @@ -0,0 +1,33 @@ +# views.py +import time + +import jwt +from django.http import StreamingHttpResponse + +from application import settings +from dvadmin.system.models import MessageCenterTargetUser +from django.core.cache import cache + + +def event_stream(user_id): + last_sent_time = 0 + + while True: + # 从 Redis 中获取最后数据库变更时间 + last_db_change_time = cache.get('last_db_change_time', 0) + # 只有当数据库发生变化时才检查总数 + if last_db_change_time and last_db_change_time > last_sent_time: + count = MessageCenterTargetUser.objects.filter(users=user_id, is_read=False).count() + yield f"data: {count}\n\n" + last_sent_time = time.time() + + time.sleep(1) + + +def sse_view(request): + token = request.GET.get('token') + decoded = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256']) + user_id = decoded.get('user_id') + response = StreamingHttpResponse(event_stream(user_id), content_type='text/event-stream') + response['Cache-Control'] = 'no-cache' + return response diff --git a/backend/application/urls.py b/backend/application/urls.py index 641b85cf216850c815203e2838b86675b6e43679..d1902fcb8537500888857997144712f55c4ce825 100644 --- a/backend/application/urls.py +++ b/backend/application/urls.py @@ -24,6 +24,7 @@ from rest_framework_simplejwt.views import ( from application import dispatch from application import settings +from application.sse_views import sse_view from dvadmin.system.views.dictionary import InitDictionaryViewSet from dvadmin.system.views.login import ( LoginView, @@ -40,6 +41,7 @@ dispatch.init_system_config() dispatch.init_dictionary() # =========== 初始化系统配置 ================= +permission_classes = [permissions.AllowAny, ] if settings.DEBUG else [permissions.IsAuthenticated, ] schema_view = get_schema_view( openapi.Info( title="Snippets API", @@ -50,7 +52,7 @@ schema_view = get_schema_view( license=openapi.License(name="BSD License"), ), public=True, - permission_classes=(permissions.IsAuthenticated,), + permission_classes=permission_classes, generator_class=CustomOpenAPISchemaGenerator, ) # 前端页面映射 @@ -115,6 +117,8 @@ urlpatterns = ( # 前端页面映射 path('web/', web_view, name='web_view'), path('web/', serve_web_files, name='serve_web_files'), + # sse + path('sse/', sse_view, name='sse'), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + static(settings.STATIC_URL, document_root=settings.STATIC_URL) diff --git a/backend/application/websocketConfig.py b/backend/application/websocketConfig.py deleted file mode 100644 index ab2cd64f3635556b69b56e8d28b663959ddb8f5a..0000000000000000000000000000000000000000 --- a/backend/application/websocketConfig.py +++ /dev/null @@ -1,183 +0,0 @@ -# -*- coding: utf-8 -*- -import urllib - -from asgiref.sync import sync_to_async, async_to_sync -from channels.db import database_sync_to_async -from channels.generic.websocket import AsyncJsonWebsocketConsumer, AsyncWebsocketConsumer -import json - -from channels.layers import get_channel_layer -from jwt import InvalidSignatureError -from rest_framework.request import Request - -from application import settings -from dvadmin.system.models import MessageCenter, Users, MessageCenterTargetUser -from dvadmin.system.views.message_center import MessageCenterTargetUserSerializer -from dvadmin.utils.serializers import CustomModelSerializer - -send_dict = {} - - -# 发送消息结构体 -def set_message(sender, msg_type, msg, unread=0): - text = { - 'sender': sender, - 'contentType': msg_type, - 'content': msg, - 'unread': unread - } - return text - - -# 异步获取消息中心的目标用户 -@database_sync_to_async -def _get_message_center_instance(message_id): - from dvadmin.system.models import MessageCenter - _MessageCenter = MessageCenter.objects.filter(id=message_id).values_list('target_user', flat=True) - if _MessageCenter: - return _MessageCenter - else: - return [] - - -@database_sync_to_async -def _get_message_unread(user_id): - """获取用户的未读消息数量""" - from dvadmin.system.models import MessageCenterTargetUser - count = MessageCenterTargetUser.objects.filter(users=user_id, is_read=False).count() - return count or 0 - - -def request_data(scope): - query_string = scope.get('query_string', b'').decode('utf-8') - qs = urllib.parse.parse_qs(query_string) - return qs - - -class DvadminWebSocket(AsyncJsonWebsocketConsumer): - async def connect(self): - try: - import jwt - self.service_uid = self.scope["url_route"]["kwargs"]["service_uid"] - decoded_result = jwt.decode(self.service_uid, settings.SECRET_KEY, algorithms=["HS256"]) - if decoded_result: - self.user_id = decoded_result.get('user_id') - self.chat_group_name = "user_" + str(self.user_id) - # 收到连接时候处理, - await self.channel_layer.group_add( - self.chat_group_name, - self.channel_name - ) - await self.accept() - # 主动推送消息 - unread_count = await _get_message_unread(self.user_id) - if unread_count == 0: - # 发送连接成功 - await self.send_json(set_message('system', 'SYSTEM', '您已上线')) - else: - await self.send_json( - set_message('system', 'SYSTEM', "请查看您的未读消息~", - unread=unread_count)) - except InvalidSignatureError: - await self.disconnect(None) - - async def disconnect(self, close_code): - # Leave room group - await self.channel_layer.group_discard(self.chat_group_name, self.channel_name) - print("连接关闭") - try: - await self.close(close_code) - except Exception: - pass - - -class MegCenter(DvadminWebSocket): - """ - 消息中心 - """ - - async def receive(self, text_data): - # 接受客户端的信息,你处理的函数 - text_data_json = json.loads(text_data) - message_id = text_data_json.get('message_id', None) - user_list = await _get_message_center_instance(message_id) - for send_user in user_list: - await self.channel_layer.group_send( - "user_" + str(send_user), - {'type': 'push.message', 'json': text_data_json} - ) - - async def push_message(self, event): - """消息发送""" - message = event['json'] - await self.send(text_data=json.dumps(message)) - - -class MessageCreateSerializer(CustomModelSerializer): - """ - 消息中心-新增-序列化器 - """ - class Meta: - model = MessageCenter - fields = "__all__" - read_only_fields = ["id"] - - -def websocket_push(user_id, message): - username = "user_" + str(user_id) - channel_layer = get_channel_layer() - async_to_sync(channel_layer.group_send)( - username, - { - "type": "push.message", - "json": message - } - ) - - -def create_message_push(title: str, content: str, target_type: int = 0, target_user: list = None, target_dept=None, - target_role=None, message: dict = None, request=Request): - if message is None: - message = {"contentType": "INFO", "content": None} - if target_role is None: - target_role = [] - if target_dept is None: - target_dept = [] - data = { - "title": title, - "content": content, - "target_type": target_type, - "target_user": target_user, - "target_dept": target_dept, - "target_role": target_role - } - message_center_instance = MessageCreateSerializer(data=data, request=request) - message_center_instance.is_valid(raise_exception=True) - message_center_instance.save() - users = target_user or [] - if target_type in [1]: # 按角色 - users = Users.objects.filter(role__id__in=target_role).values_list('id', flat=True) - if target_type in [2]: # 按部门 - users = Users.objects.filter(dept__id__in=target_dept).values_list('id', flat=True) - if target_type in [3]: # 系统通知 - users = Users.objects.values_list('id', flat=True) - targetuser_data = [] - for user in users: - targetuser_data.append({ - "messagecenter": message_center_instance.instance.id, - "users": user - }) - targetuser_instance = MessageCenterTargetUserSerializer(data=targetuser_data, many=True, request=request) - targetuser_instance.is_valid(raise_exception=True) - targetuser_instance.save() - for user in users: - username = "user_" + str(user) - unread_count = async_to_sync(_get_message_unread)(user) - channel_layer = get_channel_layer() - async_to_sync(channel_layer.group_send)( - username, - { - "type": "push.message", - "json": {**message, 'unread': unread_count} - } - ) diff --git a/backend/dvadmin/system/apps.py b/backend/dvadmin/system/apps.py index 191aade900f530c1bf4789534d6803934bd31f4c..8302f727a3e8aa8a5d77d545258cea74b9d86387 100644 --- a/backend/dvadmin/system/apps.py +++ b/backend/dvadmin/system/apps.py @@ -4,3 +4,7 @@ from django.apps import AppConfig class SystemConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'dvadmin.system' + + def ready(self): + # 注册信号 + import dvadmin.system.signals # 确保路径正确 diff --git a/backend/dvadmin/system/signals.py b/backend/dvadmin/system/signals.py index 9728228866da6276e3eec14e453a1b4b6cd8aaa0..d00770c9fff03b058432bf96c68f3b9adb630e20 100644 --- a/backend/dvadmin/system/signals.py +++ b/backend/dvadmin/system/signals.py @@ -1,4 +1,10 @@ -from django.dispatch import Signal +import time + +from django.db.models.signals import post_save, post_delete +from django.dispatch import Signal, receiver +from django.core.cache import cache +from dvadmin.system.models import MessageCenterTargetUser + # 初始化信号 pre_init_complete = Signal() detail_init_complete = Signal() @@ -10,3 +16,12 @@ post_tenants_init_complete = Signal() post_tenants_all_init_complete = Signal() # 租户创建完成信号 tenants_create_complete = Signal() + +# 全局变量用于标记最后修改时间 +last_db_change_time = time.time() + + +@receiver(post_save, sender=MessageCenterTargetUser) +@receiver(post_delete, sender=MessageCenterTargetUser) +def update_last_change_time(sender, **kwargs): + cache.set('last_db_change_time', time.time(), timeout=None) # 设置永不超时的键值对 diff --git a/backend/dvadmin/system/views/download_center.py b/backend/dvadmin/system/views/download_center.py index 4fa88bb9dead2d98be618894fd00bed3afe9aa39..4e6b0611aadaf7b51360355450f160ac9d951c92 100644 --- a/backend/dvadmin/system/views/download_center.py +++ b/backend/dvadmin/system/views/download_center.py @@ -44,6 +44,11 @@ class DownloadCenterViewSet(CustomModelViewSet): extra_filter_class = [] def get_queryset(self): + # 判断是否是 Swagger 文档生成阶段,防止报错 + if getattr(self, 'swagger_fake_view', False): + return self.queryset.model.objects.none() + + # 正常请求下的逻辑 if self.request.user.is_superuser: return super().get_queryset() return super().get_queryset().filter(creator=self.request.user) diff --git a/backend/dvadmin/system/views/message_center.py b/backend/dvadmin/system/views/message_center.py index db91b756668b623f541e88ffdf4e0116b1b2ddae..26faa3f3261656af0c31fd6f4a6c84fbd9dfdeb7 100644 --- a/backend/dvadmin/system/views/message_center.py +++ b/backend/dvadmin/system/views/message_center.py @@ -36,7 +36,7 @@ class MessageCenterSerializer(CustomModelSerializer): return serializer.data def get_user_info(self, instance, parsed_query): - if instance.target_type in (1,2,3): + if instance.target_type in (1, 2, 3): return [] users = instance.target_user.all() # You can do what ever you want in here @@ -108,7 +108,7 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer): return serializer.data def get_user_info(self, instance, parsed_query): - if instance.target_type in (1,2,3): + if instance.target_type in (1, 2, 3): return [] users = instance.target_user.all() # You can do what ever you want in here @@ -139,21 +139,6 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer): read_only_fields = ["id"] -def websocket_push(user_id, message): - """ - 主动推送消息 - """ - username = "user_" + str(user_id) - channel_layer = get_channel_layer() - async_to_sync(channel_layer.group_send)( - username, - { - "type": "push.message", - "json": message - } - ) - - class MessageCenterCreateSerializer(CustomModelSerializer): """ 消息中心-新增-序列化器 @@ -182,10 +167,6 @@ class MessageCenterCreateSerializer(CustomModelSerializer): targetuser_instance = MessageCenterTargetUserSerializer(data=targetuser_data, many=True, request=self.request) targetuser_instance.is_valid(raise_exception=True) targetuser_instance.save() - for user in users: - unread_count = MessageCenterTargetUser.objects.filter(users__id=user, is_read=False).count() - websocket_push(user, message={"sender": 'system', "contentType": 'SYSTEM', - "content": '您有一条新消息~', "unread": unread_count}) return data class Meta: @@ -225,10 +206,6 @@ class MessageCenterViewSet(CustomModelViewSet): queryset.save() instance = self.get_object() serializer = self.get_serializer(instance) - # 主动推送消息 - unread_count = MessageCenterTargetUser.objects.filter(users__id=user_id, is_read=False).count() - websocket_push(user_id, message={"sender": 'system', "contentType": 'TEXT', - "content": '您查看了一条消息~', "unread": unread_count}) return DetailResponse(data=serializer.data, msg="获取成功") @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) diff --git a/backend/requirements.txt b/backend/requirements.txt index 2e6a68a1e4aee4a6222f2a459c0ed285deafd9cc..f443f6f09bb90f9ccaf10d05b5dd21f5f953a915 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -7,25 +7,24 @@ djangorestframework==3.15.2 django-restql==0.15.4 django-simple-captcha==0.6.0 django-timezone-field==7.0 -djangorestframework-simplejwt==5.3.1 +djangorestframework_simplejwt==5.4.0 drf-yasg==1.21.7 mysqlclient==2.2.0 pypinyin==0.51.0 ua-parser==0.18.0 pyparsing==3.1.2 openpyxl==3.1.5 -requests==2.32.3 +requests==2.32.4 typing-extensions==4.12.2 tzlocal==5.2 channels==4.1.0 channels-redis==4.2.0 -websockets==11.0.3 user-agents==2.2.0 six==1.16.0 whitenoise==6.7.0 psycopg2==2.9.9 uvicorn==0.30.3 -gunicorn==22.0.0 +gunicorn==23.0.0 gevent==24.2.1 Pillow==10.4.0 pyinstaller==6.9.0 diff --git a/web/.env.development b/web/.env.development index dc36b291b2cdf3991de42c0cb05648f397f5e173..1c3ca5db362e1f7935aded834e08f0a46ed640a6 100644 --- a/web/.env.development +++ b/web/.env.development @@ -2,7 +2,7 @@ ENV = 'development' # 本地环境接口地址 -VITE_API_URL = 'http://127.0.0.1:8001' +VITE_API_URL = 'http://127.0.0.1:8000' # 是否启用按钮权限 VITE_PM_ENABLED = true diff --git a/web/src/App.vue b/web/src/App.vue index c13df045b37daea76ba83127e15361baf2ff0fa5..449b9658eb6209902cbdf96a09a9ebe4cb6b3129 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -35,7 +35,6 @@ const route = useRoute(); const stores = useTagsViewRoutes(); const storesThemeConfig = useThemeConfig(); const { themeConfig } = storeToRefs(storesThemeConfig); -import websocket from '/@/utils/websocket'; const core = useCore(); const router = useRouter(); // 获取版本号 @@ -92,63 +91,5 @@ onMounted(() => { onUnmounted(() => { mittBus.off('openSetingsDrawer', () => {}); }); -// 监听路由的变化,设置网站标题 -watch( - () => route.path, - () => { - other.useTitle(); - other.useFavicon(); - if (!websocket.websocket) { - //websockt 模块 - try { - websocket.init(wsReceive) - } catch (e) { - console.log('websocket错误'); - } - } - }, - { - deep: true, - } -); - -// websocket相关代码 -import { messageCenterStore } from '/@/stores/messageCenter'; -const wsReceive = (message: any) => { - const data = JSON.parse(message.data); - const { unread } = data; - const messageCenter = messageCenterStore(); - messageCenter.setUnread(unread); - if (data.contentType === 'SYSTEM') { - ElNotification({ - title: '系统消息', - message: data.content, - type: 'success', - position: 'bottom-right', - duration: 5000, - }); - } else if (data.contentType === 'Content') { - ElMessageBox.confirm(data.content, data.notificationTitle, { - confirmButtonText: data.notificationButton, - dangerouslyUseHTMLString: true, - cancelButtonText: '关闭', - type: 'info', - closeOnClickModal: false, - }).then(() => { - ElMessageBox.close(); - const path = data.path; - if (route.path === path) { - core.bus.emit('onNewTask', { name: 'onNewTask' }); - } else { - router.push({ path}); - } - }) - .catch(() => {}); - } -}; -onBeforeUnmount(() => { - // 关闭连接 - websocket.close(); -}); diff --git a/web/src/components/tableSelector/index.vue b/web/src/components/tableSelector/index.vue index d827a751c70554113a27942963c25980d6a9d61e..ac7aae7b38b7ed42aff61dbfc73223d3f9bb0589 100644 --- a/web/src/components/tableSelector/index.vue +++ b/web/src/components/tableSelector/index.vue @@ -4,7 +4,6 @@ class="tableSelector" multiple :collapseTags="props.tableConfig.collapseTags" - @remove-tag="removeTag" v-model="data" placeholder="请选择" @visible-change="visibleChange" @@ -29,9 +28,9 @@ max-height="200" height="200" :highlight-current-row="!props.tableConfig.isMultiple" - @selection-change="handleSelectionChange" + @selection-change="handleSelectionChange" @select="handleSelectionChange" - @selectAll="handleSelectionChange" + @selectAll="handleSelectionChange" @current-change="handleCurrentChange" > @@ -59,34 +58,36 @@ diff --git a/web/src/layout/navBars/breadcrumb/userNews.vue b/web/src/layout/navBars/breadcrumb/userNews.vue index 7005547b4606555561e218be55ee20932b956dae..aa1b067d4a97fb871e7ba1d2f17088f70c833036 100644 --- a/web/src/layout/navBars/breadcrumb/userNews.vue +++ b/web/src/layout/navBars/breadcrumb/userNews.vue @@ -2,7 +2,8 @@
{{ $t('message.user.newTitle') }}
- + +