feat: 初始化仓库

This commit is contained in:
2025-02-12 02:38:29 +08:00
commit 46e9e79670
67 changed files with 8960 additions and 0 deletions

56
.gitignore vendored Normal file
View File

@@ -0,0 +1,56 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.pyc
*.pyo
*.pyd
# Virtual environment
venv/
env/
.venv/
.venv3/
.Python
*.sqlite3
data/
logs/
# IDE-specific files
.idea/
.vscode/
# Compiled source
*.com
*.class
*.dll
*.exe
*.o
*.so
# Logs and databases
*.log
*.sql
*.sqlite
# Output files
dist/
build/
*.egg-info/
*.egg
# OS-specific files
.DS_Store
Thumbs.db
# Miscellaneous
*.bak
*.swp
*.tmp
*.tmp.*
*.~*
# Jupyter Notebook
.ipynb_checkpoints/
/test/

7
annotation/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/19 00:59
# @UpdateTime : 2025/01/19 00:59
# @Author : sonder
# @File : __init__.py.py
# @Software : PyCharm
# @Comment : 本程序

42
annotation/auth.py Normal file
View File

@@ -0,0 +1,42 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/26 16:01
# @UpdateTime : 2025/01/26 16:01
# @Author : sonder
# @File : auth.py
# @Software : PyCharm
# @Comment : 本程序为权限装饰器定义
from functools import wraps
from fastapi import Request
from controller.login import LoginController
from exceptions.exception import PermissionException
class Auth:
"""
权限装饰器
"""
def __init__(self, permission_list: list):
"""
权限装饰器
:param permission_list: 权限列表
"""
self.permission_list = permission_list
def __call__(self, func):
@wraps(func)
async def wrapper(request: Request, *args, **kwargs):
# 获取上下文信息
token = request.headers.get('Authorization') # 直接使用 request 对象
current_user = await LoginController.get_current_user(request, token)
permissions = current_user.get('permissions')
for permission in set(permissions):
if permission in self.permission_list:
# 如果用户有权限,继续执行接口逻辑
return await func(request, *args, **kwargs)
# 如果用户没有权限,返回错误信息
raise PermissionException(message="该用户无此接口权限!")
return wrapper

242
annotation/log.py Normal file
View File

@@ -0,0 +1,242 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/25 17:32
# @UpdateTime : 2025/01/25 17:32
# @Author : sonder
# @File : log.py
# @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
from fastapi import Request
from fastapi.responses import ORJSONResponse, UJSONResponse, JSONResponse
from user_agents import parse
from httpx import AsyncClient
from config.constant import BusinessType
from config.env import AppConfig, MapConfig
from controller.login import LoginController
from exceptions.exception import LoginException, ServiceWarning, ServiceException, PermissionException
from models import LoginLog, OperationLog, User
from utils.log import logger
from utils.response import Response
class Log:
"""
日志装饰器
"""
def __init__(
self,
title: str,
business_type: BusinessType,
log_type: Optional[Literal['login', 'operation']] = 'operation',
):
"""
日志装饰器
:param title: 当前日志装饰器装饰的模块标题
:param business_type: 业务类型
:param log_type: 日志类型login表示登录日志operation表示为操作日志
:return:
"""
self.title = title
self.business_type = business_type.value
self.log_type = log_type
def __call__(self, func):
@wraps(func)
async def wrapper(request: Request, *args, **kwargs): # 直接接收 request 参数
start_time = time.time()
# 获取上下文信息
token = request.headers.get('Authorization') # 直接使用 request 对象
# 获取请求方法、URL、IP、User-Agent 等信息
request_method = request.method
# 获取请求路径
request_path = request.url.path
# 获取请求IP
host = request.headers.get('X-Forwarded-For') or request.client.host
# 获取请求设备类型
user_agent = request.headers.get('User-Agent', '')
user_agent_info = parse(user_agent)
# 获取请求设备浏览器类型
browser = f'{user_agent_info.browser.family}'
# 获取请求设备操作系统类型
system_os = f'{user_agent_info.os.family}'
if user_agent_info.browser.version != ():
browser += f' {user_agent_info.browser.version[0]}'
if user_agent_info.os.version != ():
system_os += f' {user_agent_info.os.version[0]}'
# 解析 IP 地址的地理位置
location = '内网IP'
if AppConfig.app_ip_location_query: # 假设有一个配置项控制是否查询 IP 地理位置
location = await get_ip_location(host)
# 获取请求参数
content_type = request.headers.get('Content-Type')
if content_type and 'application/x-www-form-urlencoded' in content_type:
payload = await request.form()
request_params = '\n'.join([f'{key}: {value}' for key, value in payload.items()])
elif content_type and 'multipart/form-data' in content_type:
request_params = {}
else:
payload = await request.body()
path_params = request.path_params
request_params = {}
if payload:
request_params.update(json.loads(str(payload, 'utf-8')))
if path_params:
request_params.update(path_params)
request_params = json.dumps(request_params, ensure_ascii=False)
try:
# 调用原始函数
result = await func(request, *args, **kwargs) # 将 request 传递给原始函数
status = 1 # 操作成功
except (LoginException, ServiceWarning) as e:
logger.warning(e.message)
result = Response.failure(data=e.data, msg=e.message)
status = 0 # 操作失败
except ServiceException as e:
logger.error(e.message)
result = Response.error(data=e.data, msg=e.message)
status = 0 # 操作失败
except PermissionException as e:
logger.error(e.message)
result = Response.forbidden(data=e.data, msg=e.message)
status = 0 # 操作失败
except Exception as e:
logger.exception(e)
result = Response.error(msg=str(e))
status = 0 # 操作失败
# 获取操作时间
cost_time = float(time.time() - start_time) * 100
# 判断请求是否来自api文档
request_from_swagger = (
request.headers.get('referer').endswith('docs') if request.headers.get('referer') else False
)
request_from_redoc = (
request.headers.get('referer').endswith('redoc') if request.headers.get('referer') else False
)
# 根据响应结果的类型使用不同的方法获取响应结果参数
if (
isinstance(result, JSONResponse)
or isinstance(result, ORJSONResponse)
or isinstance(result, UJSONResponse)
):
result_dict = json.loads(str(result.body, 'utf-8'))
else:
if request_from_swagger or request_from_redoc:
result_dict = {}
else:
if result.status_code == 200:
result_dict = {'code': result.status_code, 'message': '获取成功'}
else:
result_dict = {'code': result.status_code, 'message': '获取失败'}
json_result = json.dumps(result_dict, ensure_ascii=False)
# 根据日志类型向对应的日志表插入数据
if self.log_type == 'login':
# 登录请求来自于api文档时不记录登录日志其余情况则记录
# if request_from_swagger or request_from_redoc:
# pass
# else:
if status == 1:
session_id = request.app.state.session_id
current_user = await User.get_or_none(username=payload.get("username"))
await LoginLog.create(
user_id=current_user.id,
login_ip=host,
login_location=location,
browser=browser,
os=system_os,
status=status,
session_id=session_id
)
else:
if "image" in request.headers.get("Accept", ""):
pass
else:
current_user = await LoginController.get_current_user(request, token)
await OperationLog.create(
operation_name=self.title,
operation_type=self.business_type,
request_method=request_method,
request_path=request_path,
operator_id=current_user.get("id"),
department_id=current_user.get("department_id"),
department_name=current_user.get("department_name"),
host=host,
location=location,
user_agent=user_agent,
browser=browser,
os=system_os,
request_params=request_params,
response_result=json_result,
status=status,
cost_time=cost_time,
)
# 返回原始函数的结果
return result
return wrapper
@alru_cache()
async def get_ip_location(ip: str) -> str:
"""
根据IP地址获取地理位置
"""
try:
ip_obj = ipaddress.ip_address(ip)
if ip_obj.is_private:
return "内网IP"
else:
# 服务地址
host = "https://api.map.baidu.com"
headers = {
'User-Agent': 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.42'}
# 接口地址
uri = "/location/ip"
params = {
"ip": ip,
"coor": "bd09ll",
"ak": MapConfig.ak,
}
paramsArr = []
for key in params:
paramsArr.append(key + "=" + params[key])
queryStr = uri + "?" + "&".join(paramsArr)
# 对queryStr进行转码safe内的保留字符不转换
encodedStr = urllib.request.quote(queryStr, safe="/:=&?#+!$,;'@()*[]")
# 在最后直接追加上您的SK
rawStr = encodedStr + MapConfig.sk
# 计算sn
sn = hashlib.md5(urllib.parse.quote_plus(rawStr).encode("utf8")).hexdigest()
# 将sn参数添加到请求中
queryStr = queryStr + "&sn=" + sn
url = host + queryStr
async with AsyncClient(headers=headers,timeout=60) as client:
response = await client.get(url)
if response.status_code == 200:
result = response.json()
if result.get("status") == 0:
return result.get("content", {}).get("address", "未知地点")
else:
return "未知地点"
else:
return "未知地点"
except ValueError:
# 如果IP地址格式无效
return "未知地点"

7
api/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/19 00:55
# @UpdateTime : 2025/01/19 00:55
# @Author : sonder
# @File : __init__.py.py
# @Software : PyCharm
# @Comment : 本程序

116
api/cache.py Normal file
View File

@@ -0,0 +1,116 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/02/04 15:18
# @UpdateTime : 2025/02/04 15:18
# @Author : sonder
# @File : cache.py
# @Software : PyCharm
# @Comment : 本程序
from fastapi import APIRouter, Depends, Path, Request
from fastapi.responses import JSONResponse
from annotation.log import Log
from config.constant import BusinessType, RedisKeyConfig
from controller.login import LoginController
from schemas.cache import CacheMonitor, GetCacheInfoResponse, CacheInfo, GetCacheKeysListResponse, \
GetCacheMonitorResponse
from schemas.common import BaseResponse
from utils.response import Response
cacheAPI = APIRouter(
prefix="/cache",
dependencies=[Depends(LoginController.get_current_user)],
)
@cacheAPI.get("/monitor", response_class=JSONResponse, response_model=GetCacheMonitorResponse,
summary="获取缓存监控信息")
@Log(title="获取缓存监控信息", business_type=BusinessType.SELECT)
async def get_cache_info(request: Request):
info = await request.app.state.redis.info()
db_size = await request.app.state.redis.dbsize()
command_stats_dict = await request.app.state.redis.info('commandstats')
command_stats = [
dict(name=key.split('_')[1], value=str(value.get('calls'))) for key, value in command_stats_dict.items()
]
cache_info = CacheMonitor(
info=info,
dbSize=db_size,
commandStats=command_stats
)
return Response.success(data=cache_info)
@cacheAPI.get("/names", response_class=JSONResponse, response_model=GetCacheInfoResponse,
summary="获取缓存名称列表")
@Log(title="获取缓存名称列表", business_type=BusinessType.SELECT)
async def get_cache_names(request: Request):
name_list = []
for key_config in RedisKeyConfig:
name_list.append(
CacheInfo(
cacheKey='',
cacheName=key_config.key,
cacheValue='',
remark=key_config.remark,
)
)
return Response.success(data=name_list)
@cacheAPI.get("/keys/{cacheName}", response_class=JSONResponse, response_model=GetCacheKeysListResponse,
summary="获取缓存键名列表")
@Log(title="获取缓存键名列表", business_type=BusinessType.SELECT)
async def get_cache_keys(request: Request, cacheName: str = Path(description="缓存名称")):
cache_keys = await request.app.state.redis.keys(f'{cacheName}*')
cache_key_list = [key.split(':', 1)[1] for key in cache_keys if key.startswith(f'{cacheName}:')]
return Response.success(data=cache_key_list)
@cacheAPI.get("/info/{cacheName}/{cacheKey}", response_class=JSONResponse, response_model=GetCacheInfoResponse,
summary="获取缓存信息")
@Log(title="获取缓存信息", business_type=BusinessType.SELECT)
async def get_cache_info(request: Request, cacheName: str = Path(description="缓存名称"),
cacheKey: str = Path(description="缓存键名")):
cache_value = await request.app.state.redis.get(f'{cacheName}:{cacheKey}')
cache_info = CacheInfo(
cacheKey=cacheKey,
cacheName=cacheName,
cacheValue=cache_value,
remark="",
)
return Response.success(data=cache_info)
@cacheAPI.delete("/cacheName/{name}", response_class=JSONResponse, response_model=BaseResponse,
summary="通过键名删除缓存")
@cacheAPI.post("/cacheName/{name}", response_class=JSONResponse, response_model=BaseResponse,
summary="通过键名删除缓存")
@Log(title="通过键名删除缓存", business_type=BusinessType.DELETE)
async def delete_cache(request: Request, name: str = Path(description="缓存名称")):
cache_keys = await request.app.state.redis.keys(f'{name}*')
if cache_keys:
await request.app.state.redis.delete(*cache_keys)
return Response.success(msg=f"删除{name}缓存成功!")
@cacheAPI.delete("/cacheKey/{key}", response_class=JSONResponse, response_model=BaseResponse,
summary="通过键值删除缓存")
@cacheAPI.post("/cacheKey/{key}", response_class=JSONResponse, response_model=BaseResponse, summary="通过键值删除缓存")
@Log(title="通过键值删除缓存", business_type=BusinessType.DELETE)
async def delete_cache_key(request: Request, key: str = Path(description="缓存键名")):
cache_keys = await request.app.state.redis.keys(f'*{key}')
if cache_keys:
await request.app.state.redis.delete(*cache_keys)
return Response.success(msg=f"删除{key}缓存成功!")
@cacheAPI.delete("/clearAll", response_class=JSONResponse, response_model=BaseResponse, summary="删除所有缓存")
@cacheAPI.post("/clearAll", response_class=JSONResponse, response_model=BaseResponse, summary="删除所有缓存")
@Log(title="删除所有缓存", business_type=BusinessType.DELETE)
async def delete_all_cache(request: Request):
cache_keys = await request.app.state.redis.keys()
if cache_keys:
await request.app.state.redis.delete(*cache_keys)
return Response.success(msg="删除所有缓存成功!")

390
api/department.py Normal file
View File

@@ -0,0 +1,390 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/20 01:30
# @UpdateTime : 2025/01/20 01:30
# @Author : sonder
# @File : department.py
# @Software : PyCharm
# @Comment : 本程序
from typing import Optional
from fastapi import APIRouter, Depends, Query, Path, Request
from fastapi.responses import JSONResponse
from annotation.log import Log
from config.constant import BusinessType
from controller.login import LoginController
from models import Department, Role, DepartmentRole
from schemas.common import BaseResponse
from schemas.department import AddDepartmentParams, GetDepartmentInfoResponse, \
GetDepartmentListResponse, AddDepartmentRoleParams, GetDepartmentRoleInfoResponse, DeleteDepartmentListParams
from utils.response import Response
departmentAPI = APIRouter(prefix="/department", dependencies=[Depends(LoginController.get_current_user)])
@departmentAPI.post("/add", response_model=BaseResponse, response_class=JSONResponse, summary="新增部门")
@Log(title="新增部门", business_type=BusinessType.INSERT)
async def add_department(request: Request, params: AddDepartmentParams,
current_user: dict = Depends(LoginController.get_current_user)):
parent_id = current_user.get("department_id")
if not params.parent_id:
params.parent_id = parent_id
department = await Department.create(
name=params.name,
parent_id=params.parent_id,
principal=params.principal,
phone=params.phone,
email=params.email,
remark=params.remark,
sort=params.sort,
status=params.status
)
if department:
return Response.success(msg="添加成功!")
else:
return Response.error(msg="添加失败!")
@departmentAPI.delete("/delete/{id}", response_model=BaseResponse, response_class=JSONResponse, summary="删除部门")
@departmentAPI.post("/delete/{id}", response_model=BaseResponse, response_class=JSONResponse, summary="删除部门")
@Log(title="删除部门", business_type=BusinessType.DELETE)
async def delete_department(request: Request, id: str = Path(description="部门ID")):
if department := await Department.get_or_none(id=id, del_flag=1):
if await delete_department_recursive(department_id=department.id):
return Response.success(msg="删除成功!")
return Response.error(msg="删除失败!")
else:
return Response.error(msg="删除失败,部门不存在!")
@departmentAPI.delete("/deleteList", response_model=BaseResponse, response_class=JSONResponse, summary="批量删除部门")
@departmentAPI.post("/deleteList", response_model=BaseResponse, response_class=JSONResponse, summary="批量删除部门")
@Log(title="批量删除部门", business_type=BusinessType.DELETE)
async def delete_department_list(request: Request, params: DeleteDepartmentListParams):
for item in set(params.ids):
if department := await Department.get_or_none(id=item, del_flag=1):
await delete_department_recursive(department_id=department.id)
return Response.success(msg="删除成功!")
async def delete_department_recursive(department_id: str):
"""
递归删除部门及其附属部门
:param department_id: 部门ID
:return:
"""
await Department.filter(id=department_id).delete()
sub_departments = await Department.filter(parentId=department_id).all()
for sub_department in sub_departments:
await delete_department_recursive(sub_department.id)
return True
@departmentAPI.put("/update/{id}", response_model=BaseResponse, response_class=JSONResponse, summary="修改部门")
@departmentAPI.post("/update/{id}", response_model=BaseResponse, response_class=JSONResponse, summary="修改部门")
@Log(title="修改部门", business_type=BusinessType.UPDATE)
async def update_department(request: Request, params: AddDepartmentParams, id: str = Path(description="部门ID")):
if department := await Department.get_or_none(id=id, del_flag=1):
department.name = params.name
department.parent_id = params.parent_id
department.principal = params.principal
department.phone = params.phone
department.email = params.email
department.remark = params.remark
department.sort = params.sort
department.status = params.status
await department.save()
return Response.success(msg="修改成功!")
else:
return Response.error(msg="修改失败,部门不存在!")
@departmentAPI.get("/info/{id}", response_model=GetDepartmentInfoResponse, response_class=JSONResponse,
summary="查询部门详情")
@Log(title="查询部门详情", business_type=BusinessType.SELECT)
async def get_department(request: Request, id: str = Path(description="部门ID")):
if department := await Department.get_or_none(id=id, del_flag=1).values(
id="id",
name="name",
parent_id="parent_id",
principal="principal",
phone="phone",
email="email",
remark="remark",
sort="sort",
status="status",
create_time="create_time",
update_time="update_time",
create_by="create_by",
update_by="update_by"
):
return Response.success(data=department)
else:
return Response.error(msg="部门不存在!")
@departmentAPI.get("/list", response_model=GetDepartmentListResponse, response_class=JSONResponse,
summary="查询部门列表")
@Log(title="查询部门列表", business_type=BusinessType.SELECT)
async def get_department_list(
request: Request,
page: int = Query(default=1, description="当前页码"),
pageSize: int = Query(default=10, description="每页条数"),
name: Optional[str] = Query(default=None, description="部门名称"),
principal: Optional[str] = Query(default=None, description="负责人"),
phone: Optional[str] = Query(default=None, description="电话"),
email: Optional[str] = Query(default=None, description="邮箱"),
remark: Optional[str] = Query(default=None, description="备注"),
sort: Optional[int] = Query(default=None, description="排序权重"),
current_user: dict = Depends(LoginController.get_current_user)
):
filterArgs = {
f'{k}__contains': v for k, v in {
'name': name,
'principal': principal,
'phone': phone,
'email': email,
'remark': remark,
'sort': sort
}.items() if v
}
department_id = current_user.get("department_id", "")
# 递归查询所有部门
all_departments = await get_department_and_subdepartments(department_id, filterArgs)
# 分页处理
total = len(all_departments)
paginated_departments = all_departments[(page - 1) * pageSize: page * pageSize]
return Response.success(data={
"result": paginated_departments,
"total": total,
"page": page
})
async def get_department_and_subdepartments(department_id: str, filterArgs: dict, visited: set = None):
"""
查询当前部门及其所有下属部门的数据,并根据 id 去重。
:param department_id: 当前部门 ID
:param filterArgs: 过滤条件
:param visited: 已访问的部门 ID 集合,用于避免循环依赖
:return: 去重后的部门列表
"""
if visited is None:
visited = set() # 初始化已访问的部门 ID 集合
# 如果当前部门 ID 已经访问过,直接返回空列表,避免死循环
if department_id in visited:
return []
visited.add(department_id) # 标记当前部门 ID 为已访问
# 查询当前部门
current_department = await Department.filter(
id=department_id,
**filterArgs
).values(
id="id",
name="name",
parent_id="parent_id",
principal="principal",
phone="phone",
email="email",
remark="remark",
sort="sort",
status="status",
create_time="create_time",
update_time="update_time",
create_by="create_by",
update_by="update_by"
)
# 查询直接子部门
sub_departments = await Department.filter(
parent_id=department_id, # 只根据 parent_id 查询
**filterArgs
).values(
id="id",
name="name",
parent_id="parent_id",
principal="principal",
phone="phone",
email="email",
remark="remark",
sort="sort",
status="status",
create_time="create_time",
update_time="update_time",
create_by="create_by",
update_by="update_by"
)
# 递归查询子部门的子部门
for department in sub_departments[:]: # 使用切片复制避免修改迭代中的列表
sub_sub_departments = await get_department_and_subdepartments(department["id"], filterArgs, visited)
sub_departments.extend(sub_sub_departments)
# 合并当前部门和所有下属部门的数据
all_departments = current_department + sub_departments
# 根据 id 去重
unique_departments = []
seen_ids = set() # 用于记录已经处理过的部门 ID
for department in all_departments:
if department["id"] not in seen_ids:
unique_departments.append(department)
seen_ids.add(department["id"])
return unique_departments
@departmentAPI.post("/addRole", response_model=BaseResponse, response_class=JSONResponse, summary="添加部门角色")
@Log(title="添加部门角色", business_type=BusinessType.INSERT)
async def add_department_role(request: Request, params: AddDepartmentRoleParams):
if await DepartmentRole.get_or_none(department_id=params.department_id, role_id=params.role_id, del_flag=1):
return Response.error(msg="该部门已存在该角色!")
if department := await Department.get_or_none(id=params.department_id, del_flag=1):
if role := await Role.get_or_none(id=params.role_id, del_flag=1):
departmentRole = await DepartmentRole.create(department_id=department.id, role_id=role.id)
if departmentRole:
return Response.success(msg="添加成功!")
else:
return Response.error(msg="添加失败!")
else:
return Response.error(msg="添加失败,角色不存在!")
else:
return Response.error(msg="添加失败,部门不存在!")
@departmentAPI.delete("/deleteRole/{id}", response_model=BaseResponse, response_class=JSONResponse,
summary="删除部门角色")
@departmentAPI.post("/deleteRole/{id}", response_model=BaseResponse, response_class=JSONResponse,
summary="删除部门角色")
@Log(title="删除部门角色", business_type=BusinessType.DELETE)
async def delete_department_role(request: Request, id: str = Path(description="部门角色ID")):
if departmentRole := await DepartmentRole.get_or_none(id=id, del_flag=1):
await departmentRole.delete()
return Response.success(msg="删除成功!")
else:
return Response.error(msg="删除失败,部门角色不存在!")
@departmentAPI.put("/updateRole/{id}", response_model=BaseResponse, response_class=JSONResponse, summary="修改部门角色")
@departmentAPI.post("/updateRole/{id}", response_model=BaseResponse, response_class=JSONResponse,
summary="修改部门角色")
@Log(title="修改部门角色", business_type=BusinessType.UPDATE)
async def update_department_role(request: Request, params: AddDepartmentRoleParams,
id: str = Path(description="部门角色ID")):
if departmentRole := await DepartmentRole.get_or_none(id=id, del_flag=1):
if department := await Department.get_or_none(id=params.department_id, del_flag=1):
if role := await Role.get_or_none(id=params.role_id, del_flag=1):
departmentRole.department_id = department.id
departmentRole.role_id = role.id
await departmentRole.save()
return Response.success(msg="修改成功!")
else:
return Response.error(msg="修改失败,角色不存在!")
else:
return Response.error(msg="修改失败,部门不存在!")
else:
return Response.error(msg="修改失败,部门角色不存在!")
@departmentAPI.get("/roleInfo", response_model=GetDepartmentRoleInfoResponse, response_class=JSONResponse,
summary="获取部门角色信息")
@Log(title="获取部门角色信息", business_type=BusinessType.SELECT)
async def get_department_role_info(request: Request, id: str = Query(description="部门角色ID")):
if departmentRole := await DepartmentRole.get_or_none(id=id, del_flag=1):
data = await departmentRole.first().values(
id="id",
department_id="department__id",
department_name="department__name",
department_phone="department__phone",
department_principal="department__principal",
department_email="department__email",
role_name="role__name",
role_code="role__code",
role_id="role__id",
create_time="create_time",
update_time="update_time"
)
return Response.success(data=data)
else:
return Response.error(msg="获取失败,部门角色不存在!")
@departmentAPI.get("/roleList", response_model=GetDepartmentListResponse, response_class=JSONResponse,
summary="获取部门角色列表")
@Log(title="获取部门角色列表", business_type=BusinessType.SELECT)
async def get_department_role_list(
request: Request,
page: int = Query(default=1, description="当前页码"),
pageSize: int = Query(default=10, description="每页条数"),
department_id: Optional[str] = Query(default=None, description="部门ID"),
department_name: Optional[str] = Query(default=None, description="部门名称"),
department_phone: Optional[str] = Query(default=None, description="部门电话"),
department_principal: Optional[str] = Query(default=None, description="部门负责人"),
department_email: Optional[str] = Query(default=None, description="部门邮箱"),
role_id: Optional[str] = Query(default=None, description="角色ID"),
role_name: Optional[str] = Query(default=None, description="角色名称"),
role_code: Optional[str] = Query(default=None, description="角色编码"),
):
filterArgs = {
f'{k}__contains': v for k, v in {
'department__id': department_id,
'department__name': department_name,
'department__phone': department_phone,
'department__principal': department_principal,
'department__email': department_email,
'role__id': role_id,
'role__name': role_name,
'role__code': role_code
}.items() if v
}
total = await DepartmentRole.filter(**filterArgs).count()
data = await DepartmentRole.filter(**filterArgs).offset((page - 1) * pageSize).limit(pageSize).values(
id="id",
department_id="department__id",
department_name="department__name",
department_phone="department__phone",
department_principal="department__principal",
department_email="department__email",
role_name="role__name",
role_code="role__code",
role_id="role__id",
create_time="create_time",
update_time="update_time"
)
return Response.success(data={
"result": data,
"total": total,
"page": page
})
@departmentAPI.get("/roleList/{id}", response_model=GetDepartmentListResponse, response_class=JSONResponse,
summary="用户获取部门角色列表")
@Log(title="获取部门角色列表", business_type=BusinessType.OTHER)
async def get_department_role_list(
request: Request,
id: str = Path(..., description="部门ID")
):
data = await Role.filter(department__id=id).values(
id="id",
department_id="department__id",
department_name="department__name",
department_phone="department__phone",
department_principal="department__principal",
department_email="department__email",
role_name="name",
role_code="code",
role_id="id",
create_time="create_time",
update_time="update_time"
)
return Response.success(data={
"result": data,
"total": len(data),
"page": 1
})

198
api/file.py Normal file
View File

@@ -0,0 +1,198 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/25 22:16
# @UpdateTime : 2025/01/25 22:16
# @Author : sonder
# @File : file.py
# @Software : PyCharm
# @Comment : 本程序
import os
from datetime import datetime
from fastapi import APIRouter, UploadFile, File, Path, Depends, Request, Query
from fastapi.responses import FileResponse, JSONResponse
from annotation.log import Log
from config.constant import BusinessType
from config.env import UploadConfig
from controller.login import LoginController
from exceptions.exception import ModelValidatorException, ServiceException
from models import File as FileModel
from schemas.common import BaseResponse
from schemas.file import UploadFileResponse, GetFileInfoResponse, GetFileListResponse
from utils.response import Response
from utils.upload import Upload
fileAPI = APIRouter(
prefix="/file",
)
@fileAPI.post("/upload", response_model=UploadFileResponse, response_class=JSONResponse, summary="上传文件")
@Log(title="上传文件", business_type=BusinessType.INSERT)
async def upload_file(
request: Request,
file: UploadFile = File(..., description="上传的文件"),
current_user: dict = Depends(LoginController.get_current_user),
):
# 1. 检查文件扩展名是否允许
file_extension = os.path.splitext(file.filename)[1][1:].lower() # 获取文件扩展名并转换为小写
if file_extension not in UploadConfig.DEFAULT_ALLOWED_EXTENSION:
raise ModelValidatorException(message="文件类型不支持")
# 2. 生成唯一的文件名
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
unique_filename = f"{current_user.get('id')}_{timestamp}.{file_extension}"
# 3. 保存文件到服务器
file_path = os.path.join(UploadConfig.UPLOAD_PATH, unique_filename)
with open(file_path, "wb") as buffer:
buffer.write(await file.read())
# 4. 构建文件的相对路径和绝对路径
relative_path = os.path.join(UploadConfig.UPLOAD_PREFIX, unique_filename) # 相对路径
absolute_path = os.path.abspath(file_path) # 绝对路径
# 5. 将文件信息保存到数据库
file_record = await FileModel.create(
name=file.filename,
size=os.path.getsize(file_path),
file_type=file.content_type,
absolute_path=absolute_path,
relative_path=relative_path,
uploader=current_user.get("id"),
)
result = await file_record.first().values(
id="id",
name="name",
size="size",
file_type="file_type",
relative_path="relative_path",
absolute_path="absolute_path",
uploader_id="uploader__id",
uploader_username="uploader__username",
uploader_nickname="uploader__nickname",
uploader_department_id="uploader__department__id",
uploader_department_name="uploader__department__name",
create_time="create_time",
update_time="update_time",
)
return Response.success(data=result)
@fileAPI.get("/{id}", summary="下载文件")
@Log(title="获取文件", business_type=BusinessType.SELECT)
async def download_file(
request: Request,
id: str = Path(..., description="文件ID"),
):
# 1. 查询文件记录
file_record = await FileModel.get_or_none(id=id)
if not file_record:
raise ServiceException(message="文件不存在!")
# 2. 检查文件是否存在
if not os.path.exists(file_record.absolute_path):
raise ServiceException(message="文件不存在!")
# 3. 返回文件内容
return FileResponse(
path=file_record.absolute_path,
filename=file_record.name,
media_type=file_record.file_type,
)
@fileAPI.get("/info/{id}", response_class=JSONResponse, response_model=GetFileInfoResponse, summary="获取文件信息")
@Log(title="获取文件信息", business_type=BusinessType.SELECT)
async def get_file_info(
request: Request,
id: str = Path(..., description="文件ID"),
current_user: dict = Depends(LoginController.get_current_user),
):
# 1. 查询文件记录
file_record = await FileModel.get_or_none(id=id)
if not file_record:
raise ServiceException(message="文件不存在!")
result = await file_record.first().values(
id="id",
name="name",
size="size",
file_type="file_type",
relative_path="relative_path",
absolute_path="absolute_path",
uploader_id="uploader__id",
uploader_username="uploader__username",
uploader_nickname="uploader__nickname",
uploader_department_id="uploader__department__id",
uploader_department_name="uploader__department__name",
create_time="create_time",
update_time="update_time",
)
return Response.success(data=result)
@fileAPI.delete("/delete/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="删除文件")
@fileAPI.post("/delete/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="删除文件")
@Log(title="删除文件", business_type=BusinessType.DELETE)
async def delete_file(
request: Request,
id: str = Path(..., description="文件ID"),
current_user: dict = Depends(LoginController.get_current_user),):
# 1. 查询文件记录
file_record = await FileModel.get_or_none(id=id)
if not file_record:
raise ServiceException(message="文件不存在!")
if await Upload.check_file_exists(file_record.absolute_path):
await Upload.delete_file(file_record.absolute_path)
await file_record.delete()
return Response.success()
@fileAPI.get("/list", response_class=JSONResponse, response_model=GetFileListResponse, summary="获取文件列表")
@Log(title="获取文件列表", business_type=BusinessType.SELECT)
async def get_file_list(
request: Request,
page: int = Query(default=1, description="页码"),
pageSize: int = Query(default=10, description="每页数量"),
name: str = Query(default=None, description="文件名"),
file_type: str = Query(default=None, description="文件类型"),
uploader_id: str = Query(default=None, description="上传者ID"),
uploader_username: str = Query(default=None, description="上传者用户名"),
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),):
# 1. 查询文件记录
filterArgs = {
f'{k}__contains': v for k, v in {
'name': name,
'file_type': file_type,
'uploader__id': uploader_id,
'uploader__username': uploader_username,
'uploader__nickname': uploader_nickname,
'uploader__department__id': department_id,
'uploader__department__name': department_name
}.items() if v
}
total = await FileModel.filter(**filterArgs).count()
result = await FileModel.filter(**filterArgs).order_by('-create_time').offset((page - 1) * pageSize).limit(
pageSize).values(
id="id",
name="name",
size="size",
file_type="file_type",
relative_path="relative_path",
absolute_path="absolute_path",
uploader_id="uploader__id",
uploader_username="uploader__username",
uploader_nickname="uploader__nickname",
uploader_department_id="uploader__department__id",
uploader_department_name="uploader__department__name",
create_time="create_time",
update_time="update_time",
)
return Response.success(data={
"total": total,
"result": result,
"page": page,
})

266
api/i18n.py Normal file
View File

@@ -0,0 +1,266 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/02/04 16:05
# @UpdateTime : 2025/02/04 16:05
# @Author : sonder
# @File : i18n.py
# @Software : PyCharm
# @Comment : 本程序
from datetime import timedelta
from typing import Optional
from fastapi import APIRouter, Depends, Path, Request, Query
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from annotation.log import Log
from config.constant import BusinessType, RedisKeyConfig
from controller.login import LoginController
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",
)
@i18nAPI.post("/addLocale", response_class=JSONResponse, response_model=BaseResponse, summary="添加国际化类型")
@Log(title="添加国际化类型", business_type=BusinessType.INSERT)
async def add_locale(request: Request, params: AddLocaleParams, current_user=Depends(LoginController.get_current_user)):
if await Locale.get_or_none(code=params.code):
return Response.error(msg="该语言代码已存在!")
locale = await Locale.create(
code=params.code,
name=params.name
)
if locale:
return Response.success(msg="添加成功!")
else:
return Response.error(msg="添加失败!")
@i18nAPI.delete("/deleteLocale/{id}", response_class=JSONResponse, response_model=BaseResponse,
summary="删除国际化类型")
@i18nAPI.post("/deleteLocale/{id}", response_class=JSONResponse, response_model=BaseResponse,
summary="删除国际化类型")
@Log(title="删除国际化类型", business_type=BusinessType.DELETE)
async def delete_locale(request: Request, id: str = Path(description="国际化类型ID"),
current_user=Depends(LoginController.get_current_user)):
if locale := await Locale.get_or_none(id=id):
# 移除语言
await I18n.filter(locale_id=locale.id).delete()
await locale.delete()
return Response.success(msg="删除成功!")
else:
return Response.error(msg="该国际化类型不存在!")
@i18nAPI.put("/updateLocale/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="修改国际化类型")
@i18nAPI.post("/updateLocale/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="修改国际化类型")
@Log(title="修改国际化类型", business_type=BusinessType.UPDATE)
async def update_locale(request: Request, params: AddLocaleParams, id: str = Path(description="国际化类型ID")):
if locale := await Locale.get_or_none(id=id):
if await Locale.get_or_none(code=params.code, name=params.name, id=id):
return Response.error(msg="该国际化类型已存在!")
locale.code = params.code
locale.name = params.name
await locale.save()
return Response.success(msg="修改成功!")
else:
return Response.error(msg="该国际化类型不存在!")
@i18nAPI.get("/locale/info/{id}", response_class=JSONResponse, response_model=GetLocaleInfoResponse,
summary="获取国际化类型信息")
@Log(title="获取国际化类型信息", business_type=BusinessType.SELECT)
async def get_locale_info(request: Request, id: str = Path(description="国际化类型ID")):
if locale := await Locale.get_or_none(id=id):
locale = {
"id": locale.id,
"code": locale.code,
"name": locale.name,
"create_time": locale.create_time,
"update_time": locale.update_time,
"create_by": locale.create_by,
"update_by": locale.update_by,
}
return Response.success(data=locale)
else:
return Response.error(msg="该国际化类型不存在!")
@i18nAPI.get("/locale/list", response_class=JSONResponse, response_model=BaseResponse, summary="获取国际化类型列表")
# @Log(title="获取国际化类型列表", business_type=BusinessType.SELECT)
async def get_locale_list(request: Request,
page: int = Query(default=1, description="当前页码"),
pageSize: int = Query(default=50, description="每页条数"),
name: Optional[str] = Query(default=None, description="国际化类型名称"),
code: Optional[str] = Query(default=None, description="国际化类型代码"),
):
filterArgs = {
f'{k}__contains': v for k, v in {
'name': name,
'code': code
}.items() if v
}
total = await Locale.filter(**filterArgs).count()
data = await Locale.filter(**filterArgs).offset((page - 1) * pageSize).limit(pageSize).distinct().values(
id="id",
code="code",
name="name",
create_time="create_time",
update_time="update_time",
create_by="create_by",
update_by="update_by"
)
return Response.success(data={
"total": total,
"result": data,
"page": page
})
@i18nAPI.post("/addI18n", response_class=JSONResponse, response_model=BaseResponse, summary="添加国际化内容")
@Log(title="添加国际化内容", business_type=BusinessType.INSERT)
async def add_i18n(request: Request, params: AddI18nParams, current_user=Depends(LoginController.get_current_user)):
if await I18n.get_or_none(key=params.key, locale_id=params.locale_id):
return Response.error(msg="该国际化内容已存在!")
locale = await Locale.get_or_none(id=params.locale_id)
i18n = await I18n.create(
key=params.key,
translation=params.translation,
locale=locale
)
if i18n:
return Response.success(msg="添加成功!")
else:
return Response.error(msg="添加失败!")
@i18nAPI.delete("/deleteI18n/{id}", response_class=JSONResponse, response_model=BaseResponse,
summary="删除国际化内容")
@i18nAPI.post("/deleteI18n/{id}", response_class=JSONResponse, response_model=BaseResponse,
summary="删除国际化内容")
@Log(title="删除国际化内容", business_type=BusinessType.DELETE)
async def delete_i18n(request: Request, id: str = Path(description="国际化内容ID"),
current_user=Depends(LoginController.get_current_user)):
if i18n := await I18n.get_or_none(id=id):
await i18n.delete()
return Response.success(msg="删除成功!")
else:
return Response.error(msg="该国际化内容不存在!")
@i18nAPI.put("/updateI18n/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="修改国际化内容")
@i18nAPI.post("/updateI18n/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="修改国际化内容")
@Log(title="修改国际化内容", business_type=BusinessType.UPDATE)
async def update_i18n(request: Request, params: AddI18nParams, id: str = Path(description="国际化内容ID"),
current_user=Depends(LoginController.get_current_user)):
if i18n := await I18n.get_or_none(id=id):
locale = await Locale.get_or_none(id=params.locale_id)
i18n.key = params.key
i18n.translation = params.translation
i18n.locale = locale
await i18n.save()
return Response.success(msg="修改成功!")
else:
return Response.error(msg="该国际化内容不存在!")
@i18nAPI.get("/info/{id}", response_class=JSONResponse, response_model=GetI18nInfoResponse,
summary="获取国际化内容信息")
@Log(title="获取国际化内容信息", business_type=BusinessType.SELECT)
async def get_i18n_info(request: Request, id: str = Path(description="国际化内容ID")):
if i18n := await I18n.get_or_none(id=id):
i18n = {
"id": i18n.id,
"key": i18n.key,
"translation": i18n.translation,
"locale_id": i18n.locale.id,
"locale_name": i18n.locale.name,
"create_time": i18n.create_time,
"update_time": i18n.update_time,
"create_by": i18n.create_by,
"update_by": i18n.update_by,
}
return Response.success(data=i18n)
else:
return Response.error(msg="该国际化内容不存在!")
@i18nAPI.get("/list", response_class=JSONResponse, response_model=GetI18nListResponse, summary="获取国际化内容列表")
@Log(title="获取国际化内容列表", business_type=BusinessType.SELECT)
async def get_i18n_list(request: Request,
page: int = Query(default=1, description="当前页码"),
pageSize: int = Query(default=50, description="每页条数"),
key: Optional[str] = Query(default=None, description="国际化内容key"),
locale_id: Optional[str] = Query(default=None, description="国际化内容语言ID"),
translation: Optional[str] = Query(default=None, description="国际化内容翻译内容"),
):
filterArgs = {
f'{k}__contains': v for k, v in {
'key': key,
'locale_id': locale_id,
'translation': translation
}.items() if v
}
total = await I18n.filter(**filterArgs).count()
data = await I18n.filter(**filterArgs).offset((page - 1) * pageSize).limit(pageSize).values(
id="id",
key="key",
translation="translation",
locale_id="locale__id",
locale_code="locale__code",
locale_name="locale__name",
create_time="create_time",
update_time="update_time",
create_by="create_by",
update_by="update_by"
)
return Response.success(data={
"total": total,
"result": data,
"page": page
})
@i18nAPI.get("/infoList/{id}", response_class=JSONResponse, response_model=GetI18nInfoListResponse,
summary="获取国际化列表")
# @Log(title="获取国际化列表", business_type=BusinessType.SELECT)
async def get_i18n_info_list(request: Request, id: str = Path(description="国际化内容语言ID")):
if locale := await Locale.get_or_none(id=id):
result = await request.app.state.redis.get(f'{RedisKeyConfig.TRANSLATION_INFO.key}:{id}')
if result:
result = eval(result)
return Response.success(data=result)
data = await I18n.filter(locale_id=locale.id).values(
id="id",
key="key",
translation="translation",
locale_id="locale__id",
locale_name="locale__name",
create_time="create_time",
update_time="update_time",
create_by="create_by",
update_by="update_by"
)
result = {}
for i18n in data:
result[f"{i18n['key']}"] = i18n["translation"]
await request.app.state.redis.set(f'{RedisKeyConfig.TRANSLATION_INFO.key}:{id}',
str(jsonable_encoder({
"data": result,
"locale": locale.code,
"name": locale.name,
})),
ex=timedelta(minutes=60))
return Response.success(data={
"data": result,
"locale": locale.code,
"name": locale.name,
})
return Response.error(msg="该国际化内容语言不存在!")

183
api/log.py Normal file
View File

@@ -0,0 +1,183 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/27 21:40
# @UpdateTime : 2025/01/27 21:40
# @Author : sonder
# @File : log.py
# @Software : PyCharm
# @Comment : 本程序
from fastapi import APIRouter, Depends, Path, Query, Request
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from annotation.auth import Auth
from annotation.log import Log
from config.constant import BusinessType, RedisKeyConfig
from controller.login import LoginController
from models import LoginLog, OperationLog
from schemas.common import BaseResponse, DeleteListParams
from schemas.log import GetLoginLogResponse, GetOperationLogResponse
from utils.response import Response
logAPI = APIRouter(
prefix="/log",
dependencies=[Depends(LoginController.get_current_user)]
)
@logAPI.get("/login", response_class=JSONResponse, response_model=GetLoginLogResponse, summary="用户获取登录日志")
async def get_login_log(request: Request,
page: int = Query(default=1, description="页码"),
pageSize: int = Query(default=10, description="每页数量"),
current_user: dict = Depends(LoginController.get_current_user),
):
online_user_list = await LoginController.get_online_user(request)
online_user_list = list(
filter(lambda x: x["user_id"] == current_user.get("id"), jsonable_encoder(online_user_list, )))
user_id = current_user.get("id")
result = await LoginLog.filter(user_id=user_id, del_flag=1).offset((page - 1) * pageSize).limit(pageSize).values(
id="id",
user_id="user__id",
username="user__username",
user_nickname="user__nickname",
department_id="user__department__id",
department_name="user__department__name",
login_ip="login_ip",
login_location="login_location",
browser="browser",
os="os",
status="status",
login_time="login_time",
session_id="session_id",
create_time="create_time",
update_time="update_time"
)
for log in result:
log["online"] = False
for item in online_user_list:
if item["session_id"] == log["session_id"]:
log["online"] = True
return Response.success(data={
"total": await LoginLog.filter(user_id=user_id).count(),
"result": result,
"page": page,
})
@logAPI.delete("/logout/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="用户强退")
@logAPI.post("/logout/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="用户强退")
@Log(title="用户强退", business_type=BusinessType.DELETE)
# @Auth(permission_list=["user:btn:logout"])
async def logout_user(request: Request, id: str = Path(description="会话ID"),
current_user: dict = Depends(LoginController.get_current_user)):
if await LoginLog.get_or_none(user_id=current_user.get("id"), session_id=id):
await request.app.state.redis.delete(f"{RedisKeyConfig.ACCESS_TOKEN.key}:{id}")
return Response.success(msg="强退成功!")
return Response.failure(msg="会话不存在!")
@logAPI.delete("/delete/login/{id}", response_model=BaseResponse, response_class=JSONResponse,
summary="用户删除登录日志")
@logAPI.post("/delete/login/{id}", response_model=BaseResponse, response_class=JSONResponse, summary="用户删除登录日志")
@Log(title="用户删除登录日志", business_type=BusinessType.DELETE)
@Auth(permission_list=["login:btn:delete"])
async def delete_login_log(id: str = Path(..., description="登录日志ID"),
current_user: dict = Depends(LoginController.get_current_user)):
if log := await LoginLog.get_or_none(id=id):
if log.user == current_user.get("id"):
log.del_flag = 0
await log.save()
return Response.success(msg="删除成功")
else:
return Response.failure(msg="无权限删除")
else:
return Response.failure(msg="删除失败,登录日志不存在!")
@logAPI.delete("/deleteList/login", response_model=BaseResponse, response_class=JSONResponse,
summary="用户删除登录日志")
@logAPI.post("/deleteList/login", response_model=BaseResponse, response_class=JSONResponse,
summary="用户删除登录日志")
@Log(title="用户批量删除登录日志", business_type=BusinessType.DELETE)
@Auth(permission_list=["login:btn:delete"])
async def delete_login_log(params: DeleteListParams,
current_user: dict = Depends(LoginController.get_current_user)):
for id in set(params.ids):
if log := await LoginLog.get_or_none(id=id):
if log.user == current_user.get("id"):
log.del_flag = 0
await log.save()
return Response.success(msg="删除成功")
@logAPI.get("/operation", response_class=JSONResponse, response_model=GetOperationLogResponse,
summary="用户获取操作日志")
async def get_operation_log(request: Request,
page: int = Query(default=1, description="页码"),
pageSize: int = Query(default=10, description="每页数量"),
current_user: dict = Depends(LoginController.get_current_user),
):
user_id = current_user.get("id")
result = await OperationLog.filter(operator_id=user_id, del_flag=1).offset((page - 1) * pageSize).limit(
pageSize).values(
id="id",
operation_name="operation_name",
operation_type="operation_type",
request_path="request_path",
request_method="request_method",
request_params="request_params",
response_result="response_result",
host="host",
location="location",
browser="browser",
os="os",
user_agent="user_agent",
operator_id="operator__id",
operator_name="operator__username",
operator_nickname="operator__nickname",
department_id="department__id",
department_name="department__name",
status="status",
operation_time="operation_time",
cost_time="cost_time"
)
return Response.success(data={
"total": await OperationLog.filter(operator_id=user_id).count(),
"result": result,
"page": page,
})
@logAPI.delete("/delete/operation/{id}", response_model=BaseResponse, response_class=JSONResponse,
summary="用户删除操作日志")
@logAPI.post("/delete/operation/{id}", response_model=BaseResponse, response_class=JSONResponse,
summary="用户删除操作日志")
@Log(title="用户删除操作日志", business_type=BusinessType.DELETE)
@Auth(permission_list=["operation:btn:delete"])
async def delete_operation_log(id: str = Path(..., description="操作日志id"),
current_user: dict = Depends(LoginController.get_current_user)):
if log := await OperationLog.get_or_none(id=id):
if log.operator == current_user.get("id"):
log.del_flag = 0
await log.save()
return Response.success(msg="删除成功")
else:
return Response.failure(msg="无权限删除")
else:
return Response.failure(msg="删除失败,操作日志不存在!")
@logAPI.delete("/deleteList/operation", response_model=BaseResponse, response_class=JSONResponse,
summary="用户删除操作日志")
@logAPI.post("/deleteList/operation", response_model=BaseResponse, response_class=JSONResponse,
summary="用户删除操作日志")
@Log(title="用户批量删除操作日志", business_type=BusinessType.DELETE)
@Auth(permission_list=["operation:btn:delete"])
async def delete_operation_log(params: DeleteListParams,
current_user: dict = Depends(LoginController.get_current_user)):
for id in set(params.ids):
if log := await OperationLog.get_or_none(id=id):
if log.operator == current_user.get("id"):
log.del_flag = 0
await log.save()
return Response.success(msg="删除成功")

210
api/login.py Normal file
View File

@@ -0,0 +1,210 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/19 01:00
# @UpdateTime : 2025/01/19 01:00
# @Author : sonder
# @File : login.py
# @Software : PyCharm
# @Comment : 本程序
import uuid
from datetime import timedelta, datetime
from fastapi import APIRouter, Request, Depends
from fastapi.encoders import jsonable_encoder
from starlette.responses import JSONResponse
from tortoise.expressions import Q
from annotation.log import Log
from config.constant import BusinessType
from config.constant import RedisKeyConfig
from controller.login import CustomOAuth2PasswordRequestForm, LoginController
from controller.query import QueryController
from models import Department, User
from schemas.common import BaseResponse
from schemas.login import LoginParams, GetUserInfoResponse, LoginResponse, GetCaptchaResponse, GetEmailCodeParams, \
ResetPasswordParams
from schemas.user import RegisterUserParams
from utils.captcha import Captcha
from utils.log import logger
from utils.mail import Email
from utils.password import Password
from utils.response import Response
loginAPI = APIRouter()
@loginAPI.post("/login", response_class=JSONResponse, summary="用户登录")
@Log(title="用户登录", business_type=BusinessType.GRANT, log_type="login")
async def login(
request: Request,
params: CustomOAuth2PasswordRequestForm = Depends()
):
user = LoginParams(
username=params.username,
password=params.password,
loginDays=params.loginDays,
code=params.code,
uuid=params.uuid
)
result = await LoginController.login(user)
if result["status"]:
await request.app.state.redis.set(
f'{RedisKeyConfig.ACCESS_TOKEN.key}:{result["session_id"]}',
result["accessToken"],
ex=timedelta(minutes=result["expiresIn"]),
)
userInfo = str(jsonable_encoder(result["userInfo"]))
await request.app.state.redis.set(
f'{RedisKeyConfig.USER_INFO.key}:{result["userInfo"]["id"]}',
userInfo,
ex=timedelta(minutes=5),
)
request.app.state.session_id = result["session_id"]
# 判断请求是否来自于api文档如果是返回指定格式的结果用于修复api文档认证成功后token显示undefined的bug
request_from_swagger = request.headers.get('referer').endswith('docs') if request.headers.get(
'referer') else False
request_from_redoc = request.headers.get('referer').endswith('redoc') if request.headers.get(
'referer') else False
if request_from_swagger or request_from_redoc:
return {'access_token': result["accessToken"], 'token_type': 'Bearer',
"expires_in": result["expiresIn"] * 60}
result.pop("status")
result.pop("expiresIn")
result.pop("session_id")
result.pop("userInfo")
return Response.success(data=result)
return Response.failure(msg="登录失败,账号或密码错误!")
@loginAPI.post("/register", response_class=JSONResponse, response_model=LoginResponse, summary="用户注册")
async def register(request: Request, params: RegisterUserParams):
result = await Email.verify_code(request, username=params.username, mail=params.email, code=params.code)
if not result["status"]:
return Response.error(msg=result["msg"])
if await QueryController.register_user_before(username=params.username, phone=params.phone, email=params.email):
return Response.error(msg="注册失败,用户已存在!")
params.password = await Password.get_password_hash(input_password=params.password)
department = await Department.get_or_none(id=params.department_id)
user = await User.create(
username=params.username,
password=params.password,
nickname=params.nickname,
phone=params.phone,
email=params.email,
gender=params.gender,
department=department,
status=params.status,
)
if user:
userParams = LoginParams(
username=params.username,
password=params.password
)
result = await LoginController.login(userParams)
if result["status"]:
await request.app.state.redis.set(
f'{RedisKeyConfig.ACCESS_TOKEN.key}:{result["session_id"]}',
result["accessToken"],
ex=timedelta(minutes=result["expiresIn"]),
)
userInfo = str(jsonable_encoder(result["userInfo"]))
await request.app.state.redis.set(
f'{RedisKeyConfig.USER_INFO.key}:{result["userInfo"]["id"]}',
userInfo,
ex=timedelta(minutes=5),
)
result.pop("status")
result.pop("expiresIn")
result.pop("session_id")
result.pop("userInfo")
return Response.success(msg="注册成功!", data=result)
return Response.error(msg="注册成功!")
else:
return Response.error(msg="注册失败!")
@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)
)
logger.info(f'编号为{session_id}的会话获取图片验证码成功')
return Response.success(data={
"uuid": session_id,
"captcha": captcha,
})
@loginAPI.post("/code", response_class=JSONResponse, response_model=BaseResponse, summary="获取邮件验证码")
async def get_code(request: Request, params: GetEmailCodeParams):
result = await Email.send_email(request, username=params.username, title=params.title, mail=params.mail)
if result:
return Response.success(msg="验证码发送成功!")
return Response.error(msg="验证码发送失败!")
@loginAPI.put("/resetPassword", response_class=JSONResponse, response_model=BaseResponse, summary="重置密码")
@loginAPI.post("/resetPassword", response_class=JSONResponse, response_model=BaseResponse, summary="重置密码")
async def reset_password(request: Request, params: ResetPasswordParams):
result = await Email.verify_code(request, username=params.username, mail=params.mail, code=params.code)
if not result["status"]:
return Response.error(msg=result["msg"])
user = await User.get_or_none(Q(username=params.username) | Q(phone=params.username), email=params.mail)
if user:
user.password = await Password.get_password_hash(input_password=params.password)
await user.save()
return Response.success(msg="密码重置成功!")
return Response.error(msg="密码重置失败,用户不存在!")
@loginAPI.get("/info", response_class=JSONResponse, response_model=GetUserInfoResponse, summary="获取用户信息")
@Log(title="获取用户信息", business_type=BusinessType.SELECT)
async def info(
request: Request,
current_user: dict = Depends(LoginController.get_current_user)
):
return Response.success(data=current_user)
@loginAPI.get("/getRoutes", response_class=JSONResponse, summary="获取路由信息")
# @Log(title="获取路由信息", business_type=BusinessType.SELECT)
async def get_routes(request: Request, current_user: dict = Depends(LoginController.get_current_user)):
routes = await request.app.state.redis.get(f'{RedisKeyConfig.USER_ROUTES.key}:{current_user["id"]}')
if routes:
return Response.success(data=eval(routes))
routes = await LoginController.get_user_routes(current_user["id"])
userRoutes = str(jsonable_encoder(routes))
await request.app.state.redis.set(
f'{RedisKeyConfig.USER_ROUTES.key}:{current_user["id"]}',
userRoutes,
ex=timedelta(minutes=5),
)
return Response.success(data=routes)
@loginAPI.post("/refreshToken", response_class=JSONResponse, response_model=LoginResponse, summary="刷新token")
@Log(title="刷新token", business_type=BusinessType.GRANT)
async def refresh_token(request: Request,
current_user: dict = Depends(LoginController.get_current_user)
):
session_id = uuid.uuid4().__str__()
accessToken = await LoginController.create_token(
data={"user": current_user, "id": current_user.get("id"), "session_id": session_id},
expires_delta=timedelta(minutes=2 * 24 * 60))
expiresTime = (datetime.now() + timedelta(minutes=2 * 24 * 60)).timestamp()
refreshToken = await LoginController.create_token(
data={"user": current_user, "id": current_user.get("id"), "session_id": session_id},
expires_delta=timedelta(minutes=(4 * 24 + 2) * 60))
return Response.success(data={"accessToken": accessToken, "refreshToken": refreshToken, "expiresTime": expiresTime})
@loginAPI.post("/logout", response_class=JSONResponse, response_model=BaseResponse, summary="用户登出")
@Log(title="退出登录", business_type=BusinessType.FORCE)
async def logout(request: Request, status: bool = Depends(LoginController.logout)):
if status:
return Response.success(data="退出成功!")
return Response.error(data="登出失败!")

228
api/permission.py Normal file
View File

@@ -0,0 +1,228 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/20 20:33
# @UpdateTime : 2025/01/20 20:33
# @Author : sonder
# @File : permission.py
# @Software : PyCharm
# @Comment : 本程序
from typing import Optional
from fastapi import APIRouter, Depends, Path, Query, Request
from fastapi.responses import JSONResponse
from annotation.log import Log
from config.constant import BusinessType
from controller.login import LoginController
from models import Permission
from schemas.common import BaseResponse
from schemas.permission import AddPermissionParams, GetPermissionInfoResponse, GetPermissionListResponse
from utils.response import Response
permissionAPI = APIRouter(
prefix="/permission",
dependencies=[Depends(LoginController.get_current_user)]
)
@permissionAPI.post("/add", response_model=BaseResponse, response_class=JSONResponse, summary="新增权限")
@Log(title="新增权限", business_type=BusinessType.INSERT)
async def add_permission(request: Request, params: AddPermissionParams):
permission = await Permission.create(
name=params.name,
parent_id=params.parent_id,
path=params.path,
title=params.title,
menu_type=params.menu_type,
rank=params.rank,
show_link=params.show_link,
show_parent=params.show_parent,
active_path=params.active_path,
component=params.component,
redirect=params.redirect,
frame_src=params.frame_src,
frame_loading=params.frame_loading,
keep_alive=params.keep_alive,
auths=params.auths,
icon=params.icon,
extra_icon=params.extra_icon,
enter_transition=params.enter_transition,
leave_transition=params.leave_transition,
fixed_tag=params.fixed_tag,
hidden_tag=params.hidden_tag,
)
if permission:
return Response.success(msg="新增权限成功!")
else:
return Response.error(msg="新增权限失败!")
@permissionAPI.delete("/delete/{id}", response_model=BaseResponse, response_class=JSONResponse, summary="删除权限")
@permissionAPI.post("/delete/{id}", response_model=BaseResponse, response_class=JSONResponse, summary="删除权限")
@Log(title="删除权限", business_type=BusinessType.DELETE)
async def delete_permission(request: Request, id: str = Path(description="权限ID")):
if permission := await Permission.get_or_none(id=id):
await permission.delete()
return Response.success(msg="删除权限成功!")
else:
return Response.error(msg="删除权限失败,权限不存在!")
@permissionAPI.put("/update/{id}", response_model=BaseResponse, response_class=JSONResponse, summary="更新权限")
@permissionAPI.post("/update/{id}", response_model=BaseResponse, response_class=JSONResponse, summary="更新权限")
@Log(title="更新权限", business_type=BusinessType.UPDATE)
async def update_permission(request: Request, params: AddPermissionParams, id: str = Path(description="权限ID"), ):
if permission := await Permission.get_or_none(id=id):
permission.name = params.name
permission.parent_id = params.parent_id
permission.path = params.path
permission.title = params.title
permission.menu_type = params.menu_type
permission.rank = params.rank
permission.show_link = params.show_link
permission.show_parent = params.show_parent
permission.active_path = params.active_path
permission.component = params.component
permission.redirect = params.redirect
permission.frame_src = params.frame_src
permission.frame_loading = params.frame_loading
permission.keep_alive = params.keep_alive
permission.auths = params.auths
permission.icon = params.icon
permission.extra_icon = params.extra_icon
permission.enter_transition = params.enter_transition
permission.leave_transition = params.leave_transition
permission.fixed_tag = params.fixed_tag
permission.hidden_tag = params.hidden_tag
await permission.save()
return Response.success(msg="更新权限成功!")
else:
return Response.error(msg="更新权限失败,权限不存在!")
@permissionAPI.get("/info/{id}", response_model=GetPermissionInfoResponse, response_class=JSONResponse,
summary="查询权限详情")
@Log(title="查询权限详情", business_type=BusinessType.SELECT)
async def get_permission(request: Request, id: str = Path(description="权限ID")):
if permission := await Permission.get_or_none(permission_id=id):
permission = await permission.first().values(
id="id",
create_by="create_by",
create_time="create_time",
update_by="update_by",
update_time="update_time",
menu_type="menu_type",
parent_id="parent_id",
path="path",
title="title",
name="name",
rank="rank",
redirect="redirect",
component="component",
icon="icon",
extra_icon="extra_icon",
enter_transition="enter_transition",
leave_transition="leave_transition",
active_path="active_path",
auths="auths",
frame_src="frame_src",
frame_loading="frame_loading",
keep_alive="keep_alive",
hidden_tag="hidden_tag",
fixed_tag="fixed_tag",
show_link="show_link",
show_parent="show_parent",
)
return Response.success(msg="查询权限详情成功!", data=permission)
else:
return Response.error(msg="查询权限详情失败,权限不存在!")
@permissionAPI.get("/list", response_model=GetPermissionListResponse, response_class=JSONResponse,
summary="查询权限列表")
@Log(title="查询权限列表", business_type=BusinessType.SELECT)
async def get_permission_list(
request: Request,
page: int = Query(default=1, description="当前页码"),
pageSize: int = Query(default=10, description="每页条数"),
id: Optional[str] = Query(default=None, description="主键"),
name: Optional[str] = Query(default=None, description="权限名称"),
parentId: Optional[str] = Query(default=None, description="父权限ID"),
path: Optional[str] = Query(default=None, description="权限路径"),
rank: Optional[int] = Query(default=None, description="排序权重"),
menuType: Optional[int] = Query(default=None, description="菜单类型0菜单、1iframe、2外链、3按钮"),
showLink: Optional[bool] = Query(default=None, description="显示菜单"),
showParent: Optional[bool] = Query(default=None, description="显示父级菜单"),
activePath: Optional[str] = Query(default=None, description="激活路径"),
component: Optional[str] = Query(default=None, description="组件路径"),
redirect: Optional[str] = Query(default=None, description="重定向路径"),
frameSrc: Optional[str] = Query(default=None, description="iframe路径"),
frameLoading: Optional[bool] = Query(default=None, description="iframe加载动画"),
keepAlive: Optional[bool] = Query(default=None, description="缓存组件"),
auths: Optional[str] = Query(default=None, description="权限标识"),
icon: Optional[str] = Query(default=None, description="菜单图标"),
extraIcon: Optional[str] = Query(default=None, description="右侧图标"),
enterTransition: Optional[str] = Query(default=None, description="进场动画"),
leaveTransition: Optional[str] = Query(default=None, description="离场动画"),
fixedTag: Optional[bool] = Query(default=None, description="固定标签页"),
hiddenTag: Optional[bool] = Query(default=None, description="隐藏标签页")
):
filterArgs = {
f'{k}__contains': v for k, v in {
"id": id,
"name": name,
"parent_id": parentId,
"path": path,
"rank": rank,
"menu_type": menuType,
"show_link": showLink,
"show_parent": showParent,
"active_path": activePath,
"component": component,
"redirect": redirect,
"frame_src": frameSrc,
"frame_loading": frameLoading,
"keep_alive": keepAlive,
"auths": auths,
"icon": icon,
"extra_icon": extraIcon,
"enter_transition": enterTransition,
"leave_transition": leaveTransition,
"fixed_tag": fixedTag,
"hidden_tag": hiddenTag
}.items() if v
}
total = await Permission.filter(**filterArgs).count()
result = await Permission.filter(**filterArgs).offset((page - 1) * pageSize).limit(pageSize).order_by(
'rank').values(
id="id",
create_by="create_by",
create_time="create_time",
update_by="update_by",
update_time="update_time",
menu_type="menu_type",
parent_id="parent_id",
title="title",
name="name",
path="path",
component="component",
rank="rank",
redirect="redirect",
icon="icon",
extra_icon="extra_icon",
enter_transition="enter_transition",
leave_transition="leave_transition",
active_path="active_path",
auths="auths",
frame_src="frame_src",
frame_loading="frame_loading",
keep_alive="keep_alive",
hidden_tag="hidden_tag",
fixed_tag="fixed_tag",
show_link="show_link",
show_parent="show_parent"
)
return Response.success(data={
"total": total,
"result": result,
"page": page,
})

333
api/role.py Normal file
View File

@@ -0,0 +1,333 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/20 22:41
# @UpdateTime : 2025/01/20 22:41
# @Author : sonder
# @File : role.py
# @Software : PyCharm
# @Comment : 本程序
from typing import Optional
from fastapi import APIRouter, Depends, Path, Query, Request
from fastapi.responses import JSONResponse
from annotation.log import Log
from config.constant import BusinessType
from controller.login import LoginController
from models import Role, Permission, RolePermission, Department
from schemas.common import BaseResponse
from schemas.role import AddRoleParams, AddRolePermissionParams, GetRolePermissionInfoResponse, \
GetRolePermissionListResponse
from utils.common import filterKeyValues
from utils.response import Response
roleAPI = APIRouter(
prefix="/role",
dependencies=[Depends(LoginController.get_current_user)]
)
@roleAPI.post("/add", response_model=BaseResponse, response_class=JSONResponse, summary="新增角色")
@Log(title="新增角色", business_type=BusinessType.INSERT)
async def add_role(request: Request, params: AddRoleParams):
if await Role.get_or_none(code=params.role_code, department_id=params.department_id, del_flag=1):
return Response.error(msg="角色编码已存在!")
department = await Department.get_or_none(id=params.department_id, del_flag=1)
if department:
role = await Role.create(
code=params.code,
name=params.name,
description=params.description,
status=params.status,
department_id=department.id,
)
else:
role = await Role.create(
code=params.role_code,
name=params.role_name,
status=params.status,
description=params.role_description,
department_id=None,
)
if role:
return Response.success(msg="新增角色成功!")
return Response.error(msg="新增角色失败!")
@roleAPI.delete("/delete/{id}", response_model=BaseResponse, response_class=JSONResponse, summary="删除角色")
@roleAPI.post("/delete/{id}", response_model=BaseResponse, response_class=JSONResponse, summary="删除角色")
@Log(title="删除角色", business_type=BusinessType.DELETE)
async def delete_role(request: Request, id: int = Path(..., description="角色ID")):
if role := await Role.get_or_none(id=id, del_flag=1):
# 移除相应角色权限
await RolePermission.filter(role_id=role.id).delete()
await role.delete()
return Response.success(msg="删除角色成功!")
return Response.error(msg="删除角色失败!")
@roleAPI.put("/update/{id}", response_model=BaseResponse, response_class=JSONResponse, summary="修改角色")
@roleAPI.post("/update/{id}", response_model=BaseResponse, response_class=JSONResponse, summary="修改角色")
@Log(title="修改角色", business_type=BusinessType.UPDATE)
async def update_role(request: Request, params: AddRoleParams, id: str = Path(..., description="角色ID")):
if role := await Role.get_or_none(id=id, del_flag=1):
role.code = params.code
role.name = params.name
role.description = params.description
department = await Department.get_or_none(id=params.department_id, del_flag=1)
role.status = params.status
if department:
role.department_id = department.id
else:
role.department_id = None
await role.save()
return Response.success(msg="修改角色成功!")
return Response.error(msg="修改角色失败!")
@roleAPI.get("/info/{id}", response_model=BaseResponse, response_class=JSONResponse, summary="查询角色详情")
@Log(title="查询角色详情", business_type=BusinessType.SELECT)
async def get_role_info(request: Request, id: int = Path(..., description="角色ID")):
if role := await Role.get_or_none(id=id, del_flag=1).values(
id="id",
create_by="create_by",
create_time="create_time",
update_by="update_by",
update_time="update_time",
code="code",
name="name",
status="status",
description="description",
department_id="department_id",
department_name="department__name",
department_principal="department__principal",
department_phone="department__phone",
department_email="department__email",
):
return Response.success(data=role)
return Response.error(msg="查询角色详情失败!")
@roleAPI.get("/list", response_model=BaseResponse, response_class=JSONResponse, summary="查询角色列表")
@Log(title="查询角色列表", business_type=BusinessType.SELECT)
async def get_role_list(
request: Request,
page: int = Query(1, description="页码"),
pageSize: int = Query(10, description="每页数量"),
name: Optional[str] = Query(None, description="角色名称"),
code: Optional[str] = Query(None, description="角色编码"),
description: Optional[str] = Query(None, description="角色描述"),
department_id: Optional[str] = Query(None, description="所属部门ID"),
status: Optional[int] = Query(None, description="状态"),
current_user: dict = Depends(LoginController.get_current_user)
):
filterArgs = {
f'{k}__contains': v for k, v in {
"name": name,
"code": code,
"description": description,
"status": status
}.items() if v
}
# 如果未提供 department_id则使用当前用户的部门 ID
if not department_id:
department_id = current_user.get("department_id")
# 查询当前部门及其下属部门的角色
all_roles = await get_role_and_subroles(department_id, filterArgs)
# 分页处理
total = len(all_roles)
paginated_roles = all_roles[(page - 1) * pageSize: page * pageSize]
return Response.success(data={
"result": paginated_roles,
"total": total,
"page": page
})
async def get_department_and_subdepartments(department_id: str, visited: set = None):
"""
递归查询当前部门及其所有下属部门的 ID。
:param department_id: 当前部门 ID
:param visited: 已访问的部门 ID 集合,用于避免循环依赖
:return: 部门 ID 列表
"""
if visited is None:
visited = set() # 初始化已访问的部门 ID 集合
# 如果当前部门 ID 已经访问过,直接返回空列表,避免死循环
if department_id in visited:
return []
visited.add(department_id) # 标记当前部门 ID 为已访问
# 查询当前部门
current_department = await Department.filter(
id=department_id
).values_list("id", flat=True)
# 查询直接子部门
sub_departments = await Department.filter(
parent_id=department_id
).values_list("id", flat=True)
# 递归查询子部门的子部门
for sub_department_id in sub_departments[:]: # 使用切片复制避免修改迭代中的列表
sub_sub_departments = await get_department_and_subdepartments(sub_department_id, visited)
sub_departments.extend(sub_sub_departments)
# 合并当前部门和所有下属部门的 ID
return current_department + sub_departments
async def get_role_and_subroles(department_id: str, filterArgs: dict):
"""
查询当前部门及其下属部门的角色。
:param department_id: 当前部门 ID
:param filterArgs: 过滤条件
:return: 角色列表
"""
# 递归查询当前部门及其下属部门的 ID
department_ids = await get_department_and_subdepartments(department_id)
# 查询这些部门的角色
roles = await Role.filter(
department__id__in=department_ids,
**filterArgs
).values(
id="id",
create_by="create_by",
create_time="create_time",
update_by="update_by",
update_time="update_time",
code="code",
name="name",
status="status",
description="description",
department_id="department__id",
department_name="department__name",
department_principal="department__principal",
department_phone="department__phone",
department_email="department__email",
)
# 根据 id 去重
unique_roles = []
seen_ids = set() # 用于记录已经处理过的角色 ID
for role in roles:
if role["id"] not in seen_ids:
unique_roles.append(role)
seen_ids.add(role["id"])
return unique_roles
@roleAPI.post("/addPermission", response_model=BaseResponse, response_class=JSONResponse, summary="新增角色权限")
@Log(title="新增角色权限", business_type=BusinessType.INSERT)
async def add_role_permission(request: Request, params: AddRolePermissionParams,
id: str = Path(..., description="角色ID")):
if role := await Role.get_or_none(id=id, del_flag=1):
# 已有角色权限
rolePermissions = await RolePermission.filter(role_id=id).all().values("permission_id")
rolePermissions = await filterKeyValues(rolePermissions, "permission_id")
# 利用集合筛选出角色权限中不存在的权限
add_list = set(params.permission_ids).difference(set(rolePermissions))
# 循环添加角色权限
for item in add_list:
permission = await Permission.get_or_none(id=item, del_flag=1)
if permission:
await RolePermission.create(
role_id=role.id,
permission_id=permission.id
)
return Response.success(msg="新增角色权限成功!")
return Response.error(msg="新增角色权限失败!")
@roleAPI.delete("/deletePermission/{id}", response_model=BaseResponse, response_class=JSONResponse,
summary="删除角色权限")
@roleAPI.post("/deletePermission/{id}", response_model=BaseResponse, response_class=JSONResponse,
summary="删除角色权限")
@Log(title="删除角色权限", business_type=BusinessType.DELETE)
async def delete_role_permission(request: Request, id: int = Path(..., description="角色权限ID")):
if rolePermission := await RolePermission.get_or_none(id=id, del_flag=1):
await rolePermission.delete()
return Response.success(msg="删除角色权限成功!")
return Response.error(msg="删除角色权限失败!")
@roleAPI.put("/updatePermission/{id}", response_model=BaseResponse, response_class=JSONResponse, summary="修改角色权限")
@roleAPI.post("/updatePermission/{id}", response_model=BaseResponse, response_class=JSONResponse,
summary="修改角色权限")
@Log(title="修改角色权限", business_type=BusinessType.UPDATE)
async def update_role_permission(request: Request, params: AddRolePermissionParams,
id: str = Path(..., description="角色ID")):
if role := await Role.get_or_none(id=id, del_flag=1):
# 已有角色权限
rolePermissions = await RolePermission.filter(role_id=role.id).all().values("permission_id")
rolePermissions = await filterKeyValues(rolePermissions, "permission_id")
# 利用集合筛选出角色权限中不存在的权限
delete_list = set(rolePermissions).difference(set(params.permission_ids))
# 利用集合筛选出角色权限中新增的权限
add_list = set(params.permission_ids).difference(set(rolePermissions))
# 循环删除角色权限
for item in delete_list:
await RolePermission.filter(role_id=id, permission_id=item).delete()
# 循环添加角色权限
for item in add_list:
await RolePermission.create(role_id=id, permission_id=item)
return Response.success(msg="修改角色权限成功!")
return Response.error(msg="修改角色权限失败!")
@roleAPI.get("/permissionInfo/{id}", response_model=GetRolePermissionInfoResponse, response_class=JSONResponse,
summary="获取角色权限信息")
@Log(title="获取角色权限信息", business_type=BusinessType.SELECT)
async def get_role_permission_info(request: Request, id: int = Path(..., description="角色权限ID")):
if rolePermission := await RolePermission.get_or_none(id=id, del_flag=1):
data = await rolePermission.first().values(
id="id",
create_by="create_by",
create_time="create_time",
update_by="update_by",
update_time="update_time",
role_id="role__id",
role_name="role__name",
role_code="role__code",
permission_id="permission__id",
permission_name="permission__title",
permission_auth="permission__auths",
permission_type="permission__menu_type"
)
return Response.success(data=data)
return Response.error(msg="获取角色权限信息失败!")
@roleAPI.get("/permissionList/{id}", response_model=GetRolePermissionListResponse, response_class=JSONResponse,
summary="获取角色权限列表")
@Log(title="获取角色权限列表", business_type=BusinessType.SELECT)
async def get_role_permission_list(request: Request, id: str = Path(..., description="角色ID")):
total = await RolePermission.filter(role_id=id).count()
data = await RolePermission.filter(role_id=id).values(
id="id",
create_by="create_by",
create_time="create_time",
update_by="update_by",
update_time="update_time",
role_id="role__id",
role_name="role__name",
role_code="role__code",
permission_id="permission__id",
permission_parent_id="permission__parent_id",
permission_name="permission__title",
permission_auth="permission__auths",
permission_type="permission__menu_type"
)
return Response.success(data={
"result": data,
"total": total,
"page": 1
})

112
api/server.py Normal file
View File

@@ -0,0 +1,112 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/02/04 15:25
# @UpdateTime : 2025/02/04 15:25
# @Author : sonder
# @File : server.py
# @Software : PyCharm
# @Comment : 本程序
import os
import platform
import socket
import time
import psutil
from fastapi import APIRouter, Depends, Request
from fastapi.responses import JSONResponse
from annotation.log import Log
from config.constant import BusinessType
from controller.login import LoginController
from schemas.server import GetServerInfoResponse, CpuInfo, MemoryInfo, SystemInfo, PythonInfo, SystemFiles, \
GetSystemInfoResult
from utils.common import bytes2human
from utils.response import Response
serverAPI = APIRouter(
prefix="/server",
dependencies=[Depends(LoginController.get_current_user)]
)
@serverAPI.get("", response_class=JSONResponse, response_model=GetServerInfoResponse, summary="获取服务器信息")
@Log(title="获取服务器信息", business_type=BusinessType.SELECT)
async def get_server_info(request: Request):
# CPU信息
# 获取CPU总核心数
cpu_num = psutil.cpu_count(logical=True)
cpu_usage_percent = psutil.cpu_times_percent()
cpu_used = cpu_usage_percent.user
cpu_sys = cpu_usage_percent.system
cpu_free = cpu_usage_percent.idle
cpu = CpuInfo(cpuNum=cpu_num, used=cpu_used, sys=cpu_sys, free=cpu_free)
# 内存信息
memory_info = psutil.virtual_memory()
memory_total = bytes2human(memory_info.total)
memory_used = bytes2human(memory_info.used)
memory_free = bytes2human(memory_info.free)
memory_usage = memory_info.percent
mem = MemoryInfo(total=memory_total, used=memory_used, free=memory_free, usage=memory_usage)
# 主机信息
# 获取主机名
hostname = socket.gethostname()
# 获取IP
computer_ip = socket.gethostbyname(hostname)
os_name = platform.platform()
computer_name = platform.node()
os_arch = platform.machine()
user_dir = os.path.abspath(os.getcwd())
sys = SystemInfo(
computerIp=computer_ip, computerName=computer_name, osArch=os_arch, osName=os_name, userDir=user_dir
)
# python解释器信息
current_pid = os.getpid()
current_process = psutil.Process(current_pid)
python_name = current_process.name()
python_version = platform.python_version()
python_home = current_process.exe()
start_time_stamp = current_process.create_time()
start_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(start_time_stamp))
current_time_stamp = time.time()
difference = current_time_stamp - start_time_stamp
# 将时间差转换为天、小时和分钟数
days = int(difference // (24 * 60 * 60)) # 每天的秒数
hours = int((difference % (24 * 60 * 60)) // (60 * 60)) # 每小时的秒数
minutes = int((difference % (60 * 60)) // 60) # 每分钟的秒数
run_time = f'{days}{hours}小时{minutes}分钟'
# 获取当前Python程序的pid
pid = os.getpid()
# 获取该进程的内存信息
current_process_memory_info = psutil.Process(pid).memory_info()
py = PythonInfo(
name=python_name,
version=python_version,
startTime=start_time,
runTime=run_time,
home=python_home,
total=bytes2human(memory_info.available),
used=bytes2human(current_process_memory_info.rss),
free=bytes2human(memory_info.available - current_process_memory_info.rss),
usage=round((current_process_memory_info.rss / memory_info.available) * 100, 2),
)
# 磁盘信息
io = psutil.disk_partitions()
sys_files = []
for i in io:
o = psutil.disk_usage(i.device)
disk_data = SystemFiles(
dirName=i.device,
sysTypeName=i.fstype,
typeName='本地固定磁盘(' + i.mountpoint.replace('\\', '') + '',
total=bytes2human(o.total),
used=bytes2human(o.used),
free=bytes2human(o.free),
usage=f'{psutil.disk_usage(i.device).percent}%',
)
sys_files.append(disk_data)
result = GetSystemInfoResult(cpu=cpu, memory=mem, system=sys, python=py, systemFiles=sys_files)
return Response.success(data=result)

349
api/user.py Normal file
View File

@@ -0,0 +1,349 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/20 00:31
# @UpdateTime : 2025/01/20 00:31
# @Author : sonder
# @File : user.py
# @Software : PyCharm
# @Comment : 本程序
import os
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, Path, Query, UploadFile, File, Request
from fastapi.responses import JSONResponse
from annotation.auth import Auth
from annotation.log import Log
from config.constant import BusinessType
from config.env import UploadConfig
from controller.login import LoginController
from controller.query import QueryController
from exceptions.exception import ModelValidatorException
from models import File as FileModel
from models import Role, Department
from models.user import User, UserRole
from schemas.common import BaseResponse
from schemas.department import GetDepartmentListResponse
from schemas.file import UploadFileResponse
from schemas.user import AddUserParams, GetUserListResponse, GetUserInfoResponse, UpdateUserParams, \
AddUserRoleParams, GetUserRoleInfoResponse, UpdateUserRoleParams, GetUserPermissionListResponse, ResetPasswordParams
from utils.common import filterKeyValues
from utils.password import Password
from utils.response import Response
userAPI = APIRouter(prefix="/user", dependencies=[Depends(LoginController.get_current_user)])
@userAPI.post("/add", response_class=JSONResponse, response_model=BaseResponse, summary="新增用户")
@Log(title="新增用户", business_type=BusinessType.INSERT)
async def add_user(
request: Request,
params: AddUserParams
):
if await QueryController.register_user_before(username=params.username, phone=params.phone, email=params.email):
return Response.error(msg="添加失败,用户已存在!")
params.password = await Password.get_password_hash(input_password=params.password)
department = await Department.get_or_none(id=params.department_id)
user = await User.create(
username=params.username,
password=params.password,
nickname=params.nickname,
phone=params.phone,
email=params.email,
gender=params.gender,
department=department,
status=params.status,
)
if user:
return Response.success(msg="添加成功!")
else:
return Response.error(msg="添加失败!")
@userAPI.delete("/delete/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="删除用户")
@userAPI.post("/delete/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="删除用户")
@Log(title="删除用户", business_type=BusinessType.DELETE)
async def delete_user(
request: Request,
id: str = Path(..., description="用户ID")):
if user := await User.get_or_none(id=id):
await user.delete()
return Response.success(msg="删除成功!")
else:
return Response.error(msg="删除失败,用户不存在!")
@userAPI.put("/update/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="更新用户")
@userAPI.post("/update/{id}", response_class=JSONResponse, response_model=BaseResponse, summary="更新用户")
@Log(title="更新用户", business_type=BusinessType.UPDATE)
async def update_user(
request: Request,
params: UpdateUserParams,
id: str = Path(..., description="用户ID")):
if user := await User.get_or_none(id=id):
user.username = params.username
user.nickname = params.nickname
user.phone = params.phone
user.email = params.email
user.gender = params.gender
user.status = params.status
if department := await Department.get_or_none(id=params.department_id):
user.department = department
else:
user.department = None
await user.save()
return Response.success(msg="更新成功!")
else:
return Response.error(msg="更新失败,用户不存在!")
@userAPI.get("/info/{id}", response_class=JSONResponse, response_model=GetUserInfoResponse, summary="获取用户信息")
@Log(title="获取用户信息", business_type=BusinessType.SELECT)
async def get_user_info(request: Request, id: str = Path(..., description="用户ID")):
if user := await User.get_or_none(id=id):
user = await user.first().values(
id="id",
create_time="create_time",
update_time="update_time",
username="username",
email="email",
phone="phone",
nickname="nickname",
gender="gender",
status="status",
department_id="department__id",
)
return Response.success(data=user)
else:
return Response.error(msg="用户不存在!")
@userAPI.get("/list", response_class=JSONResponse, response_model=GetUserListResponse, summary="获取用户列表")
@Log(title="获取用户列表", business_type=BusinessType.SELECT)
@Auth(["user:btn:queryUser"])
async def get_user_list(
request: Request,
page: int = Query(default=1, description="当前页码"),
pageSize: int = Query(default=10, description="每页数量"),
username: Optional[str] = Query(default=None, description="用户名"),
nickname: Optional[str] = Query(default=None, description="昵称"),
phone: Optional[str] = Query(default=None, description="手机号"),
email: Optional[str] = Query(default=None, description="邮箱"),
gender: Optional[str] = Query(default=None, description="性别"),
status: Optional[str] = Query(default=None, description="状态"),
department_id: Optional[str] = Query(default=None, description="部门ID"),
):
filterArgs = {
f'{k}__contains': v for k, v in {
'username': username,
'nickname': nickname,
'phone': phone,
'email': email,
'gender': gender,
'status': status,
'department_id': department_id
}.items() if v
}
total = await User.filter(**filterArgs).count()
result = await User.filter(**filterArgs).offset((page - 1) * pageSize).limit(pageSize).values(
id="id",
create_time="create_time",
update_time="update_time",
username="username",
email="email",
phone="phone",
nickname="nickname",
gender="gender",
status="status",
department_id="department__id",
)
return Response.success(data={
"result": result,
"total": total,
"page": page
})
@userAPI.post("/addRole", response_model=BaseResponse, response_class=JSONResponse, summary="添加用户角色")
@Log(title="添加用户角色", business_type=BusinessType.INSERT)
async def add_user_role(request: Request, params: AddUserRoleParams):
if await UserRole.get_or_none(user_id=params.user_id, role_id=params.role_id, del_flag=1):
return Response.error(msg="该用户已存在该角色!")
if user := await User.get_or_none(id=params.user_id, del_flag=1):
if role := await Role.get_or_none(id=params.role_id, del_flag=1):
userRole = await UserRole.create(user_id=user.id, role_id=role.id)
if userRole:
return Response.success(msg="添加成功!")
else:
return Response.error(msg="添加失败!")
else:
return Response.error(msg="添加失败,角色不存在!")
else:
return Response.error(msg="添加失败,用户不存在!")
@userAPI.delete("/deleteRole/{id}", response_model=BaseResponse, response_class=JSONResponse,
summary="删除用户角色")
@userAPI.post("/deleteRole/{id}", response_model=BaseResponse, response_class=JSONResponse,
summary="删除用户角色")
@Log(title="删除用户角色", business_type=BusinessType.DELETE)
async def delete_user_role(request: Request, id: str = Path(description="用户角色ID")):
if userRole := await UserRole.get_or_none(id=id, del_flag=1):
await userRole.delete()
return Response.success(msg="删除成功!")
else:
return Response.error(msg="删除失败,用户角色不存在!")
@userAPI.put("/updateRole", response_model=BaseResponse, response_class=JSONResponse, summary="修改用户角色")
@userAPI.post("/updateRole", response_model=BaseResponse, response_class=JSONResponse,
summary="修改用户角色")
@Log(title="修改用户角色", business_type=BusinessType.UPDATE)
async def update_user_role(request: Request, params: UpdateUserRoleParams):
# 获取用户已有角色
userRoles = await UserRole.filter(user_id=params.user_id, del_flag=1).values("role_id")
userRoles = await filterKeyValues(userRoles, "role_id")
# 利用集合找到需要添加的角色
addRoles = set(params.role_ids).difference(set(userRoles))
# 利用集合找到需要删除的角色
deleteRoles = set(userRoles).difference(set(params.role_ids))
# 添加角色
for role_id in addRoles:
if role := await Role.get_or_none(id=role_id, del_flag=1):
await UserRole.create(user_id=params.user_id, role_id=role.id)
# 删除角色
for role_id in deleteRoles:
if userRole := await UserRole.get_or_none(user_id=params.user_id, role_id=role_id, del_flag=1):
await userRole.delete()
return Response.success(msg="修改成功!")
@userAPI.get("/roleInfo/{id}", response_model=GetUserRoleInfoResponse, response_class=JSONResponse,
summary="获取用户角色信息")
@Log(title="获取用户角色信息", business_type=BusinessType.SELECT)
async def get_user_role_info(request: Request, id: str = Path(description="用户角色ID")):
if userRole := await UserRole.get_or_none(id=id, del_flag=1):
data = await userRole.first().values(
id="id",
user_id="user__id",
user_name="user__username",
role_name="role__name",
role_code="role__code",
role_id="role__id",
create_time="create_time",
update_time="update_time"
)
return Response.success(data=data)
else:
return Response.error(msg="获取失败,用户角色不存在!")
@userAPI.get("/roleList/{id}", response_model=GetDepartmentListResponse, response_class=JSONResponse,
summary="获取用户角色列表")
@Log(title="获取用户角色列表", business_type=BusinessType.SELECT)
async def get_user_role_list(
request: Request,
id: str = Path(description="用户ID"),
):
result = await UserRole.filter(user_id=id).values(
id="id",
department_id="user__department__id",
department_name="user__department__name",
department_phone="user__department__phone",
department_principal="user__department__principal",
department_email="user__department__email",
role_name="role__name",
role_code="role__code",
role_id="role__id",
create_time="create_time",
update_time="update_time"
)
return Response.success(data={
"result": result,
"total": len(result),
"page": 1
})
@userAPI.get("/permissionList/{id}", response_class=JSONResponse, response_model=GetUserPermissionListResponse,
summary="获取用户权限列表")
@Log(title="获取用户权限列表", business_type=BusinessType.SELECT)
async def get_user_permission_list(request: Request, id: str = Path(description="用户ID")):
permissions = await QueryController.get_user_permissions(user_id=id)
permissions = await filterKeyValues(permissions, "id")
# 获取用户角色
return Response.success(data=list(set(permissions)))
@userAPI.post("/avatar/{id}", response_model=UploadFileResponse, response_class=JSONResponse, summary="上传用户头像")
@Log(title="上传用户头像", business_type=BusinessType.UPDATE)
async def upload_user_avatar(
request: Request,
id: str = Path(description="用户ID"),
file: UploadFile = File(...)):
if user := await User.get_or_none(id=id):
image_mimetypes = [
'image/jpeg',
'image/png',
'image/gif',
'image/svg+xml',
'image/bmp',
'image/webp',
'image/tiff'
]
if file.content_type not in image_mimetypes:
raise ModelValidatorException(message="文件类型不支持")
# 2. 生成唯一的文件名
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
unique_filename = f"{id}_{timestamp}"
# 3. 保存文件到服务器
file_path = os.path.join(UploadConfig.UPLOAD_PATH, unique_filename)
with open(file_path, "wb") as buffer:
buffer.write(await file.read())
# 4. 构建文件的相对路径和绝对路径
relative_path = os.path.join(UploadConfig.UPLOAD_PREFIX, unique_filename) # 相对路径
absolute_path = os.path.abspath(file_path) # 绝对路径
# 5. 将文件信息保存到数据库
file_record = await FileModel.create(
name=file.filename,
size=os.path.getsize(file_path),
file_type=file.content_type,
absolute_path=absolute_path,
relative_path=relative_path,
uploader_id=id,
)
user.avatar = f"/file/{file_record.id}"
await user.save()
result = await file_record.first().values(
id="id",
name="name",
size="size",
file_type="file_type",
relative_path="relative_path",
absolute_path="absolute_path",
uploader_id="uploader__id",
uploader_username="uploader__username",
uploader_nickname="uploader__nickname",
uploader_department_id="uploader__department__id",
uploader_department_name="uploader__department__name",
create_time="create_time",
update_time="update_time",
)
return Response.success(data=result)
return Response.failure(msg="用户不存在!")
@userAPI.put("/resetPassword/{id}", response_model=BaseResponse, response_class=JSONResponse, summary="重置用户密码")
@userAPI.post("/resetPassword/{id}", response_model=BaseResponse, response_class=JSONResponse, summary="重置用户密码")
@Log(title="重置用户密码", business_type=BusinessType.UPDATE)
@Auth(permission_list=["user:btn:reset_password"])
async def reset_user_password(request: Request, params: ResetPasswordParams, id: str = Path(description="用户ID")):
if user := await User.get_or_none(id=id):
user.password = await Password.get_password_hash(params.password)
await user.save()
return Response.success(msg="重置密码成功!")
return Response.failure(msg="用户不存在!")

101
app.py Normal file
View File

@@ -0,0 +1,101 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/17 21:18
# @UpdateTime : 2025/01/17 21:18
# @Author : sonder
# @File : app.py
# @Software : PyCharm
# @Comment : 本程序为项目启动文件
from contextlib import asynccontextmanager
import uvicorn
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
from api.cache import cacheAPI
from api.department import departmentAPI
from api.file import fileAPI
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
from exceptions.handle import handle_exception
from middlewares.handle import handle_middleware
from utils.log import logger
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info(f'{AppConfig.app_name}开始启动')
app.state.redis = await Redis.create_redis_pool()
logger.info(f'{AppConfig.app_name}启动成功')
await init_db()
yield
await close_db()
await Redis.close_redis_pool(app)
app = FastAPI(
title=AppConfig.app_name,
description=f'{AppConfig.app_name}接口文档',
version=AppConfig.app_version,
lifespan=lifespan,
)
# 加载中间件处理方法
handle_middleware(app)
# 加载全局异常处理方法
handle_exception(app)
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
title="My API",
version="1.0.0",
routes=app.routes,
)
# 修改 Swagger UI 配置
openapi_schema["components"]["securitySchemes"] = {
"Bearer": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
}
app.openapi_schema = openapi_schema
return app.openapi_schema
api_list = [
{'api': loginAPI, 'tags': ['登录管理']},
{'api': userAPI, 'tags': ['用户管理']},
{'api': departmentAPI, 'tags': ['部门管理']},
{'api': roleAPI, 'tags': ['角色管理']},
{'api': permissionAPI, 'tags': ['权限管理']},
{'api': fileAPI, 'tags': ['文件管理']},
{'api': logAPI, 'tags': ['日志管理']},
{'api': cacheAPI, 'tags': ['缓存管理']},
{'api': serverAPI, 'tags': ['服务器管理']},
{'api': i18nAPI, 'tags': ['国际化管理']},
]
for api in api_list:
app.include_router(router=api.get("api"), tags=api.get("tags"))
if __name__ == '__main__':
uvicorn.run(
app='app:app',
host=AppConfig.app_host,
port=AppConfig.app_port,
root_path=AppConfig.app_root_path,
reload=AppConfig.app_reload,
log_config="uvicorn_config.json"
)

Binary file not shown.

7
config/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/17 21:13
# @UpdateTime : 2025/01/17 21:13
# @Author : sonder
# @File : __init__.py.py
# @Software : PyCharm
# @Comment : 本程序

265
config/constant.py Normal file
View File

@@ -0,0 +1,265 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 02:39
# @UpdateTime : 2025/01/18 02:39
# @Author : sonder
# @File : constant.py
# @Software : PyCharm
# @Comment : 本程序
from enum import Enum
class BusinessType(Enum):
"""
业务操作类型枚举
定义系统中的操作类型,用于记录和分类业务日志。
枚举值说明:
- OTHER: 其它操作,默认值为 0
- SELECT: 查询操作默认值为1
- INSERT: 新增操作,值为 2
- UPDATE: 修改操作,值为 3
- DELETE: 删除操作,值为 4
- GRANT: 授权操作,值为 5
- EXPORT: 导出数据操作,值为 6
- IMPORT: 导入数据操作,值为 7
- FORCE: 强制退出操作,值为 8
- GENCODE: 代码生成操作,值为 9
- CLEAN: 清空数据操作,值为 10
"""
OTHER = 0
"""
其它操作,默认值为 0
"""
SELECT = 1
"""
查询操作默认值为1
"""
INSERT = 2
"""
新增操作,值为 2
"""
UPDATE = 3
"""
修改操作,值为 3
"""
DELETE = 4
"""
删除操作,值为 4
"""
GRANT = 5
"""
授权操作,值为 5
"""
EXPORT = 6
"""
导出数据操作,值为 6
"""
IMPORT = 7
"""
导入数据操作,值为 7
"""
FORCE = 8
"""
强制退出操作,值为 8
"""
GENCODE = 9
"""
代码生成操作,值为 9
"""
CLEAN = 10
"""
清空数据操作,值为 10
"""
class CommonConstant:
"""
常用常量定义类,包含系统中常用的字符串标识和布尔值。
属性:
WWW: `www.` 主域的前缀。
HTTP: `http://` 协议前缀。
HTTPS: `https://` 协议前缀。
LOOKUP_RMI: RMI远程方法调用协议前缀。
LOOKUP_LDAP: LDAP 协议前缀。
LOOKUP_LDAPS: LDAPS 协议前缀。
YES: 系统默认值 "" 的标识。
NO: 系统默认值 "" 的标识。
DEPT_NORMAL: 部门状态,表示正常。
DEPT_DISABLE: 部门状态,表示停用。
UNIQUE: 标识检查结果为唯一。
NOT_UNIQUE: 标识检查结果为不唯一。
"""
WWW = 'www.'
"""`www.` 主域的前缀"""
HTTP = 'http://'
"""`http://` 协议前缀"""
HTTPS = 'https://'
"""`https://` 协议前缀"""
LOOKUP_RMI = 'rmi:'
"""RMI远程方法调用协议前缀"""
LOOKUP_LDAP = 'ldap:'
"""LDAP 协议前缀"""
LOOKUP_LDAPS = 'ldaps:'
"""LDAPS 协议前缀"""
YES = 'Y'
"""系统默认值 "" 的标识"""
NO = 'N'
"""系统默认值 "" 的标识"""
DEPT_NORMAL = '0'
"""部门状态,表示正常"""
DEPT_DISABLE = '1'
"""部门状态,表示停用"""
UNIQUE = True
"""标识检查结果为唯一"""
NOT_UNIQUE = False
"""标识检查结果为不唯一"""
class HttpStatusConstant:
"""
定义 HTTP 状态码的常量,描述不同操作的响应结果。
属性:
SUCCESS: 表示操作成功HTTP 状态码 200。
CREATED: 表示资源已成功创建HTTP 状态码 201。
ACCEPTED: 表示请求已被接受HTTP 状态码 202。
NO_CONTENT: 表示操作成功但无内容返回HTTP 状态码 204。
MOVED_PERM: 表示资源已被永久移除HTTP 状态码 301。
SEE_OTHER: 表示重定向到其他资源HTTP 状态码 303。
NOT_MODIFIED: 表示资源未被修改HTTP 状态码 304。
BAD_REQUEST: 参数错误HTTP 状态码 400。
UNAUTHORIZED: 表示未授权HTTP 状态码 401。
FORBIDDEN: 表示禁止访问HTTP 状态码 403。
NOT_FOUND: 表示资源或服务未找到HTTP 状态码 404。
BAD_METHOD: 不允许的 HTTP 方法HTTP 状态码 405。
CONFLICT: 表示资源冲突HTTP 状态码 409。
UNSUPPORTED_TYPE: 不支持的数据或媒体类型HTTP 状态码 415。
ERROR: 表示系统内部错误HTTP 状态码 500。
NOT_IMPLEMENTED: 接口未实现HTTP 状态码 501。
WARN: 系统警告消息,自定义状态码 601。
"""
SUCCESS = 200
"""表示操作成功HTTP 状态码 200。"""
CREATED = 201
"""表示资源已成功创建HTTP 状态码 201。"""
ACCEPTED = 202
"""表示请求已被接受HTTP 状态码 202。"""
NO_CONTENT = 204
"""表示操作成功但无内容返回HTTP 状态码 204。"""
MOVED_PERM = 301
"""表示资源已被永久移除HTTP 状态码 301。"""
SEE_OTHER = 303
"""表示重定向到其他资源HTTP 状态码 303。"""
NOT_MODIFIED = 304
"""表示资源未被修改HTTP 状态码 304。"""
BAD_REQUEST = 400
"""参数错误HTTP 状态码 400。"""
UNAUTHORIZED = 401
"""表示未授权HTTP 状态码 401。"""
FORBIDDEN = 403
"""表示禁止访问HTTP 状态码 403。"""
NOT_FOUND = 404
"""表示资源或服务未找到HTTP 状态码 404。"""
BAD_METHOD = 405
"""不允许的 HTTP 方法HTTP 状态码 405。"""
CONFLICT = 409
"""表示资源冲突HTTP 状态码 409。"""
UNSUPPORTED_TYPE = 415
"""不支持的数据或媒体类型HTTP 状态码 415。"""
ERROR = 500
"""表示系统内部错误HTTP 状态码 500。"""
NOT_IMPLEMENTED = 501
"""接口未实现HTTP 状态码 501。"""
WARN = 601
"""系统警告消息,自定义状态码 601。"""
class JobConstant:
"""
定时任务相关的常量,限制和规范任务模块的调用。
属性:
JOB_ERROR_LIST: 禁止调用的模块及违规字符串列表,包含敏感或不安全操作。
JOB_WHITE_LIST: 允许调用的模块列表,用于指定合法模块。
"""
JOB_ERROR_LIST = [
'app', 'config', 'exceptions', 'import ', 'middlewares',
'module_admin', 'open(', 'os.', 'server', 'sub_applications',
'subprocess.', 'sys.', 'utils', 'while ', '__import__', '"', "'",
',', '?', ':', ';', '/', '|', '+', '-', '=', '~', '!', '#', '$',
'%', '^', '&', '*', '<', '>', '(', ')', '[', ']', '{', '}', ' '
]
"""禁止调用的模块及违规字符串列表,包含敏感或不安全操作。"""
JOB_WHITE_LIST = ['module_task']
"""允许调用的模块列表,用于指定合法模块。"""
class RedisKeyConfig(Enum):
"""
定义 Redis 键的常量,用于缓存和存储数据。
"""
@property
def key(self):
"""
获取 Redis 键名
:return: 键名字符串
"""
return self.value.get('key')
@property
def remark(self):
"""
获取键名备注信息
:return: 备注信息字符串
"""
return self.value.get('remark')
ACCESS_TOKEN = {'key': 'access_token', 'remark': '登录令牌信息'}
"""登录令牌信息,存储用户的访问令牌。"""
USER_INFO = {'key': 'user_info', 'remark': '用户信息'}
"""用户信息,存储用户的详细信息。"""
USER_ROUTES = {'key': 'user_routes', 'remark': '用户路由信息'}
"""用户路由信息,存储用户的路由信息。"""
CAPTCHA_CODES = {'key': 'captcha_codes', 'remark': '图片验证码'}
"""图片验证码,存储验证码及其校验信息。"""
CAPTCHA_TYPES = {'key': 'captcha_types', 'remark': '图片验证码类型'}
"""图片验证码类型,存储验证码类型及其配置信息。"""
EMAIL_CODES = {'key': 'email_codes', 'remark': '邮箱验证码'}
"""邮箱验证码,存储邮箱验证码及其校验信息。"""
TRANSLATION_INFO = {'key': 'translation_info', 'remark': '国际化信息'}
"""用于存储国际化数据。"""
TRANSLATION_TYPES = {'key': 'translation_types', 'remark': '国际化类型'}
"""国际化类型,存储国际化类型及其配置信息。"""

138
config/database.py Normal file
View File

@@ -0,0 +1,138 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 02:00
# @UpdateTime : 2025/01/18 02:00
# @Author : sonder
# @File : database.py
# @Software : PyCharm
# @Comment : 本程序
import logging
import sys
from logging.handlers import RotatingFileHandler
from tortoise import Tortoise
from config.env import DataBaseConfig
from utils.log import logger, log_path_sql
async def init_db():
"""
异步初始化数据库连接。
"""
# 在数据库连接 URL 中添加时区参数(东八区)
db_url = (
f"mysql://{DataBaseConfig.db_username}:{DataBaseConfig.db_password}@"
f"{DataBaseConfig.db_host}:{DataBaseConfig.db_port}/{DataBaseConfig.db_database}"
"?charset=utf8mb4" # 指定时区为东八区,
)
await Tortoise.init(
db_url=db_url,
modules={"models": ["models"]}, # 指向 models 目录,
timezone="Asia/Shanghai",
)
# 根据 db_echo 配置是否打印 SQL 查询日志
if DataBaseConfig.db_echo:
logger.info("SQL 查询日志已启用")
await configure_tortoise_logging(enable_logging=True, log_level=DataBaseConfig.db_log_level)
else:
logger.info("SQL 查询日志已禁用")
# 禁用 SQL 查询日志
logger.remove(log_path_sql)
# 生成数据库表结构
await Tortoise.generate_schemas()
logger.success("数据库连接成功!")
async def close_db():
"""
关闭数据库连接。
"""
await Tortoise.close_connections()
logger.success("数据库连接关闭!")
async def configure_tortoise_logging(enable_logging: bool = True, log_level: int = logging.DEBUG):
"""
异步配置 Tortoise ORM 日志输出。
:param enable_logging: 是否启用日志输出
:param log_level: 日志输出级别,默认为 DEBUG
"""
aiomysql_logger = logging.getLogger("aiomysql")
tortoise_logger = logging.getLogger("tortoise")
# 清除之前的处理器,避免重复添加
if tortoise_logger.hasHandlers():
tortoise_logger.handlers.clear()
if aiomysql_logger.hasHandlers():
aiomysql_logger.handlers.clear()
if enable_logging:
# 设置日志格式
fmt = logging.Formatter(
fmt="%(asctime)s - %(name)s:%(lineno)d - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# 创建控制台处理器(输出到控制台)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(log_level)
console_handler.setFormatter(fmt)
# 创建文件处理器(输出到文件)
file_handler = RotatingFileHandler(
filename=log_path_sql,
maxBytes=50 * 1024 * 1024, # 日志文件大小达到 50MB 时轮换
backupCount=5, # 保留 5 个旧日志文件
encoding="utf-8",
)
file_handler.setLevel(log_level)
file_handler.setFormatter(fmt)
# 配置 tortoise 顶级日志记录器
tortoise_logger.setLevel(log_level)
tortoise_logger.addHandler(console_handler) # 添加控制台处理器
tortoise_logger.addHandler(file_handler) # 添加文件处理器
# 配置 aiomysql 日志记录器
aiomysql_logger.setLevel(log_level)
aiomysql_logger.addHandler(console_handler) # 添加控制台处理器
aiomysql_logger.addHandler(file_handler) # 添加文件处理器
# 配置 SQL 查询日志记录器
sql_logger = logging.getLogger("tortoise.db_client")
sql_logger.setLevel(log_level)
class SQLResultLogger(logging.Handler):
async def emit(self, record):
# 只处理 SQL 查询相关的日志
if "SELECT" in record.getMessage() or "INSERT" in record.getMessage() or "UPDATE" in record.getMessage() or "DELETE" in record.getMessage():
# 输出 SQL 查询语句
console_handler.emit(record)
file_handler.emit(record)
# 异步获取并记录查询结果
await self.log_query_result(record)
async def log_query_result(self, record):
"""
执行查询并返回结果。
"""
try:
from tortoise import Tortoise
connection = Tortoise.get_connection("default")
result = await connection.execute_query_dict(record.getMessage())
return result
except Exception as e:
return f"获取查询结果失败: {str(e)}"
# 添加自定义 SQL 查询日志处理器
sql_result_handler = SQLResultLogger()
sql_result_handler.setLevel(log_level)
sql_logger.addHandler(sql_result_handler)
else:
# 如果禁用日志,设置日志级别为 WARNING 以抑制大部分输出
tortoise_logger.setLevel(logging.WARNING)

539
config/env.py Normal file
View File

@@ -0,0 +1,539 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/17 22:18
# @UpdateTime : 2025/01/17 22:18
# @Author : sonder
# @File : env.py
# @Software : PyCharm
# @Comment : 本程序
import argparse
import os
import sys
from functools import lru_cache
from dotenv import load_dotenv
from pydantic_settings import BaseSettings
class AppSettings(BaseSettings):
"""
应用配置类,用于管理 FastAPI 应用的基本设置。
"""
app_env: str = 'dev'
"""
应用环境,默认为 'dev'
可选值:
- 'dev': 开发环境
- 'prod': 生产环境
"""
app_name: str = 'FasAPI'
"""
应用名称,默认为 'FasAPI'
用于标识当前应用的名称。
"""
app_root_path: str = '/dev-api'
"""
应用的根路径,默认为 '/dev-api'
用于设置 API 的根路径前缀。
"""
app_host: str = '0.0.0.0'
"""
应用绑定的主机地址,默认为 '0.0.0.0'
- '0.0.0.0' 表示监听所有网络接口。
- '127.0.0.1' 表示仅监听本地回环地址。
"""
app_port: int = 9090
"""
应用绑定的端口号,默认为 9090。
确保端口号未被其他服务占用。
"""
app_version: str = '1.0.0'
"""
应用版本号,默认为 '1.0.0'
用于标识当前应用的版本。
"""
app_reload: bool = True
"""
是否启用自动重载,默认为 True。
- True: 启用自动重载,适合开发环境。
- False: 禁用自动重载,适合生产环境。
"""
app_ip_location_query: bool = True
"""
是否启用 IP 地址地理位置查询,默认为 True。
- True: 启用 IP 地址地理位置查询功能。
- False: 禁用 IP 地址地理位置查询功能。
"""
app_same_time_login: bool = True
"""
是否允许同一用户同时登录多个设备,默认为 True。
- True: 允许同一用户同时登录多个设备。
- False: 禁止同一用户同时登录多个设备。
"""
class Config:
env_file_encoding = "utf-8" # 指定文件编码
class JwtSettings(BaseSettings):
"""
JWT 配置类,用于管理 JWTJSON Web Token相关的设置。
"""
jwt_secret_key: str = 'b01c66dc2c58dc6a0aabfe2144256be36226de378bf87f72c0c795dda67f4d55'
"""
JWT 签名密钥,默认为一个随机生成的字符串。
- 用于加密和解密 JWT 令牌。
- 生产环境中应使用更安全的密钥,并妥善保管。
"""
jwt_algorithm: str = 'HS256'
"""
JWT 签名算法,默认为 'HS256'
- 支持的算法包括:
- 'HS256': HMAC + SHA-256
- 'HS384': HMAC + SHA-384
- 'HS512': HMAC + SHA-512
- 'RS256': RSA + SHA-256
- 其他算法请参考 PyJWT 文档。
"""
jwt_salt: str = 'jwt_salt'
"""
JWT 盐值,默认为 'jwt_salt'
- 用于进一步增强 JWT 的安全性,防止彩虹表攻击。
- 生产环境中应使用更复杂的盐值。
"""
jwt_expire_minutes: int = 1440
"""
JWT 令牌的有效期(以分钟为单位),默认为 1440 分钟24 小时)。
- 超过有效期后,令牌将自动失效。
- 可以根据需求调整有效期。
"""
jwt_redis_expire_minutes: int = 30
"""
JWT 令牌在 Redis 中的缓存有效期(以分钟为单位),默认为 30 分钟。
- 用于存储已签发的 JWT 令牌,以便快速验证和吊销。
- 可以根据需求调整缓存有效期。
"""
class Config:
env_file_encoding = "utf-8" # 指定文件编码
class DataBaseSettings(BaseSettings):
"""
数据库配置类,用于管理数据库连接相关的设置。
"""
db_type: str = 'mysql'
"""
数据库类型,默认为 'mysql'
- 支持的数据库类型包括:
- 'mysql': MySQL 数据库
- 'postgresql': PostgreSQL 数据库
- 'sqlite': SQLite 数据库
"""
db_host: str = '127.0.0.1'
"""
数据库主机地址,默认为 '127.0.0.1'
- 如果数据库运行在本地,可以使用 'localhost''127.0.0.1'
- 如果数据库运行在远程服务器,请填写服务器的 IP 地址或域名。
"""
db_port: int = 3306
"""
数据库端口号,默认为 3306。
- MySQL 默认端口号为 3306。
- PostgreSQL 默认端口号为 5432。
- 根据实际数据库类型和配置调整端口号。
"""
db_username: str = 'root'
"""
数据库用户名,默认为 'root'
- 用于连接数据库的用户名。
- 生产环境中应使用具有最小权限的用户。
"""
db_password: str = 'mysqlroot'
"""
数据库密码,默认为 'mysqlroot'
- 用于连接数据库的密码。
- 生产环境中应使用强密码,并妥善保管。
"""
db_database: str = 'fastapi'
"""
数据库名称,默认为 'fastapi'
- 用于指定连接的数据库名称。
- 确保数据库已创建并具有相应的权限。
"""
db_echo: bool = True
"""
是否打印 SQL 语句,默认为 True。
- True: 打印所有执行的 SQL 语句,适合开发和调试。
- False: 不打印 SQL 语句,适合生产环境。
"""
db_max_overflow: int = 10
"""
连接池最大溢出连接数,默认为 10。
- 当连接池中的连接数达到最大值时,允许额外创建的连接数。
- 根据实际负载调整该值。
"""
db_log_level: int = 10
"""
数据库日志级别,默认为 10DEBUG
- 用于控制数据库日志的输出级别。
- 10: DEBUG
- 20: INFO
- 30: WARNING
- 40: ERROR
- 50: CRITICAL
- 根据实际需求调整日志级别。
"""
db_pool_size: int = 50
"""
连接池大小,默认为 50。
- 连接池中保持的最大连接数。
- 根据实际并发需求调整该值。
"""
db_pool_recycle: int = 3600
"""
连接池回收时间(秒),默认为 3600 秒1 小时)。
- 超过该时间的连接会被回收并重新创建。
- 用于避免数据库连接超时问题。
"""
db_pool_timeout: int = 30
"""
连接池超时时间(秒),默认为 30 秒。
- 从连接池获取连接的超时时间。
- 如果超时未获取到连接,会抛出异常。
"""
class Config:
env_file_encoding = "utf-8" # 指定文件编码
class RedisSettings(BaseSettings):
"""
Redis 配置类,用于管理 Redis 连接相关的设置。
"""
redis_host: str = '127.0.0.1'
"""
Redis 主机地址,默认为 '127.0.0.1'
- 如果 Redis 运行在本地,可以使用 'localhost''127.0.0.1'
- 如果 Redis 运行在远程服务器,请填写服务器的 IP 地址或域名。
"""
redis_port: int = 6379
"""
Redis 端口号,默认为 6379。
- Redis 默认端口号为 6379。
- 根据实际 Redis 配置调整端口号。
"""
redis_username: str = ''
"""
Redis 用户名,默认为空。
- 如果 Redis 启用了身份验证,请填写用户名。
- 如果未启用身份验证,可以留空。
"""
redis_password: str = ''
"""
Redis 密码,默认为空。
- 如果 Redis 启用了身份验证,请填写密码。
- 如果未启用身份验证,可以留空。
"""
redis_database: int = 2
"""
Redis 数据库索引,默认为 2。
- Redis 支持多个数据库,索引从 0 开始。
- 根据实际需求选择合适的数据库索引。
"""
class Config:
env_file_encoding = "utf-8" # 指定文件编码
class UploadSettings:
"""
上传配置类,用于管理文件上传和下载的相关设置。
"""
UPLOAD_PREFIX = '/profile'
"""
文件上传的 URL 前缀,默认为 '/profile'
- 用于在访问上传文件时添加到 URL 的前缀。
- 例如:`/profile/example.jpg`。
"""
UPLOAD_PATH = 'data/upload_path'
"""
文件上传的存储路径,默认为 'data/upload_path'
- 上传的文件将存储在此目录中。
- 如果目录不存在,会自动创建。
"""
UPLOAD_MACHINE = 'A'
"""
上传机器的标识,默认为 'A'
- 用于区分不同的上传机器或节点。
- 在多机部署时可以使用此字段。
"""
DEFAULT_ALLOWED_EXTENSION = [
# 图片
'bmp',
'gif',
'jpg',
'jpeg',
'png',
# word excel powerpoint
'doc',
'docx',
'xls',
'xlsx',
'ppt',
'pptx',
'html',
'htm',
'txt',
# 压缩文件
'rar',
'zip',
'gz',
'bz2',
# 视频格式
'mp4',
'avi',
'rmvb',
# pdf
'pdf',
]
"""
默认允许上传的文件扩展名列表。
- 包含常见的图片、文档、压缩文件、视频和 PDF 格式。
- 可以根据需求扩展或修改此列表。
"""
DOWNLOAD_PATH = 'data/download_path'
"""
文件下载的存储路径,默认为 'data/download_path'
- 下载的文件将存储在此目录中。
- 如果目录不存在,会自动创建。
"""
def __init__(self):
"""
初始化方法,确保上传和下载路径存在。
- 如果路径不存在,会自动创建。
"""
if not os.path.exists(self.UPLOAD_PATH):
os.makedirs(self.UPLOAD_PATH)
if not os.path.exists(self.DOWNLOAD_PATH):
os.makedirs(self.DOWNLOAD_PATH)
class EmailSettings(BaseSettings):
"""
邮件配置类,用于管理邮件发送相关的设置。
"""
email_username: str = ""
"""
邮件发送者的用户名,默认为空。
"""
email_password: str = ""
"""
邮件发送者的密码,默认为空。
"""
email_host: str = "smtp.qq.com"
"""
邮件服务器地址,默认为 "smtp.qq.com"
"""
email_port: int = 465
"""
邮件服务器端口,默认为 465。
"""
class Config:
env_file_encoding = "utf-8" # 指定文件编码
class MapSettings(BaseSettings):
"""
地图配置类,用于管理地图相关的设置。
"""
ak: str = ""
"""
控制台-应用管理-创建应用后获取的AK
"""
sk: str = ""
"""
控制台-应用管理-创建应用时校验方式选择sn校验后生成的SK
"""
class CachePathConfig:
"""
缓存目录配置
"""
PATH = os.path.join(os.path.abspath(os.getcwd()), 'caches')
PATHSTR = 'caches'
class GetConfig:
"""
获取配置类,用于集中管理和获取应用的所有配置。
"""
def __init__(self):
"""
初始化方法,解析命令行参数并加载环境变量。
"""
self.parse_cli_args()
@lru_cache()
def get_app_config(self) -> BaseSettings:
"""
获取应用配置。
- 返回 AppSettings 的实例。
- 使用 lru_cache 缓存结果,避免重复实例化。
"""
# 实例化应用配置模型
return AppSettings()
@lru_cache()
def get_jwt_config(self) -> BaseSettings:
"""
获取 JWT 配置。
- 返回 JwtSettings 的实例。
- 使用 lru_cache 缓存结果,避免重复实例化。
"""
# 实例化 JWT 配置模型
return JwtSettings()
@lru_cache()
def get_database_config(self) -> BaseSettings:
"""
获取数据库配置。
- 返回 DataBaseSettings 的实例。
- 使用 lru_cache 缓存结果,避免重复实例化。
"""
# 实例化数据库配置模型
return DataBaseSettings()
@lru_cache()
def get_redis_config(self) -> BaseSettings:
"""
获取 Redis 配置。
- 返回 RedisSettings 的实例。
- 使用 lru_cache 缓存结果,避免重复实例化。
"""
# 实例化 Redis 配置模型
return RedisSettings()
@lru_cache()
def get_upload_config(self) -> 'UploadSettings':
"""
获取上传配置。
- 返回 UploadSettings 的实例。
- 使用 lru_cache 缓存结果,避免重复实例化。
"""
# 实例化上传配置
return UploadSettings()
@lru_cache()
def get_email_config(self) -> 'EmailSettings':
"""
获取邮件配置。
- 返回 EmailSettings 的实例。
- 使用 lru_cache 缓存结果,避免重复实例化。
"""
# 实例化邮件配置
return EmailSettings()
@lru_cache()
def get_map_config(self) -> 'MapSettings':
"""
获取地图配置。
- 返回 MapSettings 的实例。
- 使用 lru_cache 缓存结果,避免重复实例化。
"""
# 实例化地图配置
return MapSettings()
@staticmethod
def parse_cli_args():
"""
解析命令行参数。
- 如果使用 uvicorn 启动,命令行参数无法自定义。
- 否则,使用 argparse 解析自定义命令行参数。
- 根据命令行参数设置环境变量,并加载对应的 .env 文件。
"""
if 'uvicorn' in sys.argv[0]:
# 使用 uvicorn 启动时,命令行参数需要按照 uvicorn 的文档进行配置,无法自定义参数
pass
else:
# 使用 argparse 定义命令行参数
parser = argparse.ArgumentParser(description='命令行参数')
parser.add_argument('--env', type=str, default='', help='运行环境')
# 解析命令行参数
args = parser.parse_args()
# 设置环境变量,如果未设置命令行参数,默认 APP_ENV 为 dev
os.environ['APP_ENV'] = args.env if args.env else 'dev'
# 读取运行环境
run_env = os.environ.get('APP_ENV', '')
# 运行环境未指定时默认加载 .env.dev
env_file = '.env.dev'
# 运行环境不为空时按命令行参数加载对应 .env 文件
if run_env != '':
env_file = f'.env.{run_env}'
# 加载配置
load_dotenv(env_file)
# 实例化获取配置类
get_config = GetConfig()
# 应用配置
AppConfig = get_config.get_app_config()
# JWT 配置
JwtConfig = get_config.get_jwt_config()
# 数据库配置
DataBaseConfig = get_config.get_database_config()
# Redis 配置
RedisConfig = get_config.get_redis_config()
# 上传配置
UploadConfig = get_config.get_upload_config()
# 邮件配置
EmailConfig = get_config.get_email_config()
# 地图配置
MapConfig = get_config.get_map_config()

68
config/get_redis.py Normal file
View File

@@ -0,0 +1,68 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 02:41
# @UpdateTime : 2025/01/18 02:41
# @Author : sonder
# @File : get_redis.py
# @Software : PyCharm
# @Comment : 本程序
from redis import asyncio as aioredis
from redis.exceptions import AuthenticationError, TimeoutError, RedisError
from config.env import RedisConfig
from utils.log import logger
class Redis:
"""
Redis工具类
提供与 Redis 交互的常用方法,包括连接管理和缓存初始化。
"""
@classmethod
async def create_redis_pool(cls) -> aioredis.Redis:
"""
应用启动时初始化 Redis 连接池。
该方法通过 `aioredis` 创建一个 Redis 连接池,并验证连接是否成功。
如果连接失败,会捕获异常并记录错误日志。
:return: Redis 连接对象
"""
logger.info('开始连接 Redis...')
redis = await aioredis.from_url(
url=f'redis://{RedisConfig.redis_host}',
port=RedisConfig.redis_port,
username=RedisConfig.redis_username,
password=RedisConfig.redis_password,
db=RedisConfig.redis_database,
encoding='utf-8', # 默认编码为 UTF-8
decode_responses=True, # 自动解码 Redis 响应
)
try:
# 测试 Redis 连接
connection = await redis.ping()
if connection:
logger.info('Redis 连接成功')
else:
logger.error('Redis 连接失败')
except AuthenticationError as e:
logger.error(f'Redis 用户名或密码错误,详细错误信息:{e}')
except TimeoutError as e:
logger.error(f'Redis 连接超时,详细错误信息:{e}')
except RedisError as e:
logger.error(f'Redis 连接错误,详细错误信息:{e}')
return redis
@classmethod
async def close_redis_pool(cls, app):
"""
应用关闭时关闭 Redis 连接池。
在应用生命周期结束时,清理 Redis 连接池以释放资源。
:param app: FastAPI 应用对象,用于访问全局 Redis 连接池。
:return: None
"""
await app.state.redis.close()
logger.info('关闭 Redis 连接成功')

7
controller/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/19 00:57
# @UpdateTime : 2025/01/19 00:57
# @Author : sonder
# @File : __init__.py.py
# @Software : PyCharm
# @Comment : 本程序

258
controller/login.py Normal file
View File

@@ -0,0 +1,258 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/19 01:42
# @UpdateTime : 2025/01/19 01:42
# @Author : sonder
# @File : login.py
# @Software : PyCharm
# @Comment : 本程序
import uuid
from datetime import timedelta, datetime
from typing import Optional, Union
from fastapi import Form, Depends, Request
from fastapi.encoders import jsonable_encoder
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import jwt
from jose.exceptions import JWEInvalidAuth, ExpiredSignatureError, JWEError
from config.constant import RedisKeyConfig
from config.env import JwtConfig
from controller.query import QueryController
from exceptions.exception import AuthException
from models import LoginLog
from schemas.login import LoginParams
from utils.log import logger
from utils.password import Password
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='login')
class CustomOAuth2PasswordRequestForm(OAuth2PasswordRequestForm):
"""
自定义OAuth2PasswordRequestForm类增加验证码及会话编号参数
"""
def __init__(
self,
grant_type: str = Form(default=None, regex='password'),
username: str = Form(..., description="用户账号"),
password: str = Form(..., description="用户密码"),
scope: str = Form(default=''),
client_id: Optional[str] = Form(default=None),
client_secret: Optional[str] = Form(default=None),
loginDays: Optional[int] = Form(default=1),
code: Optional[str] = Form(default=''),
uuid: Optional[str] = Form(default=''),
):
super().__init__(
grant_type=grant_type,
username=username,
password=password,
scope=scope,
client_id=client_id,
client_secret=client_secret,
)
self.code = code
self.uuid = uuid
self.loginDays = loginDays
class LoginController:
"""
登录控制器
"""
@classmethod
async def login(cls, params: LoginParams):
"""
登录
:param params:
:return:
"""
result = await QueryController.get_user_by_username(params.username)
if result and await Password.verify_password(params.password, result.password):
userInfo = await QueryController.get_user_info(user_id=result.id.__str__())
logger.success(f"用户 {params.username} 登录成功")
session_id = uuid.uuid4().__str__()
accessToken = await cls.create_token(
data={"user": jsonable_encoder(userInfo), "id": result.id.__str__(), "session_id": session_id},
expires_delta=timedelta(minutes=params.loginDays * 24 * 60))
expiresTime = (datetime.now() + timedelta(minutes=params.loginDays * 24 * 60)).timestamp()
refreshToken = await cls.create_token(
data={"user": jsonable_encoder(userInfo), "id": result.id.__str__(), "session_id": session_id},
expires_delta=timedelta(minutes=(params.loginDays * 24 + 2) * 60))
return {"status": True, "accessToken": accessToken, "refreshToken": refreshToken,
"userInfo": userInfo,
"expiresTime": expiresTime, "session_id": session_id, "expiresIn": params.loginDays * 24 * 60}
logger.error(f"用户 {params.username} 登录失败")
return {"status": False}
@classmethod
async def create_token(cls, data: dict, expires_delta: Union[timedelta, None] = None) -> str:
"""
创建token
:param data: 存储数据
:param expires_delta: 过期时间
:return: token
"""
to_copy = data.copy()
if expires_delta:
expire = datetime.now() + expires_delta
else:
expire = datetime.now() + timedelta(minutes=JwtConfig.jwt_expire_minutes)
to_copy.update({"exp": expire})
return jwt.encode(claims=to_copy, key=JwtConfig.jwt_secret_key, algorithm=JwtConfig.jwt_algorithm)
@classmethod
async def get_current_user(cls, request: Request = Request, token: str = Depends(oauth2_scheme)):
"""
获取当前用户
:param request:
:param token:
:return:
"""
try:
if token.startswith('Bearer'):
token = token.split(' ')[1]
payload = jwt.decode(token=token, key=JwtConfig.jwt_secret_key, algorithms=[JwtConfig.jwt_algorithm])
user_id: str = payload.get("id", "")
session_id: str = payload.get('session_id', "")
if not user_id:
logger.warning('用户token不合法')
raise AuthException(data='', message='用户token不合法')
except (JWEInvalidAuth, ExpiredSignatureError, JWEError):
logger.warning('用户token已失效请重新登录')
raise AuthException(data='', message='用户token已失效请重新登录')
userInfo = await request.app.state.redis.get(f'{RedisKeyConfig.USER_INFO.key}:{user_id}')
if userInfo:
userInfo = eval(userInfo)
if not userInfo:
userInfo = await QueryController.get_user_info(user_id=user_id)
await request.app.state.redis.set(f'{RedisKeyConfig.USER_INFO.key}:{user_id}',
str(jsonable_encoder(userInfo)),
ex=timedelta(minutes=5))
if not userInfo:
logger.warning('用户token不合法')
raise AuthException(data='', message='用户token不合法')
redis_token = await request.app.state.redis.get(f'{RedisKeyConfig.ACCESS_TOKEN.key}:{session_id}')
if not redis_token:
logger.warning('用户token已失效请重新登录')
raise AuthException(data='', message='用户token已失效请重新登录')
return userInfo
@classmethod
async def logout(cls, request: Request = Request, token: str = Depends(oauth2_scheme)) -> bool:
"""
登出
"""
try:
if token.startswith('Bearer'):
token = token.split(' ')[1]
payload = jwt.decode(token=token, key=JwtConfig.jwt_secret_key, algorithms=[JwtConfig.jwt_algorithm])
session_id: str = payload.get('session_id', "")
except (JWEInvalidAuth, ExpiredSignatureError, JWEError):
logger.warning('用户token已失效请重新登录')
raise AuthException(data='', message='用户token已失效请重新登录')
redis_token = await request.app.state.redis.get(f'{RedisKeyConfig.ACCESS_TOKEN.key}:{session_id}')
if redis_token == token:
await request.app.state.redis.delete(f'{RedisKeyConfig.ACCESS_TOKEN.key}:{session_id}')
return True
return False
@classmethod
async def get_user_routes(cls, user_id: str) -> Union[list, None]:
"""
获取用户路由
"""
permissions = await QueryController.get_user_permissions(user_id=user_id)
for permission in permissions:
permission["id"] = str(permission["id"])
permission["parentId"] = str(permission["parentId"]) if permission.get("parentId") else ""
permissions = await cls.find_complete_data(permissions)
return permissions
@classmethod
async def find_node_recursive(cls, node_id: str, data: list) -> dict:
"""
递归查找节点
:param node_id: 节点ID
:param data: 数据
"""
result = {}
data = list(filter(lambda x: x.get('type') == 0, data))
for item in data:
if item["id"] == node_id:
children = []
for child_item in data:
if child_item["parentId"] == node_id:
child_node = await cls.find_node_recursive(child_item["id"], data)
if child_node:
children.append(child_node)
result = {
"name": item["name"],
"path": item["path"],
"meta": {
"title": item["title"],
"rank": item["rank"],
"icon": item["icon"],
"permissions": [item["auths"]],
},
"children": children
}
if item["component"]:
result["component"] = item["component"].replace(".vue", "").replace(".ts", "").replace(".tsx",
"").replace(
".js", "").replace(".jsx", "").strip()
if item["redirect"]:
result["redirect"] = item["redirect"]
if result["name"] == "":
result.pop("name")
if result["children"] == []:
result.pop("children")
break
return result
@classmethod
async def find_complete_data(cls, data: list) -> list:
"""
查找完整数据
:param data: 数据
"""
complete_data = []
root_ids = [item["id"] for item in data if not item["parentId"]]
for root_id in root_ids:
complete_data.append(await cls.find_node_recursive(root_id, data))
return complete_data
@classmethod
async def get_online_user(cls, request: Request) -> list:
"""
获取在线用户
"""
access_token_keys = await request.app.state.redis.keys(f'{RedisKeyConfig.ACCESS_TOKEN.key}*')
if not access_token_keys:
access_token_keys = []
access_token_values_list = [await request.app.state.redis.get(key) for key in access_token_keys]
online_info_list = []
for item in access_token_values_list:
payload = jwt.decode(item, JwtConfig.jwt_secret_key, algorithms=[JwtConfig.jwt_algorithm])
session_id = payload.get("session_id")
result = await LoginLog.get_or_none(session_id=session_id).values(
id="id",
user_id="user__id",
username="user__username",
user_nickname="user__nickname",
department_id="user__department__id",
department_name="user__department__name",
login_ip="login_ip",
login_location="login_location",
browser="browser",
os="os",
status="status",
login_time="login_time",
session_id="session_id",
create_time="create_time",
update_time="update_time"
)
online_info_list.append(result)
return online_info_list

132
controller/query.py Normal file
View File

@@ -0,0 +1,132 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/23 21:22
# @UpdateTime : 2025/01/23 21:22
# @Author : sonder
# @File : query.py
# @Software : PyCharm
# @Comment : 本程序
from typing import Union
from tortoise.expressions import Q
from models import User, UserRole, RolePermission
from utils.common import filterKeyValues
class QueryController:
"""
数据库查询控制器
"""
@classmethod
async def get_user_by_username(cls, username: str) -> Union[User, None]:
"""
根据用户名获取用户信息
:param username:用户名|手机号|邮箱
:return:
"""
return await User.get_or_none(Q(username=username) | Q(email=username) | Q(phone=username), del_flag=1)
@classmethod
async def get_user_by_id(cls, user_id: str) -> Union[User, None]:
"""
:param user_id:
:return:
"""
return await User.get_or_none(id=user_id, del_flag=1)
@classmethod
async def get_user_info(cls, user_id: str) -> Union[dict, None]:
"""
:param user_id:用户ID
"""
# 获取用户信息
userInfo = await User.get_or_none(id=user_id, del_flag=1).values(
id="id",
username="username",
phone="phone",
email="email",
avatar="avatar",
nickname="nickname",
gender="gender",
status="status",
create_time="create_time",
update_time="update_time",
department_id="department__id",
department_name="department__name"
)
# 获取用户角色
userRoles = await UserRole.filter(user_id=user_id, del_flag=1).values(
role_id="role__id",
role_name="role__name",
role_code="role__code"
)
# 获取用户角色标识
userRole = await filterKeyValues(userRoles, "role_code")
# 获取用户角色ID
userRoleIds = await filterKeyValues(userRoles, "role_id")
# 根据用户角色ID获取用户权限
permissions = []
for item in userRoleIds:
permission = await RolePermission.filter(role_id=item, del_flag=1).values(
permission_id="permission__id",
permission_name="permission__name",
permission_auths="permission__auths"
)
permissions.extend(permission)
permissions = await filterKeyValues(permissions, "permission_auths")
permissions = list(set(permissions))
userInfo["roles"] = userRole
userInfo["permissions"] = permissions
return userInfo
@classmethod
async def register_user_before(cls, username: str, phone: str, email: str) -> Union[User, None]:
"""
注册用户前,检查用户名、手机号、邮箱是否已存在
:param phone:
:param username:
:param email:
:return:
"""
return await User.get_or_none(Q(username=username) | Q(email=email) | Q(phone=phone), del_flag=1)
@classmethod
async def get_user_permissions(cls, user_id: str) -> Union[list, None]:
"""
获取用户权限
"""
# 获取用户角色
userRoles = await UserRole.filter(user_id=user_id, del_flag=1).values(
role_id="role__id",
role_name="role__name",
role_code="role__code"
)
# 获取用户角色ID
userRoleIds = await filterKeyValues(userRoles, "role_id")
# 根据用户角色ID获取用户权限
permissions = []
for item in userRoleIds:
permission = await RolePermission.filter(role_id=item, del_flag=1).values(
id="permission__id",
parentId="permission__parent_id",
name="permission__name",
title="permission__title",
type="permission__menu_type",
path="permission__path",
component="permission__component",
redirect="permission__redirect",
rank="permission__rank",
icon="permission__icon",
extraIcon="permission__extra_icon",
enterTransition="permission__enter_transition",
leaveTransition="permission__leave_transition",
activePath="permission__active_path",
auths="permission__auths",
keepAlive="permission__keep_alive",
hiddenTag="permission__hidden_tag",
showLink="permission__show_link",
showParent="permission__show_parent"
)
permissions.extend(permission)
return permissions

7
exceptions/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 01:58
# @UpdateTime : 2025/01/18 01:58
# @Author : sonder
# @File : __init__.py.py
# @Software : PyCharm
# @Comment : 本程序

162
exceptions/exception.py Normal file
View File

@@ -0,0 +1,162 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 02:21
# @UpdateTime : 2025/01/18 02:21
# @Author : sonder
# @File : exception.py
# @Software : PyCharm
# @Comment : 本程序
from typing import Optional, Any
from fastapi.responses import JSONResponse
from utils.response import Response
class LoginException(Exception):
"""
自定义登录异常。
- 用于处理登录相关的异常情况。
"""
def __init__(self, message: str = "登录失败", data: Optional[Any] = None):
"""
初始化登录异常。
:param message: 异常消息,默认为 "登录失败"
:param data: 异常相关的数据
"""
self.message = message
self.data = data
def to_response(self) -> JSONResponse:
"""
将异常转换为统一的响应格式。
:return: JSONResponse 对象
"""
return Response.failure(msg=self.message, data=self.data)
class AuthException(Exception):
"""
自定义令牌异常。
- 用于处理身份验证相关的异常情况。
"""
def __init__(self, message: str = "身份验证失败", data: Optional[Any] = None):
"""
初始化令牌异常。
:param message: 异常消息,默认为 "身份验证失败"
:param data: 异常相关的数据
"""
self.message = message
self.data = data
def to_response(self) -> JSONResponse:
"""
将异常转换为统一的响应格式。
:return: JSONResponse 对象
"""
return Response.unauthorized(msg=self.message, data=self.data)
class PermissionException(Exception):
"""
自定义权限异常。
- 用于处理权限相关的异常情况。
"""
def __init__(self, message: str = "权限不足", data: Optional[Any] = None):
"""
初始化权限异常。
:param message: 异常消息,默认为 "权限不足"
:param data: 异常相关的数据
"""
self.message = message
self.data = data
def to_response(self) -> JSONResponse:
"""
将异常转换为统一的响应格式。
:return: JSONResponse 对象
"""
return Response.forbidden(msg=self.message, data=self.data)
class ServiceException(Exception):
"""
自定义服务异常。
- 用于处理服务层逻辑相关的异常情况。
"""
def __init__(self, message: str = "服务异常", data: Optional[Any] = None):
"""
初始化服务异常。
:param message: 异常消息,默认为 "服务异常"
:param data: 异常相关的数据
"""
self.message = message
self.data = data
def to_response(self) -> JSONResponse:
"""
将异常转换为统一的响应格式。
:return: JSONResponse 对象
"""
return Response.error(msg=self.message, data=self.data)
class ServiceWarning(Exception):
"""
自定义服务警告。
- 用于处理服务层逻辑中的警告情况(非致命错误)。
"""
def __init__(self, message: str = "服务警告", data: Optional[Any] = None):
"""
初始化服务警告。
:param message: 警告消息,默认为 "服务警告"
:param data: 警告相关的数据
"""
self.message = message
self.data = data
def to_response(self) -> JSONResponse:
"""
将警告转换为统一的响应格式。
:return: JSONResponse 对象
"""
return Response.failure(msg=self.message, data=self.data)
class ModelValidatorException(Exception):
"""
自定义模型校验异常。
- 用于处理数据模型校验失败的异常情况。
"""
def __init__(self, message: str = "数据校验失败", data: Optional[Any] = None):
"""
初始化模型校验异常。
:param message: 异常消息,默认为 "数据校验失败"
:param data: 异常相关的数据
"""
self.message = message
self.data = data
def to_response(self) -> JSONResponse:
"""
将异常转换为统一的响应格式。
:return: JSONResponse 对象
"""
return Response.failure(msg=self.message, data=self.data)

134
exceptions/handle.py Normal file
View File

@@ -0,0 +1,134 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 02:24
# @UpdateTime : 2025/01/18 02:24
# @Author : sonder
# @File : handle.py
# @Software : PyCharm
# @Comment : 本程序
from fastapi import FastAPI, Request
from fastapi.exceptions import HTTPException
from pydantic_validation_decorator import FieldValidationError
from starlette.responses import JSONResponse
from exceptions.exception import (
AuthException,
LoginException,
ModelValidatorException,
PermissionException,
ServiceException,
ServiceWarning,
)
from utils.log import logger
from utils.response import Response
def handle_exception(app: FastAPI):
"""
全局异常处理拦截器。
- 捕获并处理所有异常,返回统一的接口响应格式。
"""
@app.exception_handler(AuthException)
async def auth_exception_handler(request: Request, exc: AuthException):
"""
处理自定义身份验证异常AuthException
- 返回 401 未授权响应。
"""
logger.warning(f"身份验证异常: {exc.message}")
return Response.unauthorized(data=exc.data, msg=exc.message)
@app.exception_handler(LoginException)
async def login_exception_handler(request: Request, exc: LoginException):
"""
处理自定义登录异常LoginException
- 返回 400 失败响应。
"""
logger.warning(f"登录异常: {exc.message}")
return Response.failure(data=exc.data, msg=exc.message)
@app.exception_handler(ModelValidatorException)
async def model_validator_exception_handler(request: Request, exc: ModelValidatorException):
"""
处理自定义模型校验异常ModelValidatorException
- 返回 400 失败响应。
"""
logger.warning(f"模型校验异常: {exc.message}")
return Response.failure(data=exc.data, msg=exc.message)
@app.exception_handler(FieldValidationError)
async def field_validation_error_handler(request: Request, exc: FieldValidationError):
"""
处理字段校验异常FieldValidationError
- 返回 400 失败响应。
"""
logger.warning(f"字段校验异常: {exc.message}")
return Response.failure(msg=exc.message)
@app.exception_handler(PermissionException)
async def permission_exception_handler(request: Request, exc: PermissionException):
"""
处理自定义权限异常PermissionException
- 返回 403 未授权响应。
"""
logger.warning(f"权限异常: {exc.message}")
return Response.forbidden(data=exc.data, msg=exc.message)
@app.exception_handler(ServiceException)
async def service_exception_handler(request: Request, exc: ServiceException):
"""
处理自定义服务异常ServiceException
- 返回 500 错误响应。
"""
logger.error(f"服务异常: {exc.message}")
return Response.error(data=exc.data, msg=exc.message)
@app.exception_handler(ServiceWarning)
async def service_warning_handler(request: Request, exc: ServiceWarning):
"""
处理自定义服务警告ServiceWarning
- 返回 400 失败响应。
"""
logger.warning(f"服务警告: {exc.message}")
return Response.failure(data=exc.data, msg=exc.message)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
"""
处理 FastAPI 的 HTTP 异常。
- 返回统一的错误响应格式。
"""
logger.warning(f"HTTP 异常: {exc.detail}")
return Response.failure(msg=exc.detail)
@app.exception_handler(Exception)
async def exception_handler(request: Request, exc: Exception):
"""
处理其他未捕获的异常。
- 返回 500 错误响应。
"""
logger.exception(f"未捕获的异常: {str(exc)}")
return Response.error(msg="服务器内部错误")
@app.exception_handler(404)
async def not_found_exception_handler(request: Request, exc: HTTPException):
"""
处理 404 未找到资源异常。
- 返回 404 失败响应。
"""
logger.warning(f"404 异常: {request.url} 未找到")
return JSONResponse(
content={"code": 404, "msg": "无效路径!", "data": None},
status_code=404,
)
@app.exception_handler(405)
async def method_not_allowed_handler(request: Request, exc: HTTPException):
"""
处理 405 方法不允许异常。
- 返回 405 失败响应。
"""
logger.warning(f"405 异常: {request.method} 方法不允许")
return JSONResponse(
status_code=405,
content={"code": 405, "msg": "请求方法错误", "data": None},
)

7
middlewares/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 02:44
# @UpdateTime : 2025/01/18 02:44
# @Author : sonder
# @File : __init__.py.py
# @Software : PyCharm
# @Comment : 本程序

31
middlewares/cors.py Normal file
View File

@@ -0,0 +1,31 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 02:44
# @UpdateTime : 2025/01/18 02:44
# @Author : sonder
# @File : cors.py
# @Software : PyCharm
# @Comment : 本程序
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
def add_cors_middleware(app: FastAPI):
"""
添加跨域中间件
:param app: FastAPI对象
:return:
"""
# 前端页面url
origins = [
"*"
]
# 后台api允许跨域
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
)

19
middlewares/gzip.py Normal file
View File

@@ -0,0 +1,19 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 02:45
# @UpdateTime : 2025/01/18 02:45
# @Author : sonder
# @File : gzip.py
# @Software : PyCharm
# @Comment : 本程序
from fastapi import FastAPI
from starlette.middleware.gzip import GZipMiddleware
def add_gzip_middleware(app: FastAPI):
"""
添加gzip压缩中间件
:param app: FastAPI对象
:return:
"""
app.add_middleware(GZipMiddleware, minimum_size=1000, compresslevel=9)

21
middlewares/handle.py Normal file
View File

@@ -0,0 +1,21 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 02:45
# @UpdateTime : 2025/01/18 02:45
# @Author : sonder
# @File : handle.py
# @Software : PyCharm
# @Comment : 本程序
from fastapi import FastAPI
from middlewares.cors import add_cors_middleware
from middlewares.gzip import add_gzip_middleware
def handle_middleware(app: FastAPI):
"""
全局中间件处理
"""
# 加载跨域中间件
add_cors_middleware(app)
# 加载gzip压缩中间件
add_gzip_middleware(app)

30
models/__init__.py Normal file
View File

@@ -0,0 +1,30 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 02:42
# @UpdateTime : 2025/01/18 02:42
# @Author : sonder
# @File : __init__.py.py
# @Software : PyCharm
# @Comment : 本程序
from models.department import Department, DepartmentRole
from models.file import File
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',
'DepartmentRole',
'File',
'LoginLog',
'OperationLog',
'Permission',
'Role',
'RolePermission',
'User',
'UserRole',
'I18n',
'Locale'
]

59
models/common.py Normal file
View File

@@ -0,0 +1,59 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 02:43
# @UpdateTime : 2025/01/18 02:43
# @Author : sonder
# @File : common.py
# @Software : PyCharm
# @Comment : 本程序
from tortoise import fields, models
class BaseModel(models.Model):
"""
抽象模型,用于定义数据表的公共字段。
"""
id = fields.UUIDField(pk=True, description="主键", autoincrement=True)
"""
自增 UUID作为主键。
- 使用 UUIDField 生成唯一标识符。
"""
del_flag = fields.SmallIntField(default=1, description="删除标志 1存在 0删除")
"""
删除标志。
- 1 代表存在0 代表删除。
- 默认为 1。
"""
create_by = fields.CharField(max_length=255, default='', description="创建者")
"""
创建者。
- 默认为空字符串。
"""
create_time = fields.DatetimeField(auto_now_add=True, description="创建时间", null=True)
"""
创建时间。
- 自动设置为当前时间。
- 允许为空null=True
"""
update_by = fields.CharField(max_length=255, default='', description="更新者")
"""
更新者。
- 默认为空字符串。
"""
update_time = fields.DatetimeField(auto_now=True, description="更新时间", null=True)
"""
更新时间。
- 自动更新为当前时间。
- 允许为空null=True
"""
class Meta:
abstract = True # 标记为抽象类,不会创建对应的数据库表
ordering = ["-create_time"] # 默认按创建时间倒序排序
indexes = ("del_flag",) # 为 del_flag 字段创建索引

155
models/department.py Normal file
View File

@@ -0,0 +1,155 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 03:21
# @UpdateTime : 2025/01/18 03:21
# @Author : sonder
# @File : department.py
# @Software : PyCharm
# @Comment : 本程序
from tortoise import fields
from models.common import BaseModel
class Department(BaseModel):
"""
部门表模型。
"""
name = fields.CharField(
max_length=100,
description="部门名称",
source_field="name" # 映射到数据库字段 name
)
"""
部门名称。
- 最大长度为 100 个字符。
- 映射到数据库字段 name。
"""
parent_id = fields.CharField(
max_length=36,
default="",
null=True,
description="父部门ID",
source_field="parent_id" # 映射到数据库字段 parent_id
)
"""
父部门ID。
- 用于表示部门的层级关系。
- 默认为空字符串,表示顶级部门。
- 映射到数据库字段 parent_id。
"""
sort = fields.IntField(
default=0,
description="排序权重0最高",
source_field="sort" # 映射到数据库字段 sort
)
"""
排序权重。
- 用于部门列表的排序,值越小越靠前。
- 默认为 0。
- 映射到数据库字段 sort。
"""
phone = fields.CharField(
max_length=30,
null=True,
description="部门电话",
source_field="phone" # 映射到数据库字段 phone
)
"""
部门电话。
- 最大长度为 30 个字符。
- 允许为空。
- 映射到数据库字段 phone。
"""
principal = fields.CharField(
max_length=64,
description="部门负责人",
source_field="principal" # 映射到数据库字段 principal
)
"""
部门负责人。
- 最大长度为 64 个字符。
- 映射到数据库字段 principal。
"""
email = fields.CharField(
max_length=128,
null=True,
description="部门邮箱",
source_field="email" # 映射到数据库字段 email
)
"""
部门邮箱。
- 最大长度为 128 个字符。
- 允许为空。
- 映射到数据库字段 email。
"""
status = fields.SmallIntField(
default=1,
description="状态0正常 1停用",
source_field="status" # 映射到数据库字段 status
)
"""
状态。
- 1 表示正常0 表示停用。
- 默认为 1。
- 映射到数据库字段 status。
"""
remark = fields.CharField(
max_length=255,
null=True,
description="备注信息",
source_field="remark" # 映射到数据库字段 remark
)
"""
备注信息。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 remark。
"""
class Meta:
table = "department" # 数据库表名
table_description = "部门表" # 表描述
ordering = ["sort", "-create_time"] # 默认按排序权重和创建时间排序
class DepartmentRole(BaseModel):
"""
部门角色表模型。
"""
department = fields.ForeignKeyField(
"models.Department",
related_name="department_roles",
description="部门ID",
source_field="department_id" # 映射到数据库字段 department_id
)
"""
部门ID。
- 外键关联到 Department 表。
- 映射到数据库字段 department_id。
"""
role = fields.ForeignKeyField(
"models.Role",
related_name="department_roles",
description="角色ID",
source_field="role_id" # 映射到数据库字段 role_id
)
"""
角色ID。
- 外键关联到 Role 表。
- 映射到数据库字段 role_id。
"""
class Meta:
table = "department_role" # 数据库表名
table_description = "部门角色表" # 表描述
unique_together = (("department_id", "role_id"),) # 唯一约束,防止重复分配

93
models/file.py Normal file
View File

@@ -0,0 +1,93 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/19 00:46
# @UpdateTime : 2025/01/19 00:46
# @Author : sonder
# @File : file.py
# @Software : PyCharm
# @Comment : 本程序
from tortoise import fields
from models.common import BaseModel
class File(BaseModel):
"""
文件表模型。
"""
name = fields.CharField(
max_length=255,
description="文件名",
source_field="name" # 映射到数据库字段 name
)
"""
文件名。
- 包括文件扩展名。
- 最大长度为 255 个字符。
- 映射到数据库字段 name。
"""
size = fields.BigIntField(
description="文件大小(字节)",
source_field="size" # 映射到数据库字段 size
)
"""
文件大小。
- 单位:字节。
- 映射到数据库字段 size。
"""
file_type = fields.CharField(
max_length=100,
description="文件类型",
source_field="file_type" # 映射到数据库字段 file_type
)
"""
文件类型。
- 例如image/png、application/pdf。
- 最大长度为 100 个字符。
- 映射到数据库字段 file_type。
"""
absolute_path = fields.CharField(
max_length=512,
description="绝对路径",
source_field="absolute_path" # 映射到数据库字段 absolute_path
)
"""
绝对路径。
- 文件在服务器上的绝对路径。
- 最大长度为 512 个字符。
- 映射到数据库字段 absolute_path。
"""
relative_path = fields.CharField(
max_length=512,
description="相对路径",
source_field="relative_path" # 映射到数据库字段 relative_path
)
"""
相对路径。
- 文件相对于某个根目录的相对路径。
- 最大长度为 512 个字符。
- 映射到数据库字段 relative_path。
"""
uploader = fields.ForeignKeyField(
"models.User",
related_name="uploaded_files",
null=True, # 允许为空
description="上传人员",
source_field="uploader_id" # 映射到数据库字段 uploader_id
)
"""
上传人员。
- 外键关联到 User 表。
- 允许为空(例如系统自动上传的文件)。
- 映射到数据库字段 uploader_id。
"""
class Meta:
table = "file" # 数据库表名
table_description = "文件表" # 表描述
ordering = ["-create_time"] # 默认按创建时间倒序排序

87
models/i18n.py Normal file
View File

@@ -0,0 +1,87 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/02/04 15:43
# @UpdateTime : 2025/02/04 15:43
# @Author : sonder
# @File : i18n.py
# @Software : PyCharm
# @Comment : 本程序
from tortoise import fields
from models.common import BaseModel
class Locale(BaseModel):
"""
语言模型.
"""
code = fields.CharField(
max_length=10,
description="语言代码",
source_field="code",
unique=True
)
"""
语言代码。
- 例如en英语、zh中文、fr法语
- 最大长度为10个字符。
- 映射到数据库字段 code
"""
name = fields.CharField(
max_length=50,
description="语言名称",
source_field="name",
unique=True
)
"""
语言名称。
- 最大长度为50个字符。
- 映射到数据库字段 name
"""
class Meta:
table = "locale"
table_description = "语言表"
class I18n(BaseModel):
"""
国际化模型.
"""
key = fields.CharField(
max_length=255,
description="国际化key",
source_field="key"
)
"""
国际化key。
- 最大长度为255个字符。
- 映射到数据库字段 key
"""
locale = fields.ForeignKeyField(
"models.Locale",
related_name="i18n",
description="语言",
source_field="locale_id"
)
"""
语言。
- 关联到 Locale 模型。
- 映射到数据库字段 locale_id
"""
translation = fields.TextField(
description="翻译内容",
source_field="translation"
)
"""
翻译内容。
- 存储具体的翻译文本。
- 映射到数据库字段 translation
"""
class Meta:
table = "i18n"
table_description = "国际化表"

345
models/log.py Normal file
View File

@@ -0,0 +1,345 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/19 00:03
# @UpdateTime : 2025/01/19 00:03
# @Author : sonder
# @File : log.py
# @Software : PyCharm
# @Comment : 本程序
from tortoise import fields
from models.common import BaseModel
class LoginLog(BaseModel):
"""
系统访问记录表模型。
"""
user = fields.ForeignKeyField(
"models.User",
related_name="login_logs",
description="用户ID",
source_field="user_id" # 映射到数据库字段 user_id
)
"""
用户ID。
- 外键关联到 User 表。
- 映射到数据库字段 user_id。
"""
login_ip = fields.CharField(
max_length=50,
description="登录IP地址",
source_field="login_ip" # 映射到数据库字段 login_ip
)
"""
登录IP地址。
- 最大长度为 50 个字符。
- 映射到数据库字段 login_ip。
"""
login_location = fields.CharField(
max_length=255,
null=True,
description="登录地点",
source_field="login_location" # 映射到数据库字段 login_location
)
"""
登录地点。
- 根据 IP 地址解析的地理位置信息。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 login_location。
"""
browser = fields.CharField(
max_length=255,
null=True,
description="浏览器类型",
source_field="browser" # 映射到数据库字段 browser
)
"""
浏览器类型。
- 记录用户登录时使用的浏览器类型。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 browser。
"""
os = fields.CharField(
max_length=255,
null=True,
description="操作系统",
source_field="os" # 映射到数据库字段 os
)
"""
操作系统。
- 记录用户登录时使用的操作系统。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 os。
"""
status = fields.SmallIntField(
default=1,
description="登录状态1成功0失败",
source_field="status" # 映射到数据库字段 status
)
"""
登录状态。
- 1成功
- 0失败
- 默认为 1。
- 映射到数据库字段 status。
"""
login_time = fields.DatetimeField(
auto_now_add=True,
description="登录时间",
source_field="login_time" # 映射到数据库字段 login_time
)
"""
登录时间。
- 自动设置为当前时间。
- 映射到数据库字段 login_time。
"""
session_id = fields.CharField(
max_length=36,
null=True,
description="会话ID",
source_field="session_id"
)
"""
会话ID。
- 记录用户登录时的会话ID。
- 允许为空。
- 映射到数据库字段 session_id。
"""
class Meta:
table = "login_log" # 数据库表名
table_description = "系统访问记录表" # 表描述
ordering = ["-login_time"] # 默认按登录时间倒序排序
class OperationLog(BaseModel):
"""
操作日志表模型。
"""
operation_name = fields.CharField(
max_length=255,
description="操作名称",
source_field="operation_name" # 映射到数据库字段 operation_name
)
"""
操作名称。
- 最大长度为 255 个字符。
- 映射到数据库字段 operation_name。
"""
operation_type = fields.SmallIntField(
description="操作类型(增删改查)",
source_field="operation_type" # 映射到数据库字段 operation_type
)
"""
操作类型。
- 增、删、改、查等操作类型。
- 最大长度为 50 个字符。
- 映射到数据库字段 operation_type。
"""
request_path = fields.TextField(
description="请求路径",
source_field="request_path" # 映射到数据库字段 request_path
)
"""
请求路径。
- 记录用户请求的 API 路径。
- 最大长度为 255 个字符。
- 映射到数据库字段 request_path。
"""
request_method = fields.CharField(
max_length=10,
description="请求方法",
source_field="request_method" # 映射到数据库字段 request_method
)
"""
请求方法。
- 记录用户请求的 HTTP 方法(如 GET、POST、PUT、DELETE
- 最大长度为 10 个字符。
- 映射到数据库字段 request_method。
"""
operator = fields.ForeignKeyField(
"models.User",
related_name="operation_logs",
null=True, # 允许操作人员为空
description="操作人员",
source_field="operator_id" # 映射到数据库字段 operator_id
)
"""
操作人员。
- 外键关联到 User 表。
- 允许为空。
- 映射到数据库字段 operator_id。
"""
department = fields.ForeignKeyField(
"models.Department",
related_name="operation_logs",
null=True, # 允许操作人员为空
description="操作人员所属部门",
source_field="department_id" # 映射到数据库字段 department_id
)
"""
操作人员所属部门。
- 外键关联到 Department 表。
- 允许为空。
- 映射到数据库字段 department_id。
"""
department_name = fields.CharField(
max_length=255,
description="部门名称",
source_field="department_name" # 映射到数据库字段 department_name
)
"""
部门名称。
- 记录操作人员所属的部门名称。
- 最大长度为 255 个字符。
- 映射到数据库字段 department_name。
"""
host = fields.CharField(
max_length=50,
description="主机地址",
source_field="host" # 映射到数据库字段 host
)
"""
主机地址。
- 记录用户请求的 IP 地址。
- 最大长度为 50 个字符。
- 映射到数据库字段 host。
"""
location = fields.CharField(
max_length=255,
null=True,
description="操作地点",
source_field="location" # 映射到数据库字段 location
)
"""
操作地点。
- 根据 IP 地址解析的地理位置信息。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 location。
"""
user_agent = fields.TextField(
null=True,
description="用户请求头",
source_field="user_agent" # 映射到数据库字段 user_agent
)
"""
用户请求头。
- 记录用户请求的 User-Agent 信息。
- 允许为空。
- 映射到数据库字段 user_agent。
"""
browser = fields.CharField(
max_length=255,
null=True,
description="浏览器类型",
source_field="browser" # 映射到数据库字段 browser
)
"""
浏览器类型。
- 记录用户登录时使用的浏览器类型。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 browser。
"""
os = fields.CharField(
max_length=255,
null=True,
description="操作系统",
source_field="os" # 映射到数据库字段 os
)
"""
操作系统。
- 记录用户登录时使用的操作系统。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 os。
"""
request_params = fields.TextField(
null=True,
description="请求参数",
source_field="request_params" # 映射到数据库字段 request_params
)
"""
请求参数。
- 记录用户请求的参数任意格式如字符串、JSON、XML 等)。
- 允许为空。
- 映射到数据库字段 request_params。
"""
response_result = fields.TextField(
null=True,
description="返回结果",
source_field="response_result" # 映射到数据库字段 response_result
)
"""
返回结果。
- 记录操作的返回结果任意格式如字符串、JSON、XML 等)。
- 允许为空。
- 映射到数据库字段 response_result。
"""
status = fields.SmallIntField(
default=1,
description="操作状态1成功0失败",
source_field="status" # 映射到数据库字段 status
)
"""
操作状态。
- 1成功
- 0失败
- 默认为 1。
- 映射到数据库字段 status。
"""
operation_time = fields.DatetimeField(
auto_now_add=True,
description="操作时间",
source_field="operation_time" # 映射到数据库字段 operation_time
)
"""
操作时间。
- 自动设置为当前时间。
- 映射到数据库字段 operation_time。
"""
cost_time = fields.FloatField(
default=0,
description="消耗时间(毫秒)",
source_field="cost_time" # 映射到数据库字段 cost_time
)
"""
消耗时间。
- 记录操作消耗的时间(单位:毫秒)。
- 默认为 0。
- 映射到数据库字段 cost_time。
"""
class Meta:
table = "operation_log" # 数据库表名
table_description = "操作日志表" # 表描述
ordering = ["-operation_time"] # 默认按操作时间倒序排序

286
models/permission.py Normal file
View File

@@ -0,0 +1,286 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 03:40
# @UpdateTime : 2025/01/18 03:40
# @Author : sonder
# @File : permission.py
# @Software : PyCharm
# @Comment : 本程序
from tortoise import fields
from models.common import BaseModel
class Permission(BaseModel):
"""
权限表模型。
"""
menu_type = fields.SmallIntField(
description="菜单类型0菜单、1iframe、2外链、3按钮",
source_field="menu_type" # 映射到数据库字段 menu_type
)
"""
菜单类型。
- 0菜单
- 1iframe
- 2外链
- 3按钮
- 映射到数据库字段 menu_type。
"""
parent_id = fields.CharField(
max_length=36,
default="",
description="父权限ID",
source_field="parent_id" # 映射到数据库字段 parent_id
)
"""
父权限ID。
- 用于表示权限的层级关系。
- 默认为空字符串,表示顶级权限。
- 映射到数据库字段 parent_id。
"""
title = fields.CharField(
max_length=255,
description="菜单名称",
source_field="title" # 映射到数据库字段 title
)
"""
菜单名称。
- 兼容国际化、非国际化。
- 最大长度为 255 个字符。
- 映射到数据库字段 title。
"""
name = fields.CharField(
max_length=255,
null=True,
description="路由名称",
source_field="name" # 映射到数据库字段 name
)
"""
路由名称。
- 必须唯一,并且与前端路由组件的 `name` 保持一致。
- 最大长度为 255 个字符。
- 映射到数据库字段 name。
"""
path = fields.CharField(
max_length=255,
description="路由路径",
source_field="path" # 映射到数据库字段 path
)
"""
路由路径。
- 最大长度为 255 个字符。
- 映射到数据库字段 path。
"""
component = fields.CharField(
max_length=255,
null=True,
description="组件路径",
source_field="component" # 映射到数据库字段 component
)
"""
组件路径。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 component。
"""
rank = fields.IntField(
default=1,
description="菜单排序",
source_field="rank" # 映射到数据库字段 rank
)
"""
菜单排序。
- 平台规定只有 `home` 路由的 `rank` 才能为 0。
- 默认为 1。
- 映射到数据库字段 rank。
"""
redirect = fields.CharField(
max_length=255,
null=True,
description="路由重定向",
source_field="redirect" # 映射到数据库字段 redirect
)
"""
路由重定向。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 redirect。
"""
icon = fields.CharField(
max_length=255,
null=True,
description="菜单图标",
source_field="icon" # 映射到数据库字段 icon
)
"""
菜单图标。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 icon。
"""
extra_icon = fields.CharField(
max_length=255,
null=True,
description="右侧图标",
source_field="extra_icon" # 映射到数据库字段 extra_icon
)
"""
右侧图标。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 extra_icon。
"""
enter_transition = fields.CharField(
max_length=255,
null=True,
description="进场动画",
source_field="enter_transition" # 映射到数据库字段 enter_transition
)
"""
进场动画。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 enter_transition。
"""
leave_transition = fields.CharField(
max_length=255,
null=True,
description="离场动画",
source_field="leave_transition" # 映射到数据库字段 leave_transition
)
"""
离场动画。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 leave_transition。
"""
active_path = fields.CharField(
max_length=255,
null=True,
description="菜单激活路径",
source_field="active_path" # 映射到数据库字段 active_path
)
"""
菜单激活路径。
- 用于指定激活菜单的路径。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 active_path。
"""
auths = fields.CharField(
max_length=255,
null=True,
description="权限标识",
source_field="auths" # 映射到数据库字段 auths
)
"""
权限标识。
- 用于按钮级别权限设置。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 auths。
"""
frame_src = fields.CharField(
max_length=255,
null=True,
description="iframe链接地址",
source_field="frame_src" # 映射到数据库字段 frame_src
)
"""
iframe 链接地址。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 frame_src。
"""
frame_loading = fields.BooleanField(
default=True,
description="iframe加载动画",
source_field="frame_loading" # 映射到数据库字段 frame_loading
)
"""
iframe 加载动画。
- 是否开启首次加载动画。
- 默认为 True。
- 映射到数据库字段 frame_loading。
"""
keep_alive = fields.BooleanField(
default=False,
description="缓存页面",
source_field="keep_alive" # 映射到数据库字段 keep_alive
)
"""
缓存页面。
- 是否缓存该路由页面。
- 默认为 False。
- 映射到数据库字段 keep_alive。
"""
hidden_tag = fields.BooleanField(
default=False,
description="隐藏标签页",
source_field="hidden_tag" # 映射到数据库字段 hidden_tag
)
"""
隐藏标签页。
- 是否禁止将当前菜单名称添加到标签页。
- 默认为 False。
- 映射到数据库字段 hidden_tag。
"""
fixed_tag = fields.BooleanField(
default=False,
description="固定标签页",
source_field="fixed_tag" # 映射到数据库字段 fixed_tag
)
"""
固定标签页。
- 是否固定显示在标签页且不可关闭。
- 默认为 False。
- 映射到数据库字段 fixed_tag。
"""
show_link = fields.BooleanField(
default=True,
description="显示菜单",
source_field="show_link" # 映射到数据库字段 show_link
)
"""
显示菜单。
- 是否显示该菜单。
- 默认为 True。
- 映射到数据库字段 show_link。
"""
show_parent = fields.BooleanField(
default=True,
description="显示父级菜单",
source_field="show_parent" # 映射到数据库字段 show_parent
)
"""
显示父级菜单。
- 是否显示父级菜单。
- 默认为 True。
- 映射到数据库字段 show_parent。
"""
class Meta:
table = "permission" # 数据库表名
table_description = "权限表" # 表描述
ordering = ["rank", "-create_time"] # 默认按排序权重和创建时间排序

119
models/role.py Normal file
View File

@@ -0,0 +1,119 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 23:00
# @UpdateTime : 2025/01/18 23:00
# @Author : sonder
# @File : role.py
# @Software : PyCharm
# @Comment : 本程序
from tortoise import fields
from models.common import BaseModel
class Role(BaseModel):
"""
角色表模型。
"""
name = fields.CharField(
max_length=255,
description="角色名称",
source_field="role_name" # 映射到数据库字段 role_name
)
"""
角色名称。
- 允许重复,因为不同部门可能有相同的角色名称。
- 最大长度为 255 个字符。
- 映射到数据库字段 role_name。
"""
code = fields.CharField(
max_length=255,
unique=True,
description="角色编码",
source_field="role_code" # 映射到数据库字段 role_code
)
"""
角色编码。
- 用于系统内部识别角色。
- 必须唯一。
- 最大长度为 255 个字符。
- 映射到数据库字段 role_code。
"""
description = fields.CharField(
max_length=255,
null=True,
description="角色描述",
source_field="role_description" # 映射到数据库字段 role_description
)
"""
角色描述。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 role_description。
"""
status=fields.SmallIntField(
default=1,
description="角色状态",
source_field="status"
)
"""
角色状态。
- 1: 正常
- 0: 禁用
- 映射到数据库字段 status。
"""
permissions = fields.ManyToManyField(
"models.Permission",
related_name="roles",
through="role_permission",
description="角色权限"
)
"""
角色权限。
- 多对多关系,表示角色拥有的权限。
- 通过中间表 `role_permission` 关联权限表。
"""
department = fields.ForeignKeyField(
"models.Department",
related_name="roles",
null=True,
description="所属部门",
source_field="department_id" # 映射到数据库字段 department_id
)
"""
所属部门。
- 表示角色所属的部门。
- 如果为 null则表示角色是全局角色。
- 映射到数据库字段 department_id。
"""
class Meta:
table = "role" # 数据库表名
table_description = "角色表" # 表描述
ordering = ["-create_time"] # 默认按创建时间倒序排序
class RolePermission(BaseModel):
"""
角色权限中间表。
"""
role = fields.ForeignKeyField(
"models.Role",
related_name="role_permissions",
source_field="role_id" # 映射到数据库字段 role_id
)
permission = fields.ForeignKeyField(
"models.Permission",
related_name="role_permissions",
source_field="permission_id" # 映射到数据库字段 permission_id
)
class Meta:
table = "role_permission" # 数据库表名
table_description = "角色权限中间表" # 表描述

172
models/user.py Normal file
View File

@@ -0,0 +1,172 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 03:20
# @UpdateTime : 2025/01/18 03:20
# @Author : sonder
# @File : user.py
# @Software : PyCharm
# @Comment : 本程序
from tortoise import fields
from models.common import BaseModel
class User(BaseModel):
"""
用户表模型。
"""
username = fields.CharField(
max_length=255,
unique=True,
description="用户名",
source_field="username" # 映射到数据库字段 username
)
"""
用户名。
- 必须唯一。
- 最大长度为 255 个字符。
- 映射到数据库字段 username。
"""
password = fields.CharField(
max_length=255,
description="密码",
source_field="password" # 映射到数据库字段 password
)
"""
密码。
- 存储加密后的密码。
- 最大长度为 255 个字符。
- 映射到数据库字段 password。
"""
email = fields.CharField(
max_length=255,
null=True,
description="邮箱",
source_field="email" # 映射到数据库字段 email
)
"""
邮箱。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 email。
"""
phone = fields.CharField(
max_length=30,
null=True,
description="手机号",
source_field="phone" # 映射到数据库字段 phone
)
"""
手机号。
- 最大长度为 30 个字符。
- 允许为空。
- 映射到数据库字段 phone。
"""
nickname = fields.CharField(
max_length=255,
null=True,
description="昵称",
source_field="nickname" # 映射到数据库字段 nickname
)
"""
昵称。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 nickname。
"""
avatar = fields.CharField(
max_length=255,
null=True,
description="头像",
source_field="avatar" # 映射到数据库字段 avatar
)
"""
头像。
- 最大长度为 255 个字符。
- 允许为空。
- 映射到数据库字段 avatar。
"""
gender = fields.SmallIntField(
default=0,
description="性别1男0女",
source_field="gender" # 映射到数据库字段 gender
)
"""
性别。
- 1
- 0
- 默认为 0。
- 映射到数据库字段 gender。
"""
status = fields.SmallIntField(
default=1,
description="用户状态1启用0禁用",
source_field="status" # 映射到数据库字段 status
)
"""
用户状态。
- 1启用
- 0禁用
- 默认为 1。
- 映射到数据库字段 status。
"""
department = fields.ForeignKeyField(
"models.Department",
related_name="users",
null=True,
description="所属部门",
source_field="department_id" # 映射到数据库字段 department_id
)
"""
所属部门。
- 外键关联到 Department 表。
- 如果为 null则表示用户未分配部门。
- 映射到数据库字段 department_id。
"""
roles = fields.ManyToManyField(
"models.Role",
related_name="users",
through="user_role",
description="用户角色"
)
"""
用户角色。
- 多对多关系,表示用户拥有的角色。
- 通过中间表 `user_role` 关联角色表。
"""
class Meta:
table = "user" # 数据库表名
table_description = "用户表" # 表描述
ordering = ["-create_time"] # 默认按创建时间倒序排序
class UserRole(BaseModel):
"""
用户角色中间表。
"""
user = fields.ForeignKeyField(
"models.User",
related_name="user_roles",
source_field="user_id" # 映射到数据库字段 user_id
)
role = fields.ForeignKeyField(
"models.Role",
related_name="user_roles",
source_field="role_id" # 映射到数据库字段 role_id
)
class Meta:
table = "user_role" # 数据库表名
table_description = "用户角色中间表" # 表描述
unique_together = (("user_id", "role_id"),) # 唯一约束,防止重复分配

BIN
requirements.txt Normal file

Binary file not shown.

7
schemas/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 02:43
# @UpdateTime : 2025/01/18 02:43
# @Author : sonder
# @File : __init__.py.py
# @Software : PyCharm
# @Comment : 本程序

57
schemas/cache.py Normal file
View File

@@ -0,0 +1,57 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/02/04 15:20
# @UpdateTime : 2025/02/04 15:20
# @Author : sonder
# @File : cache.py
# @Software : PyCharm
# @Comment : 本程序
from typing import Optional, Any, List
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
from schemas.common import BaseResponse
class CacheMonitor(BaseModel):
"""
缓存监控信息
"""
model_config = ConfigDict(alias_generator=to_camel)
command_stats: Optional[List] = Field(default=[], description='命令统计')
db_size: Optional[int] = Field(default=None, description='Key数量')
info: Optional[dict] = Field(default={}, description='Redis信息')
class CacheInfo(BaseModel):
"""
缓存信息
"""
model_config = ConfigDict(alias_generator=to_camel)
cache_key: Optional[str] = Field(default=None, description='缓存键名')
cache_name: Optional[str] = Field(default=None, description='缓存名称')
cache_value: Optional[Any] = Field(default=None, description='缓存内容')
remark: Optional[str] = Field(default=None, description='备注')
class GetCacheMonitorResponse(BaseResponse):
"""
获取缓存监控信息响应
"""
data: CacheMonitor = Field(default={}, description="缓存监控信息查询结果")
class GetCacheInfoResponse(BaseResponse):
"""
获取缓存信息响应
"""
data: List[CacheInfo] = Field(default=[], description="缓存信息查询结果")
class GetCacheKeysListResponse(BaseResponse):
"""
获取缓存键名列表
"""
data: List[str] = Field(default=[], description="缓存键名列表")

48
schemas/common.py Normal file
View File

@@ -0,0 +1,48 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/19 01:44
# @UpdateTime : 2025/01/19 01:44
# @Author : sonder
# @File : common.py
# @Software : PyCharm
# @Comment : 本程序
from typing import List
from pydantic import BaseModel, Field
class BaseResponse(BaseModel):
"""
基础响应模型
"""
code: int = Field(default=200, description="响应码")
msg: str = Field(default="操作成功!", description="响应信息")
data: dict = Field(default=None, description="响应数据")
success: bool = Field(default=True, description="操作是否成功")
time: str = Field(default="", description="响应时间")
class ListQueryResult(BaseModel):
"""
列表查询结果
"""
result: List = Field(default=[], description="列表数据")
total: int = Field(default=0, description="总条数")
page: int = Field(default=1, description="当前页码")
class DeleteListParams(BaseModel):
"""
批量删除参数
"""
ids: List[str] = Field(default=[], description="删除ID列表")
class Config:
json_schema_extra = {
"example": {
"ids": [
"1",
"2",
"3"
]
}
}

178
schemas/department.py Normal file
View File

@@ -0,0 +1,178 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/19 02:19
# @UpdateTime : 2025/01/19 02:19
# @Author : sonder
# @File : department.py
# @Software : PyCharm
# @Comment : 本程序
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, Field
from schemas.common import BaseResponse, ListQueryResult
class DepartmentInfo(BaseModel):
"""
部门表基础模型。
"""
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(..., max_length=100, description="部门名称")
parent_id: Optional[str] = Field(default=None, max_length=36, description="父部门ID")
sort: int = Field(default=0, description="排序权重0最高")
phone: Optional[str] = Field(default=None, max_length=30, description="部门电话")
principal: str = Field(..., max_length=64, description="部门负责人")
email: Optional[str] = Field(default=None, max_length=128, description="部门邮箱")
remark: Optional[str] = Field(default=None, max_length=255, description="备注信息")
status: Optional[int] = Field(default=None, description="状态")
class Config:
json_schema_extra = {
"example": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"create_by": "admin",
"create_time": "2023-10-01T12:00:00",
"update_by": "admin",
"update_time": "2023-10-01T12:00:00",
"name": "研发部",
"parent_id": "",
"sort": 0,
"phone": "1234567890",
"principal": "张三",
"email": "dev@example.com",
"remark": "研发部门",
"status": 1
}
}
class GetDepartmentInfoResponse(BaseResponse):
"""
获取部门信息响应模型。
"""
data: DepartmentInfo = Field(default=None, description="响应数据")
class AddDepartmentParams(BaseModel):
"""
添加部门参数模型。
"""
name: str = Field(..., max_length=100, description="部门名称")
parent_id: Optional[str] = Field(default=None, max_length=36, description="父部门ID")
sort: int = Field(default=0, description="排序权重0最高")
phone: Optional[str] = Field(default=None, max_length=30, description="部门电话")
principal: str = Field(..., max_length=64, description="部门负责人")
email: Optional[str] = Field(default=None, max_length=128, description="部门邮箱")
remark: Optional[str] = Field(default=None, max_length=255, description="备注信息")
status: Optional[int] = Field(default=None, description="状态")
class Config:
json_schema_extra = {
"example": {
"name": "研发部",
"parent_id": "",
"sort": 0,
"phone": "1234567890",
"principal": "张三",
"email": "dev@example.com",
"remark": "研发部门",
"status": 1
}
}
class DeleteDepartmentListParams(BaseModel):
"""
删除部门参数模型。
"""
ids: List[str] = Field(..., description="部门ID列表")
class GetDepartmentListResult(ListQueryResult):
"""
获取部门列表结果模型。
"""
result: List[DepartmentInfo] = Field(default=[], description="部门列表")
class GetDepartmentListResponse(BaseResponse):
"""
获取部门列表响应模型。
"""
data: GetDepartmentListResult = Field(default=None, description="响应数据")
class AddDepartmentRoleParams(BaseModel):
"""
添加部门角色参数模型。
"""
department_id: str = Field(..., max_length=36, description="部门ID")
role_id: str = Field(..., max_length=36, description="角色ID")
class Config:
json_schema_extra = {
"example": {
"department_id": "550e8400-e29b-41d4-a716-446655440000",
"role_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
class DepartmentRoleInfo(BaseModel):
"""
部门角色信息模型。
"""
id: str = Field(..., max_length=36, description="主键ID")
department_id: str = Field(..., max_length=36, description="部门ID")
department_name: str = Field(..., max_length=100, description="部门名称")
department_phone: str = Field(..., max_length=30, description="部门电话")
department_principal: str = Field(..., max_length=64, description="部门负责人")
department_email: str = Field(..., max_length=128, description="部门邮箱")
role_name: str = Field(..., max_length=100, description="角色名称")
role_code: str = Field(..., max_length=100, description="角色编码")
role_id: str = Field(..., max_length=36, description="角色ID")
create_time: datetime = Field(..., description="创建时间")
update_time: datetime = Field(..., description="更新时间")
class Config:
json_schema_extra = {
"example": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"department_id": "550e8400-e29b-41d4-a716-446655440000",
"department_name": "研发部",
"department_phone": "1234567890",
"department_principal": "张三",
"department_email": "dev@example.com",
"role_name": "管理员",
"role_code": "admin",
"role_id": "550e8400-e29b-41d4-a716-446655440000",
"create_time": "2023-10-01T12:00:00",
"update_time": "2023-10-01T12:00:00"
}
}
class GetDepartmentRoleInfoResponse(BaseResponse):
"""
获取部门角色信息响应模型。
"""
data: DepartmentRoleInfo = Field(default=None, description="响应数据")
class GetDepartmentRoleListResult(ListQueryResult):
"""
获取部门角色列表结果模型。
"""
result: List[DepartmentRoleInfo] = Field(default=[], description="部门角色列表")
class GetDepartmentRoleListResponse(BaseResponse):
"""
获取部门角色列表响应模型。
"""
data: GetDepartmentRoleListResult = Field(default=None, description="响应数据")

79
schemas/file.py Normal file
View File

@@ -0,0 +1,79 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/26 01:15
# @UpdateTime : 2025/01/26 01:15
# @Author : sonder
# @File : file.py
# @Software : PyCharm
# @Comment : 本程序
from typing import Optional, List
from pydantic import BaseModel, Field
from schemas.common import BaseResponse, ListQueryResult
class FileInfo(BaseModel):
"""
文件信息
"""
id: str = Field(..., title="文件id")
name: str = Field(..., title="文件名")
size: int = Field(..., title="文件大小")
file_type: str = Field(..., title="文件类型")
absolute_path: str = Field(..., title="绝对路径")
relative_path: str = Field(..., title="相对路径")
uploader_id: Optional[str] = Field(..., title="上传者ID")
uploader_username: Optional[str] = Field(..., title="上传者用户名")
uploader_nickname: Optional[str] = Field(..., title="上传者昵称")
uploader_department_id: Optional[str] = Field(..., title="上传者部门ID")
uploader_department_name: Optional[str] = Field(..., title="上传者部门名称")
update_time: str = Field(..., title="更新时间")
create_time: str = Field(..., title="创建时间")
class Config:
json_schema_extra = {
"example": {
"id": "1",
"name": "test.txt",
"size": 1024,
"file_type": "text/plain",
"absolute_path": "/home/test.txt",
"relative_path": "/test.txt",
"uploader_id": "1",
"uploader_username": "test",
"uploader_nickname": "test",
"uploader_department_id": "1",
"uploader_department_name": "test",
"update_time": "2025-01-26 01:15:00",
"create_time": "2025-01-26 01:15:00"
}
}
class UploadFileResponse(BaseResponse):
"""
上传文件响应模型
"""
data: FileInfo = Field(..., title="文件信息")
class GetFileInfoResponse(BaseResponse):
"""
获取文件响应模型
"""
data: FileInfo = Field(..., title="文件信息")
class GetFileListResult(ListQueryResult):
"""
获取文件列表结果模型
"""
result: List[FileInfo] = Field(..., title="文件列表")
class GetFileListResponse(BaseResponse):
"""
获取文件列表响应模型
"""
data: GetFileListResult = Field(..., title="文件列表结果")

121
schemas/i18n.py Normal file
View File

@@ -0,0 +1,121 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/02/04 15:55
# @UpdateTime : 2025/02/04 15:55
# @Author : sonder
# @File : i18n.py
# @Software : PyCharm
# @Comment : 本程序
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, Field, ConfigDict
from pydantic.alias_generators import to_camel
from schemas.common import BaseResponse, ListQueryResult
class LocaleInfo(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="更新时间")
code: str = Field(default="", description="语言代码")
name: str = Field(default="", description="语言名称")
class AddLocaleParams(BaseModel):
"""
添加语言类型参数
"""
code: str = Field(default="", description="语言代码")
name: str = Field(default="", description="语言名称")
class GetLocaleInfoResponse(BaseResponse):
"""
获取语言模型信息响应
"""
data: LocaleInfo = Field(default=None, description="语言模型信息")
class GetLocaleInfoResult(ListQueryResult):
"""
获取语言模型信息结果
"""
result: List[LocaleInfo] = Field(default=[], description="语言模型信息")
class GetLocaleListResponse(BaseResponse):
"""
获取语言模型信息响应
"""
data: GetLocaleInfoResult = Field(default={}, description="语言模型信息")
class I18nInfo(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="更新时间")
key: str = Field(default="", description="国际化key")
locale_code: str = Field(default="", description="语言代码")
locale_id: str = Field(default="", description="语言ID")
locale_name: str = Field(default="", description="语言名称")
translation: str = Field(default="", description="翻译内容")
class AddI18nParams(BaseModel):
"""
添加国际化参数
"""
key: str = Field(default="", description="国际化key")
locale_id: str = Field(default="", description="语言ID")
translation: str = Field(default="", description="翻译内容")
class GetI18nInfoResponse(BaseResponse):
"""
获取国际化模型信息响应
"""
data: I18nInfo = Field(default=None, description="国际化模型信息")
class GetI18nInfoResult(ListQueryResult):
"""
获取国际化模型信息结果
"""
result: List[I18nInfo] = Field(default=[], description="国际化模型信息")
class GetI18nListResponse(BaseResponse):
"""
获取国际化模型信息响应
"""
data: GetI18nInfoResult = Field(default=None, description="国际化模型信息")
class I18nList(BaseModel):
"""
国际化模型信息
"""
model_config = ConfigDict(alias_generator=to_camel)
locale: str = Field(default="", description="语言名称")
name: str = Field(default="", description="语言名称")
data: dict = Field(default={}, description="国际化模型信息")
class GetI18nInfoListResponse(BaseResponse):
"""
获取国际化模型信息响应
"""
data: I18nList = Field(default=None, description="国际化模型信息")

135
schemas/log.py Normal file
View File

@@ -0,0 +1,135 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/27 21:41
# @UpdateTime : 2025/01/27 21:41
# @Author : sonder
# @File : log.py
# @Software : PyCharm
# @Comment : 本程序
from pydantic import BaseModel, Field
from schemas.common import BaseResponse, ListQueryResult
class LoginLogInfo(BaseModel):
"""
登录日志信息
"""
id: str = Field(default="", description="登录日志ID")
user_id: str = Field(default="", description="用户ID")
username: str = Field(default="", description="用户名")
user_nickname: str = Field(default="", description="用户昵称")
department_id: str = Field(default="", description="部门ID")
department_name: str = Field(default="", description="部门名称")
login_ip: str = Field(default="", description="登录IP")
login_location: str = Field(default="", description="登录地点")
browser: str = Field(default="", description="登录浏览器")
os: str = Field(default="", description="登录操作系统")
status: int = Field(default="", description="登录状态")
login_time: str = Field(default="", description="登录时间")
session_id: str = Field(default="", description="会话ID")
online: bool = Field(default=False, description="是否在线")
create_time: str = Field(default="", description="创建时间")
update_time: str = Field(default="", description="更新时间")
class Config:
json_schema_extra = {
"example": {
"id": "1",
"user_id": "1",
"username": "admin",
"user_nickname": "管理员",
"department_id": "1",
"department_name": "超级管理员",
"login_ip": "127.0.0.1",
"login_location": "中国",
"browser": "Chrome",
"os": "Windows",
"status": 1,
"login_time": "2025-01-27 21:41:00",
"session_id": "1",
"online": True,
"create_time": "2025-01-27 21:41:00",
"update_time": "2025-01-27 21:41:00"
}
}
class LoginLogResult(ListQueryResult):
"""
登录日志查询结果
"""
result: list[LoginLogInfo] = Field(default=[], description="登录日志列表")
class GetLoginLogResponse(BaseResponse):
"""
获取登录日志响应
"""
data: LoginLogResult = Field(default=[], description="登录日志查询结果")
class OperationLogInfo(BaseModel):
"""
操作日志信息
"""
id: str = Field(default="", description="操作日志ID")
operation_name: str = Field(default="", description="操作名称")
operation_type: int = Field(default=1, description="操作类型")
request_path: str = Field(default="", description="请求路径")
request_method: str = Field(default="", description="请求方法")
request_params: str = Field(default="", description="请求参数")
request_result: str = Field(default="", description="请求结果")
host: str = Field(default="", description="请求主机")
location: str = Field(default="", description="请求地址")
browser: str = Field(default="", description="请求浏览器")
os: str = Field(default="", description="请求操作系统")
user_agent: str = Field(default="", description="请求头")
operator_id: str = Field(default="", description="操作人员ID")
operator_name: str = Field(default="", description="操作人员名称")
operator_nickname: str = Field(default="", description="操作人员昵称")
department_id: str = Field(default="", description="操作人员部门ID")
department_name: str = Field(default="", description="操作人员部门名称")
operation_time: str = Field(default="", description="操作时间")
cost_time: float = Field(default=0.0, description="操作耗时")
status: int = Field(default="", description="操作状态")
class Config:
json_schema_extra = {
"example": {
"id": "1",
"operation_name": "登录",
"operation_type": 1,
"request_path": "/login",
"request_method": "POST",
"request_params": "{\"username\": \"admin\", \"password\": \"123456\"}",
"request_result": "{\"code\": 200, \"data\": {\"token\":\"eyJ0eXAiOiJKV1Qi}",
"host": "127.0.0.1",
"location": "中国",
"browser": "Chrome",
"os": "Windows",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36",
"operator_id": "1",
"operator_name": "admin",
"operator_nickname": "管理员",
"department_id": "1",
"department_name": "超级管理员",
"operation_time": "2025-01-27 21:41:00",
"cost_time": 0.123,
"status": 1
}
}
class OperationLogResult(ListQueryResult):
"""
操作日志查询结果
"""
result: list[OperationLogInfo] = Field(default=[], description="操作日志列表")
class GetOperationLogResponse(BaseResponse):
"""
获取操作日志响应
"""
data: OperationLogResult = Field(default=[], description="操作日志查询结果")

185
schemas/login.py Normal file
View File

@@ -0,0 +1,185 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/19 01:35
# @UpdateTime : 2025/01/19 01:35
# @Author : sonder
# @File : login.py
# @Software : PyCharm
# @Comment : 本程序
from typing import Optional
from pydantic import BaseModel, Field
from pydantic_validation_decorator import Network, Size, NotBlank, Xss
from schemas.common import BaseResponse
class LoginParams(BaseModel):
"""
登录请求模型
"""
username: str = Field(default="", description="用户名")
password: str = Field(default="", description="密码")
loginDays: Optional[int] = Field(default=1, description="登录天数")
code: Optional[str] = Field(default="", description="验证码")
uuid: Optional[str] = Field(default="", description="验证码UUID")
class LoginResult(BaseModel):
"""
登录响应模型
"""
accessToken: str = Field(default="", description="访问令牌")
refreshToken: str = Field(default="", description="刷新令牌")
expiresTime: int = Field(default=0, description="令牌过期时间戳")
class LoginResponse(BaseResponse):
"""
登录响应模型
"""
data: LoginResult = Field(default=None, description="响应数据")
class UserInfo(BaseModel):
"""
用户信息模型
"""
id: str = Field(default="", description="用户ID")
username: str = Field(default="", description="用户名")
nickname: str = Field(default="", description="用户昵称")
avatar: str = Field(default="", description="用户头像")
gender: int = Field(default=0, description="用户性别")
email: str = Field(default="", description="用户邮箱")
phone: str = Field(default="", description="用户手机号")
status: int = Field(default=0, description="用户状态")
department_id: str = Field(default="", description="用户部门ID")
department_name: str = Field(default="", description="用户部门名称")
roles: list = Field(default=[], description="用户角色")
permissions: list = Field(default=[], description="用户权限")
create_time: str = Field(default="", description="创建时间")
update_time: str = Field(default="", description="更新时间")
@Xss(field_name='username', message='用户账号不能包含脚本字符')
@NotBlank(field_name='username', message='用户账号不能为空')
@Size(field_name='username', min_length=0, max_length=30, message='用户账号长度不能超过30个字符')
def get_user_name(self):
return self.user_name
@Xss(field_name='nickname', message='用户昵称不能包含脚本字符')
@Size(field_name='nickname', min_length=0, max_length=30, message='用户昵称长度不能超过30个字符')
def get_nick_name(self):
return self.nick_name
@Network(field_name='email', field_type='EmailStr', message='邮箱格式不正确')
@Size(field_name='email', min_length=0, max_length=50, message='邮箱长度不能超过50个字符')
def get_email(self):
return self.email
class Config:
json_schema_extra = {
"example": {
"id": "1",
"username": "admin",
"nickname": "管理员",
"avatar": "https://www.example.com/avatar.jpg",
"gender": 1,
"email": "admin@example.com",
"phone": "1234567890",
"status": 1,
"department_id": "1",
"department_name": "技术部",
"roles": ["admin"],
"permissions": ["read", "write"],
"create_time": "2022-01-01 00:00:00",
"update_time": "2022-01-01 00:00:00"
}
}
class GetUserInfoResponse(BaseResponse):
"""
获取用户信息响应模型
"""
data: UserInfo = Field(default=None, description="响应数据")
class UpdateUserTokenResponse(BaseResponse):
"""
更新用户令牌响应模型
"""
data: LoginResult = Field(default=None, description="响应数据")
class GetCaptchaResult(BaseModel):
"""
获取验证码结果模型
"""
uuid: str = Field(default="", description="验证码UUID")
captcha: str = Field(default="", description="验证码图片")
class Config:
json_schema_extra = {
"example": {
"uuid": "1234567890",
"captcha": "base64编码的图片"
}
}
class GetEmailCodeParams(BaseModel):
"""
获取邮箱验证码请求模型
"""
username:str=Field(default="", description="用户名")
title: str = Field(default="注册", description="邮件类型")
mail: str = Field(default="", description="邮箱地址")
@Network(field_name='mail', field_type='EmailStr', message='邮箱格式不正确')
@Size(field_name='mail', min_length=0, max_length=50, message='邮箱长度不能超过50个字符')
def get_email(self):
return self.mail
class Config:
json_schema_extra = {
"example": {
"username": "admin",
"title": "注册",
"mail": "admin@example.com"
}
}
class ResetPasswordParams(BaseModel):
"""
重置密码请求模型
"""
username: str = Field(default="", description="用户名")
mail: str = Field(default="", description="邮箱地址")
code: str = Field(default="", description="验证码")
password: str = Field(default="", description="新密码")
@Network(field_name='mail', field_type='EmailStr', message='邮箱格式不正确')
@Size(field_name='mail', min_length=0, max_length=50, message='邮箱长度不能超过50个字符')
def get_email(self):
return self.mail
@Size(field_name='code', min_length=0, max_length=4, message='验证码长度不能超过4个字符')
def get_code(self):
return self.code
class Config:
json_schema_extra = {
"example": {
"username": "admin",
"mail": "admin@example.com",
"code": "1234",
"password": "123456"
}
}
class GetCaptchaResponse(BaseResponse):
"""
获取验证码响应模型
"""
data: GetCaptchaResult = Field(default=None, description="响应数据")

152
schemas/permission.py Normal file
View File

@@ -0,0 +1,152 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/20 20:32
# @UpdateTime : 2025/01/20 20:32
# @Author : sonder
# @File : permission.py
# @Software : PyCharm
# @Comment : 本程序
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, Field
from schemas.common import BaseResponse, ListQueryResult
class PermissionInfo(BaseModel):
"""
权限表基础模型。
"""
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="更新时间")
menu_type: int = Field(..., description="菜单类型0菜单、1iframe、2外链、3按钮")
parent_id: str = Field(default="", max_length=36, description="父权限ID")
title: str = Field(..., max_length=255, description="菜单名称")
name: Optional[str] = Field(default=None, max_length=255, description="路由名称")
path: Optional[str] = Field(default=None, max_length=255, description="路由路径")
component: Optional[str] = Field(default=None, max_length=255, description="组件路径")
rank: int = Field(default=1, description="菜单排序")
redirect: Optional[str] = Field(default=None, max_length=255, description="路由重定向")
icon: Optional[str] = Field(default=None, max_length=255, description="菜单图标")
extra_icon: Optional[str] = Field(default=None, max_length=255, description="右侧图标")
enter_transition: Optional[str] = Field(default=None, max_length=255, description="进场动画")
leave_transition: Optional[str] = Field(default=None, max_length=255, description="离场动画")
active_path: Optional[str] = Field(default=None, max_length=255, description="菜单激活路径")
auths: Optional[str] = Field(default=None, max_length=255, description="权限标识")
frame_src: Optional[str] = Field(default=None, max_length=255, description="iframe链接地址")
frame_loading: bool = Field(default=True, description="iframe加载动画")
keep_alive: bool = Field(default=False, description="缓存页面")
hidden_tag: bool = Field(default=False, description="隐藏标签页")
fixed_tag: bool = Field(default=False, description="固定标签页")
show_link: bool = Field(default=True, description="显示菜单")
show_parent: bool = Field(default=True, description="显示父级菜单")
class Config:
json_schema_extra = {
"example": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"create_by": "admin",
"create_time": "2023-10-01T12:00:00",
"update_by": "admin",
"update_time": "2023-10-01T12:00:00",
"menu_type": 0,
"parent_id": "",
"title": "首页",
"name": "home",
"path": "/home",
"component": "HomeView",
"rank": 1,
"redirect": None,
"icon": "home",
"extra_icon": None,
"enter_transition": None,
"leave_transition": None,
"active_path": None,
"auths": None,
"frame_src": None,
"frame_loading": True,
"keep_alive": False,
"hidden_tag": False,
"fixed_tag": False,
"show_link": True,
"show_parent": True
}
}
class GetPermissionInfoResponse(BaseResponse):
"""
获取权限信息响应模型。
"""
data: PermissionInfo = Field(default=None, description="响应数据")
class AddPermissionParams(BaseModel):
"""
添加权限参数模型。
"""
name: str = Field(..., max_length=255, description="路由名称")
path: str = Field(..., max_length=255, description="路由路径")
title: str = Field(..., max_length=255, description="菜单名称")
component: Optional[str] = Field(default=None, max_length=255, description="组件路径")
rank: int = Field(default=1, description="菜单排序")
redirect: Optional[str] = Field(default=None, max_length=255, description="路由重定向")
icon: Optional[str] = Field(default=None, max_length=255, description="菜单图标")
extra_icon: Optional[str] = Field(default=None, max_length=255, description="右侧图标")
enter_transition: Optional[str] = Field(default=None, max_length=255, description="进场动画")
leave_transition: Optional[str] = Field(default=None, max_length=255, description="离场动画")
active_path: Optional[str] = Field(default=None, max_length=255, description="菜单激活路径")
auths: Optional[str] = Field(default=None, max_length=255, description="权限标识")
frame_src: Optional[str] = Field(default=None, max_length=255, description="iframe链接地址")
frame_loading: bool = Field(default=True, description="iframe加载动画")
keep_alive: bool = Field(default=False, description="缓存页面")
hidden_tag: bool = Field(default=False, description="隐藏标签页")
fixed_tag: bool = Field(default=False, description="固定标签页")
show_link: bool = Field(default=True, description="显示菜单")
show_parent: bool = Field(default=True, description="显示父级菜单")
parent_id: str = Field(default="", max_length=36, description="父级菜单ID")
menu_type: int = Field(default=0, description="菜单类型")
class Config:
json_schema_extra = {
"example": {
"name": "home",
"path": "/home",
"title": "首页",
"component": "HomeView",
"rank": 1,
"redirect": None,
"icon": "home",
"extra_icon": None,
"enter_transition": None,
"leave_transition": None,
"active_path": None,
"auths": None,
"frame_src": None,
"frame_loading": True,
"keep_alive": False,
"hidden_tag": False,
"fixed_tag": False,
"show_link": True,
"show_parent": True,
"parent_id": "",
"menu_type": 0
}
}
class GetPermissionListResult(ListQueryResult):
"""
获取权限列表结果模型。
"""
result: List[PermissionInfo] = Field(default=[], description="权限列表")
class GetPermissionListResponse(BaseResponse):
"""
获取权限列表响应模型。
"""
data: GetPermissionListResult = Field(default=None, description="响应数据")

169
schemas/role.py Normal file
View File

@@ -0,0 +1,169 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/20 22:40
# @UpdateTime : 2025/01/20 22:40
# @Author : sonder
# @File : role.py
# @Software : PyCharm
# @Comment : 本程序
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, Field
from schemas.common import BaseResponse, ListQueryResult
class RoleInfo(BaseModel):
"""
角色表基础模型。
"""
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(..., max_length=255, description="角色名称")
code: str = Field(..., max_length=255, description="角色编码")
status: int = Field(default=1, description="状态")
description: Optional[str] = Field(default=None, max_length=255, description="角色描述")
department_id: Optional[str] = Field(default=None, max_length=36, description="所属部门ID")
department_name: Optional[str] = Field(default=None, max_length=255, description="所属部门名称")
department_phone: Optional[str] = Field(default=None, max_length=255, description="所属部门电话")
department_principal: Optional[str] = Field(default=None, max_length=255, description="所属部门领导人")
department_email: Optional[str] = Field(default=None, max_length=255, description="所属部门邮件")
class Config:
json_schema_extra = {
"example": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"create_by": "admin",
"create_time": "2023-10-01T12:00:00",
"update_by": "admin",
"update_time": "2023-10-01T12:00:00",
"name": "管理员",
"code": "admin",
"status": 1,
"description": "系统管理员角色",
"department_id": "770e8400-e29b-41d4-a716-446655440000",
"department_name": "技术部",
"department_phone": "1234567890",
"department_principal": "张三",
"department_email": "zhangsan@example.com"
}
}
class GetRoleInfoResponse(BaseResponse):
"""
获取角色信息响应模型。
"""
data: RoleInfo = Field(default=None, description="角色信息")
class AddRoleParams(BaseModel):
"""
添加角色请求参数模型。
"""
name: str = Field(..., max_length=255, description="角色名称")
code: str = Field(..., max_length=255, description="角色编码")
description: Optional[str] = Field(default=None, max_length=255, description="角色描述")
department_id: Optional[str] = Field(default=None, max_length=36, description="所属部门ID")
status: Optional[int] = Field(default=1, description="状态")
class Config:
json_schema_extra = {
"example": {
"name": "管理员",
"code": "admin",
"description": "系统管理员角色",
"department_id": "770e8400-e29b-41d4-a716-446655440000",
"status": 1
}
}
class GetRoleListResult(ListQueryResult):
"""
获取角色列表结果模型。
"""
result: List[RoleInfo] = Field(default=None, description="角色列表")
class GetRoleListResponse(BaseResponse):
"""
获取角色列表响应模型。
"""
data: GetRoleListResult = Field(default=None, description="角色列表结果")
class AddRolePermissionParams(BaseModel):
"""
添加角色权限请求参数模型。
"""
permission_ids: List[str] = Field(..., description="权限ID列表")
class Config:
json_schema_extra = {
"example": {
"permission_ids": ["1", "2", "3"]
}
}
class RolePermissionInfo(BaseModel):
"""
角色权限信息模型。
"""
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="更新时间")
role_id: str = Field(..., max_length=36, description="角色ID")
role_code: str = Field(..., max_length=255, description="角色编码")
role_name: str = Field(..., max_length=255, description="角色名称")
permission_id: str = Field(..., max_length=36, description="权限ID")
permission__parent_id: str = Field(default="", max_length=36, description="父级权限ID")
permission_name: str = Field(..., max_length=255, description="权限名称")
permission_code: str = Field(..., max_length=255, description="权限编码")
permission_type: str = Field(..., max_length=255, description="权限类型")
class Config:
json_schema_extra = {
"example": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"create_by": "admin",
"create_time": "2023-10-01T12:00:00",
"update_by": "admin",
"update_time": "2023-10-01T12:00:00",
"role_id": "550e8400-e29b-41d4-a716-446655440000",
"role_code": "admin",
"role_name": "管理员",
"permission_id": "550e8400-e29b-41d4-a716-446655440000",
"permission__parent_id": "550e8400-e29b-41d4-a716-446655440000",
"permission_name": "权限名称",
"permission_code": "admin",
"permission_type": "admin"
}
}
class GetRolePermissionInfoResponse(BaseResponse):
"""
获取角色权限信息响应模型。
"""
data: RolePermissionInfo = Field(default=None, description="角色权限信息")
class GetRolePermissionListResult(ListQueryResult):
"""
获取角色权限列表结果模型。
"""
result: List[RolePermissionInfo] = Field(default=None, description="角色权限列表")
class GetRolePermissionListResponse(BaseResponse):
"""
获取角色权限列表响应模型。
"""
data: GetRolePermissionListResult = Field(default=None, description="角色权限列表结果")

98
schemas/server.py Normal file
View File

@@ -0,0 +1,98 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/02/04 15:26
# @UpdateTime : 2025/02/04 15:26
# @Author : sonder
# @File : server.py
# @Software : PyCharm
# @Comment : 本程序
from typing import Optional, List
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
from schemas.common import BaseResponse
class CpuInfo(BaseModel):
"""
CPU信息
"""
model_config = ConfigDict(alias_generator=to_camel)
cpu_num: Optional[int] = Field(default=None, description='核心数')
used: Optional[float] = Field(default=None, description='CPU用户使用率')
sys: Optional[float] = Field(default=None, description='CPU系统使用率')
free: Optional[float] = Field(default=None, description='CPU当前空闲率')
class MemoryInfo(BaseModel):
"""
内存信息
"""
model_config = ConfigDict(alias_generator=to_camel)
total: Optional[str] = Field(default=None, description='内存总量')
used: Optional[str] = Field(default=None, description='已用内存')
free: Optional[str] = Field(default=None, description='剩余内存')
usage: Optional[float] = Field(default=None, description='使用率')
class SystemInfo(BaseModel):
"""
系统信息
"""
model_config = ConfigDict(alias_generator=to_camel)
computer_ip: Optional[str] = Field(default=None, description='服务器IP')
computer_name: Optional[str] = Field(default=None, description='服务器名称')
os_arch: Optional[str] = Field(default=None, description='系统架构')
os_name: Optional[str] = Field(default=None, description='操作系统')
user_dir: Optional[str] = Field(default=None, description='项目路径')
class PythonInfo(MemoryInfo):
"""
Python信息
"""
model_config = ConfigDict(alias_generator=to_camel)
name: Optional[str] = Field(default=None, description='Python名称')
version: Optional[str] = Field(default=None, description='Python版本')
start_time: Optional[str] = Field(default=None, description='启动时间')
run_time: Optional[str] = Field(default=None, description='运行时长')
home: Optional[str] = Field(default=None, description='安装路径')
class SystemFiles(BaseModel):
"""
系统磁盘信息
"""
model_config = ConfigDict(alias_generator=to_camel)
dir_name: Optional[str] = Field(default=None, description='盘符路径')
sys_type_name: Optional[str] = Field(default=None, description='盘符类型')
type_name: Optional[str] = Field(default=None, description='文件类型')
total: Optional[str] = Field(default=None, description='总大小')
used: Optional[str] = Field(default=None, description='已经使用量')
free: Optional[str] = Field(default=None, description='剩余大小')
usage: Optional[str] = Field(default=None, description='资源的使用率')
class GetSystemInfoResult(BaseModel):
"""
获取系统信息结果
"""
model_config = ConfigDict(alias_generator=to_camel)
cpu: Optional[CpuInfo] = Field(description='CPU相关信息')
python: Optional[PythonInfo] = Field(description='Python相关信息')
memory: Optional[MemoryInfo] = Field(description='內存相关信息')
system: Optional[SystemInfo] = Field(description='服务器相关信息')
system_files: Optional[List[SystemFiles]] = Field(description='磁盘相关信息')
class GetServerInfoResponse(BaseResponse):
"""
获取服务器信息响应
"""
data: GetSystemInfoResult = Field(default={}, description="服务器信息查询结果")

350
schemas/user.py Normal file
View File

@@ -0,0 +1,350 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/19 02:10
# @UpdateTime : 2025/01/19 02:10
# @Author : sonder
# @File : user.py
# @Software : PyCharm
# @Comment : 本程序
from datetime import datetime
from typing import Optional, List
from uuid import UUID
from pydantic import BaseModel, Field
from pydantic_validation_decorator import Xss, NotBlank, Size, Network
from schemas.common import BaseResponse, ListQueryResult
class UserBase(BaseModel):
"""
用户表基础模型。
"""
id: UUID = Field(..., description="主键")
del_flag: int = Field(default=1, description="删除标志 1存在 0删除")
create_by: str = Field(default="", description="创建者")
create_time: str = Field(..., description="创建时间")
update_by: str = Field(default="", description="更新者")
update_time: str = Field(..., description="更新时间")
username: str = Field(..., max_length=255, description="用户名")
password: str = Field(..., max_length=255, description="密码")
email: Optional[str] = Field(default=None, max_length=255, description="邮箱")
phone: Optional[str] = Field(default=None, max_length=30, description="手机号")
nickname: Optional[str] = Field(default=None, max_length=255, description="昵称")
avatar: Optional[str] = Field(default=None, max_length=255, description="头像")
gender: int = Field(default=0, description="性别1男0女")
status: int = Field(default=1, description="用户状态1启用0禁用")
department_id: Optional[str] = Field(default=None, max_length=36, description="所属部门")
@Xss(field_name='username', message='用户账号不能包含脚本字符')
@NotBlank(field_name='username', message='用户账号不能为空')
@Size(field_name='username', min_length=0, max_length=30, message='用户账号长度不能超过30个字符')
def get_user_name(self):
return self.user_name
@Xss(field_name='nickname', message='用户昵称不能包含脚本字符')
@Size(field_name='nickname', min_length=0, max_length=30, message='用户昵称长度不能超过30个字符')
def get_nick_name(self):
return self.nick_name
@Network(field_name='email', field_type='EmailStr', message='邮箱格式不正确')
@Size(field_name='email', min_length=0, max_length=50, message='邮箱长度不能超过50个字符')
def get_email(self):
return self.email
class Config:
json_schema_extra = {
"example": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"del_flag": 1,
"create_by": "admin",
"create_time": "2025-01-19 02:10:00",
"update_by": "admin",
"update_time": "2025-01-19 02:10:00",
"username": "admin",
"password": "admin",
"email": "admin@example.com",
"phone": "13800138000",
"nickname": "管理员",
"gender": 1,
"status": 1,
"department_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
class UserInfo(BaseModel):
"""
用户表基础模型。
"""
id: str = Field(..., description="主键")
create_time: str = Field(..., description="创建时间") # 使用蛇形命名
update_time: str = Field(..., description="更新时间") # 使用蛇形命名
username: str = Field(..., max_length=255, description="用户名")
email: Optional[str] = Field(default=None, max_length=255, description="邮箱")
phone: Optional[str] = Field(default=None, max_length=30, description="手机号")
nickname: Optional[str] = Field(default=None, max_length=255, description="昵称")
avatar: Optional[str] = Field(default=None, max_length=255, description="头像")
gender: int = Field(default=0, description="性别1男0女")
status: int = Field(default=1, description="用户状态1启用0禁用")
department_id: Optional[str] = Field(default=None, max_length=36, description="所属部门")
class Config:
json_schema_extra = {
"example": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"create_time": "2025-01-19 02:10:00",
"update_time": "2025-01-19 02:10:00",
"username": "admin",
"email": "admin@example.com",
"phone": "13800138000",
"nickname": "管理员",
"gender": 1,
"status": 1,
"department_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
class GetUserInfoResponse(BaseResponse):
"""
获取用户信息响应模型。
"""
data: UserInfo = Field(default=None, description="响应数据")
class AddUserParams(BaseModel):
"""
添加用户参数模型。
"""
username: str = Field(..., max_length=255, description="用户名")
password: str = Field(..., max_length=255, description="密码")
email: Optional[str] = Field(default=None, max_length=255, description="邮箱")
phone: Optional[str] = Field(default=None, max_length=30, description="手机号")
nickname: Optional[str] = Field(default=None, max_length=255, description="昵称")
avatar: Optional[str] = Field(default=None, max_length=255, description="头像")
gender: int = Field(default=0, description="性别1男0女")
status: int = Field(default=1, description="用户状态1启用0禁用")
department_id: Optional[str] = Field(default=None, max_length=36, description="所属部门")
@Xss(field_name='username', message='用户账号不能包含脚本字符')
@NotBlank(field_name='username', message='用户账号不能为空')
@Size(field_name='username', min_length=0, max_length=30, message='用户账号长度不能超过30个字符')
def get_user_name(self):
return self.user_name
@Xss(field_name='nickname', message='用户昵称不能包含脚本字符')
@Size(field_name='nickname', min_length=0, max_length=30, message='用户昵称长度不能超过30个字符')
def get_nick_name(self):
return self.nick_name
@Network(field_name='email', field_type='EmailStr', message='邮箱格式不正确')
@Size(field_name='email', min_length=0, max_length=50, message='邮箱长度不能超过50个字符')
def get_email(self):
return self.email
class Config:
json_schema_extra = {
"example": {
"username": "admin",
"password": "admin",
"email": "admin@example.com",
"phone": "13800138000",
"nickname": "管理员",
"gender": 1,
"status": 1,
"department_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
class RegisterUserParams(AddUserParams):
"""
注册用户参数模型。
"""
code: str = Field(..., max_length=10, description="验证码")
class Config:
json_schema_extra = {
"example": {
"username": "admin",
"password": "admin",
"email": "admin@example.com",
"phone": "13800138000",
"nickname": "管理员",
"gender": 1,
"status": 1,
"department_id": "550e8400-e29b-41d4-a716-446655440000",
"code": "123456"
}
}
class UpdateUserParams(BaseModel):
"""
更新用户参数模型。
"""
username: str = Field(..., max_length=255, description="用户名")
email: Optional[str] = Field(default=None, max_length=255, description="邮箱")
phone: Optional[str] = Field(default=None, max_length=30, description="手机号")
nickname: Optional[str] = Field(default=None, max_length=255, description="昵称")
avatar: Optional[str] = Field(default=None, max_length=255, description="头像")
gender: int = Field(default=0, description="性别1男0女")
status: int = Field(default=1, description="用户状态1启用0禁用")
department_id: Optional[str] = Field(default=None, max_length=36, description="所属部门")
@Xss(field_name='username', message='用户账号不能包含脚本字符')
@NotBlank(field_name='username', message='用户账号不能为空')
@Size(field_name='username', min_length=0, max_length=30, message='用户账号长度不能超过30个字符')
def get_user_name(self):
return self.user_name
@Xss(field_name='nickname', message='用户昵称不能包含脚本字符')
@Size(field_name='nickname', min_length=0, max_length=30, message='用户昵称长度不能超过30个字符')
def get_nick_name(self):
return self.nick_name
@Network(field_name='email', field_type='EmailStr', message='邮箱格式不正确')
@Size(field_name='email', min_length=0, max_length=50, message='邮箱长度不能超过50个字符')
def get_email(self):
return self.email
class Config:
json_schema_extra = {
"example": {
"username": "admin",
"email": "admin@example.com",
"phone": "13800138000",
"nickname": "管理员",
"gender": 1,
"status": 1,
"department_id": "550e8400-e29b-41d4-a716-446655440000"
}
}
class GetUserListResult(ListQueryResult):
"""
获取用户列表结果模型。
"""
result: List[UserInfo] = Field(default=[], description="部门列表")
class GetUserListResponse(BaseResponse):
"""
获取用户列表响应模型。
"""
data: GetUserListResult = Field(default=None, description="响应数据")
class AddUserRoleParams(BaseModel):
"""
添加用户角色参数模型。
"""
user_id: str = Field(..., max_length=36, description="用户ID")
role_id: str = Field(default=[], max_length=36, description="角色ID")
class Config:
json_schema_extra = {
"example": {
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"role_ids": "550e8400-e29b-41d4-a716-446655440000"
}
}
class UpdateUserRoleParams(BaseModel):
"""
更新用户角色参数模型。
"""
user_id: str = Field(..., max_length=36, description="用户ID")
role_ids: List[str] = Field(default=[], max_length=36, description="角色ID")
class Config:
json_schema_extra = {
"example": {
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"role_ids": ["550e8400-e29b-41d4-a716-446655440000"]
}
}
class UserRoleInfo(BaseModel):
"""
用户角色信息模型。
"""
id: str = Field(..., max_length=36, description="主键ID")
user_id: str = Field(..., max_length=36, description="用户ID")
user_name: str = Field(..., max_length=100, description="用户账号")
role_name: str = Field(..., max_length=100, description="角色名称")
role_code: str = Field(..., max_length=100, description="角色编码")
role_id: str = Field(..., max_length=36, description="角色ID")
create_time: datetime = Field(..., description="创建时间")
update_time: datetime = Field(..., description="更新时间")
@Xss(field_name='user_name', message='用户账号不能包含脚本字符')
@NotBlank(field_name='user_name', message='用户账号不能为空')
@Size(field_name='user_name', min_length=0, max_length=30, message='用户账号长度不能超过30个字符')
def get_user_name(self):
return self.user_name
@Xss(field_name='role_name', message='角色名称不能包含脚本字符')
@NotBlank(field_name='role_name', message='角色名称不能为空')
@Size(field_name='role_name', min_length=0, max_length=30, message='角色名称长度不能超过30个字符')
def get_role_name(self):
return self.role_name
class Config:
json_schema_extra = {
"example": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"user_name": "zhangsan",
"role_name": "管理员",
"role_code": "admin",
"role_id": "550e8400-e29b-41d4-a716-446655440000",
"create_time": "2023-10-01T12:00:00",
"update_time": "2023-10-01T12:00:00"
}
}
class GetUserRoleInfoResponse(BaseResponse):
"""
获取用户角色信息响应模型。
"""
data: UserRoleInfo = Field(default=None, description="响应数据")
class GetUserRoleListResult(ListQueryResult):
"""
获取用户角色列表结果模型。
"""
result: List[UserRoleInfo] = Field(default=[], description="用户角色列表")
class GetUserRoleListResponse(BaseResponse):
"""
获取用户角色列表响应模型。
"""
data: GetUserRoleListResult = Field(default=None, description="响应数据")
class GetUserPermissionListResponse(BaseResponse):
"""
获取用户权限列表响应模型。
"""
data: List[str] = Field(default=[], description="响应数据")
class ResetPasswordParams(BaseModel):
"""
重置密码参数模型。
"""
password: str = Field(..., max_length=100, description="新密码")
class Config:
json_schema_extra = {
"example": {
"password": "123456"
}
}

147
templates/mail_en.html Normal file
View File

@@ -0,0 +1,147 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Email Verification Code</title>
<style>
/* 样式保持不变 */
table {
width: 700px;
margin: 0 auto;
}
#top {
width: 700px;
border-bottom: 1px solid #ccc;
margin: 0 auto 30px;
}
#top table {
font: 12px Tahoma, Arial, sans-serif;
height: 40px;
}
#content {
width: 680px;
padding: 0 10px;
margin: 0 auto;
}
#content_top {
line-height: 1.5;
font-size: 14px;
margin-bottom: 25px;
color: #4d4d4d;
}
#content_top strong {
display: block;
margin-bottom: 15px;
}
#content_top strong span {
color: #f60;
font-size: 16px;
}
#verificationCode {
color: #f60;
font-size: 24px;
}
#content_bottom {
margin-bottom: 30px;
}
#content_bottom small {
display: block;
margin-bottom: 20px;
font-size: 12px;
color: #747474;
}
#bottom {
width: 700px;
margin: 0 auto;
}
#bottom div {
padding: 10px 10px 0;
border-top: 1px solid #ccc;
color: #747474;
margin-bottom: 20px;
line-height: 1.3em;
font-size: 12px;
}
#content_top strong span {
font-size: 18px;
color: #FE4F70;
}
#sign {
text-align: right;
font-size: 18px;
color: #FE4F70;
font-weight: bold;
}
#verificationCode {
height: 100px;
width: 680px;
text-align: center;
margin: 30px 0;
}
#verificationCode div {
height: 100px;
width: 680px;
}
.button {
color: #FE4F70;
margin-left: 10px;
height: 80px;
width: 80px;
resize: none;
font-size: 42px;
border: none;
outline: none;
padding: 10px 15px;
background: #ededed;
text-align: center;
border-radius: 17px;
box-shadow: 6px 6px 12px #cccccc,
-6px -6px 12px #ffffff;
}
.button:hover {
box-shadow: inset 6px 6px 4px #d1d1d1,
inset -6px -6px 4px #ffffff;
}
</style>
</head>
<body>
<table>
<tbody>
<tr>
<td>
<div id="top">
<table>
<tbody><tr><td></td></tr></tbody>
</table>
</div>
<div id="content">
<div id="content_top">
<strong>Dear User: Hello!</strong>
<strong>
You are performing <span>{{ TITLE }}</span> operation. Please enter the following verification code to complete the operation:
</strong>
<div id="verificationCode">
{{ CODE }}
</div>
</div>
<div id="content_bottom">
<small>
Note: This operation may modify your password, login email, or bound phone number. If this is not your operation, please log in and change your password to ensure account security.
<br>(Staff will not ask you for this verification code. Please do not disclose it!)
</small>
</div>
</div>
<div id="bottom">
<div>
<p>This is a system email. Please do not reply.<br>
Please keep your email safe to avoid account theft.
</p>
<p id="sign">——{{ PROJECTNAME }}</p>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>

