add banbanmini backend

This commit is contained in:
HycJack
2026-03-24 15:04:36 +08:00
parent 0d0f995dc2
commit 7510ca6df1
197 changed files with 13008 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
import os
import sys
import asyncio
import aiomysql
import subprocess
import datetime
import gzip
import shutil
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from config import settings
BACKUP_DIR = os.path.join(Path(__file__).parent.parent, "backups")
os.makedirs(BACKUP_DIR, exist_ok=True)
async def backup_database(name=None, compress=True):
"""备份MySQL数据库"""
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
if name:
backup_name = f"{name}_{timestamp}"
else:
backup_name = f"backup_{timestamp}"
output_file = os.path.join(BACKUP_DIR, f"{backup_name}.sql")
cmd = [
"mysqldump",
"-h", settings.db_host,
"-P", str(settings.db_port),
"-u", settings.db_user,
f"-p{settings.db_password}",
"--single-transaction",
"--routines",
"--triggers",
"--events",
settings.db_name
]
try:
print(f"开始备份数据库...")
with open(output_file, 'w') as f:
process = subprocess.Popen(
cmd,
stdout=f,
stderr=subprocess.PIPE
)
_, stderr = process.communicate()
if process.returncode != 0:
print(f"备份失败: {stderr.decode()}")
if os.path.exists(output_file):
os.remove(output_file)
return False
if compress:
compressed_file = f"{output_file}.gz"
print(f"压缩备份文件...")
with open(output_file, 'rb') as f_in:
with gzip.open(compressed_file, 'wb') as f_out:
shutil.copyfileobj(f_in, f_out)
os.remove(output_file) # 删除未压缩的SQL文件
output_file = compressed_file
file_size = os.path.getsize(output_file) / (1024 * 1024) # 转换为MB
print(f"备份完成: {output_file} ({file_size:.2f} MB)")
return output_file
except Exception as e:
print(f"备份过程中出错: {e}")
if os.path.exists(output_file):
os.remove(output_file)
return False
async def list_backups():
"""列出所有可用的备份文件"""
if not os.path.exists(BACKUP_DIR):
return []
backups = [f for f in os.listdir(BACKUP_DIR) if f.endswith('.sql') or f.endswith('.sql.gz')]
backups.sort(reverse=True) # 按文件名排序,最新的在前面
return backups
async def cleanup_old_backups(keep=10):
"""清理旧备份,仅保留指定数量的最新备份"""
backups = await list_backups()
if len(backups) <= keep:
print(f"当前共有 {len(backups)} 个备份,未超过保留数量 {keep},不需要清理")
return
to_delete = backups[keep:]
print(f"将删除 {len(to_delete)} 个旧备份,保留 {keep} 个最新备份")
for backup in to_delete:
try:
file_path = os.path.join(BACKUP_DIR, backup)
os.remove(file_path)
print(f"已删除: {backup}")
except Exception as e:
print(f"删除备份 {backup} 时出错: {e}")

View File

