refactor: 重构服务层方法命名规范

fix: 修复文件下载和删除功能
fix: 修复Redis哈希获取方法返回值类型
fix: 修复权限检查逻辑

perf: 优化部门和服务详情查询性能
perf: 优化角色数据范围显示

style: 清理无用导入和注释
style: 统一CRUD方法命名

docs: 更新main.py中的命令说明

chore: 移动IP定位工具类位置
chore: 更新.gitignore忽略迁移版本文件
This commit is contained in:
zhangtao
2025-10-03 03:31:45 +08:00
parent 7b9d8d7854
commit 119964e6f9
42 changed files with 326 additions and 545 deletions
+11 -11
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Depends, Path, Query, Body, WebSocket, Request
from fastapi import APIRouter, Depends, Path, Body, WebSocket
from fastapi.responses import JSONResponse, StreamingResponse
from app.common.response import StreamResponse, SuccessResponse
@@ -41,54 +41,54 @@ async def chat_controller(
@MCPRouter.get("/detail/{id}", summary="获取 MCP 服务器详情", description="获取 MCP 服务器详情")
async def get_mcp_detail_controller(
async def detail_controller(
id: int = Path(..., description="MCP ID"),
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:query"]))
) -> JSONResponse:
result_dict = await McpService.get_mcp_detail_service(auth=auth, id=id)
result_dict = await McpService.detail_service(auth=auth, id=id)
logger.info(f"获取 MCP 服务器详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取 MCP 服务器详情成功")
@MCPRouter.get("/list", summary="查询 MCP 服务器列表", description="查询 MCP 服务器列表")
async def get_mcp_list_controller(
async def list_controller(
page: PaginationQueryParam = Depends(),
search: McpQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:query"]))
) -> JSONResponse:
result_dict_list = await McpService.get_mcp_list_service(auth=auth, search=search, order_by=page.order_by)
result_dict_list = await McpService.list_service(auth=auth, search=search, order_by=page.order_by)
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
logger.info(f"查询 MCP 服务器列表成功")
return SuccessResponse(data=result_dict, msg="查询 MCP 服务器列表成功")
@MCPRouter.post("/create", summary="创建 MCP 服务器", description="创建 MCP 服务器")
async def create_mcp_controller(
async def create_controller(
data: McpCreateSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:create"]))
) -> JSONResponse:
result_dict = await McpService.create_mcp_service(auth=auth, data=data)
result_dict = await McpService.create_service(auth=auth, data=data)
logger.info(f"创建 MCP 服务器成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建 MCP 服务器成功")
@MCPRouter.put("/update/{id}", summary="修改 MCP 服务器", description="修改 MCP 服务器")
async def update_mcp_controller(
async def update_controller(
data: McpUpdateSchema,
id: int = Path(..., description="MCP ID"),
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:update"]))
) -> JSONResponse:
result_dict = await McpService.update_mcp_service(auth=auth, id=id, data=data)
result_dict = await McpService.update_service(auth=auth, id=id, data=data)
logger.info(f"修改 MCP 服务器成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="修改 MCP 服务器成功")
@MCPRouter.delete("/delete", summary="删除 MCP 服务器", description="删除 MCP 服务器")
async def delete_mcp_controller(
async def delete_controller(
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(permissions=["ai:mcp:delete"]))
) -> JSONResponse:
await McpService.delete_mcp_service(auth=auth, ids=ids)
await McpService.delete_service(auth=auth, ids=ids)
logger.info(f"删除 MCP 服务器成功: {ids}")
return SuccessResponse(msg="删除 MCP 服务器成功")
-205
View File
@@ -1,205 +0,0 @@
# -*- coding: utf-8 -*-
import asyncio
import httpx, aiofiles
import numpy as np
from openai import AsyncOpenAI
from app.core.logger import logger
class AIClient:
def __init__(self, kb_filepath=None, model="qwen3:4b", embedding_model="nomic-embed-text"):
# AI模型配置
self.model = model
self.embedding_model = embedding_model
# 创建HTTP客户端
self.http_client = httpx.AsyncClient(
timeout=30.0,
follow_redirects=True
)
# 初始化OpenAI客户端(用于与Ollama交互)
self.client = AsyncOpenAI(
api_key="ollama",
base_url="http://127.0.0.1:11434/v1",
http_client=self.http_client
)
# 知识库相关属性
self.docs = []
self.embeds = None
# 如果提供了知识库文件路径,则加载知识库
self.kb_loaded = False
self.kb_filepath = kb_filepath
# RAG提示词模板
self.prompt_template = """
基于以下知识回答用户的问题:
1: %s
2: %s
3: %s
4: %s
5: %s
用户的问题: %s
请根据提供的知识,用中文简洁准确地回答问题。如果提供的知识不足以回答,请说明这一点。
"""
# 知识库相关异步方法
async def load_kb(self):
"""异步加载知识库文件"""
if not self.kb_filepath or self.kb_loaded:
return
try:
# 异步读取文件
async with aiofiles.open(self.kb_filepath, 'r', encoding='utf-8') as f:
content = await f.read()
self.docs = self.split_content(content)
self.embeds = await self.encode(self.docs)
self.kb_loaded = True
logger.info(f"成功加载知识库,包含 {len(self.docs)} 个文档片段")
except Exception as e:
logger.error(f"加载知识库失败: {str(e)}")
raise
@staticmethod
def split_content(content):
"""将内容分割成文档块"""
chunks = []
# 按换行符分割成行
lines = content.splitlines()
for line in lines:
stripped_line = line.strip()
if stripped_line:
chunks.append(stripped_line)
return chunks
async def encode(self, texts):
"""异步使用Ollama生成嵌入向量"""
embeds = []
for text in texts:
try:
# 使用AsyncOpenAI客户端异步生成嵌入
response = await self.client.embeddings.create(
model=self.embedding_model,
input=text
)
embeds.append(response.data[0].embedding)
except Exception as e:
logger.error(f"生成嵌入向量失败 for text: {text[:30]}...: {str(e)}")
# 对于失败的嵌入,添加一个零向量
embeds.append([0.0] * 768) # 假设nomic-embed-text生成768维向量
return np.array(embeds)
@staticmethod
def similarity(e1, e2):
"""计算余弦相似度"""
dot_product = np.dot(e1, e2)
norm_e1 = np.linalg.norm(e1)
norm_e2 = np.linalg.norm(e2)
if norm_e1 == 0 or norm_e2 == 0:
return 0.0 # 避免除以零
return dot_product / (norm_e1 * norm_e2)
async def search(self, text, top_k=5):
"""异步在知识库中搜索相似文本"""
# 确保知识库已加载
if not self.kb_loaded:
await self.load_kb()
if not self.embeds.any():
logger.warning("知识库为空,无法进行搜索")
return []
# 生成查询文本的嵌入向量
query_embed = (await self.encode([text]))[0]
# 计算与所有文档的相似度
sims = [(idx, self.similarity(query_embed, doc_embed))
for idx, doc_embed in enumerate(self.embeds)]
# 按相似度排序
sims.sort(key=lambda x: x[1], reverse=True)
# 返回前top_k个匹配结果
top_matches = [self.docs[idx] for idx, _ in sims[:top_k]]
return top_matches
# RAG相关异步方法
async def build_rag_prompt(self, query):
"""异步构建RAG提示词"""
# 搜索知识库获取相关上下文
context = await self.search(query)
# 确保上下文有5个元素,不足的用空字符串填充
context += [""] * (5 - len(context))
# 构建提示词
return self.prompt_template % (
context[0], context[1], context[2], context[3], context[4], query
)
# AI处理相关方法
async def process(self, query: str, use_rag=True):
"""处理查询并返回流式响应,支持RAG模式"""
system_prompt = """你是一个有用的AI助手,可以帮助用户回答问题和提供帮助。请用中文回答用户的问题。"""
# 如果启用RAG,构建增强提示词
if use_rag and self.kb_filepath:
user_query = await self.build_rag_prompt(query)
else:
user_query = query
try:
# 使用 await 调用异步客户端
response = await self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_query}
],
stream=True
)
# 流式返回响应
async for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
except Exception as e:
logger.error(f"AI处理查询失败: {str(e)}")
yield f"抱歉,处理您的请求时出现了错误: {str(e)}"
async def close(self):
"""关闭客户端连接"""
if hasattr(self, 'client'):
await self.client.close()
if hasattr(self, 'http_client'):
await self.http_client.aclose()
async def chat_query(message: str, kb_filepath=None):
"""处理聊天查询的异步函数"""
# 创建AI客户端实例,传入知识库文件路径
# message = message + "/no_think"
ai_client = AIClient(kb_filepath=kb_filepath)
try:
# 处理消息,启用RAG
async for response in ai_client.process(message, use_rag=True):
print(response, end='', flush=True)
finally:
# 确保关闭客户端连接
await ai_client.close()
if __name__ == "__main__":
# 在异步事件循环中运行聊天查询
asyncio.run(chat_query("帕金森氏症介绍,怎么治疗", kb_filepath='帕金森氏症en.txt'))
+6 -8
View File
@@ -1,22 +1,20 @@
# -*- coding: utf-8 -*-
from typing import AsyncGenerator, List, Dict, Optional, Any
from typing import List, Dict, Optional, Any
from app.core.exceptions import CustomException
from app.core.logger import logger
from app.api.v1.module_system.auth.schema import AuthSchema
from app.utils.ai_util import AIClient
from .schema import McpCreateSchema, McpUpdateSchema, McpOutSchema, ChatQuerySchema
from .param import McpQueryParam
from .crud import McpCRUD
from .model import McpModel
class McpService:
"""MCP服务层"""
@classmethod
async def get_mcp_detail_service(cls, auth: AuthSchema, id: int) -> Dict[str, Any]:
async def detail_service(cls, auth: AuthSchema, id: int) -> Dict[str, Any]:
"""详情"""
obj = await McpCRUD(auth).get_by_id_crud(id=id)
if not obj:
@@ -24,7 +22,7 @@ class McpService:
return McpOutSchema.model_validate(obj).model_dump()
@classmethod
async def get_mcp_list_service(cls, auth: AuthSchema, search: Optional[McpQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict[str, Any]]:
async def list_service(cls, auth: AuthSchema, search: Optional[McpQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict[str, Any]]:
"""列表查询"""
if order_by:
order_by = eval(str(order_by))
@@ -32,7 +30,7 @@ class McpService:
return [McpOutSchema.model_validate(obj).model_dump() for obj in obj_list]
@classmethod
async def create_mcp_service(cls, auth: AuthSchema, data: McpCreateSchema) -> Dict[str, Any]:
async def create_service(cls, auth: AuthSchema, data: McpCreateSchema) -> Dict[str, Any]:
"""创建"""
obj = await McpCRUD(auth).get_by_name_crud(name=data.name)
if obj:
@@ -41,7 +39,7 @@ class McpService:
return McpOutSchema.model_validate(obj).model_dump()
@classmethod
async def update_mcp_service(cls, auth: AuthSchema, id: int, data: McpUpdateSchema) -> Dict[str, Any]:
async def update_service(cls, auth: AuthSchema, id: int, data: McpUpdateSchema) -> Dict[str, Any]:
"""更新"""
obj = await McpCRUD(auth).get_by_id_crud(id=id)
if not obj:
@@ -53,7 +51,7 @@ class McpService:
return McpOutSchema.model_validate(obj).model_dump()
@classmethod
async def delete_mcp_service(cls, auth: AuthSchema, ids: List[int]) -> None:
async def delete_service(cls, auth: AuthSchema, ids: List[int]) -> None:
"""删除"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
@@ -26,7 +26,7 @@ async def get_obj_detail_controller(
id: int = Path(..., description="应用ID"),
auth: AuthSchema = Depends(AuthPermission(permissions=["application:myapp:query"]))
) -> JSONResponse:
result_dict = await ApplicationService.get_application_detail_service(id=id, auth=auth)
result_dict = await ApplicationService.detail_service(id=id, auth=auth)
logger.info(f"获取应用详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取应用详情成功")
@@ -36,7 +36,7 @@ async def get_obj_list_controller(
search: ApplicationQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(permissions=["application:myapp:query"]))
) -> JSONResponse:
result_dict_list = await ApplicationService.get_application_list_service(auth=auth, search=search, order_by=page.order_by)
result_dict_list = await ApplicationService.list_service(auth=auth, search=search, order_by=page.order_by)
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
logger.info(f"查询应用列表成功")
return SuccessResponse(data=result_dict, msg="查询应用列表成功")
@@ -46,7 +46,7 @@ async def create_obj_controller(
data: ApplicationCreateSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["application:myapp:create"]))
) -> JSONResponse:
result_dict = await ApplicationService.create_application_service(auth=auth, data=data)
result_dict = await ApplicationService.create_service(auth=auth, data=data)
logger.info(f"创建应用成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建应用成功")
@@ -56,7 +56,7 @@ async def update_obj_controller(
id: int = Path(..., description="应用ID"),
auth: AuthSchema = Depends(AuthPermission(permissions=["application:myapp:update"]))
) -> JSONResponse:
result_dict = await ApplicationService.update_application_service(auth=auth, id=id, data=data)
result_dict = await ApplicationService.update_service(auth=auth, id=id, data=data)
logger.info(f"修改应用成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="修改应用成功")
@@ -65,7 +65,7 @@ async def delete_obj_controller(
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(permissions=["application:myapp:delete"]))
) -> JSONResponse:
await ApplicationService.delete_application_service(auth=auth, ids=ids)
await ApplicationService.delete_service(auth=auth, ids=ids)
logger.info(f"删除应用成功: {ids}")
return SuccessResponse(msg="删除应用成功")
@@ -74,6 +74,6 @@ async def batch_set_available_obj_controller(
data: BatchSetAvailable,
auth: AuthSchema = Depends(AuthPermission(permissions=["application:myapp:patch"]))
) -> JSONResponse:
await ApplicationService.set_application_available_service(auth=auth, data=data)
await ApplicationService.set_available_service(auth=auth, data=data)
logger.info(f"批量修改应用状态成功: {data.ids}")
return SuccessResponse(msg="批量修改应用状态成功")
@@ -20,7 +20,7 @@ class ApplicationCRUD(CRUDBase[ApplicationModel, ApplicationCreateSchema, Applic
"""获取应用详情"""
return await self.get(id=id)
async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[ApplicationModel]:
async def list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[ApplicationModel]:
"""列表查询"""
return await self.list(search=search, order_by=order_by)
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
from typing import List, Dict
from typing import List, Dict, Optional
from app.core.base_schema import BatchSetAvailable
from app.core.exceptions import CustomException
@@ -17,7 +17,7 @@ class ApplicationService:
"""
@classmethod
async def get_application_detail_service(cls, auth: AuthSchema, id: int) -> Dict:
async def detail_service(cls, auth: AuthSchema, id: int) -> Dict:
"""获取应用详情"""
obj = await ApplicationCRUD(auth).get_by_id_crud(id=id)
if not obj:
@@ -25,18 +25,18 @@ class ApplicationService:
return ApplicationOutSchema.model_validate(obj).model_dump()
@classmethod
async def get_application_list_service(cls, auth: AuthSchema, search: ApplicationQueryParam = None, order_by: List[Dict[str, str]] = None) -> List[Dict]:
async def list_service(cls, auth: AuthSchema, search: Optional[ApplicationQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict]:
"""应用列表查询"""
if order_by:
order_by = eval(order_by) if isinstance(order_by, str) else order_by
# 过滤空值
search_dict = {k: v for k, v in search.__dict__.items() if v is not None} if search else {}
obj_list = await ApplicationCRUD(auth).get_list_crud(search=search_dict, order_by=order_by)
obj_list = await ApplicationCRUD(auth).list_crud(search=search_dict, order_by=order_by)
return [ApplicationOutSchema.model_validate(obj).model_dump() for obj in obj_list]
@classmethod
async def create_application_service(cls, auth: AuthSchema, data: ApplicationCreateSchema) -> Dict:
async def create_service(cls, auth: AuthSchema, data: ApplicationCreateSchema) -> Dict:
"""创建应用"""
# 检查名称是否重复
obj = await ApplicationCRUD(auth).get(name=data.name)
@@ -47,7 +47,7 @@ class ApplicationService:
return ApplicationOutSchema.model_validate(obj).model_dump()
@classmethod
async def update_application_service(cls, auth: AuthSchema, id: int, data: ApplicationUpdateSchema) -> Dict:
async def update_service(cls, auth: AuthSchema, id: int, data: ApplicationUpdateSchema) -> Dict:
"""更新应用"""
obj = await ApplicationCRUD(auth).get_by_id_crud(id=id)
if not obj:
@@ -62,7 +62,7 @@ class ApplicationService:
return ApplicationOutSchema.model_validate(obj).model_dump()
@classmethod
async def delete_application_service(cls, auth: AuthSchema, ids: list[int]) -> None:
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
"""删除应用"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
@@ -73,6 +73,6 @@ class ApplicationService:
await ApplicationCRUD(auth).delete_crud(ids=ids)
@classmethod
async def set_application_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
"""批量设置应用状态"""
await ApplicationCRUD(auth).set_available_crud(ids=data.ids, status=data.status)
@@ -1,12 +1,14 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Body, Depends, UploadFile, Request
from pathlib import Path
from fastapi import APIRouter, BackgroundTasks, Body, Depends, UploadFile, Request
from fastapi.responses import JSONResponse, FileResponse
from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute
from app.core.logger import logger
from app.common.response import SuccessResponse, UploadFileResponse
from app.utils.upload_util import UploadUtil
from .service import FileService
FileRouter = APIRouter(route_class=OperationLogRoute, prefix="/file", tags=["文件管理"])
@@ -22,8 +24,13 @@ async def upload_controller(
@FileRouter.post("/download", summary="下载文件", description="下载文件", dependencies=[Depends(AuthPermission(permissions=["common:file:download"]))])
async def download_controller(
file_path: str = Body(..., description="文件路径"),
background_tasks: BackgroundTasks,
file_path: str = Body(..., description="文件路径"),
delete: bool = Body(False, description="是否删除文件"),
) -> FileResponse:
result = await FileService.download_service(file_path=file_path)
if delete:
background_tasks.add_task(UploadUtil.delete_file, Path(file_path))
logger.info(f"下载文件成功")
return UploadFileResponse(file_path=result.file_path, file_name=result.file_name)
return UploadFileResponse(file_path=result.file_path, filename=result.file_name)
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from pathlib import Path
from typing import Dict
from fastapi import UploadFile, BackgroundTasks
@@ -33,12 +34,10 @@ class FileService:
@classmethod
async def download_service(cls, background_tasks: BackgroundTasks, file_path: str, delete: bool) -> DownloadFileSchema:
async def download_service(cls, file_path: str) -> DownloadFileSchema:
"""
下载文件
:param background_tasks: 后台任务
:param file_path: 文件路径
:param delete: 是否在下载完成后删除文件
:return: 结果
"""
if not file_path:
@@ -46,9 +45,8 @@ class FileService:
if not UploadUtil.check_file_exists(file_path):
raise CustomException(msg="文件不存在")
file_name = UploadUtil.download_file(file_path)
if delete:
background_tasks.add_task(UploadUtil.delete_file, file_path)
return DownloadFileSchema(
file_path=file_path,
file_name=file_name,
file_name=str(file_name),
)
@@ -3,7 +3,6 @@
from fastapi import APIRouter, Body, Depends, Path, UploadFile
from fastapi.responses import JSONResponse, StreamingResponse
import urllib.parse
import json
from app.common.response import StreamResponse, SuccessResponse
from app.common.request import PaginationService
@@ -29,7 +28,7 @@ async def get_obj_detail_controller(
id: int = Path(..., description="示例ID"),
auth: AuthSchema = Depends(AuthPermission(permissions=["demo:example:query"]))
) -> JSONResponse:
result_dict = await DemoService.get_demo_detail_service(id=id, auth=auth)
result_dict = await DemoService.detail_service(id=id, auth=auth)
logger.info(f"获取示例详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取示例详情成功")
@@ -39,7 +38,7 @@ async def get_obj_list_controller(
search: DemoQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(permissions=["demo:example:query"]))
) -> JSONResponse:
result_dict_list = await DemoService.get_demo_list_service(auth=auth, search=search, order_by=page.order_by)
result_dict_list = await DemoService.list_service(auth=auth, search=search, order_by=page.order_by)
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
logger.info("查询示例列表成功")
return SuccessResponse(data=result_dict, msg="查询示例列表成功")
@@ -49,7 +48,7 @@ async def create_obj_controller(
data: DemoCreateSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["demo:example:create"]))
) -> JSONResponse:
result_dict = await DemoService.create_demo_service(auth=auth, data=data)
result_dict = await DemoService.create_service(auth=auth, data=data)
logger.info(f"创建示例成功: {result_dict.get('name')}")
return SuccessResponse(data=result_dict, msg="创建示例成功")
@@ -59,7 +58,7 @@ async def update_obj_controller(
id: int = Path(..., description="示例ID"),
auth: AuthSchema = Depends(AuthPermission(permissions=["demo:example:update"]))
) -> JSONResponse:
result_dict = await DemoService.update_demo_service(auth=auth, id=id, data=data)
result_dict = await DemoService.update_service(auth=auth, id=id, data=data)
logger.info(f"修改示例成功: {result_dict.get('name')}")
return SuccessResponse(data=result_dict, msg="修改示例成功")
@@ -68,7 +67,7 @@ async def delete_obj_controller(
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(permissions=["demo:example:delete"]))
) -> JSONResponse:
await DemoService.delete_demo_service(auth=auth, ids=ids)
await DemoService.delete_service(auth=auth, ids=ids)
logger.info(f"删除示例成功: {ids}")
return SuccessResponse(msg="删除示例成功")
@@ -77,7 +76,7 @@ async def batch_set_available_obj_controller(
data: BatchSetAvailable,
auth: AuthSchema = Depends(AuthPermission(permissions=["demo:example:patch"]))
) -> JSONResponse:
await DemoService.set_demo_available_service(auth=auth, data=data)
await DemoService.set_available_service(auth=auth, data=data)
logger.info(f"批量修改示例状态成功: {data.ids}")
return SuccessResponse(msg="批量修改示例状态成功")
@@ -87,7 +86,7 @@ async def export_obj_list_controller(
auth: AuthSchema = Depends(AuthPermission(permissions=["demo:example:export"]))
) -> StreamingResponse:
# 获取全量数据
result_dict_list = await DemoService.get_demo_list_service(search=search, auth=auth)
result_dict_list = await DemoService.list_service(search=search, auth=auth)
export_result = await DemoService.batch_export_service(obj_list=result_dict_list)
logger.info('导出示例成功')
@@ -19,7 +19,7 @@ class DemoCRUD(CRUDBase[DemoModel, DemoCreateSchema, DemoUpdateSchema]):
"""详情"""
return await self.get(id=id)
async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[DemoModel]:
async def list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[DemoModel]:
"""列表查询"""
return await self.list(search=search, order_by=order_by)
@@ -22,7 +22,7 @@ class DemoService:
"""
@classmethod
async def get_demo_detail_service(cls, auth: AuthSchema, id: int) -> Dict:
async def detail_service(cls, auth: AuthSchema, id: int) -> Dict:
"""详情"""
obj = await DemoCRUD(auth).get_by_id_crud(id=id)
if not obj:
@@ -30,14 +30,14 @@ class DemoService:
return DemoOutSchema.model_validate(obj).model_dump()
@classmethod
async def get_demo_list_service(cls, auth: AuthSchema, search: Optional[DemoQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict]:
async def list_service(cls, auth: AuthSchema, search: Optional[DemoQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> List[Dict]:
"""列表查询"""
search_dict = search.__dict__ if search else None
obj_list = await DemoCRUD(auth).get_list_crud(search=search_dict, order_by=order_by)
obj_list = await DemoCRUD(auth).list_crud(search=search_dict, order_by=order_by)
return [DemoOutSchema.model_validate(obj).model_dump() for obj in obj_list]
@classmethod
async def create_demo_service(cls, auth: AuthSchema, data: DemoCreateSchema) -> Dict:
async def create_service(cls, auth: AuthSchema, data: DemoCreateSchema) -> Dict:
"""创建"""
obj = await DemoCRUD(auth).get(name=data.name)
if obj:
@@ -46,7 +46,7 @@ class DemoService:
return DemoOutSchema.model_validate(obj).model_dump()
@classmethod
async def update_demo_service(cls, auth: AuthSchema, id: int, data: DemoUpdateSchema) -> Dict:
async def update_service(cls, auth: AuthSchema, id: int, data: DemoUpdateSchema) -> Dict:
"""更新"""
# 检查数据是否存在
obj = await DemoCRUD(auth).get_by_id_crud(id=id)
@@ -62,7 +62,7 @@ class DemoService:
return DemoOutSchema.model_validate(obj).model_dump()
@classmethod
async def delete_demo_service(cls, auth: AuthSchema, ids: List[int]) -> None:
async def delete_service(cls, auth: AuthSchema, ids: List[int]) -> None:
"""删除"""
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
@@ -76,7 +76,7 @@ class DemoService:
await DemoCRUD(auth).delete_crud(ids=ids)
@classmethod
async def set_demo_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
"""批量设置状态"""
await DemoCRUD(auth).set_available_crud(ids=data.ids, status=data.status)
@@ -0,0 +1,221 @@
import re
from datetime import datetime
from typing import List
from app.common.constant import GenConstant
from app.config.setting import settings
from app.api.v1.module_generator.gencode.schema import GenTableColumnOutSchema as GenTableColumnSchema, GenTableOutSchema as GenTableSchema
from .string_util import StringUtil
class GenUtils:
"""代码生成器工具类"""
@classmethod
def init_table(cls, gen_table: GenTableSchema) -> None:
"""
初始化表信息
param gen_table: 业务表对象
param oper_name: 操作人
:return:
"""
gen_table.class_name = cls.convert_class_name(gen_table.table_name)
gen_table.package_name = settings.package_name
gen_table.module_name = cls.get_module_name(settings.package_name)
gen_table.business_name = cls.get_business_name(gen_table.table_name)
gen_table.function_name = cls.replace_text(gen_table.table_comment)
gen_table.function_author = settings.author
gen_table.created_at = datetime.now()
gen_table.updated_at = datetime.now()
@classmethod
def init_column_field(cls, column: GenTableColumnSchema, table: GenTableSchema) -> None:
"""
初始化列属性字段
param column: 业务表字段对象
param table: 业务表对象
:return:
"""
data_type = cls.get_db_type(column.column_type)
column_name = column.column_name
column.table_id = table.id
# 设置Python字段名
column.python_field = cls.to_camel_case(column_name)
# 设置默认类型
column.python_type = StringUtil.get_mapping_value_by_key_ignore_case(
GenConstant.DB_TO_PYTHON_TYPE_MAPPING, data_type
)
column.query_type = GenConstant.QUERY_EQ
if cls.arrays_contains(GenConstant.COLUMNTYPE_STR, data_type) or cls.arrays_contains(
GenConstant.COLUMNTYPE_TEXT, data_type
):
# 字符串长度超过500设置为文本域
column_length = cls.get_column_length(column.column_type)
html_type = (
GenConstant.HTML_TEXTAREA
if column_length >= 500 or cls.arrays_contains(GenConstant.COLUMNTYPE_TEXT, data_type)
else GenConstant.HTML_INPUT
)
column.html_type = html_type
elif cls.arrays_contains(GenConstant.COLUMNTYPE_TIME, data_type):
column.html_type = GenConstant.HTML_DATETIME
elif cls.arrays_contains(GenConstant.COLUMNTYPE_NUMBER, data_type):
column.html_type = GenConstant.HTML_INPUT
# 插入字段(默认所有字段都需要插入)
column.is_insert = GenConstant.REQUIRE
# 编辑字段
if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_EDIT, column_name) and not column.pk:
column.is_edit = GenConstant.REQUIRE
# 列表字段
if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_LIST, column_name) and not column.pk:
column.is_list = GenConstant.REQUIRE
# 查询字段
if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_QUERY, column_name) and not column.pk:
column.is_query = GenConstant.REQUIRE
# 查询字段类型
if column_name.lower().endswith('name'):
column.query_type = GenConstant.QUERY_LIKE
# 状态字段设置单选框
if column_name.lower().endswith('status'):
column.html_type = GenConstant.HTML_RADIO
# 类型&性别字段设置下拉框
elif column_name.lower().endswith('type') or column_name.lower().endswith('sex'):
column.html_type = GenConstant.HTML_SELECT
# 图片字段设置图片上传控件
elif column_name.lower().endswith('image'):
column.html_type = GenConstant.HTML_IMAGE_UPLOAD
# 文件字段设置文件上传控件
elif column_name.lower().endswith('file'):
column.html_type = GenConstant.HTML_FILE_UPLOAD
# 内容字段设置富文本控件
elif column_name.lower().endswith('content'):
column.html_type = GenConstant.HTML_EDITOR
column.create_by = table.create_by
column.create_time = datetime.now()
column.update_by = table.update_by
column.update_time = datetime.now()
@classmethod
def arrays_contains(cls, arr: List[str], target_value: str) -> bool:
"""
校验数组是否包含指定值
param arr: 数组
param target_value: 需要校验的值
:return: 校验结果
"""
return target_value in arr
@classmethod
def get_module_name(cls, package_name: str) -> str:
"""
获取模块名
param package_name: 包名
:return: 模块名
"""
return package_name.split('.')[-1]
@classmethod
def get_business_name(cls, table_name: str) -> str:
"""
获取业务名
param table_name: 业务表名
:return: 业务名
"""
return table_name.split('_')[-1]
@classmethod
def convert_class_name(cls, table_name: str) -> str:
"""
表名转换成Python类名
param table_name: 业务表名
:return: Python类名
"""
auto_remove_pre = settings.auto_remove_pre
table_prefix = settings.table_prefix
if auto_remove_pre and table_prefix:
search_list = table_prefix.split(',')
table_name = cls.replace_first(table_name, search_list)
return StringUtil.convert_to_camel_case(table_name)
@classmethod
def replace_first(cls, replacement: str, search_list: List[str]) -> str:
"""
批量替换前缀
param replacement: 需要被替换的字符串
param search_list: 可替换的字符串列表
:return: 替换后的字符串
"""
for search_string in search_list:
if replacement.startswith(search_string):
return replacement.replace(search_string, '', 1)
return replacement
@classmethod
def replace_text(cls, text: str) -> str:
"""
关键字替换
param text: 需要被替换的字符串
:return: 替换后的字符串
"""
return re.sub(r'(?:表|若依)', '', text)
@classmethod
def get_db_type(cls, column_type: str) -> str:
"""
获取数据库类型字段
param column_type: 字段类型
:return: 数据库类型
"""
if '(' in column_type:
return column_type.split('(')[0]
return column_type
@classmethod
def get_column_length(cls, column_type: str) -> int:
"""
获取字段长度
param column_type: 字段类型
:return: 字段长度
"""
if '(' in column_type:
length = len(column_type.split('(')[1].split(')')[0])
return length
return 0
@classmethod
def split_column_type(cls, column_type: str) -> List[str]:
"""
拆分列类型
param column_type: 字段类型
:return: 拆分结果
"""
if '(' in column_type and ')' in column_type:
return column_type.split('(')[1].split(')')[0].split(',')
return []
@classmethod
def to_camel_case(cls, text: str) -> str:
"""
将字符串转换为驼峰命名
param text: 需要转换的字符串
:return: 驼峰命名
"""
parts = text.split('_')
return parts[0] + ''.join(word.capitalize() for word in parts[1:])
@@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
import re
import httpx
from app.core.logger import logger
class IpLocalUtil:
"""
获取IP归属地工具类
"""
@classmethod
def is_valid_ip(cls, ip: str) -> bool:
"""
校验IP格式是否合法
:param ip: IP地址
:return: 是否合法
"""
ip_pattern = r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
return bool(re.match(ip_pattern, ip))
@classmethod
def is_private_ip(cls, ip: str) -> bool:
"""
校验IP是否为内网IP
:param ip: IP地址
:return: 是否为内网IP
"""
ip_parts = list(map(int, ip.split('.')))
# 检查是否为 10.0.0.0/8
if ip_parts[0] == 10:
return True
# 检查是否为 172.16.0.0/12
if ip_parts[0] == 172 and 16 <= ip_parts[1] <= 31:
return True
# 检查是否为 192.168.0.0/16
if ip_parts[0] == 192 and ip_parts[1] == 168:
return True
# 检查是否为 127.0.0.0/8
if ip_parts[0] == 127:
return True
return False
@classmethod
async def _make_api_request(cls, client, url):
"""
单独的 API 请求方法包含重试机制
"""
max_retries = 3
for attempt in range(max_retries):
try:
response = await client.get(url, timeout=10)
if response.status_code == 200:
return response
except Exception as e:
if attempt < max_retries - 1:
continue
logger.error(f"请求 {url} 失败: {e}")
return None
@classmethod
async def get_ip_location(cls, ip: str) -> str | None:
"""
获取IP归属地信息
:param ip: IP地址
:return: IP归属地信息
"""
# 校验IP格式
if not cls.is_valid_ip(ip):
logger.error(f"IP格式不合法: {ip}")
return "未知"
# 内网IP直接返回
if cls.is_private_ip(ip):
return '内网IP'
try:
# 使用ip-api.com API获取IP归属地信息
async with httpx.AsyncClient(timeout=10.0) as client:
# 尝试使用 ip9.com.cn API
url = f'https://ip9.com.cn/get?ip={ip}'
response = await cls._make_api_request(client, url)
if response and response.json().get('ret') == 200:
result = response.json().get('data', {})
return f"{result.get('country','')}-{result.get('prov','')}-{result.get('city','')}-{result.get('area','')}-{result.get('isp','')}"
# 尝试使用百度 API
url = f'https://qifu-api.baidubce.com/ip/geo/v1/district?ip={ip}'
response = await cls._make_api_request(client, url)
if response and response.json().get('code') == "Success":
data = response.json().get('data', {})
# 修正原代码中的格式错误
return f"{data.get('country','')}-{data.get('prov','')}-{data.get('city','')}-{data.get('district','')}-{data.get('isp','')}"
except Exception as e:
logger.error(f"获取IP归属地失败: {e}")
return "未知"
@@ -0,0 +1,489 @@
# -*- coding:utf-8 -*-
import json
import os
from datetime import datetime
from jinja2 import Environment, FileSystemLoader
from typing import Dict, List, Set
from app.common.constant import GenConstant
from app.config.setting import settings
from app.api.v1.module_generator.gencode.schema import GenTableOutSchema as GenTableSchema, GenTableColumnOutSchema as GenTableColumnSchema
from app.core.base_model import CamelCaseUtil, SnakeCaseUtil
from app.core.exceptions import CustomException
from .string_util import StringUtil
from . import jinja2_util
class TemplateInitializer:
"""
模板引擎初始化类
"""
@classmethod
def init_jinja2(cls):
"""
初始化 Jinja2 模板引擎
:return: Jinja2 环境对象
"""
try:
# 修复模板路径,使用正确的相对路径
template_dir = settings.TEMPLATE_DIR
env = Environment(
loader=FileSystemLoader(template_dir),
keep_trailing_newline=True,
trim_blocks=True,
lstrip_blocks=True,
)
env.filters.update(
{
'camel_to_snake': SnakeCaseUtil.camel_to_snake,
'snake_to_camel': CamelCaseUtil.snake_to_camel,
'snake_to_pascal_case': jinja2_util.snake_to_pascal_case,
'is_base_column': jinja2_util.is_base_column,
'get_column_options': jinja2_util.get_column_options,
'get_sqlalchemy_type': jinja2_util.get_sqlalchemy_type,
}
)
return env
except Exception as e:
raise RuntimeError(f'初始化Jinja2模板引擎失败: {e}')
class TemplateUtils:
"""
模板工具类
"""
# 项目路径
FRONTEND_PROJECT_PATH = 'frontend'
BACKEND_PROJECT_PATH = 'backend'
DEFAULT_PARENT_MENU_ID = '3'
@classmethod
def prepare_context(cls, gen_table: GenTableSchema):
"""
准备模板变量
:param gen_table: 生成表的配置信息
:return: 模板上下文字典
"""
if not gen_table.options:
raise CustomException(msg='请先完善生成配置信息')
class_name = gen_table.class_name
module_name = gen_table.module_name
business_name = gen_table.business_name
package_name = gen_table.package_name
tpl_category = gen_table.tpl_category
function_name = gen_table.function_name
context = {
'tplCategory': tpl_category,
'tableName': gen_table.table_name,
'functionName': function_name if StringUtil.is_not_empty(function_name) else '【请填写功能名称】',
'ClassName': class_name,
'className': class_name.lower(),
'moduleName': module_name,
'BusinessName': business_name.capitalize(),
'businessName': business_name,
'basePackage': cls.get_package_prefix(package_name),
'packageName': package_name,
'author': gen_table.function_author,
'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'pkColumn': gen_table.pk_column,
'doImportList': cls.get_do_import_list(gen_table),
'voImportList': cls.get_vo_import_list(gen_table),
'permissionPrefix': cls.get_permission_prefix(module_name, business_name),
'columns': gen_table.columns,
'table': gen_table,
'dicts': cls.get_dicts(gen_table),
'dbType': settings.DATABASE_TYPE,
'column_not_add_show': GenConstant.COLUMNNAME_NOT_ADD_SHOW,
'column_not_edit_show': GenConstant.COLUMNNAME_NOT_EDIT_SHOW,
}
# 设置菜单、树形结构、子表的上下文
cls.set_menu_context(context, gen_table)
if tpl_category == GenConstant.TPL_TREE:
cls.set_tree_context(context, gen_table)
if tpl_category == GenConstant.TPL_SUB:
cls.set_sub_context(context, gen_table)
return context
@classmethod
def set_menu_context(cls, context: Dict, gen_table: GenTableSchema):
"""
设置菜单上下文
:param context: 模板上下文字典
:param gen_table: 生成表的配置信息
:return: 新的模板上下文字典
"""
options = gen_table.options
if options:
params_obj = json.loads(options)
context['parentMenuId'] = cls.get_parent_menu_id(params_obj)
@classmethod
def set_tree_context(cls, context: Dict, gen_table: GenTableSchema):
"""
设置树形结构上下文
:param context: 模板上下文字典
:param gen_table: 生成表的配置信息
:return: 新的模板上下文字典
"""
options = gen_table.options
if options:
params_obj = json.loads(options)
context['treeCode'] = cls.get_tree_code(params_obj)
context['treeParentCode'] = cls.get_tree_parent_code(params_obj)
context['treeName'] = cls.get_tree_name(params_obj)
context['expandColumn'] = cls.get_expand_column(gen_table)
@classmethod
def set_sub_context(cls, context: Dict, gen_table: GenTableSchema):
"""
设置子表上下文
:param context: 模板上下文字典
:param gen_table: 生成表的配置信息
:return: 新的模板上下文字典
"""
sub_table = gen_table.sub_table
sub_table_name = gen_table.sub_table_name
sub_table_fk_name = gen_table.sub_table_fk_name
# 修复类型检查问题,确保sub_table存在
if sub_table:
sub_class_name = sub_table.class_name or ""
else:
sub_class_name = ""
sub_table_fk_class_name = StringUtil.convert_to_camel_case(sub_table_fk_name or "")
context['subTable'] = sub_table
context['subTableName'] = sub_table_name
context['subTableFkName'] = sub_table_fk_name
context['subTableFkClassName'] = sub_table_fk_class_name
context['subTableFkclassName'] = sub_table_fk_class_name.lower()
context['subClassName'] = sub_class_name
context['subclassName'] = sub_class_name.lower()
@classmethod
def get_template_list(cls, tpl_category: str, tpl_web_type: str):
"""
获取模板列表
:param tpl_category: 生成模板类型
:param tpl_web_type: 前端类型
:return: 模板列表
"""
use_web_type = 'vue'
if tpl_web_type == 'element-plus':
use_web_type = 'vue/v3'
templates = [
'python/controller.py.j2',
'python/crud.py.j2',
'python/model.py.j2',
'python/schema.py.j2',
'python/service.py.j2',
'sql/sql.j2',
'vue/api.js.j2',
]
if tpl_category == GenConstant.TPL_CRUD:
templates.append(f'{use_web_type}/index.vue.j2')
elif tpl_category == GenConstant.TPL_TREE:
templates.append(f'{use_web_type}/index-tree.vue.j2')
elif tpl_category == GenConstant.TPL_SUB:
templates.append(f'{use_web_type}/index.vue.j2')
# templates.append('python/sub-domain.python.jinja2')
return templates
@classmethod
def get_file_name(cls, template: List[str], gen_table: GenTableSchema):
"""
根据模板生成文件名
:param template: 模板列表
:param gen_table: 生成表的配置信息
:return: 模板生成文件名
"""
package_name = gen_table.package_name
module_name = gen_table.module_name
business_name = gen_table.business_name
vue_path = cls.FRONTEND_PROJECT_PATH
python_path = f'{cls.BACKEND_PROJECT_PATH}/{package_name.replace(".", "/")}'
if 'controller.py.j2' in template:
return f'{python_path}/{business_name}_controller.py'
elif 'crud.py.j2' in template:
return f'{python_path}/{business_name}_crud.py'
elif 'model.py.j2' in template:
return f'{python_path}/{business_name}_model.py'
elif 'service.py.j2' in template:
return f'{python_path}/{business_name}_service.py'
elif 'schema.py.j2' in template:
return f'{python_path}/{business_name}_schema.py'
elif 'sql.j2' in template:
return f'{cls.BACKEND_PROJECT_PATH}/sql/{business_name}_menu.sql'
elif 'api.js.j2' in template:
return f'{vue_path}/api/{module_name}/{business_name}.js'
elif 'index.vue.j2' in template or 'index-tree.vue.j2' in template:
return f'{vue_path}/views/{module_name}/{business_name}/index.vue'
return ''
@classmethod
def get_package_prefix(cls, package_name: str):
"""
获取包前缀
:param package_name: 包名
:return: 包前缀
"""
return package_name[: package_name.rfind('.')]
@classmethod
def get_vo_import_list(cls, gen_table: GenTableSchema):
"""
获取vo模板导入包列表
:param gen_table: 生成表的配置信息
:return: 导入包列表
"""
columns = gen_table.columns or []
import_list = set()
for column in columns:
if column.python_type in GenConstant.TYPE_DATE:
import_list.add(f'from datetime import {column.python_type}')
elif column.python_type == GenConstant.TYPE_DECIMAL:
import_list.add('from decimal import Decimal')
# 修复类型检查问题,确保sub_table存在且有columns属性
if gen_table.sub and gen_table.sub_table:
sub_columns = gen_table.sub_table.columns or []
for sub_column in sub_columns:
if sub_column.python_type in GenConstant.TYPE_DATE:
import_list.add(f'from datetime import {sub_column.python_type}')
elif sub_column.python_type == GenConstant.TYPE_DECIMAL:
import_list.add('from decimal import Decimal')
return cls.merge_same_imports(list(import_list), 'from datetime import')
@classmethod
def get_do_import_list(cls, gen_table: GenTableSchema):
"""
获取do模板导入包列表
:param gen_table: 生成表的配置信息
:return: 导入包列表
"""
columns = gen_table.columns or []
import_list = set()
import_list.add('from sqlalchemy import Column')
for column in columns:
data_type = cls.get_db_type(column.column_type)
if data_type in GenConstant.COLUMNTYPE_GEOMETRY:
import_list.add('from geoalchemy2 import Geometry')
import_list.add(
f'from sqlalchemy import {StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY_TYPE_MAPPING, data_type)}'
)
# 修复类型检查问题,确保sub_table存在且有columns属性
if gen_table.sub and gen_table.sub_table:
import_list.add('from sqlalchemy import ForeignKey')
sub_columns = gen_table.sub_table.columns or []
for sub_column in sub_columns:
data_type = cls.get_db_type(sub_column.column_type)
import_list.add(
f'from sqlalchemy import {StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY_TYPE_MAPPING, data_type)}'
)
return cls.merge_same_imports(list(import_list), 'from sqlalchemy import')
@classmethod
def get_db_type(cls, column_type: str) -> str:
"""
获取数据库类型字段
param column_type: 字段类型
:return: 数据库类型
"""
if '(' in column_type:
return column_type.split('(')[0]
return column_type
@classmethod
def merge_same_imports(cls, imports: List[str], import_start: str) -> List[str]:
"""
合并相同的导入语句
:param imports: 导入语句列表
:param import_start: 导入语句的起始字符串
:return: 合并后的导入语句列表
"""
merged_imports = []
_imports = []
for import_stmt in imports:
if import_stmt.startswith(import_start):
imported_items = import_stmt.split('import')[1].strip()
_imports.extend(imported_items.split(', '))
else:
merged_imports.append(import_stmt)
if _imports:
merged_datetime_import = f'{import_start} {", ".join(_imports)}'
merged_imports.append(merged_datetime_import)
return merged_imports
@classmethod
def get_dicts(cls, gen_table: GenTableSchema):
"""
获取字典列表
:param gen_table: 生成表的配置信息
:return: 字典列表
"""
columns = gen_table.columns or []
dicts = set()
cls.add_dicts(dicts, columns)
if gen_table.sub_table is not None:
sub_columns = gen_table.sub_table.columns or []
cls.add_dicts(dicts, sub_columns)
return ', '.join(dicts)
@classmethod
def add_dicts(cls, dicts: Set[str], columns: List):
"""
添加字典列表
:param dicts: 字典列表
:param columns: 字段列表
:return: 新的字典列表
"""
for column in columns:
if (
not column.super_column
and StringUtil.is_not_empty(column.dict_type)
and StringUtil.equals_any_ignore_case(
column.html_type, [GenConstant.HTML_SELECT, GenConstant.HTML_RADIO, GenConstant.HTML_CHECKBOX]
)
):
dicts.add(f"'{column.dict_type}'")
@classmethod
def get_permission_prefix(cls, module_name: str, business_name: str):
"""
获取权限前缀
:param module_name: 模块名
:param business_name: 业务名
:return: 权限前缀
"""
return f'{module_name}:{business_name}'
@classmethod
def get_parent_menu_id(cls, params_obj: Dict):
"""
获取上级菜单ID
:param params_obj: 菜单参数字典
:return: 上级菜单ID
"""
if params_obj and params_obj.get(GenConstant.PARENT_MENU_ID):
return params_obj.get(GenConstant.PARENT_MENU_ID)
return cls.DEFAULT_PARENT_MENU_ID
@classmethod
def get_tree_code(cls, params_obj: Dict):
"""
获取树编码
:param params_obj: 菜单参数字典
:return: 树编码
"""
if GenConstant.TREE_CODE in params_obj:
return cls.to_camel_case(params_obj.get(GenConstant.TREE_CODE, 'treeCode'))
return ''
@classmethod
def get_tree_parent_code(cls, params_obj: Dict):
"""
获取树父编码
:param params_obj: 菜单参数字典
:return: 树父编码
"""
if GenConstant.TREE_PARENT_CODE in params_obj:
return cls.to_camel_case(params_obj.get(GenConstant.TREE_PARENT_CODE, 'treeParentCode'))
return ''
@classmethod
def get_tree_name(cls, params_obj: Dict):
"""
获取树名称
:param params_obj: 菜单参数字典
:return: 树名称
"""
if GenConstant.TREE_NAME in params_obj:
return cls.to_camel_case(params_obj.get(GenConstant.TREE_NAME, 'treeName'))
return ''
@classmethod
def get_expand_column(cls, gen_table: GenTableSchema):
"""
获取展开列
:param gen_table: 生成表的配置信息
:return: 展开列
"""
options = gen_table.options
if not options:
return 0
params_obj = json.loads(options)
tree_name = params_obj.get(GenConstant.TREE_NAME)
num = 0
for column in gen_table.columns or []:
if column.list:
num += 1
if column.column_name == tree_name:
break
return num
@classmethod
def to_camel_case(cls, text: str) -> str:
"""
将字符串转换为驼峰命名
:param text: 待转换的字符串
:return: 转换后的驼峰命名字符串
"""
parts = text.split('_')
return parts[0] + ''.join(word.capitalize() for word in parts[1:])
@classmethod
def get_sqlalchemy_type(cls, column_type: str):
"""
获取SQLAlchemy类型
:param column_type: 列类型
:return: SQLAlchemy类型
"""
if '(' in column_type:
column_type_list = column_type.split('(')
if column_type_list[0] in GenConstant.COLUMNTYPE_STR:
sqlalchemy_type = (
StringUtil.get_mapping_value_by_key_ignore_case(
GenConstant.DB_TO_SQLALCHEMY_TYPE_MAPPING, column_type_list[0]
)
+ '('
+ column_type_list[1]
)
else:
sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case(
GenConstant.DB_TO_SQLALCHEMY_TYPE_MAPPING, column_type_list[0]
)
else:
sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case(
GenConstant.DB_TO_SQLALCHEMY_TYPE_MAPPING, column_type
)
return sqlalchemy_type
+3 -3
View File
@@ -89,7 +89,7 @@ async def clear_monitor_cache_name_controller(
"""清除指定缓存名称下的所有缓存"""
result = await CacheService.clear_cache_monitor_cache_name_service(redis=redis, cache_name=cache_name)
if not result:
raise CustomException(message='清除缓存失败', data=result)
raise CustomException(msg='清除缓存失败', data=result)
logger.info(f'清除缓存{cache_name}成功')
return SuccessResponse(msg=f'{cache_name}对应键值清除成功', data=result)
@@ -107,7 +107,7 @@ async def clear_monitor_cache_key_controller(
"""清除指定缓存键"""
result = await CacheService.clear_cache_monitor_cache_key_service(redis=redis, cache_key=cache_key)
if not result:
raise CustomException(message='清除缓存失败', data=result)
raise CustomException(msg='清除缓存失败', data=result)
logger.info(f'清除缓存键{cache_key}成功')
return SuccessResponse(msg=f'{cache_key}清除成功', data=result)
@@ -124,6 +124,6 @@ async def clear_monitor_cache_all_controller(
"""清除所有缓存"""
result = await CacheService.clear_cache_monitor_all_service(redis=redis)
if not result:
raise CustomException(message='清除缓存失败', data=result)
raise CustomException(msg='清除缓存失败', data=result)
logger.info('清除所有缓存成功')
return SuccessResponse(msg='所有缓存清除成功', data=result)
@@ -2,10 +2,10 @@
from fastapi import APIRouter, Body, Depends, Path, Query, Request, UploadFile, Form
from fastapi.responses import JSONResponse, StreamingResponse, FileResponse
import urllib.parse
import os
from typing import List, Optional
# from oss2 import auth # 预留阿里云OSS,后期使用
from app.common.response import StreamResponse, SuccessResponse, ErrorResponse
from app.common.request import PaginationService
from app.utils.common_util import bytes2file_response
@@ -13,7 +13,6 @@ from app.core.base_params import PaginationQueryParam
from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute
from app.core.logger import logger
from ...module_system.auth.schema import AuthSchema
from .param import ResourceSearchQueryParam
from .schema import (
ResourceMoveSchema,
@@ -25,18 +24,21 @@ from .service import ResourceService
ResourceRouter = APIRouter(route_class=OperationLogRoute, prefix="/resource", tags=["资源管理"])
@ResourceRouter.get("/list", summary="获取目录列表", description="获取指定目录下的文件和子目录列表")
@ResourceRouter.get(
"/list",
summary="获取目录列表",
description="获取指定目录下的文件和子目录列表",
dependencies=[Depends(AuthPermission(permissions=["monitor:resource:query"]))]
)
async def get_directory_list_controller(
request: Request,
page: PaginationQueryParam = Depends(),
search: ResourceSearchQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:query"]))
) -> JSONResponse:
"""获取目录列表"""
# 获取资源列表(与案例模块保持一致的分页实现)
result_dict_list = await ResourceService.get_resources_list_service(
search=search,
order_by=page.order_by,
base_url=str(request.base_url)
)
# 使用分页服务进行分页处理(与案例模块保持一致)
@@ -50,16 +52,18 @@ async def get_directory_list_controller(
return SuccessResponse(data=result_dict, msg="获取目录列表成功")
@ResourceRouter.post("/upload", summary="上传文件", description="上传文件到指定目录")
@ResourceRouter.post(
"/upload",
summary="上传文件",
description="上传文件到指定目录",
dependencies=[Depends(AuthPermission(permissions=["monitor:resource:upload"]))])
async def upload_file_controller(
file: UploadFile,
request: Request,
target_path: Optional[str] = Form(None, description="目标目录路径"),
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:upload"]))
target_path: Optional[str] = Form(None, description="目标目录路径")
) -> JSONResponse:
"""上传文件"""
result_dict = await ResourceService.upload_file_service(
auth=auth,
file=file,
target_path=target_path,
base_url=str(request.base_url)
@@ -68,15 +72,18 @@ async def upload_file_controller(
return SuccessResponse(data=result_dict, msg="上传文件成功")
@ResourceRouter.get("/download", summary="下载文件", description="下载指定文件")
@ResourceRouter.get(
"/download",
summary="下载文件",
description="下载指定文件",
dependencies=[Depends(AuthPermission(permissions=["monitor:resource:download"]))]
)
async def download_file_controller(
request: Request,
path: str = Query(..., description="文件路径"),
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:download"]))
path: str = Query(..., description="文件路径")
) -> FileResponse:
"""下载文件"""
file_path = await ResourceService.download_file_service(
auth=auth,
file_path=path,
base_url=str(request.base_url)
)
@@ -93,70 +100,94 @@ async def download_file_controller(
)
@ResourceRouter.delete("/delete", summary="删除文件", description="删除指定文件或目录")
@ResourceRouter.delete(
"/delete",
summary="删除文件",
description="删除指定文件或目录",
dependencies=[Depends(AuthPermission(permissions=["monitor:resource:delete"]))]
)
async def delete_files_controller(
paths: List[str] = Body(..., description="文件路径列表"),
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:delete"]))
paths: List[str] = Body(..., description="文件路径列表")
) -> JSONResponse:
"""删除文件"""
await ResourceService.delete_file_service(auth=auth, paths=paths)
await ResourceService.delete_file_service(paths=paths)
logger.info(f"删除文件成功: {paths}")
return SuccessResponse(msg="删除文件成功")
@ResourceRouter.post("/move", summary="移动文件", description="移动文件或目录")
@ResourceRouter.post(
"/move",
summary="移动文件",
description="移动文件或目录",
dependencies=[Depends(AuthPermission(permissions=["monitor:resource:move"]))]
)
async def move_file_controller(
data: ResourceMoveSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:move"]))
data: ResourceMoveSchema
) -> JSONResponse:
"""移动文件"""
await ResourceService.move_file_service(auth=auth, data=data)
await ResourceService.move_file_service(data=data)
logger.info(f"移动文件成功: {data.source_path} -> {data.target_path}")
return SuccessResponse(msg="移动文件成功")
@ResourceRouter.post("/copy", summary="复制文件", description="复制文件或目录")
@ResourceRouter.post(
"/copy",
summary="复制文件",
description="复制文件或目录",
dependencies=[Depends(AuthPermission(permissions=["monitor:resource:copy"]))]
)
async def copy_file_controller(
data: ResourceCopySchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:copy"]))
data: ResourceCopySchema
) -> JSONResponse:
"""复制文件"""
await ResourceService.copy_file_service(auth=auth, data=data)
await ResourceService.copy_file_service(data=data)
logger.info(f"复制文件成功: {data.source_path} -> {data.target_path}")
return SuccessResponse(msg="复制文件成功")
@ResourceRouter.post("/rename", summary="重命名文件", description="重命名文件或目录")
@ResourceRouter.post(
"/rename",
summary="重命名文件",
description="重命名文件或目录",
dependencies=[Depends(AuthPermission(permissions=["monitor:resource:rename"]))]
)
async def rename_file_controller(
data: ResourceRenameSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:rename"]))
data: ResourceRenameSchema
) -> JSONResponse:
"""重命名文件"""
await ResourceService.rename_file_service(auth=auth, data=data)
await ResourceService.rename_file_service(data=data)
logger.info(f"重命名文件成功: {data.old_path} -> {data.new_name}")
return SuccessResponse(msg="重命名文件成功")
@ResourceRouter.post("/create-dir", summary="创建目录", description="在指定路径创建新目录")
@ResourceRouter.post(
"/create-dir",
summary="创建目录",
description="在指定路径创建新目录",
dependencies=[Depends(AuthPermission(permissions=["monitor:resource:create_dir"]))]
)
async def create_directory_controller(
data: ResourceCreateDirSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:create_dir"]))
data: ResourceCreateDirSchema
) -> JSONResponse:
"""创建目录"""
await ResourceService.create_directory_service(auth=auth, data=data)
await ResourceService.create_directory_service(data=data)
logger.info(f"创建目录成功: {data.parent_path}/{data.dir_name}")
return SuccessResponse(msg="创建目录成功")
@ResourceRouter.post("/export", summary="导出资源列表", description="导出资源列表")
@ResourceRouter.post(
"/export",
summary="导出资源列表",
description="导出资源列表",
dependencies=[Depends(AuthPermission(permissions=["monitor:resource:export"]))]
)
async def export_resource_list_controller(
request: Request,
search: ResourceSearchQueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:resource:export"]))
search: ResourceSearchQueryParam = Depends()
) -> StreamingResponse:
"""导出资源列表"""
# 获取搜索结果
result_dict_list = await ResourceService.search_resources_service(
result_dict_list = await ResourceService.get_resources_list_service(
search=search,
base_url=str(request.base_url)
)
@@ -11,7 +11,6 @@ from app.core.exceptions import CustomException
from app.core.logger import logger
from app.utils.excel_util import ExcelUtil
from app.config.setting import settings
from ...module_system.auth.schema import AuthSchema
from .param import ResourceSearchQueryParam
from .schema import (
ResourceItemSchema,
@@ -160,12 +159,7 @@ class ResourceService:
return {}
@classmethod
async def get_directory_list_service(
cls,
path: Optional[str] = None,
include_hidden: bool = False,
base_url: Optional[str] = None
) -> Dict:
async def get_directory_list_service(cls, path: Optional[str] = None, include_hidden: bool = False, base_url: Optional[str] = None) -> Dict:
"""获取目录列表"""
try:
# 如果没有指定路径,使用静态文件根目录
@@ -224,12 +218,7 @@ class ResourceService:
raise CustomException(msg=f'获取目录列表失败: {str(e)}')
@classmethod
async def search_resources_service(
cls,
search: Optional[ResourceSearchQueryParam] = None,
order_by: Optional[str] = None,
base_url: Optional[str] = None
) -> List[Dict]:
async def get_resources_list_service(cls, search: Optional[ResourceSearchQueryParam] = None, order_by: Optional[str] = None, base_url: Optional[str] = None) -> List[Dict]:
"""搜索资源列表(用于分页和导出)"""
try:
# 确定搜索路径
@@ -278,22 +267,10 @@ class ResourceService:
return sorted_resources
except CustomException:
raise
except Exception as e:
logger.error(f'搜索资源失败: {str(e)}')
raise CustomException(msg=f'搜索资源失败: {str(e)}')
@classmethod
async def get_resources_list_service(
cls,
search: Optional[ResourceSearchQueryParam] = None,
order_by: Optional[str] = None,
base_url: Optional[str] = None
) -> List[Dict]:
"""获取资源列表(用于分页查询)"""
return await cls.search_resources_service(search=search, order_by=order_by, base_url=base_url)
@classmethod
async def export_resource_service(cls, data_list: List[Dict[str, Any]]) -> bytes:
"""导出资源列表"""
@@ -343,7 +320,6 @@ class ResourceService:
return stats
@classmethod
def _sort_results(cls, results: List[Dict], order_by: Optional[str] = None) -> List[Dict]:
"""排序搜索结果"""
@@ -386,13 +362,7 @@ class ResourceService:
return results
@classmethod
async def upload_file_service(
cls,
auth: AuthSchema,
file: UploadFile,
target_path: Optional[str] = None,
base_url: Optional[str] = None
) -> Dict:
async def upload_file_service(cls, file: UploadFile, target_path: Optional[str] = None, base_url: Optional[str] = None) -> Dict:
"""上传文件到指定目录"""
if not file or not file.filename:
raise CustomException(msg="请选择要上传的文件")
@@ -445,7 +415,6 @@ class ResourceService:
return ResourceUploadSchema(
filename=filename,
file_path=file_url, # 返回HTTP URL而不是文件系统路径
file_url=file_url,
file_size=file_info.get('size', 0),
upload_time=datetime.now()
@@ -456,7 +425,7 @@ class ResourceService:
raise CustomException(msg=f"文件上传失败: {str(e)}")
@classmethod
async def download_file_service(cls, auth: AuthSchema, file_path: str, base_url: Optional[str] = None) -> str:
async def download_file_service(cls, file_path: str, base_url: Optional[str] = None) -> str:
"""下载文件(返回文件路径)"""
try:
safe_path = cls._get_safe_path(file_path)
@@ -479,7 +448,7 @@ class ResourceService:
raise CustomException(msg=f"下载文件失败: {str(e)}")
@classmethod
async def delete_file_service(cls, auth: AuthSchema, paths: List[str]) -> None:
async def delete_file_service(cls, paths: List[str]) -> None:
"""删除文件或目录"""
if not paths:
raise CustomException(msg='删除失败,删除路径不能为空')
@@ -504,7 +473,7 @@ class ResourceService:
raise CustomException(msg=f"删除失败 {path}: {str(e)}")
@classmethod
async def batch_delete_service(cls, auth: AuthSchema, paths: List[str]) -> Dict[str, List[str]]:
async def batch_delete_service(cls, paths: List[str]) -> Dict[str, List[str]]:
"""批量删除文件或目录"""
if not paths:
raise CustomException(msg='删除失败,删除路径不能为空')
@@ -539,7 +508,7 @@ class ResourceService:
}
@classmethod
async def move_file_service(cls, auth: AuthSchema, data: ResourceMoveSchema) -> None:
async def move_file_service(cls, data: ResourceMoveSchema) -> None:
"""移动文件或目录"""
try:
source_path = cls._get_safe_path(data.source_path)
@@ -574,7 +543,7 @@ class ResourceService:
raise CustomException(msg=f"移动失败: {str(e)}")
@classmethod
async def copy_file_service(cls, auth: AuthSchema, data: ResourceCopySchema) -> None:
async def copy_file_service(cls, data: ResourceCopySchema) -> None:
"""复制文件或目录"""
try:
source_path = cls._get_safe_path(data.source_path)
@@ -606,7 +575,7 @@ class ResourceService:
raise CustomException(msg=f"复制失败: {str(e)}")
@classmethod
async def rename_file_service(cls, auth: AuthSchema, data: ResourceRenameSchema) -> None:
async def rename_file_service(cls, data: ResourceRenameSchema) -> None:
"""重命名文件或目录"""
try:
old_path = cls._get_safe_path(data.old_path)
@@ -632,7 +601,7 @@ class ResourceService:
raise CustomException(msg=f"重命名失败: {str(e)}")
@classmethod
async def create_directory_service(cls, auth: AuthSchema, data: ResourceCreateDirSchema) -> None:
async def create_directory_service(cls, data: ResourceCreateDirSchema) -> None:
"""创建目录"""
try:
parent_path = cls._get_safe_path(data.parent_path)
@@ -4,7 +4,6 @@ from typing import Optional, Union
from datetime import datetime
from pydantic import ConfigDict, Field, BaseModel, model_validator
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
from ..user.schema import UserOutSchema
+1 -14
View File
@@ -26,11 +26,6 @@ class DeptCRUD(CRUDBase[DeptModel, DeptCreateSchema, DeptUpdateSchema]):
obj = await self.get(id=id)
if not obj:
return None
if obj.parent_id:
parent = await self.get(id=obj.parent_id)
if parent:
obj.parent_name = parent.name
return obj
async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[DeptModel]:
@@ -41,15 +36,7 @@ class DeptCRUD(CRUDBase[DeptModel, DeptCreateSchema, DeptUpdateSchema]):
:param order_by: 排序字段
:return: 部门列表
"""
obj_list = await self.list(search=search, order_by=order_by)
parent_ids = [obj.parent_id for obj in obj_list if obj.parent_id]
if parent_ids:
parents = await self.list(search={"id": ("in", parent_ids)})
parent_map = {p.id: p.name for p in parents}
for obj in obj_list:
if obj.parent_id:
obj.parent_name = parent_map.get(obj.parent_id)
return obj_list
return await self.list(search=search, order_by=order_by)
async def get_tree_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[DeptModel]:
"""
@@ -36,6 +36,10 @@ class DeptService:
:return: 部门详情对象
"""
dept = await DeptCRUD(auth).get_by_id_crud(id=id)
if dept and dept.parent_id:
parent = await DeptCRUD(auth).get(id=dept.parent_id)
if parent:
DeptOutSchema.parent_name = parent.name
return DeptOutSchema.model_validate(dept).model_dump()
@classmethod
@@ -3,7 +3,6 @@
import json
from typing import Any, List, Dict, Optional
from redis.asyncio.client import Redis
from sqlalchemy.ext.asyncio import AsyncSession
from app.common.enums import RedisInitKeyConfig
from app.utils.excel_util import ExcelUtil
+2 -2
View File
@@ -8,7 +8,7 @@ from .model import OperationLogModel
from .schema import OperationLogCreateSchema
class OperationLogCRUD(CRUDBase[OperationLogModel, OperationLogCreateSchema, None]):
class OperationLogCRUD(CRUDBase[OperationLogModel, OperationLogCreateSchema, OperationLogCreateSchema]):
"""操作日志数据层"""
def __init__(self, auth: AuthSchema) -> None:
@@ -23,7 +23,7 @@ class OperationLogCRUD(CRUDBase[OperationLogModel, OperationLogCreateSchema, Non
:param data: 操作日志创建模型
:return: 操作日志记录
"""
return await self.create(data=data.model_dump())
return await self.create(data=data)
async def get_by_id_crud(self, id: int) -> Optional[OperationLogModel]:
"""
+1 -14
View File
@@ -26,11 +26,6 @@ class MenuCRUD(CRUDBase[MenuModel, MenuCreateSchema, MenuUpdateSchema]):
obj = await self.get(id=id)
if not obj:
return None
if obj.parent_id:
parent = await self.get(id=obj.parent_id)
if parent:
obj.parent_name = parent.name
return obj
async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[MenuModel]:
@@ -41,15 +36,7 @@ class MenuCRUD(CRUDBase[MenuModel, MenuCreateSchema, MenuUpdateSchema]):
:param order_by: 排序字段
:return: 菜单列表
"""
obj_list = await self.list(search=search, order_by=order_by)
parent_ids = [obj.parent_id for obj in obj_list if obj.parent_id]
if parent_ids:
parents = await self.list(search={"id": ("in", parent_ids)})
parent_map = {p.id: p.name for p in parents}
for obj in obj_list:
if obj.parent_id:
obj.parent_name = parent_map.get(obj.parent_id)
return obj_list
return await self.list(search=search, order_by=order_by)
async def get_tree_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[MenuModel]:
"""
@@ -29,6 +29,11 @@ class MenuService:
@classmethod
async def get_menu_detail_service(cls, auth: AuthSchema, id: int) -> Dict:
menu = await MenuCRUD(auth).get_by_id_crud(id=id)
if menu and menu.parent_id:
parent = await MenuCRUD(auth).get_by_id_crud(id=menu.parent_id)
if parent:
MenuOutSchema.parent_name = parent.name
menu_dict = MenuOutSchema.model_validate(menu).model_dump()
return menu_dict
@@ -124,14 +124,19 @@ class ParamsService:
raise CustomException(msg=f'{exist_obj.config_name} 删除失败,系统初始化配置不可以删除')
await ParamsCRUD(auth).delete_obj_crud(ids=ids)
# 同步删除Redis缓存
redis_key = f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:{exist_obj.config_key}"
try:
await RedisCURD(redis).delete(redis_key)
logger.info(f"删除系统配置成功: {id}")
except Exception as e:
logger.error(f"删除系统配置失败: {e}")
raise CustomException(msg="删除字典类型失败")
for id in ids:
exist_obj = await ParamsCRUD(auth).get_obj_by_id_crud(id=id)
if not exist_obj:
continue
redis_key = f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:{exist_obj.config_key}"
try:
await RedisCURD(redis).delete(redis_key)
logger.info(f"删除系统配置成功: {id}")
except Exception as e:
logger.error(f"删除系统配置失败: {e}")
raise CustomException(msg="删除字典类型失败")
@classmethod
async def export_obj_service(cls, data_list: List[Dict[str, Any]]) -> bytes:
+12 -10
View File
@@ -29,11 +29,12 @@ class RoleCRUD(CRUDBase[RoleModel, RoleCreateSchema, RoleUpdateSchema]):
"""设置角色的菜单权限"""
roles = await self.list(search={"id": ("in", role_ids)})
menus = await MenuCRUD(self.auth).get_list_crud(search={"id": ("in", menu_ids)})
await self.update_relationships(
objs_to_update=roles,
relationship_field="menus",
related_objs=menus
)
for obj in roles:
relationship = obj.menus
relationship.clear()
relationship.extend(menus)
await self.db.flush()
async def set_role_data_scope_crud(self, role_ids: List[int], data_scope: int) -> None:
"""设置角色的数据范围"""
@@ -43,11 +44,12 @@ class RoleCRUD(CRUDBase[RoleModel, RoleCreateSchema, RoleUpdateSchema]):
"""设置角色的部门权限"""
roles = await self.list(search={"id": ("in", role_ids)})
depts = await DeptCRUD(self.auth).get_list_crud(search={"id": ("in", dept_ids)})
await self.update_relationships(
objs_to_update=roles,
relationship_field="depts",
related_objs=depts
)
for obj in roles:
relationship = obj.depts
relationship.clear()
relationship.extend(depts)
await self.db.flush()
async def set_available_crud(self, ids: List[int], status: bool) -> None:
"""设置角色的可用状态"""
@@ -113,7 +113,7 @@ class RoleService:
data = role_list.copy()
for item in data:
item['status'] = '正常' if item.get('status') else '停用'
item['data_scope'] = data_scope_map.get(item.get('data_scope'))
item['data_scope'] = data_scope_map.get(item.get('data_scope', 1), '')
item['creator'] = item.get('creator', {}).get('name', '未知') if isinstance(item.get('creator'), dict) else '未知'
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
+19 -14
View File
@@ -79,7 +79,7 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
Returns:
Optional[UserModel]: 更新后的用户信息
"""
return await self.update(id=id, data={"last_login": datetime.now()})
return await self.update(id=id, data=UserUpdateSchema(last_login=datetime.now()))
async def set_available_crud(self, ids: List[int], status: bool) -> None:
"""
@@ -104,11 +104,13 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
role_objs = await RoleCRUD(self.auth).get_list_crud(search={"id": ("in", role_ids)})
else:
role_objs = []
await self.update_relationships(
objs_to_update=user_objs,
relationship_field="roles",
related_objs=role_objs
)
for obj in user_objs:
relationship = obj.roles
relationship.clear()
relationship.extend(role_objs)
await self.db.flush()
async def set_user_positions_crud(self, user_ids: List[int], position_ids: List[int]) -> None:
"""
@@ -123,11 +125,12 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
position_objs = await PositionCRUD(self.auth).get_list_crud(search={"id": ("in", position_ids)})
else:
position_objs = []
await self.update_relationships(
objs_to_update=user_objs,
relationship_field="positions",
related_objs=position_objs
)
for obj in user_objs:
relationship = obj.positions
relationship.clear()
relationship.extend(position_objs)
await self.db.flush()
async def change_password_crud(self, id: int, password_hash: str) -> Optional[UserModel]:
"""
@@ -140,7 +143,9 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
Returns:
Optional[UserModel]: 更新后的用户信息
"""
return await self.update(id=id, data={"password": password_hash})
return await self.update(id=id, data=UserUpdateSchema(password=password_hash))
async def forget_password_crud(self, id: int, password_hash: str) -> Optional[UserModel]:
"""
@@ -153,7 +158,7 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
Returns:
Optional[UserModel]: 更新后的用户信息
"""
return await self.update(id=id, data={"password": password_hash})
return await self.update(id=id, data=UserUpdateSchema(password=password_hash))
async def register_user_crud(self, data: UserForgetPasswordSchema) -> Optional[UserModel]:
"""
@@ -168,4 +173,4 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
if await self.get_by_username_crud(username=data.username):
return None
return await self.create(data=data)
return await self.create(data=UserCreateSchema(**data.model_dump()))
@@ -9,7 +9,7 @@ from app.api.v1.module_system.role.schema import RoleOutSchema
class CurrentUserUpdateSchema(BaseModel):
"""基础用户信息"""
name: str = Field(..., max_length=32, description="名称")
name: Optional[str] = Field(default=None, max_length=32, description="名称")
mobile: Optional[str] = Field(default=None, description="手机号")
email: Optional[EmailStr] = Field(default=None, description="邮箱")
gender: Optional[str] = Field(default=None, description="性别")
@@ -66,7 +66,7 @@ class UserCreateSchema(CurrentUserUpdateSchema):
model_config = ConfigDict(from_attributes=True)
username: str = Field(default=..., max_length=32, description="用户名")
password: str | None = Field(default=None, max_length=128, description="密码哈希值")
password: Optional[str] = Field(default=None, max_length=128, description="密码哈希值")
status: bool = Field(default=True, description="是否可用")
is_superuser: bool = Field(default=False, description="是否超管")
description: Optional[str] = Field(default=None, max_length=255, description="备注")
@@ -80,15 +80,12 @@ class UserUpdateSchema(UserCreateSchema):
"""更新"""
model_config = ConfigDict(from_attributes=True)
password: str | None = Field(default=None, max_length=128, description="密码哈希值")
last_login: Optional[DateTimeStr] = Field(default=None, description="最后登录时间")
class UserOutSchema(UserCreateSchema, BaseSchema):
"""响应"""
model_config = ConfigDict(arbitrary_types_allowed=True, from_attributes=True)
password: str | None = Field(default=None, max_length=128, description="密码哈希值", exclude=True) # password 不返回
last_login: Optional[DateTimeStr] = Field(default=None, description="最后登录时间")
dept_name: Optional[str] = Field(default=None, description='部门名称')
dept: Optional[CommonSchema] = Field(default=None, description='部门')
roles: Optional[List[RoleOutSchema]] = Field(default=[], description='角色')
@@ -45,9 +45,10 @@ class UserService:
# 如果用户绑定了部门,则获取部门名称
if user.dept_id:
dept = await DeptCRUD(auth).get_by_id_crud(id=user.dept_id)
user.dept_name = dept.name if dept else None
UserOutSchema.dept_name = dept.name if dept else None
else:
user.dept_name = None
UserOutSchema.dept_name = None
return UserOutSchema.model_validate(user).model_dump()
@@ -58,9 +59,9 @@ class UserService:
for user in user_list:
if user.dept_id:
dept = await DeptCRUD(auth).get_by_id_crud(id=user.dept_id)
user.dept_name = dept.name if dept else None
UserOutSchema.dept_name = dept.name if dept else None
else:
user.dept_name = None
UserOutSchema.dept_name = None
user_dict = UserOutSchema.model_validate(user).model_dump()
user_dict_list.append(user_dict)
@@ -80,9 +81,11 @@ class UserService:
raise CustomException(msg='部门不存在')
# 创建用户
data.password = PwdUtil.set_password_hash(password=data.password)
user_dict = data.model_dump(exclude_unset=True, exclude={"role_ids", "position_ids"})
new_user = await UserCRUD(auth).create(data=user_dict)
if data.password:
data.password = PwdUtil.set_password_hash(password=data.password)
# user_dict = data.model_dump(exclude_unset=True, exclude={"role_ids", "position_ids"})
# new_user = await UserCRUD(auth).create(data=user_dict)
new_user = await UserCRUD(auth).create(data=data)
# 设置角色和岗位
if data.role_ids and len(data.role_ids) > 0:
@@ -118,8 +121,9 @@ class UserService:
data.password = PwdUtil.set_password_hash(password=data.password)
# 更新用户
user_dict = data.model_dump(exclude_unset=True, exclude={"role_ids", "position_ids"})
new_user = await UserCRUD(auth).update(id=id, data=user_dict)
# user_dict = data.model_dump(exclude_unset=True, exclude={"role_ids", "position_ids"})
# new_user = await UserCRUD(auth).update(id=id, data=user_dict)
new_user = await UserCRUD(auth).update(id=id, data=data)
# 更新角色和岗位
if data.role_ids and len(data.role_ids) > 0:
@@ -206,10 +210,13 @@ class UserService:
@classmethod
async def update_current_user_info_service(cls, auth: AuthSchema, data: CurrentUserUpdateSchema) -> Dict:
"""更新当前用户信息"""
if not auth.user:
raise CustomException(msg="用户不存在")
user = await UserCRUD(auth).get_by_id_crud(id=auth.user.id)
if not user:
raise CustomException(msg="用户不存在")
new_user = await UserCRUD(auth).update(id=auth.user.id, data=data)
user_update_data = UserUpdateSchema(**data.model_dump())
new_user = await UserCRUD(auth).update(id=auth.user.id, data=user_update_data)
return UserOutSchema.model_validate(new_user).model_dump()
@classmethod
@@ -240,6 +247,8 @@ class UserService:
@classmethod
async def change_user_password_service(cls, auth: AuthSchema, data: UserChangePasswordSchema) -> Dict:
"""修改用户密码"""
if not auth.user:
raise CustomException(msg="用户不存在")
if not data.old_password or not data.new_password:
raise CustomException(msg='密码不能为空')
@@ -282,12 +291,12 @@ class UserService:
data.password = PwdUtil.set_password_hash(password=data.password)
data.name = data.username
data.creator_id = 1
dict_data = data.model_dump(exclude_unset=True)
# dict_data['creator_id'] = data.creator_id
# dict_data['dept_id'] = data.dept_id
# dict_data['description'] = data.description
result = await UserCRUD(auth).create(data=dict_data)
await UserCRUD(auth).set_user_roles_crud(user_ids=[result.id], role_ids=data.role_ids)
# dict_data = data.model_dump(exclude_unset=True)
# result = await UserCRUD(auth).create(data=dict_data)
user_create_data = UserCreateSchema(**data.model_dump())
result = await UserCRUD(auth).create(data=user_create_data)
if data.role_ids:
await UserCRUD(auth).set_user_roles_crud(user_ids=[result.id], role_ids=data.role_ids)
# await UserCRUD(auth).set_user_positions_crud(user_ids=[result.id], position_ids=data.position_ids)
return UserOutSchema.model_validate(result).model_dump()
@@ -368,8 +377,8 @@ class UserService:
user_data = {
"username": str(row['username']).strip(),
"name": str(row['name']).strip(),
"email": str(row['email']).strip() if not pd.isna(row['email']) else None,
"mobile": str(row['mobile']).strip() if not pd.isna(row['mobile']) else None,
"email": str(row['email']).strip(),
"mobile": str(row['mobile']).strip(),
"gender": gender,
"status": status,
"dept_id": dept_id,
@@ -380,12 +389,14 @@ class UserService:
exists_user = await UserCRUD(auth).get_by_username_crud(username=user_data["username"])
if exists_user:
if update_support:
await UserCRUD(auth).update(id=exists_user.id, data=user_data)
user_update_data = UserUpdateSchema(**user_data)
await UserCRUD(auth).update(id=exists_user.id, data=user_update_data)
success_count += 1
else:
error_msgs.append(f"{index+1}行: 用户 {user_data['username']} 已存在")
else:
await UserCRUD(auth).create(data=user_data)
user_create_data = UserCreateSchema(**user_data)
await UserCRUD(auth).create(data=user_create_data)
success_count += 1
except Exception as e: