供应链开发第二天
# 发送短信验证码
发送的短信验证码是存储在redis中的!
先来看看样子:
后端 dahe/settings.py 中关于redis的配置
redis的本地安装,启动; 国产redis可视化软件 QuickRedis的使用.. 零零散散的总结,用到了自行翻阅.
CACHES = {
"default": { # -- ▲ 这里default
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"CONNECTION_POOL_KWARGS": {
"max_connections": 100,
"encoding": "utf-8",
}
# "PASSWORD": "密码",
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
后端 shipper/views.py 中关于发送短信验证码的业务逻辑代码.
pip install django-redis -i https://pypi.tuna.tsinghua.edu.cn/simple
★ 发送短信的业务逻辑主要是这几步, 牢记!
step1> 对请求体中的数据进行校验 (手机号格式+手机号必须已注册,即在数据库中存在)
step2> 调用第三方短信接口-发送短信 暂略.
step3> 将验证码保存到redis中 (启动redis服务,配置redis连接,请求保存).
step4> 返回json数据.
from django.core.validators import RegexValidator
from rest_framework import exceptions
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.views import APIView
from ext import ret
from repository import models
class SendSmsSerializer(serializers.Serializer):
mobile = serializers.CharField(required=True, validators=[RegexValidator(r"^1\d{10}$", message="格式错误")])
def validate_mobile(self, value):
exists = models.Company.objects.filter(mobile=value).exists()
if not exists:
raise exceptions.ValidationError("手机号未注册!")
return value
class SendSmsView(APIView):
def post(self, request):
try:
# print(request.data)
# 1.对请求体中的数据进行校验 (手机号格式+手机号必须已注册,即在数据库中存在)
ser = SendSmsSerializer(data=request.data)
if not ser.is_valid():
return Response({"code": ret.FIELD_ERROR, 'msg': 'error', 'detail': ser.errors})
# 2.调用第三方短信接口-发送短信 略
import random
random_code = random.randint(1000, 9999)
pass # To do: 调用第三方短信接口
# 3.将验证码保存到redis中 (启动redis服务,配置redis连接,请求保存)
from django_redis import get_redis_connection
conn = get_redis_connection("default")
conn.set(ser.validated_data['mobile'], random_code, ex=60) # 60s过期
# 4.返回
return Response({"code": ret.SUCCESS, 'msg': '验证码发送成功!'})
except Exception as e:
# 可能是第三方短信接口异常;也有可能是redis相关的错误!!
return Response({"code": ret.SUMMARY_ERROR, 'msg': '短信发送异常'})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
前端 src/views/LoginView.vue 中关于发送短信验证码的代码
const smsModel = reactive({mobile: '', code: '',})
const smsError = reactive({mobile: '', code: '',})
const btnSmsText = ref("发送验证码")
const btnSmsDisabled = ref(false)
const smsRules = reactive({
mobile: [{required: true, message: '手机号必填', trigger: 'blur'},],
code: [{required: true, message: '验证码必填', trigger: 'blur'},],
})
function sendSmsRemind() {
btnSmsDisabled.value = true;
let txt = 10;
let interval = window.setInterval(() => {
txt -= 1
btnSmsText.value = `${txt}秒后重发`
if (txt < 1) {
btnSmsText.value = '重新发送'
window.clearInterval(interval)
btnSmsDisabled.value = false;
}
}, 1000)
}
function doSendSms() {
// 发送验证码之前主动校验手机号是否合法 会去smsRules中找mobile字段的规则
proxy.$refs.smsRef.validateField("mobile", (valid) => {
// 前端手机号字段校验失败
if (!valid) {
// console.log("手机号校验失败!");
return false;
}
// 前端手机号校验成功,发送网络请求(基于axios发送)
proxy.$axios.post(
"http://127.0.0.1:8000/api/send/sms/",
{mobile: smsModel.mobile},
).then((res) => {
// console.log(res)
if (res.data.code === 0) {
ElMessage.success(res.data.msg)
sendSmsRemind() // 短信验证码发送成功,发送按钮开始倒计时!!
} else if (res.data.code === -1) {
validateFormError(smsError, res.data.detail);
} else {
ElMessage.error(res.data.msg)
}
})
})
}
function validateFormError(errorDict, resError) {
for (let key in resError) {
errorDict[key] = resError[key][0];
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 短信登陆
其实大体上跟用户名密码登陆差不多.. 区别就在于 短信登陆会查询redis数据库.
PS: 点击发送验证码后, 若超过有效期1分钟后再输入验证码, 点击登陆, 验证码字段下会提示 验证码不存在或过期!!
后端 shipper/views.py 中关于短信验证码登陆的业务逻辑代码.
class LoginSmsSerializer(serializers.Serializer):
mobile = serializers.CharField(required=True, validators=[RegexValidator(r"^1\d{10}$", message="格式错误")])
code = serializers.CharField(required=True, validators=[RegexValidator(r"^\d{4}$", message="格式错误")])
def validate_mobile(self, value):
exists = models.Company.objects.filter(mobile=value).exists()
if not exists:
raise exceptions.ValidationError("手机号未注册!")
return value
def validate_code(self, value):
mobile = self.initial_data.get("mobile")
# 连接redis
from django_redis import get_redis_connection
conn = get_redis_connection("default")
# 去redis中获取数据
# 若没有或者过期了,取到的肯定是空的!还需注意,有的话,它是字节类型的数据!!eg:b'7880'
cache_code = conn.get(mobile)
if not cache_code:
# 该手机号未发送验证码或该手机号验证码已过期
raise exceptions.ValidationError("验证码不存在或已过期")
cache_code = cache_code.decode("utf-8")
if cache_code != value: # 若验证码里包含字母啥的,需要统一 lower或upper
raise exceptions.ValidationError("验证码输入错误!")
# 切记,验证通过后,记得在redis中去除这条数据,以免在有效期里多次使用!!
conn.delete(mobile)
return value
class LoginSmsView(APIView):
def post(self, request):
"""短信验证码登陆"""
try:
ser = LoginSmsSerializer(data=request.data)
if not ser.is_valid():
return Response({"code": ret.FIELD_ERROR, "msg": "error.", "detail": ser.errors})
instance = models.Company.objects.filter(mobile=ser.validated_data["mobile"]).first()
token = create_token({'user_id': instance.id, 'name': instance.name})
return Response({
"code": ret.SUCCESS,
'msg': "登陆成功!",
'data': {"token": token, 'name': instance.name}
})
except Exception as e:
return Response({"code": ret.SUMMARY_ERROR, 'msg': '数据库连接有误!'})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
前端 src/views/LoginView.vue 中关于短信验证码登陆的代码
function doSmsLogin() {
clearFormError(smsError)
proxy.$refs.smsRef.validate((valid) => {
if (!valid) {
return false
}
// console.log("校验成功!", smsModel)
// let smsLoginError_res = {code: -1, error: {mobile: ["手机号码已存在"], code: ["验证码失效"]}}
// validateFormError(smsError, smsLoginError_res.error);
proxy.$axios.post(
"http://127.0.0.1:8000/api/login/sms/ ",
smsModel
).then((res) => {
// console.log(res.data)
if (res.data.code === 0) {
// res.data -- {"code": 0,"msg":"登陆成功!","data": {"token": "xxxx","name": "大和实业"}}
ElMessage.success(res.data.msg)
// 登陆成功,token写入vuex+持久化+跳转
store.commit("login", res.data.data); // proxy.$store.commit("login", res.data.data);
router.replace({name: "Basic"}) // proxy.router({name: "Basic"})
} else if (res.data.code === -1) {
// 后端校验不通过,登陆失败 在表单上显示具体某些个字段的错误
// res.data -- {"code": -1, "msg": "error.", "detail": {"code": ["验证码输入错误!"]}}
validateFormError(smsError, res.data.detail);
} else {
// 后端校验不通过,登陆失败 利用ElMessage显示整体错误
// res.data -- {code: -2, msg: '数据库连接有误!'}
ElMessage.error(res.data.msg)
}
})
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# ★ 登陆过程总结
总结下登陆过程前后端都做了什么? 得明白知晓大体的业务流程!!
我很喜欢总结试图找到共同的一些规则.. 总结的东西时常只可意会不可言传,哈哈哈
我们可以强行挖掘一些共同的逻辑,这三个接口都是前端校验通过后,向后端发送axios请求
后端拿到数据后,根据返回给前端的code分为三部分:
1> is_valid的校验 不通过返回的code是-1
[首先]需要像前端校验数据的规则一样对这些字段进行字段自身的校验
[其次]可以根据业务逻辑在字段的钩子中对字段进行一些校验
以验证码登陆接口中的验证码字段的钩子为例,验证码不存在、过期、比对不一致等(注意:该钩子用到了上一个校验通过的mobile字段)
2> 整体的校验 不通过返回的code是-2
eg: - 用户名或密码错误(按道理,更容易想到的逻辑将该判断放到全局钩子里,
但代码不好写,这是其一;其二是返回给前端的信息中说错误可能是用户名错误也可能是密码错误,没有明确在哪个字段下展示)
- redis错误、第三方短信接口错误 使用try..except..将整个逻辑代码框起来
你想啊,关于验证码发送,前端只传过来一个mobile字段,对应后端业务逻辑代码中要进行生成验证码,将验证码保存redis等处理
这些业务逻辑断然不可能在is_valid中实现,因为前端传过来的只有mobile字段,so,只能在is_valid后面实现验证码的相关逻辑.
3> 接口返回成功的数据 返回的code是0
前端根据后端返回数据中的code进行处理
1> code = -1 在对应表单字段下展示错误信息
2> code = -2 弹出框展示整体错误
3> code = 0 弹出框展示成功信息 + 后续的一些业务处理
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
顺便说一点, 手机号密码登陆、发送短信验证码、验证码登陆,这三个接口,
在后端的序列化器类用的都是 serializers.Serializer
, 因为这三个接口不涉及保存更新的sava操作.. 使用它更方便
根据需求来,并不是一味的使用 ModelSerializer 就是好的.
# 注册
此处的注册示例是简略版的, 验证码相关的、名字是否重名等业务逻辑都一概省略啦!!
省略的逻辑如何编写在登陆模块的代码中都能找到相似的处理!! 无需担心.
先来看看效果
后端 shipper/views.py
class RegisterSerializer(serializers.ModelSerializer):
mobile = serializers.CharField(required=True, validators=[RegexValidator(r"^1\d{10}$", message="格式错误")])
code = serializers.CharField(required=True, validators=[RegexValidator(r"^\d{4}$", message="格式错误")])
confirm_password = serializers.CharField(required=True)
class Meta:
model = models.Company
fields = ['name', 'mobile', 'code', 'password', 'confirm_password']
# 验证码登陆-手机号是必须存在;注册-手机号必须不存在.
def validate_mobile(self, value):
exists = models.Company.objects.filter(mobile=value).exists()
if exists:
raise exceptions.ValidationError("手机号已经注册!")
return value
def validate_password(self, value):
md5_string = md5(value)
return md5_string
def validate_confirm_password(self, value):
password = self.initial_data.get('password')
if value != password:
raise exceptions.ValidationError("密码不一致")
return value
class RegisterView(APIView):
def post(self, request):
try:
ser = RegisterSerializer(data=request.data)
if not ser.is_valid():
return Response({"code": ret.FIELD_ERROR, "msg": "error.", "detail": ser.errors})
ser.validated_data.pop("code")
ser.validated_data.pop("confirm_password")
ser.save()
return Response({"code": ret.SUCCESS, "msg": "注册成功!"})
except Exception as exc:
return Response({"code": ret.SUMMARY_ERROR, 'msg': '注册失败!'})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
前端 src/views/RegisterView.vue 细心的你可以发现, regModel、regError等都被写在了state中!!
import {getCurrentInstance, reactive} from 'vue'
import {ElMessage} from "element-plus";
import {useRouter} from 'vue-router'
import {validateFormError, clearFormError} from '@/plugins/from'
const router = useRouter();
const {proxy} = getCurrentInstance()
const validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('重复密码不能为空'))
} else if (value !== state.regModel.password) {
callback(new Error("两次密码输入不一致!"))
} else {
callback()
}
}
const state = reactive({
regModel: {
name: '小川药业', mobile: '13888888881', code: '1024', password: 'root123', confirm_password: 'root123',
},
regError: {
name: '', mobile: '', code: '', password: '', confirm_password: ''
},
regRules: {
name: [
{required: true, message: '企业名称必填', trigger: 'blur'},
],
mobile: [
{required: true, message: '手机号必填', trigger: 'blur'},
{pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur'},
],
code: [
{required: true, message: '验证码必填', trigger: 'blur'},
],
password: [
{required: true, message: '密码必填', trigger: 'blur'},
{min: 6, max: 10, message: '密码至少有6位', trigger: 'blur'},
],
confirm_password: [
{validator: validatePass, trigger: 'blur'},
],
}
})
function doRegister() {
clearFormError(state.regError)
proxy.$refs.regRef.validate((valid) => {
if (!valid) {
return false
}
proxy.$axios.post(
"http://127.0.0.1:8000/api/register/ ",
state.regModel
).then((res) => {
if (res.data.code === 0) {
ElMessage.success(res.data.msg)
router.push({name: "Login"})
} else if (res.data.code === -1) {
validateFormError(state.regError, res.data.detail);
} else {
ElMessage.error(res.data.msg)
}
})
})
}
function resetRegisterForm() {
proxy.$refs.regRef.resetFields()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# ★ 代码整理和小总结
到目前为止,我们一共写了 5个接口, /api/basic/接口是,后端认证,前端axios拦截的测试接口,暂且不论..
总结下登陆注册相关的4个接口!!
# 登陆注册
■ 用户名密码登陆
□ 前端校验规则
- mobile 必填,长度为11位
- password 必填,密码长度在6到20位之间且包含至少一个数字和一个字母
输入框失去焦点的表单校验 + 点击登陆按钮时表单的主动校验, 前端校验通过后, 携带表单数据向后端发送axios请求
□ 后端校验规则 serializers.Serializer
1> is_valid
- mobile 不为空
- password 不为空 >> password钩子,返回md5加密后的值
2> 整体错误,查询数据库 用户名或密码不正确
3> 登陆成功,生产token,返回用户信息
instance = models.Company.objects.filter(**ser.validated_data).first()
token = create_token({'user_id': instance.id, 'name': instance.name})
□ 前端axios回调
- code=-1 在表单上显示具体某些个字段的错误
- code=-2 ElMessage显示整体错误
- code=0 ElMessage显示登陆成功+token写入vuex+持久化+跳转+状态展示,即显示哪个用户登录啦
■ 发送短信验证码
□ 前端校验规则
- mobile 必填
输入框失去焦点的表单校验 + 点击发送验证码按钮时,主动校验mobile字段的规则, 前端校验通过后, 携带mobile字段值向后端发送axios请求
□ 后端校验规则 serializers.Serializer
1> is_valid
- mobile 不为空,由1开头的11位数组成 >> mobile钩子,查询数据库手机号,手机号得注册才行
2> 调用第三方短信接口发送短信,并启动redis服务,配置redis连接,保存发送的短信验证码
try..except.. 整体错误,可能是第三方短信接口异常;也有可能是redis相关的错误!!
3> 发送短信验证码成功
□ 前端axios回调
- code=-1 在表单上显示具体某些个字段的错误
- code=-2 ElMessage显示整体错误
- code=0 ElMessage显示短信验证码发送成功,发送短信按钮进入倒计时
■ 短信验证码登陆
□ 前端校验规则
- mobile 必填
- code 验证码必填
输入框失去焦点的表单校验 + 点击登陆按钮时表单的主动校验, 前端校验通过后, 携带表单数据向后端发送axios请求
□ 后端校验规则 serializers.Serializer
1> is_valid
- mobile 不为空,由1开头的11位数组成 >> mobile钩子,查询数据库手机号,手机号得已经了注册才行
- code 不为空,由4位数字组成 >> code的钩子,拿到手机号在redis中存储的验证码,"验证码不存在或已过期"、"验证码比对后错误"
2> try..except.. 整体错误,redis相关的错误
3> 登陆成功,生产token,返回用户信息
instance = models.Company.objects.filter(mobile=ser.validated_data["mobile"]).first()
token = create_token({'user_id': instance.id, 'name': instance.name})
□ 前端axios回调
- code=-1 在表单上显示具体某些个字段的错误
- code=-2 ElMessage显示整体错误
- code=0 ElMessage显示登陆成功+token写入vuex+持久化+跳转+状态展示,即显示哪个用户登录啦
■ 注册
⚠️ 注:注册过程实际上需要发送验证码,需要多写一个发送注册验证码的接口,注册接口中也需要对验证码进行验证.这些都省略了.
因为这部分功能的实现在验证码登陆时已经编写过了,大同小异,所以注册功能的实现一切从简.
□ 前端校验规则
- name 必填
- mobile 手机格式得正确
- code 验证码必填
- password 必填,密码至少为6位
- confirm_password 必填,两次密码输入必须一致 => 自定义校验规则,{validator: validatePass, trigger: 'blur'},
输入框失去焦点的表单校验 + 点击注册按钮时表单的主动校验, 前端校验通过后, 携带表单数据向后端发送axios请求
□ 后端校验规则 serializers.ModelSerializer 数据库中有name、mobile、password字段 可自动生成
fields = ['name', 'mobile', 'code', 'password', 'confirm_password']
1> is_valid
- mobile 重写,不为空,由1开头的11位数组成 >> mobile钩子,查询数据库手机号,手机号得未注册才行
- code 额外字段,不为空,4位数字组成
- confirm_password 额外字段,不为空 >> confirm_password钩子,比较两次密码是否一致
- password(db自动生成) >> password钩子,返回md5加密后的值
2> try..except.. 整体错误
3> ser.validated_data.pop("code")
ser.validated_data.pop("confirm_password")
ser.save()
return Response({"code": ret.SUCCESS, "msg": "注册成功!"})
□ 前端axios回调
- code=-1 在表单上显示具体某些个字段的错误
- code=-2 ElMessage显示整体错误
- code=0 ElMessage显示注册成功+跳转登陆界面
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# 前后端代码整理
■ 后端
- 多个app,整理到apps文件夹里 记得在那个前面加上apps前缀
- 将views.py中的业务代码根据业务逻辑拆分出多个py文件放到views文件夹里
再对py文件里的代码进一步拆分,序列化器类分离了出来
- 对urls根路由进行路由分发
- 创建一个server文件夹,将ext和utils目录都放了进去
■ 前端
- 堆积在一起的vue组件根据业务功能进行划分,归纳到不同的文件夹下 (也有根据模版划分,但根据功能划分的居多)
- axios请求的url可统一放到一个文件中,便于修改.“这个暂时没弄” url可配置前缀,配置后就可以省略前缀啦
- 看前端路由的结构
path: '', redirect: {name: "Home"}, # 当我访问127.0.0.1:8080时,自动跳转到127.0.0.1:8080/account
path: /:xx(.*) # 浏览器url乱写,404找不到
path: /login name: Login
path: /register name: Register
path: /account name: Front
path: '' name: 'Home', # 当我访问127.0.0.1:8080/account时,自动加载的是Home组件
path: '/auth' name: 'Auth',
path: '/pub', name: 'Pub',
path: '/pub/list', name: 'PubList',
- 点击Logo图标跳转到Home组件,未登录的话会被导航守卫拦截,无需担心.
- 修改一下,登陆成功后都跳转到Home组件!
- 侧边栏的处理
- 结构的调整
- 动态的默认选中,以适应刷新!
import {useRoute} from "vue-router";
import {computed} from "vue";
const route = useRoute();
// const activeRouter = route.name 其实不用computed计算属性,直接赋值是可以的.因为刷新会重载生命周期!
const activeRouter = computed(() => route.name)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 基本信息页面
该页面使用了elment-plus里的 卡片、分割线、标签、链接等!
先展示效果
细品一下里面的flex布局,是咋弄的!! 对第二栏、第三栏的布局进行细细分析!!
前端 src/views/account/BasicView.vue
<template>
<div>
<el-card class="box-card">
<template #header>
<span style="font-weight: bold;font-size: 18px;">基本信息</span>
</template>
<div style="padding: 10px 20px">
<el-row justify="start">
<el-row justify="center" align="middle">
<img src="@/assets/logo.png" alt="" style="height: 90px">
</el-row>
<div class="info">
<div>
<span>用户ID: </span>
<span>160214</span>
</div>
<div>
<span>注册时间: </span>
<span>2022-12-04</span>
</div>
<el-tag style="margin-top:10px" type="success">已实名</el-tag>
<div>
<el-tag type="danger">未实名</el-tag>
<router-link :to="{name:'Auth'}">
<el-link type="danger" :underline="false"
style="font-weight: normal;margin-left: 10px">
点击前往认证
</el-link>
</router-link>
</div>
</div>
</el-row>
<el-divider border-style="dashed"/>
<div class="rows">
<div class="group">
<div class="key">
<el-icon><User/></el-icon>
<span>用户名</span>
</div>
<div class="txt">大和实业</div>
</div>
<div>
<a class="link" href="#">
<el-icon><Edit/></el-icon>
修改
</a>
</div>
</div>
<el-divider border-style="dashed"/>
<div class="rows">
<div class="group">
<div class="key">
<el-icon>
<Iphone/>
</el-icon>
<span>绑定手机</span>
</div>
<div class="txt">您已绑定 1388****881(该手机号用于登录、找回密码)</div>
</div>
<div>
<a class="link" href="#">
<el-icon>
<Edit/>
</el-icon>
修改
</a>
</div>
</div>
<el-divider border-style="dashed"/>
<el-row justify="center" align="middle" style="height: 80px;">
<el-button type="primary" plain style="width: 180px;height: 50px;">退出登录</el-button>
</el-row>
</div>
</el-card>
</div>
</template>
<script setup>
import {User, Edit, Iphone} from '@element-plus/icons-vue'
import {getCurrentInstance} from 'vue'
const {proxy} = getCurrentInstance()
proxy.$axios.get("/api/shipper/basic/").then((res) => {
console.log(res);
})
</script>
<style scoped>
/* 第一栏 */
.info {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
margin-left: 20px;
}
.info > div {
font-size: 14px;
padding: 2px 0;
color: #636e72;
}
/* 第二栏、第三栏 */
.rows {
display: flex;
justify-content: space-between;
align-items: center;
}
.rows .group {
display: flex;
justify-content: space-between;
align-items: center;
}
.rows .group .key {
display: flex;
align-items: center;
font-size: 18px;
width: 200px;
}
.rows .group .key span {
margin-left: 10px;
}
.rows .group .txt {
font-size: 15px;
color: #b2bec3;
}
.rows .link {
display: flex;
align-items: center;
color: #0088f5;
font-size: 14px;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# 基本信息-加载初始数据
GET请求获取当前登陆用户的基本信息.
☆ 我比较关心的三个点: 当你再次看到该小节的内容,务必围绕这三点仔细思考下,知其然知其所以然!
1> 认证成功后才能访问. - 后端认证类
2> api返回数据的格式务必遵循规范. - 重写RetrieveModelMixin
3> 当前登陆用户只能看到自己的基本信息. - 添加过滤类 (你细品下self.get_object()
的源码).
# 展示数据
前端基本信息页面对应组件挂载时,主动向后端发送axios请求!获取页面中字段存储在数据库中的值,并在页面上展示!!
先来看看结果!!
简单提一提几个要点: 具体代码该小节最后有截图.
1> 前端向后端发送api的路由编写规则
http://127.0.0.1:8000/api/shipper/basic/
- GET 获取数据列表
- POST 创建数据
http://127.0.0.1:8000/api/shipper/basic/1/
- GET 获取单条数据的详细信息
- PUT 对单条数据进行全局更新
- PATCH 对单条数据进行局部更新
- DELETE 删除单条数据
2> 在前面登陆相关的api中,登陆成功后,后端只返回了前端token和name,前端得到后,写入了vuex中并持久化.
加载初始数据的api,还需要一个id表明当前登陆的是哪个用户,所以,登陆成功后,后端得token和name,以及id!!
3> 注: 基本信息加载初始数据,涉及到 拦截器相关处理(简单说,axios携带token,后端进行认证). 在前面我们已经实现了,特地提醒下!!
4> 关于后端视图类的编写,我们选择了使用GenericViewSet和RetrieveModelMixin,路由方面使用的是SimpleRouter.
★ 虽然代码少了,但使用默认的RetrieveModelMixin会存在一些问题,有写不是我们预想的效果.
To do 紧接着就会解决.
- 重写RetrieveModelMixin,解决响应的数据格式,包括成功和数据没找到
- 我们自己写的视图类里写的是 queryset = models.Company.objects.all() !!
需要保证当前登陆用户只能看到自己的基本信息,所以需要再加上个过滤类!!
5> 为了避免网络问题导致页面请求后端的数据不能第一时间到达,导致页面字段出现空白.
我们在前端使用了element-plus的加载组件!!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 重写RetrieveModelMixin
为啥要重写它, 因为它默认直接返回前端data啦! 返回的数据的格式不满足我们的规定!!
怎么个规定? 像前面登陆注册返回的数据格式一样!!有code字段、data字段等.
我们想让api响应的数据是这样子的:
来,理一理逻辑,看看到底是怎么一回事
在上面,我们通过结果的gif图,可以清晰的看到api返回的数据是
{"id":1,"name":"大和实业","mobile":"138****8881","ctime":"2023-11-20","auth_type":1,"auth_type_text":"未认证"}
这是RetrieveModelMixin视图类的源码造成的.
class RetrieveModelMixin:
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
return Response(serializer.data)
我们需要重写它!!重写时,要考虑两个方面.
1> 成功获取数据,返回的数据格式
return Response({"code": ret.SUCCESS, "data": serializer.data})
2> 数据没找到,返回的数据格式
- 如果不作任何处理,数据没找到,默认返回的是 {"detail":"未找到。"}
PS:没找到是源码self.get_object()报的错,然后被dispatch捕获到啦!
- 我们可以自己写try..except..
return Response({"code": ret.SUMMARY_ERROR, "msg": "获取初始数据失败!"})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
重写的代码如下:
from rest_framework.response import Response
from server.ext import ret
class RetrieveModelMixin:
def retrieve(self, request, *args, **kwargs):
try:
instance = self.get_object() # 抛出异常,models.Company.objects.get(id="12345")
serializer = self.get_serializer(instance)
return Response({"code": ret.SUCCESS, "data": serializer.data})
except Exception as e:
return Response({"code": ret.SUMMARY_ERROR, "msg": "获取初始数据失败!"})
2
3
4
5
6
7
8
9
10
11
# 添加过滤类
必须考虑两个问题: 1> 认证成功后访问(写认证类,前面已经实现) 2> 当前用户只能查看到自己的基本信息!!
现目前后端代码
from server.ext.mixins import RetrieveModelMixin
class BasicView(GenericViewSet, RetrieveModelMixin):
authentication_classes = [JwtAuthentication, ] # 保证了前端发送axios时必须得有token信息的请求头,且token信息无误.
queryset = models.Company.objects.all()
serializer_class = BasicModelSerializer
2
3
4
5
6
7
若保持现状,可在apifox中进行实验.. 当前登陆用户的id是1, 该用户可以获取到自己的以及id为2、id为3.. 其他用户的基本信息!!
为啥? 多的不解释了,从两个方面切入思考,很容易想通
■ queryset = models.Company.objects.all() 看到没,啥也不做,默认获取的数据集是所有数据!
■ self.get_object()的源码! 获取数据集-过滤-获取单条数据-细粒度的权限控制
造成的原因想通了, 解决方案也就出来了, 添加自定义的过滤类!!
在过滤后的数据中取某个id的数据.. 只要过滤后的数据中只有当前用户的数据,那么当前用户就不可能取到其他用户的数据!!
from rest_framework.filters import BaseFilterBackend
class MineFilterBackend(BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
user_id = request.user["user_id"]
return queryset.filter(id=user_id)
""" 通俗点来说:
- 啥也不做
queryset = models.Company.objects.all()
self.get_object()获取单条数据时 queryset.get(id=1) # id值随便造!
- 有过滤类后
queryset = models.Company.objects.all().filter(id=1)
self.get_object()获取单条数据时 queryset.get(id=1) # id值仅限于1啦!
"""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 完整代码
粘贴下基本信息展示的前后端关键代码
后端的关键代码如下: Ps - 后端路由方面使用的是SimpleRouter,这部分代码没粘贴.
注: 提醒一点,截图中的过滤类中的request.user是认证组件通过后赋值的.
前端的关键代码如下:
# ★回顾单条数据更新
★★★ 举个例子简单回顾一下单条数据的更新:
book = Publish.objects.all().filter(pk=kwargs['pk']).first()
ser = PublishModelSerializer(instance=book, data=request.data, partial=partial)
if ser.is_valid():
ser.save()
return Response({'code': 100, 'msg': '修改成功', 'data': ser.data})
else:
return Response({'code': 999, 'msg': ser.errors})
★★★ 序列化器有两大功能
1. 数据校验
2. 对象序列化返回
write_only=True 该字段仅功能1时使用-仅校验,前端必须得提供
read_only=True 该字段仅功能2时使用-仅序列化,前端无需提供
ψ(`∇´)ψ 详细的简略过程可以看 "第二次学drf里Serializer总结里的 验证+存储+序列化的那张图!!"整个过程写的很清除啦!
复习它时,注意几点:
- 序列化器决定了使用哪些字段,并将其分为了两类
- 验证时,会获取request.data中 可序列化字段的字段名对应的值;
- save时,用的是验证过后的 validated_data, 得特别注意source_attrs!
- ser.data进行序列化时,是用的是刚刚存储到数据库中更新后的那个对象!!
▲ 特别特别注意,更新时,只需要有字段是ORM表中的字段即可!! 像下面的例子,Company表中有mobile字段,并没有old字段!!
from apps.repository import models
obj = models.Company.objects.filter(mobile=13888888881).first()
if obj:
print(obj.mobile) # 13888888881
obj.old = 13888888881
obj.mobile = 13888888882
obj.save()
print(obj.mobile) # 13888888882
print(obj.old) # 13888888881
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 基本信息-更新名称
点击用户名一栏的 修改按钮, 弹出一个弹出框!
先来看看效果
在上面的gif中,还有一处需要改善的地方:
当我们更新用户名后,导航栏中当前登陆用户的名字需要同步修改.只需要做两步就可以实现.
1> src/store/index.js
updateName(state, name) {
state.name = name;
localStorage.setItem('name', name);
},
2> 更改用户名的axios回调中,添加这么一行代码
store.commit("updateName", res.data.data.name);
经过上述两步,在App.vue中 导航栏中当前登陆用户名字是用监听属性实现的,所以vuex中name改变了,会同步改.
const name = computed(() => store.state.name);
2
3
4
5
6
7
8
9
10
11
# 关键代码
粘贴前后端的关键代码!
前端关键代码如下:
Ps: 其实,doUpdateName点击事件里的axios请求也应该加上 .catch
回调, 捕获 登陆凭证失效后.then
里的异常!!
Ps: 上图中, 我写错了一个地方, 在显示具体字段错误时, 与后端相对应, 应该是 res.data.dta
, 而不是res.data.detail
后端关键代码如下:
同样的! 和基本信息-加载初始数据逻辑基本一致. 第4点是基本信息-更新名称新增的!
1.认证 认证成功后才能访问
2.重写UpdateModelMixin 保证返回的数据格式符合规范
3.过滤类 保证当前用户只能修改自己的基本信息
4.需要两个序列化器类,保证通过该api接口只能修改name这一个字段!!
防止通过apifox等接口工具恶意修改其他字段!!
1,3不用改,前面加载初始数据已经完成了;2,4需要我们自己来编写
★★★ 额外注意一点,局部更新和全字段更新!
serializer = NameModelSerializer(instance=instance, data=request.data, partial=True)
partial=True 局部更新
instance=instance 过滤类限制了哪个instance (详见self.get_object对应的源码!)
NameModelSerializer 限制了能更新的字段
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Ps: 细心的你也许会发现, 我在BasicModelSerializer的mobile字段的自定义方法里写了read_only=False,其实没啥用的!!
自定义方法默认read_only=True, 改变不了, 你看源码就知道了!!
# 中间件拦截预检
★★★
截图中的标注 第三点:过滤
在里面对request.user进行了一个判断,如果为空,就返回queryset. 我们再把整个过程进行分析下!!
core跨域问题分为 简单请求 & 复杂请求
当请求为复杂请求时(添加了请求头、请求体的数据为json都是复杂请求) 会先发送一个OPTION预检请求,接着再发送真正的请求!
所以我们得分析这个OPTION预检经历了什么?
- 预检先到了中间件,直接就放行了
from django.utils.deprecation import MiddlewareMixin
class AuthMiddleware(MiddlewareMixin):
def process_response(self, request, response):
response["Access-Control-Allow-Origin"] = "*"
response["Access-Control-Allow-Methods"] = "*" # "PUT,DELETE,GET,POST"
response["Access-Control-Allow-Headers"] = "*"
return response
- 开始执行drf中 dispatch里的 认证、权限、限流等
这里,我们只写了个认证组件.在我们写的认证组件中,我们进行了判断
class JwtAuthentication(BaseAuthentication):
def authenticate(self, request):
if request.method == "OPTIONS":
return
回忆下,在认证组件里直接return None意味着什么?意味着继续执行下一个认证类,这里没有其他认证类了.
那么就意味着,支持匿名用户访问,request.user=None request.auth=None
- 往后,就开始执行视图函数了.
在视图函数代码中,self.get_object()对应的源码会调用我们写的过滤类.
因为此时是匿名用户访问,request.user=None 所以不在过滤类里加那个判断的话,request.user["user_id"]肯定会报错!!
★ 那有什么方法一劳永逸,让预检请求压根不用经历disptch哪些步骤、压根走不到视图函数?!
▲ 当然,你可以在中间件里直接将预检拦截!!
from django.utils.deprecation import MiddlewareMixin
from django.http import HttpResponse
class AuthMiddleware(MiddlewareMixin):
def process_request(self, request):
if request.method == "OPTIONS":
return HttpResponse("")
def process_response(self, request, response):
response["Access-Control-Allow-Origin"] = "*"
response["Access-Control-Allow-Methods"] = "*" # "PUT,DELETE,GET,POST"
response["Access-Control-Allow-Headers"] = "*"
return response
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 基本信息-更新手机号
同样的, 点击手机号一栏的 修改按钮, 弹出一个弹出框!
先来康康效果
# 后端 - 重写UpdateModelMixin
后端关键代码, 注意的点 在截图中都已经批注了!!
old、moblie、new_mobile 你细品!
# 前端关键代码
截图如下
# 补充说明
一个小小的bug.
供应链项目的 更新手机号 有bug..
如果我用postman向api地址发送patch请求.. 只传mobile字段进去..
尽管后端报错了(报错是因为ser.data序列化返回时候 数据库中没有old字段的值,找不到报的错...)
但是 数据库中的mobile字段值依旧能更新成功...
我理了下逻辑,看序列化器类,old和mobile字段是可验证的必填字段 前端是put请求的话,前端必传这两个字段,但因为是patch请求,是局部更新
所以前端可以只用传mobile字段,正因为old字段没传,所以后端也不会校验old字段的.只会验证mobile字段..
但这并不影响mobile字段的更新(old字段不是数据表中的字段)
2
3
4
5
6
7
解决方案:
[方案1]
def validate_mobile(self,val):
old = self.initial_data.get('old')
if not old:
raise exceptions.ValidationError("原手机号必须传递")
return val
[方案2] 推荐 在视图类中重写partial_update方法!
尽管前端是patch请求,这样操作后,相当于走了put的逻辑..使得old字段前端必须传!!
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = False
return self.update(request, *args, **kwargs)
2
3
4
5
6
7
8
9
10
11
12