refactor(gencode): 重构代码生成模块,优化字段初始化和类型处理

fix(setting): 确保日志目录存在

perf(logger): 改进日志文件轮转处理,优化异常处理

feat(gencode): 添加子表关联字段支持

style(constant): 更新字段常量命名规范

chore: 移除前端低代码生成器相关文件

fix(gencode): 修复CRUD操作中的字段过滤逻辑

refactor(gen_util): 重构字段初始化逻辑,增强类型安全

docs: 更新代码注释和文档

test: 移除无效测试文件

build: 更新依赖版本

ci: 优化CI配置
This commit is contained in:
zhangtao
2025-11-06 00:31:21 +08:00
parent 451a978d1d
commit 6383c53580
36 changed files with 297 additions and 8123 deletions
+76 -55
View File
@@ -6,7 +6,7 @@ from typing import List
from app.common.constant import GenConstant
from app.config.setting import settings
from app.utils.string_util import StringUtil
from app.api.v1.module_generator.gencode.schema import GenTableSchema, GenTableColumnSchema
from app.api.v1.module_generator.gencode.schema import GenTableOutSchema, GenTableSchema, GenTableColumnSchema
class GenUtils:
@@ -24,46 +24,47 @@ class GenUtils:
- None
"""
# 只有当字段为None时才设置默认值
if gen_table.class_name is None:
gen_table.class_name = cls.convert_class_name(gen_table.table_name or "")
if gen_table.package_name is None:
gen_table.package_name = settings.package_name
if gen_table.module_name is None:
gen_table.module_name = settings.package_name.split('.')[-1]
if gen_table.business_name is None:
gen_table.business_name = gen_table.table_name.split('_')[-1]
if gen_table.function_name is None:
gen_table.function_name = re.sub(r'(?:表|测试)', '', gen_table.table_comment or "")
gen_table.class_name = cls.convert_class_name(gen_table.table_name or "")
gen_table.package_name = settings.package_name
gen_table.module_name = settings.package_name.split('.')[-1]
gen_table.business_name = gen_table.table_name.split('_')[-1]
gen_table.function_name = re.sub(r'(?:表|测试)', '', gen_table.table_comment or "")
@classmethod
def init_column_field(cls, column: GenTableColumnSchema, table: GenTableSchema) -> None:
def init_column_field(cls, column: GenTableColumnSchema, table: GenTableOutSchema) -> None:
"""
初始化列属性字段
参数:
- column (GenTableColumnSchema): 业务表字段对象。
- table (GenTableSchema): 业务表对象。
- table (GenTableOutSchema): 业务表对象。
返回:
- None
"""
data_type = cls.get_db_type(column.column_type or "")
column_name = column.column_name or ""
# 只有当table_id为None时才设置
if column.table_id is None:
column.table_id = table.table_id
# 只有当python_field为None时才设置
if column.python_field is None:
column.python_field = column_name
column.table_id = table.id
column.python_field = cls.to_camel_case(column_name)
# 只有当python_type为None时才设置默认类型
if column.python_type is None:
# 根据数据库类型映射到Python类型(统一使用大写键以兼容MySQL)
column.python_type = GenConstant.DB_TO_PYTHON.get(data_type.upper(), "Any")
column.python_type = StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_PYTHON, data_type)
# 查询类型:优先根据字段语义(如以name结尾走LIKE),否则默认EQ
if column.query_type is None:
column.query_type = GenConstant.QUERY_LIKE if column_name.lower().endswith("name") else GenConstant.QUERY_EQ
column.query_type = GenConstant.QUERY_LIKE
# 确保is_pk等字段为字符串格式
# 将布尔值或其他类型转换为字符串'1'或'0'
if column.is_pk is not None and not isinstance(column.is_pk, str):
column.is_pk = '1' if bool(column.is_pk) else '0'
if column.is_increment is not None and not isinstance(column.is_increment, str):
column.is_increment = '1' if bool(column.is_increment) else '0'
if column.is_required is not None and not isinstance(column.is_required, str):
column.is_required = '1' if bool(column.is_required) else '0'
# 确保None值默认为'0'
column.is_pk = column.is_pk or '0'
column.is_increment = column.is_increment or '0'
column.is_required = column.is_required or '0'
# HTML类型:根据数据库类型设置基础控件,再根据字段语义进行覆写
if column.html_type is None:
if cls.arrays_contains(GenConstant.COLUMNTYPE_STR, data_type) or cls.arrays_contains(
GenConstant.COLUMNTYPE_TEXT, data_type
@@ -96,37 +97,50 @@ class GenUtils:
# 只有当is_insert为None时才设置插入字段(默认所有字段都需要插入)
if column.is_insert is None:
column.is_insert = GenConstant.REQUIRE
else:
# 确保is_insert为字符串格式
column.is_insert = str(column.is_insert) if column.is_insert is not None else '0'
# 只有当is_edit为None时才设置编辑字段
if column.is_edit is None and not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_EDIT, column_name) and not (column.is_pk is not None and column.is_pk == GenConstant.REQUIRE):
column.is_edit = GenConstant.REQUIRE
if column.is_edit is None:
if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_EDIT, column_name) and column.is_pk != '1':
column.is_edit = GenConstant.REQUIRE
else:
column.is_edit = '0'
else:
# 确保is_edit为字符串格式
column.is_edit = str(column.is_edit) if column.is_edit is not None else '0'
# 只有当is_list为None时才设置列表字段
if column.is_list is None and not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_LIST, column_name) and not (column.is_pk is not None and column.is_pk == GenConstant.REQUIRE):
column.is_list = GenConstant.REQUIRE
if column.is_list is None:
if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_LIST, column_name) and column.is_pk != '1':
column.is_list = GenConstant.REQUIRE
else:
column.is_list = '0'
else:
# 确保is_list为字符串格式
column.is_list = str(column.is_list) if column.is_list is not None else '0'
# 只有当is_query为None时才设置查询字段
if column.is_query is None and not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_QUERY, column_name) and not (column.is_pk is not None and column.is_pk == GenConstant.REQUIRE):
column.is_query = GenConstant.REQUIRE
# 只有当query_type为None时才设置查询字段类型
if column.query_type is None:
if column_name.lower().endswith('name'):
column.query_type = GenConstant.QUERY_LIKE
if column.is_query is None:
if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_QUERY, column_name) and column.is_pk != '1':
column.is_query = GenConstant.REQUIRE
else:
column.is_query = '0'
else:
# 确保is_query为字符串格式
column.is_query = str(column.is_query) if column.is_query is not None else '0'
@classmethod
def arrays_contains(cls, arr: List[str], target_value: str) -> bool:
"""
校验数组是否包含指定值(忽略大小写)
校验数组是否包含指定值
参数:
- arr (List[str]): 数组。
- target_value (str): 需要校验的值。
返回:
- bool: 校验结果。
param arr: 数组
param target_value: 需要校验的值
:return: 校验结果
"""
if target_value is None:
return False
return any(item.lower() == target_value.lower() for item in arr)
return target_value in arr
@classmethod
def convert_class_name(cls, table_name: str) -> str:
@@ -189,13 +203,9 @@ class GenUtils:
返回:
- int: 字段长度(优先取第一个长度值,无法解析时返回0)。
"""
if '(' in column_type and ')' in column_type:
try:
inner = column_type.split('(')[1].split(')')[0]
first = inner.split(',')[0].strip()
return int(first)
except Exception:
return 0
if '(' in column_type:
length = len(column_type.split('(')[1].split(')')[0])
return length
return 0
@classmethod
@@ -211,4 +221,15 @@ class GenUtils:
"""
if '(' in column_type and ')' in column_type:
return column_type.split('(')[1].split(')')[0].split(',')
return []
return []
@classmethod
def to_camel_case(cls, text: str) -> str:
"""
将字符串转换为驼峰命名
param text: 需要转换的字符串
:return: 驼峰命名
"""
parts = text.split('_')
return parts[0] + ''.join(word.capitalize() for word in parts[1:])
+99 -75
View File
@@ -39,28 +39,31 @@ class Jinja2TemplateUtil:
返回:
- Environment: Jinja2 环境对象。
"""
if cls._env is None:
# 确保模板目录存在
template_dir = settings.TEMPLATE_DIR
if not os.path.exists(template_dir):
raise RuntimeError(f'模板目录不存在: {template_dir}')
try:
if cls._env is None:
# 确保模板目录存在
template_dir = settings.TEMPLATE_DIR
if not os.path.exists(template_dir):
raise RuntimeError(f'模板目录不存在: {template_dir}')
cls._env = Environment(
loader=FileSystemLoader(settings.TEMPLATE_DIR),
autoescape=select_autoescape(['html', 'xml', 'jinja']), # 自动转义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
cls._env = Environment(
loader=FileSystemLoader(settings.TEMPLATE_DIR),
autoescape=select_autoescape(['html', 'xml', 'jinja', 'j2']), # 自动转义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:
@@ -90,36 +93,27 @@ class Jinja2TemplateUtil:
- Dict[str, Any]: 模板上下文字典。
"""
# 处理options为None的情况
options = gen_table.options or '{}'
try:
params_obj = json.loads(options)
except json.JSONDecodeError:
params_obj = {}
# 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 ''
# 确保pk_column不为None
pk_column = gen_table.pk_column
if pk_column is None and gen_table.columns:
# 如果没有明确的主键列,使用第一个列作为主键
pk_column = gen_table.columns[0]
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.capitalize() if business_name else '',
'base_package': cls.get_package_prefix(package_name) if package_name else '',
'business_name': business_name.capitalize(),
'base_package': cls.get_package_prefix(package_name),
'package_name': package_name,
'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'pk_column': pk_column,
'do_import_list': cls.get_import_list(gen_table, "model"),
'vo_import_list': cls.get_import_list(gen_table, "schema"),
'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,
@@ -127,12 +121,10 @@ class Jinja2TemplateUtil:
'db_type': settings.DATABASE_TYPE,
'column_not_add_show': GenConstant.COLUMNNAME_NOT_ADD_SHOW,
'column_not_edit_show': GenConstant.COLUMNNAME_NOT_EDIT_SHOW,
'primary_key': pk_column.python_field if pk_column else ''
}
return context
@classmethod
def get_template_list(cls):
"""
@@ -156,6 +148,7 @@ class Jinja2TemplateUtil:
]
return templates
@classmethod
def get_file_name(cls, template: str, gen_table: GenTableOutSchema):
"""
@@ -215,42 +208,60 @@ class Jinja2TemplateUtil:
return package_name[: package_name.rfind('.')] if '.' in package_name else package_name
@classmethod
def get_import_list(cls, gen_table: GenTableOutSchema, model_type: str):
def get_schema_import_list(cls, gen_table: GenTableOutSchema):
"""
获取导入包列表
获取schema模板导入包列表
参数:
- gen_table (GenTableOutSchema): 生成表的配置信息。
- model_type (str): 模型类型 ("model""schema")
返回:
- List[str]: 导入包列表。
:param gen_table: 生成表的配置信息
:return: 导入包列表
"""
columns = gen_table.columns or []
import_list = set()
if model_type == "model":
import_list.add('from sqlalchemy import Column')
for column in columns:
column_type = column.column_type or ''
if model_type == "schema":
# Schema特定导入逻辑
if column_type in GenConstant.TYPE_DATE:
import_list.add(f'from datetime import {column_type}')
elif column_type == GenConstant.TYPE_DECIMAL:
import_list.add('from decimal import Decimal')
else: # model
# Model特定导入逻辑
data_type = cls.get_db_type(column_type)
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()
import_list.add('from sqlalchemy import Column')
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')
sqlalchemy_type = GenConstant.DB_TO_SQLALCHEMY.get(data_type)
if sqlalchemy_type:
import_list.add(f'from sqlalchemy import {sqlalchemy_type}')
return cls.merge_same_imports(list(import_list),
'from datetime import' if model_type == "schema" else 'from sqlalchemy import')
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:
@@ -367,10 +378,23 @@ class Jinja2TemplateUtil:
返回:
- str: SQLAlchemy 类型字符串。
"""
column_type = getattr(column, 'column_type', str(column))
if not column_type:
return "String"
# 直接映射,简化逻辑
base_type = column_type.split('(')[0] if '(' in column_type else column_type
return GenConstant.DB_TO_SQLALCHEMY.get(base_type, "String")
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