This commit is contained in:
孙小云 2026-02-04 17:25:39 +08:00
parent 1dcedf3150
commit ddd85bc176
3 changed files with 567 additions and 2 deletions

View File

@ -30,6 +30,15 @@ dingtalk:
access_token: ed3533e05cf13e090c098436ee6cd52b2adfa2d85b5b2b9da1ae2bccdaecb8f3 access_token: ed3533e05cf13e090c098436ee6cd52b2adfa2d85b5b2b9da1ae2bccdaecb8f3
secret: SEC66372694e16e7e931f53aefb4b847b7fb6c42350a10f0f27fbf4151785353261 secret: SEC66372694e16e7e931f53aefb4b847b7fb6c42350a10f0f27fbf4151785353261
# 数据库配置(用于数据库管理页面)
database:
container_name: ruoyi-mysql # MySQL 容器名称
host: localhost
port: 3306
user: root
password: password
charset: utf8mb4
# 基础设施服务配置(只部署一次) # 基础设施服务配置(只部署一次)
infrastructure: infrastructure:
- name: ruoyi-mysql - name: ruoyi-mysql

View File

@ -17,6 +17,15 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from flask import Flask, request, jsonify, render_template_string from flask import Flask, request, jsonify, render_template_string
# 数据库相关导入
try:
import pymysql
pymysql.install_as_MySQLdb()
PYMYSQL_AVAILABLE = True
except ImportError:
PYMYSQL_AVAILABLE = False
print("警告: pymysql 未安装,数据库管理功能将不可用。请运行: pip install pymysql")
# 添加当前目录到 Python 路径 # 添加当前目录到 Python 路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
@ -862,7 +871,7 @@ class DeploymentServer:
<h3>📖 使用说明</h3> <h3>📖 使用说明</h3>
<p><strong>触发部署</strong>点击下方表格中的"部署链接"或直接访问 <code>http://IP:9999/项目名称</code></p> <p><strong>触发部署</strong>点击下方表格中的"部署链接"或直接访问 <code>http://IP:9999/项目名称</code></p>
<p><strong>示例</strong><code>http://IP:9999/tuoheng-device</code> 触发 tuoheng-device 项目部署</p> <p><strong>示例</strong><code>http://IP:9999/tuoheng-device</code> 触发 tuoheng-device 项目部署</p>
<p><strong>查看日志</strong><a href="/logs" style="color: #1976d2; font-weight: bold;">点击这里查看部署日志</a> | <a href="/containers" style="color: #1976d2; font-weight: bold;">查看容器日志</a></p> <p><strong>查看日志</strong><a href="/logs" style="color: #1976d2; font-weight: bold;">点击这里查看部署日志</a> | <a href="/containers" style="color: #1976d2; font-weight: bold;">查看容器日志</a> | <a href="/database" style="color: #1976d2; font-weight: bold;">数据库管理</a></p>
</div> </div>
<h2>🌐 服务访问入口</h2> <h2>🌐 服务访问入口</h2>
@ -1466,11 +1475,553 @@ class DeploymentServer:
except Exception as e: except Exception as e:
return jsonify({'logs': [], 'error': str(e)}) return jsonify({'logs': [], 'error': str(e)})
@self.app.route('/database')
def view_database():
"""数据库管理页面"""
if not PYMYSQL_AVAILABLE:
return '''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>数据库管理 - 错误</title>
</head>
<body>
<h1>数据库管理功能不可用</h1>
<p>pymysql 模块未安装请运行: pip install pymysql</p>
<a href="/">返回首页</a>
</body>
</html>
'''
# 获取数据库配置
db_config = self.monitor.config.get('database', {})
container_name = db_config.get('container_name', 'ruoyi-mysql')
html = f'''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>数据库管理</title>
<style>
body {{
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}}
.header {{
background-color: #1976d2;
color: white;
padding: 20px;
border-radius: 5px;
margin-bottom: 20px;
}}
.header h1 {{
margin: 0;
font-size: 24px;
}}
.back-link {{
color: white;
text-decoration: none;
font-size: 14px;
display: inline-block;
margin-top: 10px;
}}
.back-link:hover {{
text-decoration: underline;
}}
.container {{
max-width: 1400px;
margin: 0 auto;
}}
.panel {{
background-color: white;
border-radius: 5px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
.panel h2 {{
margin-top: 0;
color: #333;
border-bottom: 2px solid #1976d2;
padding-bottom: 10px;
}}
.db-list {{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
margin-bottom: 20px;
}}
.db-item {{
padding: 15px;
background-color: #e3f2fd;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s;
text-align: center;
font-weight: bold;
}}
.db-item:hover {{
background-color: #1976d2;
color: white;
transform: translateY(-2px);
}}
.db-item.active {{
background-color: #1976d2;
color: white;
}}
.table-list {{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 8px;
margin-bottom: 20px;
}}
.table-item {{
padding: 10px;
background-color: #f5f5f5;
border-radius: 3px;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
}}
.table-item:hover {{
background-color: #4caf50;
color: white;
}}
.sql-editor {{
width: 100%;
min-height: 150px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 14px;
resize: vertical;
}}
.btn {{
background-color: #1976d2;
color: white;
border: none;
padding: 10px 20px;
border-radius: 3px;
cursor: pointer;
font-size: 14px;
margin-right: 10px;
}}
.btn:hover {{
background-color: #1565c0;
}}
.btn-success {{
background-color: #4caf50;
}}
.btn-success:hover {{
background-color: #45a049;
}}
.result-container {{
margin-top: 20px;
overflow-x: auto;
}}
table {{
width: 100%;
border-collapse: collapse;
background-color: white;
}}
th, td {{
padding: 12px;
text-align: left;
border: 1px solid #ddd;
}}
th {{
background-color: #1976d2;
color: white;
font-weight: bold;
}}
tr:hover {{
background-color: #f5f5f5;
}}
.loading {{
text-align: center;
padding: 20px;
color: #999;
}}
.error {{
background-color: #ffebee;
color: #c62828;
padding: 15px;
border-radius: 3px;
margin-top: 10px;
}}
.success {{
background-color: #e8f5e9;
color: #2e7d32;
padding: 15px;
border-radius: 3px;
margin-top: 10px;
}}
.info {{
background-color: #e3f2fd;
padding: 15px;
border-radius: 3px;
margin-bottom: 20px;
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗄 数据库管理 - {container_name}</h1>
<a href="/" class="back-link"> 返回首页</a>
</div>
<div class="info">
<strong>说明</strong>
<ul style="margin: 10px 0;">
<li>点击数据库名称查看该数据库的所有表</li>
<li>点击表名查看表数据默认显示前100条</li>
<li>可以在SQL编辑器中执行自定义SQL查询</li>
<li> 请谨慎执行修改数据的SQL语句</li>
</ul>
</div>
<div class="panel">
<h2>📚 数据库列表</h2>
<div id="dbList" class="db-list">
<div class="loading">正在加载数据库列表...</div>
</div>
</div>
<div class="panel" id="tablePanel" style="display: none;">
<h2>📋 表列表 - <span id="currentDb"></span></h2>
<div id="tableList" class="table-list">
<div class="loading">正在加载表列表...</div>
</div>
</div>
<div class="panel">
<h2>💻 SQL 查询</h2>
<div>
<label for="sqlEditor"><strong>SQL 语句</strong></label>
<textarea id="sqlEditor" class="sql-editor" placeholder="输入 SQL 查询语句例如SELECT * FROM table_name LIMIT 10"></textarea>
</div>
<div style="margin-top: 10px;">
<button class="btn btn-success" onclick="executeSQL()"> 执行查询</button>
<button class="btn" onclick="clearSQL()">🗑 清空</button>
</div>
<div id="sqlMessage"></div>
</div>
<div class="panel" id="resultPanel" style="display: none;">
<h2>📊 查询结果</h2>
<div id="resultContainer" class="result-container"></div>
</div>
</div>
<script>
let currentDatabase = null;
// 加载数据库列表
function loadDatabases() {{
fetch('/api/database/list')
.then(response => response.json())
.then(data => {{
const dbList = document.getElementById('dbList');
if (data.success && data.databases) {{
dbList.innerHTML = data.databases.map(db =>
`<div class="db-item" onclick="selectDatabase('${{db}}')">${{db}}</div>`
).join('');
// 自动选择 ry-cloud 数据库
if (data.databases.includes('ry-cloud')) {{
selectDatabase('ry-cloud');
}}
}} else {{
dbList.innerHTML = '<div class="error">加载失败: ' + (data.error || '未知错误') + '</div>';
}}
}})
.catch(error => {{
document.getElementById('dbList').innerHTML =
'<div class="error">加载失败: ' + error.message + '</div>';
}});
}}
// 选择数据库
function selectDatabase(dbName) {{
currentDatabase = dbName;
// 更新UI
document.querySelectorAll('.db-item').forEach(item => {{
item.classList.remove('active');
if (item.textContent === dbName) {{
item.classList.add('active');
}}
}});
document.getElementById('currentDb').textContent = dbName;
document.getElementById('tablePanel').style.display = 'block';
// 加载表列表
loadTables(dbName);
}}
// 加载表列表
function loadTables(dbName) {{
const tableList = document.getElementById('tableList');
tableList.innerHTML = '<div class="loading">正在加载表列表...</div>';
fetch(`/api/database/tables?db=${{dbName}}`)
.then(response => response.json())
.then(data => {{
if (data.success && data.tables) {{
tableList.innerHTML = data.tables.map(table =>
`<div class="table-item" onclick="viewTable('${{dbName}}', '${{table}}')">${{table}}</div>`
).join('');
}} else {{
tableList.innerHTML = '<div class="error">加载失败: ' + (data.error || '未知错误') + '</div>';
}}
}})
.catch(error => {{
tableList.innerHTML = '<div class="error">加载失败: ' + error.message + '</div>';
}});
}}
// 查看表数据
function viewTable(dbName, tableName) {{
const sql = `SELECT * FROM \`${{tableName}}\` LIMIT 100`;
document.getElementById('sqlEditor').value = sql;
executeSQL(dbName);
}}
// 执行SQL
function executeSQL(dbName) {{
const sql = document.getElementById('sqlEditor').value.trim();
const db = dbName || currentDatabase;
if (!sql) {{
showMessage('请输入SQL语句', 'error');
return;
}}
if (!db) {{
showMessage('请先选择数据库', 'error');
return;
}}
showMessage('正在执行查询...', 'info');
fetch('/api/database/query', {{
method: 'POST',
headers: {{
'Content-Type': 'application/json'
}},
body: JSON.stringify({{
database: db,
sql: sql
}})
}})
.then(response => response.json())
.then(data => {{
if (data.success) {{
showMessage(`查询成功返回 ${{data.rows || 0}} 行数据`, 'success');
displayResults(data);
}} else {{
showMessage('查询失败: ' + (data.error || '未知错误'), 'error');
document.getElementById('resultPanel').style.display = 'none';
}}
}})
.catch(error => {{
showMessage('查询失败: ' + error.message, 'error');
document.getElementById('resultPanel').style.display = 'none';
}});
}}
// 显示查询结果
function displayResults(data) {{
const resultPanel = document.getElementById('resultPanel');
const resultContainer = document.getElementById('resultContainer');
if (!data.data || data.data.length === 0) {{
resultContainer.innerHTML = '<div class="loading">查询成功,但没有返回数据</div>';
resultPanel.style.display = 'block';
return;
}}
// 获取列名
const columns = data.columns || Object.keys(data.data[0]);
// 生成表格
let html = '<table><thead><tr>';
columns.forEach(col => {{
html += `<th>${{col}}</th>`;
}});
html += '</tr></thead><tbody>';
data.data.forEach(row => {{
html += '<tr>';
columns.forEach(col => {{
const value = row[col];
const displayValue = value === null ? '<em style="color: #999;">NULL</em>' :
(typeof value === 'object' ? JSON.stringify(value) : value);
html += `<td>${{displayValue}}</td>`;
}});
html += '</tr>';
}});
html += '</tbody></table>';
resultContainer.innerHTML = html;
resultPanel.style.display = 'block';
// 滚动到结果区域
resultPanel.scrollIntoView({{ behavior: 'smooth', block: 'start' }});
}}
// 显示消息
function showMessage(message, type) {{
const messageDiv = document.getElementById('sqlMessage');
messageDiv.innerHTML = `<div class="${{type}}">${{message}}</div>`;
if (type === 'success' || type === 'info') {{
setTimeout(() => {{
messageDiv.innerHTML = '';
}}, 3000);
}}
}}
// 清空SQL
function clearSQL() {{
document.getElementById('sqlEditor').value = '';
document.getElementById('sqlMessage').innerHTML = '';
}}
// 页面加载时加载数据库列表
loadDatabases();
</script>
</body>
</html>
'''
return html
@self.app.route('/api/database/list')
def get_database_list():
"""获取数据库列表API"""
if not PYMYSQL_AVAILABLE:
return jsonify({'success': False, 'error': 'pymysql 未安装'})
try:
db_config = self.monitor.config.get('database', {})
container_name = db_config.get('container_name', 'ruoyi-mysql')
# 通过 docker exec 连接到容器内的 MySQL
cmd = f"docker exec {container_name} mysql -uroot -ppassword -e 'SHOW DATABASES;'"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10)
if result.returncode != 0:
return jsonify({'success': False, 'error': f'连接数据库失败: {result.stderr}'})
# 解析数据库列表
databases = []
for line in result.stdout.strip().split('\n')[1:]: # 跳过第一行标题
db_name = line.strip()
if db_name and db_name not in ['information_schema', 'performance_schema', 'mysql', 'sys']:
databases.append(db_name)
return jsonify({'success': True, 'databases': databases})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@self.app.route('/api/database/tables')
def get_table_list():
"""获取表列表API"""
if not PYMYSQL_AVAILABLE:
return jsonify({'success': False, 'error': 'pymysql 未安装'})
try:
db_name = request.args.get('db')
if not db_name:
return jsonify({'success': False, 'error': '缺少数据库名称'})
db_config = self.monitor.config.get('database', {})
container_name = db_config.get('container_name', 'ruoyi-mysql')
# 通过 docker exec 连接到容器内的 MySQL
cmd = f"docker exec {container_name} mysql -uroot -ppassword -e 'USE {db_name}; SHOW TABLES;'"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10)
if result.returncode != 0:
return jsonify({'success': False, 'error': f'查询表列表失败: {result.stderr}'})
# 解析表列表
tables = []
for line in result.stdout.strip().split('\n')[1:]: # 跳过第一行标题
table_name = line.strip()
if table_name:
tables.append(table_name)
return jsonify({'success': True, 'tables': tables})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@self.app.route('/api/database/query', methods=['POST'])
def execute_query():
"""执行SQL查询API"""
if not PYMYSQL_AVAILABLE:
return jsonify({'success': False, 'error': 'pymysql 未安装'})
try:
data = request.get_json()
database = data.get('database')
sql = data.get('sql', '').strip()
if not database or not sql:
return jsonify({'success': False, 'error': '缺少必要参数'})
db_config = self.monitor.config.get('database', {})
container_name = db_config.get('container_name', 'ruoyi-mysql')
# 转义SQL中的单引号
sql_escaped = sql.replace("'", "'\\''")
# 通过 docker exec 执行 SQL
cmd = f"docker exec {container_name} mysql -uroot -ppassword {database} -e '{sql_escaped}' --batch"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
if result.returncode != 0:
return jsonify({'success': False, 'error': f'SQL执行失败: {result.stderr}'})
# 解析查询结果
lines = result.stdout.strip().split('\n')
if not lines or not lines[0]:
return jsonify({'success': True, 'data': [], 'columns': [], 'rows': 0})
# 第一行是列名
columns = lines[0].split('\t')
# 解析数据行
data_rows = []
for line in lines[1:]:
if line:
values = line.split('\t')
row_dict = {}
for i, col in enumerate(columns):
row_dict[col] = values[i] if i < len(values) else None
data_rows.append(row_dict)
return jsonify({
'success': True,
'data': data_rows,
'columns': columns,
'rows': len(data_rows)
})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@self.app.route('/<project_name>') @self.app.route('/<project_name>')
def deploy_project(project_name): def deploy_project(project_name):
"""触发项目部署""" """触发项目部署"""
# 避免与其他路由冲突 # 避免与其他路由冲突
if project_name in ['logs', 'api', 'containers', 'container-logs']: if project_name in ['logs', 'api', 'containers', 'container-logs', 'database']:
return jsonify({'code': 404, 'message': '路径不存在'}) return jsonify({'code': 404, 'message': '路径不存在'})
Logger.info(f"收到HTTP部署请求: {project_name}") Logger.info(f"收到HTTP部署请求: {project_name}")

View File

@ -58,6 +58,11 @@ if ! python3 -c "import flask" 2>/dev/null; then
python3 -m pip install --user --break-system-packages flask 2>/dev/null || \ python3 -m pip install --user --break-system-packages flask 2>/dev/null || \
python3 -m pip install --user flask python3 -m pip install --user flask
fi fi
if ! python3 -c "import pymysql" 2>/dev/null; then
echo "安装 PyMySQL用于数据库管理功能..."
python3 -m pip install --user --break-system-packages pymysql 2>/dev/null || \
python3 -m pip install --user pymysql
fi
echo "✓ Python 依赖检查完成" echo "✓ Python 依赖检查完成"
# 5. 删除已存在的 PM2 进程 # 5. 删除已存在的 PM2 进程