147
templates/mail_zh.html Normal file
View File

@@ -0,0 +1,147 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>邮箱验证码</title>
<style>
/* 样式保持不变 */
table {
width: 700px;
margin: 0 auto;
}
#top {
width: 700px;
border-bottom: 1px solid #ccc;
margin: 0 auto 30px;
}
#top table {
font: 12px Tahoma, Arial, ;
height: 40px;
}
#content {
width: 680px;
padding: 0 10px;
margin: 0 auto;
}
#content_top {
line-height: 1.5;
font-size: 14px;
margin-bottom: 25px;
color: #4d4d4d;
}
#content_top strong {
display: block;
margin-bottom: 15px;
}
#content_top strong span {
color: #f60;
font-size: 16px;
}
#verificationCode {
color: #f60;
font-size: 24px;
}
#content_bottom {
margin-bottom: 30px;
}
#content_bottom small {
display: block;
margin-bottom: 20px;
font-size: 12px;
color: #747474;
}
#bottom {
width: 700px;
margin: 0 auto;
}
#bottom div {
padding: 10px 10px 0;
border-top: 1px solid #ccc;
color: #747474;
margin-bottom: 20px;
line-height: 1.3em;
font-size: 12px;
}
#content_top strong span {
font-size: 18px;
color: #FE4F70;
}
#sign {
text-align: right;
font-size: 18px;
color: #FE4F70;
font-weight: bold;
}
#verificationCode {
height: 100px;
width: 680px;
text-align: center;
margin: 30px 0;
}
#verificationCode div {
height: 100px;
width: 680px;
}
.button {
color: #FE4F70;
margin-left: 10px;
height: 80px;
width: 80px;
resize: none;
font-size: 42px;
border: none;
outline: none;
padding: 10px 15px;
background: #ededed;
text-align: center;
border-radius: 17px;
box-shadow: 6px 6px 12px #cccccc,
-6px -6px 12px #ffffff;
}
.button:hover {
box-shadow: inset 6px 6px 4px #d1d1d1,
inset -6px -6px 4px #ffffff;
}
</style>
</head>
<body>
<table>
<tbody>
<tr>
<td>
<div id="top">
<table>
<tbody><tr><td></td></tr></tbody>
</table>
</div>
<div id="content">
<div id="content_top">
<strong>尊敬的用户:您好!</strong>
<strong>
您正在进行<span>{{ TITLE }}</span>操作,请在验证码中输入以下验证码完成操作:
</strong>
<div id="verificationCode">
{{ CODE }}
</div>
</div>
<div id="content_bottom">
<small>
注意:此操作可能会修改您的密码、登录邮箱或绑定手机。如非本人操作,请及时登录并修改密码以保证帐户安全
<br>(工作人员不会向你索取此验证码,请勿泄漏!)
</small>
</div>
</div>
<div id="bottom">
<div>
<p>此为系统邮件,请勿回复<br>
请保管好您的邮箱,避免账号被他人盗用
</p>
<p id="sign">——{{ PROJECTNAME }}</p>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>

