| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884 |
- import os
- from typing import List
- from jinja2 import Template
- from ruoyi_common.utils import StringUtil
- from ruoyi_generator.domain.entity import GenTable
- from ruoyi_generator.config import GeneratorConfig
- import zipfile
- from io import BytesIO
- from datetime import datetime
- import re
- def to_underscore(name: str) -> str:
- """
- 将驼峰命名转换为下划线命名
-
- Args:
- name (str): 驼峰命名的字符串
-
- Returns:
- str: 下划线命名的字符串
- """
- # 在大写字母前添加下划线,然后转为小写
- s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
- return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
- class GenUtils:
- @staticmethod
- def get_file_name(template_file: str, table: GenTable) -> str:
- """
- 根据模板文件名和表信息生成文件名
-
- Args:
- template_file (str): 模板文件名
- table (GenTable): 表信息
-
- Returns:
- str: 生成的文件名
- """
- # 标准化路径分隔符
- template_file = template_file.replace('\\', '/')
-
- # 移除.vm后缀
- base_name = template_file[:-3] if template_file.endswith('.vm') else template_file
-
- # 确定模块路径:后端代码使用 pythonModelName,前端代码使用 modelName
- if table.package_name:
- # 使用 package_name 作为路径(如 com.yy.test -> com/yy/test)
- module_path = table.package_name.replace('.', '/')
- else:
- # 如果 package_name 为空,使用 pythonModelName 作为后端模块名
- # 注意:这里只用于后端 Python 代码路径,前端代码路径在下面单独处理
- module_path = GeneratorConfig.python_model_name if hasattr(GeneratorConfig, 'python_model_name') else (table.module_name if table.module_name else 'ruoyi_generator')
-
- # 根据模板类型生成文件名和路径
- if 'py/entity.py' in template_file:
- # Entity文件放在 domain/entity/ 目录下,使用下划线命名法
- entity_name = to_underscore(table.class_name)
- return f"{module_path}/domain/entity/{entity_name}.py"
- elif 'py/controller.py' in template_file:
- # 使用下划线命名法
- controller_name = f"{to_underscore(table.class_name)}_controller"
- return f"{module_path}/controller/{controller_name}.py"
- elif 'py/service.py' in template_file:
- # 使用下划线命名法
- service_name = f"{to_underscore(table.class_name)}_service"
- return f"{module_path}/service/{service_name}.py"
- elif 'py/mapper.py' in template_file:
- # 使用下划线命名法
- mapper_name = f"{to_underscore(table.class_name)}_mapper"
- return f"{module_path}/mapper/{mapper_name}.py"
- elif 'py/po.py' in template_file:
- # PO文件放在 domain/po/ 目录下,使用下划线命名法
- po_name = f"{to_underscore(table.class_name)}_po"
- return f"{module_path}/domain/po/{po_name}.py"
- elif 'vue/index.vue' in template_file or 'vue/index-tree.vue' in template_file:
- # 无论是树表还是普通表,Vue文件名都是index.vue
- # 使用数据库中的 module_name(前端模块名)
- frontend_module = table.module_name if table.module_name else GeneratorConfig.model_name
- return f"vue/views/{frontend_module}/{table.business_name}/index.vue"
- elif 'js/api.js' in template_file:
- # 使用数据库中的 module_name(前端模块名)
- frontend_module = table.module_name if table.module_name else GeneratorConfig.model_name
- return f"vue/api/{frontend_module}/{table.business_name}.js"
- elif 'py/__init__.py' in template_file:
- # 模块根目录的 __init__.py
- return f"{module_path}/__init__.py"
- elif 'sql/menu.sql' in template_file:
- return f"sql/{table.business_name}_menu.sql"
- elif 'README.md' in template_file:
- return f"{table.business_name}_README.md"
- else:
- # 处理其他模板文件,保持原有目录结构
- filename = os.path.basename(base_name)
- if '.' not in filename:
- filename += '.py' # 默认添加.py扩展名
- return filename
- @staticmethod
- def get_table_prefix() -> str:
- """
- 获取表前缀
-
- Returns:
- str: 表前缀
- """
- return GeneratorConfig.table_prefix or ""
- @staticmethod
- def remove_table_prefix(table_name: str) -> str:
- """
- 移除表前缀
-
- Args:
- table_name (str): 表名
-
- Returns:
- str: 移除前缀后的表名
- """
- prefix = GenUtils.get_table_prefix()
- if prefix and table_name.startswith(prefix):
- return table_name[len(prefix):]
- return table_name
- @staticmethod
- def table_to_class_name(table_name: str) -> str:
- """
- 将表名转换为类名
-
- Args:
- table_name (str): 表名
-
- Returns:
- str: 类名
- """
- # 移除表前缀
- clean_table_name = GenUtils.remove_table_prefix(table_name)
- # 转换为驼峰命名
- return GenUtils.to_camel_case(clean_table_name)
- @staticmethod
- def get_business_name(table_name: str) -> str:
- """
- 获取业务名
-
- Args:
- table_name (str): 表名
-
- Returns:
- str: 业务名
- """
- # 移除表前缀
- clean_table_name = GenUtils.remove_table_prefix(table_name)
- # 获取下划线分隔的第一部分
- return GenUtils.substring_before(clean_table_name, "_") if "_" in clean_table_name else clean_table_name
- @staticmethod
- def get_import_path(package_name: str, module_name: str, module_type: str, class_name: str = None) -> str:
- """
- 生成导入路径
-
- Args:
- package_name (str): 包名,如 "com.yy.project" 或空字符串
- module_name (str): 前端模块名(从数据库读取,用于前端代码),但这里应该传入 pythonModelName
- module_type (str): 模块类型,如 "domain", "service", "mapper", "controller"
- class_name (str): 类名(可选,用于PO导入)
-
- Returns:
- str: 导入路径,Python包名保持点分隔格式
- """
- # Python 后端代码使用 pythonModelName,而不是前端模块名
- # 如果 package_name 为空,使用 pythonModelName 作为 Python 模块名
- if not package_name:
- # 使用 pythonModelName 作为 Python 模块名
- python_package = GeneratorConfig.python_model_name if hasattr(GeneratorConfig, 'python_model_name') else (module_name if module_name else 'ruoyi_generator')
- else:
- # Python导入路径使用点分隔,保持原样
- # 例如: "com.yy.project" -> "com.yy.project"
- python_package = package_name
-
- # 生成导入路径
- if module_type == "domain" and class_name:
- # PO 文件在 domain/po/ 目录下
- return f"{python_package}.domain.po"
- elif module_type == "domain":
- return f"{python_package}.domain.entity"
- else:
- return f"{python_package}.{module_type}"
- @staticmethod
- def to_camel_case(name: str) -> str:
- """
- 将下划线命名转换为驼峰命名
-
- Args:
- name (str): 下划线命名
-
- Returns:
- str: 驼峰命名
- """
- if hasattr(StringUtil, 'to_camel_case'):
- return StringUtil.to_camel_case(name)
- # 如果StringUtil没有to_camel_case方法,则手动实现
- parts = name.split('_')
- if len(parts) == 1:
- return parts[0]
- return parts[0] + ''.join(word.capitalize() for word in parts[1:])
- @staticmethod
- def substring_before(string: str, separator: str) -> str:
- """
- 获取字符串中分隔符之前的部分
-
- Args:
- string (str): 输入字符串
- separator (str): 分隔符
-
- Returns:
- str: 分隔符之前的部分
- """
- if hasattr(StringUtil, 'substring_before'):
- return StringUtil.substring_before(string, separator)
- # 如果StringUtil没有substring_before方法,则手动实现
- if separator in string:
- return string.split(separator, 1)[0]
- return string
- @staticmethod
- def substring_after(string: str, separator: str) -> str:
- """
- 获取字符串中分隔符之后的部分
-
- Args:
- string (str): 输入字符串
- separator (str): 分隔符
-
- Returns:
- str: 分隔符之后的部分
- """
- if hasattr(StringUtil, 'substring_after'):
- return StringUtil.substring_after(string, separator)
- # 如果StringUtil没有substring_after方法,则手动实现
- if separator in string:
- return string.split(separator, 1)[1]
- return ""
-
- @staticmethod
- def generator_code(table: GenTable) -> BytesIO:
- """
- 生成代码
-
- Args:
- table (GenTable): 表信息
-
- Returns:
- BytesIO: 生成的代码文件
- """
- # 设置列的 list_index 属性
- GenUtils.set_column_list_index(table)
-
- # 设置主键列
- pk_columns = [column for column in table.columns if column.is_pk == '1']
- if pk_columns:
- table.pk_column = pk_columns[0]
- else:
- table.pk_column = None
-
- # 从 options 中解析 parentMenuId
- if table.options:
- import json
- try:
- if isinstance(table.options, str):
- options_dict = json.loads(table.options)
- else:
- options_dict = table.options
- # 从 options 中提取 parentMenuId 并设置到 table
- if 'parentMenuId' in options_dict:
- table.parent_menu_id = options_dict.get('parentMenuId')
- except Exception as e:
- print(f"解析 options 字段出错: {e}")
-
- # 强制使用前端模块名(modelName),而不是 Python 模块名
- # module_name 必须使用 modelName(test),不能使用 pythonModelName(ruoyi_test)
- # 如果 module_name 是空的、等于 python_model_name 或包含 python_model_name,强制替换为 model_name
- original_module_name = table.module_name
- if not table.module_name or table.module_name == GeneratorConfig.python_model_name or (table.module_name and GeneratorConfig.python_model_name in table.module_name):
- table.module_name = GeneratorConfig.model_name
- if original_module_name != table.module_name:
- print(f"警告:table.module_name 从 '{original_module_name}' 强制替换为 '{table.module_name}'(前端模块名)")
-
- # 获取模板目录
- template_dir = os.path.join(os.path.dirname(__file__), 'vm')
-
- # 定义核心模板文件
- core_templates = [
- 'py/entity.py.vm',
- 'py/po.py.vm',
- 'py/controller.py.vm',
- 'py/service.py.vm',
- 'py/mapper.py.vm',
- 'js/api.js.vm',
- 'sql/menu.sql.vm'
- ]
-
- # 根据表类型添加相应的Vue模板
- if table.tpl_category == 'tree':
- core_templates.append('vue/index-tree.vue.vm')
- else:
- core_templates.append('vue/index.vue.vm')
-
- # 创建内存中的ZIP文件
- zip_buffer = BytesIO()
-
- # 确定模块路径,用于生成 __init__.py
- # 后端 Python 代码使用 pythonModelName,而不是前端模块名
- if table.package_name:
- module_path = table.package_name.replace('.', '/')
- else:
- # 使用 pythonModelName 作为后端模块路径
- module_path = GeneratorConfig.python_model_name if hasattr(GeneratorConfig, 'python_model_name') else (table.module_name if table.module_name else 'ruoyi_generator')
-
- # 收集需要生成 __init__.py 的目录和文件信息
- init_dirs = set()
- # 收集每个目录下的文件,用于生成导入语句
- dir_files = {} # {dir_path: [file_info]}
-
- with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
- # 跟踪已添加的文件名以避免重复
- added_files = set()
-
- # 首先生成模块根目录的 __init__.py
- init_template_path = os.path.join(template_dir, 'py/__init__.py.vm')
- if os.path.exists(init_template_path):
- try:
- with open(init_template_path, 'r', encoding='utf-8') as f:
- template_content = f.read()
-
- # 准备模板上下文(单个表时,tables 为 None)
- context = {
- 'table': table,
- 'tables': None, # 单个表生成时,tables 为 None
- 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- 'underscore': to_underscore,
- 'get_import_path': GenUtils.get_import_path
- }
-
- # 使用Jinja2渲染模板
- template = Template(template_content)
- rendered_content = template.render(**context)
-
- # 生成文件名
- init_file_path = f"{module_path}/__init__.py"
-
- # 将渲染后的内容写入ZIP文件
- if rendered_content.strip():
- zip_file.writestr(init_file_path, rendered_content)
- added_files.add(init_file_path)
- except Exception as e:
- print(f"处理模块 __init__.py 模板时出错: {e}")
-
- # 处理每个核心模板文件
- for relative_path in core_templates:
- template_path = os.path.join(template_dir, relative_path)
- if os.path.exists(template_path):
- # 读取模板内容
- try:
- with open(template_path, 'r', encoding='utf-8') as f:
- template_content = f.read()
-
- # 准备模板上下文
- # table.module_name 是从数据库读取的前端模块名(真正的模块名,用于权限、前端、SQL)
- # GeneratorConfig.python_model_name 是 Python 模块名(只用于 Python 后端代码路径)
- context = {
- 'table': table,
- 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- 'underscore': to_underscore, # 添加自定义过滤器
- 'get_import_path': GenUtils.get_import_path # 添加导入路径生成函数
- }
-
- # 使用Jinja2渲染模板
- template = Template(template_content)
- rendered_content = template.render(**context)
-
- # 如果是 SQL 模板,恢复原始 module_name(虽然已经强制设置了,但为了安全)
- if 'sql/menu.sql' in relative_path:
- pass # 已经强制设置为 model_name,不需要恢复
-
- # 生成文件名
- output_file_name = GenUtils.get_file_name(relative_path, table)
-
- # 收集目录路径和文件信息,用于生成 __init__.py
- # 跳过 sql 和 vue 目录,这些目录不需要 __init__.py
- dir_path = os.path.dirname(output_file_name)
- if dir_path and 'sql' not in dir_path and 'vue' not in dir_path:
- init_dirs.add(dir_path)
- # 同时收集父目录(但也要跳过 sql 和 vue)
- parts = dir_path.split('/')
- for i in range(1, len(parts)):
- parent_dir = '/'.join(parts[:i])
- if parent_dir and 'sql' not in parent_dir and 'vue' not in parent_dir:
- init_dirs.add(parent_dir)
-
- # 收集文件信息用于生成导入语句
- file_name = os.path.basename(output_file_name)
- file_base = os.path.splitext(file_name)[0]
-
- if dir_path not in dir_files:
- dir_files[dir_path] = []
-
- # 根据文件类型确定导入的类名
- if '/entity/' in output_file_name and '.py' in output_file_name:
- # Entity 文件在 domain/entity/ 目录下
- dir_files[dir_path].append(('entity', table.class_name, table))
- elif '/po/' in output_file_name and '_po.py' in output_file_name:
- # PO 文件在 domain/po/ 目录下
- dir_files[dir_path].append(('po', f"{to_underscore(table.class_name)}_po", table))
- elif '_service.py' in output_file_name:
- dir_files[dir_path].append(('service', f"{table.class_name}Service", table))
- elif '_mapper.py' in output_file_name:
- dir_files[dir_path].append(('mapper', f"{table.class_name}Mapper", table))
- elif '_controller.py' in output_file_name:
- dir_files[dir_path].append(('controller', 'gen', table))
-
- # 检查渲染后的内容是否为空
- if rendered_content.strip():
- # 将渲染后的内容写入ZIP文件
- zip_file.writestr(output_file_name, rendered_content)
- else:
- print(f"警告: 模板 {relative_path} 渲染后内容为空")
- except Exception as e:
- print(f"处理模板 {relative_path} 时出错: {e}")
-
- # 为每个目录生成 __init__.py 文件,使其成为完整的 Python 模块
- for dir_path in sorted(init_dirs):
- # 跳过模块根目录,因为已经在开始时生成
- if dir_path == module_path:
- continue
-
- # 跳过 sql 和 vue 目录,这些目录不需要 __init__.py
- if 'sql' in dir_path or 'vue' in dir_path or dir_path.startswith('sql/') or dir_path.startswith('vue/'):
- continue
-
- init_file_path = os.path.join(dir_path, '__init__.py').replace('\\', '/')
-
- # 生成 __init__.py 内容
- init_lines = ["# -*- coding: utf-8 -*-"]
- init_lines.append(f"# @Module: {dir_path}")
- init_lines.append("")
-
- # 特殊处理 controller 目录:参考 ruoyi_generator/controller/__init__.py 的格式
- if 'controller' in dir_path and dir_path.endswith('/controller'):
- # 在 controller/__init__.py 中为每个 controller 创建蓝图
- if dir_path in dir_files:
- # 先导入 Blueprint
- init_lines.append("from flask import Blueprint")
- init_lines.append("")
-
- # 为每个 controller 创建蓝图
- for file_type, class_name, table_info in dir_files[dir_path]:
- if file_type == 'controller':
- blueprint_name = to_underscore(table_info.class_name)
- url_prefix = f"/{table_info.module_name}/{table_info.business_name}"
- init_lines.append(f"{blueprint_name} = Blueprint('{blueprint_name}', __name__, url_prefix='{url_prefix}')")
-
- # 导入各个 controller 模块
- init_lines.append("")
- init_lines.append("")
- for file_type, class_name, table_info in dir_files[dir_path]:
- if file_type == 'controller':
- controller_module_name = f"{to_underscore(table_info.class_name)}_controller"
- init_lines.append(f"from . import {controller_module_name}")
- else:
- # 其他目录正常生成导入语句
- if dir_path in dir_files:
- imports = []
- for file_type, class_name, table_info in dir_files[dir_path]:
- if file_type == 'entity':
- # Entity 文件在 domain/entity/ 目录下,导入时使用文件名
- entity_file_name = to_underscore(class_name)
- imports.append(f"from .{entity_file_name} import {class_name}")
- elif file_type == 'po':
- # PO 文件在 domain/po/ 目录下,导入时使用文件名
- po_file_name = class_name # class_name 已经是 address_info_po
- imports.append(f"from .{po_file_name} import {class_name}")
- elif file_type == 'service':
- imports.append(f"from .{to_underscore(class_name.replace('Service', ''))}_service import {class_name}")
- elif file_type == 'mapper':
- imports.append(f"from .{to_underscore(class_name.replace('Mapper', ''))}_mapper import {class_name}")
-
- if imports:
- init_lines.extend(sorted(set(imports)))
-
- init_content = "\n".join(init_lines) + "\n"
- zip_file.writestr(init_file_path, init_content)
-
- zip_buffer.seek(0)
- return zip_buffer
-
- @staticmethod
- def batch_generator_code(tables: List[GenTable]) -> BytesIO:
- """
- 批量生成代码
-
- Args:
- tables (List[GenTable]): 表列表
-
- Returns:
- BytesIO: 生成的代码文件
- """
- # 为每个表设置列的 list_index 属性和主键列
- for table in tables:
- GenUtils.set_column_list_index(table)
- # 设置主键列
- pk_columns = [column for column in table.columns if column.is_pk == '1']
- if pk_columns:
- table.pk_column = pk_columns[0]
- else:
- table.pk_column = None
-
- # 从 options 中解析 parentMenuId
- if table.options:
- import json
- try:
- if isinstance(table.options, str):
- options_dict = json.loads(table.options)
- else:
- options_dict = table.options
- # 从 options 中提取 parentMenuId 并设置到 table
- if 'parentMenuId' in options_dict:
- table.parent_menu_id = options_dict.get('parentMenuId')
- except Exception as e:
- print(f"解析 options 字段出错: {e}")
-
- # 强制使用前端模块名(modelName),而不是 Python 模块名
- # module_name 必须使用 modelName(test),不能使用 pythonModelName(ruoyi_test)
- if not table.module_name or table.module_name == GeneratorConfig.python_model_name or table.module_name.strip() == GeneratorConfig.python_model_name:
- table.module_name = GeneratorConfig.model_name
- # 额外检查:如果 module_name 等于 python_model_name,强制替换
- if table.module_name == GeneratorConfig.python_model_name:
- table.module_name = GeneratorConfig.model_name
-
- # 定义核心模板文件(每个表都会生成,但 __init__.py 只生成一次)
- core_templates = [
- 'py/entity.py.vm',
- 'py/po.py.vm',
- 'py/controller.py.vm',
- 'py/service.py.vm',
- 'py/mapper.py.vm',
- 'js/api.js.vm',
- 'sql/menu.sql.vm'
- ]
-
- # 创建内存中的ZIP文件
- zip_buffer = BytesIO()
-
- # 获取模板目录
- template_dir = os.path.join(os.path.dirname(__file__), 'vm')
-
- # 确定模块路径(使用第一个表的模块路径)
- if tables and len(tables) > 0:
- first_table = tables[0]
- if first_table.package_name:
- module_path = first_table.package_name.replace('.', '/')
- else:
- # 使用 pythonModelName 作为后端模块路径
- module_path = GeneratorConfig.python_model_name if hasattr(GeneratorConfig, 'python_model_name') else (first_table.module_name if first_table.module_name else 'ruoyi_generator')
- else:
- module_path = GeneratorConfig.python_model_name if hasattr(GeneratorConfig, 'python_model_name') else 'ruoyi_generator'
-
- # 收集需要生成 __init__.py 的目录和文件信息
- init_dirs = set()
- # 收集每个目录下的文件,用于生成导入语句 {dir_path: [(file_type, class_name, table)]}
- dir_files = {}
-
- with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
- # 跟踪已添加的文件名以避免重复
- added_files = set()
-
- # 首先为模块根目录生成 __init__.py(只生成一次,使用第一个表的信息)
- if tables and len(tables) > 0:
- init_template_path = os.path.join(template_dir, 'py/__init__.py.vm')
- if os.path.exists(init_template_path):
- try:
- with open(init_template_path, 'r', encoding='utf-8') as f:
- template_content = f.read()
-
- # 准备模板上下文(批量生成时,传入所有表)
- # table.module_name 是从数据库读取的前端模块名(真正的模块名,用于权限、前端、SQL)
- # GeneratorConfig.python_model_name 是 Python 模块名(只用于 Python 后端代码路径)
- context = {
- 'table': first_table, # 用于兼容模板中的 table 变量
- 'tables': tables, # 传入所有表,用于循环注册所有蓝图
- 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- 'underscore': to_underscore,
- 'get_import_path': GenUtils.get_import_path
- }
-
- # 使用Jinja2渲染模板
- template = Template(template_content)
- rendered_content = template.render(**context)
-
- # 生成文件名
- init_file_path = f"{module_path}/__init__.py"
-
- # 将渲染后的内容写入ZIP文件(只写入一次)
- if rendered_content.strip() and init_file_path not in added_files:
- zip_file.writestr(init_file_path, rendered_content)
- added_files.add(init_file_path)
- except Exception as e:
- print(f"处理模块 __init__.py 模板时出错: {e}")
-
- # 处理每个表
- for table in tables:
- # 根据表类型添加相应的Vue模板
- if table.tpl_category == 'tree':
- current_templates = core_templates + ['vue/index-tree.vue.vm']
- else:
- current_templates = core_templates + ['vue/index.vue.vm']
-
- # 处理每个核心模板文件
- for relative_path in current_templates:
- # 跳过模块根目录的 __init__.py,因为已经在开始时生成
- if relative_path == 'py/__init__.py.vm':
- continue
-
- template_path = os.path.join(template_dir, relative_path)
- if os.path.exists(template_path):
- # 读取模板内容
- try:
- with open(template_path, 'r', encoding='utf-8') as f:
- template_content = f.read()
-
- # 准备模板上下文
- # table.module_name 是从数据库读取的前端模块名(真正的模块名,用于权限、前端、SQL)
- # GeneratorConfig.python_model_name 是 Python 模块名(只用于 Python 后端代码路径)
- context = {
- 'table': table,
- 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- 'underscore': to_underscore, # 添加自定义过滤器
- 'get_import_path': GenUtils.get_import_path # 添加导入路径生成函数
- }
-
- # 使用Jinja2渲染模板
- template = Template(template_content)
- rendered_content = template.render(**context)
-
- # 生成文件名
- output_file_name = GenUtils.get_file_name(relative_path, table)
-
- # 收集目录路径和文件信息,用于生成 __init__.py
- # 跳过 sql 和 vue 目录,这些目录不需要 __init__.py
- dir_path = os.path.dirname(output_file_name)
- if dir_path and 'sql' not in dir_path and 'vue' not in dir_path:
- init_dirs.add(dir_path)
- # 同时收集父目录(但也要跳过 sql 和 vue)
- parts = dir_path.split('/')
- for i in range(1, len(parts)):
- parent_dir = '/'.join(parts[:i])
- if parent_dir and 'sql' not in parent_dir and 'vue' not in parent_dir:
- init_dirs.add(parent_dir)
-
- # 收集文件信息用于生成导入语句
- if dir_path not in dir_files:
- dir_files[dir_path] = []
-
- # 根据文件类型确定导入的类名
- if '/entity/' in output_file_name and '.py' in output_file_name:
- # Entity 文件在 domain/entity/ 目录下
- dir_files[dir_path].append(('entity', table.class_name, table))
- elif '/po/' in output_file_name and '_po.py' in output_file_name:
- # PO 文件在 domain/po/ 目录下
- dir_files[dir_path].append(('po', f"{to_underscore(table.class_name)}_po", table))
- elif '_service.py' in output_file_name:
- dir_files[dir_path].append(('service', f"{table.class_name}Service", table))
- elif '_mapper.py' in output_file_name:
- dir_files[dir_path].append(('mapper', f"{table.class_name}Mapper", table))
- elif '_controller.py' in output_file_name:
- dir_files[dir_path].append(('controller', 'gen', table))
-
- # 检查是否已添加同名文件
- if output_file_name in added_files:
- # 为重复文件添加序号
- name, ext = os.path.splitext(output_file_name)
- counter = 1
- new_name = f"{name}_{counter}{ext}"
- while new_name in added_files:
- counter += 1
- new_name = f"{name}_{counter}{ext}"
- output_file_name = new_name
-
- # 检查渲染后的内容是否为空
- if rendered_content.strip():
- # 将渲染后的内容写入ZIP文件
- zip_file.writestr(output_file_name, rendered_content)
- added_files.add(output_file_name)
- else:
- print(f"警告: 模板 {relative_path} 渲染后内容为空")
- except Exception as e:
- print(f"处理表 {table.table_name} 的模板 {relative_path} 时出错: {e}")
-
- # 为每个目录生成 __init__.py 文件,使其成为完整的 Python 模块
- for dir_path in sorted(init_dirs):
- # 跳过模块根目录,因为已经在开始时生成
- if dir_path == module_path:
- continue
-
- # 跳过 sql 和 vue 目录,这些目录不需要 __init__.py
- if 'sql' in dir_path or 'vue' in dir_path or dir_path.startswith('sql/') or dir_path.startswith('vue/'):
- continue
-
- init_file_path = os.path.join(dir_path, '__init__.py').replace('\\', '/')
-
- # 生成 __init__.py 内容
- init_lines = ["# -*- coding: utf-8 -*-"]
- init_lines.append(f"# @Module: {dir_path}")
- init_lines.append("")
-
- # 特殊处理 controller 目录:参考 ruoyi_generator/controller/__init__.py 的格式
- if 'controller' in dir_path and dir_path.endswith('/controller'):
- # 在 controller/__init__.py 中为每个 controller 创建蓝图
- if dir_path in dir_files:
- # 先导入 Blueprint
- init_lines.append("from flask import Blueprint")
- init_lines.append("")
-
- # 为每个 controller 创建蓝图
- for file_type, class_name, table_info in dir_files[dir_path]:
- if file_type == 'controller':
- blueprint_name = to_underscore(table_info.class_name)
- url_prefix = f"/{table_info.module_name}/{table_info.business_name}"
- init_lines.append(f"{blueprint_name} = Blueprint('{blueprint_name}', __name__, url_prefix='{url_prefix}')")
-
- # 导入各个 controller 模块
- init_lines.append("")
- init_lines.append("")
- for file_type, class_name, table_info in dir_files[dir_path]:
- if file_type == 'controller':
- controller_module_name = f"{to_underscore(table_info.class_name)}_controller"
- init_lines.append(f"from . import {controller_module_name}")
- else:
- # 其他目录正常生成导入语句
- if dir_path in dir_files:
- imports = []
- for file_type, class_name, table_info in dir_files[dir_path]:
- if file_type == 'entity':
- # Entity 文件在 domain/entity/ 目录下,导入时使用文件名
- entity_file_name = to_underscore(class_name)
- imports.append(f"from .{entity_file_name} import {class_name}")
- elif file_type == 'po':
- # PO 文件在 domain/po/ 目录下,导入时使用文件名
- po_file_name = class_name # class_name 已经是 address_info_po
- imports.append(f"from .{po_file_name} import {class_name}")
- elif file_type == 'service':
- imports.append(f"from .{to_underscore(class_name.replace('Service', ''))}_service import {class_name}")
- elif file_type == 'mapper':
- imports.append(f"from .{to_underscore(class_name.replace('Mapper', ''))}_mapper import {class_name}")
-
- if imports:
- init_lines.extend(sorted(set(imports)))
-
- init_content = "\n".join(init_lines) + "\n"
- zip_file.writestr(init_file_path, init_content)
-
- zip_buffer.seek(0)
- return zip_buffer
-
- @staticmethod
- def set_column_list_index(table: GenTable):
- """
- 为表的列设置 list_index 属性,用于 Vue 模板中的 columns 数组索引
-
- Args:
- table (GenTable): 表信息
- """
- if not table.columns:
- return
-
- list_index = 0
- for column in table.columns:
- if column.is_list == '1':
- # 使用 setattr 动态添加属性
- setattr(column, 'list_index', list_index)
- list_index += 1
- @staticmethod
- def preview_code(table: GenTable) -> dict:
- """
- 预览代码
-
- Args:
- table (GenTable): 表信息
-
- Returns:
- dict: 预览代码
- """
- # 设置列的 list_index 属性
- GenUtils.set_column_list_index(table)
-
- # 设置主键列
- pk_columns = [column for column in table.columns if column.is_pk == '1']
- if pk_columns:
- table.pk_column = pk_columns[0]
- else:
- table.pk_column = None
-
- # 从 options 中解析 parentMenuId
- if table.options:
- import json
- try:
- if isinstance(table.options, str):
- options_dict = json.loads(table.options)
- else:
- options_dict = table.options
- # 从 options 中提取 parentMenuId 并设置到 table
- if 'parentMenuId' in options_dict:
- table.parent_menu_id = options_dict.get('parentMenuId')
- except Exception as e:
- print(f"解析 options 字段出错: {e}")
-
- # 强制使用前端模块名(modelName),而不是 Python 模块名
- # module_name 必须使用 modelName(test),不能使用 pythonModelName(ruoyi_test)
- # 如果 module_name 是空的、等于 python_model_name 或包含 python_model_name,强制替换为 model_name
- original_module_name = table.module_name
- if not table.module_name or table.module_name == GeneratorConfig.python_model_name or (table.module_name and GeneratorConfig.python_model_name in table.module_name):
- table.module_name = GeneratorConfig.model_name
- if original_module_name != table.module_name:
- print(f"警告:table.module_name 从 '{original_module_name}' 强制替换为 '{table.module_name}'(前端模块名)")
-
- # 获取模板目录
- template_dir = os.path.join(os.path.dirname(__file__), 'vm')
-
- # 存储预览代码的字典
- preview_data = {}
-
- # 定义需要预览的核心模板文件
- core_templates = [
- 'py/entity.py.vm',
- 'py/po.py.vm',
- 'py/controller.py.vm',
- 'py/service.py.vm',
- 'py/mapper.py.vm',
- 'js/api.js.vm',
- 'sql/menu.sql.vm'
- ]
-
- # 根据表类型添加相应的Vue模板,但预览时都使用index.vue.vm作为文件名
- if table.tpl_category == 'tree':
- core_templates.append('vue/index-tree.vue.vm')
- else:
- core_templates.append('vue/index.vue.vm')
-
- # 处理每个核心模板文件
- for relative_path in core_templates:
- template_path = os.path.join(template_dir, relative_path)
- if os.path.exists(template_path):
- # 读取模板内容
- try:
- with open(template_path, 'r', encoding='utf-8') as f:
- template_content = f.read()
-
- # 准备模板上下文
- # table.module_name 是从数据库读取的前端模块名(真正的模块名,用于权限、前端、SQL)
- # GeneratorConfig.python_model_name 是 Python 模块名(只用于 Python 后端代码路径)
- context = {
- 'table': table,
- 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- 'underscore': to_underscore, # 添加自定义过滤器
- 'get_import_path': GenUtils.get_import_path # 添加导入路径生成函数
- }
-
- # 使用Jinja2渲染模板
- template = Template(template_content)
- rendered_content = template.render(**context)
-
- # 存储渲染后的内容
- preview_data[relative_path] = rendered_content
- except Exception as e:
- # 如果渲染失败,存储错误信息
- preview_data[relative_path] = f"模板渲染失败: {str(e)}"
- else:
- preview_data[relative_path] = "模板文件不存在"
-
- return preview_data
|