From 7d0c1a589640f4b1e8b0426fd08709b2035d2395 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Thu, 31 Jul 2025 21:17:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 132 +++++++++++ app.py | 21 +- static/js/app.js | 583 ++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 647 insertions(+), 89 deletions(-) create mode 100644 CLAUDE.md 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 = ` + +
+
+
+ + + 共 ${filteredDifferenceResults.length} 条差异记录 +
+
+
+
+ ${generateDifferencePagination(currentDifferencePage, totalPages)} +
+
+
+ + +
+
+ + +
+
+ `; + + // 显示当前页数据 + currentPageData.forEach((diff, index) => { + const globalIndex = startIndex + index + 1; if (diff.message) { // 记录不存在的情况 html += ` -
- 差异 #${index + 1} -

主键: ${JSON.stringify(diff.key)}

-

${diff.message}

+
+
+
+
+ 差异 #${globalIndex} + 记录缺失 +
+
+ +
+
+

主键: ${JSON.stringify(diff.key)}

+
+
+

${diff.message}

+
`; } else { @@ -542,60 +602,72 @@ function displayDifferences(differences) { const jsonClass = isJson ? 'json-field' : ''; html += ` -
-
-
- 差异 #${index + 1} - ${isJson ? 'JSON字段' : ''} - ${isArray ? '数组字段' : ''} +
+
+
+
+ 差异 #${globalIndex} + 字段差异 + ${isJson ? 'JSON字段' : ''} + ${isArray ? '数组字段' : ''} +
+
+ + +
- +

主键: ${JSON.stringify(diff.key)}

+

差异字段: ${diff.field}

-

主键: ${JSON.stringify(diff.key)}

-

字段: ${diff.field}

- -
-
- - ${diff.field} -
- - -
-
-
- 生产环境 -
-
-
-
-
${escapeHtml(diff.pro_value)}
- +
+
+
+
+ + ${diff.field}
-
-
- - -
-
-
- 测试环境 -
-
-
-
-
${escapeHtml(diff.test_value)}
- + + +
+
+
+ 生产环境 +
+
+
+
+
${escapeHtml(diff.pro_value)}
+ +
+
+
+ + +
+
+
+ 测试环境 +
+
+
+
+
${escapeHtml(diff.test_value)}
+ +
+
@@ -605,6 +677,19 @@ function displayDifferences(differences) { } }); + // 底部分页 + if (totalPages > 1) { + html += ` +
+
+
+ ${generateDifferencePagination(currentDifferencePage, totalPages)} +
+
+
+ `; + } + differencesContainer.innerHTML = html; } @@ -699,8 +784,24 @@ function displayIdenticalResults() { allFields.forEach(fieldName => { const proValue = proFields[fieldName] || ''; const testValue = testFields[fieldName] || ''; - const isJson = (proValue.includes('{') || proValue.includes('[')) || - (testValue.includes('{') || testValue.includes('[')); + + // 更安全的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 += `
@@ -719,9 +820,9 @@ function displayIdenticalResults() {
-
${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) {