326 lines
13 KiB
Python
326 lines
13 KiB
Python
#!/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()
|
||
|
||
|