欢迎来到十九分快乐的博客

生死看淡,不服就干。

12.余额充值-背包管理-mongoengine - celery整合

种植园

余额充值

在orchard.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/socket.io.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 orchard" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="orchard-bg">
			<img src="../static/images/bg2.png">
			<img class="board_bg2" src="../static/images/board_bg2.png">
		</div>
    <img class="back" @click="back" src="../static/images/user_back.png" alt="">
    <div class="header">
			<div class="info">
				<div class="avatar" @click='to_user'>
					<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="62" :rounded="true"></v-avatar>
						<v-avatar v-else-if="user_data.nickname" :username="user_data.nickname" :size="62" :rounded="true"></v-avatar>
						<v-avatar v-else :username="user_data.id" :size="62" :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" @click='user_recharge'>
				<div class="balance">
					<p class="title"><img src="../static/images/money.png" alt="">钱包</p>
					<p class="num">{{user_data.money}}</p>
				</div>
				<div class="balance">
					<p class="title"><img src="../static/images/integral.png" alt="">果子</p>
					<p class="num">{{user_data.credit}}</p>
				</div>
			</div>
      <div class="menu-list">
        <div class="menu">
          <img src="../static/images/menu1.png" alt="">
          排行榜
        </div>
        <div class="menu">
          <img src="../static/images/menu2.png" alt="">
          签到有礼
        </div>
        <div class="menu">
          <img src="../static/images/menu3.png" alt="">
          道具商城
        </div>
        <div class="menu">
          <img src="../static/images/menu4.png" alt="">
          邮件中心
        </div>
      </div>
		</div>
		<div class="footer" >
      <ul class="menu-list">
        <li class="menu">新手</li>
        <li class="menu">背包</li>
        <li class="menu-center" @click="to_shop">商店</li>
        <li class="menu">消息</li>
        <li class="menu">好友</li>
      </ul>
    </div>
	</div>
	<script>
	apiready = function(){
		Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
		new Vue({
			el:"#app",
			data(){
				return {
					user_data:{},  // 当前用户信息
          music_play:true,
          namespace: '/mofang', // websocket命名空间
          socket: null,
					recharge_list: [10,20,30,50,100,200,500,1000], // 允许充值的金额
				}
			},
      created(){
				// socket建立连接
				this.socket_connect();
        // 自动加载我的果园页面
				this.to_my_orchard();
				// 获取用户数据
				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(){
					let self = this;
					// 检查token是否过期,过期从新刷新token
					self.game.check_user_login(self, ()=>{
						// 获取token
						let token = this.game.getfs('access_token') || this.game.getdata('access_token')
						// 根据token获取用户数据
						this.user_data = this.game.get_user_by_token(token)
					})
				},

				// socket连接
        socket_connect(){
          this.socket = io.connect(this.game.config.SOCKET_SERVER + this.namespace, {transports: ['websocket']});
          this.socket.on('connect', ()=>{
              this.game.print("开始连接服务端");
          });
        },

        // 跳转我的果园页面
        to_my_orchard(){
          this.game.check_user_login(this, ()=>{
            this.game.openFrame("my_orchard","my_orchard.html","from_right",{
    					marginTop: 174,     //相对父页面上外边距的距离,数字类型
    					marginLeft: 0,      //相对父页面左外边距的距离,数字类型
    			    marginBottom: 54,     //相对父页面下外边距的距离,数字类型
    			    marginRight: 0     //相对父页面右外边距的距离,数字类型
    				});
          })
        },

        // 点击商店打开道具商城页面
				to_shop(){
          this.game.check_user_login(this, ()=>{
            this.game.openFrame('shop', 'shop.html', 'from_top');
          })
        },

				// 点击头像,跳转用户中心页面
				to_user(){
					this.game.check_user_login(this, ()=>{
            this.game.openFrame('user', 'user.html', 'from_right');
          })
				},

				// 点击钱包进行用户充值,设置充值金额
				user_recharge(){
					api.actionSheet({
								title: '余额充值',
								cancelTitle: '取消',
								buttons: this.recharge_list
						}, (ret, err)=>{
								if( ret.buttonIndex <= this.recharge_list.length ){
										 // 充值金额
										 money = this.recharge_list[ret.buttonIndex-1];
										 // 调用支付宝重置
										 this.game.print(money,1);
								}
					});
				},


        back(){
          this.game.openWin("root");
        },
			}
		});
	}
	</script>
</body>
</html>

用户选择了对应的充值金额以后,我们就要让后端基于接口配置参数生成支付参数。对于这块,与以往在PC端开发不一样,我们现在正在开发的是APP应用,所以这里我们需要进行另一种方式实现支付。

自定义Loader调试工具

我们现在开发使用的APPLoader功能只能让我们调用官方内置的模块应用,如果要使用第三方开发者编写的模块,则必须安装自定义Loader开发工具,对于在APICloud中如果要实现支付,我们可以直接采用官网提供的第三方模块集成到项目中直接实现支付。

  1. 首先来到APICloud开发者管理后台,把Alipayplus模块加载到APP。

    1599708262506

  2. 点击Alipayplus进入模块详情。

    1599708320671

  3. 把模块使用到指定APP中。[下图只做参看, 项目已定义叫appdemo]

    image-20210326094637980

一般开发APICloud的时候,如果使用到了第三方模块,则在基于这些开发模块的代码必须运行在自定义Loader里面。所以,为了节省时间,我们往往会一次性多添加几个模块进来,方便测试,不需要每次新增模块,每次生成。

image-20210326100150781

把生成的自定义Loader安装的到模拟器或者调试手机中。

image-20210326102905685

基于Alipayplus模块完成支付

支付流程:

1605512011892

服务端提供充值api接口

支付宝支付配置

接下来,服务端中需要完成的就是生成订单参数和接收支付结果,所以我们先 安装alipay的sdk,集成到flask项目中。

  1. 安装:
pip install python-alipay-sdk --upgrade
  1. 配置支付宝的公钥和私钥,保存到application/settings/alipay_keys目录下。
mkdir -p application/settings/alipay_keys
cd application/settings/alipay_keys
openssl
genrsa -out app_private_key.pem   2048  # 私钥
rsa -in app_private_key.pem -pubout -out app_public_key.pem # 根据私钥导出公钥
exit

目前属于开发阶段,我们采用沙箱环境来完成。https://openhome.alipay.com/platform/appDaily.htm?tab=info

image-20210326104011313

保存商户公钥(app_public_key.pem的内容,首行和末行是秘钥声明,不属于内容范围)到支付宝开放平台上, 并从开放平台中把支付宝公钥保存到项目的keys目录中(保存在pem文件中需要添加首行和末行的秘钥声明)。

image-20201228175510468

支付宝公钥 application/settings/alipay_keys/alipay_public_key.pem

-----BEGIN PUBLIC KEY-----
支付宝公钥,不要添加多余信息,例如本来不换行的就不要给它换行。
-----END PUBLIC KEY-----
  1. 开发环境, 配置支付宝相关信息,application/settings/dev.py,代码:
"""支付接口配置信息-支付宝"""
ALIPAY_DEV_GATEWAY = "https://openapi.alipaydev.com/gateway.do"  # 沙箱网关地址
ALIPAY_GETWATE     = "https://openapi.alipay.com/gateway.do"     # 真实网关地址
# 支付接口应用ID
ALIPAY_APP_ID = "2021000117635432"
# 秘钥加密算法
ALIPAY_SIGN_TYPE = "RSA2"
# 异步回调地址
ALIPAY_NOTIFY_URL = "https://www.mofang.com/users/alipay/notify" # 异步回调通知[只能线上测试]
# 应用私钥
APP_PRIVATE_KEY_PATH = "application/settings/alipay_keys/app_private_key.pem"
# 支付宝公钥信息
ALIPAY_PUBLIC_KEY_PATH = "application/settings/alipay_keys/alipay_public_key.pem"
# 是否处于沙箱模式,开发时没有真实接口账号则值必须为True,否则无法完成支付的
ALIPAY_SANDBOX = True
# 充值金额列表
RECHARGE_LIST = [10, 20, 50, 100, 200, 500, 1000]

  1. 封装支付宝充值接口 application/utils/payments.py
'''支付充值接口封装'''
import os
from alipay import AliPay, AliPayConfig

# 支付宝接口操作辅助类
class AlipayClient(object):
    '''支付宝接口操作辅助类'''
    def __init__(self, app):
        self.app = app
    
    # 实例化sdk    
    @property
    def sdk(self):
        # 应用私钥
        app_private_key_string = open(
            os.path.join(self.app.BASE_DIR, self.app.config.get("APP_PRIVATE_KEY_PATH"))).read()
        # 支付宝公钥
        alipay_public_key_string = open(
            os.path.join(self.app.BASE_DIR, self.app.config.get("ALIPAY_PUBLIC_KEY_PATH"))).read()

        # 实例化支付宝SDK
        alipay = AliPay(
            appid=self.app.config.get("ALIPAY_APP_ID"),
            app_notify_url=None,  # 默认回调url
            app_private_key_string=app_private_key_string,
            # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
            alipay_public_key_string=alipay_public_key_string,
            sign_type="RSA2",  # 加密方式
            debug=self.app.config.get("ALIPAY_SANDBOX"),  # 是否使用沙箱调试模式
            config=AliPayConfig(timeout=15)  # 可选, 请求超时时间
        )
        return alipay
    
    # 生成订单参数
    def create_app_pay(self,money, out_trade_number, order_subject):
        '''
        生成订单参数
        :param money: 充值金额
        :param out_trade_number: 订单号
        :param order_subject: 订单标题
        :return: 
        '''
        # 返回支付订单参数
        order_string = self.sdk.client_api(  # alipay3.0以后的写法
            "alipay.trade.app.pay",  # 接口名称,原来的接口方法名
            biz_content={
                "out_trade_no": out_trade_number,
                "total_amount": money,
                "subject": order_subject
            },
            notify_url=self.app.config.get("ALIPAY_NOTIFY_URL"),  # 支付完成以后的异步回调地址
        )
        return order_string

    # 支付宝支付后返回的结果
    def recharge_result(self, out_trade_no):
        '''
        支付宝支付后返回的结果
        :param out_trade_no: 订单号
        :return: 
        '''
        # 支付宝支付返回的结果
        res = self.sdk.server_api(
            "alipay.trade.query",  # 查询订单信息
            biz_content={
                "out_trade_no": out_trade_no
            }
        )
        # print(res)
        '''{
        'code': '10000', 
        'msg': 'Success', 
        'buyer_logon_id': 'nmi***@sandbox.com',
        'buyer_pay_amount': '0.00',
         'buyer_user_id': '2088622955697780', 
         'buyer_user_type': 'PRIVATE', 
         'invoice_amount': '0.00',
         'out_trade_no': '2106282046250000000127668897', 
         'point_amount': '0.00', 'receipt_amount': '0.00',
         'send_pay_date': '2021-06-28 20:47:09', 
         'total_amount': '100.00', 'trade_no': '2021062822001497780501537140',
         'trade_status': 'TRADE_SUCCESS'
         }'''
        # 支付是否成功,主要看trade_status状态,
        # ["TRADE_SUCCESS", "TRADE_FINISHED"] 都是成功
        return res

服务端提供充值api接口

  1. 视图,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 application import message, code, oss
# 引入decorator中的装饰器,get_user_object根据token获取用户模型对象
from application.utils import captcha, decorator
# 引入支付宝
from application.utils.payments import AlipayClient
from . import tasks  # 引入定时任务
from . import services
# 引入构造器(序列化器)
from .marshmallow import MobileSchema, ValidationError, UserSchema, UpdateMobileSchema


# 充值金额选项列表
def recharge_list():
    return {
        'errno': code.CODE_OK,
        'errmsg': message.ok,
        'recharge_list': current_app.config.get('RECHARGE_LIST')
    }

# 生成充值记录接口,返回充值订单信息
@jwt_required()
@decorator.get_user_object
def recharge(user, money):
    '''
    生成充值记录接口,返回充值订单信息
    :param user: 装饰器通过token获取的用户模型对象
    :param money: 充值金额
    :return:
    '''

    money = float(money)
    # 查看输入充值金额是否符合规范
    if money not in current_app.config.get('RECHARGE_LIST'):
        return {
            'errno': code.CODE_PARAMS_ERROR,
            'errmsg': message.params_error
        }

    # 生成订单号
    out_trade_number = services.create_trade_number(user)
    # 订单标题
    order_subject = "余额充值-%s元" % money
    # 创建充值记录
    try:
        services.add_racharge_log(user, out_trade_number, order_subject, money)
    except services.ReChargeError:
        return {
            'errno': code.CODE_DATA_SAVE_ERROR,
            'errmsg': message.data_save_error,
        }

    # 实例化支付宝SDK
    alipay = AlipayClient(current_app)

    # 返回支付订单参数
    order_string = alipay.create_app_pay(money, out_trade_number, order_subject)

    return {
        "errno": code.CODE_OK,
        "errmsg": message.ok,
        "sandbox": current_app.config.get("ALIPAY_SANDBOX"),
        "order_string": order_string,
        "order_number": out_trade_number,
    }
  1. 数据处理层, users/services.py,代码:
# 生成订单号
def create_trade_number(user):
    '''生成订单号'''
    return datetime.now().strftime("%y%m%d%H%M%S") + "%08d" % user.id + "%08d" % random.randint(0, 99999999)

# 充值记录失败错误
class ReChargeError(Exception):
    """充值记录生成失败"""
    pass

# 添加充值记录到MongoDB
def add_racharge_log(user, out_trade_number, order_subject, money):
    '''
    添加充值记录到MongoDB
    :param user: 用户模型对象
    :param out_trade_number: 订单号
    :param order_subject: 订单标题
    :param money: 充值金额
    :return:
    '''
    document = {
        "user_id": user.id,  # 用户ID
        "out_trade_number": out_trade_number,  # 订单号,一般可以在mongoDB设置唯一索引,避免重复
        "order_subject": order_subject,  # 订单标题
        "money": money,  # 充值金额
        "create_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),  # 下单时间
        "pay_status": False,  # 充值支付状态
        "pay_time": "",  # 实际到付时间
    }
    # 添加记录到mongo
    try:
        mongo.db.user_recharge_log.insert_one(document)
    except:
        raise ReChargeError

  1. 错误提示码和提示信息

错误提示码,application/utils/code.py,代码:

CODE_DATA_SAVE_ERROR = 1013     # 数据储存失败

错误提示,application/utils/message.py,代码:

data_save_error = '数据储存失败'

  1. 路由 users/urls.py,代码:
from application import path, api_rpc
# 引入当前蓝图应用视图 , 引入rpc视图
from . import views, api

# 蓝图路径与函数映射列表
urlpatterns = [
    path('invite.png', views.invite_code), # 生成邀请码,推广应用
    path('invite', views.invite), # 唤醒或下载APP软件
]

# 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), # 更新交易密码
    api_rpc('get_apply_friend_history', api.get_apply_friend_history), # 获取好友申请列表
    api_rpc('search_user_info', api.search_user_info), # 搜索用户信息
    api_rpc('apply_friend', api.apply_friend), # 添加用户好友申请记录
    api_rpc('add_friend', api.add_friend), # 添加用户好友关系记录
    api_rpc('cancel_apply_friend', api.cancel_apply_friend), # 用户取消自己申请的好友记录
    api_rpc('get_friend_list', api.get_friend_list), # 获取好友列表
    api_rpc('recharge_list', api.recharge_list), # 获取用户充值金额列表
    api_rpc('recharge', api.recharge), # 用户进行金额充值
]


客户端发起充值请求,并调用alipayplus模块发起支付.

html/orchard.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/socket.io.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 orchard" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="orchard-bg">
			<img src="../static/images/bg2.png">
			<img class="board_bg2" src="../static/images/board_bg2.png">
		</div>
    <img class="back" @click="back" src="../static/images/user_back.png" alt="">
    <div class="header">
			<div class="info">
				<div class="avatar" @click='to_user'>
					<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="62" :rounded="true"></v-avatar>
						<v-avatar v-else-if="user_data.nickname" :username="user_data.nickname" :size="62" :rounded="true"></v-avatar>
						<v-avatar v-else :username="user_data.id" :size="62" :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" @click='user_recharge'>
				<div class="balance">
					<p class="title"><img src="../static/images/money.png" alt="">钱包</p>
					<p class="num">{{user_data.money}}</p>
				</div>
				<div class="balance">
					<p class="title"><img src="../static/images/integral.png" alt="">果子</p>
					<p class="num">{{user_data.credit}}</p>
				</div>
			</div>
      <div class="menu-list">
        <div class="menu">
          <img src="../static/images/menu1.png" alt="">
          排行榜
        </div>
        <div class="menu">
          <img src="../static/images/menu2.png" alt="">
          签到有礼
        </div>
        <div class="menu">
          <img src="../static/images/menu3.png" alt="">
          道具商城
        </div>
        <div class="menu">
          <img src="../static/images/menu4.png" alt="">
          邮件中心
        </div>
      </div>
		</div>
		<div class="footer" >
      <ul class="menu-list">
        <li class="menu">新手</li>
        <li class="menu">背包</li>
        <li class="menu-center" @click="to_shop">商店</li>
        <li class="menu">消息</li>
        <li class="menu">好友</li>
      </ul>
    </div>
	</div>
	<script>
	apiready = function(){
		Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
		new Vue({
			el:"#app",
			data(){
				return {
					user_data:{},  // 当前用户信息
          music_play:true,
          namespace: '/mofang', // websocket命名空间
          socket: null,
					recharge_list: [], // 允许充值的金额列表
				}
			},
      created(){
				// socket建立连接
				this.socket_connect();
        // 自动加载我的果园页面
				this.to_my_orchard();
				// 获取用户数据
				this.get_user_data()
				// 监听事件变化
				this.listen()
				// 获取充值金额列表
				this.get_recharge_list()
      },
			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(){
					let self = this;
					// 检查token是否过期,过期从新刷新token
					self.game.check_user_login(self, ()=>{
						// 获取token
						let token = this.game.getfs('access_token') || this.game.getdata('access_token')
						// 根据token获取用户数据
						this.user_data = this.game.get_user_by_token(token)
					})
				},

				// socket连接
        socket_connect(){
          this.socket = io.connect(this.game.config.SOCKET_SERVER + this.namespace, {transports: ['websocket']});
          this.socket.on('connect', ()=>{
              this.game.print("开始连接服务端");
          });
        },

        // 跳转我的果园页面
        to_my_orchard(){
          this.game.check_user_login(this, ()=>{
            this.game.openFrame("my_orchard","my_orchard.html","from_right",{
    					marginTop: 174,     //相对父页面上外边距的距离,数字类型
    					marginLeft: 0,      //相对父页面左外边距的距离,数字类型
    			    marginBottom: 54,     //相对父页面下外边距的距离,数字类型
    			    marginRight: 0     //相对父页面右外边距的距离,数字类型
    				});
          })
        },

        // 点击商店打开道具商城页面
				to_shop(){
          this.game.check_user_login(this, ()=>{
            this.game.openFrame('shop', 'shop.html', 'from_top');
          })
        },

				// 点击头像,跳转用户中心页面
				to_user(){
					this.game.check_user_login(this, ()=>{
            this.game.openFrame('user', 'user.html', 'from_right');
          })
				},

				// 获取充值金额列表
				get_recharge_list(){
					let self = this;
					self.game.check_user_login(self,()=>{
						self.game.post(self,{
							'method': 'Users.recharge_list',
							'params': {},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									self.recharge_list = data.result.recharge_list
								}
							}
						})
					})
				},

				// 点击钱包进行用户充值,设置充值金额
				user_recharge(){
					api.actionSheet({
								title: '余额充值',
								cancelTitle: '取消',
								buttons: this.recharge_list
						}, (ret, err)=>{
								if( ret.buttonIndex <= this.recharge_list.length ){
										 // 充值金额
										 let money = this.recharge_list[ret.buttonIndex-1];
										 this.game.print(money,1);
										// 发送支付宝充值请求
										this.recharge_app_pay(money)
								}
					});
				},

				// 发送支付宝充值请求
				recharge_app_pay(money){
					// 获取支付宝支付对象
					let aliPayPlus = api.require("aliPayPlus");
					let self = this;
					// 向服务端发送请求获取终止订单信息
					self.game.check_user_login(self, ()=>{
						let token = this.game.getdata("access_token") || this.game.getfs("access_token");
						self.game.post(self,{
							'method': 'Users.recharge',
							'params':{
								'money': money,
							},
							'header': {
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 本次充值的订单参数
									let order_string = data.result.order_string;

									// 支付完成以后,支付APP返回的响应状态码
									let resultCode = {
										"9000": "支付成功!",
										"8000": "支付正在处理中,请稍候!",
										"4000": "支付失败!请联系我们的工作人员~",
										"5000": "支付失败,重复的支付操作",
										"6002": "网络连接出错",
										"6004": "支付正在处理中,请稍后",
									}
									// 唤醒支付宝APP,发起支付
									aliPayPlus.payOrder({
										orderInfo: order_string,
										sandbox: data.result.sandbox, // 将来APP上线需要修改成false
									},(ret, err)=>{
										if(resultCode[ret.code]){
											// 提示支付结果
											self.game.tips(resultCode[ret.code]);
											// 根据结果提示,到服务端查询订单结果和修改订单状态
											}
									})
								}
							}
						})
					})
				},


        back(){
          this.game.openWin("root");
        },
			}
		});
	}
	</script>
</body>
</html>


服务端处理支付结果

在客户端中,当用户充值成功以后,手机支付宝APP,就会跳转回到魔方客户端,通过参数ret.code判断是否是9000,来识别是否支付成功了。此时,我们需要在服务端,修改订单状态以及用户的余额。所以需要让客户端接收到9000状态码时,发送同步通知结果给服务端。服务端需要发送请求到支付宝官网根据订单号获取订单的支付状态,然后进一步确认支付是否成功了。

  1. 视图users.api,代码:
