/**
* Redis比较工具的JavaScript功能
* 支持Redis集群配置、连接测试、数据比较等功能
*/
// 全局变量
let currentResults = null;
let isQuerying = false;
let currentImportTarget = null; // 记录当前导入目标集群
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initializePage();
bindEvents();
loadRedisConfigGroups();
loadRedisQueryHistory();
});
/**
* 初始化页面
*/
function initializePage() {
// 加载默认配置
loadDefaultConfig();
// 绑定节点删除事件
bindNodeEvents();
// 绑定查询模式切换事件
bindQueryModeEvents();
}
/**
* 绑定事件
*/
function bindEvents() {
// 查询模式切换
document.querySelectorAll('input[name="queryMode"]').forEach(radio => {
radio.addEventListener('change', toggleQueryMode);
});
// 节点删除按钮事件委托
document.addEventListener('click', function(e) {
if (e.target.closest('.remove-node')) {
e.target.closest('.node-input').remove();
}
});
}
/**
* 绑定节点相关事件
*/
function bindNodeEvents() {
// 初始绑定现有的删除按钮(如果只有一个节点则禁用删除)
updateNodeDeleteButtons();
}
/**
* 绑定查询模式切换事件
*/
function bindQueryModeEvents() {
const randomMode = document.getElementById('randomMode');
const specifiedMode = document.getElementById('specifiedMode');
randomMode.addEventListener('change', toggleQueryMode);
specifiedMode.addEventListener('change', toggleQueryMode);
}
/**
* 切换查询模式
*/
function toggleQueryMode() {
const randomMode = document.getElementById('randomMode').checked;
const randomOptions = document.getElementById('randomOptions');
const specifiedOptions = document.getElementById('specifiedOptions');
if (randomMode) {
randomOptions.style.display = 'block';
specifiedOptions.style.display = 'none';
} else {
randomOptions.style.display = 'none';
specifiedOptions.style.display = 'block';
}
}
/**
* 添加节点
*/
function addNode(clusterId) {
const container = document.getElementById(`${clusterId}Nodes`);
if (!container) {
console.error(`节点容器未找到: ${clusterId}Nodes`);
return;
}
const nodeInput = document.createElement('div');
nodeInput.className = 'node-input';
nodeInput.innerHTML = `
`;
container.appendChild(nodeInput);
updateNodeDeleteButtons();
}
/**
* 更新节点删除按钮状态
*/
function updateNodeDeleteButtons() {
// 每个集群至少需要一个节点
['cluster1', 'cluster2'].forEach(clusterId => {
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;
});
}
});
}
/**
* 获取集群配置
*/
function getClusterConfig(clusterId) {
const name = document.getElementById(`${clusterId}Name`).value;
const password = document.getElementById(`${clusterId}Password`).value;
// 获取节点列表
const nodes = [];
const nodeInputs = document.querySelectorAll(`#${clusterId}Nodes .node-input`);
nodeInputs.forEach(nodeInput => {
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 });
}
});
return {
name,
nodes,
password: password || null,
socket_timeout: 3,
socket_connect_timeout: 3,
max_connections_per_node: 16
};
}
/**
* 获取查询选项
*/
function getQueryOptions() {
const randomMode = document.getElementById('randomMode').checked;
if (randomMode) {
return {
mode: 'random',
count: parseInt(document.getElementById('sampleCount').value),
pattern: document.getElementById('keyPattern').value,
source_cluster: document.getElementById('sourceCluster').value
};
} else {
const keysText = document.getElementById('specifiedKeys').value.trim();
const keys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : [];
return {
mode: 'specified',
keys: keys
};
}
}
/**
* 测试连接
*/
async function testConnection(clusterId) {
const config = getClusterConfig(clusterId);
const clusterName = config.name;
if (!config.nodes || config.nodes.length === 0) {
showAlert('请至少配置一个Redis节点', 'warning');
return;
}
try {
showLoading(`正在测试${clusterName}连接...`);
const response = await fetch('/api/redis/test-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
cluster_config: config,
cluster_name: clusterName
})
});
const result = await response.json();
hideLoading();
if (result.success) {
showAlert(`${clusterName}连接成功!连接耗时: ${result.data.connection_time.toFixed(3)}秒`, 'success');
// 高亮成功的集群配置 - 使用正确的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');
}
} catch (error) {
hideLoading();
showAlert(`连接测试失败: ${error.message}`, 'danger');
}
}
/**
* 执行Redis比较
*/
async function executeRedisComparison() {
if (isQuerying) {
showAlert('查询正在进行中,请稍候...', '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: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
cluster1_config: cluster1Config,
cluster2_config: cluster2Config,
query_options: queryOptions
})
});
console.log('Redis比较API响应状态:', response.status);
const result = await response.json();
console.log('Redis比较API响应结果:', result);
if (response.ok && result.success !== false) {
currentResults = result;
displayResults(result);
showAlert('Redis数据比较完成,历史记录已自动保存!', 'success');
// 刷新历史记录列表(后台已自动保存)
loadRedisQueryHistory();
} else {
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) {
console.error('Redis比较请求异常:', error);
showAlert(`请求失败: ${error.message}。请检查网络连接和服务器状态。`, 'danger');
} finally {
isQuerying = false;
hideLoading();
}
}
/**
* 显示结果
*/
function displayResults(results) {
// 显示统计卡片
displayStatsCards(results.stats);
// 显示详细结果
displayDifferenceResults(results.different_results || []);
displayIdenticalResults(results.identical_results || []);
displayMissingResults(results.missing_results || []);
displayPerformanceReport(results.performance_report);
// 显示原生数据
displayRawData(results);
// 更新标签页计数
updateTabCounts(results);
// 显示结果区域 - 使用正确的ID
const resultSection = document.getElementById('resultSection');
if (resultSection) {
resultSection.style.display = 'block';
// 滚动到结果区域
resultSection.scrollIntoView({ behavior: 'smooth' });
}
}
/**
* 显示统计卡片
*/
function displayStatsCards(stats) {
const container = document.getElementById('stats');
if (!container) {
console.error('统计卡片容器未找到');
return;
}
container.innerHTML = `
${stats.total_keys}
总Key数量
${stats.identical_count}
相同数据
${stats.identical_percentage}%
${stats.different_count}
差异数据
${stats.different_percentage}%
${stats.missing_in_cluster1 + stats.missing_in_cluster2 + stats.both_missing}
缺失数据
${stats.missing_percentage}%
`;
}
/**
* 显示差异结果
*/
function displayDifferenceResults(differences) {
const container = document.getElementById('differences-content');
if (!container) {
console.error('差异结果容器未找到');
return;
}
if (differences.length === 0) {
container.innerHTML = '未发现数据差异
';
return;
}
let html = '';
differences.forEach((diff, index) => {
html += `
Key: ${diff.key}
${currentResults.clusters.cluster1_name || '集群1'}:
${formatRedisValue(diff.cluster1_value)}
${currentResults.clusters.cluster2_name || '集群2'}:
${formatRedisValue(diff.cluster2_value)}
${diff.message}
`;
});
container.innerHTML = html;
}
/**
* 显示相同结果
*/
function displayIdenticalResults(identical) {
const container = document.getElementById('identical-content');
if (!container) {
console.error('相同结果容器未找到');
return;
}
if (identical.length === 0) {
container.innerHTML = '没有相同的数据
';
return;
}
let html = '';
identical.forEach((item, index) => {
html += `
Key: ${item.key}
值:
${formatRedisValue(item.value)}
数据一致
`;
});
container.innerHTML = html;
}
/**
* 显示缺失结果
*/
function displayMissingResults(missing) {
const container = document.getElementById('missing-content');
if (!container) {
console.error('缺失结果容器未找到');
return;
}
if (missing.length === 0) {
container.innerHTML = '没有缺失的数据
';
return;
}
let html = '';
missing.forEach((item, index) => {
html += `
Key: ${item.key}
${item.message}
${item.cluster1_value !== undefined ? `
${currentResults.clusters.cluster1_name || '集群1'}:
${formatRedisValue(item.cluster1_value)}
` : ''}
${item.cluster2_value !== undefined ? `
${currentResults.clusters.cluster2_name || '集群2'}:
${formatRedisValue(item.cluster2_value)}
` : ''}
`;
});
container.innerHTML = html;
}
/**
* 显示性能报告
*/
function displayPerformanceReport(performanceReport) {
const container = document.getElementById('performanceReport');
const connections = performanceReport.connections || {};
const operations = performanceReport.operations || {};
let html = `
性能统计报告
连接统计
集群 |
状态 |
耗时 |
`;
Object.entries(connections).forEach(([clusterName, status]) => {
const statusClass = status.success ? 'success' : 'danger';
const statusText = status.success ? '成功' : '失败';
html += `
${clusterName} |
${statusText} |
${status.connect_time.toFixed(3)}s |
`;
});
html += `
操作统计
操作 |
耗时 |
`;
if (operations.scan_time > 0) {
html += `扫描Keys | ${operations.scan_time.toFixed(3)}s |
`;
}
Object.entries(operations.queries || {}).forEach(([operation, duration]) => {
html += `${operation} | ${duration.toFixed(3)}s |
`;
});
if (operations.comparison_time > 0) {
html += `数据比对 | ${operations.comparison_time.toFixed(3)}s |
`;
}
html += `
总耗时 |
${performanceReport.total_time.toFixed(3)}s |
`;
container.innerHTML = html;
}
/**
* 更新标签页计数
*/
function updateTabCounts(results) {
document.getElementById('diff-count').textContent = (results.different_results || []).length;
document.getElementById('identical-count').textContent = (results.identical_results || []).length;
document.getElementById('missing-count').textContent = (results.missing_results || []).length;
}
/**
* 格式化Redis值显示
*/
function formatRedisValue(value) {
if (value === null) {
return '(null)';
}
if (value === undefined) {
return '(undefined)';
}
// 如果是字符串且看起来像JSON,尝试格式化
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value);
return JSON.stringify(parsed, null, 2);
} catch (e) {
// 不是JSON,直接返回
return value;
}
}
return String(value);
}
/**
* 加载默认配置
*/
async function loadDefaultConfig() {
try {
const response = await fetch('/api/redis/default-config');
const config = await response.json();
// 这里可以根据需要设置默认值,当前HTML已经包含了合理的默认值
} catch (error) {
console.warn('加载默认配置失败:', error);
}
}
/**
* 清空结果
*/
function clearResults() {
const resultSection = document.getElementById('resultSection');
if (resultSection) {
resultSection.style.display = 'none';
}
currentResults = null;
}
/**
* 显示加载状态
*/
function showLoading(message = '正在处理...') {
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() {
const loadingElement = document.querySelector('.loading') || document.getElementById('loadingIndicator');
if (loadingElement) {
loadingElement.style.display = 'none';
}
}
/**
* 显示提示消息
*/
function showAlert(message, type = 'info') {
// 移除现有的alert
const existingAlert = document.querySelector('.alert-custom');
if (existingAlert) {
existingAlert.remove();
}
// 创建新的alert
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show alert-custom`;
alertDiv.innerHTML = `
${message}
`;
// 插入到页面顶部
const container = document.querySelector('.container');
container.insertBefore(alertDiv, container.firstChild);
// 5秒后自动消失
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
/**
* Redis配置管理功能
*/
// 加载Redis配置组列表
async function loadRedisConfigGroups() {
try {
const response = await fetch('/api/redis/config-groups');
const result = await response.json();
const select = document.getElementById('redisConfigGroupSelect');
if (!select) {
console.error('未找到Redis配置组下拉框元素: redisConfigGroupSelect');
return;
}
select.innerHTML = '';
if (result.success && result.data) {
result.data.forEach(group => {
const option = document.createElement('option');
option.value = group.id;
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');
}
}
// 显示保存Redis配置对话框
function showSaveRedisConfigDialog() {
// 生成默认名称
const timestamp = new Date().toLocaleString('zh-CN');
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('redisConfigName').value.trim();
const description = document.getElementById('redisConfigDescription').value.trim();
if (!name) {
showAlert('请输入配置组名称', 'warning');
return;
}
const cluster1Config = getClusterConfig('cluster1');
const cluster2Config = getClusterConfig('cluster2');
const queryOptions = getQueryOptions();
if (!cluster1Config.nodes || cluster1Config.nodes.length === 0) {
showAlert('请配置集群1信息', 'warning');
return;
}
if (!cluster2Config.nodes || cluster2Config.nodes.length === 0) {
showAlert('请配置集群2信息', 'warning');
return;
}
try {
const response = await fetch('/api/redis/config-groups', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: name,
description: description,
cluster1_config: cluster1Config,
cluster2_config: cluster2Config,
query_options: queryOptions
})
});
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
bootstrap.Modal.getInstance(document.getElementById('saveRedisConfigModal')).hide();
loadRedisConfigGroups(); // 重新加载配置组列表
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`保存失败: ${error.message}`, 'danger');
}
}
// 加载选定的Redis配置组
async function loadSelectedRedisConfigGroup() {
const groupId = document.getElementById('redisConfigGroupSelect').value;
if (!groupId) {
showAlert('请选择配置组', 'warning');
return;
}
try {
const response = await fetch(`/api/redis/config-groups/${groupId}`);
const result = await response.json();
if (result.success && result.data) {
const config = result.data;
// 加载集群1配置
loadClusterConfig('cluster1', config.cluster1_config);
// 加载集群2配置
loadClusterConfig('cluster2', config.cluster2_config);
// 加载查询选项
loadQueryOptions(config.query_options);
showAlert(`配置组 "${config.name}" 加载成功`, 'success');
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`加载失败: ${error.message}`, 'danger');
}
}
// 加载集群配置到界面
function loadClusterConfig(clusterId, config) {
// 设置集群名称
const nameElement = document.getElementById(`${clusterId}Name`);
if (nameElement && config.name) {
nameElement.value = config.name;
}
// 设置密码
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);
}
}
}
// 加载查询选项
function loadQueryOptions(queryOptions) {
if (queryOptions.mode === 'random') {
document.getElementById('randomMode').checked = true;
document.getElementById('sampleCount').value = queryOptions.count || 100;
document.getElementById('keyPattern').value = queryOptions.pattern || '*';
document.getElementById('sourceCluster').value = queryOptions.source_cluster || 'cluster2';
} else {
document.getElementById('specifiedMode').checked = true;
document.getElementById('specifiedKeys').value = (queryOptions.keys || []).join('\n');
}
toggleQueryMode();
}
// 显示Redis配置管理对话框
function showManageRedisConfigDialog() {
loadRedisConfigGroupsForManagement();
new bootstrap.Modal(document.getElementById('manageRedisConfigModal')).show();
}
// 为管理界面加载Redis配置组
async function loadRedisConfigGroupsForManagement() {
try {
const response = await fetch('/api/redis/config-groups');
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 += `
${group.name || '未命名'} |
${group.description || '无描述'} |
${new Date(group.created_at).toLocaleString('zh-CN')} |
|
`;
});
html += '
';
container.innerHTML = html;
console.log(`成功加载 ${result.data.length} 个Redis配置组到管理界面`);
} else {
container.innerHTML = '暂无Redis配置组
';
console.log('没有找到Redis配置组数据');
}
} catch (error) {
const container = document.getElementById('redisConfigGroupList');
if (container) {
container.innerHTML = '加载失败: ' + error.message + '
';
}
console.error('加载Redis配置组失败:', error);
showAlert('加载Redis配置组失败: ' + error.message, 'danger');
}
}
// 通过ID加载Redis配置组
async function loadRedisConfigGroupById(groupId) {
try {
const response = await fetch(`/api/redis/config-groups/${groupId}`);
const result = await response.json();
if (result.success && result.data) {
const config = result.data;
// 加载配置
loadClusterConfig('cluster1', config.cluster1_config);
loadClusterConfig('cluster2', config.cluster2_config);
loadQueryOptions(config.query_options);
// 关闭管理对话框
bootstrap.Modal.getInstance(document.getElementById('manageRedisConfigModal')).hide();
showAlert(`配置组 "${config.name}" 加载成功`, 'success');
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`加载失败: ${error.message}`, 'danger');
}
}
// 删除Redis配置组
async function deleteRedisConfigGroup(groupId, groupName) {
if (!confirm(`确定要删除配置组 "${groupName}" 吗?此操作不可恢复。`)) {
return;
}
try {
const response = await fetch(`/api/redis/config-groups/${groupId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
loadRedisConfigGroupsForManagement(); // 重新加载列表
loadRedisConfigGroups(); // 重新加载下拉框
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`删除失败: ${error.message}`, 'danger');
}
}
// 显示Redis配置导入对话框
function showImportRedisConfigDialog(targetCluster) {
currentImportTarget = targetCluster;
document.getElementById('redisConfigImportText').value = '';
new bootstrap.Modal(document.getElementById('importRedisConfigModal')).show();
}
// 导入Redis配置
async function importRedisConfig() {
const configText = document.getElementById('redisConfigImportText').value.trim();
if (!configText) {
showAlert('请输入配置内容', 'warning');
return;
}
try {
const response = await fetch('/api/redis/import-config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
config_text: configText
})
});
const result = await response.json();
if (result.success && result.data) {
// 将配置应用到目标集群
loadClusterConfig(currentImportTarget, result.data);
bootstrap.Modal.getInstance(document.getElementById('importRedisConfigModal')).hide();
showAlert(result.message, 'success');
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`导入失败: ${error.message}`, 'danger');
}
}
/**
* Redis查询历史管理功能
*/
// 加载Redis查询历史
async function loadRedisQueryHistory() {
try {
const response = await fetch('/api/redis/query-history');
const result = await response.json();
const select = document.getElementById('redisHistorySelect');
if (!select) {
console.error('未找到Redis查询历史下拉框元素: redisHistorySelect');
return;
}
select.innerHTML = '';
if (result.success && result.data) {
result.data.forEach(history => {
const option = document.createElement('option');
option.value = history.id;
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');
}
}
// 显示保存Redis查询历史对话框
function showSaveRedisHistoryDialog() {
if (!currentResults) {
showAlert('请先执行Redis比较查询', 'warning');
return;
}
// 生成默认名称
const timestamp = new Date().toLocaleString('zh-CN');
document.getElementById('redisHistoryName').value = `Redis查询_${timestamp}`;
document.getElementById('redisHistoryDescription').value = `Redis比较结果 - 总计${currentResults.stats.total_keys}个Key,发现${currentResults.stats.different_count}处差异`;
new bootstrap.Modal(document.getElementById('saveRedisHistoryModal')).show();
}
// 保存Redis查询历史
async function saveRedisQueryHistory() {
if (!currentResults) {
showAlert('没有可保存的查询结果', 'warning');
return;
}
const name = document.getElementById('redisHistoryName').value.trim();
const description = document.getElementById('redisHistoryDescription').value.trim();
if (!name) {
showAlert('请输入历史记录名称', 'warning');
return;
}
try {
const response = await fetch('/api/redis/query-history', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: name,
description: description,
cluster1_config: getClusterConfig('cluster1'),
cluster2_config: getClusterConfig('cluster2'),
query_options: getQueryOptions(),
query_keys: currentResults.query_options?.keys || [],
results_summary: currentResults.stats,
execution_time: currentResults.performance_report?.total_time || 0,
total_keys: currentResults.stats.total_keys,
different_count: currentResults.stats.different_count,
identical_count: currentResults.stats.identical_count,
missing_count: currentResults.stats.missing_in_cluster1 + currentResults.stats.missing_in_cluster2,
raw_results: {
identical_results: currentResults.identical_results,
different_results: currentResults.different_results,
missing_results: currentResults.missing_results,
performance_report: currentResults.performance_report
}
})
});
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
bootstrap.Modal.getInstance(document.getElementById('saveRedisHistoryModal')).hide();
loadRedisQueryHistory(); // 重新加载历史记录列表
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`保存失败: ${error.message}`, 'danger');
}
}
// 加载选定的Redis查询历史
async function loadSelectedRedisHistory() {
const historyId = document.getElementById('redisHistorySelect').value;
if (!historyId) {
showAlert('请选择历史记录', 'warning');
return;
}
try {
const response = await fetch(`/api/redis/query-history/${historyId}`);
const result = await response.json();
if (result.success && result.data) {
const history = result.data;
// 加载配置
loadClusterConfig('cluster1', history.cluster1_config);
loadClusterConfig('cluster2', history.cluster2_config);
loadQueryOptions(history.query_options);
// 如果有原始结果,直接显示
if (history.raw_results) {
const resultsData = {
stats: history.results_summary,
identical_results: history.raw_results.identical_results || [],
different_results: history.raw_results.different_results || [],
missing_results: history.raw_results.missing_results || [],
performance_report: history.raw_results.performance_report || {},
clusters: {
cluster1_name: history.cluster1_config.name,
cluster2_name: history.cluster2_config.name
}
};
currentResults = resultsData;
displayResults(resultsData);
showAlert(`历史记录 "${history.name}" 加载成功`, 'success');
} else {
showAlert(`历史记录 "${history.name}" 配置加载成功,但没有结果数据`, 'info');
}
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`加载失败: ${error.message}`, 'danger');
}
}
// 显示Redis查询历史管理对话框
function showManageRedisHistoryDialog() {
loadRedisHistoryForManagement();
new bootstrap.Modal(document.getElementById('manageRedisHistoryModal')).show();
}
// 为管理界面加载Redis查询历史
async function loadRedisHistoryForManagement() {
try {
const response = await fetch('/api/redis/query-history');
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 += '名称 | 描述 | Key数量 | 差异数 | 执行时间 | 创建时间 | 操作 |
';
result.data.forEach(history => {
const safeName = (history.name || '').replace(/'/g, "\\'");
html += `
${history.name || '未命名'} |
${history.description || '无描述'} |
${history.total_keys || 0} |
${history.different_count || 0} |
${(history.execution_time || 0).toFixed(3)}s |
${new Date(history.created_at).toLocaleString('zh-CN')} |
|
`;
});
html += '
';
container.innerHTML = html;
console.log(`成功加载 ${result.data.length} 个Redis历史记录到管理界面`);
} else {
container.innerHTML = '暂无Redis查询历史
';
console.log('没有找到Redis历史记录数据');
}
} catch (error) {
const container = document.getElementById('redisHistoryList');
if (container) {
container.innerHTML = '加载失败: ' + error.message + '
';
}
console.error('加载Redis历史记录失败:', error);
showAlert('加载Redis历史记录失败: ' + error.message, 'danger');
}
}
// 通过ID加载Redis查询历史
async function loadRedisHistoryById(historyId) {
try {
const response = await fetch(`/api/redis/query-history/${historyId}`);
const result = await response.json();
if (result.success && result.data) {
const history = result.data;
// 加载配置
loadClusterConfig('cluster1', history.cluster1_config);
loadClusterConfig('cluster2', history.cluster2_config);
loadQueryOptions(history.query_options);
// 如果有原始结果,直接显示
if (history.raw_results) {
const resultsData = {
stats: history.results_summary,
identical_results: history.raw_results.identical_results || [],
different_results: history.raw_results.different_results || [],
missing_results: history.raw_results.missing_results || [],
performance_report: history.raw_results.performance_report || {},
clusters: {
cluster1_name: history.cluster1_config.name,
cluster2_name: history.cluster2_config.name
}
};
currentResults = resultsData;
displayResults(resultsData);
}
// 关闭管理对话框
bootstrap.Modal.getInstance(document.getElementById('manageRedisHistoryModal')).hide();
showAlert(`历史记录 "${history.name}" 加载成功`, 'success');
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`加载失败: ${error.message}`, 'danger');
}
}
// 删除Redis查询历史
async function deleteRedisHistory(historyId, historyName) {
if (!confirm(`确定要删除历史记录 "${historyName}" 吗?此操作不可恢复。`)) {
return;
}
try {
const response = await fetch(`/api/redis/query-history/${historyId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
loadRedisHistoryForManagement(); // 重新加载列表
loadRedisQueryHistory(); // 重新加载下拉框
} else {
showAlert(result.error, 'danger');
}
} catch (error) {
showAlert(`删除失败: ${error.message}`, 'danger');
}
}
/**
* Redis查询日志功能
*/
// 显示Redis查询日志对话框
function showRedisQueryLogsDialog() {
loadRedisQueryLogs();
new bootstrap.Modal(document.getElementById('redisQueryLogsModal')).show();
}
// 加载Redis查询日志
async function loadRedisQueryLogs() {
try {
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) {
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 = '暂无Redis查询日志
';
}
} catch (error) {
document.getElementById('redisQueryLogs').innerHTML = '加载日志失败: ' + error.message + '
';
}
}
// 刷新Redis查询日志
function refreshRedisQueryLogs() {
loadRedisQueryLogs();
showAlert('查询日志已刷新', 'info');
}
// 清空Redis查询日志
async function clearRedisQueryLogs() {
if (!confirm('确定要清空所有查询日志吗?此操作不可恢复。')) {
return;
}
try {
const response = await fetch('/api/redis/query-logs', {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showAlert(result.message, 'success');
loadRedisQueryLogs(); // 重新加载日志
} else {
showAlert(result.error, 'danger');
}
} 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 = '';
} 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');
}