Files
ai-agent-admin/backend-fastapi/online_dev/form_manager/service.py
T
2026-06-09 21:18:33 +08:00

1628 lines
64 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 的 SQLPostgreSQL / 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)}")