341 lines
10 KiB
Python
341 lines
10 KiB
Python
"""
|
|
字段元数据自动生成工具
|
|
从 SQLAlchemy Model 或 Pydantic Schema 自动生成 FIELD_METADATA
|
|
"""
|
|
from typing import Dict, Any, Type, get_origin, get_args, Union
|
|
from sqlalchemy import inspect
|
|
from sqlalchemy.orm import DeclarativeMeta
|
|
from pydantic import BaseModel
|
|
import inspect as py_inspect
|
|
|
|
|
|
def generate_field_metadata(
|
|
model: Type[DeclarativeMeta],
|
|
sensitive_fields: list = None,
|
|
maskable_fields: list = None,
|
|
hidden_fields: list = None,
|
|
field_labels: Dict[str, str] = None
|
|
) -> Dict[str, Dict[str, Any]]:
|
|
"""
|
|
从 SQLAlchemy Model 自动生成字段元数据
|
|
|
|
Args:
|
|
model: SQLAlchemy Model 类
|
|
sensitive_fields: 敏感字段列表,如 ['mobile', 'email']
|
|
maskable_fields: 可脱敏字段列表,如 ['mobile', 'email', 'id_card']
|
|
hidden_fields: 默认隐藏字段列表,如 ['password']
|
|
field_labels: 字段中文名称映射,如 {'name': '姓名', 'mobile': '手机号'}
|
|
|
|
Returns:
|
|
字段元数据字典
|
|
|
|
Example:
|
|
>>> FIELD_METADATA = generate_field_metadata(
|
|
... User,
|
|
... sensitive_fields=['mobile', 'email', 'password'],
|
|
... maskable_fields=['mobile', 'email'],
|
|
... hidden_fields=['password'],
|
|
... field_labels={'name': '姓名', 'mobile': '手机号'}
|
|
... )
|
|
"""
|
|
sensitive_fields = sensitive_fields or []
|
|
maskable_fields = maskable_fields or []
|
|
hidden_fields = hidden_fields or []
|
|
field_labels = field_labels or {}
|
|
|
|
metadata = {}
|
|
|
|
# 获取 Model 的所有列
|
|
mapper = inspect(model)
|
|
|
|
for column in mapper.columns:
|
|
field_name = column.name
|
|
|
|
# 跳过内部字段
|
|
if field_name.startswith('_'):
|
|
continue
|
|
|
|
# 确定字段类型
|
|
field_type = _get_field_type(column.type)
|
|
|
|
# 确定默认权限
|
|
default_permission = "hidden" if field_name in hidden_fields else "read"
|
|
|
|
# 生成字段标签
|
|
label = field_labels.get(field_name) or _generate_label(field_name, column.comment)
|
|
|
|
metadata[field_name] = {
|
|
"label": label,
|
|
"field_type": field_type,
|
|
"sensitive": field_name in sensitive_fields,
|
|
"maskable": field_name in maskable_fields,
|
|
"default_permission": default_permission
|
|
}
|
|
|
|
return metadata
|
|
|
|
|
|
def _get_field_type(column_type) -> str:
|
|
"""
|
|
根据 SQLAlchemy 列类型确定字段类型
|
|
|
|
Args:
|
|
column_type: SQLAlchemy 列类型
|
|
|
|
Returns:
|
|
字段类型字符串: string/integer/boolean/datetime/float
|
|
"""
|
|
type_name = column_type.__class__.__name__.lower()
|
|
|
|
if 'int' in type_name or 'serial' in type_name:
|
|
return "integer"
|
|
elif 'bool' in type_name:
|
|
return "boolean"
|
|
elif 'date' in type_name or 'time' in type_name:
|
|
return "datetime"
|
|
elif 'float' in type_name or 'numeric' in type_name or 'decimal' in type_name:
|
|
return "float"
|
|
else:
|
|
return "string"
|
|
|
|
|
|
def _generate_label(field_name: str, comment: str = None) -> str:
|
|
"""
|
|
生成字段标签
|
|
|
|
优先使用数据库注释,如果没有则根据字段名生成
|
|
|
|
Args:
|
|
field_name: 字段名
|
|
comment: 数据库注释
|
|
|
|
Returns:
|
|
字段标签
|
|
"""
|
|
if comment:
|
|
return comment
|
|
|
|
# 常见字段名映射
|
|
common_labels = {
|
|
'id': 'ID',
|
|
'name': '名称',
|
|
'code': '编码',
|
|
'title': '标题',
|
|
'description': '描述',
|
|
'remark': '备注',
|
|
'status': '状态',
|
|
'sort': '排序',
|
|
'create_time': '创建时间',
|
|
'update_time': '更新时间',
|
|
'created_at': '创建时间',
|
|
'updated_at': '更新时间',
|
|
'is_deleted': '是否删除',
|
|
'is_active': '是否激活',
|
|
'username': '用户名',
|
|
'password': '密码',
|
|
'email': '邮箱',
|
|
'mobile': '手机号',
|
|
'phone': '电话',
|
|
'address': '地址',
|
|
'avatar': '头像',
|
|
'gender': '性别',
|
|
'age': '年龄',
|
|
'dept_id': '部门ID',
|
|
'user_id': '用户ID',
|
|
'role_id': '角色ID',
|
|
}
|
|
|
|
# 如果在常见映射中,直接返回
|
|
if field_name in common_labels:
|
|
return common_labels[field_name]
|
|
|
|
# 处理带前缀的字段
|
|
if field_name.startswith('sys_'):
|
|
base_name = field_name[4:]
|
|
if base_name in common_labels:
|
|
return f"系统{common_labels[base_name]}"
|
|
|
|
# 处理下划线分隔的字段名
|
|
if '_' in field_name:
|
|
parts = field_name.split('_')
|
|
# 尝试翻译每个部分
|
|
translated_parts = [common_labels.get(part, part.title()) for part in parts]
|
|
return ''.join(translated_parts)
|
|
|
|
# 默认返回首字母大写的字段名
|
|
return field_name.replace('_', ' ').title()
|
|
|
|
|
|
def generate_field_metadata_from_schema(
|
|
schema: Type[BaseModel],
|
|
model: Type[DeclarativeMeta] = None
|
|
) -> Dict[str, Dict[str, Any]]:
|
|
"""
|
|
从 Pydantic Response Schema 生成字段元数据
|
|
|
|
优势:
|
|
1. 根据 Schema 的 Optional 类型判断字段是否必填
|
|
2. 必填字段标记为 required=True,前端禁止隐藏
|
|
3. 可选字段可以被隐藏
|
|
|
|
Args:
|
|
schema: Pydantic Response Schema 类
|
|
model: SQLAlchemy Model 类(可选,用于获取数据库注释)
|
|
|
|
Returns:
|
|
字段元数据字典
|
|
"""
|
|
metadata = {}
|
|
|
|
# 获取 Schema 的所有字段
|
|
schema_fields = schema.model_fields
|
|
|
|
# 如果提供了 model,获取数据库注释
|
|
db_comments = {}
|
|
if model:
|
|
mapper = inspect(model)
|
|
for column in mapper.columns:
|
|
if column.comment:
|
|
db_comments[column.name] = column.comment
|
|
|
|
# 自动识别敏感/可脱敏字段的关键词
|
|
sensitive_keywords = ['password', 'passwd', 'pwd', 'secret', 'token', 'key']
|
|
maskable_keywords = ['mobile', 'phone', 'tel', 'email', 'mail', 'id_card', 'idcard', 'name']
|
|
hidden_keywords = ['password', 'passwd', 'pwd', 'secret', 'token', 'key']
|
|
|
|
for field_name, field_info in schema_fields.items():
|
|
# 判断字段是否必填(非 Optional)
|
|
is_required = field_info.is_required()
|
|
|
|
# 获取字段类型
|
|
field_type = _get_pydantic_field_type(field_info.annotation)
|
|
|
|
# 生成标签
|
|
label = db_comments.get(field_name) or _generate_label(field_name)
|
|
|
|
# 判断是否敏感/可脱敏
|
|
field_name_lower = field_name.lower()
|
|
is_sensitive = any(keyword in field_name_lower for keyword in sensitive_keywords)
|
|
is_maskable = any(keyword in field_name_lower for keyword in maskable_keywords)
|
|
is_hidden = any(keyword in field_name_lower for keyword in hidden_keywords)
|
|
|
|
# 默认权限
|
|
default_permission = "hidden" if is_hidden else "read"
|
|
|
|
metadata[field_name] = {
|
|
"label": label,
|
|
"field_type": field_type,
|
|
"required": is_required, # 必填字段,前端禁止隐藏
|
|
"sensitive": is_sensitive,
|
|
"maskable": is_maskable,
|
|
"default_permission": default_permission
|
|
}
|
|
|
|
return metadata
|
|
|
|
|
|
def _get_pydantic_field_type(annotation) -> str:
|
|
"""
|
|
从 Pydantic 字段类型获取字段类型字符串
|
|
|
|
Args:
|
|
annotation: Pydantic 字段类型注解
|
|
|
|
Returns:
|
|
字段类型字符串
|
|
"""
|
|
# 处理 Optional 类型
|
|
origin = get_origin(annotation)
|
|
if origin is Union:
|
|
args = get_args(annotation)
|
|
# Optional[X] 实际是 Union[X, None],取第一个非 None 类型
|
|
annotation = next((arg for arg in args if arg is not type(None)), str)
|
|
|
|
# 获取类型名称
|
|
if hasattr(annotation, '__name__'):
|
|
type_name = annotation.__name__.lower()
|
|
else:
|
|
type_name = str(annotation).lower()
|
|
|
|
if 'int' in type_name:
|
|
return "integer"
|
|
elif 'bool' in type_name:
|
|
return "boolean"
|
|
elif 'datetime' in type_name or 'date' in type_name:
|
|
return "datetime"
|
|
elif 'float' in type_name or 'decimal' in type_name:
|
|
return "float"
|
|
else:
|
|
return "string"
|
|
|
|
|
|
def auto_generate_field_metadata(model: Type[DeclarativeMeta]) -> Dict[str, Dict[str, Any]]:
|
|
"""
|
|
完全自动生成字段元数据(使用默认规则)
|
|
|
|
自动识别敏感字段:
|
|
- password, passwd, pwd: 密码相关
|
|
- mobile, phone, tel: 手机号相关
|
|
- email, mail: 邮箱相关
|
|
- id_card, idcard, identity: 身份证相关
|
|
- bank_card, bankcard: 银行卡相关
|
|
- secret, token, key: 密钥相关
|
|
|
|
Args:
|
|
model: SQLAlchemy Model 类
|
|
|
|
Returns:
|
|
字段元数据字典
|
|
"""
|
|
# 自动识别敏感字段
|
|
sensitive_keywords = [
|
|
'password', 'passwd', 'pwd',
|
|
'mobile', 'phone', 'tel',
|
|
'email', 'mail',
|
|
'id_card', 'idcard', 'identity',
|
|
'bank_card', 'bankcard',
|
|
'secret', 'token', 'key'
|
|
]
|
|
|
|
# 自动识别可脱敏字段
|
|
maskable_keywords = [
|
|
'mobile', 'phone', 'tel',
|
|
'email', 'mail',
|
|
'id_card', 'idcard', 'identity',
|
|
'name', # 姓名可脱敏
|
|
]
|
|
|
|
# 自动识别隐藏字段
|
|
hidden_keywords = [
|
|
'password', 'passwd', 'pwd',
|
|
'secret', 'token', 'key'
|
|
]
|
|
|
|
mapper = inspect(model)
|
|
|
|
sensitive_fields = []
|
|
maskable_fields = []
|
|
hidden_fields = []
|
|
|
|
for column in mapper.columns:
|
|
field_name = column.name.lower()
|
|
|
|
# 检查是否为敏感字段
|
|
if any(keyword in field_name for keyword in sensitive_keywords):
|
|
sensitive_fields.append(column.name)
|
|
|
|
# 检查是否可脱敏
|
|
if any(keyword in field_name for keyword in maskable_keywords):
|
|
maskable_fields.append(column.name)
|
|
|
|
# 检查是否默认隐藏
|
|
if any(keyword in field_name for keyword in hidden_keywords):
|
|
hidden_fields.append(column.name)
|
|
|
|
return generate_field_metadata(
|
|
model,
|
|
sensitive_fields=sensitive_fields,
|
|
maskable_fields=maskable_fields,
|
|
hidden_fields=hidden_fields
|
|
)
|