ai-robot-core/ai-service/app/services/metadata_schema_service.py

326 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Metadata Schema Service.
动态元数据模式管理服务,支持租户自定义元数据字段配置。
"""
import logging
import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import col
from app.models.entities import (
MetadataField,
MetadataFieldType,
MetadataSchema,
MetadataSchemaCreate,
MetadataSchemaUpdate,
)
logger = logging.getLogger(__name__)
class MetadataSchemaService:
"""
元数据模式服务
管理租户的动态元数据字段配置
"""
def __init__(self, session: AsyncSession):
self._session = session
async def get_schema(
self,
tenant_id: str,
schema_id: str | None = None,
) -> MetadataSchema | None:
"""
获取元数据模式
Args:
tenant_id: 租户 ID
schema_id: 模式 ID可选不传则获取默认模式
Returns:
MetadataSchema 或 None
"""
if schema_id:
stmt = select(MetadataSchema).where(
MetadataSchema.tenant_id == tenant_id,
MetadataSchema.id == uuid.UUID(schema_id),
)
else:
stmt = select(MetadataSchema).where(
MetadataSchema.tenant_id == tenant_id,
MetadataSchema.is_default == True,
MetadataSchema.is_enabled == True,
).order_by(col(MetadataSchema.created_at).desc())
result = await self._session.execute(stmt)
return result.scalar_one_or_none()
async def list_schemas(
self,
tenant_id: str,
include_disabled: bool = False,
) -> list[MetadataSchema]:
"""
列出租户所有元数据模式
Args:
tenant_id: 租户 ID
include_disabled: 是否包含禁用的模式
Returns:
MetadataSchema 列表
"""
stmt = select(MetadataSchema).where(
MetadataSchema.tenant_id == tenant_id,
)
if not include_disabled:
stmt = stmt.where(MetadataSchema.is_enabled == True)
stmt = stmt.order_by(col(MetadataSchema.created_at).desc())
result = await self._session.execute(stmt)
return list(result.scalars().all())
async def create_schema(
self,
tenant_id: str,
schema_create: MetadataSchemaCreate,
) -> MetadataSchema:
"""
创建元数据模式
Args:
tenant_id: 租户 ID
schema_create: 创建数据
Returns:
创建的 MetadataSchema
"""
schema = MetadataSchema(
tenant_id=tenant_id,
name=schema_create.name,
description=schema_create.description,
fields=[f.model_dump() if hasattr(f, 'model_dump') else f for f in schema_create.fields],
is_default=schema_create.is_default,
is_enabled=True,
)
self._session.add(schema)
await self._session.flush()
logger.info(
f"[MetadataSchemaService] Created schema: tenant={tenant_id}, "
f"name={schema.name}, fields_count={len(schema.fields)}"
)
return schema
async def update_schema(
self,
tenant_id: str,
schema_id: str,
schema_update: MetadataSchemaUpdate,
) -> MetadataSchema | None:
"""
更新元数据模式
Args:
tenant_id: 租户 ID
schema_id: 模式 ID
schema_update: 更新数据
Returns:
更新后的 MetadataSchema 或 None
"""
stmt = select(MetadataSchema).where(
MetadataSchema.tenant_id == tenant_id,
MetadataSchema.id == uuid.UUID(schema_id),
)
result = await self._session.execute(stmt)
schema = result.scalar_one_or_none()
if not schema:
return None
if schema_update.name is not None:
schema.name = schema_update.name
if schema_update.description is not None:
schema.description = schema_update.description
if schema_update.fields is not None:
schema.fields = schema_update.fields
if schema_update.is_default is not None:
if schema_update.is_default:
await self._unset_other_defaults(tenant_id, schema_id)
schema.is_default = True
if schema_update.is_enabled is not None:
schema.is_enabled = schema_update.is_enabled
schema.updated_at = datetime.utcnow()
await self._session.flush()
logger.info(
f"[MetadataSchemaService] Updated schema: tenant={tenant_id}, "
f"schema_id={schema_id}"
)
return schema
async def delete_schema(
self,
tenant_id: str,
schema_id: str,
) -> bool:
"""
删除元数据模式
Args:
tenant_id: 租户 ID
schema_id: 模式 ID
Returns:
是否删除成功
"""
stmt = select(MetadataSchema).where(
MetadataSchema.tenant_id == tenant_id,
MetadataSchema.id == uuid.UUID(schema_id),
)
result = await self._session.execute(stmt)
schema = result.scalar_one_or_none()
if not schema:
return False
if schema.is_default:
logger.warning(
f"[MetadataSchemaService] Cannot delete default schema: "
f"tenant={tenant_id}, schema_id={schema_id}"
)
return False
await self._session.delete(schema)
await self._session.flush()
logger.info(
f"[MetadataSchemaService] Deleted schema: tenant={tenant_id}, "
f"schema_id={schema_id}"
)
return True
async def _unset_other_defaults(
self,
tenant_id: str,
exclude_schema_id: str,
) -> None:
"""取消其他模式的默认状态"""
stmt = select(MetadataSchema).where(
MetadataSchema.tenant_id == tenant_id,
MetadataSchema.is_default == True,
MetadataSchema.id != uuid.UUID(exclude_schema_id),
)
result = await self._session.execute(stmt)
other_schemas = result.scalars().all()
for other in other_schemas:
other.is_default = False
async def get_field_definitions(
self,
tenant_id: str,
schema_id: str | None = None,
) -> dict[str, dict[str, Any]]:
"""
获取字段定义映射
Args:
tenant_id: 租户 ID
schema_id: 模式 ID可选
Returns:
字段名到字段定义的映射,如 {"grade": {"label": "年级", "type": "select", "options": [...]}}
"""
schema = await self.get_schema(tenant_id, schema_id)
if not schema:
return {}
field_map = {}
for field in schema.fields:
field_name = field.get("name") if isinstance(field, dict) else field.name
field_map[field_name] = field if isinstance(field, dict) else field.model_dump()
return field_map
async def validate_metadata(
self,
tenant_id: str,
metadata: dict[str, Any],
schema_id: str | None = None,
) -> tuple[bool, list[str]]:
"""
验证元数据是否符合模式定义
Args:
tenant_id: 租户 ID
metadata: 元数据字典
schema_id: 模式 ID可选
Returns:
(是否有效, 错误消息列表)
"""
field_defs = await self.get_field_definitions(tenant_id, schema_id)
if not field_defs:
return True, []
errors = []
for field_name, field_def in field_defs.items():
field_type = field_def.get("field_type", "string")
required = field_def.get("required", False)
options = field_def.get("options", [])
value = metadata.get(field_name)
if required and value is None:
errors.append(f"字段 '{field_def.get('label', field_name)}' 是必填的")
continue
if value is None:
continue
if field_type == MetadataFieldType.SELECT.value:
if value not in options:
errors.append(
f"字段 '{field_def.get('label', field_name)}' 的值 '{value}' 不在允许选项中"
)
elif field_type == MetadataFieldType.MULTI_SELECT.value:
if not isinstance(value, list):
errors.append(f"字段 '{field_def.get('label', field_name)}' 应该是多选值")
else:
for v in value:
if v not in options:
errors.append(
f"字段 '{field_def.get('label', field_name)}' 的值 '{v}' 不在允许选项中"
)
elif field_type == MetadataFieldType.NUMBER.value:
if not isinstance(value, (int, float)):
try:
float(value)
except (ValueError, TypeError):
errors.append(f"字段 '{field_def.get('label', field_name)}' 应该是数字")
elif field_type == MetadataFieldType.BOOLEAN.value:
if not isinstance(value, bool):
if value not in ["true", "false", "1", "0", 1, 0]:
errors.append(f"字段 '{field_def.get('label', field_name)}' 应该是布尔值")
return len(errors) == 0, errors