Files
opsxsql/数据审核平台菜单优化/app/OPTIMIZATION_GUIDE.md
T
loganandClaude Opus 4.7 439155c92e Initial commit: SQL audit platform v0.1.0
FastAPI backend with auth, user, instance, ticket, query, dashboard APIs.
React + TypeScript frontend with mock data UI (10 pages).
SQLite + async SQLAlchemy, JWT auth, role-based access control.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 11:11:57 +08:00

67 KiB
Raw Blame History

DataGuard 数据库变更审核平台 — 全面系统性优化指南

版本: v3.0 | 日期: 2026-05-20
范围: FastAPI 后端 + Vue3 前端,支持 MySQL / PostgreSQL / MongoDB / Redis


1. 总体架构优化建议

1.1 FastAPI 后端最佳项目结构

backend/
├── app/
│   ├── __init__.py
│   ├── main.py                    # FastAPI 应用入口,注册 router/middleware
│   ├── core/
│   │   ├── config.py              # Pydantic Settings 多环境配置
│   │   ├── exceptions.py          # 全局异常定义
│   │   ├── security.py            # JWT / OAuth2 / 密码哈希
│   │   ├── permissions.py         # RBAC 权限校验装饰器
│   │   └── database.py            # 多数据库连接池管理
│   ├── api/
│   │   ├── __init__.py
│   │   └── v1/
│   │       ├── __init__.py
│   │       ├── router.py          # APIRouter 聚合
│   │       ├── endpoints/
│   │       │   ├── auth.py
│   │       │   ├── user.py
│   │       │   ├── role.py
│   │       │   ├── instance.py
│   │       │   ├── ticket.py
│   │       │   ├── query.py
│   │       │   ├── audit_rule.py
│   │       │   ├── workflow.py
│   │       │   ├── config.py
│   │       │   ├── task.py
│   │       │   └── dashboard.py
│   ├── models/
│   │   ├── __init__.py
│   │   ├── user.py                # SQLAlchemy Model
│   │   ├── instance.py
│   │   ├── ticket.py
│   │   └── audit_log.py
│   ├── schemas/
│   │   ├── __init__.py
│   │   ├── base.py                # 通用 Pydantic Schema
│   │   ├── user.py                # Request / Response DTO
│   │   ├── ticket.py
│   │   └── instance.py
│   ├── services/
│   │   ├── __init__.py
│   │   ├── ticket_service.py      # 业务逻辑层
│   │   ├── query_service.py
│   │   ├── instance_service.py
│   │   └── audit_service.py
│   ├── crud/
│   │   ├── __init__.py
│   │   ├── base.py                # 通用 CRUD 基类
│   │   ├── user.py
│   │   └── ticket.py
│   ├── dependencies/
│   │   ├── __init__.py
│   │   ├── auth_dep.py            # get_current_user / require_role
│   │   └── db_dep.py              # get_db / get_redis
│   ├── tasks/
│   │   ├── __init__.py
│   │   └── celery_tasks.py        # 异步任务定义
│   ├── utils/
│   │   ├── sql_parser.py          # SQL AST 解析
│   │   ├── sql_rollback.py        # Binlog 回滚生成
│   │   ├── data_mask.py           # 数据脱敏
│   │   ├── notify.py              # 多渠道通知
│   │   └── audit_logger.py        # 审计日志记录
│   └── plugins/
│       ├── mysql_driver.py        # MySQL 连接封装
│       ├── pg_driver.py           # PostgreSQL 连接封装
│       ├── mongo_driver.py        # MongoDB 连接封装
│       └── redis_driver.py        # Redis 连接封装
├── migrations/                    # Alembic 数据库迁移
├── tests/
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml
├── alembic.ini
└── .env / .env.test / .env.prod

核心原则:

  • APIRouter 按模块拆分,v1/router.py 统一聚合
  • 依赖注入: Depends(get_db), Depends(require_role("admin"))
  • Clean Architecture: API -> Service -> CRUD -> Model,禁止跨层调用
  • 异步优先: 所有 IO 操作使用 async/await

1.2 Vue3 前端架构优化

frontend/
├── src/
│   ├── main.ts
│   ├── App.vue
│   ├── router/
│   │   ├── index.ts               # 路由定义 + 路由守卫
│   │   └── guards.ts              # 权限守卫 / 登录守卫
│   ├── stores/
│   │   ├── auth.ts                # Pinia: 用户/权限/菜单
│   │   ├── ticket.ts              # Pinia: 工单状态
│   │   ├── instance.ts            # Pinia: 数据源状态
│   │   └── config.ts              # Pinia: 全局配置
│   ├── api/
│   │   ├── request.ts             # axios 封装 + 拦截器
│   │   ├── auth.ts                # API 接口定义
│   │   ├── ticket.ts
│   │   ├── instance.ts
│   │   └── ...
│   ├── composables/
│   │   ├── useAuth.ts             # 权限相关组合式函数
│   │   ├── usePermission.ts
│   │   ├── useTable.ts            # 表格分页/筛选/排序通用逻辑
│   │   └── useNotification.ts
│   ├── views/
│   │   ├── dashboard/
│   │   ├── tickets/
│   │   ├── queries/
│   │   ├── instances/
│   │   ├── workflow/
│   │   ├── config/
│   │   └── system/
│   ├── components/
│   │   ├── common/                # 通用组件
│   │   │   ├── PageHeader.vue
│   │   │   ├── DataTable.vue      # 通用表格(分页/排序/筛选)
│   │   │   ├── FilterBar.vue
│   │   │   ├── EmptyState.vue
│   │   │   └── StatCards.vue
│   │   ├── sql-editor/            # SQL 编辑器相关
│   │   │   └── MonacoEditor.vue
│   │   └── workflow/              # 审批流程可视化
│   │       └── FlowChart.vue
│   ├── types/
│   │   └── api.ts                 # API 类型定义(与 Pydantic 同步)
│   └── utils/
│       ├── formatter.ts
│       └── constants.ts

状态管理策略:

  • Pinia: 全局状态(用户、权限、菜单、配置)
  • TanStack Query (Vue Query): 服务端状态管理,自动缓存/刷新/去重
// stores/auth.ts - Pinia 示例
import { defineStore } from 'pinia';

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null as User | null,
    token: localStorage.getItem('token') || '',
    permissions: [] as string[],
  }),
  getters: {
    isAdmin: (s) => s.user?.role === 'admin',
    isDBA: (s) => s.user?.role === 'dba',
  },
  actions: {
    async login(credentials: LoginForm) {
      const res = await apiAuth.login(credentials);
      this.user = res.user;
      this.token = res.token;
      this.permissions = res.permissions;
      localStorage.setItem('token', res.token);
    },
    hasPermission(action: string): boolean {
      return this.permissions.includes(action);
    },
  },
});

1.3 前后端类型同步方案

使用 Pydantic + OpenAPI 自动生成前端类型:

# backend/app/schemas/ticket.py
from pydantic import BaseModel, Field
from datetime import datetime
from enum import Enum

class TicketStatus(str, Enum):
    PENDING = "pending"
    REVIEWING = "reviewing"
    APPROVED = "approved"
    REJECTED = "rejected"
    EXECUTING = "executing"
    SUCCESS = "success"
    FAILED = "failed"

class TicketCreate(BaseModel):
    title: str = Field(..., min_length=5, max_length=200)
    db_type: str = Field(..., pattern="^(mysql|postgres|mongo|redis)$")
    instance_id: int
    sql_content: str = Field(..., min_length=1)
    rollback_sql: str | None = None
    reason: str = Field(..., min_length=10)
    priority: str = "medium"

class TicketResponse(TicketCreate):
    id: int
    status: TicketStatus
    applicant_id: int
    applicant_name: str
    create_time: datetime
    workflow_steps: list[dict] | None = None

    class Config:
        from_attributes = True

前端类型生成命令:

# 使用 openapi-typescript 从 FastAPI OpenAPI 文档自动生成
npx openapi-typescript http://localhost:8000/openapi.json --output src/types/api.ts

1.4 多环境隔离与多租户

# backend/app/core/config.py
from pydantic_settings import BaseSettings
from functools import lru_cache

