mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-20 20:39:55 +00:00
refactor: 重构服务层方法命名规范
fix: 修复文件下载和删除功能 fix: 修复Redis哈希获取方法返回值类型 fix: 修复权限检查逻辑 perf: 优化部门和服务详情查询性能 perf: 优化角色数据范围显示 style: 清理无用导入和注释 style: 统一CRUD方法命名 docs: 更新main.py中的命令说明 chore: 移动IP定位工具类位置 chore: 更新.gitignore忽略迁移版本文件
This commit is contained in:
@@ -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 服务器成功")
|
||||
|
||||
|
||||
@@ -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'))
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]:
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user