run_contract_stg_s1_s4.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. """S1–S4 贴源契约:同一 Mock 载荷以 DB/API 源标签写入对应 mdp_stg_*,比对 source_biz_key。
  2. 不跑完整 std(S1–S4 transform 依赖多表 JOIN);验证执行器 Writer 键规格一致即可。
  3. """
  4. from __future__ import annotations
  5. import json
  6. import sys
  7. from datetime import datetime
  8. import requests
  9. from _db import get_conn
  10. MOCK = "http://127.0.0.1:8018"
  11. TOKEN = "uat-mock-token"
  12. TS = datetime.now().strftime("%Y%m%d%H%M%S")
  13. # (mod, path, stg_table, source_table, biz_fields)
  14. CASES = [
  15. ("S1", "/api/sales-order", "mdp_stg_so", "crm_seorder", ["bill_no"]),
  16. ("S1", "/api/shipment", "mdp_stg_ship_trans", "ASNBOLShipperDetail", ["Id", "Line"]),
  17. ("S2", "/api/work-order", "mdp_stg_schedule", "WorkOrdMaster", ["Domain", "WorkOrd"]),
  18. ("S2", "/api/schedule", "mdp_stg_schedule", "ScheduleResultOpMaster", ["Domain", "WorkOrd", "Op", "WorkDate"]),
  19. ("S3", "/api/purchase-order", "mdp_stg_purchase_order", "PurOrdDetail", ["Domain", "PurOrd", "Line"]),
  20. ("S3", "/api/delivery", "mdp_stg_delivery", "srm_polist_ds", ["dsnum"]),
  21. ("S3", "/api/supplier", "mdp_stg_supplier", "SuppMaster", ["Domain", "Supp"]),
  22. ("S4", "/api/s4-shipment", "mdp_stg_s4_shipment", "scm_shdzb", ["glid", "id"]),
  23. ("S4", "/api/s4-iqc", "mdp_stg_s4_iqc", "PurOrdRctDetail", ["Domain", "Receiver", "Line"]),
  24. ("S4", "/api/s4-return", "mdp_stg_s4_return", "srm_polist_ds", ["dsnum"]),
  25. ]
  26. def fetch(path: str) -> list[dict]:
  27. r = requests.get(MOCK.rstrip("/") + path, headers={"Authorization": f"Bearer {TOKEN}"}, timeout=30)
  28. r.raise_for_status()
  29. rows = r.json().get("data", {}).get("list")
  30. if not isinstance(rows, list):
  31. raise RuntimeError(f"{path} invalid")
  32. return rows
  33. def biz_key(row: dict, fields: list[str]) -> str:
  34. vals = []
  35. for f in fields:
  36. # mock 样本字段大小写可能不一致
  37. if f in row and row[f] not in (None, ""):
  38. vals.append(str(row[f]))
  39. continue
  40. # 尝试常见别名
  41. alt = next((k for k in row if k.lower() == f.lower()), None)
  42. if alt is None or row[alt] in (None, ""):
  43. return str(row.get("bizKey") or row.get("id") or row.get("Id") or row.get("RecID"))
  44. vals.append(str(row[alt]))
  45. return "#".join(vals)
  46. def upsert(cur, stg: str, source_system: str, source_table: str, row: dict, fields: list[str], batch: str):
  47. biz = biz_key(row, fields)
  48. rid = str(row.get("RecID") or row.get("id") or row.get("Id") or row.get("bizKey") or biz)
  49. raw = json.dumps(row, ensure_ascii=False)
  50. cur.execute(
  51. f"""
  52. INSERT INTO {stg}
  53. (tenant_id, source_system, source_table, source_row_id, source_biz_key,
  54. raw_data, sync_batch_id, sync_time, process_status)
  55. VALUES (0, %s, %s, %s, %s, %s, %s, NOW(), 'PENDING')
  56. ON DUPLICATE KEY UPDATE
  57. source_row_id=VALUES(source_row_id), raw_data=VALUES(raw_data),
  58. sync_batch_id=VALUES(sync_batch_id), sync_time=VALUES(sync_time),
  59. process_status='PENDING'
  60. """,
  61. (source_system, source_table, rid, biz, raw, batch),
  62. )
  63. def keys_of(cur, stg: str, batch: str) -> set[str]:
  64. cur.execute(f"SELECT source_biz_key AS k FROM {stg} WHERE sync_batch_id=%s", (batch,))
  65. return {str(r["k"]) for r in cur.fetchall() if r["k"] is not None}
  66. def run_case(conn, mod: str, path: str, stg: str, source_table: str, fields: list[str]) -> bool:
  67. rows = fetch(path)
  68. db_batch = f"CT_DB_{mod}_{source_table}_{TS}"
  69. api_batch = f"CT_API_{mod}_{source_table}_{TS}"
  70. with conn.cursor() as cur:
  71. for r in rows:
  72. upsert(cur, stg, "AIDOPDEV_MYSQL", source_table, r, fields, db_batch)
  73. keys_db = keys_of(cur, stg, db_batch)
  74. cur.execute(f"DELETE FROM {stg} WHERE sync_batch_id=%s", (db_batch,))
  75. for r in rows:
  76. upsert(cur, stg, "WMS_API", source_table, r, fields, api_batch)
  77. keys_api = keys_of(cur, stg, api_batch)
  78. label = f"{mod}:{source_table}"
  79. print(f"[{label}] db={len(keys_db)} api={len(keys_api)} path={path}")
  80. if not keys_db or not keys_api:
  81. print(f"[FAIL] {label} empty", file=sys.stderr)
  82. return False
  83. if keys_db != keys_api:
  84. print(f"[FAIL] {label} only_db={sorted(keys_db-keys_api)[:5]} only_api={sorted(keys_api-keys_db)[:5]}", file=sys.stderr)
  85. return False
  86. print(f"[PASS] {label}")
  87. return True
  88. def main() -> int:
  89. conn = get_conn()
  90. try:
  91. results = [run_case(conn, *c) for c in CASES]
  92. finally:
  93. conn.close()
  94. if not all(results):
  95. print(f"[FAIL] {sum(1 for x in results if not x)}/{len(results)} cases", file=sys.stderr)
  96. return 1
  97. print(f"[PASS] S1-S4 stg contract {len(results)} cases (ts={TS})")
  98. return 0
  99. if __name__ == "__main__":
  100. sys.exit(main())