Browse Source

初始化

fangchen 5 years ago
commit
3ccf5361a9
100 changed files with 6125 additions and 0 deletions
  1. 47 0
      .gitignore
  2. 45 0
      README.md
  3. 24 0
      client/LICENSE
  4. 53 0
      client/README.md
  5. 199 0
      client/app.js
  6. 19 0
      client/app.json
  7. 12 0
      client/app.wxss
  8. 35 0
      client/config.js
  9. 1 0
      client/imgs/PK_equal.svg
  10. 1 0
      client/imgs/PK_fail.svg
  11. 1 0
      client/imgs/PK_success.svg
  12. 1 0
      client/imgs/VS.svg
  13. BIN
      client/imgs/entry_fighting.jpg
  14. BIN
      client/imgs/entry_friends.jpg
  15. 1 0
      client/imgs/entry_qr.svg
  16. BIN
      client/imgs/entry_rank.jpg
  17. 1 0
      client/imgs/exp.svg
  18. 1 0
      client/imgs/quit.svg
  19. 1 0
      client/imgs/share.svg
  20. 16 0
      client/package.json
  21. 97 0
      client/pages/entry/entry.js
  22. 1 0
      client/pages/entry/entry.json
  23. 28 0
      client/pages/entry/entry.wxml
  24. 58 0
      client/pages/entry/entry.wxss
  25. 66 0
      client/pages/fight/fight.js
  26. 3 0
      client/pages/fight/fight.json
  27. 2 0
      client/pages/fight/fight.wxml
  28. 1 0
      client/pages/fight/fight.wxss
  29. 24 0
      client/pages/fighting_match/fighting_match.js
  30. 1 0
      client/pages/fighting_match/fighting_match.json
  31. 15 0
      client/pages/fighting_match/fighting_match.wxml
  32. 123 0
      client/pages/fighting_match/fighting_match.wxss
  33. 280 0
      client/pages/fighting_room/fighting_room.js
  34. 1 0
      client/pages/fighting_room/fighting_room.json
  35. 64 0
      client/pages/fighting_room/fighting_room.wxml
  36. 193 0
      client/pages/fighting_room/fighting_room.wxss
  37. 46 0
      client/pages/fighting_sort/fighting_sort.js
  38. 1 0
      client/pages/fighting_sort/fighting_sort.json
  39. 6 0
      client/pages/fighting_sort/fighting_sort.wxml
  40. 16 0
      client/pages/fighting_sort/fighting_sort.wxss
  41. 85 0
      client/pages/friends_match/friends_match.js
  42. 1 0
      client/pages/friends_match/friends_match.json
  43. 23 0
      client/pages/friends_match/friends_match.wxml
  44. 99 0
      client/pages/friends_match/friends_match.wxss
  45. 77 0
      client/pages/friends_sort/friends_sort.js
  46. 1 0
      client/pages/friends_sort/friends_sort.json
  47. 15 0
      client/pages/friends_sort/friends_sort.wxml
  48. 55 0
      client/pages/friends_sort/friends_sort.wxss
  49. 73 0
      client/pages/rank/rank.js
  50. 1 0
      client/pages/rank/rank.json
  51. 47 0
      client/pages/rank/rank.wxml
  52. 56 0
      client/pages/rank/rank.wxss
  53. 22 0
      client/pages/userInfos/userInfo.js
  54. 3 0
      client/pages/userInfos/userInfo.json
  55. 10 0
      client/pages/userInfos/userInfo.wxml
  56. 23 0
      client/pages/userInfos/userInfo.wxss
  57. 40 0
      client/project.config.json
  58. 1579 0
      client/utils/animate.wxss
  59. 6 0
      client/utils/base_bg.wxss
  60. 83 0
      client/utils/tunnelMacth.js
  61. 46 0
      client/utils/upDateShareInfoToUser_network.js
  62. 15 0
      client/utils/upDateUser_networkFromClickId.js
  63. 42 0
      client/utils/util.js
  64. 24 0
      client/vendor/wafer2-client-sdk/LICENSE
  65. 247 0
      client/vendor/wafer2-client-sdk/README.md
  66. 25 0
      client/vendor/wafer2-client-sdk/index.js
  67. 20 0
      client/vendor/wafer2-client-sdk/lib/constants.js
  68. 182 0
      client/vendor/wafer2-client-sdk/lib/login.js
  69. 112 0
      client/vendor/wafer2-client-sdk/lib/request.js
  70. 18 0
      client/vendor/wafer2-client-sdk/lib/session.js
  71. 536 0
      client/vendor/wafer2-client-sdk/lib/tunnel.js
  72. 18 0
      client/vendor/wafer2-client-sdk/lib/utils.js
  73. 32 0
      client/vendor/wafer2-client-sdk/lib/wxTunnel.js
  74. 47 0
      client/vendor/wafer2-client-sdk/package.json
  75. BIN
      imgs_md/1.gif
  76. BIN
      imgs_md/2.gif
  77. BIN
      imgs_md/3.gif
  78. BIN
      imgs_md/4.gif
  79. 55 0
      project.config.json
  80. 16 0
      server/.eslintrc.json
  81. 126 0
      server/README.md
  82. 19 0
      server/app.js
  83. 43 0
      server/config.js
  84. 18 0
      server/config.local.js.1.example
  85. 17 0
      server/controllers/getGId.js
  86. 35 0
      server/controllers/getRankFriendsData.js
  87. 18 0
      server/controllers/getRankGlobalData.js
  88. 10 0
      server/controllers/get_score.js
  89. 30 0
      server/controllers/index.js
  90. 9 0
      server/controllers/login.js
  91. 29 0
      server/controllers/message.js
  92. 34 0
      server/controllers/qr_address.js
  93. 10 0
      server/controllers/question_sort.js
  94. 10 0
      server/controllers/storeFriendsNetwork.js
  95. 11 0
      server/controllers/storeUser_network.js
  96. 459 0
      server/controllers/tunnel.js
  97. 10 0
      server/controllers/upDateShareInfoToUser_network.js
  98. 10 0
      server/controllers/upDateUser_networkFromClickId.js
  99. 9 0
      server/controllers/upload.js
  100. 0 0
      server/controllers/user.js

+ 47 - 0
.gitignore

@@ -0,0 +1,47 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+# Coverage directory used by tools like istanbul
+coverage
+# nyc test coverage
+.nyc_output
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+# Bower dependency directory (https://bower.io/)
+bower_components
+# node-waf configuration
+.lock-wscript
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+# Dependency directories
+node_modules/
+jspm_packages/
+# Typescript v1 declaration files
+typings/
+# Optional npm cache directory
+.npm
+# Optional eslint cache
+.eslintcache
+# Optional REPL history
+.node_repl_history
+# Output of 'npm pack'
+*.tgz
+# Yarn Integrity file
+.yarn-integrity
+# dotenv environment variables file
+.env
+.vscode
+# ignore sh
+sh/
+# ignore test sdk.config.json
+sdk.config.json
+# ignore local config
+server/config.local.js

+ 45 - 0
README.md

@@ -0,0 +1,45 @@
+# wechat-weapp-Game-TNFB
+一款基于wafer2方案的实时在线答题微信小程序游戏
+
+##技术栈:
+* 前端:微信小程序
+* 后端:koa+knex
+
+## 软件截图:
+![图1](imgs_md/1.gif)  ![图2](imgs_md/2.gif)  
+![图3](imgs_md/3.gif)  ![图4](imgs_md/4.gif)
+
+## 项目特点:
+* 包含前后端整套代码
+* 采用wafer2-node解决方案,前端一个人即可完成整个项目
+* 采用websocket,实现实时通信
+
+
+## 目录结构说明:
+```
+├─client # 小程序端代码
+│   ├─imgs # 图片存放处
+│   ├─pages # 各个页面代码的存放处
+│   │─utils # 工具包
+│   ├─app.js # 全局JS
+│   ├─app.json # 全局配置
+│   ├─app.wxss # 全局样式
+├─server # 服务器端代码
+└─project.config.json # 项目配置      
+
+```
+
+
+## 使用步骤:
+```
+1、克隆项目到本地:git clone git@github.com:wechat-wxapp/wechat-weapp-Game-TNFB.git 
+2、开通腾讯云
+3、修改client/config.js,修改请求地址为腾讯云解决方案分配的域名。修改appId为你的appId。
+4、修改server/config.js,修改appId、appSecret为你自己的,默认的数据库密码为appId。
+5、真机上测试注意打开调试默认,绕过域名检测。
+
+```
+
+### 项目用于学习交流, 转载请注明出处
+
+

+ 24 - 0
client/LICENSE

@@ -0,0 +1,24 @@
+LICENSE - "MIT License"
+
+Copyright (c) 2016 by Tencent Cloud
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.

+ 53 - 0
client/README.md

@@ -0,0 +1,53 @@
+腾讯云微信小程序一站式解决方案客户端示例
+=====================================
+
+本示例包含:
+
+1. 登录接口使用示例
+2. 进行带会话的网络请求示例
+3. WebSocket 信道服务使用示例
+
+> 注意:所有示例均需要配合解决方案的云资源运行,具体请到[腾讯云控制台](https://console.qcloud.com/la)进行购买和配置。
+
+## 运行示例
+
+云资源准备好之后,修改 `config.js` 里面的服务域名:
+
+```js
+// 此处主机域名修改成腾讯云解决方案分配的域名
+var host = 'yourid.qcloud.la';
+```
+
+修改之后,就可以使用微信开发者工具运行本示例。
+
+## 源码简介
+
+```tree
+Demo
+├── LICENSE
+├── README.md
+├── app.js
+├── app.json
+├── bower.json
+├── config.js
+├── package.json
+├── pages
+│   ├── chat
+│   │   ├── chat.js
+│   │   ├── chat.wxml
+│   │   └── chat.wxss
+│   └── index
+│       ├── index.js
+│       ├── index.wxml
+│       └── index.wxss
+└── vendor
+    └── qcloud-weapp-client-sdk/
+```
+
+`app.js` 是小程序入口文件。
+
+`app.json` 是小程序的微信配置,其中指定了本示例的两个页面,页面分别在 `pages/index/` 和 `pages/chat/` 目录下。
+
+`config.js` 是我们小程序自己的业务配置。
+
+`wafer2-client-sdk` 是[客户端 SDK](https://github.com/tencentyun/wafer2-client-sdk)

+ 199 - 0
client/app.js

@@ -0,0 +1,199 @@
+/**
+ * @fileOverview 微信小程序的入口文件
+ */
+
+var qcloud = require('./vendor/wafer2-client-sdk/index');
+var config = require('./config');
+var util = require('./utils/util.js')
+
+App({
+  // 小程序初始化时执行,我们初始化客户端的登录地址,以支持所有的会话操作
+  appData: {
+    appId: config.service.appId,
+    baseUrl: `${config.service.host}/weapp/`,
+    tunnelStatus: 'close',//统一管理唯一的信道连接的状态:connect、close、reconnecting、reconnect、error
+    friendsFightingRoom:undefined,//好友对战时创建的唯一房间名,作为好友匹配的标识
+  },
+  onLaunch(opt) {
+    this.appData.opt = opt
+    qcloud.setLoginUrl(config.service.loginUrl);  //设置登录地址
+    this.doLogin();
+  },
+  onShow(opt) {
+    this.storeUser_network(opt)//每次打开程序都启动存储用户关系表
+  },
+  doLogin() { //登录
+    let that = this
+    util.showBusy('正在登录');
+    qcloud.login({
+      success(result) {//此处的result竟然不包含openid,所以res取缓存中的数据
+        util.showSuccess('登录成功')
+        let res = wx.getStorageSync('user_info_F2C224D4-2BCE-4C64-AF9F-A6D872000D1A');
+        if (that.userInfoReadyCallback) {
+          that.userInfoReadyCallback(res)
+        }
+      },
+      fail(error) {
+        util.showModel('登录失败', error);
+      }
+    });
+  },
+  pageGetUserInfo(page, openIdReadyCallback) { //在page中获取用户信息
+    const userInfo = wx.getStorageSync('user_info_F2C224D4-2BCE-4C64-AF9F-A6D872000D1A')
+    if (userInfo) {
+      page.setData({
+        userInfo,
+        openId: userInfo.openId
+      })
+      this.appData.openId = userInfo.openId
+      if (openIdReadyCallback) {
+        openIdReadyCallback(userInfo.openId)
+      }
+    } else {
+      this.userInfoReadyCallback = (userInfo) => {  //获取用户信息后的回调函数
+        page.setData({  //每个page都会自动存储userInfo和openId
+          userInfo,
+          openId: userInfo.openId
+        })
+        if (openIdReadyCallback) {  //如果设置了openid的回调函数,则调用回调
+          openIdReadyCallback(userInfo.openId)
+        }
+      }
+    }
+  },
+  //tunnel:由于一个小程序只能同时连接一个信道而且设计页面跳转后信道对象会销毁问题,所以将其放在app.js中统一管理
+  tunnelCreate() {//创建一个新信道,并监听相关数据的变化
+    const that = this
+    const tunnel = that.tunnel = new qcloud.Tunnel(config.service.tunnelUrl)  //放在app对象下供全局使用
+    tunnel.open()
+    tunnel.on('connect', () => {//监听信道连接
+      console.info("tunnelStatus = 'connect'")
+      this.appData.tunnelStatus = 'connect' //改变信道状态为已连接
+      if (that.tunnelConnectCallback) {//设置回调
+        that.tunnelConnectCallback()
+      }
+    })
+    tunnel.on('close', () => {//监听信道断开
+      console.info("tunnelStatus = 'close'")
+      this.appData.tunnelStatus = 'close' //改变信道状态为已断开
+      if (that.tunnelCloseCallback) {//设置回调
+        that.tunnelCloseCallback()
+      }
+    })
+    tunnel.on('reconnecting', () => {//监听信道重新链接
+      console.info("tunnelStatus = 'reconnecting'")
+      this.appData.tunnelStatus = 'reconnecting' //改变信道状态为重新连接中
+      if (that.tunnelReconnectingCallback) {//设置回调
+        that.tunnelReconnectingCallback()
+      }
+    })
+    tunnel.on('reconnect', () => {//监听信道重新连接成功
+      console.info("tunnelStatus = 'reconnect'")
+      console.info('重连后的信道为:' + tunnel.socketUrl.slice(tunnel.socketUrl.indexOf('tunnelId=') + 9, tunnel.socketUrl.indexOf('&')))
+      this.appData.tunnelStatus = 'reconnect' //改变信道状态为重新连接成功
+      if (that.tunnelReconnectCallback) {//设置回调
+        that.tunnelReconnectCallback()
+      }
+    })
+    tunnel.on('error', () => {//监听信道发生错误
+      console.info("tunnelStatus = 'error'")
+      this.appData.tunnelStatus = 'error' //改变信道状态为发生错误
+      util.showSuccess('您已断线,请检查联网')
+      wx.navigateBack({
+        url: '../entry/entry'
+      })
+      if (that.tunnelErrorCallback) {//设置回调
+        that.tunnelErrorCallback()
+      }
+    })
+    tunnel.on('PING', () => {//PING-PONG机制:监听服务器PING
+      console.info("接收到PING")
+      tunnel.emit('PONG', {//给出回应
+        openId: this.appData.openId
+      })
+      console.info("发出了PONG")
+    })
+    
+  },
+
+  /******************用户关系点击表操作******************/
+  //注意1:所有从分享中启动的页面onLoad都添加:  
+  /*
+    app.appData.fromClickId = opt.currentClickId
+    app.upDateUser_networkFromClickId = require('../../utils/upDateUser_networkFromClickId.js').upDateUser_networkFromClickId
+    wx.showShareMenu({
+      withShareTicket: true
+    })
+  */
+  //注意2:所有分享页面路径都添加:?currentClickId=' + app.appData.currentClickId,
+  //注意3:所有分享页面的分享成功回调都添加: require('../../utils/upDateShareInfoToUser_network.js').upDateShareInfoToUser_network(app,that,res)
+
+  storeUser_network(opt) {
+    const that = this
+    const userInfo = wx.getStorageSync('user_info_F2C224D4-2BCE-4C64-AF9F-A6D872000D1A')
+    if (userInfo) {//已缓存的用户数据直接用
+      getGId(that, userInfo, opt)
+    } else {
+      this.userInfoReadyCallback = (userInfo) => {  //获取用户信息后的回调函数
+        getGId(that, userInfo, opt)
+      }
+    }
+    function getGId(that, userInfo, opt) {
+      //判断是否是从微信群内打开该程序的
+      wx.getShareInfo({
+        shareTicket: opt.shareTicket,
+        //含GId的情况
+        success: (res) => {
+          qcloud.request({
+            login: false,
+            data: {
+              appId: that.appData.appId,
+              openId: userInfo.openId,
+              encryptedData: res.encryptedData,
+              iv: res.iv
+            },
+            url: `${that.appData.baseUrl}getGId`,
+            success: (res) => {
+              let GId = res.data.data
+              store(that, userInfo, opt, GId)
+            }
+          })
+        },
+        //不含GId的情况
+        fail: function (res) {
+          store(that, userInfo, opt)
+        }
+      })
+    }
+    function store(that, userInfo, opt, GId = '') {  //参数内要写that:that作为一个对象不能凭空产生
+      let data = {
+        //clickId:自动生成的数据,
+        fromClickId: that.appData.fromClickId ? that.appData.fromClickId : 0,//从哪个clickId那里打开的
+        appId: that.appData.appId,
+        openId: userInfo.openId,
+        fromGId: GId,
+        scene: opt.scene,
+        //time:自动生成的数据,
+        //param_1:转发时才会更新当前clickId中的param_1数据
+      }
+      //将数据存储到数据库点击表中,同时将得到的clickId放在全局变量供page分享时调用
+      qcloud.request({
+        login: false,
+        data,
+        url: `${that.appData.baseUrl}storeUser_network`,
+        success: (res) => {
+          let currentClickId = res.data.data
+          that.appData.currentClickId = currentClickId//设置当前新插入的clickId为全局fromClickId
+          let fromClickId = that.appData.fromClickId
+          if (that.upDateUser_networkFromClickId && fromClickId) {//存在fromClickId,则进行数据库更新
+            that.upDateUser_networkFromClickId(that, currentClickId, fromClickId)
+          }
+        }
+      });
+    }
+  },
+  globalData: {
+    condition: true
+  },
+  /******************用户关系点击表操作******************/
+});

+ 19 - 0
client/app.json

@@ -0,0 +1,19 @@
+{
+  "pages": [
+    "pages/entry/entry",
+    "pages/fighting_sort/fighting_sort",
+    "pages/fighting_match/fighting_match",
+    "pages/friends_sort/friends_sort",
+    "pages/friends_match/friends_match",
+    "pages/rank/rank",
+    "pages/userInfos/userInfo",
+    "pages/fighting_room/fighting_room"
+  ],
+  "window": {
+    "backgroundColor": "#052152",
+    "backgroundTextStyle": "light",
+    "navigationBarBackgroundColor": "#3883FA",
+    "navigationBarTitleText": "知识比拼",
+    "navigationBarTextStyle": "white"
+  }
+}

+ 12 - 0
client/app.wxss

@@ -0,0 +1,12 @@
+/**app.wxss**/
+.container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  box-sizing: border-box;
+  font-size: 30rpx;
+} 
+image{
+  width: 60rpx;
+  height: 60rpx;
+}

+ 35 - 0
client/config.js

@@ -0,0 +1,35 @@
+/**
+ * 小程序配置文件
+ */
+
+// 此处主机域名修改成腾讯云解决方案分配的域名
+//var host = 'https://2ljpzczu.qcloud.la'  //开发环境
+var host = 'http://10.32.2.208:5757'  //开发环境
+// var host = 'https://dgt.dgtis.com'  //开发环境
+
+// var appId = 'wx0eedaadeee1d7f8a'
+var appId ='wxc65eea6a56ef0fd5'
+
+var config = {
+
+    // 下面的地址配合云端 Demo 工作
+    service: {
+        appId,
+
+        host,
+
+        // 登录地址,用于建立会话
+        loginUrl: `${host}/weapp/login`,
+
+        // 测试的请求地址,用于测试会话
+        requestUrl: `${host}/weapp/user`,
+
+        // 测试的信道服务地址
+        tunnelUrl: `${host}/weapp/tunnel`,
+
+        // 上传图片接口
+        uploadUrl: `${host}/weapp/upload`
+    }
+}
+
+module.exports = config

File diff suppressed because it is too large
+ 1 - 0
client/imgs/PK_equal.svg


File diff suppressed because it is too large
+ 1 - 0
client/imgs/PK_fail.svg


File diff suppressed because it is too large
+ 1 - 0
client/imgs/PK_success.svg


File diff suppressed because it is too large
+ 1 - 0
client/imgs/VS.svg


BIN
client/imgs/entry_fighting.jpg


BIN
client/imgs/entry_friends.jpg


File diff suppressed because it is too large
+ 1 - 0
client/imgs/entry_qr.svg


BIN
client/imgs/entry_rank.jpg


File diff suppressed because it is too large
+ 1 - 0
client/imgs/exp.svg


File diff suppressed because it is too large
+ 1 - 0
client/imgs/quit.svg


File diff suppressed because it is too large
+ 1 - 0
client/imgs/share.svg


+ 16 - 0
client/package.json

@@ -0,0 +1,16 @@
+{
+  "name": "qcloud-weapp-client-demo",
+  "version": "2.0.0",
+  "description": "腾讯云微信小程序客户端 DEMO",
+  "main": "app.js",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/tencentyun/weapp-client-demo.git"
+  },
+  "keywords": [],
+  "author": "CFETeam",
+  "license": "MIT",
+  "dependencies": {
+    "wafer2-client-sdk": "^1.0.0"
+  }
+}

