refactor(代码生成): 简化代码生成模块配置项和模板逻辑

移除不必要的配置字段如作者、模板类型等,优化字段初始化逻辑
重构模板生成逻辑,移除主子表和树表模板支持
优化前端代码生成页面,简化配置步骤和表单验证
更新文档中的代码生成模块说明和快速开始指南
This commit is contained in:
zhangtao
2025-11-03 01:27:12 +08:00
parent 2c7174db32
commit d02844dd70
18 changed files with 1101 additions and 1300 deletions
@@ -61,7 +61,7 @@ async def get_gen_db_table_list_controller(
返回:
- JSONResponse: 包含查询结果和分页信息的JSON响应
"""
result_dict_list = await GenTableService.get_gen_db_table_list_service(auth=auth, search=search, order_by=page.order_by)
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)
logger.info('获取数据库表列表成功')
return SuccessResponse(data=result_dict, msg="获取数据库表列表成功")
@@ -103,7 +103,6 @@ async def gen_table_detail_controller(
返回:
- JSONResponse: 包含业务表详细信息的JSON响应
"""
# 统一走服务层的聚合逻辑,避免控制器拼装重复代码
gen_table_detail_result = await GenTableService.get_gen_table_detail_service(auth, table_id)
logger.info(f'获取table_id为{table_id}的信息成功')
return SuccessResponse(data=gen_table_detail_result, msg="获取业务表详细信息成功")
@@ -146,7 +145,6 @@ async def update_gen_table_controller(
返回:
- JSONResponse: 包含编辑结果的JSON响应
"""
await GenTableService.validate_edit(data)
result_dict = await GenTableService.update_gen_table_service(auth, data, table_id)
logger.info('编辑业务表信息成功')
return SuccessResponse(data=result_dict, msg="编辑业务表信息成功")
@@ -236,7 +234,7 @@ async def preview_code_controller(
return SuccessResponse(data=preview_code_result, msg="预览代码成功")
@GenRouter.post("/synch_db/{table_name}", summary="同步数据库", description="同步数据库")
@GenRouter.post("/sync_db/{table_name}", summary="同步数据库", description="同步数据库")
async def sync_db_controller(
table_name: str = Path(..., description="表名"),
auth: AuthSchema = Depends(AuthPermission(["generator:db:sync"]))
@@ -253,4 +251,4 @@ async def sync_db_controller(
"""
result = await GenTableService.sync_db_service(auth, table_name)
logger.info(f'同步数据库,表名:{table_name},成功')
return SuccessResponse(msg="同步数据库成功", data=result)
return SuccessResponse(msg="同步数据库成功", data=result)
@@ -15,7 +15,6 @@ from .schema import (
GenTableOutSchema,
GenTableColumnSchema,
GenTableColumnOutSchema,
GenTableColumnDeleteSchema,
GenDBTableSchema,
)
@@ -452,6 +451,7 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
WHERE
table_schema = (SELECT DATABASE())
AND table_name = :table_name
ORDER BY ordinal_position
"""
else:
# 修复SQLite查询语句,使用PRAGMA获取表结构信息
@@ -466,6 +466,7 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
type as column_type
FROM
pragma_table_info(:table_name)
ORDER BY cid
"""
query = text(query_sql).bindparams(table_name=table_name)
@@ -539,13 +540,13 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
if column_ids:
await self.delete(ids=column_ids)
async def delete_gen_table_column_by_column_id_dao(self, data: GenTableColumnDeleteSchema) -> None:
async def delete_gen_table_column_by_column_id_dao(self, column_ids: List[int]) -> None:
"""根据业务表字段ID批量删除业务表字段。
参数:
- data (GenTableColumnDeleteSchema): 业务表字段删除模型
- column_ids (List[int]): 业务表字段ID列表
返回:
- None
"""
return await self.delete(ids=data.column_ids)
return await self.delete(ids=column_ids)
@@ -17,18 +17,12 @@ class GenTableModel(CreatorMixin):
table_name: Mapped[Optional[str]] = mapped_column(String(200), nullable=True, default='', comment='表名称')
table_comment: Mapped[Optional[str]] = mapped_column(String(500), nullable=True, default='', comment='表描述')
sub_table_name: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, comment='关联子表的表名')
sub_table_fk_name: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, comment='子表关联的外键名')
class_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, default='', comment='实体类名称')
tpl_category: Mapped[Optional[str]] = mapped_column(String(200), nullable=True, default='crud', comment='使用的模板(crud单表操作 tree树表操作)')
tpl_web_type: Mapped[Optional[str]] = mapped_column(String(30), nullable=True, default='', comment='前端模板类型(element-ui模版 element-plus模版)')
package_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment='生成包路径')
module_name: Mapped[Optional[str]] = mapped_column(String(30), nullable=True, comment='生成模块名')
business_name: Mapped[Optional[str]] = mapped_column(String(30), nullable=True, comment='生成业务名')
function_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment='生成功能名')
function_author: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment='生成功能作者')
gen_type: Mapped[Optional[str]] = mapped_column(String(1), nullable=True, default='0', comment='生成代码方式(0zip压缩包 1自定义路径)')
gen_path: Mapped[Optional[str]] = mapped_column(String(200), nullable=True, default='/', comment='生成路径(不填默认项目路径)')
gen_type: Mapped[Optional[str]] = mapped_column(String(1), nullable=True, default='0', comment='生成代码方式(0zip压缩包 1生成项目路径)')
options: Mapped[Optional[str]] = mapped_column(String(1000), nullable=True, comment='其它生成选项')
# 关系定义
@@ -1,9 +1,8 @@
# -*- coding:utf-8 -*-
from typing import List, Literal, Optional
from pydantic import BaseModel, ConfigDict, Field, model_validator
from pydantic import BaseModel, ConfigDict, Field
from app.common.constant import GenConstant
from app.core.base_schema import BaseSchema
@@ -16,9 +15,7 @@ class GenTableOptionSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)
parent_menu_id: Optional[int] = Field(default=None, description='所属父级分类')
tree_code: Optional[str] = Field(default=None, description='tree_code')
tree_name: Optional[str] = Field(default=None, description='tree_name')
tree_parent_code: Optional[str] = Field(default=None, description='tree_parent_code')
class GenDBTableSchema(BaseModel):
@@ -37,25 +34,18 @@ class GenDBTableSchema(BaseModel):
class GenTableBaseSchema(BaseModel):
"""代码生成业务表基础模型(创建/更新共享字段)。
- 说明:`params`为前端结构体,后端持久化为`options`的JSON。
- 模板:`tpl_category` 区分 CRUD/Tree/Sub`tpl_web_type` 区分 element-plus 等。
"""
model_config = ConfigDict(from_attributes=True)
table_id: Optional[int] = Field(default=None, description='编号')
table_name: Optional[str] = Field(default=None, description='表名称')
table_name: str= Field(..., description='表名称')
table_comment: Optional[str] = Field(default=None, description='表描述')
sub_table_name: Optional[str] = Field(default=None, description='关联子表的表名')
sub_table_fk_name: Optional[str] = Field(default=None, description='子表关联的外键名')
class_name: Optional[str] = Field(default=None, description='实体类名称')
tpl_category: Optional[str] = Field(default=None, description='使用的模板(crud单表操作 tree树表操作)')
tpl_web_type: Optional[str] = Field(default=None, description='前端模板类型(element-ui模版 element-plus模版)')
package_name: Optional[str] = Field(default=None, description='生成包路径')
module_name: Optional[str] = Field(default=None, description='生成模块名')
business_name: Optional[str] = Field(default=None, description='生成业务名')
function_name: Optional[str] = Field(default=None, description='生成功能名')
function_author: Optional[str] = Field(default=None, description='生成功能作者')
gen_type: Optional[Literal['0', '1']] = Field(default=None, description='生成代码方式(0zip压缩包 1自定义路径)')
gen_path: Optional[str] = Field(default=None, description='生成路径(不填默认项目路径)')
gen_type: Optional[Literal['0', '1']] = Field(default=None, description='生成代码方式(0zip压缩包 1生成项目路径)')
options: Optional[str] = Field(default=None, description='其它生成选项')
description: Optional[str] = Field(default=None, description='功能描述')
@@ -65,27 +55,13 @@ class GenTableBaseSchema(BaseModel):
class GenTableSchema(GenTableBaseSchema):
"""代码生成业务表更新模型(扩展聚合字段)。
- 聚合:`columns`字段包含字段列表;`pk_column`主键字段;子表结构`sub_table`。
- 便捷:`sub/tree/crud`基于`tpl_category`自动推导布尔标记。
"""
pk_column: Optional['GenTableColumnOutSchema'] = Field(default=None, description='主键信息')
sub_table: Optional['GenTableSchema'] = Field(default=None, description='子表信息')
columns: Optional[List['GenTableColumnOutSchema']] = Field(default=None, description='表列信息')
tree_code: Optional[str] = Field(default=None, description='树编码字段tree_code')
tree_parent_code: Optional[str] = Field(default=None, description='树父编码字段')
tree_name: Optional[str] = Field(default=None, description='树名称字段ree_name')
parent_menu_id: Optional[int] = Field(default=None, description='上级菜单ID字段')
parent_menu_name: Optional[str] = Field(default=None, description='上级菜单名称字段')
sub: Optional[bool] = Field(default=None, description='是否为子表')
tree: Optional[bool] = Field(default=None, description='是否为树表')
crud: Optional[bool] = Field(default=None, description='是否为单表')
@model_validator(mode='after')
def check_some_is(self) -> 'GenTableSchema':
self.sub = True if self.tpl_category and self.tpl_category == GenConstant.TPL_SUB else False
self.tree = True if self.tpl_category and self.tpl_category == GenConstant.TPL_TREE else False
self.crud = True if self.tpl_category and self.tpl_category == GenConstant.TPL_CRUD else False
return self
class GenTableOutSchema(GenTableSchema, BaseSchema):
@@ -95,30 +71,8 @@ class GenTableOutSchema(GenTableSchema, BaseSchema):
"""
model_config = ConfigDict(from_attributes=True)
# 添加数据验证和转换的root_validator
@model_validator(mode='before')
def handle_null_values(cls, values):
"""将关键字段的None转换为安全默认值,避免前端渲染异常。"""
# 处理None值,转换为空字符串或适当的默认值
# 检查values是否为对象而非字典
if hasattr(values, '__dict__'):
# 如果是对象,获取其字典表示
values_dict = values.__dict__
for key in ['table_name', 'table_comment', 'class_name', 'columns']:
if key in values_dict and values_dict[key] is None:
if key != 'columns':
setattr(values, key, '')
else:
setattr(values, key, [])
return values
elif isinstance(values, dict):
# 如果是字典,执行原来的逻辑
for key, value in values.items():
if value is None:
if key in ['table_name', 'table_comment', 'class_name', 'columns']:
values[key] = '' if key != 'columns' else []
return values
return values
# 修复:确保columns字段默认为空列表而不是None
columns: Optional[List['GenTableColumnOutSchema']] = Field(default_factory=list, description='表列信息')
class GenTableColumnSchema(BaseModel):
@@ -158,22 +112,13 @@ class GenTableColumnOutSchema(GenTableColumnSchema, BaseSchema):
model_config = ConfigDict(from_attributes=True)
cap_python_field: Optional[str] = Field(default=None, description='字段大写形式')
pk: Optional[bool] = Field(default=None, description='是否主键')
increment: Optional[bool] = Field(default=None, description='是否自增')
required: Optional[bool] = Field(default=None, description='是否必填')
unique: Optional[bool] = Field(default=None, description='是否唯一')
insert: Optional[bool] = Field(default=None, description='是否为插入字段')
edit: Optional[bool] = Field(default=None, description='是否编辑字段')
list: Optional[bool] = Field(default=None, description='是否列表字段')
query: Optional[bool] = Field(default=None, description='是否查询字段')
super_column: Optional[bool] = Field(default=None, description='是否为基类字段')
usable_column: Optional[bool] = Field(default=None, description='是否为基类字段白名单')
class GenTableColumnDeleteSchema(BaseModel):
"""删除代码生成业务表字段模型(批量)。
- 说明:仅包含待删除的字段ID列表。
"""
model_config = ConfigDict(from_attributes=True)
column_ids: List[int] = Field(..., description='需要删除的代码生成业务表字段ID')
pk: Optional[bool] = Field(default=False, description='是否主键')
increment: Optional[bool] = Field(default=False, description='是否自增')
required: Optional[bool] = Field(default=False, description='是否必填')
unique: Optional[bool] = Field(default=False, description='是否唯一')
insert: Optional[bool] = Field(default=False, description='是否为插入字段')
edit: Optional[bool] = Field(default=False, description='是否编辑字段')
list: Optional[bool] = Field(default=False, description='是否列表字段')
query: Optional[bool] = Field(default=False, description='是否查询字段')
super_column: Optional[bool] = Field(default=False, description='是否为基类字段')
usable_column: Optional[bool] = Field(default=False, description='是否为基类字段白名单')
@@ -14,16 +14,28 @@ from app.core.exceptions import CustomException
from app.common.constant import GenConstant
from app.api.v1.module_system.auth.schema import AuthSchema
from app.utils.gen_util import GenUtils
from app.utils.jinja2_template_util import Jinja2TemplateInitializerUtil, Jinja2TemplateUtil
from .schema import GenTableOptionSchema, GenTableSchema, GenTableOutSchema, GenTableColumnSchema, GenTableColumnOutSchema, GenTableColumnDeleteSchema
from app.utils.jinja2_template_util import Jinja2TemplateUtil
from .schema import GenTableOptionSchema, GenTableSchema, GenTableOutSchema, GenTableColumnSchema, GenTableColumnOutSchema
from .param import 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`结构完整
@@ -31,13 +43,18 @@ class GenTableService:
gen_table = await cls.get_gen_table_by_id_service(auth, table_id)
gen_tables = await cls.get_gen_table_all_service(auth)
gen_columns = await GenTableColumnService.get_gen_table_column_list_by_table_id_service(auth, table_id)
# 修复:确保options不为None再解析
if gen_table.options:
table_options = GenTableOptionSchema(**json.loads(gen_table.options))
gen_table.parent_menu_id = table_options.parent_menu_id
try:
table_options = GenTableOptionSchema(**json.loads(gen_table.options))
gen_table.parent_menu_id = table_options.parent_menu_id
except Exception as e:
logger.warning(f"解析表选项时出错: {str(e)}")
gen_table.columns = gen_columns
return dict(info=gen_table, rows=gen_columns, tables=gen_tables)
@classmethod
@handle_service_exception
async def get_gen_table_list_service(cls, auth: AuthSchema, search: GenTableQueryParam) -> List[Dict]:
"""
获取代码生成业务表列表信息
@@ -53,6 +70,7 @@ class GenTableService:
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, order_by: Optional[List[Dict[str, str]]] = None) -> list[Any]:
"""获取数据库表列表(跨方言)。
- 备注返回已转换为字典的结构适用于前端直接展示排序参数保留扩展位但当前未使用
@@ -61,11 +79,16 @@ class GenTableService:
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`列表
"""
gen_db_table_list_result = await GenTableCRUD(auth=auth).get_db_table_list_by_names(table_names)
# 验证输入参数
if not table_names:
raise CustomException(msg="表名列表不能为空")
gen_db_table_list_result = await GenTableCRUD(auth).get_db_table_list_by_names(table_names)
# 检查是否有未找到的表
found_table_names = [table.table_name for table in gen_db_table_list_result]
@@ -76,11 +99,14 @@ class GenTableService:
# 修复:将GenDBTableSchema对象转换为字典后再传递给GenTableOutSchema
result = []
for gen_table in gen_db_table_list_result:
result.append(GenTableOutSchema(**gen_table.model_dump()))
# 确保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]) -> Literal[True] | None:
"""导入表结构到生成器(持久化并初始化列)。
- 备注避免重复导入为每列调用`GenUtils.init_column_field`填充默认属性保留语义一致性
@@ -137,10 +163,15 @@ class GenTableService:
raise CustomException(msg=f'导入失败, {str(e)}')
@classmethod
@handle_service_exception
async def create_table_service(cls, auth: AuthSchema, sql: str) -> Literal[True] | None:
"""创建表结构并导入至代码生成模块。
- 校验使用`sqlglot`确保仅包含`CREATE TABLE`语句失败抛出明确异常
"""
# 验证SQL非空
if not sql or not sql.strip():
raise CustomException(msg='SQL语句不能为空')
try:
sql_statements = sqlglot_parse(sql, dialect=settings.DATABASE_TYPE)
# 校验sql语句是否为合法的建表语句
@@ -200,6 +231,7 @@ class GenTableService:
return 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`的列避免误创建
@@ -229,8 +261,13 @@ class GenTableService:
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_dao(ids)
@@ -240,6 +277,7 @@ class GenTableService:
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`补充选项
@@ -249,20 +287,33 @@ class GenTableService:
raise CustomException(msg='业务表不存在')
result = GenTableOutSchema.model_validate(gen_table)
# 设置额外选项
result = await cls.set_table_from_options(result)
# 确保columns字段为列表,即使为None
if result.columns is None:
result.columns = []
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()
gen_table_all_dict = [GenTableOutSchema.model_validate(gen_table).model_dump() for gen_table in gen_table_all]
result = [GenTableOutSchema(**gen_table) for gen_table in gen_table_all_dict]
result = []
for gen_table in gen_table_all:
try:
# 确保转换为输出模型,并处理可能的None值
table_out = GenTableOutSchema.model_validate(gen_table)
if table_out.columns is None:
table_out.columns = []
result.append(table_out)
except Exception as e:
logger.warning(f"转换业务表时出错: {str(e)}")
continue
return result
@classmethod
@handle_service_exception
async def preview_code_service(cls, auth: AuthSchema, table_id: int) -> Dict[Any, Any]:
"""
预览代码根据模板渲染内存结果
@@ -271,30 +322,36 @@ class GenTableService:
gen_table = GenTableOutSchema.model_validate(
await GenTableCRUD(auth).get_gen_table_by_id(table_id)
)
await cls.set_sub_table(auth, gen_table)
await cls.set_pk_column(gen_table)
env = Jinja2TemplateInitializerUtil.init_jinja2()
env = Jinja2TemplateUtil.get_env()
context = Jinja2TemplateUtil.prepare_context(gen_table)
# 处理tpl_category和tpl_web_type为None的情况
tpl_category = gen_table.tpl_category or ''
tpl_web_type = gen_table.tpl_web_type or 'element-plus'
template_list = Jinja2TemplateUtil.get_template_list(tpl_category, tpl_web_type)
template_list = Jinja2TemplateUtil.get_template_list()
preview_code_result = {}
for template in template_list:
render_content = await env.get_template(template).render_async(**context)
preview_code_result[template] = render_content
try:
render_content = await env.get_template(template).render_async(**context)
preview_code_result[template] = render_content
except Exception as e:
logger.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:
"""生成代码至指定路径(安全写入+可跳过覆盖)。
- 安全限制写入在项目根目录内越界路径自动回退到项目根目录
- 覆盖尊重`settings.allow_overwrite`不允许时跳过写入
"""
# 验证表名非空
if not table_name or not table_name.strip():
raise CustomException(msg='表名不能为空')
if not settings.allow_overwrite:
logger.error('【系统预设】不允许生成文件覆盖到本地')
raise CustomException(msg='【系统预设】不允许生成文件覆盖到本地')
env = Jinja2TemplateInitializerUtil.init_jinja2()
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]:
@@ -304,6 +361,7 @@ class GenTableService:
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:
@@ -313,29 +371,44 @@ class GenTableService:
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:
env = Jinja2TemplateInitializerUtil.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 = await env.get_template(template_file).render_async(**render_info[2])
zip_file.writestr(output_file, render_content)
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:
logger.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='业务表不存在')
@@ -383,26 +456,17 @@ class GenTableService:
else:
await GenTableColumnCRUD(auth).create_gen_table_column_crud(column)
else:
# 设置table_id以确保新字段能正确关联到表
column.table_id = table.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_dao(
GenTableColumnDeleteSchema(column_ids=[column.id])
)
await GenTableColumnCRUD(auth).delete_gen_table_column_by_column_id_dao([column.id])
except Exception as e:
raise CustomException(msg=f'同步失败: {str(e)}')
@classmethod
async def set_sub_table(cls, auth: AuthSchema, gen_table: GenTableOutSchema) -> None:
"""设置主子表信息(如存在子表则补充其结构)。"""
if gen_table.sub_table_name:
gen_table_dao = GenTableCRUD(auth=auth)
sub_table = await gen_table_dao.get_gen_table_by_name(gen_table.sub_table_name)
if sub_table:
gen_table.sub_table = GenTableOutSchema.model_validate(sub_table)
@classmethod
async def set_pk_column(cls, gen_table: GenTableOutSchema) -> None:
"""设置主键列信息(主表/子表)。
@@ -410,59 +474,13 @@ class GenTableService:
"""
if gen_table.columns:
for column in gen_table.columns:
if getattr(column, 'pk', None) or getattr(column, 'is_pk', '') == '1':
# 修复:确保正确检查主键标识
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]
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 getattr(column, 'pk', None) or getattr(column, 'is_pk', '') == '1':
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: GenTableOutSchema) -> GenTableOutSchema:
"""设置代码生成其他选项值(从options反序列化)。"""
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: GenTableSchema) -> None:
"""编辑保存参数校验(树/子表约束)。"""
if edit_gen_table.tpl_category == GenConstant.TPL_TREE:
# 从options字段获取参数,而不是params
if not edit_gen_table.options:
raise CustomException(msg='树表参数不能为空')
# 处理json解析异常
try:
params_obj = json.loads(edit_gen_table.options)
except json.JSONDecodeError:
raise CustomException(msg='树表参数格式不正确')
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='子表关联的外键名不能为空')
@classmethod
async def __get_gen_render_info(cls, auth: AuthSchema, table_name: str) -> List[Any]:
@@ -485,14 +503,9 @@ class GenTableService:
raise CustomException(msg=f"业务表 {table_name} 不存在")
gen_table = GenTableOutSchema.model_validate(gen_table_model)
await cls.set_sub_table(auth, gen_table)
await cls.set_pk_column(gen_table)
context = Jinja2TemplateUtil.prepare_context(gen_table)
template_list = Jinja2TemplateUtil.get_template_list(
gen_table.tpl_category or "",
gen_table.tpl_web_type or ""
)
# 修复:确保get_file_name返回的文件名不为空
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]
@@ -500,25 +513,39 @@ class GenTableService:
@classmethod
def __get_gen_path(cls, gen_table: GenTableOutSchema, template: str) -> Optional[str]:
"""根据GenTableOutSchema对象和模板名称生成路径。"""
gen_path = (gen_table.gen_path or '').strip()
file_name = Jinja2TemplateUtil.get_file_name(template, gen_table)
# 默认写入到项目根目录(backend的上一级)
project_root = str(settings.BASE_DIR.parent)
if gen_path in ['', '/']:
return os.path.join(project_root, file_name)
else:
return os.path.join(gen_path, file_name)
try:
file_name = Jinja2TemplateUtil.get_file_name(template, gen_table)
# 默认写入到项目根目录(backend的上一级)
project_root = str(settings.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)):
logger.warning(f"路径越界,回退到项目根目录: {file_name}")
# 回退到项目根目录下的generated文件夹
full_path = os.path.join(project_root, "generated", os.path.basename(file_name))
return full_path
except Exception as e:
logger.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[GenTableColumnOutSchema]:
"""获取业务表字段列表信息(输出模型)。"""
gen_table_column_list_result = await GenTableColumnCRUD(auth).list_gen_table_column_crud({"table_id": table_id})
return [
GenTableColumnOutSchema.model_validate(gen_table_column)
for gen_table_column in gen_table_column_list_result
]
result = []
for gen_table_column in gen_table_column_list_result:
try:
# 转换为输出模型
column_out = GenTableColumnOutSchema.model_validate(gen_table_column)
result.append(column_out)
except Exception as e:
logger.warning(f"转换字段模型时出错: {str(e)}")
continue
return result