compare_dict_snapshots.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. #!/usr/bin/env python3
  2. """Compare sys_dict_type / sys_dict_data across SQL snapshots."""
  3. from __future__ import annotations
  4. import re
  5. from collections import defaultdict
  6. from pathlib import Path
  7. ROOT = Path(__file__).resolve().parents[2]
  8. SX_SQL = ROOT / "docs/sql/sx.sql"
  9. PORTAL_SQL = ROOT / "docs/sql/shanxia2.0.sql"
  10. DIBANG_SQL = ROOT / "docs/sql/dibang.sql"
  11. def parse_insert_values(line: str) -> list[str] | None:
  12. m = re.search(r"VALUES\s*\((.*)\)\s*;?\s*$", line, re.I)
  13. if not m:
  14. return None
  15. inner = m.group(1)
  16. parts: list[str] = []
  17. cur: list[str] = []
  18. in_str = False
  19. esc = False
  20. for ch in inner:
  21. if esc:
  22. cur.append(ch)
  23. esc = False
  24. continue
  25. if ch == "\\":
  26. esc = True
  27. cur.append(ch)
  28. continue
  29. if ch == "'":
  30. in_str = not in_str
  31. cur.append(ch)
  32. continue
  33. if ch == "," and not in_str:
  34. parts.append("".join(cur).strip())
  35. cur = []
  36. continue
  37. cur.append(ch)
  38. if cur:
  39. parts.append("".join(cur).strip())
  40. return parts
  41. def unquote(v: str) -> str:
  42. v = v.strip()
  43. if v.upper() == "NULL":
  44. return ""
  45. if v.startswith("'") and v.endswith("'"):
  46. return v[1:-1].replace("''", "'").replace("\\'", "'")
  47. return v
  48. def load_sx_dicts(path: Path) -> tuple[dict[str, dict], dict[str, list[dict]]]:
  49. types: dict[str, dict] = {}
  50. data_by_type: dict[str, list[dict]] = defaultdict(list)
  51. for line in path.read_text(encoding="utf-8", errors="ignore").splitlines():
  52. if "INSERT INTO `sys_dict_type`" in line:
  53. vals = parse_insert_values(line)
  54. if not vals or len(vals) < 4:
  55. continue
  56. # dict_id, tenant_id, dict_name, dict_type, ...
  57. dict_type = unquote(vals[3])
  58. types[dict_type] = {
  59. "dict_name": unquote(vals[2]),
  60. "tenant_id": unquote(vals[1]),
  61. "remark": unquote(vals[9]) if len(vals) > 9 else "",
  62. }
  63. elif "INSERT INTO `sys_dict_data`" in line:
  64. vals = parse_insert_values(line)
  65. if not vals or len(vals) < 6:
  66. continue
  67. # dict_code, tenant_id, dict_sort, dict_label, dict_value, dict_type, ...
  68. dict_type = unquote(vals[5])
  69. data_by_type[dict_type].append(
  70. {
  71. "label": unquote(vals[3]),
  72. "value": unquote(vals[4]),
  73. "sort": unquote(vals[2]),
  74. "css_class": unquote(vals[6]) if len(vals) > 6 else "",
  75. "list_class": unquote(vals[7]) if len(vals) > 7 else "",
  76. }
  77. )
  78. return types, data_by_type
  79. def load_portal_dicts(path: Path) -> tuple[dict[str, dict], dict[str, list[dict]]]:
  80. types: dict[str, dict] = {}
  81. data_by_type: dict[str, list[dict]] = defaultdict(list)
  82. for line in path.read_text(encoding="utf-8", errors="ignore").splitlines():
  83. if "INSERT INTO `sys_dict_type`" in line:
  84. vals = parse_insert_values(line)
  85. if not vals or len(vals) < 3:
  86. continue
  87. # id, dict_name, dict_type, tenant_no, ...
  88. dict_type = unquote(vals[2])
  89. types[dict_type] = {
  90. "dict_name": unquote(vals[1]),
  91. "tenant_no": unquote(vals[3]),
  92. "remark": unquote(vals[5]) if len(vals) > 5 else "",
  93. }
  94. elif "INSERT INTO `sys_dict_data`" in line:
  95. vals = parse_insert_values(line)
  96. if not vals or len(vals) < 5:
  97. continue
  98. # id, dict_label, dict_value, tag_type, dict_type, tenant_no, ...
  99. dict_type = unquote(vals[4])
  100. data_by_type[dict_type].append(
  101. {
  102. "label": unquote(vals[1]),
  103. "value": unquote(vals[2]),
  104. "tag_type": unquote(vals[3]),
  105. "sort": unquote(vals[6]) if len(vals) > 6 else "",
  106. }
  107. )
  108. return types, data_by_type
  109. def item_key(item: dict) -> str:
  110. return f"{item['value']}|{item['label']}"
  111. def compare_data(sx_items: list[dict], portal_items: list[dict]) -> dict:
  112. sx_map = {item_key(i): i for i in sx_items}
  113. portal_map = {item_key(i): i for i in portal_items}
  114. sx_keys = set(sx_map)
  115. portal_keys = set(portal_map)
  116. return {
  117. "only_sx": sorted(sx_keys - portal_keys),
  118. "only_portal": sorted(portal_keys - sx_keys),
  119. "common": sorted(sx_keys & portal_keys),
  120. "sx_count": len(sx_items),
  121. "portal_count": len(portal_items),
  122. }
  123. def main() -> None:
  124. sx_types, sx_data = load_sx_dicts(SX_SQL)
  125. portal_types, portal_data = load_portal_dicts(PORTAL_SQL)
  126. sx_type_keys = set(sx_types)
  127. portal_type_keys = set(portal_types)
  128. only_sx_types = sorted(sx_type_keys - portal_type_keys)
  129. only_portal_types = sorted(portal_type_keys - sx_type_keys)
  130. common_types = sorted(sx_type_keys & portal_type_keys)
  131. def count_by_prefix(types: set[str], prefix: str) -> int:
  132. return sum(1 for t in types if t.startswith(prefix))
  133. sx_data_total = sum(len(v) for v in sx_data.values())
  134. portal_data_total = sum(len(v) for v in portal_data.values())
  135. portal_active = sum(
  136. 1 for items in portal_data.values() for i in items
  137. ) # placeholder
  138. print("=" * 72)
  139. print("字典对比总览(基于 docs/sql 快照)")
  140. print("=" * 72)
  141. print(f"{'':28} {'sx.sql':>12} {'shanxia2.0':>12}")
  142. print(f"{'字典类型数':28} {len(sx_type_keys):>12} {len(portal_type_keys):>12}")
  143. print(f"{'字典项数':28} {sx_data_total:>12} {portal_data_total:>12}")
  144. print(f"{'crm_* 类型数':28} {count_by_prefix(sx_type_keys,'crm_'):>12} {count_by_prefix(portal_type_keys,'crm_'):>12}")
  145. print(f"{'xg_* 类型数(学工)':28} {count_by_prefix(sx_type_keys,'xg_'):>12} {count_by_prefix(portal_type_keys,'xg_'):>12}")
  146. print(f"{'sys_* 类型数':28} {count_by_prefix(sx_type_keys,'sys_'):>12} {count_by_prefix(portal_type_keys,'sys_'):>12}")
  147. print(f"{'pl_* 类型数(运输)':28} {count_by_prefix(sx_type_keys,'pl_'):>12} {count_by_prefix(portal_type_keys,'pl_'):>12}")
  148. print()
  149. # semantic alias pairs
  150. alias_pairs = [
  151. ("sys_user_sex", "sys_sex"),
  152. ("sys_show_hide", "sys_visible"),
  153. ("sys_normal_disable", "sys_status"),
  154. ]
  155. print("--- 语义相近但类型码不同的「别名」---")
  156. for sx_t, portal_t in alias_pairs:
  157. in_sx = sx_t in sx_types or portal_t in sx_types
  158. in_portal = sx_t in portal_types or portal_t in portal_types
  159. if in_sx or in_portal:
  160. print(
  161. f" sx: {sx_t} ({len(sx_data.get(sx_t,[]))}项) "
  162. f"| portal: {portal_t} ({len(portal_data.get(portal_t,[]))}项) "
  163. f"+ portal {sx_t} ({len(portal_data.get(sx_t,[]))}项)"
  164. )
  165. print()
  166. crm_prefixes = ("crm_", "xs_", "bx_")
  167. crm_only_sx = [t for t in only_sx_types if t.startswith(crm_prefixes) or "crm" in t or t.startswith("xs_")]
  168. other_only_sx = [t for t in only_sx_types if t not in crm_only_sx]
  169. print("=" * 72)
  170. print("字典类型 (sys_dict_type) 差异明细")
  171. print("=" * 72)
  172. print(f"类型码相同(两边都有): {len(common_types)}")
  173. print(f"仅 sx 有(门户缺失): {len(only_sx_types)}")
  174. print(f"仅门户有(sx 无,多为 2.0 新增): {len(only_portal_types)}")
  175. print()
  176. print("--- 仅门户有的字典类型(shanxia2.0 新增能力)---")
  177. for t in only_portal_types:
  178. print(f" {t:40} {portal_types[t]['dict_name']}")
  179. print()
  180. print("--- 仅 sx 有 · CRM/业务相关(建议评估是否迁入)---")
  181. for t in crm_only_sx:
  182. print(f" {t:40} {sx_types[t]['dict_name']} (data:{len(sx_data.get(t, []))})")
  183. print()
  184. print("--- 仅 sx 有 · 学工/校园 xg_*(门户一般不需要)---")
  185. xg_only = [t for t in only_sx_types if t.startswith("xg_")]
  186. for t in xg_only[:20]:
  187. print(f" {t:40} {sx_types[t]['dict_name']} (data:{len(sx_data.get(t, []))})")
  188. if len(xg_only) > 20:
  189. print(f" ... 共 {len(xg_only)} 个 xg_* 类型")
  190. print()
  191. print("--- 仅 sx 有 · 若依框架 sys_*(可能与门户同名不同义)---")
  192. sys_overlap_missing = [t for t in only_sx_types if t.startswith("sys_")]
  193. for t in sys_overlap_missing:
  194. print(f" {t:40} {sx_types[t]['dict_name']} (data:{len(sx_data.get(t, []))})")
  195. print()
  196. print("--- 仅 sx 有 · 其他 ---")
  197. for t in other_only_sx:
  198. if not t.startswith("xg_"):
  199. print(f" {t:40} {sx_types[t]['dict_name']} (data:{len(sx_data.get(t, []))})")
  200. print()
  201. print("=" * 72)
  202. print("同名类型下字典项 (sys_dict_data) 差异(sx ∩ 门户)")
  203. print("=" * 72)
  204. diff_types = []
  205. for t in common_types:
  206. cmp = compare_data(sx_data.get(t, []), portal_data.get(t, []))
  207. if cmp["only_sx"] or cmp["only_portal"] or cmp["sx_count"] != cmp["portal_count"]:
  208. diff_types.append((t, cmp))
  209. print(f"类型码相同但字典项不完全一致: {len(diff_types)} / {len(common_types)}")
  210. print()
  211. for t, cmp in sorted(diff_types, key=lambda x: x[0]):
  212. sx_name = sx_types[t]["dict_name"]
  213. portal_name = portal_types[t]["dict_name"]
  214. print(f"[{t}] sx:{sx_name} | portal:{portal_name}")
  215. print(f" sx项数={cmp['sx_count']} portal项数={cmp['portal_count']}")
  216. if cmp["only_sx"]:
  217. print(f" 仅sx有 ({len(cmp['only_sx'])}):")
  218. for k in cmp["only_sx"][:8]:
  219. print(f" - {k}")
  220. if len(cmp["only_sx"]) > 8:
  221. print(f" ... +{len(cmp['only_sx']) - 8}")
  222. if cmp["only_portal"]:
  223. print(f" 仅门户有 ({len(cmp['only_portal'])}):")
  224. for k in cmp["only_portal"][:8]:
  225. print(f" - {k}")
  226. if len(cmp["only_portal"]) > 8:
  227. print(f" ... +{len(cmp['only_portal']) - 8}")
  228. print()
  229. print("=" * 72)
  230. print("CRM 业务字典(sx 有、门户无)明细")
  231. print("=" * 72)
  232. crm_types = sorted(
  233. t
  234. for t in only_sx_types
  235. if t.startswith("crm_") or t.startswith("xs_") or t in ("contract_type", "contract_nature")
  236. )
  237. for t in crm_types:
  238. print(f"\n## {t} — {sx_types[t]['dict_name']}")
  239. for item in sorted(sx_data.get(t, []), key=lambda x: (x.get("sort") or "", x["value"])):
  240. print(f" {item['value']:20} {item['label']}")
  241. print()
  242. print("=" * 72)
  243. print("地磅系统 dibang.sql")
  244. print("=" * 72)
  245. dibang_text = DIBANG_SQL.read_text(encoding="utf-8", errors="ignore")
  246. has_dict = "sys_dict" in dibang_text
  247. print("含 sys_dict 表/数据:", "是" if has_dict else "否(地磅无独立字典表,枚举多在业务字段或前端硬编码)")
  248. if __name__ == "__main__":
  249. main()