class Settings(BaseSettings):
    ENV: str = "development"        # development / testing / production
    DEBUG: bool = True
    
    # JWT
    SECRET_KEY: str
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
    
    # MySQL
    MYSQL_HOST: str = "localhost"
    MYSQL_PORT: int = 3306
    MYSQL_USER: str
    MYSQL_PASSWORD: str
    MYSQL_DB: str
    
    # PostgreSQL
    POSTGRES_HOST: str = "localhost"
    POSTGRES_PORT: int = 5432
    POSTGRES_USER: str
    POSTGRES_PASSWORD: str
    POSTGRES_DB: str
    
    # MongoDB
    MONGO_URI: str = "mongodb://localhost:27017"
    MONGO_DB: str = "dataguard"
    
    # Redis (平台缓存 + Celery Broker)
    REDIS_URL: str = "redis://localhost:6379/0"
    
    # Celery
    CELERY_BROKER_URL: str = "redis://localhost:6379/1"
    CELERY_RESULT_BACKEND: str = "redis://localhost:6379/2"
    
    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"

@lru_cache()
def get_settings() -> Settings:
    return Settings()

多租户方案:

  • 共享数据库 + 租户ID隔离: 所有表增加 tenant_id 字段
  • Schema 隔离: PostgreSQL 支持每租户一个 Schema
  • 数据源隔离: 每租户独立的数据源配置表,操作自带 tenant_id 过滤
# 全局租户上下文
from contextvars import ContextVar

tenant_ctx: ContextVar[int] = ContextVar('tenant_id', default=0)

# CRUD 基类自动注入 tenant_id 过滤
class CRUDBase:
    async def get_multi(self, db: AsyncSession, *, skip=0, limit=100):
        tenant_id = tenant_ctx.get()
        stmt = select(self.model).where(self.model.tenant_id == tenant_id)
        ...

2. 核心功能模块深度优化

2.1 数据源管理

当前不足: 前端展示为主,缺少后端连接池管理、健康检查、自动重连。

优化点:

优化项 说明
连接池管理 每实例独立连接池,支持 max/min 连接数配置
健康检查 每 30s 探活,连续 3 次失败标记为断开
自动重连 指数退避策略重连
密码加密 实例密码 AES 加密存储
连接复用 查询审计复用只读连接,工单执行使用独立连接

代码结构:

# backend/app/core/database.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from motor.motor_asyncio import AsyncIOMotorClient
import aioredis

class ConnectionPoolManager:
    _pools: dict[int, Any] = {}    # instance_id -> engine/client
    
    async def get_mysql_engine(self, instance_id: int):
        if instance_id not in self._pools:
            cfg = await self._get_instance_config(instance_id)
            engine = create_async_engine(
                f"mysql+aiomysql://{cfg.user}:{cfg.password}@{cfg.host}:{cfg.port}/{cfg.db}",
                pool_size=5, max_overflow=10, pool_recycle=3600,
                echo=False,
            )
            self._pools[instance_id] = engine
        return self._pools[instance_id]
    
    async def get_mongo_client(self, instance_id: int):
        if instance_id not in self._pools:
            cfg = await self._get_instance_config(instance_id)
            client = AsyncIOMotorClient(
                f"mongodb://{cfg.user}:{cfg.password}@{cfg.host}:{cfg.port}",
                maxPoolSize=10, minPoolSize=1,
            )
            self._pools[instance_id] = client
        return client
    
    async def health_check(self, instance_id: int) -> bool:
        """探活检查,连续失败3次标记 offline"""
        ...

pool_mgr = ConnectionPoolManager()

2.2 SQL 工单全生命周期

当前不足: 审批流程硬编码,缺少自动审核引擎、回滚 SQL 自动生成。

优化点:

提交(Submit) 
  -> 自动审核(AutoAudit: 语法检查 + 规则匹配 + 影响分析) 
  -> 人工审批(ManualReview: 多级审批流) 
  -> 执行(Execute: 预检查 + 执行 + 结果记录) 
  -> 回滚(Rollback: Binlog逆向 + 备份恢复)

状态机设计:

# backend/app/services/ticket_fsm.py
from enum import Enum, auto

class TicketState(str, Enum):
    DRAFT = "draft"
    AUTO_AUDITING = "auto_auditing"
    AUTO_AUDIT_FAILED = "auto_audit_failed"
    PENDING_REVIEW = "pending_review"
    REVIEWING = "reviewing"
    APPROVED = "approved"
    REJECTED = "rejected"
    PRE_EXECUTE = "pre_execute"
    EXECUTING = "executing"
    SUCCESS = "success"
    FAILED = "failed"
    ROLLING_BACK = "rolling_back"
    ROLLED_BACK = "rolled_back"

class TicketFSM:
    TRANSITIONS = {
        TicketState.DRAFT: [TicketState.AUTO_AUDITING],
        TicketState.AUTO_AUDITING: [TicketState.PENDING_REVIEW, TicketState.AUTO_AUDIT_FAILED],
        TicketState.AUTO_AUDIT_FAILED: [TicketState.DRAFT],  # 可修改重提
        TicketState.PENDING_REVIEW: [TicketState.REVIEWING],
        TicketState.REVIEWING: [TicketState.APPROVED, TicketState.REJECTED],
        TicketState.APPROVED: [TicketState.PRE_EXECUTE],
        TicketState.PRE_EXECUTE: [TicketState.EXECUTING],
        TicketState.EXECUTING: [TicketState.SUCCESS, TicketState.FAILED],
        TicketState.FAILED: [TicketState.ROLLING_BACK],
        TicketState.ROLLING_BACK: [TicketState.ROLLED_BACK],
    }
    
    def can_transition(self, current: TicketState, target: TicketState) -> bool:
        return target in self.TRANSITIONS.get(current, [])
    
    async def transition(self, ticket_id: int, target: TicketState, operator_id: int):
        ticket = await crud.ticket.get(db, ticket_id)
        if not self.can_transition(ticket.status, target):
            raise BusinessException(f"非法状态流转: {ticket.status} -> {target}")
        ...

自动审核引擎:

# backend/app/services/auto_audit.py
class AutoAuditEngine:
    """自动审核引擎:语法解析 + 规则匹配 + 影响分析"""
    
    async def audit(self, ticket_id: int) -> AuditResult:
        ticket = await crud.ticket.get(db, ticket_id)
        
        # 1. SQL 语法解析
        statements = sqlparse.parse(ticket.sql_content)
        for stmt in statements:
            if stmt.get_type() == 'UNKNOWN':
                return AuditResult(passed=False, score=0, issues=["SQL 语法错误"])
        
        # 2. 规则匹配
        rules = await crud.audit_rule.get_active_rules(db, db_type=ticket.db_type)
        issues = []
        for rule in rules:
            result = await self._check_rule(rule, ticket.sql_content)
            if not result.passed:
                issues.append(result)
        
        # 3. 影响行数估算 (MySQL/PostgreSQL)
        if ticket.db_type in ('mysql', 'postgres'):
            explain = await self._explain_sql(ticket)
            estimated_rows = explain.get('rows', 0)
            if estimated_rows > 10000:
                issues.append(AuditIssue(
                    level='warning', 
                    message=f'预估影响 {estimated_rows} 行,建议分批执行'
                ))
        
        # 4. 生成回滚 SQL (MySQL Binlog)
        if ticket.db_type == 'mysql':
            rollback_sql = await self._generate_rollback(ticket)
            await crud.ticket.update(db, ticket_id, rollback_sql=rollback_sql)
        
        score = max(0, 100 - len([i for i in issues if i.level == 'error']) * 20 
                          - len([i for i in issues if i.level == 'warning']) * 5)
        
        return AuditResult(
            passed=score >= 60 and not any(i.level == 'error' for i in issues),
            score=score,
            issues=issues,
        )

回滚 SQL 生成 (MySQL):

# backend/app/utils/sql_rollback.py
class RollbackGenerator:
    """基于 Binlog ROW 格式生成逆向 SQL"""
    
    async def generate_for_update(self, instance_id: int, table: str, 
                                   where_clause: str) -> str:
        """
        1. 连接 MySQL 读取 Binlog(或直接从表读取变更前数据)
        2. 使用 PRIMARY KEY 定位行
        3. 生成 UPDATE ... SET col=old_val WHERE pk=val
        """
        engine = await pool_mgr.get_mysql_engine(instance_id)
        async with engine.connect() as conn:
            # 获取表结构 + 主键
            pk = await self._get_primary_key(conn, table)
            
            # 读取变更前数据(通过 Flashback 或备份表)
            backup_table = f"_dg_backup_{table}_{int(time.time())}"
            await conn.execute(text(f"CREATE TABLE {backup_table} AS SELECT * FROM {table} WHERE {where_clause}"))
            
            # 生成回滚 SQL
            rollback = f"""-- 回滚 SQL(基于 Binlog ROW 解析)
-- 备份表: {backup_table}
-- 恢复方式: 从备份表还原
INSERT INTO {table} SELECT * FROM {backup_table} 
ON DUPLICATE KEY UPDATE 
  c1=VALUES(c1), c2=VALUES(c2);
"""
            return rollback
    
    async def generate_for_delete(self, instance_id: int, table: str,
                                   where_clause: str) -> str:
        """DELETE 回滚 = INSERT 被删数据"""
        ...

2.3 在线 SQL 查询器

当前不足: textarea 编辑,无语法高亮、无自动补全。

优化方案:

// frontend/src/components/sql-editor/MonacoEditor.vue
<template>
  <div ref="editorRef" class="sql-editor" />
</template>

<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import * as monaco from 'monaco-editor';
import { setupMySQLLanguage } from './mysql-language';
import { setupPGLanguage } from './pg-language';

const props = defineProps<{
  modelValue: string;
  dbType: 'mysql' | 'postgres' | 'mongo' | 'redis';
}>();

const editorRef = ref<HTMLElement>();
let editor: monaco.editor.IStandaloneCodeEditor;

onMounted(() => {
  // 注册数据库语法高亮
  if (props.dbType === 'mysql') setupMySQLLanguage();
  if (props.dbType === 'postgres') setupPGLanguage();
  
  editor = monaco.editor.create(editorRef.value!, {
    value: props.modelValue,
    language: props.dbType === 'mysql' ? 'mysql' : props.dbType === 'postgres' ? 'pgsql' : 'sql',
    theme: 'vs-dark',
    minimap: { enabled: false },
    automaticLayout: true,
    fontSize: 13,
    fontFamily: 'JetBrains Mono, monospace',
    suggestOnTriggerCharacters: true,
    quickSuggestions: true,
    wordBasedSuggestions: true,
  });
  
  // 自动补全:表名/字段名
  monaco.languages.registerCompletionItemProvider('mysql', {
    triggerCharacters: ['.', ' '],
    provideCompletionItems: async (model, position) => {
      // 从后端获取当前库的表名和字段名
      const suggestions = await fetchSchemaSuggestions(props.dbType);
      return { suggestions };
    },
  });
});
</script>

后端查询执行:

# backend/app/services/query_service.py
class QueryService:
    async def execute_query(self, instance_id: int, sql: str, user_id: int,
                            max_rows: int = 10000) -> QueryResult:
        instance = await crud.instance.get(db, instance_id)
        
        # 1. 权限检查
        if not await self._can_query(user_id, instance_id):
            raise PermissionDenied("无权访问该数据源")
        
        # 2. SQL 拦截检查
        forbidden_keywords = ['DROP', 'TRUNCATE', 'DELETE', 'UPDATE', 'INSERT', 'CREATE', 'ALTER']
        upper_sql = sql.strip().upper()
        if any(kw in upper_sql for kw in forbidden_keywords):
            raise BusinessException("查询模式禁止执行写操作")
        
        # 3. 添加 LIMIT
        if not re.search(r'LIMIT\s+\d+', upper_sql):
            sql = f"{sql.rstrip(';')} LIMIT {max_rows};"
        
        # 4. 执行(带超时)
        start = time.time()
        engine = await pool_mgr.get_mysql_engine(instance_id)
        async with engine.connect() as conn:
            result = await asyncio.wait_for(
                conn.execute(text(sql)), timeout=30
            )
            rows = result.mappings().all()
            columns = list(result.keys())
        elapsed = int((time.time() - start) * 1000)
        
        # 5. 敏感数据脱敏
        masked_rows = await self._mask_sensitive_data(instance_id, columns, rows)
        
        # 6. 记录审计日志
        await audit_logger.log_query(user_id, instance_id, sql, elapsed, len(rows))
        
        return QueryResult(columns=columns, rows=masked_rows, elapsed_ms=elapsed)

2.4 数据字典

【补充需求】需确认是否需要自动采集数据字典(从 information_schema 读取)还是手动维护。

# backend/app/services/data_dict.py
class DataDictionaryService:
    async def sync_mysql_schema(self, instance_id: int):
        """从 MySQL information_schema 自动同步表结构"""
        engine = await pool_mgr.get_mysql_engine(instance_id)
        async with engine.connect() as conn:
            # 获取所有表
            tables = await conn.execute(text("""
                SELECT TABLE_NAME, TABLE_COMMENT 
                FROM information_schema.TABLES 
                WHERE TABLE_SCHEMA = DATABASE()
            """))
            
            for table in tables.mappings():
                # 获取字段
                columns = await conn.execute(text(f"""
                    SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT,
                           IS_NULLABLE, COLUMN_DEFAULT, COLUMN_KEY
                    FROM information_schema.COLUMNS
                    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '{table.TABLE_NAME}'
                """))
                
                # 保存到数据字典表
                await crud.data_dict.save_table(instance_id, table, columns)

2.5 Redis 专用管理功能

当前不足: Redis 与关系数据库混用同一套界面,缺少 Redis 特有的管理功能。

优化点:

功能 说明
Key 浏览 树形结构展示,支持按 pattern 过滤
Key 操作 GET / SET / DEL / EXPIRE / TTL
数据结构支持 String / Hash / List / Set / ZSet
实时监控 内存使用、命中率、QPS、慢查询
批量操作 批量删除、批量设置 TTL
序列化识别 自动识别 JSON / Protobuf / 压缩数据
# backend/app/plugins/redis_driver.py
class RedisManager:
    async def get_keys(self, instance_id: int, pattern: str = "*", 
                       cursor: int = 0, count: int = 100) -> dict:
        client = await pool_mgr.get_redis_client(instance_id)
        cur, keys = await client.scan(cursor=cursor, match=pattern, count=count)
        
        results = []
        for key in keys:
            key_type = await client.type(key)
            ttl = await client.ttl(key)
            memory = await client.memory_usage(key) or 0
            results.append({
                'key': key.decode(),
                'type': key_type.decode(),
                'ttl': ttl,
                'memory': memory,
            })
        return {'cursor': cur, 'keys': results}
    
    async def get_value(self, instance_id: int, key: str) -> dict:
        """根据类型返回格式化后的值"""
        client = await pool_mgr.get_redis_client(instance_id)
        key_type = (await client.type(key)).decode()
        
        handlers = {
            'string': lambda: client.get(key),
            'hash': lambda: client.hgetall(key),
            'list': lambda: client.lrange(key, 0, -1),
            'set': lambda: client.smembers(key),
            'zset': lambda: client.zrange(key, 0, -1, withscores=True),
        }
        
        raw = await handlers[key_type]()
        return {'type': key_type, 'value': self._deserialize(raw)}
    
    def _deserialize(self, raw) -> Any:
        """自动识别并反序列化 JSON / 压缩数据"""
        if isinstance(raw, bytes):
            try:
                return json.loads(raw.decode('utf-8'))
            except (json.JSONDecodeError, UnicodeDecodeError):
                return raw.decode('utf-8', errors='replace')
        if isinstance(raw, dict):
            return {k.decode() if isinstance(k, bytes) else k: 
                    self._deserialize(v) for k, v in raw.items()}
        if isinstance(raw, (list, set, tuple)):
            return [self._deserialize(v) for v in raw]
        return raw

2.6 仪表盘与监控

优化点:

  • 工单趋势(日/周/月)
  • 状态分布饼图
  • 慢查询 TOP10
  • 数据源健康状态
  • 待办审批提醒
