flask-03
我们当前开发的项目属于社交类型项目,所以关于用户的信息和功能直接贯穿了整个项目。所以此处实现用户模块功能,我们先把用户基本信息构建起来,并通过基本信息实现用户注册登录相关功能,后面遇到业务再继续扩展。
用户注册
创建并注册用户蓝图
先删除原来编写在apps/home蓝图下的测试视图home.views和测试模型代码home.models,当然数据库中的测试数据和表结构也要删除。
创建并注册用户蓝图以及路由信息。
./command.sh blue users
application/settings/dev.py,代码:
from typing import List """蓝图列表""" INSTALL_BLUEPRINT: List = [ "application.apps.home", "application.apps.users", ]
application.urls,代码:
from typing import List from application import path urlpatterns: List = [ path("/home", "home.urls"), path("/users", "users.urls"), ]
提交版本
git add . git commit -m "delete home any codes and create users's module" git push git checkout -b feature/users
用户相关模型
application/utils/models.py,代码:
from application import db from datetime import datetime class BaseModel(db.Model): """公共模型""" __abstract__ = True # 抽象模型 id = db.Column(db.Integer, primary_key=True, comment="主键ID") name = db.Column(db.String(255), default="", comment="名称/标题") orders = db.Column(db.Integer, default=0, comment="排序") status = db.Column(db.Boolean, default=True, comment="状态(是否显示,是否激活)") created_time = db.Column(db.DateTime, default=datetime.now, comment="创建时间") updated_time = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间") deleted_time = db.Column(db.DateTime, default=None, comment="删除时间") def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self.name)
application.apps.users.models,代码:
from application.utils.models import BaseModel, db from werkzeug.security import generate_password_hash, check_password_hash class User(BaseModel): """用户基本信息表""" __tablename__ = "ym_user" name = db.Column(db.String(255), index=True, comment="用户账户") nickname = db.Column(db.String(255), comment="用户昵称") _password = db.Column(db.String(255), comment="登录密码") age = db.Column(db.SmallInteger, comment="年龄") money = db.Column(db.Numeric(7, 2), default=0.0, comment="账户余额") credit = db.Column(db.Integer, default=0, comment="用户积分") ip_address = db.Column(db.String(255), default="", index=True, comment="登录IP") intro = db.Column(db.String(500), default="", comment="个性签名") avatar = db.Column(db.String(255), default="", comment="头像url地址") sex = db.Column(db.SmallInteger, default=0, comment="性别") # 0表示未设置,保密, 1表示男,2表示女 email = db.Column(db.String(32), index=True, default="", nullable=False, comment="邮箱地址") mobile = db.Column(db.String(32), index=True, nullable=False, comment="手机号码") unique_id = db.Column(db.String(255), index=True, default="", comment="客户端唯一标记符") province = db.Column(db.String(255), default="", comment="省份") city = db.Column(db.String(255), default="", comment="城市") area = db.Column(db.String(255), default="", comment="地区") info = db.relationship("UserProfile", uselist=False, backref="user", primaryjoin="User.id==UserProfile.user_id", foreign_keys="UserProfile.user_id") @property def password(self): # user.password return self._password @password.setter def password(self, rawpwd): # user.password = '123456' """密码加密""" self._password = generate_password_hash(rawpwd) def check_password(self, rawpwd): """验证密码""" return check_password_hash(self.password, rawpwd) class UserProfile(BaseModel): """用户详情信息表""" __tablename__ = "ym_user_profile" user_id = db.Column(db.Integer, index=True, comment="用户ID") education = db.Column(db.Integer, comment="学历教育") middle_school = db.Column(db.String(255), default="", comment="初中/中专") high_school = db.Column(db.String(255), default="", comment="高中/高职") college_school = db.Column(db.String(255), default="", comment="大学/大专") profession_cate = db.Column(db.String(255), default="", comment="职业类型") profession_info = db.Column(db.String(255), default="", comment="职业名称") position = db.Column(db.SmallInteger, default=0, comment="职位/职称") emotion_status = db.Column(db.SmallInteger, default=0, comment="情感状态") birthday = db.Column(db.DateTime, default="", comment="生日") hometown_province = db.Column(db.String(255), default="", comment="家乡省份") hometown_city = db.Column(db.String(255), default="", comment="家乡城市") hometown_area = db.Column(db.String(255), default="", comment="家乡地区") hometown_address = db.Column(db.String(255), default="", comment="家乡地址") living_province = db.Column(db.String(255), default="", comment="现居住省份") living_city = db.Column(db.String(255), default="", comment="现居住城市") living_area = db.Column(db.String(255), default="", comment="现居住地区") living_address = db.Column(db.String(255), default="", comment="现居住地址") """ 外界开发中,不过是SQLAlachemy或者django的ORM,大部分的公司都会放弃使用外键约束来关联查询数据库表。 因为外键约束,在数据库操作过程中,需要消耗额外的维护成本来管理这个外键关系。因此在大数据的查询中,一般都会设置成逻辑外键[虚拟外键]。数据库本身维护的外键一般我们称之为 "物理外键". """
删除原来数据表,让flask重新运行项目即可创建上面模型对应的数据表了,初始化主程序中已经自动建表,application.__init__:
# db创建数据表 with app.app_context(): db.create_all()
提交版本
git add . git commit -m "fix: add user's model" git push --set-upstream origin feature/users
注册功能实现
手机号码唯一验证接口
在开发中,针对客户端提交的数据进行验证或提供模型数据转换格式成字典给客户端。可以使用Marshmallow模块来进行。
application/apps/users/serializers.py,代码:
from marshmallow import Schema, fields, validate, validates, ValidationError from .models import User from application import message class MobileSchema(Schema): """手机号唯一校验序列化器""" mobile: fields.String = fields.String(required=True, validate=validate.Regexp( regex=r"^1[3-9]\d{9}$", error=message.mobile_format_error ), error_messages={"required": message.mobile_is_required}) @validates("mobile") def validate_mobile(self, mobile: str) -> str: """到数据库查询验证是否曾经注册过当前手机号""" user: User = User.query.filter(User.mobile == mobile).first() if user: raise ValidationError(message=message.mobile_is_used, field_name="mobile") return mobile
application/utils/message.py,服务端错误提示,代码:
success: str = "成功!" """用户模块""" # 注册相关 mobile_is_required: str = "手机号不能为空!" mobile_format_error: str = "手机号格式有误!" mobile_is_used: str = "当前手机号已经被使用!"
服务端接口操作返回状态码,application/utils/code.py,代码:
CODE_OK: int = 0 # 接口操作成功 CODE_VALIDATE_ERROR: int = 1001 # 验证有误!
application/__init__.py,引入message,方便其他地方导报,代码:
from application.utils import message, code
application/apps/users/api.py,视图调用序列化构造器,实现手机号验证接口,代码:
from typing import Dict, Union, Any from . import serializers from application import message, code def check_mobile(mobile: str) -> Dict[str, Any]: """ 验证手机号格式与是否唯一 :param mobile: 手机号码 :return: """ # 调用构造器反序列化验证 ms: serializers.MobileSchema = serializers.MobileSchema() try: ms.load({"mobile": mobile}) result: Dict[str, Any] = {"errno": code.CODE_OK, "errmsg": message.success} except serializers.ValidationError as e: result: Dict[str, Any] = {'errno': code.CODE_VALIDATE_ERROR, "errmsg": e.messages["mobile"][0]} return result
users/urls.py,代码:
from typing import List from application import path from . import api apipatterns: List = [ path("mobile", api.check_mobile) ]
提交版本
git add . git commit -m "api: check mobile" git push
接口测试,可以通过自己编写单元测试来完成,也可以使用JSON-RPC提供的browse界面来完成。
访问:http://192.168.21.253:5000/api/browse/#/Users.mobile
客户端请求验证手机号
main.js中添加配置相关信息,保存成一个config对象,并创建生成uuid的方法。
class Game{ constructor(bg_music){ // 构造函数,相当于 python的 __init__方法 this.init(); this.play_music(bg_music); } init(){ // 初始化函数 console.log("系统初始化"); this.rem(); this.init_config(); } init_config(){ // 初始化配置 this.config = { "server_api": "http://192.168.21.253:5000/api", // api服务端的网关地址 } } print(data){ // 打印数据 console.log(JSON.stringify(data)); } rem(){ // 样式适配[保证在任何尺寸下的手机里面,页面效果保持] if(window.innerWidth<1200){ this.UIWidth = document.documentElement.clientWidth; this.UIHeight = document.documentElement.clientHeight; document.documentElement.style.fontSize = (0.01*this.UIWidth*3)+"px"; document.querySelector("#app").style.height=this.UIHeight+"px" } window.onresize = ()=>{ if(window.innerWidth<1200){ this.UIWidth = document.documentElement.clientWidth; this.UIHeight = document.documentElement.clientHeight; document.documentElement.style.fontSize = (0.01*this.UIWidth*3)+"px"; } } } stop_music(){ // 暂停音乐 this.print("停止") document.body.removeChild(this.audio); } play_music(src){ // 播放音乐 this.audio = document.createElement("audio"); this.source = document.createElement("source"); this.source.type = "audio/mp3"; this.audio.autoplay = "autoplay"; this.source.src=src; this.audio.appendChild(this.source); document.body.appendChild(this.audio); var t = setInterval(()=>{ if(this.audio.readyState > 0){ if(this.audio.ended){ clearInterval(t); document.body.removeChild(this.audio); } } },100); } goWin(url,pageParam){ // 打开窗口 const name = url.replace(".html",""); if(name === "root"){ api.openWin({"name":"root"}); return; } api.openWin({ name: name, // 自定义窗口名称 bounces: false, // 窗口是否上下拉动 reload: false, // 如果页面已经在之前被打开了,是否要重新加载当前窗口中的页面 url: url, // 窗口创建时展示的html页面的本地路径[相对于当前代码所在文件的路径] animation:{ // 打开新建窗口时的过渡动画效果 type: "push", //动画类型(详见动画类型常量) subType: "from_right", //动画子类型(详见动画子类型常量) duration:300 //动画过渡时间,默认300毫秒 }, pageParam: pageParam // 传递给下一个窗口使用的参数.将来可以在新窗口中通过 api.pageParam.name 获取 }); } outWin(name){ // 关闭窗口 api.closeWin({"name": name}); } goFrame(url,pageParam){ // 打开帧页面 const name = url.replace(".html",""); api.openFrame({ name: name, url: url, rect: { x: 0, y: 0, w: 'auto', h: 'auto' }, useWKWebView:true, historyGestureEnabled:true, bounces:false, animation:{ type:"push", subType:"from_right", duration:300 }, pageParam: pageParam }); } outFrame(name){ // 关闭帧页面 api.closeFrame({ name: name, }); } uuid(){ // 生成UUID函数 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c)=>{ var r = Math.random()*16|0,v=c=='x'?r:r&0x3|0x8; return v.toString(16); }) } }
html/register.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="../css/main.css">
<script src="../script/vue.3.2.41.js"></script>
<script src="../script/axios.1.1.3.min.js"></script>
<script src="../script/main.js"></script>
</head>
<body>
<div class="app" id="app">
<img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../image/player.png">
<div class="bg">
<img src="../image/bg0.jpg">
</div>
<div class="form">
<div class="form-title">
<img src="../image/register.png">
<img class="back" @click="open_login" src="../image/back.png">
</div>
<div class="form-data">
<div class="form-data-bg">
<img src="../image/bg1.png">
</div>
<div class="form-item">
<label class="text">手机</label>
<input type="text" v-model="mobile" placeholder="请输入手机号">
</div>
<div class="form-item">
<label class="text">验证码</label>
<input type="text" class="code" v-model="code" placeholder="请输入验证码">
<img class="refresh" src="../image/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="re_password" placeholder="请再次输入密码">
</div>
<div class="form-item">
<input type="checkbox" class="agree" v-model="agree" checked>
<label><span class="agree_text">同意嘤鸣App的《用户协议》和《隐私协议》</span></label>
</div>
<div class="form-item">
<img class="commit" @click="game.play_music('../mp3/btn1.mp3')" src="../image/commit.png"/>
</div>
</div>
</div>
</div>
<script>
const app = Vue.createApp({
data(){
return {
agree: true,
mobile: "13012345678",
code: "1234",
password: "123456",
re_password: "123456",
music_play: true, // 默认播放背景音乐的控制属性
}
},
watch:{
music_play(){
if(this.music_play){
this.game.play_music("../mp3/bg1.mp3");
}else{
this.game.stop_music();
}
},
mobile(){
// 监听,当mobile手机号格式没有问题,则请求服务端验证手机号是否已经注册了
if(/^1[3-9]\d{9}$/.test(this.mobile)){
this.check_mobile();
}
}
},
methods: {
open_login(){
this.game.goFrame("login.html");
},
check_mobile(){
// 校验手机号是否已经注册
let uuid = this.game.uuid();
axios.post(this.game.config.server_api, {
"jsonrpc": "2.0",
"id": uuid,
"method": "Users.mobile",
"params": {
"mobile": this.mobile
}
}).then(response=>{
if(response.data.id === uuid){
if(response.data.result.errno !== 0){
api.alert({
title: "错误警告",
msg: response.data.result.errmsg,
})
}
}
}).catch(error=>{
api.alert({
"title":"错误警告",
msg: error.response.data.error.data.message
});
})
}
}
})
app.mount("#app");
app.config.globalProperties.game = new Game("../mp3/bg1.mp3");
</script>
</body>
</html>
保存用户注册信息接口
创建Marshmallow构造器[暂时不涉及到手机验证码功能]
users/serializers.py,代码:
from typing import List, Dict, Any from marshmallow import Schema, fields, validate, validates, ValidationError, decorators from marshmallow_sqlalchemy import SQLAlchemyAutoSchema, auto_field from sqlalchemy.orm import scoped_session from flask import request from .models import User from application import db, message class MobileSchema(Schema): """手机号唯一校验序列化器""" mobile: fields.String = fields.String(required=True, validate=validate.Regexp( regex=r"^1[3-9]\d{9}$", error=message.mobile_format_error ), error_messages={"required": message.mobile_is_required}) @validates("mobile") def validate_mobile(self, mobile: str) -> str: """到数据库查询验证是否曾经注册过当前手机号""" user: User = User.query.filter(User.mobile == mobile).first() if user: raise ValidationError(message=message.mobile_is_used, field_name="mobile") return mobile class UserSchema(SQLAlchemyAutoSchema): """用户注册信息序列化器""" mobile: fields.String = auto_field(required=True, load_only=True, validates=validate.Regexp( regex="^1[3-9]\d{9}$", error=message.mobile_format_error )) password: fields.String = fields.String(required=True, load_only=True, validate=validate.Length( min=6, max=16, error=message.password_length_error )) re_password: fields.String = fields.String(required=True, load_only=True) sms_code: fields.String = fields.String(required=True, load_only=True, validate=validate.Length( min=4, max=4, error=message.sms_code_length_error )) class Meta: model: User = User include_fk: bool = True # 启用外键关系 include_relationships: bool = True # 模型关系外部属性 # 如果要全换全部字段,就不要声明fields或exclude字段即可 fields: List[str] = ["id", "name", "mobile", "password", "re_password", "sms_code"] sqla_session: scoped_session = db.session @decorators.validates_schema def validates_schema(self, data, **kwargs) -> Dict[str, Any]: """全字段校验""" # 校验密码和确认密码 if data["password"] != data["re_password"]: raise ValidationError(message=message.password_not_match, field_name="re_password") # todo 校验短信验证码 data.pop("re_password") data.pop("sms_code") return data @decorators.post_load def save_object(self, data, **kwargs) -> User: data["name"] = data["mobile"] print(request.environ) data["ip_address"] = request.environ["REMOTE_ADDR"] # 客户端本次请求的IP地址 user: User = User(**data) self.session.add(user) self.session.commit() return user