+ 97 - 0
client/pages/entry/entry.js

@@ -0,0 +1,97 @@
+//index.js
+var qcloud = require('../../vendor/wafer2-client-sdk/index')
+var config = require('../../config')
+var util = require('../../utils/util.js')
+const app = getApp();
+Page({
+	data: {
+		score: 0,
+		condition:app.globalData.condition
+	},
+	onLoad(opt) {
+		console.log(opt)
+		app.appData.fromClickId = opt.currentClickId
+		app.upDateUser_networkFromClickId = require('../../utils/upDateUser_networkFromClickId.js').upDateUser_networkFromClickId
+		wx.showShareMenu({
+			withShareTicket: true
+		})
+		app.pageGetUserInfo(this, this.getScore)
+	},
+	onShow() {
+		if (this.data.openId) {
+			this.getScore(this.data.openId)
+		}
+		this.closeTunnel()//当信道连接或者重连了时,关闭已连接的信道
+	},
+	// 从授权页面跳过来重新赋值userInfo 要不会没有头像信息
+	fnInit(e){
+		var userInfo = e.detail.userInfo
+		this.setData({
+			userInfo
+		})
+	},
+	onShareAppMessage(res) {
+		const that = this;
+		return {
+			title: '谁才是头脑王者?比比看吧!',
+			path: `/pages/entry/entry?currentClickId=${app.appData.currentClickId}`,
+			success: (res) => {
+				//转发时向用户关系表中更新一条转发记录(个人为person,群为GId)。
+				require('../../utils/upDateShareInfoToUser_network.js').upDateShareInfoToUser_network(app, that, res)
+			}
+		}
+	},
+	getScore(openId) {
+		if (openId) {
+			qcloud.request({
+				login: false,
+				url: `${app.appData.baseUrl}get_score`,
+				data: {
+					openId
+				},
+				success: (res) => {
+					let score = res.data.data;
+					this.setData({
+						score
+					})
+				},
+				fail(error) {
+					util.showModel('请求失败', error);
+				},
+			});
+		}
+	},
+	gotoFighting() {
+		wx.navigateTo({
+			url: '../fighting_sort/fighting_sort'
+		})
+	},
+	gotoFriends() {
+		wx.navigateTo({
+			url: '../friends_sort/friends_sort'
+		})
+	},
+	gotoCustomRoom() {
+		wx.navigateTo({
+			url: '../custom_room/custom_room'
+		})
+	},
+	gotoRank() {
+		wx.navigateTo({
+			url: '../rank/rank'
+		})
+	},
+	get_qr_address() {
+		qcloud.request({
+			login: false,
+			url: `${app.appData.baseUrl}qr_address`,
+			success: (res) => { },
+		});
+	},
+	closeTunnel() {
+		//当信道连接或者重连了时,关闭已连接的信道
+		if (app.appData.tunnelStatus == 'connect' || app.appData.tunnelStatus == 'reconnect') {
+			app.tunnel.close();
+		}
+	}
+})

+ 1 - 0
client/pages/entry/entry.json

@@ -0,0 +1 @@
+{}

+ 28 - 0
client/pages/entry/entry.wxml

@@ -0,0 +1,28 @@
+<view class='container'>
+  <!-- <view class="yonghu">
+    <button class="conditions" open-type="getUserInfo" wx:if="{{condition}}" bind:tap="clckTap" lang="zh_CN" bindgetuserinfo="onGotUserInfo">
+      获取用户信息
+    </button>
+  </view> -->
+  <view class='person-infor'>
+    <image src='{{userInfo.avatarUrl}}' class='avatarUrl'></image>
+    <view style='text-align:center;'>
+      <view>{{userInfo.nickName}}</view>
+      <view>得分:{{score}}</view>
+    </view>
+    <image src='../../imgs/entry_qr.svg' class='qr' bindtap='get_qr_address'></image> <!--注意不要跳转到web_view-->
+  </view>
+
+  <view class='choose' bindtap='gotoFighting'>
+    <image src='../../imgs/entry_fighting.jpg'></image>
+    <view>排位赛</view>
+  </view>
+   <view class='choose' bindtap='gotoFriends'>
+    <image src='../../imgs/entry_friends.jpg'></image>
+    <view>好友对战</view>
+  </view> 
+  <view class='choose' bindtap='gotoRank'>
+    <image src='../../imgs/entry_rank.jpg'></image>
+    <view>好友排名</view>
+  </view>
+</view>

+ 58 - 0
client/pages/entry/entry.wxss

@@ -0,0 +1,58 @@
+@import "../../utils/base_bg.wxss";
+
+.person-infor {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16rpx;
+  width: 90%;
+  margin: 0 auto;
+  background-color: #155fd8;
+  border-radius: 10rpx;
+  color: white;
+}
+
+.avatarUrl {
+  width: 100rpx;
+  height: 100rpx;
+  border: 4rpx solid white;
+  border-radius: 50%;
+}
+
+.qr {
+  width: 100rpx;
+  height: 100rpx;
+}
+
+.choose {
+  position: relative;
+  margin-top: 20rpx;
+  color: #f6f6f6;
+}
+
+.choose view {
+  font-weight: bold;
+  font-size: 36rpx;
+  position: absolute;
+  left: 70%;
+  top: 15%;
+}
+
+.choose image {
+  width: 600rpx;
+  height: 320rpx;
+  border-radius: 10rpx;
+}
+
+.yonghu{
+  width: 100%;
+}
+
+.conditions{
+  font-size:25rpx;
+  padding:0 10rpx;
+  border-radius:5rpx;
+  margin:11rpx 25rpx;
+  width:180rpx;
+
+}

+ 66 - 0
client/pages/fight/fight.js

@@ -0,0 +1,66 @@
+// pages/fight/fight.js
+Page({
+
+  /**
+   * 页面的初始数据
+   */
+  data: {
+
+  },
+
+  /**
+   * 生命周期函数--监听页面加载
+   */
+  onLoad: function (options) {
+
+  },
+
+  /**
+   * 生命周期函数--监听页面初次渲染完成
+   */
+  onReady: function () {
+
+  },
+
+  /**
+   * 生命周期函数--监听页面显示
+   */
+  onShow: function () {
+
+  },
+
+  /**
+   * 生命周期函数--监听页面隐藏
+   */
+  onHide: function () {
+
+  },
+
+  /**
+   * 生命周期函数--监听页面卸载
+   */
+  onUnload: function () {
+
+  },
+
+  /**
+   * 页面相关事件处理函数--监听用户下拉动作
+   */
+  onPullDownRefresh: function () {
+
+  },
+
+  /**
+   * 页面上拉触底事件的处理函数
+   */
+  onReachBottom: function () {
+
+  },
+
+  /**
+   * 用户点击右上角分享
+   */
+  onShareAppMessage: function () {
+
+  }
+})

+ 3 - 0
client/pages/fight/fight.json

@@ -0,0 +1,3 @@
+{
+  "usingComponents": {}
+}

+ 2 - 0
client/pages/fight/fight.wxml

@@ -0,0 +1,2 @@
+<!--pages/fight/fight.wxml-->
+<text>pages/fight/fight.wxml</text>

+ 1 - 0
client/pages/fight/fight.wxss

@@ -0,0 +1 @@
+/* pages/fight/fight.wxss */

+ 24 - 0
client/pages/fighting_match/fighting_match.js

@@ -0,0 +1,24 @@
+const qcloud = require('../../vendor/wafer2-client-sdk/index')
+const config = require('../../config')
+const util = require('../../utils/util.js')
+const match = require('../../utils/tunnelMacth.js').match//引入匹配函数
+const app = getApp();
+Page({
+  data: {
+    status: '初始化...',
+    number: 0,
+  },
+  onLoad: function (opt) {
+    this.matchTimeRun()
+    app.pageGetUserInfo(this, match(this, app, opt))//开始匹配
+  },
+  matchTimeRun() {
+    let number = 1;
+    let time = setInterval(() => {
+      this.setData({
+        number
+      })
+      number++
+    }, 1000)
+  },
+})

+ 1 - 0
client/pages/fighting_match/fighting_match.json

@@ -0,0 +1 @@
+{}

+ 15 - 0
client/pages/fighting_match/fighting_match.wxml

@@ -0,0 +1,15 @@
+<!--pages/pipei/pipei.wxml-->
+
+<view class='img-contain'>
+  <image src='{{userInfo.avatarUrl}}' class='avatarUrl'></image>
+</view>
+<view style='text-align:center;'>{{status}}</view>
+<view class='time'>
+  <view class='t0'>{{number}}</view>
+  <view class="t1"></view>
+  <view class="t2"></view>
+  <view class="t3"></view>
+  <view class="t4"></view>
+  <view class="t5"></view>
+  <view class="t6"></view>
+</view>

+ 123 - 0
client/pages/fighting_match/fighting_match.wxss

@@ -0,0 +1,123 @@
+@import "../../utils/base_bg.wxss";
+
+.img-contain {
+  text-align: center;
+  padding: 150rpx 0;
+}
+
+.avatarUrl {
+  width: 160rpx;
+  height: 160rpx;
+  border: 6rpx solid white;
+  border-radius: 50%;
+}
+
+.time {
+  position: relative;
+  width: 400rpx;
+  height: 400rpx;
+  margin: 100rpx auto;
+}
+
+.t0 {
+  position: absolute;
+  font-size: 90rpx;
+  width: 100%;
+  text-align: center;
+  height: 400rpx;
+  line-height: 400rpx;
+}
+
+.t4, .t5, .t6 {
+  position: absolute;
+  width: 160rpx;
+  height: 160rpx;
+  left: 114rpx;
+  top: 120rpx;
+  border-width: 8rpx;
+  background: transparent;
+  border-style: solid;
+  border-color: #121502;
+  border-radius: 50%;
+}
+
+.t5 {
+  width: 200rpx;
+  height: 200rpx;
+  left: 94rpx;
+  top: 100rpx;
+}
+
+.t6 {
+  width: 240rpx;
+  height: 240rpx;
+  left: 74rpx;
+  top: 80rpx;
+}
+
+.t1, .t2, .t3 {
+  position: absolute;
+  width: 160rpx;
+  height: 160rpx;
+  left: 114rpx;
+  top: 120rpx;
+  border-width: 8rpx;
+  background: transparent;
+  border-style: solid;
+  border-color: transparent #c11212 #c11212 #c11212;
+  border-radius: 50%;
+  -webkit-animation: ".t1" 2s infinite linear; /***一秒循环执行**/
+  z-index: 100;
+}
+
+.t2 {
+  width: 200rpx;
+  height: 200rpx;
+  left: 94rpx;
+  top: 100rpx;
+  -webkit-animation: ".t2" 2.5s infinite linear;
+}
+
+.t3 {
+  width: 240rpx;
+  height: 240rpx;
+  left: 74rpx;
+  top: 80rpx;
+  -webkit-animation: ".t3" 3.5s infinite linear;
+}
+
+@-webkit-keyframes ".t1" {
+  /**关键帧名称**/
+
+  0% {
+    -webkit-transform: rotate(180deg);
+  }
+
+  100% {
+    -webkit-transform: rotate(-180deg);
+  }
+}
+
+@-webkit-keyframes ".t2" {
+  /**关键帧名称**/
+
+  0% {
+    -webkit-transform: rotate(0deg);
+  }
+
+  100% {
+    -webkit-transform: rotate(360deg);
+  }
+}
+
+@-webkit-keyframes ".t3" {
+  /**关键帧名称**/
+
+  0% {
+    -webkit-transform: rotate(360deg);
+  }
+
+  100% {
+    -webkit-transform: rotate(0deg);
+  }
+}

+ 280 - 0
client/pages/fighting_room/fighting_room.js

@@ -0,0 +1,280 @@
+const qcloud = require('../../vendor/wafer2-client-sdk/index')
+const config = require('../../config')
+const util = require('../../utils/util.js')
+const app = getApp()
+const option = {
+	CHOICE_DELAY_SHOW: 1500,//选项延时1.5S显示
+}
+
+Page({
+	data: {
+		roomName: '',//对战房间号
+		userInfo_me: '', //本人用户信息
+		userInfo_others: '',//对手用户信息
+		countdown: 10,//倒计时
+		question: '',//websocket服务器传过来的问题及答案
+		hasClick: false,//判断是否已选答案,不能重新选择
+		localClick: false,//是否本地单击的答案
+		tunnelIdReplacing: 0,//tunnelIdReplacing存在2种转态:0表示不存在信道替换,1表示信道正在替换中:禁止发送数据
+		clickIndex: '',//判断用户选择了哪个答案
+		answerColor: '',//根据选择正确与否给选项添加背景颜色
+		scoreMyself: 0,//自己的总分
+		status_users_others: {
+			openId: '', //对手的openid
+			userChoose: '',//对手选择了第几项
+			answerColor: '',//对手是否选择正确
+		},//对手的答题状态
+		score_others: 0,//对手的总分
+		game_over: false,  //判断此次PK是否结束
+		win: 2,  //0:表示输,1:表示赢,2:表示平手
+		sendNumber: 0,//每一轮的答题次数不能超过1次
+	},
+	onLoad(options) {
+		app.appData.fromClickId = options.currentClickId
+		app.upDateUser_networkFromClickId = require('../../utils/upDateUser_networkFromClickId.js').upDateUser_networkFromClickId
+		wx.showShareMenu({
+			withShareTicket: true
+		})
+		this.setData({
+			roomName: options.roomName,
+			userInfo_me: wx.getStorageSync('user_me'),
+			userInfo_others: wx.getStorageSync('user_others'),
+		})
+		wx.removeStorageSync('user_me')//清理缓存
+		wx.removeStorageSync('user_others')//清理缓存
+		this.startAnimate()//定义开始动画
+		this.fightingReady(options.roomName) //通知服务器我已准备好了
+		this.exceptionalListener()  //监听异常情况,如断线重新连接  
+	},
+	onShareAppMessage(res) {
+		const that = this;
+		return {
+			title: '谁才是头脑王者?比比看吧!',
+			path: `/pages/entry/entry?currentClickId=${app.appData.currentClickId}`,
+			success(res) {
+				//转发时向用户关系表中更新一条转发记录(个人为person,群为GId)。
+				require('../../utils/upDateShareInfoToUser_network.js').upDateShareInfoToUser_network(app, that, res)
+				wx.redirectTo({
+					url: '../entry/entry'
+				})
+			}
+		}
+	},
+	fightingReady(roomName) { //通知服务器我已准备好了
+		const that = this
+		const tunnel = this.tunnel = app.tunnel
+		//tunnel.emit('has_ready', { roomName })//通知服务器,已经准备好可以答题了
+
+		//监听后台是否收到前端发送的选项
+		tunnel.on('getAnswer', (res) => {
+			that.setData({//答题后将hasClick设置为true,防止重新选择答案
+				hasClick: true
+			})
+		})
+		//监听是否在重连,若是,则禁止发送数据到后台
+		app.tunnelReconnectingCallback = () => {
+			that.setData({
+				tunnelIdReplacing: 1,//tunnelIdReplacing存在2种转态:0表示不存在信道替换,1表示信道正在替换中 
+			})
+		}
+		//监听逃跑者的信息
+		tunnel.on('runawayNotice', (res) => {
+			console.log('对手已逃跑')
+			util.showSuccess(res.message)
+			that.setData({
+				game_over: true,
+				win: 1,
+			})
+			app.tunnel.close()
+		})
+		//监听服务器端发送过来的问题
+		let getNextQuestions, timerCountdown, timerReset  //定义倒计时定时器,定义重置定时器(注意:只有将timer_countdown定义在最外边才能清除掉上一个定时器)
+		tunnel.on('sendQuestion', (res) => {
+			console.log('收到题目', res)
+			let question = res.question
+			if (Object.getOwnPropertyNames(question).length) {
+				question.answer = JSON.parse(question.answer)//将答案转换为js对象
+			}
+			//显示对手的答题状态
+			if (res.choicePlayer1[0] !== that.data.userInfo_me.openId) {
+				that.setData({
+					status_users_others: {
+						openId: res.choicePlayer1[0],
+						userChoose: res.choicePlayer1[1],
+						answerColor: res.choicePlayer1[2],
+					},
+					score_others: res.choicePlayer1[3],//对手总分单独拎出来,不更新
+					animate_rightAnswer: 'right',//显示正确答案
+				})
+			} else {
+				that.setData({
+					status_users_others: {
+						openId: res.choicePlayer2[0],
+						userChoose: res.choicePlayer2[1],
+						answerColor: res.choicePlayer2[2],
+					},
+					score_others: res.choicePlayer2[3],//对手总分单独拎出来,不更新
+					animate_rightAnswer: 'right',//显示正确答案
+				})
+			}
+
+			clearTimeout(getNextQuestions)
+			if (Object.getOwnPropertyNames(question).length) {
+				getNextQuestions = setTimeout(function () { //先等待2s查看对方的选择状态,再开始下一题
+					reset(that)//运行重置函数  
+				}, 2000)
+			} else {  //当question中无问题时,即回答完所有问题
+				getNextQuestions = setTimeout(function () { //答完题显示战果
+					if (that.data.scoreMyself > that.data.score_others) {
+						that.setData({
+							game_over: true,
+							win: 1,
+						})
+					} else if (that.data.scoreMyself < that.data.score_others) {
+						that.setData({
+							game_over: true,
+							win: 0,
+						})
+					}
+					else {
+						that.setData({
+							game_over: true
+						})
+					}
+					//将当前用户的比赛结果发送给服务器
+					tunnel.emit('fightingResult', {
+						openId: that.data.userInfo_me.openId,
+						fightingResult: that.data.win
+					})
+				}, 2000)
+			}
+			function reset(that) {//定义重置函数
+				//获取新题目后,倒计时归为10,将clickIndex清空,hasClick改为未选择.
+				that.setData({
+					question,//更新题目
+					animate_showChoice: '',
+					countdown: 10,
+					localClick: false,
+					hasClick: false,
+					clickIndex: '',
+					answerColor: '',
+					//scoreMyself: 0,
+					status_users_others: {
+						openId: '',
+						userChoose: '',
+						answerColor: '',
+						//scoreMyself: 0,
+					},
+					sendNumber: 0,
+					animate_rightAnswer: '',
+				})
+
+				//(重新)开始倒计时
+				clearInterval(timerCountdown)//获取新题目后,倒计时定时器清空(注意:只有将timer_countdown定义在最外边才能清除掉上一个定时器)
+				let countdown = that.data.countdown;
+				setTimeout(() => {//2S后显示选项和开始倒计时
+					that.setData({ animate_showChoice: 'fadeIn' })
+					timerCountdown = setInterval(function () {
+						countdown--
+						that.setData({
+							countdown
+						})
+						if (countdown == 0) {
+							clearInterval(timerCountdown)
+						}
+					}, 1000)
+				}, option.CHOICE_DELAY_SHOW)
+
+				//(重新)设置定时器,若用户未选择答案,10s后也将用户结果发给服务器
+				clearTimeout(timerReset);
+				timerReset = setTimeout(() => {
+					if (!that.data.localClick && !that.data.hasClick) {
+						that.sendAnswer(that)
+					}
+				}, 11000)
+			}
+		})
+	},
+	answer(e) {//开始答题
+		const that = this
+		if (!that.data.localClick) {  //防止重新选择答案
+			if (e.currentTarget.dataset.right) {//判断答案是否正确
+				that.setData({
+					clickIndex: e.currentTarget.dataset.index,
+					answerColor: 'right'
+				})
+				//答对了则加分,时间越少加分越多,总分累加
+				that.setData({
+					scoreMyself: that.data.scoreMyself + that.data.countdown * 10
+				})
+			} else {
+				that.setData({
+					clickIndex: e.currentTarget.dataset.index,
+					answerColor: 'error'
+				})
+			}
+			that.setData({
+				localClick: true//本地已经点击,若hasClick仍未false,则说明没有发送数据出去
+			})
+			that.sendAnswer(that)
+		}
+	},
+	//ping-pong机制,监听前端是否与服务端保持长连接,防止答题期间的卡死问题
+	sendAnswer(that) {//若存在信道替换,且信道替换还未完成,则不发送数据到后台,但是本地还是算点击一次,
+		if (that.data.sendNumber > 0) {
+			return
+		}
+		if (that.data.tunnelIdReplacing == 1 || !(app.appData.tunnelStatus == 'connect' || app.appData.tunnelStatus == 'reconnect')) {//当信道正在替换中时,禁止发送数据
+			return
+		}
+		that.tunnel.emit('answer', {  //将选项、是否正确、总分3项发送给服务器,若双方都已发送,服务器直接返回下一题
+			roomName: that.data.roomName,
+			choice: {
+				openId: that.data.userInfo_me.openId,
+				userChoose: that.data.clickIndex,
+				answerColor: that.data.answerColor,
+				scoreMyself: that.data.scoreMyself,
+			}
+		})
+		let sendNumber = that.data.sendNumber + 1//BUG:保证每一轮发送次数不能大于1,至于为什么会大于1,暂时不清楚原因
+		that.setData({
+			sendNumber,
+		})
+	},
+	//异常处理:重新连接、网络错误
+	exceptionalListener() {
+		const that = this
+		const tunnel = this.tunnel = app.tunnel
+		//重新连接后,信道id会改变,通知服务器信道id已改变,待接收到服务器端信道ID已替换的通知后再将选项发送给服务器
+		app.tunnelReconnectCallback = () => {
+			//信道id完成替换后,将本地的userInfo_me中的tunnel_id进行更新
+			tunnel.on('tunnelIdReplaced', (res) => {//监听信道替换成功
+				let userInfo_me = that.data.userInfo_me
+				userInfo_me.tunnel_id = res.newTunnelId
+				that.setData({//更新当前用户的信道idh和信道替换状态
+					userInfo_me,
+					tunnelIdReplacing: 0
+				})
+				if (that.data.localClick && !that.data.hasClick) {//如果本地已点击过,并且还没发送成功,则向服务器再次发送数据
+					that.sendAnswer(that)
+				}
+			})
+		}
+	},
+	continue_fighting() {
+		wx.reLaunch({
+			url: '../entry/entry',
+		})
+	},
+	startAnimate() {
+		const that = this
+		that.setData({
+			zoomIn: 'zoomIn'
+		})
+		setTimeout(function () {
+			that.setData({
+				zoomOut: 'zoomOut'
+			})
+		}, 1500)
+	}
+})

