fix: 账号页闪烁/浏览类型/截图复制/时区统一
This commit is contained in:
139
email_service.py
139
email_service.py
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user