一、邮箱相关功能概览
当前项目包含 3 种邮箱相关功能 :

二、功能一:注册邮箱验证

1. 后端实现
接口路由 :
- POST /api/register - 注册时发送验证邮件
- GET /api/verify-email?token=xxx - 邮件链接直接验证
- POST /api/verify-email - 前端手动输入令牌验证
- POST /api/resend-verification-email - 重新发送验证邮件
核心流程 :
PlainText
用户注册 → 创建用户(is_active=False) → 生成Token → 存入数据库 → 发送邮
件 → 用户点击链接 → 验证Token → 激活用户(is_active=True)
Token 特性 :
-
使用 secrets.token_urlsafe(32) 生成安全随机字符串
-
存储在数据库 verification_token 表
-
有效期 24 小时
-
一次性使用(验证后删除)
邮件内容 :
-
包含验证按钮(链接到后端验证接口)
-
显示完整 Token 供复制
-
提供备用链接(防止按钮无法点击)
python
def send_verification_email(user, verification_url, token):
"""发送邮箱验证邮件"""
try:
config = current_app.config
# 构建邮件内容
subject = '房屋租赁系统 - 邮箱验证'
html_content = f"""
<html>
<head>
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background-color: #5b6bf8; color: white; padding: 20px; text-align: center; border-radius: 8px; }}
.content {{ padding: 20px; background-color: #f9f9f9; border-radius: 8px; margin-top: 20px; }}
.button {{ display: inline-block; background-color: #5b6bf8; color: white; padding: 12px 30px; text-decoration: none; border-radius: 6px; margin: 20px 0; }}
.token-box {{ background-color: #fff; border: 2px dashed #5b6bf8; border-radius: 8px; padding: 20px; margin: 20px 0; text-align: center; }}
.token-label {{ color: #666; font-size: 14px; margin-bottom: 10px; }}
.token-code {{ font-size: 28px; font-weight: bold; color: #5b6bf8; letter-spacing: 3px; word-break: break-all; background-color: #f0f4ff; padding: 12px; border-radius: 6px; margin: 10px 0; }}
.copy-hint {{ color: #999; font-size: 12px; margin-top: 10px; }}
.footer {{ text-align: center; margin-top: 20px; color: #666; font-size: 12px; }}
</style>
</head>
<body>
<div class="header">
<h1>欢迎加入房屋租赁系统</h1>
</div>
<div class="content">
<h2>尊敬的 {user.username},您好!</h2>
<p>感谢您注册房屋租赁系统。为了完成注册,请点击下方按钮验证您的邮箱:</p>
<div style="text-align: center;">
<a href="{verification_url}" class="button">验证邮箱</a>
</div>
<div class="token-box">
<p class="token-label">您的验证令牌(可直接复制):</p>
<div class="token-code">{token}</div>
<p class="copy-hint">请复制上方令牌,在验证页面输入完成验证</p>
</div>
<p>如果按钮无法点击,请复制以下链接到浏览器地址栏:</p>
<p style="word-break: break-all; color: #666; font-size: 12px;">{verification_url}</p>
<p>本验证链接有效期为 24 小时,过期后请重新申请。</p>
</div>
<div class="footer">
<p>此邮件由系统自动发送,请勿直接回复。</p>
</div>
</body>
</html>
"""
# 创建邮件
msg = MIMEMultipart()
# 设置 From 头:QQ邮箱要求使用纯邮箱地址格式
msg['From'] = config['MAIL_USERNAME']
msg['To'] = user.email
msg['Subject'] = subject
msg.attach(MIMEText(html_content, 'html', 'utf-8'))
# 发送邮件
if config['MAIL_USE_SSL']:
server = smtplib.SMTP_SSL(config['MAIL_SERVER'], config['MAIL_PORT'])
else:
server = smtplib.SMTP(config['MAIL_SERVER'], config['MAIL_PORT'])
if config['MAIL_USE_TLS']:
server.starttls()
server.login(config['MAIL_USERNAME'], config['MAIL_PASSWORD'])
text = msg.as_string()
server.sendmail(config['MAIL_USERNAME'], user.email, text)
server.quit()
current_app.logger.info(f"验证邮件已发送到 {user.email}")
return True
except Exception as e:
current_app.logger.error(f"发送验证邮件失败: {str(e)}")
return False
@auth_bp.route('/register', methods=['POST'])
def register():
"""用户注册(支持短信验证码或邮箱)"""
data = request.get_json()
username = data.get('username')
password = data.get('password')
phone = data.get('phone')
email = data.get('email')
code = data.get('code')
use_sms = data.get('use_sms', False)
use_email = data.get('use_email', False)
# 获取极验验证参数
lot_number = data.get('lot_number')
captcha_output = data.get('captcha_output')
pass_token = data.get('pass_token')
gen_time = data.get('gen_time')
# 验证极验
if not verify_geetest(lot_number, captcha_output, pass_token, gen_time):
return jsonify({"msg": "验证码验证失败"}), 400
if use_email:
# 邮箱注册
if not username or not password or not email:
return jsonify({"msg": "用户名、密码和邮箱都是必填项"}), 400
if User.query.filter_by(username=username).first():
return jsonify({"msg": "用户名已存在"}), 400
if User.query.filter_by(email=email).first():
return jsonify({"msg": "该邮箱已注册,请直接登录"}), 400
# 创建用户,设置为未激活状态
hashed_password = generate_password_hash(password)
new_user = User(
username=username,
password_hash=hashed_password,
email=email,
phone='',
is_active=False,
email_verified=False
)
db.session.add(new_user)
db.session.commit()
# 生成验证令牌
verification_token = VerificationToken.create_email_verification_token(new_user.id)
# 构建验证链接(后端直接验证)
verification_url = f"{request.url_root}api/verify-email?token={verification_token.token}"
# 发送验证邮件
email_sent = send_verification_email(new_user, verification_url, verification_token.token)
return jsonify({
"msg": "注册成功,请查收邮件验证您的邮箱",
"email_sent": email_sent,
"user_id": new_user.id
})
elif use_sms:
# 短信注册(手机注册无需邮箱验证)
if not phone or not code:
return jsonify({"msg": "手机号和验证码都是必填项"}), 400
if not username:
return jsonify({"msg": "用户名是必填项"}), 400
if not password:
return jsonify({"msg": "密码是必填项"}), 400
config = get_sms_config()
phone_key = f"register:{phone}"
if phone_key not in sms_codes:
return jsonify({"msg": "请先获取验证码"}), 400
sms_info = sms_codes[phone_key]
current_time = time.time()
if sms_info['used']:
return jsonify({"msg": "验证码已使用"}), 400
if current_time - sms_info['time'] > config['valid_time']:
return jsonify({"msg": "验证码已过期"}), 400
if sms_info['code'] != code:
return jsonify({"msg": "验证码错误"}), 400
sms_info['used'] = True
if User.query.filter_by(username=username).first():
return jsonify({"msg": "用户名已存在"}), 400
if User.query.filter_by(phone=phone).first():
return jsonify({"msg": "该手机号已注册,请直接登录"}), 400
# 手机注册直接激活
hashed_password = generate_password_hash(password)
new_user = User(
username=username,
password_hash=hashed_password,
phone=phone,
is_active=True
)
db.session.add(new_user)
db.session.commit()
return jsonify({"msg": "注册成功"})
else:
# 普通注册(无验证,不推荐)
if not username or not password:
return jsonify({"msg": "用户名和密码都是必填项"}), 400
if User.query.filter_by(username=username).first():
return jsonify({"msg": "用户名已存在"}), 400
hashed_password = generate_password_hash(password)
new_user = User(
username=username,
password_hash=hashed_password,
phone='',
is_active=True
)
db.session.add(new_user)
db.session.commit()
return jsonify({"msg": "注册成功"})
python
def render_verify_result(status, message):
"""渲染邮箱验证结果页面"""
if status == 'success':
icon_class = 'fa-check-circle'
icon_color = '#52c41a'
bg_gradient = 'linear-gradient(135deg, #52c41a, #73d13d)'
else:
icon_class = 'fa-times-circle'
icon_color = '#ff4d4f'
bg_gradient = 'linear-gradient(135deg, #ff4d4f, #ff7875)'
html = f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>邮箱验证 - 房屋租赁系统</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: #f5f5f5;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}}
.container {{
background-color: #fff;
border-radius: 24px;
padding: 60px 40px 40px;
max-width: 480px;
width: 100%;
text-align: center;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.12);
}}
.icon-wrapper {{
width: 120px;
height: 120px;
background: {bg_gradient};
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin: 0 auto 24px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}}
.icon {{
font-size: 60px;
color: #fff;
}}
.title {{
font-size: 28px;
font-weight: 700;
color: #333;
margin-bottom: 16px;
}}
.message {{
font-size: 16px;
color: #666;
line-height: 1.6;
margin-bottom: 32px;
}}
.version {{
margin-top: 32px;
font-size: 12px;
color: #999;
}}
</style>
</head>
<body>
<div class="container">
<div class="icon-wrapper">
<i class="fa-solid {icon_class} icon"></i>
</div>
<h1 class="title">{'验证成功' if status == 'success' else '验证失败'}</h1>
<p class="message">{message}</p>
<p class="version">房屋出租管理系统 v1.0</p>
</div>
</body>
</html>
"""
return Response(html, mimetype='text/html')
@auth_bp.route('/verify-email', methods=['GET'])
def verify_email_get():
"""通过邮件链接验证邮箱(GET请求)"""
token = request.args.get('token')
if not token:
return render_verify_result('error', '验证令牌不能为空')
# 验证令牌
verification_token = VerificationToken.verify_token(token, 'email_verification')
if not verification_token:
return render_verify_result('error', '验证链接无效或已过期')
# 获取用户
user = User.query.get(verification_token.user_id)
if not user:
return render_verify_result('error', '用户不存在')
if user.email_verified:
return render_verify_result('success', '邮箱已经验证过了,您可以直接登录')
# 激活用户
user.is_active = True
user.email_verified = True
db.session.commit()
# 删除令牌
VerificationToken.delete_token(token)
return render_verify_result('success', '邮箱验证成功,账号已激活!')
python
@auth_bp.route('/resend-verification-email', methods=['POST'])
def resend_verification_email():
"""重新发送验证邮件"""
data = request.get_json()
email = data.get('email')
if not email:
return jsonify({"msg": "邮箱不能为空"}), 400
# 获取极验验证参数
lot_number = data.get('lot_number')
captcha_output = data.get('captcha_output')
pass_token = data.get('pass_token')
gen_time = data.get('gen_time')
# 验证极验
if not verify_geetest(lot_number, captcha_output, pass_token, gen_time):
return jsonify({"msg": "验证码验证失败"}), 400
# 查找用户
user = User.query.filter_by(email=email).first()
if not user:
return jsonify({"msg": "该邮箱未注册"}), 404
if user.email_verified:
return jsonify({"msg": "邮箱已经验证过了"}), 400
# 删除旧的验证令牌
old_tokens = VerificationToken.query.filter_by(
user_id=user.id,
token_type='email_verification'
).all()
for token in old_tokens:
db.session.delete(token)
db.session.commit()
# 创建新的验证令牌
verification_token = VerificationToken.create_email_verification_token(user.id)
# 构建验证链接(后端直接验证)
verification_url = f"{request.url_root}api/verify-email?token={verification_token.token}"
# 发送验证邮件
email_sent = send_verification_email(user, verification_url, verification_token.token)
return jsonify({
"msg": "验证邮件已重新发送",
"email_sent": email_sent
})
2.前端实现
页面文件 :
-
uniapp/pages/auth/register.vue - 注册页面,注册成功后显示全屏遮罩弹窗
-
uniapp - 邮箱验证页面
验证页面功能 :
-
自动验证(URL 带 token 参数时)
-
手动输入 Token 验证
-
重新发送验证邮件(带极验验证码)
-
60秒发送间隔倒计时

三、功能二:忘记密码邮箱验证码

1. 后端实现
接口路由 :
- POST /api/send-email-code - 发送邮箱验证码
- POST /api/forgot-password - 使用验证码重置密码
核心流程 :
PlainText
用户输入邮箱 → 极验验证 → 检查邮箱已注册 → 生成6位验证码 → 发送邮件 → 用
户输入验证码 → 验证并重置密码
验证码特性 :
-
6位数字验证码
-
存储在内存字典 email_codes
-
有效期 5 分钟(300秒)
-
发送间隔 60 秒
-
一次性使用(验证后标记 used=True)
邮件内容 :
-
大字体显示 6 位验证码
-
提示 5 分钟内有效
-
简洁的 HTML 模板
python
def generate_sms_code(length=6):
"""生成随机短信验证码"""
return ''.join(random.choice(string.digits) for _ in range(length))
# 内存存储邮箱验证码(生产环境建议使用Redis)
email_codes = {}
email_send_times = {}
def send_email_verification_code(email, code):
"""发送邮箱验证码"""
try:
config = current_app.config
subject = '房屋租赁系统 - 忘记密码验证码'
html_content = f"""
<html>
<head>
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background-color: #5b6bf8; color: white; padding: 20px; text-align: center; border-radius: 8px; }}
.content {{ padding: 20px; background-color: #f9f9f9; border-radius: 8px; margin-top: 20px; }}
.token-box {{ background-color: #fff; border: 2px dashed #5b6bf8; border-radius: 8px; padding: 20px; margin: 20px 0; text-align: center; }}
.token-label {{ color: #666; font-size: 14px; margin-bottom: 10px; }}
.token-code {{ font-size: 36px; font-weight: bold; color: #5b6bf8; letter-spacing: 5px; background-color: #f0f4ff; padding: 15px; border-radius: 6px; margin: 10px 0; }}
.copy-hint {{ color: #999; font-size: 12px; margin-top: 10px; }}
.footer {{ text-align: center; margin-top: 20px; color: #666; font-size: 12px; }}
</style>
</head>
<body>
<div class="header">
<h1>忘记密码 - 验证码</h1>
</div>
<div class="content">
<h2>您好!</h2>
<p>您正在重置房屋租赁系统的密码,请使用以下验证码完成操作:</p>
<div class="token-box">
<p class="token-label">您的验证码:</p>
<div class="token-code">{code}</div>
<p class="copy-hint">请在5分钟内使用此验证码</p>
</div>
<p>如果您没有进行此操作,请忽略此邮件。</p>
</div>
<div class="footer">
<p>此邮件由系统自动发送,请勿直接回复。</p>
</div>
</body>
</html>
"""
msg = MIMEMultipart()
msg['From'] = config['MAIL_USERNAME']
msg['To'] = email
msg['Subject'] = subject
msg.attach(MIMEText(html_content, 'html', 'utf-8'))
if config['MAIL_USE_SSL']:
server = smtplib.SMTP_SSL(config['MAIL_SERVER'], config['MAIL_PORT'])
else:
server = smtplib.SMTP(config['MAIL_SERVER'], config['MAIL_PORT'])
if config['MAIL_USE_TLS']:
server.starttls()
server.login(config['MAIL_USERNAME'], config['MAIL_PASSWORD'])
text = msg.as_string()
server.sendmail(config['MAIL_USERNAME'], email, text)
server.quit()
current_app.logger.info(f"验证码邮件已发送到 {email}")
return True
except Exception as e:
current_app.logger.error(f"发送验证码邮件失败: {str(e)}")
return False
@auth_bp.route('/send-email-code', methods=['POST'])
def send_email_code():
"""发送邮箱验证码(用于忘记密码)"""
data = request.get_json()
email = data.get('email')
# 获取极验验证参数
lot_number = data.get('lot_number')
captcha_output = data.get('captcha_output')
pass_token = data.get('pass_token')
gen_time = data.get('gen_time')
if not email:
return jsonify({"msg": "邮箱是必填项"}), 400
# 验证极验
if not verify_geetest(lot_number, captcha_output, pass_token, gen_time):
return jsonify({"msg": "验证码验证失败"}), 400
# 检查邮箱是否已注册
user = User.query.filter_by(email=email).first()
if not user:
return jsonify({"msg": "该邮箱未注册"}), 400
email_key = f"forget:{email}"
current_time = time.time()
email_interval = 60 # 60秒间隔
email_valid_time = 300 # 5分钟有效
# 检查是否有未过期的验证码
if email_key in email_codes:
email_info = email_codes[email_key]
if not email_info['used'] and (current_time - email_info['time'] <= email_valid_time):
return jsonify({"msg": "验证码仍在有效期内,请直接使用"}), 400
if email_key in email_send_times:
last_send_time = email_send_times[email_key]
if current_time - last_send_time < email_interval:
remaining = int(email_interval - (current_time - last_send_time))
return jsonify({"msg": f"请{remaining}秒后再试"}), 400
code = generate_sms_code()
# 发送验证码邮件
email_sent = send_email_verification_code(email, code)
if not email_sent:
return jsonify({"msg": "验证码邮件发送失败,请稍后重试"}), 500
email_codes[email_key] = {
'code': code,
'time': current_time,
'used': False
}
email_send_times[email_key] = current_time
return jsonify({"msg": "验证码已发送到您的邮箱"})
2.前端实现
页面文件 :
-
uniapp/pages/auth/forget.vue - 忘记密码页面
功能特性 :
-
手机号/邮箱切换标签
-
邮箱格式验证(包含 @)
-
极验验证码保护
-
60秒倒计时
-
表单验证(根据重置类型验证不同字段)
四、功能三:重新发送验证邮件
1. 后端实现
接口路由 :
- POST /api/resend-verification-email - 重新发送注册验证邮件
核心流程 :
用户输入邮箱 → 极验验证 → 查找用户 → 删除旧Token → 生成新Token → 发送邮
件
功能特性 :
- 需要极验验证码
- 删除旧的验证令牌
- 生成新的验证令牌
- 重新发送邮件
python
@auth_bp.route('/resend-verification-email', methods=['POST'])
def resend_verification_email():
"""重新发送验证邮件"""
data = request.get_json()
email = data.get('email')
if not email:
return jsonify({"msg": "邮箱不能为空"}), 400
# 获取极验验证参数
lot_number = data.get('lot_number')
captcha_output = data.get('captcha_output')
pass_token = data.get('pass_token')
gen_time = data.get('gen_time')
# 验证极验
if not verify_geetest(lot_number, captcha_output, pass_token, gen_time):
return jsonify({"msg": "验证码验证失败"}), 400
# 查找用户
user = User.query.filter_by(email=email).first()
if not user:
return jsonify({"msg": "该邮箱未注册"}), 404
if user.email_verified:
return jsonify({"msg": "邮箱已经验证过了"}), 400
# 删除旧的验证令牌
old_tokens = VerificationToken.query.filter_by(
user_id=user.id,
token_type='email_verification'
).all()
for token in old_tokens:
db.session.delete(token)
db.session.commit()
# 创建新的验证令牌
verification_token = VerificationToken.create_email_verification_token(user.id)
# 构建验证链接(后端直接验证)
verification_url = f"{request.url_root}api/verify-email?token={verification_token.token}"
# 发送验证邮件
email_sent = send_verification_email(user, verification_url, verification_token.token)
return jsonify({
"msg": "验证邮件已重新发送",
"email_sent": email_sent
})
五、数据库设计
1. User 模型相关字段

2.VerificationToken 模型

六、邮箱发送配置
配置项 (在 config.py 中):

发送方式 :
- 使用 Python smtplib 库
- 支持 SSL/TLS 连接
- HTML 格式邮件
- From 头使用纯邮箱地址格式
七、安全机制

八、存在的问题和改进建议