7
utils/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 01:59
# @UpdateTime : 2025/01/18 01:59
# @Author : sonder
# @File : __init__.py.py
# @Software : PyCharm
# @Comment : 本程序

112
utils/captcha.py Normal file
View File

@@ -0,0 +1,112 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/26 20:59
# @UpdateTime : 2025/01/26 20:59
# @Author : sonder
# @File : captcha.py
# @Software : PyCharm
# @Comment : 本程序
import base64
import io
import os
import random
import string
from PIL import Image, ImageDraw, ImageFont
class Captcha:
"""
验证码类
"""
@classmethod
async def create_captcha(cls, captcha_type: str = "0"):
"""
生成验证码
:param captcha_type: 验证码类型0为数字1为数字和字母
:return: 验证码图片和验证码base64字符串
"""
# 创建空白图像
image = Image.new('RGB', (120, 40), color='#EAEAEA')
draw = ImageDraw.Draw(image)
# 设置字体
font_path = os.path.join(os.path.abspath(os.getcwd()), 'assets', 'font', 'MiSans-Medium.ttf')
font = ImageFont.truetype(font_path, size=25)
if captcha_type == '0':
# 生成两个0-9之间的随机整数
num1 = random.randint(0, 9)
num2 = random.randint(0, 9)
# 从运算符列表中随机选择一个
operational_character_list = ['+', '-', '*']
operational_character = random.choice(operational_character_list)
# 根据选择的运算符进行计算
if operational_character == '+':
result = num1 + num2
elif operational_character == '-':
result = num1 - num2
else:
result = num1 * num2
# 生成算术题文本
text = f'{num1} {operational_character} {num2} = ?'
# 计算文本宽度以居中显示
text_width = draw.textlength(text, font=font)
x = (120 - text_width) / 2
draw.text((x, 5), text, fill='blue', font=font)
else:
# 生成随机字母和数字组合
result = ''.join(random.choices(string.ascii_letters + string.digits, k=4))
# 绘制每个字符,并添加随机旋转和倾斜
x = 10
for char in result:
# 创建单个字符的图像
char_image = Image.new('RGBA', (25, 40), color=(234, 234, 234, 0))
char_draw = ImageDraw.Draw(char_image)
char_draw.text((0, 0), char, font=font, fill=(0, 0, 255))
# 随机旋转字符
char_image = char_image.rotate(random.randint(-40, 40), expand=1)
# 随机倾斜字符
char_image = char_image.transform(char_image.size, Image.AFFINE,
(1, random.uniform(-0.3, 0.3), 0, 0, 1, 0))
# 将字符粘贴到主图像上
image.paste(char_image, (x, 0), char_image)
x += 25
# 添加干扰元素
cls._add_noise(image)
cls._add_lines(image)
# 将图像数据保存到内存中
buffer = io.BytesIO()
image.save(buffer, format='PNG')
# 将图像数据转换为base64字符串
base64_string = base64.b64encode(buffer.getvalue()).decode()
return [base64_string, result]
@staticmethod
def _add_noise(image):
"""
添加噪点干扰
"""
draw = ImageDraw.Draw(image)
for _ in range(150): # 添加100个噪点
x = random.randint(0, 120)
y = random.randint(0, 40)
draw.point((x, y), fill=(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)))
@staticmethod
def _add_lines(image):
"""
添加干扰线
"""
draw = ImageDraw.Draw(image)
for _ in range(10): # 添加5条干扰线
x1 = random.randint(0, 120)
y1 = random.randint(0, 40)
x2 = random.randint(0, 120)
y2 = random.randint(0, 40)
draw.line((x1, y1, x2, y2),
fill=(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)),
width=1)

