// 全局变量 let currentResults = null; let currentIdenticalPage = 1; let identicalPageSize = 10; let filteredIdenticalResults = []; let currentDifferencePage = 1; let differencePageSize = 10; let filteredDifferenceResults = []; let isShardingMode = false; // 分表模式标志 // 页面加载完成后初始化 document.addEventListener('DOMContentLoaded', function() { loadDefaultConfig(); loadConfigGroups(); // 加载配置组列表 bindShardingEvents(); // 绑定分表相关事件 }); // 绑定分表相关事件 function bindShardingEvents() { // 分表模式切换事件 const enableShardingCheckbox = document.getElementById('enableSharding'); if (enableShardingCheckbox) { enableShardingCheckbox.addEventListener('change', toggleShardingMode); } // 生产环境分表开关变化事件 const useShardingProCheckbox = document.getElementById('use_sharding_for_pro'); if (useShardingProCheckbox) { useShardingProCheckbox.addEventListener('change', function() { updateTableNameHints(); }); } // 测试环境分表开关变化事件 const useShardingTestCheckbox = document.getElementById('use_sharding_for_test'); if (useShardingTestCheckbox) { useShardingTestCheckbox.addEventListener('change', function() { updateTableNameHints(); }); } } // 切换分表模式 function toggleShardingMode() { isShardingMode = document.getElementById('enableSharding').checked; const shardingConfig = document.getElementById('shardingConfig'); const executeButton = document.getElementById('executeButtonText'); const keyInputHint = document.getElementById('key_input_hint'); const keysField = document.getElementById('keys'); if (isShardingMode) { // 启用分表模式 shardingConfig.style.display = 'block'; executeButton.textContent = '执行分表查询比对'; keyInputHint.textContent = '分表模式:Key值应包含时间戳用于计算分表索引,支持单主键和复合主键(逗号分隔)'; keysField.placeholder = '单主键:wmid 或 复合主键:docid,id (推荐使用包含时间戳的字段)'; keysField.value = ''; // 更新查询Key输入框的占位符 const queryValues = document.getElementById('query_values'); queryValues.placeholder = '请输入查询的Key值,一行一个\n单主键示例(包含时间戳):\nwmid_1609459200\nwmid_1610064000\nwmid_1610668800\n\n复合主键示例(逗号分隔):\ndocid_1609459200,id1\ndocid_1610064000,id2\ndocid_1610668800,id3'; } else { // 禁用分表模式 shardingConfig.style.display = 'none'; executeButton.textContent = '执行查询比对'; keyInputHint.textContent = '单表模式:支持单主键和复合主键查询,复合主键用逗号分隔'; keysField.placeholder = '单主键:docid 或 复合主键:docid,id'; keysField.value = ''; // 更新查询Key输入框的占位符 const queryValues = document.getElementById('query_values'); queryValues.placeholder = '请输入查询的Key值,一行一个\n单主键示例:\nkey1\nkey2\nkey3\n\n复合主键示例(逗号分隔):\ndocid1,id1\ndocid2,id2\ndocid3,id3'; } updateTableNameHints(); } // 更新表名字段的提示文本 function updateTableNameHints() { const proTableHint = document.getElementById('pro_table_hint'); const testTableHint = document.getElementById('test_table_hint'); const useShardingPro = document.getElementById('use_sharding_for_pro'); const useShardingTest = document.getElementById('use_sharding_for_test'); if (isShardingMode) { proTableHint.textContent = (useShardingPro && useShardingPro.checked) ? '基础表名(自动添加索引后缀)' : '完整表名'; testTableHint.textContent = (useShardingTest && useShardingTest.checked) ? '基础表名(自动添加索引后缀)' : '完整表名'; } else { proTableHint.textContent = '完整表名'; testTableHint.textContent = '完整表名'; } } // 加载配置组列表 async function loadConfigGroups() { try { const response = await fetch('/api/config-groups'); const result = await response.json(); if (result.success) { const select = document.getElementById('configGroupSelect'); select.innerHTML = ''; result.data.forEach(group => { const option = document.createElement('option'); option.value = group.id; option.textContent = `${group.name} (${group.description || '无描述'})`; select.appendChild(option); }); if (result.data.length === 0) { console.log('暂无配置组,数据库已就绪'); } } else { console.error('加载配置组失败:', result.error); // 如果是数据库问题,显示友好提示 if (result.error && result.error.includes('table')) { showAlert('info', '正在初始化数据库,请稍后再试'); } } } catch (error) { console.error('加载配置组失败:', error); showAlert('warning', '配置组功能暂时不可用,但不影响基本查询功能'); } } // 加载选中的配置组 async function loadSelectedConfigGroup() { const groupId = document.getElementById('configGroupSelect').value; if (!groupId) { showAlert('warning', '请先选择一个配置组'); return; } try { const response = await fetch(`/api/config-groups/${groupId}`); const result = await response.json(); if (result.success) { const config = result.data; // 调试输出 console.log('加载配置组数据:', config); console.log('查询配置数据:', config.query_config); // 填充生产环境配置 document.getElementById('pro_cluster_name').value = config.pro_config.cluster_name || ''; document.getElementById('pro_datacenter').value = config.pro_config.datacenter || ''; document.getElementById('pro_hosts').value = (config.pro_config.hosts || []).join(','); document.getElementById('pro_port').value = config.pro_config.port || 9042; document.getElementById('pro_username').value = config.pro_config.username || ''; document.getElementById('pro_password').value = config.pro_config.password || ''; document.getElementById('pro_keyspace').value = config.pro_config.keyspace || ''; document.getElementById('pro_table').value = config.pro_config.table || ''; // 填充测试环境配置 document.getElementById('test_cluster_name').value = config.test_config.cluster_name || ''; document.getElementById('test_datacenter').value = config.test_config.datacenter || ''; document.getElementById('test_hosts').value = (config.test_config.hosts || []).join(','); document.getElementById('test_port').value = config.test_config.port || 9042; document.getElementById('test_username').value = config.test_config.username || ''; document.getElementById('test_password').value = config.test_config.password || ''; document.getElementById('test_keyspace').value = config.test_config.keyspace || ''; document.getElementById('test_table').value = config.test_config.table || ''; // 填充查询配置 - 增加调试输出和保护性检查 if (config.query_config) { console.log('设置keys字段:', config.query_config.keys); const keysElement = document.getElementById('keys'); console.log('keys元素:', keysElement); console.log('keys元素当前值:', keysElement ? keysElement.value : 'element not found'); if (keysElement) { keysElement.value = (config.query_config.keys || []).join(','); console.log('keys字段设置后的值:', keysElement.value); // 强制触发多种事件以确保UI更新 keysElement.dispatchEvent(new Event('input', { bubbles: true })); keysElement.dispatchEvent(new Event('change', { bubbles: true })); // 添加延迟更新,确保模态框关闭后再次设置 setTimeout(() => { keysElement.value = (config.query_config.keys || []).join(','); keysElement.dispatchEvent(new Event('input', { bubbles: true })); console.log('延迟设置后的值:', keysElement.value); }, 100); } else { console.error('未找到keys输入元素!'); } const fieldsToCompareElement = document.getElementById('fields_to_compare'); const excludeFieldsElement = document.getElementById('exclude_fields'); if (fieldsToCompareElement) { fieldsToCompareElement.value = (config.query_config.fields_to_compare || []).join(','); fieldsToCompareElement.dispatchEvent(new Event('input', { bubbles: true })); fieldsToCompareElement.dispatchEvent(new Event('change', { bubbles: true })); setTimeout(() => { fieldsToCompareElement.value = (config.query_config.fields_to_compare || []).join(','); fieldsToCompareElement.dispatchEvent(new Event('input', { bubbles: true })); }, 100); } if (excludeFieldsElement) { excludeFieldsElement.value = (config.query_config.exclude_fields || []).join(','); excludeFieldsElement.dispatchEvent(new Event('input', { bubbles: true })); excludeFieldsElement.dispatchEvent(new Event('change', { bubbles: true })); setTimeout(() => { excludeFieldsElement.value = (config.query_config.exclude_fields || []).join(','); excludeFieldsElement.dispatchEvent(new Event('input', { bubbles: true })); }, 100); } } else { console.warn('查询配置为空或未定义'); document.getElementById('keys').value = ''; document.getElementById('fields_to_compare').value = ''; document.getElementById('exclude_fields').value = ''; } // 加载分表配置 if (config.sharding_config) { // 设置分表启用状态 document.getElementById('enableSharding').checked = config.sharding_config.enabled || false; toggleShardingMode(); // 填充分表配置 document.getElementById('use_sharding_for_pro').checked = config.sharding_config.use_sharding_for_pro || false; document.getElementById('use_sharding_for_test').checked = config.sharding_config.use_sharding_for_test || false; document.getElementById('pro_interval_seconds').value = config.sharding_config.interval_seconds || 604800; document.getElementById('pro_table_count').value = config.sharding_config.table_count || 14; document.getElementById('test_interval_seconds').value = config.sharding_config.interval_seconds || 604800; document.getElementById('test_table_count').value = config.sharding_config.table_count || 14; } else { // 禁用分表模式 document.getElementById('enableSharding').checked = false; toggleShardingMode(); } showAlert('success', `配置组 "${config.name}" 加载成功`); } else { showAlert('danger', result.error || '加载配置组失败'); } } catch (error) { showAlert('danger', '加载配置组失败: ' + error.message); } } // 显示保存配置组对话框 function showSaveConfigDialog() { const modalContent = ` `; // 移除现有modal const existingModal = document.getElementById('saveConfigModal'); if (existingModal) { existingModal.remove(); } // 添加新modal到body document.body.insertAdjacentHTML('beforeend', modalContent); // 显示modal const modal = new bootstrap.Modal(document.getElementById('saveConfigModal')); modal.show(); } // 保存配置组 async function saveConfigGroup() { const name = document.getElementById('configGroupName').value.trim(); const description = document.getElementById('configGroupDesc').value.trim(); if (!name) { showAlert('warning', '请输入配置组名称'); return; } const config = getCurrentConfig(); // 获取分表配置 const shardingConfig = getShardingConfig().sharding_config; try { const response = await fetch('/api/config-groups', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name, description: description, pro_config: config.pro_config, test_config: config.test_config, query_config: config.query_config, sharding_config: shardingConfig }) }); const result = await response.json(); if (result.success) { // 关闭modal const modal = bootstrap.Modal.getInstance(document.getElementById('saveConfigModal')); modal.hide(); // 重新加载配置组列表 await loadConfigGroups(); showAlert('success', result.message + '(包含分表配置)'); } else { showAlert('danger', result.error || '保存配置组失败'); } } catch (error) { showAlert('danger', '保存配置组失败: ' + error.message); } } // 显示管理配置组对话框 async function showManageConfigDialog() { try { const response = await fetch('/api/config-groups'); const result = await response.json(); if (result.success) { let configGroupsList = ''; if (result.data.length === 0) { configGroupsList = '

暂无配置组

'; } else { result.data.forEach(group => { const createdDate = new Date(group.created_at).toLocaleString(); const updatedDate = new Date(group.updated_at).toLocaleString(); configGroupsList += `
${group.name}

${group.description || '无描述'}

