vue3_flask实现mysql数据库对比功能

实现对mysql中两个数据库的表、表结构、表数据的对比功能, 初始化界面效果如下图

基础环境请参考

vue3+flask+sqlite前后端项目实战

代码文件结构变化

javascript 复制代码
api/    # 后端相关
├── daos/
│   ├── __init__.py
│   └── db_compare_dao.py    # 新增
├── routes/
│   ├── __init__.py
│   └── db_compare_routes.py  # 新增
├── run.py                    # 原有代码增加

ui/    # 前端相关
├── src/
│   ├── views/
│       └── DbCompare.vue  # 新增
│   ├── router/
│       └── index.js       # 原有代码增加

后端相关

api/daos/db_compare_dao.py

javascript 复制代码
import pymysql
import logging
from datetime import datetime
from typing import Dict, List, Tuple, Optional

# 配置日志系统
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[logging.StreamHandler()]
)
logger = logging.getLogger('db_comparison')


def get_db_connection(db_config: Dict):
    """获取数据库连接"""
    return pymysql.connect(
        host=db_config['host'],
        port=db_config['port'],
        user=db_config['user'],
        password=db_config['password'],
        database=db_config['database'],
        cursorclass=pymysql.cursors.DictCursor,
        charset='utf8mb4'
    )


def compare_columns(col1: Dict, col2: Dict) -> bool:
    """比较两个列的属性是否一致"""
    return (
            col1['COLUMN_NAME'] == col2['COLUMN_NAME'] and
            col1['DATA_TYPE'] == col2['DATA_TYPE'] and
            col1['IS_NULLABLE'] == col2['IS_NULLABLE'] and
            str(col1.get('COLUMN_DEFAULT')) == str(col2.get('COLUMN_DEFAULT')) and
            col1.get('CHARACTER_MAXIMUM_LENGTH') == col2.get('CHARACTER_MAXIMUM_LENGTH')
    )


def get_column_diff_details(col1: Dict, col2: Dict) -> List[str]:
    """获取列属性的具体差异"""
    differences = []
    if col1['DATA_TYPE'] != col2['DATA_TYPE']:
        differences.append(f"类型不同({col1['DATA_TYPE']} vs {col2['DATA_TYPE']})")
    if col1['IS_NULLABLE'] != col2['IS_NULLABLE']:
        differences.append(f"可空性不同({col1['IS_NULLABLE']} vs {col2['IS_NULLABLE']})")
    if str(col1.get('COLUMN_DEFAULT')) != str(col2.get('COLUMN_DEFAULT')):
        differences.append(f"默认值不同({col1.get('COLUMN_DEFAULT')} vs {col2.get('COLUMN_DEFAULT')})")
    if col1.get('CHARACTER_MAXIMUM_LENGTH') != col2.get('CHARACTER_MAXIMUM_LENGTH'):
        differences.append(
            f"长度不同({col1.get('CHARACTER_MAXIMUM_LENGTH')} vs {col2.get('CHARACTER_MAXIMUM_LENGTH')})")
    return differences


def get_table_structure(cursor, db_name: str) -> List[Dict]:
    """获取表结构信息"""
    cursor.execute(f"""
        SELECT 
            TABLE_NAME, 
            COLUMN_NAME, 
            DATA_TYPE, 
            IS_NULLABLE, 
            COLUMN_DEFAULT, 
            CHARACTER_MAXIMUM_LENGTH,
            COLUMN_TYPE,
            EXTRA
        FROM INFORMATION_SCHEMA.COLUMNS 
        WHERE TABLE_SCHEMA = '{db_name}'
        ORDER BY TABLE_NAME, ORDINAL_POSITION
    """)
    return cursor.fetchall()