36
utils/common.py Normal file
View File

@@ -0,0 +1,36 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/19 00:52
# @UpdateTime : 2025/01/19 00:52
# @Author : sonder
# @File : common.py
# @Software : PyCharm
# @Comment : 本程序
def bytes2human(n, format_str='%(value).1f%(symbol)s'):
"""Used by various scripts. See:
http://goo.gl/zeJZl
>>> bytes2human(10000)
'9.8K'
>>> bytes2human(100001221)
'95.4M'
"""
symbols = ('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')
prefix = {}
for i, s in enumerate(symbols[1:]):
prefix[s] = 1 << (i + 1) * 10
for symbol in reversed(symbols[1:]):
if n >= prefix[symbol]:
value = float(n) / prefix[symbol]
return format_str % locals()
return format_str % dict(symbol=symbols[0], value=n)
async def filterKeyValues(dataList: list, key: str) -> list:
"""
获取列表字段数据
:param dataList: 数据列表
:param key: 关键字
:return:
"""
return [item[key] for item in dataList]

180
utils/cron.py Normal file
View File

@@ -0,0 +1,180 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/19 00:53
# @UpdateTime : 2025/01/19 00:53
# @Author : sonder
# @File : cron.py
# @Software : PyCharm
# @Comment : 本程序
import re
from datetime import datetime
class Cron:
"""
Cron表达式工具类
"""
@classmethod
def __valid_range(cls, search_str: str, start_range: int, end_range: int):
match = re.match(r'^(\d+)-(\d+)$', search_str)
if match:
start, end = int(match.group(1)), int(match.group(2))
return start_range <= start < end <= end_range
return False
@classmethod
def __valid_sum(
cls, search_str: str, start_range_a: int, start_range_b: int, end_range_a: int, end_range_b: int,
sum_range: int
):
match = re.match(r'^(\d+)/(\d+)$', search_str)
if match:
start, end = int(match.group(1)), int(match.group(2))
return (
start_range_a <= start <= start_range_b
and end_range_a <= end <= end_range_b
and start + end <= sum_range
)
return False
@classmethod
def validate_second_or_minute(cls, second_or_minute: str):
"""
校验秒或分钟值是否正确
:param second_or_minute: 秒或分钟值
:return: 校验结果
"""
if (
second_or_minute == '*'
or ('-' in second_or_minute and cls.__valid_range(second_or_minute, 0, 59))
or ('/' in second_or_minute and cls.__valid_sum(second_or_minute, 0, 58, 1, 59, 59))
or re.match(r'^(?:[0-5]?\d|59)(?:,[0-5]?\d|59)*$', second_or_minute)
):
return True
return False
@classmethod
def validate_hour(cls, hour: str):
"""
校验小时值是否正确
:param hour: 小时值
:return: 校验结果
"""
if (
hour == '*'
or ('-' in hour and cls.__valid_range(hour, 0, 23))
or ('/' in hour and cls.__valid_sum(hour, 0, 22, 1, 23, 23))
or re.match(r'^(?:0|[1-9]|1\d|2[0-3])(?:,(?:0|[1-9]|1\d|2[0-3]))*$', hour)
):
return True
return False
@classmethod
def validate_day(cls, day: str):
"""
校验日值是否正确
:param day: 日值
:return: 校验结果
"""
if (
day in ['*', '?', 'L']
or ('-' in day and cls.__valid_range(day, 1, 31))
or ('/' in day and cls.__valid_sum(day, 1, 30, 1, 30, 31))
or ('W' in day and re.match(r'^(?:[1-9]|1\d|2\d|3[01])W$', day))
or re.match(r'^(?:0|[1-9]|1\d|2[0-9]|3[0-1])(?:,(?:0|[1-9]|1\d|2[0-9]|3[0-1]))*$', day)
):
return True
return False
@classmethod
def validate_month(cls, month: str):
"""
校验月值是否正确
:param month: 月值
:return: 校验结果
"""
if (
month == '*'
or ('-' in month and cls.__valid_range(month, 1, 12))
or ('/' in month and cls.__valid_sum(month, 1, 11, 1, 11, 12))
or re.match(r'^(?:0|[1-9]|1[0-2])(?:,(?:0|[1-9]|1[0-2]))*$', month)
):
return True
return False
@classmethod
def validate_week(cls, week: str):
"""
校验周值是否正确
:param week: 周值
:return: 校验结果
"""
if (
week in ['*', '?']
or ('-' in week and cls.__valid_range(week, 1, 7))
or ('#' in week and re.match(r'^[1-7]#[1-4]$', week))
or ('L' in week and re.match(r'^[1-7]L$', week))
or re.match(r'^[1-7](?:(,[1-7]))*$', week)
):
return True
return False
@classmethod
def validate_year(cls, year: str):
"""
校验年值是否正确
:param year: 年值
:return: 校验结果
"""
current_year = int(datetime.now().year)
future_years = [current_year + i for i in range(9)]
if (
year == '*'
or ('-' in year and cls.__valid_range(year, current_year, 2099))
or ('/' in year and cls.__valid_sum(year, current_year, 2098, 1, 2099 - current_year, 2099))
or ('#' in year and re.match(r'^[1-7]#[1-4]$', year))
or ('L' in year and re.match(r'^[1-7]L$', year))
or (
(len(year) == 4 or ',' in year)
and all(int(item) in future_years and current_year <= int(item) <= 2099 for item in year.split(','))
)
):
return True
return False
@classmethod
def validate_cron_expression(cls, cron_expression: str):
"""
校验Cron表达式是否正确
:param cron_expression: Cron表达式
:return: 校验结果
"""
values = cron_expression.split()
if len(values) != 6 and len(values) != 7:
return False
second_validation = cls.validate_second_or_minute(values[0])
minute_validation = cls.validate_second_or_minute(values[1])
hour_validation = cls.validate_hour(values[2])
day_validation = cls.validate_day(values[3])
month_validation = cls.validate_month(values[4])
week_validation = cls.validate_week(values[5])
validation = (
second_validation
and minute_validation
and hour_validation
and day_validation
and month_validation
and week_validation
)
if len(values) == 6:
return validation
if len(values) == 7:
year_validation = cls.validate_year(values[6])
return validation and year_validation

