From 07467d27ae7487186263295803e17c2f53e50195 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Mon, 4 Aug 2025 21:55:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dreids?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 4 +- modules/api_routes.py | 141 +++++- static/js/redis_compare.js | 849 +++++++++++++++++++++++++++------ templates/redis_compare.html | 898 +++++++++++++++++------------------ 4 files changed, 1275 insertions(+), 617 deletions(-) diff --git a/app.py b/app.py index af52ed2..6ded8aa 100644 --- a/app.py +++ b/app.py @@ -52,6 +52,6 @@ if __name__ == '__main__': logger.info("=== BigDataTool 启动 ===") logger.info("应用架构:模块化") logger.info("支持功能:单表查询、分表查询、多主键查询、配置管理、查询历史") - logger.info("访问地址:http://localhost:5001") + logger.info("访问地址:http://localhost:5000") logger.info("API文档:/api/* 路径下的所有端点") - app.run(debug=True, port=5001) \ No newline at end of file + app.run(debug=True, port=5000) \ No newline at end of file diff --git a/modules/api_routes.py b/modules/api_routes.py index 92747cc..bfbacfb 100644 --- a/modules/api_routes.py +++ b/modules/api_routes.py @@ -51,6 +51,10 @@ def setup_routes(app, query_log_collector): def redis_compare(): return render_template('redis_compare.html') + @app.route('/redis-js-test') + def redis_js_test(): + return render_template('redis_js_test.html') + @app.route('/redis-test') def redis_test(): return render_template('redis_test.html') @@ -783,6 +787,62 @@ def setup_routes(app, query_log_collector): logger.info(f"Redis比较完成") logger.info(f"比较统计: 总计{result['stats']['total_keys']}个key,相同{result['stats']['identical_count']}个,不同{result['stats']['different_count']}个") + # 增强结果,添加原生数据信息 + enhanced_result = result.copy() + enhanced_result['raw_data'] = { + 'cluster1_data': [], + 'cluster2_data': [] + } + + # 从比较结果中提取原生数据信息 + for item in result.get('identical_results', []): + if 'key' in item and 'value' in item: + enhanced_result['raw_data']['cluster1_data'].append({ + 'key': item['key'], + 'value': item['value'], + 'type': 'identical' + }) + enhanced_result['raw_data']['cluster2_data'].append({ + 'key': item['key'], + 'value': item['value'], + 'type': 'identical' + }) + + for item in result.get('different_results', []): + if 'key' in item: + if 'cluster1_value' in item: + enhanced_result['raw_data']['cluster1_data'].append({ + 'key': item['key'], + 'value': item['cluster1_value'], + 'type': 'different' + }) + if 'cluster2_value' in item: + enhanced_result['raw_data']['cluster2_data'].append({ + 'key': item['key'], + 'value': item['cluster2_value'], + 'type': 'different' + }) + + for item in result.get('missing_results', []): + if 'key' in item: + if 'cluster1_value' in item and item['cluster1_value'] is not None: + enhanced_result['raw_data']['cluster1_data'].append({ + 'key': item['key'], + 'value': item['cluster1_value'], + 'type': 'missing' + }) + if 'cluster2_value' in item and item['cluster2_value'] is not None: + enhanced_result['raw_data']['cluster2_data'].append({ + 'key': item['key'], + 'value': item['cluster2_value'], + 'type': 'missing' + }) + + logger.info(f"原生数据统计: 集群1={len(enhanced_result['raw_data']['cluster1_data'])}条, 集群2={len(enhanced_result['raw_data']['cluster2_data'])}条") + + # 使用增强结果进行后续处理 + result = enhanced_result + # 自动保存Redis查询历史记录 try: # 生成历史记录名称 @@ -790,29 +850,42 @@ def setup_routes(app, query_log_collector): history_name = f"Redis比较_{timestamp}" history_description = f"自动保存 - Redis比较{result['stats']['total_keys']}个Key,发现{result['stats']['different_count']}处差异" - # 保存历史记录 - history_id = save_query_history( + # 计算查询键值列表 + query_keys = [] + if query_options.get('mode') == 'specified': + query_keys = query_options.get('keys', []) + elif query_options.get('mode') == 'random': + # 对于随机模式,从结果中提取实际查询的键 + for item in result.get('identical_results', []): + if 'key' in item: + query_keys.append(item['key']) + for item in result.get('different_results', []): + if 'key' in item: + query_keys.append(item['key']) + for item in result.get('missing_results', []): + if 'key' in item: + query_keys.append(item['key']) + + # 保存Redis查询历史记录 + history_id = save_redis_query_history( name=history_name, description=history_description, - pro_config=cluster1_config, - test_config=cluster2_config, - query_config=query_options, - query_keys=result.get('query_options', {}).get('keys', []), + cluster1_config=cluster1_config, + cluster2_config=cluster2_config, + query_options=query_options, + query_keys=query_keys, results_summary=result['stats'], execution_time=result['performance_report']['total_time'], total_keys=result['stats']['total_keys'], - differences_count=result['stats']['different_count'], + different_count=result['stats']['different_count'], identical_count=result['stats']['identical_count'], - query_type='redis', - # 添加查询结果数据 + missing_count=result['stats']['missing_in_cluster1'] + result['stats']['missing_in_cluster2'] + result['stats']['both_missing'], raw_results={ 'identical_results': result['identical_results'], 'different_results': result['different_results'], 'missing_results': result['missing_results'], 'performance_report': result['performance_report'] - }, - differences_data=result['different_results'], - identical_data=result['identical_results'] + } ) # 关联查询日志与历史记录 @@ -824,6 +897,8 @@ def setup_routes(app, query_log_collector): except Exception as e: logger.warning(f"保存Redis查询历史记录失败: {e}") + import traceback + logger.error(f"详细错误信息: {traceback.format_exc()}") # 结束查询批次 query_log_collector.end_current_batch() @@ -1021,4 +1096,44 @@ def setup_routes(app, query_log_collector): if success: return jsonify({'success': True, 'message': 'Redis查询历史记录删除成功'}) else: - return jsonify({'success': False, 'error': 'Redis查询历史记录删除失败'}), 500 \ No newline at end of file + return jsonify({'success': False, 'error': 'Redis查询历史记录删除失败'}), 500 + + # Redis查询日志API + @app.route('/api/redis/query-logs', methods=['GET']) + def api_get_redis_query_logs(): + """获取Redis查询日志""" + try: + limit = request.args.get('limit', 100, type=int) + # 获取最新的查询日志 + logs = query_log_collector.get_logs(limit=limit) + + # 过滤Redis相关的日志 + redis_logs = [] + for log in logs: + if (log.get('message') and 'redis' in log.get('message', '').lower()) or log.get('query_type') == 'redis': + redis_logs.append(log) + + return jsonify({'success': True, 'data': redis_logs}) + except Exception as e: + logger.error(f"获取Redis查询日志失败: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + @app.route('/api/redis/query-logs/history/', methods=['GET']) + def api_get_redis_query_logs_by_history(history_id): + """获取特定历史记录的Redis查询日志""" + try: + logs = query_log_collector.get_logs_by_history_id(history_id) + return jsonify({'success': True, 'data': logs}) + except Exception as e: + logger.error(f"获取历史记录 {history_id} 的Redis查询日志失败: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + @app.route('/api/redis/query-logs', methods=['DELETE']) + def api_clear_redis_query_logs(): + """清空Redis查询日志""" + try: + query_log_collector.clear_logs() + return jsonify({'success': True, 'message': 'Redis查询日志清空成功'}) + except Exception as e: + logger.error(f"清空Redis查询日志失败: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 \ No newline at end of file diff --git a/static/js/redis_compare.js b/static/js/redis_compare.js index 2f39942..6da4364 100644 --- a/static/js/redis_compare.js +++ b/static/js/redis_compare.js @@ -87,7 +87,12 @@ function toggleQueryMode() { * 添加节点 */ function addNode(clusterId) { - const container = document.getElementById(`${clusterId}-nodes`); + const container = document.getElementById(`${clusterId}Nodes`); + if (!container) { + console.error(`节点容器未找到: ${clusterId}Nodes`); + return; + } + const nodeInput = document.createElement('div'); nodeInput.className = 'node-input'; nodeInput.innerHTML = ` @@ -107,13 +112,15 @@ function addNode(clusterId) { function updateNodeDeleteButtons() { // 每个集群至少需要一个节点 ['cluster1', 'cluster2'].forEach(clusterId => { - const container = document.getElementById(`${clusterId}-nodes`); - const nodeInputs = container.querySelectorAll('.node-input'); - const deleteButtons = container.querySelectorAll('.remove-node'); - - deleteButtons.forEach(btn => { - btn.disabled = nodeInputs.length <= 1; - }); + const container = document.getElementById(`${clusterId}Nodes`); + if (container) { + const nodeInputs = container.querySelectorAll('.node-input'); + const deleteButtons = container.querySelectorAll('.remove-node'); + + deleteButtons.forEach(btn => { + btn.disabled = nodeInputs.length <= 1; + }); + } }); } @@ -121,18 +128,16 @@ function updateNodeDeleteButtons() { * 获取集群配置 */ function getClusterConfig(clusterId) { - const name = document.getElementById(`${clusterId}-name`).value; - const password = document.getElementById(`${clusterId}-password`).value; - const timeout = parseInt(document.getElementById(`${clusterId}-timeout`).value); - const maxConn = parseInt(document.getElementById(`${clusterId}-max-conn`).value); + const name = document.getElementById(`${clusterId}Name`).value; + const password = document.getElementById(`${clusterId}Password`).value; // 获取节点列表 const nodes = []; - const nodeInputs = document.querySelectorAll(`#${clusterId}-nodes .node-input`); + const nodeInputs = document.querySelectorAll(`#${clusterId}Nodes .node-input`); nodeInputs.forEach(nodeInput => { - const host = nodeInput.querySelector('.node-host').value.trim(); - const port = parseInt(nodeInput.querySelector('.node-port').value); + const host = nodeInput.querySelector('input[type="text"]').value.trim(); + const port = parseInt(nodeInput.querySelector('input[type="number"]').value); if (host && port) { nodes.push({ host, port }); @@ -143,9 +148,9 @@ function getClusterConfig(clusterId) { name, nodes, password: password || null, - socket_timeout: timeout, - socket_connect_timeout: timeout, - max_connections_per_node: maxConn + socket_timeout: 3, + socket_connect_timeout: 3, + max_connections_per_node: 16 }; } @@ -205,11 +210,16 @@ async function testConnection(clusterId) { if (result.success) { showAlert(`${clusterName}连接成功!连接耗时: ${result.data.connection_time.toFixed(3)}秒`, 'success'); - // 高亮成功的集群配置 - document.getElementById(`${clusterId}-config`).classList.add('active'); - setTimeout(() => { - document.getElementById(`${clusterId}-config`).classList.remove('active'); - }, 2000); + // 高亮成功的集群配置 - 使用正确的ID选择器 + const configElement = document.querySelector(`.cluster-config[data-cluster="${clusterId}"]`) || + document.querySelector(`#${clusterId}Config`) || + document.querySelector(`[id*="${clusterId}"]`); + if (configElement) { + configElement.classList.add('active'); + setTimeout(() => { + configElement.classList.remove('active'); + }, 2000); + } } else { showAlert(`${clusterName}连接失败: ${result.error}`, 'danger'); } @@ -228,32 +238,37 @@ async function executeRedisComparison() { return; } - // 获取配置 - const cluster1Config = getClusterConfig('cluster1'); - const cluster2Config = getClusterConfig('cluster2'); - const queryOptions = getQueryOptions(); - - // 验证配置 - if (!cluster1Config.nodes || cluster1Config.nodes.length === 0) { - showAlert('请配置集群1的Redis节点', 'warning'); - return; - } - - if (!cluster2Config.nodes || cluster2Config.nodes.length === 0) { - showAlert('请配置集群2的Redis节点', 'warning'); - return; - } - - if (queryOptions.mode === 'specified' && (!queryOptions.keys || queryOptions.keys.length === 0)) { - showAlert('请输入要查询的Key列表', 'warning'); - return; - } - try { + // 获取配置 + const cluster1Config = getClusterConfig('cluster1'); + const cluster2Config = getClusterConfig('cluster2'); + const queryOptions = getQueryOptions(); + + // 验证配置 + if (!cluster1Config.nodes || cluster1Config.nodes.length === 0) { + showAlert('请配置集群1的Redis节点', 'warning'); + return; + } + + if (!cluster2Config.nodes || cluster2Config.nodes.length === 0) { + showAlert('请配置集群2的Redis节点', 'warning'); + return; + } + + if (queryOptions.mode === 'specified' && (!queryOptions.keys || queryOptions.keys.length === 0)) { + showAlert('请输入要查询的Key列表', 'warning'); + return; + } + isQuerying = true; showLoading('正在执行Redis数据比较,请稍候...'); clearResults(); + console.log('发送Redis比较请求...'); + console.log('集群1配置:', cluster1Config); + console.log('集群2配置:', cluster2Config); + console.log('查询选项:', queryOptions); + const response = await fetch('/api/redis/compare', { method: 'POST', headers: { @@ -266,7 +281,10 @@ async function executeRedisComparison() { }) }); + console.log('Redis比较API响应状态:', response.status); + const result = await response.json(); + console.log('Redis比较API响应结果:', result); if (response.ok && result.success !== false) { currentResults = result; @@ -276,10 +294,19 @@ async function executeRedisComparison() { // 刷新历史记录列表(后台已自动保存) loadRedisQueryHistory(); } else { - showAlert(`比较失败: ${result.error}`, 'danger'); + const errorMsg = result.error || `HTTP ${response.status} 错误`; + console.error('Redis比较失败:', errorMsg); + + // 为常见错误提供友好的提示 + if (errorMsg.includes('连接失败')) { + showAlert(`Redis比较失败: ${errorMsg}。请检查Redis服务器是否运行,网络连接是否正常。`, 'danger'); + } else { + showAlert(`Redis比较失败: ${errorMsg}`, 'danger'); + } } } catch (error) { - showAlert(`请求失败: ${error.message}`, 'danger'); + console.error('Redis比较请求异常:', error); + showAlert(`请求失败: ${error.message}。请检查网络连接和服务器状态。`, 'danger'); } finally { isQuerying = false; hideLoading(); @@ -299,21 +326,31 @@ function displayResults(results) { displayMissingResults(results.missing_results || []); displayPerformanceReport(results.performance_report); + // 显示原生数据 + displayRawData(results); + // 更新标签页计数 updateTabCounts(results); - // 显示结果区域 - document.getElementById('results').style.display = 'block'; - - // 滚动到结果区域 - document.getElementById('results').scrollIntoView({ behavior: 'smooth' }); + // 显示结果区域 - 使用正确的ID + const resultSection = document.getElementById('resultSection'); + if (resultSection) { + resultSection.style.display = 'block'; + // 滚动到结果区域 + resultSection.scrollIntoView({ behavior: 'smooth' }); + } } /** * 显示统计卡片 */ function displayStatsCards(stats) { - const container = document.getElementById('statsCards'); + const container = document.getElementById('stats'); + if (!container) { + console.error('统计卡片容器未找到'); + return; + } + container.innerHTML = `
@@ -349,7 +386,11 @@ function displayStatsCards(stats) { * 显示差异结果 */ function displayDifferenceResults(differences) { - const container = document.getElementById('differenceResults'); + const container = document.getElementById('differences-content'); + if (!container) { + console.error('差异结果容器未找到'); + return; + } if (differences.length === 0) { container.innerHTML = '
未发现数据差异
'; @@ -363,11 +404,11 @@ function displayDifferenceResults(differences) {
Key: ${diff.key}
- ${currentResults.clusters.cluster1_name}: + ${currentResults.clusters.cluster1_name || '集群1'}:
${formatRedisValue(diff.cluster1_value)}
- ${currentResults.clusters.cluster2_name}: + ${currentResults.clusters.cluster2_name || '集群2'}:
${formatRedisValue(diff.cluster2_value)}
@@ -385,7 +426,11 @@ function displayDifferenceResults(differences) { * 显示相同结果 */ function displayIdenticalResults(identical) { - const container = document.getElementById('identicalResults'); + const container = document.getElementById('identical-content'); + if (!container) { + console.error('相同结果容器未找到'); + return; + } if (identical.length === 0) { container.innerHTML = '
没有相同的数据
'; @@ -415,7 +460,11 @@ function displayIdenticalResults(identical) { * 显示缺失结果 */ function displayMissingResults(missing) { - const container = document.getElementById('missingResults'); + const container = document.getElementById('missing-content'); + if (!container) { + console.error('缺失结果容器未找到'); + return; + } if (missing.length === 0) { container.innerHTML = '
没有缺失的数据
'; @@ -432,13 +481,13 @@ function displayMissingResults(missing) {
${item.cluster1_value !== undefined ? `
- ${currentResults.clusters.cluster1_name}: + ${currentResults.clusters.cluster1_name || '集群1'}:
${formatRedisValue(item.cluster1_value)}
` : ''} ${item.cluster2_value !== undefined ? `
- ${currentResults.clusters.cluster2_name}: + ${currentResults.clusters.cluster2_name || '集群2'}:
${formatRedisValue(item.cluster2_value)}
` : ''} @@ -582,7 +631,10 @@ async function loadDefaultConfig() { * 清空结果 */ function clearResults() { - document.getElementById('results').style.display = 'none'; + const resultSection = document.getElementById('resultSection'); + if (resultSection) { + resultSection.style.display = 'none'; + } currentResults = null; } @@ -590,17 +642,24 @@ function clearResults() { * 显示加载状态 */ function showLoading(message = '正在处理...') { - const loadingElement = document.querySelector('.loading'); - const messageElement = loadingElement.querySelector('span'); - messageElement.textContent = message; - loadingElement.style.display = 'block'; + const loadingElement = document.querySelector('.loading') || document.getElementById('loadingIndicator'); + if (loadingElement) { + const messageElement = loadingElement.querySelector('span') || loadingElement.querySelector('#loadingText'); + if (messageElement) { + messageElement.textContent = message; + } + loadingElement.style.display = 'block'; + } } /** * 隐藏加载状态 */ function hideLoading() { - document.querySelector('.loading').style.display = 'none'; + const loadingElement = document.querySelector('.loading') || document.getElementById('loadingIndicator'); + if (loadingElement) { + loadingElement.style.display = 'none'; + } } /** @@ -644,6 +703,11 @@ async function loadRedisConfigGroups() { const result = await response.json(); const select = document.getElementById('redisConfigGroupSelect'); + if (!select) { + console.error('未找到Redis配置组下拉框元素: redisConfigGroupSelect'); + return; + } + select.innerHTML = ''; if (result.success && result.data) { @@ -653,9 +717,13 @@ async function loadRedisConfigGroups() { option.textContent = `${group.name} - ${group.description || '无描述'}`; select.appendChild(option); }); + console.log(`成功加载 ${result.data.length} 个Redis配置组`); + } else { + console.log('没有找到Redis配置组数据'); } } catch (error) { console.error('加载Redis配置组失败:', error); + showAlert('加载Redis配置组失败: ' + error.message, 'danger'); } } @@ -663,16 +731,16 @@ async function loadRedisConfigGroups() { function showSaveRedisConfigDialog() { // 生成默认名称 const timestamp = new Date().toLocaleString('zh-CN'); - document.getElementById('redisConfigGroupName').value = `Redis配置_${timestamp}`; - document.getElementById('redisConfigGroupDescription').value = ''; + document.getElementById('redisConfigName').value = `Redis配置_${timestamp}`; + document.getElementById('redisConfigDescription').value = ''; new bootstrap.Modal(document.getElementById('saveRedisConfigModal')).show(); } // 保存Redis配置组 async function saveRedisConfigGroup() { - const name = document.getElementById('redisConfigGroupName').value.trim(); - const description = document.getElementById('redisConfigGroupDescription').value.trim(); + const name = document.getElementById('redisConfigName').value.trim(); + const description = document.getElementById('redisConfigDescription').value.trim(); if (!name) { showAlert('请输入配置组名称', 'warning'); @@ -759,39 +827,40 @@ async function loadSelectedRedisConfigGroup() { // 加载集群配置到界面 function loadClusterConfig(clusterId, config) { // 设置集群名称 - document.getElementById(`${clusterId}-name`).value = config.name || ''; - - // 设置密码 - document.getElementById(`${clusterId}-password`).value = config.password || ''; - - // 设置超时和连接数 - document.getElementById(`${clusterId}-timeout`).value = config.socket_timeout || 3; - document.getElementById(`${clusterId}-max-conn`).value = config.max_connections_per_node || 16; - - // 清空现有节点 - const container = document.getElementById(`${clusterId}-nodes`); - container.innerHTML = ''; - - // 添加节点 - if (config.nodes && config.nodes.length > 0) { - config.nodes.forEach(node => { - const nodeInput = document.createElement('div'); - nodeInput.className = 'node-input'; - nodeInput.innerHTML = ` - - - - `; - container.appendChild(nodeInput); - }); - } else { - // 添加默认节点 - addNode(clusterId); + const nameElement = document.getElementById(`${clusterId}Name`); + if (nameElement && config.name) { + nameElement.value = config.name; } - updateNodeDeleteButtons(); + // 设置密码 + const passwordElement = document.getElementById(`${clusterId}Password`); + if (passwordElement && config.password) { + passwordElement.value = config.password; + } + + // 清空现有节点 + const container = document.getElementById(`${clusterId}Nodes`); + if (container) { + container.innerHTML = ''; + + // 添加节点 + if (config.nodes && config.nodes.length > 0) { + config.nodes.forEach(node => { + addRedisNode(clusterId); + const nodeInputs = container.querySelectorAll('.node-input'); + const lastNodeInput = nodeInputs[nodeInputs.length - 1]; + + const hostInput = lastNodeInput.querySelector('input[type="text"]'); + const portInput = lastNodeInput.querySelector('input[type="number"]'); + + if (hostInput) hostInput.value = node.host || ''; + if (portInput) portInput.value = node.port || 6379; + }); + } else { + // 添加默认节点 + addRedisNode(clusterId); + } + } } // 加载查询选项 @@ -822,22 +891,28 @@ async function loadRedisConfigGroupsForManagement() { const result = await response.json(); const container = document.getElementById('redisConfigGroupList'); + if (!container) { + console.error('Redis配置组列表容器未找到: redisConfigGroupList'); + showAlert('Redis配置组列表容器未找到,请检查页面结构', 'danger'); + return; + } if (result.success && result.data && result.data.length > 0) { let html = '
'; html += ''; result.data.forEach(group => { + const safeName = (group.name || '').replace(/'/g, "\\'"); html += ` - + @@ -847,11 +922,18 @@ async function loadRedisConfigGroupsForManagement() { html += '
名称描述创建时间操作
${group.name}${group.name || '未命名'} ${group.description || '无描述'} ${new Date(group.created_at).toLocaleString('zh-CN')} -
'; container.innerHTML = html; + console.log(`成功加载 ${result.data.length} 个Redis配置组到管理界面`); } else { container.innerHTML = '
暂无Redis配置组
'; + console.log('没有找到Redis配置组数据'); } } catch (error) { - document.getElementById('redisConfigGroupList').innerHTML = '
加载失败: ' + error.message + '
'; + const container = document.getElementById('redisConfigGroupList'); + if (container) { + container.innerHTML = '
加载失败: ' + error.message + '
'; + } + console.error('加载Redis配置组失败:', error); + showAlert('加载Redis配置组失败: ' + error.message, 'danger'); } } @@ -960,6 +1042,11 @@ async function loadRedisQueryHistory() { const result = await response.json(); const select = document.getElementById('redisHistorySelect'); + if (!select) { + console.error('未找到Redis查询历史下拉框元素: redisHistorySelect'); + return; + } + select.innerHTML = ''; if (result.success && result.data) { @@ -969,9 +1056,13 @@ async function loadRedisQueryHistory() { option.textContent = `${history.name} - ${new Date(history.created_at).toLocaleString('zh-CN')}`; select.appendChild(option); }); + console.log(`成功加载 ${result.data.length} 个Redis查询历史记录`); + } else { + console.log('没有找到Redis查询历史数据'); } } catch (error) { console.error('加载Redis查询历史失败:', error); + showAlert('加载Redis查询历史失败: ' + error.message, 'danger'); } } @@ -1109,25 +1200,31 @@ async function loadRedisHistoryForManagement() { const result = await response.json(); const container = document.getElementById('redisHistoryList'); + if (!container) { + console.error('历史记录列表容器未找到: redisHistoryList'); + showAlert('历史记录列表容器未找到,请检查页面结构', 'danger'); + return; + } if (result.success && result.data && result.data.length > 0) { let html = '
'; html += ''; result.data.forEach(history => { + const safeName = (history.name || '').replace(/'/g, "\\'"); html += ` - + - - - + + + @@ -1137,11 +1234,18 @@ async function loadRedisHistoryForManagement() { html += '
名称描述Key数量差异数执行时间创建时间操作
${history.name}${history.name || '未命名'} ${history.description || '无描述'}${history.total_keys}${history.different_count}${history.execution_time.toFixed(3)}s${history.total_keys || 0}${history.different_count || 0}${(history.execution_time || 0).toFixed(3)}s ${new Date(history.created_at).toLocaleString('zh-CN')} -
'; container.innerHTML = html; + console.log(`成功加载 ${result.data.length} 个Redis历史记录到管理界面`); } else { container.innerHTML = '
暂无Redis查询历史
'; + console.log('没有找到Redis历史记录数据'); } } catch (error) { - document.getElementById('redisHistoryList').innerHTML = '
加载失败: ' + error.message + '
'; + const container = document.getElementById('redisHistoryList'); + if (container) { + container.innerHTML = '
加载失败: ' + error.message + '
'; + } + console.error('加载Redis历史记录失败:', error); + showAlert('加载Redis历史记录失败: ' + error.message, 'danger'); } } @@ -1227,41 +1331,31 @@ function showRedisQueryLogsDialog() { // 加载Redis查询日志 async function loadRedisQueryLogs() { try { - const response = await fetch('/api/query-logs?limit=1000'); + const response = await fetch('/api/redis/query-logs?limit=1000'); const result = await response.json(); const container = document.getElementById('redisQueryLogs'); if (result.success && result.data && result.data.length > 0) { - // 过滤Redis相关的日志 - const redisLogs = result.data.filter(log => - (log.message && log.message.toLowerCase().includes('redis')) || - log.query_type === 'redis' - ); - - if (redisLogs.length > 0) { - let html = ''; - redisLogs.forEach(log => { - const levelClass = log.level === 'ERROR' ? 'text-danger' : - log.level === 'WARNING' ? 'text-warning' : 'text-info'; - const timestamp = log.timestamp || '未知时间'; - const level = log.level || 'INFO'; - const message = log.message || '无消息内容'; - - html += ` -
- [${timestamp}] - ${level} - ${message} -
- `; - }); - container.innerHTML = html; - } else { - container.innerHTML = '
暂无Redis查询日志
'; - } + let html = ''; + result.data.forEach(log => { + const levelClass = log.level === 'ERROR' ? 'text-danger' : + log.level === 'WARNING' ? 'text-warning' : 'text-info'; + const timestamp = log.timestamp || '未知时间'; + const level = log.level || 'INFO'; + const message = log.message || '无消息内容'; + + html += ` +
+ [${timestamp}] + ${level} + ${message} +
+ `; + }); + container.innerHTML = html; } else { - container.innerHTML = '
暂无查询日志
'; + container.innerHTML = '
暂无Redis查询日志
'; } } catch (error) { document.getElementById('redisQueryLogs').innerHTML = '
加载日志失败: ' + error.message + '
'; @@ -1281,7 +1375,7 @@ async function clearRedisQueryLogs() { } try { - const response = await fetch('/api/query-logs', { + const response = await fetch('/api/redis/query-logs', { method: 'DELETE' }); @@ -1296,4 +1390,481 @@ async function clearRedisQueryLogs() { } catch (error) { showAlert(`清空日志失败: ${error.message}`, 'danger'); } +} + +// 显示导入Redis配置对话框 +function showImportRedisConfigDialog() { + updateConfigTemplate(); + new bootstrap.Modal(document.getElementById('importRedisConfigModal')).show(); +} + +// 更新配置模板 +function updateConfigTemplate() { + const format = document.getElementById('configFormat').value; + const templateElement = document.getElementById('configTemplate'); + + if (format === 'yaml') { + templateElement.textContent = `# Redis集群配置示例 (YAML格式) +cluster1: + clusterName: "redis-production" + clusterAddress: "10.20.2.109:6470" + clusterPassword: "" + cachePrefix: "message.status.Reader." + cacheTtl: 2000 + async: true + nodes: + - host: "10.20.2.109" + port: 6470 + - host: "10.20.2.110" + port: 6470 + +cluster2: + clusterName: "redis-test" + clusterAddress: "10.20.2.109:6471" + clusterPassword: "" + cachePrefix: "message.status.Reader." + cacheTtl: 2000 + async: true + nodes: + - host: "10.20.2.109" + port: 6471 + - host: "10.20.2.110" + port: 6471 + +queryOptions: + mode: "random" # random 或 specified + count: 100 # 随机采样数量 + pattern: "*" # Key匹配模式 + sourceCluster: "cluster2" + # 指定Key模式下的键值列表 + keys: + - "user:1001" + - "user:1002"`; + } else { + templateElement.textContent = `{ + "cluster1": { + "clusterName": "redis-production", + "clusterAddress": "10.20.2.109:6470", + "clusterPassword": "", + "cachePrefix": "message.status.Reader.", + "cacheTtl": 2000, + "async": true, + "nodes": [ + {"host": "10.20.2.109", "port": 6470}, + {"host": "10.20.2.110", "port": 6470} + ] + }, + "cluster2": { + "clusterName": "redis-test", + "clusterAddress": "10.20.2.109:6471", + "clusterPassword": "", + "cachePrefix": "message.status.Reader.", + "cacheTtl": 2000, + "async": true, + "nodes": [ + {"host": "10.20.2.109", "port": 6471}, + {"host": "10.20.2.110", "port": 6471} + ] + }, + "queryOptions": { + "mode": "random", + "count": 100, + "pattern": "*", + "sourceCluster": "cluster2", + "keys": ["user:1001", "user:1002"] + } +}`; + } +} + +// 导入Redis配置 +async function importRedisConfig() { + const format = document.getElementById('configFormat').value; + const content = document.getElementById('configContent').value.trim(); + + if (!content) { + showAlert('请输入配置内容', 'warning'); + return; + } + + try { + let config; + + if (format === 'yaml') { + // 解析YAML格式(简单解析,不使用第三方库) + config = parseSimpleYaml(content); + } else { + // 解析JSON格式 + config = JSON.parse(content); + } + + // 验证配置结构 + if (!config.cluster1 || !config.cluster2) { + showAlert('配置格式错误:缺少cluster1或cluster2配置', 'danger'); + return; + } + + // 应用配置到页面 + if (config.cluster1) { + loadClusterConfig('cluster1', config.cluster1); + } + + if (config.cluster2) { + loadClusterConfig('cluster2', config.cluster2); + } + + if (config.queryOptions) { + loadQueryOptions(config.queryOptions); + } + + // 关闭对话框 + bootstrap.Modal.getInstance(document.getElementById('importRedisConfigModal')).hide(); + + showAlert('配置导入成功!', 'success'); + + } catch (error) { + showAlert(`配置解析失败: ${error.message}`, 'danger'); + } +} + +// 简单的YAML解析器(仅支持基本格式) +function parseSimpleYaml(yamlContent) { + const lines = yamlContent.split('\n'); + const result = {}; + let currentSection = null; + let currentSubSection = null; + + for (let line of lines) { + line = line.trim(); + + // 跳过注释和空行 + if (!line || line.startsWith('#')) continue; + + // 检查是否是主节点 + if (line.match(/^[a-zA-Z][a-zA-Z0-9_]*:$/)) { + currentSection = line.slice(0, -1); + result[currentSection] = {}; + currentSubSection = null; + } + // 检查是否是子节点 + else if (line.match(/^[a-zA-Z][a-zA-Z0-9_]*:/) && currentSection) { + const [key, ...valueParts] = line.split(':'); + let value = valueParts.join(':').trim(); + + // 处理字符串值 + if (value.startsWith('"') && value.endsWith('"')) { + value = value.slice(1, -1); + } else if (value === 'true') { + value = true; + } else if (value === 'false') { + value = false; + } else if (!isNaN(value) && value !== '') { + value = Number(value); + } + + if (key === 'nodes') { + result[currentSection][key] = []; + currentSubSection = key; + } else { + result[currentSection][key] = value; + } + } + // 处理数组项 + else if (line.startsWith('- ') && currentSubSection === 'nodes') { + const nodeStr = line.substring(2).trim(); + if (nodeStr.includes('host:') && nodeStr.includes('port:')) { + // 解析 "host: xxx, port: xxx" 格式 + const hostMatch = nodeStr.match(/host:\s*"?([^",]+)"?/); + const portMatch = nodeStr.match(/port:\s*(\d+)/); + + if (hostMatch && portMatch) { + result[currentSection][currentSubSection].push({ + host: hostMatch[1], + port: parseInt(portMatch[1]) + }); + } + } + } + } + + return result; +} + +// 添加Redis节点 +function addRedisNode(clusterId) { + const nodeContainer = document.getElementById(`${clusterId}Nodes`); + + const nodeDiv = document.createElement('div'); + nodeDiv.className = 'node-input'; + nodeDiv.innerHTML = ` + + + + `; + + nodeContainer.appendChild(nodeDiv); +} + +// 删除Redis节点 +function removeRedisNode(button, clusterId) { + const nodeContainer = document.getElementById(`${clusterId}Nodes`); + const nodeInputs = nodeContainer.querySelectorAll('.node-input'); + + // 至少保留一个节点 + if (nodeInputs.length <= 1) { + showAlert('至少需要保留一个Redis节点', 'warning'); + return; + } + + // 删除当前节点 + button.parentElement.remove(); +} + +// 切换查询模式 +function toggleQueryMode() { + const randomMode = document.getElementById('randomMode'); + const randomOptions = document.getElementById('randomOptions'); + const specifiedOptions = document.getElementById('specifiedOptions'); + + if (randomMode.checked) { + randomOptions.style.display = 'block'; + specifiedOptions.style.display = 'none'; + } else { + randomOptions.style.display = 'none'; + specifiedOptions.style.display = 'block'; + } +} + +/** + * 原生数据展示功能 + */ + +// 全局变量存储原始数据和当前视图状态 +let rawDataState = { + cluster1: { data: null, view: 'formatted' }, + cluster2: { data: null, view: 'formatted' } +}; + +/** + * 显示原生数据 + */ +function displayRawData(results) { + console.log('开始显示原生数据'); + + // 从结果中收集所有键值数据 + const cluster1Data = collectClusterData(results, 'cluster1'); + const cluster2Data = collectClusterData(results, 'cluster2'); + + // 存储原始数据 + rawDataState.cluster1.data = cluster1Data; + rawDataState.cluster2.data = cluster2Data; + + // 显示数据 + renderRawData('cluster1', cluster1Data, 'formatted'); + renderRawData('cluster2', cluster2Data, 'formatted'); + + console.log(`原生数据显示完成 - 集群1: ${cluster1Data.length}条, 集群2: ${cluster2Data.length}条`); +} + +/** + * 从结果中收集集群数据 + */ +function collectClusterData(results, clusterType) { + const data = []; + const clusterField = clusterType === 'cluster1' ? 'cluster1_value' : 'cluster2_value'; + + // 从相同结果中收集数据 + if (results.identical_results) { + results.identical_results.forEach(item => { + if (item.key && item.value !== undefined) { + data.push({ + key: item.key, + value: item.value, + type: 'identical' + }); + } + }); + } + + // 从差异结果中收集数据 + if (results.different_results) { + results.different_results.forEach(item => { + if (item.key && item[clusterField] !== undefined) { + data.push({ + key: item.key, + value: item[clusterField], + type: 'different' + }); + } + }); + } + + // 从缺失结果中收集数据 + if (results.missing_results) { + results.missing_results.forEach(item => { + if (item.key && item[clusterField] !== undefined) { + data.push({ + key: item.key, + value: item[clusterField], + type: 'missing' + }); + } + }); + } + + return data; +} + +/** + * 渲染原生数据 + */ +function renderRawData(clusterId, data, viewType) { + const container = document.getElementById(`${clusterId}-raw-data`); + if (!container) { + console.error(`原生数据容器未找到: ${clusterId}-raw-data`); + return; + } + + if (!data || data.length === 0) { + container.innerHTML = '
无数据
'; + return; + } + + let html = ''; + + if (viewType === 'formatted') { + // 格式化视图 + html = '
'; + data.forEach((item, index) => { + const typeClass = getTypeClass(item.type); + const formattedValue = formatRedisValue(item.value); + + html += ` +
+
+ Key: ${escapeHtml(item.key)} + ${item.type} +
+
+
${escapeHtml(formattedValue)}
+
+
+ `; + }); + html += '
'; + } else { + // 原始视图 + html = '
'; + html += '
';
+        data.forEach((item, index) => {
+            html += `Key: ${escapeHtml(item.key)}\n`;
+            html += `Type: ${item.type}\n`;
+            html += `Value: ${escapeHtml(String(item.value))}\n`;
+            if (index < data.length - 1) {
+                html += '\n' + '='.repeat(50) + '\n\n';
+            }
+        });
+        html += '
'; + html += '
'; + } + + container.innerHTML = html; +} + +/** + * 获取类型对应的CSS类 + */ +function getTypeClass(type) { + switch (type) { + case 'identical': return 'border-success'; + case 'different': return 'border-warning'; + case 'missing': return 'border-danger'; + default: return 'border-secondary'; + } +} + +/** + * HTML转义 + */ +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +/** + * 切换原生数据视图 + */ +function switchRawDataView(clusterId, viewType) { + if (!rawDataState[clusterId] || !rawDataState[clusterId].data) { + console.error(`没有${clusterId}的原生数据`); + return; + } + + // 更新按钮状态 + const container = document.getElementById(`${clusterId}-raw-data`); + const buttonGroup = container.parentElement.querySelector('.btn-group'); + + if (buttonGroup) { + const buttons = buttonGroup.querySelectorAll('button'); + buttons.forEach(btn => { + btn.classList.remove('active'); + const btnText = btn.textContent.trim(); + if ((viewType === 'formatted' && btnText === '格式化') || + (viewType === 'raw' && btnText === '原始')) { + btn.classList.add('active'); + } + }); + } + + // 更新视图状态 + rawDataState[clusterId].view = viewType; + + // 重新渲染数据 + renderRawData(clusterId, rawDataState[clusterId].data, viewType); + + console.log(`${clusterId}视图已切换到: ${viewType}`); +} + +/** + * 导出原生数据 + */ +function exportRawData(clusterId) { + if (!rawDataState[clusterId] || !rawDataState[clusterId].data) { + showAlert(`没有${clusterId}的原生数据可导出`, 'warning'); + return; + } + + const data = rawDataState[clusterId].data; + const clusterName = clusterId === 'cluster1' ? + (currentResults?.clusters?.cluster1_name || '集群1') : + (currentResults?.clusters?.cluster2_name || '集群2'); + + // 生成导出内容 + let content = `Redis原生数据导出 - ${clusterName}\n`; + content += `导出时间: ${new Date().toLocaleString('zh-CN')}\n`; + content += `数据总数: ${data.length}\n`; + content += '='.repeat(60) + '\n\n'; + + data.forEach((item, index) => { + content += `[${index + 1}] Key: ${item.key}\n`; + content += `Type: ${item.type}\n`; + content += `Value:\n${item.value}\n`; + content += '\n' + '-'.repeat(40) + '\n\n'; + }); + + // 创建下载 + const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `redis_raw_data_${clusterId}_${new Date().getTime()}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + showAlert(`${clusterName}原生数据导出成功`, 'success'); } \ No newline at end of file diff --git a/templates/redis_compare.html b/templates/redis_compare.html index b5162ec..7ffb5a1 100644 --- a/templates/redis_compare.html +++ b/templates/redis_compare.html @@ -67,11 +67,28 @@ .node-input input { margin-right: 10px; } - .performance-section { - background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%); + .btn-redis { + background: linear-gradient(135deg, #dc143c 0%, #b91c1c 100%); + border: none; + color: white; + } + .btn-redis:hover { + background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%); + color: white; + transform: translateY(-1px); + } + .redis-logo { + color: #dc143c; + } + .cluster-config { + border: 2px solid #e9ecef; border-radius: 10px; - padding: 20px; - margin-top: 20px; + padding: 15px; + margin-bottom: 15px; + } + .cluster-config.active { + border-color: #dc143c; + background-color: #fff8f8; } .log-viewer { background-color: #1e1e1e; @@ -94,28 +111,22 @@ color: #007bff; text-decoration: none; } - .btn-redis { - background: linear-gradient(135deg, #dc143c 0%, #b91c1c 100%); - border: none; - color: white; + .pagination { + --bs-pagination-padding-x: 0.5rem; + --bs-pagination-padding-y: 0.25rem; + --bs-pagination-font-size: 0.875rem; } - .btn-redis:hover { - background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%); - color: white; - transform: translateY(-1px); - } - .redis-logo { - color: #dc143c; - } - .cluster-config { - border: 2px solid #e9ecef; - border-radius: 10px; - padding: 20px; - margin-bottom: 15px; - } - .cluster-config.active { - border-color: #dc143c; - background-color: #fff8f8; + + /* 确保提示消息在最顶层 */ + .alert { + position: fixed !important; + top: 20px !important; + left: 50% !important; + transform: translateX(-50%) !important; + z-index: 9999 !important; + min-width: 300px !important; + max-width: 600px !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important; } @@ -141,7 +152,7 @@
-
+
-
+
-
- -
-

Redis集群比对工具

-

专业的Redis集群数据比对工具,支持随机采样和指定Key查询

-
-
+

+ Redis集群比对工具 + 专业的Redis集群数据比对工具,支持随机采样和指定Key查询 +

- - +
- -
-
-

配置管理

- -
- -
-
-
-
配置组管理
-
-
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
-
- - -
-
-
-
查询历史
-
-
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
-
-
- - -
-
-
-
-
配置导入
-
-
- - - - 支持YAML格式配置导入,如:clusterName、clusterAddress、clusterPassword等 - -
-
-
-
-
-
-
查询日志
-
-
- - - 查看Redis比较操作的详细执行日志 - -
-
-
-
-
-
- - -
+ +
-

Redis集群配置

+

配置管理

-
- -
-
-
集群1 (生产环境)
- -
- - + +
+
+
配置组管理
+
+
+
+
+
- -
- -
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
历史记录与日志
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ + +
+
+
Redis集群配置
+
+
+ +
+
集群1 (生产)
+
+
+ + +
+
+
+ +
- - -
-
- -
- - -
-
- - + +
- - +
- -
- -
-
- - -
-
-
集群2 (测试环境)
- -
- - + + +
+
集群2 (测试)
+
+
+ + +
- -
- -
+
+ +
- - -
-
- -
- - -
-
- - + +
- - + +
+
+
+
+
+
+
+ + +
+
+

查询配置

+ + +
+
+
查询模式
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+ + +
+ +
+
+
+
+ + +
+ + +
+
+ + + -
- - -
-
-
-

查询选项

- -
- -
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
- - -
-
-
- - - -
-
-
- - -
-
-
- - - -
-
- Loading... -
- 正在执行Redis数据比较,请稍候... -
-
-
-
- - -