完成基本分表查询

This commit is contained in:
2025-08-02 01:23:33 +08:00
parent 689328eeca
commit b7a05e56d0
4 changed files with 1087 additions and 28 deletions

View File

@@ -6,13 +6,93 @@ 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 (推荐使用包含时间戳的字段)';
keysField.value = 'wmid';
// 更新查询Key输入框的占位符
const queryValues = document.getElementById('query_values');
queryValues.placeholder = '请输入查询的Key值一行一个\n分表查询示例包含时间戳\nwmid_1609459200\nwmid_1610064000\nwmid_1610668800';
} else {
// 禁用分表模式
shardingConfig.style.display = 'none';
executeButton.textContent = '执行查询比对';
keyInputHint.textContent = '单表模式输入普通Key值';
keysField.placeholder = 'docid';
keysField.value = 'docid';
// 更新查询Key输入框的占位符
const queryValues = document.getElementById('query_values');
queryValues.placeholder = '请输入查询的Key值一行一个\n单表查询示例\nkey1\nkey2\nkey3';
}
updateTableNameHints();
}
// 更新表名字段的提示文本
function updateTableNameHints() {
const proTableHint = document.getElementById('pro_table_hint');
const testTableHint = document.getElementById('test_table_hint');
const useShardingPro = document.getElementById('use_sharding_for_pro');
const useShardingTest = document.getElementById('use_sharding_for_test');
if (isShardingMode) {
proTableHint.textContent = (useShardingPro && useShardingPro.checked) ?
'基础表名(自动添加索引后缀)' : '完整表名';
testTableHint.textContent = (useShardingTest && useShardingTest.checked) ?
'基础表名(自动添加索引后缀)' : '完整表名';
} else {
proTableHint.textContent = '完整表名';
testTableHint.textContent = '完整表名';
}
}
// 加载配置组列表
async function loadConfigGroups() {
try {
@@ -155,6 +235,9 @@ async function saveConfigGroup() {
const config = getCurrentConfig();
// 获取分表配置
const shardingConfig = getShardingConfig().sharding_config;
try {
const response = await fetch('/api/config-groups', {
method: 'POST',
@@ -164,7 +247,8 @@ async function saveConfigGroup() {
body: JSON.stringify({
name: name,
description: description,
...config
...config,
sharding_config: shardingConfig
})
});
@@ -178,7 +262,7 @@ async function saveConfigGroup() {
// 重新加载配置组列表
await loadConfigGroups();
showAlert('success', result.message);
showAlert('success', result.message + '(包含分表配置)');
} else {
showAlert('danger', result.error || '保存配置组失败');
}
@@ -437,13 +521,30 @@ async function executeQuery() {
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 {
const response = await fetch('/api/query', {
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(config)
body: JSON.stringify(requestConfig)
});
if (!response.ok) {
@@ -455,6 +556,9 @@ async function executeQuery() {
currentResults = results;
displayResults(results);
// 自动刷新查询日志
autoRefreshLogsAfterQuery();
} catch (error) {
showAlert('danger', '查询失败: ' + error.message);
} finally {
@@ -462,11 +566,31 @@ async function executeQuery() {
}
}
// 获取分表查询配置
function getShardingConfig() {
const baseConfig = getCurrentConfig();
return {
...baseConfig,
sharding_config: {
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
}
};
}
// 显示查询结果
function displayResults(results) {
// 显示统计信息
displayStats(results);
// 显示分表查询信息(如果有)
displayShardingInfo(results);
// 更新选项卡计数
document.getElementById('diff-count').textContent = results.differences.length;
document.getElementById('identical-count').textContent = results.identical_results.length;
@@ -487,7 +611,99 @@ function displayResults(results) {
// 显示结果区域
document.getElementById('results').style.display = 'block';
showAlert('success', `查询完成!共处理${results.total_keys}个Key发现${results.differences.length}处差异,${results.identical_results.length}条记录完全相同`);
// 根据查询类型显示不同的成功消息
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 = '<div class="row">';
// 生产环境分表信息
if (shardingInfo.pro_shards) {
html += `
<div class="col-md-6">
<h6 class="text-primary"><i class="fas fa-server"></i> 生产环境分表信息</h6>
<div class="mb-3">
<small class="text-muted">配置:${shardingInfo.pro_shards.interval_seconds}秒间隔,${shardingInfo.pro_shards.table_count}张分表</small>
</div>
<div class="shard-tables-info">
`;
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 += `<span class="${cssClass}">${table}</span>`;
});
}
html += '</div></div>';
}
// 测试环境分表信息
if (shardingInfo.test_shards) {
html += `
<div class="col-md-6">
<h6 class="text-success"><i class="fas fa-flask"></i> 测试环境分表信息</h6>
<div class="mb-3">
<small class="text-muted">配置:${shardingInfo.test_shards.interval_seconds}秒间隔,${shardingInfo.test_shards.table_count}张分表</small>
</div>
<div class="shard-tables-info">
`;
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 += `<span class="${cssClass}">${table}</span>`;
});
}
html += '</div></div>';
}
html += '</div>';
// 添加分表计算统计信息
if (shardingInfo.calculation_stats) {
html += `
<div class="row mt-3">
<div class="col-12">
<h6><i class="fas fa-calculator"></i> 分表计算统计</h6>
<div class="row">
<div class="col-md-3">
<small class="text-muted">处理Key数<strong>${shardingInfo.calculation_stats.total_keys || 0}</strong></small>
</div>
<div class="col-md-3">
<small class="text-muted">成功解析时间戳:<strong>${shardingInfo.calculation_stats.successful_extractions || 0}</strong></small>
</div>
<div class="col-md-3">
<small class="text-muted">计算出分表数:<strong>${shardingInfo.calculation_stats.unique_shards || 0}</strong></small>
</div>
<div class="col-md-3">
<small class="text-muted">解析失败:<strong class="text-danger">${shardingInfo.calculation_stats.failed_extractions || 0}</strong></small>
</div>
</div>
</div>
</div>
`;
}
document.getElementById('shardingInfo').innerHTML = html;
}
// 显示统计信息
@@ -2722,4 +2938,131 @@ function showAlert(type, message) {
alert.remove();
}
}, 5000);
}
// 查询日志相关功能
let allQueryLogs = []; // 存储所有日志
async function refreshQueryLogs() {
try {
const response = await fetch('/api/query-logs');
const result = await response.json();
if (result.success && result.data) {
allQueryLogs = result.data;
filterLogsByLevel();
} else {
document.getElementById('query-logs').innerHTML = '<div class="alert alert-warning">无法获取查询日志</div>';
}
} catch (error) {
console.error('获取查询日志失败:', error);
document.getElementById('query-logs').innerHTML = '<div class="alert alert-danger">获取查询日志失败</div>';
}
}
function filterLogsByLevel() {
const showInfo = document.getElementById('log-level-info').checked;
const showWarning = document.getElementById('log-level-warning').checked;
const showError = document.getElementById('log-level-error').checked;
const filteredLogs = allQueryLogs.filter(log => {
switch(log.level) {
case 'INFO': return showInfo;
case 'WARNING': return showWarning;
case 'ERROR': return showError;
default: return true;
}
});
displayQueryLogs(filteredLogs);
}
async function clearQueryLogs() {
if (!confirm('确定要清空所有查询日志吗?')) {
return;
}
try {
const response = await fetch('/api/query-logs', {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
document.getElementById('query-logs').innerHTML = '<div class="alert alert-info">查询日志已清空</div>';
showAlert('success', '查询日志已清空');
} else {
showAlert('danger', '清空查询日志失败: ' + result.error);
}
} catch (error) {
console.error('清空查询日志失败:', error);
showAlert('danger', '清空查询日志失败');
}
}
function displayQueryLogs(logs) {
const container = document.getElementById('query-logs');
if (!logs || logs.length === 0) {
container.innerHTML = '<div class="alert alert-info">暂无查询日志</div>';
return;
}
const logHtml = logs.map(log => {
const levelClass = {
'INFO': 'text-primary',
'WARNING': 'text-warning',
'ERROR': 'text-danger',
'DEBUG': 'text-secondary'
}[log.level] || 'text-dark';
const levelIcon = {
'INFO': 'fas fa-info-circle',
'WARNING': 'fas fa-exclamation-triangle',
'ERROR': 'fas fa-times-circle',
'DEBUG': 'fas fa-bug'
}[log.level] || 'fas fa-circle';
// 改进SQL高亮显示
let message = escapeHtml(log.message);
// 高亮SQL查询语句
if (message.includes('执行查询SQL:')) {
message = message.replace(/执行查询SQL: (SELECT.*?);/g,
'执行查询SQL: <br><code class="bg-light d-block p-2 text-dark" style="font-size: 0.9em;">$1;</code>');
}
// 高亮重要信息
message = message.replace(/(\d+\.\d{3}秒)/g, '<strong class="text-success">$1</strong>');
message = message.replace(/(返回记录数=\d+)/g, '<strong class="text-info">$1</strong>');
message = message.replace(/(执行时间=[\d.]+秒)/g, '<strong class="text-success">$1</strong>');
return `
<div class="border-bottom py-2 log-entry" data-level="${log.level}">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<span class="${levelClass}">
<i class="${levelIcon}"></i>
<strong>[${log.level}]</strong>
</span>
<div class="ms-4 mt-1">${message}</div>
</div>
<small class="text-muted ms-2 flex-shrink-0" style="min-width: 140px;">${log.timestamp}</small>
</div>
</div>
`;
}).join('');
container.innerHTML = logHtml;
// 自动滚动到底部
container.scrollTop = container.scrollHeight;
}
// 在查询执行后自动刷新日志
function autoRefreshLogsAfterQuery() {
// 延迟一下确保后端日志已经记录
setTimeout(() => {
refreshQueryLogs();
}, 500);
}