@@ -0,0 +1,385 @@
import os
import sys
import asyncio
from pathlib import Path
import aiomysql
import datetime
import inspect
sys.path.insert(0, str(Path(__file__).parent.parent))
from config import settings
from database.models import Base
from sqlalchemy import inspect as sqlalchemy_inspect
from utils.logger import session_logger
async def get_mysql_connection():
"""建立MySQL数据库连接"""
try:
conn = await aiomysql.connect(
host=settings.db_host,
port=settings.db_port,
user=settings.db_user,
password=settings.db_password,
db=settings.db_name,
autocommit=False
)
return conn
except Exception as e:
print(f"数据库连接失败: {e}")
return None
async def get_all_tables(conn):
"""获取数据库中所有表"""
async with conn.cursor() as cursor:
await cursor.execute("SHOW TABLES")
tables = await cursor.fetchall()
return [table[0] for table in tables]
async def get_model_tables():
"""获取模型定义的所有表"""
return [table.__tablename__ for table in Base.__subclasses__()]
async def get_table_columns(conn, table_name):
"""获取表的所有列"""
async with conn.cursor() as cursor:
await cursor.execute(f"SHOW COLUMNS FROM `{table_name}`")
columns = await cursor.fetchall()
return [column[0] for column in columns]
async def get_model_columns(model_class):
"""获取模型定义的所有列"""
inspector = sqlalchemy_inspect(model_class)
return [column.key for column in inspector.mapper.column_attrs]
async def identify_unused_tables(conn):
"""识别未使用的表"""
db_tables = await get_all_tables(conn)
model_tables = await get_model_tables()
unused_tables = [table for table in db_tables if table not in model_tables]
return unused_tables
async def identify_unused_columns(conn):
"""识别未使用的列"""
results = {}
model_dict = {model.__tablename__: model for model in Base.__subclasses__()}
for table_name, model_class in model_dict.items():
try:
db_columns = await get_table_columns(conn, table_name)
model_columns = await get_model_columns(model_class)
unused_columns = [col for col in db_columns if col not in model_columns]
if unused_columns:
results[table_name] = unused_columns
except Exception as e:
print(f"检查表 {table_name} 列时出错: {e}")
return results
async def get_table_foreign_keys(conn, table_name):
"""获取表的外键约束"""
async with conn.cursor() as cursor:
query = """
SELECT
CONSTRAINT_NAME,
TABLE_NAME,
COLUMN_NAME,
REFERENCED_TABLE_NAME,
REFERENCED_COLUMN_NAME
FROM
INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE
REFERENCED_TABLE_NAME IS NOT NULL
AND TABLE_SCHEMA = %s
AND TABLE_NAME = %s
"""
await cursor.execute(query, (settings.db_name, table_name))
foreign_keys = await cursor.fetchall()
return foreign_keys
async def get_referenced_tables(conn, table_name):
"""获取引用指定表的所有表"""
async with conn.cursor() as cursor:
query = """
SELECT
TABLE_NAME,
CONSTRAINT_NAME
FROM
INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE
REFERENCED_TABLE_NAME = %s
AND TABLE_SCHEMA = %s
"""
await cursor.execute(query, (table_name, settings.db_name))
references = await cursor.fetchall()
if references:
print(f"{table_name} 被以下表引用:")
for ref in references:
print(f" - {ref[0]} (约束: {ref[1]})")
return references
async def disable_foreign_key_checks(conn):
"""临时禁用外键约束检查"""
async with conn.cursor() as cursor:
await cursor.execute("SET FOREIGN_KEY_CHECKS = 0")
print("已临时禁用外键约束检查")
async def enable_foreign_key_checks(conn):
"""重新启用外键约束检查"""
async with conn.cursor() as cursor:
await cursor.execute("SET FOREIGN_KEY_CHECKS = 1")
print("已重新启用外键约束检查")
async def sort_tables_for_deletion(conn, tables):
"""对表进行排序,以确保先删除引用者,再删除被引用者"""
dependency_graph = {}
for table in tables:
references = await get_referenced_tables(conn, table)
dependency_graph[table] = [ref[0] for ref in references if ref[0] in tables]
result = []
visited = set()
temp_mark = set()
def visit(node):
if node in temp_mark:
return
if node not in visited:
temp_mark.add(node)
for dependent in dependency_graph.get(node, []):
visit(dependent)
temp_mark.remove(node)
visited.add(node)
result.append(node)
for table in tables:
if table not in visited:
visit(table)
return result[::-1]
async def drop_tables(conn, tables, dry_run=True):
"""删除未使用的表"""
if not tables:
return True
if not dry_run:
try:
print("禁用外键约束检查以确保安全删除所有表...")
await disable_foreign_key_checks(conn)
for table in tables:
try:
async with conn.cursor() as cursor:
await cursor.execute(f"DROP TABLE IF EXISTS `{table}`")
print(f"已删除表: {table}")
except Exception as e:
print(f"删除表 {table} 时出错: {e}")
await conn.rollback()
await enable_foreign_key_checks(conn)
return False
await conn.commit()
await enable_foreign_key_checks(conn)
return True
except Exception as e:
print(f"删除表时出错: {e}")
try:
await enable_foreign_key_checks(conn)
await conn.rollback()
except:
pass
return False
else:
for table in tables:
print(f"将删除表: {table}")
return True
async def drop_columns(conn, column_dict, dry_run=True):
"""删除未使用的列"""
for table, columns in column_dict.items():
try:
for column in columns:
if dry_run:
print(f"将从表 {table} 删除列: {column}")
else:
async with conn.cursor() as cursor:
await cursor.execute(f"ALTER TABLE `{table}` DROP COLUMN `{column}`")
print(f"已从表 {table} 删除列: {column}")
except Exception as e:
print(f"从表 {table} 删除列时出错: {e}")
if not dry_run:
await conn.rollback()
return False
if not dry_run:
await conn.commit()
return True
async def cleanup_database(dry_run=True):
"""清理数据库中未使用的表和列"""
conn = await get_mysql_connection()
if not conn:
return False
try:
unused_tables = await identify_unused_tables(conn)
unused_columns = await identify_unused_columns(conn)
print("\n=== 数据库清理报告 ===")
if unused_tables:
print(f"\n发现 {len(unused_tables)} 个未使用的表:")
for table in unused_tables:
print(f" - {table}")
if not dry_run:
success = await drop_tables(conn, unused_tables, dry_run=False)
if not success:
print("删除表失败,已回滚操作")
return False
else:
print("\n未发现任何未使用的表")
if unused_columns:
total_columns = sum(len(cols) for cols in unused_columns.values())
print(f"\n发现 {total_columns} 个未使用的列:")
for table, columns in unused_columns.items():
for column in columns:
print(f" - {table}.{column}")
if not dry_run:
success = await drop_columns(conn, unused_columns, dry_run=False)
if not success:
print("删除列失败,已回滚操作")
return False
else:
print("\n未发现任何未使用的列")
if dry_run:
print("\n这是一次模拟运行,未执行任何实际更改。要执行更改,请使用 --execute 选项。")
else:
print("\n数据库清理成功完成!")
return True
except Exception as e:
print(f"清理数据库时发生错误: {e}")
return False
finally:
conn.close()
async def remove_orphaned_records(dry_run=True):
"""删除孤立记录(有外键但对应的父记录不存在)"""
conn = await get_mysql_connection()
if not conn:
return False
try:
async with conn.cursor() as cursor:
await cursor.execute("""
SELECT COUNT(*) FROM conversation_messages
WHERE conversation_id NOT IN (SELECT id FROM conversation_histories)
""")
count = (await cursor.fetchone())[0]
if count > 0:
print(f"\n发现 {count} 条孤立的会话消息记录")
if not dry_run:
await cursor.execute("""
DELETE FROM conversation_messages
WHERE conversation_id NOT IN (SELECT id FROM conversation_histories)
""")
print(f"已删除 {count} 条孤立的会话消息记录")
else:
print("\n未发现孤立的会话消息记录")
async with conn.cursor() as cursor:
await cursor.execute("""
SELECT COUNT(*) FROM role_languages
WHERE role_id NOT IN (SELECT id FROM roles)
""")
count = (await cursor.fetchone())[0]
if count > 0:
print(f"\n发现 {count} 条孤立的角色语言记录")
if not dry_run:
await cursor.execute("""
DELETE FROM role_languages
WHERE role_id NOT IN (SELECT id FROM roles)
""")
print(f"已删除 {count} 条孤立的角色语言记录")
else:
print("\n未发现孤立的角色语言记录")
if not dry_run:
await conn.commit()
return True
except Exception as e:
print(f"清理孤立记录时发生错误: {e}")
if not dry_run:
await conn.rollback()
return False
finally:
conn.close()
async def drop_specific_tables(conn, tables_to_drop, dry_run=True):
"""删除指定的表,无论它们是否被使用"""
if dry_run:
print(f"\n将删除以下指定表:")
for table in tables_to_drop:
print(f" - {table}")
return True
try:
await disable_foreign_key_checks(conn)
for table in tables_to_drop:
try:
async with conn.cursor() as cursor:
await cursor.execute(f"DROP TABLE IF EXISTS `{table}`")
print(f"已删除表: {table}")
except Exception as e:
print(f"删除表 {table} 时出错: {e}")
await conn.commit()
print(f"指定表已成功删除")
return True
except Exception as e:
await conn.rollback()
print(f"删除指定表时出错: {e}")
return False
finally:
await enable_foreign_key_checks(conn)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="数据库清理工具")
parser.add_argument('--execute', action='store_true', help='执行实际更改(不使用此选项则只进行模拟运行)')
parser.add_argument('--drop-tables', type=str, help='指定要删除的表,用逗号分隔')
args = parser.parse_args()
dry_run = not args.execute
async def main():
if args.drop_tables:
conn = await get_mysql_connection()
if not conn:
return False
try:
tables_to_drop = [t.strip() for t in args.drop_tables.split(',')]
await drop_specific_tables(conn, tables_to_drop, dry_run)
finally:
conn.close()
else:
result = await cleanup_database(dry_run)
if result:
await remove_orphaned_records(dry_run)
asyncio.run(main())

