385 Commits
Author SHA1 Message Date
bwstudio 9ba96bcc3e style(public): 前台导航页响应式适配 + 暗色模式 + 锚点目录
主要改动
- templates/public/base.html
  - 增加 3 套响应式断点:≤900px(平板)、≤600px(手机)、≤360px(小屏)
  - topbar 在小屏下压缩 padding/字号,actions 自适应换行
  - grid 卡片在手机端最小 160px,小屏单列
  - 卡片字号、间距在手机端缩放
  - 增加 sticky 分类目录条占位
  - 增加回到顶部按钮(滚动后显示)
  - 增加暗色模式 CSS(prefers-color-scheme + data-theme 手动覆盖)和主题切换按钮
  - 补充 theme-color、viewport 等移动端 meta
- templates/public/index.html
  - 输出 5 个分类的 sticky 目录导航(点击锚点跳转)
  - 每个 section 设置 id=cat-N 锚点
  - 卡片 title/url 增加 title 提示
- templates/public/category.html
  - 目录条加返回首页 + 当前分类名
  - section 锚点统一为 cat-detail

验证
- curl 抓取 /site/ 响应 200,HTML 中 10 项响应式/交互特性检查全部通过
- 服务器本地运行无异常
2026-09-05 15:33:45 +08:00
bwstudio 115585c00d feat(nav): 从 nav.bwhome.top 同步 5 个分类共 54 条导航
分类与数量
- 常用分类(4 条):短信平台、BT网站大全、菜鸟教程、旺柯博客
- 旺珂内网(10 条):MoviePilot / Jellyfin / qBittorent / 旁路由 / 小雅电影 / QNAP / NextCloud / PVE / BW gitea / 家庭服务器
- 常用网站(4 条):百度 / 豆瓣 / 新浪 / DeepSeek
- 休闲娱乐(18 条):HDFans / OKPT / HDTime / 小蚂蚁PT / HDArea / 铂金学院 / carpt / hdkyl / tosky / 52PT / 1PTBA / PTZONE / 馒头 / 铜钟音乐 / PTFans / 咖啡 / BTSCHOOL / 库非
- 技术网站(18 条):JFinal / shadmin / layuijfinal / respeed / 网站代理 / Layui / GitHub加速 / down.npee / DNS查询 / DOCK状态 / 博旺 / chromeDriver / 系统更新 / 免费节点 / Clash节点 / 狐蒂云 / chatgpt / NVIDIA AI

数据来源:https://nav.bwhome.top(OneNav 系统)
已 flask admin init --force 灌入数据库并通过 /site/api/navs 验证
2026-09-05 15:15:33 +08:00
bwstudio b208087f74 feat(nav): 新增前台公开导航页与后台导航管理
新增模块
- Nav 模型(applications/models/admin_nav.py):公开导航表,与 admin_power 解耦
- NavOutSchema / NavManageSchema(applications/schemas/admin_nav.py)
- 前台公开视图 applications/view/public/nav.py:/site/、/site/category/<name>、/site/api/navs,无需登录
- 后台管理视图 applications/view/system/nav.py:CRUD + 启用/禁用,挂入 system_bp
- 前台模板 templates/public/{base,index,category}.html(独立配色,无需登录即可访问)
- 后台模板 templates/system/nav/{main,add,edit}.html(Layui table + form)

调整
- applications/view/__init__.py:注册前台蓝图到根
- applications/view/system/__init__.py:注册后台 nav 子蓝图
- applications/common/script/admin.py:admin init/export 支持 navs 数组
- applications/common/script/app_data/init.json:新增 10 条导航种子 + 4 条 system:nav 权限

数据库
- 新增 public_nav 表(含 category 索引)
- migration: 38751d88d42c_add_public_nav_table.py
2026-09-05 14:58:08 +08:00
bwstudio 3debece5d3 0905初始化
- 在 .gitignore 中新增 .workbuddy/ 目录(WorkBuddy 运行时数据,不入版本库)
- 工程基线:venv 已建(Python 3.11.7)、迁移已生成、admin 种子数据已灌入
2026-09-05 14:40:26 +08:00
不胜舟 97eda740fe 区分测试生产环境部署配置 2026-01-16 19:21:30 +08:00
不胜舟 372b0030c3 重构 admin 初始化:改为 JSON 配置,增加导入导出功能 2026-01-16 18:48:57 +08:00
不胜舟 12c31a8e8a 重构 admin 初始化:改为 JSON 配置,增加导入导出功能 2026-01-16 18:43:03 +08:00
wojiaoyishang 490f538261 移除启用的定时任务文件夹 2025-12-14 13:26:12 +08:00
梦见吃了一大碗皮卡丘andGitee 9f64f7bdac !143 update templates/system/task/main.html.
Merge pull request !143 from laoknas/N/A
2025-12-14 01:47:53 +00:00
梦见吃了一大碗皮卡丘andGitee 96b93b08a1 !142 update templates/system/task/add.html.
Merge pull request !142 from laoknas/N/A
2025-12-14 01:47:44 +00:00
梦见吃了一大碗皮卡丘andGitee 3bfcd1d21f !141 update applications/extensions/__init__.py.
Merge pull request !141 from laoknas/N/A
2025-12-14 01:47:35 +00:00
laoknasandGitee c55e47ba4d update templates/system/task/main.html.
程序中已经没有改计划任务了,应该移除此模板。(会与计划任务插件冲突)

Signed-off-by: laoknas <kuai410022283@qq.com>
2025-12-13 16:55:45 +00:00
laoknasandGitee 29c6e5dbb6 update templates/system/task/add.html.
程序中已经没有改计划任务了,应该移除此模板。(会与计划任务插件冲突)

Signed-off-by: laoknas <kuai410022283@qq.com>
2025-12-13 16:54:57 +00:00
laoknasandGitee 0ebc9394ad update applications/extensions/__init__.py.
在 init_plugs 函数中添加了两个关键的事件广播调用:
- 第33行: broadcast_execute(app, 'event_init')
- 第34行: broadcast_execute(app, 'event_finish')

### 执行流程优化
修改后的 init_plugs 函数执行流程更加合理:

1. 加载插件模块 :注册所有启用的插件
2. 触发 event_begin :通知插件系统开始初始化
3. 初始化核心功能 :登录、数据库、邮件、上传等
4. 初始化系统蓝图 :模板指令、错误视图等
5. 触发 event_init :执行插件的主要初始化逻辑
6. 触发 event_finish :执行插件的最终初始化操作


### 事件触发时机
事件名称 触发时机 用途 event_begin 插件加载后,核心功能初始化前 插件准备工作 event_init 核心功能初始化完成后 插件主要初始化逻辑 event_finish 所有初始化完成后 插件最终初始化操作,包括应用上下文初始化

这个修改是插件系统架构的重要优化,确保了所有插件能够在正确的时机执行初始化逻辑,特别是对于复杂的任务插件至关重要。

Signed-off-by: laoknas <kuai410022283@qq.com>
2025-12-13 16:50:39 +00:00
不胜舟andGitee da453c839e !138 fix #ICW0DF:修复readme资源结果错别字
Merge pull request !138 from ChenWenhui/master
2025-09-02 08:47:50 +00:00
chenwenhui01 3986210925 fix:readme资源结果错别字 2025-09-02 16:38:52 +08:00
不胜舟 5bcc28a492 feat(api): 新增 CSRF 校验 2025-09-02 11:37:27 +08:00
不胜舟 000330dbd7 feat(cli): 新增检查路由鉴权装饰器的命令 2025-09-01 20:22:54 +08:00
不胜舟 ad7b79129e 引入接口频率限制机制 2025-08-25 22:42:52 +08:00
不胜舟 4da732538f 引入接口频率限制机制 2025-08-25 22:01:33 +08:00
我叫以赏andGitee 041447585c !136 fix: 当在lxc容器运行时,获取磁盘信息失败
Merge pull request !136 from zmf96/master
2025-05-18 15:22:58 +00:00
zmf96andGitee 49d1db3571 fix: 当在lxc容器运行时,获取磁盘信息失败
Signed-off-by: zmf96 <1602893+zmf96@user.noreply.gitee.com>
2025-05-18 15:21:49 +00:00
我叫以赏andGitee d9286699bf !135 fix:打开基本资料页面后,刷新浏览器会引发js无法运行
Merge pull request !135 from 浩浩/master
2025-04-18 12:37:40 +00:00
dinghao d5dd7302ad fix:打开基本资料页面后,刷新浏览器会引发js无法运行 2025-04-18 18:40:52 +08:00
我叫以赏andGitee 9561e8ae00 更新 README
Signed-off-by: 我叫以赏 <lovepikachu@foxmail.com>
2025-03-02 04:31:58 +00:00
我叫以赏andGitee d9e690b5e9 !134 【轻量级 PR】:update applications/view/system/rights.py.
Merge pull request !134 from 星辰大海/N/A
2025-02-25 11:28:31 +00:00
星辰大海andGitee d49b3c1a79 update applications/view/system/rights.py.
管理员也需要隐藏不要显示的菜单。

Signed-off-by: 星辰大海 <2317903+shaodj@user.noreply.gitee.com>
2025-02-25 11:27:47 +00:00
我叫以赏andGitee 7340e45f4b Update: 同步项目 Pear Admin Layui 的框架
Signed-off-by: 我叫以赏 <lovepikachu@foxmail.com>
2025-02-13 02:17:34 +00:00
我叫以赏andGitee ee8bf1cbfc Update: 更新文档
Signed-off-by: 我叫以赏 <lovepikachu@foxmail.com>
2025-02-13 02:16:35 +00:00
我叫以赏andGitee 634dabb8cc !133 Update: 同步项目框架与 Pear Admin 4.0 同步,完善部分功能
Merge pull request !133 from 我叫以赏/master
2025-02-13 02:14:07 +00:00
wojiaoyishang 23db487356 添加文档构建章节 2025-01-31 10:51:11 +08:00
wojiaoyishang c5b70d90fb 更新文档与预览地址 2025-01-30 17:04:30 +08:00
wojiaoyishang 552bf77924 完善文档并添加示例插件 2025-01-30 16:56:12 +08:00
wojiaoyishang ac1f82e81d 基本完成文档 2025-01-30 10:31:50 +08:00
wojiaoyishang 41bda82c60 完善开发文档与调整初始化项目架构 2025-01-28 17:47:41 +08:00
wojiaoyishang da5917344e 更新文档,完善插件功能 2025-01-27 21:34:42 +08:00
wojiaoyishang feed91dcd0 完善文档最佳实践 2025-01-27 20:41:22 +08:00
wojiaoyishang 5153daf7da 更新文档 2025-01-27 19:48:27 +08:00
wojiaoyishang 155af93c77 完善开发文档与合并 admin_log 文件 2025-01-27 11:30:59 +08:00
wojiaoyishang 0db36a6218 完善开发文档与更新函数注释 2025-01-27 11:16:26 +08:00
wojiaoyishang de4f2c6d52 完善快速开始文档 2025-01-26 20:45:54 +08:00
wojiaoyishang fa6cc47063 更新 docker 构建 2025-01-26 16:38:32 +08:00
wojiaoyishang c9b08a07f4 修改搜索表单的按钮样式 2025-01-26 16:12:59 +08:00
wojiaoyishang 145ccbfb3e 修复权限按钮类型添加问题,修复日志页面表格未对齐问题 2025-01-26 16:04:05 +08:00
wojiaoyishang 40d0ea956d 更正 docker 配置文件 2025-01-25 14:08:43 +08:00
wojiaoyishang 184c4ad80d 更正插件与配置文件 2025-01-25 14:03:37 +08:00
wojiaoyishang ba7cc42628 数据库中的权限地址直接改为跳转之后的地址,防止 https 出现错误 2025-01-25 10:07:31 +08:00
我叫以赏andGitee cb5e352e8d fix: 修正序列化表述错误
Merge pull request !1 from 星辰大海/N/A
2025-01-25 01:52:12 +00:00
wojiaoyishang 8a99864b41 删除用于测试的 print 2025-01-25 09:51:02 +08:00
星辰大海andGitee de644f0f34 update applications/models/admin_dept.py.
Signed-off-by: 星辰大海 <2317903+shaodj@user.noreply.gitee.com>
2025-01-24 22:33:09 +00:00
wojiaoyishang 2ee703d2eb 错误处理页面进行更新 2025-01-24 19:13:41 +08:00
wojiaoyishang 3c4377915b Update: 增加后台消息接口 2025-01-24 18:44:53 +08:00
wojiaoyishang f9ad038805 Update: 增加主页页脚 2025-01-24 18:37:40 +08:00
wojiaoyishang e27fa53ec8 Update: 同步项目框架与 Pear Admin 4.0 同步,完善部分功能 2025-01-24 18:02:49 +08:00
wojiaoyishang ed1f402c04 Update: (步骤备份)完善邮件管理、文件上传的页面更改 2025-01-24 18:01:02 +08:00
wojiaoyishang 52838222db Update: (步骤备份)完善字典 2025-01-23 21:16:16 +08:00
wojiaoyishang de36a9936a Update: (步骤备份)完善系统监控 2025-01-21 20:58:37 +08:00
wojiaoyishang 3bb59d3786 Update: (步骤备份)修复刷新内置标签页的问题 2025-01-21 16:09:27 +08:00
wojiaoyishang fa7e62b12b Update: (步骤备份)部门管理更新完成 2025-01-21 12:07:37 +08:00
wojiaoyishang b6a64f15dc Update: (步骤备份)更新夜间模式同步 2025-01-21 11:02:03 +08:00
wojiaoyishang 813363f0d4 Update: (步骤备份)更新部门管理前端 2025-01-20 21:38:08 +08:00
wojiaoyishang 517d6f3d7f Update: (步骤备份)修复权限搜索 2025-01-20 21:13:10 +08:00
wojiaoyishang 83eb664a45 Update: (步骤备份)更改主题色通知方式 2025-01-20 16:44:39 +08:00
wojiaoyishang 61be7a09c6 Update: (步骤备份)选项管理修改完成,增加批量删除功能 2025-01-20 16:38:38 +08:00
wojiaoyishang 9f8fe2c83e Update: (步骤备份)角色管理页面更新完成并增加参数效验 2025-01-20 14:59:41 +08:00
wojiaoyishang 830b9b246d Update: (步骤备份)登录界面和用户管理页面修改完成。 2025-01-19 20:24:13 +08:00
wojiaoyishang d4a1b0ec57 Update: (步骤备份)更新 Pear Admin 框架版本与 Pear Admin Layui 一致,尝试重写页面 2025-01-19 20:03:18 +08:00
我叫以赏andGitee 43370ad078 更新预览地址
Signed-off-by: 我叫以赏 <lovepikachu@foxmail.com>
2024-12-27 13:00:52 +00:00
我叫以赏andGitee 22fa797809 !121 fix 字典分类提交时候,验证type_code 是否为空
Merge pull request !121 from jiangsi/N/A
2024-12-27 09:44:27 +00:00
我叫以赏andGitee 1c6ac42b96 !128 【轻量级PR】remove put session operation in index_curd.get_captcha
Merge pull request !128 from qxdo/master
2024-12-27 09:36:41 +00:00
我叫以赏andGitee 149f12ce18 !123 update templates/system/mail/main.html.
Merge pull request !123 from laoknas/N/A
2024-08-14 13:32:05 +00:00
我叫以赏andGitee a9b304ab5a !124 update templates/system/admin_log/main.html.
Merge pull request !124 from laoknas/N/A
2024-08-14 13:31:48 +00:00
我叫以赏andGitee b7833142b0 !125 update templates/system/monitor.html.
Merge pull request !125 from laoknas/N/A
2024-08-14 13:31:29 +00:00
qxdoandGitee 6def11b009 移除内部session操作,会导致session首次创建不一致的问题
Signed-off-by: qxdo <qxdo123@gmail.com>
2024-08-14 12:28:46 +00:00
不胜舟andGitee 15b5fa4dd8 !127 【fix】修复修改主题为light-theme后菜单栏选中菜单名称不显示问题
Merge pull request !127 from redoing/master
2024-08-08 04:04:11 +00:00
redoing bbc545494b fix#修复修改主题为light-theme后菜单栏选中菜单名称不显示问题
修改主题theme.js中颜色和次要颜色逻辑
2024-08-08 11:03:25 +08:00
不胜舟andGitee 4b26a5ddcc !126 update templates/system/console/console.html.
Merge pull request !126 from laoknas/N/A
2024-07-25 06:08:22 +00:00
laoknasandGitee a7e0b18396 update templates/system/console/console.html.
src="{{ url_for('static', filename='system/admin/images/avatar.jpg') }}" avatar.jpg路径错误

Signed-off-by: laoknas <kuai410022283@qq.com>
2024-07-25 05:48:07 +00:00
laoknasandGitee da83a2a3f2 update templates/system/monitor.html.
<link rel="stylesheet" href="{{ url_for('static', filename='system/admin/css/other/console2.css') }}"/> css路径错误

Signed-off-by: laoknas <kuai410022283@qq.com>
2024-07-25 05:24:24 +00:00
laoknasandGitee 0037087c1e update templates/system/admin_log/main.html.
Signed-off-by: laoknas <kuai410022283@qq.com>
2024-07-25 05:19:43 +00:00
laoknasandGitee 5266ea4fc6 update templates/system/mail/main.html.
Signed-off-by: laoknas <kuai410022283@qq.com>
2024-07-25 05:18:46 +00:00
我叫以赏andGitee dad768cb18 更新 requirements.txt
Signed-off-by: 我叫以赏 <lovepikachu@foxmail.com>
2024-06-26 05:56:49 +00:00
我叫以赏andGitee 175b3f5f5b !106 更新 applications/common/helper.py 中函数注释的错误
Merge pull request !106 from 星辰大海/N/A
2024-06-26 02:36:54 +00:00
我叫以赏andGitee eb4f95e2bd !101 更新 docs/install.md.docs 中一处描述错误
Merge pull request !101 from z601350993/N/A
2024-06-26 02:36:21 +00:00
不胜舟andGitee a8642dcf29 !122 更新 README.md 使项目介绍更加完善
Merge pull request !122 from 我叫以赏/master
2024-06-26 02:28:01 +00:00
我叫以赏andGitee 1cadad5076 更新需要忽略的文件
Signed-off-by: 我叫以赏 <lovepikachu@foxmail.com>
2024-06-26 02:25:12 +00:00
我叫以赏andGitee fabe7a09bf doc: 更新 README 文档
Signed-off-by: 我叫以赏 <lovepikachu@foxmail.com>
2024-06-26 02:23:38 +00:00
我叫以赏andGitee 874084492e update applications/common/utils/validate.py.
新版本将会移除 flask.escape 按照官方说法使用 markupsafe.escape  代替,可以避免 Python Warning。

Signed-off-by: 我叫以赏 <lovepikachu@foxmail.com>
2024-02-10 04:21:13 +00:00
我叫以赏andGitee dff1cc18ce fix: 移出不必要的导入
Signed-off-by: 我叫以赏 <lovepikachu@foxmail.com>
2024-02-10 04:08:09 +00:00
不胜舟 21408f1bf3 add flask-session扩展 2024-01-14 17:56:46 +08:00
不胜舟 f59767096b fix 记住密码 2024-01-12 21:29:27 +08:00
不胜舟 57f2ce1413 add flask-session扩展 2024-01-12 15:52:27 +08:00
jiangsiandGitee 60674d7ab7 数据字典功能,具体的字典需要依赖type_code 和字典分类关联,需要在添加字典分类的时候验证是否填写了typecode,不然会出现type_code 为none的情况。 并且type_code在字典分类的编辑状态为readonly的。
Signed-off-by: jiangsi <jiangsi@gmail.com>
2023-12-24 18:39:56 +00:00
不胜舟 e984df7cd0 update README.md 2023-12-07 14:53:19 +08:00
不胜舟 1245dc541f 增加flask-wtf,引入csrf token验证 2023-12-07 14:00:29 +08:00
不胜舟andGitee 61f82f826b !119 修复登录页面验证码刷新问题
Merge pull request !119 from Huo-WenBo/master
2023-11-22 05:57:33 +00:00
HuoWenBo 5ea61e40d0 Merge remote-tracking branch 'origin/master' 2023-11-19 12:01:50 +08:00
HuoWenBo c09bca8419 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	templates/system/login.html
2023-11-19 12:00:53 +08:00
HuoWenBo 05d19f1dad Merge remote-tracking branch 'origin/master'
# Conflicts:
#	templates/system/login.html
2023-11-18 12:54:00 +08:00
HuoWenBo 9aa7420da7 🔧修复验证码刷新BUG 2023-11-18 12:53:46 +08:00
HuoWenBo a476856ce1 🔧修复验证码刷新BUG 2023-11-18 12:49:26 +08:00
HuoWenBo 009ab8effe Merge remote-tracking branch 'origin/master'
# Conflicts:
#	requirements.txt
2023-11-18 12:46:28 +08:00
不胜舟andGitee 749e25d22d !111 【轻量级 PR】:update templates/system/power/add.html.
Merge pull request !111 from Teishorin/N/A
2023-11-15 07:10:52 +00:00
不胜舟andGitee 8ca48511e9 !112 【轻量级 PR】:update templates/system/power/edit.html.
Merge pull request !112 from Teishorin/N/A
2023-11-15 07:10:26 +00:00
不胜舟 2683e1214c 修复用户管理部门树显示,回退sqlalchemy引用,修复头像修改 2023-11-14 19:44:38 +08:00
不胜舟andGitee af6fb5d273 !117 update applications/view/system/dept.py.
Merge pull request !117 from 陈心巧/N/A
2023-11-10 05:08:49 +00:00
陈心巧andGitee 2e806f1dcf update applications/view/system/dept.py.
修正部门不能更新bug。

id = json.get("deptId"),  会得到tuple
在其后的 Dept.query.filter_by(id=id) , 会生成形如 update 。。。。 from xx where id=( id, ) 的sql,导致执行sql失败
#id = json.get("deptId"), 
改成 
id = str_escape(json.get("deptId")) 即可。

Signed-off-by: 陈心巧 <12432082+chenxinqiao@user.noreply.gitee.com>
2023-11-09 08:11:17 +00:00
不胜舟andGitee 8a51e19cc0 !116 【轻量级 PR】:update templates/system/dept/main.html.
Merge pull request !116 from 陈心巧/N/A
2023-11-09 08:05:45 +00:00
陈心巧andGitee f02362859d update templates/system/dept/main.html.
点击编辑按钮后,obj.data['deptId'] 因其未定义会抛出错误,导致ajax请求获取该项内容失败,导致该项待编辑内容不能读取。改成 obj.data['id'] 即可。

Signed-off-by: 陈心巧 <12432082+chenxinqiao@user.noreply.gitee.com>
2023-11-09 08:05:03 +00:00
不胜舟andGitee a37e6928d2 !115 【轻量级 PR】:update docs/install.md.
Merge pull request !115 from 陈心巧/N/A
2023-09-22 04:23:31 +00:00
陈心巧andGitee dbb03e3e44 update docs/install.md.
requirement.txt 改成了requirements.txt。

bat 是 win下的, 这里原来写反了。

另外 在执行 flask db init 会报错导入不了BaseQuery,因为  Flask-SQLAlchemy 在3.0后,BaseQuery改成了Query。

Signed-off-by: 陈心巧 <12432082+chenxinqiao@user.noreply.gitee.com>
2023-09-22 04:15:08 +00:00
不胜舟andGitee 4a55d8feb2 !114 更新一些小 BUG
Merge pull request !114 from Huo-WenBo/master
2023-09-21 09:36:35 +00:00
Huo-WenBoandGitee 9abb4ae6a9 修复 from flask_sqlalchemyimport BaseQuery 不能导入的问题
Signed-off-by: Huo-WenBo <1684240320@qq.com>
2023-09-21 06:44:33 +00:00
Huo-WenBoandGitee 09c210363e update requirements.txt.
重新生成项目 requirements.txt

Signed-off-by: Huo-WenBo <1684240320@qq.com>
2023-09-21 06:43:20 +00:00
HuoWenBo 895377714c 重新生成 requirements.txt 需求文件
修复了 from flask_sqlalchemy import BaseQuery 不能导入的问题
2023-09-21 14:26:16 +08:00
HuoWenBo 0db116d729 Merge remote-tracking branch 'origin/master' 2023-09-21 14:17:15 +08:00
TeishorinandGitee e4e04f5c1a update templates/system/power/edit.html.
将排序改为必填项,防止因为没有填写排序号导致左侧导航条消失的问题

Signed-off-by: Teishorin <shandongdxl2004@126.com>
2023-09-08 11:26:45 +00:00
TeishorinandGitee db118f04ff update templates/system/power/add.html.
把排序列为必填项,防止因为没有填写序号导致左侧边栏消失

Signed-off-by: Teishorin <shandongdxl2004@126.com>
2023-09-08 11:22:18 +00:00
VincentFuand不胜舟 201ba7915e !107 【轻量级 PR】:update applications/common/utils/validate.py.
Merge pull request !107 from VincentFu/N/A
2023-08-16 07:14:31 +00:00
星辰大海andGitee 7ea6fd0f58 update applications/common/helper.py.
小于描述错误

Signed-off-by: 星辰大海 <shaodj@wxxingda.com>
2023-08-13 00:38:04 +00:00
不胜舟andGitee fa48eac75a !105 更改内存硬盘单位自适应
Merge pull request !105 from Huo-WenBo/master
2023-07-25 03:54:29 +00:00
HuoWenBo e199410bcb 更改内存单位显示为自适应 2023-07-25 11:49:59 +08:00
HuoWenBo c058e03fbf 更改内存硬盘单位自适应 2023-07-25 11:40:54 +08:00
不胜舟andGitee 10ae5f39f0 !102 【轻量级 PR】:启动时报错“No module named 'validators'”
Merge pull request !102 from z601350993/N/A
2023-07-16 12:03:05 +00:00
不胜舟andGitee dbea226c89 !103 【轻量级 PR】:update templates/system/dept/main.html.
Merge pull request !103 from 星辰大海/N/A
2023-07-16 10:34:31 +00:00
星辰大海andGitee 442279ab0d update templates/system/dept/main.html.
Signed-off-by: 星辰大海 <shaodj@wxxingda.com>
2023-07-16 09:56:53 +00:00
z601350993andGitee a2c6de82df 启动时报错“No module named 'validators'”
Signed-off-by: z601350993 <601350993@qq.com>
2023-07-15 23:06:41 +00:00
z601350993andGitee 757bafa8d2 update docs/install.md.docs
#### 运行项目   
windows 和 linux 写反了。

Signed-off-by: z601350993 <601350993@qq.com>
2023-07-15 22:51:23 +00:00
我叫以赏andGitee 4cabce3a8b fix: 修复插件报错提示 2023-07-04 05:39:47 +00:00
不胜舟andGitee c8140399c4 !99 权限管理 编辑、移除按钮失效
Merge pull request !99 from finalpro/master
2023-06-26 12:57:58 +00:00
finalproandGitee 5596f32940 修复编辑、移除按钮动作传参取值
Signed-off-by: finalpro <finalpro@163.com>
2023-06-25 13:11:21 +00:00
不胜舟 6cd58bbd9d fix: 部门禁用 2023-06-21 19:43:33 +08:00
不胜舟 90b6564ce6 完善文档 2023-06-21 01:44:52 +08:00
不胜舟 917579684e 去除首页面,优化命名,优化schema 2023-06-21 01:25:44 +08:00
不胜舟 4e7a85bead 去除首页面,优化命名,优化schema 2023-06-21 01:15:56 +08:00
不胜舟 d71e2e220f 去除logo 2023-05-31 00:12:30 +08:00
不胜舟 f60a7dfd85 精简功能 2023-05-30 18:46:30 +08:00
不胜舟 b3b5547116 Merge remote-tracking branch 'origin/master' 2023-05-30 01:00:16 +08:00
不胜舟 3690b8c22a 精简功能 2023-05-30 01:00:06 +08:00
六翼羊王 c6b71d2dfe 修改docker-compose文件路径 2023-05-29 07:48:55 +00:00
六翼羊王andGitee 47dffad5a7 删除文件 start.sh 2023-05-29 07:44:16 +00:00
不胜舟 c760302ff8 更改项目启动方式 2023-05-29 00:26:42 +08:00
不胜舟andGitee 384385dd10 !71 【轻量级 PR】:请求中的 Content-Type 不是 application/json,则从 request.values 中读取请求参数
Merge pull request !71 from funewa/N/A
2023-05-18 03:26:17 +00:00
不胜舟andGitee 7c68c8f572 !95 图标选择器不回显
Merge pull request !95 from BuerYouth/N/A
2023-05-11 04:11:47 +00:00
BuerYouthandGitee c035e024e4 fix:修复图标选择器不回显
Signed-off-by: BuerYouth <bueryouth@qq.com>
2023-05-10 12:26:02 +00:00
六翼羊王andGitee 7743580873 update README.md.
Signed-off-by: 六翼羊王 <1993333339@qq.com>
2023-05-07 09:57:52 +00:00
六翼羊王andGitee d4dcb55a36 update dockercompose.yaml.
Signed-off-by: 六翼羊王 <1993333339@qq.com>
2023-05-07 09:26:50 +00:00
六翼羊王andGitee 7ce0e0192e update dockercompose.yaml.
Signed-off-by: 六翼羊王 <1993333339@qq.com>
2023-05-07 09:26:01 +00:00
六翼羊王andGitee 1cf2a6a2e2 update .dockerignore.
Signed-off-by: 六翼羊王 <1993333339@qq.com>
2023-05-07 09:18:41 +00:00
六翼羊王andGitee 8e4d9a9151 update Dockerfile.
Signed-off-by: 六翼羊王 <1993333339@qq.com>
2023-05-07 09:18:22 +00:00
六翼羊王andGitee 65bf586d0c update Dockerfile.
Signed-off-by: 六翼羊王 <1993333339@qq.com>
2023-05-07 09:18:06 +00:00
六翼羊王andGitee 9896d87968 update start.sh.
Signed-off-by: 六翼羊王 <1993333339@qq.com>
2023-05-07 09:17:18 +00:00
六翼羊王andGitee 9d618d1fde update Dockerfile.
Signed-off-by: 六翼羊王 <1993333339@qq.com>
2023-05-07 08:36:57 +00:00
六翼羊王andGitee 12a6baf599 !94 修改了docker 一键部署,测试OK
Merge pull request !94 from Ranee/master
2023-05-07 08:18:37 +00:00
ranee@ubuntu 157a83711f 修改了基于docker 一键部署的Dockerfile 和dockercompose.yaml 2023-05-07 02:34:58 +08:00
ranee@ubuntu 0d0998ec3c 移动到了dockerdata 文件中 2023-05-07 02:33:03 +08:00
ranee@ubuntu 7ceb114822 移动到了dockerdata 文件中 2023-05-07 02:32:18 +08:00
ranee@ubuntu acc1b7d10b 修改了docker部署部分 2023-05-07 02:31:46 +08:00
ranee@ubuntu 6685310b5f 移动到dockerdata 文件夹中 2023-05-07 02:31:08 +08:00
ranee@ubuntu 631d86e22a 去掉了requirement,会导致主目录的同名文件被忽略 2023-05-07 02:30:37 +08:00
六翼羊王andGitee f45d202af6 update dockerdata/restart_mysql_data.sh.
Signed-off-by: 六翼羊王 <1993333339@qq.com>
2023-05-03 06:46:58 +00:00
六翼羊王andGitee 85d60365cf update README.md.
Signed-off-by: 六翼羊王 <1993333339@qq.com>
2023-05-03 03:36:07 +00:00
六翼羊王andGitee 1c4edb9cee 重置docker数据库
Signed-off-by: 六翼羊王 <1993333339@qq.com>
2023-05-03 03:34:30 +00:00
六翼羊王andGitee 251cd14810 update dockerdata/start.sh.
Signed-off-by: 六翼羊王 <1993333339@qq.com>
2023-05-03 03:23:21 +00:00
不胜舟andGitee 575fc3fa88 !90 【轻量级 PR】:fix database connect fail when password contain "@"
Merge pull request !90 from 彭华林/N/A
2023-05-02 17:12:01 +00:00
彭华林andGitee 31a6c83222 fix database connect fail when password contain "@"
# 
MYSQL_PASSWORD = "test@1234"
python app.py will run fail.

