From 45dcda0cc093ac340d722280652ee6dbc5cbf6ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E5=AD=90-=E6=9D=8E?= <1537080775@qq.com> Date: Tue, 6 May 2025 05:49:02 +0000 Subject: [PATCH 01/13] =?UTF-8?q?=E5=85=BC=E5=AE=B9OAuth2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 木子-李 <1537080775@qq.com> --- backend/dvadmin/system/models.py | 8 +- web/src/router/index.ts | 12 ++ web/src/views/system/login/api.ts | 7 + .../views/system/login/component/oauth2.vue | 139 ++++++++++++++++++ web/src/views/system/login/index.vue | 6 +- web/src/views/system/login/types.ts | 8 + 6 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 web/src/views/system/login/component/oauth2.vue create mode 100644 web/src/views/system/login/types.ts diff --git a/backend/dvadmin/system/models.py b/backend/dvadmin/system/models.py index c70f2561b..ca97fe214 100644 --- a/backend/dvadmin/system/models.py +++ b/backend/dvadmin/system/models.py @@ -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/web/src/router/index.ts b/web/src/router/index.ts index 0815e9eee..37708c6f4 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -98,10 +98,22 @@ export function formatTwoStageRoutes(arr: any) { const frameOutRoutes = staticRoutes.map(item => item.path) +const checkToken = ()=>{ + const urlParams = new URLSearchParams(window.location.search); + const _oauth2_token = urlParams.get('_oauth2_token'); + if (_oauth2_token) { + Session.set('token', _oauth2_token); + const cleanUrl = window.location.href.split('?')[0]; + window.history.replaceState({}, '', cleanUrl); + useUserInfo(pinia).setUserInfos(); + + } +} // 路由加载前 router.beforeEach(async (to, from, next) => { // 检查浏览器本地版本与线上版本是否一致,判断是否需要刷新页面进行更新 await checkVersion() + checkToken() NProgress.configure({showSpinner: false}); if (to.meta.title) NProgress.start(); const token = Session.get('token'); diff --git a/web/src/views/system/login/api.ts b/web/src/views/system/login/api.ts index 7f835567d..4502a753f 100644 --- a/web/src/views/system/login/api.ts +++ b/web/src/views/system/login/api.ts @@ -28,3 +28,10 @@ export function getUserInfo() { method: 'get', }); } + +export function getBackends() { + return request({ + url: '/api/dvadmin3_social_oauth2/backend/get_login_backend/', + method: 'get', + }); +} \ No newline at end of file diff --git a/web/src/views/system/login/component/oauth2.vue b/web/src/views/system/login/component/oauth2.vue new file mode 100644 index 000000000..1b0c0c68b --- /dev/null +++ b/web/src/views/system/login/component/oauth2.vue @@ -0,0 +1,139 @@ + + + 其他快速方式登录 + + + + + {{ v.app_name }} + + + + + + + + + + diff --git a/web/src/views/system/login/index.vue b/web/src/views/system/login/index.vue index 0f4d9c861..897ddc412 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 000000000..5e2de5977 --- /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; +} + -- Gitee From 3a25cdb53c7d33310335f134097ffd792667dc3b Mon Sep 17 00:00:00 2001 From: liqiang <1206709430@qq.com> Date: Thu, 8 May 2025 05:22:32 +0800 Subject: [PATCH 02/13] =?UTF-8?q?refactor(system):=20=E7=A7=BB=E9=99=A4=20?= =?UTF-8?q?Role=20=E6=A8=A1=E5=9E=8B=E4=B8=AD=E7=9A=84=20FlowBaseModel?= =?UTF-8?q?=E7=BB=A7=E6=89=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 从 Role 类中删除了对 FlowBaseModel 的继承 - 这个改动简化了 Role 模型的结构,可能会影响与流程相关的功能 --- backend/dvadmin/system/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/dvadmin/system/models.py b/backend/dvadmin/system/models.py index c70f2561b..d6c229b6e 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="角色顺序") -- Gitee From 150b92163f7bce4ee87406ffd2bc1d12be54b530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8C=BF=E5=B0=8F=E5=A4=A9?= <1638245306@qq.com> Date: Sun, 1 Jun 2025 19:51:25 +0800 Subject: [PATCH 03/13] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=8F=98=E5=8C=96:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 初始化生成utf-8编码 --- .../dvadmin/system/management/commands/generate_init_json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/dvadmin/system/management/commands/generate_init_json.py b/backend/dvadmin/system/management/commands/generate_init_json.py index b0743448e..7d6eb082d 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 -- Gitee From 5a980f3b547b5fe016fafb69b3df1b0881bfe0a9 Mon Sep 17 00:00:00 2001 From: 1638245306 <1638245306@qq.com> Date: Mon, 9 Jun 2025 15:08:51 +0800 Subject: [PATCH 04/13] =?UTF-8?q?feat(user):=20=E6=B7=BB=E5=8A=A0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=20ID=20=E5=B1=9E=E6=80=A7=E5=B9=B6=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 UserInfosState 接口中添加 id属性 - 在 userInfo store 中添加用户 ID 相关逻辑 - 更新 getUserInfos 和 updateUserInfos 方法以处理用户 ID - 注释掉水平菜单滚动定位代码 --- web/src/layout/navMenu/horizontal.vue | 2 +- web/src/stores/interface/index.ts | 1 + web/src/stores/userInfo.ts | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/web/src/layout/navMenu/horizontal.vue b/web/src/layout/navMenu/horizontal.vue index e7fcfca07..192db7fd0 100644 --- a/web/src/layout/navMenu/horizontal.vue +++ b/web/src/layout/navMenu/horizontal.vue @@ -93,7 +93,7 @@ const initElMenuOffsetLeft = () => { nextTick(() => { let els = document.querySelector('.el-menu.el-menu--horizontal li.is-active'); if (!els) return false; - elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft = els.offsetLeft; + // elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft = els.offsetLeft; }); }; // 路由过滤递归函数 diff --git a/web/src/stores/interface/index.ts b/web/src/stores/interface/index.ts index aeb842098..145375a3e 100644 --- a/web/src/stores/interface/index.ts +++ b/web/src/stores/interface/index.ts @@ -6,6 +6,7 @@ import {useFrontendMenuStore} from "/@/stores/frontendMenu"; // 用户信息 export interface UserInfosState { + id:'', avatar: string; username: string; name: string; diff --git a/web/src/stores/userInfo.ts b/web/src/stores/userInfo.ts index 01aea201a..9250f9316 100644 --- a/web/src/stores/userInfo.ts +++ b/web/src/stores/userInfo.ts @@ -12,6 +12,7 @@ import headerImage from '/@/assets/img/headerImage.png'; export const useUserInfo = defineStore('userInfo', { state: (): UserInfosStates => ({ userInfos: { + id:'', avatar: '', username: '', name: '', @@ -37,6 +38,7 @@ export const useUserInfo = defineStore('userInfo', { this.userInfos.pwd_change_count = count; }, async updateUserInfos(userInfos:any) { + this.userInfos.id = userInfos.id; this.userInfos.username = userInfos.name; this.userInfos.avatar = userInfos.avatar; this.userInfos.name = userInfos.name; @@ -54,6 +56,7 @@ export const useUserInfo = defineStore('userInfo', { this.userInfos = Session.get('userInfo'); } else { let userInfos: any = await this.getApiUserInfo(); + this.userInfos.id = userInfos.id; this.userInfos.username = userInfos.data.name; this.userInfos.avatar = userInfos.data.avatar; this.userInfos.name = userInfos.data.name; @@ -74,6 +77,7 @@ export const useUserInfo = defineStore('userInfo', { url: '/api/system/user/user_info/', method: 'get', }).then((res:any)=>{ + this.userInfos.id = res.data.id; this.userInfos.username = res.data.name; this.userInfos.avatar = (res.data.avatar && getBaseURL(res.data.avatar)) || headerImage; this.userInfos.name = res.data.name; -- Gitee From a0a7c25b18fb856ce14835b8e8086879c2c91c92 Mon Sep 17 00:00:00 2001 From: liqiang <1206709430@qq.com> Date: Wed, 11 Jun 2025 15:58:25 +0800 Subject: [PATCH 05/13] =?UTF-8?q?refactor(del=5Fmigrations):=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=20.venv=20=E7=9B=AE=E5=BD=95=E6=8E=92=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在需要排除的目录列表中添加了 .venv,以避免删除虚拟环境目录中的迁移文件 --- backend/del_migrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/del_migrations.py b/backend/del_migrations.py index d649e051d..775d1dccc 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: -- Gitee From 8ea49866bc4f6a4c1806577244ae5e10dd1f5405 Mon Sep 17 00:00:00 2001 From: liqiang <1206709430@qq.com> Date: Thu, 12 Jun 2025 06:10:47 +0800 Subject: [PATCH 06/13] =?UTF-8?q?feat(system):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=AD=98=E5=82=A8=E5=BC=95=E6=93=8E=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增文件存储引擎配置选项,支持本地、阿里云oss和腾讯云cos - 在系统配置中添加文件存储相关设置- 实现阿里云oss和腾讯云cos的文件上传功能 - 更新文件列表视图,支持不同存储引擎的文件上传和访问 --- backend/application/settings.py | 4 + .../system/fixtures/init_dictionary.json | 45 ++++ .../system/fixtures/init_systemconfig.json | 249 +++++++++++++++++- backend/dvadmin/system/views/file_list.py | 12 +- backend/dvadmin/utils/aliyunoss.py | 62 +++++ backend/dvadmin/utils/tencentcos.py | 56 ++++ backend/requirements.txt | 4 +- 7 files changed, 424 insertions(+), 8 deletions(-) create mode 100644 backend/dvadmin/utils/aliyunoss.py create mode 100644 backend/dvadmin/utils/tencentcos.py diff --git a/backend/application/settings.py b/backend/application/settings.py index 1d0adf79b..71ed7d53b 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/dvadmin/system/fixtures/init_dictionary.json b/backend/dvadmin/system/fixtures/init_dictionary.json index f750c4074..c4bd186dc 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 98c95cd70..cc692f257 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/views/file_list.py b/backend/dvadmin/system/views/file_list.py index c0fed8d28..a155ea113 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/utils/aliyunoss.py b/backend/dvadmin/utils/aliyunoss.py new file mode 100644 index 000000000..b4e2894f3 --- /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/tencentcos.py b/backend/dvadmin/utils/tencentcos.py new file mode 100644 index 000000000..a51512479 --- /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 9cae2cca7..2e6a68a1e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -29,4 +29,6 @@ gunicorn==22.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 -- Gitee From ed915aa2cb9bba213581be364935e6f34f9a85d5 Mon Sep 17 00:00:00 2001 From: 1638245306 <1638245306@qq.com> Date: Tue, 17 Jun 2025 11:35:46 +0800 Subject: [PATCH 07/13] =?UTF-8?q?feat(core):=20=E6=96=B0=E5=A2=9E=E8=BD=AF?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=92=8C=E5=B7=A5=E4=BD=9C=E6=B5=81=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=AD=9B=E9=80=89=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 CoreModelManager 类,实现软删除和工作流状态的筛选 - 在 CoreModel 中集成新功能- 增加 objects 和 all_objects 两个 Manager,支持不同查询需求 --- backend/dvadmin/utils/models.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/backend/dvadmin/utils/models.py b/backend/dvadmin/utils/models.py index b387ea4b2..1283351c6 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 = '核心模型' -- Gitee From 5dcbae292a9c8571678c92290a9a4eb05081be98 Mon Sep 17 00:00:00 2001 From: 1638245306 <1638245306@qq.com> Date: Wed, 18 Jun 2025 18:47:51 +0800 Subject: [PATCH 08/13] =?UTF-8?q?refactor(system):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E6=9D=83=E9=99=90=E5=AD=97=E5=85=B8=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/views/system/role/crud.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/views/system/role/crud.tsx b/web/src/views/system/role/crud.tsx index 9b4bf88b4..ce8ff5a2d 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'), }), }, }, -- Gitee From 1052f6a07b7d466987a83237c553aa944c116485 Mon Sep 17 00:00:00 2001 From: 1638245306 <1638245306@qq.com> Date: Wed, 18 Jun 2025 18:59:44 +0800 Subject: [PATCH 09/13] =?UTF-8?q?feat(user):=20=E6=B7=BB=E5=8A=A0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=95=B0=E6=8D=AE=E5=AF=BC=E5=87=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增导出数据功能,位于用户管理页面的导出按钮 - 点击导出按钮后,弹出确认框,提示用户是否确定导出数据 - 确定导出后,调用 exportData 函数执行导出操作 --- web/src/views/system/user/crud.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/src/views/system/user/crud.tsx b/web/src/views/system/user/crud.tsx index bb22e6951..b93511f5c 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)) } } }, -- Gitee From 0005d45d85d19088bc8646e26c41784c888cb53b Mon Sep 17 00:00:00 2001 From: 1638245306 <1638245306@qq.com> Date: Wed, 18 Jun 2025 19:03:24 +0800 Subject: [PATCH 10/13] =?UTF-8?q?style(icon-selector):=20=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E5=9B=BE=E6=A0=87=E9=80=89=E6=8B=A9=E5=99=A8=E6=A0=87?= =?UTF-8?q?=E9=A2=98=E7=9A=84=E4=BD=8D=E7=BD=AE=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 .icon-selector-warp-title 类的 position 属性从 absolute改为 relative - 此修改解决了标题在某些情况下的定位问题,确保布局的稳定性 --- web/src/theme/iconSelector.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/theme/iconSelector.scss b/web/src/theme/iconSelector.scss index 569f61430..52d6c5325 100644 --- a/web/src/theme/iconSelector.scss +++ b/web/src/theme/iconSelector.scss @@ -7,7 +7,7 @@ overflow: hidden; position: relative; .icon-selector-warp-title { - position: absolute; + position: relative; height: 40px; line-height: 40px; left: 15px; -- Gitee From c6c54d8013835345f59f3448bd9bd0dd1a6ea435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=BE=89?= Date: Thu, 19 Jun 2025 13:41:57 +0800 Subject: [PATCH 11/13] =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/stores/interface/index.ts | 13 +++++++------ web/src/stores/userInfo.ts | 4 ++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/web/src/stores/interface/index.ts b/web/src/stores/interface/index.ts index 145375a3e..9f73c9212 100644 --- a/web/src/stores/interface/index.ts +++ b/web/src/stores/interface/index.ts @@ -2,18 +2,19 @@ * 定义接口来定义对象的类型 * `stores` 全部类型定义在这里 */ -import {useFrontendMenuStore} from "/@/stores/frontendMenu"; +import { useFrontendMenuStore } from "/@/stores/frontendMenu"; // 用户信息 export interface UserInfosState { - id:'', + id: '', avatar: string; + is_superuser: boolean, username: string; name: string; email: string; mobile: string; gender: string; - pwd_change_count:null|number; + pwd_change_count: null | number; dept_info: { dept_id: number; dept_name: string; @@ -108,9 +109,9 @@ export interface ConfigStates { export interface FrontendMenu { arrayRouter: Array; - treeRouter:Array; + treeRouter: Array; - frameOutRoutes:Array; + frameOutRoutes: Array; - frameInRoutes:Array; + frameInRoutes: Array; } diff --git a/web/src/stores/userInfo.ts b/web/src/stores/userInfo.ts index 9250f9316..c896214ee 100644 --- a/web/src/stores/userInfo.ts +++ b/web/src/stores/userInfo.ts @@ -20,6 +20,7 @@ export const useUserInfo = defineStore('userInfo', { mobile: '', gender: '', pwd_change_count:null, + is_superuser: false, dept_info: { dept_id: 0, dept_name: '', @@ -48,6 +49,7 @@ export const useUserInfo = defineStore('userInfo', { this.userInfos.dept_info = userInfos.dept_info; this.userInfos.role_info = userInfos.role_info; this.userInfos.pwd_change_count = userInfos.pwd_change_count; + this.userInfos.is_superuser = userInfos.is_superuser; Session.set('userInfo', this.userInfos); }, async setUserInfos() { @@ -66,6 +68,7 @@ export const useUserInfo = defineStore('userInfo', { this.userInfos.dept_info = userInfos.data.dept_info; this.userInfos.role_info = userInfos.data.role_info; this.userInfos.pwd_change_count = userInfos.data.pwd_change_count; + this.userInfos.is_superuser = userInfos.data.is_superuser; Session.set('userInfo', this.userInfos); } }, @@ -87,6 +90,7 @@ export const useUserInfo = defineStore('userInfo', { this.userInfos.dept_info = res.data.dept_info; this.userInfos.role_info = res.data.role_info; this.userInfos.pwd_change_count = res.data.pwd_change_count; + this.userInfos.is_superuser = res.data.is_superuser; Session.set('userInfo', this.userInfos); }) }, -- Gitee From b89f1671c31750ec870d0257f8378aae8bdae0bc Mon Sep 17 00:00:00 2001 From: 1638245306 <1638245306@qq.com> Date: Thu, 19 Jun 2025 22:54:14 +0800 Subject: [PATCH 12/13] =?UTF-8?q?fix(system):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=8F=9C=E5=8D=95=E6=9C=AA=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E7=88=B6=E8=8F=9C=E5=8D=95=E6=97=B6=E7=9A=84=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E9=97=AE=E9=A2=98-=20=E5=9C=A8=E6=8F=90=E4=BA=A4=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E8=A1=A8=E5=8D=95=E6=97=B6=EF=BC=8C=E5=A6=82=E6=9E=9C?= =?UTF-8?q?=E6=9C=AA=E9=80=89=E6=8B=A9=E7=88=B6=E8=8F=9C=E5=8D=95=EF=BC=8C?= =?UTF-8?q?=E5=B0=86=20parent=20=E5=AD=97=E6=AE=B5=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E4=B8=BA=20null=20-=E7=A1=AE=E4=BF=9D=E5=9C=A8=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=88=96=E6=B7=BB=E5=8A=A0=E8=8F=9C=E5=8D=95=E6=97=B6?= =?UTF-8?q?=EF=BC=8C=E7=88=B6=E8=8F=9C=E5=8D=95=E5=AD=97=E6=AE=B5=E7=9A=84?= =?UTF-8?q?=E5=80=BC=E6=98=AF=E6=AD=A3=E7=A1=AE=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/views/system/menu/components/MenuFormCom/index.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/src/views/system/menu/components/MenuFormCom/index.vue b/web/src/views/system/menu/components/MenuFormCom/index.vue index 5f64d5add..1bcc61fd5 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); -- Gitee From e8212501e29be75223a7a93b8215b0558cc1b2d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=B0=8F=E6=B6=9B?= <1537080775@qq.com> Date: Sun, 22 Jun 2025 13:09:49 +0800 Subject: [PATCH 13/13] =?UTF-8?q?refactor(system):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E4=B8=AD=E5=BF=83=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 WebSocket相关代码 - 新增 SSE (Server-Sent Events) 实现消息推送 - 优化消息中心未读数量展示和更新逻辑- 调整消息中心相关 API 和前端展示 --- backend/application/asgi.py | 13 +- backend/application/routing.py | 7 - backend/application/sse_views.py | 33 ++++ backend/application/urls.py | 6 +- backend/application/websocketConfig.py | 183 ------------------ backend/dvadmin/system/apps.py | 4 + backend/dvadmin/system/signals.py | 17 +- .../dvadmin/system/views/download_center.py | 5 + .../dvadmin/system/views/message_center.py | 27 +-- backend/requirements.txt | 7 +- web/.env.development | 2 +- web/src/App.vue | 59 ------ web/src/components/tableSelector/index.vue | 100 +++++----- web/src/layout/navBars/breadcrumb/user.vue | 121 ++++++------ .../layout/navBars/breadcrumb/userNews.vue | 42 ++-- web/src/stores/userInfo.ts | 3 - web/src/views/system/messageCenter/crud.tsx | 19 +- .../system/role/components/RoleMenuTree.vue | 1 + 18 files changed, 201 insertions(+), 448 deletions(-) delete mode 100644 backend/application/routing.py create mode 100644 backend/application/sse_views.py delete mode 100644 backend/application/websocketConfig.py diff --git a/backend/application/asgi.py b/backend/application/asgi.py index 14aacecf5..37e9f3595 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 d4df9f888..000000000 --- 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 000000000..f1cbe014c --- /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 641b85cf2..d1902fcb8 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 ab2cd64f3..000000000 --- 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 191aade90..8302f727a 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 972822886..d00770c9f 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 4fa88bb9d..4e6b0611a 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 db91b7566..26faa3f32 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 2e6a68a1e..f443f6f09 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 dc36b291b..1c3ca5db3 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 c13df045b..449b9658e 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 d827a751c..ac7aae7b3 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 7005547b4..aa1b067d4 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') }} - + + @@ -21,7 +22,7 @@
{{ v.app_name }}