+ 1 - 0
client/pages/fighting_room/fighting_room.json

@@ -0,0 +1 @@
+{}

+ 64 - 0
client/pages/fighting_room/fighting_room.wxml

@@ -0,0 +1,64 @@
+<view class='header'>
+  <view class='animated lightSpeedIn-left '>
+    <image src='{{userInfo_me.avatarUrl}}'></image>
+    <view style='font-size:26rpx'>{{userInfo_me.nickName}}</view>
+    <view wx:if="{{game_over}}">
+      <text style='font-size:80rpx'>{{scoreMyself}}</text>分</view>
+  </view>
+
+  <view class='countdown animated rubberBand' wx:if="{{game_over ? false : true}}">{{countdown}}</view>
+
+  <view class='animated lightSpeedIn-right'>
+    <image src='{{userInfo_others.avatarUrl}}'></image>
+    <view style='font-size:26rpx'>{{userInfo_others.nickName}}</view>
+    <view wx:if="{{game_over}}">
+      <text style='font-size:80rpx'>{{score_others}}</text>分 </view>
+  </view>
+</view>
+
+<view class='summarize animated flipInX' wx:if="{{game_over}}">
+  <image src="../../imgs/{{win==2? 'PK_equal': win==1?'PK_success' : 'PK_fail'}}.svg" style='width:300rpx;height:300rpx;'></image>
+</view>
+
+<view class='body' wx:if="{{game_over ? false : true}}">
+  <view class='animated animated_zoomIn {{zoomIn}} {{zoomOut}}' wx:if="{{question ? false : true}}">
+    <image src='../../imgs/VS.svg' style='width:600rpx;height:600rpx;'></image>
+  </view>
+  <view class='title animated zoomIn'>{{question.ask}}</view>
+  <view class='content animated fadeIn' wx:if="{{question}}">
+    <view>
+      <view>{{scoreMyself}}</view>
+      <view class='process'>
+        <view class='process_son' style='height: {{scoreMyself/500*100}}%;'></view>
+      </view>
+    </view>
+
+    <view style='width:80%;' class='animated {{animate_showChoice}}' wx:if="{{animate_showChoice}}">
+      <view wx:for="{{question.answer}}" bindtap='answer' data-index="{{index}}" data-right="{{item.right}}" class="{{index==clickIndex&&clickIndex!=='' ? answerColor : ''}}  {{index==status_users_others.userChoose&&status_users_others.userChoose!=='' ? status_users_others.answerColor : ''}}  {{item.right ? animate_rightAnswer:''}} answer">
+        <view class="invisible {{index==clickIndex&&clickIndex!==''&&answerColor=='right' ? 'sign' : ''}}">○</view>
+        <view class="invisible {{index==clickIndex&&clickIndex!==''&&answerColor=='error' ? 'sign' : ''}}">×</view>
+        <view style='width:100%;'>{{item.answer}}</view>
+        <view class="invisible {{index==status_users_others.userChoose&&status_users_others.userChoose!==''&&status_users_others.answerColor=='right' ? 'sign' : ''}}" style='left: 88%;'>○</view>
+        <view class="invisible {{index==status_users_others.userChoose&&status_users_others.userChoose!==''&&status_users_others.answerColor=='error' ? 'sign' : ''}}" style='left: 88%;'>×</view>
+      </view>
+    </view>
+
+    <view>
+      <view>{{score_others}}</view>
+      <view class='process'>
+        <view class='process_son' style='height: {{score_others/500*100}}%;'></view>
+      </view>
+    </view>
+  </view>
+</view>
+
+<view class='body_summarize animated flipInX' wx:if="{{game_over ? true : false}}">
+  <view class='exp'>
+    <image src='../../imgs/exp.svg'></image>
+    <view style='margin-left:20rpx;'>{{win===2 ? '+0' : win===1 ?'+10':'-10'}}</view>
+  </view>
+  <view>
+    <button bindtap='continue_fighting'>继续挑战</button>
+    <button open-type='share'>炫耀成绩</button>
+  </view>
+</view>

+ 193 - 0
client/pages/fighting_room/fighting_room.wxss

@@ -0,0 +1,193 @@
+@import "../../utils/animate.wxss";
+@import "../../utils/base_bg.wxss";
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 60rpx;
+  font-size: 32rpx;
+}
+.header view{
+  text-align: center;
+}
+
+.header image {
+  width: 120rpx;
+  height: 120rpx;
+  border-radius: 50%;
+  border: 6rpx solid #fff;
+}
+
+.header .countdown {
+  font-size: 70rpx;
+  color: #fff;
+  font-weight: bold;
+}
+
+.animated {
+  animation-duration: 2s;
+  animation-fill-mode: forwards;
+}
+
+.lightSpeedIn-left {
+  animation-name: lightSpeedIn-left;
+  animation-timing-function: ease-out;
+}
+
+@keyframes lightSpeedIn-left {
+  from {
+    transform: translate3d(-600%, 0, 0) skewX(-15deg);
+    opacity: 0;
+  }
+
+  60% {
+    transform: skewX(15deg);
+    opacity: 1;
+  }
+
+  80% {
+    transform: skewX(-5deg);
+    opacity: 1;
+  }
+
+  to {
+    transform: none;
+    opacity: 1;
+  }
+}
+
+.lightSpeedIn-right {
+  animation-name: lightSpeedIn-right;
+  animation-timing-function: ease-out;
+}
+
+@keyframes lightSpeedIn-right {
+  from {
+    transform: translate3d(600%, 0, 0) skewX(15deg);
+    opacity: 0;
+  }
+
+  60% {
+    transform: skewX(-15deg);
+    opacity: 1;
+  }
+
+  80% {
+    transform: skewX(5deg);
+    opacity: 1;
+  }
+
+  to {
+    transform: none;
+    opacity: 1;
+  }
+}
+
+.body {
+  text-align: center;
+}
+
+.body .title {
+  width: 80%;
+  margin: 80rpx auto;
+}
+
+.body .content {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  overflow: hidden;
+}
+
+.answer {
+  display: flex;
+  justify-content: space-between;
+  color: #3883fa;
+  background: #fff;
+  border-radius: 20rpx;
+  height: 100rpx;
+  line-height: 100rpx;
+  margin: 40rpx 0;
+  position: relative;
+}
+
+.answer view {
+  text-align: center;
+}
+
+.right {
+  /*选择正确的答案颜色*/
+  background: #3ede58;
+  color: #fff;
+}
+
+.error {
+  /*选择错误的答案颜色*/
+  background: #e53117;
+  color: #fff;
+}
+
+.invisible {
+  display: none;
+}
+
+.sign {
+  display: inline-block;
+  font-weight: bold;
+  font-size: 50rpx;
+  position: absolute;
+  height: 100rpx;
+  line-height: 100rpx;
+  left: 5%;
+}
+
+.process {
+  width: 28rpx;
+  height: 560rpx;
+  border-radius: 17rpx;
+  border: 6rpx solid #89d693;
+  background: #15120b;
+  transform: rotate(-180deg);
+  margin: 0 30rpx;
+}
+
+.process_son {
+  width: 28rpx;
+  border-radius: 14rpx;
+  background: #3ede58;
+}
+
+.summarize {
+  position: absolute;
+  top: 0rpx;
+  z-index: 1000;
+  width: 100%;
+  text-align: center;
+}
+
+.body_summarize {
+  margin-top: 200rpx;
+}
+
+.exp {
+  display: flex;
+  justify-content: center;
+  font-size: 40rpx;
+  margin: 100rpx 0;
+}
+
+.body_summarize button {
+  font-weight: bold;
+  width: 60%;
+  margin-top: 40rpx;
+  color: #fff;
+  height: 92rpx;
+  line-height: 80rpx;
+  background: transparent;
+  border: 6rpx solid #fff;
+}
+
+.animated_zoomIn {
+  animation-duration: 1.5s;
+}

+ 46 - 0
client/pages/fighting_sort/fighting_sort.js

@@ -0,0 +1,46 @@
+let qcloud = require('../../vendor/wafer2-client-sdk/index')
+let config = require('../../config')
+let util = require('../../utils/util.js')
+let app = getApp()
+
+Page({
+  data: {
+    question_sort: [],
+  },
+  onLoad (options) {
+    this.getFightingSort()
+  },
+  onShow(){
+    this.closeTunnel()//当信道连接或者重连了时,关闭已连接的信道
+  },
+  getFightingSort() {
+    //util.showBusy('正在请求');
+    qcloud.request({
+      login: false,
+      url: `${app.appData.baseUrl}question_sort`,
+      success: (res) => {
+        // util.showSuccess('请求成功完成');
+        let data0 = res.data.data;
+        this.setData({
+          question_sort: data0
+        })
+      },
+      fail(error) {
+        util.showModel('请求失败', error);
+        console.log('request fail', error);
+      },
+    });
+  },
+  gotoFightingMatch(e) {
+    console.log(e);
+    wx.redirectTo({
+			url: `../fighting_match/fighting_match?sortId=${e.target.dataset.sortid} `
+    })
+  },
+  closeTunnel(){
+    //当信道连接或者重连了时,关闭已连接的信道
+    if (app.appData.tunnelStatus === 'connect' || app.appData.tunnelStatus === 'reconnect' ){
+      app.tunnel.close();
+    }
+  }
+})

+ 1 - 0
client/pages/fighting_sort/fighting_sort.json

@@ -0,0 +1 @@
+{}

+ 6 - 0
client/pages/fighting_sort/fighting_sort.wxml

@@ -0,0 +1,6 @@
+<view class='title'>选择您擅长的领域开始答题吧</view>
+<view>
+  <button wx:for="{{question_sort}}" class='sort' bindtap='gotoFightingMatch' data-sortid='{{item.id}}'>
+    {{item.sort}}
+  </button>
+</view>

+ 16 - 0
client/pages/fighting_sort/fighting_sort.wxss

@@ -0,0 +1,16 @@
+@import "../../utils/base_bg.wxss";
+.title{
+  text-align: center;
+}
+.sort{
+  width: 80%;
+  height: 80rpx;
+  margin: 30rpx auto;
+  line-height: 80rpx;
+  text-align: center;
+  font-size: 38rpx;
+  border: 4rpx solid white;
+  border-radius: 10rpx;
+  background: transparent;
+  color: #fff;
+}

+ 85 - 0
client/pages/friends_match/friends_match.js

@@ -0,0 +1,85 @@
+
+var qcloud = require('../../vendor/wafer2-client-sdk/index')
+var config = require('../../config')
+var util = require('../../utils/util.js')
+const match = require('../../utils/tunnelMacth.js').match//引入匹配函数
+const app = getApp();
+
+Page({
+  data: {
+    status: '初始化...',
+  },
+  onLoad (opt) {
+    app.appData.fromClickId = opt.currentClickId
+    app.upDateUser_networkFromClickId = require('../../utils/upDateUser_networkFromClickId.js').upDateUser_networkFromClickId
+    wx.showShareMenu({
+      withShareTicket: true
+    })
+    this.setData({ opt })
+    if (opt.scene == 1044) {  //打开页面,若含opt.scene == 1044,则表示改页面来自转发
+      app.pageGetUserInfo(this, this.storeFriensNetwork)
+    } else {
+      app.pageGetUserInfo(this)
+    }
+    app.pageGetUserInfo(this, match(this, app, opt))//开始匹配
+  },
+  onShareAppMessage (res) {
+    const that = this
+    console.log('this.data.opt', this.data.opt)
+    return {
+      title: '我才是' + this.data.opt.sortName + '领域的王者,敢来挑战吗?',
+      path: '/pages/friends_match/friends_match?scene=1044&fromOpenId=' + this.data.openId + '&sortId=' + this.data.opt.sortId + '&sortName=' + this.data.opt.sortName + '&currentClickId=' + app.appData.currentClickId + '&friendsFightingRoom=' + this.data.opt.friendsFightingRoom,
+      success: (res) => {
+        //转发时向用户关系表中更新一条转发记录(个人为person,群为GId)。
+        require('../../utils/upDateShareInfoToUser_network.js').upDateShareInfoToUser_network(app, that, res)
+      }
+    }
+  },
+  storeFriensNetwork () {
+    const that = this;
+    let [page, app] = [this, getApp()];
+    let baseData = {
+      openId: this.data.openId,
+      appId: app.appData.appId,
+      fromOpenId: this.data.opt.fromOpenId,
+      fromGId: ''
+    }
+    wx.getShareInfo({
+      shareTicket: app.appData.opt.shareTicket,  //当是从后台打开转发小程序,这时无法获取群信息
+      success: (res) => {
+        if (app.appData.gId) {
+          baseData.fromGId = app.appData.gId
+          storeFriendsNetwork(baseData)
+        } else {
+          app.gIdReadyCallback = (gId) => {
+            baseData.fromGId = gId
+            storeFriendsNetwork(baseData)
+          }
+        }
+      },
+      fail (res) {
+        storeFriendsNetwork(baseData)
+      }
+    })
+    function storeFriendsNetwork(data) {
+      const that = this;
+      qcloud.request({
+        login: false,
+        url: `${app.appData.baseUrl }storeFriendsNetwork`,
+        data,
+        success(res) {
+          console.info('【storeFriensNetwork】:存储finalData和clickId成功')
+        },
+        fail(error) {
+          util.showModel('请求失败', error);
+          console.log('request fail', error);
+        },
+      });
+    }
+  },
+  goback() {
+    wx.reLaunch({
+      url: '../entry/entry',
+    })
+  },
+})

+ 1 - 0
client/pages/friends_match/friends_match.json

@@ -0,0 +1 @@
+{}

+ 23 - 0
client/pages/friends_match/friends_match.wxml

@@ -0,0 +1,23 @@
+<view class='header'>
+  <view class='animated lightSpeedIn-left '>
+    <image src='{{userInfo.avatarUrl}}'></image>
+    <view style='text-align:center'>{{userInfo.nickName}}</view>
+  </view>
+
+  <view class='animated rubberBand'>{{status}}</view>
+  <view class='animated lightSpeedIn-right'>
+    <image src='{{user_others.avatarUrl}}'></image>
+    <view style='text-align:center'>{{user_others.nickName}}</view>
+  </view>
+
+</view>
+<view class='body'>
+  <button class='containCircle animated-slow fadeInUp' bindtap='goback'>
+    <image src='../../imgs/quit.svg'></image>
+    <view>首页</view>
+  </button>
+  <button open-type='share' class='containCircle animated-slow fadeInUp'>
+    <image src='../../imgs/share.svg'></image>
+    <view>邀请</view>
+  </button>
+</view>

+ 99 - 0
client/pages/friends_match/friends_match.wxss

@@ -0,0 +1,99 @@
+@import "../../utils/animate.wxss";
+@import "../../utils/base_bg.wxss";
+.header{
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 60rpx;
+  font-size: 32rpx;
+}
+.header image{
+  width: 120rpx;
+  height: 120rpx;
+  border-radius: 50%;
+  border: 6rpx solid #fff;
+}
+.animated {
+  animation-duration: 2s;
+  animation-fill-mode: forwards;
+}
+.lightSpeedIn-left {
+  animation-name: lightSpeedIn-left;
+  animation-timing-function: ease-out;
+}
+@keyframes lightSpeedIn-left {
+  from {
+    transform: translate3d(-600%, 0, 0) skewX(-15deg);
+    opacity: 0;
+  }
+  60% {
+    transform: skewX(15deg);
+    opacity: 1;
+  }
+  80% {
+    transform: skewX(-5deg);
+    opacity: 1;
+  }
+  to {
+    transform: none;
+    opacity: 1;
+  }
+}
+.lightSpeedIn-right {
+  animation-name: lightSpeedIn-right;
+  animation-timing-function: ease-out;
+}
+@keyframes lightSpeedIn-right {
+  from {
+    transform: translate3d(600%, 0, 0) skewX(15deg);
+    opacity: 0;
+  }
+  60% {
+    transform: skewX(-15deg);
+    opacity: 1;
+  }
+  80% {
+    transform: skewX(5deg);
+    opacity: 1;
+  }
+  to {
+    transform: none;
+    opacity: 1;
+  }
+}
+
+.body{
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 28rpx;
+  color: #1396DB;
+  margin-top: 300rpx;
+}
+
+.body .containCircle{
+  width: 160rpx;
+  height: 160rpx;
+  border-radius: 50%;
+  border: 4rpx solid #fff;
+  background: #fff;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-content:center;
+  align-items: center;
+  margin: 30rpx;
+  font-size: 28rpx;
+  color: #1295DA;
+  line-height: 30rpx;
+  opacity: 0;
+}
+.body image{
+  width: 80rpx;
+  height: 80rpx;
+}
+.animated-slow {
+  animation-duration: 1.5s;
+  animation-fill-mode: forwards;
+  animation-delay: 1.2s;
+}

