Files
2025-12-23 22:25:02 +08:00

855 lines
37 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
06 - 班级数据入库
从 his_xssj 表将班级数据导入学工表:
- sys_dept
- xg_class_msg
- xg_class_teacher_counselor_rel
脚本特性:
- 列名映射可配置(见 COLUMN_MAP)
- 学院编号映射(见 COLLEGE_CODE_TO_NAME)
- 对班级存在性检测:使用 sys_dept(dept_name, dept_category)
- 使用时间戳+序号生成 dept_id 与 class id(与现有脚本保持一致)
- 对班级中学生状态判定 class_status 与 is_graduation
- 对班主任通过 sys_user.nick_name 进行唯一性解析
使用方法:
- 在运行前确认 `db_config.py` 中 `DB_CONFIG` 可用
- 如需先预览请设置 DRY_RUN = True
"""
from __future__ import annotations
import time
from datetime import datetime
from typing import Dict, Any, List, Optional, Tuple
import pymysql
from db_config import DB_CONFIG
import os
import json
# 可根据 his_xssj 表模型调整映射(使用真实中文列名)
COLUMN_MAP: Dict[str, str] = {
"class_code": "班级编号", # 班级编号
"class_name": "班级名称", # 班级名称
"major_code": "专业编号", # 专业编号
"major_name": "专业名称", # 专业名称
"college_code": "学院编号", # 学院编号
"enrollment_year": "入学年份", # 入学年份(文本,需要转换为 int)
"grade": "所属年级", # 所属年级
"school_system": "学制编码", # 学制编码(文本,需要转换为 int)
"class_type": "层次编号", # 层次编号
"training_type": "学生类别", # 学生类别/培养方式
"class_teacher_name": "班主任", # 班主任姓名
"student_status": "学生状态", # 学生状态(判定是否为'在读')
"enrollment_date": "入学日期" # 入学日期(可为空)
}
# 学院编号对应关系(题主给定)
COLLEGE_CODE_TO_NAME: Dict[str, str] = {
"14": "智能控制学院",
"15": "智能制造学院",
"19": "人文学院",
"18": "餐饮休闲学院",
"28": "数字信息学院",
"16": "医药健康学院",
"17": "生态园林学院"
}
def extract_distinct_classes_to_his_bjsj(connection, dry_run: bool = False) -> int:
"""
(已移除写入逻辑)该函数保留为占位:如果需要从脚本内写入 his_bjsj,请使用外部迁移或重新启用写入逻辑。
当前实现仅返回 0 或在 dry_run 模式下打印示例,不执行对 his_bjsj 的 INSERT/DELETE 操作。
"""
select_sql = """
SELECT DISTINCT
`入学年份` AS enrollment_year,
`入学日期` AS enrollment_date,
`教学系部` AS teaching_dept,
`班级名称` AS class_name,
`班主任` AS class_teacher,
`学生类别` AS student_type,
`学院编号` AS college_code,
`专业编号` AS major_code,
`专业名称` AS major_name,
`层次编号` AS level_code,
`报读层次` AS level_name,
`所属年级` AS grade,
`班级编号` AS class_code,
`学制编码` AS school_system
FROM his_xssj
"""
with connection.cursor() as cursor:
cursor.execute(select_sql)
rows = cursor.fetchall()
if dry_run:
print(f"[DRY_RUN] 将从 his_xssj 提取 {len(rows)} 条去重班级记录(示例前 5 条)")
for r in rows[:5]:
print(r)
return 0
def get_connection():
return pymysql.connect(cursorclass=pymysql.cursors.DictCursor, **DB_CONFIG)
def fetch_distinct_classes(connection) -> List[Dict[str, Any]]:
"""
从 his_xssj 中取出按班级分组的基础信息(去重后的班级列表)
"""
cols = COLUMN_MAP
sql = f"""
SELECT
`{cols['class_code']}` AS class_code,
`{cols['class_name']}` AS class_name,
`{cols['major_code']}` AS major_code,
`{cols['major_name']}` AS major_name,
`{cols['college_code']}` AS college_code,
`{cols['enrollment_year']}` AS enrollment_year,
`{cols['grade']}` AS grade,
`{cols['school_system']}` AS school_system,
`{cols['class_type']}` AS class_type,
`{cols['training_type']}` AS training_type,
`{cols['class_teacher_name']}` AS class_teacher_name,
`{cols['enrollment_date']}` AS enrollment_date
FROM his_xssj
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
ORDER BY enrollment_year ASC, class_name ASC
"""
with connection.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchall()
def fetch_classes_from_his_bjsj(connection) -> List[Dict[str, Any]]:
"""
从 his_bjsj 中读取班级数据,作为后续插入学工表的唯一数据源。
返回的字段与原来从 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)。
注意:his_bjsj 表需事先存在且已由 extract_distinct_classes_to_his_bjsj 填充。
"""
sql = """
SELECT
class_code AS class_code,
class_name AS class_name,
major_code AS major_code,
major_name AS major_name,
college_code AS college_code,
enrollment_year AS enrollment_year,
grade AS grade,
school_system AS school_system,
level_code AS class_type,
student_type AS training_type,
class_teacher AS class_teacher_name,
enrollment_date AS enrollment_date
FROM his_bjsj
ORDER BY enrollment_year ASC, class_name ASC
"""
with connection.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchall()
def class_has_active_student(connection, class_code: str) -> bool:
"""
判断班级内是否存在至少一名学生状态为 '在读'
"""
cols = COLUMN_MAP
sql = f"""
SELECT COUNT(1) AS cnt FROM his_xssj
WHERE `{cols['class_code']}` = %s AND `{cols['student_status']}` = '在读'
"""
with connection.cursor() as cursor:
cursor.execute(sql, (class_code,))
row = cursor.fetchone()
return (row and row.get("cnt", 0) > 0)
def resolve_major_dept(connection, major_name: str, major_code: str, college_code: Optional[str] = None) -> Optional[Dict[str, Any]]:
"""
根据规则查找专业部门,优先尝试精确匹配 (dept_category = 'zy-'+major_code AND dept_name = major_name)。
若未命中且提供了 college_code,则:
1) 通过 dept_category = 'xy-'+college_code 在 sys_dept 查到学院 parent_id;
2) 在该学院的子节点中(parent_id = 学院dept_id,且 dept_category LIKE 'zy-%')按 dept_name 精确匹配 major_name。
返回 dict 包含 dept_id, ancestors, parent_id 等或 None。
"""
# 1) 精确匹配(保留原有逻辑)
if major_name is not None and major_code is not None and str(major_code).strip() != "":
sql = """
SELECT dept_id, parent_id, ancestors, dept_name, dept_category
FROM sys_dept
WHERE dept_name = %s AND dept_category = %s
LIMIT 1
"""
with connection.cursor() as cursor:
cursor.execute(sql, (major_name, f"zy-{major_code}"))
row = cursor.fetchone()
if row:
return row
# 2) 若未匹配到,且提供 college_code,则根据 his_bjsj 的 college_code 关联学院,再在学院下按名称查找专业
if college_code:
try:
sql_college = "SELECT dept_id FROM sys_dept WHERE dept_category = %s LIMIT 1"
with connection.cursor() as cursor:
cursor.execute(sql_college, (f"xy-{college_code}",))
college_row = cursor.fetchone()
if college_row and college_row.get("dept_id") is not None:
college_dept_id = college_row["dept_id"]
sql_child = """
SELECT dept_id, parent_id, ancestors, dept_name, dept_category
FROM sys_dept
WHERE parent_id = %s AND dept_category LIKE %s AND dept_name = %s
LIMIT 1
"""
with connection.cursor() as cursor:
cursor.execute(sql_child, (college_dept_id, "zy-%", major_name))
child = cursor.fetchone()
if child:
return child
except Exception:
# 不阻塞主流程,继续回退为 None
pass
return None
def resolve_college_dept(connection, college_code: str) -> Optional[Dict[str, Any]]:
"""
通过学院编号查找学院在 sys_dept 中的 dept_id(dept_category = 'xy-' + code)
"""
sql = """
SELECT dept_id, dept_name
FROM sys_dept
WHERE dept_category = %s
LIMIT 1
"""
with connection.cursor() as cursor:
cursor.execute(sql, (f"xy-{college_code}",))
return cursor.fetchone()
def resolve_teacher_user(connection, teacher_name: str) -> Optional[int]:
"""
通过 sys_user.nick_name 查找教师 user_id
- 唯一命中返回 user_id
- 多于或少于 1 条返回 None
"""
if not teacher_name:
return None
sql = """
SELECT user_id FROM sys_user WHERE nick_name = %s
"""
with connection.cursor() as cursor:
cursor.execute(sql, (teacher_name,))
rows = cursor.fetchall()
if len(rows) == 1:
return rows[0].get("user_id")
return None
def find_similar_majors(connection, major_name: str, limit: int = 5) -> List[Dict[str, Any]]:
"""
在 sys_dept 中查找与 major_name 相似的专业(dept_category LIKE 'zy-%')作为候选建议。
返回最多 limit 条候选,元素包含 dept_id 与 dept_name。
"""
if not major_name:
return []
sql = """
SELECT dept_id, dept_name, dept_category
FROM sys_dept
WHERE dept_category LIKE %s AND dept_name LIKE %s
LIMIT %s
"""
with connection.cursor() as cursor:
cursor.execute(sql, ('zy-%', f"%{major_name}%", limit))
return cursor.fetchall()
LOG_DIR = "logs"
SKIP_LOG_FILE = os.path.join(LOG_DIR, "06_insert_classes_skipped_majors.jsonl")
def ensure_log_dir():
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR, exist_ok=True)
def log_skipped_major(connection, row: Dict[str, Any]):
"""
将未找到父专业(major)导致跳过的班级信息记录到日志文件(JSONL),并返回候选建议列表。
"""
ensure_log_dir()
suggestions = find_similar_majors(connection, row.get("major_name") or "")
# 尝试获取 his_xssj 中该班级的所有原始记录(完整行),以便人工核对
his_rows: List[Dict[str, Any]] = []
class_code = row.get("class_code")
his_class_col = COLUMN_MAP.get("class_code")
try:
with connection.cursor() as c:
if his_class_col and class_code is not None:
sql = f"SELECT * FROM his_xssj WHERE `{his_class_col}` = %s"
c.execute(sql, (class_code,))
his_rows = c.fetchall()
except Exception:
his_rows = []
entry = {
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"class_code": class_code,
"class_name": row.get("class_name"),
"major_code": row.get("major_code"),
"major_name": row.get("major_name"),
"college_code": row.get("college_code"),
"row": row,
"his_rows_count": len(his_rows),
"his_rows": his_rows,
"suggestions": suggestions
}
try:
with open(SKIP_LOG_FILE, "a", encoding="utf-8") as fh:
fh.write(json.dumps(entry, ensure_ascii=False, default=str) + "\n")
except Exception:
# 写日志失败不应阻塞主流程
pass
return suggestions
def get_parent_ancestors(connection, parent_id: int) -> str:
sql = "SELECT ancestors FROM sys_dept WHERE dept_id = %s LIMIT 1"
with connection.cursor() as cursor:
cursor.execute(sql, (parent_id,))
row = cursor.fetchone()
if row and row.get("ancestors"):
return f"{row['ancestors']},{parent_id}"
return f"0,100,101,{parent_id}"
def get_max_order_num(connection, parent_id: int) -> int:
sql = "SELECT MAX(order_num) AS max_order FROM sys_dept WHERE parent_id = %s"
with connection.cursor() as cursor:
cursor.execute(sql, (parent_id,))
row = cursor.fetchone()
return int(row["max_order"]) if row and row["max_order"] is not None else 0
def generate_id(base_timestamp: str, index: int) -> int:
"""
生成 dept_id / class id: timestamp + 四位序号(与原脚本保持一致)
"""
return int(f"{base_timestamp}{str(index).zfill(4)}")
def parse_date_to_mysql(value: Any) -> Optional[str]:
"""
将各种可能的日期/时间值规范化为 MySQL DATETIME 字符串 'YYYY-MM-DD HH:MM:SS'。
- 支持 datetime 对象直接格式化
- 支持常见字符串格式,如 'YYYY-MM-DD HH:MM:SS', 'YYYY/MM/DD HH:MM:SS', 'M/D/YYYY H:M:S', 'YYYY-MM-DD', 'M/D/YYYY'
- 若无法解析或为 None,返回 None
"""
if value is None:
return None
if isinstance(value, datetime):
return value.strftime("%Y-%m-%d %H:%M:%S")
if not isinstance(value, str):
try:
# 尝试将数字型视为时间戳(秒)
ts = float(value)
dt = datetime.fromtimestamp(ts)
return dt.strftime("%Y-%m-%d %H:%M:%S")
except Exception:
return None
s = value.strip()
if s == "":
return None
patterns = [
"%Y-%m-%d %H:%M:%S",
"%Y/%m/%d %H:%M:%S",
"%m/%d/%Y %H:%M:%S",
"%m/%d/%y %H:%M:%S",
"%Y-%m-%d",
"%Y/%m/%d",
"%m/%d/%Y",
"%m/%d/%y",
]
for p in patterns:
try:
dt = datetime.strptime(s, p)
# 如果没有时分秒,补 00:00:00
return dt.strftime("%Y-%m-%d %H:%M:%S")
except Exception:
continue
# 兜底:尝试解析含有空格并用 '/' 或 '-' 的混合格式(例如 '9/1/2009 0:0:0' 已被覆盖上面,但保留此处以防)
try:
parts = s.split()
date_part = parts[0]
time_part = parts[1] if len(parts) > 1 else "0:0:0"
md = date_part.replace("-", "/")
combined = f"{md} {time_part}"
dt = datetime.strptime(combined, "%m/%d/%Y %H:%M:%S")
return dt.strftime("%Y-%m-%d %H:%M:%S")
except Exception:
return None
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):
sql = """
INSERT INTO sys_dept (
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
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
"""
params = (
dept_id, tenant_id, parent_id, ancestors, dept_name, dept_category,
order_num, None, None, None, '0', '0', 0, 1, current_time, 1, current_time
)
cursor.execute(sql, params)
def insert_xg_class_msg(cursor, class_data: Dict[str, Any]):
sql = """
INSERT INTO xg_class_msg (
id, tenant_id, create_dept, create_by, create_time,
update_by, update_time, deleted,
college_id, college_name, major_id, major_name,
class_code, class_name, class_type, grade, enrollment_year,
graduation_year, training_type, degree_type,
counselor_id, counselor_name, class_teacher_id, class_teacher_name,
class_advisor_id, class_advisor_name,
class_monitor_id, class_monitor_name,
deputy_monitor_id, deputy_monitor_name,
class_status, is_graduation, graduation_date, school_system
) VALUES (
%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
)
"""
params = (
class_data.get("id"),
class_data.get("tenant_id", "000000"),
class_data.get("create_dept", 0),
class_data.get("create_by", 1),
class_data.get("create_time"),
class_data.get("update_by", 1),
class_data.get("update_time"),
class_data.get("deleted", 0),
class_data.get("college_id"),
class_data.get("college_name"),
class_data.get("major_id"),
class_data.get("major_name"),
class_data.get("class_code"),
class_data.get("class_name"),
class_data.get("class_type"),
class_data.get("grade"),
class_data.get("enrollment_year"),
class_data.get("graduation_year"),
class_data.get("training_type"),
None, # degree_type
None, None,
class_data.get("class_teacher_id"),
class_data.get("class_teacher_name"),
None, None, None, None, None, None,
class_data.get("class_status"),
class_data.get("is_graduation"),
class_data.get("graduation_date"),
class_data.get("school_system")
)
cursor.execute(sql, params)
def insert_teacher_rel(cursor, rel_data: Dict[str, Any]):
sql = """
INSERT INTO xg_class_teacher_counselor_rel (
id, tenant_id, create_dept, create_by, create_time,
update_by, update_time, deleted, class_id, class_name,
teacher_id, teacher_name, teacher_type, start_date, end_date, remark
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
"""
params = (
rel_data.get("id"),
rel_data.get("tenant_id", "000000"),
rel_data.get("create_dept", 0),
rel_data.get("create_by", 1),
rel_data.get("create_time"),
rel_data.get("update_by", 1),
rel_data.get("update_time"),
rel_data.get("deleted", 0),
rel_data.get("class_id"),
rel_data.get("class_name"),
rel_data.get("teacher_id"),
rel_data.get("teacher_name"),
rel_data.get("teacher_type"),
rel_data.get("start_date"),
rel_data.get("end_date"),
rel_data.get("remark")
)
cursor.execute(sql, params)
def main():
print("开始:06 - 班级数据入库")
base_timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
seq_counter = 0
connection = get_connection()
try:
# 在开始时询问是否删除 sys_dept 中 dept_category 以 bj- 开头的记录
def prompt_delete_sys_dept_bj(conn) -> bool:
try:
ans = input("是否先删除 `sys_dept` 中 dept_category 以 'bj-' 开头的记录? [y/N]: ").strip().lower()
except (EOFError, KeyboardInterrupt):
print("\\n未检测到交互输入,默认不删除 sys_dept 的 bj- 记录。")
return False
if ans not in ("y", "yes"):
return False
try:
with conn.cursor() as c:
c.execute("SELECT COUNT(1) AS cnt FROM sys_dept WHERE dept_category LIKE %s", (f"bj-%",))
cnt = int(c.fetchone().get("cnt", 0))
print(f"将删除 sys_dept 中 {cnt} 条 dept_category LIKE 'bj-%' 的记录。")
try:
confirm = input("确认删除并继续?此操作不可恢复。输入 y 确认: ").strip().lower()
except (EOFError, KeyboardInterrupt):
print("\\n未检测到交互输入,取消删除。")
return False
if confirm not in ("y", "yes"):
print("已取消删除操作,继续执行导入(不删除)。")
return False
with conn.cursor() as c:
c.execute("DELETE FROM sys_dept WHERE dept_category LIKE %s", (f"bj-%",))
conn.commit()
print("已删除并提交:sys_dept 中 dept_category LIKE 'bj-%' 的记录。")
return True
except Exception as e:
print("删除 sys_dept 过程中发生异常,跳过删除:", e)
return False
try:
deleted_sys_dept_bj = prompt_delete_sys_dept_bj(connection)
if deleted_sys_dept_bj:
print("用户已确认并删除 sys_dept 中 bj- 开头的记录,继续后续流程。")
else:
print("未删除 sys_dept 的 bj- 记录,继续执行导入流程。")
except Exception as e:
print("询问或删除 sys_dept(bj-) 过程中发生异常,继续执行导入:", e)
# 在正式执行导入前,询问用户是否要先删除历史数据
def prompt_delete_tables(conn) -> bool:
"""
交互式询问并在确认后删除 xg_class_msg 与 xg_class_teacher_counselor_rel 中的数据。
返回 True 表示已执行删除并提交,False 表示未删除。
"""
try:
ans = input("是否先删除 `xg_class_msg` 与 `xg_class_teacher_counselor_rel` 中的现有数据? [y/N]: ").strip().lower()
except (EOFError, KeyboardInterrupt):
print("\\n未检测到交互输入,默认不删除。")
return False
if ans not in ("y", "yes"):
return False
with conn.cursor() as c:
c.execute("SELECT COUNT(1) AS cnt FROM xg_class_msg")
cnt_msg = int(c.fetchone().get("cnt", 0))
c.execute("SELECT COUNT(1) AS cnt FROM xg_class_teacher_counselor_rel")
cnt_rel = int(c.fetchone().get("cnt", 0))
print(f"将删除 xg_class_msg 中 {cnt_msg} 条,xg_class_teacher_counselor_rel 中 {cnt_rel} 条。")
try:
confirm = input("确认删除并继续?此操作不可恢复。输入 y 确认: ").strip().lower()
except (EOFError, KeyboardInterrupt):
print("\\n未检测到交互输入,取消删除。")
return False
if confirm not in ("y", "yes"):
print("已取消删除操作,继续执行导入(不删除)。")
return False
with conn.cursor() as c:
# 先删除关系表,再删除班级表
c.execute("DELETE FROM xg_class_teacher_counselor_rel")
c.execute("DELETE FROM xg_class_msg")
conn.commit()
print("已删除并提交:xg_class_msg 与 xg_class_teacher_counselor_rel。")
return True
# 在继续前询问是否删除历史数据(仅在交互式环境有意义)
try:
deleted = prompt_delete_tables(connection)
if deleted:
print("用户已确认并删除历史数据,继续执行后续导入流程。")
else:
print("未删除历史数据,继续执行导入流程。")
except Exception as e:
print("询问或删除过程中发生异常,继续执行导入:", e)
# 注意:脚本已移除对 his_bjsj 的写入/删除操作。请确保 his_bjsj 表由外部迁移或手动创建并填充。
print("提示:脚本不会对 his_bjsj 执行删除或插入操作,后续将直接从 his_bjsj 读取数据。")
# 从 his_bjsj 中读取班级数据作为唯一数据源(his_bjsj 应由前面的抽取步骤或外部迁移提前填充)
class_rows = fetch_classes_from_his_bjsj(connection)
print(f"共发现 {len(class_rows)} 个去重后的班级记录(将按专业分组并插入)")
# 预分组:按 major(parent) 收集要插入的班级以便计算 order_num
by_parent: Dict[int, List[Dict[str, Any]]] = {}
prepared_classes: List[Dict[str, Any]] = []
for r in class_rows:
class_code = r.get("class_code")
class_name = r.get("class_name")
major_code = str(r.get("major_code") or "")
major_name = r.get("major_name")
major_dept = resolve_major_dept(connection, major_name, major_code, str(r.get("college_code") or ""))
# 若未命中,则尝试从 his_bjsj 补全班级信息后再次匹配
if not major_dept:
try:
with connection.cursor() as c:
c.execute("SELECT major_code, major_name, college_code, enrollment_year, enrollment_date FROM his_bjsj WHERE class_code = %s LIMIT 1", (class_code,))
bj = c.fetchone()
if bj:
# 用 his_bjsj 的值覆盖 row 中的同类字段以便后续匹配
major_code = str(bj.get("major_code") or "")
major_name = bj.get("major_name") or major_name
# 也更新原始 row,便于日志和后续处理
r["major_code"] = major_code
r["major_name"] = major_name
r["college_code"] = bj.get("college_code") or r.get("college_code")
r["enrollment_year"] = bj.get("enrollment_year") or r.get("enrollment_year")
r["enrollment_date"] = bj.get("enrollment_date") or r.get("enrollment_date")
major_dept = resolve_major_dept(connection, major_name, major_code, str(r.get("college_code") or ""))
except Exception:
major_dept = None
if not major_dept:
# 记录并打印更详细的信息(包含专业编号与名称),同时输出候选建议
suggestions = log_skipped_major(connection, r)
print(f"跳过班级(未找到父专业):{class_code} {class_name} 专业({major_code} - {major_name})")
if suggestions:
cand_str = "; ".join([f"{s.get('dept_id')}:{s.get('dept_name')}" for s in suggestions])
print(f" 建议候选专业:{cand_str}")
else:
print(" 无相似候选专业。已写入 logs/06_insert_classes_skipped_majors.jsonl 以便人工核对。")
continue
parent_id = major_dept["dept_id"]
prepared = {
"row": r,
"parent_id": parent_id,
"major_dept": major_dept
}
by_parent.setdefault(parent_id, []).append(prepared)
prepared_classes.append(prepared)
# 在每个 parent_id 下按 enrollment_year, class_name 排序并计算 order_num 起点
insertion_plan: List[Tuple[int, Dict[str, Any]]] = [] # list of (order_num, prepared)
for parent_id, class_list in by_parent.items():
max_order = get_max_order_num(connection, parent_id)
# sort by enrollment_year (统一转为 int,发生异常或为空时视为 0) 然后按 class_name
def _safe_year_key(p: Dict[str, Any]) -> int:
val = p["row"].get("enrollment_year")
if val is None or val == "":
return 0
try:
return int(val)
except Exception:
return 0
class_list.sort(key=lambda p: (_safe_year_key(p), p["row"].get("class_name") or ""))
for idx, item in enumerate(class_list, start=1):
item_order = max_order + idx
item["order_num"] = item_order
insertion_plan.append((item_order, item))
# 按插入顺序执行插入(可调整为按parent分批提交)
inserted_count = 0
class_msg_inserted = 0
teacher_rel_inserted = 0
# 使用单个连接事务(最后 commit)
with connection.cursor() as cursor:
for item in sorted(insertion_plan, key=lambda x: (x[1]["parent_id"], x[0])):
prepared = item[1]
row = prepared["row"]
parent_id = prepared["parent_id"]
order_num = prepared["order_num"]
class_code = row.get("class_code")
class_name = row.get("class_name") or ""
enrollment_year = row.get("enrollment_year")
grade = row.get("grade") or str(enrollment_year or "")
school_system_raw = row.get("school_system")
try:
school_system = int(school_system_raw) if school_system_raw not in (None, "") else None
except Exception:
school_system = None
class_type = row.get("class_type")
training_type = row.get("training_type")
teacher_name = row.get("class_teacher_name")
college_code = str(row.get("college_code") or "")
# 判断班级是否已存在(sys_dept: dept_category = 'bj-' + class_code)
dept_category = f"bj-{class_code}"
cursor.execute("SELECT COUNT(1) AS cnt FROM sys_dept WHERE dept_name = %s AND dept_category = %s", (class_name, dept_category))
if cursor.fetchone().get("cnt", 0) > 0:
print(f"已存在 sys_dept - 跳过: {class_name} ({dept_category})")
continue
# 生成 dept_id
seq_counter += 1
dept_id = generate_id(base_timestamp, seq_counter)
ancestors = get_parent_ancestors(connection, parent_id)
current_time = datetime.now()
# 插入 sys_dept
insert_sys_dept(cursor, dept_id, "000000", parent_id, ancestors, class_name[:30], dept_category, order_num, current_time)
inserted_count += 1
print(f"插入 sys_dept: dept_id={dept_id}, parent_id={parent_id}, name={class_name}, order_num={order_num}")
# ========== 构建并插入 xg_class_msg ==========
# major info
major_info = prepared["major_dept"]
major_id = major_info.get("dept_id")
major_name = major_info.get("dept_name", row.get("major_name"))
# college info(通过 college_code -> name -> 在 sys_dept 查找)
college_name_guess = COLLEGE_CODE_TO_NAME.get(college_code)
college_id = None
college_name = None
if college_name_guess:
# 尝试在 sys_dept 中查找学院 dept_id
cursor.execute("SELECT dept_id, dept_name FROM sys_dept WHERE dept_category = %s LIMIT 1", (f"xy-{college_code}",))
crow = cursor.fetchone()
if crow:
college_id = crow.get("dept_id")
college_name = crow.get("dept_name")
else:
# 退回到 mapping 的名称
college_name = college_name_guess
else:
# 尝试直接用 college_code 在 sys_dept 查找
col_dept = resolve_college_dept(connection, college_code)
if col_dept:
college_id = col_dept.get("dept_id")
college_name = col_dept.get("dept_name")
# 如果仍未解析到 college_id,尝试通过学院名称在 sys_dept 中查找(fallback)
if not college_id:
# 优先使用映射名称,其次使用已有 college_name
search_name = COLLEGE_CODE_TO_NAME.get(college_code) or college_name
if search_name:
try:
cursor.execute("SELECT dept_id, dept_name FROM sys_dept WHERE dept_name = %s AND dept_category LIKE %s LIMIT 1", (search_name, "xy-%"))
crow2 = cursor.fetchone()
if crow2:
college_id = crow2.get("dept_id")
college_name = crow2.get("dept_name")
except Exception:
# 不阻塞主流程,保留 college_id 为 None
pass
# 班级在读/毕业判断
has_active = class_has_active_student(connection, class_code)
class_status = '1' if has_active else '3'
is_graduation = 0 if has_active else 1
# 计算毕业年份
try:
enrollment_year_int = int(enrollment_year) if enrollment_year else None
except Exception:
enrollment_year_int = None
graduation_year = None
if enrollment_year_int is not None and school_system is not None:
try:
graduation_year = enrollment_year_int + school_system
except Exception:
graduation_year = None
# 尝试解析班主任 user_id,并填充到 class_msg.class_teacher_id
class_teacher_id = resolve_teacher_user(connection, teacher_name)
class_msg = {
"id": dept_id,
"tenant_id": "000000",
"create_time": current_time,
"update_time": current_time,
"college_id": college_id,
"college_name": college_name,
"major_id": major_id,
"major_name": major_name,
"class_code": class_code,
"class_name": class_name,
"class_type": class_type,
"grade": grade,
"enrollment_year": enrollment_year_int,
"graduation_year": graduation_year,
"training_type": training_type,
"class_teacher_id": class_teacher_id,
"class_teacher_name": teacher_name,
"class_status": class_status,
"is_graduation": is_graduation,
"graduation_date": None,
"school_system": school_system
}
insert_xg_class_msg(cursor, class_msg)
class_msg_inserted += 1
print(f"插入 xg_class_msg: id={dept_id}, class_code={class_code}, class_name={class_name}")
# ========== 插入班主任关系表(如能唯一解析到 teacher_id) ==========
teacher_id = resolve_teacher_user(connection, teacher_name)
if teacher_id:
seq_counter += 1
rel_id = generate_id(base_timestamp, seq_counter)
# 处理 start_date:优先使用 enrollment_date 的解析结果;若为空且有 enrollment_year,则使用该年 9 月 1 日
parsed_enroll_date = parse_date_to_mysql(row.get("enrollment_date"))
start_date_value = parsed_enroll_date
try:
if not start_date_value:
# enrollment_year_int 在前面已计算
if enrollment_year_int:
start_date_value = f"{enrollment_year_int}-09-01 00:00:00"
except Exception:
start_date_value = parsed_enroll_date
rel = {
"id": rel_id,
"tenant_id": "000000",
"create_time": current_time,
"update_time": current_time,
"class_id": dept_id,
"class_name": class_name,
"teacher_id": teacher_id,
"teacher_name": teacher_name,
"teacher_type": "class_teacher",
"start_date": start_date_value,
"end_date": None,
"remark": None
}
insert_teacher_rel(cursor, rel)
teacher_rel_inserted += 1
print(f"插入班主任关系: rel_id={rel_id}, class_id={dept_id}, teacher_id={teacher_id}")
else:
if teacher_name:
print(f"无法唯一解析班主任 '{teacher_name}' 到 sys_user.user_id,跳过关系插入")
# 提交事务
connection.commit()
print("✓ 事务已提交")
# 结束统计
print("=" * 60)
print(f"sys_dept 插入: {inserted_count}")
print(f"xg_class_msg 插入: {class_msg_inserted}")
print(f"xg_class_teacher_counselor_rel 插入: {teacher_rel_inserted}")
print("=" * 60)
except Exception as e:
print("✗ 处理过程中发生异常:", e)
finally:
connection.close()
if __name__ == "__main__":
main()