[
  {
    "path": ".gitignore",
    "content": "node_modules/\nnpm-debug.log\nsftp-config.json"
  },
  {
    "path": "LICENSE",
    "content": "LICENSE - \"MIT License\"\n\nCopyright (c) 2016 by Tencent Cloud\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# Wafer 服务端 Demo - Node.js\n\n本项目是 [腾讯云微信小程序服务端 SDK - Node.js](https://github.com/tencentyun/wafer-node-server-sdk) 的使用示例。示例需要和 [微信小程序客户端示例](https://github.com/tencentyun/wafer-client-demo) 配合一起使用。\n\n## 运行示例\n\n按照[小程序创建资源配置指引](https://github.com/tencentyun/weapp-doc)进行操作，可以得到运行本示例所需的资源和服务，其中包括已部署好的示例代码及自动下发的 SDK 配置文件 `/etc/qcloud/sdk.config`。\n\n- 示例代码部署目录：`/data/release/node-weapp-demo`\n- 运行示例的 Node 版本：`v4.6.0`\n- Node 进程管理工具：`pm2`\n\n## 项目结构\n\n```\nDemo\n├── README.md\n├── app.js\n├── business\n│   └── chat-tunnel-handler.js\n├── config.js\n├── globals.js\n├── package.json\n├── process.json\n├── routes\n│   ├── index.js\n│   ├── welcome.js\n│   ├── login.js\n│   ├── user.js\n│   └── tunnel.js\n└── setup-qcloud-sdk.js\n```\n\n其中，`app.js` 是 启动文件，`config.js` 配置了启动服务监听的端口号，`process.json` 是运行本示例 的 `pm2` 配置文件。\n\n`setup-qcloud-sdk.js` 用于初始化 SDK 配置，配置从文件 `/etc/qcloud/sdk.config` 中读取。 配置文件包含如下配置项：\n\n```json\n{\n    \"serverHost\": \"业务服务器的主机名\",\n    \"authServerUrl\": \"鉴权服务器地址\",\n    \"tunnelServerUrl\": \"信道服务器地址\",\n    \"tunnelSignatureKey\": \"和信道服务器通信的签名密钥\"\n}\n```\n\n`routes/` 目录包含了示例用到的4个路由，路由和处理文件映射关系如下：\n\n```\n// 首页指引\n/ => routes/welcome.js\n\n// 登录\n/login => routes/login.js\n\n// 获取微信用户信息\n/user => routes/user.js\n\n// 处理信道请求\n/tunnel => routes/tunnel.js\n```\n\n`business/chat-tunnel-handler.js` 是业务处理信道请求的示例代码。\n\n## 如何在demo基础上进行开发\n进入目录 `/data/release/node-weapp-demo`，将写好的代码上传到routes目录下\n\n重启服务：  pm2 restart all\n\n\n## 更新 SDK 版本\n\n进入目录 `/data/release/node-weapp-demo`，然后先后执行命令 `npm update`、`pm2 restart process.json` 即可。\n"
  },
  {
    "path": "app.js",
    "content": "'use strict';\n\nrequire('./globals');\nrequire('./setup-qcloud-sdk');\n\nconst http = require('http');\nconst express = require('express');\nconst bodyParser = require('body-parser');\nconst morgan = require('morgan');\nconst config = require('./config');\n\nconst app = express();\n\napp.set('query parser', 'simple');\napp.set('case sensitive routing', true);\napp.set('jsonp callback name', 'callback');\napp.set('strict routing', true);\napp.set('trust proxy', true);\n\napp.disable('x-powered-by');\n\n// 记录请求日志\napp.use(morgan('tiny'));\n\n// parse `application/x-www-form-urlencoded`\napp.use(bodyParser.urlencoded({ extended: true }));\n\n// parse `application/json`\napp.use(bodyParser.json());\n\napp.use('/', require('./routes'));\n\n// 打印异常日志\nprocess.on('uncaughtException', error => {\n    console.log(error);\n});\n\n// 启动server\nhttp.createServer(app).listen(config.port, () => {\n    console.log('Express server listening on port: %s', config.port);\n});\n"
  },
  {
    "path": "business/chat-tunnel-handler.js",
    "content": "'use strict';\n\nconst TunnelService = require('qcloud-weapp-server-sdk').TunnelService;\n\n/**\n * 调用 TunnelService.broadcast() 进行广播\n * @param  {String} type    消息类型\n * @param  {String} content 消息内容\n */\nconst $broadcast = (type, content) => {\n    TunnelService.broadcast(connectedTunnelIds, type, content)\n        .then(result => {\n            let invalidTunnelIds = result.data && result.data.invalidTunnelIds || [];\n\n            if (invalidTunnelIds.length) {\n                debug('检测到无效的信道 IDs =>', invalidTunnelIds);\n\n                // 从`userMap`和`connectedTunnelIds`中将无效的信道记录移除\n                invalidTunnelIds.forEach(tunnelId => {\n                    delete userMap[tunnelId];\n\n                    let index = connectedTunnelIds.indexOf(tunnelId);\n                    if (~index) {\n                        connectedTunnelIds.splice(index, 1);\n                    }\n                });\n            }\n        });\n};\n\n/**\n * 调用 TunnelService.closeTunnel() 关闭信道\n * @param  {String} tunnelId 信道ID\n */\nconst $close = (tunnelId) => {\n    TunnelService.closeTunnel(tunnelId);\n};\n\n// 保存 WebSocket 信道对应的用户\n// 在实际的业务中，应该使用数据库进行存储跟踪，这里作为示例只是演示其作用\nlet userMap = {};\n\n// 保存 当前已连接的 WebSocket 信道ID列表\nlet connectedTunnelIds = [];\n\n/**\n * 实现 WebSocket 信道处理器\n * 本示例配合客户端 Demo 实现一个简单的聊天室功能\n */\nclass ChatTunnelHandler {\n    /**\n     * 实现 onRequest 方法\n     * 在客户端请求 WebSocket 信道连接之后，\n     * 会调用 onRequest 方法，此时可以把信道 ID 和用户信息关联起来\n     */\n    onRequest(tunnelId, userInfo) {\n        debug(`${this.constructor.name} [onRequest] =>`, { tunnelId, userInfo });\n\n        if (typeof userInfo === 'object') {\n            // 保存 信道ID => 用户信息 的映射\n            userMap[tunnelId] = userInfo;\n        }\n    }\n\n    /**\n     * 实现 onConnect 方法\n     * 在客户端成功连接 WebSocket 信道服务之后会调用该方法，\n     * 此时通知所有其它在线的用户当前总人数以及刚加入的用户是谁\n     */\n    onConnect(tunnelId) {\n        debug(`${this.constructor.name} [onConnect] =>`, { tunnelId });\n\n        if (tunnelId in userMap) {\n            connectedTunnelIds.push(tunnelId);\n\n            $broadcast('people', {\n                'total': connectedTunnelIds.length,\n                'enter': userMap[tunnelId],\n            });\n        } else {\n            debug(`Unknown tunnelId(${tunnelId}) was connectd, close it`);\n            $close(tunnelId);\n        }\n    }\n\n    /**\n     * 实现 onMessage 方法\n     * 客户端推送消息到 WebSocket 信道服务器上后，会调用该方法，此时可以处理信道的消息。\n     * 在本示例，我们处理 `speak` 类型的消息，该消息表示有用户发言。\n     * 我们把这个发言的信息广播到所有在线的 WebSocket 信道上\n     */\n    onMessage(tunnelId, type, content) {\n        debug(`${this.constructor.name} [onMessage] =>`, { tunnelId, type, content });\n\n        switch (type) {\n        case 'speak':\n            if (tunnelId in userMap) {\n                $broadcast('speak', {\n                    'who': userMap[tunnelId],\n                    'word': content.word,\n                });\n            } else {\n                $close(tunnelId);\n            }\n            break;\n\n        default:\n            // ...\n            break;\n        }\n    }\n\n    /**\n     * 实现 onClose 方法\n     * 客户端关闭 WebSocket 信道或者被信道服务器判断为已断开后，\n     * 会调用该方法，此时可以进行清理及通知操作\n     */\n    onClose(tunnelId) {\n        debug(`${this.constructor.name} [onClose] =>`, { tunnelId });\n\n        if (!(tunnelId in userMap)) {\n            debug(`${this.constructor.name} [onClose][Invalid TunnelId]=>`, tunnelId);\n            $close(tunnelId);\n            return;\n        }\n\n        const leaveUser = userMap[tunnelId];\n        delete userMap[tunnelId];\n\n        const index = connectedTunnelIds.indexOf(tunnelId);\n        if (~index) {\n            connectedTunnelIds.splice(index, 1);\n        }\n\n        // 聊天室没有人了（即无信道ID）不再需要广播消息\n        if (connectedTunnelIds.length > 0) {\n            $broadcast('people', {\n                'total': connectedTunnelIds.length,\n                'leave': leaveUser,\n            });\n        }\n    }\n}\n\nmodule.exports = ChatTunnelHandler;"
  },
  {
    "path": "config.js",
    "content": "'use strict';\n\nmodule.exports = {\n    /**\n     * Node 服务器启动端口，如果是自行搭建，请保证负载均衡上的代理地址指向这个端口\n     */\n    port: '5757',\n};\n"
  },
  {
    "path": "globals.js",
    "content": "global.debug = (() => {\n    const log = console.log.bind(console);\n\n    return function () {\n        log('========================================');\n        log.apply(null, Array.from(arguments).map(item => {\n            return (typeof item === 'object'\n                ? JSON.stringify(item, null, 2)\n                : item);\n        }));\n        log('========================================\\n');\n    };\n})();"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"wafer-node-server-demo\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Wafer 服务端 Demo - Node.js\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"start\": \"pm2 start process.json\"\n  },\n  \"keywords\": [],\n  \"author\": \"CFETeam\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"body-parser\": \"^1.15.2\",\n    \"express\": \"^4.14.0\",\n    \"lodash\": \"^4.16.1\",\n    \"morgan\": \"^1.7.0\",\n    \"qcloud-weapp-server-sdk\": \"latest\"\n  }\n}\n"
  },
  {
    "path": "process.json",
    "content": "{\n    \"name\": \"weapp\",\n    \"script\": \"app.js\",\n    \"cwd\": \"./\",\n    \"exec_mode\": \"fork\",\n    \"watch\": true,\n    \"env\": {\n        \"NODE_ENV\": \"production\",\n        \"DEBUG_SDK\": \"yes\"\n    }\n}"
  },
  {
    "path": "routes/index.js",
    "content": "'use strict';\n\nconst express = require('express');\nconst router = express.Router();\n\nrouter.get('/', require('./welcome'));\nrouter.get('/login', require('./login'));\nrouter.get('/user', require('./user'));\nrouter.all('/tunnel', require('./tunnel'));\n\nmodule.exports = router;"
  },
  {
    "path": "routes/login.js",
    "content": "'use strict';\n\nconst LoginService = require('qcloud-weapp-server-sdk').LoginService;\n\nmodule.exports = (req, res) => {\n    LoginService.create(req, res).login();\n};"
  },
  {
    "path": "routes/tunnel.js",
    "content": "'use strict';\n\nconst TunnelService = require('qcloud-weapp-server-sdk').TunnelService;\nconst ChatTunnelHandler = require('../business/chat-tunnel-handler');\n\nmodule.exports = (req, res) => {\n    let handler = new ChatTunnelHandler();\n    TunnelService.create(req, res).handle(handler, {\n        'checkLogin': true,\n    });\n};"
  },
  {
    "path": "routes/user.js",
    "content": "'use strict';\n\nconst LoginService = require('qcloud-weapp-server-sdk').LoginService;\n\nmodule.exports = (req, res) => {\n    const loginService = LoginService.create(req, res);\n\n    loginService.check()\n        .then(data => {\n            res.json({\n                'code': 0,\n                'message': 'ok',\n                'data': {\n                    'userInfo': data.userInfo,\n                },\n            });\n        });\n};"
  },
  {
    "path": "routes/welcome.js",
    "content": "const html =\n`<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title>腾讯云微信小程序服务器 Demo - Node.js</title>\n    <style type=\"text/css\">\n\n    ::selection { background-color: #327F2D; color: white; }\n    ::-moz-selection { background-color: #327F2D; color: white; }\n\n    body {\n        background-color: #fff;\n        margin: 40px;\n        font: 13px/20px normal Helvetica, Arial, sans-serif;\n        color: #4F5155;\n    }\n\n    a {\n        color: #003399;\n        background-color: transparent;\n        font-weight: normal;\n        text-decoration: none;\n    }\n\n    h1 {\n        color: #444;\n        background-color: transparent;\n        border-bottom: 1px solid #D0D0D0;\n        font-size: 19px;\n        font-weight: normal;\n        margin: 0 0 14px 0;\n        padding: 14px 0;\n    }\n\n    #container {\n        margin: 10px;\n        padding: 10px 20px;\n        border: 1px solid #D0D0D0;\n        box-shadow: 0 0 8px #D0D0D0;\n    }\n    </style>\n</head>\n<body>\n    <div id=\"container\">\n        <h1>腾讯云微信小程序服务端 Demo - Node.js</h1>\n        <p>会话管理服务</p>\n        <ul>\n            <li><a href=\"/login\">登录服务</a></li>\n            <li><a href=\"/user\">检查登录</a></li>\n        </ul>\n        <p>信道服务</p>\n        <ul>\n            <li><a href=\"/tunnel\">获得信道地址</a></li>\n        </ul>\n    </div>\n</body>\n</html>\n`;\n\nmodule.exports = (req, res) => {\n    res.send(html);\n};"
  },
  {
    "path": "setup-qcloud-sdk.js",
    "content": "const os = require('os');\nconst fs = require('fs');\nconst qcloud = require('qcloud-weapp-server-sdk');\n\nconst sdkConfig = (() => {\n    // Windows\n    if (os.type().toLowerCase().startsWith('windows')) {\n        return 'C:\\\\qcloud\\\\sdk.config';\n    }\n\n    // Linux\n    return '/etc/qcloud/sdk.config';\n})();\n\ntry {\n    const stats = fs.statSync(sdkConfig);\n\n    if (!stats.isFile()) {\n        throw new Error('File not exists.');\n    }\n} catch (e) {\n    debug(`SDK 配置文件（${sdkConfig}）不存在`);\n    process.exit(1);\n}\n\nconst config = (() => {\n    try {\n        const content = fs.readFileSync(sdkConfig, 'utf8');\n        return JSON.parse(content);\n    } catch (e) {\n        debug(`SDK 配置文件（${sdkConfig}）内容不合法`);\n        process.exit(1);\n    }\n})();\n\nqcloud.config({\n    ServerHost: config.serverHost,\n    AuthServerUrl: config.authServerUrl,\n    TunnelServerUrl: config.tunnelServerUrl,\n    TunnelSignatureKey: config.tunnelSignatureKey,\n});\n\n// 网络请求超时时长（单位：毫秒）\nqcloud.config.setNetworkTimeout(config.networkTimeout);\n\ndebug('[当前 SDK 使用配置] =>', config);\n"
  }
]