06-insert_classes.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. 06 - 班级数据入库
  5. 从 his_xssj 表将班级数据导入学工表:
  6. - sys_dept
  7. - xg_class_msg
  8. - xg_class_teacher_counselor_rel
  9. 脚本特性:
  10. - 列名映射可配置(见 COLUMN_MAP)
  11. - 学院编号映射(见 COLLEGE_CODE_TO_NAME)
  12. - 对班级存在性检测:使用 sys_dept(dept_name, dept_category)
  13. - 使用时间戳+序号生成 dept_id 与 class id(与现有脚本保持一致)
  14. - 对班级中学生状态判定 class_status 与 is_graduation
  15. - 对班主任通过 sys_user.nick_name 进行唯一性解析
  16. 使用方法:
  17. - 在运行前确认 `db_config.py` 中 `DB_CONFIG` 可用
  18. - 如需先预览请设置 DRY_RUN = True
  19. """
  20. from __future__ import annotations
  21. import time
  22. from datetime import datetime
  23. from typing import Dict, Any, List, Optional, Tuple
  24. import pymysql
  25. from db_config import DB_CONFIG
  26. import os
  27. import json
  28. # 可根据 his_xssj 表模型调整映射(使用真实中文列名)
  29. COLUMN_MAP: Dict[str, str] = {
  30. "class_code": "班级编号", # 班级编号
  31. "class_name": "班级名称", # 班级名称
  32. "major_code": "专业编号", # 专业编号
  33. "major_name": "专业名称", # 专业名称
  34. "college_code": "学院编号", # 学院编号
  35. "enrollment_year": "入学年份", # 入学年份(文本,需要转换为 int)
  36. "grade": "所属年级", # 所属年级
  37. "school_system": "学制编码", # 学制编码(文本,需要转换为 int)
  38. "class_type": "层次编号", # 层次编号
  39. "training_type": "学生类别", # 学生类别/培养方式
  40. "class_teacher_name": "班主任", # 班主任姓名
  41. "student_status": "学生状态", # 学生状态(判定是否为'在读')
  42. "enrollment_date": "入学日期" # 入学日期(可为空)
  43. }
  44. # 学院编号对应关系(题主给定)
  45. COLLEGE_CODE_TO_NAME: Dict[str, str] = {
  46. "14": "智能控制学院",
  47. "15": "智能制造学院",
  48. "19": "人文学院",
  49. "18": "餐饮休闲学院",
  50. "28": "数字信息学院",
  51. "16": "医药健康学院",
  52. "17": "生态园林学院"
  53. }
  54. def extract_distinct_classes_to_his_bjsj(connection, dry_run: bool = False) -> int:
  55. """
  56. (已移除写入逻辑)该函数保留为占位:如果需要从脚本内写入 his_bjsj,请使用外部迁移或重新启用写入逻辑。
  57. 当前实现仅返回 0 或在 dry_run 模式下打印示例,不执行对 his_bjsj 的 INSERT/DELETE 操作。
  58. """
  59. select_sql = """
  60. SELECT DISTINCT
  61. `入学年份` AS enrollment_year,
  62. `入学日期` AS enrollment_date,
  63. `教学系部` AS teaching_dept,
  64. `班级名称` AS class_name,
  65. `班主任` AS class_teacher,
  66. `学生类别` AS student_type,
  67. `学院编号` AS college_code,
  68. `专业编号` AS major_code,
  69. `专业名称` AS major_name,
  70. `层次编号` AS level_code,
  71. `报读层次` AS level_name,
  72. `所属年级` AS grade,
  73. `班级编号` AS class_code,
  74. `学制编码` AS school_system
  75. FROM his_xssj
  76. """
  77. with connection.cursor() as cursor:
  78. cursor.execute(select_sql)
  79. rows = cursor.fetchall()
  80. if dry_run:
  81. print(f"[DRY_RUN] 将从 his_xssj 提取 {len(rows)} 条去重班级记录(示例前 5 条)")
  82. for r in rows[:5]:
  83. print(r)
  84. return 0
  85. def get_connection():
  86. return pymysql.connect(cursorclass=pymysql.cursors.DictCursor, **DB_CONFIG)
  87. def fetch_distinct_classes(connection) -> List[Dict[str, Any]]:
  88. """
  89. 从 his_xssj 中取出按班级分组的基础信息(去重后的班级列表)
  90. """
  91. cols = COLUMN_MAP
  92. sql = f"""
  93. SELECT
  94. `{cols['class_code']}` AS class_code,
  95. `{cols['class_name']}` AS class_name,
  96. `{cols['major_code']}` AS major_code,
  97. `{cols['major_name']}` AS major_name,
  98. `{cols['college_code']}` AS college_code,
  99. `{cols['enrollment_year']}` AS enrollment_year,
  100. `{cols['grade']}` AS grade,
  101. `{cols['school_system']}` AS school_system,
  102. `{cols['class_type']}` AS class_type,
  103. `{cols['training_type']}` AS training_type,
  104. `{cols['class_teacher_name']}` AS class_teacher_name,
  105. `{cols['enrollment_date']}` AS enrollment_date
  106. FROM his_xssj
  107. GROUP BY class_code, class_name, major_code, major_name, college_code, enrollment_year, grade, school_system, class_type, training_type, class_teacher_name, enrollment_date
  108. ORDER BY enrollment_year ASC, class_name ASC
  109. """
  110. with connection.cursor() as cursor:
  111. cursor.execute(sql)
  112. return cursor.fetchall()
  113. def fetch_classes_from_his_bjsj(connection) -> List[Dict[str, Any]]:
  114. """
  115. 从 his_bjsj 中读取班级数据,作为后续插入学工表的唯一数据源。
  116. 返回的字段与原来从 his_xssj 聚合出的字段结构一致(class_code, class_name, major_code, major_name, college_code, enrollment_year, grade, school_system, class_type, training_type, class_teacher_name, enrollment_date)。
  117. 注意:his_bjsj 表需事先存在且已由 extract_distinct_classes_to_his_bjsj 填充。
  118. """
  119. sql = """
  120. SELECT
  121. class_code AS class_code,
  122. class_name AS class_name,
  123. major_code AS major_code,
  124. major_name AS major_name,
  125. college_code AS college_code,
  126. enrollment_year AS enrollment_year,
  127. grade AS grade,
  128. school_system AS school_system,
  129. level_code AS class_type,
  130. student_type AS training_type,
  131. class_teacher AS class_teacher_name,
  132. enrollment_date AS enrollment_date
  133. FROM his_bjsj
  134. ORDER BY enrollment_year ASC, class_name ASC
  135. """
  136. with connection.cursor() as cursor:
  137. cursor.execute(sql)
  138. return cursor.fetchall()
  139. def class_has_active_student(connection, class_code: str) -> bool:
  140. """
  141. 判断班级内是否存在至少一名学生状态为 '在读'
  142. """
  143. cols = COLUMN_MAP
  144. sql = f"""
  145. SELECT COUNT(1) AS cnt FROM his_xssj
  146. WHERE `{cols['class_code']}` = %s AND `{cols['student_status']}` = '在读'
  147. """
  148. with connection.cursor() as cursor:
  149. cursor.execute(sql, (class_code,))
  150. row = cursor.fetchone()
  151. return (row and row.get("cnt", 0) > 0)
  152. def resolve_major_dept(connection, major_name: str, major_code: str, college_code: Optional[str] = None) -> Optional[Dict[str, Any]]:
  153. """
  154. 根据规则查找专业部门,优先尝试精确匹配 (dept_category = 'zy-'+major_code AND dept_name = major_name)。
  155. 若未命中且提供了 college_code,则:
  156. 1) 通过 dept_category = 'xy-'+college_code 在 sys_dept 查到学院 parent_id;
  157. 2) 在该学院的子节点中(parent_id = 学院dept_id,且 dept_category LIKE 'zy-%')按 dept_name 精确匹配 major_name。
  158. 返回 dict 包含 dept_id, ancestors, parent_id 等或 None。
  159. """
  160. # 1) 精确匹配(保留原有逻辑)
  161. if major_name is not None and major_code is not None and str(major_code).strip() != "":
  162. sql = """
  163. SELECT dept_id, parent_id, ancestors, dept_name, dept_category
  164. FROM sys_dept
  165. WHERE dept_name = %s AND dept_category = %s
  166. LIMIT 1
  167. """
  168. with connection.cursor() as cursor:
  169. cursor.execute(sql, (major_name, f"zy-{major_code}"))
  170. row = cursor.fetchone()
  171. if row:
  172. return row
  173. # 2) 若未匹配到,且提供 college_code,则根据 his_bjsj 的 college_code 关联学院,再在学院下按名称查找专业
  174. if college_code:
  175. try:
  176. sql_college = "SELECT dept_id FROM sys_dept WHERE dept_category = %s LIMIT 1"
  177. with connection.cursor() as cursor:
  178. cursor.execute(sql_college, (f"xy-{college_code}",))
  179. college_row = cursor.fetchone()
  180. if college_row and college_row.get("dept_id") is not None:
  181. college_dept_id = college_row["dept_id"]
  182. sql_child = """
  183. SELECT dept_id, parent_id, ancestors, dept_name, dept_category
  184. FROM sys_dept
  185. WHERE parent_id = %s AND dept_category LIKE %s AND dept_name = %s
  186. LIMIT 1
  187. """
  188. with connection.cursor() as cursor:
  189. cursor.execute(sql_child, (college_dept_id, "zy-%", major_name))
  190. child = cursor.fetchone()
  191. if child:
  192. return child
  193. except Exception:
  194. # 不阻塞主流程,继续回退为 None
  195. pass
  196. return None
  197. def resolve_college_dept(connection, college_code: str) -> Optional[Dict[str, Any]]:
  198. """
  199. 通过学院编号查找学院在 sys_dept 中的 dept_id(dept_category = 'xy-' + code)
  200. """
  201. sql = """
  202. SELECT dept_id, dept_name
  203. FROM sys_dept
  204. WHERE dept_category = %s
  205. LIMIT 1
  206. """
  207. with connection.cursor() as cursor:
  208. cursor.execute(sql, (f"xy-{college_code}",))
  209. return cursor.fetchone()
  210. def resolve_teacher_user(connection, teacher_name: str) -> Optional[int]:
  211. """
  212. 通过 sys_user.nick_name 查找教师 user_id
  213. - 唯一命中返回 user_id
  214. - 多于或少于 1 条返回 None
  215. """
  216. if not teacher_name:
  217. return None
  218. sql = """
  219. SELECT user_id FROM sys_user WHERE nick_name = %s
  220. """
  221. with connection.cursor() as cursor:
  222. cursor.execute(sql, (teacher_name,))
  223. rows = cursor.fetchall()
  224. if len(rows) == 1:
  225. return rows[0].get("user_id")
  226. return None
  227. def find_similar_majors(connection, major_name: str, limit: int = 5) -> List[Dict[str, Any]]:
  228. """
  229. 在 sys_dept 中查找与 major_name 相似的专业(dept_category LIKE 'zy-%')作为候选建议。
  230. 返回最多 limit 条候选,元素包含 dept_id 与 dept_name。
  231. """
  232. if not major_name:
  233. return []
  234. sql = """
  235. SELECT dept_id, dept_name, dept_category
  236. FROM sys_dept
  237. WHERE dept_category LIKE %s AND dept_name LIKE %s
  238. LIMIT %s
  239. """
  240. with connection.cursor() as cursor:
  241. cursor.execute(sql, ('zy-%', f"%{major_name}%", limit))
  242. return cursor.fetchall()
  243. LOG_DIR = "logs"
  244. SKIP_LOG_FILE = os.path.join(LOG_DIR, "06_insert_classes_skipped_majors.jsonl")
  245. def ensure_log_dir():
  246. if not os.path.exists(LOG_DIR):
  247. os.makedirs(LOG_DIR, exist_ok=True)
  248. def log_skipped_major(connection, row: Dict[str, Any]):
  249. """
  250. 将未找到父专业(major)导致跳过的班级信息记录到日志文件(JSONL),并返回候选建议列表。
  251. """
  252. ensure_log_dir()
  253. suggestions = find_similar_majors(connection, row.get("major_name") or "")
  254. # 尝试获取 his_xssj 中该班级的所有原始记录(完整行),以便人工核对
  255. his_rows: List[Dict[str, Any]] = []
  256. class_code = row.get("class_code")
  257. his_class_col = COLUMN_MAP.get("class_code")
  258. try:
  259. with connection.cursor() as c:
  260. if his_class_col and class_code is not None:
  261. sql = f"SELECT * FROM his_xssj WHERE `{his_class_col}` = %s"
  262. c.execute(sql, (class_code,))
  263. his_rows = c.fetchall()
  264. except Exception:
  265. his_rows = []
  266. entry = {
  267. "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
  268. "class_code": class_code,
  269. "class_name": row.get("class_name"),
  270. "major_code": row.get("major_code"),
  271. "major_name": row.get("major_name"),
  272. "college_code": row.get("college_code"),
  273. "row": row,
  274. "his_rows_count": len(his_rows),
  275. "his_rows": his_rows,
  276. "suggestions": suggestions
  277. }
  278. try:
  279. with open(SKIP_LOG_FILE, "a", encoding="utf-8") as fh:
  280. fh.write(json.dumps(entry, ensure_ascii=False, default=str) + "\n")
  281. except Exception:
  282. # 写日志失败不应阻塞主流程
  283. pass
  284. return suggestions
  285. def get_parent_ancestors(connection, parent_id: int) -> str:
  286. sql = "SELECT ancestors FROM sys_dept WHERE dept_id = %s LIMIT 1"
  287. with connection.cursor() as cursor:
  288. cursor.execute(sql, (parent_id,))
  289. row = cursor.fetchone()
  290. if row and row.get("ancestors"):
  291. return f"{row['ancestors']},{parent_id}"
  292. return f"0,100,101,{parent_id}"
  293. def get_max_order_num(connection, parent_id: int) -> int:
  294. sql = "SELECT MAX(order_num) AS max_order FROM sys_dept WHERE parent_id = %s"
  295. with connection.cursor() as cursor:
  296. cursor.execute(sql, (parent_id,))
  297. row = cursor.fetchone()
  298. return int(row["max_order"]) if row and row["max_order"] is not None else 0
  299. def generate_id(base_timestamp: str, index: int) -> int:
  300. """
  301. 生成 dept_id / class id: timestamp + 四位序号(与原脚本保持一致)
  302. """
  303. return int(f"{base_timestamp}{str(index).zfill(4)}")
  304. def parse_date_to_mysql(value: Any) -> Optional[str]:
  305. """
  306. 将各种可能的日期/时间值规范化为 MySQL DATETIME 字符串 'YYYY-MM-DD HH:MM:SS'。
  307. - 支持 datetime 对象直接格式化
  308. - 支持常见字符串格式,如 'YYYY-MM-DD HH:MM:SS', 'YYYY/MM/DD HH:MM:SS', 'M/D/YYYY H:M:S', 'YYYY-MM-DD', 'M/D/YYYY'
  309. - 若无法解析或为 None,返回 None
  310. """
  311. if value is None:
  312. return None
  313. if isinstance(value, datetime):
  314. return value.strftime("%Y-%m-%d %H:%M:%S")
  315. if not isinstance(value, str):
  316. try:
  317. # 尝试将数字型视为时间戳(秒)
  318. ts = float(value)
  319. dt = datetime.fromtimestamp(ts)
  320. return dt.strftime("%Y-%m-%d %H:%M:%S")
  321. except Exception:
  322. return None
  323. s = value.strip()
  324. if s == "":
  325. return None
  326. patterns = [
  327. "%Y-%m-%d %H:%M:%S",
  328. "%Y/%m/%d %H:%M:%S",
  329. "%m/%d/%Y %H:%M:%S",
  330. "%m/%d/%y %H:%M:%S",
  331. "%Y-%m-%d",
  332. "%Y/%m/%d",
  333. "%m/%d/%Y",
  334. "%m/%d/%y",
  335. ]
  336. for p in patterns:
  337. try:
  338. dt = datetime.strptime(s, p)
  339. # 如果没有时分秒,补 00:00:00
  340. return dt.strftime("%Y-%m-%d %H:%M:%S")
  341. except Exception:
  342. continue
  343. # 兜底:尝试解析含有空格并用 '/' 或 '-' 的混合格式(例如 '9/1/2009 0:0:0' 已被覆盖上面,但保留此处以防)
  344. try:
  345. parts = s.split()
  346. date_part = parts[0]
  347. time_part = parts[1] if len(parts) > 1 else "0:0:0"
  348. md = date_part.replace("-", "/")
  349. combined = f"{md} {time_part}"
  350. dt = datetime.strptime(combined, "%m/%d/%Y %H:%M:%S")
  351. return dt.strftime("%Y-%m-%d %H:%M:%S")
  352. except Exception:
  353. return None
  354. def insert_sys_dept(cursor, dept_id: int, tenant_id: str, parent_id: int, ancestors: str, dept_name: str, dept_category: str, order_num: int, current_time: datetime):
  355. sql = """
  356. INSERT INTO sys_dept (
  357. dept_id, tenant_id, parent_id, ancestors, dept_name, dept_category,
  358. order_num, leader, phone, email, status, del_flag,
  359. create_dept, create_by, create_time, update_by, update_time
  360. ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
  361. """
  362. params = (
  363. dept_id, tenant_id, parent_id, ancestors, dept_name, dept_category,
  364. order_num, None, None, None, '0', '0', 0, 1, current_time, 1, current_time
  365. )
  366. cursor.execute(sql, params)
  367. def insert_xg_class_msg(cursor, class_data: Dict[str, Any]):
  368. sql = """
  369. INSERT INTO xg_class_msg (
  370. id, tenant_id, create_dept, create_by, create_time,
  371. update_by, update_time, deleted,
  372. college_id, college_name, major_id, major_name,
  373. class_code, class_name, class_type, grade, enrollment_year,
  374. graduation_year, training_type, degree_type,
  375. counselor_id, counselor_name, class_teacher_id, class_teacher_name,
  376. class_advisor_id, class_advisor_name,
  377. class_monitor_id, class_monitor_name,
  378. deputy_monitor_id, deputy_monitor_name,
  379. class_status, is_graduation, graduation_date, school_system
  380. ) VALUES (
  381. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
  382. )
  383. """
  384. params = (
  385. class_data.get("id"),
  386. class_data.get("tenant_id", "000000"),
  387. class_data.get("create_dept", 0),
  388. class_data.get("create_by", 1),
  389. class_data.get("create_time"),
  390. class_data.get("update_by", 1),
  391. class_data.get("update_time"),
  392. class_data.get("deleted", 0),
  393. class_data.get("college_id"),
  394. class_data.get("college_name"),
  395. class_data.get("major_id"),
  396. class_data.get("major_name"),
  397. class_data.get("class_code"),
  398. class_data.get("class_name"),
  399. class_data.get("class_type"),
  400. class_data.get("grade"),
  401. class_data.get("enrollment_year"),
  402. class_data.get("graduation_year"),
  403. class_data.get("training_type"),
  404. None, # degree_type
  405. None, None,
  406. class_data.get("class_teacher_id"),
  407. class_data.get("class_teacher_name"),
  408. None, None, None, None, None, None,
  409. class_data.get("class_status"),
  410. class_data.get("is_graduation"),
  411. class_data.get("graduation_date"),
  412. class_data.get("school_system")
  413. )
  414. cursor.execute(sql, params)
  415. def insert_teacher_rel(cursor, rel_data: Dict[str, Any]):
  416. sql = """
  417. INSERT INTO xg_class_teacher_counselor_rel (
  418. id, tenant_id, create_dept, create_by, create_time,
  419. update_by, update_time, deleted, class_id, class_name,
  420. teacher_id, teacher_name, teacher_type, start_date, end_date, remark
  421. ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
  422. """
  423. params = (
  424. rel_data.get("id"),
  425. rel_data.get("tenant_id", "000000"),
  426. rel_data.get("create_dept", 0),
  427. rel_data.get("create_by", 1),
  428. rel_data.get("create_time"),
  429. rel_data.get("update_by", 1),
  430. rel_data.get("update_time"),
  431. rel_data.get("deleted", 0),
  432. rel_data.get("class_id"),
  433. rel_data.get("class_name"),
  434. rel_data.get("teacher_id"),
  435. rel_data.get("teacher_name"),
  436. rel_data.get("teacher_type"),
  437. rel_data.get("start_date"),
  438. rel_data.get("end_date"),
  439. rel_data.get("remark")
  440. )
  441. cursor.execute(sql, params)
  442. def main():
  443. print("开始:06 - 班级数据入库")
  444. base_timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
  445. seq_counter = 0
  446. connection = get_connection()
  447. try:
  448. # 在开始时询问是否删除 sys_dept 中 dept_category 以 bj- 开头的记录
  449. def prompt_delete_sys_dept_bj(conn) -> bool:
  450. try:
  451. ans = input("是否先删除 `sys_dept` 中 dept_category 以 'bj-' 开头的记录? [y/N]: ").strip().lower()
  452. except (EOFError, KeyboardInterrupt):
  453. print("\\n未检测到交互输入,默认不删除 sys_dept 的 bj- 记录。")
  454. return False
  455. if ans not in ("y", "yes"):
  456. return False
  457. try:
  458. with conn.cursor() as c:
  459. c.execute("SELECT COUNT(1) AS cnt FROM sys_dept WHERE dept_category LIKE %s", (f"bj-%",))
  460. cnt = int(c.fetchone().get("cnt", 0))
  461. print(f"将删除 sys_dept 中 {cnt} 条 dept_category LIKE 'bj-%' 的记录。")
  462. try:
  463. confirm = input("确认删除并继续?此操作不可恢复。输入 y 确认: ").strip().lower()
  464. except (EOFError, KeyboardInterrupt):
  465. print("\\n未检测到交互输入,取消删除。")
  466. return False
  467. if confirm not in ("y", "yes"):
  468. print("已取消删除操作,继续执行导入(不删除)。")
  469. return False
  470. with conn.cursor() as c:
  471. c.execute("DELETE FROM sys_dept WHERE dept_category LIKE %s", (f"bj-%",))
  472. conn.commit()
  473. print("已删除并提交:sys_dept 中 dept_category LIKE 'bj-%' 的记录。")
  474. return True
  475. except Exception as e:
  476. print("删除 sys_dept 过程中发生异常,跳过删除:", e)
  477. return False
  478. try:
  479. deleted_sys_dept_bj = prompt_delete_sys_dept_bj(connection)
  480. if deleted_sys_dept_bj:
  481. print("用户已确认并删除 sys_dept 中 bj- 开头的记录,继续后续流程。")
  482. else:
  483. print("未删除 sys_dept 的 bj- 记录,继续执行导入流程。")
  484. except Exception as e:
  485. print("询问或删除 sys_dept(bj-) 过程中发生异常,继续执行导入:", e)
  486. # 在正式执行导入前,询问用户是否要先删除历史数据
  487. def prompt_delete_tables(conn) -> bool:
  488. """
  489. 交互式询问并在确认后删除 xg_class_msg 与 xg_class_teacher_counselor_rel 中的数据。
  490. 返回 True 表示已执行删除并提交,False 表示未删除。
  491. """
  492. try:
  493. ans = input("是否先删除 `xg_class_msg` 与 `xg_class_teacher_counselor_rel` 中的现有数据? [y/N]: ").strip().lower()
  494. except (EOFError, KeyboardInterrupt):
  495. print("\\n未检测到交互输入,默认不删除。")
  496. return False
  497. if ans not in ("y", "yes"):
  498. return False
  499. with conn.cursor() as c:
  500. c.execute("SELECT COUNT(1) AS cnt FROM xg_class_msg")
  501. cnt_msg = int(c.fetchone().get("cnt", 0))
  502. c.execute("SELECT COUNT(1) AS cnt FROM xg_class_teacher_counselor_rel")
  503. cnt_rel = int(c.fetchone().get("cnt", 0))
  504. print(f"将删除 xg_class_msg 中 {cnt_msg} 条,xg_class_teacher_counselor_rel 中 {cnt_rel} 条。")
  505. try:
  506. confirm = input("确认删除并继续?此操作不可恢复。输入 y 确认: ").strip().lower()
  507. except (EOFError, KeyboardInterrupt):
  508. print("\\n未检测到交互输入,取消删除。")
  509. return False
  510. if confirm not in ("y", "yes"):
  511. print("已取消删除操作,继续执行导入(不删除)。")
  512. return False
  513. with conn.cursor() as c:
  514. # 先删除关系表,再删除班级表
  515. c.execute("DELETE FROM xg_class_teacher_counselor_rel")
  516. c.execute("DELETE FROM xg_class_msg")
  517. conn.commit()
  518. print("已删除并提交:xg_class_msg 与 xg_class_teacher_counselor_rel。")
  519. return True
  520. # 在继续前询问是否删除历史数据(仅在交互式环境有意义)
  521. try:
  522. deleted = prompt_delete_tables(connection)
  523. if deleted:
  524. print("用户已确认并删除历史数据,继续执行后续导入流程。")
  525. else:
  526. print("未删除历史数据,继续执行导入流程。")
  527. except Exception as e:
  528. print("询问或删除过程中发生异常,继续执行导入:", e)
  529. # 注意:脚本已移除对 his_bjsj 的写入/删除操作。请确保 his_bjsj 表由外部迁移或手动创建并填充。
  530. print("提示:脚本不会对 his_bjsj 执行删除或插入操作,后续将直接从 his_bjsj 读取数据。")
  531. # 从 his_bjsj 中读取班级数据作为唯一数据源(his_bjsj 应由前面的抽取步骤或外部迁移提前填充)
  532. class_rows = fetch_classes_from_his_bjsj(connection)
  533. print(f"共发现 {len(class_rows)} 个去重后的班级记录(将按专业分组并插入)")
  534. # 预分组:按 major(parent) 收集要插入的班级以便计算 order_num
  535. by_parent: Dict[int, List[Dict[str, Any]]] = {}
  536. prepared_classes: List[Dict[str, Any]] = []
  537. for r in class_rows:
  538. class_code = r.get("class_code")
  539. class_name = r.get("class_name")
  540. major_code = str(r.get("major_code") or "")
  541. major_name = r.get("major_name")
  542. major_dept = resolve_major_dept(connection, major_name, major_code, str(r.get("college_code") or ""))
  543. # 若未命中,则尝试从 his_bjsj 补全班级信息后再次匹配
  544. if not major_dept:
  545. try:
  546. with connection.cursor() as c:
  547. c.execute("SELECT major_code, major_name, college_code, enrollment_year, enrollment_date FROM his_bjsj WHERE class_code = %s LIMIT 1", (class_code,))
  548. bj = c.fetchone()
  549. if bj:
  550. # 用 his_bjsj 的值覆盖 row 中的同类字段以便后续匹配
  551. major_code = str(bj.get("major_code") or "")
  552. major_name = bj.get("major_name") or major_name
  553. # 也更新原始 row,便于日志和后续处理
  554. r["major_code"] = major_code
  555. r["major_name"] = major_name
  556. r["college_code"] = bj.get("college_code") or r.get("college_code")
  557. r["enrollment_year"] = bj.get("enrollment_year") or r.get("enrollment_year")
  558. r["enrollment_date"] = bj.get("enrollment_date") or r.get("enrollment_date")
  559. major_dept = resolve_major_dept(connection, major_name, major_code, str(r.get("college_code") or ""))
  560. except Exception:
  561. major_dept = None
  562. if not major_dept:
  563. # 记录并打印更详细的信息(包含专业编号与名称),同时输出候选建议
  564. suggestions = log_skipped_major(connection, r)
  565. print(f"跳过班级(未找到父专业):{class_code} {class_name} 专业({major_code} - {major_name})")
  566. if suggestions:
  567. cand_str = "; ".join([f"{s.get('dept_id')}:{s.get('dept_name')}" for s in suggestions])
  568. print(f" 建议候选专业:{cand_str}")
  569. else:
  570. print(" 无相似候选专业。已写入 logs/06_insert_classes_skipped_majors.jsonl 以便人工核对。")
  571. continue
  572. parent_id = major_dept["dept_id"]
  573. prepared = {
  574. "row": r,
  575. "parent_id": parent_id,
  576. "major_dept": major_dept
  577. }
  578. by_parent.setdefault(parent_id, []).append(prepared)
  579. prepared_classes.append(prepared)
  580. # 在每个 parent_id 下按 enrollment_year, class_name 排序并计算 order_num 起点
  581. insertion_plan: List[Tuple[int, Dict[str, Any]]] = [] # list of (order_num, prepared)
  582. for parent_id, class_list in by_parent.items():
  583. max_order = get_max_order_num(connection, parent_id)
  584. # sort by enrollment_year (统一转为 int,发生异常或为空时视为 0) 然后按 class_name
  585. def _safe_year_key(p: Dict[str, Any]) -> int:
  586. val = p["row"].get("enrollment_year")
  587. if val is None or val == "":
  588. return 0
  589. try:
  590. return int(val)
  591. except Exception:
  592. return 0
  593. class_list.sort(key=lambda p: (_safe_year_key(p), p["row"].get("class_name") or ""))
  594. for idx, item in enumerate(class_list, start=1):
  595. item_order = max_order + idx
  596. item["order_num"] = item_order
  597. insertion_plan.append((item_order, item))
  598. # 按插入顺序执行插入(可调整为按parent分批提交)
  599. inserted_count = 0
  600. class_msg_inserted = 0
  601. teacher_rel_inserted = 0
  602. # 使用单个连接事务(最后 commit)
  603. with connection.cursor() as cursor:
  604. for item in sorted(insertion_plan, key=lambda x: (x[1]["parent_id"], x[0])):
  605. prepared = item[1]
  606. row = prepared["row"]
  607. parent_id = prepared["parent_id"]
  608. order_num = prepared["order_num"]
  609. class_code = row.get("class_code")
  610. class_name = row.get("class_name") or ""
  611. enrollment_year = row.get("enrollment_year")
  612. grade = row.get("grade") or str(enrollment_year or "")
  613. school_system_raw = row.get("school_system")
  614. try:
  615. school_system = int(school_system_raw) if school_system_raw not in (None, "") else None
  616. except Exception:
  617. school_system = None
  618. class_type = row.get("class_type")
  619. training_type = row.get("training_type")
  620. teacher_name = row.get("class_teacher_name")
  621. college_code = str(row.get("college_code") or "")
  622. # 判断班级是否已存在(sys_dept: dept_category = 'bj-' + class_code)
  623. dept_category = f"bj-{class_code}"
  624. cursor.execute("SELECT COUNT(1) AS cnt FROM sys_dept WHERE dept_name = %s AND dept_category = %s", (class_name, dept_category))
  625. if cursor.fetchone().get("cnt", 0) > 0:
  626. print(f"已存在 sys_dept - 跳过: {class_name} ({dept_category})")
  627. continue
  628. # 生成 dept_id
  629. seq_counter += 1
  630. dept_id = generate_id(base_timestamp, seq_counter)
  631. ancestors = get_parent_ancestors(connection, parent_id)
  632. current_time = datetime.now()
  633. # 插入 sys_dept
  634. insert_sys_dept(cursor, dept_id, "000000", parent_id, ancestors, class_name[:30], dept_category, order_num, current_time)
  635. inserted_count += 1
  636. print(f"插入 sys_dept: dept_id={dept_id}, parent_id={parent_id}, name={class_name}, order_num={order_num}")
  637. # ========== 构建并插入 xg_class_msg ==========
  638. # major info
  639. major_info = prepared["major_dept"]
  640. major_id = major_info.get("dept_id")
  641. major_name = major_info.get("dept_name", row.get("major_name"))
  642. # college info(通过 college_code -> name -> 在 sys_dept 查找)
  643. college_name_guess = COLLEGE_CODE_TO_NAME.get(college_code)
  644. college_id = None
  645. college_name = None
  646. if college_name_guess:
  647. # 尝试在 sys_dept 中查找学院 dept_id
  648. cursor.execute("SELECT dept_id, dept_name FROM sys_dept WHERE dept_category = %s LIMIT 1", (f"xy-{college_code}",))
  649. crow = cursor.fetchone()
  650. if crow:
  651. college_id = crow.get("dept_id")
  652. college_name = crow.get("dept_name")
  653. else:
  654. # 退回到 mapping 的名称
  655. college_name = college_name_guess
  656. else:
  657. # 尝试直接用 college_code 在 sys_dept 查找
  658. col_dept = resolve_college_dept(connection, college_code)
  659. if col_dept:
  660. college_id = col_dept.get("dept_id")
  661. college_name = col_dept.get("dept_name")
  662. # 如果仍未解析到 college_id,尝试通过学院名称在 sys_dept 中查找(fallback)
  663. if not college_id:
  664. # 优先使用映射名称,其次使用已有 college_name
  665. search_name = COLLEGE_CODE_TO_NAME.get(college_code) or college_name
  666. if search_name:
  667. try:
  668. cursor.execute("SELECT dept_id, dept_name FROM sys_dept WHERE dept_name = %s AND dept_category LIKE %s LIMIT 1", (search_name, "xy-%"))
  669. crow2 = cursor.fetchone()
  670. if crow2:
  671. college_id = crow2.get("dept_id")
  672. college_name = crow2.get("dept_name")
  673. except Exception:
  674. # 不阻塞主流程,保留 college_id 为 None
  675. pass
  676. # 班级在读/毕业判断
  677. has_active = class_has_active_student(connection, class_code)
  678. class_status = '1' if has_active else '3'
  679. is_graduation = 0 if has_active else 1
  680. # 计算毕业年份
  681. try:
  682. enrollment_year_int = int(enrollment_year) if enrollment_year else None
  683. except Exception:
  684. enrollment_year_int = None
  685. graduation_year = None
  686. if enrollment_year_int is not None and school_system is not None:
  687. try:
  688. graduation_year = enrollment_year_int + school_system
  689. except Exception:
  690. graduation_year = None
  691. # 尝试解析班主任 user_id,并填充到 class_msg.class_teacher_id
  692. class_teacher_id = resolve_teacher_user(connection, teacher_name)
  693. class_msg = {
  694. "id": dept_id,
  695. "tenant_id": "000000",
  696. "create_time": current_time,
  697. "update_time": current_time,
  698. "college_id": college_id,
  699. "college_name": college_name,
  700. "major_id": major_id,
  701. "major_name": major_name,
  702. "class_code": class_code,
  703. "class_name": class_name,
  704. "class_type": class_type,
  705. "grade": grade,
  706. "enrollment_year": enrollment_year_int,
  707. "graduation_year": graduation_year,
  708. "training_type": training_type,
  709. "class_teacher_id": class_teacher_id,
  710. "class_teacher_name": teacher_name,
  711. "class_status": class_status,
  712. "is_graduation": is_graduation,
  713. "graduation_date": None,
  714. "school_system": school_system
  715. }
  716. insert_xg_class_msg(cursor, class_msg)
  717. class_msg_inserted += 1
  718. print(f"插入 xg_class_msg: id={dept_id}, class_code={class_code}, class_name={class_name}")
  719. # ========== 插入班主任关系表(如能唯一解析到 teacher_id) ==========
  720. teacher_id = resolve_teacher_user(connection, teacher_name)
  721. if teacher_id:
  722. seq_counter += 1
  723. rel_id = generate_id(base_timestamp, seq_counter)
  724. # 处理 start_date:优先使用 enrollment_date 的解析结果;若为空且有 enrollment_year,则使用该年 9 月 1 日
  725. parsed_enroll_date = parse_date_to_mysql(row.get("enrollment_date"))
  726. start_date_value = parsed_enroll_date
  727. try:
  728. if not start_date_value:
  729. # enrollment_year_int 在前面已计算
  730. if enrollment_year_int:
  731. start_date_value = f"{enrollment_year_int}-09-01 00:00:00"
  732. except Exception:
  733. start_date_value = parsed_enroll_date
  734. rel = {
  735. "id": rel_id,
  736. "tenant_id": "000000",
  737. "create_time": current_time,
  738. "update_time": current_time,
  739. "class_id": dept_id,
  740. "class_name": class_name,
  741. "teacher_id": teacher_id,
  742. "teacher_name": teacher_name,
  743. "teacher_type": "class_teacher",
  744. "start_date": start_date_value,
  745. "end_date": None,
  746. "remark": None
  747. }
  748. insert_teacher_rel(cursor, rel)
  749. teacher_rel_inserted += 1
  750. print(f"插入班主任关系: rel_id={rel_id}, class_id={dept_id}, teacher_id={teacher_id}")
  751. else:
  752. if teacher_name:
  753. print(f"无法唯一解析班主任 '{teacher_name}' 到 sys_user.user_id,跳过关系插入")
  754. # 提交事务
  755. connection.commit()
  756. print("✓ 事务已提交")
  757. # 结束统计
  758. print("=" * 60)
  759. print(f"sys_dept 插入: {inserted_count}")
  760. print(f"xg_class_msg 插入: {class_msg_inserted}")
  761. print(f"xg_class_teacher_counselor_rel 插入: {teacher_rel_inserted}")
  762. print("=" * 60)
  763. except Exception as e:
  764. print("✗ 处理过程中发生异常:", e)
  765. finally:
  766. connection.close()
  767. if __name__ == "__main__":
  768. main()