创建: ${createdDate} | 更新: ${updatedDate}
`; }); } const modalContent = ` `; // 移除现有modal const existingModal = document.getElementById('manageConfigModal'); if (existingModal) { existingModal.remove(); } // 添加新modal到body document.body.insertAdjacentHTML('beforeend', modalContent); // 显示modal const modal = new bootstrap.Modal(document.getElementById('manageConfigModal')); modal.show(); } else { showAlert('danger', '加载配置组列表失败'); } } catch (error) { showAlert('danger', '加载配置组列表失败: ' + error.message); } } // 通过ID加载配置组 async function loadConfigGroupById(groupId) { try { const response = await fetch(`/api/config-groups/${groupId}`); const result = await response.json(); if (result.success) { const config = result.data; // 填充表单数据(与loadSelectedConfigGroup相同逻辑) document.getElementById('pro_cluster_name').value = config.pro_config.cluster_name || ''; document.getElementById('pro_datacenter').value = config.pro_config.datacenter || ''; document.getElementById('pro_hosts').value = (config.pro_config.hosts || []).join(','); document.getElementById('pro_port').value = config.pro_config.port || 9042; document.getElementById('pro_username').value = config.pro_config.username || ''; document.getElementById('pro_password').value = config.pro_config.password || ''; document.getElementById('pro_keyspace').value = config.pro_config.keyspace || ''; document.getElementById('pro_table').value = config.pro_config.table || ''; document.getElementById('test_cluster_name').value = config.test_config.cluster_name || ''; document.getElementById('test_datacenter').value = config.test_config.datacenter || ''; document.getElementById('test_hosts').value = (config.test_config.hosts || []).join(','); document.getElementById('test_port').value = config.test_config.port || 9042; document.getElementById('test_username').value = config.test_config.username || ''; document.getElementById('test_password').value = config.test_config.password || ''; document.getElementById('test_keyspace').value = config.test_config.keyspace || ''; document.getElementById('test_table').value = config.test_config.table || ''; // 填充查询配置 - 增加调试输出和保护性检查 if (config.query_config) { console.log('loadConfigGroupById - 设置keys字段:', config.query_config.keys); const keysElement = document.getElementById('keys'); console.log('loadConfigGroupById - keys元素:', keysElement); if (keysElement) { keysElement.value = (config.query_config.keys || []).join(','); console.log('loadConfigGroupById - keys字段设置后的值:', keysElement.value); // 强制触发多种事件以确保UI更新 keysElement.dispatchEvent(new Event('input', { bubbles: true })); keysElement.dispatchEvent(new Event('change', { bubbles: true })); // 添加延迟更新,确保模态框关闭后再次设置 setTimeout(() => { keysElement.value = (config.query_config.keys || []).join(','); keysElement.dispatchEvent(new Event('input', { bubbles: true })); console.log('loadConfigGroupById - 延迟设置后的值:', keysElement.value); }, 100); } else { console.error('loadConfigGroupById - 未找到keys输入元素!'); } const fieldsToCompareElement = document.getElementById('fields_to_compare'); const excludeFieldsElement = document.getElementById('exclude_fields'); if (fieldsToCompareElement) { fieldsToCompareElement.value = (config.query_config.fields_to_compare || []).join(','); fieldsToCompareElement.dispatchEvent(new Event('input', { bubbles: true })); fieldsToCompareElement.dispatchEvent(new Event('change', { bubbles: true })); setTimeout(() => { fieldsToCompareElement.value = (config.query_config.fields_to_compare || []).join(','); fieldsToCompareElement.dispatchEvent(new Event('input', { bubbles: true })); }, 100); } if (excludeFieldsElement) { excludeFieldsElement.value = (config.query_config.exclude_fields || []).join(','); excludeFieldsElement.dispatchEvent(new Event('input', { bubbles: true })); excludeFieldsElement.dispatchEvent(new Event('change', { bubbles: true })); setTimeout(() => { excludeFieldsElement.value = (config.query_config.exclude_fields || []).join(','); excludeFieldsElement.dispatchEvent(new Event('input', { bubbles: true })); }, 100); } } else { console.warn('loadConfigGroupById - 查询配置为空或未定义'); document.getElementById('keys').value = ''; document.getElementById('fields_to_compare').value = ''; document.getElementById('exclude_fields').value = ''; } // 更新下拉框选中状态 document.getElementById('configGroupSelect').value = groupId; // 加载分表配置 if (config.sharding_config) { // 设置分表启用状态 document.getElementById('enableSharding').checked = config.sharding_config.enabled || false; toggleShardingMode(); // 填充分表配置 document.getElementById('use_sharding_for_pro').checked = config.sharding_config.use_sharding_for_pro || false; document.getElementById('use_sharding_for_test').checked = config.sharding_config.use_sharding_for_test || false; document.getElementById('pro_interval_seconds').value = config.sharding_config.interval_seconds || 604800; document.getElementById('pro_table_count').value = config.sharding_config.table_count || 14; document.getElementById('test_interval_seconds').value = config.sharding_config.interval_seconds || 604800; document.getElementById('test_table_count').value = config.sharding_config.table_count || 14; } else { // 禁用分表模式 document.getElementById('enableSharding').checked = false; toggleShardingMode(); } // 关闭管理modal const modal = bootstrap.Modal.getInstance(document.getElementById('manageConfigModal')); modal.hide(); showAlert('success', `配置组 "${config.name}" 加载成功`); } else { showAlert('danger', result.error || '加载配置组失败'); } } catch (error) { showAlert('danger', '加载配置组失败: ' + error.message); } } // 删除配置组 async function deleteConfigGroup(groupId, groupName) { if (!confirm(`确定要删除配置组 "${groupName}" 吗?此操作不可撤销。`)) { return; } try { const response = await fetch(`/api/config-groups/${groupId}`, { method: 'DELETE' }); const result = await response.json(); if (result.success) { showAlert('success', result.message); // 重新加载配置组列表 await loadConfigGroups(); // 重新显示管理对话框 setTimeout(() => { showManageConfigDialog(); }, 500); } else { showAlert('danger', result.error || '删除配置组失败'); } } catch (error) { showAlert('danger', '删除配置组失败: ' + error.message); } } // 加载默认配置 async function loadDefaultConfig() { try { const response = await fetch('/api/default-config'); const config = await response.json(); // 填充生产环境配置 document.getElementById('pro_cluster_name').value = config.pro_config.cluster_name || ''; document.getElementById('pro_datacenter').value = config.pro_config.datacenter || ''; document.getElementById('pro_hosts').value = config.pro_config.hosts.join(','); document.getElementById('pro_port').value = config.pro_config.port; document.getElementById('pro_username').value = config.pro_config.username; document.getElementById('pro_password').value = config.pro_config.password; document.getElementById('pro_keyspace').value = config.pro_config.keyspace; document.getElementById('pro_table').value = config.pro_config.table; // 填充测试环境配置 document.getElementById('test_cluster_name').value = config.test_config.cluster_name || ''; document.getElementById('test_datacenter').value = config.test_config.datacenter || ''; document.getElementById('test_hosts').value = config.test_config.hosts.join(','); document.getElementById('test_port').value = config.test_config.port; document.getElementById('test_username').value = config.test_config.username; document.getElementById('test_password').value = config.test_config.password; document.getElementById('test_keyspace').value = config.test_config.keyspace; document.getElementById('test_table').value = config.test_config.table; // 填充查询配置 document.getElementById('keys').value = config.keys.join(','); document.getElementById('fields_to_compare').value = config.fields_to_compare.join(','); document.getElementById('exclude_fields').value = config.exclude_fields.join(','); showAlert('success', '默认配置加载成功'); } catch (error) { showAlert('danger', '加载默认配置失败: ' + error.message); } } // 获取当前配置 function getCurrentConfig() { return { pro_config: { cluster_name: document.getElementById('pro_cluster_name').value, datacenter: document.getElementById('pro_datacenter').value, hosts: document.getElementById('pro_hosts').value.split(',').map(h => h.trim()).filter(h => h), port: parseInt(document.getElementById('pro_port').value) || 9042, username: document.getElementById('pro_username').value, password: document.getElementById('pro_password').value, keyspace: document.getElementById('pro_keyspace').value, table: document.getElementById('pro_table').value }, test_config: { cluster_name: document.getElementById('test_cluster_name').value, datacenter: document.getElementById('test_datacenter').value, hosts: document.getElementById('test_hosts').value.split(',').map(h => h.trim()).filter(h => h), port: parseInt(document.getElementById('test_port').value) || 9042, username: document.getElementById('test_username').value, password: document.getElementById('test_password').value, keyspace: document.getElementById('test_keyspace').value, table: document.getElementById('test_table').value }, query_config: { keys: document.getElementById('keys').value.split(',').map(k => k.trim()).filter(k => k), fields_to_compare: document.getElementById('fields_to_compare').value .split(',').map(f => f.trim()).filter(f => f), exclude_fields: document.getElementById('exclude_fields').value .split(',').map(f => f.trim()).filter(f => f) }, values: document.getElementById('query_values').value .split('\n').map(v => v.trim()).filter(v => v) }; } // 执行查询比对 async function executeQuery() { const config = getCurrentConfig(); // 验证配置 if (!config.values.length) { showAlert('warning', '请输入查询Key值'); return; } if (!config.query_config.keys.length) { showAlert('warning', '请输入主键字段'); return; } // 显示加载动画 document.getElementById('loading').style.display = 'block'; document.getElementById('results').style.display = 'none'; // 更新加载文本 const loadingText = document.getElementById('loadingText'); if (isShardingMode) { loadingText.textContent = '正在执行分表查询比对...'; } else { loadingText.textContent = '正在执行查询比对...'; } try { let apiEndpoint = '/api/query'; let requestConfig = config; // 如果启用了分表模式,使用分表查询API和配置 if (isShardingMode) { apiEndpoint = '/api/sharding-query'; requestConfig = getShardingConfig(); } const response = await fetch(apiEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestConfig) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || '查询失败'); } const results = await response.json(); currentResults = results; displayResults(results); // 自动刷新查询日志 autoRefreshLogsAfterQuery(); } catch (error) { showAlert('danger', '查询失败: ' + error.message); } finally { document.getElementById('loading').style.display = 'none'; } } // 获取分表查询配置 function getShardingConfig() { const baseConfig = getCurrentConfig(); return { ...baseConfig, sharding_config: { enabled: document.getElementById('enableSharding').checked, use_sharding_for_pro: document.getElementById('use_sharding_for_pro').checked, use_sharding_for_test: document.getElementById('use_sharding_for_test').checked, // 生产环境分表参数 pro_interval_seconds: parseInt(document.getElementById('pro_interval_seconds').value) || 604800, pro_table_count: parseInt(document.getElementById('pro_table_count').value) || 14, // 测试环境分表参数 test_interval_seconds: parseInt(document.getElementById('test_interval_seconds').value) || 604800, test_table_count: parseInt(document.getElementById('test_table_count').value) || 14, // 保持向后兼容的通用参数 interval_seconds: parseInt(document.getElementById('pro_interval_seconds').value) || 604800, table_count: parseInt(document.getElementById('pro_table_count').value) || 14 } }; } // 显示查询结果 function displayResults(results) { // 显示统计信息 displayStats(results); // 显示分表查询信息(如果有) displayShardingInfo(results); // 更新选项卡计数 document.getElementById('diff-count').textContent = results.differences.length; document.getElementById('identical-count').textContent = results.identical_results.length; // 计算原始数据总数 const totalRawData = (results.raw_pro_data ? results.raw_pro_data.length : 0) + (results.raw_test_data ? results.raw_test_data.length : 0); document.getElementById('rawdata-count').textContent = totalRawData; // 初始化相同结果分页数据 filteredIdenticalResults = results.identical_results; currentIdenticalPage = 1; // 初始化差异结果分页数据 filteredDifferenceResults = results.differences; currentDifferencePage = 1; // 显示各个面板内容 displayDifferences(); displayIdenticalResults(); displayRawData(results); displayComparisonSummary(results.summary); // 显示结果区域 document.getElementById('results').style.display = 'block'; // 根据查询类型显示不同的成功消息 const queryType = isShardingMode ? '分表查询' : '单表查询'; showAlert('success', `${queryType}完成!共处理${results.total_keys}个Key,发现${results.differences.length}处差异,${results.identical_results.length}条记录完全相同`); } // 显示分表查询信息 function displayShardingInfo(results) { const shardingInfoContainer = document.getElementById('shardingInfoContainer'); if (!isShardingMode || !results.sharding_info) { shardingInfoContainer.style.display = 'none'; return; } const shardingInfo = results.sharding_info; shardingInfoContainer.style.display = 'block'; let html = '
'; // 生产环境分表信息 if (shardingInfo.pro_shards) { html += `
生产环境分表信息
配置:${shardingInfo.pro_shards.interval_seconds}秒间隔,${shardingInfo.pro_shards.table_count}张分表
`; if (shardingInfo.pro_shards.queried_tables) { shardingInfo.pro_shards.queried_tables.forEach(table => { const hasError = shardingInfo.pro_shards.error_tables && shardingInfo.pro_shards.error_tables.includes(table); const cssClass = hasError ? 'shard-error-info' : 'shard-table-info'; html += `${table}`; }); } html += '
'; } // 测试环境分表信息 if (shardingInfo.test_shards) { html += `
测试环境分表信息
配置:${shardingInfo.test_shards.interval_seconds}秒间隔,${shardingInfo.test_shards.table_count}张分表
`; if (shardingInfo.test_shards.queried_tables) { shardingInfo.test_shards.queried_tables.forEach(table => { const hasError = shardingInfo.test_shards.error_tables && shardingInfo.test_shards.error_tables.includes(table); const cssClass = hasError ? 'shard-error-info' : 'shard-table-info'; html += `${table}`; }); } html += '
'; } html += '
'; // 添加分表计算统计信息 if (shardingInfo.calculation_stats) { html += `
分表计算统计
处理Key数:${shardingInfo.calculation_stats.total_keys || 0}
成功解析时间戳:${shardingInfo.calculation_stats.successful_extractions || 0}
计算出分表数:${shardingInfo.calculation_stats.unique_shards || 0}
解析失败:${shardingInfo.calculation_stats.failed_extractions || 0}
`; } document.getElementById('shardingInfo').innerHTML = html; } // 显示统计信息 function displayStats(results) { const statsHtml = `

