user_service.py 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102
  1. """
  2. 用户服务模块(SQLAlchemy ORM 版本)
  3. """
  4. import uuid
  5. import random
  6. import asyncio
  7. import logging
  8. from typing import Dict, Optional, Any, Tuple
  9. from datetime import datetime
  10. from sqlalchemy import select, update, func, insert
  11. from sqlalchemy.ext.asyncio import AsyncSession
  12. from database import db_manager
  13. from models_db import User, UserUIState, UserBackgroundImage, BackgroundImage, UserAvatar
  14. logger = logging.getLogger(__name__)
  15. # 默认头像 ID
  16. DEFAULT_AVATAR_ID = "avatar_default"
  17. REGISTERED_AVATAR_ID = "avatar_registered_cap"
  18. LEVEL_THRESHOLDS_MS: Tuple[Tuple[int, int], ...] = (
  19. (5, 240_000),
  20. (4, 120_000),
  21. (3, 60_000),
  22. (2, 30_000),
  23. (1, 10_000),
  24. )
  25. class UserService:
  26. """用户管理服务"""
  27. @staticmethod
  28. def _calculate_user_level(total_ms: int) -> int:
  29. """
  30. 根据累计说话时长计算用户等级
  31. """
  32. for level, threshold in LEVEL_THRESHOLDS_MS:
  33. if total_ms >= threshold:
  34. return level
  35. return 0
  36. async def _init_user_background_image(self, session: AsyncSession, user_id: str) -> None:
  37. """
  38. 初始化用户背景图片(添加到已获得列表)
  39. Args:
  40. session: 数据库会话
  41. user_id: 用户ID
  42. """
  43. try:
  44. default_image_id = "default"
  45. # 检查是否已存在,避免重复添加
  46. result = await session.execute(
  47. select(UserBackgroundImage).where(
  48. UserBackgroundImage.user_id == user_id,
  49. UserBackgroundImage.image_id == default_image_id
  50. )
  51. )
  52. existing = result.scalar_one_or_none()
  53. if not existing:
  54. user_bg_image = UserBackgroundImage(
  55. user_id=user_id,
  56. image_id=default_image_id,
  57. obtained_type="default",
  58. obtained_source="system_init"
  59. )
  60. session.add(user_bg_image)
  61. logger.debug(f"✅ 为用户初始化背景图片: user_id={user_id}, image_id={default_image_id}")
  62. else:
  63. logger.debug(f"⏭️ 用户已拥有默认背景图片: user_id={user_id}")
  64. except Exception as e:
  65. # 背景图片初始化失败不应该影响用户创建,只记录日志
  66. logger.warning(f"⚠️ 初始化用户背景图片失败: user_id={user_id}, error={e}")
  67. async def _init_user_avatar(self, session: AsyncSession, user_id: str, user_type: str = "guest") -> None:
  68. """
  69. 初始化用户头像(添加到已获得列表)
  70. Args:
  71. session: 数据库会话
  72. user_id: 用户ID
  73. user_type: 用户类型(guest 或 registered)
  74. """
  75. try:
  76. # 游客用户只分配默认头像
  77. # 注册用户同时分配默认头像和注册赠送头像
  78. avatars_to_add = []
  79. if user_type == "registered":
  80. # 注册用户:默认头像 + 注册赠送头像
  81. avatars_to_add = [
  82. (DEFAULT_AVATAR_ID, "default", "system_init"),
  83. (REGISTERED_AVATAR_ID, "register", "register_bonus"),
  84. ]
  85. else:
  86. # 游客用户:只分配默认头像
  87. avatars_to_add = [
  88. (DEFAULT_AVATAR_ID, "default", "system_init"),
  89. ]
  90. for avatar_id, obtained_type, obtained_source in avatars_to_add:
  91. # 检查是否已存在,避免重复添加
  92. result = await session.execute(
  93. select(UserAvatar).where(
  94. UserAvatar.user_id == user_id,
  95. UserAvatar.avatar_id == avatar_id
  96. )
  97. )
  98. existing = result.scalar_one_or_none()
  99. if not existing:
  100. user_avatar = UserAvatar(
  101. user_id=user_id,
  102. avatar_id=avatar_id,
  103. obtained_type=obtained_type,
  104. obtained_source=obtained_source
  105. )
  106. session.add(user_avatar)
  107. logger.debug(f"✅ 为用户初始化头像: user_id={user_id}, avatar_id={avatar_id}, user_type={user_type}")
  108. else:
  109. logger.debug(f"⏭️ 用户已拥有头像: user_id={user_id}, avatar_id={avatar_id}")
  110. except Exception as e:
  111. # 头像初始化失败不应该影响用户创建,只记录日志
  112. logger.warning(f"⚠️ 初始化用户头像失败: user_id={user_id}, error={e}")
  113. async def _generate_temp_username(self, session: AsyncSession) -> str:
  114. """
  115. 生成临时用户名(格式:User + 7位随机数字)
  116. 确保用户名不重复
  117. Args:
  118. session: 数据库会话
  119. Returns:
  120. 临时用户名
  121. """
  122. max_attempts = 100 # 最大重试次数,避免无限循环
  123. for attempt in range(max_attempts):
  124. # 生成7位随机数字(1000000-9999999)
  125. random_number = random.randint(1000000, 9999999)
  126. temp_username = f"User{random_number}"
  127. # 检查用户名是否已存在
  128. result = await session.execute(
  129. select(User).where(User.username == temp_username)
  130. )
  131. existing_user = result.scalar_one_or_none()
  132. if not existing_user:
  133. logger.debug(f"生成临时用户名: {temp_username} (尝试次数: {attempt + 1})")
  134. return temp_username
  135. # 如果100次都重复,使用时间戳的后7位作为备选方案
  136. # 使用当前时间戳的毫秒数取模,确保是7位数字
  137. for fallback_attempt in range(10): # 备选方案也最多尝试10次
  138. timestamp_suffix = int(datetime.now().timestamp() * 1000) % 10000000
  139. # 确保至少是7位数字(如果不足7位,前面补0)
  140. fallback_number = str(timestamp_suffix).zfill(7)
  141. temp_username = f"User{fallback_number}"
  142. # 再次检查是否重复
  143. result = await session.execute(
  144. select(User).where(User.username == temp_username)
  145. )
  146. existing_user = result.scalar_one_or_none()
  147. if not existing_user:
  148. logger.warning(f"⚠️ 临时用户名生成达到最大重试次数,使用备选方案: {temp_username}")
  149. return temp_username
  150. # 如果还是重复,等待1毫秒后重试(确保时间戳变化)
  151. await asyncio.sleep(0.001)
  152. # 最后的备选方案:使用UUID的十六进制转数字(取后7位)
  153. uuid_hex = uuid.uuid4().hex
  154. final_number = str(int(uuid_hex[:8], 16) % 10000000).zfill(7)
  155. temp_username = f"User{final_number}"
  156. logger.error(f"❌ 临时用户名生成异常,使用最终备选方案: {temp_username}")
  157. return temp_username
  158. async def create_guest_user(self, username: str = "", bot_name: Optional[str] = None) -> Dict:
  159. """
  160. 创建游客用户
  161. Args:
  162. username: 用户名(默认为空字符串,为空时自动生成临时用户名)
  163. bot_name: 用户给机器人取的名字(可选)
  164. Returns:
  165. 用户信息字典(包含user_id)
  166. """
  167. try:
  168. # 生成唯一用户ID
  169. user_id = str(uuid.uuid4())
  170. # 使用 ORM 创建用户
  171. async with db_manager.get_session() as session:
  172. # 如果用户名为空,生成临时用户名
  173. if not username or username.strip() == "":
  174. username = await self._generate_temp_username(session)
  175. logger.info(f"🔧 自动生成临时用户名: {username}")
  176. user = User(
  177. user_id=user_id,
  178. username=username,
  179. user_type="guest",
  180. bot_name=bot_name,
  181. current_background_image_id="default", # 设置默认背景图片
  182. )
  183. session.add(user)
  184. await session.flush() # 先 flush 确保 user 有 ID,但不提交事务
  185. # 初始化背景图片
  186. await self._init_user_background_image(session, user_id)
  187. # 初始化默认头像
  188. await self._init_user_avatar(session, user_id, user_type="guest")
  189. await session.commit()
  190. await session.refresh(user)
  191. # 初始化 user_ui_states 的 current_avatar_id(游客用户使用默认头像)
  192. await self.upsert_user_ui_state(
  193. user_id=user_id,
  194. current_avatar_id=DEFAULT_AVATAR_ID
  195. )
  196. logger.info(f"✅ 创建游客用户成功: user_id={user_id}, username={username}, bot_name={bot_name}")
  197. return {
  198. "user_id": user_id,
  199. "username": username,
  200. "user_type": "guest",
  201. "bot_name": bot_name,
  202. "user_level": 0,
  203. "current_background_image_id": "default",
  204. "created_at": datetime.now().isoformat()
  205. }
  206. except Exception as e:
  207. logger.error(f"❌ 创建用户失败: {e}")
  208. raise
  209. async def get_user(self, user_id: str) -> Optional[Dict]:
  210. """
  211. 获取用户信息
  212. Args:
  213. user_id: 用户ID
  214. Returns:
  215. 用户信息字典,如果不存在返回None
  216. 包含 current_background_image_metadata 字段(背景图片的metadata配置)
  217. """
  218. try:
  219. async with db_manager.get_session() as session:
  220. result = await session.execute(
  221. select(User).where(User.user_id == user_id)
  222. )
  223. user = result.scalar_one_or_none()
  224. if user:
  225. user_dict = user.to_dict()
  226. # 如果用户有背景图片ID,查询背景图片的metadata
  227. if user.current_background_image_id:
  228. try:
  229. bg_result = await session.execute(
  230. select(BackgroundImage).where(
  231. BackgroundImage.image_id == user.current_background_image_id
  232. )
  233. )
  234. bg_image = bg_result.scalar_one_or_none()
  235. if bg_image and bg_image.metadata_json:
  236. user_dict["current_background_image_metadata"] = bg_image.metadata_json
  237. except Exception as e:
  238. logger.warning(f"⚠️ 获取背景图片metadata失败: image_id={user.current_background_image_id}, error={e}")
  239. return user_dict
  240. return None
  241. except Exception as e:
  242. logger.error(f"❌ 获取用户信息失败: user_id={user_id}, error={e}")
  243. return None
  244. async def update_user_last_active(self, user_id: str):
  245. """
  246. 更新用户最后活跃时间
  247. Args:
  248. user_id: 用户ID
  249. """
  250. try:
  251. async with db_manager.get_session() as session:
  252. await session.execute(
  253. update(User)
  254. .where(User.user_id == user_id)
  255. .values(last_active_at=func.now())
  256. )
  257. await session.commit()
  258. except Exception as e:
  259. logger.warning(f"⚠️ 更新用户活跃时间失败: user_id={user_id}, error={e}")
  260. async def update_username(self, user_id: str, username: str) -> bool:
  261. """
  262. 更新用户名
  263. Args:
  264. user_id: 用户ID
  265. username: 用户名
  266. Returns:
  267. 是否更新成功
  268. """
  269. try:
  270. async with db_manager.get_session() as session:
  271. result = await session.execute(
  272. update(User)
  273. .where(User.user_id == user_id)
  274. .values(username=username)
  275. )
  276. await session.commit()
  277. if result.rowcount > 0:
  278. logger.info(f"✅ 更新用户名成功: user_id={user_id}, username={username}")
  279. return True
  280. return False
  281. except Exception as e:
  282. logger.error(f"❌ 更新用户名失败: user_id={user_id}, error={e}")
  283. return False
  284. async def update_current_background_image_id(self, user_id: str, background_image_id: str) -> bool:
  285. """
  286. 更新用户当前使用的背景图片ID
  287. Args:
  288. user_id: 用户ID
  289. background_image_id: 背景图片ID
  290. Returns:
  291. 是否更新成功
  292. """
  293. try:
  294. async with db_manager.get_session() as session:
  295. result = await session.execute(
  296. update(User)
  297. .where(User.user_id == user_id)
  298. .values(current_background_image_id=background_image_id)
  299. )
  300. await session.commit()
  301. if result.rowcount > 0:
  302. logger.info(f"✅ 更新用户背景图片ID成功: user_id={user_id}, background_image_id={background_image_id}")
  303. return True
  304. return False
  305. except Exception as e:
  306. logger.error(f"❌ 更新用户背景图片ID失败: user_id={user_id}, error={e}")
  307. return False
  308. async def update_bot_name(self, user_id: str, bot_name: str) -> bool:
  309. """
  310. 更新用户给机器人取的名字
  311. Args:
  312. user_id: 用户ID
  313. bot_name: 机器人名字
  314. Returns:
  315. 是否更新成功
  316. """
  317. import time
  318. start_time = time.time()
  319. try:
  320. async with db_manager.get_session() as session:
  321. # 🔥 先查询当前值,避免不必要的更新
  322. query_start = time.time()
  323. result = await session.execute(
  324. select(User.bot_name).where(User.user_id == user_id)
  325. )
  326. current_bot_name = result.scalar_one_or_none()
  327. query_duration = (time.time() - query_start) * 1000
  328. # 如果值相同,直接返回(避免不必要的 UPDATE)
  329. if current_bot_name == bot_name:
  330. total_duration = (time.time() - start_time) * 1000
  331. logger.info(
  332. f"⏭️ 机器人名字未变化,跳过更新: user_id={user_id}, "
  333. f"bot_name={bot_name}, 耗时={total_duration:.1f}ms"
  334. )
  335. return True
  336. # 执行更新
  337. update_start = time.time()
  338. update_result = await session.execute(
  339. update(User)
  340. .where(User.user_id == user_id)
  341. .values(bot_name=bot_name)
  342. )
  343. await session.commit()
  344. update_duration = (time.time() - update_start) * 1000
  345. total_duration = (time.time() - start_time) * 1000
  346. if update_result.rowcount > 0:
  347. logger.info(
  348. f"✅ 更新机器人名字成功: user_id={user_id}, "
  349. f"bot_name={bot_name}, "
  350. f"查询耗时={query_duration:.1f}ms, "
  351. f"更新耗时={update_duration:.1f}ms, "
  352. f"总耗时={total_duration:.1f}ms"
  353. )
  354. return True
  355. else:
  356. logger.warning(
  357. f"⚠️ 更新机器人名字失败: 未找到用户 user_id={user_id}, "
  358. f"总耗时={total_duration:.1f}ms"
  359. )
  360. return False
  361. except asyncio.TimeoutError:
  362. total_duration = (time.time() - start_time) * 1000
  363. logger.error(
  364. f"❌ 更新机器人名字超时: user_id={user_id}, "
  365. f"耗时={total_duration:.1f}ms"
  366. )
  367. return False
  368. except Exception as e:
  369. total_duration = (time.time() - start_time) * 1000
  370. logger.error(
  371. f"❌ 更新机器人名字失败: user_id={user_id}, "
  372. f"error={e}, 耗时={total_duration:.1f}ms"
  373. )
  374. return False
  375. async def update_speech_duration(self, user_id: str, duration_ms: int) -> Optional[Dict[str, int]]:
  376. """
  377. 更新用户累积说话时长
  378. Args:
  379. user_id: 用户ID
  380. duration_ms: 本次说话时长(毫秒)
  381. Returns:
  382. 包含 total_duration_ms 与 user_level 的字典,失败时返回 None
  383. """
  384. try:
  385. if duration_ms <= 0:
  386. return None # 时长无效,跳过更新
  387. async with db_manager.get_session() as session:
  388. result = await session.execute(
  389. select(User.total_speech_duration_ms, User.user_level)
  390. .where(User.user_id == user_id)
  391. .with_for_update()
  392. )
  393. row = result.one_or_none()
  394. if row is None:
  395. logger.warning(f"⚠️ 更新说话时长失败,未找到用户: user_id={user_id}")
  396. return None
  397. current_total = (row[0] or 0)
  398. current_level = (row[1] or 0)
  399. new_total = current_total + duration_ms
  400. new_level = self._calculate_user_level(new_total)
  401. update_payload: Dict[str, Any] = {
  402. "total_speech_duration_ms": new_total,
  403. }
  404. if new_level != current_level:
  405. update_payload["user_level"] = new_level
  406. await session.execute(
  407. update(User)
  408. .where(User.user_id == user_id)
  409. .values(**update_payload)
  410. )
  411. await session.commit()
  412. logger.info(
  413. f"✅ 更新用户说话时长成功: user_id={user_id}, "
  414. f"duration_ms={duration_ms}, total_ms={new_total}, level={new_level}"
  415. )
  416. return {
  417. "total_duration_ms": new_total,
  418. "user_level": new_level,
  419. }
  420. except Exception as e:
  421. logger.error(f"❌ 更新用户说话时长失败: user_id={user_id}, error={e}")
  422. return None
  423. async def get_user_ui_state(self, user_id: str) -> Optional[Dict[str, Any]]:
  424. """
  425. 获取用户前端可视化状态
  426. """
  427. try:
  428. async with db_manager.get_session() as session:
  429. result = await session.execute(
  430. select(UserUIState).where(UserUIState.user_id == user_id)
  431. )
  432. state = result.scalar_one_or_none()
  433. if state is None:
  434. return None
  435. return state.to_dict()
  436. except Exception as e: # noqa: BLE001
  437. logger.error(f"❌ 获取用户UI状态失败: user_id={user_id}, error={e}")
  438. return None
  439. async def upsert_user_ui_state(
  440. self,
  441. user_id: str,
  442. *,
  443. current_page: Optional[str] = None,
  444. visited_routes: Optional[list[str]] = None,
  445. has_shown_tap_to_chat: Optional[bool] = None,
  446. has_shown_cancel_tooltip: Optional[bool] = None,
  447. has_shown_send_tooltip: Optional[bool] = None,
  448. has_shown_progress_bar_tooltip: Optional[bool] = None,
  449. has_shown_mission_stone_modal: Optional[bool] = None,
  450. has_shown_conversation_stone_modal: Optional[bool] = None,
  451. has_clicked_mission_stone: Optional[bool] = None,
  452. has_clicked_conversation_stone: Optional[bool] = None,
  453. avatar_c_unlocked: Optional[bool] = None,
  454. current_avatar_id: Optional[str] = None,
  455. has_shared_avatar: Optional[bool] = None,
  456. ) -> Optional[Dict[str, Any]]:
  457. """
  458. 创建或更新用户前端可视化状态(部分字段更新)
  459. """
  460. try:
  461. async with db_manager.get_session() as session:
  462. result = await session.execute(
  463. select(UserUIState).where(UserUIState.user_id == user_id).with_for_update()
  464. )
  465. state: Optional[UserUIState] = result.scalar_one_or_none()
  466. if state is None:
  467. # 新建记录,仅写入非空字段
  468. data: Dict[str, Any] = {"user_id": user_id}
  469. if current_page is not None:
  470. data["current_page"] = current_page
  471. if visited_routes is not None:
  472. data["visited_routes"] = visited_routes
  473. if has_shown_tap_to_chat is not None:
  474. data["has_shown_tap_to_chat"] = has_shown_tap_to_chat
  475. if has_shown_cancel_tooltip is not None:
  476. data["has_shown_cancel_tooltip"] = has_shown_cancel_tooltip
  477. if has_shown_send_tooltip is not None:
  478. data["has_shown_send_tooltip"] = has_shown_send_tooltip
  479. if has_shown_progress_bar_tooltip is not None:
  480. data["has_shown_progress_bar_tooltip"] = has_shown_progress_bar_tooltip
  481. if has_shown_mission_stone_modal is not None:
  482. data["has_shown_mission_stone_modal"] = has_shown_mission_stone_modal
  483. if has_shown_conversation_stone_modal is not None:
  484. data["has_shown_conversation_stone_modal"] = has_shown_conversation_stone_modal
  485. if has_clicked_mission_stone is not None:
  486. data["has_clicked_mission_stone"] = has_clicked_mission_stone
  487. if has_clicked_conversation_stone is not None:
  488. data["has_clicked_conversation_stone"] = has_clicked_conversation_stone
  489. if avatar_c_unlocked is not None:
  490. data["avatar_c_unlocked"] = avatar_c_unlocked
  491. if current_avatar_id is not None:
  492. data["current_avatar_id"] = current_avatar_id
  493. if has_shared_avatar is not None:
  494. data["has_shared_avatar"] = has_shared_avatar
  495. stmt = insert(UserUIState).values(**data)
  496. await session.execute(stmt)
  497. else:
  498. # 更新现有记录(仅覆盖非空字段)
  499. if current_page is not None:
  500. state.current_page = current_page
  501. if visited_routes is not None:
  502. state.visited_routes = visited_routes
  503. if has_shown_tap_to_chat is not None:
  504. state.has_shown_tap_to_chat = has_shown_tap_to_chat
  505. if has_shown_cancel_tooltip is not None:
  506. state.has_shown_cancel_tooltip = has_shown_cancel_tooltip
  507. if has_shown_send_tooltip is not None:
  508. state.has_shown_send_tooltip = has_shown_send_tooltip
  509. if has_shown_progress_bar_tooltip is not None:
  510. state.has_shown_progress_bar_tooltip = has_shown_progress_bar_tooltip
  511. if has_shown_mission_stone_modal is not None:
  512. state.has_shown_mission_stone_modal = has_shown_mission_stone_modal
  513. if has_shown_conversation_stone_modal is not None:
  514. state.has_shown_conversation_stone_modal = has_shown_conversation_stone_modal
  515. if has_clicked_mission_stone is not None:
  516. state.has_clicked_mission_stone = has_clicked_mission_stone
  517. if has_clicked_conversation_stone is not None:
  518. state.has_clicked_conversation_stone = has_clicked_conversation_stone
  519. if avatar_c_unlocked is not None:
  520. state.avatar_c_unlocked = avatar_c_unlocked
  521. if current_avatar_id is not None:
  522. state.current_avatar_id = current_avatar_id
  523. if has_shared_avatar is not None:
  524. state.has_shared_avatar = has_shared_avatar
  525. session.add(state)
  526. await session.commit()
  527. # 重新读取最新状态
  528. result = await session.execute(
  529. select(UserUIState).where(UserUIState.user_id == user_id)
  530. )
  531. latest_state = result.scalar_one_or_none()
  532. if latest_state is None:
  533. return None
  534. return latest_state.to_dict()
  535. except Exception as e: # noqa: BLE001
  536. logger.error(f"❌ 更新用户UI状态失败: user_id={user_id}, error={e}")
  537. return None
  538. async def register_with_phone(
  539. self,
  540. phone: str,
  541. username: str = "",
  542. user_id: Optional[str] = None,
  543. area_code: Optional[str] = None,
  544. ) -> Dict:
  545. """
  546. 使用手机号注册
  547. Args:
  548. phone: 手机号
  549. username: 用户名(可选)
  550. user_id: 可选的现有用户ID,如果提供则更新该用户而不是创建新用户
  551. Returns:
  552. 用户信息字典
  553. """
  554. try:
  555. async with db_manager.get_session() as session:
  556. # 检查手机号是否已被其他用户注册
  557. result = await session.execute(
  558. select(User).where(User.phone == phone)
  559. )
  560. existing_phone_user = result.scalar_one_or_none()
  561. if existing_phone_user:
  562. # 如果提供了 user_id,检查是否是同一个用户
  563. if user_id and existing_phone_user.user_id == user_id:
  564. # 同一个用户,可能是重新注册,允许继续
  565. pass
  566. else:
  567. raise ValueError("该手机号已被注册")
  568. # 如果提供了 user_id,使用现有用户
  569. final_user_id = None
  570. if user_id:
  571. result = await session.execute(
  572. select(User).where(User.user_id == user_id)
  573. )
  574. existing_user = result.scalar_one_or_none()
  575. if not existing_user:
  576. raise ValueError(f"用户ID {user_id} 不存在")
  577. if existing_user.user_type == "registered":
  578. raise ValueError("该用户已经注册过了")
  579. # 更新现有用户
  580. if not username or username.strip() == "":
  581. # 如果用户已有用户名,保留;否则生成新用户名
  582. if not existing_user.username or existing_user.username.strip() == "":
  583. username = await self._generate_temp_username(session)
  584. else:
  585. username = existing_user.username
  586. existing_user.phone = phone
  587. if area_code is not None:
  588. existing_user.area_code = area_code
  589. existing_user.username = username
  590. existing_user.user_type = "registered"
  591. existing_user.is_verified = True
  592. existing_user.registered_at = datetime.now()
  593. user = existing_user
  594. final_user_id = user_id
  595. await session.flush()
  596. # 如果用户还没有背景图片,初始化背景图片
  597. if not existing_user.current_background_image_id or existing_user.current_background_image_id == "default":
  598. await self._init_user_background_image(session, user_id)
  599. # 初始化头像(注册用户:默认头像 + 注册赠送头像)
  600. await self._init_user_avatar(session, user_id, user_type="registered")
  601. await session.commit()
  602. await session.refresh(user)
  603. else:
  604. # 创建新用户(原有逻辑)
  605. new_user_id = str(uuid.uuid4())
  606. if not username or username.strip() == "":
  607. username = await self._generate_temp_username(session)
  608. user = User(
  609. user_id=new_user_id,
  610. username=username,
  611. phone=phone,
  612. area_code=area_code,
  613. user_type="registered",
  614. is_verified=True,
  615. registered_at=datetime.now(),
  616. current_background_image_id="default", # 设置默认背景图片
  617. )
  618. session.add(user)
  619. await session.flush() # 先 flush 确保 user 有 ID,但不提交事务
  620. final_user_id = new_user_id
  621. # 初始化背景图片
  622. await self._init_user_background_image(session, new_user_id)
  623. # 初始化头像(注册用户:默认头像 + 注册赠送头像)
  624. await self._init_user_avatar(session, new_user_id, user_type="registered")
  625. await session.commit()
  626. await session.refresh(user)
  627. # 初始化 user_ui_states 的 current_avatar_id(注册用户优先使用注册赠送头像)
  628. final_user_id = final_user_id if final_user_id else user.user_id
  629. await self.upsert_user_ui_state(
  630. user_id=final_user_id,
  631. current_avatar_id=REGISTERED_AVATAR_ID
  632. )
  633. logger.info(f"✅ 手机号注册成功: user_id={final_user_id}, area_code={area_code}, phone={phone}")
  634. return user.to_dict()
  635. except Exception as e:
  636. logger.error(f"❌ 手机号注册失败: {e}")
  637. raise
  638. async def register_with_email(self, email: str, username: str = "", user_id: Optional[str] = None) -> Dict:
  639. """
  640. 使用邮箱注册
  641. Args:
  642. email: 邮箱
  643. username: 用户名(可选)
  644. user_id: 可选的现有用户ID,如果提供则更新该用户而不是创建新用户
  645. Returns:
  646. 用户信息字典
  647. """
  648. try:
  649. async with db_manager.get_session() as session:
  650. # 检查邮箱是否已被其他用户注册
  651. result = await session.execute(
  652. select(User).where(User.email == email)
  653. )
  654. existing_email_user = result.scalar_one_or_none()
  655. if existing_email_user:
  656. # 如果提供了 user_id,检查是否是同一个用户
  657. if user_id and existing_email_user.user_id == user_id:
  658. # 同一个用户,可能是重新注册,允许继续
  659. pass
  660. else:
  661. raise ValueError("该邮箱已被注册")
  662. # 如果提供了 user_id,使用现有用户
  663. final_user_id = None
  664. if user_id:
  665. result = await session.execute(
  666. select(User).where(User.user_id == user_id)
  667. )
  668. existing_user = result.scalar_one_or_none()
  669. if not existing_user:
  670. raise ValueError(f"用户ID {user_id} 不存在")
  671. if existing_user.user_type == "registered":
  672. raise ValueError("该用户已经注册过了")
  673. # 更新现有用户
  674. if not username or username.strip() == "":
  675. # 如果用户已有用户名,保留;否则生成新用户名
  676. if not existing_user.username or existing_user.username.strip() == "":
  677. username = await self._generate_temp_username(session)
  678. else:
  679. username = existing_user.username
  680. existing_user.email = email
  681. existing_user.username = username
  682. existing_user.user_type = "registered"
  683. existing_user.is_verified = True
  684. existing_user.registered_at = datetime.now()
  685. user = existing_user
  686. final_user_id = user_id
  687. await session.flush()
  688. # 如果用户还没有背景图片,初始化背景图片
  689. if not existing_user.current_background_image_id or existing_user.current_background_image_id == "default":
  690. await self._init_user_background_image(session, user_id)
  691. # 初始化头像(注册用户:默认头像 + 注册赠送头像)
  692. await self._init_user_avatar(session, user_id, user_type="registered")
  693. await session.commit()
  694. await session.refresh(user)
  695. else:
  696. # 创建新用户(原有逻辑)
  697. new_user_id = str(uuid.uuid4())
  698. if not username or username.strip() == "":
  699. username = await self._generate_temp_username(session)
  700. user = User(
  701. user_id=new_user_id,
  702. username=username,
  703. email=email,
  704. user_type="registered",
  705. is_verified=True,
  706. registered_at=datetime.now(),
  707. current_background_image_id="default", # 设置默认背景图片
  708. )
  709. session.add(user)
  710. await session.flush() # 先 flush 确保 user 有 ID,但不提交事务
  711. final_user_id = new_user_id
  712. # 初始化背景图片
  713. await self._init_user_background_image(session, new_user_id)
  714. # 初始化头像(注册用户:默认头像 + 注册赠送头像)
  715. await self._init_user_avatar(session, new_user_id, user_type="registered")
  716. await session.commit()
  717. await session.refresh(user)
  718. # 初始化 user_ui_states 的 current_avatar_id(注册用户优先使用注册赠送头像)
  719. final_user_id = final_user_id if final_user_id else user.user_id
  720. await self.upsert_user_ui_state(
  721. user_id=final_user_id,
  722. current_avatar_id=REGISTERED_AVATAR_ID
  723. )
  724. logger.info(f"✅ 邮箱注册成功: user_id={final_user_id}, email={email}")
  725. return user.to_dict()
  726. except Exception as e:
  727. logger.error(f"❌ 邮箱注册失败: {e}")
  728. raise
  729. async def login_with_wechat(self, openid: str, username: str = "") -> Dict:
  730. """
  731. 使用微信OpenID登录/注册
  732. Args:
  733. openid: 微信OpenID
  734. username: 用户名(可选,仅新用户时使用)
  735. Returns:
  736. 用户信息字典
  737. """
  738. try:
  739. async with db_manager.get_session() as session:
  740. # 检查是否已存在
  741. result = await session.execute(
  742. select(User).where(User.wechat_openid == openid)
  743. )
  744. existing_user = result.scalar_one_or_none()
  745. if existing_user:
  746. # 已存在,直接返回
  747. logger.info(f"✅ 微信登录成功: user_id={existing_user.user_id}, openid={openid}")
  748. return existing_user.to_dict()
  749. # 新用户,创建账号
  750. user_id = str(uuid.uuid4())
  751. if not username or username.strip() == "":
  752. username = await self._generate_temp_username(session)
  753. user = User(
  754. user_id=user_id,
  755. username=username,
  756. wechat_openid=openid,
  757. user_type="registered",
  758. is_verified=True,
  759. registered_at=datetime.now(),
  760. current_background_image_id="default", # 设置默认背景图片
  761. )
  762. session.add(user)
  763. await session.flush() # 先 flush 确保 user 有 ID,但不提交事务
  764. # 初始化背景图片
  765. await self._init_user_background_image(session, user_id)
  766. # 初始化头像(注册用户:默认头像 + 注册赠送头像)
  767. await self._init_user_avatar(session, user_id, user_type="registered")
  768. await session.commit()
  769. await session.refresh(user)
  770. # 初始化 user_ui_states 的 current_avatar_id(注册用户优先使用注册赠送头像)
  771. await self.upsert_user_ui_state(
  772. user_id=user_id,
  773. current_avatar_id=REGISTERED_AVATAR_ID
  774. )
  775. logger.info(f"✅ 微信注册成功: user_id={user_id}, openid={openid}")
  776. return user.to_dict()
  777. except Exception as e:
  778. logger.error(f"❌ 微信登录/注册失败: {e}")
  779. raise
  780. async def login_with_qq(
  781. self,
  782. openid: str,
  783. username: str = "",
  784. unionid: Optional[str] = None,
  785. ) -> Dict:
  786. """
  787. 使用QQ OpenID登录/注册
  788. Args:
  789. openid: QQ OpenID
  790. username: 用户名(可选,仅新用户时使用)
  791. Returns:
  792. 用户信息字典
  793. """
  794. try:
  795. async with db_manager.get_session() as session:
  796. # 优先用 openid 查询
  797. result = await session.execute(
  798. select(User).where(User.qq_openid == openid)
  799. )
  800. existing_user = result.scalar_one_or_none()
  801. # 如果未命中且拿到了 unionid,则尝试用 unionid 查询
  802. if not existing_user and unionid:
  803. union_result = await session.execute(
  804. select(User).where(User.qq_unionid == unionid)
  805. )
  806. existing_user = union_result.scalar_one_or_none()
  807. # 若命中用户还未绑定 openid,则补充绑定
  808. if existing_user and not existing_user.qq_openid:
  809. existing_user.qq_openid = openid
  810. session.add(existing_user)
  811. await session.commit()
  812. await session.refresh(existing_user)
  813. logger.info(
  814. f"🔁 已根据 unionid={unionid} 绑定新的 QQ openid={openid}"
  815. )
  816. if existing_user:
  817. updated = False
  818. if unionid and not existing_user.qq_unionid:
  819. existing_user.qq_unionid = unionid
  820. updated = True
  821. if updated:
  822. session.add(existing_user)
  823. await session.commit()
  824. await session.refresh(existing_user)
  825. logger.info(
  826. f"✅ QQ登录成功: user_id={existing_user.user_id}, "
  827. f"openid={openid}, unionid={unionid}"
  828. )
  829. return existing_user.to_dict()
  830. # 新用户,创建账号
  831. user_id = str(uuid.uuid4())
  832. if not username or username.strip() == "":
  833. username = await self._generate_temp_username(session)
  834. user = User(
  835. user_id=user_id,
  836. username=username,
  837. qq_openid=openid,
  838. qq_unionid=unionid,
  839. user_type="registered",
  840. is_verified=True,
  841. registered_at=datetime.now(),
  842. current_background_image_id="default", # 设置默认背景图片
  843. )
  844. session.add(user)
  845. await session.flush() # 先 flush 确保 user 有 ID,但不提交事务
  846. # 初始化背景图片
  847. await self._init_user_background_image(session, user_id)
  848. # 初始化头像(注册用户:默认头像 + 注册赠送头像)
  849. await self._init_user_avatar(session, user_id, user_type="registered")
  850. await session.commit()
  851. await session.refresh(user)
  852. # 初始化 user_ui_states 的 current_avatar_id(注册用户优先使用注册赠送头像)
  853. await self.upsert_user_ui_state(
  854. user_id=user_id,
  855. current_avatar_id=REGISTERED_AVATAR_ID
  856. )
  857. logger.info(
  858. f"✅ QQ注册成功: user_id={user_id}, openid={openid}, unionid={unionid}"
  859. )
  860. return user.to_dict()
  861. except Exception as e:
  862. logger.error(f"❌ QQ登录/注册失败: {e}")
  863. raise
  864. async def login_with_phone(self, phone: str, area_code: Optional[str] = None) -> Optional[Dict]:
  865. """
  866. 使用手机号登录
  867. Args:
  868. phone: 手机号
  869. Returns:
  870. 用户信息字典,如果不存在返回None
  871. """
  872. try:
  873. async with db_manager.get_session() as session:
  874. result = await session.execute(
  875. select(User).where(
  876. User.phone == phone,
  877. *( [User.area_code == area_code] if area_code else [] )
  878. )
  879. )
  880. user = result.scalar_one_or_none()
  881. # 兼容旧数据:未存区号时再用空区号回退查询
  882. if not user and area_code:
  883. fallback_result = await session.execute(
  884. select(User).where(User.phone == phone, User.area_code.is_(None))
  885. )
  886. user = fallback_result.scalar_one_or_none()
  887. if user:
  888. logger.info(f"✅ 手机号登录成功: user_id={user.user_id}, area_code={area_code}, phone={phone}")
  889. return user.to_dict()
  890. return None
  891. except Exception as e:
  892. logger.error(f"❌ 手机号登录失败: {e}")
  893. return None
  894. async def login_with_email(self, email: str) -> Optional[Dict]:
  895. """
  896. 使用邮箱登录
  897. Args:
  898. email: 邮箱
  899. Returns:
  900. 用户信息字典,如果不存在返回None
  901. """
  902. try:
  903. async with db_manager.get_session() as session:
  904. result = await session.execute(
  905. select(User).where(User.email == email)
  906. )
  907. user = result.scalar_one_or_none()
  908. if user:
  909. logger.info(f"✅ 邮箱登录成功: user_id={user.user_id}, email={email}")
  910. return user.to_dict()
  911. return None
  912. except Exception as e:
  913. logger.error(f"❌ 邮箱登录失败: {e}")
  914. return None
  915. async def check_user_exists_by_qq_openid(self, openid: str, unionid: Optional[str] = None) -> bool:
  916. """
  917. 检查QQ OpenID对应的用户是否存在
  918. Args:
  919. openid: QQ OpenID
  920. unionid: QQ UnionID(可选)
  921. Returns:
  922. 用户是否存在
  923. """
  924. try:
  925. async with db_manager.get_session() as session:
  926. # 优先用 openid 查询
  927. result = await session.execute(
  928. select(User).where(User.qq_openid == openid)
  929. )
  930. existing_user = result.scalar_one_or_none()
  931. # 如果未命中且拿到了 unionid,则尝试用 unionid 查询
  932. if not existing_user and unionid:
  933. union_result = await session.execute(
  934. select(User).where(User.qq_unionid == unionid)
  935. )
  936. existing_user = union_result.scalar_one_or_none()
  937. return existing_user is not None
  938. except Exception as e:
  939. logger.error(f"❌ 检查QQ用户是否存在失败: {e}")
  940. return False
  941. # 全局用户服务实例
  942. user_service = UserService()