美多商城项目-02

短信验证码实现逻辑

最终要变成这样三步:

  1. 前端加载注册页时: 请求
    GET /users/image_codes/<uuid>/ 拿到图片验证码
    → 你这一步已经实现
  2. 用户输入图形验证码 & 点击「获取短信验证码」按钮:
    前端发请求到
    GET /sms_codes/<mobile>/?image_code=xxx&image_code_id=uuid
    后端做:
    • 校验图形验证码(从 Redis 取 img_<uuid>
    • 限制 60 秒内同一手机号只能发一次
    • 生成 4 位短信验证码,写入 Redis sms_<mobile>
    • 调用 Celery 异步任务发短信
  3. 用户填完表单点..击「注册」:
    后端 RegisterView.post
    • 不再校验图片验证码
    • 改为去 Redis 拿 sms_<mobile>,对比前端传来的 sms_code
    • 正确就创建用户

云通讯短信

注册容联云:https://www.yuntongxun.com/

进入免费测试指南:https://doc.yuntongxun.com/p/5a531a353b8496dd00dcdfe2

按照文档:绑定测试账号->寻找 python SDK

美多商城项目-02

Python SDK 文档:https://doc.yuntongxun.com/p/5f029ae7a80948a1006e776e

安装SDK

pip install ronglian_sms_sdk

见最下面的调用示例(见文档即可):

from ronglian_sms_sdk import SmsSDK

accId = '容联云通讯分配的主账号ID'
accToken = '容联云通讯分配的主账号TOKEN'
appId = '容联云通讯分配的应用ID'

def send_message():
    sdk = SmsSDK(accId, accToken, appId)
    # 模板ID,测试时填“1”
    tid = '容联云通讯创建的模板ID'
    mobile = '手机号1,手机号2'
    # 短信中的变量
    datas = ('变量1', '变量2')
    resp = sdk.sendMessage(tid, mobile, datas)
    print(resp)

# 调用
if __name__ == "__main__":
    send_message()
参数名类型说明
accIdString开发者主账号(ACCOUNT SID),登录云通讯网站后可在控制台首页看到
accTokenString主账号令牌(AUTH TOKEN),登录云通讯网站后可在控制台首页看到
appIdString应用 ID(APPID),请使用管理后台已创建应用的 APPID

在控制台自行查看上述参数:https://console.yuntongxun.com/member/main

美多商城项目-02

免费开发测试需注意

1.免费开发测试需要使用到"控制台首页",开发者主账户相关信息,如主账号、应用ID等。
2.免费开发测试使用的模板ID为1,具体内容:【云通讯】您的验证码是{1},请于{2}分钟内正确输入。其中{1}和{2}为短信模板参数。
3.测试成功后,即可申请短信模板并 正式使用 。

执行测试

美多商城项目-02
美多商城项目-02
美多商城项目-02

云通信后端逻辑

第一步:在 settings.py 放短信的配置(避免硬编码)

libs/ 专门放“工具类代码”,不要不要硬编码云通讯账号信息 → 而是放到 settings.py(未来上线时你可以把这些放到环境变量,提高安全性。)

meiduo_mall/settings.py 里新增

# 容联云通讯短信配置

RONG_LIAN = {
    "accId": "你的accId",
    "accToken": "你的accToken",
    "appId": "你的appId",
}

第二步:封装短信发送类(libs/sms/ronglian.py)

封装一个短信发送类 libs/sms/ronglian.py(sms 是新建的 python 软件包)

# libs/sms/ronglian.py

import json
from ronglian_sms_sdk import SmsSDK
from django.conf import settings
import logging

logger = logging.getLogger("meiduo")


class RongLianSMS:
    """容联云通讯短信发送封装"""

    def __init__(self):
        cfg = settings.RONG_LIAN
        self.sdk = SmsSDK(cfg["accId"], cfg["accToken"], cfg["appId"])

    def send(self, mobile, template_id, datas):
        """
        datas: ('验证码', '有效期')
        """
        try:
            resp_str = self.sdk.sendMessage(template_id, mobile, datas)

            # SDK 返回的是字符串,需要手动 loads
            resp = json.loads(resp_str)

            logger.info(f"短信发送 → 手机号={mobile}, 返回={resp}")

            # 判断发送是否成功
            if resp.get("statusCode") == "000000":
                return True
            else:
                logger.error(f"短信发送失败 → {resp}")
                return False

        except Exception as e:
            logger.error("短信 SDK 调用异常", exc_info=True)
            return False

第三步:Celery 异步发短信任务

首先安装 celery:

pip install celery

根目录创建celery_tasks软件包 以及celery_tasks下的 sms python 软件包

目录结构:

meiduo_mall/
├── __init__.py
├── celeryconfig.py -> 配置文件
├── main.py -> 应用入口(创建 app 对象)
└── sms
    ├── __init__.py
    └── tasks.py -> 任务函数所在(业务逻辑)

celery_tasks/main.py

# celery_tasks/main.py
import os
from celery import Celery

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "meiduo_mall.settings")

