feat: 添加邮件功能第五阶段 - 用户邮箱绑定

1. 添加邮箱绑定验证邮件模板 (templates/email/bind_email.html)
2. 在email_service.py中添加:
   - send_bind_email_verification() 发送绑定验证邮件
   - verify_bind_email_token() 验证绑定Token
3. 在database.py中添加:
   - update_user_email() 更新用户邮箱
4. 在app.py中添加API:
   - GET /api/user/email - 获取用户邮箱信息
   - POST /api/user/bind-email - 发送绑定验证邮件
   - GET /api/verify-bind-email/<token> - 验证绑定Token
   - POST /api/user/unbind-email - 解绑邮箱
5. 更新templates/index.html:
   - 将"修改密码"弹窗改为"个人设置"
   - 添加邮箱绑定/解绑功能UI
   - 显示邮箱状态(未绑定/待验证/已验证)

🤖 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:20:29 +08:00
parent 0ccddd8c63
commit 29d4bdfbcb
5 changed files with 463 additions and 14 deletions

View File

@@ -1404,6 +1404,122 @@ def confirm_password_reset(token: str) -> Optional[Dict[str, Any]]:
return {'user_id': result['user_id'], 'email': result['email']}
# ============ 邮箱绑定验证 ============
def send_bind_email_verification(
user_id: int,
email: str,
username: str,
base_url: str = None
) -> Dict[str, Any]:
"""
发送邮箱绑定验证邮件
Args:
user_id: 用户ID
email: 要绑定的邮箱
username: 用户名
base_url: 网站基础URL
Returns:
{'success': bool, 'error': str, 'token': str}
"""
# 检查发送频率限制绑定邮件限制1分钟
if not check_rate_limit(email, EMAIL_TYPE_BIND):
return {
'success': False,
'error': '发送太频繁请1分钟后再试',
'token': None
}
# 使旧的绑定token失效
with db_pool.get_db() as conn:
cursor = conn.cursor()
cursor.execute("""
UPDATE email_tokens SET used = 1
WHERE user_id = ? AND token_type = ? AND used = 0
""", (user_id, EMAIL_TYPE_BIND))
conn.commit()
# 生成新的验证Token
token = generate_email_token(email, EMAIL_TYPE_BIND, user_id)
# 获取base_url
if not base_url:
settings = get_email_settings()
base_url = settings.get('base_url', '')
if not base_url:
try:
from app_config import Config
base_url = Config.BASE_URL
except:
base_url = 'http://localhost:51233'
# 生成验证链接
verify_url = f"{base_url.rstrip('/')}/api/verify-bind-email/{token}"
# 读取邮件模板
template_path = os.path.join(os.path.dirname(__file__), 'templates', 'email', 'bind_email.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><a href="{{ verify_url }}">{{ verify_url }}</a></p>
<p>此链接1小时内有效。</p>
</body>
</html>
"""
# 替换模板变量
html_body = html_template.replace('{{ username }}', username)
html_body = html_body.replace('{{ verify_url }}', verify_url)
# 纯文本版本
text_body = f"""
您好,{username}
您正在绑定此邮箱到您的账号。请点击下面的链接完成验证:
{verify_url}
此链接1小时内有效。
如果这不是您的操作,请忽略此邮件。
"""
# 发送邮件
result = send_email(
to_email=email,
subject='【知识管理平台】邮箱绑定验证',
body=text_body,
html_body=html_body,
email_type=EMAIL_TYPE_BIND,
user_id=user_id
)
if result['success']:
return {'success': True, 'error': '', 'token': token}
else:
return {'success': False, 'error': result['error'], 'token': None}
def verify_bind_email_token(token: str) -> Optional[Dict[str, Any]]:
"""
验证邮箱绑定Token
Returns:
成功返回 {'user_id': int, 'email': str},失败返回 None
"""
return verify_email_token(token, EMAIL_TYPE_BIND)
# ============ 异步发送队列 ============
class EmailQueue: