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/settings.py b/backend/application/settings.py index 1d0adf79b43c695cbb576518c213ea2837303a07..71ed7d53b0c1b29c413fcfffeab29be5d5ea1868 100644 --- a/backend/application/settings.py +++ b/backend/application/settings.py @@ -399,8 +399,12 @@ DICTIONARY_CONFIG = {} # ================================================= # # 租户共享app TENANT_SHARED_APPS = [] +# 普通租户独有app +TENANT_EXCLUSIVE_APPS = [] # 插件 urlpatterns PLUGINS_URL_PATTERNS = [] +# 所有模式有的 +SHARED_APPS = [] # ********** 一键导入插件配置开始 ********** # 例如: # from dvadmin_upgrade_center.settings import * # 升级中心 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/del_migrations.py b/backend/del_migrations.py index d649e051d6f6294f61630d7ef54c06e83519969d..775d1dcccaae550bc24fa895dda6bfff886e5a0f 100644 --- a/backend/del_migrations.py +++ b/backend/del_migrations.py @@ -2,7 +2,7 @@ import os -exclude = ["venv"] # 需要排除的文件目录 +exclude = ["venv", ".venv"] # 需要排除的文件目录 for root, dirs, files in os.walk('.'): dirs[:] = list(set(dirs) - set(exclude)) if 'migrations' in dirs: 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/fixtures/init_dictionary.json b/backend/dvadmin/system/fixtures/init_dictionary.json index f750c4074c0ad4c5aa54d85ed5982e562469c005..c4bd186dc70b1d3a914aef7949cf61f6b65b97f2 100644 --- a/backend/dvadmin/system/fixtures/init_dictionary.json +++ b/backend/dvadmin/system/fixtures/init_dictionary.json @@ -546,5 +546,50 @@ "children": [] } ] + }, + { + "label": "文件存储引擎", + "value": "file_engine", + "type": 0, + "color": null, + "is_value": false, + "status": true, + "sort": 9, + "remark": null, + "children": [ + { + "label": "本地", + "value": "local", + "type": 0, + "color": "primary", + "is_value": true, + "status": true, + "sort": 1, + "remark": null, + "children": [] + }, + { + "label": "阿里云oss", + "value": "oss", + "type": 0, + "color": "success", + "is_value": true, + "status": true, + "sort": 2, + "remark": null, + "children": [] + }, + { + "label": "腾讯cos", + "value": "cos", + "type": 0, + "color": "warning", + "is_value": true, + "status": true, + "sort": 3, + "remark": null, + "children": [] + } + ] } ] \ No newline at end of file diff --git a/backend/dvadmin/system/fixtures/init_systemconfig.json b/backend/dvadmin/system/fixtures/init_systemconfig.json index 98c95cd7088a78c088e566e489b9bef32ef1ebdf..cc692f257d3e4747529ec7582f64653265c1e3e4 100644 --- a/backend/dvadmin/system/fixtures/init_systemconfig.json +++ b/backend/dvadmin/system/fixtures/init_systemconfig.json @@ -235,5 +235,252 @@ "children": [] } ] - } + }, + { + "title": "文件存储配置", + "key": "file_storage", + "value": null, + "sort": 0, + "status": true, + "data_options": null, + "form_item_type": 0, + "rule": null, + "placeholder": null, + "setting": null, + "children": [ + { + "title": "存储引擎", + "key": "file_engine", + "value": "local", + "sort": 1, + "status": true, + "data_options": null, + "form_item_type": 4, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请选择存储引擎", + "setting": "file_engine", + "children": [] + }, + { + "title": "文件是否备份", + "key": "file_backup", + "value": false, + "sort": 2, + "status": true, + "data_options": null, + "form_item_type": 9, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "启用云存储时,文件是否备份到本地", + "setting": null, + "children": [] + }, + { + "title": "阿里云-AccessKey", + "key": "aliyun_access_key", + "value": null, + "sort": 3, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入AccessKey", + "setting": null, + "children": [] + }, + { + "title": "阿里云-Secret", + "key": "aliyun_access_secret", + "value": null, + "sort": 4, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入Secret", + "setting": null, + "children": [] + }, + { + "title": "阿里云-Endpoint", + "key": "aliyun_endpoint", + "value": null, + "sort": 5, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入Endpoint", + "setting": null, + "children": [] + }, + { + "title": "阿里云-上传路径", + "key": "aliyun_path", + "value": "/media/", + "sort": 5, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入上传路径", + "setting": null, + "children": [] + }, + { + "title": "阿里云-Bucket", + "key": "aliyun_bucket", + "value": null, + "sort": 7, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入Bucket", + "setting": null, + "children": [] + },{ + "title": "阿里云-cdn地址", + "key": "aliyun_cdn_url", + "value": null, + "sort": 7, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入cdn地址", + "setting": null, + "children": [] + }, + { + "title": "腾讯云-SecretId", + "key": "tencent_secret_id", + "value": null, + "sort": 8, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入SecretId", + "setting": null, + "children": [] + }, + { + "title": "腾讯云-SecretKey", + "key": "tencent_secret_key", + "value": null, + "sort": 9, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入SecretKey", + "setting": null, + "children": [] + }, + { + "title": "腾讯云-Region", + "key": "tencent_region", + "value": null, + "sort": 10, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入Region", + "setting": null, + "children": [] + }, + { + "title": "腾讯云-Bucket", + "key": "tencent_bucket", + "value": null, + "sort": 11, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入Bucket", + "setting": null, + "children": [] + }, + { + "title": "腾讯云-上传路径", + "key": "tencent_path", + "value": "/media/", + "sort": 12, + "status": false, + "data_options": null, + "form_item_type": 0, + "rule": [ + { + "required": false, + "message": "必填项不能为空" + } + ], + "placeholder": "请输入上传路径", + "setting": null, + "children": [] + } + ] + } ] \ No newline at end of file diff --git a/backend/dvadmin/system/management/commands/generate_init_json.py b/backend/dvadmin/system/management/commands/generate_init_json.py index 6ce83b00ba34877483f421d20a72f119ca38e748..e0de3e346891770fdd76299718360a17ce2c7093 100644 --- a/backend/dvadmin/system/management/commands/generate_init_json.py +++ b/backend/dvadmin/system/management/commands/generate_init_json.py @@ -29,7 +29,7 @@ class Command(BaseCommand): def serializer_data(self, serializer, query_set: QuerySet): serializer = serializer(query_set, many=True) data = json.loads(json.dumps(serializer.data, ensure_ascii=False)) - with open(os.path.join(BASE_DIR, f'init_{query_set.model._meta.model_name}.json'), 'w') as f: + with open(os.path.join(BASE_DIR, f'init_{query_set.model._meta.model_name}.json'), 'w',encoding='utf-8') as f: json.dump(data, f, indent=4, ensure_ascii=False) return diff --git a/backend/dvadmin/system/models.py b/backend/dvadmin/system/models.py index c70f2561b6ff5d6a007d996b1fdc688c367a68f7..b6fe27980ea63fdf6ddf8b179ba03313e5a2138b 100644 --- a/backend/dvadmin/system/models.py +++ b/backend/dvadmin/system/models.py @@ -9,8 +9,8 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError from application import dispatch from dvadmin.utils.models import CoreModel, table_prefix, get_custom_app_models -from dvadmin3_flow.base_model import FlowBaseModel -class Role(CoreModel,FlowBaseModel): + +class Role(CoreModel): name = models.CharField(max_length=64, verbose_name="角色名称", help_text="角色名称") key = models.CharField(max_length=64, unique=True, verbose_name="权限字符", help_text="权限字符") sort = models.IntegerField(default=1, verbose_name="角色顺序", help_text="角色顺序") @@ -77,7 +77,13 @@ class Users(CoreModel, AbstractUser): objects = CustomUserManager() def set_password(self, raw_password): - super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest()) + if raw_password: + super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest()) + + def save(self, *args, **kwargs): + if self.name == "": + self.name = self.username + super().save(*args, **kwargs) class Meta: db_table = table_prefix + "system_users" 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/file_list.py b/backend/dvadmin/system/views/file_list.py index c0fed8d289efb3bdb018edc10c73aeef5b4d4276..a155ea1133009c85763ad061d33ecda9459f2c9c 100644 --- a/backend/dvadmin/system/views/file_list.py +++ b/backend/dvadmin/system/views/file_list.py @@ -35,8 +35,8 @@ class FileSerializer(CustomModelSerializer): fields = "__all__" def create(self, validated_data): - file_engine = dispatch.get_system_config_values("fileStorageConfig.file_engine") or 'local' - file_backup = dispatch.get_system_config_values("fileStorageConfig.file_backup") + file_engine = dispatch.get_system_config_values("file_storage.file_engine") or 'local' + file_backup = dispatch.get_system_config_values("file_storage.file_backup") file = self.initial_data.get('file') file_size = file.size validated_data['name'] = str(file) @@ -52,15 +52,15 @@ class FileSerializer(CustomModelSerializer): if file_backup: validated_data['url'] = file if file_engine == 'oss': - from dvadmin_cloud_storage.views.aliyun import ali_oss_upload - file_path = ali_oss_upload(file) + from dvadmin.utils.aliyunoss import ali_oss_upload + file_path = ali_oss_upload(file, file_name=validated_data['name']) if file_path: validated_data['file_url'] = file_path else: raise ValueError("上传失败") elif file_engine == 'cos': - from dvadmin_cloud_storage.views.tencent import tencent_cos_upload - file_path = tencent_cos_upload(file) + from dvadmin.utils.tencentcos import tencent_cos_upload + file_path = tencent_cos_upload(file, file_name=validated_data['name']) if file_path: validated_data['file_url'] = file_path else: 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/dvadmin/utils/aliyunoss.py b/backend/dvadmin/utils/aliyunoss.py new file mode 100644 index 0000000000000000000000000000000000000000..b4e2894f39b329018d4113294d3efc22782c64eb --- /dev/null +++ b/backend/dvadmin/utils/aliyunoss.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +import oss2 +from rest_framework.exceptions import ValidationError + +from application import dispatch + + +# 进度条 +# 当无法确定待上传的数据长度时,total_bytes的值为None。 +def percentage(consumed_bytes, total_bytes): + if total_bytes: + rate = int(100 * (float(consumed_bytes) / float(total_bytes))) + print('\r{0}% '.format(rate), end='') + + +def ali_oss_upload(file, file_name): + """ + 阿里云OSS上传 + """ + try: + file.seek(0) + file_read = file.read() + except Exception as e: + file_read = file + if not file: + raise ValidationError('请上传文件') + # 转存到oss + path_prefix = dispatch.get_system_config_values("file_storage.aliyun_path") + if not path_prefix.endswith('/'): + path_prefix = path_prefix + '/' + if path_prefix.startswith('/'): + path_prefix = path_prefix[1:] + base_fil_name = f'{path_prefix}{file_name}' + # 获取OSS配置 + # 获取的AccessKey + access_key_id = dispatch.get_system_config_values("file_storage.aliyun_access_key") + access_key_secret = dispatch.get_system_config_values("file_storage.aliyun_access_secret") + auth = oss2.Auth(access_key_id, access_key_secret) + # 这个是需要用特定的地址,不同地域的服务器地址不同,不要弄错了 + # 参考官网给的地址配置https://www.alibabacloud.com/help/zh/object-storage-service/latest/regions-and-endpoints#concept-zt4-cvy-5db + endpoint = dispatch.get_system_config_values("file_storage.aliyun_endpoint") + bucket_name = dispatch.get_system_config_values("file_storage.aliyun_bucket") + if bucket_name.endswith(endpoint): + bucket_name = bucket_name.replace(f'.{endpoint}', '') + # 你的项目名称,类似于不同的项目上传的图片前缀url不同 + bucket = oss2.Bucket(auth, endpoint, bucket_name) # 项目名称 + # 生成外网访问的文件路径 + aliyun_cdn_url = dispatch.get_system_config_values("file_storage.aliyun_cdn_url") + if aliyun_cdn_url: + if aliyun_cdn_url.endswith('/'): + aliyun_cdn_url = aliyun_cdn_url[1:] + file_path = f"{aliyun_cdn_url}/{base_fil_name}" + else: + file_path = f"https://{bucket_name}.{endpoint}/{base_fil_name}" + # 这个是阿里提供的SDK方法 + res = bucket.put_object(base_fil_name, file_read, progress_callback=percentage) + # 如果上传状态是200 代表成功 返回文件外网访问路径 + if res.status == 200: + return file_path + else: + return None diff --git a/backend/dvadmin/utils/models.py b/backend/dvadmin/utils/models.py index b387ea4b21c5d68e2541359f83435d0a41a74dab..1283351c61e8a18131f6de61e565098f2e6e21dc 100644 --- a/backend/dvadmin/utils/models.py +++ b/backend/dvadmin/utils/models.py @@ -81,6 +81,26 @@ class SoftDeleteModel(models.Model): super().delete(using=using, *args, **kwargs) +class CoreModelManager(models.Manager): + def get_queryset(self): + is_deleted = getattr(self.model, 'is_soft_delete', False) + flow_work_status = getattr(self.model, 'flow_work_status', False) + queryset = super().get_queryset() + if flow_work_status: + queryset = queryset.filter(flow_work_status=1) + if is_deleted: + queryset = queryset.filter(is_deleted=False) + return queryset + def create(self,request: Request=None, **kwargs): + data = {**kwargs} + if request: + request_user = request.user + data["creator"] = request_user + data["modifier"] = request_user.id + data["dept_belong_id"] = request_user.dept_id + # 调用父类的create方法执行实际的创建操作 + return super().create(**data) + class CoreModel(models.Model): """ 核心标准抽象模型模型,可直接继承使用 @@ -98,7 +118,8 @@ class CoreModel(models.Model): verbose_name="修改时间") create_datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间", verbose_name="创建时间") - + objects = CoreModelManager() + all_objects = models.Manager() class Meta: abstract = True verbose_name = '核心模型' diff --git a/backend/dvadmin/utils/tencentcos.py b/backend/dvadmin/utils/tencentcos.py new file mode 100644 index 0000000000000000000000000000000000000000..a5151247912656a1cc7d1c6b1f0caf639d848176 --- /dev/null +++ b/backend/dvadmin/utils/tencentcos.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +from rest_framework.exceptions import ValidationError + +from application import dispatch +from qcloud_cos import CosConfig +from qcloud_cos import CosS3Client + + +# 进度条 +# 当无法确定待上传的数据长度时,total_bytes的值为None。 +def percentage(consumed_bytes, total_bytes): + if total_bytes: + rate = int(100 * (float(consumed_bytes) / float(total_bytes))) + print('\r{0}% '.format(rate), end='') + +def tencent_cos_upload(file, file_name): + try: + file.seek(0) + file_read = file.read() + except Exception as e: + file_read = file + if not file: + raise ValidationError('请上传文件') + # 生成文件名 + path_prefix = dispatch.get_system_config_values("file_storage.tencent_path") + if not path_prefix.endswith('/'): + path_prefix = path_prefix + '/' + if path_prefix.startswith('/'): + path_prefix = path_prefix[1:] + base_fil_name = f'{path_prefix}{file_name}' + # 获取cos配置 + # 1. 设置用户属性, 包括 secret_id, secret_key, region等。Appid 已在 CosConfig 中移除,请在参数 Bucket 中带上 Appid。Bucket 由 BucketName-Appid 组成 + secret_id = dispatch.get_system_config_values("file_storage.tencent_secret_id") # 用户的 SecretId,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140 + secret_key = dispatch.get_system_config_values("file_storage.tencent_secret_key") # 用户的 SecretKey,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参见 https://cloud.tencent.com/document/product/598/37140 + region = dispatch.get_system_config_values("file_storage.tencent_region") # 替换为用户的 region,已创建桶归属的 region 可以在控制台查看,https://console.cloud.tencent.com/cos5/bucket # COS 支持的所有 region 列表参见https://cloud.tencent.com/document/product/436/6224 + bucket = dispatch.get_system_config_values("file_storage.tencent_bucket") # 要访问的桶名称 + config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key) + client = CosS3Client(config) + # 访问地址 + base_file_url = f'https://{bucket}.cos.{region}.myqcloud.com' + # 生成外网访问的文件路径 + if base_file_url.endswith('/'): + file_path = base_file_url + base_fil_name + else: + file_path = f'{base_file_url}/{base_fil_name}' + # 这个是阿里提供的SDK方法 bucket是调用的4.1中配置的变量名 + try: + response = client.put_object( + Bucket=bucket, + Body=file_read, + Key=base_fil_name, + EnableMD5=False + ) + return file_path + except: + return None diff --git a/backend/requirements.txt b/backend/requirements.txt index 9cae2cca7e0fe4085fa49dec92d9f5203dd10aea..f443f6f09bb90f9ccaf10d05b5dd21f5f953a915 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -7,26 +7,27 @@ 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 -dvadmin3-celery==3.1.6 \ No newline at end of file +dvadmin3-celery==3.1.6 +oss2==2.19.1 +cos-python-sdk-v5==1.9.37 \ No newline at end of file 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') }}
- + +
diff --git a/web/src/views/system/login/index.vue b/web/src/views/system/login/index.vue index 0f4d9c86186777cfc8bb71bd38e7e01a9ea8f874..897ddc4121cf00fabb1900f0be02265ef9c37814 100644 --- a/web/src/views/system/login/index.vue +++ b/web/src/views/system/login/index.vue @@ -34,7 +34,9 @@ -->
- + + + @@ -81,6 +83,8 @@ const Account = defineAsyncComponent(() => import('/@/views/system/login/compone const Mobile = defineAsyncComponent(() => import('/@/views/system/login/component/mobile.vue')); const Scan = defineAsyncComponent(() => import('/@/views/system/login/component/scan.vue')); const ChangePwd = defineAsyncComponent(() => import('/@/views/system/login/component/changePwd.vue')); +const OAuth2 = defineAsyncComponent(() => import('/@/views/system/login/component/oauth2.vue')); + import _ from "lodash-es"; import {useUserInfo} from "/@/stores/userInfo"; const { userInfos } = storeToRefs(useUserInfo()); diff --git a/web/src/views/system/login/types.ts b/web/src/views/system/login/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..5e2de5977f664e6468533432de28dbdc4b521635 --- /dev/null +++ b/web/src/views/system/login/types.ts @@ -0,0 +1,8 @@ + +export interface OAuth2Backend { + app_name: string; + backend_name: string; + icon: string; + authentication_url: string; +} + diff --git a/web/src/views/system/menu/components/MenuFormCom/index.vue b/web/src/views/system/menu/components/MenuFormCom/index.vue index 5f64d5add10e36acb797a4828dbc93e8d10b6c5d..1bcc61fd5c6ec99a693a4f1eca1c84bc1b36895c 100644 --- a/web/src/views/system/menu/components/MenuFormCom/index.vue +++ b/web/src/views/system/menu/components/MenuFormCom/index.vue @@ -254,6 +254,9 @@ const handleSubmit = () => { let res; menuBtnLoading.value = true; if (menuFormData.id) { + if (menuFormData.parent == undefined) { + menuFormData.parent = null + } res = await UpdateObj(menuFormData); } else { res = await AddObj(menuFormData); diff --git a/web/src/views/system/messageCenter/crud.tsx b/web/src/views/system/messageCenter/crud.tsx index dddc1a65a928433bc6de25cb503d1cc7561f989e..267922879f345484e4a45aeecd1f2911b5b8059a 100644 --- a/web/src/views/system/messageCenter/crud.tsx +++ b/web/src/views/system/messageCenter/crud.tsx @@ -131,10 +131,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat data: [ { value: 0, label: '按用户' }, { value: 1, label: '按角色' }, - { - value: 2, - label: '按部门', - }, + { value: 2, label: '按部门' }, { value: 3, label: '通知公告' }, ], }), @@ -142,14 +139,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat component: { optionName: 'el-radio-button', }, - rules: [ - { - required: true, - message: '必选项', - // @ts-ignore - trigger: ['blur', 'change'], - }, - ], + rules: [{ required: true, message: '必选项', trigger: ['blur', 'change'] }], }, }, target_user: { @@ -191,10 +181,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat }), rules: [ // 表单校验规则 - { - required: true, - message: '必填项', - }, + { required: true, message: '必填项' }, ], }, column: { diff --git a/web/src/views/system/role/components/RoleMenuTree.vue b/web/src/views/system/role/components/RoleMenuTree.vue index c296e9a90a5a59eb2cb627f5ad4e2a32c554a9ba..90c24bd3e3b90874ec527148ad8449a859c99260 100644 --- a/web/src/views/system/role/components/RoleMenuTree.vue +++ b/web/src/views/system/role/components/RoleMenuTree.vue @@ -11,6 +11,7 @@ highlight-current show-checkbox default-expand-all + :check-on-click-leaf="false" > diff --git a/web/src/views/system/role/crud.tsx b/web/src/views/system/role/crud.tsx index 9b4bf88b42138ba0c4447a21cd10cb8faa9aff37..ce8ff5a2df30af39400f53fc36117635680c75a9 100644 --- a/web/src/views/system/role/crud.tsx +++ b/web/src/views/system/role/crud.tsx @@ -181,7 +181,7 @@ export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOp }, }, dict: dict({ - value: dictionary('button_status_bool'), + data: dictionary('button_status_bool'), }), }, }, diff --git a/web/src/views/system/user/crud.tsx b/web/src/views/system/user/crud.tsx index bb22e6951cc18f68fb7824740d26665f06b5836c..b93511f5cd4529d1a483d8d78cffc9b978c8dae2 100644 --- a/web/src/views/system/user/crud.tsx +++ b/web/src/views/system/user/crud.tsx @@ -19,6 +19,7 @@ import { computed } from "vue"; import { Md5 } from 'ts-md5'; import { commonCrudConfig } from "/@/utils/commonCrud"; import { ElMessageBox } from 'element-plus'; +import {exportData} from "./api"; export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet { const pageRequest = async (query: UserPageQuery) => { return await api.GetList(query); @@ -81,9 +82,9 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp title: "导出",//鼠标停留显示的信息 show: auth('user:Export'), click: (ctx: any) => ElMessageBox.confirm( - '确定重设密码吗?', '提示', + '确定导出数据吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' } - ).then(() => resetToDefaultPasswordRequest(ctx.row)) + ).then(() => exportData(ctx.row)) } } },