124
utils/log.py Normal file
View File

@@ -0,0 +1,124 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 02:08
# @UpdateTime : 2025/01/18 02:08
# @Author : sonder
# @File : log.py
# @Software : PyCharm
# @Comment : 本程序
import os
import sys
import time
from loguru import logger
# 日志存储目录
log_path = os.path.join(os.getcwd(), 'logs')
if not os.path.exists(log_path):
os.makedirs(log_path) # 如果目录不存在,则创建
# 按天创建日志目录
daily_log_path = os.path.join(log_path, time.strftime("%Y-%m-%d"))
if not os.path.exists(daily_log_path):
os.makedirs(daily_log_path)
# 定义按级别分开的日志文件路径
log_path_debug = os.path.join(daily_log_path, 'debug.log')
log_path_info = os.path.join(daily_log_path, 'info.log')
log_path_error = os.path.join(daily_log_path, 'error.log')
log_path_warning = os.path.join(daily_log_path, 'warning.log')
log_path_sql = os.path.join(daily_log_path, 'sql.log') # SQL 查询日志文件
# 定义合并后的日志文件路径
log_path_all = os.path.join(daily_log_path, 'all.log')
# 移除默认的日志处理器
logger.remove()
# 添加控制台日志处理器(彩色输出)
logger.add(
sink=sys.stdout,
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
level="DEBUG", # 控制台输出所有级别的日志
colorize=True, # 启用彩色输出
enqueue=True, # 异步写入日志
)
# 添加按级别分开的日志文件处理器
logger.add(
sink=log_path_debug,
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} - {message}",
level="DEBUG", # 只记录 DEBUG 级别的日志
rotation="50 MB", # 日志文件大小达到 50MB 时轮换
retention="30 days", # 日志文件保留 30 天
compression="zip", # 压缩旧日志文件
encoding="utf-8",
enqueue=True, # 异步写入日志
filter=lambda record: record["level"].name == "DEBUG", # 只处理 DEBUG 级别的日志
)
logger.add(
sink=log_path_info,
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} - {message}",
level="INFO", # 只记录 INFO 级别的日志
rotation="50 MB",
retention="30 days",
compression="zip",
encoding="utf-8",
enqueue=True,
filter=lambda record: record["level"].name == "INFO", # 只处理 INFO 级别的日志
)
logger.add(
sink=log_path_warning,
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} - {message}",
level="WARNING", # 只记录 WARNING 级别的日志
rotation="50 MB",
retention="30 days",
compression="zip",
encoding="utf-8",
enqueue=True,
filter=lambda record: record["level"].name == "WARNING", # 只处理 WARNING 级别的日志
)
logger.add(
sink=log_path_error,
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} - {message}",
level="ERROR", # 只记录 ERROR 级别的日志
rotation="50 MB",
retention="30 days",
compression="zip",
encoding="utf-8",
enqueue=True,
filter=lambda record: record["level"].name == "ERROR", # 只处理 ERROR 级别的日志
)
# 添加 SQL 查询日志文件处理器
logger.add(
sink=log_path_sql,
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} - {message}",
level="DEBUG", # 记录所有 SQL 查询日志
rotation="50 MB",
retention="30 days",
compression="zip",
encoding="utf-8",
enqueue=True,
filter=lambda record: "tortoise.db_client" in record["name"], # 只处理 SQL 查询日志
)
# 添加合并后的日志文件处理器
logger.add(
sink=log_path_all,
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} - {message}",
level="DEBUG", # 记录所有级别的日志
rotation="50 MB",
retention="30 days",
compression="zip",
encoding="utf-8",
enqueue=True,
)
# 自定义日志颜色
logger.level("DEBUG", color="<blue>") # DEBUG 级别为蓝色
logger.level("INFO", color="<green>") # INFO 级别为绿色
logger.level("WARNING", color="<yellow>") # WARNING 级别为金色
logger.level("ERROR", color="<red>") # ERROR 级别为红色