${results.total_keys}

总Key数量

${results.pro_count}

生产表记录

${results.test_count}

测试表记录

${results.identical_results.length}

相同记录

${results.differences.length}

差异记录

${Math.round((results.identical_results.length / results.total_keys) * 100)}%

一致性比例

`; document.getElementById('stats').innerHTML = statsHtml; } // 显示差异详情 function displayDifferences() { const differencesContainer = document.getElementById('differences'); // 保存当前搜索框的值 const currentSearchValue = document.getElementById('differenceSearch')?.value || ''; if (!filteredDifferenceResults || !filteredDifferenceResults.length) { differencesContainer.innerHTML = '

未发现差异

'; return; } // 调试日志:检查差异数据 console.log('差异数据:', filteredDifferenceResults); // 按主键分组差异 const groupedDifferences = groupDifferencesByKey(filteredDifferenceResults); const totalGroups = Object.keys(groupedDifferences).length; // 计算分页(基于主键组) const groupKeys = Object.keys(groupedDifferences); const totalPages = Math.ceil(groupKeys.length / differencePageSize); const startIndex = (currentDifferencePage - 1) * differencePageSize; const endIndex = startIndex + differencePageSize; const currentPageKeys = groupKeys.slice(startIndex, endIndex); // 统计字段差异类型 const fieldStats = {}; filteredDifferenceResults.forEach(diff => { if (diff.field) { fieldStats[diff.field] = (fieldStats[diff.field] || 0) + 1; } }); let html = `
${Object.entries(fieldStats).map(([field, count]) => ` `).join('')}
共 ${totalGroups} 个主键组,${filteredDifferenceResults.length} 条差异记录
${generateDifferencePagination(currentDifferencePage, totalPages)}
`; // 显示当前页的主键组 currentPageKeys.forEach((keyStr, groupIndex) => { const diffs = groupedDifferences[keyStr]; const globalIndex = startIndex + groupIndex + 1; // 将字符串化的key解析回对象 let keyObj; try { keyObj = JSON.parse(keyStr); } catch (e) { // 如果解析失败,假设它本身就是一个简单值 keyObj = keyStr; } html += `
主键组 #${globalIndex} ${diffs.length} 个差异字段

主键: ${formatCompositeKey(keyObj)}

`; // 显示该主键下的所有差异字段 diffs.forEach((diff, diffIndex) => { if (diff.message) { // 记录不存在的情况 html += `
${diff.message}
`; } else { // 字段值差异的情况 const isJson = diff.is_json; const isArray = diff.is_array; const jsonClass = isJson ? 'json-field' : ''; const fieldId = `field_${globalIndex}_${diffIndex}`; // 调试:打印差异数据 console.log(`差异 ${diff.field}:`, { pro_value: diff.pro_value, test_value: diff.test_value, is_json: diff.is_json, is_array: diff.is_array }); // 确保值存在 const proValue = diff.pro_value !== undefined && diff.pro_value !== null ? diff.pro_value : '无数据'; const testValue = diff.test_value !== undefined && diff.test_value !== null ? diff.test_value : '无数据'; html += `
${diff.field} ${isJson ? 'JSON' : ''} ${isArray ? '数组' : ''}
生产环境
${escapeHtml(proValue)}
测试环境
${escapeHtml(testValue)}
`; } }); html += `
`; }); // 底部分页 if (totalPages > 1) { html += `
${generateDifferencePagination(currentDifferencePage, totalPages)}
`; } differencesContainer.innerHTML = html; // 恢复搜索框的焦点和光标位置 if (currentSearchValue) { const searchInput = document.getElementById('differenceSearch'); if (searchInput) { searchInput.focus(); searchInput.setSelectionRange(searchInput.value.length, searchInput.value.length); } } } // HTML转义函数,防止XSS function escapeHtml(text) { // 处理undefined和null值 if (text === undefined || text === null) { return '无数据'; } // 确保是字符串 const str = String(text); // 如果是空字符串 if (str === '') { return '空值'; } const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } // 显示相同结果 function displayIdenticalResults() { const identicalContainer = document.getElementById('identical-results'); // 保存当前搜索框的值 const currentSearchValue = document.getElementById('identicalSearch')?.value || ''; if (!filteredIdenticalResults.length) { identicalContainer.innerHTML = '

没有完全相同的记录

'; return; } // 计算分页 const totalPages = Math.ceil(filteredIdenticalResults.length / identicalPageSize); const startIndex = (currentIdenticalPage - 1) * identicalPageSize; const endIndex = startIndex + identicalPageSize; const currentPageData = filteredIdenticalResults.slice(startIndex, endIndex); let html = `
共 ${filteredIdenticalResults.length} 条记录
${generatePagination(currentIdenticalPage, totalPages)}
`; // 显示当前页数据 currentPageData.forEach((result, index) => { const globalIndex = startIndex + index + 1; html += `
相同记录 #${globalIndex} 完全匹配

主键: ${formatCompositeKey(result.key)}

`; // 显示字段对比,左右布局 const proFields = result.pro_fields || {}; const testFields = result.test_fields || {}; const allFields = new Set([...Object.keys(proFields), ...Object.keys(testFields)]); html += '
字段对比 (生产环境 vs 测试环境):
'; allFields.forEach(fieldName => { const proValue = proFields[fieldName] || ''; const testValue = testFields[fieldName] || ''; // 更安全的JSON检测方法 let isJson = false; try { if (typeof proValue === 'string' && (proValue.startsWith('{') || proValue.startsWith('['))) { JSON.parse(proValue); isJson = true; } } catch (e1) { try { if (typeof testValue === 'string' && (testValue.startsWith('{') || testValue.startsWith('['))) { JSON.parse(testValue); isJson = true; } } catch (e2) { // 如果都不是有效的JSON,保持isJson为false } } html += `
${fieldName} ${isJson ? 'JSON' : ''}
${escapeHtml(String(proValue))}
${escapeHtml(String(testValue))}
`; }); html += `
`; }); // 底部分页 if (totalPages > 1) { html += `
${generatePagination(currentIdenticalPage, totalPages)}
`; } identicalContainer.innerHTML = html; // 恢复搜索框的焦点和光标位置 if (currentSearchValue) { const searchInput = document.getElementById('identicalSearch'); if (searchInput) { searchInput.focus(); searchInput.setSelectionRange(searchInput.value.length, searchInput.value.length); } } } // 生成分页导航 function generatePagination(currentPage, totalPages) { if (totalPages <= 1) return ''; let html = ''; return html; } // 跳转到指定页面 function goToIdenticalPage(page) { const totalPages = Math.ceil(filteredIdenticalResults.length / identicalPageSize); if (page < 1 || page > totalPages) return; currentIdenticalPage = page; displayIdenticalResults(); } // 改变每页显示数量 function changePageSize(newSize) { if (newSize === 'custom') { // 显示自定义输入框 const customInput = document.getElementById('customPageSize'); const select = document.querySelector('select[onchange="changePageSize(this.value)"]'); customInput.style.display = 'inline-block'; customInput.focus(); return; } identicalPageSize = parseInt(newSize); currentIdenticalPage = 1; displayIdenticalResults(); } // 设置自定义页面大小 function setCustomPageSize(size) { const pageSize = parseInt(size); if (pageSize && pageSize > 0 && pageSize <= 1000) { identicalPageSize = pageSize; currentIdenticalPage = 1; // 隐藏输入框,更新下拉框显示 const customInput = document.getElementById('customPageSize'); const select = document.querySelector('select[onchange="changePageSize(this.value)"]'); customInput.style.display = 'none'; // 如果不是预设值,保持custom选中状态 const presetValues = [5, 10, 20, 50, 100]; if (!presetValues.includes(pageSize)) { select.value = 'custom'; } else { select.value = pageSize.toString(); } displayIdenticalResults(); } else { showAlert('warning', '请输入1-1000之间的有效数字'); } } // 处理自定义页面大小输入框回车事件 function handleCustomPageSizeEnter(event) { if (event.key === 'Enter') { setCustomPageSize(event.target.value); } } // 搜索相同结果 let currentIdenticalSearchTerm = ''; function searchIdenticalResults(searchTerm) { if (!currentResults) return; // 保存搜索词 currentIdenticalSearchTerm = searchTerm; if (!searchTerm.trim()) { filteredIdenticalResults = currentResults.identical_results; } else { const term = searchTerm.toLowerCase(); filteredIdenticalResults = currentResults.identical_results.filter(result => { // 搜索主键 const keyStr = JSON.stringify(result.key).toLowerCase(); if (keyStr.includes(term)) return true; // 搜索字段内容 const proFields = result.pro_fields || {}; const testFields = result.test_fields || {}; const allValues = [...Object.values(proFields), ...Object.values(testFields)]; return allValues.some(value => String(value).toLowerCase().includes(term) ); }); } currentIdenticalPage = 1; displayIdenticalResults(); } // 清除相同记录搜索 function clearIdenticalSearch() { currentIdenticalSearchTerm = ''; document.getElementById('identicalSearch').value = ''; filteredIdenticalResults = currentResults.identical_results; currentIdenticalPage = 1; displayIdenticalResults(); } // 生成差异分页导航 function generateDifferencePagination(currentPage, totalPages) { if (totalPages <= 1) return ''; let html = ''; return html; } // 跳转到指定差异页面 function goToDifferencePage(page) { const totalPages = Math.ceil(filteredDifferenceResults.length / differencePageSize); if (page < 1 || page > totalPages) return; currentDifferencePage = page; displayDifferences(); } // 改变差异每页显示数量 function changeDifferencePageSize(newSize) { if (newSize === 'custom_diff') { // 显示自定义输入框 const customInput = document.getElementById('customDiffPageSize'); const select = document.querySelector('select[onchange="changeDifferencePageSize(this.value)"]'); customInput.style.display = 'inline-block'; customInput.focus(); return; } differencePageSize = parseInt(newSize); currentDifferencePage = 1; displayDifferences(); } // 设置自定义差异页面大小 function setCustomDifferencePageSize(size) { const pageSize = parseInt(size); if (pageSize && pageSize > 0 && pageSize <= 1000) { differencePageSize = pageSize; currentDifferencePage = 1; // 隐藏输入框,更新下拉框显示 const customInput = document.getElementById('customDiffPageSize'); const select = document.querySelector('select[onchange="changeDifferencePageSize(this.value)"]'); customInput.style.display = 'none'; // 如果不是预设值,保持custom选中状态 const presetValues = [5, 10, 20, 50, 100]; if (!presetValues.includes(pageSize)) { select.value = 'custom_diff'; } else { select.value = pageSize.toString(); } displayDifferences(); } else { showAlert('warning', '请输入1-1000之间的有效数字'); } } // 处理自定义差异页面大小输入框回车事件 function handleCustomDiffPageSizeEnter(event) { if (event.key === 'Enter') { setCustomDifferencePageSize(event.target.value); } } // 搜索差异结果 let currentSearchTerm = ''; function searchDifferenceResults(searchTerm) { if (!currentResults) return; // 保存搜索词 currentSearchTerm = searchTerm; if (!searchTerm.trim()) { // 如果有字段筛选,应用字段筛选;否则显示全部 if (currentFieldFilter) { filteredDifferenceResults = currentResults.differences.filter(diff => diff.field === currentFieldFilter); } else { filteredDifferenceResults = currentResults.differences; } } else { const term = searchTerm.toLowerCase(); let baseResults = currentFieldFilter ? currentResults.differences.filter(diff => diff.field === currentFieldFilter) : currentResults.differences; filteredDifferenceResults = baseResults.filter(diff => { // 搜索主键 const keyStr = JSON.stringify(diff.key).toLowerCase(); if (keyStr.includes(term)) return true; // 搜索字段名 if (diff.field && diff.field.toLowerCase().includes(term)) return true; // 搜索字段值 if (diff.pro_value && String(diff.pro_value).toLowerCase().includes(term)) return true; if (diff.test_value && String(diff.test_value).toLowerCase().includes(term)) return true; // 搜索消息 if (diff.message && diff.message.toLowerCase().includes(term)) return true; return false; }); } currentDifferencePage = 1; displayDifferences(); } // 清除差异搜索 function clearDifferenceSearch() { currentSearchTerm = ''; document.getElementById('differenceSearch').value = ''; // 重新应用字段筛选 if (currentFieldFilter) { filteredDifferenceResults = currentResults.differences.filter(diff => diff.field === currentFieldFilter); } else { filteredDifferenceResults = currentResults.differences; } currentDifferencePage = 1; displayDifferences(); } // 复制到剪贴板 function copyToClipboard(text, button) { navigator.clipboard.writeText(text).then(function() { // 显示复制成功反馈 const originalIcon = button.innerHTML; button.innerHTML = ''; button.classList.add('btn-success'); button.classList.remove('btn-outline-secondary'); setTimeout(() => { button.innerHTML = originalIcon; button.classList.remove('btn-success'); button.classList.add('btn-outline-secondary'); }, 1000); }).catch(function(err) { console.error('复制失败:', err); showAlert('danger', '复制失败,请手动复制'); }); } // JS字符串转义 function escapeForJs(str) { if (str === null || str === undefined) { return ''; } return String(str) .replace(/\\/g, '\\\\') // 反斜杠必须先处理 .replace(/'/g, "\\'") // 单引号 .replace(/"/g, '\\"') // 双引号 .replace(/\n/g, '\\n') // 换行符 .replace(/\r/g, '\\r') // 回车符 .replace(/\t/g, '\\t') // 制表符 .replace(/\f/g, '\\f') // 换页符 .replace(/\b/g, '\\b') // 退格符 .replace(/\0/g, '\\0'); // 空字符 } // 显示原生数据 function showRawData(keyStr) { if (!currentResults) { showAlert('warning', '请先执行查询操作'); return; } try { // 解析key const key = JSON.parse(keyStr); console.log('查找原始数据,key:', key); // 在原生数据中查找对应的记录 let proData = null; let testData = null; // 查找生产环境数据 if (currentResults.raw_pro_data) { proData = currentResults.raw_pro_data.find(item => { // 支持复合主键比较 if (typeof key === 'object' && !Array.isArray(key)) { // 复合主键情况:比较所有主键字段 const matches = Object.keys(key).every(keyField => { const itemValue = item[keyField]; const keyValue = key[keyField]; // 如果都是undefined或null,认为匹配 if (itemValue == null && keyValue == null) { return true; } // 转换为字符串进行比较 const itemStr = String(itemValue); const keyStr = String(keyValue); // 直接比较字符串 return itemStr === keyStr; }); if (matches) { console.log('找到生产环境数据:', item); } return matches; } else { // 单主键情况(兼容旧代码) return false; } }); // 如果没找到,输出调试信息 if (!proData) { console.log('未找到生产数据,查找的key:', key); console.log('可用的数据:', currentResults.raw_pro_data.map(item => ({ statusid: item.statusid }))); } } // 查找测试环境数据 if (currentResults.raw_test_data) { testData = currentResults.raw_test_data.find(item => { // 支持复合主键比较 if (typeof key === 'object' && !Array.isArray(key)) { // 复合主键情况:比较所有主键字段 const matches = Object.keys(key).every(keyField => { const itemValue = item[keyField]; const keyValue = key[keyField]; // 如果都是undefined或null,认为匹配 if (itemValue == null && keyValue == null) { return true; } // 转换为字符串进行比较 const itemStr = String(itemValue); const keyStr = String(keyValue); // 直接比较字符串 return itemStr === keyStr; }); if (matches) { console.log('找到测试环境数据:', item); } return matches; } else { // 单主键情况(兼容旧代码) return false; } }); // 如果没找到,输出调试信息 if (!testData) { console.log('未找到测试数据,查找的key:', key); console.log('可用的数据:', currentResults.raw_test_data.map(item => ({ statusid: item.statusid }))); } } console.log('查找结果 - 生产数据:', proData, '测试数据:', testData); // 创建模态框内容 const modalContent = ` `; // 移除现有modal const existingModal = document.getElementById('rawDataModal'); if (existingModal) { existingModal.remove(); } document.body.insertAdjacentHTML('beforeend', modalContent); // 显示模态框 const modal = new bootstrap.Modal(document.getElementById('rawDataModal')); modal.show(); // 保存当前数据供复制使用 window.currentRawData = { pro: proData ? JSON.stringify(proData, null, 2) : '无数据', test: testData ? JSON.stringify(testData, null, 2) : '无数据', combined: `生产环境数据:\n${proData ? JSON.stringify(proData, null, 2) : '无数据'}\n\n测试环境数据:\n${testData ? JSON.stringify(testData, null, 2) : '无数据'}` }; // 延迟渲染对比视图和树形视图 setTimeout(() => { console.log('开始渲染视图,生产数据:', proData, '测试数据:', testData); // 渲染对比视图 try { renderDiffView(proData, testData); console.log('对比视图渲染完成'); } catch (e) { console.error('对比视图渲染失败:', e); } // 渲染树形视图 try { renderTreeView('proTreeView', proData); renderTreeView('testTreeView', testData); console.log('树形视图渲染完成'); } catch (e) { console.error('树形视图渲染失败:', e); } // 为格式化视图添加同步滚动 setupSyncScroll('formatted'); // 为树形视图添加同步滚动 setupSyncScroll('tree'); // 添加标签页切换事件监听器,确保切换时重新渲染 const tabButtons = document.querySelectorAll('#rawDataTabs button[data-bs-toggle="tab"]'); tabButtons.forEach(button => { button.addEventListener('shown.bs.tab', (event) => { const targetId = event.target.getAttribute('data-bs-target'); console.log('切换到标签页:', targetId); if (targetId === '#diff') { // 重新渲染对比视图 renderDiffView(proData, testData); } else if (targetId === '#tree') { // 重新渲染树形视图 renderTreeView('proTreeView', proData); renderTreeView('testTreeView', testData); } }); }); }, 100); } catch (error) { console.error('显示原生数据失败:', error); showAlert('danger', '显示原生数据失败: ' + error.message); } } // 复制原生数据 function copyRawData() { if (window.currentRawData) { const combinedData = window.currentRawData.combined || (window.currentRawData.pro + '\n\n' + window.currentRawData.test); navigator.clipboard.writeText(combinedData).then(() => { showAlert('success', '原生数据已复制到剪贴板'); }).catch(err => { console.error('复制失败:', err); showAlert('danger', '复制失败,请手动选择复制'); }); } else { showAlert('warning', '无可复制的数据'); } } // 显示差异数据的原生数据 function showDifferenceRawData(keyStr) { showRawData(keyStr); // 复用相同的原生数据显示逻辑 } // 渲染对比视图 function renderDiffView(proData, testData) { const diffViewContainer = document.getElementById('diffView'); // 确保容器存在 if (!diffViewContainer) { console.error('对比视图容器不存在'); return; } if (!proData && !testData) { diffViewContainer.innerHTML = '

无数据可对比

'; return; } // 获取所有字段 const allFields = new Set([ ...(proData ? Object.keys(proData) : []), ...(testData ? Object.keys(testData) : []) ]); // 分离相同和不同的字段 const differentFields = []; const identicalFields = []; allFields.forEach(field => { const proValue = proData ? proData[field] : undefined; const testValue = testData ? testData[field] : undefined; // 特殊处理:区分 null 和 undefined // undefined 表示字段不存在,null 表示字段存在但值为 null let isEqual = false; // 如果一个是 undefined,另一个不是(包括 null),则不相等 if (proValue === undefined && testValue !== undefined) { isEqual = false; } else if (proValue !== undefined && testValue === undefined) { isEqual = false; } else if (proValue === testValue) { // 两个都是 undefined 或者值完全相同 isEqual = true; } else if (proValue !== undefined && testValue !== undefined) { // 两个都有值,尝试规范化比较 try { const proNormalized = normalizeValue(proValue); const testNormalized = normalizeValue(testValue); isEqual = JSON.stringify(proNormalized) === JSON.stringify(testNormalized); } catch (e) { isEqual = JSON.stringify(proValue) === JSON.stringify(testValue); } } if (isEqual) { identicalFields.push(field); } else { differentFields.push(field); } }); let html = ''; // 如果有差异字段,先显示差异统计 if (differentFields.length > 0) { html += `
发现 ${differentFields.length} 个字段存在差异
`; } html += '
'; html += ` `; // 先显示差异字段(重点突出) if (differentFields.length > 0) { html += ''; differentFields.forEach(field => { const proValue = proData ? proData[field] : undefined; const testValue = testData ? testData[field] : undefined; // 判断值的类型和差异类型 let diffType = ''; let proDisplay = ''; let testDisplay = ''; if (proValue === undefined && testValue !== undefined) { diffType = 'missing-in-pro'; proDisplay = ' 缺失'; testDisplay = formatValue(testValue); } else if (proValue !== undefined && testValue === undefined) { diffType = 'missing-in-test'; proDisplay = formatValue(proValue); testDisplay = ' 缺失'; } else { diffType = 'value-diff'; proDisplay = formatValue(proValue); testDisplay = formatValue(testValue); } html += ` `; }); } // 再显示相同字段 if (identicalFields.length > 0) { html += ''; identicalFields.forEach(field => { const value = proData ? proData[field] : testData[field]; const displayValue = formatValue(value); html += ` `; }); } html += '
字段名 生产环境 测试环境 状态
差异字段
${field} ${getFieldTypeIcon(proValue || testValue)} ${proDisplay} ${testDisplay} 不同
相同字段
${field} ${getFieldTypeIcon(value)} ${displayValue} ${displayValue} 相同
'; // 添加快速跳转按钮 if (differentFields.length > 0 && identicalFields.length > 0) { html = `
` + html; } diffViewContainer.innerHTML = html; } // 格式化值显示 function formatValue(value) { if (value === null) return 'null'; if (value === undefined) return 'undefined'; if (value === '') return '(空字符串)'; const type = typeof value; if (type === 'object') { // 特殊处理数组 if (Array.isArray(value)) { // 检查是否是JSON字符串数组 let isJsonStringArray = value.length > 0 && value.every(item => { if (typeof item === 'string') { try { if ((item.startsWith('{') && item.endsWith('}')) || (item.startsWith('[') && item.endsWith(']'))) { JSON.parse(item); return true; } } catch (e) {} } return false; }); if (isJsonStringArray) { // 解析并格式化JSON字符串数组 try { const parsedArray = value.map(item => JSON.parse(item)); // 如果都有key字段,按key排序 if (parsedArray.every(item => 'key' in item)) { parsedArray.sort((a, b) => a.key - b.key); } const formatted = JSON.stringify(parsedArray, null, 2); return `
${escapeHtml(formatted)}
`; } catch (e) { // 如果解析失败,按原样显示 } } } // 对象或数组,格式化显示 try { const formatted = JSON.stringify(value, null, 2); return `
${escapeHtml(formatted)}
`; } catch (e) { return `
${escapeHtml(String(value))}
`; } } if (type === 'string') { // 检查是否是JSON字符串 try { if ((value.startsWith('{') && value.endsWith('}')) || (value.startsWith('[') && value.endsWith(']'))) { const parsed = JSON.parse(value); const formatted = JSON.stringify(parsed, null, 2); return `
${escapeHtml(formatted)}
`; } } catch (e) { // 不是有效的JSON,作为普通字符串处理 } // 长文本处理 if (value.length > 200) { return `
${escapeHtml(value)}
`; } } return escapeHtml(String(value)); } // 获取字段类型图标 function getFieldTypeIcon(value) { if (value === undefined) { return ''; } if (value === null) { return 'NULL'; } const type = typeof value; if (Array.isArray(value)) { return '[]'; } if (type === 'object') { return '{}'; } if (type === 'string') { // 检查是否是JSON字符串 try { if (value.startsWith('{') || value.startsWith('[')) { JSON.parse(value); return 'JSON'; } } catch (e) { // 不是JSON } // 检查是否是日期字符串 if (/^\d{4}-\d{2}-\d{2}/.test(value)) { return '📅'; } } if (type === 'number') { return '#'; } if (type === 'boolean') { return ''; } return ''; } // 滚动到表格指定部分 function scrollToTableSection(sectionType) { const targetRow = document.querySelector(`#diffView .table-${sectionType}`); if (targetRow) { targetRow.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } // 规范化值用于比较(处理JSON键顺序问题) function normalizeValue(value) { if (typeof value === 'string') { // 尝试解析JSON字符串 try { if ((value.startsWith('{') && value.endsWith('}')) || (value.startsWith('[') && value.endsWith(']'))) { value = JSON.parse(value); } else { return value; } } catch (e) { return value; } } if (value === null || typeof value !== 'object') { return value; } if (Array.isArray(value)) { // 对于数组,需要特殊处理 // 1. 先尝试解析每个元素(可能是JSON字符串) const parsedArray = value.map(item => { if (typeof item === 'string') { try { if ((item.startsWith('{') && item.endsWith('}')) || (item.startsWith('[') && item.endsWith(']'))) { return JSON.parse(item); } } catch (e) { // 保持原始字符串 } } return item; }); // 2. 检查是否是对象数组,如果是,可能需要按某个键排序 if (parsedArray.length > 0 && typeof parsedArray[0] === 'object' && !Array.isArray(parsedArray[0])) { // 检查是否所有元素都有相同的键 const firstKeys = Object.keys(parsedArray[0]).sort().join(','); const allSameStructure = parsedArray.every(item => typeof item === 'object' && !Array.isArray(item) && Object.keys(item).sort().join(',') === firstKeys ); if (allSameStructure) { // 如果有 key 字段,按 key 排序 if ('key' in parsedArray[0]) { return parsedArray .map(item => normalizeValue(item)) .sort((a, b) => { const aKey = a.key; const bKey = b.key; if (aKey < bKey) return -1; if (aKey > bKey) return 1; return 0; }); } // 否则按整个对象的字符串表示排序 return parsedArray .map(item => normalizeValue(item)) .sort((a, b) => { const aStr = JSON.stringify(a); const bStr = JSON.stringify(b); if (aStr < bStr) return -1; if (aStr > bStr) return 1; return 0; }); } } // 递归处理每个元素 return parsedArray.map(item => normalizeValue(item)); } // 对象:按键排序 const sortedObj = {}; Object.keys(value).sort().forEach(key => { sortedObj[key] = normalizeValue(value[key]); }); return sortedObj; } // 渲染树形视图 function renderTreeView(containerId, data) { const container = document.getElementById(containerId); // 确保容器存在 if (!container) { console.error('树形视图容器不存在:', containerId); return; } if (!data) { container.innerHTML = '

