293 lines
11 KiB
Python
293 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
03-插入老师数据到 sys_user 表
|
||
|
||
功能:
|
||
- 保留 user_id = 1 的用户,其他用户的 del_flag 都改成 '1'(虚拟删除,可交互确认)
|
||
- 从 teacher.xlsx 读取教师信息,插入到 sys_user
|
||
- user_name: 工号
|
||
- nick_name: 姓名
|
||
- phonenumber: 手机号(取前11位数字)
|
||
- sex: 男->'0', 女->'1'(其他为空)
|
||
- password: 使用固定哈希值
|
||
- account_type: '1'
|
||
- dept_id: 通过部门名称关联 sys_dept.dept_id(找不到会写 NULL 并打印警告)
|
||
- 跳过已存在相同 user_name 的记录(可选择覆盖)
|
||
|
||
用法:
|
||
python 03-insert_sys_user.py # 交互式确认虚拟删除
|
||
python 03-insert_sys_user.py --yes # 自动确认虚拟删除
|
||
python 03-insert_sys_user.py --teacher-file /path/to/teacher.xlsx
|
||
python 03-insert_sys_user.py --dry-run # 仅打印将执行的操作,不写库
|
||
"""
|
||
from __future__ import annotations
|
||
import os
|
||
import sys
|
||
import re
|
||
import argparse
|
||
import datetime
|
||
from typing import List, Dict, Any, Optional
|
||
import pandas as pd
|
||
from sqlalchemy import create_engine, text
|
||
import importlib.util
|
||
|
||
|
||
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}"
|
||
engine = create_engine(uri, pool_pre_ping=True)
|
||
return engine
|
||
|
||
|
||
def find_column(df: pd.DataFrame, keywords: List[str]) -> Optional[str]:
|
||
for col in df.columns:
|
||
name = str(col).strip()
|
||
low = name.lower()
|
||
for k in keywords:
|
||
if k in low:
|
||
return name
|
||
return None
|
||
|
||
|
||
def read_teacher_df_from_db(engine, table_name: str = "his_lssj") -> pd.DataFrame:
|
||
"""
|
||
从数据库表 his_lssj 读取教师数据,返回 pandas DataFrame(列名使用表中的列名)。
|
||
假定表中包含列:`工号`, `姓名`, `部门`, `性别`, `手机号码`
|
||
"""
|
||
print(f"从数据库表 `{table_name}` 读取教师数据(假定列:工号, 姓名, 部门, 性别, 手机号码)")
|
||
q = f"SELECT `工号`, `姓名`, `部门`, `性别`, `手机号码` FROM `{table_name}`"
|
||
df = pd.read_sql_query(q, con=engine)
|
||
return df
|
||
|
||
|
||
def normalize_phone(raw: Optional[str]) -> Optional[str]:
|
||
if raw is None:
|
||
return None
|
||
s = str(raw)
|
||
digits = "".join(re.findall(r"\d", s))
|
||
return digits[:11] if digits else None
|
||
|
||
|
||
def map_sex(raw: Optional[str]) -> Optional[str]:
|
||
if raw is None:
|
||
return None
|
||
s = str(raw).strip()
|
||
if s in ("男", "M", "m", "male", "Male"):
|
||
return "0"
|
||
if s in ("女", "F", "f", "female", "Female"):
|
||
return "1"
|
||
return None
|
||
|
||
|
||
def soft_delete_other_users(engine, yes: bool, dry_run: bool = False):
|
||
# Renamed behavior: perform physical DELETE except user_id = 1
|
||
if not yes:
|
||
resp = input("将删除 sys_user 中除 user_id=1 外的所有用户,确认?(yes/no) [no]: ").strip().lower()
|
||
if resp not in ("y", "yes"):
|
||
print("取消删除步骤。")
|
||
return
|
||
q_count = text("SELECT COUNT(1) as c FROM sys_user WHERE user_id != 1")
|
||
with engine.connect() as conn:
|
||
cnt = conn.execute(q_count).scalar()
|
||
print(f"将被删除的用户数:{cnt}")
|
||
if dry_run:
|
||
print("dry-run 模式,跳过删除。")
|
||
return
|
||
q = text("DELETE FROM sys_user WHERE user_id != 1")
|
||
with engine.begin() as conn:
|
||
conn.execute(q)
|
||
print("已删除其他用户(保留 user_id = 1)。")
|
||
|
||
|
||
def generate_user_id(conn):
|
||
r = conn.execute(text("SELECT MAX(user_id) as m FROM sys_user")).fetchone()
|
||
max_id = int(r[0]) if r and r[0] is not None else 0
|
||
return max_id + 1
|
||
|
||
|
||
def get_dept_id(conn, dept_name: str) -> Optional[int]:
|
||
if dept_name is None or str(dept_name).strip() == "":
|
||
return None
|
||
r = conn.execute(text("SELECT dept_id FROM sys_dept WHERE dept_name = :name LIMIT 1"), {"name": dept_name.strip()}).fetchone()
|
||
return int(r[0]) if r and r[0] is not None else None
|
||
|
||
|
||
def user_exists(conn, user_name: str, dept_id: Optional[int]) -> bool:
|
||
# skip only if both user_name and dept_id match an existing record
|
||
q = text(
|
||
"SELECT 1 FROM sys_user WHERE user_name = :u AND "
|
||
"((dept_id = :did) OR (dept_id IS NULL AND :did IS NULL)) LIMIT 1"
|
||
)
|
||
r = conn.execute(q, {"u": user_name, "did": dept_id}).fetchone()
|
||
return bool(r)
|
||
|
||
|
||
def dept_has_users(conn, dept_id: int) -> bool:
|
||
"""检查指定 dept_id 下是否已有用户存在(用于决定是否跳过该部门的批量插入)"""
|
||
if dept_id is None:
|
||
return False
|
||
r = conn.execute(text("SELECT 1 FROM sys_user WHERE dept_id = :did LIMIT 1"), {"did": dept_id}).fetchone()
|
||
return bool(r)
|
||
|
||
|
||
def insert_user(conn, data: Dict[str, Any]):
|
||
cols = ", ".join(f"`{k}`" for k in data.keys())
|
||
vals = ", ".join(f":{k}" for k in data.keys())
|
||
ins = text(f"INSERT INTO sys_user ({cols}) VALUES ({vals})")
|
||
conn.execute(ins, data)
|
||
|
||
|
||
def user_has_role(conn, user_id: int, role_id: int) -> bool:
|
||
r = conn.execute(text("SELECT 1 FROM sys_user_role WHERE user_id = :uid AND role_id = :rid LIMIT 1"), {"uid": user_id, "rid": role_id}).fetchone()
|
||
return bool(r)
|
||
|
||
|
||
def insert_user_role(conn, user_id: int, role_id: int):
|
||
ins = text("INSERT INTO sys_user_role (`user_id`, `role_id`) VALUES (:uid, :rid)")
|
||
conn.execute(ins, {"uid": user_id, "rid": role_id})
|
||
|
||
|
||
def main():
|
||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||
config_path = os.path.join(base_dir, "db_config.py")
|
||
if not os.path.exists(config_path):
|
||
print(f"找不到配置文件: {config_path}")
|
||
sys.exit(1)
|
||
db_config = load_db_config_from_file(config_path)
|
||
engine = build_engine_from_config(db_config)
|
||
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument("--yes", action="store_true", help="自动确认虚拟删除现有用户(除 user_id=1)")
|
||
parser.add_argument("--teacher-file", type=str, default=os.path.join(base_dir, "teacher.xlsx"))
|
||
parser.add_argument("--dry-run", action="store_true", help="只打印将执行的操作,不写入数据库")
|
||
args = parser.parse_args()
|
||
|
||
# 1) 虚拟删除其他用户
|
||
soft_delete_other_users(engine, args.yes, dry_run=args.dry_run)
|
||
|
||
# 2) 从 his_lssj 表读取教师数据(更快)
|
||
df = read_teacher_df_from_db(engine, table_name="his_lssj")
|
||
# 直接使用约定列名(表中创建时已用这些列名)
|
||
col_workno = "工号"
|
||
col_name = "姓名"
|
||
col_phone = "手机号码"
|
||
col_dept = "部门"
|
||
col_sex = "性别"
|
||
# ensure columns exist
|
||
for c in (col_workno, col_name):
|
||
if c not in df.columns:
|
||
print(f"必需列 {c} 在表 his_lssj 中未找到,请检查表结构或使用 Excel 源。")
|
||
return
|
||
|
||
password_hash = "$2a$10$FGldU5BGGq9ONuBmYhxLweFhmbkE45keMKASNZpjETXawE1oGbtZy"
|
||
now_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
|
||
# iterate rows and insert
|
||
inserted = 0
|
||
skipped = 0
|
||
warnings = 0
|
||
# cache per-dept decision to avoid repeated checks: dept_id -> 'skip'|'insert'
|
||
dept_decision: Dict[Optional[int], str] = {}
|
||
with engine.begin() as conn:
|
||
for idx, row in df.iterrows():
|
||
user_name_val = str(row.get(col_workno)).strip() if row.get(col_workno) is not None else ""
|
||
if not user_name_val:
|
||
skipped += 1
|
||
continue
|
||
|
||
# determine department first so existence check can use dept_id
|
||
dept_raw = str(row.get(col_dept)).strip() if col_dept and row.get(col_dept) is not None else ""
|
||
dept_id = get_dept_id(conn, dept_raw) if dept_raw else None
|
||
if dept_raw and not dept_id:
|
||
print(f"警告: 第 {idx+1} 行,部门 '{dept_raw}' 在 sys_dept 中未找到,dept_id 将设为 NULL。")
|
||
warnings += 1
|
||
# If we've decided to skip this department because it already has users, skip adding users under it
|
||
if dept_id in dept_decision and dept_decision[dept_id] == "skip":
|
||
skipped += 1
|
||
continue
|
||
|
||
# First time encountering this dept: if dept already has users, mark skip
|
||
if dept_id not in dept_decision:
|
||
try:
|
||
if dept_has_users(conn, dept_id):
|
||
dept_decision[dept_id] = "skip"
|
||
skipped += 1
|
||
continue
|
||
else:
|
||
dept_decision[dept_id] = "insert"
|
||
except Exception:
|
||
dept_decision[dept_id] = "insert"
|
||
|
||
if user_exists(conn, user_name_val, dept_id):
|
||
skipped += 1
|
||
continue
|
||
|
||
nick = str(row.get(col_name)).strip() if row.get(col_name) is not None else ""
|
||
raw_phone = row.get(col_phone) if col_phone else None
|
||
phone = normalize_phone(raw_phone)
|
||
sex_val = map_sex(row.get(col_sex)) if col_sex else None
|
||
|
||
# generate user_id
|
||
new_user_id = generate_user_id(conn)
|
||
|
||
user_record = {
|
||
"user_id": new_user_id,
|
||
"tenant_id": "000000",
|
||
"dept_id": dept_id,
|
||
"user_name": user_name_val,
|
||
"nick_name": nick or user_name_val,
|
||
"user_type": "sys_user",
|
||
"email": None,
|
||
"phonenumber": phone,
|
||
"sex": sex_val or "0",
|
||
"avatar": None,
|
||
"password": password_hash,
|
||
"status": "0",
|
||
"del_flag": "0",
|
||
"login_ip": "",
|
||
"login_date": None,
|
||
"create_dept": 103,
|
||
"create_by": 1,
|
||
"create_time": now_str,
|
||
"update_by": 1,
|
||
"update_time": now_str,
|
||
"remark": None,
|
||
"account_type": "1",
|
||
}
|
||
|
||
if args.dry_run:
|
||
print("DRY:", user_record)
|
||
inserted += 1
|
||
continue
|
||
|
||
insert_user(conn, user_record)
|
||
# assign roles to the new user
|
||
role_ids = [1938120323613728770, 4]
|
||
for rid in role_ids:
|
||
if not user_has_role(conn, new_user_id, rid):
|
||
insert_user_role(conn, new_user_id, rid)
|
||
inserted += 1
|
||
|
||
print(f"完成:插入 {inserted} 条,跳过 {skipped} 条,警告 {warnings} 条。")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
|
||
|