diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9e021f6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,132 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 项目架构 + +这是一个基于 Flask 的数据库查询比对工具,用于比较 Cassandra 数据库中生产环境和测试环境的数据差异。 + +### 核心组件架构 + +**后端 (Flask)** +- `app.py`: 主应用文件,包含所有API端点和数据处理逻辑 + - 数据库连接管理(Cassandra + SQLite) + - 查询执行和结果比对算法 + - 配置组管理(CRUD操作) + - JSON字段特殊处理和数组比较逻辑 +- `config_groups.db`: SQLite数据库,存储用户保存的配置组 + +**前端 (原生JavaScript + Bootstrap)** +- `templates/db_compare.html`: 主界面模板,包含配置表单和结果展示 +- `templates/index.html`: 工具集合首页 +- `static/js/app.js`: 核心前端逻辑 + - 配置管理和表单处理 + - 差异结果的分页展示系统 + - 原生数据展示(多种视图模式:格式化、原始、差异对比、树形) + - 高级错误处理和用户反馈 + +### 关键功能模块 + +**数据比对引擎** +- 支持复杂JSON字段的深度比较 +- 数组字段的顺序无关比较 +- 字段级别的差异统计和分析 +- 数据质量评估和建议生成 + +**用户界面特性** +- 分页系统(差异记录和相同记录) +- 实时搜索和过滤 +- 原生数据展示(JSON语法高亮、树形视图) +- 配置导入导出和管理 +- 详细的错误诊断和故障排查指南 + +## 开发相关命令 + +### 环境设置 +```bash +# 安装依赖 +pip install -r requirements.txt + +# 运行应用(默认端口5000) +python app.py + +# 自定义端口运行 +# 修改app.py最后一行:app.run(debug=True, port=5001) +``` + +### 开发模式 +应用默认运行在debug模式,代码修改后自动重启。访问 http://localhost:5000 查看首页,http://localhost:5000/db-compare 使用比对工具。 + +## API架构说明 + +### 核心API端点 +- `GET /api/default-config`: 获取默认数据库配置 +- `POST /api/query`: 执行数据库查询比对(主要功能) +- `GET|POST|DELETE /api/config-groups`: 配置组的CRUD操作 +- `POST /api/init-db`: 初始化SQLite数据库 + +### 查询比对流程 +1. 前端发送配置和Key值列表到 `/api/query` +2. 后端创建两个Cassandra连接(生产+测试) +3. 并行执行查询,获取原始数据 +4. 运行比较算法,生成差异报告 +5. 返回完整结果(差异、统计、原始数据) + +## 数据结构和配置 + +### 数据库配置结构 +```javascript +{ + pro_config: { + cluster_name, datacenter, hosts[], port, + username, password, keyspace, table + }, + test_config: { /* 同上 */ }, + keys: ["主键字段名"], + fields_to_compare: ["字段1", "字段2"], // 空数组=全部字段 + exclude_fields: ["排除字段"], + values: ["key1", "key2", "key3"] // 要查询的Key值 +} +``` + +### 查询结果结构 +```javascript +{ + total_keys, pro_count, test_count, + differences: [{ key, field, pro_value, test_value, message }], + identical_results: [{ key, pro_fields, test_fields }], + field_diff_count: { "field_name": count }, + raw_pro_data: [], raw_test_data: [], + summary: { overview, percentages, field_analysis, recommendations } +} +``` + +## 开发注意事项 + +### Cassandra连接处理 +- 连接包含详细的错误诊断和重试机制 +- 使用DCAwareRoundRobinPolicy避免负载均衡警告 +- 连接超时设置为10秒 +- 失败时提供网络连通性测试 + +### 前端状态管理 +- `currentResults`: 存储最新查询结果 +- 分页状态:`currentIdenticalPage`, `currentDifferencePage` +- 过滤状态:`filteredIdenticalResults`, `filteredDifferenceResults` + +### JSON和数组字段处理 +- `normalize_json_string()`: 标准化JSON字符串用于比较 +- `compare_array_values()`: 数组的顺序无关比较 +- `is_json_field()`: 智能检测JSON字段 +- 前端提供专门的JSON语法高亮和树形展示 + +### 错误处理策略 +- 后端:分类错误(connection_error, validation_error, query_error, system_error) +- 前端:详细错误展示,包含配置信息、解决建议、连接测试工具 +- 提供交互式故障排查指南 + +### 性能考虑 +- 大数据集的分页处理 +- 原生数据的延迟加载 +- JSON格式化的客户端缓存 +- 搜索和过滤的防抖处理 \ No newline at end of file diff --git a/app.py b/app.py index 5b69a59..cc0c56a 100644 --- a/app.py +++ b/app.py @@ -36,6 +36,25 @@ def init_database(): ) ''') + # 创建查询历史表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS query_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + pro_config TEXT NOT NULL, + test_config TEXT NOT NULL, + query_config TEXT NOT NULL, + query_keys TEXT NOT NULL, + results_summary TEXT NOT NULL, + execution_time REAL NOT NULL, + total_keys INTEGER NOT NULL, + differences_count INTEGER NOT NULL, + identical_count INTEGER NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + conn.commit() conn.close() logger.info("数据库初始化完成") @@ -751,4 +770,4 @@ def api_init_database(): return jsonify({'success': False, 'error': '数据库初始化失败'}), 500 if __name__ == '__main__': - app.run(debug=True) + app.run(debug=True, port=5001) diff --git a/static/js/app.js b/static/js/app.js index 2ad5934..72b0f42 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -3,6 +3,9 @@ let currentResults = null; let currentIdenticalPage = 1; let identicalPageSize = 10; let filteredIdenticalResults = []; +let currentDifferencePage = 1; +let differencePageSize = 10; +let filteredDifferenceResults = []; // 页面加载完成后初始化 document.addEventListener('DOMContentLoaded', function() { @@ -472,8 +475,12 @@ function displayResults(results) { filteredIdenticalResults = results.identical_results; currentIdenticalPage = 1; + // 初始化差异结果分页数据 + filteredDifferenceResults = results.differences; + currentDifferencePage = 1; + // 显示各个面板内容 - displayDifferences(results.differences); + displayDifferences(); displayIdenticalResults(); displayComparisonSummary(results.summary); @@ -516,23 +523,76 @@ function displayStats(results) { } // 显示差异详情 -function displayDifferences(differences) { +function displayDifferences() { const differencesContainer = document.getElementById('differences'); - if (!differences.length) { + if (!filteredDifferenceResults.length) { differencesContainer.innerHTML = '
未发现差异
'; return; } - let html = ''; - differences.forEach((diff, index) => { + // 计算分页 + const totalPages = Math.ceil(filteredDifferenceResults.length / differencePageSize); + const startIndex = (currentDifferencePage - 1) * differencePageSize; + const endIndex = startIndex + differencePageSize; + const currentPageData = filteredDifferenceResults.slice(startIndex, endIndex); + + let html = ` + +主键: ${JSON.stringify(diff.key)}
-${diff.message}
+主键: ${JSON.stringify(diff.key)}
+${diff.message}
+主键: ${JSON.stringify(diff.key)}
+差异字段: ${diff.field}
主键: ${JSON.stringify(diff.key)}
-字段: ${diff.field}
- -${escapeHtml(diff.pro_value)}- +
${escapeHtml(diff.test_value)}- + + +
${escapeHtml(diff.pro_value)}+ +
${escapeHtml(diff.test_value)}+ +
${escapeHtml(proValue)}+
${escapeHtml(String(proValue))}@@ -738,9 +839,9 @@ function displayIdenticalResults() {
${escapeHtml(testValue)}+
${escapeHtml(String(testValue))}@@ -872,6 +973,107 @@ function searchIdenticalResults(searchTerm) { 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) { + differencePageSize = parseInt(newSize); + currentDifferencePage = 1; + displayDifferences(); +} + +// 搜索差异结果 +function searchDifferenceResults(searchTerm) { + if (!currentResults) return; + + if (!searchTerm.trim()) { + filteredDifferenceResults = currentResults.differences; + } else { + const term = searchTerm.toLowerCase(); + filteredDifferenceResults = currentResults.differences.filter(diff => { + // 搜索主键 + const keyStr = JSON.stringify(diff.key).toLowerCase(); + if (keyStr.includes(term)) return true; + + // 搜索字段名 + if (diff.field && diff.field.toLowerCase().includes(term)) return true; + + // 搜索字段值 + if (diff.pro_value && String(diff.pro_value).toLowerCase().includes(term)) return true; + if (diff.test_value && String(diff.test_value).toLowerCase().includes(term)) return true; + + // 搜索消息 + if (diff.message && diff.message.toLowerCase().includes(term)) return true; + + return false; + }); + } + + currentDifferencePage = 1; + displayDifferences(); +} + // 复制到剪贴板 function copyToClipboard(text, button) { navigator.clipboard.writeText(text).then(function() { @@ -894,7 +1096,19 @@ function copyToClipboard(text, button) { // JS字符串转义 function escapeForJs(str) { - return str.replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r'); + 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'); // 空字符 } // 显示原生数据 @@ -919,37 +1133,76 @@ function showRawData(keyStr) {