migrate_backfill_default_chatbot_scene.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. """
  2. 迁移脚本:给老用户补齐默认 /chatbot 场景(幂等)
  3. 默认规则:
  4. - 幕布:fresh_green_wallpaper
  5. - slot_04:stump_stool
  6. - slot_03:french_window
  7. 并确保用户拥有:
  8. - fresh_green_wallpaper
  9. - stump_stool
  10. - french_window
  11. """
  12. import asyncio
  13. import sys
  14. from pathlib import Path
  15. from typing import Any
  16. import aiomysql
  17. backend_dir = Path(__file__).resolve().parents[1]
  18. if str(backend_dir) not in sys.path:
  19. sys.path.insert(0, str(backend_dir))
  20. from config import settings
  21. from shop_item_image_utils import scene_image_name_from_shop_row
  22. REQUIRED_TABLES = [
  23. "users",
  24. "shop_items",
  25. "user_assets",
  26. "user_chatbot_backdrops",
  27. "user_chatbot_slot_placements",
  28. ]
  29. ITEM_CODES = {
  30. "backdrop": "fresh_green_wallpaper",
  31. "seat": "stump_stool",
  32. "window": "french_window",
  33. }
  34. async def _table_exists(conn: aiomysql.Connection, table_name: str) -> bool:
  35. async with conn.cursor() as cursor:
  36. await cursor.execute(
  37. """
  38. SELECT COUNT(*) FROM information_schema.tables
  39. WHERE table_schema = %s AND table_name = %s
  40. """,
  41. (settings.MYSQL_DATABASE, table_name),
  42. )
  43. row: Any = await cursor.fetchone()
  44. return bool(row and row[0] > 0)
  45. async def _column_exists(conn: aiomysql.Connection, table: str, col: str) -> bool:
  46. async with conn.cursor() as cursor:
  47. await cursor.execute(
  48. """
  49. SELECT COUNT(*) FROM information_schema.columns
  50. WHERE table_schema=%s AND table_name=%s AND column_name=%s
  51. """,
  52. (settings.MYSQL_DATABASE, table, col),
  53. )
  54. row: Any = await cursor.fetchone()
  55. return bool(row and row[0] > 0)
  56. async def _col_is_bigint(conn: aiomysql.Connection, table: str, col: str) -> bool:
  57. async with conn.cursor() as cursor:
  58. await cursor.execute(
  59. """
  60. SELECT DATA_TYPE FROM information_schema.columns
  61. WHERE table_schema=%s AND table_name=%s AND column_name=%s
  62. """,
  63. (settings.MYSQL_DATABASE, table, col),
  64. )
  65. row: Any = await cursor.fetchone()
  66. return bool(row and "bigint" in str(row[0]).lower())
  67. async def migrate() -> bool:
  68. conn: aiomysql.Connection | None = None
  69. try:
  70. conn = await aiomysql.connect(
  71. host=settings.MYSQL_HOST,
  72. port=settings.MYSQL_PORT,
  73. user=settings.MYSQL_USER,
  74. password=settings.MYSQL_PASSWORD,
  75. db=settings.MYSQL_DATABASE,
  76. charset="utf8mb4",
  77. autocommit=False,
  78. )
  79. missing = [t for t in REQUIRED_TABLES if not await _table_exists(conn, t)]
  80. if missing:
  81. print(f"❌ 缺少必要表: {missing}")
  82. return False
  83. ua_bigint = await _col_is_bigint(conn, "user_assets", "user_id")
  84. ub_bigint = await _col_is_bigint(conn, "user_chatbot_backdrops", "user_id")
  85. up_bigint = await _col_is_bigint(conn, "user_chatbot_slot_placements", "user_id")
  86. ua_uid_expr = "u.id" if ua_bigint else "u.user_id"
  87. ub_uid_expr = "u.id" if ub_bigint else "u.user_id"
  88. up_uid_expr = "u.id" if up_bigint else "u.user_id"
  89. has_render_col = await _column_exists(conn, "shop_items", "render_image_name")
  90. async with conn.cursor() as cursor:
  91. if has_render_col:
  92. await cursor.execute(
  93. """
  94. SELECT item_code, id, image_name, render_image_name, default_z_index
  95. FROM shop_items
  96. WHERE item_code IN (%s,%s,%s)
  97. """,
  98. (ITEM_CODES["backdrop"], ITEM_CODES["seat"], ITEM_CODES["window"]),
  99. )
  100. rows = await cursor.fetchall()
  101. by_code = {
  102. r[0]: {
  103. "id": int(r[1]),
  104. "image_name": str(r[2]),
  105. "render_image_name": r[3],
  106. "z": int(r[4] or 0),
  107. }
  108. for r in rows
  109. }
  110. else:
  111. await cursor.execute(
  112. """
  113. SELECT item_code, id, image_name, default_z_index
  114. FROM shop_items
  115. WHERE item_code IN (%s,%s,%s)
  116. """,
  117. (ITEM_CODES["backdrop"], ITEM_CODES["seat"], ITEM_CODES["window"]),
  118. )
  119. rows = await cursor.fetchall()
  120. by_code = {
  121. r[0]: {
  122. "id": int(r[1]),
  123. "image_name": str(r[2]),
  124. "render_image_name": None,
  125. "z": int(r[3] or 0),
  126. }
  127. for r in rows
  128. }
  129. for k, code in ITEM_CODES.items():
  130. if code not in by_code:
  131. print(f"❌ shop_items 缺少 {code}")
  132. return False
  133. seat = by_code[ITEM_CODES["seat"]]
  134. window = by_code[ITEM_CODES["window"]]
  135. backdrop = by_code[ITEM_CODES["backdrop"]]
  136. # 1) 补 user_assets(拥有默认三件,used_qty 先不抬高)
  137. for role in ("seat", "window", "backdrop"):
  138. item_id = by_code[ITEM_CODES[role]]["id"]
  139. await cursor.execute(
  140. f"""
  141. INSERT INTO user_assets
  142. (user_id, item_id, owned_qty, used_qty, last_acquired_at, created_at, updated_at)
  143. SELECT {ua_uid_expr}, %s, 1, 0, NOW(), NOW(), NOW()
  144. FROM users u
  145. WHERE NOT EXISTS (
  146. SELECT 1 FROM user_assets ua
  147. WHERE ua.user_id = {ua_uid_expr} AND ua.item_id = %s
  148. )
  149. """,
  150. (item_id, item_id),
  151. )
  152. print("✅ user_assets 默认三件补齐完成")
  153. # 2) 补默认幕布
  154. await cursor.execute(
  155. f"""
  156. INSERT INTO user_chatbot_backdrops
  157. (user_id, backdrop_item_id, z_index, created_at, updated_at)
  158. SELECT {ub_uid_expr}, %s, 0, NOW(), NOW()
  159. FROM users u
  160. WHERE NOT EXISTS (
  161. SELECT 1 FROM user_chatbot_backdrops ub
  162. WHERE ub.user_id = {ub_uid_expr}
  163. )
  164. """,
  165. (backdrop["id"],),
  166. )
  167. print("✅ user_chatbot_backdrops 默认幕布补齐完成")
  168. # 3) 槽位摆放:slot_04 放 seat;slot_03 放 window
  169. # 先通过 user_assets 取每个用户对应 user_asset_id
  170. await cursor.execute(
  171. f"""
  172. INSERT INTO user_chatbot_slot_placements
  173. (user_id, backdrop_item_id, slot_code, user_asset_id, item_id, z_index, image_name_snapshot, placed_at, updated_at)
  174. SELECT
  175. {up_uid_expr},
  176. %s,
  177. 'slot_04_row4_col2',
  178. ua.id,
  179. %s,
  180. %s,
  181. %s,
  182. NOW(),
  183. NOW()
  184. FROM users u
  185. JOIN user_assets ua
  186. ON ua.user_id = {ua_uid_expr}
  187. AND ua.item_id = %s
  188. ON DUPLICATE KEY UPDATE
  189. user_asset_id=VALUES(user_asset_id),
  190. item_id=VALUES(item_id),
  191. z_index=VALUES(z_index),
  192. image_name_snapshot=VALUES(image_name_snapshot),
  193. updated_at=NOW()
  194. """,
  195. (
  196. backdrop["id"],
  197. seat["id"],
  198. seat["z"],
  199. scene_image_name_from_shop_row(seat),
  200. seat["id"],
  201. ),
  202. )
  203. await cursor.execute(
  204. f"""
  205. INSERT INTO user_chatbot_slot_placements
  206. (user_id, backdrop_item_id, slot_code, user_asset_id, item_id, z_index, image_name_snapshot, placed_at, updated_at)
  207. SELECT
  208. {up_uid_expr},
  209. %s,
  210. 'slot_03_center_2x3',
  211. ua.id,
  212. %s,
  213. %s,
  214. %s,
  215. NOW(),
  216. NOW()
  217. FROM users u
  218. JOIN user_assets ua
  219. ON ua.user_id = {ua_uid_expr}
  220. AND ua.item_id = %s
  221. ON DUPLICATE KEY UPDATE
  222. user_asset_id=VALUES(user_asset_id),
  223. item_id=VALUES(item_id),
  224. z_index=VALUES(z_index),
  225. image_name_snapshot=VALUES(image_name_snapshot),
  226. updated_at=NOW()
  227. """,
  228. (
  229. backdrop["id"],
  230. window["id"],
  231. window["z"],
  232. scene_image_name_from_shop_row(window),
  233. window["id"],
  234. ),
  235. )
  236. print("✅ user_chatbot_slot_placements 默认槽位补齐完成")
  237. # 4) used_qty 与默认摆放同步(seat/window 标记已使用1个)
  238. await cursor.execute(
  239. f"""
  240. UPDATE user_assets ua
  241. JOIN users u ON ua.user_id = {ua_uid_expr}
  242. SET ua.used_qty = GREATEST(ua.used_qty, 1), ua.updated_at = NOW()
  243. WHERE ua.item_id IN (%s,%s)
  244. """,
  245. (seat["id"], window["id"]),
  246. )
  247. await conn.commit()
  248. print("🎉 migrate_backfill_default_chatbot_scene 完成")
  249. return True
  250. except Exception as exc: # noqa: BLE001
  251. print(f"❌ 迁移失败: {exc}")
  252. if conn is not None:
  253. try:
  254. await conn.rollback()
  255. except Exception:
  256. pass
  257. return False
  258. finally:
  259. if conn is not None:
  260. conn.close()
  261. if __name__ == "__main__":
  262. if len(sys.argv) > 1 and sys.argv[1] == "--force":
  263. confirm = "y"
  264. else:
  265. confirm = input("\n是否继续为老用户回填默认/chatbot场景? (y/n): ").strip().lower()
  266. if confirm != "y":
  267. sys.exit(0)
  268. ok = asyncio.run(migrate())
  269. sys.exit(0 if ok else 1)