// 全局变量
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 = ' (推荐使用包含时间戳的字段)';
keysField.value = '';
// 更新查询Key输入框的占位符
const queryValues = document.getElementById('query_values');
queryValues.placeholder = '请输入查询的Key值,一行一个\n分表查询示例(包含时间戳):\nwmid_1609459200\nwmid_1610064000\nwmid_1610668800';
} else {
// 禁用分表模式
shardingConfig.style.display = 'none';
executeButton.textContent = '执行查询比对';
keyInputHint.textContent = '单表模式:输入普通Key值';
keysField.placeholder = '';
keysField.value = '';
// 更新查询Key输入框的占位符
const queryValues = document.getElementById('query_values');
queryValues.placeholder = '请输入查询的Key值,一行一个\n单表查询示例:\nkey1\nkey2\nkey3';
}
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,
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;
// 初始化相同结果分页数据
filteredIdenticalResults = results.identical_results;
currentIdenticalPage = 1;
// 初始化差异结果分页数据
filteredDifferenceResults = results.differences;
currentDifferencePage = 1;
// 显示各个面板内容
displayDifferences();
displayIdenticalResults();
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');
if (!filteredDifferenceResults.length) {
differencesContainer.innerHTML = ' 未发现差异
';
return;
}
// 计算分页
const totalPages = Math.ceil(filteredDifferenceResults.length / differencePageSize);
const startIndex = (currentDifferencePage - 1) * differencePageSize;
const endIndex = startIndex + differencePageSize;
const currentPageData = filteredDifferenceResults.slice(startIndex, endIndex);
let html = `
${generateDifferencePagination(currentDifferencePage, totalPages)}
`;
// 显示当前页数据
currentPageData.forEach((diff, index) => {
const globalIndex = startIndex + index + 1;
if (diff.message) {
// 记录不存在的情况
html += `
`;
} else {
// 字段值差异的情况
const isJson = diff.is_json;
const isArray = diff.is_array;
const jsonClass = isJson ? 'json-field' : '';
html += `
${escapeHtml(diff.pro_value)}
${escapeHtml(diff.test_value)}
`;
}
});
// 底部分页
if (totalPages > 1) {
html += `
${generateDifferencePagination(currentDifferencePage, totalPages)}
`;
}
differencesContainer.innerHTML = html;
}
// HTML转义函数,防止XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 显示相同结果
function displayIdenticalResults() {
const identicalContainer = document.getElementById('identical-results');
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 = `
${generatePagination(currentIdenticalPage, totalPages)}
`;
// 显示当前页数据
currentPageData.forEach((result, index) => {
const globalIndex = startIndex + index + 1;
html += `
`;
// 显示字段对比,左右布局
const proFields = result.pro_fields || {};
const testFields = result.test_fields || {};
const allFields = new Set([...Object.keys(proFields), ...Object.keys(testFields)]);
html += '
';
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 += `
${escapeHtml(String(proValue))}
${escapeHtml(String(testValue))}
`;
});
html += `
`;
});
// 底部分页
if (totalPages > 1) {
html += `
${generatePagination(currentIdenticalPage, totalPages)}
`;
}
identicalContainer.innerHTML = html;
}
// 生成分页导航
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);
}
}
// 搜索相同结果
function searchIdenticalResults(searchTerm) {
if (!currentResults) return;
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 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);
}
}
// 搜索差异结果
function searchDifferenceResults(searchTerm) {
if (!currentResults) return;
if (!searchTerm.trim()) {
filteredDifferenceResults = currentResults.differences;
} else {
const term = searchTerm.toLowerCase();
filteredDifferenceResults = currentResults.differences.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 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);
// 在原生数据中查找对应的记录
let proData = null;
let testData = null;
// 查找生产环境数据
if (currentResults.raw_pro_data) {
proData = currentResults.raw_pro_data.find(item => {
// 比较主键
return JSON.stringify(item[Object.keys(key)[0]]) === JSON.stringify(key[Object.keys(key)[0]]);
});
}
// 查找测试环境数据
if (currentResults.raw_test_data) {
testData = currentResults.raw_test_data.find(item => {
// 比较主键
return JSON.stringify(item[Object.keys(key)[0]]) === JSON.stringify(key[Object.keys(key)[0]]);
});
}
// 创建模态框内容
const modalContent = `
-
-
-
-
-
同步滚动已启用
生产环境原始数据
${proData ? escapeHtml(JSON.stringify(proData)) : '无数据'}
测试环境原始数据
${testData ? escapeHtml(JSON.stringify(testData)) : '无数据'}
`;
// 移除现有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(() => {
renderDiffView(proData, testData);
renderTreeView('proTreeView', proData);
renderTreeView('testTreeView', testData);
// 为格式化视图添加同步滚动
setupSyncScroll('formatted');
// 为树形视图添加同步滚动
setupSyncScroll('tree');
}, 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 (!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 += `
${field}
${getFieldTypeIcon(proValue || testValue)}
|
${proDisplay} |
${testDisplay} |
不同
|
`;
});
}
// 再显示相同字段
if (identicalFields.length > 0) {
html += '相同字段 |
';
identicalFields.forEach(field => {
const value = proData ? proData[field] : testData[field];
const displayValue = formatValue(value);
html += `
${field}
${getFieldTypeIcon(value)}
|
${displayValue} |
${displayValue} |
相同
|
`;
});
}
html += '
';
// 添加快速跳转按钮
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 (!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}%
${summary.field_analysis.most_different_fields.length > 0 ?
summary.field_analysis.most_different_fields.map(([field, count]) => `
${field}
${count}次
`).join('') :
'
无字段差异统计
'
}
${summary.recommendations.map(recommendation => `
-
${recommendation}
`).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 += `
${field} |
${count} |
${percentage}% |
`;
});
html += '
';
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 {
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;
historyList += `
${history.name}
${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 || '';
// 填充查询配置
document.getElementById('keys').value = (history.query_config.keys || []).join(',');
document.getElementById('fields_to_compare').value = (history.query_config.fields_to_compare || []).join(',');
document.getElementById('exclude_fields').value = (history.query_config.exclude_fields || []).join(',');
// 填充查询Key值
document.getElementById('query_values').value = (history.query_keys || []).join('\n');
// 关闭历史记录modal
const modal = bootstrap.Modal.getInstance(document.getElementById('queryHistoryModal'));
modal.hide();
showAlert('success', `历史记录 "${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 = `
名称: ${history.name}
描述: ${history.description || '无描述'}
创建时间: ${new Date(history.created_at).toLocaleString()}
执行时间: ${history.execution_time.toFixed(2)}秒
查询Key数: ${history.total_keys}
相同记录: ${history.identical_count}
差异记录: ${history.differences_count}
一致性: ${history.total_keys > 0 ? Math.round((history.identical_count / history.total_keys) * 100) : 0}%
主键字段: ${(history.query_config.keys || []).join(', ')}
比较字段: ${(history.query_config.fields_to_compare || []).join(', ') || '全部字段'}
排除字段: ${(history.query_config.exclude_fields || []).join(', ') || '无'}
查询Key值:
${(history.query_keys || []).join('\n')}
${summary ? `
数据质量评估
${summary.data_quality?.quality_level?.level || 'N/A'}
${summary.data_quality?.quality_level?.description || ''}
一致性评分
${summary.data_quality?.consistency_score || 0}%
${summary.recommendations?.length > 0 ? `
改进建议
${summary.recommendations.map(rec => `
-
${rec}
`).join('')}
` : ''}
` : ''}
`;
// 移除现有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 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 = `
当前查询统计:
• 查询Key数: ${currentResults.total_keys}
• 相同记录: ${currentResults.identical_results.length}
• 差异记录: ${currentResults.differences.length}
`;
// 移除现有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();
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
})
});
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 = `
请粘贴配置数据,支持以下格式:
示例格式:
clusterName: "Hot Cluster"
clusterNodes: "10.20.2.22,10.20.2.23"
port: 9042
datacenter: "cs01"
username: "cbase"
password: "antducbaseadmin@2022"
keyspace: "yuqing_skinny"
table: "status_test"
`;
// 移除现有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 = `
${message}
`;
// 插入到页面顶部
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() {
try {
const response = await fetch('/api/query-logs');
const result = await response.json();
if (result.success && result.data) {
allQueryLogs = result.data;
filterLogsByLevel();
} else {
document.getElementById('query-logs').innerHTML = '无法获取查询日志
';
}
} catch (error) {
console.error('获取查询日志失败:', error);
document.getElementById('query-logs').innerHTML = '获取查询日志失败
';
}
}
function filterLogsByLevel() {
const showInfo = document.getElementById('log-level-info').checked;
const showWarning = document.getElementById('log-level-warning').checked;
const showError = document.getElementById('log-level-error').checked;
const filteredLogs = allQueryLogs.filter(log => {
switch(log.level) {
case 'INFO': return showInfo;
case 'WARNING': return showWarning;
case 'ERROR': return showError;
default: return true;
}
});
displayQueryLogs(filteredLogs);
}
async function clearQueryLogs() {
if (!confirm('确定要清空所有查询日志吗?')) {
return;
}
try {
const response = await fetch('/api/query-logs', {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
document.getElementById('query-logs').innerHTML = '查询日志已清空
';
showAlert('success', '查询日志已清空');
} else {
showAlert('danger', '清空查询日志失败: ' + result.error);
}
} catch (error) {
console.error('清空查询日志失败:', error);
showAlert('danger', '清空查询日志失败');
}
}
function displayQueryLogs(logs) {
const container = document.getElementById('query-logs');
if (!logs || logs.length === 0) {
container.innerHTML = '暂无查询日志
';
return;
}
const logHtml = logs.map(log => {
const levelClass = {
'INFO': 'text-primary',
'WARNING': 'text-warning',
'ERROR': 'text-danger',
'DEBUG': 'text-secondary'
}[log.level] || 'text-dark';
const levelIcon = {
'INFO': 'fas fa-info-circle',
'WARNING': 'fas fa-exclamation-triangle',
'ERROR': 'fas fa-times-circle',
'DEBUG': 'fas fa-bug'
}[log.level] || 'fas fa-circle';
// 改进SQL高亮显示
let message = escapeHtml(log.message);
// 高亮SQL查询语句
if (message.includes('执行查询SQL:')) {
message = message.replace(/执行查询SQL: (SELECT.*?);/g,
'执行查询SQL:
$1;
');
}
// 高亮重要信息
message = message.replace(/(\d+\.\d{3}秒)/g, '$1');
message = message.replace(/(返回记录数=\d+)/g, '$1');
message = message.replace(/(执行时间=[\d.]+秒)/g, '$1');
return `
[${log.level}]
${message}
${log.timestamp}
`;
}).join('');
container.innerHTML = logHtml;
// 自动滚动到底部
container.scrollTop = container.scrollHeight;
}
// 在查询执行后自动刷新日志
function autoRefreshLogsAfterQuery() {
// 延迟一下确保后端日志已经记录
setTimeout(() => {
refreshQueryLogs();
}, 500);
}