run_demo_s8_migration.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. #!/usr/bin/env python3
  2. """Curated, credential-free S8 migration into UATDEMO."""
  3. from __future__ import annotations
  4. import json
  5. import re
  6. from datetime import datetime
  7. from pathlib import Path
  8. import pymysql
  9. from pymysql.cursors import DictCursor
  10. ROOT = Path(__file__).resolve().parents[4]
  11. DATABASE_JSON = ROOT / "server" / "Admin.NET.Application" / "Configuration" / "Database.json"
  12. SOURCE_TENANT = 797403760988230
  13. SOURCE_FACTORY = 797403760988231
  14. TARGET_TENANT = 838257237606469
  15. TARGET_FACTORY = 838257237676101
  16. def connect() -> pymysql.Connection:
  17. raw = DATABASE_JSON.read_text(encoding="utf-8-sig")
  18. connection_string = next(
  19. value
  20. for value in re.findall(
  21. r'(?m)^\s*"ConnectionString"\s*:\s*"([^"]+)"', raw
  22. )
  23. if "Database=aidopdev" in value
  24. )
  25. parts = {
  26. item.split("=", 1)[0].strip().lower(): item.split("=", 1)[1].strip()
  27. for item in connection_string.split(";")
  28. if "=" in item
  29. }
  30. return pymysql.connect(
  31. host=parts["server"],
  32. port=int(parts["port"]),
  33. user=parts["uid"],
  34. password=parts["pwd"],
  35. database=parts["database"],
  36. charset="utf8mb4",
  37. autocommit=False,
  38. cursorclass=DictCursor,
  39. )
  40. def columns(cur: DictCursor, table: str) -> list[str]:
  41. cur.execute(f"SHOW COLUMNS FROM `{table}`")
  42. return [
  43. row["Field"]
  44. for row in cur.fetchall()
  45. if "auto_increment" not in (row["Extra"] or "")
  46. ]
  47. def insert(cur: DictCursor, table: str, row: dict[str, object]) -> int:
  48. writable = columns(cur, table)
  49. values = {key: row.get(key) for key in writable}
  50. cur.execute(
  51. f"INSERT INTO `{table}` ({','.join(f'`{key}`' for key in writable)}) "
  52. f"VALUES ({','.join(['%s'] * len(writable))})",
  53. tuple(values[key] for key in writable),
  54. )
  55. return int(cur.lastrowid)
  56. def source_rows(cur: DictCursor, table: str) -> list[dict[str, object]]:
  57. cur.execute(
  58. f"""
  59. SELECT * FROM `{table}`
  60. WHERE tenant_id=%s AND factory_id=%s
  61. ORDER BY id
  62. """,
  63. (SOURCE_TENANT, SOURCE_FACTORY),
  64. )
  65. return list(cur.fetchall())
  66. def existing_id(
  67. cur: DictCursor, table: str, key: str, value: object
  68. ) -> int | None:
  69. cur.execute(
  70. f"""
  71. SELECT id FROM `{table}`
  72. WHERE tenant_id=%s AND factory_id=%s AND `{key}`=%s
  73. ORDER BY id LIMIT 1
  74. """,
  75. (TARGET_TENANT, TARGET_FACTORY, value),
  76. )
  77. row = cur.fetchone()
  78. return int(row["id"]) if row else None
  79. def target_scope(row: dict[str, object]) -> dict[str, object]:
  80. result = dict(row)
  81. result["tenant_id"] = TARGET_TENANT
  82. result["factory_id"] = TARGET_FACTORY
  83. return result
  84. def ensure_unassigned_department(cur: DictCursor) -> int:
  85. cur.execute(
  86. """
  87. SELECT RecID FROM DepartmentMaster
  88. WHERE tenant_id=%s AND factory_ref_id=%s AND Department='未分配'
  89. ORDER BY RecID LIMIT 1
  90. """,
  91. (TARGET_TENANT, TARGET_FACTORY),
  92. )
  93. row = cur.fetchone()
  94. if row:
  95. return int(row["RecID"])
  96. cur.execute(
  97. """
  98. INSERT INTO DepartmentMaster
  99. (company_ref_id,factory_ref_id,tenant_id,Domain,Department,Descr,
  100. IsActive,CreateTime,CreateUser)
  101. VALUES(%s,%s,%s,'UATDEMO','未分配','S8 migration fallback',1,NOW(),
  102. 'uat-s8-migration')
  103. """,
  104. (TARGET_FACTORY, TARGET_FACTORY, TARGET_TENANT),
  105. )
  106. return int(cur.lastrowid)
  107. def copy_simple(
  108. cur: DictCursor,
  109. table: str,
  110. natural_key: str,
  111. transform,
  112. ) -> tuple[int, dict[int, int]]:
  113. inserted = 0
  114. id_map: dict[int, int] = {}
  115. for source in source_rows(cur, table):
  116. old_id = int(source["id"])
  117. existing = existing_id(cur, table, natural_key, source[natural_key])
  118. if existing:
  119. id_map[old_id] = existing
  120. continue
  121. row = transform(target_scope(source))
  122. row.pop("id", None)
  123. new_id = insert(cur, table, row)
  124. id_map[old_id] = new_id
  125. inserted += 1
  126. return inserted, id_map
  127. def main() -> None:
  128. conn = connect()
  129. summary: dict[str, int] = {}
  130. try:
  131. with conn.cursor() as cur:
  132. department_id = ensure_unassigned_department(cur)
  133. summary["scenes_inserted"], _ = copy_simple(
  134. cur,
  135. "ado_s8_scene_config",
  136. "scene_code",
  137. lambda row: row,
  138. )
  139. def sanitize_source(row: dict[str, object]) -> dict[str, object]:
  140. row.update(
  141. endpoint=None,
  142. auth_type="NONE",
  143. enabled=0,
  144. last_check_at=None,
  145. last_check_status=None,
  146. )
  147. return row
  148. summary["data_sources_inserted"], data_source_map = copy_simple(
  149. cur,
  150. "ado_s8_data_source",
  151. "data_source_code",
  152. sanitize_source,
  153. )
  154. def sanitize_rule(row: dict[str, object]) -> dict[str, object]:
  155. old_ds = row.get("data_source_id")
  156. row["data_source_id"] = (
  157. data_source_map.get(int(old_ds)) if old_ds else None
  158. )
  159. row.update(
  160. enabled=0,
  161. next_run_at=None,
  162. last_run_at=None,
  163. last_status=None,
  164. last_error=None,
  165. last_duration_ms=None,
  166. last_run_id=None,
  167. lock_token=None,
  168. locked_by=None,
  169. lock_until=None,
  170. running_started_at=None,
  171. consecutive_failure_count=0,
  172. paused_until=None,
  173. pause_reason=None,
  174. )
  175. return row
  176. summary["rules_inserted"], rule_map = copy_simple(
  177. cur,
  178. "ado_s8_watch_rule",
  179. "rule_code",
  180. sanitize_rule,
  181. )
  182. cur.execute(
  183. """
  184. SELECT type_code FROM ado_s8_exception_type
  185. WHERE (tenant_id=0 OR tenant_id=%s)
  186. AND (factory_id=0 OR factory_id=%s)
  187. """,
  188. (TARGET_TENANT, TARGET_FACTORY),
  189. )
  190. valid_types = {str(row["type_code"]) for row in cur.fetchall()}
  191. exception_map: dict[int, int] = {}
  192. exceptions_inserted = 0
  193. for source in source_rows(cur, "ado_s8_exception"):
  194. old_id = int(source["id"])
  195. type_code = str(source.get("exception_type_code") or "")
  196. if old_id in {329, 331, 332} or type_code not in valid_types:
  197. continue
  198. existing = existing_id(
  199. cur, "ado_s8_exception", "exception_code", source["exception_code"]
  200. )
  201. if existing:
  202. exception_map[old_id] = existing
  203. continue
  204. row = target_scope(source)
  205. row.pop("id", None)
  206. old_ds = row.get("source_data_source_id")
  207. old_rule = row.get("source_rule_id")
  208. row["source_data_source_id"] = (
  209. data_source_map.get(int(old_ds)) if old_ds else None
  210. )
  211. row["source_rule_id"] = (
  212. rule_map.get(int(old_rule)) if old_rule else None
  213. )
  214. row["occurrence_dept_id"] = department_id
  215. row["responsible_dept_id"] = department_id
  216. for field in (
  217. "responsible_group_id",
  218. "assignee_id",
  219. "reporter_id",
  220. "created_by",
  221. "updated_by",
  222. "verifier_id",
  223. "active_flow_instance_id",
  224. "active_flow_biz_type",
  225. ):
  226. row[field] = None
  227. new_id = insert(cur, "ado_s8_exception", row)
  228. exception_map[old_id] = new_id
  229. exceptions_inserted += 1
  230. summary["exceptions_inserted"] = exceptions_inserted
  231. timeline_inserted = 0
  232. cur.execute(
  233. """
  234. SELECT t.* FROM ado_s8_exception_timeline t
  235. JOIN ado_s8_exception e ON e.id=t.exception_id
  236. WHERE e.tenant_id=%s AND e.factory_id=%s
  237. ORDER BY t.id
  238. """,
  239. (SOURCE_TENANT, SOURCE_FACTORY),
  240. )
  241. for source in cur.fetchall():
  242. old_exception = int(source["exception_id"])
  243. if old_exception not in exception_map:
  244. continue
  245. target_exception = exception_map[old_exception]
  246. cur.execute(
  247. """
  248. SELECT 1 FROM ado_s8_exception_timeline
  249. WHERE exception_id=%s AND action_code=%s AND created_at=%s
  250. LIMIT 1
  251. """,
  252. (
  253. target_exception,
  254. source["action_code"],
  255. source["created_at"],
  256. ),
  257. )
  258. if cur.fetchone():
  259. continue
  260. row = dict(source)
  261. row.pop("id", None)
  262. row["exception_id"] = target_exception
  263. row["operator_id"] = None
  264. row["operator_name"] = "Demo演示"
  265. insert(cur, "ado_s8_exception_timeline", row)
  266. timeline_inserted += 1
  267. summary["timelines_inserted"] = timeline_inserted
  268. detection_inserted = 0
  269. for source in source_rows(cur, "ado_s8_detection_log"):
  270. old_rule = source.get("rule_id")
  271. if not old_rule or int(old_rule) not in rule_map:
  272. continue
  273. old_exception = source.get("exception_id")
  274. row = target_scope(source)
  275. row.pop("id", None)
  276. row["rule_id"] = rule_map[int(old_rule)]
  277. row["exception_id"] = (
  278. exception_map.get(int(old_exception)) if old_exception else None
  279. )
  280. cur.execute(
  281. """
  282. SELECT 1 FROM ado_s8_detection_log
  283. WHERE tenant_id=%s AND factory_id=%s
  284. AND rule_id=%s AND detected_at=%s
  285. AND COALESCE(run_id,'')=COALESCE(%s,'')
  286. AND COALESCE(source_object_id,'')=COALESCE(%s,'')
  287. LIMIT 1
  288. """,
  289. (
  290. TARGET_TENANT,
  291. TARGET_FACTORY,
  292. row["rule_id"],
  293. row["detected_at"],
  294. row.get("run_id"),
  295. row.get("source_object_id"),
  296. ),
  297. )
  298. if cur.fetchone():
  299. continue
  300. insert(cur, "ado_s8_detection_log", row)
  301. detection_inserted += 1
  302. summary["detections_inserted"] = detection_inserted
  303. states_inserted = 0
  304. for source in source_rows(cur, "ado_s8_rule_detection_state"):
  305. cur.execute(
  306. """
  307. SELECT id FROM ado_s8_rule_detection_state
  308. WHERE tenant_id=%s AND factory_id=%s
  309. AND rule_code=%s AND dedup_key=%s
  310. LIMIT 1
  311. """,
  312. (
  313. TARGET_TENANT,
  314. TARGET_FACTORY,
  315. source["rule_code"],
  316. source["dedup_key"],
  317. ),
  318. )
  319. if cur.fetchone():
  320. continue
  321. row = target_scope(source)
  322. row.pop("id", None)
  323. active = row.get("active_exception_id")
  324. row["active_exception_id"] = (
  325. exception_map.get(int(active)) if active else None
  326. )
  327. insert(cur, "ado_s8_rule_detection_state", row)
  328. states_inserted += 1
  329. summary["states_inserted"] = states_inserted
  330. conn.commit()
  331. checks = {
  332. "scenes": "ado_s8_scene_config",
  333. "data_sources": "ado_s8_data_source",
  334. "rules": "ado_s8_watch_rule",
  335. "exceptions": "ado_s8_exception",
  336. "detections": "ado_s8_detection_log",
  337. "states": "ado_s8_rule_detection_state",
  338. "notifications": "ado_s8_notification_log",
  339. }
  340. for label, table in checks.items():
  341. cur.execute(
  342. f"SELECT COUNT(*) count FROM `{table}` "
  343. "WHERE tenant_id=%s AND factory_id=%s",
  344. (TARGET_TENANT, TARGET_FACTORY),
  345. )
  346. summary[f"{label}_total"] = int(cur.fetchone()["count"])
  347. cur.execute(
  348. """
  349. SELECT COUNT(*) count FROM ado_s8_exception_timeline t
  350. JOIN ado_s8_exception e ON e.id=t.exception_id
  351. WHERE e.tenant_id=%s AND e.factory_id=%s
  352. """,
  353. (TARGET_TENANT, TARGET_FACTORY),
  354. )
  355. summary["timelines_total"] = int(cur.fetchone()["count"])
  356. except Exception:
  357. conn.rollback()
  358. raise
  359. finally:
  360. conn.close()
  361. evidence = Path(__file__).with_name("WP4-DEMO-S8-execution-evidence.json")
  362. evidence.write_text(
  363. json.dumps(
  364. {
  365. "executed_at": datetime.now().isoformat(timespec="seconds"),
  366. "summary": summary,
  367. },
  368. ensure_ascii=False,
  369. indent=2,
  370. ),
  371. encoding="utf-8",
  372. )
  373. print(json.dumps(summary, ensure_ascii=False, indent=2))
  374. if __name__ == "__main__":
  375. main()