# 创建 Celery 应用
app = Celery("meiduo")

# 直接加载独立 Celery 配置
app.config_from_object("celery_tasks.celeryconfig")

# 明确告诉 Celery 去扫描 celery_tasks/sms/tasks.py
app.autodiscover_tasks([
    "celery_tasks.sms",
])

celery_tasks/celeryconfig.py

##  在生产环境,celeryconfig 通常不是 python 文件,而是环境变量控制:

# broker_url = os.getenv("CELERY_BROKER_URL")
# result_backend = os.getenv("CELERY_RESULT_BACKEND")
# timezone = "Asia/Shanghai"

# 环境变量控制
# export CELERY_BROKER_URL=redis://10.0.0.1:6379/5
# export CELERY_RESULT_BACKEND=redis://10.0.0.1:6379/6


# =========== Celery 配置 ===========

# 使用 Redis 作为 Celery broker(任务调度队列)
broker_url = "redis://127.0.0.1:6379/5"

# Celery worker 执行结果存储(可不需要,但建议加上)
result_backend = "redis://127.0.0.1:6379/6"

# 时区与 Django 保持一致
timezone = "Asia/Shanghai"

task_serializer = "json"
result_serializer = "json"
accept_content = ["json"]

celery_tasks/sms/tasks.py

# celery_tasks/sms/tasks.py
from celery_tasks.main import app
from libs.sms.ronglian import RongLianSMS

import logging

# 使用 Celery 的 logger(而不是 print)
# 好处:日志等级可控、可写入日志文件、不会和多进程输出乱序
logger = logging.getLogger("celery")


@app.task(
    bind=True,              # bind=True:使第一个参数是 task 对象本身(self),好用于 retry()
    default_retry_delay=5   # 任务失败后默认等待 5 秒再重试
)
def send_sms_code(self, mobile, sms_code):
    """
    异步发送短信验证码任务(Celery Worker 执行)
    :param mobile: 手机号
    :param sms_code: 验证码内容
    """

    # 记录任务开始日志(方便排查)
    logger.info(f"开始向 {mobile} 发送短信验证码 {sms_code}")

    try:
        # 调用容联云短信 SDK 发送短信
        # template_id = "1" 表示你在容联云平台配置的模板编号
        # datas = (短信验证码, 有效期)
        ok = RongLianSMS().send(
            mobile,
            "1",
            (sms_code, "5")
        )

        # 打印容联云返回结果 True / False
        logger.info(f"容联云返回: ok={ok}")

        # 如果 SDK 返回失败,主动抛异常触发 retry()
        if not ok:
            raise Exception("短信发送失败")

    except Exception as e:
        # 记录错误日志(包括容联云 API 错误、网络异常等)
        logger.error(f"短信发送失败: {e}")

        # Celery 自动重试(最多重试 10 次)
        # retry() 会再次把任务丢回队列,不会阻塞当前 worker
        raise self.retry(exc=e, max_retries=10)

在 celery_tasks/init.py 中显式导入 task 模块

from .main import app
import celery_tasks.sms.tasks

启动 Celery Worker

进入项目根目录,启动 worker:

celery -A celery_tasks.main worker -l info

你应该看到如下日志:

美多商城项目-02

celery 异步执行原理讲解:

1、Django 用户触发发送验证码接口

例如 users/sms_codes/<mobile>/

后端代码:

send_sms_code.delay(mobile, sms_code)

把任务推到 Redis(broker)里排队,不在主进程里执行,几毫秒就返回给前端(不会卡主)

2、Redis Broker(队列)存储任务

Celery 会把任务序列化成 JSON:

{
  "id": "uuid",
  "task": "celery_tasks.sms.tasks.send_sms_code",
  "args": ["手机号", "验证码"],
  "kwargs": {}
}

Celery Worker 后台执行真正的发送逻辑

Worker 启动命令:celery -A celery_tasks.main worker -l info

  • Worker 会:
  • 连接 Redis
  • 监听是否有新任务
  • 收到任务后执行 send_sms_code()

这时候才会调用:RongLianSMS().send(mobile, template_id, datas),真正运行云通讯 SDK 发送短信。

美多商城项目-02

第四步:在 users/views.py 写“短信验证码接口”

把它放在 ImageCodeView 下面即可

# ============================================================
# 短信验证码
# ============================================================
from django.views import View
from django import http
from django_redis import get_redis_connection
from random import randint
import logging

logger = logging.getLogger("django")

# 过期时间(你可以放 settings 也可以写在本文件顶部)
SMS_CODE_EXPIRE_TIME = 300     # 短信验证码 300 秒
SMS_FLAG_EXPIRE_TIME = 60      # 发送频率标记 60 秒


class SmsCodeView(View):
    """
    发送短信验证码
    URL: /sms_codes/<mobile>/?image_code=xxx&image_code_id=uuid
    """

    def get(self, request, mobile):
        # 1. 接收参数
        image_code = request.GET.get("image_code")
        image_code_id = request.GET.get("image_code_id")

        if not all([mobile, image_code, image_code_id]):
            return http.JsonResponse({"code": "4003", "errmsg": "缺少参数"})

        # 2. 取 Redis 图片验证码
        redis_conn = get_redis_connection("verify_codes")
        redis_key = f"img_{image_code_id}"
        real_image_code = redis_conn.get(redis_key)

        if real_image_code is None:
            return http.JsonResponse({"code": "4001", "errmsg": "验证码已过期"})

        # 用完删除
        redis_conn.delete(redis_key)

        # 校验(忽略大小写)
        if real_image_code.decode().lower() != image_code.lower():
            return http.JsonResponse({"code": "4001", "errmsg": "验证码错误"})

        # 3. 发送频率控制:60秒
        send_flag = redis_conn.get(f"send_flag_{mobile}")
        if send_flag:
            return http.JsonResponse({"code": "4002", "errmsg": "请勿频繁发送短信"})

        # 4. 生成短信验证码
        sms_code = "%04d" % randint(0, 9999)
        logger.info(f"{mobile} 的短信验证码是:{sms_code}")

        # 5. Redis 管道一次性写入
        pl = redis_conn.pipeline()
        pl.setex(f"sms_{mobile}", SMS_CODE_EXPIRE_TIME, sms_code)
        pl.setex(f"send_flag_{mobile}", SMS_FLAG_EXPIRE_TIME, 1)
        pl.execute()

        # 6. Celery 异步发送短信
        from celery_tasks.sms.tasks import send_sms_code
        send_sms_code.delay(mobile, sms_code)

        # 7. 返回成功
        return http.JsonResponse({"code": "0", "errmsg": "短信发送成功"})

第五步:注册时不再校验图片验证码,而是校验短信验证码

在 RegisterView.post 里删掉你之前的图片校验:

美多商城项目-02

第六步:完善 url

apps/users/urls.py

from django.urls import path
from .views import SmsCodeView # 导入

urlpatterns = [
    ...
    path('sms_codes/<mobile>/', SmsCodeView.as_view()), # 添加这行
]

