Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.base_schema import ResponseModel
|
||||
from app.resource_registry import ResourceRegistry
|
||||
from core.resource_scope.field_permission.schema import (
|
||||
FieldPermissionResponse,
|
||||
FieldPermissionBatchUpdate,
|
||||
ResourceFieldsMetadataResponse,
|
||||
ResourceFieldMetadata
|
||||
)
|
||||
from core.resource_scope.field_permission.service import ResourceFieldPermissionService
|
||||
|
||||
router = APIRouter(prefix="/field-permissions", tags=["字段权限配置"])
|
||||
|
||||
|
||||
@router.get("/resource-fields", response_model=List[ResourceFieldsMetadataResponse], summary="获取所有资源的字段元数据")
|
||||
async def get_all_resource_fields(
|
||||
application_id: Optional[str] = Query(None, alias="applicationId", description="应用ID,子应用访问时只显示该应用的资源")
|
||||
):
|
||||
"""获取所有已注册资源的字段元数据"""
|
||||
# 获取资源列表,支持按应用过滤
|
||||
all_resources = ResourceRegistry.get_all_resources(application_id=application_id)
|
||||
result = []
|
||||
|
||||
for resource_item in all_resources:
|
||||
resource_type = resource_item['resource_type']
|
||||
resource_info = ResourceRegistry.get_resource(resource_type)
|
||||
if not resource_info:
|
||||
continue
|
||||
|
||||
# 优先从 service_class 获取字段元数据,否则从注册表获取
|
||||
field_metadata = None
|
||||
service_class = resource_info.get('service')
|
||||
if service_class and hasattr(service_class, 'FIELD_METADATA'):
|
||||
field_metadata = service_class.FIELD_METADATA
|
||||
elif resource_info.get('field_metadata'):
|
||||
field_metadata = resource_info.get('field_metadata')
|
||||
|
||||
if not field_metadata:
|
||||
continue
|
||||
fields = [
|
||||
ResourceFieldMetadata(
|
||||
field_name=field_name,
|
||||
label=meta.get('label', field_name),
|
||||
field_type=meta.get('field_type', 'string'),
|
||||
required=meta.get('required', False),
|
||||
sensitive=meta.get('sensitive', False),
|
||||
maskable=meta.get('maskable', False),
|
||||
default_permission=meta.get('default_permission', 'write')
|
||||
)
|
||||
for field_name, meta in field_metadata.items()
|
||||
]
|
||||
|
||||
result.append(ResourceFieldsMetadataResponse(
|
||||
resource_type=resource_type,
|
||||
display_name=resource_info.get('display_name', resource_type),
|
||||
fields=fields
|
||||
))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/resource-fields/{resource_type}", response_model=ResourceFieldsMetadataResponse, summary="获取指定资源的字段元数据")
|
||||
async def get_resource_fields(resource_type: str):
|
||||
"""获取指定资源的字段元数据"""
|
||||
resource_info = ResourceRegistry.get_resource(resource_type)
|
||||
if not resource_info:
|
||||
raise HTTPException(status_code=404, detail=f"资源类型不存在: {resource_type}")
|
||||
|
||||
# 优先从 service_class 获取字段元数据,否则从注册表获取
|
||||
field_metadata = None
|
||||
service_class = resource_info.get('service')
|
||||
if service_class and hasattr(service_class, 'FIELD_METADATA'):
|
||||
field_metadata = service_class.FIELD_METADATA
|
||||
elif resource_info.get('field_metadata'):
|
||||
field_metadata = resource_info.get('field_metadata')
|
||||
|
||||
if not field_metadata:
|
||||
raise HTTPException(status_code=404, detail=f"资源未定义字段元数据: {resource_type}")
|
||||
|
||||
fields = [
|
||||
ResourceFieldMetadata(
|
||||
field_name=field_name,
|
||||
label=meta.get('label', field_name),
|
||||
field_type=meta.get('field_type', 'string'),
|
||||
required=meta.get('required', False),
|
||||
sensitive=meta.get('sensitive', False),
|
||||
maskable=meta.get('maskable', False),
|
||||
default_permission=meta.get('default_permission', 'write')
|
||||
)
|
||||
for field_name, meta in field_metadata.items()
|
||||
]
|
||||
|
||||
return ResourceFieldsMetadataResponse(
|
||||
resource_type=resource_type,
|
||||
display_name=resource_info.get('display_name', resource_type),
|
||||
fields=fields
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{role_id}", response_model=List[FieldPermissionResponse], summary="获取角色的字段权限配置")
|
||||
async def get_role_field_permissions(
|
||||
role_id: str,
|
||||
resource_type: str = Query(..., description="资源类型"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取角色在指定资源类型下的字段权限配置"""
|
||||
configs = await ResourceFieldPermissionService.get_by_role_and_resource(
|
||||
db, role_id, resource_type
|
||||
)
|
||||
return configs
|
||||
|
||||
|
||||
@router.post("/batch", response_model=ResponseModel, summary="批量更新字段权限配置")
|
||||
async def batch_update_field_permissions(
|
||||
data: FieldPermissionBatchUpdate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""批量更新角色的字段权限配置"""
|
||||
# 验证资源类型是否存在
|
||||
if not ResourceRegistry.validate_resource_type(data.resource_type):
|
||||
raise HTTPException(status_code=400, detail=f"资源类型不存在: {data.resource_type}")
|
||||
|
||||
await ResourceFieldPermissionService.batch_update(db, data)
|
||||
return ResponseModel(message="字段权限配置成功")
|
||||
|
||||
|
||||
@router.delete("/{role_id}", response_model=ResponseModel, summary="删除角色的字段权限配置")
|
||||
async def delete_role_field_permissions(
|
||||
role_id: str,
|
||||
resource_type: str = Query(..., description="资源类型"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""删除角色在指定资源类型下的所有字段权限配置"""
|
||||
from sqlalchemy import delete
|
||||
from core.resource_scope.field_permission.model import ResourceFieldPermissionConfig
|
||||
|
||||
stmt = delete(ResourceFieldPermissionConfig).where(
|
||||
ResourceFieldPermissionConfig.role_id == role_id,
|
||||
ResourceFieldPermissionConfig.resource_type == resource_type
|
||||
)
|
||||
await db.execute(stmt)
|
||||
await db.commit()
|
||||
|
||||
return ResponseModel(message="删除成功")
|
||||
@@ -0,0 +1,24 @@
|
||||
from sqlalchemy import Column, String, Index
|
||||
|
||||
from app.base_model import BaseModel
|
||||
|
||||
|
||||
class ResourceFieldPermissionConfig(BaseModel):
|
||||
"""角色资源字段权限配置(列权限)"""
|
||||
__tablename__ = "sys_role_resource_field_permission_config"
|
||||
|
||||
role_id = Column(String(50), nullable=False, comment="角色ID")
|
||||
resource_type = Column(String(100), nullable=False, comment="资源类型")
|
||||
field_name = Column(String(100), nullable=False, comment="字段名称")
|
||||
permission_type = Column(
|
||||
String(20),
|
||||
nullable=False,
|
||||
default="read",
|
||||
comment="权限类型: read/write/hidden/masked"
|
||||
)
|
||||
mask_rule = Column(String(50), nullable=True, comment="脱敏规则: phone/email/id_card/name")
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_role_resource_field', 'role_id', 'resource_type', 'field_name', unique=True),
|
||||
Index('idx_role_resource', 'role_id', 'resource_type'),
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, ConfigDict, field_validator
|
||||
|
||||
from app.base_schema import CSTDatetime
|
||||
|
||||
|
||||
class FieldPermissionBase(BaseModel):
|
||||
"""字段权限基础 Schema"""
|
||||
field_name: str
|
||||
permission_type: str = "read" # read/write/hidden/masked
|
||||
mask_rule: Optional[str] = None
|
||||
|
||||
@field_validator('permission_type')
|
||||
@classmethod
|
||||
def validate_permission_type(cls, v):
|
||||
allowed = ['read', 'write', 'hidden', 'masked']
|
||||
if v not in allowed:
|
||||
raise ValueError(f'permission_type must be one of {allowed}')
|
||||
return v
|
||||
|
||||
@field_validator('mask_rule')
|
||||
@classmethod
|
||||
def validate_mask_rule(cls, v, info):
|
||||
if info.data.get('permission_type') == 'masked' and not v:
|
||||
raise ValueError('mask_rule is required when permission_type is masked')
|
||||
if v:
|
||||
allowed = ['phone', 'email', 'id_card', 'name', 'default']
|
||||
if v not in allowed:
|
||||
raise ValueError(f'mask_rule must be one of {allowed}')
|
||||
return v
|
||||
|
||||
|
||||
class FieldPermissionCreate(FieldPermissionBase):
|
||||
"""创建字段权限配置"""
|
||||
role_id: str
|
||||
resource_type: str
|
||||
|
||||
|
||||
class FieldPermissionUpdate(BaseModel):
|
||||
"""更新字段权限配置"""
|
||||
permission_type: Optional[str] = None
|
||||
mask_rule: Optional[str] = None
|
||||
|
||||
|
||||
class FieldPermissionResponse(FieldPermissionBase):
|
||||
"""字段权限配置响应"""
|
||||
id: str
|
||||
role_id: str
|
||||
resource_type: str
|
||||
sort: int = 0
|
||||
is_deleted: bool = False
|
||||
sys_create_datetime: Optional[CSTDatetime] = None
|
||||
sys_update_datetime: Optional[CSTDatetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class FieldPermissionBatchUpdate(BaseModel):
|
||||
"""批量更新字段权限配置"""
|
||||
role_id: str
|
||||
resource_type: str
|
||||
configs: List[FieldPermissionBase]
|
||||
|
||||
|
||||
class ResourceFieldMetadata(BaseModel):
|
||||
"""资源字段元数据"""
|
||||
field_name: str
|
||||
label: str
|
||||
field_type: str # string/integer/boolean/datetime
|
||||
required: bool = False # 是否必填字段(必填字段不可隐藏)
|
||||
sensitive: bool = False # 是否敏感字段
|
||||
maskable: bool = False # 是否可脱敏
|
||||
default_permission: str = "write"
|
||||
|
||||
|
||||
class ResourceFieldsMetadataResponse(BaseModel):
|
||||
"""资源字段元数据响应"""
|
||||
resource_type: str
|
||||
display_name: str
|
||||
fields: List[ResourceFieldMetadata]
|
||||
@@ -0,0 +1,152 @@
|
||||
from typing import List, Dict
|
||||
from sqlalchemy import select, delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.base_service import BaseService
|
||||
from core.resource_scope.field_permission.model import ResourceFieldPermissionConfig
|
||||
from core.resource_scope.field_permission.schema import (
|
||||
FieldPermissionCreate,
|
||||
FieldPermissionUpdate,
|
||||
FieldPermissionBatchUpdate
|
||||
)
|
||||
|
||||
|
||||
class ResourceFieldPermissionService(BaseService[
|
||||
ResourceFieldPermissionConfig,
|
||||
FieldPermissionCreate,
|
||||
FieldPermissionUpdate
|
||||
]):
|
||||
"""资源字段权限配置服务"""
|
||||
|
||||
model = ResourceFieldPermissionConfig
|
||||
|
||||
@classmethod
|
||||
async def get_by_role_and_resource(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
role_id: str,
|
||||
resource_type: str
|
||||
) -> List[ResourceFieldPermissionConfig]:
|
||||
"""获取角色在指定资源类型下的字段权限配置"""
|
||||
stmt = select(cls.model).where(
|
||||
cls.model.role_id == role_id,
|
||||
cls.model.resource_type == resource_type,
|
||||
cls.model.is_deleted == False
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
async def get_by_roles_and_resource(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
role_ids: List[str],
|
||||
resource_type: str
|
||||
) -> List[ResourceFieldPermissionConfig]:
|
||||
"""获取多个角色在指定资源类型下的字段权限配置"""
|
||||
stmt = select(cls.model).where(
|
||||
cls.model.role_id.in_(role_ids),
|
||||
cls.model.resource_type == resource_type,
|
||||
cls.model.is_deleted == False
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@classmethod
|
||||
async def batch_update(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
data: FieldPermissionBatchUpdate
|
||||
) -> None:
|
||||
"""批量更新字段权限配置"""
|
||||
from app.field_permission_cache import FieldPermissionCache
|
||||
|
||||
# 1. 删除该角色在该资源类型下的所有字段权限配置
|
||||
stmt = delete(cls.model).where(
|
||||
cls.model.role_id == data.role_id,
|
||||
cls.model.resource_type == data.resource_type
|
||||
)
|
||||
await db.execute(stmt)
|
||||
|
||||
# 2. 批量插入新的配置
|
||||
for config in data.configs:
|
||||
new_config = cls.model(
|
||||
role_id=data.role_id,
|
||||
resource_type=data.resource_type,
|
||||
field_name=config.field_name,
|
||||
permission_type=config.permission_type,
|
||||
mask_rule=config.mask_rule
|
||||
)
|
||||
db.add(new_config)
|
||||
|
||||
await db.commit()
|
||||
|
||||
# 3. 清除缓存
|
||||
await FieldPermissionCache.delete(data.role_id, data.resource_type)
|
||||
# 清除包含该角色的所有多角色缓存
|
||||
await FieldPermissionCache.delete_by_role(data.role_id)
|
||||
|
||||
@classmethod
|
||||
async def merge_field_permissions(
|
||||
cls,
|
||||
configs: List[ResourceFieldPermissionConfig],
|
||||
strategy: str = "most_permissive"
|
||||
) -> Dict[str, Dict]:
|
||||
"""
|
||||
合并多个角色的字段权限配置
|
||||
|
||||
Args:
|
||||
configs: 字段权限配置列表
|
||||
strategy: 合并策略
|
||||
- most_permissive: 最宽松(默认)
|
||||
- most_restrictive: 最严格
|
||||
|
||||
Returns:
|
||||
合并后的字段权限字典
|
||||
"""
|
||||
field_perms = {}
|
||||
|
||||
# 权限等级定义(数字越大权限越高)
|
||||
perm_levels = {
|
||||
'hidden': 0,
|
||||
'masked': 1,
|
||||
'read': 2,
|
||||
'write': 3
|
||||
}
|
||||
|
||||
for config in configs:
|
||||
field_name = config.field_name
|
||||
current_perm = config.permission_type
|
||||
current_level = perm_levels.get(current_perm, 0)
|
||||
|
||||
if field_name not in field_perms:
|
||||
field_perms[field_name] = {
|
||||
'permission': current_perm,
|
||||
'mask_rule': config.mask_rule,
|
||||
'level': current_level
|
||||
}
|
||||
else:
|
||||
existing_level = field_perms[field_name]['level']
|
||||
|
||||
if strategy == "most_permissive":
|
||||
# 取权限最高的
|
||||
if current_level > existing_level:
|
||||
field_perms[field_name] = {
|
||||
'permission': current_perm,
|
||||
'mask_rule': config.mask_rule,
|
||||
'level': current_level
|
||||
}
|
||||
elif strategy == "most_restrictive":
|
||||
# 取权限最低的
|
||||
if current_level < existing_level:
|
||||
field_perms[field_name] = {
|
||||
'permission': current_perm,
|
||||
'mask_rule': config.mask_rule,
|
||||
'level': current_level
|
||||
}
|
||||
|
||||
# 移除临时的 level 字段
|
||||
for field_name in field_perms:
|
||||
del field_perms[field_name]['level']
|
||||
|
||||
return field_perms
|
||||
Reference in New Issue
Block a user