# backend/app/services/dashboard.py
class DashboardService:
    async def get_stats(self, tenant_id: int) -> DashboardStats:
        # 使用缓存加速
        cache_key = f"dashboard:stats:{tenant_id}"
        cached = await redis.get(cache_key)
        if cached:
            return DashboardStats.parse_raw(cached)
        
        stats = DashboardStats(
            ticket_total=await crud.ticket.count(db, tenant_id=tenant_id),
            ticket_pending=await crud.ticket.count(db, status='pending', tenant_id=tenant_id),
            ticket_month=await crud.ticket.count(db, 
                create_time__gte=datetime.now() - timedelta(days=30), tenant_id=tenant_id),
            ticket_executing=await crud.ticket.count(db, status='executing', tenant_id=tenant_id),
            rules_total=await crud.audit_rule.count(db, tenant_id=tenant_id),
            rules_active=await crud.audit_rule.count(db, enabled=True, tenant_id=tenant_id),
            queries_today=await crud.query_audit.count(db,
                timestamp__gte=datetime.now() - timedelta(days=1), tenant_id=tenant_id),
        )
        
        await redis.setex(cache_key, 60, stats.json())
        return stats

2.7 审核规则引擎

当前不足: 规则硬编码,不支持自定义和版本控制。

优化方案: 规则引擎支持自定义 + 版本控制。

# backend/app/models/audit_rule.py
class AuditRule(Base):
    __tablename__ = "audit_rules"
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100), nullable=False)
    db_type = Column(String(20), nullable=False)    # mysql / postgres / mongo / redis / all
    rule_type = Column(String(30), nullable=False)  # regex / ast / risk / custom
    pattern = Column(Text)                          # 正则/AST模式
    action = Column(String(20), default="warn")     # block / warn / require_approval
    description = Column(Text)
    enabled = Column(Boolean, default=True)
    version = Column(Integer, default=1)            # 版本号
    priority = Column(Integer, default=100)         # 优先级,越小越先匹配
    created_by = Column(Integer, ForeignKey("users.id"))
    created_at = Column(DateTime, default=datetime.now)
    updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
    
    # 版本历史
    history = relationship("AuditRuleHistory", back_populates="rule")

class AuditRuleHistory(Base):
    __tablename__ = "audit_rule_history"
    
    id = Column(Integer, primary_key=True)
    rule_id = Column(Integer, ForeignKey("audit_rules.id"))
    version = Column(Integer)
    pattern = Column(Text)
    action = Column(String(20))
    changed_by = Column(Integer)
    changed_at = Column(DateTime, default=datetime.now)
    change_reason = Column(Text)

规则引擎执行:

# backend/app/services/rule_engine.py
class RuleEngine:
    async def check(self, sql: str, db_type: str) -> RuleCheckResult:
        rules = await crud.audit_rule.get_active(db, db_type=db_type, 
                                                  order_by=AuditRule.priority)
        violations = []
        
        for rule in rules:
            matched = False
            
            if rule.rule_type == 'regex':
                matched = re.search(rule.pattern, sql, re.IGNORECASE)
            
            elif rule.rule_type == 'ast':
                matched = self._check_ast(sql, rule.pattern)
            
            elif rule.rule_type == 'risk':
                matched = await self._check_risk(sql, db_type, rule.pattern)
            
            elif rule.rule_type == 'custom':
                # 调用用户自定义 Python 函数
                matched = await self._exec_custom_rule(rule.pattern, sql, db_type)
            
            if matched:
                violations.append(RuleViolation(
                    rule_id=rule.id,
                    rule_name=rule.name,
                    action=rule.action,
                    message=rule.description,
                ))
                
                if rule.action == 'block':
                    break  # 拦截规则直接终止
        
        return RuleCheckResult(
            passed=not any(v.action == 'block' for v in violations),
            violations=violations,
        )

内置规则列表:

规则ID 名称 类型 动作 适用数据库
R001 禁止无 WHERE 的 UPDATE/DELETE ast block mysql, postgres
R002 禁止 SELECT * regex warn mysql, postgres
R003 禁止 DROP TABLE/DATABASE regex block mysql, postgres
R004 禁止 TRUNCATE TABLE regex block mysql, postgres
R005 大表变更检查 (>100万行) risk warn mysql, postgres
R006 禁止执行时间超过 10s risk block mysql, postgres
R007 MongoDB 禁止 db.dropDatabase() regex block mongo
R008 Redis 禁止 FLUSHALL/FLUSHDB regex block redis
R009 禁止无索引查询 (type=ALL) risk warn mysql, postgres
R010 UPDATE/DELETE 必须提供回滚 SQL ast require_approval mysql, postgres

3. 安全与合规强化

3.1 SQL 注入防护

# 后端:严格使用参数化查询,禁止字符串拼接 SQL
# ✅ 正确
result = await conn.execute(
    text("SELECT * FROM users WHERE id = :user_id"),
    {"user_id": user_id}
)

# ❌ 错误 — 绝对禁止
result = await conn.execute(f"SELECT * FROM users WHERE id = {user_id}")

# 前端:所有用户输入做转义
import html
escaped_input = html.escape(user_input)

3.2 不同数据库的权限最小化

数据库 查询账号 执行账号
MySQL SELECT, SHOW SELECT, INSERT, UPDATE, DELETE + 特定表
PostgreSQL CONNECT, USAGE, SELECT 按需 schema 级别授权
MongoDB find insert, update, remove + 特定 collection
Redis 只读实例 / ACL 限制命令集 按需配置可执行命令
# 执行前权限校验
async def check_execute_permission(instance_id: int, sql: str, user_id: int):
    # 1. 解析 SQL 类型
    stmt_type = sqlparse.parse(sql)[0].get_type()
    
    # 2. 检查用户是否有该实例的执行权限
    if not await rbac.check_instance_permission(user_id, instance_id, stmt_type):
        raise PermissionDenied(f"无权在实例 {instance_id} 上执行 {stmt_type}")
    
    # 3. 行级权限检查
    tables = extract_tables(sql)
    for table in tables:
        if not await rbac.check_table_permission(user_id, instance_id, table):
            raise PermissionDenied(f"无权访问表 {table}")

3.3 敏感数据脱敏

# backend/app/utils/data_mask.py
class DataMasker:
    RULES = [
        {'pattern': r'^1[3-9]\d{9}$', 'type': 'phone', 'mask': lambda v: v[:3]+'****'+v[-4:]},
        {'pattern': r'^[\w.-]+@[\w.-]+\.\w+$', 'type': 'email', 'mask': lambda v: v[:3]+'***@'+v.split('@')[1]},
        {'pattern': r'^\d{17}[\dXx]$', 'type': 'id_card', 'mask': lambda v: v[:6]+'********'+v[-4:]},
        {'pattern': r'^\d{16,19}$', 'type': 'bank_card', 'mask': lambda v: '**** **** **** '+v[-4:]},
    ]
    
    COLUMN_PATTERNS = {
        'phone': ['phone', 'mobile', 'tel'],
        'email': ['email', 'e_mail', 'mail'],
        'id_card': ['id_card', 'idcard', 'identity', 'ssn'],
        'bank_card': ['bank_card', 'bankcard', 'card_no'],
    }
    
    async def mask_value(self, column_name: str, value: str) -> str:
        col_lower = column_name.lower()
        for data_type, keywords in self.COLUMN_PATTERNS.items():
            if any(kw in col_lower for kw in keywords):
                rule = next(r for r in self.RULES if r['type'] == data_type)
                return rule['mask'](str(value))
        return value
    
    async def mask_result(self, columns: list[str], rows: list[dict]) -> list[dict]:
        to_mask = {i: None for i, col in enumerate(columns) 
                   if any(kw in col.lower() for kw in sum(self.COLUMN_PATTERNS.values(), []))}
        
        for row in rows:
            for col_idx in to_mask:
                col_name = columns[col_idx]
                val = str(row.get(col_name, ''))
                for rule in self.RULES:
                    if re.match(rule['pattern'], val):
                        row[col_name] = rule['mask'](val)
                        break
        return rows

3.4 操作审计日志(含变更前后对比)

# backend/app/utils/audit_logger.py
class AuditLogger:
    async def log_change(self, user_id: int, action: str, target_type: str,
                         target_id: int, before: dict, after: dict):
        """记录变更操作,包含前后数据对比"""
        diff = self._compute_diff(before, after)
        
        log = AuditLog(
            user_id=user_id,
            action=action,            # CREATE / UPDATE / DELETE / EXECUTE
            target_type=target_type,  # ticket / instance / user / query
            target_id=target_id,
            before_data=json.dumps(before, ensure_ascii=False, default=str),
            after_data=json.dumps(after, ensure_ascii=False, default=str),
            diff=json.dumps(diff, ensure_ascii=False),
            ip_address=RequestContext.get_ip(),
            user_agent=RequestContext.get_ua(),
            created_at=datetime.now(),
        )
        await db.add(log)
    
    def _compute_diff(self, before: dict, after: dict) -> list[dict]:
        """计算变更差异"""
        diff = []
        all_keys = set(before.keys()) | set(after.keys())
        for key in all_keys:
            old_val = before.get(key)
            new_val = after.get(key)
            if old_val != new_val:
                diff.append({
                    'field': key,
                    'old': old_val,
                    'new': new_val,
                })
        return diff