Signed-off-by: 彭华林 <12873625+hualpeng@user.noreply.gitee.com>
2023-05-02 16:31:59 +00:00
不胜舟andGitee a340533ff9 update applications/extensions/init_sqlalchemy.py.
Signed-off-by: 不胜舟 <1650473152@qq.com>
2023-04-26 14:07:50 +00:00
不胜舟 361e3c1bba edit md 2023-04-26 11:42:44 +08:00
六翼羊王andGitee 5b5e989f8a update docker-compose.yml.
Signed-off-by: 六翼羊王 <1993333339@qq.com>
2023-04-21 01:34:07 +00:00
六翼羊王andGitee 6092922bc2 update README.md.
Signed-off-by: 六翼羊王 <1993333339@qq.com>
2023-04-17 08:01:08 +00:00
六翼羊王andGitee c2d12db216 update dockerdata/start.sh.
修改启动文件

Signed-off-by: 六翼羊王 <1993333339@qq.com>
2023-04-17 07:29:39 +00:00
六翼羊王andGitee 392301bec5 update dockerdata/start.sh.
Signed-off-by: 六翼羊王 <1993333339@qq.com>
2023-04-17 07:26:16 +00:00
root 39b8a43685 修改docker-compose部署方式 2023-04-17 07:20:51 +00:00
我叫以赏andGitee 08713149b1 doc: 补充 README.md 文件缺失图片
Signed-off-by: 我叫以赏 <lovepikachu@foxmail.com>
2023-04-16 22:01:11 +00:00
不胜舟andGitee cbe5894c1d !89 doc: 完善 master 分支文档,并移除不需要的图片 & fix: 简化插件结构同步主项目
Merge pull request !89 from 我叫以赏/master
2023-04-16 08:57:37 +00:00
我叫以赏 72bf49d19e doc: 删除多余文件 2023-04-16 16:51:57 +08:00
我叫以赏 1c11295163 fix: 优化插件目录结构,默认不启用插件 2023-04-16 16:48:49 +08:00
我叫以赏 9e6b085ac5 fix: 修复插件不能使用的问题 2023-04-16 16:47:19 +08:00
我叫以赏 a1a318da44 doc: 更新 master 分支文档,并移除不需要的图片 2023-04-16 16:46:43 +08:00
我叫以赏 c86488d5bd doc: 更新 master 分支文档,并移除不需要的图片 2023-04-16 16:43:44 +08:00
我叫以赏 a88af0091b doc: 更新 master 分支文档,并移除不需要的 2023-04-16 16:41:56 +08:00
我叫以赏 0569d32784 doc: 更新 master 文档,并移除多余图片 2023-04-16 16:38:18 +08:00
我叫以赏 d71f8ce368 修复在更新目录结构后插件无法使用的问题 2023-04-16 15:55:29 +08:00
六翼羊王 787c657818 dockerignore文件 2023-04-13 01:22:48 +08:00
六翼羊王 5c9d8f4c36 修改docker-compose部署方式 2023-04-13 01:20:57 +08:00
不胜舟andGitee 7e6d48510c !87 修改docker-compose部署方式
Merge pull request !87 from 六翼羊王/master
2023-04-11 02:16:55 +00:00
六翼羊王 4a93167036 服务镜像构建文件
rename

增加docker-compose部署方式

提交docker-compose.yml

完整镜像构建文件

环境镜像构建文件

服务镜像构建文件

docker-compose方式使用的config.py

MySQL初始化数据

MySQL配置文件

rename

服务镜像启动脚本
2023-04-11 18:02:05 +08:00
不胜舟andGitee ab4a13bd4f !85 修正封装js命名问题
Merge pull request !85 from jmj/origin
2023-04-10 04:45:56 +00:00
jmj 4e20962935 修正icon封装js命名问题---会导致权限管理页面父级显示问题。 2023-04-10 12:36:21 +08:00
不胜舟 964d81a377 edit md 2023-04-07 02:14:05 +08:00
不胜舟 e18a50ba30 修复命令行工具 2023-04-06 20:08:27 +08:00
不胜舟 34f12842b0 rename 2023-04-06 17:50:02 +08:00
不胜舟 24aed42d69 Merge remote-tracking branch 'origin/master' 2023-04-06 17:44:00 +08:00
不胜舟 0b4dbf4259 简化配置文件 2023-04-06 17:43:27 +08:00
不胜舟andGitee 38301b8376 update README.md.
Signed-off-by: 不胜舟 <1650473152@qq.com>
2023-04-06 09:01:44 +00:00
不胜舟andGitee 347ee49d46 删除文件 pear.sql 2023-04-06 09:00:30 +00:00
不胜舟 5d10a304b3 更新前端 2023-04-06 16:41:48 +08:00
不胜舟 2364ebb519 修改文档 2023-04-06 16:25:04 +08:00
不胜舟 c5a8a1a653 修复json请求获取错误 2023-04-06 16:19:59 +08:00
不胜舟 186cc7e5a1 Merge remote-tracking branch 'origin/master' 2023-04-06 16:13:58 +08:00
不胜舟 43fd86a2ce 修改文档 2023-04-06 16:13:31 +08:00
不胜舟andGitee 08b2249e45 删除文件 migrations 2023-04-06 08:11:54 +00:00
不胜舟 bced84f92d 修改数据库初始化逻辑,完善权限,废弃flaskenv 2023-04-06 16:09:49 +08:00
不胜舟 269cc154ab 规范python包文件名,更新群聊 2023-04-05 23:53:44 +08:00
不胜舟 ac333f6027 修复图片显示错误 2023-03-12 23:13:42 +08:00
不胜舟 16e04fd8f2 增加参数验证 2023-03-08 14:09:36 +08:00
不胜舟andGitee 75adf7393c !79 【轻量级 PR】:update applications/view/admin/user.py.
Merge pull request !79 from ω•ƒεï/N/A
2023-03-08 03:30:57 +00:00
ω•ƒεïandGitee 2a523a3b41 update applications/view/admin/user.py.
用户管理 分页插件报错,TypeError: 'Pagination' object is not iterable,需要query 改成query.items解决

Signed-off-by: ω•ƒεï <164175317@qq.com>
2023-03-08 02:32:14 +00:00
不胜舟 990b8dd73e 修改用户管理bug 2023-02-19 18:01:44 +08:00
不胜舟 3a73465365 修复用户管理bug 2023-02-18 23:10:22 +08:00
不胜舟andGitee df756f9f0b !75 插件功能修改
Merge pull request !75 from 我叫以赏/master
2023-02-13 05:36:07 +00:00
我叫以赏andGitee fb993d3ade upload
Signed-off-by: 我叫以赏 <422880152@qq.com>
2023-02-13 00:26:48 +00:00
我叫以赏andGitee 0fb0937a8e update applications/common/utils/validate.py.
Signed-off-by: 我叫以赏 <422880152@qq.com>
2023-02-13 00:22:20 +00:00
我叫以赏andGitee 7c91bb3e0d update applications/common/utils/validate.py.
Signed-off-by: 我叫以赏 <422880152@qq.com>
2023-02-13 00:21:43 +00:00
我叫以赏andGitee c849c5cfed update applications/common/curd.py.
Signed-off-by: 我叫以赏 <422880152@qq.com>
2023-02-13 00:20:58 +00:00
我叫以赏andGitee c0b179eff1 update plugins/realip/__init__.py.
Signed-off-by: 我叫以赏 <422880152@qq.com>
2023-02-12 23:27:03 +00:00
我叫以赏andGitee 910227ef0c 删除文件 applications/common/utils/console.py 2023-02-12 23:26:04 +00:00
我叫以赏andGitee 3dcc167c27 删除文件 applications/common/utils/role.py 2023-02-12 23:25:08 +00:00
我叫以赏andGitee bf7f826250 删除文件 applications/common/utils/user.py 2023-02-12 23:24:59 +00:00
我叫以赏andGitee 25abd7e564 删除文件 applications/common/utils/power.py 2023-02-12 23:24:46 +00:00
我叫以赏andGitee 035dd21d11 删除文件 applications/common/utils/department.py 2023-02-12 23:24:36 +00:00
wojiaoyishang 9c920fefcc 修正init 2023-02-12 12:29:20 +08:00
wojiaoyishang 8e9d3519e4 修正init 2023-02-12 12:24:25 +08:00
wojiaoyishang e2ec08df62 更新注释 2023-02-11 20:48:07 +08:00
wojiaoyishang 6ef0de9d9e 更新注释 2023-02-11 20:39:12 +08:00
我叫以赏andGitee c7448f7833 删除文件 .flaskenv 2023-02-11 12:34:31 +00:00
wojiaoyishang a3c9ba1e3a 更新注释 2023-02-11 20:32:33 +08:00
wojiaoyishang 54ac9cefa3 更新注释 2023-02-11 20:29:56 +08:00
wojiaoyishang 196d53aa88 保留原邮件发送函数 2023-02-11 13:52:58 +08:00
wojiaoyishang 67b1462453 dev文件夹合并至utils 2023-02-11 13:34:46 +08:00
wojiaoyishang 0ad00448d8 移除表plugin_setting 2023-02-11 12:37:16 +08:00
wojiaoyishang 5b8c441b8b 去除插开发者信息 2023-02-11 08:43:48 +08:00
wojiaoyishang 7f7e142dc4 去除插开发者信息 2023-02-11 08:28:23 +08:00
不胜舟andGitee d36d312c9b !72 更新插件功能
Merge pull request !72 from 我叫以赏/master
2023-02-10 15:59:52 +00:00
wojiaoyishang b92c450be4 更新插件功能 2023-02-10 23:05:54 +08:00
wojiaoyishang 7bef64f632 更新插件功能 2023-02-10 21:29:39 +08:00
funewaandGitee 133774ced7 请求中的 Content-Type 不是 application/json,则从 request.values 中读取请求参数
Signed-off-by: funewa <yuge@funewa.com>
2023-02-07 06:19:53 +00:00
不胜舟andGitee 51db1944ed !60 update templates/admin/user/main.html.
Merge pull request !60 from Teishorin/N/A
2023-02-07 05:26:11 +00:00
不胜舟andGitee 23aebc2513 !58 update templates/admin/console/console.html.
Merge pull request !58 from Teishorin/N/A
2023-02-07 05:25:59 +00:00
不胜舟andGitee 417471bd79 !59 update templates/admin/common/footer.html.
Merge pull request !59 from Teishorin/N/A
2023-02-07 05:25:48 +00:00
不胜舟andGitee 61f0ea26a0 !61 update templates/admin/admin_log/main.html.
Merge pull request !61 from Teishorin/N/A
2023-02-07 05:25:37 +00:00
不胜舟andGitee 54a9acaf18 !62 update templates/admin/console/console.html.
Merge pull request !62 from Teishorin/N/A
2023-02-07 05:25:23 +00:00
不胜舟andGitee d6404fc1b4 !63 update templates/admin/mail/main.html.
Merge pull request !63 from Teishorin/N/A
2023-02-07 05:25:01 +00:00
不胜舟andGitee d4a298234d !64 update templates/admin/monitor.html.
Merge pull request !64 from Teishorin/N/A
2023-02-07 05:24:51 +00:00
不胜舟andGitee bd494c5aa1 !65 update templates/index/index.html.
Merge pull request !65 from Teishorin/N/A
2023-02-07 05:24:07 +00:00
不胜舟andGitee 0e5a389c12 !67 update applications/configs/config.py.
Merge pull request !67 from Teishorin/N/A
2023-02-07 05:23:32 +00:00
不胜舟andGitee b178f7fea5 !69 update applications/common/admin_log.py.
Merge pull request !69 from funewa/N/A
2023-02-07 05:21:45 +00:00
funewaandGitee 2c89cdb344 update applications/common/admin_log.py.
BUG修复原来只记录GET数据-》更新后可以记录POST PUT记录

Signed-off-by: funewa <yuge@funewa.com>
2023-02-07 04:15:27 +00:00
TeishorinandGitee 9189e0cb58 update applications/configs/config.py.
新增配置多数据库连接的连接串写法示例
涵盖 MySQL、MsSQL、Oracle、SQLite、Postgres

Signed-off-by: Teishorin <shandongdxl2004@126.com>
2023-01-24 07:52:20 +00:00
TeishorinandGitee 7368cb58b7 update templates/index/index.html.
修正url_for函数拼接静态文件路径可能发生的308错误:filename这里的路径不需要以“/”开头

Signed-off-by: Teishorin <shandongdxl2004@126.com>
2023-01-24 07:37:41 +00:00
TeishorinandGitee 627c51df8a update templates/admin/monitor.html.
修正url_for函数拼接静态文件路径可能发生的308错误:filename这里的路径不需要以“/”开头

Signed-off-by: Teishorin <shandongdxl2004@126.com>
2023-01-24 07:35:37 +00:00
TeishorinandGitee 3bf759aaac update templates/admin/mail/main.html.
修正url_for函数拼接静态文件路径可能发生的308错误:filename这里的路径不需要以“/”开头

Signed-off-by: Teishorin <shandongdxl2004@126.com>
2023-01-24 07:34:36 +00:00
TeishorinandGitee 7d406a9b06 update templates/admin/console/console.html.
修正url_for函数拼接静态文件路径可能发生的308错误:filename这里的路径不需要以“/”开头

Signed-off-by: Teishorin <shandongdxl2004@126.com>
2023-01-24 07:33:50 +00:00
TeishorinandGitee 42895b182f update templates/admin/admin_log/main.html.
修正url_for函数拼接静态文件路径可能发生的308错误:filename这里的路径不需要以“/”开头

Signed-off-by: Teishorin <shandongdxl2004@126.com>
2023-01-24 07:32:43 +00:00
TeishorinandGitee 21ba21bdf6 update templates/admin/user/main.html.
修正url_for函数拼接静态文件路径可能发生的308错误:filename这里的路径不需要以“/”开头

Signed-off-by: Teishorin <shandongdxl2004@126.com>
2023-01-24 07:31:18 +00:00
TeishorinandGitee 18d773ed9b update templates/admin/common/footer.html.
修正url_for函数拼接静态文件路径可能发生的308错误:filename这里的路径不需要以“/”开头

Signed-off-by: Teishorin <shandongdxl2004@126.com>
2023-01-24 07:27:29 +00:00
TeishorinandGitee f1fc957e3d update templates/admin/console/console.html.
修复URL拼接错误,在flask 2.2.2中,不修改的话会报如下错误:
127.0.0.1 - - [23/Jan/2023 23:07:45] "GET /static//admin/admin/css/other/console1.css HTTP/1.1" 308 -

Signed-off-by: Teishorin <shandongdxl2004@126.com>
2023-01-23 15:10:42 +00:00
不胜舟andGitee fd42cae20f !57 update templates/admin/login.html.
Merge pull request !57 from Teishorin/N/A
2023-01-23 10:02:03 +00:00
不胜舟andGitee 0b3406b55e !56 update templates/admin/common/header.html.
Merge pull request !56 from Teishorin/N/A
2023-01-23 09:59:59 +00:00
TeishorinandGitee 1b63075333 update templates/admin/login.html.
修正地址拼接的 308异常,删除多余的“/”,否则会显示:
“127.0.0.1 - - [23/Jan/2023 17:10:18] "GET /static//admin/admin/css/other/login.css HTTP/1.1" 308 -”
“127.0.0.1 - - [23/Jan/2023 17:10:18] "GET /static//admin/component/pear/css/pear.css HTTP/1.1" 308 -”

测试环境,python3.8  Flask2.2.2

Signed-off-by: Teishorin <shandongdxl2004@126.com>
2023-01-23 09:18:52 +00:00
TeishorinandGitee 2c94ad94e6 update templates/admin/common/header.html.
修正地址拼接的 308异常,删除多余的“/”,否则会显示:“127.0.0.1 - - [23/Jan/2023 17:10:18] "GET /static//admin/component/pear/css/pear.css HTTP/1.1" 308 -”

