| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103 |
- 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: 下划线命名的字符串
- """
- # 处理 None 或非字符串类型
- if name is None:
- return ""
- if not isinstance(name, str):
- return str(name)
- # 在大写字母前添加下划线,然后转为小写
- s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
- return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
- def to_camel_case(name: str, pascal: bool = True) -> str:
- """
- 将下划线命名或普通字符串转换为驼峰命名。
- - name = "schedule_info" -> pascal=True => "ScheduleInfo"
- - name = "schedule_info" -> pascal=False => "scheduleInfo"
- - name = "ScheduleInfo" -> pascal=True/False => "ScheduleInfo"/"scheduleInfo"
- """
- if name is None:
- return ""
- if not isinstance(name, str):
- name = str(name)
- if not name:
- return ""
- # 如果本身已经是驼峰(包含大写且不含下划线),直接按需调整首字母
- if "_" not in name and any(ch.isupper() for ch in name):
- base = name[0].upper() + name[1:]
- else:
- parts = [p for p in name.split("_") if p]
- if not parts:
- return ""
- base = "".join(p.lower().capitalize() for p in parts)
- return base if pascal else (base[0].lower() + base[1:] if base else base)
- def capitalize_first(name: str) -> str:
- """
- 将字符串首字母大写
-
- Args:
- name (str): 输入字符串
-
- Returns:
- str: 首字母大写的字符串
- """
- if name is None or not isinstance(name, str) or len(name) == 0:
- return ""
- return name[0].upper() + name[1:] if len(name) > 1 else name.upper()
- def get_tree_column_index(column, all_columns):
- """
- 计算树表中列的索引(排除主键列)
-
- Args:
- column: 当前列对象
- all_columns: 所有列的列表
-
- Returns:
- int: 列在树表中的索引
- """
- index = 0
- for col in all_columns:
- if col.is_list == '1' and not (col.is_pk == '1'):
- # 使用 column_id 或 column_name 来比较列对象
- if hasattr(column, 'column_id') and hasattr(col, 'column_id'):
- if col.column_id == column.column_id:
- return index
- elif hasattr(column, 'column_name') and hasattr(col, 'column_name'):
- if col.column_name == column.column_name:
- return index
- index += 1
- return 0
- def get_filtered_columns(columns, filter_func):
- """
- 过滤列并返回过滤后的列表
-
- Args:
- columns: 所有列的列表
- filter_func: 过滤函数,接受一个列对象,返回True表示保留
-
- Returns:
- list: 过滤后的列列表
- """
- return [col for col in columns if filter_func(col)]
- 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 or 'vue/index-sub.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')
- elif table.tpl_category == 'sub':
- core_templates.append('vue/index-sub.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,
- 'get_tree_column_index': get_tree_column_index
- }
-
- # 使用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 后端代码路径)
- # 为树表准备过滤后的列列表
- if 'index-tree.vue' in relative_path:
- # 过滤出满足条件的列
- list_cols = [col for col in table.columns if col.is_list and not (col.is_pk == '1') and getattr(col, 'list_index', None) is not None]
- query_cols = [col for col in table.columns if col.is_query]
- required_cols = [col for col in table.columns if col.is_required]
- else:
- list_cols = None
- query_cols = None
- required_cols = None
-
- # 预计算双驼峰命名的类名,避免模板中重复调用
- class_name_pascal = to_camel_case(table.class_name)
-
- context = {
- 'table': table,
- 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- 'underscore': to_underscore, # 下划线命名工具
- 'capitalize_first': capitalize_first, # 首字母大写工具
- 'to_camel_case': to_camel_case, # 保留用于其他场景
- 'get_import_path': GenUtils.get_import_path, # 导入路径生成函数
- 'get_tree_column_index': get_tree_column_index, # 树表列索引计算函数
- 'list_cols': list_cols, # 树表的列表列
- 'query_cols': query_cols, # 树表的查询列
- 'required_cols': required_cols, # 树表的必填列
- 'class_name_pascal': class_name_pascal # 预计算的双驼峰类名
- }
-
- # 使用Jinja2渲染模板
- template = Template(template_content)
- # 如果是树表模板,打印列信息用于调试
- if 'index-tree.vue' in relative_path:
- print(f"[DEBUG] 渲染树表模板前,列信息:")
- for col in table.columns:
- list_idx = getattr(col, 'list_index', 'NOT_SET')
- print(f" - {col.column_name}: is_list={col.is_list}, is_pk={col.is_pk}, list_index={list_idx} (type={type(list_idx)})")
- try:
- rendered_content = template.render(**context)
- except Exception as e:
- import traceback
- error_detail = traceback.format_exc()
- print(f"[ERROR] 模板渲染失败: {relative_path}")
- print(f"[ERROR] 错误信息: {str(e)}")
- print(f"[ERROR] 详细堆栈:\n{error_detail}")
- raise
-
- # 如果是 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', to_camel_case(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", f"{table.class_name}Po"), table))
- elif '_service.py' in output_file_name:
- # Service 文件,类名使用双驼峰,文件名使用下划线
- dir_files[dir_path].append(('service', (f"{to_underscore(table.class_name)}_service", f"{table.class_name}Service"), table))
- elif '_mapper.py' in output_file_name:
- # Mapper 文件,类名使用双驼峰,文件名使用下划线
- dir_files[dir_path].append(('mapper', (f"{to_underscore(table.class_name)}_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_info, table_info in dir_files[dir_path]:
- if file_type == 'entity':
- # Entity 文件在 domain/entity/ 目录下,导入时使用文件名
- entity_file_name = to_underscore(class_name_info)
- imports.append(f"from .{entity_file_name} import {class_name_info}")
- elif file_type == 'po':
- # PO 文件在 domain/po/ 目录下,文件名和类名分开处理
- file_name, class_name = class_name_info
- imports.append(f"from .{file_name} import {class_name}")
- elif file_type == 'service':
- # Service 文件,文件名和类名分开处理
- file_name, class_name = class_name_info
- imports.append(f"from .{file_name} import {class_name}")
- elif file_type == 'mapper':
- # Mapper 文件,文件名和类名分开处理
- file_name, class_name = class_name_info
- imports.append(f"from .{file_name} 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,
- 'get_tree_column_index': get_tree_column_index
- }
-
- # 使用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']
- elif table.tpl_category == 'sub':
- current_templates = core_templates + ['vue/index-sub.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 后端代码路径)
- # 为树表准备过滤后的列列表
- if 'index-tree.vue' in relative_path:
- list_cols = [col for col in table.columns if col.is_list and not (col.is_pk == '1') and getattr(col, 'list_index', None) is not None]
- query_cols = [col for col in table.columns if col.is_query]
- required_cols = [col for col in table.columns if col.is_required]
- else:
- list_cols = None
- query_cols = None
- required_cols = None
-
- # 预计算双驼峰命名的类名,避免模板中重复调用
- class_name_pascal = to_camel_case(table.class_name)
-
- context = {
- 'table': table,
- 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- 'underscore': to_underscore, # 下划线命名工具
- 'capitalize_first': capitalize_first, # 首字母大写工具
- 'to_camel_case': to_camel_case, # 保留用于其他场景
- 'get_import_path': GenUtils.get_import_path, # 导入路径生成函数
- 'list_cols': list_cols, # 树表的列表列
- 'query_cols': query_cols, # 树表的查询列
- 'required_cols': required_cols, # 树表的必填列
- 'class_name_pascal': class_name_pascal # 预计算的双驼峰类名
- }
-
- # 使用Jinja2渲染模板
- template = Template(template_content)
- # 如果是树表模板,打印列信息用于调试
- if 'index-tree.vue' in relative_path:
- print(f"[DEBUG] 批量渲染树表模板前,列信息:")
- for col in table.columns:
- list_idx = getattr(col, 'list_index', 'NOT_SET')
- print(f" - {col.column_name}: is_list={col.is_list}, is_pk={col.is_pk}, list_index={list_idx} (type={type(list_idx)})")
- try:
- rendered_content = template.render(**context)
- except Exception as e:
- import traceback
- error_detail = traceback.format_exc()
- print(f"[ERROR] 模板渲染失败: {relative_path} (表: {table.table_name})")
- print(f"[ERROR] 错误信息: {str(e)}")
- print(f"[ERROR] 详细堆栈:\n{error_detail}")
- raise
-
- # 生成文件名
- 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', to_camel_case(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", f"{to_camel_case(table.class_name)}Po"), table))
- elif '_service.py' in output_file_name:
- # Service 文件,类名使用双驼峰,文件名使用下划线
- dir_files[dir_path].append(('service', (f"{to_underscore(table.class_name)}_service", f"{to_camel_case(table.class_name)}Service"), table))
- elif '_mapper.py' in output_file_name:
- # Mapper 文件,类名使用双驼峰,文件名使用下划线
- dir_files[dir_path].append(('mapper', (f"{to_underscore(table.class_name)}_mapper", f"{to_camel_case(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_info, table_info in dir_files[dir_path]:
- if file_type == 'entity':
- # Entity 文件在 domain/entity/ 目录下,导入时使用文件名
- entity_file_name = to_underscore(class_name_info)
- imports.append(f"from .{entity_file_name} import {class_name_info}")
- elif file_type == 'po':
- # PO 文件在 domain/po/ 目录下,文件名和类名分开处理
- file_name, class_name = class_name_info
- imports.append(f"from .{file_name} import {class_name}")
- elif file_type == 'service':
- # Service 文件,文件名和类名分开处理
- file_name, class_name = class_name_info
- imports.append(f"from .{file_name} import {class_name}")
- elif file_type == 'mapper':
- # Mapper 文件,文件名和类名分开处理
- file_name, class_name = class_name_info
- imports.append(f"from .{file_name} 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
-
- # 如果是树表,需要排除主键列
- is_tree = table.tpl_category == 'tree'
- print(f"[DEBUG] set_column_list_index: 表类型={table.tpl_category}, 是树表={is_tree}, 列数={len(table.columns)}")
-
- list_index = 0
- for column in table.columns:
- # 对于树表,只处理 is_list='1' 且不是主键的列
- # 对于普通表,处理所有 is_list='1' 的列
- if column.is_list == '1' or column.is_list == 1:
- if is_tree and (column.is_pk == '1' or column.is_pk == 1):
- # 树表的主键列不设置 list_index(树表中主键列不显示在列表中)
- # 设置为 None,但模板中会通过 not (column.is_pk == '1') 过滤掉
- setattr(column, 'list_index', None)
- print(f"[DEBUG] 树表主键列: {column.column_name}, is_list={column.is_list}, is_pk={column.is_pk}, list_index=None")
- continue
- # 使用 setattr 动态添加属性
- setattr(column, 'list_index', list_index)
- print(f"[DEBUG] 设置列索引: {column.column_name}, is_list={column.is_list}, is_pk={column.is_pk}, list_index={list_index}")
- list_index += 1
- else:
- # 非列表列也设置 list_index 为 None,保持一致性
- setattr(column, 'list_index', None)
- print(f"[DEBUG] 非列表列: {column.column_name}, is_list={column.is_list}, list_index=None")
- @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 后端代码路径)
- # 为树表准备过滤后的列列表
- if 'index-tree.vue' in relative_path:
- list_cols = [col for col in table.columns if col.is_list and not (col.is_pk == '1') and getattr(col, 'list_index', None) is not None]
- query_cols = [col for col in table.columns if col.is_query]
- required_cols = [col for col in table.columns if col.is_required]
- else:
- list_cols = None
- query_cols = None
- required_cols = None
-
- # 预计算双驼峰命名的类名,避免模板中重复调用
- class_name_pascal = to_camel_case(table.class_name)
-
- context = {
- 'table': table,
- 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- 'underscore': to_underscore, # 下划线命名工具
- 'capitalize_first': capitalize_first, # 首字母大写工具
- 'to_camel_case': to_camel_case, # 保留用于其他场景
- 'get_import_path': GenUtils.get_import_path, # 导入路径生成函数
- 'list_cols': list_cols, # 树表的列表列
- 'query_cols': query_cols, # 树表的查询列
- 'required_cols': required_cols, # 树表的必填列
- 'class_name_pascal': class_name_pascal # 预计算的双驼峰类名
- }
-
- # 使用Jinja2渲染模板
- template = Template(template_content)
- # 如果是树表模板,打印列信息用于调试
- if 'index-tree.vue' in relative_path:
- print(f"[DEBUG] 预览树表模板前,列信息:")
- for col in table.columns:
- list_idx = getattr(col, 'list_index', 'NOT_SET')
- print(f" - {col.column_name}: is_list={col.is_list}, is_pk={col.is_pk}, list_index={list_idx} (type={type(list_idx)})")
- rendered_content = template.render(**context)
-
- # 存储渲染后的内容
- preview_data[relative_path] = rendered_content
- except Exception as e:
- # 如果渲染失败,存储错误信息
- import traceback
- error_detail = traceback.format_exc()
- print(f"[ERROR] 模板渲染失败: {relative_path}")
- print(f"[ERROR] 错误信息: {str(e)}")
- print(f"[ERROR] 详细堆栈:\n{error_detail}")
- preview_data[relative_path] = f"模板渲染失败: {str(e)}\n详细错误:\n{error_detail}"
- else:
- preview_data[relative_path] = "模板文件不存在"
-
- return preview_data
|