Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions db-admin-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# 数据库管理工具前后端交互示例

这个示例展示了数据库管理工具如何实现前后端交互,特别是 SQL 执行的完整流程。

## 架构说明

### 前端 (frontend.html)
- 提供 SQL 编辑器界面
- 使用 Fetch API 发送请求
- 展示查询结果表格
- 处理错误信息

### 后端 (backend.js)
- Express.js 服务器
- PostgreSQL 连接池管理
- SQL 执行和结果处理
- 安全性控制(权限、超时等)

## 交互流程

1. **用户输入 SQL**
- 在前端 SQL 编辑器中输入查询语句

2. **前端发送请求**
```javascript
POST /api/execute-sql
{
"sql": "SELECT * FROM users",
"database": "testdb",
"schema": "public"
}
```

3. **后端处理**
- 验证用户身份和权限
- 从连接池获取数据库连接
- 执行 SQL 语句
- 格式化查询结果

4. **返回响应**
```javascript
{
"success": true,
"data": {
"rows": [...],
"fields": [...],
"executionTime": 23
}
}
```

5. **前端展示结果**
- 解析响应数据
- 生成表格展示结果
- 显示执行时间等信息

## 安全措施

1. **身份验证**: JWT token 验证
2. **权限控制**: 基于用户角色的 SQL 操作限制
3. **SQL 注入防护**: 参数化查询
4. **超时控制**: 防止长时间运行的查询
5. **错误处理**: 避免暴露敏感信息

## 运行示例

1. 安装依赖:
```bash
npm install
```

2. 配置数据库连接(修改 backend.js 中的连接参数)

3. 启动后端服务:
```bash
npm start
```

4. 在浏览器中打开 frontend.html

## 扩展功能

实际的数据库管理工具还包括:
- WebSocket 支持实时查询进度
- 查询历史记录
- SQL 自动补全
- 表结构可视化编辑
- 数据导入导出
- 批量 SQL 执行
- 事务管理
- 查询计划分析
297 changes: 297 additions & 0 deletions db-admin-example/backend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
// Node.js 后端示例 - 使用 Express 和 pg (PostgreSQL客户端)
const express = require('express');
const { Pool } = require('pg');
const cors = require('cors');
const jwt = require('jsonwebtoken');

const app = express();
const port = 3000;

// 中间件
app.use(cors());
app.use(express.json());

// PostgreSQL 连接池配置
const pool = new Pool({
host: 'localhost',
port: 5432,
database: 'testdb',
user: 'dbuser',
password: 'dbpassword',
max: 20, // 连接池最大连接数
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});

// 认证中间件
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];

if (!token) {
return res.status(401).json({ success: false, error: '未提供认证令牌' });
}

// 实际应用中应该验证 JWT token
// jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
// if (err) return res.status(403).json({ success: false, error: '无效的令牌' });
// req.user = user;
// next();
// });

// 示例中简化处理
req.user = { id: 1, username: 'testuser' };
next();
}

// SQL 执行 API
app.post('/api/execute-sql', authenticateToken, async (req, res) => {
const { sql, database, schema, timeout = 30000 } = req.body;

// 1. 验证输入
if (!sql || typeof sql !== 'string') {
return res.status(400).json({
success: false,
error: '请提供有效的 SQL 语句'
});
}

// 2. 基本的 SQL 安全检查(实际应用中需要更严格的检查)
const sqlLower = sql.toLowerCase().trim();
const isSelect = sqlLower.startsWith('select');
const isShow = sqlLower.startsWith('show');
const isDescribe = sqlLower.startsWith('describe') || sqlLower.startsWith('\\d');

// 检查用户权限(示例:只允许 SELECT 查询)
if (!req.user.isAdmin && !isSelect && !isShow && !isDescribe) {
return res.status(403).json({
success: false,
error: '权限不足:只允许执行 SELECT 查询'
});
}

const startTime = Date.now();
let client;

try {
// 3. 从连接池获取客户端
client = await pool.connect();

// 4. 设置查询超时
await client.query(`SET statement_timeout = ${timeout}`);

// 5. 如果指定了 schema,设置 search_path
if (schema) {
await client.query('SET search_path TO $1', [schema]);
}

// 6. 执行 SQL
console.log(`用户 ${req.user.username} 执行 SQL:`, sql);
const result = await client.query(sql);

// 7. 计算执行时间
const executionTime = Date.now() - startTime;

// 8. 格式化响应数据
let responseData = {
success: true,
data: {
executionTime: executionTime
}
};

if (result.rows && result.rows.length > 0) {
// SELECT 查询结果
responseData.data.rows = result.rows;
responseData.data.rowCount = result.rowCount;
responseData.data.fields = result.fields.map(field => ({
name: field.name,
type: getFieldTypeName(field.dataTypeID)
}));
} else {
// INSERT/UPDATE/DELETE 结果
responseData.data.affectedRows = result.rowCount;
responseData.data.command = result.command;
}

// 9. 记录操作日志
logSqlExecution(req.user, sql, executionTime, true);

res.json(responseData);

} catch (error) {
console.error('SQL 执行错误:', error);

// 记录错误日志
logSqlExecution(req.user, sql, Date.now() - startTime, false, error.message);

// 返回错误信息(生产环境中应该隐藏详细错误信息)
res.status(500).json({
success: false,
error: formatSqlError(error)
});

} finally {
// 10. 释放连接回连接池
if (client) {
client.release();
}
}
});

