| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 |
- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
- """
- 微信客服消息cursor持久化管理模块
- 用于管理每个客服账号的消息拉取cursor,避免重复处理消息
- 作者:开发工程师
- 日期:2025年01月
- """
- import os
- import json
- import logging
- import sqlite3
- import threading
- from typing import Dict, Optional
- from config import Config
- logger = logging.getLogger(__name__)
- class CursorManager:
- """Cursor持久化管理器"""
-
- def __init__(self, db_path: str = None):
- self.db_path = db_path or Config.DATABASE_PATH
- self.lock = threading.Lock()
- self._init_database()
-
- def _init_database(self):
- """初始化数据库表"""
- try:
- with sqlite3.connect(self.db_path) as conn:
- cursor = conn.cursor()
-
- # 创建cursor存储表
- cursor.execute('''
- CREATE TABLE IF NOT EXISTS kf_cursors (
- open_kfid TEXT PRIMARY KEY,
- cursor TEXT NOT NULL,
- last_updated INTEGER NOT NULL,
- message_count INTEGER DEFAULT 0,
- created_time INTEGER DEFAULT (strftime('%s', 'now'))
- )
- ''')
-
- # 创建消息去重表
- cursor.execute('''
- CREATE TABLE IF NOT EXISTS processed_messages (
- msgid TEXT PRIMARY KEY,
- open_kfid TEXT NOT NULL,
- external_userid TEXT,
- msgtype TEXT,
- origin INTEGER,
- processed_time INTEGER DEFAULT (strftime('%s', 'now')),
- reply_sent INTEGER DEFAULT 0
- )
- ''')
-
- # 创建索引
- cursor.execute('''
- CREATE INDEX IF NOT EXISTS idx_processed_messages_time
- ON processed_messages(processed_time)
- ''')
-
- cursor.execute('''
- CREATE INDEX IF NOT EXISTS idx_processed_messages_kfid
- ON processed_messages(open_kfid)
- ''')
-
- conn.commit()
- logger.info("Cursor管理数据库初始化完成")
-
- except Exception as e:
- logger.error(f"初始化cursor数据库失败: {e}")
-
- def get_cursor(self, open_kfid: str) -> str:
- """
- 获取指定客服账号的cursor
-
- Args:
- open_kfid: 客服账号ID
-
- Returns:
- cursor字符串,如果不存在则返回空字符串
- """
- try:
- with self.lock:
- with sqlite3.connect(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- 'SELECT cursor FROM kf_cursors WHERE open_kfid = ?',
- (open_kfid,)
- )
- result = cursor.fetchone()
-
- if result:
- logger.debug(f"获取cursor成功 - {open_kfid}: {result[0][:20]}...")
- return result[0]
- else:
- logger.info(f"客服账号 {open_kfid} 无历史cursor,将进行首次拉取")
- return ""
-
- except Exception as e:
- logger.error(f"获取cursor失败: {e}")
- return ""
-
- def save_cursor(self, open_kfid: str, cursor: str, message_count: int = 0) -> bool:
- """
- 保存指定客服账号的cursor
-
- Args:
- open_kfid: 客服账号ID
- cursor: 新的cursor值
- message_count: 本次处理的消息数量
-
- Returns:
- 保存是否成功
- """
- try:
- with self.lock:
- with sqlite3.connect(self.db_path) as conn:
- db_cursor = conn.cursor()
-
- # 使用UPSERT语法
- db_cursor.execute('''
- INSERT OR REPLACE INTO kf_cursors
- (open_kfid, cursor, last_updated, message_count)
- VALUES (?, ?, strftime('%s', 'now'),
- COALESCE((SELECT message_count FROM kf_cursors WHERE open_kfid = ?), 0) + ?)
- ''', (open_kfid, cursor, open_kfid, message_count))
-
- conn.commit()
- logger.info(f"保存cursor成功 - {open_kfid}: {cursor[:20]}..., 消息数: {message_count}")
- return True
-
- except Exception as e:
- logger.error(f"保存cursor失败: {e}")
- return False
-
- def is_message_processed(self, msgid: str) -> bool:
- """
- 检查消息是否已经处理过
-
- Args:
- msgid: 消息ID
-
- Returns:
- 是否已处理
- """
- try:
- with self.lock:
- with sqlite3.connect(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute(
- 'SELECT 1 FROM processed_messages WHERE msgid = ?',
- (msgid,)
- )
- result = cursor.fetchone()
- return result is not None
-
- except Exception as e:
- logger.error(f"检查消息处理状态失败: {e}")
- return False
-
- def mark_message_processed(self, msgid: str, open_kfid: str, external_userid: str = "",
- msgtype: str = "", origin: int = 0, reply_sent: bool = False) -> bool:
- """
- 标记消息为已处理
-
- Args:
- msgid: 消息ID
- open_kfid: 客服账号ID
- external_userid: 外部用户ID
- msgtype: 消息类型
- origin: 消息来源
- reply_sent: 是否已发送回复
-
- Returns:
- 标记是否成功
- """
- try:
- with self.lock:
- with sqlite3.connect(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute('''
- INSERT OR REPLACE INTO processed_messages
- (msgid, open_kfid, external_userid, msgtype, origin, reply_sent)
- VALUES (?, ?, ?, ?, ?, ?)
- ''', (msgid, open_kfid, external_userid, msgtype, origin, int(reply_sent)))
-
- conn.commit()
- logger.debug(f"标记消息已处理: {msgid}")
- return True
-
- except Exception as e:
- logger.error(f"标记消息处理状态失败: {e}")
- return False
-
- def cleanup_old_records(self, days: int = 30) -> bool:
- """
- 清理旧的处理记录
-
- Args:
- days: 保留天数
-
- Returns:
- 清理是否成功
- """
- try:
- with self.lock:
- with sqlite3.connect(self.db_path) as conn:
- cursor = conn.cursor()
-
- # 清理旧的消息处理记录
- cursor.execute('''
- DELETE FROM processed_messages
- WHERE processed_time < strftime('%s', 'now', '-{} days')
- '''.format(days))
-
- deleted_count = cursor.rowcount
- conn.commit()
-
- if deleted_count > 0:
- logger.info(f"清理了 {deleted_count} 条旧的消息处理记录")
-
- return True
-
- except Exception as e:
- logger.error(f"清理旧记录失败: {e}")
- return False
-
- def get_statistics(self) -> Dict:
- """
- 获取cursor管理统计信息
-
- Returns:
- 统计信息字典
- """
- try:
- with self.lock:
- with sqlite3.connect(self.db_path) as conn:
- cursor = conn.cursor()
-
- # 获取客服账号数量
- cursor.execute('SELECT COUNT(*) FROM kf_cursors')
- kf_count = cursor.fetchone()[0]
-
- # 获取总消息处理数量
- cursor.execute('SELECT SUM(message_count) FROM kf_cursors')
- total_messages = cursor.fetchone()[0] or 0
-
- # 获取今日处理消息数量
- cursor.execute('''
- SELECT COUNT(*) FROM processed_messages
- WHERE processed_time >= strftime('%s', 'now', 'start of day')
- ''')
- today_messages = cursor.fetchone()[0]
-
- # 获取已回复消息数量
- cursor.execute('SELECT COUNT(*) FROM processed_messages WHERE reply_sent = 1')
- replied_messages = cursor.fetchone()[0]
-
- return {
- 'kf_accounts': kf_count,
- 'total_messages': total_messages,
- 'today_messages': today_messages,
- 'replied_messages': replied_messages,
- 'reply_rate': round(replied_messages / max(total_messages, 1) * 100, 2)
- }
-
- except Exception as e:
- logger.error(f"获取统计信息失败: {e}")
- return {}
-
- def get_kf_account_info(self, open_kfid: str) -> Optional[Dict]:
- """
- 获取指定客服账号的详细信息
-
- Args:
- open_kfid: 客服账号ID
-
- Returns:
- 账号信息字典
- """
- try:
- with self.lock:
- with sqlite3.connect(self.db_path) as conn:
- cursor = conn.cursor()
-
- # 获取cursor信息
- cursor.execute('''
- SELECT cursor, last_updated, message_count, created_time
- FROM kf_cursors WHERE open_kfid = ?
- ''', (open_kfid,))
- cursor_info = cursor.fetchone()
-
- if not cursor_info:
- return None
-
- # 获取今日消息数量
- cursor.execute('''
- SELECT COUNT(*) FROM processed_messages
- WHERE open_kfid = ? AND processed_time >= strftime('%s', 'now', 'start of day')
- ''', (open_kfid,))
- today_count = cursor.fetchone()[0]
-
- return {
- 'open_kfid': open_kfid,
- 'cursor': cursor_info[0][:20] + '...' if cursor_info[0] else '',
- 'last_updated': cursor_info[1],
- 'total_messages': cursor_info[2],
- 'today_messages': today_count,
- 'created_time': cursor_info[3]
- }
-
- except Exception as e:
- logger.error(f"获取客服账号信息失败: {e}")
- return None
- # 全局cursor管理器实例
- cursor_manager = CursorManager()
|