feat: 新增代码生成功能

This commit is contained in:
insistence
2025-02-14 17:42:36 +08:00
parent 7cca5c88f3
commit 1d36c0c56e
30 changed files with 4400 additions and 1032 deletions

View File

@@ -7,6 +7,7 @@ from openpyxl.styles import Alignment, PatternFill
from openpyxl.utils import get_column_letter
from openpyxl.worksheet.datavalidation import DataValidation
from sqlalchemy.engine.row import Row
from sqlalchemy.orm.collections import InstrumentedList
from typing import Any, Dict, List, Literal, Union
from config.database import Base
from config.env import CachePathConfig
@@ -58,6 +59,9 @@ class SqlalchemyUtil:
if isinstance(obj, Base):
base_dict = obj.__dict__.copy()
base_dict.pop('_sa_instance_state', None)
for name, value in base_dict.items():
if isinstance(value, InstrumentedList):
base_dict[name] = cls.serialize_result(value, 'snake_to_camel')
elif isinstance(obj, dict):
base_dict = obj.copy()
if transform_case == 'snake_to_camel':

View File

@@ -0,0 +1,152 @@
import re
from datetime import datetime
from typing import List
from config.constant import GenConstant
from config.env import GenConfig
from module_generator.entity.vo.gen_vo import GenTableColumnModel, GenTableModel
from utils.string_util import StringUtil
class GenUtils:
"""代码生成器工具类"""
@classmethod
def init_table(cls, gen_table: GenTableModel, oper_name: str) -> None:
"""初始化表信息"""
gen_table.class_name = cls.convert_class_name(gen_table.table_name)
gen_table.package_name = GenConfig.package_name
gen_table.module_name = cls.get_module_name(GenConfig.package_name)
gen_table.business_name = cls.get_business_name(gen_table.table_name)
gen_table.function_name = cls.replace_text(gen_table.table_comment)
gen_table.function_author = GenConfig.author
gen_table.create_by = oper_name
gen_table.create_time = datetime.now()
gen_table.update_by = oper_name
gen_table.update_time = datetime.now()
@classmethod
def init_column_field(cls, column: GenTableColumnModel, table: GenTableModel) -> None:
"""初始化列属性字段"""
data_type = cls.get_db_type(column.column_type)
column_name = column.column_name
column.table_id = table.table_id
column.create_by = table.create_by
# 设置Python字段名
column.python_field = cls.to_camel_case(column_name)
# 设置默认类型
column.python_type = StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.MYSQL_TO_PYTHON_TYPE_MAPPING, data_type)
column.query_type = GenConstant.QUERY_EQ
if cls.arrays_contains(GenConstant.COLUMNTYPE_STR, data_type) or cls.arrays_contains(
GenConstant.COLUMNTYPE_TEXT, data_type
):
# 字符串长度超过500设置为文本域
column_length = cls.get_column_length(column.column_type)
html_type = (
GenConstant.HTML_TEXTAREA
if column_length >= 500 or cls.arrays_contains(GenConstant.COLUMNTYPE_TEXT, data_type)
else GenConstant.HTML_INPUT
)
column.html_type = html_type
elif cls.arrays_contains(GenConstant.COLUMNTYPE_TIME, data_type):
column.html_type = GenConstant.HTML_DATETIME
elif cls.arrays_contains(GenConstant.COLUMNTYPE_NUMBER, data_type):
column.html_type = GenConstant.HTML_INPUT
# 插入字段(默认所有字段都需要插入)
column.is_insert = GenConstant.REQUIRE
# 编辑字段
if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_EDIT, column_name) and not column.is_pk:
column.is_edit = GenConstant.REQUIRE
# 列表字段
if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_LIST, column_name) and not column.is_pk:
column.is_list = GenConstant.REQUIRE
# 查询字段
if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_QUERY, column_name) and not column.is_pk:
column.is_query = GenConstant.REQUIRE
# 查询字段类型
if column_name.lower().endswith('name'):
column.query_type = GenConstant.QUERY_LIKE
# 状态字段设置单选框
if column_name.lower().endswith('status'):
column.html_type = GenConstant.HTML_RADIO
# 类型&性别字段设置下拉框
elif column_name.lower().endswith('type') or column_name.lower().endswith('sex'):
column.html_type = GenConstant.HTML_SELECT
# 图片字段设置图片上传控件
elif column_name.lower().endswith('image'):
column.html_type = GenConstant.HTML_IMAGE_UPLOAD
# 文件字段设置文件上传控件
elif column_name.lower().endswith('file'):
column.html_type = GenConstant.HTML_FILE_UPLOAD
# 内容字段设置富文本控件
elif column_name.lower().endswith('content'):
column.html_type = GenConstant.HTML_EDITOR
@classmethod
def arrays_contains(cls, arr: List[str], target_value: str) -> bool:
"""校验数组是否包含指定值"""
return target_value in arr
@classmethod
def get_module_name(cls, package_name: str) -> str:
"""获取模块名"""
return package_name.split('.')[-1]
@classmethod
def get_business_name(cls, table_name: str) -> str:
"""获取业务名"""
return table_name.split('_')[-1]
@classmethod
def convert_class_name(cls, table_name: str) -> str:
"""表名转换成Python类名"""
auto_remove_pre = GenConfig.auto_remove_pre
table_prefix = GenConfig.table_prefix
if auto_remove_pre and table_prefix:
search_list = table_prefix.split(',')
table_name = cls.replace_first(table_name, search_list)
return StringUtil.convert_to_camel_case(table_name)
@classmethod
def replace_first(cls, replacement: str, search_list: List[str]) -> str:
"""批量替换前缀"""
for search_string in search_list:
if replacement.startswith(search_string):
return replacement.replace(search_string, '', 1)
return replacement
@classmethod
def replace_text(cls, text: str) -> str:
"""关键字替换"""
return re.sub(r'(?:表|若依)', '', text)
@classmethod
def get_db_type(cls, column_type: str) -> str:
"""获取数据库类型字段"""
if '(' in column_type:
return column_type.split('(')[0]
return column_type
@classmethod
def get_column_length(cls, column_type: str) -> int:
"""获取字段长度"""
if '(' in column_type:
length = len(column_type.split('(')[1].split(')')[0])
return length
return 0
@classmethod
def split_column_type(cls, column_type: str) -> List[str]:
"""拆分列类型"""
if '(' in column_type and ')' in column_type:
return column_type.split('(')[1].split(')')[0].split(',')
return []
@classmethod
def to_camel_case(cls, text: str) -> str:
"""将字符串转换为驼峰命名"""
parts = text.split('_')
return parts[0] + ''.join(word.capitalize() for word in parts[1:])

