import json from typing import Optional, List from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.core.security import get_current_user, require_role from app.core.permissions import check_instance_access, get_user_accessible_group_ids from app.models.user import User from app.models.instance import Instance from app.models.instance_group import user_groups from app.schemas.instance import InstanceCreate, InstanceUpdate, InstanceResponse, ConnectionTestResult from app.services.instance_service import InstanceService from app.core.crypto import encrypt_password, decrypt_password from app.services.audit_log_service import AuditLogService router = APIRouter(prefix="/instances", tags=["数据源管理"]) def _serialize_json_fields(instance: Instance, workflow_name: str = None) -> dict: """将 Instance 的 JSON text 字段反序列化为 dict/list,返回 Response dict""" d = { "id": instance.id, "name": instance.name, "db_type": instance.db_type, "host": instance.host, "port": instance.port, "username": instance.username, "database": instance.database, "env": instance.env, "description": instance.description, "status": instance.status, "version": instance.version, "access_mode": instance.access_mode or "readwrite", "workflow_id": instance.workflow_id, "workflow_name": workflow_name, "excluded_databases": json.loads(instance.excluded_databases) if instance.excluded_databases else None, "mask_fields": json.loads(instance.mask_fields) if instance.mask_fields else None, "created_at": instance.created_at, } return d async def _get_workflow_names(db: AsyncSession, instances: list) -> dict: """批量获取 workflow_id -> name 映射""" from app.models.workflow import WorkflowDefinition wids = list({i.workflow_id for i in instances if i.workflow_id}) if not wids: return {} result = await db.execute(select(WorkflowDefinition).where(WorkflowDefinition.id.in_(wids))) return {w.id: w.name for w in result.scalars().all()} @router.get("", response_model=List[InstanceResponse]) async def list_instances( db_type: Optional[str] = None, env: Optional[str] = None, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): stmt = select(Instance).order_by(Instance.id) if db_type: stmt = stmt.where(Instance.db_type == db_type) if env: stmt = stmt.where(Instance.env == env) # P0: 权限组过滤 — 非 admin 只能看到自己权限组内的 + 公开实例 if current_user.role != "admin": result = await db.execute( select(user_groups.c.group_id).where(user_groups.c.user_id == current_user.id) ) user_group_ids = [row[0] for row in result.all()] if user_group_ids: stmt = stmt.where( (Instance.group_id.in_(user_group_ids)) | (Instance.group_id.is_(None)) ) else: # 没有任何权限组,只能看公开实例 stmt = stmt.where(Instance.group_id.is_(None)) result = await db.execute(stmt) instances = result.scalars().all() wnames = await _get_workflow_names(db, instances) return [_serialize_json_fields(i, wnames.get(i.workflow_id)) for i in instances] @router.post("", response_model=InstanceResponse, status_code=201) async def create_instance( data: InstanceCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin", "dba")), ): instance = Instance( name=data.name, db_type=data.db_type, host=data.host, port=data.port, username=data.username, password=encrypt_password(data.password), database=data.database, env=data.env, description=data.description, access_mode=data.access_mode, workflow_id=data.workflow_id, excluded_databases=json.dumps(data.excluded_databases, ensure_ascii=False) if data.excluded_databases else None, mask_fields=json.dumps([m.model_dump() for m in data.mask_fields], ensure_ascii=False) if data.mask_fields else None, ) db.add(instance) await db.flush() await AuditLogService.log( db, user_id=current_user.id, action="CREATE_INSTANCE", target_type="instance", target_id=instance.id, detail=f"创建数据源: {data.name} ({data.db_type}@{data.host}:{data.port})", ) await db.commit() await db.refresh(instance) wnames = await _get_workflow_names(db, [instance]) return _serialize_json_fields(instance, wnames.get(instance.workflow_id)) @router.post("/test-connection", response_model=ConnectionTestResult) async def test_new_connection( data: InstanceCreate, current_user: User = Depends(require_role("admin", "dba")), ): """测试连接参数(不保存实例)""" fake = Instance( name=data.name, db_type=data.db_type, host=data.host, port=data.port, username=data.username, password=data.password, database=data.database, env=data.env, access_mode=data.access_mode, ) return await InstanceService.test_connection(fake) @router.get("/{instance_id}", response_model=InstanceResponse) async def get_instance( instance_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): result = await db.execute(select(Instance).where(Instance.id == instance_id)) instance = result.scalar_one_or_none() if not instance: raise HTTPException(status_code=404, detail="数据源不存在") # P0: 检查访问权限 await check_instance_access(current_user, instance, db) wnames = await _get_workflow_names(db, [instance]) return _serialize_json_fields(instance, wnames.get(instance.workflow_id)) @router.put("/{instance_id}", response_model=InstanceResponse) async def update_instance( instance_id: int, data: InstanceUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin", "dba")), ): result = await db.execute(select(Instance).where(Instance.id == instance_id)) instance = result.scalar_one_or_none() if not instance: raise HTTPException(status_code=404, detail="数据源不存在") update_data = data.model_dump(exclude_unset=True) if "password" in update_data: update_data["password"] = encrypt_password(update_data["password"]) # workflow_id: 0 means clear if "workflow_id" in update_data and update_data["workflow_id"] == 0: update_data["workflow_id"] = None if "excluded_databases" in update_data: update_data["excluded_databases"] = json.dumps(update_data["excluded_databases"], ensure_ascii=False) if update_data["excluded_databases"] else None if "mask_fields" in update_data: update_data["mask_fields"] = json.dumps([m if isinstance(m, dict) else m for m in update_data["mask_fields"]], ensure_ascii=False) if update_data["mask_fields"] else None for key, value in update_data.items(): setattr(instance, key, value) await db.commit() await db.refresh(instance) wnames = await _get_workflow_names(db, [instance]) return _serialize_json_fields(instance, wnames.get(instance.workflow_id)) @router.delete("/{instance_id}") async def delete_instance( instance_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin", "dba")), ): result = await db.execute(select(Instance).where(Instance.id == instance_id)) instance = result.scalar_one_or_none() if not instance: raise HTTPException(status_code=404, detail="数据源不存在") await AuditLogService.log( db, user_id=current_user.id, action="DELETE_INSTANCE", target_type="instance", target_id=instance.id, detail=f"删除数据源: {instance.name}", ) await db.delete(instance) await db.commit() return {"message": "数据源已删除"} @router.post("/{instance_id}/test", response_model=ConnectionTestResult) async def test_connection( instance_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin", "dba")), ): result = await db.execute(select(Instance).where(Instance.id == instance_id)) instance = result.scalar_one_or_none() if not instance: raise HTTPException(status_code=404, detail="数据源不存在") instance.password = decrypt_password(instance.password) return await InstanceService.test_connection(instance) @router.get("/{instance_id}/databases") async def list_databases( instance_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): result = await db.execute(select(Instance).where(Instance.id == instance_id)) instance = result.scalar_one_or_none() if not instance: raise HTTPException(status_code=404, detail="数据源不存在") # P0: 检查访问权限 await check_instance_access(current_user, instance, db) return await InstanceService.list_databases(instance) @router.get("/{instance_id}/databases/{database}/tables") async def list_tables( instance_id: int, database: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): result = await db.execute(select(Instance).where(Instance.id == instance_id)) instance = result.scalar_one_or_none() if not instance: raise HTTPException(status_code=404, detail="数据源不存在") # P0: 检查访问权限 await check_instance_access(current_user, instance, db) return await InstanceService.list_tables(instance, database)