# 根据用户信息和订单号查询充值结果
@jwt_required()
@decorator.get_user_object
def check_recharge_result(user, order_number):
    '''
    根据用户信息和订单号查询充值结果
    :param user: 装饰器通过token获取的用户模型对象
    :param order_number: 订单号
    :return:
    '''

    # 获取用户订单记录信息
    order = services.get_order_by_order_number(user, order_number)
    if not order:
        return {
            "errno": code.CODE_ORDER_NOT_EXISTS,
            "errmsg": message.order_not_exists,
        }

    # 实例化支付宝SDK
    alipay = AlipayClient(current_app)

    # 支付宝支付返回的结果
    ret = alipay.recharge_result(order_number)

    # 判断是否支付成功
    if ret.get('trade_status') and ret.get('trade_status') in ["TRADE_SUCCESS", "TRADE_FINISHED"]:
        '''支付成功了'''
        # 根据订单结果来判断订单状态,修改本地MongoDB的充值日志,并给用户添加对应的金额
        try:
            services.user_recharge_success(user, order)
            # 重新刷新token值
            token = services.generate_user_token(user)
            return {
                'errno': code.CODE_OK,
                'errmsg': message.ok,
                **token
            }
        except services.ReChargeHandleError:
            return {
                'errno': code.CODE_RECHARGE_HANDLER_ERROR,
                'errmsg': message.recharge_handler_error
            }

    return {
        'errno': code.CODE_RECHARGE_FAIL,
        'errmsg': message.recharge_fail
    }

  1. 数据处理层, users.services,代码:
# 获取用户订单记录信息
def get_order_by_order_number(user, order_number):
    '''
    获取订单信息
    :param user: 用户模型对象
    :param order_number: 订单号
    :return:
    '''
    query = {
        'user_id': user.id,
        'out_trade_number': order_number
    }

    return mongo.db.user_recharge_log.find_one(query)

# 充值处理异常!
class ReChargeHandleError(Exception):
    pass

# 充值成功后的数据处理
def user_recharge_success(user, order):
    '''
    充值成功后的数据处理
    :param user: 用户模型对象
    :param order: 订单信息
    :return:
    '''
    try:
        # 1.修改充值订单记录
        query = {
            'user_id': user.id,
            'out_trade_number': order['out_trade_number']
        }
        update = {
            '$set':{
                'pay_status': True,
                'pay_time': datetime.now().strftime("%Y-%m-%d %H:%M:%S"), # 到付时间
            }
        }
        mongo.db.user_recharge_log.update_one(query, update)

        # 2.修改用户金额
        user.money = float(user.money) + float(order['money'])
        db.session.commit()

    except Exception:
        raise ReChargeHandleError

  1. 提示码和提示信息

application/utils/code.py,错误提示码,代码:

CODE_ORDER_NOT_EXISTS = 1014    # 订单不存在
CODE_RECHARGE_HANDLER_ERROR = 1015 # 充值处理过程发生异常
CODE_RECHARGE_FAIL = 1016       # 充值失败

application/utils/message.py,错误文本提示,代码:

order_not_exists = '订单不存在'
recharge_handler_error = '充值处理过程发生异常'
recharge_fail = '充值失败'

  1. 路由users.urls,代码:
    api_rpc('check_recharge_result', api.check_recharge_result), # 检验用户充值结果(是否成功)


客户端支付成功处理

种植园页面 html/orchard.html,在支付宝APP支付完成以后,发起ajax请求到后台确认订单是否支付完成。代码:

<!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/socket.io.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 orchard" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="orchard-bg">
			<img src="../static/images/bg2.png">
			<img class="board_bg2" src="../static/images/board_bg2.png">
		</div>
    <img class="back" @click="back" src="../static/images/user_back.png" alt="">
    <div class="header">
			<div class="info">
				<div class="avatar" @click='to_user'>
					<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="62" :rounded="true"></v-avatar>
						<v-avatar v-else-if="user_data.nickname" :username="user_data.nickname" :size="62" :rounded="true"></v-avatar>
						<v-avatar v-else :username="user_data.id" :size="62" :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" @click='user_recharge'>
				<div class="balance">
					<p class="title"><img src="../static/images/money.png" alt="">钱包</p>
					<p class="num">{{game.number_format(user_data.money)}}</p>
				</div>
				<div class="balance">
					<p class="title"><img src="../static/images/integral.png" alt="">果子</p>
					<p class="num">{{game.number_format(user_data.credit)}}</p>
				</div>
			</div>
      <div class="menu-list">
        <div class="menu">
          <img src="../static/images/menu1.png" alt="">
          排行榜
        </div>
        <div class="menu">
          <img src="../static/images/menu2.png" alt="">
          签到有礼
        </div>
        <div class="menu">
          <img src="../static/images/menu3.png" alt="">
          道具商城
        </div>
        <div class="menu">
          <img src="../static/images/menu4.png" alt="">
          邮件中心
        </div>
      </div>
		</div>
		<div class="footer" >
      <ul class="menu-list">
        <li class="menu">新手</li>
        <li class="menu">背包</li>
        <li class="menu-center" @click="to_shop">商店</li>
        <li class="menu">消息</li>
        <li class="menu">好友</li>
      </ul>
    </div>
	</div>
	<script>
	apiready = function(){
		Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
		new Vue({
			el:"#app",
			data(){
				return {
					user_data:{},  // 当前用户信息
          music_play:true,
          namespace: '/mofang', // websocket命名空间
          socket: null,
					recharge_list: [], // 允许充值的金额列表
				}
			},
      created(){
				// socket建立连接
				this.socket_connect();
        // 自动加载我的果园页面
				this.to_my_orchard();
				// 获取用户数据
				this.get_user_data()
				// 监听事件变化
				this.listen()
				// 获取充值金额列表
				this.get_recharge_list()
      },
			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(){
					let self = this;
					// 检查token是否过期,过期从新刷新token
					self.game.check_user_login(self, ()=>{
						// 获取token
						let token = this.game.getfs('access_token') || this.game.getdata('access_token')
						// 根据token获取用户数据
						this.user_data = this.game.get_user_by_token(token)
					})
				},

				// socket连接
        socket_connect(){
          this.socket = io.connect(this.game.config.SOCKET_SERVER + this.namespace, {transports: ['websocket']});
          this.socket.on('connect', ()=>{
              this.game.print("开始连接服务端");
          });
        },

        // 跳转我的果园页面
        to_my_orchard(){
          this.game.check_user_login(this, ()=>{
            this.game.openFrame("my_orchard","my_orchard.html","from_right",{
    					marginTop: 174,     //相对父页面上外边距的距离,数字类型
    					marginLeft: 0,      //相对父页面左外边距的距离,数字类型
    			    marginBottom: 54,     //相对父页面下外边距的距离,数字类型
    			    marginRight: 0     //相对父页面右外边距的距离,数字类型
    				});
          })
        },

        // 点击商店打开道具商城页面
				to_shop(){
          this.game.check_user_login(this, ()=>{
            this.game.openFrame('shop', 'shop.html', 'from_top');
          })
        },

				// 点击头像,跳转用户中心页面
				to_user(){
					this.game.check_user_login(this, ()=>{
            this.game.openFrame('user', 'user.html', 'from_right');
          })
				},

				// 获取充值金额列表
				get_recharge_list(){
					let self = this;
					self.game.check_user_login(self,()=>{
						self.game.post(self,{
							'method': 'Users.recharge_list',
							'params': {},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									self.recharge_list = data.result.recharge_list
								}
							}
						})
					})
				},

				// 点击钱包进行用户充值,设置充值金额
				user_recharge(){
					api.actionSheet({
								title: '余额充值',
								cancelTitle: '取消',
								buttons: this.recharge_list
						}, (ret, err)=>{
								if( ret.buttonIndex <= this.recharge_list.length ){
										 // 充值金额
										 let money = this.recharge_list[ret.buttonIndex-1];
										//  this.game.print(money,1);
										// 发送支付宝充值请求
										this.recharge_app_pay(money)
								}
					});
				},

				// 发送支付宝充值请求
				recharge_app_pay(money){
					// 获取支付宝支付对象
					let aliPayPlus = api.require("aliPayPlus");
					let self = this;
					// 向服务端发送请求获取终止订单信息
					self.game.check_user_login(self, ()=>{
						let token = this.game.getdata("access_token") || this.game.getfs("access_token");
						self.game.post(self,{
							'method': 'Users.recharge',
							'params':{
								'money': money,
							},
							'header': {
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 本次充值的订单参数
									let order_string = data.result.order_string;

									// 支付完成以后,支付APP返回的响应状态码
									let resultCode = {
										"9000": "支付成功!",
										"8000": "支付正在处理中,请稍候!",
										"4000": "支付失败!请联系我们的工作人员~",
										"5000": "支付失败,重复的支付操作",
										"6002": "网络连接出错",
										"6004": "支付正在处理中,请稍后",
									}
									// 唤醒支付宝APP,发起支付
									aliPayPlus.payOrder({
										orderInfo: order_string,
										sandbox: data.result.sandbox, // 将来APP上线需要修改成false
									},(ret, err)=>{
										if(resultCode[ret.code]){
											// 提示支付结果
											if(ret.code != 9000){
												self.game.tips(resultCode[ret.code]);
											}else {
												// 支付成功,向服务端请求验证支付结果 - 参数订单号
												self.check_recharge_result(data.result.order_number);
											}
										}
									})
								}
							}
						})
					})
				},

				// 向服务端发送请求,校验充值是否成功
				check_recharge_result(order_number){
					let self = this;
					self.game.check_user_login(self, ()=>{
						let token = this.game.getdata("access_token") || this.game.getfs("access_token");
						self.game.post(self,{
							'method': 'Users.check_recharge_result',
							'params':{
								'order_number':order_number,
							},
							'header':{
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 充值成功
									self.game.tips('充值成功!')
									// 用户数据更改过,重新刷新token值
									let token = self.game.getfs("access_token");
									// 删除token值
									self.game.deldata(["access_token","refresh_token"]);
									self.game.delfs(["access_token","refresh_token"]);
									if(token){
                    // 记住密码的情况
                    self.game.setfs({
                      "access_token": data.result.access_token,
                      "refresh_token": data.result.refresh_token,
                    });
                  }else{
                    // 不记住密码的情况
                    self.game.setdata({
                      "access_token": data.result.access_token,
                      "refresh_token": data.result.refresh_token,
                    });
									}
									// 全局广播充值成功
									self.game.sendEvent('recharge_success')
									// 全局广播刷新token值
									self.game.sendEvent('update_token')
								}
							}
						})
					})
				},

        back(){
          this.game.openWin("root");
        },
			}
		});
	}
	</script>
</body>
</html>


代码优化,上面服务端接口中,调用alipaySDK的时候,出现大量重复代码,将来如果继续调用SQK,我们最好现在对AlipaySDK进行封装处理。utils.payments,代码:

<!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/socket.io.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 orchard" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="orchard-bg">
			<img src="../static/images/bg2.png">
			<img class="board_bg2" src="../static/images/board_bg2.png">
		</div>
    <img class="back" @click="back" src="../static/images/user_back.png" alt="">
    <div class="header">
			<div class="info">
				<div class="avatar" @click='to_user'>
					<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="62" :rounded="true"></v-avatar>
						<v-avatar v-else-if="user_data.nickname" :username="user_data.nickname" :size="62" :rounded="true"></v-avatar>
						<v-avatar v-else :username="user_data.id" :size="62" :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" @click='user_recharge'>
				<div class="balance">
					<p class="title"><img src="../static/images/money.png" alt="">钱包</p>
					<p class="num">{{user_data.money}}</p>
				</div>
				<div class="balance">
					<p class="title"><img src="../static/images/integral.png" alt="">果子</p>
					<p class="num">{{user_data.credit}}</p>
				</div>
			</div>
      <div class="menu-list">
        <div class="menu">
          <img src="../static/images/menu1.png" alt="">
          排行榜
        </div>
        <div class="menu">
          <img src="../static/images/menu2.png" alt="">
          签到有礼
        </div>
        <div class="menu">
          <img src="../static/images/menu3.png" alt="">
          道具商城
        </div>
        <div class="menu">
          <img src="../static/images/menu4.png" alt="">
          邮件中心
        </div>
      </div>
		</div>
		<div class="footer" >
      <ul class="menu-list">
        <li class="menu">新手</li>
        <li class="menu">背包</li>
        <li class="menu-center" @click="to_shop">商店</li>
        <li class="menu">消息</li>
        <li class="menu">好友</li>
      </ul>
    </div>
	</div>
	<script>
	apiready = function(){
		Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
		new Vue({
			el:"#app",
			data(){
				return {
					user_data:{},  // 当前用户信息
          music_play:true,
          namespace: '/mofang', // websocket命名空间
          socket: null,
					recharge_list: [], // 允许充值的金额列表
				}
			},
      created(){
				// socket建立连接
				this.socket_connect();
        // 自动加载我的果园页面
				this.to_my_orchard();
				// 获取用户数据
				this.get_user_data()
				// 监听事件变化
				this.listen()
				// 获取充值金额列表
				this.get_recharge_list()
      },
			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(){
					let self = this;
					// 检查token是否过期,过期从新刷新token
					self.game.check_user_login(self, ()=>{
						// 获取token
						let token = this.game.getfs('access_token') || this.game.getdata('access_token')
						// 根据token获取用户数据
						this.user_data = this.game.get_user_by_token(token)
					})
				},

				// socket连接
        socket_connect(){
          this.socket = io.connect(this.game.config.SOCKET_SERVER + this.namespace, {transports: ['websocket']});
          this.socket.on('connect', ()=>{
              this.game.print("开始连接服务端");
          });
        },

        // 跳转我的果园页面
        to_my_orchard(){
          this.game.check_user_login(this, ()=>{
            this.game.openFrame("my_orchard","my_orchard.html","from_right",{
    					marginTop: 174,     //相对父页面上外边距的距离,数字类型
    					marginLeft: 0,      //相对父页面左外边距的距离,数字类型
    			    marginBottom: 54,     //相对父页面下外边距的距离,数字类型
    			    marginRight: 0     //相对父页面右外边距的距离,数字类型
    				});
          })
        },

        // 点击商店打开道具商城页面
				to_shop(){
          this.game.check_user_login(this, ()=>{
            this.game.openFrame('shop', 'shop.html', 'from_top');
          })
        },

				// 点击头像,跳转用户中心页面
				to_user(){
					this.game.check_user_login(this, ()=>{
            this.game.openFrame('user', 'user.html', 'from_right');
          })
				},

				// 获取充值金额列表
				get_recharge_list(){
					let self = this;
					self.game.check_user_login(self,()=>{
						self.game.post(self,{
							'method': 'Users.recharge_list',
							'params': {},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									self.recharge_list = data.result.recharge_list
								}
							}
						})
					})
				},

				// 点击钱包进行用户充值,设置充值金额
				user_recharge(){
					api.actionSheet({
								title: '余额充值',
								cancelTitle: '取消',
								buttons: this.recharge_list
						}, (ret, err)=>{
								if( ret.buttonIndex <= this.recharge_list.length ){
										 // 充值金额
										 let money = this.recharge_list[ret.buttonIndex-1];
										//  this.game.print(money,1);
										// 发送支付宝充值请求
										this.recharge_app_pay(money)
								}
					});
				},

				// 发送支付宝充值请求
				recharge_app_pay(money){
					// 获取支付宝支付对象
					let aliPayPlus = api.require("aliPayPlus");
					let self = this;
					// 向服务端发送请求获取终止订单信息
					self.game.check_user_login(self, ()=>{
						let token = this.game.getdata("access_token") || this.game.getfs("access_token");
						self.game.post(self,{
							'method': 'Users.recharge',
							'params':{
								'money': money,
							},
							'header': {
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 本次充值的订单参数
									let order_string = data.result.order_string;

									// 支付完成以后,支付APP返回的响应状态码
									let resultCode = {
										"9000": "支付成功!",
										"8000": "支付正在处理中,请稍候!",
										"4000": "支付失败!请联系我们的工作人员~",
										"5000": "支付失败,重复的支付操作",
										"6002": "网络连接出错",
										"6004": "支付正在处理中,请稍后",
									}
									// 唤醒支付宝APP,发起支付
									aliPayPlus.payOrder({
										orderInfo: order_string,
										sandbox: data.result.sandbox, // 将来APP上线需要修改成false
									},(ret, err)=>{
										if(resultCode[ret.code]){
											// 提示支付结果
											if(ret.code != 9000){
												self.game.tips(resultCode[ret.code]);
											}else {
												// 支付成功,向服务端请求验证支付结果 - 参数订单号
												self.check_recharge_result(data.result.order_number);
											}
										}
									})
								}
							}
						})
					})
				},

				// 向服务端发送请求,校验充值是否成功
				check_recharge_result(order_number){
					let self = this;
					self.game.check_user_login(self, ()=>{
						let token = this.game.getdata("access_token") || this.game.getfs("access_token");
						self.game.post(self,{
							'method': 'Users.check_recharge_result',
							'params':{
								'order_number':order_number,
							},
							'header':{
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 充值成功
									self.game.tips('充值成功!')
									// 用户数据更改过,重新刷新token值
									let token = self.game.getfs("access_token");
									// 删除token值
									self.game.deldata(["access_token","refresh_token"]);
									self.game.delfs(["access_token","refresh_token"]);
									if(token){
                    // 记住密码的情况
                    self.game.setfs({
                      "access_token": data.result.access_token,
                      "refresh_token": data.result.refresh_token,
                    });
                  }else{
                    // 不记住密码的情况
                    self.game.setdata({
                      "access_token": data.result.access_token,
                      "refresh_token": data.result.refresh_token,
                    });
									}
									// 全局广播充值成功
									self.game.sendEvent('recharge_success')
									// 全局广播刷新token值
									self.game.sendEvent('update_token')
								}
							}
						})
					})
				},

        back(){
          this.game.openWin("root");
        },
			}
		});
	}
	</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" @click='user_recharge'>
					<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" @click='to_invite_friend'>
				<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" @click='to_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">未认证</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 {
          user_data:{}, // 当前用户数据
					recharge_list:[], // 用户充值金额列表
				}
			},
      // 页面加载之前获取用户数据
      created(){
				// 获取用户数据
				this.get_user_data()
				// 监听事件变化
				this.listen()
				// 获取充值金额列表
				this.get_recharge_list()
      },

			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(){
					let self = this;
					self.game.check_user_login(self, ()=>{
						// 获取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')
        },

				// 点击好友列表,跳转带好友列表页面
				to_friend_list(){
					this.game.openFrame('friends', 'friends.html')
					this.game.openFrame('friend_list', 'friend_list.html', null, {
						x: 0,             // 左上角x轴坐标
						y: 194,           // 左上角y轴坐标
						w: 'auto',        // 当前帧页面的宽度, auto表示满屏
						h: 'auto'         // 当前帧页面的高度, auto表示满屏
					})
				},

				// 点击邀请好友,跳转到邀请好友页面
				to_invite_friend(){
					this.game.openFrame('invite', 'invite.html', 'from_top')
				},

				// 获取充值金额列表
				get_recharge_list(){
					let self = this;
					self.game.check_user_login(self,()=>{
						self.game.post(self,{
							'method': 'Users.recharge_list',
							'params': {},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									self.recharge_list = data.result.recharge_list
								}
							}
						})
					})
				},

				// 点击钱包进行用户充值,设置充值金额
				user_recharge(){
					api.actionSheet({
								title: '余额充值',
								cancelTitle: '取消',
								buttons: this.recharge_list
						}, (ret, err)=>{
								if( ret.buttonIndex <= this.recharge_list.length ){
										 // 充值金额
										 let money = this.recharge_list[ret.buttonIndex-1];
										//  this.game.print(money,1);
										// 发送支付宝充值请求
										this.recharge_app_pay(money)
								}
					});
				},

				// 发送支付宝充值请求
				recharge_app_pay(money){
					// 获取支付宝支付对象
					let aliPayPlus = api.require("aliPayPlus");
					let self = this;
					// 向服务端发送请求获取终止订单信息
					self.game.check_user_login(self, ()=>{
						let token = this.game.getdata("access_token") || this.game.getfs("access_token");
						self.game.post(self,{
							'method': 'Users.recharge',
							'params':{
								'money': money,
							},
							'header': {
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 本次充值的订单参数
									let order_string = data.result.order_string;

									// 支付完成以后,支付APP返回的响应状态码
									let resultCode = {
										"9000": "支付成功!",
										"8000": "支付正在处理中,请稍候!",
										"4000": "支付失败!请联系我们的工作人员~",
										"5000": "支付失败,重复的支付操作",
										"6002": "网络连接出错",
										"6004": "支付正在处理中,请稍后",
									}
									// 唤醒支付宝APP,发起支付
									aliPayPlus.payOrder({
										orderInfo: order_string,
										sandbox: data.result.sandbox, // 将来APP上线需要修改成false
									},(ret, err)=>{
										if(resultCode[ret.code]){
											// 提示支付结果
											if(ret.code != 9000){
												self.game.tips(resultCode[ret.code]);
											}else {
												// 支付成功,向服务端请求验证支付结果 - 参数订单号
												self.check_recharge_result(data.result.order_number);
											}
										}
									})
								}
							}
						})
					})
				},

				// 向服务端发送请求,校验充值是否成功
				check_recharge_result(order_number){
					let self = this;
					self.game.check_user_login(self, ()=>{
						let token = this.game.getdata("access_token") || this.game.getfs("access_token");
						self.game.post(self,{
							'method': 'Users.check_recharge_result',
							'params':{
								'order_number':order_number,
							},
							'header':{
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 充值成功
									self.game.tips('充值成功!')
									// 用户数据更改过,重新刷新token值
									let token = self.game.getfs("access_token");
									// 删除token值
									self.game.deldata(["access_token","refresh_token"]);
									self.game.delfs(["access_token","refresh_token"]);
									if(token){
										// 记住密码的情况
										self.game.setfs({
											"access_token": data.result.access_token,
											"refresh_token": data.result.refresh_token,
										});
									}else{
										// 不记住密码的情况
										self.game.setdata({
											"access_token": data.result.access_token,
											"refresh_token": data.result.refresh_token,
										});
									}
									// 全局广播充值成功
									self.game.sendEvent('recharge_success')
									// 全局广播刷新token值
									self.game.sendEvent('update_token')
								}
							}
						})
					})
				},

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


背包管理