View File

@@ -0,0 +1,92 @@
import os
import sys
import asyncio
import argparse
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from scripts.backup_db import backup_database, list_backups, cleanup_old_backups, BACKUP_DIR
from scripts.restore_db import restore_database, interactive_restore
from scripts.cleanup_db import cleanup_database, remove_orphaned_records
async def main():
parser = argparse.ArgumentParser(description='数据库备份和恢复工具')
subparsers = parser.add_subparsers(dest='command', help='子命令')
backup_parser = subparsers.add_parser('backup', help='备份数据库')
backup_parser.add_argument('--name', type=str, help='备份名称,不含扩展名')
backup_parser.add_argument('--no-compress', action='store_true', help='不压缩备份文件')
backup_parser.add_argument('--keep', type=int, default=10, help='保留的备份数量')
restore_parser = subparsers.add_parser('restore', help='恢复数据库')
restore_parser.add_argument('--file', type=str, help='要恢复的备份文件')
list_parser = subparsers.add_parser('list', help='列出所有备份')
cleanup_parser = subparsers.add_parser('cleanup', help='清理旧备份')
cleanup_parser.add_argument('--keep', type=int, default=10, help='保留的备份数量')
clean_db_parser = subparsers.add_parser('clean-db', help='清理数据库中未使用的表和字段')
clean_db_parser.add_argument('--execute', action='store_true', help='执行实际清理(不使用此选项则只进行模拟运行)')
clean_db_parser.add_argument('--orphaned', action='store_true', help='仅清理孤立记录')
clean_db_parser.add_argument('--schema', action='store_true', help='仅清理未使用的表和字段')
clean_db_parser.add_argument('--backup-first', action='store_true', help='清理前先进行备份')
clean_db_parser.add_argument('--drop-tables', type=str, help='指定要删除的表,用逗号分隔')
args = parser.parse_args()
if args.command == 'backup':
await backup_database(args.name, not args.no_compress if hasattr(args, 'no_compress') else True)
await cleanup_old_backups(args.keep if hasattr(args, 'keep') else 10)
elif args.command == 'restore':
if args.file:
await restore_database(args.file)
else:
await interactive_restore()
elif args.command == 'list':
backups = await list_backups()
if backups:
print("\n可用的备份文件:")
for backup in backups:
file_path = os.path.join(BACKUP_DIR, backup)
size = os.path.getsize(file_path) / (1024 * 1024) # 转换为MB
print(f"{backup} ({size:.2f} MB)")
else:
print("没有找到可用的备份")
elif args.command == 'cleanup':
keep = args.keep if hasattr(args, 'keep') else 10
await cleanup_old_backups(keep)
elif args.command == 'clean-db':
dry_run = not args.execute
if args.backup_first:
print("清理前进行数据库备份...")
await backup_database(f"pre_cleanup_{int(asyncio.get_event_loop().time())}", True)
if args.drop_tables:
from scripts.cleanup_db import get_mysql_connection, drop_specific_tables
conn = await get_mysql_connection()
if conn:
try:
tables_to_drop = [t.strip() for t in args.drop_tables.split(',')]
await drop_specific_tables(conn, tables_to_drop, dry_run)
finally:
conn.close()
else:
if not args.orphaned:
print("\n分析数据库架构...")
await cleanup_database(dry_run)
if not args.schema:
print("\n检查孤立记录...")
await remove_orphaned_records(dry_run)
if dry_run:
print("\n这是一次模拟运行,没有执行任何实际更改。")
print("要执行实际的清理操作,请使用 --execute 选项。")
else:
parser.print_help()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,109 @@
device_id,serial_number
talkingQ_B0EC1F2,3b4c5d6e7f
talkingQ_94B7164,3b4c5d6e7f
talkingQ_4C2B7F9,0a1b2c3d4e
talkingQ_f91D6b8,5a9b8d3f7c
talkingQ_1a2b7C3,f6a1c5e9d8
talkingQ_7D3c1F4,8b9c7a2f1b
talkingQ_2C8f3D6,4e9b0d7c1a
talkingQ_0b6F4D3,a3b9c5d7e8
talkingQ_9A7b4d1,b2f3a9d6c7
talkingQ_5B8a1C9,f0d6c3b2a1
talkingQ_7F3d2B6,c5d1e8f9b6
talkingQ_8D1f5c7,6b9d0a3c5f
talkingQ_4F1C3b9,a7e9f2c4d0
talkingQ_2d4b6F7,8c5a1b3d9e
talkingQ_7A9b4F2,e3c6d9f8b7
talkingQ_1f6D4B2,d9b1a7c5e0
talkingQ_5b1f7D9,3a4c2e8b5f
talkingQ_8B4C3d9,f7a6c1d4e9
talkingQ_0F7a6B4,2c9d5f3b7a
talkingQ_6c5d2F8,a1b9c7d3f5
talkingQ_3B1f7d5,9c2a4f8e6d
talkingQ_2A7b9d4,3f6b1e9c8a
talkingQ_9c3d8B7,5f1a2d6e4c
talkingQ_7f4B3C1,9a5e7b3f0d
talkingQ_B4B7164,3b4c5d6e7f
talkingQ_E05AF3A,3b4c5d6e7f
talkingQ_A8B7164,3b4c5d6e7f
talkingQ_4d7f2B9,8c1b9e3f7a
talkingQ_C4B7164,3b4c5d6e7f
talkingQ_C0B7164,3b4c5d6e7f
talkingQ_F4B7164,3b4c5d6e7f
talkingQ_D0B7164,3b4c5d6e7f
talkingQ_F0B7164,3b4c5d6e7f
talkingQ_ACB7164,3b4c5d6e7f
talkingQ_E8B7164,3b4c5d6e7f
talkingQ_6B1F9a3,5d7c2e0a9f
talkingQ_3A7d5F4,c9b0e6d2a4
talkingQ_5B9d2A7,1e4f3c6a9b
talkingQ_2d3F1B6,c4e9a5b7d2
talkingQ_9A6b5d2,7c1a4f9e0b
talkingQ_1A2B3C4,5d6e7f8a9b
talkingQ_5D6E7F8,1a2b3c4d5e
talkingQ_7G8H9I0,6b7c8d9e0f
talkingQ_2J3K4L5,8f9g0h1j2k
talkingQ_9M0N1O2,3l4m5n6o7p
talkingQ_3P4Q5R6,9q0r1s2t3u
talkingQ_6S7T8U9,4v5w6x7y8z
talkingQ_4V5W6X7,0a1b2c3d4e
talkingQ_8Y9Z0A1,5f6g7h8i9j
talkingQ_5B6C7D8,0k1l2m3n4o
talkingQ_3E4F5G6,7p8q9r0s1t
talkingQ_7H8I9J0,2u3v4w5x6y
talkingQ_1K2L3M4,8z9a0b1c2d
talkingQ_4N5O6P7,3e4f5g6h7i
talkingQ_6Q7R8S9,9j0k1l2m3n
talkingQ_2T3U4V5,4o5p6q7r8s
talkingQ_9W0X1Y2,0t1u2v3w4x
talkingQ_5Z6A7B8,5y6z7a8b9c
talkingQ_3C4D5E6,0d1e2f3g4h
talkingQ_7F8G9H0,6i7j8k9l0m
talkingQ_1I2J3K4,1n2o3p4q5r
talkingQ_4L5M6N7,7s8t9u0v1w
talkingQ_6O7P8Q9,2x3y4z5a6b
talkingQ_2R3S4T5,8c9d0e1f2g
talkingQ_9U0V1W2,3h4i5j6k7l
talkingQ_5X6Y7Z8,9m0n1o2p3q
talkingQ_3A4B5C6,4r5s6t7u8v
talkingQ_7D8E9F0,0w1x2y3z4a
talkingQ_1G2H3I4,5b6c7d8e9f
talkingQ_4J5K6L7,0g1h2i3j4k
talkingQ_6M7N8O9,6l7m8n9o0p
talkingQ_2P3Q4R5,1q2r3s4t5u
talkingQ_9S0T1U2,7v8w9x0y1z
talkingQ_5V6W7X8,2a3b4c5d6e
talkingQ_3Y4Z5A6,8f9g0h1i2j
talkingQ_7B8C9D0,3k4l5m6n7o
talkingQ_1E2F3G4,9p0q1r2s3t
talkingQ_4H5I6J7,4u5v6w7x8y
talkingQ_6K7L8M9,0z1a2b3c4d
talkingQ_2N3O4P5,5e6f7g8h9i
talkingQ_9Q0R1S2,0j1k2l3m4n
talkingQ_5T6U7V8,6o7p8q9r0s
talkingQ_3W4X5Y6,1t2u3v4w5x
talkingQ_7Z8A9B0,7y8z9a0b1c
talkingQ_1C2D3E4,2d3e4f5g6h
talkingQ_4F5G6H7,8i9j0k1l2m
talkingQ_6I7J8K9,3n4o5p6q7r
talkingQ_2L3M4N5,9s0t1u2v3w
talkingQ_9O0P1Q2,4x5y6z7a8b
talkingQ_5R6S7T8,0c1d2e3f4g
talkingQ_8B9D3E4,a1b2c3d4e5
talkingQ_2A4F7C8,f6e5d4c3b2
talkingQ_1E3F5G7,d8c7b9a0f1
talkingQ_4G7H9K1,c2b3a4d5e6
talkingQ_5F2D4A8,b7c8d9e1f0
talkingQ_7J3L5O6,e1f2d3c4b5
talkingQ_3C8B7E4,d9a6f5b2c3
talkingQ_9A1D3F5,e4b6c7d8a9
talkingQ_6B5C2D4,a7e8f9b0c1
talkingQ_4G8H3J7,d2f5c9b4a0
talkingQ_D485583,3b4c5d6e7f
talkingQ_8C85583,3b4c5d6e7f
talkingQ_4486583,3b4c5d6e7f
talkingQ_68A81F2,3b4c5d6e7f
talkingQ_70A81F2,3b4c5d6e7f
talkingQ_7885583,3b4c5d6e7f
talkingQ_7886583,3b4c5d6e7f
talkingQ_C8A71F2,3b4c5d6e7f
1 device_id serial_number
2 talkingQ_B0EC1F2 3b4c5d6e7f
3 talkingQ_94B7164 3b4c5d6e7f
4 talkingQ_4C2B7F9 0a1b2c3d4e
5 talkingQ_f91D6b8 5a9b8d3f7c
6 talkingQ_1a2b7C3 f6a1c5e9d8
7 talkingQ_7D3c1F4 8b9c7a2f1b
8 talkingQ_2C8f3D6 4e9b0d7c1a
9 talkingQ_0b6F4D3 a3b9c5d7e8
10 talkingQ_9A7b4d1 b2f3a9d6c7
11 talkingQ_5B8a1C9 f0d6c3b2a1
12 talkingQ_7F3d2B6 c5d1e8f9b6
13 talkingQ_8D1f5c7 6b9d0a3c5f
14 talkingQ_4F1C3b9 a7e9f2c4d0
15 talkingQ_2d4b6F7 8c5a1b3d9e
16 talkingQ_7A9b4F2 e3c6d9f8b7
17 talkingQ_1f6D4B2 d9b1a7c5e0
18 talkingQ_5b1f7D9 3a4c2e8b5f
19 talkingQ_8B4C3d9 f7a6c1d4e9
20 talkingQ_0F7a6B4 2c9d5f3b7a
21 talkingQ_6c5d2F8 a1b9c7d3f5
22 talkingQ_3B1f7d5 9c2a4f8e6d
23 talkingQ_2A7b9d4 3f6b1e9c8a
24 talkingQ_9c3d8B7 5f1a2d6e4c
25 talkingQ_7f4B3C1 9a5e7b3f0d
26 talkingQ_B4B7164 3b4c5d6e7f
27 talkingQ_E05AF3A 3b4c5d6e7f
28 talkingQ_A8B7164 3b4c5d6e7f
29 talkingQ_4d7f2B9 8c1b9e3f7a
30 talkingQ_C4B7164 3b4c5d6e7f
31 talkingQ_C0B7164 3b4c5d6e7f
32 talkingQ_F4B7164 3b4c5d6e7f
33 talkingQ_D0B7164 3b4c5d6e7f
34 talkingQ_F0B7164 3b4c5d6e7f
35 talkingQ_ACB7164 3b4c5d6e7f
36 talkingQ_E8B7164 3b4c5d6e7f
37 talkingQ_6B1F9a3 5d7c2e0a9f
38 talkingQ_3A7d5F4 c9b0e6d2a4
39 talkingQ_5B9d2A7 1e4f3c6a9b
40 talkingQ_2d3F1B6 c4e9a5b7d2
41 talkingQ_9A6b5d2 7c1a4f9e0b
42 talkingQ_1A2B3C4 5d6e7f8a9b
43 talkingQ_5D6E7F8 1a2b3c4d5e
44 talkingQ_7G8H9I0 6b7c8d9e0f
45 talkingQ_2J3K4L5 8f9g0h1j2k
46 talkingQ_9M0N1O2 3l4m5n6o7p
47 talkingQ_3P4Q5R6 9q0r1s2t3u
48 talkingQ_6S7T8U9 4v5w6x7y8z
49 talkingQ_4V5W6X7 0a1b2c3d4e
50 talkingQ_8Y9Z0A1 5f6g7h8i9j
51 talkingQ_5B6C7D8 0k1l2m3n4o
52 talkingQ_3E4F5G6 7p8q9r0s1t
53 talkingQ_7H8I9J0 2u3v4w5x6y
54 talkingQ_1K2L3M4 8z9a0b1c2d
55 talkingQ_4N5O6P7 3e4f5g6h7i
56 talkingQ_6Q7R8S9 9j0k1l2m3n
57 talkingQ_2T3U4V5 4o5p6q7r8s
58 talkingQ_9W0X1Y2 0t1u2v3w4x
59 talkingQ_5Z6A7B8 5y6z7a8b9c
60 talkingQ_3C4D5E6 0d1e2f3g4h
61 talkingQ_7F8G9H0 6i7j8k9l0m
62 talkingQ_1I2J3K4 1n2o3p4q5r
63 talkingQ_4L5M6N7 7s8t9u0v1w
64 talkingQ_6O7P8Q9 2x3y4z5a6b
65 talkingQ_2R3S4T5 8c9d0e1f2g
66 talkingQ_9U0V1W2 3h4i5j6k7l
67 talkingQ_5X6Y7Z8 9m0n1o2p3q
68 talkingQ_3A4B5C6 4r5s6t7u8v
69 talkingQ_7D8E9F0 0w1x2y3z4a
70 talkingQ_1G2H3I4 5b6c7d8e9f
71 talkingQ_4J5K6L7 0g1h2i3j4k
72 talkingQ_6M7N8O9 6l7m8n9o0p
73 talkingQ_2P3Q4R5 1q2r3s4t5u
74 talkingQ_9S0T1U2 7v8w9x0y1z
75 talkingQ_5V6W7X8 2a3b4c5d6e
76 talkingQ_3Y4Z5A6 8f9g0h1i2j
77 talkingQ_7B8C9D0 3k4l5m6n7o
78 talkingQ_1E2F3G4 9p0q1r2s3t
79 talkingQ_4H5I6J7 4u5v6w7x8y
80 talkingQ_6K7L8M9 0z1a2b3c4d
81 talkingQ_2N3O4P5 5e6f7g8h9i
82 talkingQ_9Q0R1S2 0j1k2l3m4n
83 talkingQ_5T6U7V8 6o7p8q9r0s
84 talkingQ_3W4X5Y6 1t2u3v4w5x
85 talkingQ_7Z8A9B0 7y8z9a0b1c
86 talkingQ_1C2D3E4 2d3e4f5g6h
87 talkingQ_4F5G6H7 8i9j0k1l2m
88 talkingQ_6I7J8K9 3n4o5p6q7r
89 talkingQ_2L3M4N5 9s0t1u2v3w
90 talkingQ_9O0P1Q2 4x5y6z7a8b
91 talkingQ_5R6S7T8 0c1d2e3f4g
92 talkingQ_8B9D3E4 a1b2c3d4e5
93 talkingQ_2A4F7C8 f6e5d4c3b2
94 talkingQ_1E3F5G7 d8c7b9a0f1
95 talkingQ_4G7H9K1 c2b3a4d5e6
96 talkingQ_5F2D4A8 b7c8d9e1f0
97 talkingQ_7J3L5O6 e1f2d3c4b5
98 talkingQ_3C8B7E4 d9a6f5b2c3
99 talkingQ_9A1D3F5 e4b6c7d8a9
100 talkingQ_6B5C2D4 a7e8f9b0c1
101 talkingQ_4G8H3J7 d2f5c9b4a0
102 talkingQ_D485583 3b4c5d6e7f
103 talkingQ_8C85583 3b4c5d6e7f
104 talkingQ_4486583 3b4c5d6e7f
105 talkingQ_68A81F2 3b4c5d6e7f
106 talkingQ_70A81F2 3b4c5d6e7f
107 talkingQ_7885583 3b4c5d6e7f
108 talkingQ_7886583 3b4c5d6e7f
109 talkingQ_C8A71F2 3b4c5d6e7f

