(根据居然老师直播课内容整理)
一、用户基本信息
1、用户基本信息逻辑分析
- 以下是要实现的后端逻辑
- 用户模型补充email_active字段
- 查询并渲染用户基本信息
- 添加邮箱
- 发送邮箱验证邮件
- 验证邮箱
2、用户模型补充email_active字段
- 用户模型中有email字段,但没有邮箱是否激活的字段,需要添加字段
- 对已存在数据有表中,增加字段时,一般需要添加缺省值
email_active = models.BooleanField(default=False, verbose_name='邮箱验证状态')
python manage.py makemigrations
python manage.py migrate
3、查询用户基本信息
- 获取用户中心页面时,查询用户信息,将相关内容做为参数传到前端即可
- UserInfoView.get()方法中,request会带有users信息
# ./apps/users/views.py
context = {
"username": request.user.username,
"mobile": request.user.mobile,
"email": request.user.email,
"email_active": request.user.email_active,
}
return render(request, 'user_center_info.html',context=context)
4、渲染用户基本信息
- 前端显示用户信息时,有3种方法:
- ajax 方式查询获取展示
- django的DTL模板语法方式展示
- vue 方式展示
- 为统一前端实现方式,本项目模板中采用vue方式展示
- 为了保持模板语法一致,将django中模板渲染变量写到js里面,赋值给js变量
- user_center_info.html:
- user_center_info.html
- user_center_info.js
5、添加邮箱信息
5.1 请求方式
选项 |
方案 |
请求方法 |
PUT |
请求地址 |
/email/ |
5.2 请求参数:
参数名 |
类型 |
是否必传 |
说明 |
email |
string |
是 |
{email:邮箱名} |
5.3 响应结果:HTML
字段 |
说明 |
邮箱验证失败 |
响应错误提示 |
邮箱验证成功 |
重定向到用户中心 |
5.4 后端views实现
- 在users.views.py 中定义一个EmailView类,用put方法保存邮箱名称到用户表中
- 接收参数:email
- put方法参数存放在request.body中
- request.body数据是字节类型的,需要decode()转换成字符串
- 再转换成json类型
- 校验邮箱:
- 保存邮箱信息
- 正常情况下,用户登录后才会到达此界面,需要校验是用户是否登录(后面再优化)
- 确认登录后,修改request.user.email属性值并保存,即可完成邮箱信息保存
- 发送激活邮件(后面再优化)
- 返回响应结果
# ./apps/users/views.py
class EmailView(View):
"""添加邮箱"""
def put(self,request):
# 接收参数
# print(request.body) # 建议调试看一下数据及数据类型
json_str = request.body.decode()
# print(type(json_str))
json_dict = json.loads(json_str)
email = json_dict.get('email')
# 校验参数
if not re.match(r'^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
return http.HttpResponseForbidden('参数邮箱有误')
# 存数据
try:
request.user.email = email
request.user.save()
except Exception as e:
return http.JsonResponse({'code': RETCODE.DBERR, 'errmsg': '添加邮箱失败'})
# 发送邮件
# 响应结果
return http.JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK'})
5.5 定义路由
# 保存邮件
path('email/', views.EmailView.as_view()),
5.6 前端user_center_info.js
- 点击保存时,执行此js的save_mail函数
- 首选判断邮箱是否正确,
- 如果正确,就提交后台
- 定义url
- 定义put数据传递方式
- 用ajax方式发送请求
- 返回类型是json
6、后端判断用户是否登录
6.1 自定义判断用户是否登录的扩展类:返回JSON
- 获取个人用户中心页面时,写过判断用户是否登录的功能
- UserInfoView类继续 django的LoginRequiredMixin类和view类
- LoginRequiredMixin返回的HttpResponseRedirect
- 而put方法返回的是JsonResponse
- 所以,继承LoginRequiredMixin类,改写返回方法
- 将自定义类存放到./utils/views.py 中
- 重写handle_no_permission方法
# /utils/views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django import http
from utils.response_code import RETCODE
class LoginRequiredJSONMixin(LoginRequiredMixin):
"""自定义判断用户是否登录的扩展类:返回JSON"""
def handle_no_permission(self):
"""直接响应JSON数据"""
return http.JsonResponse({'code': RETCODE.SESSIONERR, 'errmsg': '用户未登陆'})
6.2 优化EmailView类
- 继承自定义判断用户是否登录的扩展类,即可实现用户登录判断
class EmailView(LoginRequiredJSONMixin,View):
.....
7、发送邮箱验证邮件
7.1 Django发送邮件的配置
7.2 定义和调用发送邮件异步任务
- 发送邮箱验证邮件是耗时的操作,不能阻塞商城的响应,所以需要异步发送邮件。
- 我们继续使用Celery实现异步任务。
7.2.1 定义发送邮件异步任务
- 在celery_tasks应用目录下,创建 email包
- 定义tasks.py : 完成相关的发送邮件的业务逻辑
7.2.2 注册发送邮件异步任务
- 注册任务:只到包名,系统会自动找tasks.py任务文件
7.2.3 加载配置文件
- celery可以独立与django运行,属不同的进程,所以配置参数不可共用,celery需加载配置参数
7.2.4 相关代码实现
- /celery_tasks/email/tasks.py
from django.core.mail import send_mail
from django.conf import settings
from celery_tasks.main import celery_app
@celery_app.task(name="send_verify_email")
def send_sms_code(email,verify_url):
'''
发送邮件的异步任务
:param email: 发送邮箱
:param verify_url: 激活链接
'''
subject = "商城邮箱验证"
html_message = '<p>尊敬的用户您好!</p>' \
'<p>感谢您使用商城。</p>' \
'<p>您的邮箱为:%s 。请点击此链接激活您的邮箱:</p>' \
'<p><a href="%s">%s<a></p>' % (email, verify_url, verify_url)
send_mail(subject, '', from_email=settings.EMAIL_FROM, recipient_list=[email], html_message=html_message)
from celery import Celery
import os
# 加载配置文件
if not os.getenv('DJANGO_SETTINGS_MODULE'):
os.environ['DJANGO_SETTINGS_MODULE'] = 'lgshop.dev'
# 创建celery实例
celery_app = Celery('lg')
# 加载celery配置
celery_app.config_from_object('celery_tasks.config')
# 注册任务
celery_app.autodiscover_tasks(['celery_tasks.sms', 'celery_tasks.email'])
7.2.5 调用异步发送邮件任务
- /apps/user/views.py
一定要注意:send_verify_email.delay() 而 send_verify_email() 不是异步发送
8、生成邮箱验证链接
- 邮箱验证链接:http://www.meiduo.site:8000/users/emails/verification/?token=加密后的{‘user_id’: 1, ‘email’: ‘[email protected]’}
- http://www.meiduo.site:8000/ 应定义为常量,放到配置文件中
- /users/emails/verification/ 为路由
- token 为参数变量
- 加密算法:Django 的itsdangerous加解密
- /lgshop/dev.py
- /apps/users/utils.py
def generate_verify_email_url(user):
"""
生成邮箱激活链接
:return: 邮箱激活链接
http://www.meiduo.site:8000/users/emails/verification/?token=eyJhbGciOiJIUzUxMiIsImlhdCI6MTYxMjUxMzEwMSwiZXhwIjoxNjEyNTk5NTAxfQ.eyJ1c2VyX2lkIjoxLCJlbWFpbCI6IjI3MDUxODU4MzRAcXEuY29tIn0.bNHVCzCjBw2yO3RKqnI3tICLN97xwvKcqFC-qip2XAEpG2hXuCl2vn3E8Q_WxRF0i_z3scqsWokz8rnOV-pxQw
"""
s = Serializer(settings.SECRET_KEY, constants.VERIFY_EMAIL_TOKEN_EXPIRES)
data = {'user_id': user.id, 'email': user.email}
token = s.dumps(data)
return settings.EMAIL_VERIFY_URL + '?token=' + token.decode()
def check_verify_email_token(token):
"""
反序列token 获取user
:param token: 序列化之后的用户信息
:return: user
"""
#反序化参数
s = Serializer(settings.SECRET_KEY, constants.VERIFY_EMAIL_TOKEN_EXPIRES)
print("s=",type(s),s)
try:
data = s.loads(token)
except:
return None
else:
user_id = data.get('user_id')
email = data.get('email')
try:
user = User.objects.get(id=user_id, email=email)
except User.DoesNotExist:
return None
else:
return user
- /apps/users/views.py
9、验证激活邮件的后台逻辑
9.1 请求方式
选项 |
方案 |
请求方法 |
GET |
请求地址 |
/emails/verification/ |
9.2 请求参数:查询参数
参数名 |
类型 |
是否必传 |
说明 |
token |
string |
是 |
邮箱激活链接 |
9.3 响应结果:HTML
字段 |
说明 |
邮箱验证失败 |
响应错误提示 |
邮箱验证成功 |
重定向到用户中心 |
9.4 veiw实现
- 获取参数
- request.GET.get(“token”).decode()
- 反序列化参数并查询用户
- user=check_verify_email_token(token)
- 修改激活状态
- 返回响应状态
# /apps/users/views.py
class VerifyEmailView(View):
"""验证邮箱"""
def get(self, request):
# 接收参数
token=request.GET.get("token")
if not token:
return http.HttpResponseForbidden('缺少token')
# 解密 token => user {'user_id': 1, 'email': '[email protected]'}
# 查询用户
user=check_verify_email_token(token)
if user.email_active == 0:
# 没有激活 email_active 设置为true
user.email_active = True
user.save()
else:
# email_active 是否已经激活
return http.HttpResponseForbidden('邮箱已经被激活')
# 响应结果
return redirect(reverse('users:info'))
9.5 注册路由
- /apps/users/url.py
二、修改密码
-
- 用户点击修改密码后,判断用户是否登录,如果登录,可获取注册页面
- 用户输入原密码和两次新密码后,点击确定提交后端
- 后端接收数据后,会校验密码:是否符合规定要求、两次新密码是否相同、新旧密码是否一样等
- 校验通不过,返回错误信息
- 验证原密码是否正确
- 保存新密码
- 清理session, 跳转登录界面
1、接口设计和定义
1.1 请求方式:
选项 |
方案 |
请求方法 |
POST |
请求地址 |
/users/changepassword/ |
1.2 请求参数:
参数名 |
类型 |
是否必传 |
说明 |
old_password |
string |
是 |
原密码 |
new_password |
string |
是 |
新密码 |
new_password2 |
string |
是 |
确认新密码 |
1.3 响应结果:html
响应结果 |
响应内容 |
change_password_errmsg |
错误信息 |
2、后端view实现
class ChangePassword(LoginRequiredMixin,View):
def get(self,request):
"""提供修改密码界面"""
return render(request, 'user_center_pass.html')
def post(self,request):
changepassword_form = ChangepasswordForm(request.POST)
if changepassword_form.is_valid():
old_passwd = changepassword_form.cleaned_data.get('old_password')
new_password = changepassword_form.cleaned_data.get('new_password')
if not request.user.check_password(old_passwd):
return render(request,'user_center_pass.html', {
'change_password_errmsg':'原始密码错误'})
try:
request.user.set_password(new_password)
request.user.save()
except Exception as e:
logger.error(e)
return render(request, 'user_center_pass.html', {
'change_password_errmsg': '修改密码失败'})
logout(request)
response = redirect(reverse('users:login'))
response.delete_cookie('username')
return response
else:
print(type(changepassword_form.errors.get_json_data()))
print(changepassword_form.errors.get_json_data())
print(changepassword_form.errors["__all__"])
context = {
'change_password_errmsg': changepassword_form.errors["__all__"]
}
return render(request, 'user_center_pass.html', context=context)
username = forms.CharField(max_length=20, min_length=5, required=True,
error_messages={
"max_length": "用户名长度最长为20", "min_length": "用户名最少长度为5"})
password = forms.CharField(max_length=20, min_length=8, required=True,
error_messages={
"max_length": "密码长度最长为20", "min_length": "密码最少长度为8"})
remembered = forms.BooleanField(required=False)
class ChangepasswordForm(forms.Form):
old_password = forms.CharField(max_length=20, min_length=8, required=True,
error_messages={
"max_length": "密码长度最长为20", "min_length": "密码最少长度为8",
"required": "密码必须填写"})
new_password = forms.CharField(max_length=20, min_length=8, required=True,
error_messages={
"max_length": "确认密码长度最长为20", "min_length": "确认密码最少长度为8",
"required": "确认密码必须填写"})
new_password2 = forms.CharField(max_length=20, min_length=8, required=True,
error_messages={
"max_length": "确认密码长度最长为20", "min_length": "确认密码最少长度为8",
"required": "确认密码必须填写"})
def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get('new_password')
password2 = cleaned_data.get('new_password2')
password3 = cleaned_data.get('old_password')
if password != password2:
raise forms.ValidationError('两次密码不一致')
if password == password3:
raise forms.ValidationError('输入的新旧密码不能相同')
return cleaned_data
3、路由定义
4、前端实现