def get_table_hash(cursor, table_name: str) -> Tuple[Optional[str], int]:
    """获取表的哈希值和行数"""
    try:
        # 1. 获取表的列信息
        cursor.execute(f"SHOW COLUMNS FROM `{table_name}`")
        columns = [row['Field'] for row in cursor.fetchall()]

        if not columns:
            logger.warning(f"表 {table_name} 没有列")
            return None, 0

        # 2. 获取行数
        cursor.execute(f"SELECT COUNT(*) AS count FROM `{table_name}`")
        row_count = cursor.fetchone()['count']

        if row_count == 0:
            logger.info(f"表 {table_name} 是空表")
            return None, 0

        # 3. 准备安全的列名列表
        safe_columns = [f"`{col}`" for col in columns]
        columns_concat = ", ".join(safe_columns)

        # 4. 增加 GROUP_CONCAT 最大长度限制
        cursor.execute("SET SESSION group_concat_max_len = 1000000")

        # 5. 构建并执行哈希查询
        query = f"""
            SELECT MD5(
                GROUP_CONCAT(
                    CONCAT_WS('|', {columns_concat})
                    ORDER BY `{columns[0]}`
                    SEPARATOR '||'
                )
            ) AS hash,
            COUNT(*) AS count
            FROM `{table_name}`
        """

        cursor.execute(query)
        result = cursor.fetchone()

        return (result['hash'], row_count)

    except Exception as e:
        logger.error(f"获取表 {table_name} 哈希失败: {e}")
        return None, 0