+ 77 - 0
client/pages/friends_sort/friends_sort.js

@@ -0,0 +1,77 @@
+let qcloud = require('../../vendor/wafer2-client-sdk/index')
+let config = require('../../config')
+let util = require('../../utils/util.js')
+
+const app = getApp();
+Page({
+  data: {
+    question_sort: [],
+    sortId: '',
+    sortName: '',
+    showShareButton:false
+  },
+  onLoad (opt) {
+    app.appData.fromClickId = opt.currentClickId
+    app.upDateUser_networkFromClickId = require('../../utils/upDateUser_networkFromClickId.js').upDateUser_networkFromClickId
+    wx.showShareMenu({
+      withShareTicket: true
+    })
+    app.pageGetUserInfo(this)
+    this.getFriends_sort()
+  },
+  onShow () {
+    this.closeTunnel()//当信道连接或者重连了时,关闭已连接的信道
+  },
+  onShareAppMessage (res) {
+    const that = this;
+    app.appData.friendsFightingRoom = new Date().getTime().toString() + parseInt(Math.random() * 10000000)//创建:时间+随机数
+    return {
+      title: '谁才是' + this.data.sortName + '领域的王者?比比看吧!',
+      path: '/pages/friends_match/friends_match?scene=1044&fromOpenId=' + that.data.openId + '&sortId=' + that.data.sortId + '&sortName=' + that.data.sortName + '&currentClickId=' + app.appData.currentClickId + '&friendsFightingRoom=' + app.appData.friendsFightingRoom,
+      success: function (res) {
+        that.setData({ showShareButton:false})
+        //转发时向用户关系表中更新一条转发记录(个人为person,群为GId)。
+        require('../../utils/upDateShareInfoToUser_network.js').upDateShareInfoToUser_network(app, that, res)
+        wx.navigateTo({
+             url: '../friends_match/friends_match?scene=1044&fromOpenId=' + that.data.openId + '&sortId=' + that.data.sortId + '&sortName=' + that.data.sortName + '&currentClickId=' + app.appData.currentClickId + '&friendsFightingRoom=' + app.appData.friendsFightingRoom,
+        })
+      }
+    }
+  },
+  closeTunnel() {
+    //当信道连接或者重连了时,关闭已连接的信道
+    if (app.appData.tunnelStatus == 'connect' || app.appData.tunnelStatus == 'reconnect') {
+      app.tunnel.close()
+    }
+  },
+  getFriends_sort() {
+    //util.showBusy('正在请求');
+    qcloud.request({
+      login: false,
+      url: `${app.appData.baseUrl}question_sort`,
+      success: (res) => {
+        // util.showSuccess('请求成功完成');
+        let data0 = res.data.data;
+        this.setData({
+          question_sort: data0
+        })
+      },
+      fail(error) {
+        util.showModel('请求失败', error);
+        console.log('request fail', error);
+      },
+    });
+  },
+  getShareInfo(e) {
+    this.setData({
+      sortId: e.target.dataset.sortid,
+      sortName: e.target.dataset.sortname,
+      showShareButton:true
+    })
+  },
+  closeShareButton(){
+    this.setData({
+      showShareButton: false
+    })
+  }
+})

+ 1 - 0
client/pages/friends_sort/friends_sort.json

@@ -0,0 +1 @@
+{}

+ 15 - 0
client/pages/friends_sort/friends_sort.wxml

@@ -0,0 +1,15 @@
+<view class='title'>
+  <text> 选择好友之间较量的领域</text>
+</view>
+<view>
+  <button  wx:for="{{question_sort}}" class="{{index+1==sortId&&sortId!=='' ? 'animated fadeIn' : ''}} sort" bindtap='getShareInfo' data-sortid='{{item.id}}' data-sortname='{{item.sort}}'>{{item.sort}}</button>
+</view>
+
+
+<view class='enrollToast' wx:if='{{showShareButton}}'>
+  <view class='toast'></view>
+  <view class='content'>
+    <button type='primary' open-type='share'>转发给好友</button>
+    <button  style='margin-top:50rpx;' bindtap='closeShareButton'>取消</button>
+  </view>
+</view>

+ 55 - 0
client/pages/friends_sort/friends_sort.wxss

@@ -0,0 +1,55 @@
+@import "../../utils/animate.wxss";
+@import "../../utils/base_bg.wxss";
+
+.title {
+  text-align: center;
+}
+
+.sort {
+  width: 80%;
+  height: 80rpx;
+  margin: 30rpx auto;
+  line-height: 80rpx;
+  text-align: center;
+  font-size: 38rpx;
+  border: 4rpx solid white;
+  border-radius: 10rpx;
+  background: transparent;
+  color: #fff;
+}
+
+.fadeIn {
+  background: #e03e39;
+}
+
+.enrollToast {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 110%;
+  font-size: 36rpx;
+}
+
+.enrollToast .toast {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  opacity: 0.8;
+  background: #666;
+  z-index: 100;
+}
+
+.enrollToast .content {
+  position: relative;
+  top: 50%;
+  z-index: 200;
+  width: 70%;
+  height: 20%;
+  background: #fff;
+  left: 5%;
+  border-radius: 10rpx;
+  padding:100rpx 10%;
+  opacity: 0.9;
+}
+

+ 73 - 0
client/pages/rank/rank.js

@@ -0,0 +1,73 @@
+var qcloud = require('../../vendor/wafer2-client-sdk/index')
+var config = require('../../config')
+var util = require('../../utils/util.js')
+const app = getApp();
+Page({
+  data: {
+    currentTab: 0,
+    friendsData: [],
+    globalData: [],
+    loadNumber: 0//全球排名数据加载次数
+  },
+  onLoad: function (opt) {
+    wx.showShareMenu({
+      withShareTicket: true
+    })
+    app.pageGetUserInfo(this)
+    this.getRankGlobalData();
+  },
+  onShow() {
+    this.getRankFriendsData();
+  },
+  onReachBottom: function () {//下拉加载
+    const that = this
+    if (that.data.currentTab) {
+      that.getRankGlobalData()
+    }
+  },
+  getRankGlobalData() {//加载全球排名的数据
+    const that = this
+    qcloud.request({
+      login: false,
+      url: app.appData.baseUrl + 'getRankGlobalData',
+      data: {
+        loadNumber: that.data.loadNumber
+      },
+      success: (res) => {
+        that.setData({
+          globalData: that.data.globalData.concat(res.data.data),//数据叠加
+          loadNumber: that.data.loadNumber+1
+        })
+      },
+      fail(error) {
+        util.showModel('请求失败', error);
+        console.log('request fail', error);
+      },
+    })
+  },
+  getRankFriendsData: function () {
+    const that = this
+    qcloud.request({
+      login: false,
+      url: app.appData.baseUrl + 'getRankFriendsData',
+      data: {
+        openId: this.data.openId
+      },
+      success: (res) => {
+        this.setData({
+          friendsData: res.data.data
+        })
+      },
+      fail(error) {
+        util.showModel('请求失败', error);
+        console.log('request fail', error);
+      },
+    });
+  },
+  swichNav(e) {
+    var that = this;
+    that.setData({
+      currentTab: e.target.dataset.current,
+    })
+  },
+})

+ 1 - 0
client/pages/rank/rank.json

@@ -0,0 +1 @@
+{}

+ 47 - 0
client/pages/rank/rank.wxml

@@ -0,0 +1,47 @@
+<view >
+
+  <view class='tab'>
+    <view class="{{currentTab==0 ? 'on' : ''}}" data-current="0" bindtap="swichNav">好友排名</view>
+    <view class="{{currentTab==1 ? 'on' : ''}}" data-current="1" bindtap="swichNav">全球排名</view>
+  </view>
+  <view >
+
+    <view wx:if='{{currentTab==0}}' >
+      <view wx:for="{{friendsData}}" class='item'>
+        <view style='width:10%;font-size:38rpx;'>{{index+1}}</view>
+        <view style='width:30%;'>
+          <image src='{{item.avatarUrl}}'></image>
+        </view>
+        <view style='width:70%;'>
+          <view class='itemDetail'>
+            <text>{{item.nickName}}</text>
+            <text style='font-size:28rpx;'>来自:{{item.city}}</text>
+          </view>
+          <view class='itemDetail'>
+            <text>最强王者</text>
+            <text style='font-size:28rpx;'>得分:{{item.score}}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+    <view wx:if='{{currentTab==1}}'>
+      <view wx:for="{{globalData}}" class='item'>
+        <view style='width:10%;font-size:38rpx;'>{{index+1}}</view>
+        <view style='width:30%;'>
+          <image src='{{item.avatarUrl}}'></image>
+        </view>
+        <view style='width:70%;'>
+          <view class='itemDetail'>
+            <text>{{item.nickName}}</text>
+            <text style='font-size:28rpx;'>来自:{{item.city?item.city:'德玛西亚'}}</text>
+          </view>
+          <view class='itemDetail'>
+            <text>最强王者</text>
+            <text style='font-size:28rpx;'>得分:{{item.score}}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+
+</view>

File diff suppressed because it is too large
+ 56 - 0
client/pages/rank/rank.wxss


+ 22 - 0
client/pages/userInfos/userInfo.js

@@ -0,0 +1,22 @@
+var qcloud = require('../../vendor/wafer2-client-sdk/index')
+const app = getApp();
+Page({
+  data: {
+
+  },
+  onLoad: function (options) {
+
+  },
+  onGotUserInfo: function (event) {
+    if (event.detail.errMsg == 'getUserInfo:ok') {
+      wx.navigateBack({
+        url: '../entry/entry',
+        success: function (e) {
+          var pages = getCurrentPages();
+          var prevPage = pages[0];
+          prevPage.fnInit(event);
+        }
+      });
+    }
+  }
+})

+ 3 - 0
client/pages/userInfos/userInfo.json

@@ -0,0 +1,3 @@
+{
+  "usingComponents": {}
+}

+ 10 - 0
client/pages/userInfos/userInfo.wxml

@@ -0,0 +1,10 @@
+<view >
+  <view class="titles">
+    请点击确认按钮,开启知识比拼。
+  </view>
+  <view class="conditions">
+    <button open-type="getUserInfo" lang="zh_CN" bindgetuserinfo="onGotUserInfo">
+      确认
+    </button>
+  </view>
+</view>

File diff suppressed because it is too large
+ 23 - 0
client/pages/userInfos/userInfo.wxss


+ 40 - 0
client/project.config.json

@@ -0,0 +1,40 @@
+{
+	"description": "项目配置文件",
+	"packOptions": {
+		"ignore": []
+	},
+	"setting": {
+		"urlCheck": false,
+		"es6": true,
+		"postcss": true,
+		"minified": true,
+		"newFeature": true,
+		"autoAudits": false
+	},
+	"compileType": "miniprogram",
+	"libVersion": "2.6.2",
+	"appid": "wxc65eea6a56ef0fd5",
+	"projectname": "GITHUB",
+	"debugOptions": {
+		"hidedInDevtools": []
+	},
+	"isGameTourist": false,
+	"condition": {
+		"search": {
+			"current": -1,
+			"list": []
+		},
+		"conversation": {
+			"current": -1,
+			"list": []
+		},
+		"game": {
+			"currentL": -1,
+			"list": []
+		},
+		"miniprogram": {
+			"current": -1,
+			"list": []
+		}
+	}
+}

File diff suppressed because it is too large
+ 1579 - 0
client/utils/animate.wxss


File diff suppressed because it is too large
+ 6 - 0
client/utils/base_bg.wxss


+ 83 - 0
client/utils/tunnelMacth.js

@@ -0,0 +1,83 @@
+const util = require('./util.js')
+
+function match(page, app, opt) {
+  const that = page
+  if (app.appData.tunnelStatus !== 'close') {
+    app.tunnel.close()
+  }
+  app.tunnelCreate()//新建信道,并监听相关变化
+  const tunnel = app.tunnel
+  function getCurrentTunnelId() {
+    return app.tunnel.socketUrl.slice(app.tunnel.socketUrl.indexOf('tunnelId=') + 9, app.tunnel.socketUrl.indexOf('&'))
+  }
+  app.tunnelConnectCallback = () => {
+    let userInfo = that.data.userInfo
+    userInfo.tunnelId = getCurrentTunnelId()
+    that.setData({
+      status: '已连接,对手匹配中...',
+      userInfo,//用户信息存储当前的信道ID
+    })
+    tunnel.emit('updateMatchInfo', {//发起匹配
+      openId: that.data.openId,
+      sortId: opt.sortId,
+      friendsFightingRoom: opt.friendsFightingRoom//匹配者含friendsFightingRoom则说明是好友之间的匹配
+    })
+  }
+
+  app.tunnelCloseCallback = () => {
+    that.setData({ status: '连接已关闭' })
+    //util.showSuccess('连接已断开')
+  }
+
+  app.tunnelReconnectCallback = () => {
+    util.showSuccess('已重新连接')
+    let userInfo = that.data.userInfo
+    userInfo.tunnelId = getCurrentTunnelId()
+    that.setData({
+      status: '网络已重连,匹配中...',
+      userInfo,
+    })
+    tunnel.emit('updateMatchInfo', {//发起匹配
+      openId: that.data.openId,
+      sortId: opt.sortId,
+      friendsFightingRoom: opt.friendsFightingRoom//匹配者含friendsFightingRoom则说明是好友之间的匹配
+    })
+  }
+
+  app.tunnelReconnectCallback = () => {
+    util.showSuccess('已重新连接')
+    let userInfo = that.data.userInfo
+    userInfo.tunnelId =  getCurrentTunnelId()
+    that.setData({
+      status: '网络重连成功,对手匹配中...',
+      userInfo,
+    })
+  }
+
+  app.tunnelErrorCallback = (error) => {
+    that.setData({ status: '信道发生错误:' + error })
+    util.showSuccess('连接错误')
+  }
+
+  tunnel.on('matchNotice', (res) => {//监听匹配成功
+    console.log('res', res)
+    let user_me, user_others
+    if (res.player1.openId === that.data.openId){
+      user_me = res.player1
+      user_others=res.player2
+    }else{
+      user_me = res.player2
+      user_others = res.player1
+    }
+    wx.setStorageSync('user_me', user_me)
+    wx.setStorageSync('user_others', user_others)
+    that.setData({ status: user_me.nickName + ' VS ' + user_others.nickName })
+    setTimeout(goto_fighting_room, 2000)//延迟1s跳转到战队页面
+    function goto_fighting_room() {
+      wx.redirectTo({ //navigateTo不会会卸载该页面,只是将当前页面隐藏了,redirectTo会销毁当前页面
+				url: `../fighting_room/fighting_room?roomName=${res.player1.roomName}`
+      })
+    }
+  })
+}
+module.exports = { match }

+ 46 - 0
client/utils/upDateShareInfoToUser_network.js

@@ -0,0 +1,46 @@
+//用于转发时向用户关系表中更新一条转发记录(个人为person,群为GId)
+const qcloud = require('../vendor/wafer2-client-sdk/index')
+function upDateShareInfoToUser_network(app, page, share_res) {
+  let upDate = (content, clickId) => {
+    qcloud.request({
+      login: false,
+      data: {
+        clickId,
+        content,
+      },
+      url: app.appData.baseUrl + 'upDateShareInfoToUser_network',
+      success: (res) => { }
+    })
+  }
+  const shareTickets = share_res.shareTickets
+  if (!shareTickets) {
+    //IOS转发给个人的时候shareTickets为null
+    upDate('person', app.appData.currentClickId)
+  } else {
+    wx.getShareInfo({
+      shareTicket: shareTickets[0],
+      success: (res) => {
+        qcloud.request({
+          login: false,
+          data: {
+            appId: app.appData.appId,
+            openId: page.data.openId,
+            encryptedData: res.encryptedData,
+            iv: res.iv
+          },
+          url: app.appData.baseUrl + 'getGId',
+          success: (res) => {
+            let GId = res.data.data
+            upDate(GId, app.appData.currentClickId)
+          }
+        })
+      },
+      fail: (res) => {
+        //Android转发给个人的时候shareTickets不为null,而是判断为fail
+        upDate('person', app.appData.currentClickId)
+      }
+    })
+  }
+}
+
+module.exports = { upDateShareInfoToUser_network }

+ 15 - 0
client/utils/upDateUser_networkFromClickId.js

@@ -0,0 +1,15 @@
+//用于转发时向用户关系表中更新一条从哪个clickId打开的记录
+const qcloud = require('../vendor/wafer2-client-sdk/index')
+let upDateUser_networkFromClickId = (app,currentClickId, fromClickId) => {
+  qcloud.request({
+    login: false,
+    data: {
+      currentClickId,
+      fromClickId
+    },
+    url: app.appData.baseUrl + 'upDateUser_networkFromClickId',
+    success: (res) => { }
+  })
+}
+
+module.exports = {upDateUser_networkFromClickId}

+ 42 - 0
client/utils/util.js

@@ -0,0 +1,42 @@
+const formatTime = date => {
+  const year = date.getFullYear()
+  const month = date.getMonth() + 1
+  const day = date.getDate()
+  const hour = date.getHours()
+  const minute = date.getMinutes()
+  const second = date.getSeconds()
+
+  return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':')
+}
+
+const formatNumber = n => {
+  n = n.toString()
+  return n[1] ? n : '0' + n
+}
+
+
+// 显示繁忙提示
+var showBusy = text => wx.showToast({
+    title: text,
+    icon: 'loading',
+    duration: 5000
+})
+
+// 显示成功提示
+var showSuccess = text => wx.showToast({
+    title: text,
+    icon: 'success'
+})
+
+// 显示失败提示
+var showModel = (title, content) => {
+    wx.hideToast();
+
+    wx.showModal({
+        title,
+        content: JSON.stringify(content),
+        showCancel: false
+    })
+}
+
+module.exports = { formatTime, showBusy, showSuccess, showModel }

+ 24 - 0
client/vendor/wafer2-client-sdk/LICENSE

@@ -0,0 +1,24 @@
+LICENSE - "MIT License"
+
+Copyright (c) 2016 by Tencent Cloud
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.

+ 247 - 0
client/vendor/wafer2-client-sdk/README.md

