run_wp_sd3_migrate_actions.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. #!/usr/bin/env python3
  2. """Idempotent migration of action_items_json into ado_smart_ops_improvement_action."""
  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.constants import CLIENT
  10. ROOT = Path(__file__).resolve().parents[4]
  11. CONFIG = ROOT / "server" / "Admin.NET.Application" / "Configuration" / "Database.json"
  12. EVIDENCE = ROOT / "doc" / "plan" / "UAT留证" / "2026-08-17-智慧诊断正式落地"
  13. INVALID_TENANTS = {0, 1, 1300000000001}
  14. def connect() -> pymysql.Connection:
  15. raw = CONFIG.read_text(encoding="utf-8-sig")
  16. value = next(
  17. item
  18. for item in re.findall(r'(?m)^\s*"ConnectionString"\s*:\s*"([^"]+)"', raw)
  19. if "Database=aidopdev" in item
  20. )
  21. parts = {
  22. item.split("=", 1)[0].strip().lower(): item.split("=", 1)[1].strip()
  23. for item in value.split(";")
  24. if "=" in item
  25. }
  26. return pymysql.connect(
  27. host=parts["server"],
  28. port=int(parts["port"]),
  29. user=parts["uid"],
  30. password=parts["pwd"],
  31. database=parts["database"],
  32. charset="utf8mb4",
  33. autocommit=True,
  34. client_flag=CLIENT.MULTI_STATEMENTS,
  35. cursorclass=pymysql.cursors.DictCursor,
  36. )
  37. def normalize_status(raw: str | None) -> str:
  38. value = (raw or "").strip().lower()
  39. if value in {"completed", "done"}:
  40. return "completed"
  41. if value in {"doing", "in_progress"}:
  42. return "in_progress"
  43. if value in {"cancelled", "canceled"}:
  44. return "cancelled"
  45. return "pending"
  46. def parse_owner_user_id(owner: str | None) -> int | None:
  47. if owner is None:
  48. return None
  49. text = str(owner).strip()
  50. return int(text) if text.isdigit() else None
  51. def parse_date(raw: str | None):
  52. if not raw:
  53. return None
  54. text = str(raw).strip()
  55. for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%Y-%m-%dT%H:%M:%S"):
  56. try:
  57. return datetime.strptime(text[:19] if "T" in text else text[:10], fmt if "T" not in text else "%Y-%m-%dT%H:%M:%S")
  58. except ValueError:
  59. continue
  60. return None
  61. def parse_drafts(raw: str | None) -> list[dict]:
  62. if not raw or raw.strip() in {"", "[]", "null"}:
  63. return []
  64. try:
  65. data = json.loads(raw)
  66. except json.JSONDecodeError:
  67. return []
  68. if not isinstance(data, list):
  69. return []
  70. items = []
  71. for item in data:
  72. if not isinstance(item, dict):
  73. continue
  74. content = (item.get("content") or item.get("Content") or "").strip()
  75. if not content:
  76. continue
  77. items.append(
  78. {
  79. "content": content,
  80. "owner": item.get("owner") or item.get("Owner"),
  81. "dueDate": item.get("dueDate") or item.get("DueDate"),
  82. "status": item.get("status") or item.get("Status"),
  83. "completedAt": item.get("completedAt") or item.get("CompletedAt"),
  84. }
  85. )
  86. return items
  87. def main() -> None:
  88. conn = connect()
  89. report = {"migratedPlans": [], "skipped": [], "failed": []}
  90. try:
  91. with conn.cursor() as cur:
  92. cur.execute(
  93. """
  94. SELECT Id, TenantId, FactoryId, PlanNo, OwnerUserId, ActionItemsJson
  95. FROM ado_smart_ops_improvement_plan
  96. WHERE ActionItemsJson IS NOT NULL
  97. AND ActionItemsJson NOT IN ('', '[]', 'null')
  98. """
  99. )
  100. plans = cur.fetchall()
  101. for plan in plans:
  102. tenant_id = int(plan["TenantId"] or 0)
  103. plan_id = int(plan["Id"])
  104. key = {"planId": str(plan_id), "tenantId": str(tenant_id), "planNo": plan["PlanNo"]}
  105. if tenant_id in INVALID_TENANTS:
  106. report["skipped"].append({**key, "reason": "invalid_tenant"})
  107. continue
  108. factory_id = int(plan["FactoryId"] or 0)
  109. if factory_id <= 0 or factory_id == tenant_id:
  110. report["skipped"].append({**key, "reason": "invalid_factory"})
  111. continue
  112. cur.execute(
  113. "SELECT COUNT(*) AS n FROM ado_smart_ops_improvement_action WHERE tenant_id=%s AND plan_id=%s",
  114. (tenant_id, plan_id),
  115. )
  116. if cur.fetchone()["n"] > 0:
  117. report["skipped"].append({**key, "reason": "already_migrated"})
  118. continue
  119. drafts = parse_drafts(plan["ActionItemsJson"])
  120. if not drafts:
  121. report["skipped"].append({**key, "reason": "empty_or_unparseable"})
  122. continue
  123. now = datetime.now()
  124. inserted = 0
  125. for idx, draft in enumerate(drafts, start=1):
  126. cur.execute(
  127. """
  128. INSERT INTO ado_smart_ops_improvement_action
  129. (tenant_id, factory_id, plan_id, sequence_no, content, owner_user_id, owner_name,
  130. due_date, status, completed_at, version, create_user_id, update_user_id, create_time, update_time)
  131. VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,1,%s,%s,%s,%s)
  132. """,
  133. (
  134. tenant_id,
  135. factory_id,
  136. plan_id,
  137. idx,
  138. draft["content"][:500],
  139. parse_owner_user_id(draft["owner"]),
  140. (str(draft["owner"])[:100] if draft["owner"] else None),
  141. parse_date(draft["dueDate"]),
  142. normalize_status(draft["status"]),
  143. parse_date(draft["completedAt"]),
  144. plan["OwnerUserId"],
  145. plan["OwnerUserId"],
  146. now,
  147. now,
  148. ),
  149. )
  150. inserted += 1
  151. report["migratedPlans"].append({**key, "actions": inserted})
  152. finally:
  153. conn.close()
  154. EVIDENCE.mkdir(parents=True, exist_ok=True)
  155. out = EVIDENCE / "03-action-json-migration.json"
  156. out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
  157. print(json.dumps(report, ensure_ascii=False, indent=2))
  158. if __name__ == "__main__":
  159. main()