105
utils/mail.py Normal file
View File

@@ -0,0 +1,105 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/26 21:51
# @UpdateTime : 2025/01/26 21:51
# @Author : sonder
# @File : mail.py
# @Software : PyCharm
# @Comment : 本程序
import random
from datetime import timedelta
from email.message import EmailMessage
from email.utils import formataddr
from aiosmtplib import send
from fastapi import Request
from jinja2 import Environment, FileSystemLoader
from config.constant import RedisKeyConfig
from config.env import AppConfig, EmailConfig
from utils.log import logger
class Email:
"""
邮件发送类,用于发送邮件。
"""
@classmethod
async def generate_verification_code(cls, length: int = 4) -> str:
"""
随机生成数字验证码
:param length: 验证码长度
:return:
"""
return ''.join(str(random.randint(0, 9)) for _ in range(length))
@classmethod
async def send_email(cls, request: Request, username: str, title: str = "注册", mail: str = "") -> bool:
"""
发送邮件
:param request: 请求对象
:param username: 用户账号
:param title: 邮件标题
:param mail: 邮箱地址
"""
code = await cls.generate_verification_code(4)
codeStr = ""
for i in code:
codeStr += f"""<button class="button">{i}</button>"""
env = Environment(loader=FileSystemLoader('templates'))
template = env.get_template('mail_en.html')
content = template.render(
TITLE=title,
CODE=codeStr,
PROJECTNAME=AppConfig.app_name,
)
subject = f"{AppConfig.app_name}-{title} Verification Code"
sendName = AppConfig.app_name
hostname = EmailConfig.email_host
port = EmailConfig.email_port
message = EmailMessage()
message["From"] = formataddr((sendName, EmailConfig.email_username))
message["To"] = mail
message["Subject"] = subject
message.set_content(content, subtype="html")
try:
await send(
message,
hostname=hostname,
port=port,
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))
logger.info(f"发送邮件至{mail}成功,验证码:{code}")
return True
except Exception as e:
logger.error(e)
return False
@classmethod
async def verify_code(cls, request: Request, username: str, mail: str, code: str) -> dict:
"""
验证验证码
:param request: 请求对象
:param username: 用户账号
:param mail: 邮箱地址
:param code: 验证码
"""
redis_code = await request.app.state.redis.get(f"{RedisKeyConfig.EMAIL_CODES.key}:{mail}-{username}")
if redis_code is None:
return {
"status": False,
"msg": "验证码已过期"
}
if str(redis_code).lower() == code.lower():
await request.app.state.redis.delete(f"{RedisKeyConfig.EMAIL_CODES.key}:{mail}-{username}")
return {
"status": True,
"msg": "验证码正确"
}
return {
"status": False,
"msg": "验证码错误"
}

