当前位置: 首页 > news >正文

vue3_flask实现mysql数据库对比功能

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

在这里插入图片描述

基础环境请参考

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

代码文件结构变化

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

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'] andcol1['DATA_TYPE'] == col2['DATA_TYPE'] andcol1['IS_NULLABLE'] == col2['IS_NULLABLE'] andstr(col1.get('COLUMN_DEFAULT')) == str(col2.get('COLUMN_DEFAULT')) andcol1.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 differencesdef 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,EXTRAFROM 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 countFROM `{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, 0def 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_msgdb1 = db2 = Nonestart_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()): rowfor row in structure1}db2_fields = {(row['TABLE_NAME'].lower(), row['COLUMN_NAME'].lower()): rowfor 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"] = Falseadd_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"] = Falseadd_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"] = Falseadd_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"] = Falseadd_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"] += 1if table_result["data_issues"]:result["details"]["data"].append({"table": table,"issues": table_result["data_issues"]})result["details"]["summary"]["tables_with_data_diff"] += 1if table_result["is_identical"]:result["details"]["identical_tables"].append(table)result["details"]["summary"]["tables_identical"] += 1else:result["details"]["summary"]["tables_with_differences"] += 1# 计算耗时end_time = datetime.now()duration = end_time - start_timeresult["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 orresult["details"]["summary"]["tables_with_data_diff"] > 0 orresult["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_msgexcept pymysql.Error as e:error_msg = f"数据库错误: {str(e)}"add_log(error_msg, "error")result["status"] = "error"result["message"] = error_msgexcept Exception as e:error_msg = f"系统错误: {str(e)}"add_log(error_msg, "error")result["status"] = "error"result["message"] = error_msgfinally: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_timeresult["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

from flask import Blueprint, request, jsonify
from api.daos.db_compare_dao import compare_databasesdb_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

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行的后端地址,需与上图中后端启动的地址保持一致

<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-buttontype="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-tagv-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-itemv-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-itemv-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

import DbCompare from '../views/DbCompare.vue';{ path: '/db_compare', component: DbCompare },

在这里插入图片描述

前端启动

cd ui
npm run dev

在这里插入图片描述

执行对比

对比结果汇总

在这里插入图片描述

对比结果 - 结构差异

在这里插入图片描述

对比结果 - 数据差异

在这里插入图片描述

对比结果 - 完整日志

在这里插入图片描述

声明:
本文中的代码示例由 DeepSeek 生成,经过人工整理和核对后符合工作实际需求。
DeepSeek 是一款强大的AI编程助手,能高效解决技术问题,推荐大家使用!

相关文章:

vue3_flask实现mysql数据库对比功能

实现对mysql中两个数据库的表、表结构、表数据的对比功能, 效果如下图 基础环境请参考 vue3flasksqlite前后端项目实战 代码文件结构变化 api/ # 后端相关 ├── daos/ │ ├── __init__.py │ └── db_compare_dao.py # 新增 ├── routes/ │ ├── _…...

【数据结构】2-3-1单链表的定义

数据结构知识点合集 知识点 单链表存储结构 优点&#xff1a;不要求大片连续空间&#xff0c;改变容量方便&#xff1b;缺点&#xff1a;不可随机存取&#xff0c;要耗费一定空间存放指针 /*单链表节点定义*/ typedef struct LNode{ElemType data;struct LNode *next; }LNo…...

面试题总结一

