# -*- coding:utf-8 -*- import json import os from datetime import datetime from jinja2.environment import Environment from jinja2 import Environment, FileSystemLoader, select_autoescape, Template from typing import Dict, List, Any, Set from app.common.constant import GenConstant from app.config.setting import settings from app.api.v1.module_generator.gencode.schema import GenTableOutSchema, GenTableColumnOutSchema from app.utils.common_util import CamelCaseUtil, SnakeCaseUtil from app.utils.string_util import StringUtil class Jinja2TemplateUtil: """ 模板处理工具类 """ # 项目路径 FRONTEND_PROJECT_PATH = 'frontend' BACKEND_PROJECT_PATH = 'backend' # 默认上级菜单,系统工具 DEFAULT_PARENT_MENU_ID = "3" # 环境对象 _env = None @classmethod def get_env(cls): """ 获取模板环境对象。 参数: - 无 返回: - Environment: Jinja2 环境对象。 """ 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', '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: """ 获取模板。 参数: - template_path (str): 模板路径。 返回: - Template: Jinja2 模板对象。 异常: - TemplateNotFound: 模板未找到时抛出。 """ return cls.get_env().get_template(template_path) @classmethod def prepare_context(cls, gen_table: GenTableOutSchema) -> dict[str, Any]: """ 准备模板变量。 参数: - gen_table (GenTableOutSchema): 生成表的配置信息。 返回: - Dict[str, Any]: 模板上下文字典。 """ # 处理options为None的情况 # 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 '' 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(), 'base_package': cls.get_package_prefix(package_name), 'package_name': package_name, 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), '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, 'dicts': cls.get_dicts(gen_table), 'db_type': settings.DATABASE_TYPE, 'column_not_add_show': GenConstant.COLUMNNAME_NOT_ADD_SHOW, 'column_not_edit_show': GenConstant.COLUMNNAME_NOT_EDIT_SHOW, } return context @classmethod def get_template_list(cls): """ 获取模板列表。 参数: - 无 返回: - List[str]: 模板路径列表。 """ templates = [ 'python/controller.py.j2', 'python/service.py.j2', 'python/crud.py.j2', 'python/schema.py.j2', 'python/param.py.j2', 'python/model.py.j2', 'sql/sql.sql.j2', 'ts/api.ts.j2', 'vue/index.vue.j2', ] return templates @classmethod def get_file_name(cls, template: str, gen_table: GenTableOutSchema): """ 根据模板生成文件名。 参数: - template (str): 模板路径字符串。 - gen_table (GenTableOutSchema): 生成表的配置信息。 返回: - str: 模板生成的文件名。 异常: - ValueError: 当无法生成有效文件名时抛出。 """ module_name = gen_table.module_name or '' business_name = gen_table.business_name or '' # 验证必要的参数 if not module_name or not business_name: raise ValueError(f"无法为模板 {template} 生成文件名:模块名或业务名未设置") # 映射表方式简化 template_mapping = { 'controller.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/module_{module_name}/{business_name}/controller.py', 'service.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/module_{module_name}/{business_name}/service.py', 'crud.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/module_{module_name}/{business_name}/crud.py', 'model.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/module_{module_name}/{business_name}/model.py', 'param.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/module_{module_name}/{business_name}/param.py', 'schema.py.j2': f'{cls.BACKEND_PROJECT_PATH}/app/api/v1/module_{module_name}/{business_name}/schema.py', 'sql.sql.j2': f'{cls.BACKEND_PROJECT_PATH}/sql/module_{module_name}/{business_name}_menu.sql', 'api.ts.j2': f'{cls.FRONTEND_PROJECT_PATH}/src/api/module_{module_name}/{business_name}.ts', 'index.vue.j2': f'{cls.FRONTEND_PROJECT_PATH}/src/views/module_{module_name}/{business_name}/index.vue' } # 查找匹配的模板路径 for key, path in template_mapping.items(): if key in template: return path # 默认处理 template_name = template.split('/')[-1].replace('.j2', '') return f'{cls.BACKEND_PROJECT_PATH}/generated/{template_name}' @classmethod def get_package_prefix(cls, package_name: str) -> str: """ 获取包前缀。 参数: - package_name (str): 包名。 返回: - str: 包前缀。 """ # 修复:当包名中不存在'.'时,直接返回原包名 return package_name[: package_name.rfind('.')] if '.' in package_name else package_name @classmethod def get_schema_import_list(cls, gen_table: GenTableOutSchema): """ 获取schema模板导入包列表 :param gen_table: 生成表的配置信息 :return: 导入包列表 """ columns = gen_table.columns or [] import_list = set() for column in columns: 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') 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: """ 获取数据库字段类型。 参数: - column_type (str): 字段类型字符串。 返回: - str: 数据库类型(去除长度等修饰)。 """ if '(' in column_type: return column_type.split('(')[0] return column_type @classmethod def merge_same_imports(cls, imports: List[str], import_start: str) -> List[str]: """ 合并相同的导入语句。 参数: - imports (List[str]): 导入语句列表。 - import_start (str): 导入语句的起始字符串。 返回: - List[str]: 合并后的导入语句列表。 """ 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: GenTableOutSchema): """ 获取字典列表。 参数: - gen_table (GenTableOutSchema): 生成表的配置信息。 返回: - str: 以逗号分隔的字典类型字符串。 """ columns = gen_table.columns or [] dicts = set() cls.add_dicts(dicts, columns) # 处理sub_table为None的情况 if gen_table.sub_table is not None: # 处理sub_table.columns为None的情况 sub_columns = gen_table.sub_table.columns or [] cls.add_dicts(dicts, sub_columns) return ', '.join(dicts) @classmethod def add_dicts(cls, dicts: Set[str], columns: List[GenTableColumnOutSchema]): """ 添加字典类型到集合。 参数: - dicts (Set[str]): 字典类型集合。 - columns (List[GenTableColumnOutSchema]): 字段列表。 返回: - Set[str]: 更新后的字典类型集合。 """ for column in columns: # 处理column.super_column, column.dict_type, column.html_type为None的情况 super_column = column.super_column if column.super_column is not None else False dict_type = column.dict_type or '' html_type = column.html_type or '' if ( not super_column and StringUtil.is_not_empty(dict_type) and StringUtil.equals_any_ignore_case( html_type, [GenConstant.HTML_SELECT, GenConstant.HTML_RADIO, GenConstant.HTML_CHECKBOX] ) ): dicts.add(f"'{dict_type}'") @classmethod def get_permission_prefix(cls, module_name: str | None, business_name: str | None) -> str: """ 获取权限前缀。 参数: - module_name (str | None): 模块名。 - business_name (str | None): 业务名。 返回: - str: 权限前缀字符串。 """ return f'{module_name}:{business_name}' @classmethod def get_sqlalchemy_type(cls, column): """ 获取 SQLAlchemy 类型。 参数: - column_type (Any): 列类型或包含 `column_type` 属性的对象。 返回: - str: SQLAlchemy 类型字符串。 """ 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