mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-08 02:36:07 +00:00
+3
-3
@@ -386,9 +386,9 @@ Thanks to the contributions and support of the following open-source projects:
|
|||||||
|
|
||||||
## 🎨 Community
|
## 🎨 Community
|
||||||
|
|
||||||
| WeChat QR Code | Group QR Code | WeChat Pay QR Code |
|
| Group QR Code | WeChat Pay QR Code |
|
||||||
| --- | --- | --- |
|
| --- | --- |
|
||||||
|  |  |  |
|
|  |  |
|
||||||
|
|
||||||
## ❤️ Support the Project
|
## ❤️ Support the Project
|
||||||
|
|
||||||
|
|||||||
@@ -386,9 +386,9 @@ docker compose down
|
|||||||
|
|
||||||
## 🎨 社区交流
|
## 🎨 社区交流
|
||||||
|
|
||||||
| 微信二维码 | 群组二维码 | 微信支付二维码 |
|
| 群组二维码 | 微信支付二维码 |
|
||||||
| --- | --- | --- |
|
| --- | --- |
|
||||||
|  |  |  |
|
|  |  |
|
||||||
|
|
||||||
## ❤️ 支持项目
|
## ❤️ 支持项目
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Callable
|
||||||
|
from langchain.agents import AgentState, create_agent
|
||||||
|
from langchain.agents.middleware import ModelRequest, ModelResponse, after_model, before_model, dynamic_prompt, wrap_model_call
|
||||||
|
from langchain.messages import AIMessage, HumanMessage, RemoveMessage, SystemMessage
|
||||||
|
from langchain.tools import tool, ToolRuntime
|
||||||
|
from langchain.chat_models import init_chat_model
|
||||||
|
from langgraph.checkpoint.memory import InMemorySaver
|
||||||
|
from langchain.agents.structured_output import MultipleStructuredOutputsError, StructuredOutputValidationError, ToolStrategy
|
||||||
|
from langgraph.graph.message import REMOVE_ALL_MESSAGES
|
||||||
|
from langgraph.runtime import Runtime
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
# =================定义提示词=================
|
||||||
|
SYSTEM_PROMPT = """You are an expert weather forecaster, who speaks in puns.
|
||||||
|
|
||||||
|
You have access to two tools:
|
||||||
|
|
||||||
|
- get_weather_for_location: use this to get the weather for a specific location
|
||||||
|
- get_user_location: use this to get the user's location
|
||||||
|
|
||||||
|
If a user asks you for the weather, make sure you know the location. If you can tell from the question that they mean wherever they are, use the get_user_location tool to find their location.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# =================定义工具=================
|
||||||
|
@tool
|
||||||
|
def get_weather_for_location(city: str) -> str:
|
||||||
|
"""Get weather for a given city."""
|
||||||
|
return f"It's always sunny in {city}!"
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Context:
|
||||||
|
"""Custom runtime context schema."""
|
||||||
|
user_id: str
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def get_user_location(runtime: ToolRuntime[Context]) -> str:
|
||||||
|
"""Retrieve user information based on user ID."""
|
||||||
|
user_id = runtime.context.user_id
|
||||||
|
return "Florida" if user_id == "1" else "SF"
|
||||||
|
|
||||||
|
|
||||||
|
# =================定义模型=================
|
||||||
|
model = init_chat_model(
|
||||||
|
"claude-sonnet-4-5-20250929",
|
||||||
|
temperature=0.5,
|
||||||
|
timeout=10,
|
||||||
|
max_tokens=1000
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =================定义响应模型=================
|
||||||
|
class ResponseFormat(BaseModel):
|
||||||
|
"""Response schema for the agent."""
|
||||||
|
rating: int | None = Field(description="Rating from 1-5", ge=1, le=5)
|
||||||
|
comment: str = Field(description="Review comment")
|
||||||
|
punny_response: str
|
||||||
|
weather_conditions: str | None = None
|
||||||
|
|
||||||
|
# =================定义存储记忆=================
|
||||||
|
checkpointer = InMemorySaver()
|
||||||
|
|
||||||
|
# =================定义动态提示词=================
|
||||||
|
@dynamic_prompt
|
||||||
|
def dynamic_system_prompt(request: ModelRequest) -> str:
|
||||||
|
user_name = getattr(request.runtime.context, "user_name", "User")
|
||||||
|
system_prompt = f"You are a helpful assistant. Address the user as {user_name}."
|
||||||
|
return system_prompt
|
||||||
|
|
||||||
|
@before_model
|
||||||
|
def trim_messages(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
|
||||||
|
"""Keep only the last few messages to fit context window."""
|
||||||
|
messages = state["messages"]
|
||||||
|
|
||||||
|
if len(messages) <= 3:
|
||||||
|
return None # No changes needed
|
||||||
|
|
||||||
|
first_msg = messages[0]
|
||||||
|
recent_messages = messages[-3:] if len(messages) % 2 == 0 else messages[-4:]
|
||||||
|
new_messages = [first_msg] + recent_messages
|
||||||
|
|
||||||
|
return {
|
||||||
|
"messages": [
|
||||||
|
RemoveMessage(id=REMOVE_ALL_MESSAGES),
|
||||||
|
*new_messages
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@after_model
|
||||||
|
def validate_response(state: AgentState, runtime: Runtime) -> dict | None:
|
||||||
|
"""Remove messages containing sensitive words."""
|
||||||
|
STOP_WORDS = ["password", "secret"]
|
||||||
|
last_message = state["messages"][-1]
|
||||||
|
if any(word in last_message.content for word in STOP_WORDS):
|
||||||
|
return {"messages": [RemoveMessage(id=last_message.id or "")]}
|
||||||
|
return None
|
||||||
|
|
||||||
|
@wrap_model_call
|
||||||
|
def inject_file_context(
|
||||||
|
request: ModelRequest,
|
||||||
|
handler: Callable[[ModelRequest], ModelResponse]
|
||||||
|
) -> ModelResponse:
|
||||||
|
"""Inject context about files user has uploaded this session."""
|
||||||
|
# Read from State: get uploaded files metadata
|
||||||
|
uploaded_files = request.state.get("uploaded_files", [])
|
||||||
|
|
||||||
|
if uploaded_files:
|
||||||
|
# Build context about available files
|
||||||
|
file_descriptions = []
|
||||||
|
for file in uploaded_files:
|
||||||
|
file_descriptions.append(
|
||||||
|
f"- {file['name']} ({file['type']}): {file['summary']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
file_context = f"""Files you have access to in this conversation:
|
||||||
|
{chr(10).join(file_descriptions)}
|
||||||
|
|
||||||
|
Reference these files when answering questions."""
|
||||||
|
|
||||||
|
# Inject file context before recent messages
|
||||||
|
messages = [
|
||||||
|
*request.messages,
|
||||||
|
{"role": "user", "content": file_context},
|
||||||
|
]
|
||||||
|
request = request.override(messages=messages)
|
||||||
|
|
||||||
|
|
||||||
|
def custom_error_handler(error: Exception) -> str:
|
||||||
|
if isinstance(error, StructuredOutputValidationError):
|
||||||
|
return "There was an issue with the format. Try again."
|
||||||
|
elif isinstance(error, MultipleStructuredOutputsError):
|
||||||
|
return "Multiple structured outputs were returned. Pick the most relevant one."
|
||||||
|
else:
|
||||||
|
return f"Error: {str(error)}"
|
||||||
|
|
||||||
|
# =================定义智能体=================
|
||||||
|
agent = create_agent(
|
||||||
|
model=model,
|
||||||
|
system_prompt=SYSTEM_PROMPT,
|
||||||
|
tools=[get_user_location, get_weather_for_location],
|
||||||
|
middleware=[dynamic_system_prompt, trim_messages, validate_response, inject_file_context],
|
||||||
|
context_schema=Context,
|
||||||
|
response_format=ToolStrategy(schema=ResponseFormat,handle_errors=(ValueError, TypeError, custom_error_handler)),
|
||||||
|
checkpointer=checkpointer
|
||||||
|
)
|
||||||
|
|
||||||
|
# =================定义线程=================
|
||||||
|
config = {"configurable": {"thread_id": "1"}}
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
SystemMessage("You are a poetry expert"),
|
||||||
|
HumanMessage("Write a haiku about spring"),
|
||||||
|
AIMessage("Cherry blossoms bloom...")
|
||||||
|
]
|
||||||
|
|
||||||
|
# =================运行智能体=================
|
||||||
|
response = agent.invoke(
|
||||||
|
input=messages,
|
||||||
|
config=config,
|
||||||
|
context=Context(user_id="1")
|
||||||
|
)
|
||||||
|
|
||||||
|
# =================解析响应=================
|
||||||
|
print(response['structured_response'])
|
||||||
|
# ResponseFormat(
|
||||||
|
# punny_response="Florida is still having a 'sun-derful' day! The sunshine is playing 'ray-dio' hits all day long! I'd say it's the perfect weather for some 'solar-bration'! If you were hoping for rain, I'm afraid that idea is all 'washed up' - the forecast remains 'clear-ly' brilliant!",
|
||||||
|
# weather_conditions="It's always sunny in Florida!"
|
||||||
|
# )
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
""" 图像输入 """
|
||||||
|
# From URL
|
||||||
|
message = {
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": "Describe the content of this image."},
|
||||||
|
{"type": "image", "url": "https://example.com/path/to/image.jpg"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# From base64 data
|
||||||
|
message = {
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": "Describe the content of this image."},
|
||||||
|
{
|
||||||
|
"type": "image",
|
||||||
|
"base64": "AAAAIGZ0eXBtcDQyAAAAAGlzb21tcDQyAAACAGlzb2...",
|
||||||
|
"mime_type": "image/jpeg",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# From provider-managed File ID
|
||||||
|
message = {
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": "Describe the content of this image."},
|
||||||
|
{"type": "image", "file_id": "file-abc123"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
""" PDF文档输入 """
|
||||||
|
# From URL
|
||||||
|
message = {
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": "Describe the content of this document."},
|
||||||
|
{"type": "file", "url": "https://example.com/path/to/document.pdf"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# From base64 data
|
||||||
|
message = {
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": "Describe the content of this document."},
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"base64": "AAAAIGZ0eXBtcDQyAAAAAGlzb21tcDQyAAACAGlzb2...",
|
||||||
|
"mime_type": "application/pdf",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# From provider-managed File ID
|
||||||
|
message = {
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": "Describe the content of this document."},
|
||||||
|
{"type": "file", "file_id": "file-abc123"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
""" 音频输入 """
|
||||||
|
# From base64 data
|
||||||
|
message = {
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": "Describe the content of this audio."},
|
||||||
|
{
|
||||||
|
"type": "audio",
|
||||||
|
"base64": "AAAAIGZ0eXBtcDQyAAAAAGlzb21tcDQyAAACAGlzb2...",
|
||||||
|
"mime_type": "audio/wav",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# From provider-managed File ID
|
||||||
|
message = {
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": "Describe the content of this audio."},
|
||||||
|
{"type": "audio", "file_id": "file-abc123"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
""" 视频输入 """
|
||||||
|
# From base64 data
|
||||||
|
message = {
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": "Describe the content of this video."},
|
||||||
|
{
|
||||||
|
"type": "video",
|
||||||
|
"base64": "AAAAIGZ0eXBtcDQyAAAAAGlzb21tcDQyAAACAGlzb2...",
|
||||||
|
"mime_type": "video/mp4",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# From provider-managed File ID
|
||||||
|
message = {
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": "Describe the content of this video."},
|
||||||
|
{"type": "video", "file_id": "file-abc123"},
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -15,14 +15,42 @@ class AIClient:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# 使用LangChain的ChatOpenAI类
|
# 使用LangChain的ChatOpenAI类
|
||||||
self.client = ChatOpenAI(
|
self.model = ChatOpenAI(
|
||||||
api_key=settings.OPENAI_API_KEY,
|
api_key=lambda: settings.OPENAI_API_KEY,
|
||||||
base_url=settings.OPENAI_BASE_URL,
|
|
||||||
model=settings.OPENAI_MODEL,
|
model=settings.OPENAI_MODEL,
|
||||||
|
base_url=settings.OPENAI_BASE_URL,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
streaming=True
|
streaming=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def process(self, query: str) -> AsyncGenerator[str, Any]:
|
||||||
|
"""
|
||||||
|
处理查询并返回流式响应
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- query (str): 用户查询。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- AsyncGenerator[str, Any]: 流式响应内容。
|
||||||
|
"""
|
||||||
|
system_prompt = """你是一个有用的AI助手,可以帮助用户回答问题和提供帮助。请用中文回答用户的问题。"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 使用LangChain的异步流式生成
|
||||||
|
messages = [
|
||||||
|
SystemMessage(content=system_prompt),
|
||||||
|
HumanMessage(content=query)
|
||||||
|
]
|
||||||
|
|
||||||
|
# 使用LangChain的流式响应
|
||||||
|
async for chunk in self.model.astream(messages):
|
||||||
|
yield chunk.text
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 记录详细错误,返回友好提示
|
||||||
|
log.error(f"AI处理查询失败: {str(e)}")
|
||||||
|
yield self._friendly_error_message(e)
|
||||||
|
|
||||||
def _friendly_error_message(self, e: Exception) -> str:
|
def _friendly_error_message(self, e: Exception) -> str:
|
||||||
"""将 OpenAI 或网络异常转换为友好的中文提示。"""
|
"""将 OpenAI 或网络异常转换为友好的中文提示。"""
|
||||||
# 尝试获取状态码与错误体
|
# 尝试获取状态码与错误体
|
||||||
@@ -66,38 +94,3 @@ class AIClient:
|
|||||||
|
|
||||||
# 默认兜底
|
# 默认兜底
|
||||||
return f"处理您的请求时出现错误:{msg}"
|
return f"处理您的请求时出现错误:{msg}"
|
||||||
|
|
||||||
async def process(self, query: str) -> AsyncGenerator[str, Any]:
|
|
||||||
"""
|
|
||||||
处理查询并返回流式响应
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- query (str): 用户查询。
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- AsyncGenerator[str, Any]: 流式响应内容。
|
|
||||||
"""
|
|
||||||
system_prompt = """你是一个有用的AI助手,可以帮助用户回答问题和提供帮助。请用中文回答用户的问题。"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 使用LangChain的异步流式生成
|
|
||||||
messages = [
|
|
||||||
SystemMessage(content=system_prompt),
|
|
||||||
HumanMessage(content=query)
|
|
||||||
]
|
|
||||||
|
|
||||||
# 使用LangChain的流式响应
|
|
||||||
async for chunk in self.client.astream(messages):
|
|
||||||
if chunk.content:
|
|
||||||
# 确保只返回字符串类型
|
|
||||||
if isinstance(chunk.content, str):
|
|
||||||
yield chunk.content
|
|
||||||
elif isinstance(chunk.content, (list, dict)):
|
|
||||||
# 处理列表或字典类型的内容
|
|
||||||
import json
|
|
||||||
yield json.dumps(chunk.content)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# 记录详细错误,返回友好提示
|
|
||||||
log.error(f"AI处理查询失败: {str(e)}")
|
|
||||||
yield self._friendly_error_message(e)
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import asyncio
|
||||||
|
from fastmcp import Client
|
||||||
|
from fastmcp import FastMCP
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
# 转换为 MCP 服务器
|
||||||
|
mcp = FastMCP.from_fastapi(app=app)
|
||||||
|
|
||||||
|
async def demo():
|
||||||
|
async with Client(mcp) as client:
|
||||||
|
# 列出可用工具
|
||||||
|
tools = await client.list_tools()
|
||||||
|
for tool in tools:
|
||||||
|
print(f"工具: {tool.name}")
|
||||||
|
print(f"描述: {tool.description}")
|
||||||
|
if tool.inputSchema:
|
||||||
|
print(f"参数: {tool.inputSchema}")
|
||||||
|
# 访问标签和其他元数据
|
||||||
|
if hasattr(tool, 'meta') and tool.meta:
|
||||||
|
fastmcp_meta = tool.meta.get('_fastmcp', {})
|
||||||
|
print(f"标签: {fastmcp_meta.get('tags', [])}")
|
||||||
|
|
||||||
|
resources = await client.list_resources()
|
||||||
|
for resource in resources:
|
||||||
|
print(f"资源 URI: {resource.uri}")
|
||||||
|
print(f"名称: {resource.name}")
|
||||||
|
print(f"描述: {resource.description}")
|
||||||
|
print(f"MIME 类型: {resource.mimeType}")
|
||||||
|
# 访问标签和其他元数据
|
||||||
|
if hasattr(resource, '_meta') and resource._meta:
|
||||||
|
fastmcp_meta = resource._meta.get('_fastmcp', {})
|
||||||
|
print(f"标签: {fastmcp_meta.get('tags', [])}")
|
||||||
|
|
||||||
|
prompts = await client.list_prompts()
|
||||||
|
for prompt in prompts:
|
||||||
|
print(f"提示: {prompt.name}")
|
||||||
|
print(f"描述: {prompt.description}")
|
||||||
|
if prompt.arguments:
|
||||||
|
print(f"参数: {[arg.name for arg in prompt.arguments]}")
|
||||||
|
# 访问标签和其他元数据
|
||||||
|
if hasattr(prompt, '_meta') and prompt._meta:
|
||||||
|
fastmcp_meta = prompt._meta.get('_fastmcp', {})
|
||||||
|
print(f"标签: {fastmcp_meta.get('tags', [])}")
|
||||||
|
|
||||||
|
# 创建产品
|
||||||
|
result = await client.call_tool(
|
||||||
|
"create_product_products_post",
|
||||||
|
{
|
||||||
|
"name": "无线键盘",
|
||||||
|
"price": 79.99,
|
||||||
|
"category": "电子产品",
|
||||||
|
"description": "蓝牙机械键盘"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
print(f"已创建产品: {result.data}")
|
||||||
|
|
||||||
|
# 列出价格低于 $100 的电子产品
|
||||||
|
result = await client.call_tool(
|
||||||
|
"list_products_products_get",
|
||||||
|
{"category": "电子产品", "max_price": 100}
|
||||||
|
)
|
||||||
|
print(f"经济实惠的电子产品: {result.data}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(demo())
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from fastmcp import FastMCP
|
||||||
|
from fastmcp.prompts.prompt import Message
|
||||||
|
from mcp.types import PromptMessage, TextContent
|
||||||
|
|
||||||
|
app = FastMCP("Math")
|
||||||
|
mcp = FastMCP.from_fastapi(app=app)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def add(a: int, b: int) -> int:
|
||||||
|
"""Add two numbers"""
|
||||||
|
return a + b
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def multiply(a: int, b: int) -> int:
|
||||||
|
"""Multiply two numbers"""
|
||||||
|
return a * b
|
||||||
|
|
||||||
|
# 返回字符串的基本动态资源
|
||||||
|
@mcp.resource("resource://greeting")
|
||||||
|
def get_greeting() -> str:
|
||||||
|
"""提供简单的问候消息。"""
|
||||||
|
return "Hello from FastMCP Resources!"
|
||||||
|
|
||||||
|
# 返回 JSON 数据的资源(字典会自动序列化)
|
||||||
|
@mcp.resource("data://config")
|
||||||
|
def get_config() -> dict:
|
||||||
|
"""以 JSON 形式提供应用程序配置。"""
|
||||||
|
return {
|
||||||
|
"theme": "dark",
|
||||||
|
"version": "1.2.0",
|
||||||
|
"features": ["tools", "resources"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# 返回字符串的基本提示(自动转换为用户消息)
|
||||||
|
@mcp.prompt
|
||||||
|
def ask_about_topic(topic: str) -> str:
|
||||||
|
"""生成询问主题解释的用户消息。"""
|
||||||
|
return f"Can you please explain the concept of '{topic}'?"
|
||||||
|
|
||||||
|
# 返回特定消息类型的提示
|
||||||
|
@mcp.prompt
|
||||||
|
def generate_code_request(language: str, task_description: str) -> PromptMessage:
|
||||||
|
"""生成请求代码生成的用户消息。"""
|
||||||
|
content = f"Write a {language} function that performs the following task: {task_description}"
|
||||||
|
return PromptMessage(role="user", content=TextContent(type="text", text=content))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
mcp.run(transport="stdio")
|
||||||
|
# fastmcp run my_server.py:mcp --transport http --port 8000
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import json
|
import json
|
||||||
import importlib
|
import importlib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -17,12 +16,14 @@ from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
|||||||
from apscheduler.triggers.cron import CronTrigger
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
from apscheduler.triggers.date import DateTrigger
|
from apscheduler.triggers.date import DateTrigger
|
||||||
from apscheduler.triggers.interval import IntervalTrigger
|
from apscheduler.triggers.interval import IntervalTrigger
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from redis.asyncio.client import Redis
|
||||||
|
|
||||||
|
from app.common.enums import RedisInitKeyConfig
|
||||||
from app.config.setting import settings
|
from app.config.setting import settings
|
||||||
from app.core.database import engine, db_session, async_db_session
|
from app.core.database import engine, db_session, async_db_session
|
||||||
from app.core.exceptions import CustomException
|
from app.core.exceptions import CustomException
|
||||||
from app.core.logger import log
|
from app.core.logger import log
|
||||||
|
from app.core.redis_crud import RedisCURD
|
||||||
from app.utils.cron_util import CronUtil
|
from app.utils.cron_util import CronUtil
|
||||||
|
|
||||||
from app.api.v1.module_application.job.model import JobModel
|
from app.api.v1.module_application.job.model import JobModel
|
||||||
@@ -61,6 +62,8 @@ class SchedulerUtil:
|
|||||||
"""
|
"""
|
||||||
定时任务相关方法
|
定时任务相关方法
|
||||||
"""
|
"""
|
||||||
|
# 类变量,存储应用的Redis连接
|
||||||
|
redis_instance = None
|
||||||
@classmethod
|
@classmethod
|
||||||
def scheduler_event_listener(cls, event: JobEvent | JobExecutionEvent) -> None:
|
def scheduler_event_listener(cls, event: JobEvent | JobExecutionEvent) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -82,42 +85,27 @@ class SchedulerUtil:
|
|||||||
# 获取事件类型和任务ID
|
# 获取事件类型和任务ID
|
||||||
event_type = event.__class__.__name__
|
event_type = event.__class__.__name__
|
||||||
# 初始化任务状态
|
# 初始化任务状态
|
||||||
status = True
|
status = "0"
|
||||||
exception_info = ''
|
exception_info = ''
|
||||||
if isinstance(event, JobExecutionEvent) and event.exception:
|
if isinstance(event, JobExecutionEvent) and event.exception:
|
||||||
exception_info = str(event.exception)
|
exception_info = str(event.exception)
|
||||||
status = False
|
status = "1"
|
||||||
if hasattr(event, 'job_id'):
|
if hasattr(event, 'job_id'):
|
||||||
job_id = event.job_id
|
job_id = event.job_id
|
||||||
query_job = cls.get_job(job_id=job_id)
|
query_job = cls.get_job(job_id=job_id)
|
||||||
if query_job:
|
if query_job:
|
||||||
query_job_info = query_job.__getstate__()
|
job_message = (f"事件类型: {event_type}, 任务ID: {job_id}, "
|
||||||
# 获取任务名称
|
f"状态: {status}, 错误详情: {exception_info}, "
|
||||||
job_name = query_job_info.get('name')
|
f"执行于{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
# 获取任务组名
|
|
||||||
job_group = query_job._jobstore_alias
|
|
||||||
# # 获取任务执行器
|
|
||||||
job_executor = query_job_info.get('executor')
|
|
||||||
# 获取调用目标字符串
|
|
||||||
invoke_target = query_job_info.get('func')
|
|
||||||
# 获取调用函数位置参数
|
|
||||||
job_args = ','.join(map(str, query_job_info.get('args', [])))
|
|
||||||
# 获取调用函数关键字参数
|
|
||||||
job_kwargs = json.dumps(query_job_info.get('kwargs'))
|
|
||||||
# 获取任务触发器
|
|
||||||
job_trigger = str(query_job_info.get('trigger'))
|
|
||||||
# 构造日志消息
|
|
||||||
job_message = f"事件类型: {event_type}, 任务ID: {job_id}, 任务名称: {job_name}, 状态: {status}, 任务组: {job_group}, 错误详情: {exception_info}, 执行于{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
|
||||||
|
|
||||||
# 创建ORM对象
|
|
||||||
job_log = JobLogModel(
|
job_log = JobLogModel(
|
||||||
job_name=job_name,
|
job_name=query_job.name,
|
||||||
job_group=job_group,
|
job_group=query_job._jobstore_alias,
|
||||||
job_executor=job_executor,
|
job_executor=query_job.executor,
|
||||||
invoke_target=invoke_target,
|
invoke_target=query_job.func.__module__ + '.' + query_job.func.__qualname__,
|
||||||
job_args=job_args,
|
job_args=str(query_job.args),
|
||||||
job_kwargs=job_kwargs,
|
job_kwargs=str(query_job.kwargs),
|
||||||
job_trigger=job_trigger,
|
job_trigger=query_job.trigger,
|
||||||
job_message=job_message,
|
job_message=job_message,
|
||||||
status=status,
|
status=status,
|
||||||
exception_info=exception_info,
|
exception_info=exception_info,
|
||||||
@@ -126,34 +114,15 @@ class SchedulerUtil:
|
|||||||
job_id=job_id,
|
job_id=job_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 使用线程池执行操作以避免阻塞调度器和数据库锁定问题
|
with db_session.begin() as session:
|
||||||
executor = ThreadPoolExecutor(max_workers=1)
|
try:
|
||||||
executor.submit(cls._save_job_log_async_wrapper, job_log)
|
session.add(job_log)
|
||||||
executor.shutdown(wait=False)
|
session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _save_job_log_async_wrapper(cls, job_log) -> None:
|
async def init_system_scheduler(cls, redis: Redis) -> None:
|
||||||
"""
|
|
||||||
异步保存任务日志的包装器函数,在独立线程中运行
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- job_log (JobLogModel): 任务日志对象
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- None
|
|
||||||
"""
|
|
||||||
with db_session.begin() as session:
|
|
||||||
try:
|
|
||||||
session.add(job_log)
|
|
||||||
session.commit()
|
|
||||||
except Exception as e:
|
|
||||||
session.rollback()
|
|
||||||
log.error(f"保存任务日志失败: {str(e)}")
|
|
||||||
finally:
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def init_system_scheduler(cls) -> None:
|
|
||||||
"""
|
"""
|
||||||
应用启动时初始化定时任务。
|
应用启动时初始化定时任务。
|
||||||
|
|
||||||
@@ -164,33 +133,21 @@ class SchedulerUtil:
|
|||||||
from app.api.v1.module_application.job.crud import JobCRUD
|
from app.api.v1.module_application.job.crud import JobCRUD
|
||||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||||
log.info('🔎 开始启动定时任务...')
|
log.info('🔎 开始启动定时任务...')
|
||||||
|
# 保存Redis连接到类变量
|
||||||
|
cls.redis_instance = redis
|
||||||
# 启动调度器
|
# 启动调度器
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
|
|
||||||
# 添加事件监听器
|
# 添加事件监听器
|
||||||
scheduler.add_listener(cls.scheduler_event_listener, EVENT_ALL)
|
scheduler.add_listener(cls.scheduler_event_listener, EVENT_ALL)
|
||||||
|
|
||||||
async with async_db_session() as session:
|
async with async_db_session() as session:
|
||||||
async with session.begin():
|
async with session.begin():
|
||||||
auth = AuthSchema(db=session)
|
auth = AuthSchema(db=session)
|
||||||
job_list = await JobCRUD(auth).get_obj_list_crud()
|
job_list = await JobCRUD(auth).get_obj_list_crud()
|
||||||
|
|
||||||
# 只在一个实例上初始化任务
|
|
||||||
# 使用Redis锁确保只有一个实例执行任务初始化
|
# 使用Redis锁确保只有一个实例执行任务初始化
|
||||||
import redis.asyncio as redis
|
redis_client = RedisCURD(redis)
|
||||||
redis_client = redis.Redis(
|
lock_key = f'{RedisInitKeyConfig.APSCHEDULER_LOCK_KEY.key}:job'
|
||||||
host=settings.REDIS_HOST,
|
|
||||||
port=int(settings.REDIS_PORT),
|
|
||||||
username=settings.REDIS_USER,
|
|
||||||
password=settings.REDIS_PASSWORD,
|
|
||||||
db=int(settings.REDIS_DB_NAME),
|
|
||||||
)
|
|
||||||
|
|
||||||
# 尝试获取锁,过期时间10秒
|
# 尝试获取锁,过期时间10秒
|
||||||
lock_key = "scheduler_init_lock"
|
lock_acquired, lock_value = await redis_client.lock(lock_key, 10)
|
||||||
lock_acquired = await redis_client.set(lock_key, "1", ex=10, nx=True)
|
|
||||||
|
|
||||||
if lock_acquired:
|
if lock_acquired:
|
||||||
try:
|
try:
|
||||||
for item in job_list:
|
for item in job_list:
|
||||||
@@ -198,18 +155,16 @@ class SchedulerUtil:
|
|||||||
existing_job = cls.get_job(job_id=item.id)
|
existing_job = cls.get_job(job_id=item.id)
|
||||||
if existing_job:
|
if existing_job:
|
||||||
cls.remove_job(job_id=item.id) # 删除旧任务
|
cls.remove_job(job_id=item.id) # 删除旧任务
|
||||||
|
|
||||||
# 添加新任务
|
# 添加新任务
|
||||||
cls.add_job(item)
|
cls.add_job(item)
|
||||||
|
|
||||||
# 根据数据库中保存的状态来设置任务状态
|
# 根据数据库中保存的状态来设置任务状态
|
||||||
if hasattr(item, 'status') and item.status == "1":
|
if item.status == "1":
|
||||||
# 如果任务状态为暂停,则立即暂停刚添加的任务
|
# 如果任务状态为暂停,则立即暂停刚添加的任务
|
||||||
cls.pause_job(job_id=item.id)
|
cls.pause_job(job_id=item.id)
|
||||||
log.info('✅️ 系统初始定时任务加载成功')
|
log.info('✅️ 系统初始定时任务加载成功')
|
||||||
finally:
|
finally:
|
||||||
# 释放锁
|
# 释放锁
|
||||||
await redis_client.delete(lock_key)
|
await redis_client.unlock(lock_key, lock_value)
|
||||||
else:
|
else:
|
||||||
# 等待其他实例完成初始化
|
# 等待其他实例完成初始化
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -259,34 +214,45 @@ class SchedulerUtil:
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def _task_wrapper(cls, func, job_id, *args, **kwargs):
|
async def _task_wrapper(cls, func, job_id, *args, **kwargs):
|
||||||
"""任务执行包装器,添加分布式锁防止并发执行"""
|
"""任务执行包装器,添加分布式锁防止并发执行"""
|
||||||
from redis.asyncio import Redis
|
import asyncio
|
||||||
from app.config.setting import settings
|
# 使用类变量中的Redis连接
|
||||||
from app.core.logger import log
|
if not cls.redis_instance:
|
||||||
|
log.error(f"任务 {job_id} 执行失败:Redis连接未初始化")
|
||||||
|
return None
|
||||||
|
|
||||||
# 使用项目配置创建Redis连接池
|
redis_client = RedisCURD(redis=cls.redis_instance)
|
||||||
redis_client = Redis(
|
lock_key = f"{RedisInitKeyConfig.APSCHEDULER_LOCK_KEY.key}:{job_id}"
|
||||||
host=settings.REDIS_HOST,
|
lock_acquired = False
|
||||||
port=int(settings.REDIS_PORT),
|
lock_value = ""
|
||||||
username=settings.REDIS_USER,
|
renewal_task = None
|
||||||
password=settings.REDIS_PASSWORD,
|
|
||||||
db=int(settings.REDIS_DB_NAME),
|
|
||||||
encoding='utf-8',
|
|
||||||
decode_responses=True,
|
|
||||||
health_check_interval=20,
|
|
||||||
max_connections=settings.POOL_SIZE,
|
|
||||||
socket_timeout=settings.POOL_TIMEOUT
|
|
||||||
)
|
|
||||||
|
|
||||||
lock_key = f"job_lock:{job_id}"
|
# 定义锁续约函数
|
||||||
lock_expire = 30 # 锁过期时间,根据任务实际执行时间调整
|
async def renew_lock():
|
||||||
lock_acquired = None
|
"""定期续约锁的过期时间"""
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
# 等待锁过期时间的2/3后进行续约
|
||||||
|
await asyncio.sleep(20) # 30秒的2/3
|
||||||
|
# 使用redis_client.renew_lock续约锁,验证锁持有者
|
||||||
|
success = await redis_client.renew_lock(lock_key, 30, lock_value)
|
||||||
|
if success:
|
||||||
|
log.info(f"任务 {job_id} 锁续约成功")
|
||||||
|
else:
|
||||||
|
log.warning(f"任务 {job_id} 锁续约失败:锁可能已被其他实例获取")
|
||||||
|
break
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
log.info(f"任务 {job_id} 锁续约任务已取消")
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"任务 {job_id} 锁续约失败: {str(e)}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 获取分布式锁,使用nx=True确保原子性操作
|
# 获取分布式锁,使用原子性的lock方法
|
||||||
lock_acquired = await redis_client.set(lock_key, "1", ex=lock_expire, nx=True)
|
lock_acquired, lock_value = await redis_client.lock(lock_key, 30)
|
||||||
|
|
||||||
if lock_acquired:
|
if lock_acquired:
|
||||||
log.info(f"任务 {job_id} 获取执行锁成功")
|
log.info(f"任务 {job_id} 获取执行锁成功")
|
||||||
|
# 启动锁续约任务
|
||||||
|
renewal_task = asyncio.create_task(renew_lock())
|
||||||
|
|
||||||
# 执行任务
|
# 执行任务
|
||||||
if iscoroutinefunction(func):
|
if iscoroutinefunction(func):
|
||||||
return await func(*args, **kwargs)
|
return await func(*args, **kwargs)
|
||||||
@@ -295,7 +261,8 @@ class SchedulerUtil:
|
|||||||
log.info(f"任务 {job_id} 开始执行同步函数: {func.__name__}, 参数: {args}-{kwargs}")
|
log.info(f"任务 {job_id} 开始执行同步函数: {func.__name__}, 参数: {args}-{kwargs}")
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
result = await loop.run_in_executor(None, func, *args, **kwargs)
|
# 使用lambda包装函数调用,以支持关键字参数
|
||||||
|
result = await loop.run_in_executor(None, lambda: func(*args, **kwargs))
|
||||||
log.info(f"任务 {job_id} 同步函数执行完成,结果: {result}")
|
log.info(f"任务 {job_id} 同步函数执行完成,结果: {result}")
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -306,12 +273,18 @@ class SchedulerUtil:
|
|||||||
log.info(f"任务 {job_id} 获取执行锁失败,跳过本次执行")
|
log.info(f"任务 {job_id} 获取执行锁失败,跳过本次执行")
|
||||||
return None
|
return None
|
||||||
finally:
|
finally:
|
||||||
|
# 取消锁续约任务
|
||||||
|
if renewal_task and not renewal_task.done():
|
||||||
|
renewal_task.cancel()
|
||||||
|
try:
|
||||||
|
await renewal_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
# 释放锁
|
# 释放锁
|
||||||
if lock_acquired:
|
if lock_acquired:
|
||||||
await redis_client.delete(lock_key)
|
await redis_client.unlock(lock_key, lock_value)
|
||||||
log.info(f"任务 {job_id} 释放执行锁")
|
log.info(f"任务 {job_id} 释放执行锁")
|
||||||
# 关闭Redis连接
|
|
||||||
await redis_client.close()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_job(cls, job_info: JobModel) -> Job:
|
def add_job(cls, job_info: JobModel) -> Job:
|
||||||
|
|||||||
+9
-9
@@ -25,7 +25,7 @@ DemoRouter = APIRouter(route_class=OperationLogRoute, prefix="/demo", tags=["示
|
|||||||
@DemoRouter.get("/detail/{id}", summary="获取示例详情", description="获取示例详情")
|
@DemoRouter.get("/detail/{id}", summary="获取示例详情", description="获取示例详情")
|
||||||
async def get_obj_detail_controller(
|
async def get_obj_detail_controller(
|
||||||
id: int = Path(..., description="示例ID"),
|
id: int = Path(..., description="示例ID"),
|
||||||
auth: AuthSchema = Depends(AuthPermission(["module_gencode:demo:detail"]))
|
auth: AuthSchema = Depends(AuthPermission(["module_example:demo:detail"]))
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
获取示例详情
|
获取示例详情
|
||||||
@@ -45,7 +45,7 @@ async def get_obj_detail_controller(
|
|||||||
async def get_obj_list_controller(
|
async def get_obj_list_controller(
|
||||||
page: PaginationQueryParam = Depends(),
|
page: PaginationQueryParam = Depends(),
|
||||||
search: DemoQueryParam = Depends(),
|
search: DemoQueryParam = Depends(),
|
||||||
auth: AuthSchema = Depends(AuthPermission(["module_gencode:demo:query"]))
|
auth: AuthSchema = Depends(AuthPermission(["module_example:demo:query"]))
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
查询示例列表
|
查询示例列表
|
||||||
@@ -72,7 +72,7 @@ async def get_obj_list_controller(
|
|||||||
@DemoRouter.post("/create", summary="创建示例", description="创建示例")
|
@DemoRouter.post("/create", summary="创建示例", description="创建示例")
|
||||||
async def create_obj_controller(
|
async def create_obj_controller(
|
||||||
data: DemoCreateSchema,
|
data: DemoCreateSchema,
|
||||||
auth: AuthSchema = Depends(AuthPermission(["module_gencode:demo:create"]))
|
auth: AuthSchema = Depends(AuthPermission(["module_example:demo:create"]))
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
创建示例
|
创建示例
|
||||||
@@ -92,7 +92,7 @@ async def create_obj_controller(
|
|||||||
async def update_obj_controller(
|
async def update_obj_controller(
|
||||||
data: DemoUpdateSchema,
|
data: DemoUpdateSchema,
|
||||||
id: int = Path(..., description="示例ID"),
|
id: int = Path(..., description="示例ID"),
|
||||||
auth: AuthSchema = Depends(AuthPermission(["module_gencode:demo:update"]))
|
auth: AuthSchema = Depends(AuthPermission(["module_example:demo:update"]))
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
修改示例
|
修改示例
|
||||||
@@ -112,7 +112,7 @@ async def update_obj_controller(
|
|||||||
@DemoRouter.delete("/delete", summary="删除示例", description="删除示例")
|
@DemoRouter.delete("/delete", summary="删除示例", description="删除示例")
|
||||||
async def delete_obj_controller(
|
async def delete_obj_controller(
|
||||||
ids: list[int] = Body(..., description="ID列表"),
|
ids: list[int] = Body(..., description="ID列表"),
|
||||||
auth: AuthSchema = Depends(AuthPermission(["module_gencode:demo:delete"]))
|
auth: AuthSchema = Depends(AuthPermission(["module_example:demo:delete"]))
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
删除示例
|
删除示例
|
||||||
@@ -131,7 +131,7 @@ async def delete_obj_controller(
|
|||||||
@DemoRouter.patch("/available/setting", summary="批量修改示例状态", description="批量修改示例状态")
|
@DemoRouter.patch("/available/setting", summary="批量修改示例状态", description="批量修改示例状态")
|
||||||
async def batch_set_available_obj_controller(
|
async def batch_set_available_obj_controller(
|
||||||
data: BatchSetAvailable,
|
data: BatchSetAvailable,
|
||||||
auth: AuthSchema = Depends(AuthPermission(["module_gencode:demo:patch"]))
|
auth: AuthSchema = Depends(AuthPermission(["module_example:demo:patch"]))
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
批量修改示例状态
|
批量修改示例状态
|
||||||
@@ -150,7 +150,7 @@ async def batch_set_available_obj_controller(
|
|||||||
@DemoRouter.post('/export', summary="导出示例", description="导出示例")
|
@DemoRouter.post('/export', summary="导出示例", description="导出示例")
|
||||||
async def export_obj_list_controller(
|
async def export_obj_list_controller(
|
||||||
search: DemoQueryParam = Depends(),
|
search: DemoQueryParam = Depends(),
|
||||||
auth: AuthSchema = Depends(AuthPermission(["module_gencode:demo:export"]))
|
auth: AuthSchema = Depends(AuthPermission(["module_example:demo:export"]))
|
||||||
) -> StreamingResponse:
|
) -> StreamingResponse:
|
||||||
"""
|
"""
|
||||||
导出示例
|
导出示例
|
||||||
@@ -177,7 +177,7 @@ async def export_obj_list_controller(
|
|||||||
@DemoRouter.post('/import', summary="导入示例", description="导入示例")
|
@DemoRouter.post('/import', summary="导入示例", description="导入示例")
|
||||||
async def import_obj_list_controller(
|
async def import_obj_list_controller(
|
||||||
file: UploadFile,
|
file: UploadFile,
|
||||||
auth: AuthSchema = Depends(AuthPermission(["module_gencode:demo:import"]))
|
auth: AuthSchema = Depends(AuthPermission(["module_example:demo:import"]))
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
导入示例
|
导入示例
|
||||||
@@ -193,7 +193,7 @@ async def import_obj_list_controller(
|
|||||||
log.info(f"导入示例成功: {batch_import_result}")
|
log.info(f"导入示例成功: {batch_import_result}")
|
||||||
return SuccessResponse(data=batch_import_result, msg="导入示例成功")
|
return SuccessResponse(data=batch_import_result, msg="导入示例成功")
|
||||||
|
|
||||||
@DemoRouter.post('/download/template', summary="获取示例导入模板", description="获取示例导入模板", dependencies=[Depends(AuthPermission(["module_gencode:demo:download"]))])
|
@DemoRouter.post('/download/template', summary="获取示例导入模板", description="获取示例导入模板", dependencies=[Depends(AuthPermission(["module_example:demo:download"]))])
|
||||||
async def export_obj_template_controller() -> StreamingResponse:
|
async def export_obj_template_controller() -> StreamingResponse:
|
||||||
"""
|
"""
|
||||||
获取示例导入模板
|
获取示例导入模板
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import enum
|
||||||
|
from sqlalchemy import JSON, String, Integer, BIGINT, Float, Boolean, Date, Time, DateTime, Enum, Text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
from datetime import date, time, datetime
|
||||||
|
|
||||||
|
from app.core.base_model import ModelMixin, UserMixin
|
||||||
|
|
||||||
|
|
||||||
|
class StatusEnum(enum.Enum):
|
||||||
|
"""状态枚举"""
|
||||||
|
ACTIVE = "active"
|
||||||
|
INACTIVE = "inactive"
|
||||||
|
|
||||||
|
|
||||||
|
class DemoModel(ModelMixin, UserMixin):
|
||||||
|
"""
|
||||||
|
示例表 - 涵盖大多数常用数据类型
|
||||||
|
"""
|
||||||
|
__tablename__: str = 'gen_demo'
|
||||||
|
__table_args__: dict[str, str] = ({'comment': '示例表'})
|
||||||
|
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||||
|
|
||||||
|
# 字符串类型
|
||||||
|
name: Mapped[str] = mapped_column(String(64), nullable=False, comment='名称')
|
||||||
|
a: Mapped[int | None] = mapped_column(Integer, nullable=True, comment='整数')
|
||||||
|
b: Mapped[int | None] = mapped_column(BIGINT, nullable=True, comment='大整数')
|
||||||
|
c: Mapped[float | None] = mapped_column(Float, nullable=True, comment='浮点数')
|
||||||
|
d: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, comment='布尔型')
|
||||||
|
e: Mapped[date | None] = mapped_column(Date, nullable=True, comment='日期')
|
||||||
|
f: Mapped[time | None] = mapped_column(Time, nullable=True, comment='时间')
|
||||||
|
g: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, comment='日期时间')
|
||||||
|
h: Mapped[str | None] = mapped_column(Text, nullable=True, comment='长文本')
|
||||||
|
i: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment='元数据(JSON格式)')
|
||||||
|
|
||||||
+16
-1
@@ -1,10 +1,11 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from datetime import date, time, datetime
|
||||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
|
|
||||||
from app.core.base_schema import BaseSchema, UserBySchema
|
from app.core.base_schema import BaseSchema, UserBySchema
|
||||||
from app.core.validator import DateTimeStr
|
from app.core.validator import DateTimeStr, DateStr, TimeStr
|
||||||
|
|
||||||
|
|
||||||
class DemoCreateSchema(BaseModel):
|
class DemoCreateSchema(BaseModel):
|
||||||
@@ -12,6 +13,15 @@ class DemoCreateSchema(BaseModel):
|
|||||||
name: str = Field(..., description='名称')
|
name: str = Field(..., description='名称')
|
||||||
status: str = Field(default="0", description="是否启用(0:启用 1:禁用)")
|
status: str = Field(default="0", description="是否启用(0:启用 1:禁用)")
|
||||||
description: str | None = Field(default=None, description="描述")
|
description: str | None = Field(default=None, description="描述")
|
||||||
|
a: int | None = Field(default=None, description="整数")
|
||||||
|
b: int | None = Field(default=None, description="大整数")
|
||||||
|
c: float | None = Field(default=None, description="浮点数")
|
||||||
|
d: bool = Field(default=True, description="布尔型")
|
||||||
|
e: date | None = Field(default=None, description="日期")
|
||||||
|
f: time | None = Field(default=None, description="时间")
|
||||||
|
g: datetime | None = Field(default=None, description="日期时间")
|
||||||
|
h: str | None = Field(default=None, description="长文本")
|
||||||
|
i: dict | None = Field(default=None, description="元数据(JSON格式)")
|
||||||
|
|
||||||
@field_validator('name')
|
@field_validator('name')
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -51,6 +61,11 @@ class DemoOutSchema(DemoCreateSchema, BaseSchema, UserBySchema):
|
|||||||
"""响应模型"""
|
"""响应模型"""
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
# 覆盖日期时间字段,使用可序列化的类型
|
||||||
|
e: DateStr | None = Field(default=None, description="日期")
|
||||||
|
f: TimeStr | None = Field(default=None, description="时间")
|
||||||
|
g: DateTimeStr | None = Field(default=None, description="日期时间")
|
||||||
|
|
||||||
|
|
||||||
class DemoQueryParam:
|
class DemoQueryParam:
|
||||||
"""示例查询参数"""
|
"""示例查询参数"""
|
||||||
+1
@@ -1,6 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from fastapi import UploadFile
|
from fastapi import UploadFile
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from sqlalchemy import String
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
|
||||||
|
|
||||||
from app.core.base_model import ModelMixin, UserMixin
|
|
||||||
|
|
||||||
|
|
||||||
class DemoModel(ModelMixin, UserMixin):
|
|
||||||
"""
|
|
||||||
示例表
|
|
||||||
"""
|
|
||||||
__tablename__: str = 'gen_demo'
|
|
||||||
__table_args__: dict[str, str] = ({'comment': '示例表'})
|
|
||||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
|
||||||
|
|
||||||
name: Mapped[str | None] = mapped_column(String(64), nullable=True, default='', comment='名称')
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from sqlalchemy.engine.row import Row
|
from sqlalchemy.engine.result import Result
|
||||||
from sqlalchemy import and_, select, text
|
from sqlalchemy.engine.reflection import Inspector
|
||||||
from typing import Sequence
|
from sqlalchemy import Inspector, select, text, inspect
|
||||||
from sqlglot.expressions import Expression
|
from typing import Any, Sequence
|
||||||
|
|
||||||
from app.core.logger import log
|
from app.core.logger import log
|
||||||
from app.config.setting import settings
|
from app.config.setting import settings
|
||||||
@@ -128,79 +128,41 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
|||||||
返回:
|
返回:
|
||||||
- list[dict]: 数据库表列表信息(已转为可序列化字典)。
|
- list[dict]: 数据库表列表信息(已转为可序列化字典)。
|
||||||
"""
|
"""
|
||||||
|
database_name = settings.DATABASE_NAME
|
||||||
|
database_type = settings.DATABASE_TYPE
|
||||||
|
|
||||||
# 使用更健壮的方式检测数据库方言
|
from app.core.database import engine
|
||||||
if settings.DATABASE_TYPE == "postgres":
|
inspector: Inspector = inspect(engine)
|
||||||
query_sql = (
|
table_names = inspector.get_table_names()
|
||||||
select(
|
|
||||||
text("t.table_catalog as database_name"),
|
|
||||||
text("t.table_name as table_name"),
|
|
||||||
text("t.table_type as table_type"),
|
|
||||||
text("pd.description as table_comment"),
|
|
||||||
)
|
|
||||||
.select_from(text(
|
|
||||||
"information_schema.tables t \n"
|
|
||||||
"LEFT JOIN pg_catalog.pg_class c ON c.relname = t.table_name \n"
|
|
||||||
"LEFT JOIN pg_catalog.pg_namespace n ON n.nspname = t.table_schema AND c.relnamespace = n.oid \n"
|
|
||||||
"LEFT JOIN pg_catalog.pg_description pd ON pd.objoid = c.oid AND pd.objsubid = 0"
|
|
||||||
))
|
|
||||||
.where(
|
|
||||||
and_(
|
|
||||||
text("t.table_catalog = (select current_database())"),
|
|
||||||
text("t.is_insertable_into = 'YES'"),
|
|
||||||
text("t.table_schema = 'public'"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
query_sql = (
|
|
||||||
select(
|
|
||||||
text("table_schema as database_name"),
|
|
||||||
text("table_name as table_name"),
|
|
||||||
text("table_type as table_type"),
|
|
||||||
text("table_comment as table_comment"),
|
|
||||||
)
|
|
||||||
.select_from(text("information_schema.tables"))
|
|
||||||
.where(
|
|
||||||
and_(
|
|
||||||
text("table_schema = (select database())"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 动态条件构造
|
|
||||||
params = {}
|
|
||||||
if search and search.table_name:
|
|
||||||
query_sql = query_sql.where(
|
|
||||||
text("lower(table_name) like lower(:table_name)")
|
|
||||||
)
|
|
||||||
params['table_name'] = f"%{search.table_name}%"
|
|
||||||
if search and search.table_comment:
|
|
||||||
# 对于PostgreSQL,表注释字段是pd.description,而不是table_comment
|
|
||||||
if settings.DATABASE_TYPE == "postgres":
|
|
||||||
query_sql = query_sql.where(
|
|
||||||
text("lower(pd.description) like lower(:table_comment)")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
query_sql = query_sql.where(
|
|
||||||
text("lower(table_comment) like lower(:table_comment)")
|
|
||||||
)
|
|
||||||
params['table_comment'] = f"%{search.table_comment}%"
|
|
||||||
|
|
||||||
# 执行查询并绑定参数
|
|
||||||
all_data = (await self.auth.db.execute(query_sql, params)).fetchall()
|
|
||||||
|
|
||||||
# 将Row对象转换为字典列表,解决JSON序列化问题
|
|
||||||
dict_data = []
|
dict_data = []
|
||||||
for row in all_data:
|
for table_name in table_names:
|
||||||
# 检查row是否为Row对象
|
try:
|
||||||
if isinstance(row, Row):
|
table_comment = inspector.get_table_comment(table_name)
|
||||||
# 使用._mapping获取字典
|
comment = table_comment.get('text', '') if isinstance(table_comment, dict) else table_comment
|
||||||
dict_row = GenDBTableSchema(**dict(row._mapping)).model_dump()
|
table_comment = comment or ""
|
||||||
dict_data.append(dict_row)
|
except Exception as e:
|
||||||
else:
|
log.warning(f"获取表 {table_name} 的注释失败: {e}")
|
||||||
dict_row = GenDBTableSchema(**dict(row)).model_dump()
|
table_comment = ""
|
||||||
dict_data.append(dict_row)
|
|
||||||
|
# 统一处理 search 为 None 的情况,避免重复判断
|
||||||
|
if search:
|
||||||
|
# 表名过滤:忽略大小写,支持模糊匹配
|
||||||
|
if search.table_name and search.table_name.lower() not in table_name.lower():
|
||||||
|
continue
|
||||||
|
# 表注释过滤:忽略大小写,支持模糊匹配;table_comment 为 None 时视为空字符串
|
||||||
|
if search.table_comment and search.table_comment not in table_comment:
|
||||||
|
continue
|
||||||
|
|
||||||
|
table_info = {
|
||||||
|
"database_name": database_name,
|
||||||
|
"table_name": table_name,
|
||||||
|
"table_type": database_type,
|
||||||
|
"table_comment": table_comment
|
||||||
|
}
|
||||||
|
|
||||||
|
dict_data.append(GenDBTableSchema(**table_info).model_dump())
|
||||||
|
|
||||||
return dict_data
|
return dict_data
|
||||||
|
|
||||||
async def get_db_table_list_by_names(self, table_names: list[str]) -> list[GenDBTableSchema]:
|
async def get_db_table_list_by_names(self, table_names: list[str]) -> list[GenDBTableSchema]:
|
||||||
@@ -216,68 +178,18 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
|||||||
# 处理空列表情况
|
# 处理空列表情况
|
||||||
if not table_names:
|
if not table_names:
|
||||||
return []
|
return []
|
||||||
|
# 调用get_db_table_list获取所有表信息
|
||||||
|
all_tables = await self.get_db_table_list()
|
||||||
|
|
||||||
# 使用更健壮的方式检测数据库方言
|
# 过滤出指定名称的表
|
||||||
if settings.DATABASE_TYPE == "postgres":
|
table_names_set = set(table_names) # 转换为集合以提高查找效率
|
||||||
# PostgreSQL使用ANY操作符和正确的参数绑定
|
filtered_tables = [
|
||||||
query_sql = """
|
GenDBTableSchema(**table)
|
||||||
SELECT
|
for table in all_tables
|
||||||
t.table_catalog as database_name,
|
if table["table_name"] in table_names_set
|
||||||
t.table_name as table_name,
|
]
|
||||||
t.table_type as table_type,
|
|
||||||
pd.description as table_comment
|
|
||||||
FROM
|
|
||||||
information_schema.tables t
|
|
||||||
LEFT JOIN pg_catalog.pg_class c ON c.relname = t.table_name
|
|
||||||
LEFT JOIN pg_catalog.pg_namespace n ON n.nspname = t.table_schema AND c.relnamespace = n.oid
|
|
||||||
LEFT JOIN pg_catalog.pg_description pd ON pd.objoid = c.oid AND pd.objsubid = 0
|
|
||||||
WHERE
|
|
||||||
t.table_catalog = (select current_database())
|
|
||||||
AND t.is_insertable_into = 'YES'
|
|
||||||
AND t.table_schema = 'public'
|
|
||||||
AND t.table_name = ANY(:table_names)
|
|
||||||
"""
|
|
||||||
else:
|
|
||||||
query_sql = """
|
|
||||||
SELECT
|
|
||||||
table_schema as database_name,
|
|
||||||
table_name as table_name,
|
|
||||||
table_type as table_type,
|
|
||||||
table_comment as table_comment
|
|
||||||
FROM
|
|
||||||
information_schema.tables
|
|
||||||
WHERE
|
|
||||||
table_schema = (select database())
|
|
||||||
AND table_name IN :table_names
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 创建新的数据库会话上下文来执行查询,避免受外部事务状态影响
|
return filtered_tables
|
||||||
try:
|
|
||||||
# 去重表名列表,避免重复查询
|
|
||||||
unique_table_names = list(set(table_names))
|
|
||||||
|
|
||||||
# 使用只读事务执行查询,不影响主事务
|
|
||||||
if settings.DATABASE_TYPE == "postgres":
|
|
||||||
gen_db_table_list = (await self.auth.db.execute(text(query_sql), {"table_names": unique_table_names})).fetchall()
|
|
||||||
else:
|
|
||||||
gen_db_table_list = (await self.auth.db.execute(text(query_sql), {"table_names": tuple(unique_table_names)})).fetchall()
|
|
||||||
except Exception as e:
|
|
||||||
log.error(f"查询表信息时发生错误: {e}")
|
|
||||||
# 查询错误时直接抛出,不需要事务处理
|
|
||||||
raise
|
|
||||||
|
|
||||||
# 将Row对象转换为字典列表,解决JSON序列化问题
|
|
||||||
dict_data = []
|
|
||||||
for row in gen_db_table_list:
|
|
||||||
# 检查row是否为Row对象
|
|
||||||
if isinstance(row, Row):
|
|
||||||
# 使用._mapping获取字典
|
|
||||||
dict_row = GenDBTableSchema(**dict(row._mapping))
|
|
||||||
dict_data.append(dict_row)
|
|
||||||
else:
|
|
||||||
dict_row = GenDBTableSchema(**dict(row))
|
|
||||||
dict_data.append(dict_row)
|
|
||||||
return dict_data
|
|
||||||
|
|
||||||
async def check_table_exists(self, table_name: str) -> bool:
|
async def check_table_exists(self, table_name: str) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -289,41 +201,9 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
|||||||
返回:
|
返回:
|
||||||
- bool: 如果表存在返回True,否则返回False。
|
- bool: 如果表存在返回True,否则返回False。
|
||||||
"""
|
"""
|
||||||
try:
|
from app.core.database import engine
|
||||||
# 根据不同数据库类型使用不同的查询方式
|
inspector: Inspector = inspect(engine)
|
||||||
if settings.DATABASE_TYPE.lower() == 'mysql':
|
return inspector.has_table(table_name)
|
||||||
query = text("SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = :table_name")
|
|
||||||
else:
|
|
||||||
query = text("SELECT 1 FROM pg_tables WHERE tablename = :table_name")
|
|
||||||
|
|
||||||
result = await self.auth.db.execute(query, {"table_name": table_name})
|
|
||||||
return result.scalar() is not None
|
|
||||||
except Exception as e:
|
|
||||||
log.error(f"检查表格存在性时发生错误: {e}")
|
|
||||||
# 出错时返回False,避免误报表已存在
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def create_table_by_sql(self, sql_statements: list[Expression | None]) -> bool:
|
|
||||||
"""
|
|
||||||
根据SQL语句创建表结构。
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- sql (str): 创建表的SQL语句。
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- bool: 是否创建成功。
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 执行SQL但不手动提交事务,由框架管理事务生命周期
|
|
||||||
for sql_statement in sql_statements:
|
|
||||||
if not sql_statement:
|
|
||||||
continue
|
|
||||||
sql = sql_statement.sql(dialect=settings.DATABASE_TYPE)
|
|
||||||
await self.auth.db.execute(text(sql))
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
log.error(f"创建表时发生错误: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def execute_sql(self, sql: str) -> bool:
|
async def execute_sql(self, sql: str) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -356,6 +236,82 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
|
|||||||
"""
|
"""
|
||||||
super().__init__(model=GenTableColumnModel, auth=auth)
|
super().__init__(model=GenTableColumnModel, auth=auth)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sync_get_table_columns(database_type, table_name):
|
||||||
|
"""
|
||||||
|
同步函数:获取数据库表的列信息
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- database_type: 数据库类型
|
||||||
|
- table_name: 表名
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list: 列信息列表
|
||||||
|
"""
|
||||||
|
# 使用SQLAlchemy Inspector获取表列信息
|
||||||
|
from app.core.database import engine
|
||||||
|
inspector: Inspector = inspect(engine)
|
||||||
|
|
||||||
|
# 获取列信息
|
||||||
|
columns = inspector.get_columns(table_name)
|
||||||
|
|
||||||
|
# 获取主键信息
|
||||||
|
try:
|
||||||
|
pk_constraint = inspector.get_pk_constraint(table_name)
|
||||||
|
primary_keys = set(pk_constraint.get("constrained_columns", [])) if pk_constraint else set()
|
||||||
|
except Exception:
|
||||||
|
primary_keys = set()
|
||||||
|
|
||||||
|
# 获取唯一约束信息
|
||||||
|
unique_columns = set()
|
||||||
|
try:
|
||||||
|
unique_constraints = inspector.get_unique_constraints(table_name)
|
||||||
|
for constraint in unique_constraints:
|
||||||
|
unique_columns.update(constraint.get("column_names", []))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 处理列信息
|
||||||
|
columns_list = []
|
||||||
|
for idx, column in enumerate(columns):
|
||||||
|
# 获取列的基本信息
|
||||||
|
column_name = column['name']
|
||||||
|
column_type = str(column['type'])
|
||||||
|
is_nullable = column.get('nullable', True)
|
||||||
|
column_default = column.get('default', None)
|
||||||
|
# 获取列注释(如果有的话)
|
||||||
|
column_comment = column.get('comment', '')
|
||||||
|
# 判断是否为主键
|
||||||
|
is_pk = column_name in primary_keys
|
||||||
|
# 判断是否为唯一约束
|
||||||
|
is_unique = column_name in unique_columns
|
||||||
|
# 判断是否为自增列(基于数据库类型和列类型)
|
||||||
|
is_increment = column.get('autoincrement', False) in (True, 'auto')
|
||||||
|
# 获取列长度(如果适用)
|
||||||
|
column_length = None
|
||||||
|
# 使用getattr安全地获取length属性,避免访问不存在时抛出AttributeError
|
||||||
|
column_length = getattr(column['type'], 'length', None)
|
||||||
|
if column_length is not None:
|
||||||
|
column_length = str(getattr(column['type'], 'length', ''))
|
||||||
|
|
||||||
|
# 构造列信息字典
|
||||||
|
column_info = {
|
||||||
|
"column_name": column_name,
|
||||||
|
"column_comment": column_comment or '',
|
||||||
|
"column_type": column_type,
|
||||||
|
"column_length": column_length or '',
|
||||||
|
"column_default": str(column_default) if column_default is not None else '',
|
||||||
|
"sort": idx + 1, # 序号从1开始
|
||||||
|
"is_pk": 1 if is_pk else 0,
|
||||||
|
"is_increment": 1 if is_increment else 0,
|
||||||
|
"is_nullable": 1 if is_nullable else 0,
|
||||||
|
"is_unique": 1 if is_unique else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
columns_list.append(column_info)
|
||||||
|
|
||||||
|
return columns_list
|
||||||
|
|
||||||
async def get_gen_table_column_by_id(self, id: int, preload: list | None = None) -> GenTableColumnModel | None:
|
async def get_gen_table_column_by_id(self, id: int, preload: list | None = None) -> GenTableColumnModel | None:
|
||||||
"""根据业务表字段ID获取业务表字段信息。
|
"""根据业务表字段ID获取业务表字段信息。
|
||||||
|
|
||||||
@@ -408,99 +364,17 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
|
|||||||
raise ValueError("数据表名称不能为空")
|
raise ValueError("数据表名称不能为空")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if settings.DATABASE_TYPE == "mysql":
|
# 直接调用同步方法获取列信息
|
||||||
query_sql = """
|
columns_info = GenTableColumnCRUD._sync_get_table_columns(
|
||||||
SELECT
|
settings.DATABASE_TYPE,
|
||||||
c.column_name AS column_name,
|
table_name
|
||||||
c.column_comment AS column_comment,
|
)
|
||||||
c.column_type AS column_type,
|
|
||||||
c.character_maximum_length AS column_length,
|
|
||||||
c.column_default AS column_default,
|
|
||||||
c.ordinal_position AS sort,
|
|
||||||
(CASE WHEN c.column_key = 'PRI' THEN 1 ELSE 0 END) AS is_pk,
|
|
||||||
(CASE WHEN c.extra = 'auto_increment' THEN 1 ELSE 0 END) AS is_increment,
|
|
||||||
(CASE WHEN (c.is_nullable = 'NO' AND c.column_key != 'PRI') THEN 1 ELSE 0 END) AS is_nullable,
|
|
||||||
(CASE
|
|
||||||
WHEN c.column_name IN (
|
|
||||||
SELECT k.column_name
|
|
||||||
FROM information_schema.key_column_usage k
|
|
||||||
JOIN information_schema.table_constraints t
|
|
||||||
ON k.constraint_name = t.constraint_name
|
|
||||||
WHERE k.table_schema = c.table_schema
|
|
||||||
AND k.table_name = c.table_name
|
|
||||||
AND t.constraint_type = 'UNIQUE'
|
|
||||||
) THEN 1 ELSE 0
|
|
||||||
END) AS is_unique
|
|
||||||
FROM
|
|
||||||
information_schema.columns c
|
|
||||||
WHERE c.table_schema = (SELECT DATABASE())
|
|
||||||
AND c.table_name = :table_name
|
|
||||||
ORDER BY
|
|
||||||
c.ordinal_position
|
|
||||||
"""
|
|
||||||
else:
|
|
||||||
query_sql = """
|
|
||||||
SELECT
|
|
||||||
c.column_name AS column_name,
|
|
||||||
COALESCE(pgd.description, '') AS column_comment,
|
|
||||||
c.udt_name AS column_type,
|
|
||||||
c.character_maximum_length AS column_length,
|
|
||||||
c.column_default AS column_default,
|
|
||||||
c.ordinal_position AS sort,
|
|
||||||
(CASE WHEN EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.table_constraints tc
|
|
||||||
JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name
|
|
||||||
WHERE tc.table_name = c.table_name
|
|
||||||
AND tc.constraint_type = 'PRIMARY KEY'
|
|
||||||
AND ccu.column_name = c.column_name
|
|
||||||
) THEN 1 ELSE 0 END) AS is_pk,
|
|
||||||
(CASE WHEN c.column_default LIKE 'nextval%' THEN 1 ELSE 0 END) AS is_increment,
|
|
||||||
(CASE WHEN c.is_nullable = 'NO' THEN 1 ELSE 0 END) AS is_nullable,
|
|
||||||
(CASE WHEN EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.table_constraints tc
|
|
||||||
JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name
|
|
||||||
WHERE tc.table_name = c.table_name
|
|
||||||
AND tc.constraint_type = 'UNIQUE'
|
|
||||||
AND ccu.column_name = c.column_name
|
|
||||||
) THEN 1 ELSE 0 END) AS is_unique
|
|
||||||
FROM
|
|
||||||
information_schema.columns c
|
|
||||||
LEFT JOIN pg_catalog.pg_description pgd ON
|
|
||||||
pgd.objoid = (SELECT oid FROM pg_class WHERE relname = c.table_name)
|
|
||||||
AND pgd.objsubid = c.ordinal_position
|
|
||||||
WHERE c.table_catalog = current_database()
|
|
||||||
AND c.table_schema = 'public'
|
|
||||||
AND c.table_name = :table_name
|
|
||||||
ORDER BY
|
|
||||||
c.ordinal_position
|
|
||||||
"""
|
|
||||||
|
|
||||||
query = text(query_sql).bindparams(table_name=table_name)
|
|
||||||
result = await self.auth.db.execute(query)
|
|
||||||
rows = result.fetchall() if result else []
|
|
||||||
|
|
||||||
# 确保rows是可迭代对象
|
|
||||||
if not rows:
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
# 转换为GenTableColumnOutSchema对象列表
|
||||||
columns_list = []
|
columns_list = []
|
||||||
for row in rows:
|
for column_info in columns_info:
|
||||||
# 防御性编程:检查row是否有足够的元素
|
columns_list.append(GenTableColumnOutSchema(**column_info))
|
||||||
if len(row) >= 10:
|
|
||||||
columns_list.append(
|
|
||||||
GenTableColumnOutSchema(
|
|
||||||
column_name=row[0],
|
|
||||||
column_comment=row[1],
|
|
||||||
column_type=row[2],
|
|
||||||
column_length=str(row[3]) if row[3] is not None else '',
|
|
||||||
column_default=str(row[4]) if row[4] is not None else '',
|
|
||||||
sort=row[5],
|
|
||||||
is_pk=row[6],
|
|
||||||
is_increment=row[7],
|
|
||||||
is_nullable=row[8],
|
|
||||||
is_unique=row[9],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return columns_list
|
return columns_list
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"获取表{table_name}的字段列表时出错: {str(e)}")
|
log.error(f"获取表{table_name}的字段列表时出错: {str(e)}")
|
||||||
|
|||||||
@@ -2,17 +2,15 @@
|
|||||||
|
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
import zipfile
|
import zipfile
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from sqlglot.expressions import Add, Alter, Create, Delete, Drop, Expression, Insert, Table, TruncateTable, Update
|
from sqlglot.expressions import Add, Alter, Create, Delete, Drop, Expression, Insert, Table, TruncateTable, Update
|
||||||
from sqlglot import parse as sqlglot_parse
|
import sqlglot
|
||||||
|
|
||||||
from app.config.path_conf import BASE_DIR
|
from app.config.path_conf import BASE_DIR
|
||||||
from app.config.setting import settings
|
from app.config.setting import settings
|
||||||
from app.core.logger import log
|
from app.core.logger import log
|
||||||
from app.core.exceptions import CustomException
|
from app.core.exceptions import CustomException
|
||||||
|
|
||||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||||
from .tools.jinja2_template_util import Jinja2TemplateUtil
|
from .tools.jinja2_template_util import Jinja2TemplateUtil
|
||||||
from .tools.gen_util import GenUtils
|
from .tools.gen_util import GenUtils
|
||||||
@@ -37,8 +35,14 @@ class GenTableService:
|
|||||||
@classmethod
|
@classmethod
|
||||||
@handle_service_exception
|
@handle_service_exception
|
||||||
async def get_gen_table_detail_service(cls, auth: AuthSchema, table_id: int) -> dict:
|
async def get_gen_table_detail_service(cls, auth: AuthSchema, table_id: int) -> dict:
|
||||||
"""获取业务表详细信息(含字段与其他表列表)。
|
"""获取详细信息。
|
||||||
- 备注:优先解析`options`为`GenTableOptionSchema`,设置`parent_menu_id`等选项;保证`columns`与`tables`结构完整。
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- table_id (int): 业务表ID。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 包含业务表详细信息的字典。
|
||||||
"""
|
"""
|
||||||
gen_table = await cls.get_gen_table_by_id_service(auth, table_id)
|
gen_table = await cls.get_gen_table_by_id_service(auth, table_id)
|
||||||
return GenTableOutSchema.model_validate(gen_table).model_dump()
|
return GenTableOutSchema.model_validate(gen_table).model_dump()
|
||||||
@@ -62,8 +66,14 @@ class GenTableService:
|
|||||||
@classmethod
|
@classmethod
|
||||||
@handle_service_exception
|
@handle_service_exception
|
||||||
async def get_gen_db_table_list_service(cls, auth: AuthSchema, search: GenTableQueryParam) -> list[Any]:
|
async def get_gen_db_table_list_service(cls, auth: AuthSchema, search: GenTableQueryParam) -> list[Any]:
|
||||||
"""获取数据库表列表(跨方言)。
|
"""获取数据库表列表。
|
||||||
- 备注:返回已转换为字典的结构,适用于前端直接展示;排序参数保留扩展位但当前未使用。
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- search (GenTableQueryParam): 查询参数模型。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list[Any]: 包含数据库表列表信息的任意类型列表。
|
||||||
"""
|
"""
|
||||||
gen_db_table_list_result = await GenTableCRUD(auth=auth).get_db_table_list(search)
|
gen_db_table_list_result = await GenTableCRUD(auth=auth).get_db_table_list(search)
|
||||||
return gen_db_table_list_result
|
return gen_db_table_list_result
|
||||||
@@ -72,28 +82,34 @@ class GenTableService:
|
|||||||
@handle_service_exception
|
@handle_service_exception
|
||||||
async def get_gen_db_table_list_by_name_service(cls, auth: AuthSchema, table_names: list[str]) -> list[GenTableOutSchema]:
|
async def get_gen_db_table_list_by_name_service(cls, auth: AuthSchema, table_names: list[str]) -> list[GenTableOutSchema]:
|
||||||
"""根据表名称组获取数据库表信息。
|
"""根据表名称组获取数据库表信息。
|
||||||
- 校验:如有不存在的表名,抛出明确异常;返回统一的`GenTableOutSchema`列表。
|
|
||||||
"""
|
|
||||||
# 验证输入参数
|
|
||||||
if not table_names:
|
|
||||||
raise CustomException(msg="表名列表不能为空")
|
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- table_names (list[str]): 业务表名称列表。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list[GenTableOutSchema]: 包含业务表详细信息的模型列表。
|
||||||
|
"""
|
||||||
gen_db_table_list_result = await GenTableCRUD(auth).get_db_table_list_by_names(table_names)
|
gen_db_table_list_result = await GenTableCRUD(auth).get_db_table_list_by_names(table_names)
|
||||||
|
|
||||||
# 修复:将GenDBTableSchema对象转换为字典后再传递给GenTableOutSchema
|
# 修复:将GenDBTableSchema对象转换为字典后再传递给GenTableOutSchema
|
||||||
result = []
|
result = []
|
||||||
for gen_table in gen_db_table_list_result:
|
for gen_table in gen_db_table_list_result:
|
||||||
# 确保table_name不为None
|
result.append(GenTableOutSchema(**gen_table.model_dump()))
|
||||||
if gen_table.table_name is not None:
|
|
||||||
result.append(GenTableOutSchema(**gen_table.model_dump()))
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@handle_service_exception
|
@handle_service_exception
|
||||||
async def import_gen_table_service(cls, auth: AuthSchema, gen_table_list: list[GenTableOutSchema]) -> bool | None:
|
async def import_gen_table_service(cls, auth: AuthSchema, gen_table_list: list[GenTableOutSchema]) -> bool:
|
||||||
"""导入表结构到生成器(持久化并初始化列)。
|
"""导入表结构到生成器。
|
||||||
- 备注:避免重复导入;为每列调用`GenUtils.init_column_field`填充默认属性,保留语义一致性。
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- gen_table_list (list[GenTableOutSchema]): 包含业务表详细信息的模型列表。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- bool: 成功时返回True,失败时抛出异常。
|
||||||
"""
|
"""
|
||||||
# 检查是否有表需要导入
|
# 检查是否有表需要导入
|
||||||
if not gen_table_list:
|
if not gen_table_list:
|
||||||
@@ -138,25 +154,44 @@ class GenTableService:
|
|||||||
@handle_service_exception
|
@handle_service_exception
|
||||||
async def create_table_service(cls, auth: AuthSchema, sql: str) -> bool | None:
|
async def create_table_service(cls, auth: AuthSchema, sql: str) -> bool | None:
|
||||||
"""创建表结构并导入至代码生成模块。
|
"""创建表结构并导入至代码生成模块。
|
||||||
- 校验:使用`sqlglot`确保仅包含`CREATE TABLE`语句;失败抛出明确异常。
|
|
||||||
- 唯一性检查:在创建前检查该表是否已存在于数据库中。
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- sql (str): 包含`CREATE TABLE`语句的SQL字符串。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- bool | None: 成功时返回True,失败时抛出异常。
|
||||||
"""
|
"""
|
||||||
# 验证SQL非空
|
# 验证SQL非空
|
||||||
if not sql or not sql.strip():
|
if not sql or not sql.strip():
|
||||||
raise CustomException(msg='SQL语句不能为空')
|
raise CustomException(msg='SQL语句不能为空')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 解析SQL语句
|
# 解析SQL语句
|
||||||
sql_statements = sqlglot_parse(sql, dialect=settings.DATABASE_TYPE)
|
sql_statements = sqlglot.parse(sql, dialect=settings.DATABASE_TYPE)
|
||||||
if not sql_statements:
|
if not sql_statements:
|
||||||
raise CustomException(msg='无法解析SQL语句,请检查SQL语法')
|
raise CustomException(msg='无法解析SQL语句,请检查SQL语法')
|
||||||
|
|
||||||
# 校验sql语句是否为合法的建表语句
|
# 校验sql语句是否为合法的建表语句
|
||||||
if not cls.__is_valid_create_table(sql_statements):
|
validate_create = [isinstance(sql_statement, Create) for sql_statement in sql_statements]
|
||||||
|
validate_forbidden_keywords = [
|
||||||
|
isinstance(
|
||||||
|
sql_statement,
|
||||||
|
(Add, Alter, Delete, Drop, Insert, TruncateTable, Update),
|
||||||
|
)
|
||||||
|
for sql_statement in sql_statements
|
||||||
|
]
|
||||||
|
if not any(validate_create) or any(validate_forbidden_keywords):
|
||||||
raise CustomException(msg='sql语句不是合法的建表语句')
|
raise CustomException(msg='sql语句不是合法的建表语句')
|
||||||
|
|
||||||
# 获取要创建的表名
|
# 获取要创建的表名
|
||||||
table_names = cls.__get_table_names(sql_statements)
|
table_names = []
|
||||||
|
for sql_statement in sql_statements:
|
||||||
|
if isinstance(sql_statement, Create):
|
||||||
|
table = sql_statement.find(Table)
|
||||||
|
if table and table.name:
|
||||||
|
table_names.append(table.name)
|
||||||
|
table_names = list(set(table_names))
|
||||||
|
|
||||||
# 创建CRUD实例
|
# 创建CRUD实例
|
||||||
gen_table_crud = GenTableCRUD(auth=auth)
|
gen_table_crud = GenTableCRUD(auth=auth)
|
||||||
|
|
||||||
@@ -172,104 +207,30 @@ class GenTableService:
|
|||||||
raise CustomException(msg=f'表 {table_name} 已在代码生成模块中存在,请检查并修改表名后重试')
|
raise CustomException(msg=f'表 {table_name} 已在代码生成模块中存在,请检查并修改表名后重试')
|
||||||
|
|
||||||
# 表不存在,执行SQL语句创建表
|
# 表不存在,执行SQL语句创建表
|
||||||
result = await gen_table_crud.create_table_by_sql(sql_statements)
|
for sql_statement in sql_statements:
|
||||||
if not result:
|
if not isinstance(sql_statement, Create):
|
||||||
raise CustomException(msg=f'创建表 {table_names} 失败,请检查SQL语句')
|
continue
|
||||||
|
exc_sql = sql_statement.sql(dialect=settings.DATABASE_TYPE)
|
||||||
# 导入表结构到代码生成模块 - 简化逻辑,移除多余的None检查
|
log.info(f'执行SQL语句: {exc_sql}')
|
||||||
gen_table_list = await cls.get_gen_db_table_list_by_name_service(auth, table_names)
|
if not await gen_table_crud.execute_sql(exc_sql):
|
||||||
|
raise CustomException(msg=f'执行SQL语句 {exc_sql} 失败,请检查数据库')
|
||||||
import_result = await cls.import_gen_table_service(auth, gen_table_list)
|
return True
|
||||||
|
|
||||||
return import_result
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise CustomException(msg=f'创建表结构失败: {str(e)}')
|
raise CustomException(msg=f'创建表结构失败: {str(e)}')
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@handle_service_exception
|
|
||||||
async def execute_sql_service(cls, auth: AuthSchema, gen_table: GenTableOutSchema) -> bool:
|
|
||||||
"""
|
|
||||||
执行菜单 SQL(INSERT / DO 块)并写入 sys_menu。
|
|
||||||
- 仅处理菜单 SQL,不再混杂建表逻辑;
|
|
||||||
- 文件不存在时给出友好提示;
|
|
||||||
- 统一异常信息,日志与业务提示分离。
|
|
||||||
"""
|
|
||||||
sql_path = f'{BASE_DIR}/sql/menu/{gen_table.module_name}/{gen_table.business_name}.sql'
|
|
||||||
|
|
||||||
# 文件存在性前置检查,避免多余解析开销
|
|
||||||
if not os.path.isfile(sql_path):
|
|
||||||
raise CustomException(msg=f'菜单 SQL 文件不存在: {sql_path}')
|
|
||||||
|
|
||||||
sql = Path(sql_path).read_text(encoding='utf-8').strip()
|
|
||||||
if not sql:
|
|
||||||
raise CustomException(msg='菜单 SQL 文件内容为空')
|
|
||||||
|
|
||||||
# 仅做语法校验,不限制关键字;真正的语义安全由数据库权限控制
|
|
||||||
try:
|
|
||||||
statements = sqlglot_parse(sql, dialect=settings.DATABASE_TYPE)
|
|
||||||
if not statements:
|
|
||||||
raise CustomException(msg='菜单 SQL 语法解析失败,请检查文件内容')
|
|
||||||
except Exception as e:
|
|
||||||
log.error(f'菜单 SQL 解析异常: {e}')
|
|
||||||
raise CustomException(msg='菜单 SQL 语法错误,请检查文件内容')
|
|
||||||
|
|
||||||
# 执行 SQL
|
|
||||||
try:
|
|
||||||
await GenTableCRUD(auth).execute_sql(sql)
|
|
||||||
log.info(f'成功执行菜单 SQL: {sql_path}')
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
log.error(f'菜单 SQL 执行失败: {e}')
|
|
||||||
raise CustomException(msg='菜单 SQL 执行失败,请确认语句及数据库状态')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def __is_valid_create_table(cls, sql_statements: list[Expression | None]) -> bool:
|
|
||||||
"""
|
|
||||||
校验SQL语句是否为合法的建表语句。
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- sql_statements (list[Expression | None]): SQL的AST列表。
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- bool: 校验结果。
|
|
||||||
"""
|
|
||||||
validate_create = [isinstance(sql_statement, Create) for sql_statement in sql_statements]
|
|
||||||
validate_forbidden_keywords = [
|
|
||||||
isinstance(
|
|
||||||
sql_statement,
|
|
||||||
(Add, Alter, Delete, Drop, Insert, TruncateTable, Update),
|
|
||||||
)
|
|
||||||
for sql_statement in sql_statements
|
|
||||||
]
|
|
||||||
if not any(validate_create) or any(validate_forbidden_keywords):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def __get_table_names(cls, sql_statements: list[Expression | None]) -> list[str]:
|
|
||||||
"""
|
|
||||||
获取SQL语句中所有的建表表名。
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- sql_statements (list[Expression | None]): SQL的AST列表。
|
|
||||||
|
|
||||||
返回:
|
|
||||||
- list[str]: 建表表名列表。
|
|
||||||
"""
|
|
||||||
table_names = []
|
|
||||||
for sql_statement in sql_statements:
|
|
||||||
if isinstance(sql_statement, Create):
|
|
||||||
table = sql_statement.find(Table)
|
|
||||||
if table and table.name:
|
|
||||||
table_names.append(table.name)
|
|
||||||
return list(set(table_names))
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@handle_service_exception
|
@handle_service_exception
|
||||||
async def update_gen_table_service(cls, auth: AuthSchema, data: GenTableSchema, table_id: int) -> dict[str, Any]:
|
async def update_gen_table_service(cls, auth: AuthSchema, data: GenTableSchema, table_id: int) -> dict[str, Any]:
|
||||||
"""编辑业务表信息(含选项与字段)。
|
"""编辑业务表信息。
|
||||||
- 备注:将`params`序列化写入`options`以持久化;仅更新存在`id`的列,避免误创建。
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- data (GenTableSchema): 包含业务表详细信息的模型。
|
||||||
|
- table_id (int): 业务表ID。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict[str, Any]: 更新后的业务表信息。
|
||||||
"""
|
"""
|
||||||
# 处理params为None的情况
|
# 处理params为None的情况
|
||||||
gen_table_info = await cls.get_gen_table_by_id_service(auth, table_id)
|
gen_table_info = await cls.get_gen_table_by_id_service(auth, table_id)
|
||||||
@@ -294,7 +255,15 @@ class GenTableService:
|
|||||||
@classmethod
|
@classmethod
|
||||||
@handle_service_exception
|
@handle_service_exception
|
||||||
async def delete_gen_table_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
async def delete_gen_table_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||||
"""删除业务表信息(先删字段,再删表)。"""
|
"""删除业务表信息(先删字段,再删表)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- ids (list[int]): 业务表ID列表。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
# 验证ID列表非空
|
# 验证ID列表非空
|
||||||
if not ids:
|
if not ids:
|
||||||
raise CustomException(msg="ID列表不能为空")
|
raise CustomException(msg="ID列表不能为空")
|
||||||
@@ -311,7 +280,13 @@ class GenTableService:
|
|||||||
@handle_service_exception
|
@handle_service_exception
|
||||||
async def get_gen_table_by_id_service(cls, auth: AuthSchema, table_id: int) -> GenTableOutSchema:
|
async def get_gen_table_by_id_service(cls, auth: AuthSchema, table_id: int) -> GenTableOutSchema:
|
||||||
"""获取需要生成代码的业务表详细信息。
|
"""获取需要生成代码的业务表详细信息。
|
||||||
- 备注:去除SQLAlchemy内部状态;将`None`值转为适配前端的默认值;解析`options`补充选项。
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- table_id (int): 业务表ID。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- GenTableOutSchema: 业务表详细信息模型。
|
||||||
"""
|
"""
|
||||||
gen_table = await GenTableCRUD(auth=auth).get_gen_table_by_id(table_id)
|
gen_table = await GenTableCRUD(auth=auth).get_gen_table_by_id(table_id)
|
||||||
if not gen_table:
|
if not gen_table:
|
||||||
@@ -323,7 +298,14 @@ class GenTableService:
|
|||||||
@classmethod
|
@classmethod
|
||||||
@handle_service_exception
|
@handle_service_exception
|
||||||
async def get_gen_table_all_service(cls, auth: AuthSchema) -> list[GenTableOutSchema]:
|
async def get_gen_table_all_service(cls, auth: AuthSchema) -> list[GenTableOutSchema]:
|
||||||
"""获取所有业务表信息(列表)。"""
|
"""获取所有业务表信息(列表)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list[GenTableOutSchema]: 业务表详细信息模型列表。
|
||||||
|
"""
|
||||||
gen_table_all = await GenTableCRUD(auth=auth).get_gen_table_all() or []
|
gen_table_all = await GenTableCRUD(auth=auth).get_gen_table_all() or []
|
||||||
result = []
|
result = []
|
||||||
for gen_table in gen_table_all:
|
for gen_table in gen_table_all:
|
||||||
@@ -340,7 +322,13 @@ class GenTableService:
|
|||||||
async def preview_code_service(cls, auth: AuthSchema, table_id: int) -> dict[str, Any]:
|
async def preview_code_service(cls, auth: AuthSchema, table_id: int) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
预览代码(根据模板渲染内存结果)。
|
预览代码(根据模板渲染内存结果)。
|
||||||
- 备注:构建Jinja2上下文;根据模板类型与前端类型选择模板清单;返回文件名到内容映射。
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- table_id (int): 业务表ID。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict[str, Any]: 文件名到渲染内容的映射。
|
||||||
"""
|
"""
|
||||||
gen_table = GenTableOutSchema.model_validate(
|
gen_table = GenTableOutSchema.model_validate(
|
||||||
await GenTableCRUD(auth).get_gen_table_by_id(table_id)
|
await GenTableCRUD(auth).get_gen_table_by_id(table_id)
|
||||||
@@ -365,18 +353,181 @@ class GenTableService:
|
|||||||
async def generate_code_service(cls, auth: AuthSchema, table_name: str) -> bool:
|
async def generate_code_service(cls, auth: AuthSchema, table_name: str) -> bool:
|
||||||
"""生成代码至指定路径(安全写入+可跳过覆盖)。
|
"""生成代码至指定路径(安全写入+可跳过覆盖)。
|
||||||
- 安全:限制写入在项目根目录内;越界路径自动回退到项目根目录。
|
- 安全:限制写入在项目根目录内;越界路径自动回退到项目根目录。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- table_name (str): 业务表名。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- bool: 生成是否成功。
|
||||||
"""
|
"""
|
||||||
# 验证表名非空
|
# 验证表名非空
|
||||||
if not table_name or not table_name.strip():
|
if not table_name or not table_name.strip():
|
||||||
raise CustomException(msg='表名不能为空')
|
raise CustomException(msg='表名不能为空')
|
||||||
|
|
||||||
env = Jinja2TemplateUtil.get_env()
|
env = Jinja2TemplateUtil.get_env()
|
||||||
render_info = await cls.__get_gen_render_info(auth, table_name)
|
render_info = await cls.__get_gen_render_info(auth, table_name)
|
||||||
gen_table_schema = render_info[3]
|
gen_table_schema: GenTableOutSchema = render_info[3]
|
||||||
|
|
||||||
|
from app.api.v1.module_system.menu.crud import MenuCRUD
|
||||||
|
from app.api.v1.module_system.menu.schema import MenuCreateSchema
|
||||||
|
from app.utils.common_util import CamelCaseUtil
|
||||||
|
# 构建权限前缀
|
||||||
|
permission_prefix = f"{gen_table_schema.module_name}:{gen_table_schema.business_name}"
|
||||||
|
# 创建菜单 CRUD 实例
|
||||||
|
menu_crud = MenuCRUD(auth)
|
||||||
|
if not gen_table_schema.business_name:
|
||||||
|
raise CustomException(msg='业务名称不能为空')
|
||||||
|
if not gen_table_schema.function_name:
|
||||||
|
raise CustomException(msg='功能名称不能为空')
|
||||||
|
if not gen_table_schema.package_name:
|
||||||
|
raise CustomException(msg='包名不能为空')
|
||||||
|
# 1. 先检查并创建菜单(目录菜单、功能菜单、按钮权限)
|
||||||
|
# 检查是否需要创建目录菜单
|
||||||
|
if gen_table_schema.parent_menu_id:
|
||||||
|
# 如果传了上级菜单ID(菜单类型=1),则不创建目录菜单,直接使用该ID作为功能菜单的父ID
|
||||||
|
dir_menu_id = gen_table_schema.parent_menu_id
|
||||||
|
else:
|
||||||
|
# 如果没传上级菜单ID,则需要创建新的模块目录菜单(类型=1:目录)
|
||||||
|
existing_dir_menu = await menu_crud.get(name=gen_table_schema.business_name)
|
||||||
|
if existing_dir_menu:
|
||||||
|
dir_menu_id = existing_dir_menu.id
|
||||||
|
else:
|
||||||
|
dir_parent_menu = await menu_crud.create(
|
||||||
|
MenuCreateSchema(
|
||||||
|
name=gen_table_schema.package_name,
|
||||||
|
type=1,
|
||||||
|
order=9999,
|
||||||
|
permission=None,
|
||||||
|
icon="menu",
|
||||||
|
route_name=CamelCaseUtil.snake_to_camel(gen_table_schema.package_name),
|
||||||
|
route_path=f"/{gen_table_schema.package_name}",
|
||||||
|
component_path=None,
|
||||||
|
redirect=f"/{gen_table_schema.package_name}/{gen_table_schema.business_name}",
|
||||||
|
hidden=False,
|
||||||
|
keep_alive=True,
|
||||||
|
always_show=False,
|
||||||
|
title=gen_table_schema.package_name,
|
||||||
|
params=None,
|
||||||
|
affix=False,
|
||||||
|
parent_id=gen_table_schema.parent_menu_id, # 这里应该是None,因为上面已经判断过了
|
||||||
|
status="0",
|
||||||
|
description=f"{gen_table_schema.business_name}目录"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
dir_menu_id = dir_parent_menu.id
|
||||||
|
|
||||||
|
# 检查功能菜单是否已存在,如果存在则抛出错误
|
||||||
|
existing_func_menu = await menu_crud.get(name=gen_table_schema.function_name, type=2)
|
||||||
|
if existing_func_menu:
|
||||||
|
raise CustomException(msg=f"功能菜单名称 '{gen_table_schema.function_name}' 已存在,不能重复创建")
|
||||||
|
else:
|
||||||
|
# 创建功能菜单(类型=2:菜单)
|
||||||
|
parent_menu = await menu_crud.create(
|
||||||
|
MenuCreateSchema(
|
||||||
|
name=gen_table_schema.function_name,
|
||||||
|
type=2,
|
||||||
|
order=9999,
|
||||||
|
permission=f"{permission_prefix}:query",
|
||||||
|
icon="menu",
|
||||||
|
route_name=CamelCaseUtil.snake_to_camel(gen_table_schema.business_name),
|
||||||
|
route_path=f"/{gen_table_schema.package_name}/{gen_table_schema.business_name}",
|
||||||
|
component_path=f"{gen_table_schema.module_name}/{gen_table_schema.business_name}/index",
|
||||||
|
redirect=None,
|
||||||
|
hidden=False,
|
||||||
|
keep_alive=True,
|
||||||
|
always_show=False,
|
||||||
|
title=gen_table_schema.function_name,
|
||||||
|
params=None,
|
||||||
|
affix=False,
|
||||||
|
parent_id=dir_menu_id, # 使用目录菜单ID或用户指定的parent_menu_id作为父ID
|
||||||
|
status="0",
|
||||||
|
description=f"{gen_table_schema.function_name}功能菜单"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# 创建按钮权限(类型=3:按钮/权限)
|
||||||
|
buttons = [
|
||||||
|
{
|
||||||
|
"name": f"{gen_table_schema.function_name}查询",
|
||||||
|
"permission": f"{permission_prefix}:query",
|
||||||
|
"order": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": f"{gen_table_schema.function_name}详情",
|
||||||
|
"permission": f"{permission_prefix}:detail",
|
||||||
|
"order": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": f"{gen_table_schema.function_name}新增",
|
||||||
|
"permission": f"{permission_prefix}:create",
|
||||||
|
"order": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": f"{gen_table_schema.function_name}修改",
|
||||||
|
"permission": f"{permission_prefix}:update",
|
||||||
|
"order": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": f"{gen_table_schema.function_name}删除",
|
||||||
|
"permission": f"{permission_prefix}:delete",
|
||||||
|
"order": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": f"{gen_table_schema.function_name}批量状态修改",
|
||||||
|
"permission": f"{permission_prefix}:patch",
|
||||||
|
"order": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": f"{gen_table_schema.function_name}导出",
|
||||||
|
"permission": f"{permission_prefix}:export",
|
||||||
|
"order": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": f"{gen_table_schema.function_name}导入",
|
||||||
|
"permission": f"{permission_prefix}:import",
|
||||||
|
"order": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": f"{gen_table_schema.function_name}下载导入模板",
|
||||||
|
"permission": f"{permission_prefix}:download",
|
||||||
|
"order": 9
|
||||||
|
}
|
||||||
|
]
|
||||||
|
for button in buttons:
|
||||||
|
# 检查按钮权限是否已存在
|
||||||
|
await menu_crud.create(
|
||||||
|
MenuCreateSchema(
|
||||||
|
name=button["name"],
|
||||||
|
type=3,
|
||||||
|
order=button["order"],
|
||||||
|
permission=button["permission"],
|
||||||
|
icon=None,
|
||||||
|
route_name=None,
|
||||||
|
route_path=None,
|
||||||
|
component_path=None,
|
||||||
|
redirect=None,
|
||||||
|
hidden=False,
|
||||||
|
keep_alive=True,
|
||||||
|
always_show=False,
|
||||||
|
title=button["name"],
|
||||||
|
params=None,
|
||||||
|
affix=False,
|
||||||
|
parent_id=parent_menu.id,
|
||||||
|
status="0",
|
||||||
|
description=f"{gen_table_schema.function_name}功能按钮"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
log.info(f"成功创建按钮权限: {button['name']}")
|
||||||
|
log.info(f"成功创建{gen_table_schema.function_name}菜单及按钮权限")
|
||||||
|
|
||||||
|
# 2. 菜单创建成功后,再生成页面代码
|
||||||
for template in render_info[0]:
|
for template in render_info[0]:
|
||||||
try:
|
try:
|
||||||
render_content = await env.get_template(template).render_async(**render_info[2])
|
render_content = await env.get_template(template).render_async(**render_info[2])
|
||||||
gen_path = cls.__get_gen_path(gen_table_schema, template)
|
|
||||||
|
file_name = Jinja2TemplateUtil.get_file_name(template, gen_table_schema)
|
||||||
|
full_path = BASE_DIR.parent.joinpath(file_name)
|
||||||
|
gen_path = str(full_path)
|
||||||
|
|
||||||
if not gen_path:
|
if not gen_path:
|
||||||
raise CustomException(msg='【代码生成】生成路径为空')
|
raise CustomException(msg='【代码生成】生成路径为空')
|
||||||
|
|
||||||
@@ -385,10 +536,16 @@ class GenTableService:
|
|||||||
|
|
||||||
with open(gen_path, 'w', encoding='utf-8') as f:
|
with open(gen_path, 'w', encoding='utf-8') as f:
|
||||||
f.write(render_content)
|
f.write(render_content)
|
||||||
|
|
||||||
|
module_init_path = BASE_DIR.parent.joinpath(f'backend/app/api/v1/{gen_table_schema.module_name}/__init__.py')
|
||||||
|
if not module_init_path.exists():
|
||||||
|
# 创建module_name目录的__init__.py文件
|
||||||
|
os.makedirs(os.path.dirname(module_init_path), exist_ok=True)
|
||||||
|
with open(module_init_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write('# -*- coding: utf-8 -*-')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise CustomException(msg=f'渲染模板失败,表名:{gen_table_schema.table_name},详细错误信息:{str(e)}')
|
raise CustomException(msg=f'渲染模板失败,表名:{gen_table_schema.table_name},详细错误信息:{str(e)}')
|
||||||
|
|
||||||
await cls.execute_sql_service(auth, gen_table_schema)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -397,17 +554,22 @@ class GenTableService:
|
|||||||
"""
|
"""
|
||||||
批量生成代码并打包为ZIP。
|
批量生成代码并打包为ZIP。
|
||||||
- 备注:内存生成并压缩,兼容多模板类型;供下载使用。
|
- 备注:内存生成并压缩,兼容多模板类型;供下载使用。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- table_names (list[str]): 业务表名列表。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- bytes: 包含所有生成代码的ZIP文件内容。
|
||||||
"""
|
"""
|
||||||
# 验证表名列表非空
|
# 验证表名列表非空
|
||||||
if not table_names:
|
if not table_names:
|
||||||
raise CustomException(msg="表名列表不能为空")
|
raise CustomException(msg="表名列表不能为空")
|
||||||
|
|
||||||
zip_buffer = io.BytesIO()
|
zip_buffer = io.BytesIO()
|
||||||
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
||||||
for table_name in table_names:
|
for table_name in table_names:
|
||||||
if not table_name.strip():
|
if not table_name.strip():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
env = Jinja2TemplateUtil.get_env()
|
env = Jinja2TemplateUtil.get_env()
|
||||||
render_info = await cls.__get_gen_render_info(auth, table_name)
|
render_info = await cls.__get_gen_render_info(auth, table_name)
|
||||||
@@ -418,7 +580,6 @@ class GenTableService:
|
|||||||
log.error(f"批量生成代码时处理表 {table_name} 出错: {str(e)}")
|
log.error(f"批量生成代码时处理表 {table_name} 出错: {str(e)}")
|
||||||
# 继续处理其他表,不中断整个过程
|
# 继续处理其他表,不中断整个过程
|
||||||
continue
|
continue
|
||||||
|
|
||||||
zip_data = zip_buffer.getvalue()
|
zip_data = zip_buffer.getvalue()
|
||||||
zip_buffer.close()
|
zip_buffer.close()
|
||||||
return zip_data
|
return zip_data
|
||||||
@@ -426,13 +587,19 @@ class GenTableService:
|
|||||||
@classmethod
|
@classmethod
|
||||||
@handle_service_exception
|
@handle_service_exception
|
||||||
async def sync_db_service(cls, auth: AuthSchema, table_name: str) -> None:
|
async def sync_db_service(cls, auth: AuthSchema, table_name: str) -> None:
|
||||||
"""同步数据库表结构至生成器(保留用户配置)。
|
"""
|
||||||
- 备注:按数据库实际字段重建或更新生成器字段;保留字典/查询/展示等用户自定义属性;清理已删除字段。
|
同步数据库表结构到业务表。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- table_name (str): 业务表名。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
# 验证表名非空
|
# 验证表名非空
|
||||||
if not table_name or not table_name.strip():
|
if not table_name or not table_name.strip():
|
||||||
raise CustomException(msg='表名不能为空')
|
raise CustomException(msg='表名不能为空')
|
||||||
|
|
||||||
gen_table = await GenTableCRUD(auth).get_gen_table_by_name(table_name)
|
gen_table = await GenTableCRUD(auth).get_gen_table_by_name(table_name)
|
||||||
if not gen_table:
|
if not gen_table:
|
||||||
raise CustomException(msg='业务表不存在')
|
raise CustomException(msg='业务表不存在')
|
||||||
@@ -447,40 +614,27 @@ class GenTableService:
|
|||||||
db_table_column_names = [column.column_name for column in db_table_columns]
|
db_table_column_names = [column.column_name for column in db_table_columns]
|
||||||
try:
|
try:
|
||||||
for column in db_table_columns:
|
for column in db_table_columns:
|
||||||
# 仅在缺省时初始化默认属性(包含 table_id 关联)
|
|
||||||
GenUtils.init_column_field(column, table)
|
GenUtils.init_column_field(column, table)
|
||||||
# 利用schema层的默认值,移除多余的None检查
|
|
||||||
if column.column_name in table_column_map:
|
if column.column_name in table_column_map:
|
||||||
prev_column = table_column_map[column.column_name]
|
prev_column = table_column_map[column.column_name]
|
||||||
# 复用旧记录ID,确保执行更新
|
|
||||||
if hasattr(prev_column, 'id') and prev_column.id:
|
if hasattr(prev_column, 'id') and prev_column.id:
|
||||||
column.id = prev_column.id
|
column.id = prev_column.id
|
||||||
|
|
||||||
# 保留用户配置的显示与查询属性 - 使用getattr确保安全访问
|
|
||||||
if hasattr(prev_column, 'dict_type') and prev_column.dict_type:
|
if hasattr(prev_column, 'dict_type') and prev_column.dict_type:
|
||||||
column.dict_type = prev_column.dict_type
|
column.dict_type = prev_column.dict_type
|
||||||
if hasattr(prev_column, 'query_type') and prev_column.query_type:
|
if hasattr(prev_column, 'query_type') and prev_column.query_type:
|
||||||
column.query_type = prev_column.query_type
|
column.query_type = prev_column.query_type
|
||||||
if hasattr(prev_column, 'html_type') and prev_column.html_type:
|
if hasattr(prev_column, 'html_type') and prev_column.html_type:
|
||||||
column.html_type = prev_column.html_type
|
column.html_type = prev_column.html_type
|
||||||
|
|
||||||
# 保留关键用户自定义属性 - 安全处理is_pk
|
|
||||||
is_pk_bool = False
|
is_pk_bool = False
|
||||||
if hasattr(prev_column, 'is_pk'):
|
if hasattr(prev_column, 'is_pk'):
|
||||||
# 处理不同类型的is_pk值
|
|
||||||
if isinstance(prev_column.is_pk, bool):
|
if isinstance(prev_column.is_pk, bool):
|
||||||
is_pk_bool = prev_column.is_pk
|
is_pk_bool = prev_column.is_pk
|
||||||
else:
|
else:
|
||||||
is_pk_bool = str(prev_column.is_pk) == '1'
|
is_pk_bool = str(prev_column.is_pk) == '1'
|
||||||
|
|
||||||
# 安全处理nullable属性
|
|
||||||
if hasattr(prev_column, 'is_nullable') and not is_pk_bool:
|
if hasattr(prev_column, 'is_nullable') and not is_pk_bool:
|
||||||
column.is_nullable = prev_column.is_nullable
|
column.is_nullable = prev_column.is_nullable
|
||||||
|
|
||||||
# 保留其他重要用户设置
|
|
||||||
if hasattr(prev_column, 'python_field'):
|
if hasattr(prev_column, 'python_field'):
|
||||||
column.python_field = prev_column.python_field or column.python_field
|
column.python_field = prev_column.python_field or column.python_field
|
||||||
|
|
||||||
if hasattr(column, 'id') and column.id:
|
if hasattr(column, 'id') and column.id:
|
||||||
await GenTableColumnCRUD(auth).update_gen_table_column_crud(column.id, column)
|
await GenTableColumnCRUD(auth).update_gen_table_column_crud(column.id, column)
|
||||||
else:
|
else:
|
||||||
@@ -501,6 +655,12 @@ class GenTableService:
|
|||||||
async def set_pk_column(cls, gen_table: GenTableOutSchema) -> None:
|
async def set_pk_column(cls, gen_table: GenTableOutSchema) -> None:
|
||||||
"""设置主键列信息(主表/子表)。
|
"""设置主键列信息(主表/子表)。
|
||||||
- 备注:同时兼容`pk`布尔与`is_pk == '1'`字符串两种标识。
|
- 备注:同时兼容`pk`布尔与`is_pk == '1'`字符串两种标识。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- gen_table (GenTableOutSchema): 业务表详细信息模型。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
if gen_table.columns:
|
if gen_table.columns:
|
||||||
for column in gen_table.columns:
|
for column in gen_table.columns:
|
||||||
@@ -531,34 +691,30 @@ class GenTableService:
|
|||||||
# 检查表是否存在
|
# 检查表是否存在
|
||||||
if gen_table_model is None:
|
if gen_table_model is None:
|
||||||
raise CustomException(msg=f"业务表 {table_name} 不存在")
|
raise CustomException(msg=f"业务表 {table_name} 不存在")
|
||||||
|
|
||||||
gen_table = GenTableOutSchema.model_validate(gen_table_model)
|
gen_table = GenTableOutSchema.model_validate(gen_table_model)
|
||||||
await cls.set_pk_column(gen_table)
|
await cls.set_pk_column(gen_table)
|
||||||
context = Jinja2TemplateUtil.prepare_context(gen_table)
|
context = Jinja2TemplateUtil.prepare_context(gen_table)
|
||||||
template_list = Jinja2TemplateUtil.get_template_list()
|
template_list = Jinja2TemplateUtil.get_template_list()
|
||||||
output_files = [Jinja2TemplateUtil.get_file_name(template, gen_table) for template in template_list]
|
output_files = [Jinja2TemplateUtil.get_file_name(template, gen_table) for template in template_list]
|
||||||
|
|
||||||
return [template_list, output_files, context, gen_table]
|
return [template_list, output_files, context, gen_table]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __get_gen_path(cls, gen_table: GenTableOutSchema, template: str) -> str | None:
|
def __get_gen_path(cls, gen_table: GenTableOutSchema, template: str) -> str | None:
|
||||||
"""根据GenTableOutSchema对象和模板名称生成路径。"""
|
"""根据GenTableOutSchema对象和模板名称生成路径。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- gen_table (GenTableOutSchema): 业务表详细信息模型。
|
||||||
|
- template (str): 模板名称。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str | None: 生成的文件路径,若失败则返回None。
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
file_name = Jinja2TemplateUtil.get_file_name(template, gen_table)
|
file_name = Jinja2TemplateUtil.get_file_name(template, gen_table)
|
||||||
# 默认写入到项目根目录(backend的上一级)
|
full_path = BASE_DIR.parent.joinpath(file_name)
|
||||||
project_root = str(BASE_DIR.parent)
|
return str(full_path)
|
||||||
full_path = os.path.join(project_root, file_name)
|
|
||||||
|
|
||||||
# 确保路径在项目根目录内,防止路径遍历攻击
|
|
||||||
if not os.path.abspath(full_path).startswith(os.path.abspath(project_root)):
|
|
||||||
log.error(f"路径越界,回退到项目根目录: {file_name}")
|
|
||||||
# 回退到项目根目录下的generated文件夹
|
|
||||||
full_path = os.path.join(project_root, "generated", os.path.basename(file_name))
|
|
||||||
|
|
||||||
return full_path
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"生成路径时出错: {str(e)}")
|
raise CustomException(msg=f"生成路径时出错: {str(e)}")
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class GenTableColumnService:
|
class GenTableColumnService:
|
||||||
@@ -567,7 +723,15 @@ class GenTableColumnService:
|
|||||||
@classmethod
|
@classmethod
|
||||||
@handle_service_exception
|
@handle_service_exception
|
||||||
async def get_gen_table_column_list_by_table_id_service(cls, auth: AuthSchema, table_id: int) -> list[dict[str, Any]]:
|
async def get_gen_table_column_list_by_table_id_service(cls, auth: AuthSchema, table_id: int) -> list[dict[str, Any]]:
|
||||||
"""获取业务表字段列表信息(输出模型)。"""
|
"""获取业务表字段列表信息(输出模型)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- table_id (int): 业务表ID。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list[dict[str, Any]]: 业务表字段列表,每个元素为字段详细信息字典。
|
||||||
|
"""
|
||||||
gen_table_column_list_result = await GenTableColumnCRUD(auth).list_gen_table_column_crud({"table_id": table_id})
|
gen_table_column_list_result = await GenTableColumnCRUD(auth).list_gen_table_column_crud({"table_id": table_id})
|
||||||
result = [GenTableColumnOutSchema.model_validate(gen_table_column).model_dump() for gen_table_column in gen_table_column_list_result]
|
result = [GenTableColumnOutSchema.model_validate(gen_table_column).model_dump() for gen_table_column in gen_table_column_list_result]
|
||||||
return result
|
return result
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
-- 统一的菜单 SQL(兼容 MySQL / PostgreSQL),对齐到 sys_menu 表结构
|
|
||||||
{# 布尔值与保留字列名处理 #}
|
|
||||||
{% set b_true = 1 if db_type == 'mysql' else true %}
|
|
||||||
{% set b_false = 0 if db_type == 'mysql' else false %}
|
|
||||||
{% set order_col = '`order`' if db_type == 'mysql' else '"order"' %}
|
|
||||||
{% set sys_menu = '`sys_menu`' if db_type == 'mysql' else '"sys_menu"' %}
|
|
||||||
{% set icon = "menu" %}
|
|
||||||
{% set set_uuid = "UUID()" if db_type == 'mysql' else "gen_random_uuid()" %}
|
|
||||||
|
|
||||||
{% if db_type == 'mysql' %}
|
|
||||||
-- 父菜单(类型=2:菜单)
|
|
||||||
INSERT INTO {{ sys_menu }}
|
|
||||||
(`name`, `type`, {{ order_col }}, `permission`, `icon`, `route_name`, `route_path`, `component_path`, `redirect`, `hidden`, `keep_alive`, `always_show`, `title`, `params`, `affix`, `parent_id`, `uuid`, `status`, `description`, `created_time`, `updated_time`)
|
|
||||||
VALUES
|
|
||||||
('{{ function_name }}', 2, 9999, '{{ permission_prefix }}:query', '{{ icon }}', '{{ business_name|snake_to_camel }}', '/{{ module_name }}/{{ business_name }}', '{{ module_name }}/{{ business_name }}/index', NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}', NULL, {{ b_false }}, {{ parent_menu_id }}, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW());
|
|
||||||
-- 获取父菜单ID(MySQL)
|
|
||||||
SELECT @parentId := LAST_INSERT_ID();
|
|
||||||
|
|
||||||
-- 按钮权限(类型=3:按钮/权限)
|
|
||||||
INSERT INTO {{ sys_menu }}
|
|
||||||
(`name`, `type`, {{ order_col }}, `permission`, `icon`, `route_name`, `route_path`, `component_path`, `redirect`, `hidden`, `keep_alive`, `always_show`, `title`, `params`, `affix`, `parent_id`, `uuid`, `status`, `description`, `created_time`, `updated_time`)
|
|
||||||
VALUES
|
|
||||||
('{{ function_name }}查询', 3, 1, '{{ permission_prefix }}:query', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}查询', NULL, {{ b_false }}, @parentId, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW()),
|
|
||||||
('{{ function_name }}新增', 3, 2, '{{ permission_prefix }}:create', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}新增', NULL, {{ b_false }}, @parentId, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW()),
|
|
||||||
('{{ function_name }}修改', 3, 3, '{{ permission_prefix }}:update', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}修改', NULL, {{ b_false }}, @parentId, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW()),
|
|
||||||
('{{ function_name }}删除', 3, 4, '{{ permission_prefix }}:delete', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}删除', NULL, {{ b_false }}, @parentId, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW()),
|
|
||||||
('{{ function_name }}导出', 3, 5, '{{ permission_prefix }}:export', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}导出', NULL, {{ b_false }}, @parentId, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW()),
|
|
||||||
('{{ function_name }}导入', 3, 6, '{{ permission_prefix }}:import', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}导入', NULL, {{ b_false }}, @parentId, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW()),
|
|
||||||
('{{ function_name }}批量状态修改', 3, 7, '{{ permission_prefix }}:patch', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}批量状态修改', NULL, {{ b_false }}, @parentId, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW()),
|
|
||||||
('{{ function_name }}下载导入模板', 3, 8, '{{ permission_prefix }}:download', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}下载导入模板', NULL, {{ b_false }}, @parentId, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW());
|
|
||||||
|
|
||||||
{% elif db_type == 'postgres' %}
|
|
||||||
-- 菜单 SQL(PostgreSQL DO 块方案)
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
parent_id INTEGER;
|
|
||||||
BEGIN
|
|
||||||
-- 父菜单(类型=2:菜单)
|
|
||||||
INSERT INTO {{ sys_menu }}
|
|
||||||
(name, type, {{ order_col }}, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, uuid, status, description, created_time, updated_time )
|
|
||||||
VALUES
|
|
||||||
('{{ function_name }}', 2, 9999, '{{ permission_prefix }}:query', '{{ icon }}', '{{ business_name|snake_to_camel }}', '/{{ module_name }}/{{ business_name }}', '{{ module_name }}/{{ business_name }}/index', NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}', NULL, {{ b_false }}, {{ parent_menu_id }}, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW())
|
|
||||||
RETURNING id INTO parent_id;
|
|
||||||
|
|
||||||
-- 按钮权限(类型=3:按钮/权限)
|
|
||||||
INSERT INTO {{ sys_menu }}
|
|
||||||
(name, type, {{ order_col }}, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, uuid, status, description, created_time, updated_time )
|
|
||||||
VALUES
|
|
||||||
('{{ function_name }}查询', 3, 1, '{{ permission_prefix }}:query', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}查询', NULL, {{ b_false }}, parent_id, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW()),
|
|
||||||
('{{ function_name }}新增', 3, 2, '{{ permission_prefix }}:create', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}新增', NULL, {{ b_false }}, parent_id, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW()),
|
|
||||||
('{{ function_name }}修改', 3, 3, '{{ permission_prefix }}:update', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}修改', NULL, {{ b_false }}, parent_id, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW()),
|
|
||||||
('{{ function_name }}删除', 3, 4, '{{ permission_prefix }}:delete', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}删除', NULL, {{ b_false }}, parent_id, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW()),
|
|
||||||
('{{ function_name }}导出', 3, 5, '{{ permission_prefix }}:export', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}导出', NULL, {{ b_false }}, parent_id, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW()),
|
|
||||||
('{{ function_name }}导入', 3, 6, '{{ permission_prefix }}:import', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}导入', NULL, {{ b_false }}, parent_id, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW()),
|
|
||||||
('{{ function_name }}批量状态修改', 3, 7, '{{ permission_prefix }}:patch', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}批量状态修改', NULL, {{ b_false }}, parent_id, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW()),
|
|
||||||
('{{ function_name }}下载导入模板', 3, 8, '{{ permission_prefix }}:download', NULL, NULL, NULL, NULL, NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}下载导入模板', NULL, {{ b_false }}, parent_id, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW());
|
|
||||||
|
|
||||||
-- 可选:输出插入的父菜单ID(调试用)
|
|
||||||
RAISE NOTICE '{{ function_name }}菜单创建完成,父菜单ID: %', parent_id;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
{% else %}
|
|
||||||
生成菜单 SQL 语句错误:{{ db_type }} 数据库不支持,请使用 mysql 或 postgres 数据库。
|
|
||||||
{% endif %}
|
|
||||||
@@ -22,8 +22,6 @@ class Jinja2TemplateUtil:
|
|||||||
# 项目路径
|
# 项目路径
|
||||||
FRONTEND_PROJECT_PATH = 'frontend'
|
FRONTEND_PROJECT_PATH = 'frontend'
|
||||||
BACKEND_PROJECT_PATH = 'backend'
|
BACKEND_PROJECT_PATH = 'backend'
|
||||||
# 默认上级菜单,系统工具
|
|
||||||
DEFAULT_PARENT_MENU_ID = 7
|
|
||||||
|
|
||||||
# 环境对象
|
# 环境对象
|
||||||
_env = None
|
_env = None
|
||||||
@@ -116,7 +114,7 @@ class Jinja2TemplateUtil:
|
|||||||
'db_type': settings.DATABASE_TYPE,
|
'db_type': settings.DATABASE_TYPE,
|
||||||
'column_not_add_show': GenConstant.COLUMNNAME_NOT_ADD_SHOW,
|
'column_not_add_show': GenConstant.COLUMNNAME_NOT_ADD_SHOW,
|
||||||
'column_not_edit_show': GenConstant.COLUMNNAME_NOT_EDIT_SHOW,
|
'column_not_edit_show': GenConstant.COLUMNNAME_NOT_EDIT_SHOW,
|
||||||
'parent_menu_id': int(gen_table.parent_menu_id) if gen_table.parent_menu_id is not None else int(cls.DEFAULT_PARENT_MENU_ID),
|
'parent_menu_id': int(gen_table.parent_menu_id) if gen_table.parent_menu_id else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
return context
|
return context
|
||||||
@@ -138,7 +136,6 @@ class Jinja2TemplateUtil:
|
|||||||
'python/schema.py.j2',
|
'python/schema.py.j2',
|
||||||
'python/model.py.j2',
|
'python/model.py.j2',
|
||||||
'python/__init__.py.j2',
|
'python/__init__.py.j2',
|
||||||
'sql/sql.sql.j2',
|
|
||||||
'ts/api.ts.j2',
|
'ts/api.ts.j2',
|
||||||
'vue/index.vue.j2',
|
'vue/index.vue.j2',
|
||||||
]
|
]
|
||||||
@@ -175,7 +172,6 @@ class Jinja2TemplateUtil:
|
|||||||
'schema.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/schema.py',
|
'schema.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/schema.py',
|
||||||
'model.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/model.py',
|
'model.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/model.py',
|
||||||
'__init__.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/__init__.py',
|
'__init__.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/__init__.py',
|
||||||
'sql.sql.j2': f'{cls.BACKEND_PROJECT_PATH}/sql/menu/{module_name}/{business_name}.sql',
|
|
||||||
'api.ts.j2': f'{cls.FRONTEND_PROJECT_PATH}/src/api/{module_name}/{business_name}.ts',
|
'api.ts.j2': f'{cls.FRONTEND_PROJECT_PATH}/src/api/{module_name}/{business_name}.ts',
|
||||||
'index.vue.j2': f'{cls.FRONTEND_PROJECT_PATH}/src/views/{module_name}/{business_name}/index.vue'
|
'index.vue.j2': f'{cls.FRONTEND_PROJECT_PATH}/src/views/{module_name}/{business_name}/index.vue'
|
||||||
}
|
}
|
||||||
@@ -185,9 +181,8 @@ class Jinja2TemplateUtil:
|
|||||||
if key in template:
|
if key in template:
|
||||||
return path
|
return path
|
||||||
|
|
||||||
# 默认处理
|
# 遍历完所有映射都没找到匹配项,才抛出异常
|
||||||
template_name = template.split('/')[-1].replace('.j2', '')
|
raise ValueError(f"未找到模板 '{template}' 的路径映射")
|
||||||
return f'{cls.BACKEND_PROJECT_PATH}/generated/{template_name}'
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_package_prefix(cls, package_name: str) -> str:
|
def get_package_prefix(cls, package_name: str) -> str:
|
||||||
|
|||||||
@@ -127,22 +127,30 @@ class DeptService:
|
|||||||
- None
|
- None
|
||||||
|
|
||||||
异常:
|
异常:
|
||||||
- CustomException: 当删除对象为空或部门不存在时抛出。
|
- CustomException: 当删除对象为空时抛出。
|
||||||
"""
|
"""
|
||||||
if len(ids) < 1:
|
if len(ids) < 1:
|
||||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||||
|
|
||||||
|
# 获取所有部门列表,用于构建树形关系
|
||||||
|
all_depts = await DeptCRUD(auth).get_list_crud()
|
||||||
|
|
||||||
|
# 构建子部门ID映射
|
||||||
|
child_id_map = get_child_id_map(model_list=all_depts)
|
||||||
|
|
||||||
|
# 收集所有需要删除的部门ID,包括直接指定的ID和它们的所有子部门ID
|
||||||
|
delete_ids_set = set()
|
||||||
|
|
||||||
for id in ids:
|
for id in ids:
|
||||||
dept = await DeptCRUD(auth).get_by_id_crud(id=id)
|
# 递归获取该ID的所有子部门ID
|
||||||
if not dept:
|
all_descendants = get_child_recursion(id=id, id_map=child_id_map)
|
||||||
raise CustomException(msg='删除失败,该部门不存在')
|
delete_ids_set.update(all_descendants)
|
||||||
# 校验是否存在子级部门,存在则禁止删除
|
|
||||||
dept_list = await DeptCRUD(auth).get_list_crud()
|
# 将集合转换为列表
|
||||||
id_map = get_child_id_map(model_list=dept_list)
|
delete_ids = list(delete_ids_set)
|
||||||
for id in ids:
|
|
||||||
descendants = get_child_recursion(id=id, id_map=id_map)
|
# 执行批量删除操作
|
||||||
if len(descendants) > 1:
|
await DeptCRUD(auth).delete(ids=delete_ids)
|
||||||
raise CustomException(msg='删除失败,存在子级部门,请先删除子级部门')
|
|
||||||
await DeptCRUD(auth).delete(ids=ids)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def batch_set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
async def batch_set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||||
|
|||||||
@@ -44,15 +44,14 @@ class MenuCreateSchema(BaseModel):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# 路由名/路径规范
|
# 路由名/路径规范
|
||||||
import re
|
|
||||||
if "route_name" in values and isinstance(values["route_name"], str):
|
|
||||||
rn = values["route_name"]
|
|
||||||
if rn and not re.match(r"^[A-Za-z][A-Za-z0-9_.-]{1,99}$", rn):
|
|
||||||
raise ValueError("路由名称需字母开头,仅含字母/数字/_ . -")
|
|
||||||
if "route_path" in values and isinstance(values["route_path"], str):
|
if "route_path" in values and isinstance(values["route_path"], str):
|
||||||
rp = values["route_path"]
|
rp = values["route_path"]
|
||||||
if rp and not rp.startswith("/"):
|
if rp and not rp.startswith("/"):
|
||||||
raise ValueError("路由路径需以 / 开头")
|
raise ValueError("路由路径需以 / 开头")
|
||||||
|
if "component_path" in values and isinstance(values["component_path"], str):
|
||||||
|
cp = values["component_path"]
|
||||||
|
if cp and cp.startswith("/"):
|
||||||
|
raise ValueError("组件路径不能以 / 开头")
|
||||||
return values
|
return values
|
||||||
|
|
||||||
@model_validator(mode='after')
|
@model_validator(mode='after')
|
||||||
|
|||||||
@@ -133,18 +133,26 @@ class MenuService:
|
|||||||
"""
|
"""
|
||||||
if len(ids) < 1:
|
if len(ids) < 1:
|
||||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||||
|
|
||||||
|
# 获取所有菜单列表,用于构建树形关系
|
||||||
|
all_menus = await MenuCRUD(auth).get_list_crud()
|
||||||
|
|
||||||
|
# 构建子菜单ID映射
|
||||||
|
child_id_map = get_child_id_map(model_list=all_menus)
|
||||||
|
|
||||||
|
# 收集所有需要删除的菜单ID,包括直接指定的ID和它们的所有子菜单ID
|
||||||
|
delete_ids_set = set()
|
||||||
|
|
||||||
for id in ids:
|
for id in ids:
|
||||||
menu = await MenuCRUD(auth).get_by_id_crud(id=id)
|
# 递归获取该ID的所有子菜单ID
|
||||||
if not menu:
|
all_descendants = get_child_recursion(id=id, id_map=child_id_map)
|
||||||
raise CustomException(msg='删除失败,该菜单不存在')
|
delete_ids_set.update(all_descendants)
|
||||||
# 校验是否存在子级菜单,存在则禁止删除
|
|
||||||
menu_list = await MenuCRUD(auth).get_list_crud()
|
# 将集合转换为列表
|
||||||
id_map = get_child_id_map(model_list=menu_list)
|
delete_ids = list(delete_ids_set)
|
||||||
for id in ids:
|
|
||||||
descendants = get_child_recursion(id=id, id_map=id_map)
|
# 执行批量删除操作
|
||||||
if len(descendants) > 1:
|
await MenuCRUD(auth).delete(ids=delete_ids)
|
||||||
raise CustomException(msg='删除失败,存在子级菜单,请先删除子级菜单')
|
|
||||||
await MenuCRUD(auth).delete(ids=ids)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def set_menu_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
async def set_menu_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class RedisInitKeyConfig(Enum):
|
|||||||
CAPTCHA_CODES = {'key': 'captcha_codes', 'remark': '图片验证码'}
|
CAPTCHA_CODES = {'key': 'captcha_codes', 'remark': '图片验证码'}
|
||||||
SYSTEM_CONFIG = {'key': 'system_config', 'remark': '系统配置'}
|
SYSTEM_CONFIG = {'key': 'system_config', 'remark': '系统配置'}
|
||||||
SYSTEM_DICT = {'key':'system_dict','remark': '数据字典'}
|
SYSTEM_DICT = {'key':'system_dict','remark': '数据字典'}
|
||||||
|
APSCHEDULER_LOCK_KEY = {'key': 'scheduler_job_lock', 'remark': '定时任务初始化锁'}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def key(self) -> str:
|
def key(self) -> str:
|
||||||
|
|||||||
@@ -35,23 +35,24 @@ class Settings(BaseSettings):
|
|||||||
# ******************* API文档配置 ****************** #
|
# ******************* API文档配置 ****************** #
|
||||||
# ================================================= #
|
# ================================================= #
|
||||||
DEBUG: bool = True # 调试模式
|
DEBUG: bool = True # 调试模式
|
||||||
TITLE: str = "🎉 FastapiAdmin 🎉 -dev" # 文档标题
|
TITLE: str = "🎉 FastapiAdmin 🎉 " # 文档标题
|
||||||
VERSION: str = '0.1.0' # 版本号
|
VERSION: str = '0.1.0' # 版本号
|
||||||
DESCRIPTION: str = "该项目是一个基于python的web服务框架,基于fastapi和sqlalchemy实现。" # 文档描述
|
DESCRIPTION: str = "该项目是一个基于python的web服务框架,基于fastapi和sqlalchemy实现。" # 文档描述
|
||||||
SUMMARY: str = "接口汇总" # 文档概述
|
SUMMARY: str = "接口汇总" # 文档概述
|
||||||
DOCS_URL: str = "/docs" # Swagger UI路径
|
DOCS_URL: str = "/docs" # Swagger UI路径
|
||||||
REDOC_URL: str = "/redoc" # ReDoc路径
|
REDOC_URL: str = "/redoc" # ReDoc路径
|
||||||
ROOT_PATH: str = "/api/v1" # API路由前缀
|
ROOT_PATH: str = "/api/v1" # API路由前缀
|
||||||
|
|
||||||
|
# ================================================= #
|
||||||
|
# ******************** 日志配置 ******************** #
|
||||||
|
# ================================================= #
|
||||||
|
LOGGER_LEVEL: str = 'DEBUG' # 日志级别
|
||||||
|
|
||||||
# ================================================= #
|
# ================================================= #
|
||||||
# ******************** 跨域配置 ******************** #
|
# ******************** 跨域配置 ******************** #
|
||||||
# ================================================= #
|
# ================================================= #
|
||||||
CORS_ORIGIN_ENABLE: bool = True # 是否启用跨域
|
CORS_ORIGIN_ENABLE: bool = True # 是否启用跨域
|
||||||
# ALLOW_ORIGINS: List[str] = ["*"] # 允许的域名列表
|
ALLOW_ORIGINS: List[str] = ["*"] # 允许的域名列表
|
||||||
ALLOW_ORIGINS: List[str] = [
|
|
||||||
'http://localhost:8001',
|
|
||||||
'http://localhost:5180',
|
|
||||||
] # 允许的域名列表
|
|
||||||
ALLOW_METHODS: List[str] = ["*"] # 允许的HTTP方法
|
ALLOW_METHODS: List[str] = ["*"] # 允许的HTTP方法
|
||||||
ALLOW_HEADERS: List[str] = ["*"] # 允许的请求头
|
ALLOW_HEADERS: List[str] = ["*"] # 允许的请求头
|
||||||
ALLOW_CREDENTIALS: bool = True # 是否允许携带cookie
|
ALLOW_CREDENTIALS: bool = True # 是否允许携带cookie
|
||||||
@@ -65,9 +66,7 @@ class Settings(BaseSettings):
|
|||||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 60 * 24 * 1 # access_token过期时间(秒)1 天
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 60 * 24 * 1 # access_token过期时间(秒)1 天
|
||||||
REFRESH_TOKEN_EXPIRE_MINUTES: int = 60 * 60 * 24 * 7 # refresh_token过期时间(秒)7 天
|
REFRESH_TOKEN_EXPIRE_MINUTES: int = 60 * 60 * 24 * 7 # refresh_token过期时间(秒)7 天
|
||||||
TOKEN_TYPE: str = "bearer" # token类型
|
TOKEN_TYPE: str = "bearer" # token类型
|
||||||
TOKEN_REQUEST_PATH_EXCLUDE: list[str] = [ # JWT / RBAC 路由白名单
|
TOKEN_REQUEST_PATH_EXCLUDE: list[str] = ['api/v1/auth/login'] # JWT / RBAC 路由白名单
|
||||||
'api/v1/auth/login',
|
|
||||||
]
|
|
||||||
|
|
||||||
# ================================================= #
|
# ================================================= #
|
||||||
# ******************** 数据库配置 ******************* #
|
# ******************** 数据库配置 ******************* #
|
||||||
@@ -86,11 +85,8 @@ class Settings(BaseSettings):
|
|||||||
AUTOFETCH: bool = False # 是否自动刷新
|
AUTOFETCH: bool = False # 是否自动刷新
|
||||||
EXPIRE_ON_COMMIT: bool = False # 是否在提交时过期
|
EXPIRE_ON_COMMIT: bool = False # 是否在提交时过期
|
||||||
|
|
||||||
# 数据库类型
|
|
||||||
DATABASE_TYPE: Literal['mysql', 'postgres', 'sqlite', 'dm'] = 'mysql'
|
|
||||||
|
|
||||||
|
|
||||||
# MySQL/PostgreSQL数据库连接
|
# MySQL/PostgreSQL数据库连接
|
||||||
|
DATABASE_TYPE: Literal['mysql', 'postgres', 'sqlite', 'dm'] = 'mysql'
|
||||||
DATABASE_HOST: str = 'localhost'
|
DATABASE_HOST: str = 'localhost'
|
||||||
DATABASE_PORT: int = 3306
|
DATABASE_PORT: int = 3306
|
||||||
DATABASE_USER: str = 'root'
|
DATABASE_USER: str = 'root'
|
||||||
|
|||||||
@@ -119,20 +119,19 @@ class DiscoverRouter:
|
|||||||
"""
|
"""
|
||||||
base_pkg = importlib.import_module(self.base_package)
|
base_pkg = importlib.import_module(self.base_package)
|
||||||
base_dir = Path(next(iter(base_pkg.__path__)))
|
base_dir = Path(next(iter(base_pkg.__path__)))
|
||||||
log.info(f"📁 基础包路径: {base_dir}, 包名: {base_pkg.__name__}")
|
log.debug(f"📁 基础包名: {base_pkg.__name__}")
|
||||||
return base_dir, base_pkg.__name__
|
return base_dir, base_pkg.__name__
|
||||||
|
|
||||||
def _iter_controller_files(self, base_dir: Path) -> Iterable[Path]:
|
def _iter_controller_files(self, base_dir: Path) -> Iterable[Path]:
|
||||||
"""递归查找并返回所有 `controller.py` 文件,按路径排序保证确定性。"""
|
"""递归查找并返回所有 `controller.py` 文件,按路径排序保证确定性。"""
|
||||||
try:
|
try:
|
||||||
files = sorted(base_dir.rglob("controller.py"), key=lambda p: p.as_posix())
|
files = sorted(base_dir.rglob("controller.py"), key=lambda p: p.as_posix())
|
||||||
log.info(f"🔍 发现 {len(files)} 个控制器文件")
|
|
||||||
return files
|
return files
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
log.error(f"❌️ 权限错误: 无法访问目录 {base_dir}: {str(e)}")
|
log.error(f"❌️ 权限错误: 无法访问目录 {base_dir}: {str(e)}")
|
||||||
return []
|
return []
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"❌️ 查找控制器文件失败: {str(e)}")
|
log.error(f"❌️ 查找 controller.py 文件失败: {str(e)}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _resolve_prefix(self, top_module: str) -> str | None:
|
def _resolve_prefix(self, top_module: str) -> str | None:
|
||||||
@@ -143,12 +142,12 @@ class DiscoverRouter:
|
|||||||
return None
|
return None
|
||||||
if not top_module.startswith(self.module_prefix):
|
if not top_module.startswith(self.module_prefix):
|
||||||
if self.debug:
|
if self.debug:
|
||||||
log.warning(f"⚠️ 目录 {top_module} 不符合前缀约定 {self.module_prefix}")
|
log.warning(f"⚠️ 目录 {top_module} 不符合前缀约定(必须以 {self.module_prefix} 开头)")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
mapped = self.prefix_map.get(top_module)
|
mapped = self.prefix_map.get(top_module)
|
||||||
if mapped:
|
if mapped:
|
||||||
log.info(f"🔄 模块 {top_module} 映射到前缀 {mapped}")
|
log.debug(f"🔄 模块 {top_module} 映射到前缀 {mapped}")
|
||||||
return mapped
|
return mapped
|
||||||
|
|
||||||
prefix = f"/{top_module[len(self.module_prefix):]}"
|
prefix = f"/{top_module[len(self.module_prefix):]}"
|
||||||
@@ -181,10 +180,9 @@ class DiscoverRouter:
|
|||||||
self._seen_router_ids.add(rid)
|
self._seen_router_ids.add(rid)
|
||||||
container.include_router(attr)
|
container.include_router(attr)
|
||||||
added += 1
|
added += 1
|
||||||
log.info(f"➕ 注册路由 {attr_name} 到容器")
|
|
||||||
|
|
||||||
if router_count == 0:
|
if router_count == 0:
|
||||||
log.warning(f"⚠️ 模块 {mod_name} 中未发现 APIRouter 实例")
|
log.warning(f"⚠️ 模块 {mod_name} 中未发现接口路由,跳过注册")
|
||||||
|
|
||||||
return added
|
return added
|
||||||
|
|
||||||
@@ -200,7 +198,7 @@ class DiscoverRouter:
|
|||||||
- included_routers: 注册的路由数量
|
- included_routers: 注册的路由数量
|
||||||
- container_count: 容器数量
|
- container_count: 容器数量
|
||||||
"""
|
"""
|
||||||
log.info("🚀 开始路由发现与注册...")
|
log.debug("🚀 开始路由发现与注册...")
|
||||||
base_dir, base_pkg = self._get_base_dir_and_pkg()
|
base_dir, base_pkg = self._get_base_dir_and_pkg()
|
||||||
containers: dict[str, APIRouter] = {}
|
containers: dict[str, APIRouter] = {}
|
||||||
container_counts: dict[str, int] = {}
|
container_counts: dict[str, int] = {}
|
||||||
@@ -215,12 +213,12 @@ class DiscoverRouter:
|
|||||||
scanned_files += 1
|
scanned_files += 1
|
||||||
|
|
||||||
if rel_path in self.exclude_files:
|
if rel_path in self.exclude_files:
|
||||||
log.warning(f"⚠️ 文件 {rel_path} 被排除")
|
log.warning(f"⚠️ 排除文件: {rel_path}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
parts = file.relative_to(base_dir).parts
|
parts = file.relative_to(base_dir).parts
|
||||||
if len(parts) < 2:
|
if len(parts) < 2:
|
||||||
log.warning(f"⚠️ 文件路径不完整: {rel_path},跳过")
|
log.warning(f"⚠️ 跳过不完整路径: {rel_path}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
top_module = parts[0]
|
top_module = parts[0]
|
||||||
@@ -233,12 +231,12 @@ class DiscoverRouter:
|
|||||||
try:
|
try:
|
||||||
mod = importlib.import_module(mod_path)
|
mod = importlib.import_module(mod_path)
|
||||||
imported_modules += 1
|
imported_modules += 1
|
||||||
log.info(f"📥 导入模块: {mod_path}")
|
log.debug(f"📥 导入分系统模块: {mod_path}")
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
log.error(f"❌️ 未找到控制器模块: {mod_path}")
|
log.error(f"❌️ 未找到模块: {mod_path}")
|
||||||
continue
|
continue
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
log.error(f"❌️ 导入控制器失败: {mod_path} -> {str(e)}")
|
log.error(f"❌️ 导入模块失败: {mod_path} -> {str(e)}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
container = containers.setdefault(prefix, APIRouter(prefix=prefix))
|
container = containers.setdefault(prefix, APIRouter(prefix=prefix))
|
||||||
@@ -247,7 +245,7 @@ class DiscoverRouter:
|
|||||||
included_routers += added
|
included_routers += added
|
||||||
container_counts[prefix] = container_counts.get(prefix, 0) + added
|
container_counts[prefix] = container_counts.get(prefix, 0) + added
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"❌️ 注册控制器路由失败: {mod_path} -> {str(e)}")
|
log.error(f"❌️ 注册路由失败: {mod_path} -> {str(e)}")
|
||||||
|
|
||||||
# 将容器路由按前缀名称排序后注册到根路由,保证顺序稳定
|
# 将容器路由按前缀名称排序后注册到根路由,保证顺序稳定
|
||||||
for prefix in sorted(containers.keys()):
|
for prefix in sorted(containers.keys()):
|
||||||
@@ -259,7 +257,7 @@ class DiscoverRouter:
|
|||||||
self._router.include_router(container)
|
self._router.include_router(container)
|
||||||
# 更丰富的注册日志(含路由数量)
|
# 更丰富的注册日志(含路由数量)
|
||||||
count = container_counts.get(prefix, 0)
|
count = container_counts.get(prefix, 0)
|
||||||
log.info(f"✅️ 已注册模块容器: {prefix} (路由数: {count})")
|
log.debug(f"✅️ 注册分系统: {prefix} (路由数: {count})")
|
||||||
|
|
||||||
# 更新统计信息
|
# 更新统计信息
|
||||||
stats = {
|
stats = {
|
||||||
@@ -271,12 +269,11 @@ class DiscoverRouter:
|
|||||||
self._discovery_stats = stats
|
self._discovery_stats = stats
|
||||||
|
|
||||||
# 生成总结日志
|
# 生成总结日志
|
||||||
log.info(
|
log.debug(
|
||||||
(
|
f"✅️ 路由发现完成: 扫描 {scanned_files} 个文件, "
|
||||||
f"✅️ 路由发现完成: 扫描文件 {scanned_files}, "
|
f"导入 {imported_modules} 个模块, "
|
||||||
f"导入模块 {imported_modules}, 注册路由 {included_routers}, "
|
f"注册 {included_routers} 个路由, "
|
||||||
f"容器 {len(containers)}"
|
f"创建 {len(containers)} 个路由"
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
@@ -296,7 +293,7 @@ class DiscoverRouter:
|
|||||||
"""
|
"""
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
log_level = "DEBUG" if debug else "INFO"
|
log_level = "DEBUG" if debug else "INFO"
|
||||||
log.info(f"⚙️ 调试模式已{'开启' if debug else '关闭'},日志级别: {log_level}")
|
log.debug(f"⚙️ 调试模式已{'开启' if debug else '关闭'},日志级别: {log_level}")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def add_exclude_dir(self, dir_name: str) -> 'DiscoverRouter':
|
def add_exclude_dir(self, dir_name: str) -> 'DiscoverRouter':
|
||||||
@@ -309,7 +306,7 @@ class DiscoverRouter:
|
|||||||
- self: 支持链式调用
|
- self: 支持链式调用
|
||||||
"""
|
"""
|
||||||
self.exclude_dirs.add(dir_name)
|
self.exclude_dirs.add(dir_name)
|
||||||
log.info(f"📝 添加排除目录: {dir_name}")
|
log.debug(f"📝 添加排除目录: {dir_name}")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def add_prefix_map(self, module_name: str, prefix: str) -> 'DiscoverRouter':
|
def add_prefix_map(self, module_name: str, prefix: str) -> 'DiscoverRouter':
|
||||||
@@ -323,7 +320,7 @@ class DiscoverRouter:
|
|||||||
- self: 支持链式调用
|
- self: 支持链式调用
|
||||||
"""
|
"""
|
||||||
self.prefix_map[module_name] = prefix
|
self.prefix_map[module_name] = prefix
|
||||||
log.info(f"📝 添加前缀映射: {module_name} -> {prefix}")
|
log.debug(f"📝 添加前缀映射: {module_name} -> {prefix}")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@_log_error_handling
|
@_log_error_handling
|
||||||
@@ -338,7 +335,7 @@ class DiscoverRouter:
|
|||||||
if rid not in self._seen_router_ids:
|
if rid not in self._seen_router_ids:
|
||||||
self._seen_router_ids.add(rid)
|
self._seen_router_ids.add(rid)
|
||||||
self._router.include_router(router, tags=tags)
|
self._router.include_router(router, tags=tags)
|
||||||
log.info(f"📌 手动注册路由,标签: {tags}")
|
log.debug(f"📌 手动注册路由,标签: {tags or '无'}")
|
||||||
else:
|
else:
|
||||||
log.warning(f"⚠️ 路由已存在,跳过重复注册")
|
log.warning(f"⚠️ 路由已存在,跳过重复注册")
|
||||||
|
|
||||||
@@ -351,6 +348,3 @@ router = _discoverer.router
|
|||||||
|
|
||||||
# 导出 DiscoverRouter 类供外部使用
|
# 导出 DiscoverRouter 类供外部使用
|
||||||
__all__ = ["DiscoverRouter", "router"]
|
__all__ = ["DiscoverRouter", "router"]
|
||||||
|
|
||||||
|
|
||||||
# 执行自动发现注册(已由 DiscoverRouter 实例内部处理)
|
|
||||||
@@ -67,7 +67,6 @@ def setup_logging():
|
|||||||
1. 控制台彩色输出
|
1. 控制台彩色输出
|
||||||
2. 文件日志轮转
|
2. 文件日志轮转
|
||||||
3. 错误日志单独存储
|
3. 错误日志单独存储
|
||||||
4. 智能异步策略:开发环境同步(避免reload资源泄漏),生产环境异步(高性能)
|
|
||||||
"""
|
"""
|
||||||
global _logger_handlers
|
global _logger_handlers
|
||||||
|
|
||||||
@@ -88,18 +87,11 @@ def setup_logging():
|
|||||||
"<level>{message}</level>"
|
"<level>{message}</level>"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 智能选择异步策略:开发环境禁用异步(避免reload时资源泄漏),生产环境启用异步(提升性能)
|
|
||||||
use_async = not settings.DEBUG
|
|
||||||
|
|
||||||
# 步骤3:配置控制台输出
|
# 步骤3:配置控制台输出
|
||||||
handler_id = logger.add(
|
handler_id = logger.add(
|
||||||
sys.stdout,
|
sys.stdout,
|
||||||
format=log_format,
|
format=log_format,
|
||||||
level="DEBUG" if settings.DEBUG else "INFO",
|
level=settings.LOGGER_LEVEL
|
||||||
enqueue=use_async, # 开发同步,生产异步
|
|
||||||
backtrace=True, # 显示完整的异常回溯
|
|
||||||
diagnose=True, # 显示变量值等诊断信息
|
|
||||||
colorize=True # 启用彩色输出
|
|
||||||
)
|
)
|
||||||
_logger_handlers.append(handler_id)
|
_logger_handlers.append(handler_id)
|
||||||
|
|
||||||
@@ -117,7 +109,6 @@ def setup_logging():
|
|||||||
retention=30, # 日志保留天数,超过此天数的日志文件将被自动清理
|
retention=30, # 日志保留天数,超过此天数的日志文件将被自动清理
|
||||||
compression="gz",
|
compression="gz",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
enqueue=use_async # 开发同步,生产异步
|
|
||||||
)
|
)
|
||||||
_logger_handlers.append(handler_id)
|
_logger_handlers.append(handler_id)
|
||||||
|
|
||||||
@@ -130,14 +121,13 @@ def setup_logging():
|
|||||||
retention=30, # 日志保留天数,超过此天数的日志文件将被自动清理
|
retention=30, # 日志保留天数,超过此天数的日志文件将被自动清理
|
||||||
compression="gz",
|
compression="gz",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
enqueue=use_async, # 开发同步,生产异步
|
|
||||||
backtrace=True,
|
backtrace=True,
|
||||||
diagnose=True
|
diagnose=True
|
||||||
)
|
)
|
||||||
_logger_handlers.append(handler_id)
|
_logger_handlers.append(handler_id)
|
||||||
|
|
||||||
# 步骤7:配置标准库日志
|
# 步骤7:配置标准库日志
|
||||||
logging.basicConfig(handlers=[InterceptHandler()], level="DEBUG" if settings.DEBUG else "INFO", force=True)
|
logging.basicConfig(handlers=[InterceptHandler()], level=settings.LOGGER_LEVEL, force=True)
|
||||||
logger_name_list = [name for name in logging.root.manager.loggerDict]
|
logger_name_list = [name for name in logging.root.manager.loggerDict]
|
||||||
|
|
||||||
# 步骤8:配置第三方库日志
|
# 步骤8:配置第三方库日志
|
||||||
|
|||||||
@@ -101,6 +101,74 @@ class RedisCURD:
|
|||||||
log.error(f"设置缓存失败: {str(e)}")
|
log.error(f"设置缓存失败: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def lock(self, key: str, expire: int, value: str | None = None) -> tuple[bool, str]:
|
||||||
|
"""获取分布式锁
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- key (str): 锁键名
|
||||||
|
- expire (int): 锁过期时间,单位为秒
|
||||||
|
- value (str, optional): 锁值,默认值为None(自动生成UUID)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- tuple[bool, str]: (获取锁是否成功, 锁值)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import uuid
|
||||||
|
# 如果没有提供value,生成唯一的UUID
|
||||||
|
lock_value = value if value else str(uuid.uuid4())
|
||||||
|
# 使用setnx命令实现原子性锁获取
|
||||||
|
result = await self.redis.set(
|
||||||
|
name=key,
|
||||||
|
value=lock_value,
|
||||||
|
ex=expire,
|
||||||
|
nx=True # 只有当键不存在时才设置
|
||||||
|
)
|
||||||
|
return (result is not None, lock_value)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"获取分布式锁失败: {str(e)}")
|
||||||
|
return (False, "")
|
||||||
|
|
||||||
|
async def unlock(self, key: str, value: str) -> bool:
|
||||||
|
"""释放分布式锁(安全版本,验证锁值)
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- key (str): 锁键名
|
||||||
|
- value (str): 锁值,用于验证锁的持有者
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- bool: 如果释放锁成功则返回True,否则返回False
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 使用Lua脚本确保原子性验证和删除
|
||||||
|
script = """
|
||||||
|
if redis.call('get', KEYS[1]) == ARGV[1] then
|
||||||
|
return redis.call('del', KEYS[1])
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
"""
|
||||||
|
result = await self.redis.eval(script, 1, key, value) # pyright: ignore[reportGeneralTypeIssues]
|
||||||
|
return result == 1
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"释放分布式锁失败: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def unlock_simple(self, key: str) -> bool:
|
||||||
|
"""释放分布式锁(简单版本,不验证锁值)
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- key (str): 锁键名
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- bool: 如果释放锁成功则返回True,否则返回False
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await self.redis.delete(key)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"释放分布式锁失败: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
async def delete(self, *keys: str) -> bool:
|
async def delete(self, *keys: str) -> bool:
|
||||||
"""删除缓存
|
"""删除缓存
|
||||||
|
|
||||||
@@ -165,6 +233,32 @@ class RedisCURD:
|
|||||||
log.error(f"获取缓存过期时间失败: {str(e)}")
|
log.error(f"获取缓存过期时间失败: {str(e)}")
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
|
async def renew_lock(self, key: str, expire: int, value: str) -> bool:
|
||||||
|
"""续约分布式锁
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- key (str): 锁键名
|
||||||
|
- expire (int): 新的过期时间,单位为秒
|
||||||
|
- value (str): 锁值,用于验证锁的持有者
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- bool: 如果续约锁成功则返回True,否则返回False
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 使用Lua脚本确保原子性验证和续约
|
||||||
|
script = """
|
||||||
|
if redis.call('get', KEYS[1]) == ARGV[1] then
|
||||||
|
return redis.call('expire', KEYS[1], ARGV[2])
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
"""
|
||||||
|
result = await self.redis.eval(script, 1, key, value, str(expire)) # pyright: ignore[reportGeneralTypeIssues]
|
||||||
|
return result == 1
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"续约分布式锁失败: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
async def expire(self, key: str, expire: int) -> bool:
|
async def expire(self, key: str, expire: int) -> bool:
|
||||||
"""设置缓存过期时间
|
"""设置缓存过期时间
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime, date, time
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from pydantic import AfterValidator, PlainSerializer, WithJsonSchema
|
from pydantic import AfterValidator, PlainSerializer, WithJsonSchema
|
||||||
|
|
||||||
@@ -17,6 +17,20 @@ DateTimeStr = Annotated[
|
|||||||
WithJsonSchema({'type': 'string'}, mode='serialization')
|
WithJsonSchema({'type': 'string'}, mode='serialization')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# 自定义日期字符串类型
|
||||||
|
DateStr = Annotated[
|
||||||
|
date,
|
||||||
|
PlainSerializer(lambda x: x.strftime('%Y-%m-%d') if isinstance(x, date) else str(x), return_type=str),
|
||||||
|
WithJsonSchema({'type': 'string'}, mode='serialization')
|
||||||
|
]
|
||||||
|
|
||||||
|
# 自定义时间字符串类型
|
||||||
|
TimeStr = Annotated[
|
||||||
|
time,
|
||||||
|
PlainSerializer(lambda x: x.strftime('%H:%M:%S') if isinstance(x, time) else str(x), return_type=str),
|
||||||
|
WithJsonSchema({'type': 'string'}, mode='serialization')
|
||||||
|
]
|
||||||
|
|
||||||
# 自定义手机号类型
|
# 自定义手机号类型
|
||||||
Telephone = Annotated[
|
Telephone = Annotated[
|
||||||
str,
|
str,
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from re import T
|
|
||||||
from starlette.responses import HTMLResponse
|
|
||||||
from typing import Any, AsyncGenerator
|
from typing import Any, AsyncGenerator
|
||||||
from fastapi import Depends, FastAPI, Request, Response
|
from fastapi import Depends, FastAPI, Request, Response
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.concurrency import asynccontextmanager
|
from fastapi.concurrency import asynccontextmanager
|
||||||
from fastapi.openapi.docs import (
|
from fastapi.openapi.docs import (
|
||||||
@@ -22,13 +21,7 @@ from app.core.discover import router
|
|||||||
from app.core.exceptions import CustomException, handle_exception
|
from app.core.exceptions import CustomException, handle_exception
|
||||||
from app.utils.common_util import import_module, import_modules_async
|
from app.utils.common_util import import_module, import_modules_async
|
||||||
from app.scripts.initialize import InitializeData
|
from app.scripts.initialize import InitializeData
|
||||||
|
from app.utils.console import console_close, console_run
|
||||||
from app.api.v1.module_application.job.tools.ap_scheduler import SchedulerUtil
|
|
||||||
from app.api.v1.module_system.params.service import ParamsService
|
|
||||||
from app.api.v1.module_system.dict.service import DictDataService
|
|
||||||
|
|
||||||
# 导入WebSocket路由器
|
|
||||||
from app.api.v1.module_application.ai.ws import WS_AI
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
|
async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
|
||||||
@@ -41,6 +34,10 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
|
|||||||
返回:
|
返回:
|
||||||
- AsyncGenerator[Any, Any]: 生命周期上下文生成器。
|
- AsyncGenerator[Any, Any]: 生命周期上下文生成器。
|
||||||
"""
|
"""
|
||||||
|
from app.api.v1.module_system.params.service import ParamsService
|
||||||
|
from app.api.v1.module_system.dict.service import DictDataService
|
||||||
|
from app.api.v1.module_application.job.tools.ap_scheduler import SchedulerUtil
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await InitializeData().init_db()
|
await InitializeData().init_db()
|
||||||
log.info(f"✅ {settings.DATABASE_TYPE}数据库初始化完成")
|
log.info(f"✅ {settings.DATABASE_TYPE}数据库初始化完成")
|
||||||
@@ -50,22 +47,15 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
|
|||||||
log.info("✅ Redis系统配置初始化完成")
|
log.info("✅ Redis系统配置初始化完成")
|
||||||
await DictDataService().init_dict_service(redis=app.state.redis)
|
await DictDataService().init_dict_service(redis=app.state.redis)
|
||||||
log.info("✅ Redis数据字典初始化完成")
|
log.info("✅ Redis数据字典初始化完成")
|
||||||
await SchedulerUtil.init_system_scheduler()
|
await SchedulerUtil.init_system_scheduler(redis=app.state.redis)
|
||||||
scheduler_jobs_count = len(SchedulerUtil.get_all_jobs())
|
log.info(f"✅ 定时任务调度器初始化完成")
|
||||||
scheduler_status = SchedulerUtil.get_job_status()
|
await FastAPILimiter.init(redis=app.state.redis, prefix=settings.REQUEST_LIMITER_REDIS_PREFIX, http_callback=http_limit_callback, ws_callback=ws_limit_callback)
|
||||||
log.info(f"✅ 定时任务调度器初始化完成 ({scheduler_jobs_count} 个任务)")
|
log.info("✅ 请求限流器初始化完成")
|
||||||
|
|
||||||
# 6. 初始化请求限制器
|
|
||||||
await FastAPILimiter.init(
|
|
||||||
redis=app.state.redis,
|
|
||||||
prefix=settings.REQUEST_LIMITER_REDIS_PREFIX,
|
|
||||||
http_callback=http_limit_callback,
|
|
||||||
)
|
|
||||||
log.info("✅ 请求限制器初始化完成")
|
|
||||||
|
|
||||||
# 导入并显示最终的启动信息面板
|
# 导入并显示最终的启动信息面板
|
||||||
from app.utils.console import run as console_run
|
|
||||||
from app.common.enums import EnvironmentEnum
|
from app.common.enums import EnvironmentEnum
|
||||||
|
scheduler_jobs_count = len(SchedulerUtil.get_all_jobs())
|
||||||
|
scheduler_status = SchedulerUtil.get_job_status()
|
||||||
console_run(
|
console_run(
|
||||||
host=settings.SERVER_HOST,
|
host=settings.SERVER_HOST,
|
||||||
port=settings.SERVER_PORT,
|
port=settings.SERVER_PORT,
|
||||||
@@ -88,11 +78,11 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
|
|||||||
log.info("✅ 定时任务调度器已关闭")
|
log.info("✅ 定时任务调度器已关闭")
|
||||||
await FastAPILimiter.close()
|
await FastAPILimiter.close()
|
||||||
log.info("✅ 请求限制器已关闭")
|
log.info("✅ 请求限制器已关闭")
|
||||||
|
console_close()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"❌ 应用关闭过程中发生错误: {str(e)}")
|
log.error(f"❌ 应用关闭过程中发生错误: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
def register_middlewares(app: FastAPI) -> None:
|
def register_middlewares(app: FastAPI) -> None:
|
||||||
"""
|
"""
|
||||||
注册全局中间件。
|
注册全局中间件。
|
||||||
@@ -131,6 +121,7 @@ def register_routers(app: FastAPI) -> None:
|
|||||||
返回:
|
返回:
|
||||||
- None
|
- None
|
||||||
"""
|
"""
|
||||||
|
from app.api.v1.module_application.ai.ws import WS_AI
|
||||||
# 手动注册WebSocket路由,不使用速率限制器
|
# 手动注册WebSocket路由,不使用速率限制器
|
||||||
app.include_router(router=WS_AI, dependencies=[Depends(WebSocketRateLimiter(times=1, seconds=5))])
|
app.include_router(router=WS_AI, dependencies=[Depends(WebSocketRateLimiter(times=1, seconds=5))])
|
||||||
# 先将动态路由注册到应用,使用速率限制器
|
# 先将动态路由注册到应用,使用速率限制器
|
||||||
@@ -203,7 +194,6 @@ async def http_limit_callback(request: Request, response: Response, expire: int)
|
|||||||
data={'Retry-After': str(expires)},
|
data={'Retry-After': str(expires)},
|
||||||
)
|
)
|
||||||
|
|
||||||
# 为WebSocket添加默认的回调函数
|
|
||||||
async def ws_limit_callback(ws: WebSocket, expire: int):
|
async def ws_limit_callback(ws: WebSocket, expire: int):
|
||||||
"""
|
"""
|
||||||
WebSocket请求限制时的默认回调函数
|
WebSocket请求限制时的默认回调函数
|
||||||
|
|||||||
@@ -1532,7 +1532,7 @@
|
|||||||
"route_path": "/application",
|
"route_path": "/application",
|
||||||
"component_path": null,
|
"component_path": null,
|
||||||
"status": "0",
|
"status": "0",
|
||||||
"keep_alive": false,
|
"keep_alive": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"always_show": false,
|
"always_show": false,
|
||||||
"title": "应用管理",
|
"title": "应用管理",
|
||||||
@@ -1882,7 +1882,7 @@
|
|||||||
"route_path": "/monitor",
|
"route_path": "/monitor",
|
||||||
"component_path": null,
|
"component_path": null,
|
||||||
"status": "0",
|
"status": "0",
|
||||||
"keep_alive": false,
|
"keep_alive": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"always_show": false,
|
"always_show": false,
|
||||||
"title": "监控管理",
|
"title": "监控管理",
|
||||||
@@ -1901,7 +1901,7 @@
|
|||||||
"route_path": "/monitor/online",
|
"route_path": "/monitor/online",
|
||||||
"component_path": "module_monitor/online/index",
|
"component_path": "module_monitor/online/index",
|
||||||
"status": "0",
|
"status": "0",
|
||||||
"keep_alive": false,
|
"keep_alive": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"always_show": false,
|
"always_show": false,
|
||||||
"title": "在线用户",
|
"title": "在线用户",
|
||||||
@@ -1920,7 +1920,7 @@
|
|||||||
"route_path": null,
|
"route_path": null,
|
||||||
"component_path": null,
|
"component_path": null,
|
||||||
"status": "0",
|
"status": "0",
|
||||||
"keep_alive": false,
|
"keep_alive": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"always_show": false,
|
"always_show": false,
|
||||||
"title": "在线用户强制下线",
|
"title": "在线用户强制下线",
|
||||||
@@ -1941,7 +1941,7 @@
|
|||||||
"route_path": "/monitor/server",
|
"route_path": "/monitor/server",
|
||||||
"component_path": "module_monitor/server/index",
|
"component_path": "module_monitor/server/index",
|
||||||
"status": "0",
|
"status": "0",
|
||||||
"keep_alive": false,
|
"keep_alive": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"always_show": false,
|
"always_show": false,
|
||||||
"title": "服务器监控",
|
"title": "服务器监控",
|
||||||
@@ -1960,7 +1960,7 @@
|
|||||||
"route_path": "/monitor/cache",
|
"route_path": "/monitor/cache",
|
||||||
"component_path": "module_monitor/cache/index",
|
"component_path": "module_monitor/cache/index",
|
||||||
"status": "0",
|
"status": "0",
|
||||||
"keep_alive": false,
|
"keep_alive": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"always_show": false,
|
"always_show": false,
|
||||||
"title": "缓存监控",
|
"title": "缓存监控",
|
||||||
@@ -1979,7 +1979,7 @@
|
|||||||
"route_path": null,
|
"route_path": null,
|
||||||
"component_path": null,
|
"component_path": null,
|
||||||
"status": "0",
|
"status": "0",
|
||||||
"keep_alive": false,
|
"keep_alive": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"always_show": false,
|
"always_show": false,
|
||||||
"title": "清除缓存",
|
"title": "清除缓存",
|
||||||
@@ -2175,7 +2175,7 @@
|
|||||||
"route_path": "/generator",
|
"route_path": "/generator",
|
||||||
"component_path": null,
|
"component_path": null,
|
||||||
"status": "0",
|
"status": "0",
|
||||||
"keep_alive": false,
|
"keep_alive": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"always_show": false,
|
"always_show": false,
|
||||||
"title": "代码管理",
|
"title": "代码管理",
|
||||||
@@ -2388,7 +2388,7 @@
|
|||||||
"route_path": "/common",
|
"route_path": "/common",
|
||||||
"component_path": null,
|
"component_path": null,
|
||||||
"status": "0",
|
"status": "0",
|
||||||
"keep_alive": false,
|
"keep_alive": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"always_show": false,
|
"always_show": false,
|
||||||
"title": "接口管理",
|
"title": "接口管理",
|
||||||
@@ -2407,7 +2407,7 @@
|
|||||||
"route_path": "/common/docs",
|
"route_path": "/common/docs",
|
||||||
"component_path": "module_common/docs/index",
|
"component_path": "module_common/docs/index",
|
||||||
"status": "0",
|
"status": "0",
|
||||||
"keep_alive": false,
|
"keep_alive": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"always_show": false,
|
"always_show": false,
|
||||||
"title": "Swagger文档",
|
"title": "Swagger文档",
|
||||||
@@ -2426,7 +2426,7 @@
|
|||||||
"route_path": "/common/redoc",
|
"route_path": "/common/redoc",
|
||||||
"component_path": "module_common/redoc/index",
|
"component_path": "module_common/redoc/index",
|
||||||
"status": "0",
|
"status": "0",
|
||||||
"keep_alive": false,
|
"keep_alive": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"always_show": false,
|
"always_show": false,
|
||||||
"title": "Redoc文档",
|
"title": "Redoc文档",
|
||||||
@@ -2438,33 +2438,33 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "模块管理",
|
"name": "案例管理",
|
||||||
"type": 1,
|
"type": 1,
|
||||||
"icon": "menu",
|
"icon": "menu",
|
||||||
"order": 7,
|
"order": 7,
|
||||||
"permission": null,
|
"permission": null,
|
||||||
"route_name": "Gencode",
|
"route_name": "Example",
|
||||||
"route_path": "/gencode",
|
"route_path": "/example",
|
||||||
"component_path": null,
|
"component_path": null,
|
||||||
"status": "0",
|
"status": "0",
|
||||||
"keep_alive": false,
|
"keep_alive": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"always_show": false,
|
"always_show": false,
|
||||||
"title": "模块管理",
|
"title": "案例管理",
|
||||||
"params": null,
|
"params": null,
|
||||||
"affix": false,
|
"affix": false,
|
||||||
"redirect": "/gencode/demo",
|
"redirect": "/example/demo",
|
||||||
"description": "模块管理",
|
"description": "案例管理",
|
||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"name": "示例管理",
|
"name": "示例管理",
|
||||||
"type": 2,
|
"type": 2,
|
||||||
"icon": "menu",
|
"icon": "menu",
|
||||||
"order": 1,
|
"order": 1,
|
||||||
"permission": "module_gencode:demo:query",
|
"permission": "module_example:demo:query",
|
||||||
"route_name": "Demo",
|
"route_name": "Demo",
|
||||||
"route_path": "/gencode/demo",
|
"route_path": "/example/demo",
|
||||||
"component_path": "module_gencode/demo/index",
|
"component_path": "module_example/demo/index",
|
||||||
"status": "0",
|
"status": "0",
|
||||||
"keep_alive": true,
|
"keep_alive": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
@@ -2480,7 +2480,7 @@
|
|||||||
"type": 3,
|
"type": 3,
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"order": 1,
|
"order": 1,
|
||||||
"permission": "module_gencode:demo:create",
|
"permission": "module_example:demo:create",
|
||||||
"route_name": null,
|
"route_name": null,
|
||||||
"route_path": null,
|
"route_path": null,
|
||||||
"component_path": null,
|
"component_path": null,
|
||||||
@@ -2499,7 +2499,7 @@
|
|||||||
"type": 3,
|
"type": 3,
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"order": 2,
|
"order": 2,
|
||||||
"permission": "module_gencode:demo:update",
|
"permission": "module_example:demo:update",
|
||||||
"route_name": null,
|
"route_name": null,
|
||||||
"route_path": null,
|
"route_path": null,
|
||||||
"component_path": null,
|
"component_path": null,
|
||||||
@@ -2518,7 +2518,7 @@
|
|||||||
"type": 3,
|
"type": 3,
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"order": 3,
|
"order": 3,
|
||||||
"permission": "module_gencode:demo:delete",
|
"permission": "module_example:demo:delete",
|
||||||
"route_name": null,
|
"route_name": null,
|
||||||
"route_path": null,
|
"route_path": null,
|
||||||
"component_path": null,
|
"component_path": null,
|
||||||
@@ -2537,7 +2537,7 @@
|
|||||||
"type": 3,
|
"type": 3,
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"order": 4,
|
"order": 4,
|
||||||
"permission": "module_gencode:demo:patch",
|
"permission": "module_example:demo:patch",
|
||||||
"route_name": null,
|
"route_name": null,
|
||||||
"route_path": null,
|
"route_path": null,
|
||||||
"component_path": null,
|
"component_path": null,
|
||||||
@@ -2556,7 +2556,7 @@
|
|||||||
"type": 3,
|
"type": 3,
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"order": 5,
|
"order": 5,
|
||||||
"permission": "module_gencode:demo:export",
|
"permission": "module_example:demo:export",
|
||||||
"route_name": null,
|
"route_name": null,
|
||||||
"route_path": null,
|
"route_path": null,
|
||||||
"component_path": null,
|
"component_path": null,
|
||||||
@@ -2575,7 +2575,7 @@
|
|||||||
"type": 3,
|
"type": 3,
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"order": 6,
|
"order": 6,
|
||||||
"permission": "module_gencode:demo:import",
|
"permission": "module_example:demo:import",
|
||||||
"route_name": null,
|
"route_name": null,
|
||||||
"route_path": null,
|
"route_path": null,
|
||||||
"component_path": null,
|
"component_path": null,
|
||||||
@@ -2594,7 +2594,7 @@
|
|||||||
"type": 3,
|
"type": 3,
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"order": 7,
|
"order": 7,
|
||||||
"permission": "module_gencode:demo:download",
|
"permission": "module_example:demo:download",
|
||||||
"route_name": null,
|
"route_name": null,
|
||||||
"route_path": null,
|
"route_path": null,
|
||||||
"component_path": null,
|
"component_path": null,
|
||||||
@@ -2613,7 +2613,7 @@
|
|||||||
"type": 3,
|
"type": 3,
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"order": 8,
|
"order": 8,
|
||||||
"permission": "module_gencode:demo:detail",
|
"permission": "module_example:demo:detail",
|
||||||
"route_name": null,
|
"route_name": null,
|
||||||
"route_path": null,
|
"route_path": null,
|
||||||
"component_path": null,
|
"component_path": null,
|
||||||
@@ -2632,7 +2632,7 @@
|
|||||||
"type": 3,
|
"type": 3,
|
||||||
"icon": null,
|
"icon": null,
|
||||||
"order": 9,
|
"order": 9,
|
||||||
"permission": "module_gencode:demo:query",
|
"permission": "module_example:demo:query",
|
||||||
"route_name": null,
|
"route_name": null,
|
||||||
"route_path": null,
|
"route_path": null,
|
||||||
"component_path": null,
|
"component_path": null,
|
||||||
|
|||||||
@@ -11,14 +11,12 @@ from app.config.setting import settings
|
|||||||
|
|
||||||
console = get_console()
|
console = get_console()
|
||||||
|
|
||||||
|
def console_run(host: str, port: int, reload: bool, *,
|
||||||
def create_service_panel(
|
redis_ready: Optional[bool] = None,
|
||||||
host: str, port: int, reload: bool, *,
|
scheduler_jobs: Optional[int] = None,
|
||||||
redis_ready: Optional[bool] = None,
|
scheduler_status: Optional[str] = None
|
||||||
scheduler_jobs: Optional[int] = None,
|
) -> None:
|
||||||
scheduler_status: Optional[str] = None,
|
"""显示启动信息面板"""
|
||||||
) -> Panel:
|
|
||||||
"""创建简洁的服务启动信息面板"""
|
|
||||||
|
|
||||||
url = f'http://{host}:{port}'
|
url = f'http://{host}:{port}'
|
||||||
base_url = f'{url}{settings.ROOT_PATH}'
|
base_url = f'{url}{settings.ROOT_PATH}'
|
||||||
@@ -48,34 +46,16 @@ def create_service_panel(
|
|||||||
docs_info,
|
docs_info,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Panel(
|
result = Panel(
|
||||||
renderable=final_content,
|
renderable=final_content,
|
||||||
title="[bold purple]🚀 服务启动完成[/]",
|
title="[bold purple]🚀 服务启动完成[/]",
|
||||||
border_style="green",
|
border_style="green",
|
||||||
padding=(1, 2)
|
padding=(1, 2)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
console.print(result)
|
||||||
|
|
||||||
def run(host: str, port: int, reload: bool, *,
|
def console_close():
|
||||||
redis_ready: Optional[bool] = None,
|
|
||||||
scheduler_jobs: Optional[int] = None,
|
|
||||||
scheduler_status: Optional[str] = None
|
|
||||||
) -> None:
|
|
||||||
"""显示启动信息面板"""
|
|
||||||
|
|
||||||
# 创建并显示启动面板
|
|
||||||
service_panel = create_service_panel(
|
|
||||||
host=host,
|
|
||||||
port=port,
|
|
||||||
reload=reload,
|
|
||||||
redis_ready=redis_ready,
|
|
||||||
scheduler_jobs=scheduler_jobs,
|
|
||||||
scheduler_status=scheduler_status,
|
|
||||||
)
|
|
||||||
|
|
||||||
console.print(service_panel)
|
|
||||||
|
|
||||||
def display_shutdown_info():
|
|
||||||
"""显示关闭信息"""
|
"""显示关闭信息"""
|
||||||
shutdown_content = Text()
|
shutdown_content = Text()
|
||||||
shutdown_content.append("🛑 ", style="bold red")
|
shutdown_content.append("🛑 ", style="bold red")
|
||||||
@@ -83,11 +63,11 @@ def display_shutdown_info():
|
|||||||
shutdown_content.append(f"\n⏰ {datetime.now().strftime('%H:%M:%S')}")
|
shutdown_content.append(f"\n⏰ {datetime.now().strftime('%H:%M:%S')}")
|
||||||
shutdown_content.append("\n👋 感谢使用!", style="dim")
|
shutdown_content.append("\n👋 感谢使用!", style="dim")
|
||||||
|
|
||||||
shutdown_panel = Panel(
|
result = Panel(
|
||||||
shutdown_content,
|
shutdown_content,
|
||||||
title="[bold red]服务关闭[/]",
|
title="[bold red]服务关闭[/]",
|
||||||
border_style="red",
|
border_style="red",
|
||||||
padding=(1, 2)
|
padding=(1, 2)
|
||||||
)
|
)
|
||||||
|
|
||||||
console.print(shutdown_panel)
|
console.print(result)
|
||||||
|
|||||||
Vendored
+1
-1
@@ -22,7 +22,7 @@ DESCRIPTION = "该项目是一个基于python的web服务框架,基于fastapi
|
|||||||
DEMO_ENABLE = False
|
DEMO_ENABLE = False
|
||||||
|
|
||||||
# 数据库配置
|
# 数据库配置
|
||||||
DATABASE_TYPE = "sqlite" # mysql、postgres、[qlite、dm这俩种不支持代码生成]
|
DATABASE_TYPE = "mysql" # mysql、postgres、[qlite、dm这俩种不支持代码生成]
|
||||||
|
|
||||||
# 数据库配置
|
# 数据库配置
|
||||||
DATABASE_HOST = "localhost"
|
DATABASE_HOST = "localhost"
|
||||||
|
|||||||
@@ -12,11 +12,8 @@ from app.common.enums import EnvironmentEnum
|
|||||||
|
|
||||||
|
|
||||||
fastapiadmin_cli = typer.Typer()
|
fastapiadmin_cli = typer.Typer()
|
||||||
|
|
||||||
# 初始化 Alembic 配置
|
|
||||||
alembic_cfg = Config("alembic.ini")
|
alembic_cfg = Config("alembic.ini")
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> FastAPI:
|
def create_app() -> FastAPI:
|
||||||
"""创建 FastAPI 应用实例"""
|
"""创建 FastAPI 应用实例"""
|
||||||
from app.plugin.init_app import (
|
from app.plugin.init_app import (
|
||||||
@@ -78,10 +75,6 @@ def run(env: Annotated[EnvironmentEnum, typer.Option("--env", help="运行环境
|
|||||||
factory=True,
|
factory=True,
|
||||||
log_config=None
|
log_config=None
|
||||||
)
|
)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
from app.utils.console import display_shutdown_info
|
|
||||||
display_shutdown_info()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
+17
-16
@@ -11,7 +11,7 @@ httpx==0.28.1 # HTTP 客户端
|
|||||||
croniter==6.0.0 # 实现cron表达式验证和解析执行计划
|
croniter==6.0.0 # 实现cron表达式验证和解析执行计划
|
||||||
pandas==2.2.2 # 数据处理
|
pandas==2.2.2 # 数据处理
|
||||||
openpyxl==3.1.5 # Excel
|
openpyxl==3.1.5 # Excel
|
||||||
SQLAlchemy==2.0.36 # 数据库ORM
|
SQLAlchemy==2.0.45 # 数据库ORM
|
||||||
pillow==11.0.0 # 图片处理
|
pillow==11.0.0 # 图片处理
|
||||||
passlib==1.7.4 # 密码加密
|
passlib==1.7.4 # 密码加密
|
||||||
user-agents==2.2.0 # 获取用户UA
|
user-agents==2.2.0 # 获取用户UA
|
||||||
@@ -23,23 +23,24 @@ greenlet==3.1.1 # 协程框架
|
|||||||
bcrypt==4.0.1 # 密码加密解析,切勿升级,如果升级,请同时升级python版本
|
bcrypt==4.0.1 # 密码加密解析,切勿升级,如果升级,请同时升级python版本
|
||||||
itsdangerous==2.2.0 # 用于安全处理各种数据,如密码、密钥等
|
itsdangerous==2.2.0 # 用于安全处理各种数据,如密码、密钥等
|
||||||
aiofiles==24.1.0 # 文件操作
|
aiofiles==24.1.0 # 文件操作
|
||||||
redis==5.2.1 # redis 同/异步操作数据库(用户celery配套使用)redis 异步操作数据库 redis已经完全具备了aioredis的功能,无需重复安全,且aioredis已经不再维护也不兼容3.10+的版本
|
redis==7.1.0 # redis 同/异步操作数据库(用户celery配套使用)redis 异步操作数据库 redis已经完全具备了aioredis的功能,无需重复安全,且aioredis已经不再维护也不兼容3.10+的版本
|
||||||
|
cryptography==45.0.2 # mysql8 密码加密
|
||||||
|
rich==13.9.4 # 终端打印美化
|
||||||
|
pydantic_validation_decorator==0.1.4 # 模型验证
|
||||||
|
loguru==0.7.3
|
||||||
|
fastapi-limiter==0.1.6
|
||||||
|
sqlglot[rs]==27.8.0 # sql 解析
|
||||||
asyncmy==0.2.9 # mysql 异步操作数据库:基于 mysqlclient:asyncmy 是 mysqlclient 的异步版本,mysqlclient 是一个 C 语言编写的 MySQL 客户端,性能较高。性能:asyncmy 通常在性能上优于 aiomysql,特别是在高并发和大数据量的场景下。
|
asyncmy==0.2.9 # mysql 异步操作数据库:基于 mysqlclient:asyncmy 是 mysqlclient 的异步版本,mysqlclient 是一个 C 语言编写的 MySQL 客户端,性能较高。性能:asyncmy 通常在性能上优于 aiomysql,特别是在高并发和大数据量的场景下。
|
||||||
PyMySQL==1.1.2 # mysql 同步步操作数据库基于 pymysql:aiomysql 是 pymysql 的异步版本,pymysql 是一个纯 Python 实现的 MySQL 客户端。成熟度:aiomysql 相对较为成熟,社区支持较好,文档也比较完善。
|
PyMySQL==1.1.2 # mysql 同步步操作数据库基于 pymysql:aiomysql 是 pymysql 的异步版本,pymysql 是一个纯 Python 实现的 MySQL 客户端。成熟度:aiomysql 相对较为成熟,社区支持较好,文档也比较完善。
|
||||||
asyncpg==0.30.0 # postgresql 异步操作数据库基于 psycopg2:asyncpg 是 psycopg2 的异步版本,psycopg2 是一个 pure-Python PostgreSQL 数据库适配器。性能:asyncpg 通常在性能上优于 psycopg2,特别是在高并发和大数据量的场景下。
|
asyncpg==0.30.0 # postgresql 异步操作数据库基于 psycopg2:asyncpg 是 psycopg2 的异步版本,psycopg2 是一个 pure-Python PostgreSQL 数据库适配器。性能:asyncpg 通常在性能上优于 psycopg2,特别是在高并发和大数据量的场景下。
|
||||||
psycopg2==2.9.10 # postgresql 同步操作数据库基于 psycopg2:psycopg2 是一个 pure-Python PostgreSQL 适配器。
|
psycopg2==2.9.10 # postgresql 同步操作数据库基于 psycopg2:psycopg2 是一个 pure-Python PostgreSQL 适配器。
|
||||||
cryptography==45.0.2 # mysql8 密码加密
|
aiosqlite==0.17.0 # sqlite 异步操作数据库
|
||||||
openai==1.55.2 # ai 大模型
|
|
||||||
rich==13.9.4 # 终端打印美化
|
|
||||||
sqlglot[rs]==27.8.0 # sql 解析
|
|
||||||
pydantic_validation_decorator==0.1.4 # 模型验证
|
|
||||||
loguru==0.7.3
|
|
||||||
fastapi-limiter==0.1.6
|
|
||||||
# dmPython # 达梦数据库驱动
|
|
||||||
# aiosqlite==0.17.0 # sqlite 异步操作数据库
|
|
||||||
# motor==3.6.0 # mongodb 驱动
|
|
||||||
|
|
||||||
# amqp==5.3.1
|
|
||||||
# python-socketio==5.14.3
|
# python-socketio==5.14.3
|
||||||
langchain
|
# dmPython # 达梦数据库驱动
|
||||||
langchain-openai
|
# openai==1.55.2 # ai 大模型
|
||||||
|
langchain==1.2.0
|
||||||
|
langchain-openai==1.1.6
|
||||||
|
langchain-anthropic==1.3.0
|
||||||
|
langchain-mcp-adapters==0.2.1
|
||||||
|
fastmcp==2.14.1
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
import request from "@/utils/request";
|
import request from "@/utils/request";
|
||||||
|
|
||||||
const API_PATH = "/gencode/demo";
|
const API_PATH = "/example/demo";
|
||||||
|
|
||||||
const DemoAPI = {
|
const DemoAPI = {
|
||||||
getDemoList(query: DemoPageQuery) {
|
getDemoList(query: DemoPageQuery) {
|
||||||
@@ -94,8 +94,26 @@ export interface DemoTable extends BaseType {
|
|||||||
name?: string;
|
name?: string;
|
||||||
created_by?: CommonType;
|
created_by?: CommonType;
|
||||||
updated_by?: CommonType;
|
updated_by?: CommonType;
|
||||||
|
a?: number;
|
||||||
|
b?: number;
|
||||||
|
c?: number;
|
||||||
|
d?: boolean;
|
||||||
|
e?: string;
|
||||||
|
f?: string;
|
||||||
|
g?: string;
|
||||||
|
h?: string;
|
||||||
|
i?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DemoForm extends BaseFormType {
|
export interface DemoForm extends BaseFormType {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
a?: number;
|
||||||
|
b?: number;
|
||||||
|
c?: number;
|
||||||
|
d?: boolean;
|
||||||
|
e?: string;
|
||||||
|
f?: string;
|
||||||
|
g?: string;
|
||||||
|
h?: string;
|
||||||
|
i?: Record<string, any>;
|
||||||
}
|
}
|
||||||
@@ -707,8 +707,8 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.message-body {
|
.message-body {
|
||||||
.fold-button {
|
.fold-button {
|
||||||
margin-bottom: 8px;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
margin-bottom: 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
|
|
||||||
@@ -725,17 +725,17 @@ onUnmounted(() => {
|
|||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
|
position: relative;
|
||||||
max-height: 120px;
|
max-height: 120px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: "";
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
content: "";
|
||||||
background: linear-gradient(to bottom, transparent, var(--el-bg-color));
|
background: linear-gradient(to bottom, transparent, var(--el-bg-color));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+245
-16
@@ -54,7 +54,7 @@
|
|||||||
<!-- 查询、重置、展开/收起按钮 -->
|
<!-- 查询、重置、展开/收起按钮 -->
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPerm="['module_gencode:demo:query']"
|
v-hasPerm="['module_example:demo:query']"
|
||||||
type="primary"
|
type="primary"
|
||||||
icon="search"
|
icon="search"
|
||||||
@click="handleQuery"
|
@click="handleQuery"
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
查询
|
查询
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPerm="['module_gencode:demo:query']"
|
v-hasPerm="['module_example:demo:query']"
|
||||||
icon="refresh"
|
icon="refresh"
|
||||||
@click="handleResetQuery"
|
@click="handleResetQuery"
|
||||||
>
|
>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
<el-row :gutter="10">
|
<el-row :gutter="10">
|
||||||
<el-col :span="1.5">
|
<el-col :span="1.5">
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPerm="['module_gencode:demo:create']"
|
v-hasPerm="['module_example:demo:create']"
|
||||||
type="success"
|
type="success"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
@click="handleOpenDialog('create')"
|
@click="handleOpenDialog('create')"
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="1.5">
|
<el-col :span="1.5">
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPerm="['module_gencode:demo:delete']"
|
v-hasPerm="['module_example:demo:delete']"
|
||||||
type="danger"
|
type="danger"
|
||||||
icon="delete"
|
icon="delete"
|
||||||
:disabled="selectIds.length === 0"
|
:disabled="selectIds.length === 0"
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="1.5">
|
<el-col :span="1.5">
|
||||||
<el-dropdown v-hasPerm="['module_gencode:demo:patch']" trigger="click">
|
<el-dropdown v-hasPerm="['module_example:demo:patch']" trigger="click">
|
||||||
<el-button type="default" :disabled="selectIds.length === 0" icon="ArrowDown">
|
<el-button type="default" :disabled="selectIds.length === 0" icon="ArrowDown">
|
||||||
更多
|
更多
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
<el-col :span="1.5">
|
<el-col :span="1.5">
|
||||||
<el-tooltip content="导入">
|
<el-tooltip content="导入">
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPerm="['module_gencode:demo:import']"
|
v-hasPerm="['module_example:demo:import']"
|
||||||
type="success"
|
type="success"
|
||||||
icon="upload"
|
icon="upload"
|
||||||
circle
|
circle
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
<el-col :span="1.5">
|
<el-col :span="1.5">
|
||||||
<el-tooltip content="导出">
|
<el-tooltip content="导出">
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPerm="['module_gencode:demo:export']"
|
v-hasPerm="['module_example:demo:export']"
|
||||||
type="warning"
|
type="warning"
|
||||||
icon="download"
|
icon="download"
|
||||||
circle
|
circle
|
||||||
@@ -181,7 +181,7 @@
|
|||||||
<el-col :span="1.5">
|
<el-col :span="1.5">
|
||||||
<el-tooltip content="刷新">
|
<el-tooltip content="刷新">
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPerm="['module_gencode:demo:query']"
|
v-hasPerm="['module_example:demo:query']"
|
||||||
type="primary"
|
type="primary"
|
||||||
icon="refresh"
|
icon="refresh"
|
||||||
circle
|
circle
|
||||||
@@ -243,6 +243,12 @@
|
|||||||
prop="name"
|
prop="name"
|
||||||
min-width="140"
|
min-width="140"
|
||||||
/>
|
/>
|
||||||
|
<el-table-column
|
||||||
|
v-if="tableColumns.find((col) => col.prop === 'uuid')?.show"
|
||||||
|
label="UUID"
|
||||||
|
prop="uuid"
|
||||||
|
min-width="180"
|
||||||
|
/>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
v-if="tableColumns.find((col) => col.prop === 'status')?.show"
|
v-if="tableColumns.find((col) => col.prop === 'status')?.show"
|
||||||
label="状态"
|
label="状态"
|
||||||
@@ -255,6 +261,72 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
v-if="tableColumns.find((col) => col.prop === 'a')?.show"
|
||||||
|
label="整数"
|
||||||
|
prop="a"
|
||||||
|
min-width="100"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
v-if="tableColumns.find((col) => col.prop === 'b')?.show"
|
||||||
|
label="大整数"
|
||||||
|
prop="b"
|
||||||
|
min-width="120"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
v-if="tableColumns.find((col) => col.prop === 'c')?.show"
|
||||||
|
label="浮点数"
|
||||||
|
prop="c"
|
||||||
|
min-width="100"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
v-if="tableColumns.find((col) => col.prop === 'd')?.show"
|
||||||
|
label="布尔值"
|
||||||
|
prop="d"
|
||||||
|
min-width="100"
|
||||||
|
>
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="scope.row.d ? 'success' : 'danger'">
|
||||||
|
{{ scope.row.d ? "是" : "否" }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
v-if="tableColumns.find((col) => col.prop === 'e')?.show"
|
||||||
|
label="日期"
|
||||||
|
prop="e"
|
||||||
|
min-width="120"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
v-if="tableColumns.find((col) => col.prop === 'f')?.show"
|
||||||
|
label="时间"
|
||||||
|
prop="f"
|
||||||
|
min-width="120"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
v-if="tableColumns.find((col) => col.prop === 'g')?.show"
|
||||||
|
label="日期时间"
|
||||||
|
prop="g"
|
||||||
|
min-width="180"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
v-if="tableColumns.find((col) => col.prop === 'h')?.show"
|
||||||
|
label="长文本"
|
||||||
|
prop="h"
|
||||||
|
min-width="140"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
v-if="tableColumns.find((col) => col.prop === 'i')?.show"
|
||||||
|
label="元数据"
|
||||||
|
prop="i"
|
||||||
|
min-width="140"
|
||||||
|
>
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tooltip :content="JSON.stringify(scope.row.i)" placement="top">
|
||||||
|
<div class="json-preview">{{ JSON.stringify(scope.row.i) }}</div>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
v-if="tableColumns.find((col) => col.prop === 'description')?.show"
|
v-if="tableColumns.find((col) => col.prop === 'description')?.show"
|
||||||
label="描述"
|
label="描述"
|
||||||
@@ -302,7 +374,7 @@
|
|||||||
>
|
>
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPerm="['module_gencode:demo:detail']"
|
v-hasPerm="['module_example:demo:detail']"
|
||||||
type="info"
|
type="info"
|
||||||
size="small"
|
size="small"
|
||||||
link
|
link
|
||||||
@@ -312,7 +384,7 @@
|
|||||||
详情
|
详情
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPerm="['module_gencode:demo:update']"
|
v-hasPerm="['module_example:demo:update']"
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
link
|
link
|
||||||
@@ -322,7 +394,7 @@
|
|||||||
编辑
|
编辑
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPerm="['module_gencode:demo:delete']"
|
v-hasPerm="['module_example:demo:delete']"
|
||||||
type="danger"
|
type="danger"
|
||||||
size="small"
|
size="small"
|
||||||
link
|
link
|
||||||
@@ -358,11 +430,45 @@
|
|||||||
<el-descriptions-item label="名称" :span="2">
|
<el-descriptions-item label="名称" :span="2">
|
||||||
{{ detailFormData.name }}
|
{{ detailFormData.name }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="UUID" :span="2">
|
||||||
|
{{ detailFormData.uuid }}
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="状态" :span="2">
|
<el-descriptions-item label="状态" :span="2">
|
||||||
<el-tag :type="detailFormData.status ? 'success' : 'danger'">
|
<el-tag :type="detailFormData.status ? 'success' : 'danger'">
|
||||||
{{ detailFormData.status ? "启用" : "停用" }}
|
{{ detailFormData.status ? "启用" : "停用" }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="整数" :span="2">
|
||||||
|
{{ detailFormData.a }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="大整数" :span="2">
|
||||||
|
{{ detailFormData.b }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="浮点数" :span="2">
|
||||||
|
{{ detailFormData.c }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="布尔值" :span="2">
|
||||||
|
<el-tag :type="detailFormData.d ? 'success' : 'danger'">
|
||||||
|
{{ detailFormData.d ? "是" : "否" }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="日期" :span="2">
|
||||||
|
{{ detailFormData.e }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="时间" :span="2">
|
||||||
|
{{ detailFormData.f }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="日期时间" :span="2">
|
||||||
|
{{ detailFormData.g }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="长文本" :span="2">
|
||||||
|
{{ detailFormData.h }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="元数据" :span="2">
|
||||||
|
<el-tooltip :content="JSON.stringify(detailFormData.i)" placement="top">
|
||||||
|
<div class="json-preview">{{ JSON.stringify(detailFormData.i) }}</div>
|
||||||
|
</el-tooltip>
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="描述" :span="2">
|
<el-descriptions-item label="描述" :span="2">
|
||||||
{{ detailFormData.description }}
|
{{ detailFormData.description }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -399,6 +505,54 @@
|
|||||||
<el-radio value="1">停用</el-radio>
|
<el-radio value="1">停用</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="整数" prop="a">
|
||||||
|
<el-input-number v-model="formData.a" placeholder="请输入整数" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="大整数" prop="b">
|
||||||
|
<el-input-number v-model="formData.b" placeholder="请输入大整数" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="浮点数" prop="c">
|
||||||
|
<el-input-number
|
||||||
|
v-model="formData.c"
|
||||||
|
placeholder="请输入浮点数"
|
||||||
|
:step="0.01"
|
||||||
|
:precision="2"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="布尔值" prop="d">
|
||||||
|
<el-switch v-model="formData.d" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="日期" prop="e">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="formData.e"
|
||||||
|
type="date"
|
||||||
|
placeholder="请选择日期"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="时间" prop="f">
|
||||||
|
<el-time-picker v-model="formData.f" placeholder="请选择时间" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="日期时间" prop="g">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="formData.g"
|
||||||
|
type="datetime"
|
||||||
|
placeholder="请选择日期时间"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="长文本" prop="h">
|
||||||
|
<el-input v-model="formData.h" :rows="4" type="textarea" placeholder="请输入长文本" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="元数据" prop="i">
|
||||||
|
<el-input
|
||||||
|
:model-value="JSON.stringify(formData.i)"
|
||||||
|
:rows="4"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请输入JSON格式的元数据"
|
||||||
|
@update:model-value="(val) => (formData.i = val ? JSON.parse(val) : undefined)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="描述" prop="description">
|
<el-form-item label="描述" prop="description">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="formData.description"
|
v-model="formData.description"
|
||||||
@@ -451,7 +605,7 @@ defineOptions({
|
|||||||
import { ref, reactive, onMounted } from "vue";
|
import { ref, reactive, onMounted } from "vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { ResultEnum } from "@/enums/api/result.enum";
|
import { ResultEnum } from "@/enums/api/result.enum";
|
||||||
import DemoAPI, { DemoTable, DemoForm, DemoPageQuery } from "@/api/module_gencode/demo";
|
import DemoAPI, { DemoTable, DemoForm, DemoPageQuery } from "@/api/module_example/demo";
|
||||||
import ImportModal from "@/components/CURD/ImportModal.vue";
|
import ImportModal from "@/components/CURD/ImportModal.vue";
|
||||||
import ExportModal from "@/components/CURD/ExportModal.vue";
|
import ExportModal from "@/components/CURD/ExportModal.vue";
|
||||||
import DatePicker from "@/components/DatePicker/index.vue";
|
import DatePicker from "@/components/DatePicker/index.vue";
|
||||||
@@ -477,7 +631,17 @@ const tableColumns = ref([
|
|||||||
{ prop: "selection", label: "选择框", show: true },
|
{ prop: "selection", label: "选择框", show: true },
|
||||||
{ prop: "index", label: "序号", show: true },
|
{ prop: "index", label: "序号", show: true },
|
||||||
{ prop: "name", label: "名称", show: true },
|
{ prop: "name", label: "名称", show: true },
|
||||||
|
{ prop: "uuid", label: "UUID", show: true },
|
||||||
{ prop: "status", label: "状态", show: true },
|
{ prop: "status", label: "状态", show: true },
|
||||||
|
{ prop: "a", label: "整数", show: true },
|
||||||
|
{ prop: "b", label: "大整数", show: true },
|
||||||
|
{ prop: "c", label: "浮点数", show: true },
|
||||||
|
{ prop: "d", label: "布尔值", show: true },
|
||||||
|
{ prop: "e", label: "日期", show: true },
|
||||||
|
{ prop: "f", label: "时间", show: true },
|
||||||
|
{ prop: "g", label: "日期时间", show: true },
|
||||||
|
{ prop: "h", label: "长文本", show: true },
|
||||||
|
{ prop: "i", label: "元数据", show: true },
|
||||||
{ prop: "description", label: "描述", show: true },
|
{ prop: "description", label: "描述", show: true },
|
||||||
{ prop: "created_time", label: "创建时间", show: true },
|
{ prop: "created_time", label: "创建时间", show: true },
|
||||||
{ prop: "updated_time", label: "更新时间", show: true },
|
{ prop: "updated_time", label: "更新时间", show: true },
|
||||||
@@ -489,7 +653,17 @@ const tableColumns = ref([
|
|||||||
// 仅用于导出字段的列(排除非数据列及嵌套对象列)
|
// 仅用于导出字段的列(排除非数据列及嵌套对象列)
|
||||||
const exportColumns = [
|
const exportColumns = [
|
||||||
{ prop: "name", label: "名称" },
|
{ prop: "name", label: "名称" },
|
||||||
|
{ prop: "uuid", label: "UUID" },
|
||||||
{ prop: "status", label: "状态" },
|
{ prop: "status", label: "状态" },
|
||||||
|
{ prop: "a", label: "整数" },
|
||||||
|
{ prop: "b", label: "大整数" },
|
||||||
|
{ prop: "c", label: "浮点数" },
|
||||||
|
{ prop: "d", label: "布尔值" },
|
||||||
|
{ prop: "e", label: "日期" },
|
||||||
|
{ prop: "f", label: "时间" },
|
||||||
|
{ prop: "g", label: "日期时间" },
|
||||||
|
{ prop: "h", label: "长文本" },
|
||||||
|
{ prop: "i", label: "元数据" },
|
||||||
{ prop: "description", label: "描述" },
|
{ prop: "description", label: "描述" },
|
||||||
{ prop: "created_time", label: "创建时间" },
|
{ prop: "created_time", label: "创建时间" },
|
||||||
{ prop: "updated_time", label: "更新时间" },
|
{ prop: "updated_time", label: "更新时间" },
|
||||||
@@ -497,7 +671,7 @@ const exportColumns = [
|
|||||||
|
|
||||||
// 导入/导出配置
|
// 导入/导出配置
|
||||||
const curdContentConfig = {
|
const curdContentConfig = {
|
||||||
permPrefix: "module_gencode:demo",
|
permPrefix: "module_example:demo",
|
||||||
cols: exportColumns as any,
|
cols: exportColumns as any,
|
||||||
importTemplate: () => DemoAPI.downloadTemplateDemo(),
|
importTemplate: () => DemoAPI.downloadTemplateDemo(),
|
||||||
exportsAction: async (params: any) => {
|
exportsAction: async (params: any) => {
|
||||||
@@ -564,6 +738,15 @@ const formData = reactive<DemoForm>({
|
|||||||
name: "",
|
name: "",
|
||||||
status: "0",
|
status: "0",
|
||||||
description: undefined,
|
description: undefined,
|
||||||
|
a: undefined,
|
||||||
|
b: undefined,
|
||||||
|
c: undefined,
|
||||||
|
d: true,
|
||||||
|
e: undefined,
|
||||||
|
f: undefined,
|
||||||
|
g: undefined,
|
||||||
|
h: undefined,
|
||||||
|
i: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 弹窗状态
|
// 弹窗状态
|
||||||
@@ -642,7 +825,16 @@ const initialFormData: DemoForm = {
|
|||||||
id: undefined,
|
id: undefined,
|
||||||
name: "",
|
name: "",
|
||||||
status: "0",
|
status: "0",
|
||||||
description: "",
|
description: undefined,
|
||||||
|
a: undefined,
|
||||||
|
b: undefined,
|
||||||
|
c: undefined,
|
||||||
|
d: true,
|
||||||
|
e: undefined,
|
||||||
|
f: undefined,
|
||||||
|
g: undefined,
|
||||||
|
h: undefined,
|
||||||
|
i: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 重置表单
|
// 重置表单
|
||||||
@@ -692,11 +884,48 @@ async function handleSubmit() {
|
|||||||
dataFormRef.value.validate(async (valid: any) => {
|
dataFormRef.value.validate(async (valid: any) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
// 处理元数据字段,将字符串转换为JSON对象
|
||||||
|
const submitData = { ...formData };
|
||||||
|
if (submitData.i && typeof submitData.i === "string") {
|
||||||
|
try {
|
||||||
|
submitData.i = JSON.parse(submitData.i);
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error("元数据格式错误,请输入有效的JSON字符串" + error.message);
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理时间字段格式
|
||||||
|
const formatDate = (date: Date | string | undefined): string | undefined => {
|
||||||
|
if (!date) return undefined;
|
||||||
|
const d = typeof date === "string" ? new Date(date) : date;
|
||||||
|
return d.toISOString().split("T")[0]; // 只保留日期部分 YYYY-MM-DD
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (time: Date | string | undefined): string | undefined => {
|
||||||
|
if (!time) return undefined;
|
||||||
|
const t = typeof time === "string" ? new Date(time) : time;
|
||||||
|
return t.toTimeString().split(" ")[0]; // 只保留时间部分 HH:MM:SS
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (datetime: Date | string | undefined): string | undefined => {
|
||||||
|
if (!datetime) return undefined;
|
||||||
|
const dt = typeof datetime === "string" ? new Date(datetime) : datetime;
|
||||||
|
const datePart = dt.toISOString().split("T")[0];
|
||||||
|
const timePart = dt.toTimeString().split(" ")[0];
|
||||||
|
return `${datePart} ${timePart}`; // 日期时间格式 YYYY-MM-DD HH:MM:SS
|
||||||
|
};
|
||||||
|
|
||||||
|
// 应用格式化
|
||||||
|
submitData.e = formatDate(submitData.e);
|
||||||
|
submitData.f = formatTime(submitData.f);
|
||||||
|
submitData.g = formatDateTime(submitData.g);
|
||||||
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
|
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
|
||||||
const id = formData.id;
|
const id = formData.id;
|
||||||
if (id) {
|
if (id) {
|
||||||
try {
|
try {
|
||||||
await DemoAPI.updateDemo(id, { id, ...formData });
|
await DemoAPI.updateDemo(id, { id, ...submitData });
|
||||||
dialogVisible.visible = false;
|
dialogVisible.visible = false;
|
||||||
resetForm();
|
resetForm();
|
||||||
handleCloseDialog();
|
handleCloseDialog();
|
||||||
@@ -708,7 +937,7 @@ async function handleSubmit() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
await DemoAPI.createDemo(formData);
|
await DemoAPI.createDemo(submitData);
|
||||||
dialogVisible.visible = false;
|
dialogVisible.visible = false;
|
||||||
resetForm();
|
resetForm();
|
||||||
handleCloseDialog();
|
handleCloseDialog();
|
||||||
@@ -435,9 +435,9 @@
|
|||||||
<el-icon><QuestionFilled /></el-icon>
|
<el-icon><QuestionFilled /></el-icon>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<el-input v-model="info.package_name" disabled>
|
<el-input v-model="info.package_name">
|
||||||
<template #prepend>默认不允许修改</template>
|
<template #prepend>接口路径: api/v1/</template>
|
||||||
<template #append>目的为简化生成代码</template>
|
<template #append>/{{ info.business_name }}</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -449,10 +449,7 @@
|
|||||||
<el-icon><QuestionFilled /></el-icon>
|
<el-icon><QuestionFilled /></el-icon>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<el-input v-model="info.module_name" disabled>
|
<el-input v-model="info.module_name" />
|
||||||
<template #prepend>默认不允许修改</template>
|
|
||||||
<template #append>目的为简化生成代码</template>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
@@ -488,10 +485,12 @@
|
|||||||
<el-tree-select
|
<el-tree-select
|
||||||
v-model="info.parent_menu_id"
|
v-model="info.parent_menu_id"
|
||||||
:data="menuOptions"
|
:data="menuOptions"
|
||||||
placeholder="请选择系统菜单, 不选择默认分配到模块管理(id:7)"
|
placeholder="请选择系统菜单,不选创建目录"
|
||||||
check-strictly
|
check-strictly
|
||||||
|
show-checkbox
|
||||||
filterable
|
filterable
|
||||||
:render-after-expand="false"
|
:render-after-expand="false"
|
||||||
|
clearable
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -520,8 +519,8 @@
|
|||||||
<el-descriptions-item :label="info.function_name + '功能,后端序列化层'">
|
<el-descriptions-item :label="info.function_name + '功能,后端序列化层'">
|
||||||
backend/app/api/v1/{{ info.module_name }}/{{ info.business_name }}/schema.py
|
backend/app/api/v1/{{ info.module_name }}/{{ info.business_name }}/schema.py
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item :label="info.function_name + '功能,数据库业务菜单'">
|
<el-descriptions-item :label="info.function_name + '功能,后端初始化'">
|
||||||
backend/sql/menu/{{ info.module_name }}.{{ info.business_name }}.sql
|
backend/app/api/v1/{{ info.module_name }}/{{ info.business_name }}/__init__.py
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item :label="info.function_name + '功能,前端接口层'">
|
<el-descriptions-item :label="info.function_name + '功能,前端接口层'">
|
||||||
frontend/src/api/{{ info.module_name }}/{{ info.business_name }}.ts
|
frontend/src/api/{{ info.module_name }}/{{ info.business_name }}.ts
|
||||||
@@ -748,7 +747,6 @@
|
|||||||
<el-radio-button value="all">全部</el-radio-button>
|
<el-radio-button value="all">全部</el-radio-button>
|
||||||
<el-radio-button value="frontend">前端</el-radio-button>
|
<el-radio-button value="frontend">前端</el-radio-button>
|
||||||
<el-radio-button value="backend">后端</el-radio-button>
|
<el-radio-button value="backend">后端</el-radio-button>
|
||||||
<el-radio-button value="sql">数据库</el-radio-button>
|
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
<span class="ml-3 text-sm color-#909399">类型</span>
|
<span class="ml-3 text-sm color-#909399">类型</span>
|
||||||
<el-checkbox-group v-model="previewTypes" size="small">
|
<el-checkbox-group v-model="previewTypes" size="small">
|
||||||
@@ -965,8 +963,8 @@ const preview = reactive({
|
|||||||
active_name: "controller.py",
|
active_name: "controller.py",
|
||||||
});
|
});
|
||||||
|
|
||||||
const previewScope = ref<"all" | "frontend" | "backend" | "sql">("all");
|
const previewScope = ref<"all" | "frontend" | "backend">("all");
|
||||||
const previewTypeOptions = ["ts", "vue", "python", "sql"];
|
const previewTypeOptions = ["ts", "vue", "python"];
|
||||||
const previewTypes = ref<string[]>([...previewTypeOptions]);
|
const previewTypes = ref<string[]>([...previewTypeOptions]);
|
||||||
const code = ref<string>("");
|
const code = ref<string>("");
|
||||||
const treeData = ref<TreeNode[]>([]);
|
const treeData = ref<TreeNode[]>([]);
|
||||||
@@ -1054,16 +1052,13 @@ const filteredTreeData = computed<TreeNode[]>(() => {
|
|||||||
parentPath.some((part) => part === "frontend" || part === "vue") ||
|
parentPath.some((part) => part === "frontend" || part === "vue") ||
|
||||||
label.includes(".vue") ||
|
label.includes(".vue") ||
|
||||||
label.includes(".ts");
|
label.includes(".ts");
|
||||||
const isSqlDatabase = parentPath.some((part) => part === "sql") || label.includes(".sql");
|
|
||||||
|
|
||||||
if (previewScope.value === "backend" && !isPythonBackend) return false;
|
if (previewScope.value === "backend" && !isPythonBackend) return false;
|
||||||
if (previewScope.value === "frontend" && !isVueFrontend) return false;
|
if (previewScope.value === "frontend" && !isVueFrontend) return false;
|
||||||
if (previewScope.value === "sql" && !isSqlDatabase) return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 类型过滤:根据文件内容特征判断类型
|
// 类型过滤:根据文件内容特征判断类型
|
||||||
if (label.endsWith(".py")) return previewTypes.value.includes("python");
|
if (label.endsWith(".py")) return previewTypes.value.includes("python");
|
||||||
if (label.endsWith(".sql")) return previewTypes.value.includes("sql");
|
|
||||||
if (label.endsWith(".vue")) return previewTypes.value.includes("vue");
|
if (label.endsWith(".vue")) return previewTypes.value.includes("vue");
|
||||||
if (label.endsWith(".ts")) return previewTypes.value.includes("ts");
|
if (label.endsWith(".ts")) return previewTypes.value.includes("ts");
|
||||||
|
|
||||||
@@ -1093,8 +1088,11 @@ const canGenerate = computed(() => ids.value.length > 0);
|
|||||||
|
|
||||||
/** 一键复制代码 */
|
/** 一键复制代码 */
|
||||||
const handleCopyCode = () => {
|
const handleCopyCode = () => {
|
||||||
if (code.value) {
|
// 优先检查代码预览区域的code变量,然后检查创建表对话框的createContent变量
|
||||||
copy(code.value);
|
const content = code.value || createContent.value;
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
copy(content);
|
||||||
ElMessage.success("代码复制成功");
|
ElMessage.success("代码复制成功");
|
||||||
} else {
|
} else {
|
||||||
ElMessage.warning("没有可复制的代码");
|
ElMessage.warning("没有可复制的代码");
|
||||||
@@ -1104,7 +1102,6 @@ const handleCopyCode = () => {
|
|||||||
/** 获取文件树节点图标 */
|
/** 获取文件树节点图标 */
|
||||||
function getFileTreeNodeIcon(label: string): string {
|
function getFileTreeNodeIcon(label: string): string {
|
||||||
if (label.endsWith(".py")) return "python";
|
if (label.endsWith(".py")) return "python";
|
||||||
if (label.endsWith(".sql")) return "sql";
|
|
||||||
if (label.endsWith(".vue")) return "vue";
|
if (label.endsWith(".vue")) return "vue";
|
||||||
if (label.endsWith(".ts")) return "typescript";
|
if (label.endsWith(".ts")) return "typescript";
|
||||||
return "file";
|
return "file";
|
||||||
@@ -1246,9 +1243,7 @@ async function handleGenTable(targetGenType: string, row?: GenTableSchema): Prom
|
|||||||
loading.value = false;
|
loading.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await GencodeAPI.genCodeToPath(tbNames[0]);
|
await GencodeAPI.genCodeToPath(tbNames[0]);
|
||||||
ElMessage.success("代码已生成到指定路径");
|
|
||||||
} else {
|
} else {
|
||||||
// ZIP压缩包下载
|
// ZIP压缩包下载
|
||||||
const tableNamesArray = Array.isArray(tbNames) ? tbNames : [tbNames];
|
const tableNamesArray = Array.isArray(tbNames) ? tbNames : [tbNames];
|
||||||
@@ -1384,55 +1379,55 @@ async function handleDelete(row?: GenTableSchema): Promise<void> {
|
|||||||
/** 加载SQL示例 */
|
/** 加载SQL示例 */
|
||||||
function loadExampleMysql(): void {
|
function loadExampleMysql(): void {
|
||||||
const exampleSql = `-- MySQL SQL案例
|
const exampleSql = `-- MySQL SQL案例
|
||||||
CREATE TABLE \`gen_demo01\` (
|
CREATE TABLE \`gen_demo01\` (
|
||||||
\`name\` varchar(64) DEFAULT NULL COMMENT '名称',
|
\`name\` varchar(64) DEFAULT NULL COMMENT '名称',
|
||||||
\`id\` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
\`id\` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
\`uuid\` varchar(64) NOT NULL COMMENT 'UUID全局唯一标识',
|
\`uuid\` varchar(64) NOT NULL COMMENT 'UUID全局唯一标识',
|
||||||
\`status\` varchar(10) NOT NULL COMMENT '是否启用(0:启用 1:禁用)',
|
\`status\` varchar(10) NOT NULL COMMENT '是否启用(0:启用 1:禁用)',
|
||||||
\`description\` text COMMENT '备注/描述',
|
\`description\` text COMMENT '备注/描述',
|
||||||
\`created_time\` datetime NOT NULL COMMENT '创建时间',
|
\`created_time\` datetime NOT NULL COMMENT '创建时间',
|
||||||
\`updated_time\` datetime NOT NULL COMMENT '更新时间',
|
\`updated_time\` datetime NOT NULL COMMENT '更新时间',
|
||||||
\`created_id\` int DEFAULT NULL COMMENT '创建人ID',
|
\`created_id\` int DEFAULT NULL COMMENT '创建人ID',
|
||||||
\`updated_id\` int DEFAULT NULL COMMENT '更新人ID',
|
\`updated_id\` int DEFAULT NULL COMMENT '更新人ID',
|
||||||
PRIMARY KEY (\`id\`),
|
PRIMARY KEY (\`id\`),
|
||||||
UNIQUE KEY \`uuid\` (\`uuid\`),
|
UNIQUE KEY \`uuid\` (\`uuid\`),
|
||||||
KEY \`ix_gen_demo01_created_id\` (\`created_id\`),
|
KEY \`ix_gen_demo01_created_id\` (\`created_id\`),
|
||||||
KEY \`ix_gen_demo01_updated_id\` (\`updated_id\`),
|
KEY \`ix_gen_demo01_updated_id\` (\`updated_id\`),
|
||||||
CONSTRAINT \`gen_demo01_ibfk_1\` FOREIGN KEY (\`created_id\`) REFERENCES \`sys_user\` (\`id\`) ON DELETE SET NULL ON UPDATE CASCADE,
|
CONSTRAINT \`gen_demo01_ibfk_1\` FOREIGN KEY (\`created_id\`) REFERENCES \`sys_user\` (\`id\`) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
CONSTRAINT \`gen_demo01_ibfk_2\` FOREIGN KEY (\`updated_id\`) REFERENCES \`sys_user\` (\`id\`) ON DELETE SET NULL ON UPDATE CASCADE
|
CONSTRAINT \`gen_demo01_ibfk_2\` FOREIGN KEY (\`updated_id\`) REFERENCES \`sys_user\` (\`id\`) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='示例表'`;
|
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='示例表'`;
|
||||||
createContent.value = exampleSql;
|
createContent.value = exampleSql;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadExamplePostgres(): void {
|
function loadExamplePostgres(): void {
|
||||||
const exampleSql = `-- Psstgres SQL案例
|
const exampleSql = `-- Psstgres SQL案例
|
||||||
CREATE TABLE gen_demo01(
|
CREATE TABLE gen_demo01(
|
||||||
id SERIAL NOT NULL,
|
id SERIAL NOT NULL,
|
||||||
uuid varchar(64) NOT NULL,
|
uuid varchar(64) NOT NULL,
|
||||||
name varchar(64),
|
name varchar(64),
|
||||||
status varchar(10) NOT NULL,
|
status varchar(10) NOT NULL,
|
||||||
description text,
|
description text,
|
||||||
created_time timestamp without time zone NOT NULL,
|
created_time timestamp without time zone NOT NULL,
|
||||||
updated_time timestamp without time zone NOT NULL,
|
updated_time timestamp without time zone NOT NULL,
|
||||||
created_id integer,
|
created_id integer,
|
||||||
updated_id integer,
|
updated_id integer,
|
||||||
PRIMARY KEY(id),
|
PRIMARY KEY(id),
|
||||||
CONSTRAINT gen_demo01_created_id_fkey FOREIGN key(created_id) REFERENCES sys_user(id),
|
CONSTRAINT gen_demo01_created_id_fkey FOREIGN key(created_id) REFERENCES sys_user(id),
|
||||||
CONSTRAINT gen_demo01_updated_id_fkey FOREIGN key(updated_id) REFERENCES sys_user(id)
|
CONSTRAINT gen_demo01_updated_id_fkey FOREIGN key(updated_id) REFERENCES sys_user(id)
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX en_demo01_uuid_key ON public.gen_demo01 USING btree (uuid);
|
CREATE UNIQUE INDEX en_demo01_uuid_key ON public.gen_demo01 USING btree (uuid);
|
||||||
CREATE INDEX ix_gen_demo01_created_id ON public.gen_demo01 USING btree (created_id);
|
CREATE INDEX ix_gen_demo01_created_id ON public.gen_demo01 USING btree (created_id);
|
||||||
CREATE INDEX ix_gen_demo01_updated_id ON public.gen_demo01 USING btree (updated_id);
|
CREATE INDEX ix_gen_demo01_updated_id ON public.gen_demo01 USING btree (updated_id);
|
||||||
COMMENT ON TABLE gen_demo01 IS '示例表';
|
COMMENT ON TABLE gen_demo01 IS '示例表';
|
||||||
COMMENT ON COLUMN gen_demo01.name IS '名称';
|
COMMENT ON COLUMN gen_demo01.name IS '名称';
|
||||||
COMMENT ON COLUMN gen_demo01.id IS '主键ID';
|
COMMENT ON COLUMN gen_demo01.id IS '主键ID';
|
||||||
COMMENT ON COLUMN gen_demo01.uuid IS 'UUID全局唯一标识';
|
COMMENT ON COLUMN gen_demo01.uuid IS 'UUID全局唯一标识';
|
||||||
COMMENT ON COLUMN gen_demo01.status IS '是否启用(0:启用 1:禁用)';
|
COMMENT ON COLUMN gen_demo01.status IS '是否启用(0:启用 1:禁用)';
|
||||||
COMMENT ON COLUMN gen_demo01.description IS '备注/描述';
|
COMMENT ON COLUMN gen_demo01.description IS '备注/描述';
|
||||||
COMMENT ON COLUMN gen_demo01.created_time IS '创建时间';
|
COMMENT ON COLUMN gen_demo01.created_time IS '创建时间';
|
||||||
COMMENT ON COLUMN gen_demo01.updated_time IS '更新时间';
|
COMMENT ON COLUMN gen_demo01.updated_time IS '更新时间';
|
||||||
COMMENT ON COLUMN gen_demo01.created_id IS '创建人ID';
|
COMMENT ON COLUMN gen_demo01.created_id IS '创建人ID';
|
||||||
COMMENT ON COLUMN gen_demo01.updated_id IS '更新人ID';`;
|
COMMENT ON COLUMN gen_demo01.updated_id IS '更新人ID';`;
|
||||||
createContent.value = exampleSql;
|
createContent.value = exampleSql;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1589,9 +1584,10 @@ async function submitForm() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交表单数据,确保columns是必需的
|
// 提交表单数据,确保columns是必需的,并且parent_menu_id总是被包含
|
||||||
const tableData = {
|
const tableData = {
|
||||||
...info,
|
...info,
|
||||||
|
parent_menu_id: info.parent_menu_id ?? null, // 将undefined转换为null,确保属性被传输
|
||||||
columns: info.columns || [], // 确保columns存在
|
columns: info.columns || [], // 确保columns存在
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -216,12 +216,17 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="菜单标题" prop="title" min-width="200" />
|
<el-table-column label="菜单标题" prop="title" min-width="200" />
|
||||||
<el-table-column label="权限标识" prop="permission" min-width="220" />
|
<el-table-column label="权限标识" prop="permission" show-overflow-tooltip min-width="220" />
|
||||||
<el-table-column label="路由名称" prop="route_name" min-width="200" />
|
<el-table-column label="路由名称" prop="route_name" min-width="200" />
|
||||||
<el-table-column label="路由路径" prop="route_path" min-width="200" />
|
<el-table-column label="路由路径" prop="route_path" min-width="200" />
|
||||||
<el-table-column label="组件路径" prop="component_path" min-width="200" />
|
<el-table-column
|
||||||
|
label="组件路径"
|
||||||
|
prop="component_path"
|
||||||
|
show-overflow-tooltip
|
||||||
|
min-width="200"
|
||||||
|
/>
|
||||||
<el-table-column label="路由参数" prop="params" min-width="100" />
|
<el-table-column label="路由参数" prop="params" min-width="100" />
|
||||||
<el-table-column label="描述" prop="description" min-width="200" />
|
<el-table-column label="描述" prop="description" show-overflow-tooltip min-width="200" />
|
||||||
<el-table-column label="创建时间" prop="created_time" min-width="200" sortable />
|
<el-table-column label="创建时间" prop="created_time" min-width="200" sortable />
|
||||||
<el-table-column label="更新时间" prop="updated_time" min-width="200" sortable />
|
<el-table-column label="更新时间" prop="updated_time" min-width="200" sortable />
|
||||||
<el-table-column fixed="right" label="操作" align="center" min-width="260">
|
<el-table-column fixed="right" label="操作" align="center" min-width="260">
|
||||||
|
|||||||
Reference in New Issue
Block a user