feat: 数据库备份+平台配置初始化(证书类型/信用规则/等级/徽章/市场分类)

This commit is contained in:
Pine
2026-09-08 17:59:22 +08:00
parent 8db84f27f1
commit a7100a2a4d
4 changed files with 5592 additions and 0 deletions
+28
View File
@@ -282,6 +282,7 @@ async def seed_data(session: AsyncSession) -> None:
await _seed_org_members(session, now)
await _seed_carrier_park_binding(session, now)
await _seed_system_configs(session, now)
await _seed_platform_config(session, now)
await session.commit()
return
@@ -338,6 +339,7 @@ async def seed_data(session: AsyncSession) -> None:
await _seed_port_pages(session, now)
await _seed_org_members(session, now)
await _seed_carrier_park_binding(session, now)
await _seed_platform_config(session, now)
await session.commit()
@@ -978,3 +980,29 @@ async def _seed_org_members(session: AsyncSession, now: str) -> None:
for org_id, user_id, role, is_admin in members:
session.add(OrganizationMember(org_id=org_id, user_id=user_id, role=role,
is_admin=is_admin, status="active", joined_at=now))
async def _seed_platform_config(session: AsyncSession, now: str) -> None:
"""平台基础配置:证书类型 / 信用规则 / 信用等级 / 信用维度 / 徽章 / 市场分类。
幂等:以 certification_types 表为空作为标记,读取 serverrun/mysql/seed_config.sql 执行。
生产环境首次部署时自动初始化,避免配置缺失导致信用/认证/徽章体系不可用。
"""
from sqlalchemy import text as _text
from pathlib import Path as _Path
# 幂等标记:certification_types 已有数据则跳过
existing = await session.scalar(_text("SELECT COUNT(*) FROM certification_types"))
if existing and existing > 0:
return
sql_file = _Path(__file__).resolve().parents[2] / "serverrun" / "mysql" / "seed_config.sql"
if not sql_file.exists():
return
sql_text = sql_file.read_text(encoding="utf-8")
# 逐条执行 INSERTsqlalchemy text 不支持多语句,按分号拆分)
for stmt in sql_text.split(";"):
stmt = stmt.strip()
if stmt and stmt.upper().startswith("INSERT"):
await session.execute(_text(stmt))
+163
View File
@@ -0,0 +1,163 @@
#!/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())
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long