| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- #!/usr/bin/env python3
- """Compare sys_dict_type / sys_dict_data across SQL snapshots."""
- from __future__ import annotations
- import re
- from collections import defaultdict
- from pathlib import Path
- ROOT = Path(__file__).resolve().parents[2]
- SX_SQL = ROOT / "docs/sql/sx.sql"
- PORTAL_SQL = ROOT / "docs/sql/shanxia2.0.sql"
- DIBANG_SQL = ROOT / "docs/sql/dibang.sql"
- def parse_insert_values(line: str) -> list[str] | None:
- m = re.search(r"VALUES\s*\((.*)\)\s*;?\s*$", line, re.I)
- if not m:
- return None
- inner = m.group(1)
- parts: list[str] = []
- cur: list[str] = []
- in_str = False
- esc = False
- for ch in inner:
- if esc:
- cur.append(ch)
- esc = False
- continue
- if ch == "\\":
- esc = True
- cur.append(ch)
- continue
- if ch == "'":
- in_str = not in_str
- cur.append(ch)
- continue
- if ch == "," and not in_str:
- parts.append("".join(cur).strip())
- cur = []
- continue
- cur.append(ch)
- if cur:
- parts.append("".join(cur).strip())
- return parts
- def unquote(v: str) -> str:
- v = v.strip()
- if v.upper() == "NULL":
- return ""
- if v.startswith("'") and v.endswith("'"):
- return v[1:-1].replace("''", "'").replace("\\'", "'")
- return v
- def load_sx_dicts(path: Path) -> tuple[dict[str, dict], dict[str, list[dict]]]:
- types: dict[str, dict] = {}
- data_by_type: dict[str, list[dict]] = defaultdict(list)
- for line in path.read_text(encoding="utf-8", errors="ignore").splitlines():
- if "INSERT INTO `sys_dict_type`" in line:
- vals = parse_insert_values(line)
- if not vals or len(vals) < 4:
- continue
- # dict_id, tenant_id, dict_name, dict_type, ...
- dict_type = unquote(vals[3])
- types[dict_type] = {
- "dict_name": unquote(vals[2]),
- "tenant_id": unquote(vals[1]),
- "remark": unquote(vals[9]) if len(vals) > 9 else "",
- }
- elif "INSERT INTO `sys_dict_data`" in line:
- vals = parse_insert_values(line)
- if not vals or len(vals) < 6:
- continue
- # dict_code, tenant_id, dict_sort, dict_label, dict_value, dict_type, ...
- dict_type = unquote(vals[5])
- data_by_type[dict_type].append(
- {
- "label": unquote(vals[3]),
- "value": unquote(vals[4]),
- "sort": unquote(vals[2]),
- "css_class": unquote(vals[6]) if len(vals) > 6 else "",
- "list_class": unquote(vals[7]) if len(vals) > 7 else "",
- }
- )
- return types, data_by_type
- def load_portal_dicts(path: Path) -> tuple[dict[str, dict], dict[str, list[dict]]]:
- types: dict[str, dict] = {}
- data_by_type: dict[str, list[dict]] = defaultdict(list)
- for line in path.read_text(encoding="utf-8", errors="ignore").splitlines():
- if "INSERT INTO `sys_dict_type`" in line:
- vals = parse_insert_values(line)
- if not vals or len(vals) < 3:
- continue
- # id, dict_name, dict_type, tenant_no, ...
- dict_type = unquote(vals[2])
- types[dict_type] = {
- "dict_name": unquote(vals[1]),
- "tenant_no": unquote(vals[3]),
- "remark": unquote(vals[5]) if len(vals) > 5 else "",
- }
- elif "INSERT INTO `sys_dict_data`" in line:
- vals = parse_insert_values(line)
- if not vals or len(vals) < 5:
- continue
- # id, dict_label, dict_value, tag_type, dict_type, tenant_no, ...
- dict_type = unquote(vals[4])
- data_by_type[dict_type].append(
- {
- "label": unquote(vals[1]),
- "value": unquote(vals[2]),
- "tag_type": unquote(vals[3]),
- "sort": unquote(vals[6]) if len(vals) > 6 else "",
- }
- )
- return types, data_by_type
- def item_key(item: dict) -> str:
- return f"{item['value']}|{item['label']}"
- def compare_data(sx_items: list[dict], portal_items: list[dict]) -> dict:
- sx_map = {item_key(i): i for i in sx_items}
- portal_map = {item_key(i): i for i in portal_items}
- sx_keys = set(sx_map)
- portal_keys = set(portal_map)
- return {
- "only_sx": sorted(sx_keys - portal_keys),
- "only_portal": sorted(portal_keys - sx_keys),
- "common": sorted(sx_keys & portal_keys),
- "sx_count": len(sx_items),
- "portal_count": len(portal_items),
- }
- def main() -> None:
- sx_types, sx_data = load_sx_dicts(SX_SQL)
- portal_types, portal_data = load_portal_dicts(PORTAL_SQL)
- sx_type_keys = set(sx_types)
- portal_type_keys = set(portal_types)
- only_sx_types = sorted(sx_type_keys - portal_type_keys)
- only_portal_types = sorted(portal_type_keys - sx_type_keys)
- common_types = sorted(sx_type_keys & portal_type_keys)
- def count_by_prefix(types: set[str], prefix: str) -> int:
- return sum(1 for t in types if t.startswith(prefix))
- sx_data_total = sum(len(v) for v in sx_data.values())
- portal_data_total = sum(len(v) for v in portal_data.values())
- portal_active = sum(
- 1 for items in portal_data.values() for i in items
- ) # placeholder
- print("=" * 72)
- print("字典对比总览(基于 docs/sql 快照)")
- print("=" * 72)
- print(f"{'':28} {'sx.sql':>12} {'shanxia2.0':>12}")
- print(f"{'字典类型数':28} {len(sx_type_keys):>12} {len(portal_type_keys):>12}")
- print(f"{'字典项数':28} {sx_data_total:>12} {portal_data_total:>12}")
- print(f"{'crm_* 类型数':28} {count_by_prefix(sx_type_keys,'crm_'):>12} {count_by_prefix(portal_type_keys,'crm_'):>12}")
- print(f"{'xg_* 类型数(学工)':28} {count_by_prefix(sx_type_keys,'xg_'):>12} {count_by_prefix(portal_type_keys,'xg_'):>12}")
- print(f"{'sys_* 类型数':28} {count_by_prefix(sx_type_keys,'sys_'):>12} {count_by_prefix(portal_type_keys,'sys_'):>12}")
- print(f"{'pl_* 类型数(运输)':28} {count_by_prefix(sx_type_keys,'pl_'):>12} {count_by_prefix(portal_type_keys,'pl_'):>12}")
- print()
- # semantic alias pairs
- alias_pairs = [
- ("sys_user_sex", "sys_sex"),
- ("sys_show_hide", "sys_visible"),
- ("sys_normal_disable", "sys_status"),
- ]
- print("--- 语义相近但类型码不同的「别名」---")
- for sx_t, portal_t in alias_pairs:
- in_sx = sx_t in sx_types or portal_t in sx_types
- in_portal = sx_t in portal_types or portal_t in portal_types
- if in_sx or in_portal:
- print(
- f" sx: {sx_t} ({len(sx_data.get(sx_t,[]))}项) "
- f"| portal: {portal_t} ({len(portal_data.get(portal_t,[]))}项) "
- f"+ portal {sx_t} ({len(portal_data.get(sx_t,[]))}项)"
- )
- print()
- crm_prefixes = ("crm_", "xs_", "bx_")
- crm_only_sx = [t for t in only_sx_types if t.startswith(crm_prefixes) or "crm" in t or t.startswith("xs_")]
- other_only_sx = [t for t in only_sx_types if t not in crm_only_sx]
- print("=" * 72)
- print("字典类型 (sys_dict_type) 差异明细")
- print("=" * 72)
- print(f"类型码相同(两边都有): {len(common_types)}")
- print(f"仅 sx 有(门户缺失): {len(only_sx_types)}")
- print(f"仅门户有(sx 无,多为 2.0 新增): {len(only_portal_types)}")
- print()
- print("--- 仅门户有的字典类型(shanxia2.0 新增能力)---")
- for t in only_portal_types:
- print(f" {t:40} {portal_types[t]['dict_name']}")
- print()
- print("--- 仅 sx 有 · CRM/业务相关(建议评估是否迁入)---")
- for t in crm_only_sx:
- print(f" {t:40} {sx_types[t]['dict_name']} (data:{len(sx_data.get(t, []))})")
- print()
- print("--- 仅 sx 有 · 学工/校园 xg_*(门户一般不需要)---")
- xg_only = [t for t in only_sx_types if t.startswith("xg_")]
- for t in xg_only[:20]:
- print(f" {t:40} {sx_types[t]['dict_name']} (data:{len(sx_data.get(t, []))})")
- if len(xg_only) > 20:
- print(f" ... 共 {len(xg_only)} 个 xg_* 类型")
- print()
- print("--- 仅 sx 有 · 若依框架 sys_*(可能与门户同名不同义)---")
- sys_overlap_missing = [t for t in only_sx_types if t.startswith("sys_")]
- for t in sys_overlap_missing:
- print(f" {t:40} {sx_types[t]['dict_name']} (data:{len(sx_data.get(t, []))})")
- print()
- print("--- 仅 sx 有 · 其他 ---")
- for t in other_only_sx:
- if not t.startswith("xg_"):
- print(f" {t:40} {sx_types[t]['dict_name']} (data:{len(sx_data.get(t, []))})")
- print()
- print("=" * 72)
- print("同名类型下字典项 (sys_dict_data) 差异(sx ∩ 门户)")
- print("=" * 72)
- diff_types = []
- for t in common_types:
- cmp = compare_data(sx_data.get(t, []), portal_data.get(t, []))
- if cmp["only_sx"] or cmp["only_portal"] or cmp["sx_count"] != cmp["portal_count"]:
- diff_types.append((t, cmp))
- print(f"类型码相同但字典项不完全一致: {len(diff_types)} / {len(common_types)}")
- print()
- for t, cmp in sorted(diff_types, key=lambda x: x[0]):
- sx_name = sx_types[t]["dict_name"]
- portal_name = portal_types[t]["dict_name"]
- print(f"[{t}] sx:{sx_name} | portal:{portal_name}")
- print(f" sx项数={cmp['sx_count']} portal项数={cmp['portal_count']}")
- if cmp["only_sx"]:
- print(f" 仅sx有 ({len(cmp['only_sx'])}):")
- for k in cmp["only_sx"][:8]:
- print(f" - {k}")
- if len(cmp["only_sx"]) > 8:
- print(f" ... +{len(cmp['only_sx']) - 8}")
- if cmp["only_portal"]:
- print(f" 仅门户有 ({len(cmp['only_portal'])}):")
- for k in cmp["only_portal"][:8]:
- print(f" - {k}")
- if len(cmp["only_portal"]) > 8:
- print(f" ... +{len(cmp['only_portal']) - 8}")
- print()
- print("=" * 72)
- print("CRM 业务字典(sx 有、门户无)明细")
- print("=" * 72)
- crm_types = sorted(
- t
- for t in only_sx_types
- if t.startswith("crm_") or t.startswith("xs_") or t in ("contract_type", "contract_nature")
- )
- for t in crm_types:
- print(f"\n## {t} — {sx_types[t]['dict_name']}")
- for item in sorted(sx_data.get(t, []), key=lambda x: (x.get("sort") or "", x["value"])):
- print(f" {item['value']:20} {item['label']}")
- print()
- print("=" * 72)
- print("地磅系统 dibang.sql")
- print("=" * 72)
- dibang_text = DIBANG_SQL.read_text(encoding="utf-8", errors="ignore")
- has_dict = "sys_dict" in dibang_text
- print("含 sys_dict 表/数据:", "是" if has_dict else "否(地磅无独立字典表,枚举多在业务字段或前端硬编码)")
- if __name__ == "__main__":
- main()
|