构建Django项目
创建项目及环境

构建和完善创建 packages 和 directorys,如下图

快速配置并验证 jinjia2 模板
线路设计:
URL "/" ----> 路由 path("", index) ----> 执行 index() ---->
render("index.html") ----> 从 templates/ 读取 index.html ---->
Jinja2 渲染 ----> 输出到浏览器
修改模板引擎,设置模板路径,指定jinjia2环境
修改 meiduo_mall/setting.py

TEMPLATES = [
{
'BACKEND': 'django.template.backends.jinja2.Jinja2', # 使用 Jinja2
'DIRS': [os.path.join(BASE_DIR, 'templates')], # 全局模板目录
'APP_DIRS': True,
'OPTIONS': {
'environment': 'utils.jinja2.environment', # 指定 Jinja2 环境到 utils 目录
},
},
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [os.path.join(BASE_DIR, 'templates')],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
创建jinjia2 的环境配置放在 uitls 下:
内容包括:
- 创建 Jinja2 Environment
- 注册 static 和 url 以及 csrf全局函数
utlils/jinja2.py
from jinja2 import Environment
from django.contrib.staticfiles.storage import staticfiles_storage
from django.urls import reverse
from django.middleware.csrf import get_token
def environment(**options):
env = Environment(**options)
env.globals.update({
# 静态文件:{{ static('css/main.css') }}
"static": staticfiles_storage.url,
# URL 反向解析:{{ url('users:info') }}
"url": reverse,
# CSRF 隐藏 input:{{ csrf_field(request) | safe }}
"csrf_field": lambda request: (
f'<input type="hidden" name="csrfmiddlewaretoken" value="{get_token(request)}">'
),
})
return env
添加路由,这里快速添加index() 写在 urls.py,让 Django 能找到首页,无需添加 view.py
修改 meiduo_mall/urls.py
from django.contrib import admin
from django.urls import path
from django.shortcuts import render # 新增导入
# 新增函数,这里省略从 view.py 中导入
def index(request):
return render(request, "index.html", {"name": "TestName"})
urlpatterns = [
path("admin/", admin.site.urls),
path("", index, name="index"), # 新增路由
]
新建模板:templates/index.html,并快速验证 Jinja2 是否正常工作
templates/index.html
<h1>Hello Jinja2!</h1>
<p>My name is: {{ name }}</p>
项目根目录 meiduo_mall启动测试:
python manage.py runserver

配置 MySQL
这里快速配置,过程原理及细节可详见https://www.ljh.cool/43387.html 修改数据库引擎部分
预先在本地安装 MySQL,并创建数据库
mysql> CREATE DATABASE meiduo_mall CHARACTER SET UTF8mb4 COLLATE utf8mb4_unicode_ci;
Query OK, 1 row affected (0.00 sec)
安装 mysqlclient
pip install mysqlclient
修改 settings.py 配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', # 设置为 MySQL
'NAME': 'meiduo_mall', # 数据库名称
'USER': 'your_username', # 您的用户名
'PASSWORD': 'your_password', # 您的密码
'HOST': 'localhost', # 数据库主机
'PORT': '3306', # MySQL 的默认端口
}
}
执行迁移,让 Django 在 MySQL 中建表
python manage.py makemigrations # 生成迁移文件
python manage.py migrate # 更新数据库

启动项目验证,如果有 Django 系统表就说明已经成功启动
python manage.py runserver

配置 Redis
这里快速配置,原理及步骤可详见https://www.ljh.cool/43241.html
Mac 上安装 Redis,我这里是用的 Mac Homebrew,
brew install redis
brew services start redis
# 测试 Redis 是否工作:
redis-cli ping

在 Django 中使用 Redis
安装 django-redis
pip install django-redis
在 settings.py 中新增配置 Redis 缓存及 session
default 库 0 号用来做缓存,比如:
- 商品缓存
- 页面缓存
- 用户基本信息缓存
- API 缓存
session 库 1 号只用来存
- 用户登录状态
- session 变量
- 浏览器 sessionid 映射
# meiduo_mall/settings.py
# -------- Redis 缓存配置 --------
CACHES = {
"default": { # 0 号库
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/0",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"SERIALIZER": "django_redis.serializers.pickle.PickleSerializer",
}
},
"session": { # 1 号库(专门存 Session)
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
# -------- 指定 Session 使用 Redis session --------
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "session"
快速验证 Redis 是否已连接成功
在任何 view 中写个测试,例如在 meiduo_mall/urls.py 临时写一个测试 API:
from django.core.cache import cache
from django.http import HttpResponse
def test_redis(request):
cache.set("name", "zhangsan", 30) # 设置 30 秒缓存
value = cache.get("name")
return HttpResponse(f"Redis OK: {value}")
然后加路由:
path("test_redis/", test_redis),
访问:
http://127.0.0.1:8000/test_redis/

访问 Redis 验证缓存是否写入
运行 Redis CLI:
redis-cli

配置日志
在 settings.py 添加全局 LOGGING
实现效果:
# -------- 日志模块 ----------
LOG_DIR = os.path.join(BASE_DIR, "logs")
if not os.path.exists(LOG_DIR):
os.makedirs(LOG_DIR)
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
# 日志格式定义
"formatters": {
"standard": {
"format": "[%(asctime)s] [%(levelname)s] [%(name)s:%(lineno)d] %(message)s"
},
"simple": {
"format": "[%(levelname)s] %(message)s"
}
},
# Handlers:日志输出到哪里
"handlers": {
# 写入 debug/info/warning 日志
"default": {
"level": "INFO",
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": os.path.join(LOG_DIR, "django.log"),
"when": "midnight",
"backupCount": 10,
"encoding": "utf-8",
"formatter": "standard",
},
# 写入 error 日志
"error": {
"level": "ERROR",
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": os.path.join(LOG_DIR, "error.log"),
"when": "midnight",
"backupCount": 10,
"encoding": "utf-8",
"formatter": "standard",
},
# 控制台输出(开发环境)
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "simple"
},
},
# 具体 logger 设置
"loggers": {
# 默认 Django 日志
"django": {
"handlers": ["default", "console"],
"level": "INFO",
"propagate": True
},
# 专门记录错误
"django.request": {
"handlers": ["error"],
"level": "ERROR",
"propagate": False,
},
# 你的项目 logger
"meiduo": {
"handlers": ["default", "console"],
"level": "DEBUG",
"propagate": True
}
}
}
是用并验证日志生成
可以在任意 Django 模块里写(这里为了测试,可以直接添加到之前创建的 urls.py 的 index 函数中):
import logging
logger = logging.getLogger("meiduo")
def index(request):
logger.info("Index 页面被访问了")
logger.error("测试一下错误日志")
return render(request, "index.html")
日志会自动写入:
logs/django.log
logs/error.log(为空,可以构造错误,这里不去模拟构造了)

