| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- 从 temp_staff_data 读取数据并更新 sys_user 表
- 逻辑:
- 1. 对于 temp_staff_data 中每一行,使用 nickname = 姓名 AND account_type = '1' 在 sys_user 中查找唯一记录
- 2. 若找到唯一记录:
- - 计算 user_no = zero_pad( sys_user.user_name, 6 ),并写回到 sys_user.user_no
- - 将 sys_user.user_name 和 sys_user.phonenumber 都更新为 temp_staff_data 中的手机号字段值
- 3. 若未找到或找到多条记录,跳过并记录日志
- 用法:
- python update_sys_user_from_temp.py --db-config ../学校历史数据迁移/db_config.py --dry-run
- """
- from __future__ import annotations
- import os
- import sys
- import argparse
- import importlib.util
- from typing import Optional, Sequence
- import logging
- import pandas as pd
- from sqlalchemy import create_engine, text
- LOG = logging.getLogger("update_sys_user")
- LOG.setLevel(logging.INFO)
- ch = logging.StreamHandler()
- ch.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
- LOG.addHandler(ch)
- def load_db_config_from_file(config_path: str) -> dict:
- spec = importlib.util.spec_from_file_location("db_config", config_path)
- module = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(module) # type: ignore
- if not hasattr(module, "DB_CONFIG"):
- raise RuntimeError(f"No DB_CONFIG found in {config_path}")
- return getattr(module, "DB_CONFIG")
- def build_engine_from_config(db_config: dict):
- user = db_config.get("user")
- password = db_config.get("password")
- host = db_config.get("host", "localhost")
- port = db_config.get("port", 3306)
- database = db_config.get("database")
- charset = db_config.get("charset", "utf8mb4")
- uri = f"mysql+pymysql://{user}:{password}@{host}:{port}/{database}?charset={charset}"
- return create_engine(uri, pool_pre_ping=True)
- def pick_column_name(possible_names: Sequence[str], available: Sequence[str]) -> Optional[str]:
- """从可能的列名集合中挑选 temp_staff_data 中存在的一项"""
- for name in possible_names:
- if name in available:
- return name
- return None
- def zero_pad_to_six(value) -> str:
- """将给定值转换为字符串并左侧补0到6位(保留原始字符串的数字内容优先)"""
- if value is None:
- return ""
- s = str(value).strip()
- # 尝试提取整数部分
- try:
- # 支持像 '00123' 或 '123' 的情况
- num = int(s)
- return str(num).zfill(6)
- except Exception:
- # 若不能转为整数,直接用原始字符串左补0(或裁切)
- return s.zfill(6)[-6:]
- def main(db_config_path: str, table_name: str = "temp_staff_data", dry_run: bool = True):
- base_dir = os.path.dirname(os.path.abspath(__file__))
- if not os.path.isabs(db_config_path):
- # 相对路径从当前脚本目录解析
- db_config_path = os.path.join(base_dir, db_config_path)
- if not os.path.exists(db_config_path):
- LOG.error("找不到 db_config 文件: %s", db_config_path)
- sys.exit(1)
- db_config = load_db_config_from_file(db_config_path)
- engine = build_engine_from_config(db_config)
- # 使用事务(begin)确保更新会被提交
- with engine.begin() as conn:
- # 读取表头,确定列名映射
- res = conn.execute(text(f"SELECT * FROM `{table_name}` LIMIT 1"))
- cols = list(res.keys())
- LOG.info("temp_staff_data 可用列: %s", cols)
- name_col = pick_column_name(["姓名", "name"], cols)
- phone_col = pick_column_name(["手机号", "手机号_常用的一个_", "phone", "phonenumber"], cols)
- if not name_col or not phone_col:
- LOG.error("无法在 %s 中找到 必需 列 (姓名/手机号)。请检查表结构。", table_name)
- return
- # 读取所有数据(若数据量巨大可以改为分批处理)
- df = pd.read_sql(text(f"SELECT * FROM `{table_name}`"), conn)
- LOG.info("读取到 %d 条 temp_staff_data 记录", len(df))
- updated = 0
- skipped_no_user = 0
- skipped_multi = 0
- errors = 0
- for _, row in df.iterrows():
- staff_name = row.get(name_col)
- phone = row.get(phone_col)
- if pd.isna(staff_name) or staff_name is None:
- LOG.warning("跳过一行,姓名为空")
- skipped_no_user += 1
- continue
- staff_name_str = str(staff_name).strip()
- # 查找 sys_user
- try:
- users = conn.execute(
- text("SELECT user_id, user_name FROM sys_user WHERE nick_name = :nick AND account_type = '1'"),
- {"nick": staff_name_str},
- ).fetchall()
- except Exception as e:
- LOG.exception("查询 sys_user 失败: %s", e)
- errors += 1
- continue
- if len(users) == 0:
- LOG.info("未找到 sys_user (nickname=%s, account_type=1),跳过", staff_name_str)
- skipped_no_user += 1
- continue
- if len(users) > 1:
- LOG.warning("找到多条 sys_user (nickname=%s, account_type=1),跳过以避免歧义", staff_name_str)
- skipped_multi += 1
- continue
- user_row = users[0]
- user_id = user_row[0]
- current_user_name = user_row[1]
- # 计算 user_no 并更新 user_name & phonenumber
- user_no = zero_pad_to_six(current_user_name)
- new_user_name = "" if phone is None or (isinstance(phone, float) and pd.isna(phone)) else str(phone).strip()
- new_phonenumber = new_user_name
- LOG.info("准备更新 sys_user.id=%s: user_no=%s, user_name=%s, phonenumber=%s",
- user_id, user_no, new_user_name, new_phonenumber)
- if dry_run:
- LOG.info("dry-run 模式,未执行写入")
- updated += 1
- continue
- try:
- conn.execute(
- text("UPDATE sys_user SET user_no = :user_no, user_name = :user_name, phonenumber = :phone WHERE user_id = :id"),
- {"user_no": user_no, "user_name": new_user_name, "phone": new_phonenumber, "id": user_id},
- )
- updated += 1
- except Exception as e:
- LOG.exception("更新 sys_user.id=%s 失败: %s", user_id, e)
- errors += 1
- LOG.info("处理完成,统计: updated=%d, skipped_no_user=%d, skipped_multi=%d, errors=%d",
- updated, skipped_no_user, skipped_multi, errors)
- if __name__ == "__main__":
- parser = argparse.ArgumentParser(description="从 temp_staff_data 更新 sys_user")
- parser.add_argument("--db-config", type=str, default="../学校历史数据迁移/db_config.py", help="数据库配置文件路径(python模块含 DB_CONFIG)")
- parser.add_argument("--table", type=str, default="temp_staff_data", help="临时表名")
- parser.add_argument("--dry-run", action="store_true", help="仅打印将要执行的操作,不写入数据库")
- args = parser.parse_args()
- main(db_config_path=args.db_config, table_name=args.table, dry_run=args.dry_run)
|