魔坊APP项目-15-邀请好友(业务逻辑流程图、服务端提供邀请好友的二维码生成接口、客户端通过第三方识别微信二维码,服务端提供接口允许访问、App配置私有协议,允许第三方应用通过私有协议,唤醒APP)

邀请好友

1.业务逻辑流程图

魔坊APP项目-15-邀请好友(业务逻辑流程图、服务端提供邀请好友的二维码生成接口、客户端通过第三方识别微信二维码,服务端提供接口允许访问、App配置私有协议,允许第三方应用通过私有协议,唤醒APP)

客户端提供点击"邀请好友"以后的页面frame,html/invite.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/main.js"></script>
	<script src="../static/js/uuid.js"></script>
	<script src="../static/js/settings.js"></script>
</head>
<body>
	<div class="app frame avatar" id="app">
    <div class="box">
      <p class="title">邀请好友</p>
      <img class="close" @click="close_frame" src="../static/images/close_btn1.png" alt="">
      <div class="content">
				<img class="invite_code" src="../static/images/code.jpg" alt="">
			</div>
			<p class="invite_tips">长按保存图片到相册</p>
    </div>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
					prev:{name:"",url:"",params:{}},
					current:{name:"invite",url:"invite.html",params:{}},
				}
			},
			methods:{
        close_frame(){
          this.game.outFrame("invite");
        },
			}
		});
	}
	</script>
</body>
</html>

main.css,代码:

....
.friends_list{
  max-height: 37.2rem;
  overflow: scroll;
}
.friends_list .avatar{
  width: 6.39rem;
  height: 6.39rem;
  position: relative;
}
.friends_list .avatar_bf{
  position: absolute;
  z-index: 1;
  margin: auto;
  width: 4.56rem;
  height: 4.56rem;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}
.friends_list .user_avatar{
  position: absolute;
  z-index: 1;
  width: 4.56rem;
  height: 4.56rem;
  margin: auto;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  border-radius: 1rem;
}
.friends_list .avatar_border{
  position: absolute;
  z-index: 1;
  margin: auto;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  width: 6.1rem;
  height: 6.1rem;
}
.friends_list{
  position: absolute;
  top: 0rem;
  left: 3.6rem;
}
.friends_list .item{
  position: relative;
  background-color: rgba(196,81,9,0.1);
  border-radius: 4px;
  height: 7rem;
  width: 25.8rem;
  margin-bottom: 1rem;
  box-shadow: 2px 2px 5px rgba(9,9,9,0.1);
}
.friends_list .item .avatar{
  position: absolute;
  left: 1rem;
  top: 0;
  bottom: 0;
  margin: auto;
}
.friends_list .item .info{
  position: absolute;
  left: 8rem;
  top: 2rem;
  color: #fff;
  width: 10rem;
}
.friends_list .item .behavior{
  position: absolute;
  left: 16rem;
  font-size: 1.5rem;
  text-align: center;
  line-height: 4rem;
  height: 4rem;
  width: 4rem;
  color: #fff;
  top: 0;
  bottom: 0;
  margin: auto;
  box-shadow: 2px 2px 5px #333333;
}
.friends_list .item .pick{
  background: #336633;
  border-radius: 50%;
}
.friends_list .item .protect{
  background: #990000;
  border-top-right-radius: 5px;
  border-top-left-radius: 5px;
  border-bottom-left-radius: 30px;
  border-bottom-right-radius: 30px;
}
.friends_list .item .goto{
  position: absolute;
  left: 23rem;
  top: 0;
  bottom: 0;
  margin: auto;
  width: 0.96rem;
  height: 1.8rem;
}
.frame input::-webkit-input-placeholder,
.frame textarea::-webkit-input-placeholder{
  color: #fff;
}
.add_friend .box{
  top: 4rem;
  height: 55.56rem;
  background: url("../images/long_bg1.png") no-repeat 0 0;
  background-size: 100%;
}
.add_friend .nickname{
  margin: 4rem 4.6rem 2rem;
  width: 19rem;
  height: 4rem;
  line-height: 4rem;
  background-color: #cc9966;
  outline: none;
  border: 1px solid #330000;
  text-align: center;
  font-size: 1rem;
  color: #ffffcc;
}

.add_friend .friends_list{
  position: absolute;
  top: 15rem;
  left: 3.6rem;
  max-height: 37.2rem;
  overflow: scroll;
}
.add_friend .friends_list .item{
  position: relative;
  margin-left: 1rem;
  background-color: rgba(196,81,9,0.1);
  border-radius: 4px;
  height: 4rem;
  width: 19rem;
  margin-bottom: 1rem;
  box-shadow: 2px 2px 5px rgba(9,9,9,0.1);
}
.add_friend .friends_list .avatar{
  width: 3.84rem;
  height: 3.84rem;
  position: absolute;
  left: 1rem;
}

.add_friend .friends_list .avatar_bf{
  position: absolute;
  z-index: 1;
  margin: auto;
  width: 2.74rem;
  height: 2.74rem;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}

.add_friend .friends_list .user_avatar{
  position: absolute;
  z-index: 1;
  width: 2.74rem;
  height: 2.74rem;
  margin: auto;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  border-radius: 1rem;
}
.add_friend .friends_list .avatar_border{
  position: absolute;
  z-index: 1;
  margin: auto;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  width: 3.66rem;
  height: 3.66rem;
}
.add_friend .friends_list .item .info{
  top: 0.6rem;
  left: 6rem;
}
.add_friend .friends_list .item .time{
  font-size: 0.6rem;
}
.friends_list .item .status{
  position: absolute;
  left: 12rem;
  top: 1.2rem;
  width: 8rem;
  text-align: center;
  height: 2rem;
  color: #fff;
}
.invite_code{
  width: 14rem;
  height: 14rem;
  position: absolute;
  left: 7rem;
  top: 11rem;
}
.invite_tips{
  position: absolute;
  left: 7rem;
  top: 26.4rem;
  text-align: center;
  color: #fff;
  font-size: 1.5rem;
}

用户中心首页, 实现点击打开页面,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/main.js"></script>
	<script src="../static/js/uuid.js"></script>
	<script src="../static/js/settings.js"></script>
</head>
<body>
	<div class="app user" id="app">
		<div class="bg">
      <img src="../static/images/bg0.jpg">
    </div>
		<img class="back" @click="goto_index" src="../static/images/user_back.png" alt="">
		<img class="setting" @click='goto_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="avatar" alt="">
					<img class="avatar_border" src="../static/images/avatar_border.png" alt="">
				</div>
				<p class="user_name">{{nickname}}</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" @click='open_invite_page'>
				<img class="invite_btn" src="../static/images/invite.png" alt="">
			</div>
		</div>
		<div class="menu">
				<div class="item" @click='open_friend_list'>
					<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">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>

			</ul>
		</div>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
					nickname: '',
					avatar: '',
					prev:{name:"",url:"",params:{}},
					current:{name:"user",url:"user.html",params:{}},
				}
			},
			created(){
				this.get_user_info();
				this.change_avatar();
			},
			methods:{
					open_invite_page(){
						// 打开邀请好友页面
						this.game.goFrame('invite', 'invite.html', this.current, null, {
							type: 'push',
							subType: 'from_top',
							duration: 300
						});
					},
					open_friend_list(){
						// 打开好友列表数据页面
						this.game.goFrame('friends', 'friends.html', this.current);
						this.game.goFrame('friend_list', 'friend_list.html', this.current, {
								x: 0,
								y: 190,
								w: 'auto',
								h: 'auto',
						}, null, true);	
					},
					change_avatar(){
						api.addEventListener({
						    name: 'change_avatar'
						}, (ret, err)=>{
						    if( ret ){
						         var token = this.game.get('access_token') || this.game.fget('access_token');
										 this.avatar = `${this.settings.avatar_url}?sign=${ret.value.avatar}&token=${token}`;
						    }
						});

					},
					get_user_info(){
						var token = this.game.get('access_token') || this.game.fget('access_token');
						// 获取当前登录用户基本信息
						this.axios.post('',{
							'jsonrpc': '2.0',
							'id': this.uuid(),
							'method': 'User.info',
							'params': {}
						},{
							headers: {
								Authorization: 'jwt ' + token,
							}
						}).then(response=>{
							var res = response.data.result;
							this.game.print(res);
							if(parseInt(res.errno) === 1000){
								this.nickname = res.nickname;
								this.avatar = `${this.settings.avatar_url}?sign=${res.avatar}&token=${token}`;
							}
						})
					},
				  goto_index(){
            // 返回首页
            this.game.outWin("user");
          },
					goto_setting(){
						// 进入设置
						this.game.goFrame('setting', 'setting.html', this.current);

					}
			}
		});
	}
	</script>
</body>
</html>

2.服务端提供邀请好友的二维码生成接口

flask-qrcode,文档: https://marcoagner.github.io/Flask-QRcode/

安装二维码生成模块

pip install flask-qrcode

初始化qrcode,application/__init__.py,代码:


import os,sys

from flask import Flask
from flask_script import Manager
from flask_sqlalchemy import SQLAlchemy
from flask_redis import FlaskRedis
from flask_session import Session
from flask_migrate import Migrate, MigrateCommand
from flask_jsonrpc import JSONRPC
from flask_marshmallow import Marshmallow
from flask_jwt_extended import JWTManager
from flask_admin import Admin
from flask_babelex import Babel
from faker import Faker
from flask_pymongo import PyMongo
from flask_qrcode import QRcode

from application.utils import init_blueprint
from application.utils.config import load_config
from application.utils.session import init_session
from application.utils.logger import Log
from application.utils.commands import load_command

# 创建终端脚本管理对象
manager = Manager()

# 创建数据库链接对象
db = SQLAlchemy()

# redis链接对象
redis = FlaskRedis()

# Session存储对象
session_store = Session()

# 数据迁移实例对象
migrate = Migrate()

# 日志对象
log = Log()

# jsonrpc模块实例对象
jsonrpc = JSONRPC()

# 数据转换器的对象创建
ma = Marshmallow()

# jwt认证模块实例化
jwt = JWTManager()

# flask_admin模块实例化
admin = Admin()

# flask_babelex模块实例化
babel = Babel()

# mongoDB
mongo = PyMongo()

# qrcode
QRCode = QRcode()

def init_app(config_path):
    """全局初始化"""
    # 创建app应用对象
    app = Flask(__name__)
    # 项目根目录
    app.BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

    # 加载导包路径
    sys.path.insert(0, os.path.join(app.BASE_DIR,"application/utils/language"))

    # 加载配置
    Config = load_config(config_path)
    app.config.from_object(Config)

    # 数据库初始化
    db.init_app(app)
    app.db = db
    redis.init_app(app)
    mongo.init_app(app)

    # 数据转换器的初始化
    ma.init_app(app)

    # session存储初始化
    init_session(app)
    session_store.init_app(app)

    # 数据迁移初始化
    migrate.init_app(app, db)
    # 添加数据迁移的命令到终端脚本工具中
    manager.add_command('db', MigrateCommand)

    # 日志初始化
    app.log = log.init_app(app)

    # 蓝图注册
    init_blueprint(app)

    # jsonrpc初始化
    jsonrpc.service_url = "/api"  # api接口的url地址前缀
    jsonrpc.init_app(app)

    # jwt初始化
    jwt.init_app(app)

    # admin初始化
    admin.init_app(app)

    # 国际化本地化模块的初始化
    babel.init_app(app)

    # 初始化终端脚本工具
    manager.app = app

    # 数据种子生成器[faker]
    app.faker = Faker(app.config.get('LANGUAGE'))
    
    # qrcode初始化配置
    QRCode.init_app(app)

    # 注册自定义命令
    load_command(manager)

    return manager

users/views.py,视图提供生成二维码接口,代码:

from application import QRCode
from flask import make_response, request
@jwt_required  # 验证jwt
def invite_code():
    """邀请好友的二维码"""
    current_user_id = get_jwt_identity()
    user = User.query.get(current_user_id)
    if user is None:
        return {
            'errno': status.CODE_NO_USER,
            'errmsg': message.user_not_exists
        }
    status_path = os.path.join(current_app.BASE_DIR, current_app.config['STATIC_DIR'])
    if not user.avatar:
        user.avatar = current_app.config['DEFAULT_AVATAR']
    avatar = status_path + '/' + user.avatar
    data = current_app.config.get('SERVER_URL', request.host_url[:-1]) + '/users/invite/download?uid=%s' % current_user_id
    image = QRCode.qrcode(data, box_size=16, icon_img=avatar)
    b64_image = image[image.find(',') + 1:]
    qrcode_image = base64.b64decode(b64_image)
    response = make_response(qrcode_image)
    response.headers['Content-Type'] = 'image/png'
    return response

users/urls.py,代码:

from . import views
from application.utils import path
urlpatterns = [
    path('/avatar', views.avatar),
    path('/invite/code', views.invite_code),
    path('/invite/download', views.invite_download),
]

application/settings/dev.py,配置代码:

    # 用户默认头像
    DEFAULT_AVATAR = '54270a03-3587-4638-9156-b1f479efc958.jpeg'
    
    # 服务端带外提供的url地址
    # SERVER_URL = "http://127.0.0.1:5000"

方法二,提供二维码生成接口的视图代码,也可以可以基于原生的QRCode进行编写.

def qrcode():
    import qrcode
    from PIL import Image
    from io import BytesIO
    text = 'https://127.0.0.1:5000/user/invitation?Invitation_user=31'

    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_H,
        box_size=5,
        border=4,
    )

    # 添加数据
    qr.add_data(text)
    # 填充数据
    qr.make(fit=True)
    # 生成图片
    img = qr.make_image(fill_color="#009696", back_color="white")

    # 添加logo,打开logo照片
    static_path = os.path.join(current_app.BASE_DIR, current_app.config["STATIC_DIR"])
    icon = Image.open(static_path+'/1b93988a-ee96-45dc-8df6-65ab062dcfc6.jpeg')
    ext = "jpeg"
    # 获取图片的宽高
    img_w, img_h = img.size
    # 参数设置logo的大小
    factor = 6
    size_w = int(img_w / factor)
    size_h = int(img_h / factor)
    icon_w, icon_h = icon.size
    if icon_w > size_w:
        icon_w = size_w
    if icon_h > size_h:
        icon_h = size_h
    # 重新设置logo的尺寸
    print(icon_w)
    print(icon_h)
    icon = icon.resize((icon_w+20, icon_h+20), Image.ANTIALIAS)
    # 得到画图的x,y坐标,居中显示
    w = int((img_w - icon_w-10) / 2)
    h = int((img_h - icon_h-10) / 2)
    # 黏贴logo照
    img.paste(icon, (w, h), mask=None)
    byte_io = BytesIO()
    img.save(byte_io, ext)
    byte_io.seek(0)
    return send_file(byte_io, mimetype='image/%s' % ext)

客户端获取二维码,html/invite.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/main.js"></script>
	<script src="../static/js/uuid.js"></script>
	<script src="../static/js/settings.js"></script>
</head>
<body>
	<div class="app frame avatar" id="app">
    <div class="box">
      <p class="title">邀请好友</p>
      <img class="close" @click="close_frame" src="../static/images/close_btn1.png" alt="">
      <div class="content">
				<img class="invite_code" :src="code_url" alt="">
			</div>
			<p class="invite_tips">长按保存图片到相册</p>
    </div>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
          code_url: '',  // 二维码url地址
					prev:{name:"",url:"",params:{}},
					current:{name:"invite",url:"invite.html",params:{}},
				}
			},
      created(){
        this.get_qrcode();
      },
			methods:{
        get_qrcode(){
          // 获取二维码
          var token = this.game.get('access_token') || this.game.fget('access_token');
          this.code_url = `${this.settings.code_url}/users/invite/code?token=${token}`;
        },
        close_frame(){
          this.game.outFrame("invite");
        },
			}
		});
	}
	</script>
</body>
</html>

客户端用户长按页面, 保存图片到相册中,html/invite.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/main.js"></script>
	<script src="../static/js/uuid.js"></script>
	<script src="../static/js/settings.js"></script>
</head>
<body>
	<div class="app frame avatar" id="app">
    <div class="box">
      <p class="title">邀请好友</p>
      <img class="close" @click="close_frame" src="../static/images/close_btn1.png" alt="">
      <div class="content">
				<img class="invite_code" :src="code_url" alt="">
			</div>
			<p class="invite_tips">长按保存图片到相册</p>
    </div>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
          code_url: '',  // 二维码url地址
					prev:{name:"",url:"",params:{}},
					current:{name:"invite",url:"invite.html",params:{}},
				}
			},
      created(){
        this.get_qrcode();
      },
			methods:{
        get_qrcode(){
          // 获取二维码
          var token = this.game.get('access_token') || this.game.fget('access_token');
          this.code_url = `${this.settings.code_url}/users/invite/code?token=${token}`;
          // 监听页面是否被长按
          api.addEventListener({
              name: 'longpress'
          }, (ret, err)=>{
              api.saveMediaToAlbum({
                  path: this.code_url
              }, (ret, err)=>{ 
                  if( ret && ret.status ){
                       alert('保存成功');
                  }else{
                       alert('保存失败');
                  }
              });
              
          });
          
        },
        close_frame(){
          this.game.outFrame("invite");
        },
			}
		});
	}
	</script>