def compare_databases(db1_config: Dict, db2_config: Dict) -> Dict:
    """比较两个数据库的结构和数据"""
    result = {
        "status": "success",
        "message": "",
        "details": {
            "structure": [],
            "data": [],
            "tables": [],
            "identical_tables": [],  # 新增:完全相同的表列表
            "summary": {
                "total_tables": 0,
                "tables_with_structure_diff": 0,
                "tables_with_data_diff": 0,
                "tables_missing": 0,
                "tables_identical": 0,  # 完全相同表的数量
                "tables_with_differences": 0,  # 有差异表的数量
                "tables_only_in_db1": 0,  # 仅在db1中存在的表
                "tables_only_in_db2": 0,  # 仅在db2中存在的表
                "tables_compared": 0,  # 实际比较的表数量
                "start_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                "end_time": "",
                "duration": ""
            }
        },
        "logs": []
    }

    def add_log(message: str, level: str = "info"):
        """添加日志到结果和控制台"""
        timestamp = datetime.now().strftime('%H:%M:%S')
        log_msg = f"[{timestamp}] {message}"
        result["logs"].append(log_msg)

        if level == "error":
            logger.error(log_msg)
        elif level == "warning":
            logger.warning(log_msg)
        else:
            logger.info(log_msg)
        return log_msg

    db1 = db2 = None
    start_time = datetime.now()

    try:
        add_log("开始数据库对比任务")
        add_log(f"数据库1配置: {db1_config['host']}:{db1_config['port']}/{db1_config['database']}")
        add_log(f"数据库2配置: {db2_config['host']}:{db2_config['port']}/{db2_config['database']}")

        # 连接数据库
        add_log("正在连接数据库...")
        db1 = get_db_connection(db1_config)
        db2 = get_db_connection(db2_config)
        add_log("数据库连接成功")

        with db1.cursor() as cursor1, db2.cursor() as cursor2:
            # 获取所有表名
            cursor1.execute("SHOW TABLES")
            tables1 = [list(t.values())[0].lower() for t in cursor1.fetchall()]
            cursor2.execute("SHOW TABLES")
            tables2 = [list(t.values())[0].lower() for t in cursor2.fetchall()]

            all_tables = set(tables1 + tables2)
            result["details"]["summary"]["total_tables"] = len(all_tables)
            result["details"]["summary"]["tables_only_in_db1"] = len(set(tables1) - set(tables2))
            result["details"]["summary"]["tables_only_in_db2"] = len(set(tables2) - set(tables1))
            add_log(f"数据库1有 {len(tables1)} 个表,数据库2有 {len(tables2)} 个表,共 {len(all_tables)} 个唯一表")

            # 表存在性检查
            for table in set(tables1) - set(tables2):
                issue_msg = f"表 {table} 不存在于数据库2"
                result["details"]["tables"].append({
                    "table": table,
                    "issues": [{
                        "type": "table",
                        "message": issue_msg,
                        "severity": "high"
                    }]
                })
                add_log(issue_msg, "warning")

            for table in set(tables2) - set(tables1):
                issue_msg = f"表 {table} 不存在于数据库1"
                result["details"]["tables"].append({
                    "table": table,
                    "issues": [{
                        "type": "table",
                        "message": issue_msg,
                        "severity": "high"
                    }]
                })
                add_log(issue_msg, "warning")

            # 获取表结构
            add_log("正在获取数据库表结构...")
            structure1 = get_table_structure(cursor1, db1_config['database'])
            structure2 = get_table_structure(cursor2, db2_config['database'])

            # 转换为字典便于查找
            db1_fields = {
                (row['TABLE_NAME'].lower(), row['COLUMN_NAME'].lower()): row
                for row in structure1
            }
            db2_fields = {
                (row['TABLE_NAME'].lower(), row['COLUMN_NAME'].lower()): row
                for row in structure2
            }

            # 比较共有表
            common_tables = set(tables1) & set(tables2)
            result["details"]["summary"]["tables_compared"] = len(common_tables)
            add_log(f"共 {len(common_tables)} 个表需要比较结构和数据")

            for table_idx, table in enumerate(common_tables, 1):
                table_result = {
                    "table": table,
                    "structure_issues": [],
                    "data_issues": [],
                    "is_identical": True
                }

                add_log(f"正在比较表 ({table_idx}/{len(common_tables)}): {table}")

                # 比较表结构
                db1_table_fields = {k[1]: v for k, v in db1_fields.items() if k[0] == table}
                db2_table_fields = {k[1]: v for k, v in db2_fields.items() if k[0] == table}

                # 检查字段存在性
                for field in set(db1_table_fields.keys()) - set(db2_table_fields.keys()):
                    issue_msg = f"表 {table} 字段 {field} 不存在于数据库2"
                    table_result["structure_issues"].append({
                        "type": "structure",
                        "field": field,
                        "message": issue_msg,
                        "db1_type": db1_table_fields[field]['DATA_TYPE'],
                        "db2_type": None
                    })
                    table_result["is_identical"] = False
                    add_log(issue_msg, "warning")

                for field in set(db2_table_fields.keys()) - set(db1_table_fields.keys()):
                    issue_msg = f"表 {table} 字段 {field} 不存在于数据库1"
                    table_result["structure_issues"].append({
                        "type": "structure",
                        "field": field,
                        "message": issue_msg,
                        "db1_type": None,
                        "db2_type": db2_table_fields[field]['DATA_TYPE']
                    })
                    table_result["is_identical"] = False
                    add_log(issue_msg, "warning")

                # 比较共有字段
                common_fields = set(db1_table_fields.keys()) & set(db2_table_fields.keys())
                for field in common_fields:
                    col1 = db1_table_fields[field]
                    col2 = db2_table_fields[field]

                    if not compare_columns(col1, col2):
                        differences = get_column_diff_details(col1, col2)
                        issue_msg = f"表 {table} 字段 {field} 属性不一致: {', '.join(differences)}"
                        table_result["structure_issues"].append({
                            "type": "structure",
                            "field": field,
                            "message": issue_msg,
                            "db1_type": col1['DATA_TYPE'],
                            "db2_type": col2['DATA_TYPE'],
                            "details": differences
                        })
                        table_result["is_identical"] = False
                        add_log(issue_msg, "warning")

                # 比较表数据
                hash1, row_count1 = get_table_hash(cursor1, table)
                hash2, row_count2 = get_table_hash(cursor2, table)

                if hash1 != hash2 or row_count1 != row_count2:
                    issue_details = {
                        "type": "data",
                        "message": f"表 {table} 数据不一致",
                        "db1_row_count": row_count1,
                        "db2_row_count": row_count2,
                        "db1_hash": hash1,
                        "db2_hash": hash2,
                        "severity": "medium"
                    }

                    if row_count1 != row_count2:
                        issue_details["message"] += f" (行数不同: {row_count1} vs {row_count2})"
                    else:
                        issue_details["message"] += " (行数相同但内容不同)"

                    table_result["data_issues"].append(issue_details)
                    table_result["is_identical"] = False
                    add_log(issue_details["message"], "warning")
                else:
                    add_log(f"表 {table} 数据一致 (行数: {row_count1})")

                # 记录结果
                if table_result["structure_issues"]:
                    result["details"]["structure"].append({
                        "table": table,
                        "issues": table_result["structure_issues"]
                    })
                    result["details"]["summary"]["tables_with_structure_diff"] += 1

                if table_result["data_issues"]:
                    result["details"]["data"].append({
                        "table": table,
                        "issues": table_result["data_issues"]
                    })
                    result["details"]["summary"]["tables_with_data_diff"] += 1

                if table_result["is_identical"]:
                    result["details"]["identical_tables"].append(table)
                    result["details"]["summary"]["tables_identical"] += 1
                else:
                    result["details"]["summary"]["tables_with_differences"] += 1

        # 计算耗时
        end_time = datetime.now()
        duration = end_time - start_time
        result["details"]["summary"]["end_time"] = end_time.strftime('%Y-%m-%d %H:%M:%S')
        result["details"]["summary"]["duration"] = str(duration)

        # 汇总结果消息
        has_differences = (
                result["details"]["summary"]["tables_with_structure_diff"] > 0 or
                result["details"]["summary"]["tables_with_data_diff"] > 0 or
                result["details"]["summary"]["tables_missing"] > 0
        )

        if has_differences:
            diff_details = []
            if result["details"]["summary"]["tables_missing"] > 0:
                diff_details.append(f"{result['details']['summary']['tables_missing']}个表缺失")
            if result["details"]["summary"]["tables_with_structure_diff"] > 0:
                diff_details.append(f"{result['details']['summary']['tables_with_structure_diff']}个表结构不同")
            if result["details"]["summary"]["tables_with_data_diff"] > 0:
                diff_details.append(f"{result['details']['summary']['tables_with_data_diff']}个表数据不同")

            final_msg = (
                f"对比完成,共比较 {result['details']['summary']['tables_compared']} 个表。"
                f"发现差异: {', '.join(diff_details)}。"
                f"完全相同表: {result['details']['summary']['tables_identical']} 个。"
                f"耗时: {duration}"
            )
        else:
            final_msg = (
                f"数据库完全一致,共 {result['details']['summary']['tables_identical']} 个表完全相同。"
                f"耗时: {duration}"
            )

        add_log(final_msg)
        result["message"] = final_msg

    except pymysql.Error as e:
        error_msg = f"数据库错误: {str(e)}"
        add_log(error_msg, "error")
        result["status"] = "error"
        result["message"] = error_msg
    except Exception as e:
        error_msg = f"系统错误: {str(e)}"
        add_log(error_msg, "error")
        result["status"] = "error"
        result["message"] = error_msg
    finally:
        if db1:
            db1.close()
            add_log("数据库1连接已关闭")
        if db2:
            db2.close()
            add_log("数据库2连接已关闭")

        result["details"]["summary"]["end_time"] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        duration = datetime.now() - start_time
        result["details"]["summary"]["duration"] = str(duration)

    add_log("对比任务结束")
    print(result)
    return result


# 示例用法
if __name__ == "__main__":
    db1_config = {
        'host': 'localhost',
        'port': 3306,
        'user': 'root',
        'password': 'password',
        'database': 'database1'
    }

    db2_config = {
        'host': 'localhost',
        'port': 3306,
        'user': 'root',
        'password': 'password',
        'database': 'database2'
    }

    comparison_result = compare_databases(db1_config, db2_config)
    print("\n对比结果:", comparison_result["message"])
    print("\n详细摘要:")
    for k, v in comparison_result["details"]["summary"].items():
        print(f"{k}: {v}")

    if comparison_result["details"]["identical_tables"]:
        print("\n完全相同表:", comparison_result["details"]["identical_tables"])

api/routes/db_compare_routes.py

javascript 复制代码
from flask import Blueprint, request, jsonify
from api.daos.db_compare_dao import compare_databases

db_compare_bp = Blueprint('db_compare', __name__)


@db_compare_bp.route('/compare', methods=['POST'])
def compare():
    print('jinlaile')
    try:
        data = request.get_json()
        if not data:
            return jsonify({
                "status": "error",
                "message": "No JSON data provided"
            }), 400

        # 提取并验证必要参数
        required_fields = [
            'db1_host', 'db1_user', 'db1_password', 'db1_database',
            'db2_host', 'db2_user', 'db2_password', 'db2_database'
        ]

        missing_fields = [field for field in required_fields if field not in data]
        if missing_fields:
            return jsonify({
                "status": "error",
                "message": f"Missing required fields: {', '.join(missing_fields)}"
            }), 400

        # 构建配置字典
        db1_config = {
            "host": data["db1_host"],
            "user": data["db1_user"],
            "password": data["db1_password"],
            "database": data["db1_database"],
            "port": data.get("db1_port", 3306),
            "charset": data.get("db1_charset", "utf8mb4")
        }

        db2_config = {
            "host": data["db2_host"],
            "user": data["db2_user"],
            "password": data["db2_password"],
            "database": data["db2_database"],
            "port": data.get("db2_port", 3306),
            "charset": data.get("db2_charset", "utf8mb4")
        }

        # 执行比较
        result = compare_databases(db1_config, db2_config)
        return jsonify(result)

    except Exception as e:
        return jsonify({
            "status": "error",
            "message": f"Internal server error: {str(e)}"
        }), 500

api/run.py

javascript 复制代码
from flask import Flask
from flask_cors import CORS
from utils.sqlite_util import init_db
from routes.user_route import bp as user_bp
from routes.db_compare_routes import db_compare_bp   # 新增

app = Flask(__name__)
CORS(app, resources={r"/*": {"origins": "*"}})

app.config.from_object('config.Config')



app.register_blueprint(user_bp, url_prefix='/api')
app.register_blueprint(db_compare_bp, url_prefix='/db')    # 新增

if __name__ == '__main__':
    init_db()
    app.run(debug=True, host='0.0.0.0')

后端启动

前端相关

ui/views/DbCompare.vue

该代码块中的第244行的后端地址,需与上图中后端启动的ip:port保持一致

javascript 复制代码
<template>
  <div class="container">
    <div class="header">
      <h2>数据库对比工具</h2>
      <el-button class="back-button" type="info" @click="goBack" size="small">返回</el-button>
    </div>

    <el-row :gutter="20" class="db-input-row">
      <el-col :span="12">
        <el-card class="db-card">
          <div slot="header" class="clearfix">
            <span>数据库1 (源数据库)</span>
          </div>
          <el-form label-position="top">
            <el-row :gutter="10">
              <el-col :span="16">
                <el-input v-model="db1.host" placeholder="主机地址,例如: 192.168.1.100"></el-input>
              </el-col>
              <el-col :span="8">
                <el-input v-model.number="db1.port" placeholder="端口,默认: 3306" type="number"></el-input>
              </el-col>
            </el-row>
            <el-row :gutter="10" class="form-row">
              <el-col :span="8">
                <el-input v-model="db1.user" placeholder="用户名"></el-input>
              </el-col>
              <el-col :span="8">
                <el-input v-model="db1.password" type="password" placeholder="密码"></el-input>
              </el-col>
              <el-col :span="8">
                <el-input v-model="db1.database" placeholder="数据库名"></el-input>
              </el-col>
            </el-row>
          </el-form>
        </el-card>
      </el-col>

      <el-col :span="12">
        <el-card class="db-card">
          <div slot="header" class="clearfix">
            <span>数据库2 (目标数据库)</span>
          </div>
          <el-form label-position="top">
            <el-row :gutter="10">
              <el-col :span="16">
                <el-input v-model="db2.host" placeholder="主机地址,例如: 192.168.1.101"></el-input>
              </el-col>
              <el-col :span="8">
                <el-input v-model.number="db2.port" placeholder="端口,默认: 3306" type="number"></el-input>
              </el-col>
            </el-row>
            <el-row :gutter="10" class="form-row">
              <el-col :span="8">
                <el-input v-model="db2.user" placeholder="用户名"></el-input>
              </el-col>
              <el-col :span="8">
                <el-input v-model="db2.password" type="password" placeholder="密码"></el-input>
              </el-col>
              <el-col :span="8">
                <el-input v-model="db2.database" placeholder="数据库名"></el-input>
              </el-col>
            </el-row>
          </el-form>
        </el-card>
      </el-col>
    </el-row>

    <div class="button-container">
      <el-button
        type="primary"
        @click="compareDatabases"
        :loading="loading"
        size="large"
      >
        开始对比
      </el-button>
      <el-button
        @click="resetForm"
        size="large"
      >
        重置
      </el-button>
    </div>

    <!-- 结果展示部分保持不变 -->
    <el-card class="result-card">
      <h2>对比结果</h2>

      <div v-if="errorMessage" class="error-message">
        <el-alert :title="errorMessage" type="error" :closable="false" />
      </div>

      <div v-else-if="rawResponse" class="stats-container">
        <el-row :gutter="20">
          <el-col :span="6">
            <el-statistic title="总表数" :value="rawResponse.details.summary.total_tables" />
          </el-col>
          <el-col :span="6">
            <el-statistic title="完全相同表" :value="rawResponse.details.summary.tables_identical" />
          </el-col>
          <el-col :span="6">
            <el-statistic title="结构差异表" :value="rawResponse.details.summary.tables_with_structure_diff" />
          </el-col>
          <el-col :span="6">
            <el-statistic title="数据差异表" :value="rawResponse.details.summary.tables_with_data_diff" />
          </el-col>
        </el-row>

        <div class="time-info">
          <span>开始时间: {{ rawResponse.details.summary.start_time }}</span>
          <span>结束时间: {{ rawResponse.details.summary.end_time }}</span>
          <span>耗时: {{ rawResponse.details.summary.duration }}</span>
        </div>
      </div>

      <el-tabs v-if="rawResponse" type="border-card" class="result-tabs">
        <el-tab-pane label="摘要">
          <div class="summary-message"></div>

          <div v-if="rawResponse.details.identical_tables.length > 0" class="identical-tables">
            <h3>完全相同表 ({{ rawResponse.details.identical_tables.length }}个):</h3>
            <el-tag
              v-for="table in rawResponse.details.identical_tables"
              :key="table"
              type="success"
              class="table-tag"
            >
              {{ table }}
            </el-tag>
          </div>
        </el-tab-pane>

        <el-tab-pane label="结构差异" v-if="rawResponse.details.structure.length > 0">
          <el-collapse v-model="activeCollapse">
            <el-collapse-item
              v-for="table in rawResponse.details.structure"
              :key="table.table"
              :title="`表: ${table.table}`"
              :name="table.table"
            >
              <div v-for="issue in table.issues" :key="issue.field" class="issue-item">
                <el-alert
                  :title="`字段 ${issue.field} 属性不一致: ${issue.message}`"
                  type="warning"
                  :closable="false"
                />
                <div v-if="issue.details" class="issue-details">
                  <p>【{{ db1.database }}】库的表【 {{table.table}}】字段【 {{issue.field}}】属性默认值为: 【{{issue.message.split('(')[1].split(' vs ')[0]}}】</p>
                  <p>【{{ db2.database }}】库的表【{{table.table}}】字段【 {{issue.field}}】属性默认值为: 【{{issue.message.split('(')[1].split(' vs ')[1].split(')')[0]}}】</p>
                </div>
              </div>
            </el-collapse-item>
          </el-collapse>
        </el-tab-pane>

        <el-tab-pane label="数据差异" v-if="rawResponse.details.data.length > 0">
          <el-collapse v-model="activeCollapse">
            <el-collapse-item
              v-for="table in rawResponse.details.data"
              :key="table.table"
              :title="`表: ${table.table}`"
              :name="table.table"
            >
              <div v-for="issue in table.issues" :key="issue.message" class="issue-item">
                <el-alert :title="issue.message" type="error" :closable="false" />
                <div class="data-details">
                  <p>{{ db1.database }} 行数: {{ issue.db1_row_count }}</p>
                  <p>{{ db2.database }} 行数: {{ issue.db2_row_count }}</p>
                </div>
              </div>
            </el-collapse-item>
          </el-collapse>
        </el-tab-pane>

        <el-tab-pane label="完整日志">
          <div class="full-log-container">
            <pre class="full-log-content">{{ rawResponse.logs.join('\n') }}</pre>
          </div>
        </el-tab-pane>
      </el-tabs>

      <div v-else class="empty-result">
        <el-empty description="暂无对比结果" />
      </div>
    </el-card>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
