cursor_manager.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. 微信客服消息cursor持久化管理模块
  5. 用于管理每个客服账号的消息拉取cursor,避免重复处理消息
  6. 作者:开发工程师
  7. 日期:2025年01月
  8. """
  9. import os
  10. import json
  11. import logging
  12. import sqlite3
  13. import threading
  14. from typing import Dict, Optional
  15. from config import Config
  16. logger = logging.getLogger(__name__)
  17. class CursorManager:
  18. """Cursor持久化管理器"""
  19. def __init__(self, db_path: str = None):
  20. self.db_path = db_path or Config.DATABASE_PATH
  21. self.lock = threading.Lock()
  22. self._init_database()
  23. def _init_database(self):
  24. """初始化数据库表"""
  25. try:
  26. with sqlite3.connect(self.db_path) as conn:
  27. cursor = conn.cursor()
  28. # 创建cursor存储表
  29. cursor.execute('''
  30. CREATE TABLE IF NOT EXISTS kf_cursors (
  31. open_kfid TEXT PRIMARY KEY,
  32. cursor TEXT NOT NULL,
  33. last_updated INTEGER NOT NULL,
  34. message_count INTEGER DEFAULT 0,
  35. created_time INTEGER DEFAULT (strftime('%s', 'now'))
  36. )
  37. ''')
  38. # 创建消息去重表
  39. cursor.execute('''
  40. CREATE TABLE IF NOT EXISTS processed_messages (
  41. msgid TEXT PRIMARY KEY,
  42. open_kfid TEXT NOT NULL,
  43. external_userid TEXT,
  44. msgtype TEXT,
  45. origin INTEGER,
  46. processed_time INTEGER DEFAULT (strftime('%s', 'now')),
  47. reply_sent INTEGER DEFAULT 0
  48. )
  49. ''')
  50. # 创建索引
  51. cursor.execute('''
  52. CREATE INDEX IF NOT EXISTS idx_processed_messages_time
  53. ON processed_messages(processed_time)
  54. ''')
  55. cursor.execute('''
  56. CREATE INDEX IF NOT EXISTS idx_processed_messages_kfid
  57. ON processed_messages(open_kfid)
  58. ''')
  59. conn.commit()
  60. logger.info("Cursor管理数据库初始化完成")
  61. except Exception as e:
  62. logger.error(f"初始化cursor数据库失败: {e}")
  63. def get_cursor(self, open_kfid: str) -> str:
  64. """
  65. 获取指定客服账号的cursor
  66. Args:
  67. open_kfid: 客服账号ID
  68. Returns:
  69. cursor字符串,如果不存在则返回空字符串
  70. """
  71. try:
  72. with self.lock:
  73. with sqlite3.connect(self.db_path) as conn:
  74. cursor = conn.cursor()
  75. cursor.execute(
  76. 'SELECT cursor FROM kf_cursors WHERE open_kfid = ?',
  77. (open_kfid,)
  78. )
  79. result = cursor.fetchone()
  80. if result:
  81. logger.debug(f"获取cursor成功 - {open_kfid}: {result[0][:20]}...")
  82. return result[0]
  83. else:
  84. logger.info(f"客服账号 {open_kfid} 无历史cursor,将进行首次拉取")
  85. return ""
  86. except Exception as e:
  87. logger.error(f"获取cursor失败: {e}")
  88. return ""
  89. def save_cursor(self, open_kfid: str, cursor: str, message_count: int = 0) -> bool:
  90. """
  91. 保存指定客服账号的cursor
  92. Args:
  93. open_kfid: 客服账号ID
  94. cursor: 新的cursor值
  95. message_count: 本次处理的消息数量
  96. Returns:
  97. 保存是否成功
  98. """
  99. try:
  100. with self.lock:
  101. with sqlite3.connect(self.db_path) as conn:
  102. db_cursor = conn.cursor()
  103. # 使用UPSERT语法
  104. db_cursor.execute('''
  105. INSERT OR REPLACE INTO kf_cursors
  106. (open_kfid, cursor, last_updated, message_count)
  107. VALUES (?, ?, strftime('%s', 'now'),
  108. COALESCE((SELECT message_count FROM kf_cursors WHERE open_kfid = ?), 0) + ?)
  109. ''', (open_kfid, cursor, open_kfid, message_count))
  110. conn.commit()
  111. logger.info(f"保存cursor成功 - {open_kfid}: {cursor[:20]}..., 消息数: {message_count}")
  112. return True
  113. except Exception as e:
  114. logger.error(f"保存cursor失败: {e}")
  115. return False
  116. def is_message_processed(self, msgid: str) -> bool:
  117. """
  118. 检查消息是否已经处理过
  119. Args:
  120. msgid: 消息ID
  121. Returns:
  122. 是否已处理
  123. """
  124. try:
  125. with self.lock:
  126. with sqlite3.connect(self.db_path) as conn:
  127. cursor = conn.cursor()
  128. cursor.execute(
  129. 'SELECT 1 FROM processed_messages WHERE msgid = ?',
  130. (msgid,)
  131. )
  132. result = cursor.fetchone()
  133. return result is not None
  134. except Exception as e:
  135. logger.error(f"检查消息处理状态失败: {e}")
  136. return False
  137. def mark_message_processed(self, msgid: str, open_kfid: str, external_userid: str = "",
  138. msgtype: str = "", origin: int = 0, reply_sent: bool = False) -> bool:
  139. """
  140. 标记消息为已处理
  141. Args:
  142. msgid: 消息ID
  143. open_kfid: 客服账号ID
  144. external_userid: 外部用户ID
  145. msgtype: 消息类型
  146. origin: 消息来源
  147. reply_sent: 是否已发送回复
  148. Returns:
  149. 标记是否成功
  150. """
  151. try:
  152. with self.lock:
  153. with sqlite3.connect(self.db_path) as conn:
  154. cursor = conn.cursor()
  155. cursor.execute('''
  156. INSERT OR REPLACE INTO processed_messages
  157. (msgid, open_kfid, external_userid, msgtype, origin, reply_sent)
  158. VALUES (?, ?, ?, ?, ?, ?)
  159. ''', (msgid, open_kfid, external_userid, msgtype, origin, int(reply_sent)))
  160. conn.commit()
  161. logger.debug(f"标记消息已处理: {msgid}")
  162. return True
  163. except Exception as e:
  164. logger.error(f"标记消息处理状态失败: {e}")
  165. return False
  166. def cleanup_old_records(self, days: int = 30) -> bool:
  167. """
  168. 清理旧的处理记录
  169. Args:
  170. days: 保留天数
  171. Returns:
  172. 清理是否成功
  173. """
  174. try:
  175. with self.lock:
  176. with sqlite3.connect(self.db_path) as conn:
  177. cursor = conn.cursor()
  178. # 清理旧的消息处理记录
  179. cursor.execute('''
  180. DELETE FROM processed_messages
  181. WHERE processed_time < strftime('%s', 'now', '-{} days')
  182. '''.format(days))
  183. deleted_count = cursor.rowcount
  184. conn.commit()
  185. if deleted_count > 0:
  186. logger.info(f"清理了 {deleted_count} 条旧的消息处理记录")
  187. return True
  188. except Exception as e:
  189. logger.error(f"清理旧记录失败: {e}")
  190. return False
  191. def get_statistics(self) -> Dict:
  192. """
  193. 获取cursor管理统计信息
  194. Returns:
  195. 统计信息字典
  196. """
  197. try:
  198. with self.lock:
  199. with sqlite3.connect(self.db_path) as conn:
  200. cursor = conn.cursor()
  201. # 获取客服账号数量
  202. cursor.execute('SELECT COUNT(*) FROM kf_cursors')
  203. kf_count = cursor.fetchone()[0]
  204. # 获取总消息处理数量
  205. cursor.execute('SELECT SUM(message_count) FROM kf_cursors')
  206. total_messages = cursor.fetchone()[0] or 0
  207. # 获取今日处理消息数量
  208. cursor.execute('''
  209. SELECT COUNT(*) FROM processed_messages
  210. WHERE processed_time >= strftime('%s', 'now', 'start of day')
  211. ''')
  212. today_messages = cursor.fetchone()[0]
  213. # 获取已回复消息数量
  214. cursor.execute('SELECT COUNT(*) FROM processed_messages WHERE reply_sent = 1')
  215. replied_messages = cursor.fetchone()[0]
  216. return {
  217. 'kf_accounts': kf_count,
  218. 'total_messages': total_messages,
  219. 'today_messages': today_messages,
  220. 'replied_messages': replied_messages,
  221. 'reply_rate': round(replied_messages / max(total_messages, 1) * 100, 2)
  222. }
  223. except Exception as e:
  224. logger.error(f"获取统计信息失败: {e}")
  225. return {}
  226. def get_kf_account_info(self, open_kfid: str) -> Optional[Dict]:
  227. """
  228. 获取指定客服账号的详细信息
  229. Args:
  230. open_kfid: 客服账号ID
  231. Returns:
  232. 账号信息字典
  233. """
  234. try:
  235. with self.lock:
  236. with sqlite3.connect(self.db_path) as conn:
  237. cursor = conn.cursor()
  238. # 获取cursor信息
  239. cursor.execute('''
  240. SELECT cursor, last_updated, message_count, created_time
  241. FROM kf_cursors WHERE open_kfid = ?
  242. ''', (open_kfid,))
  243. cursor_info = cursor.fetchone()
  244. if not cursor_info:
  245. return None
  246. # 获取今日消息数量
  247. cursor.execute('''
  248. SELECT COUNT(*) FROM processed_messages
  249. WHERE open_kfid = ? AND processed_time >= strftime('%s', 'now', 'start of day')
  250. ''', (open_kfid,))
  251. today_count = cursor.fetchone()[0]
  252. return {
  253. 'open_kfid': open_kfid,
  254. 'cursor': cursor_info[0][:20] + '...' if cursor_info[0] else '',
  255. 'last_updated': cursor_info[1],
  256. 'total_messages': cursor_info[2],
  257. 'today_messages': today_count,
  258. 'created_time': cursor_info[3]
  259. }
  260. except Exception as e:
  261. logger.error(f"获取客服账号信息失败: {e}")
  262. return None
  263. # 全局cursor管理器实例
  264. cursor_manager = CursorManager()