Files
FastapiAdmin/backend/app/api/v1/module_generator/gencode/service.py
T
zhangtao 60fe97afc3 refactor(gen): 重构代码生成模块,实现权限认证和分页支持
- 重构GenTableDao和GenTableColumnDao,继承CRUDBase,添加权限鉴权支持
- 优化分页逻辑,支持非分页时返回完整结果集
- 调整查询条件,支持大小写不敏感模糊查询和时间范围过滤
- 统一接口响应格式,返回SuccessResponse或ErrorResponse
- 替换旧的依赖和注解,改用新的认证和日志中间件
- 文档README.en.md和README.md内容格式及示例完善与优化
- alembic/env.py中数据库URL配置优化,增加异常检测保障环境配置正确
- 代码生成控制器genController新增代码批量生成下载流和本地生成文件覆盖检测
- 删除或更新废弃代码,清理无用导入,提升项目整体代码质量和可维护性
2025-09-20 01:52:23 +08:00

592 lines
25 KiB
Python

# -*- coding:utf-8 -*-
import io
import json
import os
import zipfile
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Any, List, Dict, Optional, Sequence
from app.config.setting import settings
from app.core.exceptions import CustomException
from app.utils.common_util import CamelCaseUtil
from app.utils.gen_util import GenUtils
from app.utils.template_util import TemplateInitializer, TemplateUtils
from app.common.constant import GenConstant
from app.common.response import SuccessResponse
from app.api.v1.module_system.user.schema import UserOutSchema
from .schema import (
DeleteGenTableSchema,
EditGenTableSchema,
GenTableColumnSchema,
GenTableSchema,
)
from .param import GenTableQueryParam
from .crud import GenTableColumnDao, GenTableDao
from .model import GenTableModel, GenTableColumnModel
from app.api.v1.module_system.auth.schema import AuthSchema
# 定义默认的GenConfig值
GEN_PATH = "generated_code" # 默认生成路径
class GenTableService:
"""
代码生成业务表服务层
"""
@classmethod
async def get_gen_table_list_services(
cls, auth: AuthSchema, query_object: GenTableQueryParam, is_page: bool = False
):
"""
获取代码生成业务表列表信息service
:param auth: 认证信息
:param query_object: 查询参数对象
:param is_page: 是否开启分页
:return: 代码生成业务列表信息对象
"""
# 确保db是AsyncSession类型
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
gen_table_list_result = await gen_table_dao.get_gen_table_list(auth.db, query_object, is_page)
return gen_table_list_result
@classmethod
async def get_gen_db_table_list_services(
cls, auth: AuthSchema, query_object: GenTableQueryParam, is_page: bool = False
):
"""
获取数据库列表信息service
:param auth: 认证信息
:param query_object: 查询参数对象
:param is_page: 是否开启分页
:return: 数据库列表信息对象
"""
# 确保db是AsyncSession类型
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
gen_db_table_list_result = await gen_table_dao.get_gen_db_table_list(auth.db, query_object, is_page)
return gen_db_table_list_result
@classmethod
async def get_gen_db_table_list_by_name_services(cls, auth: AuthSchema, table_names: List[str]) -> list[GenTableSchema]:
"""
根据表名称组获取数据库列表信息service
:param auth: 认证信息
:param table_names: 表名称组
:return: 数据库列表信息对象
"""
# 确保db是AsyncSession类型
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
gen_db_table_list_result = await gen_table_dao.get_gen_db_table_list_by_names(auth.db, table_names)
return [GenTableSchema(**gen_table) for gen_table in CamelCaseUtil.transform_result(gen_db_table_list_result)]
@classmethod
async def import_gen_table_services(
cls, auth: AuthSchema, gen_table_list: List[GenTableSchema], current_user: UserOutSchema
):
"""
导入表结构service
:param auth: 认证信息
:param gen_table_list: 导入表列表
:param current_user: 当前用户信息对象
:return: 导入结果
"""
try:
# 确保db是AsyncSession类型
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
gen_table_column_dao = GenTableColumnDao(auth=auth)
for table in gen_table_list:
table_name = table.table_name
GenUtils.init_table(table, current_user.username) # 使用username而不是user.user_name
add_gen_table = await gen_table_dao.create(data=table.model_dump())
if add_gen_table:
table.table_id = add_gen_table.table_id
gen_table_columns = await gen_table_column_dao.get_gen_db_table_columns_by_name(auth.db, table_name or "")
for column in [
GenTableColumnSchema(**gen_table_column)
for gen_table_column in CamelCaseUtil.transform_result(gen_table_columns)
]:
GenUtils.init_column_field(column, table)
await gen_table_column_dao.create(data=column.model_dump())
if isinstance(auth.db, AsyncSession):
await auth.db.commit()
return SuccessResponse(msg='导入成功')
except Exception as e:
if isinstance(auth.db, AsyncSession):
try:
await auth.db.rollback()
except:
pass # 忽略回滚错误
raise CustomException(msg=f'导入失败, {str(e)}')
@classmethod
async def edit_gen_table_services(cls, auth: AuthSchema, page_object: EditGenTableSchema) -> Dict[str, Any]:
"""
编辑业务表信息service
:param auth: 认证信息
:param page_object: 编辑业务表对象
:return: 编辑业务表校验结果
"""
# 确保db是AsyncSession类型
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
gen_table_column_dao = GenTableColumnDao(auth=auth)
# 检查必要字段是否存在
if page_object.table_id is None:
raise CustomException(msg='业务表ID不能为空')
edit_gen_table = page_object.model_dump(exclude_unset=True, by_alias=True)
gen_table_info = await cls.get_gen_table_by_id_services(auth, page_object.table_id)
if gen_table_info.table_id:
try:
# 处理params字段,确保不为None
params = edit_gen_table.get('params')
if params is not None:
edit_gen_table['options'] = json.dumps(params)
else:
edit_gen_table['options'] = '{}' # 默认空对象
# 移除params字段,因为options字段已经包含了序列化的params
edit_gen_table.pop('params', None)
await gen_table_dao.update(id=page_object.table_id, data=edit_gen_table)
if page_object.columns:
for gen_table_column in page_object.columns:
gen_table_column.update_by = page_object.update_by
gen_table_column.update_time = datetime.now()
if gen_table_column.column_id is not None:
await gen_table_column_dao.update(
id=gen_table_column.column_id,
data=gen_table_column.model_dump(by_alias=True)
)
if isinstance(auth.db, AsyncSession):
await auth.db.commit()
return {"is_success": True, "message": "更新成功"}
except Exception as e:
if isinstance(auth.db, AsyncSession):
try:
await auth.db.rollback()
except:
pass # 忽略回滚错误
raise CustomException(msg=f'更新失败: {str(e)}')
else:
raise CustomException(msg='业务表不存在')
@classmethod
async def delete_gen_table_services(cls, auth: AuthSchema, page_object: DeleteGenTableSchema) -> SuccessResponse:
"""
删除业务表信息service
:param auth: 认证信息
:param page_object: 删除业务表对象
:return: 删除业务表校验结果
"""
# 确保db是AsyncSession类型
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
gen_table_column_dao = GenTableColumnDao(auth=auth)
if page_object.table_ids:
table_id_list = page_object.table_ids.split(',')
try:
for table_id in table_id_list:
await gen_table_dao.delete(ids=[int(table_id)])
# 删除相关的字段信息
# 这里需要先查询出所有相关的column_id,然后删除
columns = await gen_table_column_dao.get_gen_table_column_list_by_table_id(auth.db, int(table_id))
if columns:
column_ids = [column.column_id for column in columns]
await gen_table_column_dao.delete(ids=column_ids)
if isinstance(auth.db, AsyncSession):
await auth.db.commit()
return SuccessResponse(msg='删除成功')
except Exception as e:
if isinstance(auth.db, AsyncSession):
try:
await auth.db.rollback()
except:
pass # 忽略回滚错误
raise CustomException(msg=f'删除失败: {str(e)}')
else:
raise CustomException(msg='传入业务表id为空')
@classmethod
async def get_gen_table_by_id_services(cls, auth: AuthSchema, table_id: int) -> GenTableSchema:
"""
获取需要生成的业务表详细信息service
:param auth: 认证信息
:param table_id: 需要生成的业务表id
:return: 需要生成的业务表id对应的信息
"""
# 确保db是AsyncSession类型
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
gen_table = await gen_table_dao.get_gen_table_by_id(auth.db, table_id)
if gen_table:
result = await cls.set_table_from_options(GenTableSchema(**CamelCaseUtil.transform_result(gen_table)))
return result
else:
raise CustomException(msg='业务表不存在')
@classmethod
async def get_gen_table_all_services(cls, auth: AuthSchema) -> list[GenTableSchema]:
"""
获取所有业务表信息service
:param auth: 认证信息
:return: 所有业务表信息
"""
# 确保db是AsyncSession类型
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
gen_table_all = await gen_table_dao.get_gen_table_all(auth.db)
result = [GenTableSchema(**gen_table) for gen_table in CamelCaseUtil.transform_result(gen_table_all)]
return result
@classmethod
async def create_table_services(cls, auth: AuthSchema, sql: str, current_user: UserOutSchema) -> SuccessResponse:
"""
创建表结构service
:param auth: 认证信息
:param sql: 建表语句
:param current_user: 当前用户信息对象
:return: 创建表结构结果
"""
# 移除sqlglot相关代码,因为导入失败
raise CustomException(msg='建表功能暂不可用')
@classmethod
async def preview_code_services(cls, auth: AuthSchema, table_id: int) -> dict[Any, Any]:
"""
预览代码service
:param auth: 认证信息
:param table_id: 业务表id
:return: 预览数据列表
"""
gen_table = await cls.get_gen_table_by_id_services(auth, table_id)
await cls.set_sub_table(auth, gen_table)
await cls._set_pk_column(gen_table)
env = TemplateInitializer.init_jinja2()
context = TemplateUtils.prepare_context(gen_table)
template_list = TemplateUtils.get_template_list(
gen_table.tpl_category or "",
gen_table.tpl_web_type or ""
)
preview_code_result = {}
for template in template_list:
render_content = env.get_template(template).render(**context)
preview_code_result[template] = render_content
return preview_code_result
@classmethod
async def generate_code_services(cls, auth: AuthSchema, table_name: str) -> SuccessResponse:
"""
生成代码至指定路径service
:param auth: 认证信息
:param table_name: 业务表名称
:return: 生成代码结果
"""
env = TemplateInitializer.init_jinja2()
render_info = await cls.__get_gen_render_info(auth, table_name)
for template in render_info[0]:
try:
render_content = env.get_template(template).render(**render_info[2])
gen_path = cls.__get_gen_path(render_info[3], template)
if gen_path:
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'渲染模板失败,表名:{render_info[3].table_name},详细错误信息:{str(e)}')
return SuccessResponse(msg='生成代码成功')
@classmethod
async def batch_gen_code_services(cls, auth: AuthSchema, table_names: List[str]) -> bytes:
"""
批量生成代码service
:param auth: 认证信息
:param table_names: 业务表名称组
:return: 下载代码结果
"""
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for table_name in table_names:
env = TemplateInitializer.init_jinja2()
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 = env.get_template(template_file).render(**render_info[2])
zip_file.writestr(output_file, render_content)
zip_data = zip_buffer.getvalue()
zip_buffer.close()
return zip_data
@classmethod
async def __get_gen_render_info(cls, auth: AuthSchema, table_name: str) -> list[Any]:
"""
获取生成代码渲染模板相关信息
:param auth: 认证信息
:param table_name: 业务表名称
:return: 生成代码渲染模板相关信息
"""
# 确保db是AsyncSession类型
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
gen_table = await gen_table_dao.get_gen_table_by_name(auth.db, table_name)
if gen_table:
gen_table_schema = GenTableSchema(**CamelCaseUtil.transform_result(gen_table))
await cls.set_sub_table(auth, gen_table_schema)
await cls._set_pk_column(gen_table_schema)
context = TemplateUtils.prepare_context(gen_table_schema)
template_list = TemplateUtils.get_template_list(
gen_table_schema.tpl_category or "",
gen_table_schema.tpl_web_type or ""
)
output_files = [TemplateUtils.get_file_name([template], gen_table_schema)[0] for template in template_list]
return [template_list, output_files, context, gen_table_schema]
else:
raise CustomException(msg=f'业务表 {table_name} 不存在')
@classmethod
def __get_gen_path(cls, gen_table: GenTableSchema, template: str) -> Optional[str]:
"""
根据GenTableModel对象和模板名称生成路径
:param gen_table: GenTableModel对象
:param template: 模板名称
:return: 生成的路径
"""
try:
gen_path = gen_table.gen_path or ""
if gen_path == '/':
file_name = TemplateUtils.get_file_name([template], gen_table)[0]
return os.path.join(os.getcwd(), GEN_PATH, file_name)
else:
file_name = TemplateUtils.get_file_name([template], gen_table)[0]
return os.path.join(gen_path, file_name)
except Exception:
return None
@classmethod
async def sync_db_services(cls, auth: AuthSchema, table_name: str) -> SuccessResponse:
"""
同步数据库service
:param auth: 认证信息
:param table_name: 业务表名称
:return: 同步数据库结果
"""
# 确保db是AsyncSession类型
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
gen_table_dao = GenTableDao(auth=auth)
gen_table_column_dao = GenTableColumnDao(auth=auth)
gen_table = await gen_table_dao.get_gen_table_by_name(auth.db, table_name)
if gen_table:
table = GenTableSchema(**CamelCaseUtil.transform_result(gen_table))
table_columns = table.columns or [] # 确保不为None
table_column_map = {column.column_name: column for column in table_columns}
query_db_table_columns = await gen_table_column_dao.get_gen_db_table_columns_by_name(auth.db, table_name)
db_table_columns = [
GenTableColumnSchema(**column) for column in CamelCaseUtil.transform_result(query_db_table_columns)
]
if not db_table_columns:
raise CustomException('同步数据失败,原表结构不存在')
db_table_column_names = [column.column_name for column in db_table_columns]
try:
for column in db_table_columns:
GenUtils.init_column_field(column, table)
if column.column_name in table_column_map:
prev_column = table_column_map[column.column_name]
column.column_id = prev_column.column_id
if getattr(column, 'list', False): # 使用getattr安全访问属性
column.dict_type = prev_column.dict_type
column.query_type = prev_column.query_type
if (
prev_column.is_required != ''
and not column.pk
and (column.insert or column.edit)
and (column.usable_column or column.super_column)
):
column.is_required = prev_column.is_required
column.html_type = prev_column.html_type
if column.column_id is not None:
await gen_table_column_dao.update(id=column.column_id, data=column.model_dump(by_alias=True))
else:
await gen_table_column_dao.create(data=column.model_dump(by_alias=True))
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 column.column_id is not None:
await gen_table_column_dao.delete(ids=[column.column_id])
if isinstance(auth.db, AsyncSession):
await auth.db.commit()
return SuccessResponse(msg='同步成功')
except Exception as e:
if isinstance(auth.db, AsyncSession):
try:
await auth.db.rollback()
except:
pass # 忽略回滚错误
raise CustomException(msg=f'同步失败: {str(e)}')
else:
raise CustomException('业务表不存在')
@classmethod
async def set_sub_table(cls, auth: AuthSchema, gen_table: GenTableSchema) -> None:
"""
设置主子表信息
:param auth: 认证信息
:param gen_table: 业务表信息
:return:
"""
# 确保db是AsyncSession类型
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
if gen_table.sub_table_name:
gen_table_dao = GenTableDao(auth=auth)
sub_table = await gen_table_dao.get_gen_table_by_name(auth.db, gen_table.sub_table_name)
if sub_table:
gen_table.sub_table = GenTableSchema(**CamelCaseUtil.transform_result(sub_table))
@classmethod
async def _set_pk_column(cls, gen_table: GenTableSchema) -> None:
"""
设置主键列信息
:param gen_table: 业务表信息
:return:
"""
if gen_table.columns:
for column in gen_table.columns:
if column.pk:
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]
if gen_table.tpl_category == GenConstant.TPL_SUB and gen_table.sub_table:
if gen_table.sub_table.columns:
for column in gen_table.sub_table.columns:
if column.pk:
gen_table.sub_table.pk_column = column
break
if gen_table.sub_table.pk_column is None and gen_table.sub_table.columns:
gen_table.sub_table.pk_column = gen_table.sub_table.columns[0]
@classmethod
async def set_table_from_options(cls, gen_table: GenTableSchema) -> GenTableSchema:
"""
设置代码生成其他选项值
:param gen_table: 生成对象
:return: 设置后的生成对象
"""
params_obj = json.loads(gen_table.options) if gen_table.options else None
if params_obj:
gen_table.tree_code = params_obj.get(GenConstant.TREE_CODE)
gen_table.tree_parent_code = params_obj.get(GenConstant.TREE_PARENT_CODE)
gen_table.tree_name = params_obj.get(GenConstant.TREE_NAME)
gen_table.parent_menu_id = params_obj.get(GenConstant.PARENT_MENU_ID)
gen_table.parent_menu_name = params_obj.get(GenConstant.PARENT_MENU_NAME)
return gen_table
@classmethod
async def validate_edit(cls, edit_gen_table: EditGenTableSchema):
"""
编辑保存参数校验
:param edit_gen_table: 编辑业务表对象
"""
if edit_gen_table.tpl_category == GenConstant.TPL_TREE:
# 检查params是否为None
if edit_gen_table.params is None:
raise CustomException(msg='树表参数不能为空')
params_obj = edit_gen_table.params.model_dump(by_alias=True)
if GenConstant.TREE_CODE not in params_obj:
raise CustomException(msg='树编码字段不能为空')
elif GenConstant.TREE_PARENT_CODE not in params_obj:
raise CustomException(msg='树父编码字段不能为空')
elif GenConstant.TREE_NAME not in params_obj:
raise CustomException(msg='树名称字段不能为空')
elif edit_gen_table.tpl_category == GenConstant.TPL_SUB:
if not edit_gen_table.sub_table_name:
raise CustomException(msg='关联子表的表名不能为空')
elif not edit_gen_table.sub_table_fk_name:
raise CustomException(msg='子表关联的外键名不能为空')
class GenTableColumnService:
"""
代码生成业务表字段服务层
"""
@classmethod
async def get_gen_table_column_list_by_table_id_services(cls, auth: AuthSchema, table_id: int):
"""
获取业务表字段列表信息service
:param auth: 认证信息
:param table_id: 业务表格id
:return: 业务表字段列表信息对象
"""
# 确保db是AsyncSession类型
if not isinstance(auth.db, AsyncSession):
raise CustomException(msg='数据库会话类型不正确')
gen_table_column_dao = GenTableColumnDao(auth=auth)
gen_table_column_list_result = await gen_table_column_dao.get_gen_table_column_list_by_table_id(auth.db, table_id)
return [
GenTableColumnSchema(**gen_table_column)
for gen_table_column in CamelCaseUtil.transform_result(gen_table_column_list_result)
]