message_service.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. """
  2. 消息存储服务(SQLAlchemy ORM 版本)
  3. """
  4. import uuid
  5. import json
  6. import asyncio
  7. from datetime import datetime
  8. from typing import Awaitable, Callable, Dict, Optional
  9. import logging
  10. from sqlalchemy import select, update, func
  11. from sqlalchemy.orm import selectinload
  12. from database import db_manager
  13. from models_db import Message, User
  14. import redis.asyncio as aioredis
  15. logger = logging.getLogger(__name__)
  16. class MessageService:
  17. """消息存储服务"""
  18. def __init__(self, db_manager=None):
  19. """
  20. 初始化消息服务
  21. Args:
  22. db_manager: 数据库管理器实例(可选,用于延迟初始化)
  23. """
  24. self.db = db_manager
  25. self.redis = None
  26. self._redis_provider: Optional[Callable[[], Awaitable[aioredis.Redis]]] = None
  27. if db_manager:
  28. # 🔥 延迟获取Redis(在首次使用时)
  29. self._redis_provider = db_manager.get_redis
  30. async def set_db_manager(self, db_manager):
  31. """设置数据库管理器(用于延迟初始化)"""
  32. self.db = db_manager
  33. self._redis_provider = db_manager.get_redis
  34. self.redis = None
  35. async def _ensure_redis(self):
  36. """确保Redis连接可用"""
  37. if not self._redis_provider:
  38. if not self.db:
  39. raise RuntimeError("Redis 未初始化,缺少数据库管理器")
  40. self._redis_provider = self.db.get_redis
  41. redis_client = await self._redis_provider()
  42. if not redis_client:
  43. raise RuntimeError("Redis 客户端不可用")
  44. self.redis = redis_client
  45. return redis_client
  46. async def save_message(
  47. self,
  48. user_id: str,
  49. role: str,
  50. content: str,
  51. emotion: Optional[str] = None,
  52. metadata: Optional[dict] = None
  53. ) -> Dict:
  54. """
  55. 保存消息到MySQL和Redis
  56. Args:
  57. user_id: 用户ID
  58. role: 角色 ('user', 'assistant', 'system')
  59. content: 消息内容
  60. emotion: 情感标签(可选)
  61. metadata: 扩展信息(可选)
  62. Returns:
  63. 消息信息(包含round_number)
  64. """
  65. try:
  66. # 🔥 确保Redis连接可用
  67. await self._ensure_redis()
  68. # 1. 获取当前轮次
  69. logger.debug(f"📥 [保存消息] 步骤1: 获取当前轮次")
  70. current_round = await self._get_or_init_round(user_id, role)
  71. # 2. 生成消息ID
  72. logger.debug(f"📥 [保存消息] 步骤2: 生成消息ID")
  73. message_id = str(uuid.uuid4())
  74. logger.debug(f"📥 [保存消息] 消息ID: {message_id}")
  75. # 3. 保存到MySQL(使用 ORM)
  76. logger.debug(f"📥 [保存消息] 步骤3: 保存到MySQL")
  77. async with db_manager.get_session() as session:
  78. message = Message(
  79. message_id=message_id,
  80. user_id=user_id,
  81. role=role,
  82. content=content,
  83. round_number=current_round,
  84. emotion=emotion,
  85. message_metadata=metadata,
  86. token_count=len(content) // 2 # 粗略估算
  87. )
  88. session.add(message)
  89. await session.commit()
  90. # 4. 构建消息对象
  91. logger.debug(f"📥 [保存消息] 步骤4: 构建消息对象")
  92. message_obj = {
  93. "message_id": message_id,
  94. "role": role,
  95. "content": content,
  96. "round_number": current_round,
  97. "emotion": emotion,
  98. "created_at": datetime.now().isoformat()
  99. }
  100. # 5. 更新Redis缓存(最近15轮=30条消息)
  101. logger.debug(f"📥 [保存消息] 步骤5: 更新Redis缓存")
  102. await self._cache_message(user_id, message_obj)
  103. logger.debug(f"📥 [保存消息] Redis缓存更新完成")
  104. # 6. 更新用户统计
  105. logger.debug(f"📥 [保存消息] 步骤6: 更新用户统计")
  106. await self._update_user_stats(user_id, current_round, role)
  107. logger.debug(f"📥 [保存消息] 用户统计更新完成")
  108. # 7. 检查是否需要触发总结(每15轮)
  109. logger.debug(f"📥 [保存消息] 步骤7: 检查是否需要触发总结")
  110. if role == 'assistant':
  111. is_multiple_of_15 = (current_round % 15 == 0)
  112. if is_multiple_of_15:
  113. # 异步触发总结任务(使用 create_task 并添加异常处理)
  114. logger.info(f"🔄 [总结触发] 触发总结任务: user={user_id}, round={current_round}")
  115. task = asyncio.create_task(
  116. self._trigger_summary(user_id, current_round)
  117. )
  118. logger.debug(f"🔄 [总结触发] 总结任务已创建: user={user_id}, task={task}")
  119. # 添加异常回调,确保任务失败时能记录日志
  120. def log_task_exception(task):
  121. try:
  122. task.result() # 这会抛出异常(如果有)
  123. except Exception as e:
  124. logger.error(f"❌ [总结任务] 执行失败: user={user_id}, error={e}")
  125. import traceback
  126. logger.error(f"❌ [总结任务] 异常堆栈: {traceback.format_exc()}")
  127. task.add_done_callback(log_task_exception)
  128. logger.debug(f"🔄 [总结触发] 异常回调已添加: user={user_id}")
  129. else:
  130. logger.debug(f"🔍 [总结触发检查] 不需要触发总结: user={user_id}, current_round={current_round} 不是15的倍数")
  131. else:
  132. logger.debug(f"🔍 [总结触发检查] 跳过(非assistant消息): user={user_id}, role={role}")
  133. return message_obj
  134. except Exception as e:
  135. logger.error(f"❌ 保存消息失败: user={user_id}, error={e}")
  136. raise
  137. async def _get_or_init_round(self, user_id: str, role: str) -> int:
  138. """
  139. 获取当前轮次
  140. 规则:user消息开始新轮次,assistant消息沿用当前轮次
  141. Args:
  142. user_id: 用户ID
  143. role: 角色
  144. Returns:
  145. 当前轮次
  146. """
  147. meta_key = f"user:{user_id}:meta"
  148. current_round = 1 # 默认值
  149. logger.debug(f"🔍 [获取轮次] 开始: user={user_id}, role={role}, meta_key={meta_key}")
  150. try:
  151. # 从Redis获取(增加超时控制)
  152. logger.debug(f"🔍 [获取轮次] 从Redis读取: user={user_id}, key={meta_key}")
  153. current_round_str = await asyncio.wait_for(
  154. self.redis.hget(meta_key, 'current_round'),
  155. timeout=2.0 # 2秒超时
  156. )
  157. logger.debug(f"🔍 [获取轮次] Redis返回值: user={user_id}, current_round_str={current_round_str}")
  158. if current_round_str:
  159. redis_round = int(current_round_str)
  160. logger.debug(f"🔍 [获取轮次] Redis中的轮次: user={user_id}, redis_round={redis_round}")
  161. if role == 'user':
  162. # user消息:从MySQL查询最大轮次,然后+1(新轮次)
  163. max_round = await asyncio.wait_for(
  164. self._get_max_round(user_id),
  165. timeout=3.0
  166. )
  167. current_round = max_round + 1
  168. logger.debug(f"🔍 [获取轮次] user消息,新轮次: user={user_id}, max_round={max_round} -> current_round={current_round}")
  169. else:
  170. # assistant消息:使用Redis中的当前轮次(应该和对应的user消息相同)
  171. # 如果Redis中没有值或值异常,则查询MySQL中最近一条user消息的round_number
  172. if redis_round > 0:
  173. current_round = redis_round
  174. logger.debug(f"🔍 [获取轮次] assistant消息,使用Redis轮次: user={user_id}, current_round={current_round}")
  175. else:
  176. # Redis值异常,查询MySQL中最近一条user消息的round_number
  177. result = await asyncio.wait_for(
  178. self._get_latest_user_round(user_id),
  179. timeout=3.0
  180. )
  181. if result:
  182. current_round = result
  183. logger.debug(f"🔍 [获取轮次] assistant消息,使用MySQL最近user消息轮次: user={user_id}, current_round={current_round}")
  184. else:
  185. # 没有user消息,说明这是第一轮对话
  186. current_round = 1
  187. logger.debug(f"🔍 [获取轮次] assistant消息,没有user消息,使用默认轮次: user={user_id}, current_round={current_round}")
  188. else:
  189. # 从MySQL获取最大轮次
  190. logger.debug(f"🔍 [获取轮次] Redis无值,从MySQL查询: user={user_id}")
  191. max_round = await asyncio.wait_for(
  192. self._get_max_round(user_id),
  193. timeout=3.0 # 3秒超时
  194. )
  195. logger.debug(f"🔍 [获取轮次] MySQL最大轮次: user={user_id}, max_round={max_round}")
  196. if role == 'user':
  197. current_round = max_round + 1
  198. logger.debug(f"🔍 [获取轮次] user消息,新轮次: user={user_id}, current_round={current_round}")
  199. else:
  200. # assistant 消息:总是使用 max_round + 1,因为每一轮对话都有唯一的轮次号
  201. current_round = max_round + 1
  202. logger.debug(f"🔍 [获取轮次] assistant消息,新轮次: user={user_id}, current_round={current_round}")
  203. # 确保 current_round 至少为 1(防止为 0 的情况)
  204. if current_round <= 0:
  205. logger.warning(f"⚠️ 计算出的轮次异常: user={user_id}, role={role}, current_round={current_round}, 使用默认值1")
  206. current_round = 1
  207. # 更新Redis(user消息时更新,assistant消息时如果计算出的值更大也更新)
  208. should_update_redis = False
  209. if role == 'user':
  210. should_update_redis = True
  211. logger.debug(f"🔍 [获取轮次] user消息,需要更新Redis: user={user_id}, current_round={current_round}")
  212. elif role == 'assistant':
  213. # assistant 消息:如果计算出的 current_round 大于 Redis 中的值,也更新
  214. # 这样可以修复 Redis 中异常值(如 0)的问题
  215. try:
  216. logger.debug(f"🔍 [获取轮次] assistant消息,检查是否需要更新Redis: user={user_id}")
  217. redis_round_str = await asyncio.wait_for(
  218. self.redis.hget(meta_key, 'current_round'),
  219. timeout=1.0
  220. )
  221. if redis_round_str:
  222. redis_round = int(redis_round_str)
  223. logger.debug(f"🔍 [获取轮次] Redis中的轮次: user={user_id}, redis_round={redis_round}, current_round={current_round}")
  224. if current_round > redis_round:
  225. should_update_redis = True
  226. logger.info(f"🔄 修复Redis轮次: user={user_id}, Redis={redis_round} -> {current_round}")
  227. else:
  228. logger.debug(f"🔍 [获取轮次] 不需要更新Redis: user={user_id}, redis_round={redis_round} >= current_round={current_round}")
  229. else:
  230. # Redis 中没有值,也更新
  231. should_update_redis = True
  232. logger.debug(f"🔍 [获取轮次] Redis无值,需要更新: user={user_id}, current_round={current_round}")
  233. except Exception as e:
  234. logger.warning(f"⚠️ Redis读取失败(不影响主流程): user={user_id}, error={e}")
  235. if should_update_redis:
  236. try:
  237. logger.debug(f"🔍 [获取轮次] 更新Redis: user={user_id}, current_round={current_round}")
  238. # 更新 user:{user_id}:meta 中的 current_round
  239. await asyncio.wait_for(
  240. self.redis.hset(meta_key, 'current_round', current_round),
  241. timeout=1.0
  242. )
  243. await asyncio.wait_for(
  244. self.redis.expire(meta_key, 86400),
  245. timeout=1.0
  246. )
  247. # 同时更新 round:{user_id}(保持一致性)
  248. round_key = f"round:{user_id}"
  249. await asyncio.wait_for(
  250. self.redis.set(round_key, current_round),
  251. timeout=1.0
  252. )
  253. await asyncio.wait_for(
  254. self.redis.expire(round_key, 86400),
  255. timeout=1.0
  256. )
  257. logger.debug(f"🔍 [获取轮次] Redis更新成功: user={user_id}, current_round={current_round}")
  258. except Exception as e:
  259. logger.warning(f"⚠️ Redis写入失败(不影响主流程): user={user_id}, error={e}")
  260. return current_round
  261. except asyncio.TimeoutError:
  262. logger.warning(f"⚠️ Redis/MySQL超时,使用降级方案: user={user_id}")
  263. # 降级方案:尝试快速从MySQL查询,失败则返回1
  264. try:
  265. max_round = await asyncio.wait_for(
  266. self._get_max_round(user_id),
  267. timeout=2.0
  268. )
  269. if role == 'user':
  270. current_round = max_round + 1
  271. else:
  272. # assistant 消息:总是使用 max_round + 1,因为每一轮对话都有唯一的轮次号
  273. current_round = max_round + 1
  274. # 确保至少为 1
  275. return max(current_round, 1)
  276. except:
  277. logger.error(f"❌ 降级方案也失败,返回默认轮次1")
  278. return 1
  279. except Exception as e:
  280. logger.error(f"❌ 获取轮次失败: user={user_id}, error={e}")
  281. # 最终降级:返回1
  282. return 1
  283. async def _get_max_round(self, user_id: str) -> int:
  284. """使用 ORM 获取用户最大轮次"""
  285. try:
  286. async with db_manager.get_session() as session:
  287. result = await session.execute(
  288. select(func.max(Message.round_number))
  289. .where(Message.user_id == user_id)
  290. )
  291. max_round = result.scalar()
  292. return max_round if max_round else 0
  293. except Exception as e:
  294. logger.error(f"❌ 获取最大轮次失败: user={user_id}, error={e}")
  295. return 0
  296. async def _get_latest_user_round(self, user_id: str) -> Optional[int]:
  297. """使用 ORM 获取最近一条 user 消息的轮次"""
  298. try:
  299. async with db_manager.get_session() as session:
  300. result = await session.execute(
  301. select(Message.round_number)
  302. .where(Message.user_id == user_id)
  303. .where(Message.role == 'user')
  304. .order_by(Message.created_at.desc())
  305. .limit(1)
  306. )
  307. round_number = result.scalar_one_or_none()
  308. return round_number
  309. except Exception as e:
  310. logger.error(f"❌ 获取最近user消息轮次失败: user={user_id}, error={e}")
  311. return None
  312. async def _cache_message(self, user_id: str, message: Dict):
  313. """
  314. 缓存消息到Redis(仅保留最近30条)
  315. Args:
  316. user_id: 用户ID
  317. message: 消息对象
  318. """
  319. try:
  320. cache_key = f"user:{user_id}:recent_messages"
  321. logger.debug(f"🔍 [缓存消息] 开始: user={user_id}, cache_key={cache_key}, round={message.get('round_number')}")
  322. await self.redis.lpush(cache_key, json.dumps(message, ensure_ascii=False))
  323. await self.redis.ltrim(cache_key, 0, 29) # 保留最近30条
  324. await self.redis.expire(cache_key, 86400) # 24小时过期
  325. logger.debug(f"✅ [缓存消息] 完成: user={user_id}, round={message.get('round_number')}")
  326. except Exception as e:
  327. logger.warning(f"⚠️ [缓存消息] 失败: user={user_id}, error={e}")
  328. async def _update_user_stats(self, user_id: str, round_number: int, role: str):
  329. """
  330. 更新用户统计信息
  331. Args:
  332. user_id: 用户ID
  333. round_number: 轮次
  334. role: 角色
  335. """
  336. try:
  337. logger.debug(f"🔍 [更新统计] 开始: user={user_id}, round={round_number}, role={role}")
  338. # 更新Redis元数据
  339. meta_key = f"user:{user_id}:meta"
  340. await self.redis.hincrby(meta_key, 'total_messages', 1)
  341. logger.debug(f"🔍 [更新统计] Redis消息数+1: user={user_id}")
  342. if role == 'assistant':
  343. await self.redis.hset(meta_key, 'total_rounds', round_number)
  344. logger.debug(f"🔍 [更新统计] Redis轮次更新: user={user_id}, round={round_number}")
  345. await self.redis.expire(meta_key, 86400)
  346. # 更新MySQL(异步,不阻塞)
  347. logger.debug(f"🔍 [更新统计] 创建MySQL更新任务: user={user_id}")
  348. asyncio.create_task(
  349. self._update_user_stats_db(user_id, round_number)
  350. )
  351. logger.debug(f"✅ [更新统计] 完成: user={user_id}, round={round_number}")
  352. except Exception as e:
  353. logger.warning(f"⚠️ [更新统计] 失败: user={user_id}, error={e}")
  354. async def _update_user_stats_db(self, user_id: str, round_number: int):
  355. """更新MySQL中的用户统计(使用 ORM)"""
  356. try:
  357. async with db_manager.get_session() as session:
  358. await session.execute(
  359. update(User)
  360. .where(User.user_id == user_id)
  361. .values(
  362. total_messages=User.total_messages + 1,
  363. total_rounds=round_number,
  364. last_active_at=func.now()
  365. )
  366. )
  367. await session.commit()
  368. except Exception as e:
  369. logger.error(f"❌ 更新MySQL用户统计失败: user={user_id}, error={e}")
  370. async def _trigger_summary(self, user_id: str, current_round: int):
  371. """
  372. 触发异步总结任务
  373. Args:
  374. user_id: 用户ID
  375. current_round: 当前轮次
  376. """
  377. try:
  378. logger.info(f"🚀 [总结任务] ========== 开始执行 ==========")
  379. logger.info(f"🚀 [总结任务] user={user_id}, current_round={current_round}")
  380. # 参数验证
  381. if current_round <= 0:
  382. logger.warning(f"⚠️ [总结任务] 无效的当前轮次: user={user_id}, current_round={current_round}")
  383. return
  384. logger.debug(f"🔍 [总结任务] 步骤1: 导入SummaryService")
  385. from .summary_service import SummaryService
  386. logger.debug(f"🔍 [总结任务] 步骤2: 创建SummaryService实例")
  387. summary_service = SummaryService(self.db)
  388. # 计算总结范围
  389. # 当前轮次是15的倍数,总结1到current_round
  390. start_round = 1
  391. end_round = current_round
  392. logger.info(f"📝 [总结任务] 准备生成总结: user={user_id}, start_round={start_round}, end_round={end_round}")
  393. logger.debug(f"🔍 [总结任务] 步骤3: 调用generate_summary")
  394. await summary_service.generate_summary(
  395. user_id=user_id,
  396. start_round=start_round,
  397. end_round=end_round
  398. )
  399. logger.info(f"✅ [总结任务] ========== 完成 ==========")
  400. logger.info(f"✅ [总结任务] user={user_id}, current_round={current_round}")
  401. except Exception as e:
  402. logger.error(f"❌ [总结任务] ========== 执行失败 ==========")
  403. logger.error(f"❌ [总结任务] user={user_id}, current_round={current_round}, error={e}")
  404. import traceback
  405. logger.error(f"❌ [总结任务] 异常堆栈:\n{traceback.format_exc()}")
  406. async def get_user_current_round(self, user_id: str) -> int:
  407. """
  408. 获取用户当前对话轮次
  409. Args:
  410. user_id: 用户ID
  411. Returns:
  412. 当前轮次(从1开始)
  413. """
  414. try:
  415. redis = await self._ensure_redis()
  416. meta_key = f"user:{user_id}:meta"
  417. current_round = await redis.hget(meta_key, 'current_round')
  418. if current_round:
  419. return int(current_round)
  420. # 从MySQL查询(使用 ORM)
  421. max_round = await self._get_max_round(user_id)
  422. return max_round if max_round else 0
  423. except Exception as e:
  424. logger.error(f"❌ 获取当前轮次失败: user={user_id}, error={e}")
  425. return 0
  426. # 全局消息服务实例
  427. message_service = MessageService()