feat: 添加系统配置,更新验证码接口

This commit is contained in:
2025-02-12 23:25:19 +08:00
parent 46e9e79670
commit 2f28d6d5e0
15 changed files with 362 additions and 35 deletions

View File

@@ -6,20 +6,20 @@
# @Software : PyCharm # @Software : PyCharm
# @Comment : 本程序日志装饰器定义 # @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 hashlib
import ipaddress 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 import Request
from fastapi.responses import ORJSONResponse, UJSONResponse, JSONResponse from fastapi.responses import ORJSONResponse, UJSONResponse, JSONResponse
from user_agents import parse
from httpx import AsyncClient from httpx import AsyncClient
from user_agents import parse
from config.constant import BusinessType from config.constant import BusinessType
from config.env import AppConfig, MapConfig from config.env import AppConfig, MapConfig
from controller.login import LoginController from controller.login import LoginController
@@ -226,7 +226,7 @@ async def get_ip_location(ip: str) -> str:
# 将sn参数添加到请求中 # 将sn参数添加到请求中
queryStr = queryStr + "&sn=" + sn queryStr = queryStr + "&sn=" + sn
url = host + queryStr 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) response = await client.get(url)
if response.status_code == 200: if response.status_code == 200:
result = response.json() result = response.json()
@@ -239,4 +239,3 @@ async def get_ip_location(ip: str) -> str:
except ValueError: except ValueError:
# 如果IP地址格式无效 # 如果IP地址格式无效
return "未知地点" return "未知地点"

143
api/config.py Normal file
View File

@@ -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,
})

View File

