message_service.py 22 KB

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