import { ElMessage } from 'element-plus';

const router = useRouter();
const goBack = () => {
  router.push('/dashboard');
};

// 数据库连接信息
const db1 = ref({
  host: '',
  port: 3306,
  user: 'root',
  password: '',
  database: ''
});

const db2 = ref({
  host: '',
  port: 3306,
  user: 'root',
  password: '',
  database: ''
});

const loading = ref(false);
const rawResponse = ref(null);
const errorMessage = ref('');
const activeCollapse = ref([]);

const hasDiscrepancy = computed(() => {
  if (!rawResponse.value) return false;
  return (
    rawResponse.value.details.summary.tables_with_structure_diff > 0 ||
    rawResponse.value.details.summary.tables_with_data_diff > 0 ||
    rawResponse.value.details.summary.tables_missing > 0
  );
});

const compareDatabases = async () => {
  // 验证必填字段
  if (!db1.value.host || !db1.value.database || !db2.value.host || !db2.value.database) {
    ElMessage.error('请填写完整的数据库连接信息');
    return;
  }

  // 确保端口是整数
  const db1Port = parseInt(db1.value.port) || 3306;
  const db2Port = parseInt(db2.value.port) || 3306;

  loading.value = true;
  try {
    const response = await axios.post('http://192.168.1.138:5000/db/compare', {
      db1_host: db1.value.host,
      db1_port: db1Port,
      db1_user: db1.value.user,
      db1_password: db1.value.password,
      db1_database: db1.value.database,
      db2_host: db2.value.host,
      db2_port: db2Port,
      db2_user: db2.value.user,
      db2_password: db2.value.password,
      db2_database: db2.value.database
    });

    if (response.data.status === "error") {
      errorMessage.value = response.data.message;
      ElMessage.error('请求失败:' + response.data.message);
    } else {
      rawResponse.value = response.data;
      errorMessage.value = '';

      if (hasDiscrepancy.value) {
        ElMessage.error('数据库对比发现不一致!');
      } else {
        ElMessage.success('数据库对比完成,所有内容一致');
      }
    }
  } catch (error) {
    ElMessage.error('请求失败:' + error.message);
    errorMessage.value = error.message;
  } finally {
    loading.value = false;
  }
};

const resetForm = () => {
  db1.value = {
    host: '',
    port: 3306,
    user: 'root',
    password: '',
    database: ''
  };
  db2.value = {
    host: '',
    port: 3306,
    user: 'root',
    password: '',
    database: ''
  };
  rawResponse.value = null;
  errorMessage.value = '';
};
</script>

<style scoped>
.container {
  width: 90%;
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.header h2 {
  margin: 0;
}

.back-button {
  background: #42b983 !important;
  color: white !important;
  border: none !important;
  padding: 8px 12px;
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.2s;
}

.back-button:hover {
  background: #3aa876 !important;
}

.db-input-row {
  margin-bottom: 20px;
}

.db-card {
  height: 100%;
}

.form-row {
  margin-top: 10px;
}

.button-container {
  display: flex;
  justify-content: center;
  margin: 20px 0;
  gap: 20px;
}

.result-card {
  margin-top: 20px;
  min-height: 600px;
}

.stats-container {
  margin-bottom: 20px;
  padding: 15px;
  background-color: #f5f7fa;
  border-radius: 4px;
}

.time-info {
  margin-top: 15px;
  display: flex;
  justify-content: space-between;
  font-size: 12px;
  color: #909399;
}

.result-tabs {
  margin-top: 20px;
}

.summary-message {
  margin-bottom: 20px;
}

.identical-tables {
  margin-top: 20px;
}

.table-tag {
  margin: 5px;
}

.issue-item {
  margin-bottom: 10px;
}

.issue-details {
  margin-top: 10px;
}

.data-details {
  margin-top: 10px;
  padding-left: 20px;
}

.empty-result {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 300px;
}

.full-log-container {
  max-height: 500px;
  overflow-y: auto;
  background-color: #f5f5f5;
  border-radius: 4px;
  padding: 10px;
}

.full-log-content {
  font-family: monospace;
  white-space: pre-wrap;
  margin: 0;
  line-height: 1.5;
  color: #333;
}

.error-message {
  margin-bottom: 20px;
}

.clearfix {
  clear: both;
}

:deep(.el-input__inner) {
  text-align: left !important;
}
</style>

ui/router/index.js

javascript 复制代码
import DbCompare from '../views/DbCompare.vue';

  { path: '/db_compare', component: DbCompare },

前端启动

cd ui

npm run dev

执行对比

对比结果汇总

对比结果 - 结构差异

对比结果 - 数据差异

对比结果 - 完整日志

声明:

本文中的代码示例由 DeepSeek 生成,经过人工整理和核对后符合工作实际需求。

DeepSeek 是一款强大的AI编程助手,能高效解决技术问题,推荐大家使用!

相关推荐
HashData酷克数据5 分钟前
官宣:Apache Cloudberry (Incubating) 2.0.0 发布!
数据库·开源·apache·cloudberry
秋难降6 分钟前
SQL 索引突然 “罢工”?快来看看为什么
数据库·后端·sql
TDengine (老段)35 分钟前
TDengine 时间函数 TODAY() 用户手册
大数据·数据库·物联网·oracle·时序数据库·tdengine·涛思数据
码界奇点44 分钟前
KingbaseES一体化架构与多层防护体系如何保障企业级数据库的持续稳定与弹性扩展
数据库·架构·可用性测试
悟乙己1 小时前
数据科学家如何更好地展示自己的能力
大数据·数据库·数据科学家
皆过客,揽星河1 小时前
mysql进阶语法(视图)
数据库·sql·mysql·mysql基础语法·mysql进阶语法·视图创建修改删除
tuokuac2 小时前
Redis 的相关文件作用
数据库·redis·缓存
鹧鸪云光伏与储能软件开发3 小时前
投资储能项目能赚多少钱?小程序帮你测算
运维·数据库·小程序·光伏·光伏设计软件·光伏设计
2301_779503765 小时前
MySQL主从同步--主从复制进阶
数据库·mysql
beijingliushao5 小时前
58-正则表达式
数据库·python·mysql·正则表达式