配置静态资源显示
下载静态资源
链接: https://pan.baidu.com/s/18_A1RFOOQi9SvaWO3jO8ZQ
提取码: hbnu
将静态资源下的内容复制到项目中的 static目录
后续如果希望能在 Jinja2 模板里这样使用
<link rel="stylesheet" href="{{ static('css/style.css') }}">
<img src="{{ static('images/logo.png') }}">
那么我们需要:
- 配置
STATIC_URL - 配置
STATICFILES_DIRS - 配置收集静态文件(collectstatic)
配置 settings.py
# ------------ Static Settings -------------
# 静态资源访问前缀,访问静态文件时使用http://127.0.0.1:8000/static/xxx
STATIC_URL = '/static/'
# 开发环境使用你的 static 目录,告诉 Django 静态文件放在哪里
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
]
# 部署时收集静态文件的目录,这个目录 Django collectstatic 会用(生产环境会用 nginx 指向 static_root 来加载静态文件,现在先不管也没关系,但写上比较规范。)
STATIC_ROOT = os.path.join(BASE_DIR, "static_root")
测试网页访问静态文件:

Jinja2 模板中如何引用 static?
我们已经在 utils/jinja2.py 注册了
env.globals.update({
'static': staticfiles_storage.url,
...
})
所以可以直接:
<link href="{{ static('css/base.css') }}" rel="stylesheet">
<script src="{{ static('js/jquery.min.js') }}"></script>
<img src="{{ static('images/logo.png') }}">
简单测试一个 img:
修改 templates/index.html
<h1>Hello Jinja2!</h1>
<p>My name is: {{ name }}</p>
<!--以下为添加内容-->
<img src="{{ static('images/adv01.jpg') }}" alt="">

创建用户模块应用
创建 users 子应用
mkdir -p apps/users && python manage.py startapp users apps/users

注册 users 应用(settings.py)
找到 INSTALLED_APPS,加入:
INSTALLED_APPS = [
...
'apps.users.apps.UsersConfig',
]
因为现在的项目不是users,而是apps/users/,所以还需要编辑 apps/users/apps.py,修改name = "apps.users"

准备 users 的 URL 路由结构,创建 apps/users/urls.py

from django.urls import path
from . import views
app_name = "users"
urlpatterns = [
path("info/", views.user_info, name="info"),
]
在项目 urls.py 中引入 users 路由
编辑 meiduo_mall/urls.py:
from django.urls import path, include # 引入 include
urlpatterns = [
path("admin/", admin.site.urls),
path("users/", include(("apps.users.urls", "users"), namespace="users")), # 新增
]
编写一个简单的 view 测试用户模块
编辑 apps/users/views.py:
from django.http import HttpResponse
def user_info(request):
return HttpResponse("用户中心 - 测试成功!")
可以在模板目录 templates下的 index.html 中配置下反向解析:
<a href="{{ url('users:info') }}">用户中心</a>
测试:

正式引入 register.html 作为 template(类视图写法)
复制 static目录中的register.html 到 templates/users/register.html,直接从 static 下拖拽到 templates 作为模板,这里需要修改 register.html 中所有../static/xxx 为
{{ static('css/reset.css') }}
{{ static('js/register.js') }}
{{ static('images/logo.png') }}
这样才能使静态文件变成了 Jinja2 standard 语法

修改后如下:
<!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" type="text/css" href="{{ static('css/reset.css') }}">
<link rel="stylesheet" type="text/css" href="{{ static('css/main.css') }}">
</head>
<body>
<div class="register_con">
<div class="l_con fl">
<a href="/" class="reg_logo"><img src="{{ static('images/logo.png') }}"></a>
<div class="reg_slogan">商品美 · 种类多 · 欢迎光临</div>
<div class="reg_banner"></div>
</div>
<div class="r_con fr">
<div class="reg_title clearfix">
<h1>用户注册</h1>
<a href="/users/login/">登录</a>
</div>
<div class="reg_form clearfix">
<form method="post" class="register_form">
<ul>
<li>
<label>用户名:</label>
<input type="text" name="username" id="user_name">
<span class="error_tip">请输入5-20个字符的用户</span>
</li>
<li>
<label>密码:</label>
<input type="password" name="password" id="pwd">
<span class="error_tip">请输入8-20位的密码</span>
</li>
<li>
<label>确认密码:</label>
<input type="password" name="password2" id="cpwd">
<span class="error_tip">两次输入的密码不一致</span>
</li>
<li>
<label>手机号:</label>
<input type="text" name="mobile" id="phone">
<span class="error_tip">请输入正确的手机号码</span>
</li>
<li>
<label>图形验证码:</label>
<input type="text" name="pic_code" id="pic_code" class="msg_input">
<img src="{{ static('images/pic_code.jpg') }}" alt="图形验证码" class="pic_code">
<span class="error_tip">请填写图形验证码</span>
</li>
<li>
<label>短信验证码:</label>
<input type="text" name="sms_code" id="msg_code" class="msg_input">
<a href="javascript:;" class="get_msg_code">获取短信验证码</a>
<span class="error_tip">请填写短信验证码</span>
</li>
<li class="agreement">
<input type="checkbox" name="allow" id="allow" checked="checked">
<label>同意”美多商城用户使用协议“</label>
<span class="error_tip">请勾选用户协议</span>
</li>
<li class="reg_sub">
<input type="submit" value="注 册">
</li>
</ul>
</form>
</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>
<script type="text/javascript" src="{{ static('js/host.js') }}"></script>
<script type="text/javascript" src="{{ static('js/common.js') }}"></script>
<script type="text/javascript" src="{{ static('js/register.js') }}"></script>
</body>
</html>
修改view 函数
apps/users/views.py
from django.shortcuts import render
from django.http import HttpResponse
from django.views import View
# Create your views here.
# def user_info(request):
# return HttpResponse("用户中心 - 测试成功!")
class RegisterView(View):
"""
用户注册视图
GET:返回注册页面
POST:接收表单提交注册用户(后面会写)
"""
def get(self, request):
# GET 请求 —— 渲染注册页面
return render(request, "users/register.html")
绑定 URL
apps/users/urls.py:
from django.urls import path
from . import views
from .views import RegisterView
app_name = "users"
urlpatterns = [
# path("info/", views.user_info, name="info"),
path("register/", RegisterView.as_view(), name="register"),
]
访问测试 http://127.0.0.1:8000/users/register/