46
utils/password.py Normal file
View File

@@ -0,0 +1,46 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 02:34
# @UpdateTime : 2025/01/18 02:34
# @Author : sonder
# @File : password.py
# @Software : PyCharm
# @Comment : 本程序
import hashlib
from config.env import JwtConfig
class Password:
"""
密码工具类
"""
@classmethod
async def verify_password(cls, plain_password, hashed_password):
"""
工具方法:校验当前输入的密码与数据库存储的密码是否一致
:param plain_password: 当前输入的密码
:param hashed_password: 数据库存储的密码
:return: 校验结果
"""
salt = JwtConfig.jwt_salt
# 将盐值和密码拼接在一起
password_with_salt = (salt + plain_password).encode('utf-8')
# 使用SHA256算法对拼接后的密码进行加密
password_hashed = hashlib.sha256(password_with_salt).hexdigest()
return password_hashed == hashed_password
@classmethod
async def get_password_hash(cls, input_password: str):
"""
工具方法:对当前输入的密码进行加密
:param input_password: 输入的密码
:return: 加密成功的密码
"""
salt = JwtConfig.jwt_salt
# 将盐值和密码拼接在一起
password_with_salt = (salt + input_password).encode('utf-8')
# 使用SHA256算法对拼接后的密码进行加密
return hashlib.sha256(password_with_salt).hexdigest()

225
utils/response.py Normal file
View File

@@ -0,0 +1,225 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 02:13
# @UpdateTime : 2025/01/18 02:13
# @Author : sonder
# @File : response.py
# @Software : PyCharm
# @Comment : 本程序
from datetime import datetime
from typing import Any, Dict, Optional
from fastapi import status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse, StreamingResponse
from pydantic import BaseModel
from config.constant import HttpStatusConstant
class Response:
"""
响应工具类,用于统一封装接口返回格式。
"""
@classmethod
def _build_response(
cls,
code: int,
msg: str,
data: Optional[Any] = None,
rows: Optional[Any] = None,
dict_content: Optional[Dict] = None,
model_content: Optional[BaseModel] = None,
success: bool = True,
) -> Dict:
"""
构建统一的响应结果字典。
:param code: 状态码
:param msg: 响应消息
:param data: 响应数据
:param rows: 响应行数据(通常用于分页)
:param dict_content: 自定义字典内容
:param model_content: 自定义 Pydantic 模型内容
:param success: 是否成功
:return: 统一的响应结果字典
"""
result = {
"code": code,
"msg": msg,
"success": success,
"time": datetime.now().isoformat(), # 添加时间戳
}
# 添加可选字段
if data is not None:
result["data"] = data
if rows is not None:
result["rows"] = rows
if dict_content is not None:
result.update(dict_content)
if model_content is not None:
result.update(model_content.model_dump(by_alias=True))
return result
@classmethod
def success(
cls,
msg: str = "操作成功",
data: Optional[Any] = None,
rows: Optional[Any] = None,
dict_content: Optional[Dict] = None,
model_content: Optional[BaseModel] = None,
) -> JSONResponse:
"""
成功响应方法。
:param msg: 响应消息,默认为 "操作成功"
:param data: 响应数据
:param rows: 响应行数据(通常用于分页)
:param dict_content: 自定义字典内容
:param model_content: 自定义 Pydantic 模型内容
:return: JSONResponse 对象
"""
result = cls._build_response(
code=HttpStatusConstant.SUCCESS,
msg=msg,
data=data,
rows=rows,
dict_content=dict_content,
model_content=model_content,
success=True,
)
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder(result))
@classmethod
def failure(
cls,
msg: str = "操作失败",
data: Optional[Any] = None,
rows: Optional[Any] = None,
dict_content: Optional[Dict] = None,
model_content: Optional[BaseModel] = None,
) -> JSONResponse:
"""
失败响应方法。
:param msg: 响应消息,默认为 "操作失败"
:param data: 响应数据
:param rows: 响应行数据(通常用于分页)
:param dict_content: 自定义字典内容
:param model_content: 自定义 Pydantic 模型内容
:return: JSONResponse 对象
"""
result = cls._build_response(
code=400,
msg=msg,
data=data,
rows=rows,
dict_content=dict_content,
model_content=model_content,
success=False,
)
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder(result))
@classmethod
def unauthorized(
cls,
msg: str = "登录信息已过期,访问系统资源失败",
data: Optional[Any] = None,
rows: Optional[Any] = None,
dict_content: Optional[Dict] = None,
model_content: Optional[BaseModel] = None,
) -> JSONResponse:
"""
未认证响应方法。
:param msg: 响应消息,默认为 "登录信息已过期,访问系统资源失败"
:param data: 响应数据
:param rows: 响应行数据(通常用于分页)
:param dict_content: 自定义字典内容
:param model_content: 自定义 Pydantic 模型内容
:return: JSONResponse 对象
"""
result = cls._build_response(
code=HttpStatusConstant.UNAUTHORIZED,
msg=msg,
data=data,
rows=rows,
dict_content=dict_content,
model_content=model_content,
success=False,
)
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder(result))
@classmethod
def forbidden(
cls,
msg: str = "该用户无此接口权限",
data: Optional[Any] = None,
rows: Optional[Any] = None,
dict_content: Optional[Dict] = None,
model_content: Optional[BaseModel] = None,
) -> JSONResponse:
"""
未授权响应方法。
:param msg: 响应消息,默认为 "该用户无此接口权限"
:param data: 响应数据
:param rows: 响应行数据(通常用于分页)
:param dict_content: 自定义字典内容
:param model_content: 自定义 Pydantic 模型内容
:return: JSONResponse 对象
"""
result = cls._build_response(
code=HttpStatusConstant.FORBIDDEN,
msg=msg,
data=data,
rows=rows,
dict_content=dict_content,
model_content=model_content,
success=False,
)
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder(result))
@classmethod
def error(
cls,
msg: str = "接口异常",
data: Optional[Any] = None,
rows: Optional[Any] = None,
dict_content: Optional[Dict] = None,
model_content: Optional[BaseModel] = None,
) -> JSONResponse:
"""
错误响应方法。
:param msg: 响应消息,默认为 "接口异常"
:param data: 响应数据
:param rows: 响应行数据(通常用于分页)
:param dict_content: 自定义字典内容
:param model_content: 自定义 Pydantic 模型内容
:return: JSONResponse 对象
"""
result = cls._build_response(
code=HttpStatusConstant.ERROR,
msg=msg,
data=data,
rows=rows,
dict_content=dict_content,
model_content=model_content,
success=False,
)
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder(result))
@classmethod
def streaming(cls, data: Any) -> StreamingResponse:
"""
流式响应方法。
:param data: 流式传输的内容
:return: StreamingResponse 对象
"""
return StreamingResponse(content=data, status_code=status.HTTP_200_OK)

109
utils/string.py Normal file
View File

@@ -0,0 +1,109 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 23:58
# @UpdateTime : 2025/01/18 23:58
# @Author : sonder
# @File : string.py
# @Software : PyCharm
# @Comment : 本程序
from typing import List
from config.constant import CommonConstant
class String:
"""
字符串工具类
"""
@classmethod
def is_blank(cls, string: str) -> bool:
"""
校验字符串是否为''或全空格
:param string: 需要校验的字符串
:return: 校验结果
"""
if string is None:
return False
str_len = len(string)
if str_len == 0:
return True
else:
for i in range(str_len):
if string[i] != ' ':
return False
return True
@classmethod
def is_empty(cls, string) -> bool:
"""
校验字符串是否为''或None
:param string: 需要校验的字符串
:return: 校验结果
"""
return string is None or len(string) == 0
@classmethod
def is_http(cls, link: str):
"""
判断是否为http(s)://开头
:param link: 链接
:return: 是否为http(s)://开头
"""
return link.startswith(CommonConstant.HTTP) or link.startswith(CommonConstant.HTTPS)
@classmethod
def contains_ignore_case(cls, search_str: str, compare_str: str):
"""
查找指定字符串是否包含指定字符串同时串忽略大小写
:param search_str: 查找的字符串
:param compare_str: 比对的字符串
:return: 查找结果
"""
if compare_str and search_str:
return compare_str.lower() in search_str.lower()
return False
@classmethod
def contains_any_ignore_case(cls, search_str: str, compare_str_list: List[str]):
"""
查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
:param search_str: 查找的字符串
:param compare_str_list: 比对的字符串列表
:return: 查找结果
"""
if search_str and compare_str_list:
for compare_str in compare_str_list:
return cls.contains_ignore_case(search_str, compare_str)
return False
@classmethod
def startswith_case(cls, search_str: str, compare_str: str):
"""
查找指定字符串是否以指定字符串开头
:param search_str: 查找的字符串
:param compare_str: 比对的字符串
:return: 查找结果
"""
if compare_str and search_str:
return search_str.startswith(compare_str)
return False
@classmethod
def startswith_any_case(cls, search_str: str, compare_str_list: List[str]):
"""
查找指定字符串是否以指定字符串列表中的任意一个字符串开头
:param search_str: 查找的字符串
:param compare_str_list: 比对的字符串列表
:return: 查找结果
"""
if search_str and compare_str_list:
for compare_str in compare_str_list:
return cls.startswith_case(search_str, compare_str)
return False

114
utils/upload.py Normal file
View File

@@ -0,0 +1,114 @@
# _*_ coding : UTF-8 _*_
# @Time : 2025/01/18 02:37
# @UpdateTime : 2025/01/18 02:37
# @Author : sonder
# @File : upload.py
# @Software : PyCharm
# @Comment : 本程序
import os
import random
from datetime import datetime
from fastapi import UploadFile
from config.env import UploadConfig
class Upload:
"""
上传工具类
"""
@classmethod
def generate_random_number(cls):
"""
生成3位数字构成的字符串
:return: 3位数字构成的字符串
"""
random_number = random.randint(1, 999)
return f'{random_number:03}'
@classmethod
def check_file_exists(cls, filepath: str):
"""
检查文件是否存在
:param filepath: 文件路径
:return: 校验结果
"""
return os.path.exists(filepath)
@classmethod
def check_file_extension(cls, file: UploadFile):
"""
检查文件后缀是否合法
:param file: 文件对象
:return: 校验结果
"""
file_extension = file.filename.rsplit('.', 1)[-1]
if file_extension in UploadConfig.DEFAULT_ALLOWED_EXTENSION:
return True
return False
@classmethod
def check_file_timestamp(cls, filename: str):
"""
校验文件时间戳是否合法
:param filename: 文件名称
:return: 校验结果
"""
timestamp = filename.rsplit('.', 1)[0].split('_')[-1].split(UploadConfig.UPLOAD_MACHINE)[0]
try:
datetime.strptime(timestamp, '%Y%m%d%H%M%S')
return True
except ValueError:
return False
@classmethod
def check_file_machine(cls, filename: str):
"""
校验文件机器码是否合法
:param filename: 文件名称
:return: 校验结果
"""
if filename.rsplit('.', 1)[0][-4] == UploadConfig.UPLOAD_MACHINE:
return True
return False
@classmethod
def check_file_random_code(cls, filename: str):
"""
校验文件随机码是否合法
:param filename: 文件名称
:return: 校验结果
"""
valid_code_list = [f'{i:03}' for i in range(1, 999)]
if filename.rsplit('.', 1)[0][-3:] in valid_code_list:
return True
return False
@classmethod
def generate_file(cls, filepath: str):
"""
根据文件生成二进制数据
:param filepath: 文件路径
:yield: 二进制数据
"""
with open(filepath, 'rb') as response_file:
yield from response_file
@classmethod
def delete_file(cls, filepath: str):
"""
根据文件路径删除对应文件
:param filepath: 文件路径
"""
os.remove(filepath)

48
uvicorn_config.json Normal file
View File

@@ -0,0 +1,48 @@
{
"version": 1,
"disable_existing_loggers": false,
"formatters": {
"default": {
"()": "uvicorn.logging.DefaultFormatter",
"fmt": "%(levelprefix)s %(message)s",
"use_colors": true,
"datefmt": "%Y-%m-%d %H:%M:%S"
},
"access": {
"()": "uvicorn.logging.AccessFormatter",
"fmt": "%(asctime)s - %(levelprefix)s %(client_addr)s - \"%(request_line)s\" %(status_code)s",
"use_colors": true,
"datefmt": "%Y-%m-%d %H:%M:%S"
}
},
"handlers": {
"default": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr"
},
"access": {
"formatter": "access",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout"
}
},
"loggers": {
"uvicorn": {
"handlers": [
"default"
],
"level": "INFO"
},
"uvicorn.error": {
"level": "INFO"
},
"uvicorn.access": {
"handlers": [
"access"
],
"level": "INFO",
"propagate": false
}
}
}