客户端展示背包页面

  1. 种植园页面, 点击背包, 跳转到我的背包页面 html/orchard.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/socket.io.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 orchard" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="orchard-bg">
			<img src="../static/images/bg2.png">
			<img class="board_bg2" src="../static/images/board_bg2.png">
		</div>
    <img class="back" @click="back" src="../static/images/user_back.png" alt="">
    <div class="header">
			<div class="info">
				<div class="avatar" @click='to_user'>
					<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="62" :rounded="true"></v-avatar>
						<v-avatar v-else-if="user_data.nickname" :username="user_data.nickname" :size="62" :rounded="true"></v-avatar>
						<v-avatar v-else :username="user_data.id" :size="62" :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" @click='user_recharge'>
				<div class="balance">
					<p class="title"><img src="../static/images/money.png" alt="">钱包</p>
					<p class="num">{{game.number_format(user_data.money)}}</p>
				</div>
				<div class="balance">
					<p class="title"><img src="../static/images/integral.png" alt="">果子</p>
					<p class="num">{{game.number_format(user_data.credit)}}</p>
				</div>
			</div>
      <div class="menu-list">
        <div class="menu">
          <img src="../static/images/menu1.png" alt="">
          排行榜
        </div>
        <div class="menu">
          <img src="../static/images/menu2.png" alt="">
          签到有礼
        </div>
        <div class="menu">
          <img src="../static/images/menu3.png" alt="">
          道具商城
        </div>
        <div class="menu">
          <img src="../static/images/menu4.png" alt="">
          邮件中心
        </div>
      </div>
		</div>
		<div class="footer" >
      <ul class="menu-list">
        <li class="menu">新手</li>
        <li class="menu" @click='to_package'>背包</li>
        <li class="menu-center" @click="to_shop">商店</li>
        <li class="menu">消息</li>
        <li class="menu">好友</li>
      </ul>
    </div>
	</div>
	<script>
	apiready = function(){
		Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
		new Vue({
			el:"#app",
			data(){
				return {
					user_data:{},  // 当前用户信息
          music_play:true,
          namespace: '/mofang', // websocket命名空间
          socket: null,
					recharge_list: [], // 允许充值的金额列表
				}
			},
      created(){
				// socket建立连接
				this.socket_connect();
        // 自动加载我的果园页面
				this.to_my_orchard();
				// 获取用户数据
				this.get_user_data()
				// 监听事件变化
				this.listen()
				// 获取充值金额列表
				this.get_recharge_list()
      },
			methods:{
				back(){
					this.game.openWin("root");
				},
				// 监听事件
				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(){
					let self = this;
					// 检查token是否过期,过期从新刷新token
					self.game.check_user_login(self, ()=>{
						// 获取token
						let token = this.game.getfs('access_token') || this.game.getdata('access_token')
						// 根据token获取用户数据
						this.user_data = this.game.get_user_by_token(token)
					})
				},

				// socket连接
        socket_connect(){
          this.socket = io.connect(this.game.config.SOCKET_SERVER + this.namespace, {transports: ['websocket']});
          this.socket.on('connect', ()=>{
              this.game.print("开始连接服务端");
          });
        },

        // 跳转我的果园页面
        to_my_orchard(){
          this.game.check_user_login(this, ()=>{
            this.game.openFrame("my_orchard","my_orchard.html","from_right",{
    					marginTop: 174,     //相对父页面上外边距的距离,数字类型
    					marginLeft: 0,      //相对父页面左外边距的距离,数字类型
    			    marginBottom: 54,     //相对父页面下外边距的距离,数字类型
    			    marginRight: 0     //相对父页面右外边距的距离,数字类型
    				});
          })
        },

        // 点击商店打开道具商城页面
				to_shop(){
          this.game.check_user_login(this, ()=>{
            this.game.openFrame('shop', 'shop.html', 'from_top');
          })
        },

				// 点击头像,跳转用户中心页面
				to_user(){
					this.game.check_user_login(this, ()=>{
            this.game.openFrame('user', 'user.html', 'from_right');
          })
				},

				// 获取充值金额列表
				get_recharge_list(){
					let self = this;
					self.game.check_user_login(self,()=>{
						self.game.post(self,{
							'method': 'Users.recharge_list',
							'params': {},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									self.recharge_list = data.result.recharge_list
								}
							}
						})
					})
				},

				// 点击钱包进行用户充值,设置充值金额
				user_recharge(){
					api.actionSheet({
								title: '余额充值',
								cancelTitle: '取消',
								buttons: this.recharge_list
						}, (ret, err)=>{
								if( ret.buttonIndex <= this.recharge_list.length ){
										 // 充值金额
										 let money = this.recharge_list[ret.buttonIndex-1];
										//  this.game.print(money,1);
										// 发送支付宝充值请求
										this.recharge_app_pay(money)
								}
					});
				},

				// 发送支付宝充值请求
				recharge_app_pay(money){
					// 获取支付宝支付对象
					let aliPayPlus = api.require("aliPayPlus");
					let self = this;
					// 向服务端发送请求获取终止订单信息
					self.game.check_user_login(self, ()=>{
						let token = this.game.getdata("access_token") || this.game.getfs("access_token");
						self.game.post(self,{
							'method': 'Users.recharge',
							'params':{
								'money': money,
							},
							'header': {
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 本次充值的订单参数
									let order_string = data.result.order_string;

									// 支付完成以后,支付APP返回的响应状态码
									let resultCode = {
										"9000": "支付成功!",
										"8000": "支付正在处理中,请稍候!",
										"4000": "支付失败!请联系我们的工作人员~",
										"5000": "支付失败,重复的支付操作",
										"6002": "网络连接出错",
										"6004": "支付正在处理中,请稍后",
									}
									// 唤醒支付宝APP,发起支付
									aliPayPlus.payOrder({
										orderInfo: order_string,
										sandbox: data.result.sandbox, // 将来APP上线需要修改成false
									},(ret, err)=>{
										if(resultCode[ret.code]){
											// 提示支付结果
											if(ret.code != 9000){
												self.game.tips(resultCode[ret.code]);
											}else {
												// 支付成功,向服务端请求验证支付结果 - 参数订单号
												self.check_recharge_result(data.result.order_number);
											}
										}
									})
								}
							}
						})
					})
				},

				// 向服务端发送请求,校验充值是否成功
				check_recharge_result(order_number){
					let self = this;
					self.game.check_user_login(self, ()=>{
						let token = this.game.getdata("access_token") || this.game.getfs("access_token");
						self.game.post(self,{
							'method': 'Users.check_recharge_result',
							'params':{
								'order_number':order_number,
							},
							'header':{
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 充值成功
									self.game.tips('充值成功!')
									// 用户数据更改过,重新刷新token值
									let token = self.game.getfs("access_token");
									// 删除token值
									self.game.deldata(["access_token","refresh_token"]);
									self.game.delfs(["access_token","refresh_token"]);
									if(token){
                    // 记住密码的情况
                    self.game.setfs({
                      "access_token": data.result.access_token,
                      "refresh_token": data.result.refresh_token,
                    });
                  }else{
                    // 不记住密码的情况
                    self.game.setdata({
                      "access_token": data.result.access_token,
                      "refresh_token": data.result.refresh_token,
                    });
									}
									// 全局广播充值成功
									self.game.sendEvent('recharge_success')
									// 全局广播刷新token值
									self.game.sendEvent('update_token')
								}
							}
						})
					})
				},

				// 点击背包,跳转到背包页面
				to_package(){
					this.game.check_user_login(this, ()=>{
						this.game.openFrame('package', 'package.html', 'from_top')
					})
				},


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


  1. 我的背包页面 html/package.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 add_friend package" id="app">
    <div class="box">
      <p class="title">我的背包</p>
      <img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
      <div class="prop_list">
        <div class="item">
          <img src="../static/images/fruit_tree.png" alt="">
          <span>10</span>
        </div>
        <div class="item">
          <img src="../static/images/prop1.png" alt="">
          <span>10</span>
        </div>
        <div class="item"></div>
        <div class="item"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
        <div class="item lock"></div>
      </div>
    </div>
	</div>
	<script>
	apiready = function(){
    	Vue.prototype.game = new Game("../static/mp3/bg1.mp3");	
		new Vue({
			el:"#app",
			data(){
				return {
					user_id: "", // 当前登陆用户Id
					prev:{name:"",url:"",params:{}},
					current:{name:"package",url:"package.html",params:{}},
				}
			},

			created(){

			},
			methods:{
        back(){
          this.game.closeFrame();
        },
			}
		});
	}
	</script>
</body>
</html>

  1. 我的背包css样式, static/css/main.css,代码:
.package .prop_list{
  position: absolute;
  left: 4.2rem;
  top: 10rem;
  width: 21rem;
  height: 39.8rem;
  overflow: scroll;
}

.package .prop_list .item{
  background: #a63600;
  border: 3px solid #ff9900;
  width: 4rem;
  height: 4rem;
  margin: .2rem;
  float: left;
  border-radius: 5px;
  position: relative;
}

.package .prop_list .item img{
  position: absolute;
  margin: auto;
  width: 3rem;
  height: 3rem;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}
.package .prop_list .item span{
  font-size: 1.5rem;
  color: #fff;
  position: absolute;
  bottom: 0;
  right: 0;
}
.package .prop_list .lock:after{
  display: block;
  content:"x";
  font-size: 4rem;
  color: #ff9900;
  text-align: center;
  line-height: 100%;
  height: 4rem;
  width: 4rem;
}

背包道具配置信息存储方案

在背包中显示道具,会涉及到用户背包的道具显示以及容量解锁问题,所以我们需要在服务端准备一个参数配置, 用于保存种植园中用户的背包的相关参数,例如:

格子的初始化数量, 每次解锁背包格子的价格等等.

参数信息的保存与之前项目配置的信息有所不同, 不同的地方在于, 参数信息仅仅是种植园的背包参数,会在项目运营的时候允许有所改动,而项目配置的变量参数则在项目上线以后基本不做改动.最好把背包相关整个单独抽离出来,方便将来管理员进行改动。

那么,这些参数的存储方式,有以下几种:

  1. 有个单独的python默认配置文件,保存配置默认值,然后在mongo或者mysql里面也保存一份,将来如果改动了mysql/mongo中的配置信息,则项目运行时优先读取mysql/mongo的。

  2. 采用mongo/mysql进行配置保存,然后项目运行过程中,缓存到redis中。实际项目运行时,优先读取redis的。redis没有了,则读取mongo/mysql里面的,当然,mongo/mysql里面的数据被修改了, 重建缓存。[一般配置信息不会经常改动]

  3. 可以直接保存到一个json文件中,将来在服务端后台展示配置信息提供给管理员修改时,直接读取json的内容转换成字典结构,以表单的格式展示给后台管理员,在管理员做出修改后,把字典转成json格式写入到同一个json文件中。项目运行时,直接读取json中的配置内容。

当然,不管是采用哪一种方案,其实都要明确配置信息的配置项。

image-20201231085639255

这里,我们采用第一种配置存储方案。配置文件保存默认值,采用mongo保存最新的配置信息。

  1. 配置文件 settings.plant,代码:
# 初始化时,默认背包存储上限
INIT_PACKAGE_CAPACITY = 8

# 用户能使用的背包容量上限
MAX_PACKAGE_CAPACITY = 32

# 每次解锁背包容量时需要的积分数量
UNLOCK_PACKAGE = [ 10, 50, 100, 200, 500, 1000, 5000, 10000, 50000, 100000 ]

# 每次激活容器新增的格子数量,激活1次解锁4个格子
UNLOCK_PACKAGE_ITEM = 4

  1. MongoDB在获取背包配置时,检测项目的相关配置是否完成初始化操作。

application.utils.mongoutils,代码:

from flask_pymongo import pymongo

from application.settings import plant as config

# 项目开始时,MongoDB初始化操作
def mongo_init(app, mongo):
    '''项目开始时,MongoDB初始化操作'''
    # 初始化索引
    mongo_init_index(app, mongo)

    # 初始化配置
    mongo_init_config(app, mongo)

# 初始化索引
def mongo_init_index(app, mongo):
    try:
        # 充值订单的唯一索引
        mongo.db.user_recharge_log.create_index("out_trade_number", unique=True)
    except Exception:
        pass

    try:
        # 系统配置的唯一索引
        mongo.db.game_config.create_index([("config_name", pymongo.ASCENDING), ], unique=True)
    except Exception:
        pass

# 初始化配置
def mongo_init_config(app, mongo):
    # 背包配置信息初始化
    package_config_init(app, mongo)

# 背包配置信息初始化
def package_config_init(app, mongo):
    query = {"config_name": "package_settings"}
    # 先判断是否有背包配置信息
    package_settings = mongo.db.game_config.find_one(query)
    # 如果没有背包配置,则从配置文件中同步到mongoDB中
    if not package_settings:
        document = {
            "config_name": "package_settings",
            "init_package_capacity": config.INIT_PACKAGE_CAPACITY,  # # 用户默认初始化下的背包容量
            "max_package_capacity": config.MAX_PACKAGE_CAPACITY,  # 用户能使用的背包容量上限
            "unlock_package": config.UNLOCK_PACKAGE,  # 每次解锁背包容量时需要的积分数量
            "unlock_package_item": config.UNLOCK_PACKAGE_ITEM,  # 每次激活容器新增的格子数量,激活1次解锁4个格子
        }
        mongo.db.game_config.insert_one(document)

  1. 项目入口文件, 对MongoDB进行初始化加载配置 application.__init__,代码:
import eventlet
import os

# 使用eventlet提供的猴子补丁,把程序中所有基于同步IO操作的模块全部转换成协程异步。
eventlet.monkey_patch()

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_jsonrpc import JSONRPC
from flask_marshmallow import Marshmallow
from faker import Faker
from celery import Celery
from flask_jwt_extended import JWTManager
from flask_admin import Admin
from flask_babelex import Babel
from xpinyin import Pinyin
from flask_qrcode import QRcode
from flask_socketio import SocketIO
from flask_cors import CORS
from flask_pymongo import PyMongo

from application.utils.config import init_config
from application.utils.logger import Log
from application.utils.commands import load_commands
from application.utils.bluerprint import register_blueprint, path, include, api_rpc
from application.utils import message, code
from application.utils.unittest import BasicTestCase
from application.utils.OssStore import OssStore
from application.utils.mongoutils import mongo_init

# 终端脚本工具初始化
manager = Manager()

# SQLAlchemy初始化
db = SQLAlchemy()

# redis数据库初始化
# - 1.默认缓存数据库对象,配置前缀为REDIS
redis_cache = FlaskRedis(config_prefix='REDIS')
# - 2.验证相关数据库对象,配置前缀为CHECK
redis_check = FlaskRedis(config_prefix='CHECK')
# - 3.验证相关数据库对象,配置前缀为SESSION
redis_session = FlaskRedis(config_prefix='SESSION')
# - 4.种植园商品缓存库,配置前缀为ORCHARD
redis_orchard = FlaskRedis(config_prefix='ORCHARD')

# session储存配置初始化
session_store = Session()

# 自定义日志初始化
logger = Log()

# 初始化jsonrpc模块
jsonrpc = JSONRPC()

# marshmallow构造器初始化
marshmallow = Marshmallow()

# 初始化随机生成数据模块faker
faker = Faker(locale='zh-CN') # 指定中文

# 初始化异步celery
celery = Celery()

# jwt认证模块初始化
jwt = JWTManager()

# 阿里云对象存储oss初始化
oss = OssStore()

# admin后台站点初始化
admin = Admin()

# 国际化和本地化的初始化
babel = Babel()

# 文字转拼音初始化
pinyin = Pinyin()

# 二维码生成模块初始化
qrcode = QRcode()

# 初始化socketio通信模块
socketio = SocketIO()

# 解决跨域模块初始化
cors = CORS()

# 实例化PyMongo
mongo = PyMongo()

# 全局初始化
def init_app(config_path):
    """全局初始化 - 需要传入加载开发或生产环境配置路径"""
    # 创建app应用对象
    app = Flask(__name__)

    # 当前项目根目录
    app.BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

    # 开发或生产环境加载配置
    init_config(app, config_path)

    # SQLAlchemy加载配置
    db.init_app(app)

    # PyMongo加载配置
    mongo.init_app(app)
    # MongoDB初始化加载配置
    mongo_init(app, mongo)

    # redis加载配置
    redis_cache.init_app(app)
    redis_check.init_app(app)
    redis_session.init_app(app)
    redis_orchard.init_app(app)

    """一定先加载默认配置,再传入APP加载session对象"""
    # session保存数据到redis时启用的链接对象
    app.config["SESSION_REDIS"] = redis_session
    # session存储对象加载配置
    session_store.init_app(app)

    # 为日志对象加载配置
    log = logger.init_app(app)
    app.log = log

    # json-rpc加载配置
    jsonrpc.init_app(app)
    # rpc访问路径入口(只有唯一一个访问路径入口),默认/api
    jsonrpc.service_url = app.config.get('JSON_SERVER_URL', '/api')
    jsonrpc.enable_web_browsable_api = app.config.get("ENABLE_WEB_BROWSABLE_API",False)
    app.jsonrpc = jsonrpc

    # marshmallow构造器加载配置
    marshmallow.init_app(app)

    # 自动注册蓝图
    register_blueprint(app)

    # 加载celery配置
    celery.main = app.name
    celery.app = app
    # 更新配置
    celery.conf.update(app.config)
    # 自动注册任务
    celery.autodiscover_tasks(app.config.get('INSTALL_BLUEPRINT'))

    # jwt认证加载配置
    jwt.init_app(app)

    # faker作为app成员属性
    app.faker = faker

    # 注册模型,创建表
    with app.app_context():
        db.create_all()

    # 阿里云存储对象加载配置
    oss.init_app(app)

    # admin后台站点加载配置
    admin.name = app.config.get('FLASK_ADMIN_NAME') # 站点名称
    admin.template_mode = app.config.get('FLASK_TEMPLATE_MODE') # 使用的模板
    admin.init_app(app)

    # 国际化和本地化加载配置
    babel.init_app(app)

    # 二维码模块加载配置
    qrcode.init_app(app)

    # 跨域模块cors加载配置
    cors.init_app(app)

    # socketid加载配置,取代http通信,实现异步操作
    socketio.init_app(
        app,
        message_queue=app.config["MESSAGE_QUEUE"],  # 消息中间件 - Redis队列
        cors_allowed_origins=app.config["CORS_ALLOWED_ORIGINS"], # 跨域设置
        async_mode=app.config["ASYNC_MODE"], # 异步模式
        debug=app.config["DEBUG"]
    )
    # socketio作为app成员,方便调用
    app.socketio = socketio

    # 终端脚本工具加载配置
    manager.app = app

    # 自动注册自定义命令
    load_commands(manager)

    return manager

统一管理和注册websocket接口

类似之前的rpc接口和url视图一样,我们可以把项目中所有的websocket接口的命名空间名称和命名空间类进行类似路由一样的映射管理。

  1. application.utils.blueprint,提供websocket接口路由映射的方法socketapi,以及注册socket视图类(路由初始化工作),代码:
from flask import Blueprint
# 引入导包函数
from importlib import import_module


# 自动注册蓝图
def register_blueprint(app):
    """
    自动注册蓝图
    :param app: 当前flask的app实例对象
    :return:
    """

    # 从配置文件中读取总路由路径信息
    app_urls_path = app.config.get('URL_PATH')
    # 导包,导入总路由模块
    app_urls_module = import_module(app_urls_path)
    # 获取总路由列表
    app_urlpatterns = app_urls_module.urlpatterns

    # 从配置文件中读取需要注册到项目中的蓝图路径信息
    blueprint_path_list = app.config.get('INSTALL_BLUEPRINT')
    # 遍历蓝图路径列表,对每一个蓝图进行初始化
    for blueprint_path in blueprint_path_list:
        # 获取蓝图路径中最后一段的包名作为蓝图的名称
        blueprint_name = blueprint_path.split('.')[-1]
        # 创建蓝图对象
        blueprint = Blueprint(blueprint_name, blueprint_path)

        # 蓝图路由的前缀
        url_prefix = ""
        # 蓝图下的子路由列表
        urlpatterns = []

        # 获取蓝图的父级目录, 即application/apps
        blueprint_father_path =  ".".join( blueprint_path.split(".")[:-1] )
        # 循环总路由列表
        for item in app_urlpatterns:
            # 判断当前蓝图是否在总路由列表中
            if blueprint_name in item["blueprint_url_subfix"]:
                # 导入蓝图下的子路由模块
                urls_module = import_module(blueprint_father_path + "." + item["blueprint_url_subfix"])
                # 1.获取urls模块下蓝图路由列表urlpatterns
                urlpatterns = [] # 防止下边调用循环报错
                try:
                    urlpatterns = urls_module.urlpatterns
                except Exception:
                    pass
                # 提取蓝图路由的前缀
                url_prefix = item["url_prefix"]
                # 2.获取urls模块下rpc接口列表apipatterns
                apipatterns = [] # 防止下边调用循环报错
                try:
                    apipatterns = urls_module.apipatterns
                except Exception:
                    pass
                # 3.获取urls模块下socket接口列表socketpatterns
                socketpatterns = []  # 防止下边调用循环报错
                try:
                    socketpatterns = urls_module.socketpatterns
                except Exception:
                    pass
                # 从总路由中查到当前蓝图对象的前缀就不要继续往下遍历了
                break

        # 注册蓝图路由
        for url in urlpatterns:
            blueprint.add_url_rule(**url)

        # 注册rpc接口路由
        api_prefix_name = ''
        # 判断是否开启补充api前缀
        if app.config.get('JSON_PREFIX_NAME', False) == True:
            api_prefix_name = blueprint_name.title() + '.'
        # 循环rpc接口列表,进行注册
        for api in apipatterns:
            app.jsonrpc.site.register(api_prefix_name + api['name'], api['method'])

        # 注册socket路由
        try:
            from application import socketio
            for socket in socketpatterns:
                # 获取类名
                namespace_handler = socket['namespace_handler']
                socket.pop('namespace_handler')
                # 注册视图类
                socketio.on_namespace(namespace_handler(**socket))
        except:
            pass

        try:
            # 导入蓝图模型
            import_module(blueprint_path + '.models')
        except ModuleNotFoundError:
            pass

        try:
            # 导入蓝图下的admin站点配置
            import_module(blueprint_path + '.admin')
        except ModuleNotFoundError:
            pass

        try:
            # 导入蓝图下的socket文件接口
            import_module(blueprint_path + '.socket')
        except ModuleNotFoundError:
            pass

        # 把蓝图对象注册到app实例对象,并添加路由前缀
        app.register_blueprint(blueprint, url_prefix=url_prefix)

    # 后台站点首页相关初始化[这段代码在循环蓝图完成以后,在循环体之外]
    from flask_admin import AdminIndexView  # admin主视图
    from application import admin
    admin._set_admin_index_view(index_view=AdminIndexView(
        name = app.config.get('ADMIN_HOME_NAME'), # 默认首页名称
        template= app.config.get('ADMIN_HOME_TEMPLATE') # 默认首页模板路径
    ))

# 把url地址和视图方法映射关系处理成字典
def path(rule, view_func, **kwargs):
    """绑定url地址和视图的映射关系"""
    data = {
        'rule':rule,
        'view_func':view_func,
        **kwargs
    }
    return data

# 把rpc方法名和视图的映射关系处理成字典
def api_rpc(name, method, **kwargs):
    """
    :param name: rpc方法名
    :param method: 视图名称
    :param kwargs: 其他参数。。
    :return:
    """
    data = {
        'name':name,
        'method':method,
        **kwargs
    }
    return data


