day101:MoFang:模型構造器ModelSchema&註冊功能之手機號唯一驗證/儲存使用者註冊資訊/傳送簡訊驗證碼

Poke發表於2020-12-02

目錄

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>

 

相關文章