feat: 数据库备份+平台配置初始化(证书类型/信用规则/等级/徽章/市场分类)
This commit is contained in:
@@ -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")
|
||||
# 逐条执行 INSERT(sqlalchemy text 不支持多语句,按分号拆分)
|
||||
for stmt in sql_text.split(";"):
|
||||
stmt = stmt.strip()
|
||||
if stmt and stmt.upper().startswith("INSERT"):
|
||||
await session.execute(_text(stmt))
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user