day101:MoFang:模型構造器ModelSchema&註冊功能之手機號惟一驗證/保存用戶註冊信息/發送短信驗證碼

目錄

1.模型構造器:ModelSchemahtml

  1.SQLAlchemySchema前端

  2.SQLAlchemyAutoSchemaios

2.註冊功能基本實現git

  1.關於手機號碼的惟一性驗證github

  2.保存用戶註冊信息redis

  3.發送短信驗證碼sql

1.模型構造器:ModelSchema

官方文檔:https://github.com/marshmallow-code/marshmallow-sqlalchemy數據庫

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

注意:flask_marshmallow在0.12.0版本之後已經移除了ModelSchema和TableSchema這兩個模型構造器類,官方轉而推薦了使用SQLAlchemyAutoSchema和SQLAlchemySchema這2個類,先後二者用法相似。flask

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>
相關文章
相關標籤/搜索