feat: 定时任务截图打包发送邮件
1. 添加批次任务跟踪机制,收集同一定时任务的所有账号截图 2. 等所有账号执行完成后,将截图打包成ZIP发送一封邮件 3. 邮件包含任务执行详情表格和统计信息 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
171
email_service.py
171
email_service.py
@@ -1943,6 +1943,177 @@ def send_task_complete_email_async(
|
||||
thread.start()
|
||||
|
||||
|
||||
def send_batch_task_complete_email(
|
||||
user_id: int,
|
||||
email: str,
|
||||
username: str,
|
||||
schedule_name: str,
|
||||
browse_type: str,
|
||||
screenshots: List[Dict[str, Any]]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
发送批次任务完成通知邮件(多账号截图打包)
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
email: 收件人邮箱
|
||||
username: 用户名
|
||||
schedule_name: 定时任务名称
|
||||
browse_type: 浏览类型
|
||||
screenshots: 截图列表 [{'account_name': x, 'path': y, 'items': n, 'attachments': m}, ...]
|
||||
|
||||
Returns:
|
||||
{'success': bool, 'error': str}
|
||||
"""
|
||||
# 检查邮件功能是否启用
|
||||
settings = get_email_settings()
|
||||
if not settings.get('enabled', False):
|
||||
return {'success': False, 'error': '邮件功能未启用'}
|
||||
|
||||
if not settings.get('task_notify_enabled', False):
|
||||
return {'success': False, 'error': '任务通知功能未启用'}
|
||||
|
||||
if not email:
|
||||
return {'success': False, 'error': '用户未设置邮箱'}
|
||||
|
||||
if not screenshots:
|
||||
return {'success': False, 'error': '没有截图需要发送'}
|
||||
|
||||
# 获取完成时间
|
||||
complete_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# 统计信息
|
||||
total_items_sum = sum(s.get('items', 0) for s in screenshots)
|
||||
total_attachments_sum = sum(s.get('attachments', 0) for s in screenshots)
|
||||
account_count = len(screenshots)
|
||||
|
||||
# 构建账号详情HTML
|
||||
accounts_html = ""
|
||||
for s in screenshots:
|
||||
accounts_html += f"""
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #ddd;">{s.get('account_name', '未知')}</td>
|
||||
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">{s.get('items', 0)}</td>
|
||||
<td style="padding: 8px; border: 1px solid #ddd; text-align: center;">{s.get('attachments', 0)}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
# 构建HTML邮件内容
|
||||
html_content = f"""
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<h2 style="color: #667eea;">定时任务完成通知</h2>
|
||||
<p>您好,{username}!</p>
|
||||
<p>您的定时任务 <strong>{schedule_name}</strong> 已完成执行。</p>
|
||||
|
||||
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin: 15px 0;">
|
||||
<p style="margin: 5px 0;"><strong>浏览类型:</strong>{browse_type}</p>
|
||||
<p style="margin: 5px 0;"><strong>执行账号:</strong>{account_count} 个</p>
|
||||
<p style="margin: 5px 0;"><strong>总浏览条目:</strong>{total_items_sum} 条</p>
|
||||
<p style="margin: 5px 0;"><strong>总附件数量:</strong>{total_attachments_sum} 个</p>
|
||||
<p style="margin: 5px 0;"><strong>完成时间:</strong>{complete_time}</p>
|
||||
</div>
|
||||
|
||||
<h3 style="color: #667eea; margin-top: 20px;">账号执行详情</h3>
|
||||
<table style="width: 100%; border-collapse: collapse; margin: 10px 0;">
|
||||
<tr style="background: #667eea; color: white;">
|
||||
<th style="padding: 10px; border: 1px solid #ddd;">账号名称</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd;">浏览条目</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd;">附件数量</th>
|
||||
</tr>
|
||||
{accounts_html}
|
||||
</table>
|
||||
|
||||
<p style="color: #666; font-size: 12px; margin-top: 20px;">
|
||||
截图已打包为ZIP附件,请查收。
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# 收集所有截图文件
|
||||
screenshot_files = []
|
||||
for s in screenshots:
|
||||
if s.get('path') and os.path.exists(s['path']):
|
||||
try:
|
||||
with open(s['path'], 'rb') as f:
|
||||
screenshot_files.append({
|
||||
'filename': f"{s.get('account_name', 'screenshot')}_{os.path.basename(s['path'])}",
|
||||
'data': f.read()
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"[邮件] 读取截图文件失败: {e}")
|
||||
|
||||
# 如果有截图,打包成ZIP
|
||||
zip_data = None
|
||||
zip_filename = None
|
||||
if screenshot_files:
|
||||
try:
|
||||
zip_buffer = BytesIO()
|
||||
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
for sf in screenshot_files:
|
||||
zf.writestr(sf['filename'], sf['data'])
|
||||
zip_data = zip_buffer.getvalue()
|
||||
zip_filename = f"screenshots_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
|
||||
except Exception as e:
|
||||
print(f"[邮件] 打包截图失败: {e}")
|
||||
|
||||
# 发送邮件
|
||||
attachments = []
|
||||
if zip_data and zip_filename:
|
||||
attachments.append({
|
||||
'filename': zip_filename,
|
||||
'data': zip_data,
|
||||
'mime_type': 'application/zip'
|
||||
})
|
||||
|
||||
result = send_email(
|
||||
to=email,
|
||||
subject=f'【自动化学习】定时任务完成 - {schedule_name}',
|
||||
html_content=html_content,
|
||||
attachments=attachments
|
||||
)
|
||||
|
||||
if result['success']:
|
||||
# 记录发送日志
|
||||
log_email_send(
|
||||
email_type='batch_task_complete',
|
||||
to_email=email,
|
||||
subject=f'定时任务完成 - {schedule_name}',
|
||||
success=True
|
||||
)
|
||||
return {'success': True}
|
||||
else:
|
||||
log_email_send(
|
||||
email_type='batch_task_complete',
|
||||
to_email=email,
|
||||
subject=f'定时任务完成 - {schedule_name}',
|
||||
success=False,
|
||||
error=result.get('error', '')
|
||||
)
|
||||
return {'success': False, 'error': result.get('error', '发送失败')}
|
||||
|
||||
|
||||
def send_batch_task_complete_email_async(
|
||||
user_id: int,
|
||||
email: str,
|
||||
username: str,
|
||||
schedule_name: str,
|
||||
browse_type: str,
|
||||
screenshots: List[Dict[str, Any]]
|
||||
):
|
||||
"""异步发送批次任务完成通知邮件"""
|
||||
import threading
|
||||
thread = threading.Thread(
|
||||
target=send_batch_task_complete_email,
|
||||
args=(user_id, email, username, schedule_name, browse_type, screenshots),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
|
||||
|
||||
# ============ 初始化 ============
|
||||
|
||||
def init_email_service():
|
||||
|
||||
Reference in New Issue
Block a user