mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-14 05:00:07 +00:00
574 lines
22 KiB
Python
574 lines
22 KiB
Python
import json
|
|
import os
|
|
import re
|
|
from typing import Any
|
|
|
|
from jinja2 import Environment, FileSystemLoader
|
|
|
|
from common.constant import GenConstant
|
|
from config.env import DataBaseConfig
|
|
from exceptions.exception import ServiceWarning
|
|
from module_generator.entity.vo.gen_vo import GenTableColumnModel, GenTableModel
|
|
from utils.common_util import CamelCaseUtil, SnakeCaseUtil
|
|
from utils.string_util import StringUtil
|
|
from utils.time_util import GenTimeUtil, TimezoneUtil
|
|
|
|
|
|
class TemplateInitializer:
|
|
"""
|
|
模板引擎初始化类
|
|
"""
|
|
|
|
@classmethod
|
|
def init_jinja2(cls) -> Environment:
|
|
"""
|
|
初始化 Jinja2 模板引擎
|
|
|
|
:return: Jinja2 环境对象
|
|
"""
|
|
try:
|
|
template_dir = os.path.join(os.getcwd(), 'module_generator', 'templates')
|
|
env = Environment(
|
|
loader=FileSystemLoader(template_dir),
|
|
keep_trailing_newline=True,
|
|
trim_blocks=True,
|
|
lstrip_blocks=True,
|
|
)
|
|
env.filters.update(
|
|
{
|
|
'camel_to_snake': SnakeCaseUtil.camel_to_snake,
|
|
'snake_to_camel': CamelCaseUtil.snake_to_camel,
|
|
'get_sqlalchemy_type': TemplateUtils.get_sqlalchemy_type,
|
|
}
|
|
)
|
|
return env
|
|
except Exception as e:
|
|
raise RuntimeError(f'初始化Jinja2模板引擎失败: {e}') from e
|
|
|
|
|
|
class TemplateUtils:
|
|
"""
|
|
模板工具类
|
|
"""
|
|
|
|
# 项目路径
|
|
FRONTEND_PROJECT_PATH = 'frontend'
|
|
BACKEND_PROJECT_PATH = 'backend'
|
|
DEFAULT_PARENT_MENU_ID = '3'
|
|
FORM_COL_SPAN_MAP = {2: '12', 3: '8'}
|
|
|
|
@classmethod
|
|
def prepare_context(cls, gen_table: GenTableModel) -> dict[str, Any]:
|
|
"""
|
|
准备模板变量
|
|
|
|
:param gen_table: 生成表的配置信息
|
|
:return: 模板上下文字典
|
|
"""
|
|
if not gen_table.options:
|
|
raise ServiceWarning(message='请先完善生成配置信息')
|
|
gen_table = gen_table.model_copy(deep=True)
|
|
cls.prepare_time_columns(gen_table)
|
|
class_name = gen_table.class_name
|
|
module_name = gen_table.module_name
|
|
business_name = gen_table.business_name
|
|
package_name = gen_table.package_name
|
|
tpl_category = gen_table.tpl_category
|
|
function_name = gen_table.function_name
|
|
|
|
source_name = gen_table.data_source_name or DataBaseConfig.db_default_source
|
|
source_config = DataBaseConfig.get_source(source_name)
|
|
default_source_name = DataBaseConfig.db_default_source
|
|
context = {
|
|
'tplCategory': tpl_category,
|
|
'tableName': gen_table.table_name,
|
|
'tableComment': gen_table.table_comment,
|
|
'functionName': function_name if StringUtil.is_not_empty(function_name) else '【请填写功能名称】',
|
|
'ClassName': class_name,
|
|
'className': class_name.lower(),
|
|
'moduleName': module_name,
|
|
'BusinessName': business_name.capitalize(),
|
|
'businessName': business_name,
|
|
'basePackage': cls.get_package_prefix(package_name),
|
|
'packageName': package_name,
|
|
'author': gen_table.function_author,
|
|
'colSpan': cls.get_col_span(gen_table.form_col_num),
|
|
'datetime': TimezoneUtil.to_business_time(TimezoneUtil.utc_now()).strftime('%Y-%m-%d %H:%M:%S'),
|
|
'pkColumn': gen_table.pk_column,
|
|
'doImportList': cls.get_do_import_list(gen_table),
|
|
'voImportList': cls.get_vo_import_list(gen_table),
|
|
'permissionPrefix': cls.get_permission_prefix(module_name, business_name),
|
|
'columns': gen_table.columns,
|
|
'instantFields': [
|
|
column.python_field
|
|
for column in gen_table.columns or []
|
|
if column.time_kind == 'instant' and (column.insert or column.edit)
|
|
],
|
|
'instantQueryFields': [
|
|
column.python_field
|
|
for column in gen_table.columns or []
|
|
if column.time_kind == 'instant' and column.query and column.query_type != 'BETWEEN'
|
|
],
|
|
'subInstantFields': [
|
|
column.python_field for column in gen_table.sub_table.columns or [] if column.time_kind == 'instant'
|
|
]
|
|
if gen_table.sub_table
|
|
else [],
|
|
'table': gen_table,
|
|
'dicts': cls.get_dicts(gen_table),
|
|
'dbType': source_config.db_type,
|
|
'dataSourceName': source_name,
|
|
'defaultDataSourceName': default_source_name,
|
|
'dbSessionDependency': (
|
|
f"DBSessionDependency('{source_name}')"
|
|
if source_name != default_source_name
|
|
else 'DBSessionDependency()'
|
|
),
|
|
'column_not_add_show': GenConstant.COLUMNNAME_NOT_ADD_SHOW,
|
|
'column_not_edit_show': GenConstant.COLUMNNAME_NOT_EDIT_SHOW,
|
|
}
|
|
|
|
# 设置扩展功能、菜单、树形结构、子表的上下文
|
|
cls.set_extensions_context(context, gen_table)
|
|
cls.set_menu_context(context, gen_table)
|
|
if tpl_category == GenConstant.TPL_TREE:
|
|
cls.set_tree_context(context, gen_table)
|
|
if tpl_category == GenConstant.TPL_SUB:
|
|
cls.set_sub_context(context, gen_table)
|
|
|
|
return context
|
|
|
|
@classmethod
|
|
def prepare_time_columns(cls, gen_table: GenTableModel) -> None:
|
|
"""
|
|
规范化模板副本中的主表和子表时间字段
|
|
|
|
只处理模板副本,不改写数据库中保存的生成配置。
|
|
|
|
:param gen_table: 用于渲染的生成表配置副本
|
|
"""
|
|
db_type = DataBaseConfig.get_source(gen_table.data_source_name).db_type
|
|
for table in [gen_table, gen_table.sub_table]:
|
|
if table is None:
|
|
continue
|
|
for column in table.columns or []:
|
|
try:
|
|
semantics = GenTimeUtil.get_time_semantics(column.column_type, db_type)
|
|
GenTimeUtil.validate_time_control(column.column_type, db_type, column.html_type)
|
|
except ServiceWarning as exc:
|
|
raise ServiceWarning(message=f'{table.table_name}.{column.column_name}: {exc.message}') from exc
|
|
column.vo_type = column.python_type
|
|
if semantics:
|
|
column.time_kind = semantics.kind
|
|
column.python_type = semantics.python_type
|
|
column.vo_type = semantics.vo_type
|
|
column.html_type = semantics.control
|
|
|
|
@classmethod
|
|
def set_extensions_context(cls, context: dict, gen_table: GenTableModel) -> None:
|
|
"""
|
|
设置扩展功能上下文
|
|
|
|
:param context: 模板上下文字典
|
|
:param gen_table: 生成表的配置信息
|
|
"""
|
|
params_obj = json.loads(gen_table.options) if gen_table.options else None
|
|
context['genView'] = cls.get_gen_view(params_obj)
|
|
|
|
@classmethod
|
|
def set_menu_context(cls, context: dict, gen_table: GenTableModel) -> None:
|
|
"""
|
|
设置菜单上下文
|
|
|
|
:param context: 模板上下文字典
|
|
:param gen_table: 生成表的配置信息
|
|
:return: 新的模板上下文字典
|
|
"""
|
|
options = gen_table.options
|
|
params_obj = json.loads(options)
|
|
context['parentMenuId'] = cls.get_parent_menu_id(params_obj)
|
|
|
|
@classmethod
|
|
def set_tree_context(cls, context: dict, gen_table: GenTableModel) -> None:
|
|
"""
|
|
设置树形结构上下文
|
|
|
|
:param context: 模板上下文字典
|
|
:param gen_table: 生成表的配置信息
|
|
:return: 新的模板上下文字典
|
|
"""
|
|
options = gen_table.options
|
|
params_obj = json.loads(options)
|
|
context['treeCode'] = cls.get_tree_code(params_obj)
|
|
context['treeParentCode'] = cls.get_tree_parent_code(params_obj)
|
|
context['treeName'] = cls.get_tree_name(params_obj)
|
|
context['expandColumn'] = cls.get_expand_column(gen_table)
|
|
|
|
@classmethod
|
|
def set_sub_context(cls, context: dict, gen_table: GenTableModel) -> None:
|
|
"""
|
|
设置子表上下文
|
|
|
|
:param context: 模板上下文字典
|
|
:param gen_table: 生成表的配置信息
|
|
:return: 新的模板上下文字典
|
|
"""
|
|
sub_table = gen_table.sub_table
|
|
sub_table_name = gen_table.sub_table_name
|
|
sub_table_fk_name = gen_table.sub_table_fk_name
|
|
sub_class_name = sub_table.class_name
|
|
sub_table_fk_class_name = StringUtil.convert_to_camel_case(sub_table_fk_name)
|
|
context['subTable'] = sub_table
|
|
context['subTableName'] = sub_table_name
|
|
context['subTableFkName'] = sub_table_fk_name
|
|
context['subTableFkClassName'] = sub_table_fk_class_name
|
|
context['subTableFkclassName'] = sub_table_fk_class_name[0].lower() + sub_table_fk_class_name[1:]
|
|
context['subClassName'] = sub_class_name
|
|
context['subclassName'] = sub_class_name.lower()
|
|
|
|
@classmethod
|
|
def get_template_list(cls, gen_table: GenTableModel) -> list[str]:
|
|
"""
|
|
获取模板列表
|
|
|
|
:param gen_table: 生成表的配置信息
|
|
:return: 模板列表
|
|
"""
|
|
tpl_category = gen_table.tpl_category
|
|
tpl_web_type = gen_table.tpl_web_type
|
|
params_obj = json.loads(gen_table.options) if gen_table.options else None
|
|
use_web_type = 'vue'
|
|
if tpl_web_type == 'element-plus':
|
|
use_web_type = 'vue/v3'
|
|
templates = [
|
|
'python/controller.py.jinja2',
|
|
'python/dao.py.jinja2',
|
|
'python/do.py.jinja2',
|
|
'python/service.py.jinja2',
|
|
'python/vo.py.jinja2',
|
|
'sql/sql.jinja2',
|
|
'js/api.js.jinja2',
|
|
]
|
|
if tpl_category == GenConstant.TPL_CRUD:
|
|
templates.append(f'{use_web_type}/index.vue.jinja2')
|
|
elif tpl_category == GenConstant.TPL_TREE:
|
|
templates.append(f'{use_web_type}/index-tree.vue.jinja2')
|
|
elif tpl_category == GenConstant.TPL_SUB:
|
|
templates.append(f'{use_web_type}/index.vue.jinja2')
|
|
# templates.append('python/sub-domain.python.jinja2')
|
|
if cls.get_gen_view(params_obj):
|
|
templates.append(f'{use_web_type}/view.vue.jinja2')
|
|
return templates
|
|
|
|
@classmethod
|
|
def get_file_name(cls, template: str, gen_table: GenTableModel) -> str:
|
|
"""
|
|
根据模板生成文件名
|
|
|
|
:param template: 模板路径
|
|
:param gen_table: 生成表的配置信息
|
|
:return: 模板生成文件名
|
|
"""
|
|
package_name = gen_table.package_name
|
|
module_name = gen_table.module_name
|
|
business_name = gen_table.business_name
|
|
|
|
vue_path = cls.FRONTEND_PROJECT_PATH
|
|
python_path = f'{cls.BACKEND_PROJECT_PATH}/{package_name.replace(".", "/")}'
|
|
|
|
if 'controller.py.jinja2' in template:
|
|
return f'{python_path}/controller/{business_name}_controller.py'
|
|
if 'dao.py.jinja2' in template:
|
|
return f'{python_path}/dao/{business_name}_dao.py'
|
|
if 'do.py.jinja2' in template:
|
|
return f'{python_path}/entity/do/{business_name}_do.py'
|
|
if 'service.py.jinja2' in template:
|
|
return f'{python_path}/service/{business_name}_service.py'
|
|
if 'vo.py.jinja2' in template:
|
|
return f'{python_path}/entity/vo/{business_name}_vo.py'
|
|
if 'sql.jinja2' in template:
|
|
return f'{cls.BACKEND_PROJECT_PATH}/sql/{business_name}_menu.sql'
|
|
if 'api.js.jinja2' in template:
|
|
return f'{vue_path}/api/{module_name}/{business_name}.js'
|
|
if 'index.vue.jinja2' in template or 'index-tree.vue.jinja2' in template:
|
|
return f'{vue_path}/views/{module_name}/{business_name}/index.vue'
|
|
if 'view.vue.jinja2' in template:
|
|
return f'{vue_path}/views/{module_name}/{business_name}/view.vue'
|
|
return ''
|
|
|
|
@classmethod
|
|
def get_package_prefix(cls, package_name: str) -> str:
|
|
"""
|
|
获取包前缀
|
|
|
|
:param package_name: 包名
|
|
:return: 包前缀
|
|
"""
|
|
return package_name[: package_name.rfind('.')]
|
|
|
|
@classmethod
|
|
def get_vo_import_list(cls, gen_table: GenTableModel) -> list[str]:
|
|
"""
|
|
获取vo模板导入包列表
|
|
|
|
:param gen_table: 生成表的配置信息
|
|
:return: 导入包列表
|
|
"""
|
|
import_list = set()
|
|
db_type = DataBaseConfig.get_source(gen_table.data_source_name).db_type
|
|
for table in [gen_table, gen_table.sub_table]:
|
|
if table is None:
|
|
continue
|
|
for column in table.columns or []:
|
|
semantics = GenTimeUtil.get_time_semantics(column.column_type, db_type)
|
|
if semantics:
|
|
import_list.add(f'from common.types import {semantics.vo_type}')
|
|
elif column.python_type == GenConstant.TYPE_DECIMAL:
|
|
import_list.add('from decimal import Decimal')
|
|
if any(
|
|
column.query_type == 'BETWEEN' and GenTimeUtil.get_time_semantics(column.column_type, db_type)
|
|
for column in gen_table.columns or []
|
|
):
|
|
import_list.add('from common.types import BusinessDate')
|
|
return cls.merge_same_imports(sorted(import_list), 'from common.types import')
|
|
|
|
@classmethod
|
|
def get_do_import_list(cls, gen_table: GenTableModel) -> list[str]:
|
|
"""
|
|
根据实际ORM列类型构建生成模型的导入列表
|
|
|
|
:param gen_table: 生成表配置信息
|
|
:return: 已合并同源导入的语句列表
|
|
"""
|
|
import_list = {'from sqlalchemy import Column'}
|
|
if gen_table.sub:
|
|
import_list.add('from sqlalchemy import ForeignKey')
|
|
for table in [gen_table, gen_table.sub_table]:
|
|
if table is None:
|
|
continue
|
|
for column in table.columns or []:
|
|
sqlalchemy_type = cls.get_sqlalchemy_type(column.column_type, gen_table.data_source_name)
|
|
if sqlalchemy_type == 'DbUtcDateTime':
|
|
if column.column_name not in {'create_time', 'update_time'}:
|
|
import_list.add('from common.types import DbUtcDateTime')
|
|
elif sqlalchemy_type == 'Geometry':
|
|
import_list.add('from geoalchemy2 import Geometry')
|
|
elif sqlalchemy_type.startswith('TIME('):
|
|
dialect = DataBaseConfig.get_source(gen_table.data_source_name).db_type
|
|
import_list.add(f'from sqlalchemy.dialects.{dialect} import TIME')
|
|
elif sqlalchemy_type.split('(', 1)[0] in {'JSONB', 'INET', 'CIDR', 'MACADDR'}:
|
|
import_list.add(f'from sqlalchemy.dialects.postgresql import {sqlalchemy_type}')
|
|
else:
|
|
import_list.add(f'from sqlalchemy import {sqlalchemy_type.split("(", 1)[0]}')
|
|
return cls.merge_same_imports(sorted(import_list), 'from sqlalchemy import')
|
|
|
|
@classmethod
|
|
def get_db_type(cls, column_type: str) -> str:
|
|
"""
|
|
获取规范化的数据库列类型
|
|
|
|
:param column_type: 数据库列类型
|
|
:return: 保留时区限定的规范化类型名称
|
|
"""
|
|
return GenTimeUtil.normalize_db_type(column_type)
|
|
|
|
@classmethod
|
|
def merge_same_imports(cls, imports: list[str], import_start: str) -> list[str]:
|
|
"""
|
|
合并相同的导入语句
|
|
|
|
:param imports: 导入语句列表
|
|
:param import_start: 导入语句的起始字符串
|
|
:return: 合并后的导入语句列表
|
|
"""
|
|
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: GenTableModel) -> str:
|
|
"""
|
|
获取字典列表
|
|
|
|
:param gen_table: 生成表的配置信息
|
|
:return: 字典列表
|
|
"""
|
|
columns = gen_table.columns or []
|
|
dicts = set()
|
|
cls.add_dicts(dicts, columns)
|
|
if gen_table.sub_table is not None:
|
|
cls.add_dicts(dicts, gen_table.sub_table.columns)
|
|
return ', '.join(dicts)
|
|
|
|
@classmethod
|
|
def add_dicts(cls, dicts: set[str], columns: list[GenTableColumnModel]) -> None:
|
|
"""
|
|
添加字典列表
|
|
|
|
:param dicts: 字典列表
|
|
:param columns: 字段列表
|
|
:return: 新的字典列表
|
|
"""
|
|
for column in columns:
|
|
if (
|
|
not column.super_column
|
|
and StringUtil.is_not_empty(column.dict_type)
|
|
and StringUtil.equals_any_ignore_case(
|
|
column.html_type, [GenConstant.HTML_SELECT, GenConstant.HTML_RADIO, GenConstant.HTML_CHECKBOX]
|
|
)
|
|
):
|
|
dicts.add(f"'{column.dict_type}'")
|
|
|
|
@classmethod
|
|
def get_permission_prefix(cls, module_name: str, business_name: str) -> str:
|
|
"""
|
|
获取权限前缀
|
|
|
|
:param module_name: 模块名
|
|
:param business_name: 业务名
|
|
:return: 权限前缀
|
|
"""
|
|
return f'{module_name}:{business_name}'
|
|
|
|
@classmethod
|
|
def get_col_span(cls, form_col_num: int | None) -> str:
|
|
"""
|
|
根据表单列数获取栅格跨度
|
|
|
|
:param form_col_num: 表单列数
|
|
:return: 栅格跨度
|
|
"""
|
|
return cls.FORM_COL_SPAN_MAP.get(form_col_num, '24')
|
|
|
|
@classmethod
|
|
def get_parent_menu_id(cls, params_obj: dict) -> str:
|
|
"""
|
|
获取上级菜单ID
|
|
|
|
:param params_obj: 菜单参数字典
|
|
:return: 上级菜单ID
|
|
"""
|
|
if params_obj and params_obj.get(GenConstant.PARENT_MENU_ID):
|
|
return params_obj.get(GenConstant.PARENT_MENU_ID)
|
|
return cls.DEFAULT_PARENT_MENU_ID
|
|
|
|
@classmethod
|
|
def get_gen_view(cls, params_obj: dict | None) -> bool:
|
|
"""
|
|
获取是否生成详情页
|
|
|
|
:param params_obj: 生成选项
|
|
:return: 是否生成详情页
|
|
"""
|
|
if not params_obj or GenConstant.GEN_VIEW not in params_obj:
|
|
return False
|
|
value = params_obj[GenConstant.GEN_VIEW]
|
|
if isinstance(value, bool):
|
|
return value
|
|
if isinstance(value, (int, float)):
|
|
return value == 1
|
|
return str(value).lower() in {'1', 'true', 'yes', 'y'}
|
|
|
|
@classmethod
|
|
def get_tree_code(cls, params_obj: dict) -> str:
|
|
"""
|
|
获取树编码
|
|
|
|
:param params_obj: 菜单参数字典
|
|
:return: 树编码
|
|
"""
|
|
if GenConstant.TREE_CODE in params_obj:
|
|
return cls.to_camel_case(params_obj.get(GenConstant.TREE_CODE))
|
|
return ''
|
|
|
|
@classmethod
|
|
def get_tree_parent_code(cls, params_obj: dict) -> str:
|
|
"""
|
|
获取树父编码
|
|
|
|
:param params_obj: 菜单参数字典
|
|
:return: 树父编码
|
|
"""
|
|
if GenConstant.TREE_PARENT_CODE in params_obj:
|
|
return cls.to_camel_case(params_obj.get(GenConstant.TREE_PARENT_CODE))
|
|
return ''
|
|
|
|
@classmethod
|
|
def get_tree_name(cls, params_obj: dict) -> str:
|
|
"""
|
|
获取树名称
|
|
|
|
:param params_obj: 菜单参数字典
|
|
:return: 树名称
|
|
"""
|
|
if GenConstant.TREE_NAME in params_obj:
|
|
return cls.to_camel_case(params_obj.get(GenConstant.TREE_NAME))
|
|
return ''
|
|
|
|
@classmethod
|
|
def get_expand_column(cls, gen_table: GenTableModel) -> int:
|
|
"""
|
|
获取展开列
|
|
|
|
:param gen_table: 生成表的配置信息
|
|
:return: 展开列
|
|
"""
|
|
options = gen_table.options
|
|
params_obj = json.loads(options)
|
|
tree_name = params_obj.get(GenConstant.TREE_NAME)
|
|
num = 0
|
|
for column in gen_table.columns or []:
|
|
if column.list:
|
|
num += 1
|
|
if column.column_name == tree_name:
|
|
break
|
|
return num
|
|
|
|
@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:])
|
|
|
|
@classmethod
|
|
def get_sqlalchemy_type(cls, column_type: str, source_name: str | None = None) -> str:
|
|
"""
|
|
获取SQLAlchemy类型
|
|
|
|
:param column_type: 列类型
|
|
:param source_name: 数据源名称
|
|
:return: SQLAlchemy类型
|
|
"""
|
|
source_config = DataBaseConfig.get_source(source_name)
|
|
normalized = column_type.lower().strip()
|
|
base = GenTimeUtil.normalize_db_type(normalized)
|
|
semantics = GenTimeUtil.get_time_semantics(column_type, source_config.db_type)
|
|
if semantics:
|
|
precision = re.search(r'\((\d+)\)', normalized)
|
|
if semantics.kind == 'time' and precision:
|
|
keyword = 'fsp' if source_config.db_type == 'mysql' else 'precision'
|
|
return f'TIME({keyword}={precision.group(1)})'
|
|
return semantics.sqlalchemy_type
|
|
type_mapping = GenConstant.DB_TO_SQLALCHEMY_TYPE_MAPPING[source_config.db_type]
|
|
sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case(type_mapping, base) or 'String'
|
|
if '(' in normalized and sqlalchemy_type in {'String', 'CHAR', 'Numeric', 'DECIMAL'}:
|
|
precision = re.search(r'\(([^)]*)\)', normalized).group(1)
|
|
return f'{sqlalchemy_type}({precision})'
|
|
return sqlalchemy_type
|