164 lines
6.0 KiB
Python
164 lines
6.0 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""从 binlog 解码文件中恢复被误删的数据(DELETE → INSERT)"""
|
|||
|
|
import re
|
|||
|
|
import asyncio
|
|||
|
|
import asyncmy
|
|||
|
|
|
|||
|
|
BINLOG_FILE = "/tmp/binlog_decode.sql"
|
|||
|
|
|
|||
|
|
# 只恢复这些表(非政策/活动/任务/岗位/社区内容)
|
|||
|
|
RECOVER_TABLES = {
|
|||
|
|
"opc_services", "service_orders", "service_views", "service_referrals",
|
|||
|
|
"service_providers", "service_favorites", "service_inquiries", "service_reviews",
|
|||
|
|
"market_items", "market_categories", "market_purchase_orders", "market_purchases",
|
|||
|
|
"finance_records", "settlement_logs", "invoices", "contracts", "payment_bindings",
|
|||
|
|
"compute_recharge_orders", "compute_recharges", "compute_usage_records",
|
|||
|
|
"compute_balance_allocations", "investment_intents", "investor_preferences",
|
|||
|
|
"courses", "training_courses", "training_chapters", "training_lessons",
|
|||
|
|
"training_enrollments", "training_course_enrollments", "training_lesson_progress",
|
|||
|
|
"training_certificates", "skill_tests", "certifications", "certification_types",
|
|||
|
|
"opc_certifications",
|
|||
|
|
"credit_ledger", "credit_scores", "credit_dimensions", "credit_levels",
|
|||
|
|
"credit_rules", "badges", "user_badges",
|
|||
|
|
"messages", "notifications", "announcements", "survey_logs", "plan_logs",
|
|||
|
|
"invite_records", "ratings",
|
|||
|
|
"park_admissions", "company_apply_requests", "company_claim_requests",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def parse_binlog(path):
|
|||
|
|
"""解析解码后的 binlog,返回 {table: [row_dict, ...]}"""
|
|||
|
|
results = {}
|
|||
|
|
current_table = None
|
|||
|
|
current_row = {}
|
|||
|
|
in_where = False
|
|||
|
|
|
|||
|
|
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|||
|
|
for line in f:
|
|||
|
|
line = line.strip()
|
|||
|
|
# 匹配 DELETE FROM
|
|||
|
|
m = re.match(r"### DELETE FROM `opc`\.`(\w+)`", line)
|
|||
|
|
if m:
|
|||
|
|
# 保存上一行
|
|||
|
|
if current_table and current_row:
|
|||
|
|
results.setdefault(current_table, []).append(current_row)
|
|||
|
|
current_table = m.group(1)
|
|||
|
|
current_row = {}
|
|||
|
|
in_where = False
|
|||
|
|
continue
|
|||
|
|
if line == "### WHERE":
|
|||
|
|
in_where = True
|
|||
|
|
continue
|
|||
|
|
if in_where and line.startswith("### @"):
|
|||
|
|
# 解析 @N=value
|
|||
|
|
m2 = re.match(r"### @(\d+)=(.*)", line)
|
|||
|
|
if m2:
|
|||
|
|
idx = int(m2.group(1))
|
|||
|
|
val = m2.group(2)
|
|||
|
|
# 处理 NULL
|
|||
|
|
if val == "NULL":
|
|||
|
|
val = None
|
|||
|
|
else:
|
|||
|
|
# 去掉引号(简单处理)
|
|||
|
|
if val.startswith("'") and val.endswith("'"):
|
|||
|
|
val = val[1:-1]
|
|||
|
|
# 处理转义
|
|||
|
|
val = val.replace("\\'", "'").replace("\\\\", "\\")
|
|||
|
|
current_row[idx] = val
|
|||
|
|
continue
|
|||
|
|
# 遇到其他行,结束当前事件
|
|||
|
|
if current_table and current_row and not line.startswith("###"):
|
|||
|
|
results.setdefault(current_table, []).append(current_row)
|
|||
|
|
current_table = None
|
|||
|
|
current_row = {}
|
|||
|
|
in_where = False
|
|||
|
|
|
|||
|
|
# 最后一个
|
|||
|
|
if current_table and current_row:
|
|||
|
|
results.setdefault(current_table, []).append(current_row)
|
|||
|
|
|
|||
|
|
return results
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def get_columns(conn, table):
|
|||
|
|
"""获取表的列名顺序"""
|
|||
|
|
async with conn.cursor() as cur:
|
|||
|
|
await cur.execute(
|
|||
|
|
"""SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
|
|||
|
|
WHERE TABLE_SCHEMA='opc' AND TABLE_NAME=%s ORDER BY ORDINAL_POSITION""",
|
|||
|
|
(table,))
|
|||
|
|
return [r[0] for r in await cur.fetchall()]
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def main():
|
|||
|
|
print("解析 binlog...")
|
|||
|
|
data = parse_binlog(BINLOG_FILE)
|
|||
|
|
print(f"解析到 {len(data)} 个表的删除事件")
|
|||
|
|
|
|||
|
|
conn = await asyncmy.connect(
|
|||
|
|
host="47.108.226.213", port=8091,
|
|||
|
|
user="root", password="sjnxhyjashaywiuwhaja", database="opc")
|
|||
|
|
|
|||
|
|
total_recovered = 0
|
|||
|
|
for table, rows in data.items():
|
|||
|
|
if table not in RECOVER_TABLES:
|
|||
|
|
continue
|
|||
|
|
if not rows:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
cols = await get_columns(conn, table)
|
|||
|
|
if not cols:
|
|||
|
|
print(f" ⚠ {table}: 无法获取列名")
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 过滤掉已经存在的记录(通过主键判断)
|
|||
|
|
# 先查现有主键
|
|||
|
|
pk_col = cols[0] # 假设第一列是主键
|
|||
|
|
existing_ids = set()
|
|||
|
|
try:
|
|||
|
|
async with conn.cursor() as cur:
|
|||
|
|
await cur.execute(f"SELECT `{pk_col}` FROM `{table}`")
|
|||
|
|
existing_ids = {str(r[0]) for r in await cur.fetchall()}
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
inserted = 0
|
|||
|
|
skipped = 0
|
|||
|
|
async with conn.cursor() as cur:
|
|||
|
|
for row in rows:
|
|||
|
|
# 按列索引映射
|
|||
|
|
values = []
|
|||
|
|
for i, col in enumerate(cols, 1):
|
|||
|
|
val = row.get(i)
|
|||
|
|
values.append(val)
|
|||
|
|
|
|||
|
|
# 跳过已存在的
|
|||
|
|
pk_val = str(values[0]) if values[0] is not None else None
|
|||
|
|
if pk_val and pk_val in existing_ids:
|
|||
|
|
skipped += 1
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
placeholders = ",".join(["%s"] * len(cols))
|
|||
|
|
col_names = ",".join([f"`{c}`" for c in cols])
|
|||
|
|
try:
|
|||
|
|
await cur.execute(
|
|||
|
|
f"INSERT INTO `{table}` ({col_names}) VALUES ({placeholders})",
|
|||
|
|
values)
|
|||
|
|
inserted += 1
|
|||
|
|
if pk_val:
|
|||
|
|
existing_ids.add(pk_val)
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f" ✗ {table}: {e}")
|
|||
|
|
skipped += 1
|
|||
|
|
|
|||
|
|
await conn.commit()
|
|||
|
|
total_recovered += inserted
|
|||
|
|
print(f" ✓ {table}: 恢复 {inserted} 条(跳过 {skipped} 条已存在)")
|
|||
|
|
|
|||
|
|
await conn.ensure_closed()
|
|||
|
|
print(f"\n总计恢复: {total_recovered} 条")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
asyncio.run(main())
|