diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java index 487d1fbc..587ce095 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java @@ -64,14 +64,14 @@ public class SysUserServiceImpl implements ISysUserService, UserService { @Override public TableDataInfo selectPageUserList(SysUserBo user, PageQuery pageQuery) { - pageQuery.withDefaultOrder("u.user_name"); + pageQuery.withDefaultOrder("u.user_name", "asc"); Page page = baseMapper.selectPageUserList(pageQuery.build(), this.buildQueryWrapper(user)); return TableDataInfo.build(page); } @Override public TableDataInfo selectPageUserListByOr(SysUserBo user, PageQuery pageQuery) { - pageQuery.withDefaultOrder("u.user_name"); + pageQuery.withDefaultOrder("u.user_name", "asc"); Page page = baseMapper.selectPageUserList(pageQuery.build(), this.buildQueryWrapperByOr(user)); return TableDataInfo.build(page); } diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml b/backend/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml index fcaa58bb..8e495680 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml +++ b/backend/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml @@ -24,7 +24,7 @@ ${ew.getCustomSqlSegment} - , u.user_name asc, u.create_time desc + , u.create_time desc order by u.user_name asc, u.create_time desc @@ -45,7 +45,7 @@ ${ew.getCustomSqlSegment} - , u.user_name asc, u.create_time desc + , u.create_time desc order by u.user_name asc, u.create_time desc @@ -63,7 +63,7 @@ ${ew.getCustomSqlSegment} - , u.user_name asc, u.create_time desc + , u.create_time desc order by u.user_name asc, u.create_time desc @@ -80,7 +80,7 @@ ${ew.getCustomSqlSegment} - , u.user_name asc, u.create_time desc + , u.create_time desc order by u.user_name asc, u.create_time desc diff --git a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgClassMsgController.java b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgClassMsgController.java index 306996e0..402b3672 100644 --- a/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgClassMsgController.java +++ b/backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgClassMsgController.java @@ -45,6 +45,12 @@ public class XgClassMsgController extends BaseController { @SaCheckPermission("classMsg:classMsg:list") @GetMapping("/list") public TableDataInfo 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); } diff --git a/web/src/views/xg/xsjcxx/studentMsg/studentMsg/index.vue b/web/src/views/xg/xsjcxx/studentMsg/studentMsg/index.vue index 4ddf7b0f..8d05ba21 100644 --- a/web/src/views/xg/xsjcxx/studentMsg/studentMsg/index.vue +++ b/web/src/views/xg/xsjcxx/studentMsg/studentMsg/index.vue @@ -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[] => { return (nodes || []).map((node) => { @@ -2042,11 +2063,17 @@ const filterDeptTreeByStatus = (nodes: any[], status?: string): any[] => { } else { cloned.children = []; } + // 如果开启了状态过滤,仅保留符合状态的班级节点 if (targetStatus && isClassNode(cloned)) { if (cloned.classStatus !== targetStatus) { return null; } } + + // 当开启状态过滤且节点不是班级节点且没有子节点时,过滤掉该节点(和 stuRoster 行为一致) + if (targetStatus && !isClassNode(cloned) && (!cloned.children || cloned.children.length === 0)) { + return null; + } return cloned; }) .filter((item) => item !== null) as any[]; @@ -2142,8 +2169,10 @@ const extractDeptTreeData = (payload: any): any[] => { /** 构建部门树 */ const rebuildDeptTree = async (treeData: any[], resetSelection = true) => { + // 先规范化树结构,保证每个节点都有 label / disabled / children 字段 + const processed = processDeptTreeData(treeData); await ensureClassStatusMap(); - const enhanced = enhanceDeptTreeNodes(treeData); + const enhanced = enhanceDeptTreeNodes(processed); rawDeptOptions.value = enhanced; applyClassStatusFilter(!resetSelection); if (resetSelection) { @@ -3215,7 +3244,7 @@ onMounted(() => { /* 部门树组件样式 */ .dept-tree { - height: 600px; + height: calc(100vh - 280px); /* 与 stuRoster 左侧树一致,随视窗高度自适应 */ overflow-y: auto; overflow-x: auto; width: 330px; /* 固定宽度,超出时显示滚动条 */ diff --git a/学校历史数据迁移/01-import_history.py b/学校历史数据迁移/01-import_history.py new file mode 100644 index 00000000..67622e06 --- /dev/null +++ b/学校历史数据迁移/01-import_history.py @@ -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) + + diff --git a/学校历史数据迁移/02-update_sys_dept.py b/学校历史数据迁移/02-update_sys_dept.py new file mode 100644 index 00000000..32a842fc --- /dev/null +++ b/学校历史数据迁移/02-update_sys_dept.py @@ -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() + + diff --git a/学校历史数据迁移/03-insert_sys_user.py b/学校历史数据迁移/03-insert_sys_user.py new file mode 100644 index 00000000..9af1c1dd --- /dev/null +++ b/学校历史数据迁移/03-insert_sys_user.py @@ -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() + + diff --git a/学校历史数据迁移/04-01-dedupe_sys_dept.py b/学校历史数据迁移/04-01-dedupe_sys_dept.py new file mode 100644 index 00000000..ca3e414e --- /dev/null +++ b/学校历史数据迁移/04-01-dedupe_sys_dept.py @@ -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()) + + diff --git a/学校历史数据迁移/04-02-reorder_dept_order_num.py b/学校历史数据迁移/04-02-reorder_dept_order_num.py new file mode 100644 index 00000000..da34eea1 --- /dev/null +++ b/学校历史数据迁移/04-02-reorder_dept_order_num.py @@ -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()) + + diff --git a/学校历史数据迁移/04-insert_majors.py b/学校历史数据迁移/04-insert_majors.py new file mode 100644 index 00000000..39b344e5 --- /dev/null +++ b/学校历史数据迁移/04-insert_majors.py @@ -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() + + diff --git a/学校历史数据迁移/05-sort_majors_by_dept_category.py b/学校历史数据迁移/05-sort_majors_by_dept_category.py new file mode 100644 index 00000000..899a8bdc --- /dev/null +++ b/学校历史数据迁移/05-sort_majors_by_dept_category.py @@ -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() + + diff --git a/学校历史数据迁移/06-insert_classes.py b/学校历史数据迁移/06-insert_classes.py new file mode 100644 index 00000000..c66abff3 --- /dev/null +++ b/学校历史数据迁移/06-insert_classes.py @@ -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() + + diff --git a/学校历史数据迁移/07-reorder_classes_by_grade.py b/学校历史数据迁移/07-reorder_classes_by_grade.py new file mode 100644 index 00000000..5813df14 --- /dev/null +++ b/学校历史数据迁移/07-reorder_classes_by_grade.py @@ -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() + + diff --git a/学校历史数据迁移/08-insert_students_from_his.py b/学校历史数据迁移/08-insert_students_from_his.py new file mode 100644 index 00000000..ae62c0d9 --- /dev/null +++ b/学校历史数据迁移/08-insert_students_from_his.py @@ -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() + + diff --git a/学校历史数据迁移/__pycache__/db_config.cpython-312.pyc b/学校历史数据迁移/__pycache__/db_config.cpython-312.pyc new file mode 100644 index 00000000..be23cf46 Binary files /dev/null and b/学校历史数据迁移/__pycache__/db_config.cpython-312.pyc differ diff --git a/学校历史数据迁移/compare_student_headers.py b/学校历史数据迁移/compare_student_headers.py new file mode 100644 index 00000000..d3000106 --- /dev/null +++ b/学校历史数据迁移/compare_student_headers.py @@ -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() + + diff --git a/学校历史数据迁移/db_config.py b/学校历史数据迁移/db_config.py new file mode 100644 index 00000000..0909491c --- /dev/null +++ b/学校历史数据迁移/db_config.py @@ -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" # 老师数据 +} + diff --git a/学校历史数据迁移/his_lssj_create.sql b/学校历史数据迁移/his_lssj_create.sql new file mode 100644 index 00000000..828c4d5a --- /dev/null +++ b/学校历史数据迁移/his_lssj_create.sql @@ -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; \ No newline at end of file diff --git a/学校历史数据迁移/his_xssj_create.sql b/学校历史数据迁移/his_xssj_create.sql new file mode 100644 index 00000000..0665f5c7 --- /dev/null +++ b/学校历史数据迁移/his_xssj_create.sql @@ -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; \ No newline at end of file diff --git a/学校历史数据迁移/logs/06_insert_classes_skipped_majors.jsonl b/学校历史数据迁移/logs/06_insert_classes_skipped_majors.jsonl new file mode 100644 index 00000000..e2006621 --- /dev/null +++ b/学校历史数据迁移/logs/06_insert_classes_skipped_majors.jsonl @@ -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", "个人邮箱": "", "家庭住址": "浙江省杭州市余杭区径山镇长乐村", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201502032106", "姓名": "王鹏", "性别": "男", "学生状态": "退学", "身份证号": "330127200005103112", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2015", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "201802032046", "姓名": "赵仁保", "性别": "男", "学生状态": "分流", "身份证号": "422801200305171211", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2018", "入学日期": "", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "14", "专业编号": "18020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202005193114", "姓名": "林杭雪", "性别": "女", "学生状态": "退学", "身份证号": "330103200412031020", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2020", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202107113140", "姓名": "杨俊洁", "性别": "男", "学生状态": "退学", "身份证号": "330329200510231936", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2021", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202202032029", "姓名": "魏亚培", "性别": "男", "学生状态": "退学", "身份证号": "411728200703077310", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2022", "入学日期": "", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "14", "专业编号": "22020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202205193055", "姓名": "周昂呈", "性别": "男", "学生状态": "退学", "身份证号": "330108200704061354", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2022", "入学日期": "", "教学系部": "烹饪酒管系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "18", "专业编号": "22051935", "专业名称": "酒店管理(西餐运营与管理)", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202305033034", "姓名": "吴怡璇", "性别": "女", "学生状态": "退学", "身份证号": "320321200806017266", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2023", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}], "suggestions": []} +{"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", "个人邮箱": "", "家庭住址": "浙江省杭州市余杭区径山镇长乐村", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201502032106", "姓名": "王鹏", "性别": "男", "学生状态": "退学", "身份证号": "330127200005103112", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2015", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "201802032046", "姓名": "赵仁保", "性别": "男", "学生状态": "分流", "身份证号": "422801200305171211", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2018", "入学日期": "", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "14", "专业编号": "18020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202005193114", "姓名": "林杭雪", "性别": "女", "学生状态": "退学", "身份证号": "330103200412031020", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2020", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202107113140", "姓名": "杨俊洁", "性别": "男", "学生状态": "退学", "身份证号": "330329200510231936", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2021", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202202032029", "姓名": "魏亚培", "性别": "男", "学生状态": "退学", "身份证号": "411728200703077310", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2022", "入学日期": "", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "14", "专业编号": "22020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202205193055", "姓名": "周昂呈", "性别": "男", "学生状态": "退学", "身份证号": "330108200704061354", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2022", "入学日期": "", "教学系部": "烹饪酒管系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "18", "专业编号": "22051935", "专业名称": "酒店管理(西餐运营与管理)", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202305033034", "姓名": "吴怡璇", "性别": "女", "学生状态": "退学", "身份证号": "320321200806017266", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2023", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}], "suggestions": []} +{"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", "个人邮箱": "", "家庭住址": "浙江省杭州市余杭区径山镇长乐村", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201502032106", "姓名": "王鹏", "性别": "男", "学生状态": "退学", "身份证号": "330127200005103112", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2015", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "201802032046", "姓名": "赵仁保", "性别": "男", "学生状态": "分流", "身份证号": "422801200305171211", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2018", "入学日期": "", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "14", "专业编号": "18020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202005193114", "姓名": "林杭雪", "性别": "女", "学生状态": "退学", "身份证号": "330103200412031020", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2020", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202107113140", "姓名": "杨俊洁", "性别": "男", "学生状态": "退学", "身份证号": "330329200510231936", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2021", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202202032029", "姓名": "魏亚培", "性别": "男", "学生状态": "退学", "身份证号": "411728200703077310", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2022", "入学日期": "", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "14", "专业编号": "22020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202205193055", "姓名": "周昂呈", "性别": "男", "学生状态": "退学", "身份证号": "330108200704061354", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2022", "入学日期": "", "教学系部": "烹饪酒管系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "18", "专业编号": "22051935", "专业名称": "酒店管理(西餐运营与管理)", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202305033034", "姓名": "吴怡璇", "性别": "女", "学生状态": "退学", "身份证号": "320321200806017266", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2023", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}], "suggestions": []} +{"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", "个人邮箱": "", "家庭住址": "浙江省杭州市余杭区径山镇长乐村", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201502032106", "姓名": "王鹏", "性别": "男", "学生状态": "退学", "身份证号": "330127200005103112", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2015", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "201802032046", "姓名": "赵仁保", "性别": "男", "学生状态": "分流", "身份证号": "422801200305171211", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2018", "入学日期": "", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "14", "专业编号": "18020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202005193114", "姓名": "林杭雪", "性别": "女", "学生状态": "退学", "身份证号": "330103200412031020", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2020", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202107113140", "姓名": "杨俊洁", "性别": "男", "学生状态": "退学", "身份证号": "330329200510231936", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2021", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202202032029", "姓名": "魏亚培", "性别": "男", "学生状态": "退学", "身份证号": "411728200703077310", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2022", "入学日期": "", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "14", "专业编号": "22020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202205193055", "姓名": "周昂呈", "性别": "男", "学生状态": "退学", "身份证号": "330108200704061354", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2022", "入学日期": "", "教学系部": "烹饪酒管系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "18", "专业编号": "22051935", "专业名称": "酒店管理(西餐运营与管理)", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202305033034", "姓名": "吴怡璇", "性别": "女", "学生状态": "退学", "身份证号": "320321200806017266", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2023", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}], "suggestions": []} +{"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", "个人邮箱": "", "家庭住址": "浙江省杭州市余杭区径山镇长乐村", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201502032106", "姓名": "王鹏", "性别": "男", "学生状态": "退学", "身份证号": "330127200005103112", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2015", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "201802032046", "姓名": "赵仁保", "性别": "男", "学生状态": "分流", "身份证号": "422801200305171211", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2018", "入学日期": "", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "14", "专业编号": "18020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202005193114", "姓名": "林杭雪", "性别": "女", "学生状态": "退学", "身份证号": "330103200412031020", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2020", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202107113140", "姓名": "杨俊洁", "性别": "男", "学生状态": "退学", "身份证号": "330329200510231936", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2021", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202202032029", "姓名": "魏亚培", "性别": "男", "学生状态": "退学", "身份证号": "411728200703077310", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2022", "入学日期": "", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "14", "专业编号": "22020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202205193055", "姓名": "周昂呈", "性别": "男", "学生状态": "退学", "身份证号": "330108200704061354", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2022", "入学日期": "", "教学系部": "烹饪酒管系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "18", "专业编号": "22051935", "专业名称": "酒店管理(西餐运营与管理)", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202305033034", "姓名": "吴怡璇", "性别": "女", "学生状态": "退学", "身份证号": "320321200806017266", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2023", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}], "suggestions": []} +{"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", "个人邮箱": "", "家庭住址": "浙江省杭州市余杭区径山镇长乐村", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201502032106", "姓名": "王鹏", "性别": "男", "学生状态": "退学", "身份证号": "330127200005103112", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2015", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "201802032046", "姓名": "赵仁保", "性别": "男", "学生状态": "分流", "身份证号": "422801200305171211", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2018", "入学日期": "", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "14", "专业编号": "18020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202005193114", "姓名": "林杭雪", "性别": "女", "学生状态": "退学", "身份证号": "330103200412031020", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2020", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202107113140", "姓名": "杨俊洁", "性别": "男", "学生状态": "退学", "身份证号": "330329200510231936", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2021", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202202032029", "姓名": "魏亚培", "性别": "男", "学生状态": "退学", "身份证号": "411728200703077310", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2022", "入学日期": "", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "14", "专业编号": "22020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202205193055", "姓名": "周昂呈", "性别": "男", "学生状态": "退学", "身份证号": "330108200704061354", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2022", "入学日期": "", "教学系部": "烹饪酒管系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "18", "专业编号": "22051935", "专业名称": "酒店管理(西餐运营与管理)", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202305033034", "姓名": "吴怡璇", "性别": "女", "学生状态": "退学", "身份证号": "320321200806017266", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2023", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}], "suggestions": []} +{"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", "个人邮箱": "", "家庭住址": "浙江省杭州市余杭区径山镇长乐村", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201502032106", "姓名": "王鹏", "性别": "男", "学生状态": "退学", "身份证号": "330127200005103112", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2015", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "201802032046", "姓名": "赵仁保", "性别": "男", "学生状态": "分流", "身份证号": "422801200305171211", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2018", "入学日期": "", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "14", "专业编号": "18020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202005193114", "姓名": "林杭雪", "性别": "女", "学生状态": "退学", "身份证号": "330103200412031020", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2020", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202107113140", "姓名": "杨俊洁", "性别": "男", "学生状态": "退学", "身份证号": "330329200510231936", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2021", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202202032029", "姓名": "魏亚培", "性别": "男", "学生状态": "退学", "身份证号": "411728200703077310", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2022", "入学日期": "", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "14", "专业编号": "22020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202205193055", "姓名": "周昂呈", "性别": "男", "学生状态": "退学", "身份证号": "330108200704061354", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2022", "入学日期": "", "教学系部": "烹饪酒管系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "18", "专业编号": "22051935", "专业名称": "酒店管理(西餐运营与管理)", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202305033034", "姓名": "吴怡璇", "性别": "女", "学生状态": "退学", "身份证号": "320321200806017266", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2023", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}], "suggestions": []} +{"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", "个人邮箱": "", "家庭住址": "浙江省杭州市余杭区径山镇长乐村", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201502032106", "姓名": "王鹏", "性别": "男", "学生状态": "退学", "身份证号": "330127200005103112", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2015", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "201802032046", "姓名": "赵仁保", "性别": "男", "学生状态": "分流", "身份证号": "422801200305171211", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2018", "入学日期": "", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "14", "专业编号": "18020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202005193114", "姓名": "林杭雪", "性别": "女", "学生状态": "退学", "身份证号": "330103200412031020", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2020", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202107113140", "姓名": "杨俊洁", "性别": "男", "学生状态": "退学", "身份证号": "330329200510231936", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2021", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202202032029", "姓名": "魏亚培", "性别": "男", "学生状态": "退学", "身份证号": "411728200703077310", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2022", "入学日期": "", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "14", "专业编号": "22020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202205193055", "姓名": "周昂呈", "性别": "男", "学生状态": "退学", "身份证号": "330108200704061354", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2022", "入学日期": "", "教学系部": "烹饪酒管系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "18", "专业编号": "22051935", "专业名称": "酒店管理(西餐运营与管理)", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202305033034", "姓名": "吴怡璇", "性别": "女", "学生状态": "退学", "身份证号": "320321200806017266", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2023", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}], "suggestions": []} +{"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", "个人邮箱": "", "家庭住址": "浙江省杭州市余杭区径山镇长乐村", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "是", "宿舍号码": ""}, {"学号": "201502032106", "姓名": "王鹏", "性别": "男", "学生状态": "退学", "身份证号": "330127200005103112", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2015", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "201802032046", "姓名": "赵仁保", "性别": "男", "学生状态": "分流", "身份证号": "422801200305171211", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2018", "入学日期": "", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "14", "专业编号": "18020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202005193114", "姓名": "林杭雪", "性别": "女", "学生状态": "退学", "身份证号": "330103200412031020", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2020", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202107113140", "姓名": "杨俊洁", "性别": "男", "学生状态": "退学", "身份证号": "330329200510231936", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2021", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202202032029", "姓名": "魏亚培", "性别": "男", "学生状态": "退学", "身份证号": "411728200703077310", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2022", "入学日期": "", "教学系部": "电气工程系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "14", "专业编号": "22020326", "专业名称": "电气自动化设备安装与维修", "层次编号": "4", "报读层次": "六年制技师", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202205193055", "姓名": "周昂呈", "性别": "男", "学生状态": "退学", "身份证号": "330108200704061354", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2022", "入学日期": "", "教学系部": "烹饪酒管系", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "18", "专业编号": "22051935", "专业名称": "酒店管理(西餐运营与管理)", "层次编号": "3", "报读层次": "五年制高级技工", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "秋季", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}, {"学号": "202305033034", "姓名": "吴怡璇", "性别": "女", "学生状态": "退学", "身份证号": "320321200806017266", "出生日期": "", "籍贯": "", "政治面貌": "", "入学年份": "2023", "入学日期": "", "教学系部": "", "班级名称": "", "班主任": "", "录取号": "", "学生类别": "", "学院编号": "", "专业编号": "", "专业名称": "", "层次编号": "", "报读层次": "", "所属年级": "", "班级编号": "#YDQX#", "学制编码": "", "学生标识号": "", "生源省区编码": "", "生源省区": "", "银行卡号": "", "联系方式": "", "个人邮箱": "", "家庭住址": "", "招生季度": "", "毕业院校": "", "文化程度": "", "收入来源": "", "家庭年总收入": "", "家庭人均收入": "", "爱好特长": "", "户口性质": "", "户口所在地": "", "是否低保": "", "家长电话": "", "家长姓名": "", "是否空身份证": "否", "学生自定义类别": "", "是否住校": "", "宿舍号码": ""}], "suggestions": []} diff --git a/学校历史数据迁移/requirements.txt b/学校历史数据迁移/requirements.txt new file mode 100644 index 00000000..6597f517 --- /dev/null +++ b/学校历史数据迁移/requirements.txt @@ -0,0 +1,7 @@ +pandas>=2.2.0 +sqlalchemy>=2.0 +pymysql>=1.0.3 +xlrd>=2.0.1 +openpyxl>=3.0.0 + + diff --git a/学校历史数据迁移/student1127.xls b/学校历史数据迁移/student1127.xls new file mode 100644 index 00000000..d160187b Binary files /dev/null and b/学校历史数据迁移/student1127.xls differ diff --git a/学校历史数据迁移/student1222.xls b/学校历史数据迁移/student1222.xls new file mode 100644 index 00000000..0f47a348 Binary files /dev/null and b/学校历史数据迁移/student1222.xls differ diff --git a/学校历史数据迁移/sys_dept.sql b/学校历史数据迁移/sys_dept.sql new file mode 100644 index 00000000..0eabf39c --- /dev/null +++ b/学校历史数据迁移/sys_dept.sql @@ -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; diff --git a/学校历史数据迁移/sys_user.sql b/学校历史数据迁移/sys_user.sql new file mode 100644 index 00000000..53f62806 --- /dev/null +++ b/学校历史数据迁移/sys_user.sql @@ -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; diff --git a/学校历史数据迁移/sys_user_role.sql b/学校历史数据迁移/sys_user_role.sql new file mode 100644 index 00000000..abea3792 --- /dev/null +++ b/学校历史数据迁移/sys_user_role.sql @@ -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; diff --git a/学校历史数据迁移/teacher.xlsx b/学校历史数据迁移/teacher.xlsx new file mode 100644 index 00000000..fc68dd94 Binary files /dev/null and b/学校历史数据迁移/teacher.xlsx differ diff --git a/学校历史数据迁移/xg_class_msg.sql b/学校历史数据迁移/xg_class_msg.sql new file mode 100644 index 00000000..17586fa5 --- /dev/null +++ b/学校历史数据迁移/xg_class_msg.sql @@ -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; diff --git a/学校历史数据迁移/xg_class_teacher_counselor_rel.sql b/学校历史数据迁移/xg_class_teacher_counselor_rel.sql new file mode 100644 index 00000000..f76552df --- /dev/null +++ b/学校历史数据迁移/xg_class_teacher_counselor_rel.sql @@ -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; diff --git a/学校历史数据迁移/xg_student_msg.sql b/学校历史数据迁移/xg_student_msg.sql new file mode 100644 index 00000000..e670fbb2 --- /dev/null +++ b/学校历史数据迁移/xg_student_msg.sql @@ -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;