提交
This commit is contained in:
1 parent
68b8feffc1
commit
fca7096e9f
30 files changed
+4049
-8
No files matched your search
@@ -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()
|
||||
|
||||
|
||||
Reference in new issue
Block a user