generate_dict_reimport_sql.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. #!/usr/bin/env python3
  2. """Merge zkxw-dict.sql + sx-dict.sql into shanxia2.0 sys_dict_* INSERT script."""
  3. from __future__ import annotations
  4. import re
  5. from pathlib import Path
  6. ROOT = Path(__file__).resolve().parents[2]
  7. ZKXW_SQL = ROOT / "docs/sql/zkxw-dict.sql"
  8. SX_SQL = ROOT / "docs/sql/sx-dict.sql"
  9. SHANXIA_SQL = ROOT / "docs/sql/shanxia2.0.sql"
  10. OUT_SQL = ROOT / "docs/sql/reimport-dict-from-zkxw-sx.sql"
  11. TENANT = "shanxia2.0"
  12. # sx 源库部分 CRM 字典无时间戳;目标表 create_time/update_time 为 NOT NULL
  13. DEFAULT_DT = "2026-05-30 00:00:00"
  14. # shanxia2.0 门户内置字典(不在 zkxw-dict / sx-dict 中,从基线 SQL 保留)
  15. PORTAL_BASELINE_TYPES = frozenset(
  16. {
  17. "sys_status",
  18. "sys_sex",
  19. "sys_data_scope",
  20. "sys_visible",
  21. "menu_type",
  22. "leave_status",
  23. "wf_process_status",
  24. "wf_node_type",
  25. "wf_approve_type",
  26. "user_role",
  27. }
  28. )
  29. DICT_TYPE_RE = re.compile(r"^[a-z][a-z0-9_]*$")
  30. TAG_TYPE_MAP = {
  31. "default": "",
  32. "primary": "primary",
  33. "success": "success",
  34. "danger": "danger",
  35. "warning": "warning",
  36. "info": "info",
  37. }
  38. def split_sql_values(values_part: str) -> list:
  39. parts: list[str] = []
  40. cur: list[str] = []
  41. in_str = False
  42. i = 0
  43. while i < len(values_part):
  44. c = values_part[i]
  45. if in_str:
  46. if c == "\\" and i + 1 < len(values_part):
  47. cur.append(c)
  48. cur.append(values_part[i + 1])
  49. i += 2
  50. continue
  51. if c == "'":
  52. if i + 1 < len(values_part) and values_part[i + 1] == "'":
  53. cur.append("''")
  54. i += 2
  55. continue
  56. in_str = False
  57. cur.append(c)
  58. else:
  59. cur.append(c)
  60. else:
  61. if c == "'":
  62. in_str = True
  63. cur.append(c)
  64. elif c == ",":
  65. parts.append("".join(cur).strip())
  66. cur = []
  67. else:
  68. cur.append(c)
  69. i += 1
  70. if cur:
  71. parts.append("".join(cur).strip())
  72. return parts
  73. def unquote(val: str):
  74. val = val.strip()
  75. if val.upper() == "NULL":
  76. return None
  77. if val.startswith("'") and val.endswith("'"):
  78. return val[1:-1].replace("''", "'")
  79. if re.fullmatch(r"-?\d+", val):
  80. return int(val)
  81. return val
  82. def parse_insert(line: str) -> tuple[str, list[str], list] | None:
  83. m = re.match(
  84. r"INSERT INTO `(\w+)` \(([^)]+)\) VALUES \((.+)\);\s*$",
  85. line.strip(),
  86. )
  87. if not m:
  88. return None
  89. table = m.group(1)
  90. cols = [c.strip().strip("`") for c in m.group(2).split(",")]
  91. vals = [unquote(v) for v in split_sql_values(m.group(3))]
  92. return table, cols, vals
  93. def sql_literal(val) -> str:
  94. if val is None:
  95. return "NULL"
  96. if isinstance(val, int):
  97. return str(val)
  98. s = str(val).replace("\\", "\\\\").replace("'", "''")
  99. return f"'{s}'"
  100. def is_valid_dict_type(code: str | None) -> bool:
  101. return bool(code) and bool(DICT_TYPE_RE.match(code))
  102. def map_tag_type(list_class: str | None) -> str:
  103. if not list_class:
  104. return ""
  105. key = str(list_class).strip().lower()
  106. return TAG_TYPE_MAP.get(key, key if key in TAG_TYPE_MAP.values() else "")
  107. def sx_type_id(dict_id: int) -> str:
  108. return f"SXDT{dict_id:016X}"
  109. def sx_data_id(dict_code: int) -> str:
  110. return f"SXDC{dict_code:016X}"
  111. def normalize_row(row: dict) -> dict:
  112. out = dict(row)
  113. ct = out.get("create_time")
  114. ut = out.get("update_time")
  115. if not ct:
  116. out["create_time"] = ut or DEFAULT_DT
  117. if not out.get("update_time"):
  118. out["update_time"] = out["create_time"] or DEFAULT_DT
  119. return out
  120. def row_to_insert(table: str, cols: list[str], row: dict) -> str:
  121. row = normalize_row(row)
  122. ordered = [row[c] for c in cols]
  123. vals = ", ".join(sql_literal(v) for v in ordered)
  124. col_sql = ", ".join(f"`{c}`" for c in cols)
  125. return f"INSERT INTO `{table}` ({col_sql}) VALUES ({vals});"
  126. def load_portal_baseline():
  127. types: dict[str, dict] = {}
  128. data: list[dict] = []
  129. for line in SHANXIA_SQL.read_text(encoding="utf-8").splitlines():
  130. parsed = parse_insert(line)
  131. if not parsed:
  132. continue
  133. table, cols, vals = parsed
  134. row = dict(zip(cols, vals))
  135. if table == "sys_dict_type":
  136. code = row.get("dict_type")
  137. if code not in PORTAL_BASELINE_TYPES or row.get("del_flag", 0) != 0:
  138. continue
  139. types[code] = row
  140. elif table == "sys_dict_data":
  141. code = row.get("dict_type")
  142. if code not in PORTAL_BASELINE_TYPES or row.get("del_flag", 0) != 0:
  143. continue
  144. row.setdefault("tag_type", "")
  145. data.append(row)
  146. return types, data
  147. def load_zkxw():
  148. types: dict[str, dict] = {}
  149. data: list[dict] = []
  150. for line in ZKXW_SQL.read_text(encoding="utf-8").splitlines():
  151. parsed = parse_insert(line)
  152. if not parsed:
  153. continue
  154. table, cols, vals = parsed
  155. row = dict(zip(cols, vals))
  156. if row.get("del_flag", 0) != 0:
  157. continue
  158. row["tenant_no"] = TENANT
  159. if table == "sys_dict_type":
  160. code = row.get("dict_type")
  161. if is_valid_dict_type(code):
  162. types[code] = row
  163. elif table == "sys_dict_data":
  164. code = row.get("dict_type")
  165. if not is_valid_dict_type(code):
  166. continue
  167. row["tenant_no"] = TENANT
  168. row.setdefault("tag_type", "")
  169. data.append(row)
  170. return types, data
  171. def load_sx():
  172. types: dict[str, dict] = {}
  173. data: list[dict] = []
  174. for line in SX_SQL.read_text(encoding="utf-8").splitlines():
  175. parsed = parse_insert(line)
  176. if not parsed:
  177. continue
  178. table, cols, vals = parsed
  179. row = dict(zip(cols, vals))
  180. if table == "sys_dict_type":
  181. dict_id = row.get("dict_id")
  182. code = row.get("dict_type")
  183. name = row.get("dict_name")
  184. if not is_valid_dict_type(code):
  185. continue
  186. if str(name) in ("000000", TENANT, "zkxw", "0"):
  187. continue
  188. types[code] = {
  189. "id": sx_type_id(int(dict_id)),
  190. "dict_name": name,
  191. "dict_type": code,
  192. "tenant_no": TENANT,
  193. "order_num": 0,
  194. "remark": row.get("remark") or "",
  195. "create_by": None,
  196. "create_dept_id": "",
  197. "create_time": row.get("create_time"),
  198. "update_by": None,
  199. "update_time": row.get("update_time") or row.get("create_time"),
  200. "del_flag": 0,
  201. }
  202. elif table == "sys_dict_data":
  203. dict_code = row.get("dict_code")
  204. code = row.get("dict_type")
  205. label = row.get("dict_label")
  206. value = row.get("dict_value")
  207. if dict_code is None or not is_valid_dict_type(code):
  208. continue
  209. if not label or not str(label).strip():
  210. continue
  211. if str(label) in ("planned", "zsjy_visit_status") and str(value) in (
  212. "zsjy_visit_status",
  213. "planned",
  214. ):
  215. continue
  216. create_by = row.get("create_by")
  217. data.append(
  218. {
  219. "id": sx_data_id(int(dict_code)),
  220. "dict_label": label,
  221. "dict_value": value,
  222. "tag_type": map_tag_type(row.get("list_class")),
  223. "dict_type": code,
  224. "tenant_no": TENANT,
  225. "order_num": row.get("dict_sort") or 0,
  226. "remark": row.get("remark") or "",
  227. "create_by": str(create_by) if create_by is not None else None,
  228. "create_dept_id": "",
  229. "create_time": row.get("create_time"),
  230. "update_by": None,
  231. "update_time": row.get("update_time") or row.get("create_time"),
  232. "del_flag": 0,
  233. }
  234. )
  235. return types, data
  236. def main():
  237. portal_types, portal_data = load_portal_baseline()
  238. zkxw_types, zkxw_data = load_zkxw()
  239. sx_types, sx_data = load_sx()
  240. merged_types = dict(portal_types)
  241. merged_types.update(zkxw_types)
  242. merged_types.update(sx_types)
  243. type_cols = [
  244. "id",
  245. "dict_name",
  246. "dict_type",
  247. "tenant_no",
  248. "order_num",
  249. "remark",
  250. "create_by",
  251. "create_dept_id",
  252. "create_time",
  253. "update_by",
  254. "update_time",
  255. "del_flag",
  256. ]
  257. data_cols = [
  258. "id",
  259. "dict_label",
  260. "dict_value",
  261. "tag_type",
  262. "dict_type",
  263. "tenant_no",
  264. "order_num",
  265. "remark",
  266. "create_by",
  267. "create_dept_id",
  268. "create_time",
  269. "update_by",
  270. "update_time",
  271. "del_flag",
  272. ]
  273. # portal baseline -> zkxw -> sx
  274. seen_data_ids: set[str] = set()
  275. merged_data: list[dict] = []
  276. for row in portal_data + zkxw_data + sx_data:
  277. rid = row["id"]
  278. if rid in seen_data_ids:
  279. continue
  280. seen_data_ids.add(rid)
  281. if row["dict_type"] not in merged_types:
  282. continue
  283. merged_data.append(row)
  284. lines = [
  285. "-- Reimport dictionary data for shanxia2.0",
  286. "-- Sources: shanxia2.0 portal baseline + zkxw-dict.sql + sx-dict.sql",
  287. "-- Generated by: backend/scripts/generate_dict_reimport_sql.py",
  288. "",
  289. "SET NAMES utf8mb4;",
  290. "SET FOREIGN_KEY_CHECKS = 0;",
  291. "",
  292. "DELETE FROM `sys_dict_data`;",
  293. "DELETE FROM `sys_dict_type`;",
  294. "",
  295. "BEGIN;",
  296. "",
  297. "-- sys_dict_type",
  298. ]
  299. for code in sorted(merged_types.keys()):
  300. lines.append(row_to_insert("sys_dict_type", type_cols, merged_types[code]))
  301. lines.append("")
  302. lines.append("-- sys_dict_data")
  303. for row in merged_data:
  304. lines.append(row_to_insert("sys_dict_data", data_cols, row))
  305. lines.extend(["", "COMMIT;", "", "SET FOREIGN_KEY_CHECKS = 1;", ""])
  306. OUT_SQL.write_text("\n".join(lines), encoding="utf-8")
  307. print(
  308. f"Wrote {OUT_SQL.relative_to(ROOT)}: "
  309. f"{len(merged_types)} types, {len(merged_data)} data rows "
  310. f"(portal={len(portal_types)}, zkxw={len(zkxw_types)}, sx={len(sx_types)})"
  311. )
  312. if __name__ == "__main__":
  313. main()