</body>
</html>

3.客户端通过第三方识别微信二维码,服务端提供对应的接口允许访问

users/views.py

from flask import render_template

def invite_download():
    uid = request.args.get('uid')
    if 'micromessenger' in request.headers.get('User-Agent').lower():
        position = 'weixin'
    else:
        position = 'other'
        
    return render_template('users/download.html', position=position, uid=uid)

模板目录下创建对应的html模板文件,templates/users/download.html, 代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
    <meta>
    <title>Title</title>
    <style>
    body{
        background-color: #000;
    }
    img{
        width: 100%;
    }
    a{
        color: #fff;
    }
    </style>
</head>
<body>
    {% if position == 'weixin' %}
        <img src="/static/openbrowse.png" alt="">
    {% else %}
        <div id="content">
            
        </div>
        <script>
            // 尝试通过打开客户端已经安装的魔方APP
            var iframe = document.createElement('iframe');
            iframe.src = 'mofang://?uid={{ uid }}';  // app的私有协议
            iframe.hidden = true;
            document.body.appendChild(iframe);
            
            // 如果等待了4秒以后
            setTimeout(function (){
                if (!document.hidden){
                    // 在4秒内如果页面出去了。说明这个时候document.hidden是true,这段代码就不执行了。
                    // 就算是再切回来也是不执行的。
                    // 如果你进了这个函数,没离开。。那就会在4秒后跳进这里 
                    alert('你还没安装魔方APP,去下载');
                    u = navigator.userAgent;
                    let isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1;  // android终端
                    let isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);  // ios终端
                    var content = document.querySelector('#content');
                    if(isiOS){
                        // 去下载ios
                        content.innerHTML = `<a href="/static/app/mofang.apk">下载魔方APP</a>`;
                    }
                    if(isAndroid){
                        // 去下载安卓
                        content.innerHTML = `<a href="/static/app/mofang.apk">下载魔方APP</a>`;

                    }
                }
            }, 5000);
        </script>
    {% endif %}

</body>
</html>

templates->Mark Directory as->Template Folder

魔坊APP项目-15-邀请好友(业务逻辑流程图、服务端提供邀请好友的二维码生成接口、客户端通过第三方识别微信二维码,服务端提供接口允许访问、App配置私有协议,允许第三方应用通过私有协议,唤醒APP)

4.App配置私有协议, 允许第三方应用通过私有协议,唤醒APP

config.xml

 <widget id="A6151729457001"  version="0.0.1">

    <name>MFdemo</name>

    <description>

        Example For APICloud.

    </description>

    <author email="developer@apicloud.com" href="http://www.apicloud.com">

        Developer

    </author>

    <content src="html/index.html" />

    <access origin="*" />

    <preference name="pageBounce" value="false"/>

	<preference name="appBackground" value="rgba(0,0,0,0.0)"/>

	<preference name="windowBackground" value="rgba(0,0,0,0.0)"/>

	<preference name="frameBackgroundColor" value="rgba(0,0,0,0.0)"/>

	<preference name="hScrollBarEnabled" value="false"/>

	<preference name="vScrollBarEnabled" value="false"/>

	<preference name="autoLaunch" value="true"/>

	<preference name="fullScreen" value="false"/>

	<preference name="autoUpdate" value="true" />

	<preference name="smartUpdate" value="false" />

	<preference name="debug" value="true"/>

	<preference name="statusBarAppearance" value="true"/>

	<permission name="readPhoneState" />

	<permission name="camera" />

	<permission name="record" />

	<permission name="location" />

	<permission name="fileSystem" />

	<permission name="internet" />

	<permission name="bootCompleted" />

	<permission name="hardware" />

  <preference name='urlScheme' value='mofang' />

</widget>

接下来的开发,我们不能再依赖官方提供的Apploader进行功能测试了,所以我们使用由APICloud编辑器提供的本地编译, 编译自定义APPLoader来进行测试