// 获取数据库列表 API
app.get('/api/databases', authenticateToken, async (req, res) => {
try {
const result = await pool.query(
"SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname"
);

res.json({
success: true,
data: result.rows.map(row => row.datname)
});
} catch (error) {
res.status(500).json({
success: false,
error: '获取数据库列表失败'
});
}
});

// 获取表列表 API
app.get('/api/tables/:database', authenticateToken, async (req, res) => {
const { database } = req.params;
const { schema = 'public' } = req.query;

try {
const result = await pool.query(
`SELECT table_name, table_type
FROM information_schema.tables
WHERE table_schema = $1
ORDER BY table_name`,
[schema]
);

res.json({
success: true,
data: result.rows
});
} catch (error) {
res.status(500).json({
success: false,
error: '获取表列表失败'
});
}
});

// 获取表结构 API
app.get('/api/table-schema/:database/:table', authenticateToken, async (req, res) => {
const { database, table } = req.params;
const { schema = 'public' } = req.query;

try {
const result = await pool.query(
`SELECT
column_name,
data_type,
character_maximum_length,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_schema = $1 AND table_name = $2
ORDER BY ordinal_position`,
[schema, table]
);

res.json({
success: true,
data: result.rows
});
} catch (error) {
res.status(500).json({
success: false,
error: '获取表结构失败'
});
}
});

// 辅助函数:获取字段类型名称
function getFieldTypeName(oid) {
// PostgreSQL 数据类型 OID 映射
const typeMap = {
16: 'boolean',
20: 'bigint',
21: 'smallint',
23: 'integer',
25: 'text',
700: 'real',
701: 'double',
1043: 'varchar',
1082: 'date',
1083: 'time',
1114: 'timestamp',
1184: 'timestamptz',
1700: 'numeric',
2950: 'uuid',
3802: 'jsonb'
};

return typeMap[oid] || 'unknown';
}

// 辅助函数:格式化 SQL 错误信息
function formatSqlError(error) {
if (error.code === '42P01') {
return `表不存在: ${error.table}`;
} else if (error.code === '42703') {
return `列不存在: ${error.column}`;
} else if (error.code === '42601') {
return '语法错误: ' + error.message;
} else if (error.code === '57014') {
return '查询超时';
} else if (error.code === '23505') {
return '违反唯一约束';
} else if (error.code === '23503') {
return '违反外键约束';
} else {
// 生产环境中应该返回更通用的错误信息
return error.message || 'SQL 执行失败';
}
}

// 辅助函数:记录 SQL 执行日志
function logSqlExecution(user, sql, executionTime, success, error = null) {
const logEntry = {
timestamp: new Date().toISOString(),
userId: user.id,
username: user.username,
sql: sql.substring(0, 1000), // 限制日志中 SQL 长度
executionTime: executionTime,
success: success,
error: error
};

// 实际应用中应该写入日志文件或日志服务
console.log('SQL 执行日志:', logEntry);
}

// 错误处理中间件
app.use((err, req, res, next) => {
console.error('服务器错误:', err);
res.status(500).json({
success: false,
error: '服务器内部错误'
});
});

// 启动服务器
app.listen(port, () => {
console.log(`数据库管理工具后端运行在 http://localhost:${port}`);
});

// 优雅关闭
process.on('SIGTERM', async () => {
console.log('收到 SIGTERM 信号,正在关闭服务器...');
await pool.end();
process.exit(0);
});
Loading