Files
zsglpt/database.py
Yu Yon 0fd7137cea Initial commit: 知识管理平台
主要功能:
- 多用户管理系统
- 浏览器自动化(Playwright)
- 任务编排和执行
- Docker容器化部署
- 数据持久化和日志管理

技术栈:
- Flask 3.0.0
- Playwright 1.40.0
- SQLite with connection pooling
- Docker + Docker Compose

部署说明详见README.md
2025-11-16 19:03:07 +08:00

1067 lines
36 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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