116 Commits

Author SHA1 Message Date
b3868ad374 更新 README.zh.md 2025-12-09 14:48:01 +00:00
liqiang
2800d1ee14 1 2025-09-19 10:56:00 +08:00
liqiang
ab1e0268d1 提交 2025-09-19 10:49:07 +08:00
dvadmin
2d36b4357e !131 Merge remote-tracking branch 'origin/develop' into develop
Merge pull request !131 from dvadmin/develop
2025-08-07 05:55:15 +00:00
liqiang
6c99a78821 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	web/src/views/system/login/index.vue
2025-08-07 13:54:07 +08:00
dvadmin
277853b1a4 !130 !128 【轻量PR】: 修复前端请求配置与自定义配置合并逻辑
Merge pull request !130 from dvadmin/develop
2025-08-07 05:45:56 +00:00
dvadmin
3db048e650 !128 【轻量PR】: 修复前端请求配置与自定义配置合并逻辑
Merge pull request !128 from lorenzo/develop_v2
2025-08-07 05:45:06 +00:00
ahui
e470a716e5 忽略文件更新 2025-08-07 11:30:21 +08:00
lorenzo
95bf503529 fix(web): 修复前端请求配置与自定义配置合并逻辑 2025-08-01 21:37:25 +08:00
猿小天
4e9155f09b feat(viewset): 通过 IDS列表获取数据
- 在通用的 ViewSet 中添加 get_by_ids 方法
- 接收 POST 请求,从请求体中获取 ids列表
- 根据 ids 列表查询数据并返回- 若 ids 列表为空或只包含空字符串,则返回空数据
2025-07-21 13:45:09 +08:00
ahui
3e60c5b5f9 工具函数同步 2025-07-17 11:00:28 +08:00
dvadmin
b72fb5238f !125 fix(application): 修复普通用户接收消息数量统计错误
Merge pull request !125 from 木子-李/sse
2025-07-15 14:34:58 +00:00
1638245306
74d89c3968 refactor(web): 优化代码和构建配置
- 修改用户服务条款中的企业用户描述
- 在 package.json 中添加新的构建命令 build:flowH5
- 引入新的依赖库 @meetjs/vant4-kit 和 vue-qr
- 添加 rollup 插件以优化构建过程
2025-07-10 15:18:16 +08:00
1638245306
5eab2b6e4f refactor(system): 更新登录页面版权信息
- 将版权年份从 2024 修改为 2023
- 更新版权公司名称为北京巨梦科技有限公司
- 修改备案号为晋ICP备18005113号-3
2025-07-10 15:04:39 +08:00
1638245306
d2f14e41ad refactor(system): 更新登录页面版权信息
- 将版权年份从 2024 修改为 2023
- 更新版权公司名称为北京巨梦科技有限公司
- 修改备案号为晋ICP备18005113号-3
2025-07-10 15:04:21 +08:00
阿辉
483863e238 同步权限 2025-07-06 16:54:26 +08:00
李小涛
ad95bea301 fix(application): 修复普通用户接收消息数量统计错误
- 移除了对 user_id == 1 的特殊判断,统一了消息数量统计逻辑
- 优化了代码结构,提高了代码的可读性和维护性
2025-06-26 17:51:36 +08:00
李小涛
344f754fc7 actor(webref): 优化代码结构和功能
- 注释掉 oauth2.vue 中的 getBackends() 调用
- 修改 sse_views.py 中的消息中心未读消息计数逻辑
- 优化 userNews.vue 中的新闻列表数据加载
2025-06-26 17:50:53 +08:00
dvadmin
749f5d80d2 !124 refactor(theme): 优化主题样式并移除冗余代码
Merge pull request !124 from 木子-李/sse
2025-06-25 08:03:25 +00:00
李小涛
ac3bfb6b80 refactor(theme): 优化主题样式并移除冗余代码
- 将 @import 替换为 @use,提高代码的可维护性
- 统一使用 index 作为命名空间,避免变量名冲突
- 移除不必要的注释和空格,精简代码
- 删除未使用的 isSocketOpen 属性,简化数据结构
2025-06-25 10:42:01 +08:00
liqiang
ef48bdd0df refactor: 更新登录页面版权信息和备案号
- 将默认版权信息修改为 "2021-2025 django-vue-admin.com"
- 将默认备案号修改为 "晋ICP备18005113号-3"
2025-06-25 06:55:15 +08:00
liqiang
dfccd1acbc Merge remote-tracking branch 'origin/master' 2025-06-25 05:13:27 +08:00
dvadmin-开发-李强
91fb318eb6 Accept Merge Request #27: (develop -> master)
Merge Request: build(web): 更新图标字体并调整相关配置

Created By: @dvadmin-开发-李强
Accepted By: @dvadmin-开发-李强
URL: https://dvadmin-private.coding.net/p/code/d/dvadmin3/git/merge/27?initial=true
2025-06-25 05:12:31 +08:00
liqiang
a2e07a89e1 build(web): 更新图标字体并调整相关配置
- 新增 iconfont-01 和 iconfont-02 两个图标字体文件夹- 在 setIconfont.ts 中注释掉原有的图标字体 URL
- 在 main.ts 中引入新的图标字体 CSS 文件
- 更新 package.json
2025-06-24 09:59:29 +08:00
dvadmin
3029317ff2 !123 正式发布v3.2.0版本
Merge pull request !123 from dvadmin/develop
2025-06-22 13:45:30 +00:00
dvadmin
7b55a3db64 !116 兼容OAuth2
Merge pull request !116 from 木子-李/N/A
2025-06-22 13:18:46 +00:00
dvadmin
3c8b4b6ac0 !122 refactor(system): 重构消息中心功能
Merge pull request !122 from 木子-李/sse
2025-06-22 12:25:31 +00:00
李小涛
e8212501e2 refactor(system): 重构消息中心功能
- 移除 WebSocket相关代码
- 新增 SSE (Server-Sent Events) 实现消息推送
- 优化消息中心未读数量展示和更新逻辑- 调整消息中心相关 API 和前端展示
2025-06-22 13:09:49 +08:00
1638245306
fa063a8611 Merge remote-tracking branch 'origin/develop' into develop 2025-06-19 22:59:47 +08:00
1638245306
b89f1671c3 fix(system): 修复新增菜单未选择父菜单时的提交问题- 在提交菜单表单时,如果未选择父菜单,将 parent 字段设置为 null
-确保在更新或添加菜单时,父菜单字段的值是正确的
2025-06-19 22:59:14 +08:00
阿辉
c6c54d8013 用户信息更新 2025-06-19 13:41:57 +08:00
1638245306
0005d45d85 style(icon-selector): 调整图标选择器标题的位置属性
- 将 .icon-selector-warp-title 类的 position 属性从 absolute改为 relative
- 此修改解决了标题在某些情况下的定位问题,确保布局的稳定性
2025-06-18 19:03:24 +08:00
1638245306
1052f6a07b feat(user): 添加用户数据导出功能
- 新增导出数据功能,位于用户管理页面的导出按钮
- 点击导出按钮后,弹出确认框,提示用户是否确定导出数据
- 确定导出后,调用 exportData 函数执行导出操作
2025-06-18 18:59:44 +08:00
1638245306
5dcbae292a refactor(system): 修复角色权限字典类型错误 2025-06-18 18:47:51 +08:00
1638245306
ed915aa2cb feat(core): 新增软删除和工作流状态筛选功能
- 添加 CoreModelManager 类,实现软删除和工作流状态的筛选
- 在 CoreModel 中集成新功能- 增加 objects 和 all_objects 两个 Manager,支持不同查询需求
2025-06-17 11:35:46 +08:00
liqiang
82a0ef612a Merge remote-tracking branch 'origin/develop' into develop 2025-06-15 17:55:24 +08:00
liqiang
8ea49866bc feat(system): 添加文件存储引擎功能
- 新增文件存储引擎配置选项,支持本地、阿里云oss和腾讯云cos
- 在系统配置中添加文件存储相关设置- 实现阿里云oss和腾讯云cos的文件上传功能
- 更新文件列表视图,支持不同存储引擎的文件上传和访问
2025-06-12 06:10:47 +08:00
liqiang
a0a7c25b18 refactor(del_migrations):增加 .venv 目录排除
- 在需要排除的目录列表中添加了 .venv,以避免删除虚拟环境目录中的迁移文件
2025-06-11 15:58:25 +08:00
1638245306
f8c8f2963b Merge remote-tracking branch 'origin/develop' into develop 2025-06-09 15:09:04 +08:00
1638245306
5a980f3b54 feat(user): 添加用户 ID 属性并进行相关处理
- 在 UserInfosState 接口中添加 id属性
- 在 userInfo store 中添加用户 ID 相关逻辑
- 更新 getUserInfos 和 updateUserInfos 方法以处理用户 ID
- 注释掉水平菜单滚动定位代码
2025-06-09 15:08:51 +08:00
猿小天
c0be63afcd Merge remote-tracking branch 'origin/develop' into develop 2025-06-01 19:55:05 +08:00
猿小天
150b92163f 功能变化:
初始化生成utf-8编码
2025-06-01 19:51:25 +08:00
liqiang
3a25cdb53c refactor(system): 移除 Role 模型中的 FlowBaseModel继承
- 从 Role 类中删除了对 FlowBaseModel 的继承
- 这个改动简化了 Role 模型的结构,可能会影响与流程相关的功能
2025-05-08 05:22:32 +08:00
木子-李
45dcda0cc0 兼容OAuth2
Signed-off-by: 木子-李 <1537080775@qq.com>
2025-05-06 05:49:02 +00:00
dvadmin
1e500bd683 !115 Merge remote-tracking branch 'origin/develop' into develop
Merge pull request !115 from dvadmin/develop
2025-05-06 02:11:02 +00:00
liqiang
b4ffb2105f Merge remote-tracking branch 'origin/develop' into develop 2025-05-06 10:06:22 +08:00
1638245306
3398aa3ba9 build(flowH5): 更新库入口文件路径和外部依赖
- 修改入口文件路径为 src/views/plugins/dvadmin3-flow-web/src/flowH5/index.ts
- 添加 xe-utils 到外部依赖列表
2025-05-01 00:04:01 +08:00
1638245306
94149161b3 build(flowH5): 添加 flowH5 项目的配置文件
- 新增 flowH5.config.ts 文件,配置 Vite 构建项目- 设置 Vue 插件和路径别名
- 配置 Roll
2025-04-30 12:55:58 +08:00
liqiang
1907f1ac0a Merge remote-tracking branch 'origin/develop' into develop 2025-04-30 10:35:52 +08:00
1638245306
bda002398c refactor: 修改 API 文档权限为仅允许认证用户访问
- 将 API 文档的权限设置从 AllowAny 更改为 IsAuthenticated- 确保只有经过身份验证的用户才能访问 API 文档
2025-04-29 14:45:45 +08:00
阿辉
66c28bd389 日历选择器组件 2025-04-07 15:20:52 +08:00
1638245306
8eacc4aad3 feat(system): 角色模型添加工作流支持
- 引入 FlowBaseModel 以支持工作流功能
- 使 Role 模型继承 CoreModel 和 FlowBaseModel
2025-03-28 18:24:13 +08:00
1638245306
7a152d3591 feat(system): 添加获取用户递归部门名称功能
- 在 Department模型中添加了 _recursion 类方法,用于递归获取指定属性值
- 新增 get_region_name 类方法,用于获取用户的所有上级部门名称
- 该功能可以用于显示用户所在的完整部门路径
2025-03-28 15:08:56 +08:00
1638245306
4b05a28f4c fix(system): 修改用户密码设置逻辑
- 移除了对新密码进行 MD5 加密的步骤- 直接使用明文密码进行加密存储
- 保留了密码修改次数的计数功能
2025-03-28 14:44:39 +08:00
1638245306
0392b3b101 feat(plugins): 动态加载插件
- 新增插件动态加载逻辑,遍历已发现的插件列表
- 对于每个插件,尝试导入其 index.ts 文件
- 导入成功后,将插件注册到应用中,并打印加载信息
-导入失败时,打印错误信息,提示插件下无 index.ts 文件
2025-03-28 13:39:44 +08:00
dvadmin
421a44b26c !111 发布到正式版本
Merge pull request !111 from dvadmin/develop
2025-03-20 20:43:47 +00:00
liqiang
fad2cb2e18 README.zh.md更新 2025-03-21 04:42:46 +08:00
liqiang
df0b78cafc README.zh.md更新 2025-03-21 04:41:36 +08:00
liqiang
c69ea7b33e Merge remote-tracking branch 'origin/develop' into develop 2025-03-21 04:34:30 +08:00
liqiang
ce5c4c9d8d README.zh.md更新 2025-03-21 04:33:45 +08:00
liqiang
3f7aaa0228 Merge remote-tracking branch 'origin/develop' into develop 2025-03-21 04:09:33 +08:00
liqiang
e37909d478 feat(core): 新增核心工具模块并优化通知功能
- 新增 cores.tsx 文件,实现核心工具模块
- 添加任务列表和事件总线功能
- 实现系统通知和任务处理逻辑
- 在 App.vue 中集成新功能
- 优化通知显示逻辑,支持不同内容类型的通知
2025-03-21 04:09:26 +08:00
dvadmin
1578c9d710 !108 修复字段权限筛选错误,update backend/dvadmin/utils/viewset.py.
Merge pull request !108 from lxy/N/A
2025-03-20 19:54:37 +00:00
dvadmin
01604a27db !109 修复无法下载问题 update web/src/views/system/downloadCenter/crud.tsx.
Merge pull request !109 from lxy/N/A
2025-03-20 19:53:57 +00:00
dvadmin
58036dbeb9 !110 修复文件管理点击地址无法下载问题 update web/src/views/system/fileList/crud.tsx.
Merge pull request !110 from lxy/N/A
2025-03-20 19:53:34 +00:00
lxy
7917a118c8 修复文件管理点击地址无法下载问题 update web/src/views/system/fileList/crud.tsx.
Signed-off-by: lxy <46486798@qq.com>
2025-03-20 11:33:01 +00:00
lxy
86c202b94a 修复无法下载问题 update web/src/views/system/downloadCenter/crud.tsx.
Signed-off-by: lxy <46486798@qq.com>
2025-03-20 11:28:09 +00:00
lxy
a5cc87eb55 修复字段权限筛选错误,update backend/dvadmin/utils/viewset.py.
解决不是超级管理员用户加载报错,匿名用户没有角色报错