魔坊APP项目-15-邀请好友(业务逻辑流程图、服务端提供邀请好友的二维码生成接口、客户端通过第三方识别微信二维码,服务端提供接口允许访问、App配置私有协议,允许第三方应用通过私有协议,唤醒APP)魔坊APP项目-15-邀请好友(业务逻辑流程图、服务端提供邀请好友的二维码生成接口、客户端通过第三方识别微信二维码,服务端提供接口允许访问、App配置私有协议,允许第三方应用通过私有协议,唤醒APP)

由此,带来了另一个问题,就是接下来,APP中打印的信息, 不能继续通过编辑器提供的console终端来查看了,所以我们修改main.js的代码.

	/*
	print(data){
		// 打印数据
		console.log(JSON.stringify(data));
	}
	*/
	print(data, show=false){
		// 打印数据
		if(show){
			alert(JSON.stringify(data));
		}else{
			console.log(JSON.stringify(data));
		}
	}

index.html中监听是否来自第三方应用的唤醒.并接收参数.
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/main.js"></script>
	<script src="../static/js/uuid.js"></script>
	<script src="../static/js/settings.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" @click="gohome" src="../static/images/image2.png"></li>
      <li><img class="module3" src="../static/images/image3.png"></li>
      <li><img class="module4" src="../static/images/image4.png"></li>
    </ul>
  </div>
  <script>
	apiready = function(){
    init();
		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();
          }
        }
      },
      created(){
        this.app_listener();
        this.check_user_login();
      },
			methods:{
        app_listener(){
          // 使用appintenr监听并使用appParam接收URLScheme的参数
          // 收集操作保存起来,并跳转到注册页面.
          // 注册frame中, 用户注册成功以后,记录邀请信息.
          api.addEventListener({
              name: 'appintent'  // 当前事件监听必须是唯一的,整个APP中只能编写一次,否则冲突导致监听无效
          }, (ret, err)=>{
              var appParam = ret.appParam;
              this.game.print(typeof appParam);  // {"uid":"15"}
              // 保存URLScheme参数到本地
              this.game.fsave(appParam);
              // 跳转到注册页面
              this.game.goWin('user', 'register.html', this.current);
              
          });
          
        },
        check_user_login(){
          let token = this.game.get('access_token') || this.game.fget('access_token');
          this.game.checkout(this, token, (new_access_token)=>{
            if(new_access_token.errno == 1005){
              this.game.save({'access_token': ''});
              this.game.fremove('access_token');
            }
          });
        },
        gohome(){
          if(this.game.get('access_token') || this.game.fget('access_token')){
            this.game.goWin('user','user.html', this.current);
          }else {
            this.game.goWin('user','login.html', this.current);
          }
        }
			}
		})
	}
	</script>
</body>
</html>

html/register.html,代码:

