Files
2025-12-23 22:25:02 +08:00

326 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()