重大更新到2.0.0
This commit is contained in:
parent
45a8abe6bc
commit
09859d4d80
25
README.md
25
README.md
@ -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次的免费次数
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -1,8 +1,8 @@
|
||||
# FastAPI 项目
|
||||
|
||||
fastapi 源代码:https://github.com/tiangolo/fastapi
|
||||
fastapi Github:https://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:用户 - 角色 - 菜单接口服务
|
||||
- models:ORM 模型目录
|
||||
- params:查询参数依赖项目录
|
||||
- schemas:pydantic 模型,用于数据库序列化操作目录
|
||||
- utils:登录认证功能接口服务
|
||||
- curd.py:数据库操作
|
||||
- views.py:视图函数
|
||||
- core:核心文件目录
|
||||
- crud.py:关系型数据库操作核心封装
|
||||
- database.py:关系型数据库核心配置
|
||||
- data_types.py:自定义数据类型
|
||||
- exception.py:异常处理
|
||||
- logger:日志处理核心配置
|
||||
- middleware.py:中间件核心配置
|
||||
- dependencies.py:常用依赖项
|
||||
- event.py:全局事件
|
||||
- mongo_manage.py:mongodb 数据库操作核心封装
|
||||
- validator.py:pydantic 模型重用验证器
|
||||
- db:ORM模型基类
|
||||
- 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
|
||||
|
||||
代码部分:
|
||||
|
||||

|
||||
```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", "李"))
|
||||
|
||||
# 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
|
||||
)
|
||||
```
|
||||
|
||||
### v_join_query
|
||||
查询所有用户id为1或2或 4或6,并且email不为空,并且名称包括李:
|
||||
|
||||
外键字段查询,内连接
|
||||
|
||||
以常见问题类别表为例:
|
||||
|
||||
首先需要在 `crud.py/IssueCategoryDal` 的 `__init__` 方法中定义 `key_models`:
|
||||
查询第一页,每页两条数据,并返回总数,同样可以通过 `get_datas` 实现原始查询方式:
|
||||
|
||||
```python
|
||||
class IssueCategoryDal(DalBase):
|
||||
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)
|
||||
|
||||
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
|
||||
)
|
||||
```
|
||||
|
||||
使用案例:
|
||||
### 外键查询示例
|
||||
|
||||
以常见问题表为主表,查询出创建用户名称为kinit的用户,创建了哪些常见问题,并加载出用户信息:
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
完整案例:
|
||||
|
||||
```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
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
或逻辑运算查询
|
||||
|
||||
语法:
|
||||
|
||||
```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
|
||||
)
|
||||
```
|
||||
|
||||
|
@ -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。当验证失败时,如果设置为 True,FastAPI 将自动返回一个 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 令牌签名算法"""
|
||||
|
@ -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'> {
|
||||
@ -423,12 +437,12 @@ class MenuDal(DalBase):
|
||||
}
|
||||
"""
|
||||
if any([i.is_admin for i in user.roles]):
|
||||
sql = select(self.model)\
|
||||
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:
|
||||
raise CustomException("无法删除存在角色关联的菜单", code=400)
|
||||
return await super(MenuDal, self).delete_datas(ids, v_soft, **kwargs)
|
||||
count = await RoleDal(self.db).get_count(v_join=[["menus"]], v_where=[self.model.id.in_(ids)])
|
||||
if count > 0:
|
||||
raise CustomException("无法删除存在角色关联的菜单", code=400)
|
||||
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("-----------------------结束------------------------")
|
||||
|
@ -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
|
||||
|
@ -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")),
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
"""
|
||||
获取该用户是否拥有最高权限
|
||||
|
@ -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)
|
||||
|
@ -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})
|
||||
|
@ -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)
|
||||
|
||||
count_key = f"{data.telephone}_password_auth" if data.method == '0' else f"{data.telephone}_sms_auth"
|
||||
count = Count(redis_getter(request), count_key)
|
||||
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
|
||||
|
@ -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,角色权限使用")
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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="发送场景")
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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:
|
||||
"""
|
||||
获取任务信息列表
|
||||
|
||||
|
@ -1,3 +1,2 @@
|
||||
from .dict import VadminDictType, VadminDictDetails
|
||||
from .settings import VadminSystemSettings
|
||||
from .settings_tab import VadminSystemSettingsTab
|
||||
from .settings import VadminSystemSettings, VadminSystemSettingsTab
|
||||
|
@ -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="备注")
|
||||
|
@ -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")
|
||||
|
@ -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")
|
@ -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="每次进入系统中时使用")
|
||||
|
@ -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)
|
||||
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)
|
||||
conditions = self.__dict_filter(**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()
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
@ -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
|
||||
|
Binary file not shown.
@ -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} 数据已初始化完成")
|
||||
|
@ -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": []
|
||||
@ -123,11 +127,12 @@ class ExcelManage:
|
||||
for i in range(0, len(data)):
|
||||
if isinstance(data[i], datetime.datetime):
|
||||
data[i] = data[i].strftime("%Y/%m/%d %H:%M:%S")
|
||||
format_columns["date_columns"].append(i+1)
|
||||
format_columns["date_columns"].append(i + 1)
|
||||
self.sheet.append(data)
|
||||
self.__set_row_style(index+2, len(data)-1)
|
||||
self.__set_row_format(index+2, format_columns)
|
||||
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)
|
||||
self.sheet.cell(row=row, column=index+1).alignment = alignment
|
||||
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):
|
||||
"""
|
||||
设置自适应列宽
|
||||
|
65
kinit-api/utils/excel/excel_schema.py
Normal file
65
kinit-api/utils/excel/excel_schema.py
Normal 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 = "单元格的填充模式设置"
|
||||
|
@ -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):
|
||||
"""
|
||||
|
Loading…
x
Reference in New Issue
Block a user