主要功能: - 多用户管理系统 - 浏览器自动化(Playwright) - 任务编排和执行 - Docker容器化部署 - 数据持久化和日志管理 技术栈: - Flask 3.0.0 - Playwright 1.40.0 - SQLite with connection pooling - Docker + Docker Compose 部署说明详见README.md
1067 lines
36 KiB
Python
Executable File
1067 lines
36 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
数据库模块 - 使用SQLite进行数据持久化
|
||
支持VIP功能
|
||
|
||
优化内容:
|
||
1. 清理所有注释掉的代码
|
||
2. 统一使用bcrypt密码哈希
|
||
3. 优化数据库索引
|
||
4. 规范化事务处理
|
||
5. 添加数据迁移功能
|
||
6. 改进错误处理
|
||
"""
|
||
|
||
import sqlite3
|
||
import time
|
||
from datetime import datetime, timedelta
|
||
import pytz
|
||
import threading
|
||
import db_pool
|
||
from password_utils import (
|
||
hash_password_bcrypt,
|
||
verify_password_bcrypt,
|
||
is_sha256_hash,
|
||
verify_password_sha256
|
||
)
|
||
|
||
# 数据库文件路径
|
||
DB_FILE = "data/app_data.db"
|
||
|
||
# 数据库版本 (用于迁移管理)
|
||
DB_VERSION = 2
|
||
|
||
|
||
def hash_password(password):
|
||
"""Password hashing using bcrypt"""
|
||
return hash_password_bcrypt(password)
|
||
|
||
|
||
def init_database():
|
||
"""初始化数据库表结构"""
|
||
db_pool.init_pool(DB_FILE, pool_size=5)
|
||
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
# 管理员表
|
||
cursor.execute('''
|
||
CREATE TABLE IF NOT EXISTS admins (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
username TEXT UNIQUE NOT NULL,
|
||
password_hash TEXT NOT NULL,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
''')
|
||
|
||
# 用户表
|
||
cursor.execute('''
|
||
CREATE TABLE IF NOT EXISTS users (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
username TEXT UNIQUE NOT NULL,
|
||
password_hash TEXT NOT NULL,
|
||
email TEXT,
|
||
status TEXT DEFAULT 'pending',
|
||
vip_expire_time TIMESTAMP,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
approved_at TIMESTAMP
|
||
)
|
||
''')
|
||
|
||
# 账号表(关联用户)
|
||
cursor.execute('''
|
||
CREATE TABLE IF NOT EXISTS accounts (
|
||
id TEXT PRIMARY KEY,
|
||
user_id INTEGER NOT NULL,
|
||
username TEXT NOT NULL,
|
||
password TEXT NOT NULL,
|
||
remember INTEGER DEFAULT 1,
|
||
remark TEXT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||
)
|
||
''')
|
||
|
||
# VIP配置表
|
||
cursor.execute('''
|
||
CREATE TABLE IF NOT EXISTS vip_config (
|
||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||
default_vip_days INTEGER DEFAULT 0,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
''')
|
||
|
||
# 系统配置表
|
||
cursor.execute('''
|
||
CREATE TABLE IF NOT EXISTS system_config (
|
||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||
max_concurrent_global INTEGER DEFAULT 2,
|
||
max_concurrent_per_account INTEGER DEFAULT 1,
|
||
schedule_enabled INTEGER DEFAULT 0,
|
||
schedule_time TEXT DEFAULT '02:00',
|
||
schedule_browse_type TEXT DEFAULT '应读',
|
||
schedule_weekdays TEXT DEFAULT '1,2,3,4,5,6,7',
|
||
proxy_enabled INTEGER DEFAULT 0,
|
||
proxy_api_url TEXT DEFAULT '',
|
||
proxy_expire_minutes INTEGER DEFAULT 3,
|
||
enable_screenshot INTEGER DEFAULT 1,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
''')
|
||
|
||
# 任务日志表
|
||
cursor.execute('''
|
||
CREATE TABLE IF NOT EXISTS task_logs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
account_id TEXT NOT NULL,
|
||
username TEXT NOT NULL,
|
||
browse_type TEXT NOT NULL,
|
||
status TEXT NOT NULL,
|
||
total_items INTEGER DEFAULT 0,
|
||
total_attachments INTEGER DEFAULT 0,
|
||
error_message TEXT,
|
||
duration INTEGER,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||
)
|
||
''')
|
||
|
||
# 密码重置申请表
|
||
cursor.execute('''
|
||
CREATE TABLE IF NOT EXISTS password_reset_requests (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
new_password_hash TEXT NOT NULL,
|
||
status TEXT NOT NULL DEFAULT 'pending',
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
processed_at TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||
)
|
||
''')
|
||
|
||
# 数据库版本表
|
||
cursor.execute('''
|
||
CREATE TABLE IF NOT EXISTS db_version (
|
||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||
version INTEGER NOT NULL,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
''')
|
||
|
||
# ========== 创建索引 ==========
|
||
# 用户表索引
|
||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)')
|
||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_users_status ON users(status)')
|
||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_users_vip_expire ON users(vip_expire_time)')
|
||
|
||
# 账号表索引
|
||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_accounts_user_id ON accounts(user_id)')
|
||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_accounts_username ON accounts(username)')
|
||
|
||
# 任务日志表索引
|
||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_task_logs_user_id ON task_logs(user_id)')
|
||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_task_logs_status ON task_logs(status)')
|
||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_task_logs_created_at ON task_logs(created_at)')
|
||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_task_logs_user_date ON task_logs(user_id, created_at)')
|
||
|
||
# 密码重置表索引
|
||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_password_reset_status ON password_reset_requests(status)')
|
||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_password_reset_user_id ON password_reset_requests(user_id)')
|
||
|
||
# 初始化VIP配置
|
||
try:
|
||
cursor.execute('INSERT INTO vip_config (id, default_vip_days) VALUES (1, 0)')
|
||
conn.commit()
|
||
print("✓ 已创建VIP配置(默认不赠送)")
|
||
except sqlite3.IntegrityError:
|
||
pass
|
||
|
||
# 初始化系统配置
|
||
try:
|
||
cursor.execute('''
|
||
INSERT INTO system_config (
|
||
id, max_concurrent_global, schedule_enabled,
|
||
schedule_time, schedule_browse_type, schedule_weekdays
|
||
) VALUES (1, 2, 0, '02:00', '应读', '1,2,3,4,5,6,7')
|
||
''')
|
||
conn.commit()
|
||
print("✓ 已创建系统配置(默认并发2,定时任务关闭)")
|
||
except sqlite3.IntegrityError:
|
||
pass
|
||
|
||
# 初始化数据库版本
|
||
try:
|
||
cursor.execute('INSERT INTO db_version (id, version) VALUES (1, ?)', (DB_VERSION,))
|
||
conn.commit()
|
||
print(f"✓ 数据库版本: {DB_VERSION}")
|
||
except sqlite3.IntegrityError:
|
||
pass
|
||
|
||
conn.commit()
|
||
print("✓ 数据库初始化完成")
|
||
|
||
# 执行数据迁移
|
||
migrate_database()
|
||
|
||
|
||
def migrate_database():
|
||
"""数据库迁移 - 自动检测并应用必要的迁移"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
# 获取当前数据库版本
|
||
cursor.execute('SELECT version FROM db_version WHERE id = 1')
|
||
row = cursor.fetchone()
|
||
current_version = row['version'] if row else 0
|
||
|
||
print(f"当前数据库版本: {current_version}, 目标版本: {DB_VERSION}")
|
||
|
||
# 应用迁移
|
||
if current_version < 1:
|
||
_migrate_to_v1(conn)
|
||
current_version = 1
|
||
|
||
if current_version < 2:
|
||
_migrate_to_v2(conn)
|
||
current_version = 2
|
||
|
||
# 更新版本号
|
||
cursor.execute('UPDATE db_version SET version = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1',
|
||
(DB_VERSION,))
|
||
conn.commit()
|
||
|
||
if current_version < DB_VERSION:
|
||
print(f"✓ 数据库已迁移到版本 {DB_VERSION}")
|
||
|
||
|
||
def _migrate_to_v1(conn):
|
||
"""迁移到版本1 - 添加缺失字段"""
|
||
cursor = conn.cursor()
|
||
|
||
# 检查并添加 schedule_weekdays 字段
|
||
cursor.execute("PRAGMA table_info(system_config)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
if 'schedule_weekdays' not in columns:
|
||
cursor.execute('ALTER TABLE system_config ADD COLUMN schedule_weekdays TEXT DEFAULT "1,2,3,4,5,6,7"')
|
||
print(" ✓ 添加 schedule_weekdays 字段")
|
||
|
||
if 'max_concurrent_per_account' not in columns:
|
||
cursor.execute('ALTER TABLE system_config ADD COLUMN max_concurrent_per_account INTEGER DEFAULT 1')
|
||
print(" ✓ 添加 max_concurrent_per_account 字段")
|
||
|
||
# 检查并添加 duration 字段到 task_logs
|
||
cursor.execute("PRAGMA table_info(task_logs)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
if 'duration' not in columns:
|
||
cursor.execute('ALTER TABLE task_logs ADD COLUMN duration INTEGER')
|
||
print(" ✓ 添加 duration 字段到 task_logs")
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v2(conn):
|
||
"""迁移到版本2 - 添加代理配置字段"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("PRAGMA table_info(system_config)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
if 'proxy_enabled' not in columns:
|
||
cursor.execute('ALTER TABLE system_config ADD COLUMN proxy_enabled INTEGER DEFAULT 0')
|
||
print(" ✓ 添加 proxy_enabled 字段")
|
||
|
||
if 'proxy_api_url' not in columns:
|
||
cursor.execute('ALTER TABLE system_config ADD COLUMN proxy_api_url TEXT DEFAULT ""')
|
||
print(" ✓ 添加 proxy_api_url 字段")
|
||
|
||
if 'proxy_expire_minutes' not in columns:
|
||
cursor.execute('ALTER TABLE system_config ADD COLUMN proxy_expire_minutes INTEGER DEFAULT 3')
|
||
print(" ✓ 添加 proxy_expire_minutes 字段")
|
||
|
||
if 'enable_screenshot' not in columns:
|
||
cursor.execute('ALTER TABLE system_config ADD COLUMN enable_screenshot INTEGER DEFAULT 1')
|
||
print(" ✓ 添加 enable_screenshot 字段")
|
||
|
||
conn.commit()
|
||
|
||
|
||
# ==================== 管理员相关 ====================
|
||
|
||
def verify_admin(username, password):
|
||
"""验证管理员登录 - 自动从SHA256升级到bcrypt"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT * FROM admins WHERE username = ?', (username,))
|
||
admin = cursor.fetchone()
|
||
|
||
if not admin:
|
||
return None
|
||
|
||
admin_dict = dict(admin)
|
||
password_hash = admin_dict['password_hash']
|
||
|
||
# 检查是否为旧的SHA256哈希
|
||
if is_sha256_hash(password_hash):
|
||
if verify_password_sha256(password, password_hash):
|
||
# 自动升级到bcrypt
|
||
new_hash = hash_password_bcrypt(password)
|
||
cursor.execute('UPDATE admins SET password_hash = ? WHERE username = ?',
|
||
(new_hash, username))
|
||
conn.commit()
|
||
print(f"管理员 {username} 密码已自动升级到bcrypt")
|
||
return admin_dict
|
||
return None
|
||
else:
|
||
# bcrypt验证
|
||
if verify_password_bcrypt(password, password_hash):
|
||
return admin_dict
|
||
return None
|
||
|
||
|
||
def update_admin_password(username, new_password):
|
||
"""更新管理员密码"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
password_hash = hash_password(new_password)
|
||
cursor.execute('UPDATE admins SET password_hash = ? WHERE username = ?',
|
||
(password_hash, username))
|
||
conn.commit()
|
||
return cursor.rowcount > 0
|
||
|
||
|
||
def update_admin_username(old_username, new_username):
|
||
"""更新管理员用户名"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
try:
|
||
cursor.execute('UPDATE admins SET username = ? WHERE username = ?',
|
||
(new_username, old_username))
|
||
conn.commit()
|
||
return True
|
||
except sqlite3.IntegrityError:
|
||
return False
|
||
|
||
|
||
# ==================== VIP管理 ====================
|
||
|
||
def get_vip_config():
|
||
"""获取VIP配置"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT * FROM vip_config WHERE id = 1')
|
||
config = cursor.fetchone()
|
||
return dict(config) if config else {'default_vip_days': 0}
|
||
|
||
|
||
def set_default_vip_days(days):
|
||
"""设置默认VIP天数"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('''
|
||
INSERT OR REPLACE INTO vip_config (id, default_vip_days, updated_at)
|
||
VALUES (1, ?, CURRENT_TIMESTAMP)
|
||
''', (days,))
|
||
conn.commit()
|
||
return True
|
||
|
||
|
||
def set_user_vip(user_id, days):
|
||
"""设置用户VIP - days: 7=一周, 30=一个月, 365=一年, 999999=永久"""
|
||
with db_pool.get_db() as conn:
|
||
cst_tz = pytz.timezone("Asia/Shanghai")
|
||
cursor = conn.cursor()
|
||
|
||
if days == 999999:
|
||
expire_time = '2099-12-31 23:59:59'
|
||
else:
|
||
expire_time = (datetime.now(cst_tz) + timedelta(days=days)).strftime('%Y-%m-%d %H:%M:%S')
|
||
|
||
cursor.execute('UPDATE users SET vip_expire_time = ? WHERE id = ?', (expire_time, user_id))
|
||
conn.commit()
|
||
return cursor.rowcount > 0
|
||
|
||
|
||
def extend_user_vip(user_id, days):
|
||
"""延长用户VIP时间"""
|
||
user = get_user_by_id(user_id)
|
||
cst_tz = pytz.timezone("Asia/Shanghai")
|
||
|
||
if not user:
|
||
return False
|
||
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
current_expire = user.get('vip_expire_time')
|
||
|
||
if current_expire and current_expire != '2099-12-31 23:59:59':
|
||
try:
|
||
expire_time_naive = datetime.strptime(current_expire, '%Y-%m-%d %H:%M:%S')
|
||
expire_time = cst_tz.localize(expire_time_naive)
|
||
now = datetime.now(cst_tz)
|
||
if expire_time < now:
|
||
expire_time = now
|
||
new_expire = (expire_time + timedelta(days=days)).strftime('%Y-%m-%d %H:%M:%S')
|
||
except:
|
||
new_expire = (datetime.now(cst_tz) + timedelta(days=days)).strftime('%Y-%m-%d %H:%M:%S')
|
||
else:
|
||
new_expire = (datetime.now(cst_tz) + timedelta(days=days)).strftime('%Y-%m-%d %H:%M:%S')
|
||
|
||
cursor.execute('UPDATE users SET vip_expire_time = ? WHERE id = ?', (new_expire, user_id))
|
||
conn.commit()
|
||
return cursor.rowcount > 0
|
||
|
||
|
||
def remove_user_vip(user_id):
|
||
"""移除用户VIP"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('UPDATE users SET vip_expire_time = NULL WHERE id = ?', (user_id,))
|
||
conn.commit()
|
||
return cursor.rowcount > 0
|
||
|
||
|
||
def is_user_vip(user_id):
|
||
"""检查用户是否是VIP"""
|
||
cst_tz = pytz.timezone("Asia/Shanghai")
|
||
user = get_user_by_id(user_id)
|
||
|
||
if not user or not user.get('vip_expire_time'):
|
||
return False
|
||
|
||
try:
|
||
expire_time_naive = datetime.strptime(user['vip_expire_time'], '%Y-%m-%d %H:%M:%S')
|
||
expire_time = cst_tz.localize(expire_time_naive)
|
||
return datetime.now(cst_tz) < expire_time
|
||
except:
|
||
return False
|
||
|
||
|
||
def get_user_vip_info(user_id):
|
||
"""获取用户VIP信息"""
|
||
cst_tz = pytz.timezone("Asia/Shanghai")
|
||
user = get_user_by_id(user_id)
|
||
|
||
if not user:
|
||
return {'is_vip': False, 'expire_time': None, 'days_left': 0, 'username': ''}
|
||
|
||
vip_expire_time = user.get('vip_expire_time')
|
||
if not vip_expire_time:
|
||
return {'is_vip': False, 'expire_time': None, 'days_left': 0, 'username': user.get('username', '')}
|
||
|
||
try:
|
||
expire_time_naive = datetime.strptime(vip_expire_time, '%Y-%m-%d %H:%M:%S')
|
||
expire_time = cst_tz.localize(expire_time_naive)
|
||
now = datetime.now(cst_tz)
|
||
is_vip = now < expire_time
|
||
days_left = (expire_time - now).days if is_vip else 0
|
||
|
||
return {
|
||
"username": user.get("username", ""),
|
||
'is_vip': is_vip,
|
||
'expire_time': vip_expire_time,
|
||
'days_left': max(0, days_left)
|
||
}
|
||
except Exception as e:
|
||
print(f"VIP信息获取错误: {e}")
|
||
return {'is_vip': False, 'expire_time': None, 'days_left': 0, 'username': user.get('username', '')}
|
||
|
||
|
||
# ==================== 用户相关 ====================
|
||
|
||
def create_user(username, password, email=''):
|
||
"""创建新用户(待审核状态,赠送默认VIP)"""
|
||
cst_tz = pytz.timezone("Asia/Shanghai")
|
||
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
password_hash = hash_password(password)
|
||
|
||
# 获取默认VIP天数
|
||
default_vip_days = get_vip_config()['default_vip_days']
|
||
vip_expire_time = None
|
||
|
||
if default_vip_days > 0:
|
||
if default_vip_days == 999999:
|
||
vip_expire_time = '2099-12-31 23:59:59'
|
||
else:
|
||
vip_expire_time = (datetime.now(cst_tz) + timedelta(days=default_vip_days)).strftime('%Y-%m-%d %H:%M:%S')
|
||
|
||
try:
|
||
cursor.execute('''
|
||
INSERT INTO users (username, password_hash, email, status, vip_expire_time)
|
||
VALUES (?, ?, ?, 'pending', ?)
|
||
''', (username, password_hash, email, vip_expire_time))
|
||
conn.commit()
|
||
return cursor.lastrowid
|
||
except sqlite3.IntegrityError:
|
||
return None
|
||
|
||
|
||
def verify_user(username, password):
|
||
"""验证用户登录 - 自动从SHA256升级到bcrypt"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute("SELECT * FROM users WHERE username = ? AND status = 'approved'", (username,))
|
||
user = cursor.fetchone()
|
||
|
||
if not user:
|
||
return None
|
||
|
||
user_dict = dict(user)
|
||
password_hash = user_dict['password_hash']
|
||
|
||
# 检查是否为旧的SHA256哈希
|
||
if is_sha256_hash(password_hash):
|
||
if verify_password_sha256(password, password_hash):
|
||
# 自动升级到bcrypt
|
||
new_hash = hash_password_bcrypt(password)
|
||
cursor.execute('UPDATE users SET password_hash = ? WHERE id = ?',
|
||
(new_hash, user_dict['id']))
|
||
conn.commit()
|
||
print(f"用户 {username} 密码已自动升级到bcrypt")
|
||
return user_dict
|
||
return None
|
||
else:
|
||
# bcrypt验证
|
||
if verify_password_bcrypt(password, password_hash):
|
||
return user_dict
|
||
return None
|
||
|
||
|
||
def get_user_by_id(user_id):
|
||
"""根据ID获取用户"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT * FROM users WHERE id = ?', (user_id,))
|
||
user = cursor.fetchone()
|
||
return dict(user) if user else None
|
||
|
||
|
||
def get_user_by_username(username):
|
||
"""根据用户名获取用户"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT * FROM users WHERE username = ?', (username,))
|
||
user = cursor.fetchone()
|
||
return dict(user) if user else None
|
||
|
||
|
||
def get_all_users():
|
||
"""获取所有用户"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT * FROM users ORDER BY created_at DESC')
|
||
return [dict(row) for row in cursor.fetchall()]
|
||
|
||
|
||
def get_pending_users():
|
||
"""获取待审核用户"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute("SELECT * FROM users WHERE status = 'pending' ORDER BY created_at DESC")
|
||
return [dict(row) for row in cursor.fetchall()]
|
||
|
||
|
||
def approve_user(user_id):
|
||
"""审核通过用户"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('''
|
||
UPDATE users
|
||
SET status = 'approved', approved_at = CURRENT_TIMESTAMP
|
||
WHERE id = ?
|
||
''', (user_id,))
|
||
conn.commit()
|
||
return cursor.rowcount > 0
|
||
|
||
|
||
def reject_user(user_id):
|
||
"""拒绝用户"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute("UPDATE users SET status = 'rejected' WHERE id = ?", (user_id,))
|
||
conn.commit()
|
||
return cursor.rowcount > 0
|
||
|
||
|
||
def delete_user(user_id):
|
||
"""删除用户(级联删除相关账号)"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('DELETE FROM users WHERE id = ?', (user_id,))
|
||
conn.commit()
|
||
return cursor.rowcount > 0
|
||
|
||
|
||
# ==================== 账号相关 ====================
|
||
|
||
def create_account(user_id, account_id, username, password, remember=True, remark=''):
|
||
"""创建账号"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('''
|
||
INSERT INTO accounts (id, user_id, username, password, remember, remark)
|
||
VALUES (?, ?, ?, ?, ?, ?)
|
||
''', (account_id, user_id, username, password, 1 if remember else 0, remark))
|
||
conn.commit()
|
||
return cursor.lastrowid
|
||
|
||
|
||
def get_user_accounts(user_id):
|
||
"""获取用户的所有账号"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT * FROM accounts WHERE user_id = ? ORDER BY created_at DESC', (user_id,))
|
||
return [dict(row) for row in cursor.fetchall()]
|
||
|
||
|
||
def get_account(account_id):
|
||
"""获取单个账号"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT * FROM accounts WHERE id = ?', (account_id,))
|
||
row = cursor.fetchone()
|
||
return dict(row) if row else None
|
||
|
||
|
||
def update_account_remark(account_id, remark):
|
||
"""更新账号备注"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('UPDATE accounts SET remark = ? WHERE id = ?', (remark, account_id))
|
||
conn.commit()
|
||
return cursor.rowcount > 0
|
||
|
||
|
||
def delete_account(account_id):
|
||
"""删除账号"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('DELETE FROM accounts WHERE id = ?', (account_id,))
|
||
conn.commit()
|
||
return cursor.rowcount > 0
|
||
|
||
|
||
def delete_user_accounts(user_id):
|
||
"""删除用户的所有账号"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('DELETE FROM accounts WHERE user_id = ?', (user_id,))
|
||
conn.commit()
|
||
return cursor.rowcount
|
||
|
||
|
||
# ==================== 统计相关 ====================
|
||
|
||
def get_user_stats(user_id):
|
||
"""获取用户统计信息"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT COUNT(*) as count FROM accounts WHERE user_id = ?', (user_id,))
|
||
account_count = cursor.fetchone()['count']
|
||
return {'account_count': account_count}
|
||
|
||
|
||
def get_system_stats():
|
||
"""获取系统统计信息"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute('SELECT COUNT(*) as count FROM users')
|
||
total_users = cursor.fetchone()['count']
|
||
|
||
cursor.execute("SELECT COUNT(*) as count FROM users WHERE status = 'approved'")
|
||
approved_users = cursor.fetchone()['count']
|
||
|
||
cursor.execute("SELECT COUNT(*) as count FROM users WHERE status = 'pending'")
|
||
pending_users = cursor.fetchone()['count']
|
||
|
||
cursor.execute('SELECT COUNT(*) as count FROM accounts')
|
||
total_accounts = cursor.fetchone()['count']
|
||
|
||
cursor.execute('''
|
||
SELECT COUNT(*) as count FROM users
|
||
WHERE vip_expire_time IS NOT NULL
|
||
AND datetime(vip_expire_time) > datetime('now')
|
||
''')
|
||
vip_users = cursor.fetchone()['count']
|
||
|
||
return {
|
||
'total_users': total_users,
|
||
'approved_users': approved_users,
|
||
'pending_users': pending_users,
|
||
'total_accounts': total_accounts,
|
||
'vip_users': vip_users
|
||
}
|
||
|
||
|
||
# ==================== 系统配置管理 ====================
|
||
|
||
def get_system_config():
|
||
"""获取系统配置"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('SELECT * FROM system_config WHERE id = 1')
|
||
row = cursor.fetchone()
|
||
|
||
if row:
|
||
return dict(row)
|
||
|
||
# 返回默认值
|
||
return {
|
||
'max_concurrent_global': 2,
|
||
'max_concurrent_per_account': 1,
|
||
'schedule_enabled': 0,
|
||
'schedule_time': '02:00',
|
||
'schedule_browse_type': '应读',
|
||
'schedule_weekdays': '1,2,3,4,5,6,7',
|
||
'proxy_enabled': 0,
|
||
'proxy_api_url': '',
|
||
'proxy_expire_minutes': 3,
|
||
'enable_screenshot': 1
|
||
}
|
||
|
||
|
||
def update_system_config(max_concurrent=None, schedule_enabled=None, schedule_time=None,
|
||
schedule_browse_type=None, schedule_weekdays=None,
|
||
max_concurrent_per_account=None, proxy_enabled=None,
|
||
proxy_api_url=None, proxy_expire_minutes=None):
|
||
"""更新系统配置"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
updates = []
|
||
params = []
|
||
|
||
if max_concurrent is not None:
|
||
updates.append('max_concurrent_global = ?')
|
||
params.append(max_concurrent)
|
||
|
||
if schedule_enabled is not None:
|
||
updates.append('schedule_enabled = ?')
|
||
params.append(schedule_enabled)
|
||
|
||
if schedule_time is not None:
|
||
updates.append('schedule_time = ?')
|
||
params.append(schedule_time)
|
||
|
||
if schedule_browse_type is not None:
|
||
updates.append('schedule_browse_type = ?')
|
||
params.append(schedule_browse_type)
|
||
|
||
if max_concurrent_per_account is not None:
|
||
updates.append('max_concurrent_per_account = ?')
|
||
params.append(max_concurrent_per_account)
|
||
|
||
if schedule_weekdays is not None:
|
||
updates.append('schedule_weekdays = ?')
|
||
params.append(schedule_weekdays)
|
||
|
||
if proxy_enabled is not None:
|
||
updates.append('proxy_enabled = ?')
|
||
params.append(proxy_enabled)
|
||
|
||
if proxy_api_url is not None:
|
||
updates.append('proxy_api_url = ?')
|
||
params.append(proxy_api_url)
|
||
|
||
if proxy_expire_minutes is not None:
|
||
updates.append('proxy_expire_minutes = ?')
|
||
params.append(proxy_expire_minutes)
|
||
|
||
if updates:
|
||
updates.append('updated_at = CURRENT_TIMESTAMP')
|
||
sql = f"UPDATE system_config SET {', '.join(updates)} WHERE id = 1"
|
||
cursor.execute(sql, params)
|
||
conn.commit()
|
||
return True
|
||
|
||
return False
|
||
|
||
|
||
# ==================== 任务日志管理 ====================
|
||
|
||
def create_task_log(user_id, account_id, username, browse_type, status,
|
||
total_items=0, total_attachments=0, error_message='', duration=None):
|
||
"""创建任务日志记录"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cst_tz = pytz.timezone("Asia/Shanghai")
|
||
cst_time = datetime.now(cst_tz).strftime("%Y-%m-%d %H:%M:%S")
|
||
|
||
cursor.execute('''
|
||
INSERT INTO task_logs (
|
||
user_id, account_id, username, browse_type, status,
|
||
total_items, total_attachments, error_message, duration, created_at
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
''', (user_id, account_id, username, browse_type, status,
|
||
total_items, total_attachments, error_message, duration, cst_time))
|
||
|
||
conn.commit()
|
||
return cursor.lastrowid
|
||
|
||
|
||
def get_task_logs(limit=100, offset=0, date_filter=None, status_filter=None):
|
||
"""获取任务日志列表"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
sql = '''
|
||
SELECT
|
||
tl.*,
|
||
u.username as user_username
|
||
FROM task_logs tl
|
||
LEFT JOIN users u ON tl.user_id = u.id
|
||
WHERE 1=1
|
||
'''
|
||
params = []
|
||
|
||
if date_filter:
|
||
sql += " AND date(tl.created_at) = ?"
|
||
params.append(date_filter)
|
||
|
||
if status_filter:
|
||
sql += " AND tl.status = ?"
|
||
params.append(status_filter)
|
||
|
||
sql += " ORDER BY tl.created_at DESC LIMIT ? OFFSET ?"
|
||
params.extend([limit, offset])
|
||
|
||
cursor.execute(sql, params)
|
||
return [dict(row) for row in cursor.fetchall()]
|
||
|
||
|
||
def get_task_stats(date_filter=None):
|
||
"""获取任务统计信息"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cst_tz = pytz.timezone("Asia/Shanghai")
|
||
|
||
if date_filter is None:
|
||
date_filter = datetime.now(cst_tz).strftime('%Y-%m-%d')
|
||
|
||
# 当日统计
|
||
cursor.execute('''
|
||
SELECT
|
||
COUNT(*) as total_tasks,
|
||
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success_tasks,
|
||
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed_tasks,
|
||
SUM(total_items) as total_items,
|
||
SUM(total_attachments) as total_attachments
|
||
FROM task_logs
|
||
WHERE date(created_at) = ?
|
||
''', (date_filter,))
|
||
|
||
today_stats = cursor.fetchone()
|
||
|
||
# 历史累计统计
|
||
cursor.execute('''
|
||
SELECT
|
||
COUNT(*) as total_tasks,
|
||
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success_tasks,
|
||
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed_tasks,
|
||
SUM(total_items) as total_items,
|
||
SUM(total_attachments) as total_attachments
|
||
FROM task_logs
|
||
''')
|
||
|
||
total_stats = cursor.fetchone()
|
||
|
||
return {
|
||
'today': {
|
||
'total_tasks': today_stats['total_tasks'] or 0,
|
||
'success_tasks': today_stats['success_tasks'] or 0,
|
||
'failed_tasks': today_stats['failed_tasks'] or 0,
|
||
'total_items': today_stats['total_items'] or 0,
|
||
'total_attachments': today_stats['total_attachments'] or 0
|
||
},
|
||
'total': {
|
||
'total_tasks': total_stats['total_tasks'] or 0,
|
||
'success_tasks': total_stats['success_tasks'] or 0,
|
||
'failed_tasks': total_stats['failed_tasks'] or 0,
|
||
'total_items': total_stats['total_items'] or 0,
|
||
'total_attachments': total_stats['total_attachments'] or 0
|
||
}
|
||
}
|
||
|
||
|
||
def delete_old_task_logs(days=30):
|
||
"""删除N天前的任务日志"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('''
|
||
DELETE FROM task_logs
|
||
WHERE created_at < datetime('now', '-' || ? || ' days')
|
||
''', (days,))
|
||
conn.commit()
|
||
return cursor.rowcount
|
||
|
||
|
||
def get_user_run_stats(user_id, date_filter=None):
|
||
"""获取用户的运行统计信息"""
|
||
with db_pool.get_db() as conn:
|
||
cst_tz = pytz.timezone("Asia/Shanghai")
|
||
cursor = conn.cursor()
|
||
|
||
if date_filter is None:
|
||
date_filter = datetime.now(cst_tz).strftime('%Y-%m-%d')
|
||
|
||
cursor.execute('''
|
||
SELECT
|
||
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as completed,
|
||
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed,
|
||
SUM(total_items) as total_items,
|
||
SUM(total_attachments) as total_attachments
|
||
FROM task_logs
|
||
WHERE user_id = ? AND date(created_at) = ?
|
||
''', (user_id, date_filter))
|
||
|
||
stats = cursor.fetchone()
|
||
|
||
return {
|
||
'completed': stats['completed'] or 0,
|
||
'failed': stats['failed'] or 0,
|
||
'total_items': stats['total_items'] or 0,
|
||
'total_attachments': stats['total_attachments'] or 0
|
||
}
|
||
|
||
|
||
# ==================== 密码重置功能 ====================
|
||
|
||
def create_password_reset_request(user_id, new_password):
|
||
"""创建密码重置申请 - 使用bcrypt哈希"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
password_hash = hash_password_bcrypt(new_password)
|
||
|
||
try:
|
||
cursor.execute('''
|
||
INSERT INTO password_reset_requests (user_id, new_password_hash, status)
|
||
VALUES (?, ?, 'pending')
|
||
''', (user_id, password_hash))
|
||
conn.commit()
|
||
return cursor.lastrowid
|
||
except Exception as e:
|
||
print(f"创建密码重置申请失败: {e}")
|
||
return None
|
||
|
||
|
||
def get_pending_password_resets():
|
||
"""获取所有待审核的密码重置申请"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
cursor.execute('''
|
||
SELECT r.id, r.user_id, r.created_at, r.status,
|
||
u.username, u.email
|
||
FROM password_reset_requests r
|
||
JOIN users u ON r.user_id = u.id
|
||
WHERE r.status = 'pending'
|
||
ORDER BY r.created_at DESC
|
||
''')
|
||
return [dict(row) for row in cursor.fetchall()]
|
||
|
||
|
||
def approve_password_reset(request_id):
|
||
"""批准密码重置申请"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
try:
|
||
# 获取申请信息
|
||
cursor.execute('''
|
||
SELECT user_id, new_password_hash
|
||
FROM password_reset_requests
|
||
WHERE id = ? AND status = 'pending'
|
||
''', (request_id,))
|
||
|
||
result = cursor.fetchone()
|
||
if not result:
|
||
return False
|
||
|
||
user_id = result['user_id']
|
||
new_password_hash = result['new_password_hash']
|
||
|
||
# 更新用户密码
|
||
cursor.execute('UPDATE users SET password_hash = ? WHERE id = ?',
|
||
(new_password_hash, user_id))
|
||
|
||
# 更新申请状态
|
||
cursor.execute('''
|
||
UPDATE password_reset_requests
|
||
SET status = 'approved', processed_at = CURRENT_TIMESTAMP
|
||
WHERE id = ?
|
||
''', (request_id,))
|
||
|
||
conn.commit()
|
||
return True
|
||
except Exception as e:
|
||
print(f"批准密码重置失败: {e}")
|
||
return False
|
||
|
||
|
||
def reject_password_reset(request_id):
|
||
"""拒绝密码重置申请"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
try:
|
||
cursor.execute('''
|
||
UPDATE password_reset_requests
|
||
SET status = 'rejected', processed_at = CURRENT_TIMESTAMP
|
||
WHERE id = ? AND status = 'pending'
|
||
''', (request_id,))
|
||
conn.commit()
|
||
return cursor.rowcount > 0
|
||
except Exception as e:
|
||
print(f"拒绝密码重置失败: {e}")
|
||
return False
|
||
|
||
|
||
def admin_reset_user_password(user_id, new_password):
|
||
"""管理员直接重置用户密码 - 使用bcrypt哈希"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
password_hash = hash_password_bcrypt(new_password)
|
||
|
||
try:
|
||
cursor.execute('UPDATE users SET password_hash = ? WHERE id = ?',
|
||
(password_hash, user_id))
|
||
conn.commit()
|
||
return cursor.rowcount > 0
|
||
except Exception as e:
|
||
print(f"管理员重置密码失败: {e}")
|
||
return False
|
||
|
||
|
||
# ==================== 日志清理 ====================
|
||
|
||
def clean_old_operation_logs(days=30):
|
||
"""清理指定天数前的操作日志(如果存在operation_logs表)"""
|
||
with db_pool.get_db() as conn:
|
||
cursor = conn.cursor()
|
||
|
||
# 检查表是否存在
|
||
cursor.execute("""
|
||
SELECT name FROM sqlite_master
|
||
WHERE type='table' AND name='operation_logs'
|
||
""")
|
||
|
||
if not cursor.fetchone():
|
||
return 0
|
||
|
||
try:
|
||
cursor.execute('''
|
||
DELETE FROM operation_logs
|
||
WHERE created_at < datetime('now', '-' || ? || ' days')
|
||
''', (days,))
|
||
deleted_count = cursor.rowcount
|
||
conn.commit()
|
||
print(f"已清理 {deleted_count} 条旧操作日志 (>{days}天)")
|
||
return deleted_count
|
||
except Exception as e:
|
||
print(f"清理旧操作日志失败: {e}")
|
||
return 0
|