@@ -0,0 +1,247 @@
+# 微信小程序客户端腾讯云增强 SDK
+
+[![Build Status](https://travis-ci.org/tencentyun/wafer-client-sdk.svg?branch=master)](https://travis-ci.org/tencentyun/wafer-client-sdk)
+[![Coverage Status](https://coveralls.io/repos/github/tencentyun/wafer-client-sdk/badge.svg?branch=master)](https://coveralls.io/github/tencentyun/wafer-client-sdk?branch=master)
+[![License](https://img.shields.io/github/license/tencentyun/wafer-client-sdk.svg)](LICENSE)
+
+本 项目是 [Wafer](https://github.com/tencentyun/wafer-solution) 的组成部分,为小程序客户端开发提供 SDK 支持会话服务和信道服务。
+
+## SDK 获取与安装
+
+解决方案[客户端 Demo](https://github.com/tencentyun/wafer-client-demo) 已经集成并使用最新版的 SDK,需要快速了解的可以从 Demo 开始。
+
+如果需要单独开始,本 SDK 已经发布为 bower 模块,可以直接安装到小程序目录中。
+
+```sh
+npm install -g bower
+bower install qcloud-weapp-client-sdk
+```
+
+安装之后,就可以使用 `require` 引用 SDK 模块:
+
+```js
+var qcloud = require('./bower_components/qcloud-weapp-client-sdk/index.js');
+```
+
+## 会话服务
+
+[会话服务](https://github.com/tencentyun/wafer-solution/wiki/%E4%BC%9A%E8%AF%9D%E6%9C%8D%E5%8A%A1)让小程序拥有会话管理能力。
+
+### 登录
+
+登录可以在小程序和服务器之间建立会话,服务器由此可以获取到用户的标识和信息。
+
+```js
+var qcloud = require('./bower_components/qcloud-weapp-client-sdk/index.js');
+
+// 设置登录地址
+qcloud.setLoginUrl('https://199447.qcloud.la/login');
+qcloud.login({
+    success: function (userInfo) {
+        console.log('登录成功', userInfo);
+    },
+    fail: function (err) {
+        console.log('登录失败', err);
+    }
+});
+```
+本 SDK 需要配合云端 SDK 才能提供完整会话服务。通过 [setLoginUrl](#setLoginUrl) 设置登录地址,云服务器在该地址上使用云端 SDK 处理登录请求。
+
+> `setLoginUrl` 方法设置登录地址之后会一直有效,因此你可以在微信小程序启动时设置。
+
+登录成功后,可以获取到当前微信用户的基本信息。
+
+### 请求
+
+如果希望小程序的网络请求包含会话,登录之后使用 [request](#request) 方法进行网络请求即可。
+
+```js
+qcloud.request({
+    url: 'http://199447.qcloud.la/user',
+    success: function (response) {
+        console.log(response);
+    },
+    fail: function (err) {
+        console.log(err);
+    }
+});
+```
+
+如果调用 `request` 之前还没有登录,则请求不会带有会话。`request` 方法也支持 `login` 参数支持在请求之前自动登录。
+
+```js
+// 使用 login 参数之前,需要设置登录地址
+qcloud.setLoginUrl('https://199447.qcloud.la/login');
+qcloud.request({
+    login: true,
+    url: 'http://199447.qcloud.la/user',
+    success: function (response) {
+        console.log(response);
+    },
+    fail: function (err) {
+        console.log(err);
+    }
+});
+```
+
+关于会话服务详细技术说明,请参考 [Wiki](https://github.com/tencentyun/wafer-solution/wiki/%E4%BC%9A%E8%AF%9D%E6%9C%8D%E5%8A%A1)。
+
+## 信道服务
+
+[信道服务](https://github.com/tencentyun/wafer-solution/wiki/%E4%BF%A1%E9%81%93%E6%9C%8D%E5%8A%A1)小程序支持利用腾讯云的信道资源使用 WebSocket 服务。
+
+```js
+// 创建信道,需要给定后台服务地址
+var tunnel = this.tunnel = new qcloud.Tunnel('https://199447.qcloud.la/tunnel');
+
+// 监听信道内置消息,包括 connect/close/reconnecting/reconnect/error
+tunnel.on('connect', () => console.log('WebSocket 信道已连接'));
+tunnel.on('close', () => console.log('WebSocket 信道已断开'));
+tunnel.on('reconnecting', () => console.log('WebSocket 信道正在重连...'));
+tunnel.on('reconnect', () => console.log('WebSocket 信道重连成功'));
+tunnel.on('error', error => console.error('信道发生错误:', error));
+
+// 监听自定义消息(服务器进行推送)
+tunnel.on('speak', speak => console.log('收到 speak 消息:', speak));
+
+// 打开信道
+tunnel.open();
+// 发送消息
+tunnel.emit('speak', { word: "hello", who: { nickName: "techird" }});
+// 关闭信道
+tunnel.close();
+```
+
+信道服务同样需要业务服务器配合云端 SDK 支持,构造信道实例的时候需要提供业务服务器提供的信道服务地址。通过监听信道消息以及自定义消息来通过信道实现业务。
+
+关于信道使用的更完整实例,建议参考客户端 Demo 中的[三木聊天室应用源码](https://github.com/tencentyun/wafer-client-demo/blob/master/pages/chat/chat.js)。
+
+关于信道服务详细技术说明,请参考 [Wiki](https://github.com/tencentyun/wafer-solution/wiki/%E4%BF%A1%E9%81%93%E6%9C%8D%E5%8A%A1)。
+
+## API
+
+
+### setLoginUrl
+设置会话服务登录地址。
+
+#### 语法
+```js
+qcloud.setLoginUrl(loginUrl);
+```
+
+#### 参数
+|参数         |类型           |说明
+|-------------|---------------|--------------
+|loginUrl     |string         |会话服务登录地址
+
+### login
+登录,建立微信小程序会话。
+
+#### 语法
+```js
+qcloud.login(options);
+```
+
+#### 参数
+|参数         |类型           |说明
+|-------------|---------------|--------------
+|options      |PlainObject    |会话服务登录地址
+|options.success | () => void | 登录成功的回调
+|options.error | (error) => void | 登录失败的回调
+
+
+### request
+进行带会话的请求。
+
+#### 语法
+```js
+qcloud.request(options);
+```
+
+#### 参数
+|参数         |类型           |说明
+|-------------|---------------|--------------
+|options      |PlainObject    | 会话服务登录地址
+|options.login | bool         | 是否自动登录以获取会话,默认为 false
+|options.url   | string       | 必填,要请求的地址
+|options.header | PlainObject | 请求头设置,不允许设置 Referer
+|options.method | string      | 请求的方法,默认为 GET
+|options.success | (response) => void | 登录成功的回调。<ul><li>`response.statusCode`:请求返回的状态码</li><li>`response.data`:请求返回的数据</li></ul>
+|options.error | (error) => void | 登录失败的回调
+|options.complete | () => void | 登录完成后回调,无论成功还是失败
+
+### Tunnel
+
+表示一个信道。由于小程序的限制,同一时间只能有一个打开的信道。
+
+#### constructor
+
+##### 语法
+```js
+var tunnel = new Tunnel(tunnelUrl);
+```
+
+#### 参数
+|参数         |类型           |说明
+|-------------|---------------|--------------
+|tunnelUrl    |String         | 会话服务登录地址
+
+
+#### on
+监听信道上的事件。信道上事件包括系统事件和服务器推送消息。
+
+##### 语法
+```js
+tunnel.on(type, listener);
+```
+
+##### 参数
+|参数         |类型           |说明
+|-------------|---------------|--------------
+|type         |string         | 监听的事件类型
+|listener     |(message?: any) => void | 监听器,具体类型的事件发生时调用监听器。如果是消息,则会有消息内容。
+
+##### 事件
+|事件         |说明
+|-------------|-------------------------------
+|connect      |信道连接成功后回调
+|close        |信道关闭后回调
+|reconnecting |信道发生重连时回调
+|reconnected  |信道重连成功后回调
+|error        |信道发生错误后回调
+|[message]    |信道服务器推送过来的消息类型,如果消息类型和上面内置的时间类型冲突,需要在监听的时候在消息类型前加 `@`
+|\*           |监听所有事件和消息,监听器第一个参数接收到时间或消息类型 
+
+#### open
+打开信道,建立连接。由于小程序的限制,同一时间只能有一个打开的信道。
+
+##### 语法
+```js
+tunnel.open();
+```
+
+#### emit
+向信道推送消息。
+
+##### 语法
+```js
+tunnel.emit(type, content);
+```
+
+##### 参数
+|参数         |类型           |说明
+|-------------|---------------|--------------
+|type         |string         | 要推送的消息的类型
+|content      |any            | 要推送的消息的内容
+
+#### close
+关闭信道
+
+##### 语法
+```js
+tunnel.close();
+```
+
+## LICENSE
+
+[MIT](LICENSE)

+ 25 - 0
client/vendor/wafer2-client-sdk/index.js

@@ -0,0 +1,25 @@
+var constants = require('./lib/constants');
+var login = require('./lib/login');
+var Session = require('./lib/session');
+var request = require('./lib/request');
+var Tunnel = require('./lib/tunnel');
+
+var exports = module.exports = {
+    login: login.login,
+    setLoginUrl: login.setLoginUrl,
+    LoginError: login.LoginError,
+
+    clearSession: Session.clear,
+
+    request: request.request,
+    RequestError: request.RequestError,
+
+    Tunnel: Tunnel,
+};
+
+// 导出错误类型码
+Object.keys(constants).forEach(function (key) {
+    if (key.indexOf('ERR_') === 0) {
+        exports[key] = constants[key];
+    }
+});

+ 20 - 0
client/vendor/wafer2-client-sdk/lib/constants.js

@@ -0,0 +1,20 @@
+module.exports = {
+    WX_HEADER_CODE: 'X-WX-Code',
+    WX_HEADER_ENCRYPTED_DATA: 'X-WX-Encrypted-Data',
+    WX_HEADER_IV: 'X-WX-IV',
+    WX_HEADER_ID: 'X-WX-Id',
+    WX_HEADER_SKEY: 'X-WX-Skey',
+
+    WX_SESSION_MAGIC_ID: 'F2C224D4-2BCE-4C64-AF9F-A6D872000D1A',
+
+    ERR_INVALID_PARAMS: 'ERR_INVALID_PARAMS',
+
+    ERR_WX_LOGIN_FAILED: 'ERR_WX_LOGIN_FAILED',
+    ERR_WX_GET_USER_INFO: 'ERR_WX_GET_USER_INFO',
+    ERR_LOGIN_TIMEOUT: 'ERR_LOGIN_TIMEOUT',
+    ERR_LOGIN_FAILED: 'ERR_LOGIN_FAILED',
+    ERR_LOGIN_SESSION_NOT_RECEIVED: 'ERR_LOGIN_MISSING_SESSION',
+
+    ERR_SESSION_INVALID: 'ERR_SESSION_INVALID',
+    ERR_CHECK_LOGIN_FAILED: 'ERR_CHECK_LOGIN_FAILED',
+};

+ 182 - 0
client/vendor/wafer2-client-sdk/lib/login.js

@@ -0,0 +1,182 @@
+var utils = require('./utils');
+var constants = require('./constants');
+var Session = require('./session');
+
+/***
+ * @class
+ * 表示登录过程中发生的异常
+ */
+var LoginError = (function () {
+    function LoginError(type, message) {
+        Error.call(this, message);
+        this.type = type;
+        this.message = message;
+    }
+
+    LoginError.prototype = new Error();
+    LoginError.prototype.constructor = LoginError;
+
+    return LoginError;
+})();
+
+/**
+ * 微信登录,获取 code 和 encryptData
+ */
+var getWxLoginResult = function getLoginCode(callback) {
+    wx.login({
+        success: function (loginResult) {
+            wx.getUserInfo({
+                success: function (userResult) {
+                    // console.log('成功了')
+                    callback(null, {
+                        code: loginResult.code,
+                        encryptedData: userResult.encryptedData,
+                        iv: userResult.iv,
+                        userInfo: userResult.userInfo,
+                    });
+                },
+                fail: function (userError) {
+                    // console.log('失败了')
+                    //jacksplwxy:用户拒绝授权后,打开设置,让用户进行授权
+
+                    wx.navigateTo({
+                        url: '../userInfos/userInfo'
+                    })
+
+                    // 暂时隐藏弹出设置框
+
+                //     wx.showModal({
+                //         title: '登录失败!',
+                //         content: '请选择允许获取您的公开信息',
+                //         success: (res) => {
+                //             wx.openSetting({
+                //                 success: (res) => {
+                //                     console.log(res);
+                //                     if (res.authSetting['scope.userInfo']) {
+                //                         wx.getUserInfo({
+                //                             success: function (userResult) {
+                //                                 callback(null, {
+                //                                     code: loginResult.code,
+                //                                     encryptedData: userResult.encryptedData,
+                //                                     iv: userResult.iv,
+                //                                     userInfo: userResult.userInfo,
+                //                                 });
+                //                             },
+                //                         })
+                //                     }
+                //                 }
+                //             })
+                //         }
+                //     })
+                },
+                //源码:
+                /*fail: function (userError) {
+                    var error = new LoginError(constants.ERR_WX_GET_USER_INFO, '获取微信用户信息失败,请检查网络状态');
+                    error.detail = userError;
+                    callback(error, null);
+                },*/
+            });
+        },
+
+        fail: function (loginError) {
+            var error = new LoginError(constants.ERR_WX_LOGIN_FAILED, '微信登录失败,请检查网络状态');
+            error.detail = loginError;
+            callback(error, null);
+        },
+    });
+};
+
+var noop = function noop() {};
+var defaultOptions = {
+    method: 'GET',
+    success: noop,
+    fail: noop,
+    loginUrl: null,
+};
+
+/**
+ * @method
+ * 进行服务器登录,以获得登录会话
+ *
+ * @param {Object} options 登录配置
+ * @param {string} options.loginUrl 登录使用的 URL,服务器应该在这个 URL 上处理登录请求
+ * @param {string} [options.method] 请求使用的 HTTP 方法,默认为 "GET"
+ * @param {Function} options.success(userInfo) 登录成功后的回调函数,参数 userInfo 微信用户信息
+ * @param {Function} options.fail(error) 登录失败后的回调函数,参数 error 错误信息
+ */
+var login = function login(options) {
+    console.log('又走着了吗')
+    options = utils.extend({}, defaultOptions, options);
+
+    if (!defaultOptions.loginUrl) {
+        options.fail(new LoginError(constants.ERR_INVALID_PARAMS, '登录错误:缺少登录地址,请通过 setLoginUrl() 方法设置登录地址'));
+        return;
+    }
+
+    var doLogin = () => getWxLoginResult(function (wxLoginError, wxLoginResult) {
+        if (wxLoginError) {
+            options.fail(wxLoginError);
+            return;
+        }
+
+        var userInfo = wxLoginResult.userInfo;
+
+        // 构造请求头,包含 code、encryptedData 和 iv
+        var code = wxLoginResult.code;
+        var encryptedData = wxLoginResult.encryptedData;
+        var iv = wxLoginResult.iv;
+        var header = {};
+
+        header[constants.WX_HEADER_CODE] = code;
+        header[constants.WX_HEADER_ENCRYPTED_DATA] = encryptedData;
+        header[constants.WX_HEADER_IV] = iv;
+
+        // 请求服务器登录地址,获得会话信息
+        wx.request({
+            url: options.loginUrl,
+            header: header,
+            method: options.method,
+            data: options.data,
+            success: function (result) {
+                var data = result.data;
+
+                // 成功地响应会话信息
+                if (data && data.code === 0 && data.data.skey) {
+                    var res = data.data
+                    if (res.userinfo) {
+                        Session.set(res.skey); //jacksplwxy:将skey缓存起来
+                        wx.setStorageSync('user_info_' + constants.WX_SESSION_MAGIC_ID, res.userinfo); //jacksplwxy:将用户信息存储起来
+                        options.success(userInfo);
+                    } else {
+                        var errorMessage = '登录失败(' + data.error + '):' + (data.message || '未知错误');
+                        var noSessionError = new LoginError(constants.ERR_LOGIN_SESSION_NOT_RECEIVED, errorMessage);
+                        options.fail(noSessionError);
+                    }
+
+                    // 没有正确响应会话信息
+                } else {
+                    var noSessionError = new LoginError(constants.ERR_LOGIN_SESSION_NOT_RECEIVED, JSON.stringify(data));
+                    options.fail(noSessionError);
+                }
+            },
+
+            // 响应错误
+            fail: function (loginResponseError) {
+                var error = new LoginError(constants.ERR_LOGIN_FAILED, '登录失败,可能是网络错误或者服务器发生异常');
+                options.fail(error);
+            },
+        });
+    });
+
+    doLogin();
+};
+
+var setLoginUrl = function (loginUrl) {
+    defaultOptions.loginUrl = loginUrl;
+};
+
+module.exports = {
+    LoginError: LoginError,
+    login: login,
+    setLoginUrl: setLoginUrl,
+};

+ 112 - 0
client/vendor/wafer2-client-sdk/lib/request.js

@@ -0,0 +1,112 @@
+var constants = require('./constants');
+var utils = require('./utils');
+var Session = require('./session');
+var loginLib = require('./login');
+
+var noop = function noop() {};
+
+var buildAuthHeader = function buildAuthHeader(session) {
+    var header = {};
+
+    if (session) {
+        header[constants.WX_HEADER_SKEY] = session;
+    }
+
+    return header;
+};
+
+/***
+ * @class
+ * 表示请求过程中发生的异常
+ */
+var RequestError = (function () {
+    function RequestError(type, message) {
+        Error.call(this, message);
+        this.type = type;
+        this.message = message;
+    }
+
+    RequestError.prototype = new Error();
+    RequestError.prototype.constructor = RequestError;
+
+    return RequestError;
+})();
+
+function request(options) {
+    if (typeof options !== 'object') {
+        var message = '请求传参应为 object 类型,但实际传了 ' + (typeof options) + ' 类型';
+        throw new RequestError(constants.ERR_INVALID_PARAMS, message);
+    }
+
+    var requireLogin = options.login;
+    var success = options.success || noop;
+    var fail = options.fail || noop;
+    var complete = options.complete || noop;
+    var originHeader = options.header || {};
+
+    // 成功回调
+    var callSuccess = function () {
+        success.apply(null, arguments);
+        complete.apply(null, arguments);
+    };
+
+    // 失败回调
+    var callFail = function (error) {
+        fail.call(null, error);
+        complete.call(null, error);
+    };
+
+    // 是否已经进行过重试
+    var hasRetried = false;
+
+    if (requireLogin) {
+        doRequestWithLogin();
+    } else {
+        doRequest();
+    }
+
+    // 登录后再请求
+    function doRequestWithLogin() {
+        loginLib.login({ success: doRequest, fail: callFail });
+    }
+
+    // 实际进行请求的方法
+    function doRequest() {
+        var authHeader = buildAuthHeader(Session.get());
+
+        wx.request(utils.extend({}, options, {
+            header: utils.extend({}, originHeader, authHeader),
+
+            success: function (response) {
+                var data = response.data;
+                var error, message;
+                if (data && data.code === -1) {
+                    Session.clear();
+                    // 如果是登录态无效,并且还没重试过,会尝试登录后刷新凭据重新请求
+                    if (!hasRetried) {
+                        hasRetried = true;
+                        doRequestWithLogin();
+                        return;
+                    }
+
+                    message = '登录态已过期';
+                    error = new RequestError(data.error, message);
+
+                    callFail(error);
+                    return;
+                } else {
+                    callSuccess.apply(null, arguments);
+                }
+            },
+
+            fail: callFail,
+            complete: noop,
+        }));
+    };
+
+};
+
+module.exports = {
+    RequestError: RequestError,
+    request: request,
+};

+ 18 - 0
client/vendor/wafer2-client-sdk/lib/session.js

@@ -0,0 +1,18 @@
+var constants = require('./constants');
+var SESSION_KEY = 'weapp_session_' + constants.WX_SESSION_MAGIC_ID;
+
+var Session = {
+    get: function () {
+        return wx.getStorageSync(SESSION_KEY) || null;
+    },
+
+    set: function (session) {
+        wx.setStorageSync(SESSION_KEY, session);
+    },
+
+    clear: function () {
+        wx.removeStorageSync(SESSION_KEY);
+    },
+};
+
+module.exports = Session;

+ 536 - 0
client/vendor/wafer2-client-sdk/lib/tunnel.js

@@ -0,0 +1,536 @@
+var requestLib = require('./request');
+var wxTunnel = require('./wxTunnel');
+
+/**
+ * 当前打开的信道,同一时间只能有一个信道打开
+ */
+var currentTunnel = null;
+
+// 信道状态枚举
+var STATUS_CLOSED = Tunnel.STATUS_CLOSED = 'CLOSED';
+var STATUS_CONNECTING = Tunnel.STATUS_CONNECTING = 'CONNECTING';
+var STATUS_ACTIVE = Tunnel.STATUS_ACTIVE = 'ACTIVE';
+var STATUS_RECONNECTING = Tunnel.STATUS_RECONNECTING = 'RECONNECTING';
+
+// 错误类型枚举
+var ERR_CONNECT_SERVICE = Tunnel.ERR_CONNECT_SERVICE = 1001;
+var ERR_CONNECT_SOCKET = Tunnel.ERR_CONNECT_SOCKET = 1002;
+var ERR_RECONNECT = Tunnel.ERR_RECONNECT = 2001;
+var ERR_SOCKET_ERROR = Tunnel.ERR_SOCKET_ERROR = 3001;
+
+// 包类型枚举
+var PACKET_TYPE_MESSAGE = 'message';
+var PACKET_TYPE_PING = 'ping';
+var PACKET_TYPE_PONG = 'pong';
+var PACKET_TYPE_TIMEOUT = 'timeout';
+var PACKET_TYPE_CLOSE = 'close';
+
+// 断线重连最多尝试 5 次
+var DEFAULT_MAX_RECONNECT_TRY_TIMES = 5;
+
+// 每次重连前,等待时间的增量值
+var DEFAULT_RECONNECT_TIME_INCREASE = 1000;
+
+function Tunnel(serviceUrl) {
+  if (currentTunnel && currentTunnel.status !== STATUS_CLOSED) {
+    throw new Error('当前有未关闭的信道,请先关闭之前的信道,再打开新信道');
+  }
+
+  currentTunnel = this;
+
+  // 等确认微信小程序全面支持 ES6 就不用那么麻烦了
+  var me = this;
+
+  //=========================================================================
+  // 暴露实例状态以及方法
+  //=========================================================================
+  this.serviceUrl = serviceUrl;
+  this.socketUrl = null;
+  this.status = null;
+
+  this.open = openConnect;
+  this.on = registerEventHandler;
+  this.emit = emitMessagePacket;
+  this.close = close;
+
+  this.isClosed = isClosed;
+  this.isConnecting = isConnecting;
+  this.isActive = isActive;
+  this.isReconnecting = isReconnecting;
+
+
+  //=========================================================================
+  // 信道状态处理,状态说明:
+  //   closed       - 已关闭
+  //   connecting   - 首次连接
+  //   active       - 当前信道已经在工作
+  //   reconnecting - 断线重连中
+  //=========================================================================
+  function isClosed() { return me.status === STATUS_CLOSED; }
+  function isConnecting() { return me.status === STATUS_CONNECTING; }
+  function isActive() { return me.status === STATUS_ACTIVE; }
+  function isReconnecting() { return me.status === STATUS_RECONNECTING; }
+
+  function setStatus(status) {
+    var lastStatus = me.status;
+    if (lastStatus !== status) {
+      me.status = status;
+    }
+  }
+
+  // 初始为关闭状态
+  setStatus(STATUS_CLOSED);
+
+
+  //=========================================================================
+  // 信道事件处理机制
+  // 信道事件包括:
+  //   connect      - 连接已建立
+  //   close        - 连接被关闭(包括主动关闭和被动关闭)
+  //   reconnecting - 开始重连
+  //   reconnect    - 重连成功
+  //   error        - 发生错误,其中包括连接失败、重连失败、解包失败等等
+  //   [message]    - 信道服务器发送过来的其它事件类型,如果事件类型和上面内置的事件类型冲突,将在事件类型前面添加前缀 `@`
+  //=========================================================================
+  var preservedEventTypes = 'connect,close,reconnecting,reconnect,error'.split(',');
+  var eventHandlers = [];
+
+  /**
+   * 注册消息处理函数
+   * @param {string} messageType 支持内置消息类型("connect"|"close"|"reconnecting"|"reconnect"|"error")以及业务消息类型
+   */
+  function registerEventHandler(eventType, eventHandler) {
+    if (typeof eventHandler === 'function') {
+      eventHandlers.push([eventType, eventHandler]);
+    }
+  }
+
+  /**
+   * 派发事件,通知所有处理函数进行处理
+   */
+  function dispatchEvent(eventType, eventPayload) {
+    eventHandlers.forEach(function (handler) {
+      var handleType = handler[0];
+      var handleFn = handler[1];
+
+      if (handleType === '*') {
+        handleFn(eventType, eventPayload);
+      } else if (handleType === eventType) {
+        handleFn(eventPayload);
+      }
+    });
+  }
+
+  /**
+   * 派发事件,事件类型和系统保留冲突的,事件名会自动加上 '@' 前缀
+   */
+  function dispatchEscapedEvent(eventType, eventPayload) {
+    if (preservedEventTypes.indexOf(eventType) > -1) {
+      eventType = '@' + eventType;
+    }
+
+    dispatchEvent(eventType, eventPayload);
+  }
+
+
+  //=========================================================================
+  // 信道连接控制
+  //=========================================================================
+  var isFirstConnection = true;
+  var isOpening = false;
+
+  /**
+   * 连接信道服务器,获取 WebSocket 连接地址,获取地址成功后,开始进行 WebSocket 连接
+   */
+  function openConnect() {
+    if (isOpening) return;
+    isOpening = true;
+
+    // 只有关闭状态才会重新进入准备中
+    setStatus(isFirstConnection ? STATUS_CONNECTING : STATUS_RECONNECTING);
+
+    requestLib.request({
+      url: serviceUrl,
+      method: 'GET',
+      success: function (response) {
+        if (+response.statusCode === 200 && response.data && response.data.data.connectUrl) {
+          console.log('通知服务端获准备开始连接,并成功取信道通讯地址', response.data.data.connectUrl)
+          openSocket(me.socketUrl = response.data.data.connectUrl);
+        } else {
+          dispatchConnectServiceError(response);
+        }
+      },
+      fail: dispatchConnectServiceError,
+      complete: () => isOpening = false,
+    });
+
+    function dispatchConnectServiceError(detail) {
+      if (isFirstConnection) {
+        setStatus(STATUS_CLOSED);
+
+        dispatchEvent('error', {
+          code: ERR_CONNECT_SERVICE,
+          message: '连接信道服务失败,网络错误或者信道服务没有正确响应',
+          detail: detail || null,
+        });
+
+      } else {
+        startReconnect(detail);
+      }
+    }
+  }
+
+  /**
+   * 打开 WebSocket 连接,打开后,注册微信的 Socket 处理方法
+   */
+  function openSocket(url) {
+    wxTunnel.listen({
+      onOpen: handleSocketOpen,
+      onMessage: handleSocketMessage,
+      onClose: handleSocketClose,
+      onError: handleSocketError,
+    });
+    //jacksplwxy:
+    //wx.connectSocket({ url: url });
+    wx.connectSocket({
+      url: url,
+      success(){
+        console.log('开始尝试信道连接')
+      }
+    });
+
+    isFirstConnection = false;
+  }
+
+
+  //=========================================================================
+  // 处理消息通讯
+  //
+  // packet           - 数据包,序列化形式为 `${type}` 或者 `${type}:${content}`
+  // packet.type      - 包类型,包括 message, ping, pong, close
+  // packet.content?  - 当包类型为 message 的时候,会附带 message 数据
+  //
+  // message          - 消息体,会使用 JSON 序列化后作为 packet.content
+  // message.type     - 消息类型,表示业务消息类型
+  // message.content? - 消息实体,可以为任意类型,表示消息的附带数据,也可以为空
+  //
+  // 数据包示例:
+  //  - 'ping' 表示 Ping 数据包
+  //  - 'message:{"type":"speak","content":"hello"}' 表示一个打招呼的数据包
+  //=========================================================================
+
+  // 连接还没成功建立的时候,需要发送的包会先存放到队列里
+  var queuedPackets = [];
+
+  /**
+   * WebSocket 打开之后,更新状态,同时发送所有遗留的数据包
+   */
+  function handleSocketOpen() {
+    /* istanbul ignore else */
+    if (isConnecting()) {
+      dispatchEvent('connect');
+      console.log('监听到信道连接成功')
+    }
+    else if (isReconnecting()) {
+      dispatchEvent('reconnect');
+      resetReconnectionContext();
+    }
+
+    setStatus(STATUS_ACTIVE);
+    emitQueuedPackets();
+    nextPing();
+  }
+
+  /**
+   * 收到 WebSocket 数据包,交给处理函数
+   */
+  function handleSocketMessage(message) {
+    resolvePacket(message.data);
+  }
+
+  /**
+   * 发送数据包,如果信道没有激活,将先存放队列
+   */
+  function emitPacket(packet) {
+    if (isActive()) {
+      sendPacket(packet);
+    } else {
+      queuedPackets.push(packet);
+    }
+  }
+
+  /**
+   * 数据包推送到信道
+   */
+  function sendPacket(packet) {
+    var encodedPacket = [packet.type];
+
+    if (packet.content) {
+      encodedPacket.push(JSON.stringify(packet.content));
+    }
+
+    wx.sendSocketMessage({
+      data: encodedPacket.join(':'),
+      fail: handleSocketError,
+    });
+  }
+
+  function emitQueuedPackets() {
+    queuedPackets.forEach(emitPacket);
+
+    // empty queued packets
+    queuedPackets.length = 0;
+  }
+
+  /**
+   * 发送消息包
+   */
+  function emitMessagePacket(messageType, messageContent) {
+    var packet = {
+      type: PACKET_TYPE_MESSAGE,
+      content: {
+        type: messageType,
+        content: messageContent,
+      },
+    };
+
+    emitPacket(packet);
+  }
+
+  /**
+   * 发送 Ping 包
+   */
+  function emitPingPacket() {
+    emitPacket({ type: PACKET_TYPE_PING });
+  }
+
+  /**
+   * 发送关闭包
+   */
+  function emitClosePacket() {
+    emitPacket({ type: PACKET_TYPE_CLOSE });
+  }
+
+  /**
+   * 解析并处理从信道接收到的包
+   */
+  function resolvePacket(raw) {
+    var packetParts = raw.split(':');
+    var packetType = packetParts.shift();
+    var packetContent = packetParts.join(':') || null;
+    var packet = { type: packetType };
+
+    if (packetContent) {
+      try {
+        packet.content = JSON.parse(packetContent);
+      } catch (e) { }
+    }
+
+    switch (packet.type) {
+      case PACKET_TYPE_MESSAGE:
+        handleMessagePacket(packet);
+        break;
+      case PACKET_TYPE_PONG:
+        handlePongPacket(packet);
+        break;
+      case PACKET_TYPE_TIMEOUT:
+        handleTimeoutPacket(packet);
+        break;
+      case PACKET_TYPE_CLOSE:
+        handleClosePacket(packet);
+        break;
+      default:
+        handleUnknownPacket(packet);
+        break;
+    }
+  }
+
+  /**
+   * 收到消息包,直接 dispatch 给处理函数
+   */
+  function handleMessagePacket(packet) {
+    var message = packet.content;
+    dispatchEscapedEvent(message.type, message.content);
+  }
+
+
+  //=========================================================================
+  // 心跳、断开与重连处理
+  //=========================================================================
+
+  /**
+   * Ping-Pong 心跳检测超时控制,这个值有两个作用:
+   *   1. 表示收到服务器的 Pong 相应之后,过多久再发下一次 Ping
+   *   2. 如果 Ping 发送之后,超过这个时间还没收到 Pong,断开与服务器的连接
+   * 该值将在与信道服务器建立连接后被更新
+   */
+  let pingPongTimeout = 15000;
+  let pingTimer = 0;
+  let pongTimer = 0;
+
+  /**
+   * 信道服务器返回 Ping-Pong 控制超时时间
+   */
+  function handleTimeoutPacket(packet) {
+    var timeout = packet.content * 1000;
+    /* istanbul ignore else */
+    if (!isNaN(timeout)) {
+      pingPongTimeout = timeout;
+      ping();
+    }
+  }
+
+  /**
+   * 收到服务器 Pong 响应,定时发送下一个 Ping
+   */
+  function handlePongPacket(packet) {
+    nextPing();
+  }
+
+  /**
+   * 发送下一个 Ping 包
+   */
+  function nextPing() {
+    clearTimeout(pingTimer);
+    clearTimeout(pongTimer);
+    pingTimer = setTimeout(ping, pingPongTimeout);
+  }
+
+  /**
+   * 发送 Ping,等待 Pong
+   */
+  function ping() {
+    /* istanbul ignore else */
+    if (isActive()) {
+      emitPingPacket();
+
+      // 超时没有响应,关闭信道
+      pongTimer = setTimeout(handlePongTimeout, pingPongTimeout);
+    }
+  }
+
+  /**
+   * Pong 超时没有响应,信道可能已经不可用,需要断开重连
+   */
+  function handlePongTimeout() {
+    startReconnect('服务器已失去响应');
+  }
+
+  // 已经重连失败的次数
+  var reconnectTryTimes = 0;
+
+  // 最多允许失败次数
+  var maxReconnectTryTimes = Tunnel.MAX_RECONNECT_TRY_TIMES || DEFAULT_MAX_RECONNECT_TRY_TIMES;
+
+  // 重连前等待的时间
+  var waitBeforeReconnect = 0;
+
+  // 重连前等待时间增量
+  var reconnectTimeIncrease = Tunnel.RECONNECT_TIME_INCREASE || DEFAULT_RECONNECT_TIME_INCREASE;
+
+  var reconnectTimer = 0;
+
+  function startReconnect(lastError) {
+    if (reconnectTryTimes >= maxReconnectTryTimes) {
+      close();
+
+      dispatchEvent('error', {
+        code: ERR_RECONNECT,
+        message: '重连失败',
+        detail: lastError,
+      });
+    }
+    else {
+      wx.closeSocket();
+      waitBeforeReconnect += reconnectTimeIncrease;
+      setStatus(STATUS_RECONNECTING);
+      reconnectTimer = setTimeout(doReconnect, waitBeforeReconnect);
+    }
+
+    if (reconnectTryTimes === 0) {
+      dispatchEvent('reconnecting');
+    }
+
+    reconnectTryTimes += 1;
+  }
+
+  function doReconnect() {
+    openConnect();
+  }
+
+  function resetReconnectionContext() {
+    reconnectTryTimes = 0;
+    waitBeforeReconnect = 0;
+  }
+
+  /**
+   * 收到服务器的关闭请求
+   */
+  function handleClosePacket(packet) {
+    close();
+  }
+
+  function handleUnknownPacket(packet) {
+    // throw away
+  }
+
+  var isClosing = false;
+
+  /**
+   * 收到 WebSocket 断开的消息,处理断开逻辑
+   */
+  function handleSocketClose() {
+    /* istanbul ignore if */
+    if (isClosing) return;
+
+    /* istanbul ignore else */
+    if (isActive()) {
+      // 意外断开的情况,进行重连
+      startReconnect('链接已断开');
+    }
+  }
+
+  function close() {
+    isClosing = true;
+    closeSocket();
+    setStatus(STATUS_CLOSED);
+    resetReconnectionContext();
+    isFirstConnection = false;
+    clearTimeout(pingTimer);
+    clearTimeout(pongTimer);
+    clearTimeout(reconnectTimer);
+    dispatchEvent('close');
+    isClosing = false;
+  }
+
+  function closeSocket(emitClose) {
+    if (isActive() && emitClose !== false) {
+      emitClosePacket();
+    }
+
+    wx.closeSocket();
+  }
+
+
+  //=========================================================================
+  // 错误处理
+  //=========================================================================
+
+  /**
+   * 错误处理
+   */
+  function handleSocketError(detail) {
+    switch (me.status) {
+      case Tunnel.STATUS_CONNECTING:
+        dispatchEvent('error', {
+          code: ERR_SOCKET_ERROR,
+          message: '连接信道失败,网络错误或者信道服务不可用',
+          detail: detail,
+        });
+        break;
+    }
+  }
+
+}
+
+module.exports = Tunnel;

+ 18 - 0
client/vendor/wafer2-client-sdk/lib/utils.js

@@ -0,0 +1,18 @@
+
+/**
+ * 拓展对象
+ */
+exports.extend = function extend(target) {
+    var sources = Array.prototype.slice.call(arguments, 1);
+
+    for (var i = 0; i < sources.length; i += 1) {
+        var source = sources[i];
+        for (var key in source) {
+            if (source.hasOwnProperty(key)) {
+                target[key] = source[key];
+            }
+        }
+    }
+
+    return target;
+};

+ 32 - 0
client/vendor/wafer2-client-sdk/lib/wxTunnel.js

@@ -0,0 +1,32 @@
+/* istanbul ignore next */
+const noop = () => void(0);
+
+let onOpen, onClose, onMessage, onError;
+
+/* istanbul ignore next */
+function listen(listener) {
+    if (listener) {
+        onOpen = listener.onOpen;
+        onClose = listener.onClose;
+        onMessage = listener.onMessage;
+        onError = listener.onError;
+    } else {
+        onOpen = noop;
+        onClose = noop;
+        onMessage = noop;
+        onError = noop;
+    }
+}
+
+/* istanbul ignore next */
+function bind() {
+    wx.onSocketOpen(result => onOpen(result));
+    wx.onSocketClose(result => onClose(result));
+    wx.onSocketMessage(result => onMessage(result));
+    wx.onSocketError(error => onError(error));
+}
+
+listen(null);
+bind();
+
+module.exports = { listen };

+ 47 - 0
client/vendor/wafer2-client-sdk/package.json

@@ -0,0 +1,47 @@
+{
+  "_from": "wafer2-client-sdk",
+  "_id": "wafer2-client-sdk@1.0.0",
+  "_inBundle": false,
+  "_integrity": "sha1-4hExQwJ+2YIN3LOn0EtbBd8uTYg=",
+  "_location": "/wafer2-client-sdk",
+  "_phantomChildren": {},
+  "_requested": {
+    "type": "tag",
+    "registry": true,
+    "raw": "wafer2-client-sdk",
+    "name": "wafer2-client-sdk",
+    "escapedName": "wafer2-client-sdk",
+    "rawSpec": "",
+    "saveSpec": null,
+    "fetchSpec": "latest"
+  },
+  "_requiredBy": [
+    "#USER",
+    "/"
+  ],
+  "_resolved": "http://r.tnpm.oa.com/wafer2-client-sdk/download/wafer2-client-sdk-1.0.0.tgz",
+  "_shasum": "e2113143027ed9820ddcb3a7d04b5b05df2e4d88",
+  "_spec": "wafer2-client-sdk",
+  "_where": "/Users/Jason/Tencent/ide-test/wafer-client-demo",
+  "author": {
+    "name": "CFETeam"
+  },
+  "bugs": {
+    "url": "https://github.com/tencentyun/wafer2-client-sdk/issues"
+  },
+  "bundleDependencies": false,
+  "deprecated": false,
+  "description": "Wafer client SDK",
+  "directories": {
+    "lib": "lib"
+  },
+  "homepage": "https://github.com/tencentyun/wafer2-client-sdk#readme",
+  "license": "MIT",
+  "main": "index.js",
+  "name": "wafer2-client-sdk",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/tencentyun/wafer2-client-sdk.git"
+  },
+  "version": "1.0.0"
+}

BIN
imgs_md/1.gif


BIN
imgs_md/2.gif


BIN
imgs_md/3.gif


BIN
imgs_md/4.gif


+ 55 - 0
project.config.json

@@ -0,0 +1,55 @@
+{
+	"client": "client/",
+	"svr": "server/",
+	"miniprogramRoot": "client/",
+	"qcloudRoot": "server/",
+	"setting": {
+		"urlCheck": true,
+		"es6": true,
+		"postcss": true,
+		"minified": true,
+		"newFeature": true
+	},
+	"appid": "wxc65eea6a56ef0fd5",
+	"projectname": "GITHUB",
+	"libVersion": "2.6.2",
+	"condition": {
+		"search": {
+			"current": -1,
+			"list": []
+		},
+		"conversation": {
+			"current": -1,
+			"list": []
+		},
+		"miniprogram": {
+			"current": -1,
+			"list": [
+				{
+					"id": 0,
+					"name": "123",
+					"pathName": "pages/entry/entry",
+					"query": "",
+					"scene": "1044",
+					"shareInfo": {
+						"groupName": "测试模拟群2",
+						"shareName": "lFiISuN5RhgJVXnMSre_p1SpGjfbnDwpeO0Pu-T-8fw@cr4dev",
+						"shareKey": "Ea0iaYi9oGsiIDhIfPLdnaZGqJC9BbQREU4EjevV-PQCOl237si0nM1Al55O_Tu6coYxGojsaAwnzAGz4gSPGw~~"
+					}
+				},
+				{
+					"id": 1,
+					"name": "custom",
+					"pathName": "pages/custom_room/custom_room",
+					"query": "",
+					"scene": "1044",
+					"shareInfo": {
+						"groupName": "测试模拟群2",
+						"shareName": "lFiISuN5RhgJVXnMSre_p1SpGjfbnDwpeO0Pu-T-8fw@cr4dev",
+						"shareKey": "Ea0iaYi9oGsiIDhIfPLdnaZGqJC9BbQREU4EjevV-PQCOl237si0nM1Al55O_Tu6coYxGojsaAwnzAGz4gSPGw~~"
+					}
+				}
+			]
+		}
+	}
+}

+ 16 - 0
server/.eslintrc.json

@@ -0,0 +1,16 @@
+{
+  "root": true,
+  "parser": "babel-eslint",
+  "parserOptions": {
+    "sourceType": "module"
+  },
+  "extends": "standard",
+  "rules": {
+    "indent": [2, 4, { "SwitchCase": 1 }],
+    "arrow-parens": 0,
+    "generator-star-spacing": 0
+  },
+  "env": {
+    "mocha": true
+  }
+}

+ 126 - 0
server/README.md

@@ -0,0 +1,126 @@
+# 腾讯云小程序解决方案 Demo - Node.js
+
+Node.js 版本 Wafer SDK 的服务端 Demo
+
+## 下载源码
+
+你可以直接通过 git 将代码 clone 到本地,也可以点击[这里](https://github.com/tencentyun/wafer-node-server-demo/releases)下载。
+
+```bash
+git clone https://github.com/tencentyun/wafer-node-server-demo.git
+```
+
+## 开始使用
+
+#### 安装依赖
+
+```bash
+# 安装全局依赖
+npm i pm2 nodemon -g
+
+# 安装项目依赖
+npm i
+```
+
+#### 启动项目
+
+```bash
+# 开发环境,监听文件变化自动重启,并会输出 debug 信息
+tnpm run dev
+
+# 线上部署环境
+tnpm start
+```
+
+按照[小程序创建资源配置指引](https://github.com/tencentyun/weapp-doc)进行操作,可以得到运行本示例所需的资源和服务,其中包括已部署好的示例代码及自动下发的 SDK 配置文件 `/etc/qcloud/sdk.config`。
+
+- 示例代码部署目录:`/data/release/node-weapp-demo`
+- 运行示例的 Node 版本:`v8.1.0`
+- Node 进程管理工具:`pm2`
+
+## 项目结构
+
+```
+koa-weapp-demo
+├── README.md
+├── app.js
+├── controllers
+│   ├── index.js
+│   ├── login.js
+│   ├── message.js
+│   ├── tunnel.js
+│   ├── upload.js
+│   └── user.js
+├── middlewares
+│   └── response.js
+├── config.js
+├── package.json
+├── process.json
+├── nodemon.json
+├── qcloud.js
+└── routes
+    └── index.js
+```
+`app.js` 是 Demo 的主入口文件,Demo 使用 Koa 框架,在 `app.js` 创建一个 Koa 实例并响应请求。
+
+`routes/index.js` 是 Demo 的路由定义文件
+
+`controllers` 存放 Demo 所有业务逻辑的目录,`index.js` 不需要修改,他会动态的将 `controllers` 文件夹下的目录结构映射成 modules 的 Object,例如 Demo 中的目录将会被映射成如下的结构:
+
+```javascript
+// index.js 输出
+{
+  login: require('login'),
+  message: require('message'),
+  tunnel: require('tunnel'),
+  upload: require('upload'),
+  user: require('user')
+}
+```
+
+`qcloud.js` 导出了一个 SDK 的单例,包含了所有的 SDK 接口,之后使用的时候只需要 `require` 这个文件就行,无需重复初始化 SDK。
+
+`config.js` 主要的配置如下:
+
+```javascript
+{
+  port: '5757',                             // 项目启动的端口
+
+  appId: 'wx00dd00dd00dd00dd',              // 微信小程序 App ID
+  appSecret: 'abcdefg',                     // 微信小程序 App Secret
+  wxLoginExpires: 7200,                     // 微信登录态有效期
+  useQcloudLogin: false,                    // 是否使用腾讯云代理登录
+
+  /**
+   * MySQL 配置,用来存储用户登录态和用户信息
+   * 如果不提供 MySQL 配置,模式会使用自动配置好的本地镜像中的 MySQL 储存信息
+   * 具体查看文档-登录态储存和校验
+   **/
+  mysql: {
+    host: 'localhost',
+    port: 3306,
+    user: 'root',
+    db: 'cAuth',
+    pass: '',
+    char: 'utf8'
+  },
+  
+  // COS 配置,用于上传模块使用
+  cos: {
+    /**
+     * 区域
+     * 华北:cn-north
+     * 华东:cn-east
+     * 华南:cn-south
+     * 西南:cn-southwest
+     */
+    region: 'cn-south',
+    fileBucket: 'test',                    // Bucket 名称
+    uploadFolder: ''                       // 文件夹
+  }
+}
+```
+
+除了 `config.js` ,腾讯云还会在你初始化小程序解决方案的时候,向你的机器下发 `sdk.config`,里面包含了你的腾讯云 AppId、SecretId、SecretKey 和服务器等信息,无需修改,`qcloud.js` 会自动引入。如果你想要在自己的机器上部署 SDK 的 Demo,请查看[自行部署 Demo 说明]()。
+
+除此以外,关于 SDK 的详细配置信息,还可以查看 [SDK 的 API 文档]()。

+ 19 - 0
server/app.js

@@ -0,0 +1,19 @@
+const Koa = require('koa')
+const app = new Koa()
+const debug = require('debug')('koa-weapp-demo')
+const response = require('./middlewares/response')
+const bodyParser = require('./middlewares/bodyparser')
+const config = require('./config')
+
+// 使用响应处理中间件
+app.use(response)
+
+// 解析请求体
+app.use(bodyParser())
+
+// 引入路由分发
+const router = require('./routes')
+app.use(router.routes())
+
+// 启动程序,监听端口
+app.listen(config.port, () => debug(`listening on port ${config.port}`))

+ 43 - 0
server/config.js

@@ -0,0 +1,43 @@
+
+/*开发版*/
+
+const CONF = {
+  port: '5757',
+  rootPathname: '',
+  // 微信小程序 App ID
+  // appId: 'wx0eedaadeee1d7f8a',
+  appId: 'wxc65eea6a56ef0fd5',
+  // 微信小程序 App Secret
+  // appSecret: '778030e0a216a942080bb167b11f617c',
+  appSecret: '5a6b38ecca25e27030b69c9f3e7076e7',
+  // 是否使用腾讯云代理登录小程序
+  useQcloudLogin: false,
+  /**
+   * MySQL 配置,用来存储 session 和用户信息
+   * 若使用了腾讯云微信小程序解决方案
+   * 开发环境下,MySQL 的初始密码为您的微信小程序 appid
+   */
+  mysql: {
+    host: 'localhost',
+    port: 3306,
+    user: 'root',
+    db: 'cAuth',
+    // pass: 'wx0eedaadeee1d7f8a',
+    pass: 'wxc65eea6a56ef0fd5',
+    char: 'utf8mb4'
+  },
+  cos: {
+    /**
+     * 区域
+     * @查看 https://cloud.tencent.com/document/product/436/6224
+     */
+    region: 'ap-guangzhou',
+    // Bucket 名称
+    fileBucket: 'wximg',
+    // 文件夹
+    uploadFolder: ''
+  },
+  // 微信登录态有效期
+  wxLoginExpires: 7200
+}
+module.exports = process.env.NODE_ENV === 'local' ? Object.assign({}, CONF, require('./config.local')) : CONF;

+ 18 - 0
server/config.local.js.1.example

@@ -0,0 +1,18 @@
+module.exports = {
+    mysql: {
+        host: 'localhost',
+        port: 3306,
+        user: '',
+        pass: '',
+        db: '',
+        char: 'utf8mb4'
+    },
+    serverHost: 'localhost',
+    tunnelServerUrl: '',
+    tunnelSignatureKey: '',
+    // 腾讯云相关配置可以查看云 API 秘钥控制台:https://console.qcloud.com/capi
+    qcloudAppId: '',
+    qcloudSecretId: '',
+    qcloudSecretKey: '',
+    wxMessageToken: ''
+}

+ 17 - 0
server/controllers/getGId.js

@@ -0,0 +1,17 @@
+const WXBizDataCrypt = require('../tools/WXBizDataCrypt')
+module.exports = async (ctx, next) => {
+  if (ctx.state.$wxInfo.loginState === 1) {
+    const query = ctx.query
+    let appId = query.appId
+    const { mysql } = require('../qcloud')
+    let sessionKey = await mysql('cSessionInfo').where({ open_id: query.openId }).select('session_key')
+    sessionKey = sessionKey[0].session_key
+    let encryptedData = query.encryptedData
+    let iv = query.iv
+    let pc = new WXBizDataCrypt(appId, sessionKey)
+    let data = pc.decryptData(encryptedData, iv)
+    ctx.state.data = data.openGId
+  } else {
+    ctx.state.code = -1
+  }
+}

+ 35 - 0
server/controllers/getRankFriendsData.js

@@ -0,0 +1,35 @@
+module.exports = async (ctx, next) => {
+  if (ctx.state.$wxInfo.loginState === 1) {
+    const query = ctx.query
+    const { mysql } = require('../qcloud')
+    let res0 = await mysql('friendsnetwork').where({ fromOpenId: ctx.query.openId }).select('cSessionInfo.user_info', 'cSessionInfo.score').leftJoin('cSessionInfo', 'friendsnetwork.openId', 'cSessionInfo.open_id')
+    let res1 = await mysql('friendsnetwork').where({ openId: ctx.query.openId }).select('cSessionInfo.user_info', 'cSessionInfo.score').leftJoin('cSessionInfo', 'friendsnetwork.fromOpenId', 'cSessionInfo.open_id') //注意:friendsnetwork.fromOpenId为undefined的情况
+    let res_me = await mysql('cSessionInfo').where({ open_id: ctx.query.openId }).select('user_info','score')
+    let res2 = res0.concat(res1.concat(res_me))
+    let arr = []
+    res2.forEach(function (val, i) {
+      let value = JSON.parse(val.user_info)
+      value.score = val.score
+      arr[i] = value
+    })
+    function hashRemoveRepeat(arr) {  //去重函数
+      var hash = {},
+        len = arr.length,
+        result = [];
+      for (var i = 0; i < len; i++) {
+        if (!hash[arr[i].openId]) {
+          hash[arr[i].openId] = true;
+          result.push(arr[i]);
+        }
+      }
+      return result;
+    }
+    arr = hashRemoveRepeat(arr)
+    arr.sort(function (a, b) {
+      return b.score - a.score
+    })
+    ctx.state.data = arr;
+  } else {
+    ctx.state.code = -1
+  }
+}

+ 18 - 0
server/controllers/getRankGlobalData.js

@@ -0,0 +1,18 @@
+module.exports = async (ctx, next) => {
+  if (ctx.state.$wxInfo.loginState === 1) {
+    const query = ctx.query
+    const { mysql } = require('../qcloud')
+    await mysql('cSessionInfo').select('user_info', 'score').orderBy('score', 'desc').limit(10).offset(query.loadNumber * 10).then(res => {//获取原始得分
+      let arr = []
+      res.forEach(function (val, i) {
+        let value = JSON.parse(val.user_info)
+        value.score = val.score
+        arr[i] = value
+      })
+      ctx.state.data = arr
+    })
+    
+  } else {
+    ctx.state.code = -1
+  }
+}

+ 10 - 0
server/controllers/get_score.js

@@ -0,0 +1,10 @@
+module.exports = async (ctx, next) => {
+  if (ctx.state.$wxInfo.loginState === 1) {
+    const { mysql } = require('../qcloud')
+    await mysql('cSessionInfo').where({open_id: ctx.query.openId}).select('score').then(res => {//获取原始得分
+      ctx.state.data = res[0].score;
+    })
+  } else {
+    ctx.state.code = -1
+  }
+}

+ 30 - 0
server/controllers/index.js

@@ -0,0 +1,30 @@
+const _ = require('lodash')
+const fs = require('fs')
+const path = require('path')
+
+/**
+ * 映射 d 文件夹下的文件为模块
+ */
+const mapDir = d => {
+    const tree = {}
+
+    // 获得当前文件夹下的所有的文件夹和文件
+    const [dirs, files] = _(fs.readdirSync(d)).partition(p => fs.statSync(path.join(d, p)).isDirectory())
+
+    // 映射文件夹
+    dirs.forEach(dir => {
+        tree[dir] = mapDir(path.join(d, dir))
+    })
+
+    // 映射文件
+    files.forEach(file => {
+        if (path.extname(file) === '.js') {
+            tree[path.basename(file, '.js')] = require(path.join(d, file))
+        }
+    })
+
+    return tree
+}
+
+// 默认导出当前文件夹下的映射
+module.exports = mapDir(path.join(__dirname))

+ 9 - 0
server/controllers/login.js

@@ -0,0 +1,9 @@
+// 登录授权接口
+module.exports = async (ctx, next) => {
+    // 通过 Koa 中间件进行登录之后
+    // 登录信息会被存储到 ctx.state.$wxInfo
+    // 具体查看:
+    if (ctx.state.$wxInfo.loginState) {
+        ctx.state.data = ctx.state.$wxInfo.userinfo
+    }
+}

+ 29 - 0
server/controllers/message.js

@@ -0,0 +1,29 @@
+const { message: { checkSignature } } = require('../qcloud')
+
+/**
+ * 响应 GET 请求(响应微信配置时的签名检查请求)
+ */
+async function get (ctx, next) {
+    const { signature, timestamp, nonce, echostr } = ctx.query
+    if (checkSignature(signature, timestamp, nonce)) ctx.body = echostr
+    else ctx.body = 'ERR_WHEN_CHECK_SIGNATURE'
+}
+
+async function post (ctx, next) {
+    // 检查签名,确认是微信发出的请求
+    const { signature, timestamp, nonce } = ctx.query
+    if (!checkSignature(signature, timestamp, nonce)) ctx.body = 'ERR_WHEN_CHECK_SIGNATURE'
+
+    /**
+     * 解析微信发送过来的请求体
+     * 可查看微信文档:https://mp.weixin.qq.com/debug/wxadoc/dev/api/custommsg/receive.html#接收消息和事件
+     */
+    const body = ctx.request.body
+
+    ctx.body = 'success'
+}
+
+module.exports = {
+    post,
+    get
+}

+ 34 - 0
server/controllers/qr_address.js

@@ -0,0 +1,34 @@
+var axios = require('axios');
+var fs = require('fs');
+
+module.exports = async (ctx, next) => {
+  if (ctx.state.$wxInfo.loginState === 1) {
+    /* async await和koa的执行顺序问题?????*/
+    let secret = require('../config.js').appSecret,
+      appId = require('../config.js').appId
+    console.log('appId', appId)
+    // let url_access_token = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' + appId+'&secret=' + secret, access_token
+    // axios.get(url_access_token)
+    //   .then(function (res) {
+    //     access_token = res.data.access_token
+    //     console.log('access_token', access_token)
+    //     let url_qr = 'https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token=' + access_token
+    //     axios({
+    //       method: "POST",
+    //       url: url_qr,
+    //       data: { path: "pages/web_view/web_view" },
+    //       responseType: 'arraybuffer' //必须要写!
+    //     }).then((res) => {
+    //       console.log('res', res);
+    //       var originBuffer = res.data
+    //       console.log(Buffer.isBuffer(originBuffer));
+    //       fs.writeFile('./qr.jpg', originBuffer, function (err) { //此段代码写在本地吧,让图片在本地生成,在手工上传到服务器上
+    //         if (err) { console.log(err) }
+    //       });
+    //     });
+    //   })
+
+  } else {
+    ctx.state.code = -1
+  }
+}

+ 10 - 0
server/controllers/question_sort.js

@@ -0,0 +1,10 @@
+module.exports = async (ctx, next) => {
+  if (ctx.state.$wxInfo.loginState === 1) {
+    const { mysql } = require('../qcloud')
+    await mysql('question_sort').select('*').then(res => {
+      ctx.state.data = res;
+    })
+  } else {
+    ctx.state.code = -1
+  }
+}

+ 10 - 0
server/controllers/storeFriendsNetwork.js

@@ -0,0 +1,10 @@
+module.exports = async (ctx, next) => {
+  if (ctx.state.$wxInfo.loginState === 1) {
+    const query = ctx.query
+    const { mysql } = require('../qcloud')
+    await mysql('friendsnetwork').insert({ id: null, openId: query.openId, appId: query.appId, fromOpenId: query.fromOpenId, fromGId: query.fromGId, time: null}).returning('*').then(res => {
+    })
+  } else {
+    ctx.state.code = -1
+  }
+}

+ 11 - 0
server/controllers/storeUser_network.js

@@ -0,0 +1,11 @@
+module.exports = async (ctx, next) => {
+  if (ctx.state.$wxInfo.loginState === 1) {
+    const query = ctx.query
+    const { mysql } = require('../qcloud')
+    let res = await mysql('user_network').insert({ clickId: null, fromClickId: query.fromClickId, appid: query.appId, openid: query.openId, fromGId: query.fromGId, scene: query.scene, time: null, param_1: '' }).returning('*')
+    //let res = await mysql('user_network').select('clickId').orderBy('clickId', 'desc').limit(1)
+    ctx.state.data = res[0]
+  } else {
+    ctx.state.code = -1
+  }
+}

+ 459 - 0
server/controllers/tunnel.js

@@ -0,0 +1,459 @@
+const { tunnel } = require('../qcloud')
+const { mysql } = require('../qcloud')
+
+const option = {
+  MAX_SCORE_GAP: 10000,//匹配的玩家最大分差不能超过10000分
+  MATCH_SPEED: 3000,//匹配的频率:每3秒遍历匹配一次
+  QUESTION_NUMBER: 5,//答题数5个
+  SEND_QUESTIONS_DELAY: 3500,//匹配完成后,间隔3.5S后开始向前端发题
+  SEND_QUESTION_TIME: 16000,//发题的频率:每16秒发送一题
+  PING_PONG_TIME: 6000,//PING-PONG响应的PING发送频率
+  PING_PONG_OUT_TIME: 20000,//PING-PONG响应超时时间
+  MAX_NUMBER_TUNNEL_RESEND: 3,//每个信道允许出现无效信道时重新发送的次数
+}
+const players = {} //用户信息存储对象:openId为key
+const rooms = {}//房间存储对象:房间名为key
+const fightingRecord = {}//每局比赛战绩存储对象:房间名为key,包含:openId_winner,openId_loser,score_winner,score_loser
+const match = {//匹配对象:包含数据和函数
+  queueData: [],
+  init() {
+    let finished = true
+	function match_succ(player1,player2){
+		//创建房间
+        match.createRoom(player1.openId, player2.openId)
+        //队列中删除匹配好的2个玩家
+        tools.deleteQueueOpenId(player1.openId)
+        tools.deleteQueueOpenId(player2.openId)
+	}
+    const loopMatch = setInterval(() => {
+      if (finished) {
+        finished = false
+        for (let index1 = 0; index1 < this.queueData.length; index1++) {
+          let player1 = players[this.queueData[index1]]
+		  //排位赛匹配
+          if (player1.friendsFightingRoom === undefined) {
+            for (let index2 = index1; index2 < this.queueData.length; index2++) {
+              let player2 = players[this.queueData[index2]]
+              if (player2.friendsFightingRoom === undefined && player2.sortId === player1.sortId && Math.abs(player2.score - player1.score) < option.MAX_SCORE_GAP && player2.openId !== player1.openId) {
+                match_succ(player1,player2)
+				//结束该player1的匹配
+                break
+              }
+            }
+          }
+		  //好友匹配
+          if (player1.friendsFightingRoom !== undefined && player1.friendsFightingRoom !== null) {
+            for (let index2 = index1; index2 < this.queueData.length; index2++) {
+              let player2 = players[this.queueData[index2]]
+              if (player2.friendsFightingRoom !== undefined && player2.friendsFightingRoom !== null && player2.friendsFightingRoom === player1.friendsFightingRoom && player2.openId !== player1.openId) {
+                this.match_succ(player1,player2)
+                //结束该player1的匹配
+                break
+              }
+            }
+          }
+        }
+        finished = true
+      }
+    }, option.MATCH_SPEED)
+  },
+  createRoom(openId1, openId2) {
+    let roomName = new Date().getTime().toString() + parseInt(Math.random() * 10000000)//创建时间+随机数
+    rooms[roomName] = {
+      roomName,
+      player1: openId1,
+      player2: openId2,
+      library: null,
+      responseNumber: 0,//收到的响应次数
+      finished: false,//是否完成了答题
+    }
+    players[openId1].roomName = roomName
+    players[openId2].roomName = roomName
+    console.info('创建后的总房间和玩家为:', rooms, players)
+    //library,默认包含5道题目
+    mysql('question_detail').where((players[openId1].sortId == 1) ? {} : { sort_id: players[openId1].sortId }).select('*').orderByRaw('RAND()').limit(option.QUESTION_NUMBER).then(res => {
+      rooms[roomName].library = res  //将查询到的题目存到房间的题库library中
+      //房间创建完成后通知前端匹配完成
+      tools.broadcast([players[openId1].tunnelId, players[openId2].tunnelId], 'matchNotice', {
+        'player1': {
+          openId: openId1,
+          nickName: players[openId1].nickName,
+          avatarUrl: players[openId1].avatarUrl,
+          roomName,
+        },
+        'player2': {
+          openId: openId2,
+          nickName: players[openId2].nickName,
+          avatarUrl: players[openId2].avatarUrl,
+          roomName,
+        }
+      })
+      //向客户端发题
+      tools.sendQuestionMain(roomName)
+    },error=>{
+         console.log(error)
+    })
+  },
+}
+match.init()
+const tools = {//工具对象,包含常用数据和函数
+  data: {
+    timerSendQuestion: [],//每个房间的发题定时器
+    numberTunnelResend: [],//key为信道ID,记录每个信道重复发送的次数
+  },
+  //广播到指定信道
+  broadcast(tunnelIdsArray, type, content) {
+    tunnel.broadcast(tunnelIdsArray, type, content)
+      .then(result => {
+        const invalidTunnelIds = result.data && result.data.invalidTunnelIds || []
+        if (invalidTunnelIds.length) {
+          console.error('======检测到无效的信道IDs======', invalidTunnelIds)
+          invalidTunnelIds.forEach(tunnelId => {
+            let number = this.data.numberTunnelResend[tunnelId] ? this.data.numberTunnelResend[tunnelId] : 0
+            if (number < option.MAX_NUMBER_TUNNEL_RESEND) {//当发送次数不大于规定次数时,可以进行重发
+              let timer = setTimeout(() => {//1.5S后重发一次数据
+                this.data.numberTunnelResend[tunnelId] = ++number
+                tools.broadcast([tunnelId], type, content)
+                clearTimeout(timer)
+              }, 2000)
+            } else {
+                 console.log('当前的numberTunnelResend长度为:', Object.keys(this.data.numberTunnelResend).length)
+                 if (Object.keys(this.data.numberTunnelResend).length > 20) { //当累积未清理掉的无效信道达到20个时,清空一次数组
+                this.data.numberTunnelResend = []
+                console.info('清空后的numberTunnelResend为', this.data.numberTunnelResend)
+              } else {
+                this.clsTimeout(tunnelId, this.data.numberTunnelResend)
+                console.info('删除后的numberTunnelResend为', this.data.numberTunnelResend)
+              }
+            }
+          })
+        }
+         }, error => {
+              console.log(error)
+         })
+  },
+  //关闭指定信道
+  closeTunnel(tunnelId) {
+    console.info('开始关闭信道:' + tunnelId)
+    tunnel.closeTunnel(tunnelId)
+    let openId = this.getPlayersOpenId(tunnelId)
+    if (players[openId]) {
+      if (players[openId].roomName) {
+        if (rooms[players[openId].roomName]) {
+          if (!rooms[players[openId].roomName].finished) {//如果用户存在房号,则说明是战斗状态断线,视为逃跑
+            tools.runAway(openId)
+          }
+        }
+      }
+      if (players[openId]) {
+        if (players[openId].roomName) {
+          this.clsTimeout(players[openId].roomName, tools.data.timerSendQuestion)//清除发题定时器
+          delete rooms[players[openId].roomName]//删除房间
+        }
+        this.deleteQueueOpenId(openId)//删除匹配队列中的openId
+        delete players[openId]//删除玩家信息
+        console.info('信道关闭后的队列、玩家、房间和发题定时器为:', match.queueData, players, rooms, tools.data.timerSendQuestion)
+      }
+    }
+  },
+  //根据信道ID获取openId
+  getPlayersOpenId(tunnelId) {
+    for (let index in players) {
+      if (players[index].tunnelId === tunnelId) {
+        return index
+      }
+    }
+    return null
+  },
+  //删除匹配队列中的指定openId
+  deleteQueueOpenId(openId) {
+    let index = match.queueData.indexOf(openId)
+    if (~index) {
+      match.queueData.splice(index, 1)
+    }
+  },
+  //清除数组中延时定时器
+  clsTimeout(index, arr) {
+    clearTimeout(arr[index])
+    delete arr[index]
+  },
+  //主控发题函数
+  sendQuestionMain(roomName) {
+    let sendQuestionsDelay = setTimeout(() => {
+      this.sendQuestion(roomName)
+      clearTimeout(sendQuestionsDelay)
+    }, option.SEND_QUESTIONS_DELAY)
+  },
+  //发题函数
+  sendQuestion(roomName) {
+    try {
+      let openId1 = rooms[roomName].player1
+      let openId2 = rooms[roomName].player2
+      this.clsTimeout(roomName, this.data.timerSendQuestion)
+      tools.broadcast([players[openId1].tunnelId, players[openId2].tunnelId], 'sendQuestion', {
+        question: rooms[roomName].library[0] ? rooms[roomName].library[0] : {},
+        //choice = []//[openID:'',userChoose: '',//用户选择了第几个答案 answerColor: '',//用户是否答对 scoreMyself: 0,//用户总得分]
+        choicePlayer1: players[openId1].choice,
+        choicePlayer2: players[openId2].choice
+      })
+      console.info('已经向客户端发送一题')
+      this.data.timerSendQuestion[roomName] = setTimeout(() => {
+        this.sendQuestion(roomName)
+      }, option.SEND_QUESTION_TIME)
+      //当发送完{}时,清除掉定时器
+      if (rooms[roomName].library[0] ? false : true) {
+        this.clsTimeout(roomName, this.data.timerSendQuestion)
+      }
+      rooms[roomName].library.shift() //发送一个,原始题库就删除一个
+      rooms[roomName].responseNumber = 0  //初始化房间响应次数
+      players[openId1].choice[1] = ''//初始化用户选择状态:第几个答案
+      players[openId1].choice[2] = ''//初始化用户选择状态:是否答对
+      players[openId2].choice[1] = ''//初始化用户选择状态:第几个答案
+      players[openId2].choice[2] = ''//初始化用户选择状态:是否答对
+    } catch (error) {
+      console.error('错误:' + error)
+    }
+  },
+  //更新得分
+  updateScore(openId, fightingResult) {
+    mysql('cSessionInfo').where({ open_id: openId }).select('score').then(res => {//获取原始得分
+      let score = res[0].score
+      if (fightingResult === 1) {
+        score = score + 10
+      } else if (fightingResult === 0) {
+        score = score - 10
+        if (score < 0) {
+          score = 0
+        }
+      } else {
+        return
+      }
+      mysql('cSessionInfo').where({ open_id: openId }).update('score', score).then(res => {
+        console.info(openId + '得分已更新:' + score)
+      }, error => {
+           //201803010019:此处添加数据库操作失败报错
+           console.log(error)
+      })
+    })
+  },
+  //逃跑处理函数:
+  runAway(openId) {
+    //获取逃跑者和胜利者的openId
+    console.info('开始执行逃跑函数')
+    let openIdFail = openId, openIdWin
+    let room = rooms[players[openIdFail].roomName]
+    if (openIdFail === room.player1) {
+      openIdWin = room.player2
+    } else {
+      openIdWin = room.player1
+    }
+    //更新得分
+    this.updateScore(openIdWin, 1)
+    this.updateScore(openIdFail, 0)
+    //存储比赛结果
+    this.storeFightingRecord(openIdWin, 1, true)
+    this.storeFightingRecord(openIdFail, 0, true)
+    //通知赢家对方已逃跑
+    if (players[openId]) {
+      if (players[openId].roomName) {
+        rooms[players[openId].roomName].finished = true//先改变房间状态,再关闭,避免被认为逃跑行为
+      }
+    }
+    this.broadcast([players[openIdWin].tunnelId], 'runawayNotice', {
+      message: '对手已逃跑'
+    })
+    //有时出现无效信道ID,导致onclose无法删除用户信息,这里手工补充删除
+    delete players[openId]//删除玩家信息
+  },
+  //保存战绩函数:
+  //fightingResult: 0:表示输,1:表示赢,2:表示平手
+  //runAway:true/false
+  async storeFightingRecord(openId, fightingResult, runAway = false) {
+    let roomName = players[openId].roomName
+    try {
+      if (fightingRecord[roomName] ? false : true) {
+        fightingRecord[roomName] = {
+          openId_winner: '',
+          openId_loser: '',
+          score_winner: 0,
+          score_loser: 0,
+        }
+      }
+      let myRecord = fightingRecord[roomName]
+      //获取双方比赛数据
+      if (fightingResult == 0) {//0为输
+        myRecord.openId_loser = openId
+        myRecord.score_loser = players[openId].choice[3]
+      } else {
+        myRecord.openId_winner = openId
+        myRecord.score_winner = players[openId].choice[3]
+      }
+      //当获取到双方的数据时,开始存储到数据库
+      if (myRecord.openId_winner && myRecord.openId_loser) {
+        let room_name = roomName,
+          run_away = runAway,
+          open_id_winner = myRecord.openId_winner,
+          open_id_loser = myRecord.openId_loser,
+          score_winner = myRecord.score_winner,
+          score_loser = myRecord.score_loser
+        delete fightingRecord[room_name]
+         try{
+              await mysql('fighting_record').insert({ id: null, room_name, run_away, open_id_winner, open_id_loser, score_winner, score_loser, time: null })
+         }catch(error){
+              console.log(error)
+         }
+        console.log('清空后的fightingRecord为', fightingRecord)
+      }
+    } catch (error) {
+      console.log(error)
+    }
+  },
+
+}
+
+/**
+ * 实现 onConnect 方法
+ * 在客户端成功连接 WebSocket 信道服务之后会调用该方法,
+ */
+function onConnect(tunnelId) {
+  tools.broadcast([tunnelId], 'tunnelIdReplaced', {
+    newTunnelId: tunnelId
+  })
+  //PING-PONG机制:发送PING
+  clearTimeout(players[tools.getPlayersOpenId(tunnelId)].timer)
+  tools.broadcast([tunnelId], 'PING', {})
+}
+
+/**
+ * 实现 onClose 方法
+ * 客户端关闭 WebSocket 信道或者被信道服务器判断为已断开后,
+ * 会调用该方法,此时可以进行清理及通知操作
+ */
+function onClose(tunnelId) {
+  console.info('onClose监听到信道' + tunnelId + '关闭')
+  tools.closeTunnel(tunnelId)
+}
+
+/**
+ * 实现 onMessage 方法
+ * 客户端推送消息到 WebSocket 信道服务器上后,会调用该方法,此时可以处理信道的消息。
+ */
+function onMessage(tunnelId, type, content) {
+  console.info('onMessage监听到新消息:', { tunnelId, type, content })
+  // if (!(tunnelId in players)) {
+  //   tools.closeTunnel(tunnelId)
+  // }
+  switch (type) {
+    case 'PONG': //PING-PONG机制:监听PONG
+      if (tunnelId) {
+        let openId = content.openId
+        clearTimeout(players[openId].timer)//清除掉定时器
+
+        let timer = setTimeout(() => {
+          if (players[openId]) {
+            //再次设置一个定时器
+            players[openId].timer = setTimeout(() => {//ping-pong机制:监听客户端是否离线
+              console.log('开始执行PING-PONG定时器函数')
+              tools.closeTunnel(tunnelId)//如果离线,则清空用户信息
+            }, option.PING_PONG_OUT_TIME)
+            //再次发送一个PING
+            tools.broadcast([tunnelId], 'PING', {})
+            console.log(tunnelId + '发送一个PING')
+            clearTimeout(timer)
+          }
+        }, option.PING_PONG_TIME)
+      }
+      break
+
+    case 'updateMatchInfo':
+      if (tunnelId) {
+        let openId = content.openId
+        players[openId].sortId = content.sortId
+        players[openId].friendsFightingRoom = content.friendsFightingRoom
+        console.info('更新用户匹配条件信息:', players[openId])
+      }
+      break
+
+    case 'answer':
+      if (tunnelId) {
+        tools.broadcast([tunnelId], 'getAnswer', {})//通知前端,后台已收到选项
+        console.info('收到了' + players[content.choice.openId].nickName + '(' + tunnelId + ')的信息')
+        let roomName = content.roomName
+        let openId = content.choice.openId
+        rooms[roomName].responseNumber = rooms[roomName].responseNumber + 1//房间获得了1次响应
+        players[openId].choice[1] = content.choice.userChoose
+        players[openId].choice[2] = content.choice.answerColor
+        players[openId].choice[3] = content.choice.scoreMyself
+        if (rooms[roomName].responseNumber === 2) {
+          //当两位玩家都完成答题时,立刻向客户端发送下一题
+          tools.sendQuestion(roomName)
+        }
+      }
+      break
+
+    case 'fightingResult': //用户答完题监听
+      if (tunnelId) {
+        console.info('进入fightingResult阶段')
+        const fightingResult = content.fightingResult
+        const openId = content.openId
+        tools.updateScore(openId, fightingResult)//更新分数
+        tools.storeFightingRecord(openId, fightingResult) //存储比赛详情数据
+        if (rooms[players[openId].roomName]) {
+          rooms[players[openId].roomName].finished = true//先改变房间状态,再关闭,避免被认为逃跑行为
+        }
+        tools.closeTunnel(tunnelId)//关闭信道连接
+        console.info('战斗完成后的队列、玩家、房间和发题定时器为:', match.queueData, players, rooms, tools.data.timerSendQuestion)
+      }
+      break
+
+    default:
+      break
+  }
+}
+
+module.exports = {
+  get: async ctx => {//响应用户开始进行websocket连接,信道服务器连接成功后通知客户端
+    //data:{tunnel:{tunnelId:xxx,connectUrl:xxx},userinfo:{openId:xxx.nickName:xxx,...}}
+    let data = await tunnel.getTunnelUrl(ctx.req)//当用户发起信道请求的时候,会得到信道信息和用户信息
+    let userinfo = data.userinfo
+    let openId = userinfo.openId
+    if (openId in players) {//如果已经存在openId,则说明只需更新信道ID
+      players[openId].tunnelId = data.tunnel.tunnelId
+      console.info('信道变化后的队列、玩家、房间和发题定时器为:', match.queueData, players, rooms, tools.data.timerSendQuestion)
+    } else {
+      let score = await mysql('cSessionInfo').where({ open_id: data.userinfo.openId }).select('score')//[{score:4324}]
+      userinfo.score = score[0].score //number,在用户信息中加入得分
+      userinfo.tunnelId = data.tunnel.tunnelId //在用户信息中加入tunnel_id属性
+      userinfo.matchTime = new Date().getTime() //在用户信息中加入匹配的时间,如:1513670126897
+      userinfo.roomName = null//在用户信息中加入当前战斗状态,默认为null,否则为对战房间号
+      userinfo.friendsFightingRoom = null//初始值为null,匹配前会复制赋值为undefined或一个数字判断是排位赛还是好友匹配
+      userinfo.sortId = null
+      userinfo.choice = [openId, '', '', 0]//[openID:'',user_choose: '',//用户选择了第几个答案 answer_color: '',//用户是否答对 score_myself: '',//用户总得分]
+      userinfo.timer = setTimeout(() => {//ping-pong机制:监听客户端是否离线
+        //tools.closeTunnel(data.tunnel.tunnelId)//如果离线,则清空用户信息//暂时先取消此处定时器
+      }, option.PING_PONG_OUT_TIME)
+      players[openId] = userinfo  //将此用户作为合法用户,并将openId和用户信息(不包含得分,状态等数据)关联起来
+      console.info('新信道加入后的队列、玩家、房间和发题定时器为:', match.queueData, players, rooms, tools.data.timerSendQuestion)
+
+      //在匹配队列中压入一个openId,外套一个队列限制函数
+      match.queueData.push(openId)
+
+    }
+    ctx.state.data = data.tunnel //返回信道信息给用户
+  },
+
+  post: async ctx => {//用来处理信道传递过来的消息
+    const packet = await tunnel.onTunnelMessage(ctx.request.body) //onTunnelMessage:当用户消息发送到信道上时,使用该函数处理信道的消息
+    switch (packet.type) {
+      case 'connect': //用户开始进行websocket连接,信道服务器连接成功后通知服务端
+        onConnect(packet.tunnelId)
+        break
+      case 'message':
+        onMessage(packet.tunnelId, packet.content.messageType, packet.content.messageContent)
+        break
+      case 'close':
+        onClose(packet.tunnelId)
+        break
+    }
+  }
+}

+ 10 - 0
server/controllers/upDateShareInfoToUser_network.js

@@ -0,0 +1,10 @@
+module.exports = async (ctx, next) => {
+  if (ctx.state.$wxInfo.loginState === 1) {
+    const { mysql } = require('../qcloud')
+    let res = await mysql('user_network').where({ clickId: ctx.query.clickId }).update({
+      param_1: ctx.query.content
+    })
+  } else {
+    ctx.state.code = -1
+  }
+}

+ 10 - 0
server/controllers/upDateUser_networkFromClickId.js

@@ -0,0 +1,10 @@
+module.exports = async (ctx, next) => {
+  if (ctx.state.$wxInfo.loginState === 1) {
+    const { mysql } = require('../qcloud')
+    let res = await mysql('user_network').where({ clickId: ctx.query.currentClickId }).update({
+      fromClickId: ctx.query.fromClickId
+    })
+  } else {
+    ctx.state.code = -1
+  }
+}

+ 9 - 0
server/controllers/upload.js

@@ -0,0 +1,9 @@
+const { uploader } = require('../qcloud')
+
+module.exports = async ctx => {
+    // 获取上传之后的结果
+    // 具体可以查看:
+    const data = await uploader(ctx.req)
+
+    ctx.state.data = data
+}

+ 0 - 0
server/controllers/user.js


Some files were not shown because too many files changed in this diff