// 全局变量
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) {
// 显示搜索和控制界面,即使没有结果
let html = `
${currentSearchValue ? `没有找到包含"${escapeHtml(currentSearchValue)}"的差异记录` : '未发现差异'}
${currentSearchValue ? '
请尝试其他搜索条件或点击"清除"按钮查看所有结果' : ''}
`;
differencesContainer.innerHTML = html;
// 恢复搜索框的焦点和光标位置
if (currentSearchValue) {
const searchInput = document.getElementById('differenceSearch');
if (searchInput) {
searchInput.focus();
searchInput.setSelectionRange(searchInput.value.length, searchInput.value.length);
}
}
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('')}
${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 += `
`;
// 显示该主键下的所有差异字段
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 ? '数组' : ''}
`;
}
});
html += `
`;
});
// 底部分页
if (totalPages > 1) {
html += `
${generateDifferencePagination(currentDifferencePage, totalPages)}
`;
}
differencesContainer.innerHTML = html;
// 初始化全部展开/收起按钮的状态
const toggleButton = document.getElementById('toggleAllText');
if (toggleButton) {
// 由于默认展开,按钮文本应该是"全部收起"
toggleButton.innerHTML = '全部收起';
}
// 恢复搜索框的焦点和光标位置
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) {
// 显示搜索和控制界面,即使没有结果
let html = `
${(currentIdenticalSearchTerm || currentSearchValue) ? `没有找到包含"${escapeHtml(currentIdenticalSearchTerm || currentSearchValue)}"的相同记录` : '没有完全相同的记录'}
${(currentIdenticalSearchTerm || currentSearchValue) ? '
请尝试其他搜索条件或点击"清除"按钮查看所有结果' : ''}
`;
identicalContainer.innerHTML = html;
// 恢复搜索框的焦点和光标位置
if (currentSearchValue) {
const searchInput = document.getElementById('identicalSearch');
if (searchInput) {
searchInput.focus();
searchInput.setSelectionRange(searchInput.value.length, searchInput.value.length);
}
}
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;
// 恢复搜索框的焦点和光标位置
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 = `
-
-
-
-
-
同步滚动已启用
生产环境原始数据
${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(() => {
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 copyDifferenceKeys() {
if (!filteredDifferenceResults || filteredDifferenceResults.length === 0) {
showAlert('warning', '无差异数据可复制');
return;
}
try {
// 收集所有差异记录的主键
const differenceKeys = [];
const uniqueKeys = new Set();
filteredDifferenceResults.forEach(diff => {
if (diff.key) {
let keyText = '';
if (typeof diff.key === 'object' && !Array.isArray(diff.key)) {
// 复合主键:转换为逗号分隔格式
keyText = Object.values(diff.key).join(',');
} else {
// 单主键或其他格式
keyText = String(diff.key);
}
// 避免重复主键
if (!uniqueKeys.has(keyText)) {
uniqueKeys.add(keyText);
differenceKeys.push(keyText);
}
}
});
if (differenceKeys.length === 0) {
showAlert('warning', '未找到有效的主键数据');
return;
}
// 将主键列表转换为文本格式(每行一个)
const keyText = differenceKeys.join('\n');
// 复制到剪贴板
navigator.clipboard.writeText(keyText).then(() => {
showAlert('success', `已复制 ${differenceKeys.length} 个差异主键到剪贴板`);
}).catch(err => {
console.error('复制失败:', err);
showAlert('danger', '复制失败,请手动选择复制');
});
} catch (error) {
console.error('处理差异主键失败:', error);
showAlert('danger', '处理差异主键失败: ' + error.message);
}
}
// 切换所有差异详情的展开/收起状态
function toggleAllDifferenceCollapse() {
// 获取当前页面的所有差异展开区域
const collapseElements = document.querySelectorAll('#differences [id^="keyGroup"]');
const toggleButton = document.getElementById('toggleAllText');
if (collapseElements.length === 0) {
showAlert('warning', '没有找到差异详情');
return;
}
// 检查当前状态 - 如果大部分是展开的,就全部收起;否则全部展开
let expandedCount = 0;
collapseElements.forEach(element => {
if (element.classList.contains('show')) {
expandedCount++;
}
});
const shouldExpand = expandedCount < collapseElements.length / 2;
collapseElements.forEach(element => {
if (shouldExpand) {
// 展开
element.classList.add('show');
} else {
// 收起
element.classList.remove('show');
}
});
// 更新按钮文本
if (shouldExpand) {
toggleButton.innerHTML = '全部收起';
showAlert('success', `已展开 ${collapseElements.length} 个差异详情`);
} else {
toggleButton.innerHTML = '全部展开';
showAlert('success', `已收起 ${collapseElements.length} 个差异详情`);
}
}
// 显示差异数据的原生数据
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 += `
${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 (!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}%
${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 {
// 添加批量操作控制栏
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 = `
名称: ${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 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 = `
当前查询统计:
• 查询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();
// 确定查询类型和获取相应配置
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 = `
请粘贴配置数据,支持以下格式:
示例格式:
clusterName: "Example Cluster"
clusterNodes: "127.0.0.1,127.0.0.2"
port: 9042
datacenter: "dc1"
username: "cassandra"
password: "example_password"
keyspace: "example_keyspace"
table: "example_table"
`;
// 移除现有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() {
// 这个函数现在不再使用,因为查询日志已经独立到模态框中
// 如果需要刷新查询日志,请使用 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 += `
`;
// 显示过滤后的日志条目
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 += `
`;
// 显示该批次的日志条目
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 += `
`;
});
// 底部分页
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();
}