day111:MoFang:邀请好友流程&生成邀请好友二维码&第三方应用识别二维码&本地编译测试&记录邀请人信息
目录
6.客户端通过第三方识别微信二维码,服务端提供对应的接口允许访问
8.App配置私有协议, 允许第三方应用通过私有协议,唤醒APP
10.首页监听是否有来自第三方应用的唤醒:app_listener
12.对于后端注册接口(User.register):增加invite_uid的处理
1.邀请业务逻辑流程图
2.邀请好友-前端
<!DOCTYPE html> <html> <head> <title>邀请好友</title> <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"> <meta charset="utf-8"> <link rel="stylesheet" href="../static/css/main.css"> <script src="../static/js/vue.js"></script> <script src="../static/js/axios.js"></script> <script src="../static/js/main.js"></script> <script src="../static/js/uuid.js"></script> <script src="../static/js/settings.js"></script> </head> <body> <div class="app frame avatar" id="app"> <div class="box"> <p class="title">邀请好友</p> <img class="close" @click="close_frame" src="../static/images/close_btn1.png" alt=""> <div class="content"> <img class="invite_code" src="../static/images/code.jpg" alt=""> </div> <p class="invite_tips">长按保存图片到相册</p> </div> </div> <script> apiready = function(){ init(); new Vue({ el:"#app", data(){ return { prev:{name:"",url:"",params:{}}, current:{name:"invite",url:"invite.html",params:{}}, } }, methods:{ close_frame(){ this.game.outFrame("invite"); }, } }); } </script> </body> </html>
.invite_code{ width: 14rem; height: 14rem; position: absolute; left: 7rem; top: 11rem; } .invite_tips{ position: absolute; left: 7rem; top: 26.4rem; text-align: center; color: #fff; font-size: 1.5rem; }
<!DOCTYPE html> <html> <head> <title>用户中心</title> <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"> <meta charset="utf-8"> <link rel="stylesheet" href="../static/css/main.css"> <script src="../static/js/vue.js"></script> <script src="../static/js/axios.js"></script> <script src="../static/js/main.js"></script> <script src="../static/js/uuid.js"></script> <script src="../static/js/settings.js"></script> </head> <body> <div class="app user" id="app"> <div class="bg"> <img src="../static/images/bg0.jpg"> </div> <img class="back" @click="goto_index" src="../static/images/user_back.png" alt=""> <img class="setting" @click="goto_setting" src="../static/images/setting.png" alt=""> <div class="header"> <div class="info"> <div class="avatar"> <img class="avatar_bf" src="../static/images/avatar_bf.png" alt=""> <img class="user_avatar" :src="avatar" alt=""> <img class="avatar_border" src="../static/images/avatar_border.png" alt=""> </div> <p class="user_name">{{nickname}}</p> </div> <div class="wallet"> <div class="balance"> <p class="title"><img src="../static/images/money.png" alt="">钱包</p> <p class="num">99,999.00</p> </div> <div class="balance"> <p class="title"><img src="../static/images/integral.png" alt="">果子</p> <p class="num">99,999.00</p> </div> </div> <div class="invite" @click="open_invite_page"> <img class="invite_btn" src="../static/images/invite.png" alt=""> </div> </div> <div class="menu"> <div class="item" @click="open_friend_list"> <span class="title">好友列表</span> <span class="value">查看</span> </div> <div class="item"> <span class="title">我的主页</span> <span class="value">查看</span> </div> <div class="item"> <span class="title">任务列表</span> <span class="value">75%</span> </div> <div class="item"> <span class="title">收益明细</span> <span class="value">查看</span> </div> <div class="item"> <span class="title">实名认证</span> <span class="value">未认证</span> </div> </ul> </div> </div> <script> apiready = function(){ init(); new Vue({ el:"#app", data(){ return { nickname:"", avatar:"", prev:{name:"",url:"",params:{}}, current:{name:"user",url:"user.html",params:{}}, } }, created(){ this.get_user_info(); this.change_avatar(); }, methods:{ open_invite_page(){ // ***打开邀请好友页面*** this.game.goFrame("invite","invite.html", this.current,null,{ type:"push", //动画类型(详见动画类型常量) subType:"from_top", //动画子类型(详见动画子类型常量) duration:300 //动画过渡时间,默认300毫秒 }); }, open_friend_list(){ // 打开好友列表主框架页面 // this.game.goWin("friends","friends.html",this.current); // 打开好友列表数据页面 this.game.goFrame("friends","friends.html",this.current); this.game.goFrame("friend_list","friend_list.html",this.current,{ x: 0, y: 190, w: 'auto', h: 'auto', },null,true); }, change_avatar(){ api.addEventListener({ name: 'change_avatar' }, (ret, err)=>{ if( ret ){ var token = this.game.get("access_token") || this.game.fget("access_token"); this.avatar = `${this.settings.avatar_url}?sign=${ret.value.avatar}&token=${token}`; } }); }, get_user_info(){ var token = this.game.get("access_token") || this.game.fget("access_token"); // 获取当前登陆用户基本信息 this.axios.post("",{ "jsonrpc": "2.0", "id": this.uuid(), "method": "User.info", "params": {} },{ headers:{ Authorization: "jwt " + token, } }).then(response=>{ var res = response.data.result; this.game.print(res); if(parseInt(res.errno) === 1000){ this.nickname = res.nickname; this.avatar = `${this.settings.avatar_url}?sign=${res.avatar}&token=${token}`; } }) }, goto_index(){ // 返回首页 this.game.outWin("user"); }, goto_setting(){ // 进入设置 this.game.goFrame("setting","setting.html", this.current); } } }); } </script> </body> </html>
3.邀请好友-后端接口(生成二维码)
安装二维码生成模块
pip install flask-qrcode
from flask_qrcode import QRcode # qrcode QRCode = QRcode() def init_app(config_path): """全局初始化""" # qrcode初始化配置 QRCode.init_app(app) return manager
from application import QRCode from flask import make_response,request @jwt_required # 验证jwt def invite_code(): """邀请好友的二维码""" current_user_id = get_jwt_identity() user = User.query.get(current_user_id) if user is None: return { "errno": status.CODE_NO_USER, "errmsg": message.user_not_exists, } static_path = os.path.join(current_app.BASE_DIR, current_app.config["STATIC_DIR"]) # 如果用户头像不存在,则使用默认头像 if not user.avatar: user.avatar = current_app.config["DEFAULT_AVATAR"] avatar = static_path +"/"+ user.avatar data = current_app.config.get("SERVER_URL",request.host_url[:-1])+"/users/invite/download?uid=%s" % current_user_id # http://127.0.0.1:5000/users/invite/download?uid=15 image = QRCode.qrcode(data,box_size=16,icon_img=avatar) # 根据用户头像生成二维码 b64_image = image[image.find(",")+1:] qrcode_iamge = base64.b64decode(b64_image) response = make_response(qrcode_iamge) response.headers["Content-Type"] = "image/png" return response
from . import views from application.utils import path urlpatterns = [ path("/avatar", views.avatar), path("/invite/code", views.invite_code), path("/invite/download", views.invite_download), ]
# 用户默认头像 DEFAULT_AVATAR = "95822582-39d8-43ce-9498-fdced7f6a144.jpeg" # 服务端带外提供的url地址 SERVER_URL = "http://127.0.0.1:5000"
<!DOCTYPE html> <html> <head> <title>邀请好友</title> <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"> <meta charset="utf-8"> <link rel="stylesheet" href="../static/css/main.css"> <script src="../static/js/vue.js"></script> <script src="../static/js/axios.js"></script> <script src="../static/js/main.js"></script> <script src="../static/js/uuid.js"></script> <script src="../static/js/settings.js"></script> </head> <body> <div class="app frame avatar" id="app"> <div class="box"> <p class="title">邀请好友</p> <img class="close" @click="close_frame" src="../static/images/close_btn1.png" alt=""> <div class="content"> <img class="invite_code" :src="code_url" alt=""> </div> <p class="invite_tips">长按保存图片到相册</p> </div> </div> <script> apiready = function(){ init(); new Vue({ el:"#app", data(){ return { code_url:"", //二维码url地址 prev:{name:"",url:"",params:{}}, current:{name:"invite",url:"invite.html",params:{}}, } }, created(){ this.get_qrcode(); }, methods:{ get_qrcode(){ // ***获取二维码*** var token = this.game.get("access_token") || this.game.fget("access_token"); this.code_url = `${this.settings.code_url}/users/invite/code?token=${token}`; }, close_frame(){ this.game.outFrame("invite"); }, } }); } </script> </body> </html>
<!DOCTYPE html> <html> <head> <title>邀请好友</title> <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"> <meta charset="utf-8"> <link rel="stylesheet" href="../static/css/main.css"> <script src="../static/js/vue.js"></script> <script src="../static/js/axios.js"></script> <script src="../static/js/main.js"></script> <script src="../static/js/uuid.js"></script> <script src="../static/js/settings.js"></script> </head> <body> <div class="app frame avatar" id="app"> <div class="box"> <p class="title">邀请好友</p> <img class="close" @click="close_frame" src="../static/images/close_btn1.png" alt=""> <div class="content"> <img class="invite_code" :src="code_url" alt=""> </div> <p class="invite_tips">长按保存图片到相册</p> </div> </div> <script> apiready = function(){ init(); new Vue({ el:"#app", data(){ return { code_url:"", //二维码url地址 prev:{name:"",url:"",params:{}}, current:{name:"invite",url:"invite.html",params:{}}, } }, created(){ this.get_qrcode(); }, methods:{ get_qrcode(){ // 获取二维码 var token = this.game.get("access_token") || this.game.fget("access_token"); this.code_url = `${this.settings.code_url}/users/invite/code?token=${token}`; // ***监听页面是否被长按*** api.addEventListener({ name:'longpress' }, (ret, err)=>{ api.saveMediaToAlbum({ path: this.code_url }, (ret, err)=> { if (ret && ret.status) { alert('保存成功'); } else { alert('保存失败'); } }); }); }, close_frame(){ this.game.outFrame("invite"); }, } }); } </script> </body> </html>
from flask import render_template def invite_download(): uid = request.args.get("uid") if "micromessenger" in request.headers.get("User-Agent").lower(): # 判断是否是微信识别二维码 position = "weixin" else: position = "other" return render_template("users/download.html",position=position,uid=uid)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"> <meta > <title>Title</title> <style> body{ background-color: #000; } img{ width: 100%; } a{ color: #fff; } </style> </head> <body> {% if position == "weixin" %} <img src="/static/openbrowser.png" alt=""> {% else %} <div id="content"> </div> <script> // 尝试通过打开客户端已经安装的魔方APP var iframe = document.createElement("iframe"); iframe.src = "mofang://?uid={{ uid }}"; // app的私有协议 iframe.hidden=true; document.body.appendChild(iframe); // 如果等待了4秒以后, setTimeout(function() { if (!document.hidden) { // 在4秒内如果页面出去了。说明这个时候document.hidden是true,这段代码就不执行了。 // 就算是再切回来也是不执行的。 // 如果你进了这个函数,没离开。。那就会在4秒后跳进这里 alert('你还没安装魔方APP,去下载去'); u = navigator.userAgent; let isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1; //android终端 let isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端 var content = document.querySelector("#content"); if (isiOS) { // 去下载ios content.innerHTML = `<a href="/static/app/mofang.apk">下载魔方APP</a>`; } if (isAndroid){ // 去下载安卓 content.innerHTML = `<a href="/static/app/mofang.apk">下载魔方APP</a>`; } } }, 5000); </script> {% endif %} </body> </html>
<widget id="A6151729457001" version="0.0.1"> <name>MFdemo</name> <description> Example For APICloud. </description> <author email="developer@apicloud.com" href="http://www.apicloud.com"> Developer </author> <content src="html/index.html" /> <access origin="*" /> <preference name="pageBounce" value="false"/> <preference name="appBackground" value="rgba(0,0,0,0.0)"/> <preference name="windowBackground" value="rgba(0,0,0,0.0)"/> <preference name="frameBackgroundColor" value="rgba(0,0,0,0.0)"/> <preference name="hScrollBarEnabled" value="false"/> <preference name="vScrollBarEnabled" value="false"/> <preference name="autoLaunch" value="true"/> <preference name="fullScreen" value="false"/> <preference name="autoUpdate" value="true" /> <preference name="smartUpdate" value="false" /> <preference name="debug" value="true"/> <preference name="statusBarAppearance" value="true"/> <permission name="readPhoneState" /> <permission name="camera" /> <permission name="record" /> <permission name="location" /> <permission name="fileSystem" /> <permission name="internet" /> <permission name="bootCompleted" /> <permission name="hardware" /> <preference name="urlScheme" value="mofang" /> <!-- **允许第三方应用通过私有协议,唤醒APP** --> </widget>
接下来的开发,我们不能再依赖官方提供的Apploader进行功能测试了,
所以我们使用由APICloud编辑器提供的本地编译, 编译自定义APPLoader来进行测试。
print(data,show=false){ // 打印数据 if(show){ alert(JSON.stringify(data)); }else{ console.log(JSON.stringify(data)); } }
html/index.html
,代码:
<!DOCTYPE html> <html lang="en"> <head> <title>首页</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"> <meta name="format-detection" content="telephone=no,email=no,date=no,address=no"> <link rel="stylesheet" href="../static/css/main.css"> <script src="../static/js/vue.js"></script> <script src="../static/js/axios.js"></script> <script src="../static/js/main.js"></script> <script src="../static/js/uuid.js"></script> <script src="../static/js/settings.js"></script> </head> <body> <div class="app" id="app"> <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png"> <div class="bg"> <img src="../static/images/bg0.jpg"> </div> <ul> <li><img class="module1" src="../static/images/image1.png"></li> <li><img class="module2" @click="gohome" src="../static/images/image2.png"></li> <li><img class="module3" src="../static/images/image3.png"></li> <li><img class="module4" src="../static/images/image4.png"></li> </ul> </div> <script> apiready = function(){ init(); new Vue({ el:"#app", data(){ return { music_play:true, // 默认播放背景音乐 prev:{name:"",url:"",params:{}}, // 上一页状态 current:{name:"index",url:"index.html","params":{}}, // 下一页状态 } }, watch:{ music_play(){ if(this.music_play){ this.game.play_music("../static/mp3/bg1.mp3"); }else{ this.game.stop_music(); } } }, created(){ this.app_listener(); this.check_user_login(); }, methods:{ // **监听是否有来自第三方应用的唤醒** app_listener(){ // 使用appintenr监听并使用appParam接收URLScheme的参数 // 收集操作保存起来,并跳转到注册页面. // 注册frame中, 用户注册成功以后,记录邀请信息. api.addEventListener({ name:'appintent' // 当前事件监听必须是唯一的,整个APP中只能编写一次,否则冲突导致监听无效 },(ret,err)=>{ var appParam = ret.appParam; this.game.print(typeof appParam); // {"uid":"15"} // 保存URLScheme参数到本地 this.game.fsave(appParam); // 跳转到注册页面 this.game.goWin("user","register.html", this.current); }); }, check_user_login(){ let token = this.game.get("access_token") || this.game.fget("access_token"); this.game.checkout(this, token, (new_access_token)=>{ if(new_access_token.errno == 1005){ this.game.save({"access_token":""}); this.game.fremove("access_token"); } }); }, gohome(){ if(this.game.get("access_token") || this.game.fget("access_token")){ this.game.goWin("user","user.html", this.current); }else{ this.game.goWin("user","login.html", this.current); } } } }) } </script> </body> </html>
<!DOCTYPE html> <html> <head> <title>注册</title> <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"> <meta charset="utf-8"> <link rel="stylesheet" href="../static/css/main.css"> <script src="../static/js/vue.js"></script> <script src="../static/js/axios.js"></script> <script src="../static/js/main.js"></script> <script src="../static/js/uuid.js"></script> <script src="../static/js/settings.js"></script> </head> <body> <div class="app" id="app"> <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png"> <div class="bg"> <img src="../static/images/bg0.jpg"> </div> <div class="form"> <div class="form-title"> <img src="../static/images/register.png"> <img class="back" @click="back" src="../static/images/back.png"> </div> <div class="form-data"> <div class="form-data-bg"> <img src="../static/images/bg1.png"> </div> <div class="form-item"> <label class="text">手机</label> <input type="text" v-model="mobile" @change="check_mobile" placeholder="请输入手机号"> </div> <div class="form-item"> <label class="text">验证码</label> <input type="text" class="code" v-model="sms_code" placeholder="请输入验证码"> <img class="refresh" @click="send" src="../static/images/refresh.png"> </div> <div class="form-item"> <label class="text">密码</label> <input type="password" v-model="password" placeholder="请输入密码"> </div> <div class="form-item"> <label class="text">确认密码</label> <input type="password" v-model="password2" placeholder="请再次输入密码"> </div> <div class="form-item"> <input type="checkbox" class="agree" v-model="agree" checked> <label><span class="agree_text">同意磨方《用户协议》和《隐私协议》</span></label> </div> <div class="form-item"> <img class="commit" @click="registerHandle" src="../static/images/commit.png"/> </div> </div> </div> </div> <script> apiready = function(){ init(); new Vue({ el:"#app", data(){ return { is_send: false, send_interval: 60, // 短信发送冷却时间 mobile:"", password: "", password2: "", sms_code:"", agree:false, music_play:true, prev:{name:"",url:"",params:{}}, current:{name:"register",url:"register.html","params":{}}, } }, watch:{ music_play(){ if(this.music_play){ this.game.play_music("../static/mp3/bg1.mp3"); }else{ this.game.stop_music(); } } }, methods:{ send(){ // 点击发送短信 if (!/1[3-9]\d{9}/.test(this.mobile)){ api.alert({ title: "警告", msg: "手机号码格式不正确!", }); return; // 阻止代码继续往下执行 } if(this.is_send){ api.alert({ title: "警告", msg: `短信发送冷却中,请${this.send_interval}秒之后重新点击发送!`, }); return; // 阻止代码继续往下执行 } this.axios.post("",{ "jsonrpc": "2.0", "id": this.uuid(), "method": "Home.sms", "params": { "mobile": this.mobile, } }).then(response=>{ if(response.data.result.errno != 1000){ api.alert({ title: "错误提示", msg: response.data.result.errmsg, }); }else{ this.is_send=true; // 进入冷却状态 this.send_interval = 60; var timer = setInterval(()=>{ this.send_interval--; if(this.send_interval<1){ clearInterval(timer); this.is_send=false; // 退出冷却状态 } }, 1000); } }).catch(error=>{ this.game.print(error.response); }); }, registerHandle(){ // 注册处理 this.game.play_music('../static/mp3/btn1.mp3'); // 验证数据[双向验证] if (!/1[3-9]\d{9}/.test(this.mobile)){ api.alert({ title: "警告", msg: "手机号码格式不正确!", }); return; // 阻止代码继续往下执行 } if(this.password.length<3 || this.password.length > 16){ api.alert({ title: "警告", msg: "密码长度必须在3-16个字符之间!", }); return; } if(this.password != this.password2){ api.alert({ title: "警告", msg: "密码和确认密码不匹配!", }); return; // 阻止代码继续往下执行 } if(this.sms_code.length<1){ api.alert({ title: "警告", msg: "验证码不能为空!", }); return; // 阻止代码继续往下执行 } if(this.agree === false){ api.alert({ title: "警告", msg: "对不起, 必须同意磨方的用户协议和隐私协议才能继续注册!", }); return; // 阻止代码继续往下执行 } var invite_uid = 0; var uid = this.game.fget("uid"); // {"uid":"15"} if(uid>0){ invite_uid = uid; } this.axios.post("",{ "jsonrpc": "2.0", "id": this.uuid(), "method": "User.register", "params": { "mobile": this.mobile, "sms_code":this.sms_code, "password":this.password, "password2":this.password2, "invite_uid": invite_uid //**注册页面接收invite_uid参数** } }).then(response=>{ this.game.print(response.data.result); if(response.data.result.errno != 1000){ api.alert({ title: "错误提示", msg: response.data.result.errmsg, }); }else{ // 注册成功! api.confirm({ title: '磨方提示', msg: '注册成功', buttons: ['返回首页', '个人中心'] }, (ret, err)=>{ if(ret.buttonIndex == 1){ // 跳转到首页 this.game.outWin("user"); }else{ // 删除邀请人 this.game.femove("uid"); // 跳转到个人中心 this.game.goFrame("user","user.html", this.current); } }); } }).catch(error=>{ this.game.print(error.response); }); }, check_mobile(){ // 验证手机号码 this.axios.post("",{ "jsonrpc": "2.0", "id": this.uuid(), "method": "User.mobile", "params": {"mobile": this.mobile} }).then(response=>{ this.game.print(response.data.result); if(response.data.result.errno != 1000){ api.alert({ title: "错误提示", msg: response.data.result.errmsg, }); } }).catch(error=>{ this.game.print(error.response.data.error); }); }, back(){ // this.game.outWin(); // this.game.outFrame(); this.game.goGroup("user",0); } } }) } </script> </body> </html>
@jsonrpc.method("User.register") def register(mobile,password,password2, sms_code,invite_uid): """用户信息注册""" try: ms = MobileSchema() ms.load({"mobile": mobile}) us = UserSchema() user = us.load({ "mobile":mobile, "password":password, "password2":password2, "sms_code": sms_code, "invite_uid": invite_uid, # **** }) data = {"errno": status.CODE_OK,"errmsg":us.dump(user)} except ValidationError as e: data = {"errno": status.CODE_VALIDATE_ERROR,"errmsg":e.messages} return data
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema,auto_field from marshmallow import post_load,pre_load,validates_schema from application import redis class UserSchema(SQLAlchemyAutoSchema): mobile = auto_field(required=True, load_only=True) password = fields.String(required=True, load_only=True) password2 = fields.String(required=True, load_only=True) sms_code = fields.String(required=True, load_only=True) invite_uid = fields.Integer(required=True, load_only=True) # ****** class Meta: model = User include_fk = True # 启用外键关系 include_relationships = True # 模型关系外部属性 fields = ["id", "name","mobile","password","password2","sms_code","invite_uid"] # 如果要全换全部字段,就不要声明fields或exclude字段即可 sql_session = db.session @post_load() def save_object(self, data, **kwargs): invite_uid = int( data["invite_uid"] ) data.pop("password2") data.pop("sms_code") data.pop("invite_uid") # ***** data["name"] = data["mobile"] instance = User(**data) db.session.add( instance ) db.session.commit() # ***记录邀请信息到Mongdb中*** if invite_uid > 0: """只有invite_uid大于0,才是经过邀请注册进来的新用户""" # 验证是否属于有效的邀请 invite_user = User.query.get(invite_uid) if invite_user is not None: """只有邀请人存在的情况下才算有效邀请""" query = {"_id":invite_uid} ret = mongo.db.user_invite_list.find_one(query) if ret: mongo.db.user_invite_list.update(query,{"$push":{"invite_list":instance.id}}) else: data = {"_id": invite_uid, "invited_list": [instance.id]} mongo.db.user_invite_list.insert(data) # 添加好友关系 return instance @validates_schema def validate(self,data, **kwargs): # 校验密码和确认密码 if data["password"] != data["password2"]: raise ValidationError(message=Message.password_not_match,field_name="password") #todo 校验短信验证码 #1. 从redis中提取验证码 redis_sms_code = redis.get("sms_%s" % data["mobile"]) if redis_sms_code is None: raise ValidationError(message=Message.sms_code_expired,field_name="sms_code") redis_sms_code = redis_sms_code.decode() #2. 从客户端提交的数据data中提取验证码 sms_code = data["sms_code"] #3. 字符串比较,如果失败,则抛出异常,否则,直接删除验证码 if sms_code != redis_sms_code: raise ValidationError(message=Message.sms_code_error, field_name="sms_code") redis.delete("sms_%s" % data["mobile"]) return data