This commit is contained in:
zlwu committed 2025-12-23 22:25:02 +08:00
1 parent 68b8feffc1
commit fca7096e9f
30 files changed
+4049 -8

No files matched your search

@@ -0,0 +1,854 @@
#!/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()