diff --git a/db-admin-example/README.md b/db-admin-example/README.md new file mode 100644 index 0000000000000..cae510a5c4918 --- /dev/null +++ b/db-admin-example/README.md @@ -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 执行 +- 事务管理 +- 查询计划分析 \ No newline at end of file diff --git a/db-admin-example/backend.js b/db-admin-example/backend.js new file mode 100644 index 0000000000000..16f97cf69539b --- /dev/null +++ b/db-admin-example/backend.js @@ -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); +}); \ No newline at end of file diff --git a/db-admin-example/frontend.html b/db-admin-example/frontend.html new file mode 100644 index 0000000000000..22b6000a4ac1f --- /dev/null +++ b/db-admin-example/frontend.html @@ -0,0 +1,227 @@ + + +
+ + +请输入 SQL 语句并点击"执行 SQL"按钮
+