5520 lines
229 KiB
Python
5520 lines
229 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
表单数据操作服务(异步版本)
|
||
支持动态操作不同数据表,适配 PostgreSQL、MySQL
|
||
"""
|
||
import asyncio
|
||
import json
|
||
import logging
|
||
import uuid
|
||
from contextlib import asynccontextmanager
|
||
from datetime import date, datetime
|
||
from decimal import Decimal
|
||
from io import BytesIO
|
||
from typing import Any, Dict, List, Optional, Set, Tuple
|
||
|
||
from openpyxl import Workbook, load_workbook
|
||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||
from sqlalchemy import select, text
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from online_dev.form_manager.model import FormMeta, FormSubTable
|
||
from online_dev.form_data_manager.db_adapter import (
|
||
PLATFORM_SCHEMA,
|
||
FormDataDbAdapter,
|
||
create_form_data_adapter,
|
||
create_platform_adapter,
|
||
create_platform_sql_builder,
|
||
resolve_form_sql_context,
|
||
)
|
||
from online_dev.form_data_manager.dynamic_sql_builder import DynamicSQLBuilder
|
||
from online_dev.form_data_manager.exceptions import (
|
||
FormDataException,
|
||
FormNotFoundException,
|
||
RecordNotFoundException,
|
||
UniqueConstraintError,
|
||
ColumnNotFoundError,
|
||
FormDataValidationError,
|
||
ForeignKeyConstraintError,
|
||
)
|
||
from online_dev.form_data_manager.db_error_handler import (
|
||
translate_db_error,
|
||
format_error_message,
|
||
is_aborted_transaction_error,
|
||
)
|
||
from core.database_manager.service import AsyncDatabaseManagerService
|
||
from app.timezone import APP_TIMEZONE
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def get_max_import_rows() -> int:
|
||
"""根据服务器物理内存计算最大导入/导出行数"""
|
||
import psutil
|
||
total_gb = psutil.virtual_memory().total / (1024 ** 3)
|
||
thresholds = [(2, 100_000), (4, 200_000), (8, 600_000), (16, 1_048_575)]
|
||
if total_gb <= 2:
|
||
return 100_000
|
||
for i in range(len(thresholds) - 1):
|
||
low_gb, low_rows = thresholds[i]
|
||
high_gb, high_rows = thresholds[i + 1]
|
||
if total_gb <= high_gb:
|
||
ratio = (total_gb - low_gb) / (high_gb - low_gb)
|
||
return int(low_rows + ratio * (high_rows - low_rows))
|
||
return 1_048_575
|
||
|
||
|
||
def get_server_memory_gb() -> float:
|
||
"""获取服务器物理内存(GB)"""
|
||
import psutil
|
||
return round(psutil.virtual_memory().total / (1024 ** 3), 1)
|
||
|
||
|
||
MAX_IMPORT_EXPORT_ROWS = get_max_import_rows()
|
||
SERVER_MEMORY_GB = get_server_memory_gb()
|
||
logger.info(f"服务器内存: {SERVER_MEMORY_GB}GB, 导入导出行数上限: {MAX_IMPORT_EXPORT_ROWS}")
|
||
|
||
|
||
class FormDataService:
|
||
"""
|
||
表单数据操作服务(异步版本)
|
||
|
||
根据表单配置动态操作数据表,支持主表和子表的 CRUD 操作
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
form_meta: FormMeta,
|
||
sub_tables: List[FormSubTable],
|
||
db_adapter: FormDataDbAdapter,
|
||
):
|
||
"""
|
||
初始化服务
|
||
|
||
Args:
|
||
form_meta: 表单元数据
|
||
sub_tables: 子表配置列表
|
||
db_adapter: 数据库执行适配器(平台库或第三方连接)
|
||
"""
|
||
self.form_meta = form_meta
|
||
self.sub_tables = sub_tables
|
||
self.db_adapter = db_adapter
|
||
self.db_type = db_adapter.db_type
|
||
self.db_config = db_adapter.db_config
|
||
from online_dev.form_data_manager.db_adapter import create_sql_builder_for_adapter
|
||
|
||
self.sql_builder = create_sql_builder_for_adapter(db_adapter)
|
||
self._platform_adapter: Optional[FormDataDbAdapter] = None
|
||
self._platform_sql_builder = create_platform_sql_builder()
|
||
self._adapter_cache: Dict[str, FormDataDbAdapter] = {}
|
||
self._sql_builder_cache: Dict[str, DynamicSQLBuilder] = {}
|
||
# 缓存标量属性,避免 session 过期后懒加载导致 MissingGreenlet
|
||
self._form_code = form_meta.code
|
||
self._form_config = form_meta.form_config or {}
|
||
self._list_config = form_meta.list_config or {}
|
||
|
||
async def _resolve_schema(self, db: AsyncSession, schema: str) -> Optional[str]:
|
||
"""
|
||
解析 schema 中的变量
|
||
|
||
如果 schema 包含 {{application_code}} 变量,则从表单关联的应用中获取 application_code
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
schema: 原始 schema 配置(可能包含变量)
|
||
|
||
Returns:
|
||
解析后的 schema 值
|
||
"""
|
||
if not schema:
|
||
return None
|
||
|
||
# 检查是否包含变量语法
|
||
if '{{' in schema and '}}' in schema:
|
||
# 提取变量名
|
||
import re
|
||
match = re.search(r'\{\{([^}]+)\}\}', schema)
|
||
if match:
|
||
var_name = match.group(1).strip()
|
||
|
||
# 如果是 application_code 变量,从应用中获取
|
||
if 'application_code' in var_name and self.form_meta.application_id:
|
||
from core.application.model import Application
|
||
result = await db.execute(
|
||
select(Application.code).where(Application.id == self.form_meta.application_id)
|
||
)
|
||
app_code = result.scalar_one_or_none()
|
||
if app_code:
|
||
# 替换变量
|
||
return schema.replace(f'{{{{{var_name}}}}}', app_code)
|
||
|
||
return schema
|
||
|
||
@classmethod
|
||
async def create_service(cls, db: AsyncSession, form_code: str) -> "FormDataService":
|
||
"""
|
||
工厂方法:创建服务实例
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
form_code: 表单编码
|
||
"""
|
||
# 加载表单元数据
|
||
stmt = select(FormMeta).where(
|
||
FormMeta.code == form_code,
|
||
FormMeta.is_deleted == False
|
||
)
|
||
result = await db.execute(stmt)
|
||
form_meta = result.scalar_one_or_none()
|
||
|
||
if not form_meta:
|
||
raise FormNotFoundException(form_code)
|
||
|
||
# 加载子表配置
|
||
sub_stmt = select(FormSubTable).where(
|
||
FormSubTable.form_id == form_meta.id,
|
||
FormSubTable.is_deleted == False
|
||
).order_by(FormSubTable.sort)
|
||
sub_result = await db.execute(sub_stmt)
|
||
sub_tables = list(sub_result.scalars().all())
|
||
|
||
db_config = (form_meta.db_config or "default").strip() or "default"
|
||
db_adapter = await create_form_data_adapter(db_config, db)
|
||
service = cls(form_meta, sub_tables, db_adapter)
|
||
service._platform_adapter = await create_platform_adapter(db)
|
||
service._adapter_cache[db_config] = db_adapter
|
||
service._sql_builder_cache[db_config] = service.sql_builder
|
||
return service
|
||
|
||
async def _get_platform_adapter(self, db: AsyncSession) -> FormDataDbAdapter:
|
||
if self._platform_adapter is None:
|
||
self._platform_adapter = await create_platform_adapter(db)
|
||
return self._platform_adapter
|
||
|
||
async def _execute_platform_query(
|
||
self, db: AsyncSession, sql: str, params: Any = None
|
||
) -> List[Dict[str, Any]]:
|
||
adapter = await self._get_platform_adapter(db)
|
||
return await adapter.execute_query(sql, params)
|
||
|
||
async def _execute_platform_command(
|
||
self, db: AsyncSession, sql: str, params: Any = None
|
||
) -> int:
|
||
adapter = await self._get_platform_adapter(db)
|
||
return await adapter.execute_command(sql, params)
|
||
|
||
def _resolve_exec_database(self, database: Optional[str] = None) -> Optional[str]:
|
||
"""第三方 PostgreSQL 执行/事务时切换连接库(连接未配置 default 时用表单/子表 database)。"""
|
||
if not self.db_adapter.is_external or self.db_adapter.db_type != "postgresql":
|
||
return None
|
||
explicit = (database or "").strip()
|
||
if explicit:
|
||
return explicit
|
||
form_db = (self.form_meta.main_table_database or "").strip()
|
||
if form_db:
|
||
return form_db
|
||
conn_default = (getattr(self.db_adapter, "default_database", "") or "").strip()
|
||
return conn_default or None
|
||
|
||
def _table_exec_database(self, table_database: Optional[str] = None) -> Optional[str]:
|
||
"""主表/子表操作对应的 PG 连接库名(子表 table_database 优先,否则主表)。"""
|
||
effective = (table_database or "").strip() or (
|
||
self.form_meta.main_table_database or ""
|
||
).strip()
|
||
return self._resolve_exec_database(effective or None)
|
||
|
||
@asynccontextmanager
|
||
async def _business_transaction(self, database: Optional[str] = None):
|
||
"""业务库事务:第三方走 handler 事务域,default 与平台 Session 一致。"""
|
||
if self.db_adapter.is_external:
|
||
exec_db = self._resolve_exec_database(database)
|
||
async with self.db_adapter.transaction(database=exec_db):
|
||
yield
|
||
else:
|
||
yield
|
||
|
||
async def _create_savepoint(self, db: AsyncSession, name: str) -> None:
|
||
if self.db_adapter.is_external:
|
||
await self.db_adapter.create_savepoint(name)
|
||
else:
|
||
await db.execute(text(f"SAVEPOINT {name}"))
|
||
|
||
async def _release_savepoint(self, db: AsyncSession, name: str) -> None:
|
||
if self.db_adapter.is_external:
|
||
await self.db_adapter.release_savepoint(name)
|
||
else:
|
||
await db.execute(text(f"RELEASE SAVEPOINT {name}"))
|
||
|
||
async def _rollback_to_savepoint(self, db: AsyncSession, name: str) -> None:
|
||
if self.db_adapter.is_external:
|
||
await self.db_adapter.rollback_to_savepoint(name)
|
||
else:
|
||
await db.execute(text(f"ROLLBACK TO SAVEPOINT {name}"))
|
||
|
||
# ============ 字段白名单 ============
|
||
|
||
def _get_allowed_fields(self, table_type: str = "main", table_name: str = None) -> Set[str]:
|
||
"""
|
||
从表单配置中提取允许的字段(白名单)
|
||
"""
|
||
fields = set()
|
||
form_config = self._form_config
|
||
items = form_config.get("items", [])
|
||
|
||
def traverse(item_list: List[Dict], in_sub_table: str = None):
|
||
"""递归遍历表单项"""
|
||
for item in item_list:
|
||
item_type = item.get("type", "")
|
||
field = item.get("field", "")
|
||
|
||
# 子表单组件
|
||
if item_type == "sub-table":
|
||
sub_table_name = field
|
||
children = item.get("children", [])
|
||
traverse(children, sub_table_name)
|
||
continue
|
||
|
||
# 布局/展示组件,递归处理或跳过
|
||
if item_type in ("grid", "tabs", "collapse", "steps", "divider", "alert", "timeline", "text", "html", "spacer", "title", "button"):
|
||
if item.get("columns"):
|
||
for col in item["columns"]:
|
||
traverse(col.get("children", []), in_sub_table)
|
||
if item.get("items"):
|
||
for sub_item in item["items"]:
|
||
traverse(sub_item.get("children", []), in_sub_table)
|
||
continue
|
||
|
||
# 普通字段(排除虚拟字段,虚拟字段不对应数据库列)
|
||
if field:
|
||
props = item.get("props", {})
|
||
if props.get("isVirtualField"):
|
||
continue
|
||
if table_type == "main" and in_sub_table is None:
|
||
fields.add(field)
|
||
elif table_type == "sub" and in_sub_table == table_name:
|
||
fields.add(field)
|
||
|
||
traverse(items)
|
||
|
||
# 始终允许 id 字段
|
||
fields.add("id")
|
||
|
||
return fields
|
||
|
||
def _filter_fields(self, data: Dict[str, Any], allowed_fields: Set[str]) -> Dict[str, Any]:
|
||
"""过滤字段,只保留白名单中的字段"""
|
||
return {k: v for k, v in data.items() if k in allowed_fields}
|
||
|
||
def _fill_system_fields_for_create(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""
|
||
填充新增时的系统字段
|
||
sys_create_datetime, sys_update_datetime, sys_creator_id, sys_modifier_id, sys_dept_id, sort
|
||
"""
|
||
from utils.context import get_current_user_info_from_context
|
||
|
||
now = datetime.now()
|
||
user_info = get_current_user_info_from_context()
|
||
|
||
user_id = user_info.get('user_id') if user_info else None
|
||
dept_id = user_info.get('dept_id') if user_info else None
|
||
|
||
# 设置创建时间和更新时间
|
||
data['sys_create_datetime'] = now
|
||
data['sys_update_datetime'] = now
|
||
|
||
# 设置创建人和修改人
|
||
if user_id:
|
||
data['sys_creator_id'] = user_id
|
||
data['sys_modifier_id'] = user_id
|
||
|
||
# 设置部门
|
||
if dept_id:
|
||
data['sys_dept_id'] = dept_id
|
||
|
||
# 设置排序字段默认值(如果表中有 sort 字段且未提供值)
|
||
if 'sort' not in data or data.get('sort') is None:
|
||
data['sort'] = 0
|
||
|
||
return data
|
||
|
||
def _fill_system_fields_for_update(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""
|
||
填充更新时的系统字段
|
||
sys_update_datetime, sys_modifier_id
|
||
"""
|
||
from utils.context import get_current_user_info_from_context
|
||
|
||
now = datetime.now()
|
||
user_info = get_current_user_info_from_context()
|
||
|
||
user_id = user_info.get('user_id') if user_info else None
|
||
|
||
# 设置更新时间
|
||
data['sys_update_datetime'] = now
|
||
|
||
# 设置修改人
|
||
if user_id:
|
||
data['sys_modifier_id'] = user_id
|
||
|
||
# 移除不应该被更新的系统字段
|
||
data.pop('sys_create_datetime', None)
|
||
data.pop('sys_creator_id', None)
|
||
data.pop('sys_dept_id', None)
|
||
|
||
return data
|
||
|
||
def _apply_data_scope_filter(
|
||
self,
|
||
filters: Dict[str, Any],
|
||
data_scope: Dict[str, Any]
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
应用数据权限过滤条件
|
||
|
||
Args:
|
||
filters: 现有过滤条件
|
||
data_scope: 数据权限配置,格式:
|
||
{
|
||
'filter_type': 'all' | 'self' | 'dept' | 'dept_and_children' | 'custom',
|
||
'scope': 0-4,
|
||
'user_id': str | None,
|
||
'dept_id': str | None,
|
||
'dept_ids': List[str] | None
|
||
}
|
||
|
||
Returns:
|
||
合并后的过滤条件
|
||
"""
|
||
if not filters:
|
||
filters = {}
|
||
|
||
filter_type = data_scope.get('filter_type', 'all')
|
||
|
||
if filter_type == 'all':
|
||
# 全部数据,不添加过滤条件
|
||
return filters
|
||
elif filter_type == 'self':
|
||
# 仅本人数据:需要表中有 sys_creator_id 字段
|
||
user_id = data_scope.get('user_id')
|
||
if user_id:
|
||
filters['sys_creator_id'] = user_id
|
||
elif filter_type == 'dept':
|
||
# 本部门数据:需要表中有 dept_id 字段
|
||
dept_id = data_scope.get('dept_id')
|
||
if dept_id:
|
||
filters['dept_id'] = dept_id
|
||
elif filter_type == 'dept_and_children':
|
||
# 本部门及下级:需要表中有 dept_id 字段
|
||
dept_ids = data_scope.get('dept_ids', [])
|
||
if dept_ids:
|
||
filters['dept_id'] = {"type": "in", "value": dept_ids}
|
||
elif filter_type == 'custom':
|
||
# 自定义部门
|
||
dept_ids = data_scope.get('dept_ids', [])
|
||
if dept_ids:
|
||
filters['dept_id'] = {"type": "in", "value": dept_ids}
|
||
|
||
return filters
|
||
|
||
async def _apply_data_scope_filter_safe(
|
||
self,
|
||
db: AsyncSession,
|
||
table: str,
|
||
schema: Optional[str],
|
||
database: Optional[str],
|
||
filters: Dict[str, Any],
|
||
data_scope: Dict[str, Any]
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
安全地应用数据权限过滤(检查字段是否存在)
|
||
|
||
如果表中不存在所需的字段,则跳过该过滤条件
|
||
"""
|
||
if not filters:
|
||
filters = {}
|
||
|
||
filter_type = data_scope.get('filter_type', 'all')
|
||
|
||
if filter_type == 'all':
|
||
# 全部数据,不添加过滤条件
|
||
return filters
|
||
|
||
# 获取表的所有列
|
||
columns = await self._get_table_columns(db, table, schema, database)
|
||
|
||
if filter_type == 'self':
|
||
# 仅本人数据:需要表中有 sys_creator_id 字段
|
||
# 注意:字段为空时所有人可见(使用 eq_or_null)
|
||
if 'sys_creator_id' in columns:
|
||
user_id = data_scope.get('user_id')
|
||
if user_id:
|
||
filters['sys_creator_id'] = {"type": "eq_or_null", "value": user_id}
|
||
else:
|
||
logger.warning(f"表 {table} 中不存在 sys_creator_id 字段,跳过数据权限过滤")
|
||
elif filter_type == 'dept':
|
||
# 本部门数据:需要表中有 dept_id 或 sys_dept_id 字段
|
||
# 注意:字段为空时所有人可见(使用 eq_or_null)
|
||
dept_field = 'sys_dept_id' if 'sys_dept_id' in columns else ('dept_id' if 'dept_id' in columns else None)
|
||
if dept_field:
|
||
dept_id = data_scope.get('dept_id')
|
||
if dept_id:
|
||
filters[dept_field] = {"type": "eq_or_null", "value": dept_id}
|
||
else:
|
||
logger.warning(f"表 {table} 中不存在 dept_id 或 sys_dept_id 字段,跳过数据权限过滤")
|
||
elif filter_type == 'dept_and_children':
|
||
# 本部门及下级:需要表中有 dept_id 或 sys_dept_id 字段
|
||
# 注意:字段为空时所有人可见(使用 in_or_null)
|
||
dept_field = 'sys_dept_id' if 'sys_dept_id' in columns else ('dept_id' if 'dept_id' in columns else None)
|
||
if dept_field:
|
||
dept_ids = data_scope.get('dept_ids', [])
|
||
if dept_ids:
|
||
filters[dept_field] = {"type": "in_or_null", "value": dept_ids}
|
||
else:
|
||
# 如果没有部门ID,只返回字段为空的记录
|
||
filters[dept_field] = {"type": "null"}
|
||
else:
|
||
logger.warning(f"表 {table} 中不存在 dept_id 或 sys_dept_id 字段,跳过数据权限过滤")
|
||
elif filter_type == 'custom':
|
||
# 自定义部门
|
||
# 注意:字段为空时所有人可见(使用 in_or_null)
|
||
dept_field = 'sys_dept_id' if 'sys_dept_id' in columns else ('dept_id' if 'dept_id' in columns else None)
|
||
if dept_field:
|
||
dept_ids = data_scope.get('dept_ids', [])
|
||
if dept_ids:
|
||
filters[dept_field] = {"type": "in_or_null", "value": dept_ids}
|
||
else:
|
||
# 如果没有部门ID,只返回字段为空的记录
|
||
filters[dept_field] = {"type": "null"}
|
||
else:
|
||
logger.warning(f"表 {table} 中不存在 dept_id 或 sys_dept_id 字段,跳过数据权限过滤")
|
||
|
||
return filters
|
||
|
||
# ============ 数据库操作 ============
|
||
|
||
async def _try_enrich_foreign_key_error(
|
||
self,
|
||
db: AsyncSession,
|
||
exc: ForeignKeyConstraintError,
|
||
) -> ForeignKeyConstraintError:
|
||
"""在传入会话上解析引用表名;事务已中止时 rollback 后重试。"""
|
||
referenced_table = exc.context.get("referenced_table")
|
||
if not referenced_table:
|
||
return exc
|
||
|
||
display_name: Optional[str] = None
|
||
try:
|
||
display_name = await self._resolve_table_display_name(db, referenced_table)
|
||
except Exception as first_err:
|
||
if not is_aborted_transaction_error(first_err):
|
||
logger.warning(
|
||
"Failed to enrich foreign key error for table %s: %s",
|
||
referenced_table,
|
||
first_err,
|
||
)
|
||
return exc
|
||
try:
|
||
await db.rollback()
|
||
display_name = await self._resolve_table_display_name(
|
||
db, referenced_table
|
||
)
|
||
except Exception as enrich_err:
|
||
logger.warning(
|
||
"Failed to enrich foreign key error for table %s: %s",
|
||
referenced_table,
|
||
enrich_err,
|
||
)
|
||
return exc
|
||
|
||
if not display_name or display_name == referenced_table:
|
||
return exc
|
||
|
||
message = str(exc)
|
||
if "无法删除" in message:
|
||
message = (
|
||
f"无法删除,该数据已被「{display_name}」引用,"
|
||
"请先删除或解除关联数据"
|
||
)
|
||
elif referenced_table in message:
|
||
message = message.replace(referenced_table, display_name)
|
||
else:
|
||
message = f"操作失败,引用的「{display_name}」数据不存在或无效"
|
||
|
||
return ForeignKeyConstraintError(
|
||
message=message,
|
||
detail=exc.context.get("detail", ""),
|
||
referenced_table=referenced_table,
|
||
constraint_name=exc.context.get("constraint_name", ""),
|
||
)
|
||
|
||
async def _resolve_table_display_name(
|
||
self,
|
||
db: AsyncSession,
|
||
table_name: str,
|
||
) -> str:
|
||
stmt = select(FormMeta.name).where(
|
||
FormMeta.main_table == table_name,
|
||
FormMeta.is_deleted == False,
|
||
).limit(1)
|
||
result = await db.execute(stmt)
|
||
form_name = result.scalar_one_or_none()
|
||
if form_name:
|
||
return form_name
|
||
|
||
sub_stmt = (
|
||
select(FormMeta.name)
|
||
.join(FormSubTable, FormSubTable.form_id == FormMeta.id)
|
||
.where(
|
||
FormSubTable.table_name == table_name,
|
||
FormMeta.is_deleted == False,
|
||
)
|
||
.limit(1)
|
||
)
|
||
sub_result = await db.execute(sub_stmt)
|
||
return sub_result.scalar_one_or_none() or table_name
|
||
|
||
async def _execute_query(
|
||
self,
|
||
db: AsyncSession,
|
||
sql: str,
|
||
params: Any = None,
|
||
*,
|
||
database: Optional[str] = None,
|
||
enrich_fk: bool = True,
|
||
) -> List[Dict[str, Any]]:
|
||
"""执行查询,自动将数据库异常转换为业务异常"""
|
||
if params is None:
|
||
params = {}
|
||
try:
|
||
exec_db = self._resolve_exec_database(database)
|
||
return await self.db_adapter.execute_query(
|
||
sql, params, database=exec_db
|
||
)
|
||
except FormDataException:
|
||
raise
|
||
except Exception as e:
|
||
biz_exc = translate_db_error(e)
|
||
if enrich_fk and isinstance(biz_exc, ForeignKeyConstraintError):
|
||
biz_exc = await self._try_enrich_foreign_key_error(db, biz_exc)
|
||
raise biz_exc from e
|
||
|
||
async def _execute_command(
|
||
self,
|
||
db: AsyncSession,
|
||
sql: str,
|
||
params: Any = None,
|
||
*,
|
||
database: Optional[str] = None,
|
||
enrich_fk: bool = True,
|
||
) -> int:
|
||
"""执行命令,返回影响行数,自动将数据库异常转换为业务异常"""
|
||
if params is None:
|
||
params = {}
|
||
try:
|
||
self.db_adapter.ensure_write_allowed()
|
||
exec_db = self._resolve_exec_database(database)
|
||
return await self.db_adapter.execute_command(
|
||
sql, params, database=exec_db
|
||
)
|
||
except FormDataException:
|
||
raise
|
||
except Exception as e:
|
||
biz_exc = translate_db_error(e)
|
||
if enrich_fk and isinstance(biz_exc, ForeignKeyConstraintError):
|
||
biz_exc = await self._try_enrich_foreign_key_error(db, biz_exc)
|
||
raise biz_exc from e
|
||
|
||
async def _get_table_columns(
|
||
self,
|
||
db: AsyncSession,
|
||
table: str,
|
||
schema: Optional[str],
|
||
database: Optional[str],
|
||
*,
|
||
sql_builder: Optional[DynamicSQLBuilder] = None,
|
||
db_adapter: Optional[FormDataDbAdapter] = None,
|
||
) -> List[str]:
|
||
"""获取表的所有列名"""
|
||
sb = sql_builder or self.sql_builder
|
||
adapter = db_adapter or self.db_adapter
|
||
try:
|
||
if sb.db_type == "postgresql":
|
||
schema_name = schema or "public"
|
||
sql = """
|
||
SELECT column_name
|
||
FROM information_schema.columns
|
||
WHERE table_schema = :schema
|
||
AND table_name = :table
|
||
"""
|
||
params = {"schema": schema_name, "table": table}
|
||
elif sb.db_type == "mysql":
|
||
db_name = database or "information_schema"
|
||
sql = """
|
||
SELECT column_name
|
||
FROM information_schema.columns
|
||
WHERE table_schema = :database
|
||
AND table_name = :table
|
||
"""
|
||
params = {"database": db_name, "table": table}
|
||
elif sb.db_type == "sqlserver":
|
||
schema_name = schema or "dbo"
|
||
sql = """
|
||
SELECT column_name
|
||
FROM information_schema.columns
|
||
WHERE table_schema = :schema
|
||
AND table_name = :table
|
||
"""
|
||
params = {"schema": schema_name, "table": table}
|
||
elif sb.db_type == "oracle":
|
||
owner = (schema or "").upper()
|
||
sql = """
|
||
SELECT column_name
|
||
FROM all_tab_columns
|
||
WHERE owner = :schema
|
||
AND table_name = :table
|
||
"""
|
||
params = {"schema": owner, "table": table.upper()}
|
||
else:
|
||
return []
|
||
|
||
exec_db = None
|
||
if adapter.is_external and adapter.db_type == "postgresql":
|
||
exec_db = (database or "").strip() or self._resolve_exec_database()
|
||
result = await adapter.execute_query(sql, params, database=exec_db)
|
||
return [row["column_name"] for row in result]
|
||
except Exception as e:
|
||
logger.error(f"获取表列信息失败: {str(e)}")
|
||
return []
|
||
|
||
async def _check_column_exists(
|
||
self,
|
||
db: AsyncSession,
|
||
table: str,
|
||
schema: Optional[str],
|
||
database: Optional[str],
|
||
column_name: str,
|
||
*,
|
||
sql_builder: Optional[DynamicSQLBuilder] = None,
|
||
db_adapter: Optional[FormDataDbAdapter] = None,
|
||
) -> bool:
|
||
"""检查表中是否存在指定列"""
|
||
columns = await self._get_table_columns(
|
||
db, table, schema, database,
|
||
sql_builder=sql_builder, db_adapter=db_adapter,
|
||
)
|
||
return column_name in columns
|
||
|
||
async def _validate_sort_field(
|
||
self,
|
||
db: AsyncSession,
|
||
table: str,
|
||
schema: Optional[str],
|
||
database: Optional[str],
|
||
sort_field: str
|
||
) -> Optional[str]:
|
||
"""验证排序字段是否存在于表中,如果不存在则返回 None"""
|
||
try:
|
||
# 构建表名
|
||
table_name = self.sql_builder.build_table_name(table, schema, database)
|
||
|
||
# 获取表的列信息
|
||
columns = await self._get_table_columns(db, table, schema, database)
|
||
if not columns:
|
||
# 无法获取列信息,直接返回字段(不验证)
|
||
return sort_field
|
||
|
||
# 检查排序字段是否存在
|
||
if sort_field in columns:
|
||
return sort_field
|
||
|
||
logger.warning(f"排序字段 {sort_field} 不存在于表 {table_name} 中,可用字段: {columns}")
|
||
return None
|
||
|
||
except Exception as e:
|
||
logger.error(f"验证排序字段失败: {str(e)}")
|
||
# 验证失败时返回 None,使用默认排序
|
||
return None
|
||
|
||
# ============ 查询操作 ============
|
||
|
||
async def list(
|
||
self,
|
||
db: AsyncSession,
|
||
page: int = 1,
|
||
page_size: int = 20,
|
||
filters: Dict[str, Any] = None,
|
||
sort_list: List[Dict[str, str]] = None,
|
||
data_scope: Dict[str, Any] = None,
|
||
search: str = None,
|
||
search_fields: List[str] = None
|
||
) -> Dict[str, Any]:
|
||
"""分页查询列表(仅主表,支持数据权限过滤)
|
||
|
||
Args:
|
||
sort_list: 排序列表,格式为 [{"field": "name", "order": "asc"}, ...]
|
||
data_scope: 数据权限过滤配置
|
||
search: 搜索关键词
|
||
search_fields: 搜索字段列表(多字段模糊搜索,OR 关系)
|
||
"""
|
||
import time
|
||
_t0 = time.perf_counter()
|
||
|
||
table = self.form_meta.main_table
|
||
schema = await self._resolve_schema(db, self.form_meta.main_table_schema) or None
|
||
database = self.form_meta.main_table_database or None
|
||
|
||
# 应用数据权限过滤(先检查字段是否存在)
|
||
if data_scope:
|
||
filters = await self._apply_data_scope_filter_safe(db, table, schema, database, filters, data_scope)
|
||
|
||
_t1 = time.perf_counter()
|
||
logger.info(f"[list 耗时] 数据权限过滤: {(_t1 - _t0) * 1000:.1f}ms")
|
||
|
||
# 处理关联字段的名称搜索(转换为 ID 搜索)
|
||
if filters:
|
||
filters = await self._convert_relation_name_to_id(db, filters)
|
||
# 转换日期字符串为 date 对象(PostgreSQL asyncpg 需要)
|
||
filters = self._convert_date_strings(filters)
|
||
|
||
_t2 = time.perf_counter()
|
||
logger.info(f"[list 耗时] 关联字段转换+日期转换: {(_t2 - _t1) * 1000:.1f}ms")
|
||
|
||
# 处理多字段搜索(OR 关系)
|
||
if search and search_fields:
|
||
if not filters:
|
||
filters = {}
|
||
# 使用特殊的 __search__ 键来标记多字段搜索
|
||
filters['__search__'] = {
|
||
'keyword': search,
|
||
'fields': search_fields
|
||
}
|
||
|
||
# 构建 ORDER BY(支持多字段排序)
|
||
order_by = None
|
||
if sort_list:
|
||
order_clauses = []
|
||
for sort_item in sort_list:
|
||
sort_field = sort_item.get('field')
|
||
sort_order = sort_item.get('order', 'desc')
|
||
|
||
if not sort_field:
|
||
continue
|
||
|
||
# 验证排序字段是否存在于表中
|
||
valid_sort_field = await self._validate_sort_field(db, table, schema, database, sort_field)
|
||
if valid_sort_field:
|
||
direction = "DESC" if sort_order.lower() == "desc" else "ASC"
|
||
order_clauses.append(f"{self.sql_builder.quote_identifier(valid_sort_field)} {direction}")
|
||
else:
|
||
logger.warning(f"排序字段 {sort_field} 不存在于表 {table} 中,跳过")
|
||
|
||
if order_clauses:
|
||
order_by = ", ".join(order_clauses)
|
||
|
||
_t3 = time.perf_counter()
|
||
logger.info(f"[list 耗时] 排序字段验证: {(_t3 - _t2) * 1000:.1f}ms")
|
||
|
||
# 查询总数
|
||
count_sql, count_params = self.sql_builder.build_count(
|
||
table, schema, database, filters
|
||
)
|
||
main_exec_db = self._table_exec_database(database)
|
||
count_result = await self._execute_query(
|
||
db, count_sql, count_params, database=main_exec_db
|
||
)
|
||
total = count_result[0]["total"] if count_result else 0
|
||
|
||
_t4 = time.perf_counter()
|
||
logger.info(f"[list 耗时] COUNT 查询 (total={total}): {(_t4 - _t3) * 1000:.1f}ms | SQL: {count_sql}")
|
||
|
||
# 查询数据
|
||
offset = (page - 1) * page_size
|
||
data_sql, data_params = self.sql_builder.build_select(
|
||
table=table,
|
||
schema=schema,
|
||
database=database,
|
||
where=filters,
|
||
order_by=order_by,
|
||
limit=page_size,
|
||
offset=offset
|
||
)
|
||
|
||
rows = await self._execute_query(
|
||
db, data_sql, data_params, database=main_exec_db
|
||
)
|
||
|
||
_t5 = time.perf_counter()
|
||
logger.info(f"[list 耗时] 数据查询 (rows={len(rows)}): {(_t5 - _t4) * 1000:.1f}ms | SQL: {data_sql}")
|
||
|
||
# 处理特殊类型
|
||
items = [self._serialize_row(row) for row in rows]
|
||
|
||
_t6 = time.perf_counter()
|
||
logger.info(f"[list 耗时] 序列化: {(_t6 - _t5) * 1000:.1f}ms")
|
||
|
||
# 填充关联字段的显示名称
|
||
relation_fields = self._get_relation_fields()
|
||
logger.debug(f"识别到的关联字段: {relation_fields}")
|
||
if relation_fields and items:
|
||
items = await self._fill_relation_display_names(db, items, relation_fields)
|
||
|
||
_t7 = time.perf_counter()
|
||
logger.info(f"[list 耗时] 填充关联字段显示名称 (fields={len(relation_fields)}): {(_t7 - _t6) * 1000:.1f}ms")
|
||
|
||
# 填充虚拟字段的值(基于值关联配置)
|
||
items = await self._fill_virtual_fields(db, items, relation_fields, context="list")
|
||
|
||
_t8 = time.perf_counter()
|
||
logger.info(f"[list 耗时] 填充虚拟字段: {(_t8 - _t7) * 1000:.1f}ms")
|
||
|
||
# 应用字段权限过滤
|
||
items = await self.apply_field_permissions(items, db)
|
||
|
||
_t9 = time.perf_counter()
|
||
logger.info(f"[list 耗时] 字段权限过滤: {(_t9 - _t8) * 1000:.1f}ms")
|
||
logger.info(f"[list 耗时] ===== 总耗时: {(_t9 - _t0) * 1000:.1f}ms (table={table}, page={page}, page_size={page_size}, total={total}) =====")
|
||
|
||
return {
|
||
"items": items,
|
||
"total": total,
|
||
"page": page,
|
||
"page_size": page_size
|
||
}
|
||
|
||
async def list_cursor(
|
||
self,
|
||
db: AsyncSession,
|
||
page_size: int = 20,
|
||
cursor: Optional[str] = None,
|
||
cursor_direction: str = "next",
|
||
filters: Dict[str, Any] = None,
|
||
sort_list: List[Dict[str, str]] = None,
|
||
data_scope: Dict[str, Any] = None,
|
||
search: str = None,
|
||
search_fields: List[str] = None
|
||
) -> Dict[str, Any]:
|
||
"""游标分页查询列表(跳过 COUNT,使用 keyset pagination)"""
|
||
import base64
|
||
import time
|
||
_t0 = time.perf_counter()
|
||
|
||
table = self.form_meta.main_table
|
||
schema = await self._resolve_schema(db, self.form_meta.main_table_schema) or None
|
||
database = self.form_meta.main_table_database or None
|
||
|
||
if data_scope:
|
||
filters = await self._apply_data_scope_filter_safe(db, table, schema, database, filters, data_scope)
|
||
|
||
if filters:
|
||
filters = await self._convert_relation_name_to_id(db, filters)
|
||
filters = self._convert_date_strings(filters)
|
||
|
||
if search and search_fields:
|
||
if not filters:
|
||
filters = {}
|
||
filters['__search__'] = {
|
||
'keyword': search,
|
||
'fields': search_fields
|
||
}
|
||
|
||
# 构建排序字段列表 [(field, "ASC"|"DESC"), ...]
|
||
order_fields: List[Tuple[str, str]] = []
|
||
if sort_list:
|
||
for sort_item in sort_list:
|
||
sort_field = sort_item.get('field')
|
||
sort_order = sort_item.get('order', 'desc')
|
||
if not sort_field:
|
||
continue
|
||
valid_sort_field = await self._validate_sort_field(db, table, schema, database, sort_field)
|
||
if valid_sort_field:
|
||
direction = "DESC" if sort_order.lower() == "desc" else "ASC"
|
||
order_fields.append((valid_sort_field, direction))
|
||
|
||
# 确保 id 作为 tie-breaker 保证排序唯一性
|
||
id_already = any(f == "id" for f, _ in order_fields)
|
||
if not id_already:
|
||
last_dir = order_fields[-1][1] if order_fields else "DESC"
|
||
order_fields.append(("id", last_dir))
|
||
|
||
if not order_fields:
|
||
order_fields = [("id", "DESC")]
|
||
|
||
# 解码游标
|
||
cursor_values = None
|
||
if cursor:
|
||
try:
|
||
padding = 4 - len(cursor) % 4
|
||
if padding != 4:
|
||
cursor += "=" * padding
|
||
cursor_json = base64.urlsafe_b64decode(cursor).decode("utf-8")
|
||
cursor_data = json.loads(cursor_json)
|
||
cursor_values = cursor_data.get("v", {})
|
||
# asyncpg 要求 datetime/date 列传入原生对象而非字符串
|
||
if cursor_values:
|
||
for k, v in cursor_values.items():
|
||
if not isinstance(v, str):
|
||
continue
|
||
try:
|
||
parsed = datetime.fromisoformat(v)
|
||
cursor_values[k] = parsed if "T" in v or " " in v else parsed.date()
|
||
except (ValueError, TypeError):
|
||
pass
|
||
except Exception as e:
|
||
logger.warning(f"游标解码失败: {e}, 忽略游标从头查询")
|
||
cursor_values = None
|
||
|
||
_t1 = time.perf_counter()
|
||
|
||
data_sql, data_params = self.sql_builder.build_cursor_select(
|
||
table=table,
|
||
schema=schema,
|
||
database=database,
|
||
where=filters,
|
||
order_fields=order_fields,
|
||
cursor_values=cursor_values,
|
||
direction=cursor_direction,
|
||
limit=page_size
|
||
)
|
||
|
||
rows = await self._execute_query(db, data_sql, data_params)
|
||
|
||
_t2 = time.perf_counter()
|
||
logger.info(f"[list_cursor 耗时] 数据查询 (rows={len(rows)}): {(_t2 - _t1) * 1000:.1f}ms | SQL: {data_sql}")
|
||
|
||
# 判断 has_more:查询了 page_size + 1 条
|
||
has_more = len(rows) > page_size
|
||
if has_more:
|
||
rows = rows[:page_size]
|
||
|
||
# prev 方向查询时结果是反序的,需要翻转回来
|
||
if cursor_direction == "prev":
|
||
rows = list(reversed(rows))
|
||
|
||
items = [self._serialize_row(row) for row in rows]
|
||
|
||
relation_fields = self._get_relation_fields()
|
||
if relation_fields and items:
|
||
items = await self._fill_relation_display_names(db, items, relation_fields)
|
||
|
||
items = await self._fill_virtual_fields(db, items, relation_fields, context="list")
|
||
items = await self.apply_field_permissions(items, db)
|
||
|
||
# 编码游标
|
||
def _encode_cursor(row_data: Dict[str, Any]) -> Optional[str]:
|
||
if not row_data:
|
||
return None
|
||
v = {}
|
||
for field, _ in order_fields:
|
||
val = row_data.get(field)
|
||
if isinstance(val, (datetime, date)):
|
||
val = val.isoformat()
|
||
elif isinstance(val, Decimal):
|
||
val = float(val)
|
||
v[field] = val
|
||
cursor_obj = json.dumps({"v": v}, ensure_ascii=False, default=str)
|
||
return base64.urlsafe_b64encode(cursor_obj.encode("utf-8")).decode("utf-8").rstrip("=")
|
||
|
||
next_cursor = None
|
||
prev_cursor = None
|
||
|
||
if items:
|
||
# 用原始行数据(序列化前)生成游标,确保值精确
|
||
last_raw = rows[-1] if rows else None
|
||
first_raw = rows[0] if rows else None
|
||
|
||
if cursor_direction == "prev":
|
||
last_raw = rows[0] if rows else None
|
||
first_raw = rows[-1] if rows else None
|
||
|
||
if has_more:
|
||
if cursor_direction == "next":
|
||
next_cursor = _encode_cursor(dict(last_raw) if last_raw else None)
|
||
else:
|
||
prev_cursor = _encode_cursor(dict(first_raw) if first_raw else None)
|
||
|
||
# 始终提供反方向游标(如果有游标传入,说明可以往回翻)
|
||
if cursor:
|
||
if cursor_direction == "next":
|
||
prev_cursor = _encode_cursor(dict(rows[0]) if rows else None)
|
||
if has_more:
|
||
next_cursor = _encode_cursor(dict(rows[-1]) if rows else None)
|
||
else:
|
||
next_cursor = None
|
||
else:
|
||
next_cursor = _encode_cursor(dict(rows[-1]) if rows else None)
|
||
if has_more:
|
||
prev_cursor = _encode_cursor(dict(rows[0]) if rows else None)
|
||
else:
|
||
prev_cursor = None
|
||
else:
|
||
# 第一页:没有 prev_cursor
|
||
prev_cursor = None
|
||
if has_more:
|
||
next_cursor = _encode_cursor(dict(rows[-1]) if rows else None)
|
||
|
||
_t3 = time.perf_counter()
|
||
logger.info(f"[list_cursor 耗时] ===== 总耗时: {(_t3 - _t0) * 1000:.1f}ms (table={table}, page_size={page_size}, rows={len(items)}) =====")
|
||
|
||
return {
|
||
"items": items,
|
||
"has_more": has_more if cursor_direction == "next" else (cursor is not None),
|
||
"next_cursor": next_cursor,
|
||
"prev_cursor": prev_cursor,
|
||
"page_size": page_size
|
||
}
|
||
|
||
async def get_tree_children(
|
||
self,
|
||
db: AsyncSession,
|
||
parent_id: str = None,
|
||
parent_field: str = "parent_id",
|
||
data_scope: Dict[str, Any] = None
|
||
) -> List[Dict[str, Any]]:
|
||
"""获取树形数据的子节点(用于懒加载模式)
|
||
|
||
Args:
|
||
parent_id: 父节点ID,为空时获取根节点
|
||
parent_field: 父节点字段名,默认为 parent_id
|
||
data_scope: 数据权限过滤配置
|
||
|
||
Returns:
|
||
子节点列表,每个节点包含 has_children 标记
|
||
"""
|
||
table = self.form_meta.main_table
|
||
schema = await self._resolve_schema(db, self.form_meta.main_table_schema) or None
|
||
database = self.form_meta.main_table_database or None
|
||
|
||
# 验证 parent_field 是否存在
|
||
valid_parent_field = await self._validate_sort_field(db, table, schema, database, parent_field)
|
||
if not valid_parent_field:
|
||
raise ColumnNotFoundError(column=parent_field, table=table)
|
||
|
||
# 构建过滤条件
|
||
filters = {}
|
||
if parent_id:
|
||
# 获取指定父节点的子节点
|
||
filters[parent_field] = {"type": "eq", "value": parent_id}
|
||
else:
|
||
# 获取根节点(parent_field 为空或 NULL)
|
||
filters[parent_field] = {"type": "null"}
|
||
|
||
# 应用数据权限过滤
|
||
if data_scope:
|
||
filters = await self._apply_data_scope_filter_safe(db, table, schema, database, filters, data_scope)
|
||
|
||
# 查询数据(按 sort 字段排序)
|
||
order_by = f"{self.sql_builder.quote_identifier('sort')} ASC"
|
||
|
||
data_sql, data_params = self.sql_builder.build_select(
|
||
table=table,
|
||
schema=schema,
|
||
database=database,
|
||
where=filters,
|
||
order_by=order_by
|
||
)
|
||
|
||
rows = await self._execute_query(db, data_sql, data_params)
|
||
items = [self._serialize_row(row) for row in rows]
|
||
|
||
# 填充关联字段的显示名称
|
||
relation_fields = self._get_relation_fields()
|
||
if relation_fields and items:
|
||
items = await self._fill_relation_display_names(db, items, relation_fields)
|
||
|
||
# 为每个节点检查是否有子节点
|
||
if items:
|
||
table_name = self.sql_builder.build_table_name(table, schema, database)
|
||
quoted_parent_field = self.sql_builder.quote_identifier(parent_field)
|
||
|
||
# 检查表是否有 is_deleted 字段
|
||
has_is_deleted = await self._check_column_exists(db, table, schema, database, "is_deleted")
|
||
|
||
# 批量查询所有节点的子节点数量
|
||
ids = [item.get('id') for item in items if item.get('id')]
|
||
if ids:
|
||
# 构建 IN 查询
|
||
placeholders = ", ".join([f":id_{i}" for i in range(len(ids))])
|
||
where_clause = f"{quoted_parent_field} IN ({placeholders})"
|
||
if has_is_deleted:
|
||
where_clause += self.sql_builder.is_deleted_and_clause()
|
||
|
||
count_sql = f"""
|
||
SELECT {quoted_parent_field} as parent_id, COUNT(*) as child_count
|
||
FROM {table_name}
|
||
WHERE {where_clause}
|
||
GROUP BY {quoted_parent_field}
|
||
"""
|
||
count_params = {f"id_{i}": id_val for i, id_val in enumerate(ids)}
|
||
|
||
try:
|
||
count_result = await self._execute_query(db, count_sql, count_params)
|
||
# 构建父ID到子节点数量的映射
|
||
child_count_map = {row['parent_id']: row['child_count'] for row in count_result}
|
||
|
||
# 为每个节点设置 has_children 标记
|
||
for item in items:
|
||
item_id = item.get('id')
|
||
item['has_children'] = child_count_map.get(item_id, 0) > 0
|
||
except Exception as e:
|
||
logger.warning(f"查询子节点数量失败: {e}")
|
||
# 如果查询失败,默认设置为 False
|
||
for item in items:
|
||
item['has_children'] = False
|
||
|
||
return items
|
||
|
||
async def get_field_values(
|
||
self,
|
||
db: AsyncSession,
|
||
field_name: str,
|
||
page: int = 1,
|
||
page_size: int = 20,
|
||
search: str = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
获取指定字段的唯一值列表(用于过滤选项)
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
field_name: 字段名
|
||
page: 页码
|
||
page_size: 每页数量
|
||
search: 搜索关键词(模糊匹配)
|
||
|
||
Returns:
|
||
{
|
||
"items": [{"value": "值", "label": "显示文本", "count": 数量}],
|
||
"total": 总数,
|
||
"hasMore": 是否有更多
|
||
}
|
||
"""
|
||
table = self.form_meta.main_table
|
||
schema = await self._resolve_schema(db, self.form_meta.main_table_schema) or None
|
||
database = self.form_meta.main_table_database or None
|
||
|
||
# 验证字段是否存在
|
||
valid_field = await self._validate_sort_field(db, table, schema, database, field_name)
|
||
if not valid_field:
|
||
raise ColumnNotFoundError(column=field_name, table=table)
|
||
|
||
# 构建表名
|
||
table_name = self.sql_builder.build_table_name(table, schema, database)
|
||
quoted_field = self.sql_builder.quote_identifier(field_name)
|
||
|
||
# 构建 WHERE 条件(只检查 NOT NULL,不检查空字符串,因为非字符串类型会报错)
|
||
where_clause = f"{quoted_field} IS NOT NULL"
|
||
params = {}
|
||
|
||
if search:
|
||
if self.sql_builder.db_type == "postgresql":
|
||
where_clause += f" AND CAST({quoted_field} AS TEXT) ILIKE :search"
|
||
else:
|
||
where_clause += (
|
||
f" AND LOWER(CAST({quoted_field} AS VARCHAR(4000))) "
|
||
f"LIKE LOWER(:search)"
|
||
)
|
||
params["search"] = f"%{search}%"
|
||
|
||
# 检查表是否有 is_deleted 字段
|
||
has_is_deleted = await self._check_column_exists(db, table, schema, database, "is_deleted")
|
||
if has_is_deleted:
|
||
where_clause += self.sql_builder.is_deleted_and_clause()
|
||
|
||
# 查询总数
|
||
count_sql = f"""
|
||
SELECT COUNT(DISTINCT {quoted_field}) as total
|
||
FROM {table_name}
|
||
WHERE {where_clause}
|
||
"""
|
||
count_result = await self._execute_query(db, count_sql, params)
|
||
total = count_result[0]["total"] if count_result else 0
|
||
|
||
# 查询唯一值(带分页)
|
||
offset = (page - 1) * page_size
|
||
data_sql = f"""
|
||
SELECT {quoted_field} as value, COUNT(*) as count
|
||
FROM {table_name}
|
||
WHERE {where_clause}
|
||
GROUP BY {quoted_field}
|
||
ORDER BY count DESC, {quoted_field} ASC
|
||
"""
|
||
data_sql = self.sql_builder._append_limit_offset(data_sql, page_size, offset)
|
||
|
||
rows = await self._execute_query(db, data_sql, params)
|
||
|
||
# 处理结果,获取显示标签
|
||
items = []
|
||
field_options = self._get_field_options(field_name)
|
||
|
||
for row in rows:
|
||
value = row["value"]
|
||
# 序列化特殊类型
|
||
if isinstance(value, (datetime, date)):
|
||
value = value.isoformat()
|
||
elif isinstance(value, Decimal):
|
||
value = float(value)
|
||
elif isinstance(value, uuid.UUID):
|
||
value = str(value)
|
||
|
||
# 获取显示标签
|
||
label = str(value)
|
||
if field_options:
|
||
for opt in field_options:
|
||
if str(opt.get("value")) == str(value):
|
||
label = opt.get("label", str(value))
|
||
break
|
||
|
||
items.append({
|
||
"value": value,
|
||
"label": label,
|
||
"count": row["count"]
|
||
})
|
||
|
||
return {
|
||
"items": items,
|
||
"total": total,
|
||
"hasMore": (page * page_size) < total
|
||
}
|
||
|
||
def _get_unique_check_fields(self) -> List[str]:
|
||
"""
|
||
从表单配置中提取启用了唯一性校验的主表字段名列表
|
||
|
||
支持两种配置方式:
|
||
1. 表单设计器中的 props.uniqueCheck(字段级别)
|
||
2. 数据库设计中的 tableConfigs[].fields[].uniqueCheck(表级别)
|
||
"""
|
||
unique_fields = set()
|
||
form_config = self._form_config
|
||
|
||
# 方式1:从表单设计器的 items 中获取
|
||
items = form_config.get("items", [])
|
||
|
||
def traverse(item_list: List[Dict], in_sub_table: str = None):
|
||
for item in item_list:
|
||
item_type = item.get("type", "")
|
||
field = item.get("field", "")
|
||
|
||
if item_type == "sub-table":
|
||
traverse(item.get("children", []), field)
|
||
continue
|
||
|
||
if item_type in ("grid", "tabs", "collapse", "steps", "divider", "alert", "timeline", "text", "html", "spacer", "title", "button"):
|
||
if item.get("columns"):
|
||
for col in item["columns"]:
|
||
traverse(col.get("children", []), in_sub_table)
|
||
if item.get("items"):
|
||
for sub_item in item["items"]:
|
||
traverse(sub_item.get("children", []), in_sub_table)
|
||
continue
|
||
|
||
if field and in_sub_table is None:
|
||
props = item.get("props", {})
|
||
if props.get("uniqueCheck"):
|
||
unique_fields.add(field)
|
||
|
||
traverse(items)
|
||
|
||
# 方式2:从数据库设计的 tableConfigs 中获取(主表字段)
|
||
table_configs = form_config.get("tableConfigs", [])
|
||
for table_config in table_configs:
|
||
if table_config.get("type") == "main":
|
||
for field in table_config.get("fields", []):
|
||
if field.get("uniqueCheck"):
|
||
unique_fields.add(field.get("name"))
|
||
|
||
return list(unique_fields)
|
||
|
||
async def _validate_unique_fields(
|
||
self,
|
||
db: AsyncSession,
|
||
data: Dict[str, Any],
|
||
exclude_id: str = None
|
||
) -> None:
|
||
"""
|
||
批量校验数据中启用了唯一性校验的字段,不通过则抛出 FormDataException
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
data: 待写入的主表数据
|
||
exclude_id: 编辑时排除的记录ID
|
||
"""
|
||
unique_fields = self._get_unique_check_fields()
|
||
if not unique_fields:
|
||
return
|
||
|
||
duplicate_fields = []
|
||
for field_name in unique_fields:
|
||
value = data.get(field_name)
|
||
if value is None or value == '':
|
||
continue
|
||
is_unique = await self.check_unique(db, field_name, str(value), exclude_id)
|
||
if not is_unique:
|
||
duplicate_fields.append(field_name)
|
||
|
||
if duplicate_fields:
|
||
field_labels = self._get_field_labels(duplicate_fields)
|
||
messages = [f"'{field_labels.get(f, f)}'" for f in duplicate_fields]
|
||
raise UniqueConstraintError(
|
||
f"以下字段的值已存在: {', '.join(messages)}",
|
||
fields=duplicate_fields,
|
||
)
|
||
|
||
def _get_field_labels(self, field_names: List[str]) -> Dict[str, str]:
|
||
"""获取字段名到标签的映射"""
|
||
labels = {}
|
||
form_config = self._form_config
|
||
items = form_config.get("items", [])
|
||
|
||
def traverse(item_list: List[Dict]):
|
||
for item in item_list:
|
||
field = item.get("field", "")
|
||
label = item.get("label", "")
|
||
if field and label and field in field_names:
|
||
labels[field] = label
|
||
|
||
if item.get("columns"):
|
||
for col in item["columns"]:
|
||
traverse(col.get("children", []))
|
||
if item.get("items"):
|
||
for sub_item in item["items"]:
|
||
traverse(sub_item.get("children", []))
|
||
if item.get("children"):
|
||
traverse(item["children"])
|
||
|
||
traverse(items)
|
||
return labels
|
||
|
||
async def check_unique(
|
||
self,
|
||
db: AsyncSession,
|
||
field_name: str,
|
||
value: str,
|
||
exclude_id: str = None
|
||
) -> bool:
|
||
"""
|
||
检查字段值在主表中是否唯一
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
field_name: 字段名
|
||
value: 字段值
|
||
exclude_id: 排除的记录ID(编辑时排除自身)
|
||
|
||
Returns:
|
||
True 表示唯一(可用),False 表示已存在
|
||
"""
|
||
table = self.form_meta.main_table
|
||
schema = await self._resolve_schema(db, self.form_meta.main_table_schema) or None
|
||
database = self.form_meta.main_table_database or None
|
||
|
||
# 验证字段是否存在
|
||
valid_field = await self._validate_sort_field(db, table, schema, database, field_name)
|
||
if not valid_field:
|
||
raise ColumnNotFoundError(column=field_name, table=table)
|
||
|
||
# 构建查询
|
||
table_name = self.sql_builder.build_table_name(table, schema, database)
|
||
quoted_field = self.sql_builder.quote_identifier(field_name)
|
||
|
||
where_clause = f"{quoted_field} = :value"
|
||
params: Dict[str, Any] = {"value": value}
|
||
|
||
# 编辑时排除自身
|
||
if exclude_id:
|
||
quoted_id = self.sql_builder.quote_identifier("id")
|
||
where_clause += f" AND {quoted_id} != :exclude_id"
|
||
params["exclude_id"] = exclude_id
|
||
|
||
# 检查 is_deleted 字段
|
||
has_is_deleted = await self._check_column_exists(db, table, schema, database, "is_deleted")
|
||
if has_is_deleted:
|
||
where_clause += self.sql_builder.is_deleted_and_clause()
|
||
|
||
sql = f"""
|
||
SELECT COUNT(*) as cnt
|
||
FROM {table_name}
|
||
WHERE {where_clause}
|
||
"""
|
||
result = await self._execute_query(db, sql, params)
|
||
count = result[0]["cnt"] if result else 0
|
||
return count == 0
|
||
|
||
def _get_field_options(self, field_name: str) -> Optional[List[Dict]]:
|
||
"""从表单配置中获取字段的选项列表"""
|
||
form_config = self._form_config
|
||
items = form_config.get("items", [])
|
||
|
||
def find_field(item_list: List[Dict]) -> Optional[List[Dict]]:
|
||
for item in item_list:
|
||
if item.get("field") == field_name:
|
||
return item.get("options")
|
||
# 递归查找子项
|
||
for key in ["children", "columns", "items"]:
|
||
if key in item:
|
||
sub_items = item[key]
|
||
if isinstance(sub_items, list):
|
||
if key == "columns":
|
||
for col in sub_items:
|
||
if "children" in col:
|
||
result = find_field(col["children"])
|
||
if result is not None:
|
||
return result
|
||
else:
|
||
result = find_field(sub_items)
|
||
if result is not None:
|
||
return result
|
||
return None
|
||
|
||
return find_field(items)
|
||
|
||
async def get(self, db: AsyncSession, pk: Any) -> Dict[str, Any]:
|
||
"""获取单条数据(含子表)"""
|
||
table = self.form_meta.main_table
|
||
schema = await self._resolve_schema(db, self.form_meta.main_table_schema) or None
|
||
database = self.form_meta.main_table_database or None
|
||
|
||
# 查询主表
|
||
sql, params = self.sql_builder.build_select(
|
||
table=table,
|
||
schema=schema,
|
||
database=database,
|
||
where={"id": pk}
|
||
)
|
||
rows = await self._execute_query(db, sql, params)
|
||
|
||
if not rows:
|
||
raise RecordNotFoundException(pk, table=table)
|
||
|
||
result = self._serialize_row(rows[0])
|
||
|
||
# 查询子表数据
|
||
result["sub_tables"] = {}
|
||
for sub_table in self.sub_tables:
|
||
sub_data = await self._query_sub_table_data(db, sub_table, pk)
|
||
result["sub_tables"][sub_table.table_name] = sub_data
|
||
|
||
# 填充关联字段的显示名称和虚拟字段
|
||
relation_fields = self._get_relation_fields()
|
||
all_relation_fields = self._get_all_selector_relation_fields(relation_fields)
|
||
if all_relation_fields:
|
||
await self._fill_relation_display_names(db, [result], all_relation_fields)
|
||
filled = await self._fill_virtual_fields(db, [result], relation_fields, context="form")
|
||
if filled:
|
||
result = filled[0]
|
||
|
||
# 应用字段权限过滤
|
||
result = await self.apply_field_permissions(result, db)
|
||
|
||
return result
|
||
|
||
async def _query_sub_table_data(
|
||
self,
|
||
db: AsyncSession,
|
||
sub_table: FormSubTable,
|
||
main_pk: Any
|
||
) -> List[Dict[str, Any]]:
|
||
"""查询子表数据"""
|
||
sql, params = self.sql_builder.build_select(
|
||
table=sub_table.table_name,
|
||
schema=sub_table.table_schema or None,
|
||
database=sub_table.table_database or None,
|
||
where={sub_table.foreign_key: main_pk}
|
||
)
|
||
sub_db = sub_table.table_database or None
|
||
rows = await self._execute_query(
|
||
db, sql, params, database=self._table_exec_database(sub_db)
|
||
)
|
||
return [self._serialize_row(row) for row in rows]
|
||
|
||
def _is_uuid_like(self, value: str) -> bool:
|
||
"""检查值是否像 UUID(用于判断是 ID 还是名称)"""
|
||
if not isinstance(value, str):
|
||
return False
|
||
# UUID 格式: 8-4-4-4-12 或 32位无连字符
|
||
import re
|
||
uuid_pattern = re.compile(
|
||
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$|^[0-9a-f]{32}$',
|
||
re.IGNORECASE
|
||
)
|
||
return bool(uuid_pattern.match(value))
|
||
|
||
def _is_date_string(self, value: str) -> bool:
|
||
"""检查值是否为日期字符串格式 YYYY-MM-DD"""
|
||
if not isinstance(value, str):
|
||
return False
|
||
import re
|
||
return bool(re.match(r'^\d{4}-\d{2}-\d{2}$', value))
|
||
|
||
def _is_datetime_string(self, value: str) -> bool:
|
||
"""检查值是否为日期时间字符串格式 YYYY-MM-DD HH:MM:SS 或 ISO 格式"""
|
||
if not isinstance(value, str):
|
||
return False
|
||
import re
|
||
# 支持多种格式:YYYY-MM-DD HH:MM:SS, YYYY-MM-DDTHH:MM:SS, 带毫秒和时区
|
||
return bool(re.match(r'^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}', value))
|
||
|
||
def _parse_datetime_string(self, value: str) -> Optional[datetime]:
|
||
"""解析日期时间字符串为 datetime 对象"""
|
||
if not isinstance(value, str):
|
||
return None
|
||
|
||
# 尝试多种格式
|
||
formats = [
|
||
"%Y-%m-%d %H:%M:%S",
|
||
"%Y-%m-%dT%H:%M:%S",
|
||
"%Y-%m-%dT%H:%M:%S.%f",
|
||
"%Y-%m-%d",
|
||
]
|
||
|
||
# 处理带时区的格式
|
||
clean_value = value
|
||
if value.endswith('Z'):
|
||
clean_value = value[:-1]
|
||
elif '+' in value[-6:] or (value.count('-') > 2 and ':' in value[-6:]):
|
||
# 移除时区信息
|
||
clean_value = value.rsplit('+', 1)[0].rsplit('-', 1)[0] if '+' in value[-6:] else value
|
||
|
||
for fmt in formats:
|
||
try:
|
||
return datetime.strptime(clean_value, fmt)
|
||
except ValueError:
|
||
continue
|
||
|
||
return None
|
||
|
||
@staticmethod
|
||
def _convert_bool_string(value: str):
|
||
"""将字符串 'true'/'false' 转换为 Python bool,非布尔字符串返回 None"""
|
||
if value.lower() == 'true':
|
||
return True
|
||
if value.lower() == 'false':
|
||
return False
|
||
return None
|
||
|
||
def _convert_date_strings(self, filters: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""将过滤器中的日期/日期时间/布尔字符串转换为原生类型(PostgreSQL asyncpg 需要)"""
|
||
if not filters:
|
||
return filters
|
||
|
||
converted = {}
|
||
for field, value in filters.items():
|
||
if isinstance(value, dict):
|
||
filter_type = value.get("type")
|
||
filter_value = value.get("value")
|
||
|
||
# 处理布尔字符串
|
||
if filter_type in ("eq",) and isinstance(filter_value, str):
|
||
bool_val = self._convert_bool_string(filter_value)
|
||
if bool_val is not None:
|
||
converted[field] = {"type": filter_type, "value": bool_val}
|
||
continue
|
||
|
||
# 处理 gte, lte, gt, lt, eq 等类型的日期/日期时间值
|
||
# 注意:如果 case_sensitive 明确设为 false,说明是文本比较,不转换日期
|
||
case_sensitive = value.get("case_sensitive", True)
|
||
if filter_type in ("gte", "lte", "gt", "lt", "eq") and isinstance(filter_value, str) and case_sensitive:
|
||
if self._is_datetime_string(filter_value):
|
||
# 优先尝试解析为 datetime
|
||
parsed = self._parse_datetime_string(filter_value)
|
||
if parsed:
|
||
converted[field] = {"type": filter_type, "value": parsed}
|
||
else:
|
||
converted[field] = value
|
||
elif self._is_date_string(filter_value):
|
||
# 对于 datetime 字段使用日期格式查询时,需要特殊处理
|
||
# lte/lt: 使用当天的 23:59:59.999999(包含当天所有时间)
|
||
# gte/gt: 使用当天的 00:00:00
|
||
try:
|
||
parsed_date = date.fromisoformat(filter_value)
|
||
if filter_type == "lte":
|
||
# 小于等于:转换为当天结束时间
|
||
end_of_day = datetime.combine(parsed_date, datetime.max.time())
|
||
converted[field] = {"type": filter_type, "value": end_of_day}
|
||
elif filter_type == "lt":
|
||
# 小于:转换为当天开始时间(不包含当天)
|
||
start_of_day = datetime.combine(parsed_date, datetime.min.time())
|
||
converted[field] = {"type": filter_type, "value": start_of_day}
|
||
elif filter_type == "gte":
|
||
# 大于等于:转换为当天开始时间
|
||
start_of_day = datetime.combine(parsed_date, datetime.min.time())
|
||
converted[field] = {"type": filter_type, "value": start_of_day}
|
||
elif filter_type == "gt":
|
||
# 大于:转换为当天结束时间(不包含当天)
|
||
end_of_day = datetime.combine(parsed_date, datetime.max.time())
|
||
converted[field] = {"type": filter_type, "value": end_of_day}
|
||
elif filter_type == "eq":
|
||
# 精确匹配日期:转换为当天范围查询(00:00:00 - 23:59:59)
|
||
# 这样 2026-01-14 可以匹配 2026-01-14 16:16:09
|
||
start_of_day = datetime.combine(parsed_date, datetime.min.time())
|
||
end_of_day = datetime.combine(parsed_date, datetime.max.time())
|
||
converted[field] = {"type": "range", "value": [start_of_day, end_of_day]}
|
||
else:
|
||
converted[field] = {"type": filter_type, "value": parsed_date}
|
||
except ValueError:
|
||
converted[field] = value
|
||
else:
|
||
converted[field] = value
|
||
# 处理 in 类型的日期值列表
|
||
elif filter_type == "in" and isinstance(filter_value, list):
|
||
converted_values = []
|
||
for v in filter_value:
|
||
if isinstance(v, str):
|
||
if self._is_datetime_string(v):
|
||
parsed = self._parse_datetime_string(v)
|
||
converted_values.append(parsed if parsed else v)
|
||
elif self._is_date_string(v):
|
||
try:
|
||
converted_values.append(date.fromisoformat(v))
|
||
except ValueError:
|
||
converted_values.append(v)
|
||
else:
|
||
converted_values.append(v)
|
||
else:
|
||
converted_values.append(v)
|
||
converted[field] = {"type": filter_type, "value": converted_values}
|
||
# 处理 range 类型
|
||
elif filter_type == "range" and isinstance(filter_value, list) and len(filter_value) == 2:
|
||
converted_values = []
|
||
for idx, v in enumerate(filter_value):
|
||
if isinstance(v, str):
|
||
if self._is_datetime_string(v):
|
||
parsed = self._parse_datetime_string(v)
|
||
converted_values.append(parsed if parsed else v)
|
||
elif self._is_date_string(v):
|
||
# 对于 range 查询,第一个值(开始)使用当天开始,第二个值(结束)使用当天结束
|
||
try:
|
||
parsed_date = date.fromisoformat(v)
|
||
if idx == 0:
|
||
# 开始日期:当天 00:00:00
|
||
start_of_day = datetime.combine(parsed_date, datetime.min.time())
|
||
converted_values.append(start_of_day)
|
||
else:
|
||
# 结束日期:当天 23:59:59.999999
|
||
end_of_day = datetime.combine(parsed_date, datetime.max.time())
|
||
converted_values.append(end_of_day)
|
||
except ValueError:
|
||
converted_values.append(v)
|
||
else:
|
||
converted_values.append(v)
|
||
else:
|
||
converted_values.append(v)
|
||
converted[field] = {"type": filter_type, "value": converted_values}
|
||
else:
|
||
converted[field] = value
|
||
elif isinstance(value, str):
|
||
# 简单等值条件:布尔字符串
|
||
bool_val = self._convert_bool_string(value)
|
||
if bool_val is not None:
|
||
converted[field] = bool_val
|
||
# 简单等值条件的日期/日期时间字符串
|
||
elif self._is_datetime_string(value):
|
||
parsed = self._parse_datetime_string(value)
|
||
converted[field] = parsed if parsed else value
|
||
elif self._is_date_string(value):
|
||
try:
|
||
converted[field] = date.fromisoformat(value)
|
||
except ValueError:
|
||
converted[field] = value
|
||
else:
|
||
converted[field] = value
|
||
else:
|
||
converted[field] = value
|
||
|
||
return converted
|
||
|
||
async def _convert_form_data_filter_to_ids(
|
||
self,
|
||
db: AsyncSession,
|
||
relation_config: Dict[str, str],
|
||
value: Any,
|
||
) -> Any:
|
||
"""将表单选择器字段的显示名筛选转为关联表主键 ID(支持第三方库)。"""
|
||
form_code = relation_config.get("form_code")
|
||
display_column = relation_config.get("display_column", "name")
|
||
relation_key = relation_config.get("relation_key", "id")
|
||
if not form_code:
|
||
return value
|
||
|
||
is_list, parsed = self._parse_list_value(value)
|
||
if is_list and parsed and all(self._is_uuid_like(v) for v in parsed):
|
||
if len(parsed) == 1:
|
||
return parsed[0]
|
||
return {"type": "in", "value": parsed}
|
||
if isinstance(value, list) and all(self._is_uuid_like(v) for v in value):
|
||
if len(value) == 1:
|
||
return value[0]
|
||
return {"type": "in", "value": value}
|
||
if isinstance(value, str) and self._is_uuid_like(value):
|
||
return value
|
||
|
||
from online_dev.form_manager.model import FormMeta as RelatedFormMeta
|
||
|
||
stmt = select(RelatedFormMeta).where(
|
||
RelatedFormMeta.code == form_code,
|
||
RelatedFormMeta.is_deleted == False,
|
||
)
|
||
result = await db.execute(stmt)
|
||
related_form = result.scalar_one_or_none()
|
||
if not related_form:
|
||
return value
|
||
|
||
rel_adapter, rel_builder = await resolve_form_sql_context(
|
||
db,
|
||
related_form,
|
||
adapter_cache=self._adapter_cache,
|
||
builder_cache=self._sql_builder_cache,
|
||
)
|
||
table = related_form.main_table
|
||
schema = await self._resolve_schema(db, related_form.main_table_schema) or None
|
||
rel_database = related_form.main_table_database or None
|
||
table_name = rel_builder.build_table_name(table, schema, rel_database)
|
||
key_col = rel_builder.quote_identifier(relation_key)
|
||
display_col = rel_builder.quote_identifier(display_column)
|
||
rel_exec_db = self._table_exec_database(rel_database)
|
||
|
||
has_is_deleted = await self._check_column_exists(
|
||
db,
|
||
table,
|
||
related_form.main_table_schema,
|
||
related_form.main_table_database,
|
||
"is_deleted",
|
||
sql_builder=rel_builder,
|
||
db_adapter=rel_adapter,
|
||
)
|
||
deleted_clause = (
|
||
f" AND {rel_builder.is_deleted_predicate()}" if has_is_deleted else ""
|
||
)
|
||
|
||
if isinstance(value, dict) and "type" in value:
|
||
filter_type = value.get("type")
|
||
filter_value = value.get("value")
|
||
if filter_type in ("space_like_and", "space_like_or", "space_eq_and", "space_eq_or"):
|
||
return value
|
||
if filter_type == "in" and isinstance(filter_value, list):
|
||
if all(self._is_uuid_like(v) for v in filter_value):
|
||
return value
|
||
if filter_type == "like":
|
||
search_sql = (
|
||
f"SELECT {key_col} as id FROM {table_name} "
|
||
f"WHERE {display_col} LIKE :search_value{deleted_clause}"
|
||
)
|
||
params = {"search_value": f"%{filter_value}%"}
|
||
else:
|
||
case_sensitive = value.get("case_sensitive", True)
|
||
if case_sensitive:
|
||
where_clause = f"{display_col} = :search_value"
|
||
elif rel_builder.db_type == "postgresql":
|
||
where_clause = (
|
||
f"CAST({display_col} AS TEXT) ILIKE :search_value"
|
||
)
|
||
else:
|
||
where_clause = (
|
||
f"LOWER(CAST({display_col} AS VARCHAR(4000))) "
|
||
f"= LOWER(:search_value)"
|
||
)
|
||
search_sql = (
|
||
f"SELECT {key_col} as id FROM {table_name} "
|
||
f"WHERE {where_clause}{deleted_clause}"
|
||
)
|
||
params = {"search_value": filter_value}
|
||
else:
|
||
if rel_builder.db_type == "postgresql":
|
||
where_clause = f"CAST({display_col} AS TEXT) ILIKE :search_value"
|
||
else:
|
||
where_clause = f"{display_col} LIKE :search_value"
|
||
search_sql = (
|
||
f"SELECT {key_col} as id FROM {table_name} "
|
||
f"WHERE {where_clause}{deleted_clause}"
|
||
)
|
||
params = {"search_value": f"%{value}%"}
|
||
|
||
try:
|
||
rows = await rel_adapter.execute_query(
|
||
search_sql, params, database=rel_exec_db
|
||
)
|
||
except Exception as e:
|
||
logger.error("表单关联字段名称转 ID 失败 [%s]: %s", form_code, e)
|
||
return value
|
||
|
||
if not rows:
|
||
return "00000000-0000-0000-0000-000000000000"
|
||
ids = [row["id"] for row in rows]
|
||
if len(ids) == 1:
|
||
return ids[0]
|
||
return {"type": "in", "value": ids}
|
||
|
||
async def _convert_relation_name_to_id(self, db: AsyncSession, filters: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""将关联字段的名称搜索转换为 ID 搜索,或直接使用 ID 列表"""
|
||
if not filters:
|
||
return filters
|
||
|
||
# 获取关联字段配置
|
||
relation_fields = self._get_relation_fields()
|
||
|
||
if not relation_fields:
|
||
return filters
|
||
|
||
converted_filters = {}
|
||
|
||
for field, value in filters.items():
|
||
# 检查是否是关联字段
|
||
if field in relation_fields:
|
||
relation_config = relation_fields[field]
|
||
relation_type = relation_config.get("relation_type", "")
|
||
|
||
if relation_type == "form_data":
|
||
try:
|
||
converted_filters[field] = await self._convert_form_data_filter_to_ids(
|
||
db, relation_config, value
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"转换表单关联字段 {field} 搜索失败: {e}")
|
||
converted_filters[field] = value
|
||
continue
|
||
|
||
relation_table = relation_config.get("relation_table")
|
||
display_column = relation_config.get("display_column", "name")
|
||
|
||
if not relation_table:
|
||
converted_filters[field] = value
|
||
continue
|
||
|
||
try:
|
||
is_list, parsed = self._parse_list_value(value)
|
||
if is_list and parsed and all(self._is_uuid_like(v) for v in parsed):
|
||
if len(parsed) == 1:
|
||
converted_filters[field] = parsed[0]
|
||
else:
|
||
converted_filters[field] = {"type": "in", "value": parsed}
|
||
continue
|
||
if isinstance(value, list) and all(self._is_uuid_like(v) for v in value):
|
||
if len(value) == 1:
|
||
converted_filters[field] = value[0]
|
||
else:
|
||
converted_filters[field] = {"type": "in", "value": value}
|
||
continue
|
||
|
||
# 检查是否是单个 UUID(直接传递的 ID)
|
||
if isinstance(value, str) and self._is_uuid_like(value):
|
||
converted_filters[field] = value
|
||
continue
|
||
|
||
# 根据名称查询平台关联表获取 ID(始终在平台 PG)
|
||
pb = self._platform_sql_builder
|
||
table_name = pb.build_table_name(relation_table, schema=PLATFORM_SCHEMA)
|
||
|
||
# 处理不同的过滤类型
|
||
if isinstance(value, dict) and "type" in value:
|
||
filter_type = value.get("type")
|
||
filter_value = value.get("value")
|
||
|
||
# 空格搜索类型是纯文本搜索,不需要转换关联字段
|
||
if filter_type in ("space_like_and", "space_like_or", "space_eq_and", "space_eq_or"):
|
||
converted_filters[field] = value
|
||
continue
|
||
|
||
# 如果 filter_value 是 ID 列表,直接使用
|
||
if filter_type == "in" and isinstance(filter_value, list):
|
||
if all(self._is_uuid_like(v) for v in filter_value):
|
||
converted_filters[field] = value
|
||
continue
|
||
|
||
if filter_type == "like":
|
||
query_sql = f"""
|
||
SELECT {pb.quote_identifier("id")}
|
||
FROM {table_name}
|
||
WHERE {pb.quote_identifier(display_column)} LIKE :search_value
|
||
AND {pb.is_deleted_predicate()}
|
||
"""
|
||
params = {"search_value": f"%{filter_value}%"}
|
||
else:
|
||
case_sensitive = value.get("case_sensitive", True)
|
||
if case_sensitive:
|
||
where_clause = f"{pb.quote_identifier(display_column)} = :search_value"
|
||
else:
|
||
where_clause = (
|
||
f"LOWER({pb.quote_identifier(display_column)}) "
|
||
f"= LOWER(:search_value)"
|
||
)
|
||
query_sql = f"""
|
||
SELECT {pb.quote_identifier("id")}
|
||
FROM {table_name}
|
||
WHERE {where_clause}
|
||
AND {pb.is_deleted_predicate()}
|
||
"""
|
||
params = {"search_value": filter_value}
|
||
else:
|
||
query_sql = f"""
|
||
SELECT {pb.quote_identifier("id")}
|
||
FROM {table_name}
|
||
WHERE {pb.quote_identifier(display_column)} LIKE :search_value
|
||
AND {pb.is_deleted_predicate()}
|
||
"""
|
||
params = {"search_value": f"%{value}%"}
|
||
|
||
result = await self._execute_platform_query(db, query_sql, params)
|
||
|
||
if result:
|
||
# 找到匹配的 ID,使用 ID 进行搜索
|
||
ids = [row["id"] for row in result]
|
||
if len(ids) == 1:
|
||
converted_filters[field] = ids[0]
|
||
else:
|
||
# 多个匹配,使用 IN 查询
|
||
converted_filters[field] = {"type": "in", "value": ids}
|
||
else:
|
||
# 没有找到匹配的名称,返回一个不存在的 ID(确保搜索结果为空)
|
||
converted_filters[field] = "00000000-0000-0000-0000-000000000000"
|
||
|
||
except Exception as e:
|
||
logger.error(f"转换关联字段 {field} 搜索失败: {e}")
|
||
converted_filters[field] = value
|
||
else:
|
||
# 非关联字段,保持原样
|
||
converted_filters[field] = value
|
||
|
||
return converted_filters
|
||
|
||
def _get_relation_fields(self) -> Dict[str, Dict[str, str]]:
|
||
"""从表单配置中提取需要关联查询的字段"""
|
||
relation_fields = {}
|
||
list_config = self._list_config
|
||
columns = list_config.get("columns", [])
|
||
form_config = self._form_config
|
||
|
||
logger.debug(f"表单配置 form_config 类型: {type(form_config)}")
|
||
logger.debug(f"表单配置内容: {form_config}")
|
||
logger.debug(f"表单配置 items 数量: {len(form_config.get('items', []))}")
|
||
logger.debug(f"列表配置 columns 数量: {len(columns)}")
|
||
|
||
# 构建字段到组件配置的映射
|
||
field_to_component = {}
|
||
|
||
def traverse_items(items):
|
||
"""递归遍历表单项,构建字段映射"""
|
||
if not items:
|
||
return
|
||
for item in items:
|
||
item_type = item.get("type", "")
|
||
field = item.get("field", "")
|
||
|
||
# 容器类型,递归处理子项
|
||
if item_type == "grid":
|
||
for col in item.get("columns", []):
|
||
traverse_items(col.get("children", []))
|
||
elif item_type in ("collapse", "steps"):
|
||
for panel in item.get("items", []):
|
||
traverse_items(panel.get("children", []))
|
||
elif item_type == "tabs":
|
||
for panel in item.get("children", []):
|
||
traverse_items(panel.get("children", []))
|
||
elif item_type == "sub-table":
|
||
traverse_items(item.get("children", []))
|
||
elif field:
|
||
# 保存字段对应的组件配置
|
||
field_to_component[field] = item
|
||
logger.debug(f"找到字段: {field}, 类型: {item_type}")
|
||
|
||
traverse_items(form_config.get("items", []))
|
||
|
||
logger.debug(f"构建的字段映射: {list(field_to_component.keys())}")
|
||
|
||
# 遍历列表配置,查找需要关联查询的字段
|
||
for col in columns:
|
||
field = col.get("field")
|
||
display_field = col.get("displayField")
|
||
|
||
if not field or not display_field:
|
||
continue
|
||
|
||
logger.debug(f"检查字段 {field},displayField: {display_field}")
|
||
|
||
# 从表单配置中找到对应的组件
|
||
component = field_to_component.get(field)
|
||
if not component:
|
||
# 组件可能在未遍历到的容器中,尝试从列配置的保存信息中恢复
|
||
if col.get("isFormDataSelector") and col.get("formCode"):
|
||
display_field_name = col.get("displayFieldName", "")
|
||
form_code = col.get("formCode", "")
|
||
relation_key = col.get("valueField", "id") or "id"
|
||
display_column = display_field_name or col.get("labelField", "name") or "name"
|
||
relation_fields[field] = {
|
||
"display_field": display_field,
|
||
"relation_type": "form_data",
|
||
"form_code": form_code,
|
||
"relation_key": relation_key,
|
||
"display_column": display_column,
|
||
}
|
||
logger.debug(f"字段 {field} 组件未找到,从列配置恢复: formCode={form_code}, displayColumn={display_column}")
|
||
else:
|
||
logger.warning(f"字段 {field} 在表单配置中未找到对应组件")
|
||
continue
|
||
|
||
# 获取组件的 props
|
||
props = component.get("props", {})
|
||
logger.debug(f"字段 {field} 的组件类型: {component.get('type')}, props: {props}")
|
||
|
||
# 从 props 中提取关联表信息
|
||
relation_table = props.get("relationTable")
|
||
relation_key = props.get("relationKey", "id")
|
||
display_column = props.get("displayColumn", "name")
|
||
|
||
# 如果 props 中没有配置,尝试根据组件类型和字段名推断
|
||
if not relation_table:
|
||
component_type = component.get("type", "")
|
||
# 根据组件类型推断
|
||
type_mapping = {
|
||
"department-selector": "core_dept",
|
||
"dept-selector": "core_dept",
|
||
"dept-select": "core_dept",
|
||
"user-selector": "core_user",
|
||
"user-select": "core_user",
|
||
"position-selector": "core_post",
|
||
"post-selector": "core_post",
|
||
"role-selector": "core_role",
|
||
"org-selector": "core_dept",
|
||
"region-selector": "core_region",
|
||
}
|
||
relation_table = type_mapping.get(component_type)
|
||
|
||
# 如果还是没有,根据字段名推断
|
||
if not relation_table:
|
||
field_mapping = {
|
||
"dept_id": "core_dept",
|
||
"department_id": "core_dept",
|
||
"user_id": "core_user",
|
||
"manger_id": "core_user",
|
||
"manager_id": "core_user",
|
||
"position_id": "core_post",
|
||
"positon_id": "core_post",
|
||
}
|
||
relation_table = field_mapping.get(field)
|
||
|
||
if relation_table:
|
||
relation_fields[field] = {
|
||
"display_field": display_field,
|
||
"relation_table": relation_table,
|
||
"relation_key": relation_key,
|
||
"display_column": display_column,
|
||
}
|
||
logger.debug(f"字段 {field} 配置了关联表: {relation_table}")
|
||
else:
|
||
# 检查是否为表单数据选择器(select 或 table-selector 组件,数据源类型为 formData)
|
||
# 或者是新的 form-selector 组件
|
||
component_type = component.get("type", "")
|
||
|
||
# form-selector 组件的配置存储在 formSelectorConfig 中
|
||
form_selector_config = component.get("formSelectorConfig") or {}
|
||
|
||
# 数据源配置存储在 dataSource 中,而不是 props 中
|
||
data_source = component.get("dataSource") or {}
|
||
data_source_type = data_source.get("type", "") if data_source else ""
|
||
if not data_source_type:
|
||
data_source_type = props.get("dataSourceType", "")
|
||
|
||
# 获取 formCode:优先从 formSelectorConfig 获取,其次从 dataSource 获取
|
||
form_code = form_selector_config.get("formCode", "") or data_source.get("formCode", "") or props.get("formCode", "")
|
||
|
||
display_field_name = col.get("displayFieldName", "") # 从列表配置中获取要显示的字段名
|
||
|
||
# form-selector 组件直接处理
|
||
if component_type == "form-selector" and form_code:
|
||
relation_key = form_selector_config.get("valueField", "id") or "id"
|
||
label_field = form_selector_config.get("labelField", "name") or "name"
|
||
relation_fields[field] = {
|
||
"display_field": display_field,
|
||
"relation_type": "form_data",
|
||
"form_code": form_code,
|
||
"relation_key": relation_key,
|
||
"display_column": display_field_name or label_field,
|
||
}
|
||
logger.debug(f"字段 {field} 配置了表单选择器关联: formCode={form_code}, relationKey={relation_key}, displayColumn={display_field_name or label_field}")
|
||
elif component_type in ["select", "tree-select", "radio", "checkbox", "cascader", "table-selector"] and data_source_type == "formData" and form_code:
|
||
relation_key = data_source.get("formValueField") or data_source.get("valueField", "id") or "id"
|
||
label_field = data_source.get("formLabelField") or data_source.get("labelField", "name") or "name"
|
||
relation_fields[field] = {
|
||
"display_field": display_field,
|
||
"relation_type": "form_data",
|
||
"form_code": form_code,
|
||
"relation_key": relation_key,
|
||
"display_column": display_field_name or label_field,
|
||
}
|
||
logger.debug(f"字段 {field} 配置了表单数据关联: formCode={form_code}, relationKey={relation_key}, displayColumn={display_field_name or label_field}")
|
||
else:
|
||
logger.warning(f"字段 {field} 未配置 relationTable 且无法自动推断")
|
||
|
||
# 添加系统字段的关联配置(这些字段不在表单配置中,但需要关联查询)
|
||
system_field_mapping = {
|
||
"sys_creator_id": {"relation_table": "core_user", "display_column": "name"},
|
||
"sys_modifier_id": {"relation_table": "core_user", "display_column": "name"},
|
||
"sys_dept_id": {"relation_table": "core_dept", "display_column": "name"},
|
||
}
|
||
|
||
for col in columns:
|
||
field = col.get("field")
|
||
display_field = col.get("displayField")
|
||
|
||
if field in system_field_mapping and display_field:
|
||
config = system_field_mapping[field]
|
||
relation_fields[field] = {
|
||
"display_field": display_field,
|
||
"relation_table": config["relation_table"],
|
||
"relation_key": "id",
|
||
"display_column": config["display_column"],
|
||
}
|
||
logger.debug(f"系统字段 {field} 配置了关联表: {config['relation_table']}")
|
||
|
||
# 识别没有 displayField 的数据源字段(字典 / 表单数据),原地替换 value 为 label
|
||
column_fields = {col.get("field") for col in columns if col.get("field")}
|
||
for field, component in field_to_component.items():
|
||
if field in relation_fields or field not in column_fields:
|
||
continue
|
||
data_source = component.get("dataSource") or {}
|
||
ds_type = data_source.get("type", "")
|
||
|
||
if ds_type == "dict" and data_source.get("dictCode"):
|
||
display_field = f"{field}_label"
|
||
relation_fields[field] = {
|
||
"display_field": display_field,
|
||
"relation_type": "dict",
|
||
"dict_code": data_source["dictCode"],
|
||
}
|
||
logger.debug(f"字段 {field} 配置了字典数据源: dictCode={data_source['dictCode']}, displayField={display_field}")
|
||
elif ds_type == "formData" and data_source.get("formCode"):
|
||
component_type = component.get("type", "")
|
||
if component_type in ["select", "tree-select", "radio", "checkbox", "cascader", "table-selector"]:
|
||
form_code = data_source["formCode"]
|
||
relation_key = data_source.get("formValueField") or data_source.get("valueField", "id") or "id"
|
||
label_field = data_source.get("formLabelField") or data_source.get("labelField", "name") or "name"
|
||
display_field = f"{field}_name"
|
||
relation_fields[field] = {
|
||
"display_field": display_field,
|
||
"relation_type": "form_data",
|
||
"form_code": form_code,
|
||
"relation_key": relation_key,
|
||
"display_column": label_field,
|
||
}
|
||
logger.debug(f"字段 {field} 无 displayField,自动补充表单数据关联: formCode={form_code}, displayField={display_field}")
|
||
elif ds_type == "dataSource" and data_source.get("dataSourceCode"):
|
||
display_field = f"{field}_label"
|
||
relation_fields[field] = {
|
||
"display_field": display_field,
|
||
"relation_type": "data_source",
|
||
"data_source_code": data_source["dataSourceCode"],
|
||
"value_field": data_source.get("valueField") or "value",
|
||
"label_field": data_source.get("labelField") or "label",
|
||
}
|
||
# 静态选项(select/radio/checkbox/cascader/tree-select 等组件的硬编码 options)
|
||
elif component.get("type") in ["select", "radio", "checkbox", "cascader", "tree-select"] and component.get("options"):
|
||
if not ds_type or ds_type == "static":
|
||
display_field = f"{field}_label"
|
||
relation_fields[field] = {
|
||
"display_field": display_field,
|
||
"relation_type": "static",
|
||
"options": component.get("options", []),
|
||
}
|
||
logger.debug(f"字段 {field} 配置了静态选项: displayField={display_field}, 选项数={len(component.get('options', []))}")
|
||
|
||
return relation_fields
|
||
|
||
def _get_all_selector_relation_fields(self, existing: Dict[str, Dict[str, str]]) -> Dict[str, Dict[str, str]]:
|
||
"""从 form_config 中提取所有选择器字段的关联配置,补充 _get_relation_fields 可能遗漏的字段。
|
||
|
||
_get_relation_fields 依赖 list_config.columns,不在列表中的选择器字段不会被识别。
|
||
此方法直接从 form_config.items 遍历所有选择器组件,确保 get() 上下文下也能填充显示名。
|
||
"""
|
||
result = dict(existing)
|
||
form_config = self._form_config
|
||
|
||
system_type_mapping = {
|
||
"dept-selector": "core_dept",
|
||
"user-selector": "core_user",
|
||
"role-selector": "core_role",
|
||
"post-selector": "core_post",
|
||
"region-selector": "core_region",
|
||
}
|
||
|
||
def traverse(items):
|
||
if not items:
|
||
return
|
||
for item in items:
|
||
t = item.get("type", "")
|
||
field = item.get("field", "")
|
||
|
||
if t == "grid":
|
||
for col in item.get("columns", []):
|
||
traverse(col.get("children", []))
|
||
elif t in ("collapse", "steps"):
|
||
for panel in item.get("items", []):
|
||
traverse(panel.get("children", []))
|
||
elif t == "tabs":
|
||
for panel in item.get("children", []):
|
||
traverse(panel.get("children", []))
|
||
elif t == "sub-table":
|
||
traverse(item.get("children", []))
|
||
elif field and field not in result:
|
||
if t in system_type_mapping:
|
||
display_field = field.replace("_id", "_name") if field.endswith("_id") else f"{field}_name"
|
||
result[field] = {
|
||
"display_field": display_field,
|
||
"relation_table": system_type_mapping[t],
|
||
"relation_key": "id",
|
||
"display_column": "name",
|
||
}
|
||
elif t == "form-selector":
|
||
fsc = item.get("formSelectorConfig") or {}
|
||
form_code = fsc.get("formCode") or item.get("props", {}).get("formCode") or ""
|
||
if form_code:
|
||
label_field = fsc.get("labelField") or item.get("props", {}).get("labelField") or "name"
|
||
value_field = fsc.get("valueField") or "id"
|
||
if label_field not in ("name", "label"):
|
||
display_field = f"{field}_{label_field}"
|
||
elif field.endswith("_id"):
|
||
display_field = field.replace("_id", "_name")
|
||
else:
|
||
display_field = f"{field}_name"
|
||
result[field] = {
|
||
"display_field": display_field,
|
||
"relation_type": "form_data",
|
||
"form_code": form_code,
|
||
"relation_key": value_field,
|
||
"display_column": label_field,
|
||
}
|
||
elif t == "table-selector":
|
||
ds = item.get("dataSource") or {}
|
||
form_code = ds.get("formCode") or item.get("props", {}).get("formCode") or ""
|
||
if form_code:
|
||
label_field = ds.get("formLabelField") or item.get("props", {}).get("labelField") or "label"
|
||
value_field = ds.get("formValueField") or ds.get("valueField") or "id"
|
||
if label_field not in ("name", "label"):
|
||
display_field = f"{field}_{label_field}"
|
||
elif field.endswith("_id"):
|
||
display_field = field.replace("_id", "_name")
|
||
else:
|
||
display_field = f"{field}_name"
|
||
result[field] = {
|
||
"display_field": display_field,
|
||
"relation_type": "form_data",
|
||
"form_code": form_code,
|
||
"relation_key": value_field,
|
||
"display_column": label_field,
|
||
}
|
||
# 静态选项(select/radio/checkbox/cascader/tree-select 的硬编码 options)
|
||
elif t in ["select", "radio", "checkbox", "cascader", "tree-select"]:
|
||
ds = item.get("dataSource") or {}
|
||
ds_type = ds.get("type", "")
|
||
options = item.get("options", [])
|
||
if options and (not ds_type or ds_type == "static"):
|
||
display_field = f"{field}_label" if not field.endswith("_id") else field.replace("_id", "_label")
|
||
result[field] = {
|
||
"display_field": display_field,
|
||
"relation_type": "static",
|
||
"options": options,
|
||
}
|
||
|
||
traverse(form_config.get("items", []))
|
||
return result
|
||
|
||
async def _fill_relation_display_names(
|
||
self,
|
||
db: AsyncSession,
|
||
items: List[Dict[str, Any]],
|
||
relation_fields: Dict[str, Dict[str, str]]
|
||
) -> List[Dict[str, Any]]:
|
||
"""填充关联字段的显示名称"""
|
||
for field, config in relation_fields.items():
|
||
relation_type = config.get("relation_type", "")
|
||
relation_table = config.get("relation_table", "")
|
||
|
||
# 表单数据选择器特殊处理
|
||
if relation_type == "form_data":
|
||
try:
|
||
await self._fill_form_data_display_field(db, items, field, config)
|
||
except Exception as e:
|
||
logger.warning(f"填充表单数据字段 {field} 失败: {str(e)}")
|
||
try:
|
||
await db.rollback()
|
||
except Exception:
|
||
pass
|
||
continue
|
||
|
||
if relation_type == "data_source":
|
||
code = config.get("data_source_code")
|
||
value_field = config.get("value_field", "value")
|
||
label_field = config.get("label_field", "label")
|
||
display_field = config.get("display_field", f"{field}_label")
|
||
if not code:
|
||
continue
|
||
try:
|
||
from core.data_source.service import DataSourceService
|
||
|
||
rows = await DataSourceService.execute(db, code, {})
|
||
if not isinstance(rows, list):
|
||
rows = []
|
||
value_to_label = {}
|
||
for row in rows:
|
||
if isinstance(row, dict):
|
||
vk = row.get(value_field)
|
||
if vk is not None:
|
||
value_to_label[str(vk)] = row.get(label_field, vk)
|
||
for item in items:
|
||
v = item.get(field)
|
||
if v is not None:
|
||
is_list, parsed = self._parse_list_value(v)
|
||
if is_list:
|
||
item[display_field] = [
|
||
value_to_label.get(str(x), x) for x in parsed
|
||
]
|
||
else:
|
||
item[display_field] = value_to_label.get(str(v), v)
|
||
except Exception as e:
|
||
logger.warning(f"填充平台数据源字段 {field} 失败: {str(e)}")
|
||
continue
|
||
|
||
# 字典数据源:批量查询 dict_item,将 label 写入显示字段(不覆盖原值)
|
||
if relation_type == "dict":
|
||
dict_code = config.get("dict_code")
|
||
if not dict_code:
|
||
continue
|
||
try:
|
||
values = set()
|
||
for item in items:
|
||
v = item.get(field)
|
||
if v is not None:
|
||
is_list, parsed = self._parse_list_value(v)
|
||
if is_list:
|
||
values.update(str(x) for x in parsed if x is not None)
|
||
else:
|
||
values.add(str(v))
|
||
if not values:
|
||
continue
|
||
value_to_label = await self._query_dict_labels(db, dict_code, list(values))
|
||
|
||
# 确定显示字段名:优先使用配置的 display_field,否则自动生成 {field}_label
|
||
display_field = config.get("display_field", f"{field}_label")
|
||
|
||
for item in items:
|
||
v = item.get(field)
|
||
if v is not None:
|
||
is_list, parsed = self._parse_list_value(v)
|
||
if is_list:
|
||
item[display_field] = [value_to_label.get(str(x), x) for x in parsed]
|
||
else:
|
||
item[display_field] = value_to_label.get(str(v), v)
|
||
logger.debug(f"字典字段 {field} 翻译完成: dictCode={dict_code}, 映射数={len(value_to_label)}, displayField={display_field}")
|
||
except Exception as e:
|
||
logger.warning(f"填充字典字段 {field} 失败: {str(e)}")
|
||
continue
|
||
|
||
# 静态选项:从 options 中查找 label
|
||
if relation_type == "static":
|
||
options = config.get("options", [])
|
||
if not options:
|
||
continue
|
||
display_field = config.get("display_field", f"{field}_label")
|
||
try:
|
||
value_to_label = {}
|
||
for opt in options:
|
||
opt_value = opt.get("value")
|
||
if isinstance(opt_value, bool) or opt_value is not None:
|
||
value_to_label[str(opt_value)] = opt.get("label", str(opt_value))
|
||
for item in items:
|
||
v = item.get(field)
|
||
if v is not None:
|
||
is_list, parsed = self._parse_list_value(v)
|
||
if is_list:
|
||
item[display_field] = [value_to_label.get(str(x), x) for x in parsed]
|
||
else:
|
||
item[display_field] = value_to_label.get(str(v), v)
|
||
logger.debug(f"静态选项字段 {field} 翻译完成: displayField={display_field}, 选项数={len(options)}")
|
||
except Exception as e:
|
||
logger.warning(f"填充静态选项字段 {field} 失败: {str(e)}")
|
||
continue
|
||
|
||
# 如果没有关联表,跳过
|
||
if not relation_table:
|
||
logger.warning(f"字段 {field} 没有配置关联表,跳过")
|
||
continue
|
||
|
||
# 省市区组件特殊处理
|
||
if relation_table == "core_region":
|
||
try:
|
||
codes = set()
|
||
for item in items:
|
||
is_list, parsed = self._parse_list_value(item.get(field))
|
||
if is_list:
|
||
codes.update(str(v) for v in parsed if v)
|
||
|
||
if not codes:
|
||
continue
|
||
|
||
code_to_name = await self._query_region_names(db, list(codes))
|
||
display_field = config["display_field"]
|
||
|
||
for item in items:
|
||
is_list, parsed = self._parse_list_value(item.get(field))
|
||
if is_list:
|
||
names = [
|
||
code_to_name.get(str(c), str(c)) for c in parsed if c
|
||
]
|
||
item[display_field] = " / ".join(names)
|
||
except Exception as e:
|
||
logger.warning(f"填充省市区字段 {field} 失败: {str(e)}")
|
||
continue
|
||
elif relation_table:
|
||
# 其他关联组件的处理逻辑(用户、部门等)
|
||
ids = self._collect_field_ids(items, field)
|
||
if not ids:
|
||
continue
|
||
|
||
logger.info(f"准备查询关联字段 {field},关联表: {relation_table},IDs: {ids}")
|
||
|
||
try:
|
||
# 批量查询关联数据
|
||
relation_data = await self._query_relation_data(
|
||
db,
|
||
config["relation_table"],
|
||
config["relation_key"],
|
||
config["display_column"],
|
||
list(ids)
|
||
)
|
||
|
||
# 构建 ID 到名称的映射
|
||
id_to_name = {str(row["id"]): row["name"] for row in relation_data}
|
||
display_field = config["display_field"]
|
||
|
||
logger.info(f"字段 {field} 的 ID 到名称映射: {id_to_name}")
|
||
|
||
for item in items:
|
||
self._set_display_from_id_map(item, field, display_field, id_to_name)
|
||
except Exception as e:
|
||
logger.warning(f"填充关联字段 {field} 失败: {str(e)}")
|
||
# 回滚事务,避免影响后续查询
|
||
try:
|
||
await db.rollback()
|
||
except Exception:
|
||
pass
|
||
# 填充失败不影响主流程,继续处理其他字段
|
||
continue
|
||
|
||
return items
|
||
|
||
@staticmethod
|
||
def _parse_list_value(v: Any) -> Tuple[bool, list]:
|
||
"""将字段值解析为列表。
|
||
级联/多选组件的值可能是 Python list(json/jsonb 列)、JSON 字符串(varchar/text 列),
|
||
或历史数据中 Python str(list) 产生的单引号格式(如 "['a', 'b']")。
|
||
返回 (is_list, parsed_list);非列表值返回 (False, [])。
|
||
"""
|
||
if isinstance(v, list):
|
||
return True, v
|
||
if isinstance(v, str) and v.startswith("["):
|
||
try:
|
||
parsed = json.loads(v)
|
||
if isinstance(parsed, list):
|
||
return True, parsed
|
||
except (json.JSONDecodeError, ValueError):
|
||
pass
|
||
# 兼容历史数据:str(list) 产生的单引号格式,如 "['a', 'b']"
|
||
try:
|
||
import ast
|
||
parsed = ast.literal_eval(v)
|
||
if isinstance(parsed, list):
|
||
return True, parsed
|
||
except (ValueError, SyntaxError):
|
||
pass
|
||
return False, []
|
||
|
||
@staticmethod
|
||
def _collect_field_ids(items: List[Dict[str, Any]], field: str) -> set:
|
||
"""从列表行中收集关联字段 ID(兼容 JSON 字符串形式的多选值)。"""
|
||
ids: set = set()
|
||
for item in items:
|
||
v = item.get(field)
|
||
if v is None:
|
||
continue
|
||
is_list, parsed = FormDataService._parse_list_value(v)
|
||
if is_list:
|
||
ids.update(str(x) for x in parsed if x is not None)
|
||
else:
|
||
ids.add(str(v))
|
||
return ids
|
||
|
||
@staticmethod
|
||
def _set_display_from_id_map(
|
||
item: Dict[str, Any],
|
||
field: str,
|
||
display_field: str,
|
||
id_to_name: Dict[str, str],
|
||
) -> None:
|
||
"""将 ID(或 ID 列表)映射为显示名称写入 display_field。"""
|
||
value = item.get(field)
|
||
if not value:
|
||
return
|
||
is_list, parsed = FormDataService._parse_list_value(value)
|
||
if is_list:
|
||
names = [id_to_name.get(str(v), str(v)) for v in parsed if v]
|
||
item[display_field] = ", ".join(names)
|
||
else:
|
||
item[display_field] = id_to_name.get(str(value), str(value))
|
||
|
||
async def _query_dict_labels(self, db: AsyncSession, dict_code: str, values: List[str]) -> Dict[str, str]:
|
||
"""批量查询字典项的 value->label 映射"""
|
||
from core.dict.model import Dict as DictModel
|
||
from core.dict_item.model import DictItem
|
||
|
||
stmt = (
|
||
select(DictItem.value, DictItem.label)
|
||
.join(DictModel, DictItem.dict_id == DictModel.id)
|
||
.where(
|
||
DictModel.code == dict_code,
|
||
DictItem.value.in_(values),
|
||
DictItem.is_deleted == False,
|
||
)
|
||
)
|
||
result = await db.execute(stmt)
|
||
return {row.value: row.label for row in result}
|
||
|
||
async def _query_form_data_labels(
|
||
self,
|
||
db: AsyncSession,
|
||
field: str,
|
||
config: Dict[str, Any],
|
||
items: List[Dict[str, Any]]
|
||
) -> Dict[str, str]:
|
||
"""查询关联表单数据,返回 value->label 映射(用于无 displayField 的表单数据源字段)"""
|
||
form_code = config.get("form_code")
|
||
relation_key = config.get("relation_key", "id")
|
||
display_column = config.get("display_column", "name")
|
||
|
||
# 收集所有不重复的 value(兼容 JSON 字符串数组)
|
||
ids = set()
|
||
for item in items:
|
||
v = item.get(field)
|
||
if v is not None:
|
||
is_list, parsed = self._parse_list_value(v)
|
||
if is_list:
|
||
ids.update(str(x) for x in parsed if x is not None)
|
||
else:
|
||
ids.add(str(v))
|
||
|
||
if not ids or not form_code:
|
||
return {}
|
||
|
||
from online_dev.form_manager.model import FormMeta as RelatedFormMeta
|
||
|
||
stmt = select(RelatedFormMeta).where(
|
||
RelatedFormMeta.code == form_code,
|
||
RelatedFormMeta.is_deleted == False
|
||
)
|
||
result = await db.execute(stmt)
|
||
related_form = result.scalar_one_or_none()
|
||
|
||
if not related_form:
|
||
logger.warning(f"关联表单 {form_code} 不存在")
|
||
return {}
|
||
|
||
rel_adapter, rel_builder = await resolve_form_sql_context(
|
||
db,
|
||
related_form,
|
||
adapter_cache=self._adapter_cache,
|
||
builder_cache=self._sql_builder_cache,
|
||
)
|
||
table = related_form.main_table
|
||
schema = await self._resolve_schema(db, related_form.main_table_schema) or None
|
||
database = related_form.main_table_database or None
|
||
|
||
table_name = rel_builder.build_table_name(table, schema, database)
|
||
key_col = rel_builder.quote_identifier(relation_key)
|
||
display_col = rel_builder.quote_identifier(display_column)
|
||
|
||
has_is_deleted = await self._check_column_exists(
|
||
db,
|
||
table,
|
||
related_form.main_table_schema,
|
||
related_form.main_table_database,
|
||
"is_deleted",
|
||
sql_builder=rel_builder,
|
||
db_adapter=rel_adapter,
|
||
)
|
||
|
||
ids_list = list(ids)
|
||
placeholders = ", ".join([f":{i}" for i in range(len(ids_list))])
|
||
is_deleted_clause = (
|
||
f"\n AND {rel_builder.is_deleted_predicate()}"
|
||
if has_is_deleted
|
||
else ""
|
||
)
|
||
sql = f"""
|
||
SELECT {key_col} as id, {display_col} as name
|
||
FROM {table_name}
|
||
WHERE {key_col} IN ({placeholders}){is_deleted_clause}
|
||
"""
|
||
params = {str(i): ids_list[i] for i in range(len(ids_list))}
|
||
rel_exec_db = (database or "").strip() or None
|
||
rows = await rel_adapter.execute_query(sql, params, database=rel_exec_db)
|
||
|
||
return {str(row["id"]): str(row["name"]) for row in rows}
|
||
|
||
async def _fill_virtual_fields(
|
||
self,
|
||
db: AsyncSession,
|
||
items: List[Dict[str, Any]],
|
||
relation_fields: Dict[str, Dict[str, str]],
|
||
context: str = "list"
|
||
) -> List[Dict[str, Any]]:
|
||
"""填充虚拟字段的值
|
||
|
||
虚拟字段通过值关联配置(isVirtualField=true),从源字段的关联数据中
|
||
提取指定属性填充到结果中,不对应数据库列。
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
items: 数据列表
|
||
relation_fields: 已识别的关联字段配置(来自 _get_relation_fields)
|
||
context: 调用上下文,'list' 列表接口(受 showVirtualValue 开关控制),'form' 表单详情(始终填充)
|
||
"""
|
||
if not items:
|
||
return items
|
||
|
||
# 从 form_config 中提取虚拟字段配置
|
||
virtual_fields = self._get_virtual_fields()
|
||
if not virtual_fields:
|
||
return items
|
||
|
||
# 构建 list_config 中虚拟字段的 showVirtualValue 映射
|
||
list_config = self._list_config
|
||
columns = list_config.get("columns", [])
|
||
col_show_map = {}
|
||
for col in columns:
|
||
if col.get("isVirtualField"):
|
||
col_show_map[col.get("field", "")] = col.get("showVirtualValue", True)
|
||
|
||
logger.info(f"识别到的虚拟字段: {virtual_fields}")
|
||
|
||
for vf in virtual_fields:
|
||
vf_field = vf["field"] # 虚拟字段名
|
||
source_field = vf["valueSourceField"] # 源字段名
|
||
display_field = vf["valueDisplayField"] # 要提取的属性名
|
||
|
||
# 仅在列表上下文中检查 showVirtualValue 开关,表单详情始终填充
|
||
if context == "list" and not col_show_map.get(vf_field, True):
|
||
logger.info(f"虚拟字段 {vf_field} 的显示关联值已关闭,跳过")
|
||
continue
|
||
|
||
# 从 relation_fields 中获取源字段的关联配置
|
||
source_config = relation_fields.get(source_field)
|
||
|
||
# 如果源字段不在 relation_fields 中(可能未添加到列表列),从 form_config 推断
|
||
if not source_config:
|
||
source_config = self._infer_relation_config(source_field)
|
||
|
||
if not source_config:
|
||
logger.warning(f"虚拟字段 {vf_field} 的源字段 {source_field} 没有关联配置,跳过")
|
||
continue
|
||
|
||
relation_type = source_config.get("relation_type", "")
|
||
relation_table = source_config.get("relation_table", "")
|
||
relation_key = source_config.get("relation_key", "id")
|
||
|
||
# 省市区组件不支持虚拟字段
|
||
if relation_table == "core_region":
|
||
logger.warning(f"虚拟字段 {vf_field} 的源字段 {source_field} 是省市区组件,不支持虚拟字段")
|
||
continue
|
||
|
||
ids = self._collect_field_ids(items, source_field)
|
||
if not ids:
|
||
continue
|
||
|
||
try:
|
||
id_to_value = {}
|
||
|
||
if relation_type == "form_data":
|
||
# 表单数据选择器:查询关联表单的数据表
|
||
form_code = source_config.get("form_code")
|
||
if not form_code:
|
||
continue
|
||
|
||
from online_dev.form_manager.model import FormMeta as RelatedFormMeta
|
||
stmt = select(RelatedFormMeta).where(
|
||
RelatedFormMeta.code == form_code,
|
||
RelatedFormMeta.is_deleted == False
|
||
)
|
||
result = await db.execute(stmt)
|
||
related_form = result.scalar_one_or_none()
|
||
|
||
if not related_form:
|
||
logger.warning(f"虚拟字段 {vf_field} 关联表单 {form_code} 不存在")
|
||
continue
|
||
|
||
rel_adapter, rel_builder = await resolve_form_sql_context(
|
||
db,
|
||
related_form,
|
||
adapter_cache=self._adapter_cache,
|
||
builder_cache=self._sql_builder_cache,
|
||
)
|
||
table = related_form.main_table
|
||
schema = await self._resolve_schema(db, related_form.main_table_schema) or None
|
||
database = related_form.main_table_database or None
|
||
table_name = rel_builder.build_table_name(table, schema, database)
|
||
key_col = rel_builder.quote_identifier(relation_key)
|
||
val_col = rel_builder.quote_identifier(display_field)
|
||
|
||
has_is_deleted = await self._check_column_exists(
|
||
db,
|
||
table,
|
||
related_form.main_table_schema,
|
||
related_form.main_table_database,
|
||
"is_deleted",
|
||
sql_builder=rel_builder,
|
||
db_adapter=rel_adapter,
|
||
)
|
||
|
||
placeholders = ", ".join([f":{i}" for i in range(len(ids))])
|
||
ids_list = list(ids)
|
||
is_deleted_clause = (
|
||
f"\n AND {rel_builder.is_deleted_predicate()}"
|
||
if has_is_deleted
|
||
else ""
|
||
)
|
||
sql = f"""
|
||
SELECT {key_col} as id, {val_col} as val
|
||
FROM {table_name}
|
||
WHERE {key_col} IN ({placeholders}){is_deleted_clause}
|
||
"""
|
||
params = {str(i): ids_list[i] for i in range(len(ids_list))}
|
||
rel_exec_db = (database or "").strip() or None
|
||
rows = await rel_adapter.execute_query(
|
||
sql, params, database=rel_exec_db
|
||
)
|
||
id_to_value = {str(row["id"]): row["val"] for row in rows}
|
||
|
||
else:
|
||
if not relation_table:
|
||
continue
|
||
|
||
pb = self._platform_sql_builder
|
||
table_name = pb.build_table_name(relation_table, schema=PLATFORM_SCHEMA)
|
||
key_col = pb.quote_identifier(relation_key)
|
||
val_col = pb.quote_identifier(display_field)
|
||
|
||
placeholders = ", ".join([f":{i}" for i in range(len(ids))])
|
||
ids_list = list(ids)
|
||
sql = f"""
|
||
SELECT {key_col} as id, {val_col} as val
|
||
FROM {table_name}
|
||
WHERE {key_col} IN ({placeholders})
|
||
AND {pb.is_deleted_predicate()}
|
||
"""
|
||
params = {str(i): ids_list[i] for i in range(len(ids_list))}
|
||
async with db.begin_nested():
|
||
rows = await self._execute_platform_query(db, sql, params)
|
||
id_to_value = {str(row["id"]): row["val"] for row in rows}
|
||
|
||
for item in items:
|
||
source_value = item.get(source_field)
|
||
if not source_value:
|
||
item[vf_field] = ""
|
||
continue
|
||
is_list, parsed = self._parse_list_value(source_value)
|
||
if is_list:
|
||
vals = [
|
||
id_to_value.get(str(v), "") for v in parsed if v
|
||
]
|
||
item[vf_field] = ", ".join(str(v) for v in vals if v)
|
||
else:
|
||
item[vf_field] = id_to_value.get(str(source_value), "")
|
||
|
||
except Exception as e:
|
||
logger.warning(f"填充虚拟字段 {vf_field} 失败: {str(e)}")
|
||
continue
|
||
|
||
return items
|
||
|
||
def _get_virtual_fields(self) -> List[Dict[str, str]]:
|
||
"""从 form_config 中提取虚拟字段配置
|
||
|
||
Returns:
|
||
虚拟字段配置列表,每项包含:
|
||
- field: 虚拟字段名
|
||
- valueSourceField: 源字段名
|
||
- valueDisplayField: 要提取的属性名
|
||
"""
|
||
virtual_fields = []
|
||
form_config = self._form_config
|
||
|
||
def traverse(items):
|
||
if not items:
|
||
return
|
||
for item in items:
|
||
item_type = item.get("type", "")
|
||
field = item.get("field", "")
|
||
props = item.get("props", {})
|
||
|
||
# 检查是否为虚拟字段
|
||
if (field and props.get("isVirtualField") and
|
||
props.get("enableValueLink") and
|
||
props.get("valueSourceField") and
|
||
props.get("valueDisplayField")):
|
||
virtual_fields.append({
|
||
"field": field,
|
||
"valueSourceField": props["valueSourceField"],
|
||
"valueDisplayField": props["valueDisplayField"],
|
||
})
|
||
|
||
# 递归处理容器
|
||
if item_type == "grid":
|
||
for col in item.get("columns", []):
|
||
traverse(col.get("children", []))
|
||
elif item_type in ("collapse", "steps"):
|
||
for panel in item.get("items", []):
|
||
traverse(panel.get("children", []))
|
||
elif item_type == "tabs":
|
||
for panel in item.get("children", []):
|
||
traverse(panel.get("children", []))
|
||
elif item_type == "sub-table":
|
||
traverse(item.get("children", []))
|
||
|
||
traverse(form_config.get("items", []))
|
||
return virtual_fields
|
||
|
||
def _infer_relation_config(self, source_field: str) -> Optional[Dict[str, str]]:
|
||
"""从 form_config 中推断源字段的关联表配置
|
||
|
||
当源字段未出现在 list_config.columns 中时,需要从 form_config 的组件类型推断关联信息。
|
||
|
||
Args:
|
||
source_field: 源字段名
|
||
|
||
Returns:
|
||
关联配置字典,包含 relation_table, relation_key, display_column 等,或 None
|
||
"""
|
||
form_config = self._form_config
|
||
|
||
# 递归查找源字段的组件配置
|
||
def find_component(items):
|
||
if not items:
|
||
return None
|
||
for item in items:
|
||
item_type = item.get("type", "")
|
||
field = item.get("field", "")
|
||
if field == source_field:
|
||
return item
|
||
if item_type == "grid":
|
||
for col in item.get("columns", []):
|
||
found = find_component(col.get("children", []))
|
||
if found:
|
||
return found
|
||
elif item_type in ("collapse", "steps"):
|
||
for panel in item.get("items", []):
|
||
found = find_component(panel.get("children", []))
|
||
if found:
|
||
return found
|
||
elif item_type == "tabs":
|
||
for panel in item.get("children", []):
|
||
found = find_component(panel.get("children", []))
|
||
if found:
|
||
return found
|
||
elif item_type == "sub-table":
|
||
found = find_component(item.get("children", []))
|
||
if found:
|
||
return found
|
||
return None
|
||
|
||
component = find_component(form_config.get("items", []))
|
||
if not component:
|
||
return None
|
||
|
||
component_type = component.get("type", "")
|
||
props = component.get("props", {})
|
||
|
||
# 根据组件类型推断关联表
|
||
type_mapping = {
|
||
"department-selector": "core_dept",
|
||
"dept-selector": "core_dept",
|
||
"dept-select": "core_dept",
|
||
"user-selector": "core_user",
|
||
"user-select": "core_user",
|
||
"position-selector": "core_post",
|
||
"post-selector": "core_post",
|
||
"role-selector": "core_role",
|
||
"org-selector": "core_dept",
|
||
"region-selector": "core_region",
|
||
}
|
||
|
||
relation_table = props.get("relationTable") or type_mapping.get(component_type)
|
||
|
||
if relation_table:
|
||
return {
|
||
"relation_table": relation_table,
|
||
"relation_key": props.get("relationKey", "id"),
|
||
"display_column": "name",
|
||
}
|
||
|
||
# form-selector 组件
|
||
if component_type == "form-selector":
|
||
form_selector_config = component.get("formSelectorConfig") or {}
|
||
form_code = form_selector_config.get("formCode", "") or props.get("formCode", "")
|
||
if form_code:
|
||
return {
|
||
"relation_type": "form_data",
|
||
"form_code": form_code,
|
||
"relation_key": form_selector_config.get("valueField", "id") or "id",
|
||
"display_column": form_selector_config.get("labelField", "name") or "name",
|
||
}
|
||
|
||
# select/table-selector 组件,数据源为 formData
|
||
data_source = component.get("dataSource") or {}
|
||
data_source_type = data_source.get("type", "") or props.get("dataSourceType", "")
|
||
form_code = data_source.get("formCode", "") or props.get("formCode", "")
|
||
|
||
if component_type in ["select", "tree-select", "radio", "checkbox", "cascader", "table-selector"] and data_source_type == "formData" and form_code:
|
||
return {
|
||
"relation_type": "form_data",
|
||
"form_code": form_code,
|
||
"relation_key": data_source.get("formValueField") or data_source.get("valueField", "id") or "id",
|
||
"display_column": data_source.get("formLabelField") or data_source.get("labelField", "name") or "name",
|
||
}
|
||
|
||
# 根据字段名推断
|
||
field_mapping = {
|
||
"dept_id": "core_dept",
|
||
"department_id": "core_dept",
|
||
"user_id": "core_user",
|
||
"manger_id": "core_user",
|
||
"manager_id": "core_user",
|
||
"position_id": "core_post",
|
||
"positon_id": "core_post",
|
||
}
|
||
relation_table = field_mapping.get(source_field)
|
||
if relation_table:
|
||
return {
|
||
"relation_table": relation_table,
|
||
"relation_key": "id",
|
||
"display_column": "name",
|
||
}
|
||
|
||
return None
|
||
|
||
def _get_non_virtual_linked_fields(self) -> List[Dict[str, str]]:
|
||
"""从 form_config 中提取非虚拟的值关联字段配置
|
||
|
||
非虚拟关联字段(enableValueLink=true, isVirtualField=false 或未设置)
|
||
对应数据库列,需要在保存时将关联值写入数据库。
|
||
|
||
Returns:
|
||
配置列表,每项包含:
|
||
- field: 字段名(对应数据库列)
|
||
- valueSourceField: 源字段名
|
||
- valueDisplayField: 要从关联数据中提取的属性名
|
||
"""
|
||
linked_fields = []
|
||
form_config = self._form_config
|
||
|
||
def traverse(items):
|
||
if not items:
|
||
return
|
||
for item in items:
|
||
item_type = item.get("type", "")
|
||
field = item.get("field", "")
|
||
props = item.get("props", {})
|
||
|
||
if (field and not props.get("isVirtualField")
|
||
and props.get("enableValueLink")
|
||
and props.get("valueSourceField")
|
||
and props.get("valueDisplayField")):
|
||
linked_fields.append({
|
||
"field": field,
|
||
"valueSourceField": props["valueSourceField"],
|
||
"valueDisplayField": props["valueDisplayField"],
|
||
})
|
||
|
||
if item_type == "grid":
|
||
for col in item.get("columns", []):
|
||
traverse(col.get("children", []))
|
||
elif item_type in ("collapse", "steps"):
|
||
for panel in item.get("items", []):
|
||
traverse(panel.get("children", []))
|
||
elif item_type == "tabs":
|
||
for panel in item.get("children", []):
|
||
traverse(panel.get("children", []))
|
||
elif item_type == "sub-table":
|
||
traverse(item.get("children", []))
|
||
|
||
traverse(form_config.get("items", []))
|
||
return linked_fields
|
||
|
||
async def _fill_linked_field_values(
|
||
self,
|
||
db: AsyncSession,
|
||
data: Dict[str, Any],
|
||
table_type: str = "main"
|
||
) -> Dict[str, Any]:
|
||
"""保存前填充非虚拟关联字段的值
|
||
|
||
根据值关联配置,从源字段的值反查关联表,将结果写入对应的数据库列。
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
data: 待保存的数据
|
||
table_type: "main" 或 "sub"
|
||
"""
|
||
linked_fields = self._get_non_virtual_linked_fields()
|
||
if not linked_fields:
|
||
return data
|
||
|
||
relation_fields = self._get_relation_fields()
|
||
|
||
for lf in linked_fields:
|
||
target_field = lf["field"]
|
||
source_field = lf["valueSourceField"]
|
||
display_field = lf["valueDisplayField"]
|
||
|
||
source_value = data.get(source_field)
|
||
if not source_value:
|
||
continue
|
||
|
||
source_config = relation_fields.get(source_field)
|
||
if not source_config:
|
||
source_config = self._infer_relation_config(source_field)
|
||
if not source_config:
|
||
logger.warning(f"非虚拟关联字段 {target_field} 的源字段 {source_field} 没有关联配置,跳过")
|
||
continue
|
||
|
||
relation_type = source_config.get("relation_type", "")
|
||
relation_table = source_config.get("relation_table", "")
|
||
relation_key = source_config.get("relation_key", "id")
|
||
|
||
if relation_table == "core_region":
|
||
continue
|
||
|
||
ids = set()
|
||
if isinstance(source_value, list):
|
||
ids.update(str(v) for v in source_value if v)
|
||
else:
|
||
ids.add(str(source_value))
|
||
|
||
if not ids:
|
||
continue
|
||
|
||
try:
|
||
id_to_value = {}
|
||
|
||
if relation_type == "form_data":
|
||
form_code = source_config.get("form_code")
|
||
if not form_code:
|
||
continue
|
||
|
||
from online_dev.form_manager.model import FormMeta as RelatedFormMeta
|
||
stmt = select(RelatedFormMeta).where(
|
||
RelatedFormMeta.code == form_code,
|
||
RelatedFormMeta.is_deleted == False
|
||
)
|
||
result = await db.execute(stmt)
|
||
related_form = result.scalar_one_or_none()
|
||
|
||
if not related_form:
|
||
logger.warning(f"关联字段 {target_field} 的关联表单 {form_code} 不存在")
|
||
continue
|
||
|
||
rel_adapter, rel_builder = await resolve_form_sql_context(
|
||
db,
|
||
related_form,
|
||
adapter_cache=self._adapter_cache,
|
||
builder_cache=self._sql_builder_cache,
|
||
)
|
||
table = related_form.main_table
|
||
schema = await self._resolve_schema(db, related_form.main_table_schema) or None
|
||
database = related_form.main_table_database or None
|
||
table_name = rel_builder.build_table_name(table, schema, database)
|
||
key_col = rel_builder.quote_identifier(relation_key)
|
||
val_col = rel_builder.quote_identifier(display_field)
|
||
|
||
has_is_deleted = await self._check_column_exists(
|
||
db,
|
||
table,
|
||
related_form.main_table_schema,
|
||
related_form.main_table_database,
|
||
"is_deleted",
|
||
sql_builder=rel_builder,
|
||
db_adapter=rel_adapter,
|
||
)
|
||
|
||
placeholders = ", ".join([f":{i}" for i in range(len(ids))])
|
||
ids_list = list(ids)
|
||
is_deleted_clause = (
|
||
f"\n AND {rel_builder.is_deleted_predicate()}"
|
||
if has_is_deleted
|
||
else ""
|
||
)
|
||
sql = f"""
|
||
SELECT {key_col} as id, {val_col} as val
|
||
FROM {table_name}
|
||
WHERE {key_col} IN ({placeholders}){is_deleted_clause}
|
||
"""
|
||
params = {str(i): ids_list[i] for i in range(len(ids_list))}
|
||
rel_exec_db = (database or "").strip() or None
|
||
rows = await rel_adapter.execute_query(
|
||
sql, params, database=rel_exec_db
|
||
)
|
||
id_to_value = {str(row["id"]): row["val"] for row in rows}
|
||
|
||
else:
|
||
if not relation_table:
|
||
continue
|
||
|
||
pb = self._platform_sql_builder
|
||
table_name = pb.build_table_name(relation_table, schema=PLATFORM_SCHEMA)
|
||
key_col = pb.quote_identifier(relation_key)
|
||
val_col = pb.quote_identifier(display_field)
|
||
|
||
placeholders = ", ".join([f":{i}" for i in range(len(ids))])
|
||
ids_list = list(ids)
|
||
sql = f"""
|
||
SELECT {key_col} as id, {val_col} as val
|
||
FROM {table_name}
|
||
WHERE {key_col} IN ({placeholders})
|
||
AND {pb.is_deleted_predicate()}
|
||
"""
|
||
params = {str(i): ids_list[i] for i in range(len(ids_list))}
|
||
rows = await self._execute_platform_query(db, sql, params)
|
||
id_to_value = {str(row["id"]): row["val"] for row in rows}
|
||
|
||
if id_to_value:
|
||
if isinstance(source_value, list):
|
||
vals = [id_to_value.get(str(v), "") for v in source_value if v]
|
||
data[target_field] = ", ".join(str(v) for v in vals if v)
|
||
else:
|
||
resolved = id_to_value.get(str(source_value), "")
|
||
if resolved:
|
||
data[target_field] = resolved
|
||
|
||
logger.debug(f"填充非虚拟关联字段 {target_field}: source={source_field}, value={data.get(target_field)}")
|
||
|
||
except Exception as e:
|
||
logger.warning(f"填充非虚拟关联字段 {target_field} 失败: {str(e)}")
|
||
continue
|
||
|
||
return data
|
||
|
||
async def _fill_form_data_display_field(
|
||
self,
|
||
db: AsyncSession,
|
||
items: List[Dict[str, Any]],
|
||
field: str,
|
||
config: Dict[str, Any]
|
||
) -> None:
|
||
"""填充表单数据选择器的显示字段
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
items: 数据列表
|
||
field: ID 字段名(如 customer_id)
|
||
config: 配置信息,包含 form_code, display_field, display_column, relation_key
|
||
"""
|
||
form_code = config.get("form_code")
|
||
display_field = config.get("display_field")
|
||
display_column = config.get("display_column", "name")
|
||
relation_key = config.get("relation_key", "id")
|
||
|
||
if not form_code or not display_field:
|
||
return
|
||
|
||
ids = self._collect_field_ids(items, field)
|
||
if not ids:
|
||
return
|
||
|
||
try:
|
||
# 获取关联表单的元数据
|
||
from online_dev.form_manager.model import FormMeta as RelatedFormMeta
|
||
from sqlalchemy import select
|
||
|
||
stmt = select(RelatedFormMeta).where(
|
||
RelatedFormMeta.code == form_code,
|
||
RelatedFormMeta.is_deleted == False
|
||
)
|
||
result = await db.execute(stmt)
|
||
related_form = result.scalar_one_or_none()
|
||
|
||
if not related_form:
|
||
logger.warning(f"关联表单 {form_code} 不存在")
|
||
return
|
||
|
||
# 获取关联表单的表信息
|
||
table = related_form.main_table
|
||
schema = await self._resolve_schema(db, related_form.main_table_schema) or None
|
||
database = related_form.main_table_database or None
|
||
|
||
rel_adapter, rel_builder = await resolve_form_sql_context(
|
||
db,
|
||
related_form,
|
||
adapter_cache=self._adapter_cache,
|
||
builder_cache=self._sql_builder_cache,
|
||
)
|
||
table_name = rel_builder.build_table_name(table, schema, database)
|
||
key_col = rel_builder.quote_identifier(relation_key)
|
||
display_col = rel_builder.quote_identifier(display_column)
|
||
|
||
has_is_deleted = await self._check_column_exists(
|
||
db,
|
||
table,
|
||
related_form.main_table_schema,
|
||
related_form.main_table_database,
|
||
"is_deleted",
|
||
sql_builder=rel_builder,
|
||
db_adapter=rel_adapter,
|
||
)
|
||
|
||
placeholders = ", ".join([f":{i}" for i in range(len(ids))])
|
||
ids_list = list(ids)
|
||
|
||
is_deleted_clause = (
|
||
f"\n AND {rel_builder.is_deleted_predicate()}"
|
||
if has_is_deleted
|
||
else ""
|
||
)
|
||
sql = f"""
|
||
SELECT {key_col} as id, {display_col} as name
|
||
FROM {table_name}
|
||
WHERE {key_col} IN ({placeholders}){is_deleted_clause}
|
||
"""
|
||
|
||
params = {str(i): ids_list[i] for i in range(len(ids_list))}
|
||
rel_exec_db = (database or "").strip() or None
|
||
rows = await rel_adapter.execute_query(sql, params, database=rel_exec_db)
|
||
logger.debug(f"查询表单数据 {form_code}.{table},找到 {len(rows)} 条记录")
|
||
|
||
# 构建 ID 到名称的映射
|
||
id_to_name = {str(row["id"]): row["name"] for row in rows}
|
||
|
||
for item in items:
|
||
self._set_display_from_id_map(item, field, display_field, id_to_name)
|
||
|
||
except Exception as e:
|
||
logger.error(f"填充表单数据字段 {field} 失败: {str(e)}")
|
||
raise
|
||
|
||
async def _query_region_names(
|
||
self,
|
||
db: AsyncSession,
|
||
codes: List[str]
|
||
) -> Dict[str, str]:
|
||
"""批量查询省市区名称
|
||
|
||
省市区数据分散在 5 个表中:
|
||
- core_province: 2位代码(如 "14")
|
||
- core_city: 4位代码(如 "1404")
|
||
- core_area: 6位代码(如 "140406")
|
||
- core_street: 9位代码(如 "140406001")
|
||
- core_village: 12位代码(如 "140406001001")
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
codes: 行政区划代码列表
|
||
|
||
Returns:
|
||
code 到 name 的映射字典
|
||
"""
|
||
if not codes:
|
||
return {}
|
||
|
||
result = {}
|
||
|
||
try:
|
||
# 按代码长度分组
|
||
codes_by_length = {}
|
||
for code in codes:
|
||
length = len(str(code))
|
||
if length not in codes_by_length:
|
||
codes_by_length[length] = []
|
||
codes_by_length[length].append(str(code))
|
||
|
||
# 定义表名映射(代码长度 -> 表名)
|
||
table_mapping = {
|
||
2: "core_province",
|
||
4: "core_city",
|
||
6: "core_area",
|
||
9: "core_street",
|
||
12: "core_village",
|
||
}
|
||
|
||
# 分别查询每个表
|
||
for length, code_list in codes_by_length.items():
|
||
table_name = table_mapping.get(length)
|
||
if not table_name:
|
||
logger.warning(f"未知的代码长度: {length},代码: {code_list}")
|
||
continue
|
||
|
||
pb = self._platform_sql_builder
|
||
full_table_name = pb.build_table_name(table_name, schema=PLATFORM_SCHEMA)
|
||
code_col = pb.quote_identifier("code")
|
||
name_col = pb.quote_identifier("name")
|
||
|
||
# 使用参数化查询
|
||
placeholders = ", ".join([f":{i}" for i in range(len(code_list))])
|
||
|
||
sql = f"""
|
||
SELECT {code_col} as code, {name_col} as name
|
||
FROM {full_table_name}
|
||
WHERE {code_col} IN ({placeholders})
|
||
"""
|
||
|
||
# 构建参数字典
|
||
params = {str(i): code_list[i] for i in range(len(code_list))}
|
||
|
||
rows = await self._execute_platform_query(db, sql, params)
|
||
logger.info(f"查询 {table_name},找到 {len(rows)} 条记录")
|
||
|
||
# 添加到结果映射
|
||
for row in rows:
|
||
result[str(row["code"])] = row["name"]
|
||
|
||
return result
|
||
except Exception as e:
|
||
logger.error(f"查询省市区名称失败: {str(e)}")
|
||
return {}
|
||
|
||
async def _query_relation_data(
|
||
self,
|
||
db: AsyncSession,
|
||
table: str,
|
||
key_column: str,
|
||
display_column: str,
|
||
ids: List[str]
|
||
) -> List[Dict[str, Any]]:
|
||
"""批量查询关联表数据"""
|
||
if not ids:
|
||
return []
|
||
|
||
pb = self._platform_sql_builder
|
||
table_name = pb.build_table_name(table, schema=PLATFORM_SCHEMA)
|
||
key_col = pb.quote_identifier(key_column)
|
||
display_col = pb.quote_identifier(display_column)
|
||
|
||
placeholders = ", ".join([f":{i}" for i in range(len(ids))])
|
||
|
||
sql = f"""
|
||
SELECT {key_col} as id, {display_col} as name
|
||
FROM {table_name}
|
||
WHERE {key_col} IN ({placeholders})
|
||
AND {pb.is_deleted_predicate()}
|
||
"""
|
||
|
||
params = {str(i): ids[i] for i in range(len(ids))}
|
||
|
||
try:
|
||
logger.info(f"查询关联表 {table},SQL: {sql}, params: {params}")
|
||
rows = await self._execute_platform_query(db, sql, params)
|
||
logger.info(f"查询关联表 {table},找到 {len(rows)} 条记录,结果: {rows}")
|
||
return [dict(row) for row in rows]
|
||
except Exception as e:
|
||
logger.error(f"查询关联表 {table} 失败: {str(e)}, SQL: {sql}, params: {params}")
|
||
return []
|
||
|
||
# ============ 写入操作 ============
|
||
|
||
def _convert_data_types(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""转换数据类型,将字符串日期时间转换为 Python datetime 对象,处理空字符串"""
|
||
converted = {}
|
||
|
||
# 获取字段类型映射(用于判断数字类型字段)
|
||
field_types = self._get_field_types()
|
||
|
||
# 调试日志
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
# logger.info(f"字段类型映射: {field_types}")
|
||
# logger.info(f"待转换数据: {data}")
|
||
|
||
for key, value in data.items():
|
||
if value is None:
|
||
converted[key] = value
|
||
continue
|
||
|
||
# 处理空字符串
|
||
if isinstance(value, str) and value.strip() == '':
|
||
# 获取字段类型
|
||
field_type = field_types.get(key, '').lower()
|
||
# 数字类型字段的空字符串转为 None
|
||
if field_type in ('int', 'integer', 'bigint', 'smallint', 'decimal', 'numeric', 'float', 'double', 'real'):
|
||
converted[key] = None
|
||
continue
|
||
# 其他类型保持空字符串或转为 None
|
||
converted[key] = None
|
||
continue
|
||
|
||
# 获取字段类型
|
||
field_type = field_types.get(key, '').lower()
|
||
|
||
# 处理非字符串类型到字符串的转换
|
||
# 支持 PostgreSQL 的类型名:character varying, character, text 等
|
||
is_string_type = any(t in field_type for t in ['varchar', 'text', 'char', 'string'])
|
||
if not isinstance(value, str) and is_string_type:
|
||
if isinstance(value, datetime):
|
||
if value.tzinfo is not None:
|
||
value = value.astimezone(APP_TIMEZONE).replace(tzinfo=None)
|
||
converted[key] = value.isoformat()
|
||
logger.info(f"字段 {key} 从 datetime 转换为 str: {value} -> {converted[key]}")
|
||
elif isinstance(value, (list, dict)):
|
||
converted[key] = json.dumps(value, ensure_ascii=False)
|
||
logger.info(f"字段 {key} 从 {type(value).__name__} 序列化为 JSON: {converted[key]}")
|
||
else:
|
||
converted[key] = str(value)
|
||
logger.info(f"字段 {key} 从 {type(value).__name__} 转换为 str: {value} -> {str(value)}")
|
||
continue
|
||
|
||
# 处理已经是 datetime 对象的情况
|
||
if isinstance(value, datetime):
|
||
# 如果是 offset-aware datetime,先转换为配置的时区再移除时区信息
|
||
if value.tzinfo is not None:
|
||
value = value.astimezone(APP_TIMEZONE).replace(tzinfo=None)
|
||
|
||
# 检查字段类型是否是 timestamp/datetime 类型
|
||
is_datetime_type = any(t in field_type for t in ('date', 'datetime', 'timestamp', 'time'))
|
||
if is_datetime_type:
|
||
# 保持为 datetime 对象
|
||
converted[key] = value
|
||
else:
|
||
# 字段类型未知或是字符串类型,转换为 ISO 格式字符串
|
||
converted[key] = value.isoformat()
|
||
logger.info(f"字段 {key} 从 datetime 转换为 str (字段类型: {field_type}): {value} -> {converted[key]}")
|
||
continue
|
||
|
||
# 转换字符串类型的值
|
||
if isinstance(value, str):
|
||
|
||
# 1. 尝试转换布尔类型
|
||
if field_type in ('bool', 'boolean'):
|
||
# 支持多种布尔值表示
|
||
value_lower = value.lower().strip()
|
||
if value_lower in ('true', '1', 'yes', 'y', 't', '是', '真'):
|
||
converted[key] = True
|
||
continue
|
||
elif value_lower in ('false', '0', 'no', 'n', 'f', '否', '假'):
|
||
converted[key] = False
|
||
continue
|
||
# 其他值保持原样
|
||
|
||
# 2. 尝试转换整数类型
|
||
if field_type in ('int', 'integer', 'bigint', 'smallint'):
|
||
try:
|
||
# 先去除空格,支持 "25 " 这样的输入
|
||
converted[key] = int(value.strip())
|
||
continue
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
# 3. 尝试转换浮点数类型
|
||
elif field_type in ('decimal', 'numeric', 'float', 'double', 'real'):
|
||
try:
|
||
converted[key] = float(value.strip())
|
||
continue
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
# 4. 尝试解析日期时间(支持 'timestamp without time zone' 等完整类型名)
|
||
is_datetime_type = any(t in field_type for t in ('date', 'datetime', 'timestamp', 'time'))
|
||
if is_datetime_type:
|
||
try:
|
||
from datetime import time as time_type
|
||
|
||
# 时间格式
|
||
if field_type == 'time':
|
||
for fmt in ["%H:%M:%S", "%H:%M:%S.%f", "%H:%M"]:
|
||
try:
|
||
t = datetime.strptime(value, fmt).time()
|
||
converted[key] = t
|
||
break
|
||
except ValueError:
|
||
continue
|
||
else:
|
||
converted[key] = value
|
||
continue
|
||
|
||
# 先尝试使用 fromisoformat 解析(支持更多 ISO 格式)
|
||
parsed_dt = None
|
||
try:
|
||
# 处理 Z 后缀(UTC 时区标识)
|
||
iso_value = value.replace('Z', '+00:00') if value.endswith('Z') else value
|
||
parsed_dt = datetime.fromisoformat(iso_value)
|
||
except ValueError:
|
||
# fromisoformat 失败,尝试其他格式
|
||
for fmt in [
|
||
"%Y-%m-%d %H:%M:%S",
|
||
"%Y-%m-%d %H:%M:%S.%f",
|
||
"%Y-%m-%dT%H:%M:%S",
|
||
"%Y-%m-%dT%H:%M:%S.%f",
|
||
"%Y-%m-%d",
|
||
]:
|
||
try:
|
||
parsed_dt = datetime.strptime(value, fmt)
|
||
break
|
||
except ValueError:
|
||
continue
|
||
|
||
if parsed_dt is not None:
|
||
# 如果是日期格式(没有时间部分),只保留日期
|
||
if len(value) == 10 and '-' in value:
|
||
converted[key] = parsed_dt.date()
|
||
else:
|
||
# 将 UTC 时间转换为本地时间后再移除时区信息
|
||
if parsed_dt.tzinfo is not None:
|
||
# 转换为配置的时区
|
||
parsed_dt = parsed_dt.astimezone(APP_TIMEZONE)
|
||
# 移除时区信息
|
||
parsed_dt = parsed_dt.replace(tzinfo=None)
|
||
converted[key] = parsed_dt
|
||
else:
|
||
# 无法解析,保持原值
|
||
converted[key] = value
|
||
continue
|
||
except Exception:
|
||
converted[key] = value
|
||
continue
|
||
|
||
# 5. 尝试解析 JSON 类型(json, jsonb)
|
||
if field_type in ('json', 'jsonb'):
|
||
try:
|
||
converted[key] = json.loads(value)
|
||
continue
|
||
except (json.JSONDecodeError, TypeError):
|
||
# 如果解析失败,保持原字符串
|
||
pass
|
||
|
||
# 6. 数组类型(PostgreSQL array)
|
||
if field_type.endswith('[]') or field_type in ('array', 'text[]', 'varchar[]', 'integer[]'):
|
||
try:
|
||
parsed = json.loads(value)
|
||
if isinstance(parsed, list):
|
||
converted[key] = parsed
|
||
continue
|
||
except (json.JSONDecodeError, TypeError):
|
||
# 尝试按逗号分隔
|
||
if ',' in value:
|
||
converted[key] = [item.strip() for item in value.split(',')]
|
||
continue
|
||
|
||
# 默认保持原值
|
||
converted[key] = value
|
||
else:
|
||
converted[key] = value
|
||
|
||
return converted
|
||
|
||
def _get_field_types(self) -> Dict[str, str]:
|
||
"""获取字段名到类型的映射"""
|
||
field_types = {}
|
||
|
||
# 从 form_config 中获取字段类型
|
||
form_config = self._form_config
|
||
table_configs = form_config.get('tableConfigs', [])
|
||
|
||
for table_config in table_configs:
|
||
fields = table_config.get('fields', [])
|
||
for field in fields:
|
||
field_name = field.get('name', '')
|
||
field_type = field.get('type', '')
|
||
if field_name and field_type:
|
||
field_types[field_name] = field_type
|
||
|
||
return field_types
|
||
|
||
async def create(self, db: AsyncSession, data: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""新增数据(含子表,事务)"""
|
||
main_data = data.get("main") or {}
|
||
sub_tables_data = data.get("sub_tables") or {}
|
||
field_perms = await self._get_merged_field_permissions(db)
|
||
|
||
# 过滤主表字段
|
||
allowed_main_fields = self._get_allowed_fields("main")
|
||
filtered_main = self._filter_writable_fields(main_data, allowed_main_fields, field_perms)
|
||
|
||
# 转换数据类型
|
||
filtered_main = self._convert_data_types(filtered_main)
|
||
|
||
# 移除 id 字段,生成新的 UUID
|
||
filtered_main.pop("id", None)
|
||
generated_id = str(uuid.uuid4())
|
||
filtered_main["id"] = generated_id
|
||
|
||
# 填充系统字段(创建时间、创建人、部门等)
|
||
filtered_main = self._fill_system_fields_for_create(filtered_main)
|
||
|
||
# 填充非虚拟关联字段的值(从源字段反查关联表写入数据库列)
|
||
filtered_main = await self._fill_linked_field_values(db, filtered_main, table_type="main")
|
||
|
||
if not filtered_main:
|
||
raise FormDataValidationError("主表数据不能为空")
|
||
|
||
# 唯一性校验(新增时不需要排除ID)
|
||
await self._validate_unique_fields(db, filtered_main)
|
||
|
||
# 1. 插入主表
|
||
table = self.form_meta.main_table
|
||
schema = await self._resolve_schema(db, self.form_meta.main_table_schema) or None
|
||
database = self.form_meta.main_table_database or None
|
||
|
||
sql, params = self.sql_builder.build_insert(
|
||
table=table,
|
||
data=filtered_main,
|
||
schema=schema,
|
||
database=database,
|
||
return_id=False
|
||
)
|
||
|
||
main_pk = generated_id
|
||
|
||
async with self._business_transaction(database=self._table_exec_database(database)):
|
||
await self._execute_command(
|
||
db, sql, params, database=self._table_exec_database(database)
|
||
)
|
||
|
||
for sub_table in self.sub_tables:
|
||
sub_data_list = sub_tables_data.get(sub_table.table_name, [])
|
||
if not sub_data_list:
|
||
continue
|
||
|
||
allowed_sub_fields = self._get_allowed_fields("sub", sub_table.table_name)
|
||
|
||
for sub_item in sub_data_list:
|
||
filtered_sub = self._filter_writable_fields(
|
||
sub_item,
|
||
allowed_sub_fields,
|
||
field_perms,
|
||
always_allow={sub_table.foreign_key},
|
||
)
|
||
filtered_sub = self._convert_data_types(filtered_sub)
|
||
filtered_sub.pop("id", None)
|
||
filtered_sub["id"] = str(uuid.uuid4())
|
||
filtered_sub[sub_table.foreign_key] = main_pk
|
||
filtered_sub = self._fill_system_fields_for_create(filtered_sub)
|
||
filtered_sub = await self._fill_linked_field_values(
|
||
db, filtered_sub, table_type="sub"
|
||
)
|
||
|
||
if filtered_sub:
|
||
sub_sql, sub_params = self.sql_builder.build_insert(
|
||
table=sub_table.table_name,
|
||
data=filtered_sub,
|
||
schema=sub_table.table_schema or None,
|
||
database=sub_table.table_database or None,
|
||
return_id=False,
|
||
)
|
||
sub_db = sub_table.table_database or None
|
||
await self._execute_command(
|
||
db,
|
||
sub_sql,
|
||
sub_params,
|
||
database=self._table_exec_database(sub_db),
|
||
)
|
||
|
||
await db.commit()
|
||
|
||
logger.info(f"表单数据创建成功: form={self._form_code}, pk={main_pk}")
|
||
|
||
return await self.get(db, main_pk)
|
||
|
||
async def update(self, db: AsyncSession, pk: Any, data: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""更新数据(含子表,事务)"""
|
||
main_data = data.get("main") or {}
|
||
sub_tables_data = data.get("sub_tables") or {}
|
||
field_perms = await self._get_merged_field_permissions(db)
|
||
|
||
await self.get(db, pk)
|
||
|
||
main_schema_db = self.form_meta.main_table_database or None
|
||
async with self._business_transaction(
|
||
database=self._table_exec_database(main_schema_db)
|
||
):
|
||
if main_data:
|
||
allowed_main_fields = self._get_allowed_fields("main")
|
||
filtered_main = self._filter_writable_fields(
|
||
main_data, allowed_main_fields, field_perms
|
||
)
|
||
filtered_main = self._convert_data_types(filtered_main)
|
||
filtered_main.pop("id", None)
|
||
filtered_main = self._fill_system_fields_for_update(filtered_main)
|
||
filtered_main = await self._fill_linked_field_values(
|
||
db, filtered_main, table_type="main"
|
||
)
|
||
await self._validate_unique_fields(db, filtered_main, exclude_id=str(pk))
|
||
|
||
if filtered_main:
|
||
table = self.form_meta.main_table
|
||
schema = await self._resolve_schema(
|
||
db, self.form_meta.main_table_schema
|
||
) or None
|
||
database = self.form_meta.main_table_database or None
|
||
sql, params = self.sql_builder.build_update(
|
||
table=table,
|
||
data=filtered_main,
|
||
pk_field="id",
|
||
pk_value=pk,
|
||
schema=schema,
|
||
database=database,
|
||
)
|
||
await self._execute_command(
|
||
db,
|
||
sql,
|
||
params,
|
||
database=self._table_exec_database(database),
|
||
)
|
||
|
||
for sub_table in self.sub_tables:
|
||
if sub_table.table_name not in sub_tables_data:
|
||
continue
|
||
new_sub_data = sub_tables_data[sub_table.table_name]
|
||
await self._handle_sub_table_update(
|
||
db, sub_table, pk, new_sub_data, field_perms
|
||
)
|
||
|
||
await db.commit()
|
||
|
||
logger.info(f"表单数据更新成功: form={self._form_code}, pk={pk}")
|
||
|
||
return await self.get(db, pk)
|
||
|
||
async def _handle_sub_table_update(
|
||
self,
|
||
db: AsyncSession,
|
||
sub_table: FormSubTable,
|
||
main_pk: Any,
|
||
new_data: List[Dict[str, Any]],
|
||
field_perms: Optional[Dict[str, Dict]] = None,
|
||
):
|
||
"""处理子表更新(差异对比:新增/更新/删除)"""
|
||
table_name = sub_table.table_name
|
||
schema = sub_table.table_schema or None
|
||
database = sub_table.table_database or None
|
||
foreign_key = sub_table.foreign_key
|
||
|
||
# 获取现有数据
|
||
existing = await self._query_sub_table_data(db, sub_table, main_pk)
|
||
existing_map = {item["id"]: item for item in existing if "id" in item}
|
||
|
||
allowed_fields = self._get_allowed_fields("sub", table_name)
|
||
field_perms = field_perms or {}
|
||
writable_allow = {foreign_key}
|
||
|
||
# 分类处理
|
||
new_ids = set()
|
||
to_insert = []
|
||
to_update = []
|
||
|
||
for item in new_data:
|
||
item_id = item.get("id")
|
||
if item_id and item_id in existing_map:
|
||
new_ids.add(item_id)
|
||
to_update.append(item)
|
||
elif not item_id:
|
||
to_insert.append(item)
|
||
else:
|
||
to_insert.append(item)
|
||
|
||
# 找出需要删除的
|
||
to_delete = [eid for eid in existing_map.keys() if eid not in new_ids]
|
||
|
||
# 执行删除
|
||
for del_id in to_delete:
|
||
sql, params = self.sql_builder.build_delete(
|
||
table=table_name,
|
||
pk_field="id",
|
||
pk_value=del_id,
|
||
schema=schema,
|
||
database=database
|
||
)
|
||
await self._execute_command(
|
||
db, sql, params, database=self._table_exec_database(database)
|
||
)
|
||
|
||
# 执行更新
|
||
for item in to_update:
|
||
filtered = self._filter_writable_fields(item, allowed_fields, field_perms)
|
||
filtered = self._convert_data_types(filtered)
|
||
item_id = filtered.pop("id", None)
|
||
|
||
# 填充子表系统字段(更新)
|
||
filtered = self._fill_system_fields_for_update(filtered)
|
||
|
||
# 填充子表非虚拟关联字段的值
|
||
filtered = await self._fill_linked_field_values(db, filtered, table_type="sub")
|
||
|
||
if filtered and item_id:
|
||
sql, params = self.sql_builder.build_update(
|
||
table=table_name,
|
||
data=filtered,
|
||
pk_field="id",
|
||
pk_value=item_id,
|
||
schema=schema,
|
||
database=database
|
||
)
|
||
await self._execute_command(
|
||
db, sql, params, database=self._table_exec_database(database)
|
||
)
|
||
|
||
# 执行新增
|
||
for item in to_insert:
|
||
filtered = self._filter_writable_fields(
|
||
item, allowed_fields, field_perms, always_allow=writable_allow
|
||
)
|
||
filtered = self._convert_data_types(filtered)
|
||
if "id" in filtered:
|
||
del filtered["id"]
|
||
filtered["id"] = str(uuid.uuid4())
|
||
if foreign_key in filtered:
|
||
del filtered[foreign_key]
|
||
filtered[foreign_key] = main_pk
|
||
|
||
# 填充子表系统字段(新增)
|
||
filtered = self._fill_system_fields_for_create(filtered)
|
||
|
||
# 填充子表非虚拟关联字段的值
|
||
filtered = await self._fill_linked_field_values(db, filtered, table_type="sub")
|
||
|
||
if filtered and len(filtered) > 2:
|
||
sql, params = self.sql_builder.build_insert(
|
||
table=table_name,
|
||
data=filtered,
|
||
schema=schema,
|
||
database=database,
|
||
return_id=False
|
||
)
|
||
await self._execute_command(
|
||
db, sql, params, database=self._table_exec_database(database)
|
||
)
|
||
|
||
async def delete(self, db: AsyncSession, pk: Any) -> bool:
|
||
"""删除数据(含子表,事务)"""
|
||
# 验证数据存在(不存在时 get 会抛出 RecordNotFoundException)
|
||
await self.get(db, pk)
|
||
|
||
main_database = self.form_meta.main_table_database or None
|
||
async with self._business_transaction(
|
||
database=self._table_exec_database(main_database)
|
||
):
|
||
for sub_table in self.sub_tables:
|
||
sub_db = sub_table.table_database or None
|
||
sql, params = self.sql_builder.build_delete_by_foreign_key(
|
||
table=sub_table.table_name,
|
||
fk_field=sub_table.foreign_key,
|
||
fk_value=pk,
|
||
schema=sub_table.table_schema or None,
|
||
database=sub_db,
|
||
)
|
||
await self._execute_command(
|
||
db,
|
||
sql,
|
||
params,
|
||
database=self._table_exec_database(sub_db),
|
||
)
|
||
|
||
table = self.form_meta.main_table
|
||
schema = await self._resolve_schema(db, self.form_meta.main_table_schema) or None
|
||
database = self.form_meta.main_table_database or None
|
||
sql, params = self.sql_builder.build_delete(
|
||
table=table,
|
||
pk_field="id",
|
||
pk_value=pk,
|
||
schema=schema,
|
||
database=database,
|
||
)
|
||
affected = await self._execute_command(
|
||
db, sql, params, database=self._table_exec_database(database)
|
||
)
|
||
|
||
await db.commit()
|
||
|
||
logger.info(f"表单数据删除成功: form={self._form_code}, pk={pk}")
|
||
|
||
return affected > 0
|
||
|
||
async def batch_delete(self, db: AsyncSession, pks: List[Any]) -> int:
|
||
"""
|
||
批量删除(优化版:使用 IN 子句批量删除,单次事务提交)
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
pks: 主键列表
|
||
|
||
Returns:
|
||
成功删除的数量
|
||
"""
|
||
if not pks:
|
||
return 0
|
||
|
||
async with self._business_transaction():
|
||
affected = await self._batch_delete_in_transaction(db, pks)
|
||
|
||
await db.commit()
|
||
logger.info(f"批量删除成功: form={self._form_code}, count={affected}")
|
||
return affected
|
||
|
||
async def _batch_delete_in_transaction(self, db: AsyncSession, pks: List[Any]) -> int:
|
||
for sub_table in self.sub_tables:
|
||
schema = await self._resolve_schema(db, sub_table.table_schema) if sub_table.table_schema else None
|
||
database = sub_table.table_database or None
|
||
|
||
# 构建批量删除 SQL: DELETE FROM table WHERE fk_field IN (:pk0, :pk1, ...)
|
||
full_table = self.sql_builder.build_table_name(sub_table.table_name, schema, database)
|
||
fk_field = self.sql_builder.quote_identifier(sub_table.foreign_key)
|
||
|
||
placeholders = ", ".join(f":pk{i}" for i in range(len(pks)))
|
||
sql = f"DELETE FROM {full_table} WHERE {fk_field} IN ({placeholders})"
|
||
params = {f"pk{i}": pk for i, pk in enumerate(pks)}
|
||
|
||
await self._execute_command(
|
||
db, sql, params, database=self._table_exec_database(database)
|
||
)
|
||
|
||
# 2. 批量删除主表数据(使用 IN 子句)
|
||
table = self.form_meta.main_table
|
||
schema = await self._resolve_schema(db, self.form_meta.main_table_schema) or None
|
||
database = self.form_meta.main_table_database or None
|
||
|
||
full_table = self.sql_builder.build_table_name(table, schema, database)
|
||
id_field = self.sql_builder.quote_identifier("id")
|
||
|
||
placeholders = ", ".join(f":pk{i}" for i in range(len(pks)))
|
||
sql = f"DELETE FROM {full_table} WHERE {id_field} IN ({placeholders})"
|
||
params = {f"pk{i}": pk for i, pk in enumerate(pks)}
|
||
|
||
return await self._execute_command(
|
||
db, sql, params, database=self._table_exec_database(database)
|
||
)
|
||
|
||
# ============ 工具方法 ============
|
||
|
||
def _serialize_row(self, row: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""序列化行数据(处理特殊类型)"""
|
||
result = {}
|
||
for key, value in row.items():
|
||
if isinstance(value, datetime):
|
||
result[key] = value.strftime("%Y-%m-%d %H:%M:%S")
|
||
elif isinstance(value, date):
|
||
result[key] = value.strftime("%Y-%m-%d")
|
||
elif isinstance(value, Decimal):
|
||
result[key] = float(value)
|
||
elif isinstance(value, uuid.UUID):
|
||
result[key] = str(value)
|
||
elif isinstance(value, bytes):
|
||
result[key] = value.decode("utf-8", errors="ignore")
|
||
elif isinstance(value, str):
|
||
result[key] = value.strip()
|
||
else:
|
||
result[key] = value
|
||
return result
|
||
|
||
# ============ 导入导出 ============
|
||
|
||
async def export_to_excel_streaming(
|
||
self,
|
||
db: AsyncSession,
|
||
selected_fields: List[str] = None,
|
||
include_sub_tables: bool = False,
|
||
batch_size: int = 1000,
|
||
data_scope: Dict[str, Any] = None,
|
||
filters: Dict[str, Any] = None,
|
||
sort_list: List[Dict[str, str]] = None,
|
||
search: str = None,
|
||
search_fields: List[str] = None,
|
||
on_progress: Any = None
|
||
) -> BytesIO:
|
||
"""
|
||
流式导出数据到 Excel(分批查询,避免内存溢出)
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
selected_fields: 选中的字段列表
|
||
include_sub_tables: 是否导出子表
|
||
batch_size: 每批查询的数据量
|
||
data_scope: 数据权限过滤配置
|
||
filters: 过滤条件(与列表查询一致)
|
||
sort_list: 排序列表
|
||
search: 搜索关键词
|
||
search_fields: 搜索字段列表
|
||
on_progress: 进度回调函数 async fn(processed, total),可选
|
||
|
||
Returns:
|
||
Excel 文件的 BytesIO 对象
|
||
"""
|
||
wb = Workbook()
|
||
ws = wb.active
|
||
ws.title = "主表数据"
|
||
|
||
# 从列表配置中获取列定义
|
||
list_config = self._list_config
|
||
columns = list_config.get("columns", [])
|
||
|
||
# 如果指定了选择的字段,只导出这些字段
|
||
if selected_fields and columns:
|
||
columns = [col for col in columns if col.get("field") in selected_fields]
|
||
# 确保 selected_fields 中的字段都被包含
|
||
existing_fields = {col.get("field") for col in columns}
|
||
for field in selected_fields:
|
||
if field not in existing_fields:
|
||
columns.append({"field": field, "label": field.upper()})
|
||
|
||
if columns:
|
||
headers = [col.get("field") for col in columns]
|
||
column_labels = [col.get("label", col.get("field")) for col in columns]
|
||
display_fields = {
|
||
col.get("field"): col.get("displayField")
|
||
for col in columns
|
||
if col.get("displayField")
|
||
}
|
||
else:
|
||
# 如果没有列配置,先查询一条数据获取字段
|
||
first_result = await self.list(db=db, page=1, page_size=1, data_scope=data_scope)
|
||
if first_result["items"]:
|
||
headers = list(first_result["items"][0].keys())
|
||
else:
|
||
headers = []
|
||
column_labels = headers
|
||
display_fields = {}
|
||
|
||
# 从 relation_fields 补充缺少 displayField 的关联字段映射
|
||
relation_fields = self._get_relation_fields()
|
||
for field_name, config in relation_fields.items():
|
||
if field_name not in display_fields and config.get("display_field"):
|
||
display_fields[field_name] = config["display_field"]
|
||
|
||
# 先查询总数(用于进度计算)
|
||
total_count = 0
|
||
if on_progress:
|
||
count_result = await self.list(
|
||
db=db, page=1, page_size=1, data_scope=data_scope,
|
||
filters=filters, sort_list=sort_list,
|
||
search=search, search_fields=search_fields
|
||
)
|
||
total_count = min(count_result.get("total", 0), MAX_IMPORT_EXPORT_ROWS)
|
||
await on_progress(0, total_count, "querying")
|
||
|
||
# 定义样式
|
||
header_font = Font(name='微软雅黑', size=11, bold=True, color='FFFFFF')
|
||
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
|
||
header_alignment = Alignment(horizontal='center', vertical='center')
|
||
data_alignment = Alignment(horizontal='left', vertical='center')
|
||
thin_border = Border(
|
||
left=Side(style='thin', color='D0D0D0'),
|
||
right=Side(style='thin', color='D0D0D0'),
|
||
top=Side(style='thin', color='D0D0D0'),
|
||
bottom=Side(style='thin', color='D0D0D0')
|
||
)
|
||
|
||
# 写入表头
|
||
ws.append(column_labels)
|
||
for col_idx, cell in enumerate(ws[1], 1):
|
||
cell.font = header_font
|
||
cell.fill = header_fill
|
||
cell.alignment = header_alignment
|
||
cell.border = thin_border
|
||
column_letter = cell.column_letter
|
||
header_length = len(str(cell.value))
|
||
ws.column_dimensions[column_letter].width = max(12, header_length + 4)
|
||
|
||
# 子表 sheet 初始化(延迟到第一批有数据时创建表头)
|
||
sub_sheets: Dict[str, Any] = {}
|
||
sub_headers_map: Dict[str, List[str]] = {}
|
||
|
||
# 分批查询并写入数据(主表 + 子表同步处理)
|
||
page = 1
|
||
total_rows = 0
|
||
|
||
while True:
|
||
result = await self.list(
|
||
db=db, page=page, page_size=batch_size, data_scope=data_scope,
|
||
filters=filters, sort_list=sort_list,
|
||
search=search, search_fields=search_fields
|
||
)
|
||
items = result["items"]
|
||
|
||
if not items:
|
||
break
|
||
|
||
if page == 1 and total_count == 0:
|
||
total_count = min(result.get("total", 0), MAX_IMPORT_EXPORT_ROWS)
|
||
|
||
batch_ids = []
|
||
for item in items:
|
||
row = []
|
||
for header in headers:
|
||
display_field = display_fields.get(header)
|
||
if display_field and display_field in item:
|
||
value = item.get(display_field, "")
|
||
else:
|
||
value = item.get(header, "")
|
||
if isinstance(value, (list, dict)):
|
||
value = str(value)
|
||
row.append(value)
|
||
ws.append(row)
|
||
total_rows += 1
|
||
if include_sub_tables and item.get("id"):
|
||
batch_ids.append(item.get("id"))
|
||
|
||
# 当前批次的子表数据立即查询并写入
|
||
if include_sub_tables and self.sub_tables and batch_ids:
|
||
for sub_table in self.sub_tables:
|
||
sub_key = sub_table.alias or sub_table.table_name
|
||
sql, params = self.sql_builder.build_select(
|
||
table=sub_table.table_name,
|
||
schema=sub_table.table_schema or None,
|
||
database=sub_table.table_database or None,
|
||
where={sub_table.foreign_key: {"type": "in", "value": batch_ids}}
|
||
)
|
||
rows = await self._execute_query(db, sql, params)
|
||
if not rows:
|
||
continue
|
||
|
||
sub_items = [self._serialize_row(r) for r in rows]
|
||
|
||
if sub_key not in sub_sheets:
|
||
ws_sub = wb.create_sheet(title=sub_key[:31])
|
||
sub_hdrs = ["主表ID"] + [k for k in sub_items[0].keys()]
|
||
ws_sub.append(sub_hdrs)
|
||
for col_idx, cell in enumerate(ws_sub[1], 1):
|
||
cell.font = header_font
|
||
cell.fill = header_fill
|
||
cell.alignment = header_alignment
|
||
cell.border = thin_border
|
||
ws_sub.column_dimensions[cell.column_letter].width = max(12, len(str(cell.value)) + 4)
|
||
sub_sheets[sub_key] = ws_sub
|
||
sub_headers_map[sub_key] = sub_hdrs
|
||
else:
|
||
ws_sub = sub_sheets[sub_key]
|
||
sub_hdrs = sub_headers_map[sub_key]
|
||
|
||
for sub_item in sub_items:
|
||
row = [sub_item.get(sub_table.foreign_key, "")]
|
||
for h in sub_hdrs[1:]:
|
||
value = sub_item.get(h, "")
|
||
if isinstance(value, (list, dict)):
|
||
value = str(value)
|
||
row.append(value)
|
||
ws_sub.append(row)
|
||
|
||
if on_progress:
|
||
await on_progress(total_rows, total_count, "querying")
|
||
|
||
if len(items) < batch_size:
|
||
break
|
||
page += 1
|
||
if total_rows >= MAX_IMPORT_EXPORT_ROWS:
|
||
logger.warning(f"导出数据量达到上限 {MAX_IMPORT_EXPORT_ROWS} 条(服务器内存 {SERVER_MEMORY_GB}GB),停止导出")
|
||
break
|
||
|
||
if on_progress:
|
||
await on_progress(total_rows, total_count, "generating")
|
||
|
||
ws.freeze_panes = 'A2'
|
||
for ws_sub in sub_sheets.values():
|
||
ws_sub.freeze_panes = 'A2'
|
||
|
||
# 保存到内存
|
||
output = BytesIO()
|
||
wb.save(output)
|
||
output.seek(0)
|
||
|
||
logger.info(f"流式导出完成: 共 {total_rows} 条数据")
|
||
return output
|
||
|
||
async def export_to_excel(self, items: List[Dict[str, Any]], selected_fields: List[str] = None, sub_tables_data: Dict[str, List[Dict[str, Any]]] = None) -> BytesIO:
|
||
"""导出数据到 Excel(带样式,支持字段选择和子表导出)"""
|
||
wb = Workbook()
|
||
ws = wb.active
|
||
ws.title = "主表数据"
|
||
|
||
if not items:
|
||
output = BytesIO()
|
||
wb.save(output)
|
||
output.seek(0)
|
||
return output
|
||
|
||
# 从列表配置中获取列定义
|
||
list_config = self._list_config
|
||
columns = list_config.get("columns", [])
|
||
|
||
# 如果没有列配置,使用第一行数据的所有字段
|
||
if not columns:
|
||
headers = list(items[0].keys())
|
||
column_labels = headers
|
||
display_fields = {}
|
||
else:
|
||
# 如果指定了选择的字段,只导出这些字段
|
||
if selected_fields:
|
||
columns = [col for col in columns if col.get("field") in selected_fields]
|
||
|
||
# 确保 selected_fields 中的字段都被包含(即使不在 columns 配置中)
|
||
existing_fields = {col.get("field") for col in columns}
|
||
for field in selected_fields:
|
||
if field not in existing_fields:
|
||
# 添加缺失的字段(如 id)
|
||
columns.append({"field": field, "label": field.upper()})
|
||
|
||
headers = [col.get("field") for col in columns]
|
||
column_labels = [col.get("label", col.get("field")) for col in columns]
|
||
# 构建字段到 displayField 的映射
|
||
display_fields = {
|
||
col.get("field"): col.get("displayField")
|
||
for col in columns
|
||
if col.get("displayField")
|
||
}
|
||
|
||
# 定义样式
|
||
# 表头样式:深蓝色背景,白色粗体文字
|
||
header_font = Font(name='微软雅黑', size=11, bold=True, color='FFFFFF')
|
||
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
|
||
header_alignment = Alignment(horizontal='center', vertical='center')
|
||
|
||
# 数据单元格样式:居中对齐
|
||
data_alignment = Alignment(horizontal='left', vertical='center')
|
||
|
||
# 边框样式
|
||
thin_border = Border(
|
||
left=Side(style='thin', color='D0D0D0'),
|
||
right=Side(style='thin', color='D0D0D0'),
|
||
top=Side(style='thin', color='D0D0D0'),
|
||
bottom=Side(style='thin', color='D0D0D0')
|
||
)
|
||
|
||
# 写入表头
|
||
ws.append(column_labels)
|
||
|
||
# 设置表头样式
|
||
for col_idx, cell in enumerate(ws[1], 1):
|
||
cell.font = header_font
|
||
cell.fill = header_fill
|
||
cell.alignment = header_alignment
|
||
cell.border = thin_border
|
||
# 设置列宽(根据表头长度自动调整)
|
||
column_letter = cell.column_letter
|
||
header_length = len(str(cell.value))
|
||
ws.column_dimensions[column_letter].width = max(12, header_length + 4)
|
||
|
||
# 写入数据
|
||
for item in items:
|
||
row = []
|
||
for header in headers:
|
||
# 优先使用 displayField 的值(如果配置了)
|
||
display_field = display_fields.get(header)
|
||
if display_field and display_field in item:
|
||
value = item.get(display_field, "")
|
||
else:
|
||
value = item.get(header, "")
|
||
|
||
# 处理特殊类型
|
||
if isinstance(value, (list, dict)):
|
||
value = str(value)
|
||
row.append(value)
|
||
ws.append(row)
|
||
|
||
# 设置数据行样式
|
||
for row_idx in range(2, ws.max_row + 1):
|
||
for col_idx in range(1, ws.max_column + 1):
|
||
cell = ws.cell(row=row_idx, column=col_idx)
|
||
cell.alignment = data_alignment
|
||
cell.border = thin_border
|
||
# 交替行背景色(浅灰色)
|
||
if row_idx % 2 == 0:
|
||
cell.fill = PatternFill(start_color='F2F2F2', end_color='F2F2F2', fill_type='solid')
|
||
|
||
# 冻结首行(表头)
|
||
ws.freeze_panes = 'A2'
|
||
|
||
# 导出子表数据
|
||
if sub_tables_data:
|
||
for sub_table_name, sub_data_list in sub_tables_data.items():
|
||
if not sub_data_list:
|
||
continue
|
||
|
||
# 为每个子表创建新的工作表
|
||
ws_sub = wb.create_sheet(title=sub_table_name[:31]) # Excel 工作表名称最多 31 字符
|
||
|
||
# 子表表头(包含主表ID)
|
||
sub_headers = ["主表ID"] + [k for k in sub_data_list[0].keys() if k != "_main_id"]
|
||
ws_sub.append(sub_headers)
|
||
|
||
# 设置子表表头样式
|
||
for col_idx, cell in enumerate(ws_sub[1], 1):
|
||
cell.font = header_font
|
||
cell.fill = header_fill
|
||
cell.alignment = header_alignment
|
||
cell.border = thin_border
|
||
column_letter = cell.column_letter
|
||
ws_sub.column_dimensions[column_letter].width = max(12, len(str(cell.value)) + 4)
|
||
|
||
# 写入子表数据
|
||
for sub_item in sub_data_list:
|
||
row = [sub_item.get("_main_id", "")]
|
||
for header in sub_headers[1:]:
|
||
value = sub_item.get(header, "")
|
||
# 处理特殊类型
|
||
if isinstance(value, (list, dict)):
|
||
value = str(value)
|
||
row.append(value)
|
||
ws_sub.append(row)
|
||
|
||
# 设置子表数据行样式
|
||
for row_idx in range(2, ws_sub.max_row + 1):
|
||
for col_idx in range(1, ws_sub.max_column + 1):
|
||
cell = ws_sub.cell(row=row_idx, column=col_idx)
|
||
cell.alignment = data_alignment
|
||
cell.border = thin_border
|
||
if row_idx % 2 == 0:
|
||
cell.fill = PatternFill(start_color='F2F2F2', end_color='F2F2F2', fill_type='solid')
|
||
|
||
ws_sub.freeze_panes = 'A2'
|
||
|
||
# 保存到内存
|
||
output = BytesIO()
|
||
wb.save(output)
|
||
output.seek(0)
|
||
return output
|
||
|
||
async def get_import_template(self) -> BytesIO:
|
||
"""生成导入模板(带样式)"""
|
||
wb = Workbook()
|
||
ws = wb.active
|
||
ws.title = "导入模板"
|
||
|
||
# 从列表配置中获取列定义
|
||
list_config = self._list_config
|
||
columns = list_config.get("columns", [])
|
||
|
||
# 定义样式
|
||
header_font = Font(name='微软雅黑', size=11, bold=True, color='FFFFFF')
|
||
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
|
||
header_alignment = Alignment(horizontal='center', vertical='center')
|
||
thin_border = Border(
|
||
left=Side(style='thin', color='D0D0D0'),
|
||
right=Side(style='thin', color='D0D0D0'),
|
||
top=Side(style='thin', color='D0D0D0'),
|
||
bottom=Side(style='thin', color='D0D0D0')
|
||
)
|
||
|
||
if columns:
|
||
# 使用列配置生成表头(排除系统字段和关联字段的 displayField)
|
||
headers = []
|
||
for col in columns:
|
||
field = col.get("field")
|
||
# 排除系统字段
|
||
if field not in ["id", "created_at", "updated_at", "sys_create_datetime", "sys_update_datetime"]:
|
||
# 排除 displayField(如 dept_name),只保留原始字段(如 dept_id)
|
||
display_field = col.get("displayField")
|
||
if not display_field or field != display_field:
|
||
headers.append(col.get("label", field))
|
||
|
||
ws.append(headers)
|
||
|
||
# 设置表头样式
|
||
for col_idx, cell in enumerate(ws[1], 1):
|
||
cell.font = header_font
|
||
cell.fill = header_fill
|
||
cell.alignment = header_alignment
|
||
cell.border = thin_border
|
||
# 设置列宽
|
||
column_letter = cell.column_letter
|
||
header_length = len(str(cell.value))
|
||
ws.column_dimensions[column_letter].width = max(15, header_length + 4)
|
||
|
||
# 添加示例数据行(带浅黄色背景提示这是示例)
|
||
example_row = ["示例数据,请删除此行后填写实际数据"] + [""] * (len(headers) - 1)
|
||
ws.append(example_row)
|
||
for col_idx in range(1, len(headers) + 1):
|
||
cell = ws.cell(row=2, column=col_idx)
|
||
cell.fill = PatternFill(start_color='FFF2CC', end_color='FFF2CC', fill_type='solid')
|
||
cell.alignment = Alignment(horizontal='left', vertical='center')
|
||
cell.border = thin_border
|
||
cell.font = Font(name='微软雅黑', size=10, italic=True, color='808080')
|
||
else:
|
||
# 如果没有列配置,添加提示
|
||
ws.append(["请配置列表字段后重新下载模板"])
|
||
ws['A1'].font = Font(name='微软雅黑', size=12, bold=True, color='FF0000')
|
||
ws.column_dimensions['A'].width = 40
|
||
|
||
# 冻结首行
|
||
ws.freeze_panes = 'A2'
|
||
|
||
# 保存到内存
|
||
output = BytesIO()
|
||
wb.save(output)
|
||
output.seek(0)
|
||
return output
|
||
|
||
async def import_from_excel(
|
||
self,
|
||
db: AsyncSession,
|
||
file_content: bytes,
|
||
batch_size: int = 1000,
|
||
mode: str = "append",
|
||
validate_only: bool = False,
|
||
data_handling: str = "insert_only",
|
||
match_field: str = None,
|
||
on_progress=None
|
||
) -> tuple[int, int, List[Dict[str, Any]]]:
|
||
"""
|
||
从 Excel 批量导入数据
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
file_content: Excel 文件内容
|
||
batch_size: 批量插入的批次大小,默认 1000
|
||
mode: 导入模式,"append"(追加)或 "overwrite"(覆盖,先清空表再导入)
|
||
validate_only: 是否仅验证数据,不执行实际导入
|
||
data_handling: 数据处理方式 insert_only / update_only / upsert
|
||
match_field: 更新模式下用于匹配的字段名
|
||
on_progress: 可选的异步回调 (processed, total, stage, success, fail)
|
||
|
||
Returns:
|
||
(成功数量, 失败数量, 错误详情列表)
|
||
"""
|
||
from utils.context import get_current_user_info_from_context
|
||
from sqlalchemy import text as sa_text
|
||
|
||
file_io = BytesIO(file_content)
|
||
del file_content
|
||
wb = load_workbook(file_io, read_only=True)
|
||
ws = wb.active
|
||
|
||
# read_only 模式下先读表头
|
||
row_iter = ws.iter_rows(values_only=True)
|
||
headers = list(next(row_iter, []))
|
||
|
||
# read_only 模式 ws.max_row 可能不准,用预估值(后续会动态修正)
|
||
total_excel_rows = (ws.max_row - 1) if ws.max_row and ws.max_row > 1 else 0
|
||
|
||
# 从列表配置中获取字段映射
|
||
list_config = self._list_config
|
||
columns = list_config.get("columns", [])
|
||
label_to_field = {col.get("label"): col.get("field") for col in columns if col.get("label") and col.get("field")}
|
||
|
||
# 获取允许的字段
|
||
allowed_fields = self._get_allowed_fields("main")
|
||
|
||
# 系统字段(需要排除)
|
||
system_fields = {"id", "created_at", "updated_at", "sys_create_datetime", "sys_update_datetime",
|
||
"sys_creator_id", "sys_modifier_id", "sys_dept_id", "is_deleted", "sort"}
|
||
|
||
success_count = 0
|
||
fail_count = 0
|
||
error_details = []
|
||
|
||
# 获取用户信息(用于填充系统字段)
|
||
user_info = get_current_user_info_from_context()
|
||
|
||
if on_progress:
|
||
await on_progress(0, total_excel_rows, "parsing", 0, 0)
|
||
|
||
# 根据预估总行数动态计算进度推送间隔(约推送 50~100 次,最少 50 行,最多 5000 行)
|
||
progress_interval = max(50, min(5000, total_excel_rows // 100)) if total_excel_rows > 0 else 100
|
||
|
||
# 流式逐行解析(read_only 模式下 iter_rows 是惰性的)
|
||
all_rows = []
|
||
parsed_count = 0
|
||
for row in row_iter:
|
||
row_num = parsed_count + 2
|
||
try:
|
||
# 跳过空行
|
||
if all(cell is None or (isinstance(cell, str) and cell.strip() == "") for cell in row):
|
||
parsed_count += 1
|
||
continue
|
||
|
||
# 构建数据字典
|
||
data = {}
|
||
for idx, value in enumerate(row):
|
||
if idx < len(headers) and headers[idx]:
|
||
field = label_to_field.get(headers[idx], headers[idx])
|
||
if field and field not in system_fields:
|
||
if field in allowed_fields:
|
||
data[field] = value
|
||
|
||
if data:
|
||
data = self._convert_data_types(data)
|
||
data = self._filter_fields(data, allowed_fields)
|
||
|
||
is_update_mode = mode == "append" and data_handling in ("update_only", "upsert")
|
||
if not is_update_mode:
|
||
data["id"] = str(uuid.uuid4())
|
||
data = self._fill_system_fields_for_create(data)
|
||
|
||
data = self._normalize_data_for_insert(data)
|
||
|
||
validation_error = self._validate_data_against_schema(row_num, data)
|
||
if validation_error:
|
||
fail_count += 1
|
||
error_details.append({"row": row_num, "error": validation_error})
|
||
parsed_count += 1
|
||
continue
|
||
|
||
all_rows.append((row_num, data))
|
||
if len(all_rows) >= MAX_IMPORT_EXPORT_ROWS:
|
||
logger.warning(f"导入数据量达到上限 {MAX_IMPORT_EXPORT_ROWS} 条(服务器内存 {SERVER_MEMORY_GB}GB),截断后续数据")
|
||
parsed_count += 1
|
||
break
|
||
except Exception as e:
|
||
logger.error(f"解析第 {row_num} 行数据失败: {e}")
|
||
fail_count += 1
|
||
error_details.append({"row": row_num, "error": str(e)})
|
||
|
||
parsed_count += 1
|
||
if on_progress and parsed_count % progress_interval == 0:
|
||
if parsed_count > total_excel_rows:
|
||
total_excel_rows = parsed_count
|
||
progress_interval = max(50, min(5000, total_excel_rows // 100))
|
||
await on_progress(parsed_count, total_excel_rows, "parsing", 0, fail_count)
|
||
await asyncio.sleep(0)
|
||
|
||
if parsed_count != total_excel_rows:
|
||
total_excel_rows = parsed_count
|
||
if on_progress:
|
||
await on_progress(parsed_count, total_excel_rows, "parsing", 0, fail_count)
|
||
|
||
wb.close()
|
||
file_io.close()
|
||
del wb, ws, row_iter, file_io
|
||
|
||
if not all_rows:
|
||
return success_count, fail_count, error_details
|
||
|
||
# 预先解析 schema(只解析一次)
|
||
table = self.form_meta.main_table
|
||
schema = await self._resolve_schema(db, self.form_meta.main_table_schema) or None
|
||
database = self.form_meta.main_table_database or None
|
||
|
||
# **按导入模式执行不同的验证规则**
|
||
if on_progress:
|
||
await on_progress(0, len(all_rows), "validating", 0, fail_count)
|
||
|
||
is_update_mode = mode == "append" and data_handling in ("update_only", "upsert")
|
||
unique_errors: Dict[int, str] = {}
|
||
existing_set: Set[str] = set()
|
||
|
||
if mode == "overwrite":
|
||
unique_errors = await self._batch_check_unique_fields_internal_only(all_rows)
|
||
|
||
elif data_handling == "insert_only":
|
||
unique_errors = await self._batch_check_unique_fields(db, all_rows)
|
||
|
||
elif data_handling in ("update_only", "upsert") and match_field:
|
||
match_label = self._get_field_labels([match_field]).get(match_field, match_field)
|
||
|
||
for row_num, data in all_rows:
|
||
val = data.get(match_field)
|
||
if val is None or val == '':
|
||
unique_errors[row_num] = f"匹配字段 '{match_label}' 不能为空"
|
||
|
||
match_val_to_rows: Dict[str, list] = {}
|
||
for row_num, data in all_rows:
|
||
val = data.get(match_field)
|
||
if val is not None and val != '':
|
||
match_val_to_rows.setdefault(str(val), []).append(row_num)
|
||
for val, rows in match_val_to_rows.items():
|
||
if len(rows) > 1:
|
||
for r in rows[1:]:
|
||
if r not in unique_errors:
|
||
unique_errors[r] = f"匹配字段 '{match_label}' 值 '{val}' 在导入数据中重复(第 {rows[0]} 行已存在)"
|
||
|
||
if on_progress:
|
||
await on_progress(len(all_rows) // 3, len(all_rows), "validating", 0, fail_count)
|
||
|
||
match_values = [str(data.get(match_field)) for _, data in all_rows
|
||
if data.get(match_field) not in (None, '')]
|
||
existing_set = await self._batch_query_existing(db, match_field, match_values)
|
||
|
||
if on_progress:
|
||
await on_progress(len(all_rows) * 2 // 3, len(all_rows), "validating", 0, fail_count)
|
||
|
||
if data_handling == "update_only":
|
||
for row_num, data in all_rows:
|
||
val = data.get(match_field)
|
||
if val is not None and val != '' and str(val) not in existing_set and row_num not in unique_errors:
|
||
unique_errors[row_num] = f"未找到匹配记录({match_label}={val})"
|
||
else:
|
||
new_rows = [(rn, d) for rn, d in all_rows
|
||
if d.get(match_field) is not None and d.get(match_field) != ''
|
||
and str(d.get(match_field)) not in existing_set
|
||
and rn not in unique_errors]
|
||
if new_rows:
|
||
insert_unique_errors = await self._batch_check_unique_fields(db, new_rows)
|
||
unique_errors.update(insert_unique_errors)
|
||
|
||
if on_progress:
|
||
await on_progress(len(all_rows), len(all_rows), "validating", 0, fail_count)
|
||
|
||
if unique_errors:
|
||
filtered_rows = []
|
||
for row_num, data in all_rows:
|
||
if row_num in unique_errors:
|
||
fail_count += 1
|
||
error_details.append({"row": row_num, "error": unique_errors[row_num]})
|
||
else:
|
||
filtered_rows.append((row_num, data))
|
||
all_rows = filtered_rows
|
||
|
||
if not all_rows:
|
||
logger.info(f"Excel 导入完成: 成功 {success_count} 条, 失败 {fail_count} 条(所有数据验证失败)")
|
||
return success_count, fail_count, error_details[:100]
|
||
|
||
# **仅验证模式**:返回验证结果,不执行实际导入
|
||
if validate_only:
|
||
success_count = len(all_rows)
|
||
# 计算各模式下的操作预估
|
||
validate_meta: Dict[str, Any] = {"_meta": True}
|
||
if mode == "overwrite":
|
||
validate_meta["will_insert"] = success_count
|
||
validate_meta["will_update"] = 0
|
||
validate_meta["action"] = "overwrite"
|
||
elif data_handling == "insert_only":
|
||
validate_meta["will_insert"] = success_count
|
||
validate_meta["will_update"] = 0
|
||
validate_meta["action"] = "insert_only"
|
||
elif data_handling == "update_only":
|
||
validate_meta["will_insert"] = 0
|
||
validate_meta["will_update"] = success_count
|
||
validate_meta["action"] = "update_only"
|
||
elif data_handling == "upsert" and match_field:
|
||
will_update = sum(
|
||
1 for _, d in all_rows
|
||
if d.get(match_field) is not None and d.get(match_field) != ''
|
||
and str(d.get(match_field)) in existing_set
|
||
)
|
||
will_insert = success_count - will_update
|
||
validate_meta["will_insert"] = will_insert
|
||
validate_meta["will_update"] = will_update
|
||
validate_meta["action"] = "upsert"
|
||
else:
|
||
validate_meta["will_insert"] = success_count
|
||
validate_meta["will_update"] = 0
|
||
validate_meta["action"] = data_handling
|
||
|
||
truncated_errors = error_details[:100]
|
||
truncated_errors.append(validate_meta)
|
||
logger.info(f"Excel 数据验证完成: 通过 {success_count} 条, 失败 {fail_count} 条, "
|
||
f"预计新增 {validate_meta.get('will_insert', 0)} 条, "
|
||
f"预计更新 {validate_meta.get('will_update', 0)} 条")
|
||
return success_count, fail_count, truncated_errors
|
||
|
||
main_exec_db = self._table_exec_database(database)
|
||
total_valid = len(all_rows)
|
||
imported_count = 0
|
||
|
||
if all_rows:
|
||
num_fields = len(all_rows[0][1])
|
||
if num_fields > 0:
|
||
max_safe_batch = 32000 // num_fields
|
||
batch_size = max(1, min(batch_size, max_safe_batch))
|
||
|
||
if on_progress:
|
||
await on_progress(0, total_valid, "importing", success_count, fail_count)
|
||
|
||
if is_update_mode and match_field:
|
||
async with self._business_transaction(database=main_exec_db):
|
||
success_count, fail_count, error_details = await self._import_with_update(
|
||
db,
|
||
table,
|
||
schema,
|
||
database,
|
||
all_rows,
|
||
match_field,
|
||
data_handling,
|
||
success_count,
|
||
fail_count,
|
||
error_details,
|
||
on_progress=on_progress,
|
||
)
|
||
if not self.db_adapter.is_external:
|
||
await db.commit()
|
||
return success_count, fail_count, error_details[:100]
|
||
|
||
async def _import_append_batch(batch_rows: List[tuple]) -> None:
|
||
nonlocal success_count, fail_count, imported_count
|
||
batch_data_list = [item[1] for item in batch_rows]
|
||
batch_row_nums = [item[0] for item in batch_rows]
|
||
sp_batch = f"sp_batch_{uuid.uuid4().hex[:8]}"
|
||
try:
|
||
await self._create_savepoint(db, sp_batch)
|
||
await self._batch_insert_raw(
|
||
db, table, schema, database, batch_data_list
|
||
)
|
||
await self._release_savepoint(db, sp_batch)
|
||
success_count += len(batch_data_list)
|
||
except Exception as batch_err:
|
||
logger.warning(
|
||
f"批量插入失败(第 {batch_row_nums[0]}-{batch_row_nums[-1]} 行),"
|
||
f"降级逐条插入: {batch_err}"
|
||
)
|
||
await self._rollback_to_savepoint(db, sp_batch)
|
||
for row_num, data in batch_rows:
|
||
sp_single = f"sp_{uuid.uuid4().hex[:8]}"
|
||
try:
|
||
await self._create_savepoint(db, sp_single)
|
||
sql, params = self.sql_builder.build_insert(
|
||
table=table,
|
||
data=data,
|
||
schema=schema,
|
||
database=database,
|
||
return_id=False,
|
||
)
|
||
await self._execute_command(
|
||
db,
|
||
sql,
|
||
params,
|
||
enrich_fk=False,
|
||
database=main_exec_db,
|
||
)
|
||
await self._release_savepoint(db, sp_single)
|
||
success_count += 1
|
||
except Exception as single_err:
|
||
await self._rollback_to_savepoint(db, sp_single)
|
||
fail_count += 1
|
||
error_details.append({
|
||
"row": row_num,
|
||
"error": format_error_message(single_err),
|
||
})
|
||
imported_count += len(batch_rows)
|
||
|
||
if self.db_adapter.is_external:
|
||
# 第三方库:每批独立 Handler 事务提交,避免整表导入失败时全部回滚
|
||
if mode == "overwrite":
|
||
async with self._business_transaction(database=main_exec_db):
|
||
await self._truncate_table(db, table, schema, database)
|
||
logger.info(f"覆盖模式:已清空表 {table}")
|
||
for batch_start in range(0, len(all_rows), batch_size):
|
||
batch = all_rows[batch_start:batch_start + batch_size]
|
||
async with self._business_transaction(database=main_exec_db):
|
||
await _import_append_batch(batch)
|
||
db.expire_all()
|
||
if on_progress:
|
||
await on_progress(
|
||
imported_count,
|
||
total_valid,
|
||
"importing",
|
||
success_count,
|
||
fail_count,
|
||
)
|
||
else:
|
||
async with self._business_transaction(database=main_exec_db):
|
||
if mode == "overwrite":
|
||
await self._truncate_table(db, table, schema, database)
|
||
logger.info(f"覆盖模式:已清空表 {table}")
|
||
for batch_start in range(0, len(all_rows), batch_size):
|
||
batch = all_rows[batch_start:batch_start + batch_size]
|
||
await _import_append_batch(batch)
|
||
await db.commit()
|
||
db.expire_all()
|
||
if on_progress:
|
||
await on_progress(
|
||
imported_count,
|
||
total_valid,
|
||
"importing",
|
||
success_count,
|
||
fail_count,
|
||
)
|
||
|
||
logger.info(f"Excel 导入完成: 成功 {success_count} 条, 失败 {fail_count} 条")
|
||
return success_count, fail_count, error_details[:100]
|
||
|
||
async def _import_with_update(
|
||
self,
|
||
db: AsyncSession,
|
||
table: str,
|
||
schema: Optional[str],
|
||
database: Optional[str],
|
||
all_rows: List[tuple],
|
||
match_field: str,
|
||
data_handling: str,
|
||
success_count: int,
|
||
fail_count: int,
|
||
error_details: List[Dict[str, Any]],
|
||
on_progress=None
|
||
) -> tuple[int, int, List[Dict[str, Any]]]:
|
||
"""
|
||
导入数据(更新/upsert 模式)
|
||
|
||
通过 match_field 匹配已有记录:
|
||
- update_only: 匹配到则更新,未匹配到则跳过
|
||
- upsert: 匹配到则更新,未匹配到则新增
|
||
"""
|
||
full_table = self.sql_builder.build_table_name(table, schema, database)
|
||
quoted_match = self.sql_builder.quote_identifier(match_field)
|
||
total_valid = len(all_rows)
|
||
|
||
# 1. 收集所有行的匹配字段值
|
||
match_values = []
|
||
for row_num, data in all_rows:
|
||
val = data.get(match_field)
|
||
if val is not None:
|
||
match_values.append(val)
|
||
|
||
# 2. 批量查询数据库中已存在的记录(match_field -> id 的映射)
|
||
existing_map = {}
|
||
if match_values:
|
||
for i in range(0, len(match_values), 500):
|
||
batch_vals = match_values[i:i + 500]
|
||
placeholders = ", ".join([f":mv_{j}" for j in range(len(batch_vals))])
|
||
quoted_id = self.sql_builder.quote_identifier("id")
|
||
query_sql = (
|
||
f"SELECT {quoted_id}, {quoted_match} FROM {full_table} "
|
||
f"WHERE {quoted_match} IN ({placeholders})"
|
||
)
|
||
params = {f"mv_{j}": v for j, v in enumerate(batch_vals)}
|
||
try:
|
||
result = await self._execute_query(
|
||
db,
|
||
query_sql,
|
||
params,
|
||
database=self._table_exec_database(database),
|
||
)
|
||
for row in result:
|
||
existing_map[row[match_field]] = row["id"]
|
||
except Exception as e:
|
||
logger.error(f"查询已有数据失败: {e}")
|
||
|
||
# 3. 分批处理每行数据
|
||
imported_count = 0
|
||
commit_batch_size = 500
|
||
main_exec_db = self._table_exec_database(database)
|
||
import_progress_interval = max(50, min(5000, total_valid // 100)) if total_valid > 0 else 100
|
||
for row_num, data in all_rows:
|
||
match_val = data.get(match_field)
|
||
existing_id = existing_map.get(match_val) if match_val is not None else None
|
||
|
||
sp = f"sp_{uuid.uuid4().hex[:8]}"
|
||
try:
|
||
await self._create_savepoint(db, sp)
|
||
|
||
if existing_id:
|
||
update_data = {k: v for k, v in data.items() if k not in ("id", "sys_create_datetime", "sys_creator_id", "sys_dept_id")}
|
||
update_data = self._fill_system_fields_for_update(update_data)
|
||
|
||
sql, params = self.sql_builder.build_update(
|
||
table=table, data=update_data, pk_field="id", pk_value=existing_id,
|
||
schema=schema, database=database
|
||
)
|
||
await self._execute_command(
|
||
db,
|
||
sql,
|
||
params,
|
||
enrich_fk=False,
|
||
database=main_exec_db,
|
||
)
|
||
await self._release_savepoint(db, sp)
|
||
success_count += 1
|
||
elif data_handling == "upsert":
|
||
data["id"] = str(uuid.uuid4())
|
||
data = self._fill_system_fields_for_create(data)
|
||
sql, params = self.sql_builder.build_insert(
|
||
table=table, data=data, schema=schema,
|
||
database=database, return_id=False
|
||
)
|
||
await self._execute_command(
|
||
db,
|
||
sql,
|
||
params,
|
||
enrich_fk=False,
|
||
database=main_exec_db,
|
||
)
|
||
await self._release_savepoint(db, sp)
|
||
success_count += 1
|
||
else:
|
||
await self._release_savepoint(db, sp)
|
||
fail_count += 1
|
||
error_details.append({"row": row_num, "error": f"未找到匹配记录({match_field}={match_val})"})
|
||
except Exception as e:
|
||
await self._rollback_to_savepoint(db, sp)
|
||
fail_count += 1
|
||
error_details.append({
|
||
"row": row_num,
|
||
"error": format_error_message(e),
|
||
})
|
||
|
||
imported_count += 1
|
||
if (
|
||
self.db_adapter.is_external
|
||
and imported_count % commit_batch_size == 0
|
||
):
|
||
# 第三方在父级 _business_transaction 内由调用方整段提交;此处仅刷新 ORM
|
||
db.expire_all()
|
||
elif imported_count % commit_batch_size == 0 and not self.db_adapter.is_external:
|
||
await db.commit()
|
||
db.expire_all()
|
||
if on_progress and imported_count % import_progress_interval == 0:
|
||
await on_progress(imported_count, total_valid, "importing", success_count, fail_count)
|
||
|
||
if not self.db_adapter.is_external:
|
||
await db.commit()
|
||
db.expire_all()
|
||
if on_progress:
|
||
await on_progress(imported_count, total_valid, "importing", success_count, fail_count)
|
||
logger.info(f"Excel 导入完成({data_handling}): 成功 {success_count} 条, 失败 {fail_count} 条")
|
||
return success_count, fail_count, error_details
|
||
|
||
async def _truncate_table(
|
||
self, db: AsyncSession, table: str, schema: Optional[str], database: Optional[str]
|
||
) -> None:
|
||
"""
|
||
清空表数据(用于覆盖模式)
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
table: 表名
|
||
schema: Schema 名
|
||
database: 数据库名
|
||
"""
|
||
full_table = self.sql_builder.build_table_name(table, schema, database)
|
||
|
||
for sub_table in self.sub_tables:
|
||
sub_schema = (
|
||
await self._resolve_schema(db, sub_table.table_schema)
|
||
if sub_table.table_schema
|
||
else None
|
||
)
|
||
sub_database = sub_table.table_database or None
|
||
sub_full_table = self.sql_builder.build_table_name(
|
||
sub_table.table_name, sub_schema, sub_database
|
||
)
|
||
await self._execute_command(
|
||
db,
|
||
f"DELETE FROM {sub_full_table}",
|
||
database=self._table_exec_database(sub_database),
|
||
)
|
||
|
||
await self._execute_command(
|
||
db,
|
||
f"DELETE FROM {full_table}",
|
||
database=self._table_exec_database(database),
|
||
)
|
||
|
||
async def _batch_check_unique_fields_internal_only(
|
||
self, data_list: List[tuple[int, Dict[str, Any]]]
|
||
) -> Dict[int, str]:
|
||
"""
|
||
仅检查导入数据内部重复(用于覆盖模式,跳过数据库检查)
|
||
|
||
Args:
|
||
data_list: [(row_num, data), ...] 数据列表
|
||
|
||
Returns:
|
||
{row_num: error_message} 违反唯一性约束的行号和错误信息
|
||
"""
|
||
unique_fields = self._get_unique_check_fields()
|
||
if not unique_fields:
|
||
return {}
|
||
|
||
errors = {}
|
||
|
||
for field_name in unique_fields:
|
||
# 收集该字段的所有非空值及其行号
|
||
value_to_rows = {} # {value: [row_num1, row_num2, ...]}
|
||
for row_num, data in data_list:
|
||
value = data.get(field_name)
|
||
if value is None or value == '':
|
||
continue
|
||
value_str = str(value)
|
||
if value_str not in value_to_rows:
|
||
value_to_rows[value_str] = []
|
||
value_to_rows[value_str].append(row_num)
|
||
|
||
# 检查内部重复
|
||
for value, row_nums in value_to_rows.items():
|
||
if len(row_nums) > 1:
|
||
field_label = self._get_field_labels([field_name]).get(field_name, field_name)
|
||
for row_num in row_nums[1:]:
|
||
errors[row_num] = f"字段 '{field_label}' 值 '{value}' 在导入数据中重复(第 {row_nums[0]} 行已存在)"
|
||
|
||
return errors
|
||
|
||
async def _batch_check_unique_fields(
|
||
self, db: AsyncSession, data_list: List[tuple[int, Dict[str, Any]]]
|
||
) -> Dict[int, str]:
|
||
"""
|
||
批量检查唯一性约束(高性能版)
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
data_list: [(row_num, data), ...] 数据列表
|
||
|
||
Returns:
|
||
{row_num: error_message} 违反唯一性约束的行号和错误信息
|
||
"""
|
||
unique_fields = self._get_unique_check_fields()
|
||
if not unique_fields:
|
||
return {}
|
||
|
||
errors = {}
|
||
table = self.form_meta.main_table
|
||
schema = await self._resolve_schema(db, self.form_meta.main_table_schema) or None
|
||
database = self.form_meta.main_table_database or None
|
||
table_name = self.sql_builder.build_table_name(table, schema, database)
|
||
|
||
for field_name in unique_fields:
|
||
# 收集该字段的所有非空值及其行号
|
||
value_to_rows = {} # {value: [row_num1, row_num2, ...]}
|
||
for row_num, data in data_list:
|
||
value = data.get(field_name)
|
||
if value is None or value == '':
|
||
continue
|
||
value_str = str(value)
|
||
if value_str not in value_to_rows:
|
||
value_to_rows[value_str] = []
|
||
value_to_rows[value_str].append(row_num)
|
||
|
||
if not value_to_rows:
|
||
continue
|
||
|
||
# 1. 检查导入数据内部重复
|
||
for value, row_nums in value_to_rows.items():
|
||
if len(row_nums) > 1:
|
||
# 内部重复:除了第一行,其他行都标记为错误
|
||
field_label = self._get_field_labels([field_name]).get(field_name, field_name)
|
||
for row_num in row_nums[1:]:
|
||
errors[row_num] = f"字段 '{field_label}' 值 '{value}' 在导入数据中重复(第 {row_nums[0]} 行已存在)"
|
||
|
||
# 2. 分批检查数据库中是否已存在(避免超出 32767 参数限制)
|
||
values = list(value_to_rows.keys())
|
||
if not values:
|
||
continue
|
||
|
||
quoted_field = self.sql_builder.quote_identifier(field_name)
|
||
has_is_deleted = await self._check_column_exists(db, table, schema, database, "is_deleted")
|
||
existing_values: set = set()
|
||
|
||
for batch_start in range(0, len(values), 500):
|
||
batch_vals = values[batch_start:batch_start + 500]
|
||
placeholders = ", ".join(f":v{i}" for i in range(len(batch_vals)))
|
||
sql = f"SELECT {quoted_field} FROM {table_name} WHERE {quoted_field} IN ({placeholders})"
|
||
params = {f"v{i}": v for i, v in enumerate(batch_vals)}
|
||
|
||
if has_is_deleted:
|
||
sql += f" AND {self.sql_builder.is_deleted_predicate()}"
|
||
|
||
result = await self._execute_query(db, sql, params)
|
||
existing_values.update(str(row[field_name]) for row in result)
|
||
|
||
# 标记数据库中已存在的值
|
||
field_label = self._get_field_labels([field_name]).get(field_name, field_name)
|
||
for value in existing_values:
|
||
for row_num in value_to_rows[value]:
|
||
if row_num not in errors: # 避免覆盖内部重复错误
|
||
errors[row_num] = f"字段 '{field_label}' 值 '{value}' 已存在于数据库中"
|
||
|
||
return errors
|
||
|
||
async def _batch_query_existing(
|
||
self, db: AsyncSession, field: str, values: List[str]
|
||
) -> Set[str]:
|
||
"""
|
||
批量查询数据库中某字段已存在的值集合
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
field: 要查询的字段名
|
||
values: 要检查的值列表
|
||
|
||
Returns:
|
||
数据库中已存在的值集合(字符串形式)
|
||
"""
|
||
if not values:
|
||
return set()
|
||
|
||
table = self.form_meta.main_table
|
||
schema = await self._resolve_schema(db, self.form_meta.main_table_schema) or None
|
||
database = self.form_meta.main_table_database or None
|
||
table_name = self.sql_builder.build_table_name(table, schema, database)
|
||
quoted_field = self.sql_builder.quote_identifier(field)
|
||
|
||
existing: Set[str] = set()
|
||
unique_values = list(dict.fromkeys(values))
|
||
|
||
for i in range(0, len(unique_values), 500):
|
||
batch_vals = unique_values[i:i + 500]
|
||
placeholders = ", ".join(f":v{j}" for j in range(len(batch_vals)))
|
||
sql = f"SELECT {quoted_field} FROM {table_name} WHERE {quoted_field} IN ({placeholders})"
|
||
params = {f"v{j}": v for j, v in enumerate(batch_vals)}
|
||
|
||
if await self._check_column_exists(db, table, schema, database, "is_deleted"):
|
||
sql += f" AND {self.sql_builder.is_deleted_predicate()}"
|
||
|
||
result = await self._execute_query(db, sql, params)
|
||
for row in result:
|
||
existing.add(str(row[field] if field in row else list(row.values())[0]))
|
||
|
||
return existing
|
||
|
||
def _validate_data_against_schema(self, row_num: int, data: Dict[str, Any]) -> Optional[str]:
|
||
"""
|
||
根据数据库表结构验证数据
|
||
|
||
Args:
|
||
row_num: 行号
|
||
data: 数据字典
|
||
|
||
Returns:
|
||
错误信息,如果验证通过则返回 None
|
||
"""
|
||
form_config = self._form_config
|
||
table_configs = form_config.get('tableConfigs', [])
|
||
|
||
# 找到主表配置
|
||
main_table_config = None
|
||
for tc in table_configs:
|
||
if tc.get('type') == 'main':
|
||
main_table_config = tc
|
||
break
|
||
|
||
if not main_table_config:
|
||
return None
|
||
|
||
fields = main_table_config.get('fields', [])
|
||
field_map = {f.get('name'): f for f in fields if f.get('name')}
|
||
|
||
# 验证每个字段
|
||
for field_name, value in data.items():
|
||
# 跳过系统字段
|
||
if field_name in {'id', 'sys_create_datetime', 'sys_update_datetime',
|
||
'sys_creator_id', 'sys_modifier_id', 'sys_dept_id',
|
||
'is_deleted', 'sort'}:
|
||
continue
|
||
|
||
field_config = field_map.get(field_name)
|
||
if not field_config:
|
||
continue
|
||
|
||
# 1. NOT NULL 约束检查
|
||
nullable = field_config.get('nullable', True)
|
||
if not nullable and (value is None or (isinstance(value, str) and value.strip() == '')):
|
||
return f"字段 '{field_name}' 不能为空"
|
||
|
||
# 跳过 NULL 值的其他检查
|
||
if value is None:
|
||
continue
|
||
|
||
# 2. 字符串长度检查
|
||
field_type = field_config.get('type', '').lower()
|
||
max_length = field_config.get('maxLength')
|
||
|
||
if max_length and isinstance(value, str):
|
||
if 'varchar' in field_type or 'char' in field_type or 'text' in field_type:
|
||
if len(value) > max_length:
|
||
return f"字段 '{field_name}' 长度超限(最大 {max_length},实际 {len(value)})"
|
||
|
||
# 3. 数值类型检查
|
||
if 'int' in field_type or 'integer' in field_type:
|
||
if not isinstance(value, (int, float)):
|
||
try:
|
||
int(value)
|
||
except (ValueError, TypeError):
|
||
return f"字段 '{field_name}' 必须是整数"
|
||
|
||
elif 'decimal' in field_type or 'numeric' in field_type or 'float' in field_type or 'double' in field_type:
|
||
if not isinstance(value, (int, float)):
|
||
try:
|
||
float(value)
|
||
except (ValueError, TypeError):
|
||
return f"字段 '{field_name}' 必须是数值"
|
||
|
||
# 精度检查
|
||
precision = field_config.get('precision')
|
||
scale = field_config.get('scale')
|
||
if precision and isinstance(value, (int, float)):
|
||
value_str = str(value)
|
||
if '.' in value_str:
|
||
int_part, dec_part = value_str.split('.')
|
||
if len(int_part) + len(dec_part) > precision:
|
||
return f"字段 '{field_name}' 精度超限(最大 {precision})"
|
||
if scale and len(dec_part) > scale:
|
||
return f"字段 '{field_name}' 小数位数超限(最大 {scale})"
|
||
|
||
# 4. 日期时间类型检查
|
||
elif 'date' in field_type or 'time' in field_type:
|
||
if not isinstance(value, (date, datetime)):
|
||
return f"字段 '{field_name}' 必须是日期时间类型"
|
||
|
||
# 5. 布尔类型检查
|
||
elif 'bool' in field_type:
|
||
if not isinstance(value, bool):
|
||
return f"字段 '{field_name}' 必须是布尔类型"
|
||
|
||
return None
|
||
|
||
async def _batch_insert_raw(
|
||
self, db: AsyncSession, table: str, schema: Optional[str],
|
||
database: Optional[str], data_list: List[Dict[str, Any]]
|
||
) -> None:
|
||
"""
|
||
真正的批量 INSERT:一条 SQL 插入多行数据
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
table: 表名
|
||
schema: Schema 名
|
||
database: 数据库名
|
||
data_list: 数据列表(已标准化)
|
||
"""
|
||
if not data_list:
|
||
return
|
||
|
||
columns = list(data_list[0].keys())
|
||
cols = ", ".join(self.sql_builder.quote_identifier(c) for c in columns)
|
||
full_table = self.sql_builder.build_table_name(table, schema, database)
|
||
|
||
# 构建多行 VALUES,使用命名参数
|
||
row_placeholders = []
|
||
params = {}
|
||
for row_idx, data in enumerate(data_list):
|
||
placeholders = []
|
||
for col in columns:
|
||
param_name = f"p{row_idx}_{col}"
|
||
placeholders.append(f":{param_name}")
|
||
params[param_name] = data.get(col)
|
||
row_placeholders.append(f"({', '.join(placeholders)})")
|
||
|
||
sql = f"INSERT INTO {full_table} ({cols}) VALUES {', '.join(row_placeholders)}"
|
||
await self._execute_command(
|
||
db, sql, params, database=self._table_exec_database(database)
|
||
)
|
||
|
||
def _normalize_data_for_insert(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""
|
||
标准化数据类型,确保所有值都是数据库可接受的类型
|
||
|
||
Args:
|
||
data: 原始数据字典
|
||
|
||
Returns:
|
||
标准化后的数据字典
|
||
"""
|
||
normalized = {}
|
||
for key, value in data.items():
|
||
if value is None:
|
||
normalized[key] = None
|
||
elif isinstance(value, datetime):
|
||
# datetime 对象保持不变,数据库驱动会正确处理
|
||
normalized[key] = value
|
||
elif isinstance(value, date):
|
||
# date 对象保持不变
|
||
normalized[key] = value
|
||
elif isinstance(value, (int, float, bool)):
|
||
# 基本类型保持不变
|
||
normalized[key] = value
|
||
elif isinstance(value, (list, dict)):
|
||
# 列表和字典转换为 JSON 字符串
|
||
normalized[key] = json.dumps(value, ensure_ascii=False)
|
||
elif isinstance(value, str):
|
||
# 字符串保持不变
|
||
normalized[key] = value
|
||
else:
|
||
# 其他类型转换为字符串
|
||
normalized[key] = str(value)
|
||
return normalized
|
||
|
||
# ============ 字段权限过滤 ============
|
||
|
||
async def _get_merged_field_permissions(
|
||
self,
|
||
db: AsyncSession,
|
||
role_ids: List[str] = None
|
||
) -> Dict[str, Dict]:
|
||
"""获取当前用户合并后的字段权限配置"""
|
||
from utils.context import get_current_user_info_from_context
|
||
from core.resource_scope.field_permission.service import ResourceFieldPermissionService
|
||
|
||
if not role_ids:
|
||
user_info = get_current_user_info_from_context()
|
||
logger.debug(f"[字段权限] 从上下文获取用户信息: {user_info}")
|
||
if not user_info or not user_info.get('role_ids'):
|
||
logger.warning("[字段权限] 未获取到用户角色信息,跳过字段权限过滤")
|
||
return {}
|
||
role_ids = user_info['role_ids']
|
||
|
||
resource_type = f"form:{self._form_code}"
|
||
logger.debug(f"[字段权限] 资源类型: {resource_type}, 角色IDs: {role_ids}")
|
||
|
||
configs = await ResourceFieldPermissionService.get_by_roles_and_resource(
|
||
db, role_ids, resource_type
|
||
)
|
||
logger.debug(f"[字段权限] 获取到的配置数量: {len(configs) if configs else 0}")
|
||
|
||
if not configs:
|
||
logger.debug("[字段权限] 未找到字段权限配置,跳过过滤")
|
||
return {}
|
||
|
||
merged_perms = await ResourceFieldPermissionService.merge_field_permissions(
|
||
configs, "most_permissive"
|
||
)
|
||
logger.debug(f"[字段权限] 合并后的权限: {merged_perms}")
|
||
return merged_perms or {}
|
||
|
||
async def apply_field_permissions(
|
||
self,
|
||
data: Any,
|
||
db: AsyncSession,
|
||
role_ids: List[str] = None
|
||
) -> Any:
|
||
"""
|
||
应用字段权限过滤
|
||
|
||
Args:
|
||
data: 数据(单个对象或列表)
|
||
db: 数据库会话
|
||
role_ids: 角色ID列表,如果不传则从上下文获取
|
||
|
||
Returns:
|
||
过滤后的数据
|
||
"""
|
||
merged_perms = await self._get_merged_field_permissions(db, role_ids)
|
||
if not merged_perms:
|
||
return data
|
||
|
||
if isinstance(data, list):
|
||
return [self._apply_field_permissions(item, merged_perms) for item in data]
|
||
return self._apply_field_permissions(data, merged_perms)
|
||
|
||
@staticmethod
|
||
def _get_field_permission_type(field_name: str, field_perms: Dict[str, Dict]) -> str:
|
||
"""获取字段权限类型,未配置时默认可写"""
|
||
if not field_perms:
|
||
return 'write'
|
||
perm = field_perms.get(field_name, {})
|
||
return perm.get('permission_type') or perm.get('permission', 'write')
|
||
|
||
def _is_field_writable(self, field_name: str, field_perms: Dict[str, Dict]) -> bool:
|
||
"""判断字段是否可写"""
|
||
return self._get_field_permission_type(field_name, field_perms) == 'write'
|
||
|
||
def _filter_writable_fields(
|
||
self,
|
||
data: Dict[str, Any],
|
||
allowed_fields: Set[str],
|
||
field_perms: Dict[str, Dict],
|
||
always_allow: Optional[Set[str]] = None,
|
||
) -> Dict[str, Any]:
|
||
"""过滤字段白名单,并剔除只读/隐藏/脱敏字段的写入"""
|
||
filtered = self._filter_fields(data, allowed_fields)
|
||
if not field_perms:
|
||
return filtered
|
||
|
||
allow = always_allow or set()
|
||
return {
|
||
key: value
|
||
for key, value in filtered.items()
|
||
if key in allow or self._is_field_writable(key, field_perms)
|
||
}
|
||
|
||
def _apply_field_permissions(self, item: Dict[str, Any], field_perms: Dict[str, Dict]) -> Dict[str, Any]:
|
||
"""
|
||
应用字段权限过滤(隐藏、脱敏)
|
||
|
||
Args:
|
||
item: 数据项(字典)
|
||
field_perms: 字段权限配置
|
||
|
||
Returns:
|
||
过滤后的字典
|
||
"""
|
||
if not isinstance(item, dict):
|
||
return item
|
||
|
||
# 收集需要隐藏或脱敏的字段及其关联的 _name 字段
|
||
hidden_fields = set()
|
||
masked_fields = {} # field_name -> mask_rule
|
||
|
||
for field_name, perm in field_perms.items():
|
||
permission_type = perm.get('permission_type') or perm.get('permission', 'write')
|
||
if permission_type == 'hidden':
|
||
hidden_fields.add(field_name)
|
||
# 同时隐藏关联的 _name 字段
|
||
# 支持多种命名模式:
|
||
# - field -> field_name (如 居住地 -> 居住地_name, post_id -> post_id_name)
|
||
# - field_id -> field_name (如 manger_id -> manger_name)
|
||
hidden_fields.add(f"{field_name}_name")
|
||
if field_name.endswith('_id'):
|
||
base_name = field_name[:-3] # 去掉 _id
|
||
hidden_fields.add(f"{base_name}_name")
|
||
elif permission_type == 'masked':
|
||
masked_fields[field_name] = perm.get('mask_rule')
|
||
# 同时脱敏关联的 _name 字段(使用默认脱敏规则)
|
||
masked_fields[f"{field_name}_name"] = 'default'
|
||
if field_name.endswith('_id'):
|
||
base_name = field_name[:-3]
|
||
masked_fields[f"{base_name}_name"] = 'name'
|
||
|
||
filtered = {}
|
||
for field_name, value in item.items():
|
||
# 递归处理内嵌子表数据
|
||
if field_name == "sub_tables" and isinstance(value, dict):
|
||
filtered["sub_tables"] = {
|
||
table_name: [
|
||
self._apply_field_permissions(row, field_perms)
|
||
if isinstance(row, dict) else row
|
||
for row in rows
|
||
] if isinstance(rows, list) else rows
|
||
for table_name, rows in value.items()
|
||
}
|
||
continue
|
||
|
||
# 检查是否需要隐藏
|
||
if field_name in hidden_fields:
|
||
continue
|
||
|
||
# 检查是否需要脱敏
|
||
if field_name in masked_fields:
|
||
filtered[field_name] = self._mask_value(value, masked_fields[field_name])
|
||
continue
|
||
|
||
# 检查原始字段权限配置
|
||
perm = field_perms.get(field_name, {})
|
||
permission_type = perm.get('permission_type') or perm.get('permission', 'write')
|
||
|
||
if permission_type == 'hidden':
|
||
continue
|
||
elif permission_type == 'masked':
|
||
filtered[field_name] = self._mask_value(value, perm.get('mask_rule'))
|
||
else:
|
||
filtered[field_name] = value
|
||
|
||
return filtered
|
||
|
||
def _mask_value(self, value: Any, mask_rule: Optional[str]) -> str:
|
||
"""
|
||
脱敏处理
|
||
|
||
Args:
|
||
value: 原始值
|
||
mask_rule: 脱敏规则
|
||
|
||
Returns:
|
||
脱敏后的值
|
||
"""
|
||
if not value:
|
||
return value
|
||
|
||
value_str = str(value)
|
||
|
||
if mask_rule == "phone":
|
||
# 手机号脱敏:138****5678
|
||
if len(value_str) == 11:
|
||
return f"{value_str[:3]}****{value_str[-4:]}"
|
||
elif mask_rule == "email":
|
||
# 邮箱脱敏:abc***@example.com
|
||
if "@" in value_str:
|
||
local, domain = value_str.split("@", 1)
|
||
if len(local) > 3:
|
||
return f"{local[:3]}***@{domain}"
|
||
return f"{local[0]}***@{domain}"
|
||
elif mask_rule == "id_card":
|
||
# 身份证脱敏:110***********1234
|
||
if len(value_str) >= 8:
|
||
return f"{value_str[:3]}***********{value_str[-4:]}"
|
||
elif mask_rule == "name":
|
||
# 姓名脱敏:张*
|
||
if len(value_str) > 1:
|
||
return f"{value_str[0]}*"
|
||
return "*"
|
||
|
||
# 默认脱敏:显示前后各2个字符
|
||
if len(value_str) > 4:
|
||
return f"{value_str[:2]}***{value_str[-2:]}"
|
||
return "***"
|