<!DOCTYPE html>
<html>
<head>
	<title>注册</title>
	<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
	<meta charset="utf-8">
	<link rel="stylesheet" href="../static/css/main.css">
	<script src="../static/js/vue.js"></script>
	<script src="../static/js/axios.js"></script>
	<script src="../static/js/main.js"></script>
	<script src="../static/js/uuid.js"></script>
	<script src="../static/js/settings.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/register.png">
				<img class="back" @click="back" 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" @change="check_mobile" placeholder="请输入手机号">
				</div>
				<div class="form-item">
					<label class="text">验证码</label>
					<input type="text" class="code" v-model="sms_code" placeholder="请输入验证码">
					<img class="refresh" @click="send" src="../static/images/refresh.png">
				</div>
				<div class="form-item">
					<label class="text">密码</label>
					<input type="password" v-model="password" placeholder="请输入密码">
				</div>
				<div class="form-item">
					<label class="text">确认密码</label>
					<input type="password" v-model="password2" placeholder="请再次输入密码">
				</div>
				<div class="form-item">
					<input type="checkbox" class="agree" v-model="agree" checked>
					<label><span class="agree_text">同意磨方《用户协议》和《隐私协议》</span></label>
				</div>
				<div class="form-item">
					<img class="commit" @click="registerHandle" src="../static/images/commit.png"/>
				</div>
			</div>
		</div>
	</div>
	<script>
	apiready = function(){
		init();
		new Vue({
			el:"#app",
			data(){
				return {
					is_send: false,
					send_interval: 60, // 短信发送冷却时间
					mobile:"",
					password: "",
					password2: "",
					sms_code:"",
					agree:false,
          music_play:true,
					prev:{name:"",url:"",params:{}},
					current:{name:"register",url:"register.html","params":{}},
				}
			},
			watch:{
        music_play(){
          if(this.music_play){
            this.game.play_music("../static/mp3/bg1.mp3");
          }else{
            this.game.stop_music();
          }
        }
      },
			methods:{
				send(){
					// 点击发送短信
					if (!/1[3-9]\d{9}/.test(this.mobile)){
						api.alert({
								title: "警告",
								msg: "手机号码格式不正确!",
						});
						return; // 阻止代码继续往下执行
					}
					if(this.is_send){
						api.alert({
								title: "警告",
								msg: `短信发送冷却中,请${this.send_interval}秒之后重新点击发送!`,
						});
						return; // 阻止代码继续往下执行
					}
					this.axios.post("",{
						"jsonrpc": "2.0",
						"id": this.uuid(),
						"method": "Home.sms",
						"params": {
							"mobile": this.mobile,
						}
					}).then(response=>{
						if(response.data.result.errno != 1000){
							api.alert({
							    title: "错误提示",
							    msg: response.data.result.errmsg,
							});
						}else{
							this.is_send=true; // 进入冷却状态
							this.send_interval = 60;
							var timer = setInterval(()=>{
								this.send_interval--;
								if(this.send_interval<1){
									clearInterval(timer);
									this.is_send=false; // 退出冷却状态
								}
							}, 1000);
						}

					}).catch(error=>{
						this.game.print(error.response);
					});
				},
				registerHandle(){
					// 注册处理
					this.game.play_music('../static/mp3/btn1.mp3');
					// 验证数据[双向验证]
					if (!/1[3-9]\d{9}/.test(this.mobile)){
						api.alert({
								title: "警告",
								msg: "手机号码格式不正确!",
						});
						return; // 阻止代码继续往下执行
					}
					if(this.password.length<3 || this.password.length > 16){
						api.alert({
								title: "警告",
								msg: "密码长度必须在3-16个字符之间!",
						});
						return;
					}
					if(this.password != this.password2){
						api.alert({
								title: "警告",
								msg: "密码和确认密码不匹配!",
						});
						return; // 阻止代码继续往下执行
					}
					if(this.sms_code.length<1){
						api.alert({
								title: "警告",
								msg: "验证码不能为空!",
						});
						return; // 阻止代码继续往下执行
					}
					if(this.agree === false){
						api.alert({
								title: "警告",
								msg: "对不起, 必须同意磨方的用户协议和隐私协议才能继续注册!",
						});
						return; // 阻止代码继续往下执行
					}
					var invite_uid = 0;
					var uid = this.game.fget('uid');  // {"uid":"15"}
					if(uid>0){
						invite_uid = uid;
					}
					this.axios.post("",{
						"jsonrpc": "2.0",
						"id": this.uuid(),
						"method": "User.register",
						"params": {
							"mobile": this.mobile,
							"sms_code":this.sms_code,
							"password":this.password,
							"password2":this.password2,
							'invite_uid': invite_uid
						}
					}).then(response=>{
						this.game.print(response.data.result);
						if(response.data.result.errno != 1000){
							api.alert({
							    title: "错误提示",
							    msg: response.data.result.errmsg,
							});
						}else{
							// 注册成功!
							api.confirm({
							    title: '磨方提示',
							    msg: '注册成功',
							    buttons: ['返回首页', '个人中心']
							}, (ret, err)=>{
							    if(ret.buttonIndex == 1){
										// 跳转到首页
										this.game.outWin("user");
									}else{
										// 删除邀请人
										this.game.femove('uid')
										// 跳转到个人中心
										this.game.goFrame("user",'user.html', this.current);
									}
							});

						}

					}).catch(error=>{
						this.game.print(error.response);
					});

				},
				check_mobile(){
					// 验证手机号码
					this.axios.post("",{
					    "jsonrpc": "2.0",
					    "id": this.uuid(),
					    "method": "User.mobile",
					    "params": {"mobile": this.mobile}
					}).then(response=>{
						this.game.print(response.data.result);
						if(response.data.result.errno != 1000){
							api.alert({
							    title: "错误提示",
							    msg: response.data.result.errmsg,
							});
						}

					}).catch(error=>{
						this.game.print(error.response.data.error);
					});
				},
				back(){
          // this.game.outWin();
          // this.game.outFrame();
          this.game.goGroup("user",0);
				}
			}
		})
	}
	</script>
