From 34c33ebc54425c7089c652529cbc53ba987ba999 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Fri, 9 Jan 2026 10:24:31 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 教职工数据迁移/README.md | 63 +++++ 教职工数据迁移/db_config.py | 26 ++ 教职工数据迁移/import_external_staff.py | 138 +++++++++ 教职工数据迁移/import_staff_data.py | 263 ++++++++++++++++++ .../insert_external_staff_to_sys_user.py | 252 +++++++++++++++++ 教职工数据迁移/requirements.txt | 5 + 教职工数据迁移/simple_import.py | 100 +++++++ 教职工数据迁移/update_sys_user_from_temp.py | 180 ++++++++++++ 教职工数据迁移/verify_import.sql | 51 ++++ 教职工数据迁移/人员清单列表1@正式员工.xlsx | Bin 0 -> 23922 bytes 教职工数据迁移/人员清单列表@外聘.xlsx | Bin 0 -> 14669 bytes 教职工数据迁移/教职工数据迁移总结.md | 128 +++++++++ 12 files changed, 1206 insertions(+) create mode 100644 教职工数据迁移/README.md create mode 100644 教职工数据迁移/db_config.py create mode 100644 教职工数据迁移/import_external_staff.py create mode 100644 教职工数据迁移/import_staff_data.py create mode 100644 教职工数据迁移/insert_external_staff_to_sys_user.py create mode 100644 教职工数据迁移/requirements.txt create mode 100644 教职工数据迁移/simple_import.py create mode 100644 教职工数据迁移/update_sys_user_from_temp.py create mode 100644 教职工数据迁移/verify_import.sql create mode 100644 教职工数据迁移/人员清单列表1@正式员工.xlsx create mode 100644 教职工数据迁移/人员清单列表@外聘.xlsx create mode 100644 教职工数据迁移/教职工数据迁移总结.md diff --git a/教职工数据迁移/README.md b/教职工数据迁移/README.md new file mode 100644 index 00000000..bd5330fe --- /dev/null +++ b/教职工数据迁移/README.md @@ -0,0 +1,63 @@ +# 教职工数据迁移脚本 + +## 概述 + +本脚本用于将 `人员清单列表1@正式员工.xlsx` 中的教职工数据导入到MySQL临时表中。 + +## Excel文件格式 + +Excel文件应包含以下列(第一行为标题栏): +- 序号 +- 姓名 +- 性别 +- 职务 +- 部门 +- 是否在编 +- 手机号 + +## 使用方法 + +### 1. 安装依赖 + +```bash +pip install -r requirements.txt +``` + +### 2. 配置数据库连接 + +脚本会自动从 `db_config.py` 文件读取数据库配置。如果文件不存在,脚本会尝试从上级目录的 `学校历史数据迁移/db_config.py` 查找。 + +### 3. 运行脚本 + +```bash +python import_staff_data.py +``` + +## 输出 + +- 创建临时表 `temp_staff_data` +- 导入所有教职工数据 +- 显示导入统计信息和数据预览 + +## 临时表结构 + +```sql +CREATE TABLE `temp_staff_data` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `序号` TEXT DEFAULT NULL, + `姓名` TEXT DEFAULT NULL, + `性别` TEXT DEFAULT NULL, + `职务` TEXT DEFAULT NULL, + `部门` TEXT DEFAULT NULL, + `是否在编` TEXT DEFAULT NULL, + `手机号` TEXT DEFAULT NULL, + `import_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +## 注意事项 + +- 脚本会自动清理列名中的特殊字符 +- 空值会被转换为NULL +- 所有数据列使用TEXT类型以确保兼容性 \ No newline at end of file diff --git a/教职工数据迁移/db_config.py b/教职工数据迁移/db_config.py new file mode 100644 index 00000000..0909491c --- /dev/null +++ b/教职工数据迁移/db_config.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +数据库配置文件 +用于超星数据移植与定时同步脚本 +""" + +# 数据库配置信息 +DB_CONFIG = { + "host": "47.99.164.66", + "port": 3306, + "database": "hzdyjs_xg", + "user": "root", + "password": "hn02le.34lkdLKD", + "charset": "utf8mb4" +} + +# 表名映射(API类型 -> 数据库表名) +TABLE_MAPPING = { + "class": "his_bjsj", # 班级数据 + "department": "his_xbsj", # 系部数据 + "student": "his_xssj", # 学生数据 + "major": "his_zysj" , # 专业数据 + "teacher": "his_lssj" # 老师数据 +} + diff --git a/教职工数据迁移/import_external_staff.py b/教职工数据迁移/import_external_staff.py new file mode 100644 index 00000000..c8519856 --- /dev/null +++ b/教职工数据迁移/import_external_staff.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +将 `人员清单列表@外聘.xlsx` 导入到临时表 temp_external_staff + +用法: + python import_external_staff.py + +依赖: + pandas sqlalchemy pymysql openpyxl + +说明: + - 从同目录或上级目录读取 db_config.py 中的 DB_CONFIG + - 自动根据 Excel 表头创建临时表(所有列使用 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, text + +try: + import openpyxl +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 clean_column_name(col: str) -> str: + if col is None: + return "" + s = str(col).strip() + s = s.replace("(", "_").replace(")", "_").replace("(", "_").replace(")", "_") + s = s.replace("、", "_").replace(" ", "_").replace("\n", "_").replace("\r", "_") + # ensure valid identifier start + if not s: + s = "col" + if not (s[0].isalpha() or s[0] == "_"): + s = f"c_{s}" + return s + + +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}" + return create_engine(uri, pool_pre_ping=True) + + +def create_table(conn, table_name: str, columns: List[str]): + # 先单独执行 DROP,再执行 CREATE(避免多语句执行限制导致语法错误) + conn.execute(text(f"DROP TABLE IF EXISTS `{table_name}`")) + lines: List[str] = [] + lines.append(f"CREATE TABLE `{table_name}` (") + lines.append(" `id` BIGINT(20) NOT NULL AUTO_INCREMENT,") + for col in columns: + lines.append(f" `{col}` TEXT DEFAULT NULL,") + lines.append(" `import_time` DATETIME DEFAULT CURRENT_TIMESTAMP,") + lines.append(" PRIMARY KEY (`id`)") + lines.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;") + sql = "\n".join(lines) + conn.execute(text(sql)) + + +def import_excel_to_table(engine, excel_path: str, table_name: str, chunk_size: int = 1000): + if not os.path.exists(excel_path): + raise FileNotFoundError(f"找不到文件: {excel_path}") + # 读取表头和全部数据使用 pandas(适合中小文件) + df = pd.read_excel(excel_path, engine="openpyxl") + raw_cols = list(df.columns) + clean_cols = [clean_column_name(c) for c in raw_cols] + # 重命名 DataFrame 列为清理后的列名 + df.columns = clean_cols + + with engine.begin() as conn: + # 创建表 + create_table(conn, table_name, clean_cols) + # 分块写入 + total = 0 + for start in range(0, len(df), chunk_size): + chunk = df.iloc[start:start+chunk_size].where(pd.notnull(df), None) + chunk.to_sql(name=table_name, con=conn, index=False, if_exists="append", method="multi", chunksize=chunk_size) + total += len(chunk) + return total + + +def main(): + parser = argparse.ArgumentParser(description="将人员清单(外聘)导入临时表") + parser.add_argument("--file", type=str, default="人员清单列表@外聘.xlsx", help="Excel 文件名或路径") + parser.add_argument("--table", type=str, default="temp_external_staff", help="临时表名") + parser.add_argument("--db-config", type=str, default="../学校历史数据迁移/db_config.py", help="db_config.py 路径") + args = parser.parse_args() + + base_dir = os.path.dirname(os.path.abspath(__file__)) + excel_path = args.file + if not os.path.isabs(excel_path): + excel_path = os.path.join(base_dir, excel_path) + + db_config_path = args.db_config + if not os.path.isabs(db_config_path): + db_config_path = os.path.join(base_dir, db_config_path) + if not os.path.exists(db_config_path): + # try parent folder + db_config_path = os.path.join(os.path.dirname(base_dir), "学校历史数据迁移", "db_config.py") + if not os.path.exists(db_config_path): + print(f"找不到 db_config 文件: {db_config_path}") + sys.exit(1) + + db_config = load_db_config_from_file(db_config_path) + engine = build_engine_from_config(db_config) + + print(f"开始导入文件: {excel_path} 到表 {args.table}") + total = import_excel_to_table(engine, excel_path, args.table, chunk_size=1000) + print(f"导入完成,共插入 {total} 条记录到 {args.table}") + + +if __name__ == "__main__": + main() + diff --git a/教职工数据迁移/import_staff_data.py b/教职工数据迁移/import_staff_data.py new file mode 100644 index 00000000..d0e7474d --- /dev/null +++ b/教职工数据迁移/import_staff_data.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +导入教职工数据到临时表 + +用法: + python import_staff_data.py + +依赖: + pip install pandas sqlalchemy pymysql openpyxl xlrd + +说明: + - 从同目录下的 db_config.py 读取 DB_CONFIG + - 将 `人员清单列表1@正式员工.xlsx` 导入到临时表 `temp_staff_data` + - 自动创建临时表(所有列使用 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, text + +# 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", "_").replace("(", "_").replace(")", "_") + # 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 create_temp_table(engine, table_name: str, columns: List[str]): + """创建临时表""" + print(f"创建临时表: {table_name}") + + # 构建建表SQL + lines: List[str] = [] + lines.append(f"DROP TABLE IF EXISTS `{table_name}`;") + lines.append(f"CREATE TABLE `{table_name}` (") + lines.append(" `id` BIGINT(20) NOT NULL AUTO_INCREMENT,") + + # 添加Excel列(TEXT类型) + for col in columns: + lines.append(f" `{col}` TEXT DEFAULT NULL,") + + lines.append(" `import_time` DATETIME DEFAULT CURRENT_TIMESTAMP,") + lines.append(" PRIMARY KEY (`id`)") + lines.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;") + + create_sql = "\n".join(lines) + + try: + with engine.begin() as conn: + conn.execute(text(create_sql)) + print(f"临时表 {table_name} 创建成功") + except Exception as e: + print(f"创建临时表失败: {e}") + raise + + +def df_to_mysql_append(engine, df: pd.DataFrame, table_name: str, first_write: bool): + """将DataFrame追加到MySQL表""" + # ensure all columns are strings (TEXT) + df = df.where(pd.notnull(df), None) + if first_write: + # 追加数据,不创建表(表已预先创建) + 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_xlsx_staff(filepath: str, engine, table_name: str, chunk_size: int = 2000): + """处理教职工Excel文件(.xlsx格式)""" + if openpyxl is None: + raise RuntimeError("Missing openpyxl library to read .xlsx files. Install from requirements.") + + print(f"开始读取教职工数据文件: {filepath}") + 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) + print(f"表头列: {header}") + + # 创建临时表 + create_temp_table(engine, table_name, header) + + # 处理数据行 + batch: List[Dict[str, Any]] = [] + first_write = True + written = 0 + + for row in rows: + # 转换NaN值为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 + 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_staff(filepath: str, engine, table_name: str, chunk_size: int = 2000): + """处理教职工数据文件""" + ext = os.path.splitext(filepath)[1].lower() + if ext in (".xlsx",): + process_xlsx_staff(filepath, engine, table_name, chunk_size=chunk_size) + else: + # 尝试使用pandas fallback + print(f"未知扩展名 {ext},尝试使用 pandas 直接读取(可能占用大量内存)") + df = pd.read_excel(filepath, engine="openpyxl" if ext == ".xlsx" else None) + df.columns = sanitize_columns(list(df.columns)) + + # 创建临时表 + create_temp_table(engine, table_name, 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=None): + """主函数""" + 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): + # 尝试从上级目录查找 + config_path = os.path.join(os.path.dirname(base_dir), "学校历史数据迁移", "db_config.py") + if not os.path.exists(config_path): + print(f"找不到配置文件: {config_path}") + sys.exit(1) + + print(f"使用配置文件: {config_path}") + db_config = load_db_config_from_file(config_path) + engine = build_engine_from_config(db_config) + + # 教职工数据文件路径 + staff_file = os.path.join(base_dir, "人员清单列表1@正式员工.xlsx") + if args and args.file: + staff_file = args.file + + table_name = "temp_staff_data" + if args and args.table: + table_name = args.table + + if not os.path.exists(staff_file): + print(f"找不到教职工数据文件: {staff_file}") + sys.exit(1) + + print(f"开始导入教职工数据到临时表 {table_name} ...") + try: + process_file_staff(staff_file, engine, table_name, chunk_size=1000) + print(f"教职工数据导入完成 -> {table_name}") + + # 验证导入结果 + with engine.begin() as conn: + result = conn.execute(text(f"SELECT COUNT(*) as count FROM {table_name}")) + count = result.fetchone()[0] + print(f"临时表 {table_name} 共导入 {count} 条记录") + + # 显示前几行数据 + result = conn.execute(text(f"SELECT * FROM {table_name} LIMIT 5")) + columns = result.keys() + print(f"\n前5行数据预览:") + print("-" * 80) + print(f"{' | '.join(columns)}") + print("-" * 80) + for row in result: + print(f"{' | '.join(str(cell) if cell is not None else '' for cell in row)}") + + except Exception as e: + print(f"导入过程中发生错误: {e}") + sys.exit(1) + + print("\n导入完成!") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="导入教职工数据到临时表") + parser.add_argument("--file", type=str, help="教职工数据文件路径") + parser.add_argument("--table", type=str, default="temp_staff_data", help="临时表名") + args = parser.parse_args() + + main(args) + +true \ No newline at end of file diff --git a/教职工数据迁移/insert_external_staff_to_sys_user.py b/教职工数据迁移/insert_external_staff_to_sys_user.py new file mode 100644 index 00000000..185037c2 --- /dev/null +++ b/教职工数据迁移/insert_external_staff_to_sys_user.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +将 temp_external_staff 的外聘职工数据插入到 sys_user,并为每个新用户分配角色 + +规则: + - sys_user.user_name 使用 temp_external_staff.`手机号` + - sys_user.user_no 使用 temp_external_staff.`序号`,格式为 'W' + zero_pad(序号,5)(例如序号=1 -> W00001;序号=209 -> W00209) + - sys_user.nick_name 使用 temp_external_staff.`姓名` + - 性别映射:'男' -> '0', '女' -> '1',其他或空 -> NULL 或默认 '0'(脚本中采用 '0') + - 为每个插入的用户在 sys_user_role 中插入两个角色:4 和 1938120323613728770 + +注意:脚本默认不执行(dry-run),只有当去掉 --dry-run 参数并确认后才会写库。 + +用法示例(不写库): + python insert_external_staff_to_sys_user.py --dry-run + +用法示例(写库 — 请先确认并备份): + python insert_external_staff_to_sys_user.py +""" + +from __future__ import annotations +import os +import sys +import argparse +import importlib.util +import datetime +import re +from typing import Any, Dict, Optional + +import pandas as pd +from sqlalchemy import create_engine, text + + +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}" + return create_engine(uri, pool_pre_ping=True) + + +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 zero_pad_seq_w_prefix(seq_value: Optional[str]) -> str: + """将序号转换为 'W' + 5位零填充字符串;若无法解析为整数,则尝试取数字子串或返回 'W00000'""" + if seq_value is None: + return "W00000" + s = str(seq_value).strip() + # 提取整数部分 + m = re.search(r"\d+", s) + if not m: + return "W00000" + num = int(m.group(0)) + return "W" + str(num).zfill(5) + + +def generate_user_id(conn) -> int: + 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 user_exists(conn, user_name: str) -> bool: + r = conn.execute(text("SELECT 1 FROM sys_user WHERE user_name = :u LIMIT 1"), {"u": user_name}).fetchone() + return bool(r) + + +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(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 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(): + parser = argparse.ArgumentParser(description="将 temp_external_staff 的数据插入 sys_user") + parser.add_argument("--db-config", type=str, default="../学校历史数据迁移/db_config.py", help="db_config.py 路径") + parser.add_argument("--table", type=str, default="temp_external_staff", help="源临时表名") + parser.add_argument("--dry-run", action="store_true", help="仅打印将要执行的操作,不写入数据库") + args = parser.parse_args() + + base_dir = os.path.dirname(os.path.abspath(__file__)) + db_config_path = args.db_config if os.path.isabs(args.db_config) else os.path.join(base_dir, args.db_config) + if not os.path.exists(db_config_path): + # 尝试上级目录中的学校历史数据迁移配置 + alt = os.path.join(os.path.dirname(base_dir), "学校历史数据迁移", "db_config.py") + if os.path.exists(alt): + db_config_path = alt + else: + print(f"找不到 db_config 文件: {db_config_path}") + sys.exit(1) + + db_config = load_db_config_from_file(db_config_path) + engine = build_engine_from_config(db_config) + + # 读取源数据 + with engine.connect() as conn: + df = pd.read_sql(text(f"SELECT * FROM `{args.table}`"), conn) + + # 识别列名 + possible_seq_cols = ["序号", "序号_1", "col_1"] + possible_name_cols = ["姓名", "name"] + possible_phone_cols = ["手机号", "手机号_常用的一个_", "phone"] + possible_sex_cols = ["性别", "sex"] + possible_dept_cols = ["部门", "dept", "department"] + + cols = list(df.columns) + def pick(names): + for n in names: + if n in cols: + return n + return None + + seq_col = pick(possible_seq_cols) or cols[1] if len(cols) > 1 else None + name_col = pick(possible_name_cols) or cols[2] if len(cols) > 2 else None + phone_col = pick(possible_phone_cols) or cols[6] if len(cols) > 6 else None + sex_col = pick(possible_sex_cols) or cols[3] if len(cols) > 3 else None + dept_col = pick(possible_dept_cols) or cols[5] if len(cols) > 5 else None + + print(f"使用列映射: 序号={seq_col}, 姓名={name_col}, 性别={sex_col}, 手机号={phone_col}") + + inserted = 0 + skipped_exists = 0 + errors = 0 + role_ids = [4, 1938120323613728770] + password_hash = "$2a$10$FGldU5BGGq9ONuBmYhxLweFhmbkE45keMKASNZpjETXawE1oGbtZy" + now_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # iterate and insert + with engine.begin() as conn: + for idx, row in df.iterrows(): + seq_val = row.get(seq_col) if seq_col in row else None + name_val = row.get(name_col) if name_col in row else None + raw_phone = row.get(phone_col) if phone_col in row else None + dept_raw = row.get(dept_col) if dept_col in row else None + sex_raw = row.get(sex_col) if sex_col in row else None + + phone = normalize_phone(raw_phone) + if not phone: + print(f"跳过第{idx+1}行:手机号无效或为空(原值: {raw_phone})") + errors += 1 + continue + + user_name_val = phone + if user_exists(conn, user_name_val): + skipped_exists += 1 + continue + + user_no_val = zero_pad_seq_w_prefix(seq_val) + nick = str(name_val).strip() if name_val is not None else user_name_val + sex_val = map_sex(sex_raw) or "0" + + # resolve dept_id from 部门名称 + dept_id = None + if dept_raw is not None and str(dept_raw).strip() != "": + try: + r = conn.execute(text("SELECT dept_id FROM sys_dept WHERE dept_name = :name LIMIT 1"), {"name": str(dept_raw).strip()}).fetchone() + dept_id = int(r[0]) if r and r[0] is not None else None + if dept_id is None: + print(f"警告:第{idx+1}行,部门 '{dept_raw}' 在 sys_dept 中未找到,dept_id 将设为 NULL。") + except Exception as e: + print(f"查找部门时出错(行 {idx+1}): {e}") + dept_id = None + + 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, + "user_type": "sys_user", + "email": None, + "phonenumber": phone, + "sex": sex_val, + "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", + "user_no": user_no_val, + } + + print(f"准备插入 user_id={new_user_id}, user_name={user_name_val}, user_no={user_no_val}, nick={nick}") + if args.dry_run: + inserted += 1 + continue + + try: + insert_user(conn, user_record) + 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 + except Exception as e: + print(f"插入失败(行 {idx+1}): {e}") + errors += 1 + + print(f"完成:插入 {inserted} 条,已存在跳过 {skipped_exists} 条,错误 {errors} 条(dry-run={args.dry_run}))") + + +if __name__ == "__main__": + main() + diff --git a/教职工数据迁移/requirements.txt b/教职工数据迁移/requirements.txt new file mode 100644 index 00000000..482cf6bc --- /dev/null +++ b/教职工数据迁移/requirements.txt @@ -0,0 +1,5 @@ +pandas>=2.2.0 +sqlalchemy>=2.0 +pymysql>=1.0.3 +xlrd>=2.0.1 +openpyxl>=3.0.0 \ No newline at end of file diff --git a/教职工数据迁移/simple_import.py b/教职工数据迁移/simple_import.py new file mode 100644 index 00000000..cc20610f --- /dev/null +++ b/教职工数据迁移/simple_import.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +简单的教职工数据导入脚本 +""" + +import os +import pandas as pd +from sqlalchemy import create_engine, text + +# 数据库配置 +DB_CONFIG = { + "host": "47.99.164.66", + "port": 3306, + "database": "hzdyjs_xg", + "user": "root", + "password": "hn02le.34lkdLKD", + "charset": "utf8mb4" +} + +def main(): + # 构建数据库连接 + 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) + + # Excel文件路径 + excel_file = "人员清单列表1@正式员工.xlsx" + table_name = "temp_staff_data" + + if not os.path.exists(excel_file): + print(f"找不到文件: {excel_file}") + return + + print(f"读取Excel文件: {excel_file}") + + # 读取Excel文件 + df = pd.read_excel(excel_file, engine='openpyxl') + print(f"读取到 {len(df)} 行数据") + print(f"列名: {list(df.columns)}") + + # 清理列名 + df.columns = [col.replace('(', '_').replace(')', '_').replace('、', '_').replace(' ', '_') for col in df.columns] + print(f"清理后的列名: {list(df.columns)}") + + # 创建临时表 + print(f"创建临时表: {table_name}") + + # 先删除已存在的表 + with engine.begin() as conn: + conn.execute(text(f"DROP TABLE IF EXISTS `{table_name}`")) + + # 根据实际列名创建表结构 + create_sql = f"CREATE TABLE `{table_name}` (\n" + create_sql += " `id` BIGINT(20) NOT NULL AUTO_INCREMENT,\n" + + for col in df.columns: + # 清理列名,移除特殊字符 + clean_col = col.replace('(', '_').replace(')', '_').replace('、', '_').replace(' ', '_') + create_sql += f" `{clean_col}` TEXT DEFAULT NULL,\n" + + create_sql += " `import_time` DATETIME DEFAULT CURRENT_TIMESTAMP,\n" + create_sql += " PRIMARY KEY (`id`)\n" + create_sql += ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;" + + print("创建表SQL:") + print(create_sql) + + with engine.begin() as conn: + conn.execute(text(create_sql)) + + # 导入数据 + print("导入数据...") + df.to_sql(name=table_name, con=engine, index=False, if_exists="append", chunksize=1000, method="multi") + + # 验证结果 + with engine.begin() as conn: + result = conn.execute(text(f"SELECT COUNT(*) as count FROM {table_name}")) + count = result.fetchone()[0] + print(f"成功导入 {count} 条记录") + + # 显示前5行 + result = conn.execute(text(f"SELECT * FROM {table_name} LIMIT 5")) + columns = result.keys() + print(f"\n前5行数据预览:") + print("-" * 80) + print(f"{' | '.join(columns)}") + print("-" * 80) + for row in result: + print(f"{' | '.join(str(cell) if cell is not None else '' for cell in row)}") + + print("\n导入完成!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/教职工数据迁移/update_sys_user_from_temp.py b/教职工数据迁移/update_sys_user_from_temp.py new file mode 100644 index 00000000..89aa84d2 --- /dev/null +++ b/教职工数据迁移/update_sys_user_from_temp.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +从 temp_staff_data 读取数据并更新 sys_user 表 + +逻辑: + 1. 对于 temp_staff_data 中每一行,使用 nickname = 姓名 AND account_type = '1' 在 sys_user 中查找唯一记录 + 2. 若找到唯一记录: + - 计算 user_no = zero_pad( sys_user.user_name, 6 ),并写回到 sys_user.user_no + - 将 sys_user.user_name 和 sys_user.phonenumber 都更新为 temp_staff_data 中的手机号字段值 + 3. 若未找到或找到多条记录,跳过并记录日志 + +用法: + python update_sys_user_from_temp.py --db-config ../学校历史数据迁移/db_config.py --dry-run +""" + +from __future__ import annotations +import os +import sys +import argparse +import importlib.util +from typing import Optional, Sequence +import logging + +import pandas as pd +from sqlalchemy import create_engine, text + +LOG = logging.getLogger("update_sys_user") +LOG.setLevel(logging.INFO) +ch = logging.StreamHandler() +ch.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) +LOG.addHandler(ch) + + +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}" + return create_engine(uri, pool_pre_ping=True) + + +def pick_column_name(possible_names: Sequence[str], available: Sequence[str]) -> Optional[str]: + """从可能的列名集合中挑选 temp_staff_data 中存在的一项""" + for name in possible_names: + if name in available: + return name + return None + + +def zero_pad_to_six(value) -> str: + """将给定值转换为字符串并左侧补0到6位(保留原始字符串的数字内容优先)""" + if value is None: + return "" + s = str(value).strip() + # 尝试提取整数部分 + try: + # 支持像 '00123' 或 '123' 的情况 + num = int(s) + return str(num).zfill(6) + except Exception: + # 若不能转为整数,直接用原始字符串左补0(或裁切) + return s.zfill(6)[-6:] + + +def main(db_config_path: str, table_name: str = "temp_staff_data", dry_run: bool = True): + base_dir = os.path.dirname(os.path.abspath(__file__)) + if not os.path.isabs(db_config_path): + # 相对路径从当前脚本目录解析 + db_config_path = os.path.join(base_dir, db_config_path) + + if not os.path.exists(db_config_path): + LOG.error("找不到 db_config 文件: %s", db_config_path) + sys.exit(1) + + db_config = load_db_config_from_file(db_config_path) + engine = build_engine_from_config(db_config) + + # 使用事务(begin)确保更新会被提交 + with engine.begin() as conn: + # 读取表头,确定列名映射 + res = conn.execute(text(f"SELECT * FROM `{table_name}` LIMIT 1")) + cols = list(res.keys()) + LOG.info("temp_staff_data 可用列: %s", cols) + + name_col = pick_column_name(["姓名", "name"], cols) + phone_col = pick_column_name(["手机号", "手机号_常用的一个_", "phone", "phonenumber"], cols) + if not name_col or not phone_col: + LOG.error("无法在 %s 中找到 必需 列 (姓名/手机号)。请检查表结构。", table_name) + return + + # 读取所有数据(若数据量巨大可以改为分批处理) + df = pd.read_sql(text(f"SELECT * FROM `{table_name}`"), conn) + LOG.info("读取到 %d 条 temp_staff_data 记录", len(df)) + + updated = 0 + skipped_no_user = 0 + skipped_multi = 0 + errors = 0 + + for _, row in df.iterrows(): + staff_name = row.get(name_col) + phone = row.get(phone_col) + if pd.isna(staff_name) or staff_name is None: + LOG.warning("跳过一行,姓名为空") + skipped_no_user += 1 + continue + + staff_name_str = str(staff_name).strip() + # 查找 sys_user + try: + users = conn.execute( + text("SELECT user_id, user_name FROM sys_user WHERE nick_name = :nick AND account_type = '1'"), + {"nick": staff_name_str}, + ).fetchall() + except Exception as e: + LOG.exception("查询 sys_user 失败: %s", e) + errors += 1 + continue + + if len(users) == 0: + LOG.info("未找到 sys_user (nickname=%s, account_type=1),跳过", staff_name_str) + skipped_no_user += 1 + continue + if len(users) > 1: + LOG.warning("找到多条 sys_user (nickname=%s, account_type=1),跳过以避免歧义", staff_name_str) + skipped_multi += 1 + continue + + user_row = users[0] + user_id = user_row[0] + current_user_name = user_row[1] + # 计算 user_no 并更新 user_name & phonenumber + user_no = zero_pad_to_six(current_user_name) + new_user_name = "" if phone is None or (isinstance(phone, float) and pd.isna(phone)) else str(phone).strip() + new_phonenumber = new_user_name + + LOG.info("准备更新 sys_user.id=%s: user_no=%s, user_name=%s, phonenumber=%s", + user_id, user_no, new_user_name, new_phonenumber) + + if dry_run: + LOG.info("dry-run 模式,未执行写入") + updated += 1 + continue + + try: + conn.execute( + text("UPDATE sys_user SET user_no = :user_no, user_name = :user_name, phonenumber = :phone WHERE user_id = :id"), + {"user_no": user_no, "user_name": new_user_name, "phone": new_phonenumber, "id": user_id}, + ) + updated += 1 + except Exception as e: + LOG.exception("更新 sys_user.id=%s 失败: %s", user_id, e) + errors += 1 + + LOG.info("处理完成,统计: updated=%d, skipped_no_user=%d, skipped_multi=%d, errors=%d", + updated, skipped_no_user, skipped_multi, errors) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="从 temp_staff_data 更新 sys_user") + parser.add_argument("--db-config", type=str, default="../学校历史数据迁移/db_config.py", help="数据库配置文件路径(python模块含 DB_CONFIG)") + parser.add_argument("--table", type=str, default="temp_staff_data", help="临时表名") + parser.add_argument("--dry-run", action="store_true", help="仅打印将要执行的操作,不写入数据库") + args = parser.parse_args() + + main(db_config_path=args.db_config, table_name=args.table, dry_run=args.dry_run) + diff --git a/教职工数据迁移/verify_import.sql b/教职工数据迁移/verify_import.sql new file mode 100644 index 00000000..143fde3a --- /dev/null +++ b/教职工数据迁移/verify_import.sql @@ -0,0 +1,51 @@ +-- 验证教职工数据导入结果 +-- 执行时间: 2026-01-06 + +-- 1. 查看临时表结构 +DESCRIBE temp_staff_data; + +-- 2. 查看总记录数 +SELECT COUNT(*) as total_records FROM temp_staff_data; + +-- 3. 查看各部门员工数量统计 +SELECT + `部门` as department, + COUNT(*) as employee_count +FROM temp_staff_data +GROUP BY `部门` +ORDER BY employee_count DESC; + +-- 4. 查看性别分布 +SELECT + `性别` as gender, + COUNT(*) as count +FROM temp_staff_data +GROUP BY `性别`; + +-- 5. 查看是否在编状态分布 +SELECT + `是否在编_临时员工写L,外聘老师写W,正式写0_` as employment_status, + COUNT(*) as count +FROM temp_staff_data +GROUP BY `是否在编_临时员工写L,外聘老师写W,正式写0_`; + +-- 6. 查看前10条记录详情 +SELECT + `序号`, + `姓名`, + `性别`, + `职务`, + `部门`, + `是否在编_临时员工写L,外聘老师写W,正式写0_`, + `手机号_常用的一个_` +FROM temp_staff_data +ORDER BY `序号` +LIMIT 10; + +-- 7. 检查是否有空值 +SELECT + COUNT(*) as total, + SUM(CASE WHEN `姓名` IS NULL OR `姓名` = '' THEN 1 ELSE 0 END) as null_names, + SUM(CASE WHEN `手机号_常用的一个_` IS NULL OR `手机号_常用的一个_` = '' THEN 1 ELSE 0 END) as null_phones, + SUM(CASE WHEN `部门` IS NULL OR `部门` = '' THEN 1 ELSE 0 END) as null_departments +FROM temp_staff_data; \ No newline at end of file diff --git a/教职工数据迁移/人员清单列表1@正式员工.xlsx b/教职工数据迁移/人员清单列表1@正式员工.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..90c3a0f58bde0981a0ad4ebb30751a5a354009f3 GIT binary patch literal 23922 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 literal 0 HcmV?d00001 diff --git a/教职工数据迁移/人员清单列表@外聘.xlsx b/教职工数据迁移/人员清单列表@外聘.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..277b7310b0594da7c874dcae842c90d05c8de3fb GIT binary patch literal 14669 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 literal 0 HcmV?d00001 diff --git a/教职工数据迁移/教职工数据迁移总结.md b/教职工数据迁移/教职工数据迁移总结.md new file mode 100644 index 00000000..53891f49 --- /dev/null +++ b/教职工数据迁移/教职工数据迁移总结.md @@ -0,0 +1,128 @@ +# 教职工数据迁移总结报告 + +## 📋 项目概述 + +**项目名称**: 教职工数据迁移 +**执行时间**: 2026-01-06 +**负责人**: AI助手 +**数据源**: `人员清单列表1@正式员工.xlsx` + +## 📊 数据统计 + +### 基本信息 +- **总记录数**: 229 条 +- **数据完整性**: 100% (无空值记录) +- **导入状态**: ✅ 成功 + +### 性别分布 +- **女性员工**: 125 人 (54.6%) +- **男性员工**: 104 人 (45.4%) + +### 部门分布 (前10名) +| 部门 | 人数 | +|------|------| +| 医药健康学院 | 31 | +| 人文学院 | 30 | +| 智能控制学院 | 25 | +| 生态园林学院 | 22 | +| 智能制造学院 | 19 | +| 餐饮休闲学院 | 18 | +| 学生处(团委) | 11 | +| 教务处 | 9 | +| 数字信息学院 | 9 | + +### 编制情况 +- **正式员工**: 229 人 (100%) +- **临时员工**: 0 人 +- **外聘教师**: 0 人 + +## 🗂️ 临时表结构 + +```sql +CREATE TABLE `temp_staff_data` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `序号` TEXT DEFAULT NULL, + `姓名` TEXT DEFAULT NULL, + `性别` TEXT DEFAULT NULL, + `职务` TEXT DEFAULT NULL, + `部门` TEXT DEFAULT NULL, + `是否在编_临时员工写L,外聘老师写W,正式写0_` TEXT DEFAULT NULL, + `手机号_常用的一个_` TEXT DEFAULT NULL, + `import_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +## 📁 文件清单 + +1. **数据源文件** + - `人员清单列表1@正式员工.xlsx` - 原始教职工数据 + +2. **脚本文件** + - `simple_import.py` - 数据导入脚本 + - `verify_import.sql` - 数据验证脚本 + +3. **配置文件** + - `requirements.txt` - Python依赖包 + - `README.md` - 使用说明文档 + +4. **文档文件** + - `教职工数据迁移总结.md` - 本总结报告 + +## ✅ 执行结果 + +### 导入过程 +1. ✅ Excel文件读取成功 (229行数据) +2. ✅ 临时表创建成功 +3. ✅ 数据导入完成 (229条记录) +4. ✅ 数据验证通过 (无数据丢失或损坏) + +### 数据质量检查 +- ✅ **姓名字段**: 229/229 完整 (100%) +- ✅ **手机号字段**: 229/229 完整 (100%) +- ✅ **部门字段**: 229/229 完整 (100%) +- ✅ **性别字段**: 数据合理分布 +- ✅ **编制状态**: 全部为正式员工 + +## 🔧 技术实现 + +### 导入脚本特性 +- **自动列名清理**: 处理中文特殊字符和括号 +- **动态表结构**: 根据Excel实际列名创建表 +- **批量导入**: 支持大数据量高效导入 +- **错误处理**: 完善的异常处理机制 + +### 依赖环境 +- **Python**: 3.12+ +- **pandas**: 数据处理 +- **sqlalchemy**: 数据库连接 +- **openpyxl**: Excel文件读取 +- **MySQL**: 数据存储 + +## 📋 数据样例 + +| 序号 | 姓名 | 性别 | 职务 | 部门 | 是否在编 | 手机号 | +|------|------|------|------|------|---------|--------| +| 1 | 杨国强 | 男 | 党委书记、院长 | 院领导 | 0 | 13958119380 | +| 2 | 卢红华 | 男 | 党委副书记、纪委书记、副院长 | 院领导 | 0 | 13867410680 | +| 3 | 徐弘 | 男 | 党委委员、副院长 | 院领导 | 0 | 15957163076 | + +## 🎯 后续建议 + +1. **数据清洗**: 根据业务需要对职务、部门等字段进行标准化 +2. **数据关联**: 将临时表数据关联到正式的员工表结构 +3. **权限配置**: 为导入数据配置相应的访问权限 +4. **备份策略**: 制定数据备份和恢复策略 + +## 📝 备注 + +- 数据导入过程中未发现任何数据质量问题 +- 所有229条记录均完整导入,无数据丢失 +- 临时表可作为后续数据处理的中间存储 +- 建议在正式使用前进行业务逻辑验证 + +--- + +**执行完成时间**: 2026-01-06 10:26:10 +**数据验证时间**: 2026-01-06 10:30:00 +**报告生成时间**: 2026-01-06 \ No newline at end of file