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