#!/usr/bin/env python # -*- coding: utf-8 -*- """ 表单元数据管理服务(异步版本) 数据权限: - 使用 list_with_data_scope() 自动应用数据权限 - 支持本人、本部门、本部门及下级、全部等数据范围 """ import logging from typing import Any, Dict, List, Optional, Tuple from sqlalchemy import select, update, delete, func, and_, or_ from sqlalchemy.ext.asyncio import AsyncSession from core.application.model import Application from online_dev.form_manager.model import FormMeta, FormSubTable from app.data_scope_utils import get_data_scope_filter, apply_data_scope_to_conditions logger = logging.getLogger(__name__) # 资源类型(用于数据权限配置) RESOURCE_TYPE = "form" RESOURCE_DISPLAY_NAME = "表单管理" class FormServiceException(Exception): """表单服务异常""" pass class FormService: """表单元数据管理服务""" @staticmethod def build_app_scope_condition( application_id: str = None, *, for_selection: bool = False, ): """ 构建表单应用范围过滤条件。 for_selection=True 时,除本应用表单外,还包含其他应用中已发布且全局可见的表单。 """ if application_id: local = FormMeta.application_id == application_id else: local = FormMeta.application_id.is_(None) if not for_selection: return local shared = and_( FormMeta.globally_visible == True, FormMeta.status == "published", ) return or_(local, shared) # ============ 查询 ============ @staticmethod async def list( db: AsyncSession, page: int = 1, page_size: int = 20, application_id: str = None, name: str = None, code: str = None, form_type: str = None, status: str = None, include_globally_visible: bool = False, ) -> Dict[str, Any]: """分页查询表单列表(包含应用名称)""" conditions = [FormMeta.is_deleted == False] # 应用过滤 conditions.append( FormService.build_app_scope_condition( application_id, for_selection=include_globally_visible, ) ) if name: conditions.append(FormMeta.name.ilike(f"%{name}%")) if code: conditions.append(FormMeta.code.ilike(f"%{code}%")) if form_type: conditions.append(FormMeta.form_type == form_type) if status: conditions.append(FormMeta.status == status) # 获取总数 count_stmt = select(func.count(FormMeta.id)).where(and_(*conditions)) total_result = await db.execute(count_stmt) total = total_result.scalar() or 0 # 获取列表(使用 LEFT JOIN 查询应用名称和编码) offset = (page - 1) * page_size stmt = ( select( FormMeta, Application.name.label('application_name'), Application.code.label('application_code'), ) .outerjoin(Application, FormMeta.application_id == Application.id) .where(and_(*conditions)) .order_by(FormMeta.sort, FormMeta.sys_create_datetime.desc()) .offset(offset) .limit(page_size) ) result = await db.execute(stmt) items = [] for form, app_name, app_code in result: form.application_name = app_name or "主应用" form.application_code = app_code or "" items.append(form) return { "items": items, "total": total, "page": page, "page_size": page_size } @staticmethod async def list_with_data_scope( db: AsyncSession, page: int = 1, page_size: int = 20, application_id: str = None, name: str = None, code: str = None, form_type: str = None, status: str = None, include_globally_visible: bool = False, ) -> Dict[str, Any]: """ 分页查询表单列表(带数据权限过滤) 自动从上下文获取当前用户信息,应用数据权限过滤 """ conditions = [FormMeta.is_deleted == False] # 应用过滤 conditions.append( FormService.build_app_scope_condition( application_id, for_selection=include_globally_visible, ) ) if name: conditions.append(FormMeta.name.ilike(f"%{name}%")) if code: conditions.append(FormMeta.code.ilike(f"%{code}%")) if form_type: conditions.append(FormMeta.form_type == form_type) if status: conditions.append(FormMeta.status == status) # 获取数据权限过滤条件并应用 data_scope_filter = await get_data_scope_filter(db, RESOURCE_TYPE) scope_conditions = apply_data_scope_to_conditions(FormMeta, data_scope_filter) conditions.extend(scope_conditions) # 获取总数 count_stmt = select(func.count(FormMeta.id)).where(and_(*conditions)) total_result = await db.execute(count_stmt) total = total_result.scalar() or 0 # 获取列表(使用 LEFT JOIN 查询应用名称和编码) offset = (page - 1) * page_size stmt = ( select( FormMeta, Application.name.label('application_name'), Application.code.label('application_code'), ) .outerjoin(Application, FormMeta.application_id == Application.id) .where(and_(*conditions)) .order_by(FormMeta.sort, FormMeta.sys_create_datetime.desc()) .offset(offset) .limit(page_size) ) result = await db.execute(stmt) items = [] for form, app_name, app_code in result: form.application_name = app_name or "主应用" form.application_code = app_code or "" items.append(form) return { "items": items, "total": total, "page": page, "page_size": page_size } @staticmethod async def get(db: AsyncSession, form_id: str) -> FormMeta: """获取表单详情""" stmt = select(FormMeta).where( FormMeta.id == form_id, FormMeta.is_deleted == False ) result = await db.execute(stmt) form = result.scalar_one_or_none() if not form: raise FormServiceException(f"表单不存在: {form_id}") return form @staticmethod async def get_sub_tables(db: AsyncSession, form_id: str) -> List[FormSubTable]: """获取表单子表配置""" stmt = select(FormSubTable).where( FormSubTable.form_id == form_id, FormSubTable.is_deleted == False ).order_by(FormSubTable.sort) result = await db.execute(stmt) return list(result.scalars().all()) @staticmethod async def get_by_code(db: AsyncSession, code: str) -> FormMeta: """根据编码获取表单""" stmt = select(FormMeta).where( FormMeta.code == code, FormMeta.is_deleted == False ) result = await db.execute(stmt) form = result.scalar_one_or_none() if not form: raise FormServiceException(f"表单不存在: {code}") return form # ============ 创建 ============ @staticmethod async def create( db: AsyncSession, data: Dict[str, Any], user_id: str = None ) -> FormMeta: """创建表单""" code = data.get("code") # 检查编码唯一性 stmt = select(FormMeta).where( FormMeta.code == code, FormMeta.is_deleted == False ) result = await db.execute(stmt) if result.scalar_one_or_none(): raise FormServiceException(f"表单编码已存在: {code}") sub_tables_data = data.pop("sub_tables", []) # 从上下文获取用户信息 from utils.context import get_current_user_info_from_context user_info = get_current_user_info_from_context() creator_id = user_id or (user_info.get('user_id') if user_info else None) dept_id = user_info.get('dept_id') if user_info else None # 创建主表单 form = FormMeta( application_id=data.get("application_id"), name=data.get("name"), code=code, form_type=data.get("form_type", "normal"), description=data.get("description", ""), db_config=data.get("db_config"), main_table=data.get("main_table"), main_table_schema=data.get("main_table_schema", ""), main_table_database=data.get("main_table_database", ""), form_config=data.get("form_config", {}), list_config=data.get("list_config", {}), sort=data.get("sort", 0), show_in_mobile=data.get("show_in_mobile", False), globally_visible=data.get("globally_visible", False), icon=data.get("icon", ""), icon_bg_color=data.get("icon_bg_color", ""), sys_creator_id=creator_id, sys_modifier_id=creator_id, sys_dept_id=dept_id, ) db.add(form) await db.flush() # 创建子表关联 for idx, sub_data in enumerate(sub_tables_data): sub_table = FormSubTable( form_id=form.id, table_name=sub_data.get("table_name"), table_schema=sub_data.get("table_schema", ""), table_database=sub_data.get("table_database", ""), alias=sub_data.get("alias", ""), foreign_key=sub_data.get("foreign_key"), related_field=sub_data.get("related_field", "id"), relation_type=sub_data.get("relation_type", "one-to-many"), sort=sub_data.get("sort", idx), sys_creator_id=user_id, sys_modifier_id=user_id, ) db.add(sub_table) await db.commit() await db.refresh(form) logger.info(f"表单创建成功: {form.code}") return form # ============ 更新 ============ @staticmethod async def update( db: AsyncSession, form_id: str, data: Dict[str, Any], user_id: str = None ) -> FormMeta: """更新表单""" form = await FormService.get(db, form_id) # 更新基本字段 if "name" in data and data["name"] is not None: form.name = data["name"] if "form_type" in data and data["form_type"] is not None: form.form_type = data["form_type"] if "description" in data and data["description"] is not None: form.description = data["description"] if "sort" in data and data["sort"] is not None: form.sort = data["sort"] if "show_in_mobile" in data and data["show_in_mobile"] is not None: form.show_in_mobile = data["show_in_mobile"] if "globally_visible" in data and data["globally_visible"] is not None: form.globally_visible = data["globally_visible"] if "icon" in data and data["icon"] is not None: form.icon = data["icon"] if "icon_bg_color" in data and data["icon_bg_color"] is not None: form.icon_bg_color = data["icon_bg_color"] if "form_config" in data and data["form_config"] is not None: form.form_config = data["form_config"] if "list_config" in data and data["list_config"] is not None: form.list_config = data["list_config"] # 更新数据库配置字段 if "db_config" in data and data["db_config"] is not None: form.db_config = data["db_config"] if "main_table" in data and data["main_table"] is not None: form.main_table = data["main_table"] if "main_table_schema" in data and data["main_table_schema"] is not None: form.main_table_schema = data["main_table_schema"] if "main_table_database" in data and data["main_table_database"] is not None: form.main_table_database = data["main_table_database"] form.sys_modifier_id = user_id # 更新子表关联 if "sub_tables" in data and data["sub_tables"] is not None: # 删除现有子表关联 delete_stmt = update(FormSubTable).where( FormSubTable.form_id == form_id ).values(is_deleted=True) await db.execute(delete_stmt) # 创建新的子表关联 for idx, sub_data in enumerate(data["sub_tables"]): sub_table = FormSubTable( form_id=form_id, table_name=sub_data.get("table_name"), table_schema=sub_data.get("table_schema", ""), table_database=sub_data.get("table_database", ""), alias=sub_data.get("alias", ""), foreign_key=sub_data.get("foreign_key"), related_field=sub_data.get("related_field", "id"), relation_type=sub_data.get("relation_type", "one-to-many"), sort=sub_data.get("sort", idx), sys_creator_id=user_id, sys_modifier_id=user_id, ) db.add(sub_table) await db.commit() await db.refresh(form) logger.info(f"表单更新成功: {form.code}") return form # ============ 删除 ============ @staticmethod async def _cleanup_form_publish_resources(db: AsyncSession, form: FormMeta) -> None: """清理表单发布产生的菜单、API/字段/数据权限及资源注册(取消发布、删除时共用)""" from core.menu.model import Menu from core.permission.model import Permission from core.resource_scope.field_permission.model import ResourceFieldPermissionConfig from core.resource_scope.scope_permission.model import ResourceDataScopeConfig from app.resource_registry import ResourceRegistry resource_type = f"form:{form.code}" # 1. 物理删除对应菜单 delete_menu_stmt = delete(Menu).where( Menu.path == f"/form-render/{form.code}" ) menu_result = await db.execute(delete_menu_stmt) if menu_result.rowcount > 0: logger.info(f"物理删除表单菜单: {form.code}, 删除数量: {menu_result.rowcount}") # 2. 物理删除 API 权限 delete_perm_stmt = delete(Permission).where( Permission.code.like(f"form:{form.code}:%") ) perm_result = await db.execute(delete_perm_stmt) if perm_result.rowcount > 0: logger.info(f"物理删除表单API权限: {form.code}, 删除数量: {perm_result.rowcount}") # 3. 物理删除字段权限配置 delete_field_perm_stmt = delete(ResourceFieldPermissionConfig).where( ResourceFieldPermissionConfig.resource_type == resource_type ) field_perm_result = await db.execute(delete_field_perm_stmt) if field_perm_result.rowcount > 0: logger.info(f"物理删除表单字段权限: {form.code}, 删除数量: {field_perm_result.rowcount}") # 4. 物理删除数据权限配置 delete_scope_stmt = delete(ResourceDataScopeConfig).where( ResourceDataScopeConfig.resource_type == resource_type ) scope_result = await db.execute(delete_scope_stmt) if scope_result.rowcount > 0: logger.info(f"物理删除表单数据权限: {form.code}, 删除数量: {scope_result.rowcount}") # 5. 从资源注册表中移除 if ResourceRegistry.unregister(resource_type): logger.info(f"从资源注册表移除: {resource_type}") @staticmethod async def delete(db: AsyncSession, form_id: str) -> bool: """删除表单(物理删除,并清理菜单与权限配置)""" from core.menu.service import MenuService form = await FormService.get(db, form_id) await FormService._cleanup_form_publish_resources(db, form) delete_sub_stmt = delete(FormSubTable).where( FormSubTable.form_id == form_id ) await db.execute(delete_sub_stmt) await db.delete(form) await db.commit() await MenuService.invalidate_cache() logger.info(f"表单删除成功: {form.code}") return True @staticmethod async def batch_delete(db: AsyncSession, form_ids: List[str]) -> int: """批量删除表单(物理删除,并清理菜单与权限配置)""" from core.menu.service import MenuService stmt = select(FormMeta).where( FormMeta.id.in_(form_ids), FormMeta.is_deleted == False, ) result = await db.execute(stmt) forms = list(result.scalars().all()) for form in forms: await FormService._cleanup_form_publish_resources(db, form) delete_sub_stmt = delete(FormSubTable).where( FormSubTable.form_id.in_(form_ids) ) await db.execute(delete_sub_stmt) delete_stmt = delete(FormMeta).where( FormMeta.id.in_(form_ids), FormMeta.is_deleted == False, ) result = await db.execute(delete_stmt) await db.commit() await MenuService.invalidate_cache() count = result.rowcount logger.info(f"批量删除表单成功: {count} 个") return count # ============ 发布/取消发布 ============ @staticmethod async def publish( db: AsyncSession, form_id: str, publish_config: Dict[str, Any] = None ) -> FormMeta: """发布表单并创建菜单和权限""" from core.menu.model import Menu from core.menu.service import MenuService from core.permission.model import Permission form = await FormService.get(db, form_id) if form.status == "published": raise FormServiceException("表单已发布") sub_tables = await FormService.get_sub_tables(db, form_id) table_validation = await FormService.validate_form_tables( db, { "db_config": form.db_config, "main_table": form.main_table, "main_table_schema": form.main_table_schema or "", "main_table_database": form.main_table_database or "", "sub_tables": [ { "table_name": s.table_name, "table_schema": s.table_schema or "", "table_database": s.table_database or "", "foreign_key": s.foreign_key, } for s in sub_tables ], }, ) if not table_validation.get("valid"): if not table_validation.get("connection_ok"): raise FormServiceException( table_validation.get("connection_message") or "数据库连接不可用,无法发布" ) if not table_validation.get("main_table_exists"): raise FormServiceException( f"主表在目标库中不存在: {form.main_table}" ) for check in table_validation.get("sub_table_checks") or []: if not check.get("exists"): raise FormServiceException( f"子表在目标库中不存在: {check.get('table_name')}" ) # 更新表单状态 form.status = "published" form.version += 1 # 保存发布配置到 list_config if publish_config: list_config = form.list_config or {} list_config["publish_config"] = { "allow_add": publish_config.get("allow_add", True), "allow_edit": publish_config.get("allow_edit", True), "allow_delete": publish_config.get("allow_delete", True), "allow_export": publish_config.get("allow_export", True), "allow_import": publish_config.get("allow_import", False), } form.list_config = list_config # 创建或更新菜单 menu_record = None if publish_config: menu_parent_id = publish_config.get("menu_parent_id") # 检查是否已存在该表单的菜单 menu_stmt = select(Menu).where( Menu.path == f"/form-render/{form.code}" ) menu_result = await db.execute(menu_stmt) existing_menu = menu_result.scalar_one_or_none() if existing_menu: # 更新现有菜单 existing_menu.name = publish_config.get("menu_name", form.name) existing_menu.title = publish_config.get("menu_name", form.name) existing_menu.parent_id = menu_parent_id existing_menu.icon = publish_config.get("menu_icon", "lucide:file-text") existing_menu.order = publish_config.get("menu_order", 0) existing_menu.type = "online_form" existing_menu.application_id = form.application_id menu_record = existing_menu logger.info(f"更新表单菜单: {form.code}") else: # 创建新菜单 new_menu = Menu( application_id=form.application_id, name=publish_config.get("menu_name", form.name), title=publish_config.get("menu_name", form.name), path=f"/form-render/{form.code}", component="online-dev/form-render/index", type="online_form", parent_id=menu_parent_id, icon=publish_config.get("menu_icon", "lucide:file-text"), order=publish_config.get("menu_order", 0), ) db.add(new_menu) await db.flush() menu_record = new_menu logger.info(f"创建表单菜单: {form.code}") # 创建表单操作权限 if menu_record: await FormService._create_form_permissions( db, form, menu_record.id, publish_config ) await db.commit() await db.refresh(form) # 清空菜单缓存 await MenuService.invalidate_cache() logger.info("已清空菜单缓存") logger.info(f"表单发布成功: {form.code}, version={form.version}") return form @staticmethod async def _create_form_permissions( db: AsyncSession, form: FormMeta, menu_id: str, publish_config: Dict[str, Any] = None ): """创建表单操作权限""" from core.permission.model import Permission from app.resource_registry import ResourceRegistry # 注册表单资源类型到资源注册表(用于数据权限和字段权限配置) resource_type = f"form:{form.code}" # 生成字段元数据 field_metadata = FormService._generate_form_field_metadata(form) ResourceRegistry.register( resource_type=resource_type, service_class=None, # 表单没有对应的 Service 类 display_name=form.name, application_id=form.application_id, field_metadata=field_metadata ) logger.info(f"注册表单资源类型: {resource_type}, application_id={form.application_id}, fields={len(field_metadata)}") # 定义标准操作权限 actions = [ ("view", "查看", True, 0, "GET"), ("add", "新增", publish_config.get("allow_add", True) if publish_config else True, 1, "POST"), ("edit", "编辑", publish_config.get("allow_edit", True) if publish_config else True, 2, "PUT"), ("delete", "删除", publish_config.get("allow_delete", True) if publish_config else True, 3, "DELETE"), ("export", "导出", publish_config.get("allow_export", True) if publish_config else True, 1, "POST"), ("import", "导入", publish_config.get("allow_import", False) if publish_config else False, 1, "POST"), ] # HTTP 方法映射 http_method_map = {"GET": 0, "POST": 1, "PUT": 2, "DELETE": 3, "PATCH": 4, "ALL": 5} for action, name, enabled, http_method_int, http_method_str in actions: perm_code = f"form:{form.code}:{action}" # 检查权限是否已存在 existing_stmt = select(Permission).where( Permission.menu_id == menu_id, Permission.code == perm_code, Permission.is_deleted == False ) existing_result = await db.execute(existing_stmt) existing_perm = existing_result.scalar_one_or_none() if existing_perm: # 更新现有权限的启用状态 existing_perm.is_active = enabled existing_perm.name = f"{form.name}-{name}" logger.info(f"更新表单权限: {perm_code}, enabled={enabled}") else: # 创建新权限 perm = Permission( menu_id=menu_id, name=f"{form.name}-{name}", code=perm_code, permission_type=1, # API权限 api_path=f"/api/core/form-data/{form.code}", http_method=http_method_int, is_active=enabled, sort=actions.index((action, name, enabled, http_method_int, http_method_str)) ) db.add(perm) logger.info(f"创建表单权限: {perm_code}, enabled={enabled}") @staticmethod async def unpublish(db: AsyncSession, form_id: str) -> FormMeta: """取消发布表单并删除菜单、权限等相关数据""" from core.menu.service import MenuService form = await FormService.get(db, form_id) if form.status == "draft": raise FormServiceException("表单未发布") form.status = "draft" await FormService._cleanup_form_publish_resources(db, form) await db.commit() await db.refresh(form) await MenuService.invalidate_cache() logger.info("已清空菜单缓存") logger.info(f"表单取消发布完成: {form.code}") return form # ============ 复制 ============ @staticmethod async def copy( db: AsyncSession, form_id: str, new_code: str, new_name: str = None, user_id: str = None ) -> FormMeta: """复制表单""" source = await FormService.get(db, form_id) # 检查新编码唯一性 stmt = select(FormMeta).where( FormMeta.code == new_code, FormMeta.is_deleted == False ) result = await db.execute(stmt) if result.scalar_one_or_none(): raise FormServiceException(f"表单编码已存在: {new_code}") # 创建新表单 new_form = FormMeta( application_id=source.application_id, name=new_name or f"{source.name}_副本", code=new_code, form_type=source.form_type, description=source.description, status="draft", version=1, db_config=source.db_config, main_table=source.main_table, main_table_schema=source.main_table_schema, main_table_database=source.main_table_database, show_in_mobile=source.show_in_mobile, globally_visible=False, icon=source.icon, icon_bg_color=source.icon_bg_color, form_config=source.form_config, list_config=source.list_config, sort=source.sort, sys_creator_id=user_id, sys_modifier_id=user_id, ) db.add(new_form) await db.flush() # 复制子表关联 sub_tables = await FormService.get_sub_tables(db, form_id) for sub in sub_tables: new_sub = FormSubTable( form_id=new_form.id, table_name=sub.table_name, table_schema=sub.table_schema, table_database=sub.table_database, alias=sub.alias, foreign_key=sub.foreign_key, related_field=sub.related_field, relation_type=sub.relation_type, sort=sub.sort, sys_creator_id=user_id, sys_modifier_id=user_id, ) db.add(new_sub) await db.commit() await db.refresh(new_form) logger.info(f"表单复制成功: {source.code} -> {new_code}") return new_form # ============ 导入/导出 ============ @staticmethod async def export_config(db: AsyncSession, form_id: str) -> Dict[str, Any]: """导出表单配置(含数据库表 DDL)""" form = await FormService.get(db, form_id) sub_tables = await FormService.get_sub_tables(db, form_id) sub_tables_data = [] for sub in sub_tables: sub_tables_data.append({ "table_name": sub.table_name, "table_schema": sub.table_schema, "table_database": sub.table_database, "alias": sub.alias, "foreign_key": sub.foreign_key, "related_field": sub.related_field, "relation_type": sub.relation_type, "sort": sub.sort }) table_ddl = {"main_table": "", "sub_tables": {}} export_db_type = "" db_service = None try: from core.database_manager.service import AsyncDatabaseManagerService db_service = await AsyncDatabaseManagerService.create(form.db_config or "default") export_db_type = getattr(db_service, "db_type", "") or "" schema_name = form.main_table_schema or None if form.main_table: table_ddl["main_table"] = await db_service.get_table_ddl( form.main_table, schema_name ) for sub in sub_tables: if sub.table_name: sub_schema = sub.table_schema or schema_name table_ddl["sub_tables"][sub.table_name] = await db_service.get_table_ddl( sub.table_name, sub_schema ) except Exception as e: logger.warning(f"导出表单 DDL 失败(不影响配置导出): {e}") return { "name": form.name, "code": form.code, "form_type": form.form_type, "description": form.description, "show_in_mobile": form.show_in_mobile or False, "globally_visible": form.globally_visible or False, "export_db_type": export_db_type, "db_config": form.db_config, "main_table": form.main_table, "main_table_schema": form.main_table_schema, "main_table_database": form.main_table_database, "form_config": form.form_config, "list_config": form.list_config, "sub_tables": sub_tables_data, "table_ddl": table_ddl } @staticmethod async def _check_table_exists( db_config: str, table_name: str, schema_name: str = None, database: str = None, ) -> bool: """检查数据库表是否存在""" try: from core.database_manager.service import AsyncDatabaseManagerService db_service = await AsyncDatabaseManagerService.create(db_config or "default") tables = await db_service.get_tables( database=database, schema_name=schema_name, ) return any(t.get("table_name") == table_name for t in tables) except Exception as e: logger.warning(f"检查表是否存在失败: {table_name}, {e}") return False @staticmethod async def validate_form_tables(db: AsyncSession, data: Dict[str, Any]) -> Dict[str, Any]: """校验表单主/子表在目标连接上是否存在""" db_config = (data.get("db_config") or "default").strip() or "default" main_table = data.get("main_table") or "" main_schema = data.get("main_table_schema") or "" main_database = data.get("main_table_database") or "" sub_tables = data.get("sub_tables") or [] connection_ok = True connection_message = "" try: from core.database_connection.resolver import ConnectionResolver await ConnectionResolver.resolve(db_config, db) except Exception as e: connection_ok = False connection_message = str(e) main_exists = ( await FormService._check_table_exists( db_config, main_table, main_schema or None, main_database or None ) if main_table else False ) sub_checks = [] for sub in sub_tables: if isinstance(sub, dict): sub_name = sub.get("table_name", "") sub_schema = sub.get("table_schema", "") or main_schema sub_database = sub.get("table_database", "") or main_database else: sub_name = getattr(sub, "table_name", "") sub_schema = getattr(sub, "table_schema", "") or main_schema sub_database = getattr(sub, "table_database", "") or main_database exists = ( await FormService._check_table_exists( db_config, sub_name, sub_schema or None, sub_database or None ) if sub_name else False ) sub_checks.append( { "table_name": sub_name, "schema_name": sub_schema, "exists": exists, "has_ddl": False, } ) database_warnings: List[str] = [] if connection_ok and db_config != "default": try: from core.database_connection.resolver import ConnectionResolver target_info = await ConnectionResolver.resolve(db_config, db) if target_info.db_type == "mysql": conn_db = (target_info.database or "").strip() if main_database and conn_db and main_database.lower() != conn_db.lower(): database_warnings.append( f"主表库名「{main_database}」与连接默认库「{conn_db}」不一致," "请确认 MySQL 账号具备跨库访问权限,或统一连接默认库与表单库名" ) elif main_database and not conn_db: database_warnings.append( f"连接未配置默认库,将依赖 SQL 中的库名「{main_database}」访问表" ) for sub in sub_tables: if isinstance(sub, dict): sub_name = sub.get("table_name", "") sub_db = sub.get("table_database", "") or main_database else: sub_name = getattr(sub, "table_name", "") sub_db = getattr(sub, "table_database", "") or main_database if ( sub_name and sub_db and conn_db and sub_db.lower() != conn_db.lower() ): database_warnings.append( f"子表「{sub_name}」库名「{sub_db}」与连接默认库「{conn_db}」不一致" ) elif target_info.db_type == "postgresql": conn_db = (target_info.database or "").strip() if not conn_db and not main_database: database_warnings.append( "PostgreSQL 连接未配置默认库且表单未填写主表 database," "运行时将使用 postgres 系统库,可能导致找不到业务表" ) elif not conn_db and main_database: database_warnings.append( f"连接未配置默认库,运行时将使用表单主表 database「{main_database}」" ) elif target_info.db_type == "oracle": conn_svc = (target_info.database or "").strip() if main_database and conn_svc and main_database != conn_svc: database_warnings.append( f"表单主表 database「{main_database}」与连接 service「{conn_svc}」不一致," "Oracle 不按库名切换连接,请统一连接 service 与表单配置" ) except Exception as e: logger.warning("校验库名绑定失败: %s", e) all_ok = connection_ok and main_exists and all(c["exists"] for c in sub_checks) return { "valid": all_ok, "connection_ok": connection_ok, "connection_message": connection_message, "db_config": db_config, "main_table_exists": main_exists, "sub_table_checks": sub_checks, "database_warnings": database_warnings, } @staticmethod async def _get_available_schemas(db_config: str) -> List[str]: """获取可用的Schema列表""" try: from core.database_manager.service import AsyncDatabaseManagerService db_service = await AsyncDatabaseManagerService.create(db_config or "default") schemas = await db_service.get_schemas() return [s.get("name", "") for s in schemas if s.get("name")] except Exception as e: logger.warning(f"获取Schema列表失败: {e}") return ["public"] @staticmethod async def _resolve_table_schema( db_config: str, table_name: str, preferred_schema: str = None, ) -> str: """解析表在目标库中实际所在的 schema(导入时 meta 可能与真实库不一致)""" if not table_name: return preferred_schema or "public" schemas_to_try: List[str] = [] if preferred_schema: schemas_to_try.append(preferred_schema) for schema in await FormService._get_available_schemas(db_config): if schema and schema not in schemas_to_try: schemas_to_try.append(schema) if "public" not in schemas_to_try: schemas_to_try.append("public") for schema in schemas_to_try: if await FormService._check_table_exists(db_config, table_name, schema): return schema return preferred_schema or "public" @staticmethod def _generate_create_schema_sql(schema: str, db_type: str) -> str: """生成创建 Schema 的 SQL(PostgreSQL / SQL Server)""" if not schema: return "" if db_type == "postgresql": return f'CREATE SCHEMA IF NOT EXISTS "{schema}";' if db_type == "sqlserver": return f""" IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '{schema}') BEGIN EXEC('CREATE SCHEMA [{schema}]') END; """ return "" @staticmethod async def _ensure_schemas_exist( db_config: str, schemas: List[str], database: str = None, ) -> None: """确保目标 Schema 存在(仅 PostgreSQL / SQL Server)""" unique_schemas = sorted({s.strip() for s in schemas if s and s.strip()}) if not unique_schemas: return from core.database_manager.service import AsyncDatabaseManagerService db_service = await AsyncDatabaseManagerService.create(db_config) db_type = getattr(db_service, "db_type", None) if db_type not in ("postgresql", "sqlserver"): return available = await FormService._get_available_schemas(db_config) for schema in unique_schemas: if schema in available: continue sql = FormService._generate_create_schema_sql(schema, db_type) if not sql: continue result = await db_service.execute_ddl(sql, database=database) if not result.get("success"): raise FormServiceException( f"创建 Schema [{schema}] 失败: {result.get('message')}" ) logger.info(f"导入时自动创建 Schema: {schema}") @staticmethod def _collect_import_target_schemas(data: Dict[str, Any]) -> List[str]: """收集导入配置中涉及的目标 Schema""" schemas: List[str] = [] main_schema = (data.get("main_table_schema") or "").strip() if main_schema: schemas.append(main_schema) for sub in data.get("sub_tables", []) or []: if isinstance(sub, dict): sub_schema = (sub.get("table_schema") or "").strip() else: sub_schema = (getattr(sub, "table_schema", "") or "").strip() if sub_schema: schemas.append(sub_schema) return schemas @staticmethod def _sync_form_config_table_meta(data: Dict[str, Any]) -> None: """将顶层库表绑定同步到 form_config.tableConfigs.meta""" form_config = data.get("form_config") or {} table_configs = form_config.get("tableConfigs") or [] if not table_configs: return db_config = data.get("db_config", "default") main_schema = data.get("main_table_schema", "") main_database = data.get("main_table_database", "") sub_tables = data.get("sub_tables", []) or [] sub_map = {} for sub in sub_tables: if isinstance(sub, dict): sub_map[sub.get("table_name", "")] = sub else: sub_map[getattr(sub, "table_name", "")] = sub for tc in table_configs: if not isinstance(tc, dict): continue meta = tc.setdefault("meta", {}) meta["dbName"] = db_config if tc.get("type") == "main": if main_database: meta["database"] = main_database if main_schema: meta["schema"] = main_schema else: sub = sub_map.get(tc.get("tableName", "")) if sub: if isinstance(sub, dict): if sub.get("table_database"): meta["database"] = sub["table_database"] if sub.get("table_schema"): meta["schema"] = sub["table_schema"] else: if sub.table_database: meta["database"] = sub.table_database if sub.table_schema: meta["schema"] = sub.table_schema @staticmethod async def check_import(db: AsyncSession, data: Dict[str, Any]) -> Dict[str, Any]: """导入预检查:检查编码冲突、表和schema是否存在""" code = data.get("code", "") db_config = data.get("db_config", "default") main_table = data.get("main_table", "") main_table_schema = data.get("main_table_schema", "") or "public" main_table_database = data.get("main_table_database", "") or "" sub_tables = data.get("sub_tables", []) table_ddl = data.get("table_ddl") or {} code_exists = False if code: stmt = select(FormMeta).where(FormMeta.code == code, FormMeta.is_deleted == False) result = await db.execute(stmt) code_exists = result.scalar_one_or_none() is not None main_exists = ( await FormService._check_table_exists( db_config, main_table, main_table_schema, main_table_database or None ) if main_table else False ) main_has_ddl = bool(table_ddl.get("main_table")) sub_table_checks = [] ddl_sub = table_ddl.get("sub_tables", {}) for sub in sub_tables: sub_name = sub.get("table_name", "") if isinstance(sub, dict) else sub.table_name sub_schema = (sub.get("table_schema", "") if isinstance(sub, dict) else sub.table_schema) or main_table_schema sub_database = ( (sub.get("table_database", "") if isinstance(sub, dict) else getattr(sub, "table_database", "")) or main_table_database ) exists = ( await FormService._check_table_exists( db_config, sub_name, sub_schema or None, sub_database or None ) if sub_name else False ) sub_table_checks.append({ "table_name": sub_name, "schema_name": sub_schema, "exists": exists, "has_ddl": bool(ddl_sub.get(sub_name)) }) all_tables_exist = main_exists and all(c["exists"] for c in sub_table_checks) can_import = not code_exists and all_tables_exist available_schemas = await FormService._get_available_schemas(db_config) target_db_type = "" try: from core.database_connection.resolver import ConnectionResolver target_info = await ConnectionResolver.resolve(db_config, db) target_db_type = getattr(target_info, "db_type", "") or "" except Exception as e: logger.warning("导入预检查解析目标库类型失败: %s", e) return { "code_exists": code_exists, "main_table_check": { "table_name": main_table, "schema_name": main_table_schema, "exists": main_exists, "has_ddl": main_has_ddl }, "sub_table_checks": sub_table_checks, "can_import": can_import, "available_schemas": available_schemas, "target_db_type": target_db_type, } @staticmethod def _replace_table_name_in_ddl(ddl: str, old_name: str, new_name: str, new_schema: str = None) -> str: """替换DDL中的表名和schema""" import re escaped = re.escape(old_name) # CREATE TABLE "schema"."table" or CREATE TABLE schema.table pattern_with_schema = rf'(CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?)"?[\w]+"?\."?{escaped}"?' # CREATE TABLE "table" or CREATE TABLE table pattern_without_schema = rf'(CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?)"?{escaped}"?' if new_schema: target = f'\\1"{new_schema}"."{new_name}"' else: target = f'\\1"{new_name}"' result = re.sub(pattern_with_schema, target, ddl, count=1, flags=re.IGNORECASE) if result == ddl: if new_schema: target_no_schema = f'\\1"{new_schema}"."{new_name}"' else: target_no_schema = f'\\1"{new_name}"' result = re.sub(pattern_without_schema, target_no_schema, ddl, count=1, flags=re.IGNORECASE) return result @staticmethod async def import_config( db: AsyncSession, data: Dict[str, Any], user_id: str = None ) -> FormMeta: """导入表单配置(支持自动建表、表名重映射)""" required_fields = ["name", "code", "db_config", "main_table"] for field in required_fields: if not data.get(field): raise FormServiceException(f"缺少必要字段: {field}") auto_create = data.pop("auto_create_tables", False) create_schema = data.pop("create_schema_if_not_exists", False) table_ddl = data.pop("table_ddl", None) or {} rename_mappings = data.pop("table_rename_mappings", []) export_db_type = (data.pop("export_db_type", None) or "").strip().lower() db_config = data.get("db_config", "default") if auto_create and table_ddl and export_db_type: from core.database_connection.resolver import ConnectionResolver def _norm_db_type(t: str) -> str: t = (t or "").lower() if t in ("postgres", "psql"): return "postgresql" if t == "mssql": return "sqlserver" return t target_info = await ConnectionResolver.resolve(db_config, db) src_t = _norm_db_type(export_db_type) tgt_t = _norm_db_type(target_info.db_type) if src_t and tgt_t and src_t != tgt_t: raise FormServiceException( "CROSS_DIALECT_AUTO_CREATE:" f"导出库类型为 {src_t},目标连接类型为 {tgt_t}" ) rename_map = {} for m in rename_mappings: orig = m.get("original_name", "") if isinstance(m, dict) else m.original_name new_n = m.get("new_name", "") if isinstance(m, dict) else m.new_name new_s = m.get("new_schema") if isinstance(m, dict) else m.new_schema if orig and new_n: rename_map[orig] = {"new_name": new_n, "new_schema": new_s} original_main_table = data["main_table"] if original_main_table in rename_map: mapping = rename_map[original_main_table] data["main_table"] = mapping["new_name"] if mapping["new_schema"]: data["main_table_schema"] = mapping["new_schema"] sub_tables = data.get("sub_tables", []) for sub in sub_tables: sub_name = sub.get("table_name", "") if isinstance(sub, dict) else sub.table_name if sub_name in rename_map: mapping = rename_map[sub_name] if isinstance(sub, dict): sub["table_name"] = mapping["new_name"] if mapping["new_schema"]: sub["table_schema"] = mapping["new_schema"] else: sub.table_name = mapping["new_name"] if mapping["new_schema"]: sub.table_schema = mapping["new_schema"] db_config = data.get("db_config", "default") main_database = data.get("main_table_database", "") or None if create_schema: await FormService._ensure_schemas_exist( db_config, FormService._collect_import_target_schemas(data), main_database, ) if auto_create and table_ddl: main_table = data.get("main_table", "") main_schema = data.get("main_table_schema", "") try: from core.database_manager.service import AsyncDatabaseManagerService db_service = await AsyncDatabaseManagerService.create(db_config) main_ddl = table_ddl.get("main_table", "") if main_ddl and main_table: if not await FormService._check_table_exists(db_config, main_table, main_schema): if original_main_table in rename_map: main_ddl = FormService._replace_table_name_in_ddl( main_ddl, original_main_table, main_table, main_schema ) result = await db_service.execute_ddl(main_ddl, schema_name=main_schema or None) if not result.get("success"): raise FormServiceException(f"创建主表失败: {result.get('message')}") logger.info(f"导入时自动创建主表: {main_schema}.{main_table}") sub_ddls = table_ddl.get("sub_tables", {}) for sub in data.get("sub_tables", []): sub_name = sub.get("table_name", "") if isinstance(sub, dict) else sub.table_name sub_schema = sub.get("table_schema", "") if isinstance(sub, dict) else sub.table_schema original_sub_name = None for orig_name, mp in rename_map.items(): if mp["new_name"] == sub_name: original_sub_name = orig_name break ddl_key = original_sub_name or sub_name ddl = sub_ddls.get(ddl_key, "") if ddl and sub_name: if not await FormService._check_table_exists(db_config, sub_name, sub_schema or main_schema): if original_sub_name: ddl = FormService._replace_table_name_in_ddl( ddl, original_sub_name, sub_name, sub_schema or main_schema ) result = await db_service.execute_ddl(ddl, schema_name=sub_schema or main_schema or None) if not result.get("success"): raise FormServiceException(f"创建子表 {sub_name} 失败: {result.get('message')}") logger.info(f"导入时自动创建子表: {sub_schema or main_schema}.{sub_name}") except FormServiceException: raise except Exception as e: raise FormServiceException(f"自动建表失败: {str(e)}") # 导入时校正 schema:导出环境的 schema 可能与目标库不一致 main_table = data.get("main_table", "") if main_table: data["main_table_schema"] = await FormService._resolve_table_schema( db_config, main_table, data.get("main_table_schema") or None, ) for sub in data.get("sub_tables", []): if isinstance(sub, dict): sub_name = sub.get("table_name", "") sub_schema = sub.get("table_schema", "") or data.get("main_table_schema", "") else: sub_name = sub.table_name sub_schema = sub.table_schema or data.get("main_table_schema", "") if sub_name: resolved_sub_schema = await FormService._resolve_table_schema( db_config, sub_name, sub_schema or None ) if isinstance(sub, dict): sub["table_schema"] = resolved_sub_schema else: sub.table_schema = resolved_sub_schema FormService._sync_form_config_table_meta(data) return await FormService.create(db, data, user_id) # ============ 获取表单类型列表 ============ @staticmethod def get_form_types() -> List[Dict[str, str]]: """获取所有表单类型""" return [ {"value": "normal", "label": "普通表单"}, {"value": "workflow", "label": "流程表单"}, ] @staticmethod def _generate_form_field_metadata(form: FormMeta) -> Dict[str, Dict[str, Any]]: """ 根据表单配置生成字段元数据 用于字段权限配置 """ field_metadata = {} # 从表单配置中提取字段信息 form_config = form.form_config or {} items = form_config.get('items', []) def extract_fields(items_list): """递归提取字段信息""" for item in items_list: field_name = item.get('field') item_type = item.get('type', '') # 处理容器类型,递归提取子项 # collapse: 子项在 items[].children 中 if item_type == 'collapse': collapse_items = item.get('items', []) for collapse_item in collapse_items: collapse_children = collapse_item.get('children', []) if collapse_children: extract_fields(collapse_children) continue # grid: 子项在 columns[].children 中 if item_type == 'grid': columns = item.get('columns', []) for column in columns: column_children = column.get('children', []) if column_children: extract_fields(column_children) continue # tabs: 子项在 tabs[].children 中 if item_type == 'tabs': tabs = item.get('tabs', []) for tab in tabs: tab_children = tab.get('children', []) if tab_children: extract_fields(tab_children) continue # 其他容器类型(card, row, col, 展示组件等): 子项在 children 中 if item_type in ['card', 'row', 'col', 'divider', 'alert', 'timeline', 'text', 'html', 'spacer', 'title', 'steps']: children = item.get('children', []) if children: extract_fields(children) continue # sub-table: 子表字段也需要提取 if item_type == 'sub-table': # 子表本身作为一个字段 if field_name and not field_name.startswith('_'): field_metadata[field_name] = { 'label': item.get('label', field_name), 'field_type': 'sub-table', 'required': item.get('props', {}).get('required', False), 'sensitive': False, 'maskable': False, 'default_permission': 'write' } # 子表内的字段也提取 children = item.get('children', []) if children: extract_fields(children) continue # 跳过没有字段名的项 if not field_name: continue # 跳过内部字段 if field_name.startswith('_'): continue field_metadata[field_name] = { 'label': item.get('label', field_name), 'field_type': item_type or 'string', 'required': item.get('props', {}).get('required', False), 'sensitive': False, 'maskable': False, 'default_permission': 'write' } extract_fields(items) return field_metadata @staticmethod async def get_published_forms_simple(db: AsyncSession, application_id: str = None, all_apps: bool = False) -> List[Dict[str, Any]]: """ 获取已发布表单的简单列表(用于下拉选择) Args: application_id: 过滤指定应用,为 None 且 all_apps=False 时只返回无应用的表单 all_apps: 为 True 时返回所有应用的已发布表单(移动端工作台使用) Returns: [{code, name, mainTable, application_id, application_name, fields: [{field, label, type}]}] """ from core.application.model import Application conditions = [ FormMeta.status == "published", FormMeta.is_deleted == False ] if application_id: conditions.append( FormService.build_app_scope_condition( application_id, for_selection=not all_apps, ) ) elif not all_apps: conditions.append( FormService.build_app_scope_condition( None, for_selection=True, ) ) if all_apps: conditions.append(FormMeta.show_in_mobile == True) stmt = select(FormMeta, Application.name.label("application_name")).outerjoin( Application, Application.id == FormMeta.application_id ).where( *conditions ).order_by(FormMeta.name) result = await db.execute(stmt) rows = result.all() simple_list = [] for row in rows: form = row[0] application_name = row[1] # 提取表单字段信息 fields = [] form_config = form.form_config or {} items = form_config.get("items", []) def extract_fields(item_list: List[Dict], in_sub_table: bool = False): """递归提取字段""" for item in item_list: item_type = item.get("type", "") field = item.get("field", "") label = item.get("label", "") # 跳过子表和布局组件 if item_type == "sub-table": continue # 布局组件,递归处理 if item_type in ("grid", "tabs", "collapse", "steps", "button"): if item.get("columns"): for col in item["columns"]: extract_fields(col.get("children", []), in_sub_table) if item.get("items"): for sub_item in item["items"]: extract_fields(sub_item.get("children", []), in_sub_table) continue # 普通字段(排除非数据字段) if field and not in_sub_table and item_type not in ("divider", "alert", "timeline", "text", "html", "spacer", "title", "button"): fields.append({ "field": field, "label": label or field, "type": item_type }) extract_fields(items) # 添加常用的系统字段到字段列表开头 system_fields = [ {"field": "id", "label": "ID", "type": "string"}, {"field": "sys_create_datetime", "label": "创建时间", "type": "datetime"}, {"field": "sys_update_datetime", "label": "更新时间", "type": "datetime"}, {"field": "sys_creator_id", "label": "创建人ID", "type": "string"}, {"field": "sys_modifier_id", "label": "修改人ID", "type": "string"}, {"field": "sys_dept_id", "label": "部门ID", "type": "string"}, {"field": "sort", "label": "排序", "type": "number"}, ] # 将系统字段添加到开头 fields = system_fields + fields simple_list.append({ "code": form.code, "name": form.name, "mainTable": form.main_table, "application_id": form.application_id, "application_name": application_name, "icon": form.icon or "", "icon_bg_color": form.icon_bg_color or "", "form_type": form.form_type or "normal", "fields": fields }) return simple_list @staticmethod async def register_published_forms_to_registry(db: AsyncSession): """ 启动时加载已发布的表单并注册资源类型到 ResourceRegistry 用于数据权限和字段权限配置 """ from app.resource_registry import ResourceRegistry # 查询所有已发布的表单 stmt = select(FormMeta).where( FormMeta.status == "published", FormMeta.is_deleted == False ) result = await db.execute(stmt) published_forms = result.scalars().all() for form in published_forms: resource_type = f"form:{form.code}" # 生成字段元数据 field_metadata = FormService._generate_form_field_metadata(form) ResourceRegistry.register( resource_type=resource_type, service_class=None, display_name=form.name, application_id=form.application_id, field_metadata=field_metadata ) logger.info(f"启动时注册表单资源类型: {resource_type}, application_id={form.application_id}, fields={len(field_metadata)}")