04-02-reorder_dept_order_num.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 04-02-reorder_dept_order_num.py
  5. 目的:
  6. 将 `sys_dept` 表中,同一个学院(parent_id)下的专业记录(可通过 dept_category 判断)按照 `dept_name` 升序重新设置 `order_num`(从 1 开始递增)。
  7. 行为:
  8. - 默认 dry-run:打印将要执行的 UPDATE 语句,不做修改。
  9. - 传入 --apply 会在事务中执行更新。
  10. - 支持通过 --major-only 控制是否仅对 dept_category LIKE 'zy-%' 的记录进行排序(默认 True)。
  11. - 支持 --limit 控制处理的 parent_id 数量(用于测试)。
  12. 使用:
  13. python3 04-02-reorder_dept_order_num.py --host ... --user ... --database ... [--apply]
  14. """
  15. from __future__ import annotations
  16. import argparse
  17. import getpass
  18. import importlib.util
  19. import os
  20. import sys
  21. import logging
  22. from typing import List, Tuple, Dict
  23. import pymysql
  24. from pymysql.connections import Connection
  25. logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
  26. logger = logging.getLogger("reorder_dept_order_num")
  27. def load_local_db_config() -> dict:
  28. cfg_path = os.path.join(os.path.dirname(__file__), "db_config.py")
  29. if not os.path.exists(cfg_path):
  30. return {}
  31. try:
  32. spec = importlib.util.spec_from_file_location("local_db_config", cfg_path)
  33. module = importlib.util.module_from_spec(spec)
  34. spec.loader.exec_module(module) # type: ignore
  35. return getattr(module, "DB_CONFIG", {}) or {}
  36. except Exception as e:
  37. logger.warning("加载本地 db_config.py 失败: %s", e)
  38. return {}
  39. def parse_args():
  40. local_db = load_local_db_config()
  41. host_default = local_db.get("host", os.getenv("DB_HOST", "localhost"))
  42. port_default = int(local_db.get("port", os.getenv("DB_PORT", 3306)))
  43. user_default = local_db.get("user", os.getenv("DB_USER", "root"))
  44. password_default = local_db.get("password", os.getenv("DB_PASSWORD", ""))
  45. database_default = local_db.get("database", os.getenv("DB_NAME", "hzdyjs_xg"))
  46. p = argparse.ArgumentParser(description="Reorder sys_dept.order_num by dept_name under the same parent_id")
  47. p.add_argument("--host", default=host_default)
  48. p.add_argument("--port", type=int, default=port_default)
  49. p.add_argument("--user", default=user_default)
  50. p.add_argument("--password", default=password_default)
  51. p.add_argument("--database", default=database_default)
  52. p.add_argument("--charset", default="utf8mb4")
  53. p.add_argument("--apply", action="store_true", help="Apply changes (default dry-run)")
  54. p.add_argument("--limit", type=int, default=0, help="Limit number of parent groups to process (for testing)")
  55. p.add_argument("--major-only", action="store_true", default=True, help="Only reorder records where dept_category LIKE 'zy-%' (default True)")
  56. return p.parse_args()
  57. def get_db_conn(args) -> Connection:
  58. pwd = args.password or getpass.getpass("DB password: ")
  59. return pymysql.connect(
  60. host=args.host,
  61. port=args.port,
  62. user=args.user,
  63. password=pwd,
  64. database=args.database,
  65. charset=args.charset,
  66. cursorclass=pymysql.cursors.DictCursor,
  67. autocommit=False,
  68. )
  69. def find_parent_ids(conn: Connection, major_only: bool = True, limit: int = 0) -> List[int]:
  70. """
  71. Find parent_id values that have child departments to reorder.
  72. If major_only is True, only consider children with dept_category LIKE 'zy-%'.
  73. """
  74. if major_only:
  75. sql = """
  76. SELECT parent_id
  77. FROM sys_dept
  78. WHERE dept_category LIKE 'zy-%%' AND del_flag = '0'
  79. GROUP BY parent_id
  80. ORDER BY parent_id
  81. """
  82. else:
  83. sql = """
  84. SELECT parent_id
  85. FROM sys_dept
  86. WHERE parent_id IS NOT NULL AND del_flag = '0'
  87. GROUP BY parent_id
  88. ORDER BY parent_id
  89. """
  90. if limit and limit > 0:
  91. sql = sql + f" LIMIT {limit}"
  92. with conn.cursor() as cur:
  93. cur.execute(sql)
  94. rows = cur.fetchall()
  95. return [r["parent_id"] for r in rows if r["parent_id"] is not None]
  96. def fetch_children(conn: Connection, parent_id: int, major_only: bool = True) -> List[Dict]:
  97. """
  98. Fetch child departments for a parent_id, ordered by dept_name asc then dept_id asc.
  99. """
  100. if major_only:
  101. sql = """
  102. SELECT dept_id, dept_name, order_num
  103. FROM sys_dept
  104. WHERE parent_id = %s AND dept_category LIKE 'zy-%%' AND del_flag = '0'
  105. ORDER BY dept_name ASC, dept_id ASC
  106. """
  107. else:
  108. sql = """
  109. SELECT dept_id, dept_name, order_num
  110. FROM sys_dept
  111. WHERE parent_id = %s AND del_flag = '0'
  112. ORDER BY dept_name ASC, dept_id ASC
  113. """
  114. with conn.cursor() as cur:
  115. cur.execute(sql, (parent_id,))
  116. return cur.fetchall()
  117. def apply_updates(conn: Connection, updates: List[Tuple[int, int]], dry_run: bool):
  118. """
  119. updates: list of (dept_id, new_order_num)
  120. """
  121. if not updates:
  122. return
  123. logger.info(" %d updates to apply", len(updates))
  124. for dept_id, new_order in updates:
  125. sql = "UPDATE sys_dept SET order_num = %s WHERE dept_id = %s"
  126. logger.info(" %s -- params: (%s, %s)", sql, new_order, dept_id)
  127. if not dry_run:
  128. with conn.cursor() as cur:
  129. cur.execute(sql, (new_order, dept_id))
  130. def process(conn: Connection, parent_ids: List[int], major_only: bool, apply: bool):
  131. dry_run = not apply
  132. summary = {"parents": 0, "updates": 0}
  133. for parent_id in parent_ids:
  134. children = fetch_children(conn, parent_id, major_only=major_only)
  135. if not children:
  136. logger.debug("Parent %s has no children to reorder", parent_id)
  137. continue
  138. updates = []
  139. for idx, child in enumerate(children, start=1):
  140. dept_id = child["dept_id"]
  141. current_order = child.get("order_num") or 0
  142. if current_order != idx:
  143. updates.append((dept_id, idx))
  144. if updates:
  145. logger.info("Parent %s: will update %d rows", parent_id, len(updates))
  146. apply_updates(conn, updates, dry_run=dry_run)
  147. summary["updates"] += len(updates)
  148. else:
  149. logger.info("Parent %s: no updates needed", parent_id)
  150. summary["parents"] += 1
  151. return summary
  152. def main():
  153. args = parse_args()
  154. conn = get_db_conn(args)
  155. try:
  156. parent_ids = find_parent_ids(conn, major_only=args.major_only, limit=args.limit)
  157. if not parent_ids:
  158. logger.info("No parent groups found to process.")
  159. return 0
  160. logger.info("Found %d parent groups to process (limit=%s, major_only=%s).", len(parent_ids), args.limit, args.major_only)
  161. # show parents
  162. for pid in parent_ids:
  163. logger.info(" parent_id=%s", pid)
  164. if args.apply:
  165. logger.info("Applying updates inside a transaction...")
  166. try:
  167. summary = process(conn, parent_ids, args.major_only, apply=True)
  168. conn.commit()
  169. logger.info("Committed. Processed %d parents, total updates %d", summary["parents"], summary["updates"])
  170. except Exception as e:
  171. conn.rollback()
  172. logger.exception("Error during apply, rolled back. Error: %s", e)
  173. return 2
  174. else:
  175. logger.info("Dry-run mode: showing planned updates (no DB changes).")
  176. summary = process(conn, parent_ids, args.major_only, apply=False)
  177. logger.info("Dry-run finished. Processed %d parents, planned updates %d. Re-run with --apply to execute.", summary["parents"], summary["updates"])
  178. return 0
  179. finally:
  180. conn.close()
  181. if __name__ == "__main__":
  182. sys.exit(main())