重大更新到2.0.0

This commit is contained in:
ktianc 2023-08-24 23:14:45 +08:00
parent 45a8abe6bc
commit 09859d4d80
44 changed files with 1001 additions and 743 deletions

View File

@ -19,7 +19,7 @@
Kinit 是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
- 后端采用现代、快速(高性能) [FastAPI](https://fastapi.tiangolo.com/zh/) 异步框架 + 自动生成交互式API文档 + (强制类型约束)[Pydantic](https://docs.pydantic.dev/1.10/) + (高效率)[SQLAlchemy](https://www.sqlalchemy.org/)
- 后端采用现代、快速(高性能) [FastAPI](https://fastapi.tiangolo.com/zh/) 异步框架 + 自动生成交互式API文档 + (强制类型约束)[Pydantic](https://docs.pydantic.dev/1.10/) + (高效率)[SQLAlchemy 2.0]([SQLAlchemy Documentation — SQLAlchemy 2.0 Documentation](https://docs.sqlalchemy.org/en/20/index.html))
- PC端采用 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin) 、[Vue3](https://cn.vuejs.org/guide/introduction.html)、[Element Plus](https://element-plus.gitee.io/zh-CN/guide/design.html)、[TypeScript](https://www.tslang.cn/)等主流技术开发;
- 移动端采用 [uni-app](https://uniapp.dcloud.net.cn/component/)[Vue2](https://v2.cn.vuejs.org/v2/guide/)[uView 2](https://www.uviewui.com/components/intro.html)为主要技术开发;
- 后端加入 [Typer](https://typer.tiangolo.com/) 命令行应用,简单化数据初始化,数据表模型迁移等操作;
@ -34,24 +34,14 @@ Kinit 是一套全部开源的快速开发平台,毫无保留给个人及企
[vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin)一套基于vue3、element-plus、typescript4、vite3的后台集成方案
[RuoYi 若依官方网站](http://www.ruoyi.vip/)RuoYi 是一个后台管理系统基于经典技术组合Spring Boot、Apache Shiro、MyBatis、Thymeleaf主要目的让开发者注重专注业务降低技术难度从而节省人力成本缩短项目周期提高软件安全质量。
[RuoYi 若依官方网站](http://www.ruoyi.vip/)RuoYi 是一个优秀的 Java 后台管理系统
[django-vue-admin](https://gitee.com/liqianglog/django-vue-admin)基于RBAC模型的权限控制的一整套基础开发平台前后端分离后端采用 django+django-rest-framework前端采用 vue+ElementUI。
[django-vue-admin](https://gitee.com/liqianglog/django-vue-admin)django-vue-admin 是一个优秀的基于 Django 开发后台管理系统
[Ant Design Pro](https://preview.pro.ant.design/dashboard/analysis):开箱即用的中台前端/设计解决方案
[Gin-Vue-Admin](https://demo.gin-vue-admin.com)基于vite+vue3+gin搭建的开发基础平台支持TS,JS混用集成jwt鉴权权限管理动态路由显隐可控组件分页封装多点登录拦截资源权限上传下载代码生成器表单生成器等开发必备功能。
[Vben Admin](https://doc.vvbin.cn/guide/introduction.html)Vue Vben Admin 是一个免费开源的中后台模版。使用了最新的`vue3`,`vite2`,`TypeScript`等主流技术开发,开箱即用的中后台前端解决方案,也可用于学习参考。
[中华人民共和国行政区划 (github.com)](https://github.com/modood/Administrative-divisions-of-China):省级(省份)、 地级(城市)、 县级(区县)、 乡级(乡镇街道)、 村级(村委会居委会) ,中国省市区镇村二级三级四级五级联动地址数据。
[Vue Admin Plus](https://vue-admin-beautiful.com/admin-plus/#/index)vue-admin-better是github开源admin中最优秀的集成框架之一它是国内首个基于vue3.0的开源admin项目同时支持电脑手机平板默认分支使用vue3.x+antdv开发master分支使用的是vue2.x+element开发。
[小诺开源技术 (xiaonuo.vip)](https://www.xiaonuo.vip/):国内首个国密前后端分离快速开发平台
[my-web](https://gitee.com/newgateway/my-web)MyWeb 是一个企业级中后台前端/设计解决方案的的项目工程模板,它可以帮助你快速搭建企业级中后台产品原型
## 在线体验
PC端演示地址http://kinit.ktianc.top
@ -134,18 +124,13 @@ github地址https://github.com/vvandk/kinit
- [x] 我的基础功能:编辑资料、头像修改、密码修改、常见问题、关于我们等
## TODO
- [ ] 多租户方案
- [ ] 自动化编排服务使用docker-compose部署项目
- [ ] 可视化低代码表单:接入低代码表单,[vform3](https://vform666.com/vform3.html?from=element_plus)
## 前序准备
### 后端技术
- [Python3](https://www.python.org/downloads/windows/):熟悉 python3 基础语法
- [FastAPI](https://fastapi.tiangolo.com/zh/) - 熟悉后台接口 Web 框架.
- [FastAPI](https://fastapi.tiangolo.com/zh/) - 熟悉后台接口 Web 框架
- [SQLAlchemy 2.0](https://docs.sqlalchemy.org/en/20/index.html) - 数据数据库操作
- [Typer](https://typer.tiangolo.com/) - 熟悉命令行工具的使用
- [MySQL](https://www.mysql.com/) 和 [MongoDB](https://www.mongodb.com/) 和 [Redis](https://redis.io/) - 熟悉数据存储数据库
- [iP查询接口文档](https://user.ip138.com/ip/doc)IP查询第三方服务有1000次的免费次数

View File

@ -2,7 +2,7 @@
import { reactive, ref, unref, watch } from 'vue'
import { Form } from '@/components/Form'
import { useI18n } from '@/hooks/web/useI18n'
import { ElButton, ElCheckbox, ElLink } from 'element-plus'
import { ElButton, ElLink } from 'element-plus'
import { useForm } from '@/hooks/web/useForm'
import { getRoleMenusApi } from '@/api/login'
import { useAuthStoreWithOut } from '@/store/modules/auth'
@ -106,7 +106,7 @@ const schema = reactive<FormSchema[]>([
])
const iconSize = 30
const remember = ref(false)
// const remember = ref(false)
const { register, elFormRef, methods } = useForm()
const loading = ref(false)
const iconColor = '#999'
@ -189,7 +189,7 @@ const toTelephoneSignIn = () => {
<template #tool>
<div class="flex justify-between items-center w-[100%]">
<ElCheckbox v-model="remember" :label="t('login.remember')" size="small" />
<!-- <ElCheckbox v-model="remember" :label="t('login.remember')" size="small" /> -->
<ElLink type="primary" :underline="false">{{ t('login.forgetPassword') }}</ElLink>
</div>
</template>

View File

@ -75,7 +75,7 @@ defineExpose({
let selectAll = ref(false)
let defaultExpandAll = ref(true)
let checkStrictly = ref(true) //
let checkStrictly = ref(false) // bug
// key
const getTreeNodeKeys = (nodes: Recordable[]): number[] => {
@ -119,7 +119,7 @@ function handleCheckedTreeNodeAll() {
label="全选/全不选"
size="large"
/>
<ElCheckbox v-model="checkStrictly" label="父子联动" size="large" />
<!-- <ElCheckbox v-model="checkStrictly" label="父子联动" size="large" /> -->
</div>
<div class="max-h-390px border p-10px overflow-auto">
<ElTree

View File

@ -74,7 +74,7 @@ getData()
:editorConfig="editorConfig"
/>
<div class="mt-10px" style="float: right">
<ElButton type="primary" @click="save">立即保存</ElButton>
<ElButton :loading="loading" type="primary" @click="save">立即保存</ElButton>
</div>
</template>

View File

@ -59,7 +59,7 @@ getData()
<template>
<Form @register="register">
<template #active>
<ElButton type="primary" @click="save">立即提交</ElButton>
<ElButton :loading="loading" type="primary" @click="save">立即提交</ElButton>
</template>
</Form>
</template>

View File

@ -152,7 +152,7 @@ getData()
</template>
<template #active>
<ElButton type="primary" @click="save">立即提交</ElButton>
<ElButton :loading="loading" type="primary" @click="save">立即提交</ElButton>
</template>
</Form>
</template>

View File

@ -59,7 +59,7 @@ getData()
<template>
<Form @register="register">
<template #active>
<ElButton type="primary" @click="save">立即提交</ElButton>
<ElButton :loading="loading" type="primary" @click="save">立即提交</ElButton>
</template>
</Form>
</template>

View File

@ -73,7 +73,7 @@ getData()
:editorConfig="editorConfig"
/>
<div class="mt-10px" style="float: right">
<ElButton type="primary" @click="save">立即保存</ElButton>
<ElButton :loading="loading" type="primary" @click="save">立即保存</ElButton>
</div>
</template>

View File

@ -59,7 +59,7 @@ getData()
<template>
<Form @register="register">
<template #active>
<ElButton type="primary" @click="save">立即提交</ElButton>
<ElButton :loading="loading" type="primary" @click="save">立即提交</ElButton>
</template>
</Form>
</template>

View File

@ -1,8 +1,8 @@
# FastAPI 项目
fastapi 源代码https://github.com/tiangolo/fastapi
fastapi Githubhttps://github.com/tiangolo/fastapi
fastapi 中文文档https://fastapi.tiangolo.com/zh/
fastapi 官方文档https://fastapi.tiangolo.com/zh/
fastapi 更新说明https://fastapi.tiangolo.com/zh/release-notes/
@ -16,25 +16,50 @@ alembic 中文文档https://hellowac.github.io/alembic_doc/zh/_front_matter.h
Typer 官方文档https://typer.tiangolo.com/
SQLAlchemy 2.0 (官方): https://docs.sqlalchemy.org/en/20/intro.html#installation
SQLAlchemy 1.4 迁移到 2.0 官方https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html#whatsnew-20-orm-declarative-typing
PEP 484 语法官方https://peps.python.org/pep-0484/
## 项目结构
使用的是仿照 Django 项目结构:
- alembic数据库迁移配置目录
- versions_dev开发环境数据库迁移文件目录
- versions_pro生产环境数据库迁移文件目录
- env.py映射类配置文件
- application主项目配置目录也存放了主路由文件
- config基础环境配置文件
- development.py开发环境
- production.py生产环境
- settings.py主项目配置文件
- urls.py主路由文件
- apps项目的app存放目录
- vadmin基础服务
- auth用户 - 角色 - 菜单接口服务
- modelsORM 模型目录
- params查询参数依赖项目录
- schemaspydantic 模型,用于数据库序列化操作目录
- utils登录认证功能接口服务
- curd.py数据库操作
- views.py视图函数
- core核心文件目录
- crud.py关系型数据库操作核心封装
- database.py关系型数据库核心配置
- data_types.py自定义数据类型
- exception.py异常处理
- logger日志处理核心配置
- middleware.py中间件核心配置
- dependencies.py常用依赖项
- event.py全局事件
- mongo_manage.pymongodb 数据库操作核心封装
- validator.pypydantic 模型重用验证器
- dbORM模型基类
- logs日志目录
- static静态资源存放目录
- tests测试接口文件目录
- utils封装的一些工具类目录
- main.py主程序入口文件
- alembic.ini数据库迁移配置文件
@ -43,7 +68,9 @@ Typer 官方文档https://typer.tiangolo.com/
开发语言Python 3.10
开发框架Fastapi 0.95.0
开发框架Fastapi 0.101.1
ORM 框架SQLAlchemy 2.0.20
## 开发工具
@ -66,14 +93,28 @@ pip3 install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
# 第三方源:
1. 阿里源: https://mirrors.aliyun.com/pypi/simple/
# 线上安装更新依赖库
/opt/env/kinit-pro-310/bin/pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
```
### 数据初始化
```shell
# 项目根目录下执行,需提前创建好数据库
# 项目根目录下执行,需提前创建好数据库,并且数据库应该为空
# 会自动将模型迁移到数据库,并生成初始化数据
# 在执行前一定要确认要操作的环境与application/settings.DEBUG 设置的环境是一致的,
# 不然会导致创建表和生成数据不在一个数据库中!!!!!!!!!!!!!!!!!!!!!!
# 比如要初始化开发环境那么env参数应该为 dev并且 application/settings.DEBUG 应该 = True
# 比如要初始化生产环境那么env参数应该为 pro并且 application/settings.DEBUG 应该 = False
# 生产环境
python main.py init
# 开发环境
python main.py init --env dev
```
### 运行启动
@ -92,7 +133,6 @@ http://127.0.0.1:9000/docs
```
Git更新ignore文件直接修改gitignore是不会生效的需要先去掉已经托管的文件修改完成之后再重新添加并提交。
```
第一步:
git rm -r --cached .
@ -112,15 +152,19 @@ git commit -m "clear cached"
# 执行命令(生产环境):
python main.py migrate
# 执行命令(测试环境):
# 执行命令(开发环境):
python main.py migrate --env dev
# 开发环境的原命令
alembic --name dev revision --autogenerate -m 2.0
alembic --name dev upgrade head
```
生成迁移文件后会在alembic迁移目录中的version目录中多个迁移文件
## 查询数据
### 查询过滤
### 自定义的一些查询过滤
```python
# 日期查询
@ -160,7 +204,49 @@ python main.py migrate --env dev
代码部分:
![image-20230514113859232](D:\programming\ktianc\project\kinit-pro\images\image-20230514113859232.png)
```python
def __dict_filter(self, **kwargs) -> list[BinaryExpression]:
"""
字典过滤
:param model:
:param kwargs:
"""
conditions = []
for field, value in kwargs.items():
if value is not None and value != "":
attr = getattr(self.model, field)
if isinstance(value, tuple):
if len(value) == 1:
if value[0] == "None":
conditions.append(attr.is_(None))
elif value[0] == "not None":
conditions.append(attr.isnot(None))
else:
raise CustomException("SQL查询语法错误")
elif len(value) == 2 and value[1] not in [None, [], ""]:
if value[0] == "date":
# 根据日期查询, 关键函数是func.time_format和func.date_format
conditions.append(func.date_format(attr, "%Y-%m-%d") == value[1])
elif value[0] == "like":
conditions.append(attr.like(f"%{value[1]}%"))
elif value[0] == "in":
conditions.append(attr.in_(value[1]))
elif value[0] == "between" and len(value[1]) == 2:
conditions.append(attr.between(value[1][0], value[1][1]))
elif value[0] == "month":
conditions.append(func.date_format(attr, "%Y-%m") == value[1])
elif value[0] == "!=":
conditions.append(attr != value[1])
elif value[0] == ">":
conditions.append(attr > value[1])
elif value[0] == "<=":
conditions.append(attr <= value[1])
else:
raise CustomException("SQL查询语法错误")
else:
conditions.append(attr == value)
return conditions
```
示例:
@ -168,120 +254,51 @@ python main.py migrate --env dev
```python
users = UserDal(db).get_datas(limit=0, id=("in", [1,2,4,6]), email=("not None"), name=("like", "李"))
```
### v_join_query
外键字段查询,内连接
以常见问题类别表为例:
首先需要在 `crud.py/IssueCategoryDal``__init__` 方法中定义 `key_models`
```python
class IssueCategoryDal(DalBase):
def __init__(self, db: AsyncSession):
key_models = {
# 外键字段名,也可以自定义
"create_user": {
# 外键对应的orm模型
"model": vadminAuthModels.VadminUser,
# 如果对同一个模型只有一个外键关联时,下面这个 onclause 可以省略不写,一个以上时必须写,需要分清楚要查询的是哪个
# 这里其实可以省略不写,但是为了演示这里写出来了
"onclause": models.VadminIssueCategory.create_user_id == vadminAuthModels.VadminUser.id
}
}
super(IssueCategoryDal, self).__init__(
db,
models.VadminIssueCategory,
schemas.IssueCategorySimpleOut,
key_models
# limit=0表示返回所有结果数据
# 这里的 get_datas 默认返回的是 pydantic 模型数据
# 如果需要返回用户对象列表,使用如下语句:
users = UserDal(db).get_datas(
limit=0,
id=("in", [1,2,4,6]),
email=("not None"),
name=("like", "李"),
v_return_objs=True
)
```
使用案例:
查询所有用户id为1或2或 4或6并且email不为空并且名称包括李
查询第一页,每页两条数据,并返回总数,同样可以通过 `get_datas` 实现原始查询方式:
```python
async def test(self):
"""
v_join_query 示例方法
获取用户名称包含李 创建出的常见问题类别
"""
v_join_query = {
# 与 key_models 中定义的外键字段名定义的一样
"create_user": {
# 外键表字段名:查询值
"name": ("like", "李")
}
}
v_options = [joinedload(self.model.create_user)]
datas = self.get_datas(limit=0, v_join_query=v_join_query, v_options=v_options)
```
v_where = [VadminUser.id.in_([1,2,4,6]), VadminUser.email.isnot(None), VadminUser.name.like(f"%李%")]
users, count = UserDal(db).get_datas(limit=2, v_where=v_where, v_return_count=True)
完整案例:
```python
class IssueCategoryDal(DalBase):
def __init__(self, db: AsyncSession):
key_models = {
# 外键字段名,也可以自定义
"create_user": {
# 外键对应的orm模型
"model": vadminAuthModels.VadminUser,
# 如果对同一个模型只有一个外键关联时,下面这个 onclause 可以省略不写,一个以上时必须写,需要分清楚要查询的是哪个
# 这里其实可以省略不写,但是为了演示这里写出来了
"onclause": models.VadminIssueCategory.create_user_id == vadminAuthModels.VadminUser.id
}
}
super(IssueCategoryDal, self).__init__(
db,
models.VadminIssueCategory,
schemas.IssueCategorySimpleOut,
key_models
# 这里的 get_datas 默认返回的是 pydantic 模型数据
# 如果需要返回用户对象列表,使用如下语句:
users, count = UserDal(db).get_datas(
limit=2,
v_where=v_where,
v_return_count=True
v_return_objs=True
)
async def test(self):
"""
v_join_query 示例方法
获取用户名称包含李 创建出的常见问题类别
"""
v_join_query = {
# 与 key_models 中定义的外键字段名定义的一样
"create_user": {
# 外键表字段名:查询值
"name": ("like", "李")
}
}
v_options = [joinedload(self.model.create_user)]
datas = self.get_datas(limit=0, v_join_query=v_join_query, v_options=v_options)
```
### v_or
### 外键查询示例
或逻辑运算查询
语法:
以常见问题表为主表查询出创建用户名称为kinit的用户创建了哪些常见问题并加载出用户信息
```python
# 普通查询
v_or = [(字段名称, 值), (字段名称, 值), ... ]
# 模糊查询
v_or = [(字段名称, ("like", 值)), (字段名称, ("like", 值)), ... ]
# 组合查询
v_or = [(字段名称, ("like", 值)), (字段名称, ("in", [值, 值, 值, ...])), ... ]
# 外键查询,需要先定义 key_models
v_or = [("fk", key_models 中定义的外键字段名, 外键表字段名称, ("like", 值)), ("fk", key_models 中定义的外键字段名, 外键表字段名称, ("like", 值)), ... ]
```
比如查询一个用户手机号为`13409090909`或者`15390909090`
```python
v_or = [("telephone", "13409090909"), ("telephone", "15390909090") ]
user = UserDal(db).get_data(v_or=v_or)
v_options = [joinedload(VadminIssue.create_user)]
v_join = [["create_user"]]
v_where = [VadminUser.name == "kinit"]
datas = await crud.IssueCategoryDal(auth.db).get_datas(
limit=0,
v_options=options,
v_join=v_join,
v_where=v_where,
v_return_objs=True
)
```

View File

@ -11,7 +11,7 @@ from fastapi.security import OAuth2PasswordBearer
"""
系统版本
"""
VERSION = "1.10.5"
VERSION = "2.0.0"
"""安全警告: 不要在生产中打开调试运行!"""
DEBUG = True
@ -44,8 +44,14 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
如果是与认证关联性比较强的接口则无法使用
"""
OAUTH_ENABLE = True
"""登录认证视图"""
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/api/login", auto_error=True) if OAUTH_ENABLE else lambda: ""
"""
配置 OAuth2 密码流认证方式
官方文档https://fastapi.tiangolo.com/zh/tutorial/security/first-steps/#fastapi-oauth2passwordbearer
auto_error:(bool) 可选参数默认为 True当验证失败时如果设置为 TrueFastAPI 将自动返回一个 401 未授权的响应如果设置为 False你需要自己处理身份验证失败的情况
这里的 auto_error 设置为 False 是因为存在 OpenAuth开放认证无认证也可以访问
如果设置为 True那么 FastAPI 会自动报错即无认证时 OpenAuth 会失效所以不能使用 True
"""
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/api/login", auto_error=False) if OAUTH_ENABLE else lambda: ""
"""安全的随机密钥,该密钥将用于对 JWT 令牌进行签名"""
SECRET_KEY = 'vgb0tnl9d58+6n-6h-ea&u^1#s0ccp!794=kbvqacjq75vzps$'
"""用于设定 JWT 令牌签名算法"""

View File

@ -10,6 +10,7 @@ from typing import Any
from aioredis import Redis
from fastapi import UploadFile
from sqlalchemy.orm import joinedload
from sqlalchemy.orm.strategy_options import _AbstractLoad
from core.exception import CustomException
from fastapi.encoders import jsonable_encoder
from sqlalchemy import select
@ -26,10 +27,11 @@ from utils.tools import test_password
from . import models, schemas
from application import settings
from utils.excel.excel_manage import ExcelManage
from apps.vadmin.system import crud as vadminSystemCRUD
from apps.vadmin.system import crud as vadmin_system_crud
import copy
from utils import status
from utils.wx.oauth import WXOAuth
from datetime import datetime
class UserDal(DalBase):
@ -45,13 +47,24 @@ class UserDal(DalBase):
def __init__(self, db: AsyncSession):
super(UserDal, self).__init__(db, models.VadminUser, schemas.UserSimpleOut)
async def update_login_info(self, user: models.VadminUser, last_ip: str) -> None:
"""
更新当前登录信息
:param user: 用户对象
:param last_ip: 最近一次登录 IP
:return:
"""
user.last_ip = last_ip
user.last_login = datetime.now()
await self.db.flush()
async def create_data(
self,
data: schemas.UserIn,
v_options: list = None,
v_options: list[_AbstractLoad] = None,
v_return_obj: bool = False,
v_schema: Any = None
):
) -> Any:
"""
创建用户
"""
@ -65,7 +78,7 @@ class UserDal(DalBase):
if data.role_ids:
roles = await RoleDal(self.db).get_datas(limit=0, id=("in", data.role_ids), v_return_objs=True)
for role in roles:
obj.roles.append(role)
obj.roles.add(role)
await self.flush(obj)
return await self.out_dict(obj, v_options, v_return_obj, v_schema)
@ -73,10 +86,10 @@ class UserDal(DalBase):
self,
data_id: int,
data: schemas.UserUpdate,
v_options: list = None,
v_options: list[_AbstractLoad] = None,
v_return_obj: bool = False,
v_schema: Any = None
):
) -> Any:
"""
更新用户信息
"""
@ -84,18 +97,18 @@ class UserDal(DalBase):
data_dict = jsonable_encoder(data)
for key, value in data_dict.items():
if key == "role_ids":
if obj.roles:
obj.roles.clear()
if value:
roles = await RoleDal(self.db).get_datas(limit=0, id=("in", value), v_return_objs=True)
if obj.roles:
obj.roles.clear()
for role in roles:
obj.roles.append(role)
obj.roles.add(role)
continue
setattr(obj, key, value)
await self.flush(obj)
return await self.out_dict(obj, None, v_return_obj, v_schema)
async def reset_current_password(self, user: models.VadminUser, data: schemas.ResetPwd):
async def reset_current_password(self, user: models.VadminUser, data: schemas.ResetPwd) -> None:
"""
重置密码
"""
@ -107,9 +120,8 @@ class UserDal(DalBase):
user.password = self.model.get_password_hash(data.password)
user.is_reset_password = True
await self.flush(user)
return True
async def update_current_info(self, user: models.VadminUser, data: schemas.UserUpdateBaseInfo):
async def update_current_info(self, user: models.VadminUser, data: schemas.UserUpdateBaseInfo) -> Any:
"""
更新当前用户基本信息
"""
@ -125,7 +137,7 @@ class UserDal(DalBase):
await self.flush(user)
return await self.out_dict(user)
async def export_query_list(self, header: list, params: UserParams):
async def export_query_list(self, header: list, params: UserParams) -> dict:
"""
导出用户查询列表为excel
"""
@ -133,7 +145,7 @@ class UserDal(DalBase):
# 获取表头
row = list(map(lambda i: i.get("label"), header))
rows = []
options = await vadminSystemCRUD.DictTypeDal(self.db).get_dicts_details(["sys_vadmin_gender"])
options = await vadmin_system_crud.DictTypeDal(self.db).get_dicts_details(["sys_vadmin_gender"])
for user in datas:
data = []
for item in header:
@ -142,6 +154,8 @@ class UserDal(DalBase):
value = getattr(user, field, "")
if field == "is_active":
value = "可用" if value else "停用"
elif field == "is_staff":
value = "" if value else ""
elif field == "gender":
result = list(filter(lambda i: i["value"] == value, options["sys_vadmin_gender"]))
value = result[0]["label"] if result else ""
@ -154,7 +168,7 @@ class UserDal(DalBase):
em.close()
return {"url": file_url, "filename": "用户列表.xlsx"}
async def get_import_headers_options(self):
async def get_import_headers_options(self) -> None:
"""
补全表头数据选项
"""
@ -165,13 +179,13 @@ class UserDal(DalBase):
role_options["options"] = [{"label": role.name, "value": role.id} for role in roles]
# 性别选择项
dict_types = await vadminSystemCRUD.DictTypeDal(self.db).get_dicts_details(["sys_vadmin_gender"])
dict_types = await vadmin_system_crud.DictTypeDal(self.db).get_dicts_details(["sys_vadmin_gender"])
gender_options = self.import_headers[3]
assert isinstance(gender_options, dict)
sys_vadmin_gender = dict_types.get("sys_vadmin_gender")
gender_options["options"] = [{"label": item["label"], "value": item["value"]} for item in sys_vadmin_gender]
async def download_import_template(self):
async def download_import_template(self) -> dict:
"""
下载用户最新版导入模板
"""
@ -181,7 +195,7 @@ class UserDal(DalBase):
em.close()
return {"url": em.file_url, "filename": "用户导入模板.xlsx"}
async def import_users(self, file: UploadFile):
async def import_users(self, file: UploadFile) -> dict:
"""
批量导入用户数据
"""
@ -206,7 +220,7 @@ class UserDal(DalBase):
"error_url": im.generate_error_url()
}
async def init_password(self, ids: list[int]):
async def init_password(self, ids: list[int]) -> list:
"""
初始化所选用户密码
将用户密码改为系统默认密码并将初始化密码状态改为false
@ -226,7 +240,7 @@ class UserDal(DalBase):
await self.db.flush()
return result
async def init_password_send_sms(self, ids: list[int], rd: Redis):
async def init_password_send_sms(self, ids: list[int], rd: Redis) -> list:
"""
初始化所选用户密码并发送通知短信
将用户密码改为系统默认密码并将初始化密码状态改为false
@ -242,13 +256,13 @@ class UserDal(DalBase):
try:
send_result = (await sms.main_async(password=password))[0]
user["send_sms_status"] = send_result
user["send_sms_msg"] = "" if send_result else "发送失败,请联系管理员"
user["send_sms_msg"] = "" if send_result else "短信发送失败,请联系管理员"
except CustomException as e:
user["send_sms_status"] = False
user["send_sms_msg"] = e.msg
return result
async def init_password_send_email(self, ids: list[int], rd: Redis):
async def init_password_send_email(self, ids: list[int], rd: Redis) -> list:
"""
初始化所选用户密码并发送通知邮件
将用户密码改为系统默认密码并将初始化密码状态改为false
@ -268,7 +282,7 @@ class UserDal(DalBase):
try:
send_result = await es.send_email([email], subject, body)
user["send_sms_status"] = send_result
user["send_sms_msg"] = "" if send_result else "发送失败,请联系管理员"
user["send_sms_msg"] = "" if send_result else "短信发送失败,请联系管理员"
except CustomException as e:
user["send_sms_status"] = False
user["send_sms_msg"] = e.msg
@ -277,7 +291,7 @@ class UserDal(DalBase):
user["send_sms_msg"] = "未获取到邮箱地址"
return result
async def update_current_avatar(self, user: models.VadminUser, file: UploadFile):
async def update_current_avatar(self, user: models.VadminUser, file: UploadFile) -> str:
"""
更新当前用户头像
"""
@ -286,7 +300,7 @@ class UserDal(DalBase):
await self.flush(user)
return result
async def update_wx_server_openid(self, code: str, user: models.VadminUser, redis: Redis):
async def update_wx_server_openid(self, code: str, user: models.VadminUser, redis: Redis) -> bool:
"""
更新用户服务端微信平台openid
"""
@ -299,7 +313,7 @@ class UserDal(DalBase):
await self.flush(user)
return True
async def delete_datas(self, ids: list[int], v_soft: bool = False, **kwargs):
async def delete_datas(self, ids: list[int], v_soft: bool = False, **kwargs) -> None:
"""
删除多个用户软删除
删除后清空所关联的角色
@ -323,16 +337,16 @@ class RoleDal(DalBase):
async def create_data(
self,
data: schemas.RoleIn,
v_options: list = None,
v_options: list[_AbstractLoad] = None,
v_return_obj: bool = False,
v_schema: Any = None
):
) -> Any:
"""创建数据"""
obj = self.model(**data.model_dump(exclude={'menu_ids'}))
menus = await MenuDal(db=self.db).get_datas(limit=0, id=("in", data.menu_ids), v_return_objs=True)
if data.menu_ids:
menus = await MenuDal(db=self.db).get_datas(limit=0, id=("in", data.menu_ids), v_return_objs=True)
for menu in menus:
obj.menus.append(menu)
obj.menus.add(menu)
await self.flush(obj)
return await self.out_dict(obj, v_options, v_return_obj, v_schema)
@ -340,37 +354,37 @@ class RoleDal(DalBase):
self,
data_id: int,
data: schemas.RoleIn,
v_options: list = None,
v_options: list[_AbstractLoad] = None,
v_return_obj: bool = False,
v_schema: Any = None
):
) -> Any:
"""更新单个数据"""
obj = await self.get_data(data_id, v_options=[joinedload(self.model.menus)])
obj_dict = jsonable_encoder(data)
for key, value in obj_dict.items():
if key == "menu_ids":
if obj.menus:
obj.menus.clear()
if value:
menus = await MenuDal(db=self.db).get_datas(limit=0, id=("in", value), v_return_objs=True)
if obj.menus:
obj.menus.clear()
for menu in menus:
obj.menus.append(menu)
obj.menus.add(menu)
continue
setattr(obj, key, value)
await self.flush(obj)
return await self.out_dict(obj, None, v_return_obj, v_schema)
async def get_role_menu_tree(self, role_id: int):
async def get_role_menu_tree(self, role_id: int) -> list:
role = await self.get_data(role_id, v_options=[joinedload(self.model.menus)])
return [i.id for i in role.menus]
async def get_select_datas(self):
async def get_select_datas(self) -> list:
"""获取选择数据,全部数据"""
sql = select(self.model)
queryset = await self.db.execute(sql)
return [schemas.RoleSelectOut.model_validate(i).model_dump() for i in queryset.scalars().all()]
queryset = await self.db.scalars(sql)
return [schemas.RoleSelectOut.model_validate(i).model_dump() for i in queryset.all()]
async def delete_datas(self, ids: list[int], v_soft: bool = False, **kwargs):
async def delete_datas(self, ids: list[int], v_soft: bool = False, **kwargs) -> None:
"""
删除多个角色硬删除
如果存在用户关联则无法删除
@ -378,8 +392,8 @@ class RoleDal(DalBase):
:param v_soft: 是否执行软删除
:param kwargs: 其他更新字段
"""
objs = await self.get_datas(limit=0, id=("in", ids), user_total_number=(">", 0), v_return_objs=True)
if objs:
user_count = await UserDal(self.db).get_count(v_join=[["roles"]], v_where=[models.VadminRole.id.in_(ids)])
if user_count > 0:
raise CustomException("无法删除存在用户关联的角色", code=400)
return await super(RoleDal, self).delete_datas(ids, v_soft, **kwargs)
@ -389,7 +403,7 @@ class MenuDal(DalBase):
def __init__(self, db: AsyncSession):
super(MenuDal, self).__init__(db, models.VadminMenu, schemas.MenuSimpleOut)
async def get_tree_list(self, mode: int):
async def get_tree_list(self, mode: int) -> list:
"""
1获取菜单树列表
2获取菜单树选择项添加/修改菜单时使用
@ -399,8 +413,8 @@ class MenuDal(DalBase):
sql = select(self.model).where(self.model.disabled == 0, self.model.is_delete == False)
else:
sql = select(self.model).where(self.model.is_delete == False)
queryset = await self.db.execute(sql)
datas = queryset.scalars().all()
queryset = await self.db.scalars(sql)
datas = list(queryset.all())
roots = filter(lambda i: not i.parent_id, datas)
if mode == 1:
menus = self.generate_tree_list(datas, roots)
@ -410,7 +424,7 @@ class MenuDal(DalBase):
raise CustomException("获取菜单失败,无可用选项", code=400)
return self.menus_order(menus)
async def get_routers(self, user: models.VadminUser):
async def get_routers(self, user: models.VadminUser) -> list:
"""
获取路由表
declare interface AppCustomRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
@ -425,10 +439,10 @@ class MenuDal(DalBase):
if any([i.is_admin for i in user.roles]):
sql = select(self.model) \
.where(self.model.disabled == 0, self.model.menu_type != "2", self.model.is_delete == False)
queryset = await self.db.execute(sql)
datas = queryset.scalars().all()
queryset = await self.db.scalars(sql)
datas = list(queryset.all())
else:
options = [joinedload(models.VadminUser.roles), joinedload("roles.menus")]
options = [joinedload(models.VadminUser.roles).subqueryload(models.VadminRole.menus)]
user = await UserDal(self.db).get_data(user.id, v_options=options)
datas = set()
for role in user.roles:
@ -492,7 +506,7 @@ class MenuDal(DalBase):
return data
@classmethod
def menus_order(cls, datas: list, order: str = "order", children: str = "children"):
def menus_order(cls, datas: list, order: str = "order", children: str = "children") -> list:
"""
菜单排序
"""
@ -502,7 +516,7 @@ class MenuDal(DalBase):
item[children] = sorted(item[children], key=lambda menu: menu[order])
return result
async def delete_datas(self, ids: list[int], v_soft: bool = False, **kwargs):
async def delete_datas(self, ids: list[int], v_soft: bool = False, **kwargs) -> None:
"""
删除多个菜单
如果存在角色关联则无法删除
@ -510,10 +524,30 @@ class MenuDal(DalBase):
:param v_soft: 是否执行软删除
:param kwargs: 其他更新字段
"""
options = [joinedload(self.model.roles)]
objs = await self.get_datas(limit=0, id=("in", ids), v_return_objs=True, v_options=options)
for obj in objs:
if obj.roles:
count = await RoleDal(self.db).get_count(v_join=[["menus"]], v_where=[self.model.id.in_(ids)])
if count > 0:
raise CustomException("无法删除存在角色关联的菜单", code=400)
return await super(MenuDal, self).delete_datas(ids, v_soft, **kwargs)
await super(MenuDal, self).delete_datas(ids, v_soft, **kwargs)
class TestDal(DalBase):
def __init__(self, db: AsyncSession):
super(TestDal, self).__init__(db, models.VadminUser, schemas.UserSimpleOut)
async def test(self):
# print("-----------------------开始------------------------")
options = [joinedload(self.model.roles)]
v_where = [self.model.id == 1, models.VadminRole.id == 1]
v_join = [[self.model.roles]]
v_start_sql = select(self.model)
result, count = await self.get_datas(
v_start_sql=v_start_sql,
v_join=v_join,
v_options=options,
v_where=v_where
)
if result:
print(result)
print(count)
# print("-----------------------结束------------------------")

View File

@ -7,7 +7,7 @@
# @desc : 简要说明
from .m2m import vadmin_user_roles, vadmin_role_menus
from .m2m import vadmin_auth_user_roles, vadmin_auth_role_menus
from .menu import VadminMenu
from .role import VadminRole
from .user import VadminUser

View File

@ -6,27 +6,22 @@
# @IDE : PyCharm
# @desc : 关联中间表
"""
Table 操作博客http://www.ttlsa.com/python/sqlalchemy-concise-guide/
"""
from db.db_base import Model
from sqlalchemy import Column, Table, Integer, ForeignKey, INT
from db.db_base import Base
from sqlalchemy import ForeignKey, Column, Table, Integer
vadmin_user_roles = Table(
'vadmin_auth_user_roles',
Model.metadata,
Column("id", INT, primary_key=True, unique=True, comment='主键ID', index=True, autoincrement=True),
Column('user_id', Integer, ForeignKey('vadmin_auth_user.id', ondelete='CASCADE'), primary_key=True),
Column('role_id', Integer, ForeignKey('vadmin_auth_role.id', ondelete='CASCADE'), primary_key=True),
vadmin_auth_user_roles = Table(
"vadmin_auth_user_roles",
Base.metadata,
Column("user_id", Integer, ForeignKey("vadmin_auth_user.id", ondelete="CASCADE")),
Column("role_id", Integer, ForeignKey("vadmin_auth_role.id", ondelete="CASCADE")),
)
vadmin_role_menus = Table(
'vadmin_auth_role_menus',
Model.metadata,
Column("id", INT, primary_key=True, unique=True, comment='主键ID', index=True, autoincrement=True),
Column('role_id', Integer, ForeignKey('vadmin_auth_role.id', ondelete='CASCADE'), primary_key=True),
Column('menu_id', Integer, ForeignKey('vadmin_auth_menu.id', ondelete='CASCADE'), primary_key=True),
vadmin_auth_role_menus = Table(
"vadmin_auth_role_menus",
Base.metadata,
Column("role_id", Integer, ForeignKey("vadmin_auth_role.id", ondelete="CASCADE")),
Column("menu_id", Integer, ForeignKey("vadmin_auth_menu.id", ondelete="CASCADE")),
)

View File

@ -7,39 +7,59 @@
# @desc : 菜单模型
from sqlalchemy.orm import relationship
from .m2m import vadmin_role_menus
from db.db_base import BaseModel
from sqlalchemy import Column, String, Boolean, Integer, ForeignKey
from sqlalchemy import String, Boolean, Integer, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
class VadminMenu(BaseModel):
__tablename__ = "vadmin_auth_menu"
__table_args__ = ({'comment': '菜单表'})
# class MenuTypes(Enum):
# dir = "0"
# menu = "1"
# button = "2"
title = Column(String(50), index=True, nullable=False, comment="名称")
icon = Column(String(50), comment="菜单图标")
redirect = Column(String(100), comment="重定向地址")
component = Column(String(50), comment="前端组件地址")
path = Column(String(50), comment="前端路由地址")
disabled = Column(Boolean, default=False, comment="是否禁用")
hidden = Column(Boolean, default=False, comment="是否隐藏")
order = Column(Integer, comment="排序")
menu_type = Column(String(8), comment="菜单类型")
parent_id = Column(ForeignKey("vadmin_auth_menu.id", ondelete='CASCADE'), comment="父菜单")
perms = Column(String(50), comment="权限标识", unique=False, nullable=True, index=True)
noCache = Column(Boolean, comment="如果设置为true则不会被 <keep-alive> 缓存(默认 false)", default=False)
breadcrumb = Column(Boolean, comment="如果设置为false则不会在breadcrumb面包屑中显示(默认 true)", default=True)
affix = Column(Boolean, comment="如果设置为true则会一直固定在tag项中(默认 false)", default=False)
noTagsView = Column(Boolean, comment="如果设置为true则不会出现在tag中(默认 false)", default=False)
canTo = Column(Boolean, comment="设置为true即使hidden为true也依然可以进行路由跳转(默认 false)", default=False)
alwaysShow = Column(Boolean, comment="""当你一个路由下面的 children 声明的路由大于1个时自动会变成嵌套的模式
title: Mapped[str] = mapped_column(String(50), comment="名称")
icon: Mapped[str | None] = mapped_column(String(50), comment="菜单图标")
redirect: Mapped[str | None] = mapped_column(String(100), comment="重定向地址")
component: Mapped[str | None] = mapped_column(String(50), comment="前端组件地址")
path: Mapped[str | None] = mapped_column(String(50), comment="前端路由地址")
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否禁用")
hidden: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否隐藏")
order: Mapped[int] = mapped_column(Integer, comment="排序")
menu_type: Mapped[str] = mapped_column(String(8), comment="菜单类型")
parent_id: Mapped[int | None] = mapped_column(
Integer,
ForeignKey("vadmin_auth_menu.id", ondelete='CASCADE'),
comment="父菜单"
)
perms: Mapped[str | None] = mapped_column(String(50), comment="权限标识", unique=False, index=True)
noCache: Mapped[bool] = mapped_column(
Boolean,
comment="如果设置为true则不会被 <keep-alive> 缓存(默认 false)",
default=False
)
breadcrumb: Mapped[bool] = mapped_column(
Boolean,
comment="如果设置为false则不会在breadcrumb面包屑中显示(默认 true)",
default=True
)
affix: Mapped[bool] = mapped_column(
Boolean,
comment="如果设置为true则会一直固定在tag项中(默认 false)",
default=False
)
noTagsView: Mapped[bool] = mapped_column(
Boolean,
comment="如果设置为true则不会出现在tag中(默认 false)",
default=False
)
canTo: Mapped[bool] = mapped_column(
Boolean,
comment="设置为true即使hidden为true也依然可以进行路由跳转(默认 false)",
default=False
)
alwaysShow: Mapped[bool] = mapped_column(
Boolean,
comment="""当你一个路由下面的 children 声明的路由大于1个时自动会变成嵌套的模式
只有一个时会将那个子路由当做根路由显示在侧边栏若你想不管路由下面的 children 声明的个数都显示你的根路由
你可以设置 alwaysShow: true这样它就会忽略之前定义的规则一直显示根路由(默认 true)""", default=True)
roles = relationship("VadminRole", back_populates='menus', secondary=vadmin_role_menus)
你可以设置 alwaysShow: true这样它就会忽略之前定义的规则一直显示根路由(默认 true)""",
default=True
)

View File

@ -6,28 +6,22 @@
# @IDE : PyCharm
# @desc : 角色模型
from sqlalchemy.orm import relationship
from sqlalchemy_utils import aggregated
from .user import VadminUser
from sqlalchemy.orm import relationship, Mapped, mapped_column
from db.db_base import BaseModel
from sqlalchemy import Column, String, Boolean, Integer, func
from .m2m import vadmin_user_roles, vadmin_role_menus
from sqlalchemy import String, Boolean, Integer
from .menu import VadminMenu
from .m2m import vadmin_auth_role_menus
class VadminRole(BaseModel):
__tablename__ = "vadmin_auth_role"
__table_args__ = ({'comment': '角色表'})
name = Column(String(50), index=True, nullable=False, comment="名称")
role_key = Column(String(50), index=True, nullable=False, comment="权限字符")
disabled = Column(Boolean, default=False, comment="是否禁用")
order = Column(Integer, comment="排序")
desc = Column(String(255), comment="描述")
is_admin = Column(Boolean, comment="是否为超级角色", default=False)
name: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="名称")
role_key: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="权限字符")
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否禁用")
order: Mapped[int | None] = mapped_column(Integer, comment="排序")
desc: Mapped[str | None] = mapped_column(String(255), comment="描述")
is_admin: Mapped[bool] = mapped_column(Boolean, comment="是否为超级角色", default=False)
users = relationship("VadminUser", back_populates='roles', secondary=vadmin_user_roles)
menus = relationship("VadminMenu", back_populates='roles', secondary=vadmin_role_menus)
@aggregated('users', Column(Integer, default=0, comment="用户总数"))
def user_total_number(self):
return func.count(VadminUser.id)
menus: Mapped[set[VadminMenu]] = relationship(secondary=vadmin_auth_role_menus)

View File

@ -6,13 +6,13 @@
# @IDE : PyCharm
# @desc : 用户模型
import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import relationship
from datetime import datetime
from sqlalchemy.orm import relationship, Mapped, mapped_column
from db.db_base import BaseModel
from sqlalchemy import Column, String, Boolean, DateTime
from sqlalchemy import String, Boolean, DateTime
from passlib.context import CryptContext
from .m2m import vadmin_user_roles
from .role import VadminRole
from .m2m import vadmin_auth_user_roles
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
@ -21,22 +21,26 @@ class VadminUser(BaseModel):
__tablename__ = "vadmin_auth_user"
__table_args__ = ({'comment': '用户表'})
avatar = Column(String(500), nullable=True, comment='头像')
telephone = Column(String(11), nullable=False, index=True, comment="手机号", unique=False)
email = Column(String(50), nullable=True, comment="邮箱地址")
name = Column(String(50), index=True, nullable=False, comment="姓名")
nickname = Column(String(50), nullable=True, comment="昵称")
password = Column(String(255), nullable=True, comment="密码")
gender = Column(String(8), nullable=True, comment="性别")
is_active = Column(Boolean, default=True, comment="是否可用")
is_reset_password = Column(Boolean, default=False, comment="是否已经重置密码,没有重置的,登陆系统后必须重置密码")
last_ip = Column(String(50), nullable=True, comment="最后一次登录IP")
last_login = Column(DateTime, nullable=True, comment="最近一次登录时间")
is_staff = Column(Boolean, default=False, comment="是否为工作人员")
wx_server_openid = Column(String(255), comment="服务端微信平台openid")
is_wx_server_openid = Column(Boolean, default=False, comment="是否已有服务端微信平台openid")
avatar: Mapped[str | None] = mapped_column(String(500), comment='头像')
telephone: Mapped[str] = mapped_column(String(11), nullable=False, index=True, comment="手机号", unique=False)
email: Mapped[str | None] = mapped_column(String(50), comment="邮箱地址")
name: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="姓名")
nickname: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="昵称")
password: Mapped[str] = mapped_column(String(255), nullable=True, comment="密码")
gender: Mapped[str | None] = mapped_column(String(8), nullable=True, comment="性别")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否可用")
is_reset_password: Mapped[bool] = mapped_column(
Boolean,
default=False,
comment="是否已经重置密码,没有重置的,登陆系统后必须重置密码"
)
last_ip: Mapped[str | None] = mapped_column(String(50), comment="最后一次登录IP")
last_login: Mapped[datetime | None] = mapped_column(DateTime, comment="最近一次登录时间")
is_staff: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否为工作人员")
wx_server_openid: Mapped[str | None] = mapped_column(String(255), comment="服务端微信平台openid")
is_wx_server_openid: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否已有服务端微信平台openid")
roles = relationship("VadminRole", back_populates='users', secondary=vadmin_user_roles)
roles: Mapped[set[VadminRole]] = relationship(secondary=vadmin_auth_user_roles)
# generate hash password
@staticmethod
@ -48,17 +52,6 @@ class VadminUser(BaseModel):
def verify_password(password: str, hashed_password: str) -> bool:
return pwd_context.verify(password, hashed_password)
async def update_login_info(self, db: AsyncSession, last_ip: str):
"""
更新当前登录信息
:param db: 数据库
:param last_ip: 最近一次登录 IP
:return:
"""
self.last_ip = last_ip
self.last_login = datetime.datetime.now()
await db.flush()
def is_admin(self) -> bool:
"""
获取该用户是否拥有最高权限

View File

@ -5,10 +5,11 @@
# @IDE : PyCharm
# @desc : 获取认证后的信息工具
from typing import Annotated
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from apps.vadmin.auth.crud import UserDal
from apps.vadmin.auth.models import VadminUser
from apps.vadmin.auth.models import VadminUser, VadminRole
from core.exception import CustomException
from utils import status
from .validation import AuthValidation
@ -28,7 +29,7 @@ class OpenAuth(AuthValidation):
async def __call__(
self,
request: Request,
token: str = Depends(settings.oauth2_scheme),
token: Annotated[str, Depends(settings.oauth2_scheme)],
db: AsyncSession = Depends(db_getter)
):
"""
@ -93,7 +94,7 @@ class FullAdminAuth(AuthValidation):
if not settings.OAUTH_ENABLE:
return Auth(db=db)
telephone = self.validate_token(request, token)
options = [joinedload(VadminUser.roles), joinedload("roles.menus")]
options = [joinedload(VadminUser.roles).subqueryload(VadminRole.menus)]
user = await UserDal(db).get_data(telephone=telephone, v_return_none=True, v_options=options, is_staff=True)
result = await self.validate_user(request, user, db)
permissions = self.get_user_permissions(user)

View File

@ -126,7 +126,7 @@ async def wx_login_for_access_token(
return ErrorResponse(msg=str(e))
# 更新登录时间
await user.update_login_info(db, request.client.host)
await UserDal(db).update_login_info(user, request.client.host)
# 登录成功创建 token
access_token = LoginManage.create_token({"sub": user.telephone, "is_refresh": False})

View File

@ -7,13 +7,12 @@
# @desc : 登录验证装饰器
from fastapi import Request
from pydantic import BaseModel, validator, field_validator
from pydantic import BaseModel, field_validator
from sqlalchemy.ext.asyncio import AsyncSession
from application.settings import DEFAULT_AUTH_ERROR_MAX_NUMBER, DEMO
from application.settings import DEFAULT_AUTH_ERROR_MAX_NUMBER, DEMO, REDIS_DB_ENABLE
from apps.vadmin.auth import crud, schemas
from core.database import redis_getter
from core.validator import vali_telephone
from typing import Optional
from utils.count import Count
@ -64,12 +63,15 @@ class LoginValidation:
result = await self.func(self, data=data, user=user, request=request)
if REDIS_DB_ENABLE:
count_key = f"{data.telephone}_password_auth" if data.method == '0' else f"{data.telephone}_sms_auth"
count = Count(redis_getter(request), count_key)
else:
count = None
if not result.status:
self.result.msg = result.msg
if not DEMO:
if not DEMO and count:
number = await count.add(ex=86400)
if number >= DEFAULT_AUTH_ERROR_MAX_NUMBER:
await count.reset()
@ -81,10 +83,10 @@ class LoginValidation:
elif data.platform in ["0", "1"] and not user.is_staff:
self.result.msg = "此手机号无权限!"
else:
if not DEMO:
if not DEMO and count:
await count.delete()
self.result.msg = "OK"
self.result.status = True
self.result.user = schemas.UserSimpleOut.model_validate(user)
await user.update_login_info(db, request.client.host)
await crud.UserDal(db).update_login_info(user, request.client.host)
return self.result

View File

@ -5,6 +5,7 @@
# @File : views.py
# @IDE : PyCharm
# @desc : 简要说明
from aioredis import Redis
from fastapi import APIRouter, Depends, Body, UploadFile, Request
from sqlalchemy.orm import joinedload
@ -12,13 +13,22 @@ from core.database import redis_getter
from utils.response import SuccessResponse, ErrorResponse
from . import schemas, crud, models
from core.dependencies import IdList
from apps.vadmin.auth.utils.current import AllUserAuth, FullAdminAuth
from apps.vadmin.auth.utils.current import AllUserAuth, FullAdminAuth, OpenAuth
from apps.vadmin.auth.utils.validation.auth import Auth
from .params import UserParams, RoleParams
app = APIRouter()
###########################################################
# 接口测试
###########################################################
@app.get("/test", summary="接口测试")
async def test(auth: Auth = Depends(OpenAuth())):
await crud.TestDal(auth.db).test()
return SuccessResponse()
###########################################################
# 用户管理
###########################################################
@ -30,8 +40,12 @@ async def get_users(
model = models.VadminUser
options = [joinedload(model.roles)]
schema = schemas.UserOut
datas = await crud.UserDal(auth.db).get_datas(**params.dict(), v_options=options, v_schema=schema)
count = await crud.UserDal(auth.db).get_count(**params.to_count())
datas, count = await crud.UserDal(auth.db).get_datas(
**params.dict(),
v_options=options,
v_schema=schema,
v_return_count=True
)
return SuccessResponse(datas, count=count)
@ -67,7 +81,7 @@ async def get_user(
model = models.VadminUser
options = [joinedload(model.roles)]
schema = schemas.UserOut
return SuccessResponse(await crud.UserDal(auth.db).get_data(data_id, options, v_schema=schema))
return SuccessResponse(await crud.UserDal(auth.db).get_data(data_id, v_options=options, v_schema=schema))
@app.post("/user/current/reset/password", summary="重置当前用户密码")
@ -145,8 +159,7 @@ async def get_roles(
params: RoleParams = Depends(),
auth: Auth = Depends(FullAdminAuth(permissions=["auth.role.list"]))
):
datas = await crud.RoleDal(auth.db).get_datas(**params.dict())
count = await crud.RoleDal(auth.db).get_count(**params.to_count())
datas, count = await crud.RoleDal(auth.db).get_datas(**params.dict(), v_return_count=True)
return SuccessResponse(datas, count=count)
@ -187,7 +200,7 @@ async def get_role(
model = models.VadminRole
options = [joinedload(model.menus)]
schema = schemas.RoleOut
return SuccessResponse(await crud.RoleDal(auth.db).get_data(data_id, options, v_schema=schema))
return SuccessResponse(await crud.RoleDal(auth.db).get_data(data_id, v_options=options, v_schema=schema))
###########################################################
@ -239,7 +252,7 @@ async def put_menus(
auth: Auth = Depends(FullAdminAuth(permissions=["auth.menu.view", "auth.menu.update"]))
):
schema = schemas.MenuSimpleOut
return SuccessResponse(await crud.MenuDal(auth.db).get_data(data_id, None, v_schema=schema))
return SuccessResponse(await crud.MenuDal(auth.db).get_data(data_id, v_schema=schema))
@app.get("/role/menus/tree/{role_id}", summary="获取菜单列表树信息以及角色菜单权限ID角色权限使用")

View File

@ -7,11 +7,8 @@
# @desc : 帮助中心 - 增删改查
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from core.crud import DalBase
from . import models, schemas
from apps.vadmin.auth import models as vadminAuthModels
class IssueDal(DalBase):
@ -19,47 +16,16 @@ class IssueDal(DalBase):
def __init__(self, db: AsyncSession):
super(IssueDal, self).__init__(db, models.VadminIssue, schemas.IssueSimpleOut)
async def add_view_number(self, data_id: int):
async def add_view_number(self, data_id: int) -> None:
"""
更新常见问题查看次数+1
"""
obj = await self.get_data(data_id)
obj: models.VadminIssue = await self.get_data(data_id)
obj.view_number = obj.view_number + 1 if obj.view_number else 1
await self.flush(obj)
return True
class IssueCategoryDal(DalBase):
def __init__(self, db: AsyncSession):
key_models = {
# 外键字段名,也可以自定义
"create_user": {
# 外键对应的orm模型
"model": vadminAuthModels.VadminUser,
# 如果对同一个模型只有一个外键关联时,下面这个 onclause 可以省略不写,一个以上时必须写,需要分清楚要查询的是哪个
# 这里其实可以省略不写,但是为了演示这里写出来了
"onclause": models.VadminIssueCategory.create_user_id == vadminAuthModels.VadminUser.id
}
}
super(IssueCategoryDal, self).__init__(
db,
models.VadminIssueCategory,
schemas.IssueCategorySimpleOut,
key_models
)
async def test(self):
"""
v_join_query 示例方法
获取用户名称包含李 创建出的常见问题类别
"""
v_join_query = {
# 与 key_models 中定义的外键字段名定义的一样
"create_user": {
# 外键表字段名:查询值
"name": ("like", "")
}
}
v_options = [joinedload(self.model.create_user)]
datas = self.get_datas(limit=0, v_join_query=v_join_query, v_options=v_options)
super(IssueCategoryDal, self).__init__(db, models.VadminIssueCategory, schemas.IssueCategorySimpleOut)

View File

@ -6,37 +6,49 @@
# @IDE : PyCharm
# @desc : 常见问题
from sqlalchemy.orm import relationship
from sqlalchemy.orm import relationship, Mapped, mapped_column
from apps.vadmin.auth.models import VadminUser
from db.db_base import BaseModel
from sqlalchemy import Column, String, Boolean, Integer, ForeignKey, Text
from sqlalchemy import String, Boolean, Integer, ForeignKey
class VadminIssueCategory(BaseModel):
__tablename__ = "vadmin_help_issue_category"
__table_args__ = ({'comment': '常见问题类别表'})
name = Column(String(50), index=True, nullable=False, comment="类别名称")
platform = Column(String(8), index=True, nullable=False, comment="展示平台")
is_active = Column(Boolean, default=True, comment="是否可见")
name: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="类别名称")
platform: Mapped[str] = mapped_column(String(8), index=True, nullable=False, comment="展示平台")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否可见")
issues = relationship("VadminIssue", back_populates='category')
issues: Mapped[list["VadminIssue"]] = relationship(back_populates='category')
create_user_id = Column(ForeignKey("vadmin_auth_user.id", ondelete='SET NULL'), comment="创建人")
create_user = relationship("VadminUser", foreign_keys=create_user_id)
create_user_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("vadmin_auth_user.id", ondelete='RESTRICT'),
comment="创建人"
)
create_user: Mapped[VadminUser] = relationship(foreign_keys=create_user_id)
class VadminIssue(BaseModel):
__tablename__ = "vadmin_help_issue"
__table_args__ = ({'comment': '常见问题记录表'})
category_id = Column(ForeignKey("vadmin_help_issue_category.id", ondelete='CASCADE'), comment="类别")
category = relationship("VadminIssueCategory", foreign_keys=category_id, back_populates='issues')
category_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("vadmin_help_issue_category.id", ondelete='CASCADE'),
comment="类别"
)
category: Mapped[list["VadminIssueCategory"]] = relationship(foreign_keys=category_id, back_populates='issues')
title = Column(String(255), index=True, nullable=False, comment="标题")
content = Column(Text, comment="内容")
view_number = Column(Integer, default=0, comment="查看次数")
is_active = Column(Boolean, default=True, comment="是否可见")
create_user_id = Column(ForeignKey("vadmin_auth_user.id", ondelete='SET NULL'), comment="创建人")
create_user = relationship("VadminUser", foreign_keys=create_user_id)
title: Mapped[str] = mapped_column(String(255), index=True, nullable=False, comment="标题")
content: Mapped[str] = mapped_column(String(5000), comment="内容")
view_number: Mapped[int] = mapped_column(Integer, default=0, comment="查看次数")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否可见")
create_user_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("vadmin_auth_user.id", ondelete='RESTRICT'),
comment="创建人"
)
create_user: Mapped[VadminUser] = relationship(foreign_keys=create_user_id)

View File

@ -27,8 +27,12 @@ async def get_issue_categorys(p: params.IssueCategoryParams = Depends(), auth: A
model = models.VadminIssueCategory
options = [joinedload(model.create_user)]
schema = schemas.IssueCategoryListOut
datas = await crud.IssueCategoryDal(auth.db).get_datas(**p.dict(), v_options=options, v_schema=schema)
count = await crud.IssueCategoryDal(auth.db).get_count(**p.to_count())
datas, count = await crud.IssueCategoryDal(auth.db).get_datas(
**p.dict(),
v_options=options,
v_schema=schema,
v_return_count=True
)
return SuccessResponse(datas, count=count)
@ -67,6 +71,7 @@ async def get_issue_category_platform(platform: str, db: AsyncSession = Depends(
options = [joinedload(model.issues)]
schema = schemas.IssueCategoryPlatformOut
result = await crud.IssueCategoryDal(db).get_datas(
limit=0,
platform=platform,
is_active=True,
v_schema=schema,
@ -83,8 +88,12 @@ async def get_issues(p: params.IssueParams = Depends(), auth: Auth = Depends(All
model = models.VadminIssue
options = [joinedload(model.create_user), joinedload(model.category)]
schema = schemas.IssueListOut
datas = await crud.IssueDal(auth.db).get_datas(**p.dict(), v_options=options, v_schema=schema)
count = await crud.IssueDal(auth.db).get_count(**p.to_count())
datas, count = await crud.IssueDal(auth.db).get_datas(
**p.dict(),
v_options=options,
v_schema=schema,
v_return_count=True
)
return SuccessResponse(datas, count=count)

View File

@ -5,10 +5,8 @@
# @File : crud.py
# @IDE : PyCharm
# @desc : 数据库 增删改查操作
import random
# sqlalchemy 查询操作https://segmentfault.com/a/1190000016767008
# sqlalchemy 关联查询https://www.jianshu.com/p/dfad7c08c57a
# sqlalchemy 关联查询详细https://blog.csdn.net/u012324798/article/details/103940527
from motor.motor_asyncio import AsyncIOMotorDatabase
from sqlalchemy.ext.asyncio import AsyncSession
from . import models, schemas

View File

@ -6,12 +6,15 @@
# @IDE : PyCharm
# @desc : 登录记录模型
import json
from sqlalchemy.orm import Mapped, mapped_column
from application.settings import LOGIN_LOG_RECORD
from apps.vadmin.auth.utils.validation import LoginForm, WXLoginForm
from utils.ip_manage import IPManage
from sqlalchemy.ext.asyncio import AsyncSession
from db.db_base import BaseModel
from sqlalchemy import Column, String, Boolean, TEXT
from sqlalchemy import String, Boolean
from fastapi import Request
from starlette.requests import Request as StarletteRequest
from user_agents import parse
@ -21,23 +24,23 @@ class VadminLoginRecord(BaseModel):
__tablename__ = "vadmin_record_login"
__table_args__ = ({'comment': '登录记录表'})
telephone = Column(String(255), index=True, nullable=False, comment="手机号")
status = Column(Boolean, default=True, comment="是否登录成功")
platform = Column(String(8), comment="登陆平台")
login_method = Column(String(8), comment="认证方式")
ip = Column(String(50), comment="登陆地址")
address = Column(String(255), comment="登陆地点")
country = Column(String(255), comment="国家")
province = Column(String(255), comment="")
city = Column(String(255), comment="城市")
county = Column(String(255), comment="区/县")
operator = Column(String(255), comment="运营商")
postal_code = Column(String(255), comment="邮政编码")
area_code = Column(String(255), comment="地区区号")
browser = Column(String(50), comment="浏览器")
system = Column(String(50), comment="操作系统")
response = Column(TEXT, comment="响应信息")
request = Column(TEXT, comment="请求信息")
telephone: Mapped[str] = mapped_column(String(255), index=True, nullable=False, comment="手机号")
status: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否登录成功")
platform: Mapped[str] = mapped_column(String(8), comment="登陆平台")
login_method: Mapped[str] = mapped_column(String(8), comment="认证方式")
ip: Mapped[str | None] = mapped_column(String(50), comment="登陆地址")
address: Mapped[str | None] = mapped_column(String(255), comment="登陆地点")
country: Mapped[str | None] = mapped_column(String(255), comment="国家")
province: Mapped[str | None] = mapped_column(String(255), comment="")
city: Mapped[str | None] = mapped_column(String(255), comment="城市")
county: Mapped[str | None] = mapped_column(String(255), comment="区/县")
operator: Mapped[str | None] = mapped_column(String(255), comment="运营商")
postal_code: Mapped[str | None] = mapped_column(String(255), comment="邮政编码")
area_code: Mapped[str | None] = mapped_column(String(255), comment="地区区号")
browser: Mapped[str | None] = mapped_column(String(50), comment="浏览器")
system: Mapped[str | None] = mapped_column(String(50), comment="操作系统")
response: Mapped[str | None] = mapped_column(String(5000), comment="响应信息")
request: Mapped[str | None] = mapped_column(String(5000), comment="请求信息")
@classmethod
async def create_login_record(

View File

@ -6,18 +6,18 @@
# @IDE : PyCharm
# @desc : 短信发送记录模型
from sqlalchemy.orm import Mapped, mapped_column
from db.db_base import BaseModel
from sqlalchemy import Column, String, Boolean, ForeignKey
from sqlalchemy import Integer, String, Boolean, ForeignKey
class VadminSMSSendRecord(BaseModel):
__tablename__ = "vadmin_record_sms_send"
__table_args__ = ({'comment': '短信发送记录表'})
user_id = Column(ForeignKey("vadmin_auth_user.id", ondelete='CASCADE'), comment="操作人")
status = Column(Boolean, default=True, comment="发送状态")
content = Column(String(255), comment="发送内容")
telephone = Column(String(11), comment="目标手机号")
desc = Column(String(255), comment="失败描述")
scene = Column(String(50), comment="发送场景")
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("vadmin_auth_user.id", ondelete='CASCADE'), comment="操作人")
status: Mapped[bool] = mapped_column(Boolean, default=True, comment="发送状态")
content: Mapped[str] = mapped_column(String(255), comment="发送内容")
telephone: Mapped[str] = mapped_column(String(11), comment="目标手机号")
desc: Mapped[str | None] = mapped_column(String(255), comment="失败描述")
scene: Mapped[str | None] = mapped_column(String(50), comment="发送场景")

View File

@ -7,9 +7,8 @@
from fastapi import APIRouter, Depends
from motor.motor_asyncio import AsyncIOMotorDatabase
from utils.response import SuccessResponse
from . import crud, schemas
from . import crud
from apps.vadmin.auth.utils.current import AllUserAuth
from apps.vadmin.auth.utils.validation.auth import Auth
from .params import LoginParams, OperationParams, SMSParams
@ -23,8 +22,7 @@ app = APIRouter()
###########################################################
@app.get("/logins", summary="获取登录日志列表")
async def get_record_login(p: LoginParams = Depends(), auth: Auth = Depends(AllUserAuth())):
datas = await crud.LoginRecordDal(auth.db).get_datas(**p.dict())
count = await crud.LoginRecordDal(auth.db).get_count(**p.to_count())
datas, count = await crud.LoginRecordDal(auth.db).get_datas(**p.dict(), v_return_count=True)
return SuccessResponse(datas, count=count)
@ -41,8 +39,7 @@ async def get_record_operation(
@app.get("/sms/send/list", summary="获取短信发送列表")
async def get_sms_send_list(p: SMSParams = Depends(), auth: Auth = Depends(AllUserAuth())):
datas = await crud.SMSSendRecordDal(auth.db).get_datas(**p.dict())
count = await crud.SMSSendRecordDal(auth.db).get_count(**p.to_count())
datas, count = await crud.SMSSendRecordDal(auth.db).get_datas(**p.dict(), v_return_count=True)
return SuccessResponse(datas, count=count)

View File

@ -6,9 +6,6 @@
# @IDE : PyCharm
# @desc : 数据库 增删改查操作
# sqlalchemy 查询操作https://segmentfault.com/a/1190000016767008
# sqlalchemy 关联查询https://www.jianshu.com/p/dfad7c08c57a
# sqlalchemy 关联查询详细https://blog.csdn.net/u012324798/article/details/103940527
import json
import os
from enum import Enum
@ -19,13 +16,15 @@ from motor.motor_asyncio import AsyncIOMotorDatabase
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from application.settings import STATIC_ROOT, SUBSCRIBE
from application.settings import STATIC_ROOT, SUBSCRIBE, REDIS_DB_ENABLE
from core.database import redis_getter
from core.mongo_manage import MongoManage
from utils.file.file_manage import FileManage
from . import models, schemas
from core.crud import DalBase
from core.exception import CustomException
from utils import status
from fastapi import Request
class DictTypeDal(DalBase):
@ -53,7 +52,7 @@ class DictTypeDal(DalBase):
data[obj.dict_type] = [schemas.DictDetailsSimpleOut.model_validate(i).model_dump() for i in obj.details]
return data
async def get_select_datas(self):
async def get_select_datas(self) -> list:
"""获取选择数据,全部数据"""
sql = select(self.model)
queryset = await self.db.execute(sql)
@ -82,7 +81,7 @@ class SettingsDal(DalBase):
result[data.config_key] = data.config_value
return result
async def update_datas(self, datas: dict, rd: Redis):
async def update_datas(self, datas: dict, request: Request) -> None:
"""
更新系统配置信息
@ -104,12 +103,13 @@ class SettingsDal(DalBase):
sql = update(self.model).where(self.model.config_key == "web_ico").values(config_value=web_ico)
await self.db.execute(sql)
else:
sql = update(self.model).where(self.model.config_key == key).values(config_value=value)
sql = update(self.model).where(self.model.config_key == str(key)).values(config_value=value)
await self.db.execute(sql)
if "wx_server_app_id" in datas:
if "wx_server_app_id" in datas and REDIS_DB_ENABLE:
rd = redis_getter(request)
await rd.client().set("wx_server", json.dumps(datas))
async def get_base_config(self):
async def get_base_config(self) -> dict:
"""
获取系统基本信息
"""
@ -127,7 +127,7 @@ class SettingsTabDal(DalBase):
def __init__(self, db: AsyncSession):
super(SettingsTabDal, self).__init__(db, models.VadminSystemSettingsTab, schemas.SettingsTabSimpleOut)
async def get_classify_tab_values(self, classify: list[str], hidden: bool | None = False):
async def get_classify_tab_values(self, classify: list[str], hidden: bool | None = False) -> dict:
"""
获取系统配置分类下的标签信息
"""
@ -143,7 +143,7 @@ class SettingsTabDal(DalBase):
)
return self.__generate_values(datas)
async def get_tab_name_values(self, tab_names: list[str], hidden: bool | None = False):
async def get_tab_name_values(self, tab_names: list[str], hidden: bool | None = False) -> dict:
"""
获取系统配置标签下的标签信息
"""
@ -160,7 +160,7 @@ class SettingsTabDal(DalBase):
return self.__generate_values(datas)
@classmethod
def __generate_values(cls, datas: list[models.VadminSystemSettingsTab]):
def __generate_values(cls, datas: list[models.VadminSystemSettingsTab]) -> dict:
"""
生成字典值
"""
@ -281,7 +281,7 @@ class TaskDal(MongoManage):
v_order: str = None,
v_order_field: str = None,
**kwargs
):
) -> tuple:
"""
获取任务信息列表

View File

@ -1,3 +1,2 @@
from .dict import VadminDictType, VadminDictDetails
from .settings import VadminSystemSettings
from .settings_tab import VadminSystemSettingsTab
from .settings import VadminSystemSettings, VadminSystemSettingsTab

View File

@ -6,7 +6,7 @@
# @IDE : PyCharm
# @desc : 系统字典模型
from sqlalchemy.orm import relationship
from sqlalchemy.orm import relationship, Mapped, mapped_column
from db.db_base import BaseModel
from sqlalchemy import Column, String, Boolean, ForeignKey, Integer
@ -15,22 +15,26 @@ class VadminDictType(BaseModel):
__tablename__ = "vadmin_system_dict_type"
__table_args__ = ({'comment': '字典类型表'})
dict_name = Column(String(50), index=True, nullable=False, comment="字典名称")
dict_type = Column(String(50), index=True, nullable=False, comment="字典类型")
disabled = Column(Boolean, default=False, comment="字典状态,是否禁用")
remark = Column(String(255), comment="备注")
details = relationship("VadminDictDetails", back_populates="dict_type")
dict_name: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="字典名称")
dict_type: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="字典类型")
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="字典状态,是否禁用")
remark: Mapped[str | None] = mapped_column(String(255), comment="备注")
details: Mapped[list["VadminDictDetails"]] = relationship(back_populates="dict_type")
class VadminDictDetails(BaseModel):
__tablename__ = "vadmin_system_dict_details"
__table_args__ = ({'comment': '字典详情表'})
label = Column(String(50), index=True, nullable=False, comment="字典标签")
value = Column(String(50), index=True, nullable=False, comment="字典键值")
disabled = Column(Boolean, default=False, comment="字典状态,是否禁用")
is_default = Column(Boolean, default=False, comment="是否默认")
order = Column(Integer, comment="字典排序")
dict_type_id = Column(Integer, ForeignKey("vadmin_system_dict_type.id", ondelete='CASCADE'), comment="关联字典类型")
dict_type = relationship("VadminDictType", foreign_keys=dict_type_id, back_populates="details")
remark = Column(String(255), comment="备注")
label: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="字典标签")
value: Mapped[str] = mapped_column(String(50), index=True, nullable=False, comment="字典键值")
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="字典状态,是否禁用")
is_default: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否默认")
order: Mapped[int] = mapped_column(Integer, comment="字典排序")
dict_type_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("vadmin_system_dict_type.id", ondelete='CASCADE'),
comment="关联字典类型"
)
dict_type: Mapped[VadminDictType] = relationship(foreign_keys=dict_type_id, back_populates="details")
remark: Mapped[str | None] = mapped_column(String(255), comment="备注")

View File

@ -5,20 +5,38 @@
# @File : settings.py
# @IDE : PyCharm
# @desc : 系统字典模型
from sqlalchemy.orm import relationship
from sqlalchemy.orm import relationship, Mapped, mapped_column
from db.db_base import BaseModel
from sqlalchemy import Column, String, TEXT, Integer, ForeignKey, Boolean
from sqlalchemy import String, Integer, ForeignKey, Boolean, Text
class VadminSystemSettingsTab(BaseModel):
__tablename__ = "vadmin_system_settings_tab"
__table_args__ = ({'comment': '系统配置分类表'})
title: Mapped[str] = mapped_column(String(255), comment="标题")
classify: Mapped[str] = mapped_column(String(255), index=True, nullable=False, comment="分类键")
tab_label: Mapped[str] = mapped_column(String(255), comment="tab标题")
tab_name: Mapped[str] = mapped_column(String(255), index=True, nullable=False, unique=True, comment="tab标识符")
hidden: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否隐藏")
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否禁用")
settings: Mapped[list["VadminSystemSettings"]] = relationship(back_populates="tab")
class VadminSystemSettings(BaseModel):
__tablename__ = "vadmin_system_settings"
__table_args__ = ({'comment': '系统配置表'})
config_label = Column(String(255), comment="配置表标签")
config_key = Column(String(255), index=True, nullable=False, unique=True, comment="配置表键")
config_value = Column(TEXT, comment="配置表内容")
remark = Column(String(255), comment="备注信息")
disabled = Column(Boolean, default=False, comment="是否禁用")
config_label: Mapped[str] = mapped_column(String(255), comment="配置表标签")
config_key: Mapped[str] = mapped_column(String(255), index=True, nullable=False, unique=True, comment="配置表键")
config_value: Mapped[str | None] = mapped_column(Text, comment="配置表内容")
remark: Mapped[str | None] = mapped_column(String(255), comment="备注信息")
disabled: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否禁用")
tab_id = Column(Integer, ForeignKey("vadmin_system_settings_tab.id", ondelete='CASCADE'), comment="关联tab标签")
tab = relationship("VadminSystemSettingsTab", foreign_keys=tab_id, back_populates="settings")
tab_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("vadmin_system_settings_tab.id", ondelete='CASCADE'),
comment="关联tab标签"
)
tab: Mapped[VadminSystemSettingsTab] = relationship(foreign_keys=tab_id, back_populates="settings")

View File

@ -1,25 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/7/7 13:41
# @File : settings_tab.py
# @IDE : PyCharm
# @desc : 系统配置分类模型
from sqlalchemy.orm import relationship
from db.db_base import BaseModel
from sqlalchemy import Column, String, Boolean
class VadminSystemSettingsTab(BaseModel):
__tablename__ = "vadmin_system_settings_tab"
__table_args__ = ({'comment': '系统配置分类表'})
title = Column(String(255), comment="标题")
classify = Column(String(255), index=True, nullable=False, comment="分类键")
tab_label = Column(String(255), comment="tab标题")
tab_name = Column(String(255), index=True, nullable=False, unique=True, comment="tab标识符")
hidden = Column(Boolean, default=False, comment="是否隐藏")
disabled = Column(Boolean, default=False, comment="是否禁用")
settings = relationship("VadminSystemSettings", back_populates="tab")

View File

@ -5,9 +5,8 @@
# @IDE : PyCharm
# @desc : 主要接口文件
# UploadFile 库依赖pip install python-multipart
from aioredis import Redis
from fastapi import APIRouter, Depends, Body, UploadFile, Form
from fastapi import APIRouter, Depends, Body, UploadFile, Form, Request
from motor.motor_asyncio import AsyncIOMotorDatabase
from sqlalchemy.ext.asyncio import AsyncSession
from application.settings import ALIYUN_OSS
@ -21,7 +20,7 @@ from core.dependencies import IdList
from apps.vadmin.auth.utils.current import AllUserAuth, FullAdminAuth, OpenAuth
from apps.vadmin.auth.utils.validation.auth import Auth
from .params import DictTypeParams, DictDetailParams, TaskParams
from apps.vadmin.auth import crud as vadminAuthCRUD
from apps.vadmin.auth import crud as vadmin_auth_crud
from .params.task import TaskRecordParams
app = APIRouter()
@ -32,8 +31,7 @@ app = APIRouter()
###########################################################
@app.get("/dict/types", summary="获取字典类型列表")
async def get_dict_types(p: DictTypeParams = Depends(), auth: Auth = Depends(AllUserAuth())):
datas = await crud.DictTypeDal(auth.db).get_datas(**p.dict())
count = await crud.DictTypeDal(auth.db).get_count(**p.to_count())
datas, count = await crud.DictTypeDal(auth.db).get_datas(**p.dict(), v_return_count=True)
return SuccessResponse(datas, count=count)
@ -70,7 +68,7 @@ async def put_dict_types(data_id: int, data: schemas.DictType, auth: Auth = Depe
@app.get("/dict/types/{data_id}", summary="获取字典类型详细")
async def get_dict_type(data_id: int, auth: Auth = Depends(AllUserAuth())):
schema = schemas.DictTypeSimpleOut
return SuccessResponse(await crud.DictTypeDal(auth.db).get_data(data_id, None, v_schema=schema))
return SuccessResponse(await crud.DictTypeDal(auth.db).get_data(data_id, v_schema=schema))
###########################################################
@ -85,8 +83,7 @@ async def create_dict_details(data: schemas.DictDetails, auth: Auth = Depends(Al
async def get_dict_details(params: DictDetailParams = Depends(), auth: Auth = Depends(AllUserAuth())):
if not params.dict_type_id:
return ErrorResponse(msg="未获取到字典类型!")
datas = await crud.DictDetailsDal(auth.db).get_datas(**params.dict())
count = await crud.DictDetailsDal(auth.db).get_count(**params.to_count())
datas, count = await crud.DictDetailsDal(auth.db).get_datas(**params.dict(), v_return_count=True)
return SuccessResponse(datas, count=count)
@ -104,7 +101,7 @@ async def put_dict_details(data_id: int, data: schemas.DictDetails, auth: Auth =
@app.get("/dict/details/{data_id}", summary="获取字典元素详情")
async def get_dict_detail(data_id: int, auth: Auth = Depends(AllUserAuth())):
schema = schemas.DictDetailsSimpleOut
return SuccessResponse(await crud.DictDetailsDal(auth.db).get_data(data_id, None, v_schema=schema))
return SuccessResponse(await crud.DictDetailsDal(auth.db).get_data(data_id, v_schema=schema))
###########################################################
@ -140,7 +137,7 @@ async def upload_image_to_local(file: UploadFile, path: str = Form(...)):
###########################################################
@app.post("/sms/send", summary="发送短信验证码(阿里云服务)")
async def sms_send(telephone: str, rd: Redis = Depends(redis_getter), auth: Auth = Depends(OpenAuth())):
user = await vadminAuthCRUD.UserDal(auth.db).get_data(telephone=telephone, v_return_none=True)
user = await vadmin_auth_crud.UserDal(auth.db).get_data(telephone=telephone, v_return_none=True)
if not user:
return ErrorResponse("手机号不存在!")
sms = CodeSMS(telephone, rd)
@ -162,11 +159,11 @@ async def get_settings_tabs_values(tab_id: int, auth: Auth = Depends(FullAdminAu
@app.put("/settings/tabs/values", summary="更新系统配置信息")
async def put_settings_tabs_values(
request: Request,
datas: dict = Body(...),
auth: Auth = Depends(FullAdminAuth()),
rd: Redis = Depends(redis_getter)
auth: Auth = Depends(FullAdminAuth())
):
return SuccessResponse(await crud.SettingsDal(auth.db).update_datas(datas, rd))
return SuccessResponse(await crud.SettingsDal(auth.db).update_datas(datas, request))
@app.get("/settings/base/config", summary="获取系统基础配置", description="每次进入系统中时使用")

View File

@ -1,33 +1,26 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:18
# @Update Time : 2023/8/21 22:18
# @File : crud.py
# @IDE : PyCharm
# @desc : 数据库 增删改查操作
# sqlalchemy 查询操作https://segmentfault.com/a/1190000016767008
# sqlalchemy 查询操作(官方文档): https://www.osgeo.cn/sqlalchemy/orm/queryguide.html
# sqlalchemy 增删改操作https://www.osgeo.cn/sqlalchemy/tutorial/orm_data_manipulation.html#updating-orm-objects
# SQLAlchemy lazy load和eager load: https://www.jianshu.com/p/dfad7c08c57a
# Mysql中内连接,左连接和右连接的区别总结:https://www.cnblogs.com/restartyang/articles/9080993.html
# SQLAlchemy INNER JOIN 内连接
# selectinload 官方文档:
# https://www.osgeo.cn/sqlalchemy/orm/loading_relationships.html?highlight=selectinload#sqlalchemy.orm.selectinload
# SQLAlchemy LEFT OUTER JOIN 左连接
# joinedload 官方文档:
# https://www.osgeo.cn/sqlalchemy/orm/loading_relationships.html?highlight=selectinload#sqlalchemy.orm.joinedload
# sqlalchemy 官方文档https://docs.sqlalchemy.org/en/20/index.html
# sqlalchemy 查询操作(官方文档): https://docs.sqlalchemy.org/en/20/orm/queryguide/select.html
# sqlalchemy 增删改操作https://docs.sqlalchemy.org/en/20/orm/queryguide/dml.html
# sqlalchemy 1.x 语法迁移到 2.x :https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#migration-20-query-usage
import datetime
from typing import Set
from fastapi import HTTPException
from fastapi.encoders import jsonable_encoder
from sqlalchemy import func, delete, update, or_
from sqlalchemy.future import select
from sqlalchemy import func, delete, update, BinaryExpression, ScalarResult, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import InstrumentedAttribute
from sqlalchemy.orm.strategy_options import _AbstractLoad
from starlette import status
from core.exception import CustomException
from sqlalchemy.sql.selectable import Select
from sqlalchemy.sql.selectable import Select as SelectType
from typing import Any
@ -35,124 +28,200 @@ class DalBase:
# 倒叙
ORDER_FIELD = ["desc", "descending"]
def __init__(self, db: AsyncSession, model: Any, schema: Any, key_models: dict = None):
def __init__(self, db: AsyncSession, model: Any, schema: Any):
self.db = db
self.model = model
self.schema = schema
self.key_models = key_models
async def get_data(
self,
data_id: int = None,
v_options: list = None,
v_join_query: dict = None,
v_or: list[tuple] = None,
v_start_sql: SelectType = None,
v_select_from: list[Any] = None,
v_join: list[list[str | InstrumentedAttribute, BinaryExpression | None]] = None,
v_outerjoin: list[list[str | InstrumentedAttribute, BinaryExpression | None]] = None,
v_options: list[_AbstractLoad] = None,
v_where: list[BinaryExpression] = None,
v_order: str = None,
v_order_field: str = None,
v_return_none: bool = False,
v_schema: Any = None,
**kwargs
):
) -> Any:
"""
获取单个数据默认使用 ID 查询否则使用关键词查询
:param data_id: 数据 ID
:param v_options: 指示应使用select在预加载中加载给定的属性
:param v_join_query: 外键字段查询内连接
:param v_or: 或逻辑查询
:param v_start_sql: 初始 sql
:param v_select_from: 用于指定查询从哪个表开始通常与 .join() 等方法一起使用
:param v_join: 创建内连接INNER JOIN操作返回两个表中满足连接条件的交集
:param v_outerjoin: 用于创建外连接OUTER JOIN操作返回两个表中满足连接条件的并集包括未匹配的行并用 NULL 值填充
:param v_options: 用于为查询添加附加选项如预加载延迟加载等
:param v_where: 当前表查询条件原始表达式
:param v_order: 排序默认正序 desc 是倒叙
:param v_order_field: 排序字段
:param v_return_none: 是否返回空 None否认 抛出异常默认抛出异常
:param v_schema: 指定使用的序列化对象
:param kwargs: 查询参数
:return: 默认返回 ORM 对象如果存在 v_schema 则会返回 v_schema 结果
"""
sql = select(self.model).where(self.model.is_delete == False)
if not isinstance(v_start_sql, SelectType):
v_start_sql = select(self.model).where(self.model.is_delete == False)
if data_id:
sql = sql.where(self.model.id == data_id)
sql = self.add_filter_condition(sql, v_options, v_join_query, v_or, **kwargs)
if v_order_field and (v_order in self.ORDER_FIELD):
sql = sql.order_by(getattr(self.model, v_order_field).desc(), self.model.id.desc())
elif v_order_field:
sql = sql.order_by(getattr(self.model, v_order_field), self.model.id)
elif v_order and (v_order in self.ORDER_FIELD):
sql = sql.order_by(self.model.create_datetime.desc())
queryset = await self.db.execute(sql)
data = queryset.scalars().unique().first()
v_start_sql = v_start_sql.where(self.model.id == data_id)
queryset: ScalarResult = await self.filter_core(
v_start_sql=v_start_sql,
v_select_from=v_select_from,
v_join=v_join,
v_outerjoin=v_outerjoin,
v_options=v_options,
v_where=v_where,
v_order=v_order,
v_order_field=v_order_field,
v_return_sql=False,
**kwargs
)
if v_options:
data = queryset.unique().first()
else:
data = queryset.first()
if not data and v_return_none:
return None
if data and v_schema:
return v_schema.model_validate(data).model_dump()
if data:
return data
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="未找到此数据")
async def get_datas(
self,
page: int = 1,
limit: int = 10,
v_options: list = None,
v_join_query: dict = None,
v_or: list[tuple] = None,
v_start_sql: SelectType = None,
v_select_from: list[Any] = None,
v_join: list[list[str | InstrumentedAttribute, BinaryExpression | None]] = None,
v_outerjoin: list[list[str | InstrumentedAttribute, BinaryExpression | None]] = None,
v_options: list[_AbstractLoad] = None,
v_where: list[BinaryExpression] = None,
v_order: str = None,
v_order_field: str = None,
v_return_count: bool = False,
v_return_scalars: bool = False,
v_return_objs: bool = False,
v_start_sql: Any = None,
v_schema: Any = None,
**kwargs
) -> list:
) -> list[Any] | ScalarResult | tuple:
"""
获取数据列表
:param page: 页码
:param limit: 当前页数据量
:param v_options: 指示应使用select在预加载中加载给定的属性
:param v_join_query: 外键字段查询
:param v_or: 或逻辑查询
:param v_start_sql: 初始 sql
:param v_select_from: 用于指定查询从哪个表开始通常与 .join() 等方法一起使用
:param v_join: 创建内连接INNER JOIN操作返回两个表中满足连接条件的交集
:param v_outerjoin: 用于创建外连接OUTER JOIN操作返回两个表中满足连接条件的并集包括未匹配的行并用 NULL 值填充
:param v_options: 用于为查询添加附加选项如预加载延迟加载等
:param v_where: 当前表查询条件原始表达式
:param v_order: 排序默认正序 desc 是倒叙
:param v_order_field: 排序字段
:param v_return_count: 默认为 False是否返回 count 过滤后的数据总数不会影响其他返回结果会一起返回为一个数组
:param v_return_scalars: 返回scalars后的结果
:param v_return_objs: 是否返回对象
:param v_start_sql: 初始 sql
:param v_schema: 指定使用的序列化对象
:param kwargs: 查询参数
:param kwargs: 查询参数使用的是自定义表达式
:return: 返回值优先级v_return_scalars > v_return_objs > v_schema
"""
if not isinstance(v_start_sql, Select):
v_start_sql = select(self.model).where(self.model.is_delete == False)
sql = self.add_filter_condition(v_start_sql, v_options, v_join_query, v_or, **kwargs)
if v_order_field and (v_order in self.ORDER_FIELD):
sql = sql.order_by(getattr(self.model, v_order_field).desc(), self.model.id.desc())
elif v_order_field:
sql = sql.order_by(getattr(self.model, v_order_field), self.model.id)
elif v_order in self.ORDER_FIELD:
sql = sql.order_by(self.model.id.desc())
sql: SelectType = await self.filter_core(
v_start_sql=v_start_sql,
v_select_from=v_select_from,
v_join=v_join,
v_outerjoin=v_outerjoin,
v_options=v_options,
v_where=v_where,
v_order=v_order,
v_order_field=v_order_field,
v_return_sql=True,
**kwargs
)
count = 0
if v_return_count:
count_sql = select(func.count()).select_from(sql.alias())
count_queryset = await self.db.execute(count_sql)
count = count_queryset.one()[0]
if limit != 0:
sql = sql.offset((page - 1) * limit).limit(limit)
queryset = await self.db.execute(sql)
queryset = await self.db.scalars(sql)
if v_return_scalars:
if v_return_count:
return queryset, count
return queryset
if v_options:
result = queryset.unique().all()
else:
result = queryset.all()
if v_return_objs:
return queryset.scalars().unique().all()
return [await self.out_dict(i, v_schema=v_schema) for i in queryset.scalars().unique().all()]
if v_return_count:
return list(result), count
return list(result)
datas = [await self.out_dict(i, v_schema=v_schema) for i in result]
if v_return_count:
return datas, count
return datas
async def get_count(
self,
v_options: list = None,
v_join_query: dict = None,
v_or: list[tuple] = None,
v_select_from: list[Any] = None,
v_join: list[list[str | InstrumentedAttribute, BinaryExpression | None]] = None,
v_outerjoin: list[list[str | InstrumentedAttribute, BinaryExpression | None]] = None,
v_where: list[BinaryExpression] = None,
**kwargs
) -> int:
"""
获取数据总数
:param v_options: 指示应使用select在预加载中加载给定的属性
:param v_join_query: 外键字段查询
:param v_or: 或逻辑查询
:param v_select_from: 用于指定查询从哪个表开始通常与 .join() 等方法一起使用
:param v_join: 创建内连接INNER JOIN操作返回两个表中满足连接条件的交集
:param v_outerjoin: 用于创建外连接OUTER JOIN操作返回两个表中满足连接条件的并集包括未匹配的行并用 NULL 值填充
:param v_where: 当前表查询条件原始表达式
:param kwargs: 查询参数
"""
sql = select(func.count(self.model.id).label('total')).where(self.model.is_delete == False)
sql = self.add_filter_condition(sql, v_options, v_join_query, v_or, **kwargs)
v_start_sql = select(func.count(self.model.id))
sql = await self.filter_core(
v_start_sql=v_start_sql,
v_select_from=v_select_from,
v_join=v_join,
v_outerjoin=v_outerjoin,
v_where=v_where,
v_return_sql=True,
**kwargs
)
queryset = await self.db.execute(sql)
return queryset.one()['total']
return queryset.one()[0]
async def create_data(self, data, v_options: list = None, v_return_obj: bool = False, v_schema: Any = None):
async def create_data(
self,
data,
v_options: list[_AbstractLoad] = None,
v_return_obj: bool = False,
v_schema: Any = None
) -> Any:
"""
创建数据
创建单个数据
:param data: 创建数据
:param v_options: 指示应使用select在预加载中加载给定的属性
:param v_schema: 指定使用的序列化对象
@ -165,14 +234,25 @@ class DalBase:
await self.flush(obj)
return await self.out_dict(obj, v_options, v_return_obj, v_schema)
# async def create_datas(self, datas: list[dict]) -> None:
# """
# 批量创建数据,暂不启用
# SQLAlchemy 2.0 批量插入不支持 MySQL 返回对象:
# https://docs.sqlalchemy.org/en/20/orm/queryguide/dml.html#getting-new-objects-with-returning
#
# :param datas: 字典数据列表
# """
# await self.db.execute(insert(self.model), datas)
# await self.db.flush()
async def put_data(
self,
data_id: int,
data: Any,
v_options: list = None,
v_options: list[_AbstractLoad] = None,
v_return_obj: bool = False,
v_schema: Any = None
):
) -> Any:
"""
更新单个数据
:param data_id: 修改行数据的 ID
@ -188,7 +268,7 @@ class DalBase:
await self.flush(obj)
return await self.out_dict(obj, None, v_return_obj, v_schema)
async def delete_datas(self, ids: list[int], v_soft: bool = False, **kwargs):
async def delete_datas(self, ids: list[int], v_soft: bool = False, **kwargs) -> None:
"""
删除多条数据
:param ids: 数据集
@ -207,100 +287,152 @@ class DalBase:
await self.db.execute(delete(self.model).where(self.model.id.in_(ids)))
await self.flush()
def add_filter_condition(
self,
sql: select,
v_options: list = None,
v_join_query: dict = None,
v_or: list[tuple] = None,
**kwargs
) -> select:
async def flush(self, obj: Any = None) -> Any:
"""
添加过滤条件以及内连接过滤条件
刷新到数据库
"""
if obj:
self.db.add(obj)
await self.db.flush()
if obj:
await self.db.refresh(obj)
return obj
当外键模型在查询模型中存在多个外键时则需要添加onclause属性
:param sql:
async def out_dict(
self,
obj: Any,
v_options: list[_AbstractLoad] = None,
v_return_obj: bool = False,
v_schema: Any = None
) -> Any:
"""
序列化
:param obj:
:param v_options: 指示应使用select在预加载中加载给定的属性
:param v_join_query: 外键字段查询内连接
:param v_or: 或逻辑
:param v_return_obj: 是否返回对象
:param v_schema: 指定使用的序列化对象
:return:
"""
if v_options:
obj = await self.get_data(obj.id, v_options=v_options)
if v_return_obj:
return obj
if v_schema:
return v_schema.model_validate(obj).model_dump()
return self.schema.model_validate(obj).model_dump()
async def filter_core(
self,
v_start_sql: SelectType = None,
v_select_from: list[Any] = None,
v_join: list[list[str | InstrumentedAttribute, BinaryExpression | None]] = None,
v_outerjoin: list[list[str | InstrumentedAttribute, BinaryExpression | None]] = None,
v_options: list[_AbstractLoad] = None,
v_where: list[BinaryExpression] = None,
v_order: str = None,
v_order_field: str = None,
v_return_sql: bool = False,
**kwargs
) -> ScalarResult | SelectType:
"""
数据过滤核心功能
:param v_start_sql: 初始 sql
:param v_select_from: 用于指定查询从哪个表开始通常与 .join() 等方法一起使用
:param v_join: 创建内连接INNER JOIN操作返回两个表中满足连接条件的交集
:param v_outerjoin: 用于创建外连接OUTER JOIN操作返回两个表中满足连接条件的并集包括未匹配的行并用 NULL 值填充
:param v_options: 用于为查询添加附加选项如预加载延迟加载等
:param v_where: 当前表查询条件原始表达式
:param v_order: 排序默认正序 desc 是倒叙
:param v_order_field: 排序字段
:param v_return_sql: 是否直接返回 sql
:return: 返回过滤后的总数居 sql
"""
if not isinstance(v_start_sql, SelectType):
v_start_sql = select(self.model).where(self.model.is_delete == False)
sql = self.add_relation(
v_start_sql=v_start_sql,
v_select_from=v_select_from,
v_join=v_join,
v_outerjoin=v_outerjoin,
v_options=v_options
)
if v_where:
sql = sql.where(*v_where)
sql = self.add_filter_condition(sql, **kwargs)
if v_order_field and (v_order in self.ORDER_FIELD):
sql = sql.order_by(getattr(self.model, v_order_field).desc(), self.model.id.desc())
elif v_order_field:
sql = sql.order_by(getattr(self.model, v_order_field), self.model.id)
elif v_order in self.ORDER_FIELD:
sql = sql.order_by(self.model.id.desc())
if v_return_sql:
return sql
queryset = await self.db.scalars(sql)
return queryset
def add_relation(
self,
v_start_sql: SelectType,
v_select_from: list[Any] = None,
v_join: list[list[str | InstrumentedAttribute, BinaryExpression | None]] = None,
v_outerjoin: list[list[str | InstrumentedAttribute, BinaryExpression | None]] = None,
v_options: list[_AbstractLoad] = None,
) -> SelectType:
"""
:param v_start_sql: 初始 sql
:param v_select_from: 用于指定查询从哪个表开始通常与 .join() 等方法一起使用
:param v_join: 创建内连接INNER JOIN操作返回两个表中满足连接条件的交集
:param v_outerjoin: 用于创建外连接OUTER JOIN操作返回两个表中满足连接条件的并集包括未匹配的行并用 NULL 值填充
:param v_options: 用于为查询添加附加选项如预加载延迟加载等
"""
if v_select_from:
v_start_sql = v_start_sql.select_from(*v_select_from)
if v_join:
for relation in v_join:
table = relation[0]
if isinstance(table, str):
table = getattr(self.model, table)
if len(relation) == 2:
v_start_sql = v_start_sql.join(table, relation[1])
else:
v_start_sql = v_start_sql.join(table)
if v_outerjoin:
for relation in v_outerjoin:
table = relation[0]
if isinstance(table, str):
table = getattr(self.model, table)
if len(relation) == 2:
v_start_sql = v_start_sql.outerjoin(table, relation[1])
else:
v_start_sql = v_start_sql.outerjoin(table)
if v_options:
v_start_sql = v_start_sql.options(*v_options)
return v_start_sql
def add_filter_condition(self, sql: SelectType, **kwargs) -> SelectType:
"""
添加过滤条件
:param sql:
:param kwargs: 关键词参数
"""
v_select_from: Set[str] = set()
v_join: Set[str] = set()
v_join_left: Set[str] = set()
if v_join_query:
for key, value in v_join_query.items():
foreign_key = self.key_models.get(key)
conditions = self.__dict_filter(foreign_key.get("model"), **value)
conditions = self.__dict_filter(**kwargs)
if conditions:
sql = sql.where(*conditions)
v_join.add(key)
if v_or:
sql = self.__or_filter(sql, v_or, v_join_left, v_join)
sql = self.__generate_join_conditions(sql, v_join, "join", v_select_from)
sql = self.__generate_join_conditions(sql, v_join_left, "outerjoin", v_select_from)
# 多对多关系查询使用
for item in v_select_from:
sql = sql.select_from(item)
conditions = self.__dict_filter(self.model, **kwargs)
if conditions:
sql = sql.where(*conditions)
if v_options:
sql = sql.options(*[load for load in v_options])
return sql
def __generate_join_conditions(self, sql, model_keys: Set[str], join_type: str, v_select_from: []):
"""
生成 join 条件
"""
for item in model_keys:
foreign_key = self.key_models.get(item)
join = foreign_key.get("join", None)
model = foreign_key.get("model")
if join:
v_select_from.add(model)
model = join
if join_type == "join":
sql = sql.join(model, onclause=foreign_key.get("onclause"))
elif join_type == "outerjoin":
sql = sql.outerjoin(model, onclause=foreign_key.get("onclause"))
return sql
def __or_filter(self, sql: select, v_or: list[tuple], v_join_left: Set[str], v_join: Set[str]):
"""
或逻辑操作
:param sql:
:param v_or: 或逻辑
:param v_join_left: 左连接
:param v_join: 内连接
"""
or_list = []
for item in v_or:
if len(item) == 2:
model = self.model
condition = {item[0]: item[1]}
or_list.extend(self.__dict_filter(model, **condition))
elif len(item) == 4 and item[0] == "fk":
model = self.key_models.get(item[1]).get("model")
condition = {item[2]: item[3]}
conditions = self.__dict_filter(model, **condition)
if conditions:
or_list.extend(conditions)
v_join_left.add(item[1])
if item[1] in v_join:
v_join.remove(item[1])
else:
raise CustomException(msg="v_or 获取查询属性失败,语法错误!")
if or_list:
sql = sql.where(or_(i for i in or_list))
return sql
@staticmethod
def __dict_filter(model, **kwargs):
def __dict_filter(self, **kwargs) -> list[BinaryExpression]:
"""
字典过滤
:param model:
@ -309,7 +441,7 @@ class DalBase:
conditions = []
for field, value in kwargs.items():
if value is not None and value != "":
attr = getattr(model, field)
attr = getattr(self.model, field)
if isinstance(value, tuple):
if len(value) == 1:
if value[0] == "None":
@ -341,31 +473,3 @@ class DalBase:
else:
conditions.append(attr == value)
return conditions
async def flush(self, obj: Any = None):
"""
刷新到数据库
"""
if obj:
self.db.add(obj)
await self.db.flush()
if obj:
await self.db.refresh(obj)
return obj
async def out_dict(self, obj: Any, v_options: list = None, v_return_obj: bool = False, v_schema: Any = None):
"""
序列化
:param obj:
:param v_options: 指示应使用select在预加载中加载给定的属性
:param v_return_obj: 是否返回对象
:param v_schema: 指定使用的序列化对象
:return:
"""
if v_options:
obj = await self.get_data(obj.id, v_options=v_options)
if v_return_obj:
return obj
if v_schema:
return v_schema.model_validate(obj).model_dump()
return self.schema.model_validate(obj).model_dump()

View File

@ -1,26 +1,26 @@
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/10/18 22:19
# @Update Time : 2023/8/18 9:00
# @File : database.py
# @IDE : PyCharm
# @desc : SQLAlchemy 部分
"""
导入 SQLAlchemy 部分
安装 pip install sqlalchemy
中文文档https://www.osgeo.cn/sqlalchemy/
安装 pip install sqlalchemy[asyncio]
官方文档https://docs.sqlalchemy.org/en/20/intro.html#installation
"""
from typing import AsyncGenerator
from aioredis import Redis
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.ext.declarative import declared_attr, declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker, AsyncAttrs
from sqlalchemy.orm import DeclarativeBase, declared_attr
from application.settings import SQLALCHEMY_DATABASE_URL, REDIS_DB_ENABLE, MONGO_DB_ENABLE
from fastapi import Request
from core.exception import CustomException
from motor.motor_asyncio import AsyncIOMotorDatabase
def create_async_engine_session(database_url: str):
def create_async_engine_session(database_url: str) -> async_sessionmaker[AsyncSession]:
"""
创建数据库会话
@ -35,7 +35,7 @@ def create_async_engine_session(database_url: str):
:param database_url: 数据库地址
:return:
"""
engine = create_async_engine(
async_engine = create_async_engine(
database_url,
echo=False,
pool_pre_ping=True,
@ -44,18 +44,30 @@ def create_async_engine_session(database_url: str):
max_overflow=5,
connect_args={}
)
return sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=True, class_=AsyncSession)
return async_sessionmaker(
autocommit=False,
autoflush=False,
bind=async_engine,
expire_on_commit=True,
class_=AsyncSession
)
class Base:
"""将表名改为小写"""
class Base(AsyncAttrs, DeclarativeBase):
"""
创建基本映射类
稍后我们将继承该类创建每个 ORM 模型
"""
@declared_attr
def __tablename__(self):
# 如果有自定义表名就取自定义,没有就取小写类名
table_name = self.__tablename__
@declared_attr.directive
def __tablename__(cls) -> str:
"""
将表名改为小写
如果有自定义表名就取自定义没有就取小写类名
"""
table_name = cls.__tablename__
if not table_name:
model_name = self.__name__
model_name = cls.__name__
ls = []
for index, char in enumerate(model_name):
if char.isupper() and index != 0:
@ -65,29 +77,16 @@ class Base:
return table_name
"""
创建基本映射类
稍后我们将继承该类创建每个 ORM 模型
"""
Model = declarative_base(name='Model', cls=Base)
""" 附上两个SQLAlchemy教程
Python3+SQLAlchemy+Sqlite3实现ORM教程
https://www.cnblogs.com/jiangxiaobo/p/12350561.html
SQLAlchemy基础知识 Autoflush和Autocommit
https://www.jianshu.com/p/b219c3dd4d1e
"""
async def db_getter():
async def db_getter() -> AsyncGenerator[AsyncSession, None]:
"""
获取主数据库
数据库依赖项它将在单个请求中使用然后在请求完成后将其关闭
函数的返回类型被注解为 AsyncGenerator[int, None]其中 AsyncSession 是生成的值的类型 None 表示异步生成器没有终止条件
"""
async with create_async_engine_session(SQLALCHEMY_DATABASE_URL)() as session:
# 创建一个新的事务,半自动 commit
async with session.begin():
yield session

View File

@ -5,21 +5,27 @@
# @IDE : PyCharm
# @desc : 数据库公共 ORM 模型
from core.database import Model
from sqlalchemy import Column, DateTime, Integer, func, Boolean
from datetime import datetime
from sqlalchemy.orm import Mapped, mapped_column
from core.database import Base
from sqlalchemy import DateTime, Integer, func, Boolean
# 使用命令alembic init alembic 初始化迁移数据库环境
# 这时会生成alembic文件夹 和 alembic.ini文件
class BaseModel(Model):
class BaseModel(Base):
"""
公共 ORM 模型基表
"""
__abstract__ = True
id = Column(Integer, primary_key=True, unique=True, comment='主键ID', index=True, nullable=False)
create_datetime = Column(DateTime, server_default=func.now(), comment='创建时间')
update_datetime = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment='更新时间')
delete_datetime = Column(DateTime, nullable=True, comment='删除时间')
is_delete = Column(Boolean, default=False, comment="是否软删除")
id: Mapped[int] = mapped_column(Integer, primary_key=True, comment='主键ID')
create_datetime: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), comment='创建时间')
update_datetime: Mapped[datetime] = mapped_column(
DateTime,
server_default=func.now(),
onupdate=func.now(),
comment='更新时间'
)
delete_datetime: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, comment='删除时间')
is_delete: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否软删除")

Binary file not shown.

View File

@ -18,7 +18,7 @@ class CreateApp:
def __init__(self, path: str):
"""
@params path: app 路径根目录为apps填写apps后面路径即可例子vadmin/auth
:param path: app 路径根目录为apps填写apps后面路径即可例子vadmin/auth
"""
self.app_path = os.path.join(self.APPS_ROOT, path)
self.path = path
@ -46,7 +46,7 @@ class CreateApp:
"""
创建 python
@params path: 绝对路径
:param path: 绝对路径
"""
if self.exist(path):
return

View File

@ -7,13 +7,14 @@
# @desc : 简要说明
from enum import Enum
from sqlalchemy import insert
from core.database import db_getter
from utils.excel.excel_manage import ExcelManage
from application.settings import BASE_DIR, VERSION
import os
from apps.vadmin.auth import models as auth_models
from apps.vadmin.system import models as system_models
from sqlalchemy.sql.schema import Table
from apps.vadmin.help import models as help_models
import subprocess
@ -76,20 +77,16 @@ class InitializeData:
"""
生成数据
@params table_name: 表名
@params model: 数据表模型
:param table_name: 表名
:param model: 数据表模型
"""
async_session = db_getter()
db = await async_session.__anext__()
if isinstance(model, Table):
for data in self.datas.get(table_name):
await db.execute(model.insert().values(**data))
else:
for data in self.datas.get(table_name):
db.add(model(**data))
print(f"{table_name} 表数据已生成")
datas = self.datas.get(table_name)
await db.execute(insert(model), datas)
await db.flush()
await db.commit()
print(f"{table_name} 表数据已生成")
async def generate_menu(self):
"""
@ -113,7 +110,7 @@ class InitializeData:
"""
生成用户
"""
await self.__generate_data("vadmin_auth_user_roles", auth_models.vadmin_user_roles)
await self.__generate_data("vadmin_auth_user_roles", auth_models.vadmin_auth_user_roles)
async def generate_system_tab(self):
"""
@ -139,6 +136,18 @@ class InitializeData:
"""
await self.__generate_data("vadmin_system_dict_details", system_models.VadminDictDetails)
async def generate_help_issue_category(self):
"""
生成常见问题类别数据
"""
await self.__generate_data("vadmin_help_issue_category", help_models.VadminIssueCategory)
async def generate_help_issue(self):
"""
生成常见问题详情数据
"""
await self.__generate_data("vadmin_help_issue", help_models.VadminIssue)
async def run(self, env: Environment = Environment.pro):
"""
执行初始化工作
@ -152,4 +161,6 @@ class InitializeData:
await self.generate_dict_type()
await self.generate_system_config()
await self.generate_dict_details()
await self.generate_help_issue_category()
await self.generate_help_issue()
print(f"环境:{env} {VERSION} 数据已初始化完成")

View File

@ -14,7 +14,8 @@ from openpyxl import load_workbook, Workbook
from application.settings import TEMP_DIR, TEMP_URL
import hashlib
import random
from openpyxl.styles import Alignment
from openpyxl.styles import Alignment, Font, PatternFill, Border, Side
from .excel_schema import AlignmentModel, FontModel, PatternFillModel
class ExcelManage:
@ -116,6 +117,9 @@ class ExcelManage:
"""
if header:
self.sheet.append(header)
pattern_fill_style = PatternFillModel(start_color='D9D9D9', end_color='D9D9D9', fill_type='solid')
font_style = FontModel(bold=True)
self.__set_row_style(1, len(header), pattern_fill_style=pattern_fill_style, font_style=font_style)
for index, data in enumerate(rows):
format_columns = {
"date_columns": []
@ -125,9 +129,10 @@ class ExcelManage:
data[i] = data[i].strftime("%Y/%m/%d %H:%M:%S")
format_columns["date_columns"].append(i + 1)
self.sheet.append(data)
self.__set_row_style(index+2, len(data)-1)
self.__set_row_style(index + 2, len(data))
self.__set_row_format(index + 2, format_columns)
self.__auto_width()
self.__set_row_border()
def save_excel(self, filename: str = None):
"""
@ -148,22 +153,34 @@ class ExcelManage:
# 返回访问路由
return f"{TEMP_URL}/{date}/{name}"
def __set_row_style(self, row: int, max_column: int):
def __set_row_style(
self,
row: int,
max_column: int,
alignment_style: AlignmentModel = AlignmentModel(),
font_style: FontModel = FontModel(),
pattern_fill_style: PatternFillModel = PatternFillModel()
):
"""
设置行样式
:param row:
:param max_column: 最大列
:param alignment_style: 单元格内容的对齐设置
:param font_style: 单元格内容的字体样式设置
:param pattern_fill_style: 单元格的填充模式设置
"""
for index in range(0, max_column):
# 设置单元格对齐方式
# Alignment(horizontal=水平对齐模式,vertical=垂直对齐模式,text_rotation=旋转角度,wrap_text=是否自动换行)
alignment = Alignment(horizontal='center', vertical='center', text_rotation=0, wrap_text=False)
alignment = Alignment(**alignment_style.model_dump())
font = Font(**font_style.model_dump())
pattern_fill = PatternFill(**pattern_fill_style.model_dump())
self.sheet.cell(row=row, column=index + 1).alignment = alignment
self.sheet.cell(row=row, column=index + 1).font = font
self.sheet.cell(row=row, column=index + 1).fill = pattern_fill
def __set_row_format(self, row: int, columns: dict):
"""
设置行样式
格式化行数据类型
:param row:
:param columns: 列数据
@ -171,6 +188,22 @@ class ExcelManage:
for index in columns.get("date_columns", []):
self.sheet.cell(row=row, column=index).number_format = "yyyy/mm/dd h:mm:ss"
def __set_row_border(self):
"""
设置行边框
"""
# 创建 Border 对象并设置边框样式
border = Border(
left=Side(border_style="thin", color="000000"),
right=Side(border_style="thin", color="000000"),
top=Side(border_style="thin", color="000000"),
bottom=Side(border_style="thin", color="000000")
)
# 设置整个表格的边框
for row in self.sheet.iter_rows():
for cell in row:
cell.border = border
def __auto_width(self):
"""
设置自适应列宽

View File

@ -0,0 +1,65 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/08/24 22:19
# @File : excel_schema.py
# @IDE : PyCharm
# @desc :
from pydantic import BaseModel, Field
class AlignmentModel(BaseModel):
horizontal: str = Field('center', description="水平对齐方式。可选值:'left''center''right''justify''distributed'")
vertical: str = Field('center', description="垂直对齐方式。可选值:'top''center''bottom''justify''distributed'")
textRotation: int = Field(0, description="文本旋转角度(以度为单位)。默认为 0。")
wrapText: bool = Field(None, description="自动换行文本。设置为 True 时,如果文本内容超出单元格宽度,会自动换行显示。")
shrinkToFit: bool = Field(
None,
description="缩小字体以适应单元格。设置为 True 时,如果文本内容超出单元格宽度,会自动缩小字体大小以适应。"
)
indent: int = Field(0, description="文本缩进级别。默认为 0。")
relativeIndent: int = Field(0, description="相对缩进级别。默认为 0。")
justifyLastLine: bool = Field(
None,
description="对齐换行文本的末尾行。设置为 True 时,如果设置了文本换行,末尾的行也会被对齐。"
)
readingOrder: int = Field(0, description="阅读顺序。默认为 0。")
class Config:
title = "对齐设置模型"
description = "用于设置单元格内容的对齐样式。"
class FontModel(BaseModel):
name: str = Field(None, description="字体名称")
size: float = Field(None, description="字体大小(磅为单位)")
bold: bool = Field(None, description="是否加粗")
italic: bool = Field(None, description="是否斜体")
underline: str = Field(None, description="下划线样式。可选值:'single''double''singleAccounting''doubleAccounting'")
strikethrough: bool = Field(None, description="是否有删除线")
outline: bool = Field(None, description="是否轮廓字体")
shadow: bool = Field(None, description="是否阴影字体")
condense: bool = Field(None, description="是否压缩字体")
extend: bool = Field(None, description="是否扩展字体")
vertAlign: str = Field(None, description="垂直对齐方式。可选值:'superscript''subscript''baseline'")
color: dict = Field(None, description="字体颜色")
scheme: str = Field(None, description="字体方案。可选值:'major''minor'")
charset: int = Field(None, description="字符集编号")
family: int = Field(None, description="字体族编号")
class Config:
title = "字体设置模型"
description = "用于设置单元格内容的字体样式"
class PatternFillModel(BaseModel):
start_color: str = Field(None, description="起始颜色RGB 值或颜色名称)")
end_color: str = Field(None, description="结束颜色RGB 值或颜色名称)")
fill_type: str = Field("solid", description="填充类型('none''solid''darkDown' 等)")
class Config:
title = "填充模式设置模型"
description = "单元格的填充模式设置"

View File

@ -43,7 +43,9 @@ class EmailSender:
try:
self.server.login(self.email, self.password)
except smtplib.SMTPAuthenticationError:
raise CustomException("邮箱服务器认证失败!")
raise CustomException("邮件发送失败,邮箱服务器认证失败!")
except AttributeError:
raise CustomException("邮件发送失败,邮箱服务器认证失败!")
async def send_email(self, to_emails: List[str], subject: str, body: str, attachments: List[str] = None):
"""