scenario_gift_service.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. """
  2. 特殊情景对话(?scene=window|reading|eating)首句成功发送后,赠送对应商店物品到 user_assets。
  3. 商品以 item_code 为准(兼容不同库 shop_items.id 自增差异)。
  4. 参考:window→coffee_mug(常 id=11)、reading→books(常 id=8)、eating→snacks(常 id=24)
  5. """
  6. import logging
  7. from typing import Union
  8. from sqlalchemy import select, text
  9. from sqlalchemy.ext.asyncio import AsyncSession
  10. from config import settings
  11. from database import db_manager
  12. from models_db import User
  13. from scenario_config import SCENE_EATING, SCENE_READING, SCENE_WINDOW
  14. logger = logging.getLogger(__name__)
  15. SCENE_GIFT_ITEM_CODE_BY_SCENE: dict[str, str] = {
  16. SCENE_WINDOW: "coffee_mug",
  17. SCENE_READING: "books",
  18. SCENE_EATING: "snacks",
  19. }
  20. async def _mysql_table_exists(session: AsyncSession, table_name: str) -> bool:
  21. try:
  22. r = await session.execute(
  23. text(
  24. """
  25. SELECT COUNT(*) AS c
  26. FROM information_schema.tables
  27. WHERE table_schema = :db AND table_name = :table
  28. """
  29. ),
  30. {"db": settings.MYSQL_DATABASE, "table": table_name},
  31. )
  32. row = r.mappings().first()
  33. return bool(row and int(row.get("c") or 0) > 0)
  34. except Exception:
  35. return False
  36. async def _mysql_column_data_type(
  37. session: AsyncSession, table_name: str, column_name: str
  38. ) -> str:
  39. try:
  40. r = await session.execute(
  41. text(
  42. """
  43. SELECT DATA_TYPE
  44. FROM information_schema.columns
  45. WHERE table_schema = :db
  46. AND table_name = :table
  47. AND column_name = :column
  48. """
  49. ),
  50. {"db": settings.MYSQL_DATABASE, "table": table_name, "column": column_name},
  51. )
  52. row = r.mappings().first()
  53. return str((row or {}).get("DATA_TYPE") or "")
  54. except Exception:
  55. return ""
  56. async def _user_assets_uid_value(
  57. session: AsyncSession, user_id_uuid: str
  58. ) -> Union[str, int, None]:
  59. dt = await _mysql_column_data_type(session, "user_assets", "user_id")
  60. if not dt:
  61. return None
  62. if "bigint" in dt.lower():
  63. r = await session.execute(select(User.id).where(User.user_id == user_id_uuid))
  64. rid = r.scalar_one_or_none()
  65. return int(rid) if rid is not None else None
  66. return user_id_uuid
  67. async def grant_scene_gift_if_needed(user_id_uuid: str, scene: str) -> None:
  68. """
  69. 若本次为特殊情景且用户尚无该场景对应商品,则插入 user_assets 一行(owned=1, used=0)。
  70. 失败仅打日志,不抛异常。
  71. """
  72. item_code = SCENE_GIFT_ITEM_CODE_BY_SCENE.get(scene)
  73. if not item_code or not user_id_uuid or user_id_uuid == "guest_user":
  74. return
  75. try:
  76. async with db_manager.get_session() as session:
  77. if not await _mysql_table_exists(session, "shop_items") or not await _mysql_table_exists(
  78. session, "user_assets"
  79. ):
  80. return
  81. ua_uid = await _user_assets_uid_value(session, user_id_uuid)
  82. if ua_uid is None:
  83. return
  84. item_res = await session.execute(
  85. text(
  86. """
  87. SELECT id FROM shop_items
  88. WHERE item_code = :code AND is_active = 1
  89. LIMIT 1
  90. """
  91. ),
  92. {"code": item_code},
  93. )
  94. item_row = item_res.mappings().first()
  95. if not item_row or item_row.get("id") is None:
  96. logger.warning(
  97. "⚠️ 情景赠送:shop_items 无商品 item_code=%s,跳过 scene=%s",
  98. item_code,
  99. scene,
  100. )
  101. return
  102. item_id = int(item_row["id"])
  103. exists = await session.execute(
  104. text(
  105. """
  106. SELECT id FROM user_assets
  107. WHERE user_id = :uid AND item_id = :iid
  108. LIMIT 1
  109. """
  110. ),
  111. {"uid": ua_uid, "iid": item_id},
  112. )
  113. if exists.scalar_one_or_none() is not None:
  114. return
  115. await session.execute(
  116. text(
  117. """
  118. INSERT INTO user_assets
  119. (user_id, item_id, owned_qty, used_qty, last_acquired_at, created_at, updated_at)
  120. VALUES
  121. (:uid, :iid, 1, 0, NOW(), NOW(), NOW())
  122. """
  123. ),
  124. {"uid": ua_uid, "iid": item_id},
  125. )
  126. await session.commit()
  127. logger.info(
  128. "✅ 情景赠送商品:user_id=%s scene=%s item_code=%s item_id=%s",
  129. user_id_uuid,
  130. scene,
  131. item_code,
  132. item_id,
  133. )
  134. except Exception as exc: # noqa: BLE001
  135. logger.warning(
  136. "⚠️ 情景赠送失败:user_id=%s scene=%s error=%s",
  137. user_id_uuid,
  138. scene,
  139. exc,
  140. )