# 使用示例
@AuditLogger.track("工单审批")
async def approve_ticket(ticket_id: int, user_id: int, comment: str):
    before = await crud.ticket.get(db, ticket_id)
    after = await crud.ticket.update(db, ticket_id, status='approved', approver_id=user_id)
    await audit_logger.log_change(user_id, 'APPROVE', 'ticket', ticket_id, 
                                   before.__dict__, after.__dict__)

3.5 RBAC + 行级/字段级数据权限

# backend/app/core/permissions.py
class RBAC:
    # ========== 行级权限 ==========
    async def check_instance_permission(self, user_id: int, instance_id: int) -> bool:
        """用户是否有该实例的访问权限"""
        user = await crud.user.get(db, user_id)
        if user.role == 'admin':
            return True
        # 检查用户-实例绑定表
        return await crud.user_instance.exists(db, user_id=user_id, instance_id=instance_id)
    
    # ========== 字段级权限 ==========
    async def filter_columns(self, user_id: int, instance_id: int, 
                              table: str, columns: list[str]) -> list[str]:
        """过滤用户无权查看的字段"""
        user = await crud.user.get(db, user_id)
        if user.role in ('admin', 'dba'):
            return columns
        
        # 查询字段权限
        allowed = await crud.column_permission.get_allowed(
            db, user_id=user_id, instance_id=instance_id, table=table
        )
        return [c for c in columns if c in allowed]
    
    # ========== 数据范围权限 ==========
    async def get_data_scope(self, user_id: int) -> DataScope:
        """获取用户的数据范围权限(全部/本部门/本人)"""
        user = await crud.user.get(db, user_id)
        scope_map = {
            'admin': DataScope.ALL,
            'dba': DataScope.ALL,
            'developer': DataScope.SELF,
            'guest': DataScope.SELF,
        }
        return scope_map.get(user.role, DataScope.NONE)

# 装饰器封装
from functools import wraps

def require_role(*roles: str):
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            current_user = kwargs.get('current_user')
            if not current_user or current_user.role not in roles:
                raise PermissionDenied(f"需要角色: {', '.join(roles)}")
            return await func(*args, **kwargs)
        return wrapper
    return decorator

# API 使用
@router.post("/tickets/{ticket_id}/approve")
async def approve_ticket(
    ticket_id: int,
    current_user: User = Depends(get_current_user),
):
    # 自动校验 role in ('admin', 'dba')
    require_role('admin', 'dba')
    ...

3.6 JWT + OAuth2 最佳实践

# backend/app/core/security.py
from jose import jwt, JWTError
from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

class SecurityManager:
    ALGORITHM = "HS256"
    ACCESS_TOKEN_EXPIRE = 30  # minutes
    REFRESH_TOKEN_EXPIRE = 7  # days
    
    @staticmethod
    def verify_password(plain: str, hashed: str) -> bool:
        return pwd_context.verify(plain, hashed)
    
    @staticmethod
    def hash_password(password: str) -> str:
        return pwd_context.hash(password)
    
    @classmethod
    def create_token(cls, user_id: int, token_type: str = "access") -> str:
        expire = datetime.utcnow() + timedelta(
            minutes=cls.ACCESS_TOKEN_EXPIRE if token_type == "access" else cls.REFRESH_TOKEN_EXPIRE
        )
        payload = {
            "sub": str(user_id),
            "type": token_type,
            "exp": expire,
            "iat": datetime.utcnow(),
            "jti": secrets.token_urlsafe(16),  # 唯一标识,用于撤销
        }
        return jwt.encode(payload, settings.SECRET_KEY, algorithm=cls.ALGORITHM)
    
    @classmethod
    async def verify_token(cls, token: str) -> TokenPayload:
        try:
            payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[cls.ALGORITHM])
            # 检查 Token 是否在撤销列表
            if await redis.sismember("revoked_tokens", payload.get("jti")):
                raise JWTError("Token 已撤销")
            return TokenPayload(**payload)
        except JWTError as e:
            raise AuthenticationFailed(f"无效的 Token: {e}")