Signed-off-by: lxy <46486798@qq.com>
2025-03-20 06:47:21 +00:00
1638245306
2fabc210b2 refactor(system): 恢复密码加密逻辑
- 将明文密码修改为经过 MD5 加密的密码
- 保留了原有的密码加密方式,提高了系统安全性
2025-03-19 15:54:58 +08:00
1638245306
4c644ae8cb build(development): 修改开发环境 API 地址并添加新的构建脚本
- 将开发环境 API 地址从 http://127.0.0.1:8000 改为 http://127.0.0.1:8001- 在 package.json 中添加了 build:dev 脚本,用于开发环境的构建
2025-03-17 14:40:12 +08:00
liqiang
351bae1fea build(.env): 修改 API 请求基础 URL
- 将 VITE_API_URL 从 'http://dvadmin3api.django.icu:8001' 修改为 'http://127.0.0.1:8000'- 此修改便于本地开发和测试
2025-03-15 14:52:29 +08:00
1638245306
bc7bc3cda6 feat(system): 优化配置页面表单组件
- 在 crudTable组件中添加 afterSubmit 钩子,更新 modelValue
-重构 formContent 组件中的 formData,使用 ref 替代 reactive
- 优化 formContent 组件的提交逻辑,简化代码结构
2025-03-14 17:33:51 +08:00
liqiang
aea5ca938b Merge branch 'develop' of gitee.com:huge-dream/django-vue3-admin into develop 2025-03-14 16:59:46 +08:00
1638245306
8706de5ef4 feat(system): 优化配置组件的样式和功能
- 调整表单布局,设置宽度为 500px,栏数为 24
- 在标题、键名和键值字段的表单中添加占位符
2025-03-14 16:42:04 +08:00
dvadmin
ed96740ab1 !106 完善初始化配置导出
Merge pull request !106 from lorenzo/dev
2025-03-13 10:32:43 +00:00
dvadmin
f719241938 !105 feat: 从表结构生成前端基础文件
Merge pull request !105 from lorenzo/master
2025-03-13 10:31:04 +00:00
1638245306
ac22e47883 docs(README): 添加给框架点赞二维码图片
- 在 README.md 中添加了支付宝和微信的二维码图片链接
- 通过此修改,方便用户扫描二维码给框架进行赞赏
2025-03-13 17:49:35 +08:00
1638245306
7555ba1707 Merge remote-tracking branch 'origin/develop' into develop 2025-03-13 17:40:39 +08:00
1638245306
341ea62412 feat(system): 优化配置管理数组展示方式
- 新增 crudTable 组件用于数组数据的展示和编辑
- 替换原有的 vxe-table 实现,简化代码结构
- 优化数组类型表单项的渲染逻辑
- 调整环境变量中的 API 地址
2025-03-13 17:40:30 +08:00
liqiang
2ab80758f0 fix(permission): 修复权限获取 bug
- 将 MenuButton 查询结果改为 values_list,获取多个匹配项的 ID
- 更新 RoleMenuButtonPermission 查询条件,支持多个菜单按钮 ID
2025-03-02 01:00:43 +08:00
liqiang
a030409c46 fix(system): 修复权限获取 bug
- 优化权限查询逻辑,先判断菜单按钮对象是否存在
- 修复了在某些情况下可能导致权限获取失败的问题
2025-03-02 00:05:44 +08:00
liqiang
036d8da9b6 build(web): 更新基础镜像版本
- 将 dvadmin3-base-web 镜像版本从16.19-alpine升级到 18.20-alpine
2025-03-02 00:04:42 +08:00
阿辉
7fb219bb31 文件选择器修复清空时会报错null的bug 2025-02-28 18:42:28 +08:00
阿辉
b10c3ebdc0 文件选择器修复windows下文件路径反斜杠问题 2025-02-24 10:26:49 +08:00
阿辉
778401dd2d Merge branch 'develop' of https://e.coding.net/dvadmin-private/code/dvadmin3 into develop 2025-02-21 17:11:44 +08:00
阿辉
418c78fa83 文件选择器多选图片情况下无法删除图片 2025-02-21 17:11:41 +08:00
1638245306
48d3d86017 Merge remote-tracking branch 'origin/develop' into develop 2025-02-09 23:17:35 +08:00
1638245306
c6a2073537 feat(utils): 完善字段权限控制并添加角色过滤
- 添加了对普通用户进行字段权限过滤的逻辑
- 使用 deepcopy 复制 serializer_class.Meta 以避免直接修改原类
- 修改 get_menu_field 方法,根据用户角色过滤字段权限
2025-02-09 23:17:26 +08:00
7emotions
78ec0ce069 fix: 修复导出Users配置缺失的role与dept 2025-02-06 20:20:12 +08:00
7emotions
857d2940f8 fix: 修复RoleMenuButtonInitSerializer重置逻辑 2025-02-06 19:56:20 +08:00
7emotions
82e689fd83 feat: 导出角色权限配置与按钮权限 2025-02-06 19:21:14 +08:00
liqiang
0cda4c04f7 默认添加dvadmin3-celery 2025-02-06 16:37:52 +08:00
7emotions
91742ee27b feat: 从表结构生成前端基础文件 2025-02-04 00:10:46 +08:00
liqiang
3c6afdac76 支持npm 安装的@great-dream插件动态菜单 2025-02-02 09:04:13 +08:00
liqiang
f299889c4a 支持npm 安装的@great-dream插件动态菜单 2025-02-01 23:29:52 +08:00
liqiang
c82fcbb468 celery 优化 2025-02-01 18:29:51 +08:00
liqiang
eaf5fdcc55 框架优化 2025-02-01 18:29:43 +08:00
liqiang
40b7bd2c94 组件更新 2025-01-30 12:55:58 +08:00
liqiang
868621a3f1 Merge branch 'master' into develop 2025-01-30 12:47:26 +08:00
dvadmin
eefc43d863 !95 角色授权增加默认接口权限
1.授权时带入默认权限
2.查看权限,如果单接口权限与默认接口权限一致,显示默认接口权限
3.复选框等宽显示
2025-01-30 04:44:58 +00:00
dvadmin
8b21a69a15 !104 feat: 优化docker部署
Merge pull request !104 from lorenzo/master
2025-01-30 04:44:28 +00:00
dvadmin
0964c6300a !103 [fix]修复bug
[fix]修复更新时间晒选取值错误bug
[fix]优化commonCrudConfig写法,兼容ts语法,修复使用commonCrudConfig IDE提示飘红的问题&增加width可自定义功能
[fix]修复版本号生成可能为undefind的bug&开发环境版本不进行版本校验代码不生效bug
2025-01-30 04:43:07 +00:00
dvadmin
8c2db4f5ff !101 增加角色批量授权用户
Merge pull request !101 from lxy/cherry-pick-1736306630
2025-01-30 04:42:23 +00:00
liqiang
e15a49a2bd 支持外链接 2025-01-30 12:40:30 +08:00
lorenzo
ecfad1952a feat: 优化docker部署
Signed-off-by: lorenzo <paradise_c@qq.com>
2025-01-27 15:57:12 +00:00
lorenzo
92b8cde7ae feat: 优化docker部署
Signed-off-by: lorenzo <paradise_c@qq.com>
2025-01-27 15:54:13 +00:00
vFeng
6fbe3a214d [fix]注释无效接口dept_lazy_tree
Signed-off-by: vFeng <1914007838@qq.com>
2025-01-27 09:01:17 +00:00
vFeng
4695066e11 [fix]修复开发环境无需校验前端版本bug
Signed-off-by: vFeng <1914007838@qq.com>
2025-01-27 06:33:37 +00:00
vFeng
d125eac5a6 [fix]修复版本号生成可能为undefind的bug&开发环境版本不进行版本校验代码不生效bug
Signed-off-by: vFeng <1914007838@qq.com>
2025-01-27 06:22:21 +00:00
vFeng
83d6565cad [fix]优化commonCrudConfig写法,兼容ts语法,修复使用commonCrudConfig IDE提示飘红的问题&增加width可自定义功能
Signed-off-by: vFeng <1914007838@qq.com>
2025-01-27 05:33:54 +00:00
vFeng
1e2a9a652e [fix]修复更新时间晒选取值错误bug
Signed-off-by: vFeng <1914007838@qq.com>
2025-01-27 05:25:49 +00:00
阿辉
c781d1f559 文件选择器支持image类型的多选 2025-01-22 15:53:01 +08:00
lxy
ffc6246105 增加角色批量授权用户
(cherry picked commit from <gitee.com//lxy0722/django-vue3-admin/commit/702eb7d7aeb2a9c30416a8ebb503a4b8e3367492>
2025-01-08 03:23:50 +00:00
1638245306
77bfc87679 Merge remote-tracking branch 'origin/develop' into develop 2025-01-07 21:38:22 +08:00
1638245306
6ab0c3e758 refactor: 修复主键列表字段的 Swagger 文档生成
- 更新 keys 变量定义,使用 Schema 嵌套来正确表示主键列表的类型
-优化 Swagger 文档中的请求体定义,提高 API 文档的准确性和可读性
2025-01-07 20:06:24 +08:00
lxy
9d230e4752 角色授权增加默认设置
(cherry picked commit from <gitee.com//huge-dream/django-vue3-admin/commit/06a772fcd5335c272159d9c45590cf8b559210d1>
2025-01-02 06:28:06 +00:00
192 changed files with 10988 additions and 4105 deletions

2
.gitignore vendored
View File

@@ -5,3 +5,5 @@
.history/ .history/
.vscode/ .vscode/
web/package-lock.json web/package-lock.json
*.bat

View File

@@ -54,16 +54,15 @@
## 交流 ## 交流
- 交流社区:[戳我](https://bbs.django-vue-admin.com)👩‍👦‍👦 - 交流社区:[戳我](https://bbs.django-vue-admin.com)👩‍👦‍👦
- 插件市场:[戳我](https://bbs.django-vue-admin.com/plugMarket.html)👩‍👦‍👦 - 插件市场:[戳我](https://bbs.django-vue-admin.com/plugMarket.html)👩‍👦‍👦
- django-vue-admin交流01群(已满)812482043 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=aJVwjDvH-Es4MPJQuoO32N0SucK22TE5&jump_from=webapi) - django-vue-admin交流01群(已满)812482043 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=aJVwjDvH-Es4MPJQuoO32N0SucK22TE5&jump_from=webapi)
- django-vue-admin交流02群(已满)687252418 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=4jJN4IjWGfxJ8YJXbb_gTsuWjR34WLdc&jump_from=webapi) - django-vue-admin交流02群(已满)687252418 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=4jJN4IjWGfxJ8YJXbb_gTsuWjR34WLdc&jump_from=webapi)
- django-vue-admin交流03群442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213) - django-vue-admin交流03群(已满)442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213)
- django-vue-admin交流04群442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213)
- 二维码
<img src='https://images.gitee.com/uploads/images/2022/0530/233203_5fb11883_5074988.jpeg' width='200'>
## 源码地址 ## 源码地址
@@ -88,7 +87,19 @@ github地址[https://github.com/huge-dream/django-vue3-admin](https://github.
13. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html)基于Django-Vue-Admin框架开发的应用和插件。 13. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html)基于Django-Vue-Admin框架开发的应用和插件。
## 插件市场 🔌 ## 插件市场 🔌
更新中... 1. #### [dvadmin3-folw 后台审批流插件](https://bbs.django-vue-admin.com/plugMarket/139.html)
2. #### [dvadmin3 celery插件前端](https://bbs.django-vue-admin.com/plugMarket/134.html)
3. #### [dvadmin3 celery插件后端](https://bbs.django-vue-admin.com/plugMarket/133.html)
4. #### [dvadmin3-build插件](https://bbs.django-vue-admin.com/plugMarket/136.html)
5. #### [dvadmin3-uniapp](https://e.coding.net/dvadmin-private/code/dvadmin3-uniapp-app.git)
6. #### dvadmin3-folw-uniapp 审批(开发中,近期上线)
## 仓库分支说明 💈 ## 仓库分支说明 💈
主分支master稳定版本 主分支master稳定版本
@@ -210,5 +221,19 @@ docker-compose up -d --build
![image-10](https://foruda.gitee.com/images/1701350501421625746/f8dd215e_5074988.png) ![image-10](https://foruda.gitee.com/images/1701350501421625746/f8dd215e_5074988.png)
## 审批流插件
![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/97fbbf29673edfd66a1edd49237791bb.png)
![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/c43aa51278cbc478287c718d22397479.png)
![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/9732a5cca9c1166d1a65c35e313ab90d.png)
![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/3ca9dd0801ce76d21435abcc8a3d505a.png)
![输入链接说明](https://bbs.django-vue-admin.com/uploads/20250321/a87a8d2329ef66880af5b0f16c5ff823.png)

2
backend/.gitignore vendored
View File

@@ -93,10 +93,8 @@ ENV/
*.pyc *.pyc
conf/* conf/*
!conf/env.example.py !conf/env.example.py
db.sqlite3
media/ media/
__pypackages__/ __pypackages__/
package-lock.json package-lock.json
gunicorn.pid gunicorn.pid
plugins/*
!plugins/__init__.py !plugins/__init__.py

View File

@@ -8,9 +8,7 @@ https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
""" """
import os import os
from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter
from channels.security.websocket import AllowedHostsOriginValidator
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
@@ -18,15 +16,6 @@ os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
http_application = get_asgi_application() http_application = get_asgi_application()
from application.routing import websocket_urlpatterns
application = ProtocolTypeRouter({ application = ProtocolTypeRouter({
"http": http_application, "http": http_application,
'websocket': AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(
websocket_urlpatterns # 指明路由文件是devops/routing.py
)
)
),
}) })

View File

@@ -1,6 +1,8 @@
import functools import functools
import os import os
from celery.signals import task_postrun
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
from django.conf import settings from django.conf import settings
@@ -38,3 +40,12 @@ def retry_base_task_error():
return wrapper return wrapper
return wraps return wraps
@task_postrun.connect
def add_periodic_task_name(sender, task_id, task, args, kwargs, **extras):
periodic_task_name = kwargs.get('periodic_task_name')
if periodic_task_name:
from django_celery_results.models import TaskResult
# 更新 TaskResult 表中的 periodic_task_name 字段
TaskResult.objects.filter(task_id=task_id).update(periodic_task_name=periodic_task_name)

View File

@@ -1,7 +0,0 @@
# -*- coding: utf-8 -*-
from django.urls import path
from application.websocketConfig import MegCenter
websocket_urlpatterns = [
path('ws/<str:service_uid>/', MegCenter.as_asgi()), # consumers.DvadminWebSocket 是该路由的消费者
]

View File

@@ -28,7 +28,7 @@ from conf.env import *
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure--z8%exyzt7e_%i@1+#1mm=%lb5=^fx_57=1@a+_y7bg5-w%)sm" SECRET_KEY = "django-insecure--z8%exyz2sae4e_%i@1+#1mm=%lb5=^fx_57=1@a+_y7bg5-w%)sm"
# 初始化plugins插件路径到环境变量中 # 初始化plugins插件路径到环境变量中
PLUGINS_PATH = os.path.join(BASE_DIR, "plugins") PLUGINS_PATH = os.path.join(BASE_DIR, "plugins")
sys.path.insert(0, os.path.join(PLUGINS_PATH)) sys.path.insert(0, os.path.join(PLUGINS_PATH))
@@ -399,11 +399,16 @@ DICTIONARY_CONFIG = {}
# ================================================= # # ================================================= #
# 租户共享app # 租户共享app
TENANT_SHARED_APPS = [] TENANT_SHARED_APPS = []
# 普通租户独有app
TENANT_EXCLUSIVE_APPS = []
# 插件 urlpatterns # 插件 urlpatterns
PLUGINS_URL_PATTERNS = [] PLUGINS_URL_PATTERNS = []
# 所有模式有的
SHARED_APPS = []
# ********** 一键导入插件配置开始 ********** # ********** 一键导入插件配置开始 **********
# 例如: # 例如:
# from dvadmin_upgrade_center.settings import * # 升级中心 # from dvadmin_upgrade_center.settings import * # 升级中心
from code_info.settings import * # celery 异步任务
# from dvadmin3_celery.settings import * # celery 异步任务 # from dvadmin3_celery.settings import * # celery 异步任务
# from dvadmin_third.settings import * # 第三方用户管理 # from dvadmin_third.settings import * # 第三方用户管理
# from dvadmin_ak_sk.settings import * # 秘钥管理管理 # from dvadmin_ak_sk.settings import * # 秘钥管理管理
@@ -412,3 +417,22 @@ PLUGINS_URL_PATTERNS = []
#from dvadmin_uniapp.settings import * #from dvadmin_uniapp.settings import *
# ... # ...
# ********** 一键导入插件配置结束 ********** # ********** 一键导入插件配置结束 **********
if locals().get('ENVIRONMENT') != 'local':
# 1. 定义统一的迁移文件存放目录(例如:`./all_migrations/`
BASE_MIGRATIONS_DIR = "all_migrations"
# 2. 自动生成 MIGRATION_MODULES
# 2. 定义需要排除的 App如 Django 内置 App 或第三方 App
EXCLUDED_APPS = ('django','admin','auth.','contenttypes.','sessions.','psqlextra','rest_framework','drf_yasg',)
MIGRATION_MODULES = {}
for app in INSTALLED_APPS:
if app.startswith(EXCLUDED_APPS):
continue
# 判断是否存在目录并在目录下创建一个__init__.py 文件
app_migrations_dir = os.path.join(BASE_DIR, BASE_MIGRATIONS_DIR, app.split('.')[-1], 'migrations')
if not os.path.exists(app_migrations_dir):
os.makedirs(os.path.join(app_migrations_dir))
open(os.path.join(BASE_DIR, BASE_MIGRATIONS_DIR, app.split('.')[-1],'migrations', '__init__.py'), 'w').close()
MIGRATION_MODULES.update(
{app.split('.')[-1]: f"{BASE_MIGRATIONS_DIR}.{app.split('.')[-1]}.migrations"})

View File

@@ -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

View File

@@ -24,6 +24,7 @@ from rest_framework_simplejwt.views import (
from application import dispatch from application import dispatch
from application import settings from application import settings
from application.sse_views import sse_view
from dvadmin.system.views.dictionary import InitDictionaryViewSet from dvadmin.system.views.dictionary import InitDictionaryViewSet
from dvadmin.system.views.login import ( from dvadmin.system.views.login import (
LoginView, LoginView,
@@ -40,6 +41,7 @@ dispatch.init_system_config()
dispatch.init_dictionary() dispatch.init_dictionary()
# =========== 初始化系统配置 ================= # =========== 初始化系统配置 =================
permission_classes = [permissions.AllowAny, ] if settings.DEBUG else [permissions.IsAuthenticated, ]
schema_view = get_schema_view( schema_view = get_schema_view(
openapi.Info( openapi.Info(
title="Snippets API", title="Snippets API",
@@ -50,7 +52,7 @@ schema_view = get_schema_view(
license=openapi.License(name="BSD License"), license=openapi.License(name="BSD License"),
), ),
public=True, public=True,
permission_classes=(permissions.AllowAny,), permission_classes=permission_classes,
generator_class=CustomOpenAPISchemaGenerator, generator_class=CustomOpenAPISchemaGenerator,
) )
# 前端页面映射 # 前端页面映射
@@ -115,6 +117,8 @@ urlpatterns = (
# 前端页面映射 # 前端页面映射
path('web/', web_view, name='web_view'), path('web/', web_view, name='web_view'),
path('web/<path:filename>', serve_web_files, name='serve_web_files'), path('web/<path:filename>', 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.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+ static(settings.STATIC_URL, document_root=settings.STATIC_URL) + static(settings.STATIC_URL, document_root=settings.STATIC_URL)

View File

@@ -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}
}
)

BIN
backend/db.sqlite3 Normal file

Binary file not shown.

View File

@@ -2,7 +2,7 @@
import os import os
exclude = ["venv"] # 需要排除的文件目录 exclude = ["venv", ".venv"] # 需要排除的文件目录
for root, dirs, files in os.walk('.'): for root, dirs, files in os.walk('.'):
dirs[:] = list(set(dirs) - set(exclude)) dirs[:] = list(set(dirs) - set(exclude))
if 'migrations' in dirs: if 'migrations' in dirs:

View File

@@ -4,3 +4,7 @@ from django.apps import AppConfig
class SystemConfig(AppConfig): class SystemConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'dvadmin.system' name = 'dvadmin.system'
def ready(self):
# 注册信号
import dvadmin.system.signals # 确保路径正确

View File

@@ -19,6 +19,20 @@ class UsersInitSerializer(CustomModelSerializer):
""" """
初始化获取数信息(用于生成初始化json文件) 初始化获取数信息(用于生成初始化json文件)
""" """
role_key = serializers.SerializerMethodField()
dept_key = serializers.SerializerMethodField()
def get_dept_key(self, obj):
if obj.dept:
return obj.dept.key
else:
return None
def get_role_key(self, obj):
if obj.role.all():
return [role.key for role in obj.role.all()]
else:
return []
def save(self, **kwargs): def save(self, **kwargs):
instance = super().save(**kwargs) instance = super().save(**kwargs)
@@ -35,7 +49,7 @@ class UsersInitSerializer(CustomModelSerializer):
model = Users model = Users
fields = ["username", "email", 'mobile', 'avatar', "name", 'gender', 'user_type', "dept", 'user_type', fields = ["username", "email", 'mobile', 'avatar', "name", 'gender', 'user_type', "dept", 'user_type',
'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'creator', 'dept_belong_id', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'creator', 'dept_belong_id',
'password', 'last_login', 'is_superuser'] 'password', 'last_login', 'is_superuser', 'role_key' ,'dept_key']
read_only_fields = ['id'] read_only_fields = ['id']
extra_kwargs = { extra_kwargs = {
'creator': {'write_only': True}, 'creator': {'write_only': True},
@@ -175,15 +189,21 @@ class RoleMenuInitSerializer(CustomModelSerializer):
""" """
初始化角色菜单(用于生成初始化json文件) 初始化角色菜单(用于生成初始化json文件)
""" """
role__key = serializers.CharField(max_length=100, required=True) role__key = serializers.CharField(source='role.key')
menu__web_path = serializers.CharField(max_length=100, required=True) menu__web_path = serializers.CharField(source='menu.web_path')
menu__component_name = serializers.CharField(max_length=100, required=True, allow_blank=True) menu__component_name = serializers.CharField(source='menu.component_name', allow_blank=True)
def update(self, instance, validated_data):
init_data = self.initial_data
role_id = Role.objects.filter(key=init_data['role__key']).first()
menu_id = Menu.objects.filter(web_path=init_data['menu__web_path'], component_name=init_data['menu__component_name']).first()
validated_data['role'] = role_id
validated_data['menu'] = menu_id
return super().update(instance, validated_data)
def create(self, validated_data): def create(self, validated_data):
init_data = self.initial_data init_data = self.initial_data
validated_data.pop('menu__web_path')
validated_data.pop('menu__component_name')
validated_data.pop('role__key')
role_id = Role.objects.filter(key=init_data['role__key']).first() role_id = Role.objects.filter(key=init_data['role__key']).first()
menu_id = Menu.objects.filter(web_path=init_data['menu__web_path'], component_name=init_data['menu__component_name']).first() menu_id = Menu.objects.filter(web_path=init_data['menu__web_path'], component_name=init_data['menu__component_name']).first()
validated_data['role'] = role_id validated_data['role'] = role_id
@@ -206,14 +226,22 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer):
""" """
初始化角色菜单按钮(用于生成初始化json文件) 初始化角色菜单按钮(用于生成初始化json文件)
""" """
role__key = serializers.CharField(max_length=100, required=True) role__key = serializers.CharField(source='role.key')
menu_button__value = serializers.CharField(max_length=100, required=True) menu_button__value = serializers.CharField(source='menu_button.value')
data_range = serializers.CharField(max_length=100, required=False) data_range = serializers.CharField(max_length=100, required=False)
def update(self, instance, validated_data):
init_data = self.initial_data
role_id = Role.objects.filter(key=init_data['role__key']).first()
menu_button_id = MenuButton.objects.filter(value=init_data['menu_button__value']).first()
validated_data['role'] = role_id
validated_data['menu_button'] = menu_button_id
instance = super().create(validated_data)
instance.dept.set([])
return super().update(instance, validated_data)
def create(self, validated_data): def create(self, validated_data):
init_data = self.initial_data init_data = self.initial_data
validated_data.pop('menu_button__value')
validated_data.pop('role__key')
role_id = Role.objects.filter(key=init_data['role__key']).first() role_id = Role.objects.filter(key=init_data['role__key']).first()
menu_button_id = MenuButton.objects.filter(value=init_data['menu_button__value']).first() menu_button_id = MenuButton.objects.filter(value=init_data['menu_button__value']).first()
validated_data['role'] = role_id validated_data['role'] = role_id
@@ -223,7 +251,7 @@ class RoleMenuButtonInitSerializer(CustomModelSerializer):
return instance return instance
def save(self, **kwargs): def save(self, **kwargs):
if self.instance and self.initial_data.get('reset'): if not self.instance or self.initial_data.get('reset'):
return super().save(**kwargs) return super().save(**kwargs)
return self.instance return self.instance

View File

@@ -1,6 +1,6 @@
[ [
{ {
"name": "DVAdmin团队", "name": "总部",
"key": "dvadmin", "key": "dvadmin",
"sort": 1, "sort": 1,
"owner": "", "owner": "",
@@ -11,7 +11,7 @@
"children": [ "children": [
{ {
"name": "运营部", "name": "运营部",
"key": "", "key": "zongbu",
"sort": 2, "sort": 2,
"owner": "", "owner": "",
"phone": "", "phone": "",

View File

@@ -546,5 +546,50 @@
"children": [] "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": []
}
]
} }
] ]

View File

@@ -11,326 +11,11 @@
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
"parent": null,
"children": [ "children": [
{
"name": "菜单管理",
"icon": "iconfont icon-caidan",
"sort": 1,
"is_link": false,
"is_catalog": false,
"web_path": "/menu",
"component": "system/menu/index",
"component_name": "menu",
"status": true,
"cache": false,
"visible": true,
"parent": 1,
"children": [],
"menu_button": [
{
"name": "查询",
"value": "menu:Search",
"api": "/api/system/menu/",
"method": 0
},
{
"name": "详情",
"value": "menu:Retrieve",
"api": "/api/system/menu/{id}/",
"method": 0
},
{
"name": "查询所有",
"value": "menu:SearchAll",
"api": "/api/system/menu/get_all_menu/",
"method": 0
},
{
"name": "路由",
"value": "menu:router",
"api": "/api/system/menu/web_router/",
"method": 0
},
{
"name": "查询按钮权限",
"value": "btn:Search",
"api": "/api/system/menu_button/",
"method": 0
},
{
"name": "查询列权限",
"value": "column:Search",
"api": "/api/system/column/",
"method": 0
},
{
"name": "新增",
"value": "menu:Create",
"api": "/api/system/menu/",
"method": 1
},
{
"name": "上移",
"value": "menu:MoveUp",
"api": "/api/system/menu/mode_up/",
"method": 1
},
{
"name": "下移",
"value": "menu:MoveDown",
"api": "/api/system/menu/mode_down/",
"method": 1
},
{
"name": "新增按钮权限",
"value": "btn:Create",
"api": "/api/system/menu_button/",
"method": 1
},
{
"name": "新增列权限",
"value": "column:Create",
"api": "/api/system/column/",
"method": 1
},
{
"name": "自动匹配列权限",
"value": "column:Match",
"api": "/api/system/column/auto_match_fields/",
"method": 1
},
{
"name": "编辑",
"value": "menu:Update",
"api": "/api/system/menu/{id}/",
"method": 2
},
{
"name": "修改按钮权限",
"value": "btn:Update",
"api": "/api/system/menu_button/{id}/",
"method": 2
},
{
"name": "编辑列权限",
"value": "column:Update",
"api": "/api/system/column/{id}/",
"method": 2
},
{
"name": "删除",
"value": "menu:Delete",
"api": "/api/system/menu/{id}/",
"method": 3
},
{
"name": "删除按钮权限",
"value": "btn:Delete",
"api": "/api/system/menu_button/{id}/",
"method": 3
},
{
"name": "删除列权限",
"value": "column:Delete",
"api": "/api/system/column/{id}/",
"method": 3
}
],
"menu_field": []
},
{
"name": "部门管理",
"icon": "ele-OfficeBuilding",
"sort": 3,
"is_link": false,
"is_catalog": false,
"web_path": "/dept",
"component": "system/dept/index",
"component_name": "dept",
"status": true,
"cache": false,
"visible": true,
"parent": 1,
"children": [],
"menu_button": [
{
"name": "查询",
"value": "dept:Search",
"api": "/api/system/dept/",
"method": 0
},
{
"name": "详情",
"value": "dept:Retrieve",
"api": "/api/system/dept/{id}/",
"method": 0
},
{
"name": "获取所有部门",
"value": "dept:SearchAll",
"api": "/api/system/dept/all_dept/",
"method": 0
},
{
"name": "部门顶部信息",
"value": "dept:HeaderInfo",
"api": "/api/system/dept/dept_info/",
"method": 0
},
{
"name": "新增",
"value": "dept:Create",
"api": "/api/system/dept/",
"method": 1
},
{
"name": "上移",
"value": "dept:MoveUp",
"api": "/api/system/dept/mode_up/",
"method": 1
},
{
"name": "下移",
"value": "dept:MoveDown",
"api": "/api/system/dept/mode_down/",
"method": 1
},
{
"name": "编辑",
"value": "dept:Update",
"api": "/api/system/dept/{id}/",
"method": 2
},
{
"name": "删除",
"value": "dept:Delete",
"api": "/api/system/dept/{id}/",
"method": 3
}
],
"menu_field": []
},
{
"name": "角色管理",
"icon": "ele-ColdDrink",
"sort": 4,
"is_link": false,
"is_catalog": false,
"web_path": "/role",
"component": "system/role/index",
"component_name": "role",
"status": true,
"cache": false,
"visible": true,
"parent": 1,
"children": [],
"menu_button": [
{
"name": "查询",
"value": "role:Search",
"api": "/api/system/role/",
"method": 0
},
{
"name": "详情",
"value": "role:Retrieve",
"api": "/api/system/role/{id}/",
"method": 0
},
{
"name": "权限配置",
"value": "role:Permission",
"api": "/api/system/role/{id}/",
"method": 0
},
{
"name": "新增",
"value": "role:Create",
"api": "/api/system/role/",
"method": 1
},
{
"name": "编辑",
"value": "role:Update",
"api": "/api/system/role/{id}/",
"method": 2
},
{
"name": "保存",
"value": "role:Save",
"api": "/api/system/role/{id}/",
"method": 2
},
{
"name": "删除",
"value": "role:Delete",
"api": "/api/system/role/{id}/",
"method": 3
}
],
"menu_field": [
{
"field_name": "create_datetime",
"title": "创建时间",
"model": "Role"
},
{
"field_name": "creator",
"title": "创建人",
"model": "Role"
},
{
"field_name": "dept_belong_id",
"title": "数据归属部门",
"model": "Role"
},
{
"field_name": "description",
"title": "描述",
"model": "Role"
},
{
"field_name": "id",
"title": "Id",
"model": "Role"
},
{
"field_name": "key",
"title": "权限字符",
"model": "Role"
},
{
"field_name": "modifier",
"title": "修改人",
"model": "Role"
},
{
"field_name": "name",
"title": "角色名称",
"model": "Role"
},
{
"field_name": "sort",
"title": "角色顺序",
"model": "Role"
},
{
"field_name": "status",
"title": "角色状态",
"model": "Role"
},
{
"field_name": "update_datetime",
"title": "修改时间",
"model": "Role"
}
]
},
{ {
"name": "用户管理", "name": "用户管理",
"icon": "iconfont icon-icon-", "icon": "iconfont icon-icon-",
"sort": 6, "sort": 1,
"is_link": false, "is_link": false,
"is_catalog": false, "is_catalog": false,
"web_path": "/user", "web_path": "/user",
@@ -339,7 +24,6 @@
"status": true, "status": true,
"cache": false, "cache": false,
"visible": true, "visible": true,
"parent": 1,
"children": [], "children": [],
"menu_button": [ "menu_button": [
{ {
@@ -348,18 +32,24 @@
"api": "/api/system/user/", "api": "/api/system/user/",
"method": 0 "method": 0
}, },
{
"name": "详情",
"value": "user:Retrieve",
"api": "/api/system/user/{id}/",
"method": 0
},
{ {
"name": "新增", "name": "新增",
"value": "user:Create", "value": "user:Create",
"api": "/api/system/user/", "api": "/api/system/user/",
"method": 1 "method": 1
}, },
{
"name": "编辑",
"value": "user:Update",
"api": "/api/system/user/{id}/",
"method": 2
},
{
"name": "删除",
"value": "user:Delete",
"api": "/api/system/user/{id}/",
"method": 3
},
{ {
"name": "导出", "name": "导出",
"value": "user:Export", "value": "user:Export",
@@ -373,10 +63,16 @@
"method": 1 "method": 1
}, },
{ {
"name": "编辑", "name": "获取导入模板",
"value": "user:Update", "value": "user:ImportTemplate",
"api": "/api/system/user/{id}/", "api": "/api/system/user/import/",
"method": 2 "method": 0
},
{
"name": "批量更新模板",
"value": "user:BatchUpdateTemplate",
"api": "/api/system/user/update_template/",
"method": 0
}, },
{ {
"name": "重设密码", "name": "重设密码",
@@ -386,15 +82,9 @@
}, },
{ {
"name": "重置密码", "name": "重置密码",
"value": "user:DefaultPassword", "value": "user:ResetDefaultPassword",
"api": "/api/system/user/{id}/reset_to_default_password/", "api": "/api/system/user/{id}/reset_to_default_password/",
"method": 2 "method": 2
},
{
"name": "删除",
"value": "user:Delete",
"api": "/api/system/user/{id}/",
"method": 3
} }
], ],
"menu_field": [ "menu_field": [
@@ -475,6 +165,359 @@
} }
] ]
}, },
{
"name": "菜单管理",
"icon": "iconfont icon-caidan",
"sort": 2,
"is_link": false,
"is_catalog": false,
"web_path": "/menu",
"component": "system/menu/index",
"component_name": "menu",
"status": true,
"cache": false,
"visible": true,
"children": [],
"menu_button": [
{
"name": "查询",
"value": "menu:Search",
"api": "/api/system/menu/",
"method": 0
},
{
"name": "单例",
"value": "menu:Retrieve",
"api": "/api/system/menu/{id}/",
"method": 0
},
{
"name": "新增",
"value": "menu:Create",
"api": "/api/system/menu/",
"method": 1
},
{
"name": "编辑",
"value": "menu:Update",
"api": "/api/system/menu/{id}/",
"method": 2
},
{
"name": "删除",
"value": "menu:Delete",
"api": "/api/system/menu/{id}/",
"method": 3
},
{
"name": "查询所有",
"value": "menu:SearchAll",
"api": "/api/system/menu/get_all_menu/",
"method": 0
},
{
"name": "路由",
"value": "menu:router",
"api": "/api/system/menu/web_router/",
"method": 0
},
{
"name": "查询按钮",
"value": "menu:SearchButton",
"api": "/api/system/menu_button/",
"method": 0
},
{
"name": "新增按钮",
"value": "menu:CreateButton",
"api": "/api/system/menu_button/",
"method": 1
},
{
"name": "编辑按钮",
"value": "menu:UpdateButton",
"api": "/api/system/menu_button/{id}/",
"method": 2
},
{
"name": "删除按钮",
"value": "menu:DeleteButton",
"api": "/api/system/menu_button/{id}/",
"method": 3
},
{
"name": "上移",
"value": "menu:MoveUp",
"api": "/api/system/menu/mode_up/",
"method": 1
},
{
"name": "下移",
"value": "menu:MoveDown",
"api": "/api/system/menu/mode_down/",
"method": 1
},
{
"name": "查询列权限",
"value": "column:Search",
"api": "/api/system/column/",
"method": 0
},
{
"name": "新增列权限",
"value": "column:Create",
"api": "/api/system/column/",
"method": 1
},
{
"name": "编辑列权限",
"value": "column:Update",
"api": "/api/system/column/{id}/",
"method": 2
},
{
"name": "删除列权限",
"value": "column:Delete",
"api": "/api/system/column/{id}/",
"method": 3
},
{
"name": "自动匹配列权限",
"value": "column:Match",
"api": "/api/system/column/auto_match_fields/",
"method": 1
}
],
"menu_field": []
},
{
"name": "部门管理",
"icon": "ele-OfficeBuilding",
"sort": 3,
"is_link": false,
"is_catalog": false,
"web_path": "/dept",
"component": "system/dept/index",
"component_name": "dept",
"status": true,
"cache": false,
"visible": true,
"children": [],
"menu_button": [
{
"name": "查询",
"value": "dept:Search",
"api": "/api/system/dept/",
"method": 0
},
{
"name": "详情",
"value": "dept:Retrieve",
"api": "/api/system/dept/{id}/",
"method": 0
},
{
"name": "获取所有部门",
"value": "dept:SearchAll",
"api": "/api/system/dept/all_dept/",
"method": 0
},
{
"name": "部门顶部信息",
"value": "dept:HeaderInfo",
"api": "/api/system/dept/dept_info/",
"method": 0
},
{
"name": "新增",
"value": "dept:Create",
"api": "/api/system/dept/",
"method": 1
},
{
"name": "上移",
"value": "dept:MoveUp",
"api": "/api/system/dept/mode_up/",
"method": 1
},
{
"name": "下移",
"value": "dept:MoveDown",
"api": "/api/system/dept/mode_down/",
"method": 1
},
{
"name": "编辑",
"value": "dept:Update",
"api": "/api/system/dept/{id}/",
"method": 2
},
{
"name": "删除",
"value": "dept:Delete",
"api": "/api/system/dept/{id}/",
"method": 3
}
],
"menu_field": []
},
{
"name": "角色管理",
"icon": "ele-ColdDrink",
"sort": 4,
"is_link": false,
"is_catalog": false,
"web_path": "/role",
"component": "system/role/index",
"component_name": "role",
"status": true,
"cache": false,
"visible": true,
"children": [],
"menu_button": [
{
"name": "查询",
"value": "role:Search",
"api": "/api/system/role/",
"method": 0
},
{
"name": "单例",
"value": "role:Retrieve",
"api": "/api/system/role/{id}/",
"method": 0
},
{
"name": "新增",
"value": "role:Create",
"api": "/api/system/role/",
"method": 1
},
{
"name": "编辑",
"value": "role:Update",
"api": "/api/system/role/{id}/",
"method": 2
},
{
"name": "删除",
"value": "role:Delete",
"api": "/api/system/role/{id}/",
"method": 3
},
{
"name": "获取所有可授权数据范围的部门",
"value": "role:AllDataRangeDept",
"api": "/api/system/role_menu_button_permision/role_to_dept_all/",
"method": 0
},
{
"name": "获取所有可授权菜单",
"value": "role:AllCanMenu",
"api": "/api/system/role_menu_button_permision/get_role_menu/",
"method": 0
},
{
"name": "获取所有已授权用户",
"value": "role:AllAuthorizedUser",
"api": "/api/system/role/get_role_users/",
"method": 0
},
{
"name": "获取菜单所有可授权按钮",
"value": "role:AllMenuButton",
"api": "/api/system/role_menu_button_permision/get_role_menu_btn_field/",
"method": 0
},
{
"name": "授权菜单",
"value": "role:SetMenu",
"api": "/api/system/role_menu_button_permision/set_role_menu/",
"method": 2
},
{
"name": "授权菜单按钮",
"value": "role:SetMenuButton",
"api": "/api/system/role_menu_button_permision/set_role_menu_btn/",
"method": 2
},
{
"name": "授权数据范围",
"value": "role:SetDataRange",
"api": "/api/system/role_menu_button_permision/set_role_menu_btn_data_range/",
"method": 2
},
{
"name": "获取所有用户",
"value": "role:AllUser",
"api": "/api/system/user/",
"method": 0
},
{
"name": "授权用户予角色",
"value": "role:SetUserRole",
"api": "/api/system/role/{id}/set_role_users/",
"method": 2
}
],
"menu_field": [
{
"field_name": "create_datetime",
"title": "创建时间",
"model": "Role"
},
{
"field_name": "creator",
"title": "创建人",
"model": "Role"
},
{
"field_name": "dept_belong_id",
"title": "数据归属部门",
"model": "Role"
},
{
"field_name": "description",
"title": "描述",
"model": "Role"
},
{
"field_name": "id",
"title": "Id",
"model": "Role"
},
{
"field_name": "key",
"title": "权限字符",
"model": "Role"
},
{
"field_name": "modifier",
"title": "修改人",
"model": "Role"
},
{
"field_name": "name",
"title": "角色名称",
"model": "Role"
},
{
"field_name": "sort",
"title": "角色顺序",
"model": "Role"
},
{
"field_name": "status",
"title": "角色状态",
"model": "Role"
},
{
"field_name": "update_datetime",
"title": "修改时间",
"model": "Role"
}
]
},
{ {
"name": "消息中心", "name": "消息中心",
"icon": "iconfont icon-xiaoxizhongxin", "icon": "iconfont icon-xiaoxizhongxin",
@@ -690,38 +733,10 @@
"menu_button": [ "menu_button": [
{ {
"name": "查询", "name": "查询",
"value": "Search", "value": "downloadCenter:Search",
"api": "/api/system/downloadCenter/", "api": "/api/system/download_center/"
"method": 0
},
{
"name": "详情",
"value": "Retrieve",
"api": "/api/system/downloadCenter/{id}/",
"method": 0
},
{
"name": "新增",
"value": "Create",
"api": "/api/system/downloadCenter/",
"method": 1
},
{
"name": "编辑",
"value": "Update",
"api": "/api/system/downloadCenter/{id}/",
"method": 2
},
{
"name": "删除",
"value": "Delete",
"api": "/api/system/downloadCenter/{id}/",
"method": 3
}
]
} }
], ],
"menu_button": [],
"menu_field": [] "menu_field": []
}, },
{ {
@@ -1441,4 +1456,8 @@
"menu_button": [], "menu_button": [],
"menu_field": [] "menu_field": []
} }
],
"menu_button": [],
"menu_field": []
}
] ]

View File

@@ -97,7 +97,7 @@
"parent": 1, "parent": 1,
"title": "网站标题", "title": "网站标题",
"key": "site_title", "key": "site_title",
"value": "Dvadmin", "value": "协众防重码",
"sort": 1, "sort": 1,
"status": true, "status": true,
"data_options": null, "data_options": null,
@@ -111,7 +111,7 @@
"parent": 1, "parent": 1,
"title": "网站名称", "title": "网站名称",
"key": "site_name", "key": "site_name",
"value": "企业级后台管理系统", "value": "协众防重码系统",
"sort": 1, "sort": 1,
"status": true, "status": true,
"data_options": null, "data_options": null,
@@ -158,7 +158,7 @@
"parent": 1, "parent": 1,
"title": "版权信息", "title": "版权信息",
"key": "copyright", "key": "copyright",
"value": "2021-2024 django-vue-admin.com 版权所有", "value": "2021-2025 协众防重码系统 版权所有",
"sort": 4, "sort": 4,
"status": true, "status": true,
"data_options": null, "data_options": null,
@@ -177,7 +177,7 @@
"parent": 1, "parent": 1,
"title": "备案信息", "title": "备案信息",
"key": "keep_record", "key": "keep_record",
"value": "晋ICP备18005113号-3", "value": "",
"sort": 5, "sort": 5,
"status": true, "status": true,
"data_options": null, "data_options": null,
@@ -196,7 +196,7 @@
"parent": 1, "parent": 1,
"title": "帮助链接", "title": "帮助链接",
"key": "help_url", "key": "help_url",
"value": "https://django-vue-admin.com", "value": "#",
"sort": 6, "sort": 6,
"status": true, "status": true,
"data_options": null, "data_options": null,
@@ -235,5 +235,252 @@
"children": [] "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": []
}
]
} }
] ]

View File

@@ -16,7 +16,8 @@
"last_name": "", "last_name": "",
"is_staff": true, "is_staff": true,
"is_active": true, "is_active": true,
"password": "pbkdf2_sha256$260000$g17x5wlSiW1FZAh1Eudchw$ZeSAqj3Xak0io8v/pmPW0BX9EX5R2zFXDwbbD68oBFk=", "description": "A01",
"password": "pbkdf2_sha256$600000$ITjuHRKiVCgPpiuCYxAaEp$aA9LDAfujtdMJRstK9YcBPz9a9MkfFG5Tsq1NviWxy0=",
"last_login": null, "last_login": null,
"is_superuser": true "is_superuser": true
}, },
@@ -34,7 +35,8 @@
"last_name": "", "last_name": "",
"is_staff": true, "is_staff": true,
"is_active": true, "is_active": true,
"password": "pbkdf2_sha256$260000$g17x5wlSiW1FZAh1Eudchw$ZeSAqj3Xak0io8v/pmPW0BX9EX5R2zFXDwbbD68oBFk=", "description": "A02",
"password": "pbkdf2_sha256$600000$ITjuHRKiVCgPpiuCYxAaEp$aA9LDAfujtdMJRstK9YcBPz9a9MkfFG5Tsq1NviWxy0=",
"last_login": null, "last_login": null,
"is_superuser": false "is_superuser": false
}, },
@@ -49,11 +51,12 @@
"role": [], "role": [],
"role_key": ["public"], "role_key": ["public"],
"dept_key": "technology", "dept_key": "technology",
"description": "A03",
"first_name": "", "first_name": "",
"last_name": "", "last_name": "",
"is_staff": true, "is_staff": true,
"is_active": true, "is_active": true,
"password": "pbkdf2_sha256$260000$g17x5wlSiW1FZAh1Eudchw$ZeSAqj3Xak0io8v/pmPW0BX9EX5R2zFXDwbbD68oBFk=", "password": "pbkdf2_sha256$600000$ITjuHRKiVCgPpiuCYxAaEp$aA9LDAfujtdMJRstK9YcBPz9a9MkfFG5Tsq1NviWxy0=",
"last_login": null, "last_login": null,
"is_superuser": false "is_superuser": false
} }

View File

@@ -10,7 +10,7 @@ django.setup()
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from application.settings import BASE_DIR from application.settings import BASE_DIR
from dvadmin.system.models import Menu, Users, Dept, Role, ApiWhiteList, Dictionary, SystemConfig from dvadmin.system.models import Menu, Users, Dept, Role, ApiWhiteList, Dictionary, SystemConfig, RoleMenuButtonPermission, RoleMenuPermission
from dvadmin.system.fixtures.initSerializer import UsersInitSerializer, DeptInitSerializer, RoleInitSerializer, \ from dvadmin.system.fixtures.initSerializer import UsersInitSerializer, DeptInitSerializer, RoleInitSerializer, \
MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer, SystemConfigInitSerializer, \ MenuInitSerializer, ApiWhiteListInitSerializer, DictionaryInitSerializer, SystemConfigInitSerializer, \
RoleMenuInitSerializer, RoleMenuButtonInitSerializer RoleMenuInitSerializer, RoleMenuButtonInitSerializer
@@ -29,7 +29,7 @@ class Command(BaseCommand):
def serializer_data(self, serializer, query_set: QuerySet): def serializer_data(self, serializer, query_set: QuerySet):
serializer = serializer(query_set, many=True) serializer = serializer(query_set, many=True)
data = json.loads(json.dumps(serializer.data, ensure_ascii=False)) 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) json.dump(data, f, indent=4, ensure_ascii=False)
return return
@@ -57,6 +57,12 @@ class Command(BaseCommand):
def generate_system_config(self): def generate_system_config(self):
self.serializer_data(SystemConfigInitSerializer, SystemConfig.objects.filter(parent_id__isnull=True)) self.serializer_data(SystemConfigInitSerializer, SystemConfig.objects.filter(parent_id__isnull=True))
def generate_role_menu(self):
self.serializer_data(RoleMenuInitSerializer, RoleMenuPermission.objects.all())
def generate_role_menu_button(self):
self.serializer_data(RoleMenuButtonInitSerializer, RoleMenuButtonPermission.objects.all())
def handle(self, *args, **options): def handle(self, *args, **options):
generate_name = options.get('generate_name') generate_name = options.get('generate_name')
generate_name_dict = { generate_name_dict = {
@@ -67,6 +73,8 @@ class Command(BaseCommand):
"api_white_list": self.generate_api_white_list, "api_white_list": self.generate_api_white_list,
"dictionary": self.generate_dictionary, "dictionary": self.generate_dictionary,
"system_config": self.generate_system_config, "system_config": self.generate_system_config,
"role_menu": self.generate_role_menu,
"role_menu_button": self.generate_role_menu_button,
} }
if not generate_name: if not generate_name:
for ele in generate_name_dict.keys(): for ele in generate_name_dict.keys():

View File

@@ -77,8 +77,14 @@ class Users(CoreModel, AbstractUser):
objects = CustomUserManager() objects = CustomUserManager()
def set_password(self, raw_password): def set_password(self, raw_password):
if raw_password:
super().set_password(hashlib.md5(raw_password.encode(encoding="UTF-8")).hexdigest()) 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: class Meta:
db_table = table_prefix + "system_users" db_table = table_prefix + "system_users"
verbose_name = "用户表" verbose_name = "用户表"
@@ -122,6 +128,27 @@ class Dept(CoreModel):
help_text="上级部门", help_text="上级部门",
) )
@classmethod
def _recursion(cls, instance, parent, result):
new_instance = getattr(instance, parent, None)
res = []
data = getattr(instance, result, None)
if data:
res.append(data)
if new_instance:
array = cls._recursion(new_instance, parent, result)
res += array
return res
@classmethod
def get_region_name(cls, obj):
"""
获取某个用户的递归所有部门名称
"""
dept_name_all = cls._recursion(obj, "parent", "name")
dept_name_all.reverse()
return "/".join(dept_name_all)
@classmethod @classmethod
def recursion_all_dept(cls, dept_id: int, dept_all_list=None, dept_list=None): def recursion_all_dept(cls, dept_id: int, dept_all_list=None, dept_list=None):
""" """

View File

@@ -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() pre_init_complete = Signal()
detail_init_complete = Signal() detail_init_complete = Signal()
@@ -10,3 +16,12 @@ post_tenants_init_complete = Signal()
post_tenants_all_init_complete = Signal() post_tenants_all_init_complete = Signal()
# 租户创建完成信号 # 租户创建完成信号
tenants_create_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) # 设置永不超时的键值对

View File

@@ -49,7 +49,7 @@ urlpatterns = [
path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})), path('system_config/get_relation_info/', SystemConfigViewSet.as_view({'get': 'get_relation_info'})),
# path('login_log/', LoginLogViewSet.as_view({'get': 'list'})), # path('login_log/', LoginLogViewSet.as_view({'get': 'list'})),
# path('login_log/<int:pk>/', LoginLogViewSet.as_view({'get': 'retrieve'})), # path('login_log/<int:pk>/', LoginLogViewSet.as_view({'get': 'retrieve'})),
path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})), # path('dept_lazy_tree/', DeptViewSet.as_view({'get': 'dept_lazy_tree'})),
path('clause/privacy.html', PrivacyView.as_view()), path('clause/privacy.html', PrivacyView.as_view()),
path('clause/terms_service.html', TermsServiceView.as_view()), path('clause/terms_service.html', TermsServiceView.as_view()),
] ]

View File

@@ -44,6 +44,11 @@ class DownloadCenterViewSet(CustomModelViewSet):
extra_filter_class = [] extra_filter_class = []
def get_queryset(self): def get_queryset(self):
# 判断是否是 Swagger 文档生成阶段,防止报错
if getattr(self, 'swagger_fake_view', False):
return self.queryset.model.objects.none()
# 正常请求下的逻辑
if self.request.user.is_superuser: if self.request.user.is_superuser:
return super().get_queryset() return super().get_queryset()
return super().get_queryset().filter(creator=self.request.user) return super().get_queryset().filter(creator=self.request.user)

View File

@@ -35,8 +35,8 @@ class FileSerializer(CustomModelSerializer):
fields = "__all__" fields = "__all__"
def create(self, validated_data): def create(self, validated_data):
file_engine = dispatch.get_system_config_values("fileStorageConfig.file_engine") or 'local' file_engine = dispatch.get_system_config_values("file_storage.file_engine") or 'local'
file_backup = dispatch.get_system_config_values("fileStorageConfig.file_backup") file_backup = dispatch.get_system_config_values("file_storage.file_backup")
file = self.initial_data.get('file') file = self.initial_data.get('file')
file_size = file.size file_size = file.size
validated_data['name'] = str(file) validated_data['name'] = str(file)
@@ -52,15 +52,15 @@ class FileSerializer(CustomModelSerializer):
if file_backup: if file_backup:
validated_data['url'] = file validated_data['url'] = file
if file_engine == 'oss': if file_engine == 'oss':
from dvadmin_cloud_storage.views.aliyun import ali_oss_upload from dvadmin.utils.aliyunoss import ali_oss_upload
file_path = ali_oss_upload(file) file_path = ali_oss_upload(file, file_name=validated_data['name'])
if file_path: if file_path:
validated_data['file_url'] = file_path validated_data['file_url'] = file_path
else: else:
raise ValueError("上传失败") raise ValueError("上传失败")
elif file_engine == 'cos': elif file_engine == 'cos':
from dvadmin_cloud_storage.views.tencent import tencent_cos_upload from dvadmin.utils.tencentcos import tencent_cos_upload
file_path = tencent_cos_upload(file) file_path = tencent_cos_upload(file, file_name=validated_data['name'])
if file_path: if file_path:
validated_data['file_url'] = file_path validated_data['file_url'] = file_path
else: else:

View File

@@ -139,21 +139,6 @@ class MessageCenterTargetUserListSerializer(CustomModelSerializer):
read_only_fields = ["id"] 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): class MessageCenterCreateSerializer(CustomModelSerializer):
""" """
消息中心-新增-序列化器 消息中心-新增-序列化器
@@ -182,10 +167,6 @@ class MessageCenterCreateSerializer(CustomModelSerializer):
targetuser_instance = MessageCenterTargetUserSerializer(data=targetuser_data, many=True, request=self.request) targetuser_instance = MessageCenterTargetUserSerializer(data=targetuser_data, many=True, request=self.request)
targetuser_instance.is_valid(raise_exception=True) targetuser_instance.is_valid(raise_exception=True)
targetuser_instance.save() 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 return data
class Meta: class Meta:
@@ -225,10 +206,6 @@ class MessageCenterViewSet(CustomModelViewSet):
queryset.save() queryset.save()
instance = self.get_object() instance = self.get_object()
serializer = self.get_serializer(instance) 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="获取成功") return DetailResponse(data=serializer.data, msg="获取成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated]) @action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated])

View File

@@ -10,16 +10,17 @@ from rest_framework import serializers
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from dvadmin.system.models import Role, Menu, MenuButton, Dept from dvadmin.system.models import Role, Menu, MenuButton, Dept, Users
from dvadmin.system.views.dept import DeptSerializer from dvadmin.system.views.dept import DeptSerializer
from dvadmin.system.views.menu import MenuSerializer from dvadmin.system.views.menu import MenuSerializer
from dvadmin.system.views.menu_button import MenuButtonSerializer from dvadmin.system.views.menu_button import MenuButtonSerializer
from dvadmin.utils.crud_mixin import FastCrudMixin from dvadmin.utils.crud_mixin import FastCrudMixin
from dvadmin.utils.field_permission import FieldPermissionMixin from dvadmin.utils.field_permission import FieldPermissionMixin
from dvadmin.utils.json_response import SuccessResponse, DetailResponse from dvadmin.utils.json_response import SuccessResponse, DetailResponse, ErrorResponse
from dvadmin.utils.serializers import CustomModelSerializer from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.validator import CustomUniqueValidator from dvadmin.utils.validator import CustomUniqueValidator
from dvadmin.utils.viewset import CustomModelViewSet from dvadmin.utils.viewset import CustomModelViewSet
from dvadmin.utils.permission import CustomPermission
class RoleSerializer(CustomModelSerializer): class RoleSerializer(CustomModelSerializer):
@@ -107,7 +108,6 @@ class MenuButtonPermissionSerializer(CustomModelSerializer):
fields = '__all__' fields = '__all__'
class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin): class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
""" """
角色管理接口 角色管理接口
@@ -142,3 +142,62 @@ class RoleViewSet(CustomModelViewSet, FastCrudMixin,FieldPermissionMixin):
role.users_set.add(*movedKeys) role.users_set.add(*movedKeys)
serializer = RoleSerializer(role) serializer = RoleSerializer(role)
return DetailResponse(data=serializer.data, msg="更新成功") return DetailResponse(data=serializer.data, msg="更新成功")
@action(methods=['GET'], detail=False, permission_classes=[IsAuthenticated, CustomPermission])
def get_role_users(self, request):
"""
获取角色已授权、未授权的用户
已授权的用户:1
未授权的用户:0
"""
role_id = request.query_params.get('role_id', None)
if not role_id:
return ErrorResponse(msg="请选择角色")
if request.query_params.get('authorized', 0) == "1":
queryset = Users.objects.filter(role__id=role_id).exclude(is_superuser=True)
else:
queryset = Users.objects.exclude(role__id=role_id).exclude(is_superuser=True)
if name := request.query_params.get('name', None):
queryset = queryset.filter(name__icontains=name)
if dept := request.query_params.get('dept', None):
queryset = queryset.filter(dept=dept)
page = self.paginate_queryset(queryset.values('id', 'name', 'dept__name'))
if page is not None:
return self.get_paginated_response(page)
return SuccessResponse(data=page)
@action(methods=['DELETE'], detail=True, permission_classes=[IsAuthenticated, CustomPermission])
def remove_role_user(self, request, pk):
"""
角色-删除用户
"""
user_id = request.data.get('user_id', None)
if not user_id:
return ErrorResponse(msg="请选择用户")
role = self.get_object()
role.users_set.remove(*user_id)
return SuccessResponse(msg="删除成功")
@action(methods=['POST'], detail=True, permission_classes=[IsAuthenticated, CustomPermission])
def add_role_users(self, request, pk):
"""
角色-添加用户
"""
users_id = request.data.get('users_id', None)
if not users_id:
return ErrorResponse(msg="请选择用户")
role = self.get_object()
role.users_set.add(*users_id)
return DetailResponse(msg="添加成功")

View File

@@ -231,9 +231,17 @@ class RoleMenuButtonPermissionViewSet(CustomModelViewSet):
isCheck = data.get('isCheck', None) isCheck = data.get('isCheck', None)
roleId = data.get('roleId', None) roleId = data.get('roleId', None)
btnId = data.get('btnId', None) btnId = data.get('btnId', None)
data_range = data.get('data_range', None) or 0 # 默认仅本人权限
dept = data.get('dept', None) or [] # 默认空部门
if isCheck: if isCheck:
# 添加权限:创建关联记录 # 添加权限:创建关联记录
RoleMenuButtonPermission.objects.create(role_id=roleId, menu_button_id=btnId) instance = RoleMenuButtonPermission.objects.create(role_id=roleId,
menu_button_id=btnId,
data_range=data_range)
# 自定义部门权限
if data_range == 4 and dept:
instance.dept.set(dept)
else: else:
# 删除权限:移除关联记录 # 删除权限:移除关联记录
RoleMenuButtonPermission.objects.filter(role_id=roleId, menu_button_id=btnId).delete() RoleMenuButtonPermission.objects.filter(role_id=roleId, menu_button_id=btnId).delete()

View File

@@ -336,7 +336,7 @@ class UserViewSet(CustomModelViewSet):
verify_password = check_password(str(old_pwd_md5), request.user.password) verify_password = check_password(str(old_pwd_md5), request.user.password)
if verify_password: if verify_password:
# request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest()) # request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
request.user.password = make_password(new_pwd) request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest())
request.user.pwd_change_count += 1 request.user.pwd_change_count += 1
request.user.save() request.user.save()
return DetailResponse(data=None, msg="修改成功") return DetailResponse(data=None, msg="修改成功")
@@ -352,7 +352,7 @@ class UserViewSet(CustomModelViewSet):
if new_pwd != new_pwd2: if new_pwd != new_pwd2:
return ErrorResponse(msg="两次密码不匹配") return ErrorResponse(msg="两次密码不匹配")
else: else:
request.user.password = make_password(hashlib.md5(new_pwd.encode(encoding='UTF-8')).hexdigest()) request.user.password = make_password(new_pwd)
request.user.pwd_change_count += 1 request.user.pwd_change_count += 1
request.user.save() request.user.save()
return DetailResponse(data=None, msg="修改成功") return DetailResponse(data=None, msg="修改成功")

View File

@@ -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

View File

@@ -22,7 +22,7 @@ from django_filters.rest_framework import DjangoFilterBackend
from django_filters.utils import get_model_field from django_filters.utils import get_model_field
from rest_framework.filters import BaseFilterBackend from rest_framework.filters import BaseFilterBackend
from django_filters.conf import settings from django_filters.conf import settings
from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission from dvadmin.system.models import Dept, ApiWhiteList, RoleMenuButtonPermission, MenuButton
from dvadmin.utils.models import CoreModel from dvadmin.utils.models import CoreModel
class CoreModelFilterBankend(BaseFilterBackend): class CoreModelFilterBankend(BaseFilterBackend):
@@ -33,7 +33,7 @@ class CoreModelFilterBankend(BaseFilterBackend):
create_datetime_after = request.query_params.get('create_datetime_after', None) create_datetime_after = request.query_params.get('create_datetime_after', None)
create_datetime_before = request.query_params.get('create_datetime_before', None) create_datetime_before = request.query_params.get('create_datetime_before', None)
update_datetime_after = request.query_params.get('update_datetime_after', None) update_datetime_after = request.query_params.get('update_datetime_after', None)
update_datetime_before = request.query_params.get('update_datetime_after', None) update_datetime_before = request.query_params.get('update_datetime_before', None)
if any([create_datetime_after, create_datetime_before, update_datetime_after, update_datetime_before]): if any([create_datetime_after, create_datetime_before, update_datetime_after, update_datetime_before]):
create_filter = Q() create_filter = Q()
if create_datetime_after and create_datetime_before: if create_datetime_after and create_datetime_before:
@@ -149,11 +149,14 @@ class DataLevelPermissionsFilter(BaseFilterBackend):
if _pk: # 判断是否是单例查询 if _pk: # 判断是否是单例查询
re_api = re.sub(_pk,'{id}', api) re_api = re.sub(_pk,'{id}', api)
role_id_list = request.user.role.values_list('id', flat=True) role_id_list = request.user.role.values_list('id', flat=True)
# 修复权限获取bug
menu_button_ids = MenuButton.objects.filter(api=re_api,method=method).values_list('id', flat=True)
role_permission_list = []
if menu_button_ids:
role_permission_list=RoleMenuButtonPermission.objects.filter( role_permission_list=RoleMenuButtonPermission.objects.filter(
role__in=role_id_list, role__in=role_id_list,
role__status=1, role__status=1,
menu_button__api=re_api, menu_button_id__in=menu_button_ids).values(
menu_button__method=method).values(
'data_range' 'data_range'
) )
dataScope_list = [] # 权限范围列表 dataScope_list = [] # 权限范围列表

View File

@@ -81,6 +81,26 @@ class SoftDeleteModel(models.Model):
super().delete(using=using, *args, **kwargs) 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): class CoreModel(models.Model):
""" """
核心标准抽象模型模型,可直接继承使用 核心标准抽象模型模型,可直接继承使用
@@ -98,7 +118,8 @@ class CoreModel(models.Model):
verbose_name="修改时间") verbose_name="修改时间")
create_datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间", create_datetime = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间",
verbose_name="创建时间") verbose_name="创建时间")
objects = CoreModelManager()
all_objects = models.Manager()
class Meta: class Meta:
abstract = True abstract = True
verbose_name = '核心模型' verbose_name = '核心模型'

View File

@@ -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

View File

@@ -6,6 +6,8 @@
@Created on: 2021/6/1 001 22:57 @Created on: 2021/6/1 001 22:57
@Remark: 自定义视图集 @Remark: 自定义视图集
""" """
import copy
from django.db import transaction from django.db import transaction
from django_filters import DateTimeFromToRangeFilter from django_filters import DateTimeFromToRangeFilter
from django_filters.rest_framework import FilterSet from django_filters.rest_framework import FilterSet
@@ -67,12 +69,14 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
kwargs.setdefault('context', self.get_serializer_context()) kwargs.setdefault('context', self.get_serializer_context())
# 全部以可见字段为准 # 全部以可见字段为准
can_see = self.get_menu_field(serializer_class) can_see = self.get_menu_field(serializer_class)
# 排除掉序列化器级的字段 # 排除掉序列化器级的字段(排除字段权限中未授权的字段)
# sub_set = set(serializer_class._declared_fields.keys()) - set(can_see)
# for field in sub_set:
# serializer_class._declared_fields.pop(field)
# if not self.request.user.is_superuser: # if not self.request.user.is_superuser:
# serializer_class.Meta.fields = can_see # exclude_set = set(serializer_class._declared_fields.keys()) - set(can_see)
# for field in exclude_set:
# serializer_class._declared_fields.pop(field)
# meta = copy.deepcopy(serializer_class.Meta)
# meta.fields = list(can_see)
# serializer_class.Meta = meta
# 在分页器中使用 # 在分页器中使用
self.request.permission_fields = can_see self.request.permission_fields = can_see
if isinstance(self.request.data, list): if isinstance(self.request.data, list):
@@ -83,15 +87,17 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
def get_menu_field(self, serializer_class): def get_menu_field(self, serializer_class):
"""获取字段权限""" """获取字段权限"""
finded = False
for model in get_custom_app_models(): if not any(model['object'] is serializer_class.Meta.model for model in get_custom_app_models()):
if model['object'] is serializer_class.Meta.model:
finded = True
break
if finded is False:
return [] return []
return MenuField.objects.filter(model=model['model']
).values('field_name', 'title') # 匿名用户没有角色
ret = FieldPermission.objects.filter(field__model=serializer_class.Meta.model.__name__)
if hasattr(self.request.user, 'role'):
roles = self.request.user.role.values_list('id', flat=True)
ret = ret.filter(is_query=True, role__in=roles)
return ret.values_list('field__field_name', flat=True)
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data, request=request) serializer = self.get_serializer(data=request.data, request=request)
@@ -131,8 +137,7 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
instance.delete() instance.delete()
return DetailResponse(data=[], msg="删除成功") return DetailResponse(data=[], msg="删除成功")
keys = openapi.Schema(description='主键列表', type=openapi.TYPE_ARRAY, items=openapi.TYPE_STRING) keys = openapi.Schema(description='主键列表', type=openapi.TYPE_ARRAY, items=openapi.Schema(type=openapi.TYPE_STRING))
@swagger_auto_schema(request_body=openapi.Schema( @swagger_auto_schema(request_body=openapi.Schema(
type=openapi.TYPE_OBJECT, type=openapi.TYPE_OBJECT,
required=['keys'], required=['keys'],
@@ -147,3 +152,13 @@ class CustomModelViewSet(ModelViewSet, ImportSerializerMixin, ExportSerializerMi
return SuccessResponse(data=[], msg="删除成功") return SuccessResponse(data=[], msg="删除成功")
else: else:
return ErrorResponse(msg="未获取到keys字段") return ErrorResponse(msg="未获取到keys字段")
@action(methods=['post'], detail=False)
def get_by_ids(self, request):
"""通过IDS列表获取数据"""
ids = request.data.get('ids', [])
if ids and ids != ['']:
queryset = self.get_queryset().filter(id__in=ids)
serializer = self.get_serializer(queryset, many=True)
return DetailResponse(data=serializer.data)
return DetailResponse(data=None)

View File

@@ -0,0 +1,72 @@
# dvadmin3-build
## 介绍
一款适用于**django-vue3-admin** 编译打包exe、macOS的dmg文件等打包工具。支持加密代码、一键启动项目无需考虑环境。
**dvadmin3-build** 是一个方便的工具,用于将**django-vue3-admin**项目编译打包为可执行文件如exe或macOS的dmg文件等。它提供了以下好处
- 方便的部署:使用**dvadmin3-build**,您无需担心环境依赖和配置问题。您可以将整个项目打包为一个可执行文件或者安装包,方便快速部署在任何支持的操作系统上。
- 代码加密:**dvadmin3-build** 支持代码加密,可以将您的项目源代码加密为二进制格式,增加代码的安全性和保护知识产权。
- 一键启动项目:打包后的可执行文件或安装包可以通过简单的双击来启动项目,无需手动设置和配置环境,减少了部署和启动的复杂性。
- 跨平台支持:**dvadmin3-build** 可以将**django-vue3-admin**项目打包为适用于多个操作系统的可执行文件或安装包包括Windows、macOS、Ubuntu、中标麒麟等操作系统中。
- 易于使用:**dvadmin3-build** 提供了简单易懂的命令行界面,使得打包过程更加简便和高效。只需一个简单的命令,即可完成项目的打包。
## 功能支持项
- [ ] 支持平台
- [x] Windows
- [x] MacOS
- [ ] Ubuntu
- [ ] 中标麒麟
- [ ] 支持功能
- [x] 一键启动 dvadmin3
- [x] 托盘最小化
- [ ] dvadmin 初始化
- [ ] 数据库配置
- [ ] 端口配置
- [ ] 支持celery异步模块
- [ ] 进程守护
## 功能及使用方法
### 安装依赖
pip install dvadmin3_build-1.0.0-py3-none-any.whl
### 前端编译
yarn run build:local
### 后端
#### settings.py 中添加模块
~~~
INSTALLED_APPS = [
...
"dvadmin3_build"
]
HIDDEN_IMPORTS = [
'xxxx' # 添加 app 中自己的模块,用于编译
]
~~~
#### 迁移与初始化(项目已迁移过的不再需要)
~~~
python manage.py makemigrations
python manage.py migrate
python manage.py init
python manage.py init_data
~~~
#### 编译
~~~
# 编译后位于 dist 目录
python manage.py build
# windows 打包需要安装 InstallForgeSetup.exe使用 dvadmin3_InstallForge.ifp 模板dvadmin3_build/windows_build_tools 目录下)
# InstallForge 打包教程https://www.pythonguis.com/tutorials/packaging-pyqt6-applications-windows-pyinstaller/#setup
~~~

View File

View File

@@ -0,0 +1,21 @@
#!/bin/sh
# 获取传入的全局参数
dist_folder=$1
icon_path=$2
# 清空dmg文件夹。
rm -rf "$dist_folder/DVAServer"
rm -rf "$dist_folder/main"
# 如果DMG已经存在则删除它。
test -f "$dist_folder/DVAServer.dmg" && rm "$dist_folder/DVAServer.dmg"
create-dmg \
--volname "DVAServer" \
--volicon $icon_path \
--window-pos 200 120 \
--window-size 600 300 \
--icon-size 100 \
--icon "DVAServer.app" 175 120 \
--hide-extension "DVAServer.app" \
--app-drop-link 425 120 \
"$dist_folder/DVAServer.dmg" \
"$dist_folder/"

View File

@@ -0,0 +1,49 @@
# Form implementation generated from reading ui file 'dvadmin_main.ui'
#
# Created by: PyQt6 UI code generator 6.4.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_DvadminManager(object):
def setupUi(self, DvadminManager):
DvadminManager.setObjectName("DvadminManager")
DvadminManager.resize(400, 300)
DvadminManager.setMinimumSize(QtCore.QSize(400, 300))
DvadminManager.setMaximumSize(QtCore.QSize(400, 300))
self.label = QtWidgets.QLabel(parent=DvadminManager)
self.label.setGeometry(QtCore.QRect(10, 13, 60, 21))
self.label.setObjectName("label")
self.status_label = QtWidgets.QLabel(parent=DvadminManager)
self.status_label.setGeometry(QtCore.QRect(80, 12, 60, 21))
self.status_label.setObjectName("status_label")
self.start_button = QtWidgets.QPushButton(parent=DvadminManager)
self.start_button.setGeometry(QtCore.QRect(210, 10, 81, 26))
self.start_button.setObjectName("start_button")
self.stop_button = QtWidgets.QPushButton(parent=DvadminManager)
self.stop_button.setGeometry(QtCore.QRect(310, 9, 81, 26))
self.stop_button.setObjectName("stop_button")
self.line = QtWidgets.QFrame(parent=DvadminManager)
self.line.setGeometry(QtCore.QRect(0, 40, 401, 16))
self.line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
self.line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
self.line.setObjectName("line")
self.log_label = QtWidgets.QTextEdit(parent=DvadminManager)
self.log_label.setGeometry(QtCore.QRect(-1, 47, 411, 261))
self.log_label.setObjectName("log_label")
self.retranslateUi(DvadminManager)
QtCore.QMetaObject.connectSlotsByName(DvadminManager)
def retranslateUi(self, DvadminManager):
_translate = QtCore.QCoreApplication.translate
DvadminManager.setWindowTitle(_translate("DvadminManager", "服务管理器"))
self.label.setText(_translate("DvadminManager", "运行状态:"))
self.status_label.setText(_translate("DvadminManager", "未启动"))
self.start_button.setText(_translate("DvadminManager", "启动服务"))
self.stop_button.setText(_translate("DvadminManager", "结束服务"))
self.log_label.setHtml(_translate("DvadminManager", ""))

View File

@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DvadminManager</class>
<widget class="QDialog" name="DvadminManager">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>300</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>400</width>
<height>300</height>
</size>
</property>
<property name="windowTitle">
<string>服务管理器</string>
</property>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>10</x>
<y>13</y>
<width>60</width>
<height>21</height>
</rect>
</property>
<property name="text">
<string>运行状态:</string>
</property>
</widget>
<widget class="QLabel" name="status_label">
<property name="geometry">
<rect>
<x>80</x>
<y>12</y>
<width>60</width>
<height>21</height>
</rect>
</property>
<property name="text">
<string>未启动</string>
</property>
</widget>
<widget class="QPushButton" name="start_button">
<property name="geometry">
<rect>
<x>210</x>
<y>10</y>
<width>81</width>
<height>26</height>
</rect>
</property>
<property name="text">
<string>启动服务</string>
</property>
</widget>
<widget class="QPushButton" name="stop_button">
<property name="geometry">
<rect>
<x>310</x>
<y>9</y>
<width>81</width>
<height>26</height>
</rect>
</property>
<property name="text">
<string>结束服务</string>
</property>
</widget>
<widget class="Line" name="line">
<property name="geometry">
<rect>
<x>0</x>
<y>40</y>
<width>401</width>
<height>16</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
<widget class="QTextEdit" name="log_label">
<property name="geometry">
<rect>
<x>-1</x>
<y>47</y>
<width>411</width>
<height>261</height>
</rect>
</property>
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'.AppleSystemUIFont'; font-size:13pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,8 @@
# extra-hooks/hooks-uvicorn.py
from PyInstaller.utils.hooks import get_package_paths, collect_submodules
datas = [
(get_package_paths('uvicorn')[1], 'uvicorn'),
]
hiddenimports = collect_submodules('whitenoise')

View File

@@ -0,0 +1,41 @@
# UTF-8
#
# For more details about fixed file info 'ffi' see:
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
VSVersionInfo(
ffi=FixedFileInfo(
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
# Set not needed items to zero 0.
filevers=(1, 0, 0, 0),
prodvers=(1, 0, 0, 0),
# Contains a bitmask that specifies the valid bits 'flags'r
mask=0x3f,
# Contains a bitmask that specifies the Boolean attributes of the file.
flags=0x0,
# The operating system for which this file was designed.
# 0x4 - NT and there is no need to change it.
OS=0x40004,
# The general type of file.
# 0x1 - the file is an application.
fileType=0x1,
# The function of the file.
# 0x0 - the function is not defined for this fileType
subtype=0x0,
# Creation date and time stamp.
date=(0, 0)
),
kids=[
StringFileInfo(
[
StringTable(
'080404b0',
[StringStruct('CompanyName', '北京信码新创科技有限公司'),
StringStruct('FileDescription', '边缘Agent'),
StringStruct('FileVersion', '1.0.0.0'),
StringStruct('LegalCopyright', 'Copyright (C) 2021-2025 北京信码新创科技有限公司 All Rights Reserved'),
StringStruct('ProductName', '边缘Agent'),
StringStruct('ProductVersion', '1.0.0.0')])
]),
VarFileInfo([VarStruct('Translation', [2052, 1200])])
]
)

View File

@@ -0,0 +1,57 @@
import json
import os
from pathlib import Path
from django.core.management.base import BaseCommand
from application.settings import BASE_DIR
from application import settings
class Command(BaseCommand):
"""
生产初始化菜单: python manage.py build
"""
def add_arguments(self, parser):
pass
def handle(self, *args, **options):
print(args, options)
base_path = Path(__file__).resolve().parent.parent
# main.spec 路径
main_spec_path = os.path.join(base_path.parent, 'main.spec')
# 执行编译
import subprocess
# 执行命令
HIDDEN_IMPORTS = ','.join(getattr(settings, 'HIDDEN_IMPORTS', []))
command = f'export BASE_DIR="{BASE_DIR}" && export HIDDEN_IMPORTS="{HIDDEN_IMPORTS}" && rm -rf {os.path.join(BASE_DIR, "dist")} && pyinstaller -y --clean {main_spec_path}'
if os.sys.platform.startswith('win'):
# Windows操作系统
# command = f'setx BASE_DIR "{BASE_DIR}" && set HIDDEN_IMPORTS "{HIDDEN_IMPORTS}" && del {os.path.join(BASE_DIR, "dist")} && pyinstaller -y --clean {main_spec_path}'
command = f'setx BASE_DIR "{BASE_DIR}" && setx HIDDEN_IMPORTS "{HIDDEN_IMPORTS}" && pyinstaller -y --clean {main_spec_path}'
print(command)
print("当前环境是 Windows")
elif os.sys.platform.startswith('linux'):
# Linux操作系统
print("当前环境是 Linux")
command += f' && rm -rf {os.path.join(BASE_DIR, "build")}'
elif os.sys.platform.startswith('darwin'):
# macOS操作系统
print("当前环境是 macOS")
build_dmg_path = os.path.join(base_path.parent, 'builddmg.sh')
# 判断logo 是否存在
logo_path = os.path.join(BASE_DIR, 'static', 'logo.icns')
if not os.path.exists(logo_path):
# 文件不存在的处理逻辑
logo_path = os.path.join(base_path.parent, 'static', 'logo.icns')
command += f' && chmod +x {build_dmg_path} && {build_dmg_path} {os.path.join(BASE_DIR, "dist")} {logo_path}'
command += f' && rm -rf {os.path.join(BASE_DIR, "build")}'
print(command)
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
for line in process.stdout:
print(line.replace('\n', ''))
# # 等待进程结束
process.wait()

View File

@@ -0,0 +1,304 @@
import json
import os
import re
import signal
import subprocess
import webbrowser
import time
import threading
from pathlib import Path
from PyQt6.QtCore import QObject, QRunnable, QThreadPool, QTimer, pyqtSignal, Qt
from PyQt6.QtNetwork import QLocalServer
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel, QSystemTrayIcon, QMenu, QMessageBox
from PyQt6.QtGui import QIcon, QTextCharFormat, QColor, QTextCursor
from PyQt6 import QtCore, QtGui, QtWidgets
import sys
# # 编译ui
# pyuic6 dvadmin_main.ui -o dvadmin_main.py
# 由于编译问题把dvadmin_main.py代码复制到本脚本中
class Ui_DvadminManager(object):
def setupUi(self, DvadminManager):
DvadminManager.setObjectName("DvadminManager")
DvadminManager.resize(400, 300)
DvadminManager.setMinimumSize(QtCore.QSize(400, 300))
DvadminManager.setMaximumSize(QtCore.QSize(400, 300))
self.label = QtWidgets.QLabel(parent=DvadminManager)
self.label.setGeometry(QtCore.QRect(10, 13, 60, 21))
self.label.setObjectName("label")
self.status_label = QtWidgets.QLabel(parent=DvadminManager)
self.status_label.setGeometry(QtCore.QRect(80, 12, 60, 21))
self.status_label.setObjectName("status_label")
self.start_button = QtWidgets.QPushButton(parent=DvadminManager)
self.start_button.setGeometry(QtCore.QRect(210, 10, 81, 26))
self.start_button.setObjectName("start_button")
self.stop_button = QtWidgets.QPushButton(parent=DvadminManager)
self.stop_button.setGeometry(QtCore.QRect(310, 9, 81, 26))
self.stop_button.setObjectName("stop_button")
self.line = QtWidgets.QFrame(parent=DvadminManager)
self.line.setGeometry(QtCore.QRect(0, 40, 401, 16))
self.line.setFrameShape(QtWidgets.QFrame.Shape.HLine)
self.line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken)
self.line.setObjectName("line")
self.log_label = QtWidgets.QTextEdit(parent=DvadminManager)
self.log_label.setGeometry(QtCore.QRect(-1, 47, 411, 261))
self.log_label.setObjectName("log_label")
self.retranslateUi(DvadminManager)
QtCore.QMetaObject.connectSlotsByName(DvadminManager)
def retranslateUi(self, DvadminManager):
_translate = QtCore.QCoreApplication.translate
DvadminManager.setWindowTitle(_translate("DvadminManager", "服务管理器"))
self.label.setText(_translate("DvadminManager", "运行状态:"))
self.status_label.setText(_translate("DvadminManager", "未启动"))
self.start_button.setText(_translate("DvadminManager", "启动服务"))
self.stop_button.setText(_translate("DvadminManager", "结束服务"))
self.log_label.setHtml(_translate("DvadminManager", ""))
class SelectWorkerSignals(QObject):
result = pyqtSignal(str)
stop = pyqtSignal(bool)
list_process = []
class ServerWorkerSignals(QObject):
result = pyqtSignal(str)
class SelectWorker(QRunnable):
def __init__(self):
super().__init__()
self.signals = SelectWorkerSignals()
self.is_run = True
def run(self):
# 模拟异步操作
import time
import psutil
while self.is_run:
# 遍历进程列表,结束即关闭服务
# 获取所有正在运行的进程列表
processes = psutil.process_iter()
new_list_process = []
for process in processes:
# 如果进程名称包含"uvicorn",则进行监控
if process.pid in list_process:
new_list_process.append(process.pid)
if list_process and not new_list_process:
self.signals.result.emit("异步,进程不存在!")
# 等待1秒
time.sleep(1)
def handle_stop_result(self, result):
"""
异步获取进程结果后执行
"""
self.is_run = result
class ServerWorker(QRunnable):
def __init__(self):
super().__init__()
self.signals = ServerWorkerSignals()
def run(self):
# 启动dvadmin服务
import os
print("启动dvadmin服务")
# import main
# main.run()
# 定义要执行的命令
command = f"./main"
if os.sys.platform.startswith('win'):
# Windows操作系统
print("当前环境是Windows", Path(__file__).resolve().parent)
print("当前环境是Windows", Path(__file__).resolve())
command = f"{Path(__file__).resolve().parent.parent}/main.exe"
elif os.sys.platform.startswith('linux'):
# Linux操作系统
print("当前环境是Linux")
elif os.sys.platform.startswith('darwin'):
# macOS操作系统
print("当前环境是macOS")
command = f"{Path(__file__).resolve().parent.parent}/MacOS/main"
else:
# 其他操作系统
print("当前环境是其他操作系统")
self.signals.result.emit(json.dumps({"code": 2001, "msg": command}))
global list_process
# 使用subprocess.Popen执行命令
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
self.signals.result.emit(json.dumps({"code": 2000, "msg": "服务启动成功..."}))
# 持续获取输出结果
pid = process.pid
list_process.append(pid)
for line in process.stdout:
match = re.search(r'Started server process \[(\d+)\]', line)
# 判断是否匹配成功
if match:
# 获取匹配到的数字
number = match.group(1)
list_process.append(number)
list_process = list(set(list_process))
self.signals.result.emit(json.dumps({"code": 2001, "msg": line.replace('\n', '')}))
# 等待进程结束
process.wait()
class MainWindow(QMainWindow, Ui_DvadminManager):
def __init__(self):
super().__init__()
self.setupUi(self)
# 开始、结束按钮
self.start_button.clicked.connect(self.start_service)
self.stop_button.clicked.connect(self.stop_service)
# 托盘按钮及事件
self.tray_icon = QSystemTrayIcon(QIcon(os.path.join(Path(__file__).resolve().parent, 'static','logo.icns')), self)
self.tray_icon.activated.connect(self.tray_icon_activated)
self.tray_menu = QMenu(self)
self.tray_menu.addAction(self.start_button.text(), self.start_service)
self.tray_menu.addAction(self.stop_button.text(), self.stop_service)
self.tray_menu.addSeparator()
self.tray_menu.addAction("退出", QApplication.quit)
self.tray_icon.setContextMenu(self.tray_menu)
self.tray_icon.show()
self.log_label.setReadOnly(True)
# 信号
self.select_worker = SelectWorker()
self.select_worker.signals.result.connect(self.handle_select_result)
self.select_worker.signals.stop.connect(self.select_worker.handle_stop_result)
self.server_worker = ServerWorker()
self.server_worker.signals.result.connect(self.handle_server_servers)
# 异步
self.select_threadpool = QThreadPool()
self.server_threadpool = QThreadPool()
def handle_select_result(self, result):
"""
异步获取进程结果后执行
"""
self.append_to_log('服务进程异常,服务已停止...', color='red')
global list_process
list_process = []
self.status_label.setText("已停止")
self.status_label.setStyleSheet("color: red;")
def handle_server_servers(self, result):
"""
启动服务
"""
json_result = json.loads(result)
if json_result.get('code') == 2000:
self.append_to_log(json_result.get('msg'), color='green')
# 启动成功打开浏览器
url = "http://127.0.0.1:8000/web/"
def open_browser_after_delay(url, delay):
time.sleep(delay)
webbrowser.open(url)
threading.Thread(target=open_browser_after_delay, args=(url, 3)).start()
self.status_label.setText("运行中")
self.status_label.setStyleSheet("color: green;")
elif json_result.get('code') == 2001:
self.append_to_log(json_result.get('msg'))
else:
self.append_to_log(json_result.get('msg'), color='red')
self.status_label.setText("已停止")
self.status_label.setStyleSheet("color: red;")
def append_to_log(self, message, color=None):
"""
添加日志颜色
"""
cursor = self.log_label.textCursor()
format = QTextCharFormat()
if color:
if color == "green":
format.setForeground(QColor("green"))
elif color == "red":
format.setForeground(QColor("red"))
cursor.movePosition(QTextCursor.MoveOperation.End)
cursor.insertText(message, format)
cursor.insertBlock()
self.log_label.setTextCursor(cursor)
self.log_label.ensureCursorVisible()
self.log_label.verticalScrollBar().setValue(self.log_label.verticalScrollBar().maximum())
def start_service(self):
global list_process
if not list_process:
# 启动服务,执行启动脚本
self.server_threadpool.clear()
self.select_threadpool.clear()
self.select_worker.signals.stop.emit(True)
self.select_threadpool.startOnReservedThread(self.select_worker.run)
self.server_threadpool.startOnReservedThread(self.server_worker.run)
self.status_label.setText("正在启动中")
self.status_label.setStyleSheet("color: green;")
else:
self.append_to_log("服务已启动...")
def stop_service(self):
"""
停止服务
"""
global list_process
if list_process:
for pid in list_process:
try:
os.kill(int(pid), signal.SIGTERM)
except Exception as e:
print('Exception', e)
pass
list_process = []
self.select_worker.signals.stop.emit(False)
self.select_threadpool.clear()
self.server_threadpool.clear()
self.status_label.setText("已停止")
self.status_label.setStyleSheet("color: red;")
self.service_running = False
self.append_to_log("服务已停止...", color='red')
def tray_icon_activated(self, reason):
"""
托盘激活
"""
if reason == QSystemTrayIcon.ActivationReason.Trigger:
self.showNormal()
self.activateWindow()
def showEvent(self, event):
# 自定义的显示事件处理
if self.status_label.text() != '已停止':
self.select_worker.signals.stop.emit(True)
self.select_threadpool.startOnReservedThread(self.select_worker.run)
return super().showEvent(event)
def closeEvent(self, event):
event.ignore()
self.hide()
self.select_worker.signals.stop.emit(False)
if __name__ == '__main__':
app = QApplication(sys.argv)
server = QLocalServer()
if server.listen('DVAServer'):
app.setQuitOnLastWindowClosed(False)
app.setWindowIcon(QIcon(os.path.join(Path(__file__).resolve().parent, 'static','logo.icns')))
window = MainWindow()
window.show()
sys.exit(app.exec())
else:
message_box = QMessageBox(QMessageBox.Icon.Information, 'Information', '应用程序已在运行')
message_box.exec()
sys.exit(0) # 如果已有实例在运行,则退出应用程序

View File

@@ -0,0 +1,28 @@
import setuptools
with open("README.md", "r") as fh:
long_description = fh.read()
setuptools.setup(
name="dvadmin3-build",
version="1.0.0",
author="DVAdmin",
author_email="liqiang@django-vue-admin.com",
description="一款适用于django-vue3-admin 编译打包exe、macOS的dmg文件等打包工具。支持加密代码、一键启动项目无需考虑环境。",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://gitee.com/huge-dream/dvadmin-build",
packages=setuptools.find_packages(),
python_requires='>=3.7, <4',
install_requires=[
"pyinstaller>=6.8.0",
"PyQt6>=6.4.2",
"psutil==6.0.0",
],
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
include_package_data=True
)

Binary file not shown.

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class CodeInfoConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'plugins.code_info'

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,49 @@
[ {
"name": "扫码信息",
"icon": "ele-Bell",
"sort": 8,
"is_link": false,
"is_catalog": true,
"web_path": "/releaseInfo",
"component": "",
"component_name": "",
"status": true,
"cache": true,
"visible": true,
"parent": null,
"children": [
{
"name": "扫码数据",
"icon": "ele-Bell",
"sort": 1,
"is_link": false,
"is_catalog": false,
"web_path": "/scanData",
"component": "plugins/scanInfo/src/scanData/index",
"component_name": "scanData",
"status": true,
"cache": false,
"visible": true,
"children": [],
"menu_button": [],
"menu_field": []
},{
"name": "异常扫码记录",
"icon": "ele-Bell",
"sort": 2,
"is_link": false,
"is_catalog": false,
"web_path": "/scanRecord",
"component": "plugins/scanInfo/src/scanRecord/index",
"component_name": "scanRecord",
"status": true,
"cache": false,
"visible": true,
"children": [],
"menu_button": [],
"menu_field": []
}
],
"menu_button": [],
"menu_field": []
}]

View File

@@ -0,0 +1,33 @@
# 初始化
import os
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "application.settings")
django.setup()
from dvadmin.system.fixtures.initSerializer import MenuInitSerializer, SystemConfigInitSerializer, \
DictionaryInitSerializer
from dvadmin.utils.core_initialize import CoreInitialize
class Initialize(CoreInitialize):
def init_menu(self):
"""
初始化菜单信息
"""
self.init_base(MenuInitSerializer, unique_fields=['name', 'web_path', 'component', 'component_name'])
def init_dictionary(self):
"""
初始化字典表
"""
self.init_base(DictionaryInitSerializer, unique_fields=['value', 'parent', ])
def run(self):
self.init_menu()
self.init_dictionary()
print(22)
if __name__ == '__main__':
Initialize(app='release_info').run()

View File

@@ -0,0 +1,29 @@
from django.db import models
from django.db import models
from dvadmin.utils.models import CoreModel
table_prefix = "release_info_"
class ScanData(CoreModel):
# 产品件号、供应商代码、生产批次、产品序列码、版本号、扫码时间、班次和人员信息
code = models.CharField(null=True, blank=True, max_length=255, verbose_name='扫码值', help_text='扫码值')
product_code = models.CharField(null=True, blank=True, max_length=255, verbose_name='产品件号', help_text='产品件号')
supplier_code = models.CharField(null=True, blank=True, max_length=255, verbose_name='供应商代码', help_text='供应商代码')
production_batch = models.CharField(null=True, blank=True, max_length=255, verbose_name='生产批次', help_text='生产批次')
product_serial_number = models.CharField(null=True, blank=True, max_length=255, verbose_name='产品序列码', help_text='产品序列码')
version_number = models.CharField(null=True, blank=True, max_length=255, verbose_name='版本号', help_text='版本号')
shift = models.CharField(null=True, blank=True, max_length=255, verbose_name='班次', help_text='班次')
STATUS_EMU = (
(0, "重复扫码"),
(1, "正常"),
(2, "未识别码"),
)
status = models.IntegerField(default=1, choices=STATUS_EMU, verbose_name='状态', help_text='状态')
class Meta:
db_table = table_prefix + "scan_data"
verbose_name = "扫码数据"
verbose_name_plural = verbose_name
ordering = ('-create_datetime',)

View File

@@ -0,0 +1,22 @@
from application import settings
# ================================================= #
# ***************** 插件配置区开始 *******************
# ================================================= #
# 路由配置
plugins_url_patterns = [
{"re_path": r'api/code_info/', "include": "code_info.urls"},
]
# app 配置
apps = ['code_info']
# 租户模式中public模式共享app配置
tenant_shared_apps = ['code_info']
# ================================================= #
# ******************* 插件配置区结束 *****************
# ================================================= #
# ********** 赋值到 settings 中 **********
settings.INSTALLED_APPS += [app for app in apps if app not in settings.INSTALLED_APPS]
settings.TENANT_SHARED_APPS += tenant_shared_apps
# ********** 注册路由 **********
settings.PLUGINS_URL_PATTERNS += plugins_url_patterns

View File

@@ -0,0 +1,15 @@
from django.urls import path
from rest_framework import routers
from plugins.code_info.views.scan_data import ScanDataViewSet
from plugins.code_info.views.scan_record import ScanRecordViewSet
route_url = routers.SimpleRouter()
route_url.register(r'scan_data', ScanDataViewSet,basename='scan_data')
route_url.register(r'scan_record', ScanRecordViewSet)
urlpatterns = [
]
urlpatterns += route_url.urls

View File

@@ -0,0 +1,67 @@
from dvadmin.utils.field_permission import FieldPermissionMixin
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.validator import CustomValidationError
from dvadmin.utils.viewset import CustomModelViewSet
from plugins.code_info.models import ScanData
class ScanDataSerializer(CustomModelSerializer):
"""
扫码数据-序列化器
"""
class Meta:
model = ScanData
fields = "__all__"
read_only_fields = ["id"]
class CreateScanDataSerializer(CustomModelSerializer):
"""
扫码数据-序列化器
"""
def create(self, validated_data):
code = validated_data.get("code")
print(code)
code_list = code.split("/")
if len(code_list) == 5:
validated_data["product_code"] = code_list[0]
validated_data["supplier_code"] = code_list[1]
validated_data["production_batch"] = code_list[2]
validated_data["product_serial_number"] = code_list[3]
validated_data["version_number"] = code_list[4]
validated_data["shift"] = self.request.user.description
instance = super().create(validated_data)
# 1.格式错误
if len(code_list) != 5:
instance.status = 2
instance.save()
raise CustomValidationError("数据格式错误")
# 2.查询数据是否已存在数据库
print("ScanData.objects.filter(code=code, status=1)",ScanData.objects.filter(code=code, status=1))
if ScanData.objects.filter(code=code, status=1).exclude(id=instance.id).exists():
instance.status = 0
instance.save()
raise CustomValidationError("重复扫码")
return instance
class Meta:
model = ScanData
fields = "__all__"
read_only_fields = ["id"]
class ScanDataViewSet(CustomModelViewSet, FieldPermissionMixin):
"""
扫码数据接口
list:查询
create:新增
update:修改
retrieve:单例
destroy:删除
"""
queryset = ScanData.objects.filter(status=1)
serializer_class = ScanDataSerializer
create_serializer_class = CreateScanDataSerializer
extra_filter_class = []

View File

@@ -0,0 +1,29 @@
from dvadmin.utils.field_permission import FieldPermissionMixin
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
from plugins.code_info.models import ScanData
class ScanRecordSerializer(CustomModelSerializer):
"""
扫码数据-序列化器
"""
class Meta:
model = ScanData
fields = "__all__"
read_only_fields = ["id"]
class ScanRecordViewSet(CustomModelViewSet, FieldPermissionMixin):
"""
异常扫码数据记录接口
list:查询
create:新增
update:修改
retrieve:单例
destroy:删除
"""
queryset = ScanData.objects.exclude(status=1)
serializer_class = ScanRecordSerializer
extra_filter_class = []

View File

@@ -7,25 +7,26 @@ djangorestframework==3.15.2
django-restql==0.15.4 django-restql==0.15.4
django-simple-captcha==0.6.0 django-simple-captcha==0.6.0
django-timezone-field==7.0 django-timezone-field==7.0
djangorestframework-simplejwt==5.3.1 djangorestframework_simplejwt==5.4.0
drf-yasg==1.21.7 drf-yasg==1.21.10
mysqlclient==2.2.0
pypinyin==0.51.0 pypinyin==0.51.0
ua-parser==0.18.0 ua-parser==0.18.0
pyparsing==3.1.2 pyparsing==3.1.2
openpyxl==3.1.5 openpyxl==3.1.5
requests==2.32.3 requests==2.32.4
typing-extensions==4.12.2 typing-extensions==4.12.2
tzlocal==5.2 tzlocal==5.2
channels==4.1.0 channels==4.1.0
channels-redis==4.2.0 channels-redis==4.2.0
websockets==11.0.3
user-agents==2.2.0 user-agents==2.2.0
six==1.16.0 six==1.16.0
whitenoise==6.7.0 whitenoise==6.7.0
psycopg2==2.9.9 psycopg2==2.9.9
uvicorn==0.30.3 uvicorn==0.30.3
gunicorn==22.0.0 gunicorn==23.0.0
gevent==24.2.1 gevent==24.2.1
Pillow==10.4.0 Pillow==10.4.0
pyinstaller==6.9.0 pyinstaller==6.9.0
dvadmin3-celery==3.1.6
oss2==2.19.1
cos-python-sdk-v5==1.9.37

View File

@@ -82,7 +82,7 @@
<br> <br>
<h2>巨梦科技企业客户服务说明</h2> <h2>巨梦科技企业客户服务说明</h2>
<h3> <h3>
1巨梦科技平台提供给多家企业客户使用企业客户通过巨梦科技平台进行发布用户活动等如果功夫企火星企业用户违反了隐私政策或发生其它侵犯用户权益的行为用户可要求巨梦科技提供必要的配合与巨梦科技企业用户进行沟通和维权维权过程中产生的费用由用户自行承担</h3> 1巨梦科技平台提供给多家企业客户使用企业客户通过巨梦科技平台进行发布用户活动等如果企业用户违反了隐私政策或发生其它侵犯用户权益的行为用户可要求巨梦科技提供必要的配合与巨梦科技企业用户进行沟通和维权维权过程中产生的费用由用户自行承担</h3>
<br> <br>
<h2>其他事宜</h2> <h2>其他事宜</h2>
<h3> <h3>

78
backend/util/currency.py Normal file
View File

@@ -0,0 +1,78 @@
from uuid import uuid4
from datetime import datetime
from django.db import connection
from django.db.models import Model
from django.core.cache import cache
def create_code(model,prefix):
current_date = datetime.now().strftime('%Y%m%d%H%M%S')
code = f"{prefix}{current_date}" + str(uuid4().int)[:6]
return code
def lock(key):
if callable(key): # @lock
def inner(*args, **kwargs):
with cache.lock(key='lock'):
return key(*args, **kwargs)
inner.__name__ = key.__name__
else: # @lock(key='aaa')
def inner(func):
def _inner(*args, **kwargs):
with cache.lock(key=key):
return func(*args, **kwargs)
_inner.__name__ = func.__name__
return _inner
return inner
def recursion_down_fast(instance:Model, parent='parent', key='id') -> list[int]:
"""向下递归instance的所有子级且返回一维列表使用sql优化速度非常快"""
if not instance:
return []
sql = f"""
WITH RECURSIVE children AS (
SELECT id, {key} AS param_{key} FROM {instance.__class__._meta.db_table} WHERE {parent}_id = %s UNION ALL
SELECT a.id, a.{key} AS param_{key} FROM {instance.__class__._meta.db_table} a
INNER JOIN children b ON a.{parent}_id = b.id
) SELECT param_{key} FROM children;
"""
with connection.cursor() as cursor:
cursor.execute(sql, [getattr(instance, key)])
data = cursor.fetchall()
return [getattr(instance, key), *[i[0] for i in data]]
def recursion_up_fast(instance: Model, parent='parent', key='id') -> list[int]:
"""向上递归instance的所有父级使用sql优化速度非常快"""
if not instance:
return []
sql = f"""
WITH RECURSIVE parents AS (
SELECT id, {key} as param_{key}, {parent}_id FROM {instance.__class__._meta.db_table} WHERE id = %s UNION ALL
SELECT a.id, a.{key} as param_{key}, a.{parent}_id FROM {instance.__class__._meta.db_table} a
INNER JOIN parents b ON a.id = b.{parent}_id
) SELECT param_{key} FROM parents;
"""
with connection.cursor() as cursor:
cursor.execute(sql, [getattr(instance, key)])
data = cursor.fetchall()
return [getattr(instance, key), *[i[0] for i in data]]
def recursion_up_joint(instance: Model, parent='parent', key='name', joint='/') -> str:
"""向上递归instance所有父级并拼接需要的值返回完整路径使用sql优化速度非常快"""
if instance is None:
return ''
sql = f"""
WITH RECURSIVE parents AS (
SELECT id, {parent}_id, {key}::TEXT AS path FROM {instance.__class__._meta.db_table} WHERE {key} = %s AND id = %s UNION ALL
SELECT a.id, a.{parent}_id, (a.{key} || '{joint}' || b.path)::TEXT FROM {instance.__class__._meta.db_table} a
INNER JOIN parents b ON a.id = b.{parent}_id
) SELECT path FROM parents where {parent}_id IS NULL;
"""
with connection.cursor() as cursor:
cursor.execute(sql, [getattr(instance, key), instance.pk])
data = cursor.fetchall()
try:
return data[0][0]
except IndexError:
raise Exception('找不到初始数据')

87
crud-gen.sh Normal file
View File

@@ -0,0 +1,87 @@
if ! [ -f ".env" ];then
echo ".env file not found"
exit 1
fi
if [ -z "$3" ]; then
echo "Use: $0 <app_name> <view_name> <table_name>"
exit 1
fi
DIR=./web/src/views/$1/$2
# 设置数据库连接信息
HOST="177.10.0.13"
USER="root"
PASSWORD=$(cat .env | grep MYSQL_PASSWORD | sed 's/^.*MYSQL_PASSWORD=//g')
DATABASE="django-vue3-admin"
TABLE=$3
TARGET_FILE="./web/src/views/$1/$2/crud.tsx"
# 表是否存在
TABLE_EXISTS=$(mysql -h $HOST -u $USER -p$PASSWORD -D $DATABASE -e "SHOW TABLES LIKE '$TABLE';" -N | grep "$TABLE" | wc -l)
if [ "$TABLE_EXISTS" -eq 0 ]; then
echo "Table $TABLE does not exist in database $DATABASE."
exit 1
fi
mkdir -p $DIR
cp -r ./web/src/views/template/* $DIR
sed -i "s/VIEWSETNAME/$2/g" $DIR/*
sed -n -e :a -e '1,5!{P;N;D;};N;ba' -i $TARGET_FILE
# 查询表结构
QUERY="SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT, IS_NULLABLE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '$DATABASE' AND TABLE_NAME = '$TABLE' ORDER BY ORDINAL_POSITION;"
# 使用 MySQL 查询获取字段信息,并生成 fast-crud 配置
mysql -h $HOST -u $USER -p$PASSWORD -D $DATABASE -e "$QUERY" -N | while read COLUMN_NAME DATA_TYPE COLUMN_COMMENT IS_NULLABLE; do
# 映射 MySQL 数据类型到 fast-crud 类型
case "$DATA_TYPE" in
"int"|"bigint"|"smallint"|"mediumint"|"tinyint"|"decimal"|"float"|"double")
TYPE="number"
;;
"date"|"datetime"|"timestamp")
TYPE="date"
;;
*)
TYPE="text"
;;
esac
echo " $COLUMN_NAME: {
title: '$COLUMN_NAME',
type: '$TYPE',
search: { show: true },
column: {
minWidth: 120,
sortable: 'custom',
},
form: {" >> $TARGET_FILE
if [ "$IS_NULLABLE" = "NO" ]; then
echo " helper: {
render() {
return <div style={"color:blue"}>$COLUMN_NAME 是必填的</div>;
}
},
rules: [{
required: true, message: '$COLUMN_NAME 是必填的'
}]," >> $TARGET_FILE
fi
echo " component: {
placeholder: '请输入 $COLUMN_NAME',
},
},
}," >> $TARGET_FILE
done
echo " },
},
};
}" >> $TARGET_FILE

View File

@@ -1,4 +1,4 @@
FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:16.19-alpine FROM registry.cn-zhangjiakou.aliyuncs.com/dvadmin-pro/dvadmin3-base-web:18.20-alpine
WORKDIR /web/ WORKDIR /web/
COPY web/. . COPY web/. .
RUN yarn install --registry=https://registry.npmmirror.com RUN yarn install --registry=https://registry.npmmirror.com

52
init.sh
View File

@@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
ENV_FILE=".env" ENV_FILE=".env"
HOST="177.10.0.13"
# 检查 .env 文件是否存在 # 检查 .env 文件是否存在
if [ -f "$ENV_FILE" ]; then if [ -f "$ENV_FILE" ]; then
echo "$ENV_FILE 文件已存在。" echo "$ENV_FILE 文件已存在。"
@@ -15,17 +16,60 @@ else
echo "REDIS随机密码已生成并写入 $ENV_FILE 文件。" echo "REDIS随机密码已生成并写入 $ENV_FILE 文件。"
awk 'BEGIN { cmd="cp -i ./backend/conf/env.example.py ./backend/conf/env.py "; print "n" |cmd; }' awk 'BEGIN { cmd="cp -i ./backend/conf/env.example.py ./backend/conf/env.py "; print "n" |cmd; }'
sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '177.10.0.13'|g" ./backend/conf/env.py sed -i "s|DATABASE_HOST = '127.0.0.1'|DATABASE_HOST = '$HOST'|g" ./backend/conf/env.py
sed -i "s|REDIS_HOST = '127.0.0.1'|REDIS_HOST = '177.10.0.15'|g" ./backend/conf/env.py sed -i "s|REDIS_HOST = '127.0.0.1'|REDIS_HOST = '177.10.0.15'|g" ./backend/conf/env.py
sed -i "s|DATABASE_PASSWORD = 'DVADMIN3'|DATABASE_PASSWORD = '$MYSQL_PASSWORD'|g" ./backend/conf/env.py sed -i "s|DATABASE_PASSWORD = 'DVADMIN3'|DATABASE_PASSWORD = '$MYSQL_PASSWORD'|g" ./backend/conf/env.py
sed -i "s|REDIS_PASSWORD = 'DVADMIN3'|REDIS_PASSWORD = '$REDIS_PASSWORD'|g" ./backend/conf/env.py sed -i "s|REDIS_PASSWORD = 'DVADMIN3'|REDIS_PASSWORD = '$REDIS_PASSWORD'|g" ./backend/conf/env.py
echo "初始化密码创建成功" echo "初始化密码创建成功"
fi fi
echo "正在启动容器..."
docker-compose up -d docker-compose up -d
docker exec dvadmin3-django python manage.py makemigrations
docker exec dvadmin3-django python manage.py migrate if [ $? -ne 0 ]; then
docker exec dvadmin3-django python manage.py init echo "docker-compose up -d 执行失败!"
exit 1
fi
MYSQL_PORT=3306
REDIS_PORT=6379
check_mysql() {
if nc -z "$HOST" "$MYSQL_PORT" >/dev/null 2>&1; then
echo "MySQL 服务正在运行在 $HOST:$MYSQL_PORT"
return 0
else
return 1
fi
}
check_redis() {
if nc -z "$HOST" "$REDIS_PORT" >/dev/null 2>&1; then
echo "Redis 服务正在运行在 $HOST:$REDIS_PORT"
return 0
else
return 1
fi
}
i=1
while [ $i -le 8 ]; do
if check_mysql || check_redis; then
echo "正在迁移数据..."
docker exec dvadmin3-django python3 manage.py makemigrations
docker exec dvadmin3-django python3 manage.py migrate
echo "正在初始化数据..."
docker exec dvadmin3-django python3 manage.py init
echo "欢迎使用dvadmin3项目" echo "欢迎使用dvadmin3项目"
echo "登录地址http://ip:8080" echo "登录地址http://ip:8080"
echo "如访问不到,请检查防火墙配置" echo "如访问不到,请检查防火墙配置"
exit 0
else
echo "$i 次尝试MySQL 或 REDIS服务未运行等待 2 秒后重试..."
sleep 2
fi
i=$((i+1))
done
echo "尝试 5 次后MySQL 或 REDIS服务仍未运行"
exit 1

52
redis-8.2.1/README.md Normal file
View File

@@ -0,0 +1,52 @@
### [English](https://github.com/redis-windows/redis-windows/blob/main/README.md) | [简体中文](https://github.com/redis-windows/redis-windows/blob/main/README.zh_CN.md)
# Redis Windows Version
### With the powerful automated building capability of GitHub Actions, we can compile the latest version of Redis for Windows system in real-time.
The entire compilation process is completely transparent and open, with the compilation script located in the [.github/workflows/](https://github.com/redis-windows/redis-windows/tree/main/.github/workflows) directory and the compilation logs available on the [Actions](https://github.com/redis-windows/redis-windows/actions) page. In addition, we have added a hash calculation step when the compilation is completed, and the result is printed in the log. This is unmodifiable and recorded in the release page. You can verify the hash value of the downloaded file against the log and release page.
Our project is absolutely pure and without any hidden features, and can withstand the scrutiny of all experts. If you have any good ideas, please feel free to communicate with us.
## We provide three operation modes:
1. Run the start.bat script in the project to start directly with one click.
2. Use the command line.
3. Support running as a system service.
### Command line startup:
cmd startup:
```shell
redis-server.exe redis.conf
```
powershell startup:
```shell
./redis-server.exe redis.conf
```
### Service installation:
Can achieve automatic startup on boot. Please run it as an administrator and change RedisService.exe to the actual directory where it is stored.
```shell
sc.exe create Redis binpath=C:\Software\Redis\RedisService.exe start= auto
```
Start service
```shell
net start Redis
```
Out of Service
```shell
net stop Redis
```
Uninstall service
```shell
sc.exe delete Redis
```
![image](https://user-images.githubusercontent.com/515784/215540157-65f55297-cde2-49b3-8ab3-14dca7e11ee0.png)
Project Home: https://github.com/redis-windows/redis-windows
## Acknowledgement:
[![NetEngine](https://avatars.githubusercontent.com/u/36178221?s=180&v=4)](https://www.zhihu.com/question/424272611/answer/2611312760)
[![JetBrains Logo (Main) logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg)](https://www.jetbrains.com/?from=redis-windows)
## Disclaimer
We suggest that you use it for local development and follow Redis official guidance to deploy it on Linux for production environment. This project doesn't bear any responsibility for any losses caused by using it and is only for learning and exchange purposes.

View File

@@ -0,0 +1,59 @@
### [English](https://github.com/redis-windows/redis-windows/blob/main/README.md) | [简体中文](https://github.com/redis-windows/redis-windows/blob/main/README.zh_CN.md)
# Redis Windows版本
### 借助GitHub Actions强大的自动化构建能力为我们实时编译适用于Windows系统的Redis最新版本
整个编译过程完全公开透明, 编译脚本在[.github/workflows/](https://github.com/redis-windows/redis-windows/tree/main/.github/workflows) 目录中,编译日志可在 [Actions](https://github.com/redis-windows/redis-windows/actions)页面查看。此外,我们在编译结束,新增了哈希值计算环节,计算结果打印在日志中,这是任何人不可修改的,并写入发布页面。您可以核对下载到本地的文件的哈希值,是否与日志和发布页面的一致。本项目绝对纯洁无私货,经得起各位大佬审查。如您有好的想法,也欢迎交流。
### 提供三种运行模式
直接运行项目中的 start.bat 脚本,一键启动
或者使用命令行
还支持以系统服务运行
## 命令行启动
cmd 启动
```shell
redis-server.exe redis.conf
```
powershell 启动
```shell
./redis-server.exe redis.conf
```
## 安装服务
可实现开机自启动
请以管理员身份运行并将RedisService.exe改为您实际存放的路径
```shell
sc.exe create Redis binpath=C:\Software\Redis\RedisService.exe start= auto
```
启动服务
```shell
net start Redis
```
停止服务
```shell
net stop Redis
```
卸载服务
```shell
sc.exe delete Redis
```
![image](https://user-images.githubusercontent.com/515784/215540157-65f55297-cde2-49b3-8ab3-14dca7e11ee0.png)
项目主页: https://github.com/redis-windows/redis-windows
### 鸣谢
[![NetEngine](https://avatars.githubusercontent.com/u/36178221?s=180&v=4)](https://www.zhihu.com/question/424272611/answer/2611312760)
[![JetBrains Logo (Main) logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg)](https://www.jetbrains.com/?from=redis-windows)
### 免责声明
建议您在本地开发环节使用生产环境请按照Redis官方指导在Linux中部署。本项目不承担由此给您带来的任何损失仅供学习交流。

Binary file not shown.

BIN
redis-8.2.1/cygcrypto-3.dll Normal file

Binary file not shown.

Binary file not shown.

BIN
redis-8.2.1/cygiconv-2.dll Normal file

Binary file not shown.

BIN
redis-8.2.1/cygintl-8.dll Normal file

Binary file not shown.

BIN
redis-8.2.1/cygssl-3.dll Normal file

Binary file not shown.

BIN
redis-8.2.1/cygstdc++-6.dll Normal file

Binary file not shown.

BIN
redis-8.2.1/cygwin1.dll Normal file

Binary file not shown.

BIN
redis-8.2.1/cygz.dll Normal file

Binary file not shown.

BIN
redis-8.2.1/dump.rdb Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
redis-8.2.1/redis-cli.exe Normal file

Binary file not shown.

376
redis-8.2.1/redis-full.conf Normal file
View File

@@ -0,0 +1,376 @@
include redis.conf
loadmodule ./modules/redisbloom/redisbloom.so
loadmodule ./modules/redisearch/redisearch.so
loadmodule ./modules/redisjson/rejson.so
loadmodule ./modules/redistimeseries/redistimeseries.so
############################## QUERY ENGINE CONFIG ############################
# Keep numeric ranges in numeric tree parent nodes of leafs for `x` generations.
# numeric, valid range: [0, 2], default: 0
#
# search-_numeric-ranges-parents 0
# The number of iterations to run while performing background indexing
# before we call usleep(1) (sleep for 1 micro-second) and make sure that we
# allow redis to process other commands.
# numeric, valid range: [1, UINT32_MAX], default: 100
#
# search-bg-index-sleep-gap 100
# The default dialect used in search queries.
# numeric, valid range: [1, 4], default: 1
#
# search-default-dialect 1
# the fork gc will only start to clean when the number of not cleaned document
# will exceed this threshold.
# numeric, valid range: [1, LLONG_MAX], default: 100
#
# search-fork-gc-clean-threshold 100
# interval (in seconds) in which to retry running the forkgc after failure.
# numeric, valid range: [1, LLONG_MAX], default: 5
#
# search-fork-gc-retry-interval 5
# interval (in seconds) in which to run the fork gc (relevant only when fork
# gc is used).
# numeric, valid range: [1, LLONG_MAX], default: 30
#
# search-fork-gc-run-interval 30
# the amount of seconds for the fork GC to sleep before exiting.
# numeric, valid range: [0, LLONG_MAX], default: 0
#
# search-fork-gc-sleep-before-exit 0
# Scan this many documents at a time during every GC iteration.
# numeric, valid range: [1, LLONG_MAX], default: 100
#
# search-gc-scan-size 100
# Max number of cursors for a given index that can be opened inside of a shard.
# numeric, valid range: [0, LLONG_MAX], default: 128
#
# search-index-cursor-limit 128
# Maximum number of results from ft.aggregate command.
# numeric, valid range: [0, (1ULL << 31)], default: 1ULL << 31
#
# search-max-aggregate-results 2147483648
# Maximum prefix expansions to be used in a query.
# numeric, valid range: [1, LLONG_MAX], default: 200
#
# search-max-prefix-expansions 200
# Maximum runtime document table size (for this process).
# numeric, valid range: [1, 100000000], default: 1000000
#
# search-max-doctablesize 1000000
# max idle time allowed to be set for cursor, setting it high might cause
# high memory consumption.
# numeric, valid range: [1, LLONG_MAX], default: 300000
#
# search-cursor-max-idle 300000
# Maximum number of results from ft.search command.
# numeric, valid range: [0, 1ULL << 31], default: 1000000
#
# search-max-search-results 1000000
# Number of worker threads to use for background tasks when the server is
# in an operation event.
# numeric, valid range: [1, 16], default: 4
#
# search-min-operation-workers 4
# Minimum length of term to be considered for phonetic matching.
# numeric, valid range: [1, LLONG_MAX], default: 3
#
# search-min-phonetic-term-len 3
# the minimum prefix for expansions (`*`).
# numeric, valid range: [1, LLONG_MAX], default: 2
#
# search-min-prefix 2
# the minimum word length to stem.
# numeric, valid range: [2, UINT32_MAX], default: 4
#
# search-min-stem-len 4
# Delta used to increase positional offsets between array
# slots for multi text values.
# Can control the level of separation between phrases in different
# array slots (related to the SLOP parameter of ft.search command)"
# numeric, valid range: [1, UINT32_MAX], default: 100
#
# search-multi-text-slop 100
# Used for setting the buffer limit threshold for vector similarity tiered
# HNSW index, so that if we are using WORKERS for indexing, and the
# number of vectors waiting in the buffer to be indexed exceeds this limit,
# we insert new vectors directly into HNSW.
# numeric, valid range: [0, LLONG_MAX], default: 1024
#
# search-tiered-hnsw-buffer-limit 1024
# Query timeout.
# numeric, valid range: [1, LLONG_MAX], default: 500
#
# search-timeout 500
# minimum number of iterators in a union from which the iterator will
# will switch to heap-based implementation.
# numeric, valid range: [1, LLONG_MAX], default: 20
# switch to heap based implementation.
#
# search-union-iterator-heap 20
# The maximum memory resize for vector similarity indexes (in bytes).
# numeric, valid range: [0, UINT32_MAX], default: 0
#
# search-vss-max-resize 0
# Number of worker threads to use for query processing and background tasks.
# numeric, valid range: [0, 16], default: 0
# This configuration also affects the number of connections per shard.
#
# search-workers 0
# The number of high priority tasks to be executed at any given time by the
# worker thread pool, before executing low priority tasks. After this number
# of high priority tasks are being executed, the worker thread pool will
# execute high and low priority tasks alternately.
# numeric, valid range: [0, LLONG_MAX], default: 1
#
# search-workers-priority-bias-threshold 1
# Load extension scoring/expansion module. Immutable.
# string, default: ""
#
# search-ext-load ""
# Path to Chinese dictionary configuration file (for Chinese tokenization). Immutable.
# string, default: ""
#
# search-friso-ini ""
# Action to perform when search timeout is exceeded (choose RETURN or FAIL).
# enum, valid values: ["return", "fail"], default: "fail"
#
# search-on-timeout fail
# Determine whether some index resources are free on a second thread.
# bool, default: yes
#
# search-_free-resource-on-thread yes
# Enable legacy compression of double to float.
# bool, default: no
#
# search-_numeric-compress no
# Disable print of time for ft.profile. For testing only.
# bool, default: yes
#
# search-_print-profile-clock yes
# Intersection iterator orders the children iterators by their relative estimated
# number of results in ascending order, so that if we see first iterators with
# a lower count of results we will skip a larger number of results, which
# translates into faster iteration. If this flag is set, we use this
# optimization in a way where union iterators are being factorize by the number
# of their own children, so that we sort by the number of children times the
# overall estimated number of results instead.
# bool, default: no
#
# search-_prioritize-intersect-union-children no
# Set to run without memory pools.
# bool, default: no
#
# search-no-mem-pools no
# Disable garbage collection (for this process).
# bool, default: no
#
# search-no-gc no
# Enable commands filter which optimize indexing on partial hash updates.
# bool, default: no
#
# search-partial-indexed-docs no
# Disable compression for DocID inverted index. Boost CPU performance.
# bool, default: no
#
# search-raw-docid-encoding no
# Number of search threads in the coordinator thread pool.
# numeric, valid range: [1, LLONG_MAX], default: 20
#
# search-threads 20
# Timeout for topology validation (in milliseconds). After this timeout,
# any pending requests will be processed, even if the topology is not fully connected.
# numeric, valid range: [0, LLONG_MAX], default: 30000
#
# search-topology-validation-timeout 30000
############################## TIME SERIES CONFIG #############################
# The maximal number of per-shard threads for cross-key queries when using cluster mode
# (TS.MRANGE, TS.MREVRANGE, TS.MGET, and TS.QUERYINDEX).
# Note: increasing this value may either increase or decrease the performance.
# integer, valid range: [1..16], default: 3
# This is a load-time configuration parameter.
#
# ts-num-threads 3
# Default compaction rules for newly created key with TS.ADD, TS.INCRBY, and TS.DECRBY.
# Has no effect on keys created with TS.CREATE.
# This default value is applied to each new time series upon its creation.
# string, see documentation for rules format, default: no compaction rules
#
# ts-compaction-policy ""
# Default chunk encoding for automatically-created compacted time series.
# This default value is applied to each new compacted time series automatically
# created when ts-compaction-policy is specified.
# valid values: COMPRESSED, UNCOMPRESSED, default: COMPRESSED
#
# ts-encoding COMPRESSED
# Default retention period, in milliseconds. 0 means no expiration.
# This default value is applied to each new time series upon its creation.
# If ts-compaction-policy is specified - it is overridden for created
# compactions as specified in ts-compaction-policy.
# integer, valid range: [0 .. LLONG_MAX], default: 0
#
# ts-retention-policy 0
# Default policy for handling insertion (TS.ADD and TS.MADD) of multiple
# samples with identical timestamps.
# This default value is applied to each new time series upon its creation.
# string, valid values: BLOCK, FIRST, LAST, MIN, MAX, SUM, default: BLOCK
#
# ts-duplicate-policy BLOCK
# Default initial allocation size, in bytes, for the data part of each new chunk
# This default value is applied to each new time series upon its creation.
# integer, valid range: [48 .. 1048576]; must be a multiple of 8, default: 4096
#
# ts-chunk-size-bytes 4096
# Default values for newly created time series.
# Many sensors report data periodically. Often, the difference between the measured
# value and the previous measured value is negligible and related to random noise
# or to measurement accuracy limitations. In such situations it may be preferable
# not to add the new measurement to the time series.
# A new sample is considered a duplicate and is ignored if the following conditions are met:
# - The time series is not a compaction;
# - The time series' DUPLICATE_POLICY IS LAST;
# - The sample is added in-order (timestamp >= max_timestamp);
# - The difference of the current timestamp from the previous timestamp
# (timestamp - max_timestamp) is less than or equal to ts-ignore-max-time-diff
# - The absolute value difference of the current value from the value at the previous maximum timestamp
# (abs(value - value_at_max_timestamp) is less than or equal to ts-ignore-max-val-diff.
# where max_timestamp is the timestamp of the sample with the largest timestamp in the time series,
# and value_at_max_timestamp is the value at max_timestamp.
# ts-ignore-max-time-diff: integer, valid range: [0 .. LLONG_MAX], default: 0
# ts-ignore-max-val-diff: double, Valid range: [0 .. DBL_MAX], default: 0
#
# ts-ignore-max-time-diff 0
# ts-ignore-max-val-diff 0
########################### BLOOM FILTERS CONFIG ##############################
# Defaults values for new Bloom filters created with BF.ADD, BF.MADD, BF.INSERT, and BF.RESERVE
# These defaults are applied to each new Bloom filter upon its creation.
# Error ratio
# The desired probability for false positives.
# For a false positive rate of 0.1% (1 in 1000) - the value should be 0.001.
# double, Valid range: (0 .. 1), value greater than 0.25 is treated as 0.25, default: 0.01
#
# bf-error-rate 0.01
# Initial capacity
# The number of entries intended to be added to the filter.
# integer, valid range: [1 .. 1GB], default: 100
#
# bf-initial-size 100
# Expansion factor
# When capacity is reached, an additional sub-filter is created.
# The size of the new sub-filter is the size of the last sub-filter multiplied
# by expansion.
# integer, [0 .. 32768]. 0 is equivalent to NONSCALING. default: 2
#
# bf-expansion-factor 2
########################### CUCKOO FILTERS CONFIG #############################
# Defaults values for new Cuckoo filters created with
# CF.ADD, CF.ADDNX, CF.INSERT, CF.INSERTNX, and CF.RESERVE
# These defaults are applied to each new Cuckoo filter upon its creation.
# Initial capacity
# A filter will likely not fill up to 100% of its capacity.
# Make sure to reserve extra capacity if you want to avoid expansions.
# value is rounded to the next 2^n integer.
# integer, valid range: [2*cf-bucket-size .. 1GB], default: 1024
#
# cf-initial-size 1024
# Number of items in each bucket
# The minimal false positive rate is 2/255 ~ 0.78% when bucket size of 1 is used.
# Larger buckets increase the error rate linearly, but improve the fill rate.
# integer, valid range: [1 .. 255], default: 2
#
# cf-bucket-size 2
# Maximum iterations
# Number of attempts to swap items between buckets before declaring filter
# as full and creating an additional filter.
# A lower value improves performance. A higher value improves fill rate.
# integer, Valid range: [1 .. 65535], default: 20
#
# cf-max-iterations 20
# Expansion factor
# When a new filter is created, its size is the size of the current filter
# multiplied by this factor.
# integer, Valid range: [0 .. 32768], 0 is equivalent to NONSCALING, default: 1
#
# cf-expansion-factor 1
# Maximum expansions
# integer, Valid range: [1 .. 65536], default: 32
#
# cf-max-expansions 32
################################## SECURITY ###################################
#
# The following is a list of command categories and their meanings:
#
# * search - Query engine related.
# * json - Data type: JSON related.
# * timeseries - Data type: time series related.
# * bloom - Data type: Bloom filter related.
# * cuckoo - Data type: cuckoo filter related.
# * topk - Data type: top-k related.
# * cms - Data type: count-min sketch related.
# * tdigest - Data type: t-digest related.

Binary file not shown.

Binary file not shown.

2361
redis-8.2.1/redis.conf Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
1656

361
redis-8.2.1/sentinel.conf Normal file
View File

@@ -0,0 +1,361 @@
# Example sentinel.conf
# By default protected mode is disabled in sentinel mode. Sentinel is reachable
# from interfaces different than localhost. Make sure the sentinel instance is
# protected from the outside world via firewalling or other means.
protected-mode no
# port <sentinel-port>
# The port that this sentinel instance will run on
port 26379
# By default Redis Sentinel does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis-sentinel.pid when
# daemonized.
daemonize no
# When running daemonized, Redis Sentinel writes a pid file in
# /var/run/redis-sentinel.pid by default. You can specify a custom pid file
# location here.
pidfile /var/run/redis-sentinel.pid
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably)
# warning (only very important / critical messages are logged)
# nothing (nothing is logged)
loglevel notice
# Specify the log file name. Also the empty string can be used to force
# Sentinel to log on the standard output. Note that if you use standard
# output for logging but daemonize, logs will be sent to /dev/null
logfile ""
# To enable logging to the system logger, just set 'syslog-enabled' to yes,
# and optionally update the other syslog parameters to suit your needs.
# syslog-enabled no
# Specify the syslog identity.
# syslog-ident sentinel
# Specify the syslog facility. Must be USER or between LOCAL0-LOCAL7.
# syslog-facility local0
# sentinel announce-ip <ip>
# sentinel announce-port <port>
#
# The above two configuration directives are useful in environments where,
# because of NAT, Sentinel is reachable from outside via a non-local address.
#
# When announce-ip is provided, the Sentinel will claim the specified IP address
# in HELLO messages used to gossip its presence, instead of auto-detecting the
# local address as it usually does.
#
# Similarly when announce-port is provided and is valid and non-zero, Sentinel
# will announce the specified TCP port.
#
# The two options don't need to be used together, if only announce-ip is
# provided, the Sentinel will announce the specified IP and the server port
# as specified by the "port" option. If only announce-port is provided, the
# Sentinel will announce the auto-detected local IP and the specified port.
#
# Example:
#
# sentinel announce-ip 1.2.3.4
# dir <working-directory>
# Every long running process should have a well-defined working directory.
# For Redis Sentinel to chdir to /tmp at startup is the simplest thing
# for the process to don't interfere with administrative tasks such as
# unmounting filesystems.
dir /tmp
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
#
# Tells Sentinel to monitor this master, and to consider it in O_DOWN
# (Objectively Down) state only if at least <quorum> sentinels agree.
#
# Note that whatever is the ODOWN quorum, a Sentinel will require to
# be elected by the majority of the known Sentinels in order to
# start a failover, so no failover can be performed in minority.
#
# Replicas are auto-discovered, so you don't need to specify replicas in
# any way. Sentinel itself will rewrite this configuration file adding
# the replicas using additional configuration options.
# Also note that the configuration file is rewritten when a
# replica is promoted to master.
#
# Note: master name should not include special characters or spaces.
# The valid charset is A-z 0-9 and the three characters ".-_".
sentinel monitor mymaster 127.0.0.1 6379 2
# sentinel auth-pass <master-name> <password>
#
# Set the password to use to authenticate with the master and replicas.
# Useful if there is a password set in the Redis instances to monitor.
#
# Note that the master password is also used for replicas, so it is not
# possible to set a different password in masters and replicas instances
# if you want to be able to monitor these instances with Sentinel.
#
# However you can have Redis instances without the authentication enabled
# mixed with Redis instances requiring the authentication (as long as the
# password set is the same for all the instances requiring the password) as
# the AUTH command will have no effect in Redis instances with authentication
# switched off.
#
# Example:
#
# sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# sentinel auth-user <master-name> <username>
#
# This is useful in order to authenticate to instances having ACL capabilities,
# that is, running Redis 6.0 or greater. When just auth-pass is provided the
# Sentinel instance will authenticate to Redis using the old "AUTH <pass>"
# method. When also an username is provided, it will use "AUTH <user> <pass>".
# In the Redis servers side, the ACL to provide just minimal access to
# Sentinel instances, should be configured along the following lines:
#
# user sentinel-user >somepassword +client +subscribe +publish \
# +ping +info +multi +slaveof +config +client +exec on
# sentinel down-after-milliseconds <master-name> <milliseconds>
#
# Number of milliseconds the master (or any attached replica or sentinel) should
# be unreachable (as in, not acceptable reply to PING, continuously, for the
# specified period) in order to consider it in S_DOWN state (Subjectively
# Down).
#
# Default is 30 seconds.
sentinel down-after-milliseconds mymaster 30000
# IMPORTANT NOTE: starting with Redis 6.2 ACL capability is supported for
# Sentinel mode, please refer to the Redis website https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/
# for more details.
# Sentinel's ACL users are defined in the following format:
#
# user <username> ... acl rules ...
#
# For example:
#
# user worker +@admin +@connection ~* on >ffa9203c493aa99
#
# For more information about ACL configuration please refer to the Redis
# website at https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/ and redis server configuration
# template redis.conf.
# ACL LOG
#
# The ACL Log tracks failed commands and authentication events associated
# with ACLs. The ACL Log is useful to troubleshoot failed commands blocked
# by ACLs. The ACL Log is stored in memory. You can reclaim memory with
# ACL LOG RESET. Define the maximum entry length of the ACL Log below.
acllog-max-len 128
# Using an external ACL file
#
# Instead of configuring users here in this file, it is possible to use
# a stand-alone file just listing users. The two methods cannot be mixed:
# if you configure users here and at the same time you activate the external
# ACL file, the server will refuse to start.
#
# The format of the external ACL user file is exactly the same as the
# format that is used inside redis.conf to describe users.
#
# aclfile /etc/redis/sentinel-users.acl
# requirepass <password>
#
# You can configure Sentinel itself to require a password, however when doing
# so Sentinel will try to authenticate with the same password to all the
# other Sentinels. So you need to configure all your Sentinels in a given
# group with the same "requirepass" password. Check the following documentation
# for more info: https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/
#
# IMPORTANT NOTE: starting with Redis 6.2 "requirepass" is a compatibility
# layer on top of the ACL system. The option effect will be just setting
# the password for the default user. Clients will still authenticate using
# AUTH <password> as usually, or more explicitly with AUTH default <password>
# if they follow the new protocol: both will work.
#
# New config files are advised to use separate authentication control for
# incoming connections (via ACL), and for outgoing connections (via
# sentinel-user and sentinel-pass)
#
# The requirepass is not compatible with aclfile option and the ACL LOAD
# command, these will cause requirepass to be ignored.
# sentinel sentinel-user <username>
#
# You can configure Sentinel to authenticate with other Sentinels with specific
# user name.
# sentinel sentinel-pass <password>
#
# The password for Sentinel to authenticate with other Sentinels. If sentinel-user
# is not configured, Sentinel will use 'default' user with sentinel-pass to authenticate.
# sentinel parallel-syncs <master-name> <numreplicas>
#
# How many replicas we can reconfigure to point to the new replica simultaneously
# during the failover. Use a low number if you use the replicas to serve query
# to avoid that all the replicas will be unreachable at about the same
# time while performing the synchronization with the master.
sentinel parallel-syncs mymaster 1
# sentinel failover-timeout <master-name> <milliseconds>
#
# Specifies the failover timeout in milliseconds. It is used in many ways:
#
# - The time needed to re-start a failover after a previous failover was
# already tried against the same master by a given Sentinel, is two
# times the failover timeout.
#
# - The time needed for a replica replicating to a wrong master according
# to a Sentinel current configuration, to be forced to replicate
# with the right master, is exactly the failover timeout (counting since
# the moment a Sentinel detected the misconfiguration).
#
# - The time needed to cancel a failover that is already in progress but
# did not produced any configuration change (SLAVEOF NO ONE yet not
# acknowledged by the promoted replica).
#
# - The maximum time a failover in progress waits for all the replicas to be
# reconfigured as replicas of the new master. However even after this time
# the replicas will be reconfigured by the Sentinels anyway, but not with
# the exact parallel-syncs progression as specified.
#
# Default is 3 minutes.
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#
# sentinel notification-script and sentinel reconfig-script are used in order
# to configure scripts that are called to notify the system administrator
# or to reconfigure clients after a failover. The scripts are executed
# with the following rules for error handling:
#
# If script exits with "1" the execution is retried later (up to a maximum
# number of times currently set to 10).
#
# If script exits with "2" (or an higher value) the script execution is
# not retried.
#
# If script terminates because it receives a signal the behavior is the same
# as exit code 1.
#
# A script has a maximum running time of 60 seconds. After this limit is
# reached the script is terminated with a SIGKILL and the execution retried.
# NOTIFICATION SCRIPT
#
# sentinel notification-script <master-name> <script-path>
#
# Call the specified notification script for any sentinel event that is
# generated in the WARNING level (for instance -sdown, -odown, and so forth).
# This script should notify the system administrator via email, SMS, or any
# other messaging system, that there is something wrong with the monitored
# Redis systems.
#
# The script is called with just two arguments: the first is the event type
# and the second the event description.
#
# The script must exist and be executable in order for sentinel to start if
# this option is provided.
#
# Example:
#
# sentinel notification-script mymaster /var/redis/notify.sh
# CLIENTS RECONFIGURATION SCRIPT
#
# sentinel client-reconfig-script <master-name> <script-path>
#
# When the master changed because of a failover a script can be called in
# order to perform application-specific tasks to notify the clients that the
# configuration has changed and the master is at a different address.
#
# The following arguments are passed to the script:
#
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
#
# <state> is currently always "start"
# <role> is either "leader" or "observer"
#
# The arguments from-ip, from-port, to-ip, to-port are used to communicate
# the old address of the master and the new address of the elected replica
# (now a master).
#
# This script should be resistant to multiple invocations.
#
# Example:
#
# sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
# SECURITY
#
# By default SENTINEL SET will not be able to change the notification-script
# and client-reconfig-script at runtime. This avoids a trivial security issue
# where clients can set the script to anything and trigger a failover in order
# to get the program executed.
sentinel deny-scripts-reconfig yes
# REDIS COMMANDS RENAMING (DEPRECATED)
#
# WARNING: avoid using this option if possible, instead use ACLs.
#
# Sometimes the Redis server has certain commands, that are needed for Sentinel
# to work correctly, renamed to unguessable strings. This is often the case
# of CONFIG and SLAVEOF in the context of providers that provide Redis as
# a service, and don't want the customers to reconfigure the instances outside
# of the administration console.
#
# In such case it is possible to tell Sentinel to use different command names
# instead of the normal ones. For example if the master "mymaster", and the
# associated replicas, have "CONFIG" all renamed to "GUESSME", I could use:
#
# SENTINEL rename-command mymaster CONFIG GUESSME
#
# After such configuration is set, every time Sentinel would use CONFIG it will
# use GUESSME instead. Note that there is no actual need to respect the command
# case, so writing "config guessme" is the same in the example above.
#
# SENTINEL SET can also be used in order to perform this configuration at runtime.
#
# In order to set a command back to its original name (undo the renaming), it
# is possible to just rename a command to itself:
#
# SENTINEL rename-command mymaster CONFIG CONFIG
# HOSTNAMES SUPPORT
#
# Normally Sentinel uses only IP addresses and requires SENTINEL MONITOR
# to specify an IP address. Also, it requires the Redis replica-announce-ip
# keyword to specify only IP addresses.
#
# You may enable hostnames support by enabling resolve-hostnames. Note
# that you must make sure your DNS is configured properly and that DNS
# resolution does not introduce very long delays.
#
SENTINEL resolve-hostnames no
# When resolve-hostnames is enabled, Sentinel still uses IP addresses
# when exposing instances to users, configuration files, etc. If you want
# to retain the hostnames when announced, enable announce-hostnames below.
#
SENTINEL announce-hostnames no
# When master_reboot_down_after_period is set to 0, Sentinel does not fail over
# when receiving a -LOADING response from a master. This was the only supported
# behavior before version 7.0.
#
# Otherwise, Sentinel will use this value as the time (in ms) it is willing to
# accept a -LOADING response after a master has been rebooted, before failing
# over.
SENTINEL master-reboot-down-after-period mymaster 0

View File

@@ -1,6 +1,6 @@
# port 端口号 # port 端口号
VITE_PORT = 8080 VITE_PORT = 8080
VITE_API_URL = 'http://dvadmin3api.django.icu:8001' VITE_API_URL = 'http://127.0.0.1:8000'
# open 运行 npm run dev 时自动打开浏览器 # open 运行 npm run dev 时自动打开浏览器
VITE_OPEN = false VITE_OPEN = false

View File

@@ -1,168 +0,0 @@
# Django-Vue3-Admin
[![img](https://img.shields.io/badge/license-MIT-blue.svg)](https://gitee.com/huge-dream/django-vue3-admin/blob/master/LICENSE) [![img](https://img.shields.io/badge/python-%3E=3.7.x-green.svg)](https://python.org/) [![PyPI - Django Version badge](https://img.shields.io/badge/django%20versions-3.2-blue)](https://docs.djangoproject.com/zh-hans/3.2/) [![img](https://img.shields.io/badge/node-%3E%3D%2012.0.0-brightgreen)](https://nodejs.org/zh-cn/) [![img](https://gitee.com/huge-dream/django-vue3-admin/badge/star.svg?theme=dark)](https://gitee.com/huge-dream/django-vue3-admin)
[preview](https://demo.dvadmin.com) | [Official website document](https://www.django-vue-admin.com) | [qq group](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [community](https://bbs.django-vue-admin.com) | [plugins market](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
💡 **「About」**
We are a group of young people who love Code. In this hot era, we hope to calm down and bring some of our colors and colors through code.
Because of love, so embrace the future
## framework introduction
💡 [django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin.git) Is a set of all open source rapid development platform, no reservation for individuals and enterprises free use.
* 🧑🤝🧑Front-end adoption Vue3+TS+pinia+fastcrud。
* 👭The backend uses the Python language Django framework as well as the powerful[Django REST Framework](https://pypi.org/project/djangorestframework)。
* 👫Permission authentication use[Django REST Framework SimpleJWT](https://pypi.org/project/djangorestframework-simplejwt)Supports the multi-terminal authentication system.
* 👬Support loading dynamic permission menu, multi - way easy permission control.
* 💏 Special thanks: [vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/).
* 💡 💏 Special thanks:[jetbrains](https://www.jetbrains.com/) To provide a free IntelliJ IDEA license for this open source project.
## Online experience
👩‍👧‍👦👩‍👧‍👦 demo address:[https://demo.dvadmin.com](https://demo.dvadmin.com)
* demo accountsuperadmin
* demo passwordadmin123456
👩👦👦docs:[https://django-vue-admin.com](https://django-vue-admin.com)
## communication
* Communication community:[click here](https://bbs.django-vue-admin.com)👩‍👦‍👦
* plugins market:[click here](https://bbs.django-vue-admin.com/plugMarket.html)👩‍👦‍👦
## source code url:
gitee(Main push)[https://gitee.com/huge-dream/django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin)👩‍👦‍👦
githubno data
## core function
1. 👨‍⚕️ Menu management: Configure the system menu, operation permissions, button permissions, back-end interface permissions, etc.
2. 🧑‍⚕️ Department management: Configure the system organization (company, department, role).
3. 👩‍⚕️ Role management: role menu permission allocation, data permission allocation, set roles according to the department for data range permission division.
4. 🧑‍🎓 Rights Specifies the rights of the authorization role.
5. 👨‍🎓 User management: The user is the system operator, this function mainly completes the system user configuration.
6. 👬 Interface whitelist: specifies the interface that does not need permission verification.
7. 🧑‍🔧 Dictionary management: Maintenance of some fixed data frequently used in the system.
8. 🧑‍🔧 Regional management: to manage provinces, cities, counties and regions.
9. 📁 Attachment management: Unified management of all files and pictures on the platform.
10. 🗓 operation logs: log and query the system normal operation; Log and query system exception information.
11.🔌 [plugins market] (<https://bbs.django-vue-admin.com/plugMarket.html>) : based on the Django framework - Vue - Admin application and plug-in development.
## plugins market 🔌
* Celery Asynchronous task[dvadmin-celery](https://gitee.com/huge-dream/dvadmin-celery)
* Upgrade center backend[dvadmin-upgrade-center](https://gitee.com/huge-dream/dvadmin-upgrade-center)
* Upgrade center front[dvadmin-upgrade-center-web](https://gitee.com/huge-dream/dvadmin-upgrade-center-web)
## before start project you need:
~~~
Python >= 3.8.0
nodejs >= 14.0
Mysql >= 5.7.0 (Optional. The default database is sqlite3. 8.0 is recommended)
Redis(Optional, the latest edition)
~~~
## frontend♝
```bash
# clone code
git clone https://gitee.com/huge-dream/django-vue3-admin.git
# enter code dir
cd web
# install dependence
npm install --registry=https://registry.npm.taobao.org
# Start service
npm run dev
# Visit http://localhost:8080 in your browser
# Parameters such as boot port can be configured in the #.env.development file
# Build the production environment
# npm run build
```
## backend💈
~~~bash
1. enter code dir cd backend
2. copy ./conf/env.example.py to ./conf dirrename as env.py
3. in env.py configure database information
mysql database recommended version: 8.0
mysql database character set: utf8mb4
4. install pip dependence
pip3 install -r requirements.txt
5. Execute the migration command:
python3 manage.py makemigrations
python3 manage.py migrate
6. Initialization data
python3 manage.py init
7. Initialize provincial, municipal and county data:
python3 manage.py init_area
8. start backend
python3 manage.py runserver 0.0.0.0:8000
or daphne :
daphne -b 0.0.0.0 -p 8000 application.asgi:application
~~~
### visit backend swagger
* visit url[http://localhost:8080](http://localhost:8080) (The default address is this one. If you want to change it, follow the configuration file)
* account`superadmin` password`admin123456`
### docker-compose
~~~shell
docker-compose up -d
# Initialize backend data (first execution only)
docker exec -ti dvadmin-django bash
python manage.py makemigrations
python manage.py migrate
python manage.py init_area
python manage.py init
exit
frontend urlhttp://127.0.0.1:8080
backend urlhttp://127.0.0.1:8080/api
# Change 127.0.0.1 to your own public ip address on the server
account`superadmin` password`admin123456`
# docker-compose stop
docker-compose down
# docker-compose restart
docker-compose restart
# docker-compose on start build
docker-compose up -d --build
~~~
## Demo screenshot✅
![image-01](https://images.gitee.com/uploads/images/2022/0530/234137_b58c8f98_5074988.png)
![image-02](https://images.gitee.com/uploads/images/2022/0530/234240_39834603_5074988.png)
![image-03](https://images.gitee.com/uploads/images/2022/0530/234339_35e728a0_5074988.png)
![image-04](https://images.gitee.com/uploads/images/2022/0530/234426_957036b0_5074988.png)
![image-05](https://images.gitee.com/uploads/images/2022/0530/234458_898be492_5074988.png)
![image-06](https://images.gitee.com/uploads/images/2022/0530/234521_35b40076_5074988.png)
![image-07](https://images.gitee.com/uploads/images/2022/0530/234615_c2325639_5074988.png)
![image-08](https://images.gitee.com/uploads/images/2022/0530/234639_1ed6cc93_5074988.png)
![image-09](https://images.gitee.com/uploads/images/2022/0530/234815_cea2c53f_5074988.png)
![image-10](https://images.gitee.com/uploads/images/2022/0530/234840_5f3e5f53_5074988.png)

View File

@@ -1,208 +0,0 @@
# Django-Vue3-Admin
[![img](https://img.shields.io/badge/license-MIT-blue.svg)](https://gitee.com/liqianglog/django-vue-admin/blob/master/LICENSE) [![img](https://img.shields.io/badge/python-%3E=3.7.x-green.svg)](https://python.org/) [![PyPI - Django Version badge](https://img.shields.io/badge/django%20versions-3.2-blue)](https://docs.djangoproject.com/zh-hans/3.2/) [![img](https://img.shields.io/badge/node-%3E%3D%2012.0.0-brightgreen)](https://nodejs.org/zh-cn/) [![img](https://gitee.com/liqianglog/django-vue-admin/badge/star.svg?theme=dark)](https://gitee.com/liqianglog/django-vue-admin)
[预 览](https://demo.dvadmin.com) | [官网文档](https://www.django-vue-admin.com) | [群聊](https://qm.qq.com/cgi-bin/qm/qr?k=fOdnHhC8DJlRHGYSnyhoB8P5rgogA6Vs&jump_from=webapi) | [社区](https://bbs.django-vue-admin.com) | [插件市场](https://bbs.django-vue-admin.com/plugMarket.html) | [Github](https://github.com/liqianglog/django-vue-admin)
💡 **「关于」**
我们是一群热爱代码的青年在这个炙热的时代下我们希望静下心来通过Code带来一点我们的色彩和颜色。
因为热爱,所以拥抱未来!
## 平台简介
💡 [django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin.git) 是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
django-vue3-admin 基于 vue3 + CompositionAPI + typescript + vite + element plus, 是一款全栈,快速,开源的后台管理系统!
* 🧑‍🤝‍🧑前端采用 Vue3+TS+pinia+fastcrud(感谢[vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/))
* 👭后端采用 Python 语言 Django 框架以及强大的 [Django REST Framework](https://pypi.org/project/djangorestframework)。
* 👫权限认证使用[Django REST Framework SimpleJWT](https://pypi.org/project/djangorestframework-simplejwt),支持多终端认证系统。
* 👬支持加载动态权限菜单,多方式轻松权限控制。
* 💏特别鸣谢:[vue-next-admin](https://lyt-top.gitee.io/vue-next-admin-doc-preview/)。
* 💡 特别感谢[jetbrains](https://www.jetbrains.com/) 为本开源项目提供免费的 IntelliJ IDEA 授权。
#### 🏭 环境支持
| Edge | Firefox | Chrome | Safari |
| --------- | ------------ | ----------- | ----------- |
| Edge ≥ 79 | Firefox ≥ 78 | Chrome ≥ 64 | Safari ≥ 12 |
> 由于 Vue3 不再支持 IE11故而 ElementPlus 也不支持 IE11 及之前版本。
## 在线体验
👩‍👧‍👦演示地址:[https://demo.dvadmin.com](https://demo.dvadmin.com)
- 账号superadmin
- 密码admin123456
👩‍👦‍👦文档地址:[coding](https://dvadmin-private.coding.net/share/km/cec69f3d-30fe-47d5-bd97-e9e851f0b776/K-2)
## 交流
- 交流社区:[戳我](https://bbs.django-vue-admin.com)👩‍👦‍👦
- 插件市场:[戳我](https://bbs.django-vue-admin.com/plugMarket.html)👩‍👦‍👦
- django-vue-admin交流01群(已满)812482043 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=aJVwjDvH-Es4MPJQuoO32N0SucK22TE5&jump_from=webapi)
- django-vue-admin交流02群(已满)687252418 [点击链接加入群聊](https://qm.qq.com/cgi-bin/qm/qr?k=4jJN4IjWGfxJ8YJXbb_gTsuWjR34WLdc&jump_from=webapi)
- django-vue-admin交流03群442108213 [点击链接加入群聊](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wsm5oSz3K8dElBYUDtLTcQSEPhINFkl8&authKey=M6sbER0z59ZakgBr5erFeZyFZU15CI52bErNZa%2FxSvvGIuVAbY0N5866v89hm%2FK4&noverify=0&group_code=442108213)
- 二维码
<img src='https://images.gitee.com/uploads/images/2022/0530/233203_5fb11883_5074988.jpeg' width='200'>
## 源码地址
gitee地址(主推)[https://gitee.com/huge-dream/django-vue3-admin](https://gitee.com/huge-dream/django-vue3-admin)👩‍👦‍👦
github地址暂无
## 内置功能
1. 👨‍⚕️菜单管理:配置系统菜单,操作权限,按钮权限标识、后端接口权限等。
2. 🧑‍⚕️部门管理:配置系统组织机构(公司、部门、角色)。
3. 👩‍⚕️角色管理:角色菜单权限分配、数据权限分配、设置角色按部门进行数据范围权限划分。
4. 🧑‍🎓按钮权限权限:授权角色的按钮权限和接口权限,可做到每一个接口都能授权数据范围。
5. 🧑‍🎓字段权限权限:授权页面的字段显示权限。
5. 👨‍🎓用户管理:用户是系统操作者,该功能主要完成系统用户配置。
6. 👬接口白名单:配置不需要进行权限校验的接口。
7. 🧑‍🔧字典管理:对系统中经常使用的一些较为固定的数据进行维护。
8. 🧑‍🔧地区管理:对省市县区域进行管理。
9. 📁附件管理:对平台上所有文件、图片等进行统一管理。
10. 🗓️操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
11. 🔌[插件市场 ](https://bbs.django-vue-admin.com/plugMarket.html)基于Django-Vue-Admin框架开发的应用和插件。
## 插件市场 🔌
- Celery异步任务[dvadmin-celery](https://gitee.com/huge-dream/dvadmin-celery)
- 升级中心后端:[dvadmin-upgrade-center](https://gitee.com/huge-dream/dvadmin-upgrade-center)
- 升级中心前端:[dvadmin-upgrade-center-web](https://gitee.com/huge-dream/dvadmin-upgrade-center-web)
## 准备工作
~~~
Python >= 3.8.0 (推荐3.8+版本)
nodejs >= 14.0 (推荐最新)
Mysql >= 5.7.0 (可选默认数据库sqlite3推荐8.0版本)
Redis(可选,最新版)
~~~
## 前端♝
```bash
# 克隆项目
git clone https://gitee.com/huge-dream/django-vue3-admin.git
# 进入项目目录
cd web
# 安装依赖
npm install --registry=https://registry.npm.taobao.org
# 启动服务
npm run dev
# 浏览器访问 http://localhost:8080
# .env.development 文件中可配置启动端口等参数
# 构建生产环境
# npm run build
```
## 后端💈
~~~bash
1. 进入项目目录 cd backend
2. 在项目根目录中,复制 ./conf/env.example.py 文件为一份新的到 ./conf 文件夹下,并重命名为 env.py
3. 在 env.py 中配置数据库信息
mysql数据库版本建议8.0
mysql数据库字符集utf8mb4
4. 安装依赖环境
pip3 install -r requirements.txt
5. 执行迁移命令:
python3 manage.py makemigrations
python3 manage.py migrate
6. 初始化数据
python3 manage.py init
7. 初始化省市县数据:
python3 manage.py init_area
8. 启动项目
python3 manage.py runserver 0.0.0.0:8000
或使用 daphne :
daphne -b 0.0.0.0 -p 8000 application.asgi:application
~~~
### 访问项目
- 访问地址:[http://localhost:8080](http://localhost:8080) (默认为此地址,如有修改请按照配置文件)
- 账号:`superadmin` 密码:`admin123456`
### docker-compose 运行
~~~shell
# 先安装docker-compose (自行百度安装),执行此命令等待安装如有使用celery插件请打开docker-compose.yml中celery 部分注释
docker-compose up -d
# 初始化后端数据(第一次执行即可)
docker exec -ti dvadmin-django bash
python manage.py makemigrations
python manage.py migrate
python manage.py init_area
python manage.py init
exit
前端地址http://127.0.0.1:8080
后端地址http://127.0.0.1:8080/api
# 在服务器上请把127.0.0.1 换成自己公网ip
账号superadmin 密码admin123456
# docker-compose 停止
docker-compose down
# docker-compose 重启
docker-compose restart
# docker-compose 启动时重新进行 build
docker-compose up -d --build
~~~
## 演示图✅
![image-01](https://images.gitee.com/uploads/images/2022/0530/234137_b58c8f98_5074988.png)
![image-02](https://images.gitee.com/uploads/images/2022/0530/234240_39834603_5074988.png)
![image-03](https://images.gitee.com/uploads/images/2022/0530/234339_35e728a0_5074988.png)
![image-04](https://images.gitee.com/uploads/images/2022/0530/234426_957036b0_5074988.png)
![image-05](https://images.gitee.com/uploads/images/2022/0530/234458_898be492_5074988.png)
![image-06](https://images.gitee.com/uploads/images/2022/0530/234521_35b40076_5074988.png)
![image-07](https://images.gitee.com/uploads/images/2022/0530/234615_c2325639_5074988.png)
![image-08](https://images.gitee.com/uploads/images/2022/0530/234639_1ed6cc93_5074988.png)
![image-09](https://images.gitee.com/uploads/images/2022/0530/234815_cea2c53f_5074988.png)
![image-10](https://images.gitee.com/uploads/images/2022/0530/234840_5f3e5f53_5074988.png)

77
web/flowH5.config.ts Normal file
View File

@@ -0,0 +1,77 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path, {resolve} from 'path';
import vueJsx from "@vitejs/plugin-vue-jsx";
import vueSetupExtend from "vite-plugin-vue-setup-extend";
import { terser } from 'rollup-plugin-terser';
import postcss from 'rollup-plugin-postcss';
import pxtorem from 'postcss-pxtorem';
const pathResolve = (dir: string) => {
return resolve(__dirname, '.', dir);
};
export default defineConfig({
build: {
// outDir: '../backend/static/previewer',
lib: {
entry: path.resolve(__dirname, 'src/views/plugins/dvadmin3-flow-web/src/flowH5/index.ts'), // 库的入口文件
name: 'previewer', // 库的全局变量名称
fileName: (format) => `index.${format}.js`, // 输出文件名格式
},
rollupOptions: {
input:{
previewer: path.resolve(__dirname, 'src/views/plugins/dvadmin3-flow-web/src/flowH5/index.ts'),
},
external: ['vue','xe-utils'], // 指定外部依赖
output:{
// dir: '../backend/static/previewer', // 输出目录
entryFileNames: 'index.[format].js', // 入口文件名格式
format: 'commonjs',
globals: {
vue: 'Vue'
},
chunkFileNames: `[name].[hash].js`
},
plugins: [
terser({
compress: {
drop_console: false, // 确保不移除 console.log
},
}),
postcss({
plugins: [
pxtorem({
rootValue: 37.5,
unitPrecision: 5,
propList: ['*'],
selectorBlackList: [],
replace: true,
mediaQuery: false,
minPixelValue: 0,
exclude: /node_modules/i,
}),
],
}),
],
},
},
plugins: [
vue(),
vueJsx(),
vueSetupExtend(),
],
resolve: {
alias: {
'/@': path.resolve(__dirname, 'src'), // '@' 别名指向 'src' 目录
'@views': pathResolve('./src/views'),
'/src':path.resolve(__dirname, 'src')
},
},
css:{
postcss:{
}
},
define: {
'process.env': {}
}
});

View File

@@ -1,13 +1,15 @@
{ {
"name": "django-vue3-admin", "name": "django-vue3-admin",
"version": "3.0.4", "version": "3.2.0",
"description": "是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。\n django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台权限粒度达到列级别前后端分离后端采用django + django-rest-framework前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus", "description": "是一套全部开源的快速开发平台,毫无保留给个人免费使用、团体授权使用。\n django-vue3-admin 基于RBAC模型的权限控制的一整套基础开发平台权限粒度达到列级别前后端分离后端采用django + django-rest-framework前端采用基于 vue3 + CompositionAPI + typescript + vite + element plus",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "vite --force", "dev": "vite --force",
"build:dev": "vite build --mode development",
"build": "vite build", "build": "vite build",
"build:local": "vite build --mode local_prod", "build:local": "vite build --mode local_prod",
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/" "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/",
"build:flowH5": "vite build --config flowH5.config.ts"
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
@@ -15,7 +17,9 @@
"@fast-crud/fast-extends": "^1.21.2", "@fast-crud/fast-extends": "^1.21.2",
"@fast-crud/ui-element": "^1.21.2", "@fast-crud/ui-element": "^1.21.2",
"@fast-crud/ui-interface": "^1.21.2", "@fast-crud/ui-interface": "^1.21.2",
"@great-dream/dvadmin3-celery-web": "^3.1.3",
"@iconify/vue": "^4.1.2", "@iconify/vue": "^4.1.2",
"@meetjs/vant4-kit": "^1.0.1",
"@types/lodash": "^4.17.7", "@types/lodash": "^4.17.7",
"@vitejs/plugin-vue-jsx": "^4.0.1", "@vitejs/plugin-vue-jsx": "^4.0.1",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
@@ -24,6 +28,7 @@
"axios": "^1.7.4", "axios": "^1.7.4",
"countup.js": "^2.8.0", "countup.js": "^2.8.0",
"cropperjs": "^1.6.2", "cropperjs": "^1.6.2",
"date-holidays": "^3.24.1",
"e-icon-picker": "2.1.1", "e-icon-picker": "2.1.1",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"echarts-gl": "^2.0.9", "echarts-gl": "^2.0.9",
@@ -34,7 +39,9 @@
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"js-table2excel": "^1.1.2", "js-table2excel": "^1.1.2",
"jsplumb": "^2.15.6", "jsplumb": "^2.15.6",
"less": "^4.3.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lunar-javascript": "^1.7.1",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.0.28", "pinia": "^2.0.28",
@@ -43,17 +50,23 @@
"print-js": "^1.6.0", "print-js": "^1.6.0",
"qrcodejs2-fixes": "^0.0.2", "qrcodejs2-fixes": "^0.0.2",
"qs": "^6.11.0", "qs": "^6.11.0",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-terser": "^7.0.2",
"screenfull": "^6.0.2", "screenfull": "^6.0.2",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.0",
"splitpanes": "^3.1.5", "splitpanes": "^3.1.5",
"tailwindcss": "^3.2.7", "tailwindcss": "^3.2.7",
"ts-md5": "^1.3.1", "ts-md5": "^1.3.1",
"upgrade": "^1.1.0", "upgrade": "^1.1.0",
"vant": "^4.9.19",
"vant4-kit": "^1.0.3",
"vue": "^3.4.38", "vue": "^3.4.38",
"vue-clipboard3": "^2.0.0", "vue-clipboard3": "^2.0.0",
"vue-cropper": "^1.0.8", "vue-cropper": "^1.0.8",
"vue-draggable-plus": "^0.6.0",
"vue-grid-layout": "^3.0.0-beta1", "vue-grid-layout": "^3.0.0-beta1",
"vue-i18n": "^9.14.0", "vue-i18n": "^9.14.0",
"vue-qr": "^4.0.9",
"vue-router": "^4.4.3", "vue-router": "^4.4.3",
"vxe-table": "^4.6.18", "vxe-table": "^4.6.18",
"xe-utils": "^3.5.30" "xe-utils": "^3.5.30"

View File

@@ -5,13 +5,14 @@
<LockScreen v-if="themeConfig.isLockScreen" /> <LockScreen v-if="themeConfig.isLockScreen" />
<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime > 1" /> <Setings ref="setingsRef" v-show="themeConfig.lockScreenTime > 1" />
<CloseFull v-if="!themeConfig.isLockScreen" /> <CloseFull v-if="!themeConfig.isLockScreen" />
<Scanned ref="scannedRef" ></Scanned>
<!-- <Upgrade v-if="getVersion" />--> <!-- <Upgrade v-if="getVersion" />-->
</el-config-provider> </el-config-provider>
</template> </template>
<script setup lang="ts" name="app"> <script setup lang="ts" name="app">
import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch, onBeforeUnmount } from 'vue'; import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch, onBeforeUnmount } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes'; import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
@@ -20,22 +21,28 @@ import other from '/@/utils/other';
import { Local, Session } from '/@/utils/storage'; import { Local, Session } from '/@/utils/storage';
import mittBus from '/@/utils/mitt'; import mittBus from '/@/utils/mitt';
import setIntroduction from '/@/utils/setIconfont'; import setIntroduction from '/@/utils/setIconfont';
import Scan from './scan';
const scan = new Scan(200); // 200是扫码枪有效输入间隔毫秒
let removeScanListener: () => void;
// 引入组件 // 引入组件
const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index.vue')); const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index.vue'));
const Setings = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/setings.vue')); const Setings = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/setings.vue'));
const CloseFull = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/closeFull.vue')); const CloseFull = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/closeFull.vue'));
const Upgrade = defineAsyncComponent(() => import('/@/layout/upgrade/index.vue')); const Scanned = defineAsyncComponent(() => import('/@/layout/Scanned/index.vue'));
import { ElMessageBox, ElNotification, NotificationHandle } from 'element-plus';
import { useCore } from '/@/utils/cores';
// 定义变量内容 // 定义变量内容
const { messages, locale } = useI18n(); const { messages, locale } = useI18n();
const setingsRef = ref(); const setingsRef = ref();
const scannedRef = ref();
const route = useRoute(); const route = useRoute();
const stores = useTagsViewRoutes(); const stores = useTagsViewRoutes();
const storesThemeConfig = useThemeConfig(); const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig); const { themeConfig } = storeToRefs(storesThemeConfig);
import websocket from '/@/utils/websocket'; const core = useCore();
import { ElNotification } from 'element-plus'; const router = useRouter();
// 获取版本号 // 获取版本号
const getVersion = computed(() => { const getVersion = computed(() => {
let isVersion = false; let isVersion = false;
@@ -60,14 +67,35 @@ onBeforeMount(() => {
// 设置批量第三方 js // 设置批量第三方 js
setIntroduction.jsCdn(); setIntroduction.jsCdn();
}); });
// 扫码后的 处理
const handleScan = (code: string) => {
console.log('Scanned code:', code);
scannedRef.value.isShow = true;
scannedRef.value.scanCode = code;
scannedRef.value.postData();
// 处理扫描后的逻辑
};
// 页面加载时 // 页面加载时
onMounted(() => { onMounted(() => {
// 开始监听扫码枪扫码并设置回调
removeScanListener = scan.onScan(handleScan);
scan.start();
nextTick(() => { nextTick(() => {
// 监听布局配'置弹窗点击打开 // 监听布局配'置弹窗点击打开
mittBus.on('openSetingsDrawer', () => { mittBus.on('openSetingsDrawer', () => {
setingsRef.value.openDrawer(); setingsRef.value.openDrawer();
}); });
// 设置皮肤缓存版本,每次更新版本可以所有用户清空缓存
const themeConfigVersion = '1.0.0'
// 获取缓存中的布局配置 // 获取缓存中的布局配置
if (Local.get('themeConfigVersion') !== themeConfigVersion) {
Local.clear();
Local.set('themeConfigVersion', themeConfigVersion);
window.location.reload();
return
}
if (Local.get('themeConfig')) { if (Local.get('themeConfig')) {
storesThemeConfig.setThemeConfig({ themeConfig: Local.get('themeConfig') }); storesThemeConfig.setThemeConfig({ themeConfig: Local.get('themeConfig') });
document.documentElement.style.cssText = Local.get('themeConfigStyle'); document.documentElement.style.cssText = Local.get('themeConfigStyle');
@@ -80,47 +108,13 @@ onMounted(() => {
}); });
// 页面销毁时,关闭监听布局配置/i18n监听 // 页面销毁时,关闭监听布局配置/i18n监听
onUnmounted(() => { onUnmounted(() => {
mittBus.off('openSetingsDrawer', () => {}); // 结束监听
}); scan.stop();
// 监听路由的变化,设置网站标题 // 移除全局事件监听器
watch( if (removeScanListener) removeScanListener();
() => route.path,
() => {
other.useTitle();
other.useFavicon();
if (!websocket.websocket) {
//websockt 模块
try {
websocket.init(wsReceive)
} catch (e) {
console.log('websocket错误');
}
}
},
{
deep: true,
}
);
// websocket相关代码 mittBus.off('openSetingsDrawer', () => {});
import { messageCenterStore } from '/@/stores/messageCenter'; mittBus.off('scanDataDoRefresh', () => {});
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,
});
}
};
onBeforeUnmount(() => {
// 关闭连接
websocket.close();
}); });
</script> </script>

View File

@@ -0,0 +1,55 @@
@font-face {
font-family: "iconfont"; /* Project id 3882322 */
src: url('iconfont.woff2?t=1676037377315') format('woff2'),
url('iconfont.woff?t=1676037377315') format('woff'),
url('iconfont.ttf?t=1676037377315') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-xiaoxizhongxin:before {
content: "\e665";
}
.icon-xitongshezhi:before {
content: "\e7ba";
}
.icon-caozuorizhi:before {
content: "\e611";
}
.icon-guanlidenglurizhi:before {
content: "\ea45";
}
.icon-rizhi:before {
content: "\e60c";
}
.icon-system:before {
content: "\e684";
}
.icon-Area:before {
content: "\eaa2";
}
.icon-file:before {
content: "\e671";
}
.icon-dict:before {
content: "\e626";
}
.icon-configure:before {
content: "\e733";
}

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More