update_sys_user_from_temp.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 从 temp_staff_data 读取数据并更新 sys_user 表
  5. 逻辑:
  6. 1. 对于 temp_staff_data 中每一行,使用 nickname = 姓名 AND account_type = '1' 在 sys_user 中查找唯一记录
  7. 2. 若找到唯一记录:
  8. - 计算 user_no = zero_pad( sys_user.user_name, 6 ),并写回到 sys_user.user_no
  9. - 将 sys_user.user_name 和 sys_user.phonenumber 都更新为 temp_staff_data 中的手机号字段值
  10. 3. 若未找到或找到多条记录,跳过并记录日志
  11. 用法:
  12. python update_sys_user_from_temp.py --db-config ../学校历史数据迁移/db_config.py --dry-run
  13. """
  14. from __future__ import annotations
  15. import os
  16. import sys
  17. import argparse
  18. import importlib.util
  19. from typing import Optional, Sequence
  20. import logging
  21. import pandas as pd
  22. from sqlalchemy import create_engine, text
  23. LOG = logging.getLogger("update_sys_user")
  24. LOG.setLevel(logging.INFO)
  25. ch = logging.StreamHandler()
  26. ch.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
  27. LOG.addHandler(ch)
  28. def load_db_config_from_file(config_path: str) -> dict:
  29. spec = importlib.util.spec_from_file_location("db_config", config_path)
  30. module = importlib.util.module_from_spec(spec)
  31. spec.loader.exec_module(module) # type: ignore
  32. if not hasattr(module, "DB_CONFIG"):
  33. raise RuntimeError(f"No DB_CONFIG found in {config_path}")
  34. return getattr(module, "DB_CONFIG")
  35. def build_engine_from_config(db_config: dict):
  36. user = db_config.get("user")
  37. password = db_config.get("password")
  38. host = db_config.get("host", "localhost")
  39. port = db_config.get("port", 3306)
  40. database = db_config.get("database")
  41. charset = db_config.get("charset", "utf8mb4")
  42. uri = f"mysql+pymysql://{user}:{password}@{host}:{port}/{database}?charset={charset}"
  43. return create_engine(uri, pool_pre_ping=True)
  44. def pick_column_name(possible_names: Sequence[str], available: Sequence[str]) -> Optional[str]:
  45. """从可能的列名集合中挑选 temp_staff_data 中存在的一项"""
  46. for name in possible_names:
  47. if name in available:
  48. return name
  49. return None
  50. def zero_pad_to_six(value) -> str:
  51. """将给定值转换为字符串并左侧补0到6位(保留原始字符串的数字内容优先)"""
  52. if value is None:
  53. return ""
  54. s = str(value).strip()
  55. # 尝试提取整数部分
  56. try:
  57. # 支持像 '00123' 或 '123' 的情况
  58. num = int(s)
  59. return str(num).zfill(6)
  60. except Exception:
  61. # 若不能转为整数,直接用原始字符串左补0(或裁切)
  62. return s.zfill(6)[-6:]
  63. def main(db_config_path: str, table_name: str = "temp_staff_data", dry_run: bool = True):
  64. base_dir = os.path.dirname(os.path.abspath(__file__))
  65. if not os.path.isabs(db_config_path):
  66. # 相对路径从当前脚本目录解析
  67. db_config_path = os.path.join(base_dir, db_config_path)
  68. if not os.path.exists(db_config_path):
  69. LOG.error("找不到 db_config 文件: %s", db_config_path)
  70. sys.exit(1)
  71. db_config = load_db_config_from_file(db_config_path)
  72. engine = build_engine_from_config(db_config)
  73. # 使用事务(begin)确保更新会被提交
  74. with engine.begin() as conn:
  75. # 读取表头,确定列名映射
  76. res = conn.execute(text(f"SELECT * FROM `{table_name}` LIMIT 1"))
  77. cols = list(res.keys())
  78. LOG.info("temp_staff_data 可用列: %s", cols)
  79. name_col = pick_column_name(["姓名", "name"], cols)
  80. phone_col = pick_column_name(["手机号", "手机号_常用的一个_", "phone", "phonenumber"], cols)
  81. if not name_col or not phone_col:
  82. LOG.error("无法在 %s 中找到 必需 列 (姓名/手机号)。请检查表结构。", table_name)
  83. return
  84. # 读取所有数据(若数据量巨大可以改为分批处理)
  85. df = pd.read_sql(text(f"SELECT * FROM `{table_name}`"), conn)
  86. LOG.info("读取到 %d 条 temp_staff_data 记录", len(df))
  87. updated = 0
  88. skipped_no_user = 0
  89. skipped_multi = 0
  90. errors = 0
  91. for _, row in df.iterrows():
  92. staff_name = row.get(name_col)
  93. phone = row.get(phone_col)
  94. if pd.isna(staff_name) or staff_name is None:
  95. LOG.warning("跳过一行,姓名为空")
  96. skipped_no_user += 1
  97. continue
  98. staff_name_str = str(staff_name).strip()
  99. # 查找 sys_user
  100. try:
  101. users = conn.execute(
  102. text("SELECT user_id, user_name FROM sys_user WHERE nick_name = :nick AND account_type = '1'"),
  103. {"nick": staff_name_str},
  104. ).fetchall()
  105. except Exception as e:
  106. LOG.exception("查询 sys_user 失败: %s", e)
  107. errors += 1
  108. continue
  109. if len(users) == 0:
  110. LOG.info("未找到 sys_user (nickname=%s, account_type=1),跳过", staff_name_str)
  111. skipped_no_user += 1
  112. continue
  113. if len(users) > 1:
  114. LOG.warning("找到多条 sys_user (nickname=%s, account_type=1),跳过以避免歧义", staff_name_str)
  115. skipped_multi += 1
  116. continue
  117. user_row = users[0]
  118. user_id = user_row[0]
  119. current_user_name = user_row[1]
  120. # 计算 user_no 并更新 user_name & phonenumber
  121. user_no = zero_pad_to_six(current_user_name)
  122. new_user_name = "" if phone is None or (isinstance(phone, float) and pd.isna(phone)) else str(phone).strip()
  123. new_phonenumber = new_user_name
  124. LOG.info("准备更新 sys_user.id=%s: user_no=%s, user_name=%s, phonenumber=%s",
  125. user_id, user_no, new_user_name, new_phonenumber)
  126. if dry_run:
  127. LOG.info("dry-run 模式,未执行写入")
  128. updated += 1
  129. continue
  130. try:
  131. conn.execute(
  132. text("UPDATE sys_user SET user_no = :user_no, user_name = :user_name, phonenumber = :phone WHERE user_id = :id"),
  133. {"user_no": user_no, "user_name": new_user_name, "phone": new_phonenumber, "id": user_id},
  134. )
  135. updated += 1
  136. except Exception as e:
  137. LOG.exception("更新 sys_user.id=%s 失败: %s", user_id, e)
  138. errors += 1
  139. LOG.info("处理完成,统计: updated=%d, skipped_no_user=%d, skipped_multi=%d, errors=%d",
  140. updated, skipped_no_user, skipped_multi, errors)
  141. if __name__ == "__main__":
  142. parser = argparse.ArgumentParser(description="从 temp_staff_data 更新 sys_user")
  143. parser.add_argument("--db-config", type=str, default="../学校历史数据迁移/db_config.py", help="数据库配置文件路径(python模块含 DB_CONFIG)")
  144. parser.add_argument("--table", type=str, default="temp_staff_data", help="临时表名")
  145. parser.add_argument("--dry-run", action="store_true", help="仅打印将要执行的操作,不写入数据库")
  146. args = parser.parse_args()
  147. main(db_config_path=args.db_config, table_name=args.table, dry_run=args.dry_run)