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>
2056 lines
67 KiB
Markdown
2056 lines
67 KiB
Markdown
# 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)**: 服务端状态管理,自动缓存/刷新/去重
|
||
|
||
```typescript
|
||
// 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 自动生成前端类型:
|
||
|
||
```python
|
||
# 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
|
||
```
|
||
|
||
前端类型生成命令:
|
||
```bash
|
||
# 使用 openapi-typescript 从 FastAPI OpenAPI 文档自动生成
|
||
npx openapi-typescript http://localhost:8000/openapi.json --output src/types/api.ts
|
||
```
|
||
|
||
### 1.4 多环境隔离与多租户
|
||
|
||
```python
|
||
# 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` 过滤
|
||
|
||
```python
|
||
# 全局租户上下文
|
||
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 加密存储 |
|
||
| 连接复用 | 查询审计复用只读连接,工单执行使用独立连接 |
|
||
|
||
**代码结构**:
|
||
|
||
```python
|
||
# 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逆向 + 备份恢复)
|
||
```
|
||
|
||
**状态机设计**:
|
||
|
||
```python
|
||
# 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}")
|
||
...
|
||
```
|
||
|
||
**自动审核引擎**:
|
||
|
||
```python
|
||
# 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)**:
|
||
|
||
```python
|
||
# 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 编辑,无语法高亮、无自动补全。
|
||
|
||
**优化方案**:
|
||
|
||
```typescript
|
||
// 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>
|
||
```
|
||
|
||
**后端查询执行**:
|
||
|
||
```python
|
||
# 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 读取)还是手动维护。
|
||
|
||
```python
|
||
# 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 / 压缩数据 |
|
||
|
||
```python
|
||
# 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
|
||
- 数据源健康状态
|
||
- 待办审批提醒
|
||
|
||
```python
|
||
# 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 审核规则引擎
|
||
|
||
**当前不足**: 规则硬编码,不支持自定义和版本控制。
|
||
|
||
**优化方案**: 规则引擎支持自定义 + 版本控制。
|
||
|
||
```python
|
||
# 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)
|
||
```
|
||
|
||
**规则引擎执行**:
|
||
|
||
```python
|
||
# 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 注入防护
|
||
|
||
```python
|
||
# 后端:严格使用参数化查询,禁止字符串拼接 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 限制命令集 | 按需配置可执行命令 |
|
||
|
||
```python
|
||
# 执行前权限校验
|
||
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 敏感数据脱敏
|
||
|
||
```python
|
||
# 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 操作审计日志(含变更前后对比)
|
||
|
||
```python
|
||
# 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 + 行级/字段级数据权限
|
||
|
||
```python
|
||
# 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 最佳实践
|
||
|
||
```python
|
||
# 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 内核)**
|
||
|
||
```typescript
|
||
// 安装
|
||
// 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 工单审批流程可视化
|
||
|
||
```vue
|
||
<!-- 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 通知系统
|
||
|
||
```python
|
||
# 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 移动端适配与深色主题
|
||
|
||
```css
|
||
/* 移动端断点 */
|
||
@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)
|
||
|
||
```python
|
||
# 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 处理与不同数据库执行引擎
|
||
|
||
```python
|
||
# 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 规则引擎性能优化
|
||
|
||
```python
|
||
# 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 数据库连接池管理
|
||
|
||
```python
|
||
# 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 核心代码框架
|
||
|
||
```python
|
||
# 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 最佳实践
|
||
|
||
```typescript
|
||
// 组合式函数示例
|
||
// 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
|
||
|
||
```python
|
||
# 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
|
||
# 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"]
|
||
```
|
||
|
||
```yaml
|
||
# 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
|
||
# 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";
|
||
}
|
||
}
|
||
```
|
||
|
||
```python
|
||
# 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 流程
|
||
|
||
```yaml
|
||
# .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 监控与日志
|
||
|
||
```python
|
||
# 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
|
||
```
|
||
|
||
```yaml
|
||
# 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 天前日志到对象存储;热数据保留本地 |
|
||
|
||
---
|
||
|
||
*文档完成。如需调整细节或补充特定章节,请告知。*
|