From aee8c58b044a053b5f50fd162433edb4dcc18e49 Mon Sep 17 00:00:00 2001 From: kinghwj Date: Fri, 11 Sep 2026 21:59:31 +0800 Subject: [PATCH] =?UTF-8?q?chore(config):=20=E5=88=87=E6=8D=A2=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E6=95=B0=E6=8D=AE=E6=BA=90=E5=88=B0=E8=BF=9C=E7=A8=8B?= =?UTF-8?q?=20MySQL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api_v2.py | 4 +- app.py | 11 +-- migrate_mysql_to_remote.py | 145 +++++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 migrate_mysql_to_remote.py diff --git a/api_v2.py b/api_v2.py index 3d938e3..ef6b2dd 100644 --- a/api_v2.py +++ b/api_v2.py @@ -38,8 +38,8 @@ WEREAD_API_KEY = os.environ.get("WEREAD_API_KEY", "") WEREAD_SKILL_VERSION = os.environ.get("WEREAD_SKILL_VERSION", "1.0.4") MYSQL_CONFIG = { - "host": os.environ.get("WEREAD_DB_HOST", "127.0.0.1"), - "port": int(os.environ.get("WEREAD_DB_PORT", 3306)), + "host": os.environ.get("WEREAD_DB_HOST", "124.222.38.85"), + "port": int(os.environ.get("WEREAD_DB_PORT", 13306)), "user": os.environ.get("WEREAD_DB_USER", "root"), "password": os.environ.get("WEREAD_DB_PASSWORD", "258963"), "database": os.environ.get("WEREAD_DB_NAME", "weread_dashboard"), diff --git a/app.py b/app.py index 6c75f42..4a99cb2 100644 --- a/app.py +++ b/app.py @@ -51,11 +51,12 @@ app.register_blueprint(v2_bp) # 数据库连接配置 # ====================================================================== MYSQL_CONFIG = { - "host": "127.0.0.1", # MySQL 服务器地址 - "port": 3306, # MySQL 端口 - "user": "root", # 数据库用户名 - "password": "258963", # 数据库密码 - "database": "weread_dashboard", # 数据库名称 + # 默认使用远程 MySQL;仍可通过 WEREAD_DB_* 环境变量覆盖 + "host": os.environ.get("WEREAD_DB_HOST", "124.222.38.85"), + "port": int(os.environ.get("WEREAD_DB_PORT", 13306)), + "user": os.environ.get("WEREAD_DB_USER", "root"), + "password": os.environ.get("WEREAD_DB_PASSWORD", "258963"), + "database": os.environ.get("WEREAD_DB_NAME", "weread_dashboard"), "charset": "utf8mb4", # 字符集(支持完整 Unicode 含 emoji) "cursorclass": pymysql.cursors.DictCursor, # 使用字典游标,方便按字段名取值 } diff --git a/migrate_mysql_to_remote.py b/migrate_mysql_to_remote.py new file mode 100644 index 0000000..1811476 --- /dev/null +++ b/migrate_mysql_to_remote.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +"""将本地 MySQL 数据迁移到远程 MySQL。 + +默认配置: + 源库:127.0.0.1:3306/weread_dashboard + 目标库:124.222.38.85:13306/weread_dashboard + +密码优先从环境变量读取: + SOURCE_DB_PASSWORD / TARGET_DB_PASSWORD + +脚本不会创建数据库或表;请先在目标 MySQL 执行 schema_mysql.sql。 +目标表会被清空后重新写入,因此执行前请确认目标库中没有需要保留的数据。 +""" + +import argparse +import os +import sys +from typing import Dict, Iterable, List, Tuple + +import pymysql + + +def db_config(prefix: str, defaults: Dict[str, object]) -> Dict[str, object]: + return { + "host": os.getenv(f"{prefix}_DB_HOST", defaults["host"]), + "port": int(os.getenv(f"{prefix}_DB_PORT", defaults["port"])), + "user": os.getenv(f"{prefix}_DB_USER", defaults["user"]), + "password": os.getenv(f"{prefix}_DB_PASSWORD", ""), + "database": os.getenv(f"{prefix}_DB_NAME", defaults["database"]), + "charset": "utf8mb4", + "autocommit": False, + } + + +SOURCE = db_config("SOURCE", { + "host": "127.0.0.1", "port": 3306, "user": "root", "database": "weread_dashboard" +}) +TARGET = db_config("TARGET", { + "host": "124.222.38.85", "port": 13306, "user": "root", "database": "weread_dashboard" +}) + + +def q(identifier: str) -> str: + """安全引用 MySQL 标识符。表名和列名均来自数据库元数据。""" + return "`" + identifier.replace("`", "``") + "`" + + +def tables(conn) -> List[str]: + with conn.cursor() as cur: + cur.execute("SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'") + return [row[0] for row in cur.fetchall()] + + +def columns(conn, table: str) -> List[str]: + with conn.cursor() as cur: + cur.execute(f"SHOW COLUMNS FROM {q(table)}") + return [row[0] for row in cur.fetchall()] + + +def row_count(conn, table: str) -> int: + with conn.cursor() as cur: + cur.execute(f"SELECT COUNT(*) FROM {q(table)}") + return int(cur.fetchone()[0]) + + +def migrate_table(source, target, table: str, batch_size: int) -> Tuple[int, int]: + source_cols = columns(source, table) + target_cols = set(columns(target, table)) + common = [col for col in source_cols if col in target_cols] + if not common: + raise RuntimeError(f"表 {table} 没有可写入的同名列") + + col_sql = ", ".join(q(col) for col in common) + placeholders = ", ".join(["%s"] * len(common)) + insert_sql = f"INSERT INTO {q(table)} ({col_sql}) VALUES ({placeholders})" + + copied = 0 + with source.cursor() as sc, target.cursor() as tc: + sc.execute(f"SELECT {col_sql} FROM {q(table)}") + while True: + batch = sc.fetchmany(batch_size) + if not batch: + break + tc.executemany(insert_sql, batch) + copied += len(batch) + return copied, len(source_cols) - len(common) + + +def migrate(batch_size: int, only: Iterable[str] = ()) -> None: + source = pymysql.connect(**SOURCE) + target = pymysql.connect(**TARGET) + try: + source_tables = tables(source) + target_tables = set(tables(target)) + selected = list(only) if only else source_tables + missing = [t for t in selected if t not in target_tables] + if missing: + raise RuntimeError("目标库缺少表:" + ", ".join(missing)) + + print(f"源库: {SOURCE['host']}:{SOURCE['port']}/{SOURCE['database']}") + print(f"目标库: {TARGET['host']}:{TARGET['port']}/{TARGET['database']}") + print(f"迁移表数: {len(selected)},批大小: {batch_size}\n") + + with target.cursor() as cur: + cur.execute("SET FOREIGN_KEY_CHECKS = 0") + for table in selected: + before = row_count(source, table) + cur.execute(f"TRUNCATE TABLE {q(table)}") + copied, skipped_cols = migrate_table(source, target, table, batch_size) + target.commit() + after = row_count(target, table) + note = f",跳过源表独有列 {skipped_cols} 个" if skipped_cols else "" + status = "✓" if before == after == copied else "!" + print(f"{status} {table:32s} 源 {before:8d} / 目标 {after:8d}{note}") + cur.execute("SET FOREIGN_KEY_CHECKS = 1") + target.commit() + except Exception: + target.rollback() + raise + finally: + source.close() + target.close() + + +def main() -> int: + parser = argparse.ArgumentParser(description="本地 MySQL → 远程 MySQL 数据迁移") + parser.add_argument("--batch-size", type=int, default=1000) + parser.add_argument("--tables", nargs="*", help="只迁移指定表;不传则迁移源库全部表") + args = parser.parse_args() + if args.batch_size < 1: + parser.error("--batch-size 必须大于 0") + try: + migrate(args.batch_size, args.tables or ()) + print("\n迁移完成。") + return 0 + except pymysql.MySQLError as exc: + print(f"MySQL 错误:{exc}", file=sys.stderr) + return 2 + except Exception as exc: + print(f"迁移失败:{exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main())