fix: 修复邮箱绑定验证错误及多项改进

1. 修复email_verified字段缺失导致的500错误
2. 将邮件主题从"知识管理平台"改为"自动化学习"
3. 增大验证码字体(28->42)和图片尺寸(120x40->160x60)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-11 22:45:44 +08:00
parent bda780ed5c
commit 5c80fa0860
4 changed files with 163 additions and 21 deletions

45
app.py
View File

@@ -1045,34 +1045,34 @@ def generate_captcha():
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
import io import io
# 创建图片 # 创建图片 - 增大尺寸以便显示更大的字体
width, height = 120, 40 width, height = 160, 60
image = Image.new('RGB', (width, height), color=(255, 255, 255)) image = Image.new('RGB', (width, height), color=(255, 255, 255))
draw = ImageDraw.Draw(image) draw = ImageDraw.Draw(image)
# 添加干扰线 # 添加干扰线
for _ in range(5): for _ in range(6):
x1 = random.randint(0, width) x1 = random.randint(0, width)
y1 = random.randint(0, height) y1 = random.randint(0, height)
x2 = random.randint(0, width) x2 = random.randint(0, width)
y2 = random.randint(0, height) y2 = random.randint(0, height)
draw.line([(x1, y1), (x2, y2)], fill=(random.randint(0, 200), random.randint(0, 200), random.randint(0, 200))) draw.line([(x1, y1), (x2, y2)], fill=(random.randint(0, 200), random.randint(0, 200), random.randint(0, 200)), width=1)
# 添加干扰点 # 添加干扰点
for _ in range(50): for _ in range(80):
x = random.randint(0, width) x = random.randint(0, width)
y = random.randint(0, height) y = random.randint(0, height)
draw.point((x, y), fill=(random.randint(0, 200), random.randint(0, 200), random.randint(0, 200))) draw.point((x, y), fill=(random.randint(0, 200), random.randint(0, 200), random.randint(0, 200)))
# 绘制验证码文字 # 绘制验证码文字 - 增大字体
try: try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 28) font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 42)
except: except:
font = ImageFont.load_default() font = ImageFont.load_default()
for i, char in enumerate(code): for i, char in enumerate(code):
x = 10 + i * 25 + random.randint(-3, 3) x = 12 + i * 35 + random.randint(-3, 3)
y = random.randint(2, 8) y = random.randint(5, 12)
color = (random.randint(0, 150), random.randint(0, 150), random.randint(0, 150)) color = (random.randint(0, 150), random.randint(0, 150), random.randint(0, 150))
draw.text((x, y), char, font=font, fill=color) draw.text((x, y), char, font=font, fill=color)
@@ -2169,7 +2169,8 @@ def run_task(user_id, account_id, browse_type, enable_screenshot=True, source="m
# 发送任务完成邮件通知(不截图时在此发送) # 发送任务完成邮件通知(不截图时在此发送)
try: try:
user_info = database.get_user_by_id(user_id) user_info = database.get_user_by_id(user_id)
if user_info and user_info.get('email'): # 检查用户是否开启了邮件通知
if user_info and user_info.get('email') and database.get_user_email_notify(user_id):
account_name = account.remark if account.remark else account.username account_name = account.remark if account.remark else account.username
email_service.send_task_complete_email_async( email_service.send_task_complete_email_async(
user_id=user_id, user_id=user_id,
@@ -2538,7 +2539,8 @@ def take_screenshot_for_account(user_id, account_id, browse_type="应读", sourc
# 发送任务完成邮件通知 # 发送任务完成邮件通知
try: try:
user_info = database.get_user_by_id(user_id) user_info = database.get_user_by_id(user_id)
if user_info and user_info.get('email'): # 检查用户是否开启了邮件通知
if user_info and user_info.get('email') and database.get_user_email_notify(user_id):
screenshot_path = None screenshot_path = None
if result and result.get('success') and result.get('filename'): if result and result.get('success') and result.get('filename'):
screenshot_path = os.path.join(SCREENSHOTS_DIR, result['filename']) screenshot_path = os.path.join(SCREENSHOTS_DIR, result['filename'])
@@ -2967,6 +2969,27 @@ def unbind_user_email():
return jsonify({"error": "解绑失败"}), 500 return jsonify({"error": "解绑失败"}), 500
@app.route('/api/user/email-notify', methods=['GET'])
@login_required
def get_user_email_notify():
"""获取用户邮件通知偏好"""
enabled = database.get_user_email_notify(current_user.id)
return jsonify({"enabled": enabled})
@app.route('/api/user/email-notify', methods=['POST'])
@login_required
def update_user_email_notify():
"""更新用户邮件通知偏好"""
data = request.get_json()
enabled = data.get('enabled', True)
if database.update_user_email_notify(current_user.id, enabled):
return jsonify({"success": True})
else:
return jsonify({"error": "更新失败"}), 500
@app.route('/api/run_stats', methods=['GET']) @app.route('/api/run_stats', methods=['GET'])
@login_required @login_required
def get_run_stats(): def get_run_stats():

View File

@@ -834,6 +834,13 @@ def update_user_email(user_id, email, verified=False):
"""更新用户邮箱""" """更新用户邮箱"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:
cursor = conn.cursor() cursor = conn.cursor()
# 先检查email_verified字段是否存在不存在则添加
try:
cursor.execute('SELECT email_verified FROM users LIMIT 1')
except:
cursor.execute('ALTER TABLE users ADD COLUMN email_verified INTEGER DEFAULT 0')
conn.commit()
cursor.execute(''' cursor.execute('''
UPDATE users UPDATE users
SET email = ?, email_verified = ? SET email = ?, email_verified = ?
@@ -843,6 +850,41 @@ def update_user_email(user_id, email, verified=False):
return cursor.rowcount > 0 return cursor.rowcount > 0
def update_user_email_notify(user_id, enabled):
"""更新用户邮件通知偏好"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
# 先检查字段是否存在
try:
cursor.execute('SELECT email_notify_enabled FROM users LIMIT 1')
except:
cursor.execute('ALTER TABLE users ADD COLUMN email_notify_enabled INTEGER DEFAULT 1')
conn.commit()
cursor.execute('''
UPDATE users
SET email_notify_enabled = ?
WHERE id = ?
''', (int(enabled), user_id))
conn.commit()
return cursor.rowcount > 0
def get_user_email_notify(user_id):
"""获取用户邮件通知偏好(默认开启)"""
with db_pool.get_db() as conn:
cursor = conn.cursor()
# 先检查字段是否存在
try:
cursor.execute('SELECT email_notify_enabled FROM users WHERE id = ?', (user_id,))
row = cursor.fetchone()
if row is None:
return True
return bool(row[0]) if row[0] is not None else True
except:
return True # 字段不存在时默认开启
def get_all_users(): def get_all_users():
"""获取所有用户""" """获取所有用户"""
with db_pool.get_db() as conn: with db_pool.get_db() as conn:

View File

@@ -100,7 +100,7 @@ def init_email_tables():
password TEXT DEFAULT '', password TEXT DEFAULT '',
use_ssl INTEGER DEFAULT 1, use_ssl INTEGER DEFAULT 1,
use_tls INTEGER DEFAULT 0, use_tls INTEGER DEFAULT 0,
sender_name TEXT DEFAULT '知识管理平台', sender_name TEXT DEFAULT '自动化学习',
sender_email TEXT DEFAULT '', sender_email TEXT DEFAULT '',
daily_limit INTEGER DEFAULT 0, daily_limit INTEGER DEFAULT 0,
daily_sent INTEGER DEFAULT 0, daily_sent INTEGER DEFAULT 0,
@@ -411,7 +411,7 @@ def create_smtp_config(data: Dict[str, Any]) -> int:
password, password,
int(data.get('use_ssl', True)), int(data.get('use_ssl', True)),
int(data.get('use_tls', False)), int(data.get('use_tls', False)),
data.get('sender_name', '知识管理平台'), data.get('sender_name', '自动化学习'),
data.get('sender_email', ''), data.get('sender_email', ''),
data.get('daily_limit', 0) data.get('daily_limit', 0)
)) ))
@@ -829,7 +829,7 @@ def test_smtp_config(config_id: int, test_email: str) -> Dict[str, Any]:
sender.connect() sender.connect()
sender.send( sender.send(
test_email, test_email,
'知识管理平台 - SMTP配置测试', '自动化学习 - SMTP配置测试',
f'这是一封测试邮件。\n\n配置名称: {config["name"]}\nSMTP服务器: {config["host"]}:{config["port"]}\n\n如果您收到此邮件说明SMTP配置正确。', f'这是一封测试邮件。\n\n配置名称: {config["name"]}\nSMTP服务器: {config["host"]}:{config["port"]}\n\n如果您收到此邮件说明SMTP配置正确。',
None, None,
None None
@@ -1211,7 +1211,7 @@ def send_register_verification_email(
text_body = f""" text_body = f"""
您好,{username} 您好,{username}
感谢您注册知识管理平台。请点击下面的链接验证您的邮箱地址: 感谢您注册自动化学习。请点击下面的链接验证您的邮箱地址:
{verify_url} {verify_url}
@@ -1223,7 +1223,7 @@ def send_register_verification_email(
# 发送邮件 # 发送邮件
result = send_email( result = send_email(
to_email=email, to_email=email,
subject='知识管理平台】邮箱验证', subject='自动化学习】邮箱验证',
body=text_body, body=text_body,
html_body=html_body, html_body=html_body,
email_type=EMAIL_TYPE_REGISTER, email_type=EMAIL_TYPE_REGISTER,
@@ -1355,7 +1355,7 @@ def send_password_reset_email(
# 发送邮件 # 发送邮件
result = send_email( result = send_email(
to_email=email, to_email=email,
subject='知识管理平台】密码重置', subject='自动化学习】密码重置',
body=text_body, body=text_body,
html_body=html_body, html_body=html_body,
email_type=EMAIL_TYPE_RESET, email_type=EMAIL_TYPE_RESET,
@@ -1515,7 +1515,7 @@ def send_bind_email_verification(
# 发送邮件 # 发送邮件
result = send_email( result = send_email(
to_email=email, to_email=email,
subject='知识管理平台】邮箱绑定验证', subject='自动化学习】邮箱绑定验证',
body=text_body, body=text_body,
html_body=html_body, html_body=html_body,
email_type=EMAIL_TYPE_BIND, email_type=EMAIL_TYPE_BIND,
@@ -1832,7 +1832,7 @@ def send_task_complete_email(
result = send_email( result = send_email(
to_email=email, to_email=email,
subject=f'知识管理平台】任务完成 - {account_name}', subject=f'自动化学习】任务完成 - {account_name}',
body=text_body, body=text_body,
html_body=html_body, html_body=html_body,
email_type=EMAIL_TYPE_TASK_COMPLETE, email_type=EMAIL_TYPE_TASK_COMPLETE,
@@ -1851,7 +1851,7 @@ def send_task_complete_email(
attachment = [{'filename': screenshot_filename, 'data': screenshot_data}] attachment = [{'filename': screenshot_filename, 'data': screenshot_data}]
result2 = send_email( result2 = send_email(
to_email=email, to_email=email,
subject=f'知识管理平台】任务截图 - {account_name}', subject=f'自动化学习】任务截图 - {account_name}',
body=f'这是 {account_name} 的任务截图。', body=f'这是 {account_name} 的任务截图。',
attachments=attachment, attachments=attachment,
email_type=EMAIL_TYPE_TASK_COMPLETE, email_type=EMAIL_TYPE_TASK_COMPLETE,
@@ -1898,7 +1898,7 @@ def send_task_complete_email(
result = send_email( result = send_email(
to_email=email, to_email=email,
subject=f'知识管理平台】任务完成 - {account_name}', subject=f'自动化学习】任务完成 - {account_name}',
body=text_body, body=text_body,
html_body=html_body, html_body=html_body,
attachments=attachments, attachments=attachments,

View File

@@ -768,6 +768,20 @@
<button class="btn btn-text" onclick="unbindEmail()" style="color: #e74c3c;">解绑</button> <button class="btn btn-text" onclick="unbindEmail()" style="color: #e74c3c;">解绑</button>
</div> </div>
</div> </div>
<!-- 邮件通知开关 -->
<div id="emailNotifySection" style="margin-top: 15px; padding-top: 15px; border-top: 1px dashed #ddd; display: none;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<span style="color: #333;">任务完成通知</span>
<div style="font-size: 12px; color: #666; margin-top: 3px;">定时任务完成后发送邮件</div>
</div>
<label style="position: relative; display: inline-block; width: 50px; height: 26px;">
<input type="checkbox" id="emailNotifySwitch" onchange="toggleEmailNotify()" style="opacity: 0; width: 0; height: 0;">
<span style="position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .3s; border-radius: 26px;"></span>
<span id="emailNotifySlider" style="position: absolute; content: ''; height: 20px; width: 20px; left: 3px; bottom: 3px; background-color: white; transition: .3s; border-radius: 50%;"></span>
</label>
</div>
</div>
</div> </div>
<!-- 修改密码 --> <!-- 修改密码 -->
@@ -2081,6 +2095,7 @@
.then(data => { .then(data => {
const bindSection = document.getElementById('emailBindSection'); const bindSection = document.getElementById('emailBindSection');
const boundSection = document.getElementById('emailBoundSection'); const boundSection = document.getElementById('emailBoundSection');
const notifySection = document.getElementById('emailNotifySection');
const statusSpan = document.getElementById('emailStatus'); const statusSpan = document.getElementById('emailStatus');
const boundEmail = document.getElementById('boundEmail'); const boundEmail = document.getElementById('boundEmail');
const bindInput = document.getElementById('bindEmail'); const bindInput = document.getElementById('bindEmail');
@@ -2088,16 +2103,21 @@
if (data.email && data.email_verified) { if (data.email && data.email_verified) {
bindSection.style.display = 'none'; bindSection.style.display = 'none';
boundSection.style.display = 'block'; boundSection.style.display = 'block';
notifySection.style.display = 'block';
boundEmail.textContent = data.email; boundEmail.textContent = data.email;
statusSpan.innerHTML = '<span style="color: #27ae60;">已验证</span>'; statusSpan.innerHTML = '<span style="color: #27ae60;">已验证</span>';
// 加载邮件通知偏好
loadEmailNotify();
} else if (data.email) { } else if (data.email) {
bindSection.style.display = 'block'; bindSection.style.display = 'block';
boundSection.style.display = 'none'; boundSection.style.display = 'none';
notifySection.style.display = 'none';
bindInput.value = data.email; bindInput.value = data.email;
statusSpan.innerHTML = '<span style="color: #f39c12;">待验证</span>'; statusSpan.innerHTML = '<span style="color: #f39c12;">待验证</span>';
} else { } else {
bindSection.style.display = 'block'; bindSection.style.display = 'block';
boundSection.style.display = 'none'; boundSection.style.display = 'none';
notifySection.style.display = 'none';
bindInput.value = ''; bindInput.value = '';
statusSpan.innerHTML = '<span style="color: #999;">未绑定</span>'; statusSpan.innerHTML = '<span style="color: #999;">未绑定</span>';
} }
@@ -2105,6 +2125,63 @@
.catch(() => {}); .catch(() => {});
} }
function loadEmailNotify() {
fetch('/api/user/email-notify')
.then(r => r.json())
.then(data => {
const checkbox = document.getElementById('emailNotifySwitch');
const slider = document.getElementById('emailNotifySlider');
const bg = checkbox.nextElementSibling;
checkbox.checked = data.enabled;
if (data.enabled) {
bg.style.backgroundColor = '#27ae60';
slider.style.transform = 'translateX(24px)';
} else {
bg.style.backgroundColor = '#ccc';
slider.style.transform = 'translateX(0)';
}
})
.catch(() => {});
}
function toggleEmailNotify() {
const checkbox = document.getElementById('emailNotifySwitch');
const slider = document.getElementById('emailNotifySlider');
const bg = checkbox.nextElementSibling;
const enabled = checkbox.checked;
// 立即更新UI
if (enabled) {
bg.style.backgroundColor = '#27ae60';
slider.style.transform = 'translateX(24px)';
} else {
bg.style.backgroundColor = '#ccc';
slider.style.transform = 'translateX(0)';
}
fetch('/api/user/email-notify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: enabled })
})
.then(r => r.json())
.then(data => {
if (data.success) {
showToast(enabled ? '已开启邮件通知' : '已关闭邮件通知', 'success');
} else {
// 恢复状态
checkbox.checked = !enabled;
loadEmailNotify();
showToast('设置失败', 'error');
}
})
.catch(() => {
checkbox.checked = !enabled;
loadEmailNotify();
showToast('网络错误', 'error');
});
}
function bindEmail() { function bindEmail() {
const email = document.getElementById('bindEmail').value.trim(); const email = document.getElementById('bindEmail').value.trim();
if (!email) { if (!email) {