|
|
import re
|
|
|
from datetime import datetime
|
|
|
from typing import Any
|
|
|
|
|
|
from jinja2 import Environment, FileSystemLoader, Template
|
|
|
|
|
|
from app.common.constant import GenConstant
|
|
|
from app.config.path_conf import TEMPLATE_DIR
|
|
|
from app.config.setting import settings
|
|
|
from app.plugin.module_generator.gencode.schema import (
|
|
|
GenTableColumnOutSchema,
|
|
|
GenTableOutSchema,
|
|
|
)
|
|
|
from app.utils.common_util import CamelCaseUtil, SnakeCaseUtil
|
|
|
from app.utils.gen_util import GenUtils
|
|
|
from app.utils.string_util import StringUtil
|
|
|
|
|
|
|
|
|
class Jinja2TemplateUtil:
|
|
|
"""
|
|
|
模板处理工具类
|
|
|
"""
|
|
|
|
|
|
@classmethod
|
|
|
def normalize_db_column_type_for_mapping(cls, column_type: str | None) -> str:
|
|
|
"""
|
|
|
与 ``GenUtils.get_db_type`` 一致地去掉 COLLATE / UNSIGNED,便于与 ``DB_TO_SQLALCHEMY`` 键匹配。
|
|
|
|
|
|
参数:
|
|
|
- column_type (str | None): 原始列类型字符串。
|
|
|
|
|
|
返回:
|
|
|
- str: 规范化后的类型片段;空输入返回空字符串。
|
|
|
"""
|
|
|
ct = (column_type or "").strip()
|
|
|
if not ct:
|
|
|
return ""
|
|
|
collate_pattern = re.compile(r"\s+COLLATE\s+", re.IGNORECASE)
|
|
|
if collate_pattern.search(ct):
|
|
|
ct = collate_pattern.split(ct)[0].strip()
|
|
|
unsigned_pattern = re.compile(r"\s+UNSIGNED", re.IGNORECASE)
|
|
|
if unsigned_pattern.search(ct):
|
|
|
ct = unsigned_pattern.sub("", ct).strip()
|
|
|
return ct
|
|
|
|
|
|
# 项目路径
|
|
|
FRONTEND_PROJECT_PATH = settings.CODEGEN_FRONTEND_PROJECT_PATH
|
|
|
BACKEND_PROJECT_PATH = settings.CODEGEN_BACKEND_PROJECT_PATH
|
|
|
|
|
|
|
|
|
# 环境对象
|
|
|
_env = None
|
|
|
|
|
|
@classmethod
|
|
|
def get_env(cls):
|
|
|
"""
|
|
|
获取模板环境对象。
|
|
|
|
|
|
参数:
|
|
|
- 无
|
|
|
|
|
|
返回:
|
|
|
- Environment: Jinja2 环境对象。
|
|
|
"""
|
|
|
try:
|
|
|
if cls._env is None:
|
|
|
cls._env = Environment(
|
|
|
loader=FileSystemLoader(TEMPLATE_DIR),
|
|
|
autoescape=False, # 自动转义HTML
|
|
|
trim_blocks=True, # 删除多余的空行
|
|
|
lstrip_blocks=True, # 删除行首空格
|
|
|
keep_trailing_newline=True, # 保留行尾换行符
|
|
|
enable_async=True, # 开启异步支持
|
|
|
)
|
|
|
cls._env.filters.update(
|
|
|
{
|
|
|
"camel_to_snake": SnakeCaseUtil.camel_to_snake,
|
|
|
"snake_to_camel": CamelCaseUtil.snake_to_camel,
|
|
|
"get_sqlalchemy_type": cls.get_sqlalchemy_type,
|
|
|
"python_to_ts_type": cls.python_type_to_ts_type,
|
|
|
}
|
|
|
)
|
|
|
return cls._env
|
|
|
except Exception as e:
|
|
|
raise RuntimeError(f"初始化Jinja2模板引擎失败: {e}")
|
|
|
|
|
|
@classmethod
|
|
|
def get_template(cls, template_path: str) -> Template:
|
|
|
"""
|
|
|
获取模板。
|
|
|
|
|
|
参数:
|
|
|
- template_path (str): 模板路径。
|
|
|
|
|
|
返回:
|
|
|
- Template: Jinja2 模板对象。
|
|
|
|
|
|
异常:
|
|
|
- TemplateNotFound: 模板未找到时抛出。
|
|
|
"""
|
|
|
return cls.get_env().get_template(template_path)
|
|
|
|
|
|
@classmethod
|
|
|
def business_name_to_slug(cls, business_name: str | None) -> str:
|
|
|
"""
|
|
|
业务路径可含斜杠(如 ``demo/subdir``)用于目录与路由前缀;
|
|
|
Python 函数/方法名仅使用最后一段并规范为合法 snake_case 片段。
|
|
|
|
|
|
参数:
|
|
|
- business_name (str | None): 业务路径或名称。
|
|
|
|
|
|
返回:
|
|
|
- str: 用于 Python 标识的 slug,默认 entity。
|
|
|
"""
|
|
|
s = (business_name or "").strip().strip("/")
|
|
|
if not s:
|
|
|
return "entity"
|
|
|
if "/" in s:
|
|
|
s = s.split("/")[-1]
|
|
|
s = re.sub(r"[^a-zA-Z0-9_]", "_", s)
|
|
|
if not s:
|
|
|
return "entity"
|
|
|
if s[0].isdigit():
|
|
|
s = "_" + s
|
|
|
return s
|
|
|
|
|
|
@classmethod
|
|
|
def business_name_to_path(cls, business_name: str | None) -> str:
|
|
|
"""把 business_name 规范为可用于目录/路由的多段路径(保留 `/`)。
|
|
|
|
|
|
约定:`business_name` 允许 `a/b/c` 表示多级菜单目录。
|
|
|
- 目录/路由:使用完整多段
|
|
|
- 文件名/route_name:使用最后一段 slug(见 `business_name_to_slug`)
|
|
|
|
|
|
参数:
|
|
|
- business_name (str | None): 业务路径或名称。
|
|
|
|
|
|
返回:
|
|
|
- str: 多段路径字符串(小写 slug),默认 entity。
|
|
|
"""
|
|
|
s = (business_name or "").strip().strip("/")
|
|
|
if not s:
|
|
|
return "entity"
|
|
|
# 每段都做一次轻度规范(与 schema 的 slug 规则一致:a-z0-9_)
|
|
|
segs = []
|
|
|
for raw in [p for p in s.split("/") if p]:
|
|
|
seg = re.sub(r"[^a-zA-Z0-9_]", "_", raw).lower()
|
|
|
seg = re.sub(r"_+", "_", seg).strip("_") or "entity"
|
|
|
if seg[0].isdigit():
|
|
|
seg = "_" + seg
|
|
|
segs.append(seg)
|
|
|
return "/".join(segs) if segs else "entity"
|
|
|
|
|
|
@classmethod
|
|
|
def prepare_context(cls, gen_table: GenTableOutSchema) -> dict[str, Any]:
|
|
|
"""
|
|
|
准备模板变量。
|
|
|
|
|
|
参数:
|
|
|
- gen_table (GenTableOutSchema): 生成表的配置信息。
|
|
|
|
|
|
返回:
|
|
|
- Dict[str, Any]: 模板上下文字典。
|
|
|
"""
|
|
|
# 处理options为None的情况
|
|
|
# if not gen_table.options:
|
|
|
# raise ValueError('请先完善生成配置信息')
|
|
|
class_name = gen_table.class_name or ""
|
|
|
package_name = (gen_table.package_name or "").strip()
|
|
|
module_name = (gen_table.module_name or "").strip()
|
|
|
business_name = (gen_table.business_name or "").strip()
|
|
|
function_name = gen_table.function_name or ""
|
|
|
|
|
|
# 生成规则(对齐 module_example/demo):
|
|
|
# - 分系统根:package_name = module_xxx
|
|
|
# - 目录固定为:module_xxx / module_name(不再额外使用业务名作为目录层级)
|
|
|
# - 权限前缀固定为:module_xxx:module_name(操作在模板里再拼 :query/:create...)
|
|
|
business_path = cls.business_name_to_path(business_name)
|
|
|
business_name_slug = cls.business_name_to_slug(business_name)
|
|
|
permission_prefix = ":".join([s for s in [package_name, module_name] if s])
|
|
|
api_route_prefix = cls.get_api_route_prefix(package_name)
|
|
|
|
|
|
_cols = gen_table.columns or []
|
|
|
table_column_names = frozenset(c.column_name for c in _cols if getattr(c, "column_name", None))
|
|
|
has_dict_column = any(
|
|
|
getattr(c, "dict_type", None) for c in _cols
|
|
|
)
|
|
|
has_image_column = any(
|
|
|
getattr(c, "html_type", None) == "imageUpload" for c in _cols
|
|
|
)
|
|
|
|
|
|
sub_class_name = ""
|
|
|
sub_model_class_name = ""
|
|
|
sub_rel_list_name = ""
|
|
|
parent_rel_name = ""
|
|
|
if gen_table.sub and gen_table.sub_table:
|
|
|
st = gen_table.sub_table
|
|
|
scn = (st.class_name or GenUtils.convert_class_name(gen_table.sub_table_name or "")).strip()
|
|
|
sub_class_name = scn
|
|
|
sub_model_class_name = f"{scn}Model"
|
|
|
sub_rel_list_name = f"{SnakeCaseUtil.camel_to_snake(scn)}_list"
|
|
|
parent_rel_name = SnakeCaseUtil.camel_to_snake(gen_table.class_name or "")
|
|
|
|
|
|
context = {
|
|
|
"table_name": gen_table.table_name or "",
|
|
|
"table_comment": gen_table.table_comment or "",
|
|
|
"function_name": function_name if StringUtil.is_not_empty(function_name) else "【请填写功能名称】",
|
|
|
"class_name": class_name,
|
|
|
"module_name": module_name,
|
|
|
"business_name": business_name,
|
|
|
"business_path": business_path,
|
|
|
"business_file": business_name_slug,
|
|
|
"business_name_slug": business_name_slug,
|
|
|
"base_package": cls.get_package_prefix(package_name),
|
|
|
"package_name": package_name,
|
|
|
"menu_route_first_segment": cls.get_menu_route_first_segment(gen_table),
|
|
|
"datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
|
"pk_column": gen_table.pk_column,
|
|
|
"model_import_list": cls.get_model_import_list(gen_table),
|
|
|
"schema_import_list": cls.get_schema_import_list(gen_table),
|
|
|
"permission_prefix": permission_prefix,
|
|
|
"api_route_prefix": api_route_prefix,
|
|
|
"columns": gen_table.columns or [],
|
|
|
"table_column_names": table_column_names,
|
|
|
"table": gen_table,
|
|
|
"dicts": cls.get_dicts(gen_table),
|
|
|
"db_type": settings.DATABASE_TYPE,
|
|
|
"column_not_add_show": GenConstant.COLUMNNAME_NOT_ADD_SHOW,
|
|
|
"column_not_edit_show": GenConstant.COLUMNNAME_NOT_EDIT_SHOW,
|
|
|
"parent_menu_id": int(gen_table.parent_menu_id) if gen_table.parent_menu_id else None,
|
|
|
"is_sub_entity": False,
|
|
|
"sub_class_name": sub_class_name,
|
|
|
"sub_model_class_name": sub_model_class_name,
|
|
|
"sub_module_name": (gen_table.sub_table.module_name if gen_table.sub and gen_table.sub_table else ""),
|
|
|
"sub_rel_list_name": sub_rel_list_name,
|
|
|
"parent_rel_name": parent_rel_name,
|
|
|
"parent_list_rel_name": "",
|
|
|
"parent_table_name": "",
|
|
|
"parent_model_class_name": "",
|
|
|
# 数据表实际主键列名(用于生成前端行键等;ModelMixin 仍默认带 id 字段)
|
|
|
"pk_column_name": (gen_table.pk_column.column_name if gen_table.pk_column else None) or "id",
|
|
|
"parent_pk_column_name": (gen_table.pk_column.column_name if gen_table.pk_column else None) or "id",
|
|
|
"sub_table_fk_name": "",
|
|
|
"has_dict_column": has_dict_column,
|
|
|
"has_image_column": has_image_column,
|
|
|
}
|
|
|
|
|
|
return context
|
|
|
|
|
|
@classmethod
|
|
|
def get_menu_route_first_segment(cls, gen_table: GenTableOutSchema) -> str:
|
|
|
"""
|
|
|
前端页面路由首段(与写入菜单 ``route_path`` 第一段一致):始终为 ``module_xxx``。
|
|
|
|
|
|
懒加载 ``GenTableService`` 避免与 ``service`` 模块循环依赖。
|
|
|
|
|
|
参数:
|
|
|
- gen_table (GenTableOutSchema): 生成表配置。
|
|
|
|
|
|
返回:
|
|
|
- str: 路由首段(module_xxx)。
|
|
|
"""
|
|
|
from app.plugin.module_generator.gencode.service import GenTableService
|
|
|
|
|
|
pid = int(gen_table.parent_menu_id) if gen_table.parent_menu_id is not None else None
|
|
|
return GenTableService._menu_route_first_segment(
|
|
|
pid,
|
|
|
gen_table.package_name or "",
|
|
|
gen_table.module_name,
|
|
|
)
|
|
|
|
|
|
@classmethod
|
|
|
def prepare_sub_render_context(cls, parent: GenTableOutSchema, sub: GenTableOutSchema) -> dict[str, Any]:
|
|
|
"""
|
|
|
子表业务代码渲染上下文(与主表同模块、独立业务目录)。
|
|
|
|
|
|
参数:
|
|
|
- parent (GenTableOutSchema): 主表配置。
|
|
|
- sub (GenTableOutSchema): 子表配置。
|
|
|
|
|
|
返回:
|
|
|
- dict[str, Any]: 子表模板上下文字典。
|
|
|
"""
|
|
|
ctx = cls.prepare_context(sub)
|
|
|
scn = (sub.class_name or GenUtils.convert_class_name(sub.table_name or "")).strip()
|
|
|
ctx["is_sub_entity"] = True
|
|
|
ctx["parent_class_name"] = parent.class_name or ""
|
|
|
ctx["parent_model_class_name"] = f"{parent.class_name}Model"
|
|
|
ctx["parent_table_name"] = parent.table_name or ""
|
|
|
ctx["parent_pk_column_name"] = (parent.pk_column.column_name if parent.pk_column else None) or "id"
|
|
|
ctx["parent_rel_name"] = SnakeCaseUtil.camel_to_snake(parent.class_name or "parent")
|
|
|
ctx["parent_list_rel_name"] = f"{SnakeCaseUtil.camel_to_snake(scn)}_list"
|
|
|
ctx["sub_table_fk_name"] = (parent.sub_table_fk_name or "").strip()
|
|
|
ctx["model_import_list"] = cls.get_model_import_list(sub, is_sub_entity=True)
|
|
|
ctx["schema_import_list"] = cls.get_schema_import_list(sub)
|
|
|
return ctx
|
|
|
|
|
|
@classmethod
|
|
|
def get_template_list(cls):
|
|
|
"""
|
|
|
获取主表模板列表。
|
|
|
|
|
|
参数:
|
|
|
- 无
|
|
|
返回:
|
|
|
- List[str]: 模板路径列表。
|
|
|
"""
|
|
|
templates = [
|
|
|
"python/controller.py.jinja2",
|
|
|
"python/service.py.jinja2",
|
|
|
"python/crud.py.jinja2",
|
|
|
"python/schema.py.jinja2",
|
|
|
"python/model.py.jinja2",
|
|
|
"python/__init__.py.jinja2",
|
|
|
"ts/api.ts.jinja2",
|
|
|
"vue/index.vue.jinja2",
|
|
|
]
|
|
|
return templates
|
|
|
|
|
|
@classmethod
|
|
|
def get_sub_table_template_list(cls):
|
|
|
"""
|
|
|
获取子表模板列表(仅 model / schema / __init__,不含 controller/service/crud/vue/api)。
|
|
|
|
|
|
参数:
|
|
|
- 无
|
|
|
|
|
|
返回:
|
|
|
- List[str]: 子表模板路径列表。
|
|
|
"""
|
|
|
return [
|
|
|
"python/model.py.jinja2",
|
|
|
"python/schema.py.jinja2",
|
|
|
"python/__init__.py.jinja2",
|
|
|
]
|
|
|
|
|
|
@classmethod
|
|
|
def get_file_name(cls, template: str, gen_table: GenTableOutSchema):
|
|
|
"""
|
|
|
根据模板生成文件名。
|
|
|
|
|
|
参数:
|
|
|
- template (str): 模板路径字符串。
|
|
|
- gen_table (GenTableOutSchema): 生成表的配置信息。
|
|
|
|
|
|
返回:
|
|
|
- str: 模板生成的文件名。
|
|
|
|
|
|
异常:
|
|
|
- ValueError: 当无法生成有效文件名时抛出。
|
|
|
"""
|
|
|
package_name = (gen_table.package_name or "").strip()
|
|
|
module_name = (gen_table.module_name or "").strip()
|
|
|
|
|
|
if not package_name:
|
|
|
raise ValueError(f"无法为模板 {template} 生成文件名:包名未设置")
|
|
|
if not module_name:
|
|
|
raise ValueError(f"无法为模板 {template} 生成文件名:模块名未设置")
|
|
|
|
|
|
# 目录固定为:module_xxx/{module_name}
|
|
|
backend_base = f"{cls.BACKEND_PROJECT_PATH}/app/plugin/{package_name}"
|
|
|
frontend_view_base = f"{cls.FRONTEND_PROJECT_PATH}/src/views/{package_name}"
|
|
|
frontend_api_base = f"{cls.FRONTEND_PROJECT_PATH}/src/api/{package_name}"
|
|
|
|
|
|
backend_dir = f"{backend_base}/{module_name}"
|
|
|
view_dir = f"{frontend_view_base}/{module_name}"
|
|
|
api_path = f"{frontend_api_base}/{module_name}.ts"
|
|
|
|
|
|
template_mapping = {
|
|
|
"controller.py.jinja2": f"{backend_dir}/controller.py",
|
|
|
"service.py.jinja2": f"{backend_dir}/service.py",
|
|
|
"crud.py.jinja2": f"{backend_dir}/crud.py",
|
|
|
"schema.py.jinja2": f"{backend_dir}/schema.py",
|
|
|
"model.py.jinja2": f"{backend_dir}/model.py",
|
|
|
"__init__.py.jinja2": f"{backend_dir}/__init__.py",
|
|
|
"api.ts.jinja2": api_path,
|
|
|
"index.vue.jinja2": f"{view_dir}/index.vue",
|
|
|
}
|
|
|
|
|
|
# 查找匹配的模板路径
|
|
|
for key, path in template_mapping.items():
|
|
|
if key in template:
|
|
|
return path
|
|
|
|
|
|
# 遍历完所有映射都没找到匹配项,才抛出异常
|
|
|
raise ValueError(f"未找到模板 '{template}' 的路径映射")
|
|
|
|
|
|
@classmethod
|
|
|
def get_package_prefix(cls, package_name: str) -> str:
|
|
|
"""
|
|
|
获取包前缀。
|
|
|
|
|
|
参数:
|
|
|
- package_name (str): 包名。
|
|
|
|
|
|
返回:
|
|
|
- str: 包前缀。
|
|
|
"""
|
|
|
# 修复:当包名中不存在'.'时,直接返回原包名
|
|
|
return package_name[: package_name.rfind(".")] if "." in package_name else package_name
|
|
|
|
|
|
@classmethod
|
|
|
def get_schema_import_list(cls, gen_table: GenTableOutSchema):
|
|
|
"""
|
|
|
获取 schema 模板所需的 Python 导入语句集合。
|
|
|
|
|
|
参数:
|
|
|
- gen_table (GenTableOutSchema): 生成表配置(含主子表列)。
|
|
|
|
|
|
返回:
|
|
|
- set[str]: 导入语句字符串集合。
|
|
|
"""
|
|
|
columns = gen_table.columns or []
|
|
|
import_list = set()
|
|
|
has_datetime_import = False
|
|
|
has_date_import = False
|
|
|
has_time_import = False
|
|
|
|
|
|
for column in columns:
|
|
|
# 处理datetime类型的导入
|
|
|
if column.python_type and column.python_type in GenConstant.TYPE_DATE:
|
|
|
if column.python_type == "datetime":
|
|
|
has_datetime_import = True
|
|
|
elif column.python_type == "date":
|
|
|
has_date_import = True
|
|
|
elif column.python_type == "time":
|
|
|
has_time_import = True
|
|
|
elif column.python_type == GenConstant.TYPE_DECIMAL:
|
|
|
import_list.add("from decimal import Decimal")
|
|
|
|
|
|
if gen_table.sub and gen_table.sub_table and gen_table.sub_table.columns:
|
|
|
sub_columns = gen_table.sub_table.columns or []
|
|
|
for sub_column in sub_columns:
|
|
|
# 处理datetime类型的导入
|
|
|
if sub_column.python_type and sub_column.python_type in GenConstant.TYPE_DATE:
|
|
|
if sub_column.python_type == "datetime":
|
|
|
has_datetime_import = True
|
|
|
elif sub_column.python_type == "date":
|
|
|
has_date_import = True
|
|
|
elif sub_column.python_type == "time":
|
|
|
has_time_import = True
|
|
|
elif sub_column.python_type == GenConstant.TYPE_DECIMAL:
|
|
|
import_list.add("from decimal import Decimal")
|
|
|
|
|
|
# 添加datetime导入
|
|
|
if has_datetime_import:
|
|
|
import_list.add("from datetime import datetime")
|
|
|
if has_date_import:
|
|
|
import_list.add("from datetime import date")
|
|
|
if has_time_import:
|
|
|
import_list.add("from datetime import time")
|
|
|
|
|
|
return import_list
|
|
|
|
|
|
@classmethod
|
|
|
def get_model_import_list(cls, gen_table: GenTableOutSchema, *, is_sub_entity: bool = False) -> list[str]:
|
|
|
"""
|
|
|
获取 model 模板所需的 Python 导入语句列表(含合并后的 sqlalchemy 导入)。
|
|
|
|
|
|
参数:
|
|
|
- gen_table (GenTableOutSchema): 生成表配置。
|
|
|
- is_sub_entity (bool): 是否为子表独立生成(含外键与 relationship)。
|
|
|
|
|
|
返回:
|
|
|
- list[str]: 导入语句列表。
|
|
|
"""
|
|
|
columns = gen_table.columns or []
|
|
|
import_list = set()
|
|
|
has_datetime_import = False
|
|
|
has_date_import = False
|
|
|
has_time_import = False
|
|
|
|
|
|
# 基类 ModelMixin/TenantMixin/UserMixin 已定义的列,无需导入 SQLAlchemy 类型
|
|
|
_BASE_MODEL_COLUMNS = {
|
|
|
'id', 'uuid', 'tenant_id',
|
|
|
'created_time', 'updated_time', 'created_id', 'updated_id',
|
|
|
'is_deleted', 'deleted_time', 'deleted_id',
|
|
|
}
|
|
|
|
|
|
for column in columns:
|
|
|
if column.column_name in _BASE_MODEL_COLUMNS:
|
|
|
continue
|
|
|
if column.column_type:
|
|
|
data_type = cls.get_db_type(column.column_type)
|
|
|
if data_type in GenConstant.COLUMNTYPE_GEOMETRY:
|
|
|
import_list.add("from geoalchemy2 import Geometry")
|
|
|
import_list.add(f"from sqlalchemy import {StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY, data_type)}")
|
|
|
# 处理datetime类型的导入
|
|
|
if column.python_type and column.python_type in GenConstant.TYPE_DATE:
|
|
|
if column.python_type == "datetime":
|
|
|
has_datetime_import = True
|
|
|
elif column.python_type == "date":
|
|
|
has_date_import = True
|
|
|
elif column.python_type == "time":
|
|
|
has_time_import = True
|
|
|
# 处理Decimal类型的导入
|
|
|
elif column.python_type == GenConstant.TYPE_DECIMAL:
|
|
|
import_list.add("from decimal import Decimal")
|
|
|
if gen_table.sub or is_sub_entity:
|
|
|
import_list.add("from sqlalchemy import ForeignKey")
|
|
|
if gen_table.sub and not is_sub_entity and gen_table.sub_table and gen_table.sub_table.columns:
|
|
|
sub_columns = gen_table.sub_table.columns or []
|
|
|
for sub_column in sub_columns:
|
|
|
if sub_column.column_type:
|
|
|
data_type = cls.get_db_type(sub_column.column_type)
|
|
|
import_list.add(f"from sqlalchemy import {StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY, data_type)}")
|
|
|
# 处理datetime类型的导入
|
|
|
if sub_column.python_type and sub_column.python_type in GenConstant.TYPE_DATE:
|
|
|
if sub_column.python_type == "datetime":
|
|
|
has_datetime_import = True
|
|
|
elif sub_column.python_type == "date":
|
|
|
has_date_import = True
|
|
|
elif sub_column.python_type == "time":
|
|
|
has_time_import = True
|
|
|
# 处理Decimal类型的导入
|
|
|
elif sub_column.python_type == GenConstant.TYPE_DECIMAL:
|
|
|
import_list.add("from decimal import Decimal")
|
|
|
|
|
|
# 添加datetime导入
|
|
|
if has_datetime_import:
|
|
|
import_list.add("from datetime import datetime")
|
|
|
if has_date_import:
|
|
|
import_list.add("from datetime import date")
|
|
|
if has_time_import:
|
|
|
import_list.add("from datetime import time")
|
|
|
|
|
|
merged = cls.merge_same_imports(list(import_list), "from sqlalchemy import")
|
|
|
if gen_table.sub or is_sub_entity:
|
|
|
merged.append("from sqlalchemy.orm import relationship")
|
|
|
return merged
|
|
|
|
|
|
@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()
|
|
|
|
|
|
# 处理PostgreSQL数组类型(如 integer[], text[])
|
|
|
if "[]" in column_type:
|
|
|
return "array"
|
|
|
|
|
|
# 提取基本类型
|
|
|
if "(" in column_type:
|
|
|
return column_type.split("(")[0]
|
|
|
return column_type
|
|
|
|
|
|
@classmethod
|
|
|
def merge_same_imports(cls, imports: list[str], import_start: str) -> list[str]:
|
|
|
"""
|
|
|
合并相同的导入语句。
|
|
|
|
|
|
参数:
|
|
|
- imports (list[str]): 导入语句列表。
|
|
|
- import_start (str): 导入语句的起始字符串。
|
|
|
|
|
|
返回:
|
|
|
- list[str]: 合并后的导入语句列表。
|
|
|
"""
|
|
|
merged_imports = []
|
|
|
imports_ = []
|
|
|
for import_stmt in imports:
|
|
|
if import_stmt.startswith(import_start):
|
|
|
imported_items = import_stmt.split("import")[1].strip()
|
|
|
imports_.extend(imported_items.split(", "))
|
|
|
else:
|
|
|
merged_imports.append(import_stmt)
|
|
|
|
|
|
if imports_:
|
|
|
# 去重并过滤空字符串,然后用逗号连接
|
|
|
unique_imports = [item for item in imports_ if item]
|
|
|
if len(unique_imports) > 0:
|
|
|
merged_datetime_import = f"{import_start} {', '.join(unique_imports)}"
|
|
|
merged_imports.append(merged_datetime_import)
|
|
|
|
|
|
return merged_imports
|
|
|
|
|
|
@classmethod
|
|
|
def get_dicts(cls, gen_table: GenTableOutSchema):
|
|
|
"""
|
|
|
获取字典列表。
|
|
|
|
|
|
参数:
|
|
|
- gen_table (GenTableOutSchema): 生成表的配置信息。
|
|
|
|
|
|
返回:
|
|
|
- str: 以逗号分隔的字典类型字符串。
|
|
|
"""
|
|
|
columns = gen_table.columns or []
|
|
|
dicts = set()
|
|
|
cls.add_dicts(dicts, columns)
|
|
|
# 处理sub_table为None的情况
|
|
|
if gen_table.sub_table is not None:
|
|
|
# 处理sub_table.columns为None的情况
|
|
|
sub_columns = gen_table.sub_table.columns or []
|
|
|
cls.add_dicts(dicts, sub_columns)
|
|
|
return ", ".join(dicts)
|
|
|
|
|
|
@classmethod
|
|
|
def add_dicts(cls, dicts: set[str], columns: list[GenTableColumnOutSchema]) -> None:
|
|
|
"""
|
|
|
添加字典类型到集合。
|
|
|
|
|
|
参数:
|
|
|
- dicts (set[str]): 字典类型集合。
|
|
|
- columns (list[GenTableColumnOutSchema]): 字段列表。
|
|
|
|
|
|
返回:
|
|
|
- set[str]: 更新后的字典类型集合。
|
|
|
"""
|
|
|
for column in columns:
|
|
|
super_column = column.super_column if column.super_column is not None else "0"
|
|
|
dict_type = column.dict_type or ""
|
|
|
html_type = column.html_type or ""
|
|
|
|
|
|
if (
|
|
|
not super_column
|
|
|
and StringUtil.is_not_empty(dict_type)
|
|
|
and StringUtil.equals_any_ignore_case(
|
|
|
html_type,
|
|
|
[
|
|
|
GenConstant.HTML_SELECT,
|
|
|
GenConstant.HTML_RADIO,
|
|
|
GenConstant.HTML_CHECKBOX,
|
|
|
],
|
|
|
)
|
|
|
):
|
|
|
dicts.add(f"'{dict_type}'")
|
|
|
|
|
|
@classmethod
|
|
|
def get_permission_prefix(cls, module_name: str | None, business_name: str | None) -> str:
|
|
|
"""
|
|
|
获取权限前缀。
|
|
|
|
|
|
参数:
|
|
|
- module_name (str | None): 模块名。
|
|
|
- business_name (str | None): 业务名。
|
|
|
|
|
|
返回:
|
|
|
- str: 权限前缀字符串。
|
|
|
"""
|
|
|
mn = (module_name or "").strip()
|
|
|
bn = (business_name or "").strip().replace("/", ":")
|
|
|
if not bn:
|
|
|
return mn
|
|
|
return f"{mn}:{bn}"
|
|
|
|
|
|
@classmethod
|
|
|
def python_type_to_ts_type(cls, python_type: str | None) -> str:
|
|
|
"""
|
|
|
将列上的 Python 类型(`get_db_type` + `DB_TO_PYTHON` 映射结果)转为前端 TS 类型片段。
|
|
|
|
|
|
与 JSON 序列化习惯一致:Decimal、日期时间多为字符串;dict/list 用宽松类型。
|
|
|
|
|
|
参数:
|
|
|
- python_type (str | None): Python 类型名。
|
|
|
|
|
|
返回:
|
|
|
- str: 前端 TypeScript 类型片段。
|
|
|
"""
|
|
|
if not python_type or not str(python_type).strip():
|
|
|
return "string"
|
|
|
p = str(python_type).strip()
|
|
|
mapping: dict[str, str] = {
|
|
|
"int": "number",
|
|
|
"float": "number",
|
|
|
"bool": "boolean",
|
|
|
"Decimal": "string",
|
|
|
"date": "string",
|
|
|
"time": "string",
|
|
|
"datetime": "string",
|
|
|
"timedelta": "string",
|
|
|
"dict": "Record<string, unknown>",
|
|
|
"list": "unknown[]",
|
|
|
"bytes": "string",
|
|
|
"str": "string",
|
|
|
}
|
|
|
return mapping.get(p, "string")
|
|
|
|
|
|
@classmethod
|
|
|
def get_api_route_prefix(cls, module_name: str | None) -> str:
|
|
|
"""
|
|
|
获取前端 API 路径首段,与 `discover` 中插件路由前缀一致(`module_xxx` → `xxx`)。
|
|
|
|
|
|
参数:
|
|
|
- module_name (str | None): 模块名,如 ``module_example``。
|
|
|
|
|
|
返回:
|
|
|
- str: 路由前缀,如 ``example``。
|
|
|
"""
|
|
|
if not module_name:
|
|
|
return ""
|
|
|
if module_name.startswith("module_"):
|
|
|
return module_name[7:]
|
|
|
return module_name
|
|
|
|
|
|
@classmethod
|
|
|
def get_sqlalchemy_type(cls, column: Any) -> str:
|
|
|
"""
|
|
|
获取 SQLAlchemy 类型。
|
|
|
|
|
|
参数:
|
|
|
- column (Any): 列对象或列类型字符串。
|
|
|
|
|
|
返回:
|
|
|
- str: SQLAlchemy 类型字符串。
|
|
|
"""
|
|
|
# 获取column_type和column_length
|
|
|
column_type = column
|
|
|
column_length = None
|
|
|
|
|
|
# 检查是否是对象
|
|
|
if hasattr(column, "column_type"):
|
|
|
column_type = column.column_type or ""
|
|
|
column_length = column.column_length or None
|
|
|
|
|
|
column_type = cls.normalize_db_column_type_for_mapping(column_type)
|
|
|
|
|
|
# MySQL:仅 tinyint(1) 映射为 Boolean;其余 tinyint 走 SmallInteger(见 GenConstant.DB_TO_SQLALCHEMY)
|
|
|
ct_norm = (column_type or "").strip()
|
|
|
if settings.DATABASE_TYPE != "postgres" and ct_norm:
|
|
|
ct_lower = ct_norm.lower()
|
|
|
if ct_lower.startswith("tinyint(1)"):
|
|
|
return "Boolean"
|
|
|
|
|
|
# 首先尝试匹配完整类型(包括括号)
|
|
|
sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY, column_type)
|
|
|
|
|
|
# 特殊处理PostgreSQL类型
|
|
|
if settings.DATABASE_TYPE == "postgres":
|
|
|
if column_type.upper() == "BOOLEAN":
|
|
|
return "Boolean"
|
|
|
elif column_type.upper() == "REAL":
|
|
|
return "Float"
|
|
|
elif column_type.upper() == "DOUBLE PRECISION":
|
|
|
return "Float"
|
|
|
elif column_type.upper() == "TIMESTAMP":
|
|
|
return "DateTime"
|
|
|
elif column_type.upper() == "JSONB":
|
|
|
return "JSONB"
|
|
|
elif column_type.upper() == "UUID":
|
|
|
return "Uuid"
|
|
|
elif column_type.upper() == "BYTEA":
|
|
|
return "LargeBinary"
|
|
|
|
|
|
# get_mapping_value_by_key_ignore_case 未命中时返回 "",须与 None 同样视为未匹配
|
|
|
if not sqlalchemy_type and "(" in column_type:
|
|
|
# 如果没有匹配到,再尝试剥离括号
|
|
|
column_type_list = column_type.split("(")
|
|
|
col_type = column_type_list[0]
|
|
|
# 将 'character' 映射为 'char' 以匹配常量定义
|
|
|
if col_type.lower() == "character":
|
|
|
col_type = "char"
|
|
|
sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY, col_type)
|
|
|
# 如果是字符串类型且包含括号参数,保持原参数
|
|
|
if sqlalchemy_type in ["String", "CHAR"]:
|
|
|
sqlalchemy_type += "(" + column_type_list[1]
|
|
|
# 如果是Numeric或DECIMAL类型且包含括号参数,保持原参数
|
|
|
elif sqlalchemy_type in ["Numeric", "DECIMAL"]:
|
|
|
sqlalchemy_type += "(" + column_type_list[1]
|
|
|
elif not sqlalchemy_type:
|
|
|
# 处理没有括号的类型
|
|
|
col_type = column_type
|
|
|
# 将 'character' 映射为 'char' 以匹配常量定义
|
|
|
if col_type.lower() == "character":
|
|
|
col_type = "char"
|
|
|
sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY, col_type)
|
|
|
# 如果是字符串类型且没有指定长度,使用column_length或默认255
|
|
|
if sqlalchemy_type in ["String", "CHAR"]:
|
|
|
length = column_length if column_length and column_length.isdigit() else "255"
|
|
|
sqlalchemy_type += f"({length})"
|
|
|
else:
|
|
|
# 对于已经匹配到的类型,如果是字符串类型且column有长度信息,添加长度
|
|
|
if sqlalchemy_type in ["String", "CHAR"] and "(" not in sqlalchemy_type:
|
|
|
# 检查column_length是否有效
|
|
|
length = column_length if column_length and column_length.isdigit() else "255"
|
|
|
sqlalchemy_type += f"({length})"
|
|
|
|
|
|
# 如果没有找到匹配的类型,使用String(column_length)或String(255)作为默认类型
|
|
|
if not sqlalchemy_type:
|
|
|
length = column_length if column_length and column_length.isdigit() else "255"
|
|
|
sqlalchemy_type = f"String({length})"
|
|
|
return sqlalchemy_type
|