@@ -137,7 +137,7 @@ async def get_file_info(
async def delete_file( async def delete_file(
request: Request, request: Request,
id: str = Path(..., description="文件ID"), id: str = Path(..., description="文件ID"),
current_user: dict = Depends(LoginController.get_current_user),): current_user: dict = Depends(LoginController.get_current_user), ):
# 1. 查询文件记录 # 1. 查询文件记录
file_record = await FileModel.get_or_none(id=id) file_record = await FileModel.get_or_none(id=id)
if not file_record: if not file_record:
@@ -161,7 +161,7 @@ async def get_file_list(
uploader_nickname: str = Query(default=None, description="上传者昵称"), uploader_nickname: str = Query(default=None, description="上传者昵称"),
department_id: str = Query(default=None, description="上传者部门ID"), department_id: str = Query(default=None, description="上传者部门ID"),
department_name: str = Query(default=None, description="上传者部门名称"), department_name: str = Query(default=None, description="上传者部门名称"),
current_user: dict = Depends(LoginController.get_current_user),): current_user: dict = Depends(LoginController.get_current_user), ):
# 1. 查询文件记录 # 1. 查询文件记录
filterArgs = { filterArgs = {
f'{k}__contains': v for k, v in { f'{k}__contains': v for k, v in {

View File

@@ -15,11 +15,11 @@ from fastapi.responses import JSONResponse
from annotation.log import Log from annotation.log import Log
from config.constant import BusinessType, RedisKeyConfig from config.constant import BusinessType, RedisKeyConfig
from controller.login import LoginController from controller.login import LoginController
from models import I18n, Locale
from schemas.common import BaseResponse from schemas.common import BaseResponse
from schemas.i18n import AddLocaleParams, GetLocaleInfoResponse, AddI18nParams, GetI18nInfoResponse, \ from schemas.i18n import AddLocaleParams, GetLocaleInfoResponse, AddI18nParams, GetI18nInfoResponse, \
GetI18nInfoListResponse, GetI18nListResponse GetI18nInfoListResponse, GetI18nListResponse
from utils.response import Response from utils.response import Response
from models import I18n, Locale
i18nAPI = APIRouter( i18nAPI = APIRouter(
prefix="/i18n", prefix="/i18n",
@@ -263,4 +263,3 @@ async def get_i18n_info_list(request: Request, id: str = Path(description="国
"name": locale.name, "name": locale.name,
}) })
return Response.error(msg="该国际化内容语言不存在!") return Response.error(msg="该国际化内容语言不存在!")

View File

@@ -124,7 +124,19 @@ async def register(request: Request, params: RegisterUserParams):
@loginAPI.get("/captcha", response_class=JSONResponse, response_model=GetCaptchaResponse, summary="获取验证码") @loginAPI.get("/captcha", response_class=JSONResponse, response_model=GetCaptchaResponse, summary="获取验证码")
async def get_captcha(request: Request): async def get_captcha(request: Request):
captcha_result = await Captcha.create_captcha("1") captcha_enabled = (
True
if await request.app.state.redis.get(f'{RedisKeyConfig.SYSTEM_CONFIG.key}:account_captcha_enabled')
== 'true'
else False
)
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()) session_id = str(uuid.uuid4())
captcha = captcha_result[0] captcha = captcha_result[0]
result = captcha_result[-1] result = captcha_result[-1]
@@ -136,6 +148,13 @@ async def get_captcha(request: Request):
return Response.success(data={ return Response.success(data={
"uuid": session_id, "uuid": session_id,
"captcha": captcha, "captcha": captcha,
"captcha_enabled": captcha_enabled,
})
else:
return Response.success(data={
"uuid": None,
"captcha": None,
"captcha_enabled": captcha_enabled,
}) })

6
app.py
View File

@@ -13,15 +13,16 @@ from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi from fastapi.openapi.utils import get_openapi
from api.cache import cacheAPI from api.cache import cacheAPI
from api.config import configApi
from api.department import departmentAPI from api.department import departmentAPI
from api.file import fileAPI from api.file import fileAPI
from api.i18n import i18nAPI
from api.log import logAPI from api.log import logAPI
from api.login import loginAPI from api.login import loginAPI
from api.permission import permissionAPI from api.permission import permissionAPI
from api.role import roleAPI from api.role import roleAPI
from api.server import serverAPI from api.server import serverAPI
from api.user import userAPI from api.user import userAPI
from api.i18n import i18nAPI
from config.database import init_db, close_db from config.database import init_db, close_db
from config.env import AppConfig from config.env import AppConfig
from config.get_redis import Redis from config.get_redis import Redis
@@ -36,6 +37,7 @@ async def lifespan(app: FastAPI):
app.state.redis = await Redis.create_redis_pool() app.state.redis = await Redis.create_redis_pool()
logger.info(f'{AppConfig.app_name}启动成功') logger.info(f'{AppConfig.app_name}启动成功')
await init_db() await init_db()
await Redis.init_system_config(app)
yield yield
await close_db() await close_db()
await Redis.close_redis_pool(app) await Redis.close_redis_pool(app)
@@ -84,7 +86,7 @@ api_list = [
{'api': cacheAPI, 'tags': ['缓存管理']}, {'api': cacheAPI, 'tags': ['缓存管理']},
{'api': serverAPI, 'tags': ['服务器管理']}, {'api': serverAPI, 'tags': ['服务器管理']},
{'api': i18nAPI, 'tags': ['国际化管理']}, {'api': i18nAPI, 'tags': ['国际化管理']},
{'api': configApi, 'tags': ['配置管理']},
] ]
for api in api_list: for api in api_list:

View File

@@ -263,3 +263,5 @@ class RedisKeyConfig(Enum):
"""用于存储国际化数据。""" """用于存储国际化数据。"""
TRANSLATION_TYPES = {'key': 'translation_types', 'remark': '国际化类型'} TRANSLATION_TYPES = {'key': 'translation_types', 'remark': '国际化类型'}
"""国际化类型,存储国际化类型及其配置信息。""" """国际化类型,存储国际化类型及其配置信息。"""
SYSTEM_CONFIG = {'key': 'system_config', 'remark': '系统配置信息'}
"""系统配置信息,存储系统的配置信息。"""

View File

@@ -473,6 +473,7 @@ class GetConfig:
""" """
# 实例化邮件配置 # 实例化邮件配置
return EmailSettings() return EmailSettings()
@lru_cache() @lru_cache()
def get_map_config(self) -> 'MapSettings': def get_map_config(self) -> 'MapSettings':
""" """

View File

@@ -9,7 +9,9 @@
from redis import asyncio as aioredis from redis import asyncio as aioredis
from redis.exceptions import AuthenticationError, TimeoutError, RedisError from redis.exceptions import AuthenticationError, TimeoutError, RedisError
from config.constant import RedisKeyConfig
from config.env import RedisConfig from config.env import RedisConfig
from models import Config
from utils.log import logger from utils.log import logger
@@ -66,3 +68,18 @@ class Redis:
""" """
await app.state.redis.close() await app.state.redis.close()
logger.info('关闭 Redis 连接成功') 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'), )

View File

@@ -6,13 +6,14 @@
# @Software : PyCharm # @Software : PyCharm
# @Comment : 本程序 # @Comment : 本程序
from models.config import Config
from models.department import Department, DepartmentRole from models.department import Department, DepartmentRole
from models.file import File from models.file import File
from models.i18n import I18n, Locale
from models.log import LoginLog, OperationLog from models.log import LoginLog, OperationLog
from models.permission import Permission from models.permission import Permission
from models.role import Role, RolePermission from models.role import Role, RolePermission
from models.user import User, UserRole from models.user import User, UserRole
from models.i18n import I18n,Locale
__all__ = [ __all__ = [
'Department', 'Department',
@@ -26,5 +27,6 @@ __all__ = [
'User', 'User',
'UserRole', 'UserRole',
'I18n', 'I18n',
'Locale' 'Locale',
'Config'
] ]

70
models/config.py Normal file
View File

@@ -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 = "系统配置表"

View File

@@ -54,7 +54,7 @@ class Role(BaseModel):
- 映射到数据库字段 role_description。 - 映射到数据库字段 role_description。
""" """
status=fields.SmallIntField( status = fields.SmallIntField(
default=1, default=1,
description="角色状态", description="角色状态",
source_field="status" source_field="status"

70
schemas/config.py Normal file
View File

@@ -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="响应数据")

View File

@@ -114,14 +114,16 @@ class GetCaptchaResult(BaseModel):
""" """
获取验证码结果模型 获取验证码结果模型
""" """
uuid: str = Field(default="", description="验证码UUID") uuid: Optional[str] = Field(default=None, description="验证码UUID")
captcha: str = Field(default="", description="验证码图片") captcha: Optional[str] = Field(default=None, description="验证码图片")
captcha_enabled: Optional[bool] = Field(default=False, description="是否开启验证码")
class Config: class Config:
json_schema_extra = { json_schema_extra = {
"example": { "example": {
"uuid": "1234567890", "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="邮件类型") title: str = Field(default="注册", description="邮件类型")
mail: str = Field(default="", description="邮箱地址") mail: str = Field(default="", description="邮箱地址")

View File

@@ -71,7 +71,8 @@ class Email:
username=EmailConfig.email_username, username=EmailConfig.email_username,
password=EmailConfig.email_password 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}") logger.info(f"发送邮件至{mail}成功,验证码:{code}")
return True return True
except Exception as e: except Exception as e: