user_service.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806
  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
  14. logger = logging.getLogger(__name__)
  15. LEVEL_THRESHOLDS_MS: Tuple[Tuple[int, int], ...] = (
  16. (5, 240_000),
  17. (4, 120_000),
  18. (3, 60_000),
  19. (2, 30_000),
  20. (1, 10_000),
  21. )
  22. class UserService:
  23. """用户管理服务"""
  24. @staticmethod
  25. def _calculate_user_level(total_ms: int) -> int:
  26. """
  27. 根据累计说话时长计算用户等级
  28. """
  29. for level, threshold in LEVEL_THRESHOLDS_MS:
  30. if total_ms >= threshold:
  31. return level
  32. return 0
  33. async def _init_user_background_image(self, session: AsyncSession, user_id: str) -> None:
  34. """
  35. 初始化用户背景图片(添加到已获得列表)
  36. Args:
  37. session: 数据库会话
  38. user_id: 用户ID
  39. """
  40. try:
  41. default_image_id = "default"
  42. # 检查是否已存在,避免重复添加
  43. result = await session.execute(
  44. select(UserBackgroundImage).where(
  45. UserBackgroundImage.user_id == user_id,
  46. UserBackgroundImage.image_id == default_image_id
  47. )
  48. )
  49. existing = result.scalar_one_or_none()
  50. if not existing:
  51. user_bg_image = UserBackgroundImage(
  52. user_id=user_id,
  53. image_id=default_image_id,
  54. obtained_type="default",
  55. obtained_source="system_init"
  56. )
  57. session.add(user_bg_image)
  58. logger.debug(f"✅ 为用户初始化背景图片: user_id={user_id}, image_id={default_image_id}")
  59. else:
  60. logger.debug(f"⏭️ 用户已拥有默认背景图片: user_id={user_id}")
  61. except Exception as e:
  62. # 背景图片初始化失败不应该影响用户创建,只记录日志
  63. logger.warning(f"⚠️ 初始化用户背景图片失败: user_id={user_id}, error={e}")
  64. async def _generate_temp_username(self, session: AsyncSession) -> str:
  65. """
  66. 生成临时用户名(格式:User + 7位随机数字)
  67. 确保用户名不重复
  68. Args:
  69. session: 数据库会话
  70. Returns:
  71. 临时用户名
  72. """
  73. max_attempts = 100 # 最大重试次数,避免无限循环
  74. for attempt in range(max_attempts):
  75. # 生成7位随机数字(1000000-9999999)
  76. random_number = random.randint(1000000, 9999999)
  77. temp_username = f"User{random_number}"
  78. # 检查用户名是否已存在
  79. result = await session.execute(
  80. select(User).where(User.username == temp_username)
  81. )
  82. existing_user = result.scalar_one_or_none()
  83. if not existing_user:
  84. logger.debug(f"生成临时用户名: {temp_username} (尝试次数: {attempt + 1})")
  85. return temp_username
  86. # 如果100次都重复,使用时间戳的后7位作为备选方案
  87. # 使用当前时间戳的毫秒数取模,确保是7位数字
  88. for fallback_attempt in range(10): # 备选方案也最多尝试10次
  89. timestamp_suffix = int(datetime.now().timestamp() * 1000) % 10000000
  90. # 确保至少是7位数字(如果不足7位,前面补0)
  91. fallback_number = str(timestamp_suffix).zfill(7)
  92. temp_username = f"User{fallback_number}"
  93. # 再次检查是否重复
  94. result = await session.execute(
  95. select(User).where(User.username == temp_username)
  96. )
  97. existing_user = result.scalar_one_or_none()
  98. if not existing_user:
  99. logger.warning(f"⚠️ 临时用户名生成达到最大重试次数,使用备选方案: {temp_username}")
  100. return temp_username
  101. # 如果还是重复,等待1毫秒后重试(确保时间戳变化)
  102. await asyncio.sleep(0.001)
  103. # 最后的备选方案:使用UUID的十六进制转数字(取后7位)
  104. uuid_hex = uuid.uuid4().hex
  105. final_number = str(int(uuid_hex[:8], 16) % 10000000).zfill(7)
  106. temp_username = f"User{final_number}"
  107. logger.error(f"❌ 临时用户名生成异常,使用最终备选方案: {temp_username}")
  108. return temp_username
  109. async def create_guest_user(self, username: str = "", bot_name: Optional[str] = None) -> Dict:
  110. """
  111. 创建游客用户
  112. Args:
  113. username: 用户名(默认为空字符串,为空时自动生成临时用户名)
  114. bot_name: 用户给机器人取的名字(可选)
  115. Returns:
  116. 用户信息字典(包含user_id)
  117. """
  118. try:
  119. # 生成唯一用户ID
  120. user_id = str(uuid.uuid4())
  121. # 使用 ORM 创建用户
  122. async with db_manager.get_session() as session:
  123. # 如果用户名为空,生成临时用户名
  124. if not username or username.strip() == "":
  125. username = await self._generate_temp_username(session)
  126. logger.info(f"🔧 自动生成临时用户名: {username}")
  127. user = User(
  128. user_id=user_id,
  129. username=username,
  130. user_type="guest",
  131. bot_name=bot_name,
  132. current_background_image_id="default", # 设置默认背景图片
  133. )
  134. session.add(user)
  135. await session.flush() # 先 flush 确保 user 有 ID,但不提交事务
  136. # 初始化背景图片
  137. await self._init_user_background_image(session, user_id)
  138. await session.commit()
  139. await session.refresh(user)
  140. logger.info(f"✅ 创建游客用户成功: user_id={user_id}, username={username}, bot_name={bot_name}")
  141. return {
  142. "user_id": user_id,
  143. "username": username,
  144. "user_type": "guest",
  145. "bot_name": bot_name,
  146. "user_level": 0,
  147. "current_background_image_id": "default",
  148. "created_at": datetime.now().isoformat()
  149. }
  150. except Exception as e:
  151. logger.error(f"❌ 创建用户失败: {e}")
  152. raise
  153. async def get_user(self, user_id: str) -> Optional[Dict]:
  154. """
  155. 获取用户信息
  156. Args:
  157. user_id: 用户ID
  158. Returns:
  159. 用户信息字典,如果不存在返回None
  160. 包含 current_background_image_metadata 字段(背景图片的metadata配置)
  161. """
  162. try:
  163. async with db_manager.get_session() as session:
  164. result = await session.execute(
  165. select(User).where(User.user_id == user_id)
  166. )
  167. user = result.scalar_one_or_none()
  168. if user:
  169. user_dict = user.to_dict()
  170. # 如果用户有背景图片ID,查询背景图片的metadata
  171. if user.current_background_image_id:
  172. try:
  173. bg_result = await session.execute(
  174. select(BackgroundImage).where(
  175. BackgroundImage.image_id == user.current_background_image_id
  176. )
  177. )
  178. bg_image = bg_result.scalar_one_or_none()
  179. if bg_image and bg_image.metadata_json:
  180. user_dict["current_background_image_metadata"] = bg_image.metadata_json
  181. except Exception as e:
  182. logger.warning(f"⚠️ 获取背景图片metadata失败: image_id={user.current_background_image_id}, error={e}")
  183. return user_dict
  184. return None
  185. except Exception as e:
  186. logger.error(f"❌ 获取用户信息失败: user_id={user_id}, error={e}")
  187. return None
  188. async def update_user_last_active(self, user_id: str):
  189. """
  190. 更新用户最后活跃时间
  191. Args:
  192. user_id: 用户ID
  193. """
  194. try:
  195. async with db_manager.get_session() as session:
  196. await session.execute(
  197. update(User)
  198. .where(User.user_id == user_id)
  199. .values(last_active_at=func.now())
  200. )
  201. await session.commit()
  202. except Exception as e:
  203. logger.warning(f"⚠️ 更新用户活跃时间失败: user_id={user_id}, error={e}")
  204. async def update_username(self, user_id: str, username: str) -> bool:
  205. """
  206. 更新用户名
  207. Args:
  208. user_id: 用户ID
  209. username: 用户名
  210. Returns:
  211. 是否更新成功
  212. """
  213. try:
  214. async with db_manager.get_session() as session:
  215. result = await session.execute(
  216. update(User)
  217. .where(User.user_id == user_id)
  218. .values(username=username)
  219. )
  220. await session.commit()
  221. if result.rowcount > 0:
  222. logger.info(f"✅ 更新用户名成功: user_id={user_id}, username={username}")
  223. return True
  224. return False
  225. except Exception as e:
  226. logger.error(f"❌ 更新用户名失败: user_id={user_id}, error={e}")
  227. return False
  228. async def update_bot_name(self, user_id: str, bot_name: str) -> bool:
  229. """
  230. 更新用户给机器人取的名字
  231. Args:
  232. user_id: 用户ID
  233. bot_name: 机器人名字
  234. Returns:
  235. 是否更新成功
  236. """
  237. import time
  238. start_time = time.time()
  239. try:
  240. async with db_manager.get_session() as session:
  241. # 🔥 先查询当前值,避免不必要的更新
  242. query_start = time.time()
  243. result = await session.execute(
  244. select(User.bot_name).where(User.user_id == user_id)
  245. )
  246. current_bot_name = result.scalar_one_or_none()
  247. query_duration = (time.time() - query_start) * 1000
  248. # 如果值相同,直接返回(避免不必要的 UPDATE)
  249. if current_bot_name == bot_name:
  250. total_duration = (time.time() - start_time) * 1000
  251. logger.info(
  252. f"⏭️ 机器人名字未变化,跳过更新: user_id={user_id}, "
  253. f"bot_name={bot_name}, 耗时={total_duration:.1f}ms"
  254. )
  255. return True
  256. # 执行更新
  257. update_start = time.time()
  258. update_result = await session.execute(
  259. update(User)
  260. .where(User.user_id == user_id)
  261. .values(bot_name=bot_name)
  262. )
  263. await session.commit()
  264. update_duration = (time.time() - update_start) * 1000
  265. total_duration = (time.time() - start_time) * 1000
  266. if update_result.rowcount > 0:
  267. logger.info(
  268. f"✅ 更新机器人名字成功: user_id={user_id}, "
  269. f"bot_name={bot_name}, "
  270. f"查询耗时={query_duration:.1f}ms, "
  271. f"更新耗时={update_duration:.1f}ms, "
  272. f"总耗时={total_duration:.1f}ms"
  273. )
  274. return True
  275. else:
  276. logger.warning(
  277. f"⚠️ 更新机器人名字失败: 未找到用户 user_id={user_id}, "
  278. f"总耗时={total_duration:.1f}ms"
  279. )
  280. return False
  281. except asyncio.TimeoutError:
  282. total_duration = (time.time() - start_time) * 1000
  283. logger.error(
  284. f"❌ 更新机器人名字超时: user_id={user_id}, "
  285. f"耗时={total_duration:.1f}ms"
  286. )
  287. return False
  288. except Exception as e:
  289. total_duration = (time.time() - start_time) * 1000
  290. logger.error(
  291. f"❌ 更新机器人名字失败: user_id={user_id}, "
  292. f"error={e}, 耗时={total_duration:.1f}ms"
  293. )
  294. return False
  295. async def update_speech_duration(self, user_id: str, duration_ms: int) -> Optional[Dict[str, int]]:
  296. """
  297. 更新用户累积说话时长
  298. Args:
  299. user_id: 用户ID
  300. duration_ms: 本次说话时长(毫秒)
  301. Returns:
  302. 包含 total_duration_ms 与 user_level 的字典,失败时返回 None
  303. """
  304. try:
  305. if duration_ms <= 0:
  306. return None # 时长无效,跳过更新
  307. async with db_manager.get_session() as session:
  308. result = await session.execute(
  309. select(User.total_speech_duration_ms, User.user_level)
  310. .where(User.user_id == user_id)
  311. .with_for_update()
  312. )
  313. row = result.one_or_none()
  314. if row is None:
  315. logger.warning(f"⚠️ 更新说话时长失败,未找到用户: user_id={user_id}")
  316. return None
  317. current_total = (row[0] or 0)
  318. current_level = (row[1] or 0)
  319. new_total = current_total + duration_ms
  320. new_level = self._calculate_user_level(new_total)
  321. update_payload: Dict[str, Any] = {
  322. "total_speech_duration_ms": new_total,
  323. }
  324. if new_level != current_level:
  325. update_payload["user_level"] = new_level
  326. await session.execute(
  327. update(User)
  328. .where(User.user_id == user_id)
  329. .values(**update_payload)
  330. )
  331. await session.commit()
  332. logger.info(
  333. f"✅ 更新用户说话时长成功: user_id={user_id}, "
  334. f"duration_ms={duration_ms}, total_ms={new_total}, level={new_level}"
  335. )
  336. return {
  337. "total_duration_ms": new_total,
  338. "user_level": new_level,
  339. }
  340. except Exception as e:
  341. logger.error(f"❌ 更新用户说话时长失败: user_id={user_id}, error={e}")
  342. return None
  343. async def get_user_ui_state(self, user_id: str) -> Optional[Dict[str, Any]]:
  344. """
  345. 获取用户前端可视化状态
  346. """
  347. try:
  348. async with db_manager.get_session() as session:
  349. result = await session.execute(
  350. select(UserUIState).where(UserUIState.user_id == user_id)
  351. )
  352. state = result.scalar_one_or_none()
  353. if state is None:
  354. return None
  355. return state.to_dict()
  356. except Exception as e: # noqa: BLE001
  357. logger.error(f"❌ 获取用户UI状态失败: user_id={user_id}, error={e}")
  358. return None
  359. async def upsert_user_ui_state(
  360. self,
  361. user_id: str,
  362. *,
  363. current_page: Optional[str] = None,
  364. visited_routes: Optional[list[str]] = None,
  365. has_shown_tap_to_chat: Optional[bool] = None,
  366. has_shown_cancel_tooltip: Optional[bool] = None,
  367. has_shown_send_tooltip: Optional[bool] = None,
  368. has_shown_progress_bar_tooltip: Optional[bool] = None,
  369. has_shown_mission_stone_modal: Optional[bool] = None,
  370. has_shown_conversation_stone_modal: Optional[bool] = None,
  371. avatar_c_unlocked: Optional[bool] = None,
  372. has_shared_avatar: Optional[bool] = None,
  373. ) -> Optional[Dict[str, Any]]:
  374. """
  375. 创建或更新用户前端可视化状态(部分字段更新)
  376. """
  377. try:
  378. async with db_manager.get_session() as session:
  379. result = await session.execute(
  380. select(UserUIState).where(UserUIState.user_id == user_id).with_for_update()
  381. )
  382. state: Optional[UserUIState] = result.scalar_one_or_none()
  383. if state is None:
  384. # 新建记录,仅写入非空字段
  385. data: Dict[str, Any] = {"user_id": user_id}
  386. if current_page is not None:
  387. data["current_page"] = current_page
  388. if visited_routes is not None:
  389. data["visited_routes"] = visited_routes
  390. if has_shown_tap_to_chat is not None:
  391. data["has_shown_tap_to_chat"] = has_shown_tap_to_chat
  392. if has_shown_cancel_tooltip is not None:
  393. data["has_shown_cancel_tooltip"] = has_shown_cancel_tooltip
  394. if has_shown_send_tooltip is not None:
  395. data["has_shown_send_tooltip"] = has_shown_send_tooltip
  396. if has_shown_progress_bar_tooltip is not None:
  397. data["has_shown_progress_bar_tooltip"] = has_shown_progress_bar_tooltip
  398. if has_shown_mission_stone_modal is not None:
  399. data["has_shown_mission_stone_modal"] = has_shown_mission_stone_modal
  400. if has_shown_conversation_stone_modal is not None:
  401. data["has_shown_conversation_stone_modal"] = has_shown_conversation_stone_modal
  402. if avatar_c_unlocked is not None:
  403. data["avatar_c_unlocked"] = avatar_c_unlocked
  404. if has_shared_avatar is not None:
  405. data["has_shared_avatar"] = has_shared_avatar
  406. stmt = insert(UserUIState).values(**data)
  407. await session.execute(stmt)
  408. else:
  409. # 更新现有记录(仅覆盖非空字段)
  410. if current_page is not None:
  411. state.current_page = current_page
  412. if visited_routes is not None:
  413. state.visited_routes = visited_routes
  414. if has_shown_tap_to_chat is not None:
  415. state.has_shown_tap_to_chat = has_shown_tap_to_chat
  416. if has_shown_cancel_tooltip is not None:
  417. state.has_shown_cancel_tooltip = has_shown_cancel_tooltip
  418. if has_shown_send_tooltip is not None:
  419. state.has_shown_send_tooltip = has_shown_send_tooltip
  420. if has_shown_progress_bar_tooltip is not None:
  421. state.has_shown_progress_bar_tooltip = has_shown_progress_bar_tooltip
  422. if has_shown_mission_stone_modal is not None:
  423. state.has_shown_mission_stone_modal = has_shown_mission_stone_modal
  424. if has_shown_conversation_stone_modal is not None:
  425. state.has_shown_conversation_stone_modal = has_shown_conversation_stone_modal
  426. if avatar_c_unlocked is not None:
  427. state.avatar_c_unlocked = avatar_c_unlocked
  428. if has_shared_avatar is not None:
  429. state.has_shared_avatar = has_shared_avatar
  430. session.add(state)
  431. await session.commit()
  432. # 重新读取最新状态
  433. result = await session.execute(
  434. select(UserUIState).where(UserUIState.user_id == user_id)
  435. )
  436. latest_state = result.scalar_one_or_none()
  437. if latest_state is None:
  438. return None
  439. return latest_state.to_dict()
  440. except Exception as e: # noqa: BLE001
  441. logger.error(f"❌ 更新用户UI状态失败: user_id={user_id}, error={e}")
  442. return None
  443. async def register_with_phone(self, phone: str, username: str = "") -> Dict:
  444. """
  445. 使用手机号注册
  446. Args:
  447. phone: 手机号
  448. username: 用户名(可选)
  449. Returns:
  450. 用户信息字典
  451. """
  452. try:
  453. async with db_manager.get_session() as session:
  454. # 检查手机号是否已注册
  455. result = await session.execute(
  456. select(User).where(User.phone == phone)
  457. )
  458. existing_user = result.scalar_one_or_none()
  459. if existing_user:
  460. raise ValueError("该手机号已被注册")
  461. # 生成用户ID和用户名
  462. user_id = str(uuid.uuid4())
  463. if not username or username.strip() == "":
  464. username = await self._generate_temp_username(session)
  465. user = User(
  466. user_id=user_id,
  467. username=username,
  468. phone=phone,
  469. user_type="registered",
  470. is_verified=True,
  471. registered_at=datetime.now(),
  472. current_background_image_id="default", # 设置默认背景图片
  473. )
  474. session.add(user)
  475. await session.flush() # 先 flush 确保 user 有 ID,但不提交事务
  476. # 初始化背景图片
  477. await self._init_user_background_image(session, user_id)
  478. await session.commit()
  479. await session.refresh(user)
  480. logger.info(f"✅ 手机号注册成功: user_id={user_id}, phone={phone}")
  481. return user.to_dict()
  482. except Exception as e:
  483. logger.error(f"❌ 手机号注册失败: {e}")
  484. raise
  485. async def register_with_email(self, email: str, username: str = "") -> Dict:
  486. """
  487. 使用邮箱注册
  488. Args:
  489. email: 邮箱
  490. username: 用户名(可选)
  491. Returns:
  492. 用户信息字典
  493. """
  494. try:
  495. async with db_manager.get_session() as session:
  496. # 检查邮箱是否已注册
  497. result = await session.execute(
  498. select(User).where(User.email == email)
  499. )
  500. existing_user = result.scalar_one_or_none()
  501. if existing_user:
  502. raise ValueError("该邮箱已被注册")
  503. # 生成用户ID和用户名
  504. user_id = str(uuid.uuid4())
  505. if not username or username.strip() == "":
  506. username = await self._generate_temp_username(session)
  507. user = User(
  508. user_id=user_id,
  509. username=username,
  510. email=email,
  511. user_type="registered",
  512. is_verified=True,
  513. registered_at=datetime.now(),
  514. current_background_image_id="default", # 设置默认背景图片
  515. )
  516. session.add(user)
  517. await session.flush() # 先 flush 确保 user 有 ID,但不提交事务
  518. # 初始化背景图片
  519. await self._init_user_background_image(session, user_id)
  520. await session.commit()
  521. await session.refresh(user)
  522. logger.info(f"✅ 邮箱注册成功: user_id={user_id}, email={email}")
  523. return user.to_dict()
  524. except Exception as e:
  525. logger.error(f"❌ 邮箱注册失败: {e}")
  526. raise
  527. async def login_with_wechat(self, openid: str, username: str = "") -> Dict:
  528. """
  529. 使用微信OpenID登录/注册
  530. Args:
  531. openid: 微信OpenID
  532. username: 用户名(可选,仅新用户时使用)
  533. Returns:
  534. 用户信息字典
  535. """
  536. try:
  537. async with db_manager.get_session() as session:
  538. # 检查是否已存在
  539. result = await session.execute(
  540. select(User).where(User.wechat_openid == openid)
  541. )
  542. existing_user = result.scalar_one_or_none()
  543. if existing_user:
  544. # 已存在,直接返回
  545. logger.info(f"✅ 微信登录成功: user_id={existing_user.user_id}, openid={openid}")
  546. return existing_user.to_dict()
  547. # 新用户,创建账号
  548. user_id = str(uuid.uuid4())
  549. if not username or username.strip() == "":
  550. username = await self._generate_temp_username(session)
  551. user = User(
  552. user_id=user_id,
  553. username=username,
  554. wechat_openid=openid,
  555. user_type="registered",
  556. is_verified=True,
  557. registered_at=datetime.now(),
  558. current_background_image_id="default", # 设置默认背景图片
  559. )
  560. session.add(user)
  561. await session.flush() # 先 flush 确保 user 有 ID,但不提交事务
  562. # 初始化背景图片
  563. await self._init_user_background_image(session, user_id)
  564. await session.commit()
  565. await session.refresh(user)
  566. logger.info(f"✅ 微信注册成功: user_id={user_id}, openid={openid}")
  567. return user.to_dict()
  568. except Exception as e:
  569. logger.error(f"❌ 微信登录/注册失败: {e}")
  570. raise
  571. async def login_with_qq(
  572. self,
  573. openid: str,
  574. username: str = "",
  575. unionid: Optional[str] = None,
  576. ) -> Dict:
  577. """
  578. 使用QQ OpenID登录/注册
  579. Args:
  580. openid: QQ OpenID
  581. username: 用户名(可选,仅新用户时使用)
  582. Returns:
  583. 用户信息字典
  584. """
  585. try:
  586. async with db_manager.get_session() as session:
  587. # 优先用 openid 查询
  588. result = await session.execute(
  589. select(User).where(User.qq_openid == openid)
  590. )
  591. existing_user = result.scalar_one_or_none()
  592. # 如果未命中且拿到了 unionid,则尝试用 unionid 查询
  593. if not existing_user and unionid:
  594. union_result = await session.execute(
  595. select(User).where(User.qq_unionid == unionid)
  596. )
  597. existing_user = union_result.scalar_one_or_none()
  598. # 若命中用户还未绑定 openid,则补充绑定
  599. if existing_user and not existing_user.qq_openid:
  600. existing_user.qq_openid = openid
  601. session.add(existing_user)
  602. await session.commit()
  603. await session.refresh(existing_user)
  604. logger.info(
  605. f"🔁 已根据 unionid={unionid} 绑定新的 QQ openid={openid}"
  606. )
  607. if existing_user:
  608. updated = False
  609. if unionid and not existing_user.qq_unionid:
  610. existing_user.qq_unionid = unionid
  611. updated = True
  612. if updated:
  613. session.add(existing_user)
  614. await session.commit()
  615. await session.refresh(existing_user)
  616. logger.info(
  617. f"✅ QQ登录成功: user_id={existing_user.user_id}, "
  618. f"openid={openid}, unionid={unionid}"
  619. )
  620. return existing_user.to_dict()
  621. # 新用户,创建账号
  622. user_id = str(uuid.uuid4())
  623. if not username or username.strip() == "":
  624. username = await self._generate_temp_username(session)
  625. user = User(
  626. user_id=user_id,
  627. username=username,
  628. qq_openid=openid,
  629. qq_unionid=unionid,
  630. user_type="registered",
  631. is_verified=True,
  632. registered_at=datetime.now(),
  633. current_background_image_id="default", # 设置默认背景图片
  634. )
  635. session.add(user)
  636. await session.flush() # 先 flush 确保 user 有 ID,但不提交事务
  637. # 初始化背景图片
  638. await self._init_user_background_image(session, user_id)
  639. await session.commit()
  640. await session.refresh(user)
  641. logger.info(
  642. f"✅ QQ注册成功: user_id={user_id}, openid={openid}, unionid={unionid}"
  643. )
  644. return user.to_dict()
  645. except Exception as e:
  646. logger.error(f"❌ QQ登录/注册失败: {e}")
  647. raise
  648. async def login_with_phone(self, phone: str) -> Optional[Dict]:
  649. """
  650. 使用手机号登录
  651. Args:
  652. phone: 手机号
  653. Returns:
  654. 用户信息字典,如果不存在返回None
  655. """
  656. try:
  657. async with db_manager.get_session() as session:
  658. result = await session.execute(
  659. select(User).where(User.phone == phone)
  660. )
  661. user = result.scalar_one_or_none()
  662. if user:
  663. logger.info(f"✅ 手机号登录成功: user_id={user.user_id}, phone={phone}")
  664. return user.to_dict()
  665. return None
  666. except Exception as e:
  667. logger.error(f"❌ 手机号登录失败: {e}")
  668. return None
  669. async def login_with_email(self, email: str) -> Optional[Dict]:
  670. """
  671. 使用邮箱登录
  672. Args:
  673. email: 邮箱
  674. Returns:
  675. 用户信息字典,如果不存在返回None
  676. """
  677. try:
  678. async with db_manager.get_session() as session:
  679. result = await session.execute(
  680. select(User).where(User.email == email)
  681. )
  682. user = result.scalar_one_or_none()
  683. if user:
  684. logger.info(f"✅ 邮箱登录成功: user_id={user.user_id}, email={email}")
  685. return user.to_dict()
  686. return None
  687. except Exception as e:
  688. logger.error(f"❌ 邮箱登录失败: {e}")
  689. return None
  690. # 全局用户服务实例
  691. user_service = UserService()