03-insert_sys_user.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 03-插入老师数据到 sys_user 表
  5. 功能:
  6. - 保留 user_id = 1 的用户,其他用户的 del_flag 都改成 '1'(虚拟删除,可交互确认)
  7. - 从 teacher.xlsx 读取教师信息,插入到 sys_user
  8. - user_name: 工号
  9. - nick_name: 姓名
  10. - phonenumber: 手机号(取前11位数字)
  11. - sex: 男->'0', 女->'1'(其他为空)
  12. - password: 使用固定哈希值
  13. - account_type: '1'
  14. - dept_id: 通过部门名称关联 sys_dept.dept_id(找不到会写 NULL 并打印警告)
  15. - 跳过已存在相同 user_name 的记录(可选择覆盖)
  16. 用法:
  17. python 03-insert_sys_user.py # 交互式确认虚拟删除
  18. python 03-insert_sys_user.py --yes # 自动确认虚拟删除
  19. python 03-insert_sys_user.py --teacher-file /path/to/teacher.xlsx
  20. python 03-insert_sys_user.py --dry-run # 仅打印将执行的操作,不写库
  21. """
  22. from __future__ import annotations
  23. import os
  24. import sys
  25. import re
  26. import argparse
  27. import datetime
  28. from typing import List, Dict, Any, Optional
  29. import pandas as pd
  30. from sqlalchemy import create_engine, text
  31. import importlib.util
  32. def load_db_config_from_file(config_path: str) -> dict:
  33. spec = importlib.util.spec_from_file_location("db_config", config_path)
  34. module = importlib.util.module_from_spec(spec)
  35. spec.loader.exec_module(module) # type: ignore
  36. if not hasattr(module, "DB_CONFIG"):
  37. raise RuntimeError(f"No DB_CONFIG found in {config_path}")
  38. return getattr(module, "DB_CONFIG")
  39. def build_engine_from_config(db_config: dict):
  40. user = db_config.get("user")
  41. password = db_config.get("password")
  42. host = db_config.get("host", "localhost")
  43. port = db_config.get("port", 3306)
  44. database = db_config.get("database")
  45. charset = db_config.get("charset", "utf8mb4")
  46. uri = f"mysql+pymysql://{user}:{password}@{host}:{port}/{database}?charset={charset}"
  47. engine = create_engine(uri, pool_pre_ping=True)
  48. return engine
  49. def find_column(df: pd.DataFrame, keywords: List[str]) -> Optional[str]:
  50. for col in df.columns:
  51. name = str(col).strip()
  52. low = name.lower()
  53. for k in keywords:
  54. if k in low:
  55. return name
  56. return None
  57. def read_teacher_df_from_db(engine, table_name: str = "his_lssj") -> pd.DataFrame:
  58. """
  59. 从数据库表 his_lssj 读取教师数据,返回 pandas DataFrame(列名使用表中的列名)。
  60. 假定表中包含列:`工号`, `姓名`, `部门`, `性别`, `手机号码`
  61. """
  62. print(f"从数据库表 `{table_name}` 读取教师数据(假定列:工号, 姓名, 部门, 性别, 手机号码)")
  63. q = f"SELECT `工号`, `姓名`, `部门`, `性别`, `手机号码` FROM `{table_name}`"
  64. df = pd.read_sql_query(q, con=engine)
  65. return df
  66. def normalize_phone(raw: Optional[str]) -> Optional[str]:
  67. if raw is None:
  68. return None
  69. s = str(raw)
  70. digits = "".join(re.findall(r"\d", s))
  71. return digits[:11] if digits else None
  72. def map_sex(raw: Optional[str]) -> Optional[str]:
  73. if raw is None:
  74. return None
  75. s = str(raw).strip()
  76. if s in ("男", "M", "m", "male", "Male"):
  77. return "0"
  78. if s in ("女", "F", "f", "female", "Female"):
  79. return "1"
  80. return None
  81. def soft_delete_other_users(engine, yes: bool, dry_run: bool = False):
  82. # Renamed behavior: perform physical DELETE except user_id = 1
  83. if not yes:
  84. resp = input("将删除 sys_user 中除 user_id=1 外的所有用户,确认?(yes/no) [no]: ").strip().lower()
  85. if resp not in ("y", "yes"):
  86. print("取消删除步骤。")
  87. return
  88. q_count = text("SELECT COUNT(1) as c FROM sys_user WHERE user_id != 1")
  89. with engine.connect() as conn:
  90. cnt = conn.execute(q_count).scalar()
  91. print(f"将被删除的用户数:{cnt}")
  92. if dry_run:
  93. print("dry-run 模式,跳过删除。")
  94. return
  95. q = text("DELETE FROM sys_user WHERE user_id != 1")
  96. with engine.begin() as conn:
  97. conn.execute(q)
  98. print("已删除其他用户(保留 user_id = 1)。")
  99. def generate_user_id(conn):
  100. r = conn.execute(text("SELECT MAX(user_id) as m FROM sys_user")).fetchone()
  101. max_id = int(r[0]) if r and r[0] is not None else 0
  102. return max_id + 1
  103. def get_dept_id(conn, dept_name: str) -> Optional[int]:
  104. if dept_name is None or str(dept_name).strip() == "":
  105. return None
  106. r = conn.execute(text("SELECT dept_id FROM sys_dept WHERE dept_name = :name LIMIT 1"), {"name": dept_name.strip()}).fetchone()
  107. return int(r[0]) if r and r[0] is not None else None
  108. def user_exists(conn, user_name: str, dept_id: Optional[int]) -> bool:
  109. # skip only if both user_name and dept_id match an existing record
  110. q = text(
  111. "SELECT 1 FROM sys_user WHERE user_name = :u AND "
  112. "((dept_id = :did) OR (dept_id IS NULL AND :did IS NULL)) LIMIT 1"
  113. )
  114. r = conn.execute(q, {"u": user_name, "did": dept_id}).fetchone()
  115. return bool(r)
  116. def dept_has_users(conn, dept_id: int) -> bool:
  117. """检查指定 dept_id 下是否已有用户存在(用于决定是否跳过该部门的批量插入)"""
  118. if dept_id is None:
  119. return False
  120. r = conn.execute(text("SELECT 1 FROM sys_user WHERE dept_id = :did LIMIT 1"), {"did": dept_id}).fetchone()
  121. return bool(r)
  122. def insert_user(conn, data: Dict[str, Any]):
  123. cols = ", ".join(f"`{k}`" for k in data.keys())
  124. vals = ", ".join(f":{k}" for k in data.keys())
  125. ins = text(f"INSERT INTO sys_user ({cols}) VALUES ({vals})")
  126. conn.execute(ins, data)
  127. def user_has_role(conn, user_id: int, role_id: int) -> bool:
  128. 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()
  129. return bool(r)
  130. def insert_user_role(conn, user_id: int, role_id: int):
  131. ins = text("INSERT INTO sys_user_role (`user_id`, `role_id`) VALUES (:uid, :rid)")
  132. conn.execute(ins, {"uid": user_id, "rid": role_id})
  133. def main():
  134. base_dir = os.path.dirname(os.path.abspath(__file__))
  135. config_path = os.path.join(base_dir, "db_config.py")
  136. if not os.path.exists(config_path):
  137. print(f"找不到配置文件: {config_path}")
  138. sys.exit(1)
  139. db_config = load_db_config_from_file(config_path)
  140. engine = build_engine_from_config(db_config)
  141. parser = argparse.ArgumentParser()
  142. parser.add_argument("--yes", action="store_true", help="自动确认虚拟删除现有用户(除 user_id=1)")
  143. parser.add_argument("--teacher-file", type=str, default=os.path.join(base_dir, "teacher.xlsx"))
  144. parser.add_argument("--dry-run", action="store_true", help="只打印将执行的操作,不写入数据库")
  145. args = parser.parse_args()
  146. # 1) 虚拟删除其他用户
  147. soft_delete_other_users(engine, args.yes, dry_run=args.dry_run)
  148. # 2) 从 his_lssj 表读取教师数据(更快)
  149. df = read_teacher_df_from_db(engine, table_name="his_lssj")
  150. # 直接使用约定列名(表中创建时已用这些列名)
  151. col_workno = "工号"
  152. col_name = "姓名"
  153. col_phone = "手机号码"
  154. col_dept = "部门"
  155. col_sex = "性别"
  156. # ensure columns exist
  157. for c in (col_workno, col_name):
  158. if c not in df.columns:
  159. print(f"必需列 {c} 在表 his_lssj 中未找到,请检查表结构或使用 Excel 源。")
  160. return
  161. password_hash = "$2a$10$FGldU5BGGq9ONuBmYhxLweFhmbkE45keMKASNZpjETXawE1oGbtZy"
  162. now_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  163. # iterate rows and insert
  164. inserted = 0
  165. skipped = 0
  166. warnings = 0
  167. # cache per-dept decision to avoid repeated checks: dept_id -> 'skip'|'insert'
  168. dept_decision: Dict[Optional[int], str] = {}
  169. with engine.begin() as conn:
  170. for idx, row in df.iterrows():
  171. user_name_val = str(row.get(col_workno)).strip() if row.get(col_workno) is not None else ""
  172. if not user_name_val:
  173. skipped += 1
  174. continue
  175. # determine department first so existence check can use dept_id
  176. dept_raw = str(row.get(col_dept)).strip() if col_dept and row.get(col_dept) is not None else ""
  177. dept_id = get_dept_id(conn, dept_raw) if dept_raw else None
  178. if dept_raw and not dept_id:
  179. print(f"警告: 第 {idx+1} 行,部门 '{dept_raw}' 在 sys_dept 中未找到,dept_id 将设为 NULL。")
  180. warnings += 1
  181. # If we've decided to skip this department because it already has users, skip adding users under it
  182. if dept_id in dept_decision and dept_decision[dept_id] == "skip":
  183. skipped += 1
  184. continue
  185. # First time encountering this dept: if dept already has users, mark skip
  186. if dept_id not in dept_decision:
  187. try:
  188. if dept_has_users(conn, dept_id):
  189. dept_decision[dept_id] = "skip"
  190. skipped += 1
  191. continue
  192. else:
  193. dept_decision[dept_id] = "insert"
  194. except Exception:
  195. dept_decision[dept_id] = "insert"
  196. if user_exists(conn, user_name_val, dept_id):
  197. skipped += 1
  198. continue
  199. nick = str(row.get(col_name)).strip() if row.get(col_name) is not None else ""
  200. raw_phone = row.get(col_phone) if col_phone else None
  201. phone = normalize_phone(raw_phone)
  202. sex_val = map_sex(row.get(col_sex)) if col_sex else None
  203. # generate user_id
  204. new_user_id = generate_user_id(conn)
  205. user_record = {
  206. "user_id": new_user_id,
  207. "tenant_id": "000000",
  208. "dept_id": dept_id,
  209. "user_name": user_name_val,
  210. "nick_name": nick or user_name_val,
  211. "user_type": "sys_user",
  212. "email": None,
  213. "phonenumber": phone,
  214. "sex": sex_val or "0",
  215. "avatar": None,
  216. "password": password_hash,
  217. "status": "0",
  218. "del_flag": "0",
  219. "login_ip": "",
  220. "login_date": None,
  221. "create_dept": 103,
  222. "create_by": 1,
  223. "create_time": now_str,
  224. "update_by": 1,
  225. "update_time": now_str,
  226. "remark": None,
  227. "account_type": "1",
  228. }
  229. if args.dry_run:
  230. print("DRY:", user_record)
  231. inserted += 1
  232. continue
  233. insert_user(conn, user_record)
  234. # assign roles to the new user
  235. role_ids = [1938120323613728770, 4]
  236. for rid in role_ids:
  237. if not user_has_role(conn, new_user_id, rid):
  238. insert_user_role(conn, new_user_id, rid)
  239. inserted += 1
  240. print(f"完成:插入 {inserted} 条,跳过 {skipped} 条,警告 {warnings} 条。")
  241. if __name__ == "__main__":
  242. main()