初始化项目

This commit is contained in:
2025-07-31 21:17:00 +08:00
parent 6fecd70ca5
commit 7d0c1a5896
3 changed files with 647 additions and 89 deletions

132
CLAUDE.md Normal file
View File

@@ -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格式化的客户端缓存
- 搜索和过滤的防抖处理

21
app.py
View File

@@ -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)

View File

@@ -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 = '<p class="text-success"><i class="fas fa-check"></i> 未发现差异</p>';
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 = `
<!-- 分页控制 -->
<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" style="width: auto;" onchange="changeDifferencePageSize(this.value)">
<option value="5" ${differencePageSize == 5 ? 'selected' : ''}>5条</option>
<option value="10" ${differencePageSize == 10 ? 'selected' : ''}>10条</option>
<option value="20" ${differencePageSize == 20 ? 'selected' : ''}>20条</option>
<option value="50" ${differencePageSize == 50 ? 'selected' : ''}>50条</option>
</select>
<span class="ms-3 text-muted">共 ${filteredDifferenceResults.length} 条差异记录</span>
</div>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-end align-items-center">
${generateDifferencePagination(currentDifferencePage, totalPages)}
</div>
</div>
</div>
<!-- 搜索框 -->
<div class="mb-3">
<div class="input-group">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input type="text" class="form-control" placeholder="搜索主键、字段名或值内容..."
onkeyup="searchDifferenceResults(this.value)" id="differenceSearch">
</div>
</div>
`;
// 显示当前页数据
currentPageData.forEach((diff, index) => {
const globalIndex = startIndex + index + 1;
if (diff.message) {
// 记录不存在的情况
html += `
<div class="difference-item">
<strong>差异 #${index + 1}</strong>
<p><strong>主键:</strong> ${JSON.stringify(diff.key)}</p>
<p class="text-warning"><i class="fas fa-exclamation-triangle"></i> ${diff.message}</p>
<div class="difference-item card mb-3 border-warning">
<div class="card-header bg-light">
<div class="row align-items-center">
<div class="col">
<strong>差异 #${globalIndex}</strong>
<span class="badge bg-warning ms-2">记录缺失</span>
</div>
<div class="col-auto">
<button class="btn btn-sm btn-outline-info" onclick="showDifferenceRawData('${escapeForJs(JSON.stringify(diff.key))}')">
<i class="fas fa-code"></i> 原生数据
</button>
</div>
</div>
<p class="mb-0 mt-2"><strong>主键:</strong> ${JSON.stringify(diff.key)}</p>
</div>
<div class="card-body">
<p class="text-warning"><i class="fas fa-exclamation-triangle"></i> ${diff.message}</p>
</div>
</div>
`;
} else {
@@ -542,60 +602,72 @@ function displayDifferences(differences) {
const jsonClass = isJson ? 'json-field' : '';
html += `
<div class="difference-item">
<div class="d-flex justify-content-between align-items-center mb-2">
<div>
<strong>差异 #${index + 1}</strong>
${isJson ? '<span class="badge bg-info ms-2">JSON字段</span>' : ''}
${isArray ? '<span class="badge bg-warning ms-2">数组字段</span>' : ''}
<div class="difference-item card mb-3 border-danger">
<div class="card-header bg-light">
<div class="row align-items-center">
<div class="col">
<strong>差异 #${globalIndex}</strong>
<span class="badge bg-danger ms-2">字段差异</span>
${isJson ? '<span class="badge bg-info ms-2">JSON字段</span>' : ''}
${isArray ? '<span class="badge bg-warning ms-2">数组字段</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="#diffCollapse${globalIndex}">
<i class="fas fa-eye"></i> 查看详情
</button>
<button class="btn btn-sm btn-outline-info" onclick="showDifferenceRawData('${escapeForJs(JSON.stringify(diff.key))}')">
<i class="fas fa-code"></i> 原生数据
</button>
</div>
</div>
<button class="btn btn-sm btn-outline-info" onclick="showDifferenceRawData('${escapeForJs(JSON.stringify(diff.key))}')">
<i class="fas fa-code"></i> 原生数据
</button>
<p class="mb-0 mt-2"><strong>主键:</strong> ${JSON.stringify(diff.key)}</p>
<p class="mb-0"><strong>差异字段:</strong> ${diff.field}</p>
</div>
<p><strong>主键:</strong> ${JSON.stringify(diff.key)}</p>
<p><strong>字段:</strong> ${diff.field}</p>
<div class="mb-4">
<div class="field-header mb-2">
<i class="fas fa-tag text-primary"></i>
<strong>${diff.field}</strong>
</div>
<!-- 生产环境数据行 -->
<div class="row mb-2">
<div class="col-2">
<h6 class="text-success mb-0 d-flex align-items-center">
<i class="fas fa-server me-2"></i>生产环境
</h6>
</div>
<div class="col-10">
<div class="field-container position-relative">
<pre class="field-value bg-light p-2 rounded mb-0 ${jsonClass}">${escapeHtml(diff.pro_value)}</pre>
<button class="btn btn-sm btn-outline-secondary copy-btn"
onclick="copyToClipboard('${escapeForJs(diff.pro_value)}', this)"
title="复制内容">
<i class="fas fa-copy"></i>
</button>
<div class="collapse" id="diffCollapse${globalIndex}">
<div class="card-body">
<div class="mb-4">
<div class="field-header mb-2">
<i class="fas fa-tag text-primary"></i>
<strong>${diff.field}</strong>
</div>
</div>
</div>
<!-- 测试环境数据行 -->
<div class="row">
<div class="col-2">
<h6 class="text-info mb-0 d-flex align-items-center">
<i class="fas fa-flask me-2"></i>测试环境
</h6>
</div>
<div class="col-10">
<div class="field-container position-relative">
<pre class="field-value bg-light p-2 rounded mb-0 ${jsonClass}">${escapeHtml(diff.test_value)}</pre>
<button class="btn btn-sm btn-outline-secondary copy-btn"
onclick="copyToClipboard('${escapeForJs(diff.test_value)}', this)"
title="复制内容">
<i class="fas fa-copy"></i>
</button>
<!-- 生产环境数据行 -->
<div class="row mb-2">
<div class="col-2">
<h6 class="text-success mb-0 d-flex align-items-center">
<i class="fas fa-server me-2"></i>生产环境
</h6>
</div>
<div class="col-10">
<div class="field-container position-relative">
<pre class="field-value bg-light p-2 rounded mb-0 ${jsonClass}">${escapeHtml(diff.pro_value)}</pre>
<button class="btn btn-sm btn-outline-secondary copy-btn"
onclick="copyToClipboard('${escapeForJs(diff.pro_value)}', this)"
title="复制内容">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
</div>
<!-- 测试环境数据行 -->
<div class="row">
<div class="col-2">
<h6 class="text-info mb-0 d-flex align-items-center">
<i class="fas fa-flask me-2"></i>测试环境
</h6>
</div>
<div class="col-10">
<div class="field-container position-relative">
<pre class="field-value bg-light p-2 rounded mb-0 ${jsonClass}">${escapeHtml(diff.test_value)}</pre>
<button class="btn btn-sm btn-outline-secondary copy-btn"
onclick="copyToClipboard('${escapeForJs(diff.test_value)}', this)"
title="复制内容">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
</div>
</div>
</div>
@@ -605,6 +677,19 @@ function displayDifferences(differences) {
}
});
// 底部分页
if (totalPages > 1) {
html += `
<div class="row mt-3">
<div class="col-12">
<div class="d-flex justify-content-center">
${generateDifferencePagination(currentDifferencePage, totalPages)}
</div>
</div>
</div>
`;
}
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 += `
<div class="mb-4">
@@ -719,9 +820,9 @@ function displayIdenticalResults() {
</div>
<div class="col-10">
<div class="field-container position-relative">
<pre class="field-value bg-light p-2 rounded mb-0">${escapeHtml(proValue)}</pre>
<pre class="field-value bg-light p-2 rounded mb-0">${escapeHtml(String(proValue))}</pre>
<button class="btn btn-sm btn-outline-secondary copy-btn"
onclick="copyToClipboard('${escapeForJs(proValue)}', this)"
onclick="copyToClipboard('${escapeForJs(String(proValue))}', this)"
title="复制内容">
<i class="fas fa-copy"></i>
</button>
@@ -738,9 +839,9 @@ function displayIdenticalResults() {
</div>
<div class="col-10">
<div class="field-container position-relative">
<pre class="field-value bg-light p-2 rounded mb-0">${escapeHtml(testValue)}</pre>
<pre class="field-value bg-light p-2 rounded mb-0">${escapeHtml(String(testValue))}</pre>
<button class="btn btn-sm btn-outline-secondary copy-btn"
onclick="copyToClipboard('${escapeForJs(testValue)}', this)"
onclick="copyToClipboard('${escapeForJs(String(testValue))}', this)"
title="复制内容">
<i class="fas fa-copy"></i>
</button>
@@ -872,6 +973,107 @@ function searchIdenticalResults(searchTerm) {
displayIdenticalResults();
}
// 生成差异分页导航
function generateDifferencePagination(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="goToDifferencePage(${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="goToDifferencePage(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="goToDifferencePage(${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="goToDifferencePage(${totalPages})">${totalPages}</a></li>`;
}
// 下一页
html += `
<li class="page-item ${currentPage === totalPages ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="goToDifferencePage(${currentPage + 1})">
<i class="fas fa-chevron-right"></i>
</a>
</li>
`;
html += '</ul></nav>';
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) {
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-code"></i> 原生查询数据
<i class="fas fa-code"></i> 原生查询数据对比
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="text-success mb-0">
<i class="fas fa-server"></i> 生产环境原生数据
</h6>
<button class="btn btn-sm btn-outline-secondary"
onclick="copyToClipboard('${escapeForJs(JSON.stringify(proRecord, null, 2))}', this)">
<i class="fas fa-copy"></i> 复制
<!-- 操作工具栏 -->
<div class="row mb-3">
<div class="col-12">
<div class="btn-group" role="group">
<input type="radio" class="btn-check" name="viewMode" id="viewMode1" value="formatted" checked onchange="switchViewMode(this.value)">
<label class="btn btn-outline-primary" for="viewMode1">
<i class="fas fa-code"></i> 格式化显示
</label>
<input type="radio" class="btn-check" name="viewMode" id="viewMode2" value="raw" onchange="switchViewMode(this.value)">
<label class="btn btn-outline-primary" for="viewMode2">
<i class="fas fa-align-left"></i> 原始显示
</label>
<input type="radio" class="btn-check" name="viewMode" id="viewMode3" value="diff" onchange="switchViewMode(this.value)">
<label class="btn btn-outline-primary" for="viewMode3">
<i class="fas fa-not-equal"></i> 差异对比
</label>
</div>
<div class="btn-group ms-3" role="group">
<button class="btn btn-outline-secondary" onclick="copyAllRawData()">
<i class="fas fa-copy"></i> 复制全部
</button>
<button class="btn btn-outline-info" onclick="downloadRawData()">
<i class="fas fa-download"></i> 下载数据
</button>
</div>
<pre class="bg-light p-3 rounded" style="max-height: 400px; overflow-y: auto;">${escapeHtml(JSON.stringify(proRecord, null, 2))}</pre>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="text-info mb-0">
<i class="fas fa-flask"></i> 测试环境原生数据
</h6>
<button class="btn btn-sm btn-outline-secondary"
onclick="copyToClipboard('${escapeForJs(JSON.stringify(testRecord, null, 2))}', this)">
<i class="fas fa-copy"></i> 复制
</button>
</div>
<!-- 数据显示区域 -->
<div id="rawDataContent">
<div class="row">
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="text-success mb-0">
<i class="fas fa-server"></i> 生产环境原生数据
</h6>
<button class="btn btn-sm btn-outline-secondary"
onclick="copyToClipboard('${escapeForJs(JSON.stringify(proRecord, null, 2))}', this)">
<i class="fas fa-copy"></i> 复制
</button>
</div>
<pre class="bg-light p-3 rounded raw-data-container" style="max-height: 500px; overflow-y: auto;" id="proRawData">${escapeHtml(JSON.stringify(proRecord, null, 2))}</pre>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="text-info mb-0">
<i class="fas fa-flask"></i> 测试环境原生数据
</h6>
<button class="btn btn-sm btn-outline-secondary"
onclick="copyToClipboard('${escapeForJs(JSON.stringify(testRecord, null, 2))}', this)">
<i class="fas fa-copy"></i> 复制
</button>
</div>
<pre class="bg-light p-3 rounded raw-data-container" style="max-height: 500px; overflow-y: auto;" id="testRawData">${escapeHtml(JSON.stringify(testRecord, null, 2))}</pre>
</div>
<pre class="bg-light p-3 rounded" style="max-height: 400px; overflow-y: auto;">${escapeHtml(JSON.stringify(testRecord, null, 2))}</pre>
</div>
</div>
<!-- 隐藏的数据存储 -->
<div style="display: none;">
<div id="hiddenProData">${escapeHtml(JSON.stringify(proRecord, null, 2))}</div>
<div id="hiddenTestData">${escapeHtml(JSON.stringify(testRecord, null, 2))}</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
@@ -977,6 +1230,160 @@ function showRawData(keyStr) {
}
}
// 切换原生数据显示模式
function switchViewMode(mode) {
const proData = document.getElementById('hiddenProData').textContent;
const testData = document.getElementById('hiddenTestData').textContent;
const proContainer = document.getElementById('proRawData');
const testContainer = document.getElementById('testRawData');
switch(mode) {
case 'formatted':
// 格式化JSON显示
try {
const proObj = JSON.parse(proData);
const testObj = JSON.parse(testData);
proContainer.innerHTML = escapeHtml(JSON.stringify(proObj, null, 2));
testContainer.innerHTML = escapeHtml(JSON.stringify(testObj, null, 2));
proContainer.className = 'bg-light p-3 rounded raw-data-container';
testContainer.className = 'bg-light p-3 rounded raw-data-container';
} catch (e) {
proContainer.innerHTML = escapeHtml(proData);
testContainer.innerHTML = escapeHtml(testData);
}
break;
case 'raw':
// 原始单行显示
try {
const proObj = JSON.parse(proData);
const testObj = JSON.parse(testData);
proContainer.innerHTML = escapeHtml(JSON.stringify(proObj));
testContainer.innerHTML = escapeHtml(JSON.stringify(testObj));
proContainer.className = 'bg-light p-3 rounded raw-data-container';
testContainer.className = 'bg-light p-3 rounded raw-data-container';
} catch (e) {
proContainer.innerHTML = escapeHtml(proData);
testContainer.innerHTML = escapeHtml(testData);
}
break;
case 'diff':
// 差异对比显示
try {
const proObj = JSON.parse(proData);
const testObj = JSON.parse(testData);
const diffResult = generateFieldDiff(proObj, testObj);
document.getElementById('rawDataContent').innerHTML = `
<div class="row">
<div class="col-12">
<h6 class="mb-3"><i class="fas fa-not-equal"></i> 字段差异对比</h6>
${diffResult}
</div>
</div>
`;
} catch (e) {
showAlert('warning', '无法生成差异对比:' + e.message);
}
break;
}
}
// 生成字段差异对比HTML
function generateFieldDiff(proObj, testObj) {
const allFields = new Set([...Object.keys(proObj), ...Object.keys(testObj)]);
let html = '';
allFields.forEach(field => {
const proValue = proObj[field];
const testValue = testObj[field];
const isEqual = JSON.stringify(proValue) === JSON.stringify(testValue);
const isProMissing = !(field in proObj);
const isTestMissing = !(field in testObj);
let statusBadge = '';
let cardClass = '';
if (isProMissing) {
statusBadge = '<span class="badge bg-warning">生产缺失</span>';
cardClass = 'border-warning';
} else if (isTestMissing) {
statusBadge = '<span class="badge bg-info">测试缺失</span>';
cardClass = 'border-info';
} else if (isEqual) {
statusBadge = '<span class="badge bg-success">相同</span>';
cardClass = 'border-success';
} else {
statusBadge = '<span class="badge bg-danger">不同</span>';
cardClass = 'border-danger';
}
html += `
<div class="card mb-3 ${cardClass}">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<strong>${field}</strong>
${statusBadge}
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6 class="text-success">
<i class="fas fa-server"></i> 生产环境
</h6>
<pre class="bg-light p-2 rounded" style="max-height: 200px; overflow-y: auto;">${isProMissing ? '字段不存在' : escapeHtml(JSON.stringify(proValue, null, 2))}</pre>
</div>
<div class="col-md-6">
<h6 class="text-info">
<i class="fas fa-flask"></i> 测试环境
</h6>
<pre class="bg-light p-2 rounded" style="max-height: 200px; overflow-y: auto;">${isTestMissing ? '字段不存在' : escapeHtml(JSON.stringify(testValue, null, 2))}</pre>
</div>
</div>
</div>
</div>
`;
});
return html || '<p class="text-muted">无字段数据可对比</p>';
}
// 复制全部原生数据
function copyAllRawData() {
const proData = document.getElementById('hiddenProData').textContent;
const testData = document.getElementById('hiddenTestData').textContent;
const allData = `生产环境数据:\n${proData}\n\n测试环境数据:\n${testData}`;
navigator.clipboard.writeText(allData).then(() => {
showAlert('success', '已复制全部原生数据到剪贴板');
}).catch(err => {
showAlert('danger', '复制失败: ' + err.message);
});
}
// 下载原生数据
function downloadRawData() {
const proData = document.getElementById('hiddenProData').textContent;
const testData = document.getElementById('hiddenTestData').textContent;
const exportData = {
timestamp: new Date().toISOString(),
production_data: JSON.parse(proData),
test_data: JSON.parse(testData)
};
const dataStr = JSON.stringify(exportData, null, 2);
const dataBlob = new Blob([dataStr], {type: 'application/json'});
const link = document.createElement('a');
link.href = URL.createObjectURL(dataBlob);
link.download = `raw_data_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
link.click();
}
// 显示差异数据的原生数据
function showDifferenceRawData(keyStr) {
showRawData(keyStr); // 复用相同的原生数据显示逻辑