</body>
</html>

settings.js

function init(){
  if (Game) {
    var game = new Game("../mp3/bg1.mp3");
    Vue.prototype.game = game;
  }
  server_url = 'http://192.168.20.180:5000';
  if(axios){
    // 初始化axios
    axios.defaults.baseURL = server_url+"/api" // 服务端api接口网关地址
    axios.defaults.timeout = 2500; // 请求超时时间
    axios.defaults.withCredentials = false; // 跨域请求资源的情况下,忽略cookie的发送
    Vue.prototype.axios = axios;
    Vue.prototype.uuid  = UUID.generate;
  }
  // 接口相关的配置项
  Vue.prototype.settings = {
    captcha_app_id: "2041284967",  // 腾讯防水墙验证码应用ID
    avatar_url: server_url+"/users/avatar",
    code_url: server_url,
  }
}

针对用户的注册功能, 增加invite_uid的处理,users/views.py视图代码:

@jsonrpc.method("User.register")
def register(mobile,password,password2, sms_code, invite_uid):
    """用户信息注册"""

    try:
        ms = MobileSchema()
        ms.load({"mobile": mobile})

        us = UserSchema()
        user = us.load({
            "mobile":mobile,
            "password":password,
            "password2":password2,
            "sms_code": sms_code,
            'invite_uid': invite_uid,
        })
        data = {"errno": status.CODE_OK,"errmsg":us.dump(user)}
    except ValidationError as e:
        data = {"errno": status.CODE_VALIDATE_ERROR,"errmsg":e.messages}
    return data

users/marshmallow.py,代码

from marshmallow_sqlalchemy import SQLAlchemyAutoSchema,auto_field
from marshmallow import post_load,pre_load,validates_schema
from application import redis
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)
    invite_uid = fields.Integer(required=True, load_only=True)

    class Meta:
        model = User
        include_fk = True # 启用外键关系
        include_relationships = True # 模型关系外部属性
        fields = ["id", "name","mobile","password","password2","sms_code", 'invite_uid']  # 如果要全换全部字段,就不要声明fields或exclude字段即可
        sql_session = db.session

    @post_load()
    def save_object(self, data, **kwargs):
        invite_uid = int(data['invite_uid'])
        data.pop("password2")
        data.pop("sms_code")
        data.pop('invite_uid')
        data["name"] = data["mobile"]
        instance = User(**data)
        db.session.add( instance )
        db.session.commit()

        # 记录邀请信息到Mongdb中
        if invite_uid > 0:
            """只有invite_uid大于0,才是经过邀请注册进来的新用户"""
            # 验证是否属于有效的邀请
            invite_user = User.query.get(invite_uid)
            if invite_user is not None:
                """只有邀请人存在的情况下才算有效邀请"""
                query = {'_id': invite_uid}
                ret = mongo.db.user_invite_list.find_one(query)
                if ret:
                    mongo.db.user_invite_list.update(query, {'$push': {'invite_list': instance.id}})
                else:
                    data = {'_id': invite_uid, 'invited_list': [instance.id]}
                    mongo.db.user_invite_list.insert(data)
                # 添加好友关系
        
        return instance

    @validates_schema
    def validate(self,data, **kwargs):
        # 校验密码和确认密码
        if data["password"] != data["password2"]:
            raise ValidationError(message=Message.password_not_match,field_name="password")

        #todo 校验短信验证码
        #1. 从redis中提取验证码
        redis_sms_code = redis.get("sms_%s" % data["mobile"])
        if redis_sms_code is None:
            raise ValidationError(message=Message.sms_code_expired,field_name="sms_code")
        redis_sms_code = redis_sms_code.decode()
        #2. 从客户端提交的数据data中提取验证码
        sms_code = data["sms_code"]
        #3. 字符串比较,如果失败,则抛出异常,否则,直接删除验证码
        if sms_code != redis_sms_code:
            raise ValidationError(message=Message.sms_code_error, field_name="sms_code")

        redis.delete("sms_%s" % data["mobile"])

        return data
上一篇:Swiper Usage&&API


下一篇:E1. Permutation Minimization by Deque