day101:MoFang:模型构造器ModelSchema&注册功能之手机号唯一验证/保存用户注册信息/发送短信验证码

目录

1.模型构造器:ModelSchema

  1.SQLAlchemySchema

  2.SQLAlchemyAutoSchema

2.注册功能基本实现

  1.关于手机号码的唯一性验证

  2.保存用户注册信息

  3.发送短信验证码

1.模型构造器:ModelSchema

官方文档:https://github.com/marshmallow-code/marshmallow-sqlalchemy

​       https://marshmallow-sqlalchemy.readthedocs.io/en/latest/

注意:flask_marshmallow在0.12.0版本以后已经移除了ModelSchema和TableSchema这两个模型构造器类,官方转而推荐了使用SQLAlchemyAutoSchema和SQLAlchemySchema这2个类,前后两者用法类似。

1.SQLAlchemySchema

from marshmallow_sqlalchemy import SQLAlchemySchema,SQLAlchemyAutoSchema,auto_field
class UserSchema5(SQLAlchemySchema):
    # auto_field的作用,设置当前数据【字段的类型】和【选项声明】自动从模型中对应的字段中提取
    # name = auto_field()
    # 1.此处,数据库中根本没有username,需要在第一个参数位置,声明当前数据字典的类型和选项声明从模型的哪个字段提取的
    username = auto_field("name",dump_only=True)
    
    # 2.可以在原字段基础上面,增加或者覆盖模型中原来的声明
    created_time = auto_field(format="%Y-%m-%d")
    
    # 3.甚至可以声明一些不是模型的字段
    token = fields.String()
    class Meta:
        model = User
        fields = ["username","created_time","token"]

def index5():
    """单个模型数据的序列化处理"""
    from datetime import datetime
    user1 = User(
        name="xiaoming",
        password="123456",
        age=16,
        email="333@qq.com",
        money=31.50,
        created_time= datetime.now(),
    )
    user1.token = "abc"
    # 把模型对象转换成字典格式
    data1 = UserSchema5().dump(user1)
    print(type(data1),data1)
    return "ok"

总结

1.auto_field用来设置字段的类型和选项声明

2.auto_field的第一个参数含义:给字段设置别名

3.auto_field可以给原字段增加或覆盖模型中原来的声明

4.甚至可以声明一些非模型内的字段:直接模型类对象.属性即可

2.SQLAlchemyAutoSchema

SQLAlchemySchema使用起来,虽然比上面的Schema简单许多,但是还是需要给转换的字段全部统一写上才转换这些字段

如果不想编写字段信息,直接从模型中复制,也可以使用SQLAlchemyAutoSchema。

class UserSchema6(SQLAlchemyAutoSchema):
    token = fields.String()
    class Meta:
        model = User
        include_fk = False # 启用外键关系
        include_relationships = False # 模型关系外部属性
        fields = ["name","created_time","info","token"] # 如果要全换全部字段,就不要声明fields或exclude字段即可
        sql_session = db.session

def index():
    """单个模型数据的序列化处理"""
    from datetime import datetime
    user1 = User(
        name="xiaoming",
        password="123456",
        age=16,
        email="333@qq.com",
        money=31.50,
        created_time= datetime.now(),
        info=UserProfile(position="助教")
    )
    # 把模型对象转换成字典格式
    user1.token="abcccccc"
    data1 = UserSchema6().dump(user1)
    print(type(data1),data1)
    return "ok"

总结

1.不需要编写字段信息

2.相关设置全部写在class Meta中

2.注册功能基本实现

1.关于手机号码的唯一性验证

1.验证手机号码唯一的接口

在开发中,针对客户端提交的数据进行验证或提供模型数据转换格式成字典给客户端。可以使用Marshmallow模块来进行。

application.apps.users.views,代码:

from application import jsonrpc,db
from .marshmallow import MobileSchema
from marshmallow import ValidationError
from message import ErrorMessage as Message
from status import APIStatus as status
@jsonrpc.method("User.mobile")
def mobile(mobile):
    """验证手机号码是否已经注册"""
    ms = MobileSchema()
    try:
        ms.load({"mobile":mobile}) # 对用户在前端输入的手机号进行反序列化校验
        ret = {"errno":status.CODE_OK, "errmsg":Message.ok}
    except ValidationError as e:
        ret = {"errno":status.CODE_VALIDATE_ERROR, "errmsg": e.messages["mobile"][0]}
    return ret

application.apps.users.marshmallow,代码:

from marshmallow import Schema,fields,validate,validates,ValidationError
from message import ErrorMessage as Message
from .models import User
class MobileSchema(Schema):
    '''验证手机号所使用的序列化器'''
    # 1.验证手机号格式是否正确
    mobile = fields.String(required=True,validate=validate.Regexp("^1[3-9]\d{9}$",error=Message.mobile_format_error))

    # 2.验证手机号是否已经存在(被注册过了)
    @validates("mobile")
    def validates_mobile(self,data):
        user = User.query.filter(User.mobile==data).first()
        if user is not None:
            raise ValidationError(message=Message.mobile_is_use)
        return data

