From 2f28d6d5e060b7051b807cb307b8397208624d4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9A=93=E6=9C=88=E5=BD=92=E5=B0=98?= Date: Wed, 12 Feb 2025 23:25:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=EF=BC=8C=E6=9B=B4=E6=96=B0=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E7=A0=81=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annotation/log.py | 19 +++--- api/config.py | 143 ++++++++++++++++++++++++++++++++++++++++++++ api/file.py | 4 +- api/i18n.py | 3 +- api/login.py | 41 +++++++++---- app.py | 6 +- config/constant.py | 2 + config/env.py | 1 + config/get_redis.py | 17 ++++++ models/__init__.py | 6 +- models/config.py | 70 ++++++++++++++++++++++ models/role.py | 2 +- schemas/config.py | 70 ++++++++++++++++++++++ schemas/login.py | 10 ++-- utils/mail.py | 3 +- 15 files changed, 362 insertions(+), 35 deletions(-) create mode 100644 api/config.py create mode 100644 models/config.py create mode 100644 schemas/config.py diff --git a/annotation/log.py b/annotation/log.py index cec3355..f69cebb 100644 --- a/annotation/log.py +++ b/annotation/log.py @@ -6,20 +6,20 @@ # @Software : PyCharm # @Comment : 本程序日志装饰器定义 -import json -import time -from datetime import datetime -from functools import wraps, lru_cache -from async_lru import alru_cache -from typing import Optional, Literal -import urllib import hashlib import ipaddress +import json +import time +import urllib +from functools import wraps +from typing import Optional, Literal +from async_lru import alru_cache from fastapi import Request from fastapi.responses import ORJSONResponse, UJSONResponse, JSONResponse -from user_agents import parse from httpx import AsyncClient +from user_agents import parse + from config.constant import BusinessType from config.env import AppConfig, MapConfig from controller.login import LoginController @@ -226,7 +226,7 @@ async def get_ip_location(ip: str) -> str: # 将sn参数添加到请求中 queryStr = queryStr + "&sn=" + sn url = host + queryStr - async with AsyncClient(headers=headers,timeout=60) as client: + async with AsyncClient(headers=headers, timeout=60) as client: response = await client.get(url) if response.status_code == 200: result = response.json() @@ -239,4 +239,3 @@ async def get_ip_location(ip: str) -> str: except ValueError: # 如果IP地址格式无效 return "未知地点" - diff --git a/api/config.py b/api/config.py new file mode 100644 index 0000000..66f2f2f --- /dev/null +++ b/api/config.py @@ -0,0 +1,143 @@ +# _*_ coding : UTF-8 _*_ +# @Time : 2025/02/12 16:58 +# @UpdateTime : 2025/02/12 16:58 +# @Author : sonder +# @File : config.py +# @Software : PyCharm +# @Comment : 本程序 + +from typing import Optional + +from fastapi import APIRouter, Depends, Path, Request, Query +from fastapi.responses import JSONResponse + +from annotation.log import Log +from config.constant import BusinessType +from config.get_redis import Redis +from controller.login import LoginController +from models import Config +from schemas.common import BaseResponse +from schemas.config import AddConfigParams, DeleteConfigListParams, GetConfigInfoResponse, GetConfigListResponse +from utils.response import Response + +configApi = APIRouter( + prefix="/config", + dependencies=[Depends(LoginController.get_current_user)], +) + + +@configApi.post("/add", response_class=JSONResponse, response_model=BaseResponse, summary="新增配置") +@Log(title="新增配置", business_type=BusinessType.INSERT) +async def add_config(request: Request, params: AddConfigParams): + if await Config.get_or_none(name=params.name, key=params.key): + return Response.error(msg="配置已存在") + config = await Config.create( + name=params.name, + key=params.key, + value=params.value, + remark=params.remark, + type=params.type, + ) + if config: + await Redis.init_system_config(request.app) + return Response.success(msg="新增成功") + else: + return Response.error(msg="新增失败") + + +@configApi.delete("/delete/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="删除配置") +@configApi.post("/delete/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="删除配置") +@Log(title="删除配置", business_type=BusinessType.DELETE) +async def delete_config(request: Request, id: str = Path(description="配置ID")): + if config := await Config.get_or_none(id=id): + await config.delete() + await Redis.init_system_config(request.app) + return Response.success(msg="删除成功") + else: + return Response.error(msg="配置不存在") + + +@configApi.delete("/deleteList", response_class=JSONResponse, response_model=BaseResponse, summary="批量删除配置") +@configApi.post("/deleteList", response_class=JSONResponse, response_model=BaseResponse, summary="批量删除配置") +@Log(title="批量删除配置", business_type=BusinessType.DELETE) +async def delete_config_list(request: Request, params: DeleteConfigListParams): + for id in set(params.ids): + if config := await Config.get_or_none(id=id): + await config.delete() + await Redis.init_system_config(request.app) + return Response.success(msg="删除成功") + + +@configApi.put("/update/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="修改配置") +@configApi.post("/update/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="修改配置") +@Log(title="修改配置", business_type=BusinessType.UPDATE) +async def update_config(request: Request, params: AddConfigParams, id: str = Path(description="配置ID")): + if config := await Config.get_or_none(id=id): + config.name = params.name + config.key = params.key + config.value = params.value + config.remark = params.remark + config.type = params.type + await config.save() + await Redis.init_system_config(request.app) + return Response.success(msg="修改成功") + else: + return Response.error(msg="配置不存在") + + +@configApi.get("/info/{id}", response_class=JSONResponse, response_model=GetConfigInfoResponse, summary="获取配置信息") +@Log(title="获取配置信息", business_type=BusinessType.SELECT) +async def get_config_info(request: Request, id: str = Path(description="配置ID")): + if config := await Config.get_or_none(id=id): + data = { + "id": config.id, + "name": config.name, + "key": config.key, + "value": config.value, + "remark": config.remark, + "type": config.type, + "create_time": config.create_time, + "create_by": config.create_by, + "update_time": config.update_time, + "update_by": config.update_by, + } + return Response.success(data=data) + else: + return Response.error(msg="配置不存在") + + +@configApi.get("/list", response_class=JSONResponse, response_model=GetConfigListResponse, summary="获取配置列表") +@Log(title="获取配置列表", business_type=BusinessType.SELECT) +async def get_config_list(request: Request, + page: int = Query(default=1, description="当前页码"), + pageSize: int = Query(default=10, description="每页数量"), + key: Optional[str] = Query(default=None, description="配置键名"), + name: Optional[str] = Query(default=None, description="配置名称"), + type: Optional[str] = Query(default=None, description="系统内置"), + ): + filterArgs = { + f'{k}__contains': v for k, v in { + 'name': name, + 'key': key, + 'type': type, + }.items() if v + } + total = await Config.filter(**filterArgs).count() + data = await Config.filter(**filterArgs).offset((page - 1) * pageSize).limit(pageSize).values( + id="id", + name="name", + key="key", + value="value", + remark="remark", + type="type", + create_time="create_time", + create_by="create_by", + update_time="update_time", + update_by="update_by", + ) + return Response.success(data={ + "total": total, + "result": data, + "page": page, + "pageSize": pageSize, + }) diff --git a/api/file.py b/api/file.py index 07d2dac..37ff0df 100644 --- a/api/file.py +++ b/api/file.py @@ -137,7 +137,7 @@ async def get_file_info( async def delete_file( request: Request, id: str = Path(..., description="文件ID"), -current_user: dict = Depends(LoginController.get_current_user),): + current_user: dict = Depends(LoginController.get_current_user), ): # 1. 查询文件记录 file_record = await FileModel.get_or_none(id=id) if not file_record: @@ -161,7 +161,7 @@ async def get_file_list( uploader_nickname: str = Query(default=None, description="上传者昵称"), department_id: str = Query(default=None, description="上传者部门ID"), department_name: str = Query(default=None, description="上传者部门名称"), -current_user: dict = Depends(LoginController.get_current_user),): + current_user: dict = Depends(LoginController.get_current_user), ): # 1. 查询文件记录 filterArgs = { f'{k}__contains': v for k, v in { diff --git a/api/i18n.py b/api/i18n.py index e01e5a7..bd2e2c1 100644 --- a/api/i18n.py +++ b/api/i18n.py @@ -15,11 +15,11 @@ from fastapi.responses import JSONResponse from annotation.log import Log from config.constant import BusinessType, RedisKeyConfig from controller.login import LoginController +from models import I18n, Locale from schemas.common import BaseResponse from schemas.i18n import AddLocaleParams, GetLocaleInfoResponse, AddI18nParams, GetI18nInfoResponse, \ GetI18nInfoListResponse, GetI18nListResponse from utils.response import Response -from models import I18n, Locale i18nAPI = APIRouter( prefix="/i18n", @@ -263,4 +263,3 @@ async def get_i18n_info_list(request: Request, id: str = Path(description="国 "name": locale.name, }) return Response.error(msg="该国际化内容语言不存在!") - diff --git a/api/login.py b/api/login.py index d959599..eedf67a 100644 --- a/api/login.py +++ b/api/login.py @@ -124,19 +124,38 @@ async def register(request: Request, params: RegisterUserParams): @loginAPI.get("/captcha", response_class=JSONResponse, response_model=GetCaptchaResponse, summary="获取验证码") async def get_captcha(request: Request): - captcha_result = await Captcha.create_captcha("1") - session_id = str(uuid.uuid4()) - captcha = captcha_result[0] - result = captcha_result[-1] - await request.app.state.redis.set( - f'{RedisKeyConfig.CAPTCHA_CODES.key}:{session_id}', result, ex=timedelta(minutes=2) + captcha_enabled = ( + True + if await request.app.state.redis.get(f'{RedisKeyConfig.SYSTEM_CONFIG.key}:account_captcha_enabled') + == 'true' + else False ) - logger.info(f'编号为{session_id}的会话获取图片验证码成功') + if captcha_enabled: + captcha_type = ( + await request.app.state.redis.get(f'{RedisKeyConfig.SYSTEM_CONFIG.key}:account_captcha_type') + if await request.app.state.redis.get(f'{RedisKeyConfig.SYSTEM_CONFIG.key}:account_captcha_type') + else "1" + ) + captcha_result = await Captcha.create_captcha(captcha_type) + session_id = str(uuid.uuid4()) + captcha = captcha_result[0] + result = captcha_result[-1] + await request.app.state.redis.set( + f'{RedisKeyConfig.CAPTCHA_CODES.key}:{session_id}', result, ex=timedelta(minutes=2) + ) + logger.info(f'编号为{session_id}的会话获取图片验证码成功') - return Response.success(data={ - "uuid": session_id, - "captcha": captcha, - }) + return Response.success(data={ + "uuid": session_id, + "captcha": captcha, + "captcha_enabled": captcha_enabled, + }) + else: + return Response.success(data={ + "uuid": None, + "captcha": None, + "captcha_enabled": captcha_enabled, + }) @loginAPI.post("/code", response_class=JSONResponse, response_model=BaseResponse, summary="获取邮件验证码") diff --git a/app.py b/app.py index 88655f5..04bde64 100644 --- a/app.py +++ b/app.py @@ -13,15 +13,16 @@ from fastapi import FastAPI from fastapi.openapi.utils import get_openapi from api.cache import cacheAPI +from api.config import configApi from api.department import departmentAPI from api.file import fileAPI +from api.i18n import i18nAPI from api.log import logAPI from api.login import loginAPI from api.permission import permissionAPI from api.role import roleAPI from api.server import serverAPI from api.user import userAPI -from api.i18n import i18nAPI from config.database import init_db, close_db from config.env import AppConfig from config.get_redis import Redis @@ -36,6 +37,7 @@ async def lifespan(app: FastAPI): app.state.redis = await Redis.create_redis_pool() logger.info(f'{AppConfig.app_name}启动成功') await init_db() + await Redis.init_system_config(app) yield await close_db() await Redis.close_redis_pool(app) @@ -84,7 +86,7 @@ api_list = [ {'api': cacheAPI, 'tags': ['缓存管理']}, {'api': serverAPI, 'tags': ['服务器管理']}, {'api': i18nAPI, 'tags': ['国际化管理']}, - + {'api': configApi, 'tags': ['配置管理']}, ] for api in api_list: diff --git a/config/constant.py b/config/constant.py index d981860..441ebb4 100644 --- a/config/constant.py +++ b/config/constant.py @@ -263,3 +263,5 @@ class RedisKeyConfig(Enum): """用于存储国际化数据。""" TRANSLATION_TYPES = {'key': 'translation_types', 'remark': '国际化类型'} """国际化类型,存储国际化类型及其配置信息。""" + SYSTEM_CONFIG = {'key': 'system_config', 'remark': '系统配置信息'} + """系统配置信息,存储系统的配置信息。""" diff --git a/config/env.py b/config/env.py index 092e084..971b26b 100644 --- a/config/env.py +++ b/config/env.py @@ -473,6 +473,7 @@ class GetConfig: """ # 实例化邮件配置 return EmailSettings() + @lru_cache() def get_map_config(self) -> 'MapSettings': """ diff --git a/config/get_redis.py b/config/get_redis.py index e3e27b3..8d2a803 100644 --- a/config/get_redis.py +++ b/config/get_redis.py @@ -9,7 +9,9 @@ from redis import asyncio as aioredis from redis.exceptions import AuthenticationError, TimeoutError, RedisError +from config.constant import RedisKeyConfig from config.env import RedisConfig +from models import Config from utils.log import logger @@ -66,3 +68,18 @@ class Redis: """ await app.state.redis.close() logger.info('关闭 Redis 连接成功') + + @classmethod + async def init_system_config(cls, app): + """ + 初始化系统配置 + """ + # 获取以sys_config:开头的键列表 + keys = await app.state.redis.keys(f'{RedisKeyConfig.SYSTEM_CONFIG.key}:*') + # 删除匹配的键 + if keys: + await app.state.redis.delete(*keys) + config = await Config.all().values() + for item in config: + await app.state.redis.set(f"{RedisKeyConfig.SYSTEM_CONFIG.key}:{item.get('key')}", + item.get('value'), ) diff --git a/models/__init__.py b/models/__init__.py index 5c53b61..7ecd587 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -6,13 +6,14 @@ # @Software : PyCharm # @Comment : 本程序 +from models.config import Config from models.department import Department, DepartmentRole from models.file import File +from models.i18n import I18n, Locale from models.log import LoginLog, OperationLog from models.permission import Permission from models.role import Role, RolePermission from models.user import User, UserRole -from models.i18n import I18n,Locale __all__ = [ 'Department', @@ -26,5 +27,6 @@ __all__ = [ 'User', 'UserRole', 'I18n', - 'Locale' + 'Locale', + 'Config' ] diff --git a/models/config.py b/models/config.py new file mode 100644 index 0000000..d3f8d12 --- /dev/null +++ b/models/config.py @@ -0,0 +1,70 @@ +# _*_ coding : UTF-8 _*_ +# @Time : 2025/02/12 16:43 +# @UpdateTime : 2025/02/12 16:43 +# @Author : sonder +# @File : config.py +# @Software : PyCharm +# @Comment : 本程序 + +from tortoise import fields + +from models.common import BaseModel + + +class Config(BaseModel): + """ + 系统配置模型 + """ + name = fields.CharField( + max_length=100, + description="配置名称", + source_field="name" + ) + """ + 配置名称。 + - 最大长度为 100 个字符 + - 映射到数据库字段 name + """ + key = fields.CharField( + max_length=100, + description="配置键名", + source_field="key" + ) + """ + 配置键名。 + - 最大长度为 100 个字符 + - 映射到数据库字段 key + """ + value = fields.CharField( + max_length=100, + description="配置值", + source_field="value" + ) + """ + 配置值。 + - 最大长度为 100 个字符 + - 映射到数据库字段 value + """ + type = fields.BooleanField( + default=False, + description="系统内置", + source_field="type" + ) + """ + 是否为系统内置 + - 默认为不是 + """ + remark = fields.TextField( + null=True, + description="备注", + source_field="remark" + ) + """ + 备注信息。 + - 最大长度为 255 个字符 + - 可为空 + """ + + class Meta: + table = "sys_config" + table_description = "系统配置表" diff --git a/models/role.py b/models/role.py index e7179f5..2ec7b57 100644 --- a/models/role.py +++ b/models/role.py @@ -54,7 +54,7 @@ class Role(BaseModel): - 映射到数据库字段 role_description。 """ - status=fields.SmallIntField( + status = fields.SmallIntField( default=1, description="角色状态", source_field="status" diff --git a/schemas/config.py b/schemas/config.py new file mode 100644 index 0000000..96698fa --- /dev/null +++ b/schemas/config.py @@ -0,0 +1,70 @@ +# _*_ coding : UTF-8 _*_ +# @Time : 2025/02/12 17:03 +# @UpdateTime : 2025/02/12 17:03 +# @Author : sonder +# @File : config.py +# @Software : PyCharm +# @Comment : 本程序 +from datetime import datetime +from typing import Optional, List + +from pydantic import BaseModel, Field, ConfigDict +from pydantic.alias_generators import to_camel + +from schemas.common import BaseResponse, ListQueryResult + + +class ConfigInfo(BaseModel): + """ + 配置信息模型 + """ + model_config = ConfigDict(alias_generator=to_camel) + id: str = Field(..., description="主键") + create_by: str = Field(default="", description="创建者") + create_time: Optional[datetime] = Field(default=None, description="创建时间") + update_by: str = Field(default="", description="更新者") + update_time: Optional[datetime] = Field(default=None, description="更新时间") + name: str = Field(default="", description="配置名称") + key: str = Field(default="", description="配置键名") + value: str = Field(default="", description="配置值") + type: bool = Field(default=False, description="系统内置") + remark: str = Field(default="", description="备注") + + +class AddConfigParams(BaseModel): + """ + 添加配置参数模型 + """ + name: str = Field(..., max_length=100, description="配置名称") + key: str = Field(..., max_length=100, description="配置键名") + value: str = Field(..., max_length=100, description="配置值") + type: bool = Field(default=False, description="系统内置") + remark: Optional[str] = Field(default=None, max_length=255, description="备注信息") + + +class DeleteConfigListParams(BaseModel): + """ + 批量删除配置参数模型 + """ + ids: List[str] = Field(default=[], description="配置ID") + + +class GetConfigInfoResponse(BaseResponse): + """ + 获取配置模型信息响应 + """ + data: ConfigInfo = Field(default=None, description="响应数据") + + +class GetConfigInfoResult(ListQueryResult): + """ + 获取配置模型信息结果 + """ + result: List[ConfigInfo] = Field(default=[], description="列表数据") + + +class GetConfigListResponse(BaseResponse): + """ + 获取配置列表响应 + """ + data: GetConfigInfoResult = Field(default=None, description="响应数据") diff --git a/schemas/login.py b/schemas/login.py index b6a3491..ab7be55 100644 --- a/schemas/login.py +++ b/schemas/login.py @@ -114,14 +114,16 @@ class GetCaptchaResult(BaseModel): """ 获取验证码结果模型 """ - uuid: str = Field(default="", description="验证码UUID") - captcha: str = Field(default="", description="验证码图片") + uuid: Optional[str] = Field(default=None, description="验证码UUID") + captcha: Optional[str] = Field(default=None, description="验证码图片") + captcha_enabled: Optional[bool] = Field(default=False, description="是否开启验证码") class Config: json_schema_extra = { "example": { "uuid": "1234567890", - "captcha": "base64编码的图片" + "captcha": "base64编码的图片", + "captcha_enabled": True } } @@ -130,7 +132,7 @@ class GetEmailCodeParams(BaseModel): """ 获取邮箱验证码请求模型 """ - username:str=Field(default="", description="用户名") + username: str = Field(default="", description="用户名") title: str = Field(default="注册", description="邮件类型") mail: str = Field(default="", description="邮箱地址") diff --git a/utils/mail.py b/utils/mail.py index c567a3b..4b6e0c0 100644 --- a/utils/mail.py +++ b/utils/mail.py @@ -71,7 +71,8 @@ class Email: username=EmailConfig.email_username, password=EmailConfig.email_password ) - await request.app.state.redis.set(f"{RedisKeyConfig.EMAIL_CODES.key}:{mail}-{username}", code, ex=timedelta(minutes=2)) + await request.app.state.redis.set(f"{RedisKeyConfig.EMAIL_CODES.key}:{mail}-{username}", code, + ex=timedelta(minutes=2)) logger.info(f"发送邮件至{mail}成功,验证码:{code}") return True except Exception as e: