完善查询
This commit is contained in:
27
app.py
27
app.py
@@ -906,9 +906,11 @@ def execute_mixed_query(pro_session, test_session, pro_config, test_config, keys
|
||||
|
||||
# 处理生产环境查询
|
||||
if sharding_config.get('use_sharding_for_pro', False):
|
||||
# 获取生产环境分表配置参数,优先使用专用参数,否则使用通用参数
|
||||
pro_interval = sharding_config.get('pro_interval_seconds') or sharding_config.get('interval_seconds', 604800)
|
||||
pro_table_count = sharding_config.get('pro_table_count') or sharding_config.get('table_count', 14)
|
||||
|
||||
# 记录生产环境分表配置信息
|
||||
pro_interval = sharding_config.get('pro_interval_seconds', 604800)
|
||||
pro_table_count = sharding_config.get('pro_table_count', 14)
|
||||
logger.info(f"=== 生产环境分表配置 ===")
|
||||
logger.info(f"启用分表查询: True")
|
||||
logger.info(f"时间间隔: {pro_interval}秒 ({pro_interval//86400}天)")
|
||||
@@ -954,9 +956,11 @@ def execute_mixed_query(pro_session, test_session, pro_config, test_config, keys
|
||||
|
||||
# 处理测试环境查询
|
||||
if sharding_config.get('use_sharding_for_test', False):
|
||||
# 获取测试环境分表配置参数,优先使用专用参数,否则使用通用参数
|
||||
test_interval = sharding_config.get('test_interval_seconds') or sharding_config.get('interval_seconds', 604800)
|
||||
test_table_count = sharding_config.get('test_table_count') or sharding_config.get('table_count', 14)
|
||||
|
||||
# 记录测试环境分表配置信息
|
||||
test_interval = sharding_config.get('test_interval_seconds', 604800)
|
||||
test_table_count = sharding_config.get('test_table_count', 14)
|
||||
logger.info(f"=== 测试环境分表配置 ===")
|
||||
logger.info(f"启用分表查询: True")
|
||||
logger.info(f"时间间隔: {test_interval}秒 ({test_interval//86400}天)")
|
||||
@@ -980,8 +984,8 @@ def execute_mixed_query(pro_session, test_session, pro_config, test_config, keys
|
||||
results['test_data'] = test_data
|
||||
results['sharding_info']['test_shards'] = {
|
||||
'enabled': True,
|
||||
'interval_seconds': sharding_config.get('test_interval_seconds', 604800),
|
||||
'table_count': sharding_config.get('test_table_count', 14),
|
||||
'interval_seconds': test_interval,
|
||||
'table_count': test_table_count,
|
||||
'queried_tables': test_queried_tables,
|
||||
'error_tables': test_error_tables,
|
||||
'failed_keys': test_failed_keys
|
||||
@@ -1214,7 +1218,16 @@ def sharding_query_compare():
|
||||
logger.info(f" values数量: {len(values)}")
|
||||
logger.info(f" fields_to_compare: {fields_to_compare}")
|
||||
logger.info(f" exclude_fields: {exclude_fields}")
|
||||
logger.info(f" sharding_config: {sharding_config}")
|
||||
logger.info(f" sharding_config原始数据: {sharding_config}")
|
||||
logger.info(f" sharding_config具体参数:")
|
||||
logger.info(f" use_sharding_for_pro: {sharding_config.get('use_sharding_for_pro')}")
|
||||
logger.info(f" use_sharding_for_test: {sharding_config.get('use_sharding_for_test')}")
|
||||
logger.info(f" pro_interval_seconds: {sharding_config.get('pro_interval_seconds')}")
|
||||
logger.info(f" pro_table_count: {sharding_config.get('pro_table_count')}")
|
||||
logger.info(f" test_interval_seconds: {sharding_config.get('test_interval_seconds')}")
|
||||
logger.info(f" test_table_count: {sharding_config.get('test_table_count')}")
|
||||
logger.info(f" interval_seconds: {sharding_config.get('interval_seconds')}")
|
||||
logger.info(f" table_count: {sharding_config.get('table_count')}")
|
||||
|
||||
logger.info(f"分表查询配置:{len(values)}个key值,生产表:{pro_config['table']},测试表:{test_config['table']}")
|
||||
|
||||
|
362
static/js/app.js
362
static/js/app.js
@@ -718,6 +718,13 @@ function getShardingConfig() {
|
||||
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
|
||||
}
|
||||
@@ -736,6 +743,11 @@ function displayResults(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;
|
||||
@@ -747,6 +759,7 @@ function displayResults(results) {
|
||||
// 显示各个面板内容
|
||||
displayDifferences();
|
||||
displayIdenticalResults();
|
||||
displayRawData(results);
|
||||
displayComparisonSummary(results.summary);
|
||||
|
||||
// 显示结果区域
|
||||
@@ -3202,4 +3215,353 @@ function autoRefreshLogsAfterQuery() {
|
||||
setTimeout(() => {
|
||||
refreshQueryLogs();
|
||||
}, 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 = '<div class="alert alert-info">没有原始数据可显示</div>';
|
||||
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 = `
|
||||
<!-- 分页控制 -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center">
|
||||
<label class="form-label me-2 mb-0">每页显示:</label>
|
||||
<select class="form-select form-select-sm me-2" style="width: auto;" onchange="changeRawDataPageSize(this.value)">
|
||||
<option value="10" ${rawDataPageSize == 10 ? 'selected' : ''}>10条</option>
|
||||
<option value="20" ${rawDataPageSize == 20 ? 'selected' : ''}>20条</option>
|
||||
<option value="50" ${rawDataPageSize == 50 ? 'selected' : ''}>50条</option>
|
||||
<option value="100" ${rawDataPageSize == 100 ? 'selected' : ''}>100条</option>
|
||||
</select>
|
||||
<span class="ms-2 text-muted">共 ${filteredRawData.length} 条记录</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex justify-content-end align-items-center">
|
||||
${generateRawDataPagination(currentRawDataPage, totalPages)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 显示数据记录
|
||||
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) {
|
||||
keyValue = keyValues.join(', ');
|
||||
}
|
||||
} else {
|
||||
// 如果没有配置主键,尝试常见的主键字段名
|
||||
const commonKeyFields = ['id', 'statusid', 'key', 'uuid'];
|
||||
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 += `
|
||||
<div class="card mb-3 border-${item.environment === 'production' ? 'primary' : 'success'}">
|
||||
<div class="card-header bg-light">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<strong>记录 #${globalIndex}</strong>
|
||||
<span class="badge ${envBadgeClass} ms-2">${item.displayName}</span>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-sm btn-outline-secondary me-2" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#rawCollapse${globalIndex}">
|
||||
<i class="fas fa-eye"></i> 查看详情
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-info" onclick='copyRawRecord(${JSON.stringify(JSON.stringify(item.data))})'>
|
||||
<i class="fas fa-copy"></i> 复制
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-0 mt-2"><strong>主键:</strong> ${keyValue}</p>
|
||||
</div>
|
||||
<div class="collapse" id="rawCollapse${globalIndex}">
|
||||
<div class="card-body">
|
||||
<pre class="bg-light p-3 rounded" style="max-height: 500px; overflow-y: auto; font-size: 0.9em; line-height: 1.4;">${escapeHtml(jsonData)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// 底部分页
|
||||
if (totalPages > 1) {
|
||||
html += `
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-center">
|
||||
${generateRawDataPagination(currentRawDataPage, totalPages)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// 生成原始数据分页导航
|
||||
function generateRawDataPagination(currentPage, totalPages) {
|
||||
if (totalPages <= 1) return '';
|
||||
|
||||
let html = '<nav><ul class="pagination pagination-sm mb-0">';
|
||||
|
||||
// 上一页
|
||||
html += `
|
||||
<li class="page-item ${currentPage === 1 ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="goToRawDataPage(${currentPage - 1})">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
// 页码
|
||||
const startPage = Math.max(1, currentPage - 2);
|
||||
const endPage = Math.min(totalPages, currentPage + 2);
|
||||
|
||||
if (startPage > 1) {
|
||||
html += '<li class="page-item"><a class="page-link" href="#" onclick="goToRawDataPage(1)">1</a></li>';
|
||||
if (startPage > 2) {
|
||||
html += '<li class="page-item disabled"><span class="page-link">...</span></li>';
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
html += `
|
||||
<li class="page-item ${i === currentPage ? 'active' : ''}">
|
||||
<a class="page-link" href="#" onclick="goToRawDataPage(${i})">${i}</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
if (endPage < totalPages) {
|
||||
if (endPage < totalPages - 1) {
|
||||
html += '<li class="page-item disabled"><span class="page-link">...</span></li>';
|
||||
}
|
||||
html += `<li class="page-item"><a class="page-link" href="#" onclick="goToRawDataPage(${totalPages})">${totalPages}</a></li>`;
|
||||
}
|
||||
|
||||
// 下一页
|
||||
html += `
|
||||
<li class="page-item ${currentPage === totalPages ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="goToRawDataPage(${currentPage + 1})">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
html += '</ul></nav>';
|
||||
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);
|
||||
}
|
||||
}
|
@@ -605,6 +605,11 @@
|
||||
<i class="fas fa-check-circle"></i> 相同结果 <span class="badge bg-success ms-1" id="identical-count">0</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="rawdata-tab" data-bs-toggle="tab" data-bs-target="#rawdata-panel" type="button" role="tab">
|
||||
<i class="fas fa-database"></i> 原始数据 <span class="badge bg-info ms-1" id="rawdata-count">0</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="summary-tab" data-bs-toggle="tab" data-bs-target="#summary-panel" type="button" role="tab">
|
||||
<i class="fas fa-chart-pie"></i> 比较总结
|
||||
@@ -639,6 +644,40 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 原始数据面板 -->
|
||||
<div class="tab-pane fade" id="rawdata-panel" role="tabpanel">
|
||||
<div class="container-fluid">
|
||||
<!-- 筛选控制区 -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center">
|
||||
<label class="form-label me-2 mb-0">显示环境:</label>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showProData" checked onchange="filterRawData()">
|
||||
<label class="form-check-label" for="showProData">生产环境</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="showTestData" checked onchange="filterRawData()">
|
||||
<label class="form-check-label" for="showTestData">测试环境</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||
<input type="text" class="form-control" placeholder="搜索Key或字段值..."
|
||||
onkeyup="searchRawData(this.value)" id="rawDataSearch">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 原始数据内容 -->
|
||||
<div id="raw-data-content">
|
||||
<!-- 原始数据将在这里动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 比较总结面板 -->
|
||||
<div class="tab-pane fade" id="summary-panel" role="tabpanel">
|
||||
<div id="comparison-summary">
|
||||
|
Reference in New Issue
Block a user