feat: 添加邮件功能第四阶段 - 任务完成通知
1. 添加任务完成邮件模板 (templates/email/task_complete.html) 2. 在email_service.py中添加: - task_notify_enabled 字段支持 - send_task_complete_email() 函数,支持附件大小限制和分批发送 - send_task_complete_email_async() 异步发送函数 - MAX_ATTACHMENT_SIZE 常量 (10MB) 3. 更新app.py: - 邮件设置API支持task_notify_enabled字段 - 截图回调中集成任务完成邮件发送 4. 更新admin.html: - 添加"启用任务完成通知"开关 - 更新loadEmailSettings/updateEmailSettings函数 附件超过10MB时会自动分两封邮件发送(通知+截图),作为容错机制 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
27
app.py
27
app.py
@@ -2517,6 +2517,29 @@ def take_screenshot_for_account(user_id, account_id, browse_type="应读", sourc
|
|||||||
source=source
|
source=source
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 发送任务完成邮件通知
|
||||||
|
try:
|
||||||
|
user_info = database.get_user_by_id(user_id)
|
||||||
|
if user_info and user_info.get('email'):
|
||||||
|
screenshot_path = None
|
||||||
|
if result and result.get('success') and result.get('filename'):
|
||||||
|
screenshot_path = os.path.join(SCREENSHOTS_DIR, result['filename'])
|
||||||
|
|
||||||
|
account_name = account.remark if account.remark else account.username
|
||||||
|
email_service.send_task_complete_email_async(
|
||||||
|
user_id=user_id,
|
||||||
|
email=user_info['email'],
|
||||||
|
username=user_info['username'],
|
||||||
|
account_name=account_name,
|
||||||
|
browse_type=browse_type,
|
||||||
|
total_items=browse_result.get('total_items', 0),
|
||||||
|
total_attachments=browse_result.get('total_attachments', 0),
|
||||||
|
screenshot_path=screenshot_path,
|
||||||
|
log_callback=lambda msg: log_to_client(msg, user_id, account_id)
|
||||||
|
)
|
||||||
|
except Exception as email_error:
|
||||||
|
logger.warning(f"发送任务完成邮件失败: {email_error}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"截图回调出错: {e}")
|
logger.error(f"截图回调出错: {e}")
|
||||||
|
|
||||||
@@ -3716,12 +3739,14 @@ def update_email_settings_api():
|
|||||||
failover_enabled = data.get('failover_enabled', True)
|
failover_enabled = data.get('failover_enabled', True)
|
||||||
register_verify_enabled = data.get('register_verify_enabled')
|
register_verify_enabled = data.get('register_verify_enabled')
|
||||||
base_url = data.get('base_url')
|
base_url = data.get('base_url')
|
||||||
|
task_notify_enabled = data.get('task_notify_enabled')
|
||||||
|
|
||||||
email_service.update_email_settings(
|
email_service.update_email_settings(
|
||||||
enabled=enabled,
|
enabled=enabled,
|
||||||
failover_enabled=failover_enabled,
|
failover_enabled=failover_enabled,
|
||||||
register_verify_enabled=register_verify_enabled,
|
register_verify_enabled=register_verify_enabled,
|
||||||
base_url=base_url
|
base_url=base_url,
|
||||||
|
task_notify_enabled=task_notify_enabled
|
||||||
)
|
)
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
248
email_service.py
248
email_service.py
@@ -55,6 +55,11 @@ RATE_LIMIT_BIND = 60 # 绑定邮件: 1分钟
|
|||||||
QUEUE_WORKERS = int(os.environ.get('EMAIL_QUEUE_WORKERS', '2'))
|
QUEUE_WORKERS = int(os.environ.get('EMAIL_QUEUE_WORKERS', '2'))
|
||||||
QUEUE_MAX_SIZE = int(os.environ.get('EMAIL_QUEUE_MAX_SIZE', '100'))
|
QUEUE_MAX_SIZE = int(os.environ.get('EMAIL_QUEUE_MAX_SIZE', '100'))
|
||||||
|
|
||||||
|
# 附件大小限制(字节)
|
||||||
|
# 大多数邮件服务商限制单封邮件附件大小为10-25MB
|
||||||
|
# 为安全起见,设置为10MB,超过则分批发送
|
||||||
|
MAX_ATTACHMENT_SIZE = int(os.environ.get('EMAIL_MAX_ATTACHMENT_SIZE', str(10 * 1024 * 1024))) # 10MB
|
||||||
|
|
||||||
|
|
||||||
# ============ 数据库操作 ============
|
# ============ 数据库操作 ============
|
||||||
|
|
||||||
@@ -108,6 +113,7 @@ def init_email_tables():
|
|||||||
enabled INTEGER DEFAULT 0,
|
enabled INTEGER DEFAULT 0,
|
||||||
failover_enabled INTEGER DEFAULT 1,
|
failover_enabled INTEGER DEFAULT 1,
|
||||||
register_verify_enabled INTEGER DEFAULT 0,
|
register_verify_enabled INTEGER DEFAULT 0,
|
||||||
|
task_notify_enabled INTEGER DEFAULT 0,
|
||||||
base_url TEXT DEFAULT '',
|
base_url TEXT DEFAULT '',
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
@@ -201,9 +207,15 @@ def get_email_settings() -> Dict[str, Any]:
|
|||||||
except:
|
except:
|
||||||
cursor.execute("ALTER TABLE email_settings ADD COLUMN base_url TEXT DEFAULT ''")
|
cursor.execute("ALTER TABLE email_settings ADD COLUMN base_url TEXT DEFAULT ''")
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT task_notify_enabled FROM email_settings LIMIT 1")
|
||||||
|
except:
|
||||||
|
cursor.execute("ALTER TABLE email_settings ADD COLUMN task_notify_enabled INTEGER DEFAULT 0")
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT enabled, failover_enabled, register_verify_enabled, base_url, updated_at
|
SELECT enabled, failover_enabled, register_verify_enabled, base_url,
|
||||||
|
task_notify_enabled, updated_at
|
||||||
FROM email_settings WHERE id = 1
|
FROM email_settings WHERE id = 1
|
||||||
""")
|
""")
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
@@ -213,13 +225,15 @@ def get_email_settings() -> Dict[str, Any]:
|
|||||||
'failover_enabled': bool(row[1]),
|
'failover_enabled': bool(row[1]),
|
||||||
'register_verify_enabled': bool(row[2]) if row[2] is not None else False,
|
'register_verify_enabled': bool(row[2]) if row[2] is not None else False,
|
||||||
'base_url': row[3] or '',
|
'base_url': row[3] or '',
|
||||||
'updated_at': row[4]
|
'task_notify_enabled': bool(row[4]) if row[4] is not None else False,
|
||||||
|
'updated_at': row[5]
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
'enabled': False,
|
'enabled': False,
|
||||||
'failover_enabled': True,
|
'failover_enabled': True,
|
||||||
'register_verify_enabled': False,
|
'register_verify_enabled': False,
|
||||||
'base_url': '',
|
'base_url': '',
|
||||||
|
'task_notify_enabled': False,
|
||||||
'updated_at': None
|
'updated_at': None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +242,8 @@ def update_email_settings(
|
|||||||
enabled: bool,
|
enabled: bool,
|
||||||
failover_enabled: bool,
|
failover_enabled: bool,
|
||||||
register_verify_enabled: bool = None,
|
register_verify_enabled: bool = None,
|
||||||
base_url: str = None
|
base_url: str = None,
|
||||||
|
task_notify_enabled: bool = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""更新全局邮件设置"""
|
"""更新全局邮件设置"""
|
||||||
with db_pool.get_db() as conn:
|
with db_pool.get_db() as conn:
|
||||||
@@ -246,6 +261,10 @@ def update_email_settings(
|
|||||||
updates.append('base_url = ?')
|
updates.append('base_url = ?')
|
||||||
params.append(base_url)
|
params.append(base_url)
|
||||||
|
|
||||||
|
if task_notify_enabled is not None:
|
||||||
|
updates.append('task_notify_enabled = ?')
|
||||||
|
params.append(int(task_notify_enabled))
|
||||||
|
|
||||||
cursor.execute(f"""
|
cursor.execute(f"""
|
||||||
UPDATE email_settings
|
UPDATE email_settings
|
||||||
SET {', '.join(updates)}
|
SET {', '.join(updates)}
|
||||||
@@ -1567,6 +1586,229 @@ def create_zip_attachment(files: List[Dict[str, Any]], zip_filename: str = 'scre
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============ 任务完成通知邮件 ============
|
||||||
|
|
||||||
|
def send_task_complete_email(
|
||||||
|
user_id: int,
|
||||||
|
email: str,
|
||||||
|
username: str,
|
||||||
|
account_name: str,
|
||||||
|
browse_type: str,
|
||||||
|
total_items: int,
|
||||||
|
total_attachments: int,
|
||||||
|
screenshot_path: str = None,
|
||||||
|
log_callback: Callable = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
发送任务完成通知邮件(支持附件大小限制,超过则分批发送)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: 用户ID
|
||||||
|
email: 收件人邮箱
|
||||||
|
username: 用户名
|
||||||
|
account_name: 账号名称
|
||||||
|
browse_type: 浏览类型
|
||||||
|
total_items: 浏览条目数
|
||||||
|
total_attachments: 附件数量
|
||||||
|
screenshot_path: 截图文件路径
|
||||||
|
log_callback: 日志回调函数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{'success': bool, 'error': str, 'emails_sent': int}
|
||||||
|
"""
|
||||||
|
# 检查邮件功能是否启用
|
||||||
|
settings = get_email_settings()
|
||||||
|
if not settings.get('enabled', False):
|
||||||
|
return {'success': False, 'error': '邮件功能未启用', 'emails_sent': 0}
|
||||||
|
|
||||||
|
if not settings.get('task_notify_enabled', False):
|
||||||
|
return {'success': False, 'error': '任务通知功能未启用', 'emails_sent': 0}
|
||||||
|
|
||||||
|
if not email:
|
||||||
|
return {'success': False, 'error': '用户未设置邮箱', 'emails_sent': 0}
|
||||||
|
|
||||||
|
# 获取完成时间
|
||||||
|
complete_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# 读取截图文件
|
||||||
|
screenshot_data = None
|
||||||
|
screenshot_filename = None
|
||||||
|
if screenshot_path and os.path.exists(screenshot_path):
|
||||||
|
try:
|
||||||
|
with open(screenshot_path, 'rb') as f:
|
||||||
|
screenshot_data = f.read()
|
||||||
|
screenshot_filename = os.path.basename(screenshot_path)
|
||||||
|
except Exception as e:
|
||||||
|
if log_callback:
|
||||||
|
log_callback(f"[邮件] 读取截图文件失败: {e}")
|
||||||
|
|
||||||
|
# 读取邮件模板
|
||||||
|
template_path = os.path.join(os.path.dirname(__file__), 'templates', 'email', 'task_complete.html')
|
||||||
|
try:
|
||||||
|
with open(template_path, 'r', encoding='utf-8') as f:
|
||||||
|
html_template = f.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
html_template = """
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>任务完成通知</h1>
|
||||||
|
<p>您好,{{ username }}!</p>
|
||||||
|
<p>您的浏览任务已完成。</p>
|
||||||
|
<p>账号:{{ account_name }}</p>
|
||||||
|
<p>浏览类型:{{ browse_type }}</p>
|
||||||
|
<p>浏览条目:{{ total_items }} 条</p>
|
||||||
|
<p>附件数量:{{ total_attachments }} 个</p>
|
||||||
|
<p>完成时间:{{ complete_time }}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 准备发送
|
||||||
|
emails_sent = 0
|
||||||
|
last_error = ''
|
||||||
|
|
||||||
|
# 检查附件大小,决定是否需要分批发送
|
||||||
|
if screenshot_data and len(screenshot_data) > MAX_ATTACHMENT_SIZE:
|
||||||
|
# 附件超过限制,不压缩直接发送(大图片压缩效果不大)
|
||||||
|
# 这种情况很少见,但作为容错处理
|
||||||
|
batch_info = '(由于文件较大,截图将单独发送)'
|
||||||
|
|
||||||
|
# 先发送不带附件的通知邮件
|
||||||
|
html_body = html_template.replace('{{ username }}', username)
|
||||||
|
html_body = html_body.replace('{{ account_name }}', account_name)
|
||||||
|
html_body = html_body.replace('{{ browse_type }}', browse_type)
|
||||||
|
html_body = html_body.replace('{{ total_items }}', str(total_items))
|
||||||
|
html_body = html_body.replace('{{ total_attachments }}', str(total_attachments))
|
||||||
|
html_body = html_body.replace('{{ complete_time }}', complete_time)
|
||||||
|
html_body = html_body.replace('{{ batch_info }}', batch_info)
|
||||||
|
|
||||||
|
text_body = f"""
|
||||||
|
您好,{username}!
|
||||||
|
|
||||||
|
您的浏览任务已完成。
|
||||||
|
|
||||||
|
账号:{account_name}
|
||||||
|
浏览类型:{browse_type}
|
||||||
|
浏览条目:{total_items} 条
|
||||||
|
附件数量:{total_attachments} 个
|
||||||
|
完成时间:{complete_time}
|
||||||
|
|
||||||
|
截图将在下一封邮件中发送。
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = send_email(
|
||||||
|
to_email=email,
|
||||||
|
subject=f'【知识管理平台】任务完成 - {account_name}',
|
||||||
|
body=text_body,
|
||||||
|
html_body=html_body,
|
||||||
|
email_type=EMAIL_TYPE_TASK_COMPLETE,
|
||||||
|
user_id=user_id,
|
||||||
|
log_callback=log_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
if result['success']:
|
||||||
|
emails_sent += 1
|
||||||
|
if log_callback:
|
||||||
|
log_callback(f"[邮件] 任务通知已发送")
|
||||||
|
else:
|
||||||
|
last_error = result['error']
|
||||||
|
|
||||||
|
# 单独发送截图附件
|
||||||
|
attachment = [{'filename': screenshot_filename, 'data': screenshot_data}]
|
||||||
|
result2 = send_email(
|
||||||
|
to_email=email,
|
||||||
|
subject=f'【知识管理平台】任务截图 - {account_name}',
|
||||||
|
body=f'这是 {account_name} 的任务截图。',
|
||||||
|
attachments=attachment,
|
||||||
|
email_type=EMAIL_TYPE_TASK_COMPLETE,
|
||||||
|
user_id=user_id,
|
||||||
|
log_callback=log_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
if result2['success']:
|
||||||
|
emails_sent += 1
|
||||||
|
if log_callback:
|
||||||
|
log_callback(f"[邮件] 截图附件已发送")
|
||||||
|
else:
|
||||||
|
last_error = result2['error']
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 正常情况:附件大小在限制内,一次性发送
|
||||||
|
batch_info = ''
|
||||||
|
attachments = None
|
||||||
|
|
||||||
|
if screenshot_data:
|
||||||
|
attachments = [{'filename': screenshot_filename, 'data': screenshot_data}]
|
||||||
|
|
||||||
|
html_body = html_template.replace('{{ username }}', username)
|
||||||
|
html_body = html_body.replace('{{ account_name }}', account_name)
|
||||||
|
html_body = html_body.replace('{{ browse_type }}', browse_type)
|
||||||
|
html_body = html_body.replace('{{ total_items }}', str(total_items))
|
||||||
|
html_body = html_body.replace('{{ total_attachments }}', str(total_attachments))
|
||||||
|
html_body = html_body.replace('{{ complete_time }}', complete_time)
|
||||||
|
html_body = html_body.replace('{{ batch_info }}', batch_info)
|
||||||
|
|
||||||
|
text_body = f"""
|
||||||
|
您好,{username}!
|
||||||
|
|
||||||
|
您的浏览任务已完成。
|
||||||
|
|
||||||
|
账号:{account_name}
|
||||||
|
浏览类型:{browse_type}
|
||||||
|
浏览条目:{total_items} 条
|
||||||
|
附件数量:{total_attachments} 个
|
||||||
|
完成时间:{complete_time}
|
||||||
|
|
||||||
|
{'截图已附在邮件中。' if screenshot_data else ''}
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = send_email(
|
||||||
|
to_email=email,
|
||||||
|
subject=f'【知识管理平台】任务完成 - {account_name}',
|
||||||
|
body=text_body,
|
||||||
|
html_body=html_body,
|
||||||
|
attachments=attachments,
|
||||||
|
email_type=EMAIL_TYPE_TASK_COMPLETE,
|
||||||
|
user_id=user_id,
|
||||||
|
log_callback=log_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
if result['success']:
|
||||||
|
emails_sent += 1
|
||||||
|
if log_callback:
|
||||||
|
log_callback(f"[邮件] 任务通知已发送")
|
||||||
|
else:
|
||||||
|
last_error = result['error']
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': emails_sent > 0,
|
||||||
|
'error': last_error if emails_sent == 0 else '',
|
||||||
|
'emails_sent': emails_sent
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def send_task_complete_email_async(
|
||||||
|
user_id: int,
|
||||||
|
email: str,
|
||||||
|
username: str,
|
||||||
|
account_name: str,
|
||||||
|
browse_type: str,
|
||||||
|
total_items: int,
|
||||||
|
total_attachments: int,
|
||||||
|
screenshot_path: str = None,
|
||||||
|
log_callback: Callable = None
|
||||||
|
):
|
||||||
|
"""异步发送任务完成通知邮件"""
|
||||||
|
import threading
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=send_task_complete_email,
|
||||||
|
args=(user_id, email, username, account_name, browse_type,
|
||||||
|
total_items, total_attachments, screenshot_path, log_callback),
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
|
||||||
# ============ 初始化 ============
|
# ============ 初始化 ============
|
||||||
|
|
||||||
def init_email_service():
|
def init_email_service():
|
||||||
|
|||||||
@@ -1233,6 +1233,15 @@
|
|||||||
开启后,新用户注册需通过邮箱验证才能激活账号(优先级高于自动审核)
|
开启后,新用户注册需通过邮箱验证才能激活账号(优先级高于自动审核)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 10px;">
|
||||||
|
<label style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<input type="checkbox" id="taskNotifyEnabled" onchange="updateEmailSettings()" style="width: auto; max-width: none;">
|
||||||
|
启用任务完成通知
|
||||||
|
</label>
|
||||||
|
<div style="font-size: 12px; color: #666; margin-top: 5px;">
|
||||||
|
开启后,定时任务完成时将发送邮件通知给用户(用户需已设置邮箱)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group" style="margin-bottom: 0;">
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
<label style="display: block; margin-bottom: 5px;">网站基础URL</label>
|
<label style="display: block; margin-bottom: 5px;">网站基础URL</label>
|
||||||
<input type="text" id="baseUrl" placeholder="例如: https://example.com" style="width: 100%;" onblur="updateEmailSettings()">
|
<input type="text" id="baseUrl" placeholder="例如: https://example.com" style="width: 100%;" onblur="updateEmailSettings()">
|
||||||
@@ -2797,6 +2806,7 @@
|
|||||||
document.getElementById('emailEnabled').checked = data.enabled;
|
document.getElementById('emailEnabled').checked = data.enabled;
|
||||||
document.getElementById('failoverEnabled').checked = data.failover_enabled;
|
document.getElementById('failoverEnabled').checked = data.failover_enabled;
|
||||||
document.getElementById('registerVerifyEnabled').checked = data.register_verify_enabled || false;
|
document.getElementById('registerVerifyEnabled').checked = data.register_verify_enabled || false;
|
||||||
|
document.getElementById('taskNotifyEnabled').checked = data.task_notify_enabled || false;
|
||||||
document.getElementById('baseUrl').value = data.base_url || '';
|
document.getElementById('baseUrl').value = data.base_url || '';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -2814,6 +2824,7 @@
|
|||||||
enabled: document.getElementById('emailEnabled').checked,
|
enabled: document.getElementById('emailEnabled').checked,
|
||||||
failover_enabled: document.getElementById('failoverEnabled').checked,
|
failover_enabled: document.getElementById('failoverEnabled').checked,
|
||||||
register_verify_enabled: document.getElementById('registerVerifyEnabled').checked,
|
register_verify_enabled: document.getElementById('registerVerifyEnabled').checked,
|
||||||
|
task_notify_enabled: document.getElementById('taskNotifyEnabled').checked,
|
||||||
base_url: document.getElementById('baseUrl').value.trim()
|
base_url: document.getElementById('baseUrl').value.trim()
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
89
templates/email/task_complete.html
Normal file
89
templates/email/task_complete.html
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; font-family: 'Microsoft YaHei', Arial, sans-serif; background-color: #f5f5f5;">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f5f5f5; padding: 30px 0;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
|
||||||
|
<!-- 头部 -->
|
||||||
|
<tr>
|
||||||
|
<td style="background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%); padding: 30px; border-radius: 10px 10px 0 0; text-align: center;">
|
||||||
|
<h1 style="color: #ffffff; margin: 0; font-size: 24px;">知识管理平台</h1>
|
||||||
|
<p style="color: rgba(255,255,255,0.9); margin: 10px 0 0 0; font-size: 14px;">任务完成通知</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- 内容 -->
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 30px;">
|
||||||
|
<p style="color: #333; font-size: 16px; margin: 0 0 20px 0;">
|
||||||
|
您好,<strong>{{ username }}</strong>!
|
||||||
|
</p>
|
||||||
|
<p style="color: #666; font-size: 14px; line-height: 1.8; margin: 0 0 25px 0;">
|
||||||
|
您的浏览任务已完成,以下是任务详情:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- 任务详情 -->
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="background: #f8f9fa; border-radius: 8px; margin-bottom: 25px;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 20px;">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; color: #666; font-size: 14px;">账号:</td>
|
||||||
|
<td style="padding: 8px 0; color: #333; font-size: 14px; font-weight: bold;">{{ account_name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; color: #666; font-size: 14px;">浏览类型:</td>
|
||||||
|
<td style="padding: 8px 0; color: #333; font-size: 14px;">{{ browse_type }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; color: #666; font-size: 14px;">浏览条目:</td>
|
||||||
|
<td style="padding: 8px 0; color: #27ae60; font-size: 14px; font-weight: bold;">{{ total_items }} 条</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; color: #666; font-size: 14px;">附件数量:</td>
|
||||||
|
<td style="padding: 8px 0; color: #333; font-size: 14px;">{{ total_attachments }} 个</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; color: #666; font-size: 14px;">完成时间:</td>
|
||||||
|
<td style="padding: 8px 0; color: #333; font-size: 14px;">{{ complete_time }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- 截图信息 -->
|
||||||
|
<div style="background: #e8f5e9; border-left: 4px solid #27ae60; padding: 15px; margin: 25px 0; border-radius: 0 5px 5px 0;">
|
||||||
|
<p style="color: #2e7d32; font-size: 13px; margin: 0;">
|
||||||
|
<strong>截图已附在邮件中</strong>{{ batch_info }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="color: #999; font-size: 13px; margin: 20px 0 0 0;">
|
||||||
|
如有任何问题,请登录平台查看详情。
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- 底部 -->
|
||||||
|
<tr>
|
||||||
|
<td style="background: #f8f9fa; padding: 20px 30px; border-radius: 0 0 10px 10px; text-align: center;">
|
||||||
|
<p style="color: #999; font-size: 12px; margin: 0;">
|
||||||
|
此邮件由系统自动发送,请勿直接回复。
|
||||||
|
</p>
|
||||||
|
<p style="color: #ccc; font-size: 11px; margin: 10px 0 0 0;">
|
||||||
|
© 知识管理平台
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user