1628 lines
64 KiB
Python
1628 lines
64 KiB
Python
#!/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)}")
|