# 路由前缀和蓝图进行绑定映射
def include(url_prefix, blueprint_url_subfix):
    """

    :param url_prefix: 路由前缀
    :param blueprint_url_subfix: 蓝图路由,
            格式:蓝图包名.路由模块名
            例如:蓝图目录是home, 路由模块名是urls,则参数:home.urls
    :return:
    """
    data = {
        "url_prefix": url_prefix,
        "blueprint_url_subfix": blueprint_url_subfix
    }
    return data

# websocketk接口命名空间类和命名空间名称进行关系映射
def socketapi(namespace,namespace_handler,**kwargs):
    """
    websocketk接口命名空间类和命名空间名称进行关系映射
    :param namespace: 命名空间访问名称
    :param namespace_handler: 命名空间处理类
    :param kwargs:
    :return:
    """
    return {"namespace":namespace,"namespace_handler":namespace_handler,**kwargs}


  1. 项目入口文件引入websocket的映射方法socketapi application/__init__.py
from application.utils.bluerprint import register_blueprint, path, include, api_rpc, socketapi


  1. 在每一个蓝图下的urls中引入当前蓝图下所有socket命名空间类和上面封装路由映射方法,进行统一注册.

apps.orchard.urls,代码:

from application import path, api_rpc, socketapi
# 引入当前蓝图应用视图, 引入rpc视图
from . import views, api, socket

# 蓝图路径与函数映射列表
urlpatterns = [
    # path('/index', views.index, methods=['post']),
]
# rpc方法与函数映射列表[rpc接口列表]
# user = api.User() # 实例化类视图
apipatterns = [
    api_rpc('goods_list', api.get_goods_list), # 获取商店道具列表
    api_rpc('pay_props', api.pay_props), # 购买商店道具
]

# socket注册视图类
socketpatterns = [
    socketapi('/orchard',socket.OrchardNamespace),
]

  1. 现在我们就可以在用户登录时,返回上面的背包配置信息了。

socket视图: orchard.socket,代码:

from flask_socketio import emit, Namespace
from flask import request

from . import services

"""基于类视图接口"""

# 种植园模块命名空间
class OrchardNamespace(Namespace):
    '''种植园模块命名空间'''
    # socket链接
    def on_connect(self):
        print("用户[%s]进入了种植园!" % request.sid)
        # 返回初始化信息[不涉及当前的用户,因为我们现在不知道当前用户是谁]
        self.init_config()

    def init_config(self, ):
        """系统基本信息初始化"""
        # 返回背包初始配置信息
        package_settings = services.get_package_settings()
        emit("init_config_response", package_settings)

    # socket断开连接
    def on_disconnect(self):
        print("用户[%s]退出了种植园!" % request.sid)

    # 接收登录信息
    def on_login(self, data):
        message = ""
        # 响应登录信息
        emit("login_response", message)

    # 房间分发
    def on_join_room(self, data):
        room_id = 0

  1. 数据服务层 orchard.services,代码:
# 获取背包初始配置信息
def get_package_settings():
    '''获取背包初始配置信息'''
    query = {'config_name':'package_settings'}
    package_settings = mongo.db.game_config.find_one(query)

    # 如果没有初始化信息,就差配置文件中拿
    data = {
        "init_package_capacity": package_settings.get("INIT_PACKAGE_CAPACITY", config.INIT_PACKAGE_CAPACITY),
        "max_package_capacity": package_settings.get("MAX_PACKAGE_CAPACITY", config.MAX_PACKAGE_CAPACITY),
        "unlock_package": package_settings.get("UNLOCK_PACKAGE", config.UNLOCK_PACKAGE),
        "unlock_package_item": package_settings.get("UNLOCK_PACKAGE_ITEM", config.UNLOCK_PACKAGE_ITEM),
    }

    return data


