util.py 43 KB

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