第七步:可选临时检测方案->手机验证码是否输入正确判断

注册视图加入短信验证码校验

apps/users/views.py 修改 RegisterView.post()

from django_redis import get_redis_connection

class RegisterView(View):
    def post(self, request):
        ...

        # 获取用户填写的短信验证码
        sms_code_client = request.POST.get("sms_code")

        # 判断是否填写
        if not sms_code_client:
            return http.HttpResponseBadRequest("请填写短信验证码")

        # 从 redis 读取
        redis_conn = get_redis_connection("verify_codes")
        sms_code_server = redis_conn.get(f"sms_{mobile}")

        if sms_code_server is None:
            return http.HttpResponseBadRequest("短信验证码已过期")

        # 对比
        if sms_code_client != sms_code_server.decode():
            return http.HttpResponseBadRequest("短信验证码错误")

        # 校验成功 → 删除 redis 验证码(防止重复使用)
        redis_conn.delete(f"sms_{mobile}")

云通讯前端

static/js/register.js

// 点击按钮发送短信验证码
send_sms_code: function () {

    if (this.sending_flag == true) return;
    this.sending_flag = true;

    // 1. 输入基本校验
    this.check_mobile();
    this.check_image_code();

    if (this.error_mobile || this.error_image_code) {
        this.sending_flag = false;
        return;
    }

    // 2. 构造 URL
    var url = this.host + '/sms_codes/' + this.mobile +
        '/?image_code=' + this.image_code +
        '&image_code_id=' + this.image_code_id;

    console.log("短信请求URL:", url);

    // 3. 发请求
    axios.get(url, { responseType: 'json' })
        .then(response => {

            let code = response.data.code;

            switch (code) {
                case '0':   // ======= 成功 =======
                    console.log("短信发送成功:", response.data);

                    // 刷新图形验证码(更安全)
                    this.generate_image_code();

                    // 开始倒计时
                    var num = 60;
                    this.sms_code_tip = num + '秒';

                    var timer = setInterval(() => {
                        num -= 1;
                        if (num <= 0) {
                            clearInterval(timer);
                            this.sms_code_tip = '获取短信验证码';
                            this.sending_flag = false;
                        } else {
                            this.sms_code_tip = num + '秒';
                        }
                    }, 1000);

                    break;

                case '4001':   // 图形验证码错误
                case '4003':   // 参数缺失
                    this.error_image_code_message = response.data.errmsg;
                    this.error_image_code = true;
                    this.generate_image_code();
                    this.sending_flag = false;
                    break;

                case '4002':   // 发送频率限制
                    this.error_sms_code_message = "发送过于频繁,请稍后再试";
                    this.error_sms_code = true;
                    this.sending_flag = false;
                    break;

                default:   // 其它后端异常
                    this.error_sms_code_message = response.data.errmsg || "短信发送失败";
                    this.error_sms_code = true;
                    this.sending_flag = false;
            }

        })
        .catch(error => {
            console.log("短信请求异常:", error);

            this.error_sms_code_message = "网络异常,请稍后重试";
            this.error_sms_code = true;
            this.sending_flag = false;

            // 强制刷新验证码防止漏洞
            this.generate_image_code();
        });
},

测试:

美多商城项目-02
美多商城项目-02
美多商城项目-02

登录页面完善

基于上一章结尾登录页面继续完善登录功能

现在的问题:后端 LoginView 基本没问题,主要是前端模板和 login.js 没有对上号,导致 Vue 校验其实没生效。

一、后端 LoginView 建议小改动

登录页不是只有一种入口(主动访问 vs 被要求登录),next 可以让“被拦截的用户”回到原本的页面,不写 next → 用户体验很糟糕

加上 next 跳转逻辑,其他逻辑可以保留你现在的写法。

# apps/users/views.py 里替换 LoginView

