util.py 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049
  1. import os
  2. from typing import List
  3. from jinja2 import Template
  4. from ruoyi_common.utils import StringUtil
  5. from ruoyi_generator.domain.entity import GenTable
  6. from ruoyi_generator.config import GeneratorConfig
  7. import zipfile
  8. from io import BytesIO
  9. from datetime import datetime
  10. import re
  11. def to_underscore(name: str) -> str:
  12. """
  13. 将驼峰命名转换为下划线命名
  14. Args:
  15. name (str): 驼峰命名的字符串
  16. Returns:
  17. str: 下划线命名的字符串
  18. """
  19. # 处理 None 或非字符串类型
  20. if name is None:
  21. return ""
  22. if not isinstance(name, str):
  23. return str(name)
  24. # 在大写字母前添加下划线,然后转为小写
  25. s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
  26. return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
  27. def capitalize_first(name: str) -> str:
  28. """
  29. 将字符串首字母大写
  30. Args:
  31. name (str): 输入字符串
  32. Returns:
  33. str: 首字母大写的字符串
  34. """
  35. if name is None or not isinstance(name, str) or len(name) == 0:
  36. return ""
  37. return name[0].upper() + name[1:] if len(name) > 1 else name.upper()
  38. def get_tree_column_index(column, all_columns):
  39. """
  40. 计算树表中列的索引(排除主键列)
  41. Args:
  42. column: 当前列对象
  43. all_columns: 所有列的列表
  44. Returns:
  45. int: 列在树表中的索引
  46. """
  47. index = 0
  48. for col in all_columns:
  49. if col.is_list == '1' and not (col.is_pk == '1'):
  50. # 使用 column_id 或 column_name 来比较列对象
  51. if hasattr(column, 'column_id') and hasattr(col, 'column_id'):
  52. if col.column_id == column.column_id:
  53. return index
  54. elif hasattr(column, 'column_name') and hasattr(col, 'column_name'):
  55. if col.column_name == column.column_name:
  56. return index
  57. index += 1
  58. return 0
  59. def get_filtered_columns(columns, filter_func):
  60. """
  61. 过滤列并返回过滤后的列表
  62. Args:
  63. columns: 所有列的列表
  64. filter_func: 过滤函数,接受一个列对象,返回True表示保留
  65. Returns:
  66. list: 过滤后的列列表
  67. """
  68. return [col for col in columns if filter_func(col)]
  69. class GenUtils:
  70. @staticmethod
  71. def get_file_name(template_file: str, table: GenTable) -> str:
  72. """
  73. 根据模板文件名和表信息生成文件名
  74. Args:
  75. template_file (str): 模板文件名
  76. table (GenTable): 表信息
  77. Returns:
  78. str: 生成的文件名
  79. """
  80. # 标准化路径分隔符
  81. template_file = template_file.replace('\\', '/')
  82. # 移除.vm后缀
  83. base_name = template_file[:-3] if template_file.endswith('.vm') else template_file
  84. # 确定模块路径:后端代码使用 pythonModelName,前端代码使用 modelName
  85. if table.package_name:
  86. # 使用 package_name 作为路径(如 com.yy.test -> com/yy/test)
  87. module_path = table.package_name.replace('.', '/')
  88. else:
  89. # 如果 package_name 为空,使用 pythonModelName 作为后端模块名
  90. # 注意:这里只用于后端 Python 代码路径,前端代码路径在下面单独处理
  91. module_path = GeneratorConfig.python_model_name if hasattr(GeneratorConfig, 'python_model_name') else (table.module_name if table.module_name else 'ruoyi_generator')
  92. # 根据模板类型生成文件名和路径
  93. if 'py/entity.py' in template_file:
  94. # Entity文件放在 domain/entity/ 目录下,使用下划线命名法
  95. entity_name = to_underscore(table.class_name)
  96. return f"{module_path}/domain/entity/{entity_name}.py"
  97. elif 'py/controller.py' in template_file:
  98. # 使用下划线命名法
  99. controller_name = f"{to_underscore(table.class_name)}_controller"
  100. return f"{module_path}/controller/{controller_name}.py"
  101. elif 'py/service.py' in template_file:
  102. # 使用下划线命名法
  103. service_name = f"{to_underscore(table.class_name)}_service"
  104. return f"{module_path}/service/{service_name}.py"
  105. elif 'py/mapper.py' in template_file:
  106. # 使用下划线命名法
  107. mapper_name = f"{to_underscore(table.class_name)}_mapper"
  108. return f"{module_path}/mapper/{mapper_name}.py"
  109. elif 'py/po.py' in template_file:
  110. # PO文件放在 domain/po/ 目录下,使用下划线命名法
  111. po_name = f"{to_underscore(table.class_name)}_po"
  112. return f"{module_path}/domain/po/{po_name}.py"
  113. elif 'vue/index.vue' in template_file or 'vue/index-tree.vue' in template_file or 'vue/index-sub.vue' in template_file:
  114. # 无论是树表还是普通表,Vue文件名都是index.vue
  115. # 使用数据库中的 module_name(前端模块名)
  116. frontend_module = table.module_name if table.module_name else GeneratorConfig.model_name
  117. return f"vue/views/{frontend_module}/{table.business_name}/index.vue"
  118. elif 'js/api.js' in template_file:
  119. # 使用数据库中的 module_name(前端模块名)
  120. frontend_module = table.module_name if table.module_name else GeneratorConfig.model_name
  121. return f"vue/api/{frontend_module}/{table.business_name}.js"
  122. elif 'py/__init__.py' in template_file:
  123. # 模块根目录的 __init__.py
  124. return f"{module_path}/__init__.py"
  125. elif 'sql/menu.sql' in template_file:
  126. return f"sql/{table.business_name}_menu.sql"
  127. elif 'README.md' in template_file:
  128. return f"{table.business_name}_README.md"
  129. else:
  130. # 处理其他模板文件,保持原有目录结构
  131. filename = os.path.basename(base_name)
  132. if '.' not in filename:
  133. filename += '.py' # 默认添加.py扩展名
  134. return filename
  135. @staticmethod
  136. def get_table_prefix() -> str:
  137. """
  138. 获取表前缀
  139. Returns:
  140. str: 表前缀
  141. """
  142. return GeneratorConfig.table_prefix or ""
  143. @staticmethod
  144. def remove_table_prefix(table_name: str) -> str:
  145. """
  146. 移除表前缀
  147. Args:
  148. table_name (str): 表名
  149. Returns:
  150. str: 移除前缀后的表名
  151. """
  152. prefix = GenUtils.get_table_prefix()
  153. if prefix and table_name.startswith(prefix):
  154. return table_name[len(prefix):]
  155. return table_name
  156. @staticmethod
  157. def table_to_class_name(table_name: str) -> str:
  158. """
  159. 将表名转换为类名
  160. Args:
  161. table_name (str): 表名
  162. Returns:
  163. str: 类名
  164. """
  165. # 移除表前缀
  166. clean_table_name = GenUtils.remove_table_prefix(table_name)
  167. # 转换为驼峰命名
  168. return GenUtils.to_camel_case(clean_table_name)
  169. @staticmethod
  170. def get_business_name(table_name: str) -> str:
  171. """
  172. 获取业务名
  173. Args:
  174. table_name (str): 表名
  175. Returns:
  176. str: 业务名
  177. """
  178. # 移除表前缀
  179. clean_table_name = GenUtils.remove_table_prefix(table_name)
  180. # 获取下划线分隔的第一部分
  181. return GenUtils.substring_before(clean_table_name, "_") if "_" in clean_table_name else clean_table_name
  182. @staticmethod
  183. def get_import_path(package_name: str, module_name: str, module_type: str, class_name: str = None) -> str:
  184. """
  185. 生成导入路径
  186. Args:
  187. package_name (str): 包名,如 "com.yy.project" 或空字符串
  188. module_name (str): 前端模块名(从数据库读取,用于前端代码),但这里应该传入 pythonModelName
  189. module_type (str): 模块类型,如 "domain", "service", "mapper", "controller"
  190. class_name (str): 类名(可选,用于PO导入)
  191. Returns:
  192. str: 导入路径,Python包名保持点分隔格式
  193. """
  194. # Python 后端代码使用 pythonModelName,而不是前端模块名
  195. # 如果 package_name 为空,使用 pythonModelName 作为 Python 模块名
  196. if not package_name:
  197. # 使用 pythonModelName 作为 Python 模块名
  198. python_package = GeneratorConfig.python_model_name if hasattr(GeneratorConfig, 'python_model_name') else (module_name if module_name else 'ruoyi_generator')
  199. else:
  200. # Python导入路径使用点分隔,保持原样
  201. # 例如: "com.yy.project" -> "com.yy.project"
  202. python_package = package_name
  203. # 生成导入路径
  204. if module_type == "domain" and class_name:
  205. # PO 文件在 domain/po/ 目录下
  206. return f"{python_package}.domain.po"
  207. elif module_type == "domain":
  208. return f"{python_package}.domain.entity"
  209. else:
  210. return f"{python_package}.{module_type}"
  211. @staticmethod
  212. def to_camel_case(name: str) -> str:
  213. """
  214. 将下划线命名转换为驼峰命名
  215. Args:
  216. name (str): 下划线命名
  217. Returns:
  218. str: 驼峰命名
  219. """
  220. if hasattr(StringUtil, 'to_camel_case'):
  221. return StringUtil.to_camel_case(name)
  222. # 如果StringUtil没有to_camel_case方法,则手动实现
  223. parts = name.split('_')
  224. if len(parts) == 1:
  225. return parts[0]
  226. return parts[0] + ''.join(word.capitalize() for word in parts[1:])
  227. @staticmethod
  228. def substring_before(string: str, separator: str) -> str:
  229. """
  230. 获取字符串中分隔符之前的部分
  231. Args:
  232. string (str): 输入字符串
  233. separator (str): 分隔符
  234. Returns:
  235. str: 分隔符之前的部分
  236. """
  237. if hasattr(StringUtil, 'substring_before'):
  238. return StringUtil.substring_before(string, separator)
  239. # 如果StringUtil没有substring_before方法,则手动实现
  240. if separator in string:
  241. return string.split(separator, 1)[0]
  242. return string
  243. @staticmethod
  244. def substring_after(string: str, separator: str) -> str:
  245. """
  246. 获取字符串中分隔符之后的部分
  247. Args:
  248. string (str): 输入字符串
  249. separator (str): 分隔符
  250. Returns:
  251. str: 分隔符之后的部分
  252. """
  253. if hasattr(StringUtil, 'substring_after'):
  254. return StringUtil.substring_after(string, separator)
  255. # 如果StringUtil没有substring_after方法,则手动实现
  256. if separator in string:
  257. return string.split(separator, 1)[1]
  258. return ""
  259. @staticmethod
  260. def generator_code(table: GenTable) -> BytesIO:
  261. """
  262. 生成代码
  263. Args:
  264. table (GenTable): 表信息
  265. Returns:
  266. BytesIO: 生成的代码文件
  267. """
  268. # 设置列的 list_index 属性
  269. GenUtils.set_column_list_index(table)
  270. # 设置主键列
  271. pk_columns = [column for column in table.columns if column.is_pk == '1']
  272. if pk_columns:
  273. table.pk_column = pk_columns[0]
  274. else:
  275. table.pk_column = None
  276. # 从 options 中解析 parentMenuId
  277. if table.options:
  278. import json
  279. try:
  280. if isinstance(table.options, str):
  281. options_dict = json.loads(table.options)
  282. else:
  283. options_dict = table.options
  284. # 从 options 中提取 parentMenuId 并设置到 table
  285. if 'parentMenuId' in options_dict:
  286. table.parent_menu_id = options_dict.get('parentMenuId')
  287. except Exception as e:
  288. print(f"解析 options 字段出错: {e}")
  289. # 强制使用前端模块名(modelName),而不是 Python 模块名
  290. # module_name 必须使用 modelName(test),不能使用 pythonModelName(ruoyi_test)
  291. # 如果 module_name 是空的、等于 python_model_name 或包含 python_model_name,强制替换为 model_name
  292. original_module_name = table.module_name
  293. 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):
  294. table.module_name = GeneratorConfig.model_name
  295. if original_module_name != table.module_name:
  296. print(f"警告:table.module_name 从 '{original_module_name}' 强制替换为 '{table.module_name}'(前端模块名)")
  297. # 获取模板目录
  298. template_dir = os.path.join(os.path.dirname(__file__), 'vm')
  299. # 定义核心模板文件
  300. core_templates = [
  301. 'py/entity.py.vm',
  302. 'py/po.py.vm',
  303. 'py/controller.py.vm',
  304. 'py/service.py.vm',
  305. 'py/mapper.py.vm',
  306. 'js/api.js.vm',
  307. 'sql/menu.sql.vm'
  308. ]
  309. # 根据表类型添加相应的Vue模板
  310. if table.tpl_category == 'tree':
  311. core_templates.append('vue/index-tree.vue.vm')
  312. elif table.tpl_category == 'sub':
  313. core_templates.append('vue/index-sub.vue.vm')
  314. else:
  315. core_templates.append('vue/index.vue.vm')
  316. # 创建内存中的ZIP文件
  317. zip_buffer = BytesIO()
  318. # 确定模块路径,用于生成 __init__.py
  319. # 后端 Python 代码使用 pythonModelName,而不是前端模块名
  320. if table.package_name:
  321. module_path = table.package_name.replace('.', '/')
  322. else:
  323. # 使用 pythonModelName 作为后端模块路径
  324. module_path = GeneratorConfig.python_model_name if hasattr(GeneratorConfig, 'python_model_name') else (table.module_name if table.module_name else 'ruoyi_generator')
  325. # 收集需要生成 __init__.py 的目录和文件信息
  326. init_dirs = set()
  327. # 收集每个目录下的文件,用于生成导入语句
  328. dir_files = {} # {dir_path: [file_info]}
  329. with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
  330. # 跟踪已添加的文件名以避免重复
  331. added_files = set()
  332. # 首先生成模块根目录的 __init__.py
  333. init_template_path = os.path.join(template_dir, 'py/__init__.py.vm')
  334. if os.path.exists(init_template_path):
  335. try:
  336. with open(init_template_path, 'r', encoding='utf-8') as f:
  337. template_content = f.read()
  338. # 准备模板上下文(单个表时,tables 为 None)
  339. context = {
  340. 'table': table,
  341. 'tables': None, # 单个表生成时,tables 为 None
  342. 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
  343. 'underscore': to_underscore,
  344. 'get_import_path': GenUtils.get_import_path,
  345. 'get_tree_column_index': get_tree_column_index
  346. }
  347. # 使用Jinja2渲染模板
  348. template = Template(template_content)
  349. rendered_content = template.render(**context)
  350. # 生成文件名
  351. init_file_path = f"{module_path}/__init__.py"
  352. # 将渲染后的内容写入ZIP文件
  353. if rendered_content.strip():
  354. zip_file.writestr(init_file_path, rendered_content)
  355. added_files.add(init_file_path)
  356. except Exception as e:
  357. print(f"处理模块 __init__.py 模板时出错: {e}")
  358. # 处理每个核心模板文件
  359. for relative_path in core_templates:
  360. template_path = os.path.join(template_dir, relative_path)
  361. if os.path.exists(template_path):
  362. # 读取模板内容
  363. try:
  364. with open(template_path, 'r', encoding='utf-8') as f:
  365. template_content = f.read()
  366. # 准备模板上下文
  367. # table.module_name 是从数据库读取的前端模块名(真正的模块名,用于权限、前端、SQL)
  368. # GeneratorConfig.python_model_name 是 Python 模块名(只用于 Python 后端代码路径)
  369. # 为树表准备过滤后的列列表
  370. if 'index-tree.vue' in relative_path:
  371. # 过滤出满足条件的列
  372. 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]
  373. query_cols = [col for col in table.columns if col.is_query]
  374. required_cols = [col for col in table.columns if col.is_required]
  375. else:
  376. list_cols = None
  377. query_cols = None
  378. required_cols = None
  379. context = {
  380. 'table': table,
  381. 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
  382. 'underscore': to_underscore, # 添加自定义过滤器
  383. 'capitalize_first': capitalize_first, # 添加首字母大写函数
  384. 'get_import_path': GenUtils.get_import_path, # 添加导入路径生成函数
  385. 'get_tree_column_index': get_tree_column_index, # 添加树表列索引计算函数
  386. 'list_cols': list_cols, # 树表的列表列
  387. 'query_cols': query_cols, # 树表的查询列
  388. 'required_cols': required_cols # 树表的必填列
  389. }
  390. # 使用Jinja2渲染模板
  391. template = Template(template_content)
  392. # 如果是树表模板,打印列信息用于调试
  393. if 'index-tree.vue' in relative_path:
  394. print(f"[DEBUG] 渲染树表模板前,列信息:")
  395. for col in table.columns:
  396. list_idx = getattr(col, 'list_index', 'NOT_SET')
  397. print(f" - {col.column_name}: is_list={col.is_list}, is_pk={col.is_pk}, list_index={list_idx} (type={type(list_idx)})")
  398. try:
  399. rendered_content = template.render(**context)
  400. except Exception as e:
  401. import traceback
  402. error_detail = traceback.format_exc()
  403. print(f"[ERROR] 模板渲染失败: {relative_path}")
  404. print(f"[ERROR] 错误信息: {str(e)}")
  405. print(f"[ERROR] 详细堆栈:\n{error_detail}")
  406. raise
  407. # 如果是 SQL 模板,恢复原始 module_name(虽然已经强制设置了,但为了安全)
  408. if 'sql/menu.sql' in relative_path:
  409. pass # 已经强制设置为 model_name,不需要恢复
  410. # 生成文件名
  411. output_file_name = GenUtils.get_file_name(relative_path, table)
  412. # 收集目录路径和文件信息,用于生成 __init__.py
  413. # 跳过 sql 和 vue 目录,这些目录不需要 __init__.py
  414. dir_path = os.path.dirname(output_file_name)
  415. if dir_path and 'sql' not in dir_path and 'vue' not in dir_path:
  416. init_dirs.add(dir_path)
  417. # 同时收集父目录(但也要跳过 sql 和 vue)
  418. parts = dir_path.split('/')
  419. for i in range(1, len(parts)):
  420. parent_dir = '/'.join(parts[:i])
  421. if parent_dir and 'sql' not in parent_dir and 'vue' not in parent_dir:
  422. init_dirs.add(parent_dir)
  423. # 收集文件信息用于生成导入语句
  424. file_name = os.path.basename(output_file_name)
  425. file_base = os.path.splitext(file_name)[0]
  426. if dir_path not in dir_files:
  427. dir_files[dir_path] = []
  428. # 根据文件类型确定导入的类名
  429. if '/entity/' in output_file_name and '.py' in output_file_name:
  430. # Entity 文件在 domain/entity/ 目录下
  431. dir_files[dir_path].append(('entity', table.class_name, table))
  432. elif '/po/' in output_file_name and '_po.py' in output_file_name:
  433. # PO 文件在 domain/po/ 目录下
  434. dir_files[dir_path].append(('po', f"{to_underscore(table.class_name)}_po", table))
  435. elif '_service.py' in output_file_name:
  436. dir_files[dir_path].append(('service', f"{table.class_name}Service", table))
  437. elif '_mapper.py' in output_file_name:
  438. dir_files[dir_path].append(('mapper', f"{table.class_name}Mapper", table))
  439. elif '_controller.py' in output_file_name:
  440. dir_files[dir_path].append(('controller', 'gen', table))
  441. # 检查渲染后的内容是否为空
  442. if rendered_content.strip():
  443. # 将渲染后的内容写入ZIP文件
  444. zip_file.writestr(output_file_name, rendered_content)
  445. else:
  446. print(f"警告: 模板 {relative_path} 渲染后内容为空")
  447. except Exception as e:
  448. print(f"处理模板 {relative_path} 时出错: {e}")
  449. # 为每个目录生成 __init__.py 文件,使其成为完整的 Python 模块
  450. for dir_path in sorted(init_dirs):
  451. # 跳过模块根目录,因为已经在开始时生成
  452. if dir_path == module_path:
  453. continue
  454. # 跳过 sql 和 vue 目录,这些目录不需要 __init__.py
  455. if 'sql' in dir_path or 'vue' in dir_path or dir_path.startswith('sql/') or dir_path.startswith('vue/'):
  456. continue
  457. init_file_path = os.path.join(dir_path, '__init__.py').replace('\\', '/')
  458. # 生成 __init__.py 内容
  459. init_lines = ["# -*- coding: utf-8 -*-"]
  460. init_lines.append(f"# @Module: {dir_path}")
  461. init_lines.append("")
  462. # 特殊处理 controller 目录:参考 ruoyi_generator/controller/__init__.py 的格式
  463. if 'controller' in dir_path and dir_path.endswith('/controller'):
  464. # 在 controller/__init__.py 中为每个 controller 创建蓝图
  465. if dir_path in dir_files:
  466. # 先导入 Blueprint
  467. init_lines.append("from flask import Blueprint")
  468. init_lines.append("")
  469. # 为每个 controller 创建蓝图
  470. for file_type, class_name, table_info in dir_files[dir_path]:
  471. if file_type == 'controller':
  472. blueprint_name = to_underscore(table_info.class_name)
  473. url_prefix = f"/{table_info.module_name}/{table_info.business_name}"
  474. init_lines.append(f"{blueprint_name} = Blueprint('{blueprint_name}', __name__, url_prefix='{url_prefix}')")
  475. # 导入各个 controller 模块
  476. init_lines.append("")
  477. init_lines.append("")
  478. for file_type, class_name, table_info in dir_files[dir_path]:
  479. if file_type == 'controller':
  480. controller_module_name = f"{to_underscore(table_info.class_name)}_controller"
  481. init_lines.append(f"from . import {controller_module_name}")
  482. else:
  483. # 其他目录正常生成导入语句
  484. if dir_path in dir_files:
  485. imports = []
  486. for file_type, class_name, table_info in dir_files[dir_path]:
  487. if file_type == 'entity':
  488. # Entity 文件在 domain/entity/ 目录下,导入时使用文件名
  489. entity_file_name = to_underscore(class_name)
  490. imports.append(f"from .{entity_file_name} import {class_name}")
  491. elif file_type == 'po':
  492. # PO 文件在 domain/po/ 目录下,导入时使用文件名
  493. po_file_name = class_name # class_name 已经是 address_info_po
  494. imports.append(f"from .{po_file_name} import {class_name}")
  495. elif file_type == 'service':
  496. imports.append(f"from .{to_underscore(class_name.replace('Service', ''))}_service import {class_name}")
  497. elif file_type == 'mapper':
  498. imports.append(f"from .{to_underscore(class_name.replace('Mapper', ''))}_mapper import {class_name}")
  499. if imports:
  500. init_lines.extend(sorted(set(imports)))
  501. init_content = "\n".join(init_lines) + "\n"
  502. zip_file.writestr(init_file_path, init_content)
  503. zip_buffer.seek(0)
  504. return zip_buffer
  505. @staticmethod
  506. def batch_generator_code(tables: List[GenTable]) -> BytesIO:
  507. """
  508. 批量生成代码
  509. Args:
  510. tables (List[GenTable]): 表列表
  511. Returns:
  512. BytesIO: 生成的代码文件
  513. """
  514. # 为每个表设置列的 list_index 属性和主键列
  515. for table in tables:
  516. GenUtils.set_column_list_index(table)
  517. # 设置主键列
  518. pk_columns = [column for column in table.columns if column.is_pk == '1']
  519. if pk_columns:
  520. table.pk_column = pk_columns[0]
  521. else:
  522. table.pk_column = None
  523. # 从 options 中解析 parentMenuId
  524. if table.options:
  525. import json
  526. try:
  527. if isinstance(table.options, str):
  528. options_dict = json.loads(table.options)
  529. else:
  530. options_dict = table.options
  531. # 从 options 中提取 parentMenuId 并设置到 table
  532. if 'parentMenuId' in options_dict:
  533. table.parent_menu_id = options_dict.get('parentMenuId')
  534. except Exception as e:
  535. print(f"解析 options 字段出错: {e}")
  536. # 强制使用前端模块名(modelName),而不是 Python 模块名
  537. # module_name 必须使用 modelName(test),不能使用 pythonModelName(ruoyi_test)
  538. if not table.module_name or table.module_name == GeneratorConfig.python_model_name or table.module_name.strip() == GeneratorConfig.python_model_name:
  539. table.module_name = GeneratorConfig.model_name
  540. # 额外检查:如果 module_name 等于 python_model_name,强制替换
  541. if table.module_name == GeneratorConfig.python_model_name:
  542. table.module_name = GeneratorConfig.model_name
  543. # 定义核心模板文件(每个表都会生成,但 __init__.py 只生成一次)
  544. core_templates = [
  545. 'py/entity.py.vm',
  546. 'py/po.py.vm',
  547. 'py/controller.py.vm',
  548. 'py/service.py.vm',
  549. 'py/mapper.py.vm',
  550. 'js/api.js.vm',
  551. 'sql/menu.sql.vm'
  552. ]
  553. # 创建内存中的ZIP文件
  554. zip_buffer = BytesIO()
  555. # 获取模板目录
  556. template_dir = os.path.join(os.path.dirname(__file__), 'vm')
  557. # 确定模块路径(使用第一个表的模块路径)
  558. if tables and len(tables) > 0:
  559. first_table = tables[0]
  560. if first_table.package_name:
  561. module_path = first_table.package_name.replace('.', '/')
  562. else:
  563. # 使用 pythonModelName 作为后端模块路径
  564. 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')
  565. else:
  566. module_path = GeneratorConfig.python_model_name if hasattr(GeneratorConfig, 'python_model_name') else 'ruoyi_generator'
  567. # 收集需要生成 __init__.py 的目录和文件信息
  568. init_dirs = set()
  569. # 收集每个目录下的文件,用于生成导入语句 {dir_path: [(file_type, class_name, table)]}
  570. dir_files = {}
  571. with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
  572. # 跟踪已添加的文件名以避免重复
  573. added_files = set()
  574. # 首先为模块根目录生成 __init__.py(只生成一次,使用第一个表的信息)
  575. if tables and len(tables) > 0:
  576. init_template_path = os.path.join(template_dir, 'py/__init__.py.vm')
  577. if os.path.exists(init_template_path):
  578. try:
  579. with open(init_template_path, 'r', encoding='utf-8') as f:
  580. template_content = f.read()
  581. # 准备模板上下文(批量生成时,传入所有表)
  582. # table.module_name 是从数据库读取的前端模块名(真正的模块名,用于权限、前端、SQL)
  583. # GeneratorConfig.python_model_name 是 Python 模块名(只用于 Python 后端代码路径)
  584. context = {
  585. 'table': first_table, # 用于兼容模板中的 table 变量
  586. 'tables': tables, # 传入所有表,用于循环注册所有蓝图
  587. 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
  588. 'underscore': to_underscore,
  589. 'get_import_path': GenUtils.get_import_path,
  590. 'get_tree_column_index': get_tree_column_index
  591. }
  592. # 使用Jinja2渲染模板
  593. template = Template(template_content)
  594. rendered_content = template.render(**context)
  595. # 生成文件名
  596. init_file_path = f"{module_path}/__init__.py"
  597. # 将渲染后的内容写入ZIP文件(只写入一次)
  598. if rendered_content.strip() and init_file_path not in added_files:
  599. zip_file.writestr(init_file_path, rendered_content)
  600. added_files.add(init_file_path)
  601. except Exception as e:
  602. print(f"处理模块 __init__.py 模板时出错: {e}")
  603. # 处理每个表
  604. for table in tables:
  605. # 根据表类型添加相应的Vue模板
  606. if table.tpl_category == 'tree':
  607. current_templates = core_templates + ['vue/index-tree.vue.vm']
  608. elif table.tpl_category == 'sub':
  609. current_templates = core_templates + ['vue/index-sub.vue.vm']
  610. else:
  611. current_templates = core_templates + ['vue/index.vue.vm']
  612. # 处理每个核心模板文件
  613. for relative_path in current_templates:
  614. # 跳过模块根目录的 __init__.py,因为已经在开始时生成
  615. if relative_path == 'py/__init__.py.vm':
  616. continue
  617. template_path = os.path.join(template_dir, relative_path)
  618. if os.path.exists(template_path):
  619. # 读取模板内容
  620. try:
  621. with open(template_path, 'r', encoding='utf-8') as f:
  622. template_content = f.read()
  623. # 准备模板上下文
  624. # table.module_name 是从数据库读取的前端模块名(真正的模块名,用于权限、前端、SQL)
  625. # GeneratorConfig.python_model_name 是 Python 模块名(只用于 Python 后端代码路径)
  626. # 为树表准备过滤后的列列表
  627. if 'index-tree.vue' in relative_path:
  628. 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]
  629. query_cols = [col for col in table.columns if col.is_query]
  630. required_cols = [col for col in table.columns if col.is_required]
  631. else:
  632. list_cols = None
  633. query_cols = None
  634. required_cols = None
  635. context = {
  636. 'table': table,
  637. 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
  638. 'underscore': to_underscore, # 添加自定义过滤器
  639. 'capitalize_first': capitalize_first, # 添加首字母大写函数
  640. 'get_import_path': GenUtils.get_import_path, # 添加导入路径生成函数
  641. 'list_cols': list_cols, # 树表的列表列
  642. 'query_cols': query_cols, # 树表的查询列
  643. 'required_cols': required_cols # 树表的必填列
  644. }
  645. # 使用Jinja2渲染模板
  646. template = Template(template_content)
  647. # 如果是树表模板,打印列信息用于调试
  648. if 'index-tree.vue' in relative_path:
  649. print(f"[DEBUG] 批量渲染树表模板前,列信息:")
  650. for col in table.columns:
  651. list_idx = getattr(col, 'list_index', 'NOT_SET')
  652. print(f" - {col.column_name}: is_list={col.is_list}, is_pk={col.is_pk}, list_index={list_idx} (type={type(list_idx)})")
  653. try:
  654. rendered_content = template.render(**context)
  655. except Exception as e:
  656. import traceback
  657. error_detail = traceback.format_exc()
  658. print(f"[ERROR] 模板渲染失败: {relative_path} (表: {table.table_name})")
  659. print(f"[ERROR] 错误信息: {str(e)}")
  660. print(f"[ERROR] 详细堆栈:\n{error_detail}")
  661. raise
  662. # 生成文件名
  663. output_file_name = GenUtils.get_file_name(relative_path, table)
  664. # 收集目录路径和文件信息,用于生成 __init__.py
  665. # 跳过 sql 和 vue 目录,这些目录不需要 __init__.py
  666. dir_path = os.path.dirname(output_file_name)
  667. if dir_path and 'sql' not in dir_path and 'vue' not in dir_path:
  668. init_dirs.add(dir_path)
  669. # 同时收集父目录(但也要跳过 sql 和 vue)
  670. parts = dir_path.split('/')
  671. for i in range(1, len(parts)):
  672. parent_dir = '/'.join(parts[:i])
  673. if parent_dir and 'sql' not in parent_dir and 'vue' not in parent_dir:
  674. init_dirs.add(parent_dir)
  675. # 收集文件信息用于生成导入语句
  676. if dir_path not in dir_files:
  677. dir_files[dir_path] = []
  678. # 根据文件类型确定导入的类名
  679. if '/entity/' in output_file_name and '.py' in output_file_name:
  680. # Entity 文件在 domain/entity/ 目录下
  681. dir_files[dir_path].append(('entity', table.class_name, table))
  682. elif '/po/' in output_file_name and '_po.py' in output_file_name:
  683. # PO 文件在 domain/po/ 目录下
  684. dir_files[dir_path].append(('po', f"{to_underscore(table.class_name)}_po", table))
  685. elif '_service.py' in output_file_name:
  686. dir_files[dir_path].append(('service', f"{table.class_name}Service", table))
  687. elif '_mapper.py' in output_file_name:
  688. dir_files[dir_path].append(('mapper', f"{table.class_name}Mapper", table))
  689. elif '_controller.py' in output_file_name:
  690. dir_files[dir_path].append(('controller', 'gen', table))
  691. # 检查是否已添加同名文件
  692. if output_file_name in added_files:
  693. # 为重复文件添加序号
  694. name, ext = os.path.splitext(output_file_name)
  695. counter = 1
  696. new_name = f"{name}_{counter}{ext}"
  697. while new_name in added_files:
  698. counter += 1
  699. new_name = f"{name}_{counter}{ext}"
  700. output_file_name = new_name
  701. # 检查渲染后的内容是否为空
  702. if rendered_content.strip():
  703. # 将渲染后的内容写入ZIP文件
  704. zip_file.writestr(output_file_name, rendered_content)
  705. added_files.add(output_file_name)
  706. else:
  707. print(f"警告: 模板 {relative_path} 渲染后内容为空")
  708. except Exception as e:
  709. print(f"处理表 {table.table_name} 的模板 {relative_path} 时出错: {e}")
  710. # 为每个目录生成 __init__.py 文件,使其成为完整的 Python 模块
  711. for dir_path in sorted(init_dirs):
  712. # 跳过模块根目录,因为已经在开始时生成
  713. if dir_path == module_path:
  714. continue
  715. # 跳过 sql 和 vue 目录,这些目录不需要 __init__.py
  716. if 'sql' in dir_path or 'vue' in dir_path or dir_path.startswith('sql/') or dir_path.startswith('vue/'):
  717. continue
  718. init_file_path = os.path.join(dir_path, '__init__.py').replace('\\', '/')
  719. # 生成 __init__.py 内容
  720. init_lines = ["# -*- coding: utf-8 -*-"]
  721. init_lines.append(f"# @Module: {dir_path}")
  722. init_lines.append("")
  723. # 特殊处理 controller 目录:参考 ruoyi_generator/controller/__init__.py 的格式
  724. if 'controller' in dir_path and dir_path.endswith('/controller'):
  725. # 在 controller/__init__.py 中为每个 controller 创建蓝图
  726. if dir_path in dir_files:
  727. # 先导入 Blueprint
  728. init_lines.append("from flask import Blueprint")
  729. init_lines.append("")
  730. # 为每个 controller 创建蓝图
  731. for file_type, class_name, table_info in dir_files[dir_path]:
  732. if file_type == 'controller':
  733. blueprint_name = to_underscore(table_info.class_name)
  734. url_prefix = f"/{table_info.module_name}/{table_info.business_name}"
  735. init_lines.append(f"{blueprint_name} = Blueprint('{blueprint_name}', __name__, url_prefix='{url_prefix}')")
  736. # 导入各个 controller 模块
  737. init_lines.append("")
  738. init_lines.append("")
  739. for file_type, class_name, table_info in dir_files[dir_path]:
  740. if file_type == 'controller':
  741. controller_module_name = f"{to_underscore(table_info.class_name)}_controller"
  742. init_lines.append(f"from . import {controller_module_name}")
  743. else:
  744. # 其他目录正常生成导入语句
  745. if dir_path in dir_files:
  746. imports = []
  747. for file_type, class_name, table_info in dir_files[dir_path]:
  748. if file_type == 'entity':
  749. # Entity 文件在 domain/entity/ 目录下,导入时使用文件名
  750. entity_file_name = to_underscore(class_name)
  751. imports.append(f"from .{entity_file_name} import {class_name}")
  752. elif file_type == 'po':
  753. # PO 文件在 domain/po/ 目录下,导入时使用文件名
  754. po_file_name = class_name # class_name 已经是 address_info_po
  755. imports.append(f"from .{po_file_name} import {class_name}")
  756. elif file_type == 'service':
  757. imports.append(f"from .{to_underscore(class_name.replace('Service', ''))}_service import {class_name}")
  758. elif file_type == 'mapper':
  759. imports.append(f"from .{to_underscore(class_name.replace('Mapper', ''))}_mapper import {class_name}")
  760. if imports:
  761. init_lines.extend(sorted(set(imports)))
  762. init_content = "\n".join(init_lines) + "\n"
  763. zip_file.writestr(init_file_path, init_content)
  764. zip_buffer.seek(0)
  765. return zip_buffer
  766. @staticmethod
  767. def set_column_list_index(table: GenTable):
  768. """
  769. 为表的列设置 list_index 属性,用于 Vue 模板中的 columns 数组索引
  770. Args:
  771. table (GenTable): 表信息
  772. """
  773. if not table.columns:
  774. return
  775. # 如果是树表,需要排除主键列
  776. is_tree = table.tpl_category == 'tree'
  777. print(f"[DEBUG] set_column_list_index: 表类型={table.tpl_category}, 是树表={is_tree}, 列数={len(table.columns)}")
  778. list_index = 0
  779. for column in table.columns:
  780. # 对于树表,只处理 is_list='1' 且不是主键的列
  781. # 对于普通表,处理所有 is_list='1' 的列
  782. if column.is_list == '1' or column.is_list == 1:
  783. if is_tree and (column.is_pk == '1' or column.is_pk == 1):
  784. # 树表的主键列不设置 list_index(树表中主键列不显示在列表中)
  785. # 设置为 None,但模板中会通过 not (column.is_pk == '1') 过滤掉
  786. setattr(column, 'list_index', None)
  787. print(f"[DEBUG] 树表主键列: {column.column_name}, is_list={column.is_list}, is_pk={column.is_pk}, list_index=None")
  788. continue
  789. # 使用 setattr 动态添加属性
  790. setattr(column, 'list_index', list_index)
  791. print(f"[DEBUG] 设置列索引: {column.column_name}, is_list={column.is_list}, is_pk={column.is_pk}, list_index={list_index}")
  792. list_index += 1
  793. else:
  794. # 非列表列也设置 list_index 为 None,保持一致性
  795. setattr(column, 'list_index', None)
  796. print(f"[DEBUG] 非列表列: {column.column_name}, is_list={column.is_list}, list_index=None")
  797. @staticmethod
  798. def preview_code(table: GenTable) -> dict:
  799. """
  800. 预览代码
  801. Args:
  802. table (GenTable): 表信息
  803. Returns:
  804. dict: 预览代码
  805. """
  806. # 设置列的 list_index 属性
  807. GenUtils.set_column_list_index(table)
  808. # 设置主键列
  809. pk_columns = [column for column in table.columns if column.is_pk == '1']
  810. if pk_columns:
  811. table.pk_column = pk_columns[0]
  812. else:
  813. table.pk_column = None
  814. # 从 options 中解析 parentMenuId
  815. if table.options:
  816. import json
  817. try:
  818. if isinstance(table.options, str):
  819. options_dict = json.loads(table.options)
  820. else:
  821. options_dict = table.options
  822. # 从 options 中提取 parentMenuId 并设置到 table
  823. if 'parentMenuId' in options_dict:
  824. table.parent_menu_id = options_dict.get('parentMenuId')
  825. except Exception as e:
  826. print(f"解析 options 字段出错: {e}")
  827. # 强制使用前端模块名(modelName),而不是 Python 模块名
  828. # module_name 必须使用 modelName(test),不能使用 pythonModelName(ruoyi_test)
  829. # 如果 module_name 是空的、等于 python_model_name 或包含 python_model_name,强制替换为 model_name
  830. original_module_name = table.module_name
  831. 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):
  832. table.module_name = GeneratorConfig.model_name
  833. if original_module_name != table.module_name:
  834. print(f"警告:table.module_name 从 '{original_module_name}' 强制替换为 '{table.module_name}'(前端模块名)")
  835. # 获取模板目录
  836. template_dir = os.path.join(os.path.dirname(__file__), 'vm')
  837. # 存储预览代码的字典
  838. preview_data = {}
  839. # 定义需要预览的核心模板文件
  840. core_templates = [
  841. 'py/entity.py.vm',
  842. 'py/po.py.vm',
  843. 'py/controller.py.vm',
  844. 'py/service.py.vm',
  845. 'py/mapper.py.vm',
  846. 'js/api.js.vm',
  847. 'sql/menu.sql.vm'
  848. ]
  849. # 根据表类型添加相应的Vue模板,但预览时都使用index.vue.vm作为文件名
  850. if table.tpl_category == 'tree':
  851. core_templates.append('vue/index-tree.vue.vm')
  852. else:
  853. core_templates.append('vue/index.vue.vm')
  854. # 处理每个核心模板文件
  855. for relative_path in core_templates:
  856. template_path = os.path.join(template_dir, relative_path)
  857. if os.path.exists(template_path):
  858. # 读取模板内容
  859. try:
  860. with open(template_path, 'r', encoding='utf-8') as f:
  861. template_content = f.read()
  862. # 准备模板上下文
  863. # table.module_name 是从数据库读取的前端模块名(真正的模块名,用于权限、前端、SQL)
  864. # GeneratorConfig.python_model_name 是 Python 模块名(只用于 Python 后端代码路径)
  865. # 为树表准备过滤后的列列表
  866. if 'index-tree.vue' in relative_path:
  867. 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]
  868. query_cols = [col for col in table.columns if col.is_query]
  869. required_cols = [col for col in table.columns if col.is_required]
  870. else:
  871. list_cols = None
  872. query_cols = None
  873. required_cols = None
  874. context = {
  875. 'table': table,
  876. 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
  877. 'underscore': to_underscore, # 添加自定义过滤器
  878. 'capitalize_first': capitalize_first, # 添加首字母大写函数
  879. 'get_import_path': GenUtils.get_import_path, # 添加导入路径生成函数
  880. 'list_cols': list_cols, # 树表的列表列
  881. 'query_cols': query_cols, # 树表的查询列
  882. 'required_cols': required_cols # 树表的必填列
  883. }
  884. # 使用Jinja2渲染模板
  885. template = Template(template_content)
  886. # 如果是树表模板,打印列信息用于调试
  887. if 'index-tree.vue' in relative_path:
  888. print(f"[DEBUG] 预览树表模板前,列信息:")
  889. for col in table.columns:
  890. list_idx = getattr(col, 'list_index', 'NOT_SET')
  891. print(f" - {col.column_name}: is_list={col.is_list}, is_pk={col.is_pk}, list_index={list_idx} (type={type(list_idx)})")
  892. rendered_content = template.render(**context)
  893. # 存储渲染后的内容
  894. preview_data[relative_path] = rendered_content
  895. except Exception as e:
  896. # 如果渲染失败,存储错误信息
  897. import traceback
  898. error_detail = traceback.format_exc()
  899. print(f"[ERROR] 模板渲染失败: {relative_path}")
  900. print(f"[ERROR] 错误信息: {str(e)}")
  901. print(f"[ERROR] 详细堆栈:\n{error_detail}")
  902. preview_data[relative_path] = f"模板渲染失败: {str(e)}\n详细错误:\n{error_detail}"
  903. else:
  904. preview_data[relative_path] = "模板文件不存在"
  905. return preview_data