无数据

'; return; } container.innerHTML = createTreeNode(data, ''); } // 创建树形节点 function createTreeNode(obj, indent = '') { if (obj === null) return 'null'; if (obj === undefined) return 'undefined'; const type = typeof obj; if (type !== 'object') { return `${escapeHtml(JSON.stringify(obj))}`; } if (Array.isArray(obj)) { if (obj.length === 0) return '[]'; let html = '
[
'; obj.forEach((item, index) => { html += `${indent} ${index}: ${createTreeNode(item, indent + ' ')}`; if (index < obj.length - 1) html += ','; html += '
'; }); html += `${indent}]
`; return html; } const keys = Object.keys(obj); if (keys.length === 0) return '{}'; let html = '
{
'; keys.forEach((key, index) => { html += `${indent} "${key}": ${createTreeNode(obj[key], indent + ' ')}`; if (index < keys.length - 1) html += ','; html += '
'; }); html += `${indent}}
`; return html; } // 设置同步滚动 function setupSyncScroll(tabType) { if (tabType === 'formatted') { // 格式化视图的两个pre元素 const proPre = document.querySelector('#formatted .col-md-6:first-child pre'); const testPre = document.querySelector('#formatted .col-md-6:last-child pre'); if (proPre && testPre) { let syncing = false; proPre.addEventListener('scroll', () => { if (!syncing) { syncing = true; testPre.scrollTop = proPre.scrollTop; testPre.scrollLeft = proPre.scrollLeft; setTimeout(() => syncing = false, 10); } }); testPre.addEventListener('scroll', () => { if (!syncing) { syncing = true; proPre.scrollTop = testPre.scrollTop; proPre.scrollLeft = testPre.scrollLeft; setTimeout(() => syncing = false, 10); } }); } } else if (tabType === 'tree') { // 树形视图的两个div元素 const proTree = document.getElementById('proTreeView'); const testTree = document.getElementById('testTreeView'); if (proTree && testTree) { let syncing = false; proTree.addEventListener('scroll', () => { if (!syncing) { syncing = true; testTree.scrollTop = proTree.scrollTop; testTree.scrollLeft = proTree.scrollLeft; setTimeout(() => syncing = false, 10); } }); testTree.addEventListener('scroll', () => { if (!syncing) { syncing = true; proTree.scrollTop = testTree.scrollTop; proTree.scrollLeft = testTree.scrollLeft; setTimeout(() => syncing = false, 10); } }); } } } // 显示比较总结 function displayComparisonSummary(summary) { const summaryContainer = document.getElementById('comparison-summary'); if (!summary) { summaryContainer.innerHTML = '

无总结数据

'; return; } const qualityLevel = summary.data_quality.quality_level; const html = `
数据概览

${summary.overview.total_keys_queried}

查询Key总数

${summary.overview.identical_records}

相同记录

${summary.overview.different_records}

差异记录
数据质量评估

${qualityLevel.level}

${qualityLevel.description}

