02-update_sys_dept.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 更新 sys_dept 表:
  5. 1) 打印 sys_dept 表结构供学习
  6. 2) 可删除 sys_dept 中除 dept_id 为 100/101/102 外的所有记录(需确认或使用 --yes)
  7. 3) 从 teacher.xlsx 中读取部门列,按规则插入到 sys_dept (部分学院放到 parent_id=101,其余放到 102)
  8. 用法:
  9. python update_sys_dept.py # 交互式执行(会提示确认删除)
  10. python update_sys_dept.py --yes # 自动确认删除
  11. python update_sys_dept.py --teacher-file /path/to/teacher.xlsx
  12. 注意:
  13. - 本脚本会尝试读取 `学校历史数据迁移/db_config.py` 中的 DB_CONFIG 连接数据库
  14. - 插入时会尽量推断并填充常用字段(ancestors, parent_id, dept_name, order_num, status, del_flag, create_time)
  15. """
  16. from __future__ import annotations
  17. import os
  18. import sys
  19. import argparse
  20. import datetime
  21. from typing import List, Dict, Any, Optional
  22. import pandas as pd
  23. from sqlalchemy import create_engine, text
  24. import importlib.util
  25. def load_db_config_from_file(config_path: str) -> dict:
  26. spec = importlib.util.spec_from_file_location("db_config", config_path)
  27. module = importlib.util.module_from_spec(spec)
  28. spec.loader.exec_module(module) # type: ignore
  29. if not hasattr(module, "DB_CONFIG"):
  30. raise RuntimeError(f"No DB_CONFIG found in {config_path}")
  31. return getattr(module, "DB_CONFIG")
  32. def build_engine_from_config(db_config: dict):
  33. user = db_config.get("user")
  34. password = db_config.get("password")
  35. host = db_config.get("host", "localhost")
  36. port = db_config.get("port", 3306)
  37. database = db_config.get("database")
  38. charset = db_config.get("charset", "utf8mb4")
  39. uri = f"mysql+pymysql://{user}:{password}@{host}:{port}/{database}?charset={charset}"
  40. engine = create_engine(uri, pool_pre_ping=True)
  41. return engine
  42. def inspect_table(engine, table_name: str) -> List[Dict[str, Any]]:
  43. q = text(
  44. "SELECT COLUMN_NAME, IS_NULLABLE, COLUMN_DEFAULT, DATA_TYPE "
  45. "FROM information_schema.columns "
  46. "WHERE table_schema = :schema AND table_name = :table ORDER BY ORDINAL_POSITION"
  47. )
  48. schema = engine.url.database
  49. with engine.connect() as conn:
  50. result = conn.execute(q, {"schema": schema, "table": table_name})
  51. # Use mappings() so each row is a mapping of column_name->value
  52. rows = result.mappings().all()
  53. return [dict(r) for r in rows]
  54. def find_dept_column(df: pd.DataFrame) -> Optional[str]:
  55. candidates = []
  56. for col in df.columns:
  57. name = str(col).strip()
  58. low = name.lower()
  59. if any(k in low for k in ("部门", "学院", "系", "组织", "院", "department", "dept", "office", "unit")):
  60. candidates.append(name)
  61. if candidates:
  62. return candidates[0]
  63. # fallback: ask user by listing columns
  64. return None
  65. def parent_has_children(engine, parent_id: int) -> bool:
  66. """
  67. 检查 parent_id 下是否已有子部门记录(用于决定是否跳过该 parent 的批量插入)
  68. """
  69. q = text("SELECT 1 FROM sys_dept WHERE parent_id = :pid LIMIT 1")
  70. with engine.connect() as conn:
  71. r = conn.execute(q, {"pid": parent_id}).fetchone()
  72. return bool(r)
  73. def read_teacher_departments_from_db(engine, table_name: str = "his_lssj") -> List[str]:
  74. """
  75. 从数据库表(默认 his_lssj)读取去重的部门名称(列名 '部门')。
  76. 返回去重、排序后的部门名称列表。
  77. """
  78. print(f"从数据库表 `{table_name}` 读取部门列(列名假定为 '部门')")
  79. q = text(f"SELECT DISTINCT `部门` AS dept_name FROM `{table_name}` WHERE `部门` IS NOT NULL AND TRIM(`部门`) != ''")
  80. with engine.connect() as conn:
  81. rows = conn.execute(q).mappings().all()
  82. depts = sorted({str(r.get("dept_name")).strip() for r in rows if r.get("dept_name") is not None})
  83. print(f"发现 {len(depts)} 个去重部门名(示例前20):")
  84. for item in depts[:20]:
  85. print(" -", item)
  86. return depts
  87. def get_parent_info(engine, parent_ids: List[int]) -> Dict[int, Dict[str, Any]]:
  88. q = text("SELECT * FROM sys_dept WHERE dept_id IN :ids")
  89. with engine.connect() as conn:
  90. result = conn.execute(q, {"ids": tuple(parent_ids)})
  91. rows = result.mappings().all()
  92. out: Dict[int, Dict[str, Any]] = {}
  93. for r in rows:
  94. d = dict(r)
  95. out[int(d.get("dept_id"))] = d
  96. return out
  97. def delete_except(engine, keep_ids: List[int], yes: bool):
  98. if not yes:
  99. resp = input(f"将删除 sys_dept 表中除 dept_id IN {keep_ids} 外的所有记录,确认继续?(yes/no): ").strip().lower()
  100. if resp not in ("y", "yes"):
  101. print("取消操作。")
  102. return False
  103. q = text("DELETE FROM sys_dept WHERE dept_id NOT IN :keep")
  104. with engine.begin() as conn:
  105. conn.execute(q, {"keep": tuple(keep_ids)})
  106. print("删除完成。")
  107. return True
  108. def insert_departments(engine, dept_names: List[str], parent_mapping: Dict[str, int], parent_info: Dict[int, Dict[str, Any]]):
  109. """
  110. 插入时遵循以下规则以保持与现有数据一致性:
  111. - dept_id: 若表中已有较大数值 id(如 2025xxxxxxxxxxxx),则以 max(dept_id)+1 继续;否则以当前时间 `%Y%m%d%H%M%S` + 4位序号生成
  112. - tenant_id: 写为 '000000'
  113. - order_num: 在同一 parent_id 下取 max(order_num)+1
  114. - ancestors: 从 parent_info 推断,若父项无 ancestors 则使用 "0,{parent_id}"
  115. - 其他字段填充 create_by/update_by/create_dept 等默认值以匹配样例
  116. """
  117. inserted = 0
  118. skipped = 0
  119. now_dt = datetime.datetime.now()
  120. now_str = now_dt.strftime("%Y-%m-%d %H:%M:%S")
  121. timestamp_prefix = now_dt.strftime("%Y%m%d%H%M%S")
  122. # helper to generate unique dept_id
  123. def generate_dept_id(conn, local_counter_start=1):
  124. # get current max dept_id
  125. r = conn.execute(text("SELECT MAX(dept_id) as m FROM sys_dept")).fetchone()
  126. max_id = int(r[0]) if r and r[0] is not None else 0
  127. if max_id > 10**14:
  128. return max_id + 1
  129. # else use timestamp-based id plus 4-digit counter
  130. counter = local_counter_start
  131. while True:
  132. candidate = int(f"{timestamp_prefix}{counter:04d}")
  133. exists = conn.execute(text("SELECT 1 FROM sys_dept WHERE dept_id = :did LIMIT 1"), {"did": candidate}).fetchone()
  134. if not exists:
  135. return candidate
  136. counter += 1
  137. # Cache per-parent decision to avoid repeated checks: 'skip' or 'insert'
  138. parent_decision: Dict[int, str] = {}
  139. with engine.begin() as conn:
  140. # fetch column info once
  141. col_info = inspect_table(engine, "sys_dept")
  142. col_names = [c["COLUMN_NAME"] for c in col_info]
  143. # prepare xy counter for parent_id = 101 based on existing dept_category values
  144. xy_counter = 1
  145. try:
  146. rows = conn.execute(text("SELECT dept_category FROM sys_dept WHERE parent_id = :pid AND dept_category LIKE 'xy-%'"), {"pid": 101}).mappings().all()
  147. nums = []
  148. for r in rows:
  149. val = r.get("dept_category")
  150. if not val:
  151. continue
  152. parts = str(val).split("-")
  153. if len(parts) >= 2:
  154. try:
  155. nums.append(int(parts[-1]))
  156. except Exception:
  157. continue
  158. if nums:
  159. xy_counter = max(nums) + 1
  160. except Exception:
  161. # ignore and keep default
  162. xy_counter = 1
  163. for name in dept_names:
  164. pname = name.strip()
  165. if not pname:
  166. continue
  167. parent_id = parent_mapping.get(pname, None)
  168. if parent_id is None:
  169. parent_id = 102
  170. # If we've decided to skip inserting for this parent, continue
  171. if parent_id in parent_decision and parent_decision[parent_id] == "skip":
  172. skipped += 1
  173. continue
  174. # First time encountering this parent: if it already has children, skip inserting any under it
  175. if parent_id not in parent_decision:
  176. try:
  177. has_children = conn.execute(text("SELECT 1 FROM sys_dept WHERE parent_id = :pid LIMIT 1"), {"pid": parent_id}).fetchone()
  178. if has_children:
  179. parent_decision[parent_id] = "skip"
  180. logger = None # no logger in this module-level function; just mark skip
  181. skipped += 1
  182. continue
  183. else:
  184. parent_decision[parent_id] = "insert"
  185. except Exception:
  186. parent_decision[parent_id] = "insert"
  187. # skip if same dept_name and parent exists
  188. exists_q = text("SELECT count(1) as c FROM sys_dept WHERE dept_name = :name AND parent_id = :pid")
  189. res = conn.execute(exists_q, {"name": pname, "pid": parent_id}).fetchone()
  190. if res and int(res[0]) > 0:
  191. skipped += 1
  192. continue
  193. parent = parent_info.get(parent_id, {})
  194. parent_ancestors = parent.get("ancestors") if parent else None
  195. if parent_ancestors:
  196. ancestors = f"{parent_ancestors},{parent_id}"
  197. else:
  198. # ensure root '0' present
  199. if parent_id == 0:
  200. ancestors = "0"
  201. else:
  202. ancestors = f"0,{parent_id}"
  203. # compute next order_num under this parent
  204. r = conn.execute(text("SELECT COALESCE(MAX(order_num),0) as m FROM sys_dept WHERE parent_id = :pid"), {"pid": parent_id}).fetchone()
  205. next_order = int(r[0]) + 1 if r and r[0] is not None else 1
  206. # generate unique dept_id
  207. new_dept_id = generate_dept_id(conn)
  208. # prepare insert dict with default fields matching provided samples
  209. # set dept_category to 'xy-序号' for entries under parent_id == 101
  210. dept_category_val = None
  211. if parent_id == 101:
  212. dept_category_val = f"xy-{xy_counter}"
  213. xy_counter += 1
  214. insert_dict: Dict[str, Any] = {
  215. "dept_id": new_dept_id,
  216. "tenant_id": "000000",
  217. "parent_id": parent_id,
  218. "ancestors": ancestors,
  219. "dept_name": pname,
  220. "dept_category": dept_category_val,
  221. "order_num": next_order,
  222. "leader": None,
  223. "phone": None,
  224. "email": None,
  225. "status": "0",
  226. "del_flag": "0",
  227. "create_dept": 0,
  228. "create_by": 1,
  229. "create_time": now_str,
  230. "update_by": 1,
  231. "update_time": now_str,
  232. }
  233. # only keep keys that exist in the actual table
  234. use_cols = {k: v for k, v in insert_dict.items() if k in col_names}
  235. if "dept_name" not in use_cols:
  236. raise RuntimeError("目标表不包含 dept_name 列,无法继续")
  237. cols_part = ", ".join(f"`{k}`" for k in use_cols.keys())
  238. vals_part = ", ".join(f":{k}" for k in use_cols.keys())
  239. ins_q = text(f"INSERT INTO sys_dept ({cols_part}) VALUES ({vals_part})")
  240. conn.execute(ins_q, use_cols)
  241. inserted += 1
  242. print(f"插入完成:新增 {inserted} 条,跳过已存在 {skipped} 条。")
  243. def main():
  244. base_dir = os.path.dirname(os.path.abspath(__file__))
  245. config_path = os.path.join(base_dir, "db_config.py")
  246. if not os.path.exists(config_path):
  247. print(f"找不到配置文件: {config_path}")
  248. sys.exit(1)
  249. db_config = load_db_config_from_file(config_path)
  250. engine = build_engine_from_config(db_config)
  251. # 1. 学习表结构
  252. print("正在读取 sys_dept 表结构...")
  253. cols = inspect_table(engine, "sys_dept")
  254. for c in cols:
  255. print(f" - {c['COLUMN_NAME']} type={c['DATA_TYPE']} nullable={c['IS_NULLABLE']} default={c['COLUMN_DEFAULT']}")
  256. # 2. 删除除指定 dept_id 外的记录
  257. parser = argparse.ArgumentParser()
  258. parser.add_argument("--yes", action="store_true", help="自动确认删除")
  259. parser.add_argument("--teacher-file", type=str, default=os.path.join(base_dir, "teacher.xlsx"))
  260. args = parser.parse_args()
  261. keep_ids = [100, 101, 102]
  262. ok = delete_except(engine, keep_ids, args.yes)
  263. if not ok:
  264. print("用户取消,退出。")
  265. return
  266. # 3. 从 his_lssj 表导入部门(更快)
  267. dept_names = read_teacher_departments_from_db(engine, table_name="his_lssj")
  268. # mapping: specific colleges -> parent 101, others -> 102
  269. top_level_101 = {
  270. "人文学院", "餐饮休闲学院", "生态园林学院", "数字信息学院",
  271. "医药健康学院", "智能控制学院", "智能制造学院"
  272. }
  273. # build mapping for exact names
  274. parent_mapping = {name: 101 for name in top_level_101}
  275. # get parent info
  276. parent_info = get_parent_info(engine, [101, 102])
  277. if 101 not in parent_info or 102 not in parent_info:
  278. print("警告:未能在 sys_dept 中找到 parent_id 为 101 或 102 的记录,请确认这两个父组织是否存在。")
  279. insert_departments(engine, dept_names, parent_mapping, parent_info)
  280. if __name__ == "__main__":
  281. main()