View File

@@ -0,0 +1,112 @@
import os
import sys
import asyncio
import subprocess
import gzip
import tempfile
import shutil
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from config import settings
from scripts.backup_db import list_backups, BACKUP_DIR
async def restore_database(backup_file):
"""从备份文件恢复数据库"""
file_path = backup_file
if not os.path.isabs(backup_file):
file_path = os.path.join(BACKUP_DIR, backup_file)
if not os.path.exists(file_path):
print(f"错误: 备份文件 '{file_path}' 不存在")
return False
confirm = input(f"警告: 这将覆盖当前数据库的所有数据。确定要继续吗? (y/N): ")
if confirm.lower() != 'y':
print("操作已取消")
return False
temp_file = None
sql_file = file_path
try:
if file_path.endswith('.gz'):
print("解压备份文件...")
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.sql')
temp_file.close()
with gzip.open(file_path, 'rb') as f_in:
with open(temp_file.name, 'wb') as f_out:
shutil.copyfileobj(f_in, f_out)
sql_file = temp_file.name
cmd = [
"mysql",
"-h", settings.db_host,
"-P", str(settings.db_port),
"-u", settings.db_user,
f"-p{settings.db_password}",
settings.db_name
]
print(f"开始恢复数据库...")
with open(sql_file, 'r') as f:
process = subprocess.Popen(
cmd,
stdin=f,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout, stderr = process.communicate()
if process.returncode != 0:
print(f"恢复失败: {stderr.decode()}")
return False
print("数据库恢复成功!")
return True
except Exception as e:
print(f"恢复过程中出错: {e}")
return False
finally:
if temp_file and os.path.exists(temp_file.name):
os.unlink(temp_file.name)
async def interactive_restore():
"""交互式选择要恢复的备份文件"""
backups = await list_backups()
if not backups:
print("没有找到可用的备份文件")
return False
print("\n可用的备份文件:")
for i, backup in enumerate(backups):
file_path = os.path.join(BACKUP_DIR, backup)
size = os.path.getsize(file_path) / (1024 * 1024) # 转换为MB
print(f"{i+1}. {backup} ({size:.2f} MB)")
try:
choice = int(input("\n请选择要恢复的备份文件 (输入序号): "))
if 1 <= choice <= len(backups):
selected_backup = backups[choice-1]
return await restore_database(selected_backup)
else:
print("无效的选择")
return False
except ValueError:
print("无效的输入")
return False
async def main():
if len(sys.argv) > 1:
await restore_database(sys.argv[1])
else:
await interactive_restore()
if __name__ == "__main__":
asyncio.run(main())