6. 用户个人中心
用户中心
客户端显示个人中心页面
- 个人中心页面:
html/user.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app user" id="app">
<div class="bg">
<img src="../static/images/bg0.jpg">
</div>
<img class="back" @click="back" src="../static/images/user_back.png" alt="">
<img class="setting" src="../static/images/setting.png" alt="">
<div class="header">
<div class="info">
<div class="avatar">
<img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
<img class="user_avatar" src="../static/images/avatar.png" alt="">
<img class="avatar_border" src="../static/images/avatar_border.png" alt="">
</div>
<p class="user_name">清蒸小帅锅</p>
</div>
<div class="wallet">
<div class="balance">
<p class="title"><img src="../static/images/money.png" alt="">钱包</p>
<p class="num">99,999.00</p>
</div>
<div class="balance">
<p class="title"><img src="../static/images/integral.png" alt="">果子</p>
<p class="num">99,999.00</p>
</div>
</div>
<div class="invite">
<img class="invite_btn" src="../static/images/invite.png" alt="">
</div>
</div>
<div class="menu">
<div class="item">
<span class="title">我的主页</span>
<span class="value">查看</span>
</div>
<div class="item">
<span class="title">任务列表</span>
<span class="value">75%</span>
</div>
<div class="item">
<span class="title">收益明细</span>
<span class="value">查看</span>
</div>
<div class="item">
<span class="title">实名认证</span>
<span class="value">未认证</span>
</div>
<div class="item">
<span class="title">问题反馈</span>
<span class="value">去反馈</span>
</div>
</ul>
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
prev:{name:"",url:"",params:{}},
current:{name:"user",url:"user.html",params:{}},
}
},
methods:{
back(){
// 返回首页
this.game.closeWin();
},
}
});
}
</script>
</body>
</html>
- 追加css样式:
static/css/main.css
,代码:
.user .header{
position: absolute;
top: 1.22rem;
left: 0;
right: 0;
margin: auto;
width: 32rem;
height: 19.28rem;
background: url("../images/ucenter.png") no-repeat 0 0;
background-size: 100%;
}
.user .back{
position: absolute;
width: 3.83rem;
height: 3.89rem;
z-index: 1;
top: 2.72rem;
left: 3rem;
}
.user .setting{
position: absolute;
z-index: 1;
top: 4.2rem;
right: 3.8rem;
width: 3.06rem;
height: 3.06rem;
}
.user .info{
position: absolute;
z-index: 1;
top: 6.94rem;
left: 3.89rem;
width: 6.39rem;
height: 9.17rem;
}
.user .info .avatar{
width: 6.39rem;
height: 6.39rem;
position: relative;
}
.user .info .avatar_bf{
position: absolute;
z-index: 1;
margin: auto;
width: 4.56rem;
height: 4.56rem;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.user .info .user_avatar{
position: absolute;
z-index: 1;
width: 4.56rem;
height: 4.56rem;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.user .info .avatar_border{
position: absolute;
z-index: 1;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 6.2rem;
height: 6.2rem;
}
.user .info .user_name{
text-align: center;
}
.user .wallet{
position: absolute;
top: 5.56rem;
right: 1.94rem;
width: 16rem;
height: 10rem;
}
.user .wallet .balance{
margin-top: 1.4rem;
float: left;
margin-right: 1rem;
}
.user .wallet .title{
color: #300;
font-size: 1.2rem;
width: 6.4rem;
text-align: center;
}
.user .wallet .title img{
width: 1.4rem;
margin-right: 0.2rem;
vertical-align: sub;
height: 1.4rem;
}
.user .wallet .num{
background: url("../images/btn3.png") no-repeat 0 0;
background-size: 100%;
width: 6.4rem;
font-size: 0.8rem;
color: #fff;
height: 2rem;
line-height: 1.8rem;
text-indent: 1rem;
}
.user .invite{
position: absolute;
top: 11.6rem;
right: 6.4rem;
}
.user .invite .invite_btn{
width: 9.39rem;
height: 3.67rem;
}
.user .menu{
position: absolute;
top: 22rem;
padding-top: 4.6rem;
right: 0;
left: 0;
margin: auto;
width: 28.44rem;
height: 33.89rem;
background: url("../images/bg10.png") no-repeat 0 0;
background-size: 100%;
}
.user .menu .item{
margin-left: 2.6rem;
margin-bottom: 0.5rem;
width: 23.06rem;
height: 4.22rem;
line-height: 4.22rem;
overflow: hidden;
background: url("../images/title.png") no-repeat 0 0;
background-size: 100%;
text-indent: 2rem;
color: #fff;
font-size: 1.33rem;
}
.user .menu .item .title{
float: left;
}
.user .menu .item .value{
float: right;
padding-right: 2rem;
}
- 把素材中的图片添加静态文件中
static/images
检查用户登录状态
用户要进入到用户中心,必须要通过登录认证,前面已经登陆或者注册了,则我们再次就需要获取本地的access_token和并验证access_token的有效性。
- 检验用户是否登陆, 再跳转用户个人中心页面
html/index.html
,代码:
<!DOCTYPE html>
<html lang="en">
<head>
<title>首页</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta name="format-detection" content="telephone=no,email=no,date=no,address=no">
<link rel="stylesheet" href="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app" id="app">
<img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
<div class="bg">
<img src="../static/images/bg0.jpg">
</div>
<ul>
<li><img class="module1" src="../static/images/image1.png"></li>
<li><img class="module2" src="../static/images/image2.png" @click='to_user'></li>
<li><img class="module3" src="../static/images/image3.png"></li>
<li><img class="module4" src="../static/images/image4.png" @click='to_login'></li>
</ul>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
music_play:true, // 默认播放背景音乐
prev:{name:"",url:"",params:{}}, // 上一页状态
current:{name:"index",url:"index.html","params":{}}, // 下一页状态
}
},
watch:{
music_play(){
if(this.music_play){
this.game.play_music("../static/mp3/bg1.mp3");
}else{
this.game.stop_music();
}
}
},
methods:{
// 点击签到跳转登陆页面
to_login(){
this.game.openWin('login','login.html')
},
// 点击跳转到个人中心页面
to_user(){
// 判断用户是否登陆
this.game.check_user_login(this,() => {
this.game.openWin('user', 'user.html')
});
},
}
})
}
</script>
</body>
</html>
static/js/main.js
中实现检验用户是否登陆check_user_login
方法
class Game{
constructor(bg_music){
// 构造函数,相当于python中类的__init__方法
this.init();
if(bg_music){
this.play_music(bg_music);
}
}
init(){
// 初始化
console.log("系统初始化");
this.rem(); // 自适配方案,根据当前受屏幕,自动调整rem单位
this.init_config(); //初始化配置
this.init_http(); // 初始化http网络请求
}
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";
}
}
}
// 初始化配置
init_config(){
// 客户端项目的全局配置
this.config = {
// 服务端API地址
API_SERVER:"http://192.168.189.138:5000/api",
SMS_TIME_OUT: 60 , // 短信发送冷却时间/秒
CAPTCHA_APP_ID: "2028945858", // 防水墙验证码应用ID
}
}
// 初始化http网络请求
init_http(){
// ajax初始化
if(window.axios){
axios.defaults.baseURL = this.config.API_SERVER // 接口网关地址
axios.defaults.timeout = 2500 // 请求超时时间
axios.defaults.withCredentials = false // 跨域请求时禁止携带cookie
// 请求拦截器和响应拦截器相当于中间件作用
// 1. 添加请求拦截器
axios.interceptors.request.use((config) => {
// 请求正确
// 在发送请求之前的初始化[添加请求头],config就是本次请求数据对象
// 显示进度提示框
api.showProgress({
style: 'default', // 进度提示框风格
animationType: 'zoom', // 动画类型 缩放
title: '努力加载中...', // 标题
text: '请稍等...', // 内容
modal: true //是否模态,模态时整个页面将不可交互
});
return config // 返回对象
}, (error) => {
// 请求错误, 隐藏进度提示框
api.hideProgress();
// 弹出错误提示框
this.tips("网络错误!!");
// 返回
return Promise.reject(error);
});
// 2. 添加响应拦截器 - 找出响应数据错误
axios.interceptors.response.use((response) => {
// 关闭进度提示框
api.hideProgress();
// 判断接口返回状态码
if(response.data && response.data.error){
// 服务器报错
let error = response.data.error;
switch (error.code) {
case -32601: // 请求接口不存在
this.tips("请求地址不存在!");
break;
case 500:
this.tips("服务端程序执行错误!\n" + error.message);
break;
}
if(response.data && response.data.result){
// 判断请求唯一标识是否一致
if(axios.uuid != response.data.id){
this.tips("请求拦截错误!");
return false;
}
}
let result = response.data.result;
if(result && result.errno != 1000){
this.tips(result.errmsg);
return false
}
// return response // 没有错误的话,返回响应数据
}
return response // 没有错误的话,返回响应数据
}, (error) => {
// 关闭进度提示框
api.hideProgress();
// 网络错误提示
switch (error.message) {
case "Network Error":
this.tips('网络错误!!');
break;
}
return Promise.reject(error);
});
if(Vue){
// js语法: prototype 向对象添加属性和方法
Vue.prototype.axios = axios;
}
if(window.UUID){
// prototype 向对象添加属性和方法
Vue.prototype.uuid = UUID.generate;
}
}
}
// 窗口提示
tips(msg, duration = 5000, location = "top"){
// 参数: 提示信息 - 时间 - 显示位置(上中下)
let params = {
msg: msg,
duration: duration,
location: location
}
api.toast(params)
}
// 网络发送post请求
post(vm, data){
// 基于axios发送post请求
vm.axios.uuid = vm.uuid();
vm.axios.post("", {
"jsonrpc": "2.0",
"id": vm.axios.uuid,
"method": data.method,
"params": data.params
}, {
headers:data.header
}).then(
data.success
).catch(
data.fail
);
}
print(data){
// 打印数据
console.log(JSON.stringify(data));
}
stop_music(){
this.print("停止背景音乐");
document.body.removeChild(this.audio);
}
play_music(src){
this.print("播放背景音乐");
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);
}
//创建窗口
openWin(name,url,pageParam){
if(!pageParam){
pageParam = {}
}
api.openWin({
name: name, // 自定义窗口名称,如果是新建窗口,则名字必须是第一次出现的名字。
bounces: false, // 窗口是否上下拉动
reload: true, // 如果窗口已经在之前被打开了,是否要重新加载当前窗口中的页面
url: url, // 窗口创建时展示的html页面的本地路径[相对于当前代码所在文件的路径]
animation:{ // 打开新建窗口时的过渡动画效果
type:"push", //动画类型(详见动画类型常量)
subType:"from_right", //动画子类型(详见动画子类型常量)
duration:300 //动画过渡时间,默认300毫秒
},
pageParam: pageParam,
});
}
// 关闭指定窗口
closeWin(name=''){
let params
if(name !== ''){
params = {
name:name,
}
}
api.closeWin(params);
}
// 创建帧页面
openFrame(name,url,pageParam){
if(!pageParam){
pageParam = {}
}
api.openFrame({
name: name, // 帧页面的名称
url: url, // 帧页面打开的url地址
bounces:false, // 页面是否可以下拉拖动
reload: true, // 帧页面如果已经存在,是否重新刷新加载
useWKWebView:true, // 是否使用WKWebView来加载页面
historyGestureEnabled:true, // 是否可以通过手势来进行历史记录前进后退,只在useWKWebView参数为true时有效
vScrollBarEnabled: false, // 是否显示垂直滚动条
hScrollBarEnabled: false, // 是否显示水平滚动条
animation:{
type:"push", //动画类型(详见动画类型常量)
subType:"from_right", //动画子类型(详见动画子类型常量)
duration:300 //动画过渡时间,默认300毫秒
},
rect: { // 当前帧的宽高范围
// 方式1,设置矩形大小宽高
x: 0, // 左上角x轴坐标
y: 0, // 左上角y轴坐标
w: 'auto', // 当前帧页面的宽度, auto表示满屏
h: 'auto' // 当前帧页面的高度, auto表示满屏
// 方式2,设置矩形大小宽高
// marginLeft:, //相对父页面左外边距的距离,数字类型
// marginTop:, //相对父页面上外边距的距离,数字类型
// marginBottom:, //相对父页面下外边距的距离,数字类型
// marginRight: //相对父页面右外边距的距离,数字类型
},
pageParam: { // 要传递新建帧页面的参数,在新页面可通过 api.pageParam.name 获取
name: pageParam // name只是举例, 可以传递任意自定义参数
}
});
}
// 关闭帧页面
closeFrame(name=''){
let params
if(name !== ''){
params = {
name: name
}
}
api.closeFrame(params);
}
// 发送APP全局事件通知/全局事件广播
sendEvent(name, data){
api.sendEvent({
name: name,
extra: data
});
}
// 保存数据到本地文件系统
setfs(data){ // data 是一个字典
for(let key in data){
api.setPrefs({
key: key,
value: data[key]
});
}
}
// 根据key值来获取本地文件系统中存储的数据
getfs(key){ // key="access_token" key = ["access_token","refresh_token"]
// key值统统转化成数组类型
let keys = key;
if(!(key instanceof Array)){
keys = [key]
}
let data = {};
for(let item of keys){
data[item] = api.getPrefs({
key: item,
sync: true //执行结果的返回方式。为false时通过callback返回,为true时直接返回
});
}
if(key instanceof Array){
// 返回数组
return data
}
// 返回单个数据
return data[key]
}
// 根据key值来删除本地文件系统中存储的数据
delfs(key){
// key值统统转化成数组类型
let keys = key;
if(!(key instanceof Array)){
keys = [key]
}
for(let item of keys){
api.removePrefs({
key: item
});
}
}
// 保存数据到内存中
setdata(data){
for(let key in data){
api.setGlobalData({
key:key,
value:data[key]
});
}
}
// 根据key值来获取内存中存储的数据
getdata(key){
// 转化成数组数据处理
let keys = key;
if(!(key instanceof Array)){
keys = [key];
}
let data = {};
for(let item of keys){
data[item] = api.getGlobalData({
key:item
});
}
// 返回结果
if(key instanceof Array){
return data; // 返回数组
}
return data[key] // 返回单个数据
}
// 根据key值来删除内存中保存的数据
// 因为本身并没有提供删除内存数据的方法,所以此处我们把设置为null即可
deldata(key){
// 转化成数组数据处理
let keys = key;
if(!(key instanceof Array)){
keys = [key];
}
// 设置数值为null
for(let item of keys){
api.setGlobalData({
key:null
});
}
}
// 根据token令牌载荷获取登陆用户信息
get_user_by_token(token){
let preload = token.split('.')[1];
// 在服务端中发送给客户端的token是经过base64编码处理的,获取需解码
// btoa 字符串---->编码--->base64编码
// atob base64--->解码--->字符串
let data_str = atob(preload);
let data = JSON.parse(data_str)
this.print(data)
// 打印出的数据, sub中存有用户信息
// {"fresh":false,"iat":1623162593,"jti":"3993f774-870f-45b8-b547-71c40f7fefd1",
// "type":"access","sub":{"id":16,"name":"曹军"},"nbf":1623162593,"exp":1623163193}
return data.sub
}
// 检验用户是否登陆
check_user_login(vm, callback){
let self = this
// self.deldata('access_token') // 清除缓存token
// self.delfs('access_token')
// 从本地或内存中获取token值,判断是否存在
let token = self.getdata('access_token') || self.getfs('access_token');
if(token == ""){ // 没有得到token
self.tips('你还没有登陆,请重新登陆!')
setTimeout(() => {
self.openWin('login', 'login.html')
}, 1000)
}else {
// 如果存在token值,获取载荷中的用户信息
let preload = token.split('.')[1];
let data_str = atob(preload); // base64解码 - json数据
let data = JSON.parse(data_str); // 反序列化成字典数据
// self.print(data)
// 判断token值是否过期
if( parseInt(data.exp) > parseInt((new Date()-0)/1000) ){
// token没过期,回调函数
callback();
}else {
// token没过期,根据refresh_token获取access_token并完成回调操作
self.get_access_by_refresh(vm, callback)
}
}
}
// 通过refresh_token获取access_token
get_access_by_refresh(vm, callback){
let self = this
// 获取refresh_token
let refresh_token = self.getdata('refresh_token') || self.getfs('refresh_token')
// 向服务端发送请求获取access_token
self.post(vm, {
"method": "Users.refresh",
"params":{},
"header":{
"Authorization": "jwt " + refresh_token
},
success(response){
let data = response.data;
if(data.result && data.result.errno === 1000){
// 响应成功,保存access_token,回调函数
self.setdata({
"access_token": data.result.access_token
});
callback();
}else {
self.tips('令牌错误! 请重新登陆')
setTimeout(() => {
self.openWin('login', 'login.html')
}, 1500)
}
}
});
}
// 格式化数字(1,234,5.00)
number_format(number, decimals = 2, dec_point = '.', thousands_sep = ',') {
/*
* 参数说明:
* number:要格式化的数字
* decimals:保留几位小数
* dec_point:小数点符号
* thousands_sep:千分位符号
* */
number = (number + '').replace(/[^0-9+-Ee.]/g, '');
var n = !isFinite(+number) ? 0 : +number,
prec = !isFinite(+decimals) ? 0 : Math.abs(decimals),
sep = (typeof thousands_sep === 'undefined') ? ',' : thousands_sep,
dec = (typeof dec_point === 'undefined') ? '.' : dec_point,
s = '',
toFixedFix = function (n, prec) {
var k = Math.pow(10, prec);
return '' + Math.ceil(n * k) / k;
};
s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.');
var re = /(-?\d+)(\d{3})/;
while (re.test(s[0])) {
s[0] = s[0].replace(re, "$1" + sep + "$2");
}
if ((s[1] || '').length < prec) {
s[1] = s[1] || '';
s[1] += new Array(prec - s[1].length + 1).join('0');
}
return s.join(dec);
}
}
- 服务端提供使用refresh_token获取access_token的接口,
users/api.py
,代码:
# 验证用户是否携带 refresh_token, 刷新access_token
@jwt_required(refresh=True)
def refresh_token():
# 获取隐藏在jwt的载荷中的用户信息
user_data = get_jwt_identity()
# print(preload)
# 根据载荷中用户信息获取用户模型
user = services.get_user_by_id(id=user_data['id'])
# 判断用户是否存在
if user is None:
# jwt认证失败
return {
'errno': code.CODE_AUTOORIZATION_ERROR,
'errmsg': message.authorization_is_invalid
}
# 重新生成access_token
access_token = create_access_token(identity=user_data)
return {
"errno": code.CODE_OK,
"errmsg": message.ok,
"access_token": access_token
}
- 数据服务层获取用户模型对象,
users/services.py
,代码:
from .models import db, User
# 根据id获取用户信息
def get_user_by_id(id):
"""
根据id获取用户信息
:param id: 用户ID
:return: 用户模型对象
"""
user = User.query.filter(User.id == id).first()
return user
- 蓝图应用路由
users.urls
,代码:
from application import path, api_rpc
# 引入当前蓝图应用视图 , 引入rpc视图
from . import views, api
# 蓝图路径与函数映射列表
urlpatterns = []
# rpc方法与函数映射列表[rpc接口列表]
apipatterns = [
api_rpc('check_mobile', api.check_mobile),
api_rpc('register', api.register),
api_rpc('login', api.login),
api_rpc('access', api.access_token),
api_rpc('refresh', api.refresh_token),
]
显示当前用户信息
基于Faker生成仿真测试数据
文档:https://faker.readthedocs.io/en/master/
基本使用的方法大全,代码:
# 随机IP地址
from faker import Faker
from faker.providers import internet
fake = Faker()
fake.add_provider(internet)
print(fake.ipv4_private())
# 产生随机手机号
print(fake.phone_number())
# 产生随机姓名
print(fake.name())
# 产生随机地址
print(fake.address())
# 随机产生国家名
print(fake.country())
# 随机产生国家代码
print(fake.country_code())
# 随机产生城市名
print(fake.city_name())
# 随机产生城市
print(fake.city())
# 随机产生省份
print(fake.province())
# 产生随机email
print(fake.email())
# 产生随机IPV4地址
print(fake.ipv4())
# 产生长度在最大值与最小值之间的随机字符串
print(faker.pystr(min_chars=0, max_chars=8))
# 随机产生车牌号
print(fake.license_plate())
# 随机产生颜色
print(fake.rgb_color()) # rgb
print(fake.safe_hex_color()) # 16进制
print(fake.color_name()) # 颜色名字
print(fake.hex_color()) # 16进制
# 随机产生公司名
print(fake.company())
# 随机产生工作岗位
print(fake.job())
# 随机生成密码
print(fake.password(length=10, special_chars=True, digits=True, upper_case=True, lower_case=True))
# 随机生成uuid
print(fake.uuid4())
# 随机生成sha1
print(fake.sha1(raw_output=False))
# 随机生成md5
print(fake.md5(raw_output=False))
# 随机生成女性名字
print(fake.name_female())
# 男性名字
print(fake.name_male())
# 随机生成名字
print(fake.name())
# 生成基本信息
print(fake.profile(fields=None, sex=None))
print(fake.simple_profile(sex=None))
# 随机生成浏览器头user_agent
print(fake.user_agent())
# 随机产生时间 月份
print(fake.month_name())
# 'May'
print(fake.date_time_this_century(before_now=True, after_now=False, tzinfo=None))
# 207-09-18 12:21:32
print(fake.time_object(end_datetime=None))
# 16:51:21
print(fake.date_time_between(start_date="-10y", end_date="now", tzinfo=None))
# 2015-11-15 21:07:38
print(fake.future_date(end_date="+30d", tzinfo=None))
# 2020-04-25
print(fake.date_time(tzinfo=None, end_datetime=None))
# 2002-09-01 18:27:45
print(fake.date(pattern="%Y-%m-%d", end_datetime=None))
# '1998-08-02'
print(fake.date_time_this_month(before_now=True, after_now=False, tzinfo=None))
# 2020-04-03 16:03:21
print(fake.date_time_this_decade(before_now=True, after_now=False, tzinfo=None))
# 2020-01-09 01:15:08
print(fake.month())
# '11'
print(fake.day_of_week())
# 'Sunday'
print(fake.date_object(end_datetime=None))
# 2017-06-26
print(fake.date_this_decade(before_today=True, after_today=False))
# 2020-03-30
fake.date_this_century(before_today=True, after_today=False)
# datetime.date(2000, 6, 1)
fake.date_this_month(before_today=True, after_today=False)
# datetime.date(2018, 6, 13)
fake.past_datetime(start_date="-30d", tzinfo=None)
# datetime.datetime(2018, 6, 25, 7, 41, 34)
fake.date_this_year(before_today=True, after_today=False)
# datetime.date(2018, 2, 24)
fake.date_time_between_dates(datetime_start=None, datetime_end=None, tzinfo=None)
# datetime.datetime(2018, 6, 26, 14, 40, 5)
fake.date_time_ad(tzinfo=None, end_datetime=None)
# datetime.datetime(673, 1, 28, 18, 17, 55)
fake.date_between_dates(date_start=None, date_end=None)
# datetime.date(2018, 6, 26)
fake.future_datetime(end_date="+30d", tzinfo=None)
# datetime.datetime(2018, 7, 4, 10, 53, 6)
fake.past_date(start_date="-30d", tzinfo=None)
# datetime.date(2018, 5, 30)
fake.time(pattern="%H:%M:%S", end_datetime=None)
# '01:32:14'
fake.day_of_month()
# '19'
fake.unix_time(end_datetime=None, start_datetime=None)
fake.date_time_this_year(before_now=True, after_now=False, tzinfo=None)
# datetime.datetime(2018, 5, 24, 11, 25, 25)
fake.date_between(start_date="-30y", end_date="today")
# datetime.date(2003, 1, 11)
fake.year()
# '1993'
生成测试数据
- 因为页面中需要显示用户积分,服务端的模型中本身并没有设置当前字段,所以我们先到服务端中增加积分字段credit, 添加两个储存数据的操作add和add_all
user/models.py
from application.utils.models import BaseModel, db
from werkzeug.security import generate_password_hash, check_password_hash
class User(BaseModel):
"""用户基本信息表"""
__tablename__ = "mf_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.Numeric(7, 2), default=0.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', primaryjoin='User.id == UserProfile.user_id', foreign_keys='UserProfile.user_id', backref='user', uselist=False)
"""
密码存取器是成对出现的,函数也是一样的
"""
@property
def password(self):
return self._password
@password.setter
def password(self, rawpwd):
"""密码加密"""
self._password = generate_password_hash(rawpwd)
def check_password(self, rawpwd):
"""验证密码"""
return check_password_hash(self.password, rawpwd)
@classmethod
def add(cls, instance):
db.session.add(instance)
db.session.commit()
@classmethod
def add_all(cls, instance_list):
db.session.add_all(instance_list)
db.session.commit()
class UserProfile(BaseModel):
"""用户详情信息表"""
__tablename__ = "mf_user_profile"
user_id = db.Column(db.Integer, 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="现居住地址")
删除数据库中已有的user表,并重启项目。这时候,我们之前创建的所有用户信息都已经被清除了。开发中经常会遇到因为数据表结构发生改变导致需要清除数据的情况。我们肯定需要有一个快速还原测试数据的功能。
- 定义终端命令,快速生成测试数据
application/utils/commands.py
import os, inspect, unittest, random
from flask_script import Command, Option
from importlib import import_module
from faker.providers import internet
# 自定义注册命令
def load_commands(manager, command_path=None):
"""根据指定导包路径注册自定义命令到manage中"""
if command_path is None:
command_path = 'application.utils.commands'
# 根据路径导包
module = import_module(command_path)
# 提前文件中所有命令类成员
class_list = inspect.getmembers(module, inspect.isclass)
# print( class_list )
"""
[
('BlueprintCommand', <class 'application.utils.commands.BlueprintCommand'>),
('Command', <class 'flask_script.commands.Command'>),
('Option', <class 'flask_script.commands.Option'>)
]
"""
for item in class_list:
# 判断命令类是否是Command的子类,以为Command也是Command的子类,所有去除Command命令类注册
if issubclass(item[1],Command) and item[0] != 'Command':
# 注册命令类到manage中
manager.add_command(item[1].name, item[1])
# 生成蓝图目录结构
class BlueprintCommand(Command):
"""生成蓝图"""
# 生成蓝图命令名称
name = 'blue'
# 传递参数
option_list = [
Option('-n', '--name', dest='name'),
]
# 运行程序
def run(self, name):
# 生成蓝图目录结构
os.mkdir(name)
open(f'{name}/__init__.py', 'w')
open(f'{name}/views.py', 'w')
open(f'{name}/models.py', 'w')
with open(f'{name}/urls.py', 'w')as f:
content = """from application import path, api_rpc
# 引入当前蓝图应用视图, 引入rpc视图
from . import views, api
# 蓝图路径与函数映射列表
urlpatterns = [
# path('/index', views.index, methods=['post']),
]
# rpc方法与函数映射列表[rpc接口列表]
# user = api.User() # 实例化类视图
apipatterns = [
# api_rpc('index', api.index), # 函数映射
# api_rpc('User.login', user.login), # 类映射
]"""
f.write(content)
open(f'{name}/test.py', 'w')
open(f'{name}/api.py', 'w')
open(f'{name}/socket.py', 'w')
open(f'{name}/tasks.py', 'w')
open(f'{name}/marshmallow.py', 'w')
print(f'蓝图{name}创建完成...')
# 单元测试用例命令
class UnitTestServer(Command):
"""单元测试用例"""
# 生成命令名称
name = 'test'
def __call__(self, app, *args, **kwargs):
# 循环注册蓝图列表
for test_path in app.config.get('INSTALL_BLUEPRINT'):
# 自动找到目录下的test.py测试文件并导入
tests = unittest.TestLoader().discover(test_path)
# 运行测试用例
unittest.TextTestRunner(verbosity=2).run(tests)
# 批量生成测试数据
class FakerCommand(Command):
"""批量生成测试数据"""
name = 'faker' # 生成命令名称
# 传递的参数
option_list = [
Option('--type', '-t', dest='type', default='user'), # 指定生成数据的类型(用户数据,其他数据)
Option('--num', '-n', dest='num', default=1), # 生成数据的数量
]
# 以后想要生成数据类型列表
type_list = ['user']
def __call__(self, app, type, num):
# 判断想要生成数据的类型是否在类型列表中
if type not in self.type_list:
print("数据类型不正确\n当前Faker生成数据类型仅支持: %s" % self.type_list)
return None
num = int(num)
if num < 1:
print("生成数量不正确\n当前Faker生成数据至少1个以上")
return None
if type == 'user':
self.create_user(app,num)
# 生成指定数量的测试用户信息
def create_user(self,app,num):
"""生成指定数量的测试用户信息"""
from application.apps.users.models import User,UserProfile
faker = app.faker
faker.add_provider(internet)
user_list = [] # 用户模型对象列表
# 从配置文件中读取默认测试用户的登录密码
password = app.config.get("DEFAULT_TEST_USER_PASSWORD", "12345678")
for i in range(num):
sex = bool(random.randint(0,2))
nickname = faker.name()
# 登录账号[随机字母,6-16位]
name = faker.pystr(min_chars=6, max_chars=16)
age = random.randint(13, 50)
birthday = faker.date_time_between(start_date="-%sy" % age, end_date="-12y", tzinfo=None)
hometown_province = faker.province()
hometown_city = faker.city()
hometown_area = faker.district()
living_province = faker.province()
living_city = faker.city()
living_area = faker.district()
user = User(
nickname=nickname,
sex=sex,
name=name,
age=age,
password=password,
money=random.randint(100, 99999),
credit=random.randint(100, 99999),
ip_address=faker.ipv4_public(),
email=faker.ascii_free_email(),
mobile=faker.phone_number(),
unique_id=faker.uuid4(),
province=faker.province(),
city=faker.city(),
area=faker.district(),
info=UserProfile(
birthday=birthday,
hometown_province=hometown_province,
hometown_city=hometown_city,
hometown_area=hometown_area,
hometown_address=hometown_province + hometown_city + hometown_area + faker.street_address(),
living_province=living_province,
living_city=living_city,
living_area=living_area,
living_address=living_province + living_city + living_area + faker.street_address()
)
)
user_list.append(user)
# 存储数据
with app.app_context():
User.add_all(user_list)
print("生成%s个用户信息完成..." % num)
- 入口文件声明 faker作为app成员属性
application.__init__
,代码:
# 先内置,后官方,然后第三方,接着是自己的。
// ...
def init_app(config_path):
"""用于创建app实例对象并完成初始化过程的工厂函数"""
# 省略代码....
# faker作为app成员属性
app.faker = faker
# db创建数据表
with app.app_context():
db.create_all()
# 终端脚本工具加载配置
manage.app = app
# 自动注册自定义命令
load_commands(manage) # 导入默认的那个命令文件
return manage
- 开发环境配置测试用户的密码
application/settings/dev.py
"""默认测试用户密码"""
DEFAULT_TEST_USER_PASSWORD = '12345678'
- 终端执行如下操作,生成指定数据的用户信息。
python manage.py faker -tuser -n10
服务端增加返回用户的其他信息
在users/marshmallow.py
中UserSchema构造器,新增多个返回客户端字段。
# 用户注册数据校验模型构造器
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 = False # 启用外键关系
include_relationships = True # 模型关系外部属性
# 如果要返回客户端用户模型的全部字段,就不要声明fields或exclude字段即可
fields = ['id', 'name', "nickname", "money", "credit", "avatar",'mobile', 'password', 'password2', 'sms_code']
# 处理金钱数据转换成浮点型数据
@post_dump
def get_object(self, data, **kwargs):
data["money"] = float(data["money"])
data["credit"] = float(data["credit"])
return data
@post_load
def save_object(self, data, **kwargs):
"""保存用户基本信息"""
# 删除不必要的字段
data.pop('password2')
data.pop('sms_code')
data['name'] = faker.name() # 使用随机生成的名字作为用户初始姓名
instance = add_user(data) # 存储数据
return instance
# 多字段校验
@validates_schema
def validate_password(self, data, **kwargs):
# 校验两次密码是否输入正确
if data['password'] != data['password2']:
raise ValidationError(message=message.password_not_match, field_name='password')
# 校验短信验证码
# 1.从redis中提取验证码
redis_sms_code = redis_check.get('sms_%s' % data['mobile'])
if redis_sms_code is None:
raise ValidationError(message=message.sms_code_expired, field_name='sms_code')
# python从redis中提取的数据最终都是bytes类型,所以需要进行编码处理
redis_sms_code = redis_sms_code.decode()
# 2.从客户端提交的数据data中提取验证码
sms_code = data['sms_code']
# 3.两者比较是否相等,不相等,说明验证码输入不正确
if redis_sms_code != sms_code:
raise ValidationError(message=message.sms_code_not_match, field_name='sms_code')
return data
个人中心页面展示用户信息
html/user.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app user" id="app">
<div class="bg">
<img src="../static/images/bg0.jpg">
</div>
<img class="back" @click="back" src="../static/images/user_back.png" alt="">
<img class="setting" src="../static/images/setting.png" alt="">
<div class="header">
<div class="info">
<div class="avatar">
<img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
<img class="user_avatar" :src="user_data.avatar" alt="">
<img class="avatar_border" src="../static/images/avatar_border.png" alt="">
</div>
<p class="user_name">{{user_data.nickname}}</p>
</div>
<div class="wallet">
<div class="balance">
<p class="title"><img src="../static/images/money.png" alt="">钱包</p>
<p class="num">{{user_data.money_format}}</p>
</div>
<div class="balance">
<p class="title"><img src="../static/images/integral.png" alt="">果子</p>
<p class="num">{{user_data.credit_format}}</p>
</div>
</div>
<div class="invite">
<img class="invite_btn" src="../static/images/invite.png" alt="">
</div>
</div>
<div class="menu">
<div class="item">
<span class="title">我的主页</span>
<span class="value">查看</span>
</div>
<div class="item">
<span class="title">任务列表</span>
<span class="value">75%</span>
</div>
<div class="item">
<span class="title">收益明细</span>
<span class="value">查看</span>
</div>
<div class="item">
<span class="title">实名认证</span>
<span class="value">未认证</span>
</div>
<div class="item">
<span class="title">问题反馈</span>
<span class="value">去反馈</span>
</div>
</ul>
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg4.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
prev:{name:"",url:"",params:{}},
current:{name:"user",url:"user.html",params:{}},
user_data:{},
}
},
// 页面加载之前获取用户数据
created(){
// 获取token
let token = this.game.getfs('access_token') || this.game.getdata('access_token')
// 根据token获取用户数据
this.user_data = this.game.get_user_by_token(token)
// this.game.print(this.user_data)
// 格式化数字变成金钱格式,原始数据不变
this.user_data.money_format = this.game.number_format(this.user_data.money)
this.user_data.credit_format = this.game.number_format(this.user_data.credit)
},
methods:{
back(){
// 返回首页
this.game.closeWin();
},
}
});
}
</script>
</body>
</html>
对数值进行格式化成金额数字(1,234,56.00)形式 ;
static/js/main.js
,代码:
// 格式化数字(1,234,5.00)
number_format(number, decimals = 2, dec_point = '.', thousands_sep = ',') {
/*
* 参数说明:
* number:要格式化的数字
* decimals:保留几位小数
* dec_point:小数点符号
* thousands_sep:千分位符号
* */
number = (number + '').replace(/[^0-9+-Ee.]/g, '');
var n = !isFinite(+number) ? 0 : +number,
prec = !isFinite(+decimals) ? 0 : Math.abs(decimals),
sep = (typeof thousands_sep === 'undefined') ? ',' : thousands_sep,
dec = (typeof dec_point === 'undefined') ? '.' : dec_point,
s = '',
toFixedFix = function (n, prec) {
var k = Math.pow(10, prec);
return '' + Math.ceil(n * k) / k;
};
s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.');
var re = /(-?\d+)(\d{3})/;
while (re.test(s[0])) {
s[0] = s[0].replace(re, "$1" + sep + "$2");
}
if ((s[1] || '').length < prec) {
s[1] = s[1] || '';
s[1] += new Array(prec - s[1].length + 1).join('0');
}
return s.join(dec);
}
基于Vue-avatar组件显示用户头像
核心组件js下载 ; https://unpkg.com/@amaury-tobias/v-avatar@2.0.3/dist/v-avatar.min.js
把v-avatar-2.0.3.min.js文件添加到 static/js
目录中
用户页面引入v-avatar-2.0.3.min.js组件 ,进行头像处理, html/user.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<!-- 引入用户头像处理js文件 -->
<script src="../static/js/v-avatar-2.0.3.min.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app user" id="app">
<div class="bg">
<img src="../static/images/bg0.jpg">
</div>
<img class="back" @click="back" src="../static/images/user_back.png" alt="">
<img class="setting" src="../static/images/setting.png" alt="">
<div class="header">
<div class="info">
<div class="avatar">
<img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
<!-- 用户头像处理 -->
<div class="user_avatar">
<v-avatar v-if="user_data.avatar" :src="user_data.avatar" :size="55" :rounded="true"></v-avatar>
<v-avatar v-else-if="user_data.nickname" :username="user_data.nickname" :size="55" :rounded="true"></v-avatar>
<v-avatar v-else :username="user_data.id" :size="55" :rounded="true"></v-avatar>
</div>
<img class="avatar_border" src="../static/images/avatar_border.png" alt="">
</div>
<p class="user_name">{{user_data.nickname}}</p>
</div>
<div class="wallet">
<div class="balance">
<p class="title"><img src="../static/images/money.png" alt="">钱包</p>
<p class="num">{{user_data.money_format}}</p>
</div>
<div class="balance">
<p class="title"><img src="../static/images/integral.png" alt="">果子</p>
<p class="num">{{user_data.credit_format}}</p>
</div>
</div>
<div class="invite">
<img class="invite_btn" src="../static/images/invite.png" alt="">
</div>
</div>
<div class="menu">
<div class="item">
<span class="title">我的主页</span>
<span class="value">查看</span>
</div>
<div class="item">
<span class="title">任务列表</span>
<span class="value">75%</span>
</div>
<div class="item">
<span class="title">收益明细</span>
<span class="value">查看</span>
</div>
<div class="item">
<span class="title">实名认证</span>
<span class="value">未认证</span>
</div>
<div class="item">
<span class="title">问题反馈</span>
<span class="value">去反馈</span>
</div>
</ul>
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg4.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
prev:{name:"",url:"",params:{}},
current:{name:"user",url:"user.html",params:{}},
user_data:{},
}
},
// 页面加载之前获取用户数据
created(){
let token = this.game.getfs('access_token') || this.game.getdata('access_token')
this.user_data = this.game.get_user_by_token(token)
// this.game.print(this.user_data)
// 格式化数字变成金钱格式,原始数据不变
this.user_data.money_format = this.game.number_format(this.user_data.money)
this.user_data.credit_format = this.game.number_format(this.user_data.credit)
},
methods:{
back(){
// 返回首页
this.game.closeWin();
},
}
});
}
</script>
</body>
</html>
系统设置页面
- 个人中心点击系统设置按钮,跳转到系统设置页面
html/user.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<!-- 引入用户头像处理js文件 -->
<script src="../static/js/v-avatar-2.0.3.min.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app user" id="app">
<div class="bg">
<img src="../static/images/bg0.jpg">
</div>
<img class="back" @click="back" src="../static/images/user_back.png" alt="">
<img class="setting" @click='to_settings' src="../static/images/setting.png" alt="">
<div class="header">
<div class="info">
<div class="avatar">
<img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
<!-- 用户头像处理 -->
<div class="user_avatar">
<v-avatar v-if="user_data.avatar" :src="user_data.avatar" :size="55" :rounded="true"></v-avatar>
<v-avatar v-else-if="user_data.nickname" :username="user_data.nickname" :size="55" :rounded="true"></v-avatar>
<v-avatar v-else :username="user_data.id" :size="55" :rounded="true"></v-avatar>
</div>
<img class="avatar_border" src="../static/images/avatar_border.png" alt="">
</div>
<p class="user_name">{{user_data.nickname}}</p>
</div>
<div class="wallet">
<div class="balance">
<p class="title"><img src="../static/images/money.png" alt="">钱包</p>
<p class="num">{{user_data.money_format}}</p>
</div>
<div class="balance">
<p class="title"><img src="../static/images/integral.png" alt="">果子</p>
<p class="num">{{user_data.credit_format}}</p>
</div>
</div>
<div class="invite">
<img class="invite_btn" src="../static/images/invite.png" alt="">
</div>
</div>
<div class="menu">
<div class="item">
<span class="title">我的主页</span>
<span class="value">查看</span>
</div>
<div class="item">
<span class="title">任务列表</span>
<span class="value">75%</span>
</div>
<div class="item">
<span class="title">收益明细</span>
<span class="value">查看</span>
</div>
<div class="item">
<span class="title">实名认证</span>
<span class="value">未认证</span>
</div>
<div class="item">
<span class="title">问题反馈</span>
<span class="value">去反馈</span>
</div>
</ul>
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg4.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
prev:{name:"",url:"",params:{}},
current:{name:"user",url:"user.html",params:{}},
user_data:{},
}
},
// 页面加载之前获取用户数据
created(){
let token = this.game.getfs('access_token') || this.game.getdata('access_token')
this.user_data = this.game.get_user_by_token(token)
// this.game.print(this.user_data)
// 格式化数字变成金钱格式,原始数据不变
this.user_data.money_format = this.game.number_format(this.user_data.money)
this.user_data.credit_format = this.game.number_format(this.user_data.credit)
},
methods:{
back(){
// 返回首页
this.game.closeWin();
},
// 点击设置按钮,跳转到系统设置页面
to_settings(){
this.game.openFrame('settings', 'settings.html')
}
}
});
}
</script>
</body>
</html>
- 客户端新增系统设置页面
html/settings.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app user setting" id="app">
<div class="bg">
<img src="../static/images/form_bg.png">
</div>
<img class="back" @click="quit" src="../static/images/user_back.png" alt="">
<div class="form">
<div class="item avatar">
<span class="title">头像</span>
<span class="goto">></span>
<span class="value">
<img src="../static/images/avatar.png" alt="">
</span>
</div>
<div class="item">
<span class="title">昵称</span>
<span class="goto">></span>
<span class="value">清蒸小帅锅</span>
</div>
<div class="item">
<span class="title">手机号</span>
<span class="goto">></span>
<span class="value">139****5901</span>
</div>
<div class="item">
<span class="title">登陆密码</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">交易密码</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">地址管理</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">设备管理</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item logout">
<img @click="change_account" src="../static/images/change_account.png" alt="">
<p @click="logout">退出账号</p>
</div>
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
prev:{name:"",url:"",params:{}},
current:{name:"setting",url:"setting.html",params:{}},
}
},
methods:{
}
});
}
</script>
</body>
</html>
- 追加css样式
static/css/main.css
,样式代码:
.setting .bg img{
animation: normal;
}
.setting .back{
top: 4rem;
}
.setting .form {
top: 9rem;
}
.setting .form .item{
height: 3.9rem;
line-height: 3.9rem;
font-size: 1.25rem;
text-indent: 0.6rem;
border-bottom: 1px solid rgba(204,153,102,0.2);
}
.setting .form .avatar{
height: 6.11rem;
line-height: 6.11rem;
}
.setting .form .avatar img{
width: 4.56rem;
height: 4.56rem;
vertical-align: middle;
}
.setting .form .item .value,
.setting .form .item .goto{
float: right;
}
.setting .form .logout{
margin-top: 3rem;
text-align: center;
height: 4rem;
line-height: 2rem;
}
.setting .form .logout img{
width: 11rem;
}
退出登陆 - 切换账号
APP项目中对于用户的退出登录,一般都在系统设置中进行。
html/settings.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app user setting" id="app">
<div class="bg">
<img src="../static/images/form_bg.png">
</div>
<img class="back" @click="quit" src="../static/images/user_back.png" alt="">
<div class="form">
<div class="item avatar">
<span class="title">头像</span>
<span class="goto">></span>
<span class="value">
<img src="../static/images/avatar.png" alt="">
</span>
</div>
<div class="item">
<span class="title">昵称</span>
<span class="goto">></span>
<span class="value">清蒸小帅锅</span>
</div>
<div class="item">
<span class="title">手机号</span>
<span class="goto">></span>
<span class="value">139****5901</span>
</div>
<div class="item">
<span class="title">登陆密码</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">交易密码</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">地址管理</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">设备管理</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item logout">
<img @click="change_account" src="../static/images/change_account.png" alt="">
<p @click="logout">退出账号</p>
</div>
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
prev:{name:"",url:"",params:{}},
current:{name:"setting",url:"setting.html",params:{}},
}
},
methods:{
// 返回上一页
quit(){
// 退出当前帧页面
this.game.closeFrame();
},
// 切换账号
change_account(){
this.game.openWin("login","login.html"); // 打开登陆窗口
setTimeout(() => {
this.game.closeWin() // 退出当前窗口
},1000)
},
// 退出账号
logout(){
// 底部弹出框
api.actionSheet({
title: '您确认要退出当前登录吗?',
cancelTitle: '取消', // 取消按钮,buttonIndex=最后一个值
destructiveTitle: "退出登录", // 默认按钮,会自动高亮显示buttonIndex=1
}, (ret, err)=>{
if( ret ){
// this.game.print(ret,1);
if(ret.buttonIndex==1){
this.game.deldata(["access_token","refresh_token"]);
this.game.delfs(["access_token","refresh_token"]);
this.game.openWin("root","index.html"); // 返回主页面
setTimeout(() => {
this.game.closeWin() // 退出当前窗口
},1000)
}
}
});
},
}
});
}
</script>
</body>
</html>
系统设置页面中显示用户个人信息
- 服务端处理返回的手机号
apps/users/marshmallow.py
# 用户注册数据校验模型构造器
class UserSchema(SQLAlchemyAutoSchema):
mobile = auto_field(required=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 = False # 启用外键关系
include_relationships = True # 模型关系外部属性
# 如果要返回客户端用户模型的全部字段,就不要声明fields或exclude字段即可
fields = ['id', 'name', "nickname", "money", "credit", "avatar",'mobile', 'password', 'password2', 'sms_code']
# 对返回客户端的数据进行处理
@post_dump
def get_object(self, data, **kwargs):
data["money"] = float(data["money"])
data["credit"] = float(data["credit"])
data['mobile'] = data['mobile'][:3] + '****' + data['mobile'][-4:]
return data
@post_load
def save_object(self, data, **kwargs):
"""保存用户基本信息"""
# 删除不必要的字段
data.pop('password2')
data.pop('sms_code')
data['name'] = faker.name() # 使用随机生成的名字作为用户初始姓名
instance = add_user(data) # 存储数据
return instance
# 多字段校验
@validates_schema
def validate_password(self, data, **kwargs):
# 校验两次密码是否输入正确
if data['password'] != data['password2']:
raise ValidationError(message=message.password_not_match, field_name='password')
# 校验短信验证码
# 1.从redis中提取验证码
redis_sms_code = redis_check.get('sms_%s' % data['mobile'])
if redis_sms_code is None:
raise ValidationError(message=message.sms_code_expired, field_name='sms_code')
# python从redis中提取的数据最终都是bytes类型,所以需要进行编码处理
redis_sms_code = redis_sms_code.decode()
# 2.从客户端提交的数据data中提取验证码
sms_code = data['sms_code']
# 3.两者比较是否相等,不相等,说明验证码输入不正确
if redis_sms_code != sms_code:
raise ValidationError(message=message.sms_code_not_match, field_name='sms_code')
return data
- 系统设置页面展示用户信息 ;
html/settings.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/v-avatar-2.0.3.min.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app user setting" id="app">
<div class="bg">
<img src="../static/images/form_bg.png">
</div>
<img class="back" @click="quit" src="../static/images/user_back.png" alt="">
<div class="form">
<div class="item avatar">
<span class="title">头像</span>
<span class="goto">></span>
<span class="value">
<!-- 头像处理 -->
<v-avatar v-if="user_data.avatar" :src="user_data.avatar" :size="55" :rounded="true"></v-avatar>
<v-avatar v-else-if="user_data.nickname" :username="user_data.nickname" :size="55" :rounded="true"></v-avatar>
<v-avatar v-else :username="user_data.id" :size="55" :rounded="true"></v-avatar>
</span>
</div>
<div class="item">
<span class="title">昵称</span>
<span class="goto">></span>
<span class="value">{{user_data.nickname}}</span>
</div>
<div class="item">
<span class="title">手机号</span>
<span class="goto">></span>
<span class="value">{{user_data.mobile}}</span>
</div>
<div class="item">
<span class="title">登陆密码</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">交易密码</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">地址管理</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">设备管理</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item logout">
<img @click="change_account" src="../static/images/change_account.png" alt="">
<p @click="logout">退出账号</p>
</div>
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
user_data: {}, // 用户数据
}
},
// 页面加载之前获取用户数据
created(){
// 获取token
let token = this.game.getfs('access_token') || this.game.getdata('access_token')
// 通过token值获取用户数据
this.user_data = this.game.get_user_by_token(token)
},
methods:{
quit(){
//
this.game.closeFrame();
},
change_account(){
// 切换账号
this.game.openWin("login","login.html"); // 打开登陆窗口
this.game.closeWin(); // 退出当前窗口
},
logout(){
// 退出账号
api.actionSheet({
title: '您确认要退出当前登录吗?',
cancelTitle: '取消', // 取消按钮,buttonIndex=最后一个值
destructiveTitle: "退出登录", // 默认按钮,会自动高亮显示buttonIndex=1
buttons: ['再考虑1下','再考虑2下'] // buttonIndex=2
}, (ret, err)=>{
if( ret ){
// this.game.print(ret,1);
if(ret.buttonIndex==1){
this.game.deldata(["access_token","refresh_token"]);
this.game.delfs(["access_token","refresh_token"]);
this.game.closeWin();
}
}
});
}
}
});
}
</script>
</body>
</html>
更新头像
客户端进行头像上传处理
- 系统设置页面点击头像进入更新头像页面,
html/setting.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/v-avatar-2.0.3.min.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app user setting" id="app">
<div class="bg">
<img src="../static/images/form_bg.png">
</div>
<img class="back" @click="quit" src="../static/images/user_back.png" alt="">
<div class="form">
<div class="item avatar">
<span class="title">头像</span>
<span class="goto">></span>
<span class="value" @click="update_avatar">
<v-avatar v-if="user_info.avatar" :src="user_info.avatar" :size="55" :rounded="true"></v-avatar>
<v-avatar v-else-if="user_info.nickname" :username="user_info.nickname" :size="55" :rounded="true"></v-avatar>
<v-avatar v-else :username="user_info.id" :size="55" :rounded="true"></v-avatar>
</span>
</div>
<div class="item">
<span class="title">昵称</span>
<span class="goto">></span>
<span class="value">{{user_info.nickname}}</span>
</div>
<div class="item">
<span class="title">手机号</span>
<span class="goto">></span>
<span class="value">{{user_info.mobile}}</span>
</div>
<div class="item">
<span class="title">登陆密码</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">交易密码</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">地址管理</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">设备管理</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item logout">
<img @click="change_account" src="../static/images/change_account.png" alt="">
<p @click="logout">退出账号</p>
</div>
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
prev:{name:"",url:"",params:{}},
user_info:{},
token:{},
current:{name:"setting",url: "setting.html",params:{}},
}
},
mounted(){
this.token = this.game.getdata("access_token") || this.game.getfs("access_token");
this.user_info = this.game.get_user_data(this.token).sub;
},
methods:{
quit(){
//
this.game.closeFrame();
},
change_account(){
// 切换账号
this.game.openWin("login","login.html"); // 打开登陆窗口
this.game.closeWin(); // 退出当前窗口
},
logout(){
// 退出账号
api.actionSheet({
title: '您确认要退出当前登录吗?',
cancelTitle: '取消', // 取消按钮,buttonIndex=最后一个值
destructiveTitle: "退出登录", // 默认按钮,会自动高亮显示buttonIndex=1
buttons: ['再考虑1下','再考虑2下'] // buttonIndex=2
}, (ret, err)=>{
if( ret ){
// this.game.print(ret,1);
if(ret.buttonIndex==1){
this.game.deldata(["access_token","refresh_token"]);
this.game.delfs(["access_token","refresh_token"]);
this.game.closeWin();
}
}
});
},
update_avatar(){
// 更新头像
this.game.openFrame("avatar","avatar.html","from_top");
}
}
});
}
</script>
</body>
</html>
static/js/main.js
,修改openFrame打开帧页面方法的参数,代码:
openFrame(name,url,redirect="from_right",pageParam={}){
// 打开帧页面
api.openFrame({
name: name, // 帧页面的名称
url: url, // 帧页面打开的url地址
bounces:false, // 页面是否可以下拉拖动
reload: true, // 帧页面如果已经存在,是否重新刷新加载
useWKWebView: true,
historyGestureEnabled:true,
animation:{
type:"push", //动画类型(详见动画类型常量)
subType:redirect, //动画子类型(详见动画子类型常量)
duration:300 //动画过渡时间,默认300毫秒
},
rect: {
x: 0, // 左上角x轴坐标
y: 0, // 左上角y轴坐标
w: 'auto', // 当前帧页面的宽度, auto表示满屏
h: 'auto' // 当前帧页面的高度, auto表示满屏
}, // 当前帧的宽高范围
pageParam: pageParam, // 要传递新建帧页面的参数,在新页面可通过 api.pageParam.name 获取
});
}
- 添加更新头像页面
html/avatar.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app frame avatar" id="app">
<div class="box">
<p class="title">上传头像</p>
<img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
<div class="content">
<p class="header">!注意事项</p>
<p class="text">禁止使用有诱导性的内容,二维码,联系方式等违规违法违约的图片,一经发现,永久封号,
并保留追究法律责任的权利。</p>
</div>
<img @click="update_avatar_confirm" class="btn" src="../static/images/yes.png" alt="">
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
prev:{name:"",url:"",params:{}},
current:{name:"avatar",url:"avatar.html",params:{}},
}
},
methods:{
update_avatar_confirm(){
// 确认上传头像
},
upload_avatar(ret){
// 头像上传处理
},
back(){
this.game.closeFrame();
}
}
});
}
</script>
</body>
</html>
- 添加页面样式 ;
static/css/main.css
,代码:
.avatar.frame{
background-color: rgba(0,0,0,0.6);
}
.avatar{
overflow: hidden;
}
.avatar .box{
width: 28.89rem;
height: 34.44rem;
background: url("../images/board_bg1.png") no-repeat 0 0;
background-size: 100%;
position: absolute;
top: 11rem;
margin: 0 auto;
left: 0;
right: 0;
}
.avatar .box .title{
color: #fff;
font-size: 2rem;
text-align: center;
margin-top: 2.8rem;
}
.avatar .box .close{
width: 5.22rem;
height: 5.78rem;
position: absolute;
right: 0;
top: 8rem;
}
.avatar .box .header{
margin-top: 3.6rem;
font-size: 1.8rem;
text-align: center;
color: #ff3333;
font-weight: bold;
}
.avatar .box .text{
width: 16.67rem;
margin: 1.4rem auto 0;
font-size: 1.22rem;
color: #ffffcc;
}
.avatar .box .btn{
display: block;
width: 12.22rem;
height: 4.55rem;
margin: 1.4rem auto 0;
}
- 头像上传来源选择(图库/相册/相机), 调用
apicloud
提供的本地接口从相册或者相机中提取图片,avatar.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app frame avatar" id="app">
<div class="box">
<p class="title">上传头像</p>
<img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
<div class="content">
<p class="header">!注意事项</p>
<p class="text">禁止使用有诱导性的内容,二维码,联系方式等违规违法违约的图片,一经发现,永久封号,
并保留追究法律责任的权利。</p>
</div>
<img @click="update_avatar_confirm" class="btn" src="../static/images/yes.png" alt="">
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
prev:{name:"",url:"",params:{}},
current:{name:"avatar",url:"avatar.html",params:{}},
}
},
methods:{
update_avatar_confirm(){
// 确认上传头像的来源
api.actionSheet({
title: '请选择上传头像的来源',
cancelTitle: '取消',
buttons: ['图片库','相册','相机'],
}, (ret, err)=>{
if( ret.buttonIndex == 1 ){
// 相册
this.game.print( JSON.stringify( ret ),1 );
}else if(ret.buttonIndex == 2){
// 相机
this.game.print( JSON.stringify( ret ),1 );
}
});
},
upload_avatar(ret){
// 头像上传处理
},
back(){
this.game.closeFrame();
}
}
});
}
</script>
</body>
</html>
服务端提供头像更新接口
使用阿里云存储用户头像
- 阿里云搜索对象存储oss, 创建存储数据仓库
网址: https://oss.console.aliyun.com/overview
开发文档 : https://help.aliyun.com/document_detail/85288.html?spm=a2c4g.11186623.6.1138.1de646a1TbBdSZ
# 数据仓库用到的
BucketName(仓库名称): mofangjia
Endpoint(地域节点): oss-cn-beijing.aliyuncs.com
Bucket 域名 : mofangjia.oss-cn-beijing.aliyuncs.com
# 阿里云账号AccessKey拥有访问权限
AccessKey ID : LTAI5tAnLDQEpD1wqsV8QDx1
AccessKey Secret : 5trgAkjbpzGGREFgbAIAkZlRn7pTfc
- 安装SDK - oss2
# 方法一: 先安装 再安装oss2
apt-get install python-dev
pip install oss2
# 方法二: Anaconda安装oss2
conda install -c conda-forge oss2
# 检验SDK - oss2是否安装成功
$在命令行输入python并回车,进入Python环境。
>>> import oss2
>>> oss2.__version__
'2.0.0'
- 开发环境配置云仓库配置
application/settings/dev.py
"""阿里云仓库OSS配置"""
OSS_BUCKETNAME = 'mofangjia' # 仓库名称
OSS_ENDPOINT = 'oss-cn-beijing.aliyuncs.com' # 地域节点
OSS_BUCKET = 'mofangjia.oss-cn-beijing.aliyuncs.com' # 仓库域名
# 阿里云账号AccessKey拥有访问权限
ACCESSKEY_ID = 'LTAI5tAnLDQEpD1wqsV8QDx1'
ACCESSKEY_SECRET = '5trgAkjbpzGGREFgbAIAkZlRn7pTfc'
- 封装阿里云对象储存类
application/utils/OssStore
import oss2
from typing import Union
# 阿里云对象存储工具类
class OssStore(object):
"""阿里云对象存储工具类"""
def __init__(self, app=None): # app是应用对象
self.bucket = None # oss实例化对象
self.path = None # 仓库网址
if app is not None:
self.init_app(app)
# 初始化
def init_app(self,app=None):
if app is None:
raise Exception
# 加载配置
self.access_key_id = app.config.get('ACCESSKEY_ID')
self.access_key_secret = app.config.get('ACCESSKEY_SECRET')
self.endpoint = app.config.get('OSS_ENDPOINT')
self.bucketph = app.config.get('OSS_BUCKET')
self.bucketname = app.config.get('OSS_BUCKETNAME')
self.app = app
# auth = oss2.Auth('<yourAccessKeyId>', '<yourAccessKeySecret>')
auth = oss2.Auth(self.access_key_id, self.access_key_secret)
# 实例化oss存储对象
# bucket = oss2.Bucket(auth, 'http://oss-cn-hangzhou.aliyuncs.com', '<yourBucketName>')
self.bucket = oss2.Bucket(auth, f'http://{self.endpoint}', self.bucketname)
# 仓库网址
self.path = f'https://{self.bucketph}'
# 上传图片到云仓库
def put_data(self, filename:str, data: Union[bytes, str]):
"""
上传图片数据到云仓库
:param filename: 图片名称
:param data: 数据类型 bytes字节流, str本地图片地址
:return: 查看图片网址
"""
content = b''
# 判断数据类型
if type(data) is str:
with open(data, 'rb') as f:
content = f.read()
if type(data) is bytes:
content = data
# conten必须有数据才上传图片
if len(content) > 1:
# 上传bytes类型数据
self.bucket.put_object(filename, content)
# 返回查看图片网址
return self.path + '/' + filename
else:
return None
注意,为了保证图片不被其他第三方恶意爬取,所以我们设置需要验证token才提供给客户端,
- 编写视图接口:
application/apps/users/api.py
,代码:
import base64, uuid, os
from flask import current_app, request
from flask_jwt_extended import get_jwt_identity, jwt_required,create_refresh_token,create_access_token
# 引入构造器(序列化器)
from .marshmallow import MobileSchema, ValidationError, UserSchema
# 引入返回信息,和自定义状态码
from application import message, code, oss
from . import services
from application.utils import captcha
# 更新用户头像
@jwt_required()
def update_avatar(avatar):
"""
更新用户头像
:param avatar: base64编码字符串 "data:image/jpeg;base64,/9j/4AAQ...="
:return:
"""
# 1.处理客户端上传的头像信息
ext = avatar[avatar.find('/')+1:avatar.find(';')] # 上传头像的格式(后缀jpg/png)
b64_avatar = avatar[avatar.find(',')+1:] # 上传头像的信息
b64_image = base64.b64decode(b64_avatar) # 解码
filename = uuid.uuid4() # 头像名称 - 随机
"""本地存储图片"""
# # 2.拼接头像完整路径
# static_path = os.path.join(current_app.BASE_DIR, current_app.config.get('STATIC_DIR', 'avatar'))
# avatar_path = "%s/%s.%s" % (static_path, filename, ext)
# # 3.保存头像信息到本地
# with open(avatar,"wb") as f:
# f.write(b64_image)
"""阿里云存储头像信息"""
# 4.返回查看头像网址
avatar_path = oss.put_data(f'{filename}.{ext}', b64_image)
# 5.数据库保存用户头像信息
# 获取用户信息 - 通过客户端传过来的token
user_data = get_jwt_identity()
# 获取用户模型对象
user = services.get_user_by_id(id=user_data['id'])
# 判断用户是否存在
if user is None:
return {
'errno': code.CODE_AUTOORIZATION_ERROR,
'errmsg': message.authorization_is_invalid
}
data = {'avatar': avatar_path}
# 保存用户信息
user = services.update_user_data(user, data)
# 6.把模型对象转换成字典
us = UserSchema()
user_data = us.dump(user)
# 7.更新token值,通过载荷把头像信息传到客户端
access_token = create_access_token(identity=user_data)
refresh_token = create_refresh_token(identity=user_data)
return {
'errno': code.CODE_OK,
'errmsg': message.ok,
'access_token': access_token,
'refresh_token': refresh_token
}
- 数据服务层, 保存更新用户数据
users/services.py
# 保存或更新用户信息
def update_user_data(user, data):
"""
保存或更新用户信息
:param user: 模型类对象
:param data: 用户数据,字典类型
:return: 模型对象
"""
for key,value in data.items():
# setattr() 函数指定更改对象的指定属性的值。
setattr(user, key, value)
db.session.commit()
return user
- 如果本地存储,设置存储目录
项目配置静态文件储存路径 : application/settings/__init__.py
,代码:
# 静态文件目录存储路径
STATIC_DIR = "application/static"
# 静态目录创建存储头像目录 - avatar
- 蓝图路由
users/urls.py
from application import path, api_rpc
# 引入当前蓝图应用视图 , 引入rpc视图
from . import views, api
# 蓝图路径与函数映射列表
urlpatterns = []
# rpc方法与函数映射列表[rpc接口列表]
apipatterns = [
api_rpc('check_mobile', api.check_mobile),
api_rpc('register', api.register),
api_rpc('login', api.login),
api_rpc('access', api.access_token),
api_rpc('refresh', api.refresh_token),
api_rpc('update_avatar', api.update_avatar),
]
客户端基于axios上传图片数据
- 上传头像成功, 全局广播;
html/avatar.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app frame avatar" id="app">
<div class="box">
<p class="title">上传头像</p>
<img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
<div class="content">
<p class="header">!注意事项</p>
<p class="text">禁止使用有诱导性的内容,二维码,联系方式等违规违法违约的图片,一经发现,永久封号,
并保留追究法律责任的权利。</p>
</div>
<img @click="update_avatar_confirm" class="btn" src="../static/images/yes.png" alt="">
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
prev:{name:"",url:"",params:{}},
current:{name:"avatar",url:"avatar.html",params:{}},
}
},
methods:{
// 确认上传头像
update_avatar_confirm(){
// 底部弹出框 - 选择上传头像来源
api.actionSheet({
title: '上传文件',
cancelTitle: '取消',
buttons: ['图片库','相册','相机']
}, (ret, err) => {
if( ret ){
// alert( JSON.stringify( ret ) ); // {"buttonIndex":1} or {"buttonIndex":2}..3..4
// 本地获取图片方式图片库/相册/相机
let sourceType = ['library', 'album', 'camera']
if(ret.buttonIndex > sourceType.length){
// 如果用户选择了取消,则关闭当前修改头像的页面
this.game.closeFrame();
return
}
// 获取图片信息
api.getPicture({
sourceType: sourceType[ret.buttonIndex - 1], // 图片源类型
encodingType: 'jpg', // 返回图片类型,jpg或png
mediaValue: 'pic', // 媒体类型,图片pic或视频video
destinationType: 'base64', // 返回数据类型
allowEdit: true, // 是否可以选择图片后进行编辑
quality: 50, // 图片质量,只针对jpg格式图片
// targetWidth: 100, // 压缩后的图片宽度
// targetHeight: 100, // 压缩后的图片高度
saveToPhotoAlbum: true // 拍照或录制视频后是否保存到系统相册目录
}, (ret, err) => {
if (ret) {
// this.game.print(ret); // 返回的是base64字符串
// 获取图片数据,进行上传处理
this.upload_avatar(ret)
} else {
alert(JSON.stringify(err));
}
});
}
});
},
// 头像上传处理
upload_avatar(ret){
let self = this
// 检验用户是否登陆
self.game.check_user_login(self,() =>{
// 获取token值
let token = self.game.getdata('access_token') || self.game.getfs('access_token');
// 发送请求
self.game.post(self,{
"method": "Users.update_avatar",
"params":{
"avatar":ret.base64Data
},
"header":{
"Authorization": "jwt " + token
},
success(response){
let data = response.data;
if(data.result && data.result.errno === 1000){
let token = self.game.getdata('access_token')
// 判断当前用户是否记住密码 - 更新access_token
if(token){
// 没记住密码清框
self.game.delfs(['access_token', 'refresh_token'])
self.game.setdata({
'access_token': data.result.access_token,
'refresh_token': data.result.refresh_token
})
}else {
// 记住密码情况
self.game.deldata(['access_token', 'refresh_token'])
self.game.setfs({
'access_token': data.result.access_token,
'refresh_token': data.result.refresh_token
})
}
// 客户端提示
self.game.tips('头像上传成功!')
// 全局广播,更新头像成功
self.game.sendEvent('update_avatar_success')
self.game.sendEvent('update_token')
// 关闭当前页面
setTimeout(() => {
self.game.closeFrame()
}, 1500)
}else {
self.game.tips('头像上传失败!')
}
}
});
});
},
// 返回上一页
back(){
this.game.closeFrame();
},
}
});
}
</script>
</body>
</html>
- 封装全局广播方法
static/js/main.js
class Game{
// ...省略
// 发送APP全局事件通知/全局事件广播
sendEvent(name, data={}){
if(!data){
api.sendEvent({
name: name,
extra: data
});
}else {
api.sendEvent({
name: name
});
}
}
// ...省略
}
客户端更新头像信息 - 通过广播
关闭avatar.html
页面, 通过全局广播update_token 更新token值, 更新用户数据
更新系统设置 html/settings.html
和用户中心html/user.html
中的头像.
- 更新系统设置中的头像
html/settings.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/v-avatar-2.0.3.min.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app user setting" id="app">
<div class="bg">
<img src="../static/images/form_bg.png">
</div>
<img class="back" @click="quit" src="../static/images/user_back.png" alt="">
<div class="form">
<div class="item avatar" @click='to_updata_avatar'>
<span class="title">头像</span>
<span class="goto">></span>
<span class="value">
<!-- 头像处理 -->
<v-avatar v-if="user_data.avatar" :src="user_data.avatar" :size="55" :rounded="true"></v-avatar>
<v-avatar v-else-if="user_data.nickname" :username="user_data.nickname" :size="55" :rounded="true"></v-avatar>
<v-avatar v-else :username="user_data.id" :size="55" :rounded="true"></v-avatar>
</span>
</div>
<div class="item">
<span class="title">昵称</span>
<span class="goto">></span>
<span class="value">{{user_data.nickname}}</span>
</div>
<div class="item">
<span class="title">手机号</span>
<span class="goto">></span>
<span class="value">{{user_data.mobile}}</span>
</div>
<div class="item">
<span class="title">登陆密码</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">交易密码</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">地址管理</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">设备管理</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item logout">
<img @click="change_account" src="../static/images/change_account.png" alt="">
<p @click="logout">退出账号</p>
</div>
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
user_data: {}, // 用户数据
}
},
// 页面加载之前获取用户数据
created(){
// 获取用户数据
this.get_user_data()
// 监听事件变化
this.listen()
},
methods:{
// 监听事件
listen(){
// 监听token更新的通知
this.listen_update_token();
},
// 监听token更新的通知
listen_update_token(){
api.addEventListener({
name: 'update_token'
}, (ret, err) => {
// 更新用户数据
this.get_user_data()
});
},
// 通过token值获取用户数据
get_user_data(){
// 获取token
let token = this.game.getfs('access_token') || this.game.getdata('access_token')
// 通过token值获取用户数据
this.user_data = this.game.get_user_by_token(token)
},
// 返回上一页
quit(){
// 退出当前帧页面
this.game.closeFrame();
},
// 切换账号
change_account(){
this.game.openWin("login","login.html"); // 打开登陆窗口
setTimeout(() => {
this.game.closeWin() // 退出当前窗口
},1000)
},
// 退出账号
logout(){
// 底部弹出框
api.actionSheet({
title: '您确认要退出当前登录吗?',
cancelTitle: '取消', // 取消按钮,buttonIndex=最后一个值
destructiveTitle: "退出登录", // 默认按钮,会自动高亮显示buttonIndex=1
}, (ret, err)=>{
if( ret ){
// this.game.print(ret,1);
if(ret.buttonIndex==1){
this.game.deldata(["access_token","refresh_token"]);
this.game.delfs(["access_token","refresh_token"]);
this.game.openWin("root","index.html"); // 返回主页面
setTimeout(() => {
this.game.closeWin() // 退出当前窗口
},1000)
}
}
});
},
// 点击头像跳转到更新头像页面
to_updata_avatar(){
this.game.openFrame('avatar', 'avatar.html','from_top')
},
},
});
}
</script>
</body>
</html>
- 更新用户个人中心页面头像,
html/user.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<!-- 引入用户头像处理js文件 -->
<script src="../static/js/v-avatar-2.0.3.min.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app user" id="app">
<div class="bg">
<img src="../static/images/bg0.jpg">
</div>
<img class="back" @click="back" src="../static/images/user_back.png" alt="">
<img class="setting" @click='to_settings' src="../static/images/setting.png" alt="">
<div class="header">
<div class="info">
<div class="avatar">
<img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
<!-- 用户头像处理 -->
<div class="user_avatar">
<v-avatar v-if="user_data.avatar" :src="user_data.avatar" :size="55" :rounded="true"></v-avatar>
<v-avatar v-else-if="user_data.nickname" :username="user_data.nickname" :size="55" :rounded="true"></v-avatar>
<v-avatar v-else :username="user_data.id" :size="55" :rounded="true"></v-avatar>
</div>
<img class="avatar_border" src="../static/images/avatar_border.png" alt="">
</div>
<p class="user_name">{{user_data.nickname}}</p>
</div>
<div class="wallet">
<div class="balance">
<p class="title"><img src="../static/images/money.png" alt="">钱包</p>
<p class="num">{{user_data.money_format}}</p>
</div>
<div class="balance">
<p class="title"><img src="../static/images/integral.png" alt="">果子</p>
<p class="num">{{user_data.credit_format}}</p>
</div>
</div>
<div class="invite">
<img class="invite_btn" src="../static/images/invite.png" alt="">
</div>
</div>
<div class="menu">
<div class="item">
<span class="title">我的主页</span>
<span class="value">查看</span>
</div>
<div class="item">
<span class="title">任务列表</span>
<span class="value">75%</span>
</div>
<div class="item">
<span class="title">收益明细</span>
<span class="value">查看</span>
</div>
<div class="item">
<span class="title">实名认证</span>
<span class="value">未认证</span>
</div>
<div class="item">
<span class="title">问题反馈</span>
<span class="value">去反馈</span>
</div>
</ul>
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg4.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
prev:{name:"",url:"",params:{}},
current:{name:"user",url:"user.html",params:{}},
user_data:{},
}
},
// 页面加载之前获取用户数据
created(){
// 获取用户数据
this.get_user_data()
// 监听事件变化
this.listen()
},
methods:{
// 监听事件
listen(){
// 监听token更新的通知
this.listen_update_token();
},
// 监听token更新的通知
listen_update_token(){
api.addEventListener({
name: 'update_token'
}, (ret, err) => {
// 更新用户数据
this.get_user_data()
});
},
// 通过token值获取用户数据
get_user_data(){
// 获取token
let token = this.game.getfs('access_token') || this.game.getdata('access_token')
// 根据token获取用户数据
this.user_data = this.game.get_user_by_token(token)
// this.game.print(this.user_data)
// 格式化数字变成金钱格式,原始数据不变
this.user_data.money_format = this.game.number_format(this.user_data.money)
this.user_data.credit_format = this.game.number_format(this.user_data.credit)
},
back(){
// 返回首页
this.game.closeWin();
},
// 点击设置按钮,跳转到系统设置页面
to_settings(){
this.game.openFrame('settings', 'settings.html')
},
}
});
}
</script>
</body>
</html>
更新昵称
服务端提供更新昵称接口
- 用户视图 :
users/api.py
import base64, uuid, os
from flask import current_app, request
from flask_jwt_extended import get_jwt_identity, jwt_required,create_refresh_token,create_access_token
# 引入构造器(序列化器)
from .marshmallow import MobileSchema, ValidationError, UserSchema
# 引入返回信息,和自定义状态码
from application import message, code, oss
from . import services
from application.utils import captcha
# 更新用户昵称 - 需验证用户信息
@jwt_required()
def update_nickname(nickname):
# 1. 获取用户信息 - 那个用户要更新昵称
user_data = get_jwt_identity() # 从token载荷中获取用户信息
# 2.获取用户对象
user = services.get_user_by_id(user_data['id'])
# 判断用户是否存在
if user is None:
return {
'errno': code.CODE_AUTOORIZATION_ERROR,
'errmsg': message.authorization_is_invalid
}
# 3.保存更新用户昵称
data = {'nickname': nickname}
user = services.update_user_data(user, data)
# 4.重新序列化user对象
us = UserSchema()
user_data = us.dump(user)
# 5.重新刷新token值,把新的用户信息添加到载荷中
access_token = create_access_token(identity=user_data)
refresh_token = create_refresh_token(identity=user_data)
# 6.返回响应数据
return {
'errno': code.CODE_OK,
'errmsg': message.ok,
'access_token': access_token,
'refresh_token': refresh_token
}
- 路由
users/urls.py
from application import path, api_rpc
# 引入当前蓝图应用视图 , 引入rpc视图
from . import views, api
# 蓝图路径与函数映射列表
urlpatterns = []
# rpc方法与函数映射列表[rpc接口列表]
apipatterns = [
api_rpc('check_mobile', api.check_mobile),
api_rpc('register', api.register),
api_rpc('login', api.login),
api_rpc('access', api.access_token),
api_rpc('refresh', api.refresh_token), # 刷新access_token值
api_rpc('update_avatar', api.update_avatar), # 更新头像
api_rpc('update_nickname', api.update_nickname), # 更新昵称
]
客户端发送更新昵称请求, 并全网广播更新数据
- 在系统设置页面点击昵称跳转到更改昵称页面
html/settings.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/v-avatar-2.0.3.min.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app user setting" id="app">
<div class="bg">
<img src="../static/images/form_bg.png">
</div>
<img class="back" @click="quit" src="../static/images/user_back.png" alt="">
<div class="form">
<div class="item avatar" @click='to_updata_avatar'>
<span class="title">头像</span>
<span class="goto">></span>
<span class="value">
<!-- 头像处理 -->
<v-avatar v-if="user_data.avatar" :src="user_data.avatar" :size="55" :rounded="true"></v-avatar>
<v-avatar v-else-if="user_data.nickname" :username="user_data.nickname" :size="55" :rounded="true"></v-avatar>
<v-avatar v-else :username="user_data.id" :size="55" :rounded="true"></v-avatar>
</span>
</div>
<div class="item" @click='to_update_nickname'>
<span class="title">昵称</span>
<span class="goto">></span>
<span class="value">{{user_data.nickname}}</span>
</div>
<div class="item">
<span class="title">手机号</span>
<span class="goto">></span>
<span class="value">{{user_data.mobile}}</span>
</div>
<div class="item">
<span class="title">登陆密码</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">交易密码</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">地址管理</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">设备管理</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item logout">
<img @click="change_account" src="../static/images/change_account.png" alt="">
<p @click="logout">退出账号</p>
</div>
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
user_data: {}, // 用户数据
}
},
// 页面加载之前获取用户数据
created(){
// 获取用户数据
this.get_user_data()
// 监听事件变化
this.listen()
},
methods:{
// 监听事件
listen(){
// 监听token更新的通知
this.listen_update_token();
},
// 监听token更新的通知
listen_update_token(){
api.addEventListener({
name: 'update_token'
}, (ret, err) => {
// 更新用户数据
this.get_user_data()
});
},
// 通过token值获取用户数据
get_user_data(){
// 获取token
let token = this.game.getfs('access_token') || this.game.getdata('access_token')
// 通过token值获取用户数据
this.user_data = this.game.get_user_by_token(token)
},
// 返回上一页
quit(){
// 退出当前帧页面
this.game.closeFrame();
},
// 切换账号
change_account(){
this.game.openWin("login","login.html"); // 打开登陆窗口
setTimeout(() => {
this.game.closeWin() // 退出当前窗口
},1000)
},
// 退出账号
logout(){
// 底部弹出框
api.actionSheet({
title: '您确认要退出当前登录吗?',
cancelTitle: '取消', // 取消按钮,buttonIndex=最后一个值
destructiveTitle: "退出登录", // 默认按钮,会自动高亮显示buttonIndex=1
}, (ret, err)=>{
if( ret ){
// this.game.print(ret,1);
if(ret.buttonIndex==1){
this.game.deldata(["access_token","refresh_token"]);
this.game.delfs(["access_token","refresh_token"]);
this.game.openWin("root","index.html"); // 返回主页面
setTimeout(() => {
this.game.closeWin() // 退出当前窗口
},1000)
}
}
});
},
// 点击头像跳转到更新头像页面
to_updata_avatar(){
this.game.openFrame('avatar', 'avatar.html','from_top')
},
// 点击昵称跳转到更改昵称页面
to_update_nickname(){
this.game.openFrame('nickname', 'nickname.html', 'from_top')
}
},
});
}
</script>
</body>
</html>
- 修改昵称页面向服务端发送请求
html/nickname.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app frame avatar update_password" id="app">
<div class="box">
<p class="title">修改昵称</p>
<img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
<div class="content">
<input class="password" type="text" v-model="nickname" placeholder="用户昵称....">
</div>
<img @click="update_nickname_commit" class="btn" src="../static/images/yes.png" alt="">
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
nickname: "", //昵称
prev:{name:"",url:"",params:{}},
current:{name:"password",url:"password.html",params:{}},
}
},
methods:{
// 返回上一页
back(){
this.game.closeFrame();
},
// 提交用户昵称
update_nickname_commit(){
// 校验用户输入的昵称格式
if(this.nickname.length<1){
this.game.tips("用户昵称不能为空!");
return false;
}
if(this.nickname.length<2 || this.nickname.length > 8){
this.game.tips("昵称在2~8个字符之间!");
return false;
}
// 昵称校验没问题,向服务端发送请求
let self = this
// 检验用户是否登陆
self.game.check_user_login(self,() =>{
// 获取token值
let token = self.game.getdata('access_token') || self.game.getfs('access_token');
// 向服务端发送请求
self.game.post(self,{
"method": "Users.update_nickname",
"params":{
"nickname":self.nickname
},
// 发送请求携带token值
"header":{
"Authorization": "jwt " + token
},
success(response){
let data = response.data;
if(data.result && data.result.errno === 1000){
let token = self.game.getdata('access_token')
// 判断当前用户是否记住密码 - 更新access_token
if(token){
// 没记住密码清框
self.game.delfs(['access_token', 'refresh_token'])
self.game.setdata({
'access_token': data.result.access_token,
'refresh_token': data.result.refresh_token
})
}else {
// 记住密码情况
self.game.deldata(['access_token', 'refresh_token'])
self.game.setfs({
'access_token': data.result.access_token,
'refresh_token': data.result.refresh_token
})
}
// 客户端提示
self.game.tips('昵称更改成功!')
// 全局广播,更新头像成功
self.game.sendEvent('update_nickname_success');
self.game.sendEvent('update_token')
// 关闭当前页面
self.back()
}else {
self.game.tips('昵称更改失败!')
}
}
});
});
},
}
});
}
</script>
</body>
</html>
- 追加css样式
static/js/main.js
input::-webkit-input-placeholder { /* WebKit browsers */
color: white;
}
.update_nickname .nickname,
.update_password .password
{
margin: 8rem 4.4rem 4rem;
width: 19.50rem;
height: 4rem;
line-height: 4rem;
background-color: #cc9966;
outline: none;
border: 1px solid #330000;
text-align: center;
font-size: 1.5rem;
color: #ffffcc;
}
.update_password .password{
margin-top: 5.6rem;
margin-bottom: 0.4rem;
}
.update_password .password2{
margin-top: 0.4rem;
}
更新手机号
服务端提供修改手机号接口
- 视图:
users/api.py
import base64, uuid, os
from flask import current_app, request
from flask_jwt_extended import get_jwt_identity, jwt_required,create_refresh_token,create_access_token
# 引入构造器(序列化器)
from .marshmallow import MobileSchema, ValidationError, UserSchema, UpdateMobileSchema
# 引入返回信息,和自定义状态码
from application import message, code, oss
from . import services
from application.utils import captcha
# 更新用户手机号 - 需验证用户信息
@jwt_required()
def update_mobile(mobile, sms_code):
"""
更新用户手机号
:param mobile: 手机号
:param sms_code: 验证码
:return:
"""
# 1. 获取用户信息 - 那个用户要更新昵称
user_data = get_jwt_identity() # 从token载荷中获取用户信息
# 2.获取用户对象 - 校验客户端传过来的id是否存在
user = services.get_user_by_id(user_data['id'])
# 判断用户是否存在
if user is None:
return {
'errno': code.CODE_AUTOORIZATION_ERROR,
'errmsg': message.authorization_is_invalid
}
data = {
'id': user_data['id'],
'mobile': mobile,
'sms_code': sms_code
}
# 3.重新序列化user对象
ums = UpdateMobileSchema()
# 4.反序列化校验数据
try:
user = ums.load(data)
# 数据验证异常
except ValidationError as e:
# 验证码错误
if e.messages.get('sms_code'):
errmsg = e.messages['sms_code'][0]
return {'errno':code.CODE_VALIDATE_ERROR, 'errmsg':errmsg}
# 5.再序列化输出数据
user_data = ums.dump(user)
# 6.重新刷新token值,把新的用户信息添加到载荷中
access_token = create_access_token(identity=user_data)
refresh_token = create_refresh_token(identity=user_data)
# 7.返回响应数据
return {
'errno': code.CODE_OK,
'errmsg': message.ok,
'access_token': access_token,
'refresh_token': refresh_token
}
- 新建更换手机模型构造器
users/marshmallow.py
# 更新手机号模型构造器
class UpdateMobileSchema(SQLAlchemyAutoSchema):
mobile = auto_field(required=True)
sms_code = fields.String(required=True, load_only=True)
user_id = fields.Integer()
class Meta:
model = User
include_fk = False # 启用外键关系
include_relationships = True # 模型关系外部属性
# 如果要返回客户端用户模型的全部字段,就不要声明fields或exclude字段即可
fields = ['id', 'name', "nickname", "money", "credit", "avatar",'mobile', 'password', 'sms_code']
# 对返回客户端的数据进行处理
@post_dump
def get_object(self, data, **kwargs):
data["money"] = float(data["money"])
data["credit"] = float(data["credit"])
data['mobile'] = data['mobile'][:3] + '****' + data['mobile'][-4:]
return data
# 更新用户手机信息
@post_load
def post_load(self, data, **kwargs):
"""更新用户基本信息"""
# 获取用户模型类对象
user = services.get_user_by_id(data['id'])
# 更新用户手机号
instance = services.update_user_data(user, {'mobile': data['mobile']})
return instance
# 多字段校验
@validates_schema
def validate_password(self, data, **kwargs):
# 校验短信验证码
# 1.从redis中提取验证码
redis_sms_code = redis_check.get('sms_%s' % data['mobile'])
if redis_sms_code is None:
raise ValidationError(message=message.sms_code_expired, field_name='sms_code')
# python从redis中提取的数据最终都是bytes类型,所以需要进行编码处理
redis_sms_code = redis_sms_code.decode()
# 2.从客户端提交的数据data中提取验证码
sms_code = data['sms_code']
# 3.两者比较是否相等,不相等,说明验证码输入不正确
if redis_sms_code != sms_code:
raise ValidationError(message=message.sms_code_not_match, field_name='sms_code')
return data
- 添加路由
users/urls.py
from application import path, api_rpc
# 引入当前蓝图应用视图 , 引入rpc视图
from . import views, api
# 蓝图路径与函数映射列表
urlpatterns = []
# rpc方法与函数映射列表[rpc接口列表]
apipatterns = [
api_rpc('check_mobile', api.check_mobile),
api_rpc('register', api.register),
api_rpc('login', api.login),
api_rpc('access', api.access_token),
api_rpc('refresh', api.refresh_token), # 刷新access_token值
api_rpc('update_avatar', api.update_avatar), # 更新头像
api_rpc('update_nickname', api.update_nickname), # 更新昵称
api_rpc('update_mobile', api.update_mobile), # 更新手机号
]
客户端发送请求, 修改手机号, 并全网广播更新数据
- 在系统设置页面点击手机号跳转到更改手机号页面
html/settings.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/v-avatar-2.0.3.min.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app user setting" id="app">
<div class="bg">
<img src="../static/images/form_bg.png">
</div>
<img class="back" @click="quit" src="../static/images/user_back.png" alt="">
<div class="form">
<div class="item avatar" @click='to_updata_avatar'>
<span class="title">头像</span>
<span class="goto">></span>
<span class="value">
<!-- 头像处理 -->
<v-avatar v-if="user_data.avatar" :src="user_data.avatar" :size="55" :rounded="true"></v-avatar>
<v-avatar v-else-if="user_data.nickname" :username="user_data.nickname" :size="55" :rounded="true"></v-avatar>
<v-avatar v-else :username="user_data.id" :size="55" :rounded="true"></v-avatar>
</span>
</div>
<div class="item" @click='to_update_nickname'>
<span class="title">昵称</span>
<span class="goto">></span>
<span class="value">{{user_data.nickname}}</span>
</div>
<div class="item" @click='to_update_mobile'>
<span class="title">手机号</span>
<span class="goto">></span>
<span class="value">{{user_data.mobile}}</span>
</div>
<div class="item">
<span class="title">登陆密码</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">交易密码</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">地址管理</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">设备管理</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item logout">
<img @click="change_account" src="../static/images/change_account.png" alt="">
<p @click="logout">退出账号</p>
</div>
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
user_data: {}, // 用户数据
}
},
// 页面加载之前获取用户数据
created(){
// 获取用户数据
this.get_user_data()
// 监听事件变化
this.listen()
},
methods:{
// 监听事件
listen(){
// 监听token更新的通知
this.listen_update_token();
},
// 监听token更新的通知
listen_update_token(){
api.addEventListener({
name: 'update_token'
}, (ret, err) => {
// 更新用户数据
this.get_user_data()
});
},
// 通过token值获取用户数据
get_user_data(){
// 获取token
let token = this.game.getfs('access_token') || this.game.getdata('access_token')
// 通过token值获取用户数据
this.user_data = this.game.get_user_by_token(token)
},
// 返回上一页
quit(){
// 退出当前帧页面
this.game.closeFrame();
},
// 切换账号
change_account(){
this.game.openWin("login","login.html"); // 打开登陆窗口
setTimeout(() => {
this.game.closeWin() // 退出当前窗口
},1000)
},
// 退出账号
logout(){
// 底部弹出框
api.actionSheet({
title: '您确认要退出当前登录吗?',
cancelTitle: '取消', // 取消按钮,buttonIndex=最后一个值
destructiveTitle: "退出登录", // 默认按钮,会自动高亮显示buttonIndex=1
}, (ret, err)=>{
if( ret ){
// this.game.print(ret,1);
if(ret.buttonIndex==1){
this.game.deldata(["access_token","refresh_token"]);
this.game.delfs(["access_token","refresh_token"]);
this.game.openWin("root","index.html"); // 返回主页面
setTimeout(() => {
this.game.closeWin() // 退出当前窗口
},1000)
}
}
});
},
// 点击头像跳转到更新头像页面
to_updata_avatar(){
this.game.openFrame('avatar', 'avatar.html','from_top')
},
// 点击昵称跳转到更改昵称页面
to_update_nickname(){
this.game.openFrame('nickname', 'nickname.html', 'from_top')
},
// 点击手机号跳转到更新手机号码页面
to_update_mobile(){
this.game.openFrame('mobile', 'mobile.html', 'from_top')
},
},
});
}
</script>
</body>
</html>
- 修改手机号页面发送请求,修改手机号, 成功发送全网广播
mobile.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app" id="app">
<img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
<div class="bg">
<img src="../static/images/bg0.jpg">
</div>
<div class="form">
<div class="form-title">
<img src="../static/images/updatemobile.png">
<img class="back" @click="backpage" src="../static/images/back.png">
</div>
<div class="form-data">
<div class="form-data-bg">
<img src="../static/images/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" @click='send_sms' src="../static/images/refresh.png">
</div>
<div class="form-item">
<img class="commit" @click="update_mobile_commit" src="../static/images/commit.png"/>
</div>
</div>
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg3.mp3");
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
music_play:true,
mobile:'',
code:'', // 验证码
sms_interval:false, // 是否处于短信发送间隔冷却时间内
}
},
watch:{
music_play(){
if(this.music_play){
this.game.play_music("../static/mp3/bg3.mp3");
}else{
this.game.stop_music();
}
},
// 监听手机号码输入是否正确
mobile(){
// 先本地校验,通过再服务端校验
if(this.check_mobile_format()){
// alert('dkjks')
this.http_check_mobile()
}else{
}
},
},
methods:{
// 返回上一页,本质是关闭当前页面
backpage(){
this.game.closeFrame()
},
// 本地校验手机号
check_mobile_format(){
return /^1[3-9]\d{9}$/.test(this.mobile)
},
// 远程服务端校验手机号,发送post请求
http_check_mobile(){
let self = this
// 验证手机号是否被注册
this.game.post(this,{
method:"Users.check_mobile",
params:{"mobile":this.mobile},
success(response){
let data = response.data;
if(data.result && data.result.errno === 1001){
self.game.tips('该手机号已经被注册过!')
}
}
});
},
// 点击提交,向服务端发送更换手机号请求
update_mobile_commit(){
// 点击按钮时的声音
this.game.play_music('../static/mp3/btn1.mp3')
// 客户端进行数据验证
if(!this.check_mobile_format(this.mobile)){
this.game.tips('手机号码格式不正确')
}
if(this.code.length != 6){
this.game.tips('验证码格式不正确!');
return false;
}
// 数据校验没问题, 向服务端发送请求
this.update_mobile()
},
// 服务端发送更新手机号码请求
update_mobile(){
let self = this
// 检验用户是否登陆
self.game.check_user_login(self,() =>{
// 获取token值
let token = self.game.getdata('access_token') || self.game.getfs('access_token');
// 发送请求
self.game.post(self,{
"method": "Users.update_mobile",
"params":{
"mobile": self.mobile,
'sms_code': self.code
},
"header":{
"Authorization": "jwt " + token
},
success(response){
let data = response.data;
if(data.result && data.result.errno === 1001){
self.game.tips('验证码输入有误,请重新输入!')
}
if(data.result && data.result.errno === 1000){
let token = self.game.getdata('access_token')
// 判断当前用户是否记住密码 - 更新access_token
if(token){
// 没记住密码清框
self.game.delfs(['access_token', 'refresh_token'])
self.game.setdata({
'access_token': data.result.access_token,
'refresh_token': data.result.refresh_token
})
}else {
// 记住密码情况
self.game.deldata(['access_token', 'refresh_token'])
self.game.setfs({
'access_token': data.result.access_token,
'refresh_token': data.result.refresh_token
})
}
// 客户端提示
self.game.tips('手机号码修改成功!')
// 全局广播,更新头像成功
self.game.sendEvent('update_mobile_success')
self.game.sendEvent('update_token')
// 关闭当前页面
setTimeout(() => {
self.game.closeFrame()
}, 1500)
}
}
});
});
},
send_sms(){
// 点击按钮时的声音
this.game.play_music('../static/mp3/btn1.mp3')
// 客户端进行数据验证
if(!this.check_mobile_format(this.mobile)){
// 弹窗提示
this.game.tips('手机号码格式不正确!');
return false;
}
if(this.sms_interval){
this.game.tips('短信发送过于频繁!');
return false;
}
// 发送短信,获取验证码
let self = this
this.game.post(this,{
method:'Home.sms',
params:{'mobile':self.mobile},
success(response){
if(response.data && response.data.result && response.data.result.errno === 1000){
self.game.tips('短信发送成功!')
self.sms_interval = true // 发送短信进入冷却状态
let time = self.game.config.SMS_TIME_OUT // 获取冷却时间
// 设置定时器 - 冷却倒计时
let timer = setInterval(() => {
if(--time < 1){
self.sms_interval = false; // 退出冷却状态,允许再次发送短信
clearInterval(timer); // 关闭定时器
}
}, 1000)
}
}
})
},
}
})
}
</script>
</body>
</html>
修改登陆密码和交易密码
修复客户端在服务端返回状态非1000时没有任何提示的BUG
初始化http网络请求, 中的响应拦截器更改 static/js/main.js
,代码:
// 初始化http网络请求
init_http(){
// ajax初始化
if(window.axios){
axios.defaults.baseURL = this.config.API_SERVER // 接口网关地址
axios.defaults.timeout = 2500 // 请求超时时间
axios.defaults.withCredentials = false // 跨域请求时禁止携带cookie
// 请求拦截器和响应拦截器相当于中间件作用
// 1. 添加请求拦截器
axios.interceptors.request.use((config) => {
// 请求正确
// 在发送请求之前的初始化[添加请求头],config就是本次请求数据对象
// 显示进度提示框
api.showProgress({
style: 'default', // 进度提示框风格
animationType: 'zoom', // 动画类型 缩放
title: '努力加载中...', // 标题
text: '请稍等...', // 内容
modal: true //是否模态,模态时整个页面将不可交互
});
return config // 返回对象
}, (error) => {
// 请求错误, 隐藏进度提示框
api.hideProgress();
// 弹出错误提示框
this.tips("网络错误!!");
// 返回
return Promise.reject(error);
});
// 2. 添加响应拦截器 - 找出响应数据错误
axios.interceptors.response.use((response) => {
// 关闭进度提示框
api.hideProgress();
// 判断接口返回状态码
if(response.data && response.data.error){
// 服务器报错
let error = response.data.error;
switch (error.code) {
case -32601: // 请求接口不存在
this.tips("请求地址不存在!");
break;
case 500:
this.tips("服务端程序执行错误!\n" + error.message);
break;
}
}
if(response.data && response.data.result){
// 判断请求唯一标识是否一致
if(axios.uuid != response.data.id){
this.tips("请求拦截错误!");
return false;
}
}
let result = response.data.result;
if(result && result.errno != 1000){
this.tips(result.errmsg);
return false;
}
return response // 没有错误的话,返回响应数据
}, (error) => {
// 关闭进度提示框
api.hideProgress();
// 网络错误提示
switch (error.message) {
case "Network Error":
this.tips('网络错误!!');
break;
}
return Promise.reject(error);
});
if(Vue){
// js语法: prototype 向对象添加属性和方法
Vue.prototype.axios = axios;
}
if(window.UUID){
// prototype 向对象添加属性和方法
Vue.prototype.uuid = UUID.generate;
}
}
}
服务端提供api接口
交易密码更新字段
- 为了完成交易密码功能,我们新建一个字段_pay_password,并对外暴露pay_password加密串以及交易密码验证方法。
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__ = "mf_user"
name = db.Column(db.String(255), index=True, comment="用户账户")
nickname = db.Column(db.String(255), comment="用户昵称")
_password = db.Column(db.String(255), comment="登录密码")
_pay_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.Numeric(7, 2), default=0.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', primaryjoin='User.id == UserProfile.user_id', foreign_keys='UserProfile.user_id', backref='user', uselist=False)
"""
登录密码存取器是成对出现的,函数也是一样的
"""
@property
def password(self):
return self._password
@password.setter
def password(self, rawpwd):
"""密码加密"""
self._password = generate_password_hash(rawpwd)
def check_password(self, rawpwd):
"""验证密码"""
return check_password_hash(self.password, rawpwd)
"""
交易密码存取器是成对出现的,函数也是一样的
"""
@property
def pay_password(self):
return self._password
@pay_password.setter
def pay_password(self, rawpwd):
"""密码加密"""
self._pay_password = generate_password_hash(rawpwd)
def check_pay_password(self, rawpwd):
"""验证密码"""
return check_password_hash(self.pay_password, rawpwd)
@classmethod
def add(cls, instance):
'''
保存用户信息
:param instance: 用户模型类对象
:return:
'''
db.session.add(instance)
db.session.commit()
@classmethod
def add_all(cls, instance_list):
'''
保存用户信息
:param instance_list: 用户模型类对象列表
:return:
'''
db.session.add_all(instance_list)
db.session.commit()
class UserProfile(BaseModel):
"""用户详情信息表"""
__tablename__ = "mf_user_profile"
user_id = db.Column(db.Integer, 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="现居住地址")
def __repr__(self):
return f"<UserProfile {self.user.nickname}>"
- 终端命令行命令类生成用户时,新增pay_password属性的赋值,
application.utils.commands
,代码:
# 批量生成测试数据
class FakerCommand(Command):
"""批量生成测试数据"""
name = 'faker' # 生成命令名称
# 传递的参数
option_list = [
Option('--type', '-t', dest='type', default='user'), # 指定生成数据的类型(用户数据,其他数据)
Option('--num', '-n', dest='num', default=1), # 生成数据的数量
]
# 以后想要生成数据类型列表
type_list = ['user']
def __call__(self, app, type, num):
# 判断想要生成数据的类型是否在类型列表中
if type not in self.type_list:
print("数据类型不正确\n当前Faker生成数据类型仅支持: %s" % self.type_list)
return None
num = int(num)
if num < 1:
print("生成数量不正确\n当前Faker生成数据至少1个以上")
return None
if type == 'user':
self.create_user(app,num)
# 生成指定数量的测试用户信息
def create_user(self,app,num):
"""生成指定数量的测试用户信息"""
from application.apps.users.models import User,UserProfile
faker = app.faker
faker.add_provider(internet)
user_list = [] # 用户模型对象列表
# 从配置文件中读取默认测试用户的登录密码
password = app.config.get("DEFAULT_TEST_USER_PASSWORD", "12345678")
for i in range(num):
sex = bool(random.randint(0,2))
nickname = faker.name()
# 登录账号[随机字母,6-16位]
name = faker.pystr(min_chars=6, max_chars=16)
age = random.randint(13, 50)
birthday = faker.date_time_between(start_date="-%sy" % age, end_date="-12y", tzinfo=None)
hometown_province = faker.province()
hometown_city = faker.city()
hometown_area = faker.district()
living_province = faker.province()
living_city = faker.city()
living_area = faker.district()
user = User(
nickname=nickname,
sex=sex,
name=name,
age=age,
password=password,
pay_password=password,
money=random.randint(100, 99999),
credit=random.randint(100, 99999),
ip_address=faker.ipv4_public(),
email=faker.ascii_free_email(),
mobile=faker.phone_number(),
unique_id=faker.uuid4(),
province=faker.province(),
city=faker.city(),
area=faker.district(),
info=UserProfile(
birthday=birthday,
hometown_province=hometown_province,
hometown_city=hometown_city,
hometown_area=hometown_area,
hometown_address=hometown_province + hometown_city + hometown_area + faker.street_address(),
living_province=living_province,
living_city=living_city,
living_area=living_area,
living_address=living_province + living_city + living_area + faker.street_address()
)
)
user_list.append(user)
# 存储数据
with app.app_context():
User.add_all(user_list)
print("生成%s个用户信息完成..." % num)
- 删除数据库中关于用户基本信息和用户附加详细2个数据表以后,重新运行项目。并通过终端命令重新生成新的测试用户。
# 端命令生成新的用户数据
python manage.py faker -n20
服务端视图修改密码接口,并整理重复代码
- 视图接口,
application.apps.users.api
,代码:
import base64, uuid, os
from flask import current_app, request
from flask_jwt_extended import get_jwt_identity, jwt_required,create_refresh_token,create_access_token
# 引入构造器(序列化器)
from .marshmallow import MobileSchema, ValidationError, UserSchema, UpdateMobileSchema
# 引入返回信息,和自定义状态码
from application import message, code, oss
from . import services
# 引入decorator中的装饰器,get_user_object根据token获取用户模型对象
from application.utils import captcha, decorator
# 校验手机号
def check_mobile(mobile):
# 实例化构造器对象
ms = MobileSchema()
try:
# load反序列化校验数据
ms.load({'mobile':mobile})
res = {'errno': code.CODE_OK, 'errmsg':message.ok}
except ValidationError as e:
print(e.messages) # {'mobile': ['手机号码格式有误']}
res = {'errno': code.CODE_VALIDATE_ERROR, 'errmsg': e.messages['mobile'][0]}
return res
# 用户注册
def register(mobile:str, password:str, password2:str, sms_code):
"""
用户注册基本信息
:param mobile: 手机号码
:param password: 登录密码
:param password2: 确认密码
:param sms_code: 短信验证码
:return:
"""
# 1.验证手机是否已经注册
res = check_mobile(mobile)
if res['errno'] != code.CODE_OK:
return res
# 2.验证并保存用户信息
try:
urs = UserSchema()
# 反序列化校验数据
instance = urs.load({
'mobile':mobile,
'password':password,
'password2':password2,
'sms_code':sms_code
})
res = {'errno':code.CODE_OK, 'errmsg':message.ok, 'data':urs.dump(instance)}
# 数据验证异常
except ValidationError as e:
# 验证码错误
if e.messages.get('sms_code'):
errmsg = e.messages['sms_code'][0]
# 两次密码不一致
elif e.messages.get('password'):
errmsg = e.messages['password'][0]
else:
errmsg = message.check_data_fail
res = {'errno':code.CODE_VALIDATE_ERROR, 'errmsg':errmsg}
# 其他异常
except Exception as e:
# 打印错误日志
current_app.log.error('服务端程序发生未知异常!')
current_app.log.error(f'错误信息: {e}')
res = {'errno':code.CODE_SERVER_ERROR, 'errmsg':message.server_is_error}
return res
# 用户登录
def login(account, password, ticket, randstr):
"""
用户登录
:param account: 用户账号
:param password: 用户密码
:param ticket: 验证码客户端验证回调的票据
:param randstr: 验证码客户端验证回调的随机串
:return:
"""
# 0.验证防水墙验证码是否成功
try:
captcha.check_captcha(ticket, randstr, request.remote_addr)
except captcha.CaptchaParamsError: # 参数异常
return {
"errno": code.CODE_PARAMS_ERROR,
"errmsg": message.params_error
}
except captcha.CaptchaNetWorkError: # 网络异常
return {
"errno": code.CODE_NETWORK_ERROR,
"errmsg": message.network_error
}
except captcha.CaptchaFailError: # 数据校验失败)
return {
"errno": code.CODE_VALIDATE_ERROR,
"errmsg": message.check_data_fail
}
except Exception as e:
current_app.log.error(f'服务端程序出错: {e}')
return {
"errno": code.CODE_SERVER_ERROR,
"errmsg": message.server_is_error
}
# 1.校验数据库是否有当前用户
user = services.get_user_by_account(account)
if user is None:
return {
'errno': code.CODE_USER_NOT_EXISTS,
'errmsg': message.user_not_exists
}
# 2. 校验密码是否正确
ret = user.check_password(password)
if not ret:
return {
'errno': code.CODE_PASSWORD_ERROR,
'errmsg': message.password_error
}
# 记录用户本次登录信息
services.save_user_login_info(user)
# 3.生成jwt assess token 和 refresh token
token = services.generate_user_token(user)
# 4.返回2个token给客户端
return {
'errno': code.CODE_OK,
'errmsg': message.ok,
**token,
}
"""测试"""
# 验证用户是否携带 refresh_token, 刷新access_token
@jwt_required(refresh=True)
@decorator.get_user_object
def refresh_token(user):
'''
重新刷新token值
:param user: 装饰器通过token获取的用户模型对象
:return:
'''
# 重新生成token值
token = services.generate_user_token(user)
access_token = token['access_token']
return {
"errno": code.CODE_OK,
"errmsg": message.ok,
"access_token": access_token
}
# 更新用户头像
@jwt_required()
@decorator.get_user_object
def update_avatar(user,avatar):
"""
更新用户头像
:param user: 装饰器通过token获取的用户模型对象
:param avatar: base64编码字符串 "data:image/jpeg;base64,/9j/4AAQ...="
:return:
"""
# 1.处理客户端上传的头像信息
ext = avatar[avatar.find('/')+1:avatar.find(';')] # 上传头像的格式(后缀jpg/png)
b64_avatar = avatar[avatar.find(',')+1:] # 上传头像的信息
b64_image = base64.b64decode(b64_avatar) # 解码
filename = uuid.uuid4() # 头像名称 - 随机
"""本地存储图片"""
# # 2.拼接头像完整路径
# static_path = os.path.join(current_app.BASE_DIR, current_app.config.get('STATIC_DIR', 'avatar'))
# avatar_path = "%s/%s.%s" % (static_path, filename, ext)
# # 3.保存头像信息到本地
# with open(avatar,"wb") as f:
# f.write(b64_image)
"""阿里云存储头像信息"""
# 4.返回查看头像网址
avatar_path = oss.put_data(f'{filename}.{ext}', b64_image)
# 5.数据库保存用户头像信息
data = {'avatar': avatar_path}
# 保存用户信息
user = services.update_user_data(user, data)
# 6.生成用户token值
token= services.generate_user_token(user)
return {
'errno': code.CODE_OK,
'errmsg': message.ok,
**token,
}
# 更新用户昵称 - 需验证用户信息
@jwt_required()
@decorator.get_user_object
def update_nickname(user,nickname):
'''
更新用户昵称
:param user: 装饰器通过token获取的用户模型对象
:param nickname: 客户端传来的用户昵称
:return:
'''
# 3.保存更新用户昵称
data = {'nickname': nickname}
user = services.update_user_data(user, data)
# 5.重新刷新token值,把新的用户信息添加到载荷中
token = services.generate_user_token(user)
# 6.返回响应数据
return {
'errno': code.CODE_OK,
'errmsg': message.ok,
**token,
}
# 更新用户手机号 - 需验证用户信息
@jwt_required()
def update_mobile(mobile, sms_code):
"""
更新用户手机号
:param mobile: 手机号
:param sms_code: 验证码
:return:
"""
# 1. 获取用户信息 - 那个用户要更新昵称
user_data = get_jwt_identity() # 从token载荷中获取用户信息
# 2.获取用户对象 - 校验客户端传过来的id是否存在
user = services.get_user_by_id(user_data['id'])
# 判断用户是否存在
if user is None:
return {
'errno': code.CODE_AUTOORIZATION_ERROR,
'errmsg': message.authorization_is_invalid
}
data = {
'id': user_data['id'],
'mobile': mobile,
'sms_code': sms_code
}
# 3.重新序列化user对象
ums = UpdateMobileSchema()
# 4.反序列化校验数据
try:
user = ums.load(data)
# 数据验证异常
except ValidationError as e:
# 验证码错误
if e.messages.get('sms_code'):
errmsg = e.messages['sms_code'][0]
return {'errno':code.CODE_VALIDATE_ERROR, 'errmsg':errmsg}
# 5.再序列化输出数据
user_data = ums.dump(user)
# 6.重新刷新token值,把新的用户信息添加到载荷中
access_token = create_access_token(identity=user_data)
refresh_token = create_refresh_token(identity=user_data)
# 7.返回响应数据
return {
'errno': code.CODE_OK,
'errmsg': message.ok,
'access_token': access_token,
'refresh_token': refresh_token
}
# 用户更新登录密码接口
@jwt_required()
@decorator.get_user_object
def update_password(user, old_password, new_password, re_password):
'''
用户更新登录密码接口
:param user: 饰器通过token获取的用户模型对象
:param old_password: 旧密码
:param new_password: 新密码
:param re_password: 确认密码
:return:
'''
# 1.验证旧密码
res = user.check_password(old_password)
if not res:
return {
'errno': code.CODE_PASSWORD_ERROR,
'errmsg': message.password_error
}
# 2.验证新密码和确认密码是否一致
if new_password != re_password:
return {
'errno': code.CODE_VALIDATE_ERROR,
'errmsg': message.password_not_match
}
# 3.保存修改用户信息
data = {'password': new_password}
user = services.update_user_data(user,data)
# 4.返回响应信息
return {
'errno': code.CODE_OK,
'errmsg': message.ok
}
# 用户更新交易密码接口
@jwt_required()
@decorator.get_user_object
def update_pay_password(user, old_password, new_password, re_password):
'''
用户更新登录密码接口
:param user: 饰器通过token获取的用户模型对象
:param old_password: 旧密码
:param new_password: 新密码
:param re_password: 确认密码
:return:
'''
# 1.验证旧密码
res = user.check_pay_password(old_password)
if not res:
return {
'errno': code.CODE_PASSWORD_ERROR,
'errmsg': message.password_error
}
# 2.验证新密码和确认密码是否一致
if new_password != re_password:
return {
'errno': code.CODE_VALIDATE_ERROR,
'errmsg': message.password_not_match
}
# 3.保存修改用户信息
data = {'pay_password': new_password}
user = services.update_user_data(user,data)
# 4.返回响应信息
return {
'errno': code.CODE_OK,
'errmsg': message.ok
}
- 数据服务层,汇总重新生成token值
application.apps.users.services
,代码:
from sqlalchemy import or_
from datetime import datetime
from flask import request
from flask_jwt_extended import create_refresh_token,create_access_token
from .models import db, User
from application.utils.iptools import get_address_by_ip
# // 中间代码省略
# 生成用户token值
def generate_user_token(user):
'''
生成用户token值
:param user: 用户模型对象
:return: token字典
'''
from .marshmallow import UserSchema
# 重新序列化user对象
us = UserSchema()
user_data = us.dump(user)
# 重新刷新token值,并返回
return {
'access_token' : create_access_token(identity=user_data),
'refresh_token' : create_refresh_token(identity=user_data)
}
- 根据token获取用户模型对象的装饰器,
application.utils.decorator
,代码:
from flask_jwt_extended import get_jwt_identity
from application.apps.users import services
from application.utils import code, message
# 根据token获取用户模型对象,装饰器
def get_user_object(func):
"""获取用户模型对象"""
def inner(*args, **kwargs):
user_data = get_jwt_identity()
user = services.get_user_by_id(id=user_data['id'])
# 判断用户是否存在
if user is None:
return {
'errno': code.CODE_AUTOORIZATION_ERROR,
'errmsg': message.authorization_is_invalid
}
return func(user, *args, **kwargs)
return inner
- 注册路由地址,
application.apps.users.urls
,代码:
from application import path, api_rpc
# 引入当前蓝图应用视图 , 引入rpc视图
from . import views, api
# 蓝图路径与函数映射列表
urlpatterns = []
# rpc方法与函数映射列表[rpc接口列表]
apipatterns = [
api_rpc('check_mobile', api.check_mobile),
api_rpc('register', api.register),
api_rpc('login', api.login),
api_rpc('refresh', api.refresh_token), # 刷新access_token值
api_rpc('update_avatar', api.update_avatar), # 更新头像
api_rpc('update_nickname', api.update_nickname), # 更新昵称
api_rpc('update_mobile', api.update_mobile), # 更新手机号
api_rpc('update_password', api.update_password), # 更新登录密码
api_rpc('update_pay_password', api.update_pay_password), # 更新交易密码
]
客户端发送更新请求
登陆密码和交易密码
- 配置页面中点击登录密码或交易密码,进入修改页面。
html/setting.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/v-avatar-2.0.3.min.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app user setting" id="app">
<div class="bg">
<img src="../static/images/form_bg.png">
</div>
<img class="back" @click="quit" src="../static/images/user_back.png" alt="">
<div class="form">
<div class="item avatar" @click='to_updata_avatar'>
<span class="title">头像</span>
<span class="goto">></span>
<span class="value">
<!-- 头像处理 -->
<v-avatar v-if="user_data.avatar" :src="user_data.avatar" :size="55" :rounded="true"></v-avatar>
<v-avatar v-else-if="user_data.nickname" :username="user_data.nickname" :size="55" :rounded="true"></v-avatar>
<v-avatar v-else :username="user_data.id" :size="55" :rounded="true"></v-avatar>
</span>
</div>
<div class="item" @click='to_update_nickname'>
<span class="title">昵称</span>
<span class="goto">></span>
<span class="value">{{user_data.nickname}}</span>
</div>
<div class="item" @click='to_update_mobile'>
<span class="title">手机号</span>
<span class="goto">></span>
<span class="value">{{user_data.mobile}}</span>
</div>
<div class="item" @click='to_update_password'>
<span class="title">登陆密码</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item" @click='to_update_pay_password'>
<span class="title">交易密码</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">地址管理</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item">
<span class="title">设备管理</span>
<span class="value"></span>
<span class="goto">></span>
</div>
<div class="item logout">
<img @click="change_account" src="../static/images/change_account.png" alt="">
<p @click="logout">退出账号</p>
</div>
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
user_data: {}, // 用户数据
}
},
// 页面加载之前获取用户数据
created(){
// 获取用户数据
this.get_user_data()
// 监听事件变化
this.listen()
},
methods:{
// 监听事件
listen(){
// 监听头像更新的通知
this.listen_update_avatar();
this.listen_update_nickname();
this.listen_update_mobile();
},
// 监听头像更新的通知
listen_update_avatar(){
api.addEventListener({
name: 'update_avatar_success'
}, (ret, err) => {
// 更新用户数据
this.get_user_data()
});
},
// 监听用户昵称更新的通知
listen_update_nickname(){
api.addEventListener({
name: 'update_nickname_success'
}, (ret, err) => {
// 更新用户数据
this.get_user_data()
});
},
// 监听用户手机号码变化
listen_update_mobile(){
api.addEventListener({
name: 'update_mobile_success'
}, (ret, err) => {
// 更新用户数据
this.get_user_data()
});
},
// 通过token值获取用户数据
get_user_data(){
// 获取token
let token = this.game.getfs('access_token') || this.game.getdata('access_token')
// 通过token值获取用户数据
this.user_data = this.game.get_user_by_token(token)
},
// 返回上一页
quit(){
// 退出当前帧页面
this.game.closeFrame();
},
// 切换账号
change_account(){
this.game.openWin("login","login.html"); // 打开登陆窗口
setTimeout(() => {
this.game.closeWin() // 退出当前窗口
},1000)
},
// 退出账号
logout(){
// 底部弹出框
api.actionSheet({
title: '您确认要退出当前登录吗?',
cancelTitle: '取消', // 取消按钮,buttonIndex=最后一个值
destructiveTitle: "退出登录", // 默认按钮,会自动高亮显示buttonIndex=1
}, (ret, err)=>{
if( ret ){
// this.game.print(ret,1);
if(ret.buttonIndex==1){
this.game.deldata(["access_token","refresh_token"]);
this.game.delfs(["access_token","refresh_token"]);
this.game.openWin("root","index.html"); // 返回主页面
setTimeout(() => {
this.game.closeWin() // 退出当前窗口
},1000)
}
}
});
},
// 点击头像跳转到更新头像页面
to_updata_avatar(){
this.game.openFrame('avatar', 'avatar.html','from_top')
},
// 点击昵称跳转到更改昵称页面
to_update_nickname(){
this.game.openFrame('nickname', 'nickname.html', 'from_top')
},
// 点击手机号跳转到更新手机号码页面
to_update_mobile(){
this.game.openFrame('mobile', 'mobile.html', 'from_top')
},
// 点击登陆密码,跳转到修改登陆密码界面
to_update_password(){
this.game.openFrame('password', 'password.html', 'from_top')
},
// 点击交易密码,跳转到修改登陆交易界面
to_update_pay_password(){
this.game.openFrame('pay_password', 'pay_password.html', 'from_top')
},
},
});
}
</script>
</body>
</html>
- 登陆密码修改窗口,
html/password.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app frame avatar update_password" id="app">
<div class="box">
<p class="title">修改密码</p>
<img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
<div class="content">
<input class="password" type="text" v-model="old_password" placeholder="原密码....">
<input class="password password2" type="text" v-model="password" placeholder="新密码....">
<input class="password password2" type="text" v-model="password2" placeholder="确认新密码....">
</div>
<img @click="update_password_commit" class="btn" src="../static/images/yes.png" alt="">
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
old_password: "", //原密码
password:"",
password2:"",
prev:{name:"",url:"",params:{}},
current:{name:"password",url:"password.html",params:{}},
}
},
methods:{
back(){
this.game.closeFrame();
},
update_password_commit(){
// 提交用户密码
}
}
});
}
</script>
</body>
</html>
- 新增样式:
static/css/main.css
,代码:
.frame input::-webkit-input-placeholder { /* WebKit browsers */
color: white;
}
.update_nickname .nickname,
.update_password .password
{
margin: 8rem 4.4rem 4rem;
width: 19.50rem;
height: 4rem;
line-height: 4rem;
background-color: #cc9966;
outline: none;
border: 1px solid #330000;
text-align: center;
font-size: 1.5rem;
color: #ffffcc;
}
.update_password .password{
margin-top: 5.6rem;
margin-bottom: 0.4rem;
}
.update_password .password2{
margin-top: 0.4rem;
}
- 发送请求更新登陆密码,
html/password.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app frame avatar update_password" id="app">
<div class="box">
<p class="title">修改密码</p>
<img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
<div class="content">
<input class="password" type="password" v-model="old_password" placeholder="原密码....">
<input class="password password2" type="password" v-model="password" placeholder="新密码....">
<input class="password password2" type="password" v-model="password2" placeholder="确认新密码....">
</div>
<img @click="update_password_commit" class="btn" src="../static/images/yes.png" alt="">
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
old_password: "", //原密码
password:"",
password2:"",
prev:{name:"",url:"",params:{}},
current:{name:"password",url:"password.html",params:{}},
}
},
methods:{
back(){
this.game.closeFrame();
},
// 更新登陆密码
update_password_commit(){
// 提交用户密码
if(this.old_password.length < 1 || this.password.length < 1){
this.game.tips('原密码或新密码不能为空!');
return false;
}
if(this.password.length<6 || this.password.length > 16){
this.game.tips("新密码长度在6~16个字符之间!");
return false;
}
if(this.password != this.password2){
this.game.tips("新密码或确认新密码不一致!");
return false;
}
// 昵称校验没问题,向服务端发送请求
let self = this
// 检验用户是否登陆
self.game.check_user_login(self,() =>{
// 获取token值
let token = self.game.getdata('access_token') || self.game.getfs('access_token');
// 向服务端发送请求
self.game.post(self,{
"method": "Users.update_password",
"params":{
"old_password":self.old_password,
"new_password":self.password,
"re_password":self.password2,
},
// 发送请求携带token值
"header":{
"Authorization": "jwt " + token
},
success(response){
let data = response.data;
if(data.result && data.result.errno === 1000){
// 客户端提示
self.game.tips('登陆密码更改成功!')
// 全局广播,更新登陆密码成功
self.game.sendEvent('update_password_success');
// 登陆密码修改,需要重新登陆,
self.game.deldata(['access_token', 'refresh_token']);
self.game.delfs(['access_token', 'refresh_token']);
self.game.openWin('login', 'login.html')
// 2秒后关闭当前窗口
setTimeout(() => {
self.game.closeWin('user')
},2000)
}else {
self.game.tips('登陆密码更改失败!')
}
}
});
});
},
}
});
}
</script>
</body>
</html>
- 交易密码页面发送请求
html/pay_password.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="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app frame avatar update_password" id="app">
<div class="box">
<p class="title">修改交易密码</p>
<img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
<div class="content">
<input class="password" type="password" v-model="old_password" placeholder="原密码....">
<input class="password password2" type="password" v-model="password" placeholder="新密码....">
<input class="password password2" type="password" v-model="password2" placeholder="确认新密码....">
</div>
<img @click="update_password_commit" class="btn" src="../static/images/yes.png" alt="">
</div>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
// 在 #app 标签下渲染一个按钮组件
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
old_password: "", //原密码
password:"",
password2:"",
prev:{name:"",url:"",params:{}},
current:{name:"password",url:"password.html",params:{}},
}
},
methods:{
back(){
this.game.closeFrame();
},
// 更新登陆密码
update_password_commit(){
// 提交用户密码
if(this.old_password.length < 1 || this.password.length < 1){
this.game.tips('原密码或新密码不能为空!');
return false;
}
if(this.password.length<6 || this.password.length > 16){
this.game.tips("新密码长度在6~16个字符之间!");
return false;
}
if(this.password != this.password2){
this.game.tips("新密码或确认新密码不一致!");
return false;
}
// 昵称校验没问题,向服务端发送请求
let self = this
// 检验用户是否登陆
self.game.check_user_login(self,() =>{
// 获取token值
let token = self.game.getdata('access_token') || self.game.getfs('access_token');
// 向服务端发送请求
self.game.post(self,{
"method": "Users.update_pay_password",
"params":{
"old_password":self.old_password,
"new_password":self.password,
"re_password":self.password2,
},
// 发送请求携带token值
"header":{
"Authorization": "jwt " + token
},
success(response){
let data = response.data;
if(data.result && data.result.errno === 1000){
// 客户端提示
self.game.tips('交易密码更改成功!')
// 全局广播,更新交易密码成功
self.game.sendEvent('update_pay_password_success');
// 2秒后关闭当前窗口
setTimeout(() => {
self.back()
},2000)
}else {
self.game.tips('交易密码更改失败!')
}
}
});
});
},
}
});
}
</script>
</body>
</html>