Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55ea1e7252 | ||
|
|
b80b869dd8 | ||
|
|
7fea9743c9 | ||
|
|
c09812f85d | ||
|
|
794a8a57ad | ||
|
|
cab60da886 | ||
|
|
7501ae1dc2 | ||
|
|
9fc2eb0ae3 | ||
|
|
12d4dfe410 | ||
|
|
d54eb4dc3e | ||
|
|
246ce3b3b7 | ||
|
|
36250fad43 | ||
|
|
0d25b479e5 | ||
|
|
7e6aecc99b | ||
|
|
82de7b0273 | ||
|
|
8760f21b03 | ||
|
|
1e34ef339a | ||
|
|
5333a68416 | ||
|
|
decde3a69a | ||
|
|
9dcc05deda | ||
|
|
c0d963628b | ||
|
|
6978002393 | ||
|
|
3453b01938 | ||
|
|
820ab9f5c6 | ||
|
|
9490ccb7c0 | ||
|
|
056c4b7023 | ||
|
|
d4d8d8b294 | ||
|
|
8bc5d6e0c2 | ||
|
|
3fa8ae0a8a | ||
|
|
316ebc2441 | ||
|
|
550f99e468 | ||
|
|
b9c449ad57 | ||
|
|
db5b4e89ed | ||
|
|
733286b090 | ||
|
|
7505e4552b | ||
|
|
e918727984 | ||
|
|
00053da117 | ||
|
|
27f5d78c35 | ||
|
|
97e0160aaf | ||
|
|
04794e583f | ||
|
|
0640255519 | ||
|
|
460984f209 | ||
|
|
635dcc1dac | ||
|
|
b26237e24b | ||
|
|
a35d443fb6 | ||
|
|
ab4bb1ca40 | ||
|
|
2b7304e615 | ||
|
|
64748e513d | ||
|
|
5b07fed518 | ||
|
|
eb85e30b79 | ||
|
|
b8e0173a9c | ||
|
|
0ca75c81d4 | ||
|
|
aa86bbe372 | ||
|
|
86f07817b1 | ||
|
|
3f74011c98 | ||
|
|
ce8adda79d | ||
|
|
5470837707 | ||
|
|
c26f15ae69 | ||
|
|
4d6d998ae2 | ||
|
|
d628432621 | ||
|
|
2bcaa8e071 | ||
|
|
ebf76055a4 | ||
|
|
b2b08769b0 | ||
|
|
aae15fd07a | ||
|
|
945f746d97 | ||
|
|
dce7908041 | ||
|
|
0ba2b3aa56 | ||
|
|
17792ccacf | ||
|
|
e3dd5458ad | ||
|
|
e9bd8a17a0 | ||
|
|
b8870d4c33 | ||
|
|
8fba71c464 | ||
|
|
0cb74c5eb7 | ||
|
|
6da6768f81 | ||
|
|
05dff28523 | ||
|
|
d7c7237cd4 |
@@ -1,11 +0,0 @@
|
||||
# 构建的相关文件
|
||||
README.md
|
||||
|
||||
# Python 相关文件
|
||||
migrations/
|
||||
instance/
|
||||
flask_session/
|
||||
venv/
|
||||
.idea/
|
||||
*.log
|
||||
*.db
|
||||
@@ -0,0 +1,3 @@
|
||||
FLASK_ENV=development
|
||||
FLASK_RUN_HOST=0.0.0.0
|
||||
FLASK_APP='applications:create_app'
|
||||
+5
-14
@@ -114,20 +114,11 @@ dmypy.json
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# idea
|
||||
#ide
|
||||
.idea/
|
||||
|
||||
# 迁移文件
|
||||
migrations/
|
||||
|
||||
# sqlite
|
||||
# 数据库文件
|
||||
*.db
|
||||
|
||||
# 文件上传
|
||||
static/upload/
|
||||
|
||||
# flask session
|
||||
flask_session/
|
||||
|
||||
# WorkBuddy 运行时目录(不应纳入版本管理)
|
||||
.workbuddy/
|
||||
#上传到本地的图片文件
|
||||
*upload/
|
||||
migrations/
|
||||
@@ -1,6 +1,7 @@
|
||||
<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>
|
||||
@@ -8,14 +9,15 @@
|
||||
开 箱 即 用 的 Flask 快 速 开 发 平 台
|
||||
</h4>
|
||||
|
||||
[预览](https://pear.lovepikachu.top/) | [官网](http://www.pearadmin.com/) | [群聊](docs/source/_static/qqgroup.jpg) | [文档](https://lab.lovepikachu.top/document/pear-admin-flask)
|
||||
[预 览](http://flask.pearadmin.com) | [官 网](http://www.pearadmin.com/) | [群聊](https://jq.qq.com/?_wv=1027&k=5OdSmve) | [社区](http://forum.pearadmin.com/)
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="#">
|
||||
<img src="https://img.shields.io/badge/pear%20admin%20flask-2.0.0-green" alt="Pear Admin Layui Version">
|
||||
<img src="https://img.shields.io/badge/pear%20admin%20flask-1.0.0-green" alt="Pear Admin Layui Version">
|
||||
</a>
|
||||
<a href="#">
|
||||
<img src="https://img.shields.io/badge/Python-3.8+-green.svg" alt="Python Version">
|
||||
<img src="https://img.shields.io/badge/Python-3.6+-green.svg" alt="Python Version">
|
||||
</a>
|
||||
<a href="#">
|
||||
<img src="https://img.shields.io/badge/Mysql-5.3.2+-green.svg" alt="Mysql Version">
|
||||
@@ -24,213 +26,115 @@
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<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" />
|
||||
<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" />
|
||||
</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) 。
|
||||
在使用本项目之前最好掌握以下知识点
|
||||
|
||||
Pear Admin Flask 基于 Flask 的后台管理系统,拥抱应用广泛的python语言,通过使用本系统,即可快速构建你的功能业务
|
||||
项目旨在为 python 开发者提供一个后台管理系统的模板,可以快速构建信息管理系统。
|
||||
+ [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)
|
||||
+ [flask-restful](https://flask-restful.readthedocs.io/en/latest/)
|
||||
|
||||
项目使用 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 + Flask-restful + 基于角色的权限管理
|
||||
|
||||
| | |
|
||||
|---------------------|---------------------|
|
||||
|  |  |
|
||||
| |  |
|
||||
|  |  |
|
||||
|
||||
## 内置功能
|
||||
|
||||
- [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 # 进入到项目目录
|
||||
|
||||
# 进入 pear-admin-flask 代码根目录
|
||||
cd pear-admin-flask
|
||||
|
||||
# 切换分支
|
||||
git checkout mini
|
||||
```
|
||||
|
||||
## 修改配置
|
||||
|
||||
> **💡提示** 配置文件位于 `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
|
||||
# 然后使虚拟环境生效(windows)
|
||||
venv\Scripts\activate
|
||||
# source venv/bin/activate # (Linux激活虚拟环境)
|
||||
|
||||
# 使用 pip 安装
|
||||
pip install -r requirements.txt
|
||||
# 安装依赖
|
||||
pip install -r requirement.txt
|
||||
```
|
||||
|
||||
## 直接安装项目
|
||||
+ 数据迁移
|
||||
|
||||
默认的使用 `sqlite3` 作为测试环境的数据库进行演示,不需要按照mysql即可查看演示。如果需要二次开发,建议改成 `mysql` 。
|
||||
|
||||
如果需要在开发环境使用 mysql 作为数据库,请查看 `applications/configs/config.py` 文件里面的相关配置文件, 注释掉 sqlite 的配置即可
|
||||
|
||||
如果需要修改数据的配置信息,请在 `.flaskenv` 里面调整即可
|
||||
|
||||
```bash
|
||||
# 使用 pip 安装
|
||||
pip install -r requirements.txt
|
||||
# 同时你可以选择以模块的方式调用 pip
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
# 运行项目
|
||||
|
||||
+ 一般情况运行项目
|
||||
|
||||
```bash
|
||||
# 初始化数据库
|
||||
flask db init
|
||||
flask db migrate
|
||||
flask db migrate -m '数据初始化'
|
||||
flask db upgrade
|
||||
flask admin init
|
||||
|
||||
# 运行项目
|
||||
flask --app app.py run -h 0.0.0.0 -p 8000 --debug
|
||||
|
||||
# 或者直接调用 app.py
|
||||
python app.py
|
||||
flask init-db
|
||||
```
|
||||
|
||||
+ 使用 docker-compose 运行项目
|
||||
+ 项目启动
|
||||
|
||||
```bash
|
||||
git clone https://gitee.com/pear-admin/pear-admin-flask
|
||||
|
||||
# 安装 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
|
||||
flask run
|
||||
```
|
||||
|
||||
## Git 贡献提交规范
|
||||
|
||||
# 预览项目
|
||||
|
||||
| | |
|
||||
| ---------------------- | ---------------------- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
|
||||
# 其他说明
|
||||
|
||||
## 项目初始用户以及其密码
|
||||
|
||||
默认用户为 `admin` ,密码默认为 `123456` 。
|
||||
|
||||
|
||||
- 参考 [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` 开发中
|
||||
@@ -1,6 +0,0 @@
|
||||
from applications import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
+50
-14
@@ -1,24 +1,60 @@
|
||||
import os
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def create_app():
|
||||
app = Flask(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
def init_api(app: Flask) -> None:
|
||||
app.register_blueprint(api_bp)
|
||||
|
||||
# 引入配置
|
||||
app.config.from_object(BaseConfig)
|
||||
|
||||
# 注册flask组件
|
||||
def create_app() -> Flask:
|
||||
app = Flask('pear-admin-flask')
|
||||
|
||||
# 引入数据库配置
|
||||
app.config.from_object(config)
|
||||
|
||||
# 注册各种插件
|
||||
init_plugs(app)
|
||||
|
||||
# 注册蓝图
|
||||
init_bps(app)
|
||||
# 注册路由
|
||||
init_view(app)
|
||||
|
||||
# 注册命令
|
||||
init_script(app)
|
||||
# 注册接口(restful api)
|
||||
init_api(app)
|
||||
|
||||
# 文件上传
|
||||
configure_uploads(app, photos)
|
||||
|
||||
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
|
||||
logo()
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def logo():
|
||||
print('''
|
||||
_____ _ _ ______ _ _
|
||||
| __ \ /\ | | (_) | ____| | | |
|
||||
| |__) |__ __ _ _ __ / \ __| |_ __ ___ _ _ __ | |__ | | __ _ ___| | __
|
||||
| ___/ _ \/ _` | '__| / /\ \ / _` | '_ ` _ \| | '_ \ | __| | |/ _` / __| |/ /
|
||||
| | | __/ (_| | | / ____ \ (_| | | | | | | | | | | | | | | (_| \__ \ <
|
||||
|_| \___|\__,_|_| /_/ \_\__,_|_| |_| |_|_|_| |_| |_| |_|\__,_|___/_|\_\\
|
||||
|
||||
''')
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
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)
|
||||
@@ -1,119 +0,0 @@
|
||||
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
|
||||
@@ -1,173 +0,0 @@
|
||||
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)
|
||||
@@ -1,11 +0,0 @@
|
||||
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')
|
||||
@@ -1,247 +0,0 @@
|
||||
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
@@ -1,95 +0,0 @@
|
||||
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")
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
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
|
||||
@@ -1,135 +0,0 @@
|
||||
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.
@@ -1,41 +0,0 @@
|
||||
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)
|
||||
@@ -1,93 +0,0 @@
|
||||
"""
|
||||
集成了对 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)
|
||||
@@ -1,44 +0,0 @@
|
||||
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
|
||||
@@ -1,36 +0,0 @@
|
||||
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
|
||||
@@ -1,240 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,84 +0,0 @@
|
||||
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"
|
||||
@@ -1,34 +0,0 @@
|
||||
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,20 +0,0 @@
|
||||
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)
|
||||
@@ -1,8 +0,0 @@
|
||||
from flask import Flask
|
||||
from flask_mail import Mail
|
||||
|
||||
mail = Mail()
|
||||
|
||||
|
||||
def init_mail(app: Flask):
|
||||
mail.init_app(app)
|
||||
@@ -1,11 +0,0 @@
|
||||
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)
|
||||
@@ -1,61 +0,0 @@
|
||||
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')
|
||||
@@ -1,7 +0,0 @@
|
||||
from flask_session import Session
|
||||
|
||||
sess = Session()
|
||||
|
||||
|
||||
def init_session(app):
|
||||
sess.init_app(app)
|
||||
@@ -1,123 +0,0 @@
|
||||
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}")
|
||||
@@ -1,17 +0,0 @@
|
||||
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()}">'
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
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)
|
||||
@@ -1,11 +0,0 @@
|
||||
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
|
||||
@@ -1,18 +0,0 @@
|
||||
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='更新时间')
|
||||
@@ -1,26 +0,0 @@
|
||||
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='更新时间')
|
||||
@@ -1,14 +0,0 @@
|
||||
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)
|
||||
@@ -1,12 +0,0 @@
|
||||
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='创建时间')
|
||||
@@ -1,69 +0,0 @@
|
||||
"""
|
||||
前台公开导航模型。
|
||||
|
||||
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 '',
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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)
|
||||
@@ -1,18 +0,0 @@
|
||||
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='是否开启')
|
||||
@@ -1,16 +0,0 @@
|
||||
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'))
|
||||
@@ -1,9 +0,0 @@
|
||||
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='角色编号'), # 属性 外键
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
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)
|
||||
@@ -1,9 +0,0 @@
|
||||
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='角色编号'), # 属性 外键
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
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'])
|
||||
@@ -0,0 +1,114 @@
|
||||
import typing as t
|
||||
|
||||
from flask import jsonify
|
||||
from flask.views import MethodView
|
||||
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 DepartmentModel, UserModel
|
||||
|
||||
|
||||
class DeptModel(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()
|
||||
# TODO dtree 需要返回状态信息
|
||||
res = {
|
||||
"status": {"code": 200, "message": "默认"},
|
||||
"data": [
|
||||
{
|
||||
'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
|
||||
]
|
||||
}
|
||||
return jsonify(res)
|
||||
|
||||
@validate()
|
||||
def post(self, body: DeptModel):
|
||||
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: DeptModel):
|
||||
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 fail_api(message="更新失败")
|
||||
db.session.commit()
|
||||
return success_api(message="更新成功")
|
||||
|
||||
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="出错啦")
|
||||
@@ -0,0 +1,278 @@
|
||||
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 PowerModel(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: PowerModel):
|
||||
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: PowerModel):
|
||||
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)
|
||||
@@ -0,0 +1,162 @@
|
||||
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="角色删除成功")
|
||||
@@ -1,9 +0,0 @@
|
||||
|
||||
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
|
||||
@@ -1,12 +0,0 @@
|
||||
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"] # 排除字段列表
|
||||
@@ -1,21 +0,0 @@
|
||||
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()
|
||||
@@ -1,14 +0,0 @@
|
||||
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()
|
||||
@@ -1,19 +0,0 @@
|
||||
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
|
||||
@@ -1,17 +0,0 @@
|
||||
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
|
||||
@@ -1,12 +0,0 @@
|
||||
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()
|
||||
@@ -1,44 +0,0 @@
|
||||
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()
|
||||
@@ -1,12 +0,0 @@
|
||||
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"] # 排除字段列表
|
||||
@@ -0,0 +1,10 @@
|
||||
from flask import Blueprint
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,78 @@
|
||||
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
|
||||
@@ -0,0 +1,52 @@
|
||||
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 LoginModel(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: LoginModel):
|
||||
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="用户名或密码错误")
|
||||
@@ -0,0 +1,14 @@
|
||||
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'])
|
||||
@@ -0,0 +1,243 @@
|
||||
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 QueryModel(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 PersonModel(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: QueryModel):
|
||||
|
||||
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: PersonModel):
|
||||
"""新建单个用户"""
|
||||
|
||||
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="操作有误")
|
||||
@@ -1,14 +1,17 @@
|
||||
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
|
||||
from flask import Flask
|
||||
|
||||
from .index import index_bp
|
||||
from .logs_view import logs_bp
|
||||
from .roles import role_bp
|
||||
|
||||
from . import department
|
||||
from . import file
|
||||
from . import rights
|
||||
from . import passport
|
||||
from . import users
|
||||
|
||||
|
||||
def init_bps(app):
|
||||
# 前台公开蓝图(无需登录)
|
||||
app.register_blueprint(public_bp)
|
||||
|
||||
# 后台系统蓝图(需要登录 + 权限码)
|
||||
register_system_bps(app)
|
||||
|
||||
# 插件初始化函数
|
||||
broadcast_execute(app, 'event_init')
|
||||
def init_view(app: Flask):
|
||||
app.register_blueprint(index_bp)
|
||||
app.register_blueprint(logs_bp)
|
||||
app.register_blueprint(role_bp)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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')
|
||||
@view_logging_required
|
||||
@permission_required("admin:dept:main")
|
||||
def dept_index():
|
||||
return render_template('admin/department/dept.html')
|
||||
|
||||
|
||||
@index_bp.get('/dept/add')
|
||||
@view_logging_required
|
||||
@permission_required("admin:dept:add")
|
||||
def add():
|
||||
return render_template('admin/department/dept_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('admin/department/dept_edit.html', dept=dept)
|
||||
@@ -0,0 +1,18 @@
|
||||
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('admin/file/photo.html')
|
||||
|
||||
|
||||
@index_bp.get('/file/photo/add')
|
||||
@view_logging_required
|
||||
@permission_required("admin:file:main")
|
||||
def file_photo_add():
|
||||
return render_template('admin/file/photo_add.html')
|
||||
@@ -0,0 +1,24 @@
|
||||
from flask import Blueprint
|
||||
from flask import render_template
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
index_bp = Blueprint('index', __name__)
|
||||
|
||||
|
||||
@index_bp.route('/')
|
||||
def index():
|
||||
return render_template('index/index.html')
|
||||
|
||||
|
||||
# 首页
|
||||
@index_bp.get('/admin/')
|
||||
@login_required
|
||||
def admin_index():
|
||||
return render_template('index/admin_index.html', user=current_user)
|
||||
|
||||
|
||||
# 控制台页面
|
||||
@index_bp.get('/admin/welcome')
|
||||
@login_required
|
||||
def welcome():
|
||||
return render_template('index/welcome.html')
|
||||
@@ -0,0 +1,69 @@
|
||||
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('/')
|
||||
@permission_required("admin:log:main")
|
||||
def index():
|
||||
return render_template('admin/logs_temp/main.html')
|
||||
|
||||
|
||||
@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)
|
||||
@@ -0,0 +1,32 @@
|
||||
from flask import session, redirect, render_template, url_for
|
||||
from flask_login import login_required, logout_user, current_user
|
||||
|
||||
from common.gen_captcha import get_captcha_image
|
||||
from common.utils.http import success_api
|
||||
|
||||
# 获取验证码
|
||||
from applications.view import index_bp
|
||||
|
||||
|
||||
@index_bp.get('/passport/getCaptcha')
|
||||
def get_captcha():
|
||||
resp, code = get_captcha_image()
|
||||
session["code"] = code
|
||||
return resp
|
||||
|
||||
|
||||
# 退出登录
|
||||
@index_bp.post('/passport/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
session.pop('permissions')
|
||||
return success_api(message="注销成功")
|
||||
|
||||
|
||||
@index_bp.get('/passport/login')
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('admin.index'))
|
||||
# TODO 分离视图操作 最终实现接口登录与视图登录两套逻辑
|
||||
return render_template('index/login.html')
|
||||
@@ -1,13 +0,0 @@
|
||||
"""
|
||||
前台公开蓝图:导航聚合页 / 分类详情页
|
||||
|
||||
URL 前缀:/site(避免与后台 / 路由冲突)
|
||||
鉴权要求:无(任何用户都可访问,含未登录匿名用户)
|
||||
|
||||
注意:不要给这些视图加 @authorize 装饰器,否则会强制登录_required。
|
||||
"""
|
||||
from applications.view.public.nav import bp as nav_bp
|
||||
|
||||
|
||||
# 把 nav 蓝图作为 public 包的对外入口
|
||||
bp = nav_bp
|
||||
@@ -1,80 +0,0 @@
|
||||
"""
|
||||
前台导航页视图(无需登录)。
|
||||
|
||||
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
|
||||
@@ -0,0 +1,32 @@
|
||||
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/')
|
||||
@view_logging_required
|
||||
@permission_required("admin:power:main")
|
||||
def rights_index():
|
||||
return render_template('admin/rights/rights.html')
|
||||
|
||||
|
||||
@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('admin/rights/rights_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('admin/rights/rights_add.html')
|
||||
@@ -0,0 +1,39 @@
|
||||
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('/')
|
||||
@view_logging_required
|
||||
@permission_required("admin:role:main")
|
||||
def main():
|
||||
return render_template('admin/roles/roles.html')
|
||||
|
||||
|
||||
# 角色授权操作
|
||||
@role_bp.get('/power/<int:role_id>')
|
||||
@view_logging_required
|
||||
@permission_required("admin:role:power")
|
||||
def power(role_id):
|
||||
return render_template('admin/roles/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('admin/roles/roles_edit.html', role=role)
|
||||
|
||||
|
||||
@role_bp.get('/add')
|
||||
@view_logging_required
|
||||
@permission_required("admin:role:edit")
|
||||
def role_add():
|
||||
return render_template('admin/roles/roles_add.html')
|
||||
@@ -1,36 +0,0 @@
|
||||
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)
|
||||
@@ -1,181 +0,0 @@
|
||||
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="删除部门成功")
|
||||
@@ -1,299 +0,0 @@
|
||||
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="删除成功")
|
||||
@@ -1,85 +0,0 @@
|
||||
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="删除失败")
|
||||
@@ -1,14 +0,0 @@
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
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)
|
||||
@@ -1,98 +0,0 @@
|
||||
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="批量删除成功")
|
||||
@@ -1,170 +0,0 @@
|
||||
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)
|
||||
@@ -1,142 +0,0 @@
|
||||
"""
|
||||
后台导航管理视图(需要登录 + 权限码 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="已禁用")
|
||||
@@ -1,91 +0,0 @@
|
||||
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="注销成功")
|
||||
@@ -1,203 +0,0 @@
|
||||
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="批量删除成功")
|
||||
@@ -1,265 +0,0 @@
|
||||
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')
|
||||
@@ -1,208 +0,0 @@
|
||||
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="角色删除成功")
|
||||
@@ -1,236 +0,0 @@
|
||||
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="数据错误")
|
||||
@@ -0,0 +1,49 @@
|
||||
from flask import render_template
|
||||
from flask_login import login_required, current_user
|
||||
from sqlalchemy import desc
|
||||
|
||||
from common.utils.rights import permission_required, view_logging_required
|
||||
from models import LogModel, RoleModel, UserModel
|
||||
from . import index_bp
|
||||
|
||||
|
||||
# 用户增加
|
||||
@index_bp.get('/users/')
|
||||
@view_logging_required
|
||||
@permission_required("admin:user:main")
|
||||
def users_main():
|
||||
return render_template('admin/users/users.html')
|
||||
|
||||
|
||||
@index_bp.get('/users/add')
|
||||
@view_logging_required
|
||||
@permission_required("admin:user:add")
|
||||
def users_add_view():
|
||||
roles = RoleModel.query.all()
|
||||
return render_template('admin/users/users_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('admin/users/users_edit.html', user=user, roles=roles, checked_roles=checked_roles)
|
||||
|
||||
|
||||
@index_bp.get('/users/center')
|
||||
@login_required
|
||||
def users_center():
|
||||
user_logs = LogModel.query.filter_by(url='/passport/login').filter_by(uid=current_user.id).order_by(
|
||||
desc(LogModel.create_at)).limit(10)
|
||||
return render_template('admin/users/profile.html', user_info=current_user, user_logs=user_logs)
|
||||
|
||||
|
||||
@index_bp.get('/users/avatar')
|
||||
def users_avatar_view():
|
||||
return render_template('admin/users/profile_avatar.html')
|
||||
@@ -0,0 +1,5 @@
|
||||
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'])
|
||||
@@ -0,0 +1,504 @@
|
||||
# -*- 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
|
||||
@@ -0,0 +1,43 @@
|
||||
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
|
||||
@@ -0,0 +1,35 @@
|
||||
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,
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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
|
||||
@@ -0,0 +1,30 @@
|
||||
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
|
||||
@@ -0,0 +1,51 @@
|
||||
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 = "root"
|
||||
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
|
||||
@@ -1,25 +0,0 @@
|
||||
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"]
|
||||
@@ -1,29 +0,0 @@
|
||||
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
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,20 +0,0 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user