# 依赖注入
async def get_current_user(
    token: str = Depends(OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")),
) -> User:
    payload = await SecurityManager.verify_token(token)
    user = await crud.user.get(db, int(payload.sub))
    if not user or not user.is_active:
        raise AuthenticationFailed("用户不存在或已禁用")
    return user

3.7 其余安全措施

措施 实现
HTTPS 强制 Nginx 配置 return 301 https://
CSRF 防护 Cookie 设置 SameSite=Strict; Secure; HttpOnly
请求限流 SlowAPI + Redis 滑动窗口限流
敏感配置加密 实例密码 AES-256-CBC 加密存储
会话超时 JWT 30 分钟过期 + Refresh Token 7 天
登录失败锁定 5 次失败锁定 15 分钟
SQL 超时 所有查询强制 30 秒超时
审计日志防篡改 审计日志写入只读存储 / WORM 模式

4. 用户体验与产品细节优化

4.1 SQL 编辑器选型

推荐: Monaco Editor (VS Code 内核)

// 安装
// npm install monaco-editor sql-formatter

// 核心配置
const editor = monaco.editor.create(container, {
  language: 'mysql',           // mysql / pgsql / redis
  theme: 'vs-dark',
  fontSize: 13,
  fontFamily: 'JetBrains Mono, monospace',
  minimap: { enabled: false },
  automaticLayout: true,
  scrollbar: { vertical: 'auto' },
  suggestOnTriggerCharacters: true,
  quickSuggestions: { other: true, comments: true, strings: true },
  wordBasedSuggestions: "allDocuments",
  formatOnPaste: true,
  formatOnType: true,
});

// 数据库 schema 自动补全
const provideCompletionItems = async (model, position) => {
  const word = model.getWordUntilPosition(position);
  const tables = await fetchTables(selectedInstance.value);
  return {
    suggestions: tables.flatMap(t => [
      { label: t.name, kind: monaco.languages.CompletionItemKind.Class, insertText: t.name },
      ...t.columns.map(c => ({
        label: c.name, kind: monaco.languages.CompletionItemKind.Field,
        insertText: c.name, detail: c.type,
      })),
    ]),
  };
};

monaco.languages.registerCompletionItemProvider('mysql', { 
  triggerCharacters: ['.', ' '], 
  provideCompletionItems 
});

四数据库语法支持:

数据库 Monaco Language 语法高亮 自动补全
MySQL mysql ✅ 内置 表名/字段名
PostgreSQL pgsql ✅ 内置 表名/字段名
MongoDB javascript ⚠️ 需扩展 Collection/Field
Redis 自定义 ⚠️ 需自定义 Key/命令

4.2 工单审批流程可视化

<!-- frontend/src/components/workflow/FlowChart.vue -->
<template>
  <div class="workflow-flow">
    <div v-for="(step, i) in steps" :key="step.id" class="flow-step">
      <div class="step-node" :class="step.status">
        <component :is="stepIcons[step.type]" class="icon" />
        <div class="info">
          <div class="name">{{ step.name }}</div>
          <div class="assignee">{{ step.assignees.join(' / ') }}</div>
          <div v-if="step.time" class="time">{{ step.time }}</div>
        </div>
      </div>
      <div v-if="i < steps.length - 1" class="flow-arrow" :class="{ active: step.status === 'completed' }" />
    </div>
  </div>
</template>

<style scoped>
.workflow-flow { display: flex; align-items: center; gap: 0; }
.flow-step { display: flex; align-items: center; }
.step-node { 
  padding: 12px 20px; border-radius: 8px; border: 2px solid var(--dg-border);
  display: flex; align-items: center; gap: 12px; min-width: 160px;
}
.step-node.completed { border-color: #22c55e; background: rgba(34,197,94,0.1); }
.step-node.active { border-color: #3b82f6; background: rgba(59,130,246,0.1); box-shadow: 0 0 0 4px rgba(59,130,246,0.2); }
.flow-arrow { width: 40px; height: 2px; background: var(--dg-border); margin: 0 4px; position: relative; }
.flow-arrow.active { background: #22c55e; }
.flow-arrow::after { content: ''; position: absolute; right: 0; top: -4px; width: 0; height: 0; border-left: 6px solid currentColor; border-top: 5px solid transparent; border-bottom: 5px solid transparent; }
</style>

4.3 通知系统

# backend/app/utils/notify.py
class NotificationManager:
    CHANNELS = {
        'feishu': FeishuNotifier(),
        'dingtalk': DingtalkNotifier(),
        'wechat': WechatNotifier(),
        'telegram': TelegramNotifier(),
        'email': EmailNotifier(),
    }
    
    async def send(self, event: str, payload: dict, recipients: list[int]):
        """发送通知到所有已启用的渠道"""
        configs = await crud.notification_config.get_enabled(db, event=event)
        
        tasks = []
        for cfg in configs:
            notifier = self.CHANNELS.get(cfg.channel)
            if notifier:
                tasks.append(notifier.send(cfg.webhook, self._render_template(event, payload)))
        
        await asyncio.gather(*tasks, return_exceptions=True)
    
    def _render_template(self, event: str, payload: dict) -> str:
        templates = {
            'ticket.created': '【工单创建】{title}\n申请人: {applicant}\n时间: {time}',
            'ticket.approved': '【审批通过】{title}\n审批人: {approver}\n时间: {time}',
            'ticket.executed': '【执行完成】{title}\n状态: {status}\n耗时: {duration}',
            'ticket.failed': '【执行失败】{title}\n错误: {error}',
        }
        return templates.get(event, '').format(**payload)

# 飞书通知实现
class FeishuNotifier:
    async def send(self, webhook: str, content: str):
        async with httpx.AsyncClient() as client:
            await client.post(webhook, json={
                "msg_type": "text",
                "content": {"text": content}
            })

4.4 移动端适配与深色主题

/* 移动端断点 */
@media (max-width: 768px) {
  .sidebar { display: none; }
  .mobile-menu-btn { display: block; }
  .stats-grid { grid-template-columns: repeat(2, 1fr); }
  .table-container { overflow-x: scroll; }
  .modal { width: 95vw; max-height: 90vh; }
}

/* 深色主题变量 */
:root {
  --dg-bg-primary: #0d1117;
  --dg-bg-secondary: #161b22;
  --dg-bg-tertiary: #21262d;
  --dg-bg-hover: #1c2128;
  --dg-border: #30363d;
  --dg-border-light: #21262d;
  --dg-text-primary: #e6edf3;
  --dg-text-secondary: #8b949e;
  --dg-text-muted: #484f58;
}

/* 浅色主题(可选) */
[data-theme="light"] {
  --dg-bg-primary: #ffffff;
  --dg-bg-secondary: #f6f8fa;
  --dg-bg-tertiary: #eaeef2;
  --dg-bg-hover: #f3f4f6;
  --dg-border: #d0d7de;
  --dg-border-light: #eaeef2;
  --dg-text-primary: #1f2328;
  --dg-text-secondary: #656d76;
  --dg-text-muted: #8c959f;
}

5. 性能与可扩展性优化

5.1 异步任务队列 (Celery + Redis)

# backend/app/tasks/celery_tasks.py
from celery import Celery

celery_app = Celery('dataguard', broker=settings.CELERY_BROKER_URL)

@celery_app.task(bind=True, max_retries=3, default_retry_delay=60)
def execute_ticket(self, ticket_id: int, executor_id: int):
    """异步执行工单 SQL"""
    try:
        ticket = sync_to_async(crud.ticket.get)(ticket_id)
        
        # 1. 预执行检查
        precheck_result = sync_to_async(pre_execute_check)(ticket)
        if not precheck_result.passed:
            sync_to_async(crud.ticket.update)(ticket_id, status='failed', 
                                               error_msg=precheck_result.msg)
            return
        
        # 2. 执行 SQL
        start = time.time()
        result = sync_to_async(execute_sql)(ticket.instance_id, ticket.sql_content)
        duration = time.time() - start
        
        # 3. 记录结果
        sync_to_async(crud.ticket.update)(ticket_id, 
            status='success',
            execute_duration=duration,
            result_summary=result.summary,
        )
        
        # 4. 发送通知
        sync_to_async(notify_manager.send)('ticket.executed', {
            'title': ticket.title,
            'status': '成功',
            'duration': f'{duration:.2f}s',
        }, [ticket.applicant_id])
        
    except Exception as exc:
        self.retry(exc=exc)

@celery_app.task
def auto_audit_task(ticket_id: int):
    """自动审核任务"""
    engine = AutoAuditEngine()
    result = sync_to_async(engine.audit)(ticket_id)
    
    if result.passed:
        sync_to_async(crud.ticket.update)(ticket_id, status='pending_review')
    else:
        sync_to_async(crud.ticket.update)(ticket_id, 
            status='auto_audit_failed',
            audit_score=result.score,
            audit_issues=result.issues_json,
        )

@celery_app.task
def backup_before_execute(ticket_id: int):
    """执行前备份受影响数据"""
    ticket = sync_to_async(crud.ticket.get)(ticket_id)
    backup_table = f"_dg_backup_{ticket.target_table}_{ticket_id}_{int(time.time())}"
    sync_to_async(execute_sql)(ticket.instance_id, 
        f"CREATE TABLE {backup_table} AS SELECT * FROM {ticket.target_table} WHERE {ticket.where_clause}")
    sync_to_async(crud.ticket.update)(ticket_id, backup_table=backup_table)

5.2 大 SQL 处理与不同数据库执行引擎

# backend/app/core/execution_engine.py
from abc import ABC, abstractmethod

class ExecutionEngine(ABC):
    @abstractmethod
    async def execute(self, instance_id: int, sql: str, timeout: int = 30) -> ExecuteResult:
        pass
    
    @abstractmethod
    async def explain(self, instance_id: int, sql: str) -> ExplainResult:
        pass

class MySQLEngine(ExecutionEngine):
    async def execute(self, instance_id, sql, timeout=30):
        engine = await pool_mgr.get_mysql_engine(instance_id)
        async with engine.connect() as conn:
            result = await asyncio.wait_for(conn.execute(text(sql)), timeout=timeout)
            return ExecuteResult(
                rows=[dict(r) for r in result.mappings()],
                columns=list(result.keys()),
                rowcount=result.rowcount,
            )
    
    async def explain(self, instance_id, sql):
        engine = await pool_mgr.get_mysql_engine(instance_id)
        async with engine.connect() as conn:
            result = await conn.execute(text(f"EXPLAIN {sql}"))
            return ExplainResult(rows=[dict(r) for r in result.mappings()])

class PostgreSQLEngine(ExecutionEngine):
    async def execute(self, instance_id, sql, timeout=30):
        # 使用 EXPLAIN ANALYZE 预估 + 实际执行
        ...
    
    async def explain(self, instance_id, sql):
        # PostgreSQL JSON format EXPLAIN
        engine = await pool_mgr.get_pg_engine(instance_id)
        async with engine.connect() as conn:
            result = await conn.execute(text(f"EXPLAIN (FORMAT JSON) {sql}"))
            return ExplainResult(plan=result.scalar())

class MongoEngine(ExecutionEngine):
    async def execute(self, instance_id, command: str, timeout=30):
        client = await pool_mgr.get_mongo_client(instance_id)
        db = client.get_default_database()
        # MongoDB 命令解析执行
        result = await db.command(eval(command))
        return ExecuteResult(raw=result)

class RedisEngine(ExecutionEngine):
    async def execute(self, instance_id, command: str, timeout=30):
        client = await pool_mgr.get_redis_client(instance_id)
        parts = command.split()
        cmd, args = parts[0], parts[1:]
        result = await client.execute_command(cmd, *args)
        return ExecuteResult(raw=result)

# 工厂模式
class EngineFactory:
    ENGINES = {
        'mysql': MySQLEngine,
        'postgres': PostgreSQLEngine,
        'mongo': MongoEngine,
        'redis': RedisEngine,
    }
    
    @classmethod
    def get(cls, db_type: str) -> ExecutionEngine:
        return cls.ENGINES[db_type]()

5.3 规则引擎性能优化

# 1. 规则缓存
class RuleCache:
    async def get_active_rules(self, db_type: str) -> list[AuditRule]:
        cache_key = f"rules:active:{db_type}"
        cached = await redis.get(cache_key)
        if cached:
            return [AuditRule.parse_raw(r) for r in json.loads(cached)]
        
        rules = await crud.audit_rule.get_active(db, db_type=db_type)
        await redis.setex(cache_key, 300, json.dumps([r.json() for r in rules]))
        return rules
    
    async def invalidate(self, db_type: str):
        await redis.delete(f"rules:active:{db_type}")

# 2. 规则编译缓存(正则预编译)
class CompiledRule:
    def __init__(self, rule: AuditRule):
        self.rule = rule
        self._compiled = re.compile(rule.pattern, re.IGNORECASE) if rule.rule_type == 'regex' else None
    
    def match(self, sql: str) -> bool:
        if self._compiled:
            return bool(self._compiled.search(sql))
        return False

# 3. 短路求值:block 规则匹配后立即终止

5.4 数据库连接池管理

# backend/app/core/database.py
# 连接池配置参考

# MySQL (aiomysql + SQLAlchemy)
engine = create_async_engine(
    url,
    pool_size=5,              # 常驻连接数
    max_overflow=10,          # 最大溢出连接
    pool_recycle=3600,        # 连接回收时间(秒)
    pool_pre_ping=True,       # 使用前探活
    echo=False,
)

# PostgreSQL (asyncpg + SQLAlchemy)
engine = create_async_engine(
    url,
    pool_size=5,
    max_overflow=10,
    pool_recycle=3600,
    pool_pre_ping=True,
    echo=False,
)

# MongoDB (motor)
client = AsyncIOMotorClient(
    uri,
    maxPoolSize=10,
    minPoolSize=1,
    maxIdleTimeMS=60000,
    serverSelectionTimeoutMS=5000,
)

# Redis (aioredis)
redis = aioredis.from_url(
    url,
    max_connections=20,
    decode_responses=True,
    health_check_interval=30,
)

6. 技术实现关键点

6.1 FastAPI 核心代码框架

# backend/app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
    # 启动
    await pool_mgr.init()
    await redis_pool.init()
    await celery_app.start()
    yield
    # 关闭
    await pool_mgr.close()
    await redis_pool.close()

app = FastAPI(
    title="DataGuard API",
    version="2.0.0",
    lifespan=lifespan,
    docs_url="/docs" if settings.DEBUG else None,
)

# CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://dataguard.company.com"],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
    allow_headers=["*"],
)

# 全局异常处理
@app.exception_handler(BusinessException)
async def business_exception_handler(request, exc):
    return JSONResponse(status_code=400, content={"code": 400, "message": exc.message})

@app.exception_handler(PermissionDenied)
async def permission_exception_handler(request, exc):
    return JSONResponse(status_code=403, content={"code": 403, "message": exc.message})

# 注册路由
app.include_router(api_router, prefix="/api/v1")

# 健康检查
@app.get("/health")
async def health_check():
    return {"status": "ok", "version": "2.0.0"}

6.2 Vue3 Composition API 最佳实践

// 组合式函数示例
// composables/useTable.ts
import { ref, computed, watch } from 'vue';
import { useQuery } from '@tanstack/vue-query';

export function useTable<T>(
  fetchFn: (params: any) => Promise<{ list: T[]; total: number }>,
  options: { immediate?: boolean; defaultPageSize?: number } = {}
) {
  const search = ref('');
  const filters = ref<Record<string, any>>({});
  const pagination = ref({ page: 1, pageSize: options.defaultPageSize || 10, total: 0 });
  const sorter = ref<{ field: string; order: 'ascend' | 'descend' | null }>({ field: '', order: null });
  
  const queryKey = computed(() => [
    'table',
    pagination.value.page,
    pagination.value.pageSize,
    search.value,
    filters.value,
    sorter.value,
  ]);
  
  const { data, isLoading, error, refetch } = useQuery({
    queryKey,
    queryFn: () => fetchFn({
      page: pagination.value.page,
      pageSize: pagination.value.pageSize,
      search: search.value,
      filters: filters.value,
      sorter: sorter.value,
    }),
    select: (res) => {
      pagination.value.total = res.total;
      return res.list;
    },
  });
  
  return {
    data, isLoading, error, refetch,
    search, filters, pagination, sorter,
  };
}

// 页面使用
const { data: tickets, isLoading, search, pagination } = useTable(
  (params) => apiTicket.list(params),
  { defaultPageSize: 20 }
);

6.3 关键 Pydantic Model

# backend/app/schemas/instance.py
from pydantic import BaseModel, Field, field_validator
from typing import Literal

DbType = Literal['mysql', 'postgres', 'mongo', 'redis']

class InstanceCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    db_type: DbType
    host: str = Field(..., pattern=r'^([a-zA-Z0-9.-]+|\d+\.\d+\.\d+\.\d+)$')
    port: int = Field(..., ge=1, le=65535)
    username: str = Field(..., min_length=1)
    password: str = Field(..., min_length=1)
    database: str | None = None
    env: Literal['production', 'staging', 'development'] = 'development'
    description: str | None = None
    
    @field_validator('port')
    @classmethod
    def validate_port(cls, v: int, info) -> int:
        db_type = info.data.get('db_type')
        defaults = {'mysql': 3306, 'postgres': 5432, 'mongo': 27017, 'redis': 6379}
        if v != defaults.get(db_type):
            pass  # 允许非默认端口,仅作提示
        return v

class InstanceResponse(InstanceCreate):
    id: int
    status: str
    version: str | None
    databases: int
    tables: int
    size: str
    connections: int
    qps: int
    create_time: datetime

# backend/app/schemas/ticket.py
class TicketCreate(BaseModel):
    title: str = Field(..., min_length=5, max_length=200)
    db_type: DbType
    instance_id: int
    target_database: str
    sql_content: str = Field(..., min_length=1)
    rollback_sql: str | None = None
    reason: str = Field(..., min_length=10)
    priority: Literal['high', 'medium', 'low'] = 'medium'

class TicketApprove(BaseModel):
    action: Literal['approve', 'reject']
    comment: str = Field(..., min_length=1, max_length=500)

class TicketExecute(BaseModel):
    dry_run: bool = True  # 默认先模拟执行

6.4 Docker + 多数据库驱动配置

# Dockerfile
FROM python:3.11-slim

WORKDIR /app

# 安装系统依赖(MySQL/PostgreSQL 客户端 + MongoDB 驱动 + Redis)
RUN apt-get update && apt-get install -y \
    gcc \
    libmariadb-dev \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

COPY pyproject.toml .
RUN pip install --no-cache-dir -e ".[all]"

COPY app/ ./app/
COPY migrations/ ./migrations/

CMD ["gunicorn", "app.main:app", "-k", "uvicorn.workers.UvicornWorker", "-w", "4", "-b", "0.0.0.0:8000"]
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      - ENV=production
      - SECRET_KEY=${SECRET_KEY}
      - MYSQL_HOST=mysql
      - POSTGRES_HOST=postgres
      - MONGO_URI=mongodb://mongo:27017
      - REDIS_URL=redis://redis:6379/0
      - CELERY_BROKER_URL=redis://redis:6379/1
    depends_on:
      - mysql
      - postgres
      - mongo
      - redis
    volumes:
      - ./logs:/app/logs

  worker:
    build: .
    command: celery -A app.tasks.celery_tasks worker -l info -c 4
    environment:
      - ENV=production
      - CELERY_BROKER_URL=redis://redis:6379/1
    depends_on:
      - redis
      - mysql
      - postgres

  scheduler:
    build: .
    command: celery -A app.tasks.celery_tasks beat -l info
    depends_on:
      - redis

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: dataguard
    volumes:
      - mysql_data:/var/lib/mysql
    command: --binlog-format=ROW --server-id=1

  postgres:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: pgpass
      POSTGRES_DB: dataguard
    volumes:
      - postgres_data:/var/lib/postgresql/data

  mongo:
    image: mongo:7
    volumes:
      - mongo_data:/data/db

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - app

volumes:
  mysql_data:
  postgres_data:
  mongo_data:
  redis_data:

7. 部署与 DevOps

7.1 生产部署方案

# nginx.conf
upstream backend {
    server app:8000 max_fails=3 fail_timeout=30s;
}

server {
    listen 80;
    server_name dataguard.company.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name dataguard.company.com;
    
    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    
    # 前端静态文件
    location / {
        root /var/www/frontend;
        try_files $uri $uri/ /index.html;
        expires 7d;
    }
    
    # API 反向代理
    location /api/ {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_connect_timeout 30s;
        proxy_send_timeout 30s;
        proxy_read_timeout 30s;
    }
    
    # WebSocket(实时通知)
    location /ws/ {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
# Gunicorn 配置 gunicorn.conf.py
import multiprocessing

bind = "0.0.0.0:8000"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "uvicorn.workers.UvicornWorker"
timeout = 60
keepalive = 5
errorlog = "/app/logs/gunicorn-error.log"
accesslog = "/app/logs/gunicorn-access.log"

7.2 CI/CD 流程

# .github/workflows/ci.yml
name: CI/CD

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test-backend:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: test
        options: --health-cmd="mysqladmin ping" --health-interval=10s
      redis:
        image: redis:7-alpine
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: pip install -e ".[dev]"
      - run: pytest tests/ --cov=app --cov-report=xml
      - uses: codecov/codecov-action@v3

  test-frontend:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run lint
      - run: npm run test:unit -- --coverage
      - run: npm run build

  deploy:
    needs: [test-backend, test-frontend]
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build & Push Docker
        run: |
          docker build -t dataguard:${{ github.sha }} .
          docker push dataguard:${{ github.sha }}
      - name: Deploy to Server
        run: |
          ssh deploy@server "cd /opt/dataguard && \
            docker pull dataguard:${{ github.sha }} && \
            docker-compose up -d"

7.3 监控与日志

# Prometheus 指标
from prometheus_client import Counter, Histogram, Gauge

request_count = Counter('dg_request_total', 'Total requests', ['method', 'endpoint', 'status'])
request_latency = Histogram('dg_request_duration_seconds', 'Request latency')
active_connections = Gauge('dg_db_active_connections', 'Active DB connections', ['db_type', 'instance'])
ticket_count = Gauge('dg_tickets_total', 'Total tickets', ['status'])

# 中间件自动收集
@app.middleware("http")
async def metrics_middleware(request: Request, call_next):
    start = time.time()
    response = await call_next(request)
    duration = time.time() - start
    
    request_count.labels(
        method=request.method,
        endpoint=request.url.path,
        status=response.status_code,
    ).inc()
    request_latency.observe(duration)
    
    return response
# prometheus.yml
scrape_configs:
  - job_name: 'dataguard'
    static_configs:
      - targets: ['app:8000']
    metrics_path: /metrics
    scrape_interval: 15s

# Grafana Dashboard 关键面板:
# - API QPS / 延迟 P99
# - 数据库连接池使用率
# - 工单状态分布
# - SQL 查询 TOP10
# - 错误率告警

8. 迭代路线图

8.1 MVP(1 个月内可上线)P0

功能 优先级 预计耗时 说明
用户登录 + JWT 认证 P0 3天 FastAPI OAuth2 + Vue3 登录页
MySQL 数据源管理 P0 3天 CRUD + 连接测试 + 密码加密
SQL 工单提交 + 审批 P0 5天 状态机 + 单级审批
在线 SQL 查询 P0 4天 MySQL 查询 + 结果展示 + LIMIT 限制
基础 RBAC 权限 P0 3天 admin/dba/developer 角色
工单列表 + 筛选 P0 2天 表格 + 分页 + 状态筛选

MVP 总耗时: ~20天

8.2 v1.0(3 个月)P1

功能 优先级 预计耗时 说明
PostgreSQL 支持 P1 3天 数据源 + 查询引擎适配
MongoDB 支持 P1 5天 数据源 + 查询引擎 + 命令解析
Redis 支持 P1 4天 数据源 + Key 管理 + 命令执行
多级审批流程配置 P1 5天 流程模板 + 可视化配置
审核规则引擎 P1 5天 10条内置规则 + 自定义规则
回滚 SQL 生成 P1 5天 MySQL Binlog 逆向
数据脱敏 P1 3天 敏感字段自动识别 + 脱敏
通知系统 P1 3天 飞书/钉钉/邮件
SQL 编辑器升级 P1 4天 Monaco Editor + 语法高亮
仪表盘 P1 4天 统计图表 + 趋势
审计日志 P1 4天 操作记录 + 变更对比
LDAP 集成 P1 3天 域账号登录
配置平台 P1 4天 全局配置 + 工单规则 + DDL 拦截

v1.0 总耗时: ~60天(在 MVP 基础上增量)

8.3 v2.0(6 个月)P2

功能 优先级 预计耗时 说明
Celery 异步任务 P2 5天 工单异步执行 + 定时任务
自动任务调度 P2 4天 Cron 表达式 + 任务管理
数据字典自动采集 P2 4天 information_schema 同步
行级/字段级权限 P2 5天 细粒度数据权限控制
大 SQL 处理优化 P2 4天 分批执行 + 流式结果
查询性能分析 P2 3天 EXPLAIN 解析 + 索引建议
移动端适配 P2 5天 响应式布局 + 触摸优化
深色/浅色主题 P2 3天 CSS 变量切换
多租户架构 P2 7天 tenant_id 隔离
数据库对比 P2 5天 结构对比 + 数据对比
备份管理 P2 5天 定时备份 + 恢复
Prometheus + Grafana P2 4天 指标采集 + Dashboard
操作审计防篡改 P2 4天 WORM 存储

v2.0 总耗时: ~60天(在 v1.0 基础上增量)


9. 差异化竞争力与风险提醒

9.1 与竞品核心差异点

维度 DataGuard Bytebase Yearning Archery
多数据库支持 MySQL/PG/Mongo/Redis MySQL/PG/Mongo/Spanner/Oracle MySQL only MySQL/Redis/PG
架构 FastAPI + Vue3 Go + Vue3 Go + Vue Django + React
审批流程 可视化配置 + 多级 + 会签/或签 基础审批流 单级审批 多级审批
回滚机制 Binlog ROW + AST + 备份 pg_dump + 版本管理 Binlog 逆向 Binlog 逆向
规则引擎 自定义 + 版本控制 内置规则 内置规则 内置规则
数据脱敏 自动识别 + 实时脱敏 字段级脱敏 无 基础脱敏
Redis 管理 Key 浏览 + 数据结构 + 序列化 无 无 基础 Key 操作
开源协议 Apache-2.0 Apache-2.0 GPL-3.0 Apache-2.0

核心差异化:

  1. 四数据库深度支持: 不像 Yearning 只支持 MySQL,也不像 Archery Redis 支持薄弱
  2. 可视化审批流配置: 拖拽式配置审批流程,支持会签/或签,竞品多为硬编码
  3. Redis 专业化管理: Key 树形浏览、数据结构支持、自动序列化识别
  4. 规则引擎版本控制: 规则变更可追溯,支持自定义 Python 规则

9.2 潜在风险及规避建议

风险 级别 规避建议
Binlog 回滚限制 高 DDL 操作无法完整回滚数据,需明确告知用户;生产环境强制备份
MongoDB 事务支持 中 MongoDB 4.0+ 支持多文档事务,需检测版本并给出警告
Redis 命令安全性 高 禁止 FLUSHALL/FLUSHDB/CONFIG/DEBUG 等危险命令
SQL 超时导致连接泄漏 高 所有查询强制 asyncio.wait_for 超时,连接池设置 pool_recycle
敏感数据脱敏误伤 中 脱敏规则可配置白名单,支持按字段名/正则/数据类型匹配
JWT Token 泄露 高 短有效期(30min) + Refresh Token + 撤销列表;HTTPS 强制
数据库连接密码泄露 高 AES 加密存储;禁止前端返回密码;使用 vault 管理密钥
Celery 任务堆积 中 监控队列长度,设置任务过期时间;Flower 实时监控
并发执行冲突 中 同一表加分布式锁(Redis SETNX),防止同时执行变更
审计日志存储膨胀 低 自动归档 90 天前日志到对象存储;热数据保留本地

文档完成。如需调整细节或补充特定章节,请告知。