fix: 账号页闪烁/浏览类型/截图复制/时区统一

This commit is contained in:
2025-12-14 11:30:49 +08:00
parent 2ec88eac3b
commit a9c8aac48f
59 changed files with 685 additions and 339 deletions

View File

@@ -27,6 +27,10 @@ BEIJING_TZ = pytz.timezone('Asia/Shanghai')
def get_beijing_today():
"""获取北京时间的今天日期字符串"""
return datetime.now(BEIJING_TZ).strftime('%Y-%m-%d')
def get_beijing_now_str():
"""获取北京时间的当前时间字符串"""
return datetime.now(BEIJING_TZ).strftime('%Y-%m-%d %H:%M:%S')
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
@@ -44,7 +48,13 @@ from crypto_utils import encrypt_password, decrypt_password, is_encrypted
def parse_datetime(dt_str: str) -> datetime:
"""解析数据库中的时间字符串,支持带微秒和不带微秒的格式"""
if not dt_str:
return datetime.min
return BEIJING_TZ.localize(datetime(1970, 1, 1))
# 兼容 sqlite3 可能返回 datetime 对象的情况
if isinstance(dt_str, datetime):
return dt_str.astimezone(BEIJING_TZ) if dt_str.tzinfo else BEIJING_TZ.localize(dt_str)
text = str(dt_str)
# 尝试多种格式
formats = [
'%Y-%m-%d %H:%M:%S.%f', # 带微秒
@@ -52,11 +62,12 @@ def parse_datetime(dt_str: str) -> datetime:
]
for fmt in formats:
try:
return datetime.strptime(dt_str, fmt)
naive = datetime.strptime(text, fmt)
return BEIJING_TZ.localize(naive)
except ValueError:
continue
# 如果都失败,返回最小时间(视为过期)
return datetime.min
return BEIJING_TZ.localize(datetime(1970, 1, 1))
# ============ 常量配置 ============
@@ -146,10 +157,13 @@ def init_email_tables():
""")
# 初始化默认设置
cursor.execute("""
INSERT OR IGNORE INTO email_settings (id, enabled, failover_enabled)
VALUES (1, 0, 1)
""")
cursor.execute(
"""
INSERT OR IGNORE INTO email_settings (id, enabled, failover_enabled, updated_at)
VALUES (1, 0, 1, ?)
""",
(get_beijing_now_str(),),
)
# 3. 邮件验证Token表
cursor.execute("""
@@ -208,9 +222,12 @@ def init_email_tables():
""")
# 初始化统计记录
cursor.execute("""
INSERT OR IGNORE INTO email_stats (id) VALUES (1)
""")
cursor.execute(
"""
INSERT OR IGNORE INTO email_stats (id, last_updated) VALUES (1, ?)
""",
(get_beijing_now_str(),),
)
conn.commit()
print("[邮件服务] 数据库表初始化完成")
@@ -276,8 +293,8 @@ def update_email_settings(
cursor = conn.cursor()
# 构建动态更新语句
updates = ['enabled = ?', 'failover_enabled = ?', 'updated_at = CURRENT_TIMESTAMP']
params = [int(enabled), int(failover_enabled)]
updates = ['enabled = ?', 'failover_enabled = ?', 'updated_at = ?']
params = [int(enabled), int(failover_enabled), get_beijing_now_str()]
if register_verify_enabled is not None:
updates.append('register_verify_enabled = ?')
@@ -416,6 +433,7 @@ def create_smtp_config(data: Dict[str, Any]) -> int:
"""创建新的SMTP配置"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
now_str = get_beijing_now_str()
# 加密密码
password = encrypt_password(data.get('password', '')) if data.get('password') else ''
@@ -423,8 +441,8 @@ def create_smtp_config(data: Dict[str, Any]) -> int:
cursor.execute("""
INSERT INTO smtp_configs
(name, enabled, is_primary, priority, host, port, username, password,
use_ssl, use_tls, sender_name, sender_email, daily_limit)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
use_ssl, use_tls, sender_name, sender_email, daily_limit, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
data.get('name', '默认配置'),
int(data.get('enabled', True)),
@@ -438,7 +456,9 @@ def create_smtp_config(data: Dict[str, Any]) -> int:
int(data.get('use_tls', False)),
data.get('sender_name', '自动化学习'),
data.get('sender_email', ''),
data.get('daily_limit', 0)
data.get('daily_limit', 0),
now_str,
now_str,
))
config_id = cursor.lastrowid
@@ -484,7 +504,8 @@ def update_smtp_config(config_id: int, data: Dict[str, Any]) -> bool:
if not updates:
return False
updates.append("updated_at = CURRENT_TIMESTAMP")
updates.append("updated_at = ?")
params.append(get_beijing_now_str())
params.append(config_id)
cursor.execute(f"""
@@ -515,9 +536,9 @@ def set_primary_smtp_config(config_id: int) -> bool:
# 设置新的主配置
cursor.execute("""
UPDATE smtp_configs
SET is_primary = 1, updated_at = CURRENT_TIMESTAMP
SET is_primary = 1, updated_at = ?
WHERE id = ?
""", (config_id,))
""", (get_beijing_now_str(), config_id))
conn.commit()
return cursor.rowcount > 0
@@ -584,25 +605,26 @@ def _update_smtp_stats(config_id: int, success: bool, error: str = ''):
"""更新SMTP配置的统计信息"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
now_str = get_beijing_now_str()
if success:
cursor.execute("""
UPDATE smtp_configs
SET daily_sent = daily_sent + 1,
success_count = success_count + 1,
last_success_at = CURRENT_TIMESTAMP,
last_success_at = ?,
last_error = '',
updated_at = CURRENT_TIMESTAMP
updated_at = ?
WHERE id = ?
""", (config_id,))
""", (now_str, now_str, config_id))
else:
cursor.execute("""
UPDATE smtp_configs
SET fail_count = fail_count + 1,
last_error = ?,
updated_at = CURRENT_TIMESTAMP
updated_at = ?
WHERE id = ?
""", (error[:500], config_id))
""", (error[:500], now_str, config_id))
conn.commit()
@@ -864,11 +886,12 @@ def test_smtp_config(config_id: int, test_email: str) -> Dict[str, Any]:
# 更新最后成功时间
with db_pool.get_db() as conn:
cursor = conn.cursor()
now_str = get_beijing_now_str()
cursor.execute("""
UPDATE smtp_configs
SET last_success_at = CURRENT_TIMESTAMP, last_error = ''
SET last_success_at = ?, last_error = '', updated_at = ?
WHERE id = ?
""", (config_id,))
""", (now_str, now_str, config_id))
conn.commit()
return {'success': True, 'error': ''}
@@ -899,10 +922,10 @@ def _log_email_send(user_id: int, smtp_config_id: int, email_to: str,
cursor.execute("""
INSERT INTO email_logs
(user_id, smtp_config_id, email_to, email_type, subject, status,
error_message, attachment_count, attachment_size)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
error_message, attachment_count, attachment_size, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (user_id, smtp_config_id, email_to, email_type, subject, status,
error_message, attachment_count, attachment_size))
error_message, attachment_count, attachment_size, get_beijing_now_str()))
conn.commit()
@@ -910,6 +933,7 @@ def _update_email_stats(email_type: str, success: bool):
"""更新邮件统计"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
now_str = get_beijing_now_str()
type_field_map = {
EMAIL_TYPE_REGISTER: 'register_sent',
@@ -927,25 +951,25 @@ def _update_email_stats(email_type: str, success: bool):
SET total_sent = total_sent + 1,
total_success = total_success + 1,
{type_field} = {type_field} + 1,
last_updated = CURRENT_TIMESTAMP
last_updated = ?
WHERE id = 1
""")
""", (now_str,))
else:
cursor.execute("""
UPDATE email_stats
SET total_sent = total_sent + 1,
total_success = total_success + 1,
last_updated = CURRENT_TIMESTAMP
last_updated = ?
WHERE id = 1
""")
""", (now_str,))
else:
cursor.execute("""
UPDATE email_stats
SET total_sent = total_sent + 1,
total_failed = total_failed + 1,
last_updated = CURRENT_TIMESTAMP
last_updated = ?
WHERE id = 1
""")
""", (now_str,))
conn.commit()
@@ -1061,10 +1085,14 @@ def cleanup_email_logs(days: int = 30) -> int:
"""清理过期邮件日志"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cursor.execute("""
cutoff = (datetime.now(BEIJING_TZ) - timedelta(days=int(days))).strftime('%Y-%m-%d %H:%M:%S')
cursor.execute(
"""
DELETE FROM email_logs
WHERE datetime(created_at) < datetime('now', '-' || ? || ' days')
""", (days,))
WHERE datetime(created_at) < datetime(?)
""",
(cutoff,),
)
deleted = cursor.rowcount
conn.commit()
return deleted
@@ -1083,14 +1111,19 @@ def generate_email_token(email: str, token_type: str, user_id: int = None) -> st
EMAIL_TYPE_BIND: TOKEN_EXPIRE_BIND
}.get(token_type, TOKEN_EXPIRE_REGISTER)
expires_at = datetime.now() + timedelta(seconds=expire_seconds)
now = datetime.now(BEIJING_TZ)
now_str = now.strftime('%Y-%m-%d %H:%M:%S')
expires_at_str = (now + timedelta(seconds=expire_seconds)).strftime('%Y-%m-%d %H:%M:%S')
with db_pool.get_db() as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO email_tokens (user_id, email, token, token_type, expires_at)
VALUES (?, ?, ?, ?, ?)
""", (user_id, email, token, token_type, expires_at))
cursor.execute(
"""
INSERT INTO email_tokens (user_id, email, token, token_type, expires_at, created_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
(user_id, email, token, token_type, expires_at_str, now_str),
)
conn.commit()
return token
@@ -1122,7 +1155,7 @@ def verify_email_token(token: str, token_type: str) -> Optional[Dict[str, Any]]:
return None
# 检查是否过期
if parse_datetime(expires_at) < datetime.now():
if parse_datetime(expires_at) < datetime.now(BEIJING_TZ):
return None
# 标记为已使用
@@ -1161,7 +1194,7 @@ def check_rate_limit(email: str, token_type: str) -> bool:
return True
last_sent = parse_datetime(row[0])
elapsed = (datetime.now() - last_sent).total_seconds()
elapsed = (datetime.now(BEIJING_TZ) - last_sent).total_seconds()
return elapsed >= limit_seconds
@@ -1170,10 +1203,14 @@ def cleanup_expired_tokens() -> int:
"""清理过期Token"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
cursor.execute("""
now_str = get_beijing_now_str()
cursor.execute(
"""
DELETE FROM email_tokens
WHERE datetime(expires_at) < datetime('now')
""")
WHERE datetime(expires_at) < datetime(?)
""",
(now_str,),
)
deleted = cursor.rowcount
conn.commit()
return deleted
@@ -1436,7 +1473,7 @@ def verify_password_reset_token(token: str) -> Optional[Dict[str, Any]]:
return None
# 检查是否过期
if parse_datetime(expires_at) < datetime.now():
if parse_datetime(expires_at) < datetime.now(BEIJING_TZ):
return None
return {'user_id': user_id, 'email': email, 'token_id': token_id}
@@ -1827,7 +1864,7 @@ def send_task_complete_email(
return {'success': False, 'error': '用户未设置邮箱', 'emails_sent': 0}
# 获取完成时间
complete_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
complete_time = get_beijing_now_str()
# 读取截图文件
screenshot_data = None
@@ -2045,7 +2082,7 @@ def send_batch_task_complete_email(
return {'success': False, 'error': '没有截图需要发送'}
# 获取完成时间
complete_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
complete_time = get_beijing_now_str()
# 统计信息
total_items_sum = sum(s.get('items', 0) for s in screenshots)
@@ -2131,7 +2168,7 @@ def send_batch_task_complete_email(
else:
with open(zip_path, 'rb') as f:
zip_data = f.read()
zip_filename = f"screenshots_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
zip_filename = f"screenshots_{datetime.now(BEIJING_TZ).strftime('%Y%m%d_%H%M%S')}.zip"
attachment_note = "截图已打包为ZIP附件请查收。"
except Exception as e:
print(f"[邮件] 打包截图失败: {e}")