class LoginView(View):
    """登录视图"""

    def get(self, request):
        return render(request, "users/login.html")

    def post(self, request):
        # 1. 获取参数
        username = request.POST.get("username")
        password = request.POST.get("password")
        remembered = request.POST.get("remembered")  # 是否记住登录

        # 2. 必填检查
        if not all([username, password]):
            return http.HttpResponseBadRequest("缺少必要参数")

        # 3. 用户名格式检查(先按课程来,只支持用户名登录)
        if not re.match(r"^[a-zA-Z0-9_-]{5,20}$", username):
            return http.HttpResponseBadRequest("用户名格式不正确")

        # 4. 使用 Django 内置认证
        user = authenticate(username=username, password=password)

        if user is None:
            # 认证失败,返回登录页并带错误信息
            return render(request, "users/login.html", {"account_errmsg": "用户名或密码错误"})

        # 5. 登录成功:写 session
        login(request, user)

        # 6. 设置 session 过期时间
        if remembered == "on":
            request.session.set_expiry(30 * 24 * 3600)   # 30 天
        else:
            request.session.set_expiry(0)                # 关闭浏览器失效

        # 7. 处理 next 参数(从登录拦截回来时会带上)
        next_url = request.GET.get("next")
        if next_url:
            resp = redirect(next_url)
        else:
            resp = redirect(reverse("contents:index"))

        # 8. 设置 cookie,用于首页展示用户名
        resp.set_cookie("username", user.username, max_age=14 * 24 * 3600)

        return resp

原来只写死跳首页,现在加上 next 会更完整:

代码中:

next_url = request.GET.get("next")
if next_url:
    resp = redirect(next_url)
else:
    resp = redirect(reverse("contents:index"))

如果 URL 里带着 next 参数,比如:/login/?next=/user/,用户登录成功后应该回去 /user/,所以:resp = redirect(next_url)

如果用户是主动点击“登录”按钮进来的,而不是被拦截跳来的,这种情况 URL 就不会带 next

用户访问 /orders/ (需要登录)
        ↓
未登录 → 重定向到 /login/?next=/orders/
        ↓
用户输入用户名密码
        ↓
登录成功 → 发现 next 参数
        ↓
跳转回 /orders/

如果没有 next:

登录成功 → 回首页

二、login.html:让表单和 Vue 绑在一起

你现在的模板 没有 v-model / @submitstatic/js/login.js 里的 Vue 根本没参与登录。

改成下面这样(可以整体替换 templates/users/login.html):

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    <title>美多商城-登录</title>

    <!-- 静态文件引用 -->
    <link rel="stylesheet" href="{{ static('css/reset.css') }}">
    <link rel="stylesheet" href="{{ static('css/main.css') }}">

    <script src="{{ static('js/host.js') }}"></script>
    <script src="{{ static('js/vue-2.5.16.js') }}"></script>
    <script src="{{ static('js/axios-0.18.0.min.js') }}"></script>
</head>

