run_wp_sd8_reset_improvement.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. #!/usr/bin/env python3
  2. """WP-SD8 audit reset for Smart Diagnosis improvement-loop test batches.
  3. Default is dry-run. Only specified UAT tenants and RootCause batch markers
  4. are touched. KPI / DWD business facts are never deleted.
  5. """
  6. from __future__ import annotations
  7. import argparse
  8. import json
  9. import re
  10. from datetime import date, datetime
  11. from decimal import Decimal
  12. from pathlib import Path
  13. import pymysql
  14. ROOT = Path(__file__).resolve().parents[4]
  15. CONFIG = ROOT / "server" / "Admin.NET.Application" / "Configuration" / "Database.json"
  16. EVIDENCE = ROOT / "doc" / "plan" / "UAT留证" / "2026-08-17-智慧诊断正式落地"
  17. DEFAULT_TENANTS = (838257186181189, 838257212780613, 838257237606469)
  18. BATCH_PREFIX = "[WP-SD7/"
  19. def connect() -> pymysql.Connection:
  20. raw = CONFIG.read_text(encoding="utf-8-sig")
  21. value = next(
  22. item
  23. for item in re.findall(r'(?m)^\s*"ConnectionString"\s*:\s*"([^"]+)"', raw)
  24. if "Database=aidopdev" in item
  25. )
  26. parts = {
  27. item.split("=", 1)[0].strip().lower(): item.split("=", 1)[1].strip()
  28. for item in value.split(";")
  29. if "=" in item
  30. }
  31. return pymysql.connect(
  32. host=parts["server"],
  33. port=int(parts["port"]),
  34. user=parts["uid"],
  35. password=parts["pwd"],
  36. database=parts["database"],
  37. charset="utf8mb4",
  38. cursorclass=pymysql.cursors.DictCursor,
  39. autocommit=False,
  40. )
  41. def json_safe(value: object) -> object:
  42. if isinstance(value, datetime):
  43. return value.isoformat(timespec="seconds")
  44. if isinstance(value, date):
  45. return value.isoformat()
  46. if isinstance(value, Decimal):
  47. return str(value)
  48. if isinstance(value, dict):
  49. return {str(key): json_safe(item) for key, item in value.items()}
  50. if isinstance(value, (list, tuple)):
  51. return [json_safe(item) for item in value]
  52. if isinstance(value, int) and abs(value) > 2**53:
  53. return str(value)
  54. return value
  55. def fetch_all(cur: pymysql.cursors.DictCursor, sql: str, params: tuple = ()) -> list[dict]:
  56. cur.execute(sql, params)
  57. return [json_safe(row) for row in cur.fetchall()]
  58. def table_exists(cur: pymysql.cursors.DictCursor, name: str) -> bool:
  59. cur.execute(
  60. """
  61. SELECT COUNT(*) c FROM information_schema.tables
  62. WHERE table_schema=DATABASE() AND table_name=%s
  63. """,
  64. (name,),
  65. )
  66. return int((cur.fetchone() or {}).get("c") or 0) > 0
  67. def main() -> None:
  68. parser = argparse.ArgumentParser()
  69. parser.add_argument("--batch", default="WP-SD7", help="Batch marker inside RootCause")
  70. parser.add_argument("--apply", action="store_true", help="Actually delete; default is dry-run")
  71. parser.add_argument(
  72. "--tenants",
  73. default=",".join(str(x) for x in DEFAULT_TENANTS),
  74. help="Comma-separated tenant IDs",
  75. )
  76. args = parser.parse_args()
  77. tenant_ids = tuple(int(x.strip()) for x in args.tenants.split(",") if x.strip())
  78. marker = f"[{args.batch}/"
  79. placeholders = ",".join(["%s"] * len(tenant_ids))
  80. EVIDENCE.mkdir(parents=True, exist_ok=True)
  81. conn = connect()
  82. try:
  83. cur = conn.cursor()
  84. plans = fetch_all(
  85. cur,
  86. f"""
  87. SELECT * FROM ado_smart_ops_improvement_plan
  88. WHERE TenantId IN ({placeholders})
  89. AND RootCause LIKE %s
  90. """,
  91. (*tenant_ids, f"%{marker}%"),
  92. )
  93. plan_ids = [int(row["Id"]) for row in plans]
  94. backup = {
  95. "work_package": "WP-SD8",
  96. "dry_run": not args.apply,
  97. "batch": args.batch,
  98. "generated_at": datetime.now().isoformat(timespec="seconds"),
  99. "tenant_ids": [str(x) for x in tenant_ids],
  100. "plan_count": len(plans),
  101. "plans": plans,
  102. "actions": [],
  103. "action_logs": [],
  104. "action_notices": [],
  105. "verify_logs": [],
  106. "flow_instances": [],
  107. }
  108. if plan_ids:
  109. id_ph = ",".join(["%s"] * len(plan_ids))
  110. if table_exists(cur, "ado_smart_ops_improvement_action"):
  111. backup["actions"] = fetch_all(
  112. cur,
  113. f"SELECT * FROM ado_smart_ops_improvement_action WHERE tenant_id IN ({placeholders}) AND plan_id IN ({id_ph})",
  114. (*tenant_ids, *plan_ids),
  115. )
  116. action_ids = [int(row["id"]) for row in backup["actions"]]
  117. if action_ids and table_exists(cur, "ado_smart_ops_improvement_action_log"):
  118. a_ph = ",".join(["%s"] * len(action_ids))
  119. backup["action_logs"] = fetch_all(
  120. cur,
  121. f"SELECT * FROM ado_smart_ops_improvement_action_log WHERE tenant_id IN ({placeholders}) AND action_id IN ({a_ph})",
  122. (*tenant_ids, *action_ids),
  123. )
  124. if action_ids and table_exists(cur, "ado_smart_ops_improvement_action_notice"):
  125. a_ph = ",".join(["%s"] * len(action_ids))
  126. backup["action_notices"] = fetch_all(
  127. cur,
  128. f"SELECT * FROM ado_smart_ops_improvement_action_notice WHERE tenant_id IN ({placeholders}) AND action_id IN ({a_ph})",
  129. (*tenant_ids, *action_ids),
  130. )
  131. if table_exists(cur, "ado_smart_ops_improvement_verify_log"):
  132. backup["verify_logs"] = fetch_all(
  133. cur,
  134. f"SELECT * FROM ado_smart_ops_improvement_verify_log WHERE tenant_id IN ({placeholders}) AND plan_id IN ({id_ph})",
  135. (*tenant_ids, *plan_ids),
  136. )
  137. flow_ids = [int(row["FlowInstanceId"]) for row in plans if row.get("FlowInstanceId")]
  138. for table in ("af_flow_instance", "ado_flow_instance", "ApprovalFlowInstance"):
  139. if flow_ids and table_exists(cur, table):
  140. f_ph = ",".join(["%s"] * len(flow_ids))
  141. backup["flow_instances"] = fetch_all(
  142. cur,
  143. f"SELECT * FROM {table} WHERE Id IN ({f_ph})",
  144. tuple(flow_ids),
  145. )
  146. backup["flow_table"] = table
  147. break
  148. stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
  149. backup_path = EVIDENCE / f"08-reset-backup-{args.batch}-{stamp}.json"
  150. backup_path.write_text(json.dumps(json_safe(backup), ensure_ascii=False, indent=2), encoding="utf-8")
  151. deleted = {
  152. "notices": 0,
  153. "action_logs": 0,
  154. "actions": 0,
  155. "verify_logs": 0,
  156. "plans": 0,
  157. }
  158. if args.apply and plan_ids:
  159. id_ph = ",".join(["%s"] * len(plan_ids))
  160. action_ids = [int(x["id"]) for x in backup["actions"]]
  161. if action_ids and table_exists(cur, "ado_smart_ops_improvement_action_notice"):
  162. a_ph = ",".join(["%s"] * len(action_ids))
  163. cur.execute(
  164. f"DELETE FROM ado_smart_ops_improvement_action_notice WHERE tenant_id IN ({placeholders}) AND action_id IN ({a_ph})",
  165. (*tenant_ids, *action_ids),
  166. )
  167. deleted["notices"] = cur.rowcount
  168. if action_ids and table_exists(cur, "ado_smart_ops_improvement_action_log"):
  169. a_ph = ",".join(["%s"] * len(action_ids))
  170. cur.execute(
  171. f"DELETE FROM ado_smart_ops_improvement_action_log WHERE tenant_id IN ({placeholders}) AND action_id IN ({a_ph})",
  172. (*tenant_ids, *action_ids),
  173. )
  174. deleted["action_logs"] = cur.rowcount
  175. if backup["actions"]:
  176. cur.execute(
  177. f"DELETE FROM ado_smart_ops_improvement_action WHERE tenant_id IN ({placeholders}) AND plan_id IN ({id_ph})",
  178. (*tenant_ids, *plan_ids),
  179. )
  180. deleted["actions"] = cur.rowcount
  181. if table_exists(cur, "ado_smart_ops_improvement_verify_log"):
  182. cur.execute(
  183. f"DELETE FROM ado_smart_ops_improvement_verify_log WHERE tenant_id IN ({placeholders}) AND plan_id IN ({id_ph})",
  184. (*tenant_ids, *plan_ids),
  185. )
  186. deleted["verify_logs"] = cur.rowcount
  187. cur.execute(
  188. f"DELETE FROM ado_smart_ops_improvement_plan WHERE TenantId IN ({placeholders}) AND Id IN ({id_ph})",
  189. (*tenant_ids, *plan_ids),
  190. )
  191. deleted["plans"] = cur.rowcount
  192. conn.commit()
  193. else:
  194. conn.rollback()
  195. summary = {
  196. "work_package": "WP-SD8",
  197. "dry_run": not args.apply,
  198. "batch": args.batch,
  199. "backup": str(backup_path.relative_to(ROOT)).replace("\\", "/"),
  200. "selected_plans": [str(x) for x in plan_ids],
  201. "deleted": deleted,
  202. "kpi_dwd_untouched": True,
  203. }
  204. summary_path = EVIDENCE / "08-reset-last.json"
  205. summary_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
  206. print(json.dumps(summary, ensure_ascii=False, indent=2))
  207. finally:
  208. conn.close()
  209. if __name__ == "__main__":
  210. main()