客户端获取背包的初始化基本信息

  1. 种植园页面获取背包初始化信息, 点击背包,传递配置信息参数 orchard.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/socket.io.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 orchard" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="orchard-bg">
			<img src="../static/images/bg2.png">
			<img class="board_bg2" src="../static/images/board_bg2.png">
		</div>
    <img class="back" @click="back" src="../static/images/user_back.png" alt="">
    <div class="header">
			<div class="info">
				<div class="avatar" @click='to_user'>
					<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="62" :rounded="true"></v-avatar>
						<v-avatar v-else-if="user_data.nickname" :username="user_data.nickname" :size="62" :rounded="true"></v-avatar>
						<v-avatar v-else :username="user_data.id" :size="62" :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" @click='user_recharge'>
				<div class="balance">
					<p class="title"><img src="../static/images/money.png" alt="">钱包</p>
					<p class="num">{{game.number_format(user_data.money)}}</p>
				</div>
				<div class="balance">
					<p class="title"><img src="../static/images/integral.png" alt="">果子</p>
					<p class="num">{{game.number_format(user_data.credit)}}</p>
				</div>
			</div>
      <div class="menu-list">
        <div class="menu">
          <img src="../static/images/menu1.png" alt="">
          排行榜
        </div>
        <div class="menu">
          <img src="../static/images/menu2.png" alt="">
          签到有礼
        </div>
        <div class="menu">
          <img src="../static/images/menu3.png" alt="">
          道具商城
        </div>
        <div class="menu">
          <img src="../static/images/menu4.png" alt="">
          邮件中心
        </div>
      </div>
		</div>
		<div class="footer" >
      <ul class="menu-list">
        <li class="menu">新手</li>
        <li class="menu" @click='to_package'>背包</li>
        <li class="menu-center" @click="to_shop">商店</li>
        <li class="menu">消息</li>
        <li class="menu">好友</li>
      </ul>
    </div>
	</div>
	<script>
	apiready = function(){
		Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
		new Vue({
			el:"#app",
			data(){
				return {
					user_data:{},  // 当前用户信息
          music_play:true,
          namespace: '/orchard', // websocket命名空间
          socket: null, // websocket连接对象
					recharge_list: [], // 允许充值的金额列表
					package_init_setting: {}, // 背包初始配置信息
				}
			},
      created(){
				// socket建立连接
				this.socket_connect();
        // 自动加载我的果园页面
				this.to_my_orchard();
				// 获取用户数据
				this.get_user_data()
				// 监听事件变化
				this.listen()
				// 获取充值金额列表
				this.get_recharge_list()
      },
			methods:{
				back(){
					this.game.openWin("root");
				},
				// 监听事件
				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(){
					let self = this;
					// 检查token是否过期,过期从新刷新token
					self.game.check_user_login(self, ()=>{
						// 获取token
						let token = this.game.getfs('access_token') || this.game.getdata('access_token')
						// 根据token获取用户数据
						this.user_data = this.game.get_user_by_token(token)
					})
				},

				// socket连接
        socket_connect(){
          this.socket = io.connect(this.game.config.SOCKET_SERVER + this.namespace, {transports: ['websocket']});
          this.socket.on('connect', ()=>{
              this.game.print("开始连接服务端");
							// 获取背包初始配置信息
							this.get_package_setting()
          });
        },

				// 获取背包初始配置信息
				get_package_setting(){
					this.socket.on('init_config_response', (response)=>{
						// this.game.print(response,1)
						this.package_init_setting = response
					})
				},

        // 跳转我的果园页面
        to_my_orchard(){
          this.game.check_user_login(this, ()=>{
            this.game.openFrame("my_orchard","my_orchard.html","from_right",{
    					marginTop: 174,     //相对父页面上外边距的距离,数字类型
    					marginLeft: 0,      //相对父页面左外边距的距离,数字类型
    			    marginBottom: 54,     //相对父页面下外边距的距离,数字类型
    			    marginRight: 0     //相对父页面右外边距的距离,数字类型
    				});
          })
        },

        // 点击商店打开道具商城页面
				to_shop(){
          this.game.check_user_login(this, ()=>{
            this.game.openFrame('shop', 'shop.html', 'from_top');
          })
        },

				// 点击头像,跳转用户中心页面
				to_user(){
					this.game.check_user_login(this, ()=>{
            this.game.openFrame('user', 'user.html', 'from_right');
          })
				},

				// 获取充值金额列表
				get_recharge_list(){
					let self = this;
					self.game.check_user_login(self,()=>{
						self.game.post(self,{
							'method': 'Users.recharge_list',
							'params': {},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									self.recharge_list = data.result.recharge_list
								}
							}
						})
					})
				},

				// 点击钱包进行用户充值,设置充值金额
				user_recharge(){
					api.actionSheet({
								title: '余额充值',
								cancelTitle: '取消',
								buttons: this.recharge_list
						}, (ret, err)=>{
								if( ret.buttonIndex <= this.recharge_list.length ){
										 // 充值金额
										 let money = this.recharge_list[ret.buttonIndex-1];
										//  this.game.print(money,1);
										// 发送支付宝充值请求
										this.recharge_app_pay(money)
								}
					});
				},

				// 发送支付宝充值请求
				recharge_app_pay(money){
					// 获取支付宝支付对象
					let aliPayPlus = api.require("aliPayPlus");
					let self = this;
					// 向服务端发送请求获取终止订单信息
					self.game.check_user_login(self, ()=>{
						let token = this.game.getdata("access_token") || this.game.getfs("access_token");
						self.game.post(self,{
							'method': 'Users.recharge',
							'params':{
								'money': money,
							},
							'header': {
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 本次充值的订单参数
									let order_string = data.result.order_string;

									// 支付完成以后,支付APP返回的响应状态码
									let resultCode = {
										"9000": "支付成功!",
										"8000": "支付正在处理中,请稍候!",
										"4000": "支付失败!请联系我们的工作人员~",
										"5000": "支付失败,重复的支付操作",
										"6002": "网络连接出错",
										"6004": "支付正在处理中,请稍后",
									}
									// 唤醒支付宝APP,发起支付
									aliPayPlus.payOrder({
										orderInfo: order_string,
										sandbox: data.result.sandbox, // 将来APP上线需要修改成false
									},(ret, err)=>{
										if(resultCode[ret.code]){
											// 提示支付结果
											if(ret.code != 9000){
												self.game.tips(resultCode[ret.code]);
											}else {
												// 支付成功,向服务端请求验证支付结果 - 参数订单号
												self.check_recharge_result(data.result.order_number);
											}
										}
									})
								}
							}
						})
					})
				},

				// 向服务端发送请求,校验充值是否成功
				check_recharge_result(order_number){
					let self = this;
					self.game.check_user_login(self, ()=>{
						let token = this.game.getdata("access_token") || this.game.getfs("access_token");
						self.game.post(self,{
							'method': 'Users.check_recharge_result',
							'params':{
								'order_number':order_number,
							},
							'header':{
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 充值成功
									self.game.tips('充值成功!')
									// 用户数据更改过,重新刷新token值
									let token = self.game.getfs("access_token");
									// 删除token值
									self.game.deldata(["access_token","refresh_token"]);
									self.game.delfs(["access_token","refresh_token"]);
									if(token){
                    // 记住密码的情况
                    self.game.setfs({
                      "access_token": data.result.access_token,
                      "refresh_token": data.result.refresh_token,
                    });
                  }else{
                    // 不记住密码的情况
                    self.game.setdata({
                      "access_token": data.result.access_token,
                      "refresh_token": data.result.refresh_token,
                    });
									}
									// 全局广播充值成功
									self.game.sendEvent('recharge_success')
									// 全局广播刷新token值
									self.game.sendEvent('update_token')
								}
							}
						})
					})
				},

				// 点击背包,跳转到背包页面,并传递背包初始配置信息
				to_package(){
					this.game.check_user_login(this, ()=>{
						this.game.openFrame('package', 'package.html', 'from_top',null, {
							'package_init_setting': this.package_init_setting
						})
					})
				},


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


  1. 用户打开背包时, 接受来自orchard.html的页面背包基本配置参数, 并调整背包列表。html/package.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 add_friend package" id="app">
    <div class="box">
      <p class="title">我的背包</p>
      <img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
			<!-- 背包道具列表 -->
		  <div class="prop_list">
        <!-- <div class="item">
          <img src="../static/images/fruit_tree.png" alt="">
          <span>10</span>
        </div>
        <div class="item">
          <img src="../static/images/prop1.png" alt="">
          <span>10</span>
        </div> -->
				<!-- 循环背包列表,调整样式 -->
        <div class="item" v-for='i in package_init_setting.max_package_capacity' :class="i > package_init_setting.init_package_capacity?'lock':''"></div>
      </div>
    </div>
	</div>
	<script>
	apiready = function(){
    	Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
		new Vue({
			el:"#app",
			data(){
				return {
					user_id: "", // 当前登陆用户Id
					package_init_setting: {}, // 背包初始配置信息
				}
			},

			created(){
				// 获取其他页面传递的参数信息
				this.get_page_params();
			},
			methods:{
        back(){
          this.game.closeFrame();
        },

				// 获取其他页面传递的参数信息
				get_page_params(){
					// 获取背包初始配置信息
					this.package_init_setting = api.pageParam.package_init_setting
				},


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


经过上面的处理,背包的信息就直接被socketio从服务端获取到,并展示到背包页面了。但是在处理数据从mongoDB数据库中提取出来这个过程,操作是比较零散的,所以我们可以让数据库操作部分的代码更强的健壮,更加具有可读性和维护性,我们可以使用基于DjangoORM作为原型开发出来的MongoDB的ORM框架:mongoengine框架,来操作mongoDB的数据。

MongoEngine

这里我们是基于flask框架操作mongoengine,所以有专门的Flask-MongoEngine模块让我们对接mongoengine的安装配置和使用。

Flask-MongoEngine:http://docs.mongoengine.org/projects/flask-mongoengine/en/latest/

MongoEngine:http://docs.mongoengine.org/apireference.html#misc

  1. 安装命令:
# 安装mongoengine
# pip install mongoengine

# 安装flask-mongoengine
pip install flask-mongoengine


  1. 开发环境配置 settings.dev
'''MongoDB的ORM框架-mongoengine配置'''
MONGODB_DB = 'mofang'       # 数据库名称
MONGODB_HOST = '127.0.0.1'  # 链接数据库IP地址
MONGODB_PORT = 27017        # 链接数据库端口号
MONGODB_USERNAME = 'mofang' # 数据库管理用户名
MONGODB_PASSWORD = '123' # 数据库管理密码

  1. 项目入口文件, 初始化mongoengine配置application.__init__,代码:
import eventlet
import os

# 使用eventlet提供的猴子补丁,把程序中所有基于同步IO操作的模块全部转换成协程异步。
eventlet.monkey_patch()

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_jsonrpc import JSONRPC
from flask_marshmallow import Marshmallow
from faker import Faker
from celery import Celery
from flask_jwt_extended import JWTManager
from flask_admin import Admin
from flask_babelex import Babel
from xpinyin import Pinyin
from flask_qrcode import QRcode
from flask_socketio import SocketIO
from flask_cors import CORS
from flask_pymongo import PyMongo
from flask_mongoengine import MongoEngine

from application.utils.config import init_config
from application.utils.logger import Log
from application.utils.commands import load_commands
from application.utils.bluerprint import register_blueprint, path, include, api_rpc, socketapi
from application.utils import message, code
from application.utils.unittest import BasicTestCase
from application.utils.OssStore import OssStore
from application.utils.mongoutils import mongo_init

# 终端脚本工具初始化
manager = Manager()

# SQLAlchemy初始化
db = SQLAlchemy()

# redis数据库初始化
# - 1.默认缓存数据库对象,配置前缀为REDIS
redis_cache = FlaskRedis(config_prefix='REDIS')
# - 2.验证相关数据库对象,配置前缀为CHECK
redis_check = FlaskRedis(config_prefix='CHECK')
# - 3.验证相关数据库对象,配置前缀为SESSION
redis_session = FlaskRedis(config_prefix='SESSION')
# - 4.种植园商品缓存库,配置前缀为ORCHARD
redis_orchard = FlaskRedis(config_prefix='ORCHARD')

# session储存配置初始化
session_store = Session()

# 自定义日志初始化
logger = Log()

# 初始化jsonrpc模块
jsonrpc = JSONRPC()

# marshmallow构造器初始化
marshmallow = Marshmallow()

# 初始化随机生成数据模块faker
faker = Faker(locale='zh-CN') # 指定中文

# 初始化异步celery
celery = Celery()

# jwt认证模块初始化
jwt = JWTManager()

# 阿里云对象存储oss初始化
oss = OssStore()

# admin后台站点初始化
admin = Admin()

# 国际化和本地化的初始化
babel = Babel()

# 文字转拼音初始化
pinyin = Pinyin()

# 二维码生成模块初始化
qrcode = QRcode()

# 初始化socketio通信模块
socketio = SocketIO()

# 解决跨域模块初始化
cors = CORS()

# 实例化PyMongo
mongo = PyMongo()

# Mongoengine初始化
mongoengine = MongoEngine()

# 全局初始化
def init_app(config_path):
    """全局初始化 - 需要传入加载开发或生产环境配置路径"""
    # 创建app应用对象
    app = Flask(__name__)

    # 当前项目根目录
    app.BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

    # 开发或生产环境加载配置
    init_config(app, config_path)

    # SQLAlchemy加载配置
    db.init_app(app)

    # PyMongo加载配置
    mongo.init_app(app)
    # MongoDB初始化加载配置
    mongo_init(app, mongo)
    # MongoEngine加载配置
    mongoengine.init_app(app)

    # redis加载配置
    redis_cache.init_app(app)
    redis_check.init_app(app)
    redis_session.init_app(app)
    redis_orchard.init_app(app)

    """一定先加载默认配置,再传入APP加载session对象"""
    # session保存数据到redis时启用的链接对象
    app.config["SESSION_REDIS"] = redis_session
    # session存储对象加载配置
    session_store.init_app(app)

    # 为日志对象加载配置
    log = logger.init_app(app)
    app.log = log

    # json-rpc加载配置
    jsonrpc.init_app(app)
    # rpc访问路径入口(只有唯一一个访问路径入口),默认/api
    jsonrpc.service_url = app.config.get('JSON_SERVER_URL', '/api')
    jsonrpc.enable_web_browsable_api = app.config.get("ENABLE_WEB_BROWSABLE_API",False)
    app.jsonrpc = jsonrpc

    # marshmallow构造器加载配置
    marshmallow.init_app(app)

    # 自动注册蓝图
    register_blueprint(app)

    # 加载celery配置
    celery.main = app.name
    celery.app = app
    # 更新配置
    celery.conf.update(app.config)
    # 自动注册任务
    celery.autodiscover_tasks(app.config.get('INSTALL_BLUEPRINT'))

    # jwt认证加载配置
    jwt.init_app(app)

    # faker作为app成员属性
    app.faker = faker

    # 注册模型,创建表
    with app.app_context():
        db.create_all()

    # 阿里云存储对象加载配置
    oss.init_app(app)

    # admin后台站点加载配置
    admin.name = app.config.get('FLASK_ADMIN_NAME') # 站点名称
    admin.template_mode = app.config.get('FLASK_TEMPLATE_MODE') # 使用的模板
    admin.init_app(app)

    # 国际化和本地化加载配置
    babel.init_app(app)

    # 二维码模块加载配置
    qrcode.init_app(app)

    # 跨域模块cors加载配置
    cors.init_app(app)

    # socketid加载配置,取代http通信,实现异步操作
    socketio.init_app(
        app,
        message_queue=app.config["MESSAGE_QUEUE"],  # 消息中间件 - Redis队列
        cors_allowed_origins=app.config["CORS_ALLOWED_ORIGINS"], # 跨域设置
        async_mode=app.config["ASYNC_MODE"], # 异步模式
        debug=app.config["DEBUG"]
    )
    # socketio作为app成员,方便调用
    app.socketio = socketio

    # 终端脚本工具加载配置
    manager.app = app

    # 自动注册自定义命令
    load_commands(manager)

    return manager

模型创建

mongoengine的使用开始于 Document模型的创建,文档模型类的声明例子:

from application import mongoengine as engine
class Document(engine.Document): 
    meta = {
        'collection': '集合名称',     # 设置当前文档类模型操作是mongo里面的哪个集合
        'ordering': ['-create_at'],  # 字段排序,多个字段使用逗号隔开
        'strict': False,             # 是否使用严格语法[mongoDB的内部查询语法]
        'indexes': [                 # 索引列表
            'title',                 # 普通索引
            '$title',                # 文本索引
            '#title',                # 哈希索引
            ('title', '-rating'),    # 联合索引
            ('category', '_cls'),
            {                        # TTL索引
                'fields': ['created'],
                'expireAfterSeconds': 3600
            }
        ]
    }
    # required=True 唯一 + 索引 = 唯一索引
    username = mgdb.StringField(required=True, validators=[validators.InputRequired(message='username不能为空!')])
    password = mgdb.StringField(min_length=6, max_length=16)

    email = mgdb.EmailField()
    sex = mgdb.IntField(default=1)
    money = mgdb.DecimalField()
    website = mgdb.URLField()
    sex = mgdb.BooleanField(default=True)
    info = mgdb.ReferenceField(Info)  # 关联子文档
    love = mgdb.ListField(mgdb.StringField(max_length=30))  # 列表的成员是字符串
    content = mgdb.EmbeddedDocumentField(Content) # 长文本
    remark = mydb.DictField()


字段类型

字段 选项 说明
StringField regex =None 正则表达式
min_length=None 最小长度
max_length=None 最大长度
字符串
URLField:url地址
EmailField:邮箱地址
IntField min_value=None 最小值
max_value=None 最大值
整型
FloatField min_value=None 最小值
max_value=None 最大值
浮点型
DecimalField min_value=None 最小值
max_value=None 最大值
precision=2 默认小数位的精度
rounding='ROUND_HALF_UP' 四舍五入的舍入规则
十进制数值型
BooleanField 布尔型
DateTimeField 日期时间类型
ComplexDateTimeField separator=',' 分隔符号 精确日期时间类型
EmbeddedDocumentField document_type=文档模型类 嵌入式文档类型
DynamicField 动态数据类型
ListField field=None 成员类型
max_length=None 成员最大个数
列表类型
SortedListField field=None 成员类型
max_length=None 成员最大个数
有序列表类型
DictField field=None 成员类型 字典类型
MapField field=None 成员类型 映射类型
ReferenceField document_type –将被引用的文档类型
dbref –将引用存储为DBRef 或作为ObjectId。
reverse_delete_rule –确定删除引用对象时的操作
kwargs –传递给父级的关键字参数BaseField
关联引用类型
LazyReferenceField 同上 关联引用类型(懒惰引用)
BinaryField max_bytes=None 最大字节长度 二进制数据
ImageField size =(width,height,force) 存储图像的最大大小
thumbnail_size=(width,height,force) 生成缩略图的大小
图像类型
SequenceField collection_name –计数器集合的名称(默认为“ mongoengine.counters”)
sequence_name –集合中序列的名称(默认为“ ClassName.counter”)
value_decorator –可以用作计数器的任何对象(默认int)
自增类型
UUIDField binary=True UUID类型
PointField 地理空间坐标(经纬度)
MultiPointField 地理空间坐标列表(经纬度)

字段公共属性

属性名 默认值 描述
db_field None mongodb中存储对应的属性名
required False 是否必填
default None 默认值
unique False 是否唯一
unique_with None 是否与其他字段组成联合唯一
primary_key False 是否设置为主键
validation None 写入数据时的验证规则
choices None 是否是枚举类型
null False 是否允许设置为None

模型的常用操作

# 保存数据
# 方式1:
Document.objects.create(字段=xxx)
# 方式2:
document = Document(字段=xxx,字段=xxx)
document.save(validate=False)

# 查询数据 基于QuerySet读取结果
Document.objects.all() # 获取所有数据
Document.objects.get() # 根据主键获取数据
Document.objects.first() # 获取一条数据

# 更新数据
# 方式1:
Document.objects.update({"字段":"值","字段":"值",....})
# 方式2:
document = Document.objects.get(pk=pk)
document.字段 = 值
document.update()

# 删除数据
# 方式1:
Document.objects.filter(条件).delete()
# 方式2:
document = Document.objects.get(pk=pk)
document.delete()

# 查询条件写法一样基于filter() 和django一样,字段__运算法名称=值。
# 字段__in = []
# 字段__lte = "值"


mongoengine模型操作game_config

把之前pymongo操作game_config的代码转换成mongoengine模型操作代码。

  1. orchard.documents,系统配置的mongo文档模型类,代码:
from application import mongoengine as mgdb

# 背包初始化配置模型
class GameConfigDocument(mgdb.Document):
    '''
    系统配置模型
    一个Document对应的就是一个集合,叫文档模型类, 主要定义文档中的字段信息和索引信息
    Document类的实例对象, 就是mongoDB中的真实数据文档
    '''
    meta = {
        'collection': 'game_config', # 集合名称
        'ordering': ['-_id'],    # 倒序排列
        'strict': False,         # 是否严格语法
        'indexes': [             # 索引列表
            'config_name',       # 普通索引[注意:索引的建立可能会和pymongo里面设置的产生冲突]
        ]
    }

    # 字段声明
    config_name = mgdb.StringField(required=True, unique=True, verbose_name='配置名称')
    init_package_capacity = mgdb.IntField(required=True, verbose_name="初始化背包容量")
    max_package_capacity = mgdb.IntField(required=True, verbose_name="最大背包容量")
    unlock_package = mgdb.ListField(mgdb.IntField(), verbose_name="每次激活背包容量需要花费的积分要求")  # 列表的成员是整型
    unlock_package_item = mgdb.IntField(required=True, verbose_name="每次激活背包的格子数量")

    def __str__(self):
        return self.config_name

  1. 蓝图注册到flask应用时,我们需要让documents自动被项目识别。application.utils.blueprint,代码:
from flask import Blueprint
# 引入导包函数
from importlib import import_module


# 自动注册蓝图
def register_blueprint(app):
    """
    自动注册蓝图
    :param app: 当前flask的app实例对象
    :return:
    """

    # 从配置文件中读取总路由路径信息
    app_urls_path = app.config.get('URL_PATH')
    # 导包,导入总路由模块
    app_urls_module = import_module(app_urls_path)
    # 获取总路由列表
    app_urlpatterns = app_urls_module.urlpatterns

    # 从配置文件中读取需要注册到项目中的蓝图路径信息
    blueprint_path_list = app.config.get('INSTALL_BLUEPRINT')
    # 遍历蓝图路径列表,对每一个蓝图进行初始化
    for blueprint_path in blueprint_path_list:
        # 获取蓝图路径中最后一段的包名作为蓝图的名称
        blueprint_name = blueprint_path.split('.')[-1]
        # 创建蓝图对象
        blueprint = Blueprint(blueprint_name, blueprint_path)

        # 蓝图路由的前缀
        url_prefix = ""
        # 蓝图下的子路由列表
        urlpatterns = []

        # 获取蓝图的父级目录, 即application/apps
        blueprint_father_path =  ".".join( blueprint_path.split(".")[:-1] )
        # 循环总路由列表
        for item in app_urlpatterns:
            # 判断当前蓝图是否在总路由列表中
            if blueprint_name in item["blueprint_url_subfix"]:
                # 导入蓝图下的子路由模块
                urls_module = import_module(blueprint_father_path + "." + item["blueprint_url_subfix"])
                # 1.获取urls模块下蓝图路由列表urlpatterns
                urlpatterns = [] # 防止下边调用循环报错
                try:
                    urlpatterns = urls_module.urlpatterns
                except Exception:
                    pass
                # 提取蓝图路由的前缀
                url_prefix = item["url_prefix"]
                # 2.获取urls模块下rpc接口列表apipatterns
                apipatterns = [] # 防止下边调用循环报错
                try:
                    apipatterns = urls_module.apipatterns
                except Exception:
                    pass
                # 3.获取urls模块下socket接口列表socketpatterns
                socketpatterns = []  # 防止下边调用循环报错
                try:
                    socketpatterns = urls_module.socketpatterns
                except Exception:
                    pass
                # 从总路由中查到当前蓝图对象的前缀就不要继续往下遍历了
                break

        # 注册蓝图路由
        for url in urlpatterns:
            blueprint.add_url_rule(**url)

        # 注册rpc接口路由
        api_prefix_name = ''
        # 判断是否开启补充api前缀
        if app.config.get('JSON_PREFIX_NAME', False) == True:
            api_prefix_name = blueprint_name.title() + '.'
        # 循环rpc接口列表,进行注册
        for api in apipatterns:
            app.jsonrpc.site.register(api_prefix_name + api['name'], api['method'])

        # 注册socket路由
        try:
            from application import socketio
            for socket in socketpatterns:
                # 获取类名
                namespace_handler = socket['namespace_handler']
                socket.pop('namespace_handler')
                # 注册视图类
                socketio.on_namespace(namespace_handler(**socket))
        except:
            pass

        try:
            # 导入蓝图模型
            import_module(blueprint_path + '.models')
        except ModuleNotFoundError:
            pass

        try:
            # 导入蓝图下的admin站点配置
            import_module(blueprint_path + '.admin')
        except ModuleNotFoundError:
            pass

        try:
            # 导入蓝图下的socket文件接口
            import_module(blueprint_path + '.socket')
        except ModuleNotFoundError:
            pass
        
        try:
            # 导入蓝图下的mongoengine模型类
            import_module(blueprint_path + '.documents')
        except ModuleNotFoundError:
            pass

        # 把蓝图对象注册到app实例对象,并添加路由前缀
        app.register_blueprint(blueprint, url_prefix=url_prefix)

    # 后台站点首页相关初始化[这段代码在循环蓝图完成以后,在循环体之外]
    from flask_admin import AdminIndexView  # admin主视图
    from application import admin
    admin._set_admin_index_view(index_view=AdminIndexView(
        name = app.config.get('ADMIN_HOME_NAME'), # 默认首页名称
        template= app.config.get('ADMIN_HOME_TEMPLATE') # 默认首页模板路径
    ))

# 把url地址和视图方法映射关系处理成字典
def path(rule, view_func, **kwargs):
    """绑定url地址和视图的映射关系"""
    data = {
        'rule':rule,
        'view_func':view_func,
        **kwargs
    }
    return data

# 把rpc方法名和视图的映射关系处理成字典
def api_rpc(name, method, **kwargs):
    """
    :param name: rpc方法名
    :param method: 视图名称
    :param kwargs: 其他参数。。
    :return:
    """
    data = {
        'name':name,
        'method':method,
        **kwargs
    }
    return data


# 路由前缀和蓝图进行绑定映射
def include(url_prefix, blueprint_url_subfix):
    """

    :param url_prefix: 路由前缀
    :param blueprint_url_subfix: 蓝图路由,
            格式:蓝图包名.路由模块名
            例如:蓝图目录是home, 路由模块名是urls,则参数:home.urls
    :return:
    """
    data = {
        "url_prefix": url_prefix,
        "blueprint_url_subfix": blueprint_url_subfix
    }
    return data

# websocketk接口命名空间类和命名空间名称进行关系映射
def socketapi(namespace,namespace_handler,**kwargs):
    """
    websocketk接口命名空间类和命名空间名称进行关系映射
    :param namespace: 命名空间访问名称
    :param namespace_handler: 命名空间处理类
    :param kwargs:
    :return:
    """
    return {"namespace":namespace,"namespace_handler":namespace_handler,**kwargs}


  1. 数据处理层代码,orchard.services,修改如下:
import orjson

from application import redis_orchard, mongo, db
from application.settings import plant as config

from .models import SeedItem, PetItem, PetFoodItem, PlantItem
from .marshmallow import SeedSchema, PetSchema, PetFoodSchema, PlantSchema
from .documents import GameConfigDocument


# 错误异常类
class MongoError(Exception):
    pass

# 获取背包初始配置信息
def get_package_settings():
    '''获取背包初始配置信息'''
    '''pymongo的写法'''
    # query = {'config_name':'package_settings'}
    # package_settings = mongo.db.game_config.find_one(query)
    #
    # # 如果没有初始化信息,就差配置文件中拿
    # data = {
    #     "init_package_capacity": package_settings.get("INIT_PACKAGE_CAPACITY", config.INIT_PACKAGE_CAPACITY),
    #     "max_package_capacity": package_settings.get("MAX_PACKAGE_CAPACITY", config.MAX_PACKAGE_CAPACITY),
    #     "unlock_package": package_settings.get("UNLOCK_PACKAGE", config.UNLOCK_PACKAGE),
    #     "unlock_package_item": package_settings.get("UNLOCK_PACKAGE_ITEM", config.UNLOCK_PACKAGE_ITEM),
    # }
    # return data

    '''mongoengine的写法'''
    from .marshmallow import GameConfigSchema
    try:
        # 获取模型对象
        package_settings = GameConfigDocument.objects.get(config_name = 'package_settings')
        # 实例化构造器
        gcs = GameConfigSchema()
        # 序列化输出
        data = gcs.dump(package_settings)
        return data

    except GameConfigDocument.DoesNotExist:
        # 找不到文档
        data = {
            "init_package_capacity": config.INIT_PACKAGE_CAPACITY,
            "max_package_capacity": config.MAX_PACKAGE_CAPACITY,
            "unlock_package": config.UNLOCK_PACKAGE,
            "unlock_package_item": config.UNLOCK_PACKAGE_ITEM,
        }
        return data

    except Exception:
        raise MongoError
  1. 创建构造器 orchard/marshmallow.py,代码:
from marshmallow import Schema,fields
class GameConfigSchema(Schema):
    """背包配置的构造器"""
    init_package_capacity = fields.Integer()
    max_package_capacity  = fields.Integer()
    unlock_package        = fields.List(fields.Integer())
    unlock_package_item   = fields.Integer()


用户背包显示背包道具

服务端返回当前用户的背包道具

  1. 用户websocket登陆, 显示socket用户信息和背包的道具物品 , orchard/socket.py ,代码:
from flask_socketio import emit, Namespace, join_room
from flask import request

from application import code, message
from application.apps.users import services as user_services
from . import services

"""基于类视图接口"""

# 种植园模块命名空间
class OrchardNamespace(Namespace):
    '''种植园模块命名空间'''
    # socket链接
    def on_connect(self):
        print("用户[%s]进入了种植园!" % request.sid)
        # 返回初始化信息[不涉及当前的用户,因为我们现在不知道当前用户是谁]
        self.init_config()

    def init_config(self, ):
        """系统基本信息初始化"""
        # 返回背包初始配置信息
        package_settings = services.get_package_settings()
        emit("init_config_response", package_settings)

    # socket断开连接
    def on_disconnect(self):
        print("用户[%s]退出了种植园!" % request.sid)

    # 接收登录信息
    def on_login(self, data):
        # 加入房间
        self.on_join_room(data['uid']) # 接受客户端发送过来的用户 unique id

        # 1.用户websocket登录处理,获取用户初始信息(uid,sid,背包初始容量)
        # request.sid websocket链接客户端的会话ID
        user_info = user_services.user_websocket_login(data['uid'],request.sid)

        # todo 返回种植园的相关配置参数[种植果树的数量上限]

        msg = {
            'errno': code.CODE_OK,
            'errmsg': message.ok,
            'user_info': user_info
        }
        # 响应登录信息
        emit("login_response", msg)

        # 2.用户登录获取用户背包信息
        self.on_user_package()

    # 获取用户背包信息
    def on_user_package(self):
        '''获取用户背包信息'''
        # 1.根据sid获取用户模型对象
        user = user_services.get_user_by_sid(request.sid)
        # 判断用户是否存在
        if user is None:
            # 响应数据
            emit('user_package_response',{
                'errno':code.CODE_USER_NOT_EXISTS,
                'errmsg': message.user_not_exists,
                'package_info': {}
            })
            # 停止程序继续运行
            return

        # 2.根据用户id获取背包信息
        package_info = user_services.get_package_info_by_id(user.id)
        # print("package_info =", package_info)
        # 响应数据
        emit('user_package_response',{
            'errno': code.CODE_OK,
            'errmsg': message.ok,
            'package_info': package_info
        })


    # 房间分发
    def on_join_room(self, room):
        '''加入房间'''
        # 默认用户自己一个人一个房间,也就是一个单独的频道
        join_room(room)

因为背包还是和用户的关系更加密切,所以我们这里把关于用户登陆和用户背包的相关代码转移到users蓝图下。

  1. 创建用户配置信息和用户背包信息的文档模型,users.documents, 代码:
from application import mongoengine as mgdb
from application.settings import plant as config

# 用户websocket登录信息文档模型
class UserInfoDocument(mgdb.Document):
    '''用户websocket登录信息文档模型'''
    meta = {
        'collection': 'user_info',  # 集合名称
        'ordering': ['-_id'],       # 倒序排列
        'strict': False,            # 是否严格语法
        'indexes':[                 # 索引列表
            'uid',                  # 普通索引
            'sid',
            ('uid','sid')           # 联合索引
        ]
    }

    # 字段声明
    sid = mgdb.StringField(required=True, verbose_name='客户端websocket请求的回话ID')
    uid = mgdb.UUIDField(required=True, verbose_name='用户unique_id')
    package_number = mgdb.IntField(default=config.INIT_PACKAGE_CAPACITY, verbose_name='背包初始容量')

# 用户背包信息文档模型
class UserPackageDocument(mgdb.Document):
    '''用户背包信息文档模型'''
    meta = {
        'collection': 'user_package',   # 集合名称
        'ordering': ['_id'],            # 正序排列
        'strict': False,                # 是否严格语法
        'indexes': [
            'user_id'                   # 普通索引
        ]
    }

    # 字段声明
    user_id = mgdb.IntField(required=True,unique=True, verbose_name='用户ID')
    capacity = mgdb.IntField(default=config.INIT_PACKAGE_CAPACITY, verbose_name='当前背包容量')
    props_list = mgdb.ListField(mgdb.DictField(), verbose_name="背包内的道具列表")


  1. 创建用户和背包构造器 users.marshmallow中,用户模型构造器添加返回unique_id字段, 代码:
# 用户注册数据校验模型构造器
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', 'unique_id', '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 = services.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

# websocket用户配置的构造器
class UserInfoSchema(Schema):
    '''websocket用户配置的构造器'''
    sid = fields.String()
    uid = fields.UUID()
    package_number = fields.Integer()

# 用户背包的构造器
class UserPackageSchema(Schema):
    '''用户背包的构造器'''
    user_id = fields.Integer()
    capacity = fields.Integer()
    props_list = fields.List(fields.Dict())

  1. 数据服务层处理数据 users/services.py
from .marshmallow import UserInfoSchema, UserPackageSchema
from .documents import UserInfoDocument, UserPackageDocument

# 用户在websocket中的登录处理
def user_websocket_login(uid, sid):
    '''
    用户在websocket中的登录处理
    :param uid: 用户保存在数据库中unique_id,用于识别用户身份
    :param sid: 本次请求过来的websocket客户端的会话ID
    :return:
    '''
    # 判断当前用户是否在mongo中之前就有记录
    try:
        # 获取用户对象
        user_info = UserInfoDocument.objects.get(uid=uid, sid=sid)
    # 如果不存在
    except UserInfoDocument.DoesNotExist:
        user_info = UserInfoDocument.objects.filter(uid=uid).first()
        if user_info is None:
            # 如果没有添加用户信息记录
            user_info = UserInfoDocument.objects.create(sid=sid, uid=uid)
        else:
            # 更新sid记录
            user_info.sid = sid
            user_info.save()

    # 实例化构造器
    uis = UserInfoSchema()
    # 序列化输出
    data = uis.dump(user_info)
    return data

# 根据回话id获取用户配置信息
def get_user_info_by_sid(sid):
    '''
    根据回话id获取用户配置信息
    :param sid: 本次websocket客户端请求的会话ID
    :return:
    '''
    try:
        user_info = UserInfoDocument.objects.get(sid=sid)
        return user_info
    # 如果不存在
    except UserInfoDocument.DoesNotExist:
        return None

# 根据回话id获取用户模型对象
def get_user_by_sid(sid):
    '''
    根据回话id获取用户模型对象
    :param sid: 本次websocket客户端请求的会话ID
    :return:
    '''
    # 1.根据回话id获取用户配置信息
    user_info = get_user_info_by_sid(sid)
    # 判断是否存在
    if user_info:
        # 根据unique_id获取用户模型对象
        user = User.query.filter(User.unique_id == user_info.uid).first()
        return user
    else:
        return None

# 根据用户ID获取用户背包信息
def get_package_info_by_id(user_id):
    '''
    根据用户ID获取用户背包信息
    :param user_id: 用户ID
    :return:
    '''
    try:
        # 获取用户背包信息对象
        user_package = UserPackageDocument.objects.get(user_id=user_id)
        # 实例化构造器
        ups = UserPackageSchema()
        # 序列化输出
        data = ups.dump(user_package)
        return data
    # 如果用户背包对象不存在
    except UserPackageDocument.DoesNotExist:
        # 初始化用户背包信息
        from application.apps.orchard import services as orchard_services
        orchard_services.user_init_package(user_id)
        # 再次调用自己获取用户背包信息
        return get_package_info_by_id(user_id)

客户端展示背包道具信息

  1. 种植园页面, 用户websocket登陆获取背包信息 , 并把获取的信息传递到背包页面, html/orchard.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/socket.io.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 orchard" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="orchard-bg">
			<img src="../static/images/bg2.png">
			<img class="board_bg2" src="../static/images/board_bg2.png">
		</div>
    <img class="back" @click="back" src="../static/images/user_back.png" alt="">
    <div class="header">
			<div class="info">
				<div class="avatar" @click='to_user'>
					<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="62" :rounded="true"></v-avatar>
						<v-avatar v-else-if="user_data.nickname" :username="user_data.nickname" :size="62" :rounded="true"></v-avatar>
						<v-avatar v-else :username="user_data.id" :size="62" :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" @click='user_recharge'>
				<div class="balance">
					<p class="title"><img src="../static/images/money.png" alt="">钱包</p>
					<p class="num">{{game.number_format(user_data.money)}}</p>
				</div>
				<div class="balance">
					<p class="title"><img src="../static/images/integral.png" alt="">果子</p>
					<p class="num">{{game.number_format(user_data.credit)}}</p>
				</div>
			</div>
      <div class="menu-list">
        <div class="menu">
          <img src="../static/images/menu1.png" alt="">
          排行榜
        </div>
        <div class="menu">
          <img src="../static/images/menu2.png" alt="">
          签到有礼
        </div>
        <div class="menu">
          <img src="../static/images/menu3.png" alt="">
          道具商城
        </div>
        <div class="menu">
          <img src="../static/images/menu4.png" alt="">
          邮件中心
        </div>
      </div>
		</div>
		<div class="footer" >
      <ul class="menu-list">
        <li class="menu">新手</li>
        <li class="menu" @click='to_package'>背包</li>
        <li class="menu-center" @click="to_shop">商店</li>
        <li class="menu">消息</li>
        <li class="menu">好友</li>
      </ul>
    </div>
	</div>
	<script>
	apiready = function(){
		Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
		new Vue({
			el:"#app",
			data(){
				return {
					user_data:{},  // 当前用户信息
          music_play:true,
          namespace: '/orchard', // websocket命名空间
          socket: null, // websocket连接对象
					recharge_list: [], // 允许充值的金额列表
					package_init_setting: {}, // 背包初始配置信息
					user_info: {}, // 用户登陆初始化化信息
					user_package_info: {}, // 用户背包信息
				}
			},
      created(){
				// socket建立连接
				this.socket_connect();
        // 自动加载我的果园页面
				this.to_my_orchard();
				// 获取用户数据
				this.get_user_data()
				// 监听事件变化
				this.listen()
				// 获取充值金额列表
				this.get_recharge_list()
      },
			methods:{
				back(){
					this.game.openWin("root");
				},
				// 监听事件
				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(){
					let self = this;
					// 检查token是否过期,过期从新刷新token
					self.game.check_user_login(self, ()=>{
						// 获取token
						let token = this.game.getfs('access_token') || this.game.getdata('access_token')
						// 根据token获取用户数据
						this.user_data = this.game.get_user_by_token(token)
					})
				},

				// websocket连接处理
        socket_connect(){
          this.socket = io.connect(this.game.config.SOCKET_SERVER + this.namespace, {transports: ['websocket']});
          this.socket.on('connect', ()=>{
              this.game.print("开始连接服务端");
							// 获取背包初始配置信息
							this.get_package_setting()
							// websocket登陆处理
							this.user_websocket_login()
          });
        },

				// 获取背包初始配置信息
				get_package_setting(){
					this.socket.on('init_config_response', (response)=>{
						// this.game.print(response,1)
						this.package_init_setting = response
					})
				},

				// websocket登陆处理
				user_websocket_login(){
					// 客户端发送用户登陆请求
					this.socket.emit('login',{'uid': this.user_data.unique_id});
					// 接收登陆响应
					this.login_response();
					// 接收用户背包响应
					this.user_package_response();
				},

				// 接收登陆初始化信息
				login_response(){
					this.socket.on('login_response',(response)=>{
						// this.game.print(response,1)
						if(response.errno === 1000){
							this.user_info = response.user_info
						}
					})
				},

				// 接收用户背包信息
				user_package_response(){
					this.socket.on('user_package_response',(response)=>{
						// this.game.print(response,1)
						if(response.errno === 1000){
							this.user_package_info = response.package_info
						}else {
							this.game.tips(response.errmsg)
						}
					})
				},

        // 跳转我的果园页面
        to_my_orchard(){
          this.game.check_user_login(this, ()=>{
            this.game.openFrame("my_orchard","my_orchard.html","from_right",{
    					marginTop: 174,     //相对父页面上外边距的距离,数字类型
    					marginLeft: 0,      //相对父页面左外边距的距离,数字类型
    			    marginBottom: 54,     //相对父页面下外边距的距离,数字类型
    			    marginRight: 0     //相对父页面右外边距的距离,数字类型
    				});
          })
        },

        // 点击商店打开道具商城页面
				to_shop(){
          this.game.check_user_login(this, ()=>{
            this.game.openFrame('shop', 'shop.html', 'from_top');
          })
        },

				// 点击头像,跳转用户中心页面
				to_user(){
					this.game.check_user_login(this, ()=>{
            this.game.openFrame('user', 'user.html', 'from_right');
          })
				},

				// 获取充值金额列表
				get_recharge_list(){
					let self = this;
					self.game.check_user_login(self,()=>{
						self.game.post(self,{
							'method': 'Users.recharge_list',
							'params': {},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									self.recharge_list = data.result.recharge_list
								}
							}
						})
					})
				},

				// 点击钱包进行用户充值,设置充值金额
				user_recharge(){
					api.actionSheet({
								title: '余额充值',
								cancelTitle: '取消',
								buttons: this.recharge_list
						}, (ret, err)=>{
								if( ret.buttonIndex <= this.recharge_list.length ){
										 // 充值金额
										 let money = this.recharge_list[ret.buttonIndex-1];
										//  this.game.print(money,1);
										// 发送支付宝充值请求
										this.recharge_app_pay(money)
								}
					});
				},

				// 发送支付宝充值请求
				recharge_app_pay(money){
					// 获取支付宝支付对象
					let aliPayPlus = api.require("aliPayPlus");
					let self = this;
					// 向服务端发送请求获取终止订单信息
					self.game.check_user_login(self, ()=>{
						let token = this.game.getdata("access_token") || this.game.getfs("access_token");
						self.game.post(self,{
							'method': 'Users.recharge',
							'params':{
								'money': money,
							},
							'header': {
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 本次充值的订单参数
									let order_string = data.result.order_string;

									// 支付完成以后,支付APP返回的响应状态码
									let resultCode = {
										"9000": "支付成功!",
										"8000": "支付正在处理中,请稍候!",
										"4000": "支付失败!请联系我们的工作人员~",
										"5000": "支付失败,重复的支付操作",
										"6002": "网络连接出错",
										"6004": "支付正在处理中,请稍后",
									}
									// 唤醒支付宝APP,发起支付
									aliPayPlus.payOrder({
										orderInfo: order_string,
										sandbox: data.result.sandbox, // 将来APP上线需要修改成false
									},(ret, err)=>{
										if(resultCode[ret.code]){
											// 提示支付结果
											if(ret.code != 9000){
												self.game.tips(resultCode[ret.code]);
											}else {
												// 支付成功,向服务端请求验证支付结果 - 参数订单号
												self.check_recharge_result(data.result.order_number);
											}
										}
									})
								}
							}
						})
					})
				},

				// 向服务端发送请求,校验充值是否成功
				check_recharge_result(order_number){
					let self = this;
					self.game.check_user_login(self, ()=>{
						let token = this.game.getdata("access_token") || this.game.getfs("access_token");
						self.game.post(self,{
							'method': 'Users.check_recharge_result',
							'params':{
								'order_number':order_number,
							},
							'header':{
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 充值成功
									self.game.tips('充值成功!')
									// 用户数据更改过,重新刷新token值
									let token = self.game.getfs("access_token");
									// 删除token值
									self.game.deldata(["access_token","refresh_token"]);
									self.game.delfs(["access_token","refresh_token"]);
									if(token){
                    // 记住密码的情况
                    self.game.setfs({
                      "access_token": data.result.access_token,
                      "refresh_token": data.result.refresh_token,
                    });
                  }else{
                    // 不记住密码的情况
                    self.game.setdata({
                      "access_token": data.result.access_token,
                      "refresh_token": data.result.refresh_token,
                    });
									}
									// 全局广播充值成功
									self.game.sendEvent('recharge_success')
									// 全局广播刷新token值
									self.game.sendEvent('update_token')
								}
							}
						})
					})
				},

				// 点击背包,跳转到背包页面,并传递背包初始配置信息
				to_package(){
					this.game.check_user_login(this, ()=>{
						this.game.openFrame('package', 'package.html', 'from_top',null, {
							'package_init_setting': this.package_init_setting,
							'user_package_info': this.user_package_info,
							'user_info': this.user_info
						})
					})
				},


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

  1. 在背包页面中结束背包信息参数, 并进行内容展示 , html/package.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 add_friend package" id="app">
    <div class="box">
      <p class="title">我的背包</p>
      <img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
			<!-- 背包道具列表 -->
		  <div class="prop_list">
        <div class="item" v-for='prop in user_package_info.props_list'>
          <img :src="`../static/images/${prop.prop_image}`" alt="">
          <span>{{prop.num}}</span>
        </div>

				<!-- 循环背包列表,调整样式 -->
				<!-- 已解锁背包格子 -->
				<div class="item" v-for='i in ( user_package_info.capacity - user_package_info.props_list.length)'></div>
				<!-- 未解锁背包格子 -->
				<div class="item lock" v-for='i in (package_init_setting.max_package_capacity - user_package_info.capacity)'></div>
      </div>
    </div>
	</div>
	<script>
	apiready = function(){
    	Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
		new Vue({
			el:"#app",
			data(){
				return {
					user_id: "", // 当前登陆用户Id
					package_init_setting: {}, // 背包初始配置信息
					user_info: {}, // 用户登陆初始化化信息
					user_package_info: {}, // 用户背包信息
				}
			},

			created(){
				// 获取其他页面传递的参数信息
				this.get_page_params();
			},
			methods:{
        back(){
          this.game.closeFrame();
        },

				// 获取其他页面传递的参数信息
				get_page_params(){
					// 获取背包初始配置信息
					this.package_init_setting = api.pageParam.package_init_setting;
					// 获取用户登陆初始化信息
					this.user_info = api.pageParam.user_info;
					// 获取用户背包信息
					this.user_package_info = api.pageParam.user_package_info;
					this.game.print(this.user_package_info,1)
				},


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


用户购买道具,背包信息更新

种植园页面 orchard.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/socket.io.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 orchard" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="orchard-bg">
			<img src="../static/images/bg2.png">
			<img class="board_bg2" src="../static/images/board_bg2.png">
		</div>
    <img class="back" @click="back" src="../static/images/user_back.png" alt="">
    <div class="header">
			<div class="info">
				<div class="avatar" @click='to_user'>
					<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="62" :rounded="true"></v-avatar>
						<v-avatar v-else-if="user_data.nickname" :username="user_data.nickname" :size="62" :rounded="true"></v-avatar>
						<v-avatar v-else :username="user_data.id" :size="62" :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" @click='user_recharge'>
				<div class="balance">
					<p class="title"><img src="../static/images/money.png" alt="">钱包</p>
					<p class="num">{{game.number_format(user_data.money)}}</p>
				</div>
				<div class="balance">
					<p class="title"><img src="../static/images/integral.png" alt="">果子</p>
					<p class="num">{{game.number_format(user_data.credit)}}</p>
				</div>
			</div>
      <div class="menu-list">
        <div class="menu">
          <img src="../static/images/menu1.png" alt="">
          排行榜
        </div>
        <div class="menu">
          <img src="../static/images/menu2.png" alt="">
          签到有礼
        </div>
        <div class="menu">
          <img src="../static/images/menu3.png" alt="">
          道具商城
        </div>
        <div class="menu">
          <img src="../static/images/menu4.png" alt="">
          邮件中心
        </div>
      </div>
		</div>
		<div class="footer" >
      <ul class="menu-list">
        <li class="menu">新手</li>
        <li class="menu" @click='to_package'>背包</li>
        <li class="menu-center" @click="to_shop">商店</li>
        <li class="menu">消息</li>
        <li class="menu">好友</li>
      </ul>
    </div>
	</div>
	<script>
	apiready = function(){
		Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
		new Vue({
			el:"#app",
			data(){
				return {
					user_data:{},  // 当前用户信息
          music_play:true,
          namespace: '/orchard', // websocket命名空间
          socket: null, // websocket连接对象
					recharge_list: [], // 允许充值的金额列表
					package_init_setting: {}, // 背包初始配置信息
					user_info: {}, // 用户登陆初始化化信息
					user_package_info: {}, // 用户背包信息
				}
			},
      created(){
				// socket建立连接
				this.socket_connect();
        // 自动加载我的果园页面
				this.to_my_orchard();
				// 获取用户数据
				this.get_user_data()
				// 监听事件变化
				this.listen()
				// 获取充值金额列表
				this.get_recharge_list()
      },
			methods:{
				back(){
					this.game.openWin("root");
				},
				// 监听事件
				listen(){
					// 监听token更新的通知
					this.listen_update_token();
					// 监听是否购买道具成功的通知
					this.listen_buy_prop_success();
					// 监听是否使用道具成功的通知
					// this.listen_use_prop_success();
				},

				// 监听token更新的通知
				listen_update_token(){
					api.addEventListener({
							name: 'update_token'
					}, (ret, err) => {
						// 更新用户数据
							this.get_user_data()
					});
				},

				// 监听是否购买道具成功的通知
				listen_buy_prop_success(){
					api.addEventListener({
					    name: 'buy_prop_success'
					}, (ret, err)=>{
							// 发送请求
							this.socket.emit('user_package');
							// 获取更新的背包数据
							this.user_package_response();
					});

				},

				// 通过token值获取用户数据
				get_user_data(){
					let self = this;
					// 检查token是否过期,过期从新刷新token
					self.game.check_user_login(self, ()=>{
						// 获取token
						let token = this.game.getfs('access_token') || this.game.getdata('access_token')
						// 根据token获取用户数据
						this.user_data = this.game.get_user_by_token(token)
					})
				},

				// websocket连接处理
        socket_connect(){
          this.socket = io.connect(this.game.config.SOCKET_SERVER + this.namespace, {transports: ['websocket']});
          this.socket.on('connect', ()=>{
              this.game.print("开始连接服务端");
							// 获取背包初始配置信息
							this.get_package_setting()
							// websocket登陆处理
							this.user_websocket_login()
          });
        },

				// 获取背包初始配置信息
				get_package_setting(){
					this.socket.on('init_config_response', (response)=>{
						// this.game.print(response,1)
						this.package_init_setting = response
					})
				},

				// websocket登陆处理
				user_websocket_login(){
					// 客户端发送用户登陆请求
					this.socket.emit('login',{'uid': this.user_data.unique_id});
					// 接收登陆响应
					this.login_response();
					// 接收用户背包响应
					this.user_package_response();
				},

				// 接收登陆初始化信息
				login_response(){
					this.socket.on('login_response',(response)=>{
						// this.game.print(response,1)
						if(response.errno === 1000){
							this.user_info = response.user_info
						}
					})
				},

				// 接收用户背包信息
				user_package_response(){
					this.socket.on('user_package_response',(response)=>{
						// this.game.print(response,1)
						if(response.errno === 1000){
							this.user_package_info = response.package_info
						}else {
							this.game.tips(response.errmsg)
						}
					})
				},

        // 跳转我的果园页面
        to_my_orchard(){
          this.game.check_user_login(this, ()=>{
            this.game.openFrame("my_orchard","my_orchard.html","from_right",{
    					marginTop: 174,     //相对父页面上外边距的距离,数字类型
    					marginLeft: 0,      //相对父页面左外边距的距离,数字类型
    			    marginBottom: 54,     //相对父页面下外边距的距离,数字类型
    			    marginRight: 0     //相对父页面右外边距的距离,数字类型
    				});
          })
        },

        // 点击商店打开道具商城页面
				to_shop(){
          this.game.check_user_login(this, ()=>{
            this.game.openFrame('shop', 'shop.html', 'from_top');
          })
        },

				// 点击头像,跳转用户中心页面
				to_user(){
					this.game.check_user_login(this, ()=>{
            this.game.openFrame('user', 'user.html', 'from_right');
          })
				},

				// 获取充值金额列表
				get_recharge_list(){
					let self = this;
					self.game.check_user_login(self,()=>{
						self.game.post(self,{
							'method': 'Users.recharge_list',
							'params': {},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									self.recharge_list = data.result.recharge_list
								}
							}
						})
					})
				},

				// 点击钱包进行用户充值,设置充值金额
				user_recharge(){
					api.actionSheet({
								title: '余额充值',
								cancelTitle: '取消',
								buttons: this.recharge_list
						}, (ret, err)=>{
								if( ret.buttonIndex <= this.recharge_list.length ){
										 // 充值金额
										 let money = this.recharge_list[ret.buttonIndex-1];
										//  this.game.print(money,1);
										// 发送支付宝充值请求
										this.recharge_app_pay(money)
								}
					});
				},

				// 发送支付宝充值请求
				recharge_app_pay(money){
					// 获取支付宝支付对象
					let aliPayPlus = api.require("aliPayPlus");
					let self = this;
					// 向服务端发送请求获取终止订单信息
					self.game.check_user_login(self, ()=>{
						let token = this.game.getdata("access_token") || this.game.getfs("access_token");
						self.game.post(self,{
							'method': 'Users.recharge',
							'params':{
								'money': money,
							},
							'header': {
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 本次充值的订单参数
									let order_string = data.result.order_string;

									// 支付完成以后,支付APP返回的响应状态码
									let resultCode = {
										"9000": "支付成功!",
										"8000": "支付正在处理中,请稍候!",
										"4000": "支付失败!请联系我们的工作人员~",
										"5000": "支付失败,重复的支付操作",
										"6002": "网络连接出错",
										"6004": "支付正在处理中,请稍后",
									}
									// 唤醒支付宝APP,发起支付
									aliPayPlus.payOrder({
										orderInfo: order_string,
										sandbox: data.result.sandbox, // 将来APP上线需要修改成false
									},(ret, err)=>{
										if(resultCode[ret.code]){
											// 提示支付结果
											if(ret.code != 9000){
												self.game.tips(resultCode[ret.code]);
											}else {
												// 支付成功,向服务端请求验证支付结果 - 参数订单号
												self.check_recharge_result(data.result.order_number);
											}
										}
									})
								}
							}
						})
					})
				},

				// 向服务端发送请求,校验充值是否成功
				check_recharge_result(order_number){
					let self = this;
					self.game.check_user_login(self, ()=>{
						let token = this.game.getdata("access_token") || this.game.getfs("access_token");
						self.game.post(self,{
							'method': 'Users.check_recharge_result',
							'params':{
								'order_number':order_number,
							},
							'header':{
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 充值成功
									self.game.tips('充值成功!')
									// 用户数据更改过,重新刷新token值
									let token = self.game.getfs("access_token");
									// 删除token值
									self.game.deldata(["access_token","refresh_token"]);
									self.game.delfs(["access_token","refresh_token"]);
									if(token){
                    // 记住密码的情况
                    self.game.setfs({
                      "access_token": data.result.access_token,
                      "refresh_token": data.result.refresh_token,
                    });
                  }else{
                    // 不记住密码的情况
                    self.game.setdata({
                      "access_token": data.result.access_token,
                      "refresh_token": data.result.refresh_token,
                    });
									}
									// 全局广播充值成功
									self.game.sendEvent('recharge_success')
									// 全局广播刷新token值
									self.game.sendEvent('update_token')
								}
							}
						})
					})
				},

				// 点击背包,跳转到背包页面,并传递背包初始配置信息
				to_package(){
					this.game.check_user_login(this, ()=>{
						this.game.openFrame('package', 'package.html', 'from_top',null, {
							'package_init_setting': this.package_init_setting,
							'user_package_info': this.user_package_info,
							'user_info': this.user_info
						})
					})
				},


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


用户购买道具的时候,判断背包存储是否达到上限

[38. 28, 15] => 每个成员的值最大只能是20,超过部分拆分成新的成员, 但是数组长度最多是6,怎么计算判断.

from application import socketio
from flask import request
from application.apps.users.models import User
from flask_socketio import join_room, leave_room
from application import mongo
from .models import Goods,Setting
from status import APIStatus as status
from message import ErrorMessage as errmsg
# 建立socket通信
# @socketio.on("connect", namespace="/mofang")
# def user_connect():
#     """用户连接"""
#     print("用户%s连接过来了!" % request.sid)
#     # 主动响应数据给客户端
#     socketio.emit("server_response","hello",namespace="/mofang")

# 断开socket通信
@socketio.on("disconnect", namespace="/mofang")
def user_disconnect():
    print("用户%s退出了种植园" % request.sid )

@socketio.on("login", namespace="/mofang")
def user_login(data):
    # 分配房间
    room = data["uid"]
    join_room(room)
    # 保存当前用户和sid的绑定关系
    # 判断当前用户是否在mongo中有记录
    query = {
        "_id": data["uid"]
    }
    ret = mongo.db.user_info_list.find_one(query)
    if ret:
        mongo.db.user_info_list.update_one(query,{"$set":{"sid": request.sid}})
    else:
        mongo.db.user_info_list.insert_one({
        "_id": data["uid"],
        "sid": request.sid,
    })

    # 返回种植园的相关配置参数
    orchard_settings = {}
    setting_list = Setting.query.filter(Setting.is_deleted==False, Setting.status==True).all()
    """
    现在的格式:
        [<Setting package_number_base>, <Setting package_number_max>, <Setting package_unlock_price_1>]
    需要返回的格式:
        {
            package_number_base:4,
            package_number_max: 32,
            ...
        }
    """
    for item in setting_list:
        orchard_settings[item.name] = item.value

    # 返回当前用户相关的配置参数
    user_settings = {}
    # 从mongo中查找用户信息,判断用户是否激活了背包格子
    dict = mongo.db.user_info_list.find_one({"sid":request.sid})
    # 背包格子
    if dict.get("package_number") is None:
        user_settings["package_number"]  = orchard_settings.get("package_number_base",4)
        mongo.db.user_info_list.update_one({"sid":request.sid},{"$set":{"package_number": user_settings["package_number"]}})
    else:
        user_settings["package_number"]  = dict.get("package_number")

    socketio.emit("login_response", {
        "errno":status.CODE_OK,
        "errmsg":errmsg.ok,
        "orchard_settings":orchard_settings,
        "user_settings":user_settings
    }, namespace="/mofang", room=room)

@socketio.on("user_buy_prop", namespace="/mofang")
def user_buy_prop(data):
    """用户购买道具"""
    room = request.sid
    # 从mongo中获取当前用户信息
    user_info = mongo.db.user_info_list.find_one({"sid":request.sid})
    user = User.query.get(user_info.get("_id"))
    if user is None:
        socketio.emit("user_buy_prop_response", {"errno":status.CODE_NO_USER,"errmsg":errmsg.user_not_exists}, namespace="/mofang", room=room)
        return

    # 判断背包物品存储是否达到上限
    use_package_number = int(user_info.get("use_package_number",0)) # 当前诗经使用的格子数量
    package_number = int(user_info.get("package_number",0))         # 当前用户已经解锁的格子数量
    # 本次购买道具需要使用的格子数量
    setting = Setting.query.filter(Setting.name == "td_prop_max").first()
    if setting is None:
        td_prop_max = 10
    else:
        td_prop_max = int(setting.value)

    # 计算购买道具以后需要额外占用的格子数量
    if ("prop_%s" % data["pid"]) in user_info.get("prop_list"):
        """曾经购买过当前道具"""
        prop_num = int( user_info.get("prop_list")["prop_%s" % data["pid"]]) # 购买前的道具数量
        new_prop_num = prop_num+int(data["num"]) # 如果成功购买道具以后的数量
        old_td_num = prop_num // td_prop_max
        if prop_num % td_prop_max > 0:
            old_td_num+=1
        new_td_num = new_prop_num // td_prop_max
        if new_prop_num % td_prop_max > 0:
            new_td_num+=1
        td_num = new_td_num - old_td_num
    else:
        """新增购买的道具"""
        # 计算本次购买道具需要占用的格子数量

        if int(data["num"]) > td_prop_max:
            """需要多个格子"""
            td_num = int(data["num"]) // td_prop_max
            if int(data["num"]) % td_prop_max > 0:
                td_num+=1
        else:
            """需要一个格子"""
            td_num = 1

    if use_package_number+td_num > package_number:
        """超出存储上限"""
        socketio.emit("user_buy_prop_response", {"errno": status.CODE_NO_PACKAGE, "errmsg": errmsg.no_package},
                      namespace="/mofang", room=room)
        return

    # 从mysql中获取商品价格
    prop = Goods.query.get(data["pid"])
    if float(user.money) < float(prop.price) * int(data["num"]):
        socketio.emit("user_buy_prop_response", {"errno":status.CODE_NO_MONEY,"errmsg":errmsg.money_no_enough}, namespace="/mofang", room=room)
        return
    # 从mongo中获取用户列表信息,提取购买的商品数量进行累加和余额
    query = {"sid": request.sid}
    if user_info.get("prop_list") is None:
        """此前没有购买任何道具"""
        message = {"$set":{"prop_list":{"prop_%s" % prop.id:int(data["num"])}}}
        mongo.db.user_info_list.update_one(query,message)
    else:
        """此前有购买了道具"""
        prop_list = user_info.get("prop_list") # 道具列表
        if ("prop_%s" % prop.id) in prop_list:
            """如果再次同一款道具"""
            prop_list[("prop_%s" % prop.id)] = prop_list[("prop_%s" % prop.id)] + int(data["num"])
        else:
            """此前没有购买过这种道具"""
            prop_list[("prop_%s" % prop.id)] = int(data["num"])

        mongo.db.user_info_list.update_one(query, {"$set":{"prop_list":prop_list}})

    # 返回购买成功的信息
    socketio.emit("user_buy_prop_response", {"errno":status.CODE_OK,"errmsg":errmsg.ok}, namespace="/mofang", room=room)
    # 返回最新的用户道具列表
    user_prop()

@socketio.on("user_prop", namespace="/mofang")
def user_prop():
    """用户道具"""
    userinfo = mongo.db.user_info_list.find_one({"sid":request.sid})
    prop_list = userinfo.get("prop_list")
    prop_id_list = []
    for prop_str,num in prop_list.items():
        pid = int(prop_str[5:])
        prop_id_list.append(pid)

    data = []
    prop_list_data = Goods.query.filter(Goods.id.in_(prop_id_list)).all()
    setting = Setting.query.filter(Setting.name == "td_prop_max").first()
    if setting is None:
        td_prop_max = 10
    else:
        td_prop_max = int(setting.value)

    for prop_data in prop_list_data:
        num = int( prop_list[("prop_%s" % prop_data.id)])
        if td_prop_max > num:
            data.append({
                "num": num,
                "image": prop_data.image,
                "pid": prop_data.id
            })
        else:
            padding_time = num // td_prop_max
            padding_last = num % td_prop_max
            arr = [{
                "num": td_prop_max,
                "image": prop_data.image,
                "pid": prop_data.id
            }] * padding_time
            if padding_last != 0:
                arr.append({
                    "num": padding_last,
                    "image": prop_data.image,
                    "pid": prop_data.id
                })
            data = data + arr
    # 保存当前用户已经使用的格子数量
    mongo.db.user_info_list.update_one({"sid":request.sid},{"$set":{"use_package_number":len(data)}})
    room = request.sid
    socketio.emit("user_prop_response", {
        "errno": status.CODE_OK,
        "errmsg": errmsg.ok,
        "data":data,
    }, namespace="/mofang",
                  room=room)


背包解锁

服务端提供背包解锁的API接口

  1. socket视图类 orchard/socket.py, 代码:
from flask_socketio import emit, Namespace, join_room
from flask import request

from application import code, message
from application.apps.users import services as user_services
from . import services

"""基于类视图接口"""

# 种植园模块命名空间
class OrchardNamespace(Namespace):
    '''种植园模块命名空间'''
    # socket链接
    def on_connect(self):
        print("用户[%s]进入了种植园!" % request.sid)
        # 返回初始化信息[不涉及当前的用户,因为我们现在不知道当前用户是谁]
        self.init_config()

    def init_config(self, ):
        """系统基本信息初始化"""
        # 返回背包初始配置信息
        package_settings = services.get_package_settings()
        emit("init_config_response", package_settings)

    # socket断开连接
    def on_disconnect(self):
        print("用户[%s]退出了种植园!" % request.sid)

    # 接收登录信息
    def on_login(self, data):
        # 加入房间
        self.on_join_room(data['uid']) # 接受客户端发送过来的用户 unique id

        # 1.用户websocket登录处理,获取用户初始信息(uid,sid,背包初始容量)
        # request.sid websocket链接客户端的会话ID
        user_info = user_services.user_websocket_login(data['uid'],request.sid)

        # todo 返回种植园的相关配置参数[种植果树的数量上限]

        msg = {
            'errno': code.CODE_OK,
            'errmsg': message.ok,
            'user_info': user_info
        }
        # 响应登录信息
        emit("login_response", msg)

        # 2.用户登录获取用户背包信息
        self.on_user_package()

    # 房间分发
    def on_join_room(self, room):
        '''加入房间'''
        # 默认用户自己一个人一个房间,也就是一个单独的频道
        join_room(room)

    # 获取用户背包信息
    def on_user_package(self):
        '''获取用户背包信息'''
        # 1.根据回话sid获取用户模型对象
        user = user_services.get_user_by_sid(request.sid)
        # 判断用户是否存在
        if user is None:
            # 响应数据
            emit('user_package_response',{
                'errno':code.CODE_USER_NOT_EXISTS,
                'errmsg': message.user_not_exists,
                'package_info': {}
            })
            # 停止程序继续运行
            return

        # 2.根据用户id获取背包信息
        package_info = user_services.get_package_info_by_id(user.id)
        # print("package_info =", package_info)
        # 响应数据
        emit('user_package_response',{
            'errno': code.CODE_OK,
            'errmsg': message.ok,
            'package_info': package_info
        })

    # 解锁背包格子
    def on_unlock_package(self):
        '''解锁背包格子'''
        # 1.根据回话sid获取用户模型对象 和 websocket用户信息对象
        user = user_services.get_user_by_sid(request.sid)
        user_info = user_services.get_user_info_by_sid(request.sid)
        # 判断用户是否存在
        if user is None:
            # 响应数据
            emit('unlock_package_response', {
                'errno': code.CODE_USER_NOT_EXISTS,
                'errmsg': message.user_not_exists,
            })
            # 停止程序继续运行
            return

        # 2.根据用户id获取背包信息
        package_info = user_services.get_package_info_by_id(user.id)

        # 3.获取用户背包初始配置信息
        package_settings = services.get_package_settings()

        # 4.判断用户是否满足激活背包格子的条件
        # 4.1 判断用户背包格子使用情况
        if package_info['capacity'] >= package_settings['max_package_capacity']:
            # 背包空间不足
            emit('unlock_package_response',{
                'errno': code.CODE_PACKAGE_SPACE_NOT_ENOUGH,
                'errmsg': message.package_space_not_enough
            })
            return

        # 4.2 # 是否有足够的积分可以激活[获取剩余可激活空间,查看本次第几次激活,剩余激活的次数]
        # 4.2.1 获取已经激活的次数?(现有格子 - 注册时初始数量)/ 每次激活数量 = 已激活次数
        # print('现有格子:', package_info['capacity'])
        # print('注册时初始数量:', user_info.package_number)
        # print('每次激活数量:', package_info['capacity'])
        this_time = (package_info['capacity'] - user_info.package_number) // package_settings['unlock_package_item']
        # 4.2.2 获取本次激活所需要的积分数量
        this_time_credit = package_settings['unlock_package'][this_time]
        # 4.2.3 判断用户积分是否足够
        if user.credit < this_time_credit:
            # 积分不足
            emit('unlock_package_response', {
                'errno': code.CODE_NOT_ENOUGH_CREDIT,
                'errmsg': message.not_enough_credit
            })
            return

        # 解锁背包格子 - 参数(用户对象, 解锁格子数量,需要的积分)
        user_services.unlock_package_num(user, package_settings["unlock_package_item"], this_time_credit)

        emit('unlock_package_response', {
            'errno': code.CODE_OK,
            'errmsg': message.ok
        })



  1. 用户蓝图下的数据服务层处理,services.users_serivces代码:
# 解锁背包格子
def unlock_package_num(user, num, credit=0):
    '''
    解锁背包格子
    :param user: 用户模型对象
    :param num: 本次要增加的格子数量
    :param credit: 增加背包所需要的积分,默认为0
    :return:
    '''
    # 更改用户背包信息,储存格子增加
    user_package = UserPackageDocument.objects.get(user_id=user.id)
    user_package.capacity = int(user_package.capacity) + int(num)
    user_package.save()

    # 用户积分更改
    user.credit = int(user.credit) - int(credit)
    db.session.commit()

  1. 更改重新刷新token值视图 users/api.py
# 验证用户是否携带 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)

    return {
        "errno": code.CODE_OK,
        "errmsg": message.ok,
        **token,
    }

客户端请求背包解锁

解锁背包过程:

package.html,在用户点击解锁背包时,发起全局事件通知,让orchard.html页面发起解锁请求。同时,在解锁成功以后,需要监听orchard.html发出的全局事件通知,接收解锁后更新的背包信息。

  1. 背包页面, 发送解锁通知, 接收更新后的用户背包信息, html/package.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 add_friend package" id="app">
    <div class="box">
      <p class="title">我的背包</p>
      <img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
			<!-- 背包道具列表 -->
		  <div class="prop_list">
        <div class="item" @click='game.print(prop,1)' v-for='prop in user_package_info.props_list'>
          <img :src="`../static/images/${prop.prop_image}`" alt="">
          <span>{{prop.num}}</span>
        </div>

				<!-- 循环背包列表,调整样式 -->
				<!-- 已解锁背包格子 -->
				<div class="item" v-for='i in ( user_package_info.capacity - user_package_info.props_list.length)'></div>
				<!-- 未解锁背包格子 -->
				<div class="item lock" @click='unlock_package' v-for='i in (package_init_setting.max_package_capacity - user_package_info.capacity)'></div>
      </div>
    </div>
	</div>
	<script>
	apiready = function(){
    	Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
		new Vue({
			el:"#app",
			data(){
				return {
					user_id: "", // 当前登陆用户Id
					package_init_setting: {}, // 背包初始配置信息
					user_info: {}, // 用户登陆初始化化信息
					user_package_info: {}, // 用户背包信息
				}
			},

			created(){
				// 获取其他页面传递的参数信息
				this.get_page_params();
				// 监听事件
				this.listen();
			},
			methods:{
        back(){
          this.game.closeFrame();
        },

				// 监听事件
				listen(){
					// 监听用户背包是否更新
					this.listen_package_update();
				},

				// 监听用户背包是否更新
				listen_package_update(){
					api.addEventListener({
					    name: 'package_update'
					}, (ret, err)=>{
							this.game.print(ret,1)
						 	// 更新用户背包数据
							this.user_package_info = ret.value.user_package_info
					});

				},

				// 获取其他页面传递的参数信息
				get_page_params(){
					// 获取背包初始配置信息
					this.package_init_setting = api.pageParam.package_init_setting;
					// 获取用户登陆初始化信息
					this.user_info = api.pageParam.user_info;
					// 获取用户背包信息
					this.user_package_info = api.pageParam.user_package_info;
					// this.game.print(this.user_package_info,1)
				},

				// 解锁背包格子确认
				unlock_package(){
					api.confirm({
					    title: '确定要花费一定的积分来激活背包的容量码?',
					    buttons: ['确定', '取消']
					}, (ret, err)=>{
							if(ret.buttonIndex==1){
								// 发起解锁背包容量的全局广播
								this.game.sendEvent('unlock_package')
							}
					});

				},


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


  1. 种植园页面, 接收解锁背包容量的通知 , 发送请求更新用户背包信息, 以及重新刷新token值(用户积分发生改变), orchard.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/socket.io.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 orchard" id="app">
    <img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
    <div class="orchard-bg">
			<img src="../static/images/bg2.png">
			<img class="board_bg2" src="../static/images/board_bg2.png">
		</div>
    <img class="back" @click="back" src="../static/images/user_back.png" alt="">
    <div class="header">
			<div class="info">
				<div class="avatar" @click='to_user'>
					<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="62" :rounded="true"></v-avatar>
						<v-avatar v-else-if="user_data.nickname" :username="user_data.nickname" :size="62" :rounded="true"></v-avatar>
						<v-avatar v-else :username="user_data.id" :size="62" :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" @click='user_recharge'>
				<div class="balance">
					<p class="title"><img src="../static/images/money.png" alt="">钱包</p>
					<p class="num">{{game.number_format(user_data.money)}}</p>
				</div>
				<div class="balance">
					<p class="title"><img src="../static/images/integral.png" alt="">果子</p>
					<p class="num">{{game.number_format(user_data.credit)}}</p>
				</div>
			</div>
      <div class="menu-list">
        <div class="menu">
          <img src="../static/images/menu1.png" alt="">
          排行榜
        </div>
        <div class="menu">
          <img src="../static/images/menu2.png" alt="">
          签到有礼
        </div>
        <div class="menu">
          <img src="../static/images/menu3.png" alt="">
          道具商城
        </div>
        <div class="menu">
          <img src="../static/images/menu4.png" alt="">
          邮件中心
        </div>
      </div>
		</div>
		<div class="footer" >
      <ul class="menu-list">
        <li class="menu">新手</li>
        <li class="menu" @click='to_package'>背包</li>
        <li class="menu-center" @click="to_shop">商店</li>
        <li class="menu">消息</li>
        <li class="menu">好友</li>
      </ul>
    </div>
	</div>
	<script>
	apiready = function(){
		Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
		new Vue({
			el:"#app",
			data(){
				return {
					user_data:{},  // 当前用户信息
          music_play:true,
          namespace: '/orchard', // websocket命名空间
          socket: null, // websocket连接对象
					recharge_list: [], // 允许充值的金额列表
					package_init_setting: {}, // 背包初始配置信息
					user_info: {}, // 用户登陆初始化化信息
					user_package_info: {}, // 用户背包信息
				}
			},
      created(){
				// socket建立连接
				this.socket_connect();
        // 自动加载我的果园页面
				this.to_my_orchard();
				// 获取用户数据
				this.get_user_data()
				// 监听事件变化
				this.listen()
				// 获取充值金额列表
				this.get_recharge_list()
      },
			methods:{
				back(){
					this.game.openWin("root");
				},
				// 监听事件
				listen(){
					// 监听token更新的通知
					this.listen_update_token();
					// 监听是否购买道具成功的通知
					this.listen_buy_prop_success();
					// 监听是否使用道具成功的通知
					// this.listen_use_prop_success();
					// 监听解锁背包容量的通知
					this.listen_unlock_package();
				},

				// 监听token更新的通知
				listen_update_token(){
					api.addEventListener({
							name: 'update_token'
					}, (ret, err) => {
						// 更新用户数据
							this.get_user_data()
					});
				},

				// 监听是否购买道具成功的通知
				listen_buy_prop_success(){
					api.addEventListener({
					    name: 'buy_prop_success'
					}, (ret, err)=>{
							// 发送请求,更新用户背包数据
							this.socket.emit('user_package');
					});
				},

				// 监听解锁背包容量的通知
				listen_unlock_package(){
					api.addEventListener({
							name: 'unlock_package'
					}, (ret, err)=>{
						// 发送解锁背包的请求
						this.socket.emit('unlock_package');
						// 获取解锁背包响应数据
						this.socket.on('unlock_package_response',(response)=>{
							if(response.errno === 1000){
								// 更新用户背包数据
								this.socket.emit('user_package')
								// 用户积分改变,重新刷新token值
								this.refresh_user_token();
							}
						})
					});
				},

				// 用户积分改变,重新刷新token值
				refresh_user_token(){
					let self = this
					self.game.check_user_login(self, ()=>{
						let token = self.game.getdata('refresh_token') || self.game.getfs('refresh_token')
						self.game.post(self,{
							'method': 'Users.refresh',
							'params':{},
							'header':{
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 刷新token值
									let token = self.game.getfs("refresh_token");
	                if(token){
	                    // 记住密码的情况
	                    self.game.deldata(["access_token","refresh_token"]);
	                    self.game.setfs({
	                        "access_token": data.result.access_token,
	                        "refresh_token": data.result.refresh_token,
	                    });
	                }else{
	                    // 不记住密码的情况
	                    self.game.delfs(["access_token","refresh_token"]);
	                    self.game.setdata({
	                        "access_token": data.result.access_token,
	                        "refresh_token": data.result.refresh_token,
	                    });
	                }
									// 发布全局广播,token更新
									self.game.sendEvent('update_token')
								}
							}
						})
					})
				},


				// 通过token值获取用户数据
				get_user_data(){
					let self = this;
					// 检查token是否过期,过期从新刷新token
					self.game.check_user_login(self, ()=>{
						// 获取token
						let token = this.game.getfs('access_token') || this.game.getdata('access_token')
						// 根据token获取用户数据
						this.user_data = this.game.get_user_by_token(token)
					})
				},

				// websocket连接处理
        socket_connect(){
          this.socket = io.connect(this.game.config.SOCKET_SERVER + this.namespace, {transports: ['websocket']});
          this.socket.on('connect', ()=>{
              this.game.print("开始连接服务端");
							// 获取背包初始配置信息
							this.get_package_setting()
							// websocket登陆处理
							this.user_websocket_login()
          });
        },

				// 获取背包初始配置信息
				get_package_setting(){
					this.socket.on('init_config_response', (response)=>{
						// this.game.print(response,1)
						this.package_init_setting = response
					})
				},

				// websocket登陆处理
				user_websocket_login(){
					// 客户端发送用户登陆请求
					this.socket.emit('login',{'uid': this.user_data.unique_id});
					// 接收登陆响应
					this.login_response();
					// 接收用户背包响应
					this.user_package_response();
				},

				// 接收登陆初始化信息
				login_response(){
					this.socket.on('login_response',(response)=>{
						// this.game.print(response,1)
						if(response.errno === 1000){
							this.user_info = response.user_info
						}
					})
				},

				// 接收用户背包信息
				user_package_response(){
					this.socket.on('user_package_response',(response)=>{
						// this.game.print(response,1)
						if(response.errno === 1000){
							this.user_package_info = response.package_info;
							// 全局广播用户背包更新
							this.game.sendEvent('package_update',{
								user_package_info: this.user_package_info
							})
						}else {
							this.game.tips(response.errmsg)
						}
					})
				},


        // 跳转我的果园页面
        to_my_orchard(){
          this.game.check_user_login(this, ()=>{
            this.game.openFrame("my_orchard","my_orchard.html","from_right",{
    					marginTop: 174,     //相对父页面上外边距的距离,数字类型
    					marginLeft: 0,      //相对父页面左外边距的距离,数字类型
    			    marginBottom: 54,     //相对父页面下外边距的距离,数字类型
    			    marginRight: 0     //相对父页面右外边距的距离,数字类型
    				});
          })
        },

        // 点击商店打开道具商城页面
				to_shop(){
          this.game.check_user_login(this, ()=>{
            this.game.openFrame('shop', 'shop.html', 'from_top');
          })
        },

				// 点击头像,跳转用户中心页面
				to_user(){
					this.game.check_user_login(this, ()=>{
            this.game.openFrame('user', 'user.html', 'from_right');
          })
				},

				// 获取充值金额列表
				get_recharge_list(){
					let self = this;
					self.game.check_user_login(self,()=>{
						self.game.post(self,{
							'method': 'Users.recharge_list',
							'params': {},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									self.recharge_list = data.result.recharge_list
								}
							}
						})
					})
				},

				// 点击钱包进行用户充值,设置充值金额
				user_recharge(){
					api.actionSheet({
								title: '余额充值',
								cancelTitle: '取消',
								buttons: this.recharge_list
						}, (ret, err)=>{
								if( ret.buttonIndex <= this.recharge_list.length ){
										 // 充值金额
										 let money = this.recharge_list[ret.buttonIndex-1];
										//  this.game.print(money,1);
										// 发送支付宝充值请求
										this.recharge_app_pay(money)
								}
					});
				},

				// 发送支付宝充值请求
				recharge_app_pay(money){
					// 获取支付宝支付对象
					let aliPayPlus = api.require("aliPayPlus");
					let self = this;
					// 向服务端发送请求获取终止订单信息
					self.game.check_user_login(self, ()=>{
						let token = this.game.getdata("access_token") || this.game.getfs("access_token");
						self.game.post(self,{
							'method': 'Users.recharge',
							'params':{
								'money': money,
							},
							'header': {
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 本次充值的订单参数
									let order_string = data.result.order_string;

									// 支付完成以后,支付APP返回的响应状态码
									let resultCode = {
										"9000": "支付成功!",
										"8000": "支付正在处理中,请稍候!",
										"4000": "支付失败!请联系我们的工作人员~",
										"5000": "支付失败,重复的支付操作",
										"6002": "网络连接出错",
										"6004": "支付正在处理中,请稍后",
									}
									// 唤醒支付宝APP,发起支付
									aliPayPlus.payOrder({
										orderInfo: order_string,
										sandbox: data.result.sandbox, // 将来APP上线需要修改成false
									},(ret, err)=>{
										if(resultCode[ret.code]){
											// 提示支付结果
											if(ret.code != 9000){
												self.game.tips(resultCode[ret.code]);
											}else {
												// 支付成功,向服务端请求验证支付结果 - 参数订单号
												self.check_recharge_result(data.result.order_number);
											}
										}
									})
								}
							}
						})
					})
				},

				// 向服务端发送请求,校验充值是否成功
				check_recharge_result(order_number){
					let self = this;
					self.game.check_user_login(self, ()=>{
						let token = this.game.getdata("access_token") || this.game.getfs("access_token");
						self.game.post(self,{
							'method': 'Users.check_recharge_result',
							'params':{
								'order_number':order_number,
							},
							'header':{
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 充值成功
									self.game.tips('充值成功!')
									// 用户数据更改过,重新刷新token值
									let token = self.game.getfs("access_token");
									// 删除token值
									self.game.deldata(["access_token","refresh_token"]);
									self.game.delfs(["access_token","refresh_token"]);
									if(token){
                    // 记住密码的情况
                    self.game.setfs({
                      "access_token": data.result.access_token,
                      "refresh_token": data.result.refresh_token,
                    });
                  }else{
                    // 不记住密码的情况
                    self.game.setdata({
                      "access_token": data.result.access_token,
                      "refresh_token": data.result.refresh_token,
                    });
									}
									// 全局广播充值成功
									self.game.sendEvent('recharge_success')
									// 全局广播刷新token值
									self.game.sendEvent('update_token')
								}
							}
						})
					})
				},

				// 点击背包,跳转到背包页面,并传递背包初始配置信息
				to_package(){
					this.game.check_user_login(this, ()=>{
						this.game.openFrame('package', 'package.html', 'from_top',null, {
							'package_init_setting': this.package_init_setting,
							'user_package_info': this.user_package_info,
							'user_info': this.user_info
						})
					})
				},


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


背包中的道具详情

  1. 允许用户点击道具,打开详情。package.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 add_friend package" id="app">
    <div class="box">
      <p class="title">我的背包</p>
      <img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
			<!-- 背包道具列表 -->
		  <div class="prop_list">
        <div class="item" @click='to_prop(prop)' v-for='prop in user_package_info.props_list'>
          <img :src="`../static/images/${prop.prop_image}`" alt="">
          <span>{{prop.num}}</span>
        </div>

				<!-- 循环背包列表,调整样式 -->
				<!-- 已解锁背包格子 -->
				<div class="item" v-for='i in ( user_package_info.capacity - user_package_info.props_list.length)'></div>
				<!-- 未解锁背包格子 -->
				<div class="item lock" @click='unlock_package' v-for='i in (package_init_setting.max_package_capacity - user_package_info.capacity)'></div>
      </div>
    </div>
	</div>
	<script>
	apiready = function(){
    	Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
		new Vue({
			el:"#app",
			data(){
				return {
					user_id: "", // 当前登陆用户Id
					package_init_setting: {}, // 背包初始配置信息
					user_info: {}, // 用户登陆初始化化信息
					user_package_info: {}, // 用户背包信息
				}
			},

			created(){
				// 获取其他页面传递的参数信息
				this.get_page_params();
				// 监听事件
				this.listen();
			},
			methods:{
        back(){
          this.game.closeFrame();
        },

				// 监听事件
				listen(){
					// 监听用户背包是否更新
					this.listen_package_update();
				},

				// 监听用户背包是否更新
				listen_package_update(){
					api.addEventListener({
					    name: 'package_update'
					}, (ret, err)=>{
							// this.game.print(ret,1)
						 	// 更新用户背包数据
							this.user_package_info = ret.value.user_package_info
					});

				},

				// 获取其他页面传递的参数信息
				get_page_params(){
					// 获取背包初始配置信息
					this.package_init_setting = api.pageParam.package_init_setting;
					// 获取用户登陆初始化信息
					this.user_info = api.pageParam.user_info;
					// 获取用户背包信息
					this.user_package_info = api.pageParam.user_package_info;
					// this.game.print(this.user_package_info,1)
				},

				// 解锁背包格子确认
				unlock_package(){
					api.confirm({
					    title: '确定要花费一定的积分来激活背包的容量码?',
					    buttons: ['确定', '取消']
					}, (ret, err)=>{
							if(ret.buttonIndex==1){
								// 发起解锁背包容量的全局广播
								this.game.sendEvent('unlock_package')
							}
					});
				},

				// 点击背包道具,跳转道具详情页
				to_prop(prop){
					this.game.print(prop)
					this.game.openFrame('prop', 'prop.html', 'from_top', null, {
						'prop':prop,
					})
				},


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


  1. 道具详情页, 可以购买道具和使用道具prop.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 item" id="app">
    <div class="box">
      <p class="title">{{item.prop_name}}</p>
      <img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
      <div class="content">
				<img class="invite_code item_img" :src="`../static/images/${item.prop_image}`" alt="">
			</div>
			<div class="invite_tips item_remark">
        <p>{{item.prop_remark}}</p><br>
        <p>
          <span>库存:{{item.num}}</span>
          &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
          <span>道具类型:{{prop_type_tips[item.type]}}</span>
        </p><br><br>
        <p>
					<span @click="gotouse">立即使用</span>
					&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
					<span @click="gotopay">继续购买</span>
				</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 {
          item: {},
          type: "",
					prop_type_tips:{
						"seed": "种子",
						"food": "宠物粮",
						"plant": "种植",
						"pet": "宠物",
					},
          buy_type: ['price', 'credit'], // 金钱或积分购买
				}
			},
      created(){
        // 接受传递过来的参数
        this.get_page_params();
      },
			methods:{
        back(){
          this.game.closeFrame();
        },
        // 接受传递过来的参数
        get_page_params(){
          // 接受来自商品列表页面的参数
          this.item = api.pageParam.prop;
          this.type = api.pageParam.prop.type;
        },
				gotouse(){
					// 使用道具

				},

        // 点击购买道具
        gotopay(){
					let self = this
          let buttons = [1,2,5,10,20,50];
          api.actionSheet({
              title: `购买${this.item.prop_name}的数量`,
              cancelTitle: '取消',
              buttons: buttons,
          }, (ret, err)=>{
              if(ret.buttonIndex<=buttons.length){
                let buy_num = buttons[ret.buttonIndex-1];
                // this.game.print(buy_num,1);
								api.actionSheet({
								    title: '请选择购买方式!',
								    cancelTitle: '取消购买',
								    buttons: ['使用金钱购买','使用果子购买']
								}, (ret, err)=>{
										// 金钱是1, 果子是2
								    if(ret.buttonIndex<=buttons.length){
											// self.game.print(self.buy_type[ret.buttonIndex -1])
											// 向服务端发送购买道具请求
											self.pay_prop(buy_num, self.buy_type[ret.buttonIndex -1])
										}
								});
              }
          });
        },

				// 向服务端发送购买道具请求
				pay_prop(buy_num, buy_type){
					let self = this;
					self.game.check_user_login(self, ()=>{
						let token = self.game.getdata('access_token') || self.game.getfs('access_token')
						self.game.post(self,{
							'method': 'Orchard.pay_props',
							'params':{
								'prop_num': buy_num,
								'prop_type': self.type,
								'prop_id': self.item.prop_id,
								'buy_type': buy_type,
							},
							'header':{
								'Authorization': 'jwt ' + token
							},
							success(response){
								let data = response.data;
								if(data.result && data.result.errno === 1000){
									// 刷新token值
									let token = self.game.getfs("access_token");
                  if(token){
                      // 记住密码的情况
                      self.game.deldata(["access_token","refresh_token"]);
                      self.game.setfs({
                          "access_token": data.result.access_token,
                          "refresh_token": data.result.refresh_token,
                      });
                  }else{
                      // 不记住密码的情况
                      self.game.delfs(["access_token","refresh_token"]);
                      self.game.setdata({
                          "access_token": data.result.access_token,
                          "refresh_token": data.result.refresh_token,
                      });
                  }
									// 发布全局广播,token更新
									self.game.sendEvent('buy_prop_success')
									self.game.sendEvent('update_token')
									self.game.tips('购买道具成功~')
									// 关闭页面
									self.game.closeFrame()
								}
							}
						})
					})
				},



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


Celery进阶使用

我们继续往下开发,则会面临道具的使用问题。道具有宠物,果树,宠物粮等等,这些道具有些需要进行异步计时的,所以我们接下来会比较常用到celery这个异步任务框架。因为前面我们已经在flask中已经安装了协程模块eventlet,所以接下来让celery基于协程模式以守护进程的模式飞奔启动的,接下来我们后台运行celery。

# 当前命令必须在manage.py所在目录中运行,也就是还是原来项目根目录!!!
celery -A manage.celery worker -P eventlet -l info --logfile="./logs/celery.log" -n worker1
celery -A manage.celery worker -P eventlet -l info --logfile="./logs/celery.log" -n worker2
celery -A manage.celery worker -P eventlet -l info --logfile="./logs/celery.log" -n worker3
celery -A manage.celery worker -P eventlet -l info --logfile="./logs/celery.log" -n worker4

# 如果使用守护进程启动celery出现无限连接RabbitMQ的情况则把版本升级到celery5.0.0即可。
# 以守护进程模式在后台运行,当然,这仅仅在线下开发使用,项目正式运营部署时,牢记使用supervisor这样稳定的进程管理器来运行。
# celery_start.sh  	# 建立开启守护进程文件
# celery_stop.sh	# 建立关闭守护进程文件
# sudo chmod +x celery_st* 	# 给俩文件赋予执行权限

-- 开启守护进程文件内容 4给守护进程
#!/bin/bash
celery multi start worker -A manage.celery -P eventlet -E --pidfile="./logs/worker1.pid" --logfile="./logs/celery.log" -l info -n worker1
celery multi start worker -A manage.celery -P eventlet -E --pidfile="./logs/worker2.pid" --logfile="./logs/celery.log" -l info -n worker2
celery multi start worker -A manage.celery -P eventlet -E --pidfile="./logs/worker3.pid" --logfile="./logs/celery.log" -l info -n worker3
celery multi start worker -A manage.celery -P eventlet -E --pidfile="./logs/worker4.pid" --logfile="./logs/celery.log" -l info -n worker4

# 根据pid文件,关闭指定celrey进程
-- 关闭守护进程文件内容
#!/bin/bash
celery multi stop worker -A manage.celery --pidfile=./logs/worker1.pid  --logfile="./logs/celery.log" -l info
celery multi stop worker -A manage.celery --pidfile=./logs/worker2.pid  --logfile="./logs/celery.log" -l info
celery multi stop worker -A manage.celery --pidfile=./logs/worker3.pid  --logfile="./logs/celery.log" -l info
celery multi stop worker -A manage.celery --pidfile=./logs/worker4.pid  --logfile="./logs/celery.log" -l info

# 以后肯定有定时任务需要启动调度器,所以新开一个终端,在同一目录下运行启动命令如下
celery multi start worker -A manage.celery beat --pidfile=./logs/beat.pid -l info --logfile="./logs/celery.beat.log"  -n beat
# 当然,现在没有定时任务,启动也没什么作用。

# 安装和启动celery任务监控器
pip install flower
celery flower --port=5555 --broker=redis://127.0.0.1:6379/15


supervisor启动celery

Supervisor是用Python开发的一套通用的进程管理程序,能将一个普通的命令行进程变为后台daemon,并监控进程状态,异常退出时能自动重启。

  1. 安装模块
pip install supervisor

  1. 初始化配置
# 在项目根目录下创建存储supervisor配置目录
mkdir -p scripts && cd scripts
# 生成初始化supervisor核心配置文件
echo_supervisord_conf > supervisord.conf
# 可以通过 ls 查看scripts下是否多了supervisord.conf这个文件,表示初始化配置生成了。
# 在编辑器中打开supervisord.conf,并去掉最后一行的注释分号。
# 修改如下,表示让supervisor自动加载当前supervisord.conf所在目录下所有ini配置文件


打开supervisord.conf文件,主要修改文件中的39, 40,75关,76,169,170行去掉左边注释,其中170修改成当前目录。配置代码:

; Sample supervisor config file.
;
; For more information on the config file, please see:
; http://supervisord.org/configuration.html
;
; Notes:
;  - Shell expansion ("~" or "$HOME") is not supported.  Environment
;    variables can be expanded using this syntax: "%(ENV_HOME)s".
;  - Quotes around values are not supported, except in the case of
;    the environment= options as shown below.
;  - Comments must have a leading space: "a=b ;comment" not "a=b;comment".
;  - Command will be truncated if it looks like a config file comment, e.g.
;    "command=bash -c 'foo ; bar'" will truncate to "command=bash -c 'foo ".
;
; Warning:
;  Paths throughout this example file use /tmp because it is available on most
;  systems.  You will likely need to change these to locations more appropriate
;  for your system.  Some systems periodically delete older files in /tmp.
;  Notably, if the socket file defined in the [unix_http_server] section below
;  is deleted, supervisorctl will be unable to connect to supervisord.

[unix_http_server]
file=/tmp/supervisor.sock   ; the path to the socket file
;chmod=0700                 ; socket file mode (default 0700)
;chown=nobody:nogroup       ; socket file uid:gid owner
;username=user              ; default is no username (open server)
;password=123               ; default is no password (open server)

; Security Warning:
;  The inet HTTP server is not enabled by default.  The inet HTTP server is
;  enabled by uncommenting the [inet_http_server] section below.  The inet
;  HTTP server is intended for use within a trusted environment only.  It
;  should only be bound to localhost or only accessible from within an
;  isolated, trusted network.  The inet HTTP server does not support any
;  form of encryption.  The inet HTTP server does not use authentication
;  by default (see the username= and password= options to add authentication).
;  Never expose the inet HTTP server to the public internet.

[inet_http_server]         ; inet (TCP) server disabled by default
port=127.0.0.1:9001        ; ip_address:port specifier, *:port for all iface
;username=user              ; default is no username (open server)
;password=123               ; default is no password (open server)

[supervisord]
logfile=/tmp/supervisord.log ; main log file; default $CWD/supervisord.log
logfile_maxbytes=50MB        ; max main logfile bytes b4 rotation; default 50MB
logfile_backups=10           ; # of main logfile backups; 0 means none, default 10
loglevel=info                ; log level; default info; others: debug,warn,trace
pidfile=/tmp/supervisord.pid ; supervisord pidfile; default supervisord.pid
nodaemon=false               ; start in foreground if true; default false
silent=false                 ; no logs to stdout if true; default false
minfds=1024                  ; min. avail startup file descriptors; default 1024
minprocs=200                 ; min. avail process descriptors;default 200
;umask=022                   ; process file creation umask; default 022
;user=supervisord            ; setuid to this UNIX account at startup; recommended if root
;identifier=supervisor       ; supervisord identifier, default is 'supervisor'
;directory=/tmp              ; default is not to cd during start
;nocleanup=true              ; don't clean up tempfiles at start; default false
;childlogdir=/tmp            ; 'AUTO' child log dir, default $TEMP
;environment=KEY="value"     ; key value pairs to add to environment
;strip_ansi=false            ; strip ansi escape codes in logs; def. false

; The rpcinterface:supervisor section must remain in the config file for
; RPC (supervisorctl/web interface) to work.  Additional interfaces may be
; added by defining them in separate [rpcinterface:x] sections.

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

; The supervisorctl section configures how supervisorctl will connect to
; supervisord.  configure it match the settings in either the unix_http_server
; or inet_http_server section.

[supervisorctl]
;serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket
serverurl=http://127.0.0.1:9001 ; use an http:// url to specify an inet socket
;username=chris              ; should be same as in [*_http_server] if set
;password=123                ; should be same as in [*_http_server] if set
;prompt=mysupervisor         ; cmd line prompt (default "supervisor")
;history_file=~/.sc_history  ; use readline history if available

; The sample program section below shows all possible program subsection values.
; Create one or more 'real' program: sections to be able to control them under
; supervisor.

;[program:theprogramname]
;command=/bin/cat              ; the program (relative uses PATH, can take args)
;process_name=%(program_name)s ; process_name expr (default %(program_name)s)
;numprocs=1                    ; number of processes copies to start (def 1)
;directory=/tmp                ; directory to cwd to before exec (def no cwd)
;umask=022                     ; umask for process (default None)
;priority=999                  ; the relative start priority (default 999)
;autostart=true                ; start at supervisord start (default: true)
;startsecs=1                   ; # of secs prog must stay up to be running (def. 1)
;startretries=3                ; max # of serial start failures when starting (default 3)
;autorestart=unexpected        ; when to restart if exited after running (def: unexpected)
;exitcodes=0                   ; 'expected' exit codes used with autorestart (default 0)
;stopsignal=QUIT               ; signal used to kill process (default TERM)
;stopwaitsecs=10               ; max num secs to wait b4 SIGKILL (default 10)
;stopasgroup=false             ; send stop signal to the UNIX process group (default false)
;killasgroup=false             ; SIGKILL the UNIX process group (def false)
;user=chrism                   ; setuid to this UNIX account to run the program
;redirect_stderr=true          ; redirect proc stderr to stdout (default false)
;stdout_logfile=/a/path        ; stdout log path, NONE for none; default AUTO
;stdout_logfile_maxbytes=1MB   ; max # logfile bytes b4 rotation (default 50MB)
;stdout_logfile_backups=10     ; # of stdout logfile backups (0 means none, default 10)
;stdout_capture_maxbytes=1MB   ; number of bytes in 'capturemode' (default 0)
;stdout_events_enabled=false   ; emit events on stdout writes (default false)
;stdout_syslog=false           ; send stdout to syslog with process name (default false)
;stderr_logfile=/a/path        ; stderr log path, NONE for none; default AUTO
;stderr_logfile_maxbytes=1MB   ; max # logfile bytes b4 rotation (default 50MB)
;stderr_logfile_backups=10     ; # of stderr logfile backups (0 means none, default 10)
;stderr_capture_maxbytes=1MB   ; number of bytes in 'capturemode' (default 0)
;stderr_events_enabled=false   ; emit events on stderr writes (default false)
;stderr_syslog=false           ; send stderr to syslog with process name (default false)
;environment=A="1",B="2"       ; process environment additions (def no adds)
;serverurl=AUTO                ; override serverurl computation (childutils)

; The sample eventlistener section below shows all possible eventlistener
; subsection values.  Create one or more 'real' eventlistener: sections to be
; able to handle event notifications sent by supervisord.

;[eventlistener:theeventlistenername]
;command=/bin/eventlistener    ; the program (relative uses PATH, can take args)
;process_name=%(program_name)s ; process_name expr (default %(program_name)s)
;numprocs=1                    ; number of processes copies to start (def 1)
;events=EVENT                  ; event notif. types to subscribe to (req'd)
;buffer_size=10                ; event buffer queue size (default 10)
;directory=/tmp                ; directory to cwd to before exec (def no cwd)
;umask=022                     ; umask for process (default None)
;priority=-1                   ; the relative start priority (default -1)
;autostart=true                ; start at supervisord start (default: true)
;startsecs=1                   ; # of secs prog must stay up to be running (def. 1)
;startretries=3                ; max # of serial start failures when starting (default 3)
;autorestart=unexpected        ; autorestart if exited after running (def: unexpected)
;exitcodes=0                   ; 'expected' exit codes used with autorestart (default 0)
;stopsignal=QUIT               ; signal used to kill process (default TERM)
;stopwaitsecs=10               ; max num secs to wait b4 SIGKILL (default 10)
;stopasgroup=false             ; send stop signal to the UNIX process group (default false)
;killasgroup=false             ; SIGKILL the UNIX process group (def false)
;user=chrism                   ; setuid to this UNIX account to run the program
;redirect_stderr=false         ; redirect_stderr=true is not allowed for eventlisteners
;stdout_logfile=/a/path        ; stdout log path, NONE for none; default AUTO
;stdout_logfile_maxbytes=1MB   ; max # logfile bytes b4 rotation (default 50MB)
;stdout_logfile_backups=10     ; # of stdout logfile backups (0 means none, default 10)
;stdout_events_enabled=false   ; emit events on stdout writes (default false)
;stdout_syslog=false           ; send stdout to syslog with process name (default false)
;stderr_logfile=/a/path        ; stderr log path, NONE for none; default AUTO
;stderr_logfile_maxbytes=1MB   ; max # logfile bytes b4 rotation (default 50MB)
;stderr_logfile_backups=10     ; # of stderr logfile backups (0 means none, default 10)
;stderr_events_enabled=false   ; emit events on stderr writes (default false)
;stderr_syslog=false           ; send stderr to syslog with process name (default false)
;environment=A="1",B="2"       ; process environment additions
;serverurl=AUTO                ; override serverurl computation (childutils)

; The sample group section below shows all possible group values.  Create one
; or more 'real' group: sections to create "heterogeneous" process groups.

;[group:thegroupname]
;programs=progname1,progname2  ; each refers to 'x' in [program:x] definitions
;priority=999                  ; the relative start priority (default 999)

; The [include] section can just contain the "files" setting.  This
; setting can list multiple files (separated by whitespace or
; newlines).  It can also contain wildcards.  The filenames are
; interpreted as relative to this file.  Included files *cannot*
; include files themselves.

[include]
files = *.ini


  1. 创建scripts/mofang_celery_worker.ini文件,启动我们项目worker任务队列
cd scripts
touch mofang_celery_worker1.ini

# 想要开启多进程任务,粘贴复制下面代码到每个文件中
touch mofang_celery_worker2.ini
touch mofang_celery_worker3.ini
touch mofang_celery_worker4.ini

[program:mofang_celery_worker1]
# 启动命令
command=/home/moluo/anaconda3/envs/mofang/bin/celery worker -A manage.celery -P eventlet -l info -n worker1
# 项目根目录的绝对路径,通过pwd查看
directory=/home/moluo/mofangapi
# 项目虚拟环境
environment=PATH="/home/moluo/anaconda3/envs/mofang/bin"
# 输出日志绝对路径
stdout_logfile=/home/moluo/mofangapi/logs/celery.worker.info.log
# 错误日志绝对路径
stderr_logfile=/home/moluo/mofangapi/logs/celery.worker.error.log
# 自动启动
autostart=true
# 重启
autorestart=true
# 进程启动后跑了几秒钟,才被认定为成功启动,默认1
startsecs=10
# 进程结束后60秒才被认定结束
stopwatisecs=60
# 优先级
priority=997


  1. 创建scripts/mofang_celery_beat.ini文件,来触发我们的beat定时任务
cd scripts
touch mofang_celery_beat.ini


[program:mofang_celery_beat]
command=/home/moluo/anaconda3/envs/mofang/bin/celery -A manage.celery beat -l info
directory=/home/moluo/mofangapi
environment=PATH="/home/moluo/anaconda3/envs/mofang/bin"
stdout_logfile=/home/moluo/mofangapi/logs/celery.beat.info.log
stderr_logfile=/home/moluo/mofangapi/logs/celery.beat.error.log
autostart=true
autorestart=true
startsecs=10
stopwaitsecs=60
priority=998


  1. 创建scripts/mofang_celery_flower.ini文件,来启动我们的celery监控管理工具
cd scripts
touch mofang_celery_flower.ini


[program:mofang_celery_flower]
command=/home/moluo/anaconda3/envs/mofang/bin/celery flower --port=5555 --broker=redis://127.0.0.1:6379/15
directory=/home/moluo/mofangapi
environment=PATH="/home/moluo/anaconda3/envs/mofang/bin"
stdout_logfile=/home/moluo/mofangapi/logs/celery.flower.info.log
stderr_logfile=/home/moluo/mofangapi/logs/celery.flower.error.log
autostart=true
autorestart=true
startsecs=10
stopwaitsecs=60
priority=990


启动supervisor

启动supervisor,确保此时你在项目路径下

cd ../
supervisord -c scripts/supervisord.conf


启动supervisorctl命令行,管理上面的celery的运行。

# 重新加载配置信息
supervisorctl reload


常用操作

# 停止某一个进程,program 就是进程名称,在ini文件首行定义的[program:mofang_celery_flower] 里的 :的名称
supervisorctl stop program

supervisorctl start program  # 启动某个进程
supervisorctl restart program  # 重启某个进程
supervisorctl stop groupworker:  # 结束所有属于名为 groupworker 这个分组的进程 (start,restart 同理)
supervisorctl stop groupworker:name1  # 结束 groupworker:name1 这个进程 (start,restart 同理)
supervisorctl stop all  # 停止全部进程,注:start、restartUnlinking stale socket /tmp/supervisor.sock、stop 都不会载入最新的配置文件
supervisorctl reload  # 载入最新的配置文件,停止原有进程并按新的配置启动、管理所有进程
supervisorctl update  # 根据最新的配置文件,启动新配置或有改动的进程,配置没有改动的进程不会受影响而重启

# 查看supervisor是否启动
ps aux | grep supervisord


设置开机自启

把supervisor注册到ubuntu系统服务中并设置开机自启

cd scripts
touch supervisor.service


supervisor.service,配置内容,并保存。

[Unit]
Description=supervisor
After=network.target

[Service]
Type=forking
ExecStart=/home/moluo/anaconda3/envs/mofang/bin/supervisord -c /home/moluo/mofangapi/scripts/supervisord.conf
ExecStop=/home/moluo/anaconda3/envs/mofang/bin/supervisorctl $OPTIONS shutdown
ExecReload=/home/moluo/anaconda3/envs/mofang/bin/supervisorctl $OPTIONS reload
KillMode=process
Restart=on-failure
RestartSec=42s

[Install]
WantedBy=multi-user.target


设置开机自启

# 赋予权限
chmod 766 supervisor.service
# 复制到系统开启服务目录下
sudo cp supervisor.service /lib/systemd/system/
# 设置允许开机自启
systemctl enable supervisor.service
# 判断是否已经设置为开机自启了
systemctl is-enabled  supervisor.service
# 通过systemctl查看supervisor运行状态
systemctl status  supervisor.service
systemctl start  supervisor.service
systemctl stop  supervisor.service


解决Celery不能运行在Root用户下的错误。application/__init__.py,代码:

from celery import Celery,platforms
# celery初始化
celery = Celery()
# 允许使用超级管理员启动Celery
platforms.C_FORCE_ROOT = True


posted @ 2021-07-01 10:07  十九分快乐  阅读(264)  评论(1编辑  收藏  举报