diff --git a/README.md b/README.md index 49bf376..765927f 100644 --- a/README.md +++ b/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次的免费次数 diff --git a/kinit-admin/src/views/Login/components/LoginForm.vue b/kinit-admin/src/views/Login/components/LoginForm.vue index f8b01b0..00725c8 100644 --- a/kinit-admin/src/views/Login/components/LoginForm.vue +++ b/kinit-admin/src/views/Login/components/LoginForm.vue @@ -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([ ]) 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 = () => { diff --git a/kinit-admin/src/views/vadmin/auth/role/components/Write.vue b/kinit-admin/src/views/vadmin/auth/role/components/Write.vue index 960dfc3..8874d6c 100644 --- a/kinit-admin/src/views/vadmin/auth/role/components/Write.vue +++ b/kinit-admin/src/views/vadmin/auth/role/components/Write.vue @@ -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" /> - +
- 立即保存 + 立即保存
diff --git a/kinit-admin/src/views/vadmin/system/settings/baidu.vue b/kinit-admin/src/views/vadmin/system/settings/baidu.vue index 895d324..1d0c19b 100644 --- a/kinit-admin/src/views/vadmin/system/settings/baidu.vue +++ b/kinit-admin/src/views/vadmin/system/settings/baidu.vue @@ -59,7 +59,7 @@ getData() diff --git a/kinit-admin/src/views/vadmin/system/settings/basic.vue b/kinit-admin/src/views/vadmin/system/settings/basic.vue index 34519d1..f36b8a4 100644 --- a/kinit-admin/src/views/vadmin/system/settings/basic.vue +++ b/kinit-admin/src/views/vadmin/system/settings/basic.vue @@ -152,7 +152,7 @@ getData() diff --git a/kinit-admin/src/views/vadmin/system/settings/email.vue b/kinit-admin/src/views/vadmin/system/settings/email.vue index 0f4eaad..3aa3cf0 100644 --- a/kinit-admin/src/views/vadmin/system/settings/email.vue +++ b/kinit-admin/src/views/vadmin/system/settings/email.vue @@ -59,7 +59,7 @@ getData() diff --git a/kinit-admin/src/views/vadmin/system/settings/privacy.vue b/kinit-admin/src/views/vadmin/system/settings/privacy.vue index 4766991..7763615 100644 --- a/kinit-admin/src/views/vadmin/system/settings/privacy.vue +++ b/kinit-admin/src/views/vadmin/system/settings/privacy.vue @@ -73,7 +73,7 @@ getData() :editorConfig="editorConfig" />
- 立即保存 + 立即保存
diff --git a/kinit-admin/src/views/vadmin/system/settings/wxServer.vue b/kinit-admin/src/views/vadmin/system/settings/wxServer.vue index 5ec341a..979408c 100644 --- a/kinit-admin/src/views/vadmin/system/settings/wxServer.vue +++ b/kinit-admin/src/views/vadmin/system/settings/wxServer.vue @@ -59,7 +59,7 @@ getData() diff --git a/kinit-api/README.md b/kinit-api/README.md index 209d3b1..9be3162 100644 --- a/kinit-api/README.md +++ b/kinit-api/README.md @@ -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 代码部分: -![image-20230514113859232](D:\programming\ktianc\project\kinit-pro\images\image-20230514113859232.png) +```python +def __dict_filter(self, **kwargs) -> list[BinaryExpression]: + """ + 字典过滤 + :param model: + :param kwargs: + """ + conditions = [] + for field, value in kwargs.items(): + if value is not None and value != "": + attr = getattr(self.model, field) + if isinstance(value, tuple): + if len(value) == 1: + if value[0] == "None": + conditions.append(attr.is_(None)) + elif value[0] == "not None": + conditions.append(attr.isnot(None)) + else: + raise CustomException("SQL查询语法错误") + elif len(value) == 2 and value[1] not in [None, [], ""]: + if value[0] == "date": + # 根据日期查询, 关键函数是:func.time_format和func.date_format + conditions.append(func.date_format(attr, "%Y-%m-%d") == value[1]) + elif value[0] == "like": + conditions.append(attr.like(f"%{value[1]}%")) + elif value[0] == "in": + conditions.append(attr.in_(value[1])) + elif value[0] == "between" and len(value[1]) == 2: + conditions.append(attr.between(value[1][0], value[1][1])) + elif value[0] == "month": + conditions.append(func.date_format(attr, "%Y-%m") == value[1]) + elif value[0] == "!=": + conditions.append(attr != value[1]) + elif value[0] == ">": + conditions.append(attr > value[1]) + elif value[0] == "<=": + conditions.append(attr <= value[1]) + else: + raise CustomException("SQL查询语法错误") + else: + conditions.append(attr == value) + return conditions +``` 示例: @@ -168,120 +254,51 @@ python main.py migrate --env dev ```python users = UserDal(db).get_datas(limit=0, id=("in", [1,2,4,6]), email=("not None"), name=("like", "李")) + +# 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 +) ``` diff --git a/kinit-api/application/settings.py b/kinit-api/application/settings.py index 667a5ad..a97eab8 100644 --- a/kinit-api/application/settings.py +++ b/kinit-api/application/settings.py @@ -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 令牌签名算法""" diff --git a/kinit-api/apps/vadmin/auth/crud.py b/kinit-api/apps/vadmin/auth/crud.py index 75ac002..4fb5578 100644 --- a/kinit-api/apps/vadmin/auth/crud.py +++ b/kinit-api/apps/vadmin/auth/crud.py @@ -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 """ @@ -298,8 +312,8 @@ class UserDal(DalBase): user.wx_server_openid = openid 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 { @@ -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("-----------------------结束------------------------") diff --git a/kinit-api/apps/vadmin/auth/models/__init__.py b/kinit-api/apps/vadmin/auth/models/__init__.py index 425867d..b520efe 100644 --- a/kinit-api/apps/vadmin/auth/models/__init__.py +++ b/kinit-api/apps/vadmin/auth/models/__init__.py @@ -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 diff --git a/kinit-api/apps/vadmin/auth/models/m2m.py b/kinit-api/apps/vadmin/auth/models/m2m.py index 7da5e4a..ee70e90 100644 --- a/kinit-api/apps/vadmin/auth/models/m2m.py +++ b/kinit-api/apps/vadmin/auth/models/m2m.py @@ -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")), ) + diff --git a/kinit-api/apps/vadmin/auth/models/menu.py b/kinit-api/apps/vadmin/auth/models/menu.py index a85dec8..74fc228 100644 --- a/kinit-api/apps/vadmin/auth/models/menu.py +++ b/kinit-api/apps/vadmin/auth/models/menu.py @@ -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,则不会被 缓存(默认 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,则不会被 缓存(默认 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 + ) diff --git a/kinit-api/apps/vadmin/auth/models/role.py b/kinit-api/apps/vadmin/auth/models/role.py index a957117..e5ac2c7 100644 --- a/kinit-api/apps/vadmin/auth/models/role.py +++ b/kinit-api/apps/vadmin/auth/models/role.py @@ -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) diff --git a/kinit-api/apps/vadmin/auth/models/user.py b/kinit-api/apps/vadmin/auth/models/user.py index 43d7876..cb85ff0 100644 --- a/kinit-api/apps/vadmin/auth/models/user.py +++ b/kinit-api/apps/vadmin/auth/models/user.py @@ -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: """ 获取该用户是否拥有最高权限 diff --git a/kinit-api/apps/vadmin/auth/utils/current.py b/kinit-api/apps/vadmin/auth/utils/current.py index 4f65f93..802e06a 100644 --- a/kinit-api/apps/vadmin/auth/utils/current.py +++ b/kinit-api/apps/vadmin/auth/utils/current.py @@ -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) diff --git a/kinit-api/apps/vadmin/auth/utils/login.py b/kinit-api/apps/vadmin/auth/utils/login.py index c67bf1d..b6884b6 100644 --- a/kinit-api/apps/vadmin/auth/utils/login.py +++ b/kinit-api/apps/vadmin/auth/utils/login.py @@ -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}) diff --git a/kinit-api/apps/vadmin/auth/utils/validation/login.py b/kinit-api/apps/vadmin/auth/utils/validation/login.py index 195cc81..27148ed 100644 --- a/kinit-api/apps/vadmin/auth/utils/validation/login.py +++ b/kinit-api/apps/vadmin/auth/utils/validation/login.py @@ -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 diff --git a/kinit-api/apps/vadmin/auth/views.py b/kinit-api/apps/vadmin/auth/views.py index 6725eb8..932d203 100644 --- a/kinit-api/apps/vadmin/auth/views.py +++ b/kinit-api/apps/vadmin/auth/views.py @@ -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,角色权限使用") diff --git a/kinit-api/apps/vadmin/help/crud.py b/kinit-api/apps/vadmin/help/crud.py index 722ac9d..1e0b371 100644 --- a/kinit-api/apps/vadmin/help/crud.py +++ b/kinit-api/apps/vadmin/help/crud.py @@ -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) diff --git a/kinit-api/apps/vadmin/help/models/issue.py b/kinit-api/apps/vadmin/help/models/issue.py index cc7ebbd..f0e906b 100644 --- a/kinit-api/apps/vadmin/help/models/issue.py +++ b/kinit-api/apps/vadmin/help/models/issue.py @@ -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) diff --git a/kinit-api/apps/vadmin/help/views.py b/kinit-api/apps/vadmin/help/views.py index 004d08f..617399a 100644 --- a/kinit-api/apps/vadmin/help/views.py +++ b/kinit-api/apps/vadmin/help/views.py @@ -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) diff --git a/kinit-api/apps/vadmin/record/crud.py b/kinit-api/apps/vadmin/record/crud.py index 0955d62..f9b9f55 100644 --- a/kinit-api/apps/vadmin/record/crud.py +++ b/kinit-api/apps/vadmin/record/crud.py @@ -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 diff --git a/kinit-api/apps/vadmin/record/models/login.py b/kinit-api/apps/vadmin/record/models/login.py index 91a04a5..b4631ff 100644 --- a/kinit-api/apps/vadmin/record/models/login.py +++ b/kinit-api/apps/vadmin/record/models/login.py @@ -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( diff --git a/kinit-api/apps/vadmin/record/models/sms.py b/kinit-api/apps/vadmin/record/models/sms.py index a75a8c7..236449f 100644 --- a/kinit-api/apps/vadmin/record/models/sms.py +++ b/kinit-api/apps/vadmin/record/models/sms.py @@ -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="发送场景") diff --git a/kinit-api/apps/vadmin/record/views.py b/kinit-api/apps/vadmin/record/views.py index 3a12321..4002fed 100644 --- a/kinit-api/apps/vadmin/record/views.py +++ b/kinit-api/apps/vadmin/record/views.py @@ -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) diff --git a/kinit-api/apps/vadmin/system/crud.py b/kinit-api/apps/vadmin/system/crud.py index 903c31d..c21b91d 100644 --- a/kinit-api/apps/vadmin/system/crud.py +++ b/kinit-api/apps/vadmin/system/crud.py @@ -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: """ 获取任务信息列表 diff --git a/kinit-api/apps/vadmin/system/models/__init__.py b/kinit-api/apps/vadmin/system/models/__init__.py index 5e9fd9e..c18b7be 100644 --- a/kinit-api/apps/vadmin/system/models/__init__.py +++ b/kinit-api/apps/vadmin/system/models/__init__.py @@ -1,3 +1,2 @@ from .dict import VadminDictType, VadminDictDetails -from .settings import VadminSystemSettings -from .settings_tab import VadminSystemSettingsTab +from .settings import VadminSystemSettings, VadminSystemSettingsTab diff --git a/kinit-api/apps/vadmin/system/models/dict.py b/kinit-api/apps/vadmin/system/models/dict.py index dddae87..060ba47 100644 --- a/kinit-api/apps/vadmin/system/models/dict.py +++ b/kinit-api/apps/vadmin/system/models/dict.py @@ -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="备注") diff --git a/kinit-api/apps/vadmin/system/models/settings.py b/kinit-api/apps/vadmin/system/models/settings.py index 0590f6c..f2f083a 100644 --- a/kinit-api/apps/vadmin/system/models/settings.py +++ b/kinit-api/apps/vadmin/system/models/settings.py @@ -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") diff --git a/kinit-api/apps/vadmin/system/models/settings_tab.py b/kinit-api/apps/vadmin/system/models/settings_tab.py deleted file mode 100644 index 1807912..0000000 --- a/kinit-api/apps/vadmin/system/models/settings_tab.py +++ /dev/null @@ -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") diff --git a/kinit-api/apps/vadmin/system/views.py b/kinit-api/apps/vadmin/system/views.py index 3517119..bdf43db 100644 --- a/kinit-api/apps/vadmin/system/views.py +++ b/kinit-api/apps/vadmin/system/views.py @@ -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="每次进入系统中时使用") diff --git a/kinit-api/core/crud.py b/kinit-api/core/crud.py index 4cf5c1d..9b34d7e 100644 --- a/kinit-api/core/crud.py +++ b/kinit-api/core/crud.py @@ -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() diff --git a/kinit-api/core/database.py b/kinit-api/core/database.py index 545deb2..92e5b8f 100644 --- a/kinit-api/core/database.py +++ b/kinit-api/core/database.py @@ -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 diff --git a/kinit-api/db/db_base.py b/kinit-api/db/db_base.py index 4ee9fbf..086ecd2 100644 --- a/kinit-api/db/db_base.py +++ b/kinit-api/db/db_base.py @@ -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="是否软删除") diff --git a/kinit-api/requirements.txt b/kinit-api/requirements.txt index 7be1bfd..0ee9177 100644 Binary files a/kinit-api/requirements.txt and b/kinit-api/requirements.txt differ diff --git a/kinit-api/scripts/create_app/main.py b/kinit-api/scripts/create_app/main.py index 85b99eb..1782a45 100644 --- a/kinit-api/scripts/create_app/main.py +++ b/kinit-api/scripts/create_app/main.py @@ -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 diff --git a/kinit-api/scripts/initialize/data/init.xlsx b/kinit-api/scripts/initialize/data/init.xlsx index 562dda4..9a138b8 100644 Binary files a/kinit-api/scripts/initialize/data/init.xlsx and b/kinit-api/scripts/initialize/data/init.xlsx differ diff --git a/kinit-api/scripts/initialize/initialize.py b/kinit-api/scripts/initialize/initialize.py index 1686b29..b04506e 100644 --- a/kinit-api/scripts/initialize/initialize.py +++ b/kinit-api/scripts/initialize/initialize.py @@ -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} 数据已初始化完成") diff --git a/kinit-api/utils/excel/excel_manage.py b/kinit-api/utils/excel/excel_manage.py index 4d9974e..749fe99 100644 --- a/kinit-api/utils/excel/excel_manage.py +++ b/kinit-api/utils/excel/excel_manage.py @@ -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): """ 设置自适应列宽 diff --git a/kinit-api/utils/excel/excel_schema.py b/kinit-api/utils/excel/excel_schema.py new file mode 100644 index 0000000..e3cbd76 --- /dev/null +++ b/kinit-api/utils/excel/excel_schema.py @@ -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 = "单元格的填充模式设置" + diff --git a/kinit-api/utils/send_email.py b/kinit-api/utils/send_email.py index 14d9c49..1e469ed 100644 --- a/kinit-api/utils/send_email.py +++ b/kinit-api/utils/send_email.py @@ -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): """