mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-20 20:39:55 +00:00
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:
@@ -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:])
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user