upload project source code

This commit is contained in:
2026-04-30 18:49:43 +08:00
commit 9b394ba682
2277 changed files with 660945 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)

View File

@@ -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:
"""
执行菜单 SQLINSERT / 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

View File

@@ -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'}
)

View File

@@ -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
)

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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
)

View File

@@ -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());
-- 获取父菜单IDMySQL
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' %}
-- 菜单 SQLPostgreSQL 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 %}

View File

@@ -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 %}
}

View File

@@ -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>

View File

@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-

View File

@@ -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:])

View File

@@ -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