Signed-off-by: Teishorin <shandongdxl2004@126.com>
2023-01-23 09:14:50 +00:00
不胜舟 83964bfa78 修改鉴权逻辑 --> 管理员获得访问全部权限白名单 2022-12-28 01:38:40 +08:00
不胜舟andGitee b2207e277f !49 加入 Flask-APScheduler 設定
Merge pull request !49 from CHunYenc/master
2022-12-27 16:45:37 +00:00
不胜舟andGitee 017272cbde !51 添加邮件管理模块
Merge pull request !51 from mengfu188/master
2022-12-27 16:43:31 +00:00
不胜舟 f6d7d7d84c edit md 2022-12-28 00:34:08 +08:00
不胜舟 123f62f9d5 优化验证与过滤 2022-12-28 00:21:20 +08:00
不胜舟 656548eb64 remove 2022-12-27 22:59:18 +08:00
不胜舟 60cf94cc05 同步pear admin layui 2022-12-27 22:54:35 +08:00
不胜舟 883e7f88d5 优化用户部门查询逻辑 2022-12-27 21:56:31 +08:00
不胜舟 078b838374 更改验证码,简化目录 2022-12-27 20:48:50 +08:00
mengfu188 490302960a 添加邮件管理模块 2022-10-11 15:51:15 +08:00
CHunYenc d865081199 fix: 無法刪除 task. 2022-09-17 15:07:03 +08:00
CHunYenc 8e356923c7 feat: scheduler 儲存於 SQLAlchemyJobStore, 並在定時輸入時間時加入判斷. 2022-09-17 14:44:23 +08:00
zhengxinonly c811b895d7 修改说明文档 2022-08-31 04:31:17 +08:00
逆天的蝈蝈and不胜舟 8c4039dbb2 !47 del qq group and add issue
* 😊 del qq group and add issue
2022-08-16 08:52:02 +00:00
就眠儀式andGitee 8ca32d3d77 update README.md. 2022-08-04 05:39:29 +00:00
zhengxinonlyandGitee dc1e9afe0c !39 修复账号多角色时菜单重复,子菜单ID小于父菜单ID时无法显示的问题
Merge pull request !39 from waizking/N/A
2022-06-16 08:44:53 +00:00
就眠儀式andGitee 6220f979bf !42 update Dockerfile.
Merge pull request !42 from kyqv/N/A
2022-06-16 04:09:10 +00:00
kyqvandGitee 6ec05fd3f9 update Dockerfile.
阿里镜像源问题更新 导致docker 无法安装
2022-06-16 03:45:10 +00:00
不胜舟andGitee 8c3e43b85b !41 修复mysql连接中密码包含@特殊字符时的错误
Merge pull request !41 from stevenhu/N/A
2022-06-02 07:11:05 +00:00
stevenhuandGitee 248145b821 修复mysql连接中密码包含@特殊字符时的错误 2022-06-02 05:08:36 +00:00
就眠儀式andGitee 261a983678 update README.md. 2022-05-13 06:52:00 +00:00
sunshine 1a7e457054 解决bug 2022-05-08 21:29:44 +08:00
不胜舟andGitee 7ae59415e8 update applications/view/passport/__init__.py. 2022-04-12 11:16:54 +00:00
sunshine c50d5e7921 重写sqlalchemy的query,增加序列化方法 2022-04-12 16:55:27 +08:00
sunshine af873bf45e 升级依赖 2022-04-12 16:50:48 +08:00
waizkingandGitee 4384a3000c 修复账号多角色时菜单重复,子菜单ID小于父菜单ID时无法显示的问题 2022-03-21 03:15:25 +00:00
不胜舟andGitee 72a951a2b6 !34 修改数据库的字符集
Merge pull request !34 from zebra/N/A
2022-02-08 10:20:02 +00:00
zebraandGitee 38a5136e69 修改数据库的字符集 2022-02-08 10:18:47 +00:00
不胜舟andGitee 96dbeb0cbd !33 【轻量级 PR】:update applications/configs/config.py.
Merge pull request !33 from VincentFu/N/A
2022-01-25 07:14:04 +00:00
VincentFuandGitee 936c37c65c update applications/configs/config.py.
数据库编码设置为utf8mb4
2022-01-25 07:12:58 +00:00
resonate e51c86c83f reset 2022-01-14 12:46:40 +08:00
resonate 71a25f4774 session存入redis 2022-01-14 12:39:42 +08:00
杭州-伍个一andGitee 56563657e8 update applications/common/helper.py.
修正between查询的类型
2021-12-15 03:18:23 +00:00
mkg 6bfb10e4a4 增加自动序列化(根据orm模型)方法,增加软(逻辑)删除 2021-12-07 15:39:56 +08:00
mkg 70cb8eb35b 更新依赖 2021-11-22 14:13:08 +08:00
mkg cdea48c7d4 加入webargs验证扩展,分区用于验证的inschema和用户序列化的outschema 2021-11-17 18:07:36 +08:00
mkg fba22bbcbf 构造多级菜单树(来源mini分支) 2021-11-17 16:02:51 +08:00
不胜舟andGitee 14f96abdb3 !28 回退 'Pull Request !25 : 【轻量级 PR】:update applications/common/admin.py.'
Merge pull request !28 from 不胜舟/revert-merge-25-master
2021-11-17 02:42:15 +00:00
不胜舟andGitee 28ff95bf0d 回退 'Pull Request !25 : 【轻量级 PR】:update applications/common/admin.py.' 2021-11-17 02:41:27 +00:00
mkg 6cee5a6104 合并配置文件,解决logo打印多次问题 2021-11-16 23:18:20 +08:00
不胜舟andGitee 3add95e910 update requirement/requirement-dev.txt. 2021-11-16 09:41:18 +00:00
mkg 1db1064c11 图片上传数据库存储改为相对路径 2021-11-14 22:40:44 +08:00
mkg f75928119e 修改目录 2021-11-14 20:15:26 +08:00
mkg 9051e8138c 修改环境配置文件 2021-11-14 18:36:08 +08:00
mkg 8396f49105 简化requirement 2021-11-10 23:02:28 +08:00
不胜舟andGitee c664c70bbc !26 update applications/view/department/__init__.py.
Merge pull request !26 from sys/N/A
2021-11-09 13:30:33 +00:00
不胜舟andGitee 7a503cd94a !27 update applications/templates/admin/dept/edit.html.
Merge pull request !27 from sys/N/A
2021-11-09 13:30:20 +00:00
sysandGitee 7a9aeec8db update applications/templates/admin/dept/edit.html. 2021-11-09 06:33:20 +00:00
sysandGitee 2c57bc36ea update applications/view/department/__init__.py. 2021-11-09 06:17:05 +00:00
mkg c0fc70425b 删除.env 2021-11-08 21:39:39 +08:00
mkg f95944a781 Merge branch 'master' of https://gitee.com/pear-admin/pear-admin-flask into master 2021-11-08 18:52:46 +08:00
mkg 589e0c8ce7 提交docker-compose.yml 2021-11-08 18:52:36 +08:00
不胜舟andGitee 6e0ff64893 !25 update applications/common/admin.py.
Merge pull request !25 from sys/N/A
2021-11-06 17:26:18 +00:00
sysandGitee 044e432eb7 update applications/common/admin.py. 2021-11-06 17:22:05 +00:00
不胜舟andGitee 5887079ade !24 update applications/view/admin/user.py.
Merge pull request !24 from sys/N/A
2021-11-06 12:34:50 +00:00
sysandGitee 724e0817a5 update applications/view/admin/user.py. 2021-11-06 12:31:52 +00:00
不胜舟andGitee 9caa7a22e5 update applications/configs/config.py. 2021-11-06 09:17:49 +00:00
mkgandGitee c2042d5f80 update start.sh. 2021-10-28 07:56:41 +00:00
mkg 51f17c9539 提交Dockerfile 2021-10-28 13:12:18 +08:00
mkg 27ed99ee27 增加gunicorn启动脚本 2021-10-28 10:00:21 +08:00
mkg 7eb41e9754 修补日志的xss漏洞 2021-10-13 19:38:28 +08:00
mkg c7f266a482 Merge branch 'master' of https://gitee.com/pear-admin/pear-admin-flask into master 2021-10-08 22:58:28 +08:00
mkg f2d444b877 修补iconPicker -> icon 导致的bug 2021-10-08 22:58:14 +08:00
mkgandGitee 9e59035300 !23 update applications/view/admin/dict.py.
Merge pull request !23 from VincentFu/N/A
2021-10-06 08:23:54 +00:00
VincentFuandGitee b0ed2100a2 update applications/view/admin/dict.py.
修复字典类型更新
2021-10-05 05:53:05 +00:00
mkgandGitee 6f0c86fba6 !18 update applications/view/department/__init__.py.
Merge pull request !18 from VincentFu/N/A
2021-10-04 00:42:32 +00:00
VincentFuandGitee c71562a12f update applications/view/department/__init__.py.
这个地方也需要相应的修改,和power.py一样。
2021-10-03 17:31:39 +00:00
mkg a7fa415545 修改权限管理为post 2021-10-02 23:06:20 +08:00
mkg 24d2523385 更新依赖pearadminlayui 2021-10-02 16:11:07 +08:00
Rabbitandmkg d30a3d79de !17 修改角色状态值
* 角色状态值修复
2021-09-23 02:35:20 +00:00
mkgandGitee 860d5778cc !16 简化分页取count
Merge pull request !16 from Rabbit/master
2021-09-23 02:09:34 +00:00
xz 23dbe9ee9c 简化分页取count 2021-09-23 09:57:13 +08:00
就眠儀式andGitee 9114964ef2 update README.md. 2021-09-21 18:36:34 +00:00
mkgandGitee 56615528b9 !15 新增字典数据405
Merge pull request !15 from yanyuan/master
2021-09-09 09:54:12 +00:00
yanyuanandGitee 1b5c54daf9 update applications/view/admin/dict.py.
新增字典数据有误
2021-09-09 09:50:05 +00:00
Rabbitandmkg c0a3f5243b !14 【轻量级 PR】:update applications/view/admin/user.py.
Merge pull request !14 from Rabbit/N/A
2021-09-09 02:29:22 +00:00
Rabbitandmkg d22eb22f74 !13 【轻量级 PR】:update applications/view/admin/role.py.
Merge pull request !13 from Rabbit/N/A
2021-09-09 02:29:04 +00:00
就眠儀式andGitee 7eeb4c2489 !12 update applications/templates/admin/user/main.html.
Merge pull request !12 from Rabbit/N/A
2021-09-08 14:29:40 +00:00
RabbitandGitee d691c444ae update applications/templates/admin/user/main.html.
312行:table.reload('role-table') -> table.reload('user-table')
2021-09-08 14:27:39 +00:00
mkgandGitee 83c23ea276 !10 update applications/view/admin/power.py.
Merge pull request !10 from Rabbit/N/A
2021-09-06 11:23:43 +00:00
mkgandGitee cff67d152f !11 update applications/view/admin/user.py.
Merge pull request !11 from Rabbit/N/A
2021-09-06 11:23:25 +00:00
mkgandGitee ad67c289d3 !9 update applications/view/admin/role.py.
Merge pull request !9 from Rabbit/N/A
2021-09-06 11:23:05 +00:00
RabbitandGitee 4a4d8f763d update applications/view/admin/user.py.
多对多删除简化
2021-09-06 10:24:05 +00:00
RabbitandGitee dfc734c13a update applications/view/admin/power.py.
多对多关系删除简化
2021-09-06 10:21:51 +00:00
RabbitandGitee 654f441d05 update applications/view/admin/role.py. 2021-09-06 10:19:02 +00:00
mkg 48eebc1c95 定时任务(部分) 2021-08-26 15:31:36 +08:00
mkg 3c1764e58d 图形验证码 2021-08-26 12:12:39 +08:00
mkg 233241c962 优化验证码字符 2021-08-26 12:06:27 +08:00
mkg 30f9774ab1 Merge branch 'master' of https://gitee.com/pear-admin/pear-admin-flask into master 2021-08-25 21:17:33 +08:00
mkg a255c746d4 解决验证码可绕过安全问题 2021-08-25 21:17:16 +08:00
mkgandGitee eac2a1c941 update requirement/requirement-dev.txt. 2021-07-19 13:51:58 +00:00
mkg 110a5faaff 回退依赖版本解决不能上传问题 2021-07-19 21:40:03 +08:00
mkg f3cfdcf580 _id和id混乱导致的role编辑bug 2021-07-19 15:55:32 +08:00
mkg 34a1066cd0 user中_id改为id 2021-07-18 20:16:49 +08:00
mkg 67d05c39f3 修复过滤后count数量 2021-07-18 14:45:07 +08:00
mkg fa49130c4f 更新readme 2021-07-16 17:48:08 +08:00
mkg 5b76973d9c 删除因去除curd层复制错误导致的多余代码 2021-07-16 17:45:09 +08:00
mkg 0d57a2a8f2 更新md 2021-07-16 13:10:01 +08:00
mkg 78b7dae880 增加命令行新建空白视图功能 2021-07-16 13:04:49 +08:00
mkg 04a89144ba 扩充过滤器 2021-07-12 14:38:11 +08:00
mkg 3b3eecc1d1 删除curd层 2021-07-12 13:04:16 +08:00
mkg ca92d949dd 增加layui表格分页器layui_paginate() 2021-07-12 12:13:29 +08:00
mkg 663c7925e3 Merge branch 'master' of https://gitee.com/pear-admin/pear-admin-flask into master 2021-07-12 11:28:34 +08:00
mkg 76b2ad6f58 增加多条件查询构造器,删除部分curd层 2021-07-12 11:28:21 +08:00
mkgandGitee fd8dcfee40 update README.md. 2021-07-07 23:55:21 +00:00
mkg 0a60c1960d 更新layui,修复数据库初始化路径问题 2021-07-07 13:54:37 +08:00
mkg 28021df2ab 更新文档 2021-07-04 17:17:14 +08:00
mkg 6d42b6d56c 更新文档 2021-07-04 17:15:51 +08:00
mkgandGitee 3b467f42fe update requirement/requirement-dev.txt. 2021-07-04 09:01:37 +00:00
mkg e37749c669 Merge branch 'v2' of https://gitee.com/pear-admin/pear-admin-flask into v2 2021-06-24 13:17:45 +08:00
mkg 2cbbf25060 增加定时任务前端(部分) 2021-06-24 13:17:34 +08:00
mkgandGitee 986ae4e4bc update requirement/requirement-dev.txt. 2021-06-17 22:00:40 +08:00
mkg 103aaf4911 增加定时任务 2021-06-17 21:59:28 +08:00
mkgandGitee 7038ce64a6 update requirement/requirement-dev.txt. 2021-06-17 16:54:53 +08:00
mkgandGitee 9b8525ca1e update requirement/requirement-dev.txt. 2021-06-17 13:06:55 +08:00
mkgandGitee 254ce53387 update requirement/requirement-dev.txt. 2021-06-16 15:53:10 +08:00
mkgandGitee 0ff0c456b0 update .env. 2021-06-16 15:44:50 +08:00
mkg 57a54090bb Merge branch 'v2' of https://gitee.com/pear-admin/pear-admin-flask into v2 2021-06-16 15:40:48 +08:00
mkg e3d3424ee1 增加邮箱功能 2021-06-16 15:34:39 +08:00
mkgandGitee 99b2f611ec add .env. 2021-06-13 16:39:01 +08:00
mkgandGitee a675d78b82 !6 解决冲突
Merge pull request !6 from mkg/pr_5
2021-06-13 16:16:43 +08:00
mkg 62bd10cc60 Merge branch 'v2' into pr_5
# Conflicts:
#	applications/view/__init__.py
2021-06-13 16:14:23 +08:00
mkg 14a32e6d3c 增加数据库迁移命令和数据库初始化命令,分离schemas 2021-06-12 23:05:30 +08:00
mkgandGitee 5e08f3e49b !4 调整目录结构
Merge pull request !4 from zhengxinonly/v2
2021-06-12 21:26:24 +08:00
1296 changed files with 131500 additions and 21729 deletions
+11
View File
@@ -0,0 +1,11 @@
# 构建的相关文件
README.md
# Python 相关文件
migrations/
instance/
flask_session/
venv/
.idea/
*.log
*.db
-3
View File
@@ -1,3 +0,0 @@
FLASK_ENV=development
FLASK_RUN_HOST=0.0.0.0
FLASK_APP='applications:create_app'
+14 -5
View File
@@ -114,11 +114,20 @@ dmypy.json
# Pyre type checker
.pyre/
#ide
# idea
.idea/
# 数据库文件
# 迁移文件
migrations/
# sqlite
*.db
#上传到本地的图片文件
*upload/
migrations/
# 文件上传
static/upload/
# flask session
flask_session/
# WorkBuddy 运行时目录(不应纳入版本管理)
.workbuddy/
+184 -84
View File
@@ -1,23 +1,21 @@
<div align="center">
<br/>
<br/>
<img src="https://gitee.com/pear-admin/Pear-Admin-Layui/raw/master/admin/images/logo.png" width="90px" style="margin-top:30px;"/>
<h1 align="center">
Pear Admin Flask
</h1>
<h4 align="center">
开 箱 即 用 的 Flask 快 速 开 发 平 台
</h4>
[预 览](http://flask.pearadmin.com) | [官 网](http://www.pearadmin.com/) | [群聊](https://jq.qq.com/?_wv=1027&k=5OdSmve) | [社区](http://forum.pearadmin.com/)
</h4>
[预览](https://pear.lovepikachu.top/) | [官网](http://www.pearadmin.com/) | [群聊](docs/source/_static/qqgroup.jpg) | [文档](https://lab.lovepikachu.top/document/pear-admin-flask)
<p align="center">
<a href="#">
<img src="https://img.shields.io/badge/pear%20admin%20flask-1.0.0-green" alt="Pear Admin Layui Version">
<img src="https://img.shields.io/badge/pear%20admin%20flask-2.0.0-green" alt="Pear Admin Layui Version">
</a>
<a href="#">
<img src="https://img.shields.io/badge/Python-3.6+-green.svg" alt="Python Version">
<img src="https://img.shields.io/badge/Python-3.8+-green.svg" alt="Python Version">
</a>
<a href="#">
<img src="https://img.shields.io/badge/Mysql-5.3.2+-green.svg" alt="Mysql Version">
@@ -26,111 +24,213 @@
</div>
<div align="center">
<img width="92%" style="border-radius:10px;margin-top:20px;margin-bottom:20px;box-shadow: 2px 0 6px gray;" src="https://images.gitee.com/uploads/images/2020/1019/104805_042b888c_4835367.png" />
<img width="92%" style="border-radius:10px;margin-top:20px;margin-bottom:20px;box-shadow: 2px 0 6px gray;" src="docs/source/_static/feature.png" />
</div>
## 项目简介
Pear Admin Flask 基于 Flask 生态的后台管理系统,该项目旨在为 python 开发者提供一个快速开发前后端半分离的后台管理系统的模板
# 项目简介
在使用本项目之前最好掌握以下知识点
> **⚠️注意** Pear Admin Flask 已同步主项目 Pear Admin Layui 4.0,部分页面变化较大,如果是从旧的框架迁移,请查阅 [从旧项目迁移](https://lab.lovepikachu.top/document/pear-admin-flask/welcome/migration.html) 章节。另外,可以在此查看 [更新日志](https://lab.lovepikachu.top/document/pear-admin-flask/welcome/update.html#v2-0-0-4-0-5) 。
+ [html、css、JavaScript](https://developer.mozilla.org/zh-CN/docs/Learn/HTML)
+ [jQuery](https://www.w3school.com.cn/jquery/index.asp)
+ [layui](https://www.layui.com/)
+ [flask](https://dormousehole.readthedocs.io/en/latest/)
+ [flask-login](https://flask-login.readthedocs.io/en/latest/)
+ [flask-sqlalchemy](http://www.pythondoc.com/flask-sqlalchemy/quickstart.html)
Pear Admin Flask 基于 Flask 的后台管理系统,拥抱应用广泛的python语言,通过使用本系统,即可快速构建你的功能业务
项目旨在为 python 开发者提供一个后台管理系统的模板,可以快速构建信息管理系统。
项目使用 flask-sqlalchemy + 权限验证 + marshmallow 序列化与数据验证,以此方式集成了若干不同的功能。
## 预览
Pear Admin Flask 有以下几个版本:
[master分支版本 ](https://gitee.com/pear-admin/pear-admin-flask/tree/master/)
>flask 2.x + flask-sqlalchemy + 权限验证 + Flask-APScheduler 定时任务 + marshmallow 序列化与数据验证
[Mini 分支版本 ](https://gitee.com/pear-admin/pear-admin-flask/tree/mini/)
>flask 2.x + flask-sqlalchemy + 基于角色的权限管理
| | |
|---------------------|---------------------|
| ![](docs/assets/1.jpg) | ![](docs/assets/2.jpg) |
| ![](docs/assets/3.jpg)| ![](docs/assets/4.jpg) |
| ![](docs/assets/5.jpg) | ![](docs/assets/6.jpg) |
## 内置功能
# 内置功能
- [x] 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
- [x] 权限管理:配置系统菜单,操作权限,按钮权限标识等。
- [x] 角色管理:角色菜单权限分配。
- [x] 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
- [x] 登录日志:系统登录日志记录查询包含登录异常。
- [x] 文件上传: 图片上传示例
- [x] 服务监控:监视当前系统CPU、内存、磁盘、python版本,运行时长等相关信息。
- [x] 文件上传: 图片上传示例
# 项目分支说明
> **⚠️注意** Pear Admin Flask 不仅仅只提供一种对于 Pear Admin 后端的实现方式,所以提供了不同的分支版本,不同分支版本各有其优劣,并且由不同的开发者维护。
| 分支名称 | 特点 |
|------------------------------------------------------------------|------------------------|
| master(您目前浏览的分支版本) | 功能齐全,处于开发阶段,代码量较大。 |
| [main](https://gitee.com/pear-admin/pear-admin-flask/tree/main/) | 功能精简,代码量小,处于开发阶段,易于维护。 |
| [mini](https://gitee.com/pear-admin/pear-admin-flask/tree/mini/) | 不再更新,是最初版本的镜像。 |
## 文档
正在编写
> **⚠️注意** 由于 master 分支项目需要,暂时移除了 Flask-APScheduler 定时任务 功能。
# 版本支持情况
经过测试,此项目的(master分支)运行要求是 `>= Python 3.8` ,推荐使用 `Python 3.11`
> **💡提示** 由于 Flask 中使用的 Werkzeug 模块更新,Flask 官方并未进行更新,所以可能会出现 ImportError 。
> 此类情况的出现可以通过正确安装 `requirements.txt` 中的模块(以及其对应版本)解决。
# 项目结构
## 应用结构
```应用结构
Pear Admin Flask (master)
├─applications # 项目核心模块
│ ├─common # 公共模块(初始化数据库、公用函数)
│ ├─extensions # 注册项目插件
│ ├─schemas # 序列化模型
│ ├─models # 数据库模型
│ ├─views # 视图部分
│ ├─config.py # 项目配置
│ └─__init__.py # 项目初始化入口
├─docs # 文档说明
├─static # 静态资源文件
├─templates # 静态模板文件
└─app.py # 程序入口
```
## 资源结构
```资源结构
Pear Admin Flask
├─static # 项目设定的 Flask 资源文件夹
│ ├─admin # pear admin flask 的后端资源文件(与 pear admin layui 同步)
│ ├─index # pear admin flask 的前端资源文件
│ └─upload # 用户上传保存目录
└─templates # 项目设定的 Flask 模板文件夹
├─admin # pear admin flask 的后端管理页面模板
│ ├─admin_log # 日志页面
│ ├─common # 基本模板页面(头部模板与页脚模板)
│ ├─console # 系统监控页面模板
│ ├─dept # 部门管理页面模板
│ ├─dict # 数据字典页面模板
│ ├─mail # 邮件管理页面模板
│ ├─photo # 图片上传页面模板
│ ├─power # 权限(菜单)管理页面模板
│ ├─role # 角色管理页面模板
│ ├─task # 任务设置页面模板
│ └─user # 用户管理页面模板
├─errors # 错误页面模板
└─index # 主页模板
```
# 项目安装
## 从仓库获取
```bash
# 克隆仓库 / 手动下载
git clone https://gitee.com/pear-admin/pear-admin-flask
cd pear-admin-flask # 进入到项目目录
```
## 修改配置
> **💡提示** 配置文件位于 `applications/config.py` ,打开配置文件看到的是位于 `BaseConfig` 类下的默认配置文件,您可以编写自己的配置类并继承 `BaseConfig` 类。
项目启动时,会调用 `applications/__init__.py` ,这个文件中加载了程序的配置,所以在您编写了自己的类后不要忘记在文件 `applications/__init__.py` 中修改使用的配置类。
> **⚠️注意** 配置文件中对于数据库的配置有所更改,请查看代码中的注释修改配置。
```python
# 部分配置信息如下所示
# 验证密钥(⚠️ 一定要记得修改 ⚠️)
SECRET_KEY = "pear-system-flask"
# 数据库的配置信息
SQLALCHEMY_DATABASE_URI = 'sqlite:///../pear.db'
# 默认日志等级
LOG_LEVEL = logging.WARN
# flask-mail配置
MAIL_SERVER = 'smtp.qq.com'
MAIL_USE_TLS = False
MAIL_USE_SSL = True
MAIL_PORT = 465
MAIL_USERNAME = '123@qq.com'
MAIL_PASSWORD = 'XXXXX' # 生成的授权码
MAIL_DEFAULT_SENDER = MAIL_USERNAME
```
## 安装使用
## 虚拟环境安装项目(推荐)
> **💡提示** 为了保证项目所依赖的库不影响其他部署在同一主机上的项目,我们推荐使用虚拟环境安装。
```bash
python -m venv venv
# 进入虚拟环境下
venv\Scripts\activate.bat # Windows 提示命令符
venv\Scripts\Activate.ps1 # Windows Powershell
source venv/bin/activate # Linux
# 使用 pip 安装
pip install -r requirements.txt
```
## 直接安装项目
```bash
# 使用 pip 安装
pip install -r requirements.txt
# 同时你可以选择以模块的方式调用 pip
python -m pip install -r requirements.txt
```
# 运行项目
+ 一般情况运行项目
```bash
# 初始化数据库
flask db init
flask db migrate
flask db upgrade
flask admin init
# 运行项目
flask --app app.py run -h 0.0.0.0 -p 8000 --debug
# 或者直接调用 app.py
python app.py
```
+ 使用 docker-compose 运行项目
+ 下载源码
```bash
git clone https://gitee.com/pear-admin/pear-admin-flask
# 切换分支
git checkout mini
# 安装 docker-compose
curl -L https://github.com/docker/compose/releases/download/1.26.2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
# 运行如下命令,有输出版本,表示 docker-compose 可以用了
docker-compose --version
# 在当前目录执行如下命令即可以运行 app
docker-compose -f dockercompose.yaml up
# 看到如下表示运行成功,由于 pip 下载慢,需要一些时间,请耐心等待;如果安装失败,重新执行上面的命令即可。
# 运行后在浏览器访问 127.0.0.1:5000
#如果要停止容器运行,在当前文件夹执行如下命令:
docker-compose -f dockercompose.yaml down
```
+ 安装依赖
```bash
# 创建虚拟环境
python -m venv venv
# 然后使虚拟环境生效(windows
venv\Scripts\activate
# source venv/bin/activate # Linux激活虚拟环境)
# 预览项目
# 安装依赖
pip install -r requirement.txt
```
| | |
| ---------------------- | ---------------------- |
| ![](docs/source/_static/1.jpg) | ![](docs/source/_static/2.jpg) |
| ![](docs/source/_static/3.jpg) | ![](docs/source/_static/6.jpg) |
+ 数据迁移
默认的使用 `sqlite3` 作为测试环境的数据库进行演示,不需要按照mysql即可查看演示。如果需要二次开发,建议改成 `mysql`
# 其他说明
如果需要在开发环境使用 mysql 作为数据库,请查看 `applications/configs/config.py` 文件里面的相关配置文件, 注释掉 sqlite 的配置即可
## 项目初始用户以及其密码
如果需要修改数据的配置信息,请在 `.flaskenv` 里面调整即可
默认用户为 `admin` ,密码默认为 `123456`
```bash
flask db init
flask db migrate -m '数据初始化'
flask db upgrade
flask init-db
```
+ 项目启动
```bash
flask run
```
## Git 贡献提交规范
- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
- `feat` 增加新功能
- `fix` 修复问题/BUG
- `style` 代码风格相关无影响运行结果的
- `perf` 优化/性能提升
- `refactor` 重构
- `revert` 撤销修改
- `test` 测试相关
- `docs` 文档/注释
- `chore` 依赖更新/脚手架配置修改等
- `workflow` 工作流改进
- `ci` 持续集成
- `types` 类型定义文件更改
- `wip` 开发中
+6
View File
@@ -0,0 +1,6 @@
from applications import create_app
app = create_app()
if __name__ == '__main__':
app.run()
+14 -50
View File
@@ -1,60 +1,24 @@
import os
from flask import Flask, Blueprint
import config
from applications.view import init_view
from common import register_api
from common.flask_uploads import configure_uploads
from common.utils.upload import photos
from extensions import init_plugs
api_bp: Blueprint = Blueprint('api', __name__, url_prefix='/api/v1')
from .rights import register_rights_api
from .system import register_sys_api
from .users import register_users_api
register_rights_api(api_bp)
register_users_api(api_bp)
register_sys_api(api_bp)
from flask import Flask
from applications.common.script import init_script
from applications.config import BaseConfig
from applications.extensions import init_plugs
from applications.view import init_bps
def init_api(app: Flask) -> None:
app.register_blueprint(api_bp)
def create_app():
app = Flask(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
# 引入配置
app.config.from_object(BaseConfig)
def create_app() -> Flask:
app = Flask('pear-admin-flask')
# 引入数据库配置
app.config.from_object(config)
# 注册各种插件
# 注册flask组件
init_plugs(app)
# 注册路由
init_view(app)
# 注册蓝图
init_bps(app)
# 注册接口(restful api
init_api(app)
# 文件上传
configure_uploads(app, photos)
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
logo()
# 注册命令
init_script(app)
return app
def logo():
print('''
_____ _ _ ______ _ _
| __ \ /\ | | (_) | ____| | | |
| |__) |__ __ _ _ __ / \ __| |_ __ ___ _ _ __ | |__ | | __ _ ___| | __
| ___/ _ \/ _` | '__| / /\ \ / _` | '_ ` _ \| | '_ \ | __| | |/ _` / __| |/ /
| | | __/ (_| | | / ____ \ (_| | | | | | | | | | | | | | | (_| \__ \ <
|_| \___|\__,_|_| /_/ \_\__,_|_| |_| |_|_|_| |_| |_| |_|\__,_|___/_|\_\\
''')
+96
View File
@@ -0,0 +1,96 @@
from io import BytesIO
from flask import make_response
from flask_login import current_user
from applications.common.utils.validate import str_escape
from applications.common.utils.captcha import vieCode
from applications.extensions import db
from applications.models import AdminLog
def get_captcha():
"""
生成验证码图片及其对应的验证码字符串。
:return: 返回验证码图片的响应对象和验证码字符串。
"""
image, code = vieCode().GetCodeImage()
code = ''.join(code).lower()
out = BytesIO()
image.save(out, 'png')
out.seek(0)
resp = make_response(out.read())
resp.content_type = 'image/png'
return resp, code
def normal_log(method, url, ip, user_agent, desc, uid, is_access):
"""
记录通用日志信息到数据库。
:param method: 请求方法(如 GET、POST)。
:param url: 请求的 URL。
:param ip: 客户端的 IP 地址。
:param user_agent: 客户端的 User-Agent 信息。
:param desc: 日志描述信息。
:param uid: 用户 ID。
:param is_access: 是否成功访问(True 或 False)。
:return: 返回日志记录的 ID。
"""
info = {
'method': method,
'url': url,
'ip': ip,
'user_agent': user_agent,
'desc': desc,
'uid': uid,
'success': int(is_access)
}
log = AdminLog(
url=info.get('url'),
ip=info.get('ip'),
user_agent=info.get('user_agent'),
desc=info.get('desc'),
uid=info.get('uid'),
method=info.get('method'),
success=info.get('success')
)
db.session.add(log)
db.session.commit()
return log.id
def login_log(request, uid, is_access):
"""
记录用户登录日志。
:param request: Flask 请求对象。
:param uid: 用户 ID。
:param is_access: 是否成功登录(True 或 False)。
:return: 返回日志记录的 ID。
"""
method = request.method
url = request.path
ip = request.remote_addr
user_agent = str_escape(request.headers.get('User-Agent'))
desc = str_escape(request.form.get('username'))
return normal_log(method, url, ip, user_agent, desc, uid, is_access)
def admin_log(request, is_access, desc=None):
"""
记录管理员操作日志。
:param request: Flask 请求对象。
:param is_access: 是否成功操作(True 或 False)。
:param desc: 日志描述信息(可选)。如果未提供,则从请求数据中提取。
:return: 返回日志记录的 ID。
"""
method = request.method
url = request.path
ip = request.remote_addr
user_agent = str_escape(request.headers.get('User-Agent'))
request_data = request.json if request.headers.get('Content-Type') == 'application/json' else request.values
if desc is None:
desc = str_escape(str(dict(request_data)))
return normal_log(method, url, ip, user_agent, desc, current_user.id, is_access)
+119
View File
@@ -0,0 +1,119 @@
import datetime
from marshmallow import Schema
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
from applications.extensions import db, ma
class LogicalDeleteMixin(object):
"""
逻辑删除混入类,为模型提供软删除功能。
示例:
class Test(db.Model, LogicalDeleteMixin):
__tablename__ = 'admin_test'
id = db.Column(db.Integer, primary_key=True, comment='角色ID')
# 软删除
Test.query.filter_by(id=1).soft_delete()
# 查询所有未删除的记录
Test.query.logic_all()
"""
create_at = db.Column(db.DateTime, default=datetime.datetime.now, comment='创建时间')
update_at = db.Column(db.DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment='更新时间')
delete_at = db.Column(db.DateTime, comment='删除时间')
def auto_model_jsonify(data, model: db.Model):
"""
自动序列化模型数据为 JSON 格式,无需手动定义 Schema。
示例:
power_data = curd.auto_model_jsonify(model=Dept, data=dept)
:param data: 需要序列化的 SQLAlchemy 查询结果。
:param model: SQLAlchemy 模型类。
:return: 返回序列化后的 JSON 数据。
"""
def get_model():
return model
class AutoSchema(SQLAlchemyAutoSchema):
class Meta(Schema):
model = get_model()
include_fk = True # 包含外键
include_relationships = True # 包含关联关系
load_instance = True # 反序列化时加载为模型实例
common_schema = AutoSchema(many=True) # 支持序列化多个对象
output = common_schema.dump(data)
return output
def model_to_dicts(schema: ma.Schema, data):
"""
使用指定的 Schema 序列化 SQLAlchemy 查询结果。
:param schema: Marshmallow Schema 类。
:param data: SQLAlchemy 查询结果。
:return: 返回序列化后的数据,返回字典。
"""
common_schema = schema(many=True) # 支持序列化多个对象
output = common_schema.dump(data)
return output
def get_one_by_id(model: db.Model, id):
"""
根据 ID 查询单个记录。
:param model: SQLAlchemy 模型类。
:param id: 记录的主键 ID。
:return: 返回查询到的记录,如果未找到则返回 None。
"""
return model.query.filter_by(id=id).first()
def delete_one_by_id(model: db.Model, id):
"""
根据 ID 删除单个记录。
:param model: SQLAlchemy 模型类。
:param id: 记录的主键 ID。
:return: 返回删除操作影响的行数。
"""
r = model.query.filter_by(id=id).delete()
db.session.commit()
return r
def enable_status(model: db.Model, id):
"""
启用指定 ID 的记录。
:param model: SQLAlchemy 模型类。
:param id: 记录的主键 ID。
:return: 如果操作成功返回 True,否则返回 False。
"""
enable = 1
role = model.query.filter_by(id=id).update({"enable": enable})
if role:
db.session.commit()
return True
return False
def disable_status(model: db.Model, id):
"""
停用指定 ID 的记录。
:param model: SQLAlchemy 模型类。
:param id: 记录的主键 ID。
:return: 如果操作成功返回 True,否则返回 False。
"""
enable = 0
role = model.query.filter_by(id=id).update({"enable": enable})
if role:
db.session.commit()
return True
return False
+173
View File
@@ -0,0 +1,173 @@
from sqlalchemy import and_, func
from applications.extensions import db
class ModelFilter:
"""
ORM 多条件查询构造器,支持多种查询条件组合,自动转义特殊字符防止SQL注入。
示例:
mf = ModelFilter()
mf.exact('name', 'John')
mf.vague('email', 'example.com')
query = User.query.filter(mf.get_filter(User))
"""
filter_field = {} # 存储字段过滤条件
filter_list = [] # 存储最终的过滤条件列表
# 查询类型常量
type_exact = "exact" # 精确匹配
type_neq = "neq" # 不等于
type_greater = "greater" # 大于
type_less = "less" # 小于
type_vague = "vague" # 模糊匹配
type_contains = "contains" # 包含
type_between = "between" # 范围查询
def __init__(self):
"""初始化过滤条件存储字典和列表。"""
self.filter_field = {}
self.filter_list = []
@staticmethod
def escape_like(value: str, escape_char: str = '\\') -> str:
"""
转义LIKE查询中的特殊字符(%, _ 和转义字符本身)
:param value: 需要转义的原始字符串
:param escape_char: 转义字符(默认反斜杠)
:return: 转义后的安全字符串
"""
return (
value.replace(escape_char, escape_char * 2)
.replace('%', escape_char + '%')
.replace('_', escape_char + '_')
)
def exact(self, field_name, value):
"""
添加精确匹配条件(自动处理字符串类型参数)
:param field_name: 模型字段名称
:param value: 匹配的值(自动过滤空字符串)
"""
if value is not None and value != '':
# 字符串类型自动调用escape_like(防止特殊字符影响精确匹配)
processed_value = self.escape_like(str(value)) if isinstance(value, str) else value
self.filter_field[field_name] = {"data": processed_value, "type": self.type_exact}
def neq(self, field_name, value):
"""
添加不等于条件(自动处理字符串类型参数)
:param field_name: 模型字段名称
:param value: 不匹配的值(自动过滤空字符串)
"""
if value is not None and value != '':
# 字符串类型自动调用escape_like
processed_value = self.escape_like(str(value)) if isinstance(value, str) else value
self.filter_field[field_name] = {"data": processed_value, "type": self.type_neq}
def greater(self, field_name, value):
"""
添加大于条件(数值/日期比较)
:param field_name: 模型字段名称
:param value: 比较的数值/日期
"""
if value is not None and value != '':
self.filter_field[field_name] = {"data": value, "type": self.type_greater}
def less(self, field_name, value):
"""
添加小于条件(数值/日期比较)
:param field_name: 模型字段名称
:param value: 比较的数值/日期
"""
if value is not None and value != '':
self.filter_field[field_name] = {"data": value, "type": self.type_less}
def vague(self, field_name, value: str):
"""
添加安全模糊匹配(自动转义特殊字符,左右加%
:param field_name: 模型字段名称
:param value: 需要模糊匹配的字符串(自动过滤空值)
"""
if value and value != '':
escaped_value = self.escape_like(value)
self.filter_field[field_name] = {"data": f'%{escaped_value}%', "type": self.type_vague}
def left_vague(self, field_name, value: str):
"""
添加安全左模糊匹配(自动转义特殊字符,左侧加%
:param field_name: 模型字段名称
:param value: 需要左模糊匹配的字符串
"""
if value and value != '':
escaped_value = self.escape_like(value)
self.filter_field[field_name] = {"data": f'%{escaped_value}', "type": self.type_vague}
def right_vague(self, field_name, value: str):
"""
添加安全右模糊匹配(自动转义特殊字符,右侧加%
:param field_name: 模型字段名称
:param value: 需要右模糊匹配的字符串
"""
if value and value != '':
escaped_value = self.escape_like(value)
self.filter_field[field_name] = {"data": f'{escaped_value}%', "type": self.type_vague}
def contains(self, field_name, value: str):
"""
添加安全包含条件(自动转义特殊字符,等效于vague)
:param field_name: 模型字段名称
:param value: 需要包含的字符串
"""
if value and value != '':
escaped_value = self.escape_like(value)
self.filter_field[field_name] = {"data": f'%{escaped_value}%', "type": self.type_contains}
def between(self, field_name, value1, value2):
"""
添加范围查询条件(自动过滤无效值)
:param field_name: 模型字段名称
:param value1: 范围起始值
:param value2: 范围结束值
"""
if all([v is not None and v != '' for v in [value1, value2]]):
self.filter_field[field_name] = {"data": [value1, value2], "type": self.type_between}
def get_filter(self, model: db.Model):
"""
生成安全的SQLAlchemy过滤条件
:param model: SQLAlchemy 模型类
:return: 组合后的过滤条件(使用and_连接)
"""
for k, v in self.filter_field.items():
field = getattr(model, k)
data = v.get("data")
query_type = v.get("type")
if query_type == self.type_vague:
self.filter_list.append(field.like(data, escape='\\'))
elif query_type == self.type_contains:
self.filter_list.append(field.like(data, escape='\\'))
elif query_type == self.type_exact:
self.filter_list.append(field == data)
elif query_type == self.type_neq:
self.filter_list.append(field != data)
elif query_type == self.type_greater:
self.filter_list.append(field > data)
elif query_type == self.type_less:
self.filter_list.append(field < data)
elif query_type == self.type_between:
self.filter_list.append(field.between(data[0], data[1]))
return and_(*self.filter_list)
+11
View File
@@ -0,0 +1,11 @@
from flask import Flask
from .admin import admin_cli
from .check import check_cli
from applications.extensions.init_plugins import broadcast_execute
def init_script(app: Flask):
app.cli.add_command(admin_cli)
app.cli.add_command(check_cli)
broadcast_execute(app, 'event_finish')
+247
View File
@@ -0,0 +1,247 @@
import click
import json
import datetime
import os
from flask.cli import AppGroup
from applications.extensions import db
from applications.models import User, Role, Dept, Power
# 1. 定义命令组
admin_cli = AppGroup("admin", help="系统数据管理:包括初始化、导入和导出")
# 2. 定义数据存储的文件夹名称
DATA_DIR = "app_data"
# --- 辅助工具类 ---
class DateEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime.datetime):
return obj.strftime('%Y-%m-%d %H:%M:%S')
return json.JSONEncoder.default(self, obj)
def parse_time(item):
"""辅助函数:尝试将字典中的时间字符串转换为 datetime 对象"""
for k, v in item.items():
if k in ['create_at', 'create_time', 'update_at'] and isinstance(v, str):
try:
item[k] = datetime.datetime.strptime(v, '%Y-%m-%d %H:%M:%S')
except ValueError:
pass
return item
def model_to_dict(obj):
"""将 SQLAlchemy 模型对象转换为字典"""
data = {}
for c in obj.__table__.columns:
val = getattr(obj, c.name)
data[c.name] = val
return data
def ensure_dir_exists():
"""
确保数据文件夹存在。
修改:路径基于当前脚本文件的位置,而不是执行命令的位置。
"""
# 获取当前脚本(admin_cli.py)所在的目录
current_script_dir = os.path.dirname(os.path.abspath(__file__))
# 拼接得到 app_data 的绝对路径
base_path = os.path.join(current_script_dir, DATA_DIR)
if not os.path.exists(base_path):
os.makedirs(base_path)
return base_path
# --- 核心逻辑:导入数据 ---
def import_logic(file_path, is_force):
"""具体的导入逻辑封装"""
if not os.path.exists(file_path):
click.echo(f"错误: 找不到文件 {file_path}")
return False
click.echo(f"正在读取文件: {file_path}")
with open(file_path, 'r', encoding='utf-8') as f:
raw_data = json.load(f)
if is_force:
click.echo("警告: 正在清空旧数据...")
try:
db.session.query(User).delete()
db.session.query(Role).delete()
db.session.query(Power).delete()
db.session.query(Dept).delete()
# 新增:清空 Nav 表
from applications.models import Nav
db.session.query(Nav).delete()
db.session.commit()
except Exception as e:
db.session.rollback()
click.echo(f"清空数据失败: {str(e)}")
return False
try:
# 1. 导入部门
count_dept = 0
for item in raw_data.get("depts", []):
item = parse_time(item)
if not Dept.query.get(item['id']):
db.session.add(Dept(**item))
count_dept += 1
db.session.commit()
# 2. 导入权限
count_power = 0
for item in raw_data.get("powers", []):
item = parse_time(item)
if not Power.query.get(item['id']):
db.session.add(Power(**item))
count_power += 1
db.session.commit()
# 3. 导入角色
count_role = 0
for item in raw_data.get("roles", []):
item = parse_time(item)
power_ids = item.pop('power_ids', [])
role = Role.query.get(item['id'])
if not role:
role = Role(**item)
db.session.add(role)
count_role += 1
if power_ids:
powers = Power.query.filter(Power.id.in_(power_ids)).all()
role.power = powers
db.session.commit()
# 4. 导入用户
count_user = 0
for item in raw_data.get("users", []):
item = parse_time(item)
role_ids = item.pop('role_ids', [])
user = User.query.get(item['id'])
if not user:
user = User(**item)
db.session.add(user)
count_user += 1
if role_ids:
roles = Role.query.filter(Role.id.in_(role_ids)).all()
user.role = roles
db.session.commit()
# 5. 导入公开导航(Nav 表)
from applications.models import Nav
count_nav = 0
for item in raw_data.get("navs", []):
item = parse_time(item)
if not Nav.query.get(item.get('id')) if item.get('id') else True:
db.session.add(Nav(**item))
count_nav += 1
db.session.commit()
click.echo(f"导入成功! 新增: 部门{count_dept}, 权限{count_power}, 角色{count_role}, 用户{count_user}, 导航{count_nav}")
return True
except Exception as e:
db.session.rollback()
click.echo(f"导入失败,已回滚。错误信息: {str(e)}")
return False
# --- 命令:init (系统初始化) ---
@admin_cli.command("init", help="系统初始化:读取 app_data/init.json 并导入数据库")
@click.option('--force', is_flag=True, help='是否强制清空旧数据再导入')
def init_db(force):
"""
系统初始化命令。
默认读取当前脚本同级目录下 app_data/init.json 文件。
"""
base_path = ensure_dir_exists()
target_file = os.path.join(base_path, 'init.json')
if not os.path.exists(target_file):
click.echo(f"错误:在 {base_path} 目录下未找到 init.json 文件。")
click.echo(f"请先将基础数据文件放入该位置,或运行 export 生成。")
return
import_logic(target_file, force)
# --- 命令:export (导出) ---
@admin_cli.command("export", help="导出数据:将数据库数据保存为 JSON 文件")
@click.option('--name', default=None, help='指定导出文件名,不带后缀。如果不填则使用时间戳。')
def export_data(name):
"""
导出数据到当前脚本同级目录下的 app_data 文件夹。
"""
base_path = ensure_dir_exists()
if name:
filename = f"{name}.json"
else:
now_str = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"data_{now_str}.json"
file_path = os.path.join(base_path, filename)
click.echo(f"准备导出数据到: {file_path} ...")
data = {
"depts": [],
"powers": [],
"roles": [],
"users": [],
"navs": []
}
# 1. 导出部门
data["depts"] = [model_to_dict(d) for d in Dept.query.order_by(Dept.id).all()]
# 2. 导出权限
data["powers"] = [model_to_dict(p) for p in Power.query.order_by(Power.id).all()]
# 3. 导出角色
roles = Role.query.order_by(Role.id).all()
for r in roles:
r_dict = model_to_dict(r)
r_dict['power_ids'] = [p.id for p in r.power]
data["roles"].append(r_dict)
# 4. 导出用户
users = User.query.order_by(User.id).all()
for u in users:
u_dict = model_to_dict(u)
u_dict['role_ids'] = [r.id for r in u.role]
data["users"].append(u_dict)
# 5. 导出公开导航
from applications.models import Nav
data["navs"] = [model_to_dict(n) for n in Nav.query.order_by(Nav.category, Nav.sort, Nav.id).all()]
try:
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, cls=DateEncoder, ensure_ascii=False, indent=4)
click.echo(f"导出成功!")
except Exception as e:
click.echo(f"导出失败: {str(e)}")
# --- 命令:import (指定文件导入) ---
@admin_cli.command("import", help="导入数据:从指定的 JSON 文件恢复数据")
@click.option('--file', required=True, help='要导入的文件名 (需包含路径或位于 app_data 下)')
@click.option('--force', is_flag=True, help='是否强制清空旧数据')
def import_data(file, force):
"""
从指定文件导入数据。
"""
# 如果用户只提供了文件名,没有路径,则假设在 app_data 目录下
if not os.path.dirname(file):
file_path = os.path.join(ensure_dir_exists(), file)
else:
file_path = file
import_logic(file_path, force)
File diff suppressed because it is too large Load Diff
+95
View File
@@ -0,0 +1,95 @@
import re
import inspect
from flask.cli import AppGroup
from flask import current_app
check_cli = AppGroup('check', help='Commands for checking application aspects.')
@check_cli.command('auth')
def check_auth():
"""检查路由视图函数的 @login_required 和 @authorize(...) 装饰器使用情况。"""
app = current_app
if not app:
print("错误:未在应用上下文中运行。请使用 'flask check auth' 命令。")
return
print("\n" + "="*120)
print("Flask 路由鉴权装饰器检查报告".center(120))
print("="*120)
# 调整列宽以适应更长的权限字符串
header_format = "{:<35} {:<35} {:<30} {:<20} {:<30}"
row_format = "{:<35} {:<35} {:<30} {:<20} {:<30}"
print(header_format.format("Endpoint", "URL Rule", "View Function", "@login_required", "@authorize(...)"))
print("-" * 120)
has_missing_auth = False # 标记是否有缺失鉴权的路由
with app.app_context():
for rule in app.url_map.iter_rules():
if rule.endpoint == 'static':
continue
view_func = app.view_functions.get(rule.endpoint)
if not view_func:
continue
# --- 获取源代码 ---
try:
# 使用 getsourcelines 可能更稳定,获取整个函数定义
source_lines = inspect.getsourcelines(view_func)[0]
source_code = "".join(source_lines)
except (OSError, TypeError):
# 如果无法获取源代码(如 C 扩展),则跳过
source_code = ""
# --- 检查 @login_required 装饰器 ---
# 匹配常见的形式,包括可能的模块前缀
has_login_required = bool(re.search(r"@.*login_required", source_code))
# --- 检查 @authorize 装饰器并提取权限 ---
authorize_permissions_list = []
# 改进的正则表达式:
# @ : 匹配 @ 符号
# (?:.*?\.)? : 非捕获组,匹配可能的模块名和点 (例如 auth.)
# authorize : 匹配 authorize 函数名
# \( : 匹配左圆括号
# ([^)]*) : 捕获组,匹配括号内的所有内容 (非右括号字符)
# \) : 匹配右圆括号
# 这个模式会找到 @authorize(...) 的整个调用
authorize_calls = re.findall(r"@.*?authorize\s*\(([^)]*)\)", source_code, re.DOTALL)
for call_args in authorize_calls:
# 在找到的参数字符串中,再次使用正则提取被引号包围的权限字符串
# 匹配单引号或双引号内的内容
permissions_found = re.findall(r"['\"]([^'\"]+)['\"]", call_args)
authorize_permissions_list.extend(permissions_found)
# 将找到的所有权限字符串用逗号连接
authorize_permissions = ", ".join(authorize_permissions_list) if authorize_permissions_list else "N/A"
# --- 判断是否可能缺失鉴权 ---
# 简单判断:如果既没有 login_required 也没有 authorize 权限,则标记
if not has_login_required and not authorize_permissions_list:
has_missing_auth = True
# --- 格式化输出 ---
login_status = "✅ Yes" if has_login_required else "❌ No"
auth_status = authorize_permissions if authorize_permissions != "N/A" else "❌ N/A"
# 格式化字符串长度
def truncate(s, length):
return s if len(s) <= length else s[:length-2] + ".."
endpoint_str = truncate(rule.endpoint, 34)
rule_str = truncate(str(rule), 34)
func_name_str = truncate(view_func.__name__, 29)
print(row_format.format(endpoint_str, rule_str, func_name_str, login_status, auth_status))
print("-" * 120)
if has_missing_auth:
print("\n⚠️ 注意:以上标记为 '❌ No''❌ N/A' 的路由可能缺少鉴权,请仔细检查!")
else:
print("\n✅ 所有路由似乎都应用了至少一种鉴权机制。")
print("="*120 + "\n")
+55
View File
@@ -0,0 +1,55 @@
import time
cache_dict = {}
def cache_set_internal(key, value, expired=5):
"""
程序内部实现的记录缓存,用于简单、体量不大的缓存记录,在程序结束后销毁。对于高速、体量大的环境请配置 Redis 等服务自行记录。
记录缓存,存储键值对,并记录当前时间作为缓存的时间戳。
:param key: 键
:param value: 值
:param expired: 过期时间(秒),默认5秒
"""
cache_dict[key] = {
'value': value,
'expired_time': time.time() + expired
}
def cache_get_internal(key):
"""
获取缓存,根据键从缓存中获取值,并检查是否过期。
:param key: 键
:return: 如果缓存存在且未过期,返回缓存的值;否则返回 None
"""
if key in cache_dict:
cache_item = cache_dict[key]
if time.time() < cache_item['expired_time']:
return cache_item['value']
else:
# 如果缓存已过期,删除该缓存
del cache_dict[key]
return None
def cache_auto_internal(key, call, expired=5):
"""
如果缓存存在直接返回缓存内容,缓存不存在或者过期执行 call 函数,并取得返回值记录并返回。
:param key: 键
:param call: 获取新值的地方
:param expired: 过期时间(秒),默认5秒
"""
data = cache_get_internal(key)
if data is not None:
return data
data = call()
cache_set_internal(key, data, expired)
return data
+135
View File
@@ -0,0 +1,135 @@
import random, math
from PIL import Image, ImageDraw, ImageFont, ImageFilter
class vieCode:
__fontSize = 20 # 字体大小
__width = 120 # 画布宽度
__heigth = 45 # 画布高度
__length = 4 # 验证码长度
__draw = None # 画布对象
__img = None # 图片对象
__code = None # 验证码字符
__str = None # 自定义验证码字符集
__inCurve = True # 是否绘制干扰曲线
__inNoise = True # 是否绘制干扰点
__type = 2 # 验证码类型:1-纯字母,2-数字字母混合
__fontPatn = 'applications/common/utils/fonts/captcha.ttf' # 字体路径
def GetCodeImage(self, size=80, length=4):
"""
生成验证码图片及其对应的验证码字符。
:param size: 验证码字体大小,默认为 80。
:param length: 验证码字符长度,默认为 4。
:return: 返回验证码图片对象和验证码字符。
"""
# 准备基础数据
self.__length = length
self.__fontSize = size
self.__width = self.__fontSize * self.__length
self.__heigth = int(self.__fontSize * 1.5)
# 生成验证码图片
self.__createCode()
self.__createImage()
self.__createNoise()
self.__printString()
self.__cerateFilter()
return self.__img, self.__code
def __cerateFilter(self):
"""
对验证码图片进行模糊处理,增加识别难度。
"""
self.__img = self.__img.filter(ImageFilter.BLUR)
filter = ImageFilter.ModeFilter(8)
self.__img = self.__img.filter(filter)
def __createCode(self):
"""
生成验证码字符。
"""
# 是否使用自定义字符集
if not self.__str:
# 源文本
number = "3456789"
srcLetter = "qwertyuipasdfghjkzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM"
srcUpper = srcLetter.upper()
if self.__type == 1:
self.__str = number
else:
self.__str = srcLetter + srcUpper + number
# 随机生成验证码字符
self.__code = random.sample(self.__str, self.__length)
def __createImage(self):
"""
创建画布并设置背景颜色。
"""
bgColor = (random.randint(200, 255), random.randint(200, 255), random.randint(200, 255))
self.__img = Image.new('RGB', (self.__width, self.__heigth), bgColor)
self.__draw = ImageDraw.Draw(self.__img)
def __createNoise(self):
"""
在验证码图片上绘制干扰点。
"""
if not self.__inNoise:
return
font = ImageFont.truetype(self.__fontPatn, int(self.__fontSize / 1.5))
for i in range(5):
# 干扰点颜色
noiseColor = (random.randint(150, 200), random.randint(150, 200), random.randint(150, 200))
putStr = random.sample(self.__str, 2)
for j in range(2):
# 绘制干扰点
size = (random.randint(-10, self.__width), random.randint(-10, self.__heigth))
self.__draw.text(size, putStr[j], font=font, fill=noiseColor)
def __createCurve(self):
"""
在验证码图片上绘制干扰曲线。
"""
if not self.__inCurve:
return
x = y = 0
# 计算曲线系数
a = random.uniform(1, self.__heigth / 2)
b = random.uniform(-self.__width / 4, self.__heigth / 4)
f = random.uniform(-self.__heigth / 4, self.__heigth / 4)
t = random.uniform(self.__heigth, self.__width * 2)
xend = random.randint(self.__width / 2, self.__width * 2)
w = (2 * math.pi) / t
# 绘制曲线
color = (random.randint(30, 150), random.randint(30, 150), random.randint(30, 150))
for x in range(xend):
if w != 0:
for k in range(int(self.__heigth / 10)):
y = a * math.sin(w * x + f) + b + self.__heigth / 2
i = int(self.__fontSize / 5)
while i > 0:
px = x + i
py = y + i + k
self.__draw.point((px, py), color)
i -= i
def __printString(self):
"""
在画布上打印验证码字符。
"""
font = ImageFont.truetype(self.__fontPatn, self.__fontSize)
x = 0
# 打印字符到画布
for i in range(self.__length):
# 设置字体随机颜色
color = (random.randint(30, 150), random.randint(30, 150), random.randint(30, 150))
# 计算坐标
x = random.uniform(self.__fontSize * i * 0.95, self.__fontSize * i * 1.1)
y = self.__fontSize * random.uniform(0.3, 0.5)
# 打印字符
self.__draw.text((x, y), self.__code[i], font=font, fill=color)
Binary file not shown.
+41
View File
@@ -0,0 +1,41 @@
from flask import jsonify, request
def success_api(msg: str = "成功"):
"""
返回成功的 API 响应。
:param msg: 成功消息内容,默认为 "成功"
:return: 返回 JSON 格式的响应,包含 `success` 和 `msg` 字段。
"""
return jsonify(success=True, msg=msg)
def fail_api(msg: str = "失败"):
"""
返回失败的 API 响应。
:param msg: 失败消息内容,默认为 "失败"
:return: 返回 JSON 格式的响应,包含 `success` 和 `msg` 字段。
"""
return jsonify(success=False, msg=msg)
def table_api(msg: str = "", count=0, data=None, limit=10):
"""
返回动态表格渲染所需的 API 响应。
:param msg: 响应消息内容,默认为空字符串。
:param count: 数据总数,默认为 0。
:param data: 表格数据,默认为 None。
:param limit: 每页数据条数,默认为 10。
:return: 返回 JSON 格式的响应,包含 `msg`、`code`、`data`、`count` 和 `limit` 字段。
"""
res = {
'msg': msg,
'code': 0,
'data': data,
'count': count,
'limit': min(request.args.get('limit', default=10, type=int), 90)
}
return jsonify(res)
+93
View File
@@ -0,0 +1,93 @@
"""
集成了对 Pear Admin Flask 二次开发的邮件操作模块,并提供了相应的示例。
"""
from flask import current_app
from flask_mail import Message
from applications.common.curd import model_to_dicts
from applications.common.helper import ModelFilter
from applications.extensions import db, flask_mail
from applications.models import Mail
from applications.schemas import MailOutSchema
def get_all(receiver=None, subject=None, content=None):
"""
获取邮件列表,支持根据接收者、主题和内容进行筛选。
返回的列表中的字典结构如下::
{
"content": "", # HTML 内容
"create_at": "2022-12-25T10:51:17", # 创建时间
"id": 17, # 邮件ID
"realname": "超级管理", # 创建者姓名
"receiver": "", # 接收者
"subject": "" # 邮件主题
}
:param receiver: 接收者邮箱地址,支持模糊查询。
:param subject: 邮件主题,支持模糊查询。
:param content: 邮件内容,支持模糊查询。
:return: 返回符合条件的邮件列表。
"""
# 构造查询条件
mf = ModelFilter()
if receiver:
mf.contains(field_name="receiver", value=receiver)
if subject:
mf.contains(field_name="subject", value=subject)
if content:
mf.exact(field_name="content", value=content)
# 查询邮件数据并分页
mail = Mail.query.filter(mf.get_filter(Mail)).layui_paginate()
return model_to_dicts(schema=MailOutSchema, data=mail.items)
def add(receiver, subject, content, user_id):
"""
发送一封邮件,并将发送记录保存到数据库。 **该方法被邮件发送的视图函数调用。**
:param receiver: 接收者邮箱地址,多个邮箱用英文分号隔开。
:param subject: 邮件主题。
:param content: 邮件内容(HTML 格式)。
:param user_id: 发送者用户ID,表示谁发送了这封邮件。
可以使用 `from flask_login import current_user; current_user.id` 获取当前登录用户的ID。
:return: 发送成功返回 True,失败报错。
"""
send_mail(subject=subject, recipients=receiver.split(";"), content=content)
# 保存邮件记录到数据库
mail = Mail(receiver=receiver, subject=subject, content=content, user_id=user_id)
db.session.add(mail)
db.session.commit()
return True
def delete(id):
"""
删除指定的邮件记录。
:param id: 邮件ID。
:return: 删除成功返回 True,失败返回 False。
"""
res = Mail.query.filter_by(id=id).delete()
if not res:
return False
db.session.commit()
return True
def send_mail(subject, recipients, content):
"""
发送邮件(不记录发送日志)。
注意:如果发送失败会抛出异常,请使用 try-except 进行捕获。
:param subject: 邮件主题。
:param recipients: 接收者邮箱地址,多个邮箱用英文分号隔开。
:param content: 邮件内容(HTML 格式)。
"""
message = Message(subject=subject, recipients=recipients, html=content)
flask_mail.send(message)
+44
View File
@@ -0,0 +1,44 @@
from functools import wraps
from flask import abort, request, jsonify, session, current_app
from flask_login import login_required, current_user
from applications.common.admin import admin_log
def authorize(power: str, log: bool = False):
"""
用户权限判断,用于判断目前会话用户是否拥有访问权限。
在模板中有与之对应的全局非修饰函数 authorize ,此函数定义位于 `applications/extensions/init_template_directives.py` 。
:param power: 权限标识
:type power: str
:param log: 是否记录日志, defaults to False
:type log: bool, optional
"""
def decorator(func):
@login_required
@wraps(func)
def wrapper(*args, **kwargs):
# 定义管理员的id为1
if current_user.username == current_app.config.get("SUPERADMIN"):
if log:
admin_log(request=request, is_access=True)
return func(*args, **kwargs)
if not power in session.get('permissions'):
if log:
admin_log(request=request, is_access=False)
if request.method == 'GET':
abort(403)
else:
return jsonify(success=False, msg="权限不足!")
if log:
admin_log(request=request, is_access=True)
return func(*args, **kwargs)
return wrapper
return decorator
+36
View File
@@ -0,0 +1,36 @@
import os
from flask import current_app
from sqlalchemy import desc
from applications.extensions import db
from applications.extensions.init_upload import photos
from applications.models import Photo
from applications.schemas import PhotoOutSchema
from applications.common.curd import model_to_dicts
def get_photo(page, limit):
photo = Photo.query.order_by(desc(Photo.create_time)).paginate(page=page, per_page=limit, error_out=False)
count = Photo.query.count()
data = model_to_dicts(schema=PhotoOutSchema, data=photo.items)
return data, count
def upload_one(photo, mime):
filename = photos.save(photo)
file_url = '/_uploads/photos/' + filename
# file_url = photos.url(filename)
upload_url = current_app.config.get("UPLOADED_PHOTOS_DEST")
size = os.path.getsize(upload_url + '/' + filename)
photo = Photo(name=filename, href=file_url, mime=mime, size=size)
db.session.add(photo)
db.session.commit()
return file_url
def delete_photo_by_id(_id):
photo_name = Photo.query.filter_by(id=_id).first().name
photo = Photo.query.filter_by(id=_id).delete()
db.session.commit()
upload_url = current_app.config.get("UPLOADED_PHOTOS_DEST")
os.remove(upload_url + '/' + photo_name)
return photo
+240
View File
@@ -0,0 +1,240 @@
# XSS 过滤
import validators
from markupsafe import escape
from validators import validator
def str_escape(s):
"""
对字符串进行 XSS 过滤,返回转义后的安全字符串。
:param s: 需要转义的字符串。
:return: 返回转义后的字符串,如果输入为空则返回 None。
"""
if not s:
return None
return str(escape(s))
def between(*args, **kwargs):
"""
验证数字是否介于最小值和最大值之间。
适用于整数、浮点数、小数和日期等类型。
:param value: 需要验证的数字。
:param min: 数字的最小值(可选)。
:param max: 数字的最大值(可选)。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
示例:
>>> between(5, min=2)
True
>>> between(13.2, min=13, max=14)
True
>>> between(500, max=400)
ValidationFailure(func=between, args=...)
"""
return validators.between(*args, **kwargs)
def domain(*args, **kwargs):
"""
验证给定值是否为有效的域名。
:param value: 需要验证的域名字符串。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
示例:
>>> domain('example.com')
True
>>> domain('example.com/')
ValidationFailure(func=domain, ...)
"""
return validators.domain(*args, **kwargs)
def email(*args, **kwargs):
"""
验证给定值是否为有效的电子邮件地址。
:param value: 需要验证的电子邮件地址。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
示例:
>>> email('someone@example.com')
True
>>> email('bogus@@')
ValidationFailure(func=email, ...)
"""
return validators.email(*args, **kwargs)
def iban(*args, **kwargs):
"""
验证给定值是否为有效的 IBAN 代码。
:param value: 需要验证的 IBAN 代码。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
示例:
>>> iban('DE29100500001061045672')
True
>>> iban('123456')
ValidationFailure(func=iban, ...)
"""
return validators.iban(*args, **kwargs)
def ipv4(*args, **kwargs):
"""
验证给定值是否为有效的 IPv4 地址。
:param value: 需要验证的 IPv4 地址。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
示例:
>>> ipv4('123.0.0.7')
True
>>> ipv4('900.80.70.11')
ValidationFailure(func=ipv4, args={'value': '900.80.70.11'})
"""
return validators.ipv4(*args, **kwargs)
def ipv6(*args, **kwargs):
"""
验证给定值是否为有效的 IPv6 地址。
:param value: 需要验证的 IPv6 地址。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
示例:
>>> ipv6('abcd:ef::42:1')
True
>>> ipv6('abc.0.0.1')
ValidationFailure(func=ipv6, args={'value': 'abc.0.0.1'})
"""
return validators.ipv6(*args, **kwargs)
def length(*args, **kwargs):
"""
验证给定字符串的长度是否在指定范围内。
:param value: 需要验证的字符串。
:param min: 字符串的最小长度(可选)。
:param max: 字符串的最大长度(可选)。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
示例:
>>> length('something', min=2)
True
>>> length('something', min=9, max=9)
True
>>> length('something', max=5)
ValidationFailure(func=length, ...)
"""
return validators.length(*args, **kwargs)
def mac_address(*args, **kwargs):
"""
验证给定值是否为有效的 MAC 地址。
:param value: 需要验证的 MAC 地址。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
示例:
>>> mac_address('01:23:45:67:ab:CD')
True
>>> mac_address('00:00:00:00:00')
ValidationFailure(func=mac_address, args={'value': '00:00:00:00:00'})
"""
return validators.mac_address(*args, **kwargs)
def slug(*args, **kwargs):
"""
验证给定值是否为有效的 Slug 格式。
有效的 Slug 只能包含字母数字字符、连字符和下划线。
:param value: 需要验证的字符串。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
示例:
>>> slug('my.slug')
ValidationFailure(func=slug, args={'value': 'my.slug'})
>>> slug('my-slug-2134')
True
"""
return validators.slug(*args, **kwargs)
def url(*args, **kwargs):
"""
验证给定值是否为有效的 URL。
:param value: 需要验证的 URL。
:param public: 是否仅允许公共 URL(可选)。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
示例:
>>> url('http://foobar.dk')
True
>>> url('http://10.0.0.1')
True
>>> url('http://foobar.d')
ValidationFailure(func=url, ...)
>>> url('http://10.0.0.1', public=True)
ValidationFailure(func=url, ...)
"""
return validators.url(*args, **kwargs)
def uuid(*args, **kwargs):
"""
验证给定值是否为有效的 UUID。
:param value: 需要验证的 UUID。
:return: 如果验证成功返回 True,否则返回 ValidationFailure。
示例:
>>> uuid('2bc1c94f-0deb-43e9-92a1-4775189ec9f8')
True
>>> uuid('2bc1c94f 0deb-43e9-92a1-4775189ec9f8')
ValidationFailure(func=uuid, ...)
"""
return validators.uuid(*args, **kwargs)
@validator
def even(value):
"""
验证给定值是否为偶数。
:param value: 需要验证的数字。
:return: 如果是偶数返回 True,否则返回 ValidationFailure。
示例:
>>> even(4)
True
>>> even(5)
ValidationFailure(func=even, args={'value': 5})
"""
return not (value % 2)
+84
View File
@@ -0,0 +1,84 @@
import logging
from datetime import timedelta
class BaseConfig:
# 超级管理员账号
SUPERADMIN = 'admin'
# 系统名称
SYSTEM_NAME = 'Pear Admin'
# 主题面板的链接列表配置
SYSTEM_PANEL_LINKS = [
{
"icon": "layui-icon layui-icon-auz",
"title": "官方网站",
"href": "http://www.pearadmin.com"
},
{
"icon": "layui-icon layui-icon-auz",
"title": "开发文档",
"href": "http://www.pearadmin.com"
},
{
"icon": "layui-icon layui-icon-auz",
"title": "开源地址",
"href": "https://gitee.com/Jmysy/Pear-Admin-Layui"
}
]
# 上传图片目标文件夹
UPLOADED_PHOTOS_DEST = 'static/upload'
UPLOADED_FILES_ALLOW = ['gif', 'jpg', 'jpeg', 'png', 'webp']
UPLOADS_AUTOSERVE = True
# JSON 配置
JSON_AS_ASCII = False
# 配置多个数据库连接的连接串写法示例
# HOSTNAME: 指数据库的IP地址、USERNAME:指数据库登录的用户名、PASSWORD:指数据库登录密码、PORT:指数据库开放的端口、DATABASE:指需要连接的数据库名称
# MSSQL: f"mssql+pymssql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=cp936"
# MySQL: f"mysql+pymysql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=utf8mb4"
# Oracle: f"oracle+cx_oracle://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}"
# SQLite "sqlite:/// database.db"
# Postgres f"postgresql+psycopg2://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}"
# Oracle的第二种连接方式
# dsnStr = cx_Oracle.makedsn({HOSTNAME}, 1521, service_name='orcl')
# connect_str = "oracle://%s:%s@%s" % ('{USERNAME}', ' {PASSWORD}', dsnStr)
# 在SQLALCHEMY_BINDS 中设置:'{数据库连接别名}': '{连接串}'
# 最后在models的数据模型class中,在__tablename__前设置 __bind_key__ = '{数据库连接别名}' 即可,表示该数据模型不使用默认的数据库连接,改用“SQLALCHEMY_BINDS”中设置的其他数据库连接
# SQLALCHEMY_BINDS = {
# 'testMySQL': 'mysql+pymysql://test:123456@192.168.1.1:3306/test?charset=utf8',
# 'testMsSQL': 'mssql+pymssql://test:123456@192.168.1.1:1433/test?charset=cp936',
# 'testOracle': 'oracle+cx_oracle://test:123456@192.168.1.1:1521/test',
# 'testSQLite': 'sqlite:///database.db
# }
# 数据库的配置信息
SQLALCHEMY_DATABASE_URI = 'sqlite:///../pear.db'
# 默认日志等级
LOG_LEVEL = logging.WARN
# 发信设置
MAIL_SERVER = 'smtp.qq.com'
MAIL_USE_TLS = False
MAIL_USE_SSL = True
MAIL_PORT = 465
MAIL_USERNAME = '123@qq.com'
MAIL_PASSWORD = 'XXXXX' # 生成的授权码
MAIL_DEFAULT_SENDER = MAIL_USERNAME
# 插件配置,填写插件的文件名名称,默认不启用插件。
PLUGIN_ENABLE_FOLDERS = []
# Session 设置
PERMANENT_SESSION_LIFETIME = timedelta(days=7)
SESSION_TYPE = "filesystem" # 默认使用文件系统来保存会话
SESSION_PERMANENT = False # 会话是否持久化
SESSION_USE_SIGNER = True # 是否对发送到浏览器上 session 的 cookie 值进行加密
SECRET_KEY = "pear-system-flask"
+34
View File
@@ -0,0 +1,34 @@
from flask import Flask
from .init_sqlalchemy import db, ma, init_databases
from .init_login import init_login_manager
from .init_template_directives import init_template_directives
from .init_error_views import init_error_views
from .init_mail import init_mail, mail as flask_mail
from .init_upload import init_upload
from .init_migrate import init_migrate
from .init_session import init_session
from .init_limit import init_limit
from .init_plugins import register_plugin, broadcast_execute
def init_plugs(app: Flask) -> None:
# 注册插件a
register_plugin(app)
broadcast_execute(app, 'event_begin')
# 注册 Flask 功能
init_login_manager(app)
init_databases(app)
init_mail(app)
init_upload(app)
init_migrate(app)
init_session(app)
init_limit(app)
# 系统蓝图相关
init_template_directives(app)
init_error_views(app)
# 初始化插件
broadcast_execute(app, 'event_init')
broadcast_execute(app, 'event_finish')
@@ -1,4 +1,4 @@
from flask import render_template
from flask import render_template, jsonify
def init_error_views(app):
@@ -10,6 +10,16 @@ def init_error_views(app):
def page_not_found(e):
return render_template('errors/404.html'), 404
@app.errorhandler(405)
def page_not_found(e):
return render_template('errors/404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('errors/500.html'), 500
@app.errorhandler(429)
def ratelimit_exceeded(e):
return jsonify(
success=False, msg="请求频率超限,请稍后再试。"
)
+20
View File
@@ -0,0 +1,20 @@
from flask_limiter import Limiter
from flask_login import current_user
from flask import request, Flask
# 定义 key 函数:优先使用 current_user.id,否则用 IP
def get_user_identifier():
# 注意:current_user 可能是 AnonymousUserMixin(未登录)
if hasattr(current_user, 'id') and current_user.is_authenticated:
return str(current_user.id) # 用户 ID 作为 key
return request.remote_addr # 未登录用户则按 IP 限流
limiter = Limiter(
key_func=get_user_identifier,
# default_limits=["100 per hour"], # 可选全局默认
storage_uri="memory://" # 生产建议: "redis://localhost:6379"
)
def init_limit(app: Flask) -> None:
limiter.init_app(app)
@@ -5,11 +5,10 @@ def init_login_manager(app):
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'index.login'
login_manager.login_message = u'请登录以访问此页面'
login_manager.login_view = 'system.passport.login'
@login_manager.user_loader
def load_user(user_id):
from models import UserModel
user = UserModel.query.get(int(user_id))
from applications.models import User
user = User.query.get(int(user_id))
return user
+8
View File
@@ -0,0 +1,8 @@
from flask import Flask
from flask_mail import Mail
mail = Mail()
def init_mail(app: Flask):
mail.init_app(app)
+11
View File
@@ -0,0 +1,11 @@
from flask import Flask
from applications.extensions.init_sqlalchemy import db
from flask_migrate import Migrate
from applications.models import *
migrate = Migrate()
def init_migrate(app: Flask):
migrate.init_app(app, db)
+61
View File
@@ -0,0 +1,61 @@
import os
from flask import Flask
from flask import Blueprint
import json
import importlib
plugin_bp = Blueprint('plugin', __name__, url_prefix='/plugin')
PLUGIN_ENABLE_FOLDERS = []
PLUGIN_IMPORTLIB = []
def register_plugin(app: Flask):
"""
获取所有插件并加载
"""
global PLUGIN_ENABLE_FOLDERS
app.register_blueprint(plugin_bp)
# 载入插件过程
# plugin_folder 配置的是插件的文件夹名
PLUGIN_ENABLE_FOLDERS = app.config['PLUGIN_ENABLE_FOLDERS']
for plugin_folder in PLUGIN_ENABLE_FOLDERS:
plugin_info = {
'plugin_name': plugin_folder
}
try:
if os.path.exists("plugins/" + plugin_folder + "/__init__.json"):
with open("plugins/" + plugin_folder + "/__init__.json", "r", encoding='utf-8') as f:
plugin_info = json.loads(f.read())
# 将插件全部载入
PLUGIN_IMPORTLIB.append(importlib.import_module('plugins.' + plugin_folder))
print(f" * Plugin: Loaded plugin: {plugin_info['plugin_name']} .")
except BaseException as e:
info = f" * Plugin: Crash a error when loading {plugin_info['plugin_name'] if len(plugin_info) != 0 else 'plugin'} :" + "\n"
app.logger.error(info)
app.logger.exception(e)
def broadcast_execute(app: Flask, function_name):
for plugin in PLUGIN_IMPORTLIB:
try:
# 初始化完成事件
try:
getattr(plugin, function_name)(app)
except AttributeError: # 没有插件启用事件就不调用
pass
except BaseException as e:
app.logger.exception(e)
if function_name == 'event_finish':
with app.app_context():
broadcast_execute(app, 'event_context')
+7
View File
@@ -0,0 +1,7 @@
from flask_session import Session
sess = Session()
def init_session(app):
sess.init_app(app)
+123
View File
@@ -0,0 +1,123 @@
import datetime
import os
from flask import Flask, request
from flask_sqlalchemy import SQLAlchemy
from flask_sqlalchemy.query import Query as BaseQuery
from flask_marshmallow import Marshmallow
from marshmallow import fields
from marshmallow.validate import (
URL, Email, Range, Length, Equal, Regexp,
Predicate, NoneOf, OneOf, ContainsOnly
)
URL.default_message = '无效的链接'
Email.default_message = '无效的邮箱地址'
Range.message_min = '不能小于{min}'
Range.message_max = '不能小于{max}'
Range.message_all = '不能超过{min}{max}这个范围'
Length.message_min = '长度不得小于{min}'
Length.message_max = '长度不得大于{max}'
Length.message_all = '长度不能超过{min}{max}这个范围'
Length.message_equal = '长度必须等于{equal}'
Equal.default_message = '必须等于{other}'
Regexp.default_message = '非法输入'
Predicate.default_message = '非法输入'
NoneOf.default_message = '非法输入'
OneOf.default_message = '无效的选择'
ContainsOnly.default_message = '一个或多个无效的选择'
fields.Field.default_error_messages = {
"required": "缺少必要数据",
"null": "数据不能为空",
"validator_failed": "非法数据",
}
fields.Str.default_error_messages = {
'invalid': "不是合法文本"
}
fields.Int.default_error_messages = {
"invalid": "不是合法整数"
}
fields.Number.default_error_messages = {
"invalid": "不是合法数字"
}
fields.Boolean.default_error_messages = {
"invalid": "不是合法布尔值"
}
class Query(BaseQuery):
def soft_delete(self):
return self.update({"delete_at": datetime.datetime.now()})
def logic_all(self):
return self.filter_by(delete_at=None).all()
def all_json(self, schema: Marshmallow().Schema):
return schema(many=True).dump(self.all())
def layui_paginate(self, page=None, limit=None):
if page is None:
page = request.args.get('page', type=int)
if limit is None:
limit = min(request.args.get('limit', default=10, type=int), 90)
return self.paginate(page=page,
per_page=limit,
error_out=False
)
def layui_paginate_json(self, schema, page=None, limit=None):
if page is None:
page = request.args.get('page', 1, type=int) # 添加默认值
if limit is None:
limit = request.args.get('limit', 10, type=int) # 添加默认值
_res = self.paginate(
page=page,
per_page=limit,
error_out=False
)
return schema(many=True).dump(_res.items), _res.total, _res.page, _res.per_page
def layui_paginate_db_json(self, page=None, limit=None):
if page is None:
page = request.args.get('page', 1, type=int) # 添加默认值
if limit is None:
limit = request.args.get('limit', 10, type=int) # 添加默认值
_res = self.paginate(
page=page,
per_page=limit,
error_out=False
)
# 获取查询的列名列表
column_names = [col["name"] for col in self.column_descriptions]
# 将元组转换为字典(支持单列或多列)
data = [
dict(zip(column_names, row))
for row in _res.items
]
return data, _res.total, _res.page, _res.per_page
db = SQLAlchemy(query_class=Query)
ma = Marshmallow()
def init_databases(app: Flask):
db.init_app(app)
ma.init_app(app)
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
with app.app_context():
try:
db.engine.connect()
except Exception as e:
exit(f"数据库连接失败: {e}")
@@ -0,0 +1,17 @@
from flask import session, current_app
from flask_login import current_user
from flask_wtf.csrf import generate_csrf
def init_template_directives(app):
@app.template_global()
def authorize(power):
if current_user.username != current_app.config.get("SUPERADMIN"):
return bool(power in session.get('permissions'))
else:
return True
@app.template_global()
def csrf_input():
return f'<input type="hidden" name="csrf_token" value="{generate_csrf()}">'
+9
View File
@@ -0,0 +1,9 @@
from flask import Flask
from flask_uploads import configure_uploads
from flask_uploads import UploadSet, IMAGES
photos = UploadSet('photos', IMAGES)
def init_upload(app: Flask):
configure_uploads(app, photos)
+11
View File
@@ -0,0 +1,11 @@
from .admin_dept import Dept
from .admin_dict import DictType, DictData
from .admin_log import AdminLog
from .admin_photo import Photo
from .admin_power import Power
from .admin_role import Role
from .admin_role_power import role_power
from .admin_user import User
from .admin_user_role import user_role
from .admin_mail import Mail
from .admin_nav import Nav
+18
View File
@@ -0,0 +1,18 @@
import datetime
from applications.extensions import db
class Dept(db.Model):
__tablename__ = 'admin_dept'
id = db.Column(db.Integer, primary_key=True, comment="部门ID")
parent_id = db.Column(db.Integer, comment="父级编号")
dept_name = db.Column(db.String(50), comment="部门名称")
sort = db.Column(db.Integer, comment="排序")
leader = db.Column(db.String(50), comment="负责人")
phone = db.Column(db.String(20), comment="联系方式")
email = db.Column(db.String(50), comment="邮箱")
status = db.Column(db.Integer, comment='状态(1开启,0关闭)')
remark = db.Column(db.Text, comment="备注")
address = db.Column(db.String(255), comment="详细地址")
create_at = db.Column(db.DateTime, default=datetime.datetime.now, comment='创建时间')
update_at = db.Column(db.DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment='更新时间')
+26
View File
@@ -0,0 +1,26 @@
import datetime
from applications.extensions import db
class DictType(db.Model):
__tablename__ = 'admin_dict_type'
id = db.Column(db.Integer, primary_key=True)
type_name = db.Column(db.String(255), comment='字典类型名称')
type_code = db.Column(db.String(255), comment='字典类型标识')
description = db.Column(db.String(255), comment='字典类型描述')
enable = db.Column(db.Integer, comment='是否开启')
create_time = db.Column(db.DateTime, default=datetime.datetime.now, comment='创建时间')
update_time = db.Column(db.DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment='更新时间')
class DictData(db.Model):
__tablename__ = 'admin_dict_data'
id = db.Column(db.Integer, primary_key=True)
data_label = db.Column(db.String(255), comment='字典类型名称')
data_value = db.Column(db.String(255), comment='字典类型标识')
type_code = db.Column(db.String(255), comment='字典类型描述')
is_default = db.Column(db.Integer, comment='是否默认')
enable = db.Column(db.Integer, comment='是否开启')
remark = db.Column(db.String(255), comment='备注')
create_time = db.Column(db.DateTime, default=datetime.datetime.now, comment='创建时间')
update_time = db.Column(db.DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment='更新时间')
+14
View File
@@ -0,0 +1,14 @@
import datetime
from applications.extensions import db
class AdminLog(db.Model):
__tablename__ = 'admin_admin_log'
id = db.Column(db.Integer, primary_key=True)
method = db.Column(db.String(10))
uid = db.Column(db.Integer)
url = db.Column(db.String(255))
desc = db.Column(db.Text)
ip = db.Column(db.String(255))
success = db.Column(db.Integer)
user_agent = db.Column(db.Text)
create_time = db.Column(db.DateTime, default=datetime.datetime.now)
+12
View File
@@ -0,0 +1,12 @@
import datetime
from applications.extensions import db
class Mail(db.Model):
__tablename__ = 'admin_mail'
id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment='邮件编号')
receiver = db.Column(db.String(1024), comment='收件人邮箱')
subject = db.Column(db.String(128), comment='邮件主题')
content = db.Column(db.Text(), comment='邮件正文')
user_id = db.Column(db.Integer, comment='发送人id')
create_at = db.Column(db.DateTime, default=datetime.datetime.now, comment='创建时间')
+69
View File
@@ -0,0 +1,69 @@
"""
前台公开导航模型。
Nav 用于「公开访问的导航聚合页」,与 admin_power 表完全解耦:
- admin_power 控制后台管理菜单,需要登录
- public_nav 控制前台公开导航,无需登录
后续若要做前台用户(收藏、提交链接),只需加独立 User 模型,
不需要碰 RBAC。
"""
import datetime
from applications.extensions import db
class Nav(db.Model):
__tablename__ = 'public_nav'
id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment='导航ID')
# 分类名(如"开发工具"/"AI"/"设计资源"),用于首页聚合展示
category = db.Column(db.String(50), nullable=False, comment='分类', index=True)
# 单条导航的标题
title = db.Column(db.String(100), nullable=False, comment='标题')
# 链接 URL(站内或外链)
url = db.Column(db.String(255), nullable=False, comment='链接')
# 简短描述(鼠标悬停时可见)
description = db.Column(db.String(255), default='', comment='简介')
# layui-icon 的 class 名,例如 "layui-icon-website"
icon = db.Column(db.String(100), default='layui-icon-link', comment='图标')
# 同分类下的显示顺序
sort = db.Column(db.Integer, default=0, comment='排序')
# 1=启用,0=禁用(禁用后前台不再展示,后台列表仍可见)
status = db.Column(db.Integer, default=1, comment='状态(1启用,0关闭)')
# 1=外链(新窗口打开),0=站内(当前窗口)
is_external = db.Column(db.Integer, default=1, comment='是否外链')
# 创建者(后台管理员用户名,便于追溯),非空约束放宽以便匿名维护
create_by = db.Column(db.String(50), default='admin', comment='创建者')
create_at = db.Column(db.DateTime, default=datetime.datetime.now, comment='创建时间')
update_at = db.Column(
db.DateTime,
default=datetime.datetime.now,
onupdate=datetime.datetime.now,
comment='更新时间',
)
def to_dict(self):
"""方便模板直接读取"""
return {
'id': self.id,
'category': self.category,
'title': self.title,
'url': self.url,
'description': self.description or '',
'icon': self.icon or 'layui-icon-link',
'sort': self.sort or 0,
'status': self.status,
'is_external': self.is_external,
'create_by': self.create_by,
'create_at': self.create_at.strftime('%Y-%m-%d %H:%M:%S') if self.create_at else '',
}
+12
View File
@@ -0,0 +1,12 @@
import datetime
from applications.extensions import db
class Photo(db.Model):
__tablename__ = 'admin_photo'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False)
href = db.Column(db.String(255))
mime = db.Column(db.CHAR(50), nullable=False)
size = db.Column(db.CHAR(30), nullable=False)
create_time = db.Column(db.DateTime, default=datetime.datetime.now)
+18
View File
@@ -0,0 +1,18 @@
import datetime
from applications.extensions import db
class Power(db.Model):
__tablename__ = 'admin_power'
id = db.Column(db.Integer, primary_key=True, comment='权限编号')
name = db.Column(db.String(255), comment='权限名称')
type = db.Column(db.String(1), comment='权限类型')
code = db.Column(db.String(30), comment='权限标识')
url = db.Column(db.String(255), comment='权限路径')
open_type = db.Column(db.String(10), comment='打开方式')
parent_id = db.Column(db.Integer, comment='父类编号')
icon = db.Column(db.String(128), comment='图标')
sort = db.Column(db.Integer, comment='排序')
create_time = db.Column(db.DateTime, default=datetime.datetime.now, comment='创建时间')
update_time = db.Column(db.DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment='更新时间')
enable = db.Column(db.Integer, comment='是否开启')
+16
View File
@@ -0,0 +1,16 @@
import datetime
from applications.extensions import db
class Role(db.Model):
__tablename__ = 'admin_role'
id = db.Column(db.Integer, primary_key=True, comment='角色ID')
name = db.Column(db.String(255), comment='角色名称')
code = db.Column(db.String(255), comment='角色标识')
enable = db.Column(db.Integer, comment='是否启用')
remark = db.Column(db.String(255), comment='备注')
details = db.Column(db.String(255), comment='详情')
sort = db.Column(db.Integer, comment='排序')
create_time = db.Column(db.DateTime, default=datetime.datetime.now, comment='创建时间')
update_time = db.Column(db.DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment='更新时间')
power = db.relationship('Power', secondary="admin_role_power", backref=db.backref('role'))
+9
View File
@@ -0,0 +1,9 @@
from applications.extensions import db
# 创建中间表
role_power = db.Table(
"admin_role_power", # 中间表名称
db.Column("id", db.Integer, primary_key=True, autoincrement=True, comment='标识'), # 主键
db.Column("power_id", db.Integer, db.ForeignKey("admin_power.id"), comment='用户编号'), # 属性 外键
db.Column("role_id", db.Integer, db.ForeignKey("admin_role.id"), comment='角色编号'), # 属性 外键
)
+25
View File
@@ -0,0 +1,25 @@
import datetime
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from applications.extensions import db
class User(db.Model, UserMixin):
__tablename__ = 'admin_user'
id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment='用户ID')
username = db.Column(db.String(20), comment='用户名')
realname = db.Column(db.String(20), comment='真实名字')
avatar = db.Column(db.String(255), comment='头像', default="/static/system/admin/images/avatar.jpg")
remark = db.Column(db.String(255), comment='备注')
password_hash = db.Column(db.String(128), comment='哈希密码')
enable = db.Column(db.Integer, default=0, comment='启用')
dept_id = db.Column(db.Integer, comment='部门id')
create_at = db.Column(db.DateTime, default=datetime.datetime.now, comment='创建时间')
update_at = db.Column(db.DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment='创建时间')
role = db.relationship('Role', secondary="admin_user_role", backref=db.backref('user'), lazy='dynamic')
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def validate_password(self, password):
return check_password_hash(self.password_hash, password)
+9
View File
@@ -0,0 +1,9 @@
from applications.extensions import db
# 创建中间表
user_role = db.Table(
"admin_user_role", # 中间表名称
db.Column("id", db.Integer, primary_key=True, autoincrement=True, comment='标识'), # 主键
db.Column("user_id", db.Integer, db.ForeignKey("admin_user.id"), comment='用户编号'), # 属性 外键
db.Column("role_id", db.Integer, db.ForeignKey("admin_role.id"), comment='角色编号'), # 属性 外键
)
-42
View File
@@ -1,42 +0,0 @@
from common import register_api
from .department import DepartmentsApi, DeptEnableAPI
from .rights import (
RightsApi, PowerApi, right_power_enable_resource,
admin_configs_resource, admin_menu_resource
)
from .roles import RoleRoleApi, RolePowerApi, role_deletes, role_enable_resource
def register_rights_api(api_bp):
register_api(DepartmentsApi, 'rights_dept_api', '/dept/department/', pk='_id', app=api_bp)
api_bp.add_url_rule('/dept/department/<int:_id>/status',
view_func=DeptEnableAPI.as_view('dept_status_api'),
methods=['PUT', ])
api_bp.add_url_rule('/rights/rights',
view_func=RightsApi.as_view('dept_rights_api'),
methods=['GET', 'DELETE'])
register_api(PowerApi, 'rights_power_api', '/rights/power/', pk='_id', app=api_bp)
api_bp.add_url_rule('/rights/configs',
endpoint='rights_config_api',
view_func=admin_configs_resource,
methods=['GET'])
api_bp.add_url_rule('/rights/menu',
endpoint='rights_menu_api',
view_func=admin_menu_resource,
methods=['GET'])
api_bp.add_url_rule('/rights/power/<int:_id>/<action>',
endpoint='rights_power_action_api',
view_func=right_power_enable_resource,
methods=['PUT'])
api_bp.add_url_rule('/roles/role',
endpoint='rights_roles_role_api',
view_func=role_deletes,
methods=['DELETE'])
register_api(RoleRoleApi, 'rights_role_api', '/roles/role/', pk='_id', app=api_bp)
register_api(RolePowerApi, 'rights_role_power_api', '/roles/role_power/', pk='_id', app=api_bp)
api_bp.add_url_rule('/roles/role/<int:_id>/status',
endpoint='rights_roles_enable_api',
view_func=role_enable_resource,
methods=['PUT'])
-112
View File
@@ -1,112 +0,0 @@
import typing as t
from flask.views import MethodView
from flask_pydantic import validate
from pydantic import BaseModel, Field
from common.utils.http import success_api, fail_api, table_api
from extensions import db
from models import DepartmentModel, UserModel
class DeptSchema(BaseModel):
address: t.Optional[str]
dept_name: t.Optional[str] = Field(alias='deptName')
email: t.Optional[str]
leader: t.Optional[str]
parent_id: t.Optional[str] = Field(alias='parentId')
phone: t.Optional[str]
sort: t.Optional[int]
status: t.Optional[int]
class DepartmentsApi(MethodView):
def get(self, _id):
if _id:
dept = DepartmentModel.query.filter_by(id=_id).first()
dept_data = {
'id': dept.id,
'dept_name': dept.dept_name,
'leader': dept.leader,
'email': dept.email,
'phone': dept.phone,
'status': dept.status,
'sort': dept.sort,
'address': dept.address,
}
return dict(success=True, message='ok', dept=dept_data)
dept_data = DepartmentModel.query.order_by(DepartmentModel.sort).all()
return table_api(
result={
'items': [
{
'deptId': item.id,
'parentId': item.parent_id,
'deptName': item.dept_name,
'sort': item.sort,
'leader': item.leader,
'phone': item.phone,
'email': item.email,
'status': item.status,
'comment': item.comment,
'address': item.address,
'create_at': item.create_at.strftime('%Y-%m-%d %H:%M:%S')
} for item in dept_data
],
'total': len(dept_data)}
, code=0)
@validate()
def post(self, body: DeptSchema):
dept = DepartmentModel(
parent_id=body.parent_id,
dept_name=body.dept_name,
sort=body.sort,
leader=body.leader,
phone=body.phone,
email=body.email,
status=body.status,
address=body.address
)
db.session.add(dept)
db.session.commit()
return success_api(message="成功")
@validate()
def put(self, _id, body: DeptSchema):
data = {
"dept_name": body.dept_name,
"sort": body.sort,
"leader": body.leader,
"phone": body.phone,
"email": body.email,
"status": body.status,
"address": body.address
}
body = DepartmentModel.query.filter_by(id=_id).update(data)
if not body:
return {'success': False, 'message': "更新失败", 'code': 404}
db.session.commit()
return {'success': True, 'message': '更新成功', 'code': 200}
def delete(self, _id):
ret = DepartmentModel.query.filter_by(id=_id).delete()
UserModel.query.filter_by(dept_id=_id).update({"dept_id": None})
db.session.commit()
if ret:
return success_api(message="删除成功")
return fail_api(message="删除失败")
class DeptEnableAPI(MethodView):
def put(self, _id):
d = DepartmentModel.query.get(_id)
if d:
d.status = not d.status
db.session.commit()
message = '修改成功'
return success_api(message=message)
return fail_api(message="出错啦")
-278
View File
@@ -1,278 +0,0 @@
import copy
from collections import OrderedDict
from typing import Optional
from flask import request, jsonify, current_app
from flask.views import MethodView
from flask_login import current_user
from flask_pydantic import validate
from pydantic import BaseModel, Field
from common.utils.http import success_api, fail_api
from extensions import db
from models import RightModel, RoleModel
def get_render_config():
# 网站配置
config = {
'logo': {
# 网站名称
"title": current_app.config.get("SYSTEM_NAME"),
# 网站图标
"image": "/static/admin/admin/images/logo.png"
# 菜单配置
},
'menu': {
# 菜单数据来源
"data": "/api/v1/rights/menu",
"collaspe": True,
# 是否同时只打开一个菜单目录
"accordion": True,
"method": "GET",
# 是否开启多系统菜单模式
"control": False,
# 默认选中的菜单项
"select": "0",
# 是否开启异步菜单,false 时 data 属性设置为菜单数据,false 时为 json 文件或后端接口
"async": True
},
'tab': {
# 是否开启多选项卡
"muiltTab": True,
# 切换选项卡时,是否刷新页面状态
"keepState": True,
# 是否开启 Tab 记忆
"session": True,
# 最大可打开的选项卡数量
"tabMax": 30,
"index": {
# 标识 ID , 建议与菜单项中的 ID 一致
"id": "10",
# 页面地址
"href": "/admin/welcome",
# 标题
"title": "首页"
}
},
'theme': {
# 默认主题色,对应 colors 配置中的 ID 标识
"defaultColor": "2",
# 默认的菜单主题 dark-theme 黑 / light-theme 白
"defaultMenu": "dark-theme",
# 是否允许用户切换主题,false 时关闭自定义主题面板
"allowCustom": True
},
'colors': [
{
"id": "1",
"color": "#2d8cf0"
},
{
"id": "2",
"color": "#5FB878"
},
{
"id": "3",
"color": "#1E9FFF"
},
{
"id": "4",
"color": "#FFB800"
},
{
"id": "5",
"color": "darkgray"
},
],
'links': current_app.config.get("SYSTEM_PANEL_LINKS"),
'other': {
# 主页动画时长
"keepLoad": 1200,
# 布局顶部主题
"autoHead": False
},
'header': False
}
return config
# 生成菜单树
def make_menu_tree():
role = current_user.role
powers = []
for i in role:
# 如果角色没有被启用就直接跳过
if i.enable == 0:
continue
# 变量角色用户的权限
for p in i.power:
# 如果权限关闭了就直接跳过
if p.enable == 0:
continue
# 一二级菜单
if p.type == 0 or p.type == 1:
powers.append(p)
power_dict = [
{
'id': item.id,
'title': item.name,
'type': item.type,
'code': item.code,
'href': item.url,
'openType': item.open_type,
'parent_id': item.parent_id,
'icon': item.icon,
'sort': item.sort,
'enable': item.enable,
} for item in powers
]
power_dict.sort(key=lambda x: x['id'], reverse=True)
menu_dict = OrderedDict()
for _dict in power_dict:
if _dict['id'] in menu_dict:
# 当前节点添加子节点
_dict['children'] = copy.deepcopy(menu_dict[_dict['id']])
_dict['children'].sort(key=lambda item: item['sort'])
# 删除子节点
del menu_dict[_dict['id']]
if _dict['parent_id'] not in menu_dict:
menu_dict[_dict['parent_id']] = [_dict]
else:
menu_dict[_dict['parent_id']].append(_dict)
return sorted(menu_dict.get(0), key=lambda item: item['sort'])
class PowerSchema(BaseModel):
icon: str
open_type: Optional[str] = Field(alias='openType')
parent_id: Optional[str] = Field(alias='parentId')
power_code: Optional[str] = Field(alias='powerCode')
power_name: Optional[str] = Field(alias='powerName')
power_type: Optional[str] = Field(alias='powerType')
power_url: Optional[str] = Field(alias='powerUrl')
sort: Optional[int] = Field(alias='sort')
class RightsApi(MethodView):
def get(self):
"""获取选择父节点"""
power = RightModel.query.all()
# power_data = marshal(power, RightModel.fields())
power_data = [
{
'powerId': item.id,
'powerName': item.name,
'powerType': item.type,
'powerUrl': item.url,
'openType': item.open_type,
'parentId': item.parent_id,
'icon': item.icon,
'sort': item.sort,
'enable': item.enable,
} for item in power
]
power_data.append({"powerId": 0, "powerName": "顶级权限", "parentId": -1})
res = {
"status": {"code": 200, "message": "默认"},
"data": power_data
}
return res
def delete(self):
ids = request.form.getlist('ids[]')
for id in ids:
role = RoleModel.query.filter_by(id=id).first()
# 删除该角色的权限和用户
role.power = []
role.user = []
r = RoleModel.query.filter_by(id=id).delete()
db.session.commit()
return success_api(message="批量删除成功")
class PowerApi(MethodView):
@validate()
def post(self, body: PowerSchema):
power = RightModel(
icon=body.icon,
open_type=body.open_type,
parent_id=body.parent_id,
code=body.power_code,
name=body.power_name,
type=body.power_type,
url=body.power_url,
sort=body.sort,
enable=1
)
try:
db.session.add(power)
db.session.commit()
except Exception as e:
print(e)
return fail_api(message='数据提交失败')
return success_api(message="成功")
def delete(self, _id):
# 删除权限(目前没有判断父节点自动删除子节点)
power = RightModel.query.filter_by(id=_id).first()
role_id_list = []
roles = power.role
for role in roles:
role_id_list.append(role.id)
roles = RoleModel.query.filter(RoleModel.id.in_(role_id_list)).all()
for p in roles:
power.role.remove(p)
r = RightModel.query.filter_by(id=_id).delete()
db.session.commit()
if r:
return success_api(message="删除成功")
else:
return fail_api(message="删除失败")
@validate()
def put(self, _id, body: PowerSchema):
data = {
"icon": body.icon,
"open_type": body.open_type,
"parent_id": body.parent_id,
"code": body.power_code,
"name": body.power_name,
"type": body.power_type,
"url": body.power_url,
"sort": body.sort
}
power = RightModel.query.filter_by(id=_id).update(data)
db.session.commit()
if not power:
return fail_api(message="更新权限失败")
return success_api(message="更新权限成功")
def right_power_enable_resource(_id, action):
power = RightModel.query.get(_id)
if power:
power.enable = not power.enable
db.session.commit()
message = "修改成功"
return success_api(message=message)
else:
return fail_api(message="出错啦")
def admin_configs_resource():
return get_render_config()
def admin_menu_resource():
menu_tree = make_menu_tree()
return jsonify(menu_tree)
-162
View File
@@ -1,162 +0,0 @@
from flask import request
from flask.views import MethodView
from common.utils.http import table_api, success_api, fail_api
from extensions import db
from models import RightModel, RoleModel
def role_deletes():
ids = request.form.getlist('ids[]')
for id in ids:
role = RoleModel.query.filter_by(id=id).first()
# 删除该角色的权限和用户
role.power = []
role.user = []
r = RoleModel.query.filter_by(id=id).delete()
db.session.commit()
return success_api(message="批量删除成功")
class RoleRoleApi(MethodView):
def get(self, _id):
if not _id:
page = request.args.get('page', default=1, type=int)
limit = request.args.get('limit', default=10, type=int)
role_name = request.args.get('roleName', default="", type=str)
role_code = request.args.get('roleCode', default="", type=str)
filters = []
if role_name:
filters.append(RoleModel.name.like('%' + role_name + '%'))
if role_code:
filters.append(RoleModel.code.like('%' + role_code + '%'))
paginate = RoleModel.query.filter(*filters).paginate(page=page, per_page=limit, error_out=False)
return table_api(
result={
'items': [{'id': item.id,
'roleName': item.name,
'roleCode': item.code,
'enable': item.enable,
'comment': item.comment,
'details': item.details,
'sort': item.sort,
} for item in paginate.items],
'total': paginate.total}
, code=0)
def post(self):
# TODO 添加校验
details = request.json.get('details', '')
enable = request.json.get('enable', 0)
role_code = request.json.get('roleCode', '')
role_name = request.json.get('roleName', '')
sort = request.json.get('sort', 0)
role = RoleModel(
details=details,
enable=int(enable),
code=role_code,
name=role_name,
sort=int(sort)
)
db.session.add(role)
db.session.commit()
return success_api(message="成功")
# 更新角色
def put(self, _id):
# TODO 添加校验
role_code = request.json.get('roleCode', 0)
role_name = request.json.get('roleName', '')
sort = request.json.get('sort', 0) # int
enable = request.json.get('enable', 0) # int
details = request.json.get('details', '')
data = {
"code": role_code,
"name": role_name,
"sort": sort,
"enable": enable,
"details": details
}
role = RoleModel.query.filter_by(id=_id).update(data)
db.session.commit()
if not role:
return fail_api(message="更新角色失败")
return success_api(message="更新角色成功")
def role_enable_resource(_id):
"""启用用户"""
ret = RoleModel.query.get(_id)
ret.enable = not ret.enable
db.session.commit()
message = "修改成功"
if not ret:
return fail_api(message="出错啦")
return success_api(message=message)
class RolePowerApi(MethodView):
def get(self, _id):
# 获取角色权限
role = RoleModel.query.filter_by(id=_id).first()
# 获取权限列表的 id
check_powers_list = [rp.id for rp in role.power]
powers = RightModel.query.all() # 获取所有的权限
powers = [
{
'powerId': item.id,
'powerName': item.name,
'powerType': item.type,
'powerUrl': item.url,
'openType': item.open_type,
'parentId': item.parent_id,
'icon': item.icon,
'sort': item.sort,
'enable': item.enable,
} for item in powers]
for i in powers:
if int(i.get("powerId")) in check_powers_list:
i["checkArr"] = "1"
else:
i["checkArr"] = "0"
return {
"data": powers,
"status": {"code": 200, "message": "默认"}
}
# 保存角色权限
def put(self, _id):
power_ids = request.json.get('powerIds', '')
power_list = power_ids.split(',')
""" 更新角色权限 """
role = RoleModel.query.filter_by(id=_id).first()
powers = RightModel.query.filter(RightModel.id.in_(power_list)).all()
role.power = powers
db.session.commit()
return success_api(message="授权成功")
# 角色删除
def delete(self, _id):
role = RoleModel.query.filter_by(id=_id).first()
# 删除该角色的权限和用户
role.power = []
role.user = []
r = RoleModel.query.filter_by(id=_id).delete()
db.session.commit()
if not r:
return fail_api(message="角色删除失败")
return success_api(message="角色删除成功")
+9
View File
@@ -0,0 +1,9 @@
from .admin_role import RoleOutSchema
from .admin_power import PowerOutSchema, PowerOutSchema2
from .admin_dict import DictDataOutSchema, DictTypeOutSchema
from .admin_dept import DeptSchema
from .admin_log import LogOutSchema
from .admin_photo import PhotoOutSchema
from .admin_mail import MailOutSchema
from .admin_nav import NavOutSchema, NavManageSchema
+12
View File
@@ -0,0 +1,12 @@
from flask_marshmallow.sqla import SQLAlchemyAutoSchema
from applications.models import Dept
class DeptSchema(SQLAlchemyAutoSchema):
class Meta:
model = Dept # table = models.Album.__table__
# include_relationships = True # 输出模型对象时同时对外键,是否也一并进行处理
include_fk = True # 序列化阶段是否也一并返回主键
# fields= ["id","name"] # 启动的字段列表
# exclude = ["id","name"] # 排除字段列表
+21
View File
@@ -0,0 +1,21 @@
from applications.extensions import ma
from marshmallow import fields
class DictTypeOutSchema(ma.Schema):
id = fields.Str(attribute="id")
typeName = fields.Str(attribute="type_name")
typeCode = fields.Str(attribute="type_code")
description = fields.Str(attribute="description")
createTime = fields.Str(attribute="create_time")
updateName = fields.Str(attribute="update_time")
remark = fields.Str()
enable = fields.Str()
class DictDataOutSchema(ma.Schema):
dataId = fields.Str(attribute="id")
dataLabel = fields.Str(attribute="data_label")
dataValue = fields.Str(attribute="data_value")
remark = fields.Str()
enable = fields.Str()
+14
View File
@@ -0,0 +1,14 @@
from applications.extensions import ma
from marshmallow import fields
class LogOutSchema(ma.Schema):
id = fields.Integer()
method = fields.Str()
uid = fields.Str()
url = fields.Str()
desc = fields.Str()
ip = fields.Str()
user_agent = fields.Str()
success = fields.Bool()
create_time = fields.DateTime()
+19
View File
@@ -0,0 +1,19 @@
from applications.extensions import ma
from marshmallow import fields
from applications.models import User
# 用户models的序列化类
class MailOutSchema(ma.Schema):
id = fields.Integer()
receiver = fields.Str()
subject = fields.Str()
content = fields.Str()
realname = fields.Method("get_realname")
create_at = fields.DateTime()
def get_realname(self, obj):
if obj.user_id != None:
return User.query.filter_by(id=obj.user_id).first().realname
else:
return None
+17
View File
@@ -0,0 +1,17 @@
from flask_marshmallow.sqla import SQLAlchemyAutoSchema
from applications.models import Nav
class NavOutSchema(SQLAlchemyAutoSchema):
"""前台展示用:序列化 Nav 对象为 JSON"""
class Meta:
model = Nav
include_fk = True
class NavManageSchema(SQLAlchemyAutoSchema):
"""后台管理用:同上,前缀分开便于将来按需差异化"""
class Meta:
model = Nav
include_fk = True
+12
View File
@@ -0,0 +1,12 @@
from applications.extensions import ma
from marshmallow import fields
class PhotoOutSchema(ma.Schema):
id = fields.Integer()
name = fields.Str()
href = fields.Str()
mime = fields.Str()
size = fields.Str()
ext = fields.Str()
create_time = fields.DateTime()
+44
View File
@@ -0,0 +1,44 @@
from flask_marshmallow.sqla import SQLAlchemyAutoSchema
from applications.extensions import ma
from marshmallow import fields
from applications.models import Power
class PowerSchema(SQLAlchemyAutoSchema):
class Meta:
model = Power # table = models.Album.__table__
# include_relationships = True # 输出模型对象时同时对外键,是否也一并进行处理
include_fk = True # 序列化阶段是否也一并返回主键
# fields= ["id","name"] # 启动的字段列表
# exclude = ["id","name"] # 排除字段列表
# 权限models序列化类
class PowerOutSchema(ma.Schema):
id = fields.Integer()
title = fields.Str(attribute="name")
type = fields.Str()
code = fields.Str()
href = fields.Str(attribute="url")
openType = fields.Str(attribute="open_type")
parent_id = fields.Integer()
icon = fields.Str()
sort = fields.Integer()
create_time = fields.DateTime()
update_time = fields.DateTime()
enable = fields.Integer()
class PowerOutSchema2(ma.Schema): # 序列化类
powerId = fields.Str(attribute="id")
powerName = fields.Str(attribute="name")
powerType = fields.Str(attribute="type")
powerUrl = fields.Str(attribute="url")
openType = fields.Str(attribute="open_type")
parentId = fields.Str(attribute="parent_id")
icon = fields.Str()
sort = fields.Integer()
create_time = fields.DateTime()
update_time = fields.DateTime()
enable = fields.Integer()
+12
View File
@@ -0,0 +1,12 @@
from flask_marshmallow.sqla import SQLAlchemyAutoSchema
from applications.models import Role
class RoleOutSchema(SQLAlchemyAutoSchema):
class Meta:
model = Role # table = models.Album.__table__
# include_relationships = True # 输出模型对象时同时对外键,是否也一并进行处理
include_fk = True # 序列化阶段是否也一并返回主键
# fields= ["id","name"] # 启动的字段列表
# exclude = ["id","name"] # 排除字段列表
-8
View File
@@ -1,8 +0,0 @@
from common import register_api
from .file import FilePhotoAPI
from .passport import LoginAPI
def register_sys_api(api_bp):
register_api(LoginAPI, 'login_api', '/passport/login', pk='_id', app=api_bp)
register_api(FilePhotoAPI, 'photo_api', '/file/photo', pk='photo_id', app=api_bp)
-78
View File
@@ -1,78 +0,0 @@
from flask import request, jsonify
from flask.views import MethodView
from sqlalchemy import desc
from common.utils.http import fail_api, success_api, table_api
from common.utils.upload import upload_one, delete_photo_by_id
from models import PhotoModel
class FilePhotoAPI(MethodView):
def get(self, photo_id):
if photo_id is None:
page = request.args.get('page', type=int, default=1)
limit = request.args.get('limit', type=int, default=10)
photo_paginate = PhotoModel.query.order_by(
desc(PhotoModel.create_at)).paginate(
page=page, per_page=limit, error_out=False)
data = [
{
'id': item.id,
'name': item.name,
'href': item.href,
'mime': item.mime,
'size': item.size,
'ext': item.ext if hasattr(item, 'ext') else "",
'create_at': str(item.create_at),
} for item in photo_paginate.items
]
return table_api(
result={
'items': data,
'total': photo_paginate.total,
},
code=0)
else:
# 显示一张图片
item = PhotoModel.query.get(photo_id)
return table_api(
result={
'items': {
'id': item.id,
'name': item.name,
'href': item.href,
'mime': item.mime,
'size': item.size,
'ext': item.ext if hasattr(item, 'ext') else "",
'create_at': str(item.create_at),
},
'total': 1,
},
code=0
)
def post(self):
if 'file' in request.files:
photo = request.files['file']
mime = request.files['file'].content_type
file_url = upload_one(photo=photo, mime=mime)
res = {
"message": "上传成功",
"code": 0,
"success": True,
"data": {"src": file_url},
}
return jsonify(res)
return fail_api()
def delete(self, photo_id):
res = delete_photo_by_id(photo_id)
if res:
return success_api(message="删除成功")
else:
return fail_api(message="删除失败")
def put(self, photo_id):
pass
-52
View File
@@ -1,52 +0,0 @@
from flask import render_template, make_response
from flask import session, redirect, url_for
from flask.views import MethodView
from flask_login import current_user, login_user
from flask_pydantic import validate
from pydantic import BaseModel
from common.gen_captcha import add_auth_session
from common.utils.http import fail_api, success_api
from common.utils.rights import record_logging
from models import UserModel
class LoginSchema(BaseModel):
username: str
password: str
captcha: str
class LoginAPI(MethodView):
def get(self):
if current_user.is_authenticated:
return redirect(url_for('admin.index'))
return make_response(render_template('index/login.html'))
@validate()
def post(self, body: LoginSchema):
s_code = session.get("code", None)
session["code"] = None
if body.captcha != s_code:
return fail_api(message="验证码错误")
user = UserModel.query.filter_by(username=body.username).first()
if user is None:
return fail_api(message="不存在的用户")
if user.enable == 0:
return fail_api(message="用户被暂停使用")
if user.validate_password(body.password):
# 登录
login_user(user)
# 记录登录日志
record_logging()
# 存入权限
add_auth_session()
return success_api(message="登录成功")
record_logging()
return fail_api(message="用户名或密码错误")
-14
View File
@@ -1,14 +0,0 @@
from common import register_api
from .user import UserApi, user_role_resource, user_info
def register_users_api(api_bp):
api_bp.add_url_rule('/users/user/<int:_id>/<action>',
view_func=user_info,
methods=['PUT'])
register_api(UserApi, 'users_api', '/users/user/', pk='_id', app=api_bp)
api_bp.add_url_rule('/users/user/<int:_id>/role',
view_func=user_role_resource,
methods=['PUT'])
-243
View File
@@ -1,243 +0,0 @@
import typing as t
from flask import request, jsonify
from flask.views import MethodView
from flask_login import current_user
from flask_pydantic import validate
from pydantic import BaseModel, Field
from sqlalchemy import desc
from common.utils.http import fail_api, success_api, table_api
from extensions import db
from models import LogModel
from models import UserModel, RoleModel, DepartmentModel
def get_current_user_logs():
""" 获取当前用户日志 """
log = LogModel.query.filter_by(url='/passport/login').filter_by(uid=current_user.id).order_by(
desc(LogModel.create_at)).limit(10)
return log
def is_user_exists(username):
""" 判断用户是否存在 """
res = UserModel.query.filter_by(username=username).count()
return bool(res)
def delete_by_id(_id):
""" 删除用户 """
user = UserModel.query.filter_by(id=_id).first()
roles_id = []
for role in user.role:
roles_id.append(role.id)
roles = RoleModel.query.filter(RoleModel.id.in_(roles_id)).all()
for r in roles:
user.role.remove(r)
res = UserModel.query.filter_by(id=_id).delete()
db.session.commit()
return res
def batch_remove(ids):
""" 批量删除 """
for _id in ids:
delete_by_id(_id)
def update_user_role(_id, roles_list):
user = UserModel.query.filter_by(id=_id).first()
roles_id = []
for role in user.role:
roles_id.append(role.id)
roles = RoleModel.query.filter(RoleModel.id.in_(roles_id)).all()
for r in roles:
user.role.remove(r)
roles = RoleModel.query.filter(RoleModel.id.in_(roles_list)).all()
for r in roles:
user.role.append(r)
db.session.commit()
def users_delete():
"""批量删除"""
ids = request.form.getlist('ids[]')
batch_remove(ids)
return success_api(message="批量删除成功")
class QuerySchema(BaseModel):
page: int = 1
limit: int = 10
real_name: t.Optional[str] = Field(alias='realName')
username: t.Optional[str]
dept_id: t.Optional[str] = Field(alias='deptId', default=0)
phone: t.Optional[str]
sort: t.Optional[int]
status: t.Optional[int]
class PersonSchema(BaseModel):
role_ids: str = Field(alias='roleIds')
username: str
real_name: str = Field(alias='realName')
password: str
class UserApi(MethodView):
"""修改用户数据"""
@validate()
def get(self, _id, query: QuerySchema):
filters = []
if query.real_name:
filters.append(UserModel.realname.like('%' + query.real_name + '%'))
if query.username:
filters.append(UserModel.username.like('%' + query.username + '%'))
if query.dept_id:
filters.append(UserModel.dept_id == query.dept_id)
paginate = UserModel.query.filter(
*filters).paginate(
page=query.page, per_page=query.limit, error_out=False)
dept_name = lambda _id: DepartmentModel.query.filter_by(id=_id).first().dept_name if _id else ""
user_data = [{
'id': item.id,
'username': item.username,
'realname': item.realname,
'enable': item.enable,
'create_at': str(item.create_at),
'update_at': str(item.update_at),
'dept': dept_name(item.dept_id),
} for item in paginate.items]
return table_api(
result={
'items': user_data,
'total': paginate.total,
}
, code=0
)
@validate()
def post(self, body: PersonSchema):
"""新建单个用户"""
role_ids = body.role_ids.split(',')
if is_user_exists(body.username):
return fail_api(message="用户已经存在")
user = UserModel()
user.username = body.username
user.realname = body.real_name
user.set_password(body.password)
db.session.add(user)
db.session.commit()
""" 增加用户角色 """
user = UserModel.query.filter_by(id=user.id).first()
roles = RoleModel.query.filter(RoleModel.id.in_(role_ids)).all()
for r in roles:
user.role.append(r)
db.session.commit()
return success_api(message="增加成功", code=0)
def delete(self, _id):
# 删除用户
res = delete_by_id(_id)
if not res:
return fail_api(message="删除失败")
return success_api(message="删除成功")
class PersonModel2(BaseModel):
role_ids: str = Field(alias='roleIds')
user_id: str = Field(alias='userId')
username: str
real_name: str = Field(alias='realName')
dept_id: str = Field(alias='deptId')
@validate()
def user_role_resource(_id, body: PersonModel2):
role_ids = body.role_ids.split(',')
# 更新用户数据
UserModel.query.filter_by(id=_id).update({'username': body.username,
'realname': body.real_name,
'dept_id': body.dept_id})
db.session.commit()
update_user_role(_id, role_ids)
return success_api(message="更新成功")
def user_info(_id, action):
if action == 'info':
real_name = request.json.get('realName', '')
username = request.json.get('username', '')
remark = request.json.get('remark', '')
details = request.json.get('details', '')
ret = UserModel.query.get(_id)
ret.username = username
ret.realname = real_name
ret.remark = details
db.session.commit()
if not ret:
return fail_api(message="出错啦")
return success_api(message="更新成功")
elif action == 'status':
user_id = int(request.json.get('userId', 0)) # int
operate = int(request.json.get('operate', 0)) # int
if operate not in [0, 1]:
return {'status': 'error', 'message': '请求有误'}
if operate == 1:
user = UserModel.query.get(_id)
user.enable = operate
message = success_api(message="启动成功")
else:
user = UserModel.query.filter_by(id=user_id).update({"enable": operate})
message = success_api(message="禁用成功")
if user:
db.session.commit()
else:
return fail_api(message="出错啦")
return message
elif action == 'avatar':
url = request.json.get("avatar").get("src")
ret = UserModel.query.get(_id)
ret.avatar = url
db.session.commit()
if not ret:
return fail_api(message="出错啦")
return success_api(message="修改成功")
elif action == 'password':
oldPassword = request.json.get('oldPassword', '')
newPassword = request.json.get('newPassword', '')
confirmPassword = request.json.get('confirmPassword', '')
if not all([oldPassword, newPassword, confirmPassword]):
return {'status': 'error', 'message': '密码不能为空'}
if newPassword != confirmPassword:
return fail_api(message='确认密码不一致')
""" 修改当前用户密码 """
user = UserModel.query.get(_id)
is_right = user.validate_password(oldPassword)
if not is_right:
return jsonify(success=False, message="旧密码错误")
user.set_password(newPassword)
db.session.add(user)
db.session.commit()
return jsonify(success=True, message="更改成功")
else:
return jsonify(success=False, message="操作有误")
+12 -18
View File
@@ -1,20 +1,14 @@
from flask import Flask
from .index import index_bp
from .logs_view import logs_bp
from .roles import role_bp
from .system_view import system_bp
from .document_view import document_bp
from . import department
from . import file
from . import rights
from . import users
from applications.view.system import register_system_bps
from applications.view.public import bp as public_bp # public 包的入口就是 nav_bp
from applications.extensions.init_plugins import broadcast_execute
def init_view(app: Flask):
app.register_blueprint(index_bp)
app.register_blueprint(logs_bp)
app.register_blueprint(role_bp)
app.register_blueprint(system_bp)
app.register_blueprint(document_bp)
def init_bps(app):
# 前台公开蓝图(无需登录)
app.register_blueprint(public_bp)
# 后台系统蓝图(需要登录 + 权限码)
register_system_bps(app)
# 插件初始化函数
broadcast_execute(app, 'event_init')
-21
View File
@@ -1,21 +0,0 @@
from flask import render_template, request
from models import DepartmentModel
from common.utils.rights import permission_required, view_logging_required
from applications.view import index_bp
@index_bp.get('/dept/add')
@view_logging_required
@permission_required("admin:dept:add")
def add():
return render_template('view/system/department_add.html')
@index_bp.get('/dept/edit')
@view_logging_required
@permission_required("admin:dept:edit")
def edit():
dept_id = request.args.get("deptId", type=int)
dept = DepartmentModel.query.get(dept_id)
return render_template('view/system/department_edit.html', dept=dept)
-18
View File
@@ -1,18 +0,0 @@
from flask import Blueprint, render_template
document_bp = Blueprint('document', __name__)
@document_bp.route('/component/code/index.html')
def component_code():
return render_template('component/code/index.html')
@document_bp.route('/view/<document>/<file>')
def document_view(document, file):
return render_template(f'view/{document}/{file}')
@document_bp.get('/login.html')
def login_html():
return render_template('login.html')
-18
View File
@@ -1,18 +0,0 @@
from flask import render_template
from applications.view import index_bp
from common.utils.rights import view_logging_required, permission_required
@index_bp.get('/file')
@view_logging_required
@permission_required("admin:file:main")
def file_index():
return render_template('view/system/photo.html')
@index_bp.get('/file/photo/add')
@view_logging_required
@permission_required("admin:file:main")
def file_photo_add():
return render_template('view/system/photo_add.html')
-78
View File
@@ -1,78 +0,0 @@
from flask import Blueprint, redirect, url_for
from flask import send_file
from flask import session, render_template
from flask_login import current_user, logout_user, login_required
from common.gen_captcha import get_captcha_image
index_bp = Blueprint('index', __name__)
@index_bp.route('/')
@index_bp.route('/admin')
@login_required
def index():
return render_template('index.html')
@index_bp.route('/config/pear.config.json')
def pear_config():
return send_file('static/config/pear.config.json')
@index_bp.route('/admin/data/menu.json')
def menu():
return send_file('static/admin/data/menu.json')
@index_bp.route('/admin/data/message.json')
def message():
return send_file('static/admin/data/message.json')
@index_bp.route('/view/console/console1.html')
def console1():
# 控制后台
return render_template('view/console/console1.html')
@index_bp.route('/view/console/console2.html')
def console2():
# 数据分析
return render_template('view/console/console2.html')
@index_bp.route('/view/system/theme.html')
def theme():
# 酸爽翻倍
return render_template('view/system/theme.html')
@index_bp.route('/view/document/core.html')
def core():
# 酸爽翻倍
return render_template('view/document/core.html')
@index_bp.get('/passport/getCaptcha')
def get_captcha():
resp, code = get_captcha_image()
session["code"] = code
return resp
# 退出登录
@index_bp.post('/logout')
@login_required
def logout():
logout_user()
session.pop('permissions')
print({"message": "注销成功", "success": True})
return {"message": "注销成功", "success": True}
@index_bp.get('/login')
def login():
if current_user.is_authenticated:
return redirect(url_for('admin.index'))
return render_template('login.html')
-63
View File
@@ -1,63 +0,0 @@
from flask import Blueprint, request, render_template
from sqlalchemy import desc
from common.utils.http import table_api
from common.utils.rights import permission_required
from models import LogModel
logs_bp = Blueprint('logs', __name__, url_prefix='/logs')
@logs_bp.get('/login_log')
@permission_required("admin:log:main")
def login_log():
page = request.args.get('page', type=int)
limit = request.args.get('limit', type=int)
log_paginate = LogModel.query.filter_by(
url='/api/v1/passport/login').order_by(
desc(LogModel.create_at)).paginate(
page=page, per_page=limit, error_out=False)
data = [
{
'id': item.id,
'method': item.method,
'uid': item.uid,
'url': item.url,
'desc': item.desc,
'ip': item.ip,
'success': item.success,
'user_agent': item.user_agent,
'create_at': item.create_at.strftime('%Y-%m-%d %H:%M:%S'),
} for item in log_paginate.items
]
return table_api(result={'items': data,
'total': log_paginate.total, },
code=0)
@logs_bp.get('/access_log')
@permission_required("admin:log:main")
def operate_log():
page = request.args.get('page', type=int)
limit = request.args.get('limit', type=int)
log_paginate = LogModel.query.filter(
LogModel.url != '/api/v1/passport/login').order_by(
desc(LogModel.create_at)).paginate(
page=page, per_page=limit, error_out=False)
data = [
{
'id': item.id,
'method': item.method,
'uid': item.uid,
'url': item.url,
'desc': item.desc,
'ip': item.ip,
'success': item.success,
'user_agent': item.user_agent,
'create_at': item.create_at.strftime('%Y-%m-%d %H:%M:%S'),
} for item in log_paginate.items
]
return table_api(result={'items': data,
'total': log_paginate.total, },
code=0)
+13
View File
@@ -0,0 +1,13 @@
"""
前台公开蓝图:导航聚合页 / 分类详情页
URL 前缀:/site(避免与后台 / 路由冲突)
鉴权要求:无(任何用户都可访问,含未登录匿名用户)
注意:不要给这些视图加 @authorize 装饰器,否则会强制登录_required。
"""
from applications.view.public.nav import bp as nav_bp
# 把 nav 蓝图作为 public 包的对外入口
bp = nav_bp
+80
View File
@@ -0,0 +1,80 @@
"""
前台导航页视图(无需登录)。
URL 路径(挂在根路径下,便于匿名访问):
- GET /site 公开导航聚合首页(按分类展示所有启用的导航)
- GET /site/category/<name> 分类详情:展示指定分类下所有启用的导航
- GET /site/api/navs JSON 接口(供前端动态加载)
注意:
- 这些视图**不加** @authorize,避免 login_required 强制登录。
- 使用 url_prefix='/site' 避免与后台 / 路由冲突(后台 / 是登录后的工作台)。
"""
from collections import OrderedDict
from flask import Blueprint, render_template, request, jsonify
from applications.models import Nav
bp = Blueprint('public_nav', __name__, url_prefix='/site')
@bp.get('/')
def index():
"""
公开首页:按 category 分组聚合,渲染模板
"""
groups = _grouped_navs()
total = sum(len(items) for items in groups.values())
return render_template(
'public/index.html',
groups=groups,
total=total,
site_name='BWStudio · 导航',
)
@bp.get('/category/<string:name>')
def category_detail(name: str):
"""
分类详情页:列出指定分类下所有启用的导航
"""
items = (
Nav.query
.filter(Nav.category == name, Nav.status == 1)
.order_by(Nav.sort.asc(), Nav.id.asc())
.all()
)
return render_template(
'public/category.html',
category=name,
items=items,
site_name='BWStudio · 导航',
)
@bp.get('/api/navs')
def api_navs():
"""
JSON 接口:返回按分类聚合的导航数据
可用于首页卡片动态加载、第三方嵌入等
"""
return jsonify({
'success': True,
'msg': '请求成功',
'data': _grouped_navs(),
})
def _grouped_navs() -> "OrderedDict[str, list]":
"""按 category 分组,组内按 sort asc 排序"""
items = (
Nav.query
.filter(Nav.status == 1)
.order_by(Nav.category.asc(), Nav.sort.asc(), Nav.id.asc())
.all()
)
groups = OrderedDict()
for item in items:
groups.setdefault(item.category, []).append(item.to_dict())
return groups
-25
View File
@@ -1,25 +0,0 @@
from flask import render_template
from common.utils.rights import permission_required, view_logging_required
from models import RightModel
from applications.view import index_bp
@index_bp.get('/rights/power/<int:power_id>')
@view_logging_required
@permission_required("admin:power:edit")
def rights_edit(power_id):
power = RightModel.query.filter_by(id=power_id).first()
icon = str(power.icon).split()
if len(icon) == 2:
icon = icon[1]
else:
icon = None
return render_template('view/system/power_edit.html', power=power, icon=icon)
@index_bp.get('/rights/add')
@view_logging_required
@permission_required("admin:power:main")
def rights_add():
return render_template('view/system/power_add.html')
-31
View File
@@ -1,31 +0,0 @@
from flask import Blueprint, render_template
from common.utils.rights import permission_required, view_logging_required
from models import RoleModel
role_bp = Blueprint('role', __name__, url_prefix='/admin/role')
# 角色授权操作
@role_bp.get('/power/<int:role_id>')
@view_logging_required
@permission_required("admin:role:power")
def power(role_id):
return render_template('view/system/roles_power.html', role_id=role_id)
# 角色编辑
@role_bp.get('/edit/<int:role_id>')
@view_logging_required
@permission_required("admin:role:edit")
def role_editor(role_id):
role = RoleModel.query.filter_by(id=role_id).first()
return render_template('view/system/roles_edit.html', role=role)
@role_bp.get('/add')
@view_logging_required
@permission_required("admin:role:edit")
def role_add():
return render_template('view/system/roles_add.html')
+36
View File
@@ -0,0 +1,36 @@
from flask import Flask, Blueprint
from applications.view.system.dict import bp as dict_bp
from applications.view.system.file import bp as file_bp
from applications.view.system.index import bp as index_bp
from applications.view.system.log import bp as log_bp
from applications.view.system.mail import bp as mail_bp
from applications.view.system.monitor import bp as monitor_bp
from applications.view.system.nav import bp as nav_bp
from applications.view.system.passport import bp as passport_bp
from applications.view.system.power import bp as power_bp
from applications.view.system.rights import bp as right_bp
from applications.view.system.role import bp as role_bp
from applications.view.system.user import bp as user_bp
from applications.view.system.dept import bp as dept_bp
# 创建sys
system_bp = Blueprint('system', __name__, url_prefix='/system')
def register_system_bps(app: Flask):
# 在admin_bp下注册子蓝图
system_bp.register_blueprint(user_bp)
system_bp.register_blueprint(file_bp)
system_bp.register_blueprint(monitor_bp)
system_bp.register_blueprint(log_bp)
system_bp.register_blueprint(power_bp)
system_bp.register_blueprint(role_bp)
system_bp.register_blueprint(dict_bp)
system_bp.register_blueprint(mail_bp)
system_bp.register_blueprint(passport_bp)
system_bp.register_blueprint(right_bp)
system_bp.register_blueprint(dept_bp)
system_bp.register_blueprint(nav_bp)
app.register_blueprint(index_bp)
app.register_blueprint(system_bp)
+181
View File
@@ -0,0 +1,181 @@
from flask import Blueprint, render_template, request, jsonify
from applications.common import curd
from applications.common.utils import validate
from applications.common.utils.http import success_api, fail_api, table_api
from applications.common.utils.rights import authorize
from applications.common.utils.validate import str_escape
from applications.extensions import db
from applications.extensions.init_limit import limiter
from applications.models import Dept, User
from applications.schemas import DeptSchema
bp = Blueprint('dept', __name__, url_prefix='/dept')
@bp.get('/')
@authorize("system:dept:main", log=True)
def main():
return render_template('system/dept/main.html')
@bp.post('/data')
@limiter.limit("60 per minute")
@authorize("system:dept:main", log=True)
def data():
dept = Dept.query.order_by(Dept.sort).all()
data = DeptSchema(many=True).dump(dept)
# 创建一个字典,用于存储每个节点的子节点
tree = {}
for item in data:
item["children"] = []
tree[item["id"]] = item
# 构建树形结构
root_nodes = []
for item in data:
parent_id = item["parent_id"] if item["parent_id"] != 0 else None
if parent_id is None:
root_nodes.append(item)
else:
if parent_id in tree:
tree[parent_id]["children"].append(item)
return table_api(msg="请求成功", data=root_nodes)
@bp.get('/add')
@authorize("system:dept:add", log=True)
def add():
return render_template('system/dept/add.html')
@bp.get('/tree')
@authorize("system:dept:main", log=True)
def tree():
dept = Dept.query.order_by(Dept.sort).all()
power_data = curd.model_to_dicts(schema=DeptSchema, data=dept)
res = {
"status": {"code": 200, "message": "默认"},
"data": power_data
}
return jsonify(res)
@bp.post('/save')
@authorize("system:dept:add", log=True)
def save():
req_json = request.get_json(force=True)
dept = Dept(
parent_id=req_json.get('parentId'),
dept_name=str_escape(req_json.get('deptName')),
sort=str_escape(req_json.get('sort')),
leader=str_escape(req_json.get('leader')),
phone=str_escape(req_json.get('phone')),
email=str_escape(req_json.get('email')),
status=str_escape(req_json.get('status')),
address=str_escape(req_json.get('address'))
)
r = db.session.add(dept)
db.session.commit()
return success_api(msg="添加部门成功")
@bp.get('/edit')
@authorize("system:dept:edit", log=True)
def edit():
_id = request.args.get("deptId")
dept = curd.get_one_by_id(model=Dept, id=_id)
return render_template('system/dept/edit.html', dept=dept)
# 启用
@bp.put('/enable')
@authorize("system:dept:edit", log=True)
def enable():
id = request.get_json(force=True).get('deptId')
if id:
enable = 1
d = Dept.query.filter_by(id=id).update({"status": enable})
if d:
db.session.commit()
return success_api(msg="启用部门成功")
return fail_api(msg="出错啦")
return fail_api(msg="数据错误")
# 禁用
@bp.put('/disable')
@authorize("system:dept:edit", log=True)
def dis_enable():
id = request.get_json(force=True).get('deptId')
if id:
enable = 0
d = Dept.query.filter_by(id=id).update({"status": enable})
if d:
db.session.commit()
return success_api(msg="禁用部门成功")
return fail_api(msg="出错啦")
return fail_api(msg="数据错误")
@bp.put('/update')
@authorize("system:dept:edit", log=True)
def update():
json = request.get_json(force=True)
# id = json.get("deptId"),
id = str_escape(json.get("deptId"))
data = {
"dept_name": validate.str_escape(json.get("deptName")),
"sort": validate.str_escape(json.get("sort")),
"leader": validate.str_escape(json.get("leader")),
"phone": validate.str_escape(json.get("phone")),
"email": validate.str_escape(json.get("email")),
"status": validate.str_escape(json.get("status")),
"address": validate.str_escape(json.get("address"))
}
d = Dept.query.filter_by(id=id).update(data)
if not d:
return fail_api(msg="更新部门失败")
db.session.commit()
return success_api(msg="更新部门成功")
@bp.delete('/remove/<int:_id>')
@authorize("system:dept:remove", log=True)
def remove(_id):
d = Dept.query.filter_by(id=_id).delete()
if not d:
return fail_api(msg="删除部门失败")
User.query.filter_by(dept_id=_id).update({"dept_id": None})
db.session.commit()
return success_api(msg="删除部门成功")
# 批量删除
@bp.delete('/batchRemove')
@authorize("system:dept:remove", log=True)
def batch_remove():
ids = request.form.getlist('ids[]')
if not ids:
return fail_api(msg="未提供删除 ID")
for id in ids:
if not id.isdigit():
db.session.rollback()
return fail_api(msg="参数提供错误")
d = Dept.query.filter_by(id=id).delete()
if not d:
return fail_api(msg="删除部门失败")
User.query.filter_by(dept_id=id).update({"dept_id": None})
db.session.commit()
return success_api(msg="删除部门成功")
+299
View File
@@ -0,0 +1,299 @@
from flask import Blueprint, render_template, request, jsonify
from flask_wtf.csrf import validate_csrf
from wtforms.validators import ValidationError
from applications.common import curd
from applications.common.helper import ModelFilter
from applications.common.utils.http import table_api, success_api, fail_api
from applications.common.utils.rights import authorize
from applications.common.utils.validate import str_escape
from applications.extensions import db
from applications.extensions.init_limit import limiter
from applications.models import DictType, DictData
from applications.schemas import DictTypeOutSchema, DictDataOutSchema
bp = Blueprint('dict', __name__, url_prefix='/dict')
# 数据字典
@bp.get('/')
@authorize("system:dict:main")
def main():
return render_template('system/dict/main.html')
@bp.get('/dictType/data')
@limiter.limit("60 per minute")
@authorize("system:dict:main")
def dict_type_data():
# 获取请求参数
type_name = str_escape(request.args.get('typeName', type=str))
# 查询参数构造
mf = ModelFilter()
if type_name:
mf.vague(field_name="type_name", value=type_name)
# orm查询
# 使用分页获取data需要.items
dict_all = DictType.query.filter(mf.get_filter(DictType)).layui_paginate()
count = dict_all.total
data = curd.model_to_dicts(schema=DictTypeOutSchema, data=dict_all.items)
return table_api(data=data, count=count)
@bp.get('/dictType/add')
@authorize("system:dict:add", log=True)
def dict_type_add():
return render_template('system/dict/add.html')
@bp.post('/dictType/save')
@authorize("system:dict:add", log=True)
def dict_type_save():
req_json = request.get_json(force=True)
try:
validate_csrf(req_json.get("csrf_token"))
except ValidationError:
return fail_api(msg='非法请求')
data = {
'type_name': str_escape(req_json.get("typeName")),
'type_code': str_escape(req_json.get("typeCode")),
'enable': str_escape(req_json.get("enable")),
'description': str_escape(req_json.get("description"))
}
description = data['description']
del data['description']
if not all(data.values()):
return fail_api(msg="参数提供不足")
if description is not None:
data['description'] = description
d = DictType(**data)
db.session.add(d)
db.session.commit()
if d.id is None:
return fail_api(msg="增加失败")
return success_api(msg="增加成功")
# 编辑字典类型
@bp.get('/dictType/edit')
@authorize("system:dict:edit", log=True)
def dict_type_edit():
_id = request.args.get('dictTypeId', type=int)
dict_type = DictType.query.filter_by(id=_id).first()
return render_template('system/dict/edit.html', dict_type=dict_type)
# 编辑字典类型
@bp.put('/dictType/update')
@authorize("system:dict:edit", log=True)
def dict_type_update():
req_json = request.get_json(force=True)
id = str_escape(req_json.get("id"))
data = {
"description": str_escape(req_json.get("description")),
"enable": str_escape(req_json.get("enable")),
"type_code": str_escape(req_json.get("typeCode")),
"type_name": str_escape(req_json.get("typeName"))
}
if id is None:
return fail_api(msg="字典类型不存在")
description = data['description']
del data['description']
if not all(data.values()):
return fail_api(msg="参数提供不足")
data['description'] = description
DictType.query.filter_by(id=id).update(data)
db.session.commit()
return success_api(msg="更新成功")
# 启用字典
@bp.put('/dictType/enable')
@authorize("system:dict:edit", log=True)
def dict_type_enable():
_id = request.get_json(force=True).get('id')
if _id:
res = curd.enable_status(DictType, _id)
if not res:
return fail_api(msg="出错啦")
return success_api("启用成功")
return fail_api(msg="数据错误")
# 禁用字典
@bp.put('/dictType/disable')
@authorize("system:dict:edit", log=True)
def dict_type_dis_enable():
_id = request.get_json(force=True).get('id')
if _id:
res = curd.disable_status(DictType, _id)
if not res:
return fail_api(msg="出错啦")
return success_api("禁用成功")
return fail_api(msg="数据错误")
# 删除字典类型
@bp.delete('/dictType/remove/<int:_id>')
@authorize("system:dict:remove", log=True)
def dict_type_delete(_id):
DictData.query.filter_by(
type_code=DictType.query.filter_by(id=_id).first().type_code
).all()
res = DictType.query.filter_by(id=_id).delete()
if not res:
return fail_api(msg="删除失败")
db.session.commit()
return success_api(msg="删除成功")
@bp.get('/dictData/data')
@limiter.limit("60 per minute")
@authorize("system:dict:main", log=True)
def dict_code_data():
type_code = str_escape(request.args.get('typeCode', type=str))
dict_data = DictData.query.filter_by(type_code=type_code).layui_paginate()
count = dict_data.total
data = curd.model_to_dicts(schema=DictDataOutSchema, data=dict_data.items)
return table_api(data=data, count=count)
# 增加字典数据
@bp.get('/dictData/add')
@authorize("system:dict:add", log=True)
def dict_data_add():
type_code = request.args.get('typeCode', type=str)
return render_template('system/dict/data/add.html', type_code=type_code)
# 增加字典数据
@bp.post('/dictData/save')
@authorize("system:dict:add", log=True)
def dict_data_save():
req_json = request.get_json(force=True)
data_label = str_escape(req_json.get("dataLabel"))
data_value = str_escape(req_json.get("dataValue"))
enable = str_escape(req_json.get("enable"))
remark = str_escape(req_json.get("remark"))
type_code = str_escape(req_json.get("typeCode"))
d = DictData(data_label=data_label, data_value=data_value, enable=enable, remark=remark, type_code=type_code)
db.session.add(d)
db.session.commit()
if not d.id:
return jsonify(success=False, msg="增加失败")
return jsonify(success=True, msg="增加成功")
# 编辑字典数据
@bp.get('/dictData/edit')
@authorize("system:dict:edit", log=True)
def dict_data_edit():
_id = request.args.get('dataId', type=str)
dict_data = curd.get_one_by_id(DictData, _id)
return render_template('system/dict/data/edit.html', dict_data=dict_data)
# 编辑字典数据
@bp.put('/dictData/update')
@authorize("system:dict:edit", log=True)
def dict_data_update():
req_json = request.get_json(force=True)
id = req_json.get("dataId")
DictData.query.filter_by(id=id).update({
"data_label": str_escape(req_json.get("dataLabel")),
"data_value": str_escape(req_json.get("dataValue")),
"enable": str_escape(req_json.get("enable")),
"remark": str_escape(req_json.get("remark")),
"type_code": str_escape(req_json.get("typeCode"))
})
db.session.commit()
return success_api(msg="更新成功")
# 启用字典数据
@bp.put('/dictData/enable')
@authorize("system:dict:edit", log=True)
def dict_data_enable():
_id = request.get_json(force=True).get('dataId')
if _id:
res = curd.enable_status(model=DictData, id=_id)
if not res:
return fail_api(msg="出错啦")
return success_api(msg="启动成功")
return fail_api(msg="数据错误")
# 禁用字典数据
@bp.put('/dictData/disable')
@authorize("system:dict:edit", log=True)
def dict_data_disenable():
_id = request.get_json(force=True).get('dataId')
if _id:
res = curd.disable_status(model=DictData, id=_id)
if not res:
return fail_api(msg="出错啦")
return success_api(msg="禁用成功")
return fail_api(msg="数据错误")
# 删除字典数据
@bp.delete('dictData/remove/<int:id>')
@authorize("system:dict:remove", log=True)
def dict_data_delete(id):
res = curd.delete_one_by_id(model=DictData, id=id)
if not res:
return fail_api(msg="删除失败")
return success_api(msg="删除成功")
# 批量删除字典
@bp.delete('dictData/batchRemoveDictType')
@authorize("system:dict:remove", log=True)
def dict_type_batch_remove():
ids = request.form.getlist('ids[]')
for _id in ids:
DictData.query.filter_by(
type_code=DictType.query.filter_by(id=_id).first().type_code
).all()
res = DictType.query.filter_by(id=_id).delete()
if res == 0:
db.session.rollback()
return fail_api(msg="删除失败,请重试")
db.session.commit()
return success_api(msg="删除成功")
# 批量删除字典数据
@bp.delete('dictData/batchRemoveDictData')
@authorize("system:dict:remove", log=True)
def dict_data_batch_remove():
ids = request.form.getlist('ids[]')
for _id in ids:
res = curd.delete_one_by_id(model=DictData, id=_id)
if not res:
db.session.rollback()
return fail_api(msg="删除失败,请重试")
db.session.commit()
return success_api(msg="删除成功")
+85
View File
@@ -0,0 +1,85 @@
import os
from flask import Blueprint, request, render_template, jsonify, current_app
from applications.common.utils.http import fail_api, success_api, table_api
from applications.common.utils.rights import authorize
from applications.extensions import db
from applications.extensions.init_limit import limiter
from applications.models import Photo
from applications.common.utils import upload as upload_curd
bp = Blueprint('adminFile', __name__, url_prefix='/file')
# 图片管理
@bp.get('/')
@authorize("system:file:main")
def index():
return render_template('system/photo/photo.html')
# 图片数据
@bp.get('/table')
@limiter.limit("60 per minute")
@authorize("system:file:main")
def table():
page = request.args.get('page', type=int)
limit = request.args.get('limit', type=int)
data, count = upload_curd.get_photo(page=page, limit=limit)
return table_api(data=data, count=count)
# 上传
@bp.get('/upload')
@authorize("system:file:add", log=True)
def upload():
return render_template('system/photo/photo_add.html')
# 上传接口
@bp.post('/upload')
@authorize("system:file:add", log=True)
def upload_api():
if 'file' in request.files:
photo = request.files['file']
mime = request.files['file'].content_type
file_url = upload_curd.upload_one(photo=photo, mime=mime)
res = {
"msg": "上传成功",
"code": 0,
"success": True,
"data":
{"src": file_url}
}
return jsonify(res)
return fail_api()
# 图片删除
@bp.route('/delete', methods=['GET', 'POST'])
@authorize("system:file:delete", log=True)
def delete():
_id = request.form.get('id')
res = upload_curd.delete_photo_by_id(_id)
if res:
return success_api(msg="删除成功")
else:
return fail_api(msg="删除失败")
# 图片批量删除
@bp.route('/batchRemove', methods=['GET', 'POST'])
@authorize("system:file:delete", log=True)
def batch_remove():
ids = request.form.getlist('ids[]')
photo_name = Photo.query.filter(Photo.id.in_(ids)).all()
upload_url = current_app.config.get("UPLOADED_PHOTOS_DEST")
for p in photo_name:
os.remove(upload_url + '/' + p.name)
photo = Photo.query.filter(Photo.id.in_(ids)).delete(synchronize_session=False)
db.session.commit()
if photo:
return success_api(msg="删除成功")
else:
return fail_api(msg="删除失败")
+14
View File
@@ -0,0 +1,14 @@
from flask import Blueprint, render_template
from flask_login import login_required, current_user
bp = Blueprint('index', __name__, url_prefix='/')
# 首页
@bp.get('/')
@login_required
def index():
user = current_user
return render_template('system/index.html', user=user)
+43
View File
@@ -0,0 +1,43 @@
from flask import Blueprint, request, render_template
from sqlalchemy import desc
from applications.common.utils.http import table_api
from applications.common.utils.rights import authorize
from applications.models import AdminLog
from applications.schemas import LogOutSchema
from applications.common.curd import model_to_dicts
bp = Blueprint('log', __name__, url_prefix='/log')
# 日志管理
@bp.get('/')
@authorize("system:log:main")
def index():
return render_template('system/admin_log/main.html')
# 登录日志
@bp.get('/loginLog')
@authorize("system:log:main")
def login_log():
# orm查询
# 使用分页获取data需要.items
log = AdminLog.query.filter_by(
url='/system/passport/login'
).order_by(desc(AdminLog.create_time)).layui_paginate()
count = log.total
return table_api(data=model_to_dicts(schema=LogOutSchema, data=log.items), count=count)
# 操作日志
@bp.get('/operateLog')
@authorize("system:log:main")
def operate_log():
# orm查询
# 使用分页获取data需要.items
log = AdminLog.query.filter(
AdminLog.url != '/system/passport/login'
).order_by(
desc(AdminLog.create_time)).layui_paginate()
count = log.total
return table_api(data=model_to_dicts(schema=LogOutSchema, data=log.items), count=count)
+98
View File
@@ -0,0 +1,98 @@
from flask import Blueprint, render_template, request, current_app
from flask_login import current_user
from applications.common.curd import model_to_dicts
from applications.common.helper import ModelFilter
from applications.common.utils.http import table_api, fail_api, success_api
from applications.common.utils.rights import authorize
from applications.common.utils.validate import str_escape
from applications.extensions import db
from applications.extensions.init_limit import limiter
from applications.models import Mail
from applications.schemas import MailOutSchema
from applications.common.utils import mail
from applications.common.admin import admin_log
bp = Blueprint('adminMail', __name__, url_prefix='/mail')
# 用户管理
@bp.get('/')
@authorize("system:mail:main")
def main():
return render_template('system/mail/main.html')
# 用户分页查询
@bp.get('/data')
@limiter.limit("60 per minute")
@authorize("system:mail:main")
def data():
# 获取请求参数
receiver = str_escape(request.args.get("receiver"))
subject = str_escape(request.args.get('subject'))
content = str_escape(request.args.get('content'))
# 查询参数构造
mf = ModelFilter()
if receiver:
mf.contains(field_name="receiver", value=receiver)
if subject:
mf.contains(field_name="subject", value=subject)
if content:
mf.exact(field_name="content", value=content)
# orm查询
# 使用分页获取data需要.items
mail = Mail.query.filter(mf.get_filter(Mail)).layui_paginate()
count = mail.total
# 返回api
return table_api(data=model_to_dicts(schema=MailOutSchema, data=mail.items), count=count)
# 用户增加
@bp.get('/add')
@authorize("system:mail:add", log=True)
def add():
return render_template('system/mail/add.html')
@bp.post('/save')
@authorize("system:mail:add", log=True)
def save():
req_json = request.get_json(force=True)
receiver = str_escape(req_json.get("receiver"))
subject = str_escape(req_json.get('subject'))
content = str_escape(req_json.get('content'))
user_id = current_user.id
try:
if mail.add(receiver=receiver, subject=subject, content=content, user_id=user_id):
return success_api(msg="增加成功")
except Exception as e:
current_app.log_exception(e)
admin_log(request, False, desc="发送日志失败:" + str(e))
return success_api(msg="发送失败,请检查日志。")
# 删除用户
@bp.delete('/remove/<int:id>')
@authorize("system:mail:remove", log=True)
def delete(id):
res = Mail.query.filter_by(id=id).delete()
if not res:
return fail_api(msg="删除失败")
db.session.commit()
return success_api(msg="删除成功")
# 批量删除
@bp.delete('/batchRemove')
@authorize("system:mail:remove", log=True)
def batch_remove():
ids = request.form.getlist('ids[]')
for id in ids:
res = Mail.query.filter_by(id=id).delete()
if not res:
return fail_api(msg="批量删除失败")
db.session.commit()
return success_api(msg="批量删除成功")
+170
View File
@@ -0,0 +1,170 @@
import os
import re
import sys
import time
import psutil
import platform
from datetime import datetime
from flask import Blueprint, render_template
from applications.common.utils.http import table_api, success_api
from applications.common.utils.rights import authorize
from applications.common.utils.cache import cache_auto_internal
bp = Blueprint('adminMonitor', __name__, url_prefix='/monitor')
def get_disk_partitions_list():
disk_partitions_list = []
# 判断是否在容器中
if not os.path.exists('/.dockerenv'):
disk_partitions = psutil.disk_partitions()
for i in disk_partitions:
try:
a = psutil.disk_usage(i.mountpoint)
except PermissionError:
continue
disk_partitions_dict = {
'device': i.device,
'fstype': i.fstype,
'total': a.total,
'used': a.used,
'free': a.free,
'percent': a.percent
}
disk_partitions_list.append(disk_partitions_dict)
else:
try:
usage = psutil.disk_usage('/')
except PermissionError:
pass
disk_partitions_list.append({
'device': '/', # 设备名称(根文件系统)
'fstype': psutil.disk_partitions()[0].fstype, # 文件系统类型
'total': usage.total, # 总容量(字节)
'used': usage.used, # 已用空间(字节)
'free': usage.free, # 可用空间(字节)
'percent': usage.percent # 使用百分比
})
return disk_partitions_list
def get_basic_info():
# 主机名称
hostname = platform.node()
# 系统版本
system_version = platform.platform()
# Python 版本
python_version = platform.python_version()
# 开机时间
boot_time = datetime.fromtimestamp(psutil.boot_time()).replace(microsecond=0)
up_time = datetime.now().replace(microsecond=0) - boot_time
up_time_list = re.split(r':', str(up_time))
up_time_format = "{} 小时 {} 分钟 {}".format(up_time_list[0], up_time_list[1], up_time_list[2])
up_time_format = up_time_format.replace("days,", "")
return {
'hostname': hostname,
'system_version': system_version,
'python_version': python_version,
'boot_time': boot_time,
'up_time_format': up_time_format
}
# 系统监控
@bp.get('/')
@authorize("system:monitor:main")
def main():
# 当前时间
time_now = time.strftime('%H:%M:%S ', time.localtime(time.time()))
return render_template(
'system/monitor.html',
time_now=time_now,
**get_basic_info()
)
# 图表 api
@bp.get('/polling')
@authorize("system:monitor:main")
def ajax_polling():
# 获取 CPU 核心数
cpu_count = cache_auto_internal('cpu_count', lambda: psutil.cpu_count(), expired=999999999)
# 获取 CPU 使用率
cpus_percent = cache_auto_internal('cpus_percent',
lambda: psutil.cpu_percent(interval=1, percpu=False),
expired=5)
# 每个 CPU 的使用率
cpu_percent_per_core = cache_auto_internal('cpu_percent_per_core',
lambda: list(enumerate(psutil.cpu_percent(interval=1, percpu=True))),
expired=5)
# 获取空闲率、等待率
cpu_times_percent = cache_auto_internal('cpu_times_percent',
lambda: psutil.cpu_times_percent(interval=1, percpu=False),
expired=5)
# 内存信息
memory_information = cache_auto_internal('memory_information',
psutil.virtual_memory,
expired=5)
# 硬盘信息
disk_partitions_list = cache_auto_internal('disk_partitions_list',
get_disk_partitions_list,
expired=5)
# 系统信息
basic_info = cache_auto_internal('basic_info',
get_basic_info,
expired=5)
memory_usage = memory_information.percent
memory_used = memory_information.used
memory_total = memory_information.total
memory_free = memory_information.free
cpu_idle_percent = cpu_times_percent.idle
if hasattr(cpu_times_percent, 'iowait'):
cpu_wait_percent = cpu_times_percent.iowait
else:
cpu_wait_percent = "-"
return table_api(msg="请求成功",
count=0,
data={
'cpu_count': cpu_count,
'cpus_percent': cpus_percent,
'cpu_idle_percent': cpu_idle_percent,
'cpu_wait_percent': cpu_wait_percent,
'cpu_percent_per_core': cpu_percent_per_core,
'memory_used': memory_used,
'memory_total': memory_total,
'memory_free': memory_free,
'memory_usage': memory_usage,
'disk_partitions_list': disk_partitions_list,
'time_now': time.strftime('%H:%M:%S', time.localtime(time.time())),
'basic_info': basic_info
})
# 关闭程序
@bp.get('/kill')
@authorize("system:monitor:main")
def kill():
# 注:若是多 worker 则不生效
return success_api(msg="关闭命令已发送,请修改代码以生效。")
for proc in psutil.process_iter():
if proc.pid == os.getpid():
proc.kill()
sys.exit(1)
+142
View File
@@ -0,0 +1,142 @@
"""
后台导航管理视图(需要登录 + 权限码 system:nav:*
提供 Nav 模型的 CRUD 接口,供后台管理员维护前台公开导航。
"""
from flask import Blueprint, render_template, request
from flask_login import current_user
from applications.common.utils.http import table_api, fail_api, success_api
from applications.common.utils.rights import authorize
from applications.common.utils.validate import str_escape
from applications.extensions import db
from applications.extensions.init_limit import limiter
from applications.models import Nav
from applications.schemas import NavManageSchema
bp = Blueprint('nav', __name__, url_prefix='/nav')
@bp.get('/')
@authorize("system:nav:main")
def main():
"""导航管理主页"""
return render_template('system/nav/main.html')
@bp.get('/data')
@limiter.limit("60 per minute")
@authorize("system:nav:main")
def data():
"""表格数据接口"""
keyword = str_escape(request.args.get('keyword', type=str))
category = str_escape(request.args.get('category', type=str))
query = Nav.query
if keyword:
query = query.filter(
db.or_(
Nav.title.contains(keyword),
Nav.url.contains(keyword),
Nav.description.contains(keyword),
)
)
if category:
query = query.filter(Nav.category == category)
items = query.order_by(Nav.category.asc(), Nav.sort.asc(), Nav.id.asc()).all()
data = NavManageSchema(many=True).dump(items)
return table_api(msg="请求成功", data=data, count=len(data))
@bp.get('/add')
@authorize("system:nav:add", log=True)
def add():
return render_template('system/nav/add.html')
@bp.post('/save')
@authorize("system:nav:add", log=True)
def save():
req = request.get_json(force=True)
try:
nav = Nav(
category=str_escape(req.get('category')),
title=str_escape(req.get('title')),
url=str_escape(req.get('url')),
description=str_escape(req.get('description')),
icon=str_escape(req.get('icon')) or 'layui-icon-link',
sort=int(req.get('sort') or 0),
status=int(req.get('status') or 1),
is_external=int(req.get('isExternal') or 1),
create_by=current_user.username if current_user.is_authenticated else 'admin',
)
db.session.add(nav)
db.session.commit()
except Exception as e:
db.session.rollback()
return fail_api(msg=f"新增失败:{e}")
return success_api(msg="新增成功")
@bp.get('/edit')
@authorize("system:nav:edit", log=True)
def edit():
nav_id = request.args.get("navId", type=int)
nav = Nav.query.get(nav_id)
if not nav:
return fail_api(msg="导航不存在")
return render_template('system/nav/edit.html', nav=nav)
@bp.put('/update')
@authorize("system:nav:edit", log=True)
def update():
req = request.get_json(force=True)
nav_id = req.get('id')
nav = Nav.query.get(nav_id)
if not nav:
return fail_api(msg="导航不存在")
try:
nav.category = str_escape(req.get('category')) or nav.category
nav.title = str_escape(req.get('title')) or nav.title
nav.url = str_escape(req.get('url')) or nav.url
nav.description = str_escape(req.get('description'))
nav.icon = str_escape(req.get('icon')) or 'layui-icon-link'
nav.sort = int(req.get('sort') or 0)
nav.status = int(req.get('status') or 1)
nav.is_external = int(req.get('isExternal') or 1)
db.session.commit()
except Exception as e:
db.session.rollback()
return fail_api(msg=f"更新失败:{e}")
return success_api(msg="更新成功")
@bp.delete('/remove/<int:_id>')
@authorize("system:nav:remove", log=True)
def remove(_id):
nav = Nav.query.get(_id)
if not nav:
return fail_api(msg="导航不存在")
db.session.delete(nav)
db.session.commit()
return success_api(msg="删除成功")
@bp.put('/enable')
@authorize("system:nav:edit", log=True)
def enable():
nav_id = request.get_json(force=True).get('navId')
Nav.query.filter_by(id=nav_id).update({"status": 1})
db.session.commit()
return success_api(msg="已启用")
@bp.put('/disable')
@authorize("system:nav:edit", log=True)
def disable():
nav_id = request.get_json(force=True).get('navId')
Nav.query.filter_by(id=nav_id).update({"status": 0})
db.session.commit()
return success_api(msg="已禁用")
+91
View File
@@ -0,0 +1,91 @@
from flask import Blueprint, session, redirect, url_for, render_template, request
from flask_login import current_user, login_user, login_required, logout_user
from applications.common.admin import get_captcha, login_log
from applications.common.utils.http import fail_api, success_api
from applications.models import User
bp = Blueprint('passport', __name__, url_prefix='/passport')
# 获取验证码
@bp.get('/getCaptcha')
def captcha():
resp, code = get_captcha()
session["code"] = code
return resp
# 登录
@bp.get('/login')
def login():
if current_user.is_authenticated:
return redirect(url_for('index.index'))
return render_template('system/login.html')
# 登录
@bp.post('/login')
def login_post():
req = request.form
username = req.get('username')
password = req.get('password')
remember = bool(req.get('remember-me'))
code = req.get('captcha').__str__().lower()
if not username or not password or not code:
return fail_api(msg="用户名或密码没有输入")
s_code = session.get("code", None)
session["code"] = None
if not all([code, s_code]):
return fail_api(msg="参数错误")
if code != s_code:
return fail_api(msg="验证码错误")
user = User.query.filter_by(username=username).first()
if not user:
return fail_api(msg="不存在的用户")
if user.enable == 0:
return fail_api(msg="用户被暂停使用")
if username == user.username and user.validate_password(password):
# 登录
login_user(user, remember=remember)
# 记录登录日志
login_log(request, uid=user.id, is_access=True)
# 授权路由存入session
role = current_user.role
user_power = []
for i in role:
if i.enable == 0:
continue
for p in i.power:
if p.enable == 0:
continue
user_power.append(p.code)
session['permissions'] = user_power
# # 角色存入session
# roles = []
# for role in current_user.role.all():
# roles.append(role.id)
# session['role'] = [roles]
return success_api(msg="登录成功")
login_log(request, uid=user.id, is_access=False)
return fail_api(msg="用户名或密码错误")
# 退出登录
@bp.post('/logout')
@login_required
def logout():
logout_user()
if 'permissions' in session:
session.pop('permissions')
return success_api(msg="注销成功")
+203
View File
@@ -0,0 +1,203 @@
from flask import Blueprint, render_template, request, jsonify
from applications.common import curd
from applications.common.utils.http import success_api, fail_api, table_api
from applications.common.utils.rights import authorize
from applications.common.utils.validate import str_escape
from applications.extensions import db
from applications.extensions.init_limit import limiter
from applications.models import Power
from applications.schemas import PowerOutSchema2
from applications.schemas.admin_power import PowerSchema
bp = Blueprint('power', __name__, url_prefix='/power')
@bp.get('/')
@authorize("system:power:main")
def index():
return render_template('system/power/main.html')
@bp.post('/data')
@limiter.limit("60 per minute")
@authorize("system:power:main")
def data():
power = Power.query.all()
data = PowerSchema(many=True).dump(power)
# 创建一个字典,用于存储每个节点的子节点
tree = {}
for item in data:
item["children"] = []
tree[item["id"]] = item
# 构建树形结构
root_nodes = []
for item in data:
parent_id = item["parent_id"] if item["parent_id"] != 0 else None
if parent_id is None:
root_nodes.append(item)
else:
if parent_id in tree:
tree[parent_id]["children"].append(item)
return table_api(msg="请求成功", data=root_nodes)
@bp.get('/add')
@authorize("system:power:add", log=True)
def add():
return render_template('system/power/add.html')
@bp.get('/selectParent')
@authorize("system:power:main", log=True)
def select_parent():
power = Power.query.all()
res = curd.model_to_dicts(schema=PowerOutSchema2, data=power)
res.append({"powerId": 0, "powerName": "顶级权限", "parentId": -1})
res = {
"status": {"code": 200, "message": "默认"},
"data": res
}
return jsonify(res)
# 增加
@bp.post('/save')
@authorize("system:power:add", log=True)
def save():
req = request.get_json(force=True)
data = {
"icon": str_escape(req.get("icon")),
"open_type": str_escape(req.get("openType")),
"parent_id": str_escape(req.get("parentId")),
"code": str_escape(req.get("powerCode")),
"name": str_escape(req.get("powerName")),
"type": str_escape(req.get("powerType")),
"url": str_escape(req.get("powerUrl")),
"sort": str_escape(req.get("sort"))
}
if not data['sort'].isdigit():
return fail_api(msg="参数 sort 需为整数")
power = Power(**data)
db.session.add(power)
db.session.commit()
return success_api(msg="权限添加成功")
# 权限编辑
@bp.get('/edit/<int:_id>')
@authorize("system:power:edit", log=True)
def edit(_id):
power = curd.get_one_by_id(Power, _id)
icon = str(power.icon).split()
if len(icon) == 2:
icon = icon[1]
else:
icon = None
return render_template('system/power/edit.html', power=power, icon=icon)
# 权限更新
@bp.put('/update')
@authorize("system:power:edit", log=True)
def update():
req_json = request.get_json(force=True)
id = request.get_json(force=True).get("powerId")
data = {
"icon": str_escape(req_json.get("icon")),
"open_type": str_escape(req_json.get("openType")),
"parent_id": str_escape(req_json.get("parentId")),
"code": str_escape(req_json.get("powerCode")),
"name": str_escape(req_json.get("powerName")),
"type": str_escape(req_json.get("powerType")),
"url": str_escape(req_json.get("powerUrl")),
"sort": str_escape(req_json.get("sort"))
}
if not data['sort'].isdigit():
return fail_api(msg="参数 sort 需为整数")
res = Power.query.filter_by(id=id).update(data)
db.session.commit()
if not res:
return fail_api(msg="更新权限失败")
return success_api(msg="更新权限成功")
# 启用权限
@bp.put('/enable')
@authorize("system:power:edit", log=True)
def enable():
_id = request.get_json(force=True).get('powerId')
if _id:
res = curd.enable_status(Power, _id)
if not res:
return fail_api(msg="出错啦")
return success_api(msg="启用成功")
return fail_api(msg="数据错误")
# 禁用权限
@bp.put('/disable')
@authorize("system:power:edit", log=True)
def dis_enable():
_id = request.get_json(force=True).get('powerId')
if id:
res = curd.disable_status(Power, _id)
if not res:
return fail_api(msg="出错啦")
return success_api(msg="禁用成功")
return fail_api(msg="数据错误")
# 权限删除
@bp.delete('/remove/<int:id>')
@authorize("system:power:remove", log=True)
def remove(id):
power = Power.query.filter_by(id=id).first()
if power:
power.role = []
r = Power.query.filter_by(id=id).delete()
db.session.commit()
if r:
return success_api(msg="删除成功")
else:
return fail_api(msg="删除失败")
# 批量删除
@bp.delete('/batchRemove')
@authorize("system:power:remove", log=True)
def batch_remove():
ids = request.form.getlist('ids[]')
if not ids:
return fail_api(msg="未提供删除 ID")
for id in ids:
if not id.isdigit():
db.session.rollback()
return fail_api(msg="参数提供错误")
id = int(id)
power = Power.query.filter_by(id=id).first()
if power:
# 清空关联的角色(如果需要)
power.role = []
db.session.delete(power)
db.session.commit()
return success_api(msg="批量删除成功")
+265
View File
@@ -0,0 +1,265 @@
import copy
from collections import OrderedDict
from flask import jsonify, current_app, Blueprint, render_template
from flask_login import login_required, current_user
from ...common.utils.http import table_api
from ...models import Power
from ...schemas import PowerOutSchema
bp = Blueprint('rights', __name__, url_prefix='/rights')
# 渲染配置
@bp.get('/configs')
@login_required
def configs():
# 网站配置
config = dict(logo={
# 网站名称
"title": current_app.config.get("SYSTEM_NAME"),
# 网站图标
"image": "/static/system/admin/images/logo.png"
# 菜单配置
}, menu={
# 菜单数据来源
"data": "/system/rights/menu",
"collaspe": False,
# 是否同时只打开一个菜单目录
"accordion": True,
"method": "GET",
# 是否开启多系统菜单模式
"control": False,
# 顶部菜单宽度 PX
"controlWidth": 500,
# 默认选中的菜单项
"select": "0",
# 是否开启异步菜单,false 时 data 属性设置为菜单数据,false 时为 json 文件或后端接口
"async": True
}, tab={
# 是否开启多选项卡
"enable": True,
# 切换选项卡时,是否刷新页面状态
"keepState": True,
# 是否开启 Tab 记忆
"session": True,
# 预加载
"preload": False,
# 最大可打开的选项卡数量
"max": 30,
"index": {
# 标识 ID , 建议与菜单项中的 ID 一致
"id": "10",
# 页面地址
"href": "/system/rights/welcome",
# 标题
"title": "首页"
}
}, theme={
# 默认主题色,对应 colors 配置中的 ID 标识
"defaultColor": "2",
# 默认的菜单主题 dark-theme 黑 / light-theme 白
"defaultMenu": "dark-theme",
# 是否允许用户切换主题,false 时关闭自定义主题面板
"allowCustom": True
}, colors=[{
"id": "1",
"color": "#2d8cf0"
},
{
"id": "2",
"color": "#5FB878"
},
{
"id": "3",
"color": "#1E9FFF"
}, {
"id": "4",
"color": "#FFB800"
}, {
"id": "5",
"color": "darkgray"
}
], links=current_app.config.get("SYSTEM_PANEL_LINKS"), other={
# 主页动画时长
"keepLoad": 0,
# 布局顶部主题
"autoHead": False
}, header={
'message': '/system/rights/message'
})
return jsonify(config)
# 消息
@bp.get('/message')
@login_required
def message():
return dict(code=200,
data=[
{
"id": 1,
"title": "通知",
"children": [
{
"id": 11,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
"title": "你收到了 14 份新周报",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
},
{
"id": 12,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
"title": "曲妮妮 已通过第三轮面试",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
},
{
"id": 11,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png",
"title": "可以区分多种通知类型",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
},
{
"id": 12,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
"title": "左侧图标用于区分不同的类型",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
},
{
"id": 11,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
"title": "内容不要超过两行字",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
}
]
},
{
"id": 2,
"title": "消息",
"children": [
{
"id": 11,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
"title": "你收到了 14 份新周报",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
},
{
"id": 12,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
"title": "曲妮妮 已通过第三轮面试",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
},
{
"id": 11,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png",
"title": "可以区分多种通知类型",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
},
{
"id": 12,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png",
"title": "左侧图标用于区分不同的类型",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
},
{
"id": 11,
"avatar": "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
"title": "内容不要超过两行字",
"context": "这是消息内容。",
"form": "就眠仪式",
"time": "刚刚"
}
]
},
{
"id": 3,
"title": "代办",
"children": []
}
])
# 菜单
@bp.get('/menu')
@login_required
def menu():
if current_user.username != current_app.config.get("SUPERADMIN"):
role = current_user.role
powers = []
for i in role:
# 如果角色没有被启用就直接跳过
if i.enable == 0:
continue
# 变量角色用户的权限
for p in i.power:
# 如果权限关闭了就直接跳过
if p.enable == 0:
continue
# 一二级菜单
if int(p.type) in [0, 1] and p not in powers:
powers.append(p)
power_schema = PowerOutSchema(many=True) # 用已继承 ma.ModelSchema 类的自定制类生成序列化类
power_dict = power_schema.dump(powers) # 生成可序列化对象
power_dict.sort(key=lambda x: (x['parent_id'], x['id']), reverse=True)
menu_dict = OrderedDict()
for _dict in power_dict:
if _dict['id'] in menu_dict:
# 当前节点添加子节点
_dict['children'] = copy.deepcopy(menu_dict[_dict['id']])
_dict['children'].sort(key=lambda item: item['sort'])
# 删除子节点
del menu_dict[_dict['id']]
if _dict['parent_id'] not in menu_dict:
menu_dict[_dict['parent_id']] = [_dict]
else:
menu_dict[_dict['parent_id']].append(_dict)
return jsonify(sorted(menu_dict.get(0), key=lambda item: item['sort']))
else:
powers = Power.query.filter(Power.enable == 1).all()
power_schema = PowerOutSchema(many=True) # 用已继承 ma.ModelSchema 类的自定制类生成序列化类
power_dict = power_schema.dump(powers) # 生成可序列化对象
power_dict.sort(key=lambda x: (x['parent_id'], x['id']), reverse=True)
menu_dict = OrderedDict()
for _dict in power_dict:
if _dict['id'] in menu_dict:
# 当前节点添加子节点
_dict['children'] = copy.deepcopy(menu_dict[_dict['id']])
_dict['children'].sort(key=lambda item: item['sort'])
# 删除子节点
del menu_dict[_dict['id']]
if _dict['parent_id'] not in menu_dict:
menu_dict[_dict['parent_id']] = [_dict]
else:
menu_dict[_dict['parent_id']].append(_dict)
return jsonify(sorted(menu_dict.get(0), key=lambda item: item['sort']))
# 控制台页面
@bp.get('/welcome')
@login_required
def welcome():
return render_template('system/analysis/main.html')
+208
View File
@@ -0,0 +1,208 @@
from flask import Blueprint, render_template, request, jsonify
from flask_login import login_required
from applications.common.curd import model_to_dicts, enable_status, disable_status, get_one_by_id
from applications.common.utils.http import table_api, success_api, fail_api
from applications.common.utils.rights import authorize
from applications.common.utils.validate import str_escape
from applications.extensions import db
from applications.extensions.init_limit import limiter
from applications.models import Role, Power, User
from applications.schemas import RoleOutSchema, PowerOutSchema2
bp = Blueprint('role', __name__, url_prefix='/role')
# 用户管理
@bp.get('/')
@authorize("system:role:main")
def main():
return render_template('system/role/main.html')
# 表格数据
@bp.get('/data')
@limiter.limit("60 per minute")
@authorize("system:role:main")
def table():
role_name = str_escape(request.args.get('roleName', type=str))
role_code = str_escape(request.args.get('roleCode', type=str))
filters = []
if role_name:
filters.append(Role.name.contains(role_name))
if role_code:
filters.append(Role.code.contains(role_code))
roles = Role.query.filter(*filters).layui_paginate()
return table_api(data=RoleOutSchema(many=True).dump(roles), count=roles.total)
# 角色增加
@bp.get('/add')
@authorize("system:role:add", log=True)
def add():
return render_template('system/role/add.html')
# 角色增加
@bp.post('/save')
@authorize("system:role:add", log=True)
def save():
req = request.get_json(force=True)
data = {
"details": str_escape(req.get("details")),
"enable": str_escape(req.get("enable")),
"code": str_escape(req.get("roleCode")), # 角色标识
"name": str_escape(req.get("roleName")), # 角色名称
"sort": str_escape(req.get("sort"))
}
details = data.get('details')
del data['details']
if not all(data.values()):
return fail_api(msg="参数不足")
data['details'] = details
# 参数效验
if not data['sort'].isdigit():
return fail_api(msg="参数 sort 需为整数")
role = Role(**data)
db.session.add(role)
db.session.commit()
return success_api(msg="添加角色成功")
# 角色授权
@bp.get('/power/<int:_id>')
@authorize("system:role:power", log=True)
def power(_id):
return render_template('system/role/power.html', id=_id)
# 获取角色权限
@bp.get('/getRolePower/<int:id>')
@authorize("system:role:main", log=True)
def get_role_power(id):
role = Role.query.filter_by(id=id).first()
check_powers = role.power
check_powers_list = []
for cp in check_powers:
check_powers_list.append(cp.id)
powers = Power.query.all()
power_schema = PowerOutSchema2(many=True) # 用已继承ma.ModelSchema类的自定制类生成序列化类
output = power_schema.dump(powers) # 生成可序列化对象
for i in output:
if int(i.get("powerId")) in check_powers_list:
i["checkArr"] = "1"
else:
i["checkArr"] = "0"
res = {
"data": output,
"status": {"code": 200, "message": "默认"}
}
return jsonify(res)
# 保存角色权限
@bp.put('/saveRolePower')
@authorize("system:role:edit", log=True)
def save_role_power():
req_form = request.form
power_ids = req_form.get("powerIds")
power_list = power_ids.split(',')
role_id = req_form.get("roleId")
role = Role.query.filter_by(id=role_id).first()
if not role:
return fail_api(msg="角色不存在")
powers = Power.query.filter(Power.id.in_(power_list)).all()
role.power = powers
db.session.commit()
return success_api(msg="授权成功")
# 角色编辑
@bp.get('/edit/<int:id>')
@authorize("system:role:edit", log=True)
def edit(id):
r = get_one_by_id(model=Role, id=id)
return render_template('system/role/edit.html', role=r)
# 更新角色
@bp.put('/update')
@authorize("system:role:edit", log=True)
def update():
req_json = request.get_json(force=True)
id = req_json.get("roleId")
data = {
"code": str_escape(req_json.get("roleCode")),
"name": str_escape(req_json.get("roleName")),
"sort": str_escape(req_json.get("sort")),
"enable": str_escape(req_json.get("enable")),
"details": str_escape(req_json.get("details"))
}
if not data['enable'].isdigit():
return fail_api(msg="参数 enable 需为整数")
if not data['sort'].isdigit():
return fail_api(msg="参数 sort 需为整数")
role = Role.query.filter_by(id=id).update(data)
db.session.commit()
if not role:
return fail_api(msg="更新角色失败")
return success_api(msg="更新角色成功")
# 启用用户
@bp.put('/enable')
@authorize("system:role:edit", log=True)
def enable():
_id = request.get_json(force=True).get('roleId')
if _id:
res = enable_status(Role, _id)
if not res:
return fail_api(msg="出错啦")
return success_api(msg="启动成功")
return fail_api(msg="数据错误")
# 禁用用户
@bp.put('/disable')
@authorize("system:role:edit", log=True)
def dis_enable():
_id = request.get_json(force=True).get('roleId')
if _id:
res = disable_status(Role, _id)
if not res:
return fail_api(msg="出错啦")
return success_api(msg="禁用成功")
return fail_api(msg="数据错误")
# 角色删除
@bp.delete('/remove/<int:id>')
@authorize("system:role:remove", log=True)
def remove(id):
role = Role.query.filter_by(id=id).first()
if not role:
return fail_api(msg="角色不存在")
# 删除该角色的权限和用户
role.power = []
role.user = []
r = Role.query.filter_by(id=id).delete()
db.session.commit()
if not r:
return fail_api(msg="角色删除失败")
return success_api(msg="角色删除成功")
+236
View File
@@ -0,0 +1,236 @@
from flask import Blueprint, render_template, request
from flask_login import login_required, current_user
from sqlalchemy import desc
from applications.common import curd
from applications.common.curd import enable_status, disable_status
from applications.common.utils.http import table_api, fail_api, success_api
from applications.common.utils.rights import authorize
from applications.common.utils.validate import str_escape
from applications.extensions import db
from applications.extensions.init_limit import limiter
from applications.models import Role, Dept
from applications.models import User, AdminLog
bp = Blueprint('user', __name__, url_prefix='/user')
# 用户管理
@bp.get('/')
@authorize("system:user:main")
def main():
return render_template('system/user/main.html')
# 用户分页查询
@bp.get('/data')
@limiter.limit("60 per minute")
@authorize("system:user:main")
def data():
# 获取请求参数
real_name = str_escape(request.args.get('realname', type=str))
username = str_escape(request.args.get('username', type=str))
dept_id = request.args.get('deptId', type=int)
filters = []
if real_name:
filters.append(User.realname.contains(real_name))
if username:
filters.append(User.username.contains(username))
if dept_id:
filters.append(User.dept_id == dept_id)
# print(*filters)
query = db.session.query(
User,
Dept
).filter(*filters).outerjoin(Dept, User.dept_id == Dept.id).layui_paginate()
return table_api(
data=[{
'id': user.id,
'username': user.username,
'realname': user.realname,
'enable': user.enable,
'create_at': user.create_at,
'update_at': user.update_at,
'dept_name': dept.dept_name if dept else None
} for user, dept in query.items],
count=query.total)
# 用户增加
@bp.get('/add')
@authorize("system:user:add", log=True)
def add():
roles = Role.query.all()
return render_template('system/user/add.html', roles=roles)
@bp.post('/save')
@authorize("system:user:add", log=True)
def save():
req_json = request.get_json(force=True)
a = req_json.get("roleIds")
username = str_escape(req_json.get('username'))
real_name = str_escape(req_json.get('realName'))
password = str_escape(req_json.get('password'))
dept_id = str_escape(req_json.get('deptId'))
role_ids = a.split(',')
if not username or not real_name or not password:
return fail_api(msg="账号姓名密码不得为空")
if bool(User.query.filter_by(username=username).count()):
return fail_api(msg="用户已经存在")
user = User(username=username, realname=real_name, enable=1, dept_id=dept_id)
user.set_password(password)
db.session.add(user)
roles = Role.query.filter(Role.id.in_(role_ids)).all()
for r in roles:
user.role.append(r)
db.session.commit()
return success_api(msg="增加成功")
# 删除用户
@bp.delete('/remove/<int:id>')
@authorize("system:user:remove", log=True)
def delete(id):
user = User.query.filter_by(id=id).first()
user.role = []
res = User.query.filter_by(id=id).delete()
db.session.commit()
if not res:
return fail_api(msg="删除失败")
return success_api(msg="删除成功")
# 编辑用户
@bp.get('/edit/<int:id>')
@authorize("system:user:edit", log=True)
def edit(id):
user = curd.get_one_by_id(User, id)
roles = Role.query.all()
checked_roles = []
for r in user.role:
checked_roles.append(r.id)
return render_template('system/user/edit.html', user=user, roles=roles, checked_roles=checked_roles)
# 编辑用户
@bp.put('/update')
@authorize("system:user:edit", log=True)
def update():
req_json = request.get_json(force=True)
a = str_escape(req_json.get("roleIds"))
id = str_escape(req_json.get("userId"))
username = str_escape(req_json.get('username'))
real_name = str_escape(req_json.get('realName'))
dept_id = str_escape(req_json.get('deptId'))
role_ids = a.split(',')
User.query.filter_by(id=id).update({'username': username, 'realname': real_name, 'dept_id': dept_id})
u = User.query.filter_by(id=id).first()
roles = Role.query.filter(Role.id.in_(role_ids)).all()
u.role = roles
db.session.commit()
return success_api(msg="更新成功")
# 个人中心
@bp.get('/center')
@login_required
def center():
user_info = current_user
user_logs = AdminLog.query.filter_by(url='/passport/login').filter_by(uid=current_user.id).order_by(
desc(AdminLog.create_time)).limit(10)
return render_template('system/user/center.html', user_info=user_info, user_logs=user_logs)
# 修改头像
@bp.get('/profile')
@login_required
def profile():
return render_template('system/user/profile.html')
# 修改头像
@bp.put('/updateAvatar')
@login_required
def update_avatar():
url = request.get_json(force=True).get("avatar").get("src")
r = User.query.filter_by(id=current_user.id).update({"avatar": url})
db.session.commit()
if not r:
return fail_api(msg="出错啦")
return success_api(msg="修改成功")
# 修改当前用户信息
@bp.put('/updateInfo')
@login_required
def update_info():
req_json = request.get_json(force=True)
r = User.query.filter_by(id=current_user.id).update(
{"realname": req_json.get("realName"), "remark": req_json.get("details")})
db.session.commit()
if not r:
return fail_api(msg="出错啦")
return success_api(msg="更新成功")
# 修改当前用户密码
@bp.get('/editPassword')
@login_required
def edit_password():
return render_template('system/user/edit_password.html')
# 修改当前用户密码
@bp.put('/editPassword')
@login_required
def edit_password_put():
res_json = request.get_json(force=True)
if res_json.get("newPassword") == '':
return fail_api("新密码不得为空")
if res_json.get("newPassword") != res_json.get("confirmPassword"):
return fail_api("两次密码不一样")
user = current_user
is_right = user.validate_password(res_json.get("oldPassword"))
if not is_right:
return fail_api("旧密码错误")
user.set_password(res_json.get("newPassword"))
db.session.add(user)
db.session.commit()
return success_api("更改成功")
# 启用用户
@bp.put('/enable')
@authorize("system:user:edit", log=True)
def enable():
_id = request.get_json(force=True).get('userId')
if _id:
res = enable_status(model=User, id=_id)
if not res:
return fail_api(msg="出错啦")
return success_api(msg="启动成功")
return fail_api(msg="数据错误")
# 禁用用户
@bp.put('/disable')
@authorize("system:user:edit", log=True)
def dis_enable():
_id = request.get_json(force=True).get('userId')
if _id:
res = disable_status(model=User, id=_id)
if not res:
return fail_api(msg="出错啦")
return success_api(msg="禁用成功")
return fail_api(msg="数据错误")
-48
View File
@@ -1,48 +0,0 @@
from flask import Blueprint, render_template
system_bp = Blueprint('system', __name__)
@system_bp.route('/view/system/user.html')
def system_user():
return render_template('view/system/user.html')
@system_bp.route('/view/system/role.html')
def system_role():
return render_template('view/system/role.html')
@system_bp.route('/view/system/power.html')
def system_power():
return render_template('view/system/power.html')
@system_bp.route('/view/system/department.html')
def system_department():
return render_template('view/system/department.html')
@system_bp.route('/view/system/log.html')
def system_log():
return render_template('view/system/log.html')
@system_bp.route('/view/system/dict.html')
def system_dict():
return render_template('view/system/dict.html')
@system_bp.route('/view/system/operate/add.html')
def system_operate_add():
return render_template('view/system/operate/add.html')
@system_bp.route('/view/system/operate/edit.html')
def system_operate_edit():
return render_template('view/system/operate/edit.html')
@system_bp.route('/view/system/operate/profile.html')
def system_operate_profile():
return render_template('view/system/operate/profile.html')
-26
View File
@@ -1,26 +0,0 @@
from flask import render_template
from common.utils.rights import permission_required, view_logging_required
from models import RoleModel, UserModel
from . import index_bp
@index_bp.get('/users/add')
@view_logging_required
@permission_required("admin:user:add")
def users_add_view():
roles = RoleModel.query.all()
return render_template('view/system/user_add.html', roles=roles)
@index_bp.get('/users/<user_id>')
@view_logging_required
@permission_required("admin:user:edit")
def users_user_id_view(user_id):
# 获取编辑用户信息
user = UserModel.query.filter_by(id=user_id).first()
roles = RoleModel.query.all()
checked_roles = []
for r in user.role:
checked_roles.append(r.id)
return render_template('view/system/user_edit.html', user=user, roles=roles, checked_roles=checked_roles)
-5
View File
@@ -1,5 +0,0 @@
def register_api(view, endpoint, url, pk='id', pk_type='int', app=None):
view_func = view.as_view(endpoint)
app.add_url_rule(url, defaults={pk: None}, view_func=view_func, methods=['GET', ])
app.add_url_rule(url, view_func=view_func, methods=['POST', ])
app.add_url_rule(f'{url}<{pk_type}:{pk}>', view_func=view_func, methods=['GET', 'PUT', 'DELETE'])
-504
View File
@@ -1,504 +0,0 @@
# -*- coding: utf-8 -*-
"""
为了解决 flask_uploads 与 Werkzeug 2.0.1 冲突而直接复制源码进行适配
原始地址: https://github.com/maxcountryman/flask-uploads/blob/master/flask_uploads.py
"""
import sys
PY3 = sys.version_info[0] == 3
if PY3:
string_types = str,
else:
string_types = basestring,
import os.path
import posixpath
from flask import current_app, send_from_directory, abort, url_for
from itertools import chain
from werkzeug.datastructures import FileStorage
from werkzeug.utils import secure_filename
from flask import Blueprint
# Extension presets
#: This just contains plain text files (.txt).
TEXT = ('txt',)
#: This contains various office document formats (.rtf, .odf, .ods, .gnumeric,
#: .abw, .doc, .docx, .xls, and .xlsx). Note that the macro-enabled versions
#: of Microsoft Office 2007 files are not included.
DOCUMENTS = tuple('rtf odf ods gnumeric abw doc docx xls xlsx'.split())
#: This contains basic image types that are viewable from most browsers (.jpg,
#: .jpe, .jpeg, .png, .gif, .svg, and .bmp).
IMAGES = tuple('jpg jpe jpeg png gif svg bmp'.split())
#: This contains audio file types (.wav, .mp3, .aac, .ogg, .oga, and .flac).
AUDIO = tuple('wav mp3 aac ogg oga flac'.split())
#: This is for structured data files (.csv, .ini, .json, .plist, .xml, .yaml,
#: and .yml).
DATA = tuple('csv ini json plist xml yaml yml'.split())
#: This contains various types of scripts (.js, .php, .pl, .py .rb, and .sh).
#: If your Web server has PHP installed and set to auto-run, you might want to
#: add ``php`` to the DENY setting.
SCRIPTS = tuple('js php pl py rb sh'.split())
#: This contains archive and compression formats (.gz, .bz2, .zip, .tar,
#: .tgz, .txz, and .7z).
ARCHIVES = tuple('gz bz2 zip tar tgz txz 7z'.split())
#: This contains shared libraries and executable files (.so, .exe and .dll).
#: Most of the time, you will not want to allow this - it's better suited for
#: use with `AllExcept`.
EXECUTABLES = tuple('so exe dll'.split())
#: The default allowed extensions - `TEXT`, `DOCUMENTS`, `DATA`, and `IMAGES`.
DEFAULTS = TEXT + DOCUMENTS + IMAGES + DATA
class UploadNotAllowed(Exception):
"""
This exception is raised if the upload was not allowed. You should catch
it in your view code and display an appropriate message to the user.
"""
def tuple_from(*iters):
return tuple(itertools.chain(*iters))
def extension(filename):
ext = os.path.splitext(filename)[1]
if ext.startswith('.'):
# os.path.splitext retains . separator
ext = ext[1:]
return ext
def lowercase_ext(filename):
"""
This is a helper used by UploadSet.save to provide lowercase extensions for
all processed files, to compare with configured extensions in the same
case.
.. versionchanged:: 0.1.4
Filenames without extensions are no longer lowercased, only the
extension is returned in lowercase, if an extension exists.
:param filename: The filename to ensure has a lowercase extension.
"""
if '.' in filename:
main, ext = os.path.splitext(filename)
return main + ext.lower()
# For consistency with os.path.splitext,
# do not treat a filename without an extension as an extension.
# That is, do not return filename.lower().
return filename
def addslash(url):
if url.endswith('/'):
return url
return url + '/'
def patch_request_class(app, size=64 * 1024 * 1024):
"""
By default, Flask will accept uploads to an arbitrary size. While Werkzeug
switches uploads from memory to a temporary file when they hit 500 KiB,
it's still possible for someone to overload your disk space with a
gigantic file.
This patches the app's request class's
`~werkzeug.BaseRequest.max_content_length` attribute so that any upload
larger than the given size is rejected with an HTTP error.
.. note::
In Flask 0.6, you can do this by setting the `MAX_CONTENT_LENGTH`
setting, without patching the request class. To emulate this behavior,
you can pass `None` as the size (you must pass it explicitly). That is
the best way to call this function, as it won't break the Flask 0.6
functionality if it exists.
.. versionchanged:: 0.1.1
:param app: The app to patch the request class of.
:param size: The maximum size to accept, in bytes. The default is 64 MiB.
If it is `None`, the app's `MAX_CONTENT_LENGTH` configuration
setting will be used to patch.
"""
if size is None:
if isinstance(app.request_class.__dict__['max_content_length'],
property):
return
size = app.config.get('MAX_CONTENT_LENGTH')
reqclass = app.request_class
patched = type(reqclass.__name__, (reqclass,),
{'max_content_length': size})
app.request_class = patched
def config_for_set(uset, app, defaults=None):
"""
This is a helper function for `configure_uploads` that extracts the
configuration for a single set.
:param uset: The upload set.
:param app: The app to load the configuration from.
:param defaults: A dict with keys `url` and `dest` from the
`UPLOADS_DEFAULT_DEST` and `DEFAULT_UPLOADS_URL`
settings.
"""
config = app.config
prefix = 'UPLOADED_%s_' % uset.name.upper()
using_defaults = False
if defaults is None:
defaults = dict(dest=None, url=None)
allow_extns = tuple(config.get(prefix + 'ALLOW', ()))
deny_extns = tuple(config.get(prefix + 'DENY', ()))
destination = config.get(prefix + 'DEST')
base_url = config.get(prefix + 'URL')
if destination is None:
# the upload set's destination wasn't given
if uset.default_dest:
# use the "default_dest" callable
destination = uset.default_dest(app)
if destination is None: # still
# use the default dest from the config
if defaults['dest'] is not None:
using_defaults = True
destination = os.path.join(defaults['dest'], uset.name)
else:
raise RuntimeError("no destination for set %s" % uset.name)
if base_url is None and using_defaults and defaults['url']:
base_url = addslash(defaults['url']) + uset.name + '/'
return UploadConfiguration(destination, base_url, allow_extns, deny_extns)
def configure_uploads(app, upload_sets):
"""
Call this after the app has been configured. It will go through all the
upload sets, get their configuration, and store the configuration on the
app. It will also register the uploads module if it hasn't been set. This
can be called multiple times with different upload sets.
.. versionchanged:: 0.1.3
The uploads module/blueprint will only be registered if it is needed
to serve the upload sets.
:param app: The `~flask.Flask` instance to get the configuration from.
:param upload_sets: The `UploadSet` instances to configure.
"""
if isinstance(upload_sets, UploadSet):
upload_sets = (upload_sets,)
if not hasattr(app, 'upload_set_config'):
app.upload_set_config = {}
set_config = app.upload_set_config
defaults = dict(dest=app.config.get('UPLOADS_DEFAULT_DEST'),
url=app.config.get('UPLOADS_DEFAULT_URL'))
for uset in upload_sets:
config = config_for_set(uset, app, defaults)
set_config[uset.name] = config
should_serve = any(s.base_url is None for s in set_config.values())
if '_uploads' not in app.blueprints and should_serve:
app.register_blueprint(uploads_mod)
class All(object):
"""
This type can be used to allow all extensions. There is a predefined
instance named `ALL`.
"""
def __contains__(self, item):
return True
#: This "contains" all items. You can use it to allow all extensions to be
#: uploaded.
ALL = All()
class AllExcept(object):
"""
This can be used to allow all file types except certain ones. For example,
to ban .exe and .iso files, pass::
AllExcept(('exe', 'iso'))
to the `UploadSet` constructor as `extensions`. You can use any container,
for example::
AllExcept(SCRIPTS + EXECUTABLES)
"""
def __init__(self, items):
self.items = items
def __contains__(self, item):
return item not in self.items
class UploadConfiguration(object):
"""
This holds the configuration for a single `UploadSet`. The constructor's
arguments are also the attributes.
:param destination: The directory to save files to.
:param base_url: The URL (ending with a /) that files can be downloaded
from. If this is `None`, Flask-Uploads will serve the
files itself.
:param allow: A list of extensions to allow, even if they're not in the
`UploadSet` extensions list.
:param deny: A list of extensions to deny, even if they are in the
`UploadSet` extensions list.
"""
def __init__(self, destination, base_url=None, allow=(), deny=()):
self.destination = destination
self.base_url = base_url
self.allow = allow
self.deny = deny
@property
def tuple(self):
return (self.destination, self.base_url, self.allow, self.deny)
def __eq__(self, other):
return self.tuple == other.tuple
class UploadSet(object):
"""
This represents a single set of uploaded files. Each upload set is
independent of the others. This can be reused across multiple application
instances, as all configuration is stored on the application object itself
and found with `flask.current_app`.
:param name: The name of this upload set. It defaults to ``files``, but
you can pick any alphanumeric name you want. (For simplicity,
it's best to use a plural noun.)
:param extensions: The extensions to allow uploading in this set. The
easiest way to do this is to add together the extension
presets (for example, ``TEXT + DOCUMENTS + IMAGES``).
It can be overridden by the configuration with the
`UPLOADED_X_ALLOW` and `UPLOADED_X_DENY` configuration
parameters. The default is `DEFAULTS`.
:param default_dest: If given, this should be a callable. If you call it
with the app, it should return the default upload
destination path for that app.
"""
def __init__(self, name='files', extensions=DEFAULTS, default_dest=None):
if not name.isalnum():
raise ValueError("Name must be alphanumeric (no underscores)")
self.name = name
self.extensions = extensions
self._config = None
self.default_dest = default_dest
@property
def config(self):
"""
This gets the current configuration. By default, it looks up the
current application and gets the configuration from there. But if you
don't want to go to the full effort of setting an application, or it's
otherwise outside of a request context, set the `_config` attribute to
an `UploadConfiguration` instance, then set it back to `None` when
you're done.
"""
if self._config is not None:
return self._config
try:
return current_app.upload_set_config[self.name]
except AttributeError:
raise RuntimeError("cannot access configuration outside request")
def url(self, filename):
"""
This function gets the URL a file uploaded to this set would be
accessed at. It doesn't check whether said file exists.
:param filename: The filename to return the URL for.
"""
base = self.config.base_url
if base is None:
return url_for('_uploads.uploaded_file', setname=self.name,
filename=filename, _external=True)
else:
return base + filename
def path(self, filename, folder=None):
"""
This returns the absolute path of a file uploaded to this set. It
doesn't actually check whether said file exists.
:param filename: The filename to return the path for.
:param folder: The subfolder within the upload set previously used
to save to.
"""
if folder is not None:
target_folder = os.path.join(self.config.destination, folder)
else:
target_folder = self.config.destination
return os.path.join(target_folder, filename)
def file_allowed(self, storage, basename):
"""
This tells whether a file is allowed. It should return `True` if the
given `werkzeug.FileStorage` object can be saved with the given
basename, and `False` if it can't. The default implementation just
checks the extension, so you can override this if you want.
:param storage: The `werkzeug.FileStorage` to check.
:param basename: The basename it will be saved under.
"""
return self.extension_allowed(extension(basename))
def extension_allowed(self, ext):
"""
This determines whether a specific extension is allowed. It is called
by `file_allowed`, so if you override that but still want to check
extensions, call back into this.
:param ext: The extension to check, without the dot.
"""
return ((ext in self.config.allow) or
(ext in self.extensions and ext not in self.config.deny))
def get_basename(self, filename):
return lowercase_ext(secure_filename(filename))
def save(self, storage, folder=None, name=None):
"""
This saves a `werkzeug.FileStorage` into this upload set. If the
upload is not allowed, an `UploadNotAllowed` error will be raised.
Otherwise, the file will be saved and its name (including the folder)
will be returned.
:param storage: The uploaded file to save.
:param folder: The subfolder within the upload set to save to.
:param name: The name to save the file as. If it ends with a dot, the
file's extension will be appended to the end. (If you
are using `name`, you can include the folder in the
`name` instead of explicitly using `folder`, i.e.
``uset.save(file, name="someguy/photo_123.")``
"""
if not isinstance(storage, FileStorage):
raise TypeError("storage must be a werkzeug.FileStorage")
if folder is None and name is not None and "/" in name:
folder, name = os.path.split(name)
basename = self.get_basename(storage.filename)
if name:
if name.endswith('.'):
basename = name + extension(basename)
else:
basename = name
if not self.file_allowed(storage, basename):
raise UploadNotAllowed()
if folder:
target_folder = os.path.join(self.config.destination, folder)
else:
target_folder = self.config.destination
if not os.path.exists(target_folder):
os.makedirs(target_folder)
if os.path.exists(os.path.join(target_folder, basename)):
basename = self.resolve_conflict(target_folder, basename)
target = os.path.join(target_folder, basename)
storage.save(target)
if folder:
return posixpath.join(folder, basename)
else:
return basename
def resolve_conflict(self, target_folder, basename):
"""
If a file with the selected name already exists in the target folder,
this method is called to resolve the conflict. It should return a new
basename for the file.
The default implementation splits the name and extension and adds a
suffix to the name consisting of an underscore and a number, and tries
that until it finds one that doesn't exist.
:param target_folder: The absolute path to the target.
:param basename: The file's original basename.
"""
name, ext = os.path.splitext(basename)
count = 0
while True:
count = count + 1
newname = '%s_%d%s' % (name, count, ext)
if not os.path.exists(os.path.join(target_folder, newname)):
return newname
uploads_mod = Blueprint('_uploads', __name__, url_prefix='/_uploads')
@uploads_mod.route('/<setname>/<path:filename>')
def uploaded_file(setname, filename):
config = current_app.upload_set_config.get(setname)
if config is None:
abort(404)
return send_from_directory(config.destination, filename)
class TestingFileStorage(FileStorage):
"""
This is a helper for testing upload behavior in your application. You
can manually create it, and its save method is overloaded to set `saved`
to the name of the file it was saved to. All of these parameters are
optional, so only bother setting the ones relevant to your application.
:param stream: A stream. The default is an empty stream.
:param filename: The filename uploaded from the client. The default is the
stream's name.
:param name: The name of the form field it was loaded from. The default is
`None`.
:param content_type: The content type it was uploaded as. The default is
``application/octet-stream``.
:param content_length: How long it is. The default is -1.
:param headers: Multipart headers as a `werkzeug.Headers`. The default is
`None`.
"""
def __init__(self, stream=None, filename=None, name=None,
content_type='application/octet-stream', content_length=-1,
headers=None):
FileStorage.__init__(self, stream, filename, name=name,
content_type=content_type, content_length=content_length,
headers=None)
self.saved = None
def save(self, dst, buffer_size=16384):
"""
This marks the file as saved by setting the `saved` attribute to the
name of the file it was saved to.
:param dst: The file to save to.
:param buffer_size: Ignored.
"""
if isinstance(dst, string_types):
self.saved = dst
else:
self.saved = dst.name
-43
View File
@@ -1,43 +0,0 @@
from captcha.image import ImageCaptcha
from io import BytesIO
from PIL import Image
from random import choices
from flask import session, make_response
from flask_login import current_user
def gen_captcha(content='0123456789'):
""" 生成验证码 """
image = ImageCaptcha()
# 获取字符串
captcha_text = "".join(choices(content, k=4))
# 生成图像
captcha_image = Image.open(image.generate(captcha_text))
return captcha_text, captcha_image
# 生成验证码
def get_captcha_image():
code, image = gen_captcha()
out = BytesIO()
session["code"] = code
image.save(out, 'png')
out.seek(0)
resp = make_response(out.read())
resp.content_type = 'image/png'
return resp, code
# 授权路由存入session
def add_auth_session():
role = current_user.role
user_power = []
for i in role:
if i.enable == 0:
continue
for p in i.power:
if p.enable == 0:
continue
user_power.append(p.code)
session['permissions'] = user_power
-35
View File
@@ -1,35 +0,0 @@
import typing as t
def success_api(message: str = "成功", code: int = 200) -> t.Dict:
""" 成功响应 默认值”成功“ """
return {'success': True, 'message': message, 'code': code}
def fail_api(message: str = "失败", code: int = 404) -> t.Dict:
""" 失败响应 默认值“失败” """
return {'success': False, 'message': message, 'code': code}
def table_api(success: bool = True,
message: str = "",
result: t.Union[dict, list] = None,
code: int = 0) -> t.Dict:
"""
动态表格渲染响应
此方法返回数据给前端
{
'success': True,
'code': 10002,
'message': '提示消息',
'result':{'items':[],'total': 100}
}
注:lay_ui 表格数据需要指定 code=0
"""
return {
'success': success,
'message': message,
'code': code,
'result': result,
}
-61
View File
@@ -1,61 +0,0 @@
import typing as t
from functools import wraps
from flask import abort, request, jsonify, session
from flask_login import login_required
from flask_login import current_user
from extensions import db
from models import LogModel
def record_logging(success: bool = True) -> None:
"""
记录用户日志数据
"""
info = {
'method': request.method,
'url': request.path,
'ip': request.remote_addr,
'user_agent': request.headers.get('User-Agent'),
'desc': str(dict(request.values)),
'uid': current_user.id,
'success': success
}
log = LogModel()
for key, value in info.items():
setattr(log, key, value)
db.session.add(log)
db.session.commit()
def view_logging_required(func: t.Callable) -> t.Callable:
"""
日志装饰器,用于记录请求
"""
@wraps(func)
def wrapper(*args, **kwargs) -> t.Callable:
record_logging()
return func(*args, **kwargs)
return wrapper
def permission_required(permission: str) -> t.Callable:
"""
权限装饰器,用于过滤需要的权限
"""
def decorator(func: t.Callable):
@wraps(func)
def wrapper(*args, **kwargs) -> t.Callable:
if permission not in session.get('permissions'):
record_logging(success=False)
abort(403)
return func(*args, **kwargs)
return wrapper
return decorator
-30
View File
@@ -1,30 +0,0 @@
import os
from flask import current_app
from common.flask_uploads import UploadSet, IMAGES
from extensions import db
from models import PhotoModel
photos = UploadSet('photos', IMAGES)
def upload_one(photo, mime):
filename = photos.save(photo)
file_url = photos.url(filename)
upload_url = current_app.config.get("UPLOADED_PHOTOS_DEST")
size = os.path.getsize(upload_url + '/' + filename)
photo = PhotoModel(name=filename, href=file_url, mime=mime, size=size)
db.session.add(photo)
db.session.commit()
return file_url
def delete_photo_by_id(_id):
photo_name = PhotoModel.query.filter_by(id=_id).first().name
photo = PhotoModel.query.filter_by(id=_id).delete()
db.session.commit()
upload_url = current_app.config.get("UPLOADED_PHOTOS_DEST")
os.remove(upload_url + '/' + photo_name)
return photo
-50
View File
@@ -1,50 +0,0 @@
import logging
import os
# 主题面板的链接列表配置
SYSTEM_NAME = "Pear Admin"
SYSTEM_PANEL_LINKS = [
{
"icon": "layui-icon layui-icon-auz",
"title": "官方网站",
"href": "http://www.pearadmin.com"
},
{
"icon": "layui-icon layui-icon-auz",
"title": "开发文档",
"href": "http://www.pearadmin.com"
},
{
"icon": "layui-icon layui-icon-auz",
"title": "开源地址",
"href": "https://gitee.com/Jmysy/Pear-Admin-Layui"
}
]
SECRET_KEY = os.getenv('SECRET_KEY', 'dev key')
# mysql 配置
MYSQL_USERNAME = "windows"
MYSQL_PASSWORD = "123456"
MYSQL_HOST = "127.0.0.1"
MYSQL_PORT = 3306
MYSQL_DATABASE = "PearAdminFlask"
# redis 配置
REDIS_HOST = "127.0.0.1"
REDIS_PORT = 6379
""" Sqlalchemy 配置 """
SQLALCHEMY_DATABASE_URI = r'sqlite:///pear_admin.db'
SQLALCHEMY_TRACK_MODIFICATIONS = True
SQLALCHEMY_ECHO = False
SQLALCHEMY_POOL_RECYCLE = 8
# SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{MYSQL_USERNAME}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}"
LOG_LEVEL = logging.ERROR
# 图片文件存放位置
UPLOADED_PHOTOS_DEST = os.path.join(os.path.dirname(os.path.abspath(__name__)), 'static', 'upload')
UPLOADED_FILES_ALLOW = ['gif', 'jpg']
# JSON配置
JSON_AS_ASCII = False
+25
View File
@@ -0,0 +1,25 @@
FROM python:3.11-bookworm
# 设置时区
ENV TIME_ZONE Asia/Shanghai
RUN echo "${TIME_ZONE}" > /etc/timezone \
&& ln -sf /usr/share/zoneinfo/${TIME_ZONE} /etc/localtime
WORKDIR /app
# 1. [优化] 先只拷贝依赖文件,利用 Docker 缓存
# 只要 requirements.txt 没变,这一步和 pip install 就会直接用缓存,极大加快构建速度
COPY requirements.txt /app/
# 2. 安装依赖 (使用清华源)
RUN python3 -m pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/ \
&& python3 -m pip install --no-cache-dir gunicorn -i https://pypi.tuna.tsinghua.edu.cn/simple/
# 3. 拷贝其余项目代码
COPY . /app/
# 4. 确保脚本有执行权限 (直接处理源路径,防止移动失败)
RUN chmod +x /app/deploy/dev/start.sh
# 5. 指定启动入口
CMD ["/bin/bash", "/app/deploy/dev/start.sh"]
+29
View File
@@ -0,0 +1,29 @@
version: '3.8'
services:
flask:
build:
context: ../.. # 指向项目根目录 (pear-admin-flask)
dockerfile: deploy/dev/Dockerfile
container_name: flask_dev
restart: always
ports:
- "5000:5000"
# [优化] 挂载卷:将本地代码映射到容器内部
# 效果:你在 PyCharm 修改代码,容器里立马生效,不需要重启容器
volumes:
- ../..:/app
# [优化] 环境变量
environment:
- FLASK_DEBUG=0 # 是否开启 Flask 调试模式
- FLASK_APP=app.py
# 网络设置
networks:
- dev_network
networks:
dev_network:
driver: bridge
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# 确保进入工作目录
cd /app
echo "=== [Step 1] 检查并初始化数据库 ==="
# 使用 || true 确保即使命令报错(例如migrations文件夹已存在)也不会中断脚本
flask db init || true
flask db migrate || true
flask db upgrade || true
# (可选) 每次重启尝试初始化数据,如果已存在则会被代码逻辑跳过
flask admin init || true
echo "=== [Step 2] 启动服务 (开发模式) ==="
# [优化] 增加 --reload 参数
# 配合 docker-compose 的 volumes,当检测到代码变化时,Gunicorn 会自动重启 worker
# access-logfile - 表示将日志输出到控制台
exec gunicorn --bind 0.0.0.0:5000 \
--workers 2 \
--reload \
--access-logfile - \
--error-logfile - \
app:app

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