View File

@@ -1,4 +1,4 @@
from typing import List
from typing import Dict, List
from config.constant import CommonConstant
@@ -36,6 +36,16 @@ class StringUtil:
"""
return string is None or len(string) == 0
@classmethod
def is_not_empty(cls, string: str) -> bool:
"""
校验字符串是否不是''和None
:param string: 需要校验的字符串
:return: 校验结果
"""
return not cls.is_empty(string)
@classmethod
def is_http(cls, link: str):
"""
@@ -49,7 +59,7 @@ class StringUtil:
@classmethod
def contains_ignore_case(cls, search_str: str, compare_str: str):
"""
查找指定字符串是否包含指定字符串同时忽略大小写
查找指定字符串是否包含指定字符串同时忽略大小写
:param search_str: 查找的字符串
:param compare_str: 比对的字符串
@@ -62,15 +72,40 @@ class StringUtil:
@classmethod
def contains_any_ignore_case(cls, search_str: str, compare_str_list: List[str]):
"""
查找指定字符串是否包含指定字符串列表中的任意一个字符串同时忽略大小写
查找指定字符串是否包含指定字符串列表中的任意一个字符串同时忽略大小写
:param search_str: 查找的字符串
:param compare_str_list: 比对的字符串列表
:return: 查找结果
"""
if search_str and compare_str_list:
for compare_str in compare_str_list:
return cls.contains_ignore_case(search_str, compare_str)
return any([cls.contains_ignore_case(search_str, compare_str) for compare_str in compare_str_list])
return False
@classmethod
def equals_ignore_case(cls, search_str: str, compare_str: str):
"""
比较两个字符串是否相等同时忽略大小写
:param search_str: 查找的字符串
:param compare_str: 比对的字符串
:return: 比较结果
"""
if search_str and compare_str:
return search_str.lower() == compare_str.lower()
return False
@classmethod
def equals_any_ignore_case(cls, search_str: str, compare_str_list: List[str]):
"""
比较指定字符串是否与指定字符串列表中的任意一个字符串相等同时忽略大小写
:param search_str: 查找的字符串
:param compare_str_list: 比对的字符串列表
:return: 比较结果
"""
if search_str and compare_str_list:
return any([cls.equals_ignore_case(search_str, compare_str) for compare_str in compare_str_list])
return False
@classmethod
@@ -98,3 +133,38 @@ class StringUtil:
if search_str and compare_str_list:
return any([cls.startswith_case(search_str, compare_str) for compare_str in compare_str_list])
return False
@classmethod
def convert_to_camel_case(cls, name: str) -> str:
"""
将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串
:param name: 转换前的下划线大写方式命名的字符串
:return: 转换后的驼峰式命名的字符串
"""
if not name:
return ''
if '_' not in name:
return name[0].upper() + name[1:]
parts = name.split('_')
result = []
for part in parts:
if not part:
continue
result.append(part[0].upper() + part[1:].lower())
return ''.join(result)
@classmethod
def get_mapping_value_by_key_ignore_case(cls, mapping: Dict[str, str], key: str) -> str:
"""
根据忽略大小写的键获取字典中的对应的值
param mapping: 字典
param key: 字典的键
:return: 字典键对应的值
"""
for k, v in mapping.items():
if key.lower() == k.lower():
return v
return ''

View File

@@ -0,0 +1,296 @@
import json
import os
from datetime import datetime
from jinja2 import Environment, FileSystemLoader
from typing import Dict, List, Set
from config.constant import GenConstant
from module_generator.entity.vo.gen_vo import GenTableModel, GenTableColumnModel
from utils.common_util import CamelCaseUtil, SnakeCaseUtil
from utils.string_util import StringUtil
class TemplateInitializer:
"""
模板引擎初始化类
"""
@classmethod
def init_jinja2(cls):
"""
初始化 Jinja2 模板引擎
:return: Jinja2 环境对象
"""
try:
template_dir = os.path.join(os.getcwd(), 'module_generator', 'templates')
env = Environment(
loader=FileSystemLoader(template_dir),
keep_trailing_newline=True,
trim_blocks=True,
lstrip_blocks=True,
)
env.filters.update(
{
'camel_to_snake': SnakeCaseUtil.camel_to_snake,
'snake_to_camel': CamelCaseUtil.snake_to_camel,
'get_sqlalchemy_type': TemplateUtils.get_sqlalchemy_type,
}
)
return env
except Exception as e:
raise RuntimeError(f'初始化Jinja2模板引擎失败: {e}')
class TemplateUtils:
"""
模板工具类
"""
# 项目路径
FRONTEND_PROJECT_PATH = 'frontend'
BACKEND_PROJECT_PATH = 'backend'
DEFAULT_PARENT_MENU_ID = '3'
@classmethod
def prepare_context(cls, gen_table: GenTableModel):
"""
准备模板变量
:param gen_table: 生成表的配置信息
:return: 模板上下文字典
"""
class_name = gen_table.class_name
module_name = gen_table.module_name
business_name = gen_table.business_name
package_name = gen_table.package_name
tpl_category = gen_table.tpl_category
function_name = gen_table.function_name
context = {
'tplCategory': tpl_category,
'tableName': gen_table.table_name,
'functionName': function_name if StringUtil.is_not_empty(function_name) else '【请填写功能名称】',
'ClassName': class_name,
'className': class_name.lower(),
'moduleName': module_name,
'BusinessName': business_name.capitalize(),
'businessName': business_name,
'basePackage': cls.get_package_prefix(package_name),
'packageName': package_name,
'author': gen_table.function_author,
'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'pkColumn': gen_table.pk_column,
'importList': cls.get_import_list(gen_table),
'permissionPrefix': cls.get_permission_prefix(module_name, business_name),
'columns': gen_table.columns,
'table': gen_table,
'dicts': cls.get_dicts(gen_table),
}
# 设置菜单、树形结构、子表的上下文
cls.set_menu_context(context, gen_table)
if tpl_category == GenConstant.TPL_TREE:
cls.set_tree_context(context, gen_table)
if tpl_category == GenConstant.TPL_SUB:
cls.set_sub_context(context, gen_table)
return context
@classmethod
def set_menu_context(cls, context: Dict, gen_table: GenTableModel):
"""设置菜单上下文"""
options = gen_table.options
params_obj = json.loads(options)
context['parentMenuId'] = cls.get_parent_menu_id(params_obj)
@classmethod
def set_tree_context(cls, context: Dict, gen_table: GenTableModel):
"""设置树形结构上下文"""
options = gen_table.options
params_obj = json.loads(options)
context['treeCode'] = cls.get_tree_code(params_obj)
context['treeParentCode'] = cls.get_tree_parent_code(params_obj)
context['treeName'] = cls.get_tree_name(params_obj)
context['expandColumn'] = cls.get_expand_column(gen_table)
@classmethod
def set_sub_context(cls, context: Dict, gen_table: GenTableModel):
"""设置子表上下文"""
sub_table = gen_table.sub_table
sub_table_name = gen_table.sub_table_name
sub_table_fk_name = gen_table.sub_table_fk_name
sub_class_name = sub_table.class_name
sub_table_fk_class_name = StringUtil.convert_to_camel_case(sub_table_fk_name)
context['subTable'] = sub_table
context['subTableName'] = sub_table_name
context['subTableFkName'] = sub_table_fk_name
context['subTableFkClassName'] = sub_table_fk_class_name
context['subTableFkclassName'] = sub_table_fk_class_name.lower()
context['subClassName'] = sub_class_name
context['subclassName'] = sub_class_name.lower()
context['subImportList'] = cls.get_import_list(sub_table)
@classmethod
def get_template_list(cls, tpl_category, tpl_web_type):
"""获取模板列表"""
use_web_type = 'vue'
if tpl_web_type == 'element-plus':
use_web_type = 'vue/v3'
templates = [
'python/controller.py.jinja2',
'python/dao.py.jinja2',
'python/do.py.jinja2',
'python/service.py.jinja2',
'python/vo.py.jinja2',
'sql/sql.jinja2',
'js/api.js.jinja2',
]
if tpl_category == GenConstant.TPL_CRUD:
templates.append(f'{use_web_type}/index.vue.jinja2')
elif tpl_category == GenConstant.TPL_TREE:
templates.append(f'{use_web_type}/index-tree.vue.jinja2')
elif tpl_category == GenConstant.TPL_SUB:
templates.append(f'{use_web_type}/index.vue.jinja2')
# templates.append('python/sub-domain.python.jinja2')
return templates
@classmethod
def get_file_name(cls, template, gen_table: GenTableModel):
"""根据模板生成文件名"""
package_name = gen_table.package_name
module_name = gen_table.module_name
business_name = gen_table.business_name
vue_path = cls.FRONTEND_PROJECT_PATH
python_path = f"{cls.BACKEND_PROJECT_PATH}/{package_name.replace('.', '/')}"
if 'controller.py.jinja2' in template:
return f'{python_path}/controller/{business_name}_controller.py'
elif 'dao.py.jinja2' in template:
return f'{python_path}/dao/{business_name}_dao.py'
elif 'do.py.jinja2' in template:
return f'{python_path}/entity/do/{business_name}_do.py'
elif 'service.py.jinja2' in template:
return f'{python_path}/service/{business_name}_service.py'
elif 'vo.py.jinja2' in template:
return f'{python_path}/entity/vo/{business_name}_vo.py'
elif 'sql.jinja2' in template:
return f'{cls.BACKEND_PROJECT_PATH}/sql/{business_name}_menu.sql'
elif 'api.js.jinja2' in template:
return f'{vue_path}/api/{module_name}/{business_name}.js'
elif 'index.vue.jinja2' in template or 'index-tree.vue.j2' in template:
return f'{vue_path}/views/{module_name}/{business_name}/index.vue'
return ''
@classmethod
def get_package_prefix(cls, package_name: str):
"""获取包前缀"""
return package_name[: package_name.rfind('.')]
@classmethod
def get_import_list(cls, gen_table: GenTableModel):
"""获取导入包列表"""
columns = gen_table.columns or []
sub_gen_table = gen_table.sub_table
import_list = set()
if sub_gen_table is not None:
import_list.add('python.util.List')
for column in columns:
if not column.super_column and column.python_type in GenConstant.TYPE_DATE:
import_list.add(f'from datetime import {column.python_type}')
elif not column.super_column and column.python_type == GenConstant.TYPE_DECIMAL:
import_list.add('from decimal import Decimal')
return list(import_list)
@classmethod
def get_dicts(cls, gen_table: GenTableModel):
"""获取字典列表"""
columns = gen_table.columns or []
dicts = set()
cls.add_dicts(dicts, columns)
if gen_table.sub_table is not None:
cls.add_dicts(dicts, gen_table.sub_table.columns)
return ', '.join(dicts)
@classmethod
def add_dicts(cls, dicts: Set[str], columns: List[GenTableColumnModel]):
"""添加字典列表"""
for column in columns:
if (
column.super_column
and StringUtil.is_not_empty(column.dict_type)
and StringUtil.equals_any_ignore_case(
column.html_type, [GenConstant.HTML_SELECT, GenConstant.HTML_RADIO, GenConstant.HTML_CHECKBOX]
)
):
dicts.add(f"'{column.dict_type}'")
@classmethod
def get_permission_prefix(cls, module_name: str, business_name: str):
"""获取权限前缀"""
return f'{module_name}:{business_name}'
@classmethod
def get_parent_menu_id(cls, params_obj):
"""获取上级菜单ID"""
if params_obj and params_obj.get(GenConstant.PARENT_MENU_ID):
return params_obj.get(GenConstant.PARENT_MENU_ID)
return cls.DEFAULT_PARENT_MENU_ID
@classmethod
def get_tree_code(cls, params_obj: Dict):
"""获取树编码"""
if GenConstant.TREE_CODE in params_obj:
return cls.to_camel_case(params_obj.get(GenConstant.TREE_CODE))
return ''
@classmethod
def get_tree_parent_code(cls, params_obj: Dict):
"""获取树父编码"""
if GenConstant.TREE_PARENT_CODE in params_obj:
return cls.to_camel_case(params_obj.get(GenConstant.TREE_PARENT_CODE))
return ''
@classmethod
def get_tree_name(cls, params_obj: Dict):
"""获取树名称"""
if GenConstant.TREE_NAME in params_obj:
return cls.to_camel_case(params_obj.get(GenConstant.TREE_NAME))
return ''
@classmethod
def get_expand_column(cls, gen_table: GenTableModel):
"""获取展开列"""
options = gen_table.options
params_obj = json.loads(options)
tree_name = params_obj.get(GenConstant.TREE_NAME)
num = 0
for column in gen_table.columns or []:
if column.list:
num += 1
if column.column_name == tree_name:
break
return num
@classmethod
def to_camel_case(cls, text: str) -> str:
"""将字符串转换为驼峰命名"""
parts = text.split('_')
return parts[0] + ''.join(word.capitalize() for word in parts[1:])
@classmethod
def get_sqlalchemy_type(cls, column_type: str):
if '(' in column_type:
column_type_list = column_type.split('(')
sqlalchemy_type = (
StringUtil.get_mapping_value_by_key_ignore_case(
GenConstant.MYSQL_TO_SQLALCHEMY_TYPE_MAPPING, column_type_list[0]
)
+ '('
+ column_type_list[1]
)
else:
sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case(
GenConstant.MYSQL_TO_SQLALCHEMY_TYPE_MAPPING, column_type
)
return sqlalchemy_type