import sqlite3 import os import threading import hashlib import time import json import random import string import aiohttp import io import base64 from PIL import Image, ImageDraw, ImageFont from typing import List, Tuple, Dict, Optional, Any from loguru import logger class DBManager: """SQLite数据库管理,持久化存储Cookie和关键字""" def __init__(self, db_path: str = None): """初始化数据库连接和表结构""" # 支持环境变量配置数据库路径 if db_path is None: db_path = os.getenv('DB_PATH', 'xianyu_data.db') # 确保数据目录存在并有正确权限 db_dir = os.path.dirname(db_path) if db_dir and not os.path.exists(db_dir): try: os.makedirs(db_dir, mode=0o755, exist_ok=True) logger.info(f"创建数据目录: {db_dir}") except PermissionError as e: logger.error(f"创建数据目录失败,权限不足: {e}") # 尝试使用当前目录 db_path = os.path.basename(db_path) logger.warning(f"使用当前目录作为数据库路径: {db_path}") except Exception as e: logger.error(f"创建数据目录失败: {e}") raise # 检查目录权限 if db_dir and os.path.exists(db_dir): if not os.access(db_dir, os.W_OK): logger.error(f"数据目录没有写权限: {db_dir}") # 尝试使用当前目录 db_path = os.path.basename(db_path) logger.warning(f"使用当前目录作为数据库路径: {db_path}") self.db_path = db_path logger.info(f"数据库路径: {self.db_path}") self.conn = None self.lock = threading.RLock() # 使用可重入锁保护数据库操作 self.init_db() def init_db(self): """初始化数据库表结构""" try: self.conn = sqlite3.connect(self.db_path, check_same_thread=False) cursor = self.conn.cursor() # 创建用户表 cursor.execute(''' CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') # 创建邮箱验证码表 cursor.execute(''' CREATE TABLE IF NOT EXISTS email_verifications ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL, code TEXT NOT NULL, expires_at TIMESTAMP NOT NULL, used BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') # 创建图形验证码表 cursor.execute(''' CREATE TABLE IF NOT EXISTS captcha_codes ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, code TEXT NOT NULL, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') # 创建cookies表(添加user_id字段) cursor.execute(''' CREATE TABLE IF NOT EXISTS cookies ( id TEXT PRIMARY KEY, value TEXT NOT NULL, user_id INTEGER NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ''') # 创建keywords表 cursor.execute(''' CREATE TABLE IF NOT EXISTS keywords ( cookie_id TEXT, keyword TEXT, reply TEXT, PRIMARY KEY (cookie_id, keyword), FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE ) ''') # 创建cookie_status表 cursor.execute(''' CREATE TABLE IF NOT EXISTS cookie_status ( cookie_id TEXT PRIMARY KEY, enabled BOOLEAN DEFAULT TRUE, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE ) ''') # 创建AI回复配置表 cursor.execute(''' CREATE TABLE IF NOT EXISTS ai_reply_settings ( cookie_id TEXT PRIMARY KEY, ai_enabled BOOLEAN DEFAULT FALSE, model_name TEXT DEFAULT 'qwen-plus', api_key TEXT, base_url TEXT DEFAULT 'https://dashscope.aliyuncs.com/compatible-mode/v1', max_discount_percent INTEGER DEFAULT 10, max_discount_amount INTEGER DEFAULT 100, max_bargain_rounds INTEGER DEFAULT 3, custom_prompts TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE ) ''') # 创建AI对话历史表 cursor.execute(''' CREATE TABLE IF NOT EXISTS ai_conversations ( id INTEGER PRIMARY KEY AUTOINCREMENT, cookie_id TEXT NOT NULL, chat_id TEXT NOT NULL, user_id TEXT NOT NULL, item_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, intent TEXT, bargain_count INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (cookie_id) REFERENCES cookies (id) ON DELETE CASCADE ) ''') # 创建AI商品信息缓存表 cursor.execute(''' CREATE TABLE IF NOT EXISTS ai_item_cache ( item_id TEXT PRIMARY KEY, data TEXT NOT NULL, price REAL, description TEXT, last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') # 创建卡券表 cursor.execute(''' CREATE TABLE IF NOT EXISTS cards ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type TEXT NOT NULL CHECK (type IN ('api', 'text', 'data')), api_config TEXT, text_content TEXT, data_content TEXT, description TEXT, enabled BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') # 创建商品信息表 cursor.execute(''' CREATE TABLE IF NOT EXISTS item_info ( id INTEGER PRIMARY KEY AUTOINCREMENT, cookie_id TEXT NOT NULL, item_id TEXT NOT NULL, item_title TEXT, item_description TEXT, item_category TEXT, item_price TEXT, item_detail TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE, UNIQUE(cookie_id, item_id) ) ''') # 创建自动发货规则表 cursor.execute(''' CREATE TABLE IF NOT EXISTS delivery_rules ( id INTEGER PRIMARY KEY AUTOINCREMENT, keyword TEXT NOT NULL, card_id INTEGER NOT NULL, delivery_count INTEGER DEFAULT 1, enabled BOOLEAN DEFAULT TRUE, description TEXT, delivery_times INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (card_id) REFERENCES cards(id) ON DELETE CASCADE ) ''') # 创建默认回复表 cursor.execute(''' CREATE TABLE IF NOT EXISTS default_replies ( cookie_id TEXT PRIMARY KEY, enabled BOOLEAN DEFAULT FALSE, reply_content TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE ) ''') # 创建通知渠道表 cursor.execute(''' CREATE TABLE IF NOT EXISTS notification_channels ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type TEXT NOT NULL CHECK (type IN ('qq')), config TEXT NOT NULL, enabled BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') # 创建消息通知配置表 cursor.execute(''' CREATE TABLE IF NOT EXISTS message_notifications ( id INTEGER PRIMARY KEY AUTOINCREMENT, cookie_id TEXT NOT NULL, channel_id INTEGER NOT NULL, enabled BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (cookie_id) REFERENCES cookies(id) ON DELETE CASCADE, FOREIGN KEY (channel_id) REFERENCES notification_channels(id) ON DELETE CASCADE, UNIQUE(cookie_id, channel_id) ) ''') # 创建系统设置表 cursor.execute(''' CREATE TABLE IF NOT EXISTS system_settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL, description TEXT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') # 插入默认系统设置 cursor.execute(''' INSERT OR IGNORE INTO system_settings (key, value, description) VALUES ('admin_password_hash', ?, '管理员密码哈希'), ('theme_color', 'blue', '主题颜色') ''', (hashlib.sha256("admin123".encode()).hexdigest(),)) # 创建默认admin用户 cursor.execute(''' INSERT OR IGNORE INTO users (username, email, password_hash) VALUES ('admin', 'admin@localhost', ?) ''', (hashlib.sha256("admin123".encode()).hexdigest(),)) # 获取admin用户ID,用于历史数据绑定 cursor.execute("SELECT id FROM users WHERE username = 'admin'") admin_user = cursor.fetchone() if admin_user: admin_user_id = admin_user[0] # 将历史cookies数据绑定到admin用户(如果user_id列不存在) try: cursor.execute("SELECT user_id FROM cookies LIMIT 1") except sqlite3.OperationalError: # user_id列不存在,需要添加并更新历史数据 cursor.execute("ALTER TABLE cookies ADD COLUMN user_id INTEGER") cursor.execute("UPDATE cookies SET user_id = ? WHERE user_id IS NULL", (admin_user_id,)) else: # user_id列存在,更新NULL值 cursor.execute("UPDATE cookies SET user_id = ? WHERE user_id IS NULL", (admin_user_id,)) self.conn.commit() logger.info(f"数据库初始化成功: {self.db_path}") except Exception as e: logger.error(f"数据库初始化失败: {e}") if self.conn: self.conn.close() raise def close(self): """关闭数据库连接""" if self.conn: self.conn.close() self.conn = None def get_connection(self): """获取数据库连接,如果已关闭则重新连接""" if self.conn is None: self.conn = sqlite3.connect(self.db_path, check_same_thread=False) return self.conn # -------------------- Cookie操作 -------------------- def save_cookie(self, cookie_id: str, cookie_value: str, user_id: int = None) -> bool: """保存Cookie到数据库,如存在则更新""" with self.lock: try: cursor = self.conn.cursor() # 如果没有提供user_id,尝试从现有记录获取,否则使用admin用户ID if user_id is None: cursor.execute("SELECT user_id FROM cookies WHERE id = ?", (cookie_id,)) existing = cursor.fetchone() if existing: user_id = existing[0] else: # 获取admin用户ID作为默认值 cursor.execute("SELECT id FROM users WHERE username = 'admin'") admin_user = cursor.fetchone() user_id = admin_user[0] if admin_user else 1 cursor.execute( "INSERT OR REPLACE INTO cookies (id, value, user_id) VALUES (?, ?, ?)", (cookie_id, cookie_value, user_id) ) self.conn.commit() logger.debug(f"Cookie保存成功: {cookie_id} (用户ID: {user_id})") return True except Exception as e: logger.error(f"Cookie保存失败: {e}") self.conn.rollback() return False def delete_cookie(self, cookie_id: str) -> bool: """从数据库删除Cookie及其关键字""" with self.lock: try: cursor = self.conn.cursor() # 删除关联的关键字 cursor.execute("DELETE FROM keywords WHERE cookie_id = ?", (cookie_id,)) # 删除Cookie cursor.execute("DELETE FROM cookies WHERE id = ?", (cookie_id,)) self.conn.commit() logger.debug(f"Cookie删除成功: {cookie_id}") return True except Exception as e: logger.error(f"Cookie删除失败: {e}") self.conn.rollback() return False def get_cookie(self, cookie_id: str) -> Optional[str]: """获取指定Cookie值""" with self.lock: try: cursor = self.conn.cursor() cursor.execute("SELECT value FROM cookies WHERE id = ?", (cookie_id,)) result = cursor.fetchone() return result[0] if result else None except Exception as e: logger.error(f"获取Cookie失败: {e}") return None def get_all_cookies(self, user_id: int = None) -> Dict[str, str]: """获取所有Cookie(支持用户隔离)""" with self.lock: try: cursor = self.conn.cursor() if user_id is not None: cursor.execute("SELECT id, value FROM cookies WHERE user_id = ?", (user_id,)) else: cursor.execute("SELECT id, value FROM cookies") return {row[0]: row[1] for row in cursor.fetchall()} except Exception as e: logger.error(f"获取所有Cookie失败: {e}") return {} def get_cookie_by_id(self, cookie_id: str) -> Optional[Dict[str, str]]: """根据ID获取Cookie信息 Args: cookie_id: Cookie ID Returns: Dict包含cookie信息,包括cookies_str字段,如果不存在返回None """ with self.lock: try: cursor = self.conn.cursor() cursor.execute("SELECT id, value, created_at FROM cookies WHERE id = ?", (cookie_id,)) result = cursor.fetchone() if result: return { 'id': result[0], 'cookies_str': result[1], # 使用cookies_str字段名以匹配调用方期望 'value': result[1], # 保持向后兼容 'created_at': result[2] } return None except Exception as e: logger.error(f"根据ID获取Cookie失败: {e}") return None # -------------------- 关键字操作 -------------------- def save_keywords(self, cookie_id: str, keywords: List[Tuple[str, str]]) -> bool: """保存关键字列表,先删除旧数据再插入新数据""" with self.lock: try: cursor = self.conn.cursor() # 先删除该cookie_id的所有关键字 cursor.execute("DELETE FROM keywords WHERE cookie_id = ?", (cookie_id,)) # 插入新关键字 for keyword, reply in keywords: cursor.execute( "INSERT INTO keywords (cookie_id, keyword, reply) VALUES (?, ?, ?)", (cookie_id, keyword, reply) ) self.conn.commit() logger.info(f"关键字保存成功: {cookie_id}, {len(keywords)}条") return True except Exception as e: logger.error(f"关键字保存失败: {e}") self.conn.rollback() return False def get_keywords(self, cookie_id: str) -> List[Tuple[str, str]]: """获取指定Cookie的关键字列表""" with self.lock: try: cursor = self.conn.cursor() cursor.execute( "SELECT keyword, reply FROM keywords WHERE cookie_id = ?", (cookie_id,) ) return [(row[0], row[1]) for row in cursor.fetchall()] except Exception as e: logger.error(f"获取关键字失败: {e}") return [] def get_all_keywords(self, user_id: int = None) -> Dict[str, List[Tuple[str, str]]]: """获取所有Cookie的关键字(支持用户隔离)""" with self.lock: try: cursor = self.conn.cursor() if user_id is not None: cursor.execute(""" SELECT k.cookie_id, k.keyword, k.reply FROM keywords k JOIN cookies c ON k.cookie_id = c.id WHERE c.user_id = ? """, (user_id,)) else: cursor.execute("SELECT cookie_id, keyword, reply FROM keywords") result = {} for row in cursor.fetchall(): cookie_id, keyword, reply = row if cookie_id not in result: result[cookie_id] = [] result[cookie_id].append((keyword, reply)) return result except Exception as e: logger.error(f"获取所有关键字失败: {e}") return {} def save_cookie_status(self, cookie_id: str, enabled: bool): """保存Cookie的启用状态""" with self.lock: try: cursor = self.conn.cursor() cursor.execute(''' INSERT OR REPLACE INTO cookie_status (cookie_id, enabled, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ''', (cookie_id, enabled)) self.conn.commit() logger.debug(f"保存Cookie状态: {cookie_id} -> {'启用' if enabled else '禁用'}") except Exception as e: logger.error(f"保存Cookie状态失败: {e}") raise def get_cookie_status(self, cookie_id: str) -> bool: """获取Cookie的启用状态""" with self.lock: try: cursor = self.conn.cursor() cursor.execute('SELECT enabled FROM cookie_status WHERE cookie_id = ?', (cookie_id,)) result = cursor.fetchone() return bool(result[0]) if result else True # 默认启用 except Exception as e: logger.error(f"获取Cookie状态失败: {e}") return True # 出错时默认启用 def get_all_cookie_status(self) -> Dict[str, bool]: """获取所有Cookie的启用状态""" with self.lock: try: cursor = self.conn.cursor() cursor.execute('SELECT cookie_id, enabled FROM cookie_status') result = {} for row in cursor.fetchall(): cookie_id, enabled = row result[cookie_id] = bool(enabled) return result except Exception as e: logger.error(f"获取所有Cookie状态失败: {e}") return {} # -------------------- AI回复设置操作 -------------------- def save_ai_reply_settings(self, cookie_id: str, settings: dict) -> bool: """保存AI回复设置""" with self.lock: try: cursor = self.conn.cursor() cursor.execute(''' INSERT OR REPLACE INTO ai_reply_settings (cookie_id, ai_enabled, model_name, api_key, base_url, max_discount_percent, max_discount_amount, max_bargain_rounds, custom_prompts, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ''', ( cookie_id, settings.get('ai_enabled', False), settings.get('model_name', 'qwen-plus'), settings.get('api_key', ''), settings.get('base_url', 'https://dashscope.aliyuncs.com/compatible-mode/v1'), settings.get('max_discount_percent', 10), settings.get('max_discount_amount', 100), settings.get('max_bargain_rounds', 3), settings.get('custom_prompts', '') )) self.conn.commit() logger.debug(f"AI回复设置保存成功: {cookie_id}") return True except Exception as e: logger.error(f"保存AI回复设置失败: {e}") self.conn.rollback() return False def get_ai_reply_settings(self, cookie_id: str) -> dict: """获取AI回复设置""" with self.lock: try: cursor = self.conn.cursor() cursor.execute(''' SELECT ai_enabled, model_name, api_key, base_url, max_discount_percent, max_discount_amount, max_bargain_rounds, custom_prompts FROM ai_reply_settings WHERE cookie_id = ? ''', (cookie_id,)) result = cursor.fetchone() if result: return { 'ai_enabled': bool(result[0]), 'model_name': result[1], 'api_key': result[2], 'base_url': result[3], 'max_discount_percent': result[4], 'max_discount_amount': result[5], 'max_bargain_rounds': result[6], 'custom_prompts': result[7] } else: # 返回默认设置 return { 'ai_enabled': False, 'model_name': 'qwen-plus', 'api_key': '', 'base_url': 'https://dashscope.aliyuncs.com/compatible-mode/v1', 'max_discount_percent': 10, 'max_discount_amount': 100, 'max_bargain_rounds': 3, 'custom_prompts': '' } except Exception as e: logger.error(f"获取AI回复设置失败: {e}") return { 'ai_enabled': False, 'model_name': 'qwen-plus', 'api_key': '', 'base_url': 'https://dashscope.aliyuncs.com/compatible-mode/v1', 'max_discount_percent': 10, 'max_discount_amount': 100, 'max_bargain_rounds': 3, 'custom_prompts': '' } def get_all_ai_reply_settings(self) -> Dict[str, dict]: """获取所有账号的AI回复设置""" with self.lock: try: cursor = self.conn.cursor() cursor.execute(''' SELECT cookie_id, ai_enabled, model_name, api_key, base_url, max_discount_percent, max_discount_amount, max_bargain_rounds, custom_prompts FROM ai_reply_settings ''') result = {} for row in cursor.fetchall(): cookie_id = row[0] result[cookie_id] = { 'ai_enabled': bool(row[1]), 'model_name': row[2], 'api_key': row[3], 'base_url': row[4], 'max_discount_percent': row[5], 'max_discount_amount': row[6], 'max_bargain_rounds': row[7], 'custom_prompts': row[8] } return result except Exception as e: logger.error(f"获取所有AI回复设置失败: {e}") return {} # -------------------- 默认回复操作 -------------------- def save_default_reply(self, cookie_id: str, enabled: bool, reply_content: str = None): """保存默认回复设置""" with self.lock: try: cursor = self.conn.cursor() cursor.execute(''' INSERT OR REPLACE INTO default_replies (cookie_id, enabled, reply_content, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP) ''', (cookie_id, enabled, reply_content)) self.conn.commit() logger.debug(f"保存默认回复设置: {cookie_id} -> {'启用' if enabled else '禁用'}") except Exception as e: logger.error(f"保存默认回复设置失败: {e}") raise def get_default_reply(self, cookie_id: str) -> Optional[Dict[str, any]]: """获取指定账号的默认回复设置""" with self.lock: try: cursor = self.conn.cursor() cursor.execute(''' SELECT enabled, reply_content FROM default_replies WHERE cookie_id = ? ''', (cookie_id,)) result = cursor.fetchone() if result: enabled, reply_content = result return { 'enabled': bool(enabled), 'reply_content': reply_content or '' } return None except Exception as e: logger.error(f"获取默认回复设置失败: {e}") return None def get_all_default_replies(self) -> Dict[str, Dict[str, any]]: """获取所有账号的默认回复设置""" with self.lock: try: cursor = self.conn.cursor() cursor.execute('SELECT cookie_id, enabled, reply_content FROM default_replies') result = {} for row in cursor.fetchall(): cookie_id, enabled, reply_content = row result[cookie_id] = { 'enabled': bool(enabled), 'reply_content': reply_content or '' } return result except Exception as e: logger.error(f"获取所有默认回复设置失败: {e}") return {} def delete_default_reply(self, cookie_id: str) -> bool: """删除指定账号的默认回复设置""" with self.lock: try: cursor = self.conn.cursor() cursor.execute("DELETE FROM default_replies WHERE cookie_id = ?", (cookie_id,)) self.conn.commit() logger.debug(f"删除默认回复设置: {cookie_id}") return True except Exception as e: logger.error(f"删除默认回复设置失败: {e}") self.conn.rollback() return False # -------------------- 通知渠道操作 -------------------- def create_notification_channel(self, name: str, channel_type: str, config: str) -> int: """创建通知渠道""" with self.lock: try: cursor = self.conn.cursor() cursor.execute(''' INSERT INTO notification_channels (name, type, config) VALUES (?, ?, ?) ''', (name, channel_type, config)) self.conn.commit() channel_id = cursor.lastrowid logger.debug(f"创建通知渠道: {name} (ID: {channel_id})") return channel_id except Exception as e: logger.error(f"创建通知渠道失败: {e}") self.conn.rollback() raise def get_notification_channels(self) -> List[Dict[str, any]]: """获取所有通知渠道""" with self.lock: try: cursor = self.conn.cursor() cursor.execute(''' SELECT id, name, type, config, enabled, created_at, updated_at FROM notification_channels ORDER BY created_at DESC ''') channels = [] for row in cursor.fetchall(): channels.append({ 'id': row[0], 'name': row[1], 'type': row[2], 'config': row[3], 'enabled': bool(row[4]), 'created_at': row[5], 'updated_at': row[6] }) return channels except Exception as e: logger.error(f"获取通知渠道失败: {e}") return [] def get_notification_channel(self, channel_id: int) -> Optional[Dict[str, any]]: """获取指定通知渠道""" with self.lock: try: cursor = self.conn.cursor() cursor.execute(''' SELECT id, name, type, config, enabled, created_at, updated_at FROM notification_channels WHERE id = ? ''', (channel_id,)) row = cursor.fetchone() if row: return { 'id': row[0], 'name': row[1], 'type': row[2], 'config': row[3], 'enabled': bool(row[4]), 'created_at': row[5], 'updated_at': row[6] } return None except Exception as e: logger.error(f"获取通知渠道失败: {e}") return None def update_notification_channel(self, channel_id: int, name: str, config: str, enabled: bool = True) -> bool: """更新通知渠道""" with self.lock: try: cursor = self.conn.cursor() cursor.execute(''' UPDATE notification_channels SET name = ?, config = ?, enabled = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? ''', (name, config, enabled, channel_id)) self.conn.commit() logger.debug(f"更新通知渠道: {channel_id}") return cursor.rowcount > 0 except Exception as e: logger.error(f"更新通知渠道失败: {e}") self.conn.rollback() return False def delete_notification_channel(self, channel_id: int) -> bool: """删除通知渠道""" with self.lock: try: cursor = self.conn.cursor() cursor.execute("DELETE FROM notification_channels WHERE id = ?", (channel_id,)) self.conn.commit() logger.debug(f"删除通知渠道: {channel_id}") return cursor.rowcount > 0 except Exception as e: logger.error(f"删除通知渠道失败: {e}") self.conn.rollback() return False # -------------------- 消息通知配置操作 -------------------- def set_message_notification(self, cookie_id: str, channel_id: int, enabled: bool = True) -> bool: """设置账号的消息通知""" with self.lock: try: cursor = self.conn.cursor() cursor.execute(''' INSERT OR REPLACE INTO message_notifications (cookie_id, channel_id, enabled) VALUES (?, ?, ?) ''', (cookie_id, channel_id, enabled)) self.conn.commit() logger.debug(f"设置消息通知: {cookie_id} -> {channel_id}") return True except Exception as e: logger.error(f"设置消息通知失败: {e}") self.conn.rollback() return False def get_account_notifications(self, cookie_id: str) -> List[Dict[str, any]]: """获取账号的通知配置""" with self.lock: try: cursor = self.conn.cursor() cursor.execute(''' SELECT mn.id, mn.channel_id, mn.enabled, nc.name, nc.type, nc.config FROM message_notifications mn JOIN notification_channels nc ON mn.channel_id = nc.id WHERE mn.cookie_id = ? AND nc.enabled = 1 ORDER BY mn.id ''', (cookie_id,)) notifications = [] for row in cursor.fetchall(): notifications.append({ 'id': row[0], 'channel_id': row[1], 'enabled': bool(row[2]), 'channel_name': row[3], 'channel_type': row[4], 'channel_config': row[5] }) return notifications except Exception as e: logger.error(f"获取账号通知配置失败: {e}") return [] def get_all_message_notifications(self) -> Dict[str, List[Dict[str, any]]]: """获取所有账号的通知配置""" with self.lock: try: cursor = self.conn.cursor() cursor.execute(''' SELECT mn.cookie_id, mn.id, mn.channel_id, mn.enabled, nc.name, nc.type, nc.config FROM message_notifications mn JOIN notification_channels nc ON mn.channel_id = nc.id WHERE nc.enabled = 1 ORDER BY mn.cookie_id, mn.id ''') result = {} for row in cursor.fetchall(): cookie_id = row[0] if cookie_id not in result: result[cookie_id] = [] result[cookie_id].append({ 'id': row[1], 'channel_id': row[2], 'enabled': bool(row[3]), 'channel_name': row[4], 'channel_type': row[5], 'channel_config': row[6] }) return result except Exception as e: logger.error(f"获取所有消息通知配置失败: {e}") return {} def delete_message_notification(self, notification_id: int) -> bool: """删除消息通知配置""" with self.lock: try: cursor = self.conn.cursor() cursor.execute("DELETE FROM message_notifications WHERE id = ?", (notification_id,)) self.conn.commit() logger.debug(f"删除消息通知配置: {notification_id}") return cursor.rowcount > 0 except Exception as e: logger.error(f"删除消息通知配置失败: {e}") self.conn.rollback() return False def delete_account_notifications(self, cookie_id: str) -> bool: """删除账号的所有消息通知配置""" with self.lock: try: cursor = self.conn.cursor() cursor.execute("DELETE FROM message_notifications WHERE cookie_id = ?", (cookie_id,)) self.conn.commit() logger.debug(f"删除账号通知配置: {cookie_id}") return cursor.rowcount > 0 except Exception as e: logger.error(f"删除账号通知配置失败: {e}") self.conn.rollback() return False # -------------------- 备份和恢复操作 -------------------- def export_backup(self, user_id: int = None) -> Dict[str, any]: """导出系统备份数据(支持用户隔离)""" with self.lock: try: cursor = self.conn.cursor() backup_data = { 'version': '1.0', 'timestamp': time.time(), 'user_id': user_id, 'data': {} } if user_id is not None: # 用户级备份:只备份该用户的数据 # 备份用户的cookies cursor.execute("SELECT * FROM cookies WHERE user_id = ?", (user_id,)) columns = [description[0] for description in cursor.description] rows = cursor.fetchall() backup_data['data']['cookies'] = { 'columns': columns, 'rows': [list(row) for row in rows] } # 备份用户cookies相关的其他数据 user_cookie_ids = [row[0] for row in rows] # 获取用户的cookie_id列表 if user_cookie_ids: placeholders = ','.join(['?' for _ in user_cookie_ids]) # 备份关键字 cursor.execute(f"SELECT * FROM keywords WHERE cookie_id IN ({placeholders})", user_cookie_ids) columns = [description[0] for description in cursor.description] rows = cursor.fetchall() backup_data['data']['keywords'] = { 'columns': columns, 'rows': [list(row) for row in rows] } # 备份其他相关表 related_tables = ['cookie_status', 'default_replies', 'message_notifications', 'item_info', 'ai_reply_settings', 'ai_conversations'] for table in related_tables: cursor.execute(f"SELECT * FROM {table} WHERE cookie_id IN ({placeholders})", user_cookie_ids) columns = [description[0] for description in cursor.description] rows = cursor.fetchall() backup_data['data'][table] = { 'columns': columns, 'rows': [list(row) for row in rows] } else: # 系统级备份:备份所有数据 tables = [ 'cookies', 'keywords', 'cookie_status', 'cards', 'delivery_rules', 'default_replies', 'notification_channels', 'message_notifications', 'system_settings', 'item_info', 'ai_reply_settings', 'ai_conversations', 'ai_item_cache' ] for table in tables: cursor.execute(f"SELECT * FROM {table}") columns = [description[0] for description in cursor.description] rows = cursor.fetchall() backup_data['data'][table] = { 'columns': columns, 'rows': [list(row) for row in rows] } logger.info(f"导出备份成功,用户ID: {user_id}") return backup_data except Exception as e: logger.error(f"导出备份失败: {e}") raise def import_backup(self, backup_data: Dict[str, any], user_id: int = None) -> bool: """导入系统备份数据(支持用户隔离)""" with self.lock: try: # 验证备份数据格式 if not isinstance(backup_data, dict) or 'data' not in backup_data: raise ValueError("备份数据格式无效") # 开始事务 cursor = self.conn.cursor() cursor.execute("BEGIN TRANSACTION") if user_id is not None: # 用户级导入:只清空该用户的数据 # 获取用户的cookie_id列表 cursor.execute("SELECT id FROM cookies WHERE user_id = ?", (user_id,)) user_cookie_ids = [row[0] for row in cursor.fetchall()] if user_cookie_ids: placeholders = ','.join(['?' for _ in user_cookie_ids]) # 删除用户相关数据 related_tables = ['message_notifications', 'default_replies', 'item_info', 'cookie_status', 'keywords', 'ai_conversations', 'ai_reply_settings'] for table in related_tables: cursor.execute(f"DELETE FROM {table} WHERE cookie_id IN ({placeholders})", user_cookie_ids) # 删除用户的cookies cursor.execute("DELETE FROM cookies WHERE user_id = ?", (user_id,)) else: # 系统级导入:清空所有数据(除了用户和管理员密码) tables = [ 'message_notifications', 'notification_channels', 'default_replies', 'delivery_rules', 'cards', 'item_info', 'cookie_status', 'keywords', 'ai_conversations', 'ai_reply_settings', 'ai_item_cache', 'cookies' ] for table in tables: cursor.execute(f"DELETE FROM {table}") # 清空系统设置(保留管理员密码) cursor.execute("DELETE FROM system_settings WHERE key != 'admin_password_hash'") # 导入数据 data = backup_data['data'] for table_name, table_data in data.items(): if table_name not in ['cookies', 'keywords', 'cookie_status', 'cards', 'delivery_rules', 'default_replies', 'notification_channels', 'message_notifications', 'system_settings', 'item_info', 'ai_reply_settings', 'ai_conversations', 'ai_item_cache']: continue columns = table_data['columns'] rows = table_data['rows'] if not rows: continue # 如果是用户级导入,需要确保cookies表的user_id正确 if user_id is not None and table_name == 'cookies': # 更新所有导入的cookies的user_id updated_rows = [] for row in rows: row_dict = dict(zip(columns, row)) row_dict['user_id'] = user_id updated_rows.append([row_dict[col] for col in columns]) rows = updated_rows # 构建插入语句 placeholders = ','.join(['?' for _ in columns]) if table_name == 'system_settings': # 系统设置需要特殊处理,避免覆盖管理员密码 for row in rows: if len(row) >= 1 and row[0] != 'admin_password_hash': cursor.execute(f"INSERT INTO {table_name} ({','.join(columns)}) VALUES ({placeholders})", row) else: cursor.executemany(f"INSERT INTO {table_name} ({','.join(columns)}) VALUES ({placeholders})", rows) # 提交事务 self.conn.commit() logger.info("导入备份成功") return True except Exception as e: logger.error(f"导入备份失败: {e}") self.conn.rollback() return False # -------------------- 系统设置操作 -------------------- def get_system_setting(self, key: str) -> Optional[str]: """获取系统设置""" with self.lock: try: cursor = self.conn.cursor() cursor.execute("SELECT value FROM system_settings WHERE key = ?", (key,)) result = cursor.fetchone() return result[0] if result else None except Exception as e: logger.error(f"获取系统设置失败: {e}") return None def set_system_setting(self, key: str, value: str, description: str = None) -> bool: """设置系统设置""" with self.lock: try: cursor = self.conn.cursor() cursor.execute(''' INSERT OR REPLACE INTO system_settings (key, value, description, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP) ''', (key, value, description)) self.conn.commit() logger.debug(f"设置系统设置: {key}") return True except Exception as e: logger.error(f"设置系统设置失败: {e}") self.conn.rollback() return False def get_all_system_settings(self) -> Dict[str, str]: """获取所有系统设置""" with self.lock: try: cursor = self.conn.cursor() cursor.execute("SELECT key, value FROM system_settings") settings = {} for row in cursor.fetchall(): settings[row[0]] = row[1] return settings except Exception as e: logger.error(f"获取所有系统设置失败: {e}") return {} def verify_admin_password(self, password: str) -> bool: """验证管理员密码""" stored_hash = self.get_system_setting('admin_password_hash') if not stored_hash: return False password_hash = hashlib.sha256(password.encode()).hexdigest() return password_hash == stored_hash def update_admin_password(self, new_password: str) -> bool: """更新管理员密码""" password_hash = hashlib.sha256(new_password.encode()).hexdigest() return self.set_system_setting('admin_password_hash', password_hash, '管理员密码哈希') # ==================== 用户管理方法 ==================== def create_user(self, username: str, email: str, password: str) -> bool: """创建新用户""" with self.lock: try: cursor = self.conn.cursor() password_hash = hashlib.sha256(password.encode()).hexdigest() cursor.execute(''' INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?) ''', (username, email, password_hash)) self.conn.commit() logger.info(f"创建用户成功: {username} ({email})") return True except sqlite3.IntegrityError as e: logger.error(f"创建用户失败,用户名或邮箱已存在: {e}") self.conn.rollback() return False except Exception as e: logger.error(f"创建用户失败: {e}") self.conn.rollback() return False def get_user_by_username(self, username: str) -> Optional[Dict[str, Any]]: """根据用户名获取用户信息""" with self.lock: try: cursor = self.conn.cursor() cursor.execute(''' SELECT id, username, email, password_hash, is_active, created_at, updated_at FROM users WHERE username = ? ''', (username,)) row = cursor.fetchone() if row: return { 'id': row[0], 'username': row[1], 'email': row[2], 'password_hash': row[3], 'is_active': row[4], 'created_at': row[5], 'updated_at': row[6] } return None except Exception as e: logger.error(f"获取用户信息失败: {e}") return None def get_user_by_email(self, email: str) -> Optional[Dict[str, Any]]: """根据邮箱获取用户信息""" with self.lock: try: cursor = self.conn.cursor() cursor.execute(''' SELECT id, username, email, password_hash, is_active, created_at, updated_at FROM users WHERE email = ? ''', (email,)) row = cursor.fetchone() if row: return { 'id': row[0], 'username': row[1], 'email': row[2], 'password_hash': row[3], 'is_active': row[4], 'created_at': row[5], 'updated_at': row[6] } return None except Exception as e: logger.error(f"获取用户信息失败: {e}") return None def verify_user_password(self, username: str, password: str) -> bool: """验证用户密码""" user = self.get_user_by_username(username) if not user: return False password_hash = hashlib.sha256(password.encode()).hexdigest() return user['password_hash'] == password_hash and user['is_active'] def generate_verification_code(self) -> str: """生成6位数字验证码""" return ''.join(random.choices(string.digits, k=6)) def generate_captcha(self) -> Tuple[str, str]: """生成图形验证码 返回: (验证码文本, base64编码的图片) """ try: # 生成4位随机验证码(数字+字母) chars = string.ascii_uppercase + string.digits captcha_text = ''.join(random.choices(chars, k=4)) # 创建图片 width, height = 120, 40 image = Image.new('RGB', (width, height), color='white') draw = ImageDraw.Draw(image) # 尝试使用系统字体,如果失败则使用默认字体 try: # Windows系统字体 font = ImageFont.truetype("arial.ttf", 20) except: try: # 备用字体 font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", 20) except: # 使用默认字体 font = ImageFont.load_default() # 绘制验证码文本 for i, char in enumerate(captcha_text): # 随机颜色 color = ( random.randint(0, 100), random.randint(0, 100), random.randint(0, 100) ) # 随机位置(稍微偏移) x = 20 + i * 20 + random.randint(-3, 3) y = 8 + random.randint(-3, 3) draw.text((x, y), char, font=font, fill=color) # 添加干扰线 for _ in range(3): start = (random.randint(0, width), random.randint(0, height)) end = (random.randint(0, width), random.randint(0, height)) draw.line([start, end], fill=(random.randint(100, 200), random.randint(100, 200), random.randint(100, 200)), width=1) # 添加干扰点 for _ in range(20): x = random.randint(0, width) y = random.randint(0, height) draw.point((x, y), fill=(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))) # 转换为base64 buffer = io.BytesIO() image.save(buffer, format='PNG') img_base64 = base64.b64encode(buffer.getvalue()).decode() return captcha_text, f"data:image/png;base64,{img_base64}" except Exception as e: logger.error(f"生成图形验证码失败: {e}") # 返回简单的文本验证码作为备用 simple_code = ''.join(random.choices(string.digits, k=4)) return simple_code, "" def save_captcha(self, session_id: str, captcha_text: str, expires_minutes: int = 5) -> bool: """保存图形验证码""" with self.lock: try: cursor = self.conn.cursor() expires_at = time.time() + (expires_minutes * 60) # 删除该session的旧验证码 cursor.execute('DELETE FROM captcha_codes WHERE session_id = ?', (session_id,)) cursor.execute(''' INSERT INTO captcha_codes (session_id, code, expires_at) VALUES (?, ?, ?) ''', (session_id, captcha_text.upper(), expires_at)) self.conn.commit() logger.debug(f"保存图形验证码成功: {session_id}") return True except Exception as e: logger.error(f"保存图形验证码失败: {e}") self.conn.rollback() return False def verify_captcha(self, session_id: str, user_input: str) -> bool: """验证图形验证码""" with self.lock: try: cursor = self.conn.cursor() current_time = time.time() # 查找有效的验证码 cursor.execute(''' SELECT id FROM captcha_codes WHERE session_id = ? AND code = ? AND expires_at > ? ORDER BY created_at DESC LIMIT 1 ''', (session_id, user_input.upper(), current_time)) row = cursor.fetchone() if row: # 删除已使用的验证码 cursor.execute('DELETE FROM captcha_codes WHERE id = ?', (row[0],)) self.conn.commit() logger.debug(f"图形验证码验证成功: {session_id}") return True else: logger.warning(f"图形验证码验证失败: {session_id} - {user_input}") return False except Exception as e: logger.error(f"验证图形验证码失败: {e}") return False def save_verification_code(self, email: str, code: str, expires_minutes: int = 10) -> bool: """保存邮箱验证码""" with self.lock: try: cursor = self.conn.cursor() expires_at = time.time() + (expires_minutes * 60) cursor.execute(''' INSERT INTO email_verifications (email, code, expires_at) VALUES (?, ?, ?) ''', (email, code, expires_at)) self.conn.commit() logger.info(f"保存验证码成功: {email}") return True except Exception as e: logger.error(f"保存验证码失败: {e}") self.conn.rollback() return False def verify_email_code(self, email: str, code: str) -> bool: """验证邮箱验证码""" with self.lock: try: cursor = self.conn.cursor() current_time = time.time() # 查找有效的验证码 cursor.execute(''' SELECT id FROM email_verifications WHERE email = ? AND code = ? AND expires_at > ? AND used = FALSE ORDER BY created_at DESC LIMIT 1 ''', (email, code, current_time)) row = cursor.fetchone() if row: # 标记验证码为已使用 cursor.execute(''' UPDATE email_verifications SET used = TRUE WHERE id = ? ''', (row[0],)) self.conn.commit() logger.info(f"验证码验证成功: {email}") return True else: logger.warning(f"验证码验证失败: {email} - {code}") return False except Exception as e: logger.error(f"验证邮箱验证码失败: {e}") return False async def send_verification_email(self, email: str, code: str) -> bool: """发送验证码邮件""" try: subject = "闲鱼自动回复系统 - 邮箱验证码" html_content = f"""