${summary.data_quality.consistency_score}%
数据一致性评分
百分比统计
数据一致性: ${summary.percentages.data_consistency}%
数据差异率: ${summary.percentages.data_differences}%
数据缺失率: ${summary.percentages.missing_rate}%
字段差异TOP5
${summary.field_analysis.most_different_fields.length > 0 ? summary.field_analysis.most_different_fields.map(([field, count]) => `
${field} ${count}次
`).join('') : '

无字段差异统计

' }
改进建议
`; summaryContainer.innerHTML = html; } // 显示字段差异统计 function displayFieldStats(fieldStats) { const fieldStatsContainer = document.getElementById('field_stats'); if (!Object.keys(fieldStats).length) { fieldStatsContainer.innerHTML = '

无字段差异统计

'; return; } let html = '
'; html += ''; const totalDiffs = Object.values(fieldStats).reduce((sum, count) => sum + count, 0); Object.entries(fieldStats) .sort(([, a], [, b]) => b - a) .forEach(([field, count]) => { const percentage = ((count / totalDiffs) * 100).toFixed(1); html += ` `; }); html += '
字段名差异次数占比
${field} ${count} ${percentage}%
'; fieldStatsContainer.innerHTML = html; } // 清空结果 function clearResults() { document.getElementById('results').style.display = 'none'; document.getElementById('query_values').value = ''; currentResults = null; showAlert('info', '结果已清空'); } // 显示查询历史对话框 async function showQueryHistoryDialog() { try { const response = await fetch('/api/query-history'); const result = await response.json(); if (result.success) { let historyList = ''; if (result.data.length === 0) { historyList = '

暂无查询历史记录

'; } else { // 添加批量操作控制栏 historyList += `
`; result.data.forEach(history => { const createdDate = new Date(history.created_at).toLocaleString(); const consistencyRate = history.total_keys > 0 ? Math.round((history.identical_count / history.total_keys) * 100) : 0; const safeName = (history.name || '').replace(/'/g, "\\'"); historyList += `
${history.name} ${history.query_type === 'sharding' ? '分表查询' : '单表查询'}

${history.description || '无描述'}

${createdDate}
查询: ${history.total_keys}个Key
一致性: ${consistencyRate}%
差异: ${history.differences_count}处
`; }); } const modalContent = ` `; // 移除现有modal const existingModal = document.getElementById('queryHistoryModal'); if (existingModal) { existingModal.remove(); } // 添加新modal到body document.body.insertAdjacentHTML('beforeend', modalContent); // 显示modal const modal = new bootstrap.Modal(document.getElementById('queryHistoryModal')); modal.show(); } else { showAlert('danger', '加载查询历史记录失败'); } } catch (error) { showAlert('danger', '加载查询历史记录失败: ' + error.message); } } // 加载历史记录配置 async function loadHistoryRecord(historyId) { try { const response = await fetch(`/api/query-history/${historyId}`); const result = await response.json(); if (result.success) { const history = result.data; // 填充生产环境配置 document.getElementById('pro_cluster_name').value = history.pro_config.cluster_name || ''; document.getElementById('pro_datacenter').value = history.pro_config.datacenter || ''; document.getElementById('pro_hosts').value = (history.pro_config.hosts || []).join(','); document.getElementById('pro_port').value = history.pro_config.port || 9042; document.getElementById('pro_username').value = history.pro_config.username || ''; document.getElementById('pro_password').value = history.pro_config.password || ''; document.getElementById('pro_keyspace').value = history.pro_config.keyspace || ''; document.getElementById('pro_table').value = history.pro_config.table || ''; // 填充测试环境配置 document.getElementById('test_cluster_name').value = history.test_config.cluster_name || ''; document.getElementById('test_datacenter').value = history.test_config.datacenter || ''; document.getElementById('test_hosts').value = (history.test_config.hosts || []).join(','); document.getElementById('test_port').value = history.test_config.port || 9042; document.getElementById('test_username').value = history.test_config.username || ''; document.getElementById('test_password').value = history.test_config.password || ''; document.getElementById('test_keyspace').value = history.test_config.keyspace || ''; document.getElementById('test_table').value = history.test_config.table || ''; // 填充查询配置 - 使用与配置组加载相同的逻辑 if (history.query_config) { console.log('loadHistoryRecord - 设置keys字段:', history.query_config.keys); const keysElement = document.getElementById('keys'); console.log('loadHistoryRecord - keys元素:', keysElement); if (keysElement) { keysElement.value = (history.query_config.keys || []).join(','); console.log('loadHistoryRecord - keys字段设置后的值:', keysElement.value); // 强制触发多种事件以确保UI更新 keysElement.dispatchEvent(new Event('input', { bubbles: true })); keysElement.dispatchEvent(new Event('change', { bubbles: true })); // 添加延迟更新,确保模态框关闭后再次设置 setTimeout(() => { keysElement.value = (history.query_config.keys || []).join(','); keysElement.dispatchEvent(new Event('input', { bubbles: true })); console.log('loadHistoryRecord - 延迟设置后的值:', keysElement.value); }, 100); } else { console.error('loadHistoryRecord - 未找到keys输入元素!'); } const fieldsToCompareElement = document.getElementById('fields_to_compare'); const excludeFieldsElement = document.getElementById('exclude_fields'); if (fieldsToCompareElement) { fieldsToCompareElement.value = (history.query_config.fields_to_compare || []).join(','); fieldsToCompareElement.dispatchEvent(new Event('input', { bubbles: true })); fieldsToCompareElement.dispatchEvent(new Event('change', { bubbles: true })); setTimeout(() => { fieldsToCompareElement.value = (history.query_config.fields_to_compare || []).join(','); fieldsToCompareElement.dispatchEvent(new Event('input', { bubbles: true })); }, 100); } if (excludeFieldsElement) { excludeFieldsElement.value = (history.query_config.exclude_fields || []).join(','); excludeFieldsElement.dispatchEvent(new Event('input', { bubbles: true })); excludeFieldsElement.dispatchEvent(new Event('change', { bubbles: true })); setTimeout(() => { excludeFieldsElement.value = (history.query_config.exclude_fields || []).join(','); excludeFieldsElement.dispatchEvent(new Event('input', { bubbles: true })); }, 100); } } else { console.warn('loadHistoryRecord - 查询配置为空或未定义'); document.getElementById('keys').value = ''; document.getElementById('fields_to_compare').value = ''; document.getElementById('exclude_fields').value = ''; } // 填充查询Key值 document.getElementById('query_values').value = (history.query_keys || []).join('\n'); // 处理分表配置(如果存在) if (history.query_type === 'sharding' && history.sharding_config) { // 启用分表模式 document.getElementById('enableSharding').checked = true; toggleShardingMode(); // 填充分表配置 document.getElementById('use_sharding_for_pro').checked = history.sharding_config.use_sharding_for_pro || false; document.getElementById('use_sharding_for_test').checked = history.sharding_config.use_sharding_for_test || false; document.getElementById('pro_interval_seconds').value = history.sharding_config.pro_interval_seconds || 604800; document.getElementById('pro_table_count').value = history.sharding_config.pro_table_count || 14; document.getElementById('test_interval_seconds').value = history.sharding_config.test_interval_seconds || 604800; document.getElementById('test_table_count').value = history.sharding_config.test_table_count || 14; } else { // 禁用分表模式 document.getElementById('enableSharding').checked = false; toggleShardingMode(); } // 关闭历史记录modal const modal = bootstrap.Modal.getInstance(document.getElementById('queryHistoryModal')); modal.hide(); const queryTypeDesc = history.query_type === 'sharding' ? '分表查询' : '单表查询'; showAlert('success', `${queryTypeDesc}历史记录 "${history.name}" 加载成功`); } else { showAlert('danger', result.error || '加载历史记录失败'); } } catch (error) { showAlert('danger', '加载历史记录失败: ' + error.message); } } // 查看历史记录详情 async function viewHistoryDetail(historyId) { try { const response = await fetch(`/api/query-history/${historyId}`); const result = await response.json(); if (result.success) { const history = result.data; const summary = history.results_summary; const modalContent = ` `; // 移除现有modal const existingModal = document.getElementById('historyDetailModal'); if (existingModal) { existingModal.remove(); } // 添加新modal到body document.body.insertAdjacentHTML('beforeend', modalContent); // 显示modal const modal = new bootstrap.Modal(document.getElementById('historyDetailModal')); modal.show(); } else { showAlert('danger', '加载历史记录详情失败'); } } catch (error) { showAlert('danger', '加载历史记录详情失败: ' + error.message); } } // 加载历史记录结果 async function loadHistoryResults(historyId) { try { const response = await fetch(`/api/query-history/${historyId}/results`); const result = await response.json(); if (result.success) { // 设置当前结果数据 currentResults = result.data; // 确保必要的数组字段存在 if (!currentResults.differences) currentResults.differences = []; if (!currentResults.identical_results) currentResults.identical_results = []; if (!currentResults.raw_pro_data) currentResults.raw_pro_data = []; if (!currentResults.raw_test_data) currentResults.raw_test_data = []; if (!currentResults.field_diff_count) currentResults.field_diff_count = {}; if (!currentResults.summary) currentResults.summary = {}; // 根据查询类型设置分表模式 if (result.data.history_info && result.data.history_info.query_type === 'sharding') { isShardingMode = true; document.getElementById('enableSharding').checked = true; toggleShardingMode(); } else { isShardingMode = false; document.getElementById('enableSharding').checked = false; toggleShardingMode(); } // 显示结果 displayResults(result.data); // 关闭历史记录modal const modal = bootstrap.Modal.getInstance(document.getElementById('queryHistoryModal')); if (modal) { modal.hide(); } const queryTypeDesc = (result.data.history_info && result.data.history_info.query_type === 'sharding') ? '分表查询' : '单表查询'; const historyName = (result.data.history_info && result.data.history_info.name) || '未知'; showAlert('success', `${queryTypeDesc}历史记录结果 "${historyName}" 加载成功`); } else { showAlert('danger', result.error || '加载历史记录结果失败'); } } catch (error) { console.error('加载历史记录结果失败:', error); showAlert('danger', '加载历史记录结果失败: ' + error.message); } } // 删除历史记录 async function deleteHistoryRecord(historyId, historyName) { if (!confirm(`确定要删除历史记录 "${historyName}" 吗?此操作不可撤销。`)) { return; } try { const response = await fetch(`/api/query-history/${historyId}`, { method: 'DELETE' }); const result = await response.json(); if (result.success) { showAlert('success', result.message); // 重新显示历史记录对话框 setTimeout(() => { showQueryHistoryDialog(); }, 500); } else { showAlert('danger', result.error || '删除历史记录失败'); } } catch (error) { showAlert('danger', '删除历史记录失败: ' + error.message); } } // 显示保存历史记录对话框 function showSaveHistoryDialog() { if (!currentResults) { showAlert('warning', '请先执行查询操作,然后再保存历史记录'); return; } const modalContent = ` `; // 移除现有modal const existingModal = document.getElementById('saveHistoryModal'); if (existingModal) { existingModal.remove(); } // 添加新modal到body document.body.insertAdjacentHTML('beforeend', modalContent); // 显示modal const modal = new bootstrap.Modal(document.getElementById('saveHistoryModal')); modal.show(); } // 保存历史记录 async function saveHistoryRecord() { const name = document.getElementById('historyRecordName').value.trim(); const description = document.getElementById('historyRecordDesc').value.trim(); if (!name) { showAlert('warning', '请输入历史记录名称'); return; } if (!currentResults) { showAlert('warning', '没有可保存的查询结果'); return; } const config = getCurrentConfig(); // 确定查询类型和获取相应配置 let queryType = 'single'; let shardingConfig = null; if (isShardingMode) { queryType = 'sharding'; shardingConfig = getShardingConfig().sharding_config; } try { const response = await fetch('/api/query-history', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name, description: description, pro_config: config.pro_config, test_config: config.test_config, query_config: config.query_config, query_keys: config.values, results_summary: currentResults.summary || {}, execution_time: 0.0, total_keys: currentResults.total_keys, differences_count: currentResults.differences.length, identical_count: currentResults.identical_results.length, // 新增分表相关字段 sharding_config: shardingConfig, query_type: queryType }) }); const result = await response.json(); if (result.success) { // 关闭modal const modal = bootstrap.Modal.getInstance(document.getElementById('saveHistoryModal')); modal.hide(); showAlert('success', result.message); } else { showAlert('danger', result.error || '保存历史记录失败'); } } catch (error) { showAlert('danger', '保存历史记录失败: ' + error.message); } } // 导出配置 function exportConfig() { const config = getCurrentConfig(); const dataStr = JSON.stringify(config, null, 2); const dataBlob = new Blob([dataStr], {type: 'application/json'}); const link = document.createElement('a'); link.href = URL.createObjectURL(dataBlob); link.download = 'db_compare_config.json'; link.click(); } // 导出结果 function exportResults() { if (!currentResults) { showAlert('warning', '没有可导出的结果'); return; } let output = `数据库查询比对结果\n`; output += `生成时间: ${new Date().toLocaleString()}\n`; output += `=`.repeat(50) + '\n\n'; output += `统计信息:\n`; output += `- 总Key数量: ${currentResults.total_keys}\n`; output += `- 生产表记录: ${currentResults.pro_count}\n`; output += `- 测试表记录: ${currentResults.test_count}\n`; output += `- 发现差异: ${currentResults.differences.length}\n\n`; if (currentResults.differences.length > 0) { output += `差异详情:\n`; output += `-`.repeat(30) + '\n'; currentResults.differences.forEach((diff, index) => { output += `差异 #${index + 1}:\n`; output += `主键: ${JSON.stringify(diff.key)}\n`; if (diff.message) { output += `消息: ${diff.message}\n`; } else { output += `字段: ${diff.field}\n`; output += `生产表值: ${diff.pro_value}\n`; output += `测试表值: ${diff.test_value}\n`; } output += '\n'; }); // 字段差异统计 if (Object.keys(currentResults.field_diff_count).length > 0) { output += `字段差异统计:\n`; output += `-`.repeat(30) + '\n'; Object.entries(currentResults.field_diff_count) .sort(([, a], [, b]) => b - a) .forEach(([field, count]) => { output += `${field}: ${count}次\n`; }); } } const dataBlob = new Blob([output], {type: 'text/plain;charset=utf-8'}); const link = document.createElement('a'); link.href = URL.createObjectURL(dataBlob); link.download = `db_compare_result_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.txt`; link.click(); } // 显示一键导入对话框 function showImportDialog(env) { const envName = env === 'pro' ? '生产环境' : '测试环境'; const modalContent = ` `; // 移除现有modal const existingModal = document.getElementById('importModal'); if (existingModal) { existingModal.remove(); } // 添加新modal到body document.body.insertAdjacentHTML('beforeend', modalContent); // 显示modal const modal = new bootstrap.Modal(document.getElementById('importModal')); modal.show(); } // 导入配置数据 function importConfig(env) { const configData = document.getElementById('importConfigData').value.trim(); if (!configData) { showAlert('warning', '请输入配置数据'); return; } try { // 解析配置数据 const config = parseConfigData(configData); // 根据环境类型填充对应的表单字段 const prefix = env === 'pro' ? 'pro' : 'test'; if (config.clusterName) { document.getElementById(`${prefix}_cluster_name`).value = config.clusterName; } if (config.datacenter) { document.getElementById(`${prefix}_datacenter`).value = config.datacenter; } if (config.clusterNodes) { document.getElementById(`${prefix}_hosts`).value = config.clusterNodes; } if (config.port) { document.getElementById(`${prefix}_port`).value = config.port; } if (config.username) { document.getElementById(`${prefix}_username`).value = config.username; } if (config.password) { document.getElementById(`${prefix}_password`).value = config.password; } if (config.keyspace) { document.getElementById(`${prefix}_keyspace`).value = config.keyspace; } if (config.table) { document.getElementById(`${prefix}_table`).value = config.table; } // 关闭modal const modal = bootstrap.Modal.getInstance(document.getElementById('importModal')); modal.hide(); const envName = env === 'pro' ? '生产环境' : '测试环境'; showAlert('success', `${envName}配置导入成功!`); } catch (error) { showAlert('danger', '配置解析失败:' + error.message); } } // 解析配置数据 function parseConfigData(configText) { const config = {}; try { // 尝试按JSON格式解析 const jsonConfig = JSON.parse(configText); return jsonConfig; } catch (e) { // 如果不是JSON,按YAML格式解析 const lines = configText.split('\n'); for (const line of lines) { const trimmedLine = line.trim(); if (!trimmedLine || trimmedLine.startsWith('#')) { continue; // 跳过空行和注释 } const colonIndex = trimmedLine.indexOf(':'); if (colonIndex === -1) { continue; // 跳过无效行 } const key = trimmedLine.substring(0, colonIndex).trim(); let value = trimmedLine.substring(colonIndex + 1).trim(); // 处理带引号的值 if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } // 尝试转换数字 if (!isNaN(value) && value !== '') { config[key] = parseInt(value); } else { config[key] = value; } } return config; } } // 显示提示信息 function showAlert(type, message) { // 移除已存在的提示 const existingAlert = document.querySelector('.alert'); if (existingAlert) { existingAlert.remove(); } const alertHtml = ` `; // 插入到页面顶部 const container = document.querySelector('.container-fluid'); container.insertAdjacentHTML('afterbegin', alertHtml); // 自动隐藏 setTimeout(() => { const alert = document.querySelector('.alert'); if (alert) { alert.remove(); } }, 5000); } // 查询日志相关功能 let allQueryLogs = []; // 存储所有日志 async function refreshQueryLogs() { // 这个函数现在不再使用,因为查询日志已经独立到模态框中 // 如果需要刷新查询日志,请使用 refreshModalQueryLogs() console.warn('refreshQueryLogs() 已弃用,请使用 refreshModalQueryLogs()'); } // 显示分组查询日志 - 已弃用 function displayGroupedQueryLogs(groupedLogs) { // 这个函数已弃用,查询日志现在使用独立模态框显示 console.warn('displayGroupedQueryLogs() 已弃用,查询日志现在使用独立模态框显示'); return; } // 计算持续时间 function calculateDuration(startTime, endTime) { try { const start = new Date(startTime); const end = new Date(endTime); const diffMs = end - start; if (diffMs < 1000) { return `${diffMs}ms`; } else if (diffMs < 60000) { return `${(diffMs / 1000).toFixed(1)}秒`; } else { const minutes = Math.floor(diffMs / 60000); const seconds = Math.floor((diffMs % 60000) / 1000); return `${minutes}分${seconds}秒`; } } catch (e) { return '未知'; } } // 格式化日志消息 function formatLogMessage(message) { // HTML转义 let formatted = escapeHtml(message); // 高亮SQL查询语句 if (formatted.includes('执行查询SQL:')) { formatted = formatted.replace(/执行查询SQL: (SELECT.*?);/g, '执行查询SQL:
$1;'); } // 高亮重要信息 formatted = formatted.replace(/([\d.]+秒)/g, '$1'); formatted = formatted.replace(/(返回记录数=\d+)/g, '$1'); formatted = formatted.replace(/(执行时间=[\d.]+秒)/g, '$1'); formatted = formatted.replace(/(分表索引=\d+)/g, '$1'); formatted = formatted.replace(/(表名=\w+)/g, '$1'); return formatted; } function filterLogsByLevel() { // 这个函数已弃用,因为查询日志现在使用独立模态框显示 console.warn('filterLogsByLevel() 已弃用,请使用 filterModalLogsByLevel()'); } async function clearQueryLogs() { if (!confirm('确定要清空所有查询日志吗?这将删除内存和数据库中的所有日志记录。')) { return; } try { const response = await fetch('/api/query-logs?clear_db=true', { method: 'DELETE' }); const result = await response.json(); if (result.success) { // 如果查询日志模态框是打开的,清空显示内容 const modalLogsContainer = document.getElementById('modal-query-logs'); if (modalLogsContainer) { modalLogsContainer.innerHTML = '
查询日志已清空
'; } showAlert('success', result.message); } else { showAlert('danger', '清空查询日志失败: ' + result.error); } } catch (error) { console.error('清空查询日志失败:', error); showAlert('danger', '清空查询日志失败'); } } // 清理旧日志 async function cleanupOldLogs() { const days = prompt('请输入要保留的天数(默认30天):', '30'); if (days === null) return; // 用户取消 const daysToKeep = parseInt(days) || 30; if (daysToKeep <= 0) { showAlert('warning', '保留天数必须大于0'); return; } if (!confirm(`确定要清理超过 ${daysToKeep} 天的旧日志吗?`)) { return; } try { const response = await fetch('/api/query-logs/cleanup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ days_to_keep: daysToKeep }) }); const result = await response.json(); if (result.success) { showAlert('success', result.message); // 刷新模态框中的日志显示 refreshModalQueryLogs(); } else { showAlert('danger', '清理旧日志失败: ' + result.error); } } catch (error) { console.error('清理旧日志失败:', error); showAlert('danger', '清理旧日志失败'); } } // 显示查询日志对话框 function showQueryLogsDialog() { // 打开模态框 const modal = new bootstrap.Modal(document.getElementById('queryLogsModal')); modal.show(); // 加载查询日志 refreshModalQueryLogs(); } // 刷新模态框中的查询日志 async function refreshModalQueryLogs() { try { const response = await fetch('/api/query-logs?grouped=true&from_db=true'); const result = await response.json(); if (result.success && result.data) { if (result.grouped) { displayModalGroupedQueryLogs(result.data); } else { // 兼容旧版本的平铺显示 displayModalQueryLogs(result.data); } } else { document.getElementById('modal-query-logs').innerHTML = '
无法获取查询日志
'; } } catch (error) { console.error('获取查询日志失败:', error); document.getElementById('modal-query-logs').innerHTML = '
获取查询日志失败
'; } } // 在模态框中显示分组查询日志 function displayModalGroupedQueryLogs(groupedLogs) { const container = document.getElementById('modal-query-logs'); if (!groupedLogs || groupedLogs.length === 0) { container.innerHTML = '
暂无查询日志
'; return; } // 获取当前的过滤器状态,如果复选框不存在则默认显示所有 const showInfo = document.getElementById('modal-log-level-info')?.checked ?? true; const showWarning = document.getElementById('modal-log-level-warning')?.checked ?? true; const showError = document.getElementById('modal-log-level-error')?.checked ?? true; let html = ''; // 为每个批次生成折叠面板 groupedLogs.forEach((batchData, index) => { const [batchId, logs] = batchData; const isExpanded = index === groupedLogs.length - 1; // 默认展开最新批次 const collapseId = `modal-batch-${batchId}`; // 过滤日志 const filteredLogs = logs.filter(log => { return (log.level === 'INFO' && showInfo) || (log.level === 'WARNING' && showWarning) || (log.level === 'ERROR' && showError); }); // 如果过滤后没有日志,跳过这个批次 if (filteredLogs.length === 0) { return; } // 统计批次信息(基于原始日志) const logCounts = { INFO: logs.filter(log => log.level === 'INFO').length, WARNING: logs.filter(log => log.level === 'WARNING').length, ERROR: logs.filter(log => log.level === 'ERROR').length }; const totalLogs = logs.length; const filteredCount = filteredLogs.length; const firstLog = logs[0]; const lastLog = logs[logs.length - 1]; const duration = firstLog && lastLog ? calculateDuration(firstLog.timestamp, lastLog.timestamp) : '0秒'; // 确定批次类型和状态 const hasErrors = logCounts.ERROR > 0; const hasWarnings = logCounts.WARNING > 0; const batchStatus = hasErrors ? 'danger' : hasWarnings ? 'warning' : 'success'; const batchIcon = hasErrors ? 'fas fa-times-circle' : hasWarnings ? 'fas fa-exclamation-triangle' : 'fas fa-check-circle'; // 提取查询类型 const batchTypeMatch = firstLog?.message.match(/开始(\w+)查询批次/) || firstLog?.message.match(/🚀 开始执行(\w+)数据比较/); const batchType = batchTypeMatch ? batchTypeMatch[1] : '未知'; // 提取并格式化查询日期 const queryDate = firstLog ? new Date(firstLog.timestamp).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '未知时间'; // 检查是否有关联的历史记录ID const historyId = firstLog?.history_id; const historyBadge = historyId ? ` ID:${historyId} ` : ''; html += `
${batchType}查询批次 ${batchId} ${filteredCount}/${totalLogs}条日志 ${historyBadge}
查询时间: ${queryDate}
持续时间: ${duration}
${logCounts.INFO > 0 ? `${logCounts.INFO} INFO` : ''} ${logCounts.WARNING > 0 ? `${logCounts.WARNING} WARN` : ''} ${logCounts.ERROR > 0 ? `${logCounts.ERROR} ERROR` : ''}
`; // 显示过滤后的日志条目 filteredLogs.forEach(log => { const levelClass = log.level === 'ERROR' ? 'danger' : log.level === 'WARNING' ? 'warning' : 'info'; const levelIcon = log.level === 'ERROR' ? 'fas fa-times-circle' : log.level === 'WARNING' ? 'fas fa-exclamation-triangle' : 'fas fa-info-circle'; // 简化时间戳显示 const timeOnly = log.timestamp ? (log.timestamp.split(' ')[1] || log.timestamp) : '未知时间'; html += `
${log.level} ${timeOnly}
${formatLogMessage(log.message || '无消息内容')}
`; }); html += `
`; }); if (html === '') { html = '
没有符合过滤条件的日志
'; } container.innerHTML = html; } // 过滤模态框中的日志级别 function filterModalLogsByLevel() { // 刷新分组日志显示,应用过滤器 refreshModalQueryLogs(); } // 显示平铺的查询日志(兼容性) function displayModalQueryLogs(logs) { const container = document.getElementById('modal-query-logs'); if (!logs || logs.length === 0) { container.innerHTML = '
暂无查询日志
'; return; } const showInfo = document.getElementById('modal-log-level-info').checked; const showWarning = document.getElementById('modal-log-level-warning').checked; const showError = document.getElementById('modal-log-level-error').checked; let html = '
'; logs.forEach(log => { const shouldShow = (log.level === 'INFO' && showInfo) || (log.level === 'WARNING' && showWarning) || (log.level === 'ERROR' && showError); if (shouldShow) { const levelClass = log.level === 'ERROR' ? 'danger' : log.level === 'WARNING' ? 'warning' : 'info'; const levelIcon = log.level === 'ERROR' ? 'fas fa-times-circle' : log.level === 'WARNING' ? 'fas fa-exclamation-triangle' : 'fas fa-info-circle'; html += `
${log.level} ${log.timestamp}
${formatLogMessage(log.message)}
`; } }); html += '
'; container.innerHTML = html; } // 从查询日志模态框中加载历史记录 function loadHistoryFromLogs(historyId) { // 先关闭查询日志模态框 const logsModal = bootstrap.Modal.getInstance(document.getElementById('queryLogsModal')); if (logsModal) { logsModal.hide(); } // 延迟一下再加载历史记录,确保模态框已关闭 setTimeout(() => { loadHistoryResults(historyId); }, 300); } // 根据历史记录ID获取相关查询日志 async function loadQueryLogsByHistory(historyId) { try { const response = await fetch(`/api/query-logs/history/${historyId}`); const result = await response.json(); if (result.success && result.data) { // 显示关联的查询日志 displayHistoryRelatedLogs(result.data, historyId); return result.data; } else { console.warn('获取历史记录相关日志失败:', result.error); return []; } } catch (error) { console.error('获取历史记录相关查询日志失败:', error); return []; } } // 显示历史记录相关的查询日志 function displayHistoryRelatedLogs(groupedLogs, historyId) { const container = document.getElementById('query-logs'); if (!groupedLogs || groupedLogs.length === 0) { container.innerHTML = `
历史记录 ID ${historyId} 没有关联的查询日志
`; return; } let html = `
显示历史记录 ID ${historyId} 的相关查询日志 (共 ${groupedLogs.length} 个批次)
`; // 使用与refreshQueryLogs相同的显示逻辑 groupedLogs.forEach((batchData, index) => { const [batchId, logs] = batchData; const isExpanded = true; // 历史记录日志默认展开所有批次 const collapseId = `history-batch-${batchId}`; // 统计批次信息 const logCounts = { INFO: logs.filter(log => log.level === 'INFO').length, WARNING: logs.filter(log => log.level === 'WARNING').length, ERROR: logs.filter(log => log.level === 'ERROR').length }; const totalLogs = logs.length; const firstLog = logs[0]; const lastLog = logs[logs.length - 1]; const duration = firstLog && lastLog ? calculateDuration(firstLog.timestamp, lastLog.timestamp) : '0秒'; // 确定批次类型和状态 const hasErrors = logCounts.ERROR > 0; const hasWarnings = logCounts.WARNING > 0; const batchStatus = hasErrors ? 'danger' : hasWarnings ? 'warning' : 'success'; const batchIcon = hasErrors ? 'fas fa-times-circle' : hasWarnings ? 'fas fa-exclamation-triangle' : 'fas fa-check-circle'; // 提取查询类型 const batchTypeMatch = firstLog?.message.match(/开始(\w+)查询批次/); const batchType = batchTypeMatch ? batchTypeMatch[1] : '未知'; // 提取并格式化查询日期 const queryDate = firstLog ? new Date(firstLog.timestamp).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '未知时间'; html += `
${batchType}查询批次 ${batchId} ${totalLogs}条日志 历史记录
查询时间: ${queryDate}
持续时间: ${duration}
${logCounts.INFO > 0 ? `${logCounts.INFO} INFO` : ''} ${logCounts.WARNING > 0 ? `${logCounts.WARNING} WARN` : ''} ${logCounts.ERROR > 0 ? `${logCounts.ERROR} ERROR` : ''}
`; // 显示该批次的日志条目 logs.forEach(log => { const shouldShow = true; // 历史记录日志显示所有级别 if (shouldShow) { const levelClass = log.level === 'ERROR' ? 'danger' : log.level === 'WARNING' ? 'warning' : 'info'; const levelIcon = log.level === 'ERROR' ? 'fas fa-times-circle' : log.level === 'WARNING' ? 'fas fa-exclamation-triangle' : 'fas fa-info-circle'; // 简化时间戳显示 const timeOnly = log.timestamp.split(' ')[1] || log.timestamp; html += `
${log.level} ${timeOnly}
${formatLogMessage(log.message)}
`; } }); html += `
`; }); container.innerHTML = html; } // 在查询执行后自动刷新日志 function autoRefreshLogsAfterQuery() { // 延迟一下确保后端日志已经记录 setTimeout(() => { // 只有当查询日志模态框打开时才刷新 const queryLogsModal = document.getElementById('queryLogsModal'); if (queryLogsModal && queryLogsModal.classList.contains('show')) { refreshModalQueryLogs(); } }, 500); } // 原始数据相关变量 let currentRawData = null; let filteredRawData = []; let currentRawDataPage = 1; let rawDataPageSize = 20; // 显示原始数据 function displayRawData(results) { currentRawData = results; // 添加调试信息 console.log('displayRawData - results.query_config:', results.query_config); console.log('displayRawData - 当前数据键:', Object.keys(results)); // 合并生产和测试环境数据 const combinedData = []; if (results.raw_pro_data) { results.raw_pro_data.forEach(record => { combinedData.push({ environment: 'production', data: record, displayName: '生产环境' }); }); } if (results.raw_test_data) { results.raw_test_data.forEach(record => { combinedData.push({ environment: 'test', data: record, displayName: '测试环境' }); }); } // 按环境排序,生产环境在前 combinedData.sort((a, b) => { if (a.environment === 'production' && b.environment === 'test') return -1; if (a.environment === 'test' && b.environment === 'production') return 1; return 0; }); filteredRawData = combinedData; currentRawDataPage = 1; renderRawDataContent(); } // 渲染原始数据内容 function renderRawDataContent() { const container = document.getElementById('raw-data-content'); if (!filteredRawData.length) { container.innerHTML = '
没有原始数据可显示
'; return; } // 分页计算 const totalPages = Math.ceil(filteredRawData.length / rawDataPageSize); const startIndex = (currentRawDataPage - 1) * rawDataPageSize; const endIndex = startIndex + rawDataPageSize; const currentPageData = filteredRawData.slice(startIndex, endIndex); let html = `
共 ${filteredRawData.length} 条记录
${generateRawDataPagination(currentRawDataPage, totalPages)}
`; // 显示数据记录 currentPageData.forEach((item, index) => { const globalIndex = startIndex + index + 1; const envBadgeClass = item.environment === 'production' ? 'bg-primary' : 'bg-success'; // 获取主键值用于标识 - 修复主键解析问题,支持复合主键 let keyValue = 'N/A'; // 从当前配置获取keys,因为query_config可能不在results中 const currentConfig = getCurrentConfig(); console.log('渲染原始数据 - currentConfig.query_config.keys:', currentConfig.query_config.keys); console.log('渲染原始数据 - item.data keys:', Object.keys(item.data)); if (currentConfig && currentConfig.query_config && currentConfig.query_config.keys && currentConfig.query_config.keys.length > 0) { const keyFields = currentConfig.query_config.keys; const keyValues = keyFields.map(key => { const value = item.data[key]; console.log(`查找主键字段 ${key}: `, value); return value || 'N/A'; }).filter(val => val !== 'N/A'); if (keyValues.length > 0) { // 复合主键显示格式:docid1,id1 或 单主键显示:value1 keyValue = keyValues.join(', '); } } else { // 如果没有配置主键,尝试常见的主键字段名 const commonKeyFields = ['id', 'statusid', 'key', 'uuid', 'docid']; for (const field of commonKeyFields) { if (item.data[field] !== undefined) { keyValue = item.data[field]; break; } } } // 格式化JSON数据 const jsonData = JSON.stringify(item.data, null, 2); html += `
记录 #${globalIndex} ${item.displayName}

主键: ${keyValue}

${escapeHtml(jsonData)}
`; }); // 底部分页 if (totalPages > 1) { html += `
${generateRawDataPagination(currentRawDataPage, totalPages)}
`; } container.innerHTML = html; } // 生成原始数据分页导航 function generateRawDataPagination(currentPage, totalPages) { if (totalPages <= 1) return ''; let html = ''; return html; } // 跳转到指定原始数据页面 function goToRawDataPage(page) { const totalPages = Math.ceil(filteredRawData.length / rawDataPageSize); if (page < 1 || page > totalPages) return; currentRawDataPage = page; renderRawDataContent(); } // 改变原始数据每页显示数量 function changeRawDataPageSize(newSize) { rawDataPageSize = parseInt(newSize); currentRawDataPage = 1; renderRawDataContent(); } // 过滤原始数据(按环境) function filterRawData() { if (!currentRawData) return; const showPro = document.getElementById('showProData').checked; const showTest = document.getElementById('showTestData').checked; let combinedData = []; if (showPro && currentRawData.raw_pro_data) { currentRawData.raw_pro_data.forEach(record => { combinedData.push({ environment: 'production', data: record, displayName: '生产环境' }); }); } if (showTest && currentRawData.raw_test_data) { currentRawData.raw_test_data.forEach(record => { combinedData.push({ environment: 'test', data: record, displayName: '测试环境' }); }); } // 按环境排序 combinedData.sort((a, b) => { if (a.environment === 'production' && b.environment === 'test') return -1; if (a.environment === 'test' && b.environment === 'production') return 1; return 0; }); filteredRawData = combinedData; currentRawDataPage = 1; renderRawDataContent(); } // 搜索原始数据 function searchRawData(searchTerm) { if (!currentRawData) return; const showPro = document.getElementById('showProData').checked; const showTest = document.getElementById('showTestData').checked; let combinedData = []; if (showPro && currentRawData.raw_pro_data) { currentRawData.raw_pro_data.forEach(record => { combinedData.push({ environment: 'production', data: record, displayName: '生产环境' }); }); } if (showTest && currentRawData.raw_test_data) { currentRawData.raw_test_data.forEach(record => { combinedData.push({ environment: 'test', data: record, displayName: '测试环境' }); }); } if (searchTerm.trim()) { const term = searchTerm.toLowerCase(); combinedData = combinedData.filter(item => { // 搜索所有字段值 return Object.values(item.data).some(value => String(value).toLowerCase().includes(term) ); }); } // 按环境排序 combinedData.sort((a, b) => { if (a.environment === 'production' && b.environment === 'test') return -1; if (a.environment === 'test' && b.environment === 'production') return 1; return 0; }); filteredRawData = combinedData; currentRawDataPage = 1; renderRawDataContent(); } // 复制原始记录 function copyRawRecord(recordStr) { try { const record = JSON.parse(recordStr); const formattedData = JSON.stringify(record, null, 2); navigator.clipboard.writeText(formattedData).then(() => { showAlert('success', '原始记录已复制到剪贴板'); }).catch(err => { console.error('复制失败:', err); showAlert('danger', '复制失败,请手动选择复制'); }); } catch (error) { showAlert('danger', '复制失败: ' + error.message); } } // 格式化复合主键显示 function formatCompositeKey(key) { if (!key) { return 'N/A'; } if (typeof key === 'object' && !Array.isArray(key)) { // 复合主键情况:显示为 "docid: value1, id: value2" 格式 return Object.entries(key) .map(([field, value]) => `${field}: ${value}`) .join(', '); } else { // 单主键或其他情况:使用JSON格式 return JSON.stringify(key); } } // 切换全选Cassandra查询历史记录 function toggleAllQueryHistorySelection() { const selectAllCheckbox = document.getElementById('selectAllQueryHistory'); const historyCheckboxes = document.querySelectorAll('.query-history-checkbox'); historyCheckboxes.forEach(checkbox => { checkbox.checked = selectAllCheckbox.checked; }); updateQueryHistorySelectionCount(); } // 更新Cassandra查询历史记录选择数量 function updateQueryHistorySelectionCount() { const selectedCheckboxes = document.querySelectorAll('.query-history-checkbox:checked'); const count = selectedCheckboxes.length; const totalCheckboxes = document.querySelectorAll('.query-history-checkbox'); // 更新显示计数 const countSpan = document.getElementById('selectedQueryHistoryCount'); if (countSpan) { countSpan.textContent = count; } // 更新批量删除按钮状态 const batchDeleteBtn = document.getElementById('batchDeleteQueryHistoryBtn'); if (batchDeleteBtn) { batchDeleteBtn.disabled = count === 0; } // 更新全选复选框状态 const selectAllCheckbox = document.getElementById('selectAllQueryHistory'); if (selectAllCheckbox) { if (count === 0) { selectAllCheckbox.indeterminate = false; selectAllCheckbox.checked = false; } else if (count === totalCheckboxes.length) { selectAllCheckbox.indeterminate = false; selectAllCheckbox.checked = true; } else { selectAllCheckbox.indeterminate = true; } } } // 批量删除Cassandra查询历史记录 async function batchDeleteQueryHistory() { const selectedCheckboxes = document.querySelectorAll('.query-history-checkbox:checked'); const selectedIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value)); if (selectedIds.length === 0) { showAlert('warning', '请选择要删除的历史记录'); return; } if (!confirm(`确定要删除选中的 ${selectedIds.length} 条历史记录吗?此操作不可恢复。`)) { return; } try { const response = await fetch('/api/query-history/batch-delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ history_ids: selectedIds }) }); const result = await response.json(); if (result.success) { showAlert('success', `成功删除 ${result.deleted_count} 条历史记录`); // 重新显示历史记录列表 setTimeout(() => { showQueryHistoryDialog(); }, 500); } else { showAlert('danger', result.error || '批量删除失败'); } } catch (error) { console.error('批量删除Cassandra查询历史记录失败:', error); showAlert('danger', `批量删除失败: ${error.message}`); } } // 按主键分组差异数据 function groupDifferencesByKey(differences) { const grouped = {}; differences.forEach(diff => { const keyStr = typeof diff.key === 'object' ? JSON.stringify(diff.key) : diff.key; if (!grouped[keyStr]) { grouped[keyStr] = []; } grouped[keyStr].push(diff); }); return grouped; } // 按字段筛选差异 let currentFieldFilter = ''; function filterByField(field) { currentFieldFilter = field; // 更新按钮状态 const buttons = document.querySelectorAll('.btn-group button'); buttons.forEach(btn => { btn.classList.remove('active', 'btn-outline-primary'); btn.classList.add('btn-outline-secondary'); }); // 设置当前选中的按钮 if (field === '') { buttons[0].classList.remove('btn-outline-secondary'); buttons[0].classList.add('btn-outline-primary', 'active'); } else { buttons.forEach(btn => { if (btn.textContent.includes(field + ' (')) { btn.classList.remove('btn-outline-secondary'); btn.classList.add('btn-outline-primary', 'active'); } }); } // 筛选差异数据(考虑搜索条件) let baseResults = field === '' ? currentResults.differences : currentResults.differences.filter(diff => diff.field === field); // 如果有搜索条件,应用搜索过滤 if (currentSearchTerm && currentSearchTerm.trim()) { const term = currentSearchTerm.toLowerCase(); filteredDifferenceResults = baseResults.filter(diff => { const keyStr = JSON.stringify(diff.key).toLowerCase(); if (keyStr.includes(term)) return true; if (diff.field && diff.field.toLowerCase().includes(term)) return true; if (diff.pro_value && String(diff.pro_value).toLowerCase().includes(term)) return true; if (diff.test_value && String(diff.test_value).toLowerCase().includes(term)) return true; if (diff.message && diff.message.toLowerCase().includes(term)) return true; return false; }); } else { filteredDifferenceResults = baseResults; } // 重置页码并重新显示 currentDifferencePage = 1; displayDifferences(); }