diff --git a/app.py b/app.py index cc0c56a..f62e2b8 100644 --- a/app.py +++ b/app.py @@ -73,12 +73,12 @@ def ensure_database(): try: conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() - cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='config_groups'") - result = cursor.fetchone() - conn.close() + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('config_groups', 'query_history')") + results = cursor.fetchall() + existing_tables = [row[0] for row in results] - if not result: - logger.info("config_groups表不存在,正在创建...") + if 'config_groups' not in existing_tables or 'query_history' not in existing_tables: + logger.info("数据库表不完整,正在重新创建...") return init_database() return True @@ -417,6 +417,141 @@ def delete_config_group(group_id): finally: conn.close() +def save_query_history(name, description, pro_config, test_config, query_config, query_keys, + results_summary, execution_time, total_keys, differences_count, identical_count): + """保存查询历史记录""" + if not ensure_database(): + logger.error("数据库初始化失败") + return False + + conn = get_db_connection() + cursor = conn.cursor() + + try: + cursor.execute(''' + INSERT INTO query_history + (name, description, pro_config, test_config, query_config, query_keys, + results_summary, execution_time, total_keys, differences_count, identical_count) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + name, description, + json.dumps(pro_config), + json.dumps(test_config), + json.dumps(query_config), + json.dumps(query_keys), + json.dumps(results_summary), + execution_time, + total_keys, + differences_count, + identical_count + )) + conn.commit() + logger.info(f"查询历史记录 '{name}' 保存成功") + return True + except Exception as e: + logger.error(f"保存查询历史记录失败: {e}") + return False + finally: + conn.close() + +def get_query_history(): + """获取所有查询历史记录""" + if not ensure_database(): + logger.error("数据库初始化失败") + return [] + + conn = get_db_connection() + cursor = conn.cursor() + + try: + cursor.execute(''' + SELECT id, name, description, execution_time, total_keys, + differences_count, identical_count, created_at + FROM query_history + ORDER BY created_at DESC + ''') + rows = cursor.fetchall() + + history_list = [] + for row in rows: + history_list.append({ + 'id': row['id'], + 'name': row['name'], + 'description': row['description'], + 'execution_time': row['execution_time'], + 'total_keys': row['total_keys'], + 'differences_count': row['differences_count'], + 'identical_count': row['identical_count'], + 'created_at': row['created_at'] + }) + + return history_list + except Exception as e: + logger.error(f"获取查询历史记录失败: {e}") + return [] + finally: + conn.close() + +def get_query_history_by_id(history_id): + """根据ID获取查询历史记录详情""" + if not ensure_database(): + logger.error("数据库初始化失败") + return None + + conn = get_db_connection() + cursor = conn.cursor() + + try: + cursor.execute(''' + SELECT * FROM query_history WHERE id = ? + ''', (history_id,)) + row = cursor.fetchone() + + if row: + return { + 'id': row['id'], + 'name': row['name'], + 'description': row['description'], + 'pro_config': json.loads(row['pro_config']), + 'test_config': json.loads(row['test_config']), + 'query_config': json.loads(row['query_config']), + 'query_keys': json.loads(row['query_keys']), + 'results_summary': json.loads(row['results_summary']), + 'execution_time': row['execution_time'], + 'total_keys': row['total_keys'], + 'differences_count': row['differences_count'], + 'identical_count': row['identical_count'], + 'created_at': row['created_at'] + } + return None + except Exception as e: + logger.error(f"获取查询历史记录详情失败: {e}") + return None + finally: + conn.close() + +def delete_query_history(history_id): + """删除查询历史记录""" + if not ensure_database(): + logger.error("数据库初始化失败") + return False + + conn = get_db_connection() + cursor = conn.cursor() + + try: + cursor.execute('DELETE FROM query_history WHERE id = ?', (history_id,)) + conn.commit() + success = cursor.rowcount > 0 + if success: + logger.info(f"查询历史记录ID {history_id} 删除成功") + return success + except Exception as e: + logger.error(f"删除查询历史记录失败: {e}") + return False + finally: + conn.close() + def create_connection(config): """创建Cassandra连接""" try: @@ -684,6 +819,35 @@ def query_compare(): } logger.info(f"比对完成:发现 {len(differences)} 处差异") + + # 自动保存查询历史记录(可选,基于执行结果) + try: + # 生成历史记录名称 + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + history_name = f"查询_{timestamp}" + history_description = f"自动保存 - 查询{len(values)}个Key,发现{len(differences)}处差异" + + # 保存历史记录 + save_query_history( + name=history_name, + description=history_description, + pro_config=pro_config, + test_config=test_config, + query_config={ + 'keys': keys, + 'fields_to_compare': fields_to_compare, + 'exclude_fields': exclude_fields + }, + query_keys=values, + results_summary=summary, + execution_time=0.0, # 可以后续优化计算实际执行时间 + total_keys=len(values), + differences_count=len(differences), + identical_count=len(identical_results) + ) + except Exception as e: + logger.warning(f"保存查询历史记录失败: {e}") + return jsonify(result) except Exception as e: @@ -769,5 +933,67 @@ def api_init_database(): else: return jsonify({'success': False, 'error': '数据库初始化失败'}), 500 +# 查询历史管理API +@app.route('/api/query-history', methods=['GET']) +def api_get_query_history(): + """获取所有查询历史记录""" + history_list = get_query_history() + return jsonify({'success': True, 'data': history_list}) + +@app.route('/api/query-history', methods=['POST']) +def api_save_query_history(): + """保存查询历史记录""" + try: + data = request.json + name = data.get('name', '').strip() + description = data.get('description', '').strip() + pro_config = data.get('pro_config', {}) + test_config = data.get('test_config', {}) + query_config = data.get('query_config', {}) + query_keys = data.get('query_keys', []) + results_summary = data.get('results_summary', {}) + execution_time = data.get('execution_time', 0.0) + total_keys = data.get('total_keys', 0) + differences_count = data.get('differences_count', 0) + identical_count = data.get('identical_count', 0) + + if not name: + return jsonify({'success': False, 'error': '历史记录名称不能为空'}), 400 + + success = save_query_history( + name, description, pro_config, test_config, query_config, + query_keys, results_summary, execution_time, total_keys, + differences_count, identical_count + ) + + if success: + return jsonify({'success': True, 'message': '查询历史记录保存成功'}) + else: + return jsonify({'success': False, 'error': '查询历史记录保存失败'}), 500 + + except Exception as e: + logger.error(f"保存查询历史记录API失败: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + +@app.route('/api/query-history/', methods=['GET']) +def api_get_query_history_detail(history_id): + """获取指定查询历史记录详情""" + history_record = get_query_history_by_id(history_id) + + if history_record: + return jsonify({'success': True, 'data': history_record}) + else: + return jsonify({'success': False, 'error': '查询历史记录不存在'}), 404 + +@app.route('/api/query-history/', methods=['DELETE']) +def api_delete_query_history(history_id): + """删除查询历史记录""" + success = delete_query_history(history_id) + + if success: + return jsonify({'success': True, 'message': '查询历史记录删除成功'}) + else: + return jsonify({'success': False, 'error': '查询历史记录删除失败'}), 500 + if __name__ == '__main__': - app.run(debug=True, port=5001) + app.run(debug=True, port=5000) diff --git a/static/js/app.js b/static/js/app.js index 72b0f42..187fe92 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -493,26 +493,38 @@ function displayResults(results) { // 显示统计信息 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)}%

一致性比例

@@ -543,13 +555,18 @@ function displayDifferences() {
- + + - 共 ${filteredDifferenceResults.length} 条差异记录 + + 共 ${filteredDifferenceResults.length} 条差异记录
@@ -632,38 +649,15 @@ function displayDifferences() { ${diff.field}
- -
-
-
- 生产环境 -
-
-
-
-
${escapeHtml(diff.pro_value)}
- -
-
-
- - +
-
-
- 测试环境 -
-
-
+
-
${escapeHtml(diff.test_value)}
+
${escapeHtml(diff.pro_value)}
+${escapeHtml(diff.test_value)}
@@ -721,13 +715,18 @@ function displayIdenticalResults() {
- + + - 共 ${filteredIdenticalResults.length} 条记录 + + 共 ${filteredIdenticalResults.length} 条记录
@@ -811,38 +810,15 @@ function displayIdenticalResults() { ${isJson ? 'JSON' : ''}
- +
-
-
- 生产环境 -
-
-
+
-
${escapeHtml(String(proValue))}
+
${escapeHtml(String(proValue))}
+${escapeHtml(String(testValue))}
-
-
-
- - -
-
-
- 测试环境 -
-
-
-
-
${escapeHtml(String(testValue))}
-
@@ -940,11 +916,53 @@ function goToIdenticalPage(page) { // 改变每页显示数量 function changePageSize(newSize) { + if (newSize === 'custom') { + // 显示自定义输入框 + const customInput = document.getElementById('customPageSize'); + const select = document.querySelector('select[onchange="changePageSize(this.value)"]'); + customInput.style.display = 'inline-block'; + customInput.focus(); + return; + } + identicalPageSize = parseInt(newSize); currentIdenticalPage = 1; displayIdenticalResults(); } +// 设置自定义页面大小 +function setCustomPageSize(size) { + const pageSize = parseInt(size); + if (pageSize && pageSize > 0 && pageSize <= 1000) { + identicalPageSize = pageSize; + currentIdenticalPage = 1; + + // 隐藏输入框,更新下拉框显示 + const customInput = document.getElementById('customPageSize'); + const select = document.querySelector('select[onchange="changePageSize(this.value)"]'); + customInput.style.display = 'none'; + + // 如果不是预设值,保持custom选中状态 + const presetValues = [5, 10, 20, 50, 100]; + if (!presetValues.includes(pageSize)) { + select.value = 'custom'; + } else { + select.value = pageSize.toString(); + } + + displayIdenticalResults(); + } else { + showAlert('warning', '请输入1-1000之间的有效数字'); + } +} + +// 处理自定义页面大小输入框回车事件 +function handleCustomPageSizeEnter(event) { + if (event.key === 'Enter') { + setCustomPageSize(event.target.value); + } +} + // 搜索相同结果 function searchIdenticalResults(searchTerm) { if (!currentResults) return; @@ -1038,11 +1056,53 @@ function goToDifferencePage(page) { // 改变差异每页显示数量 function changeDifferencePageSize(newSize) { + if (newSize === 'custom_diff') { + // 显示自定义输入框 + const customInput = document.getElementById('customDiffPageSize'); + const select = document.querySelector('select[onchange="changeDifferencePageSize(this.value)"]'); + customInput.style.display = 'inline-block'; + customInput.focus(); + return; + } + differencePageSize = parseInt(newSize); currentDifferencePage = 1; displayDifferences(); } +// 设置自定义差异页面大小 +function setCustomDifferencePageSize(size) { + const pageSize = parseInt(size); + if (pageSize && pageSize > 0 && pageSize <= 1000) { + differencePageSize = pageSize; + currentDifferencePage = 1; + + // 隐藏输入框,更新下拉框显示 + const customInput = document.getElementById('customDiffPageSize'); + const select = document.querySelector('select[onchange="changeDifferencePageSize(this.value)"]'); + customInput.style.display = 'none'; + + // 如果不是预设值,保持custom选中状态 + const presetValues = [5, 10, 20, 50, 100]; + if (!presetValues.includes(pageSize)) { + select.value = 'custom_diff'; + } else { + select.value = pageSize.toString(); + } + + displayDifferences(); + } else { + showAlert('warning', '请输入1-1000之间的有效数字'); + } +} + +// 处理自定义差异页面大小输入框回车事件 +function handleCustomDiffPageSizeEnter(event) { + if (event.key === 'Enter') { + setCustomDifferencePageSize(event.target.value); + } +} + // 搜索差异结果 function searchDifferenceResults(searchTerm) { if (!currentResults) return; @@ -1113,277 +1173,145 @@ function escapeForJs(str) { // 显示原生数据 function showRawData(keyStr) { - if (!currentResults) return; + console.log('showRawData 调用,参数:', keyStr); + + if (!currentResults) { + console.error('没有查询结果数据'); + showAlert('warning', '请先执行查询操作'); + return; + } try { + // 解析主键 const key = JSON.parse(keyStr); const keyValue = Object.values(key)[0]; - // 在原生数据中查找匹配的记录 - const proRecord = currentResults.raw_pro_data.find(record => - Object.values(record).includes(keyValue) - ); - const testRecord = currentResults.raw_test_data.find(record => - Object.values(record).includes(keyValue) - ); + console.log('解析主键成功:', key, '键值:', keyValue); - let modalContent = ` -