upload project source code
This commit is contained in:
2
后端源码/yifan.action-ai.cn/api-bak/app/__init__.py
Normal file
2
后端源码/yifan.action-ai.cn/api-bak/app/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
1
后端源码/yifan.action-ai.cn/api-bak/app/alembic/README
Normal file
1
后端源码/yifan.action-ai.cn/api-bak/app/alembic/README
Normal file
@@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
||||
131
后端源码/yifan.action-ai.cn/api-bak/app/alembic/env.py
Normal file
131
后端源码/yifan.action-ai.cn/api-bak/app/alembic/env.py
Normal file
@@ -0,0 +1,131 @@
|
||||
import asyncio
|
||||
from logging.config import fileConfig
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import pool
|
||||
from alembic import context
|
||||
|
||||
from app.config.path_conf import ALEMBIC_VERSION_DIR
|
||||
from app.utils.import_util import ImportUtil
|
||||
from app.core.base_model import MappedBase
|
||||
from app.config.setting import settings
|
||||
|
||||
# 确保 alembic 版本目录存在
|
||||
ALEMBIC_VERSION_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 清除MappedBase.metadata中的表定义,避免重复注册
|
||||
if hasattr(MappedBase, 'metadata') and MappedBase.metadata.tables:
|
||||
print(f"🧹 清除已存在的表定义,当前有 {len(MappedBase.metadata.tables)} 个表")
|
||||
# 创建一个新的空metadata对象
|
||||
from sqlalchemy import MetaData
|
||||
MappedBase.metadata = MetaData()
|
||||
print("✅️ 已重置metadata")
|
||||
|
||||
# 自动查找所有模型
|
||||
print("🔍 开始查找模型...")
|
||||
found_models = ImportUtil.find_models(MappedBase)
|
||||
print(f"📊 找到 {len(found_models)} 个有效模型")
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
alembic_config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if alembic_config.config_file_name is not None:
|
||||
fileConfig(alembic_config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
target_metadata = MappedBase.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
alembic_config.set_main_option("sqlalchemy.url", settings.ASYNC_DB_URI)
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = alembic_config.get_main_option("sqlalchemy.url")
|
||||
# 确保URL不为None
|
||||
if url is None:
|
||||
raise ValueError("数据库URL未正确配置,请检查环境配置文件")
|
||||
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
url = alembic_config.get_main_option("sqlalchemy.url")
|
||||
# 确保URL不为None
|
||||
if url is None:
|
||||
raise ValueError("数据库URL未正确配置,请检查环境配置文件")
|
||||
|
||||
connectable = create_async_engine(url, poolclass=pool.NullPool)
|
||||
|
||||
async def run_async_migrations():
|
||||
async with connectable.connect() as connection:
|
||||
await connection.run_sync(do_run_migrations)
|
||||
await connectable.dispose()
|
||||
|
||||
def do_run_migrations(connection: Connection) -> None:
|
||||
def process_revision_directives(context, revision, directives):
|
||||
script = directives[0]
|
||||
|
||||
# 检查所有操作集是否为空
|
||||
all_empty = all(ops.is_empty() for ops in script.upgrade_ops_list)
|
||||
|
||||
if all_empty:
|
||||
# 如果没有实际变更,不生成迁移文件
|
||||
directives[:] = []
|
||||
print('❎️ 未检测到模型变更,不生成迁移文件')
|
||||
else:
|
||||
print('✅️ 检测到模型变更,生成迁移文件')
|
||||
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
compare_type=True,
|
||||
compare_server_default=True,
|
||||
transaction_per_migration=True,
|
||||
process_revision_directives=process_revision_directives,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
asyncio.run(run_async_migrations())
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
26
后端源码/yifan.action-ai.cn/api-bak/app/alembic/script.py.mako
Normal file
26
后端源码/yifan.action-ai.cn/api-bak/app/alembic/script.py.mako
Normal file
@@ -0,0 +1,26 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,30 @@
|
||||
"""add success_time to yifan_wx_pay_order
|
||||
|
||||
Revision ID: 7b2c6f1a9d3e
|
||||
Revises: e1db2024739c
|
||||
Create Date: 2026-01-15 10:50:00
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "7b2c6f1a9d3e"
|
||||
down_revision: Union[str, None] = "e1db2024739c"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"yifan_wx_pay_order",
|
||||
sa.Column("success_time", sa.DateTime(), nullable=True, comment="支付成功时间"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("yifan_wx_pay_order", "success_time")
|
||||
@@ -0,0 +1,32 @@
|
||||
"""add inviter_id to sys_user
|
||||
|
||||
Revision ID: 9c4f1d2a6b7e
|
||||
Revises: 7b2c6f1a9d3e
|
||||
Create Date: 2026-01-15 14:15:00
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "9c4f1d2a6b7e"
|
||||
down_revision: Union[str, None] = "7b2c6f1a9d3e"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"sys_user",
|
||||
sa.Column("inviter_id", sa.Integer(), nullable=True, comment="邀请人ID"),
|
||||
)
|
||||
op.create_index(op.f("ix_sys_user_inviter_id"), "sys_user", ["inviter_id"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index(op.f("ix_sys_user_inviter_id"), table_name="sys_user")
|
||||
op.drop_column("sys_user", "inviter_id")
|
||||
@@ -0,0 +1,142 @@
|
||||
"""迁移脚本
|
||||
|
||||
Revision ID: e1db2024739c
|
||||
Revises:
|
||||
Create Date: 2025-12-22 17:27:01.532822
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'e1db2024739c'
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('miniapp_user',
|
||||
sa.Column('openid', sa.String(length=64), nullable=False, comment='微信openid'),
|
||||
sa.Column('unionid', sa.String(length=64), nullable=True, comment='微信unionid'),
|
||||
sa.Column('session_key', sa.String(length=64), nullable=True, comment='会话密钥'),
|
||||
sa.Column('nickname', sa.String(length=64), nullable=True, comment='昵称'),
|
||||
sa.Column('avatar', sa.String(length=512), nullable=True, comment='头像URL'),
|
||||
sa.Column('phone', sa.String(length=20), nullable=True, comment='手机号'),
|
||||
sa.Column('last_login', sa.DateTime(timezone=True), nullable=True, comment='最后登录时间'),
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False, comment='主键ID'),
|
||||
sa.Column('uuid', sa.String(length=64), nullable=False, comment='UUID全局唯一标识'),
|
||||
sa.Column('status', sa.String(length=10), nullable=False, comment='是否启用(0:启用 1:禁用)'),
|
||||
sa.Column('description', sa.Text(), nullable=True, comment='备注/描述'),
|
||||
sa.Column('created_time', sa.DateTime(), nullable=False, comment='创建时间'),
|
||||
sa.Column('updated_time', sa.DateTime(), nullable=False, comment='更新时间'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('unionid'),
|
||||
sa.UniqueConstraint('uuid'),
|
||||
comment='小程序用户表'
|
||||
)
|
||||
op.create_index(op.f('ix_miniapp_user_openid'), 'miniapp_user', ['openid'], unique=True)
|
||||
op.drop_index('ix_apscheduler_jobs_next_run_time', table_name='apscheduler_jobs')
|
||||
op.drop_table('apscheduler_jobs')
|
||||
op.alter_column('gen_table_column', 'is_pk',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
server_default=sa.text('false'),
|
||||
existing_comment='是否主键',
|
||||
existing_nullable=False)
|
||||
op.alter_column('gen_table_column', 'is_increment',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
server_default=sa.text('false'),
|
||||
existing_comment='是否自增',
|
||||
existing_nullable=False)
|
||||
op.alter_column('gen_table_column', 'is_nullable',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
server_default=sa.text('true'),
|
||||
existing_comment='是否允许为空',
|
||||
existing_nullable=False)
|
||||
op.alter_column('gen_table_column', 'is_unique',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
server_default=sa.text('false'),
|
||||
existing_comment='是否唯一',
|
||||
existing_nullable=False)
|
||||
op.alter_column('gen_table_column', 'is_insert',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
server_default=sa.text('true'),
|
||||
existing_comment='是否为新增字段',
|
||||
existing_nullable=False)
|
||||
op.alter_column('gen_table_column', 'is_edit',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
server_default=sa.text('true'),
|
||||
existing_comment='是否编辑字段',
|
||||
existing_nullable=False)
|
||||
op.alter_column('gen_table_column', 'is_list',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
server_default=sa.text('true'),
|
||||
existing_comment='是否列表字段',
|
||||
existing_nullable=False)
|
||||
op.alter_column('gen_table_column', 'is_query',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
server_default=sa.text('false'),
|
||||
existing_comment='是否查询字段',
|
||||
existing_nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('gen_table_column', 'is_query',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
server_default=sa.text("'0'"),
|
||||
existing_comment='是否查询字段',
|
||||
existing_nullable=False)
|
||||
op.alter_column('gen_table_column', 'is_list',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
server_default=sa.text("'1'"),
|
||||
existing_comment='是否列表字段',
|
||||
existing_nullable=False)
|
||||
op.alter_column('gen_table_column', 'is_edit',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
server_default=sa.text("'1'"),
|
||||
existing_comment='是否编辑字段',
|
||||
existing_nullable=False)
|
||||
op.alter_column('gen_table_column', 'is_insert',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
server_default=sa.text("'1'"),
|
||||
existing_comment='是否为新增字段',
|
||||
existing_nullable=False)
|
||||
op.alter_column('gen_table_column', 'is_unique',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
server_default=sa.text("'0'"),
|
||||
existing_comment='是否唯一',
|
||||
existing_nullable=False)
|
||||
op.alter_column('gen_table_column', 'is_nullable',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
server_default=sa.text("'1'"),
|
||||
existing_comment='是否允许为空',
|
||||
existing_nullable=False)
|
||||
op.alter_column('gen_table_column', 'is_increment',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
server_default=sa.text("'0'"),
|
||||
existing_comment='是否自增',
|
||||
existing_nullable=False)
|
||||
op.alter_column('gen_table_column', 'is_pk',
|
||||
existing_type=mysql.TINYINT(display_width=1),
|
||||
server_default=sa.text("'0'"),
|
||||
existing_comment='是否主键',
|
||||
existing_nullable=False)
|
||||
op.create_table('apscheduler_jobs',
|
||||
sa.Column('id', mysql.VARCHAR(length=191), nullable=False),
|
||||
sa.Column('next_run_time', mysql.DOUBLE(asdecimal=True), nullable=True),
|
||||
sa.Column('job_state', sa.BLOB(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
mysql_collate='utf8mb4_0900_ai_ci',
|
||||
mysql_default_charset='utf8mb4',
|
||||
mysql_engine='InnoDB'
|
||||
)
|
||||
op.create_index('ix_apscheduler_jobs_next_run_time', 'apscheduler_jobs', ['next_run_time'], unique=False)
|
||||
op.drop_index(op.f('ix_miniapp_user_openid'), table_name='miniapp_user')
|
||||
op.drop_table('miniapp_user')
|
||||
# ### end Alembic commands ###
|
||||
1
后端源码/yifan.action-ai.cn/api-bak/app/api/__init__.py
Normal file
1
后端源码/yifan.action-ai.cn/api-bak/app/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
1
后端源码/yifan.action-ai.cn/api-bak/app/api/v1/__init__.py
Normal file
1
后端源码/yifan.action-ai.cn/api-bak/app/api/v1/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,537 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Body, WebSocket, Form, UploadFile, File
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
|
||||
from app.common.response import StreamResponse, SuccessResponse
|
||||
from app.common.request import PaginationService
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.logger import log
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from .service import McpService, AIProviderService, EmbeddingConfigService, KnowledgeBaseService, AIModelConfigService, AIModelTrainingService, AIModelTestService
|
||||
from .schema import (
|
||||
McpCreateSchema, McpUpdateSchema, ChatQuerySchema, McpQueryParam,
|
||||
AIProviderCreateSchema, AIProviderUpdateSchema, AIProviderQueryParam,
|
||||
EmbeddingConfigCreateSchema, EmbeddingConfigUpdateSchema, EmbeddingConfigQueryParam,
|
||||
KnowledgeBaseCreateSchema, KnowledgeBaseUpdateSchema, KnowledgeBaseQueryParam,
|
||||
AIModelConfigUpdateSchema, AIModelConfigQueryParam, AIModelTrainingChatSchema,
|
||||
AIModelTestSchema
|
||||
)
|
||||
|
||||
|
||||
AIRouter = APIRouter(route_class=OperationLogRoute, prefix="/ai", tags=["MCP智能助手"])
|
||||
AIProviderRouter = APIRouter(route_class=OperationLogRoute, prefix="/ai/provider", tags=["AI供应商配置"])
|
||||
EmbeddingConfigRouter = APIRouter(route_class=OperationLogRoute, prefix="/ai/embedding", tags=["向量化配置"])
|
||||
KnowledgeBaseRouter = APIRouter(route_class=OperationLogRoute, prefix="/ai/knowledge", tags=["知识库管理"])
|
||||
AIModelConfigRouter = APIRouter(route_class=OperationLogRoute, prefix="/ai/model", tags=["AI模型配置"])
|
||||
|
||||
|
||||
@AIRouter.post("/chat", summary="智能对话", description="与MCP智能助手进行对话")
|
||||
async def chat_controller(
|
||||
query: ChatQuerySchema,
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
智能对话接口
|
||||
|
||||
参数:
|
||||
- query (ChatQuerySchema): 聊天查询模型
|
||||
|
||||
返回:
|
||||
- StreamingResponse: 流式响应,每次返回一个聊天响应
|
||||
"""
|
||||
user_name = auth.user.name if auth.user else "未知用户"
|
||||
log.info(f"用户 {user_name} 发起智能对话: {query.message[:50]}...")
|
||||
|
||||
async def generate_response():
|
||||
try:
|
||||
async for chunk in McpService.chat_query(query=query):
|
||||
# 确保返回的是字节串
|
||||
if chunk:
|
||||
yield chunk.encode('utf-8') if isinstance(chunk, str) else chunk
|
||||
except Exception as e:
|
||||
log.error(f"流式响应出错: {str(e)}")
|
||||
yield f"抱歉,处理您的请求时出现了错误: {str(e)}".encode('utf-8')
|
||||
|
||||
return StreamResponse(generate_response(), media_type="text/plain; charset=utf-8")
|
||||
|
||||
|
||||
@AIRouter.get("/detail/{id}", summary="获取 MCP 服务器详情", description="获取 MCP 服务器详情")
|
||||
async def detail_controller(
|
||||
id: int = Path(..., description="MCP ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取 MCP 服务器详情接口
|
||||
|
||||
参数:
|
||||
- id (int): MCP 服务器ID
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含 MCP 服务器详情的 JSON 响应
|
||||
"""
|
||||
result_dict = await McpService.detail_service(auth=auth, id=id)
|
||||
log.info(f"获取 MCP 服务器详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取 MCP 服务器详情成功")
|
||||
|
||||
|
||||
@AIRouter.get("/list", summary="查询 MCP 服务器列表", description="查询 MCP 服务器列表")
|
||||
async def list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: McpQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询 MCP 服务器列表接口
|
||||
|
||||
参数:
|
||||
- page (PaginationQueryParam): 分页查询参数模型
|
||||
- search (McpQueryParam): 查询参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含 MCP 服务器列表的 JSON 响应
|
||||
"""
|
||||
result_dict_list = await McpService.list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
|
||||
log.info(f"查询 MCP 服务器列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询 MCP 服务器列表成功")
|
||||
|
||||
|
||||
@AIRouter.post("/create", summary="创建 MCP 服务器", description="创建 MCP 服务器")
|
||||
async def create_controller(
|
||||
data: McpCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
创建 MCP 服务器接口
|
||||
|
||||
参数:
|
||||
- data (McpCreateSchema): 创建 MCP 服务器模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含创建 MCP 服务器结果的 JSON 响应
|
||||
"""
|
||||
result_dict = await McpService.create_service(auth=auth, data=data)
|
||||
log.info(f"创建 MCP 服务器成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="创建 MCP 服务器成功")
|
||||
|
||||
|
||||
@AIRouter.put("/update/{id}", summary="修改 MCP 服务器", description="修改 MCP 服务器")
|
||||
async def update_controller(
|
||||
data: McpUpdateSchema,
|
||||
id: int = Path(..., description="MCP ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
修改 MCP 服务器接口
|
||||
|
||||
参数:
|
||||
- data (McpUpdateSchema): 修改 MCP 服务器模型
|
||||
- id (int): MCP 服务器ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含修改 MCP 服务器结果的 JSON 响应
|
||||
"""
|
||||
result_dict = await McpService.update_service(auth=auth, id=id, data=data)
|
||||
log.info(f"修改 MCP 服务器成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="修改 MCP 服务器成功")
|
||||
|
||||
|
||||
@AIRouter.delete("/delete", summary="删除 MCP 服务器", description="删除 MCP 服务器")
|
||||
async def delete_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除 MCP 服务器接口
|
||||
|
||||
参数:
|
||||
- ids (list[int]): MCP 服务器ID列表
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含删除 MCP 服务器结果的 JSON 响应
|
||||
"""
|
||||
await McpService.delete_service(auth=auth, ids=ids)
|
||||
log.info(f"删除 MCP 服务器成功: {ids}")
|
||||
return SuccessResponse(msg="删除 MCP 服务器成功")
|
||||
|
||||
|
||||
@AIRouter.websocket("/ws/chat", name="WebSocket聊天")
|
||||
async def websocket_chat_controller(
|
||||
websocket: WebSocket,
|
||||
):
|
||||
"""
|
||||
WebSocket聊天接口
|
||||
|
||||
ws://127.0.0.1:8001/api/v1/ai/mcp/ws/chat
|
||||
"""
|
||||
await websocket.accept()
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
# 流式发送响应
|
||||
try:
|
||||
async for chunk in McpService.chat_query(query=ChatQuerySchema(message=data)):
|
||||
if chunk:
|
||||
await websocket.send_text(chunk)
|
||||
except Exception as e:
|
||||
log.error(f"处理聊天查询出错: {str(e)}")
|
||||
await websocket.send_text(f"抱歉,处理您的请求时出现了错误: {str(e)}")
|
||||
except Exception as e:
|
||||
log.error(f"WebSocket聊天出错: {str(e)}")
|
||||
finally:
|
||||
await websocket.close()
|
||||
|
||||
|
||||
@AIModelConfigRouter.post("/test", summary="起名测试")
|
||||
async def naming_test_controller(
|
||||
test_data: AIModelTestSchema,
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
起名测试接口(用于小程序/外部调用)
|
||||
|
||||
- 读取模型配置和训练信息
|
||||
- 内部流式调用AI供应商
|
||||
- 组合响应后一次性返回
|
||||
- 仅读数据,不保存对话记录
|
||||
|
||||
参数:
|
||||
- model_type: 模型类型(enterprise_naming/personal_naming等)
|
||||
- text: 用户输入的文本
|
||||
|
||||
返回:
|
||||
- data: AI的完整响应文本
|
||||
"""
|
||||
log.info(f"[起名测试] 收到请求: model_type={test_data.model_type}, text={test_data.text[:50]}...")
|
||||
|
||||
result = await AIModelTestService.test_naming(
|
||||
model_type=test_data.model_type,
|
||||
text=test_data.text
|
||||
)
|
||||
|
||||
log.info(f"[起名测试] 响应完成,长度: {len(result)}")
|
||||
return SuccessResponse(data=result, msg="测试成功")
|
||||
|
||||
|
||||
# ============== AI供应商配置 ==============
|
||||
|
||||
@AIProviderRouter.get("/detail/{id}", summary="获取AI供应商详情")
|
||||
async def provider_detail_controller(
|
||||
id: int = Path(..., description="AI供应商ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
result_dict = await AIProviderService.detail_service(auth=auth, id=id)
|
||||
return SuccessResponse(data=result_dict, msg="获取AI供应商详情成功")
|
||||
|
||||
|
||||
@AIProviderRouter.get("/list", summary="查询AI供应商列表")
|
||||
async def provider_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: AIProviderQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
result_dict_list = await AIProviderService.list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
|
||||
return SuccessResponse(data=result_dict, msg="查询AI供应商列表成功")
|
||||
|
||||
|
||||
@AIProviderRouter.post("/create", summary="创建AI供应商")
|
||||
async def provider_create_controller(
|
||||
data: AIProviderCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
result_dict = await AIProviderService.create_service(auth=auth, data=data)
|
||||
log.info(f"创建AI供应商成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="创建AI供应商成功")
|
||||
|
||||
|
||||
@AIProviderRouter.put("/update/{id}", summary="修改AI供应商")
|
||||
async def provider_update_controller(
|
||||
data: AIProviderUpdateSchema,
|
||||
id: int = Path(..., description="AI供应商ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
result_dict = await AIProviderService.update_service(auth=auth, id=id, data=data)
|
||||
log.info(f"修改AI供应商成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="修改AI供应商成功")
|
||||
|
||||
|
||||
@AIProviderRouter.delete("/delete", summary="删除AI供应商")
|
||||
async def provider_delete_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
await AIProviderService.delete_service(auth=auth, ids=ids)
|
||||
log.info(f"删除AI供应商成功: {ids}")
|
||||
return SuccessResponse(msg="删除AI供应商成功")
|
||||
|
||||
|
||||
# ============== 向量化配置 ==============
|
||||
|
||||
@EmbeddingConfigRouter.get("/detail/{id}", summary="获取向量化配置详情")
|
||||
async def embedding_detail_controller(
|
||||
id: int = Path(..., description="向量化配置ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
result_dict = await EmbeddingConfigService.detail_service(auth=auth, id=id)
|
||||
return SuccessResponse(data=result_dict, msg="获取向量化配置详情成功")
|
||||
|
||||
|
||||
@EmbeddingConfigRouter.get("/list", summary="查询向量化配置列表")
|
||||
async def embedding_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: EmbeddingConfigQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
result_dict_list = await EmbeddingConfigService.list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
|
||||
return SuccessResponse(data=result_dict, msg="查询向量化配置列表成功")
|
||||
|
||||
|
||||
@EmbeddingConfigRouter.post("/create", summary="创建向量化配置")
|
||||
async def embedding_create_controller(
|
||||
data: EmbeddingConfigCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
result_dict = await EmbeddingConfigService.create_service(auth=auth, data=data)
|
||||
log.info(f"创建向量化配置成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="创建向量化配置成功")
|
||||
|
||||
|
||||
@EmbeddingConfigRouter.put("/update/{id}", summary="修改向量化配置")
|
||||
async def embedding_update_controller(
|
||||
data: EmbeddingConfigUpdateSchema,
|
||||
id: int = Path(..., description="向量化配置ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
result_dict = await EmbeddingConfigService.update_service(auth=auth, id=id, data=data)
|
||||
log.info(f"修改向量化配置成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="修改向量化配置成功")
|
||||
|
||||
|
||||
@EmbeddingConfigRouter.delete("/delete", summary="删除向量化配置")
|
||||
async def embedding_delete_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
await EmbeddingConfigService.delete_service(auth=auth, ids=ids)
|
||||
log.info(f"删除向量化配置成功: {ids}")
|
||||
return SuccessResponse(msg="删除向量化配置成功")
|
||||
|
||||
|
||||
# ============== 知识库管理 ==============
|
||||
|
||||
@KnowledgeBaseRouter.get("/detail/{id}", summary="获取知识库详情")
|
||||
async def knowledge_detail_controller(
|
||||
id: int = Path(..., description="知识库ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
result_dict = await KnowledgeBaseService.detail_service(auth=auth, id=id)
|
||||
return SuccessResponse(data=result_dict, msg="获取知识库详情成功")
|
||||
|
||||
|
||||
@KnowledgeBaseRouter.get("/list", summary="查询知识库列表")
|
||||
async def knowledge_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: KnowledgeBaseQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
result_dict_list = await KnowledgeBaseService.list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
|
||||
return SuccessResponse(data=result_dict, msg="查询知识库列表成功")
|
||||
|
||||
|
||||
@KnowledgeBaseRouter.post("/create", summary="创建知识库")
|
||||
async def knowledge_create_controller(
|
||||
name: str = Form(..., max_length=100, description="知识库名称"),
|
||||
embedding_config_id: int | None = Form(None, description="向量化配置ID"),
|
||||
description: str | None = Form(None, max_length=255, description="备注"),
|
||||
files: list[UploadFile] = File(default=[], description="上传的文件列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
# 构造数据对象
|
||||
data = KnowledgeBaseCreateSchema(name=name, embedding_config_id=embedding_config_id, description=description)
|
||||
result_dict = await KnowledgeBaseService.create_service(auth=auth, data=data, files=files)
|
||||
log.info(f"创建知识库成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="创建知识库成功")
|
||||
|
||||
|
||||
@KnowledgeBaseRouter.put("/update/{id}", summary="修改知识库")
|
||||
async def knowledge_update_controller(
|
||||
data: KnowledgeBaseUpdateSchema,
|
||||
id: int = Path(..., description="知识库ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
result_dict = await KnowledgeBaseService.update_service(auth=auth, id=id, data=data)
|
||||
log.info(f"修改知识库成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="修改知识库成功")
|
||||
|
||||
|
||||
@KnowledgeBaseRouter.delete("/delete", summary="删除知识库")
|
||||
async def knowledge_delete_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
await KnowledgeBaseService.delete_service(auth=auth, ids=ids)
|
||||
log.info(f"删除知识库成功: {ids}")
|
||||
return SuccessResponse(msg="删除知识库成功")
|
||||
|
||||
|
||||
@KnowledgeBaseRouter.post("/retry/{id}", summary="重试向量化")
|
||||
async def knowledge_retry_controller(
|
||||
id: int = Path(..., description="知识库ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
result_dict = await KnowledgeBaseService.retry_service(auth=auth, id=id)
|
||||
log.info(f"重试向量化成功: {id}")
|
||||
return SuccessResponse(data=result_dict, msg="已启动重新向量化")
|
||||
|
||||
|
||||
# ============== AI模型配置 ==============
|
||||
|
||||
@AIModelConfigRouter.get("/detail/{model_type}", summary="获取AI模型配置详情")
|
||||
async def model_config_detail_controller(
|
||||
model_type: str = Path(..., description="模型类型(naming/renaming/scoring/report)"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
result_dict = await AIModelConfigService.detail_service(auth=auth, model_type=model_type)
|
||||
return SuccessResponse(data=result_dict, msg="获取AI模型配置详情成功")
|
||||
|
||||
|
||||
@AIModelConfigRouter.get("/list", summary="查询AI模型配置列表")
|
||||
async def model_config_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: AIModelConfigQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
result_dict_list = await AIModelConfigService.list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
|
||||
return SuccessResponse(data=result_dict, msg="查询AI模型配置列表成功")
|
||||
|
||||
|
||||
@AIModelConfigRouter.put("/update/{model_type}", summary="更新AI模型配置")
|
||||
async def model_config_update_controller(
|
||||
data: AIModelConfigUpdateSchema,
|
||||
model_type: str = Path(..., description="模型类型(naming/renaming/scoring/report)"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
result_dict = await AIModelConfigService.update_service(auth=auth, model_type=model_type, data=data)
|
||||
log.info(f"更新AI模型配置成功: {model_type}")
|
||||
return SuccessResponse(data=result_dict, msg="更新AI模型配置成功")
|
||||
|
||||
|
||||
@AIModelConfigRouter.get("/available-models/{provider_id}", summary="获取可用模型列表")
|
||||
async def available_models_controller(
|
||||
provider_id: int = Path(..., description="AI供应商ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
result_list = await AIModelConfigService.get_available_models(auth=auth, provider_id=provider_id)
|
||||
return SuccessResponse(data=result_list, msg="获取可用模型列表成功")
|
||||
|
||||
|
||||
# ============== AI模型训练对话 ==============
|
||||
|
||||
@AIModelConfigRouter.get("/messages/{model_type}", summary="获取训练对话记录")
|
||||
async def training_messages_controller(
|
||||
model_type: str = Path(..., description="模型类型(naming/renaming/scoring/report)"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
result_list = await AIModelTrainingService.get_messages_service(auth=auth, model_type=model_type)
|
||||
return SuccessResponse(data=result_list, msg="获取训练对话记录成功")
|
||||
|
||||
|
||||
@AIModelConfigRouter.delete("/message/{message_id}", summary="删除单条训练对话记录")
|
||||
async def delete_training_message_controller(
|
||||
message_id: int = Path(..., description="对话记录ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
await AIModelTrainingService.delete_message_service(auth=auth, message_id=message_id)
|
||||
log.info(f"删除训练对话记录成功: {message_id}")
|
||||
return SuccessResponse(msg="删除训练对话记录成功")
|
||||
|
||||
|
||||
@AIModelConfigRouter.delete("/messages/{model_type}", summary="清空训练对话记录")
|
||||
async def clear_training_messages_controller(
|
||||
model_type: str = Path(..., description="模型类型(naming/renaming/scoring/report)"),
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> JSONResponse:
|
||||
await AIModelTrainingService.clear_messages_service(auth=auth, model_type=model_type)
|
||||
log.info(f"清空训练对话记录成功: {model_type}")
|
||||
return SuccessResponse(msg="清空训练对话记录成功")
|
||||
|
||||
|
||||
@AIModelConfigRouter.post("/chat", summary="训练对话")
|
||||
async def training_chat_controller(
|
||||
chat_data: AIModelTrainingChatSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission())
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
训练对话接口,流式输出
|
||||
"""
|
||||
log.info(f"[训练对话] 收到请求: model_type={chat_data.model_type}, message={chat_data.message[:50]}...")
|
||||
|
||||
async def generate_response():
|
||||
log.info("[训练对话] 开始生成响应")
|
||||
chunk_count = 0
|
||||
try:
|
||||
async for chunk in AIModelTrainingService.chat_stream(auth=auth, chat_data=chat_data):
|
||||
if chunk:
|
||||
chunk_count += 1
|
||||
yield chunk.encode('utf-8') if isinstance(chunk, str) else chunk
|
||||
log.info(f"[训练对话] 响应生成完成,共 {chunk_count} 个 chunk")
|
||||
except Exception as e:
|
||||
log.error(f"训练对话出错: {str(e)}")
|
||||
yield f"处理您的请求时出现了错误: {str(e)}".encode('utf-8')
|
||||
|
||||
return StreamResponse(generate_response(), media_type="text/plain; charset=utf-8")
|
||||
|
||||
|
||||
@AIModelConfigRouter.websocket("/ws/chat")
|
||||
async def training_websocket_chat_controller(
|
||||
websocket: WebSocket,
|
||||
):
|
||||
"""
|
||||
训练对话 WebSocket 接口
|
||||
"""
|
||||
from app.core.security import verify_token
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
|
||||
await websocket.accept()
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_json()
|
||||
model_type = data.get('model_type', 'naming')
|
||||
message = data.get('message', '')
|
||||
config_changed = data.get('config_changed', False)
|
||||
config_data = data.get('config_data')
|
||||
|
||||
# 构建chat_data
|
||||
chat_data = AIModelTrainingChatSchema(
|
||||
model_type=model_type,
|
||||
message=message,
|
||||
config_changed=config_changed,
|
||||
config_data=AIModelConfigUpdateSchema(**config_data) if config_data else None
|
||||
)
|
||||
|
||||
# 构建简化的auth(WebSocket没有正常的认证流程,实际使用时需要实现认证)
|
||||
auth = AuthSchema()
|
||||
|
||||
# 流式发送响应
|
||||
try:
|
||||
async for chunk in AIModelTrainingService.chat_stream(auth=auth, chat_data=chat_data):
|
||||
if chunk:
|
||||
await websocket.send_text(chunk)
|
||||
except Exception as e:
|
||||
log.error(f"处理训练对话出错: {str(e)}")
|
||||
await websocket.send_text(f"处理您的请求时出现了错误: {str(e)}")
|
||||
except Exception as e:
|
||||
log.error(f"WebSocket训练对话出错: {str(e)}")
|
||||
finally:
|
||||
await websocket.close()
|
||||
@@ -0,0 +1,270 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Sequence, Any
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import McpModel, AIProviderModel, EmbeddingConfigModel, KnowledgeBaseModel, AIModelConfigModel, AIModelTrainingMessageModel
|
||||
from .schema import (
|
||||
McpCreateSchema, McpUpdateSchema,
|
||||
AIProviderCreateSchema, AIProviderUpdateSchema,
|
||||
EmbeddingConfigCreateSchema, EmbeddingConfigUpdateSchema,
|
||||
KnowledgeBaseCreateSchema, KnowledgeBaseUpdateSchema,
|
||||
AIModelConfigCreateSchema, AIModelConfigUpdateSchema,
|
||||
AIModelTrainingMessageCreateSchema
|
||||
)
|
||||
|
||||
|
||||
class McpCRUD(CRUDBase[McpModel, McpCreateSchema, McpUpdateSchema]):
|
||||
"""MCP 服务器数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化CRUD
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
self.auth = auth
|
||||
super().__init__(model=McpModel, auth=auth)
|
||||
|
||||
async def get_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> McpModel | None:
|
||||
"""
|
||||
获取MCP服务器详情
|
||||
|
||||
参数:
|
||||
- id (int): MCP服务器ID
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- McpModel | None: MCP服务器模型实例(如果存在)
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def get_by_name_crud(self, name: str, preload: list[str | Any] | None = None) -> McpModel | None:
|
||||
"""
|
||||
通过名称获取MCP服务器
|
||||
|
||||
参数:
|
||||
- name (str): MCP服务器名称
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Optional[McpModel]: MCP服务器模型实例(如果存在)
|
||||
"""
|
||||
return await self.get(name=name, preload=preload)
|
||||
|
||||
async def get_list_crud(self, search: dict | None = None, order_by: list[dict[str, str]] | None = None, preload: list[str | Any] | None = None) -> Sequence[McpModel]:
|
||||
"""
|
||||
列表查询MCP服务器
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数字典
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[McpModel]: MCP服务器模型实例序列
|
||||
"""
|
||||
return await self.list(search=search or {}, order_by=order_by or [{'id': 'asc'}], preload=preload)
|
||||
|
||||
async def create_crud(self, data: McpCreateSchema) -> McpModel | None:
|
||||
"""
|
||||
创建MCP服务器
|
||||
|
||||
参数:
|
||||
- data (McpCreateSchema): 创建MCP服务器模型
|
||||
|
||||
返回:
|
||||
- Optional[McpModel]: 创建的MCP服务器模型实例(如果成功)
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_crud(self, id: int, data: McpUpdateSchema) -> McpModel | None:
|
||||
"""
|
||||
更新MCP服务器
|
||||
|
||||
参数:
|
||||
- id (int): MCP服务器ID
|
||||
- data (McpUpdateSchema): 更新MCP服务器模型
|
||||
|
||||
返回:
|
||||
- McpModel | None: 更新的MCP服务器模型实例(如果成功)
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
批量删除MCP服务器
|
||||
|
||||
参数:
|
||||
- ids (list[int]): MCP服务器ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
|
||||
class AIProviderCRUD(CRUDBase[AIProviderModel, AIProviderCreateSchema, AIProviderUpdateSchema]):
|
||||
"""AI供应商数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
self.auth = auth
|
||||
super().__init__(model=AIProviderModel, auth=auth)
|
||||
|
||||
async def get_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> AIProviderModel | None:
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def get_by_name_crud(self, name: str, preload: list[str | Any] | None = None) -> AIProviderModel | None:
|
||||
return await self.get(name=name, preload=preload)
|
||||
|
||||
async def get_list_crud(self, search: dict | None = None, order_by: list[dict[str, str]] | None = None, preload: list[str | Any] | None = None) -> Sequence[AIProviderModel]:
|
||||
return await self.list(search=search or {}, order_by=order_by or [{'id': 'asc'}], preload=preload)
|
||||
|
||||
async def create_crud(self, data: AIProviderCreateSchema) -> AIProviderModel | None:
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_crud(self, id: int, data: AIProviderUpdateSchema) -> AIProviderModel | None:
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_crud(self, ids: list[int]) -> None:
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def clear_default_crud(self) -> None:
|
||||
"""清除所有默认设置"""
|
||||
obj_list = await self.list(search={'is_default': 1})
|
||||
for obj in obj_list:
|
||||
await self.update(id=obj.id, data=AIProviderUpdateSchema(
|
||||
name=obj.name,
|
||||
provider_type=obj.provider_type,
|
||||
base_url=obj.base_url,
|
||||
api_key=obj.api_key,
|
||||
is_default=0,
|
||||
description=obj.description
|
||||
))
|
||||
|
||||
|
||||
class EmbeddingConfigCRUD(CRUDBase[EmbeddingConfigModel, EmbeddingConfigCreateSchema, EmbeddingConfigUpdateSchema]):
|
||||
"""向量化配置数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
self.auth = auth
|
||||
super().__init__(model=EmbeddingConfigModel, auth=auth)
|
||||
|
||||
async def get_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> EmbeddingConfigModel | None:
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def get_by_name_crud(self, name: str, preload: list[str | Any] | None = None) -> EmbeddingConfigModel | None:
|
||||
return await self.get(name=name, preload=preload)
|
||||
|
||||
async def get_list_crud(self, search: dict | None = None, order_by: list[dict[str, str]] | None = None, preload: list[str | Any] | None = None) -> Sequence[EmbeddingConfigModel]:
|
||||
return await self.list(search=search or {}, order_by=order_by or [{'id': 'asc'}], preload=preload)
|
||||
|
||||
async def create_crud(self, data: EmbeddingConfigCreateSchema) -> EmbeddingConfigModel | None:
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_crud(self, id: int, data: EmbeddingConfigUpdateSchema) -> EmbeddingConfigModel | None:
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_crud(self, ids: list[int]) -> None:
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def clear_default_crud(self) -> None:
|
||||
"""清除所有默认设置"""
|
||||
obj_list = await self.list(search={'is_default': 1})
|
||||
for obj in obj_list:
|
||||
await self.update(id=obj.id, data=EmbeddingConfigUpdateSchema(
|
||||
name=obj.name,
|
||||
embedding_type=obj.embedding_type,
|
||||
model_name=obj.model_name,
|
||||
base_url=obj.base_url,
|
||||
api_key=obj.api_key,
|
||||
is_default=0,
|
||||
description=obj.description
|
||||
))
|
||||
|
||||
|
||||
class KnowledgeBaseCRUD(CRUDBase[KnowledgeBaseModel, KnowledgeBaseCreateSchema, KnowledgeBaseUpdateSchema]):
|
||||
"""知识库数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
self.auth = auth
|
||||
super().__init__(model=KnowledgeBaseModel, auth=auth)
|
||||
|
||||
async def get_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> KnowledgeBaseModel | None:
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def get_by_name_crud(self, name: str, preload: list[str | Any] | None = None) -> KnowledgeBaseModel | None:
|
||||
return await self.get(name=name, preload=preload)
|
||||
|
||||
async def get_by_collection_crud(self, collection_name: str, preload: list[str | Any] | None = None) -> KnowledgeBaseModel | None:
|
||||
return await self.get(collection_name=collection_name, preload=preload)
|
||||
|
||||
async def get_list_crud(self, search: dict | None = None, order_by: list[dict[str, str]] | None = None, preload: list[str | Any] | None = None) -> Sequence[KnowledgeBaseModel]:
|
||||
return await self.list(search=search or {}, order_by=order_by or [{'id': 'desc'}], preload=preload)
|
||||
|
||||
async def create_crud(self, data: dict) -> KnowledgeBaseModel | None:
|
||||
"""create方法接受字典,因为需要额外字段"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_crud(self, id: int, data: KnowledgeBaseUpdateSchema | dict) -> KnowledgeBaseModel | None:
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_crud(self, ids: list[int]) -> None:
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
|
||||
class AIModelConfigCRUD(CRUDBase[AIModelConfigModel, AIModelConfigCreateSchema, AIModelConfigUpdateSchema]):
|
||||
"""AI模型配置数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
self.auth = auth
|
||||
super().__init__(model=AIModelConfigModel, auth=auth)
|
||||
|
||||
async def get_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> AIModelConfigModel | None:
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def get_by_model_type_crud(self, model_type: str, preload: list[str | Any] | None = None) -> AIModelConfigModel | None:
|
||||
return await self.get(model_type=model_type, preload=preload)
|
||||
|
||||
async def get_list_crud(self, search: dict | None = None, order_by: list[dict[str, str]] | None = None, preload: list[str | Any] | None = None) -> Sequence[AIModelConfigModel]:
|
||||
return await self.list(search=search or {}, order_by=order_by or [{'id': 'asc'}], preload=preload)
|
||||
|
||||
async def create_crud(self, data: AIModelConfigCreateSchema | dict) -> AIModelConfigModel | None:
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_crud(self, id: int, data: AIModelConfigUpdateSchema | dict) -> AIModelConfigModel | None:
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_crud(self, ids: list[int]) -> None:
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
|
||||
class AIModelTrainingMessageCRUD(CRUDBase[AIModelTrainingMessageModel, AIModelTrainingMessageCreateSchema, AIModelTrainingMessageCreateSchema]):
|
||||
"""AI模型训练对话数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
self.auth = auth
|
||||
super().__init__(model=AIModelTrainingMessageModel, auth=auth)
|
||||
|
||||
async def get_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> AIModelTrainingMessageModel | None:
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def get_list_by_config_crud(self, model_config_id: int, preload: list[str | Any] | None = None) -> Sequence[AIModelTrainingMessageModel]:
|
||||
"""获取某个模型配置的所有训练对话"""
|
||||
return await self.list(search={'model_config_id': model_config_id}, order_by=[{'id': 'asc'}], preload=preload)
|
||||
|
||||
async def create_crud(self, data: AIModelTrainingMessageCreateSchema | dict) -> AIModelTrainingMessageModel | None:
|
||||
return await self.create(data=data)
|
||||
|
||||
async def delete_crud(self, ids: list[int]) -> None:
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def delete_by_config_crud(self, model_config_id: int) -> None:
|
||||
"""删除某个模型配置的所有训练对话"""
|
||||
messages = await self.list(search={'model_config_id': model_config_id})
|
||||
if messages:
|
||||
ids = [m.id for m in messages]
|
||||
await self.delete(ids=ids)
|
||||
@@ -0,0 +1,155 @@
|
||||
'''
|
||||
Author: caoziyuan ziyuan.cao@zhuying.com
|
||||
Date: 2025-12-22 17:42:10
|
||||
LastEditors: caoziyuan ziyuan.cao@zhuying.com
|
||||
LastEditTime: 2025-12-22 18:03:28
|
||||
FilePath: \naming-backend\app\api\v1\module_application\ai\model.py
|
||||
Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sqlalchemy import JSON, String, Integer, Text, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class McpModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
MCP 服务器表
|
||||
MCP类型:
|
||||
- 0: stdio (标准输入输出)
|
||||
- 1: sse (Server-Sent Events)
|
||||
"""
|
||||
__tablename__: str = 'app_ai_mcp'
|
||||
__table_args__: dict[str, str] = ({'comment': 'MCP 服务器表'})
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
name: Mapped[str] = mapped_column(String(50), comment='MCP 名称')
|
||||
type: Mapped[int] = mapped_column(Integer, default=0, comment='MCP 类型(0:stdio 1:sse)')
|
||||
url: Mapped[str | None] = mapped_column(String(255), default=None, comment='远程 SSE 地址')
|
||||
command: Mapped[str | None] = mapped_column(String(255), default=None, comment='MCP 命令')
|
||||
args: Mapped[str | None] = mapped_column(String(255), default=None, comment='MCP 命令参数')
|
||||
env: Mapped[dict[str, str] | None] = mapped_column(JSON(), default=None, comment='MCP 环境变量')
|
||||
|
||||
|
||||
class AIProviderModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
AI供应商配置表
|
||||
存储AI服务供应商的接口地址和API Key
|
||||
"""
|
||||
__tablename__: str = 'app_ai_provider'
|
||||
__table_args__: dict[str, str] = ({'comment': 'AI供应商配置表'})
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
name: Mapped[str] = mapped_column(String(50), nullable=False, comment='供应商名称')
|
||||
provider_type: Mapped[str] = mapped_column(String(50), nullable=False, comment='供应商类型(openai/deepseek/anthropic/gemini/qwen等)')
|
||||
base_url: Mapped[str] = mapped_column(String(255), nullable=False, comment='接口地址BaseURL')
|
||||
api_key: Mapped[str] = mapped_column(String(255), nullable=False, comment='API Key')
|
||||
is_default: Mapped[int] = mapped_column(Integer, default=0, comment='是否默认供应商(0:否 1:是)')
|
||||
|
||||
|
||||
class EmbeddingConfigModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
知识库向量化配置表
|
||||
支持本地或远程向量化服务
|
||||
"""
|
||||
__tablename__: str = 'app_ai_embedding_config'
|
||||
__table_args__: dict[str, str] = ({'comment': '知识库向量化配置表'})
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
name: Mapped[str] = mapped_column(String(50), nullable=False, comment='配置名称')
|
||||
embedding_type: Mapped[int] = mapped_column(Integer, default=0, comment='向量化类型(0:本地 1:远程)')
|
||||
model_name: Mapped[str] = mapped_column(String(100), nullable=False, comment='Embedding模型名称')
|
||||
base_url: Mapped[str | None] = mapped_column(String(255), default=None, comment='远程接口地址(远程模式必填)')
|
||||
api_key: Mapped[str | None] = mapped_column(String(255), default=None, comment='远程API Key(远程模式必填)')
|
||||
is_default: Mapped[int] = mapped_column(Integer, default=0, comment='是否默认配置(0:否 1:是)')
|
||||
|
||||
|
||||
class KnowledgeBaseModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
知识库表
|
||||
存储知识库信息,关联向量化配置
|
||||
"""
|
||||
__tablename__: str = 'app_ai_knowledge_base'
|
||||
__table_args__: dict[str, str] = ({'comment': '知识库表'})
|
||||
__loader_options__: list[str] = ["created_by", "updated_by", "embedding_config"]
|
||||
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False, comment='知识库名称')
|
||||
embedding_config_id: Mapped[int | None] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey('app_ai_embedding_config.id', ondelete="SET NULL", onupdate="CASCADE"),
|
||||
default=None,
|
||||
nullable=True,
|
||||
index=True,
|
||||
comment='向量化配置ID'
|
||||
)
|
||||
collection_name: Mapped[str] = mapped_column(String(100), nullable=False, comment='ChromaDB集合名称')
|
||||
document_count: Mapped[int] = mapped_column(Integer, default=0, comment='文档数量')
|
||||
vector_count: Mapped[int] = mapped_column(Integer, default=0, comment='向量数量')
|
||||
kb_status: Mapped[int] = mapped_column(Integer, default=0, comment='知识库状态(0:待处理 1:处理中 2:已完成 3:处理失败)')
|
||||
error_message: Mapped[str | None] = mapped_column(Text, default=None, comment='错误信息')
|
||||
file_paths: Mapped[list[str] | None] = mapped_column(JSON, default=None, comment='文件路径列表')
|
||||
|
||||
# 关联关系
|
||||
embedding_config: Mapped["EmbeddingConfigModel | None"] = relationship(
|
||||
"EmbeddingConfigModel",
|
||||
lazy="selectin",
|
||||
foreign_keys=[embedding_config_id],
|
||||
uselist=False
|
||||
)
|
||||
|
||||
|
||||
class AIModelConfigModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
AI模型配置表
|
||||
存储不同类型AI模型的配置信息
|
||||
模型类型:
|
||||
- enterprise_naming(企业起名), enterprise_renaming(企业改名), enterprise_scoring(企业测名), enterprise_scoring_trial(企业测名试用)
|
||||
- personal_naming(个人起名), personal_renaming(个人改名), personal_scoring(个人测名), personal_scoring_trial(个人测名试用)
|
||||
"""
|
||||
__tablename__: str = 'app_ai_model_config'
|
||||
__table_args__: dict[str, str] = ({'comment': 'AI模型配置表'})
|
||||
__loader_options__: list[str] = ["created_by", "updated_by", "provider", "knowledge_bases"]
|
||||
|
||||
model_type: Mapped[str] = mapped_column(String(50), nullable=False, unique=True, comment='模型类型(enterprise_naming/enterprise_renaming/enterprise_scoring/enterprise_scoring_trial/personal_naming/personal_renaming/personal_scoring/personal_scoring_trial)')
|
||||
model_name: Mapped[str | None] = mapped_column(String(100), default=None, comment='使用的模型名称')
|
||||
provider_id: Mapped[int | None] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey('app_ai_provider.id', ondelete="SET NULL", onupdate="CASCADE"),
|
||||
default=None,
|
||||
nullable=True,
|
||||
index=True,
|
||||
comment='AI供应商ID'
|
||||
)
|
||||
system_prompt: Mapped[str | None] = mapped_column(Text, default=None, comment='系统提示词')
|
||||
temperature: Mapped[float] = mapped_column(default=1.0, comment='模型温度(0-2)')
|
||||
knowledge_base_ids: Mapped[list[int] | None] = mapped_column(JSON, default=None, comment='关联的知识库ID列表')
|
||||
|
||||
# 关联关系
|
||||
provider: Mapped["AIProviderModel | None"] = relationship(
|
||||
"AIProviderModel",
|
||||
lazy="selectin",
|
||||
foreign_keys=[provider_id],
|
||||
uselist=False
|
||||
)
|
||||
|
||||
|
||||
class AIModelTrainingMessageModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
AI模型训练对话记录表
|
||||
存储训练对话的历史记录
|
||||
"""
|
||||
__tablename__: str = 'app_ai_model_training_message'
|
||||
__table_args__: dict[str, str] = ({'comment': 'AI模型训练对话记录表'})
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
model_config_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey('app_ai_model_config.id', ondelete="CASCADE", onupdate="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment='模型配置ID'
|
||||
)
|
||||
role: Mapped[str] = mapped_column(String(20), nullable=False, comment='角色(user/assistant)')
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False, comment='消息内容')
|
||||
@@ -0,0 +1,307 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import ConfigDict, Field, HttpUrl, BaseModel
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.base_schema import BaseSchema
|
||||
from app.common.enums import McpLLMProvider, EmbeddingType, KnowledgeBaseStatus
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
from app.common.enums import McpType
|
||||
from app.core.validator import DateTimeStr
|
||||
|
||||
|
||||
class ChatQuerySchema(BaseModel):
|
||||
"""聊天查询模型"""
|
||||
message: str = Field(..., min_length=1, max_length=4000, description="聊天消息")
|
||||
|
||||
|
||||
class McpCreateSchema(BaseModel):
|
||||
"""创建 MCP 服务器参数"""
|
||||
name: str = Field(..., max_length=64, description='MCP 名称')
|
||||
type: McpType = Field(McpType.stdio, description='MCP 类型')
|
||||
description: str | None = Field(None, max_length=255, description='MCP 描述')
|
||||
url: HttpUrl | None = Field(None, description='远程 SSE 地址')
|
||||
command: str | None = Field(None, max_length=255, description='MCP 命令')
|
||||
args: str | None = Field(None, max_length=255, description='MCP 命令参数,多个参数用英文逗号隔开')
|
||||
env: dict[str, str] | None = Field(None, description='MCP 环境变量')
|
||||
|
||||
|
||||
class McpUpdateSchema(McpCreateSchema):
|
||||
"""更新 MCP 服务器参数"""
|
||||
...
|
||||
|
||||
|
||||
class McpOutSchema(McpCreateSchema, BaseSchema, UserBySchema):
|
||||
"""MCP 服务器详情"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class McpQueryParam:
|
||||
"""MCP 服务器查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = Query(None, description="MCP 名称"),
|
||||
type: McpType | None = Query(None, description="MCP 类型"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
created_id: int | None = Query(None, description="创建人"),
|
||||
updated_id: int | None = Query(None, description="更新人"),
|
||||
) -> None:
|
||||
|
||||
# 模糊查询字段
|
||||
self.name = ("like", name) if name else None
|
||||
|
||||
# 精确查询字段
|
||||
self.type = type
|
||||
self.created_id = created_id
|
||||
self.updated_id = updated_id
|
||||
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
|
||||
|
||||
class McpChatParam(BaseSchema):
|
||||
"""MCP 聊天参数"""
|
||||
pk: list[int] = Field(..., description='MCP ID 列表')
|
||||
provider: McpLLMProvider = Field(McpLLMProvider.openai, description='LLM 供应商')
|
||||
model: str = Field(..., description='LLM 名称')
|
||||
key: str = Field(..., description='LLM API Key')
|
||||
base_url: str | None = Field(None, description='自定义 LLM API 地址,必须兼容 openai 供应商')
|
||||
prompt: str = Field(..., description='用户提示词')
|
||||
|
||||
|
||||
# ============== AI供应商配置 ==============
|
||||
|
||||
class AIProviderCreateSchema(BaseModel):
|
||||
"""创建 AI供应商参数"""
|
||||
name: str = Field(..., max_length=50, description='供应商名称')
|
||||
provider_type: str = Field(..., max_length=50, description='供应商类型(openai/deepseek/anthropic/gemini/qwen等)')
|
||||
base_url: str = Field(..., max_length=255, description='接口地址BaseURL')
|
||||
api_key: str = Field(..., max_length=255, description='API Key')
|
||||
is_default: int = Field(0, description='是否默认供应商(0:否 1:是)')
|
||||
description: str | None = Field(None, max_length=255, description='备注')
|
||||
|
||||
|
||||
class AIProviderUpdateSchema(AIProviderCreateSchema):
|
||||
"""更新 AI供应商参数"""
|
||||
...
|
||||
|
||||
|
||||
class AIProviderOutSchema(AIProviderCreateSchema, BaseSchema, UserBySchema):
|
||||
"""AI供应商详情"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class AIProviderQueryParam:
|
||||
"""AI供应商查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = Query(None, description="供应商名称"),
|
||||
provider_type: str | None = Query(None, description="供应商类型"),
|
||||
is_default: int | None = Query(None, description="是否默认"),
|
||||
) -> None:
|
||||
self.name = ("like", name) if name else None
|
||||
self.provider_type = provider_type
|
||||
self.is_default = is_default
|
||||
|
||||
|
||||
# ============== 向量化配置 ==============
|
||||
|
||||
class EmbeddingConfigCreateSchema(BaseModel):
|
||||
"""创建 向量化配置参数"""
|
||||
name: str = Field(..., max_length=50, description='配置名称')
|
||||
embedding_type: int = Field(0, description='向量化类型(0:本地 1:远程)')
|
||||
model_name: str = Field(..., max_length=100, description='Embedding模型名称')
|
||||
base_url: str | None = Field(None, max_length=255, description='远程接口地址(远程模式必填)')
|
||||
api_key: str | None = Field(None, max_length=255, description='远程API Key(远程模式必填)')
|
||||
is_default: int = Field(0, description='是否默认配置(0:否 1:是)')
|
||||
description: str | None = Field(None, max_length=255, description='备注')
|
||||
|
||||
|
||||
class EmbeddingConfigUpdateSchema(EmbeddingConfigCreateSchema):
|
||||
"""更新 向量化配置参数"""
|
||||
...
|
||||
|
||||
|
||||
class EmbeddingConfigOutSchema(EmbeddingConfigCreateSchema, BaseSchema, UserBySchema):
|
||||
"""向量化配置详情"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class EmbeddingConfigQueryParam:
|
||||
"""向量化配置查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = Query(None, description="配置名称"),
|
||||
embedding_type: int | None = Query(None, description="向量化类型"),
|
||||
is_default: int | None = Query(None, description="是否默认"),
|
||||
) -> None:
|
||||
self.name = ("like", name) if name else None
|
||||
self.embedding_type = embedding_type
|
||||
self.is_default = is_default
|
||||
|
||||
|
||||
# ============== 知识库 ==============
|
||||
|
||||
class KnowledgeBaseCreateSchema(BaseModel):
|
||||
"""创建 知识库参数"""
|
||||
name: str = Field(..., max_length=100, description='知识库名称')
|
||||
embedding_config_id: int | None = Field(None, description='向量化配置ID')
|
||||
description: str | None = Field(None, max_length=255, description='备注')
|
||||
|
||||
|
||||
class KnowledgeBaseUpdateSchema(BaseModel):
|
||||
"""更新 知识库参数"""
|
||||
name: str | None = Field(None, max_length=100, description='知识库名称')
|
||||
embedding_config_id: int | None = Field(None, description='向量化配置ID')
|
||||
description: str | None = Field(None, max_length=255, description='备注')
|
||||
|
||||
|
||||
class EmbeddingConfigRefSchema(BaseModel):
|
||||
"""向量化配置引用"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
name: str
|
||||
embedding_type: int
|
||||
model_name: str
|
||||
|
||||
|
||||
class KnowledgeBaseOutSchema(BaseSchema, UserBySchema):
|
||||
"""知识库详情"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
name: str
|
||||
embedding_config_id: int | None = None
|
||||
collection_name: str
|
||||
document_count: int = 0
|
||||
vector_count: int = 0
|
||||
kb_status: int = 0
|
||||
error_message: str | None = None
|
||||
description: str | None = None
|
||||
file_paths: list[str] | None = None
|
||||
embedding_config: EmbeddingConfigRefSchema | None = None
|
||||
|
||||
|
||||
class KnowledgeBaseQueryParam:
|
||||
"""知识库查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = Query(None, description="知识库名称"),
|
||||
embedding_config_id: int | None = Query(None, description="向量化配置ID"),
|
||||
kb_status: int | None = Query(None, description="知识库状态"),
|
||||
) -> None:
|
||||
self.name = ("like", name) if name else None
|
||||
self.embedding_config_id = embedding_config_id
|
||||
self.kb_status = kb_status
|
||||
|
||||
|
||||
# ============== AI模型配置 ==============
|
||||
|
||||
# AI模型类型常量
|
||||
AI_MODEL_TYPES = {
|
||||
'enterprise_naming': '企业起名',
|
||||
'enterprise_renaming': '企业改名',
|
||||
'enterprise_scoring': '企业测名',
|
||||
'enterprise_scoring_trial': '企业测名试用',
|
||||
'personal_naming': '个人起名',
|
||||
'personal_renaming': '个人改名',
|
||||
'personal_scoring': '个人测名',
|
||||
'personal_scoring_trial': '个人测名试用',
|
||||
'yuanfen_hepan': '缘分合盘',
|
||||
'bazi_zeji': '八字择吉',
|
||||
'caiyun_jiexi': '财运解析',
|
||||
'caiyun_jiexi_qiye': '企业财运解析'
|
||||
}
|
||||
|
||||
|
||||
class AIProviderRefSchema(BaseModel):
|
||||
"""供应商引用"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
name: str
|
||||
provider_type: str
|
||||
base_url: str
|
||||
|
||||
|
||||
class AIModelConfigCreateSchema(BaseModel):
|
||||
"""创建 AI模型配置参数"""
|
||||
model_type: str = Field(..., max_length=50, description='模型类型(naming/renaming/scoring/report)')
|
||||
model_name: str | None = Field(None, max_length=100, description='使用的模型名称')
|
||||
provider_id: int | None = Field(None, description='AI供应商ID')
|
||||
system_prompt: str | None = Field(None, description='系统提示词')
|
||||
temperature: float = Field(1.0, ge=0, le=2, description='模型温度(0-2)')
|
||||
knowledge_base_ids: list[int] | None = Field(None, description='关联的知识库ID列表')
|
||||
|
||||
|
||||
class AIModelConfigUpdateSchema(BaseModel):
|
||||
"""更新 AI模型配置参数"""
|
||||
model_name: str | None = Field(None, max_length=100, description='使用的模型名称')
|
||||
provider_id: int | None = Field(None, description='AI供应商ID')
|
||||
system_prompt: str | None = Field(None, description='系统提示词')
|
||||
temperature: float | None = Field(None, ge=0, le=2, description='模型温度(0-2)')
|
||||
knowledge_base_ids: list[int] | None = Field(None, description='关联的知识库ID列表')
|
||||
|
||||
|
||||
class AIModelConfigOutSchema(BaseSchema, UserBySchema):
|
||||
"""模型配置详情"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
model_type: str
|
||||
model_name: str | None = None
|
||||
provider_id: int | None = None
|
||||
system_prompt: str | None = None
|
||||
temperature: float = 1.0
|
||||
knowledge_base_ids: list[int] | None = None
|
||||
provider: AIProviderRefSchema | None = None
|
||||
|
||||
|
||||
class AIModelConfigQueryParam:
|
||||
"""AI模型配置查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_type: str | None = Query(None, description="模型类型"),
|
||||
) -> None:
|
||||
self.model_type = model_type
|
||||
|
||||
|
||||
# ============== AI模型训练对话 ==============
|
||||
|
||||
class AIModelTrainingMessageCreateSchema(BaseModel):
|
||||
"""创建训练对话消息参数"""
|
||||
model_config_id: int = Field(..., description='模型配置ID')
|
||||
role: str = Field(..., max_length=20, description='角色(user/assistant)')
|
||||
content: str = Field(..., description='消息内容')
|
||||
|
||||
|
||||
class AIModelTrainingMessageOutSchema(BaseSchema, UserBySchema):
|
||||
"""训练对话消息详情"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
model_config_id: int
|
||||
role: str
|
||||
content: str
|
||||
|
||||
|
||||
class AIModelTrainingChatSchema(BaseModel):
|
||||
"""训练对话请求参数"""
|
||||
model_type: str = Field(..., description='模型类型')
|
||||
message: str = Field(..., min_length=1, description='用户消息')
|
||||
# 如果配置有变动,先保存配置
|
||||
config_changed: bool = Field(False, description='配置是否有变动')
|
||||
config_data: AIModelConfigUpdateSchema | None = Field(None, description='变动的配置数据')
|
||||
|
||||
|
||||
class AIModelTestSchema(BaseModel):
|
||||
"""起名测试请求参数(用于小程序/外部调用)"""
|
||||
model_type: str = Field(..., description='模型类型(enterprise_naming/personal_naming等)')
|
||||
text: str = Field(..., min_length=1, max_length=4000, description='用户输入的文本')
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import AsyncGenerator
|
||||
from openai import AsyncOpenAI, OpenAI
|
||||
from openai.types.chat.chat_completion import ChatCompletion
|
||||
import httpx
|
||||
|
||||
from app.config.setting import settings
|
||||
from app.core.logger import log
|
||||
|
||||
|
||||
class AIClient:
|
||||
"""
|
||||
AI客户端类,用于与OpenAI API交互。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.model = settings.OPENAI_MODEL
|
||||
# 创建一个不带冲突参数的httpx客户端
|
||||
self.http_client = httpx.AsyncClient(
|
||||
timeout=30.0,
|
||||
follow_redirects=True
|
||||
)
|
||||
|
||||
# 使用自定义的http客户端
|
||||
self.client = AsyncOpenAI(
|
||||
api_key=settings.OPENAI_API_KEY,
|
||||
base_url=settings.OPENAI_BASE_URL,
|
||||
http_client=self.http_client
|
||||
)
|
||||
|
||||
def _friendly_error_message(self, e: Exception) -> str:
|
||||
"""将 OpenAI 或网络异常转换为友好的中文提示。"""
|
||||
# 尝试获取状态码与错误体
|
||||
status_code = getattr(e, "status_code", None)
|
||||
body = getattr(e, "body", None)
|
||||
message = None
|
||||
error_type = None
|
||||
error_code = None
|
||||
try:
|
||||
if isinstance(body, dict) and "error" in body:
|
||||
err = body.get("error") or {}
|
||||
error_type = err.get("type")
|
||||
error_code = err.get("code")
|
||||
message = err.get("message")
|
||||
except Exception:
|
||||
# 忽略解析失败
|
||||
pass
|
||||
|
||||
text = str(e)
|
||||
msg = message or text
|
||||
|
||||
# 特定错误映射
|
||||
# 欠费/账户状态异常
|
||||
if (error_code == "Arrearage") or (error_type == "Arrearage") or ("in good standing" in (msg or "")):
|
||||
return "账户欠费或结算异常,访问被拒绝。请检查账号状态或更换有效的 API Key。"
|
||||
# 鉴权失败
|
||||
if status_code == 401 or "invalid api key" in msg.lower():
|
||||
return "鉴权失败,API Key 无效或已过期。请检查系统配置中的 API Key。"
|
||||
# 权限不足或被拒绝
|
||||
if status_code == 403 or error_type in {"PermissionDenied", "permission_denied"}:
|
||||
return "访问被拒绝,权限不足或账号受限。请检查账户权限设置。"
|
||||
# 配额不足或限流
|
||||
if status_code == 429 or error_type in {"insufficient_quota", "rate_limit_exceeded"}:
|
||||
return "请求过于频繁或配额已用尽。请稍后重试或提升账户配额。"
|
||||
# 客户端错误
|
||||
if status_code == 400:
|
||||
return f"请求参数错误或服务拒绝:{message or '请检查输入内容。'}"
|
||||
# 服务端错误
|
||||
if status_code in {500, 502, 503, 504}:
|
||||
return "服务暂时不可用,请稍后重试。"
|
||||
|
||||
# 默认兜底
|
||||
return f"处理您的请求时出现错误:{msg}"
|
||||
|
||||
async def process(self, query: str) -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
处理查询并返回流式响应
|
||||
|
||||
参数:
|
||||
- query (str): 用户查询。
|
||||
|
||||
返回:
|
||||
- AsyncGenerator[str, None]: 流式响应内容。
|
||||
"""
|
||||
system_prompt = """你是一个有用的AI助手,可以帮助用户回答问题和提供帮助。请用中文回答用户的问题。"""
|
||||
|
||||
try:
|
||||
# 使用 await 调用异步客户端
|
||||
response = await self.client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": query}
|
||||
],
|
||||
stream=True
|
||||
)
|
||||
|
||||
# 流式返回响应
|
||||
async for chunk in response:
|
||||
if chunk.choices and chunk.choices[0].delta.content:
|
||||
yield chunk.choices[0].delta.content
|
||||
|
||||
except Exception as e:
|
||||
# 记录详细错误,返回友好提示
|
||||
log.error(f"AI处理查询失败: {str(e)}")
|
||||
yield self._friendly_error_message(e)
|
||||
|
||||
async def close(self) -> None:
|
||||
"""
|
||||
关闭客户端连接
|
||||
"""
|
||||
if hasattr(self, 'client'):
|
||||
await self.client.close()
|
||||
if hasattr(self, 'http_client'):
|
||||
await self.http_client.aclose()
|
||||
@@ -0,0 +1,252 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
文档处理工具类
|
||||
支持 txt、pdf、doc、docx、md 格式解析
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, BinaryIO
|
||||
|
||||
from langchain.schema import Document
|
||||
from fastapi import UploadFile
|
||||
|
||||
from app.core.logger import log
|
||||
|
||||
|
||||
class DocumentProcessor:
|
||||
"""文档处理器"""
|
||||
|
||||
# 支持的文件扩展名
|
||||
SUPPORTED_EXTENSIONS = {'.txt', '.pdf', '.doc', '.docx', '.md'}
|
||||
|
||||
@classmethod
|
||||
def is_supported(cls, filename: str) -> bool:
|
||||
"""检查文件是否支持"""
|
||||
ext = Path(filename).suffix.lower()
|
||||
return ext in cls.SUPPORTED_EXTENSIONS
|
||||
|
||||
@classmethod
|
||||
async def process_upload_file(cls, file: UploadFile) -> List[Document]:
|
||||
"""
|
||||
处理上传的文件
|
||||
|
||||
参数:
|
||||
- file: FastAPI UploadFile 对象
|
||||
|
||||
返回:
|
||||
- 文档列表
|
||||
"""
|
||||
if not file.filename:
|
||||
log.warning("文件名为空,跳过处理")
|
||||
return []
|
||||
|
||||
ext = Path(file.filename).suffix.lower()
|
||||
|
||||
if not cls.is_supported(file.filename):
|
||||
log.warning(f"不支持的文件类型: {ext}")
|
||||
return []
|
||||
|
||||
# 读取文件内容
|
||||
content = await file.read()
|
||||
await file.seek(0) # 重置文件指针
|
||||
|
||||
# 根据文件类型处理
|
||||
try:
|
||||
if ext == '.txt':
|
||||
return cls._process_txt(content, file.filename)
|
||||
elif ext == '.md':
|
||||
return cls._process_markdown(content, file.filename)
|
||||
elif ext == '.pdf':
|
||||
return await cls._process_pdf(content, file.filename)
|
||||
elif ext in {'.doc', '.docx'}:
|
||||
return await cls._process_word(content, file.filename, ext)
|
||||
else:
|
||||
log.warning(f"未知的文件类型: {ext}")
|
||||
return []
|
||||
except Exception as e:
|
||||
log.error(f"处理文件失败: {file.filename}, 错误: {e}")
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def _process_txt(cls, content: bytes, filename: str) -> List[Document]:
|
||||
"""处理 TXT 文件"""
|
||||
try:
|
||||
# 尝试不同编码
|
||||
text = None
|
||||
for encoding in ['utf-8', 'gbk', 'gb2312', 'latin-1']:
|
||||
try:
|
||||
text = content.decode(encoding)
|
||||
break
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
if text is None:
|
||||
log.error(f"无法解码文件: {filename}")
|
||||
return []
|
||||
|
||||
return [Document(
|
||||
page_content=text,
|
||||
metadata={"source": filename, "type": "txt"}
|
||||
)]
|
||||
except Exception as e:
|
||||
log.error(f"处理 TXT 文件失败: {filename}, 错误: {e}")
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def _process_markdown(cls, content: bytes, filename: str) -> List[Document]:
|
||||
"""处理 Markdown 文件"""
|
||||
try:
|
||||
text = content.decode('utf-8')
|
||||
return [Document(
|
||||
page_content=text,
|
||||
metadata={"source": filename, "type": "markdown"}
|
||||
)]
|
||||
except Exception as e:
|
||||
log.error(f"处理 Markdown 文件失败: {filename}, 错误: {e}")
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
async def _process_pdf(cls, content: bytes, filename: str) -> List[Document]:
|
||||
"""处理 PDF 文件"""
|
||||
try:
|
||||
# 使用 pypdf 或 pdfplumber 处理 PDF
|
||||
import pypdf
|
||||
from io import BytesIO
|
||||
|
||||
pdf_file = BytesIO(content)
|
||||
reader = pypdf.PdfReader(pdf_file)
|
||||
|
||||
documents = []
|
||||
for page_num, page in enumerate(reader.pages):
|
||||
text = page.extract_text()
|
||||
if text and text.strip():
|
||||
documents.append(Document(
|
||||
page_content=text,
|
||||
metadata={
|
||||
"source": filename,
|
||||
"type": "pdf",
|
||||
"page": page_num + 1
|
||||
}
|
||||
))
|
||||
|
||||
log.info(f"PDF 文件处理完成: {filename}, 共 {len(documents)} 页")
|
||||
return documents
|
||||
|
||||
except ImportError:
|
||||
log.error("未安装 pypdf 库,请运行: pip install pypdf")
|
||||
return []
|
||||
except Exception as e:
|
||||
log.error(f"处理 PDF 文件失败: {filename}, 错误: {e}")
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
async def _process_word(cls, content: bytes, filename: str, ext: str) -> List[Document]:
|
||||
"""处理 Word 文件 (doc/docx)"""
|
||||
try:
|
||||
if ext == '.docx':
|
||||
return cls._process_docx(content, filename)
|
||||
else:
|
||||
# .doc 格式需要特殊处理
|
||||
return cls._process_doc(content, filename)
|
||||
except Exception as e:
|
||||
log.error(f"处理 Word 文件失败: {filename}, 错误: {e}")
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def _process_docx(cls, content: bytes, filename: str) -> List[Document]:
|
||||
"""处理 DOCX 文件"""
|
||||
try:
|
||||
from docx import Document as DocxDocument
|
||||
from io import BytesIO
|
||||
|
||||
docx_file = BytesIO(content)
|
||||
doc = DocxDocument(docx_file)
|
||||
|
||||
# 提取所有段落文本
|
||||
paragraphs = []
|
||||
for para in doc.paragraphs:
|
||||
if para.text.strip():
|
||||
paragraphs.append(para.text)
|
||||
|
||||
text = '\n'.join(paragraphs)
|
||||
|
||||
if text.strip():
|
||||
return [Document(
|
||||
page_content=text,
|
||||
metadata={"source": filename, "type": "docx"}
|
||||
)]
|
||||
return []
|
||||
|
||||
except ImportError:
|
||||
log.error("未安装 python-docx 库,请运行: pip install python-docx")
|
||||
return []
|
||||
except Exception as e:
|
||||
log.error(f"处理 DOCX 文件失败: {filename}, 错误: {e}")
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def _process_doc(cls, content: bytes, filename: str) -> List[Document]:
|
||||
"""
|
||||
处理 DOC 文件 (旧版 Word 格式)
|
||||
注意: .doc 格式处理需要额外依赖,这里做简单提示
|
||||
"""
|
||||
try:
|
||||
# 尝试使用 antiword 或 textract
|
||||
# 如果没有安装,建议用户转换为 docx 格式
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
# 创建临时文件
|
||||
with tempfile.NamedTemporaryFile(suffix='.doc', delete=False) as tmp:
|
||||
tmp.write(content)
|
||||
tmp_path = tmp.name
|
||||
|
||||
try:
|
||||
# 尝试使用 antiword
|
||||
result = subprocess.run(
|
||||
['antiword', tmp_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return [Document(
|
||||
page_content=result.stdout,
|
||||
metadata={"source": filename, "type": "doc"}
|
||||
)]
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
log.warning(f"无法处理 .doc 文件: {filename},建议转换为 .docx 格式")
|
||||
finally:
|
||||
os.unlink(tmp_path)
|
||||
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"处理 DOC 文件失败: {filename}, 错误: {e}")
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
async def process_files(cls, files: List[UploadFile]) -> List[Document]:
|
||||
"""
|
||||
批量处理上传的文件
|
||||
|
||||
参数:
|
||||
- files: UploadFile 列表
|
||||
|
||||
返回:
|
||||
- 所有文档列表
|
||||
"""
|
||||
all_documents = []
|
||||
|
||||
for file in files:
|
||||
if file.filename and cls.is_supported(file.filename):
|
||||
docs = await cls.process_upload_file(file)
|
||||
all_documents.extend(docs)
|
||||
log.info(f"文件处理完成: {file.filename}, 提取 {len(docs)} 个文档")
|
||||
else:
|
||||
log.warning(f"跳过不支持的文件: {file.filename}")
|
||||
|
||||
log.info(f"批量处理完成,共 {len(all_documents)} 个文档")
|
||||
return all_documents
|
||||
@@ -0,0 +1,239 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
向量化工具类
|
||||
支持本地和远程 embedding 模型
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import List, Optional
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import chromadb
|
||||
from langchain_openai import OpenAIEmbeddings
|
||||
from langchain_chroma import Chroma
|
||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
from langchain.schema import Document
|
||||
|
||||
from app.core.logger import log
|
||||
from app.config.path_conf import BASE_DIR
|
||||
|
||||
|
||||
# ChromaDB 持久化目录
|
||||
CHROMA_PERSIST_DIR = BASE_DIR / "data" / "chroma_db"
|
||||
|
||||
# 全局 ChromaDB 客户端实例(单例模式)
|
||||
_chroma_client = None
|
||||
|
||||
|
||||
def get_chroma_client():
|
||||
"""获取全局 ChromaDB 客户端实例(单例模式)"""
|
||||
global _chroma_client
|
||||
if _chroma_client is None:
|
||||
# 确保持久化目录存在
|
||||
CHROMA_PERSIST_DIR.mkdir(parents=True, exist_ok=True)
|
||||
_chroma_client = chromadb.PersistentClient(
|
||||
path=str(CHROMA_PERSIST_DIR)
|
||||
)
|
||||
return _chroma_client
|
||||
|
||||
|
||||
class EmbeddingUtil:
|
||||
"""向量化工具类"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
embedding_type: int = 0,
|
||||
model_name: str = "text-embedding-ada-002",
|
||||
base_url: Optional[str] = None,
|
||||
api_key: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
初始化向量化工具
|
||||
|
||||
参数:
|
||||
- embedding_type: 0=本地, 1=远程
|
||||
- model_name: Embedding模型名称
|
||||
- base_url: 远程接口地址(远程模式必填)
|
||||
- api_key: 远程API Key(远程模式必填)
|
||||
"""
|
||||
self.embedding_type = embedding_type
|
||||
self.model_name = model_name
|
||||
self.base_url = base_url
|
||||
self.api_key = api_key
|
||||
|
||||
# 初始化 embedding 模型
|
||||
self._embeddings = None
|
||||
|
||||
@property
|
||||
def embeddings(self):
|
||||
"""延迟加载 embedding 模型"""
|
||||
if self._embeddings is None:
|
||||
self._embeddings = self._create_embeddings()
|
||||
return self._embeddings
|
||||
|
||||
def _create_embeddings(self):
|
||||
"""创建 embedding 模型实例"""
|
||||
if self.embedding_type == 0:
|
||||
# 本地模式 - 使用 sentence-transformers
|
||||
# sentence-transformers==3.3.1
|
||||
# 本地Embedding模型(可选)手动安装 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
||||
try:
|
||||
from langchain_community.embeddings import HuggingFaceEmbeddings
|
||||
log.info(f"使用本地 Embedding 模型: {self.model_name}")
|
||||
return HuggingFaceEmbeddings(
|
||||
model_name=self.model_name,
|
||||
model_kwargs={'device': 'cpu'},
|
||||
encode_kwargs={'normalize_embeddings': True}
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(f"加载本地 Embedding 模型失败: {e}")
|
||||
raise
|
||||
else:
|
||||
# 远程模式 - 使用 OpenAI 兼容接口
|
||||
if not self.base_url or not self.api_key:
|
||||
raise ValueError("远程模式必须提供 base_url 和 api_key")
|
||||
|
||||
# 自动拼接 /v1 路径(如果未包含)
|
||||
api_base = self.base_url.rstrip('/')
|
||||
if not api_base.endswith('/v1'):
|
||||
api_base = f"{api_base}/v1"
|
||||
|
||||
log.info(f"使用远程 Embedding 模型: {self.model_name}, URL: {api_base}")
|
||||
return OpenAIEmbeddings(
|
||||
model=self.model_name,
|
||||
base_url=api_base,
|
||||
api_key=self.api_key,
|
||||
)
|
||||
|
||||
def get_vector_store(self, collection_name: str) -> Chroma:
|
||||
"""
|
||||
获取或创建向量存储
|
||||
|
||||
参数:
|
||||
- collection_name: 集合名称
|
||||
|
||||
返回:
|
||||
- Chroma 向量存储实例
|
||||
"""
|
||||
# 确保目录存在
|
||||
CHROMA_PERSIST_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
return Chroma(
|
||||
collection_name=collection_name,
|
||||
embedding_function=self.embeddings,
|
||||
persist_directory=str(CHROMA_PERSIST_DIR),
|
||||
client=get_chroma_client(),
|
||||
)
|
||||
|
||||
def split_documents(
|
||||
self,
|
||||
documents: List[Document],
|
||||
chunk_size: int = 1000,
|
||||
chunk_overlap: int = 200
|
||||
) -> List[Document]:
|
||||
"""
|
||||
分割文档
|
||||
|
||||
参数:
|
||||
- documents: 文档列表
|
||||
- chunk_size: 分块大小
|
||||
- chunk_overlap: 分块重叠大小
|
||||
|
||||
返回:
|
||||
- 分割后的文档列表
|
||||
"""
|
||||
text_splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=chunk_size,
|
||||
chunk_overlap=chunk_overlap,
|
||||
length_function=len,
|
||||
separators=["\n\n", "\n", "。", "!", "?", ".", "!", "?", " ", ""]
|
||||
)
|
||||
return text_splitter.split_documents(documents)
|
||||
|
||||
def add_documents(
|
||||
self,
|
||||
collection_name: str,
|
||||
documents: List[Document]
|
||||
) -> int:
|
||||
"""
|
||||
添加文档到向量存储
|
||||
|
||||
参数:
|
||||
- collection_name: 集合名称
|
||||
- documents: 文档列表
|
||||
|
||||
返回:
|
||||
- 添加的向量数量
|
||||
"""
|
||||
if not documents:
|
||||
return 0
|
||||
|
||||
# 分割文档
|
||||
split_docs = self.split_documents(documents)
|
||||
log.info(f"文档分割完成,共 {len(split_docs)} 个片段")
|
||||
|
||||
# 获取向量存储
|
||||
vector_store = self.get_vector_store(collection_name)
|
||||
|
||||
# 添加文档
|
||||
vector_store.add_documents(split_docs)
|
||||
log.info(f"向量存储完成,集合: {collection_name}, 向量数: {len(split_docs)}")
|
||||
|
||||
return len(split_docs)
|
||||
|
||||
def delete_collection(self, collection_name: str) -> bool:
|
||||
"""
|
||||
删除集合
|
||||
|
||||
参数:
|
||||
- collection_name: 集合名称
|
||||
|
||||
返回:
|
||||
- 是否删除成功
|
||||
"""
|
||||
try:
|
||||
client = get_chroma_client()
|
||||
client.delete_collection(collection_name)
|
||||
log.info(f"删除集合成功: {collection_name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
log.error(f"删除集合失败: {collection_name}, 错误: {e}")
|
||||
return False
|
||||
|
||||
def similarity_search(
|
||||
self,
|
||||
collection_name: str,
|
||||
query: str,
|
||||
k: int = 4
|
||||
) -> List[Document]:
|
||||
"""
|
||||
相似度搜索
|
||||
|
||||
参数:
|
||||
- collection_name: 集合名称
|
||||
- query: 查询文本
|
||||
- k: 返回结果数量
|
||||
|
||||
返回:
|
||||
- 相似文档列表
|
||||
"""
|
||||
vector_store = self.get_vector_store(collection_name)
|
||||
return vector_store.similarity_search(query, k=k)
|
||||
|
||||
def get_collection_count(self, collection_name: str) -> int:
|
||||
"""
|
||||
获取集合中的向量数量
|
||||
|
||||
参数:
|
||||
- collection_name: 集合名称
|
||||
|
||||
返回:
|
||||
- 向量数量
|
||||
"""
|
||||
try:
|
||||
client = get_chroma_client()
|
||||
collection = client.get_collection(collection_name)
|
||||
return collection.count()
|
||||
except Exception as e:
|
||||
log.warning(f"获取集合数量失败: {collection_name}, 错误: {e}")
|
||||
return 0
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
|
||||
from app.common.response import StreamResponse, SuccessResponse
|
||||
from app.common.request import PaginationService
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.logger import log
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .tools.ap_scheduler import SchedulerUtil
|
||||
from .service import JobService, JobLogService
|
||||
from .schema import (
|
||||
JobCreateSchema,
|
||||
JobUpdateSchema,
|
||||
JobQueryParam,
|
||||
JobLogQueryParam
|
||||
)
|
||||
|
||||
|
||||
JobRouter = APIRouter(route_class=OperationLogRoute, prefix="/job", tags=["定时任务"])
|
||||
|
||||
@JobRouter.get("/detail/{id}", summary="获取定时任务详情", description="获取定时任务详情")
|
||||
async def get_obj_detail_controller(
|
||||
id: int = Path(..., description="定时任务ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:query"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取定时任务详情
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含定时任务详情的JSON响应
|
||||
"""
|
||||
result_dict = await JobService.get_job_detail_service(id=id, auth=auth)
|
||||
log.info(f"获取定时任务详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取定时任务详情成功")
|
||||
|
||||
@JobRouter.get("/list", summary="查询定时任务", description="查询定时任务")
|
||||
async def get_obj_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: JobQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:query"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询定时任务
|
||||
|
||||
参数:
|
||||
- page (PaginationQueryParam): 分页查询参数模型
|
||||
- search (JobQueryParam): 查询参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含分页后的定时任务列表的JSON响应
|
||||
"""
|
||||
result_dict_list = await JobService.get_job_list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size)
|
||||
log.info(f"查询定时任务列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询定时任务列表成功")
|
||||
|
||||
@JobRouter.post("/create", summary="创建定时任务", description="创建定时任务")
|
||||
async def create_obj_controller(
|
||||
data: JobCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:create"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
创建定时任务
|
||||
|
||||
参数:
|
||||
- data (JobCreateSchema): 创建参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含创建定时任务结果的JSON响应
|
||||
"""
|
||||
result_dict = await JobService.create_job_service(auth=auth, data=data)
|
||||
log.info(f"创建定时任务成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="创建定时任务成功")
|
||||
|
||||
@JobRouter.put("/update/{id}", summary="修改定时任务", description="修改定时任务")
|
||||
async def update_obj_controller(
|
||||
data: JobUpdateSchema,
|
||||
id: int = Path(..., description="定时任务ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:update"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
修改定时任务
|
||||
|
||||
参数:
|
||||
- data (JobUpdateSchema): 更新参数模型
|
||||
- id (int): 定时任务ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含修改定时任务结果的JSON响应
|
||||
"""
|
||||
result_dict = await JobService.update_job_service(auth=auth, id=id, data=data)
|
||||
log.info(f"修改定时任务成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="修改定时任务成功")
|
||||
|
||||
@JobRouter.delete("/delete", summary="删除定时任务", description="删除定时任务")
|
||||
async def delete_obj_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除定时任务
|
||||
|
||||
参数:
|
||||
- ids (list[int]): ID列表
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含删除定时任务结果的JSON响应
|
||||
"""
|
||||
await JobService.delete_job_service(auth=auth, ids=ids)
|
||||
log.info(f"删除定时任务成功: {ids}")
|
||||
return SuccessResponse(msg="删除定时任务成功")
|
||||
|
||||
@JobRouter.post('/export', summary="导出定时任务", description="导出定时任务")
|
||||
async def export_obj_list_controller(
|
||||
search: JobQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:export"]))
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
导出定时任务
|
||||
|
||||
参数:
|
||||
- search (JobQueryParam): 查询参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- StreamingResponse: 包含导出定时任务结果的流式响应
|
||||
"""
|
||||
result_dict_list = await JobService.get_job_list_service(search=search, auth=auth)
|
||||
export_result = await JobService.export_job_service(data_list=result_dict_list)
|
||||
log.info('导出定时任务成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers = {
|
||||
'Content-Disposition': 'attachment; filename=job.xlsx'
|
||||
}
|
||||
)
|
||||
|
||||
@JobRouter.delete("/clear", summary="清空定时任务日志", description="清空定时任务日志")
|
||||
async def clear_obj_log_controller(
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
清空定时任务日志
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含清空定时任务结果的JSON响应
|
||||
"""
|
||||
await JobService.clear_job_service(auth=auth)
|
||||
log.info(f"清空定时任务成功")
|
||||
return SuccessResponse(msg="清空定时任务成功")
|
||||
|
||||
@JobRouter.put("/option", summary="暂停/恢复/重启定时任务", description="暂停/恢复/重启定时任务")
|
||||
async def option_obj_controller(
|
||||
id: int = Body(..., description="定时任务ID"),
|
||||
option: int = Body(..., description="操作类型 1: 暂停 2: 恢复 3: 重启"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:update"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
暂停/恢复/重启定时任务
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务ID
|
||||
- option (int): 操作类型 1: 暂停 2: 恢复 3: 重启
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含操作定时任务结果的JSON响应
|
||||
"""
|
||||
await JobService.option_job_service(auth=auth, id=id, option=option)
|
||||
log.info(f"操作定时任务成功: {id}")
|
||||
return SuccessResponse(msg="操作定时任务成功")
|
||||
|
||||
@JobRouter.get("/log", summary="获取定时任务日志", description="获取定时任务日志", dependencies=[Depends(AuthPermission(["module_application:job:query"]))])
|
||||
async def get_job_log_controller():
|
||||
"""
|
||||
获取定时任务日志
|
||||
|
||||
返回:
|
||||
- JSONResponse: 获取定时任务日志的JSON响应
|
||||
"""
|
||||
data = [
|
||||
{
|
||||
"id": i.id,
|
||||
"name": i.name,
|
||||
"trigger": i.trigger.__class__.__name__,
|
||||
"executor": i.executor,
|
||||
"func": i.func,
|
||||
"func_ref": i.func_ref,
|
||||
"args": i.args,
|
||||
"kwargs": i.kwargs,
|
||||
"misfire_grace_time": i.misfire_grace_time,
|
||||
"coalesce": i.coalesce,
|
||||
"max_instances": i.max_instances,
|
||||
"next_run_time": i.next_run_time,
|
||||
"state": SchedulerUtil.get_single_job_status(job_id=i.id)
|
||||
}
|
||||
for i in SchedulerUtil.get_all_jobs()
|
||||
]
|
||||
|
||||
return SuccessResponse(msg="获取定时任务日志成功", data=data)
|
||||
|
||||
|
||||
# 定时任务日志管理接口
|
||||
@JobRouter.get("/log/detail/{id}", summary="获取定时任务日志详情", description="获取定时任务日志详情")
|
||||
async def get_job_log_detail_controller(
|
||||
id: int = Path(..., description="定时任务日志ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:query"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取定时任务日志详情
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务日志ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 获取定时任务日志详情的JSON响应
|
||||
"""
|
||||
result_dict = await JobLogService.get_job_log_detail_service(id=id, auth=auth)
|
||||
log.info(f"获取定时任务日志详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取定时任务日志详情成功")
|
||||
|
||||
|
||||
@JobRouter.get("/log/list", summary="查询定时任务日志", description="查询定时任务日志")
|
||||
async def get_job_log_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: JobLogQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:query"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询定时任务日志
|
||||
|
||||
参数:
|
||||
- page (PaginationQueryParam): 分页查询参数模型
|
||||
- search (JobLogQueryParam): 查询参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 查询定时任务日志列表的JSON响应
|
||||
"""
|
||||
order_by = [{"created_time": "desc"}]
|
||||
result_dict_list = await JobLogService.get_job_log_list_service(auth=auth, search=search, order_by=order_by)
|
||||
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
|
||||
log.info(f"查询定时任务日志列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询定时任务日志列表成功")
|
||||
|
||||
|
||||
@JobRouter.delete("/log/delete", summary="删除定时任务日志", description="删除定时任务日志")
|
||||
async def delete_job_log_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除定时任务日志
|
||||
|
||||
参数:
|
||||
- ids (list[int]): ID列表
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含删除定时任务日志结果的JSON响应
|
||||
"""
|
||||
await JobLogService.delete_job_log_service(auth=auth, ids=ids)
|
||||
log.info(f"删除定时任务日志成功: {ids}")
|
||||
return SuccessResponse(msg="删除定时任务日志成功")
|
||||
|
||||
|
||||
@JobRouter.delete("/log/clear", summary="清空定时任务日志", description="清空定时任务日志")
|
||||
async def clear_job_log_controller(
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
清空定时任务日志
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含清空定时任务日志结果的JSON响应
|
||||
"""
|
||||
await JobLogService.clear_job_log_service(auth=auth)
|
||||
log.info(f"清空定时任务日志成功")
|
||||
return SuccessResponse(msg="清空定时任务日志成功")
|
||||
|
||||
|
||||
@JobRouter.post('/log/export', summary="导出定时任务日志", description="导出定时任务日志")
|
||||
async def export_job_log_list_controller(
|
||||
search: JobLogQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:export"]))
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
导出定时任务日志
|
||||
|
||||
参数:
|
||||
- search (JobLogQueryParam): 查询参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- StreamingResponse: 包含导出定时任务日志结果的流式响应
|
||||
"""
|
||||
result_dict_list = await JobLogService.get_job_log_list_service(search=search, auth=auth)
|
||||
export_result = await JobLogService.export_job_log_service(data_list=result_dict_list)
|
||||
log.info('导出定时任务日志成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename=job_log.xlsx'
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,162 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Sequence, Any
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import JobModel, JobLogModel
|
||||
from .schema import JobCreateSchema,JobUpdateSchema,JobLogCreateSchema,JobLogUpdateSchema
|
||||
|
||||
|
||||
class JobCRUD(CRUDBase[JobModel, JobCreateSchema, JobUpdateSchema]):
|
||||
"""定时任务数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化定时任务CRUD
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
self.auth = auth
|
||||
super().__init__(model=JobModel, auth=auth)
|
||||
|
||||
async def get_obj_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> JobModel | None:
|
||||
"""
|
||||
获取定时任务详情
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务ID
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- JobModel | None: 定时任务模型,如果不存在则为None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def get_obj_list_crud(self, search: dict | None = None, order_by: list[dict[str, str]] | None = None, preload: list[str | Any] | None = None) -> Sequence[JobModel]:
|
||||
"""
|
||||
获取定时任务列表
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数字典
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[JobModel]: 定时任务模型序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_obj_crud(self, data: JobCreateSchema) -> JobModel | None:
|
||||
"""
|
||||
创建定时任务
|
||||
|
||||
参数:
|
||||
- data (JobCreateSchema): 创建定时任务模型
|
||||
|
||||
返回:
|
||||
- JobModel | None: 创建的定时任务模型,如果创建失败则为None
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_obj_crud(self, id: int, data: JobUpdateSchema) -> JobModel | None:
|
||||
"""
|
||||
更新定时任务
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务ID
|
||||
- data (JobUpdateSchema): 更新定时任务模型
|
||||
|
||||
返回:
|
||||
- JobModel | None: 更新后的定时任务模型,如果更新失败则为None
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_obj_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
删除定时任务
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 定时任务ID列表
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def set_obj_field_crud(self, ids: list[int], **kwargs) -> None:
|
||||
"""
|
||||
设置定时任务的可用状态
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 定时任务ID列表
|
||||
- kwargs: 其他要设置的字段,例如 available=True 或 available=False
|
||||
"""
|
||||
return await self.set(ids=ids, **kwargs)
|
||||
|
||||
async def clear_obj_crud(self) -> None:
|
||||
"""
|
||||
清除定时任务日志
|
||||
|
||||
注意:
|
||||
- 此操作会删除所有定时任务日志,请谨慎操作
|
||||
"""
|
||||
return await self.clear()
|
||||
|
||||
|
||||
class JobLogCRUD(CRUDBase[JobLogModel, JobLogCreateSchema, JobLogUpdateSchema]):
|
||||
"""定时任务日志数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化定时任务日志CRUD
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
self.auth = auth
|
||||
super().__init__(model=JobLogModel, auth=auth)
|
||||
|
||||
async def get_obj_log_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> JobLogModel | None:
|
||||
"""
|
||||
获取定时任务日志详情
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务日志ID
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- JobLogModel | None: 定时任务日志模型,如果不存在则为None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def get_obj_log_list_crud(self, search: dict | None = None, order_by: list[dict[str, str]] | None = None, preload: list[str | Any] | None = None) -> Sequence[JobLogModel]:
|
||||
"""
|
||||
获取定时任务日志列表
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数字典
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[JobLogModel]: 定时任务日志模型序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def delete_obj_log_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
删除定时任务日志
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 定时任务日志ID列表
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def clear_obj_log_crud(self) -> None:
|
||||
"""
|
||||
清除定时任务日志
|
||||
|
||||
注意:
|
||||
- 此操作会删除所有定时任务日志,请谨慎操作
|
||||
"""
|
||||
return await self.clear()
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from app.core.logger import log
|
||||
|
||||
def job(*args, **kwargs) -> None:
|
||||
"""
|
||||
定时任务执行同步函数示例
|
||||
|
||||
参数:
|
||||
- args: 位置参数。
|
||||
- kwargs: 关键字参数。
|
||||
"""
|
||||
try:
|
||||
print(f"开始执行任务: {args}-{kwargs}")
|
||||
time.sleep(3)
|
||||
print(f'{datetime.now()}同步函数执行完成')
|
||||
except Exception as e:
|
||||
log.error(f"同步任务执行失败: {e}")
|
||||
raise
|
||||
|
||||
async def async_job(*args, **kwargs) -> None:
|
||||
"""
|
||||
定时任务执行异步函数示例
|
||||
|
||||
参数:
|
||||
- args: 位置参数。
|
||||
- kwargs: 关键字参数。
|
||||
"""
|
||||
try:
|
||||
print(f"开始执行任务: {args}-{kwargs}")
|
||||
time.sleep(3)
|
||||
print(f'{datetime.now()}异步函数执行完成')
|
||||
except Exception as e:
|
||||
log.error(f"异步任务执行失败: {e}")
|
||||
raise
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sqlalchemy import Boolean, String, Integer, Text, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class JobModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
定时任务调度表
|
||||
- 0: 运行中
|
||||
- 1: 暂停中
|
||||
"""
|
||||
__tablename__: str = 'app_job'
|
||||
__table_args__: dict[str, str] = ({'comment': '定时任务调度表'})
|
||||
__loader_options__: list[str] = ["job_logs", "created_by", "updated_by"]
|
||||
|
||||
name: Mapped[str | None] = mapped_column(String(64), nullable=True, default='', comment='任务名称')
|
||||
jobstore: Mapped[str | None] = mapped_column(String(64), nullable=True, default='default', comment='存储器')
|
||||
executor: Mapped[str | None] = mapped_column(String(64), nullable=True, default='default', comment='执行器:将运行此作业的执行程序的名称')
|
||||
trigger: Mapped[str] = mapped_column(String(64), nullable=False, comment='触发器:控制此作业计划的 trigger 对象')
|
||||
trigger_args: Mapped[str | None] = mapped_column(Text, nullable=True, comment='触发器参数')
|
||||
func: Mapped[str] = mapped_column(Text, nullable=False, comment='任务函数')
|
||||
args: Mapped[str | None] = mapped_column(Text, nullable=True, comment='位置参数')
|
||||
kwargs: Mapped[str | None] = mapped_column(Text, nullable=True, comment='关键字参数')
|
||||
coalesce: Mapped[bool] = mapped_column(Boolean, nullable=True, default=False, comment='是否合并运行:是否在多个运行时间到期时仅运行作业一次')
|
||||
max_instances: Mapped[int] = mapped_column(Integer, nullable=True, default=1, comment='最大实例数:允许的最大并发执行实例数')
|
||||
start_date: Mapped[str | None] = mapped_column(String(64), nullable=True, comment='开始时间')
|
||||
end_date: Mapped[str | None] = mapped_column(String(64), nullable=True, comment='结束时间')
|
||||
|
||||
# 关联关系
|
||||
job_logs: Mapped[list['JobLogModel'] | None] = relationship(
|
||||
back_populates="job",
|
||||
lazy="selectin"
|
||||
)
|
||||
|
||||
|
||||
class JobLogModel(ModelMixin):
|
||||
"""
|
||||
定时任务调度日志表
|
||||
"""
|
||||
__tablename__: str = 'app_job_log'
|
||||
__table_args__: dict[str, str] = ({'comment': '定时任务调度日志表'})
|
||||
__loader_options__: list[str] = ["job"]
|
||||
|
||||
job_name: Mapped[str] = mapped_column(String(64), nullable=False, comment='任务名称')
|
||||
job_group: Mapped[str] = mapped_column(String(64), nullable=False, comment='任务组名')
|
||||
job_executor: Mapped[str] = mapped_column(String(64), nullable=False, comment='任务执行器')
|
||||
invoke_target: Mapped[str] = mapped_column(String(500), nullable=False, comment='调用目标字符串')
|
||||
job_args: Mapped[str | None] = mapped_column(String(255), nullable=True, default='', comment='位置参数')
|
||||
job_kwargs: Mapped[str | None] = mapped_column(String(255), nullable=True, default='', comment='关键字参数')
|
||||
job_trigger: Mapped[str | None] = mapped_column(String(255), nullable=True, default='', comment='任务触发器')
|
||||
job_message: Mapped[str | None] = mapped_column(String(500), nullable=True, default='', comment='日志信息')
|
||||
exception_info: Mapped[str | None] = mapped_column(String(2000), nullable=True, default='', comment='异常信息')
|
||||
|
||||
# 任务关联
|
||||
job_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey('app_job.id', ondelete="CASCADE"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
comment='任务ID'
|
||||
)
|
||||
|
||||
job: Mapped["JobModel | None"] = relationship(
|
||||
back_populates="job_logs",
|
||||
lazy="selectin"
|
||||
)
|
||||
@@ -0,0 +1,146 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
from app.core.validator import DateTimeStr, datetime_validator
|
||||
|
||||
|
||||
class JobCreateSchema(BaseModel):
|
||||
"""
|
||||
定时任务调度表对应pydantic模型
|
||||
"""
|
||||
name: str = Field(..., max_length=64, description='任务名称')
|
||||
func: str = Field(..., description='任务函数')
|
||||
trigger: str = Field(..., description='触发器:控制此作业计划的 trigger 对象')
|
||||
args: str | None = Field(default=None, description='位置参数')
|
||||
kwargs: str | None = Field(default=None, description='关键字参数')
|
||||
coalesce: bool | None = Field(..., description='是否合并运行:是否在多个运行时间到期时仅运行作业一次')
|
||||
max_instances: int | None = Field(default=1, ge=1, description='最大实例数:允许的最大并发执行实例数')
|
||||
jobstore: str | None = Field(..., max_length=64, description='任务存储')
|
||||
executor: str | None = Field(..., max_length=64, description='任务执行器:将运行此作业的执行程序的名称')
|
||||
trigger_args: str | None = Field(default=None, description='触发器参数')
|
||||
start_date: str | None = Field(default=None, description='开始时间')
|
||||
end_date: str | None = Field(default=None, description='结束时间')
|
||||
description: str | None = Field(default=None, max_length=255, description='描述')
|
||||
status: str = Field(default='0', description='任务状态:启动,停止')
|
||||
|
||||
@field_validator('trigger')
|
||||
@classmethod
|
||||
def _validate_trigger(cls, v: str) -> str:
|
||||
allowed = {'cron', 'interval', 'date'}
|
||||
v = v.strip()
|
||||
if v not in allowed:
|
||||
raise ValueError('触发器必须为 cron/interval/date')
|
||||
return v
|
||||
|
||||
@model_validator(mode='after')
|
||||
def _validate_dates(self):
|
||||
"""跨字段校验:结束时间不得早于开始时间。"""
|
||||
if self.start_date and self.end_date:
|
||||
try:
|
||||
start = datetime_validator(self.start_date)
|
||||
end = datetime_validator(self.end_date)
|
||||
except Exception:
|
||||
raise ValueError('时间格式必须为 YYYY-MM-DD HH:MM:SS')
|
||||
if end < start:
|
||||
raise ValueError('结束时间不能早于开始时间')
|
||||
return self
|
||||
|
||||
|
||||
class JobUpdateSchema(JobCreateSchema):
|
||||
"""定时任务更新模型"""
|
||||
...
|
||||
|
||||
|
||||
class JobOutSchema(JobCreateSchema, BaseSchema, UserBySchema):
|
||||
"""定时任务响应模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
...
|
||||
|
||||
|
||||
class JobLogCreateSchema(BaseModel):
|
||||
"""
|
||||
定时任务调度日志表对应pydantic模型
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
job_name: str = Field(..., description='任务名称')
|
||||
job_group: str | None = Field(default=None, description='任务组名')
|
||||
job_executor: str | None = Field(default=None, description='任务执行器')
|
||||
invoke_target: str | None = Field(default=None, description='调用目标字符串')
|
||||
job_args: str | None = Field(default=None, description='位置参数')
|
||||
job_kwargs: str | None = Field(default=None, description='关键字参数')
|
||||
job_trigger: str | None = Field(default=None, description='任务触发器')
|
||||
job_message: str | None = Field(default=None, description='日志信息')
|
||||
exception_info: str | None = Field(default=None, description='异常信息')
|
||||
status: str = Field(default='0', description='任务状态:正常,失败')
|
||||
description: str | None = Field(default=None, max_length=255, description='描述')
|
||||
created_time: DateTimeStr | None = Field(default=None, description='创建时间')
|
||||
updated_time: DateTimeStr | None = Field(default=None, description='更新时间')
|
||||
|
||||
|
||||
class JobLogUpdateSchema(JobLogCreateSchema):
|
||||
"""定时任务调度日志表更新模型"""
|
||||
...
|
||||
id: int | None = Field(default=None, description='任务日志ID')
|
||||
|
||||
|
||||
class JobLogOutSchema(JobLogUpdateSchema, BaseSchema, UserBySchema):
|
||||
"""定时任务调度日志表响应模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
...
|
||||
|
||||
|
||||
class JobQueryParam:
|
||||
"""定时任务查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = Query(None, description="任务名称"),
|
||||
status: str | None = Query(None, description="状态: 启动,停止"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
created_id: int | None = Query(None, description="创建人"),
|
||||
updated_id: int | None = Query(None, description="更新人"),
|
||||
) -> None:
|
||||
|
||||
# 模糊查询字段
|
||||
self.name = ("like", f"%{name}%") if name else None
|
||||
|
||||
# 精确查询字段
|
||||
self.created_id = created_id
|
||||
self.updated_id = updated_id
|
||||
self.status = status
|
||||
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
|
||||
|
||||
class JobLogQueryParam:
|
||||
"""定时任务查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
job_id: int | None = Query(None, description="定时任务ID"),
|
||||
job_name: str | None = Query(None, description="任务名称"),
|
||||
status: str | None = Query(None, description="状态: 正常,失败"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
) -> None:
|
||||
# 定时任务ID查询
|
||||
self.job_id = job_id
|
||||
# 模糊查询字段
|
||||
self.job_name = ("like", job_name)
|
||||
# 精确查询字段
|
||||
self.status = status
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
@@ -0,0 +1,307 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.cron_util import CronUtil
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .tools.ap_scheduler import SchedulerUtil
|
||||
from .crud import JobCRUD, JobLogCRUD
|
||||
from .schema import (
|
||||
JobCreateSchema,
|
||||
JobUpdateSchema,
|
||||
JobOutSchema,
|
||||
JobLogOutSchema,
|
||||
JobQueryParam,
|
||||
JobLogQueryParam
|
||||
)
|
||||
|
||||
|
||||
class JobService:
|
||||
"""
|
||||
定时任务管理模块服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_job_detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""
|
||||
获取定时任务详情
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 定时任务ID
|
||||
|
||||
返回:
|
||||
- Dict: 定时任务详情字典
|
||||
"""
|
||||
obj = await JobCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
return JobOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_job_list_service(cls, auth: AuthSchema, search: JobQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> list[dict]:
|
||||
"""
|
||||
获取定时任务列表
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- search (JobQueryParam | None): 查询参数模型
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
|
||||
返回:
|
||||
- List[Dict]: 定时任务详情字典列表
|
||||
"""
|
||||
obj_list = await JobCRUD(auth).get_obj_list_crud(search=search.__dict__, order_by=order_by)
|
||||
return [JobOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def create_job_service(cls, auth: AuthSchema, data: JobCreateSchema) -> dict:
|
||||
"""
|
||||
创建定时任务
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- data (JobCreateSchema): 定时任务创建模型
|
||||
|
||||
返回:
|
||||
- Dict: 定时任务详情字典
|
||||
"""
|
||||
exist_obj = await JobCRUD(auth).get(name=data.name)
|
||||
if exist_obj:
|
||||
raise CustomException(msg='创建失败,该定时任务已存在')
|
||||
|
||||
obj = await JobCRUD(auth).create_obj_crud(data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg='创建失败,该数据定时任务不存在')
|
||||
SchedulerUtil().add_job(job_info=obj)
|
||||
return JobOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_job_service(cls, auth: AuthSchema, id:int, data: JobUpdateSchema) -> dict:
|
||||
"""
|
||||
更新定时任务
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 定时任务ID
|
||||
- data (JobUpdateSchema): 定时任务更新模型
|
||||
|
||||
返回:
|
||||
- dict: 定时任务详情字典
|
||||
"""
|
||||
exist_obj = await JobCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
if not exist_obj:
|
||||
raise CustomException(msg='更新失败,该定时任务不存在')
|
||||
if data.trigger == 'cron' and data.trigger_args and not CronUtil.validate_cron_expression(data.trigger_args):
|
||||
raise CustomException(msg=f'新增定时任务{data.name}失败, Cron表达式不正确')
|
||||
obj = await JobCRUD(auth).update_obj_crud(id=id, data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg='更新失败,该数据定时任务不存在')
|
||||
SchedulerUtil().modify_job(job_id=obj.id)
|
||||
return JobOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_job_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""
|
||||
删除定时任务
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- ids (list[int]): 定时任务ID列表
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
for id in ids:
|
||||
exist_obj = await JobCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
if not exist_obj:
|
||||
raise CustomException(msg='删除失败,该数据定时任务不存在')
|
||||
obj = await JobLogCRUD(auth).get(job_id=id)
|
||||
if obj:
|
||||
raise CustomException(msg=f'删除失败,该定时任务存 {exist_obj.name} 在日志记录')
|
||||
|
||||
SchedulerUtil().remove_job(job_id=id)
|
||||
await JobCRUD(auth).delete_obj_crud(ids=ids)
|
||||
|
||||
|
||||
@classmethod
|
||||
async def clear_job_service(cls, auth: AuthSchema) -> None:
|
||||
"""
|
||||
清空所有定时任务
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
SchedulerUtil().clear_jobs()
|
||||
await JobLogCRUD(auth).clear_obj_log_crud()
|
||||
await JobCRUD(auth).clear_obj_crud()
|
||||
|
||||
@classmethod
|
||||
async def option_job_service(cls, auth: AuthSchema, id: int, option: int) -> None:
|
||||
"""
|
||||
操作定时任务
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 定时任务ID
|
||||
- option (int): 操作类型, 1: 暂停 2: 恢复 3: 重启
|
||||
"""
|
||||
# 1: 暂停 2: 恢复 3: 重启
|
||||
obj = await JobCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='操作失败,该数据定时任务不存在')
|
||||
if option == 1:
|
||||
SchedulerUtil().pause_job(job_id=id)
|
||||
await JobCRUD(auth).set_obj_field_crud(ids=[id], status=False)
|
||||
elif option == 2:
|
||||
SchedulerUtil().resume_job(job_id=id)
|
||||
await JobCRUD(auth).set_obj_field_crud(ids=[id], status=True)
|
||||
elif option == 3:
|
||||
# 重启任务:先移除再添加,确保使用最新的任务配置
|
||||
SchedulerUtil().remove_job(job_id=id)
|
||||
# 获取最新的任务配置
|
||||
updated_job = await JobCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
if updated_job:
|
||||
# 重新添加任务
|
||||
SchedulerUtil.add_job(job_info=updated_job)
|
||||
# 设置状态为运行中
|
||||
await JobCRUD(auth).set_obj_field_crud(ids=[id], status=True)
|
||||
|
||||
@classmethod
|
||||
async def export_job_service(cls, data_list: list[dict]) -> bytes:
|
||||
"""
|
||||
导出定时任务列表
|
||||
|
||||
参数:
|
||||
- data_list (list[dict]): 定时任务列表
|
||||
|
||||
返回:
|
||||
- bytes: Excel文件字节流
|
||||
"""
|
||||
mapping_dict = {
|
||||
'id': '编号',
|
||||
'name': '任务名称',
|
||||
'func': '任务函数',
|
||||
'trigger': '触发器',
|
||||
'args': '位置参数',
|
||||
'kwargs': '关键字参数',
|
||||
'coalesce': '是否合并运行',
|
||||
'max_instances': '最大实例数',
|
||||
'jobstore': '任务存储',
|
||||
'executor': '任务执行器',
|
||||
'trigger_args': '触发器参数',
|
||||
'status': '任务状态',
|
||||
'message': '日志信息',
|
||||
'description': '备注',
|
||||
'created_time': '创建时间',
|
||||
'updated_time': '更新时间',
|
||||
'created_id': '创建者ID',
|
||||
'updated_id': '更新者ID',
|
||||
}
|
||||
|
||||
# 复制数据并转换状态
|
||||
data = data_list.copy()
|
||||
for item in data:
|
||||
item['status'] = '已完成' if item['status'] == '0' else '运行中' if item['status'] == '1' else '暂停'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
|
||||
class JobLogService:
|
||||
"""
|
||||
定时任务日志管理模块服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_job_log_detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""
|
||||
获取定时任务日志详情
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 定时任务日志ID
|
||||
|
||||
返回:
|
||||
- dict: 定时任务日志详情字典
|
||||
"""
|
||||
obj = await JobLogCRUD(auth).get_obj_log_by_id_crud(id=id)
|
||||
return JobLogOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_job_log_list_service(cls, auth: AuthSchema, search: JobLogQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
||||
"""
|
||||
获取定时任务日志列表
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- search (JobLogQueryParam | None): 查询参数模型, 包含分页信息和查询条件
|
||||
- order_by (list[dict] | None): 排序参数列表, 每个元素为一个字典, 包含字段名和排序方向
|
||||
|
||||
返回:
|
||||
- list[dict]: 定时任务日志详情字典列表
|
||||
"""
|
||||
obj_list = await JobLogCRUD(auth).get_obj_log_list_crud(search=search.__dict__, order_by=order_by)
|
||||
return [JobLogOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def delete_job_log_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""
|
||||
删除定时任务日志
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- ids (list[int]): 定时任务日志ID列表
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
for id in ids:
|
||||
exist_obj = await JobLogCRUD(auth).get_obj_log_by_id_crud(id=id)
|
||||
if not exist_obj:
|
||||
raise CustomException(msg=f'删除失败,该定时任务日志ID为{id}的记录不存在')
|
||||
await JobLogCRUD(auth).delete_obj_log_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def clear_job_log_service(cls, auth: AuthSchema) -> None:
|
||||
"""
|
||||
清空定时任务日志
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
# 获取所有日志ID并批量删除
|
||||
all_logs = await JobLogCRUD(auth).get_obj_log_list_crud()
|
||||
if all_logs:
|
||||
ids = [log.id for log in all_logs]
|
||||
await JobLogCRUD(auth).delete_obj_log_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def export_job_log_service(cls, data_list: list[dict]) -> bytes:
|
||||
"""
|
||||
导出定时任务日志列表
|
||||
|
||||
参数:
|
||||
- data_list (List[Dict[str, Any]]): 定时任务日志列表
|
||||
|
||||
返回:
|
||||
- bytes: Excel文件字节流
|
||||
"""
|
||||
mapping_dict = {
|
||||
'id': '编号',
|
||||
'job_name': '任务名称',
|
||||
'job_group': '任务组名',
|
||||
'job_executor': '任务执行器',
|
||||
'invoke_target': '调用目标字符串',
|
||||
'job_args': '位置参数',
|
||||
'job_kwargs': '关键字参数',
|
||||
'job_trigger': '任务触发器',
|
||||
'job_message': '日志信息',
|
||||
'exception_info': '异常信息',
|
||||
'status': '执行状态',
|
||||
'created_time': '创建时间',
|
||||
'updated_time': '更新时间',
|
||||
}
|
||||
|
||||
# 复制数据并转换状态
|
||||
data = data_list.copy()
|
||||
for item in data:
|
||||
item['status'] = '成功' if item.get('status') == '0' else '失败'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -0,0 +1,589 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import importlib
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from asyncio import iscoroutinefunction
|
||||
from apscheduler.job import Job
|
||||
from apscheduler.events import JobExecutionEvent, EVENT_ALL, JobEvent
|
||||
from apscheduler.executors.asyncio import AsyncIOExecutor
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.executors.pool import ProcessPoolExecutor
|
||||
from apscheduler.jobstores.memory import MemoryJobStore
|
||||
from apscheduler.jobstores.redis import RedisJobStore
|
||||
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from apscheduler.triggers.date import DateTrigger
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from app.config.setting import settings
|
||||
from app.core.database import engine, db_session, async_db_session
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import log
|
||||
from app.utils.cron_util import CronUtil
|
||||
|
||||
from app.api.v1.module_application.job.model import JobModel
|
||||
|
||||
job_stores = {
|
||||
'default': MemoryJobStore(),
|
||||
'sqlalchemy': SQLAlchemyJobStore(url=settings.DB_URI, engine=engine),
|
||||
'redis': RedisJobStore(
|
||||
host=settings.REDIS_HOST,
|
||||
port=int(settings.REDIS_PORT),
|
||||
username=settings.REDIS_USER,
|
||||
password=settings.REDIS_PASSWORD,
|
||||
db=int(settings.REDIS_DB_NAME),
|
||||
),
|
||||
}
|
||||
# 配置执行器
|
||||
executors = {
|
||||
'default': AsyncIOExecutor(),
|
||||
'processpool': ProcessPoolExecutor(max_workers=1) # 减少进程数量以减少资源消耗
|
||||
}
|
||||
# 配置默认参数
|
||||
job_defaults = {
|
||||
'coalesce': True, # 合并执行错过的任务
|
||||
'max_instances': 1, # 最大实例数
|
||||
}
|
||||
# 配置调度器
|
||||
scheduler = AsyncIOScheduler()
|
||||
scheduler.configure(
|
||||
jobstores=job_stores,
|
||||
executors=executors,
|
||||
job_defaults=job_defaults,
|
||||
timezone='Asia/Shanghai'
|
||||
)
|
||||
|
||||
class SchedulerUtil:
|
||||
"""
|
||||
定时任务相关方法
|
||||
"""
|
||||
@classmethod
|
||||
def scheduler_event_listener(cls, event: JobEvent | JobExecutionEvent) -> None:
|
||||
"""
|
||||
监听任务执行事件。
|
||||
|
||||
参数:
|
||||
- event (JobEvent | JobExecutionEvent): 任务事件对象。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
# 延迟导入避免循环导入
|
||||
from app.api.v1.module_application.job.model import JobLogModel
|
||||
|
||||
# 获取事件类型和任务ID
|
||||
event_type = event.__class__.__name__
|
||||
# 初始化任务状态
|
||||
status = True
|
||||
exception_info = ''
|
||||
if isinstance(event, JobExecutionEvent) and event.exception:
|
||||
exception_info = str(event.exception)
|
||||
status = False
|
||||
if hasattr(event, 'job_id'):
|
||||
job_id = event.job_id
|
||||
query_job = cls.get_job(job_id=job_id)
|
||||
if query_job:
|
||||
query_job_info = query_job.__getstate__()
|
||||
# 获取任务名称
|
||||
job_name = query_job_info.get('name')
|
||||
# 获取任务组名
|
||||
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_name=job_name,
|
||||
job_group=job_group,
|
||||
job_executor=job_executor,
|
||||
invoke_target=invoke_target,
|
||||
job_args=job_args,
|
||||
job_kwargs=job_kwargs,
|
||||
job_trigger=job_trigger,
|
||||
job_message=job_message,
|
||||
status=status,
|
||||
exception_info=exception_info,
|
||||
created_time=datetime.now(),
|
||||
updated_time=datetime.now(),
|
||||
job_id=job_id,
|
||||
)
|
||||
|
||||
# 使用线程池执行操作以避免阻塞调度器和数据库锁定问题
|
||||
executor = ThreadPoolExecutor(max_workers=1)
|
||||
executor.submit(cls._save_job_log_async_wrapper, job_log)
|
||||
executor.shutdown(wait=False)
|
||||
|
||||
@classmethod
|
||||
def _save_job_log_async_wrapper(cls, job_log) -> 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:
|
||||
"""
|
||||
应用启动时初始化定时任务。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
# 延迟导入避免循环导入
|
||||
from app.api.v1.module_application.job.crud import JobCRUD
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
log.info('🔎 开始启动定时任务...')
|
||||
|
||||
# 启动调度器
|
||||
scheduler.start()
|
||||
|
||||
# 添加事件监听器
|
||||
scheduler.add_listener(cls.scheduler_event_listener, EVENT_ALL)
|
||||
|
||||
async with async_db_session() as session:
|
||||
async with session.begin():
|
||||
auth = AuthSchema(db=session)
|
||||
job_list = await JobCRUD(auth).get_obj_list_crud()
|
||||
|
||||
# 只在一个实例上初始化任务
|
||||
# 使用Redis锁确保只有一个实例执行任务初始化
|
||||
import redis.asyncio as redis
|
||||
redis_client = redis.Redis(
|
||||
host=settings.REDIS_HOST,
|
||||
port=int(settings.REDIS_PORT),
|
||||
username=settings.REDIS_USER,
|
||||
password=settings.REDIS_PASSWORD,
|
||||
db=int(settings.REDIS_DB_NAME),
|
||||
)
|
||||
|
||||
# 尝试获取锁,过期时间10秒
|
||||
lock_key = "scheduler_init_lock"
|
||||
lock_acquired = await redis_client.set(lock_key, "1", ex=10, nx=True)
|
||||
|
||||
if lock_acquired:
|
||||
try:
|
||||
for item in job_list:
|
||||
# 检查任务是否已经存在
|
||||
existing_job = cls.get_job(job_id=item.id)
|
||||
if existing_job:
|
||||
cls.remove_job(job_id=item.id) # 删除旧任务
|
||||
|
||||
# 添加新任务
|
||||
cls.add_job(item)
|
||||
|
||||
# 根据数据库中保存的状态来设置任务状态
|
||||
if hasattr(item, 'status') and item.status == "1":
|
||||
# 如果任务状态为暂停,则立即暂停刚添加的任务
|
||||
cls.pause_job(job_id=item.id)
|
||||
log.info('✅️ 系统初始定时任务加载成功')
|
||||
finally:
|
||||
# 释放锁
|
||||
await redis_client.delete(lock_key)
|
||||
else:
|
||||
# 等待其他实例完成初始化
|
||||
import asyncio
|
||||
await asyncio.sleep(2)
|
||||
log.info('✅️ 定时任务已由其他实例初始化完成')
|
||||
|
||||
@classmethod
|
||||
async def close_system_scheduler(cls) -> None:
|
||||
"""
|
||||
关闭系统定时任务。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
try:
|
||||
# 移除所有任务
|
||||
scheduler.remove_all_jobs()
|
||||
# 等待所有任务完成后再关闭
|
||||
scheduler.shutdown(wait=True)
|
||||
log.info('✅️ 关闭定时任务成功')
|
||||
except Exception as e:
|
||||
log.error(f'关闭定时任务失败: {str(e)}')
|
||||
|
||||
@classmethod
|
||||
def get_job(cls, job_id: str | int) -> Job | None:
|
||||
"""
|
||||
根据任务ID获取任务对象。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
|
||||
返回:
|
||||
- Job | None: 任务对象,未找到则为 None。
|
||||
"""
|
||||
return scheduler.get_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def get_all_jobs(cls) -> list[Job]:
|
||||
"""
|
||||
获取全部调度任务列表。
|
||||
|
||||
返回:
|
||||
- list[Job]: 任务列表。
|
||||
"""
|
||||
return scheduler.get_jobs()
|
||||
|
||||
@classmethod
|
||||
async def _task_wrapper(cls, job_id, func, *args, **kwargs):
|
||||
"""
|
||||
任务执行包装器,添加分布式锁防止同一任务被多个实例同时执行。
|
||||
|
||||
参数:
|
||||
- job_id: 任务ID
|
||||
- func: 实际要执行的任务函数
|
||||
- *args: 任务函数位置参数
|
||||
- **kwargs: 任务函数关键字参数
|
||||
|
||||
返回:
|
||||
- 任务函数的返回值
|
||||
"""
|
||||
import redis.asyncio as redis
|
||||
import asyncio
|
||||
from app.config.setting import settings
|
||||
|
||||
# 创建Redis客户端
|
||||
redis_client = redis.Redis(
|
||||
host=settings.REDIS_HOST,
|
||||
port=int(settings.REDIS_PORT),
|
||||
username=settings.REDIS_USER,
|
||||
password=settings.REDIS_PASSWORD,
|
||||
db=int(settings.REDIS_DB_NAME),
|
||||
)
|
||||
|
||||
# 生成锁键
|
||||
lock_key = f"job_lock:{job_id}"
|
||||
|
||||
# 设置锁的过期时间(根据任务类型调整,这里设置为30秒)
|
||||
lock_expire = 30
|
||||
lock_acquired = False
|
||||
|
||||
try:
|
||||
# 尝试获取锁
|
||||
lock_acquired = await redis_client.set(lock_key, "1", ex=lock_expire, nx=True)
|
||||
|
||||
if lock_acquired:
|
||||
log.info(f"任务 {job_id} 获取执行锁成功")
|
||||
# 执行任务
|
||||
if iscoroutinefunction(func):
|
||||
return await func(*args, **kwargs)
|
||||
else:
|
||||
# 对于同步函数,使用线程池执行
|
||||
loop = asyncio.get_running_loop()
|
||||
return await loop.run_in_executor(None, func, *args, **kwargs)
|
||||
else:
|
||||
# 获取锁失败,记录日志
|
||||
log.info(f"任务 {job_id} 获取执行锁失败,跳过本次执行")
|
||||
return None
|
||||
finally:
|
||||
# 释放锁
|
||||
if lock_acquired:
|
||||
await redis_client.delete(lock_key)
|
||||
log.info(f"任务 {job_id} 释放执行锁")
|
||||
|
||||
@classmethod
|
||||
def add_job(cls, job_info: JobModel) -> Job:
|
||||
"""
|
||||
根据任务配置创建并添加调度任务。
|
||||
|
||||
参数:
|
||||
- job_info (JobModel): 任务对象信息(包含触发器、函数、参数等)。
|
||||
|
||||
返回:
|
||||
- Job: 新增的任务对象。
|
||||
"""
|
||||
# 动态导入模块
|
||||
# 1. 解析调用目标
|
||||
module_path, func_name = str(job_info.func).rsplit('.', 1)
|
||||
module_path = "app.api.v1.module_application.job.function_task." + module_path
|
||||
try:
|
||||
module = importlib.import_module(module_path)
|
||||
job_func = getattr(module, func_name)
|
||||
|
||||
# 2. 确定任务存储器:优先使用redis,确保分布式环境中任务同步
|
||||
if job_info.jobstore is None:
|
||||
job_info.jobstore = 'redis' # 改为默认使用redis存储
|
||||
|
||||
# 3. 确定执行器
|
||||
job_executor = job_info.executor
|
||||
if job_executor is None:
|
||||
job_executor = 'default'
|
||||
|
||||
if job_info.trigger_args is None:
|
||||
raise ValueError("触发器缺少参数")
|
||||
|
||||
# 异步函数必须使用默认执行器
|
||||
if iscoroutinefunction(job_func):
|
||||
job_executor = 'default'
|
||||
|
||||
# 4. 创建触发器
|
||||
if job_info.trigger == 'date':
|
||||
trigger = DateTrigger(run_date=job_info.trigger_args)
|
||||
elif job_info.trigger == 'interval':
|
||||
# 将传入的 interval 表达式拆分为不同的字段
|
||||
fields = job_info.trigger_args.strip().split()
|
||||
if len(fields) != 5:
|
||||
raise ValueError("无效的 interval 表达式")
|
||||
second, minute, hour, day, week = tuple([int(field) if field != '*' else 0 for field in fields])
|
||||
# 秒、分、时、天、周(* * * * 1)
|
||||
trigger = IntervalTrigger(
|
||||
weeks=week,
|
||||
days=day,
|
||||
hours=hour,
|
||||
minutes=minute,
|
||||
seconds=second,
|
||||
start_date=job_info.start_date,
|
||||
end_date=job_info.end_date,
|
||||
timezone='Asia/Shanghai',
|
||||
jitter=None
|
||||
)
|
||||
elif job_info.trigger == 'cron':
|
||||
# 秒、分、时、天、月、星期几、年 ()
|
||||
fields = job_info.trigger_args.strip().split()
|
||||
if len(fields) not in (6, 7):
|
||||
raise ValueError("无效的 Cron 表达式")
|
||||
if not CronUtil.validate_cron_expression(job_info.trigger_args):
|
||||
raise ValueError(f'定时任务{job_info.name}, Cron表达式不正确')
|
||||
|
||||
parsed_fields = [None if field in ('*', '?') else field for field in fields]
|
||||
if len(fields) == 6:
|
||||
parsed_fields.append(None)
|
||||
|
||||
second, minute, hour, day, month, day_of_week, year = tuple(parsed_fields)
|
||||
trigger = CronTrigger(
|
||||
second=second,
|
||||
minute=minute,
|
||||
hour=hour,
|
||||
day=day,
|
||||
month=month,
|
||||
day_of_week=day_of_week,
|
||||
year=year,
|
||||
start_date=job_info.start_date,
|
||||
end_date=job_info.end_date,
|
||||
timezone='Asia/Shanghai'
|
||||
)
|
||||
else:
|
||||
raise ValueError("无效的 trigger 触发器")
|
||||
|
||||
# 5. 添加任务(使用包装器函数)
|
||||
job = scheduler.add_job(
|
||||
func=cls._task_wrapper,
|
||||
trigger=trigger,
|
||||
args=[str(job_info.id), job_func] + (str(job_info.args).split(',') if job_info.args else []),
|
||||
kwargs=json.loads(job_info.kwargs) if job_info.kwargs else {},
|
||||
id=str(job_info.id),
|
||||
name=job_info.name,
|
||||
coalesce=job_info.coalesce,
|
||||
max_instances=1, # 确保只有一个实例执行
|
||||
jobstore=job_info.jobstore,
|
||||
executor=job_executor,
|
||||
)
|
||||
log.info(f"任务 {job_info.id} 添加到 {job_info.jobstore} 存储器成功")
|
||||
return job
|
||||
except ModuleNotFoundError:
|
||||
raise ValueError(f"未找到该模块:{module_path}")
|
||||
except AttributeError:
|
||||
raise ValueError(f"未找到该模块下的方法:{func_name}")
|
||||
except Exception as e:
|
||||
raise CustomException(msg=f"添加任务失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
def remove_job(cls, job_id: str | int) -> None:
|
||||
"""
|
||||
根据任务ID删除调度任务。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if query_job:
|
||||
scheduler.remove_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def clear_jobs(cls) -> None:
|
||||
"""
|
||||
删除所有调度任务。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
scheduler.remove_all_jobs()
|
||||
|
||||
@classmethod
|
||||
def modify_job(cls, job_id: str | int) -> Job:
|
||||
"""
|
||||
更新指定任务的配置(运行中的任务下次执行生效)。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
|
||||
返回:
|
||||
- Job: 更新后的任务对象。
|
||||
|
||||
异常:
|
||||
- CustomException: 当任务不存在时抛出。
|
||||
"""
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if not query_job:
|
||||
raise CustomException(msg=f"未找到该任务:{job_id}")
|
||||
return scheduler.modify_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def pause_job(cls, job_id: str | int) -> None:
|
||||
"""
|
||||
暂停指定任务(仅运行中可暂停,已终止不可)。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
|
||||
返回:
|
||||
- None
|
||||
|
||||
异常:
|
||||
- ValueError: 当任务不存在时抛出。
|
||||
"""
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if not query_job:
|
||||
raise ValueError(f"未找到该任务:{job_id}")
|
||||
scheduler.pause_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def resume_job(cls, job_id: str | int) -> None:
|
||||
"""
|
||||
恢复指定任务(仅暂停中可恢复,已终止不可)。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
|
||||
返回:
|
||||
- None
|
||||
|
||||
异常:
|
||||
- ValueError: 当任务不存在时抛出。
|
||||
"""
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if not query_job:
|
||||
raise ValueError(f"未找到该任务:{job_id}")
|
||||
scheduler.resume_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def reschedule_job(cls, job_id: str | int, trigger=None, **trigger_args) -> Job | None:
|
||||
"""
|
||||
重启指定任务的触发器。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
- trigger: 触发器类型
|
||||
- **trigger_args: 触发器参数
|
||||
|
||||
返回:
|
||||
- Job: 更新后的任务对象
|
||||
|
||||
异常:
|
||||
- CustomException: 当任务不存在时抛出。
|
||||
"""
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if not query_job:
|
||||
raise CustomException(msg=f"未找到该任务:{job_id}")
|
||||
|
||||
# 如果没有提供新的触发器,则使用现有触发器
|
||||
if trigger is None:
|
||||
# 获取当前任务的触发器配置
|
||||
current_trigger = query_job.trigger
|
||||
# 重新调度任务,使用当前的触发器
|
||||
return scheduler.reschedule_job(job_id=str(job_id), trigger=current_trigger)
|
||||
else:
|
||||
# 使用新提供的触发器
|
||||
return scheduler.reschedule_job(job_id=str(job_id), trigger=trigger, **trigger_args)
|
||||
|
||||
@classmethod
|
||||
def get_single_job_status(cls, job_id: str | int) -> str:
|
||||
"""
|
||||
获取单个任务的当前状态。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID
|
||||
|
||||
返回:
|
||||
- str: 任务状态('running' | 'paused' | 'stopped' | 'unknown')
|
||||
"""
|
||||
job = cls.get_job(job_id=str(job_id))
|
||||
if not job:
|
||||
return 'unknown'
|
||||
|
||||
# 检查任务是否在暂停列表中
|
||||
if job_id in scheduler._jobstores[job._jobstore_alias]._paused_jobs:
|
||||
return 'paused'
|
||||
|
||||
# 检查调度器状态
|
||||
if scheduler.state == 0: # STATE_STOPPED
|
||||
return 'stopped'
|
||||
|
||||
return 'running'
|
||||
|
||||
@classmethod
|
||||
def print_jobs(cls,jobstore: Any | None = None, out: Any | None = None):
|
||||
"""
|
||||
打印调度任务列表。
|
||||
|
||||
参数:
|
||||
- jobstore (Any | None): 任务存储别名。
|
||||
- out (Any | None): 输出目标。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
scheduler.print_jobs(jobstore=jobstore, out=out)
|
||||
|
||||
@classmethod
|
||||
def get_job_status(cls) -> str:
|
||||
"""
|
||||
获取调度器当前状态。
|
||||
|
||||
返回:
|
||||
- str: 状态字符串('stopped' | 'running' | 'paused' | 'unknown')。
|
||||
"""
|
||||
#: constant indicating a scheduler's stopped state
|
||||
STATE_STOPPED = 0
|
||||
#: constant indicating a scheduler's running state (started and processing jobs)
|
||||
STATE_RUNNING = 1
|
||||
#: constant indicating a scheduler's paused state (started but not processing jobs)
|
||||
STATE_PAUSED = 2
|
||||
if scheduler.state == STATE_STOPPED:
|
||||
return 'stopped'
|
||||
elif scheduler.state == STATE_RUNNING:
|
||||
return 'running'
|
||||
elif scheduler.state == STATE_PAUSED:
|
||||
return 'paused'
|
||||
else:
|
||||
return 'unknown'
|
||||
@@ -0,0 +1,9 @@
|
||||
'''
|
||||
Author: caoziyuan ziyuan.cao@zhuying.com
|
||||
Date: 2025-12-22 17:25:15
|
||||
LastEditors: caoziyuan ziyuan.cao@zhuying.com
|
||||
LastEditTime: 2025-12-22 17:25:48
|
||||
FilePath: \backend\app\api\v1\module_application\miniapp\__init__.py
|
||||
Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from redis.asyncio.client import Redis
|
||||
|
||||
from app.common.response import SuccessResponse
|
||||
from app.core.logger import log
|
||||
from app.core.dependencies import db_getter, redis_getter
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
|
||||
from .service import MiniappService
|
||||
from .schema import MiniappLoginSchema, MiniappLoginOutSchema
|
||||
|
||||
|
||||
MiniappRouter = APIRouter(prefix="/miniapp", tags=["小程序"])
|
||||
|
||||
|
||||
@MiniappRouter.post("/login", summary="小程序登录", description="微信小程序用户登录", response_model=MiniappLoginOutSchema)
|
||||
async def miniapp_login_controller(
|
||||
data: MiniappLoginSchema,
|
||||
db: AsyncSession = Depends(db_getter),
|
||||
redis: Redis = Depends(redis_getter),
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
小程序登录接口
|
||||
|
||||
前端调用 wx.login() 获取 code,传给此接口换取 token
|
||||
|
||||
参数:
|
||||
- data (MiniappLoginSchema): 包含微信登录code
|
||||
|
||||
返回:
|
||||
- MiniappLoginOutSchema: 包含access_token和用户信息
|
||||
"""
|
||||
auth = AuthSchema(db=db)
|
||||
result = await MiniappService.login_service(auth=auth, redis=redis, data=data)
|
||||
log.info(f"小程序用户登录成功")
|
||||
return SuccessResponse(data=result, msg="登录成功")
|
||||
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Sequence
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import MiniappUserModel
|
||||
from .schema import MiniappUserCreateSchema, MiniappUserUpdateSchema
|
||||
|
||||
|
||||
class MiniappUserCRUD(CRUDBase[MiniappUserModel, MiniappUserCreateSchema, MiniappUserUpdateSchema]):
|
||||
"""小程序用户数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
super().__init__(model=MiniappUserModel, auth=auth)
|
||||
|
||||
async def get_by_openid(self, openid: str) -> MiniappUserModel | None:
|
||||
"""根据openid获取用户"""
|
||||
return await self.get(openid=openid)
|
||||
|
||||
async def get_by_id_crud(self, id: int) -> MiniappUserModel | None:
|
||||
"""根据ID获取用户"""
|
||||
return await self.get(id=id)
|
||||
|
||||
async def update_last_login(self, id: int) -> MiniappUserModel | None:
|
||||
"""更新最后登录时间"""
|
||||
return await self.update(id=id, data={"last_login": datetime.now()})
|
||||
|
||||
async def update_session_key(self, id: int, session_key: str) -> MiniappUserModel | None:
|
||||
"""更新session_key"""
|
||||
return await self.update(id=id, data={"session_key": session_key})
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
from sqlalchemy import String, DateTime, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin
|
||||
|
||||
|
||||
class MiniappUserModel(ModelMixin):
|
||||
"""
|
||||
小程序用户模型
|
||||
|
||||
存储微信小程序用户信息
|
||||
"""
|
||||
__tablename__: str = "miniapp_user"
|
||||
__table_args__: dict[str, str] = ({'comment': '小程序用户表'})
|
||||
|
||||
openid: Mapped[str] = mapped_column(String(64), nullable=False, unique=True, index=True, comment="微信openid")
|
||||
unionid: Mapped[str | None] = mapped_column(String(64), nullable=True, unique=True, comment="微信unionid")
|
||||
session_key: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="会话密钥")
|
||||
nickname: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="昵称")
|
||||
avatar: Mapped[str | None] = mapped_column(String(512), nullable=True, comment="头像URL")
|
||||
phone: Mapped[str | None] = mapped_column(String(20), nullable=True, comment="手机号")
|
||||
last_login: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, comment="最后登录时间")
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from app.core.base_schema import BaseSchema
|
||||
|
||||
|
||||
class MiniappLoginSchema(BaseModel):
|
||||
"""小程序登录请求"""
|
||||
code: str = Field(..., min_length=1, description="微信登录code")
|
||||
|
||||
|
||||
class MiniappUserCreateSchema(BaseModel):
|
||||
"""小程序用户创建"""
|
||||
openid: str = Field(..., max_length=64, description="微信openid")
|
||||
unionid: str | None = Field(default=None, max_length=64, description="微信unionid")
|
||||
session_key: str | None = Field(default=None, max_length=64, description="会话密钥")
|
||||
nickname: str | None = Field(default=None, max_length=64, description="昵称")
|
||||
avatar: str | None = Field(default=None, max_length=512, description="头像URL")
|
||||
|
||||
|
||||
class MiniappUserUpdateSchema(MiniappUserCreateSchema):
|
||||
"""小程序用户更新"""
|
||||
phone: str | None = Field(default=None, max_length=20, description="手机号")
|
||||
|
||||
|
||||
class MiniappUserOutSchema(MiniappUserUpdateSchema, BaseSchema):
|
||||
"""小程序用户响应"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class MiniappLoginOutSchema(BaseModel):
|
||||
"""小程序登录响应"""
|
||||
access_token: str = Field(..., description="访问令牌")
|
||||
token_type: str = Field(default="Bearer", description="令牌类型")
|
||||
expires_in: int = Field(..., description="过期时间(秒)")
|
||||
user: MiniappUserOutSchema = Field(..., description="用户信息")
|
||||
@@ -0,0 +1,153 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import uuid
|
||||
import json
|
||||
import httpx
|
||||
from datetime import datetime, timedelta
|
||||
from redis.asyncio.client import Redis
|
||||
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import log
|
||||
from app.core.security import create_access_token
|
||||
from app.core.redis_crud import RedisCURD
|
||||
from app.common.enums import RedisInitKeyConfig
|
||||
from app.config.setting import settings
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema, JWTPayloadSchema
|
||||
|
||||
from .crud import MiniappUserCRUD
|
||||
from .schema import (
|
||||
MiniappLoginSchema,
|
||||
MiniappUserCreateSchema,
|
||||
MiniappUserOutSchema,
|
||||
MiniappLoginOutSchema,
|
||||
)
|
||||
|
||||
|
||||
class MiniappService:
|
||||
"""小程序服务层"""
|
||||
|
||||
# 微信登录接口
|
||||
WX_LOGIN_URL = "https://api.weixin.qq.com/sns/jscode2session"
|
||||
|
||||
@classmethod
|
||||
async def login_service(cls, auth: AuthSchema, redis: Redis, data: MiniappLoginSchema) -> dict:
|
||||
"""
|
||||
小程序登录
|
||||
|
||||
流程:
|
||||
1. 用微信code换取openid和session_key
|
||||
2. 查找或创建用户
|
||||
3. 生成JWT token
|
||||
"""
|
||||
# 1. 调用微信接口获取openid
|
||||
wx_result = await cls._get_wx_session(code=data.code)
|
||||
openid = wx_result.get("openid")
|
||||
session_key = wx_result.get("session_key")
|
||||
unionid = wx_result.get("unionid")
|
||||
|
||||
if not openid:
|
||||
raise CustomException(msg="微信登录失败,无法获取openid")
|
||||
|
||||
# 2. 查找或创建用户
|
||||
user = await MiniappUserCRUD(auth).get_by_openid(openid=openid)
|
||||
|
||||
if user:
|
||||
# 更新session_key和登录时间
|
||||
await MiniappUserCRUD(auth).update_session_key(id=user.id, session_key=session_key)
|
||||
await MiniappUserCRUD(auth).update_last_login(id=user.id)
|
||||
log.info(f"小程序用户登录: {openid}")
|
||||
else:
|
||||
# 创建新用户
|
||||
user_data = MiniappUserCreateSchema(
|
||||
openid=openid,
|
||||
unionid=unionid,
|
||||
session_key=session_key,
|
||||
)
|
||||
user = await MiniappUserCRUD(auth).create(data=user_data)
|
||||
log.info(f"小程序新用户注册: {openid}")
|
||||
|
||||
# 3. 生成token
|
||||
token_data = await cls._create_miniapp_token(redis=redis, user_id=user.id, openid=openid)
|
||||
|
||||
return MiniappLoginOutSchema(
|
||||
access_token=token_data["access_token"],
|
||||
token_type="Bearer",
|
||||
expires_in=token_data["expires_in"],
|
||||
user=MiniappUserOutSchema.model_validate(user)
|
||||
).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_user_info_service(cls, auth: AuthSchema, user_id: int) -> dict:
|
||||
"""获取用户信息"""
|
||||
user = await MiniappUserCRUD(auth).get_by_id_crud(id=user_id)
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
return MiniappUserOutSchema.model_validate(user).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def _get_wx_session(cls, code: str) -> dict:
|
||||
"""
|
||||
调用微信接口获取session信息
|
||||
|
||||
注意: 需要在配置中设置 MINIAPP_APPID 和 MINIAPP_SECRET
|
||||
"""
|
||||
appid = getattr(settings, "MINIAPP_APPID", None)
|
||||
secret = getattr(settings, "MINIAPP_SECRET", None)
|
||||
|
||||
if not appid or not secret:
|
||||
# 开发环境模拟返回
|
||||
log.warning("未配置小程序appid和secret,使用模拟数据")
|
||||
return {
|
||||
"openid": f"mock_openid_{code[:8]}",
|
||||
"session_key": "mock_session_key",
|
||||
"unionid": None
|
||||
}
|
||||
|
||||
params = {
|
||||
"appid": appid,
|
||||
"secret": secret,
|
||||
"js_code": code,
|
||||
"grant_type": "authorization_code"
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(cls.WX_LOGIN_URL, params=params)
|
||||
result = response.json()
|
||||
|
||||
if "errcode" in result and result["errcode"] != 0:
|
||||
log.error(f"微信登录失败: {result}")
|
||||
raise CustomException(msg=f"微信登录失败: {result.get('errmsg', '未知错误')}")
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def _create_miniapp_token(cls, redis: Redis, user_id: int, openid: str) -> dict:
|
||||
"""创建小程序用户token"""
|
||||
session_id = str(uuid.uuid4())
|
||||
access_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
now = datetime.now()
|
||||
|
||||
session_info = json.dumps({
|
||||
"session_id": session_id,
|
||||
"user_id": user_id,
|
||||
"openid": openid,
|
||||
"login_type": "miniapp"
|
||||
})
|
||||
|
||||
access_token = create_access_token(payload=JWTPayloadSchema(
|
||||
sub=session_info,
|
||||
is_refresh=False,
|
||||
exp=now + access_expires,
|
||||
))
|
||||
|
||||
# 存储到Redis
|
||||
await RedisCURD(redis).set(
|
||||
key=f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:miniapp:{session_id}',
|
||||
value=access_token,
|
||||
expire=int(access_expires.total_seconds())
|
||||
)
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"expires_in": int(access_expires.total_seconds())
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.common.response import SuccessResponse
|
||||
from app.common.request import PaginationService
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.logger import log
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from .service import ApplicationService
|
||||
from .schema import (
|
||||
ApplicationCreateSchema,
|
||||
ApplicationUpdateSchema,
|
||||
ApplicationQueryParam
|
||||
)
|
||||
|
||||
|
||||
MyAppRouter = APIRouter(route_class=OperationLogRoute, prefix="/myapp", tags=["应用管理"])
|
||||
|
||||
@MyAppRouter.get("/detail/{id}", summary="获取应用详情", description="获取应用详情")
|
||||
async def get_obj_detail_controller(
|
||||
id: int = Path(..., description="应用ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:myapp:query"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取应用详情
|
||||
|
||||
参数:
|
||||
- id (int): 应用ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含应用详情的JSON响应
|
||||
"""
|
||||
result_dict = await ApplicationService.detail_service(id=id, auth=auth)
|
||||
log.info(f"获取应用详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取应用详情成功")
|
||||
|
||||
@MyAppRouter.get("/list", summary="查询应用列表", description="查询应用列表")
|
||||
async def get_obj_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: ApplicationQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:myapp:query"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询应用列表
|
||||
|
||||
参数:
|
||||
- page (PaginationQueryParam): 分页参数模型
|
||||
- search (ApplicationQueryParam): 查询参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含应用列表的JSON响应
|
||||
"""
|
||||
result_dict_list = await ApplicationService.list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
|
||||
log.info(f"查询应用列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询应用列表成功")
|
||||
|
||||
@MyAppRouter.post("/create", summary="创建应用", description="创建应用")
|
||||
async def create_obj_controller(
|
||||
data: ApplicationCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:myapp:create"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
创建应用
|
||||
|
||||
参数:
|
||||
- data (ApplicationCreateSchema): 应用创建模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含创建应用详情的JSON响应
|
||||
"""
|
||||
result_dict = await ApplicationService.create_service(auth=auth, data=data)
|
||||
log.info(f"创建应用成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="创建应用成功")
|
||||
|
||||
@MyAppRouter.put("/update/{id}", summary="修改应用", description="修改应用")
|
||||
async def update_obj_controller(
|
||||
data: ApplicationUpdateSchema,
|
||||
id: int = Path(..., description="应用ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:myapp:update"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
修改应用
|
||||
|
||||
参数:
|
||||
- data (ApplicationUpdateSchema): 应用更新模型
|
||||
- id (int): 应用ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含修改应用详情的JSON响应
|
||||
"""
|
||||
result_dict = await ApplicationService.update_service(auth=auth, id=id, data=data)
|
||||
log.info(f"修改应用成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="修改应用成功")
|
||||
|
||||
@MyAppRouter.delete("/delete", summary="删除应用", description="删除应用")
|
||||
async def delete_obj_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:myapp:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除应用
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 应用ID列表
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含删除应用详情的JSON响应
|
||||
"""
|
||||
await ApplicationService.delete_service(auth=auth, ids=ids)
|
||||
log.info(f"删除应用成功: {ids}")
|
||||
return SuccessResponse(msg="删除应用成功")
|
||||
|
||||
@MyAppRouter.patch("/available/setting", summary="批量修改应用状态", description="批量修改应用状态")
|
||||
async def batch_set_available_obj_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:myapp:patch"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
批量修改应用状态
|
||||
|
||||
参数:
|
||||
- data (BatchSetAvailable): 批量修改应用状态模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 批量修改应用状态成功
|
||||
"""
|
||||
await ApplicationService.set_available_service(auth=auth, data=data)
|
||||
log.info(f"批量修改应用状态成功: {data.ids}")
|
||||
return SuccessResponse(msg="批量修改应用状态成功")
|
||||
@@ -0,0 +1,101 @@
|
||||
'''
|
||||
Author: caoziyuan ziyuan.cao@zhuying.com
|
||||
Date: 2025-12-15 17:37:50
|
||||
LastEditors: caoziyuan ziyuan.cao@zhuying.com
|
||||
LastEditTime: 2025-12-22 17:26:54
|
||||
FilePath: \backend\app\api\v1\module_application\myapp\crud.py
|
||||
Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Sequence, Any
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import ApplicationModel
|
||||
from .schema import ApplicationCreateSchema, ApplicationUpdateSchema
|
||||
|
||||
|
||||
class ApplicationCRUD(CRUDBase[ApplicationModel, ApplicationCreateSchema, ApplicationUpdateSchema]):
|
||||
"""应用系统数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化应用CRUD
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
self.auth = auth
|
||||
super().__init__(model=ApplicationModel, auth=auth)
|
||||
|
||||
async def get_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> ApplicationModel | None:
|
||||
"""
|
||||
根据id获取应用详情
|
||||
|
||||
参数:
|
||||
- id (int): 应用ID
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- ApplicationModel | None: 应用详情,如果不存在则为None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def list_crud(self, search: dict[str, Any] | None = None, order_by: list[dict[str, str]] | None = None, preload: list[str | Any] | None = None) -> Sequence[ApplicationModel]:
|
||||
"""
|
||||
列表查询应用
|
||||
|
||||
参数:
|
||||
- search (dict[str, Any] | None): 查询参数,默认None
|
||||
- order_by (list[dict[str, str]] | None): 排序参数,默认None
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[ApplicationModel]: 应用列表
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_crud(self, data: ApplicationCreateSchema) -> ApplicationModel | None:
|
||||
"""
|
||||
创建应用
|
||||
|
||||
参数:
|
||||
- data (ApplicationCreateSchema): 应用创建模型
|
||||
|
||||
返回:
|
||||
- ApplicationModel | None: 创建的应用详情,如果创建失败则为None
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_crud(self, id: int, data: ApplicationUpdateSchema) -> ApplicationModel | None:
|
||||
"""
|
||||
更新应用
|
||||
|
||||
参数:
|
||||
- id (int): 应用ID
|
||||
- data (ApplicationUpdateSchema): 应用更新模型
|
||||
|
||||
返回:
|
||||
- ApplicationModel | None: 更新后的应用详情,如果更新失败则为None
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
批量删除应用
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 应用ID列表
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def set_available_crud(self, ids: list[int], status: str) -> None:
|
||||
"""
|
||||
批量设置可用状态
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 应用ID列表
|
||||
- status (str): 可用状态,True为可用,False为不可用
|
||||
"""
|
||||
return await self.set(ids=ids, status=status)
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class ApplicationModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
应用系统表
|
||||
"""
|
||||
__tablename__: str = 'app_myapp'
|
||||
__table_args__: dict[str, str] = ({'comment': '应用系统表'})
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False, comment='应用名称')
|
||||
access_url: Mapped[str] = mapped_column(String(500), nullable=False, comment='访问地址')
|
||||
icon_url: Mapped[str | None] = mapped_column(String(300), nullable=True, comment='应用图标URL')
|
||||
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from urllib.parse import urlparse
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
|
||||
|
||||
class ApplicationCreateSchema(BaseModel):
|
||||
"""应用创建模型"""
|
||||
name: str = Field(..., max_length=64, description='应用名称')
|
||||
access_url: str = Field(..., max_length=255, description="访问地址")
|
||||
icon_url: str | None = Field(None, max_length=300, description="应用图标URL")
|
||||
status: str = Field("0", description="是否启用(0:启用 1:禁用)")
|
||||
description: str | None = Field(default=None, max_length=255, description="描述")
|
||||
|
||||
@field_validator('access_url')
|
||||
@classmethod
|
||||
def _validate_access_url(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if not v:
|
||||
raise ValueError('访问地址不能为空')
|
||||
parsed = urlparse(v)
|
||||
if parsed.scheme not in ('http', 'https'):
|
||||
raise ValueError('访问地址必须为 http/https URL')
|
||||
return v
|
||||
|
||||
@field_validator('icon_url')
|
||||
@classmethod
|
||||
def _validate_icon_url(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
v = v.strip()
|
||||
if v == "":
|
||||
return None
|
||||
parsed = urlparse(v)
|
||||
if parsed.scheme not in ('http', 'https'):
|
||||
raise ValueError('应用图标URL必须为 http/https URL')
|
||||
return v
|
||||
|
||||
|
||||
class ApplicationUpdateSchema(ApplicationCreateSchema):
|
||||
"""应用更新模型"""
|
||||
...
|
||||
|
||||
|
||||
class ApplicationOutSchema(ApplicationCreateSchema, BaseSchema, UserBySchema):
|
||||
"""应用响应模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ApplicationQueryParam:
|
||||
"""应用系统查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = Query(None, description="应用名称"),
|
||||
status: str | None = Query(None, description="是否启用"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
created_id: int | None = Query(None, description="创建人"),
|
||||
updated_id: int | None = Query(None, description="更新人"),
|
||||
) -> None:
|
||||
|
||||
# 模糊查询字段
|
||||
self.name = ("like", name) if name else None
|
||||
|
||||
# 精确查询字段
|
||||
self.status = status
|
||||
self.created_id = created_id
|
||||
self.updated_id = updated_id
|
||||
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
@@ -0,0 +1,133 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.exceptions import CustomException
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .schema import (
|
||||
ApplicationCreateSchema,
|
||||
ApplicationUpdateSchema,
|
||||
ApplicationOutSchema,
|
||||
ApplicationQueryParam
|
||||
)
|
||||
from .crud import ApplicationCRUD
|
||||
|
||||
|
||||
class ApplicationService:
|
||||
"""
|
||||
应用系统管理服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""
|
||||
获取应用详情
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 应用ID
|
||||
|
||||
返回:
|
||||
- dict: 应用详情字典
|
||||
"""
|
||||
obj = await ApplicationCRUD(auth).get_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='应用不存在')
|
||||
return ApplicationOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def list_service(cls, auth: AuthSchema, search: ApplicationQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> list[dict]:
|
||||
"""
|
||||
获取应用列表
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- search (ApplicationQueryParam | None): 查询参数模型
|
||||
- order_by (list[dict[str, str]] | None): 排序参数,支持字符串或字典列表
|
||||
|
||||
返回:
|
||||
- list[dict]: 应用详情字典列表
|
||||
"""
|
||||
# 过滤空值
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await ApplicationCRUD(auth).list_crud(search=search_dict, order_by=order_by)
|
||||
return [ApplicationOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def create_service(cls, auth: AuthSchema, data: ApplicationCreateSchema) -> dict:
|
||||
"""
|
||||
创建应用
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- data (ApplicationCreateSchema): 应用创建模型
|
||||
|
||||
返回:
|
||||
- Dict: 应用详情字典
|
||||
"""
|
||||
# 检查名称是否重复
|
||||
obj = await ApplicationCRUD(auth).get(name=data.name)
|
||||
if obj:
|
||||
raise CustomException(msg='创建失败,应用名称已存在')
|
||||
|
||||
obj = await ApplicationCRUD(auth).create_crud(data=data)
|
||||
return ApplicationOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_service(cls, auth: AuthSchema, id: int, data: ApplicationUpdateSchema) -> dict:
|
||||
"""
|
||||
更新应用
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 应用ID
|
||||
- data (ApplicationUpdateSchema): 应用更新模型
|
||||
|
||||
返回:
|
||||
- Dict: 应用详情字典
|
||||
"""
|
||||
obj = await ApplicationCRUD(auth).get_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='更新失败,该应用不存在')
|
||||
|
||||
# 检查名称重复
|
||||
exist_obj = await ApplicationCRUD(auth).get(name=data.name)
|
||||
if exist_obj and exist_obj.id != id:
|
||||
raise CustomException(msg='更新失败,应用名称重复')
|
||||
|
||||
obj = await ApplicationCRUD(auth).update_crud(id=id, data=data)
|
||||
return ApplicationOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""
|
||||
删除应用
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- ids (list[int]): 应用ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
for id in ids:
|
||||
obj = await ApplicationCRUD(auth).get_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg=f'删除失败,应用 {id} 不存在')
|
||||
await ApplicationCRUD(auth).delete_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""
|
||||
批量设置应用状态
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- data (BatchSetAvailable): 批量设置应用状态模型
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
await ApplicationCRUD(auth).set_available_crud(ids=data.ids, status=data.status)
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 工作流接口-开发中...
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.common.response import SuccessResponse
|
||||
from app.core.dependencies import db_getter, get_current_user
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.api.v1.module_common.activity.crud import ActivityCRUD
|
||||
|
||||
# 定义路由前缀
|
||||
ActivityRouter = APIRouter(prefix="/activity", tags=["活动记录"])
|
||||
|
||||
|
||||
@ActivityRouter.get("/display", summary="获取活动展示列表", description="获取前端展示格式的用户活动记录(无需登录)")
|
||||
async def get_activity_display(
|
||||
limit: int = 10,
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
) -> SuccessResponse:
|
||||
"""
|
||||
获取活动展示列表(公开接口,无需登录)
|
||||
|
||||
返回格式: ["张** 预约了 宝宝起名 服务", "李** 完成了 个人改名 测算", ...]
|
||||
"""
|
||||
# 创建一个不需要用户信息的 auth 对象
|
||||
auth = AuthSchema(db=db, check_data_scope=False)
|
||||
crud = ActivityCRUD(auth)
|
||||
|
||||
data = await crud.get_display_list(limit=limit)
|
||||
return SuccessResponse(data=data)
|
||||
|
||||
|
||||
@ActivityRouter.post("/create", summary="创建活动记录", description="创建活动记录(需要登录)")
|
||||
async def create_activity(
|
||||
user_name: str,
|
||||
action: str,
|
||||
service_name: str,
|
||||
service_type: str = None,
|
||||
sort_order: int = 0,
|
||||
auth: AuthSchema = Depends(get_current_user)
|
||||
) -> SuccessResponse:
|
||||
"""
|
||||
创建活动记录(需要登录)
|
||||
"""
|
||||
crud = ActivityCRUD(auth)
|
||||
|
||||
data = {
|
||||
"user_name": user_name,
|
||||
"action": action,
|
||||
"service_name": service_name,
|
||||
"service_type": service_type,
|
||||
"sort_order": sort_order
|
||||
}
|
||||
|
||||
result = await crud.create(data)
|
||||
return SuccessResponse(data={"id": result.id}, msg="创建成功")
|
||||
@@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import List
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_common.activity.model import ActivityModel
|
||||
from app.api.v1.module_common.activity.schema import ActivityCreate, ActivityUpdate
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
|
||||
|
||||
class ActivityCRUD(CRUDBase[ActivityModel, ActivityCreate, ActivityUpdate]):
|
||||
"""活动记录CRUD"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
super().__init__(ActivityModel, auth)
|
||||
|
||||
async def get_display_list(self, limit: int = 10) -> List[str]:
|
||||
"""
|
||||
获取前端展示格式的活动列表
|
||||
|
||||
参数:
|
||||
- limit: 返回数量限制
|
||||
|
||||
返回:
|
||||
- List[str]: 格式化的活动文本列表
|
||||
"""
|
||||
activities = await self.list(
|
||||
search={"status": "0"},
|
||||
order_by=[{"sort_order": "asc"}, {"created_time": "desc"}]
|
||||
)
|
||||
|
||||
result = []
|
||||
for activity in activities[:limit]:
|
||||
text = f"{activity.user_name} {activity.action}了 {activity.service_name} {activity.service_type or ''}".strip()
|
||||
result.append(text)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sqlalchemy import String, Integer
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin
|
||||
|
||||
|
||||
class ActivityModel(ModelMixin):
|
||||
"""用户活动记录表"""
|
||||
|
||||
__tablename__ = "biz_activity"
|
||||
|
||||
user_name: Mapped[str] = mapped_column(String(50), nullable=False, comment="用户名(脱敏)")
|
||||
action: Mapped[str] = mapped_column(String(50), nullable=False, comment="操作类型(预约/完成/购买/查看)")
|
||||
service_name: Mapped[str] = mapped_column(String(100), nullable=False, comment="服务名称")
|
||||
service_type: Mapped[str] = mapped_column(String(50), nullable=True, comment="服务类型")
|
||||
sort_order: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="排序顺序")
|
||||
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ActivityBase(BaseModel):
|
||||
"""活动记录基础模型"""
|
||||
user_name: str = Field(..., max_length=50, description="用户名(脱敏)")
|
||||
action: str = Field(..., max_length=50, description="操作类型")
|
||||
service_name: str = Field(..., max_length=100, description="服务名称")
|
||||
service_type: Optional[str] = Field(None, max_length=50, description="服务类型")
|
||||
sort_order: int = Field(default=0, description="排序顺序")
|
||||
|
||||
|
||||
class ActivityCreate(ActivityBase):
|
||||
"""创建活动记录"""
|
||||
pass
|
||||
|
||||
|
||||
class ActivityUpdate(BaseModel):
|
||||
"""更新活动记录"""
|
||||
user_name: Optional[str] = Field(None, max_length=50)
|
||||
action: Optional[str] = Field(None, max_length=50)
|
||||
service_name: Optional[str] = Field(None, max_length=100)
|
||||
service_type: Optional[str] = Field(None, max_length=50)
|
||||
sort_order: Optional[int] = None
|
||||
|
||||
|
||||
class ActivityOut(ActivityBase):
|
||||
"""活动记录输出模型"""
|
||||
id: int
|
||||
created_time: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ActivityDisplay(BaseModel):
|
||||
"""前端展示格式"""
|
||||
text: str = Field(..., description="展示文本")
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,49 @@
|
||||
'''
|
||||
Author: caoziyuan ziyuan.cao@zhuying.com
|
||||
Date: 2025-12-23 14:08:15
|
||||
LastEditors: caoziyuan ziyuan.cao@zhuying.com
|
||||
LastEditTime: 2025-12-23 14:24:55
|
||||
FilePath: \naming-backend\app\api\v1\module_common\calendar\controller.py
|
||||
Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import date
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from app.common.response import SuccessResponse
|
||||
from app.api.v1.module_common.calendar.service import CalendarService
|
||||
|
||||
# 定义路由
|
||||
CalendarRouter = APIRouter(prefix="/calendar", tags=["万年历"])
|
||||
|
||||
|
||||
@CalendarRouter.get("/today", summary="获取今日万年历", description="获取今天的农历信息、宜忌等(无需登录)")
|
||||
async def get_today_calendar() -> SuccessResponse:
|
||||
"""
|
||||
获取今日万年历信息
|
||||
|
||||
返回格式:
|
||||
{
|
||||
"lunar": "腊月 十二",
|
||||
"year": "甲辰年",
|
||||
"solar": "2024.01.22",
|
||||
"yi": ["出行", "开市", "交易", "裁衣"],
|
||||
"ji": ["动土", "安葬", "破土", "作灶"]
|
||||
}
|
||||
"""
|
||||
data = CalendarService.get_calendar_info()
|
||||
return SuccessResponse(data=data)
|
||||
|
||||
|
||||
@CalendarRouter.get("/date", summary="获取指定日期万年历", description="获取指定日期的农历信息、宜忌等(无需登录)")
|
||||
async def get_date_calendar(
|
||||
year: int = Query(..., description="年份,如:2024"),
|
||||
month: int = Query(..., ge=1, le=12, description="月份,1-12"),
|
||||
day: int = Query(..., ge=1, le=31, description="日期,1-31")
|
||||
) -> SuccessResponse:
|
||||
"""
|
||||
获取指定日期的万年历信息
|
||||
"""
|
||||
data = CalendarService.get_calendar_info(year, month, day)
|
||||
return SuccessResponse(data=data)
|
||||
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import List
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class CalendarOut(BaseModel):
|
||||
"""万年历返回数据结构"""
|
||||
lunar: str = Field(..., description="农历日期,如:腊月 十二")
|
||||
year: str = Field(..., description="农历年份,如:甲辰年")
|
||||
solar: str = Field(..., description="公历日期,如:2024.01.22")
|
||||
yi: List[str] = Field(default=[], description="宜做的事情")
|
||||
ji: List[str] = Field(default=[], description="忌做的事情")
|
||||
@@ -0,0 +1,141 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from functools import lru_cache
|
||||
from openai import OpenAI
|
||||
from cnlunar import Lunar
|
||||
|
||||
from app.config.setting import settings
|
||||
|
||||
|
||||
class CalendarService:
|
||||
"""万年历服务 - 使用 cnlunar 获取农历信息,DeepSeek 生成宜忌"""
|
||||
|
||||
# 内存缓存,存储日期对应的宜忌数据
|
||||
_yi_ji_cache: dict = {}
|
||||
|
||||
@classmethod
|
||||
def get_calendar_info(cls, year: Optional[int] = None, month: Optional[int] = None, day: Optional[int] = None) -> dict:
|
||||
"""
|
||||
获取万年历信息
|
||||
|
||||
参数:
|
||||
- year: 年份,默认今年
|
||||
- month: 月份,默认本月
|
||||
- day: 日期,默认今天
|
||||
|
||||
返回:
|
||||
- dict: 万年历信息
|
||||
"""
|
||||
# 获取日期
|
||||
if year and month and day:
|
||||
solar_date = datetime(year, month, day)
|
||||
else:
|
||||
solar_date = datetime.now()
|
||||
|
||||
# 使用 cnlunar 获取农历信息
|
||||
lunar = Lunar(solar_date)
|
||||
|
||||
# 获取农历月份和日期
|
||||
lunar_month = lunar.lunarMonthCn # 如:腊月
|
||||
lunar_day = lunar.lunarDayCn # 如:十二
|
||||
|
||||
# 获取天干地支年份
|
||||
gan_zhi_year = lunar.year8Char # 如:甲辰
|
||||
|
||||
# 使用 DeepSeek 生成宜忌(带缓存)
|
||||
yi_list, ji_list = cls._get_yi_ji_cached(solar_date, lunar)
|
||||
|
||||
return {
|
||||
"lunar": f"{lunar_month} {lunar_day}",
|
||||
"year": f"{gan_zhi_year}年",
|
||||
"solar": solar_date.strftime("%Y.%m.%d"),
|
||||
"yi": yi_list,
|
||||
"ji": ji_list,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _get_yi_ji_cached(cls, solar_date: datetime, lunar: Lunar) -> tuple[List[str], List[str]]:
|
||||
"""
|
||||
获取宜忌数据(带缓存)
|
||||
|
||||
同一天的数据只调用一次 API,后续直接返回缓存
|
||||
"""
|
||||
# 用日期字符串作为缓存 key
|
||||
cache_key = solar_date.strftime("%Y-%m-%d")
|
||||
|
||||
# 如果缓存中有数据,直接返回
|
||||
if cache_key in cls._yi_ji_cache:
|
||||
return cls._yi_ji_cache[cache_key]
|
||||
|
||||
# 缓存中没有,调用 API 获取
|
||||
yi_list, ji_list = cls._get_yi_ji_from_deepseek(solar_date, lunar)
|
||||
|
||||
# 存入缓存
|
||||
cls._yi_ji_cache[cache_key] = (yi_list, ji_list)
|
||||
|
||||
# 限制缓存大小,最多保留 30 天的数据
|
||||
if len(cls._yi_ji_cache) > 30:
|
||||
# 删除最早的一条
|
||||
oldest_key = next(iter(cls._yi_ji_cache))
|
||||
del cls._yi_ji_cache[oldest_key]
|
||||
|
||||
return yi_list, ji_list
|
||||
|
||||
@classmethod
|
||||
def _get_yi_ji_from_deepseek(cls, solar_date: datetime, lunar: Lunar) -> tuple[List[str], List[str]]:
|
||||
"""
|
||||
调用 DeepSeek API 生成宜忌数据
|
||||
|
||||
参数:
|
||||
- solar_date: 公历日期
|
||||
- lunar: 农历对象
|
||||
|
||||
返回:
|
||||
- tuple: (宜列表, 忌列表)
|
||||
"""
|
||||
try:
|
||||
# 从配置文件读取 API 配置
|
||||
client = OpenAI(
|
||||
api_key=settings.DEEPSEEK_API_KEY,
|
||||
base_url=settings.DEEPSEEK_BASE_URL
|
||||
)
|
||||
|
||||
# 构建提示词
|
||||
prompt = f"""请根据以下日期信息,生成中国传统黄历的宜忌数据。
|
||||
|
||||
日期信息:
|
||||
- 公历:{solar_date.strftime("%Y年%m月%d日")}
|
||||
- 农历:{lunar.lunarMonthCn}{lunar.lunarDayCn}
|
||||
- 天干地支:{lunar.year8Char}年 {lunar.month8Char}月 {lunar.day8Char}日
|
||||
|
||||
请严格按照以下 JSON 格式返回,不要有其他内容:
|
||||
{{"yi": ["宜做的事1", "宜做的事2", "宜做的事3", "宜做的事4"], "ji": ["忌做的事1", "忌做的事2", "忌做的事3", "忌做的事4"]}}
|
||||
|
||||
要求:
|
||||
1. 宜和忌各返回4-6个项目
|
||||
2. 使用传统黄历术语,如:祭祀、祈福、嫁娶、出行、开市、动土、安葬等
|
||||
3. 只返回 JSON,不要有任何解释"""
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=settings.DEEPSEEK_MODEL,
|
||||
messages=[
|
||||
{"role": "system", "content": "你是一个精通中国传统黄历的专家,请根据日期生成准确的宜忌信息。"},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=200
|
||||
)
|
||||
|
||||
# 解析返回的 JSON
|
||||
result_text = response.choices[0].message.content.strip()
|
||||
result = json.loads(result_text)
|
||||
|
||||
return result.get("yi", []), result.get("ji", [])
|
||||
|
||||
except Exception as e:
|
||||
# 如果 API 调用失败,返回默认值
|
||||
print(f"DeepSeek API 调用失败: {e}")
|
||||
return ["祭祀", "祈福", "出行", "开市"], ["动土", "安葬", "破土", "作灶"]
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pathlib import Path
|
||||
from fastapi import APIRouter, BackgroundTasks, Body, Depends, UploadFile, Request
|
||||
from fastapi.responses import JSONResponse, FileResponse
|
||||
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.logger import log
|
||||
from app.common.response import SuccessResponse, UploadFileResponse
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.utils.upload_util import UploadUtil
|
||||
|
||||
from .service import FileService
|
||||
|
||||
|
||||
FileRouter = APIRouter(route_class=OperationLogRoute, prefix="/file", tags=["文件管理"])
|
||||
|
||||
@FileRouter.post("/upload", summary="上传文件", description="上传文件",dependencies=[Depends(AuthPermission(["module_common:file:upload"]))])
|
||||
async def upload_controller(
|
||||
file: UploadFile,
|
||||
request: Request,
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
上传文件
|
||||
|
||||
参数:
|
||||
- file (UploadFile): 上传的文件
|
||||
- request (Request): 请求对象
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含上传文件详情的JSON响应
|
||||
"""
|
||||
result_dict = await FileService.upload_service(base_url=str(request.base_url), file=file)
|
||||
log.info(f"上传文件成功 {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="上传文件成功")
|
||||
|
||||
@FileRouter.post("/download", summary="下载文件", description="下载文件", dependencies=[Depends(AuthPermission(["module_common:file:download"]))])
|
||||
async def download_controller(
|
||||
background_tasks: BackgroundTasks,
|
||||
file_path: str = Body(..., description="文件路径"),
|
||||
delete: bool = Body(False, description="是否删除文件"),
|
||||
) -> FileResponse:
|
||||
"""
|
||||
下载文件
|
||||
|
||||
参数:
|
||||
- background_tasks (BackgroundTasks): 后台任务对象
|
||||
- file_path (str): 文件路径
|
||||
- delete (bool): 是否删除文件
|
||||
|
||||
返回:
|
||||
- FileResponse: 包含下载文件的响应
|
||||
"""
|
||||
result = await FileService.download_service(file_path=file_path)
|
||||
if delete:
|
||||
background_tasks.add_task(UploadUtil.delete_file, Path(file_path))
|
||||
log.info(f"下载文件成功")
|
||||
return UploadFileResponse(file_path=result.file_path, filename=result.file_name)
|
||||
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
from pydantic.alias_generators import to_camel
|
||||
|
||||
|
||||
class ImportFieldModel(BaseModel):
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
|
||||
|
||||
base_column: str | None = Field(description='数据库字段名', default=None)
|
||||
excel_column: str | None = Field(description='excel字段名', default=None)
|
||||
default_value: str | None = Field(description='默认值', default=None)
|
||||
is_required: bool | None = Field(description='是否必传', default=None)
|
||||
selected: bool | None = Field(description='是否勾选', default=None)
|
||||
|
||||
@model_validator(mode='before')
|
||||
@classmethod
|
||||
def _normalize(cls, data):
|
||||
if isinstance(data, dict):
|
||||
for key in ('base_column', 'excel_column', 'default_value'):
|
||||
val = data.get(key)
|
||||
if isinstance(val, str):
|
||||
val = val.strip()
|
||||
if val == '':
|
||||
val = None
|
||||
data[key] = val
|
||||
# is_required 兼容转换
|
||||
val = data.get('is_required')
|
||||
if isinstance(val, str):
|
||||
lowered = val.strip().lower()
|
||||
if lowered in {'true', '1', 'y', 'yes'}:
|
||||
data['is_required'] = True
|
||||
elif lowered in {'false', '0', 'n', 'no'}:
|
||||
data['is_required'] = False
|
||||
return data
|
||||
|
||||
@model_validator(mode='after')
|
||||
def _validate(self):
|
||||
if self.selected and not (self.base_column and self.base_column.strip()):
|
||||
raise ValueError('选中字段必须提供数据库字段名')
|
||||
if self.is_required and not (self.excel_column and self.excel_column.strip()):
|
||||
raise ValueError('必传字段必须提供excel字段名')
|
||||
return self
|
||||
|
||||
|
||||
class ImportModel(BaseModel):
|
||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
|
||||
|
||||
table_name: str | None = Field(description='表名', default=None)
|
||||
sheet_name: str | None = Field(description='Sheet名', default=None)
|
||||
filed_info: list[ImportFieldModel] | None = Field(description='字段关联表', default=None)
|
||||
file_name: str | None = Field(description='文件名', default=None)
|
||||
|
||||
@model_validator(mode='after')
|
||||
def _validate(self):
|
||||
# excel_column 不重复(忽略 None)
|
||||
if self.filed_info:
|
||||
seen = set()
|
||||
for f in self.filed_info:
|
||||
if f.excel_column:
|
||||
key = f.excel_column.strip()
|
||||
if key in seen:
|
||||
raise ValueError('excel字段名存在重复')
|
||||
seen.add(key)
|
||||
return self
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Dict
|
||||
from fastapi import UploadFile
|
||||
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.base_schema import UploadResponseSchema, DownloadFileSchema
|
||||
from app.utils.upload_util import UploadUtil
|
||||
|
||||
|
||||
class FileService:
|
||||
"""
|
||||
文件管理服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def upload_service(cls, base_url: str, file: UploadFile, upload_type: str = 'local') -> Dict:
|
||||
"""
|
||||
上传文件。
|
||||
|
||||
参数:
|
||||
- base_url (str): 基础访问 URL。
|
||||
- file (UploadFile): 上传文件对象。
|
||||
- upload_type (str): 上传类型,'local' 或 'oss',默认 'local'。
|
||||
|
||||
返回:
|
||||
- Dict: 上传响应字典。
|
||||
|
||||
异常:
|
||||
- CustomException: 当未选择文件或上传类型错误时抛出。
|
||||
"""
|
||||
if not file:
|
||||
raise CustomException(msg="请选择要上传的文件")
|
||||
if upload_type == 'local':
|
||||
filename, filepath, file_url = await UploadUtil.upload_file(file=file, base_url=base_url)
|
||||
else:
|
||||
raise CustomException(msg="上传类型错误")
|
||||
|
||||
return UploadResponseSchema(
|
||||
file_path=f'{filepath}',
|
||||
file_name=filename,
|
||||
origin_name=file.filename,
|
||||
file_url=f'{file_url}',
|
||||
).model_dump()
|
||||
|
||||
|
||||
@classmethod
|
||||
async def download_service(cls, file_path: str) -> DownloadFileSchema:
|
||||
"""
|
||||
下载文件。
|
||||
|
||||
参数:
|
||||
- file_path (str): 文件路径。
|
||||
|
||||
返回:
|
||||
- DownloadFileSchema: 下载文件响应对象。
|
||||
|
||||
异常:
|
||||
- CustomException: 当未选择文件或文件不存在时抛出。
|
||||
"""
|
||||
if not file_path:
|
||||
raise CustomException(msg="请选择要下载的文件")
|
||||
if not UploadUtil.check_file_exists(file_path):
|
||||
raise CustomException(msg="文件不存在")
|
||||
file_name = UploadUtil.download_file(file_path)
|
||||
|
||||
return DownloadFileSchema(
|
||||
file_path=file_path,
|
||||
file_name=str(file_name),
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,125 @@
|
||||
'''
|
||||
Author: caoziyuan ziyuan.cao@zhuying.com
|
||||
Date: 2025-12-23 15:20:07
|
||||
LastEditors: caoziyuan ziyuan.cao@zhuying.com
|
||||
LastEditTime: 2025-12-23 15:41:59
|
||||
FilePath: \naming-backend\app\api\v1\module_common\goodname\controller.py
|
||||
Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Query, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.common.response import SuccessResponse
|
||||
from app.core.dependencies import db_getter, get_current_user
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.api.v1.module_common.goodname.crud import NameRecordCRUD
|
||||
from app.api.v1.module_common.goodname.schema import NameRecordCreate, NameRecordUpdate, NameRecordOut
|
||||
|
||||
# 定义路由
|
||||
GoodNameRouter = APIRouter(prefix="/goodname", tags=["佳名赏析"])
|
||||
|
||||
|
||||
@GoodNameRouter.get("/list", summary="获取佳名列表", description="获取佳名赏析列表(无需登录)")
|
||||
async def get_good_name_list(
|
||||
limit: int = Query(default=3, ge=1, le=50, description="返回数量"),
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
) -> SuccessResponse:
|
||||
"""
|
||||
获取佳名赏析列表
|
||||
|
||||
返回格式:
|
||||
[
|
||||
{"name": "清芷", "source": "楚辞", "desc": "岸芷汀兰,郁郁青青"},
|
||||
{"name": "云帆", "source": "行路难", "desc": "长风破浪会有时,直挂云帆济沧海"},
|
||||
...
|
||||
]
|
||||
"""
|
||||
auth = AuthSchema(db=db, check_data_scope=False)
|
||||
crud = NameRecordCRUD(auth)
|
||||
data = await crud.get_good_names_for_display(limit)
|
||||
return SuccessResponse(data=data)
|
||||
|
||||
|
||||
@GoodNameRouter.get("/random", summary="随机获取佳名", description="随机获取佳名赏析(无需登录)")
|
||||
async def get_random_good_names(
|
||||
count: int = Query(default=3, ge=1, le=10, description="返回数量"),
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
) -> SuccessResponse:
|
||||
"""
|
||||
随机获取佳名
|
||||
"""
|
||||
auth = AuthSchema(db=db, check_data_scope=False)
|
||||
crud = NameRecordCRUD(auth)
|
||||
data = await crud.get_random_names(count)
|
||||
return SuccessResponse(data=data)
|
||||
|
||||
|
||||
@GoodNameRouter.post("/create", summary="创建起名记录", description="创建起名记录(需要登录)")
|
||||
async def create_name_record(
|
||||
data: NameRecordCreate,
|
||||
auth: AuthSchema = Depends(get_current_user)
|
||||
) -> SuccessResponse:
|
||||
"""
|
||||
创建起名记录
|
||||
"""
|
||||
crud = NameRecordCRUD(auth)
|
||||
result = await crud.create(data)
|
||||
return SuccessResponse(data={"id": result.id}, msg="创建成功")
|
||||
|
||||
|
||||
@GoodNameRouter.get("/my", summary="获取我的起名记录", description="获取当前用户的起名记录(需要登录)")
|
||||
async def get_my_name_records(
|
||||
page: int = Query(default=1, ge=1, description="页码"),
|
||||
size: int = Query(default=10, ge=1, le=100, description="每页数量"),
|
||||
auth: AuthSchema = Depends(get_current_user)
|
||||
) -> SuccessResponse:
|
||||
"""
|
||||
获取我的起名记录
|
||||
"""
|
||||
crud = NameRecordCRUD(auth)
|
||||
offset = (page - 1) * size
|
||||
result = await crud.page(
|
||||
offset=offset,
|
||||
limit=size,
|
||||
order_by=[{"created_time": "desc"}],
|
||||
search={"created_id": auth.user.id},
|
||||
out_schema=NameRecordOut
|
||||
)
|
||||
return SuccessResponse(data=result)
|
||||
|
||||
|
||||
@GoodNameRouter.put("/favorite/{record_id}", summary="收藏/取消收藏", description="收藏或取消收藏起名记录(需要登录)")
|
||||
async def toggle_favorite(
|
||||
record_id: int,
|
||||
auth: AuthSchema = Depends(get_current_user)
|
||||
) -> SuccessResponse:
|
||||
"""
|
||||
收藏/取消收藏起名记录
|
||||
"""
|
||||
crud = NameRecordCRUD(auth)
|
||||
record = await crud.get(id=record_id)
|
||||
if not record:
|
||||
return SuccessResponse(msg="记录不存在", success=False)
|
||||
|
||||
# 切换收藏状态
|
||||
new_status = 0 if record.is_favorite == 1 else 1
|
||||
await crud.update(record_id, {"is_favorite": new_status})
|
||||
|
||||
msg = "收藏成功" if new_status == 1 else "已取消收藏"
|
||||
return SuccessResponse(msg=msg)
|
||||
|
||||
|
||||
@GoodNameRouter.delete("/delete/{record_id}", summary="删除起名记录", description="删除起名记录(需要登录)")
|
||||
async def delete_name_record(
|
||||
record_id: int,
|
||||
auth: AuthSchema = Depends(get_current_user)
|
||||
) -> SuccessResponse:
|
||||
"""
|
||||
删除起名记录
|
||||
"""
|
||||
crud = NameRecordCRUD(auth)
|
||||
await crud.delete([record_id])
|
||||
return SuccessResponse(msg="删除成功")
|
||||
@@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import List
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_common.goodname.model import UserNameRecordModel
|
||||
from app.api.v1.module_common.goodname.schema import NameRecordCreate, NameRecordUpdate
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
|
||||
|
||||
class NameRecordCRUD(CRUDBase[UserNameRecordModel, NameRecordCreate, NameRecordUpdate]):
|
||||
"""起名记录 CRUD"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
super().__init__(UserNameRecordModel, auth)
|
||||
|
||||
async def get_good_names_for_display(self, limit: int = 10) -> List[dict]:
|
||||
"""
|
||||
获取佳名赏析列表(用于前端展示)
|
||||
|
||||
参数:
|
||||
- limit: 返回数量
|
||||
|
||||
返回:
|
||||
- List[dict]: 格式化的佳名列表
|
||||
"""
|
||||
records = await self.list(
|
||||
search={"status": "0"},
|
||||
order_by=[{"created_time": "desc"}]
|
||||
)
|
||||
|
||||
result = []
|
||||
for record in records[:limit]:
|
||||
result.append({
|
||||
"name": record.name,
|
||||
"source": record.source or "",
|
||||
"desc": record.source_text or record.meaning or ""
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
async def get_random_names(self, count: int = 3) -> List[dict]:
|
||||
"""
|
||||
随机获取佳名
|
||||
|
||||
参数:
|
||||
- count: 返回数量
|
||||
|
||||
返回:
|
||||
- List[dict]: 随机佳名列表
|
||||
"""
|
||||
import random
|
||||
|
||||
records = await self.list(
|
||||
search={"status": "0"},
|
||||
order_by=[{"id": "asc"}]
|
||||
)
|
||||
|
||||
if not records:
|
||||
return []
|
||||
|
||||
# 随机选择
|
||||
selected = random.sample(list(records), min(count, len(records)))
|
||||
|
||||
result = []
|
||||
for record in selected:
|
||||
result.append({
|
||||
"name": record.name,
|
||||
"source": record.source or "",
|
||||
"desc": record.source_text or record.meaning or ""
|
||||
})
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,57 @@
|
||||
'''
|
||||
Author: caoziyuan ziyuan.cao@zhuying.com
|
||||
Date: 2025-12-23 15:22:53
|
||||
LastEditors: caoziyuan ziyuan.cao@zhuying.com
|
||||
LastEditTime: 2025-12-23 15:41:35
|
||||
FilePath: \naming-backend\app\api\v1\module_common\goodname\model.py
|
||||
Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
'''
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sqlalchemy import String, Integer, Text, SmallInteger
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class UserNameRecordModel(ModelMixin, UserMixin):
|
||||
"""用户起名记录表"""
|
||||
|
||||
__tablename__ = "biz_user_name_record"
|
||||
|
||||
# ========== 基本信息 ==========
|
||||
name: Mapped[str] = mapped_column(String(50), nullable=False, comment="起的名字")
|
||||
surname: Mapped[str | None] = mapped_column(String(20), nullable=True, comment="姓氏")
|
||||
full_name: Mapped[str | None] = mapped_column(String(70), nullable=True, comment="完整姓名")
|
||||
|
||||
# ========== 名字来源 ==========
|
||||
source: Mapped[str | None] = mapped_column(String(100), nullable=True, comment="出处(如:诗经、楚辞)")
|
||||
source_text: Mapped[str | None] = mapped_column(Text, nullable=True, comment="原文/出处原句")
|
||||
meaning: Mapped[str | None] = mapped_column(Text, nullable=True, comment="名字含义/寓意")
|
||||
# ========== 起名对象信息 ==========
|
||||
gender: Mapped[int | None] = mapped_column(SmallInteger, default=0, nullable=True, comment="性别(0:未知 1:男 2:女)")
|
||||
birth_year: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="出生年份")
|
||||
birth_month: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="出生月份")
|
||||
birth_day: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="出生日期")
|
||||
birth_hour: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="出生时辰(0-23)")
|
||||
lunar_birth: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="农历生日")
|
||||
|
||||
# ========== 五行八字 ==========
|
||||
wuxing: Mapped[str | None] = mapped_column(String(20), nullable=True, comment="五行属性(如:金木水火土)")
|
||||
bazi: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="八字")
|
||||
wuxing_lack: Mapped[str | None] = mapped_column(String(20), nullable=True, comment="五行缺失")
|
||||
|
||||
# ========== 起名类型 ==========
|
||||
name_type: Mapped[int | None] = mapped_column(SmallInteger, default=1, nullable=True, comment="起名类型(1:宝宝起名 2:成人改名 3:公司起名 4:店铺起名)")
|
||||
|
||||
# ========== 评分相关 ==========
|
||||
score: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="名字评分(0-100)")
|
||||
score_detail: Mapped[str | None] = mapped_column(Text, nullable=True, comment="评分详情(JSON)")
|
||||
|
||||
# ========== 用户操作 ==========
|
||||
is_favorite: Mapped[int] = mapped_column(SmallInteger, default=0, nullable=False, comment="是否收藏(0:否 1:是)")
|
||||
is_used: Mapped[int] = mapped_column(SmallInteger, default=0, nullable=False, comment="是否已使用(0:否 1:是)")
|
||||
|
||||
# ========== 来源渠道 ==========
|
||||
channel: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="来源渠道(如:小程序、APP、网页)")
|
||||
ip_address: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="用户IP地址")
|
||||
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class GoodNameItem(BaseModel):
|
||||
"""佳名赏析项(简化版,用于列表展示)"""
|
||||
name: str = Field(..., description="名字")
|
||||
source: str = Field(..., description="出处")
|
||||
desc: str = Field(..., description="原文/释义")
|
||||
|
||||
|
||||
class NameRecordCreate(BaseModel):
|
||||
"""创建起名记录"""
|
||||
name: str = Field(..., max_length=50, description="起的名字")
|
||||
surname: Optional[str] = Field(None, max_length=20, description="姓氏")
|
||||
full_name: Optional[str] = Field(None, max_length=70, description="完整姓名")
|
||||
source: Optional[str] = Field(None, max_length=100, description="出处")
|
||||
source_text: Optional[str] = Field(None, description="原文/出处原句")
|
||||
meaning: Optional[str] = Field(None, description="名字含义/寓意")
|
||||
gender: Optional[int] = Field(0, description="性别(0:未知 1:男 2:女)")
|
||||
birth_year: Optional[int] = Field(None, description="出生年份")
|
||||
birth_month: Optional[int] = Field(None, ge=1, le=12, description="出生月份")
|
||||
birth_day: Optional[int] = Field(None, ge=1, le=31, description="出生日期")
|
||||
birth_hour: Optional[int] = Field(None, ge=0, le=23, description="出生时辰")
|
||||
lunar_birth: Optional[str] = Field(None, max_length=50, description="农历生日")
|
||||
wuxing: Optional[str] = Field(None, max_length=20, description="五行属性")
|
||||
bazi: Optional[str] = Field(None, max_length=50, description="八字")
|
||||
wuxing_lack: Optional[str] = Field(None, max_length=20, description="五行缺失")
|
||||
name_type: Optional[int] = Field(1, description="起名类型(1:宝宝起名 2:成人改名 3:公司起名 4:店铺起名)")
|
||||
score: Optional[int] = Field(None, ge=0, le=100, description="名字评分")
|
||||
score_detail: Optional[str] = Field(None, description="评分详情(JSON)")
|
||||
channel: Optional[str] = Field(None, max_length=50, description="来源渠道")
|
||||
|
||||
|
||||
class NameRecordUpdate(BaseModel):
|
||||
"""更新起名记录"""
|
||||
name: Optional[str] = Field(None, max_length=50)
|
||||
surname: Optional[str] = Field(None, max_length=20)
|
||||
full_name: Optional[str] = Field(None, max_length=70)
|
||||
source: Optional[str] = Field(None, max_length=100)
|
||||
source_text: Optional[str] = None
|
||||
meaning: Optional[str] = None
|
||||
is_favorite: Optional[int] = Field(None, description="是否收藏(0:否 1:是)")
|
||||
is_used: Optional[int] = Field(None, description="是否已使用(0:否 1:是)")
|
||||
|
||||
|
||||
class NameRecordOut(BaseModel):
|
||||
"""起名记录输出"""
|
||||
id: int
|
||||
name: str
|
||||
surname: Optional[str] = None
|
||||
full_name: Optional[str] = None
|
||||
source: Optional[str] = None
|
||||
source_text: Optional[str] = None
|
||||
meaning: Optional[str] = None
|
||||
gender: Optional[int] = None
|
||||
birth_year: Optional[int] = None
|
||||
birth_month: Optional[int] = None
|
||||
birth_day: Optional[int] = None
|
||||
lunar_birth: Optional[str] = None
|
||||
wuxing: Optional[str] = None
|
||||
bazi: Optional[str] = None
|
||||
wuxing_lack: Optional[str] = None
|
||||
name_type: Optional[int] = None
|
||||
score: Optional[int] = None
|
||||
is_favorite: int = 0
|
||||
is_used: int = 0
|
||||
created_time: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import List
|
||||
|
||||
|
||||
class GoodNameService:
|
||||
"""佳名赏析服务"""
|
||||
|
||||
# 佳名数据
|
||||
GOOD_NAMES = [
|
||||
{"name": "清芷", "source": "楚辞", "desc": "岸芷汀兰,郁郁青青"},
|
||||
{"name": "云帆", "source": "行路难", "desc": "长风破浪会有时,直挂云帆济沧海"},
|
||||
{"name": "望舒", "source": "离骚", "desc": "前望舒使先驱兮,后飞廉使奔属"},
|
||||
{"name": "思齐", "source": "论语", "desc": "见贤思齐焉,见不贤而内自省也"},
|
||||
{"name": "若华", "source": "楚辞", "desc": "桂树丛生兮山之幽,偃蹇连蜷兮枝相缭"},
|
||||
{"name": "嘉言", "source": "尚书", "desc": "嘉言罔攸伏,野无遗贤"},
|
||||
{"name": "明哲", "source": "诗经", "desc": "既明且哲,以保其身"},
|
||||
{"name": "子衿", "source": "诗经", "desc": "青青子衿,悠悠我心"},
|
||||
{"name": "静姝", "source": "诗经", "desc": "静女其姝,俟我于城隅"},
|
||||
{"name": "修远", "source": "离骚", "desc": "路漫漫其修远兮,吾将上下而求索"},
|
||||
{"name": "星汉", "source": "观沧海", "desc": "日月之行,若出其中;星汉灿烂,若出其里"},
|
||||
{"name": "霁月", "source": "世说新语", "desc": "光风霁月,坦荡胸怀"},
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_good_names(cls, limit: int = 10) -> List[dict]:
|
||||
"""
|
||||
获取佳名列表
|
||||
|
||||
参数:
|
||||
- limit: 返回数量,默认10条
|
||||
|
||||
返回:
|
||||
- List[dict]: 佳名列表
|
||||
"""
|
||||
return cls.GOOD_NAMES[:limit]
|
||||
|
||||
@classmethod
|
||||
def get_random_names(cls, count: int = 3) -> List[dict]:
|
||||
"""
|
||||
随机获取佳名
|
||||
|
||||
参数:
|
||||
- count: 返回数量,默认3条
|
||||
|
||||
返回:
|
||||
- List[dict]: 随机佳名列表
|
||||
"""
|
||||
import random
|
||||
return random.sample(cls.GOOD_NAMES, min(count, len(cls.GOOD_NAMES)))
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
HealthRouter = APIRouter(prefix="", tags=["健康检查"])
|
||||
|
||||
@HealthRouter.get("/health", summary="健康检查", description="检查系统健康状态")
|
||||
async def health_check() -> JSONResponse:
|
||||
"""
|
||||
健康检查接口
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含健康状态的JSON响应
|
||||
"""
|
||||
return JSONResponse(content={"msg": "Healthy"}, status_code=200)
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, UploadFile
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
import urllib.parse
|
||||
|
||||
from app.common.response import StreamResponse, SuccessResponse
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .service import DemoService
|
||||
from .schema import (
|
||||
DemoCreateSchema,
|
||||
DemoUpdateSchema,
|
||||
DemoQueryParam
|
||||
)
|
||||
|
||||
|
||||
DemoRouter = APIRouter(route_class=OperationLogRoute, prefix="/demo", tags=["示例模块"])
|
||||
|
||||
@DemoRouter.get("/detail/{id}", summary="获取示例详情", description="获取示例详情")
|
||||
async def get_obj_detail_controller(
|
||||
id: int = Path(..., description="示例ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_gencode:demo:query"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取示例详情
|
||||
|
||||
参数:
|
||||
- id (int): 示例ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含示例详情的JSON响应
|
||||
"""
|
||||
result_dict = await DemoService.detail_service(id=id, auth=auth)
|
||||
log.info(f"获取示例详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取示例详情成功")
|
||||
|
||||
@DemoRouter.get("/list", summary="查询示例列表", description="查询示例列表")
|
||||
async def get_obj_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: DemoQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_gencode:demo:query"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询示例列表
|
||||
|
||||
参数:
|
||||
- page (PaginationQueryParam): 分页查询参数
|
||||
- search (DemoQueryParam): 查询参数
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含示例列表分页信息的JSON响应
|
||||
"""
|
||||
# 使用数据库分页而不是应用层分页
|
||||
result_dict = await DemoService.page_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
search=search,
|
||||
order_by=page.order_by
|
||||
)
|
||||
log.info("查询示例列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询示例列表成功")
|
||||
|
||||
@DemoRouter.post("/create", summary="创建示例", description="创建示例")
|
||||
async def create_obj_controller(
|
||||
data: DemoCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_gencode:demo:create"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
创建示例
|
||||
|
||||
参数:
|
||||
- data (DemoCreateSchema): 示例创建模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含创建示例详情的JSON响应
|
||||
"""
|
||||
result_dict = await DemoService.create_service(auth=auth, data=data)
|
||||
log.info(f"创建示例成功: {result_dict.get('name')}")
|
||||
return SuccessResponse(data=result_dict, msg="创建示例成功")
|
||||
|
||||
@DemoRouter.put("/update/{id}", summary="修改示例", description="修改示例")
|
||||
async def update_obj_controller(
|
||||
data: DemoUpdateSchema,
|
||||
id: int = Path(..., description="示例ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_gencode:demo:update"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
修改示例
|
||||
|
||||
参数:
|
||||
- data (DemoUpdateSchema): 示例更新模型
|
||||
- id (int): 示例ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含修改示例详情的JSON响应
|
||||
"""
|
||||
result_dict = await DemoService.update_service(auth=auth, id=id, data=data)
|
||||
log.info(f"修改示例成功: {result_dict.get('name')}")
|
||||
return SuccessResponse(data=result_dict, msg="修改示例成功")
|
||||
|
||||
@DemoRouter.delete("/delete", summary="删除示例", description="删除示例")
|
||||
async def delete_obj_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_gencode:demo:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除示例
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 示例ID列表
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含删除示例详情的JSON响应
|
||||
"""
|
||||
await DemoService.delete_service(auth=auth, ids=ids)
|
||||
log.info(f"删除示例成功: {ids}")
|
||||
return SuccessResponse(msg="删除示例成功")
|
||||
|
||||
@DemoRouter.patch("/available/setting", summary="批量修改示例状态", description="批量修改示例状态")
|
||||
async def batch_set_available_obj_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_gencode:demo:patch"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
批量修改示例状态
|
||||
|
||||
参数:
|
||||
- data (BatchSetAvailable): 批量修改示例状态模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含批量修改示例状态详情的JSON响应
|
||||
"""
|
||||
await DemoService.set_available_service(auth=auth, data=data)
|
||||
log.info(f"批量修改示例状态成功: {data.ids}")
|
||||
return SuccessResponse(msg="批量修改示例状态成功")
|
||||
|
||||
@DemoRouter.post('/export', summary="导出示例", description="导出示例")
|
||||
async def export_obj_list_controller(
|
||||
search: DemoQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_gencode:demo:export"]))
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
导出示例
|
||||
|
||||
参数:
|
||||
- search (DemoQueryParam): 查询参数
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- StreamingResponse: 包含示例列表的Excel文件流响应
|
||||
"""
|
||||
result_dict_list = await DemoService.list_service(search=search, auth=auth)
|
||||
export_result = await DemoService.batch_export_service(obj_list=result_dict_list)
|
||||
log.info('导出示例成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename=demo.xlsx'
|
||||
}
|
||||
)
|
||||
|
||||
@DemoRouter.post('/import', summary="导入示例", description="导入示例")
|
||||
async def import_obj_list_controller(
|
||||
file: UploadFile,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_gencode:demo:import"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
导入示例
|
||||
|
||||
参数:
|
||||
- file (UploadFile): 导入的Excel文件
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含导入示例详情的JSON响应
|
||||
"""
|
||||
batch_import_result = await DemoService.batch_import_service(file=file, auth=auth, update_support=True)
|
||||
log.info(f"导入示例成功: {batch_import_result}")
|
||||
return SuccessResponse(data=batch_import_result, msg="导入示例成功")
|
||||
|
||||
@DemoRouter.post('/download/template', summary="获取示例导入模板", description="获取示例导入模板", dependencies=[Depends(AuthPermission(["module_gencode:demo:download"]))])
|
||||
async def export_obj_template_controller() -> StreamingResponse:
|
||||
"""
|
||||
获取示例导入模板
|
||||
|
||||
返回:
|
||||
- StreamingResponse: 包含示例导入模板的Excel文件流响应
|
||||
"""
|
||||
import_template_result = await DemoService.import_template_download_service()
|
||||
log.info('获取示例导入模板成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(import_template_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={
|
||||
'Content-Disposition': f'attachment; filename={urllib.parse.quote("示例导入模板.xlsx")}',
|
||||
'Access-Control-Expose-Headers': 'Content-Disposition'
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,124 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from collections.abc import Sequence
|
||||
from app.core.base_crud import CRUDBase
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import DemoModel
|
||||
from .schema import DemoCreateSchema, DemoUpdateSchema, DemoOutSchema
|
||||
|
||||
|
||||
class DemoCRUD(CRUDBase[DemoModel, DemoCreateSchema, DemoUpdateSchema]):
|
||||
"""示例数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化CRUD数据层
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
super().__init__(model=DemoModel, auth=auth)
|
||||
|
||||
async def get_by_id_crud(self, id: int, preload: list[str] | None = None) -> DemoModel | None:
|
||||
"""
|
||||
详情
|
||||
|
||||
参数:
|
||||
- id (int): 示例ID
|
||||
- preload (list[str] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- DemoModel | None: 示例模型实例或None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def list_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list[str] | None = None) -> Sequence[DemoModel]:
|
||||
"""
|
||||
列表查询
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数
|
||||
- order_by (list[dict] | None): 排序参数
|
||||
- preload (list[str] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[DemoModel]: 示例模型实例序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_crud(self, data: DemoCreateSchema) -> DemoModel | None:
|
||||
"""
|
||||
创建
|
||||
|
||||
参数:
|
||||
- data (DemoCreateSchema): 示例创建模型
|
||||
|
||||
返回:
|
||||
- DemoModel | None: 示例模型实例或None
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_crud(self, id: int, data: DemoUpdateSchema) -> DemoModel | None:
|
||||
"""
|
||||
更新
|
||||
|
||||
参数:
|
||||
- id (int): 示例ID
|
||||
- data (DemoUpdateSchema): 示例更新模型
|
||||
|
||||
返回:
|
||||
- DemoModel | None: 示例模型实例或None
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
批量删除
|
||||
|
||||
参数:
|
||||
- ids (List[int]): 示例ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def set_available_crud(self, ids: list[int], status: str) -> None:
|
||||
"""
|
||||
批量设置可用状态
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 示例ID列表
|
||||
- status (str): 可用状态
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.set(ids=ids, status=status)
|
||||
|
||||
async def page_crud(self, offset: int, limit: int, order_by: list[dict] | None = None, search: dict | None = None, preload: list | None = None) -> dict:
|
||||
"""
|
||||
分页查询
|
||||
|
||||
参数:
|
||||
- offset (int): 偏移量
|
||||
- limit (int): 每页数量
|
||||
- order_by (list[dict] | None): 排序参数
|
||||
- search (dict | None): 查询参数
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- dict: 分页数据
|
||||
"""
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
search_dict = search or {}
|
||||
|
||||
return await self.page(
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
order_by=order_by_list,
|
||||
search=search_dict,
|
||||
out_schema=DemoOutSchema,
|
||||
preload=preload
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
'''
|
||||
Author: caoziyuan ziyuan.cao@zhuying.com
|
||||
Date: 2025-12-15 17:37:50
|
||||
LastEditors: caoziyuan ziyuan.cao@zhuying.com
|
||||
LastEditTime: 2025-12-22 17:25:22
|
||||
FilePath: \backend\app\api\v1\module_gencode\demo\model.py
|
||||
Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
'''
|
||||
# -*- 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='名称')
|
||||
@@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
|
||||
|
||||
class DemoCreateSchema(BaseModel):
|
||||
"""新增模型"""
|
||||
name: str = Field(..., min_length=2, max_length=50, description='名称')
|
||||
status: str = Field(default="0", description="是否启用(0:启用 1:禁用)")
|
||||
description: str | None = Field(default=None, max_length=255, description="描述")
|
||||
|
||||
@field_validator('name')
|
||||
@classmethod
|
||||
def validate_name(cls, v: str) -> str:
|
||||
"""验证名称字段的格式和内容"""
|
||||
# 去除首尾空格
|
||||
v = v.strip()
|
||||
if not v:
|
||||
raise ValueError('名称不能为空')
|
||||
return v
|
||||
|
||||
@model_validator(mode='after')
|
||||
def _after_validation(self):
|
||||
"""
|
||||
核心业务规则校验
|
||||
"""
|
||||
# 长度校验:名称最小长度
|
||||
if len(self.name) < 2 or len(self.name) > 50:
|
||||
raise ValueError('名称长度必须在2-50个字符之间')
|
||||
# 格式校验:名称只能包含字母、数字、下划线和中划线
|
||||
if not self.name.isalnum() and not all(c in '-_' for c in self.name):
|
||||
raise ValueError('名称只能包含字母、数字、下划线和中划线')
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class DemoUpdateSchema(DemoCreateSchema):
|
||||
"""更新模型"""
|
||||
...
|
||||
|
||||
|
||||
class DemoOutSchema(DemoCreateSchema, BaseSchema, UserBySchema):
|
||||
"""响应模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class DemoQueryParam:
|
||||
"""示例查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = Query(None, description="名称"),
|
||||
status: str | None = Query(None, description="是否启用"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
created_id: int | None = Query(None, description="创建人"),
|
||||
updated_id: int | None = Query(None, description="更新人"),
|
||||
) -> None:
|
||||
|
||||
# 模糊查询字段
|
||||
self.name = ("like", name)
|
||||
|
||||
# 精确查询字段
|
||||
self.created_id = created_id
|
||||
self.updated_id = updated_id
|
||||
self.status = status
|
||||
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
@@ -0,0 +1,312 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import io
|
||||
from typing import Any
|
||||
from fastapi import UploadFile
|
||||
import pandas as pd
|
||||
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.core.logger import log
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .schema import DemoCreateSchema, DemoUpdateSchema, DemoOutSchema, DemoQueryParam
|
||||
from .crud import DemoCRUD
|
||||
|
||||
|
||||
class DemoService:
|
||||
"""
|
||||
示例管理模块服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""
|
||||
详情
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 示例ID
|
||||
|
||||
返回:
|
||||
- dict: 示例模型实例字典
|
||||
"""
|
||||
obj = await DemoCRUD(auth).get_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="该数据不存在")
|
||||
return DemoOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def list_service(cls, auth: AuthSchema, search: DemoQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> list[dict]:
|
||||
"""
|
||||
列表查询
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- search (DemoQueryParam | None): 查询参数
|
||||
- order_by (list[dict[str, str]] | None): 排序参数
|
||||
|
||||
返回:
|
||||
- list[dict]: 示例模型实例字典列表
|
||||
"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await DemoCRUD(auth).list_crud(search=search_dict, order_by=order_by)
|
||||
return [DemoOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def page_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: DemoQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> dict:
|
||||
"""
|
||||
分页查询
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- page_no (int): 页码
|
||||
- page_size (int): 每页数量
|
||||
- search (DemoQueryParam | None): 查询参数
|
||||
- order_by (list[dict[str, str]] | None): 排序参数
|
||||
|
||||
返回:
|
||||
- dict: 分页数据
|
||||
"""
|
||||
search_dict = search.__dict__ if search else {}
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
offset = (page_no - 1) * page_size
|
||||
|
||||
result = await DemoCRUD(auth).page_crud(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by_list,
|
||||
search=search_dict
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def create_service(cls, auth: AuthSchema, data: DemoCreateSchema) -> dict:
|
||||
"""
|
||||
创建
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- data (DemoCreateSchema): 示例创建模型
|
||||
|
||||
返回:
|
||||
- dict: 示例模型实例字典
|
||||
"""
|
||||
obj = await DemoCRUD(auth).get(name=data.name)
|
||||
if obj:
|
||||
raise CustomException(msg='创建失败,名称已存在')
|
||||
obj = await DemoCRUD(auth).create_crud(data=data)
|
||||
return DemoOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_service(cls, auth: AuthSchema, id: int, data: DemoUpdateSchema) -> dict:
|
||||
"""
|
||||
更新
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 示例ID
|
||||
- data (DemoUpdateSchema): 示例更新模型
|
||||
|
||||
返回:
|
||||
- dict: 示例模型实例字典
|
||||
"""
|
||||
# 检查数据是否存在
|
||||
obj = await DemoCRUD(auth).get_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='更新失败,该数据不存在')
|
||||
|
||||
# 检查名称是否重复
|
||||
exist_obj = await DemoCRUD(auth).get(name=data.name)
|
||||
if exist_obj and exist_obj.id != id:
|
||||
raise CustomException(msg='更新失败,名称重复')
|
||||
|
||||
obj = await DemoCRUD(auth).update_crud(id=id, data=data)
|
||||
return DemoOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""
|
||||
删除
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- ids (list[int]): 示例ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
|
||||
# 检查所有要删除的数据是否存在
|
||||
for id in ids:
|
||||
obj = await DemoCRUD(auth).get_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
|
||||
|
||||
await DemoCRUD(auth).delete_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""
|
||||
批量设置状态
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- data (BatchSetAvailable): 批量设置状态模型
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
await DemoCRUD(auth).set_available_crud(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def batch_export_service(cls, obj_list: list[dict[str, Any]]) -> bytes:
|
||||
"""
|
||||
批量导出
|
||||
|
||||
参数:
|
||||
- obj_list (list[dict[str, Any]]): 示例模型实例字典列表
|
||||
|
||||
返回:
|
||||
- bytes: Excel文件字节流
|
||||
"""
|
||||
mapping_dict = {
|
||||
'id': '编号',
|
||||
'name': '名称',
|
||||
'status': '状态',
|
||||
'description': '备注',
|
||||
'created_time': '创建时间',
|
||||
'updated_time': '更新时间',
|
||||
'created_id': '创建者',
|
||||
}
|
||||
|
||||
# 复制数据并转换状态
|
||||
data = obj_list.copy()
|
||||
for item in data:
|
||||
# 处理状态
|
||||
item['status'] = '启用' if item.get('status') == '0' else '停用'
|
||||
# 处理创建者
|
||||
creator_info = item.get('created_id')
|
||||
if isinstance(creator_info, dict):
|
||||
item['created_id'] = creator_info.get('name', '未知')
|
||||
else:
|
||||
item['created_id'] = '未知'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@classmethod
|
||||
async def batch_import_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
|
||||
"""
|
||||
批量导入
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- file (UploadFile): 上传的Excel文件
|
||||
- update_support (bool): 是否支持更新存在数据
|
||||
|
||||
返回:
|
||||
- str: 导入结果信息
|
||||
"""
|
||||
|
||||
header_dict = {
|
||||
'名称': 'name',
|
||||
'状态': 'status',
|
||||
'描述': 'description'
|
||||
}
|
||||
|
||||
try:
|
||||
# 读取Excel文件
|
||||
contents = await file.read()
|
||||
df = pd.read_excel(io.BytesIO(contents))
|
||||
await file.close()
|
||||
|
||||
if df.empty:
|
||||
raise CustomException(msg="导入文件为空")
|
||||
|
||||
# 检查表头是否完整
|
||||
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
|
||||
if missing_headers:
|
||||
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
|
||||
|
||||
# 重命名列名
|
||||
df.rename(columns=header_dict, inplace=True)
|
||||
|
||||
# 验证必填字段
|
||||
required_fields = ['name', 'status']
|
||||
errors = []
|
||||
for field in required_fields:
|
||||
missing_rows = df[df[field].isnull()].index.tolist()
|
||||
if missing_rows:
|
||||
field_name = [k for k,v in header_dict.items() if v == field][0]
|
||||
rows_str = "、".join([str(i+1) for i in missing_rows])
|
||||
errors.append(f"{field_name}不能为空,第{rows_str}行")
|
||||
if errors:
|
||||
raise CustomException(msg=f"导入失败,以下行缺少必要字段:\n{'; '.join(errors)}")
|
||||
|
||||
error_msgs = []
|
||||
success_count = 0
|
||||
count = 0
|
||||
|
||||
# 处理每一行数据
|
||||
for index, row in df.iterrows():
|
||||
count += 1
|
||||
try:
|
||||
# 数据转换前的类型检查
|
||||
try:
|
||||
status = True if row['status'] == '正常' else False
|
||||
except ValueError:
|
||||
error_msgs.append(f"第{count}行: 状态必须是'正常'或'停用'")
|
||||
continue
|
||||
|
||||
# 构建用户数据
|
||||
data = {
|
||||
"name": str(row['name']),
|
||||
"status": status,
|
||||
"description": str(row['description']),
|
||||
}
|
||||
|
||||
# 处理用户导入
|
||||
exists_obj = await DemoCRUD(auth).get(name=data["name"])
|
||||
if exists_obj:
|
||||
if update_support:
|
||||
await DemoCRUD(auth).update(id=exists_obj.id, data=data)
|
||||
success_count += 1
|
||||
else:
|
||||
error_msgs.append(f"第{count}行: 对象 {data['name']} 已存在")
|
||||
else:
|
||||
await DemoCRUD(auth).create(data=data)
|
||||
success_count += 1
|
||||
|
||||
except Exception as e:
|
||||
error_msgs.append(f"第{count}行: {str(e)}")
|
||||
continue
|
||||
|
||||
# 返回详细的导入结果
|
||||
result = f"成功导入 {success_count} 条数据"
|
||||
if error_msgs:
|
||||
result += "\n错误信息:\n" + "\n".join(error_msgs)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"批量导入用户失败: {str(e)}")
|
||||
raise CustomException(msg=f"导入失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def import_template_download_service(cls) -> bytes:
|
||||
"""
|
||||
下载导入模板
|
||||
|
||||
返回:
|
||||
- bytes: Excel文件字节流
|
||||
"""
|
||||
header_list = ['名称', '状态', '描述']
|
||||
selector_header_list = ['状态']
|
||||
option_list = [{'状态': ['正常', '停用']}]
|
||||
return ExcelUtil.get_excel_template(
|
||||
header_list=header_list,
|
||||
selector_header_list=selector_header_list,
|
||||
option_list=option_list
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, Body, Path
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.common.response import SuccessResponse, StreamResponse
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.common.request import PaginationService
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
|
||||
from .schema import GenTableSchema, GenTableQueryParam
|
||||
from .service import GenTableService
|
||||
|
||||
|
||||
GenRouter = APIRouter(route_class=OperationLogRoute, prefix='/gencode', tags=["代码生成模块"])
|
||||
|
||||
|
||||
@GenRouter.get("/list", summary="查询代码生成业务表列表", description="查询代码生成业务表列表")
|
||||
async def gen_table_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: GenTableQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_generator:gencode:query"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询代码生成业务表列表
|
||||
|
||||
参数:
|
||||
- page (PaginationQueryParam): 分页查询参数
|
||||
- search (GenTableQueryParam): 搜索参数
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含查询结果和分页信息的JSON响应
|
||||
"""
|
||||
result_dict_list = await GenTableService.get_gen_table_list_service(auth=auth, search=search)
|
||||
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
|
||||
log.info('获取代码生成业务表列表成功')
|
||||
return SuccessResponse(data=result_dict, msg="获取代码生成业务表列表成功")
|
||||
|
||||
|
||||
@GenRouter.get("/db/list", summary="查询数据库表列表", description="查询数据库表列表")
|
||||
async def get_gen_db_table_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: GenTableQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_generator:dblist:query"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询数据库表列表
|
||||
|
||||
参数:
|
||||
- page (PaginationQueryParam): 分页查询参数
|
||||
- search (GenTableQueryParam): 搜索参数
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含查询结果和分页信息的JSON响应
|
||||
"""
|
||||
result_dict_list = await GenTableService.get_gen_db_table_list_service(auth=auth, search=search)
|
||||
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
|
||||
log.info('获取数据库表列表成功')
|
||||
return SuccessResponse(data=result_dict, msg="获取数据库表列表成功")
|
||||
|
||||
|
||||
@GenRouter.post("/import", summary="导入表结构", description="导入表结构")
|
||||
async def import_gen_table_controller(
|
||||
table_names: List[str] = Body(..., description="表名列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_generator:gencode:import"])),
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
导入表结构
|
||||
|
||||
参数:
|
||||
- table_names (List[str]): 表名列表
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含导入结果和导入的表结构列表的JSON响应
|
||||
"""
|
||||
add_gen_table_list = await GenTableService.get_gen_db_table_list_by_name_service(auth, table_names)
|
||||
result = await GenTableService.import_gen_table_service(auth, add_gen_table_list)
|
||||
log.info('导入表结构成功')
|
||||
return SuccessResponse(msg="导入表结构成功", data=result)
|
||||
|
||||
|
||||
@GenRouter.get("/detail/{table_id}", summary="获取业务表详细信息", description="获取业务表详细信息")
|
||||
async def gen_table_detail_controller(
|
||||
table_id: int = Path(..., description="业务表ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_generator:gencode:query"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取业务表详细信息
|
||||
|
||||
参数:
|
||||
- table_id (int): 业务表ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含业务表详细信息的JSON响应
|
||||
"""
|
||||
gen_table_detail_result = await GenTableService.get_gen_table_detail_service(auth, table_id)
|
||||
log.info(f'获取table_id为{table_id}的信息成功')
|
||||
return SuccessResponse(data=gen_table_detail_result, msg="获取业务表详细信息成功")
|
||||
|
||||
|
||||
@GenRouter.post("/create", summary="创建表结构", description="创建表结构")
|
||||
async def create_table_controller(
|
||||
sql: str = Body(..., description="SQL语句,用于创建表结构"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_generator:gencode:create"])),
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
创建表结构
|
||||
|
||||
参数:
|
||||
- sql (str): SQL语句,用于创建表结构
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含创建结果的JSON响应
|
||||
"""
|
||||
result = await GenTableService.create_table_service(auth, sql)
|
||||
log.info('创建表结构成功')
|
||||
return SuccessResponse(msg="创建表结构成功", data=result)
|
||||
|
||||
|
||||
@GenRouter.put("/update/{table_id}", summary="编辑业务表信息", description="编辑业务表信息")
|
||||
async def update_gen_table_controller(
|
||||
table_id: int = Path(..., description="业务表ID"),
|
||||
data: GenTableSchema = Body(..., description="业务表信息"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_generator:gencode:update"])),
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
编辑业务表信息
|
||||
|
||||
参数:
|
||||
- table_id (int): 业务表ID
|
||||
- data (GenTableSchema): 业务表信息模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含编辑结果的JSON响应
|
||||
"""
|
||||
result_dict = await GenTableService.update_gen_table_service(auth, data, table_id)
|
||||
log.info('编辑业务表信息成功')
|
||||
return SuccessResponse(data=result_dict, msg="编辑业务表信息成功")
|
||||
|
||||
|
||||
@GenRouter.delete("/delete", summary="删除业务表信息", description="删除业务表信息")
|
||||
async def delete_gen_table_controller(
|
||||
ids: List[int] = Body(..., description="业务表ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_generator:gencode:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除业务表信息
|
||||
|
||||
参数:
|
||||
- ids (List[int]): 业务表ID列表
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含删除结果的JSON响应
|
||||
"""
|
||||
result = await GenTableService.delete_gen_table_service(auth, ids)
|
||||
log.info('删除业务表信息成功')
|
||||
return SuccessResponse(msg="删除业务表信息成功", data=result)
|
||||
|
||||
|
||||
@GenRouter.patch("/batch/output", summary="批量生成代码", description="批量生成代码")
|
||||
async def batch_gen_code_controller(
|
||||
table_names: List[str] = Body(..., description="表名列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_generator:gencode:patch"]))
|
||||
) -> StreamResponse:
|
||||
"""
|
||||
批量生成代码
|
||||
|
||||
参数:
|
||||
- table_names (List[str]): 表名列表
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- StreamResponse: 包含批量生成代码的ZIP文件流响应
|
||||
"""
|
||||
batch_gen_code_result = await GenTableService.batch_gen_code_service(auth, table_names)
|
||||
log.info(f'批量生成代码成功,表名列表:{table_names}')
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(batch_gen_code_result),
|
||||
media_type='application/zip',
|
||||
headers={'Content-Disposition': 'attachment; filename=code.zip'}
|
||||
)
|
||||
|
||||
|
||||
@GenRouter.post("/output/{table_name}", summary="生成代码到指定路径", description="生成代码到指定路径")
|
||||
async def gen_code_local_controller(
|
||||
table_name: str = Path(..., description="表名"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_generator:gencode:code"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
生成代码到指定路径
|
||||
|
||||
参数:
|
||||
- table_name (str): 表名
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含生成结果的JSON响应
|
||||
"""
|
||||
result = await GenTableService.generate_code_service(auth, table_name)
|
||||
log.info(f'生成代码,表名:{table_name},到指定路径成功')
|
||||
return SuccessResponse(msg="生成代码到指定路径成功", data=result)
|
||||
|
||||
|
||||
@GenRouter.get("/preview/{table_id}", summary="预览代码", description="预览代码")
|
||||
async def preview_code_controller(
|
||||
table_id: int = Path(..., description="业务表ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_generator:gencode:query"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
预览代码
|
||||
|
||||
参数:
|
||||
- table_id (int): 业务表ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含预览代码的JSON响应
|
||||
"""
|
||||
preview_code_result = await GenTableService.preview_code_service(auth, table_id)
|
||||
log.info(f'预览代码,表id:{table_id},成功')
|
||||
return SuccessResponse(data=preview_code_result, msg="预览代码成功")
|
||||
|
||||
|
||||
@GenRouter.post("/sync_db/{table_name}", summary="同步数据库", description="同步数据库")
|
||||
async def sync_db_controller(
|
||||
table_name: str = Path(..., description="表名"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_generator:db:sync"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
同步数据库
|
||||
|
||||
参数:
|
||||
- table_name (str): 表名
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含同步数据库结果的JSON响应
|
||||
"""
|
||||
result = await GenTableService.sync_db_service(auth, table_name)
|
||||
log.info(f'同步数据库,表名:{table_name},成功')
|
||||
return SuccessResponse(msg="同步数据库成功", data=result)
|
||||
@@ -0,0 +1,575 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sqlalchemy.engine.row import Row
|
||||
from sqlalchemy import and_, select, text
|
||||
from typing import Sequence
|
||||
from sqlglot.expressions import Expression
|
||||
|
||||
from app.core.logger import log
|
||||
from app.config.setting import settings
|
||||
from app.core.base_crud import CRUDBase
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import GenTableModel, GenTableColumnModel
|
||||
from .schema import (
|
||||
GenTableSchema,
|
||||
GenTableColumnSchema,
|
||||
GenTableColumnOutSchema,
|
||||
GenDBTableSchema,
|
||||
GenTableQueryParam
|
||||
)
|
||||
|
||||
|
||||
class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
||||
"""代码生成业务表模块数据库操作层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化CRUD操作层
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
super().__init__(model=GenTableModel, auth=auth)
|
||||
|
||||
async def get_gen_table_by_id(self, table_id: int, preload: list | None = None) -> GenTableModel | None:
|
||||
"""
|
||||
根据业务表ID获取需要生成的业务表信息。
|
||||
|
||||
参数:
|
||||
- table_id (int): 业务表ID。
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- GenTableModel | None: 业务表信息对象。
|
||||
"""
|
||||
return await self.get(id=table_id, preload=preload)
|
||||
|
||||
async def get_gen_table_by_name(self, table_name: str, preload: list | None = None) -> GenTableModel | None:
|
||||
"""
|
||||
根据业务表名称获取需要生成的业务表信息。
|
||||
|
||||
参数:
|
||||
- table_name (str): 业务表名称。
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- GenTableModel | None: 业务表信息对象。
|
||||
"""
|
||||
return await self.get(table_name=table_name, preload=preload)
|
||||
|
||||
async def get_gen_table_all(self, preload: list | None = None) -> Sequence[GenTableModel]:
|
||||
"""
|
||||
获取所有业务表信息。
|
||||
|
||||
参数:
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[GenTableModel]: 所有业务表信息列表。
|
||||
"""
|
||||
return await self.list(preload=preload)
|
||||
|
||||
async def get_gen_table_list(self, search: GenTableQueryParam | None = None, preload: list | None = None) -> Sequence[GenTableModel]:
|
||||
"""
|
||||
根据查询参数获取代码生成业务表列表信息。
|
||||
|
||||
参数:
|
||||
- search (GenTableQueryParam | None): 查询参数对象。
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[GenTableModel]: 业务表列表信息。
|
||||
"""
|
||||
return await self.list(search=search.__dict__, order_by=[{"created_time": "desc"}], preload=preload)
|
||||
|
||||
async def add_gen_table(self, add_model: GenTableSchema) -> GenTableModel:
|
||||
"""
|
||||
新增业务表信息。
|
||||
|
||||
参数:
|
||||
- add_model (GenTableSchema): 新增业务表信息模型。
|
||||
|
||||
返回:
|
||||
- GenTableModel: 新增的业务表信息对象。
|
||||
"""
|
||||
return await self.create(data=add_model)
|
||||
|
||||
async def edit_gen_table(self, table_id: int, edit_model: GenTableSchema) -> GenTableModel:
|
||||
"""
|
||||
修改业务表信息。
|
||||
|
||||
参数:
|
||||
- table_id (int): 业务表ID。
|
||||
- edit_model (GenTableSchema): 修改业务表信息模型。
|
||||
|
||||
返回:
|
||||
- GenTableSchema: 修改后的业务表信息模型。
|
||||
"""
|
||||
# 排除嵌套对象字段,避免SQLAlchemy尝试直接将字典设置到模型实例上
|
||||
return await self.update(id=table_id, data=edit_model.model_dump(exclude_unset=True, exclude={"columns"}))
|
||||
|
||||
async def delete_gen_table(self, ids: list[int]) -> None:
|
||||
"""
|
||||
删除业务表信息。除了系统表。
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 业务表ID列表。
|
||||
"""
|
||||
await self.delete(ids=ids)
|
||||
|
||||
async def get_db_table_list(self, search: GenTableQueryParam | None = None) -> list[dict]:
|
||||
"""
|
||||
根据查询参数获取数据库表列表信息。
|
||||
|
||||
参数:
|
||||
- search (GenTableQueryParam | None): 查询参数对象。
|
||||
|
||||
返回:
|
||||
- list[dict]: 数据库表列表信息(已转为可序列化字典)。
|
||||
"""
|
||||
|
||||
# 使用更健壮的方式检测数据库方言
|
||||
if settings.DATABASE_TYPE == "postgres":
|
||||
query_sql = (
|
||||
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 = []
|
||||
for row in all_data:
|
||||
# 检查row是否为Row对象
|
||||
if isinstance(row, Row):
|
||||
# 使用._mapping获取字典
|
||||
dict_row = GenDBTableSchema(**dict(row._mapping)).model_dump()
|
||||
dict_data.append(dict_row)
|
||||
else:
|
||||
dict_row = GenDBTableSchema(**dict(row)).model_dump()
|
||||
dict_data.append(dict_row)
|
||||
return dict_data
|
||||
|
||||
async def get_db_table_list_by_names(self, table_names: list[str]) -> list[GenDBTableSchema]:
|
||||
"""
|
||||
根据业务表名称列表获取数据库表信息。
|
||||
|
||||
参数:
|
||||
- table_names (list[str]): 业务表名称列表。
|
||||
|
||||
返回:
|
||||
- list[GenDBTableSchema]: 数据库表信息对象列表。
|
||||
"""
|
||||
# 处理空列表情况
|
||||
if not table_names:
|
||||
return []
|
||||
|
||||
# 使用更健壮的方式检测数据库方言
|
||||
if settings.DATABASE_TYPE == "postgres":
|
||||
# PostgreSQL使用ANY操作符和正确的参数绑定
|
||||
query_sql = """
|
||||
SELECT
|
||||
t.table_catalog as database_name,
|
||||
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
|
||||
"""
|
||||
|
||||
# 创建新的数据库会话上下文来执行查询,避免受外部事务状态影响
|
||||
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:
|
||||
"""
|
||||
检查数据库中是否已存在指定表名的表。
|
||||
|
||||
参数:
|
||||
- table_name (str): 要检查的表名。
|
||||
|
||||
返回:
|
||||
- bool: 如果表存在返回True,否则返回False。
|
||||
"""
|
||||
try:
|
||||
# 根据不同数据库类型使用不同的查询方式
|
||||
if settings.DATABASE_TYPE.lower() == 'mysql':
|
||||
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:
|
||||
"""
|
||||
执行SQL语句。
|
||||
|
||||
参数:
|
||||
- sql (str): 要执行的SQL语句。
|
||||
|
||||
返回:
|
||||
- bool: 是否执行成功。
|
||||
"""
|
||||
try:
|
||||
# 执行SQL但不手动提交事务,由框架管理事务生命周期
|
||||
await self.auth.db.execute(text(sql))
|
||||
return True
|
||||
except Exception as e:
|
||||
log.error(f"执行SQL时发生错误: {e}")
|
||||
return False
|
||||
|
||||
|
||||
class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, GenTableColumnSchema]):
|
||||
"""代码生成业务表字段模块数据库操作层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化CRUD操作层
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
super().__init__(model=GenTableColumnModel, auth=auth)
|
||||
|
||||
async def get_gen_table_column_by_id(self, id: int, preload: list | None = None) -> GenTableColumnModel | None:
|
||||
"""根据业务表字段ID获取业务表字段信息。
|
||||
|
||||
参数:
|
||||
- id (int): 业务表字段ID。
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- GenTableColumnModel | None: 业务表字段信息对象。
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def get_gen_table_column_list_by_table_id(self, table_id: int, preload: list | None = None) -> GenTableColumnModel | None:
|
||||
"""根据业务表ID获取业务表字段列表信息。
|
||||
|
||||
参数:
|
||||
- table_id (int): 业务表ID。
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- GenTableColumnModel | None: 业务表字段列表信息对象。
|
||||
"""
|
||||
return await self.get(table_id=table_id, preload=preload)
|
||||
|
||||
async def list_gen_table_column_crud_by_table_id(self, table_id: int, order_by: list | None = None, preload: list | None = None) -> Sequence[GenTableColumnModel]:
|
||||
"""根据业务表ID查询业务表字段列表。
|
||||
|
||||
参数:
|
||||
- table_id (int): 业务表ID。
|
||||
- order_by (list | None): 排序字段列表,每个元素为{"field": "字段名", "order": "asc" | "desc"}。
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[GenTableColumnModel]: 业务表字段列表信息对象序列。
|
||||
"""
|
||||
return await self.list(search={"table_id": table_id}, order_by=order_by, preload=preload)
|
||||
|
||||
async def get_gen_db_table_columns_by_name(self, table_name: str | None) -> list[GenTableColumnOutSchema]:
|
||||
"""
|
||||
根据业务表名称获取业务表字段列表信息。
|
||||
|
||||
参数:
|
||||
- table_name (str | None): 业务表名称。
|
||||
|
||||
返回:
|
||||
- list[GenTableColumnOutSchema]: 业务表字段列表信息对象。
|
||||
"""
|
||||
# 检查表名是否为空
|
||||
if not table_name:
|
||||
raise ValueError("数据表名称不能为空")
|
||||
|
||||
try:
|
||||
if settings.DATABASE_TYPE == "mysql":
|
||||
query_sql = """
|
||||
SELECT
|
||||
c.column_name AS column_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 []
|
||||
|
||||
columns_list = []
|
||||
for row in rows:
|
||||
# 防御性编程:检查row是否有足够的元素
|
||||
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
|
||||
except Exception as e:
|
||||
log.error(f"获取表{table_name}的字段列表时出错: {str(e)}")
|
||||
# 确保即使出错也返回空列表而不是None
|
||||
raise
|
||||
|
||||
async def list_gen_table_column_crud(self, search: dict | None = None, order_by: list | None = None, preload: list | None = None) -> Sequence[GenTableColumnModel]:
|
||||
"""根据业务表字段查询业务表字段列表。
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数,例如{"table_id": 1}。
|
||||
- order_by (list | None): 排序字段列表,每个元素为{"field": "字段名", "order": "asc" | "desc"}。
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[GenTableColumnModel]: 业务表字段列表信息对象序列。
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_gen_table_column_crud(self, data: GenTableColumnSchema) -> GenTableColumnModel | None:
|
||||
"""创建业务表字段。
|
||||
|
||||
参数:
|
||||
- data (GenTableColumnSchema): 业务表字段模型。
|
||||
|
||||
返回:
|
||||
- GenTableColumnModel | None: 业务表字段列表信息对象。
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_gen_table_column_crud(self, id: int, data: GenTableColumnSchema) -> GenTableColumnModel | None:
|
||||
"""更新业务表字段。
|
||||
|
||||
参数:
|
||||
- id (int): 业务表字段ID。
|
||||
- data (GenTableColumnSchema): 业务表字段模型。
|
||||
|
||||
返回:
|
||||
- GenTableColumnModel | None: 业务表字段列表信息对象。
|
||||
"""
|
||||
# 将对象转换为字典,避免SQLAlchemy直接操作对象时出现的状态问题
|
||||
data_dict = data.model_dump(exclude_unset=True)
|
||||
return await self.update(id=id, data=data_dict)
|
||||
|
||||
async def delete_gen_table_column_by_table_id_crud(self, table_ids: list[int]) -> None:
|
||||
"""根据业务表ID批量删除业务表字段。
|
||||
|
||||
参数:
|
||||
- table_ids (list[int]): 业务表ID列表。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
# 先查询出这些表ID对应的所有字段ID
|
||||
query = select(GenTableColumnModel.id).where(GenTableColumnModel.table_id.in_(table_ids))
|
||||
result = await self.auth.db.execute(query)
|
||||
column_ids = [row[0] for row in result.fetchall()]
|
||||
|
||||
# 如果有字段ID,则删除这些字段
|
||||
if column_ids:
|
||||
await self.delete(ids=column_ids)
|
||||
|
||||
async def delete_gen_table_column_by_column_id_crud(self, column_ids: list[int]) -> None:
|
||||
"""根据业务表字段ID批量删除业务表字段。
|
||||
|
||||
参数:
|
||||
- column_ids (list[int]): 业务表字段ID列表。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.delete(ids=column_ids)
|
||||
@@ -0,0 +1,133 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sqlalchemy import String, Integer, ForeignKey, Boolean
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship, validates
|
||||
from sqlalchemy.sql import expression
|
||||
|
||||
from app.config.setting import settings
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
from app.utils.common_util import SqlalchemyUtil
|
||||
|
||||
|
||||
class GenTableModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
代码生成表
|
||||
"""
|
||||
__tablename__: str = 'gen_table'
|
||||
__table_args__: dict[str, str] = ({'comment': '代码生成表'})
|
||||
__loader_options__: list[str] = ["columns", "created_by", "updated_by"]
|
||||
|
||||
table_name: Mapped[str] = mapped_column(String(200), nullable=False, default='', comment='表名称')
|
||||
table_comment: Mapped[str | None] = mapped_column(String(500), nullable=True, comment='表描述')
|
||||
|
||||
class_name: Mapped[str] = mapped_column(String(100), nullable=False, default='', comment='实体类名称')
|
||||
package_name: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='生成包路径')
|
||||
module_name: Mapped[str | None] = mapped_column(String(30), nullable=True, comment='生成模块名')
|
||||
business_name: Mapped[str | None] = mapped_column(String(30), nullable=True, comment='生成业务名')
|
||||
function_name: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='生成功能名')
|
||||
|
||||
sub_table_name: Mapped[str | None] = mapped_column(
|
||||
String(64),
|
||||
nullable=True,
|
||||
server_default=SqlalchemyUtil.get_server_default_null(settings.DATABASE_TYPE),
|
||||
comment='关联子表的表名'
|
||||
)
|
||||
sub_table_fk_name: Mapped[str | None] = mapped_column(
|
||||
String(64),
|
||||
nullable=True,
|
||||
server_default=SqlalchemyUtil.get_server_default_null(settings.DATABASE_TYPE),
|
||||
comment='子表关联的外键名'
|
||||
)
|
||||
|
||||
parent_menu_id: Mapped[int | None] = mapped_column(Integer, nullable=True, comment='父菜单ID')
|
||||
|
||||
# 关联关系
|
||||
columns: Mapped[list['GenTableColumnModel']] = relationship(
|
||||
order_by='GenTableColumnModel.sort',
|
||||
back_populates='table',
|
||||
cascade='all, delete-orphan'
|
||||
)
|
||||
|
||||
@validates('table_name')
|
||||
def validate_table_name(self, key: str, table_name: str) -> str:
|
||||
"""验证表名不为空"""
|
||||
if not table_name or not table_name.strip():
|
||||
raise ValueError('表名称不能为空')
|
||||
return table_name.strip()
|
||||
|
||||
@validates('class_name')
|
||||
def validate_class_name(self, key: str, class_name: str) -> str:
|
||||
"""验证类名不为空"""
|
||||
if not class_name or not class_name.strip():
|
||||
raise ValueError('实体类名称不能为空')
|
||||
return class_name.strip()
|
||||
|
||||
|
||||
class GenTableColumnModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
代码生成表字段
|
||||
|
||||
数据隔离策略:
|
||||
- 继承自GenTableModel的隔离级别
|
||||
- 不需要customer_id
|
||||
|
||||
用于存储代码生成器的字段配置
|
||||
"""
|
||||
__tablename__: str = 'gen_table_column'
|
||||
__table_args__: dict[str, str] = ({'comment': '代码生成表字段'})
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
# 数据库设计表字段
|
||||
column_name: Mapped[str] = mapped_column(String(200), nullable=False, comment='列名称')
|
||||
column_comment: Mapped[str | None] = mapped_column(String(500), nullable=True, comment='列描述')
|
||||
column_type: Mapped[str] = mapped_column(String(100), nullable=False, comment='列类型')
|
||||
column_length: Mapped[str | None] = mapped_column(String(50), nullable=True, comment='列长度')
|
||||
column_default: Mapped[str | None] = mapped_column(String(200), nullable=True, comment='列默认值')
|
||||
is_pk: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=expression.false(), comment='是否主键')
|
||||
is_increment: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=expression.false(), comment='是否自增')
|
||||
is_nullable: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default=expression.true(), comment='是否允许为空')
|
||||
is_unique: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=expression.false(), comment='是否唯一')
|
||||
|
||||
# Python字段映射
|
||||
python_type: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='Python类型')
|
||||
python_field: Mapped[str | None] = mapped_column(String(200), nullable=True, comment='Python字段名')
|
||||
|
||||
# 序列化配置
|
||||
is_insert: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default=expression.true(), comment='是否为新增字段')
|
||||
is_edit: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default=expression.true(), comment='是否编辑字段')
|
||||
is_list: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default=expression.true(), comment='是否列表字段')
|
||||
is_query: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default=expression.false(), comment='是否查询字段')
|
||||
query_type: Mapped[str | None] = mapped_column(String(50), nullable=True, default=None, comment='查询方式')
|
||||
|
||||
# 前端展示配置
|
||||
html_type: Mapped[str | None] = mapped_column(String(100), nullable=True, default='input', comment='显示类型')
|
||||
dict_type: Mapped[str | None] = mapped_column(String(200), nullable=True, default='', comment='字典类型')
|
||||
|
||||
# 排序和扩展配置
|
||||
sort: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment='排序')
|
||||
|
||||
# 归属关系
|
||||
table_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey('gen_table.id', ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment='归属表编号'
|
||||
)
|
||||
|
||||
# 关联关系
|
||||
table: Mapped['GenTableModel'] = relationship(back_populates='columns')
|
||||
|
||||
@validates('column_name')
|
||||
def validate_column_name(self, key: str, column_name: str) -> str:
|
||||
"""验证列名不为空"""
|
||||
if not column_name or not column_name.strip():
|
||||
raise ValueError('列名称不能为空')
|
||||
return column_name.strip()
|
||||
|
||||
@validates('column_type')
|
||||
def validate_column_type(self, key: str, column_type: str) -> str:
|
||||
"""验证列类型不为空"""
|
||||
if not column_type or not column_type.strip():
|
||||
raise ValueError('列类型不能为空')
|
||||
return column_type.strip()
|
||||
@@ -0,0 +1,127 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.base_schema import BaseSchema
|
||||
|
||||
|
||||
class GenDBTableSchema(BaseModel):
|
||||
"""数据库中的表信息(跨方言统一结构)。
|
||||
- 供“导入表结构”与“同步结构”环节使用。
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
database_name: str | None = Field(default=None, description='数据库名称')
|
||||
table_name: str | None = Field(default=None, description='表名称')
|
||||
table_type: str | None = Field(default=None, description='表类型')
|
||||
table_comment: str | None = Field(default=None, description='表描述')
|
||||
|
||||
|
||||
class GenTableColumnSchema(BaseModel):
|
||||
"""代码生成业务表字段创建模型(原始字段+生成配置)。
|
||||
- 从根本上解决问题:所有字段都设置了合理的默认值,避免None值问题
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
table_id: int = Field(default=0, description='归属表编号')
|
||||
column_name: str = Field(default='', description='列名称')
|
||||
column_comment: str | None = Field(default='', description='列描述')
|
||||
column_type: str = Field(default='varchar(255)', description='列类型')
|
||||
column_length: str | None = Field(default='', description='列长度')
|
||||
column_default: str | None = Field(default='', description='列默认值')
|
||||
is_pk: bool = Field(default=False, description='是否主键(True是 False否)')
|
||||
is_increment: bool = Field(default=False, description='是否自增(True是 False否)')
|
||||
is_nullable: bool = Field(default=True, description='是否允许为空(True是 False否)')
|
||||
is_unique: bool = Field(default=False, description='是否唯一(True是 False否)')
|
||||
python_type: str | None = Field(default='str', description='python类型')
|
||||
python_field: str | None = Field(default='', description='python字段名')
|
||||
is_insert: bool = Field(default=True, description='是否为插入字段(True是 False否)')
|
||||
is_edit: bool = Field(default=True, description='是否编辑字段(True是 False否)')
|
||||
is_list: bool = Field(default=True, description='是否列表字段(True是 False否)')
|
||||
is_query: bool = Field(default=True, description='是否查询字段(True是 False否)')
|
||||
query_type: str | None = Field(default=None, description='查询方式(等于、不等于、大于、小于、范围)')
|
||||
html_type: str | None = Field(default='input', description='显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)')
|
||||
dict_type: str | None = Field(default='', description='字典类型')
|
||||
sort: int = Field(default=0, description='排序')
|
||||
|
||||
|
||||
class GenTableColumnOutSchema(GenTableColumnSchema, BaseSchema):
|
||||
"""
|
||||
业务表字段输出模型
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
super_column: str | None = Field(default='0', description='是否为基类字段(1是 0否)')
|
||||
|
||||
|
||||
class GenTableSchema(BaseModel):
|
||||
"""代码生成业务表更新模型(扩展聚合字段)。
|
||||
- 聚合:`columns`字段包含字段列表;`pk_column`主键字段;子表结构`sub_table`。
|
||||
"""
|
||||
"""代码生成业务表基础模型(创建/更新共享字段)。
|
||||
- 说明:`params`为前端结构体,后端持久化为`options`的JSON。
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
table_name: str= Field(..., description='表名称')
|
||||
table_comment: str | None = Field(default=None, description='表描述')
|
||||
class_name: str | None = Field(default=None, description='实体类名称')
|
||||
package_name: str | None = Field(default=None, description='生成包路径')
|
||||
module_name: str | None = Field(default=None, description='生成模块名')
|
||||
business_name: str | None = Field(default=None, description='生成业务名')
|
||||
function_name: str | None = Field(default=None, description='生成功能名')
|
||||
sub_table_name: str | None = Field(default=None, description='关联子表的表名')
|
||||
sub_table_fk_name: str | None = Field(default=None, description='子表关联的外键名')
|
||||
parent_menu_id: int | None = Field(default=None, description='所属父级分类,生成页面时候生成菜单使用')
|
||||
description: str | None = Field(default=None, max_length=255, description="描述")
|
||||
|
||||
columns: list['GenTableColumnOutSchema'] | None = Field(default=None, description='表列信息')
|
||||
|
||||
@field_validator('table_name')
|
||||
@classmethod
|
||||
def table_name_update(cls, v: str) -> str:
|
||||
"""更新表名称"""
|
||||
if not v:
|
||||
raise ValueError('表名称不能为空')
|
||||
return v
|
||||
|
||||
|
||||
class GenTableOutSchema(GenTableSchema, BaseSchema):
|
||||
"""业务表输出模型(面向控制器/前端)。
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
pk_column: GenTableColumnOutSchema | None = Field(default=None, description='主键信息')
|
||||
sub_table: GenTableSchema | None = Field(default=None, description='子表信息')
|
||||
sub: bool | None = Field(default=None, description='是否为子表')
|
||||
|
||||
|
||||
class GenTableQueryParam:
|
||||
"""代码生成业务表查询参数
|
||||
- 支持按`table_name`、`table_comment`进行模糊检索(由CRUD层实现like)。
|
||||
- 空值将被忽略,不参与过滤。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
table_name: str | None = Query(None, description="表名称"),
|
||||
table_comment: str | None = Query(None, description="表注释"),
|
||||
) -> None:
|
||||
# 模糊查询字段
|
||||
self.table_name = table_name
|
||||
self.table_comment = table_comment
|
||||
|
||||
|
||||
class GenTableColumnQueryParam:
|
||||
"""代码生成业务表字段查询参数
|
||||
- `column_name`按like规则模糊查询(透传到CRUD层)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
column_name: str | None = Query(None, description="列名称"),
|
||||
) -> None:
|
||||
# 模糊查询字段:约定("like", 值)格式,便于CRUD解析
|
||||
self.column_name = ("like", column_name)
|
||||
@@ -0,0 +1,573 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
from typing import Any
|
||||
from sqlglot.expressions import Add, Alter, Create, Delete, Drop, Expression, Insert, Table, TruncateTable, Update
|
||||
from sqlglot import parse as sqlglot_parse
|
||||
|
||||
from app.config.path_conf import BASE_DIR
|
||||
from app.config.setting import settings
|
||||
from app.core.logger import log
|
||||
from app.core.exceptions import CustomException
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .tools.jinja2_template_util import Jinja2TemplateUtil
|
||||
from .tools.gen_util import GenUtils
|
||||
from .schema import GenTableSchema, GenTableOutSchema, GenTableColumnSchema, GenTableColumnOutSchema, GenTableQueryParam
|
||||
from .crud import GenTableColumnCRUD, GenTableCRUD
|
||||
|
||||
|
||||
def handle_service_exception(func):
|
||||
async def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except CustomException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise CustomException(msg=f'{func.__name__}执行失败: {str(e)}')
|
||||
return wrapper
|
||||
|
||||
|
||||
class GenTableService:
|
||||
"""代码生成业务表服务层"""
|
||||
|
||||
@classmethod
|
||||
@handle_service_exception
|
||||
async def get_gen_table_detail_service(cls, auth: AuthSchema, table_id: int) -> dict:
|
||||
"""获取业务表详细信息(含字段与其他表列表)。
|
||||
- 备注:优先解析`options`为`GenTableOptionSchema`,设置`parent_menu_id`等选项;保证`columns`与`tables`结构完整。
|
||||
"""
|
||||
gen_table = await cls.get_gen_table_by_id_service(auth, table_id)
|
||||
return GenTableOutSchema.model_validate(gen_table).model_dump()
|
||||
|
||||
@classmethod
|
||||
@handle_service_exception
|
||||
async def get_gen_table_list_service(cls, auth: AuthSchema, search: GenTableQueryParam) -> list[dict]:
|
||||
"""
|
||||
获取代码生成业务表列表信息。
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息。
|
||||
- search (GenTableQueryParam): 查询参数模型。
|
||||
|
||||
返回:
|
||||
- list[dict]: 包含业务表列表信息的字典列表。
|
||||
"""
|
||||
gen_table_list_result = await GenTableCRUD(auth=auth).get_gen_table_list(search)
|
||||
return [GenTableOutSchema.model_validate(obj).model_dump() for obj in gen_table_list_result]
|
||||
|
||||
@classmethod
|
||||
@handle_service_exception
|
||||
async def get_gen_db_table_list_service(cls, auth: AuthSchema, search: GenTableQueryParam) -> list[Any]:
|
||||
"""获取数据库表列表(跨方言)。
|
||||
- 备注:返回已转换为字典的结构,适用于前端直接展示;排序参数保留扩展位但当前未使用。
|
||||
"""
|
||||
gen_db_table_list_result = await GenTableCRUD(auth=auth).get_db_table_list(search)
|
||||
return gen_db_table_list_result
|
||||
|
||||
@classmethod
|
||||
@handle_service_exception
|
||||
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="表名列表不能为空")
|
||||
|
||||
gen_db_table_list_result = await GenTableCRUD(auth).get_db_table_list_by_names(table_names)
|
||||
|
||||
# 修复:将GenDBTableSchema对象转换为字典后再传递给GenTableOutSchema
|
||||
result = []
|
||||
for gen_table in gen_db_table_list_result:
|
||||
# 确保table_name不为None
|
||||
if gen_table.table_name is not None:
|
||||
result.append(GenTableOutSchema(**gen_table.model_dump()))
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
@handle_service_exception
|
||||
async def import_gen_table_service(cls, auth: AuthSchema, gen_table_list: list[GenTableOutSchema]) -> bool | None:
|
||||
"""导入表结构到生成器(持久化并初始化列)。
|
||||
- 备注:避免重复导入;为每列调用`GenUtils.init_column_field`填充默认属性,保留语义一致性。
|
||||
"""
|
||||
# 检查是否有表需要导入
|
||||
if not gen_table_list:
|
||||
raise CustomException(msg="导入的表结构不能为空")
|
||||
try:
|
||||
for table in gen_table_list:
|
||||
table_name = table.table_name
|
||||
# 检查表是否已存在
|
||||
existing_table = await GenTableCRUD(auth).get_gen_table_by_name(table_name)
|
||||
if existing_table:
|
||||
raise CustomException(msg=f"以下表已存在,不能重复导入: {table_name}")
|
||||
GenUtils.init_table(table)
|
||||
if not table.columns:
|
||||
table.columns = []
|
||||
add_gen_table = await GenTableCRUD(auth).add_gen_table(GenTableSchema.model_validate(table.model_dump()))
|
||||
gen_table_columns = await GenTableColumnCRUD(auth).get_gen_db_table_columns_by_name(table_name)
|
||||
if len(gen_table_columns) > 0:
|
||||
table.id = add_gen_table.id
|
||||
for column in gen_table_columns:
|
||||
column_schema = GenTableColumnSchema(
|
||||
table_id=table.id,
|
||||
column_name=column.column_name,
|
||||
column_comment=column.column_comment,
|
||||
column_type=column.column_type,
|
||||
column_length=column.column_length,
|
||||
column_default=column.column_default,
|
||||
is_pk=column.is_pk,
|
||||
is_increment=column.is_increment,
|
||||
is_nullable=column.is_nullable,
|
||||
is_unique=column.is_unique,
|
||||
sort=column.sort,
|
||||
python_type=column.python_type,
|
||||
python_field=column.python_field,
|
||||
)
|
||||
GenUtils.init_column_field(column_schema, table)
|
||||
await GenTableColumnCRUD(auth).create_gen_table_column_crud(column_schema)
|
||||
return True
|
||||
except Exception as e:
|
||||
raise CustomException(msg=f'导入失败, {str(e)}')
|
||||
|
||||
@classmethod
|
||||
@handle_service_exception
|
||||
async def create_table_service(cls, auth: AuthSchema, sql: str) -> bool | None:
|
||||
"""创建表结构并导入至代码生成模块。
|
||||
- 校验:使用`sqlglot`确保仅包含`CREATE TABLE`语句;失败抛出明确异常。
|
||||
- 唯一性检查:在创建前检查该表是否已存在于数据库中。
|
||||
"""
|
||||
# 验证SQL非空
|
||||
if not sql or not sql.strip():
|
||||
raise CustomException(msg='SQL语句不能为空')
|
||||
|
||||
try:
|
||||
# 解析SQL语句
|
||||
sql_statements = sqlglot_parse(sql, dialect=settings.DATABASE_TYPE)
|
||||
if not sql_statements:
|
||||
raise CustomException(msg='无法解析SQL语句,请检查SQL语法')
|
||||
|
||||
# 校验sql语句是否为合法的建表语句
|
||||
if not cls.__is_valid_create_table(sql_statements):
|
||||
raise CustomException(msg='sql语句不是合法的建表语句')
|
||||
|
||||
# 获取要创建的表名
|
||||
table_names = cls.__get_table_names(sql_statements)
|
||||
# 创建CRUD实例
|
||||
gen_table_crud = GenTableCRUD(auth=auth)
|
||||
|
||||
# 检查每个表是否已存在
|
||||
for table_name in table_names:
|
||||
# 检查数据库中是否已存在该表
|
||||
if await gen_table_crud.check_table_exists(table_name):
|
||||
raise CustomException(msg=f'表 {table_name} 已存在,请检查并修改表名后重试')
|
||||
|
||||
# 检查代码生成模块中是否已导入该表
|
||||
existing_table = await gen_table_crud.get_gen_table_by_name(table_name)
|
||||
if existing_table:
|
||||
raise CustomException(msg=f'表 {table_name} 已在代码生成模块中存在,请检查并修改表名后重试')
|
||||
|
||||
# 表不存在,执行SQL语句创建表
|
||||
result = await gen_table_crud.create_table_by_sql(sql_statements)
|
||||
if not result:
|
||||
raise CustomException(msg=f'创建表 {table_names} 失败,请检查SQL语句')
|
||||
|
||||
# 导入表结构到代码生成模块 - 简化逻辑,移除多余的None检查
|
||||
gen_table_list = await cls.get_gen_db_table_list_by_name_service(auth, table_names)
|
||||
|
||||
import_result = await cls.import_gen_table_service(auth, gen_table_list)
|
||||
|
||||
return import_result
|
||||
|
||||
except Exception as 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
|
||||
@handle_service_exception
|
||||
async def update_gen_table_service(cls, auth: AuthSchema, data: GenTableSchema, table_id: int) -> dict[str, Any]:
|
||||
"""编辑业务表信息(含选项与字段)。
|
||||
- 备注:将`params`序列化写入`options`以持久化;仅更新存在`id`的列,避免误创建。
|
||||
"""
|
||||
# 处理params为None的情况
|
||||
gen_table_info = await cls.get_gen_table_by_id_service(auth, table_id)
|
||||
if gen_table_info.id:
|
||||
try:
|
||||
# 直接调用edit_gen_table方法,它会在内部处理排除嵌套字段的逻辑
|
||||
result = await GenTableCRUD(auth).edit_gen_table(table_id, data)
|
||||
|
||||
# 处理data.columns为None的情况
|
||||
if data.columns:
|
||||
for gen_table_column in data.columns:
|
||||
# 确保column有id字段
|
||||
if hasattr(gen_table_column, 'id') and gen_table_column.id:
|
||||
column_schema = GenTableColumnSchema(**gen_table_column.model_dump())
|
||||
await GenTableColumnCRUD(auth).update_gen_table_column_crud(gen_table_column.id, column_schema)
|
||||
return GenTableOutSchema.model_validate(result).model_dump()
|
||||
except Exception as e:
|
||||
raise CustomException(msg=str(e))
|
||||
else:
|
||||
raise CustomException(msg='业务表不存在')
|
||||
|
||||
@classmethod
|
||||
@handle_service_exception
|
||||
async def delete_gen_table_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""删除业务表信息(先删字段,再删表)。"""
|
||||
# 验证ID列表非空
|
||||
if not ids:
|
||||
raise CustomException(msg="ID列表不能为空")
|
||||
|
||||
try:
|
||||
# 先删除相关的字段信息
|
||||
await GenTableColumnCRUD(auth=auth).delete_gen_table_column_by_table_id_crud(ids)
|
||||
# 再删除表信息
|
||||
await GenTableCRUD(auth=auth).delete_gen_table(ids)
|
||||
except Exception as e:
|
||||
raise CustomException(msg=str(e))
|
||||
|
||||
@classmethod
|
||||
@handle_service_exception
|
||||
async def get_gen_table_by_id_service(cls, auth: AuthSchema, table_id: int) -> GenTableOutSchema:
|
||||
"""获取需要生成代码的业务表详细信息。
|
||||
- 备注:去除SQLAlchemy内部状态;将`None`值转为适配前端的默认值;解析`options`补充选项。
|
||||
"""
|
||||
gen_table = await GenTableCRUD(auth=auth).get_gen_table_by_id(table_id)
|
||||
if not gen_table:
|
||||
raise CustomException(msg='业务表不存在')
|
||||
|
||||
result = GenTableOutSchema.model_validate(gen_table)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
@handle_service_exception
|
||||
async def get_gen_table_all_service(cls, auth: AuthSchema) -> list[GenTableOutSchema]:
|
||||
"""获取所有业务表信息(列表)。"""
|
||||
gen_table_all = await GenTableCRUD(auth=auth).get_gen_table_all() or []
|
||||
result = []
|
||||
for gen_table in gen_table_all:
|
||||
try:
|
||||
table_out = GenTableOutSchema.model_validate(gen_table)
|
||||
result.append(table_out)
|
||||
except Exception as e:
|
||||
log.error(f"转换业务表时出错: {str(e)}")
|
||||
continue
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
@handle_service_exception
|
||||
async def preview_code_service(cls, auth: AuthSchema, table_id: int) -> dict[str, Any]:
|
||||
"""
|
||||
预览代码(根据模板渲染内存结果)。
|
||||
- 备注:构建Jinja2上下文;根据模板类型与前端类型选择模板清单;返回文件名到内容映射。
|
||||
"""
|
||||
gen_table = GenTableOutSchema.model_validate(
|
||||
await GenTableCRUD(auth).get_gen_table_by_id(table_id)
|
||||
)
|
||||
await cls.set_pk_column(gen_table)
|
||||
env = Jinja2TemplateUtil.get_env()
|
||||
context = Jinja2TemplateUtil.prepare_context(gen_table)
|
||||
template_list = Jinja2TemplateUtil.get_template_list()
|
||||
preview_code_result = {}
|
||||
for template in template_list:
|
||||
try:
|
||||
render_content = await env.get_template(template).render_async(**context)
|
||||
preview_code_result[template] = render_content
|
||||
except Exception as e:
|
||||
log.error(f"渲染模板 {template} 时出错: {str(e)}")
|
||||
# 即使某个模板渲染失败,也继续处理其他模板
|
||||
preview_code_result[template] = f"渲染错误: {str(e)}"
|
||||
return preview_code_result
|
||||
|
||||
@classmethod
|
||||
@handle_service_exception
|
||||
async def generate_code_service(cls, auth: AuthSchema, table_name: str) -> bool:
|
||||
"""生成代码至指定路径(安全写入+可跳过覆盖)。
|
||||
- 安全:限制写入在项目根目录内;越界路径自动回退到项目根目录。
|
||||
"""
|
||||
# 验证表名非空
|
||||
if not table_name or not table_name.strip():
|
||||
raise CustomException(msg='表名不能为空')
|
||||
|
||||
env = Jinja2TemplateUtil.get_env()
|
||||
render_info = await cls.__get_gen_render_info(auth, table_name)
|
||||
gen_table_schema = render_info[3]
|
||||
for template in render_info[0]:
|
||||
try:
|
||||
render_content = await env.get_template(template).render_async(**render_info[2])
|
||||
gen_path = cls.__get_gen_path(gen_table_schema, template)
|
||||
if not gen_path:
|
||||
raise CustomException(msg='【代码生成】生成路径为空')
|
||||
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(gen_path), exist_ok=True)
|
||||
|
||||
with open(gen_path, 'w', encoding='utf-8') as f:
|
||||
f.write(render_content)
|
||||
except Exception as e:
|
||||
raise CustomException(msg=f'渲染模板失败,表名:{gen_table_schema.table_name},详细错误信息:{str(e)}')
|
||||
|
||||
await cls.execute_sql_service(auth, gen_table_schema)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@handle_service_exception
|
||||
async def batch_gen_code_service(cls, auth: AuthSchema, table_names: list[str]) -> bytes:
|
||||
"""
|
||||
批量生成代码并打包为ZIP。
|
||||
- 备注:内存生成并压缩,兼容多模板类型;供下载使用。
|
||||
"""
|
||||
# 验证表名列表非空
|
||||
if not table_names:
|
||||
raise CustomException(msg="表名列表不能为空")
|
||||
|
||||
zip_buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
||||
for table_name in table_names:
|
||||
if not table_name.strip():
|
||||
continue
|
||||
|
||||
try:
|
||||
env = Jinja2TemplateUtil.get_env()
|
||||
render_info = await cls.__get_gen_render_info(auth, table_name)
|
||||
for template_file, output_file in zip(render_info[0], render_info[1]):
|
||||
render_content = await env.get_template(template_file).render_async(**render_info[2])
|
||||
zip_file.writestr(output_file, render_content)
|
||||
except Exception as e:
|
||||
log.error(f"批量生成代码时处理表 {table_name} 出错: {str(e)}")
|
||||
# 继续处理其他表,不中断整个过程
|
||||
continue
|
||||
|
||||
zip_data = zip_buffer.getvalue()
|
||||
zip_buffer.close()
|
||||
return zip_data
|
||||
|
||||
@classmethod
|
||||
@handle_service_exception
|
||||
async def sync_db_service(cls, auth: AuthSchema, table_name: str) -> None:
|
||||
"""同步数据库表结构至生成器(保留用户配置)。
|
||||
- 备注:按数据库实际字段重建或更新生成器字段;保留字典/查询/展示等用户自定义属性;清理已删除字段。
|
||||
"""
|
||||
# 验证表名非空
|
||||
if not table_name or not table_name.strip():
|
||||
raise CustomException(msg='表名不能为空')
|
||||
|
||||
gen_table = await GenTableCRUD(auth).get_gen_table_by_name(table_name)
|
||||
if not gen_table:
|
||||
raise CustomException(msg='业务表不存在')
|
||||
table = GenTableOutSchema.model_validate(gen_table)
|
||||
if not table.id:
|
||||
raise CustomException(msg='业务表ID不能为空')
|
||||
table_columns = table.columns or []
|
||||
table_column_map = {column.column_name: column for column in table_columns}
|
||||
# 确保db_table_columns始终是列表类型,避免None值
|
||||
db_table_columns = await GenTableColumnCRUD(auth).get_gen_db_table_columns_by_name(table_name) or []
|
||||
db_table_columns = [col for col in db_table_columns if col is not None]
|
||||
db_table_column_names = [column.column_name for column in db_table_columns]
|
||||
try:
|
||||
for column in db_table_columns:
|
||||
# 仅在缺省时初始化默认属性(包含 table_id 关联)
|
||||
GenUtils.init_column_field(column, table)
|
||||
# 利用schema层的默认值,移除多余的None检查
|
||||
if column.column_name in table_column_map:
|
||||
prev_column = table_column_map[column.column_name]
|
||||
# 复用旧记录ID,确保执行更新
|
||||
if hasattr(prev_column, 'id') and prev_column.id:
|
||||
column.id = prev_column.id
|
||||
|
||||
# 保留用户配置的显示与查询属性 - 使用getattr确保安全访问
|
||||
if hasattr(prev_column, 'dict_type') and prev_column.dict_type:
|
||||
column.dict_type = prev_column.dict_type
|
||||
if hasattr(prev_column, 'query_type') and prev_column.query_type:
|
||||
column.query_type = prev_column.query_type
|
||||
if hasattr(prev_column, 'html_type') and prev_column.html_type:
|
||||
column.html_type = prev_column.html_type
|
||||
|
||||
# 保留关键用户自定义属性 - 安全处理is_pk
|
||||
is_pk_bool = False
|
||||
if hasattr(prev_column, 'is_pk'):
|
||||
# 处理不同类型的is_pk值
|
||||
if isinstance(prev_column.is_pk, bool):
|
||||
is_pk_bool = prev_column.is_pk
|
||||
else:
|
||||
is_pk_bool = str(prev_column.is_pk) == '1'
|
||||
|
||||
# 安全处理nullable属性
|
||||
if hasattr(prev_column, 'is_nullable') and not is_pk_bool:
|
||||
column.is_nullable = prev_column.is_nullable
|
||||
|
||||
# 保留其他重要用户设置
|
||||
if hasattr(prev_column, 'python_field'):
|
||||
column.python_field = prev_column.python_field or column.python_field
|
||||
|
||||
if hasattr(column, 'id') and column.id:
|
||||
await GenTableColumnCRUD(auth).update_gen_table_column_crud(column.id, column)
|
||||
else:
|
||||
await GenTableColumnCRUD(auth).create_gen_table_column_crud(column)
|
||||
else:
|
||||
# 设置table_id以确保新字段能正确关联到表
|
||||
column.table_id = table.id
|
||||
await GenTableColumnCRUD(auth).create_gen_table_column_crud(column)
|
||||
del_columns = [column for column in table_columns if column.column_name not in db_table_column_names]
|
||||
if del_columns:
|
||||
for column in del_columns:
|
||||
if hasattr(column, 'id') and column.id:
|
||||
await GenTableColumnCRUD(auth).delete_gen_table_column_by_column_id_crud([column.id])
|
||||
except Exception as e:
|
||||
raise CustomException(msg=f'同步失败: {str(e)}')
|
||||
|
||||
@classmethod
|
||||
async def set_pk_column(cls, gen_table: GenTableOutSchema) -> None:
|
||||
"""设置主键列信息(主表/子表)。
|
||||
- 备注:同时兼容`pk`布尔与`is_pk == '1'`字符串两种标识。
|
||||
"""
|
||||
if gen_table.columns:
|
||||
for column in gen_table.columns:
|
||||
# 修复:确保正确检查主键标识
|
||||
if getattr(column, 'pk', False) or getattr(column, 'is_pk', '') == '1':
|
||||
gen_table.pk_column = column
|
||||
break
|
||||
# 如果没有找到主键列且有列存在,使用第一个列作为主键
|
||||
if gen_table.pk_column is None and gen_table.columns:
|
||||
gen_table.pk_column = gen_table.columns[0]
|
||||
|
||||
@classmethod
|
||||
async def __get_gen_render_info(cls, auth: AuthSchema, table_name: str) -> list[Any]:
|
||||
"""
|
||||
获取生成代码渲染模板相关信息。
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证对象。
|
||||
- table_name (str): 业务表名称。
|
||||
|
||||
返回:
|
||||
- list[Any]: [模板列表, 输出文件名列表, 渲染上下文, 业务表对象]。
|
||||
|
||||
异常:
|
||||
- CustomException: 当业务表不存在或数据转换失败时抛出。
|
||||
"""
|
||||
gen_table_model = await GenTableCRUD(auth=auth).get_gen_table_by_name(table_name)
|
||||
# 检查表是否存在
|
||||
if gen_table_model is None:
|
||||
raise CustomException(msg=f"业务表 {table_name} 不存在")
|
||||
|
||||
gen_table = GenTableOutSchema.model_validate(gen_table_model)
|
||||
await cls.set_pk_column(gen_table)
|
||||
context = Jinja2TemplateUtil.prepare_context(gen_table)
|
||||
template_list = Jinja2TemplateUtil.get_template_list()
|
||||
output_files = [Jinja2TemplateUtil.get_file_name(template, gen_table) for template in template_list]
|
||||
|
||||
return [template_list, output_files, context, gen_table]
|
||||
|
||||
@classmethod
|
||||
def __get_gen_path(cls, gen_table: GenTableOutSchema, template: str) -> str | None:
|
||||
"""根据GenTableOutSchema对象和模板名称生成路径。"""
|
||||
try:
|
||||
file_name = Jinja2TemplateUtil.get_file_name(template, gen_table)
|
||||
# 默认写入到项目根目录(backend的上一级)
|
||||
project_root = str(BASE_DIR.parent)
|
||||
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:
|
||||
log.error(f"生成路径时出错: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
class GenTableColumnService:
|
||||
"""代码生成业务表字段服务层"""
|
||||
|
||||
@classmethod
|
||||
@handle_service_exception
|
||||
async def get_gen_table_column_list_by_table_id_service(cls, auth: AuthSchema, table_id: int) -> list[dict[str, Any]]:
|
||||
"""获取业务表字段列表信息(输出模型)。"""
|
||||
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]
|
||||
return result
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,126 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends, UploadFile, Body, Path, Query
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
|
||||
from app.common.response import SuccessResponse, StreamResponse
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.logger import log
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
|
||||
from .service import {{ class_name }}Service
|
||||
from .schema import {{ class_name }}CreateSchema, {{ class_name }}UpdateSchema, {{ class_name }}QueryParam
|
||||
|
||||
{{ class_name }}Router = APIRouter(prefix='/{{ business_name }}', tags=["{{ function_name }}模块"])
|
||||
|
||||
@{{ class_name }}Router.get("/detail/{id}", summary="获取{{ function_name }}详情", description="获取{{ function_name }}详情")
|
||||
async def get_{{ business_name }}_detail_controller(
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:query"]))
|
||||
) -> JSONResponse:
|
||||
"""获取{{ function_name }}详情接口"""
|
||||
result_dict = await {{ class_name }}Service.detail_{{ business_name }}_service(auth=auth, id=id)
|
||||
log.info(f"获取{{ function_name }}详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取{{ function_name }}详情成功")
|
||||
|
||||
@{{ class_name }}Router.get("/list", summary="查询{{ function_name }}列表", description="查询{{ function_name }}列表")
|
||||
async def get_{{ business_name }}_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: {{ class_name }}QueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:query"]))
|
||||
) -> JSONResponse:
|
||||
"""查询{{ function_name }}列表接口(数据库分页)"""
|
||||
result_dict = await {{ class_name }}Service.page_{{ business_name }}_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no if page.page_no is not None else 1,
|
||||
page_size=page.page_size if page.page_size is not None else 10,
|
||||
search=search,
|
||||
order_by=page.order_by
|
||||
)
|
||||
log.info("查询{{ function_name }}列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询{{ function_name }}列表成功")
|
||||
|
||||
@{{ class_name }}Router.post("/create", summary="创建{{ function_name }}", description="创建{{ function_name }}")
|
||||
async def create_{{ business_name }}_controller(
|
||||
data: {{ class_name }}CreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:create"]))
|
||||
) -> JSONResponse:
|
||||
"""创建{{ function_name }}接口"""
|
||||
result_dict = await {{ class_name }}Service.create_{{ business_name }}_service(auth=auth, data=data)
|
||||
log.info("创建{{ function_name }}成功")
|
||||
return SuccessResponse(data=result_dict, msg="创建{{ function_name }}成功")
|
||||
|
||||
@{{ class_name }}Router.put("/update/{id}", summary="修改{{ function_name }}", description="修改{{ function_name }}")
|
||||
async def update_{{ business_name }}_controller(
|
||||
data: {{ class_name }}UpdateSchema,
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:update"]))
|
||||
) -> JSONResponse:
|
||||
"""修改{{ function_name }}接口"""
|
||||
result_dict = await {{ class_name }}Service.update_{{ business_name }}_service(auth=auth, id=id, data=data)
|
||||
log.info("修改{{ function_name }}成功")
|
||||
return SuccessResponse(data=result_dict, msg="修改{{ function_name }}成功")
|
||||
|
||||
@{{ class_name }}Router.delete("/delete", summary="删除{{ function_name }}", description="删除{{ function_name }}")
|
||||
async def delete_{{ business_name }}_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""删除{{ function_name }}接口"""
|
||||
await {{ class_name }}Service.delete_{{ business_name }}_service(auth=auth, ids=ids)
|
||||
log.info(f"删除{{ function_name }}成功: {ids}")
|
||||
return SuccessResponse(msg="删除{{ function_name }}成功")
|
||||
|
||||
@{{ class_name }}Router.patch("/available/setting", summary="批量修改{{ function_name }}状态", description="批量修改{{ function_name }}状态")
|
||||
async def batch_set_available_{{ business_name }}_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:patch"]))
|
||||
) -> JSONResponse:
|
||||
"""批量修改{{ function_name }}状态接口"""
|
||||
await {{ class_name }}Service.set_available_{{ business_name }}_service(auth=auth, data=data)
|
||||
log.info(f"批量修改{{ function_name }}状态成功: {data.ids}")
|
||||
return SuccessResponse(msg="批量修改{{ function_name }}状态成功")
|
||||
|
||||
@{{ class_name }}Router.post('/export', summary="导出{{ function_name }}", description="导出{{ function_name }}")
|
||||
async def export_{{ business_name }}_list_controller(
|
||||
search: {{ class_name }}QueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:export"]))
|
||||
) -> StreamingResponse:
|
||||
"""导出{{ function_name }}接口"""
|
||||
result_dict_list = await {{ class_name }}Service.list_{{ business_name }}_service(search=search, auth=auth)
|
||||
export_result = await {{ class_name }}Service.batch_export_{{ business_name }}_service(obj_list=result_dict_list)
|
||||
log.info('导出{{ function_name }}成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename={{ table_name }}.xlsx'
|
||||
}
|
||||
)
|
||||
|
||||
@{{ class_name }}Router.post('/import', summary="导入{{ function_name }}", description="导入{{ function_name }}")
|
||||
async def import_{{ business_name }}_list_controller(
|
||||
file: UploadFile,
|
||||
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:import"]))
|
||||
) -> JSONResponse:
|
||||
"""导入{{ function_name }}接口"""
|
||||
batch_import_result = await {{ class_name }}Service.batch_import_{{ business_name }}_service(file=file, auth=auth, update_support=True)
|
||||
log.info("导入{{ function_name }}成功")
|
||||
|
||||
return SuccessResponse(data=batch_import_result, msg="导入{{ function_name }}成功")
|
||||
|
||||
@{{ class_name }}Router.post('/download/template', summary="获取{{ function_name }}导入模板", description="获取{{ function_name }}导入模板", dependencies=[Depends(AuthPermission(["{{ permission_prefix }}:download"]))])
|
||||
async def export_{{ business_name }}_template_controller() -> StreamingResponse:
|
||||
"""获取{{ function_name }}导入模板接口"""
|
||||
import_template_result = await {{ class_name }}Service.import_template_download_{{ business_name }}_service()
|
||||
log.info('获取{{ function_name }}导入模板成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(import_template_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={'Content-Disposition': 'attachment; filename={{ table_name }}_template.xlsx'}
|
||||
)
|
||||
@@ -0,0 +1,123 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import {{ class_name }}Model
|
||||
from .schema import {{ class_name }}CreateSchema, {{ class_name }}UpdateSchema, {{ class_name }}OutSchema
|
||||
|
||||
|
||||
class {{ class_name }}CRUD(CRUDBase[{{ class_name }}Model, {{ class_name }}CreateSchema, {{ class_name }}UpdateSchema]):
|
||||
"""{{ function_name }}数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化CRUD数据层
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
super().__init__(model={{ class_name }}Model, auth=auth)
|
||||
|
||||
async def get_by_id_{{ business_name }}_crud(self, id: int, preload: list | None = None) -> {{ class_name }}Model | None:
|
||||
"""
|
||||
详情
|
||||
|
||||
参数:
|
||||
- id (int): 对象ID
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- {{ class_name }}Model | None: 模型实例或None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def list_{{ business_name }}_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list | None = None) -> Sequence[{{ class_name }}Model]:
|
||||
"""
|
||||
列表查询
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[{{ class_name }}Model]: 模型实例序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_{{ business_name }}_crud(self, data: {{ class_name }}CreateSchema) -> {{ class_name }}Model | None:
|
||||
"""
|
||||
创建
|
||||
|
||||
参数:
|
||||
- data ({{ class_name }}CreateSchema): 创建模型
|
||||
|
||||
返回:
|
||||
- {{ class_name }}Model | None: 模型实例或None
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_{{ business_name }}_crud(self, id: int, data: {{ class_name }}UpdateSchema) -> {{ class_name }}Model | None:
|
||||
"""
|
||||
更新
|
||||
|
||||
参数:
|
||||
- id (int): 对象ID
|
||||
- data ({{ class_name }}UpdateSchema): 更新模型
|
||||
|
||||
返回:
|
||||
- {{ class_name }}Model | None: 模型实例或None
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_{{ business_name }}_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
批量删除
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 对象ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def set_available_{{ business_name }}_crud(self, ids: list[int], status: str) -> None:
|
||||
"""
|
||||
批量设置可用状态
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 对象ID列表
|
||||
- status (str): 可用状态
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.set(ids=ids, status=status)
|
||||
|
||||
async def page_{{ business_name }}_crud(self, offset: int, limit: int, order_by: list[dict] | None = None, search: dict | None = None, preload: list | None = None) -> dict:
|
||||
"""
|
||||
分页查询
|
||||
|
||||
参数:
|
||||
- offset (int): 偏移量
|
||||
- limit (int): 每页数量
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- search (dict | None): 查询参数,未提供时查询所有
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Dict: 分页数据
|
||||
"""
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
search_dict = search or {}
|
||||
return await self.page(
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
order_by=order_by_list,
|
||||
search=search_dict,
|
||||
out_schema={{ class_name }}OutSchema,
|
||||
preload=preload
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
{% for model_import in model_import_list %}
|
||||
{{ model_import }}
|
||||
{% endfor %}
|
||||
{% if table.sub %}
|
||||
from sqlalchemy.orm import relationship
|
||||
{% endif %}
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class {{ class_name }}Model(ModelMixin, UserMixin):
|
||||
"""
|
||||
{{ function_name }}表
|
||||
"""
|
||||
__tablename__: str = '{{ table_name }}'
|
||||
__table_args__: dict[str, str] = {'comment': '{{ function_name }}'}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
{% for column in columns %}
|
||||
{% if column.column_name not in ['id', 'uuid', 'status', 'description', 'created_time', 'updated_time', 'created_id', 'updated_id'] %}
|
||||
{{ column.column_name }}: Mapped[{{ column.python_type }} | None] = mapped_column({{ column.column_type|get_sqlalchemy_type }}, {% if column.pk %}primary_key=True, {% endif %}{% if column.increment %}autoincrement=True, {% endif %}{% if column.required or column.pk %}nullable=False{% else %}nullable=True{% endif %}, comment='{{ column.column_comment }}')
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if table.sub %}
|
||||
{{ sub_class_name }}_list = relationship('{{ sub_class_name }}', back_populates='{{ business_name }}')
|
||||
{% endif %}
|
||||
@@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
{% if table.sub %}
|
||||
from typing import List
|
||||
{% endif %}
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import Query
|
||||
|
||||
{% if table.created_time %}
|
||||
from app.core.validator import DateTimeStr
|
||||
{% endif %}
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
|
||||
class {{ class_name }}CreateSchema(BaseModel):
|
||||
"""
|
||||
{{ function_name }}新增模型
|
||||
"""
|
||||
{% for column in columns %}
|
||||
{% if column.column_name not in ['id', 'uuid', 'created_time', 'updated_time', 'created_id', 'updated_id'] %}
|
||||
{% if column.column_name == 'status' %}
|
||||
{{ column.column_name }}: {{ column.python_type }} = Field(default="0", description='{{ column.column_comment }}')
|
||||
{% elif column.column_name == 'description' %}
|
||||
{{ column.column_name }}: str | None = Field(default=None, max_length=255, description='{{ column.column_comment }}')
|
||||
{% else %}
|
||||
{{ column.column_name }}: {{ column.python_type }} = Field(default=..., description='{{ column.column_comment }}')
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
class {{ class_name }}UpdateSchema({{ class_name }}CreateSchema):
|
||||
"""
|
||||
{{ function_name }}更新模型
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class {{ class_name }}OutSchema({{ class_name }}CreateSchema, BaseSchema, UserBySchema):
|
||||
"""
|
||||
{{ function_name }}响应模型
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class {{ class_name }}QueryParam:
|
||||
"""{{ function_name }}查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
{% for column in columns %}
|
||||
{% if column.query_type == 'LIKE' %}
|
||||
{{ column.column_name }}: str | None = Query(None, description="{{ column.column_comment }}"),
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for column in columns %}
|
||||
{% if column.query_type == 'EQ' and column.column_name not in ['created_time', 'updated_time'] %}
|
||||
{{ column.column_name }}: {{ column.python_type }} | None = Query(None, description="{{ column.column_comment }}"),
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if table.created_time %}
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
{% endif %}
|
||||
{% if table.updated_time %}
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
{% endif %}
|
||||
|
||||
) -> None:
|
||||
|
||||
{% for column in columns %}
|
||||
{% if column.query_type == 'LIKE' %}
|
||||
# 模糊查询字段
|
||||
self.{{ column.column_name }} = ("like", {{ column.column_name }})
|
||||
{% elif column.query_type == 'EQ' and column.column_name not in ['created_time', 'updated_time'] %}
|
||||
# 精确查询字段
|
||||
self.{{ column.column_name }} = {{ column.column_name }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if table.created_time %}
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
{% endif %}
|
||||
{% if table.updated_time %}
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
{% endif %}
|
||||
@@ -0,0 +1,228 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import io
|
||||
from fastapi import UploadFile
|
||||
import pandas as pd
|
||||
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .schema import {{ class_name }}CreateSchema, {{ class_name }}UpdateSchema, {{ class_name }}OutSchema, {{ class_name }}QueryParam
|
||||
from .crud import {{ class_name }}CRUD
|
||||
|
||||
|
||||
class {{ class_name }}Service:
|
||||
"""
|
||||
{{ function_name }}服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def detail_{{ business_name }}_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""详情"""
|
||||
obj = await {{ class_name }}CRUD(auth).get_by_id_{{ business_name }}_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="该数据不存在")
|
||||
return {{ class_name }}OutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def list_{{ business_name }}_service(cls, auth: AuthSchema, search: {{ class_name }}QueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
||||
"""列表查询"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await {{ class_name }}CRUD(auth).list_{{ business_name }}_crud(search=search_dict, order_by=order_by)
|
||||
return [{{ class_name }}OutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def page_{{ business_name }}_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: {{ class_name }}QueryParam | None = None, order_by: list[dict] | None = None) -> dict:
|
||||
"""分页查询(数据库分页)"""
|
||||
search_dict = search.__dict__ if search else {}
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
offset = (page_no - 1) * page_size
|
||||
result = await {{ class_name }}CRUD(auth).page_{{ business_name }}_crud(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by_list,
|
||||
search=search_dict
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def create_{{ business_name }}_service(cls, auth: AuthSchema, data: {{ class_name }}CreateSchema) -> dict:
|
||||
"""创建"""
|
||||
# 检查唯一性约束
|
||||
{% for column in columns %}
|
||||
{% if column.is_unique == '1' %}
|
||||
obj = await {{ class_name }}CRUD(auth).get({{ column.column_name }}=data.{{ column.column_name }})
|
||||
if obj:
|
||||
raise CustomException(msg='创建失败,{{ column.column_comment }}已存在')
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
obj = await {{ class_name }}CRUD(auth).create_{{ business_name }}_crud(data=data)
|
||||
return {{ class_name }}OutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_{{ business_name }}_service(cls, auth: AuthSchema, id: int, data: {{ class_name }}UpdateSchema) -> dict:
|
||||
"""更新"""
|
||||
# 检查数据是否存在
|
||||
obj = await {{ class_name }}CRUD(auth).get_by_id_{{ business_name }}_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='更新失败,该数据不存在')
|
||||
|
||||
# 检查唯一性约束
|
||||
{% for column in columns %}
|
||||
{% if column.is_unique == '1' %}
|
||||
exist_obj = await {{ class_name }}CRUD(auth).get({{ column.column_name }}=data.{{ column.column_name }})
|
||||
if exist_obj and exist_obj.id != id:
|
||||
raise CustomException(msg='更新失败,{{ column.column_comment }}重复')
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
obj = await {{ class_name }}CRUD(auth).update_{{ business_name }}_crud(id=id, data=data)
|
||||
return {{ class_name }}OutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_{{ business_name }}_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""删除"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
for id in ids:
|
||||
obj = await {{ class_name }}CRUD(auth).get_by_id_{{ business_name }}_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
|
||||
await {{ class_name }}CRUD(auth).delete_{{ business_name }}_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_available_{{ business_name }}_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""批量设置状态"""
|
||||
await {{ class_name }}CRUD(auth).set_available_{{ business_name }}_crud(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def batch_export_{{ business_name }}_service(cls, obj_list: list[dict]) -> bytes:
|
||||
"""批量导出"""
|
||||
mapping_dict = {
|
||||
{% for column in columns %}
|
||||
'{{ column.column_name }}': '{{ column.column_comment }}',
|
||||
{% endfor %}
|
||||
'updated_id': '更新者ID',
|
||||
}
|
||||
|
||||
data = obj_list.copy()
|
||||
for item in data:
|
||||
# 状态转换
|
||||
if 'status' in item:
|
||||
item['status'] = '启用' if item.get('status') == '0' else '停用'
|
||||
# 创建者转换
|
||||
creator_info = item.get('creator')
|
||||
if isinstance(creator_info, dict):
|
||||
item['creator'] = creator_info.get('name', '未知')
|
||||
elif creator_info is None:
|
||||
item['creator'] = '未知'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@classmethod
|
||||
async def batch_import_{{ business_name }}_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
|
||||
"""批量导入"""
|
||||
header_dict = {
|
||||
{% for column in columns %}
|
||||
'{{ column.column_comment }}': '{{ column.column_name }}',
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
try:
|
||||
contents = await file.read()
|
||||
df = pd.read_excel(io.BytesIO(contents))
|
||||
await file.close()
|
||||
|
||||
if df.empty:
|
||||
raise CustomException(msg="导入文件为空")
|
||||
|
||||
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
|
||||
if missing_headers:
|
||||
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
|
||||
|
||||
df.rename(columns=header_dict, inplace=True)
|
||||
|
||||
# 验证必填字段
|
||||
{% for column in columns %}
|
||||
{% if column.required == '1' %}
|
||||
errors = []
|
||||
missing_rows = df[df['{{ column.column_name }}'].isnull()].index.tolist()
|
||||
if missing_rows:
|
||||
field_name = [k for k,v in header_dict.items() if v == field][0]
|
||||
rows_str = "、".join([str(i+1) for i in missing_rows])
|
||||
errors.append(f"{field_name}不能为空,第{rows_str}行")
|
||||
if errors:
|
||||
raise CustomException(msg=f"导入失败,以下行缺少必要字段:\n{'; '.join(errors)}")
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
error_msgs = []
|
||||
success_count = 0
|
||||
count = 0
|
||||
|
||||
for index, row in df.iterrows():
|
||||
count += 1
|
||||
try:
|
||||
data = {
|
||||
{% for column in columns %}
|
||||
"{{ column.column_name }}": row['{{ column.column_name }}'],
|
||||
{% endfor %}
|
||||
}
|
||||
# 使用CreateSchema做校验后入库
|
||||
create_schema = {{ class_name }}CreateSchema.model_validate(data)
|
||||
|
||||
# 检查唯一性约束
|
||||
{% for column in columns %}
|
||||
{% if column.is_unique == '1' %}
|
||||
exists_obj = await {{ class_name }}CRUD(auth).get({{ column.column_name }}=create_schema.{{ column.column_name }})
|
||||
if exists_obj:
|
||||
if update_support:
|
||||
await {{ class_name }}CRUD(auth).update(id=exists_obj.id, data=create_schema)
|
||||
success_count += 1
|
||||
else:
|
||||
error_msgs.append(f"第{count}行: {{ column.column_comment }} {create_schema.{{ column.column_name }}} 已存在")
|
||||
continue
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
await {{ class_name }}CRUD(auth).create_{{ business_name }}_crud(data=create_schema)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
error_msgs.append(f"第{count}行: {str(e)}")
|
||||
continue
|
||||
|
||||
result = f"成功导入 {success_count} 条数据"
|
||||
if error_msgs:
|
||||
result += "\n错误信息:\n" + "\n".join(error_msgs)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"批量导入失败: {str(e)}")
|
||||
raise CustomException(msg=f"导入失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def import_template_download_{{ business_name }}_service(cls) -> bytes:
|
||||
"""下载导入模板"""
|
||||
header_list = [
|
||||
{% for column in columns %}
|
||||
'{{ column.column_comment }}',
|
||||
{% endfor %}
|
||||
]
|
||||
selector_header_list = []
|
||||
option_list = []
|
||||
|
||||
# 添加下拉选项
|
||||
{% for column in columns %}
|
||||
{% if column.html_type == 'select' and column.dict_type %}
|
||||
selector_header_list.append('{{ column.column_comment }}')
|
||||
option_list.append({'{{ column.column_comment }}': []})
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
return ExcelUtil.get_excel_template(
|
||||
header_list=header_list,
|
||||
selector_header_list=selector_header_list,
|
||||
option_list=option_list
|
||||
)
|
||||
@@ -0,0 +1,64 @@
|
||||
-- 统一的菜单 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 %}
|
||||
@@ -0,0 +1,137 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
const API_PATH = "/{{ package_name }}/{{ business_name|lower }}";
|
||||
|
||||
const {{ class_name }}API = {
|
||||
// 列表查询
|
||||
list{{ class_name }}(query: {{ class_name }}PageQuery) {
|
||||
return request<ApiResponse<PageResult<{{ class_name }}Table[]>>>({
|
||||
url: `${API_PATH}/list`,
|
||||
method: "get",
|
||||
params: query,
|
||||
});
|
||||
},
|
||||
|
||||
// 详情查询
|
||||
detail{{ class_name }}(id: number) {
|
||||
return request<ApiResponse<{{ class_name }}Table>>({
|
||||
url: `${API_PATH}/detail/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
},
|
||||
|
||||
// 新增
|
||||
create{{ class_name }}(body: {{ class_name }}Form) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/create`,
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
// 修改(带主键)
|
||||
update{{ class_name }}(id: number, body: {{ class_name }}Form) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/update/${id}`,
|
||||
method: "put",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
// 删除(支持批量)
|
||||
delete{{ class_name }}(ids: number[]) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/delete`,
|
||||
method: "delete",
|
||||
data: ids,
|
||||
});
|
||||
},
|
||||
|
||||
// 批量启用/停用
|
||||
batch{{ class_name }}(body: BatchType) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/available/setting`,
|
||||
method: "patch",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
|
||||
// 导出
|
||||
export{{ class_name }}(query: {{ class_name }}PageQuery) {
|
||||
return request<Blob>({
|
||||
url: `${API_PATH}/export`,
|
||||
method: "post",
|
||||
data: query,
|
||||
responseType: "blob",
|
||||
});
|
||||
},
|
||||
|
||||
// 下载导入模板
|
||||
downloadTemplate{{ class_name }}() {
|
||||
return request<Blob>({
|
||||
url: `${API_PATH}/download/template`,
|
||||
method: "post",
|
||||
responseType: "blob",
|
||||
});
|
||||
},
|
||||
|
||||
// 导入
|
||||
import{{ class_name }}(body: FormData) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/import`,
|
||||
method: "post",
|
||||
data: body,
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default {{ class_name }}API;
|
||||
|
||||
// ------------------------------
|
||||
// TS 类型声明
|
||||
// ------------------------------
|
||||
|
||||
// 列表查询参数
|
||||
export interface {{ class_name }}PageQuery extends PageQuery {
|
||||
{% for column in columns %}
|
||||
{% if column.is_query and column.column != "BETWEEN" and column.column_name not in ['created_time', 'updated_time'] %}
|
||||
{{ column.column_name }}?: {{
|
||||
'string' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio')
|
||||
else 'number' if column.is_pk == '1'
|
||||
else 'number' if column.column_name in ['created_id', 'updated_id']
|
||||
else 'string'
|
||||
}};
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
created_time?: string[];
|
||||
updated_time?: string[];
|
||||
}
|
||||
|
||||
// 列表展示项
|
||||
export interface {{ class_name }}Table extends BaseType{
|
||||
{% for column in columns %}
|
||||
{% if column.column_name not in ['id', 'uuid', 'status', 'description', 'created_time', 'updated_time'] %}
|
||||
{{ column.column_name }}?: {{
|
||||
'boolean' if ('status' in (column.column_name|lower)) or (column.html_type == 'radio')
|
||||
else 'number' if column.is_pk == 1
|
||||
else 'string'
|
||||
}};
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
created_by?: creatorType;
|
||||
updated_by?: updatorType;
|
||||
}
|
||||
|
||||
// 新增/修改/详情表单参数
|
||||
export interface {{ class_name }}Form extends BaseFormType{
|
||||
{% for column in columns %}
|
||||
{% if (column.is_insert == 1 or column.is_edit == 1) and column.column_name not in ['id', 'uuid', 'status', 'description', 'created_time', 'updated_time', 'created_id', 'updated_id'] %}
|
||||
{{ column.column_name }}?: {{
|
||||
'boolean' if ('status' in (column.column_name|lower)) or (column.html_type == 'radio')
|
||||
else 'number' if column.is_pk == 1
|
||||
else 'string'
|
||||
}};
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
@@ -0,0 +1,858 @@
|
||||
<!-- {{ function_name }} -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<div v-show="visible" class="search-container">
|
||||
<el-form
|
||||
ref="queryFormRef"
|
||||
:model="queryFormData"
|
||||
label-suffix=":"
|
||||
:inline="true"
|
||||
@submit.prevent="handleQuery"
|
||||
>
|
||||
{% for column in columns %}
|
||||
{% if column.is_query == 1 %}
|
||||
{% set dict_type = column.dict_type %}
|
||||
{% set column_comment = column.column_comment if column.column_comment else '' %}
|
||||
{% set parentheseIndex = column_comment.find("(") %}
|
||||
{% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %}
|
||||
{% if column.column_name == "status" %}
|
||||
<el-form-item prop="status" label="状态">
|
||||
<el-select
|
||||
v-model="queryFormData.status"
|
||||
placeholder="请选择状态"
|
||||
style="width: 170px"
|
||||
clearable
|
||||
>
|
||||
<el-option value="0" label="启用" />
|
||||
<el-option value="1" label="停用" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
{% elif column.column_name == "created_id"%}
|
||||
<el-form-item v-if="isExpand" prop="created_id" label="创建人">
|
||||
<UserTableSelect
|
||||
v-model="queryFormData.created_id"
|
||||
@confirm-click="handleConfirm"
|
||||
@clear-click="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
{% elif column.column_name == "updated_id"%}
|
||||
<el-form-item v-if="isExpand" prop="updated_id" label="更新人">
|
||||
<UserTableSelect
|
||||
v-model="queryFormData.updated_id"
|
||||
@confirm-click="handleConfirm"
|
||||
@clear-click="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
{% elif column.column_name == "created_time"%}
|
||||
<el-form-item v-if="isExpand" prop="created_time" label="创建时间">
|
||||
<DatePicker v-model="createdDateRange" @update:model-value="handleCreatedDateRangeChange" />
|
||||
</el-form-item>
|
||||
{% elif column.column_name == "updated_time"%}
|
||||
<el-form-item v-if="isExpand" prop="updated_time" label="更新时间">
|
||||
<DatePicker v-model="updatedDateRange" @update:model-value="handleUpdatedDateRangeChange" />
|
||||
</el-form-item>
|
||||
{% elif column.html_type == "input" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.column_name }}">
|
||||
<el-input v-model="queryFormData.{{ column.column_name }}" placeholder="请输入{{ comment }}" clearable />
|
||||
</el-form-item>
|
||||
{% elif (column.html_type == "select" or column.html_type == "radio") and dict_type != "" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.column_name }}">
|
||||
<el-select v-model="queryFormData.{{ column.column_name }}" placeholder="请选择{{ comment }}" style="width: 180px" clearable>
|
||||
<el-option v-for="dict in dictStore.getDictArray('{{ dict_type }}')" :key="dict.dict_value" :label="dict.dict_label" :value="dict.dict_value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
{% elif (column.html_type == "select" or column.html_type == "radio") and dict_type %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.column_name }}">
|
||||
<el-select v-model="queryFormData.{{ column.column_name }}" placeholder="请选择{{ comment }}" clearable>
|
||||
<el-option label="请选择字典生成" value="" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
{% elif column.html_type == "datetime" and column.query_type != "BETWEEN" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.column_name }}">
|
||||
<el-date-picker v-model="queryFormData.{{ column.column_name }}" type="date" value-format="YYYY-MM-DD" clearable placeholder="请选择{{ comment }}" />
|
||||
</el-form-item>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<!-- 查询、重置、展开/收起按钮 -->
|
||||
<el-form-item>
|
||||
<el-button
|
||||
v-hasPerm="['{{ module_name }}:{{ business_name }}:query']"
|
||||
type="primary"
|
||||
icon="search"
|
||||
@click="handleQuery"
|
||||
>
|
||||
查询
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['{{ module_name }}:{{ business_name }}:query']"
|
||||
icon="refresh"
|
||||
@click="handleResetQuery"
|
||||
>
|
||||
重置
|
||||
</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable">
|
||||
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
|
||||
{{ '{{' }} isExpand ? "收起" : "展开" {{ '}}' }}
|
||||
<el-icon>
|
||||
<template v-if="isExpand">
|
||||
<ArrowUp />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArrowDown />
|
||||
</template>
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-card class="data-table">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
{{ function_name }}列表
|
||||
<el-tooltip content="{{ function_name }}列表">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--left">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-hasPerm="['{{ module_name }}:{{ business_name }}:create']"
|
||||
type="success"
|
||||
icon="plus"
|
||||
@click="handleOpenDialog('create')"
|
||||
>
|
||||
新增
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-hasPerm="['{{ module_name }}:{{ business_name }}:delete']"
|
||||
type="danger"
|
||||
icon="delete"
|
||||
:disabled="selectIds.length === 0"
|
||||
@click="handleDelete(selectIds)"
|
||||
>
|
||||
批量删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-dropdown v-hasPerm="['{{ module_name }}:{{ business_name }}:batch']" trigger="click">
|
||||
<el-button type="default" :disabled="selectIds.length === 0" icon="ArrowDown">
|
||||
更多
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :icon="Check" @click="handleMoreClick('0')">
|
||||
批量启用
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :icon="CircleClose" @click="handleMoreClick('1')">
|
||||
批量停用
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="data-table__toolbar--right">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="导入">
|
||||
<el-button
|
||||
v-hasPerm="['{{ module_name }}:{{ business_name }}:import']"
|
||||
type="success"
|
||||
icon="upload"
|
||||
circle
|
||||
@click="handleOpenImportDialog"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="导出">
|
||||
<el-button
|
||||
v-hasPerm="['{{ module_name }}:{{ business_name }}:export']"
|
||||
type="warning"
|
||||
icon="download"
|
||||
circle
|
||||
@click="handleOpenExportsModal"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="搜索显示/隐藏">
|
||||
<el-button
|
||||
v-hasPerm="['*:*:*']"
|
||||
type="info"
|
||||
icon="search"
|
||||
circle
|
||||
@click="visible = !visible"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button
|
||||
v-hasPerm="['{{ module_name }}:{{ business_name }}:query']"
|
||||
type="primary"
|
||||
icon="refresh"
|
||||
circle
|
||||
@click="handleRefresh"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-popover placement="bottom" trigger="click">
|
||||
<template #reference>
|
||||
<el-button type="danger" icon="operation" circle></el-button>
|
||||
</template>
|
||||
<el-scrollbar max-height="350px">
|
||||
<template v-for="column in tableColumns" :key="column.prop">
|
||||
<el-checkbox v-if="column.prop" v-model="column.show" :label="column.label" />
|
||||
</template>
|
||||
</el-scrollbar>
|
||||
</el-popover>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域:系统配置列表 -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
:data="pageTableData"
|
||||
highlight-current-row
|
||||
class="data-table__content"
|
||||
:height="450"
|
||||
border
|
||||
stripe
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'selection')?.show"
|
||||
type="selection"
|
||||
min-width="55"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'index')?.show"
|
||||
fixed
|
||||
label="序号"
|
||||
min-width="60"
|
||||
>
|
||||
<template #default="scope">
|
||||
{{ '{{' }} (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 {{ '}}' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
{% for column in columns %}
|
||||
{% set python_field = column.column_name %}
|
||||
{% set column_comment = column.column_comment if column.column_comment else '' %}
|
||||
{% set parentheseIndex = column_comment.find("(") %}
|
||||
{% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %}
|
||||
{% if column.is_list == 1 %}
|
||||
<el-table-column v-if="tableColumns.find((col) => col.prop === '{{ python_field }}')?.show" label="{{ comment }}" prop="{{ python_field }}" min-width="140">
|
||||
{% if python_field == "status" %}
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status == '0' ? 'success' : 'info'">
|
||||
{{ '{{' }} scope.row.status == '0' ? '启用' : '停用' {{ '}}' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
{% elif python_field == "created_id" %}
|
||||
<template #default="scope">
|
||||
<el-tag>{{ '{{' }} scope.row.created_by?.name {{ '}}' }}</el-tag>
|
||||
</template>
|
||||
{% elif python_field == "updated_id" %}
|
||||
<template #default="scope">
|
||||
<el-tag>{{ '{{' }} scope.row.updated_by?.name {{ '}}' }}</el-tag>
|
||||
</template>
|
||||
{% endif %}
|
||||
</el-table-column>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<el-table-column
|
||||
v-if="tableColumns.find((col) => col.prop === 'operation')?.show"
|
||||
fixed="right"
|
||||
label="操作"
|
||||
align="center"
|
||||
min-width="180"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPerm="['{{ module_name }}:{{ business_name }}:detail']"
|
||||
type="info"
|
||||
size="small"
|
||||
link
|
||||
icon="document"
|
||||
@click="handleOpenDialog('detail', scope.row.id)"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['{{ module_name }}:{{ business_name }}:update']"
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
icon="edit"
|
||||
@click="handleOpenDialog('update', scope.row.id)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['{{ module_name }}:{{ business_name }}:delete']"
|
||||
type="danger"
|
||||
size="small"
|
||||
link
|
||||
icon="delete"
|
||||
@click="handleDelete([scope.row.id])"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<template #footer>
|
||||
<pagination
|
||||
v-model:total="total"
|
||||
v-model:page="queryFormData.page_no"
|
||||
v-model:limit="queryFormData.page_size"
|
||||
@pagination="loadingData"
|
||||
/>
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 弹窗区域 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible.visible"
|
||||
:title="dialogVisible.title"
|
||||
@close="handleCloseDialog"
|
||||
>
|
||||
<!-- 详情 -->
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<el-descriptions :column="4" border>
|
||||
{% for column in columns %}
|
||||
{% set column_comment = column.column_comment if column.column_comment else '' %}
|
||||
{% set parentheseIndex = column_comment.find("(") %}
|
||||
{% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %}
|
||||
{% if column.column_name == 'status' %}
|
||||
<el-descriptions-item label="状态" :span="2">
|
||||
<el-tag :type="detailFormData.status == '0' ? 'success' : 'danger'">
|
||||
{{ '{{' }} detailFormData.status == '0' ? "启用" : "停用" {{ '}}' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
{% elif column.column_name == 'created_id' %}
|
||||
<el-descriptions-item label="创建人" :span="2">
|
||||
{{ '{{' }} detailFormData.created_by?.name {{ '}}' }}
|
||||
</el-descriptions-item>
|
||||
{% elif column.column_name == 'updated_id' %}
|
||||
<el-descriptions-item label="更新人" :span="2">
|
||||
{{ '{{' }} detailFormData.updated_by?.name {{ '}}' }}
|
||||
</el-descriptions-item>
|
||||
{% else %}
|
||||
<el-descriptions-item label="{{ comment }}" :span="2">
|
||||
{{ '{{' }} detailFormData.{{ column.column_name }} {{ '}}' }}
|
||||
</el-descriptions-item>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</el-descriptions>
|
||||
</template>
|
||||
|
||||
<!-- 新增、编辑表单 -->
|
||||
<template v-else>
|
||||
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-suffix=":" label-width="auto" label-position="right">
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert == 1 or column.is_edit == 1 %}
|
||||
{% set dict_type = column.dict_type %}
|
||||
{% set column_comment = column.column_comment if column.column_comment else '' %}
|
||||
{% set parentheseIndex = column_comment.find("(") %}
|
||||
{% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %}
|
||||
{% set required = 'true' if column.is_nullable == '1' else 'false' %}
|
||||
{% if column.column_name not in ['id', 'uuid', 'created_time', 'updated_time', 'created_id', 'updated_id'] %}
|
||||
{% if column.column_name == "status" %}
|
||||
<el-form-item label="状态" prop="status" :required="true">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio value="0">启用</el-radio>
|
||||
<el-radio value="1">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
{% elif column.column_name == "description" %}
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input
|
||||
v-model="formData.description"
|
||||
:rows="4"
|
||||
:maxlength="100"
|
||||
show-word-limit
|
||||
type="textarea"
|
||||
placeholder="请输入描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
{% elif column.html_type == "input" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.column_name }}" :required="{{ required }}">
|
||||
<el-input v-model="formData.{{ column.column_name }}" placeholder="请输入{{ comment }}" />
|
||||
</el-form-item>
|
||||
{% elif column.html_type == "textarea" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.column_name }}" :required="{{ required }}">
|
||||
<el-input v-model="formData.{{ column.column_name }}" type="textarea" placeholder="请输入{{ comment }}" rows="4" :maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
{% elif (column.html_type == "select" or column.html_type == "radio") and dict_type != "" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.column_name }}" :required="{{ required }}">
|
||||
<el-select v-model="formData.{{ column.column_name }}" placeholder="请选择{{ comment }}">
|
||||
<el-option v-for="dict in dictStore.getDictArray('{{ dict_type }}')" :key="dict.dict_value" :label="dict.dict_label" :value="dict.dict_value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
{% elif (column.html_type == "select" or column.html_type == "radio") and dict_type %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.column_name }}" :required="{{ required }}">
|
||||
<el-select v-model="formData.{{ column.column_name }}" placeholder="请选择{{ comment }}">
|
||||
<el-option label="请选择字典生成" value="" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
{% elif column.html_type == "date" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.column_name }}" :required="{{ required }}">
|
||||
<el-date-picker v-model="formData.{{ column.column_name }}" type="date" value-format="YYYY-MM-DD" placeholder="请选择{{ comment }}" />
|
||||
</el-form-item>
|
||||
{% elif column.html_type == "datetime" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.column_name }}" :required="{{ required }}">
|
||||
<el-date-picker v-model="formData.{{ column.column_name }}" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择{{ comment }}" />
|
||||
</el-form-item>
|
||||
{% elif column.html_type == "checkbox" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.column_name }}">
|
||||
<el-checkbox v-model="formData.{{ column.column_name }}">{{ comment }}</el-checkbox>
|
||||
</el-form-item>
|
||||
{% elif column.html_type == "imageUpload" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.column_name }}">
|
||||
<SingleImageUpload v-model="formData.{{ column.column_name }}" />
|
||||
</el-form-item>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<!-- 详情弹窗不需要确定按钮的提交逻辑 -->
|
||||
<el-button @click="handleCloseDialog">取消</el-button>
|
||||
<el-button v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">
|
||||
确定
|
||||
</el-button>
|
||||
<el-button v-else type="primary" @click="handleCloseDialog">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<ImportModal
|
||||
v-model="importDialogVisible"
|
||||
:content-config="curdContentConfig"
|
||||
@upload="handleUpload"
|
||||
/>
|
||||
|
||||
<!-- 导出弹窗 -->
|
||||
<ExportModal
|
||||
v-model="exportsDialogVisible"
|
||||
:content-config="curdContentConfig"
|
||||
:query-params="queryFormData"
|
||||
:page-data="pageTableData"
|
||||
:selection-data="selectionRows"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "{{ class_name }}",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { QuestionFilled, ArrowUp, ArrowDown, Check, CircleClose } from '@element-plus/icons-vue'
|
||||
import { formatToDateTime } from "@/utils/dateUtil";
|
||||
import { useDictStore } from "@/store";
|
||||
import { ResultEnum } from '@/enums/api/result.enum'
|
||||
import DatePicker from "@/components/DatePicker/index.vue";
|
||||
import type { IContentConfig } from "@/components/CURD/types";
|
||||
import ImportModal from "@/components/CURD/ImportModal.vue";
|
||||
import ExportModal from "@/components/CURD/ExportModal.vue";
|
||||
import {{ class_name }}API, { {{ class_name }}PageQuery, {{ class_name }}Table, {{ class_name }}Form } from '@/api/{{ module_name }}/{{ business_name }}'
|
||||
|
||||
const visible = ref(true);
|
||||
const isExpand = ref(false);
|
||||
const isExpandable = ref(true);
|
||||
const queryFormRef = ref();
|
||||
const dataFormRef = ref();
|
||||
const total = ref(0);
|
||||
const selectIds = ref<number[]>([]);
|
||||
const selectionRows = ref<{{ class_name }}Table[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 字典仓库与需要加载的字典类型
|
||||
const dictStore = useDictStore()
|
||||
const dictTypes: any = [
|
||||
{% for column in columns %}
|
||||
{% if column.dict_type %}
|
||||
'{{ column.dict_type }}',
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
|
||||
// 分页表单
|
||||
const pageTableData = ref<{{ class_name }}Table[]>([]);
|
||||
|
||||
// 表格列配置
|
||||
const tableColumns = ref([
|
||||
{ prop: "selection", label: "选择框", show: true },
|
||||
{ prop: "index", label: "序号", show: true },
|
||||
{% for column in columns %}
|
||||
{% if column.is_list == 1 %}
|
||||
{ prop: '{{ column.column_name }}', label: '{{ column.column_comment or column.column_name }}', show: true },
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{ prop: 'operation', label: '操作', show: true }
|
||||
]);
|
||||
|
||||
// 导出列(不含选择/序号/操作)
|
||||
const exportColumns = [
|
||||
{% for column in columns %}
|
||||
{% if column.is_list == 1 %}
|
||||
{ prop: '{{ column.column_name }}', label: '{{ column.column_comment or column.column_name }}' },
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
|
||||
// 导入/导出配置
|
||||
const curdContentConfig = {
|
||||
permPrefix: "{{ module_name }}:{{ business_name }}",
|
||||
cols: exportColumns as any,
|
||||
importTemplate: () => {{ class_name }}API.downloadTemplate{{ class_name }}(),
|
||||
exportsAction: async (params: any) => {
|
||||
const query: any = { ...params };
|
||||
query.status = '0';
|
||||
query.page_no = 1;
|
||||
query.page_size = 9999;
|
||||
const all: any[] = [];
|
||||
while (true) {
|
||||
const res = await {{ class_name }}API.list{{ class_name }}(query);
|
||||
const items = res.data?.data?.items || [];
|
||||
const total = res.data?.data?.total || 0;
|
||||
all.push(...items);
|
||||
if (all.length >= total || items.length === 0) break;
|
||||
query.page_no += 1;
|
||||
}
|
||||
return all;
|
||||
},
|
||||
} as unknown as IContentConfig;
|
||||
|
||||
// 详情表单
|
||||
const detailFormData = ref<{{ class_name }}Table>({});
|
||||
// 日期范围临时变量
|
||||
const createdDateRange = ref<[Date, Date] | []>([]);
|
||||
// 更新时间范围临时变量
|
||||
const updatedDateRange = ref<[Date, Date] | []>([]);
|
||||
|
||||
// 处理创建时间范围变化
|
||||
function handleCreatedDateRangeChange(range: [Date, Date]) {
|
||||
createdDateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.created_time = [formatToDateTime(range[0]), formatToDateTime(range[1])];
|
||||
} else {
|
||||
queryFormData.created_time = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理更新时间范围变化
|
||||
function handleUpdatedDateRangeChange(range: [Date, Date]) {
|
||||
updatedDateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.updated_time = [formatToDateTime(range[0]), formatToDateTime(range[1])];
|
||||
} else {
|
||||
queryFormData.updated_time = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 分页查询参数
|
||||
const queryFormData = reactive<{{ class_name }}PageQuery>({
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
{% for column in columns %}
|
||||
{% if column.is_query == 1 %}
|
||||
{{ column.column_name }}: undefined,
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
});
|
||||
|
||||
|
||||
// 编辑表单
|
||||
const formData = reactive<{{ class_name }}Form>({
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert == 1 or column.is_edit == 1 %}
|
||||
{% if column.column_name not in ['uuid', 'created_time', 'updated_time', 'created_id', 'updated_id'] %}
|
||||
{{ column.column_name }}: undefined,
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
});
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: "",
|
||||
visible: false,
|
||||
type: "create" as "create" | "update" | "detail",
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert == 1 or column.is_edit == 1 %}
|
||||
{% set required = 'true' if column.is_nullable == 1 else 'false' %}
|
||||
{{ column.column_name }}: [
|
||||
{ required: {{ required }}, message: '请输入{{ column.column_comment or column.column_name }}', trigger: 'blur' },
|
||||
],
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
});
|
||||
|
||||
// 导入弹窗显示状态
|
||||
const importDialogVisible = ref(false);
|
||||
|
||||
// 导出弹窗显示状态
|
||||
const exportsDialogVisible = ref(false);
|
||||
|
||||
// 打开导入弹窗
|
||||
function handleOpenImportDialog() {
|
||||
importDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 打开导出弹窗
|
||||
function handleOpenExportsModal() {
|
||||
exportsDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh() {
|
||||
await loadingData();
|
||||
}
|
||||
|
||||
// 加载表格数据
|
||||
async function loadingData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await {{ class_name }}API.list{{ class_name }}(queryFormData);
|
||||
pageTableData.value = response.data.data.items;
|
||||
total.value = response.data.data.total;
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
async function handleQuery() {
|
||||
queryFormData.page_no = 1;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 选择创建人后触发查询
|
||||
function handleConfirm() {
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value.resetFields();
|
||||
queryFormData.page_no = 1;
|
||||
// 重置日期范围选择器
|
||||
createdDateRange.value = [];
|
||||
updatedDateRange.value = [];
|
||||
queryFormData.created_time = undefined;
|
||||
queryFormData.updated_time = undefined;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 定义初始表单数据常量
|
||||
const initialFormData: {{ class_name }}Form = {
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert == 1 or column.is_edit == 1 %}
|
||||
{% if column.column_name not in ['uuid', 'created_time', 'updated_time', 'created_id', 'updated_id'] %}
|
||||
{{ column.column_name }}: undefined,
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
}
|
||||
// 完全重置 formData 为初始状态
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 行复选框选中项变化
|
||||
async function handleSelectionChange(selection: any) {
|
||||
selectIds.value = selection.map((item: any) => item.id);
|
||||
selectionRows.value = selection;
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
async function handleCloseDialog() {
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
async function handleOpenDialog(type: "create" | "update" | "detail", id?: number) {
|
||||
dialogVisible.type = type;
|
||||
if (id) {
|
||||
const response = await {{ class_name }}API.detail{{ class_name }}(id);
|
||||
if (type === "detail") {
|
||||
dialogVisible.title = "详情";
|
||||
Object.assign(detailFormData.value, response.data.data);
|
||||
} else if (type === "update") {
|
||||
dialogVisible.title = "修改";
|
||||
Object.assign(formData, response.data.data);
|
||||
}
|
||||
} else {
|
||||
dialogVisible.title = "新增{{ class_name }}";
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert == 1 or column.is_edit == 1 %}
|
||||
{% if column.column_name not in ['uuid', 'created_time', 'updated_time', 'created_id', 'updated_id'] %}
|
||||
formData.{{ column.column_name }} = undefined;
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
// 提交表单(防抖)
|
||||
async function handleSubmit() {
|
||||
// 表单校验
|
||||
dataFormRef.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
|
||||
const id = formData.id;
|
||||
if (id) {
|
||||
try {
|
||||
await {{ class_name }}API.update{{ class_name }}(id, { id, ...formData });
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleCloseDialog();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await {{ class_name }}API.create{{ class_name }}(formData);
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleCloseDialog();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids: number[]) {
|
||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await {{ class_name }}API.delete{{ class_name }}(ids);
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
// 批量启用/停用
|
||||
async function handleMoreClick(status: string) {
|
||||
if (selectIds.value.length) {
|
||||
ElMessageBox.confirm(`确认${status === "0" ? "启用" : "停用"}该项数据?`, "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await {{ class_name }}API.batch{{ class_name }}({ ids: selectIds.value, status });
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 处理上传
|
||||
const handleUpload = async (formData: FormData) => {
|
||||
try {
|
||||
const response = await {{ class_name }}API.import{{ class_name }}(formData);
|
||||
if (response.data.code === ResultEnum.SUCCESS) {
|
||||
ElMessage.success(`${response.data.msg},${response.data.data}`);
|
||||
importDialogVisible.value = false;
|
||||
await handleQuery();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// 预加载字典数据
|
||||
if (dictTypes.length > 0) {
|
||||
await dictStore.getDict(dictTypes)
|
||||
}
|
||||
loadingData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import re
|
||||
|
||||
from app.common.constant import GenConstant
|
||||
from app.utils.string_util import StringUtil
|
||||
|
||||
from app.api.v1.module_generator.gencode.schema import GenTableOutSchema, GenTableSchema, GenTableColumnSchema
|
||||
|
||||
|
||||
class GenUtils:
|
||||
"""代码生成器工具类"""
|
||||
|
||||
@classmethod
|
||||
def init_table(cls, gen_table: GenTableSchema) -> None:
|
||||
"""
|
||||
初始化表信息
|
||||
|
||||
参数:
|
||||
- gen_table (GenTableSchema): 业务表对象。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
# 只有当字段为None时才设置默认值
|
||||
gen_table.class_name = cls.convert_class_name(gen_table.table_name or "")
|
||||
gen_table.package_name = 'gencode'
|
||||
gen_table.module_name = f'module_{gen_table.package_name}'
|
||||
gen_table.business_name = gen_table.table_name
|
||||
gen_table.function_name = re.sub(r'(?:表|测试)', '', gen_table.table_comment or "")
|
||||
|
||||
@classmethod
|
||||
def init_column_field(cls, column: GenTableColumnSchema, table: GenTableOutSchema) -> None:
|
||||
"""
|
||||
初始化列属性字段
|
||||
|
||||
参数:
|
||||
- column (GenTableColumnSchema): 业务表字段对象。
|
||||
- table (GenTableOutSchema): 业务表对象。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
data_type = cls.get_db_type(column.column_type or "")
|
||||
column_name = column.column_name or ""
|
||||
if not table.id:
|
||||
raise ValueError("业务表ID不能为空")
|
||||
column.table_id = table.id
|
||||
column.python_field = cls.to_camel_case(column_name)
|
||||
# 只有当python_type为None时才设置默认类型
|
||||
column.python_type = StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_PYTHON, data_type)
|
||||
|
||||
if column.column_length is None:
|
||||
column.column_length = ''
|
||||
|
||||
if column.column_default is None:
|
||||
column.column_default = ''
|
||||
|
||||
if column.html_type is None:
|
||||
if cls.arrays_contains(GenConstant.COLUMNTYPE_STR, data_type) or cls.arrays_contains(
|
||||
GenConstant.COLUMNTYPE_TEXT, data_type
|
||||
):
|
||||
# 字符串长度超过500设置为文本域
|
||||
column_length = cls.get_column_length(column.column_type or "")
|
||||
html_type = (
|
||||
GenConstant.HTML_TEXTAREA
|
||||
if column_length >= 500 or cls.arrays_contains(GenConstant.COLUMNTYPE_TEXT, data_type)
|
||||
else GenConstant.HTML_INPUT
|
||||
)
|
||||
column.html_type = html_type
|
||||
elif cls.arrays_contains(GenConstant.COLUMNTYPE_TIME, data_type):
|
||||
column.html_type = GenConstant.HTML_DATETIME
|
||||
elif cls.arrays_contains(GenConstant.COLUMNTYPE_NUMBER, data_type):
|
||||
column.html_type = GenConstant.HTML_INPUT
|
||||
elif column_name.lower().endswith("status"):
|
||||
column.html_type = GenConstant.HTML_RADIO
|
||||
elif column_name.lower().endswith("type") or column_name.lower().endswith("sex"):
|
||||
column.html_type = GenConstant.HTML_SELECT
|
||||
elif column_name.lower().endswith("image"):
|
||||
column.html_type = GenConstant.HTML_IMAGE_UPLOAD
|
||||
elif column_name.lower().endswith("file"):
|
||||
column.html_type = GenConstant.HTML_FILE_UPLOAD
|
||||
elif column_name.lower().endswith("content"):
|
||||
column.html_type = GenConstant.HTML_EDITOR
|
||||
else:
|
||||
column.html_type = GenConstant.HTML_INPUT
|
||||
|
||||
# 只有当is_insert为None时才设置插入字段(默认所有字段都需要插入)
|
||||
if column.is_insert:
|
||||
column.is_insert = GenConstant.REQUIRE
|
||||
else:
|
||||
column.is_insert = False
|
||||
|
||||
# 只有当is_edit为None时才设置编辑字段
|
||||
if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_EDIT, column_name) and not column.is_pk:
|
||||
column.is_edit = GenConstant.REQUIRE
|
||||
else:
|
||||
column.is_edit = False
|
||||
|
||||
# 只有当is_list为None时才设置列表字段
|
||||
if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_LIST, column_name) and not column.is_pk:
|
||||
column.is_list = GenConstant.REQUIRE
|
||||
else:
|
||||
column.is_list = False
|
||||
|
||||
# 只有当is_query为None时才设置查询字段
|
||||
if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_QUERY, column_name) and not column.is_pk:
|
||||
column.is_query = GenConstant.REQUIRE
|
||||
# 直接设置查询类型,因为我们已经确定这是一个查询字段
|
||||
if column_name.lower().endswith('name') or data_type in ['varchar', 'char', 'text']:
|
||||
column.query_type = GenConstant.QUERY_LIKE
|
||||
else:
|
||||
column.query_type = GenConstant.QUERY_EQ
|
||||
else:
|
||||
column.is_query = False
|
||||
column.query_type = None
|
||||
|
||||
@classmethod
|
||||
def arrays_contains(cls, arr, target_value) -> bool:
|
||||
"""
|
||||
检查目标值是否在数组中
|
||||
|
||||
注意:从根本上解决问题,现在确保传入的参数都是正确的类型:
|
||||
- arr 是列表类型,且在GenConstant中定义
|
||||
- target_value 不会是None
|
||||
|
||||
参数:
|
||||
- arr: 数组类型
|
||||
- target_value: 目标值
|
||||
|
||||
返回:
|
||||
- bool: 如果目标值在数组中,返回True;否则返回False
|
||||
"""
|
||||
# 从根本上解决问题,不再需要复杂的防御性检查
|
||||
# 因为现在我们确保传入的arr是GenConstant中定义的列表常量
|
||||
# 并且target_value在调用前已经被处理过不会是None
|
||||
|
||||
# 简单直接地执行包含检查
|
||||
target_str = str(target_value).lower()
|
||||
return any(str(item).lower() == target_str for item in arr)
|
||||
|
||||
@classmethod
|
||||
def convert_class_name(cls, table_name: str) -> str:
|
||||
"""
|
||||
表名转换成 Python 类名
|
||||
|
||||
参数:
|
||||
- table_name (str): 业务表名。
|
||||
|
||||
返回:
|
||||
- str: Python 类名。
|
||||
"""
|
||||
return StringUtil.convert_to_camel_case(table_name)
|
||||
|
||||
@classmethod
|
||||
def replace_first(cls, input_string: str, search_list: list[str]) -> str:
|
||||
"""
|
||||
批量替换前缀
|
||||
|
||||
参数:
|
||||
- input_string (str): 需要被替换的字符串。
|
||||
- search_list (list[str]): 可替换的字符串列表。
|
||||
|
||||
返回:
|
||||
- str: 替换后的字符串。
|
||||
"""
|
||||
for search_string in search_list:
|
||||
if input_string.startswith(search_string):
|
||||
return input_string.replace(search_string, '', 1)
|
||||
return input_string
|
||||
|
||||
@classmethod
|
||||
def get_db_type(cls, column_type: str) -> str:
|
||||
"""
|
||||
获取数据库类型字段
|
||||
|
||||
参数:
|
||||
- column_type (str): 字段类型。
|
||||
|
||||
返回:
|
||||
- str: 数据库类型。
|
||||
"""
|
||||
if '(' in column_type:
|
||||
return column_type.split('(')[0]
|
||||
return column_type
|
||||
|
||||
@classmethod
|
||||
def get_column_length(cls, column_type: str) -> int:
|
||||
"""
|
||||
获取字段长度
|
||||
|
||||
参数:
|
||||
- column_type (str): 字段类型,例如 'varchar(255)' 或 'decimal(10,2)'
|
||||
|
||||
返回:
|
||||
- int: 字段长度(优先取第一个长度值,无法解析时返回0)。
|
||||
"""
|
||||
if '(' in column_type:
|
||||
length = len(column_type.split('(')[1].split(')')[0])
|
||||
return length
|
||||
return 0
|
||||
|
||||
@classmethod
|
||||
def split_column_type(cls, column_type: str) -> list[str]:
|
||||
"""
|
||||
拆分列类型
|
||||
|
||||
参数:
|
||||
- column_type (str): 字段类型。
|
||||
|
||||
返回:
|
||||
- list[str]: 拆分结果。
|
||||
"""
|
||||
if '(' in column_type and ')' in column_type:
|
||||
return column_type.split('(')[1].split(')')[0].split(',')
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def to_camel_case(cls, text: str) -> str:
|
||||
"""
|
||||
将字符串转换为驼峰命名
|
||||
|
||||
参数:
|
||||
- text (str): 需要转换的字符串
|
||||
|
||||
返回:
|
||||
- str: 驼峰命名
|
||||
"""
|
||||
parts = text.split('_')
|
||||
return parts[0] + ''.join(word.capitalize() for word in parts[1:])
|
||||
@@ -0,0 +1,395 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
from jinja2.environment import Environment
|
||||
from jinja2 import Environment, FileSystemLoader, Template
|
||||
from typing import Any
|
||||
|
||||
from app.common.constant import GenConstant
|
||||
from app.config.path_conf import TEMPLATE_DIR
|
||||
from app.config.setting import settings
|
||||
from app.utils.common_util import CamelCaseUtil, SnakeCaseUtil
|
||||
from app.utils.string_util import StringUtil
|
||||
|
||||
from app.api.v1.module_generator.gencode.schema import GenTableOutSchema, GenTableColumnOutSchema
|
||||
|
||||
|
||||
class Jinja2TemplateUtil:
|
||||
"""
|
||||
模板处理工具类
|
||||
"""
|
||||
|
||||
# 项目路径
|
||||
FRONTEND_PROJECT_PATH = 'frontend'
|
||||
BACKEND_PROJECT_PATH = 'backend'
|
||||
# 默认上级菜单,系统工具
|
||||
DEFAULT_PARENT_MENU_ID = 7
|
||||
|
||||
# 环境对象
|
||||
_env = None
|
||||
|
||||
@classmethod
|
||||
def get_env(cls):
|
||||
"""
|
||||
获取模板环境对象。
|
||||
|
||||
参数:
|
||||
- 无
|
||||
|
||||
返回:
|
||||
- Environment: Jinja2 环境对象。
|
||||
"""
|
||||
try:
|
||||
if cls._env is None:
|
||||
cls._env = Environment(
|
||||
loader=FileSystemLoader(TEMPLATE_DIR),
|
||||
autoescape=False, # 自动转义HTML
|
||||
trim_blocks=True, # 删除多余的空行
|
||||
lstrip_blocks=True, # 删除行首空格
|
||||
keep_trailing_newline=True, # 保留行尾换行符
|
||||
enable_async=True, # 开启异步支持
|
||||
)
|
||||
cls._env.filters.update(
|
||||
{
|
||||
'camel_to_snake': SnakeCaseUtil.camel_to_snake,
|
||||
'snake_to_camel': CamelCaseUtil.snake_to_camel,
|
||||
'get_sqlalchemy_type': cls.get_sqlalchemy_type
|
||||
}
|
||||
)
|
||||
return cls._env
|
||||
except Exception as e:
|
||||
raise RuntimeError(f'初始化Jinja2模板引擎失败: {e}')
|
||||
|
||||
@classmethod
|
||||
def get_template(cls, template_path: str) -> Template:
|
||||
"""
|
||||
获取模板。
|
||||
|
||||
参数:
|
||||
- template_path (str): 模板路径。
|
||||
|
||||
返回:
|
||||
- Template: Jinja2 模板对象。
|
||||
|
||||
异常:
|
||||
- TemplateNotFound: 模板未找到时抛出。
|
||||
"""
|
||||
return cls.get_env().get_template(template_path)
|
||||
|
||||
@classmethod
|
||||
def prepare_context(cls, gen_table: GenTableOutSchema) -> dict[str, Any]:
|
||||
"""
|
||||
准备模板变量。
|
||||
|
||||
参数:
|
||||
- gen_table (GenTableOutSchema): 生成表的配置信息。
|
||||
|
||||
返回:
|
||||
- Dict[str, Any]: 模板上下文字典。
|
||||
"""
|
||||
# 处理options为None的情况
|
||||
# if not gen_table.options:
|
||||
# raise ValueError('请先完善生成配置信息')
|
||||
class_name = gen_table.class_name or ''
|
||||
module_name = gen_table.module_name or ''
|
||||
business_name = gen_table.business_name or ''
|
||||
package_name = gen_table.package_name or ''
|
||||
function_name = gen_table.function_name or ''
|
||||
|
||||
context = {
|
||||
'table_name': gen_table.table_name or '',
|
||||
'table_comment': gen_table.table_comment or '',
|
||||
'function_name': function_name if StringUtil.is_not_empty(function_name) else '【请填写功能名称】',
|
||||
'class_name': class_name,
|
||||
'module_name': module_name,
|
||||
'business_name': business_name,
|
||||
'base_package': cls.get_package_prefix(package_name),
|
||||
'package_name': package_name,
|
||||
'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'pk_column': gen_table.pk_column,
|
||||
'model_import_list': cls.get_model_import_list(gen_table),
|
||||
'schema_import_list': cls.get_schema_import_list(gen_table),
|
||||
'permission_prefix': cls.get_permission_prefix(module_name, business_name),
|
||||
'columns': gen_table.columns or [],
|
||||
'table': gen_table,
|
||||
'dicts': cls.get_dicts(gen_table),
|
||||
'db_type': settings.DATABASE_TYPE,
|
||||
'column_not_add_show': GenConstant.COLUMNNAME_NOT_ADD_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),
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
@classmethod
|
||||
def get_template_list(cls):
|
||||
"""
|
||||
获取模板列表。
|
||||
|
||||
参数:
|
||||
- 无
|
||||
返回:
|
||||
- List[str]: 模板路径列表。
|
||||
"""
|
||||
templates = [
|
||||
'python/controller.py.j2',
|
||||
'python/service.py.j2',
|
||||
'python/crud.py.j2',
|
||||
'python/schema.py.j2',
|
||||
'python/model.py.j2',
|
||||
'python/__init__.py.j2',
|
||||
'sql/sql.sql.j2',
|
||||
'ts/api.ts.j2',
|
||||
'vue/index.vue.j2',
|
||||
]
|
||||
return templates
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_file_name(cls, template: str, gen_table: GenTableOutSchema):
|
||||
"""
|
||||
根据模板生成文件名。
|
||||
|
||||
参数:
|
||||
- template (str): 模板路径字符串。
|
||||
- gen_table (GenTableOutSchema): 生成表的配置信息。
|
||||
|
||||
返回:
|
||||
- str: 模板生成的文件名。
|
||||
|
||||
异常:
|
||||
- ValueError: 当无法生成有效文件名时抛出。
|
||||
"""
|
||||
module_name = gen_table.module_name or ''
|
||||
business_name = gen_table.business_name or ''
|
||||
|
||||
# 验证必要的参数
|
||||
if not module_name or not business_name:
|
||||
raise ValueError(f"无法为模板 {template} 生成文件名:模块名或业务名未设置")
|
||||
|
||||
# 映射表方式简化
|
||||
template_mapping = {
|
||||
'controller.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/controller.py',
|
||||
'service.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/service.py',
|
||||
'crud.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/{module_name}/{business_name}/crud.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',
|
||||
'__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',
|
||||
'index.vue.j2': f'{cls.FRONTEND_PROJECT_PATH}/src/views/{module_name}/{business_name}/index.vue'
|
||||
}
|
||||
|
||||
# 查找匹配的模板路径
|
||||
for key, path in template_mapping.items():
|
||||
if key in template:
|
||||
return path
|
||||
|
||||
# 默认处理
|
||||
template_name = template.split('/')[-1].replace('.j2', '')
|
||||
return f'{cls.BACKEND_PROJECT_PATH}/generated/{template_name}'
|
||||
|
||||
@classmethod
|
||||
def get_package_prefix(cls, package_name: str) -> str:
|
||||
"""
|
||||
获取包前缀。
|
||||
|
||||
参数:
|
||||
- package_name (str): 包名。
|
||||
|
||||
返回:
|
||||
- str: 包前缀。
|
||||
"""
|
||||
# 修复:当包名中不存在'.'时,直接返回原包名
|
||||
return package_name[: package_name.rfind('.')] if '.' in package_name else package_name
|
||||
|
||||
@classmethod
|
||||
def get_schema_import_list(cls, gen_table: GenTableOutSchema):
|
||||
"""
|
||||
获取schema模板导入包列表
|
||||
|
||||
:param gen_table: 生成表的配置信息
|
||||
:return: 导入包列表
|
||||
"""
|
||||
columns = gen_table.columns or []
|
||||
import_list = set()
|
||||
for column in columns:
|
||||
if column.python_type in GenConstant.TYPE_DATE:
|
||||
import_list.add(f'from datetime import {column.python_type}')
|
||||
elif column.python_type == GenConstant.TYPE_DECIMAL:
|
||||
import_list.add('from decimal import Decimal')
|
||||
if gen_table.sub:
|
||||
if gen_table.sub_table and gen_table.sub_table.columns:
|
||||
sub_columns = gen_table.sub_table.columns or []
|
||||
for sub_column in sub_columns:
|
||||
if sub_column.python_type in GenConstant.TYPE_DATE:
|
||||
import_list.add(f'from datetime import {sub_column.python_type}')
|
||||
elif sub_column.python_type == GenConstant.TYPE_DECIMAL:
|
||||
import_list.add('from decimal import Decimal')
|
||||
return cls.merge_same_imports(list(import_list), 'from datetime import')
|
||||
|
||||
@classmethod
|
||||
def get_model_import_list(cls, gen_table: GenTableOutSchema):
|
||||
"""
|
||||
获取do模板导入包列表
|
||||
|
||||
:param gen_table: 生成表的配置信息
|
||||
:return: 导入包列表
|
||||
"""
|
||||
columns = gen_table.columns or []
|
||||
import_list = set()
|
||||
|
||||
for column in columns:
|
||||
if column.column_type:
|
||||
data_type = cls.get_db_type(column.column_type)
|
||||
if data_type in GenConstant.COLUMNTYPE_GEOMETRY:
|
||||
import_list.add('from geoalchemy2 import Geometry')
|
||||
import_list.add(
|
||||
f'from sqlalchemy import {StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY, data_type)}'
|
||||
)
|
||||
if gen_table.sub:
|
||||
import_list.add('from sqlalchemy import ForeignKey')
|
||||
if gen_table.sub_table and gen_table.sub_table.columns:
|
||||
sub_columns = gen_table.sub_table.columns or []
|
||||
for sub_column in sub_columns:
|
||||
if sub_column.column_type:
|
||||
data_type = cls.get_db_type(sub_column.column_type)
|
||||
import_list.add(
|
||||
f'from sqlalchemy import {StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY, data_type)}'
|
||||
)
|
||||
return cls.merge_same_imports(list(import_list), 'from sqlalchemy import')
|
||||
|
||||
@classmethod
|
||||
def get_db_type(cls, column_type: str) -> str:
|
||||
"""
|
||||
获取数据库字段类型。
|
||||
|
||||
参数:
|
||||
- column_type (str): 字段类型字符串。
|
||||
|
||||
返回:
|
||||
- str: 数据库类型(去除长度等修饰)。
|
||||
"""
|
||||
if '(' in column_type:
|
||||
return column_type.split('(')[0]
|
||||
return column_type
|
||||
|
||||
@classmethod
|
||||
def merge_same_imports(cls, imports: list[str], import_start: str) -> list[str]:
|
||||
"""
|
||||
合并相同的导入语句。
|
||||
|
||||
参数:
|
||||
- imports (list[str]): 导入语句列表。
|
||||
- import_start (str): 导入语句的起始字符串。
|
||||
|
||||
返回:
|
||||
- list[str]: 合并后的导入语句列表。
|
||||
"""
|
||||
merged_imports = []
|
||||
_imports = []
|
||||
for import_stmt in imports:
|
||||
if import_stmt.startswith(import_start):
|
||||
imported_items = import_stmt.split('import')[1].strip()
|
||||
_imports.extend(imported_items.split(', '))
|
||||
else:
|
||||
merged_imports.append(import_stmt)
|
||||
|
||||
if _imports:
|
||||
merged_datetime_import = f'{import_start} {", ".join(_imports)}'
|
||||
merged_imports.append(merged_datetime_import)
|
||||
|
||||
return merged_imports
|
||||
|
||||
@classmethod
|
||||
def get_dicts(cls, gen_table: GenTableOutSchema):
|
||||
"""
|
||||
获取字典列表。
|
||||
|
||||
参数:
|
||||
- gen_table (GenTableOutSchema): 生成表的配置信息。
|
||||
|
||||
返回:
|
||||
- str: 以逗号分隔的字典类型字符串。
|
||||
"""
|
||||
columns = gen_table.columns or []
|
||||
dicts = set()
|
||||
cls.add_dicts(dicts, columns)
|
||||
# 处理sub_table为None的情况
|
||||
if gen_table.sub_table is not None:
|
||||
# 处理sub_table.columns为None的情况
|
||||
sub_columns = gen_table.sub_table.columns or []
|
||||
cls.add_dicts(dicts, sub_columns)
|
||||
return ', '.join(dicts)
|
||||
|
||||
@classmethod
|
||||
def add_dicts(cls, dicts: set[str], columns: list[GenTableColumnOutSchema]):
|
||||
"""
|
||||
添加字典类型到集合。
|
||||
|
||||
参数:
|
||||
- dicts (set[str]): 字典类型集合。
|
||||
- columns (list[GenTableColumnOutSchema]): 字段列表。
|
||||
|
||||
返回:
|
||||
- set[str]: 更新后的字典类型集合。
|
||||
"""
|
||||
for column in columns:
|
||||
super_column = column.super_column if column.super_column is not None else '0'
|
||||
dict_type = column.dict_type or ''
|
||||
html_type = column.html_type or ''
|
||||
|
||||
if (
|
||||
not super_column
|
||||
and StringUtil.is_not_empty(dict_type)
|
||||
and StringUtil.equals_any_ignore_case(
|
||||
html_type, [GenConstant.HTML_SELECT, GenConstant.HTML_RADIO, GenConstant.HTML_CHECKBOX]
|
||||
)
|
||||
):
|
||||
dicts.add(f"'{dict_type}'")
|
||||
|
||||
@classmethod
|
||||
def get_permission_prefix(cls, module_name: str | None, business_name: str | None) -> str:
|
||||
"""
|
||||
获取权限前缀。
|
||||
|
||||
参数:
|
||||
- module_name (str | None): 模块名。
|
||||
- business_name (str | None): 业务名。
|
||||
|
||||
返回:
|
||||
- str: 权限前缀字符串。
|
||||
"""
|
||||
return f'{module_name}:{business_name}'
|
||||
|
||||
@classmethod
|
||||
def get_sqlalchemy_type(cls, column):
|
||||
"""
|
||||
获取 SQLAlchemy 类型。
|
||||
|
||||
参数:
|
||||
- column_type (Any): 列类型或包含 `column_type` 属性的对象。
|
||||
|
||||
返回:
|
||||
- str: SQLAlchemy 类型字符串。
|
||||
"""
|
||||
if '(' in column:
|
||||
column_type_list = column.split('(')
|
||||
if column_type_list[0] in GenConstant.COLUMNTYPE_STR:
|
||||
sqlalchemy_type = (
|
||||
StringUtil.get_mapping_value_by_key_ignore_case(
|
||||
GenConstant.DB_TO_SQLALCHEMY, column_type_list[0]
|
||||
)
|
||||
+ '('
|
||||
+ column_type_list[1]
|
||||
)
|
||||
else:
|
||||
sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case(
|
||||
GenConstant.DB_TO_SQLALCHEMY, column_type_list[0]
|
||||
)
|
||||
else:
|
||||
sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case(
|
||||
GenConstant.DB_TO_SQLALCHEMY, column
|
||||
)
|
||||
|
||||
return sqlalchemy_type
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
2
后端源码/yifan.action-ai.cn/api-bak/app/api/v1/module_monitor/cache/__init__.py
vendored
Normal file
2
后端源码/yifan.action-ai.cn/api-bak/app/api/v1/module_monitor/cache/__init__.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
178
后端源码/yifan.action-ai.cn/api-bak/app/api/v1/module_monitor/cache/controller.py
vendored
Normal file
178
后端源码/yifan.action-ai.cn/api-bak/app/api/v1/module_monitor/cache/controller.py
vendored
Normal file
@@ -0,0 +1,178 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from redis.asyncio.client import Redis
|
||||
|
||||
from app.common.response import SuccessResponse
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.dependencies import AuthPermission, redis_getter
|
||||
from app.core.logger import log
|
||||
from app.core.router_class import OperationLogRoute
|
||||
|
||||
from .service import CacheService
|
||||
|
||||
|
||||
CacheRouter = APIRouter(route_class=OperationLogRoute, prefix="/cache", tags=["缓存监控"])
|
||||
|
||||
|
||||
@CacheRouter.get(
|
||||
'/info',
|
||||
dependencies=[Depends(AuthPermission(['module_monitor:cache:query']))],
|
||||
summary="获取缓存监控信息",
|
||||
description="获取缓存监控信息"
|
||||
)
|
||||
async def get_monitor_cache_info_controller(
|
||||
redis: Redis = Depends(redis_getter)
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取缓存监控统计信息
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含缓存监控统计信息的JSON响应
|
||||
"""
|
||||
result = await CacheService.get_cache_monitor_statistical_info_service(redis=redis)
|
||||
log.info('获取缓存监控信息成功')
|
||||
return SuccessResponse(data=result, msg='获取缓存监控信息成功')
|
||||
|
||||
|
||||
@CacheRouter.get(
|
||||
'/get/names',
|
||||
dependencies=[Depends(AuthPermission(['module_monitor:cache:query']))],
|
||||
summary="获取缓存名称列表",
|
||||
description="获取缓存名称列表"
|
||||
)
|
||||
async def get_monitor_cache_name_controller() -> JSONResponse:
|
||||
"""
|
||||
获取缓存名称列表
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含缓存名称列表的JSON响应
|
||||
"""
|
||||
result = await CacheService.get_cache_monitor_cache_name_service()
|
||||
log.info('获取缓存名称列表成功')
|
||||
return SuccessResponse(data=result, msg='获取缓存名称列表成功')
|
||||
|
||||
|
||||
@CacheRouter.get(
|
||||
'/get/keys/{cache_name}',
|
||||
dependencies=[Depends(AuthPermission(['module_monitor:cache:query']))],
|
||||
summary="获取缓存键名列表",
|
||||
description="获取缓存键名列表"
|
||||
)
|
||||
async def get_monitor_cache_key_controller(
|
||||
cache_name: str,
|
||||
redis: Redis = Depends(redis_getter)
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取指定缓存名称下的键名列表
|
||||
|
||||
参数:
|
||||
- cache_name (str): 缓存名称
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含缓存键名列表的JSON响应
|
||||
"""
|
||||
result = await CacheService.get_cache_monitor_cache_key_service(redis=redis, cache_name=cache_name)
|
||||
log.info(f'获取缓存{cache_name}的键名列表成功')
|
||||
return SuccessResponse(data=result, msg=f'获取缓存{cache_name}的键名列表成功')
|
||||
|
||||
|
||||
@CacheRouter.get(
|
||||
'/get/value/{cache_name}/{cache_key}',
|
||||
dependencies=[Depends(AuthPermission(['module_monitor:cache:query']))],
|
||||
summary="获取缓存值",
|
||||
description="获取缓存值"
|
||||
)
|
||||
async def get_monitor_cache_value_controller(
|
||||
cache_name: str,
|
||||
cache_key: str,
|
||||
redis: Redis = Depends(redis_getter)
|
||||
)-> JSONResponse:
|
||||
"""
|
||||
获取指定缓存键的值
|
||||
|
||||
参数:
|
||||
- cache_name (str): 缓存名称
|
||||
- cache_key (str): 缓存键
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含缓存值的JSON响应
|
||||
"""
|
||||
result = await CacheService.get_cache_monitor_cache_value_service(redis=redis, cache_name=cache_name, cache_key=cache_key)
|
||||
log.info(f'获取缓存{cache_name}:{cache_key}的值成功')
|
||||
return SuccessResponse(data=result, msg=f'获取缓存{cache_name}:{cache_key}的值成功')
|
||||
|
||||
|
||||
@CacheRouter.delete(
|
||||
'/delete/name/{cache_name}',
|
||||
dependencies=[Depends(AuthPermission(['module_monitor:cache:delete']))],
|
||||
summary="清除指定缓存名称的所有缓存",
|
||||
description="清除指定缓存名称的所有缓存"
|
||||
)
|
||||
async def clear_monitor_cache_name_controller(
|
||||
cache_name: str,
|
||||
redis: Redis = Depends(redis_getter)
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
清除指定缓存名称下的所有缓存
|
||||
|
||||
参数:
|
||||
- cache_name (str): 缓存名称
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含清除结果的JSON响应
|
||||
"""
|
||||
result = await CacheService.clear_cache_monitor_cache_name_service(redis=redis, cache_name=cache_name)
|
||||
if not result:
|
||||
raise CustomException(msg='清除缓存失败', data=result)
|
||||
log.info(f'清除缓存{cache_name}成功')
|
||||
return SuccessResponse(msg=f'{cache_name}对应键值清除成功', data=result)
|
||||
|
||||
|
||||
@CacheRouter.delete(
|
||||
'/delete/key/{cache_key}',
|
||||
dependencies=[Depends(AuthPermission(['module_monitor:cache:delete']))],
|
||||
summary="清除指定缓存键",
|
||||
description="清除指定缓存键"
|
||||
)
|
||||
async def clear_monitor_cache_key_controller(
|
||||
cache_key: str,
|
||||
redis: Redis = Depends(redis_getter)
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
清除指定缓存键
|
||||
|
||||
参数:
|
||||
- cache_key (str): 缓存键
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含清除结果的JSON响应
|
||||
"""
|
||||
result = await CacheService.clear_cache_monitor_cache_key_service(redis=redis, cache_key=cache_key)
|
||||
if not result:
|
||||
raise CustomException(msg='清除缓存失败', data=result)
|
||||
log.info(f'清除缓存键{cache_key}成功')
|
||||
return SuccessResponse(msg=f'{cache_key}清除成功', data=result)
|
||||
|
||||
|
||||
@CacheRouter.delete(
|
||||
'/delete/all',
|
||||
dependencies=[Depends(AuthPermission(['module_monitor:cache:delete']))],
|
||||
summary="清除所有缓存",
|
||||
description="清除所有缓存"
|
||||
)
|
||||
async def clear_monitor_cache_all_controller(
|
||||
redis: Redis = Depends(redis_getter)
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
清除所有缓存
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含清除结果的JSON响应
|
||||
"""
|
||||
result = await CacheService.clear_cache_monitor_all_service(redis=redis)
|
||||
if not result:
|
||||
raise CustomException(msg='清除缓存失败', data=result)
|
||||
log.info('清除所有缓存成功')
|
||||
return SuccessResponse(msg='所有缓存清除成功', data=result)
|
||||
23
后端源码/yifan.action-ai.cn/api-bak/app/api/v1/module_monitor/cache/schema.py
vendored
Normal file
23
后端源码/yifan.action-ai.cn/api-bak/app/api/v1/module_monitor/cache/schema.py
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Any
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class CacheMonitorSchema(BaseModel):
|
||||
"""缓存监控信息模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
command_stats: list[dict] = Field(default_factory=list, description='Redis命令统计信息')
|
||||
db_size: int = Field(default=0, description='Redis数据库中的Key总数')
|
||||
info: dict = Field(default_factory=dict, description='Redis服务器信息')
|
||||
|
||||
|
||||
class CacheInfoSchema(BaseModel):
|
||||
"""缓存对象信息模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
cache_key: str = Field(..., description='缓存键名')
|
||||
cache_name: str = Field(..., description='缓存名称')
|
||||
cache_value: Any = Field(default=None, description='缓存值')
|
||||
remark: str | None = Field(default=None, description='备注说明')
|
||||
147
后端源码/yifan.action-ai.cn/api-bak/app/api/v1/module_monitor/cache/service.py
vendored
Normal file
147
后端源码/yifan.action-ai.cn/api-bak/app/api/v1/module_monitor/cache/service.py
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from redis.asyncio.client import Redis
|
||||
|
||||
from app.common.enums import RedisInitKeyConfig
|
||||
from app.core.redis_crud import RedisCURD
|
||||
|
||||
from .schema import CacheMonitorSchema, CacheInfoSchema
|
||||
|
||||
|
||||
class CacheService:
|
||||
"""
|
||||
缓存监控模块服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_cache_monitor_statistical_info_service(cls, redis: Redis)->dict:
|
||||
"""
|
||||
获取缓存监控信息。
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis 对象。
|
||||
|
||||
返回:
|
||||
- dict: 缓存监控信息字典。
|
||||
"""
|
||||
info = await RedisCURD(redis).info()
|
||||
db_size = await RedisCURD(redis).db_size()
|
||||
command_stats_dict = await RedisCURD(redis).commandstats()
|
||||
|
||||
command_stats = [
|
||||
dict(name=key.split('_')[1], value=str(value.get('calls'))) for key, value in command_stats_dict.items()
|
||||
]
|
||||
result = CacheMonitorSchema(command_stats=command_stats, db_size=db_size, info=info)
|
||||
|
||||
return result.model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_cache_monitor_cache_name_service(cls)->list:
|
||||
"""
|
||||
获取缓存名称列表信息。
|
||||
|
||||
返回:
|
||||
- list: 缓存名称列表信息。
|
||||
"""
|
||||
name_list = []
|
||||
for key_config in RedisInitKeyConfig:
|
||||
name_list.append(
|
||||
CacheInfoSchema(
|
||||
cache_key='',
|
||||
cache_name=key_config.key,
|
||||
cache_value='',
|
||||
remark=key_config.remark,
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
return name_list
|
||||
|
||||
@classmethod
|
||||
async def get_cache_monitor_cache_key_service(cls, redis: Redis, cache_name: str)->list:
|
||||
"""
|
||||
获取缓存键名列表信息。
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis 对象。
|
||||
- cache_name (str): 缓存名称。
|
||||
|
||||
返回:
|
||||
- list: 缓存键名列表信息。
|
||||
"""
|
||||
cache_keys = await RedisCURD(redis).get_keys(f'{cache_name}*')
|
||||
cache_key_list = [key.split(':', 1)[1] for key in cache_keys if key.startswith(f'{cache_name}:')]
|
||||
|
||||
return cache_key_list
|
||||
|
||||
@classmethod
|
||||
async def get_cache_monitor_cache_value_service(cls, redis: Redis, cache_name: str, cache_key: str)->dict:
|
||||
"""
|
||||
获取缓存内容信息。
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis 对象。
|
||||
- cache_name (str): 缓存名称。
|
||||
- cache_key (str): 缓存键名。
|
||||
|
||||
返回:
|
||||
- dict: 缓存内容信息字典。
|
||||
"""
|
||||
cache_value = await RedisCURD(redis).get(f'{cache_name}:{cache_key}')
|
||||
|
||||
return CacheInfoSchema(cache_key=cache_key, cache_name=cache_name, cache_value=cache_value, remark='').model_dump()
|
||||
|
||||
@classmethod
|
||||
async def clear_cache_monitor_cache_name_service(cls, redis: Redis, cache_name: str)->bool:
|
||||
"""
|
||||
清除指定缓存名称对应的所有键值。
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis 对象。
|
||||
- cache_name (str): 缓存名称。
|
||||
|
||||
返回:
|
||||
- bool: 是否清理成功。
|
||||
"""
|
||||
cache_keys = await RedisCURD(redis).get_keys(f'{cache_name}*')
|
||||
if cache_keys:
|
||||
await RedisCURD(redis).delete(*cache_keys)
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def clear_cache_monitor_cache_key_service(cls, redis: Redis, cache_key: str)->bool:
|
||||
"""
|
||||
清除匹配指定键名的所有键值。
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis 对象。
|
||||
- cache_key (str): 缓存键名。
|
||||
|
||||
返回:
|
||||
- bool: 是否清理成功。
|
||||
"""
|
||||
cache_keys = await RedisCURD(redis).get_keys(f'*{cache_key}')
|
||||
if cache_keys:
|
||||
await RedisCURD(redis).delete(*cache_keys)
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def clear_cache_monitor_all_service(cls, redis: Redis)->bool:
|
||||
"""
|
||||
清除所有缓存。
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis 对象。
|
||||
|
||||
返回:
|
||||
- bool: 是否清理成功。
|
||||
"""
|
||||
cache_keys = await RedisCURD(redis).get_keys()
|
||||
if cache_keys:
|
||||
await RedisCURD(redis).delete(*cache_keys)
|
||||
|
||||
return True
|
||||
|
||||
# 避免清除所有的缓存,而采用上面的方式,只清除本系统内指定的所有缓存
|
||||
# return await RedisCURD(redis).clear()
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Body, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from redis.asyncio.client import Redis
|
||||
|
||||
from app.common.request import PaginationService
|
||||
from app.common.response import SuccessResponse,ErrorResponse
|
||||
from app.core.dependencies import AuthPermission, redis_getter
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.core.logger import log
|
||||
|
||||
from .schema import OnlineQueryParam
|
||||
from .service import OnlineService
|
||||
|
||||
|
||||
OnlineRouter = APIRouter(route_class=OperationLogRoute, prefix="/online", tags=["在线用户"])
|
||||
|
||||
|
||||
@OnlineRouter.get(
|
||||
'/list',
|
||||
dependencies=[Depends(AuthPermission(['module_monitor:online:query']))],
|
||||
summary="获取在线用户列表",
|
||||
description="获取在线用户列表"
|
||||
)
|
||||
async def get_online_list_controller(
|
||||
redis: Redis = Depends(redis_getter),
|
||||
paging_query: PaginationQueryParam = Depends(),
|
||||
search: OnlineQueryParam = Depends()
|
||||
)->JSONResponse:
|
||||
"""
|
||||
获取在线用户列表
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis异步客户端实例。
|
||||
- paging_query (PaginationQueryParam): 分页查询参数模型。
|
||||
- search (OnlineQueryParam): 查询参数模型。
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含在线用户列表的JSON响应。
|
||||
"""
|
||||
result_dict_list = await OnlineService.get_online_list_service(redis=redis, search=search)
|
||||
result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= paging_query.page_no, page_size = paging_query.page_size)
|
||||
log.info('获取成功')
|
||||
|
||||
return SuccessResponse(data=result_dict,msg='获取成功')
|
||||
|
||||
|
||||
@OnlineRouter.delete(
|
||||
'/delete',
|
||||
dependencies=[Depends(AuthPermission(['module_monitor:online:delete']))],
|
||||
summary="强制下线",
|
||||
description="强制下线"
|
||||
)
|
||||
async def delete_online_controller(
|
||||
session_id: str = Body(..., description="会话编号"),
|
||||
redis: Redis = Depends(redis_getter),
|
||||
)->JSONResponse:
|
||||
"""
|
||||
强制下线指定在线用户
|
||||
|
||||
参数:
|
||||
- session_id (str): 在线用户会话ID。
|
||||
- redis (Redis): Redis异步客户端实例。
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含操作结果的JSON响应。
|
||||
"""
|
||||
is_ok = await OnlineService.delete_online_service(redis=redis, session_id=session_id)
|
||||
if is_ok:
|
||||
log.info("强制下线成功")
|
||||
return SuccessResponse(msg="强制下线成功")
|
||||
else:
|
||||
log.info("强制下线失败")
|
||||
return ErrorResponse(msg="强制下线失败")
|
||||
|
||||
@OnlineRouter.delete(
|
||||
'/clear',
|
||||
dependencies=[Depends(AuthPermission(['module_monitor:online:delete']))],
|
||||
summary="清除所有在线用户",
|
||||
description="清除所有在线用户"
|
||||
)
|
||||
async def clear_online_controller(
|
||||
redis: Redis = Depends(redis_getter),
|
||||
)->JSONResponse:
|
||||
"""
|
||||
清除所有在线用户
|
||||
|
||||
参数:
|
||||
- redis (Redis): Redis异步客户端实例。
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含操作结果的JSON响应。
|
||||
"""
|
||||
is_ok = await OnlineService.clear_online_service(redis=redis)
|
||||
if is_ok:
|
||||
log.info("清除所有在线用户成功")
|
||||
return SuccessResponse(msg="清除所有在线用户成功")
|
||||
else:
|
||||
log.info("清除所有在线用户失败")
|
||||
return ErrorResponse(msg="清除所有在线用户失败")
|
||||
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
|
||||
|
||||
class OnlineOutSchema(BaseModel):
|
||||
"""
|
||||
在线用户对应pydantic模型
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
name: str = Field(..., description='用户名称')
|
||||
session_id: str = Field(..., description='会话编号')
|
||||
user_id: int = Field(..., description='用户ID')
|
||||
user_name: str = Field(..., description='用户名')
|
||||
ipaddr: str | None = Field(default=None, description='登陆IP地址')
|
||||
login_location: str | None = Field(default=None, description='登录所属地')
|
||||
os: str | None = Field(default=None, description='操作系统')
|
||||
browser: str | None = Field(default=None, description='浏览器')
|
||||
login_time: DateTimeStr | None = Field(default=None, description='登录时间')
|
||||
login_type: str | None = Field(default=None, description='登录类型 PC端 | 移动端')
|
||||
|
||||
|
||||
class OnlineQueryParam:
|
||||
"""在线用户查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = Query(None, description="登录名称"),
|
||||
ipaddr: str | None = Query(None, description="登陆IP地址"),
|
||||
login_location: str | None = Query(None, description="登录所属地"),
|
||||
) -> None:
|
||||
|
||||
# 模糊查询字段
|
||||
self.name = ("like", f"%{name}%") if name else None
|
||||
self.login_location = ("like", f"%{login_location}%") if login_location else None
|
||||
self.ipaddr = ("like", f"%{ipaddr}%") if ipaddr else None
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user