首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >接手一个老项目,光邮件验证码的发送逻辑就重构了三遍

接手一个老项目,光邮件验证码的发送逻辑就重构了三遍

原创
作者头像
外滩首席运维
发布2026-07-16 11:58:24
发布2026-07-16 11:58:24
600
举报
文章被收录于专栏:Spug运维自动化Spug运维自动化

去年接手了一个内部的后台管理系统,Python + Flask 写的,跑了快三年。注册和登录用的是邮箱验证码,发送逻辑从最初到现在改了三次,每次都是被线上问题逼的。

记录一下这三次重构的过程和踩过的坑。

第一版:直接调 smtplib

最初的实现很直接,在注册接口里同步调 smtplib 发邮件:

代码语言:python
复制
import smtplib
from email.mime.text import MIMEText

def send_code(to_email, code):
    msg = MIMEText(f'您的验证码是 {code},5 分钟内有效。')
    msg['Subject'] = '登录验证码'
    msg['From'] = 'noreply@company.com'
    msg['To'] = to_email

    with smtplib.SMTP('localhost', 25) as server:
        server.send_message(msg)

本地装了 postfix 做 MTA,出去的邮件走 ECS 公网 IP。

这版能用,但有几个问题:

第一,同步调用。SMTP 握手 + 发送一封邮件要 2-5 秒,用户点"获取验证码"之后要等这么久才能看到提示,体验很差。如果 SMTP 连接超时,接口直接 500。

第二,到达率不稳定。Gmail 基本全进垃圾箱,QQ 邮箱也经常进。后来才知道是 SPF、DKIM 没配,ECS 的 IP 信誉也不好。

第三,邮件发送和业务逻辑耦合在一起,注册接口既要生成验证码、又要发邮件、还要写缓存,一个函数干了太多事。

第二版:异步队列 + 自建 SMTP

为了解决同步阻塞的问题,加了 Celery + Redis 做异步任务队列:

代码语言:python
复制
from celery import Celery

celery_app = Celery('mail', broker='redis://localhost:6379/0')

@celery_app.task(bind=True, max_retries=3)
def send_verification_email(self, to_email, code):
    try:
        msg = MIMEText(f'您的验证码是 {code},5 分钟内有效。')
        msg['Subject'] = '登录验证码'
        msg['From'] = 'noreply@company.com'
        msg['To'] = to_email

        with smtplib.SMTP('localhost', 25) as server:
            server.send_message(msg)
    except Exception as exc:
        raise self.retry(exc=exc, countdown=10)

接口里只负责生成验证码、写 Redis、把发送任务丢进队列,响应时间降到了 200ms 以内。

同时在 postfix 上配了 SPF 和 DKIM,到达率确实好了一些。但维护成本上来了:

Celery worker 要单独跑一个进程,偶尔会挂,得加进 supervisor 管理。Redis 也得维护,虽然本来就在用,但队列堆积的时候还是得关注。

postfix 的 DKIM 密钥要管理,DNS 记录改完之后等生效的过程让人焦虑。最烦的是 IP 黑名单问题——ECS 的公网 IP 是共享的,前面用这个 IP 的人发过垃圾邮件,导致 IP 上了好几个黑名单。去一个个申请解除,有的平台要等几周。

而且还有一个隐蔽的问题:发送失败了怎么知道?Celery 的重试机制能应对临时性故障,但如果邮件发出去了、对方服务器拒收了或者进了垃圾箱,从 Celery 这边是看不出来的。用户说"没收到验证码",我只能去查 postfix 的日志,一条条翻。

第三版:换成 HTTP 接口

折腾了大半年之后,决定把自建 SMTP 这套东西全扔掉。验证码邮件的量不大,一天也就几十封,不值得为它维护一整套邮件基础设施。

换成了第三方的 HTTP 发送接口,代码变成了这样:

代码语言:python
复制
import requests

def send_verification_email(to_email, code):
    resp = requests.post(
        'https://push.spug.cc/mail/你的模板编码',
        json={
            'to': to_email,
            'code': code
        },
        timeout=5
    )
    result = resp.json()
    # code=200 只代表请求入库成功,邮件是异步发的
    return result.get('request_id')

几个变化:

不需要 Celery 了。HTTP 请求本身就很快(通常 200-500ms),直接在接口里同步调就行。如果担心偶尔超时影响注册流程,包一层 try-except,发送失败就让用户重新获取。

不需要维护 postfix 了。SPF、DKIM、DMARC 都是发送方那边维护的,IP 信誉也不用操心。之前 Gmail 全进垃圾箱的问题直接消失了。

发送结果可追踪。接口返回一个 request_id,如果用户反馈没收到,拿这个 ID 去日志接口查,能看到邮件的实际发送状态——是发出去了还是被退回了。比以前翻 postfix 日志方便很多。

有一点要注意:接口返回 code: 200 只是请求入库成功,邮件是异步发送的。如果你的业务逻辑需要确认送达,得额外写一个查询逻辑。不过验证码场景一般不需要,用户收到就用,没收到就重发。

三次重构回头看

第一版的问题是同步阻塞和邮件到达率,用 smtplib 直接发是最快的实现方式,但不适合生产环境。

第二版解决了同步问题,但引入了 Celery + SMTP 的运维成本。对于一天几十封验证码的场景,这个架构太重了。

第三版去掉了所有中间件,一个 HTTP 请求搞定。代码最少,运维成本最低,到达率反而最好。

如果让我重新来做这个需求,大概率直接从第三版开始。自建 SMTP 发邮件这条路,除非你的邮件量大到需要精细控制每一个环节,否则投入产出不成正比。

验证码邮件说到底就是个工具——用户注册的时候能收到就行,不需要花太多工程精力在"怎么把邮件发出去"这件事上。


邮件发送接口用的是 push.spug.cc,管理后台在 sms.spug.cc,有类似需求可以看看。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 第一版:直接调 smtplib
  • 第二版:异步队列 + 自建 SMTP
  • 第三版:换成 HTTP 接口
  • 三次重构回头看
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档