第一天 1. 快速排序 public class QuickSort {public static void quickSort(int[] arr, int low, int high) {if (low < high) {// 分区操作&#xff0c;获取基准元素的最终位置int pivotIndex partition(arr, low, high);// 递归排序基准元素左边的部分quickSort(arr, …...

Ubuntu24.04下安装ISPConfig全过程记录

今天在网上看到ISPConfig&#xff0c;觉得不错&#xff0c;刚好手里又有一台没用的VPS,就顺手安装一个玩玩。具体安装步骤如下&#xff1a; 一、配置服务器hosts及hostname 【安装时候需要检查】 使用root账号登录VPS后 先安装vim编辑器&#xff0c;然后编辑hosts&#xff0…...

【NGINX】 -10 keepalived + nginx + httpd 实现的双机热备+ 负载均衡

文章目录 1、主架构图1.1 IP地址规划 2、web服务器操作3、配置nginx服务器的负载均衡4、配置keepalived4.1 master4.1 backup 5、测试双机热备5.1 两台keepalived服务器均开启5.2 模拟master节点故障 1、主架构图 1.1 IP地址规划 服务器IP地址web1192.168.107.193web2192.168.…...

NC016NC017美光固态芯片NC101NC102

NC016NC017美光固态芯片NC101NC102 在存储技术的演进历程中&#xff0c;美光科技的NC016、NC017、NC101与NC102系列固态芯片&#xff0c;凭借其技术创新与市场适应性&#xff0c;成为行业关注的焦点。本文将从技术内核、产品性能、行业动向、应用场景及市场价值五个维度&#…...

C++(22):fstream的一些成员函数

目录 1 遍历读取文件 1.1 eof()方法 2 读取文件大小 2.1 seekg() 2.2 tellg() 2.3 代码实例 3 存取文字 3.1 read() 3.2 write() 3.3 代码实例 3.3.1 存取文字 3.3.2 特殊方法存储 3.3.3 特殊方法读取 4 重载的输入输出 4.1 重载的输出 << 4.2 重载的输…...

【网络】Wireshark练习3 analyse DNS||ICMP and response message

ip.addr 172.16.0.100 && ip.addr 172.16.0.5 && (dns || icmp) 包号 22–31 之所以被选中&#xff0c;是因为在整个抓包文件里&#xff0c;与执行 ping cat.inx251.edu.au 这一事件相关的所有报文&#xff0c;恰好连续出现在第 22 到第 31 条记录中。具体分…...

GBS 8.0服装裁剪计划软件在线试用

1、全新升级内核8.0&#xff0c;分床更合理&#xff0c;铺布床数更少&#xff1b; 2、支持SS AUTONESTER排料引擎切换 3、支持ASTM AAMA及国产CAD&#xff08;如布衣&#xff09;导出的DXF&#xff0c;Prj文件等 4、核心引擎优化 拖料优化 省料优化 5、经实战对比人工&…...

顺 序 表:数 据 存 储 的 “ 有 序 阵 地 ”

顺 序 表&#xff1a;数 据 存 储 的 “ 有 序 阵 地 ” 线 性 表顺 序 表 - - - 顺 序 存 储 结 构顺 序 表 的 操 作 实 现代 码 全 貌 与 功 能 介 绍顺 序 表 的 功 能 说 明代 码 效 果 展 示代 码 详 解SeqList.hSeqList.ctest.c 总 结 &#x1f4bb;作 者 简 介&#xf…...

#Redis黑马点评#(七)实战篇完结

目录 一 达人探店 1 发布探店笔记 2 查看探店笔记 3 点赞功能 ​编辑 4 点赞排行榜&#xff08;top5&#xff09; ​编辑 二 好友关注 1 关注与取关 2 共同关注 3 Feed流实现关注推送 4 实现滚动分页查询 三 附近商店 1 GEO数据结构 2 附近商户搜索功能 四 用户…...

初始C++:类和对象(中)

概述&#xff1a;本篇博客主要介绍类和对象的相关知识。 1. 类的默认成员函数 默认成员函数就是用户没有显示实现&#xff0c;编译器会自动生成的成员函数称为默认成员函数。一个类&#xff0c;在不写任何代码的情况下编译器会默认生成以下六个默认函数&#xff0c;在六个默认…...

Java开发经验——阿里巴巴编码规范实践解析3

摘要 本文深入解析了阿里巴巴编码规范中关于错误码的制定与管理原则&#xff0c;强调错误码应便于快速溯源和沟通标准化&#xff0c;避免过于复杂。介绍了错误码的命名与设计示例&#xff0c;推荐采用模块前缀、错误类型码和业务编号的结构。同时&#xff0c;探讨了项目错误信…...

ChatGPT:OpenAI Codex—一款基于云的软件工程 AI 代理,赋能 ChatGPT,革新软件开发模式

ChatGPT&#xff1a;OpenAI Codex—一款基于云的软件工程 AI 代理&#xff0c;赋能 ChatGPT&#xff0c;革新软件开发模式 导读&#xff1a;2025年5月16日&#xff0c;OpenAI 发布了 Codex&#xff0c;一个基于云的软件工程 AI 代理&#xff0c;它集成在 ChatGPT 中&#xff0c…...

iOS 内存分区

iOS内存分区 文章目录 iOS内存分区前言五大分区static、extern、const关键字比较conststaticextern与.h文件的关系extern引用变量extern声明 static和const联合使用extern和const联合使用 前言 笔者之前学习OC源码的时候,发现对于这里的几个static,extern,const的内容有遗忘,所…...

LWIP的Socket接口

Socket接口简介 类似于文件操作的一种网络连接接口&#xff0c;通常将其称之为“套接字”。lwIP的Socket接口兼容BSD Socket接口&#xff0c;但只实现完整Socket的部分功能 netconn是对RAW的封装 Socket是对netconn的封装 SOCKET结构体 struct sockaddr { u8_t sa_len; /* 长…...

【Linux笔记】——线程同步条件变量与生产者消费者模型的实现

&#x1f525;个人主页&#x1f525;&#xff1a;孤寂大仙V &#x1f308;收录专栏&#x1f308;&#xff1a;Linux &#x1f339;往期回顾&#x1f339;&#xff1a;【Linux笔记】——线程互斥与互斥锁的封装 &#x1f516;流水不争&#xff0c;争的是滔滔不息 一、线程同步的…...

Popeye

概览与定位 Popeye 是由 derailed 团队开源的 Kubernetes 集群资源 “Sanitizer”&#xff0c;它以只读方式扫描集群内的各种资源&#xff08;如 Pod、Service、Ingress、PVC、RBAC 等&#xff09;&#xff0c;并基于社区最佳实践给出问题等级及修复建议&#xff0c;覆盖配置误…...

ES(ES2023/ES14)最新更新内容,及如何减少内耗

截至2023年10月,JavaScript(ECMAScript)的最新版本是 ES2023(ES14)。 ES2023 引入了许多新特性,如findLast、toSorted等,同时优化了性能。通过减少全局变量、避免内存泄漏、优化循环、减少DOM操作、使用Web Workers、懒加载、缓存、高效数据结构和代码压缩,可以显著降低…...

电子数据取证(数字取证)技术全面指南:从基础到实践

为了后续查阅方便&#xff0c;推荐工具先放到前面 推荐工具 数字取证基础工具 综合取证平台 工具名称类型主要功能适用场景EnCase Forensic商业全面的证据获取和分析、强大的搜索能力法律诉讼、企业调查FTK (Forensic Toolkit)商业高性能处理和索引、集成内存分析大规模数据处…...

【通用智能体】Serper API 详解:搜索引擎数据获取的核心工具

Serper API 详解&#xff1a;搜索引擎数据获取的核心工具 一、Serper API 的定义与核心功能二、技术架构与核心优势2.1 技术实现原理2.2 对比传统方案的突破性优势 三、典型应用场景与代码示例3.1 SEO 监控系统3.2 竞品广告分析 四、使用成本与配额策略五、开发者注意事项六、替…...

基于 STM32 的手持式安检金属探测器设计与实现

一、硬件设计:芯片与功能模块选型及接线 1. 主控芯片选型 芯片型号:STM32F103C8T6 核心优势: 32 位 Cortex-M3 内核,主频 72MHz,满足实时数据处理需求64KB Flash+20KB SRAM,支持程序存储与数据缓存丰富外设:2 路 USART、2 路 SPI、1 路 I2C、12 位 ADC,适配多模块通信…...

虚幻引擎5-Unreal Engine笔记之Default Pawn与GamMode、Camera的关系

虚幻引擎5-Unreal Engine笔记之Default Pawn与GamMode、Camera的关系 code review! 文章目录 虚幻引擎5-Unreal Engine笔记之Default Pawn与GamMode、Camera的关系1.Default Pawn与Camera的关系1.1. Default Pawn 是什么&#xff1f;1.2. Default Pawn 的主要组件1.3. Default…...

Spring源码主线全链路拆解:从启动到关闭的完整生命周期

Spring源码主线全链路拆解&#xff1a;从启动到关闭的完整生命周期 一文看懂 Spring 框架从启动到销毁的主线流程&#xff0c;结合原理、源码路径与伪代码三位一体&#xff0c;系统学习 Spring 底层机制。 1. 启动入口与环境准备 原理说明 Spring Boot 应用入口是标准 Java 应…...

飞帆控件:可配置post/get接口

先上链接&#xff1a; post_get_ithttps://fvi.cn/796看一下这个控件的配置&#xff1a; 当 url 有某个 get 参数时&#xff0c;例如某些接口回传的参数。使用这个接口会发生这些&#xff1a; 如果检测到 url 中有该 url 参数则继续执行选择是否从 url 中删除该参数将这个参数…...

Android 自定义悬浮拖动吸附按钮

一个悬浮的拨打电话按钮&#xff0c;使用CardViewImageView可能会出现适配问题&#xff0c;也就是图片显示不全&#xff0c;出现这种问题&#xff0c;就直接替换控件了&#xff0c;因为上述的组合控件没有FloatingActionButton使用方便&#xff0c;还可以有拖动和吸附效果不是更…...

Spring AI Alibaba集成阿里云百炼大模型应用

文章目录 1.准备工作2.引入maven依赖3.application.yml4.调用4.1.非流式调用4.2.流式调用 阿里云百炼推出的智能体应用、工作流应用和智能体编排应用&#xff0c;有效解决了大模型在处理私有领域问题、获取最新信息、遵循固定流程以及自动规划复杂项目等方面的局限&#xff0c;…...

UI-TARS本地部署

UI-TARS本地部署 UI-TARS本地部署 UI-TARS 论文&#xff08;arXiv&#xff09; UI-TARS 官方仓库&#xff1a;包含部署指南、模型下载链接及示例代码。 UI-TARS-Desktop 客户端&#xff1a;支持本地桌面应用的交互控制。 模型部署框架&#xff1a;vLLM本地部署 1.下载项目…...

如何利用内网穿透实现Cursor对私有化部署大模型的跨网络访问实践

文章目录 前言1.安装Ollama2.QwQ-32B模型安装与运行3.Cursor安装与配置4. 简单使用测试5. 调用本地大模型6. 安装内网穿透7. 配置固定公网地址总结 前言 本文主要介绍如何在Windows环境下&#xff0c;使用Cursor接入Ollama启动本地部署的千问qwq-32b大模型实现辅助编程&#x…...

Linux的进程概念

目录 1、冯诺依曼体系结构 2、操作系统(Operating System) 2.1 基本概念 ​编辑 2.2 目的 3、Linux的进程 3.1 基本概念 3.1.1 PCB 3.1.2 struct task_struct 3.1.3 进程的定义 3.2 基本操作 3.2.1 查看进程 3.2.2 初识fork 3.3 进程状态 3.3.1 操作系统的进程状…...

(10)python开发经验

文章目录 1 cp35 cp36什么意思2 找不到pip3 subprocess编码错误4 导出依赖文件包含路径5 使用自己编译的python并且pyinstall打包程序 更多精彩内容&#x1f449;内容导航 &#x1f448;&#x1f449;Qt开发 &#x1f448;&#x1f449;python开发 &#x1f448; 1 cp35 cp36什…...

什么是时间戳?怎么获取?有什么用

时间戳的定义 时间戳&#xff08;Timestamp&#xff09;是指记录某个事件发生的具体时间点&#xff0c;通常以特定的格式表示。它可以精确到秒、毫秒甚至更小的单位&#xff0c;用于标识某个时刻在时间轴上的位置。 获取时间戳的方法 在不同的编程语言中&#xff0c;获取时间…...

Zookeeper 入门(二)

4. Zookeeper 的 ACL 权限控制( Access Control List ) Zookeeper 的ACL 权限控制,可以控制节点的读写操作,保证数据的安全性&#xff0c;Zookeeper ACL 权 限设置分为 3 部分组成&#xff0c;分别是&#xff1a;权限模式&#xff08;Scheme&#xff09;、授权对象&#xff08…...

[创业之路-361]:企业战略管理案例分析-2-战略制定-使命、愿景、价值观的失败案例

一、失败案例 1、使命方面的失败案例 真功夫创业者内乱&#xff1a;真功夫在创业过程中&#xff0c;由于股权结构不合理&#xff0c;共同创始人及公司大股东潘宇海与实际控制人、董事长蔡达标产生管理权矛盾。双方在公司发展方向、管理改革等方面无法达成一致&#xff0c;导致…...

dijkstra算法加训上 之 分层图最短路

来几个分层图的题练习下哈 P4568 [JLOI2011] 飞行路线 P4568 [JLOI2011] 飞行路线 - 洛谷https://www.luogu.com.cn/problem/P4568 题目描述 Alice 和 Bob 现在要乘飞机旅行&#xff0c;他们选择了一家相对便宜的航空公司。该航空公司一共在 n 个城市设有业务&#xff0c;设这…...

赋予AI更强的“思考”能力

刚刚&#xff01;北大校友、OpenAI前安全副总裁Lilian Weng最新博客来了&#xff1a;Why We Think 原文链接&#xff1a;Why We Think by Lilian Weng 这篇文章关注&#xff1a;如何让AI不仅仅是“知道”答案&#xff0c;更能“理解”问题并推导出答案。通过赋予AI更强的“思…...

微服务项目->在线oj系统(Java版 - 1)

相信自己,终会成功 目录 C/S架构与B/S架构 C/S架构&#xff08;Client/Server&#xff0c;客户端/服务器架构&#xff09; 特点&#xff1a; 优点&#xff1a; 缺点&#xff1a; 典型应用&#xff1a; B/S架构&#xff08;Browser/Server&#xff0c;浏览器/服务器架构&a…...

【深度学习】使用块的网络(VGG)

虽然 AlexNet 证明深层神经网络卓有成效&#xff0c;但它没有提供一个通用的模板来指导后续的研究人员设计新的网络。 也就是说尽管我知道了更深更大的网络更有效&#xff0c;但是不清楚怎么让它更深更大&#xff0c;从而起到一个更好的效果。 于是&#xff0c;研究人员开始从单…...

Python数据可视化 - Pyecharts绘图示例

文章目录 一、Pyecharts简介及安装1. Pyecharts简介2. 安装Pyecharts 二、准备数据三、饼图示例1. 初始化选项配置2. 饼图相关设置3. 全局配置项3.1 标题配置项3.2 图例配置项3.3 提示框配置项3.4 工具箱配置项3.5 视觉映射配置项 4. 系列配置项4.1 标签选项配置4.2 图元样式配…...

Day29

复习日 知识点回顾 类的装饰器装饰器思想的进一步理解&#xff1a;外部修改、动态类方法的定义&#xff1a;内部定义和外部定义 作业&#xff1a;复习类和函数的知识点&#xff0c;写下自己过去29天的学习心得&#xff0c;如对函数和类的理解&#xff0c;对python这门工具的理…...

Python列表全面解析:从入门到精通

文章目录 Python列表全面解析&#xff1a;从入门到精通一、列表基础1. 什么是列表&#xff1f;2. 列表特性总结表 二、列表的基本操作(基础)1. 访问元素2. 修改列表 三、列表的常用方法(基础)1. 添加元素的方法2. 删除元素的方法3. 查找和统计方法4. 排序和反转 四、列表的高级…...

Nacos数据写入流程

在 3 节点的 Nacos 集群中&#xff0c;数据写入流程和主节点&#xff08;Leader&#xff09;的角色基于 Nacos 的分布式一致性协议&#xff08;通常使用 Raft 协议&#xff09;来实现。以下以 Markdown 格式详细说明 3 节点 Nacos 集群的数据写入流程以及主节点的角色和确定方式…...

《P4551 最长异或路径》

题目描述 给定一棵 n 个点的带权树&#xff0c;结点下标从 1 开始到 n。寻找树中找两个结点&#xff0c;求最长的异或路径。 异或路径指的是指两个结点之间唯一路径上的所有边权的异或。 输入格式 第一行一个整数 n&#xff0c;表示点数。 接下来 n−1 行&#xff0c;给出…...

Ansible模块——文件属性查看,文件或目录创建和属性修改

ansible.builtin.stat 可以查看文件信息。 选项 类型 默认值 描述 pathstrnull 要检查的文件或目录的完整路径&#xff08;必需&#xff09;。 followboolfalse 如果是符号链接&#xff0c;是否跟随到目标路径上获取其状态。 get_attributesbooltrue 是否返回扩展属性&#…...

【图像生成大模型】Wan2.1:下一代开源大规模视频生成模型

Wan2.1&#xff1a;下一代开源大规模视频生成模型 引言Wan2.1 项目概述核心技术1. 3D 变分自编码器&#xff08;Wan-VAE&#xff09;2. 视频扩散 Transformer&#xff08;Video Diffusion DiT&#xff09;3. 数据处理与清洗 项目运行方式与执行步骤1. 环境准备2. 安装依赖3. 模…...

AGI大模型(25):LangChain提示词模版

我们也可以创建prompt template, 并引入一些变量到prompt template中,这样在应用的时候更加灵活。 1 代码实现 # 我们也可以创建prompt template, 并引入一些变量到prompt template中,这样在应用的时候更加灵活 from langchain_core.prompts import ChatPromptTemplate from…...

mybatis中的resultMap的association及collectio的使用

目录 1.reusltmap的说明 2.association的使用 3.collection的使用 4.总结 1.reusltmap的说明 resultmap定义了数据库的结果映射到java对象的规则&#xff0c;resultmap包含4个属性&#xff1a; id: ResultMap 的唯一标识 type: 映射到的 Java 类型&#xff08;全限定类名或…...

静态网站部署:如何通过GitHub免费部署一个静态网站

GitHub提供的免费静态网站托管服务可以无需担心昂贵的服务器费用和复杂的设置步骤&#xff0c;本篇文章中将一步步解如何通过GitHub免费部署一个静态网站&#xff0c;帮助大家将创意和作品快速展现给世界。 目录 了解基础情况 创建基础站点 在线调试站点 前端项目部署 部署…...

Android 手写签名功能详解:从原理到实践

Android 手写签名功能详解 1. 引言2. 手写签名核心实现&#xff1a;SignatureView 类3. 交互层实现&#xff1a;MainActivity 类4. 布局与配置5. 性能优化与扩展方向 1. 引言 在电子政务、金融服务等移动应用场景中&#xff0c;手写签名功能已成为提升用户体验与业务合规性的关…...

【iOS(swift)笔记-9】WKWebView无法访问网络

对于iOS 在info中添加App Transport Security Settings&#xff0c;然后在App Transport Security Settings里添加Allow Arbitrary Loadstrue 对于macOS 除了上面的操作&#xff0c;还需在项目信息的App Sandbox里有个Network打钩选项...