Files
opsxsql/backend/app/api/v1/endpoints/rollback.py
T
logan 4c9427e546 sync: 同步 sql-platform-pro 最新代码 — 权限体系安全修复 v0.8.1
- P0: 实例访问控制全线接入权限组 (829测试)
- P1: permissions字段激活 + 操作级权限检查 (843测试)
- 前端: 删除数据权限页面, 权限组合并为唯一入口
- 新增: CodeMirror编辑器 + 查询模板 + 慢查询分析
- 新增: MongoDB管理页面 + Dashboard增强
- 修复: Pydantic V2迁移 + 前端TS错误修复
2026-05-23 14:41:06 +08:00

177 lines
5.2 KiB
Python

"""回滚 SQL API 路由 — 提供回滚 SQL 生成和预览功能。"""
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.crypto import decrypt_password
from app.models.user import User
from app.models.instance import Instance
from app.models.ticket import Ticket
from app.schemas.rollback import (
RollbackPreviewRequest,
RollbackPreviewResponse,
RollbackGenerateResponse,
RollbackGenerateRequest,
)
from app.services.rollback_generator import RollbackGenerator, RollbackGeneratorError
router = APIRouter(tags=["回滚 SQL"])
async def _get_ticket(db: AsyncSession, ticket_id: int) -> Ticket:
"""获取工单并检查是否存在。
Args:
db: 数据库会话
ticket_id: 工单ID
Returns:
工单对象
Raises:
HTTPException: 工单不存在
"""
result = await db.execute(select(Ticket).where(Ticket.id == ticket_id))
ticket = result.scalar_one_or_none()
if not ticket:
raise HTTPException(status_code=404, detail="工单不存在")
return ticket
async def _get_instance(db: AsyncSession, instance_id: int) -> Instance:
"""获取实例并检查是否存在。
Args:
db: 数据库会话
instance_id: 实例ID
Returns:
实例对象
Raises:
HTTPException: 实例不存在
"""
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="数据源不存在")
return instance
@router.post("/tickets/{ticket_id}/generate-rollback", response_model=RollbackGenerateResponse)
async def generate_rollback_sql(
ticket_id: int,
data: RollbackGenerateRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_role("admin", "dba")),
):
"""为工单生成回滚 SQL。
根据工单中的 SQL 内容,自动生成对应的回滚 SQL。
支持 INSERT/UPDATE/DELETE 三种 DML 操作。
Args:
ticket_id: 工单ID
data: 生成选项(是否自动保存)
db: 数据库会话
current_user: 当前用户
Returns:
生成的回滚 SQL 信息
Raises:
HTTPException: 工单不存在、无权访问或生成失败
"""
ticket = await _get_ticket(db, ticket_id)
# 获取关联的数据源实例
instance = await _get_instance(db, ticket.instance_id)
instance.password = decrypt_password(instance.password)
# 生成回滚 SQL
generator = RollbackGenerator()
try:
rollback_sql = await generator.generate(
instance, ticket.target_database, ticket.sql_content
)
except RollbackGeneratorError as e:
raise HTTPException(status_code=400, detail=str(e))
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"回滚 SQL 生成失败: {str(e)}")
# 自动保存到工单
saved = False
if data.auto_save:
ticket.rollback_sql = rollback_sql
await db.commit()
saved = True
# 检测 SQL 类型
from app.services.sql_analyzer import SQLAnalyzer
analyzer = SQLAnalyzer()
sql_type = analyzer._detect_sql_type(ticket.sql_content)
return RollbackGenerateResponse(
ticket_id=ticket_id,
original_sql=ticket.sql_content,
rollback_sql=rollback_sql,
sql_type=sql_type.value if sql_type else None,
saved=saved,
)
@router.post("/rollback/preview", response_model=RollbackPreviewResponse)
async def preview_rollback_sql(
req: RollbackPreviewRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""预览回滚 SQL(不保存)。
为给定的 SQL 语句生成回滚 SQL 预览,不执行实际数据库操作,
也不保存到任何工单。
Args:
req: 预览请求(实例ID、数据库、SQL)
db: 数据库会话
current_user: 当前用户
Returns:
回滚 SQL 预览
Raises:
HTTPException: 实例不存在或无权限
"""
instance = await _get_instance(db, req.instance_id)
instance.password = decrypt_password(instance.password)
generator = RollbackGenerator()
try:
rollback_sql = await generator.preview(instance, req.database, req.sql)
except Exception as e:
raise HTTPException(status_code=500, detail=f"回滚 SQL 预览失败: {str(e)}")
# 检测 SQL 类型
from app.services.sql_analyzer import SQLAnalyzer
analyzer = SQLAnalyzer()
sql_type = analyzer._detect_sql_type(req.sql)
warning = None
if rollback_sql.startswith("--"):
# 提取警告信息
lines = rollback_sql.split("\n")
warning_lines = [l for l in lines if l.startswith("--") and "无法" in l]
if warning_lines:
warning = warning_lines[0].replace("-- ", "")
return RollbackPreviewResponse(
original_sql=req.sql,
rollback_sql=rollback_sql,
sql_type=sql_type.value if sql_type else None,
warning=warning,
)