util.py 55 KB

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