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

@@ -64,14 +64,14 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
@Override @Override
public TableDataInfo<SysUserVo> selectPageUserList(SysUserBo user, PageQuery pageQuery) { public TableDataInfo<SysUserVo> selectPageUserList(SysUserBo user, PageQuery pageQuery) {
pageQuery.withDefaultOrder("u.user_name"); pageQuery.withDefaultOrder("u.user_name", "asc");
Page<SysUserVo> page = baseMapper.selectPageUserList(pageQuery.build(), this.buildQueryWrapper(user)); Page<SysUserVo> page = baseMapper.selectPageUserList(pageQuery.build(), this.buildQueryWrapper(user));
return TableDataInfo.build(page); return TableDataInfo.build(page);
} }
@Override @Override
public TableDataInfo<SysUserVo> selectPageUserListByOr(SysUserBo user, PageQuery pageQuery) { public TableDataInfo<SysUserVo> selectPageUserListByOr(SysUserBo user, PageQuery pageQuery) {
pageQuery.withDefaultOrder("u.user_name"); pageQuery.withDefaultOrder("u.user_name", "asc");
Page<SysUserVo> page = baseMapper.selectPageUserList(pageQuery.build(), this.buildQueryWrapperByOr(user)); Page<SysUserVo> page = baseMapper.selectPageUserList(pageQuery.build(), this.buildQueryWrapperByOr(user));
return TableDataInfo.build(page); return TableDataInfo.build(page);
} }
@@ -24,7 +24,7 @@
${ew.getCustomSqlSegment} ${ew.getCustomSqlSegment}
<choose> <choose>
<when test="ew != null and @org.dromara.common.core.utils.StringUtils@containsAnyIgnoreCase(ew.getCustomSqlSegment, 'order by')"> <when test="ew != null and @org.dromara.common.core.utils.StringUtils@containsAnyIgnoreCase(ew.getCustomSqlSegment, 'order by')">
, u.user_name asc, u.create_time desc , u.create_time desc
</when> </when>
<otherwise> <otherwise>
order by u.user_name asc, u.create_time desc order by u.user_name asc, u.create_time desc
@@ -45,7 +45,7 @@
${ew.getCustomSqlSegment} ${ew.getCustomSqlSegment}
<choose> <choose>
<when test="ew != null and @org.dromara.common.core.utils.StringUtils@containsAnyIgnoreCase(ew.getCustomSqlSegment, 'order by')"> <when test="ew != null and @org.dromara.common.core.utils.StringUtils@containsAnyIgnoreCase(ew.getCustomSqlSegment, 'order by')">
, u.user_name asc, u.create_time desc , u.create_time desc
</when> </when>
<otherwise> <otherwise>
order by u.user_name asc, u.create_time desc order by u.user_name asc, u.create_time desc
@@ -63,7 +63,7 @@
${ew.getCustomSqlSegment} ${ew.getCustomSqlSegment}
<choose> <choose>
<when test="ew != null and @org.dromara.common.core.utils.StringUtils@containsAnyIgnoreCase(ew.getCustomSqlSegment, 'order by')"> <when test="ew != null and @org.dromara.common.core.utils.StringUtils@containsAnyIgnoreCase(ew.getCustomSqlSegment, 'order by')">
, u.user_name asc, u.create_time desc , u.create_time desc
</when> </when>
<otherwise> <otherwise>
order by u.user_name asc, u.create_time desc order by u.user_name asc, u.create_time desc
@@ -80,7 +80,7 @@
${ew.getCustomSqlSegment} ${ew.getCustomSqlSegment}
<choose> <choose>
<when test="ew != null and @org.dromara.common.core.utils.StringUtils@containsAnyIgnoreCase(ew.getCustomSqlSegment, 'order by')"> <when test="ew != null and @org.dromara.common.core.utils.StringUtils@containsAnyIgnoreCase(ew.getCustomSqlSegment, 'order by')">
, u.user_name asc, u.create_time desc , u.create_time desc
</when> </when>
<otherwise> <otherwise>
order by u.user_name asc, u.create_time desc order by u.user_name asc, u.create_time desc
@@ -45,6 +45,12 @@ public class XgClassMsgController extends BaseController {
@SaCheckPermission("classMsg:classMsg:list") @SaCheckPermission("classMsg:classMsg:list")
@GetMapping("/list") @GetMapping("/list")
public TableDataInfo<XgClassMsgVo> list(XgClassMsgBo bo, PageQuery pageQuery) { public TableDataInfo<XgClassMsgVo> list(XgClassMsgBo bo, PageQuery pageQuery) {
// Disable the framework default ordering (create_time desc) so the service's
// wrapper-defined order (grade, collegeName, majorName, classStatus) takes
// effect.
if (pageQuery != null) {
pageQuery.disableDefaultOrder();
}
return xgClassMsgService.queryPageList(bo, pageQuery); return xgClassMsgService.queryPageList(bo, pageQuery);
} }
@@ -2014,6 +2014,27 @@ const cloneDeptTreeNodes = (nodes: any[]): any[] => {
}); });
}; };
/** 规范化部门树节点(确保 label / originalLabel / disabled 存在),递归处理 */
const processDeptTreeData = (nodes: any[]): any[] => {
return (nodes || []).map((node) => {
const processed: any = { ...node };
// 保留原始 label,兼容不同接口字段(label / name)
processed.originalLabel = processed.originalLabel ?? processed.label ?? processed.name ?? '';
processed.label = processed.label ?? processed.name ?? processed.originalLabel;
// 确保 disabled 字段存在,避免某些节点被意外隐藏/不可选
processed.disabled = processed.disabled ?? false;
if (Array.isArray(processed.children) && processed.children.length > 0) {
processed.children = processDeptTreeData(processed.children);
} else {
processed.children = [];
}
return processed;
});
};
/** 增强树节点(补充班级状态) */ /** 增强树节点(补充班级状态) */
const enhanceDeptTreeNodes = (nodes: any[]): any[] => { const enhanceDeptTreeNodes = (nodes: any[]): any[] => {
return (nodes || []).map((node) => { return (nodes || []).map((node) => {
@@ -2042,11 +2063,17 @@ const filterDeptTreeByStatus = (nodes: any[], status?: string): any[] => {
} else { } else {
cloned.children = []; cloned.children = [];
} }
// 如果开启了状态过滤,仅保留符合状态的班级节点
if (targetStatus && isClassNode(cloned)) { if (targetStatus && isClassNode(cloned)) {
if (cloned.classStatus !== targetStatus) { if (cloned.classStatus !== targetStatus) {
return null; return null;
} }
} }
// 当开启状态过滤且节点不是班级节点且没有子节点时,过滤掉该节点(和 stuRoster 行为一致)
if (targetStatus && !isClassNode(cloned) && (!cloned.children || cloned.children.length === 0)) {
return null;
}
return cloned; return cloned;
}) })
.filter((item) => item !== null) as any[]; .filter((item) => item !== null) as any[];
@@ -2142,8 +2169,10 @@ const extractDeptTreeData = (payload: any): any[] => {
/** 构建部门树 */ /** 构建部门树 */
const rebuildDeptTree = async (treeData: any[], resetSelection = true) => { const rebuildDeptTree = async (treeData: any[], resetSelection = true) => {
// 先规范化树结构,保证每个节点都有 label / disabled / children 字段
const processed = processDeptTreeData(treeData);
await ensureClassStatusMap(); await ensureClassStatusMap();
const enhanced = enhanceDeptTreeNodes(treeData); const enhanced = enhanceDeptTreeNodes(processed);
rawDeptOptions.value = enhanced; rawDeptOptions.value = enhanced;
applyClassStatusFilter(!resetSelection); applyClassStatusFilter(!resetSelection);
if (resetSelection) { if (resetSelection) {
@@ -3215,7 +3244,7 @@ onMounted(() => {
/* 部门树组件样式 */ /* 部门树组件样式 */
.dept-tree { .dept-tree {
height: 600px; height: calc(100vh - 280px); /* 与 stuRoster 左侧树一致,随视窗高度自适应 */
overflow-y: auto; overflow-y: auto;
overflow-x: auto; overflow-x: auto;
width: 330px; /* 固定宽度,超出时显示滚动条 */ width: 330px; /* 固定宽度,超出时显示滚动条 */
@@ -0,0 +1,285 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
导入学校历史学生和老师数据到 MySQL 表
用法:
python import_history.py
依赖:
pip install -r requirements.txt
说明:
- 从同目录下的 db_config.py 读取 DB_CONFIG
- 将 `student.xls` 导入到表 `his_xssj`
- 将 `teacher.xlsx` 导入到表 `his_lssj`
- 自动创建表(所有列使用 TEXT 类型),按批次写入以节省内存
"""
from __future__ import annotations
import os
import sys
import math
import argparse
import importlib.util
from typing import List, Dict, Any
import pandas as pd
from sqlalchemy import create_engine, types as sql_types
# Optional, used for xls/xlsx low-level reading
try:
import xlrd # for .xls
except Exception:
xlrd = None
try:
import openpyxl # for .xlsx
except Exception:
openpyxl = None
def load_db_config_from_file(config_path: str) -> dict:
spec = importlib.util.spec_from_file_location("db_config", config_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # type: ignore
if not hasattr(module, "DB_CONFIG"):
raise RuntimeError(f"No DB_CONFIG found in {config_path}")
return getattr(module, "DB_CONFIG")
def sanitize_columns(header: List[str]) -> List[str]:
seen: Dict[str, int] = {}
out: List[str] = []
for i, col in enumerate(header):
if col is None:
name = f"col_{i+1}"
else:
name = str(col).strip()
if name == "":
name = f"col_{i+1}"
# replace problematic characters
name = name.replace(" ", "_").replace("\n", "_").replace("\r", "_")
# ensure starts with letter or underscore
if not name[0].isalpha() and name[0] != "_":
name = f"c_{name}"
base = name
cnt = seen.get(base, 0)
if cnt:
name = f"{base}_{cnt}"
seen[base] = cnt + 1
out.append(name)
return out
def df_to_mysql_append(engine, df: pd.DataFrame, table_name: str, first_write: bool):
# ensure all columns are strings (TEXT)
df = df.where(pd.notnull(df), None)
if first_write:
# create table with TEXT columns by writing empty dataframe with dtype mapping
empty = df.head(0)
dtype_map = {col: sql_types.Text() for col in empty.columns}
empty.to_sql(name=table_name, con=engine, index=False, if_exists="replace", dtype=dtype_map)
# then append the first actual df
df.to_sql(name=table_name, con=engine, index=False, if_exists="append", chunksize=1000, method="multi")
else:
df.to_sql(name=table_name, con=engine, index=False, if_exists="append", chunksize=1000, method="multi")
def process_xls(filepath: str, engine, table_name: str, chunk_size: int = 2000):
if xlrd is None:
raise RuntimeError("Missing xlrd library to read .xls files. Install from requirements.")
book = xlrd.open_workbook(filepath, formatting_info=False)
sheet = book.sheet_by_index(0)
header = [str(c).strip() for c in sheet.row_values(0)]
header = sanitize_columns(header)
total = sheet.nrows - 1
print(f"读取 .xls 文件 {filepath}, 行数(不含表头): {total}")
batch: List[Dict[str, Any]] = []
first_write = True
for r in range(1, sheet.nrows):
row = sheet.row_values(r)
# convert nan floats to None
row = [None if (isinstance(x, float) and math.isnan(x)) else x for x in row]
row_dict = {col: row[idx] if idx < len(row) else None for idx, col in enumerate(header)}
batch.append(row_dict)
if len(batch) >= chunk_size:
df = pd.DataFrame(batch)
df_to_mysql_append(engine, df, table_name, first_write)
first_write = False
batch = []
print(f"已写入 {chunk_size} 行到 {table_name}")
if batch:
df = pd.DataFrame(batch)
df_to_mysql_append(engine, df, table_name, first_write)
print(f"写入剩余 {len(batch)} 行到 {table_name}")
def process_xlsx(filepath: str, engine, table_name: str, chunk_size: int = 2000):
if openpyxl is None:
raise RuntimeError("Missing openpyxl library to read .xlsx files. Install from requirements.")
wb = openpyxl.load_workbook(filename=filepath, read_only=True, data_only=True)
sheet = wb.active
rows = sheet.iter_rows(values_only=True)
try:
header_row = next(rows)
except StopIteration:
print(f"{filepath} 是空文件")
return
header = [str(c).strip() if c is not None else "" for c in header_row]
header = sanitize_columns(header)
batch: List[Dict[str, Any]] = []
first_write = True
written = 0
for row in rows:
row = [None if (isinstance(x, float) and math.isnan(x)) else x for x in row]
row_dict = {col: row[idx] if idx < len(row) else None for idx, col in enumerate(header)}
batch.append(row_dict)
if len(batch) >= chunk_size:
df = pd.DataFrame(batch)
df_to_mysql_append(engine, df, table_name, first_write)
first_write = False
written += len(batch)
batch = []
print(f"已写入 {written} 行到 {table_name}")
if batch:
df = pd.DataFrame(batch)
df_to_mysql_append(engine, df, table_name, first_write)
written += len(batch)
print(f"写入剩余 {len(batch)} 行到 {table_name},总计写入 {written} 行")
def process_file(filepath: str, engine, table_name: str, chunk_size: int = 2000):
ext = os.path.splitext(filepath)[1].lower()
if ext == ".xls":
process_xls(filepath, engine, table_name, chunk_size=chunk_size)
elif ext in (".xlsx",):
process_xlsx(filepath, engine, table_name, chunk_size=chunk_size)
else:
# try pandas fallback (may be memory heavy)
print(f"未知扩展名 {ext},尝试使用 pandas 直接读取(可能占用大量内存)")
df = pd.read_excel(filepath, engine="openpyxl" if ext == ".xlsx" else None)
df.columns = sanitize_columns(list(df.columns))
df_to_mysql_append(engine, df, table_name, first_write=True)
def build_engine_from_config(db_config: dict):
user = db_config.get("user")
password = db_config.get("password")
host = db_config.get("host", "localhost")
port = db_config.get("port", 3306)
database = db_config.get("database")
charset = db_config.get("charset", "utf8mb4")
uri = f"mysql+pymysql://{user}:{password}@{host}:{port}/{database}?charset={charset}"
engine = create_engine(uri, pool_pre_ping=True)
return engine
def main(args):
base_dir = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(base_dir, "db_config.py")
if not os.path.exists(config_path):
print(f"找不到配置文件: {config_path}")
sys.exit(1)
db_config = load_db_config_from_file(config_path)
engine = build_engine_from_config(db_config)
# 在每次导入前询问是否清空目标表(或使用 --yes 自动确认)
should_truncate = False
if args and getattr(args, "yes", False):
should_truncate = True
else:
resp = input("是否在导入前清空目标表 `his_xssj` 和 `his_lssj`?(yes/no) [no]: ").strip().lower()
if resp in ("y", "yes"):
should_truncate = True
if should_truncate:
print("尝试清空目标表: his_xssj, his_lssj ...")
try:
with engine.begin() as conn:
# 关闭外键检查以确保能够 truncate 有外键依赖的表
conn.exec_driver_sql("SET FOREIGN_KEY_CHECKS=0;")
conn.exec_driver_sql("TRUNCATE TABLE `his_xssj`;")
conn.exec_driver_sql("TRUNCATE TABLE `his_lssj`;")
conn.exec_driver_sql("SET FOREIGN_KEY_CHECKS=1;")
print("目标表已清空(如果表存在且有权限)")
except Exception as e:
print(f"清空表时发生异常(可能表不存在或无权限),继续执行导入。错误:{e}")
else:
print("跳过清空表,继续导入(若需要清空请重新运行并使用 --yes 参数)。")
student_file = os.path.join(base_dir, "student1222.xls")
teacher_file = os.path.join(base_dir, "teacher.xlsx")
if not os.path.exists(student_file):
print(f"找不到学生文件: {student_file}")
else:
print("开始导入学生数据到表 his_xssj ...")
process_file(student_file, engine, "his_xssj", chunk_size=2000)
print("学生数据导入完成 -> his_xssj")
if not os.path.exists(teacher_file):
print(f"找不到老师文件: {teacher_file}")
else:
print("开始导入老师数据到表 his_lssj ...")
process_file(teacher_file, engine, "his_lssj", chunk_size=2000)
print("老师数据导入完成 -> his_lssj")
print("全部导入完成。")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="导入历史学生/老师数据或生成创建表SQL")
parser.add_argument("--dump-create-sql", action="store_true", help="只读取表头并生成 CREATE TABLE SQL 文件(不写入数据库)")
parser.add_argument("--student-file", type=str, default=None, help="学生文件路径,默认同目录下 student.xls")
parser.add_argument("--teacher-file", type=str, default=None, help="老师文件路径,默认同目录下 teacher.xlsx")
parser.add_argument("--yes", action="store_true", help="在导入前自动确认清空目标表(跳过交互式确认)")
args = parser.parse_args()
if args.dump_create_sql:
# generate CREATE TABLE SQL based on excel headers
def generate_create_table_sql(table_name: str, cols: List[str]) -> str:
# base fields required by project
lines: List[str] = []
lines.append(f"CREATE TABLE `{table_name}` (")
lines.append(" `id` BIGINT(20) NOT NULL AUTO_INCREMENT,")
lines.append(" `tenant_id` BIGINT(20) DEFAULT NULL,")
lines.append(" `deleted` TINYINT(1) NOT NULL DEFAULT 0,")
lines.append(" `create_by` BIGINT(20) DEFAULT NULL,")
lines.append(" `create_time` DATETIME DEFAULT NULL,")
lines.append(" `update_by` BIGINT(20) DEFAULT NULL,")
lines.append(" `update_time` DATETIME DEFAULT NULL,")
# add excel columns as TEXT
for col in cols:
lines.append(f" `{col}` TEXT DEFAULT NULL,")
lines.append(" PRIMARY KEY (`id`)")
lines.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;")
return "\n".join(lines)
base_dir = os.path.dirname(os.path.abspath(__file__))
student_path = args.student_file or os.path.join(base_dir, "student1222.xls")
teacher_path = args.teacher_file or os.path.join(base_dir, "teacher.xlsx")
for path, table in ((student_path, "his_xssj"), (teacher_path, "his_lssj")):
if not os.path.exists(path):
print(f"跳过,找不到文件: {path}")
continue
ext = os.path.splitext(path)[1].lower()
try:
# read header only
if ext == ".xls":
df = pd.read_excel(path, nrows=0, engine="xlrd")
else:
df = pd.read_excel(path, nrows=0, engine="openpyxl")
except Exception as e:
# fallback
print(f"读取表头失败 ({path}): {e}")
continue
raw_cols = list(df.columns)
safe_cols = sanitize_columns([str(c) for c in raw_cols])
sql = generate_create_table_sql(table, safe_cols)
out_file = os.path.join(base_dir, f"{table}_create.sql")
with open(out_file, "w", encoding="utf-8") as fh:
fh.write(sql)
print(f"已生成 {out_file},包含 {len(safe_cols)} 列(不含基础字段)")
else:
main(args)
@@ -0,0 +1,325 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
更新 sys_dept 表:
1) 打印 sys_dept 表结构供学习
2) 可删除 sys_dept 中除 dept_id 为 100/101/102 外的所有记录(需确认或使用 --yes)
3) 从 teacher.xlsx 中读取部门列,按规则插入到 sys_dept (部分学院放到 parent_id=101,其余放到 102)
用法:
python update_sys_dept.py # 交互式执行(会提示确认删除)
python update_sys_dept.py --yes # 自动确认删除
python update_sys_dept.py --teacher-file /path/to/teacher.xlsx
注意:
- 本脚本会尝试读取 `学校历史数据迁移/db_config.py` 中的 DB_CONFIG 连接数据库
- 插入时会尽量推断并填充常用字段(ancestors, parent_id, dept_name, order_num, status, del_flag, create_time)
"""
from __future__ import annotations
import os
import sys
import argparse
import datetime
from typing import List, Dict, Any, Optional
import pandas as pd
from sqlalchemy import create_engine, text
import importlib.util
def load_db_config_from_file(config_path: str) -> dict:
spec = importlib.util.spec_from_file_location("db_config", config_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # type: ignore
if not hasattr(module, "DB_CONFIG"):
raise RuntimeError(f"No DB_CONFIG found in {config_path}")
return getattr(module, "DB_CONFIG")
def build_engine_from_config(db_config: dict):
user = db_config.get("user")
password = db_config.get("password")
host = db_config.get("host", "localhost")
port = db_config.get("port", 3306)
database = db_config.get("database")
charset = db_config.get("charset", "utf8mb4")
uri = f"mysql+pymysql://{user}:{password}@{host}:{port}/{database}?charset={charset}"
engine = create_engine(uri, pool_pre_ping=True)
return engine
def inspect_table(engine, table_name: str) -> List[Dict[str, Any]]:
q = text(
"SELECT COLUMN_NAME, IS_NULLABLE, COLUMN_DEFAULT, DATA_TYPE "
"FROM information_schema.columns "
"WHERE table_schema = :schema AND table_name = :table ORDER BY ORDINAL_POSITION"
)
schema = engine.url.database
with engine.connect() as conn:
result = conn.execute(q, {"schema": schema, "table": table_name})
# Use mappings() so each row is a mapping of column_name->value
rows = result.mappings().all()
return [dict(r) for r in rows]
def find_dept_column(df: pd.DataFrame) -> Optional[str]:
candidates = []
for col in df.columns:
name = str(col).strip()
low = name.lower()
if any(k in low for k in ("部门", "学院", "系", "组织", "院", "department", "dept", "office", "unit")):
candidates.append(name)
if candidates:
return candidates[0]
# fallback: ask user by listing columns
return None
def parent_has_children(engine, parent_id: int) -> bool:
"""
检查 parent_id 下是否已有子部门记录(用于决定是否跳过该 parent 的批量插入)
"""
q = text("SELECT 1 FROM sys_dept WHERE parent_id = :pid LIMIT 1")
with engine.connect() as conn:
r = conn.execute(q, {"pid": parent_id}).fetchone()
return bool(r)
def read_teacher_departments_from_db(engine, table_name: str = "his_lssj") -> List[str]:
"""
从数据库表(默认 his_lssj)读取去重的部门名称(列名 '部门')。
返回去重、排序后的部门名称列表。
"""
print(f"从数据库表 `{table_name}` 读取部门列(列名假定为 '部门')")
q = text(f"SELECT DISTINCT `部门` AS dept_name FROM `{table_name}` WHERE `部门` IS NOT NULL AND TRIM(`部门`) != ''")
with engine.connect() as conn:
rows = conn.execute(q).mappings().all()
depts = sorted({str(r.get("dept_name")).strip() for r in rows if r.get("dept_name") is not None})
print(f"发现 {len(depts)} 个去重部门名(示例前20):")
for item in depts[:20]:
print(" -", item)
return depts
def get_parent_info(engine, parent_ids: List[int]) -> Dict[int, Dict[str, Any]]:
q = text("SELECT * FROM sys_dept WHERE dept_id IN :ids")
with engine.connect() as conn:
result = conn.execute(q, {"ids": tuple(parent_ids)})
rows = result.mappings().all()
out: Dict[int, Dict[str, Any]] = {}
for r in rows:
d = dict(r)
out[int(d.get("dept_id"))] = d
return out
def delete_except(engine, keep_ids: List[int], yes: bool):
if not yes:
resp = input(f"将删除 sys_dept 表中除 dept_id IN {keep_ids} 外的所有记录,确认继续?(yes/no): ").strip().lower()
if resp not in ("y", "yes"):
print("取消操作。")
return False
q = text("DELETE FROM sys_dept WHERE dept_id NOT IN :keep")
with engine.begin() as conn:
conn.execute(q, {"keep": tuple(keep_ids)})
print("删除完成。")
return True
def insert_departments(engine, dept_names: List[str], parent_mapping: Dict[str, int], parent_info: Dict[int, Dict[str, Any]]):
"""
插入时遵循以下规则以保持与现有数据一致性:
- dept_id: 若表中已有较大数值 id(如 2025xxxxxxxxxxxx),则以 max(dept_id)+1 继续;否则以当前时间 `%Y%m%d%H%M%S` + 4位序号生成
- tenant_id: 写为 '000000'
- order_num: 在同一 parent_id 下取 max(order_num)+1
- ancestors: 从 parent_info 推断,若父项无 ancestors 则使用 "0,{parent_id}"
- 其他字段填充 create_by/update_by/create_dept 等默认值以匹配样例
"""
inserted = 0
skipped = 0
now_dt = datetime.datetime.now()
now_str = now_dt.strftime("%Y-%m-%d %H:%M:%S")
timestamp_prefix = now_dt.strftime("%Y%m%d%H%M%S")
# helper to generate unique dept_id
def generate_dept_id(conn, local_counter_start=1):
# get current max dept_id
r = conn.execute(text("SELECT MAX(dept_id) as m FROM sys_dept")).fetchone()
max_id = int(r[0]) if r and r[0] is not None else 0
if max_id > 10**14:
return max_id + 1
# else use timestamp-based id plus 4-digit counter
counter = local_counter_start
while True:
candidate = int(f"{timestamp_prefix}{counter:04d}")
exists = conn.execute(text("SELECT 1 FROM sys_dept WHERE dept_id = :did LIMIT 1"), {"did": candidate}).fetchone()
if not exists:
return candidate
counter += 1
# Cache per-parent decision to avoid repeated checks: 'skip' or 'insert'
parent_decision: Dict[int, str] = {}
with engine.begin() as conn:
# fetch column info once
col_info = inspect_table(engine, "sys_dept")
col_names = [c["COLUMN_NAME"] for c in col_info]
# prepare xy counter for parent_id = 101 based on existing dept_category values
xy_counter = 1
try:
rows = conn.execute(text("SELECT dept_category FROM sys_dept WHERE parent_id = :pid AND dept_category LIKE 'xy-%'"), {"pid": 101}).mappings().all()
nums = []
for r in rows:
val = r.get("dept_category")
if not val:
continue
parts = str(val).split("-")
if len(parts) >= 2:
try:
nums.append(int(parts[-1]))
except Exception:
continue
if nums:
xy_counter = max(nums) + 1
except Exception:
# ignore and keep default
xy_counter = 1
for name in dept_names:
pname = name.strip()
if not pname:
continue
parent_id = parent_mapping.get(pname, None)
if parent_id is None:
parent_id = 102
# If we've decided to skip inserting for this parent, continue
if parent_id in parent_decision and parent_decision[parent_id] == "skip":
skipped += 1
continue
# First time encountering this parent: if it already has children, skip inserting any under it
if parent_id not in parent_decision:
try:
has_children = conn.execute(text("SELECT 1 FROM sys_dept WHERE parent_id = :pid LIMIT 1"), {"pid": parent_id}).fetchone()
if has_children:
parent_decision[parent_id] = "skip"
logger = None # no logger in this module-level function; just mark skip
skipped += 1
continue
else:
parent_decision[parent_id] = "insert"
except Exception:
parent_decision[parent_id] = "insert"
# skip if same dept_name and parent exists
exists_q = text("SELECT count(1) as c FROM sys_dept WHERE dept_name = :name AND parent_id = :pid")
res = conn.execute(exists_q, {"name": pname, "pid": parent_id}).fetchone()
if res and int(res[0]) > 0:
skipped += 1
continue
parent = parent_info.get(parent_id, {})
parent_ancestors = parent.get("ancestors") if parent else None
if parent_ancestors:
ancestors = f"{parent_ancestors},{parent_id}"
else:
# ensure root '0' present
if parent_id == 0:
ancestors = "0"
else:
ancestors = f"0,{parent_id}"
# compute next order_num under this parent
r = conn.execute(text("SELECT COALESCE(MAX(order_num),0) as m FROM sys_dept WHERE parent_id = :pid"), {"pid": parent_id}).fetchone()
next_order = int(r[0]) + 1 if r and r[0] is not None else 1
# generate unique dept_id
new_dept_id = generate_dept_id(conn)
# prepare insert dict with default fields matching provided samples
# set dept_category to 'xy-序号' for entries under parent_id == 101
dept_category_val = None
if parent_id == 101:
dept_category_val = f"xy-{xy_counter}"
xy_counter += 1
insert_dict: Dict[str, Any] = {
"dept_id": new_dept_id,
"tenant_id": "000000",
"parent_id": parent_id,
"ancestors": ancestors,
"dept_name": pname,
"dept_category": dept_category_val,
"order_num": next_order,
"leader": None,
"phone": None,
"email": None,
"status": "0",
"del_flag": "0",
"create_dept": 0,
"create_by": 1,
"create_time": now_str,
"update_by": 1,
"update_time": now_str,
}
# only keep keys that exist in the actual table
use_cols = {k: v for k, v in insert_dict.items() if k in col_names}
if "dept_name" not in use_cols:
raise RuntimeError("目标表不包含 dept_name 列,无法继续")
cols_part = ", ".join(f"`{k}`" for k in use_cols.keys())
vals_part = ", ".join(f":{k}" for k in use_cols.keys())
ins_q = text(f"INSERT INTO sys_dept ({cols_part}) VALUES ({vals_part})")
conn.execute(ins_q, use_cols)
inserted += 1
print(f"插入完成:新增 {inserted} 条,跳过已存在 {skipped} 条。")
def main():
base_dir = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(base_dir, "db_config.py")
if not os.path.exists(config_path):
print(f"找不到配置文件: {config_path}")
sys.exit(1)
db_config = load_db_config_from_file(config_path)
engine = build_engine_from_config(db_config)
# 1. 学习表结构
print("正在读取 sys_dept 表结构...")
cols = inspect_table(engine, "sys_dept")
for c in cols:
print(f" - {c['COLUMN_NAME']} type={c['DATA_TYPE']} nullable={c['IS_NULLABLE']} default={c['COLUMN_DEFAULT']}")
# 2. 删除除指定 dept_id 外的记录
parser = argparse.ArgumentParser()
parser.add_argument("--yes", action="store_true", help="自动确认删除")
parser.add_argument("--teacher-file", type=str, default=os.path.join(base_dir, "teacher.xlsx"))
args = parser.parse_args()
keep_ids = [100, 101, 102]
ok = delete_except(engine, keep_ids, args.yes)
if not ok:
print("用户取消,退出。")
return
# 3. 从 his_lssj 表导入部门(更快)
dept_names = read_teacher_departments_from_db(engine, table_name="his_lssj")
# mapping: specific colleges -> parent 101, others -> 102
top_level_101 = {
"人文学院", "餐饮休闲学院", "生态园林学院", "数字信息学院",
"医药健康学院", "智能控制学院", "智能制造学院"
}
# build mapping for exact names
parent_mapping = {name: 101 for name in top_level_101}
# get parent info
parent_info = get_parent_info(engine, [101, 102])
if 101 not in parent_info or 102 not in parent_info:
print("警告:未能在 sys_dept 中找到 parent_id 为 101 或 102 的记录,请确认这两个父组织是否存在。")
insert_departments(engine, dept_names, parent_mapping, parent_info)
if __name__ == "__main__":
main()
@@ -0,0 +1,292 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
03-插入老师数据到 sys_user 表
功能:
- 保留 user_id = 1 的用户,其他用户的 del_flag 都改成 '1'(虚拟删除,可交互确认)
- 从 teacher.xlsx 读取教师信息,插入到 sys_user
- user_name: 工号
- nick_name: 姓名
- phonenumber: 手机号(取前11位数字)
- sex: 男->'0', 女->'1'(其他为空)
- password: 使用固定哈希值
- account_type: '1'
- dept_id: 通过部门名称关联 sys_dept.dept_id(找不到会写 NULL 并打印警告)
- 跳过已存在相同 user_name 的记录(可选择覆盖)
用法:
python 03-insert_sys_user.py # 交互式确认虚拟删除
python 03-insert_sys_user.py --yes # 自动确认虚拟删除
python 03-insert_sys_user.py --teacher-file /path/to/teacher.xlsx
python 03-insert_sys_user.py --dry-run # 仅打印将执行的操作,不写库
"""
from __future__ import annotations
import os
import sys
import re
import argparse
import datetime
from typing import List, Dict, Any, Optional
import pandas as pd
from sqlalchemy import create_engine, text
import importlib.util
def load_db_config_from_file(config_path: str) -> dict:
spec = importlib.util.spec_from_file_location("db_config", config_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # type: ignore
if not hasattr(module, "DB_CONFIG"):
raise RuntimeError(f"No DB_CONFIG found in {config_path}")
return getattr(module, "DB_CONFIG")
def build_engine_from_config(db_config: dict):
user = db_config.get("user")
password = db_config.get("password")
host = db_config.get("host", "localhost")
port = db_config.get("port", 3306)
database = db_config.get("database")
charset = db_config.get("charset", "utf8mb4")
uri = f"mysql+pymysql://{user}:{password}@{host}:{port}/{database}?charset={charset}"
engine = create_engine(uri, pool_pre_ping=True)
return engine
def find_column(df: pd.DataFrame, keywords: List[str]) -> Optional[str]:
for col in df.columns:
name = str(col).strip()
low = name.lower()
for k in keywords:
if k in low:
return name
return None
def read_teacher_df_from_db(engine, table_name: str = "his_lssj") -> pd.DataFrame:
"""
从数据库表 his_lssj 读取教师数据,返回 pandas DataFrame(列名使用表中的列名)。
假定表中包含列:`工号`, `姓名`, `部门`, `性别`, `手机号码`
"""
print(f"从数据库表 `{table_name}` 读取教师数据(假定列:工号, 姓名, 部门, 性别, 手机号码)")
q = f"SELECT `工号`, `姓名`, `部门`, `性别`, `手机号码` FROM `{table_name}`"
df = pd.read_sql_query(q, con=engine)
return df
def normalize_phone(raw: Optional[str]) -> Optional[str]:
if raw is None:
return None
s = str(raw)
digits = "".join(re.findall(r"\d", s))
return digits[:11] if digits else None
def map_sex(raw: Optional[str]) -> Optional[str]:
if raw is None:
return None
s = str(raw).strip()
if s in ("男", "M", "m", "male", "Male"):
return "0"
if s in ("女", "F", "f", "female", "Female"):
return "1"
return None
def soft_delete_other_users(engine, yes: bool, dry_run: bool = False):
# Renamed behavior: perform physical DELETE except user_id = 1
if not yes:
resp = input("将删除 sys_user 中除 user_id=1 外的所有用户,确认?(yes/no) [no]: ").strip().lower()
if resp not in ("y", "yes"):
print("取消删除步骤。")
return
q_count = text("SELECT COUNT(1) as c FROM sys_user WHERE user_id != 1")
with engine.connect() as conn:
cnt = conn.execute(q_count).scalar()
print(f"将被删除的用户数:{cnt}")
if dry_run:
print("dry-run 模式,跳过删除。")
return
q = text("DELETE FROM sys_user WHERE user_id != 1")
with engine.begin() as conn:
conn.execute(q)
print("已删除其他用户(保留 user_id = 1)。")
def generate_user_id(conn):
r = conn.execute(text("SELECT MAX(user_id) as m FROM sys_user")).fetchone()
max_id = int(r[0]) if r and r[0] is not None else 0
return max_id + 1
def get_dept_id(conn, dept_name: str) -> Optional[int]:
if dept_name is None or str(dept_name).strip() == "":
return None
r = conn.execute(text("SELECT dept_id FROM sys_dept WHERE dept_name = :name LIMIT 1"), {"name": dept_name.strip()}).fetchone()
return int(r[0]) if r and r[0] is not None else None
def user_exists(conn, user_name: str, dept_id: Optional[int]) -> bool:
# skip only if both user_name and dept_id match an existing record
q = text(
"SELECT 1 FROM sys_user WHERE user_name = :u AND "
"((dept_id = :did) OR (dept_id IS NULL AND :did IS NULL)) LIMIT 1"
)
r = conn.execute(q, {"u": user_name, "did": dept_id}).fetchone()
return bool(r)
def dept_has_users(conn, dept_id: int) -> bool:
"""检查指定 dept_id 下是否已有用户存在(用于决定是否跳过该部门的批量插入)"""
if dept_id is None:
return False
r = conn.execute(text("SELECT 1 FROM sys_user WHERE dept_id = :did LIMIT 1"), {"did": dept_id}).fetchone()
return bool(r)
def insert_user(conn, data: Dict[str, Any]):
cols = ", ".join(f"`{k}`" for k in data.keys())
vals = ", ".join(f":{k}" for k in data.keys())
ins = text(f"INSERT INTO sys_user ({cols}) VALUES ({vals})")
conn.execute(ins, data)
def user_has_role(conn, user_id: int, role_id: int) -> bool:
r = conn.execute(text("SELECT 1 FROM sys_user_role WHERE user_id = :uid AND role_id = :rid LIMIT 1"), {"uid": user_id, "rid": role_id}).fetchone()
return bool(r)
def insert_user_role(conn, user_id: int, role_id: int):
ins = text("INSERT INTO sys_user_role (`user_id`, `role_id`) VALUES (:uid, :rid)")
conn.execute(ins, {"uid": user_id, "rid": role_id})
def main():
base_dir = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(base_dir, "db_config.py")
if not os.path.exists(config_path):
print(f"找不到配置文件: {config_path}")
sys.exit(1)
db_config = load_db_config_from_file(config_path)
engine = build_engine_from_config(db_config)
parser = argparse.ArgumentParser()
parser.add_argument("--yes", action="store_true", help="自动确认虚拟删除现有用户(除 user_id=1)")
parser.add_argument("--teacher-file", type=str, default=os.path.join(base_dir, "teacher.xlsx"))
parser.add_argument("--dry-run", action="store_true", help="只打印将执行的操作,不写入数据库")
args = parser.parse_args()
# 1) 虚拟删除其他用户
soft_delete_other_users(engine, args.yes, dry_run=args.dry_run)
# 2) 从 his_lssj 表读取教师数据(更快)
df = read_teacher_df_from_db(engine, table_name="his_lssj")
# 直接使用约定列名(表中创建时已用这些列名)
col_workno = "工号"
col_name = "姓名"
col_phone = "手机号码"
col_dept = "部门"
col_sex = "性别"
# ensure columns exist
for c in (col_workno, col_name):
if c not in df.columns:
print(f"必需列 {c} 在表 his_lssj 中未找到,请检查表结构或使用 Excel 源。")
return
password_hash = "$2a$10$FGldU5BGGq9ONuBmYhxLweFhmbkE45keMKASNZpjETXawE1oGbtZy"
now_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# iterate rows and insert
inserted = 0
skipped = 0
warnings = 0
# cache per-dept decision to avoid repeated checks: dept_id -> 'skip'|'insert'
dept_decision: Dict[Optional[int], str] = {}
with engine.begin() as conn:
for idx, row in df.iterrows():
user_name_val = str(row.get(col_workno)).strip() if row.get(col_workno) is not None else ""
if not user_name_val:
skipped += 1
continue
# determine department first so existence check can use dept_id
dept_raw = str(row.get(col_dept)).strip() if col_dept and row.get(col_dept) is not None else ""
dept_id = get_dept_id(conn, dept_raw) if dept_raw else None
if dept_raw and not dept_id:
print(f"警告: 第 {idx+1} 行,部门 '{dept_raw}' 在 sys_dept 中未找到,dept_id 将设为 NULL。")
warnings += 1
# If we've decided to skip this department because it already has users, skip adding users under it
if dept_id in dept_decision and dept_decision[dept_id] == "skip":
skipped += 1
continue
# First time encountering this dept: if dept already has users, mark skip
if dept_id not in dept_decision:
try:
if dept_has_users(conn, dept_id):
dept_decision[dept_id] = "skip"
skipped += 1
continue
else:
dept_decision[dept_id] = "insert"
except Exception:
dept_decision[dept_id] = "insert"
if user_exists(conn, user_name_val, dept_id):
skipped += 1
continue
nick = str(row.get(col_name)).strip() if row.get(col_name) is not None else ""
raw_phone = row.get(col_phone) if col_phone else None
phone = normalize_phone(raw_phone)
sex_val = map_sex(row.get(col_sex)) if col_sex else None
# generate user_id
new_user_id = generate_user_id(conn)
user_record = {
"user_id": new_user_id,
"tenant_id": "000000",
"dept_id": dept_id,
"user_name": user_name_val,
"nick_name": nick or user_name_val,
"user_type": "sys_user",
"email": None,
"phonenumber": phone,
"sex": sex_val or "0",
"avatar": None,
"password": password_hash,
"status": "0",
"del_flag": "0",
"login_ip": "",
"login_date": None,
"create_dept": 103,
"create_by": 1,
"create_time": now_str,
"update_by": 1,
"update_time": now_str,
"remark": None,
"account_type": "1",
}
if args.dry_run:
print("DRY:", user_record)
inserted += 1
continue
insert_user(conn, user_record)
# assign roles to the new user
role_ids = [1938120323613728770, 4]
for rid in role_ids:
if not user_has_role(conn, new_user_id, rid):
insert_user_role(conn, new_user_id, rid)
inserted += 1
print(f"完成:插入 {inserted} 条,跳过 {skipped} 条,警告 {warnings} 条。")
if __name__ == "__main__":
main()
@@ -0,0 +1,225 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
04-01-dedupe_sys_dept.py
目的:
在 sys_dept 表中,对于同一学院(parent_id)下的相同专业名称(dept_name)只保留一条记录,删除重复记录。
行为:
- 默认 dry-run:打印将要执行的 SQL 操作,不做修改。
- 传入 --apply 会执行修改(在事务中)。
- 对于重复记录,会保留 dept_id 最小的那条作为主记录(keep_id),
并将重复记录的子部门(parent_id 指向被删除 id) 统一改为 keep_id,
同时将 sys_user 表中引用的 dept_id 更新为 keep_id。
- 会打印处理摘要。
使用:
python3 04-01-dedupe_sys_dept.py --host ... --user ... --password ... --database ... [--apply]
注意:
在运行前请先备份数据库。对非数字或特殊 dept_name 不做额外过滤,脚本基于 parent_id + dept_name 进行分组。
"""
from __future__ import annotations
import argparse
import getpass
import logging
import sys
import os
import importlib.util
from typing import List, Tuple
import pymysql
from pymysql.connections import Connection
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger("dedupe_sys_dept")
def parse_args():
"""
Parse arguments. If a local db_config.py exists in the same directory and defines DB_CONFIG dict,
its values are used as defaults (same behavior as 04-insert_majors.py).
"""
# try load local db_config.py for defaults
local_db = load_local_db_config()
host_default = local_db.get("host", os.getenv("DB_HOST", "localhost"))
port_default = int(local_db.get("port", os.getenv("DB_PORT", 3306)))
user_default = local_db.get("user", os.getenv("DB_USER", "root"))
password_default = local_db.get("password", os.getenv("DB_PASSWORD", ""))
database_default = local_db.get("database", os.getenv("DB_NAME", "hzdyjs_xg"))
p = argparse.ArgumentParser(description="Deduplicate sys_dept entries by (parent_id, dept_name)")
p.add_argument("--host", default=host_default, help="DB host")
p.add_argument("--port", type=int, default=port_default, help="DB port")
p.add_argument("--user", default=user_default, help="DB user")
p.add_argument("--password", default=password_default, help="DB password (if omitted, prompted)")
p.add_argument("--database", default=database_default, help="DB name")
p.add_argument("--charset", default="utf8mb4", help="DB charset")
p.add_argument("--apply", action="store_true", help="Actually apply deletions and updates. Default is dry-run.")
p.add_argument("--limit", type=int, default=0, help="If >0, limit number of groups processed (for testing)")
return p.parse_args()
def load_local_db_config() -> dict:
"""
Attempt to load DB_CONFIG dict from db_config.py in the current script directory.
Returns empty dict if not present or on error.
"""
cfg_path = os.path.join(os.path.dirname(__file__), "db_config.py")
if not os.path.exists(cfg_path):
return {}
try:
spec = importlib.util.spec_from_file_location("local_db_config", cfg_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # type: ignore
return getattr(module, "DB_CONFIG", {}) or {}
except Exception as e:
logger.warning("加载本地 db_config.py 失败: %s", e)
return {}
def get_db_conn(args) -> Connection:
"""
Return a pymysql connection. Naming aligned with 04-insert_majors.py (`get_db_conn`).
"""
pwd = args.password or getpass.getpass("DB password: ")
return pymysql.connect(
host=args.host,
port=args.port,
user=args.user,
password=pwd,
database=args.database,
charset=args.charset,
cursorclass=pymysql.cursors.DictCursor,
autocommit=False,
)
def find_duplicate_groups(conn: Connection, limit: int = 0) -> List[Tuple[int, str, List[int]]]:
"""
返回列表,每项为 (parent_id, dept_name, [dept_id1, dept_id2, ...]),且 dept_id 列表长度 >=2
"""
sql = """
SELECT parent_id, dept_name, GROUP_CONCAT(dept_id ORDER BY dept_id ASC) AS ids, COUNT(*) AS cnt
FROM sys_dept
GROUP BY parent_id, dept_name
HAVING cnt > 1
ORDER BY parent_id, dept_name
"""
if limit and limit > 0:
sql = sql + f" LIMIT {limit}"
with conn.cursor() as cur:
cur.execute(sql)
rows = cur.fetchall()
groups = []
for r in rows:
ids = [int(x) for x in str(r["ids"]).split(",") if x.strip()]
groups.append((r["parent_id"], r["dept_name"], ids))
return groups
def reparent_children_and_users(conn: Connection, keep_id: int, delete_ids: List[int], dry_run: bool):
if not delete_ids:
return
# 1) Reparent child depts whose parent_id in delete_ids -> set parent_id = keep_id
sql_children = "SELECT dept_id FROM sys_dept WHERE parent_id IN ({ids})".format(
ids=",".join(str(i) for i in delete_ids)
)
with conn.cursor() as cur:
cur.execute(sql_children)
children = cur.fetchall()
if children:
children_ids = [r["dept_id"] for r in children]
logger.info(" Found %d child departments to reparent to %s: %s", len(children_ids), keep_id, children_ids)
if not dry_run:
sql_upd = "UPDATE sys_dept SET parent_id = %s WHERE parent_id IN ({ids})".format(
ids=",".join(str(i) for i in delete_ids)
)
with conn.cursor() as cur:
cur.execute(sql_upd, (keep_id,))
else:
logger.debug(" No child departments referencing deleted ids.")
# 2) Update sys_user.dept_id referencing deleted ids -> keep_id
sql_users = "SELECT count(*) as cnt FROM sys_user WHERE dept_id IN ({ids})".format(
ids=",".join(str(i) for i in delete_ids)
)
with conn.cursor() as cur:
cur.execute(sql_users)
cnt_row = cur.fetchone()
user_cnt = int(cnt_row["cnt"])
if user_cnt > 0:
logger.info(" Found %d users referencing deleted dept ids -> will update to %s", user_cnt, keep_id)
if not dry_run:
sql_upd_users = "UPDATE sys_user SET dept_id = %s WHERE dept_id IN ({ids})".format(
ids=",".join(str(i) for i in delete_ids)
)
with conn.cursor() as cur:
cur.execute(sql_upd_users, (keep_id,))
def delete_dept_ids(conn: Connection, delete_ids: List[int], dry_run: bool):
if not delete_ids:
return
logger.info(" Will delete dept ids: %s", delete_ids)
if dry_run:
return
sql = "DELETE FROM sys_dept WHERE dept_id IN ({ids})".format(ids=",".join(str(i) for i in delete_ids))
with conn.cursor() as cur:
cur.execute(sql)
def process_groups(conn: Connection, groups, apply: bool):
dry_run = not apply
summary = {"groups": 0, "deleted_ids_total": 0}
for parent_id, dept_name, ids in groups:
keep_id = min(ids)
delete_ids = [i for i in ids if i != keep_id]
summary["groups"] += 1
summary["deleted_ids_total"] += len(delete_ids)
logger.info("Group parent_id=%s dept_name=%r => keep=%s delete=%s", parent_id, dept_name, keep_id, delete_ids)
# Reparent children and users first
reparent_children_and_users(conn, keep_id, delete_ids, dry_run=dry_run)
# Then delete
delete_dept_ids(conn, delete_ids, dry_run=dry_run)
return summary
def main():
args = parse_args()
conn = get_db_conn(args)
try:
groups = find_duplicate_groups(conn, limit=args.limit)
if not groups:
logger.info("No duplicate groups found.")
return 0
logger.info("Found %d duplicate groups (parent_id, dept_name with count>1).", len(groups))
for idx, (parent_id, dept_name, ids) in enumerate(groups, start=1):
logger.info(" %d) parent_id=%s dept_name=%r ids=%s", idx, parent_id, dept_name, ids)
if args.apply:
logger.info("Applying changes inside a transaction...")
try:
summary = process_groups(conn, groups, apply=True)
conn.commit()
logger.info("Committed. Processed %d groups, total deleted ids %d", summary["groups"], summary["deleted_ids_total"])
except Exception as e:
conn.rollback()
logger.exception("Error during apply, rolled back. Error: %s", e)
return 2
else:
logger.info("Dry-run mode (no changes will be made). Showing what would be executed:")
_ = process_groups(conn, groups, apply=False)
logger.info("Dry-run finished. Re-run with --apply to execute.")
return 0
finally:
conn.close()
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,211 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
04-02-reorder_dept_order_num.py
目的:
将 `sys_dept` 表中,同一个学院(parent_id)下的专业记录(可通过 dept_category 判断)按照 `dept_name` 升序重新设置 `order_num`(从 1 开始递增)。
行为:
- 默认 dry-run:打印将要执行的 UPDATE 语句,不做修改。
- 传入 --apply 会在事务中执行更新。
- 支持通过 --major-only 控制是否仅对 dept_category LIKE 'zy-%' 的记录进行排序(默认 True)。
- 支持 --limit 控制处理的 parent_id 数量(用于测试)。
使用:
python3 04-02-reorder_dept_order_num.py --host ... --user ... --database ... [--apply]
"""
from __future__ import annotations
import argparse
import getpass
import importlib.util
import os
import sys
import logging
from typing import List, Tuple, Dict
import pymysql
from pymysql.connections import Connection
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger("reorder_dept_order_num")
def load_local_db_config() -> dict:
cfg_path = os.path.join(os.path.dirname(__file__), "db_config.py")
if not os.path.exists(cfg_path):
return {}
try:
spec = importlib.util.spec_from_file_location("local_db_config", cfg_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # type: ignore
return getattr(module, "DB_CONFIG", {}) or {}
except Exception as e:
logger.warning("加载本地 db_config.py 失败: %s", e)
return {}
def parse_args():
local_db = load_local_db_config()
host_default = local_db.get("host", os.getenv("DB_HOST", "localhost"))
port_default = int(local_db.get("port", os.getenv("DB_PORT", 3306)))
user_default = local_db.get("user", os.getenv("DB_USER", "root"))
password_default = local_db.get("password", os.getenv("DB_PASSWORD", ""))
database_default = local_db.get("database", os.getenv("DB_NAME", "hzdyjs_xg"))
p = argparse.ArgumentParser(description="Reorder sys_dept.order_num by dept_name under the same parent_id")
p.add_argument("--host", default=host_default)
p.add_argument("--port", type=int, default=port_default)
p.add_argument("--user", default=user_default)
p.add_argument("--password", default=password_default)
p.add_argument("--database", default=database_default)
p.add_argument("--charset", default="utf8mb4")
p.add_argument("--apply", action="store_true", help="Apply changes (default dry-run)")
p.add_argument("--limit", type=int, default=0, help="Limit number of parent groups to process (for testing)")
p.add_argument("--major-only", action="store_true", default=True, help="Only reorder records where dept_category LIKE 'zy-%' (default True)")
return p.parse_args()
def get_db_conn(args) -> Connection:
pwd = args.password or getpass.getpass("DB password: ")
return pymysql.connect(
host=args.host,
port=args.port,
user=args.user,
password=pwd,
database=args.database,
charset=args.charset,
cursorclass=pymysql.cursors.DictCursor,
autocommit=False,
)
def find_parent_ids(conn: Connection, major_only: bool = True, limit: int = 0) -> List[int]:
"""
Find parent_id values that have child departments to reorder.
If major_only is True, only consider children with dept_category LIKE 'zy-%'.
"""
if major_only:
sql = """
SELECT parent_id
FROM sys_dept
WHERE dept_category LIKE 'zy-%%' AND del_flag = '0'
GROUP BY parent_id
ORDER BY parent_id
"""
else:
sql = """
SELECT parent_id
FROM sys_dept
WHERE parent_id IS NOT NULL AND del_flag = '0'
GROUP BY parent_id
ORDER BY parent_id
"""
if limit and limit > 0:
sql = sql + f" LIMIT {limit}"
with conn.cursor() as cur:
cur.execute(sql)
rows = cur.fetchall()
return [r["parent_id"] for r in rows if r["parent_id"] is not None]
def fetch_children(conn: Connection, parent_id: int, major_only: bool = True) -> List[Dict]:
"""
Fetch child departments for a parent_id, ordered by dept_name asc then dept_id asc.
"""
if major_only:
sql = """
SELECT dept_id, dept_name, order_num
FROM sys_dept
WHERE parent_id = %s AND dept_category LIKE 'zy-%%' AND del_flag = '0'
ORDER BY dept_name ASC, dept_id ASC
"""
else:
sql = """
SELECT dept_id, dept_name, order_num
FROM sys_dept
WHERE parent_id = %s AND del_flag = '0'
ORDER BY dept_name ASC, dept_id ASC
"""
with conn.cursor() as cur:
cur.execute(sql, (parent_id,))
return cur.fetchall()
def apply_updates(conn: Connection, updates: List[Tuple[int, int]], dry_run: bool):
"""
updates: list of (dept_id, new_order_num)
"""
if not updates:
return
logger.info(" %d updates to apply", len(updates))
for dept_id, new_order in updates:
sql = "UPDATE sys_dept SET order_num = %s WHERE dept_id = %s"
logger.info(" %s -- params: (%s, %s)", sql, new_order, dept_id)
if not dry_run:
with conn.cursor() as cur:
cur.execute(sql, (new_order, dept_id))
def process(conn: Connection, parent_ids: List[int], major_only: bool, apply: bool):
dry_run = not apply
summary = {"parents": 0, "updates": 0}
for parent_id in parent_ids:
children = fetch_children(conn, parent_id, major_only=major_only)
if not children:
logger.debug("Parent %s has no children to reorder", parent_id)
continue
updates = []
for idx, child in enumerate(children, start=1):
dept_id = child["dept_id"]
current_order = child.get("order_num") or 0
if current_order != idx:
updates.append((dept_id, idx))
if updates:
logger.info("Parent %s: will update %d rows", parent_id, len(updates))
apply_updates(conn, updates, dry_run=dry_run)
summary["updates"] += len(updates)
else:
logger.info("Parent %s: no updates needed", parent_id)
summary["parents"] += 1
return summary
def main():
args = parse_args()
conn = get_db_conn(args)
try:
parent_ids = find_parent_ids(conn, major_only=args.major_only, limit=args.limit)
if not parent_ids:
logger.info("No parent groups found to process.")
return 0
logger.info("Found %d parent groups to process (limit=%s, major_only=%s).", len(parent_ids), args.limit, args.major_only)
# show parents
for pid in parent_ids:
logger.info(" parent_id=%s", pid)
if args.apply:
logger.info("Applying updates inside a transaction...")
try:
summary = process(conn, parent_ids, args.major_only, apply=True)
conn.commit()
logger.info("Committed. Processed %d parents, total updates %d", summary["parents"], summary["updates"])
except Exception as e:
conn.rollback()
logger.exception("Error during apply, rolled back. Error: %s", e)
return 2
else:
logger.info("Dry-run mode: showing planned updates (no DB changes).")
summary = process(conn, parent_ids, args.major_only, apply=False)
logger.info("Dry-run finished. Processed %d parents, planned updates %d. Re-run with --apply to execute.", summary["parents"], summary["updates"])
return 0
finally:
conn.close()
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,294 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
04 - 专业信息入库脚本
说明:
- 从 `his_xssj` 表读取学院编号、专业编号、专业名称(去重)
- 根据学院编号映射到学院名称,然后在 `sys_dept`(或目标表)中查找父节点 parent_id
- 若专业在目标表中不存在,则生成 INSERT 语句(默认 dry-run,仅打印)
- 可使用 --execute 开关执行插入
用法示例:
python3 "学校历史数据迁移/04-insert_majors.py" --host localhost --user root --password secret --database hzdyjs_xg
python3 "学校历史数据迁移/04-insert_majors.py" --execute --host ... (实际写入)
注意:
- 请提前安装依赖: pip install pymysql
"""
from __future__ import annotations
import argparse
import datetime
import logging
import os
import sys
import importlib.util
from typing import Dict, List, Optional, Tuple
import pymysql
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger("insert_majors")
COLLEGE_MAP: Dict[str, str] = {
"14": "智能控制学院",
"15": "智能制造学院",
"19": "人文学院",
"18": "餐饮休闲学院",
"28": "数字信息学院",
"16": "医药健康学院",
"17": "生态园林学院",
}
def load_local_db_config() -> Dict:
"""
尝试从当前脚本目录的 db_config.py 加载 DB_CONFIG 字典并返回(失败则返回空字典)
"""
cfg_path = os.path.join(os.path.dirname(__file__), "db_config.py")
if not os.path.exists(cfg_path):
return {}
try:
spec = importlib.util.spec_from_file_location("local_db_config", cfg_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # type: ignore
return getattr(module, "DB_CONFIG", {}) or {}
except Exception as e:
logger.warning("加载本地 db_config.py 失败: %s", e)
return {}
def get_db_conn(host: str, port: int, user: str, password: str, database: str):
"""建立并返回 pymysql 连接(默认 autocommit False)"""
return pymysql.connect(
host=host,
port=port,
user=user,
password=password,
database=database,
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor,
autocommit=False,
)
def college_has_majors(conn, parent_id: int, target_table: str) -> bool:
"""
检查指定 parent_id(学院)下是否已有专业类记录(dept_category 以 'zy-' 开头或任意子节点)
若存在任一子记录则返回 True。
"""
# note: percent signs must be escaped (%%) because PyMySQL uses Python %-formatting during mogrify
# Only consider existing records that look like majors (dept_category starting with 'zy-').
# Previously the condition included "OR dept_category IS NOT NULL" which caused false positives
# and made the script skip entire colleges when any child had a non-null dept_category.
sql = f"SELECT 1 FROM {target_table} WHERE parent_id = %s AND dept_category LIKE 'zy-%%' LIMIT 1"
with conn.cursor() as cur:
cur.execute(sql, (parent_id,))
return cur.fetchone() is not None
def fetch_distinct_majors(conn) -> List[Tuple[str, str, str]]:
"""
从 his_xssj 表读取 distinct (college_code, major_code, major_name)
假设表名为 his_xssj,字段名可调整如 xybm(学院编码)、zybm(专业编码)、zymc(专业名称)
"""
# use Chinese column names present in his_xssj table
sql = """
SELECT DISTINCT
COALESCE(`学院编号`, '') AS college_code,
COALESCE(`专业编号`, '') AS major_code,
COALESCE(`专业名称`, '') AS major_name
FROM his_xssj
WHERE `专业编号` IS NOT NULL AND `专业编号` <> '' AND `专业名称` IS NOT NULL AND `专业名称` <> ''
ORDER BY college_code, major_code;
"""
with conn.cursor() as cur:
cur.execute(sql)
rows = cur.fetchall()
return [(r["college_code"], r["major_code"], r["major_name"]) for r in rows]
def find_parent_dept(conn, college_name: str) -> Optional[Dict]:
"""
根据学院名称查找 sys_dept 中的父节点(按 dept_name 精确匹配)
返回整行记录(字典)或 None
"""
sql = "SELECT * FROM sys_dept WHERE dept_name = %s LIMIT 1"
with conn.cursor() as cur:
cur.execute(sql, (college_name,))
return cur.fetchone()
def exists_major(conn, parent_id: int, major_code: str, major_name: str, target_table: str) -> bool:
"""
判断目标表中是否已存在该专业(通过 dept_category 或 dept_name 与 parent_id 共同判断)
"""
# Consider a major existing only if BOTH dept_category and dept_name match under the same parent_id
sql = f"SELECT 1 FROM {target_table} WHERE parent_id = %s AND dept_category = %s AND dept_name = %s LIMIT 1"
with conn.cursor() as cur:
cur.execute(sql, (parent_id, f"zy-{major_code}", major_name))
return cur.fetchone() is not None
def gen_dept_id(counter: int) -> int:
"""
生成一个大整数 dept_id:
使用当前时间 (YYYYMMDDHHMMSS) + 3-digit counter 保证唯一性(可根据需要替换为雪花ID)
"""
ts = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
return int(f"{ts}{counter:03d}")
def insert_major(conn, target_table: str, parent_row: Dict, major_code: str, major_name: str, dept_id: int):
"""
插入一条 sys_dept 记录到 target_table
- parent_row: 父节点整行(包含 dept_id, ancestors)
"""
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
parent_id = parent_row["dept_id"]
parent_ancestors = parent_row.get("ancestors") or ""
if parent_ancestors:
ancestors = f"{parent_ancestors},{parent_id}"
else:
ancestors = str(parent_id)
dept_category = f"zy-{major_code}"
sql = f"""
INSERT INTO {target_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)
VALUES
(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
params = (
dept_id,
"000000", # tenant_id
parent_id,
ancestors,
major_name,
dept_category,
1, # order_num
None, # leader
None, # phone
None, # email
"0", # status
"0", # del_flag
0, # create_dept
1, # create_by
now,
1, # update_by
now,
)
with conn.cursor() as cur:
cur.execute(sql, params)
def main(argv: Optional[List[str]] = None):
# 尝试从同目录下的 db_config.py 读取默认数据库配置
local_db = load_local_db_config()
host_default = local_db.get("host", os.getenv("DB_HOST", "localhost"))
port_default = int(local_db.get("port", os.getenv("DB_PORT", 3306)))
user_default = local_db.get("user", os.getenv("DB_USER", "root"))
password_default = local_db.get("password", os.getenv("DB_PASSWORD", ""))
database_default = local_db.get("database", os.getenv("DB_NAME", "hzdyjs_xg"))
target_table_default = os.getenv("TARGET_TABLE", "sys_dept")
parser = argparse.ArgumentParser(description="从 his_xssj 导入专业到 sys_dept")
parser.add_argument("--host", default=host_default)
parser.add_argument("--port", type=int, default=port_default)
parser.add_argument("--user", default=user_default)
parser.add_argument("--password", default=password_default)
parser.add_argument("--database", default=database_default)
parser.add_argument("--target-table", default=target_table_default)
parser.add_argument("--dry-run", action="store_true", default=False, help="仅打印将要执行的 SQL(默认执行写库)")
parser.add_argument("--execute", action="store_true", default=True, help="执行插入(会在事务中执行并提交,默认开启)")
parser.add_argument("--limit", type=int, default=0, help="仅处理前 N 条(用于测试)")
args = parser.parse_args(argv)
conn = get_db_conn(args.host, args.port, args.user, args.password, args.database)
try:
majors = fetch_distinct_majors(conn)
logger.info("从 his_xssj 获取到 %d 条去重专业记录", len(majors))
counter = 1
inserted = 0
skipped = 0
sql_statements: List[str] = []
# 记录对每个学院的整体决定('skip' or 'insert'),避免对同一学院重复查询
college_decision: Dict[str, str] = {}
for idx, (college_code, major_code, major_name) in enumerate(majors, start=1):
if args.limit and inserted >= args.limit:
break
college_name = COLLEGE_MAP.get(college_code)
if not college_name:
logger.warning("未在 COLLEGE_MAP 中找到学院编码 %s,跳过: %s", college_code, major_name)
skipped += 1
continue
parent_row = find_parent_dept(conn, college_name)
if not parent_row:
logger.warning("未在 sys_dept 中找到学院父节点(dept_name=%s),请先确认父节点存在,跳过: %s", college_name, major_name)
skipped += 1
continue
# 判重:在同一父节点下,若 dept_category 和 dept_name 同时匹配则视为已存在并跳过
if exists_major(conn, parent_row["dept_id"], major_code, major_name, args.target_table):
logger.info("专业已存在,跳过: %s (%s) under %s", major_name, major_code, college_name)
skipped += 1
continue
dept_id = gen_dept_id(counter)
counter += 1
# Prepare SQL string for logging / dry-run
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
parent_id = parent_row["dept_id"]
parent_ancestors = parent_row.get("ancestors") or ""
ancestors = f"{parent_ancestors},{parent_id}" if parent_ancestors else str(parent_id)
dept_category = f"zy-{major_code}"
# escape single quotes in major_name for SQL literal
safe_major_name = str(major_name).replace("'", "''")
# ancestors is numeric-comma separated; safe to include directly
insert_sql = (
"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) "
"VALUES ({did}, '000000', {pid}, '{ancs}', '{dname}', '{dcat}', 1, NULL, NULL, NULL, '0', '0', 0, 1, '{now}', 1, '{now}');"
).format(
table=args.target_table,
did=dept_id,
pid=parent_id,
ancs=ancestors,
dname=safe_major_name,
dcat=dept_category,
now=now,
)
sql_statements.append(insert_sql)
logger.info("准备插入专业: %s (%s) parent=%s new_dept_id=%s", major_name, major_code, college_name, dept_id)
if args.execute:
insert_major(conn, args.target_table, parent_row, major_code, major_name, dept_id)
conn.commit()
inserted += 1
else:
skipped += 1
# 输出 dry-run 的 SQL
if sql_statements:
logger.info("共准备 %d 条 INSERT 语句 (dry-run=%s, execute=%s)", len(sql_statements), args.dry_run, args.execute)
for s in sql_statements:
print(s)
else:
logger.info("没有需要插入的专业记录")
logger.info("完成: 插入 %d 条, 跳过 %d 条", inserted, skipped)
finally:
conn.close()
if __name__ == "__main__":
main()
@@ -0,0 +1,201 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
05 - 按 dept_category 为同一学院下的专业排序并更新 order_num
说明:
- 在目标表(默认 `sys_dept`)中查找所有 dept_category 以 "zy-" 开头的专业记录
- 按照 `parent_id`(学院)分组,对于每个组按照 `dept_category` 排序
- 为排序后的记录按序设置 `order_num`(从 1 开始递增)
- 支持 dry-run(仅打印 SQL)和 --execute(真正更新数据库)
用法示例:
python3 "学校历史数据迁移/05-sort_majors_by_dept_category.py" --host localhost --user root --password secret --database hzdyjs_xg --dry-run
python3 "学校历史数据迁移/05-sort_majors_by_dept_category.py" --execute --host ...
依赖:
pip install pymysql
"""
from __future__ import annotations
import argparse
import importlib.util
import os
import logging
from typing import Dict, List, Optional
import pymysql
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger("sort_majors_order_num")
def load_local_db_config() -> Dict:
"""
尝试从当前脚本目录的 db_config.py 加载 DB_CONFIG 字典并返回(失败则返回空字典)
"""
cfg_path = os.path.join(os.path.dirname(__file__), "db_config.py")
if not os.path.exists(cfg_path):
return {}
try:
spec = importlib.util.spec_from_file_location("local_db_config", cfg_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # type: ignore
return getattr(module, "DB_CONFIG", {}) or {}
except Exception as e:
logger.warning("加载本地 db_config.py 失败: %s", e)
return {}
def get_db_conn(host: str, port: int, user: str, password: str, database: str):
"""建立并返回 pymysql 连接(默认 autocommit False)"""
return pymysql.connect(
host=host,
port=port,
user=user,
password=password,
database=database,
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor,
autocommit=False,
)
def fetch_parent_ids_with_majors(conn, target_table: str) -> List[int]:
"""
获取在目标表中存在以 'zy-' 开头 dept_category 的 parent_id 列表
"""
# escape percent for PyMySQL mogrify/formatting
sql = f"SELECT DISTINCT parent_id FROM {target_table} WHERE dept_category LIKE 'zy-%%' AND parent_id IS NOT NULL"
with conn.cursor() as cur:
cur.execute(sql)
rows = cur.fetchall()
return [int(r["parent_id"]) for r in rows]
def fetch_majors_by_parent(conn, target_table: str, parent_id: int) -> List[Dict]:
"""
获取指定 parent_id 下的专业记录,按 dept_category 排序(字典列表)
"""
# escape percent for PyMySQL mogrify/formatting
sql = f"SELECT dept_id, dept_name, dept_category, order_num FROM {target_table} WHERE parent_id = %s AND dept_category LIKE 'zy-%%' ORDER BY dept_category ASC, dept_id ASC"
with conn.cursor() as cur:
cur.execute(sql, (parent_id,))
rows = cur.fetchall()
return rows
def generate_update_statements(parent_id: int, majors: List[Dict]) -> List[str]:
"""
根据传入的 majors 列表生成 UPDATE 语句(字符串),从 1 开始递增 order_num
"""
statements: List[str] = []
for idx, row in enumerate(majors, start=1):
dept_id = row["dept_id"]
stmt = f"UPDATE {{table}} SET order_num = {idx} WHERE dept_id = {dept_id} AND parent_id = {parent_id};"
statements.append(stmt)
return statements
def apply_updates(conn, target_table: str, updates: List[Dict], execute: bool):
"""
执行更新操作。updates 为包含 (dept_id, new_order_num) 的字典列表
如果 execute=False,只打印 SQL(dry-run)。
"""
if not updates:
return
with conn.cursor() as cur:
for u in updates:
sql = f"UPDATE {target_table} SET order_num = %s WHERE dept_id = %s"
params = (u["order_num"], u["dept_id"])
if execute:
cur.execute(sql, params)
else:
# 打印替换后的 SQL,便于 dry-run 审核
print(cur.mogrify(sql, params).decode() if hasattr(cur, "mogrify") else sql)
if execute:
conn.commit()
def main(argv: Optional[List[str]] = None):
local_db = load_local_db_config()
host_default = local_db.get("host", os.getenv("DB_HOST", "localhost"))
port_default = int(local_db.get("port", os.getenv("DB_PORT", 3306)))
user_default = local_db.get("user", os.getenv("DB_USER", "root"))
password_default = local_db.get("password", os.getenv("DB_PASSWORD", ""))
database_default = local_db.get("database", os.getenv("DB_NAME", "hzdyjs_xg"))
target_table_default = os.getenv("TARGET_TABLE", "sys_dept")
parser = argparse.ArgumentParser(description="按 dept_category 对同一学院的专业排序并更新 order_num")
parser.add_argument("--host", default=host_default)
parser.add_argument("--port", type=int, default=port_default)
parser.add_argument("--user", default=user_default)
parser.add_argument("--password", default=password_default)
parser.add_argument("--database", default=database_default)
parser.add_argument("--target-table", default=target_table_default)
parser.add_argument("--dry-run", action="store_true", default=False, help="仅打印将要执行的 SQL(默认不开启)")
parser.add_argument("--execute", action="store_true", default=False, help="真正执行更新(需谨慎开启)")
parser.add_argument("--parent-id", type=int, default=0, help="可选:仅处理指定 parent_id(学院)")
args = parser.parse_args(argv)
conn = get_db_conn(args.host, args.port, args.user, args.password, args.database)
try:
target_table = args.target_table
execute_mode = args.execute
dry_run = args.dry_run and not args.execute
parent_ids: List[int]
if args.parent_id:
parent_ids = [args.parent_id]
else:
parent_ids = fetch_parent_ids_with_majors(conn, target_table)
logger.info("将处理 %d 个学院(parent_id)", len(parent_ids))
total_updates = 0
for parent_id in parent_ids:
majors = fetch_majors_by_parent(conn, target_table, parent_id)
if not majors:
continue
# 生成更新计划
updates = []
for idx, row in enumerate(majors, start=1):
# 仅当现有 order_num 与目标不同才更新
current_order = row.get("order_num") or 0
if current_order != idx:
updates.append({"dept_id": row["dept_id"], "order_num": idx, "dept_category": row.get("dept_category"), "dept_name": row.get("dept_name")})
if not updates:
logger.info("parent_id=%s: 无需更新(已按 dept_category 排序)", parent_id)
continue
# 打印变更预览
logger.info("parent_id=%s: 将更新 %d 条记录(按 dept_category 排序)", parent_id, len(updates))
for u in updates:
print(f"-- parent_id={parent_id} dept_id={u['dept_id']} dept_category={u.get('dept_category')} dept_name={u.get('dept_name')} => order_num={u['order_num']}")
# 执行或打印 SQL
if execute_mode:
apply_updates(conn, target_table, updates, execute=True)
total_updates += len(updates)
logger.info("parent_id=%s: 已提交 %d 条更新", parent_id, len(updates))
else:
# dry-run: 打印 SQL 语句
for u in updates:
sql = f"UPDATE {target_table} SET order_num = %s WHERE dept_id = %s;"
params = (u["order_num"], u["dept_id"])
# Using simple safe formatting for printing
try:
with conn.cursor() as cur:
print(cur.mogrify(sql, params).decode() if hasattr(cur, "mogrify") else sql % params)
except Exception:
# Fallback
print(sql.replace("%s", "{}").format(*params))
logger.info("完成: 总计更新 %d 条记录(execute=%s, dry-run=%s)", total_updates, execute_mode, dry_run)
finally:
conn.close()
if __name__ == "__main__":
main()
@@ -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()
@@ -0,0 +1,119 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
07 - 按年级重排序 sys_dept 中专业下的班级 (修改 order_num)
说明:
- 针对 sys_dept 中 dept_category 以 'zy-' 开头的专业,查找 parent_id 为该专业的子部门(dept_category LIKE 'bj-%')
- 使用 xg_class_msg.enrollment_year 或 sys_dept.dept_name 进行排序(优先 enrollment_year,空值视为 0),然后按 dept_name
- 为每个父专业从 1 开始重新分配 order_num(可修改为起始值为当前最大值 + 1)
- 脚本执行前会交互确认(避免误删/误改);支持 DRY_RUN 模式,仅打印将要更新的改动
使用方法:
- 运行前确认 `db_config.DB_CONFIG` 正确,执行: python 07-reorder_classes_by_grade.py
"""
from __future__ import annotations
from typing import List, Dict, Any, Optional, Tuple
from datetime import datetime
import pymysql
from db_config import DB_CONFIG
DRY_RUN = False
def get_connection():
return pymysql.connect(cursorclass=pymysql.cursors.DictCursor, **DB_CONFIG)
def fetch_all_majors(conn) -> List[Dict[str, Any]]:
sql = "SELECT dept_id, dept_name FROM sys_dept WHERE dept_category LIKE %s ORDER BY dept_name"
with conn.cursor() as c:
c.execute(sql, ("zy-%",))
return c.fetchall()
def fetch_classes_under_major(conn, parent_id: int) -> List[Dict[str, Any]]:
"""
返回列表包含 sys_dept.dept_id, dept_name, xg_class_msg.enrollment_year
"""
sql = """
SELECT sd.dept_id, sd.dept_name, xc.enrollment_year
FROM sys_dept sd
LEFT JOIN xg_class_msg xc ON sd.dept_id = xc.id
WHERE sd.parent_id = %s AND sd.dept_category LIKE %s
"""
with conn.cursor() as c:
c.execute(sql, (parent_id, "bj-%"))
return c.fetchall()
def normalize_year(val: Any) -> int:
if val is None or val == "":
return 0
try:
return int(val)
except Exception:
return 0
def update_order_nums(conn, updates: List[Tuple[int, int]]):
"""
updates: list of (order_num, dept_id)
"""
with conn.cursor() as c:
for order_num, dept_id in updates:
if DRY_RUN:
print(f"[DRY_RUN] UPDATE sys_dept SET order_num={order_num} WHERE dept_id={dept_id}")
else:
c.execute("UPDATE sys_dept SET order_num = %s WHERE dept_id = %s", (order_num, dept_id))
if not DRY_RUN:
conn.commit()
def prompt_confirm() -> bool:
try:
ans = input("确认按年级重排序 sys_dept 中专业下的班级并修改 order_num 吗?[y/N]: ").strip().lower()
except (EOFError, KeyboardInterrupt):
print("\n未检测到交互输入,取消操作。")
return False
return ans in ("y", "yes")
def main():
print("开始:07 - 按年级重排序 sys_dept 专业下的班级(修改 order_num)")
if not prompt_confirm():
print("已取消操作。")
return
conn = get_connection()
try:
majors = fetch_all_majors(conn)
print(f"发现 {len(majors)} 个专业,将逐个处理...")
total_changed = 0
for m in majors:
parent_id = m.get("dept_id")
major_name = m.get("dept_name")
classes = fetch_classes_under_major(conn, parent_id)
if not classes:
continue
# sort by enrollment_year(int, 0) asc, then dept_name asc
classes.sort(key=lambda r: (normalize_year(r.get("enrollment_year")), r.get("dept_name") or ""))
updates: List[Tuple[int, int]] = []
for idx, row in enumerate(classes, start=1):
updates.append((idx, row.get("dept_id")))
if updates:
print(f"专业 {major_name}({parent_id}) 将更新 {len(updates)} 条 order_num。")
update_order_nums(conn, updates)
total_changed += len(updates)
print(f"完成:共更新 order_num {total_changed} 条记录。")
except Exception as e:
print("发生异常:", e)
finally:
conn.close()
if __name__ == "__main__":
main()
@@ -0,0 +1,549 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
从 his_xssj 表把历史学生数据同步到 xg 系统表
主要写入表:
- xg_student_msg
- sys_user
- sys_user_role
特性:
- 同步前提示是否清空 xg_student_msg(需用户确认)
- 跳过 班级编号 为 '#YDQX#' 或 空字符串 的记录
- 批量插入并分批提交,包含简单的重试机制
- 通过 sys_dept 表查找班级/专业/学院信息;通过 xg_class_msg 获取班级关联字段
- 字段映射/清洗遵循项目要求(见任务描述)
注意:此脚本依赖同目录下的 `db_config.py` 提供 `DB_CONFIG` 与 `TABLE_MAPPING`
"""
from __future__ import annotations
import argparse
import logging
import sys
import time
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
import pymysql
from pymysql.cursors import DictCursor
from db_config import DB_CONFIG, TABLE_MAPPING
# 常量配置(可根据需要改为从外部配置)
STUDENT_ROLE_ID = 1938120227459309569
DEFAULT_TENANT_ID = "000000"
DEFAULT_CREATE_BY = 1
DEFAULT_UPDATE_BY = 1
DEFAULT_PASSWORD_HASH = "$2a$10$Vm51QDgTteXRQKpERT2okumJh1sVm5YpvkiVQdaih/rgaOtHrm6yi"
BATCH_SIZE = 200
DB_RETRY_MAX = 3
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
logger = logging.getLogger("sync_his_xssj")
def get_db_connection():
return pymysql.connect(
**DB_CONFIG,
cursorclass=DictCursor,
autocommit=False,
connect_timeout=30,
read_timeout=60,
write_timeout=60,
)
def parse_enrollment_date(raw_value: Optional[str]) -> Optional[datetime]:
if not raw_value:
return None
text = str(raw_value).strip()
if not text:
return None
# 常见格式 "9/1/2009 0:0:0" 或 "2009-09-01 00:00:00"
fmts = ["%m/%d/%Y %H:%M:%S", "%m/%d/%Y %H:%M", "%m/%d/%Y", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d"]
for fmt in fmts:
try:
return datetime.strptime(text, fmt)
except Exception:
continue
# 作为最后手段尝试截取日期部分
try:
return datetime.strptime(text.split()[0], "%m/%d/%Y")
except Exception:
logger.warning("无法解析入学日期 -> raw='%s'", raw_value)
return None
def normalize_phone(raw_value: Optional[str], max_len: int = 11) -> Optional[str]:
if not raw_value:
return None
s = str(raw_value).strip()
return s[:max_len] if s else None
def map_gender(text: Optional[str]) -> str:
if not text:
return "0"
t = str(text).strip()
if t == "女":
return "1"
return "0"
def map_sys_user_status(his_status: Optional[str]) -> str:
"""sys_user.status: 毕业、结业、分流、停学、退学、休学 -> 1 ; 复学、在读 -> 0"""
if not his_status:
return "0"
s = str(his_status).strip()
if s in ("毕业", "结业", "分流", "停学", "退学", "休学"):
return "1"
return "0"
def map_student_status(his_status: Optional[str]) -> str:
"""xg_student_msg.student_status 映射"""
if not his_status:
return "1"
mapping = {
"毕业": "5",
"结业": "7",
"分流": "9",
"复学": "1",
"停学": "8",
"退学": "4",
"在读": "1",
"休学": "2",
}
return mapping.get(str(his_status).strip(), "1")
def find_class_dept(connection, class_code: Optional[str], class_name: Optional[str]) -> Tuple[Optional[int], Optional[str], Optional[int], Optional[str], Optional[int], Optional[str]]:
"""
根据 his_xssj 的班级编号与班级名称,查找 sys_dept 中对应的班级记录,
并向上查找专业(zy-)与学院(xy-)节点,返回 (class_id,class_name, major_id,major_name, college_id,college_name)
"""
if not class_code or not class_name:
return None, None, None, None, None, None
dept_category = f"bj-{class_code}"
with connection.cursor() as cursor:
cursor.execute(
"SELECT dept_id, dept_name, parent_id, dept_category FROM `sys_dept` WHERE dept_category = %s AND dept_name = %s LIMIT 1",
(dept_category, class_name),
)
row = cursor.fetchone()
if not row:
logger.warning("未在 sys_dept 中找到班级 -> dept_category=%s, dept_name=%s", dept_category, class_name)
return None, None, None, None, None, None
class_id = row["dept_id"]
class_name_ret = row["dept_name"]
parent_id = row.get("parent_id")
major_id = None
major_name = None
college_id = None
college_name = None
# 向上遍历父级查找 zy- 与 xy- 前缀
current_parent = parent_id
while current_parent:
cursor.execute("SELECT dept_id, dept_name, parent_id, dept_category FROM `sys_dept` WHERE dept_id = %s LIMIT 1", (current_parent,))
parent_row = cursor.fetchone()
if not parent_row:
break
cat = parent_row.get("dept_category") or ""
if cat.startswith("zy-") and major_id is None:
major_id = parent_row["dept_id"]
major_name = parent_row["dept_name"]
if cat.startswith("xy-") and college_id is None:
college_id = parent_row["dept_id"]
college_name = parent_row["dept_name"]
current_parent = parent_row.get("parent_id")
return class_id, class_name_ret, major_id, major_name, college_id, college_name
def load_class_msg(connection, class_id: Optional[int]) -> Dict[str, Any]:
"""
从 xg_class_msg 表读取与班级相关的字段,用于补齐 xg_student_msg 的若干字段
"""
if not class_id:
return {}
with connection.cursor() as cursor:
cursor.execute("SELECT * FROM `xg_class_msg` WHERE id = %s LIMIT 1", (class_id,))
return cursor.fetchone() or {}
def sync_from_his_xssj(truncate_before_sync: bool = False):
table_his = "his_xssj"
table_xg_student = TABLE_MAPPING.get("xg_student_msg", "xg_student_msg")
table_sys_user = "sys_user"
table_sys_user_role = "sys_user_role"
connection = get_db_connection()
try:
with connection.cursor() as cursor:
# 1) 是否清空目标表
if truncate_before_sync:
logger.info("正在清空表 %s ...", table_xg_student)
cursor.execute(f"TRUNCATE TABLE `{table_xg_student}`")
connection.commit()
logger.info("已清空 %s", table_xg_student)
# 2) 读取需同步的数据(跳过 班级编号 = '#YDQX#' 或 '')
select_sql = f"SELECT * FROM `{table_his}` WHERE `班级编号` IS NOT NULL AND `班级编号` NOT IN (%s, %s)"
cursor.execute(select_sql, ("#YDQX#", ""))
rows = cursor.fetchall()
logger.info("从 %s 读取到 %d 条候选记录(已过滤 '#YDQX#' 与空班级编号)", table_his, len(rows))
if not rows:
logger.info("没有符合条件的历史学生数据,退出。")
return
# 预加载已存在的学号集合与 sys_user user_id 以便去重
cursor.execute(f"SELECT student_no FROM `{table_xg_student}`")
existing_student_nos = {r["student_no"] for r in cursor.fetchall()}
cursor.execute("SELECT user_id FROM `sys_user`")
existing_user_ids = {int(r["user_id"]) for r in cursor.fetchall()}
base_datetime = datetime.now()
timestamp_str = base_datetime.strftime("%Y%m%d%H%M%S")
sequence = 1
inserted_student_count = 0
inserted_user_count = 0
inserted_role_count = 0
batch_counter = 0
# Prepare SQL statements (只插入必要字段)
insert_xg_student_sql = f"""
INSERT INTO `{table_xg_student}` (
id, tenant_id, create_dept, create_by, create_time,
update_by, update_time, deleted, student_no, id_card,
name, gender, birth_date, political_status, phone,
is_boarder, home_address, guardian_phone, admission_category,
student_status, enrollment_year, enrollment_date,
school_system, academic_level, training_type,
major_id, major_name, class_id, class_name, grade,
college_id, college_name, counselor_id, counselor_name,
class_teacher_id, class_teacher_name
) 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, %s, %s
)
"""
insert_sys_user_sql = f"""
INSERT INTO `{table_sys_user}` (
user_id, dept_id, user_name, nick_name, user_type,
email, phonenumber, sex, avatar, password, status,
del_flag, login_ip, login_date, create_by, create_time,
update_by, update_time, remark, tenant_id, account_type
) VALUES (
%s, %s, %s, %s, %s,
%s, %s, %s, %s, %s, %s,
%s, %s, %s, %s, %s,
%s, %s, %s, %s, %s
)
"""
insert_user_role_sql = f"""
INSERT INTO `{table_sys_user_role}` (`user_id`, `role_id`) VALUES (%s, %s)
"""
for row in rows:
try:
student_no = row.get("学号") or row.get("学生学号") or row.get("xh") or row.get("xs_xh") or row.get("xs_xh_x") or row.get("学号".strip())
# 如果无法从通用列名获取学号,尝试直接使用字段名 '学号'(上面多重尝试尽量兼容)
if not student_no:
logger.warning("跳过无学号记录: %s", row)
continue
student_no = str(student_no).strip()
if student_no in existing_student_nos:
logger.info("跳过已存在学号: %s", student_no)
continue
name = row.get("姓名") or row.get("xm") or row.get("学生姓名")
if not name:
logger.warning("跳过无姓名记录: %s", row)
continue
# 跳过指定班级编号的记录(再次防护)
class_code = row.get("班级编号")
if not class_code or str(class_code).strip() in ("", "#YDQX#"):
continue
class_name = row.get("班级名称") or row.get("班级")
class_id, class_name_val, major_id, major_name, college_id, college_name = find_class_dept(connection, str(class_code).strip(), class_name)
# 从 xg_class_msg 读取额外字段
class_msg = load_class_msg(connection, class_id)
# 处理字段映射
id_card = row.get("身份证号") or row.get("sfzjh")
gender = map_gender(row.get("性别") or row.get("xs_xb"))
birth_date = None
try:
raw_birth = row.get("出生日期") or row.get("csrq")
if raw_birth:
birth_date = datetime.strptime(str(raw_birth).split()[0], "%Y-%m-%d").date()
except Exception:
birth_date = None
political = row.get("政治面貌")
political_status = None
if political:
# 用户提供的映射示例: 群众 - 13 、 预备党员 - 02 、 团员 - 03
mapping_pol = {"群众": "13", "预备党员": "02", "团员": "03"}
political_status = mapping_pol.get(str(political).strip())
phone = normalize_phone(row.get("联系方式") or row.get("联系电话") or row.get("lxdh"))
guardian_phone = normalize_phone(row.get("家长电话") or row.get("guardian_phone"), max_len=20)
is_boarder = 1 if str(row.get("是否住校") or "").strip() == "是" else 0
home_address = row.get("家庭住址") or row.get("jtdz")
admission_category = "统招"
student_status_mapped = map_student_status(row.get("学生状态") or row.get("xsdqzt") or row.get("状态"))
enrollment_year = None
try:
enr = row.get("入学年份") or row.get("rxnj")
if enr:
enrollment_year = int(str(enr).strip())
except Exception:
enrollment_year = None
enrollment_date_dt = None
try:
enr_date_raw = row.get("入学日期") or row.get("rxrq")
if enr_date_raw:
parsed = parse_enrollment_date(str(enr_date_raw))
if parsed:
enrollment_date_dt = parsed.date()
except Exception:
enrollment_date_dt = None
# 生成唯一 id (与 sys_user.user_id 一致)
if sequence > 99999:
logger.error("单次同步ID序列超过99999,停止以避免ID溢出")
break
unique_id = int(f"{timestamp_str}{sequence:05d}")
sequence += 1
# 插入 xg_student_msg
params_xg_student = (
unique_id,
DEFAULT_TENANT_ID,
class_id or 0,
DEFAULT_CREATE_BY,
base_datetime,
DEFAULT_UPDATE_BY,
base_datetime,
0,
student_no,
id_card,
name,
gender,
birth_date,
political_status,
phone,
is_boarder,
home_address,
guardian_phone,
admission_category,
student_status_mapped,
enrollment_year,
enrollment_date_dt,
class_msg.get("school_system"),
class_msg.get("class_type"),
class_msg.get("training_type"),
major_id,
major_name,
class_id,
class_name_val,
class_msg.get("grade"),
college_id,
college_name,
class_msg.get("counselor_id"),
class_msg.get("counselor_name"),
class_msg.get("class_teacher_id"),
class_msg.get("class_teacher_name"),
)
# 插入 sys_user (如果不存在)
params_sys_user = (
unique_id,
class_id,
student_no,
name,
"sys_user",
None,
phone,
gender,
None,
DEFAULT_PASSWORD_HASH,
map_sys_user_status(row.get("学生状态") or row.get("xsdqzt")),
"0",
None,
None,
DEFAULT_CREATE_BY,
base_datetime,
DEFAULT_UPDATE_BY,
base_datetime,
"历史数据导入",
DEFAULT_TENANT_ID,
"2",
)
# 执行插入(带重试)
attempts = 0
while attempts < DB_RETRY_MAX:
try:
with connection.cursor() as c:
c.execute(insert_xg_student_sql, params_xg_student)
# sys_user 可能已经存在(通过 user_id 判断)
if unique_id not in existing_user_ids:
c.execute(insert_sys_user_sql, params_sys_user)
existing_user_ids.add(unique_id)
inserted_user_count += 1
# sys_user_role
c.execute("SELECT 1 FROM `sys_user_role` WHERE user_id = %s AND role_id = %s LIMIT 1", (unique_id, STUDENT_ROLE_ID))
if not c.fetchone():
c.execute(insert_user_role_sql, (unique_id, STUDENT_ROLE_ID))
inserted_role_count += 1
inserted_student_count += 1
existing_student_nos.add(student_no)
batch_counter += 1
if batch_counter >= BATCH_SIZE:
connection.commit()
logger.info("已批量提交 %d 条记录", batch_counter)
batch_counter = 0
break
except pymysql.err.OperationalError as e:
attempts += 1
logger.warning("数据库操作失败(尝试 %d/%d): %s", attempts, DB_RETRY_MAX, e)
time.sleep(attempts * 2)
# 重新获取连接
try:
connection.ping(reconnect=True)
except Exception:
connection = get_db_connection()
except Exception as e:
logger.exception("插入记录失败: %s; 数据: %s", e, params_xg_student)
# 不重试一般数据错误,跳过此记录
break
except Exception as outer_exc:
logger.exception("处理单条 his_xssj 记录出错: %s", outer_exc)
# 提交剩余
if batch_counter > 0:
connection.commit()
logger.info("已提交剩余 %d 条记录", batch_counter)
logger.info("同步完成:插入学生 %d 条,新增 sys_user %d 条,新增角色绑定 %d 条", inserted_student_count, inserted_user_count, inserted_role_count)
finally:
try:
connection.close()
except Exception:
pass
def prompt_confirm_truncate() -> bool:
answer = input("是否先清空 `xg_student_msg` 表?输入 y 确认,其他任意键取消: ").strip().lower()
return answer == "y"
def prompt_confirm_delete_students() -> bool:
answer = input("是否删除 `sys_user` 中 account_type=2 的学生用户及其 sys_user_role 关联?输入 y 确认,其他任意键取消: ").strip().lower()
return answer == "y"
def split_batches(values: List[int], size: int = 1000) -> List[List[int]]:
if not values:
return []
return [values[i : i + size] for i in range(0, len(values), size)]
def delete_student_accounts() -> Tuple[int, int]:
"""
删除 sys_user 中 account_type='2' 的用户以及其在 sys_user_role 中的关联记录。
返回 (deleted_users_count, deleted_roles_count)
"""
deleted_users = 0
deleted_roles = 0
conn = get_db_connection()
try:
with conn.cursor() as cursor:
cursor.execute("SELECT user_id FROM `sys_user` WHERE account_type = %s", ("2",))
rows = cursor.fetchall()
user_ids = [int(r["user_id"]) for r in rows]
if not user_ids:
logger.info("sys_user 中无 account_type=2 的用户,无需删除。")
return 0, 0
logger.info("找到 %d 个 student sys_user 将被删除(包括其 sys_user_role 关联)", len(user_ids))
for batch in split_batches(user_ids, size=500):
placeholders = ",".join(["%s"] * len(batch))
# 删除角色关联
sql_roles = f"DELETE FROM `sys_user_role` WHERE user_id IN ({placeholders})"
cursor.execute(sql_roles, batch)
deleted_roles += cursor.rowcount or 0
# 删除用户
sql_users = f"DELETE FROM `sys_user` WHERE user_id IN ({placeholders})"
cursor.execute(sql_users, batch)
deleted_users += cursor.rowcount or 0
conn.commit()
logger.info("已删除 %d users, %d role rows", deleted_users, deleted_roles)
return deleted_users, deleted_roles
finally:
try:
conn.close()
except Exception:
pass
def main():
parser = argparse.ArgumentParser(description="从 his_xssj 同步历史学生到 xg 系统")
parser.add_argument("--yes-delete-students", action="store_true", help="跳过提示,直接删除 sys_user 中 account_type=2 的学生及其 sys_user_role 关联")
parser.add_argument("--yes-truncate", action="store_true", help="跳过提示,直接清空 xg_student_msg 表后同步")
args = parser.parse_args()
# 询问是否删除 sys_user 中 account_type=2 的学生用户(以及其 sys_user_role)
if getattr(args, "yes_delete_students", False):
do_delete_students = True
else:
do_delete_students = prompt_confirm_delete_students()
if do_delete_students:
deleted_users, deleted_roles = delete_student_accounts()
logger.info("删除操作完成:删除用户 %d,删除角色关联 %d", deleted_users, deleted_roles)
# 询问是否清空表
if args.yes_truncate:
do_truncate = True
else:
do_truncate = prompt_confirm_truncate()
sync_from_his_xssj(truncate_before_sync=do_truncate)
if __name__ == "__main__":
main()
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
比较两个学生 Excel 文件的表头(列名)。
用法:
python compare_student_headers.py \
--file-a 学校历史数据迁移/student1222.xls \
--file-b 学校历史数据迁移/student1127.xls
脚本会打印:
- 是否完全相同(顺序及名称)
- 仅在 A 中存在的列
- 仅在 B 中存在的列
- 顺序不同但内容相同的列(名称相同但位置不同)
"""
from __future__ import annotations
import os
import sys
import argparse
from typing import List
import pandas as pd
def read_headers(path: str) -> List[str]:
if not os.path.exists(path):
raise FileNotFoundError(path)
ext = os.path.splitext(path)[1].lower()
if ext == ".xls":
df = pd.read_excel(path, nrows=0, engine="xlrd")
else:
# .xlsx or others
df = pd.read_excel(path, nrows=0, engine="openpyxl")
return [str(c).strip() for c in list(df.columns)]
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--file-a", type=str, required=True)
parser.add_argument("--file-b", type=str, required=True)
args = parser.parse_args()
try:
headers_a = read_headers(args.file_a)
except Exception as e:
print(f"读取 {args.file_a} 表头失败: {e}")
sys.exit(2)
try:
headers_b = read_headers(args.file_b)
except Exception as e:
print(f"读取 {args.file_b} 表头失败: {e}")
sys.exit(2)
print(f"{args.file_a} 列数: {len(headers_a)}")
print(f"{args.file_b} 列数: {len(headers_b)}")
if headers_a == headers_b:
print("表头完全相同(顺序及名称一致)。")
return
set_a = set(headers_a)
set_b = set(headers_b)
only_a = sorted(list(set_a - set_b))
only_b = sorted(list(set_b - set_a))
if not only_a and not only_b:
print("列集合相同,但顺序不同。")
# show first few mismatches by position
diffs = []
for i, (ca, cb) in enumerate(zip(headers_a, headers_b)):
if ca != cb:
diffs.append((i + 1, ca, cb))
if diffs:
print("位置不一致的前若干列(位置, A列名, B列名):")
for d in diffs[:20]:
print(f" {d[0]:>3}: {d[1]} | {d[2]}")
else:
# maybe lengths differ; list remainder
if len(headers_a) != len(headers_b):
print("两表长度不同,下面列出多余部分:")
if len(headers_a) > len(headers_b):
print("A 多余:", headers_a[len(headers_b):len(headers_b)+20])
else:
print("B 多余:", headers_b[len(headers_a):len(headers_a)+20])
return
# print differences
if only_a:
print(f"仅在 A 中存在的列 ({len(only_a)}):")
for c in only_a[:100]:
print(" -", c)
if only_b:
print(f"仅在 B 中存在的列 ({len(only_b)}):")
for c in only_b[:100]:
print(" -", c)
# also list positional mismatches up to 20
diffs = []
for i in range(min(len(headers_a), len(headers_b))):
if headers_a[i] != headers_b[i]:
diffs.append((i + 1, headers_a[i], headers_b[i]))
if diffs:
print("位置不一致的前若干列(位置, A列名, B列名):")
for d in diffs[:20]:
print(f" {d[0]:>3}: {d[1]} | {d[2]}")
if __name__ == "__main__":
main()
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
数据库配置文件
用于超星数据移植与定时同步脚本
"""
# 数据库配置信息
DB_CONFIG = {
"host": "47.99.164.66",
"port": 3306,
"database": "hzdyjs_xg",
"user": "root",
"password": "hn02le.34lkdLKD",
"charset": "utf8mb4"
}
# 表名映射(API类型 -> 数据库表名)
TABLE_MAPPING = {
"class": "his_bjsj", # 班级数据
"department": "his_xbsj", # 系部数据
"student": "his_xssj", # 学生数据
"major": "his_zysj" , # 专业数据
"teacher": "his_lssj" # 老师数据
}
@@ -0,0 +1,15 @@
CREATE TABLE `his_lssj` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`tenant_id` BIGINT(20) DEFAULT NULL,
`deleted` TINYINT(1) NOT NULL DEFAULT 0,
`create_by` BIGINT(20) DEFAULT NULL,
`create_time` DATETIME DEFAULT NULL,
`update_by` BIGINT(20) DEFAULT NULL,
`update_time` DATETIME DEFAULT NULL,
`工号` TEXT DEFAULT NULL,
`姓名` TEXT DEFAULT NULL,
`部门` TEXT DEFAULT NULL,
`性别` TEXT DEFAULT NULL,
`手机号码` TEXT DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -0,0 +1,56 @@
CREATE TABLE `his_xssj` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`tenant_id` BIGINT(20) DEFAULT NULL,
`deleted` TINYINT(1) NOT NULL DEFAULT 0,
`create_by` BIGINT(20) DEFAULT NULL,
`create_time` DATETIME DEFAULT NULL,
`update_by` BIGINT(20) DEFAULT NULL,
`update_time` DATETIME DEFAULT NULL,
`学号` TEXT DEFAULT NULL,
`姓名` TEXT DEFAULT NULL,
`性别` TEXT DEFAULT NULL,
`学生状态` TEXT DEFAULT NULL,
`身份证号` TEXT DEFAULT NULL,
`出生日期` TEXT DEFAULT NULL,
`籍贯` TEXT DEFAULT NULL,
`政治面貌` TEXT DEFAULT NULL,
`入学年份` TEXT DEFAULT NULL,
`入学日期` TEXT DEFAULT NULL,
`教学系部` TEXT DEFAULT NULL,
`班级名称` TEXT DEFAULT NULL,
`班主任` TEXT DEFAULT NULL,
`录取号` TEXT DEFAULT NULL,
`学生类别` TEXT DEFAULT NULL,
`学院编号` TEXT DEFAULT NULL,
`专业编号` TEXT DEFAULT NULL,
`专业名称` TEXT DEFAULT NULL,
`层次编号` TEXT DEFAULT NULL,
`报读层次` TEXT DEFAULT NULL,
`所属年级` TEXT DEFAULT NULL,
`班级编号` TEXT DEFAULT NULL,
`学制编码` TEXT DEFAULT NULL,
`学生标识号` TEXT DEFAULT NULL,
`生源省区编码` TEXT DEFAULT NULL,
`生源省区` TEXT DEFAULT NULL,
`银行卡号` TEXT DEFAULT NULL,
`联系方式` TEXT DEFAULT NULL,
`个人邮箱` TEXT DEFAULT NULL,
`家庭住址` TEXT DEFAULT NULL,
`招生季度` TEXT DEFAULT NULL,
`毕业院校` TEXT DEFAULT NULL,
`文化程度` TEXT DEFAULT NULL,
`收入来源` TEXT DEFAULT NULL,
`家庭年总收入` TEXT DEFAULT NULL,
`家庭人均收入` TEXT DEFAULT NULL,
`爱好特长` TEXT DEFAULT NULL,
`户口性质` TEXT DEFAULT NULL,
`户口所在地` TEXT DEFAULT NULL,
`是否低保` TEXT DEFAULT NULL,
`家长电话` TEXT DEFAULT NULL,
`家长姓名` TEXT DEFAULT NULL,
`是否空身份证` TEXT DEFAULT NULL,
`学生自定义类别` TEXT DEFAULT NULL,
`是否住校` TEXT DEFAULT NULL,
`宿舍号码` TEXT DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -0,0 +1,12 @@
{"time": "2025-12-22 23:30:23", "class_code": "", "class_name": "", "major_code": "", "major_name": "", "college_code": "18", "row": {"class_code": "", "class_name": "", "major_code": "", "major_name": "", "college_code": "18", "enrollment_year": "", "grade": "", "school_system": "3", "class_type": "0", "training_type": "全日制", "class_teacher_name": "", "enrollment_date": ""}, "his_rows_count": 1, "his_rows": [{"学号": "121", "姓名": "蔡11", "性别": "男", "学生状态": "毕业", "身份证号": "", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "", "入学日期": "", "教学系部": "烹饪酒管系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "18", "专业编号": "", "专业名称": "", "层次编号": "0", "报读层次": "三年制中级技工", "所属年级": "", "班级编号": "", "学制编码": "3", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "是", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}], "suggestions": []}
{"time": "2025-12-22 23:30:25", "class_code": "#YDQX#", "class_name": "", "major_code": "", "major_name": "", "college_code": "", "row": {"class_code": "#YDQX#", "class_name": "", "major_code": "", "major_name": "", "college_code": "", "enrollment_year": "2015", "grade": "", "school_system": "", "class_type": "", "training_type": "", "class_teacher_name": "", "enrollment_date": ""}, "his_rows_count": 11, "his_rows": [{"学号": "1202031018", "姓名": "陈鹏辉", "性别": "", "学生状态": "分流", "身份证号": "330327199701180811", "出生日期": "1997-01-18", "籍贯": "浙江省温州市苍南县", "政治面貌": "团员", "入学年份": "2012", "入学日期": "9/1/2012 0:0:0", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "14", "专业编号": "12020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "6", "学生标识号": "", "生源省区编码": "90", "生源省区": "东部", "银行卡号": "", "联系方式": "13705897902", "个人邮箱": "", "家庭住址": "苍南县龙港镇林兴村81号", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "1301173005", "姓名": "王家俊", "性别": "男", "学生状态": "毕业", "身份证号": "341022199708251314", "出生日期": "1997-08-25", "籍贯": "安徽省黄山市休宁县", "政治面貌": "群众", "入学年份": "2013", "入学日期": "9/1/2013 0:0:0", "教学系部": "机械工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "15", "专业编号": "13011735", "专业名称": "模具制造", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "5", "学生标识号": "", "生源省区编码": "92", "生源省区": "中部", "银行卡号": "", "联系方式": "13105819587", "个人邮箱": "", "家庭住址": "安徽省休宁县溪口镇", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201407112007", "姓名": "何静", "性别": "女", "学生状态": "毕业", "身份证号": "330127199904250620", "出生日期": "1999-04-25", "籍贯": "", "政治面貌": "", "入学年份": "2014", "入学日期": "", "教学系部": "园林技术系", "班级名称": "", "班主任": "", "录取号": "0016695", "学生类别": "", "学院编号": "17", "专业编号": "14071136", "专业名称": "园林(技师)", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "6", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "13282435316", "个人邮箱": "", "家庭住址": "浙江省杭州市淳安县文昌镇潭头村潭头七号", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "13173681445", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201413023138", "姓名": "俞静", "性别": "女", "学生状态": "退学", "身份证号": "330184199812295423", "出生日期": "1998-12-29", "籍贯": "", "政治面貌": "", "入学年份": "2014", "入学日期": "", "教学系部": "药学技术系", "班级名称": "", "班主任": "", "录取号": "0017214", "学生类别": "", "学院编号": "16", "专业编号": "14130235", "专业名称": "药物制剂(高技)", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "5", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "15858239793", "个人邮箱": "", "家庭住址": "浙江省杭州市余杭区径山镇长乐村", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "Line truncated
{"time": "2025-12-22 23:30:29", "class_code": "#YDQX#", "class_name": "", "major_code": "", "major_name": "", "college_code": "", "row": {"class_code": "#YDQX#", "class_name": "", "major_code": "", "major_name": "", "college_code": "", "enrollment_year": "2020", "grade": "", "school_system": "", "class_type": "", "training_type": "", "class_teacher_name": "", "enrollment_date": ""}, "his_rows_count": 11, "his_rows": [{"学号": "1202031018", "姓名": "陈鹏辉", "性别": "", "学生状态": "分流", "身份证号": "330327199701180811", "出生日期": "1997-01-18", "籍贯": "浙江省温州市苍南县", "政治面貌": "团员", "入学年份": "2012", "入学日期": "9/1/2012 0:0:0", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "14", "专业编号": "12020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "6", "学生标识号": "", "生源省区编码": "90", "生源省区": "东部", "银行卡号": "", "联系方式": "13705897902", "个人邮箱": "", "家庭住址": "苍南县龙港镇林兴村81号", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "1301173005", "姓名": "王家俊", "性别": "男", "学生状态": "毕业", "身份证号": "341022199708251314", "出生日期": "1997-08-25", "籍贯": "安徽省黄山市休宁县", "政治面貌": "群众", "入学年份": "2013", "入学日期": "9/1/2013 0:0:0", "教学系部": "机械工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "15", "专业编号": "13011735", "专业名称": "模具制造", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "5", "学生标识号": "", "生源省区编码": "92", "生源省区": "中部", "银行卡号": "", "联系方式": "13105819587", "个人邮箱": "", "家庭住址": "安徽省休宁县溪口镇", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201407112007", "姓名": "何静", "性别": "女", "学生状态": "毕业", "身份证号": "330127199904250620", "出生日期": "1999-04-25", "籍贯": "", "政治面貌": "", "入学年份": "2014", "入学日期": "", "教学系部": "园林技术系", "班级名称": "", "班主任": "", "录取号": "0016695", "学生类别": "", "学院编号": "17", "专业编号": "14071136", "专业名称": "园林(技师)", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "6", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "13282435316", "个人邮箱": "", "家庭住址": "浙江省杭州市淳安县文昌镇潭头村潭头七号", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "13173681445", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201413023138", "姓名": "俞静", "性别": "女", "学生状态": "退学", "身份证号": "330184199812295423", "出生日期": "1998-12-29", "籍贯": "", "政治面貌": "", "入学年份": "2014", "入学日期": "", "教学系部": "药学技术系", "班级名称": "", "班主任": "", "录取号": "0017214", "学生类别": "", "学院编号": "16", "专业编号": "14130235", "专业名称": "药物制剂(高技)", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "5", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "15858239793", "个人邮箱": "", "家庭住址": "浙江省杭州市余杭区径山镇长乐村", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "Line truncated
{"time": "2025-12-22 23:30:30", "class_code": "#YDQX#", "class_name": "", "major_code": "", "major_name": "", "college_code": "", "row": {"class_code": "#YDQX#", "class_name": "", "major_code": "", "major_name": "", "college_code": "", "enrollment_year": "2021", "grade": "", "school_system": "", "class_type": "", "training_type": "", "class_teacher_name": "", "enrollment_date": ""}, "his_rows_count": 11, "his_rows": [{"学号": "1202031018", "姓名": "陈鹏辉", "性别": "", "学生状态": "分流", "身份证号": "330327199701180811", "出生日期": "1997-01-18", "籍贯": "浙江省温州市苍南县", "政治面貌": "团员", "入学年份": "2012", "入学日期": "9/1/2012 0:0:0", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "14", "专业编号": "12020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "6", "学生标识号": "", "生源省区编码": "90", "生源省区": "东部", "银行卡号": "", "联系方式": "13705897902", "个人邮箱": "", "家庭住址": "苍南县龙港镇林兴村81号", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "1301173005", "姓名": "王家俊", "性别": "男", "学生状态": "毕业", "身份证号": "341022199708251314", "出生日期": "1997-08-25", "籍贯": "安徽省黄山市休宁县", "政治面貌": "群众", "入学年份": "2013", "入学日期": "9/1/2013 0:0:0", "教学系部": "机械工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "15", "专业编号": "13011735", "专业名称": "模具制造", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "5", "学生标识号": "", "生源省区编码": "92", "生源省区": "中部", "银行卡号": "", "联系方式": "13105819587", "个人邮箱": "", "家庭住址": "安徽省休宁县溪口镇", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201407112007", "姓名": "何静", "性别": "女", "学生状态": "毕业", "身份证号": "330127199904250620", "出生日期": "1999-04-25", "籍贯": "", "政治面貌": "", "入学年份": "2014", "入学日期": "", "教学系部": "园林技术系", "班级名称": "", "班主任": "", "录取号": "0016695", "学生类别": "", "学院编号": "17", "专业编号": "14071136", "专业名称": "园林(技师)", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "6", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "13282435316", "个人邮箱": "", "家庭住址": "浙江省杭州市淳安县文昌镇潭头村潭头七号", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "13173681445", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201413023138", "姓名": "俞静", "性别": "女", "学生状态": "退学", "身份证号": "330184199812295423", "出生日期": "1998-12-29", "籍贯": "", "政治面貌": "", "入学年份": "2014", "入学日期": "", "教学系部": "药学技术系", "班级名称": "", "班主任": "", "录取号": "0017214", "学生类别": "", "学院编号": "16", "专业编号": "14130235", "专业名称": "药物制剂(高技)", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "5", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "15858239793", "个人邮箱": "", "家庭住址": "浙江省杭州市余杭区径山镇长乐村", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "Line truncated
{"time": "2025-12-22 23:30:31", "class_code": "#YDQX#", "class_name": "", "major_code": "", "major_name": "", "college_code": "", "row": {"class_code": "#YDQX#", "class_name": "", "major_code": "", "major_name": "", "college_code": "", "enrollment_year": "2023", "grade": "", "school_system": "", "class_type": "", "training_type": "", "class_teacher_name": "", "enrollment_date": ""}, "his_rows_count": 11, "his_rows": [{"学号": "1202031018", "姓名": "陈鹏辉", "性别": "", "学生状态": "分流", "身份证号": "330327199701180811", "出生日期": "1997-01-18", "籍贯": "浙江省温州市苍南县", "政治面貌": "团员", "入学年份": "2012", "入学日期": "9/1/2012 0:0:0", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "14", "专业编号": "12020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "6", "学生标识号": "", "生源省区编码": "90", "生源省区": "东部", "银行卡号": "", "联系方式": "13705897902", "个人邮箱": "", "家庭住址": "苍南县龙港镇林兴村81号", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "1301173005", "姓名": "王家俊", "性别": "男", "学生状态": "毕业", "身份证号": "341022199708251314", "出生日期": "1997-08-25", "籍贯": "安徽省黄山市休宁县", "政治面貌": "群众", "入学年份": "2013", "入学日期": "9/1/2013 0:0:0", "教学系部": "机械工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "15", "专业编号": "13011735", "专业名称": "模具制造", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "5", "学生标识号": "", "生源省区编码": "92", "生源省区": "中部", "银行卡号": "", "联系方式": "13105819587", "个人邮箱": "", "家庭住址": "安徽省休宁县溪口镇", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201407112007", "姓名": "何静", "性别": "女", "学生状态": "毕业", "身份证号": "330127199904250620", "出生日期": "1999-04-25", "籍贯": "", "政治面貌": "", "入学年份": "2014", "入学日期": "", "教学系部": "园林技术系", "班级名称": "", "班主任": "", "录取号": "0016695", "学生类别": "", "学院编号": "17", "专业编号": "14071136", "专业名称": "园林(技师)", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "6", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "13282435316", "个人邮箱": "", "家庭住址": "浙江省杭州市淳安县文昌镇潭头村潭头七号", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "13173681445", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201413023138", "姓名": "俞静", "性别": "女", "学生状态": "退学", "身份证号": "330184199812295423", "出生日期": "1998-12-29", "籍贯": "", "政治面貌": "", "入学年份": "2014", "入学日期": "", "教学系部": "药学技术系", "班级名称": "", "班主任": "", "录取号": "0017214", "学生类别": "", "学院编号": "16", "专业编号": "14130235", "专业名称": "药物制剂(高技)", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "5", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "15858239793", "个人邮箱": "", "家庭住址": "浙江省杭州市余杭区径山镇长乐村", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "Line truncated
{"time": "2025-12-22 23:35:15", "class_code": "", "class_name": "", "major_code": "", "major_name": "", "college_code": "18", "row": {"class_code": "", "class_name": "", "major_code": "", "major_name": "", "college_code": "18", "enrollment_year": "", "grade": "", "school_system": "3", "class_type": "0", "training_type": "全日制", "class_teacher_name": "", "enrollment_date": ""}, "his_rows_count": 1, "his_rows": [{"学号": "121", "姓名": "蔡11", "性别": "男", "学生状态": "毕业", "身份证号": "", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "", "入学日期": "", "教学系部": "烹饪酒管系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "18", "专业编号": "", "专业名称": "", "层次编号": "0", "报读层次": "三年制中级技工", "所属年级": "", "班级编号": "", "学制编码": "3", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "是", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}], "suggestions": []}
{"time": "2025-12-22 23:35:18", "class_code": "#YDQX#", "class_name": "", "major_code": "", "major_name": "", "college_code": "", "row": {"class_code": "#YDQX#", "class_name": "", "major_code": "", "major_name": "", "college_code": "", "enrollment_year": "2015", "grade": "", "school_system": "", "class_type": "", "training_type": "", "class_teacher_name": "", "enrollment_date": ""}, "his_rows_count": 11, "his_rows": [{"学号": "1202031018", "姓名": "陈鹏辉", "性别": "", "学生状态": "分流", "身份证号": "330327199701180811", "出生日期": "1997-01-18", "籍贯": "浙江省温州市苍南县", "政治面貌": "团员", "入学年份": "2012", "入学日期": "9/1/2012 0:0:0", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "14", "专业编号": "12020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "6", "学生标识号": "", "生源省区编码": "90", "生源省区": "东部", "银行卡号": "", "联系方式": "13705897902", "个人邮箱": "", "家庭住址": "苍南县龙港镇林兴村81号", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "1301173005", "姓名": "王家俊", "性别": "男", "学生状态": "毕业", "身份证号": "341022199708251314", "出生日期": "1997-08-25", "籍贯": "安徽省黄山市休宁县", "政治面貌": "群众", "入学年份": "2013", "入学日期": "9/1/2013 0:0:0", "教学系部": "机械工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "15", "专业编号": "13011735", "专业名称": "模具制造", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "5", "学生标识号": "", "生源省区编码": "92", "生源省区": "中部", "银行卡号": "", "联系方式": "13105819587", "个人邮箱": "", "家庭住址": "安徽省休宁县溪口镇", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201407112007", "姓名": "何静", "性别": "女", "学生状态": "毕业", "身份证号": "330127199904250620", "出生日期": "1999-04-25", "籍贯": "", "政治面貌": "", "入学年份": "2014", "入学日期": "", "教学系部": "园林技术系", "班级名称": "", "班主任": "", "录取号": "0016695", "学生类别": "", "学院编号": "17", "专业编号": "14071136", "专业名称": "园林(技师)", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "6", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "13282435316", "个人邮箱": "", "家庭住址": "浙江省杭州市淳安县文昌镇潭头村潭头七号", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "13173681445", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201413023138", "姓名": "俞静", "性别": "女", "学生状态": "退学", "身份证号": "330184199812295423", "出生日期": "1998-12-29", "籍贯": "", "政治面貌": "", "入学年份": "2014", "入学日期": "", "教学系部": "药学技术系", "班级名称": "", "班主任": "", "录取号": "0017214", "学生类别": "", "学院编号": "16", "专业编号": "14130235", "专业名称": "药物制剂(高技)", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "5", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "15858239793", "个人邮箱": "", "家庭住址": "浙江省杭州市余杭区径山镇长乐村", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "Line truncated
{"time": "2025-12-22 23:35:23", "class_code": "#YDQX#", "class_name": "", "major_code": "", "major_name": "", "college_code": "", "row": {"class_code": "#YDQX#", "class_name": "", "major_code": "", "major_name": "", "college_code": "", "enrollment_year": "2020", "grade": "", "school_system": "", "class_type": "", "training_type": "", "class_teacher_name": "", "enrollment_date": ""}, "his_rows_count": 11, "his_rows": [{"学号": "1202031018", "姓名": "陈鹏辉", "性别": "", "学生状态": "分流", "身份证号": "330327199701180811", "出生日期": "1997-01-18", "籍贯": "浙江省温州市苍南县", "政治面貌": "团员", "入学年份": "2012", "入学日期": "9/1/2012 0:0:0", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "14", "专业编号": "12020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "6", "学生标识号": "", "生源省区编码": "90", "生源省区": "东部", "银行卡号": "", "联系方式": "13705897902", "个人邮箱": "", "家庭住址": "苍南县龙港镇林兴村81号", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "1301173005", "姓名": "王家俊", "性别": "男", "学生状态": "毕业", "身份证号": "341022199708251314", "出生日期": "1997-08-25", "籍贯": "安徽省黄山市休宁县", "政治面貌": "群众", "入学年份": "2013", "入学日期": "9/1/2013 0:0:0", "教学系部": "机械工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "15", "专业编号": "13011735", "专业名称": "模具制造", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "5", "学生标识号": "", "生源省区编码": "92", "生源省区": "中部", "银行卡号": "", "联系方式": "13105819587", "个人邮箱": "", "家庭住址": "安徽省休宁县溪口镇", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201407112007", "姓名": "何静", "性别": "女", "学生状态": "毕业", "身份证号": "330127199904250620", "出生日期": "1999-04-25", "籍贯": "", "政治面貌": "", "入学年份": "2014", "入学日期": "", "教学系部": "园林技术系", "班级名称": "", "班主任": "", "录取号": "0016695", "学生类别": "", "学院编号": "17", "专业编号": "14071136", "专业名称": "园林(技师)", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "6", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "13282435316", "个人邮箱": "", "家庭住址": "浙江省杭州市淳安县文昌镇潭头村潭头七号", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "13173681445", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201413023138", "姓名": "俞静", "性别": "女", "学生状态": "退学", "身份证号": "330184199812295423", "出生日期": "1998-12-29", "籍贯": "", "政治面貌": "", "入学年份": "2014", "入学日期": "", "教学系部": "药学技术系", "班级名称": "", "班主任": "", "录取号": "0017214", "学生类别": "", "学院编号": "16", "专业编号": "14130235", "专业名称": "药物制剂(高技)", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "5", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "15858239793", "个人邮箱": "", "家庭住址": "浙江省杭州市余杭区径山镇长乐村", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "Line truncated
{"time": "2025-12-22 23:35:24", "class_code": "#YDQX#", "class_name": "", "major_code": "", "major_name": "", "college_code": "", "row": {"class_code": "#YDQX#", "class_name": "", "major_code": "", "major_name": "", "college_code": "", "enrollment_year": "2021", "grade": "", "school_system": "", "class_type": "", "training_type": "", "class_teacher_name": "", "enrollment_date": ""}, "his_rows_count": 11, "his_rows": [{"学号": "1202031018", "姓名": "陈鹏辉", "性别": "", "学生状态": "分流", "身份证号": "330327199701180811", "出生日期": "1997-01-18", "籍贯": "浙江省温州市苍南县", "政治面貌": "团员", "入学年份": "2012", "入学日期": "9/1/2012 0:0:0", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "14", "专业编号": "12020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "6", "学生标识号": "", "生源省区编码": "90", "生源省区": "东部", "银行卡号": "", "联系方式": "13705897902", "个人邮箱": "", "家庭住址": "苍南县龙港镇林兴村81号", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "1301173005", "姓名": "王家俊", "性别": "男", "学生状态": "毕业", "身份证号": "341022199708251314", "出生日期": "1997-08-25", "籍贯": "安徽省黄山市休宁县", "政治面貌": "群众", "入学年份": "2013", "入学日期": "9/1/2013 0:0:0", "教学系部": "机械工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "15", "专业编号": "13011735", "专业名称": "模具制造", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "5", "学生标识号": "", "生源省区编码": "92", "生源省区": "中部", "银行卡号": "", "联系方式": "13105819587", "个人邮箱": "", "家庭住址": "安徽省休宁县溪口镇", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201407112007", "姓名": "何静", "性别": "女", "学生状态": "毕业", "身份证号": "330127199904250620", "出生日期": "1999-04-25", "籍贯": "", "政治面貌": "", "入学年份": "2014", "入学日期": "", "教学系部": "园林技术系", "班级名称": "", "班主任": "", "录取号": "0016695", "学生类别": "", "学院编号": "17", "专业编号": "14071136", "专业名称": "园林(技师)", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "6", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "13282435316", "个人邮箱": "", "家庭住址": "浙江省杭州市淳安县文昌镇潭头村潭头七号", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "13173681445", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201413023138", "姓名": "俞静", "性别": "女", "学生状态": "退学", "身份证号": "330184199812295423", "出生日期": "1998-12-29", "籍贯": "", "政治面貌": "", "入学年份": "2014", "入学日期": "", "教学系部": "药学技术系", "班级名称": "", "班主任": "", "录取号": "0017214", "学生类别": "", "学院编号": "16", "专业编号": "14130235", "专业名称": "药物制剂(高技)", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "5", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "15858239793", "个人邮箱": "", "家庭住址": "浙江省杭州市余杭区径山镇长乐村", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "Line truncated
{"time": "2025-12-22 23:35:26", "class_code": "#YDQX#", "class_name": "", "major_code": "", "major_name": "", "college_code": "", "row": {"class_code": "#YDQX#", "class_name": "", "major_code": "", "major_name": "", "college_code": "", "enrollment_year": "2023", "grade": "", "school_system": "", "class_type": "", "training_type": "", "class_teacher_name": "", "enrollment_date": ""}, "his_rows_count": 11, "his_rows": [{"学号": "1202031018", "姓名": "陈鹏辉", "性别": "", "学生状态": "分流", "身份证号": "330327199701180811", "出生日期": "1997-01-18", "籍贯": "浙江省温州市苍南县", "政治面貌": "团员", "入学年份": "2012", "入学日期": "9/1/2012 0:0:0", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "14", "专业编号": "12020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "6", "学生标识号": "", "生源省区编码": "90", "生源省区": "东部", "银行卡号": "", "联系方式": "13705897902", "个人邮箱": "", "家庭住址": "苍南县龙港镇林兴村81号", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "1301173005", "姓名": "王家俊", "性别": "男", "学生状态": "毕业", "身份证号": "341022199708251314", "出生日期": "1997-08-25", "籍贯": "安徽省黄山市休宁县", "政治面貌": "群众", "入学年份": "2013", "入学日期": "9/1/2013 0:0:0", "教学系部": "机械工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "15", "专业编号": "13011735", "专业名称": "模具制造", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "5", "学生标识号": "", "生源省区编码": "92", "生源省区": "中部", "银行卡号": "", "联系方式": "13105819587", "个人邮箱": "", "家庭住址": "安徽省休宁县溪口镇", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201407112007", "姓名": "何静", "性别": "女", "学生状态": "毕业", "身份证号": "330127199904250620", "出生日期": "1999-04-25", "籍贯": "", "政治面貌": "", "入学年份": "2014", "入学日期": "", "教学系部": "园林技术系", "班级名称": "", "班主任": "", "录取号": "0016695", "学生类别": "", "学院编号": "17", "专业编号": "14071136", "专业名称": "园林(技师)", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "6", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "13282435316", "个人邮箱": "", "家庭住址": "浙江省杭州市淳安县文昌镇潭头村潭头七号", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "13173681445", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201413023138", "姓名": "俞静", "性别": "女", "学生状态": "退学", "身份证号": "330184199812295423", "出生日期": "1998-12-29", "籍贯": "", "政治面貌": "", "入学年份": "2014", "入学日期": "", "教学系部": "药学技术系", "班级名称": "", "班主任": "", "录取号": "0017214", "学生类别": "", "学院编号": "16", "专业编号": "14130235", "专业名称": "药物制剂(高技)", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "5", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "15858239793", "个人邮箱": "", "家庭住址": "浙江省杭州市余杭区径山镇长乐村", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "Line truncated
{"time": "2025-12-23 10:44:15", "class_code": "", "class_name": "", "major_code": "", "major_name": "", "college_code": "18", "row": {"class_code": "", "class_name": "", "major_code": "", "major_name": "", "college_code": "18", "enrollment_year": "", "grade": "", "school_system": "3", "class_type": null, "training_type": "全日制", "class_teacher_name": "", "enrollment_date": ""}, "his_rows_count": 1, "his_rows": [{"学号": "121", "姓名": "蔡11", "性别": "男", "学生状态": "毕业", "身份证号": "", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "", "入学日期": "", "教学系部": "烹饪酒管系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "18", "专业编号": "", "专业名称": "", "层次编号": "0", "报读层次": "三年制中级技工", "所属年级": "", "班级编号": "", "学制编码": "3", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "是", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}], "suggestions": []}
{"time": "2025-12-23 10:44:20", "class_code": "#YDQX#", "class_name": "", "major_code": "", "major_name": "", "college_code": "", "row": {"class_code": "#YDQX#", "class_name": "", "major_code": "", "major_name": "", "college_code": "", "enrollment_year": "2023", "grade": "", "school_system": "", "class_type": null, "training_type": "", "class_teacher_name": "", "enrollment_date": ""}, "his_rows_count": 11, "his_rows": [{"学号": "1202031018", "姓名": "陈鹏辉", "性别": "", "学生状态": "分流", "身份证号": "330327199701180811", "出生日期": "1997-01-18", "籍贯": "浙江省温州市苍南县", "政治面貌": "团员", "入学年份": "2012", "入学日期": "9/1/2012 0:0:0", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "14", "专业编号": "12020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "6", "学生标识号": "", "生源省区编码": "90", "生源省区": "东部", "银行卡号": "", "联系方式": "13705897902", "个人邮箱": "", "家庭住址": "苍南县龙港镇林兴村81号", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "1301173005", "姓名": "王家俊", "性别": "男", "学生状态": "毕业", "身份证号": "341022199708251314", "出生日期": "1997-08-25", "籍贯": "安徽省黄山市休宁县", "政治面貌": "群众", "入学年份": "2013", "入学日期": "9/1/2013 0:0:0", "教学系部": "机械工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "全日制", "学院编号": "15", "专业编号": "13011735", "专业名称": "模具制造", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "5", "学生标识号": "", "生源省区编码": "92", "生源省区": "中部", "银行卡号": "", "联系方式": "13105819587", "个人邮箱": "", "家庭住址": "安徽省休宁县溪口镇", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201407112007", "姓名": "何静", "性别": "女", "学生状态": "毕业", "身份证号": "330127199904250620", "出生日期": "1999-04-25", "籍贯": "", "政治面貌": "", "入学年份": "2014", "入学日期": "", "教学系部": "园林技术系", "班级名称": "", "班主任": "", "录取号": "0016695", "学生类别": "", "学院编号": "17", "专业编号": "14071136", "专业名称": "园林(技师)", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "6", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "13282435316", "个人邮箱": "", "家庭住址": "浙江省杭州市淳安县文昌镇潭头村潭头七号", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "13173681445", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201413023138", "姓名": "俞静", "性别": "女", "学生状态": "退学", "身份证号": "330184199812295423", "出生日期": "1998-12-29", "籍贯": "", "政治面貌": "", "入学年份": "2014", "入学日期": "", "教学系部": "药学技术系", "班级名称": "", "班主任": "", "录取号": "0017214", "学生类别": "", "学院编号": "16", "专业编号": "14130235", "专业名称": "药物制剂(高技)", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "5", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "15858239793", "个人邮箱": "", "家庭住址": "浙江省杭州市余杭区径山镇长乐村", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "Line truncated
@@ -0,0 +1,7 @@
pandas>=2.2.0
sqlalchemy>=2.0
pymysql>=1.0.3
xlrd>=2.0.1
openpyxl>=3.0.0
Binary file not shown.
Binary file not shown.
+45
View File
@@ -0,0 +1,45 @@
/*
Navicat Premium Dump SQL
Source Server : 47.99.164.66 hn02le.34lkdLKD
Source Server Type : MySQL
Source Server Version : 50736 (5.7.36)
Source Host : 47.99.164.66:3306
Source Schema : hzdyjs_xg
Target Server Type : MySQL
Target Server Version : 50736 (5.7.36)
File Encoding : 65001
Date: 22/12/2025 16:50:49
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_dept
-- ----------------------------
DROP TABLE IF EXISTS `sys_dept`;
CREATE TABLE `sys_dept` (
`dept_id` bigint(20) NOT NULL COMMENT '部门id',
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
`parent_id` bigint(20) DEFAULT '0' COMMENT '父部门id',
`ancestors` varchar(500) DEFAULT '' COMMENT '祖级列表',
`dept_name` varchar(30) DEFAULT '' COMMENT '部门名称',
`dept_category` varchar(100) DEFAULT NULL COMMENT '部门类别编码',
`order_num` int(4) DEFAULT '0' COMMENT '显示顺序',
`leader` bigint(20) DEFAULT NULL COMMENT '负责人',
`phone` varchar(11) DEFAULT NULL COMMENT '联系电话',
`email` varchar(50) DEFAULT NULL COMMENT '邮箱',
`status` char(1) DEFAULT '0' COMMENT '部门状态(0正常 1停用)',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`dept_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='部门表';
SET FOREIGN_KEY_CHECKS = 1;
+50
View File
@@ -0,0 +1,50 @@
/*
Navicat Premium Dump SQL
Source Server : 47.99.164.66 hn02le.34lkdLKD
Source Server Type : MySQL
Source Server Version : 50736 (5.7.36)
Source Host : 47.99.164.66:3306
Source Schema : hzdyjs_xg
Target Server Type : MySQL
Target Server Version : 50736 (5.7.36)
File Encoding : 65001
Date: 22/12/2025 11:12:33
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`tenant_id` varchar(20) DEFAULT '000000' COMMENT '租户编号',
`dept_id` bigint(20) DEFAULT NULL COMMENT '部门ID',
`user_name` varchar(30) NOT NULL COMMENT '用户账号',
`nick_name` varchar(30) NOT NULL COMMENT '用户昵称',
`user_type` varchar(10) DEFAULT 'sys_user' COMMENT '用户类型(sys_user系统用户)',
`email` varchar(50) DEFAULT '' COMMENT '用户邮箱',
`phonenumber` varchar(11) DEFAULT '' COMMENT '手机号码',
`sex` char(1) DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
`avatar` bigint(20) DEFAULT NULL COMMENT '头像地址',
`password` varchar(100) DEFAULT '' COMMENT '密码',
`status` char(1) DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
`login_ip` varchar(128) DEFAULT '' COMMENT '最后登录IP',
`login_date` datetime DEFAULT NULL COMMENT '最后登录时间',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`account_type` varchar(255) DEFAULT '1' COMMENT '账户类型 1 - 教职工 2- 学生',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';
SET FOREIGN_KEY_CHECKS = 1;
@@ -0,0 +1,30 @@
/*
Navicat Premium Dump SQL
Source Server : 47.99.164.66 hn02le.34lkdLKD
Source Server Type : MySQL
Source Server Version : 50736 (5.7.36)
Source Host : 47.99.164.66:3306
Source Schema : hzdyjs_xg
Target Server Type : MySQL
Target Server Version : 50736 (5.7.36)
File Encoding : 65001
Date: 22/12/2025 11:31:59
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`role_id` bigint(20) NOT NULL COMMENT '角色ID',
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户和角色关联表';
SET FOREIGN_KEY_CHECKS = 1;
Binary file not shown.
+98
View File
@@ -0,0 +1,98 @@
/*
Navicat Premium Dump SQL
Source Server : 47.99.164.66 hn02le.34lkdLKD
Source Server Type : MySQL
Source Server Version : 50736 (5.7.36)
Source Host : 47.99.164.66:3306
Source Schema : hzdyjs_xg
Target Server Type : MySQL
Target Server Version : 50736 (5.7.36)
File Encoding : 65001
Date: 22/12/2025 15:11:55
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for xg_class_msg
-- ----------------------------
DROP TABLE IF EXISTS `xg_class_msg`;
CREATE TABLE `xg_class_msg` (
`id` bigint(20) NOT NULL COMMENT '主键ID(雪花ID)',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
`college_id` bigint(20) DEFAULT NULL COMMENT '学院代码',
`college_name` varchar(100) DEFAULT NULL COMMENT '学院名称',
`major_id` bigint(20) DEFAULT NULL COMMENT '专业代码',
`major_name` varchar(100) DEFAULT NULL COMMENT '专业名称',
`class_code` varchar(32) NOT NULL COMMENT '班级代码',
`class_name` varchar(100) NOT NULL COMMENT '班级名称',
`class_type` varchar(20) DEFAULT '1' COMMENT '班级类型:0 - 三年制中级技工 、 1 - 三年制技师 、 2 - 四年制高级工 、3 - 五年制高级技工 、4 - 六年制技师',
`grade` varchar(10) NOT NULL COMMENT '年级',
`enrollment_year` int(4) NOT NULL COMMENT '入学年份',
`graduation_year` int(4) DEFAULT NULL COMMENT '毕业年份',
`training_type` varchar(20) DEFAULT NULL COMMENT '培养方式:全日制,在职,函授,夜大等',
`degree_type` varchar(20) DEFAULT NULL COMMENT '学位类型:学士,硕士,博士,专科',
`counselor_id` bigint(20) DEFAULT NULL COMMENT '辅导员ID',
`counselor_name` varchar(50) DEFAULT NULL COMMENT '辅导员姓名',
`class_teacher_id` bigint(20) DEFAULT NULL COMMENT '班主任ID',
`class_teacher_name` varchar(50) DEFAULT NULL COMMENT '班主任姓名',
`class_advisor_id` bigint(20) DEFAULT NULL COMMENT '班级导师ID',
`class_advisor_name` varchar(50) DEFAULT NULL COMMENT '班级导师姓名',
`class_monitor_id` bigint(20) DEFAULT NULL COMMENT '班长ID',
`class_monitor_name` varchar(50) DEFAULT NULL COMMENT '班长姓名',
`deputy_monitor_id` bigint(20) DEFAULT NULL COMMENT '副班长ID',
`deputy_monitor_name` varchar(50) DEFAULT NULL COMMENT '副班长姓名',
`class_status` char(1) DEFAULT '1' COMMENT '班级状态:1-正常,2-暂停,3-毕业,4-解散',
`is_graduation` tinyint(1) DEFAULT '0' COMMENT '是否已毕业:0-未毕业,1-已毕业',
`graduation_date` date DEFAULT NULL COMMENT '毕业日期',
`planned_capacity` int(11) DEFAULT '50' COMMENT '计划容量',
`current_student_count` int(11) DEFAULT '0' COMMENT '当前学生人数',
`max_student_count` int(11) DEFAULT '50' COMMENT '最大学生人数',
`male_count` int(11) DEFAULT '0' COMMENT '男生人数',
`female_count` int(11) DEFAULT '0' COMMENT '女生人数',
`active_count` int(11) DEFAULT '0' COMMENT '在籍人数',
`transfer_in_count` int(11) DEFAULT '0' COMMENT '转入人数',
`transfer_out_count` int(11) DEFAULT '0' COMMENT '转出人数',
`dropout_count` int(11) DEFAULT '0' COMMENT '退学人数',
`graduate_count` int(11) DEFAULT '0' COMMENT '毕业人数',
`office_location` varchar(200) DEFAULT NULL COMMENT '班级办公室位置',
`class_location` varchar(100) DEFAULT NULL COMMENT '班级教室位置',
`contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
`qq_group` varchar(20) DEFAULT NULL COMMENT 'QQ群号',
`wechat_group` varchar(100) DEFAULT NULL COMMENT '微信群',
`email` varchar(100) DEFAULT NULL COMMENT '班级邮箱',
`class_schedule` text COMMENT '课程安排(JSON格式)',
`class_culture` text COMMENT '班级文化建设',
`class_rules` text COMMENT '班级规章制度',
`class_description` varchar(500) DEFAULT NULL COMMENT '班级描述',
`last_count_update` datetime DEFAULT NULL COMMENT '最后统计更新时间',
`ext_field1` varchar(200) DEFAULT NULL COMMENT '扩展字段1',
`ext_field2` varchar(200) DEFAULT NULL COMMENT '扩展字段2',
`ext_field3` varchar(200) DEFAULT NULL COMMENT '扩展字段3',
`remark` varchar(500) DEFAULT NULL COMMENT '备注信息',
`school_system` int(11) DEFAULT '4' COMMENT '学制(年)',
PRIMARY KEY (`id`) USING BTREE,
KEY `uk_class_code_tenant` (`class_code`,`tenant_id`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_college_id` (`college_id`),
KEY `idx_enrollment_year` (`enrollment_year`),
KEY `idx_grade` (`grade`),
KEY `idx_major_id` (`major_id`),
KEY `idx_counselor_id` (`counselor_id`),
KEY `idx_class_teacher_id` (`class_teacher_id`),
KEY `idx_class_status` (`class_status`),
KEY `idx_college_grade` (`college_id`,`grade`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班级信息表-新版';
SET FOREIGN_KEY_CHECKS = 1;
@@ -0,0 +1,51 @@
/*
Navicat Premium Dump SQL
Source Server : 47.99.164.66 hn02le.34lkdLKD
Source Server Type : MySQL
Source Server Version : 50736 (5.7.36)
Source Host : 47.99.164.66:3306
Source Schema : hzdyjs_xg
Target Server Type : MySQL
Target Server Version : 50736 (5.7.36)
File Encoding : 65001
Date: 22/12/2025 15:46:09
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for xg_class_teacher_counselor_rel
-- ----------------------------
DROP TABLE IF EXISTS `xg_class_teacher_counselor_rel`;
CREATE TABLE `xg_class_teacher_counselor_rel` (
`id` bigint(20) NOT NULL COMMENT '主键ID(雪花ID)',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
`class_id` bigint(20) NOT NULL COMMENT '班级ID,关联xg_class_msg.id',
`class_name` varchar(100) DEFAULT NULL COMMENT '班级名称',
`teacher_id` bigint(20) NOT NULL COMMENT '教师ID,关联sys_user.user_id',
`teacher_name` varchar(50) DEFAULT NULL COMMENT '教师姓名',
`teacher_type` varchar(20) NOT NULL COMMENT '教师类型:class_teacher-班主任,counselor-辅导员,advisor-导师',
`start_date` datetime NOT NULL COMMENT '开始日期',
`end_date` datetime DEFAULT NULL COMMENT '结束日期',
`remark` varchar(500) DEFAULT NULL COMMENT '备注信息',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_class_id` (`class_id`),
KEY `idx_teacher_id` (`teacher_id`),
KEY `idx_teacher_type` (`teacher_type`),
KEY `idx_start_date` (`start_date`),
KEY `idx_end_date` (`end_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班级班主任辅导员关联表';
SET FOREIGN_KEY_CHECKS = 1;
+150
View File
@@ -0,0 +1,150 @@
/*
Navicat Premium Dump SQL
Source Server : 47.99.164.66 hn02le.34lkdLKD
Source Server Type : MySQL
Source Server Version : 50736 (5.7.36)
Source Host : 47.99.164.66:3306
Source Schema : hzdyjs_xg
Target Server Type : MySQL
Target Server Version : 50736 (5.7.36)
File Encoding : 65001
Date: 23/12/2025 15:15:38
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for xg_student_msg
-- ----------------------------
DROP TABLE IF EXISTS `xg_student_msg`;
CREATE TABLE `xg_student_msg` (
`id` bigint(20) NOT NULL COMMENT '主键ID(雪花ID)',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`deleted` tinyint(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
`student_no` varchar(32) NOT NULL COMMENT '学号',
`id_card` varchar(18) DEFAULT NULL COMMENT '身份证号',
`name` varchar(50) NOT NULL COMMENT '姓名',
`name_english` varchar(100) DEFAULT NULL COMMENT '英文姓名',
`former_name` varchar(50) DEFAULT NULL COMMENT '曾用名',
`gender` char(1) NOT NULL DEFAULT '0' COMMENT '性别:0-男,1-女',
`birth_date` date DEFAULT NULL COMMENT '出生日期',
`nationality` varchar(20) DEFAULT NULL COMMENT '民族 ',
`native_place` varchar(20) DEFAULT NULL COMMENT '籍贯',
`political_status` varchar(20) DEFAULT NULL COMMENT '政治面貌',
`hukou_location` varchar(1000) DEFAULT NULL COMMENT '户籍所在地',
`home_address` varchar(1000) DEFAULT NULL COMMENT '家庭地址',
`current_address` varchar(1000) DEFAULT NULL COMMENT '现住址',
`postal_code` varchar(10) DEFAULT NULL COMMENT '邮政编码',
`photo_url` varchar(500) DEFAULT NULL COMMENT '照片URL',
`photo_path` varchar(500) DEFAULT NULL COMMENT '学生照片存储路径',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号码',
`is_boarder` tinyint(1) DEFAULT '0' COMMENT '是否住校:0-否,1-是',
`education_level` varchar(20) DEFAULT NULL COMMENT '文化程度',
`household_register_type` varchar(20) DEFAULT NULL COMMENT '户口类型:农业、非农业',
`religion` varchar(200) DEFAULT NULL COMMENT '宗教信仰',
`source_region` varchar(100) DEFAULT NULL COMMENT '生源地区(省市县)',
`graduate_school` varchar(100) DEFAULT NULL COMMENT '毕业院校',
`email` varchar(100) DEFAULT NULL COMMENT '电子邮箱',
`qq` varchar(20) DEFAULT NULL COMMENT 'QQ号码',
`wechat` varchar(50) DEFAULT NULL COMMENT '微信号',
`marital_status` varchar(10) DEFAULT NULL COMMENT '婚姻状况:1 未婚、2 已婚、3 离异、4 丧偶 ',
`emergency_contact_name` varchar(50) DEFAULT NULL COMMENT '紧急联系人姓名',
`emergency_contact_phone` varchar(20) DEFAULT NULL COMMENT '紧急联系人电话',
`emergency_contact_relation` varchar(100) DEFAULT NULL COMMENT '紧急联系人关系',
`is_poor_student` tinyint(1) DEFAULT '0' COMMENT '是否贫困生:0-否,1-是',
`is_minority` tinyint(1) DEFAULT '0' COMMENT '是否少数民族:0-否,1-是',
`is_disabled` tinyint(1) DEFAULT '0' COMMENT '是否残疾:0-否,1-是',
`is_orphan` tinyint(1) DEFAULT '0' COMMENT '是否孤儿:0-否,1-是',
`is_left_behind_child` tinyint(1) DEFAULT '0' COMMENT '是否留守儿童:0-否,1-是',
`is_single_child` tinyint(1) DEFAULT '0' COMMENT '是否独生子女:0-否,1-是',
`is_low_income` tinyint(1) DEFAULT '0' COMMENT '是否低保:0-否,1-是',
`has_loan` tinyint(1) DEFAULT '0' COMMENT '是否有助学贷款:0-否,1-是',
`loan_amount` decimal(10,2) DEFAULT NULL COMMENT '助学贷款金额',
`specialties` text COMMENT '特长爱好',
`awards` text COMMENT '获奖情况',
`violations` text COMMENT '违纪记录',
`social_activities` text COMMENT '社会活动',
`father_name` varchar(50) DEFAULT NULL COMMENT '父亲姓名',
`father_phone` varchar(20) DEFAULT NULL COMMENT '父亲电话',
`father_work_unit` varchar(1000) DEFAULT NULL COMMENT '父亲工作单位',
`mother_name` varchar(50) DEFAULT NULL COMMENT '母亲姓名',
`mother_phone` varchar(20) DEFAULT NULL COMMENT '母亲电话',
`mother_work_unit` varchar(1000) DEFAULT NULL COMMENT '母亲工作单位',
`guardian_name` varchar(50) DEFAULT NULL COMMENT '监护人姓名',
`guardian_phone` varchar(20) DEFAULT NULL COMMENT '监护人电话',
`guardian_relation` varchar(20) DEFAULT NULL COMMENT '监护人关系',
`family_income` decimal(12,2) DEFAULT NULL COMMENT '家庭年收入',
`per_capita_income` decimal(12,2) DEFAULT NULL COMMENT '人均收入',
`income_source` varchar(100) DEFAULT NULL COMMENT '收入来源',
`family_economic_status` varchar(20) DEFAULT NULL COMMENT '家庭经济状况:富裕、一般、困难、特困',
`family_members` tinyint(2) DEFAULT NULL COMMENT '家庭成员数量',
`height` decimal(10,2) DEFAULT NULL COMMENT '身高(cm)',
`weight` decimal(10,2) DEFAULT NULL COMMENT '体重(kg)',
`blood_type` varchar(10) DEFAULT NULL COMMENT '血型',
`health_status` varchar(1000) DEFAULT NULL COMMENT '健康状况',
`special_condition` varchar(1000) DEFAULT NULL COMMENT '特殊体质或疾病',
`medical_history` varchar(1000) DEFAULT NULL COMMENT '病史',
`allergies` varchar(1000) DEFAULT NULL COMMENT '过敏史',
`disability_type` varchar(50) DEFAULT NULL COMMENT '残疾类型 1 - 视力残疾 2 - 听力残疾 3 - 智力残疾 9 - 其他残疾',
`disability_level` varchar(20) DEFAULT NULL COMMENT '残疾等级',
`admission_category` varchar(20) DEFAULT NULL COMMENT '录取类别:统招、自主招生、专项计划等',
`student_status` char(1) DEFAULT '1' COMMENT '学籍状态:1-在籍,2-休学,3-复学,4-退学,5-毕业 , 6- 不在籍',
`enrollment_year` int(4) DEFAULT NULL COMMENT '入学年份',
`enrollment_date` date DEFAULT NULL COMMENT '入学日期',
`bank_card_no` varchar(30) DEFAULT NULL COMMENT '银行卡号',
`bank_name` varchar(100) DEFAULT NULL COMMENT '开户银行',
`bank_branch` varchar(100) DEFAULT NULL COMMENT '开户支行',
`card_holder_name` varchar(50) DEFAULT NULL COMMENT '持卡人姓名',
`info_status` char(1) DEFAULT '1' COMMENT '信息状态:1-正常,2-待审核,3-审核中,4-已驳回,5-已锁定',
`data_source` varchar(20) DEFAULT 'manual' COMMENT '数据来源:manual-手工录入,import-批量导入,sync-系统同步',
`last_batch_id` bigint(20) DEFAULT NULL COMMENT '最后修改批次ID',
`last_audit_time` datetime DEFAULT NULL COMMENT '最后审核时间',
`is_complete` tinyint(1) DEFAULT '0' COMMENT '信息是否完整:0-不完整,1-完整',
`completion_rate` decimal(10,2) DEFAULT '0.00' COMMENT '完整度百分比',
`school_system` int(2) DEFAULT '4' COMMENT '学制(年)- 预留,数据从班级表关联',
`academic_level` varchar(20) DEFAULT NULL COMMENT '学历层次 - 预留,数据从班级表关联',
`training_type` varchar(20) DEFAULT NULL COMMENT '培养方式:全日制、在职、函授、夜大等 - 预留,数据从班级表关联',
`major_id` varchar(100) DEFAULT NULL COMMENT '专业id - 预留,数据从班级表关联',
`major_name` varchar(100) DEFAULT NULL COMMENT '专业名称 - 预留,数据从班级表关联',
`class_id` varchar(100) DEFAULT NULL COMMENT '班级id - 预留,数据从班级表关联',
`class_name` varchar(100) DEFAULT NULL COMMENT '班级名称 - 预留,数据从班级表关联',
`grade` varchar(10) DEFAULT NULL COMMENT '年级 - 预留,数据从班级表关联',
`college_id` varchar(100) DEFAULT NULL COMMENT '学院id - 预留,数据从班级表关联',
`college_name` varchar(100) DEFAULT NULL COMMENT '学院名称 - 预留,数据从班级表关联',
`counselor_id` bigint(20) DEFAULT NULL COMMENT '辅导员ID - 预留,数据从班级表关联',
`counselor_name` varchar(50) DEFAULT NULL COMMENT '辅导员姓名 - 预留,数据从班级表关联',
`class_teacher_id` bigint(20) DEFAULT NULL COMMENT '班主任ID - 预留,数据从班级表关联',
`class_teacher_name` varchar(50) DEFAULT NULL COMMENT '班主任姓名 - 预留,数据从班级表关联',
`dormitory_building` varchar(100) DEFAULT NULL COMMENT '宿舍楼栋 - 预留,数据从班级表关联 楼栋id',
`dormitory_room` varchar(100) DEFAULT NULL COMMENT '宿舍房间号 - 预留,数据从班级表关联 房间id',
`ext_field1` varchar(200) DEFAULT NULL COMMENT '扩展字段1 - 床位id',
`ext_field2` varchar(200) DEFAULT NULL COMMENT '扩展字段2',
`ext_field3` varchar(200) DEFAULT NULL COMMENT '扩展字段3',
`ext_field4` varchar(200) DEFAULT NULL COMMENT '扩展字段4',
`ext_field5` varchar(500) DEFAULT NULL COMMENT '扩展字段5',
`remark` varchar(500) DEFAULT NULL COMMENT '备注信息',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_deleted` (`deleted`),
KEY `idx_name` (`name`),
KEY `idx_student_status` (`student_status`),
KEY `idx_info_status` (`info_status`),
KEY `idx_enrollment_year` (`enrollment_year`),
KEY `idx_last_batch_id` (`last_batch_id`),
KEY `idx_phone` (`phone`),
KEY `idx_create_time` (`create_time`),
KEY `idx_status_filter` (`tenant_id`,`student_status`,`info_status`,`deleted`),
KEY `uk_student_no_tenant` (`student_no`,`tenant_id`) USING BTREE,
KEY `uk_id_card_tenant` (`id_card`,`tenant_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生信息表';
SET FOREIGN_KEY_CHECKS = 1;