用户模型创建及迁移
使用扩展 Django 自带 User 模型(AbstractUser)
- 支持手机号登录 / 邮箱验证 / 唯一性校验
- 兼容 Django auth 系统(登录、认证、中间件全部可用)
在 apps/users/models.py 中写 User 模型
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
"""
自定义用户模型(继承 Django 内置的 AbstractUser)
AbstractUser 已经包含:
- username(用户名)
- password(密码:加密后的)
- email(邮箱)
- first_name / last_name(姓名)
- is_active / is_staff / is_superuser(用户权限)
- last_login / date_joined(登录时间)
我们在这个基础上扩展自己的字段,例如手机号。
"""
# 新增手机号字段(电商项目必备)
mobile = models.CharField(
max_length=11, # 长度为 11 位
unique=True, # 手机号唯一
verbose_name="手机号" # Django admin 后台显示名称
)
class Meta:
# 指定数据库表名(不使用默认的 users_user)
db_table = "tb_users"
# 后台显示名称(单数 / 复数)
verbose_name = "用户"
verbose_name_plural = verbose_name
def __str__(self):
"""
返回用户对象的字符串形式。
在 admin 后台、shell 调试、日志打印时显示用户名,而不是 "User object(1)"。
"""
return self.username
修改 settings.py,告诉 Django 使用自定义 User,用 users 应用下的 User 类替代 Django 默认的 auth.User
添加:
# AUTH_USER_MODEL = <app_label>.<model_class_name>
AUTH_USER_MODEL = "users.User"
一旦设置了 AUTH_USER_MODEL,项目创建数据库后 不能随意更改,否则 migration 会爆炸,所以必须现在就设置。
迁移数据库
清空整个迁移历史 + 删除数据库重来
删除 apps/users/migrations/0001_initial.py
drop database meiduo_mall;
CREATE DATABASE meiduo_mall CHARACTER SET UTF8mb4 COLLATE utf8mb4_unicode_ci;
我们执行迁移,让模型入库:
python manage.py makemigrations
python manage.py migrate
这会自动创建表:tb_users
字段包括:
- id(默认 BigAutoField)
- username
- password(加密后的)
- mobile
- is_superuser
- is_active
- is_staff
- last_login
- date_joined
- email_active
等 Django 自带字段。

Vue 绑定
目前的 register.html 还是一个完全静态的,无法交互。想要迁移成支持 Vue 输入校验码+图形验证码+短信验证码+注册提交的版本,我们还需要按照下面步骤来进行迁移
| 步骤 | 内容 | 原因 |
|---|---|---|
| 1 | 给模板加 id="app" | 让 Vue 能控制页面 |
| 2 | 导入 Vue & Axios | Vue 逻辑必须依赖它们 |
| 3 | 添加 delimiters | 避免 Jinja2 与 Vue 冲突 |
| 4 | 替换 input 为 v-model、@blur | 让 Vue 接管表单 |
| 5 | 修改图形验证码 img → :src | 让验证码动态刷新 |
| 6 | 添加 @submit="on_submit" | 让 Vue 阻止不合法提交 |
步骤 1:给外层加 Vue 容器,外层加 id="app"
首先我们发现,目前已经有 register.js 已经写好并在 template/users/register.html 中导入了

