11. 种植园模块 - websocket通信 - 页面展示道具购买
种植园
我们需要完成的种植园,是一个互动频繁,并且要求有一定即时性,需要支持服务端推动推送数据的功能模块,所以如果继续基于http协议开发,那么需要通过ajax发送大量http请求,同时因为http本身属于单向通信,所以服务端无法主动发送信息提供给客户端。所以对于客户端使用来说,非常不友好,所以我们需要基于websocket通信来完成这个模块的开发。当然如果我们服务端基于websocket实现通讯的同时,那么客户端必须也要使用websocket来实现通讯才能正常运作。
websocket协议
文档:https://tools.ietf.org/html/rfc6455
一直以来,HTTP是无状态、单向通信的网络协议,即客户端请求一次,服务器响应一次,默认情况下,只允许浏览器向服务器发出请求后,服务器才能返回相应的数据。如果想让服务器消息及时下发到客户端,需要采用类似于轮询的机制,大部分情况就是客户端通过定时器使用ajax频繁地向服务器发出请求。这样的做法效率很低,而且HTTP数据包头本身的字节量较大,浪费了大量带宽和服务器资源。
为了提高效率,HTML5推出了服务端推送技术:EventSource,webWorker,Websocket。
WebSocket是一种让客户端和服务器之间能进行全双工通信(full-duplex)的技术。它是HTML最新标准HTML5的一个协议规范,本质上是个基于TCP的应用层协议,它通过HTTP/HTTPS协议发送一条特殊的握手请求进行握手后创建了一个TCP连接,此后浏览器/客户端和服务器之间便可随时随地以通过此连接来进行双向实时通信,且交换的数据包头信息量很小。
同时为了方便使用,HTML5提供了非常简单的操作就可以让前端开发者直接实现websocket通信,开发者只需要在支持WebSocket的浏览器中,创建webSocket对象之后,通过实现onopen、onmessage、onclose、onerror四个事件即可处理webSocket的请求响应。
注意:websocket是HTML5技术的一部分,但是websocket并非只能在浏览器或者HTML文档中才能使用,事实上在python或者C++等语言中只要能实现websocket协议报文,均可使用。
导读:https://blog.csdn.net/zhusongziye/article/details/80316127
客户端报文:
GET /mofang/websocket HTTP/1.1
Host: 127.0.0.1
Origin: http://127.0.0.1:5000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ== # Sec-WebSocket-Key 是基于SHA-1随机生成的
Sec-WebSocket-Version: 13
服务端报文:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= # 结合客户端提供的Sec-WebSocket-Key基于固定算法计算出来的
Sec-WebSocket-Protocol: chat
WebSocket与Socket的关系
他们两的关系就像Java和JavaScript,并非完全没有关系,只能说有点渊源。
Socket严格来说,其实并不是一个协议,而是为了方便开发者使用TCP或UDP协议而对TCP/IP协议进行封装出来的一组接口,是位于应用层和传输控制层之间的接口。通过Socket接口,我们可以更简单,更方便的使用TCP或UDP通信。
WebSocket是实现了浏览器与服务器的全双工通信协议,一个模拟Socket的新型应用层协议。
服务端基于socket提供服务
在python中实现socket服务端的方式有非常多,一种最常用的有python-socketio
,而我们现在使用的flask框架也有一个基于python-socket
模块进行了封装的flask-socketio
模块.
官方文档:https://flask-socketio.readthedocs.io/en/latest/
注意:
因为目前还有会存在一小部分的设备或者应用是不支持websocket的.所以为了保证功能的可用性,我们使用socktio模块,但是由此带来了2个问题,必须要注意的:
- python服务端使用基于socketio进行通信服务,则另一端必须也是基于socketio来进行对接通信,否则无法进行通信
- socketio在使用上还有一个版本对应的问题, 版本不对应则无法通信.回报版本错误.
如果使用了javascript版本socketio 1.x或者2.x版本,则python-socketio或者flask-socketio的版本必须是4.x
如果使用了javascript版本socketio 3.x版本, 则python-socketio或者flask-socketio的版本必须是5.x.
版本对照表:
服务端准备安装配置socketio
安装 :
pip install flask-socketio # 暂时最新版本5.1.0 - 2021.06.23
pip install -U flask-cors # 解决跨域问题
# 方案1
pip install eventlet # 网络并发,异步协程
# 猴子补丁实现异步操作
import eventlet
eventlet.monkey_patch()
# 方案2
# pip install gevent
# pip install gevent-websocket
# 猴子补丁实现异步操作
import eventlet
eventlet.monkey_patch()
我们当前使用的flask-socketio版本是5.x,所以记住javasctipt的socketio版本就必须是3.x.
- 项目入口文件模块初始化,
application/__init__.py
,代码:
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 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 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
# 终端脚本工具初始化
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')
# session储存配置初始化
session_store = Session()
# 自定义日志初始化
logger = Log()
# 初始化jsonrpc模块
jsonrpc = JSONRPC()
# 初始化随机生成数据模块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()
# 全局初始化
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)
# redis加载配置
redis_cache.init_app(app)
redis_check.init_app(app)
redis_session.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
# 自动注册蓝图
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
- 开发环境配置文件,
application/settings/dev.py
,代码:
'''socketio实现websocketio的配置'''
# 跨域访问,*允许所有访问
CORS_ALLOWED_ORIGINS="*"
# 异步模式
ASYNC_MODE="eventlet"
# ASYNC_MODE="gevent"
# 消息中间件队列
MESSAGE_QUEUE="redis://@127.0.0.1:6379/15"
- 原来的
mamage.run()
方法在启动项目时运行的是flask的app应用对象,现在要自定义终端命令覆盖原有的runserver运行项目命令,使用socketIO的run启动运行项目
自定义(runserver)终端命令application/utils/commands.py
,代码:
import sys
# 基于socketIO运行项目的终端命令
class SocketIOServer(Command):
"""
基于socketIO运行项目的终端命令
代码抄自父类Command文件中的 class Server(Command)
Runs the socketIO server i.e. socketio.run()
:param host: server host
:param port: server port
:param use_debugger: Flag whether to default to using the Werkzeug debugger.
This can be overriden in the command line
by passing the **-d** or **-D** flag.
Defaults to False, for security.
:param use_reloader: Flag whether to use the auto-reloader.
Default to True when debugging.
This can be overriden in the command line by
passing the **-r**/**-R** flag.
"""
# 终端调用别名
name = "runserver" # python manage.py runserver
# 描述信息
help = description = 'Runs the socketIO server i.e. socketio.run()'
def __init__(self, host='127.0.0.1', port=5000, use_debugger=None, use_reloader=None):
self.port = port
self.host = host
self.use_debugger = use_debugger
self.use_reloader = use_reloader if use_reloader is not None else use_debugger
def get_options(self):
options = (
Option('-h', '--host',
dest='host',
default=self.host),
Option('-p', '--port',
dest='port',
type=int,
default=self.port),
Option('-d', '--debug',
action='store_true',
dest='use_debugger',
help='enable the Werkzeug debugger (DO NOT use in production code)',
default=self.use_debugger),
Option('-D', '--no-debug',
action='store_false',
dest='use_debugger',
help='disable the Werkzeug debugger',
default=self.use_debugger),
Option('-r', '--reload',
action='store_true',
dest='use_reloader',
help='monitor Python files for changes (not 100%% safe for production use)',
default=self.use_reloader),
Option('-R', '--no-reload',
action='store_false',
dest='use_reloader',
help='do not monitor Python files for changes',
default=self.use_reloader),
)
return options
def __call__(self, app, host, port, use_debugger, use_reloader):
# we don't need to run the server in request context
# so just run it directly
if use_debugger is None:
use_debugger = app.debug
if use_debugger is None:
use_debugger = True
if sys.stderr.isatty():
print("Debugging is on. DANGER: Do not allow random users to connect to this server.", file=sys.stderr)
if use_reloader is None:
use_reloader = use_debugger
# 使用socketio运行项目
app.socketio.run(
app=app,
host=host,
port=port,
debug=use_debugger,
use_reloader=use_reloader,
)
- 在加载蓝图的过程中,自动加载每一个子应用目录下的socket.py里面的api代码,代码:
注册蓝图应用 ; application/utils/blueprint.py
,
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"])
# 获取urls模块下蓝图路由列表urlpatterns
urlpatterns = [] # 防止下边调用循环报错
try:
urlpatterns = urls_module.urlpatterns
except Exception:
pass
# 提取蓝图路由的前缀
url_prefix = item["url_prefix"]
# 获取urls模块下rpc接口列表apipatterns
apipatterns = [] # 防止下边调用循环报错
try:
apipatterns = urls_module.apipatterns
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'])
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
客户端准备
因为我们是基于falsk-socketio
模块提供的服务端,所以客户端必须基于socketIO.js
才能与其进行通信,所以客户端引入socketio.js。
socket.io.js的官方文档: https://socket.io/docs/v3
socket.io.js的github: https://github.com/socketio/socket.io/releases
下载Source code(zip) 中有socket.io.js
和socket.io.min.js
放到客户端静态文件static/js/
目录下
通信测试
服务端提供socket接口
- 服务端创建一个种植园模块的蓝图,叫orchard,并注册到项目,终端命令如下:
cd application/apps/
python ../../manage.py blue -n=orchard
- 蓝图总路由
application/urls.py
,代码:
from application import include
# 蓝图子路由列表
urlpatterns = [
include('','home.urls'), # 公共蓝图
include('/users','users.urls'), # 用户
include('/orchard','orchard.urls'), # 种植园
]
- 注册蓝图应用:
applicaion/settings/dev.py
,代码:
"""待注册的蓝图应用路径列表"""
INSTALL_BLUEPRINT = [
'application.apps.users', # 用户
'application.apps.home', # 公共蓝图
'application.apps.orchard', # 种植园蓝图
]
- 服务端提供socket接口,
orchard/secket.py
:
from flask_socketio import emit, Namespace
from flask import request
from application import socketio
"""基于函数视图接口"""
# # socket链接
# @socketio.on('connect', namespace='/mofang')
# def user_connect():
# # request.sid socketIO基于客户端生成的唯一会话ID session_id
# print(f'用户[{request.sid}]进入了种植园')
#
# # socket断开链接
# @socketio.on('disconnect', namespace='/mofang')
# def user_disconnect():
# # request.sid socketIO基于客户端生成的唯一会话ID session_id
# print(f'用户[{request.sid}]离开了种植园')
#
# # 服务端接收客户端自定义事件并响应
# @socketio.on('login', namespace='/mofang')
# def user_login(data):
# print(f'接收客户端发送的数据: {data}')
#
# # 服务端响应客户端数据
# msg = {
# 'username': 'jiayignhe',
# 'age': 15
# }
# emit('login_response', msg)
#
# # 可以连续项客户段发送数据
# msg['age'] = 16
# emit('login_response', msg)
# msg['age'] = 17
# emit('login_response', msg)
"""基于类视图接口"""
class UserNameSpace(Namespace):
'''on_事件'''
# socket链接
def on_connect(self):
# request.sid socketIO基于客户端生成的唯一会话ID session_id
print(f'用户[{request.sid}]进入了种植园')
# socket断开链接
def on_disconnect(self):
# request.sid socketIO基于客户端生成的唯一会话ID session_id
print(f'用户[{request.sid}]离开了种植园')
# 服务端接收客户端自定义事件并响应
def on_login(self,data):
print(f'接收客户端发送的数据: {data}')
# 服务端响应客户端数据
msg = {
'username': 'jiayignhe',
'age': 15
}
emit('login_response', msg)
# 可以连续项客户段发送数据
msg['age'] = 16
emit('login_response', msg)
msg['age'] = 17
emit('login_response', msg)
# 注册类视图(socket)
socketio.on_namespace(UserNameSpace(namespace='/mofang'))
客户端测试
客户端原始代码
测试代码:socketio_ceshi.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<!-- 引入socketio文件 -->
<script src="../static/js/socket.io.js"></script>
</head>
<body>
<script>
// 命名空间
namespace = '/mofang';
server_url = 'ws://192.168.19.46:5000'; // websocket的服务端地址
// 建立连接对象
var socket = io.connect(server_url + namespace, {transports: ['websocket']});
// 接收服务端事件信息
socket.on("disconnect",()=>{
alert("断开连接了");
});
socket.on('connect',()=>{
alert("客户端连接socket服务端");
// 向服务端提交数据("事件名称",数据)
socket.emit("login",{"uid":1000});
});
socket.on("login_response",(data)=>{
alert("登陆响应!!");
alert(JSON.stringify(data));
});
</script>
</body>
</html>
客户端vue结合socketio
- 客户端,
html/socket_vue.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/main.js"></script>
<script src="../static/js/v-avatar-2.0.3.min.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>
<script>
apiready = function(){
Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
new Vue({
el:"#app",
data(){
return {
user_info:{nickname:"好听的昵称"},
music_play:true,
namespace: '/mofang',
socket: null,
}
},
created(){
// socket建立连接
this.socket_connect();
},
methods:{
// socket连接
socket_connect(){
this.socket = io.connect(this.game.config.SOCKET_SERVER + this.namespace, {transports: ['websocket']});
this.socket.on('connect', ()=>{
this.game.print("开始连接服务端", 1);
});
},
back(){
this.game.openWin("root");
},
}
});
}
</script>
</body>
</html>
- 添加css样式,
static/css/main.css
,代码:
.app .orchard-bg{
margin: 0 auto;
width: 100%;
max-width: 100rem;
position: absolute;;
z-index: -1;
top: -6rem;
}
.app .orchard-bg .board_bg2{
position: absolute;
top: 1rem;
}
.orchard .back{
position: absolute;
width: 3.83rem;
height: 3.89rem;
z-index: 1;
top: 2rem;
left: 2rem;
}
.orchard .music{
right: 2rem;
}
.orchard .header{
position: absolute;
top: 0rem;
left: 0;
right: 0;
margin: auto;
width: 32rem;
height: 19.28rem;
}
.orchard .info{
position: absolute;
z-index: 1;
top: 0rem;
left: 4.4rem;
width: 8rem;
height: 9.17rem;
}
.orchard .info .avata{
width: 8rem;
height: 8rem;
position: relative;
}
.orchard .info .avatar_bf{
position: absolute;
z-index: 1;
margin: auto;
width: 6rem;
height: 6rem;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.orchard .info .user_avatar{
position: absolute;
z-index: 1;
width: 6rem;
height: 6rem;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
border-radius: 1rem;
}
.orchard .info .avatar_border{
position: absolute;
z-index: 1;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 7.2rem;
height: 7.2rem;
}
.orchard .info .user_name{
position: absolute;
left: 8rem;
top: 1rem;
width: 11rem;
height: 3rem;
line-height: 3rem;
font-size: 1.5rem;
text-shadow: 1px 1px 1px #aaa;
border-radius: 3rem;
background: #ff9900;
text-align: center;
}
.orchard .wallet{
position: absolute;
top: 3.4rem;
right: 4rem;
width: 16rem;
height: 10rem;
}
.orchard .wallet .balance{
margin-top: 1.4rem;
float: left;
margin-right: 1rem;
}
.orchard .wallet .title{
color: #fff;
font-size: 1.2rem;
width: 6.4rem;
text-align: center;
}
.orchard .wallet .title img{
width: 1.4rem;
margin-right: 0.2rem;
vertical-align: sub;
height: 1.4rem;
}
.orchard .wallet .num{
background: url("../images/btn3.png") no-repeat 0 0;
background-size: 100%;
width: 6.4rem;
font-size: 0.8rem;
color: #fff;
height: 2rem;
line-height: 1.8rem;
text-indent: 1rem;
}
.orchard .header .menu-list{
position: absolute;
top: 9rem;
left: 2rem;
}
.orchard .header .menu-list .menu{
color: #fff;
font-size: 1rem;
float: left;
width: 4rem;
height: 4rem;
text-align: center;
margin-right: 2rem;
}
.orchard .header .menu-list .menu img{
width: 3.33rem;
height: 3.61rem;
display: block;
margin: auto;
margin-bottom: 0.4rem;
}
.orchard .footer{
position: absolute;
width: 100%;
height: 6rem;
bottom: -2rem;
background: url("../images/board_bg2.png") no-repeat -1rem 0;
background-size: 110%;
}
.orchard .footer .menu-list{
width: 100%;
height: 4rem;
display: flex;
position: absolute;
top: -1rem;
}
.orchard .footer .menu-list .menu,
.orchard .footer .menu-list .menu-center{
float: left;
width: 4.44rem;
height: 5.2rem;
font-size: 1.5rem;
color: #fff;
line-height: 4.44rem;
text-align: center;
background: url("../images/btn5.png") no-repeat 0 0;
background-size: 100%;
flex: 1;
margin-left: 4px;
margin-right: 4px;
}
.orchard .footer .menu-list .menu-center{
background: url("../images/btn6.png") no-repeat 0 0;
background-size: 100%;
flex: 2;
}
- 在
static/js/main.js
封装的打开新窗口函数中, 把自动重新加载当前页面属性reload的值改成false,否则用户每次返回首页或进入种植园页面都会反复的建立websocket连接。
//创建窗口
openWin(name,url,pageParam){
if(!pageParam){
pageParam = {}
}
api.openWin({
name: name, // 自定义窗口名称,如果是新建窗口,则名字必须是第一次出现的名字。
bounces: false, // 窗口是否上下拉动
reload: false, // 如果窗口已经在之前被打开了,是否要重新加载当前窗口中的页面
url: url, // 窗口创建时展示的html页面的本地路径[相对于当前代码所在文件的路径]
animation:{ // 打开新建窗口时的过渡动画效果
type:"push", //动画类型(详见动画类型常量)
subType:"from_right", //动画子类型(详见动画子类型常量)
duration:300 //动画过渡时间,默认300毫秒
},
pageParam: pageParam,
});
}
基于事件处理信息
websocket是采用事件驱动机制建立的通信方式。
socketIO内置的公共事件:
connect:连接成功
disconnect:断开连接,
message:处理未定义事件
anything:处理自定义事件,一个自定义变量,代表事件名称,开发者自定义的
客户端:
connecting:正在连接
connect_failed:连接失败
error:错误发生,并且无法被其他事件类型所处理
reconnect_failed:重连失败
reconnect:成功重连
reconnecting:正在重连中
在业务逻辑编写的过程中,往往都是服务端如果主动发送消息给客户端,通常会提前声明另一个事件,用于监听客户端是否有回应消息。一般由socketIO提供用于发送数据的方法是: flask_socketio.emit
,flask_socketio.send
,用于监听接受数据的方法是on_事件名
。
同理,如果客户端主动发送消息给服务端,也会提前声明另一个事件,用于监听服务端是否有回应消息。
基于未定义事件进行通信
客户端代码:
<!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/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>
<script>
apiready = function(){
Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
new Vue({
el:"#app",
data(){
return {
music_play:true,
namespace: '/orchard', // 命名空间(路径)
socket: null, // socket连接对象
}
},
created(){
// socket连接
this.socket = io.connect(this.game.config.SOCKET_SERVER + this.namespace, {transports: ['websocket']});
this.connect();
},
methods:{
connect(){
this.socket.on('connect', ()=>{
this.game.print("开始连接websocket服务端");
// this.login(); // 发送登陆请求
// this.login_response(); // 用于接收服务端处理完成login事件以后的返回结果
this.undefined_event();
this.undefined_event_response();
});
this.socket.on("disconnect",()=>{
this.game.tips("服务器断开~正在重连....");
});
},
undefined_event(){
// 使用socketIO提供的未定义事件来完成前后端双向通讯
// 未定义事件: 表示不通过事件名称与服务端/客户端进行通信
// 本端可以通过send方法直接发送数据,不需要定义事件名称,数据格式支持json对象
// 对端通过on_message来接受数据.
this.socket.send({"username":"xiaocheng","password":"123456"});
},
undefined_event_response(){
this.socket.on("message", (data)=>{
// 接收来自服务端的未定义数据
this.game.print(data);
});
},
back(){
this.game.openWin("root");
},
}
});
}
</script>
</body>
</html>
服务端代码:
import flask_socketio
from application import socketio
from flask import request
from flask_socketio import emit,Namespace,send
class UserNamespace(Namespace):
"""用户的socket命名空间"""
def on_connect(self):
print("用户[%s]进入了种植园!" % request.sid)
def on_disconnect(self):
print("用户[%s]退出了种植园!" % request.sid)
def on_login(self,data):
print("登录中....%s" % data)
message = {
"user_name":"xiaoming",
"age":17,
}
emit("login_response",message)
message["age"] = 18
emit("login_response",message)
message["age"] = 19
emit("login_response",message)
message["age"] = 20
emit("login_response",message)
def on_message(self,data):
"""接受客户端的未定义事件"""
print("接受未定义事件参数:", data)
# 使用send发送未定义事件,并发送参数
send({"status":"登录成功~"})
socketio.on_namespace(UserNamespace(namespace='/orchard'))
基于自定义事件进行通信
客户端代码:
<!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/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>
<script>
apiready = function(){
Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
new Vue({
el:"#app",
data(){
return {
music_play:true,
namespace: '/orchard', // 命名空间(路径)
socket: null, // socket连接对象
}
},
created(){
// socket连接
this.socket = io.connect(this.game.config.SOCKET_SERVER + this.namespace, {transports: ['websocket']});
this.connect();
},
methods:{
connect(){
this.socket.on('connect', ()=>{
this.game.print("开始连接websocket服务端");
// this.login(); // 发送登陆请求
// this.login_response(); // 用于接收服务端处理完成login事件以后的返回结果
this.undefined_event();
this.undefined_event_response();
this.custom_event();
this.custom_event_response();
});
this.socket.on("disconnect",()=>{
this.game.tips("服务器断开~正在重连....");
});
},
undefined_event(){
// 使用socketIO提供的未定义事件来完成前后端双向通讯
// 未定义事件: 表示不通过事件名称与服务端/客户端进行通信
// 本端可以通过send方法直接发送数据,不需要定义事件名称,数据格式支持json对象
// 对端通过on_message来接受数据.
this.socket.send({"username":"xiaocheng","password":"123456"});
},
undefined_event_response(){
this.socket.on("message", (data)=>{
// 接收来自服务端的未定义数据
this.game.print(data);
});
},
custom_event(){
// 客户端自定义事件
this.socket.emit("login",{"username":"xiaocheng","password":"123456"});
},
custom_event_response(){
this.socket.on("login_response",(data)=>{
this.game.print(data);
});
},
back(){
this.game.openWin("root");
},
}
});
}
</script>
</body>
</html>
服务端代码:
import flask_socketio
from application import socketio
from flask import request
from flask_socketio import emit,Namespace,send
class UserNamespace(Namespace):
"""用户的socket命名空间"""
def on_connect(self):
print("用户[%s]进入了种植园!" % request.sid)
def on_disconnect(self):
print("用户[%s]退出了种植园!" % request.sid)
def on_login(self,data):
print("登录中....%s" % data)
message = {
"user_name":"xiaoming",
"age":17,
}
emit("login_response",message)
message["age"] = 18
emit("login_response",message)
message["age"] = 19
emit("login_response",message)
message["age"] = 20
emit("login_response",message)
def on_message(self,data):
"""接受客户端的未定义事件"""
print("接受未定义事件参数:", data)
send({"status":"登录成功~"})
socketio.on_namespace(UserNamespace(namespace='/orchard'))
服务端主动发送信息
socket.py,代码:
import flask_socketio
from application import socketio
from flask import request
from flask_socketio import emit,Namespace,send
# @socketio.on("connect", namespace="/mofang")
# def user_connect():
# # request.sid socketIO基于客户端生成的唯一会话ID session_id
# print("用户[%s]进入了种植园!" % request.sid)
#
# @socketio.on("disconnect", namespace="/mofang")
# def user_disconnect():
# print("用户[%s]退出了种植园" % request.sid )
#
#
# """服务端接受来自客户端的自定义事件"""
# @socketio.on("login", namespace="/mofang")
# def user_login(data):
# print("客户端发送数据: %s" % data)
# message = {
# "user_name":"xiaoming",
# "age":17,
# }
#
# emit("login_response", message)
# message["age"] = 18
# emit("login_response",message)
# message["age"] = 19
# emit("login_response",message)
# message["age"] = 20
# emit("login_response",message)
class UserNamespace(Namespace):
"""用户的socket命名空间"""
def on_connect(self):
print("用户[%s]进入了种植园!" % request.sid)
self.count()
def on_disconnect(self):
print("用户[%s]退出了种植园!" % request.sid)
def on_login(self,data):
print("登录中....%s" % data)
message = {
"user_name":"xiaoming",
"age":17,
}
emit("login_response",message)
message["age"] = 18
emit("login_response",message)
message["age"] = 19
emit("login_response",message)
message["age"] = 20
emit("login_response",message)
def on_message(self,data):
"""接受客户端的未定义事件"""
print("接受未定义事件参数:", data)
send({"status":"登录成功~"})
def count(self):
"""主动推送数据,总人数"""
from application.apps.users.models import User
count_people = User.query.count()
emit("count_response","当前魔方APP登录用户:%s" % count_people)
socketio.on_namespace(UserNamespace(namespace='/orchard'))
客户端接收响应信息,代码:
<!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/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>
<script>
apiready = function(){
Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
new Vue({
el:"#app",
data(){
return {
music_play:true,
namespace: '/orchard', // 命名空间(路径)
socket: null, // socket连接对象
}
},
created(){
// socket连接
this.socket = io.connect(this.game.config.SOCKET_SERVER + this.namespace, {transports: ['websocket']});
this.connect();
},
methods:{
connect(){
this.socket.on('connect', ()=>{
this.game.print("开始连接websocket服务端");
// this.login(); // 发送登陆请求
// this.login_response(); // 用于接收服务端处理完成login事件以后的返回结果
this.undefined_event();
this.undefined_event_response();
this.custom_event();
this.custom_event_response();
this.count_response();
});
this.socket.on("disconnect",()=>{
this.game.tips("服务器断开~正在重连....");
});
},
undefined_event(){
// 使用socketIO提供的未定义事件来完成前后端双向通讯
// 未定义事件: 表示不通过事件名称与服务端/客户端进行通信
// 本端可以通过send方法直接发送数据,不需要定义事件名称,数据格式支持json对象
// 对端通过on_message来接受数据.
this.socket.send({"username":"xiaocheng","password":"123456"});
},
undefined_event_response(){
this.socket.on("message", (data)=>{
// 接收来自服务端的未定义数据
this.game.print(data);
});
},
custom_event(){
// 客户端自定义事件
this.socket.emit("login",{"username":"xiaocheng","password":"123456"});
},
custom_event_response(){
this.socket.on("login_response",(data)=>{
this.game.print(data);
});
},
count_response(){
this.socket.on("count_response",(data)=>{
this.game.print(data,1);
});
},
back(){
this.game.openWin("root");
},
}
});
}
</script>
</body>
</html>
基于房间管理分发信息
import flask_socketio
from application import socketio
from flask import request
from flask_socketio import emit,Namespace,send,join_room
from application.apps.users.models import User
class UserNamespace(Namespace):
"""用户的socket命名空间"""
def on_connect(self):
print("用户[%s]进入了种植园!" % request.sid)
self.count()
def on_disconnect(self):
print("用户[%s]退出了种植园!" % request.sid)
def on_login(self,data):
print("登录中....%s" % data)
message = {
"user_name":"xiaoming",
"age":17,
}
emit("login_response",message)
message["age"] = 18
emit("login_response",message)
message["age"] = 19
emit("login_response",message)
message["age"] = 20
emit("login_response",message)
def on_message(self,data):
"""接受客户端的未定义事件"""
print("接受未定义事件参数:", data)
send({"status":"登录成功~"})
def count(self):
"""主动推送数据,总人数"""
from application.apps.users.models import User
count_people = User.query.count()
emit("count_response","当前魔方APP登录用户:%s" % count_people)
def on_join_room(self,data):
"""
大聪明, 测试数据: c36bb167-a94e-44b4-b12f-8743206cc4e8
焦秀英, 测试数据: 6e2cecc6-05f3-4b6f-b3f7-82914f95a7b4
陈丹丹,测试数据: d6fabb0a-3842-407b-9aca-73155257a796
"""
#把焦秀英和陈丹丹分一个房间
if data["uid"] == "c36bb167-a94e-44b4-b12f-8743206cc4e8":
room_id = '1'
else:
room_id = '2'
# 房间ID必须是字符串
join_room(room_id)
user = User.query.filter(User.unique_id==data["uid"]).first()
from application.apps.users.marshmallow import UserSchema
us = UserSchema()
user_info = us.dump(user)
# 发送给单个用户
print(user_info)
print(room_id)
emit("join_room_response", {"user_info": user_info, "room": room_id})
# 发送给指定房间的每一个用户
if room_id == '2':
# 房间ID必须是字符串
emit("toutoude",{"message":"你们这把当狼~"}, room=str(room_id))
socketio.on_namespace(UserNamespace(namespace='/orchard'))
客户端代码:
<!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/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>
<script>
apiready = function(){
Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
new Vue({
el:"#app",
data(){
return {
music_play:true,
namespace: '/orchard', // 命名空间(路径)
socket: null, // socket连接对象
}
},
created(){
// socket连接
this.socket = io.connect(this.game.config.SOCKET_SERVER + this.namespace, {transports: ['websocket']});
this.connect();
},
methods:{
connect(){
this.socket.on('connect', ()=>{
this.game.print("开始连接websocket服务端");
// this.login(); // 发送登陆请求
// this.login_response(); // 用于接收服务端处理完成login事件以后的返回结果
// // 未定义事件调用
// this.undefined_event();
// this.undefined_event_response();
// // 自定义事件调用
// this.custom_event();
// this.custom_event_response();
// // 服务端主动推送
// this.count_response();
// 进入不同的房间
this.join_room();
this.join_room_response();
});
this.socket.on("disconnect",()=>{
this.game.tips("服务器断开~正在重连....");
});
},
undefined_event(){
// 使用socketIO提供的未定义事件来完成前后端双向通讯
// 未定义事件: 表示不通过事件名称与服务端/客户端进行通信
// 本端可以通过send方法直接发送数据,不需要定义事件名称,数据格式支持json对象
// 对端通过on_message来接受数据.
this.socket.send({"username":"xiaocheng","password":"123456"});
},
undefined_event_response(){
this.socket.on("message", (data)=>{
// 接收来自服务端的未定义数据
this.game.print(data);
});
},
custom_event(){
// 客户端自定义事件
this.socket.emit("login",{"username":"xiaocheng","password":"123456"});
},
custom_event_response(){
this.socket.on("login_response",(data)=>{
this.game.print(data);
});
},
count_response(){
this.socket.on("count_response",(data)=>{
this.game.print(data,1);
});
},
join_room(){
// 进入不同房间
let token = this.game.getdata("access_token") || this.game.getfs("access_token");
let user = this.game.get_user_by_token(token);
var uid = user.unique_id;
this.socket.emit("join_room",{"uid":uid});
},
join_room_response(){
// 进入不同房间的服务端响应
this.socket.on("join_room_response", (data)=>{
this.game.print(data,1);
});
this.socket.on("toutoude",(data)=>{
this.game.print(data,1);
});
},
back(){
this.game.openWin("root");
},
}
});
}
</script>
</body>
</html>
服务端定时推送数据
socket.py,代码:
import flask_socketio
from application import socketio
from flask import request
from flask_socketio import emit,Namespace,send,join_room
from application.apps.users.models import User
from threading import Lock
import random
thread = None
thread_lock = Lock()
class UserNamespace(Namespace):
"""用户的socket命名空间"""
def on_connect(self):
print("用户[%s]进入了种植园!" % request.sid)
# 主动推送
# self.count()
# 定时主动推送
self.setInterval()
def on_disconnect(self):
print("用户[%s]退出了种植园!" % request.sid)
def on_login(self,data):
print("登录中....%s" % data)
message = {
"user_name":"xiaoming",
"age":17,
}
emit("login_response",message)
message["age"] = 18
emit("login_response",message)
message["age"] = 19
emit("login_response",message)
message["age"] = 20
emit("login_response",message)
def on_message(self,data):
"""接受客户端的未定义事件"""
print("接受未定义事件参数:", data)
send({"status":"登录成功~"})
def count(self):
"""主动推送数据,总人数"""
from application.apps.users.models import User
count_people = User.query.count()
emit("count_response","当前魔方APP登录用户:%s" % count_people)
def on_join_room(self,data):
"""
大聪明, 测试数据: c36bb167-a94e-44b4-b12f-8743206cc4e8
焦秀英, 测试数据: 6e2cecc6-05f3-4b6f-b3f7-82914f95a7b4
陈丹丹,测试数据: d6fabb0a-3842-407b-9aca-73155257a796
"""
#把焦秀英和陈丹丹分一个房间
if data["uid"] == "c36bb167-a94e-44b4-b12f-8743206cc4e8":
room_id = '1'
else:
room_id = '2'
# 房间ID必须是字符串
join_room(room_id)
user = User.query.filter(User.unique_id==data["uid"]).first()
from application.apps.users.marshmallow import UserSchema
us = UserSchema()
user_info = us.dump(user)
# 发送给单个用户
print(user_info)
print(room_id)
emit("join_room_response", {"user_info": user_info, "room": room_id})
# 发送给指定房间的每一个用户
if room_id == '2':
# 房间ID必须是字符串
emit("toutoude",{"message":"你们这把当狼~"}, room=str(room_id))
def setInterval(self):
global thread
with thread_lock:
if thread is None:
thread = socketio.start_background_task(target=self.background_thread,data=100)
def background_thread(self,data):
while True:
socketio.sleep(1) # socketio提供的异步
t = random.randint(1, 100) + data
print(t,data)
socketio.emit('server_response', {'data': t},namespace='/orchard')
socketio.on_namespace(UserNamespace(namespace='/orchard'))
客户端代码:
<!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/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>
<script>
apiready = function(){
Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
new Vue({
el:"#app",
data(){
return {
music_play:true,
namespace: '/orchard', // 命名空间(路径)
socket: null, // socket连接对象
}
},
created(){
// socket连接
this.socket = io.connect(this.game.config.SOCKET_SERVER + this.namespace, {transports: ['websocket']});
this.connect();
},
methods:{
connect(){
this.socket.on('connect', ()=>{
this.game.print("开始连接websocket服务端");
// this.login(); // 发送登陆请求
// this.login_response(); // 用于接收服务端处理完成login事件以后的返回结果
// // 未定义事件调用
// this.undefined_event();
// this.undefined_event_response();
// // 自定义事件调用
// this.custom_event();
// this.custom_event_response();
// // 服务端主动推送
// this.count_response();
// 进入不同的房间
// this.join_room();
// this.join_room_response();
// 服务器定时推送数据
this.server_response();
});
this.socket.on("disconnect",()=>{
this.game.tips("服务器断开~正在重连....");
});
},
undefined_event(){
// 使用socketIO提供的未定义事件来完成前后端双向通讯
// 未定义事件: 表示不通过事件名称与服务端/客户端进行通信
// 本端可以通过send方法直接发送数据,不需要定义事件名称,数据格式支持json对象
// 对端通过on_message来接受数据.
this.socket.send({"username":"xiaocheng","password":"123456"});
},
undefined_event_response(){
this.socket.on("message", (data)=>{
// 接收来自服务端的未定义数据
this.game.print(data);
});
},
custom_event(){
// 客户端自定义事件
this.socket.emit("login",{"username":"xiaocheng","password":"123456"});
},
custom_event_response(){
this.socket.on("login_response",(data)=>{
this.game.print(data);
});
},
count_response(){
this.socket.on("count_response",(data)=>{
this.game.print(data,1);
});
},
join_room(){
// 进入不同房间
let token = this.game.getdata("access_token") || this.game.getfs("access_token");
let user = this.game.get_user_by_token(token);
var uid = user.unique_id;
this.socket.emit("join_room",{"uid":uid});
},
join_room_response(){
// 进入不同房间的服务端响应
this.socket.on("join_room_response", (data)=>{
this.game.print(data,1);
});
this.socket.on("toutoude",(data)=>{
this.game.print(data,1);
});
},
server_response(){
// 服务器主动定时推送数据
this.socket.on("server_response",(data)=>{
this.game.print(data);
})
},
back(){
this.game.openWin("root");
},
}
});
}
</script>
</body>
</html>
服务端推送广播信息
import flask_socketio
from application import socketio
from flask import request
from flask_socketio import emit,Namespace,send,join_room
from application.apps.users.models import User
from threading import Lock
import random
thread = None
thread_lock = Lock()
class UserNamespace(Namespace):
"""用户的socket命名空间"""
def on_connect(self):
print("用户[%s]进入了种植园!" % request.sid)
# 主动推送
# self.count()
# 定时主动推送
# self.setInterval()
# 服务端发送广播
self.broadcast()
def on_disconnect(self):
print("用户[%s]退出了种植园!" % request.sid)
def on_login(self,data):
print("登录中....%s" % data)
message = {
"user_name":"xiaoming",
"age":17,
}
emit("login_response",message)
message["age"] = 18
emit("login_response",message)
message["age"] = 19
emit("login_response",message)
message["age"] = 20
emit("login_response",message)
def on_message(self,data):
"""接受客户端的未定义事件"""
print("接受未定义事件参数:", data)
send({"status":"登录成功~"})
def count(self):
"""主动推送数据,总人数"""
from application.apps.users.models import User
count_people = User.query.count()
emit("count_response","当前魔方APP登录用户:%s" % count_people)
def on_join_room(self,data):
"""
大聪明, 测试数据: c36bb167-a94e-44b4-b12f-8743206cc4e8
焦秀英, 测试数据: 6e2cecc6-05f3-4b6f-b3f7-82914f95a7b4
陈丹丹,测试数据: d6fabb0a-3842-407b-9aca-73155257a796
"""
#把焦秀英和陈丹丹分一个房间
if data["uid"] == "c36bb167-a94e-44b4-b12f-8743206cc4e8":
room_id = '1'
else:
room_id = '2'
# 房间ID必须是字符串
join_room(room_id)
user = User.query.filter(User.unique_id==data["uid"]).first()
from application.apps.users.marshmallow import UserSchema
us = UserSchema()
user_info = us.dump(user)
# 发送给单个用户
print(user_info)
print(room_id)
emit("join_room_response", {"user_info": user_info, "room": room_id})
# 发送给指定房间的每一个用户
if room_id == '2':
# 房间ID必须是字符串
emit("toutoude",{"message":"你们这把当狼~"}, room=str(room_id))
def setInterval(self):
global thread
with thread_lock:
if thread is None:
thread = socketio.start_background_task(target=self.background_thread,data=100)
def background_thread(self,data):
while True:
socketio.sleep(1) # socketio提供的异步
t = random.randint(1, 100) + data
# print(t,data)
socketio.emit('server_response', {'data': t},namespace='/orchard')
def broadcast(self):
data = {"uid": 12}
print(data)
# 事实上,只要服务端发送消息时,不声明房间ID, 则默认返回给整个命名空间下所有的用户都可以接收
# 广播方案1:
# emit('broadcast_all_response', data, broadcast=True)
# 广播方案2:发往指定命名空间下的广播
socketio.emit('broadcast_all_response', data, namespace='/orchard')
socketio.on_namespace(UserNamespace(namespace='/orchard'))
客户端接受广播信息,代码:
<!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/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>
<script>
apiready = function(){
Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
new Vue({
el:"#app",
data(){
return {
music_play:true,
namespace: '/orchard', // 命名空间(路径)
socket: null, // socket连接对象
}
},
created(){
// socket连接
this.socket = io.connect(this.game.config.SOCKET_SERVER + this.namespace, {transports: ['websocket']});
this.connect();
},
methods:{
connect(){
this.socket.on('connect', ()=>{
this.game.print("开始连接websocket服务端");
// this.login(); // 发送登陆请求
// this.login_response(); // 用于接收服务端处理完成login事件以后的返回结果
// // 未定义事件调用
// this.undefined_event();
// this.undefined_event_response();
// // 自定义事件调用
// this.custom_event();
// this.custom_event_response();
// // 服务端主动推送
// this.count_response();
// 进入不同的房间
// this.join_room();
// this.join_room_response();
// 服务器定时推送数据
this.server_response();
});
this.socket.on("disconnect",()=>{
this.game.tips("服务器断开~正在重连....");
});
},
undefined_event(){
// 使用socketIO提供的未定义事件来完成前后端双向通讯
// 未定义事件: 表示不通过事件名称与服务端/客户端进行通信
// 本端可以通过send方法直接发送数据,不需要定义事件名称,数据格式支持json对象
// 对端通过on_message来接受数据.
this.socket.send({"username":"xiaocheng","password":"123456"});
},
undefined_event_response(){
this.socket.on("message", (data)=>{
// 接收来自服务端的未定义数据
this.game.print(data);
});
},
custom_event(){
// 客户端自定义事件
this.socket.emit("login",{"username":"xiaocheng","password":"123456"});
},
custom_event_response(){
this.socket.on("login_response",(data)=>{
this.game.print(data);
});
},
count_response(){
this.socket.on("count_response",(data)=>{
this.game.print(data,1);
});
},
join_room(){
// 进入不同房间
let token = this.game.getdata("access_token") || this.game.getfs("access_token");
let user = this.game.get_user_by_token(token);
var uid = user.unique_id;
this.socket.emit("join_room",{"uid":uid});
},
join_room_response(){
// 进入不同房间的服务端响应
this.socket.on("join_room_response", (data)=>{
this.game.print(data,1);
});
this.socket.on("toutoude",(data)=>{
this.game.print(data,1);
});
},
server_response(){
// 服务器主动定时推送数据
this.socket.on("server_response",(data)=>{
this.game.print(data);
})
// 监听来自服务端的广播信息
this.socket.on("broadcast_all_response",(data)=>{
this.game.print(data);
})
},
back(){
this.game.openWin("root");
},
}
});
}
</script>
</body>
</html>
种植园页面展示
种植园主框架页面
- 首页页面,点击种植园跳转到种植园页面
html/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>首页</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta name="format-detection" content="telephone=no,email=no,date=no,address=no">
<link rel="stylesheet" href="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app" id="app">
<img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png">
<div class="bg">
<img src="../static/images/bg0.jpg">
</div>
<ul>
<li><img class="module1" src="../static/images/image1.png" @click='to_orchard'></li>
<li><img class="module2" src="../static/images/image2.png" @click='to_user'></li>
<li><img class="module3" src="../static/images/image3.png"></li>
<li><img class="module4" src="../static/images/image4.png" @click='to_login'></li>
</ul>
</div>
<script>
apiready = function(){
var game = new Game("../static/mp3/bg1.mp3");
Vue.prototype.game = game;
new Vue({
el:"#app",
data(){
return {
music_play:true, // 默认播放背景音乐
prev:{name:"",url:"",params:{}}, // 上一页状态
current:{name:"index",url:"index.html","params":{}}, // 下一页状态
}
},
watch:{
music_play(){
if(this.music_play){
this.game.play_music("../static/mp3/bg1.mp3");
}else{
this.game.stop_music();
}
}
},
created(){
this.listen(); // 监听事件
},
methods:{
// 监听事件
listen(){
// 监听外部访问,唤醒app
this.listen_invite();
},
// 监听外部访问,唤醒app
listen_invite(){
// 使用系统方法appintenr监听并使用appParam接收URLScheme的路由参数
// 收集操作保存起来,并跳转到登陆页面.
api.addEventListener({
name: 'appintent' // 系统方法
}, (ret, err)=>{
// 获取路由参数,保存到本地
let appParam = ret.appParam;
// this.game.print(appParam,1); //{"uid":xxx,"user_type":xxx};
this.game.setfs(appParam)
// 如果是其他用户邀请注册!
if(appParam.user_type == 'invite'){
// 跳转到登陆页面
this.game.openWin('login', 'login.html')
}
});
},
// 点击签到跳转登陆页面
to_login(){
this.game.openWin('login','login.html')
},
// 点击跳转到个人中心页面
to_user(){
// 判断用户是否登陆
this.game.check_user_login(this,() => {
this.game.openWin('user', 'user.html')
});
},
// 点击跳转到种植园页面
to_orchard(){
this.game.check_user_login(this,() => {
this.game.openWin('orchard', 'orchard.html')
});
},
}
})
}
</script>
</body>
</html>
- 种植园主框架页面
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/main.js"></script>
<script src="../static/js/v-avatar-2.0.3.min.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">
<img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
<div class="user_avatar">
<v-avatar v-if="user_info.avatar" :src="user_info.avatar" :size="62" :rounded="true"></v-avatar>
<v-avatar v-else-if="user_info.nickname" :username="user_info.nickname" :size="62" :rounded="true"></v-avatar>
<v-avatar v-else :username="user_info.id" :size="62" :rounded="true"></v-avatar>
</div>
<img class="avatar_border" src="../static/images/avatar_border.png" alt="">
</div>
<p class="user_name">好听的昵称</p>
</div>
<div class="wallet">
<div class="balance">
<p class="title"><img src="../static/images/money.png" alt="">钱包</p>
<p class="num">99,999.00</p>
</div>
<div class="balance">
<p class="title"><img src="../static/images/integral.png" alt="">果子</p>
<p class="num">99,999.00</p>
</div>
</div>
<div class="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="show">商店</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_info:{nickname:"好听的昵称"},
music_play:true,
namespace: '/orchard',
socket: null,
}
},
created(){
this.game.openFrame("my_orchard","my_orchard.html","from_right",{
marginTop: 174, //相对父页面上外边距的距离,数字类型
marginLeft: 0, //相对父页面左外边距的距离,数字类型
marginBottom: 54, //相对父页面下外边距的距离,数字类型
marginRight: 0 //相对父页面右外边距的距离,数字类型
});
},
methods:{
show(){alert("商店")},
connect(){
// socket连接
this.socket = io.connect(this.game.config.SOCKET_SERVER + this.namespace, {transports: ['websocket']});
this.socket.on('connect', ()=>{
this.game.print("开始连接服务端");
});
},
back(){
this.game.openWin("root");
},
}
});
}
</script>
</body>
</html>
- 种植园主框架css样式
static/css/main.css
/* 种植园主框架css样式 */
.app .orchard-bg{
margin: 0 auto;
width: 100%;
max-width: 100rem;
position: absolute;;
z-index: -1;
top: -6rem;
}
.app .orchard-bg .board_bg2{
position: absolute;
top: 1rem;
}
.orchard .back{
position: absolute;
width: 3.83rem;
height: 3.89rem;
z-index: 1;
top: 2rem;
left: 2rem;
}
.orchard .music{
right: 2rem;
}
.orchard .header{
position: absolute;
top: 0rem;
left: 0;
right: 0;
margin: auto;
width: 32rem;
height: 19.28rem;
}
.orchard .info{
position: absolute;
z-index: 1;
top: 0rem;
left: 4.4rem;
width: 8rem;
height: 9.17rem;
}
.orchard .info .avata{
width: 8rem;
height: 8rem;
position: relative;
}
.orchard .info .avatar_bf{
position: absolute;
z-index: 1;
margin: auto;
width: 6rem;
height: 6rem;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.orchard .info .user_avatar{
position: absolute;
z-index: 1;
width: 6rem;
height: 6rem;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
border-radius: 1rem;
}
.orchard .info .avatar_border{
position: absolute;
z-index: 1;
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 7.2rem;
height: 7.2rem;
}
.orchard .info .user_name{
position: absolute;
left: 8rem;
top: 1rem;
width: 11rem;
height: 3rem;
line-height: 3rem;
font-size: 1.5rem;
text-shadow: 1px 1px 1px #aaa;
border-radius: 3rem;
background: #ff9900;
text-align: center;
}
.orchard .wallet{
position: absolute;
top: 3.4rem;
right: 4rem;
width: 16rem;
height: 10rem;
}
.orchard .wallet .balance{
margin-top: 1.4rem;
float: left;
margin-right: 1rem;
}
.orchard .wallet .title{
color: #fff;
font-size: 1.2rem;
width: 6.4rem;
text-align: center;
}
.orchard .wallet .title img{
width: 1.4rem;
margin-right: 0.2rem;
vertical-align: sub;
height: 1.4rem;
}
.orchard .wallet .num{
background: url("../images/btn3.png") no-repeat 0 0;
background-size: 100%;
width: 6.4rem;
font-size: 0.8rem;
color: #fff;
height: 2rem;
line-height: 1.8rem;
text-indent: 1rem;
}
.orchard .header .menu-list{
position: absolute;
top: 9rem;
left: 2rem;
}
.orchard .header .menu-list .menu{
color: #fff;
font-size: 1rem;
float: left;
width: 4rem;
height: 4rem;
text-align: center;
margin-right: 2rem;
}
.orchard .header .menu-list .menu img{
width: 3.33rem;
height: 3.61rem;
display: block;
margin: auto;
margin-bottom: 0.4rem;
}
.orchard .footer{
position: absolute;
width: 100%;
height: 6rem;
bottom: -2rem;
background: url("../images/board_bg2.png") no-repeat -1rem 0;
background-size: 110%;
}
.orchard .footer .menu-list{
width: 100%;
height: 4rem;
display: flex;
position: absolute;
top: -1rem;
}
.orchard .footer .menu-list .menu,
.orchard .footer .menu-list .menu-center{
float: left;
width: 4.44rem;
height: 5.2rem;
font-size: 1.5rem;
color: #fff;
line-height: 4.44rem;
text-align: center;
background: url("../images/btn5.png") no-repeat 0 0;
background-size: 100%;
flex: 1;
margin-left: 4px;
margin-right: 4px;
}
.orchard .footer .menu-list .menu-center{
background: url("../images/btn6.png") no-repeat 0 0;
background-size: 100%;
flex: 2;
}
- socket连接服务端地址配置
static/js/main.js
// 初始化配置
init_config(){
// 客户端项目的全局配置
this.config = {
// 服务端API地址
// API_SERVER:"http://192.168.189.138:5000/api",
API_SERVER:"http://192.168.19.46:5000/api",
// http服务端网关地址
HTTP_SERVER: 'http://192.168.19.46:5000/',
// socketio连接服务端地址
SOCKET_SERVER: 'ws://192.168.19.46:5000',
SMS_TIME_OUT: 60 , // 短信发送冷却时间/秒
CAPTCHA_APP_ID: "2028945858", // 防水墙验证码应用ID
}
}
我的果园页面
html/my_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/main.js"></script>
</head>
<body>
<div class="app orchard orchard-frame" id="app">
<div class="background">
<img class="grassland2" src="../static/images/grassland2.png" alt="">
<img class="mushroom1" src="../static/images/mushroom1.png" alt="">
<img class="stake1" src="../static/images/stake1.png" alt="">
<img class="stake2" src="../static/images/stake2.png" alt="">
</div>
<div class="pet-box">
<div class="pet">
<img class="pet-item" src="../static/images/pet1.png" alt="">
</div>
<div class="pet turned_off">
<img class="turned_image" src="../static/images/turned_off.png" alt="">
<p>请购买宠物</p>
</div>
</div>
<div class="tree-list">
<div class="tree-box">
<div class="tree">
<img src="../static/images/tree4.png" alt="">
</div>
<div class="tree">
<img src="../static/images/tree3.png" alt="">
</div>
<div class="tree">
<img src="../static/images/tree4.png" alt="">
</div>
</div>
<div class="tree-box">
<div class="tree">
<img src="../static/images/tree3.png" alt="">
</div>
<div class="tree">
<img src="../static/images/tree2.png" alt="">
</div>
<div class="tree">
<img src="../static/images/tree2.png" alt="">
</div>
</div>
<div class="tree-box">
<div class="tree">
<img src="../static/images/tree1.png" alt="">
</div>
<div class="tree">
<img src="../static/images/tree0.png" alt="">
</div>
<div class="tree">
<img src="../static/images/tree0.png" alt="">
</div>
</div>
</div>
<div class="prop-list">
<div class="prop">
<img src="../static/images/prop1.png" alt="">
<span>1</span>
<p>化肥</p>
</div>
<div class="prop">
<img src="../static/images/prop2.png" alt="">
<span>0</span>
<p>修剪</p>
</div>
<div class="prop">
<img src="../static/images/prop3.png" alt="">
<span>1</span>
<p>浇水</p>
</div>
<div class="prop">
<img src="../static/images/prop4.png" alt="">
<span>1</span>
<p>宠物粮</p>
</div>
</div>
<div class="pet-hp-list">
<div class="pet-hp">
<p>宠物1 饱食度</p>
<div class="hp">
<div style="width: 85%;" class="process">85%</div>
</div>
</div>
<div class="pet-hp">
<p>宠物2 饱食度</p>
<div class="hp">
<div style="width: 0;" class="process">0%</div>
</div>
</div>
</div>
</div>
<script>
apiready = function(){
Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
new Vue({
el:"#app",
data(){
return {
namespace: '/orchard',
socket: null,
}
},
created(){
},
methods:{
}
});
}
</script>
</body>
</html>
- 我的果园页面的css样式,
static/css/main.css
代码:
/* 我的果园页面的css样式 */
.orchard-frame .background{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100rem;
}
.orchard-frame .background .grassland1{
width: 31.22rem;
height: 13.53rem;
position: absolute;
top: 4rem;
}
.orchard-frame .background .grassland2{
width: 31.22rem;
height: 13.53rem;
position: absolute;
top: 5rem;
}
.orchard-frame .background .mushroom1{
width: 4.56rem;
height: 4.83rem;
position: absolute;
right: 1rem;
top: 11rem;
}
.orchard-frame .background .stake1{
width: 4.56rem;
height: 4.83rem;
position: absolute;
top: 3rem;
left: 0rem;
}
.orchard-frame .background .stake2{
width: 6.31rem;
height: 4.83rem;
position: absolute;
top: 3rem;
left: 13rem;
}
.orchard-frame .pet-box{
position: absolute;
top: -2rem;
left: 0;
display: flex;
}
.orchard-frame .pet-box .pet{
position: relative;
width: 14.16rem;
height: 15rem;
flex: 1;
margin-left: 1rem;
margin-right: 1rem;
background: url("../images/tree1.png") no-repeat 0 -0.5rem;
background-size: 100%;
}
.orchard-frame .pet-box .turned_off .turned_image{
width: 5.14rem;
height: 6.83rem;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}
.orchard-frame .pet-box .turned_off p{
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border: 1px solid #fff;
border-radius: 1rem;
width: 8rem;
height: 3rem;
line-height: 3rem;
font-size: 1.5rem;
word-wrap: break-word;
padding: 1rem;
color: #000;
text-align: center;
background: rgba(255,255,255,.6);
}
.orchard-frame .pet-box .pet-item{
width: 10rem;
height: 10rem;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}
.orchard-frame .tree-list{
position: absolute;
top: 9rem;
width: 100%;
}
.orchard-frame .tree-box{
margin-left: 3rem;
margin-right: 3rem;
}
.orchard-frame .tree-box .tree{
width: 7.8rem;
height: 4rem;
margin-bottom: 2rem;
float: left;
}
.orchard-frame .tree-box .tree img{
width: 7.8rem;
height: 8rem;
max-height: 8rem;
}
.orchard-frame .prop-list{
position: absolute;
bottom: 0.5rem;
width: 100%;
}
.orchard-frame .prop-list .prop{
float: left;
margin-left: 1rem;
width: 3rem;
position: relative;
}
.orchard-frame .prop-list .prop img{
width: 2.5rem;
height: 2.5rem;
margin: auto;
display: block;
}
.orchard-frame .prop-list .prop span{
position: absolute;
top: -4px;
right: -4px;
border-radius: 50%;
width: 1rem;
height; 1rem;
font-size: .8rem;
color: #fff;
background-color: #cc0000;
text-align: center;
line-height: 1rem;
padding: 2px;
}
.orchard-frame .prop-list .prop p{
text-align: center;
color: #fff;
}
.orchard-frame .pet-hp-list{
position: absolute;
right: 0;
bottom: 2.8rem;
width: 11rem;
height: 4rem;
}
.orchard-frame .pet-hp-list .pet-hp{
margin-bottom: 5px;
}
.orchard-frame .pet-hp-list .pet-hp p{
}
.orchard-frame .pet-hp-list .pet-hp .hp{
border: 1px solid #fff;
border-radius: 5rem;
width: 10rem;
padding: 1px;
}
.orchard-frame .pet-hp-list .pet-hp .process{
font-size: 0.5rem;
background-color: red;
color: #fff;
border-radius: 5rem;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
text-align: center;
}
商城页面
- 种植园主页面
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/main.js"></script>
<script src="../static/js/v-avatar-2.0.3.min.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">
<img class="avatar_bf" src="../static/images/avatar_bf.png" alt="">
<div class="user_avatar">
<v-avatar v-if="user_info.avatar" :src="user_info.avatar" :size="62" :rounded="true"></v-avatar>
<v-avatar v-else-if="user_info.nickname" :username="user_info.nickname" :size="62" :rounded="true"></v-avatar>
<v-avatar v-else :username="user_info.id" :size="62" :rounded="true"></v-avatar>
</div>
<img class="avatar_border" src="../static/images/avatar_border.png" alt="">
</div>
<p class="user_name">好听的昵称</p>
</div>
<div class="wallet">
<div class="balance">
<p class="title"><img src="../static/images/money.png" alt="">钱包</p>
<p class="num">99,999.00</p>
</div>
<div class="balance">
<p class="title"><img src="../static/images/integral.png" alt="">果子</p>
<p class="num">99,999.00</p>
</div>
</div>
<div class="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_info:{nickname:"好听的昵称"},
music_play:true,
namespace: '/orchard',
socket: null,
}
},
created(){
// 字典加载我的果园页面
this.to_my_orchard();
},
methods:{
// 跳转我的果园页面
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');
})
},
connect(){
// socket连接
this.socket = io.connect(this.game.config.SOCKET_SERVER + this.namespace, {transports: ['websocket']});
this.socket.on('connect', ()=>{
this.game.print("开始连接服务端");
});
},
back(){
this.game.openWin("root");
},
}
});
}
</script>
</body>
</html>
- 道具商城页面,
html/shop.html
,代码:
<!DOCTYPE html>
<html>
<head>
<title>商店</title>
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta charset="utf-8">
<link rel="stylesheet" href="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app frame avatar update_nickname add_friend shop" id="app">
<div class="box">
<p class="title">商店</p>
<img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
<div class="friends_list shop_list">
<div class="item">
<div class="avatar shop_item">
<img src="../static/images/fruit_tree.png" alt="">
</div>
<div class="info">
<p class="username">果树</p>
<p class="time">200</p>
</div>
<div class="status">200</div>
</div>
</div>
</div>
</div>
<script>
apiready = function(){
Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
new Vue({
el:"#app",
data(){
return {
}
},
created(){
},
methods:{
back(){
this.game.closeFrame();
},
}
});
}
</script>
</body>
</html>
- 道具商城页面css样式
static/css/main.css
,代码:
/* 道具商城页面css样式 */
.shop .shop_list{
margin-left: 1rem;
margin-top: -5rem;
}
获取商店道具信息
服务端创建商品道具模型
商店的商品:
1. 种子
果树
标题
价格
描述
图片
使用流程相关参数
状态: 种子期, 成长期, 成熟期
种子期: 3 x 60 x 60
成长期: 6 x 60 x 60
成熟期: 12 x 60 x 60
2. 宠物
小狗1,小狗2,小狗3,小狗4
标题
价格
描述
图片
使用流程相关参数:
饱食度: <20%(饥饿) <50%(正常) <85%(饱食)
生命期: -1(永久),也可以是具体时间
保护植物的成功率 : 10% 0-1 <10%
3. 狗粮
狗粮1,狗粮2,狗粮3
标题
价格
描述
图片
使用流程相关参数:
饱食度: 20%
有效期: -1(永久)
4. 种植道具
化肥,....
标题
价格
描述
图片
使用流程相关参数:
缩短时间: 1小时
- 基础模型类实现保存数据方法:
application/utils/models.py
from application import db
from datetime import datetime
# 公共模型
class BaseModel(db.Model):
__abstract__ = True # 抽象模型,创建表时不会给此模型建表
id = db.Column(db.Integer, primary_key=True, comment="主键ID")
name = db.Column(db.String(255), default="", comment="名称/标题")
is_deleted = db.Column(db.Boolean, default=False, comment="逻辑删除")
orders = db.Column(db.Integer, default=0, comment="排序")
status = db.Column(db.Boolean, default=True, comment="状态(是否显示,是否激活)")
created_time = db.Column(db.DateTime, default=datetime.now, comment="创建时间")
updated_time = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment="更新时间")
def __repr__(self):
return "<%s: %s>" % (self.__class__.__name__, self.name)
@classmethod
def add(cls, instance):
'''
保存信息
:param instance: 模型对象
:return:
'''
db.session.add(instance)
db.session.commit()
@classmethod
def add_all(cls, instance_list):
'''
保存信息
:param instance_list: 模型对象列表
:return:
'''
db.session.add_all(instance_list)
db.session.commit()
- 构建商品道具模型
apps/orchard/models.py
,模型代码:
from application.utils.models import BaseModel,db
from application.settings import plant as config
# 商品道具公共模型
class Goods(BaseModel):
'''商品道具公共模型'''
__abstract__ = True # 抽象模型,不生成数据表
price = db.Column(db.Numeric(7, 2), comment="商品价格(金额)")
credit = db.Column(db.Integer, default=0, comment="商品价格(果子)")
image = db.Column(db.String(255), comment="商品图片")
remark = db.Column(db.String(255), comment="商品描述")
use_time = db.Column(db.Integer(), default=-1, comment="道具过期时间")
# 种子道具模型
class SeedItem(Goods):
'''种子道具模型'''
__tablename__ = 'mf_goods_seed'
# 种子成长状态
grow_option = (
(0, '种子期'),
(1, '成长期'),
(2, '成熟期'),
)
group = db.Column(db.Integer, default=0, comment='种子成长状态')
seed_time = db.Column(db.Integer, default=config.DEFAULT_SEED_TIME, comment='种子期成长时间')
grow_time = db.Column(db.Integer, default=config.DEFAULT_GROW_TIME, comment='成长期成长时间')
mature_time = db.Column(db.Integer, default=config.DEFAULT_MATURE_TIME, comment='成熟期成长时间')
# 宠物道具模型
class PetItem(Goods):
'''宠物道具模型'''
__tablename__ = 'mf_goods_pet'
# 宠物饱食度状态
satiety_option = (
(50, "正常"),
(85, "饱食"),
)
hunger_num = db.Column(db.Integer, default=config.PET_HUNGER_DOT, comment="饥饿度")
satiety_num = db.Column(db.Integer, default=config.PET_SATIETY_DOT, comment="饱食度")
life_time = db.Column(db.Integer, default=config.PET_LIFE_TIME, comment="生命周期")
hit_rate = db.Column(db.Integer, default=config.PET_HIT_RATE, comment="保护植物的成功率")
# 宠物粮道具模型
class PetFoodItem(Goods):
"""宠物粮道具模型"""
__tablename__ = "mf_goods_pet_food"
food_dot = db.Column(db.Integer, default=config.PET_FOOD_DOT, comment="补充饱食度")
# 种植加速成长相关道具模型 - (化肥,浇水等等)
class PlantItem(Goods):
"""种植相关道具模型"""
__tablename__ = "mf_goods_plant"
reduce_time = db.Column(db.Integer, default=config.REDUCT_TIME, comment="减少时间")
- 种植园相关配置信息,
application/settings/plant.py
,代码:
# 种子期到成长期的时间[单位:s]
DEFAULT_SEED_TIME = 3 * 60 * 60
# 成长期到成熟期的时间[单位:s]
DEFAULT_GROUP_TIME = 6 * 60 * 60
# 成熟期到枯萎的时间[单位:s]
DEFAULT_MATURE_TIME = 12 * 60 * 60
# 宠物饥饿状态的饱食度
PET_HUNGER_DOT = 20
# 宠物饱食状态的饱食度
PET_SATIETY_DOT = 80
# 宠物的生命周期
PET_LIFE_TIME = -1
# 宠物保护植物的成功率
PET_HIT_RATE = 10
# 宠物粮道具补充的饱食度
PET_FOOD_DOT = 10
# 种植道具减少的种植时间[单位:s]
REDUCT_TIME = 10 * 60
- 终端命令生成测试数 ,
application.utls.commands
,代码:
import os, inspect, unittest, random, sys
from flask_script import Command, Option
from importlib import import_module
from faker.providers import internet
# 批量生成测试数据
class FakerCommand(Command):
"""批量生成测试数据"""
name = 'faker' # 生成命令名称
# 传递的参数
option_list = [
Option('--type', '-t', dest='type', default='user'), # 指定生成数据的类型(用户数据,其他数据)
Option('--num', '-n', dest='num', default=1), # 生成数据的数量
]
# 以后想要生成数据类型列表
type_list = ['user','seed','pet','pet_food','plant']
def __call__(self, app, type, num):
# 判断想要生成数据的类型是否在类型列表中
if type not in self.type_list:
print("数据类型不正确\n当前Faker生成数据类型仅支持: %s" % self.type_list)
return None
num = int(num)
if num < 1:
print("生成数量不正确\n当前Faker生成数据至少1个以上")
return None
if type == 'user':
# 生成用户测试数据
self.create_user(app, num)
elif type == 'seed':
# 生成种子道具测试数据
self.create_seed(app, num)
elif type == 'pet':
# 生成宠物道具测试数据
self.create_pet(app, num)
elif type == 'pet_food':
# 生成宠物食物测试数据
self.create_pet_food(app, num)
elif type == 'plant':
# 生成植物加速成长道具测试数据
self.create_plant(app, num)
# 1. 生成指定数量的测试用户信息
def create_user(self,app,num):
"""生成指定数量的测试用户信息"""
from application.apps.users.models import User,UserProfile
faker = app.faker
faker.add_provider(internet)
user_list = [] # 用户模型对象列表
# 从配置文件中读取默认测试用户的登录密码
password = app.config.get("DEFAULT_TEST_USER_PASSWORD", "12345678")
for i in range(num):
sex = bool(random.randint(0,2))
nickname = faker.name()
# 登录账号[随机字母,6-16位]
name = faker.pystr(min_chars=6, max_chars=16)
age = random.randint(13, 50)
birthday = faker.date_time_between(start_date="-%sy" % age, end_date="-12y", tzinfo=None)
hometown_province = faker.province()
hometown_city = faker.city()
hometown_area = faker.district()
living_province = faker.province()
living_city = faker.city()
living_area = faker.district()
user = User(
nickname=nickname,
sex=sex,
name=name,
age=age,
password=password,
pay_password=password,
money=random.randint(100, 99999),
credit=random.randint(100, 99999),
ip_address=faker.ipv4_public(),
email=faker.ascii_free_email(),
mobile=faker.phone_number(),
unique_id=faker.uuid4(),
province=faker.province(),
city=faker.city(),
area=faker.district(),
info=UserProfile(
birthday=birthday,
hometown_province=hometown_province,
hometown_city=hometown_city,
hometown_area=hometown_area,
hometown_address=hometown_province + hometown_city + hometown_area + faker.street_address(),
living_province=living_province,
living_city=living_city,
living_area=living_area,
living_address=living_province + living_city + living_area + faker.street_address()
)
)
user_list.append(user)
# 存储数据
with app.app_context():
User.add_all(user_list)
print("生成%s个用户信息完成..." % num)
# 2. 生成指定数量的种子道具测试数据
def create_seed(self, app, num):
"""生成指定数量的种子道具测试数据"""
from application.apps.orchard.models import SeedItem
seed_list = [] # 模型对象列表
for i in range(num):
seed = SeedItem(
name = f'苹果{i}号',
price = random.randint(1, 20),
credit = random.randint(100, 1000),
image = 'fruit_tree.png',
remark = '果实饱满香甜',
)
seed_list.append(seed)
# 存储数据
with app.app_context():
SeedItem.add_all(seed_list)
print("生成%s个种子道具信息完成..." % num)
# 3. 生成指定数量的宠物道具测试数据
def create_pet(self, app, num):
"""生成指定数量的宠物道具测试数据"""
from application.apps.orchard.models import PetItem
pet_list = [] # 模型对象列表
pet_name_list = ["中华田园犬", "贵宾犬", "哮天犬"]
pet_img_list = ["pet1.png", "pet2.png", "pet3.png"]
for i in range(num):
name = random.choice(pet_name_list)
if name == '中华田园犬':
image = pet_img_list[0]
elif name == '贵宾犬':
image = pet_img_list[1]
elif name == '哮天犬':
image = pet_img_list[2]
pet = PetItem(
name = name,
price = random.randint(1, 20),
credit = random.randint(100, 1000),
remark = '看家护院好帮手',
image = image,
)
pet_list.append(pet)
# 存储数据
with app.app_context():
PetItem.add_all(pet_list)
print("生成%s个宠物道具信息完成..." % num)
# 4. 生成指定数量的宠物食物道具测试数据
def create_pet_food(self, app, num):
"""生成指定数量的宠物食物道具测试数据"""
from application.apps.orchard.models import PetFoodItem
pet_food_list = [] # 模型对象列表
for i in range(num):
pet_food = PetFoodItem(
name = f'牛奶{i}号',
price = random.randint(1, 20),
credit = random.randint(100, 1000),
remark = '补充体力',
image = 'prop4.png',
)
pet_food_list.append(pet_food)
# 存储数据
with app.app_context():
PetFoodItem.add_all(pet_food_list)
print("生成%s个宠物食物道具信息完成..." % num)
# 5. 生成指定数量的植物成长加速道具测试数据
def create_plant(self, app, num):
"""生成指定数量的植物成长加速道具测试数据"""
from application.apps.orchard.models import PlantItem
plant_list = [] # 模型对象列表
for i in range(num):
plant = PlantItem(
name = f'化肥{i}号',
price = random.randint(1, 20),
credit = random.randint(100, 1000),
remark = '补充体力',
image = 'prop1.png',
)
plant_list.append(plant)
# 存储数据
with app.app_context():
PlantItem.add_all(plant_list)
print("生成%s个植物加速成长道具信息完成..." % num)
终端下执行命令,添加测试数据:
python manage.py faker -tseed -n5 # 种子道具
python manage.py faker -tpet -n5 # 宠物道具
python manage.py faker -tpet_food -n5 # 宠物食物
python manage.py faker -tplant -n5 # 植物加速道具
- 在admin站点中进行模型管理注册,
apps/orchard/admin.py
,代码:
# 灵活配置站点需要的信息,
from application import admin, db
from flask_admin.contrib.sqla import ModelView
from .models import SeedItem, PlantItem, PetItem, PetFoodItem
class GoodsAdminModel(ModelView):
# 列表页显示字段列表
column_list = ["id","name","image","price", 'credit']
# 列表页可以直接编辑的字段列表
column_editable_list = ["name","price"]
# 是否允许查看详情
can_view_details = True
# 列表页显示直接可以搜索数据的字典
column_searchable_list = ['name','price']
# 可以进行滤器的字段列表
column_filters = ['name']
# 高级版本的分页器(比上面简单的好用)
list_pager = True
# 单页显示数量
page_size = 3
class SeedItemAdminModel(GoodsAdminModel):
pass
class PetItemAdminModel(GoodsAdminModel):
pass
class FoodItemAdminModel(GoodsAdminModel):
pass
class PlantItemAdminModel(GoodsAdminModel):
pass
# 把自定义模型类添加到后台站点
# 把当前页面添加到顶级导航下,category来设置,如果导航不存在,则自动创建
admin.add_view(SeedItemAdminModel(SeedItem,db.session,name="种子道具", category="道具商城"))
admin.add_view(PetItemAdminModel(PetItem,db.session,name="宠物道具", category="道具商城"))
admin.add_view(FoodItemAdminModel(PetFoodItem,db.session,name="宠物粮道具", category="道具商城"))
admin.add_view(PlantItemAdminModel(PlantItem,db.session,name="种植加速道具", category="道具商城"))
服务端提供商品列表api
- 视图:
orchard.api
, 代码:
from flask_jwt_extended import jwt_required
from application.utils import decorator
from application import code, message
from . import services
# 获取商品列表信息
@jwt_required()
@decorator.get_user_object
def get_goods_list(user):
'''
获取商品列表信息
:param user: 装饰器通过token值获取的用户模型对象
:return:
'''
# 获取种子列表信息
seed_list = services.get_seed_list()
# 获取宠物列表信息
pet_list = services.get_pet_list()
# 获取宠物粮列表信息
pet_food_list = services.get_pet_food_list()
# 获取种植加速道具列表信息
plant_list = services.get_plant_list()
return {
'errno': code.CODE_OK,
'errmsg': message.ok,
'seed_list': seed_list,
'pet_list': pet_list,
'pet_food_list': pet_food_list,
'plant_list': plant_list,
}
- 模型构造器
orchard.marshmallow
,代码:
from marshmallow import post_dump
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
from .models import SeedItem, PlantItem, PetItem, PetFoodItem
# 商品道具公共构造器
class ItemSchema(SQLAlchemyAutoSchema):
'''商品道具公共构造器'''
# 序列化输出结果
@post_dump
def post_dump(self, data, **kwargs):
data['price'] = float(data['price'])
return data
# 种子道具构造器
class SeedSchema(ItemSchema):
'''种子道具构造器'''
class Meta:
model = SeedItem
fields = ["id", "name","price","credit","image","remark","use_time","group","seed_time","group_time","mature_time"]
# 宠物的构造器
class PetSchema(ItemSchema):
"""宠物的构造器"""
class Meta:
model = PetItem
fields = ["id", "name","price","credit","image","remark","use_time","hunger_num", "satiety_num", "life_time",
"hit_rate"]
# 宠物粮的构造器
class PetFoodSchema(ItemSchema):
"""宠物粮的构造器"""
class Meta:
model = PetFoodItem
fields = ["id", "name","price","credit","image","remark","use_time","food_dot",]
# 种子道具的构造器
class PlantSchema(ItemSchema):
"""种子道具的构造器"""
class Meta:
model = PlantItem
fields = ["id", "name","price","credit","image","remark","use_time","reduce_time",]
- 数据服务层
orchard.services
,代码:
from .models import SeedItem, PetItem, PetFoodItem, PlantItem
from .marshmallow import SeedSchema, PetSchema, PetFoodSchema, PlantSchema
# 获取种子道具列表
def get_seed_list():
item_list = SeedItem.query.filter(SeedItem.is_deleted == False,SeedItem.status==True).all()
# 实例化构造器
ss = SeedSchema()
# 序列化输出
data = ss.dump(item_list, many=True)
return data
# 获取宠物道具列表
def get_pet_list():
item_list = PetItem.query.filter(PetItem.is_deleted == False,PetItem.status==True).all()
# 实例化构造器
ps = PetSchema()
# 序列化输出
data = ps.dump(item_list, many=True)
return data
# 获取宠物粮道具列表
def get_pet_food_list():
item_list = PetFoodItem.query.filter(PetFoodItem.is_deleted == False,PetFoodItem.status==True).all()
# 实例化构造器
pfs = PetFoodSchema()
# 序列化输出
data = pfs.dump(item_list, many=True)
return data
# 获取种植加速道具列表
def get_plant_list():
item_list = PlantItem.query.filter(PlantItem.is_deleted == False,PlantItem.status==True).all()
# 实例化构造器
pls = PlantSchema()
# 序列化输出
data = pls.dump(item_list, many=True)
return data
- 路由
orchard.urls
代码:
from application import path, api_rpc
# 引入当前蓝图应用视图, 引入rpc视图
from . import views, api
# 蓝图路径与函数映射列表
urlpatterns = [
# path('/index', views.index, methods=['post']),
]
# rpc方法与函数映射列表[rpc接口列表]
# user = api.User() # 实例化类视图
apipatterns = [
api_rpc('goods_list', api.get_goods_list), # 获取商店道具列表
]
构建查询数据缓存层 - redis
因为道具信息一般不会经常发生更改,所以我们可以在查询时构建缓存层,当然现有的redis连接以后保存项目中的其他琐碎的功能数据,例如:短信,系统缓存。。。
这里,我们新配置一个redis的连接专门保存种植园模块的缓存数据。
- 开发环境配置缓存库
settings.dev
,代码:
"""redis数据库配置"""
# 默认缓存数据库 - 0号库
REDIS_URL = 'redis://:@127.0.0.1:6379/0'
# 验证相关数据 - 1号库
CHECK_URL = 'redis://:@127.0.0.1:6379/1'
# session储存数据库 - 2号库
SESSION_URL = 'redis://:@127.0.0.1:6379/2'
# 种植园商品缓存库 - 3号库
ORCHARD_URL = 'redis://:@127.0.0.1:6379/3'
- 项目入口文件加载redis3号种植园库
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 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
# 终端脚本工具初始化
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()
# 全局初始化
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)
# 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
- 数据服务层, 获取商品可先从redis中获取 ,加速查询速度 ,在
orchard/services.py
,代码:
import orjson
from .models import SeedItem, PetItem, PetFoodItem, PlantItem
from .marshmallow import SeedSchema, PetSchema, PetFoodSchema, PlantSchema
from application import redis_orchard
# 获取种子道具列表
def get_seed_list():
# 先查询Redis缓存库中是否有商品列表
data = redis_orchard.get('seed_list')
if not data:
item_list = SeedItem.query.filter(SeedItem.is_deleted == False,SeedItem.status==True).all()
# 实例化构造器
ss = SeedSchema()
# 序列化输出
data = ss.dump(item_list, many=True)
# 把商品数据缓存到redis中
redis_orchard.set('seed_list', orjson.dumps(data))
else:
data = orjson.loads(data)
return data
# 获取宠物道具列表
def get_pet_list():
# 先查询Redis缓存库中是否有商品列表
data = redis_orchard.get('pet_list')
if not data:
item_list = PetItem.query.filter(PetItem.is_deleted == False,PetItem.status==True).all()
# 实例化构造器
ps = PetSchema()
# 序列化输出
data = ps.dump(item_list, many=True)
# 把商品数据缓存到redis中
redis_orchard.set('pet_list', orjson.dumps(data))
else:
data = orjson.loads(data)
return data
# 获取宠物粮道具列表
def get_pet_food_list():
# 先查询Redis缓存库中是否有商品列表
data = redis_orchard.get('pet_food_list')
if not data:
item_list = PetFoodItem.query.filter(PetFoodItem.is_deleted == False,PetFoodItem.status==True).all()
# 实例化构造器
pfs = PetFoodSchema()
# 序列化输出
data = pfs.dump(item_list, many=True)
redis_orchard.set('pet_food_list', orjson.dumps(data))
else:
data = orjson.loads(data)
return data
# 获取种植加速道具列表
def get_plant_list():
# 先查询Redis缓存库中是否有商品列表
data = redis_orchard.get('plant_list')
if not data:
item_list = PlantItem.query.filter(PlantItem.is_deleted == False,PlantItem.status==True).all()
# 实例化构造器
pls = PlantSchema()
# 序列化输出
data = pls.dump(item_list, many=True)
redis_orchard.set('plant_list', orjson.dumps(data))
else:
data = orjson.loads(data)
return data
客户端获取商品列表
客户端就可以在打开商店的时候, 获取商品列表,html/shop.html
,代码:
<!DOCTYPE html>
<html>
<head>
<title>商店</title>
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta charset="utf-8">
<link rel="stylesheet" href="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app frame avatar update_nickname add_friend shop" id="app">
<div class="box">
<p class="title">商店</p>
<img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
<div class="friends_list shop_list">
<div class="item" v-for='seed in seed_list'>
<div class="avatar shop_item">
<img :src="`../static/images/${seed.image}`" alt="">
</div>
<div class="info">
<p class="username">{{seed.name}}</p>
<p class="time">价格: {{seed.price.toFixed(2)}}</p>
</div>
<div class="status">购买</div>
</div>
<div class="item" v-for='pet in pet_list'>
<div class="avatar shop_item">
<img :src="`../static/images/${pet.image}`" alt="">
</div>
<div class="info">
<p class="username">{{pet.name}}</p>
<p class="time">价格: {{pet.price.toFixed(2)}}</p>
</div>
<div class="status">购买</div>
</div>
<div class="item" v-for='food in pet_food_list'>
<div class="avatar shop_item">
<img :src="`../static/images/${food.image}`" alt="">
</div>
<div class="info">
<p class="username">{{food.name}}</p>
<p class="time">价格: {{food.price.toFixed(2)}}</p>
</div>
<div class="status">购买</div>
</div>
<div class="item" v-for='plant in plant_list'>
<div class="avatar shop_item">
<img :src="`../static/images/${plant.image}`" alt="">
</div>
<div class="info">
<p class="username">{{plant.name}}</p>
<p class="time">价格: {{plant.price.toFixed(2)}}</p>
</div>
<div class="status">购买</div>
</div>
</div>
</div>
</div>
<script>
apiready = function(){
Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
new Vue({
el:"#app",
data(){
return {
seed_list: [], // 种子道具列表
pet_list: [], // 宠物道具列表
pet_food_list: [], // 宠物粮道具列表
plant_list: [], // 种植加速道具列表
}
},
created(){
// 获取商品道具列表
this.get_goods_list();
},
methods:{
back(){
this.game.closeFrame();
},
// 获取商品道具列表
get_goods_list(){
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.goods_list',
'params': {},
'header': {
'Authorization': 'jwt ' + token
},
success(response){
let data = response.data;
if(data.result && data.result.errno === 1000){
// 请求成功获取商品列表
self.seed_list = data.result.seed_list;
self.pet_list = data.result.pet_list;
self.pet_food_list = data.result.pet_food_list;
self.plant_list = data.result.plant_list;
}else {
self.game.tips(data.result.errmsg)
}
}
})
})
},
}
});
}
</script>
</body>
</html>
商店道具购买
完善道具信息的显示
- 商店页面点击购买跳转到道具详情页面
html/item_details.html
,打开的同时传递相关参数过去。
html/shop.thml
<!DOCTYPE html>
<html>
<head>
<title>商店</title>
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta charset="utf-8">
<link rel="stylesheet" href="../static/css/main.css">
<script src="../static/js/vue.js"></script>
<script src="../static/js/axios.js"></script>
<script src="../static/js/uuid.js"></script>
<script src="../static/js/main.js"></script>
</head>
<body>
<div class="app frame avatar update_nickname add_friend shop" id="app">
<div class="box">
<p class="title">商店</p>
<img class="close" @click="back" src="../static/images/close_btn1.png" alt="">
<div class="friends_list shop_list">
<div class="item" v-for='seed in seed_list'>
<div class="avatar shop_item">
<img :src="`../static/images/${seed.image}`" alt="">
</div>
<div class="info">
<p class="username">{{seed.name}}</p>
<p class="time">价格: {{seed.price.toFixed(2)}}</p>
</div>
<div class="status" @click="buy_goods(seed, 'seed')">购买</div>
</div>
<div class="item" v-for='pet in pet_list'>
<div class="avatar shop_item">
<img :src="`../static/images/${pet.image}`" alt="">
</div>
<div class="info">
<p class="username">{{pet.name}}</p>
<p class="time">价格: {{pet.price.toFixed(2)}}</p>
</div>
<div class="status" @click="buy_goods(pet, 'pet')">购买</div>
</div>
<div class="item" v-for='pet_food in pet_food_list'>
<div class="avatar shop_item">
<img :src="`../static/images/${pet_food.image}`" alt="">
</div>
<div class="info">
<p class="username">{{pet_food.name}}</p>
<p class="time">价格: {{pet_food.price.toFixed(2)}}</p>
</div>
<div class="status" @click="buy_goods(pet_food, 'pet_food')">购买</div>
</div>
<div class="item" v-for='plant in plant_list'>
<div class="avatar shop_item">
<img :src="`../static/images/${plant.image}`" alt="">
</div>
<div class="info">
<p class="username">{{plant.name}}</p>
<p class="time">价格: {{plant.price.toFixed(2)}}</p>
</div>
<div class="status" @click="buy_goods(plant, 'plant')">购买</div>
</div>
</div>
</div>
</div>
<script>
apiready = function(){
Vue.prototype.game = new Game("../static/mp3/bg1.mp3");
new Vue({
el:"#app",
data(){
return {
seed_list: [], // 种子道具列表
pet_list: [], // 宠物道具列表
pet_food_list: [], // 宠物粮道具列表
plant_list: [], // 种植加速道具列表
}
},
created(){
// 获取商品道具列表
this.get_goods_list();
},
methods:{
back(){
this.game.closeFrame();
},
// 获取商品道具列表
get_goods_list(){
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.goods_list',
'params': {},
'header': {
'Authorization': 'jwt ' + token
},
success(response){
let data = response.data;
if(data.result && data.result.errno === 1000){
// 请求成功获取商品列表
self.seed_list = data.result.seed_list;
self.pet_list = data.result.pet_list;
self.pet_food_list = data.result.pet_food_list;
self.plant_list = data.result.plant_list;
}else {
self.game.tips(data.result.errmsg)
}
}
})
})
},
// 点击购买商品,跳转到商品详情页,购买道具
buy_goods(item, type){
this.game.openFrame('item_details', 'item_details.html', 'from_top', null, {
'item':item,
'type':type
})
},
}
});
}
</script>
</body>
</html>
- 在
html/item_details.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.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.image}`" alt="">
</div>
<div class="invite_tips item_remark">
<p>{{item.remark}}</p><br>
<p>
<span>价格:{{item.price.toFixed(2)}}</span>
<span>果子:{{item.credit}}</span>
</p><br><br>
<p @click="gotopay">立即购买</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: "",
}
},
created(){
// 接受传递过来的参数
this.get_page_params();
},
methods:{
get_page_params(){
// 接受来自商店页面shop.html的参数
this.item = api.pageParam.item;
this.type = api.pageParam.type;
},
gotopay(){
// 购买道具
var buttons = [1,2,5,10,20,50];
api.actionSheet({
title: `购买${this.item.name}的数量`,
cancelTitle: '取消',
buttons: buttons,
}, (ret, err)=>{
if(ret.buttonIndex<=buttons.length){
var buy_num = buttons[ret.buttonIndex-1];
this.game.print(buy_num,1);
}
});
},
back(){
this.game.closeFrame();
},
}
});
}
</script>
</body>
</html>
- 添加css样式 : main.css,代码:
.item .item_img{
width: 10rem;
height: 10rem;
position: absolute;
left: 10rem;
top: 10rem;
}
.item .item_remark{
position: absolute;
left: 9.4rem;
top: 20rem;
font-size: 1rem;
}
服务端提供购买道具接口
- 视图
orchard.api
,代码:
from flask_jwt_extended import jwt_required
from datetime import datetime
from application.utils import decorator
from application import code, message
from application.apps.users import services as user_services
from . import services
# 获取商品列表信息
@jwt_required()
@decorator.get_user_object
def get_goods_list(user):
'''
获取商品列表信息
:param user: 装饰器通过token值获取的用户模型对象
:return:
'''
# 获取种子列表信息
seed_list = services.get_seed_list()
# 获取宠物列表信息
pet_list = services.get_pet_list()
# 获取宠物粮列表信息
pet_food_list = services.get_pet_food_list()
# 获取种植加速道具列表信息
plant_list = services.get_plant_list()
return {
'errno': code.CODE_OK,
'errmsg': message.ok,
'seed_list': seed_list,
'pet_list': pet_list,
'pet_food_list': pet_food_list,
'plant_list': plant_list,
}
# 购买商品道具
@jwt_required()
@decorator.get_user_object
def pay_props(user, prop_num, prop_type, prop_id, buy_type):
'''
购买商品道具
:param user: 装饰器通过token值获取的用户模型对象
:param prop_num: 购买道具数量
:param prop_type: 购买道具类型
:param prop_id: 道具ID
:param buy_type: 购买方式(金钱或积分)
:return:
'''
# 1.获取想要购买的道具信息
if prop_type == 'seed':
'''获取购买种子的道具信息'''
prop_info = services.get_seed_item(prop_id)
elif prop_type == 'pet':
'''获取购买宠物的道具信息'''
prop_info = services.get_pet_item(prop_id)
elif prop_type == 'pet_food':
'''获取购买宠物粮的道具信息'''
prop_info = services.get_pet_food_item(prop_id)
elif prop_type == 'plant':
'''获取购买种植加速的道具信息'''
prop_info = services.get_plant_item(prop_id)
else: # 没有要购买的道具类型
return {
'error': code.CODE_NO_SUCH_PROP,
'errmsg': message.no_such_prop
}
# 2.计算当前购买道具的成本[价格和积分]
total_price = float(prop_info['price']) * int(prop_num)
total_credit = int(prop_info['credit']) * int(prop_num)
# 3.金钱购买
if buy_type == 'price':
# 3.1 判断用户金钱是否足够
if user.money < total_price:
return {
'errno': code.CODE_NOT_ENOUGH_MONEY,
'errmsg': message.not_enough_money
}
# 3.2 添加新道具到mongDB中用户背包道具列表
document = {
"user_id": user.id, # 用户ID,
"type": prop_type, # 道具类型
"prop_id": prop_info["id"], # 道具ID
"prop_name": prop_info["name"], # 道具名称
"prop_image": prop_info["image"], # 道具图片
"prop_remark": prop_info["remark"], # 道具描述信息
"num": prop_num, # 道具数量
"use_time": prop_info["use_time"], # 道具有效使用时间
"buy_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 购买时间
}
try:
services.add_prop_package(document)
except services.PACKAGE_SPACE_NOT_ENOUGH:
# 用户背包储存空间不足
return {
'errno': code.CODE_PACKAGE_SPACE_NOT_ENOUGH,
'errmsg': message.package_space_not_enough
}
# 3.3 用户使用金钱购买道具,更新用户信息
try:
services.user_buy_prop(user,total_price,0)
except services.BUY_ERROR:
# 购买道具失败
return {
"errno": code.CODE_BUY_PROP_FAIL,
"errmsg": message.buy_prop_fail,
}
# 3.4 往mongoDB中添加用户购买记录
document = {
"user_id": user.id, # 用户ID
"type": prop_type, # 道具类型
"prop_id": prop_info["id"], # 道具ID
"num": prop_num, # 道具数量
"item_price": prop_info["price"], # 道具金钱单价
"total_price": total_price, # 购买总价格
"buy_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
services.add_buy_log(document)
# 4.积分购买
if buy_type == 'credit':
# 判断用户金钱是否足够
if user.credit < total_credit:
return {
'errno': code.CODE_NOT_ENOUGH_CREDIT,
'errmsg': message.not_enough_credit
}
# 4.2 添加新道具到mongDB中用户背包道具列表
document = {
"user_id": user.id, # 用户ID,
"type": prop_type, # 道具类型
"prop_id": prop_info["id"], # 道具ID
"prop_name": prop_info["name"], # 道具名称
"prop_image": prop_info["image"], # 道具图片
"prop_remark": prop_info["remark"], # 道具描述信息
"num": prop_num, # 道具数量
"use_time": prop_info["use_time"], # 道具有效使用时间
"buy_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 购买时间
}
services.add_prop_package(document)
# 4.3 用户使用金钱购买道具,更新用户信息
try:
services.user_buy_prop(user,0,total_credit)
except services.BUY_ERROR:
# 购买道具失败
return {
"errno": code.CODE_BUY_PROP_FAIL,
"errmsg": message.buy_prop_fail,
}
# 4.4 往mongoDB中添加用户购买记录
document = {
"user_id": user.id, # 用户ID
"type": prop_type, # 道具类型
"prop_id": prop_info["id"], # 道具ID
"num": prop_num, # 道具数量
"item_credit": prop_info["credit"], # 道具积分单价
"total_credit": total_credit, # 购买总价格
"buy_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
services.add_buy_log(document)
# 刷新用户token值
token = user_services.generate_user_token(user)
return {
'errno': code.CODE_OK,
'errmsg': message.ok,
**token
}
因为接下来,需要把道具购买日志记录和用户背包信息记录到MongoDB中,所以我们需要在flask中初始化MongoDB。
flask集成MongoDB
- 安装
pip install Flask-PyMongo
- 在项目入口文件中,初始化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
# 终端脚本工具初始化
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)
# MongoDB加载配置
mongo.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
- 开发环境配置MongoDB数据库连接地址,
application.settings.dev
,代码:
"""MongoDB数据库配置"""
# 数据库链接地址
MONGO_URI = 'mongodb://mofang:123@127.0.0.1:27017/mofang'
# 连接池的默认创建连接数
MONGO_MAX_POOL_SIZE = 20
- 数据服务层,种植园数据处理,
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
# 获取种子道具信息
def get_seed_item(prop_id):
'''
获取种子道具信息
:param prop_id: 道具ID
:return:
'''
seed_list = get_seed_list()
for item in seed_list:
if item['id'] == prop_id:
return item
else:
return {}
# 获取宠物道具信息
def get_pet_item(prop_id):
'''
获取宠物道具信息
:param prop_id: 道具ID
:return:
'''
pet_list = get_pet_list()
for item in pet_list:
if item['id'] == prop_id:
return item
else:
return {}
# 获取宠物粮道具信息
def get_pet_food_item(prop_id):
'''
获取宠物粮道具信息
:param prop_id: 道具ID
:return:
'''
pet_food_list = get_pet_food_list()
for item in pet_food_list:
if item['id'] == prop_id:
return item
else:
return {}
# 获取宠物粮道具信息
def get_plant_item(prop_id):
'''
获取种植加速道具信息
:param prop_id: 道具ID
:return:
'''
plant_list = get_plant_list()
for item in plant_list:
if item['id'] == prop_id:
return item
else:
return {}
# 用户背包储存空间不足错误
class PACKAGE_SPACE_NOT_ENOUGH(Exception):
pass
# 添加道具到用户背包列表
def add_prop_package(document):
'''
添加道具到用户背包列表
:param document: 本次添加到背包中的数据
:return:
'''
# 1.获取当前用户背包信息
query = {'user_id':document['user_id']}
package_info = mongo.db.user_package.find_one(query)
if not package_info:
# 如果当前用户没有背包,则初始化用户背包
user_init_package(document["user_id"])
# 再次查询背包信息
package_info = mongo.db.user_package.find_one(query)
# 2.获取背包中的道具列表
props_list = package_info.get('props_list')
# 3.判断当前道具是否在当前背包中已经有同类型的
has_key = -1 # 如果曾经购买过当前道具,则保存下标, 默认没有买过,所以下标为-1
has_buy = False # 设置购买状态,默认曾经没有买过
for key, props in enumerate(props_list):
# 不仅要判断道具ID还要判断道具的类型
if (document["prop_id"] == props["prop_id"]) and (document["type"] == props["type"]):
has_buy = True
has_key = key
break
# 4.判断背包是否还有空间保存道具
if props_list.__len__() == package_info['capacity']:
# 想要添加的道具还没有购买过,需要占一个背包空间
if not has_buy:
raise PACKAGE_SPACE_NOT_ENOUGH
# 5.判断之前是否购买过相同道具
if has_buy:
# 曾经购买过相同的道具,数量累加
document['num'] += props_list[has_key]['num']
update = {'$set':{f'props_list.{has_key}': document}}
else:
# 没有购买过同样的道具,本次购买的就是总数量
update = {'$push':{'props_list': document}}
mongo.db.user_package.update_one(query, update)
# 初始化指定用户的背包
def user_init_package(user_id):
'''
初始化指定用户的背包
:param user_id: 用户ID
:return:
'''
document = {
'user_id': user_id, # 用户ID
'capacity': config.INIT_PACKAGE_CAPACITY, # 背包存储上限
'props_list':[],# 道具列表
}
mongo.db.user_package.insert_one(document)
# 用户购买道具失败错误
class BUY_ERROR(Exception):
pass
# 用户购买道具
def user_buy_prop(user, total_price, total_credit):
'''
用户购买道具
:param user: 当前购买道具的用户模型
:param total_price: 本次购买道具花费的总价格
:param total_credit: 本次购买道具花费的总积分
:return:
'''
try:
# 修改用户金钱和积分信息
user.money = float(user.money) - float(total_price)
user.credit = float(user.credit) - float(total_credit)
db.session.commit()
except Exception:
raise BUY_ERROR
# 添加用户购买记录到MongoDB中
def add_buy_log(document):
'''
添加用户购买记录到MongoDB中
:param document: 本次购买道具的日志信息
:return:
'''
mongo.db.user_buy_prop_log.insert_one(document)
- 种植园配置信息,
settings.plant
,代码:
# 初始化时,默认背包存储上限
INIT_PACKAGE_CAPACITY = 8
- 提示码和提示信息
提示码,application/utils/code.py
,代码:l
CODE_NO_SUCH_PROP = 1101 # 没有该道具
CODE_NOT_ENOUGH_MONEY = 1102 # 没有足够的金钱
CODE_NOT_ENOUGH_CREDIT = 1103 # 没有足够的积分
CODE_BUY_PROP_FAIL = 1104 # 购买道具失败
CODE_PACKAGE_SPACE_NOT_ENOUGH = 1105 # 背包储存空间不足
提示信息,application/utils/message.py
,代码:
no_such_prop = '没有该道具'
not_enough_money = '没有足够的金钱'
not_enough_credit = '没有足够的积分'
buy_prop_fail = '购买道具失败'
package_space_not_enough = '背包储存空间不足'
- 路由
orchard/urls.py
代码:
from application import path, api_rpc
# 引入当前蓝图应用视图, 引入rpc视图
from . import views, api
# 蓝图路径与函数映射列表
urlpatterns = [
# path('/index', views.index, methods=['post']),
]
# rpc方法与函数映射列表[rpc接口列表]
# user = api.User() # 实例化类视图
apipatterns = [
api_rpc('goods_list', api.get_goods_list), # 获取商店道具列表
api_rpc('pay_props', api.pay_props), # 购买商店道具
]
客户端在购买成功以后,更新本地token信息
- 道具详情购买页面
html/item_details.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.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.image}`" alt="">
</div>
<div class="invite_tips item_remark">
<p>{{item.remark}}</p><br>
<p>
<span>价格:{{item.price.toFixed(2)}}</span>
<span>果子:{{item.credit}}</span>
</p><br><br>
<p @click="gotopay">立即购买</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: "",
buy_type: ['price', 'credit']
}
},
created(){
// 接受传递过来的参数
this.get_page_params();
},
methods:{
back(){
this.game.closeFrame();
},
get_page_params(){
// 接受来自商店页面shop.html的参数
this.item = api.pageParam.item;
this.type = api.pageParam.type;
},
// 点击购买道具
gotopay(){
let self = this
let buttons = [1,2,5,10,20,50];
api.actionSheet({
title: `购买${this.item.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.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,
});
}
// 发布全局广播
self.game.sendEvent('buy_prop_success')
self.game.sendEvent('update_token')
self.game.tips('购买道具成功~')
// 关闭页面
self.game.closeFrame()
}
}
})
})
},
}
});
}
</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">
<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',
socket: null,
}
},
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');
})
},
back(){
this.game.openWin("root");
},
}
});
}
</script>
</body>
</html>