mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 14:06:28 +00:00
feat: migrate from Element Plus to Ant Design Vue and update Vite configuration for better dependency management
- Updated Vite configuration to manually chunk Ant Design Vue for improved dependency management. - Added a comprehensive migration testing checklist for transitioning from Element Plus 2.13.0 to Ant Design Vue 4.x, covering various components and functionalities.
This commit is contained in:
@@ -9,7 +9,6 @@ from selenium.webdriver.chrome.options import Options
|
||||
from typing import Dict, Any
|
||||
|
||||
from backend.config import settings
|
||||
from backend.workers.email_notifier import send_success_notification, send_failure_notification
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -99,11 +98,20 @@ def get_live_x_api_payload(auth_token: str) -> str:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取现场 x-api-request-payload 时失败: {e}")
|
||||
debug_screenshot = os.path.join(settings.BASE_DIR, 'payload_debug.png')
|
||||
driver.save_screenshot(debug_screenshot)
|
||||
try:
|
||||
debug_screenshot = os.path.join(settings.BASE_DIR, 'payload_debug.png')
|
||||
driver.save_screenshot(debug_screenshot)
|
||||
except Exception as screenshot_error:
|
||||
logger.warning(f"保存调试截图失败: {screenshot_error}")
|
||||
|
||||
finally:
|
||||
driver.quit()
|
||||
# 优雅关闭 WebDriver,避免 Windows asyncio ConnectionResetError
|
||||
try:
|
||||
driver.quit()
|
||||
except Exception as e:
|
||||
# 忽略 WebDriver 关闭时的连接错误(Windows 平台常见问题)
|
||||
if "WinError 10054" not in str(e) and "ConnectionResetError" not in str(e):
|
||||
logger.warning(f"关闭 WebDriver 时出现警告: {e}")
|
||||
|
||||
return payload_signature
|
||||
|
||||
@@ -127,7 +135,8 @@ def perform_check_in(task, user_token: str) -> Dict[str, Any]:
|
||||
try:
|
||||
payload_dict = json.loads(task.payload_config) if task.payload_config else {}
|
||||
signature = payload_dict.get('Signature', 'Unknown')
|
||||
except:
|
||||
except (json.JSONDecodeError, KeyError, TypeError, AttributeError) as e:
|
||||
logger.debug(f"解析任务 {task.id} 的 payload_config 失败: {e}")
|
||||
signature = 'Unknown'
|
||||
|
||||
logger.info(f"Selenium打卡: 正在为任务 ID: {task.id} (Signature: {signature}) 执行打卡...")
|
||||
@@ -196,14 +205,21 @@ def perform_check_in(task, user_token: str) -> Dict[str, Any]:
|
||||
logger.info(f"✉️ 任务 ID: {task.id} (Signature: {signature}) 打卡请求完成!响应: {response_text}")
|
||||
|
||||
# 判断响应内容(参考 V1 实现逻辑)
|
||||
# 使用用户账户的邮箱,而不是任务的邮箱
|
||||
email = task.user.email if task.user else None
|
||||
|
||||
# 情况1: 明确包含"打卡成功" → 成功
|
||||
if "打卡成功" in response_text:
|
||||
logger.info(f"✅ 检测到成功关键字 '打卡成功',打卡成功")
|
||||
if email:
|
||||
send_success_notification(email)
|
||||
# 发送成功邮件通知
|
||||
if task.user and task.user.email:
|
||||
try:
|
||||
from backend.services.email_service import EmailService
|
||||
task_info = {
|
||||
'thread_id': payload.get('ThreadId', '未知'),
|
||||
'name': getattr(task, 'name', '打卡任务')
|
||||
}
|
||||
EmailService.notify_check_in_result(task.user, task_info, True, "打卡成功")
|
||||
except Exception as e:
|
||||
logger.error(f"发送打卡成功邮件失败: {e}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"status": "success",
|
||||
@@ -238,8 +254,18 @@ def perform_check_in(task, user_token: str) -> Dict[str, Any]:
|
||||
# 情况4: Token 失效的特征标识 → 失败
|
||||
elif ("登录" in response_text):
|
||||
logger.warning(f"⚠️ 检测到登录失败关键字,Token 可能已失效")
|
||||
if email:
|
||||
send_failure_notification(email)
|
||||
# 发送失败邮件通知
|
||||
if task.user and task.user.email:
|
||||
try:
|
||||
from backend.services.email_service import EmailService
|
||||
task_info = {
|
||||
'thread_id': payload.get('ThreadId', '未知'),
|
||||
'name': getattr(task, 'name', '打卡任务')
|
||||
}
|
||||
EmailService.notify_check_in_result(task.user, task_info, False, "Token 已失效,需要重新授权")
|
||||
except Exception as e:
|
||||
logger.error(f"发送打卡失败邮件失败: {e}")
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"status": "failure",
|
||||
|
||||
@@ -1,512 +1,119 @@
|
||||
"""
|
||||
邮件发送引擎 (底层)
|
||||
|
||||
职能:提供基础的 SMTP 邮件发送功能
|
||||
- SMTP 服务器连接
|
||||
- 邮件发送
|
||||
- 配置管理
|
||||
- 不包含业务逻辑
|
||||
"""
|
||||
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
import time
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from backend.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# --- 邮件模板 ---
|
||||
|
||||
EXPIRATION_HTML_TEMPLATE = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Token 到期提醒</title>
|
||||
<style>
|
||||
body {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
line-height: 1.6;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 600px;
|
||||
margin: 40px auto;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}}
|
||||
.header {{
|
||||
background: linear-gradient(135deg, #f44336 0%, #e91e63 100%);
|
||||
color: white;
|
||||
padding: 30px 20px;
|
||||
text-align: center;
|
||||
}}
|
||||
.header h1 {{
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}}
|
||||
.content {{
|
||||
padding: 30px 40px;
|
||||
}}
|
||||
.alert-box {{
|
||||
background-color: #fff3e0;
|
||||
border-left: 4px solid #ff9800;
|
||||
padding: 16px;
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
.info-item {{
|
||||
margin: 16px 0;
|
||||
padding: 12px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
}}
|
||||
.info-item strong {{
|
||||
color: #333;
|
||||
display: inline-block;
|
||||
min-width: 100px;
|
||||
}}
|
||||
.highlight {{
|
||||
color: #f44336;
|
||||
font-weight: 600;
|
||||
}}
|
||||
.action-button {{
|
||||
display: inline-block;
|
||||
margin: 20px 0;
|
||||
padding: 12px 32px;
|
||||
background: linear-gradient(135deg, #f44336 0%, #e91e63 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}}
|
||||
.footer {{
|
||||
background-color: #fafafa;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
border-top: 1px solid #eeeeee;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>⚠️ Token 即将到期提醒</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>您好,</p>
|
||||
<div class="alert-box">
|
||||
<p>您的接龙打卡系统 <span class="highlight">Token 即将到期</span>,为避免影响自动打卡功能,请尽快刷新您的 Token。</p>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>到期时间:</strong><span class="highlight">{exp_time}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>通知时间:</strong>{send_time}
|
||||
</div>
|
||||
<p style="margin-top: 20px; color: #666;">
|
||||
请登录系统,前往 <strong>用户设置</strong> 页面刷新您的 Token,以确保自动打卡功能正常运行。
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>此邮件由接龙自动打卡系统自动发送,请勿直接回复</p>
|
||||
<p>CheckIn App V2 © 2026</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
class EmailNotifier:
|
||||
"""邮件发送引擎(底层服务)"""
|
||||
|
||||
SUCCESS_HTML_TEMPLATE = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>打卡成功通知</title>
|
||||
<style>
|
||||
body {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
line-height: 1.6;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 600px;
|
||||
margin: 40px auto;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}}
|
||||
.header {{
|
||||
background: linear-gradient(135deg, #4caf50 0%, #66bb6a 100%);
|
||||
color: white;
|
||||
padding: 30px 20px;
|
||||
text-align: center;
|
||||
}}
|
||||
.header h1 {{
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}}
|
||||
.content {{
|
||||
padding: 30px 40px;
|
||||
}}
|
||||
.success-icon {{
|
||||
text-align: center;
|
||||
font-size: 64px;
|
||||
margin: 20px 0;
|
||||
}}
|
||||
.success-box {{
|
||||
background-color: #e8f5e9;
|
||||
border-left: 4px solid #4caf50;
|
||||
padding: 16px;
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
.info-item {{
|
||||
margin: 16px 0;
|
||||
padding: 12px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
}}
|
||||
.info-item strong {{
|
||||
color: #333;
|
||||
display: inline-block;
|
||||
min-width: 100px;
|
||||
}}
|
||||
.highlight {{
|
||||
color: #4caf50;
|
||||
font-weight: 600;
|
||||
}}
|
||||
.footer {{
|
||||
background-color: #fafafa;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
border-top: 1px solid #eeeeee;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>✅ 打卡成功</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="success-icon">🎉</div>
|
||||
<p>您好,</p>
|
||||
<div class="success-box">
|
||||
<p><strong>自动打卡已成功完成!</strong></p>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>打卡时间:</strong><span class="highlight">{send_time}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>打卡状态:</strong><span class="highlight">成功 ✓</span>
|
||||
</div>
|
||||
<p style="margin-top: 20px; color: #666;">
|
||||
您无需进行任何操作,系统已自动为您完成打卡。此邮件仅作通知。
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>此邮件由接龙自动打卡系统自动发送,请勿直接回复</p>
|
||||
<p>CheckIn App V2 © 2026</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
@staticmethod
|
||||
def get_email_config() -> Optional[dict]:
|
||||
"""
|
||||
从环境变量读取邮件配置
|
||||
|
||||
FAILURE_HTML_TEMPLATE = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>打卡失败通知</title>
|
||||
<style>
|
||||
body {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
line-height: 1.6;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 600px;
|
||||
margin: 40px auto;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}}
|
||||
.header {{
|
||||
background: linear-gradient(135deg, #f44336 0%, #e91e63 100%);
|
||||
color: white;
|
||||
padding: 30px 20px;
|
||||
text-align: center;
|
||||
}}
|
||||
.header h1 {{
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}}
|
||||
.content {{
|
||||
padding: 30px 40px;
|
||||
}}
|
||||
.error-icon {{
|
||||
text-align: center;
|
||||
font-size: 64px;
|
||||
margin: 20px 0;
|
||||
}}
|
||||
.error-box {{
|
||||
background-color: #ffebee;
|
||||
border-left: 4px solid #f44336;
|
||||
padding: 16px;
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
.info-item {{
|
||||
margin: 16px 0;
|
||||
padding: 12px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
}}
|
||||
.info-item strong {{
|
||||
color: #333;
|
||||
display: inline-block;
|
||||
min-width: 100px;
|
||||
}}
|
||||
.highlight {{
|
||||
color: #f44336;
|
||||
font-weight: 600;
|
||||
}}
|
||||
.action-box {{
|
||||
background-color: #fff3e0;
|
||||
padding: 16px;
|
||||
margin: 20px 0;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ffb74d;
|
||||
}}
|
||||
.action-box h3 {{
|
||||
margin: 0 0 12px 0;
|
||||
color: #ff6f00;
|
||||
font-size: 16px;
|
||||
}}
|
||||
.action-box ul {{
|
||||
margin: 8px 0;
|
||||
padding-left: 20px;
|
||||
}}
|
||||
.action-box li {{
|
||||
margin: 6px 0;
|
||||
color: #666;
|
||||
}}
|
||||
.footer {{
|
||||
background-color: #fafafa;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
border-top: 1px solid #eeeeee;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>❌ 打卡失败通知</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="error-icon">⚠️</div>
|
||||
<p>您好,</p>
|
||||
<div class="error-box">
|
||||
<p><strong>自动打卡失败,需要您的关注!</strong></p>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>失败时间:</strong>{send_time}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>失败原因:</strong><span class="highlight">Token 已失效(需要登录)</span>
|
||||
</div>
|
||||
<div class="action-box">
|
||||
<h3>📋 需要您执行以下操作:</h3>
|
||||
<ul>
|
||||
<li>登录接龙自动打卡系统</li>
|
||||
<li>刷新您的 Authorization Token</li>
|
||||
<li>确认 Token 更新成功</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p style="margin-top: 20px; color: #666;">
|
||||
Token 失效是正常现象,通常在一段时间后会自动过期。刷新 Token 后,系统将恢复自动打卡功能。
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>此邮件由接龙自动打卡系统自动发送,请勿直接回复</p>
|
||||
<p>CheckIn App V2 © 2026</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
Returns:
|
||||
dict: 邮件配置,如果配置不完整则返回 None
|
||||
"""
|
||||
# 检查必要的邮件配置是否存在
|
||||
if not settings.SMTP_SERVER or not settings.SMTP_SENDER_EMAIL:
|
||||
logger.debug("邮件配置未完成(SMTP_SERVER 或 SMTP_SENDER_EMAIL 为空),邮件发送功能已禁用")
|
||||
return None
|
||||
|
||||
if not settings.SMTP_PORT:
|
||||
logger.debug("邮件配置未完成(SMTP_PORT 为空),邮件发送功能已禁用")
|
||||
return None
|
||||
|
||||
def get_email_settings():
|
||||
"""
|
||||
从环境变量读取邮件配置
|
||||
# 返回配置字典
|
||||
return {
|
||||
'smtp_server': settings.SMTP_SERVER,
|
||||
'smtp_port': settings.SMTP_PORT,
|
||||
'sender_email': settings.SMTP_SENDER_EMAIL,
|
||||
'sender_password': settings.SMTP_SENDER_PASSWORD,
|
||||
'use_ssl': settings.SMTP_USE_SSL
|
||||
}
|
||||
|
||||
如果 SMTP_SERVER、SMTP_PORT 或 SMTP_SENDER_EMAIL 有任一为空,则禁用邮件功能
|
||||
@staticmethod
|
||||
def send_email(
|
||||
to_emails: List[str],
|
||||
subject: str,
|
||||
html_content: str,
|
||||
from_email: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
发送邮件(底层方法)
|
||||
|
||||
Returns:
|
||||
dict: 邮件配置,如果配置不完整则返回 None
|
||||
"""
|
||||
# 检查必要的邮件配置是否存在
|
||||
if not settings.SMTP_SERVER or not settings.SMTP_SENDER_EMAIL:
|
||||
logger.debug("邮件配置未完成(SMTP_SERVER 或 SMTP_SENDER_EMAIL 为空),邮件发送功能已禁用")
|
||||
return None
|
||||
Args:
|
||||
to_emails: 收件人邮箱列表
|
||||
subject: 邮件主题
|
||||
html_content: HTML 邮件内容
|
||||
from_email: 发件人邮箱(可选,默认使用配置中的发件人)
|
||||
|
||||
if not settings.SMTP_PORT:
|
||||
logger.debug("邮件配置未完成(SMTP_PORT 为空),邮件发送功能已禁用")
|
||||
return None
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
email_config = EmailNotifier.get_email_config()
|
||||
if not email_config:
|
||||
logger.warning("邮件配置不完整,跳过发送邮件")
|
||||
return False
|
||||
|
||||
# 返回配置字典
|
||||
return {
|
||||
'smtpserver': settings.SMTP_SERVER,
|
||||
'smtpport': settings.SMTP_PORT,
|
||||
'senderemail': settings.SMTP_SENDER_EMAIL,
|
||||
'senderpassword': settings.SMTP_SENDER_PASSWORD,
|
||||
'use_ssl': settings.SMTP_USE_SSL
|
||||
}
|
||||
try:
|
||||
# 创建邮件
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['From'] = from_email or email_config['sender_email']
|
||||
msg['To'] = ', '.join(to_emails)
|
||||
msg['Subject'] = subject
|
||||
|
||||
# 添加 HTML 正文
|
||||
html_part = MIMEText(html_content, 'html', 'utf-8')
|
||||
msg.attach(html_part)
|
||||
|
||||
def _send_email(to_email: str, subject: str, html_content: str, email_settings: dict) -> bool:
|
||||
"""
|
||||
发送邮件
|
||||
|
||||
Args:
|
||||
to_email: 收件人邮箱
|
||||
subject: 邮件主题
|
||||
html_content: HTML 邮件内容
|
||||
email_settings: 邮件配置
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
try:
|
||||
msg = MIMEMultipart()
|
||||
msg["From"] = email_settings['senderemail']
|
||||
msg["To"] = to_email
|
||||
msg["Subject"] = subject
|
||||
msg.attach(MIMEText(html_content, 'html', 'utf-8'))
|
||||
|
||||
# 根据配置选择使用 SSL 或普通 SMTP
|
||||
if email_settings.get('use_ssl', True):
|
||||
with smtplib.SMTP_SSL(email_settings['smtpserver'], int(email_settings['smtpport'])) as server:
|
||||
server.login(email_settings['senderemail'], email_settings['senderpassword'])
|
||||
server.sendmail(msg["From"], msg["To"], msg.as_string())
|
||||
else:
|
||||
with smtplib.SMTP(email_settings['smtpserver'], int(email_settings['smtpport'])) as server:
|
||||
# 连接 SMTP 服务器并发送
|
||||
if email_config.get('use_ssl', True):
|
||||
server = smtplib.SMTP_SSL(
|
||||
email_config['smtp_server'],
|
||||
int(email_config['smtp_port'])
|
||||
)
|
||||
else:
|
||||
server = smtplib.SMTP(
|
||||
email_config['smtp_server'],
|
||||
int(email_config['smtp_port'])
|
||||
)
|
||||
server.starttls()
|
||||
server.login(email_settings['senderemail'], email_settings['senderpassword'])
|
||||
server.sendmail(msg["From"], msg["To"], msg.as_string())
|
||||
|
||||
logger.info(f"已成功向 {to_email} 发送邮件,主题: {subject}")
|
||||
return True
|
||||
server.login(email_config['sender_email'], email_config['sender_password'])
|
||||
server.sendmail(msg['From'], to_emails, msg.as_string())
|
||||
server.quit()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"向 {to_email} 发送邮件时失败: {e}")
|
||||
return False
|
||||
logger.info(f"邮件发送成功: {subject} -> {', '.join(to_emails)}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"邮件发送失败: {e}")
|
||||
return False
|
||||
|
||||
def send_expiration_notification(email: str, jwt_exp: str) -> bool:
|
||||
"""
|
||||
发送 Token 到期提醒邮件
|
||||
@staticmethod
|
||||
def is_email_enabled() -> bool:
|
||||
"""
|
||||
检查邮件功能是否启用
|
||||
|
||||
Args:
|
||||
email: 收件人邮箱
|
||||
jwt_exp: Token 过期时间戳
|
||||
Returns:
|
||||
邮件功能是否可用
|
||||
"""
|
||||
return EmailNotifier.get_email_config() is not None
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
email_settings = get_email_settings()
|
||||
if not email_settings:
|
||||
return False
|
||||
|
||||
try:
|
||||
exp_time = time.strftime("%Y年%m月%d日 %H:%M:%S", time.localtime(float(jwt_exp)))
|
||||
send_time = time.strftime("%Y年%m月%d日 %H:%M:%S", time.localtime())
|
||||
|
||||
html = EXPIRATION_HTML_TEMPLATE.format(
|
||||
name=email,
|
||||
exp_time=exp_time,
|
||||
send_time=send_time
|
||||
)
|
||||
|
||||
return _send_email(email, "接龙管家Token到期通知", html, email_settings)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送过期通知邮件失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def send_success_notification(email: str) -> bool:
|
||||
"""
|
||||
发送打卡成功通知邮件
|
||||
|
||||
Args:
|
||||
email: 收件人邮箱
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
email_settings = get_email_settings()
|
||||
if not email_settings:
|
||||
return False
|
||||
|
||||
try:
|
||||
send_time = time.strftime("%Y年%m月%d日 %H:%M:%S", time.localtime())
|
||||
|
||||
html = SUCCESS_HTML_TEMPLATE.format(
|
||||
name=email,
|
||||
send_time=send_time
|
||||
)
|
||||
|
||||
return _send_email(email, "自动打卡成功通知", html, email_settings)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送成功通知邮件失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def send_failure_notification(email: str) -> bool:
|
||||
"""
|
||||
发送打卡失败通知邮件
|
||||
|
||||
Args:
|
||||
email: 收件人邮箱
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
email_settings = get_email_settings()
|
||||
if not email_settings:
|
||||
return False
|
||||
|
||||
try:
|
||||
send_time = time.strftime("%Y年%m月%d日 %H:%M:%S", time.localtime())
|
||||
|
||||
html = FAILURE_HTML_TEMPLATE.format(
|
||||
name=email,
|
||||
send_time=send_time
|
||||
)
|
||||
|
||||
return _send_email(email, "打卡失败 - 需要刷新Token", html, email_settings)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送失败通知邮件失败: {e}")
|
||||
return False
|
||||
|
||||
@@ -86,6 +86,53 @@ def get_session_data(session_id: str) -> dict:
|
||||
return None
|
||||
|
||||
|
||||
def cancel_session(session_id: str) -> bool:
|
||||
"""
|
||||
取消登录会话
|
||||
|
||||
Args:
|
||||
session_id: 会话 ID
|
||||
|
||||
Returns:
|
||||
是否成功取消
|
||||
"""
|
||||
filepath = settings.SESSION_DIR / f"{session_id}.json"
|
||||
lock_path = settings.SESSION_DIR / f"{session_id}.json.lock"
|
||||
|
||||
if not filepath.exists():
|
||||
logger.warning(f"尝试取消不存在的会话: {session_id}")
|
||||
return False
|
||||
|
||||
try:
|
||||
with FileLock(lock_path, timeout=5):
|
||||
# 读取当前会话数据
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
if not content:
|
||||
return False
|
||||
data = json.loads(content)
|
||||
|
||||
# 如果已经成功,不允许取消
|
||||
if data.get('status') == 'success':
|
||||
logger.info(f"会话 {session_id} 已成功,无法取消")
|
||||
return False
|
||||
|
||||
# 标记为已取消
|
||||
data['status'] = 'cancelled'
|
||||
data['message'] = '用户取消登录'
|
||||
|
||||
# 写回文件
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"✅ 会话 {session_id} 已取消")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"取消会话 {session_id} 失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def get_token_headless(session_id: str, jwt_sub: str = None, alias: str = None, client_ip: str = "") -> None:
|
||||
"""
|
||||
使用 Selenium 获取 QQ 扫码登录的 Token
|
||||
@@ -193,7 +240,26 @@ def get_token_headless(session_id: str, jwt_sub: str = None, alias: str = None,
|
||||
current_step = "等待用户扫描登录 (Cookie 'token' 出现)"
|
||||
cookie_name_to_find = "token"
|
||||
logger.info(f"Selenium ({session_id}): {current_step}...")
|
||||
WebDriverWait(driver, 120, 1).until(lambda d: d.get_cookie(cookie_name_to_find) is not None) # 改为 120 秒(2分钟)
|
||||
|
||||
# 自定义等待逻辑:每秒检查cookie和session状态
|
||||
max_wait_seconds = 120
|
||||
import time
|
||||
for i in range(max_wait_seconds):
|
||||
# 检查session是否被取消
|
||||
status = get_session_status(session_id)
|
||||
if status == 'cancelled':
|
||||
logger.info(f"Selenium ({session_id}): 用户取消了登录,终止会话")
|
||||
raise Exception("用户取消登录")
|
||||
|
||||
# 检查cookie是否出现
|
||||
cookie = driver.get_cookie(cookie_name_to_find)
|
||||
if cookie:
|
||||
break
|
||||
|
||||
time.sleep(1)
|
||||
else:
|
||||
# 超时未获取到cookie
|
||||
raise TimeoutException("等待扫码超时")
|
||||
|
||||
cookie = driver.get_cookie(cookie_name_to_find)
|
||||
if cookie:
|
||||
|
||||
Reference in New Issue
Block a user