You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

332 lines
12 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import re
from app.common.constant import GenConstant
from app.plugin.module_generator.gencode.schema import (
GenTableColumnSchema,
GenTableOutSchema,
GenTableSchema,
)
from app.utils.string_util import StringUtil
class GenUtils:
"""代码生成器工具类"""
@classmethod
def init_table(cls, gen_table: GenTableSchema) -> None:
"""
初始化表信息
参数:
- gen_table (GenTableSchema): 业务表对象。
返回:
- None
"""
gen_table.class_name = cls.convert_class_name(gen_table.table_name or "")
# 导入时给出“可用默认值”,减少用户点开后看到空表单:
# - module_name从表名推导去掉 gen_/tb_ 前缀)
# - package_name默认 module_{module_name}(仍可在前端改)
if gen_table.business_name is None:
gen_table.business_name = gen_table.table_name
if gen_table.module_name is None or not str(gen_table.module_name).strip():
tn = (gen_table.table_name or "").strip().lower()
if tn.startswith("gen_"):
tn = tn[4:]
elif tn.startswith("tb_"):
tn = tn[3:]
tn = re.sub(r"[^a-z0-9_]+", "_", tn)
tn = re.sub(r"_+", "_", tn).strip("_") or "module"
gen_table.module_name = tn
if gen_table.package_name is None or not str(gen_table.package_name).strip():
mn = (gen_table.module_name or "").strip()
if mn:
gen_table.package_name = mn if mn.startswith("module_") else f"module_{mn}"
fn = re.sub(r"(?:表|测试)", "", gen_table.table_comment or "")
fn = (fn or "").strip()
if not fn:
# 表注释为空时:用表名兜底,至少不为空
fn = (gen_table.table_name or "").strip()
gen_table.function_name = fn
@classmethod
def init_column_field(cls, column: GenTableColumnSchema, table: GenTableOutSchema) -> None:
"""
初始化列属性字段
参数:
- column (GenTableColumnSchema): 业务表字段对象。
- table (GenTableOutSchema): 业务表对象。
返回:
- None
"""
data_type = cls.get_db_type(column.column_type or "")
column_name = column.column_name or ""
if table.id is None:
raise ValueError("业务表ID不能为空")
column.table_id = table.id
column.python_field = cls.to_camel_case(column_name)
# 特殊处理几何类型,根据数据库类型选择不同的映射
from app.config.setting import settings
if data_type in [
"point",
"line",
"linestring",
"polygon",
"multipoint",
"multilinestring",
"multipolygon",
"geometrycollection",
"geometry",
]:
if settings.DATABASE_TYPE == "mysql":
column.python_type = "bytes"
elif settings.DATABASE_TYPE == "postgres":
column.python_type = "list"
else:
# 只有当python_type为None时才设置默认类型
column.python_type = StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_PYTHON, data_type)
if column.column_length is None:
column.column_length = ""
if column.column_default is None:
column.column_default = ""
if column.html_type is None:
# 先按“字段名语义”推断(优先级高于通用字符串规则)
lower_name = column_name.lower()
if lower_name.endswith("status"):
column.html_type = GenConstant.HTML_RADIO
elif lower_name.endswith("type") or lower_name.endswith("sex"):
column.html_type = GenConstant.HTML_SELECT
elif lower_name.endswith("image"):
column.html_type = GenConstant.HTML_IMAGE_UPLOAD
elif lower_name.endswith("file"):
column.html_type = GenConstant.HTML_FILE_UPLOAD
elif lower_name.endswith("content"):
column.html_type = GenConstant.HTML_EDITOR
# 再按“数据类型”推断
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
elif 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 or "")
column.html_type = GenConstant.HTML_TEXTAREA if column_length >= 500 or cls.arrays_contains(GenConstant.COLUMNTYPE_TEXT, data_type) else GenConstant.HTML_INPUT
else:
column.html_type = GenConstant.HTML_INPUT
# 默认新增字段:非主键且不在“新增不展示”黑名单中
# 说明schema 默认值可能为 True/False仅当调用方未显式配置时才做推断
if column.is_insert is None:
column.is_insert = bool((not column.is_pk) and (not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_ADD_SHOW, column_name)))
# 默认编辑字段:非主键且不在“不编辑”黑名单中
if column.is_edit is None:
column.is_edit = bool((not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_EDIT, column_name)) and (not column.is_pk))
# 默认列表字段:非主键且不在“不列表显示”黑名单中
if column.is_list is None:
column.is_list = bool((not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_LIST, column_name)) and (not column.is_pk))
# 默认查询字段:非主键且不在“不查询”黑名单中
if column.is_query is None:
column.is_query = bool((not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_QUERY, column_name)) and (not column.is_pk))
# 查询类型:仅当开启查询且 query_type 未显式配置时推断
if column.is_query:
if column.query_type is None:
if column_name.lower().endswith("name") or data_type in ["varchar", "char", "text"]:
column.query_type = GenConstant.QUERY_LIKE
else:
column.query_type = GenConstant.QUERY_EQ
else:
column.query_type = None
# 主键强约束:无论默认推断/历史配置如何,主键列不应出现在新增/编辑/列表/查询
if bool(column.is_pk):
column.is_insert = False
column.is_edit = False
column.is_list = False
column.is_query = False
column.query_type = None
@classmethod
def arrays_contains(cls, arr: list, target_value: str) -> bool:
"""
检查目标值是否在数组中
注意:从根本上解决问题,现在确保传入的参数都是正确的类型:
- arr 是列表类型且在GenConstant中定义
- target_value 不会是None
参数:
- arr: 数组类型
- target_value: 目标值
返回:
- bool: 如果目标值在数组中返回True否则返回False
"""
# 从根本上解决问题,不再需要复杂的防御性检查
# 因为现在我们确保传入的arr是GenConstant中定义的列表常量
# 并且target_value在调用前已经被处理过不会是None
# 移除 COLLATE 子句和 UNSIGNED 标记(不区分大小写)
target_str = str(target_value)
# 移除 COLLATE 子句
collate_pattern = re.compile(r"\s+COLLATE\s+", re.IGNORECASE)
if collate_pattern.search(target_str):
target_str = collate_pattern.split(target_str)[0].strip()
# 移除 UNSIGNED 标记
unsigned_pattern = re.compile(r"\s+UNSIGNED", re.IGNORECASE)
if unsigned_pattern.search(target_str):
target_str = unsigned_pattern.sub("", target_str).strip()
# 转换为小写进行比较
target_str = target_str.lower()
# 对于包含括号的类型如TINYINT(1)),需要特殊处理
# 先获取基本类型名称(不含括号)用于比较
target_base_type = target_str.split("(")[0] if "(" in target_str else target_str
for item in arr:
item_str = str(item).lower()
item_base_type = item_str.split("(")[0] if "(" in item_str else item_str
if target_base_type == item_base_type:
return True
return False
@classmethod
def convert_class_name(cls, table_name: str) -> str:
"""
表名转换成 Python 类名
参数:
- table_name (str): 业务表名。
返回:
- str: Python 类名。
"""
return StringUtil.convert_to_camel_case(table_name)
@classmethod
def replace_first(cls, input_string: str, search_list: list[str]) -> str:
"""
批量替换前缀
参数:
- input_string (str): 需要被替换的字符串。
- search_list (list[str]): 可替换的字符串列表。
返回:
- str: 替换后的字符串。
"""
for search_string in search_list:
if input_string.startswith(search_string):
return input_string.replace(search_string, "", 1)
return input_string
@classmethod
def get_db_type(cls, column_type: str) -> str:
"""
获取数据库类型字段
参数:
- column_type (str): 字段类型。
返回:
- str: 数据库类型。
"""
# 移除 COLLATE 子句(处理带引号和不带引号的情况,不区分大小写)
collate_pattern = re.compile(r"\s+COLLATE\s+", re.IGNORECASE)
if collate_pattern.search(column_type):
column_type = collate_pattern.split(column_type)[0].strip()
# 移除 UNSIGNED 标记(不区分大小写)
unsigned_pattern = re.compile(r"\s+UNSIGNED", re.IGNORECASE)
if unsigned_pattern.search(column_type):
column_type = unsigned_pattern.sub("", column_type).strip()
# 特殊处理tinyint(1)映射为boolean
if column_type.lower().startswith("tinyint(1)"):
return "boolean"
# 处理PostgreSQL数组类型如 integer[], text[] 或 ARRAY[INTEGER]
if "[]" in column_type or column_type.upper().startswith("ARRAY["):
return "array"
# 提取基本类型:
# - 去掉括号参数varchar(64) -> varchar
# - 去掉空格后的修饰timestamp without time zone -> timestamp
# - 统一小写
base = column_type.split("(", 1)[0].strip()
if not base:
return ""
base = base.split(None, 1)[0].strip()
return base.lower()
@classmethod
def get_column_length(cls, column_type: str) -> int:
"""
获取字段长度
参数:
- column_type (str): 字段类型,例如 'varchar(255)''decimal(10,2)'
返回:
- int: 字段长度优先取第一个长度值无法解析时返回0
"""
if not column_type:
return 0
if "(" not in column_type or ")" not in column_type:
return 0
# 形如 varchar(255) / decimal(10,2) / numeric(20, 0)
inner = column_type.split("(", 1)[1].split(")", 1)[0].strip()
if not inner:
return 0
first = inner.split(",", 1)[0].strip()
try:
return int(first)
except ValueError:
return 0
@classmethod
def split_column_type(cls, column_type: str) -> list[str]:
"""
拆分列类型
参数:
- 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:
"""
将字符串转换为驼峰命名
参数:
- text (str): 需要转换的字符串
返回:
- str: 驼峰命名
"""
parts = text.split("_")
return parts[0] + "".join(word.capitalize() for word in parts[1:])