register.js 代码内容如下,主要功能包含了:
| 功能 | register.js 对应部分 |
|---|---|
| 校验用户名 | check_username() |
| 校验密码 | check_password() |
| 确认密码 | |
| 校验手机号 | check_mobile() |
| 请求图片验证码 | generate_image_code() |
| 发送短信验证码 | send_sms_code() |
| 最终提交校验 | on_submit() |
// 创建 Vue 实例,管理整个注册页面
var vm = new Vue({
el: '#app', // Vue 绑定页面上 id="app" 的元素
// 修改 Vue 的插值语法,避免和 Django/Jinja2 的 {{ }} 冲突
delimiters: ['[[', ']]'],
// ===== 页面数据 =====
data: {
host: host, // 静态 host.js 定义的 API 域名
// 错误标识
error_name: false,
error_password: false,
error_password2: false,
error_check_password: false,
error_mobile: false,
error_image_code: false,
error_sms_code: false,
error_allow: false,
// 错误提示内容
error_name_message: '请输入5-20个字符的用户',
error_password_message: '请输入8-20位的密码',
error_password2_message: '两次输入的密码不一致',
error_mobile_message: '请输入正确的手机号码',
error_image_code_message: '请填写图形验证码',
error_sms_code_message: '请填写短信验证码',
error_allow_message: '请勾选用户协议',
// 验证码相关
image_code_id: '',
image_code_url: '',
sms_code_tip: '获取短信验证码',
sending_flag: false, // 是否正在发送短信验证码(防止重复点击)
// 表单字段
username: '',
password: '',
password2: '',
mobile: '',
image_code: '',
sms_code: '',
allow: true
},
// Vue 生命周期:页面加载完自动执行
mounted: function () {
this.generate_image_code(); // 自动加载验证码图片
},
// ===== 方法区 =====
methods: {
// 生成唯一编号(UUID)——用于验证码 ID
generateUUID: function () {
var d = new Date().getTime();
if (window.performance && typeof window.performance.now === "function") {
d += performance.now();
}
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
},
// 生成验证码 URL
generate_image_code: function () {
this.image_code_id = this.generateUUID(); // 新验证码 ID
// 临时禁用验证码请求,使用本地图片代替
this.image_code_url = "/static/images/pic_code.jpg";
// this.image_code_url = this.host + "/image_codes/" + this.image_code_id + "/";
console.log(this.image_code_url);
},
// 校验用户名格式
check_username: function () {
var re = /^[a-zA-Z0-9_-]{5,20}$/;
if (re.test(this.username)) {
this.error_name = false;
} else {
this.error_name_message = '请输入5-20个字符的用户名';
this.error_name = true;
}
// ---------- 远程检查用户名是否重复 ----------
var url = this.host + "/usernames/" + this.username + "/count/";
axios.get(url, { responseType: "json" })
.then(response => {
if (response.data.count > 0) {
this.error_name_message = "用户名已存在";
this.error_name = true;
} else {
this.error_name = false;
}
})
.catch(error => {
console.log(error);
this.error_name_message = "服务器异常";
this.error_name = 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 = true;
}
},
// 校验重复密码
check_password2: function () {
if (this.password != this.password2) {
this.error_check_password = true;
} else {
this.error_check_password = false;
}
},
// 校验手机号格式
check_mobile: function () {
var re = /^1[345789]\d{9}$/;
if (re.test(this.mobile)) {
this.error_mobile = false;
} else {
this.error_mobile_message = '您输入的手机号格式不正确';
this.error_mobile = true;
}
},
// 校验图片验证码是否为空
check_image_code: function () {
if (!this.image_code) {
this.error_image_code_message = '请填写图片验证码';
this.error_image_code = true;
} else {
this.error_image_code = false;
}
},
// 校验短信验证码是否为空
check_sms_code: function () {
if (!this.sms_code) {
this.error_sms_code_message = '请填写短信验证码';
this.error_sms_code = true;
} else {
this.error_sms_code = false;
}
},
// 是否同意用户协议
check_allow: function () {
if (!this.allow) {
this.error_allow = true;
} else {
this.error_allow = false;
}
},
// 点击按钮发送短信验证码
send_sms_code: function () {
if (this.sending_flag == true) return;
this.sending_flag = true;
// 输入校验
this.check_mobile();
this.check_image_code();
if (this.error_mobile || this.error_image_code) {
this.sending_flag = false;
return;
}
// 构造短信验证码 API URL
var url = this.host + '/sms_codes/' + this.mobile +
'/?image_code=' + this.image_code +
'&image_code_id=' + this.image_code_id;
// 发送请求
axios.get(url, {
responseType: 'json'
})
.then(response => {
// 成功
if (response.data.code == '0') {
var num = 60;
var t = setInterval(() => {
if (num == 1) {
clearInterval(t);
this.sms_code_tip = '获取短信验证码';
this.sending_flag = false;
} else {
num -= 1;
this.sms_code_tip = num + '秒';
}
}, 1000);
}
// 后端错误
else {
if (response.data.code == '4001') {
this.error_image_code_message = response.data.errmsg;
this.error_image_code = true;
} else {
this.error_sms_code_message = response.data.errmsg;
this.error_sms_code = true;
}
this.generate_image_code();
this.sending_flag = false;
}
})
.catch(error => {
console.log(error.response);
this.sending_flag = false;
});
},
// 表单提交前最终校验
on_submit(){
this.check_username();
this.check_password();
this.check_password2();
this.check_mobile();
this.check_allow();
if (
this.error_name || this.error_password || this.error_check_password ||
this.error_mobile || this.error_sms_code || this.error_allow
) {
// 阻止表单提交
window.event.returnValue = false;
}
}
}
});
原来的:
<div class="register_con">
要改成:
<div id="app">
<div class="register_con">
并在下面补上一个 </div> 来闭合 Vue 实例,因为 Vue 必须挂载元素(el: '#app')
步骤 2:导入 Vue 与 axios,head 标签内新增 Vue + axios

加在 head 标签中:
<script src="{{ static('js/vue-2.5.16.js') }}"></script>
<script src="{{ static('js/axios-0.18.0.min.js') }}"></script>
否则 register.js 会报错:
Vue is not defined
axios is not defined
步骤 3:在 register.js 中启用 delimiters
你现在的 register.js 已经写了:
delimiters: ['[[', ']]']

所以模板中要配合使用:
[[ error_name_message ]]
步骤 4:把 input 改成 Vue v-model + @blur 绑定
Vue 需要拿到你输入的内容,才能实时校验、显示错误提示
- v-model 让输入框的值跟 Vue 数据实时同步,它用来“接收用户输入”。
- @blur 绑定一个“失去焦点事件”,它用来“触发验证逻辑”。
原始写法
<input type="text" name="username" id="user_name">
需要改为:
<input type="text" name="username" id="user_name"
v-model="username"
@blur="check_username">
<span class="error_tip" v-show="error_name">[[ error_name_message ]]</span>
具体需要改下列 input
| 输入框 | 必须加的指令 |
|---|---|
| 用户名 | v-model + @blur |
| 密码 | v-model + @blur |
| 确认密码 | v-model + @blur |
| 手机号 | v-model + @blur |
| 图形验证码 | v-model |
| 短信验证码 | v-model + @blur |
| 协议 checkbox | v-model |
步骤 5:图形验证码必须改成 Vue 动态 src
Vue 在 mounted 阶段会请求后端动态验证码
不改的话永远显示静态 pic_code.jpg(就是假验证码)
<img src="{{ static('images/pic_code.jpg') }}">
须改成(虽然 js 中目前也是临时引用静态资源,这个之后会重点讲解):
<img :src="image_code_url" @click="generate_image_code">
步骤 6:修改“获取短信验证码”按钮
Vue 负责计时、调用短信接口,不加 @click,点击不会发送短信验证码
旧写法(无效)
<a href="javascript:;" class="get_msg_code">获取短信验证码</a>
需要改:
<a @click="send_sms_code" class="get_msg_code">[[ sms_code_tip ]]</a>
步骤 7:为 form 添加 @submit="on_submit" 和 csrf,改成 Vue 控制
Vue 会阻止不合法提交,Django 需要 CSRF token
原来:
<form method="post" class="register_form">
需要改:
<form method="post" class="register_form" @submit="on_submit">
{{ csrf_field(request) | safe }}
终版 templates/users/register.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>
<!-- CSS -->
<link rel="stylesheet" type="text/css" href="{{ static('css/reset.css') }}">
<link rel="stylesheet" type="text/css" href="{{ static('css/main.css') }}">
<!-- Vue + Axios -->
<script type="text/javascript" src="{{ static('js/vue-2.5.16.js') }}"></script>
<script type="text/javascript" src="{{ static('js/axios-0.18.0.min.js') }}"></script>
</head>
<body>
<div id="app">
<div class="register_con">
<div class="l_con fl">
<a href="/" class="reg_logo">
<img src="{{ static('images/logo.png') }}">
</a>
<div class="reg_slogan">商品美 · 种类多 · 欢迎光临</div>
<div class="reg_banner"></div>
</div>
<div class="r_con fr">
<div class="reg_title clearfix">
<h1>用户注册</h1>
<a href="/users/login/">登录</a>
</div>
<div class="reg_form clearfix">
<form method="post" class="register_form" @submit="on_submit">
{{ csrf_field(request) | safe }}
<ul>
<!-- 用户名 -->
<li>
<label>用户名:</label>
<input type="text" name="username" id="user_name"
v-model="username" @blur="check_username">
<span class="error_tip" v-show="error_name">
[[ error_name_message ]]
</span>
</li>
<!-- 密码 -->
<li>
<label>密码:</label>
<input type="password" name="password" id="pwd"
v-model="password" @blur="check_password">
<span class="error_tip" v-show="error_password">
[[ error_password_message ]]
</span>
</li>
<!-- 确认密码 -->
<li>
<label>确认密码:</label>
<input type="password" name="password2" id="cpwd"
v-model="password2" @blur="check_password2">
<span class="error_tip" v-show="error_check_password">
[[ error_password2_message ]]
</span>
</li>
<!-- 手机号 -->
<li>
<label>手机号:</label>
<input type="text" name="mobile" id="phone"
v-model="mobile" @blur="check_mobile">
<span class="error_tip" v-show="error_mobile">
[[ error_mobile_message ]]
</span>
</li>
<!-- 图形验证码 -->
<li>
<label>图形验证码:</label>
<input type="text" name="pic_code" id="pic_code"
class="msg_input"
v-model="image_code" @blur="check_image_code">
<img :src="image_code_url"
@click="generate_image_code"
alt="图形验证码"
class="pic_code">
<span class="error_tip" v-show="error_image_code">
[[ error_image_code_message ]]
</span>
</li>
<!-- 短信验证码 -->
<li>
<label>短信验证码:</label>
<input type="text" name="sms_code" id="msg_code"
class="msg_input"
v-model="sms_code" @blur="check_sms_code">
<a class="get_msg_code" @click="send_sms_code">
[[ sms_code_tip ]]
</a>
<span class="error_tip" v-show="error_sms_code">
[[ error_sms_code_message ]]
</span>
</li>
<!-- 协议 -->
<li class="agreement">
<input type="checkbox"
id="allow"
name="allow"
value="on"
v-model="allow"
@click="check_allow">
<label>同意”美多商城用户使用协议“</label>
<span class="error_tip" v-show="error_allow">
[[ error_allow_message ]]
</span>
</li>
<!-- 提交按钮 -->
<li class="reg_sub">
<input type="submit" value="注 册">
</li>
</ul>
</form>
</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>
<!-- 原有 JS -->
<script type="text/javascript" src="{{ static('js/host.js') }}"></script>
<script type="text/javascript" src="{{ static('js/common.js') }}"></script>
<!-- 你自己的 register.js(Vue 逻辑) -->
<script type="text/javascript" src="{{ static('js/register.js') }}"></script>
</body>
</html>
注册界面后端数据初步完善并验证入库
完善 apps/users/view.py
import logging
import re
from django import http
from django.contrib.auth import login, authenticate, logout
from django.shortcuts import render, redirect
from django.urls import reverse
from django.views import View
from apps.users.models import User
logger = logging.getLogger("django")
# ============================================================
# 用户注册
# ============================================================
class RegisterView(View):
"""
用户注册视图(前端表单提交到这里)
前端 POST 提交:username、password、password2、mobile、allow、sms_code
这里的版本暂不做短信验证码校验,可后续补充。
"""
def get(self, request):
"""渲染注册页面"""
return render(request, "users/register.html")
def post(self, request):
"""
注册流程:
1. 接收表单参数
2. 校验格式
3. 校验是否重复(用户名 + 手机号)
4. 创建用户(自动加密密码)
5. 自动登录
6. 跳转首页
"""
# 1. 接收参数
username = request.POST.get("username")
password = request.POST.get("password")
password2 = request.POST.get("password2")
mobile = request.POST.get("mobile")
allow = request.POST.get("allow")
# 2. 必填检查
if not all([username, password, password2, mobile]):
return http.HttpResponseBadRequest("缺少必要参数")
# 校验协议
if allow != "on":
return http.HttpResponseBadRequest("请勾选同意协议")
# 3. 用户名格式 5-20 位
if not re.match(r"^[0-9a-zA-Z_]{5,20}$", username):
return http.HttpResponseBadRequest("用户名格式不正确")
# 4. 密码格式 8-20 位
if not re.match(r"^[0-9A-Za-z_]{8,20}$", password):
return http.HttpResponseBadRequest("密码格式不正确")
# 两次密码一致
if password != password2:
return http.HttpResponseBadRequest("两次密码不一致")
# 5. 手机号格式
if not re.match(r"^1[3-9]\d{9}$", mobile):
return http.HttpResponseBadRequest("手机号格式不正确")
# 6. 用户名是否重复
if User.objects.filter(username=username).exists():
return http.HttpResponseBadRequest("用户名已存在")
# 7. 手机号是否重复
if User.objects.filter(mobile=mobile).exists():
return http.HttpResponseBadRequest("手机号已被注册")
# 8. 入库:create_user 会自动把密码加密
try:
user = User.objects.create_user(
username=username,
password=password,
mobile=mobile
)
except Exception as e:
logger.error("数据库异常:%s", e)
return http.HttpResponseBadRequest("数据库写入异常")
# 9. 自动登录
login(request, user)
# 10. 跳首页
return redirect("/")
# ============================================================
# 用户名重复检查(AJAX)
# /usernames/<username>/count/
# ============================================================
class UsernameCountView(View):
"""判断用户名是否重复(前端失去焦点 AJAX 调用)"""
def get(self, request, username):
try:
count = User.objects.filter(username=username).count()
except Exception as e:
logger.error("数据库查询异常:%s", e)
return http.JsonResponse({"code": 400, "errmsg": "数据库异常"})
return http.JsonResponse({"code": 0, "count": count})
# ============================================================
# 用户登录
# ============================================================
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. 密码无需正则,可自由(但也可加)
# 5. Django 内置认证:用户名 + 密码
user = authenticate(username=username, password=password)
if user is None:
return render(request, "users/login.html", {"account_errmsg": "用户名或密码错误"})
# 6. 登录成功:写 session
login(request, user)
# 7. 设置 session 过期时间
if remembered == "on":
request.session.set_expiry(30 * 24 * 3600) # 30 天
else:
request.session.set_expiry(0) # 浏览器关闭即失效
# 8. 设置 cookie,用于首页展示用户名
resp = redirect("/")
resp.set_cookie("username", user.username, max_age=14 * 24 * 3600)
return resp
# ============================================================
# 用户退出
# ============================================================
class LogoutView(View):
"""退出登录"""
def get(self, request):
# 清除 session
logout(request)
# 删除 cookie
resp = redirect("/")
resp.delete_cookie("username")
return resp
# ============================================================
# 用户中心(登录后可访问)
# ============================================================
from django.contrib.auth.mixins import LoginRequiredMixin
class UserCenterInfoView(LoginRequiredMixin, View):
"""
用户中心(必须登录才能访问)
LoginRequiredMixin 默认检查:
- user.is_authenticated
- 未登录自动跳转到 settings.LOGIN_URL
"""
def get(self, request):
return render(request, "users/user_center_info.html")
view.py 代码详解
Django 注册逻辑本质上就是:
- 获取表单输入
- 校验输入格式
- 校验是否重复(用户名、手机号)
- create_user() 创建用户
- login() 自动登录
- 返回首页
但内部很多机制是 Django 自动处理的
详解 post() 的每一步
1. 获取参数
username = request.POST.get("username")
password = request.POST.get("password")
password2 = request.POST.get("password2")
mobile = request.POST.get("mobile")
allow = request.POST.get("allow")
form 标签提交的数据,Django 会自动解析成 key-value
2. 必填项检查
if not all([username, password, password2, mobile]):
all([…]) 只要列表里有一个是空/None,就返回 False
3. 协议是否勾选
if allow != "on":
为什么是 "on"?
HTML checkbox 的特性,勾选后提交,allow=on,没选的时候,浏览器不会发送 allow 字段,所以 allow != "on" 就是“没勾选”
<input type="checkbox" name="allow" value="on">
4. 校验用户名格式、密码格式、两次密码一致性、手机号格式
if not re.match(r"^[0-9a-zA-Z_]{5,20}$", username): # 校验用户名格式
if not re.match(r"^[0-9A-Za-z_]{8,20}$", password): # 校验密码格式
if password != password2: # 校验两次密码一致性
if not re.match(r"^1[3-9]\d{9}$", mobile): # 校验手机号格式
5、从后端校验用户名是否已存在、手机号是否已注册
if User.objects.filter(username=username).exists():
if User.objects.filter(mobile=mobile).exists(): # 同上
校验过程:
- ORM 生成 SQL:SELECT 1 FROM tb_users WHERE username='xxx' LIMIT 1
- exists() 是最快的检查方式(不取整行,只看是否有记录)
6、入库:最关键一步
user = User.objects.create_user(
username=username,
password=password,
mobile=mobile
)
Django 内置 User 模型提供了方法:create_user() —— 创建普通用户(create_user(username, password, mobile))
这个方法帮我们:
- (1) 创建一个 User 对象
- (2) 调用 set_password() 加密密码(随机生成 salt 、使用 PBKDF2 Sha256 加密(默认算法)、写入到 user.password 字段)
Django 内部代码 create_user() 内部流程详解(理解即可)
def create_user(self, username, password=None, **extra_fields):
user = self.model(username=username, **extra_fields) # 创建一个 User 对象
user.set_password(password) # 调用 set_password() 加密密码
user.save(using=self._db) # 保存到数据库
return user
7、注册后自动登录(等会详细讲):
login(request, user)
login() 是 Django 的认证系统函数,它做两件事:
- 将用户 ID 写入 session:request.session['_auth_user_id'] = user.id
- 设置 sessionid cookie:浏览器保存一个 sessionid,用于判断你已登录。
确保注册完成后,无需再次输入密码,即已登录状态。
8、返回首页(等会完善,详细讲)
return redirect("/")
测试入库

查看代码步入过程:
查看入库结果

tips:这一步骤跨度较大,可以在函数入口处打断点进行debug调试,如下

跳转content首页(刚才的步骤 8)
进入到 apps 目录下,
mkdir apps/contents
python manage.py startapp contents apps/contents
apps/contents/views.py
from django.shortcuts import render
from django.views import View
import logging
logger = logging.getLogger("meiduo")
class IndexView(View):
"""
网站首页视图
后续可以扩展:轮播图、分类菜单、广告位等
"""
def get(self, request):
logger.info("访问首页")
# 如果你未来要传数据给首页,可以在 context 里加
context = {
"title": "美多商城 - 首页",
}
# 模板路径:templates/index.html(你已有)
return render(request, "index.html", context)
apps/contents/urls.py
from django.urls import path
from .views import IndexView
app_name = "contents"
urlpatterns = [
path("", IndexView.as_view(), name="index"),
]
这里定义了首页 / 路由。
须在主路由(meiduo_mall/urls.py)里 include
urlpatterns = [
# path("", index, name="index"),
# path("test_redis/", test_redis),
path("admin/", admin.site.urls),
# 首页内容模块 (新增 contents)
path("", include(("apps.contents.urls", "contents"), namespace="contents")),
# 用户模块
path("users/", include(("apps.users.urls", "users"), namespace="users")),
]
访问主页:http://www.meiduo.site:8000/

优化注册跳转至首页:
如果未来首页 URL 改为 /index/、/home/,所有 redirect("/") 都要改,大型项目中不建议这样写
所以 apps/users/views.py 还需要修改的地方有:
redirect("/") -修改为->redirect(reverse('contents:index'))
注册后登陆状态保持(刚才的步骤 7)
在 apps/user/view.py 中仅仅只有一行代码表示注册后自动登录:
login(request, user)
登录的本质 = Django 给浏览器下发一个 SessionID Cookie,
之后所有请求都带着这个 Cookie → Django 根据 sessionid → 找到 session 数据 → 找到 user_id → 自动生成 request.user
login(request, user) 是 Django 自带的内置登录功能,来自 Django 官方认证系统(django.contrib.auth),是 Django 框架核心功能,你直接调用即可
导入的是:
from django.contrib.auth import login
login(request, user) 等价于做了这些动作:
① 向 session 中写入 user_id
request.session["_auth_user_id"] = user.pk
② 设置认证 backend(定位这个用户是通过哪个认证后端登录的,如果不存 backend,下次请求 Django 不知道要用哪个 backend 来解析用户。)
request.session["_auth_user_backend"] = "django.contrib.auth.backends.ModelBackend"
③ 标记 request.user 为当前登录用户
登录后你就可以使用:
request.user.username
request.user.is_authenticated
所以 login() 自动完成了一套完整登录流程:
- Cookie 设置
- request.user 更新
- 认证 backend 绑定
- 下次请求保持登录状态
无需手写任何代码
所以我们直接在 redis 中查看 session 就好了
注册,然后查看 session


判断是否与已有用户名冲突:
效果如下:失去焦点后,自动判断已有用户名是否存在

用户名重复检查(前端 + 后端交互)流程图
┌────────────────────────┐
│ 用户在输入框输入 │
│ username │
└──────────────┬─────────┘
│(失去焦点 onblur)
▼
┌────────────────────────┐
│ 前端 Vue 调用方法 │
│ check_username() │
│ 1. 校验格式 │
│ 2. 拼接接口 URL │
│ /users/usernames/<u>/count/
│ 3. axios 发起 GET 请求 │
└──────────────┬─────────┘
│
▼
┌────────────────────────┐
│ 后端 Django │
│ UsernameCountView.get() │
│ 1. 获取 URL 中的 username│
│ 2. 查询数据库: │
│ User.objects.filter │
│ (username=u) │
│ .count() │
│ 3. 返回 JSON: │
│ {count: 0 或 1} │
└──────────────┬─────────┘
│
▼
┌────────────────────────┐
│ 前端接收响应 │
│ if count > 0: │
│ 显示“用户名已存在” │
│ else: │
│ 错误消息隐藏 │
│ 允许提交 │
└──────────────┬─────────┘
│
▼
┌────────────────────────┐
│ 用户继续填写表单 │
│ or 点击“注册”提交 │
└────────────────────────┘
前端优化:
static/js/register.js 这段代码需要优化check_username功能:
// 校验用户名格式 + 是否重复
check_username: function () {
// ① 如果为空,直接报错,不发请求
if (!this.username) {
this.error_name_message = '请输入用户名';
this.error_name = true;
return; // ← 必须 return
}
// ② 检查格式
var re = /^[a-zA-Z0-9_-]{5,20}$/;
if (!re.test(this.username)) {
this.error_name_message = '请输入5-20个字符的用户名';
this.error_name = true;
return; // ← 格式不对也不能发请求
}
// ③ 格式正确,先取消错误
this.error_name = false;
// ④ 再发送 AJAX 请求检查是否重复
var url = this.host + "/users/usernames/" + this.username + "/count/";
axios.get(url, { responseType: "json" })
.then(response => {
if (response.data.count > 0) {
this.error_name_message = "用户名已存在";
this.error_name = true;
} else {
this.error_name = false;
}
})
.catch(error => {
console.log(error);
this.error_name_message = "服务器异常";
this.error_name = true;
});
},
后端接口:
apps/users/views.py
# ============================================================
# 用户名重复检查(AJAX)
# /usernames/<username>/count/
# ============================================================
class UsernameCountView(View):
"""判断用户名是否重复(前端失去焦点 AJAX 调用)"""
def get(self, request, username):
try:
count = User.objects.filter(username=username).count()
except Exception as e:
logger.error("数据库查询异常:%s", e)
return http.JsonResponse({"code": 400, "errmsg": "数据库异常"})
return http.JsonResponse({"code": 0, "count": count})
新增路由:
apps/users/urls.py
urlpatterns = [
# 用户名重复检查(AJAX)
path('usernames/<username>/count/', UsernameCountView.as_view(), name='username_count'),
]
前后端调试技巧
前端断点技巧

核查拼接结果:

核查后端返回结果:

后端可以选择开启 debug 模式使用已有用户进行断点测试:


解析:
前端这段代码:
var url = this.host + "/users/usernames/" + this.username + "/count/";
假如
host = "http://www.meiduo.site:8000"
username = "user001"
最终生成的 URL:http://www.meiduo.site:8000//users/usernames/user001/count/
后端 URLConf 里
apps/users/urls.py:
path('usernames/<username>/count/', UsernameCountView.as_view(), name='username_count'),
/usernames/任意用户名/count/ 都会走到 UsernameCountView.get()
如何自己测试?
直接在浏览器访问:http://www.meiduo.site:8000/users/usernames/user001/count/ 会看到 JSON:如果用户存在,显示:{"code": 0, "count": 1}

实现动态图片验证码
验证码功能整体流程
验证码功能有 4 个核心步骤:
- 前端生成 UUID(每次点击图片都会生成新验证码 ID)
- 前端访问后端 URL:/users/image_codes/<uuid>/ → 后端生成验证码图片
- 后端把验证码文字存入 Redis:img_uuid = "验证码文本"
- 前端提交表单时带上 image_code(用户输入) 和 image_code_id(UUID) → 后端验证是否一致
流程图:
【前端】生成 UUID image_code_id
↓
浏览器加载验证码图片 GET /image_codes/UUID/
↓
【后端】生成验证码文本 ABCD 和图片
↓
写入 Redis: img_UUID = "ABCD"
↓
返回图片到前端显示
↓
用户填写验证码(例如 CDAB)
↓
表单提交 POST /register/
携带:
pic_code = CDAB
image_code_id = UUID
↓
【后端】从 Redis 获取真实验证码 ABCD
↓
比对 CDAB == ABCD ?
↓
一致 → 通过
不一致 → 返回错误
后端部分
将 captcha 导入到 libs 中,并安装 pillow:pip install pillow
链接: https://pan.baidu.com/s/1_tvYsq-Lnnj9w4gzUTqw4w
提取码: b3fk

① 生成验证码图片(ImageCodeView)
- 调用
captcha.generate_captcha()生成四位验证码与图片二进制流 - 把验证码文本写入 Redis:
img_<uuid> = TEXT - 返回图片(JPEG 二进制)
apps/users/views.py
# ============================================================
# 图片验证码
# ============================================================
class ImageCodeView(View):
"""
获取图形验证码
URL: /users/image_codes/<uuid>/
"""
def get(self, request, uuid):
# 生成验证码 (name, text, image)
name, text, image_data = captcha.generate_captcha()
# 存 Redis
redis_conn = get_redis_connection("verify_codes")
redis_conn.setex(f"img_{uuid}", 300, text) # 有效期5分钟
# 返回图片
return HttpResponse(image_data, content_type="image/jpeg")
② 注册时验证验证码(在RegisterView中)
RegisterView中新增(入库代码之前)
# 图形验证码校验
image_code = request.POST.get("pic_code")
image_code_id = request.POST.get("image_code_id") # 你需要在前端传入此字段
redis_conn = get_redis_connection("verify_codes")
real_image_code = redis_conn.get(f"img_{image_code_id}")
if real_image_code is None:
return http.HttpResponseBadRequest("验证码已过期")
if real_image_code.decode().lower() != image_code.lower():
return http.HttpResponseBadRequest("验证码错误")
③ URL 绑定
apps/users/urls.py
from apps.users.views import ImageCodeView
urlpatterns = [
# ===== 图形验证码 =====
path("image_codes/<uuid:uuid>/", ImageCodeView.as_view(), name="image_code"),
]
前端部分
Vue 生命周期结尾:
// Vue 生命周期:页面加载完自动执行
mounted: function () {
this.generate_image_code(); // 自动加载验证码图片
},
methods 方法区中添加:
① 生成 UUID(前端唯一验证码 ID)
// 生成唯一编号(UUID)——用于验证码 ID
generateUUID: function () {
var d = new Date().getTime();
if (window.performance && typeof window.performance.now === "function") {
d += performance.now();
}
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
},
② 每次生成验证码图片 URL
改变图片 URL → 浏览器自动刷新验证码。
// 生成验证码 URL
generate_image_code: function () {
// 1. 每次生成新的 UUID
this.image_code_id = this.generateUUID();
// 2. 重新生成验证码 URL
this.image_code_url = this.host + "/users/image_codes/" + this.image_code_id + "/";
console.log("验证码URL:", this.image_code_url);
},
③ 用户输入验证码时校验
// 校验图片验证码是否为空(后续发送短信验证码功能,会引用此处)
check_image_code: function () {
if (!this.image_code) {
this.error_image_code_message = '请填写图片验证码';
this.error_image_code = true;
} else {
this.error_image_code = false;
}
},
④ register.html 绑定验证码图片
目前的templates/users/register.html有一段需要新增<input type="hidden" name="image_code_id" :value="image_code_id">,如下:
<!-- 图形验证码 -->
<li>
<label>图形验证码:</label>
<input type="text" name="pic_code" id="pic_code"
class="msg_input"
v-model="image_code" @blur="check_image_code">
<img :src="image_code_url"
@click="generate_image_code"
alt="图形验证码"
class="pic_code">
<input type="hidden" name="image_code_id" :value="image_code_id">
<span class="error_tip" v-show="error_image_code">
[[ error_image_code_message ]]
</span>
</li>
<img :src="image_code_url"> —— 展示验证码图片
- Vue 初始化时执行
generate_image_code() generate_image_code()生成 UUID → 拼接访问 URL, 最终生成的 URL:http://xxxx/users/image_codes/<uuid>/
输入框 <input v-model="image_code"> —— 接收用户输入
这一行负责收集用户输入验证码文本
<input type="text" v-model="image_code" @blur="check_image_code">
- 用户输入的验证码绑定到 Vue 变量
image_code - 提交表单时会带到后端
隐藏字段 <input type="hidden" name="image_code_id"> —— 传递 UUID
<input type="hidden" name="image_code_id" :value="image_code_id">
- 每张验证码对应一个唯一 UUID(image_code_id)
- 你点击图片刷新时,UUID 也会刷新(generate_image_code)
- 注册时必须把这个 UUID 提交给后端
提交数据后后端就能从 Redis 找到真验证码:img_ = "真实验证码"
用户登录页面初步完善
apps/users/views.py
用户登录代码如下:
# ============================================================
# 用户登录
# ============================================================
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. 密码无需正则,可自由(但也可加)
# 5. Django 内置认证:用户名 + 密码
user = authenticate(username=username, password=password)
if user is None:
return render(request, "users/login.html", {"account_errmsg": "用户名或密码错误"})
# 6. 登录成功:写 session
login(request, user)
# 7. 设置 session 过期时间
if remembered == "on":
request.session.set_expiry(30 * 24 * 3600) # 30 天
else:
request.session.set_expiry(0) # 浏览器关闭即失效
# 8. 设置 cookie,用于首页展示用户名
resp = redirect(reverse("contents:index"))
resp.set_cookie("username", user.username, max_age=14 * 24 * 3600)
return resp
上述代码实现功能:
- 用 authenticate 检查用户名密码
- login() 写 session
- 支持 “记住登录”
- 用 cookie 展示用户名
- 用反向解析跳首页
引入 templates/users/login.html
可以从 static中找到 login.html,
需要完善:
- 静态文件路径全部适配jinjia2 模板
- 浏览器登录按钮提交后会默认提交到/users/login.html,这里用action="/users/login/"
- 后端 LoginView:password = request.POST.get("password"),HTML中修改标签:input type="password" name="pwd" 的 name 为 password
- 添加 csrf_token:{{ csrf_field(request) | safe }}
这里修后内容为:
<!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">
<!-- 登录表单 -->
<form method="post" action="/users/login/">
{{ csrf_field(request) | safe }}
<input type="text" name="username" class="name_input"
placeholder="请输入用户">
{% if account_errmsg %}
<div class="user_error">{{ account_errmsg }}</div>
{% endif %}
<input type="password" name="password" class="pass_input"
placeholder="请输入密码">
<div class="more_input clearfix">
<input type="checkbox" name="remembered">
<label>记住登录</label>
</div>
<input type="submit" value="登 录" class="input_submit">
</form>
</div>
</div>
<div class="third_party">
<a class="qq_login">QQ</a>
<a 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>
users/urls.py 须包含 login 路由
apps/users/urls.py:
from django.urls import path
from .views import RegisterView, UsernameCountView, LoginView, LogoutView, UserCenterInfoView
from apps.users.views import ImageCodeView
app_name = "users"
urlpatterns = [
path('login/', LoginView.as_view(), name='login'),
path('logout/', LogoutView.as_view(), name='logout'),
path('info/', UserCenterInfoView.as_view(), name='info'),
]
发布者:LJH,转发请注明出处:https://www.ljh.cool/44066.html