<body>
<div id="app" v-cloak>

    <div class="login_top clearfix">
        <a href="/" class="login_logo">
            <img src="{{ static('images/logo02.png') }}">
        </a>
    </div>

    <div class="login_form_bg">
        <div class="login_form_wrap clearfix">
            <div class="login_banner fl"></div>
            <div class="slogan fl">商品美 · 种类多 · 欢迎光临</div>

            <div class="login_form fr">
                <div class="login_title clearfix">
                    <a class="cur">账户登录</a>
                </div>

                <div class="form_con">
                    <div class="form_input cur">

                        <!-- 登录表单,注意:加了 @submit="on_submit($event)" -->
                        <form method="post"
                              action="/users/login/"
                              id="login-form"
                              @submit="on_submit($event)">
                            {{ csrf_field(request) | safe }}

                            <!-- 用户名 -->
                            <input type="text"
                                   name="username"
                                   class="name_input"
                                   placeholder="请输入用户名"
                                   v-model="username"
                                   @blur="check_username">

                            <!-- 前端校验错误 -->
                            <div v-show="error_username"
                                 class="user_error">
                                [[ error_username_message ]]
                            </div>


                            <!-- 密码 -->
                            <input type="password"
                                   name="password"
                                   class="pass_input"
                                   placeholder="请输入密码"
                                   v-model="password"
                                   @blur="check_password">

                            <!-- 后端返回的账号错误(用户名或密码错误) -->
                            {% if account_errmsg %}
                            <div class="pwd_error" v-show="!error_password">
                                {{ account_errmsg }}
                            </div>
                            {% endif %}


                            <!-- 前端密码错误提示 -->
                            <div v-show="error_password"
                                 class="pwd_error">
                                [[ error_password_message ]]
                            </div>

                            <div class="more_input clearfix">
                                <!-- 记住登录,勾选后提交值为 on -->
                                <input type="checkbox"
                                       name="remembered"
                                       v-model="remembered">
                                <label>记住登录</label>
                            </div>

                            <input type="submit" value="登 录" class="input_submit">
                        </form>

                    </div>
                </div>

                <div class="third_party">
                    <a href="javascript:;" class="qq_login" @click="qq_login">QQ</a>
                    <a href="javascript:;" class="weixin_login">微信</a>
                    <a href="/users/register/" class="register_btn">立即注册</a>
                </div>

            </div>
        </div>
    </div>

    <div class="footer no-mp">
        <div class="foot_link">
            <a href="#">关于我们</a>
            <span>|</span>
            <a href="#">联系我们</a>
            <span>|</span>
            <a href="#">招聘人才</a>
            <span>|</span>
            <a href="#">友情链接</a>
        </div>
        <p>CopyRight © 2016 北京美多商业股份有限公司 All Rights Reserved</p>
        <p>电话:010-****888    京ICP备*******8号</p>
    </div>

</div>

<script src="{{ static('js/common.js') }}"></script>
<script src="{{ static('js/login.js') }}"></script>

</body>
</html>

关键点:

  1. <div id="app" v-cloak> 包住整个页面,Vue 接管这块 DOM。
  2. <form ... @submit="on_submit($event)"> 让表单提交走 Vue 的 on_submit
  3. 用户名 / 密码 input 上加了 v-model@blur,让 input 的值 ↔ Vue 的数据 自动双向同步以及input 失焦(blur)的时候 → 自动执行方法
    • v-model="username" / v-model="password"
    • @blur="check_username" / @blur="check_password"
  4. 错误提示用 v-show + [[ ... ]](和你 login.js 中的 delimiters: ['[[', ']]'] 对应)。
  5. rememberedv-model 勾选,后端拿到的仍然是 on/空,和你视图里的判断匹配。

三、static/js/login.js:修变量名、修方法名

现在的 login.js 有几个小坑:

  • data 里是 error_password,方法里用的是 this.error_pwd,对不上;
  • 方法名是 check_pwd,模板里用的是 check_password
  • on_submitwindow.event.returnValue = false,不太优雅,容易出锅。

static/js/login.js 改成下面这样

// static/js/login.js

var vm = new Vue({
    el: '#app',
    // 修改Vue变量的读取语法,避免和django模板语法冲突
    delimiters: ['[[', ']]'],
    data: {
        host: typeof host !== 'undefined' ? host : '',
        error_username: false,
        error_password: false,
        error_username_message: '请输入5-20个字符的用户名',
        error_password_message: '请输入8-20位的密码',
        username: '',
        password: '',
        remembered: true
    },
    methods: {
        // 检查账号
        check_username: function () {
            var re = /^[a-zA-Z0-9_-]{5,20}$/;
            if (re.test(this.username)) {
                this.error_username = false;
            } else {
                this.error_username_message = '用户名必须是5-20位字母、数字、下划线或 -';
                this.error_username = true;
            }
        },
        // 检查密码
        check_password: function () {
            var re = /^[0-9A-Za-z]{8,20}$/;
            if (re.test(this.password)) {
                this.error_password = false;
            } else {
                this.error_password_message = '密码必须是8-20位字母或数字';
                this.error_password = true;
            }
        },
        // 表单提交
        on_submit: function (event) {
            // 执行前端校验
            this.check_username();
            this.check_password();

            // 只要有一个错误,就阻止表单提交
            if (this.error_username || this.error_password) {
                event.preventDefault();
            }
            // 否则让表单正常提交,后端再做最终校验
        },
        // qq登录(暂时你后端没写可以先留着)
        qq_login: function () {
            var next = get_query_string('next') || '/';
            var url = this.host + '/qq/login/?next=' + next;
            axios.get(url, {
                responseType: 'json'
            })
            .then(response => {
                location.href = response.data.login_url;
            })
            .catch(error => {
                console.log(error.response);
            });
        }
    }
});

