From 44396878707eaa47eedcef0b9782183304fd09cd Mon Sep 17 00:00:00 2001 From: sunyugang Date: Fri, 11 Apr 2025 14:30:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E9=A1=B9=E7=9B=AE=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E7=AE=A1=E7=90=86=E7=9A=84=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 374 +----------------- alembic/env.py | 1 - application/config/development.py | 7 + application/config/production.py | 7 + application/urls.py | 6 - apps/business/project/crud.py | 135 ++++++- apps/business/project/models/project.py | 1 + apps/business/project/params/project_image.py | 2 - apps/business/project/schemas/__init__.py | 2 +- .../business/project/schemas/project_image.py | 10 +- .../project/schemas/project_img_leafer.py | 3 +- apps/business/project/views.py | 61 ++- apps/vadmin/analysis/__init__.py | 0 apps/vadmin/analysis/views.py | 84 ---- apps/vadmin/auth/crud.py | 8 +- apps/vadmin/resource/__init__.py | 0 apps/vadmin/resource/crud.py | 20 - apps/vadmin/resource/models/__init__.py | 1 - apps/vadmin/resource/models/images.py | 27 -- apps/vadmin/resource/params/__init__.py | 1 - apps/vadmin/resource/params/images.py | 27 -- apps/vadmin/resource/schemas/__init__.py | 1 - apps/vadmin/resource/schemas/images.py | 33 -- apps/vadmin/resource/views.py | 60 --- apps/vadmin/system/crud.py | 123 +----- apps/vadmin/system/models/__init__.py | 1 - apps/vadmin/system/models/settings.py | 42 -- apps/vadmin/system/schemas/__init__.py | 2 - apps/vadmin/system/schemas/settings.py | 29 -- apps/vadmin/system/schemas/settings_tab.py | 28 -- apps/vadmin/system/views.py | 66 +--- apps/vadmin/workplace/__init__.py | 7 - apps/vadmin/workplace/views.py | 159 -------- core/event.py | 8 - requirements.txt | Bin 3792 -> 3844 bytes scripts/initialize/data/init.xlsx | Bin 50994 -> 25214 bytes utils/cache.py | 89 ----- utils/huawei_obs.py | 59 +++ utils/send_email.py | 10 +- utils/sms/aliyun.py | 3 +- utils/wx/oauth.py | 3 +- 41 files changed, 276 insertions(+), 1224 deletions(-) delete mode 100644 apps/vadmin/analysis/__init__.py delete mode 100644 apps/vadmin/analysis/views.py delete mode 100644 apps/vadmin/resource/__init__.py delete mode 100644 apps/vadmin/resource/crud.py delete mode 100644 apps/vadmin/resource/models/__init__.py delete mode 100644 apps/vadmin/resource/models/images.py delete mode 100644 apps/vadmin/resource/params/__init__.py delete mode 100644 apps/vadmin/resource/params/images.py delete mode 100644 apps/vadmin/resource/schemas/__init__.py delete mode 100644 apps/vadmin/resource/schemas/images.py delete mode 100644 apps/vadmin/resource/views.py delete mode 100644 apps/vadmin/system/models/settings.py delete mode 100644 apps/vadmin/system/schemas/settings.py delete mode 100644 apps/vadmin/system/schemas/settings_tab.py delete mode 100644 apps/vadmin/workplace/__init__.py delete mode 100644 apps/vadmin/workplace/views.py delete mode 100644 utils/cache.py create mode 100644 utils/huawei_obs.py diff --git a/README.md b/README.md index 6b43a6f..70a0832 100644 --- a/README.md +++ b/README.md @@ -1,373 +1 @@ -# FastAPI 项目 - -fastapi Github:https://github.com/tiangolo/fastapi - -fastapi 官方文档:https://fastapi.tiangolo.com/zh/ - -fastapi 更新说明:https://fastapi.tiangolo.com/zh/release-notes/ - -pydantic 官方文档:https://pydantic-docs.helpmanual.io/ - -pydantic 数据模型代码生成器官方文档 (Json -> Pydantic):https://koxudaxi.github.io/datamodel-code-generator/ - -SQLAlchemy-Utils:https://sqlalchemy-utils.readthedocs.io/en/latest/ - -alembic 中文文档:https://hellowac.github.io/alembic_doc/zh/_front_matter.html - -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:静态资源存放目录 -- utils:封装的一些工具类目录 -- main.py:主程序入口文件 -- alembic.ini:数据库迁移配置文件 - -## 开发环境 - -开发语言:Python 3.10 - -开发框架:Fastapi 0.101.1 - -ORM 框架:SQLAlchemy 2.0.20 - -## 开发工具 - -Pycharm 2022.3.2 - -推荐插件:Chinese (Simplified) Language Pack / 中文语言包 - -代码样式配置: - -![image-20230315194534959](https://ktianc.oss-cn-beijing.aliyuncs.com/kinit/public/images/image-20230315194534959.png) - -## 使用 - -``` -source /opt/env/kinit-pro/bin/activate - -# 安装依赖库 -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 -``` - -### 运行启动 - -```shell -# 直接运行main文件 -python main.py run -``` - -## 其他操作 - -在线文档地址(在配置文件里面设置路径或者关闭) - -``` -http://127.0.0.1:9000/docs -``` - -Git更新ignore文件直接修改gitignore是不会生效的,需要先去掉已经托管的文件,修改完成之后再重新添加并提交。 -``` -第一步: -git rm -r --cached . -去掉已经托管的文件 - -第二步: -修改自己的igonre文件内容 - -第三步: -git add . -git commit -m "clear cached" -``` - -## 新的数据迁移 - -- 新建模型: - - 在你的app目录下新建一个models目录,`__init__.py`导入你需要迁移的models - ```python - # app/.../your_app/models/__init__.py - from .your_model import YourModel,YourModel2 - - ``` - ```python - # app/.../your_app/models/your_model.py - from db.db_base import BaseModel - - class YourModel(BaseModel): - # 定义你的model - ... - - class YourModel2(BaseModel): - # 定义你的model - ... - ``` -- 根据模型配置你的alembic: - ``` - # alembic.ini - [dev] - ... - sqlalchemy.url = mysql+pymysql://your_username:password@ip:port/kinit - ... - ``` - ```python - # alembic/env.py - # 导入项目中的基本映射类,与 需要迁移的 ORM 模型 - from apps.vadmin.auth.models import * - ... - from apps.xxx.your_app.models import * - ``` -- 执行数据库迁移命令(终端执行执行脚本): - ```shell - # 执行命令(生产环境): - 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目录中多个迁移文件 - -## 新的CRUD - -- 新的模型文件已经建好(上一步迁移时必须) -- 在 scripts/crud_generate/main.py 添加执行命令 - -```python -# scripts/crud_generate/main.py -if __name__ == '__main__': - from apps.xxx.your_app.models import YourModel - - crud = CrudGenerate(YourModel, "中文名", "en_name") - # 只打印代码,不执行创建写入 - crud.generate_codes() - # 创建并写入代码 - crud.main() -``` - -- 生成后会自动创建crud, params,schema, views - -## 新的路由配置 - -```python -# application/urls.py - -from apps.xxx.your_app.views import app as your_app - -urlpatterns = [ - ..., - {"ApiRouter": your_app, "prefix": "/your_router", "tags": ["your_tag"]}, -] -``` - -完成后在 http://127.0.0.1:9000/docs 验证生成的接口 - -## 查询数据 - -### 自定义的一些查询过滤 - -```python -# 日期查询 -# 值的类型:str -# 值得格式:%Y-%m-%d:2023-05-14 -字段名称=("date", 值) - -# 模糊查询 -# 值的类型: str -字段名称=("like", 值) - -# in 查询 -# 值的类型:list -字段名称=("in", 值) - -# 时间区间查询 -# 值的类型:tuple 或者 list -字段名称=("between", 值) - -# 月份查询 -# 值的类型:str -# 值的格式:%Y-%m:2023-05 -字段名称=("month", 值) - -# 不等于查询 -字段名称=("!=", 值) - -# 大于查询 -字段名称=(">", 值) - -# 等于 None -字段名称=("None") - -# 不等于 None -字段名称=("not None") -``` - -代码部分: - -```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 -``` - -示例: - -查询所有用户id为1或2或 4或6,并且email不为空,并且名称包括李: - -```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 -) -``` - -查询所有用户id为1或2或 4或6,并且email不为空,并且名称包括李: - -查询第一页,每页两条数据,并返回总数,同样可以通过 `get_datas` 实现原始查询方式: - -```python -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) - -# 这里的 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 -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 -) -``` - +基于aicheckv2改写的项目,增加后台管理(用户,部门,权限) \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py index 3dc486d..1d87d26 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -38,7 +38,6 @@ from apps.vadmin.auth.models import * from apps.vadmin.system.models import * from apps.vadmin.record.models import * from apps.vadmin.help.models import * -from apps.vadmin.resource.models import * from apps.business.project.models.project import * from apps.business.train.models.train import * from apps.business.detect.models.detect import * diff --git a/application/config/development.py b/application/config/development.py index 90f96a4..9bdd423 100644 --- a/application/config/development.py +++ b/application/config/development.py @@ -47,6 +47,13 @@ ALIYUN_OSS = { "baseUrl": "baseUrl" } +HUAWEI_OBS = { + "AccessKeyID": "HPUAICKSMPQP4XSATCLX", + "SecretAccessKey": "DoAL1RWydNwSQRsodS4c34nC0XyXy6TAsetVMasy", + "server": "https://obs.cn-north-4.myhuaweicloud.com", + "bucketName": "aicheckv2" +} + """ 获取IP地址归属地 文档:https://user.ip138.com/ip/doc diff --git a/application/config/production.py b/application/config/production.py index 91df85c..14a9976 100644 --- a/application/config/production.py +++ b/application/config/production.py @@ -46,6 +46,13 @@ ALIYUN_OSS = { "baseUrl": "baseUrl" } +HUAWEI_OBS = { + "AccessKeyID": "HPUAICKSMPQP4XSATCLX", + "SecretAccessKey": "DoAL1RWydNwSQRsodS4c34nC0XyXy6TAsetVMasy", + "server": "https://obs.cn-north-4.myhuaweicloud.com", + "bucketName": "aicheckv2" +} + """ 获取IP地址归属地 文档:https://user.ip138.com/ip/doc diff --git a/application/urls.py b/application/urls.py index 11bf063..3e7f698 100644 --- a/application/urls.py +++ b/application/urls.py @@ -9,10 +9,7 @@ from apps.vadmin.auth.utils.login import app as auth_app from apps.vadmin.auth.views import app as vadmin_auth_app from apps.vadmin.system.views import app as vadmin_system_app from apps.vadmin.record.views import app as vadmin_record_app -from apps.vadmin.workplace.views import app as vadmin_workplace_app -from apps.vadmin.analysis.views import app as vadmin_analysis_app from apps.vadmin.help.views import app as vadmin_help_app -from apps.vadmin.resource.views import app as vadmin_resource_app from apps.business.project.views import app as project_app @@ -22,9 +19,6 @@ urlpatterns = [ {"ApiRouter": vadmin_auth_app, "prefix": "/vadmin/auth", "tags": ["权限管理"]}, {"ApiRouter": vadmin_system_app, "prefix": "/vadmin/system", "tags": ["系统管理"]}, {"ApiRouter": vadmin_record_app, "prefix": "/vadmin/record", "tags": ["记录管理"]}, - {"ApiRouter": vadmin_workplace_app, "prefix": "/vadmin/workplace", "tags": ["工作区管理"]}, - {"ApiRouter": vadmin_analysis_app, "prefix": "/vadmin/analysis", "tags": ["数据分析管理"]}, {"ApiRouter": vadmin_help_app, "prefix": "/vadmin/help", "tags": ["帮助中心管理"]}, - {"ApiRouter": vadmin_resource_app, "prefix": "/vadmin/resource", "tags": ["资源管理"]}, {"ApiRouter": project_app, "prefix": "/business/project", "tags": ["项目管理"]}, ] diff --git a/apps/business/project/crud.py b/apps/business/project/crud.py index 41e847b..75e03a2 100644 --- a/apps/business/project/crud.py +++ b/apps/business/project/crud.py @@ -9,18 +9,25 @@ import application.settings from . import schemas, models, params from apps.vadmin.auth.utils.validation.auth import Auth from utils import os_utils as os, random_utils as ru +from utils.huawei_obs import ObsClient +from utils import status +from core.exception import CustomException if application.settings.DEBUG: - from application.config.development import datasets_url, runs_url, detect_url, yolo_url, images_url + from application.config.development import datasets_url, runs_url, images_url else: - from application.config.production import datasets_url, runs_url, detect_url, yolo_url, images_url + from application.config.production import datasets_url, runs_url, images_url from typing import Any, List from core.crud import DalBase +from fastapi import UploadFile from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, case, and_ class ProjectInfoDal(DalBase): + """ + 项目信息 + """ def __init__(self, db: AsyncSession): super(ProjectInfoDal, self).__init__() @@ -106,15 +113,112 @@ class ProjectInfoDal(DalBase): class ProjectImageDal(DalBase): + """ + 项目图片 + """ def __init__(self, db: AsyncSession): super(ProjectImageDal, self).__init__() self.db = db self.model = models.ProjectImage - self.schema = schemas.ProjectImageSimpleOut + self.schema = schemas.ProjectImageOut + + async def img_page(self, param: params.ProjectImageParams): + """ + 分页查询图片信息,然后关联一个图片的标签数量 + """ + subquery = ( + select( + models.ProjectImgLabel.image_id, + func.ifnull(func.count(models.ProjectImgLabel.id), 0).label('label_count') + ) + .group_by(models.ProjectImgLabel.image_id) + .subquery() + ) + # 2 主查询 + query = ( + select( + models.ProjectImage, + func.ifnull(subquery.c.label_count, 0).label('label_count') + ) + .outerjoin(subquery, models.ProjectImage.id == subquery.c.image_id) + ) + v_where = [models.ProjectImage.project_id == param.project_id, models.ProjectImage.img_type == param.img_type] + sql = await self.filter_core( + v_start_sql=query, + v_where=v_where, + v_return_sql=True, + v_order=param.v_order, + v_order_field=param.v_order_field + ) + count = await self.get_count_sql(sql) + if param.limit != 0: + sql = sql.offset((param.page - 1) * param.limit).limit(param.limit) + queryset = await self.db.execute(sql) + result = queryset.all() + datas = [] + for result in result: + data = schemas.ProjectImageOut.model_validate(result[0]) + data.label_count = int(result[1]) + datas.append(data.model_dump()) + return datas, count + + async def upload_imgs(self, files: List[UploadFile], pro: schemas.ProjectInfoOut, img_type: str) -> int: + """ + 上传项目图片 + """ + image_models = [] + for file in files: + image = models.ProjectImage() + image.project_id = pro.id + image.file_name = file.filename + image.img_type = img_type + # 保存原图 + path = os.save_images(images_url, pro.project_no, file=file) + image.image_url = path + # 上传图片到obs + object_key = pro.project_no + '/' + img_type + '/' + file.filename + success, key, url = ObsClient.put_file(object_key=object_key, file_path=path) + if success: + image.object_key = object_key + image.thumb_image_url = url + else: + raise CustomException("obs上传失败", code=status.HTTP_ERROR) + image_models.append(image) + await self.create_datas(datas=image_models) + return len(image_models) + + async def check_img_name(self, file_name: str, project_id: int, img_type: str): + """ + 校验相同的项目,相同的文件类型是否有同名的文件 + """ + count = await self.get_count(v_where=[ + models.ProjectImage.file_name == file_name, + models.ProjectImage.project_id == project_id, + models.ProjectImage.img_type == img_type + ]) + return count > 0 + + async def del_img(self, ids: List[int]): + """ + 删除图片,删除数据库数据,删除本地的文件,删除obs中的文件 + """ + file_urls = [] + object_keys = [] + for img_id in ids: + image = self.get_data(data_id=img_id) + if image: + file_urls.append(image.image_url) + object_keys.append(image.object_key) + os.delete_file_if_exists(**file_urls) + ObsClient.del_objects(object_keys) + await self.delete_datas(ids) class ProjectLabelDal(DalBase): + """ + 项目标签 + """ def __init__(self, db: AsyncSession): super(ProjectLabelDal, self).__init__() @@ -139,18 +243,37 @@ class ProjectLabelDal(DalBase): class ProjectImgLabelDal(DalBase): - + """ + 图片标签信息 + """ def __init__(self, db: AsyncSession): super(ProjectImgLabelDal, self).__init__() self.db = db self.model = models.ProjectImgLabel - self.schema = schemas.ProjectImgLabelSimpleOut + + async def add_img_label(self, img_label_in: schemas.ProjectImgLeaferLabel): + # 先把历史数据都删掉,然后再保存 + image_id = img_label_in.image_id + await self.delete_datas(image_id=image_id) + img_labels = [self.model(**i.model_dump()) for i in img_label_in.label_infos] + for img in img_labels: + img.image_id = image_id + await self.create_datas(img_labels) class ProjectImgLeaferDal(DalBase): + """ + 图片标注信息-leafer.js + """ def __init__(self, db: AsyncSession): super(ProjectImgLeaferDal, self).__init__() self.db = db self.model = models.ProjectImgLeafer - self.schema = schemas.ProjectImgLeaferSimpleOut + self.schema = schemas.ProjectImgLeaferOut + + async def add_leafer(self, img_label_in: schemas.ProjectImgLeaferLabel): + # 先把历史数据都删掉,然后再保存 + image_id = img_label_in.image_id + await self.delete_datas(image_id=image_id) + await self.create_data(data=self.model(**img_label_in.model_dump())) diff --git a/apps/business/project/models/project.py b/apps/business/project/models/project.py index d777da5..1efc060 100644 --- a/apps/business/project/models/project.py +++ b/apps/business/project/models/project.py @@ -43,6 +43,7 @@ class ProjectImage(BaseModel): img_type: Mapped[str] = mapped_column(String(10)) file_name: Mapped[str] = mapped_column(String(64), nullable=False) image_url: Mapped[str] = mapped_column(String(255), nullable=False) + object_key: Mapped[str] = mapped_column(String(255), nullable=False) thumb_image_url: Mapped[str] = mapped_column(String(255), nullable=False) project_id: Mapped[int] = mapped_column(Integer) diff --git a/apps/business/project/params/project_image.py b/apps/business/project/params/project_image.py index bbd0c43..b015ac1 100644 --- a/apps/business/project/params/project_image.py +++ b/apps/business/project/params/project_image.py @@ -15,11 +15,9 @@ class ProjectImageParams(QueryParams): self, img_type: str | None = Query(None, title="图片类别"), project_id: int | None = Query(None, title="项目id"), - file_name: str | None = Query(None, title="文件名称"), params: Paging = Depends() ): super().__init__(params) self.img_type = img_type self.project_id = project_id - self.file_name = file_name diff --git a/apps/business/project/schemas/__init__.py b/apps/business/project/schemas/__init__.py index 0767d2b..65abc40 100644 --- a/apps/business/project/schemas/__init__.py +++ b/apps/business/project/schemas/__init__.py @@ -1,5 +1,5 @@ from .project_info import ProjectInfoIn, ProjectInfoOut, ProjectInfoPagerOut -from .project_image import ProjectImage, ProjectImagePager, ProjectImageOut +from .project_image import ProjectImageOut, ProjectImage from .project_label import ProjectLabel from .project_img_label import ProjectImgLabelIn from .project_img_leafer import ProjectImgLeaferLabel, ProjectImgLeaferOut diff --git a/apps/business/project/schemas/project_image.py b/apps/business/project/schemas/project_image.py index 53b8621..184a824 100644 --- a/apps/business/project/schemas/project_image.py +++ b/apps/business/project/schemas/project_image.py @@ -14,8 +14,8 @@ from typing import Optional class ProjectImage(BaseModel): id: Optional[int] = Field(None, description="id") project_id: Optional[int] = Field(..., description="项目id") - img_type: Optional[str] = Field(None, description="图片类别") file_name: Optional[str] = Field(None, description="文件名称") + thumb_image_url: Optional[str] = Field(None, description="图片在obs上的链接") create_time: DatetimeStr model_config = ConfigDict(from_attributes=True) @@ -25,14 +25,8 @@ class ProjectImageOut(BaseModel): id: Optional[int] = Field(None, description="id") project_id: Optional[int] = Field(..., description="项目id") file_name: Optional[str] = Field(None, description="文件名称") + thumb_image_url: Optional[str] = Field(None, description="图片在obs上的链接") create_time: DatetimeStr label_count: Optional[int] model_config = ConfigDict(from_attributes=True) - - -class ProjectImagePager(BaseModel): - project_id: Optional[int] = Field(..., description="项目id") - img_type: Optional[str] = Field(None, description="图片类别") - pagerNum: Optional[int] = Field(None, description="当前页码") - pagerSize: Optional[int] = Field(None, description="每页数量") diff --git a/apps/business/project/schemas/project_img_leafer.py b/apps/business/project/schemas/project_img_leafer.py index 5843ae1..bb87836 100644 --- a/apps/business/project/schemas/project_img_leafer.py +++ b/apps/business/project/schemas/project_img_leafer.py @@ -5,12 +5,11 @@ # @File : project_img_leafer.py # @IDE : PyCharm # @desc : pydantic 模型,用于数据库序列化操作 +from . import ProjectImgLabelIn from pydantic import BaseModel, Field, ConfigDict from typing import Optional, List -from . import ProjectImgLabelIn - class ProjectImgLeaferLabel(BaseModel): image_id: Optional[int] = Field(..., description="图片id") diff --git a/apps/business/project/views.py b/apps/business/project/views.py index b175f31..771796c 100644 --- a/apps/business/project/views.py +++ b/apps/business/project/views.py @@ -4,12 +4,13 @@ # @Create Time : 2025/04/03 10:25 # @File : views.py # @IDE : PyCharm -# @desc : 路由,视图文件 +# @desc : 路由,项目信息管理,包括项目主体,项目图片,项目标签和图片的标注信息 from utils.response import SuccessResponse, ErrorResponse from . import params, schemas, crud, models from core.dependencies import IdList -from fastapi import APIRouter, Depends +from typing import List +from fastapi import APIRouter, Depends, UploadFile, File, Form from apps.vadmin.auth.utils.current import FullAdminAuth from apps.vadmin.auth.utils.validation.auth import Auth @@ -42,7 +43,7 @@ async def add_project( @app.get("/info/{pro_id}", summary="查询项目信息") -async def project( +async def project_info( pro_id: int, auth: Auth = Depends(FullAdminAuth()) ): @@ -98,10 +99,64 @@ async def delete_label( label_ids: IdList = Depends(), auth: Auth = Depends(FullAdminAuth()) ): + # 删除标签信息,然后删除图片标签关联表 await crud.ProjectLabelDal(auth.db).delete_datas(label_ids.ids) + for label_id in label_ids.ids: + await crud.ProjectImgLabelDal(auth.db).delete_datas(label_id=label_id) return SuccessResponse(msg="删除成功") +@app.get("/img", summary="获取图片列表,分两种情况,一个带分页的,一个不带分页的") +async def project_pager( + param: params.ProjectImageParams = Depends(), + auth: Auth = Depends(FullAdminAuth()) +): + if param.limit: + # 分页查询,关联一个标签数量 + datas, count = await crud.ProjectImageDal(auth.db).img_page(param) + return SuccessResponse(data=datas, count=count) + else: + # 直接查询 + datas = await crud.ProjectImageDal(auth.db).get_datas(v_schema=schemas.ProjectImage, **param.dict()) + return SuccessResponse(data=datas) + + +@app.post("/img", summary="上传图片") +async def up_img( + project_id: int = Form(...), + files: List[UploadFile] = File(...), + img_type: str = Form(...), + auth: Auth = Depends(FullAdminAuth()) +): + pro = await crud.ProjectInfoDal(auth.db).get_data(data_id=project_id) + if pro is None: + return ErrorResponse(msg="项目查询失败,请稍后再试") + count = await crud.ProjectImageDal(auth.db).upload_imgs(files=files, pro=pro, img_type=img_type) + if count > 0: + return SuccessResponse(msg="上传成功") + + +@app.delete("/img", summary="删除照片") +async def del_img( + img_id: IdList = Depends(), + auth: Auth = Depends(FullAdminAuth()) +): + await crud.ProjectImageDal(auth.db).del_img(img_id.ids) + return SuccessResponse(msg="删除成功") + + +@app.post("/img_label", summary="保存图片的标注信息") +async def add_img_label( + img_label_in: schemas.ProjectImgLeaferLabel, + auth: Auth = Depends(FullAdminAuth()) +): + await crud.ProjectImgLabelDal(auth.db).add_img_label(img_label_in) + await crud.ProjectImgLeaferDal(auth.db).add_leafer(img_label_in) + return SuccessResponse(msg="保存成功") + + + + diff --git a/apps/vadmin/analysis/__init__.py b/apps/vadmin/analysis/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/vadmin/analysis/views.py b/apps/vadmin/analysis/views.py deleted file mode 100644 index f465580..0000000 --- a/apps/vadmin/analysis/views.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# @version : 1.0 -# @Create Time : 2022/10/19 15:41 -# @File : views.py -# @IDE : PyCharm -# @desc : 简要说明 - -from fastapi import APIRouter, Depends -from apps.vadmin.auth.utils.current import AllUserAuth -from utils.response import SuccessResponse -from apps.vadmin.auth.utils.validation.auth import Auth -import random - -app = APIRouter() - - -########################################################### -# 图表数据 -########################################################### -@app.get("/random/number", summary="获取随机整数") -async def get_random_number(auth: Auth = Depends(AllUserAuth())): - return SuccessResponse(random.randint(500, 20000)) - - -@app.get("/banners", summary="轮播图") -async def get_banners(auth: Auth = Depends(AllUserAuth())): - data = [ - { - "id": 1, "image": "https://ktianc.oss-cn-beijing.aliyuncs.com/kinit/system/banner/2022-11-14/1.jpg" - }, - { - "id": 2, "image": "https://ktianc.oss-cn-beijing.aliyuncs.com/kinit/system/banner/2022-11-09/banner1.png" - }, - { - "id": 3, "image": "https://ktianc.oss-cn-beijing.aliyuncs.com/kinit/system/banner/2022-11-09/banner3.png" - }, - ] - return SuccessResponse(data) - - -@app.get("/user/access/source", summary="用户来源") -async def get_user_access_source(auth: Auth = Depends(AllUserAuth())): - data = [ - {"value": 1000, "name": 'analysis.directAccess'}, - {"value": 310, "name": 'analysis.mailMarketing'}, - {"value": 234, "name": 'analysis.allianceAdvertising'}, - {"value": 135, "name": 'analysis.videoAdvertising'}, - {"value": 1548, "name": 'analysis.searchEngines'} - ] - return SuccessResponse(data) - - -@app.get("/weekly/user/activity", summary="每周用户活跃量") -async def get_weekly_user_activity(auth: Auth = Depends(AllUserAuth())): - data = [ - {"value": 13253, "name": 'analysis.monday'}, - {"value": 34235, "name": 'analysis.tuesday'}, - {"value": 26321, "name": 'analysis.wednesday'}, - {"value": 12340, "name": 'analysis.thursday'}, - {"value": 24643, "name": 'analysis.friday'}, - {"value": 1322, "name": 'analysis.saturday'}, - {"value": 1324, "name": 'analysis.sunday'} - ] - return SuccessResponse(data) - - -@app.get("/monthly/sales", summary="每月销售额") -async def get_monthly_sales(auth: Auth = Depends(AllUserAuth())): - data = [ - {"estimate": 100, "actual": 120, "name": 'analysis.january'}, - {"estimate": 120, "actual": 82, "name": 'analysis.february'}, - {"estimate": 161, "actual": 91, "name": 'analysis.march'}, - {"estimate": 134, "actual": 154, "name": 'analysis.april'}, - {"estimate": 105, "actual": 162, "name": 'analysis.may'}, - {"estimate": 160, "actual": 140, "name": 'analysis.june'}, - {"estimate": 165, "actual": 145, "name": 'analysis.july'}, - {"estimate": 114, "actual": 250, "name": 'analysis.august'}, - {"estimate": 163, "actual": 134, "name": 'analysis.september'}, - {"estimate": 185, "actual": 56, "name": 'analysis.october'}, - {"estimate": 118, "actual": 99, "name": 'analysis.november'}, - {"estimate": 123, "actual": 123, "name": 'analysis.december'} - ] - return SuccessResponse(data) diff --git a/apps/vadmin/auth/crud.py b/apps/vadmin/auth/crud.py index 5f64fe2..e74b28d 100644 --- a/apps/vadmin/auth/crud.py +++ b/apps/vadmin/auth/crud.py @@ -18,7 +18,7 @@ from sqlalchemy import select, false, and_ from core.crud import DalBase from sqlalchemy.ext.asyncio import AsyncSession from core.validator import vali_telephone -from utils.file.aliyun_oss import AliyunOSS, BucketConf +from utils.huawei_obs import ObsClient from utils.excel.import_manage import ImportManage, FieldType from utils.excel.write_xlsx import WriteXlsx from utils.send_email import EmailSender @@ -397,10 +397,10 @@ class UserDal(DalBase): :param file: :return: """ - result = await AliyunOSS(BucketConf(**settings.ALIYUN_OSS)).upload_image("avatar", file) - user.avatar = result + success, key, url = await ObsClient().put_object("avatar"+"/"+user.name+file.filename, file) + user.avatar = url await self.flush(user) - return result + return url async def update_wx_server_openid(self, code: str, user: models.VadminUser, redis: Redis) -> bool: """ diff --git a/apps/vadmin/resource/__init__.py b/apps/vadmin/resource/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/vadmin/resource/crud.py b/apps/vadmin/resource/crud.py deleted file mode 100644 index b90463f..0000000 --- a/apps/vadmin/resource/crud.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# @version : 1.0 -# @Create Time : 2023/8/25 13:15 -# @File : crud.py -# @IDE : PyCharm -# @desc : 简要说明 - -from sqlalchemy.ext.asyncio import AsyncSession -from core.crud import DalBase -from . import models, schemas - - -class ImagesDal(DalBase): - - def __init__(self, db: AsyncSession): - super(ImagesDal, self).__init__() - self.db = db - self.model = models.VadminImages - self.schema = schemas.ImagesSimpleOut diff --git a/apps/vadmin/resource/models/__init__.py b/apps/vadmin/resource/models/__init__.py deleted file mode 100644 index ca31757..0000000 --- a/apps/vadmin/resource/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .images import VadminImages diff --git a/apps/vadmin/resource/models/images.py b/apps/vadmin/resource/models/images.py deleted file mode 100644 index 525f2a7..0000000 --- a/apps/vadmin/resource/models/images.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# @version : 1.0 -# @Create Time : 2023/8/25 13:41 -# @File : images.py -# @IDE : PyCharm -# @desc : 图片素材表 - -from sqlalchemy.orm import relationship, Mapped, mapped_column -from apps.vadmin.auth.models import VadminUser -from db.db_base import BaseModel -from sqlalchemy import String, ForeignKey, Integer - - -class VadminImages(BaseModel): - __tablename__ = "vadmin_resource_images" - __table_args__ = ({'comment': '图片素材表'}) - - filename: Mapped[str] = mapped_column(String(255), nullable=False, comment="原图片名称") - image_url: Mapped[str] = mapped_column(String(500), nullable=False, 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/apps/vadmin/resource/params/__init__.py b/apps/vadmin/resource/params/__init__.py deleted file mode 100644 index d18efa1..0000000 --- a/apps/vadmin/resource/params/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .images import ImagesParams diff --git a/apps/vadmin/resource/params/images.py b/apps/vadmin/resource/params/images.py deleted file mode 100644 index 3050386..0000000 --- a/apps/vadmin/resource/params/images.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# @version : 1.0 -# @Create Time : 2023/8/25 14:59 -# @File : images.py -# @IDE : PyCharm -# @desc : 简要说明 - - -from fastapi import Depends -from core.dependencies import Paging, QueryParams - - -class ImagesParams(QueryParams): - """ - 列表分页 - """ - - def __init__( - self, - filename: str = None, - params: Paging = Depends() - ): - super().__init__(params) - self.filename = ('like', filename) - self.v_order = "desc" - self.v_order_field = "create_datetime" diff --git a/apps/vadmin/resource/schemas/__init__.py b/apps/vadmin/resource/schemas/__init__.py deleted file mode 100644 index 7e8d000..0000000 --- a/apps/vadmin/resource/schemas/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .images import Images, ImagesOut, ImagesSimpleOut diff --git a/apps/vadmin/resource/schemas/images.py b/apps/vadmin/resource/schemas/images.py deleted file mode 100644 index 451e7c1..0000000 --- a/apps/vadmin/resource/schemas/images.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# @version : 1.0 -# @Create Time : 2023/8/25 14:49 -# @File : images.py -# @IDE : PyCharm -# @desc : 简要说明 - - -from pydantic import BaseModel, ConfigDict -from core.data_types import DatetimeStr -from apps.vadmin.auth.schemas import UserSimpleOut - - -class Images(BaseModel): - filename: str - image_url: str - - create_user_id: int - - -class ImagesSimpleOut(Images): - model_config = ConfigDict(from_attributes=True) - - id: int - create_datetime: DatetimeStr - update_datetime: DatetimeStr - - -class ImagesOut(ImagesSimpleOut): - model_config = ConfigDict(from_attributes=True) - - create_user: UserSimpleOut diff --git a/apps/vadmin/resource/views.py b/apps/vadmin/resource/views.py deleted file mode 100644 index bf79970..0000000 --- a/apps/vadmin/resource/views.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# @version : 1.0 -# @Create Time : 2023/8/25 9:29 -# @File : views.py -# @IDE : PyCharm -# @desc : 简要说明 - -from fastapi import APIRouter, Depends, UploadFile -from sqlalchemy.orm import joinedload -from core.dependencies import IdList -from utils.file.aliyun_oss import AliyunOSS, BucketConf -from utils.response import SuccessResponse -from . import schemas, crud, params, models -from apps.vadmin.auth.utils.current import FullAdminAuth -from apps.vadmin.auth.utils.validation.auth import Auth -from application.settings import ALIYUN_OSS - -app = APIRouter() - - -########################################################### -# 图片资源管理 -########################################################### -@app.get("/images", summary="获取图片列表") -async def get_images_list(p: params.ImagesParams = Depends(), auth: Auth = Depends(FullAdminAuth())): - model = models.VadminImages - v_options = [joinedload(model.create_user)] - v_schema = schemas.ImagesOut - datas, count = await crud.ImagesDal(auth.db).get_datas( - **p.dict(), - v_options=v_options, - v_schema=v_schema, - v_return_count=True - ) - return SuccessResponse(datas, count=count) - - -@app.post("/images", summary="创建图片") -async def create_images(file: UploadFile, auth: Auth = Depends(FullAdminAuth())): - filepath = f"/resource/images/" - result = await AliyunOSS(BucketConf(**ALIYUN_OSS)).upload_image(filepath, file) - data = schemas.Images( - filename=file.filename, - image_url=result, - create_user_id=auth.user.id - ) - - return SuccessResponse(await crud.ImagesDal(auth.db).create_data(data=data)) - - -@app.delete("/images", summary="删除图片", description="硬删除") -async def delete_images(ids: IdList = Depends(), auth: Auth = Depends(FullAdminAuth())): - await crud.ImagesDal(auth.db).delete_datas(ids.ids, v_soft=False) - return SuccessResponse("删除成功") - - -@app.get("/images/{data_id}", summary="获取图片信息") -async def get_images(data_id: int, auth: Auth = Depends(FullAdminAuth())): - return SuccessResponse(await crud.ImagesDal(auth.db).get_data(data_id, v_schema=schemas.ImagesSimpleOut)) diff --git a/apps/vadmin/system/crud.py b/apps/vadmin/system/crud.py index da83d72..cffbe93 100644 --- a/apps/vadmin/system/crud.py +++ b/apps/vadmin/system/crud.py @@ -7,24 +7,20 @@ # @desc : 数据库 增删改查操作 import json -import os from enum import Enum from typing import Any from redis.asyncio import Redis from fastapi.encoders import jsonable_encoder from motor.motor_asyncio import AsyncIOMotorDatabase -from sqlalchemy import select, update +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import joinedload -from application.settings import STATIC_ROOT, SUBSCRIBE, REDIS_DB_ENABLE -from core.database import redis_getter +from application.settings import SUBSCRIBE 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): @@ -71,121 +67,6 @@ class DictDetailsDal(DalBase): self.schema = schemas.DictDetailsSimpleOut -class SettingsDal(DalBase): - - def __init__(self, db: AsyncSession): - super(SettingsDal, self).__init__() - self.db = db - self.model = models.VadminSystemSettings - self.schema = schemas.SettingsSimpleOut - - async def get_tab_values(self, tab_id: int) -> dict: - """ - 获取系统配置标签下的信息 - """ - datas = await self.get_datas(limit=0, tab_id=tab_id, v_return_objs=True) - result = {} - for data in datas: - if not data.disabled: - result[data.config_key] = data.config_value - return result - - async def update_datas(self, datas: dict, request: Request) -> None: - """ - 更新系统配置信息 - - 更新ico图标步骤:先将文件上传到本地,然后点击提交后,获取到文件地址,将上传的新文件覆盖原有文件 - 原因:ico图标的路径是在前端的index.html中固定的,所以目前只能改变图片,不改变路径 - """ - for key, value in datas.items(): - if key == "web_ico": - continue - elif key == "web_ico_local_path": - if not value: - continue - ico = await self.get_data(config_key="web_ico", tab_id=1) - web_ico = datas.get("web_ico") - if ico.config_value == web_ico: - continue - # 将上传的ico路径替换到static/system/favicon.ico文件 - await FileManage.async_copy_file(value, os.path.join(STATIC_ROOT, "system/favicon.ico")) - 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 == str(key)).values(config_value=value) - await self.db.execute(sql) - if "wx_server_app_id" in datas and REDIS_DB_ENABLE: - rd = redis_getter(request) - await rd.client().set("wx_server", json.dumps(datas)) - elif "sms_access_key" in datas and REDIS_DB_ENABLE: - rd = redis_getter(request) - await rd.client().set('aliyun_sms', json.dumps(datas)) - - async def get_base_config(self) -> dict: - """ - 获取系统基本信息 - """ - ignore_configs = ["wx_server_app_id", "wx_server_app_secret"] - datas = await self.get_datas(limit=0, tab_id=("in", ["1", "9"]), disabled=False, v_return_objs=True) - result = {} - for config in datas: - if config.config_key not in ignore_configs: - result[config.config_key] = config.config_value - return result - - -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) -> dict: - """ - 获取系统配置分类下的标签信息 - """ - model = models.VadminSystemSettingsTab - options = [joinedload(model.settings)] - datas = await self.get_datas( - limit=0, - v_options=options, - classify=("in", classify), - disabled=False, - v_return_objs=True, - hidden=hidden - ) - return self.__generate_values(datas) - - async def get_tab_name_values(self, tab_names: list[str], hidden: bool | None = False) -> dict: - """ - 获取系统配置标签下的标签信息 - """ - model = models.VadminSystemSettingsTab - options = [joinedload(model.settings)] - datas = await self.get_datas( - limit=0, - v_options=options, - tab_name=("in", tab_names), - disabled=False, - v_return_objs=True, - hidden=hidden - ) - return self.__generate_values(datas) - - @classmethod - def __generate_values(cls, datas: list[models.VadminSystemSettingsTab]) -> dict: - """ - 生成字典值 - """ - return { - tab.tab_name: { - item.config_key: item.config_value - for item in tab.settings - if not item.disabled - } - for tab in datas - } - - class TaskDal(MongoManage): class JobOperation(Enum): diff --git a/apps/vadmin/system/models/__init__.py b/apps/vadmin/system/models/__init__.py index c18b7be..8ff9a44 100644 --- a/apps/vadmin/system/models/__init__.py +++ b/apps/vadmin/system/models/__init__.py @@ -1,2 +1 @@ from .dict import VadminDictType, VadminDictDetails -from .settings import VadminSystemSettings, VadminSystemSettingsTab diff --git a/apps/vadmin/system/models/settings.py b/apps/vadmin/system/models/settings.py deleted file mode 100644 index f2f083a..0000000 --- a/apps/vadmin/system/models/settings.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# @version : 1.0 -# @Create Time : 2022/7/7 13:41 -# @File : settings.py -# @IDE : PyCharm -# @desc : 系统字典模型 -from sqlalchemy.orm import relationship, Mapped, mapped_column -from db.db_base import BaseModel -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: 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: 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/apps/vadmin/system/schemas/__init__.py b/apps/vadmin/system/schemas/__init__.py index 6d6f57c..9744247 100644 --- a/apps/vadmin/system/schemas/__init__.py +++ b/apps/vadmin/system/schemas/__init__.py @@ -1,4 +1,2 @@ from .dict import DictType, DictDetails, DictTypeSimpleOut, DictDetailsSimpleOut, DictTypeOptionsOut -from .settings_tab import SettingsTab, SettingsTabSimpleOut -from .settings import Settings, SettingsSimpleOut from .task import Task, TaskSimpleOut diff --git a/apps/vadmin/system/schemas/settings.py b/apps/vadmin/system/schemas/settings.py deleted file mode 100644 index 775d59d..0000000 --- a/apps/vadmin/system/schemas/settings.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# @version : 1.0 -# @Create Time : 2021/10/18 22:19 -# @File : settings.py -# @IDE : PyCharm -# @desc : pydantic 模型,用于数据库序列化操作 - - -from pydantic import BaseModel, ConfigDict -from core.data_types import DatetimeStr - - -class Settings(BaseModel): - config_label: str | None = None - config_key: str - config_value: str | None = None - remark: str | None = None - disabled: bool | None = None - tab_id: int - - -class SettingsSimpleOut(Settings): - model_config = ConfigDict(from_attributes=True) - - id: int - create_datetime: DatetimeStr - update_datetime: DatetimeStr - diff --git a/apps/vadmin/system/schemas/settings_tab.py b/apps/vadmin/system/schemas/settings_tab.py deleted file mode 100644 index fd8a9f2..0000000 --- a/apps/vadmin/system/schemas/settings_tab.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# @version : 1.0 -# @Create Time : 2021/10/18 22:19 -# @File : settings_tab.py -# @IDE : PyCharm -# @desc : pydantic 模型,用于数据库序列化操作 - - -from pydantic import BaseModel, ConfigDict -from core.data_types import DatetimeStr - - -class SettingsTab(BaseModel): - title: str - classify: str - tab_label: str - tab_name: str - hidden: bool - - -class SettingsTabSimpleOut(SettingsTab): - model_config = ConfigDict(from_attributes=True) - - id: int - create_datetime: DatetimeStr - update_datetime: DatetimeStr - diff --git a/apps/vadmin/system/views.py b/apps/vadmin/system/views.py index 2dfec84..34f3200 100644 --- a/apps/vadmin/system/views.py +++ b/apps/vadmin/system/views.py @@ -12,6 +12,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from application.settings import ALIYUN_OSS from core.database import db_getter, redis_getter, mongo_getter from utils.file.aliyun_oss import AliyunOSS, BucketConf +from utils.huawei_obs import ObsClient from utils.file.file_manage import FileManage from utils.response import SuccessResponse, ErrorResponse from utils.sms.code import CodeSMS @@ -102,34 +103,6 @@ async def get_dict_detail(data_id: int, auth: Auth = Depends(AllUserAuth())): return SuccessResponse(await crud.DictDetailsDal(auth.db).get_data(data_id, v_schema=schema)) -########################################################### -# 文件上传管理 -########################################################### -@app.post("/upload/image/to/oss", summary="上传图片到阿里云OSS") -async def upload_image_to_oss(file: UploadFile, path: str = Form(...)): - result = await AliyunOSS(BucketConf(**ALIYUN_OSS)).upload_image(path, file) - return SuccessResponse(result) - - -@app.post("/upload/video/to/oss", summary="上传视频到阿里云OSS") -async def upload_video_to_oss(file: UploadFile, path: str = Form(...)): - result = await AliyunOSS(BucketConf(**ALIYUN_OSS)).upload_video(path, file) - return SuccessResponse(result) - - -@app.post("/upload/file/to/oss", summary="上传文件到阿里云OSS") -async def upload_file_to_oss(file: UploadFile, path: str = Form(...)): - result = await AliyunOSS(BucketConf(**ALIYUN_OSS)).upload_file(path, file) - return SuccessResponse(result) - - -@app.post("/upload/image/to/local", summary="上传图片到本地") -async def upload_image_to_local(file: UploadFile, path: str = Form(...)): - manage = FileManage(file, path) - path = await manage.save_image_local() - return SuccessResponse(path) - - ########################################################### # 短信服务管理 ########################################################### @@ -142,43 +115,6 @@ async def sms_send(telephone: str, rd: Redis = Depends(redis_getter), auth: Auth return SuccessResponse(await sms.main_async()) -########################################################### -# 系统配置管理 -########################################################### -@app.post("/settings/tabs", summary="获取系统配置标签列表") -async def get_settings_tabs(classifys: list[str] = Body(...), auth: Auth = Depends(FullAdminAuth())): - return SuccessResponse(await crud.SettingsTabDal(auth.db).get_datas(limit=0, classify=("in", classifys))) - - -@app.get("/settings/tabs/values", summary="获取系统配置标签下的信息") -async def get_settings_tabs_values(tab_id: int, auth: Auth = Depends(FullAdminAuth())): - return SuccessResponse(await crud.SettingsDal(auth.db).get_tab_values(tab_id=tab_id)) - - -@app.put("/settings/tabs/values", summary="更新系统配置信息") -async def put_settings_tabs_values( - request: Request, - datas: dict = Body(...), - auth: Auth = Depends(FullAdminAuth()) -): - return SuccessResponse(await crud.SettingsDal(auth.db).update_datas(datas, request)) - - -@app.get("/settings/base/config", summary="获取系统基础配置", description="每次进入系统中时使用") -async def get_setting_base_config(db: AsyncSession = Depends(db_getter)): - return SuccessResponse(await crud.SettingsDal(db).get_base_config()) - - -@app.get("/settings/privacy", summary="获取隐私协议") -async def get_settings_privacy(auth: Auth = Depends(OpenAuth())): - return SuccessResponse((await crud.SettingsDal(auth.db).get_data(config_key="web_privacy")).config_value) - - -@app.get("/settings/agreement", summary="获取用户协议") -async def get_settings_agreement(auth: Auth = Depends(OpenAuth())): - return SuccessResponse((await crud.SettingsDal(auth.db).get_data(config_key="web_agreement")).config_value) - - ########################################################### # 定时任务管理 ########################################################### diff --git a/apps/vadmin/workplace/__init__.py b/apps/vadmin/workplace/__init__.py deleted file mode 100644 index 457f603..0000000 --- a/apps/vadmin/workplace/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# @version : 1.0 -# @Create Time : 2022/10/19 15:41 -# @File : __init__.py.py -# @IDE : PyCharm -# @desc : 简要说明 diff --git a/apps/vadmin/workplace/views.py b/apps/vadmin/workplace/views.py deleted file mode 100644 index cb77822..0000000 --- a/apps/vadmin/workplace/views.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# @version : 1.0 -# @Create Time : 2022/10/19 15:41 -# @File : views.py -# @IDE : PyCharm -# @desc : 简要说明 - -from fastapi import APIRouter, Depends -from apps.vadmin.auth.utils.current import AllUserAuth -from apps.vadmin.auth.utils.validation.auth import Auth -from utils.response import SuccessResponse -import datetime -from apps.vadmin.record.crud import LoginRecordDal - -app = APIRouter() - - -########################################################### -# 工作区管理 -########################################################### -@app.get("/project", summary="获取项目") -async def get_project(): - data = [ - { - "name": 'Mysql', - "icon": 'vscode-icons:file-type-mysql', - "message": '最流行的关系型数据库管理系统', - "personal": 'kinit', - "link": "https://www.mysql.com/", - "time": datetime.datetime.now().strftime("%Y-%m-%d") - }, - { - "name": 'FastAPI', - "icon": 'simple-icons:fastapi', - "message": '一个现代、快速(高性能)的 web 框架', - "personal": 'kinit', - "link": "https://fastapi.tiangolo.com/zh/", - "time": datetime.datetime.now().strftime("%Y-%m-%d") - }, - { - "name": 'Vue', - "icon": 'logos:vue', - "message": '渐进式 JavaScript 框架', - "personal": 'kinit', - "link": "https://cn.vuejs.org/", - "time": datetime.datetime.now().strftime("%Y-%m-%d") - }, - { - "name": 'Element-plus', - "icon": 'logos:element', - "message": '面向设计师和开发者的组件库', - "personal": 'kinit', - "link": "https://element-plus.org/zh-CN/", - "time": datetime.datetime.now().strftime("%Y-%m-%d") - }, - { - "name": 'Typescript', - "icon": 'vscode-icons:file-type-typescript-official', - "message": 'TypeScript是JavaScript类型的超集', - "personal": 'kinit', - "link": "https://www.typescriptlang.org/", - "time": datetime.datetime.now().strftime("%Y-%m-%d") - }, - { - "name": 'Vite', - "icon": 'vscode-icons:file-type-vite', - "message": 'Vite 下一代的前端工具链', - "personal": 'kinit', - "link": "https://cn.vitejs.dev/", - "time": datetime.datetime.now().strftime("%Y-%m-%d") - } - ] - return SuccessResponse(data) - - -@app.get("/dynamic", summary="获取动态") -async def get_dynamic(): - data = [ - { - "keys": ['workplace.push', 'Github'], - "time": datetime.datetime.now().strftime("%Y-%m-%d") - }, - { - "keys": ['workplace.push', 'Github'], - "time": datetime.datetime.now().strftime("%Y-%m-%d") - } - ] - return SuccessResponse(data) - - -@app.get("/team", summary="获取团队信息") -async def get_team(): - data = [ - { - "name": 'Mysql', - "icon": 'vscode-icons:file-type-mysql' - }, - { - "name": 'Vue', - "icon": 'logos:vue' - }, - { - "name": 'Element-plus', - "icon": 'logos:element' - }, - { - "name": 'Fastapi', - "icon": 'simple-icons:fastapi' - }, - { - "name": 'Typescript', - "icon": 'vscode-icons:file-type-typescript-official' - }, - { - "name": 'Vite', - "icon": 'vscode-icons:file-type-vite' - } - ] - return SuccessResponse(data) - - -@app.get("/shortcuts", summary="获取快捷操作") -async def get_shortcuts(): - data = [ - { - "name": "Gitee 项目仓库", - "link": "https://gitee.com/ktianc/kinit" - }, - { - "name": "GitHub 项目仓库", - "link": "https://github.com/vvandk/kinit" - }, - { - "name": "前端文档", - "link": "https://element-plus-admin-doc.cn/" - }, - { - "name": "Swagger UI 接口文档", - "link": "http://kinit.ktianc.top/api/docs" - }, - { - "name": "Redoc 接口文档", - "link": "http://kinit.ktianc.top/api/redoc" - }, - { - "name": "UnoCSS 中文文档", - "link": "https://unocss.nodejs.cn/guide/" - }, - { - "name": "Iconify 文档", - "link": "https://icon-sets.iconify.design/" - }, - { - "name": "echarts 文档", - "link": "https://echarts.apache.org/zh/index.html" - }, - ] - return SuccessResponse(data) diff --git a/core/event.py b/core/event.py index f43227b..df138be 100644 --- a/core/event.py +++ b/core/event.py @@ -10,13 +10,10 @@ from fastapi import FastAPI from motor.motor_asyncio import AsyncIOMotorClient from application.settings import REDIS_DB_URL, MONGO_DB_URL, MONGO_DB_NAME, EVENTS -from utils.cache import Cache from redis import asyncio as aioredis from redis.exceptions import AuthenticationError, TimeoutError, RedisError from contextlib import asynccontextmanager from utils.tools import import_modules_async -from sqlalchemy.exc import ProgrammingError -from core.logger import logger @asynccontextmanager @@ -79,11 +76,6 @@ async def connect_redis(app: FastAPI, status: bool): raise TimeoutError(f"Redis 连接超时,地址或者端口错误: {e}") except RedisError as e: raise RedisError(f"Redis 连接失败: {e}") - try: - await Cache(app.state.redis).cache_tab_names() - except ProgrammingError as e: - logger.error(f"sqlalchemy.exc.ProgrammingError: {e}") - print(f"sqlalchemy.exc.ProgrammingError: {e}") else: print("Redis 连接关闭") await app.state.redis.close() diff --git a/requirements.txt b/requirements.txt index fdd680ed4b8018a7139a8af644b83e1d7ec2cef7..bf2768ccf8b2b3c7752aec89944f85205d4103f5 100644 GIT binary patch delta 60 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 delta 41991 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

2b=aEMF0SK!~aER zkO>L-zoYX%Im*UAm=O3c>`z#OT4kV9jqaZmfU60LkwYT`6obYHZ1lgae9hnw=EDCe z{QXxU9XAh$g`Km8G7Ky`w9AM9=wt#=1O7j-Kqn#2-}OLW)wh7>&1uR1{~iey*2Df6 z!2K0WU?Ik?^9=y7a diff --git a/utils/cache.py b/utils/cache.py deleted file mode 100644 index 385136f..0000000 --- a/utils/cache.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# @version : 1.0 -# @Create Time : 2022/3/21 11:03 -# @File : cache.py -# @IDE : PyCharm -# @desc : 缓存 - -from typing import List - -from sqlalchemy import false -from sqlalchemy.future import select -from sqlalchemy.orm import joinedload -from core.logger import logger # 注意:报错就在这里,如果只写 core.logger 会写入日志报错,很难排查 -from core.database import db_getter -from apps.vadmin.system.models import VadminSystemSettingsTab -import json -from redis.asyncio.client import Redis -from core.exception import CustomException -from utils import status - - -class Cache: - - DEFAULT_TAB_NAMES = ["wx_server", "aliyun_sms", "aliyun_oss", "web_email"] - - def __init__(self, rd: Redis): - self.rd = rd - - async def __get_tab_name_values(self, tab_names: List[str]): - """ - 获取系统配置标签下的标签信息 - """ - async_session = db_getter() - session = await async_session.__anext__() - model = VadminSystemSettingsTab - v_options = [joinedload(model.settings)] - sql = select(model).where( - model.is_delete == false(), - model.tab_name.in_(tab_names), - model.disabled == false() - ).options(*[load for load in v_options]) - queryset = await session.execute(sql) - datas = queryset.scalars().unique().all() - return self.__generate_values(datas) - - @classmethod - def __generate_values(cls, datas: List[VadminSystemSettingsTab]): - """ - 生成字典值 - """ - return { - tab.tab_name: { - item.config_key: item.config_value - for item in tab.settings - if not item.disabled - } - for tab in datas - } - - async def cache_tab_names(self, tab_names: List[str] = None): - """ - 缓存系统配置 - 如果手动修改了mysql数据库中的配置 - 那么需要在redis中将对应的tab_name删除 - """ - - if not tab_names: - tab_names = self.DEFAULT_TAB_NAMES - datas = await self.__get_tab_name_values(tab_names) - - for k, v in datas.items(): - await self.rd.client().set(k, json.dumps(v)) - - async def get_tab_name(self, tab_name: str, retry: int = 3): - """ - 获取系统配置 - :param tab_name: 配置表标签名称 - :param retry: 重试次数 - """ - result = await self.rd.get(tab_name) - if not result and retry > 0: - logger.error(f"未从Redis中获取到{tab_name}配置信息,正在重新更新配置信息,重试次数:{retry}。") - await self.cache_tab_names([tab_name]) - return await self.get_tab_name(tab_name, retry - 1) - elif not result and retry == 0: - raise CustomException(f"获取{tab_name}配置信息失败,请联系管理员!", code=status.HTTP_ERROR) - else: - return json.loads(result) diff --git a/utils/huawei_obs.py b/utils/huawei_obs.py new file mode 100644 index 0000000..2a454fd --- /dev/null +++ b/utils/huawei_obs.py @@ -0,0 +1,59 @@ +from application.settings import HUAWEI_OBS + +from obs import ObsClient +from obs import DeleteObjectsRequest +from obs import Object + + +class ObsClient: + + def __int__(self): + self.obsClient = ObsClient( + access_key_id=HUAWEI_OBS['AccessKeyID'], + secret_access_key=HUAWEI_OBS['SecretAccessKey'], + server=HUAWEI_OBS["server"]) + + async def put_file(self, objectKey: str, file_path: str): + resp = await self.obsClient.putFile( + bucketName=HUAWEI_OBS["bucketName"], + objectKey=objectKey, + file_path=file_path) + if resp.status < 300: + print("objectKey", objectKey) + print("url", resp.body.objectUrl) + return True, objectKey, resp.body.objectUrl + else: + return False, None, None + + async def put_object(self, objectKey: str, file_content): + resp = await self.obsClient.put_object( + bucketName=HUAWEI_OBS["bucketName"], + objectKey=objectKey, + file_cotent=file_content) + if resp.status < 300: + print("objectKey", objectKey) + print("url", resp.body.objectUrl) + return True, objectKey, resp.body.objectUrl + else: + return False, None, None + + async def del_objects(self, object_keys: []): + objects = [] + for object_key in object_keys: + object_one = Object(key=object_key, versionId=None) + objects.append(object_one) + encoding_type = 'url' + + resp = await self.obsClient.deleteObjects( + bucketName=HUAWEI_OBS["bucketName"], + deleteObjectsRequest=DeleteObjectsRequest(quiet=False, objects=objects, encoding_type=encoding_type)) + if resp.status < 300: + return True + else: + return False + + + async def close(self): + await self.obsClient.close() + + diff --git a/utils/send_email.py b/utils/send_email.py index 1923409..3b59b19 100644 --- a/utils/send_email.py +++ b/utils/send_email.py @@ -15,7 +15,6 @@ from typing import List from redis.asyncio import Redis from core.exception import CustomException -from utils.cache import Cache class EmailSender: @@ -32,7 +31,7 @@ class EmailSender: """ 获取配置信息 """ - web_email = await Cache(self.rd).get_tab_name("web_email", retry) + web_email = [] self.email = web_email.get("email_access") self.password = web_email.get("email_password") self.smtp_server = web_email.get("email_server") @@ -84,10 +83,3 @@ class EmailSender: print('邮件发送失败!错误信息:', e) return False - -# if __name__ == '__main__': -# sender = EmailSender() -# to_emails = ['ktianc2001@163.com', '2445667550@qq.com'] -# subject = 'Test email' -# body = 'This is a test email' -# sender.send_email(to_emails, subject, body) diff --git a/utils/sms/aliyun.py b/utils/sms/aliyun.py index 8354113..aaba80b 100644 --- a/utils/sms/aliyun.py +++ b/utils/sms/aliyun.py @@ -28,7 +28,6 @@ from alibabacloud_tea_util import models as util_models from core.logger import logger import datetime from redis.asyncio.client import Redis -from utils.cache import Cache from utils.db_getter import DBGetter @@ -86,7 +85,7 @@ class AliyunSMS(DBGetter): raise ValueError("缺少 redis 对象参数!") elif not self.sign_conf or not self.template_code_conf: raise ValueError("缺少短信签名信息和短信模板ID!") - aliyun_sms = await Cache(self.rd).get_tab_name("aliyun_sms", retry) + aliyun_sms = [] self.access_key = aliyun_sms.get("sms_access_key") self.access_key_secret = aliyun_sms.get("sms_access_key_secret") self.send_interval = int(aliyun_sms.get("sms_send_interval")) diff --git a/utils/wx/oauth.py b/utils/wx/oauth.py index 3b2a685..62388cb 100644 --- a/utils/wx/oauth.py +++ b/utils/wx/oauth.py @@ -8,7 +8,6 @@ import requests from core.logger import logger -from utils.cache import Cache from utils.wx.wx_access_token import WxAccessToken from redis.asyncio import Redis @@ -35,7 +34,7 @@ class WXOAuth: """ if not self.tab_name: logger.error(f"请选择认证的微信平台") - wx_config = await Cache(self.rd).get_tab_name(self.tab_name, retry) + wx_config = [] self.appid = wx_config.get("wx_server_app_id") self.secret = wx_config.get("wx_server_app_secret")