util.py 53 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079
  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. context = {
  402. 'table': table,
  403. 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
  404. 'underscore': to_underscore, # 下划线命名工具
  405. 'capitalize_first': capitalize_first, # 首字母大写工具
  406. 'to_camel_case': to_camel_case, # 新增:驼峰命名工具
  407. 'get_import_path': GenUtils.get_import_path, # 导入路径生成函数
  408. 'get_tree_column_index': get_tree_column_index, # 树表列索引计算函数
  409. 'list_cols': list_cols, # 树表的列表列
  410. 'query_cols': query_cols, # 树表的查询列
  411. 'required_cols': required_cols # 树表的必填列
  412. }
  413. # 使用Jinja2渲染模板
  414. template = Template(template_content)
  415. # 如果是树表模板,打印列信息用于调试
  416. if 'index-tree.vue' in relative_path:
  417. print(f"[DEBUG] 渲染树表模板前,列信息:")
  418. for col in table.columns:
  419. list_idx = getattr(col, 'list_index', 'NOT_SET')
  420. print(f" - {col.column_name}: is_list={col.is_list}, is_pk={col.is_pk}, list_index={list_idx} (type={type(list_idx)})")
  421. try:
  422. rendered_content = template.render(**context)
  423. except Exception as e:
  424. import traceback
  425. error_detail = traceback.format_exc()
  426. print(f"[ERROR] 模板渲染失败: {relative_path}")
  427. print(f"[ERROR] 错误信息: {str(e)}")
  428. print(f"[ERROR] 详细堆栈:\n{error_detail}")
  429. raise
  430. # 如果是 SQL 模板,恢复原始 module_name(虽然已经强制设置了,但为了安全)
  431. if 'sql/menu.sql' in relative_path:
  432. pass # 已经强制设置为 model_name,不需要恢复
  433. # 生成文件名
  434. output_file_name = GenUtils.get_file_name(relative_path, table)
  435. # 收集目录路径和文件信息,用于生成 __init__.py
  436. # 跳过 sql 和 vue 目录,这些目录不需要 __init__.py
  437. dir_path = os.path.dirname(output_file_name)
  438. if dir_path and 'sql' not in dir_path and 'vue' not in dir_path:
  439. init_dirs.add(dir_path)
  440. # 同时收集父目录(但也要跳过 sql 和 vue)
  441. parts = dir_path.split('/')
  442. for i in range(1, len(parts)):
  443. parent_dir = '/'.join(parts[:i])
  444. if parent_dir and 'sql' not in parent_dir and 'vue' not in parent_dir:
  445. init_dirs.add(parent_dir)
  446. # 收集文件信息用于生成导入语句
  447. file_name = os.path.basename(output_file_name)
  448. file_base = os.path.splitext(file_name)[0]
  449. if dir_path not in dir_files:
  450. dir_files[dir_path] = []
  451. # 根据文件类型确定导入的类名
  452. if '/entity/' in output_file_name and '.py' in output_file_name:
  453. # Entity 文件在 domain/entity/ 目录下
  454. dir_files[dir_path].append(('entity', table.class_name, table))
  455. elif '/po/' in output_file_name and '_po.py' in output_file_name:
  456. # PO 文件在 domain/po/ 目录下
  457. dir_files[dir_path].append(('po', f"{to_underscore(table.class_name)}_po", table))
  458. elif '_service.py' in output_file_name:
  459. dir_files[dir_path].append(('service', f"{to_underscore(table.class_name)}_service", table))
  460. elif '_mapper.py' in output_file_name:
  461. dir_files[dir_path].append(('mapper', f"{to_underscore(table.class_name)}_mapper", table))
  462. elif '_controller.py' in output_file_name:
  463. dir_files[dir_path].append(('controller', 'gen', table))
  464. # 检查渲染后的内容是否为空
  465. if rendered_content.strip():
  466. # 将渲染后的内容写入ZIP文件
  467. zip_file.writestr(output_file_name, rendered_content)
  468. else:
  469. print(f"警告: 模板 {relative_path} 渲染后内容为空")
  470. except Exception as e:
  471. print(f"处理模板 {relative_path} 时出错: {e}")
  472. # 为每个目录生成 __init__.py 文件,使其成为完整的 Python 模块
  473. for dir_path in sorted(init_dirs):
  474. # 跳过模块根目录,因为已经在开始时生成
  475. if dir_path == module_path:
  476. continue
  477. # 跳过 sql 和 vue 目录,这些目录不需要 __init__.py
  478. if 'sql' in dir_path or 'vue' in dir_path or dir_path.startswith('sql/') or dir_path.startswith('vue/'):
  479. continue
  480. init_file_path = os.path.join(dir_path, '__init__.py').replace('\\', '/')
  481. # 生成 __init__.py 内容
  482. init_lines = ["# -*- coding: utf-8 -*-"]
  483. init_lines.append(f"# @Module: {dir_path}")
  484. init_lines.append("")
  485. # 特殊处理 controller 目录:参考 ruoyi_generator/controller/__init__.py 的格式
  486. if 'controller' in dir_path and dir_path.endswith('/controller'):
  487. # 在 controller/__init__.py 中为每个 controller 创建蓝图
  488. if dir_path in dir_files:
  489. # 先导入 Blueprint
  490. init_lines.append("from flask import Blueprint")
  491. init_lines.append("")
  492. # 为每个 controller 创建蓝图
  493. for file_type, class_name, table_info in dir_files[dir_path]:
  494. if file_type == 'controller':
  495. blueprint_name = to_underscore(table_info.class_name)
  496. url_prefix = f"/{table_info.module_name}/{table_info.business_name}"
  497. init_lines.append(f"{blueprint_name} = Blueprint('{blueprint_name}', __name__, url_prefix='{url_prefix}')")
  498. # 导入各个 controller 模块
  499. init_lines.append("")
  500. init_lines.append("")
  501. for file_type, class_name, table_info in dir_files[dir_path]:
  502. if file_type == 'controller':
  503. controller_module_name = f"{to_underscore(table_info.class_name)}_controller"
  504. init_lines.append(f"from . import {controller_module_name}")
  505. else:
  506. # 其他目录正常生成导入语句
  507. if dir_path in dir_files:
  508. imports = []
  509. for file_type, class_name, table_info in dir_files[dir_path]:
  510. if file_type == 'entity':
  511. # Entity 文件在 domain/entity/ 目录下,导入时使用文件名
  512. entity_file_name = to_underscore(class_name)
  513. imports.append(f"from .{entity_file_name} import {class_name}")
  514. elif file_type == 'po':
  515. # PO 文件在 domain/po/ 目录下,导入时使用文件名
  516. po_file_name = class_name # class_name 已经是 address_info_po
  517. imports.append(f"from .{po_file_name} import {class_name}")
  518. elif file_type == 'service':
  519. imports.append(f"from .{class_name} import {class_name}")
  520. elif file_type == 'mapper':
  521. imports.append(f"from .{class_name} import {class_name}")
  522. if imports:
  523. init_lines.extend(sorted(set(imports)))
  524. init_content = "\n".join(init_lines) + "\n"
  525. zip_file.writestr(init_file_path, init_content)
  526. zip_buffer.seek(0)
  527. return zip_buffer
  528. @staticmethod
  529. def batch_generator_code(tables: List[GenTable]) -> BytesIO:
  530. """
  531. 批量生成代码
  532. Args:
  533. tables (List[GenTable]): 表列表
  534. Returns:
  535. BytesIO: 生成的代码文件
  536. """
  537. # 为每个表设置列的 list_index 属性和主键列
  538. for table in tables:
  539. GenUtils.set_column_list_index(table)
  540. # 设置主键列
  541. pk_columns = [column for column in table.columns if column.is_pk == '1']
  542. if pk_columns:
  543. table.pk_column = pk_columns[0]
  544. else:
  545. table.pk_column = None
  546. # 从 options 中解析 parentMenuId
  547. if table.options:
  548. import json
  549. try:
  550. if isinstance(table.options, str):
  551. options_dict = json.loads(table.options)
  552. else:
  553. options_dict = table.options
  554. # 从 options 中提取 parentMenuId 并设置到 table
  555. if 'parentMenuId' in options_dict:
  556. table.parent_menu_id = options_dict.get('parentMenuId')
  557. except Exception as e:
  558. print(f"解析 options 字段出错: {e}")
  559. # 强制使用前端模块名(modelName),而不是 Python 模块名
  560. # module_name 必须使用 modelName(test),不能使用 pythonModelName(ruoyi_test)
  561. if not table.module_name or table.module_name == GeneratorConfig.python_model_name or table.module_name.strip() == GeneratorConfig.python_model_name:
  562. table.module_name = GeneratorConfig.model_name
  563. # 额外检查:如果 module_name 等于 python_model_name,强制替换
  564. if table.module_name == GeneratorConfig.python_model_name:
  565. table.module_name = GeneratorConfig.model_name
  566. # 定义核心模板文件(每个表都会生成,但 __init__.py 只生成一次)
  567. core_templates = [
  568. 'py/entity.py.vm',
  569. 'py/po.py.vm',
  570. 'py/controller.py.vm',
  571. 'py/service.py.vm',
  572. 'py/mapper.py.vm',
  573. 'js/api.js.vm',
  574. 'sql/menu.sql.vm'
  575. ]
  576. # 创建内存中的ZIP文件
  577. zip_buffer = BytesIO()
  578. # 获取模板目录
  579. template_dir = os.path.join(os.path.dirname(__file__), 'vm')
  580. # 确定模块路径(使用第一个表的模块路径)
  581. if tables and len(tables) > 0:
  582. first_table = tables[0]
  583. if first_table.package_name:
  584. module_path = first_table.package_name.replace('.', '/')
  585. else:
  586. # 使用 pythonModelName 作为后端模块路径
  587. 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')
  588. else:
  589. module_path = GeneratorConfig.python_model_name if hasattr(GeneratorConfig, 'python_model_name') else 'ruoyi_generator'
  590. # 收集需要生成 __init__.py 的目录和文件信息
  591. init_dirs = set()
  592. # 收集每个目录下的文件,用于生成导入语句 {dir_path: [(file_type, class_name, table)]}
  593. dir_files = {}
  594. with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
  595. # 跟踪已添加的文件名以避免重复
  596. added_files = set()
  597. # 首先为模块根目录生成 __init__.py(只生成一次,使用第一个表的信息)
  598. if tables and len(tables) > 0:
  599. init_template_path = os.path.join(template_dir, 'py/__init__.py.vm')
  600. if os.path.exists(init_template_path):
  601. try:
  602. with open(init_template_path, 'r', encoding='utf-8') as f:
  603. template_content = f.read()
  604. # 准备模板上下文(批量生成时,传入所有表)
  605. # table.module_name 是从数据库读取的前端模块名(真正的模块名,用于权限、前端、SQL)
  606. # GeneratorConfig.python_model_name 是 Python 模块名(只用于 Python 后端代码路径)
  607. context = {
  608. 'table': first_table, # 用于兼容模板中的 table 变量
  609. 'tables': tables, # 传入所有表,用于循环注册所有蓝图
  610. 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
  611. 'underscore': to_underscore,
  612. 'get_import_path': GenUtils.get_import_path,
  613. 'get_tree_column_index': get_tree_column_index
  614. }
  615. # 使用Jinja2渲染模板
  616. template = Template(template_content)
  617. rendered_content = template.render(**context)
  618. # 生成文件名
  619. init_file_path = f"{module_path}/__init__.py"
  620. # 将渲染后的内容写入ZIP文件(只写入一次)
  621. if rendered_content.strip() and init_file_path not in added_files:
  622. zip_file.writestr(init_file_path, rendered_content)
  623. added_files.add(init_file_path)
  624. except Exception as e:
  625. print(f"处理模块 __init__.py 模板时出错: {e}")
  626. # 处理每个表
  627. for table in tables:
  628. # 根据表类型添加相应的Vue模板
  629. if table.tpl_category == 'tree':
  630. current_templates = core_templates + ['vue/index-tree.vue.vm']
  631. elif table.tpl_category == 'sub':
  632. current_templates = core_templates + ['vue/index-sub.vue.vm']
  633. else:
  634. current_templates = core_templates + ['vue/index.vue.vm']
  635. # 处理每个核心模板文件
  636. for relative_path in current_templates:
  637. # 跳过模块根目录的 __init__.py,因为已经在开始时生成
  638. if relative_path == 'py/__init__.py.vm':
  639. continue
  640. template_path = os.path.join(template_dir, relative_path)
  641. if os.path.exists(template_path):
  642. # 读取模板内容
  643. try:
  644. with open(template_path, 'r', encoding='utf-8') as f:
  645. template_content = f.read()
  646. # 准备模板上下文
  647. # table.module_name 是从数据库读取的前端模块名(真正的模块名,用于权限、前端、SQL)
  648. # GeneratorConfig.python_model_name 是 Python 模块名(只用于 Python 后端代码路径)
  649. # 为树表准备过滤后的列列表
  650. if 'index-tree.vue' in relative_path:
  651. 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]
  652. query_cols = [col for col in table.columns if col.is_query]
  653. required_cols = [col for col in table.columns if col.is_required]
  654. else:
  655. list_cols = None
  656. query_cols = None
  657. required_cols = None
  658. context = {
  659. 'table': table,
  660. 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
  661. 'underscore': to_underscore, # 下划线命名工具
  662. 'capitalize_first': capitalize_first, # 首字母大写工具
  663. 'to_camel_case': to_camel_case, # 新增:驼峰命名工具
  664. 'get_import_path': GenUtils.get_import_path, # 导入路径生成函数
  665. 'list_cols': list_cols, # 树表的列表列
  666. 'query_cols': query_cols, # 树表的查询列
  667. 'required_cols': required_cols # 树表的必填列
  668. }
  669. # 使用Jinja2渲染模板
  670. template = Template(template_content)
  671. # 如果是树表模板,打印列信息用于调试
  672. if 'index-tree.vue' in relative_path:
  673. print(f"[DEBUG] 批量渲染树表模板前,列信息:")
  674. for col in table.columns:
  675. list_idx = getattr(col, 'list_index', 'NOT_SET')
  676. print(f" - {col.column_name}: is_list={col.is_list}, is_pk={col.is_pk}, list_index={list_idx} (type={type(list_idx)})")
  677. try:
  678. rendered_content = template.render(**context)
  679. except Exception as e:
  680. import traceback
  681. error_detail = traceback.format_exc()
  682. print(f"[ERROR] 模板渲染失败: {relative_path} (表: {table.table_name})")
  683. print(f"[ERROR] 错误信息: {str(e)}")
  684. print(f"[ERROR] 详细堆栈:\n{error_detail}")
  685. raise
  686. # 生成文件名
  687. output_file_name = GenUtils.get_file_name(relative_path, table)
  688. # 收集目录路径和文件信息,用于生成 __init__.py
  689. # 跳过 sql 和 vue 目录,这些目录不需要 __init__.py
  690. dir_path = os.path.dirname(output_file_name)
  691. if dir_path and 'sql' not in dir_path and 'vue' not in dir_path:
  692. init_dirs.add(dir_path)
  693. # 同时收集父目录(但也要跳过 sql 和 vue)
  694. parts = dir_path.split('/')
  695. for i in range(1, len(parts)):
  696. parent_dir = '/'.join(parts[:i])
  697. if parent_dir and 'sql' not in parent_dir and 'vue' not in parent_dir:
  698. init_dirs.add(parent_dir)
  699. # 收集文件信息用于生成导入语句
  700. if dir_path not in dir_files:
  701. dir_files[dir_path] = []
  702. # 根据文件类型确定导入的类名
  703. if '/entity/' in output_file_name and '.py' in output_file_name:
  704. # Entity 文件在 domain/entity/ 目录下
  705. dir_files[dir_path].append(('entity', table.class_name, table))
  706. elif '/po/' in output_file_name and '_po.py' in output_file_name:
  707. # PO 文件在 domain/po/ 目录下
  708. dir_files[dir_path].append(('po', f"{to_underscore(table.class_name)}_po", table))
  709. elif '_service.py' in output_file_name:
  710. dir_files[dir_path].append(('service', f"{to_underscore(table.class_name)}_service", table))
  711. elif '_mapper.py' in output_file_name:
  712. dir_files[dir_path].append(('mapper', f"{to_underscore(table.class_name)}_mapper", table))
  713. elif '_controller.py' in output_file_name:
  714. dir_files[dir_path].append(('controller', 'gen', table))
  715. # 检查是否已添加同名文件
  716. if output_file_name in added_files:
  717. # 为重复文件添加序号
  718. name, ext = os.path.splitext(output_file_name)
  719. counter = 1
  720. new_name = f"{name}_{counter}{ext}"
  721. while new_name in added_files:
  722. counter += 1
  723. new_name = f"{name}_{counter}{ext}"
  724. output_file_name = new_name
  725. # 检查渲染后的内容是否为空
  726. if rendered_content.strip():
  727. # 将渲染后的内容写入ZIP文件
  728. zip_file.writestr(output_file_name, rendered_content)
  729. added_files.add(output_file_name)
  730. else:
  731. print(f"警告: 模板 {relative_path} 渲染后内容为空")
  732. except Exception as e:
  733. print(f"处理表 {table.table_name} 的模板 {relative_path} 时出错: {e}")
  734. # 为每个目录生成 __init__.py 文件,使其成为完整的 Python 模块
  735. for dir_path in sorted(init_dirs):
  736. # 跳过模块根目录,因为已经在开始时生成
  737. if dir_path == module_path:
  738. continue
  739. # 跳过 sql 和 vue 目录,这些目录不需要 __init__.py
  740. if 'sql' in dir_path or 'vue' in dir_path or dir_path.startswith('sql/') or dir_path.startswith('vue/'):
  741. continue
  742. init_file_path = os.path.join(dir_path, '__init__.py').replace('\\', '/')
  743. # 生成 __init__.py 内容
  744. init_lines = ["# -*- coding: utf-8 -*-"]
  745. init_lines.append(f"# @Module: {dir_path}")
  746. init_lines.append("")
  747. # 特殊处理 controller 目录:参考 ruoyi_generator/controller/__init__.py 的格式
  748. if 'controller' in dir_path and dir_path.endswith('/controller'):
  749. # 在 controller/__init__.py 中为每个 controller 创建蓝图
  750. if dir_path in dir_files:
  751. # 先导入 Blueprint
  752. init_lines.append("from flask import Blueprint")
  753. init_lines.append("")
  754. # 为每个 controller 创建蓝图
  755. for file_type, class_name, table_info in dir_files[dir_path]:
  756. if file_type == 'controller':
  757. blueprint_name = to_underscore(table_info.class_name)
  758. url_prefix = f"/{table_info.module_name}/{table_info.business_name}"
  759. init_lines.append(f"{blueprint_name} = Blueprint('{blueprint_name}', __name__, url_prefix='{url_prefix}')")
  760. # 导入各个 controller 模块
  761. init_lines.append("")
  762. init_lines.append("")
  763. for file_type, class_name, table_info in dir_files[dir_path]:
  764. if file_type == 'controller':
  765. controller_module_name = f"{to_underscore(table_info.class_name)}_controller"
  766. init_lines.append(f"from . import {controller_module_name}")
  767. else:
  768. # 其他目录正常生成导入语句
  769. if dir_path in dir_files:
  770. imports = []
  771. for file_type, class_name, table_info in dir_files[dir_path]:
  772. if file_type == 'entity':
  773. # Entity 文件在 domain/entity/ 目录下,导入时使用文件名
  774. entity_file_name = to_underscore(class_name)
  775. imports.append(f"from .{entity_file_name} import {class_name}")
  776. elif file_type == 'po':
  777. # PO 文件在 domain/po/ 目录下,导入时使用文件名
  778. po_file_name = class_name # class_name 已经是 address_info_po
  779. imports.append(f"from .{po_file_name} import {class_name}")
  780. elif file_type == 'service':
  781. imports.append(f"from .{class_name} import {class_name}")
  782. elif file_type == 'mapper':
  783. imports.append(f"from .{class_name} import {class_name}")
  784. if imports:
  785. init_lines.extend(sorted(set(imports)))
  786. init_content = "\n".join(init_lines) + "\n"
  787. zip_file.writestr(init_file_path, init_content)
  788. zip_buffer.seek(0)
  789. return zip_buffer
  790. @staticmethod
  791. def set_column_list_index(table: GenTable):
  792. """
  793. 为表的列设置 list_index 属性,用于 Vue 模板中的 columns 数组索引
  794. Args:
  795. table (GenTable): 表信息
  796. """
  797. if not table.columns:
  798. return
  799. # 如果是树表,需要排除主键列
  800. is_tree = table.tpl_category == 'tree'
  801. print(f"[DEBUG] set_column_list_index: 表类型={table.tpl_category}, 是树表={is_tree}, 列数={len(table.columns)}")
  802. list_index = 0
  803. for column in table.columns:
  804. # 对于树表,只处理 is_list='1' 且不是主键的列
  805. # 对于普通表,处理所有 is_list='1' 的列
  806. if column.is_list == '1' or column.is_list == 1:
  807. if is_tree and (column.is_pk == '1' or column.is_pk == 1):
  808. # 树表的主键列不设置 list_index(树表中主键列不显示在列表中)
  809. # 设置为 None,但模板中会通过 not (column.is_pk == '1') 过滤掉
  810. setattr(column, 'list_index', None)
  811. print(f"[DEBUG] 树表主键列: {column.column_name}, is_list={column.is_list}, is_pk={column.is_pk}, list_index=None")
  812. continue
  813. # 使用 setattr 动态添加属性
  814. setattr(column, 'list_index', list_index)
  815. print(f"[DEBUG] 设置列索引: {column.column_name}, is_list={column.is_list}, is_pk={column.is_pk}, list_index={list_index}")
  816. list_index += 1
  817. else:
  818. # 非列表列也设置 list_index 为 None,保持一致性
  819. setattr(column, 'list_index', None)
  820. print(f"[DEBUG] 非列表列: {column.column_name}, is_list={column.is_list}, list_index=None")
  821. @staticmethod
  822. def preview_code(table: GenTable) -> dict:
  823. """
  824. 预览代码
  825. Args:
  826. table (GenTable): 表信息
  827. Returns:
  828. dict: 预览代码
  829. """
  830. # 设置列的 list_index 属性
  831. GenUtils.set_column_list_index(table)
  832. # 设置主键列
  833. pk_columns = [column for column in table.columns if column.is_pk == '1']
  834. if pk_columns:
  835. table.pk_column = pk_columns[0]
  836. else:
  837. table.pk_column = None
  838. # 从 options 中解析 parentMenuId
  839. if table.options:
  840. import json
  841. try:
  842. if isinstance(table.options, str):
  843. options_dict = json.loads(table.options)
  844. else:
  845. options_dict = table.options
  846. # 从 options 中提取 parentMenuId 并设置到 table
  847. if 'parentMenuId' in options_dict:
  848. table.parent_menu_id = options_dict.get('parentMenuId')
  849. except Exception as e:
  850. print(f"解析 options 字段出错: {e}")
  851. # 强制使用前端模块名(modelName),而不是 Python 模块名
  852. # module_name 必须使用 modelName(test),不能使用 pythonModelName(ruoyi_test)
  853. # 如果 module_name 是空的、等于 python_model_name 或包含 python_model_name,强制替换为 model_name
  854. original_module_name = table.module_name
  855. 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):
  856. table.module_name = GeneratorConfig.model_name
  857. if original_module_name != table.module_name:
  858. print(f"警告:table.module_name 从 '{original_module_name}' 强制替换为 '{table.module_name}'(前端模块名)")
  859. # 获取模板目录
  860. template_dir = os.path.join(os.path.dirname(__file__), 'vm')
  861. # 存储预览代码的字典
  862. preview_data = {}
  863. # 定义需要预览的核心模板文件
  864. core_templates = [
  865. 'py/entity.py.vm',
  866. 'py/po.py.vm',
  867. 'py/controller.py.vm',
  868. 'py/service.py.vm',
  869. 'py/mapper.py.vm',
  870. 'js/api.js.vm',
  871. 'sql/menu.sql.vm'
  872. ]
  873. # 根据表类型添加相应的Vue模板,但预览时都使用index.vue.vm作为文件名
  874. if table.tpl_category == 'tree':
  875. core_templates.append('vue/index-tree.vue.vm')
  876. else:
  877. core_templates.append('vue/index.vue.vm')
  878. # 处理每个核心模板文件
  879. for relative_path in core_templates:
  880. template_path = os.path.join(template_dir, relative_path)
  881. if os.path.exists(template_path):
  882. # 读取模板内容
  883. try:
  884. with open(template_path, 'r', encoding='utf-8') as f:
  885. template_content = f.read()
  886. # 准备模板上下文
  887. # table.module_name 是从数据库读取的前端模块名(真正的模块名,用于权限、前端、SQL)
  888. # GeneratorConfig.python_model_name 是 Python 模块名(只用于 Python 后端代码路径)
  889. # 为树表准备过滤后的列列表
  890. if 'index-tree.vue' in relative_path:
  891. 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]
  892. query_cols = [col for col in table.columns if col.is_query]
  893. required_cols = [col for col in table.columns if col.is_required]
  894. else:
  895. list_cols = None
  896. query_cols = None
  897. required_cols = None
  898. context = {
  899. 'table': table,
  900. 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
  901. 'underscore': to_underscore, # 下划线命名工具
  902. 'capitalize_first': capitalize_first, # 首字母大写工具
  903. 'to_camel_case': to_camel_case, # 新增:驼峰命名工具
  904. 'get_import_path': GenUtils.get_import_path, # 导入路径生成函数
  905. 'list_cols': list_cols, # 树表的列表列
  906. 'query_cols': query_cols, # 树表的查询列
  907. 'required_cols': required_cols # 树表的必填列
  908. }
  909. # 使用Jinja2渲染模板
  910. template = Template(template_content)
  911. # 如果是树表模板,打印列信息用于调试
  912. if 'index-tree.vue' in relative_path:
  913. print(f"[DEBUG] 预览树表模板前,列信息:")
  914. for col in table.columns:
  915. list_idx = getattr(col, 'list_index', 'NOT_SET')
  916. print(f" - {col.column_name}: is_list={col.is_list}, is_pk={col.is_pk}, list_index={list_idx} (type={type(list_idx)})")
  917. rendered_content = template.render(**context)
  918. # 存储渲染后的内容
  919. preview_data[relative_path] = rendered_content
  920. except Exception as e:
  921. # 如果渲染失败,存储错误信息
  922. import traceback
  923. error_detail = traceback.format_exc()
  924. print(f"[ERROR] 模板渲染失败: {relative_path}")
  925. print(f"[ERROR] 错误信息: {str(e)}")
  926. print(f"[ERROR] 详细堆栈:\n{error_detail}")
  927. preview_data[relative_path] = f"模板渲染失败: {str(e)}\n详细错误:\n{error_detail}"
  928. else:
  929. preview_data[relative_path] = "模板文件不存在"
  930. return preview_data