状态码和配置信息单独放到utils的language文件夹中,便于更改

application.utils.language.status代码:

class APIStatus():
    CODE_OK = 1000 # 接口操作成功
    CODE_VALIDATE_ERROR = 1001 # 验证有误!

application.utils.language.message.代码:

class ErrorMessage():
    ok = "ok"
    mobile_format_error = "手机号码格式有误!"
    mobile_is_use = "对不起,当前手机已经被注册!"

Tip:将language作为导包路径进行使用

为了方便导包,所以我们设置当前language作为导包路径进行使用.

application/__init__.py,代码:

def init_app(config_path):
    """全局初始化"""
    ......  

    app.BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

    # 加载导包路径
    sys.path.insert(0, os.path.join(app.BASE_DIR,"application/utils/language"))

    ......

2.客户端发送请求进行手机号验证

在客户端输入手机号,触发check_mobile方法 向后端发起请求,进行手机号验证

html/register.html代码

<div class="form-item">
    <label class="text">手机</label>
    <input type="text" v-model="mobile" @change="check_mobile" placeholder="请输入手机号">
</div>

        
<script>

    methods:{
        check_mobile(){
            // 验证手机号码
            this.axios.post("",{
                "jsonrpc": "2.0",
                "id":1,
                "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.goGroup("user",0);
            }
    }
    })
    }
</script>

在客户单请求过程中, 我们需要设置id作为唯一标识, 同时, 将来在客户端项目中多个页面都会继续使用到上面的初始化代码,所以我们一并抽取这部分代码到另一个static/js/settings文件中.

static/js/settings代码

function init(){
  var game = new Game("../mp3/bg1.mp3");
  Vue.prototype.game = game;
  // 初始化axios
  axios.defaults.baseURL = "http://192.168.20.251:5000/api" // 服务端api接口网关地址
  axios.defaults.timeout = 2500; // 请求超时时间
  axios.defaults.withCredentials = false; // 跨域请求资源的情况下,忽略cookie的发送
  Vue.prototype.axios = axios;
  Vue.prototype.uuid  = UUID.generate;
}

注册页面调用init函数进行初始化.

html/register.html代码

<div class="form-item">
    <label class="text">手机</label>
    <input type="text" v-model="mobile" @change="check_mobile" placeholder="请输入手机号">
</div>
                
<script>
    apiready = function(){
        init();
        new Vue({
            ....
            methods:{
            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.goGroup("user",0);
    }
    }
    })
    }
</script>

2.保存用户注册信息

1.保存用户注册信息接口

application.apps.users.marshmallow,代码:

from marshmallow import Schema,fields,validate,validates,ValidationError
from message import ErrorMessage as Message
from .models import User,db
class MobileSchema(Schema):
     ......

from marshmallow_sqlalchemy import SQLAlchemyAutoSchema,auto_field
from marshmallow import post_load,pre_load,validates_schema
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)
    
    class Meta:
        model = User
        include_fk = True # 启用外键关系
        include_relationships = True # 模型关系外部属性
        fields = ["id", "name","mobile","password","password2","sms_code"] # 如果要全换全部字段,就不要声明fields或exclude字段即可
        sql_session = db.session

    @post_load()
    def save_object(self, data, **kwargs):
        # 确认密码和验证码这两个字段并不需要存到数据库中
        data.pop("password2")
        data.pop("sms_code")
        
        # 注册成功后,用户默认的名称为自己的手机号
        data["name"] = data["mobile"]
        
        # 创建User模型类对象
        instance = User(**data)
        
        # 将注册信息保存到数据库中
        db.session.add( instance )
        db.session.commit()
        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 校验短信验证码

        return data

application.apps.users.views,代码:

@jsonrpc.method("User.register")
def register(mobile,password,password2, sms_code):
    """用户信息注册"""

    try:
        ms = MobileSchema()
        ms.load({"mobile": mobile})

        us = UserSchema()
        user = us.load({
            "mobile":mobile,
            "password":password,
            "password2":password2,
            "sms_code": sms_code
        })
        data = {"errno": status.CODE_OK,"errmsg":us.dump(user)}
    except ValidationError as e:
        data = {"errno": status.CODE_VALIDATE_ERROR,"errmsg":e.messages}
    return data

application.utils.language.message,代码:

class ErrorMessage():
    ok = "ok"
    mobile_format_error = "手机号码格式有误!"
    mobile_is_use = "对不起,当前手机已经被注册!"
    username_is_use = "对不起,当前用户名已经被使用!"
    password_not_match = "密码和验证密码不匹配!"

2.用户点击立即注册按钮向后端发送请求

html/register.html代码