关键修复点 :

  1. 统一变量名:
    • error_password(data)
    • 模板:v-show="error_password"
    • JS:this.error_password = true/false
  2. 方法名统一:
    • 方法:check_password
    • 模板:@blur="check_password"
  3. on_submit 正常接收 event,通过 event.preventDefault() 阻止提交,而不是 window.event

其他完善以及自查:

/users/login/ 已在 apps/users/urls.py 注册:path('login/', LoginView.as_view(), name='login'),

可以调试并查看变量是否获取

美多商城项目-02

用户名 / 手机号登录

第 1 部分:构建校验脚本

apps/users/utils.py

import re
from django.contrib.auth.backends import ModelBackend
from apps.users.models import User


"""
抽取:根据用户名或手机号获取 User 对象
"""
def get_user_by_username(username):
    try:
        # 手机号
        if re.match(r'^1[3-9]\d{9}$', username):
            user = User.objects.get(mobile=username)
        else:
            # 用户名
            user = User.objects.get(username=username)
    except User.DoesNotExist:
        return None
    return user



"""
自定义认证后端:支持 用户名/手机号 登录
继承 ModelBackend,否则 Django admin 无法使用多字段认证
"""
class UsernameMobileModelBackend(ModelBackend):

    def authenticate(self, request, username=None, password=None, **kwargs):
        """
        username: 用户输入的用户名或手机号
        password: 密码
        """
        # 1. 获取用户对象(可能是用户名,也可能是手机号)
        user = get_user_by_username(username)
 
        # 2. 校验密码
        if user and user.check_password(password):
            return user

        return None

第 2 部分:settings.py 启用自定义认证后端

meiduo_mall/settings.py

AUTHENTICATION_BACKENDS = [
    'apps.users.utils.UsernameMobileModelBackend',
]

第 3 部分:优化 LoginView

LoginView.post()

修改前

if not re.match(r"^[a-zA-Z0-9_-]{5,20}$", username):
    return http.HttpResponseBadRequest("用户名格式不正确")

修改后(支持手机号)

# 用户名 或 手机号 格式校验
if not re.match(r'^[a-zA-Z0-9_-]{5,20}$', username) and \
   not re.match(r'^1[3-9]\d{9}$', username):
    return http.HttpResponseBadRequest("请输入正确的用户名或手机号")

其他全部保持不变,因为认证后端已经替你处理手机号登录。

第 4 部分:前端校验统一升级

static/js/login.js

修改 check_username()

check_username: function () {
    var re1 = /^[a-zA-Z0-9_-]{5,20}$/;  // 用户名
    var re2 = /^1[3-9]\d{9}$/;          // 手机号

    if (re1.test(this.username) || re2.test(this.username)) {
        this.error_username = false;
    } else {
        this.error_username = true;
        this.error_username_message = "请输入正确的用户名或手机号";
    }
}

templates/users/login.html

现在的 placeholder 是:

<input type="text"
       name="username"
       class="name_input"
       placeholder="请输入用户名"
       v-model="username"
       @blur="check_username">

修改为:

<input type="text"
       name="username"
       class="name_input"
       placeholder="请输入用户名或手机号"
       v-model="username"
       @blur="check_username">

测试登录:

美多商城项目-02
美多商城项目-02

发布者:LJH,转发请注明出处:https://www.ljh.cool/44251.html

Like (0)
LJH的头像LJH
Previous 2025年11月27日 下午3:03
Next 2024年12月15日 下午11:44

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注