| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- #!/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()
|