用户点击注册按钮,向后端发起请求

1.在用户点击注册按钮向后端发送请求之前,要对手机号/密码/确认密码和验证码做前端的校验,如果出错了,直接显示错误弹窗

2.当前端验证通过后,就可以向后端发起请求了。如果成功了,直接显示跳转弹窗,失败了则显示错误弹窗

<div class="form-item">
    <label class="text">手机</label>
    <input type="text" v-model="mobile" @change="check_mobile" placeholder="请输入手机号">
</div>

<div class="form-item">
    <img class="commit" @click="registerHandle" src="../static/images/commit.png"/>
</div>
            
    <script>
    apiready = function(){
        init();
        new Vue({
            ...
            methods:{
                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; // 阻止代码继续往下执行
                    }

                    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,
                        }
                    }).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.outGroup("user");
                                    }else{
                                        // 跳转到个人中心
                                        this.game.goGroup("user",2);
                                    }
                            });

                        }

                    }).catch(error=>{
                        this.game.print(error.response.data.error);
                    });

                },
                check_mobile(){
                    .....
                },
                back(){

          this.game.goGroup("user",0);
                }
            }
        })
    }
    </script>

因为在注册成功时,会有一个跳转到个人中心页面的选项,所以我们需要在帧页面组中新增一个帧:user.html

html/index.html,frames帧页面组中新增用户中心页面的user.html,代码:

<script>
    apiready = function(){
        frames: [{
            name: 'login',
            url:   './login.html',
        },{
            name: 'register',
            url:   './register.html',
        },{
            name: 'user',
            url:   './user.html',
        }]
    }
</script>

3.发送短信验证码

1.服务端实现发送短信验证码的api接口

application.settings.dev,配置文件中填写短信接口相关配置,代码:

 

# 短信相关配置
SMS_ACCOUNT_ID = "8a216da8754a45d5017563ac8e8406ff" # 接口主账号
SMS_ACCOUNT_TOKEN = "a2054f169cbf42c8b9ef2984419079da" # 认证token令牌
SMS_APP_ID = "8a216da8754a45d5017563ac8f910705" # 应用ID
SMS_TEMPLATE_ID = 1 # 短信模板ID
SMS_EXPIRE_TIME = 60 * 5 # 短信有效时间,单位:秒/s
SMS_INTERVAL_TIME = 60 # 短信发送冷却时间,单位:秒/s

短信属于公共业务,所以在此我们把功能接口写在Home蓝图下,application.apps.home.views,代码:

from application import jsonrpc
import re,random,json
from status import APIStatus as status
from message import ErrorMessage as message
from ronglian_sms_sdk import SmsSDK
from flask import current_app
from application import redis
@jsonrpc.method(name="Home.sms")
def sms(mobile):
    """发送短信验证码"""
    # 1.验证手机号是否符合规则
    if not re.match("^1[3-9]\d{9}$",mobile):
        return {"errno": status.CODE_VALIDATE_ERROR, "errmsg": message.mobile_format_error}

    # 2.短信发送冷却时间
    ret = redis.get("int_%s" % mobile)
    if ret is not None:
        return {"errno": status.CODE_INTERVAL_TIME, "errmsg": message.sms_interval_time}

    # 3.通过随机数生成验证码
    sms_code = "%06d" % random.randint(0,999999)
    
    # 4.发送短信
    sdk = SmsSDK(
        current_app.config.get("SMS_ACCOUNT_ID"),
        current_app.config.get("SMS_ACCOUNT_TOKEN"),
        current_app.config.get("SMS_APP_ID")
    )
    ret = sdk.sendMessage(
        current_app.config.get("SMS_TEMPLATE_ID"),
        mobile,
        (sms_code, current_app.config.get("SMS_EXPIRE_TIME") // 60)
    )
    result = json.loads(ret)
    if result["statusCode"] == "000000":
        pipe = redis.pipeline()
        pipe.multi()  # 开启事务
        
        # 保存短信记录到redis中
        pipe.setex("sms_%s" % mobile,current_app.config.get("SMS_EXPIRE_TIME"),sms_code)
        
        # 进行冷却倒计时
        pipe.setex("int_%s" % mobile,current_app.config.get("SMS_INTERVAL_TIME"),"_")
        
        pipe.execute() # 提交事务
        
        # 返回结果
        return {"errno":status.CODE_OK, "errmsg": message.ok}
    else:
        return {"errno": status.CODE_SMS_ERROR, "errmsg": message.sms_send_error}

2.客户端实现发送短信

客户端点击获取验证码按钮,向后端发起请求,让后端访问ronglianyun发送短信验证码

<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>


    <script>
    apiready = function(){
        init();
        new Vue({
            
            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.data.error);
                    });
                },
            
    </script>

 

posted @ 2020-12-02 21:56  iR-Poke  阅读(532)  评论(0编辑  收藏  举报