migrate_scenario_gift_items_to_all_users.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. """
  2. 迁移脚本:仅为「已触发过特殊场景」的老用户补齐对应赠送商品
  3. 规则:
  4. - scene=window -> coffee_mug(咖啡杯)
  5. - scene=reading -> books(书)
  6. - scene=eating -> snacks(零食)
  7. 触发依据:
  8. - user_special_scenario_topic_logs.scenario_key
  9. - 同一用户同一场景只补对应那一件商品(已拥有则跳过)
  10. 执行:
  11. python migrations/migrate_scenario_gift_items_to_all_users.py
  12. 或:
  13. python migrations/migrate_scenario_gift_items_to_all_users.py --force
  14. """
  15. import asyncio
  16. import sys
  17. from pathlib import Path
  18. from typing import Any, Dict, List
  19. import aiomysql
  20. backend_dir = Path(__file__).resolve().parents[1]
  21. if str(backend_dir) not in sys.path:
  22. sys.path.insert(0, str(backend_dir))
  23. from config import settings
  24. async def _table_exists(conn: aiomysql.Connection, table_name: str) -> bool:
  25. async with conn.cursor() as cursor:
  26. await cursor.execute(
  27. """
  28. SELECT COUNT(*) FROM information_schema.tables
  29. WHERE table_schema = %s AND table_name = %s
  30. """,
  31. (settings.MYSQL_DATABASE, table_name),
  32. )
  33. row: Any = await cursor.fetchone()
  34. return bool(row and row[0] > 0)
  35. async def _column_data_type(conn: aiomysql.Connection, table_name: str, column_name: str) -> str:
  36. async with conn.cursor() as cursor:
  37. await cursor.execute(
  38. """
  39. SELECT DATA_TYPE, COLUMN_TYPE
  40. FROM information_schema.columns
  41. WHERE table_schema = %s AND table_name = %s AND column_name = %s
  42. """,
  43. (settings.MYSQL_DATABASE, table_name, column_name),
  44. )
  45. row: Any = await cursor.fetchone()
  46. if not row:
  47. return ""
  48. data_type = row[0] or ""
  49. column_type = row[1] or ""
  50. return f"{data_type}|{column_type}"
  51. async def migrate() -> bool:
  52. try:
  53. conn: aiomysql.Connection = await aiomysql.connect(
  54. host=settings.MYSQL_HOST,
  55. port=settings.MYSQL_PORT,
  56. user=settings.MYSQL_USER,
  57. password=settings.MYSQL_PASSWORD,
  58. db=settings.MYSQL_DATABASE,
  59. charset="utf8mb4",
  60. )
  61. required_tables = ["users", "shop_items", "user_assets", "user_special_scenario_topic_logs"]
  62. missing = [t for t in required_tables if not await _table_exists(conn, t)]
  63. if missing:
  64. print(f"❌ 缺少必要表:{missing}。请先创建对应表后再执行。")
  65. await conn.ensure_closed()
  66. return False
  67. user_id_type_hint = await _column_data_type(conn, "user_assets", "user_id")
  68. user_id_is_bigint = "bigint" in user_id_type_hint.lower()
  69. user_id_expr = "u.id" if user_id_is_bigint else "u.user_id"
  70. if not user_id_type_hint:
  71. print("❌ 未能读取 user_assets.user_id 字段类型,无法确定映射方式。")
  72. await conn.ensure_closed()
  73. return False
  74. gift_items: List[Dict[str, str]] = [
  75. {"scenario_key": "window", "item_code": "coffee_mug", "note": "window 窗前"},
  76. {"scenario_key": "reading", "item_code": "books", "note": "reading 看书"},
  77. {"scenario_key": "eating", "item_code": "snacks", "note": "eating 吃饭"},
  78. ]
  79. item_codes = [it["item_code"] for it in gift_items]
  80. async with conn.cursor() as cursor:
  81. await cursor.execute(
  82. f"""
  83. SELECT `id`, `item_code`, `image_name`
  84. FROM `shop_items`
  85. WHERE `item_code` IN ({", ".join(["%s"] * len(item_codes))})
  86. """,
  87. tuple(item_codes),
  88. )
  89. rows: List[Any] = list(await cursor.fetchall())
  90. if not rows:
  91. print("❌ 在 shop_items 中没有找到情景赠送商品。请先执行 chatbot_shop_items_seed.sql。")
  92. await conn.ensure_closed()
  93. return False
  94. item_code_to_item_id: Dict[str, int] = {r[1]: r[0] for r in rows}
  95. missing_item_codes = [c for c in item_codes if c not in item_code_to_item_id]
  96. if missing_item_codes:
  97. print(f"❌ 缺少物品:item_code={missing_item_codes}")
  98. await conn.ensure_closed()
  99. return False
  100. inserted_total = 0
  101. for it in gift_items:
  102. item_id = item_code_to_item_id[it["item_code"]]
  103. scenario_key = it["scenario_key"]
  104. print(
  105. f"🔄 补齐情景赠送:scenario={scenario_key}, item_code={it['item_code']} ({it['note']}) item_id={item_id}"
  106. )
  107. await cursor.execute(
  108. f"""
  109. INSERT INTO `user_assets`
  110. (`user_id`, `item_id`, `owned_qty`, `used_qty`, `last_acquired_at`, `created_at`, `updated_at`)
  111. SELECT
  112. {user_id_expr},
  113. %s,
  114. 1,
  115. 0,
  116. NOW(),
  117. NOW(),
  118. NOW()
  119. FROM `users` u
  120. WHERE EXISTS (
  121. SELECT 1
  122. FROM `user_special_scenario_topic_logs` slog
  123. WHERE slog.`user_id` = u.`user_id`
  124. AND slog.`scenario_key` = %s
  125. )
  126. AND NOT EXISTS (
  127. SELECT 1
  128. FROM `user_assets` ua
  129. WHERE ua.`item_id` = %s
  130. AND ua.`user_id` = {user_id_expr}
  131. )
  132. """,
  133. (item_id, scenario_key, item_id),
  134. )
  135. inserted = cursor.rowcount or 0
  136. inserted_total += inserted
  137. print(f"✅ 完成:{it['item_code']},新增资产行数={inserted}")
  138. await conn.commit()
  139. print(f"🎉 全部完成,总共新增资产行数={inserted_total}")
  140. await conn.ensure_closed()
  141. return True
  142. except Exception as exc: # noqa: BLE001
  143. print(f"❌ 迁移失败: {exc}")
  144. import traceback
  145. traceback.print_exc()
  146. return False
  147. if __name__ == "__main__":
  148. if len(sys.argv) > 1 and sys.argv[1] == "--force":
  149. confirm = "y"
  150. else:
  151. confirm = input("\n是否继续按已触发场景补齐情景赠送物品? (y/n): ")
  152. if confirm.lower() != "y":
  153. sys.exit(0)
  154. ok: Any = asyncio.run(migrate())
  155. sys.exit(0 if ok else 1)