04-insert_majors.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 04 - 专业信息入库脚本
  5. 说明:
  6. - 从 `his_xssj` 表读取学院编号、专业编号、专业名称(去重)
  7. - 根据学院编号映射到学院名称,然后在 `sys_dept`(或目标表)中查找父节点 parent_id
  8. - 若专业在目标表中不存在,则生成 INSERT 语句(默认 dry-run,仅打印)
  9. - 可使用 --execute 开关执行插入
  10. 用法示例:
  11. python3 "学校历史数据迁移/04-insert_majors.py" --host localhost --user root --password secret --database hzdyjs_xg
  12. python3 "学校历史数据迁移/04-insert_majors.py" --execute --host ... (实际写入)
  13. 注意:
  14. - 请提前安装依赖: pip install pymysql
  15. """
  16. from __future__ import annotations
  17. import argparse
  18. import datetime
  19. import logging
  20. import os
  21. import sys
  22. import importlib.util
  23. from typing import Dict, List, Optional, Tuple
  24. import pymysql
  25. logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
  26. logger = logging.getLogger("insert_majors")
  27. COLLEGE_MAP: Dict[str, str] = {
  28. "14": "智能控制学院",
  29. "15": "智能制造学院",
  30. "19": "人文学院",
  31. "18": "餐饮休闲学院",
  32. "28": "数字信息学院",
  33. "16": "医药健康学院",
  34. "17": "生态园林学院",
  35. }
  36. def load_local_db_config() -> Dict:
  37. """
  38. 尝试从当前脚本目录的 db_config.py 加载 DB_CONFIG 字典并返回(失败则返回空字典)
  39. """
  40. cfg_path = os.path.join(os.path.dirname(__file__), "db_config.py")
  41. if not os.path.exists(cfg_path):
  42. return {}
  43. try:
  44. spec = importlib.util.spec_from_file_location("local_db_config", cfg_path)
  45. module = importlib.util.module_from_spec(spec)
  46. spec.loader.exec_module(module) # type: ignore
  47. return getattr(module, "DB_CONFIG", {}) or {}
  48. except Exception as e:
  49. logger.warning("加载本地 db_config.py 失败: %s", e)
  50. return {}
  51. def get_db_conn(host: str, port: int, user: str, password: str, database: str):
  52. """建立并返回 pymysql 连接(默认 autocommit False)"""
  53. return pymysql.connect(
  54. host=host,
  55. port=port,
  56. user=user,
  57. password=password,
  58. database=database,
  59. charset="utf8mb4",
  60. cursorclass=pymysql.cursors.DictCursor,
  61. autocommit=False,
  62. )
  63. def college_has_majors(conn, parent_id: int, target_table: str) -> bool:
  64. """
  65. 检查指定 parent_id(学院)下是否已有专业类记录(dept_category 以 'zy-' 开头或任意子节点)
  66. 若存在任一子记录则返回 True。
  67. """
  68. # note: percent signs must be escaped (%%) because PyMySQL uses Python %-formatting during mogrify
  69. # Only consider existing records that look like majors (dept_category starting with 'zy-').
  70. # Previously the condition included "OR dept_category IS NOT NULL" which caused false positives
  71. # and made the script skip entire colleges when any child had a non-null dept_category.
  72. sql = f"SELECT 1 FROM {target_table} WHERE parent_id = %s AND dept_category LIKE 'zy-%%' LIMIT 1"
  73. with conn.cursor() as cur:
  74. cur.execute(sql, (parent_id,))
  75. return cur.fetchone() is not None
  76. def fetch_distinct_majors(conn) -> List[Tuple[str, str, str]]:
  77. """
  78. 从 his_xssj 表读取 distinct (college_code, major_code, major_name)
  79. 假设表名为 his_xssj,字段名可调整如 xybm(学院编码)、zybm(专业编码)、zymc(专业名称)
  80. """
  81. # use Chinese column names present in his_xssj table
  82. sql = """
  83. SELECT DISTINCT
  84. COALESCE(`学院编号`, '') AS college_code,
  85. COALESCE(`专业编号`, '') AS major_code,
  86. COALESCE(`专业名称`, '') AS major_name
  87. FROM his_xssj
  88. WHERE `专业编号` IS NOT NULL AND `专业编号` <> '' AND `专业名称` IS NOT NULL AND `专业名称` <> ''
  89. ORDER BY college_code, major_code;
  90. """
  91. with conn.cursor() as cur:
  92. cur.execute(sql)
  93. rows = cur.fetchall()
  94. return [(r["college_code"], r["major_code"], r["major_name"]) for r in rows]
  95. def find_parent_dept(conn, college_name: str) -> Optional[Dict]:
  96. """
  97. 根据学院名称查找 sys_dept 中的父节点(按 dept_name 精确匹配)
  98. 返回整行记录(字典)或 None
  99. """
  100. sql = "SELECT * FROM sys_dept WHERE dept_name = %s LIMIT 1"
  101. with conn.cursor() as cur:
  102. cur.execute(sql, (college_name,))
  103. return cur.fetchone()
  104. def exists_major(conn, parent_id: int, major_code: str, major_name: str, target_table: str) -> bool:
  105. """
  106. 判断目标表中是否已存在该专业(通过 dept_category 或 dept_name 与 parent_id 共同判断)
  107. """
  108. # Consider a major existing only if BOTH dept_category and dept_name match under the same parent_id
  109. sql = f"SELECT 1 FROM {target_table} WHERE parent_id = %s AND dept_category = %s AND dept_name = %s LIMIT 1"
  110. with conn.cursor() as cur:
  111. cur.execute(sql, (parent_id, f"zy-{major_code}", major_name))
  112. return cur.fetchone() is not None
  113. def gen_dept_id(counter: int) -> int:
  114. """
  115. 生成一个大整数 dept_id:
  116. 使用当前时间 (YYYYMMDDHHMMSS) + 3-digit counter 保证唯一性(可根据需要替换为雪花ID)
  117. """
  118. ts = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
  119. return int(f"{ts}{counter:03d}")
  120. def insert_major(conn, target_table: str, parent_row: Dict, major_code: str, major_name: str, dept_id: int):
  121. """
  122. 插入一条 sys_dept 记录到 target_table
  123. - parent_row: 父节点整行(包含 dept_id, ancestors)
  124. """
  125. now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  126. parent_id = parent_row["dept_id"]
  127. parent_ancestors = parent_row.get("ancestors") or ""
  128. if parent_ancestors:
  129. ancestors = f"{parent_ancestors},{parent_id}"
  130. else:
  131. ancestors = str(parent_id)
  132. dept_category = f"zy-{major_code}"
  133. sql = f"""
  134. INSERT INTO {target_table}
  135. (dept_id, tenant_id, parent_id, ancestors, dept_name, dept_category, order_num, leader, phone, email, status, del_flag, create_dept, create_by, create_time, update_by, update_time)
  136. VALUES
  137. (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
  138. """
  139. params = (
  140. dept_id,
  141. "000000", # tenant_id
  142. parent_id,
  143. ancestors,
  144. major_name,
  145. dept_category,
  146. 1, # order_num
  147. None, # leader
  148. None, # phone
  149. None, # email
  150. "0", # status
  151. "0", # del_flag
  152. 0, # create_dept
  153. 1, # create_by
  154. now,
  155. 1, # update_by
  156. now,
  157. )
  158. with conn.cursor() as cur:
  159. cur.execute(sql, params)
  160. def main(argv: Optional[List[str]] = None):
  161. # 尝试从同目录下的 db_config.py 读取默认数据库配置
  162. local_db = load_local_db_config()
  163. host_default = local_db.get("host", os.getenv("DB_HOST", "localhost"))
  164. port_default = int(local_db.get("port", os.getenv("DB_PORT", 3306)))
  165. user_default = local_db.get("user", os.getenv("DB_USER", "root"))
  166. password_default = local_db.get("password", os.getenv("DB_PASSWORD", ""))
  167. database_default = local_db.get("database", os.getenv("DB_NAME", "hzdyjs_xg"))
  168. target_table_default = os.getenv("TARGET_TABLE", "sys_dept")
  169. parser = argparse.ArgumentParser(description="从 his_xssj 导入专业到 sys_dept")
  170. parser.add_argument("--host", default=host_default)
  171. parser.add_argument("--port", type=int, default=port_default)
  172. parser.add_argument("--user", default=user_default)
  173. parser.add_argument("--password", default=password_default)
  174. parser.add_argument("--database", default=database_default)
  175. parser.add_argument("--target-table", default=target_table_default)
  176. parser.add_argument("--dry-run", action="store_true", default=False, help="仅打印将要执行的 SQL(默认执行写库)")
  177. parser.add_argument("--execute", action="store_true", default=True, help="执行插入(会在事务中执行并提交,默认开启)")
  178. parser.add_argument("--limit", type=int, default=0, help="仅处理前 N 条(用于测试)")
  179. args = parser.parse_args(argv)
  180. conn = get_db_conn(args.host, args.port, args.user, args.password, args.database)
  181. try:
  182. majors = fetch_distinct_majors(conn)
  183. logger.info("从 his_xssj 获取到 %d 条去重专业记录", len(majors))
  184. counter = 1
  185. inserted = 0
  186. skipped = 0
  187. sql_statements: List[str] = []
  188. # 记录对每个学院的整体决定('skip' or 'insert'),避免对同一学院重复查询
  189. college_decision: Dict[str, str] = {}
  190. for idx, (college_code, major_code, major_name) in enumerate(majors, start=1):
  191. if args.limit and inserted >= args.limit:
  192. break
  193. college_name = COLLEGE_MAP.get(college_code)
  194. if not college_name:
  195. logger.warning("未在 COLLEGE_MAP 中找到学院编码 %s,跳过: %s", college_code, major_name)
  196. skipped += 1
  197. continue
  198. parent_row = find_parent_dept(conn, college_name)
  199. if not parent_row:
  200. logger.warning("未在 sys_dept 中找到学院父节点(dept_name=%s),请先确认父节点存在,跳过: %s", college_name, major_name)
  201. skipped += 1
  202. continue
  203. # 判重:在同一父节点下,若 dept_category 和 dept_name 同时匹配则视为已存在并跳过
  204. if exists_major(conn, parent_row["dept_id"], major_code, major_name, args.target_table):
  205. logger.info("专业已存在,跳过: %s (%s) under %s", major_name, major_code, college_name)
  206. skipped += 1
  207. continue
  208. dept_id = gen_dept_id(counter)
  209. counter += 1
  210. # Prepare SQL string for logging / dry-run
  211. now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  212. parent_id = parent_row["dept_id"]
  213. parent_ancestors = parent_row.get("ancestors") or ""
  214. ancestors = f"{parent_ancestors},{parent_id}" if parent_ancestors else str(parent_id)
  215. dept_category = f"zy-{major_code}"
  216. # escape single quotes in major_name for SQL literal
  217. safe_major_name = str(major_name).replace("'", "''")
  218. # ancestors is numeric-comma separated; safe to include directly
  219. insert_sql = (
  220. "INSERT INTO {table} (dept_id, tenant_id, parent_id, ancestors, dept_name, dept_category, order_num, leader, phone, email, status, del_flag, create_dept, create_by, create_time, update_by, update_time) "
  221. "VALUES ({did}, '000000', {pid}, '{ancs}', '{dname}', '{dcat}', 1, NULL, NULL, NULL, '0', '0', 0, 1, '{now}', 1, '{now}');"
  222. ).format(
  223. table=args.target_table,
  224. did=dept_id,
  225. pid=parent_id,
  226. ancs=ancestors,
  227. dname=safe_major_name,
  228. dcat=dept_category,
  229. now=now,
  230. )
  231. sql_statements.append(insert_sql)
  232. logger.info("准备插入专业: %s (%s) parent=%s new_dept_id=%s", major_name, major_code, college_name, dept_id)
  233. if args.execute:
  234. insert_major(conn, args.target_table, parent_row, major_code, major_name, dept_id)
  235. conn.commit()
  236. inserted += 1
  237. else:
  238. skipped += 1
  239. # 输出 dry-run 的 SQL
  240. if sql_statements:
  241. logger.info("共准备 %d 条 INSERT 语句 (dry-run=%s, execute=%s)", len(sql_statements), args.dry_run, args.execute)
  242. for s in sql_statements:
  243. print(s)
  244. else:
  245. logger.info("没有需要插入的专业记录")
  246. logger.info("完成: 插入 %d 条, 跳过 %d 条", inserted, skipped)
  247. finally:
  248. conn.close()
  249. if __name__ == "__main__":
  250. main()