""" 字段元数据自动生成工具 从 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 )