架构
数据表
CREATE TABLE `member` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`nickname` varchar(100) NOT NULL DEFAULT '' COMMENT '会员名',
`mobile` varchar(11) NOT NULL DEFAULT '' COMMENT '会员手机号码',
`sex` tinyint(1) NOT NULL DEFAULT '0' COMMENT '性别 1:男 2:女',
`avatar` varchar(200) NOT NULL DEFAULT '' COMMENT '会员头像',
`salt` varchar(32) NOT NULL DEFAULT '' COMMENT '随机salt',
`reg_ip` varchar(100) NOT NULL DEFAULT '' COMMENT '注册ip',
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态 1:有效 0:无效',
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后一次更新时间',
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '插入时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员表';
还有一张第三方绑定关系表,这个和其类似。
代码书写
首先我们可以去微信小程序的开发者平台将其接口文档研读一遍,大致清楚上面图的流程后就可以下载小程序开发工具,然后导入项目的路径进入:
点击导入后,查看项目设置里有没有我们的信息以及点击编译,没有错误的情况下就能看到如下三个图层:
然后我们就可以去开发我们的代码了,这里需要在index.wxml中更改按钮为微信提供的更安全的button:
/*--<view class="confirm-btn" bindtap='goToIndex'>
<text >走吧,订餐去</text>
</view> */
<button class="confirm-btn" open-type="getUserInfo" bindgetuserinfo="login">
授权登录
</button>
微信授权后可提供的信息为:
// 必须是在用户已经授权的情况下调用
wx.getUserInfo({
success: function(res) {
var userInfo = res.userInfo
var nickName = userInfo.nickName
var avatarUrl = userInfo.avatarUrl
var gender = userInfo.gender //性别 0:未知、1:男、2:女
var province = userInfo.province
var city = userInfo.city
var country = userInfo.country
}
})
如果需要获取到上述信息,我们可以在index.js里直接打印e。
onReady: function(){...},
login:function (e) {
app.console(e);
}
然后在开发者工具上点击授权登录就能看到相关信息:
那么和上一篇不同的是,这里就需要前端来写主要的登录和校验功能,而后端需要先将前端传过来的信息存进数据库里才能做一些校验,还有统一拦截器的修改,我们接下来往下看。
这里还要做一些额外的配置,如果小程序前端要和我们的后端连接,在设置更改里勾选上不校验合法域名:
以及更改一下app.js文件中的globaldata:
globalData: {
userInfo: null,
version: "1.0",
shopName: "Python3 + Flask 订餐全栈系统",
domain:"http://127.0.0.1:5000/api" // 这是我本机的地址
},
然后我们的前端代码为:
login:function( e ){
var that = this;
if( !e.detail.userInfo ){
app.alert( { 'content':'登录失败,请再次点击~~' } );
return;
}
var data = e.detail.userInfo; // 微信小程序固定写法
wx.login({
success:function( res ){
if( !res.code ){
app.alert( { 'content':'登录失败,请再次点击~~' } );
return;
}
data['code'] = res.code;
wx.request({
url:app.buildUrl( '/member/login' ), // 本来应该是127.0.0.1:5000/member/login,但我们可以将上节构造的url处理器做简化
header:app.getRequestHeader(), // 将json替换成表单:'content-type': 'application/x-www-form-urlencoded',
method:'POST',
data:data,
success:function( res ){
if( res.data.code != 200 ){
app.alert( { 'content':res.data.msg } );
return;
}
// app.setCache( "token",res.data.data.token );
that.goToIndex(); // 跳至首页
}
});
}
});
}
于是就可以看到小程序端的表单信息:
关于后端代码,首先注册蓝图:
from web.controllers.api import route_api
app.register_blueprint(route_api,url_prefix="/api")
我们在controller目录下的api包下的init文件定义我们的蓝图以及其它信息,因为这个文件夹下的文件会很多,所以全部写入init文件统一管理:
# -*- coding: utf-8 -*-
from flask import Blueprint
route_api = Blueprint( 'api_page',__name__ )
from web.controllers.api.Member import *
from web.controllers.api.Food import *
from web.controllers.api.Order import *
from web.controllers.api.My import *
from web.controllers.api.Cart import *
from web.controllers.api.Address import *
@route_api.route("/")
def index():
return "Mina Api V1.0~~"
我们可以将项目启动测试一下是否能访问到这个路由,会出现Mina Api V1.0~~,没有问题的话就可以写api.Member代码了。
Member代码为:
@route_api.route("/member/login",methods=["POST","GET"])
def login():
resp = {"code":200,"msg":"操作成功","data":{}}
req = request.values
code = req["code"] if "code" in req else ""
# 前端点击登录时获取的 code,必须通过后端拿到openid唯一标识
if not code or len(code) < 1:
resp["code"] = -1
resp["msg"] = "需要code"
return jsonify(resp)
# todo 前端传过来的code通过后端获取到openid,方法进行了封装
openid = MemberService.getWeChatOpenId(code)
if openid is None:
resp["code"] = -1
resp["msg"] = "调用微信接口出错"
return jsonify(resp)
nickname = req["nickName"] if "nickName" in req else ""
sex = req["gender"] if "gender" in req else 0
avatar = req["avatarUrl"] if "avatarUrl" in req else ""
'''
判断是否已经测试过,注册了直接返回一些信息
'''
bind_info = OauthMemberBind.query.filter_by(openid=openid,type=1).first()
if not bind_info: # 如果数据库没有就进行统一注册
model_member = Member()
model_member.sex = sex
model_member.avatar = avatar
model_member.salt = MemberService.geneSalt() # todo 生成加盐字段,为了之后的登录加密
model_member.updated_time = model_member.created_time = getCurrentDate()
db.session.add(model_member)
db.session.commit()
model_bind = OauthMemberBind()
model_bind.member_id = model_member.id
model_bind.type = 1
model_bind.openid = openid
model_bind.updated_time = model_bind.created_time = getCurrentDate() # todo 创建相应的日期
db.session.add(model_bind)
db.session.commit()
bind_info = model_bind
# member_info = Member.query.fitler_by(id = bind_info.member_id).first()
return jsonify(resp)
其中上面有三个TODO的地方,其中openid是需要我们通过前端传来的code还有一系列信息对小程序端的某个api进行请求,具体的信息我们可以看如下文档:
我们在MemberService中,大致代码为:
class MemberService():
@staticmethod
def geneAuthCode(member_info=None):
m = hashlib.md5()
str = "%s-%s-%s" % (member_info.id,member_info.salt,member_info.status)
m.update(str.encode("utf-8"))
return m.hexdigest()
@staticmethod
def geneSalt(length=16):
keylist = [random.choice((string.ascii_letters + string.digits)) for i in range(length)]
# 产生16位随机的数字与字母组成的列表,然后用join拼接成字符串
return ("".join(keylist))
@staticmethod
def getWeChatOpenId( code ):
url = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code" \
.format(app.config['MINA_APP']['appid'], app.config['MINA_APP']['appkey'], code)
"""
appid:小程序 appId
secret:小程序 appSecret
js_code:登录时获取的 code,前端传过来的code
"""
r = requests.get(url) # 根据官网的要求需要发送get请求,获取到响应对象
res = json.loads(r.text) # 从中取出内容
openid = None # openid:用户唯一标识
if 'openid' in res:
openid = res['openid']
return openid # 将其当成变量返回
然后这里还会有问题,因为我们之前在后台项目中定义了全局拦截器,当我们点击小程序登录的时候会发现后端的日志会一直在302重定向,所以我们要去settings里面把"/api"也忽略掉,然后就能看到如下的图了:
然后我们就可以从数据库中的member表和oauth_member_bind表中看到我们插入的记录:
优化
我们可以想一个问题,假如我们已经注册过了之后,我们的信息显示是不是可以更换,即我们可以不需要也不必要一直去调用授权的方法,我们可以在第一步的时候可以检验是否有token,而让代码更加高效:
所以我们对后端来说加入检验登录的方法:
@route_api.route("/member/check-reg",methods = [ "GET","POST" ])
def checkReg():
resp = {'code': 200, 'msg': '操作成功~', 'data': {}}
req = request.values
code = req['code'] if 'code' in req else ''
if not code or len(code) < 1:
resp['code'] = -1
resp['msg'] = "需要code"
return jsonify(resp)
openid = MemberService.getWeChatOpenId(code)
if openid is None:
resp['code'] = -1
resp['msg'] = "调用微信出错"
return jsonify(resp)
bind_info = OauthMemberBind.query.filter_by(openid=openid, type=1).first()
if not bind_info:
resp['code'] = -1
resp['msg'] = "未绑定"
return jsonify(resp)
member_info = Member.query.filter_by( id = bind_info.member_id).first()
if not member_info:
resp['code'] = -1
resp['msg'] = "未查询到绑定信息"
return jsonify(resp)
token = "%s#%s"%( MemberService.geneAuthCode( member_info ),member_info.id )
resp['data'] = { 'token':token }
return jsonify(resp)
而小程序部分,html部分的登录按钮用if判断更改为:
<view class="confirm-btn" bindtap='goToIndex' wx:if="{{regFlag==true}}">
<text>走,逛逛去</text>
</view>
<button class="confirm-btn" open-type="getUserInfo" bindgetuserinfo="login" wx:if="{{regFlag==false}}">
授权登录
</button>
再加上checklogin.js:
checkLogin:function(){
var that = this;
wx.login({
success:function( res ){
if( !res.code ){
app.alert( { 'content':'登录失败,请再次点击~~' } );
return;
}
wx.request({
url:app.buildUrl( '/member/check-reg' ),
header:app.getRequestHeader(),
method:'POST',
data:{ code:res.code }, // 小程序只能后端做验证
success:function( res ){
if( res.data.code != 200 ){
that.setData({
regFlag:false
});
return;
}
app.setCache( "token",res.data.data.token );
//that.goToIndex();
}
});
}
});
}
那么最终的演示效果为: