diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 2d63478..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = { - env: { - node: true, - mocha: true, - es6: true - }, - parserOptions: { - ecmaVersion: 8 - }, - extends: 'eslint:recommended', - rules: { - indent: ['error', 2], - 'linebreak-style': ['error', 'unix'], - quotes: ['error', 'single'], - semi: ['error', 'never'], - 'no-console': 'off', - 'no-unused-vars': 'warn' - } -} diff --git a/.gitignore b/.gitignore index 9f76242..e33609d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1 @@ -node_modules -npm-debug.log -start.sh -.avoscloud -.leancloud - -# VIM -*~ -*.swp +*.png diff --git a/README.md b/README.md index afed19d..6fb6339 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,2 @@ -# LeanEngine Node.js Demos - -该项目是 [LeanEngine](https://leancloud.cn/docs/leanengine_overview.html) Node.js 项目的常用功能和示例仓库。包括了推荐的最佳实践和常用的代码片段,每个文件中都有较为详细的注释,适合云引擎的开发者阅读、参考,也可以将代码片段复制到你的项目中使用。 - -若希望从一个更精简的项目骨架开始开发你的新项目,请使用 [leancloud/node-js-getting-started](https://github.com/leancloud/node-js-getting-started)。 - -## 功能列表(云函数) - -下面列出的功能均以云函数实现,位于 `functions` 目录中。每个文件的开头已列出所需的依赖和配置(环境变量),你可以在安装依赖后将文件直接复制到你的 `functions` 目录中,我们的示例项目会自动加载这个目录中的文件。 - -对于需要 LeanCache 的功能,还请阅读下面的 [LeanCache](#LeanCache) 段落。 - -✅表示写了测试,✨表示实现了相对完整的功能,无需修改即可使用。 - -| 文件名 | 提供的云函数 | 介绍 | -| ------------ | ------------ | ---- | -| [amr-transcoding.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/amr-transcoding.js) | amrToMp3 | 使用 ffmpeg 将 amr 音频转码为 mp3。 | -| [associated-data.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/associated-data.js) | createPostSamplesgetPostsWithAuthorgetPostWithAuthorafterUpdate:_User | 缓存关联数据示例(需要 LeanCache)。 | -| [batch-update.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/batch-update.js) | batchUpdateByQuerybatchUpdateAll | 批量更新数据示例。 | -| [captcha-cache.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/captcha-cache.js) | getCaptchaImageCacherequestMobilePhoneVerifyCache | 使用图形验证码限制短信接口(使用 LeanCache)(需要 LeanCache)。 | -| [captcha-storage.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/captcha-storage.js) | getCaptchaImageStoragerequestMobilePhoneVerifyStorage | ✅ 使用图形验证码限制短信接口(使用云存储后端)。 | -| [crawler.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/crawler.js) | crawlWebsitecrawling | 爬虫示例,使用云队列抓取一个站点下的所有网页。 | -| [imagemagick.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/imagemagick.js) | imageMagicResize | ✅ 使用 imageMagick 处理图像。 | -| [leaderboard.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/leaderboard.js) | submitHighestgetRankRangegetScoreRangegetRankAndScorearchiveLeaderboard | ✨ 一个功能相对完整的排行榜,支持任意数量的用户排序、支持查询任意用户的排名、支持查询任意排名段的用户(需要 LeanCache)。 | -| [limited-stock-rush.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/limited-stock-rush.js) | createRushStockgetOpeningRushsrushcommitRushStock | ✨ 使用 LeanCache 应对秒杀抢购活动中短时间的大量请求(需要 LeanCache)。 | -| [login-by-app.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/login-by-app.js) | requestLoginByAppverifyByApploginByApp | 通过移动端应用验证用户身份,登录网站(比如扫码登录)。| -| [meta.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/meta.js) | getEnvironmentsgetUsergetParamsgetClientMetagetHeadersafterSave:HookObject | 从运行环境或客户端读取元信息(环境变量、请求头等)。 | -| [pubsub.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/pubsub.js) | publishMessage | 使用 Redis Pub/Sub 收发消息。 | -| [queue-delay-retry.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/queue-delay-retry.js) | queueDelayTaskqueueRetryTaskdelayTaskFuncretryTaskFunc | 云队列:延时和重试。云函数任务队列提供了一种可靠地对云函数进行延时运行、重试、结果查询的能力。 | -| [queue-result-query.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/queue-result-query.js) | createTaskqueryResultlongRunningTask | 云队列:结果查询 | -| [readonly.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/readonly.js) | updateCategorygetCategoriesafterUpdate:CategoryafterSave:CategoryafterDelete:CategoryrefreshCategories | 热点只读数据缓存示例(需要 LeanCache)。 | -| [redlock.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/redlock.js) | startTaskLoopgetCurrentTask | 用 LeanCache 实现分布式锁(需要 LeanCache)。 | -| [rtm-signature.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/rtm-signature.js) | signLoginsignStartConversationsignOperateConversationsignQueryMessagesignBlockConversationsignBlockClient | ✅ 使用云引擎实现即时通讯服务的签名。 | -| [todos.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/todos.js) | getAllTodoscreateTododeleteTodosetTodoToDone | 在云引擎中以客户端的权限来操作云存储。 | -| [weapp-decrypt.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/weapp-decrypt.js) | decryptWeappData | ✨ ✅ 解密微信小程序用户加密数据。 | -| [xml.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/xml.js) | xmlBuildObject | 使用云函数序列化 XML 对象。 | -| [rtm-onoff-status.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/functions/rtm-onoff-status.js) | _clientOnline_clientOfflinegetOnOffStatus | 即时通讯上下线状态的存储以及查询(需要 LeanCache)。 | - -## 功能列表(网站托管) - -下面列出的功能位于 `routes` 目录。每个文件的开头已列出所需的依赖和配置(环境变量),有些功能需要额外的配置,请阅读文件开头的说明,你可以在安装依赖后将文件直接复制到你的 `routes` 目录中,然后在 `app.js` 中引用,例如: - -```javascript -app.use('/wechat', require('./routes/wechat-message-callback')) -``` - -| 文件名 | 介绍 | -| ------------ | ---- | -| [wechat-message-callback.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/routes/wechat-message-callback.js) | 接收并自动回复 [微信公众平台的用户消息回调](https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140543) | -| [websocket.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/routes/websocket.js) | 简单的 WebSocket 示例:将客户端发来的消息原样发回客户端(echo)、每隔一秒向客户端发送一条消息(timer) | -| [cookie-session.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/routes/cookie-session.js) | 使用 Cookie Session 在 Cookie 中维持用户登录状态 | -| [render-ejs.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/routes/render-ejs.js) | 使用 EJS 渲染 HTML 页面 | -| [markdown.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/routes/markdown.js) | 项目主页,将 README.md 渲染成 HTML 显示在页面上 | - -## 其他 Demo - -还有一些功能相对完备的 Demo 被制作成了独立的应用: - -| Demo 地址 | 代码地址 | 介绍 | -| ------------ | ---- | ---- | -| [snapcat.leanapp.cn](https://snapcat.leanapp.cn/?url=https://leancloud.cn/docs) | [snapcat 分支](https://github.com/leancloud/leanengine-nodejs-demos/tree/snapcat) | 一个使用 chrome-headless 的截图服务。 | -| [graphql.leanapp.cn](https://graphql.leanapp.cn) | [leancloud/leancloud-graphql](https://github.com/leancloud/leancloud-graphql) | 运行在云引擎上的第三方 GraphQL 支持,允许你用 GraphQL 查询 LeanCloud 云存储中的所有数据。 | -| [leanticket.cn](https://leanticket.cn) | [leancloud/ticket](https://github.com/leancloud/ticket) | 运行在云引擎上的工单系统(即 LeanCloud 官方客服系统) | -| [status.leancloud.cn](https://status.leancloud.cn) | [leancloud/leancloud-status](https://github.com/leancloud/leancloud-status) | LeanCloud 服务状态页。 | - -## 脚本 - -这些工具脚本位于 `bin` 目录: - -| 文件名 | 使用方法 | 介绍 | -| ------------ | ---- | ---- | -| [load-test](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/bin/load-test.js) | `load-test 30` | 对自定义的代码片段进行压力测试的工具,会给出速率和耗时等统计数据。 | - -## LeanCache - -对于用到了 LeanCache 的功能,你需要在控制台上创建 LeanCache 实例,复制该项目根目录的 [redis.js](https://github.com/leancloud/leanengine-nodejs-demos/blob/master/redis.js) 到你的项目,并修改其中的 LeanCache 名称。 - -本地运行 Redis: - -* Mac 运行 `brew install redis` 安装,然后用 `redis-server` 启动。 -* Debian/Ubuntu 运行 `apt-get install redis-server`, CentOS/RHEL 运行 `yum install redis`. -* Windows 尚无官方支持。 - -## 相关文档 - -* [云引擎总览](https://leancloud.cn/docs/leanengine_overview.html) -* [云函数开发指南](https://leancloud.cn/docs/leanengine_cloudfunction_guide-node.html) -* [网站托管开发指南](https://leancloud.cn/docs/leanengine_webhosting_guide-node.html) -* [JavaScript 开发指南](https://leancloud.cn/docs/leanstorage_guide-js.html) -* [JavaScript SDK API](https://leancloud.github.io/javascript-sdk/docs/) -* [Node.js SDK API](https://github.com/leancloud/leanengine-node-sdk/blob/master/API.md) -* [命令行工具使用指南](https://leancloud.cn/docs/leanengine_cli.html) -* [LeanCache 使用指南](https://leancloud.cn/docs/leancache_guide.html) -* [云引擎常见问题和解答](https://leancloud.cn/docs/leanengine_faq.html) +# Snapcat +一个使用 chrome-headless 的截图服务。 diff --git a/app.js b/app.js deleted file mode 100644 index 4bba362..0000000 --- a/app.js +++ /dev/null @@ -1,81 +0,0 @@ -const AV = require('leanengine') -const bodyParser = require('body-parser') -const express = require('express') -const path = require('path') -const timeout = require('connect-timeout') - -// 加载云函数定义,你可以将云函数拆分到多个文件方便管理,但需要在主文件中加载它们 -require('./cloud') - -const app = express() - -// 启用 WebSocket 支持,如不需要可去除 -require('express-ws')(app) - -// 设置模板路径和默认引擎 -app.set('views', path.join(__dirname, 'views')) -app.set('view engine', 'ejs') - -// 设置静态内容路径 -app.use('/public', express.static('public')) - -// 设置默认超时时间 -app.use(timeout('15s')) - -// 加载云引擎中间件 -app.use(AV.express()) - -// 跳转 HTTP 至 HTTPS -app.enable('trust proxy') -app.use(AV.Cloud.HttpsRedirect()) - -// 加载 cookieSession 以支持 AV.User 的会话状态 -app.use(AV.Cloud.CookieSession({ secret: 'randomString', maxAge: 3600000, fetchUser: true })) - -app.use(bodyParser.json()) -app.use(bodyParser.urlencoded({ extended: false })) - -app.use('/', require('./routes/markdown')) - -app.use('/cookie-session', require('./routes/cookie-session')) -app.use('/render-ejs', require('./routes/render-ejs')) -app.use('/websocket', require('./routes/websocket')) -app.use('/wechat', require('./routes/wechat-message-callback')) - -app.use(function(req, res, next) { - // 如果任何一个路由都没有返回响应,则抛出一个 404 异常给后续的异常处理器 - if (!res.headersSent) { - var err = new Error('Not Found') - err.status = 404 - next(err) - } -}) - -// error handlers -app.use(function(err, req, res, _next) { - if (req.timedout && req.headers.upgrade === 'websocket') { - // 忽略 websocket 的超时 - return - } - - var statusCode = err.status || 500 - if (statusCode === 500) { - console.error(err.stack || err) - } - if (req.timedout) { - console.error('请求超时: url=%s, timeout=%d, 请确认方法执行耗时很长,或没有正确的 response 回调。', req.originalUrl, err.timeout) - } - res.status(statusCode) - // 默认不输出异常详情 - var error = {} - if (app.get('env') === 'development') { - // 如果是开发环境,则将异常堆栈输出到页面,方便开发调试 - error = err - } - res.json({ - message: err.message, - error: error - }) -}) - -module.exports = app diff --git a/bin/load-test.js b/bin/load-test.js deleted file mode 100755 index 9e8fd7f..0000000 --- a/bin/load-test.js +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env node - -const AV = require('leanengine') -const Measured = require('measured') -const Promise = require('bluebird') -const Queue = require('promise-queue') - -/* - * load-test [concurrent] - * concurrent: 并发请求的个数(默认 30) - * - * 对代码片段进行压力测试的脚本 - * - * npm install measured bluebird promise-queue - * - * 该脚本会对 `request` 函数进行压力测试(函数的内容需要你自行编写),给出速率和耗时等统计数据,适用于对核心业务逻辑的代码片段进行性能测试。 - */ - -const MAX_CONCURRENT = parseInt(process.argv[process.argv.length - 1]) || 30 - -console.log('MAX_CONCURRENT:', MAX_CONCURRENT) - -AV.init({ - appId: process.env.LEANCLOUD_APP_ID, - appKey: process.env.LEANCLOUD_APP_KEY, - masterKey: process.env.LEANCLOUD_APP_MASTER_KEY -}) - -const queue = new Queue(MAX_CONCURRENT, MAX_CONCURRENT * 10) -const timer = new Measured.Timer() -let counts = 0 -let errors = 0 - -function request() { - return new AV.Query('Post').find().then( () => { - return Promise.delay(20) - }) -} - -function fillQueue() { - for (var i = 0; i < MAX_CONCURRENT * 2 - queue.getQueueLength(); i++){ - queue.add(function() { - const tracker = timer.start() - - return Promise.try(request).catch( err => { - errors++ - console.error(err) - }).then( () => { - counts++ - tracker.end() - - if (queue.getQueueLength() < MAX_CONCURRENT * 2) { - fillQueue() - } - }) - }) - } -} - -fillQueue() - -setInterval( () => { - console.log('errors:', errors, 'counts:', counts) - console.log('metrics:', timer.toJSON()) -}, 1000) - -/* - * 压力测试结果 - - errors: 0 counts: 535 --> 错误数量,总请求数 - metrics: { meter: --> 请求速率统计 - { mean: 52.70664802801556, --> 总平均请求速率(个/秒) - count: 535, - currentRate: 52.93827947179752, --> 最近一秒平均请求速率(个/秒) - '1MinuteRate': 8.13646858079745, --> 最近一分钟平均请求速率(个/秒) - '5MinuteRate': 1.737546674453687, - '15MinuteRate': 0.5856293674221178 }, - histogram: --> 响应时间统计 - { min: 167.52557802200317, --> 最小响应时间(毫秒) - max: 370.25080701708794, --> 最大响应时间(毫秒) - sum: 99939.2678951025, - variance: 757.9111380733852, --> 响应时间方差(毫秒) - mean: 186.80236989738785, --> 平均响应时间(毫秒) - stddev: 27.530185943312937, --> 响应时间标准差(毫秒) - count: 535, - median: 180.83333599567413, --> 中位数响应时间(毫秒) - p75: 185.1685999929905, --> 75% 响应时间不高于(毫秒) - p95: 210.84605119228362, --> 95% 响应时间不高于(毫秒) - p99: 348.27025663375855, --> 99% 响应时间不高于(毫秒) - p999: 370.25080701708794 } } --> 99.9% 响应时间不高于(毫秒) - */ diff --git a/cloud.js b/cloud.js deleted file mode 100644 index 1238562..0000000 --- a/cloud.js +++ /dev/null @@ -1,109 +0,0 @@ -var AV = require('leanengine') -var _ = require('underscore') -const fs = require('fs') -const path = require('path') - -/** - * 加载 functions 目录下所有的云函数 - */ -fs.readdirSync(path.join(__dirname, 'functions')).forEach( file => { - require(path.join(__dirname, 'functions', file)) -}) - -// 声明 Task -var Task = AV.Object.extend('Task'); - -/** - * 一个简单的云代码方法 - */ -AV.Cloud.define('hello', function() { - return 'Hello world!' -}) - -AV.Cloud.define('whoami', function(req, res) { - console.log('whoami:', req.currentUser); - var username = req.currentUser && req.currentUser.get('username'); - res.success(username); -}); - -AV.Cloud.define('noFetchUser', {fetchUser: false}, function(req, res) { - console.log('noFetchUser: user=%s, sessionToken=%s', req.currentUser, req.sessionToken); - res.success(); -}); - -// 从 content 中查找 tag 的正则表达式 -var tagRe = /#(\w+)/g; - -/** - * Todo 的 beforeSave hook 方法 - * 将 content 中的 # 标记都提取出来,保存到 tags 属性中 - */ -AV.Cloud.beforeSave('Todo', function(req, res) { - var todo = req.object; - var tags = todo.get('content').match(tagRe); - tags = _.uniq(tags); - todo.set('tags', tags); - res.success(); -}); - -/** -云函数超时示例 -https://leancloud.cn/docs/leanengine_cloudfunction_guide-node.html#超时的处理方案 -示例中写了一个没有具体业务场景的任务。 -写自己的具体业务场景下的任务时,我们建议设计成幂等任务,使其重复执行也不会有问题。 -*/ -function doTask() { - console.log('begin task'); - return new Promise(function(resolve, reject) { - // 随机运行时长 1s ~ 20s 之间 - var randomTime = (Math.floor(Math.random() * 20 + 1)) * 1000; - setTimeout(function () { - // 随机时长为奇数时,模拟任务失败 - if (randomTime % 2000 === 0) { - resolve(); - } else { - reject(new Error('some reasons for task failed')); - } - }, randomTime); - }); - -} - -AV.Cloud.define('asyncTask', function(req, res) { - var task = new Task(); - var taskName = req.params.name; - // 存储任务队列,这里只设置了 name 字段,可以根据业务需求增加其他的字段信息 - task.set('name', taskName); - // 设置状态为「处理中」 - task.set('status', 'pending'); - task.save().then(function (task) { - // 先返回任务 Id,再执行任务 - res.success(task.id); - doTask().then(function() { - // 任务成功完成,设置状态为「成功」 - task.set('status', 'success'); - return task.save(); - }).then(function(_task) { - console.log('task succeed'); - }).catch(function (error) { - // 任务失败,设置状态为「失败」 - task.set('status', 'failure'); - task.set('errMsg', error.message); - task.save().then(function(_task) { - console.log('task failed'); - }).catch(function(error) { - console.log('更新 task 失败', error); - }); - }); - }).catch(function(error) { - // 任务队列保存失败,返回失败 - res.err(error); - }); -}); - -function printRequest(label, request) { - console.log(label, request.params, request.object, request.user, request.body); -} - - -module.exports = AV.Cloud; diff --git a/functions/amr-transcoding.js b/functions/amr-transcoding.js deleted file mode 100644 index 7aafd27..0000000 --- a/functions/amr-transcoding.js +++ /dev/null @@ -1,55 +0,0 @@ -const AV = require('leanengine') -const ffmpeg = require('fluent-ffmpeg') -const fs = require('fs') -const os = require('os') -const path = require('path') -const request = require('request') - -/* - * 使用 ffmpeg 将 amr 音频转码为 mp3 - * - * 安装依赖: - * - * npm install fluent-ffmpeg request - * - * 在 `leanengine.yaml` 中添加: - * - * systemDependencies: - * - ffmpeg - * - */ - -/* - * 参数的 file 字段接受一个 AV.File(amr 音频) - * 返回一个新的 AV.File(mp3 音频) - */ -AV.Cloud.define('amrToMp3', async request => { - const amrPath = await downloadFile(request.params.file) - const mp3Path = path.join(path.dirname(amrPath), path.basename('.amr') + '.mp3') - - await new Promise( (resolve, reject) => { - ffmpeg(amrPath) - .format('mp3') - .on('end', () => resolve() ) - .on('error', err => reject(err) ) - .save(mp3Path) - }) - - const newFileName = path.basename(request.params.file.get('name'), '.amr') + '.mp3' - const mp3File = await new AV.File(newFileName, fs.createReadStream(mp3Path)).save() - - await fs.promises.unlink(amrPath) - await fs.promises.unlink(mp3Path) - - return mp3File -}) - -function downloadFile(file) { - return new Promise( (resolve, reject) => { - const filepath = `${os.tmpdir()}${file.id}.amr` - - request(file.get('url')).pipe(fs.createWriteStream(filepath)) - .on('close', () => resolve(filepath) ) - .on('error', err => reject(err) ) - }) -} diff --git a/functions/associated-data.js b/functions/associated-data.js deleted file mode 100644 index 0136d28..0000000 --- a/functions/associated-data.js +++ /dev/null @@ -1,132 +0,0 @@ -const AV = require('leanengine') -const Promise = require('bluebird') -const _ = require('lodash') - -/* - * 缓存关联数据示例 - * - * 这种模式适合被关联的数据量少、查询频繁、不常修改,或者关联结构非常复杂(需要多次查询或需要对被关联对象做计算)的情况, - * 应用合理的话可以减少对云存储的查询次数、缩短请求的处理时间,但要注意当关联对象被修改时要及时刷新缓存,否则会出现数据不同步的情况。 - * - * 例如我们有一个社区,Post 代表一篇文章,author 字段是一个 User 对象,代表文章的作者。 - * 在这个社区中活跃用户的数量和文章数量相比较小,且用户对象上的数据也不常变化(可以通过 User 的 afterUpdate Hook 来刷新缓存)。 - * - * 安装依赖: - * - * npm install lodash bluebird - * - */ - -const {redisClient} = require('../redis') -const Post = AV.Object.extend('Post') - -/* 生成测试数据,创建 100 个 Post, 从 User 表中随机选择用户作为 author */ -AV.Cloud.define('createPostSamples', async request => { - const users = await new AV.Query(AV.User).find() - - await AV.Object.saveAll(_.range(0, 100).map( () => { - const post = new Post() - post.set('author', _.sample(users)) - return post - })) -}) - -/* 查询 100 个 Post */ -AV.Cloud.define('getPostsWithAuthor', async request => { - const posts = await new AV.Query(Post).find() - - const users = await fetchUsersFromCache(posts.map( post => { - return post.get('author').id - })) - - return posts.map( post => { - return _.extend(post.toJSON(), { - author: _.find(users, {id: post.get('author').id}) - }) - }) -}) - -/* 查询单个 Post */ -AV.Cloud.define('getPostWithAuthor', async request => { - const post = await new AV.Query(Post).get(request.params.id) - const user = await fetchUserFromCache(post.get('author').id) - - return _.extend(post.toJSON(), { - author: user - }) -}) - -/* 在 User 被修改后删除缓存 */ -AV.Cloud.afterUpdate('_User', function(request) { - redisClient.del(redisUserKey(request.object.id)).catch(console.error) -}) - -/* 从缓存中读取一个 User, 如果没有找到则从云存储中查询 */ -function fetchUserFromCache(userId) { - return redisClient.get(userId).then(function(cachedUser) { - if (cachedUser) { - // 反序列化为 AV.Object - return AV.parseJSON(JSON.parse(cachedUser)) - } else { - new AV.Query(AV.User).get(userId).then(function(user) { - if (user) { - // 将序列化后的 JSON 字符串存储到 LeanCache - redisClient.set(redisUserKey(userId), JSON.stringify(user.toFullJSON())).catch(console.error) - } - - return user - }) - } - }) -} - -/* 从缓存中读取一组 User, 如果没有找到则从云存储中查询(会进行去重并合并为一个查询)*/ -function fetchUsersFromCache(userIds) { - // 先从 LeanCache 中查询 - return redisClient.mget(_.uniq(userIds).map(redisUserKey)).then(function(cachedUsers) { - const parsedUsers = cachedUsers.map(function(user) { - // 对 User(也就是 AV.Object)进行反序列化 - return AV.parseJSON(JSON.parse(user)) - }) - - // 找到 LeanCache 中没有缓存的那些 User - const missUserIds = _.uniq(userIds.filter(function(userId) { - return !_.find(parsedUsers, {id: userId}) - })) - - return Promise.try(function() { - if (missUserIds.length) { - // 从云存储中查询 LeanCache 中没有的 User - return new AV.Query(AV.User).containedIn('objectId', missUserIds).find() - } else { - return [] - } - }).then(function(latestUsers) { - if (latestUsers.length) { - // 将从云存储中查询到的 User 缓存到 LeanCache, 此处为异步 - redisClient.mset(_.flatten(latestUsers.map(function(user) { - return [redisUserKey(user.id), JSON.stringify(user.toFullJSON())] - }))).catch(console.error) - } - - // 将来自缓存和来自云存储的用户组合到一起作为结果返回 - return userIds.map(function(userId) { - return _.find(parsedUsers, {id: userId}) || _.find(latestUsers, {id: userId}) - }) - }) - }) -} - -/* User 存储在 LeanCache 中的键名,值是经过 JSON 序列化的 AV.Object */ -function redisUserKey(userId) { - return 'users:' + userId -} - -/* - * 更进一步 - * - * - 如果数据量较大,担心占用过多内存,可以考虑为缓存设置过期时间。 - * - 这个例子侧重展示关联数据,但在其实 Post 本身也是可以缓存的。 - * - 其实获取一个 User 是获取一组 User 的特例,完全可以用 `fetchUsersFromCache([id])` 代替 `fetchUserFromCache(id)`. - * - 这个例子没有考虑到被关联的用户不存在的情况,如果一个 Post 关联了一个不存在的用户,那么会反复地从云存储查询这个用户,可以通过设置一个特殊的、表示用户不存在的值缓存到 LeanCache. - */ diff --git a/functions/batch-update.js b/functions/batch-update.js deleted file mode 100644 index e620a6d..0000000 --- a/functions/batch-update.js +++ /dev/null @@ -1,131 +0,0 @@ -const AV = require('leanengine') -const Promise = require('bluebird') - -/* - * 批量更新数据示例 - * - * LeanCloud 只提供了更新单个对象的能力,因此在需要批量更新大量对象时,我们需要先找出需要更新的对象,再逐个更新。 - * - * 下面提供了两种更新的方式,你可以根据需要选择其中一个: - * - `batchUpdateByQuery`: 通过一个查询来找到需要更新的对象(例如我们要把 status 字段从 a 更新到 b,那么我们就查询 status == a 的对象), - * 这种情况下需要保证未更新的对象一定符合这个查询、已更新的对象一定不符合这个查询,否则可能会出现遗漏或死循环。 - * - `batchUpdateAll`: 通过 createdAt 从旧到新更新一个数据表中所有的对象,如果中断需要从日志中的上次中断处重新执行(不能从头执行,否则会重复)。 - * - * 安装依赖: - * - * npm install bluebird - * - */ - -const Post = AV.Object.extend('Post') - -AV.Cloud.define('batchUpdateByQuery', async request => { - const status = request.params.status || 'a' - - const createQuery = () => { - return new AV.Query(Post).notEqualTo('status', status) - } - - await batchUpdateByQuery(createQuery, (object) => { - console.log('performUpdate for', object.id) - object.set('status', status) - return object.save() - }) - - console.log('batch update finished') -}) - -AV.Cloud.define('batchUpdateAll', async request => { - const status = request.params.status || 'a' - - const createQuery = () => { - return new AV.Query(Post) - } - - await batchUpdateAll(createQuery, (object) => { - console.log('performUpdate for', object.id) - object.set('status', status) - return object.save() - }) - - console.log('batch update finished') -}) - -/* - * batchUpdateByQuery 和 batchUpdateAll 的参数: - * - * - `createQuery: function(): AV.Query` 返回查询对象,只有符合查询的对象才会被更新。 - * - `performUpdate: function(object): Promise` 执行更新操作的函数,返回一个 Promise。 - * - * options: - * - * - `batchLimit: number` 每一批次更新对象的数量,默认 1000。 - * - `concurrencyLimit: number` 并发更新对象的数量,默认 3,商用版应用可以调到略低于工作线程数。 - * - `ignoreErrors: boolean`: 忽略更新过程中的错误。 - * - `lastCreatedAt: Date`: 从上次中断时的 createdAt 继续(只适用 batchUpdateAll)。 - * - * 性能优化建议(数据量大于十万条需要考虑): - * - * - batchUpdateByQuery 的查询需要有索引。 - * - batchUpdateAll 中的查询需要和 createdAt 有复合索引;如果需要排除的对象很少,可以考虑在 performUpdate 中进行过滤,而不是作为一个查询条件。 - */ - -function batchUpdateByQuery(createQuery, performUpdate, options = {}) { - var batchLimit = options.batchLimit || 1000 - var concurrency = options.concurrencyLimit || 3 - var ignoreErrors = options.ignoreErrors - - function next() { - var query = createQuery() - - return query.limit(batchLimit).find().then( results => { - if (results.length > 0) { - return Promise.map(results, (object) => { - return performUpdate(object).catch( err => { - if (ignoreErrors) { - console.error('ignored', err) - } else { - throw err - } - }) - }, {concurrency}).then(next) - } - }) - } - - return next() -} - -function batchUpdateAll(createQuery, performUpdate, options = {}) { - var batchLimit = options.batchLimit || 1000 - var concurrency = options.concurrencyLimit || 3 - var ignoreErrors = options.ignoreErrors - - function next(lastCreatedAt) { - var query = createQuery() - - if (lastCreatedAt) { - query.greaterThan('createdAt', lastCreatedAt) - } - - return query.ascending('createdAt').limit(batchLimit).find().then( results => { - if (results.length > 0) { - return Promise.map(results, (object) => { - return performUpdate(object).catch( err => { - if (ignoreErrors) { - console.error('ignored', err) - } else { - throw err - } - }) - }, {concurrency}).then( () => { - const nextCreatedAt = results[results.length - 1].createdAt - console.log('nextCreatedAt', nextCreatedAt) - return next(nextCreatedAt) - }) - } - }) - } - - return next(options.lastCreatedAt) -} diff --git a/functions/captcha-cache.js b/functions/captcha-cache.js deleted file mode 100644 index e720d5e..0000000 --- a/functions/captcha-cache.js +++ /dev/null @@ -1,59 +0,0 @@ -const AV = require('leanengine') -const Captchapng = require('captchapng') - -const {redisClient} = require('../redis') - -/* - * 使用图形验证码限制短信接口(使用 LeanCache 后端) - * - * 在这个例子中,我们会要求用户填写一个图形验证码,只有当验证码填写正确时,才会发送短信,来预防恶意的攻击行为。 - * - * 安装依赖: - * - * npm install captchapng - * - * 设置环境变量: - * - * env CAPTCHA_TTL=600000 # 图形验证码有效期(毫秒) - * - */ - -/* 获取一个验证码,会返回一个 captchaId 和一个 base64 格式的图形验证码 */ -AV.Cloud.define('getCaptchaImageCache', async request => { - const captchaId = Math.random().toString(); - const captchaCode = parseInt(Math.random() * 9000 + 1000) - const picture = new Captchapng(80, 30, captchaCode) - - picture.color(0, 0, 0, 0) - picture.color(80, 80, 80, 255) - - await redisClient.setex(captchaKey(captchaId), Math.round((parseInt(process.env.CAPTCHA_TTL) || 600000) / 1000), captchaCode) - - res.json({ - captchaId: captchaId, - imageUrl: 'data:image/png;base64,' + picture.getBase64() - }) -}) - -/* 提交验证码,需要提交 captchaId、captchaCode、mobilePhoneNumber,认证成功才会发送短信 */ -AV.Cloud.define('requestMobilePhoneVerifyCache', async request => { - const captchaId = request.params.captchaId - - const captchaCode = await redisClient.get(captchaKey(captchaId)) - - if (captchaCode && captchaCode === req.body.captchaCode) { - // 在验证成功后删除验证码信息,防止验证码被反复使用 - if (await redisClient.del(captchaKey(captchaId))) { - return await AV.User.requestMobilePhoneVerify(req.body.mobilePhoneNumber) - } - } - - throw new AV.Cloud.Error('图形验证码不正确或已过期', {status: 401}) -}) - -/* - * 更进一步 - * - * - 四位的短信验证码很容易被穷举出来,因此可以考虑验证码输入错误达到一定次数时,从 Redis 删除这个验证码,要求用户重新获取验证码。 - * - 这个例子中「从 Redis 查询验证码信息」和验证成功后「从 Redis 删除验证码信息」的过程并不是原子的,有关这个话题请参考 http://www.rediscookbook.org/get_and_delete.html - */ diff --git a/functions/captcha-storage.js b/functions/captcha-storage.js deleted file mode 100644 index 4667faf..0000000 --- a/functions/captcha-storage.js +++ /dev/null @@ -1,77 +0,0 @@ -const AV = require('leanengine') -const Captchapng = require('captchapng') - -/* - * 使用图形验证码限制短信接口(使用云存储后端) - * - * 在这个例子中,我们会要求用户填写一个图形验证码,只有当验证码填写正确时,才会发送短信,来预防恶意的攻击行为。 - * - * 安装依赖: - * - * npm install captchapng - * - * 设置环境变量: - * - * env CAPTCHA_TTL=600000 # 图形验证码有效期(毫秒) - * - */ - -/* 获取一个验证码,会返回一个 captchaId 和一个 base64 格式的图形验证码 */ -AV.Cloud.define('getCaptchaImageStorage', async request => { - const captchaCode = parseInt(Math.random() * 9000 + 1000) - const picture = new Captchapng(80, 30, captchaCode) - - picture.color(0, 0, 0, 0) - picture.color(80, 80, 80, 255) - - // 使用一个空的 ACL,确保没有任何用户可读可写 captcha 对象 - // 后续所有对 captcha 对象的查询和修改操作都在云引擎中, - // 并且使用 masterKey 权限进行操作。 - const captcha = await new AV.Object('Captcha').setACL(new AV.ACL()).save({ - code: captchaCode, - isUsed: false, - }) - - return { - captchaId: captcha.id, - imageUrl: 'data:image/png;base64,' + picture.getBase64() - } -}) - -/* 提交验证码,需要提交 captchaId、captchaCode、mobilePhoneNumber,认证成功才会发送短信 */ -AV.Cloud.define('requestMobilePhoneVerifyStorage', async request => { - const captchaId = request.params.captchaId - const captchaCode = parseInt(request.params.captchaCode) - - try { - // 将「验证 id 和 code 是否有效」的查询放在「更新验证码状态」的保存操作中,保证两个操作的原子性 - await AV.Object.createWithoutData('Captcha', captchaId).save({ - isUsed: true - }, { - useMasterKey: true, // 确保使用 masterKey 权限进行操作,否则无权读写 captcha 记录 - query: new AV.Query('Captcha') - .equalTo('objectId', captchaId) - .equalTo('code', captchaCode) - .greaterThanOrEqualTo('createdAt', new Date(new Date().getTime() - (parseInt(process.env.CAPTCHA_TTL || 600000)))) - .equalTo('isUsed', false), - }) - - await AV.User.requestMobilePhoneVerify(request.params.mobilePhoneNumber) - } catch (err) { - if (err.code === 305) { - // query 条件不匹配,所以对记录更新不成功 - throw new AV.Cloud.Error('图形验证码不正确或已过期', {status: 401}) - } else if (err.message.indexOf('Could not find object') === 0) { - // 指定 id 不存在 - throw new AV.Cloud.Error('图形验证码不存在', {status: 401}) - } else { - throw err - } - } -}) - -/* - * 更进一步 - * - * - 四位的短信验证码很容易被穷举出来,因此可以考虑验证码输入错误达到一定次数时,从存储服务更新验证码的状态为「已使用」,要求用户重新获取验证码。 - */ diff --git a/functions/crawler.js b/functions/crawler.js deleted file mode 100644 index b50d800..0000000 --- a/functions/crawler.js +++ /dev/null @@ -1,93 +0,0 @@ -const _ = require('lodash') -const {URL} = require('url') -const AV = require('leanengine') -const cheerio = require('cheerio') -const crypto = require('crypto') -const memoize = require('promise-memoize') -const requestPromise = require('request-promise') - -/* - * 爬虫示例,使用 Cloud Queue 抓取一个站点下的所有网页 - * - * 在这个例子中我们实现了一个简单的爬虫来抓取 LeanCloud 的文档页面, - * 云函数 crawling 是抓取的主要逻辑,它会将结果保存到云存储(CrawlerResults)中、 - * 继续将页面中的链接通过 queuePage 函数来加入队列(将 url 设置为 uniqueId 以免重复抓取)。 - * - * 这样每次抓取页面都是一次云函数调用,即使要抓取的页面的量非常大也不会有超时或中断的问题, - * 如果发生了意外的错误,队列还会进行默认的一次重试。 - * - * 安装依赖: - * - * npm install request-promise cheerio promise-memoize lodash - */ - -const CrawlerResults = AV.Object.extend('CrawlerResults') - -AV.Cloud.define('crawlWebsite', async request => { - const startUrl = request.params.startUrl || 'https://leancloud.cn/docs/' - const urlLimit = request.params.urlLimit || 'https://leancloud.cn/docs/' - - return await queuePage(new URL(startUrl), null, urlLimit) -}) - -AV.Cloud.define('crawling', async request => { - const {url, referer, urlLimit} = request.params - - try { - const $ = cheerio.load(await requestPromise(url)) - - await new CrawlerResults().save({ - url: url, - referer: referer || '', - title: $('title').text() - }) - - getPageUrls($, url).forEach( subUrl => { - subUrl.hash = '' - subUrl.search = '' - subUrl.protocol = 'https' - - if (subUrl.href.startsWith(urlLimit)) { - queuePage(subUrl, url, urlLimit).catch( err => { - if (err.code !== 409) { - console.log(err) - } - }) - } - }) - - return { - url: url, - title: $('title').text() - } - } catch (err) { - if (err.message.startsWith('404')) { - console.error(`crawling ${url} failed:`, err.message, 'referer:', referer) - } else { - throw err - } - } -}) - -// 这里用 promise-memoize 添加了一个进程内缓存来减少对 Cloud Queue 的调用次数, -// 但这个缓存并不是必须的,因为 Cloud Queue 本身会根据 uniqueId 去除, -// 因此即使这个程序以多实例运行在云引擎,也不会有问题 -const queuePage = memoize(function queuePage(url, referer, urlLimit) { - return AV.Cloud.enqueue('crawling', { - url: url.href, - referer, - urlLimit - }, { - uniqueId: md5(url.href) - }) -}, {maxAge: 60000, maxErrorAge: 60000, resolve: [String, _.noop, _.noop]}) - -function getPageUrls($, url) { - return $($('a')).map( (index, link) => { - return new URL($(link).attr('href'), url) - }).toArray() -} - -function md5(string) { - return crypto.createHash('md5').update(string).digest('hex') -} diff --git a/functions/imagemagick.js b/functions/imagemagick.js deleted file mode 100644 index a14e35f..0000000 --- a/functions/imagemagick.js +++ /dev/null @@ -1,32 +0,0 @@ -const AV = require('leanengine') -const gm = require('gm') - -/* - * 使用 imageMagick 处理图像 - * - * 安装依赖: - * - * npm install gm - * - * 在 `leanengine.yaml` 中添加: - * - * systemDependencies: - * - imagemagick - * - */ - -const imageMagick = gm.subClass({imageMagick: true}) - -AV.Cloud.define('imageMagicResize', async request => { - return new Promise( (resolve, reject) => { - imageMagick('public/leanstorage.png').resize(91, 77).toBuffer('png', (err, buffer) => { - if (err) { - reject(err) - } else { - resolve({ - imageUrl: 'data:image/png;base64,' + buffer.toString('base64') - }) - } - }) - }) -}) diff --git a/functions/leaderboard.js b/functions/leaderboard.js deleted file mode 100644 index 7b3b749..0000000 --- a/functions/leaderboard.js +++ /dev/null @@ -1,110 +0,0 @@ -const AV = require('leanengine') -const _ = require('lodash') -const moment = require('moment') - -const {redisClient} = require('../redis') -const Leaderboard = AV.Object.extend('Leaderboard') - -/* - * 使用 LeanCache 实现排行榜 - * - * 排行榜的查询会比较频繁,而且被查询的都是同一份数据,且数据变化则较少,比较适合维护在 LeanCache 中。 - * 这个例子中我们将允许用户提交自己的游戏分数,然后在 LeanCache 中维护一个全部用户的排行榜, - * 每天凌晨会将前一天的排行归档到云存储中,并清空排行榜。 - * - * 安装依赖: - * - * npm install moment lodash - * - */ - -/* 用于提交最高分数的 LUA 脚本,只会在新分数比最高分还高时才更新分数 */ -redisClient.defineCommand('setHighest', { - numberOfKeys: 1, - lua: ` - local highest = tonumber(redis.call("ZSCORE", KEYS[1], ARGV[2])) - if highest == nil or tonumber(ARGV[1]) > highest then - redis.call("ZADD", KEYS[1], ARGV[1], ARGV[2]) - end - ` -}) - -/* 排行榜存储在 LeanCache 中的键名,按照当前日期存储为一个 ZSET,值是用户 ID */ -function redisKey(time) { - return 'leaderboard:' + moment(time).format('YYYYMMDD') -} - -/* 提交当前用户的最高分数 */ -AV.Cloud.define('submitHighest', async request => { - if (request.currentUser) { - await redisClient.setHighest(redisKey(), request.params.score, request.currentUser.objectId) - } else { - throw new AV.Cloud.Error('当前未登录用户,无法提交分数', {status: 401}) - } -}) - -/* 查询排行榜的特定排名范围(默认前 100) */ -AV.Cloud.define('getRankRange', async request => { - const start = request.params.start || 0 - const end = request.params.end || 99 - - return parseLeaderboard(await redisClient.zrevrange(redisKey(), start, end, 'WITHSCORES')) -}) - -/* 查询排行榜的特定分数范围(默认前 100) */ -AV.Cloud.define('getScoreRange', async request => { - const max = request.params.start || '+inf' - const min = request.params.end || '-inf' - const limit = request.params.limit || 100 - - return parseLeaderboard(await redisClient.zrevrangebyscore(redisKey(), max, min, 'WITHSCORES', 'LIMIT', 0, limit)) -}) - -/* 查询用户在排行榜上的排名和分数(默认当前用户) */ -AV.Cloud.define('getRankAndScore', async request => { - const userId = request.params.userId || request.currentUser.objectId - - const score = await redisClient.zscore(redisKey(), userId) - - if (score === null) { - throw new AV.Cloud.Error('用户在排行榜上不存在', {status: 404}) - } else { - return { - rank: await redisClient.zrevrank(redisKey(), userId), - userId: userId, - score: score - } - } -}) - -/* 用于归档前一天排行榜的定时任务,请在控制台上新建一个每天凌晨一点的定时任务 */ -AV.Cloud.define('archiveLeaderboard', async request => { - const yesterday = moment().subtract(1, 'day') - - const leaderboard = await redisClient.zrevrange(redisKey(), 0, -1, 'WITHSCORES') - - await new Leaderboard().save({ - date: yesterday.format('YYYYMMDD'), - users: parseLeaderboard(leaderboard) - }) - - await redisClient.del(redisKey(yesterday)); -}) - -// 将 ZRANGE 的结果解析为 {ranking, userId, score} 这样的对象 -function parseLeaderboard(leaderboard) { - return _.chunk(leaderboard, 2).map(function(item, index) { - return { - ranking: index + 1, - userId: item[0], - score: parseInt(item[1]) - } - }) -} - -/* - * 更进一步 - * - * - 这个排行榜中只有用户 ID, 你可能需要结合「缓存关联数据示例」来一并显示用户的昵称等信息。 - * - 为了防止 archiveLeaderboard 被重复调用,建议在 Leaderboard 的 date 字段上设置唯一索引。 - */ diff --git a/functions/limited-stock-rush.js b/functions/limited-stock-rush.js deleted file mode 100644 index 557099f..0000000 --- a/functions/limited-stock-rush.js +++ /dev/null @@ -1,106 +0,0 @@ -const _ = require('lodash') -const AV = require('leanengine') -const Promise = require('bluebird') - -const {redisClient} = require('../redis') -const RushStock = AV.Object.extend('RushStock') - -/* - * 使用 LeanCache 实现秒杀抢购 - * - * 在秒杀抢购活动中可能会在短时间内有大量的请求,如果每个请求都需要访问云存储会占用大量的工作线程数, - * 在这个例子中我们将秒杀活动的信息存到 LeanCache 中,并用 LeanCache 来维护秒杀结果, - * 一个 LeanCache 实例可以支持 10000 QPS 甚至更多的请求, - * 在秒杀活动期间不需要访问云存储,在活动结束后再将结果提交至云存储。 - * - * 安装依赖: - * - * npm install lodash bluebird - * - */ - -/* - * 供管理员创建一个秒杀活动 - * - * quota 表示这个活动的配额,即有多少用户可以抢到; - * items 表示按顺序每个用户获得的商品,会在秒杀成功后提示给用户,你可以修改代码来生成这个字段的值。 - */ -AV.Cloud.define('createRushStock', {internal: true}, async request => { - const name = request.params.name - const quota = parseInt(request.params.quota) || 20 - - const items = _.times(quota, String) - - const rush = await new RushStock().save({ - name: name, - quota: quota, - items: items, - status: 'opening' - }) - - const rushId = rush.objectId - - await redisClient.hset('stockRushStocks', rushId, JSON.stringify({rushId, quota, items})) - - return rush -}) - -/* 供用户获取当前开放的秒杀列表 */ -AV.Cloud.define('getOpeningRushs', async request => { - return Promise.map(await redisClient.hgetall('stockRushStocks'), async (rushStringify, name) => { - const rushStock = JSON.parse(rushStringify) - const takedCount = await redisClient.llen(`stockRushStockTaked:${rushStock.rushId}`) - - return { - name: name, - rushId: rushStock.rushId, - quota: rushStock.quota, - takedCount: takedCount - } - }) -}) - -/* 供用户参与秒杀活动 */ -AV.Cloud.define('rush', async request => { - const rushId = request.params.rushId - - if (!request.currentUser) { - throw new AV.Cloud.Error('当前未登录用户', {status: 401}) - } - - const [rushStringify, takedCount] = await redisClient.multi() - .hget('stockRushStocks', rushId) - .llen(`stockRushStockTaked:${rushId}`) - .exec() - - const rushStock = JSON.parse(rushStringify) - - if (rushStock.quota < takedCount) { - throw new AV.Cloud.Error('红包已抢完') - } - - const newTakedCount = await redisClient.rpsuh(`stockRushStockTaked:${rushId}`, request.currentUser.objectId) - - if (takedCount < rushStock.quota) { - return {message: `恭喜抢到 ${rushStock.items[newTakedCount - 1]}`} - } else { - throw new AV.Cloud.Error('红包已抢完') - } -}) - -/* 供管理员将秒杀结果提交到云存储 */ -AV.Cloud.define('commitRushStock', {internal: true}, async request => { - const rushId = request.params.rushId - - const rush = await new AV.Query(RushStock).get(rushId) - const userIds = await redisClient.lrange(`stockRushStockTaked:${rushId}`, 0, rush.get('quota') - 1) - - await rush.save({ - status: 'closed', - users: userIds.map( userId => { - return AV.Object.createWithoutData('_User', userId) - }) - }) - - await redisClient.del(`stockRushStockTaked:${rushId}`) -}) diff --git a/functions/login-by-app.js b/functions/login-by-app.js deleted file mode 100644 index 3648c96..0000000 --- a/functions/login-by-app.js +++ /dev/null @@ -1,58 +0,0 @@ -const AV = require("leanengine"); -const { redisClient } = require("../redis"); -const { nanoid } = require("nanoid"); - -/* - * 通过移动端应用登录网站。 - * - * 登录网站时,网站显示二维码,供已登录的移动端应用扫描,扫描后网站变为登录状态。 - * - */ - -/* - * 供网站调用,返回一个随机 token,网站可以将 token 转换为二维码。 - */ -AV.Cloud.define("requestLoginByApp", async (request) => { - const token = nanoid(); - await redisClient.set(`loginByAppToken:${token}`, "incoming", "EX", 3600); - return token; -}); - -/* - * 供移动端应用调用,参数为(通过扫描二维码获得的)token。 - * 用户在移动端应用需处于已登陆状态。 - */ -AV.Cloud.define("verifyByApp", async (request) => { - const token = request.params.token; - const incoming = await redisClient.get(`loginByAppToken:${token}`); - if (incoming === "incoming") { - await redisClient.set( - `loginByAppToken:${token}`, - request.sessionToken, - "EX", - 3600 - ); - return "OK"; - } else { - throw new AV.Cloud.Error( - `Verification failed. Possible cause: token is invalid or expired.` - ); - } -}); - -/* - * 供网站调用,返回 sessionToken。网站凭 sessionToken 调用 AV.User.become 方法完成登录。 - */ -AV.Cloud.define("loginByApp", async (request) => { - const token = request.params.token; - const sessionToken = await redisClient.get(`loginByAppToken:${token}`); - if (sessionToken === null) { - throw new AV.Cloud.Error( - "Failed to login. Possible cause: token is invalid or expired." - ); - } else if (sessionToken === "incoming") { - throw new AV.Cloud.Error("Not verified by the corresponding App yet."); - } else { - return sessionToken; - } -}); diff --git a/functions/meta.js b/functions/meta.js deleted file mode 100644 index b77e7dd..0000000 --- a/functions/meta.js +++ /dev/null @@ -1,52 +0,0 @@ -const AV = require('leanengine') -const _ = require('lodash') - -/* - * 从运行环境或客户端读取元信息(环境变量、用户、参数请求头) - * - * 安装依赖: - * - * npm install lodash - * - */ - -// 返回环境变量 -// **注意!** 环境变量中可能包含有有你自行添加的敏感信息(如第三方平台的密钥),因此该函数只会在开发环境下工作,请谨慎在线上应用中添加该函数。 -AV.Cloud.define('getEnvironments', async request => { - if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { - // 去除 masterKey 和 LeanCache 连接字符串等含有敏感信息的环境变量 - return _.mapValues(process.env, function(value, key) { - if (_.startsWith(key, 'REDIS_URL') || _.includes(['LC_APP_MASTER_KEY'], key)){ - return null - } else { - return value - } - }) - } -}) - -// 返回客户端的当前用户 -AV.Cloud.define('getUser', async request => { - return request.currentUser -}) - -// 返回客户端的请求参数 -AV.Cloud.define('getParams', async request => { - return request.params -}) - -// 返回客户端的额外元信息(IP 地址等) -AV.Cloud.define('getClientMeta', async request => { - return request.meta -}) - -// 返回客户端的请求头 -AV.Cloud.define('getHeaders', async request => { - // 内部接口,请勿在业务代码中使用 - return request.expressReq.headers -}) - -// 在 Hook 中获取客户端 IP -AV.Cloud.afterSave('HookObject', async request => { - console.log(request.meta.remoteAddress) -}) diff --git a/functions/pubsub.js b/functions/pubsub.js deleted file mode 100644 index ac163ac..0000000 --- a/functions/pubsub.js +++ /dev/null @@ -1,25 +0,0 @@ -const AV = require('leanengine') - -const {redisClient, createClient} = require('../redis') - -/* - * 使用 Redis Pub/Sub 收发消息 - * - * 在这个例子中,我们订阅 messages 频道,将收到的消息打印出来, - * 同时我们还提供了一个用于在 messages 频道上发消息的云函数。 - * - */ - -/* Redis 的 subscribe 是阻塞的,所以我们需要新建一个连接 */ -const redisSubscriber = createClient() - -redisSubscriber.subscribe('messages') - -/* 将订阅到的消息打印出来 */ -redisSubscriber.on('messages', (channel, message) => { - console.log('received message', channel, JSON.parse(message)) -}) - -AV.Cloud.define('publishMessage', async request => { - return redisClient.publish('messages', JSON.stringify(request.params)) -}) diff --git a/functions/queue-delay-retry.js b/functions/queue-delay-retry.js deleted file mode 100644 index 46cd0f8..0000000 --- a/functions/queue-delay-retry.js +++ /dev/null @@ -1,36 +0,0 @@ -const AV = require('leanengine') - -/* - * 云函数任务队列:延时和重试 - * - * 云函数任务队列提供了一种可靠地对云函数进行延时运行、重试、结果查询的能力。 - * 在使用 AV.Cloud.enqueue 将任务加入队列后,将由管理程序确保任务的执行,即使实例重启也没有关系。 - */ - -AV.Cloud.define('queueDelayTask', async request => { - // 延时任务,在 2 秒之后执行 - return AV.Cloud.enqueue('delayTaskFunc', {name: 'world'}, {delay: 5000}) -}) - -AV.Cloud.define('queueRetryTask', async request => { - // 重试任务,每隔 2 秒重试,最多 5 次 - return AV.Cloud.enqueue('retryTaskFunc', null, {attempts: 5, backoff: 2000}) -}) - -/* - * 以下是示例中用到的云函数 - */ - -AV.Cloud.define('delayTaskFunc', async request => { - console.log('hello', request.params.name) -}) - -AV.Cloud.define('retryTaskFunc', async request => { - const random = Math.random() - - if (random >= 0.5) { - console.log('running luckyFunc: success') - } else { - throw new AV.Cloud.Error(`failed: ${random}`) - } -}) diff --git a/functions/queue-result-query.js b/functions/queue-result-query.js deleted file mode 100644 index 815dcb1..0000000 --- a/functions/queue-result-query.js +++ /dev/null @@ -1,38 +0,0 @@ -const AV = require('leanengine') -const Promise = require('bluebird') - -/* - * 云函数任务队列:结果查询 - * - * 在调用 `Cloud.enqueue` 时会返回一个 uniqueId,云引擎或客户端可以使用这个 uniqueId 进行高性能的结果查询。 - */ - -AV.Cloud.define('createTask', async request => { - const {uniqueId} = await AV.Cloud.enqueue('longRunningTask', request.params) - return {uniqueId} -}) - -/* - * 结果类似: - * - * { - * "finishedAt": "2019-05-31T07:23:19.467Z", - * "statusCode": 200, - * "result": { - * "result": "10.22490261065305583" - * }, - * "uniqueId": "b6cd3d33-908c-4c91-8ad9-527a23ac4bf5", - * "status": "success" - * } - * - * Cloud.getTaskInfo` 其实也可以放在在客户端执行。 - */ -AV.Cloud.define('queryResult', async request => { - return await AV.Cloud.getTaskInfo(request.params.uniqueId) -}) - -/* 被执行的任务 */ -AV.Cloud.define('longRunningTask', async request => { - await Promise.delay(10000) - return (request.params.base || 0) + Math.random() -}) diff --git a/functions/readonly.js b/functions/readonly.js deleted file mode 100644 index fb47993..0000000 --- a/functions/readonly.js +++ /dev/null @@ -1,66 +0,0 @@ -const AV = require('leanengine') -const _ = require('lodash') - -/* - * 热点只读数据缓存示例 - * - * 在系统中有些数据是需要非常频繁地读取的,但这些数据量很小而且不常修改,比较适合整个放到 LeanCache 中 - * - * 在这个示例中我们以缓存一个电商网站的商品分类信息为例。 - * - * 安装依赖: - * - * npm install lodash - */ - -const {redisClient} = require('../redis') -const Category = AV.Object.extend('Category') - -/* 设置特定分类的信息,如不存在会新建,会触发 afterSave 或 afterUpdate 的 Hook */ -AV.Cloud.define('updateCategory', async request => { - try { - const category = await new AV.Query(Category).equalTo('name', request.params.name).first() - - if (category) { - return category.save(request.params) - } else { - return new Category().save(request.body) - } - } catch (err) { - if (err.code == 101) { // Class or object doesn't exists. - return new Category().save(request.body) - } else { - throw err - } - } -}) - -/* 从 Redis 中获取分类信息,不会查询云存储 */ -AV.Cloud.define('getCategories', async request => { - const categories = await redisClient.hgetall('categories') - return categories.map( category => { - return AV.parseJSON(JSON.parse(category)) - }) -}) - - -/* Redis 中的数据是通过下面三个 Class Hook 来和云存储保持同步的 */ - -AV.Cloud.afterUpdate('Category', async request => { - redisClient.hset('categories', request.object.get('name'), JSON.stringify(request.object.toFullJSON())) -}) - -AV.Cloud.afterSave('Category', async request => { - redisClient.hset('categories', request.object.get('name'), JSON.stringify(request.object.toFullJSON())) -}) - -AV.Cloud.afterDelete('Category', async request => { - redisClient.hdel('categories', request.object.get('name')) -}) - -/* 我们还可以设置一个一天的定时器,每天与云存储进行一次全量的同步,以免错过某个 Class Hook */ -AV.Cloud.define('refreshCategories', async request => { - const categories = await new AV.Query(Category).find() - const categorieSerialized = _.mapValues(_.keyBy(categories, category => category.get('name')), object => JSON.stringify(object.toFullJSON())) - await redisClient.hmset('categories', categorieSerialized) -}) diff --git a/functions/redlock.js b/functions/redlock.js deleted file mode 100644 index 6258595..0000000 --- a/functions/redlock.js +++ /dev/null @@ -1,63 +0,0 @@ -var Promise = require('bluebird'); -var AV = require('leanengine'); -var os = require('os'); -var _ = require('underscore'); - -var router = require('express').Router(); -var redisClient = require('../redis').redisClient; - -/* - * 用 LeanCache 实现分布式锁 - * - * 在这个例子中,我们有一个耗时的操作(task)需要独占一项资源(some-lock),因此我们用 SET 加上 NX 参数来原子性地获取这个资源, - * 只有当获取成功才会去执行具体的任务,保证同一时间只有一个任务在执行。 - */ - -let intervalId - -/* 设置一个定时任务,每隔半秒尝试执行 task */ -AV.Cloud.define('startTaskLoop', async request => { - if (!intervalId) { - intervalId = setInterval(runTask.bind(null, 'some-lock', task), request.params.interval || 500) - } -}) - -/* 我们可以从 Redis 中查到当前是哪个 task 在持有这个锁 */ -AV.Cloud.define('getCurrentTask', async request => { - const workerId = await redisClient.get('some-lock') - return {workerId} -}) - -/* 这里以一个随机等待几百毫秒的任务为例,会在开始和结束时打印一条日志 */ -function task(taskId) { - console.log(taskId, 'got lock') - return Promise.delay(Math.random() * 1000).then(function() { - console.log(taskId, 'release lock') - }) -} - -/* 为了保证同一时间只有一个 task 在执行,我们需要用这个函数,参数分别是锁的名字和要执行的函数(需要返回一个 Promise) */ -function runTask(lock, task) { - var taskId = [lock, os.hostname(), process.pid, _.uniqueId()].join(':') - - // NX 表示仅当不存在这个键的情况下才创建(说明我们得到了这个锁),5 是锁的超时时间(秒) - redisClient.set(lock, taskId, 'EX', 5, 'NX').then(function(result) { - if (result) { - task(taskId).finally(function() { - redisClient.del(lock).catch(function(err) { - console.error(err.stack) - }) - }) - } else { - console.log(taskId, 'fail to get lock') - } - }).catch(function(err) { - console.error(err.stack) - }) -} - -/* - * 更进一步 - * - * - 这个示例大体上遵守了 Redlock 协议,可以在 http://redis.io/topics/distlock 了解到有关 Redlock 的更多内容。 - */ diff --git a/functions/rtm-onoff-status.js b/functions/rtm-onoff-status.js deleted file mode 100644 index c981146..0000000 --- a/functions/rtm-onoff-status.js +++ /dev/null @@ -1,22 +0,0 @@ -var AV = require('leanengine') - -const {redisClient} = require('../redis') - -AV.Cloud.onIMClientOnline(async (request) => { - // 设置某一客户端 ID 对应的值为 1,表示上线状态,同时清空过期计时 - redisClient.set(redisKey(request.params.peerId), 1) -}) - -AV.Cloud.onIMClientOffline(async (request) => { - // 设置某一客户端 ID 对应的值为 0,表示下线状态,同时设置过期计时 - redisClient.set(redisKey(request.params.peerId), 0, 'EX', 604800) -}) - -AV.Cloud.define('getOnOffStatus', async (request) => { - // 约定 key: ”peerIds” 对应的值是一组客户端的 ID - return redisClient.mget(request.params.peerIds.map(redisKey)) -}) - -function redisKey(key) { - return `onOffStatus:${key}` -} diff --git a/functions/rtm-signature.js b/functions/rtm-signature.js deleted file mode 100644 index ee23196..0000000 --- a/functions/rtm-signature.js +++ /dev/null @@ -1,82 +0,0 @@ -const AV = require('leanengine') -const crypto = require('crypto') - -/* - * 使用云引擎实现即时通讯服务的签名 - * - * 使用云引擎对实时通讯服务中的操作进行鉴权,鉴权成功后向客户端下发签名, - * 这文件中的例子默认会放行所有操作,你需要自行添加拒绝操作的逻辑(抛出一个异常来拒绝此次操作)。 - * - * 关于实时通讯签名的介绍见 https://leancloud.cn/docs/realtime-guide-senior.html#hash807079806 - * 关于客户端接入签名功能(JavaScript)见 https://leancloud.cn/docs/realtime_guide-js.html#hash807079806 - * - * 还可以查看测试文件(`test/rtm-signature.js`)来了解这些云函数在客户端的用法。 - */ - -const APP_ID = process.env.LEANCLOUD_APP_ID -const MASTER_KEY = process.env.LEANCLOUD_APP_MASTER_KEY - -AV.Cloud.define('signLogin', async request => { - const {clientId} = request.params - - // 这里可以执行一些检验,例如您的用户系统里面是否有匹配这个 clientId 的用户,或者该用户存在于自定义的黑名单中, - // 你可以在此抛出异常来中断签名的过程: - // throw new AV.Cloud.Error('clientId blocked') - - return sign( (timestamp, nonce) => [APP_ID, clientId, '', timestamp, nonce]) -}) - -AV.Cloud.define('signStartConversation', async request => { - const {clientId} = request.params - const members = request.params.members || [] - - return sign( (timestamp, nonce) => [APP_ID, clientId, members.sort().join(':'), timestamp, nonce]) -}) - -AV.Cloud.define('signOperateConversation', async request => { - const {clientId, conversationId, action} = request.params - const members = request.params.members || [] - - return sign( (timestamp, nonce) => [APP_ID, clientId, conversationId, members.sort().join(':'), timestamp, nonce, action]) -}) - -AV.Cloud.define('signQueryMessage', async request => { - const {clientId, conversationId} = request.params - - return sign( (timestamp, nonce) => [APP_ID, clientId, conversationId || '', timestamp, nonce]) -}) - -AV.Cloud.define('signBlockConversation', async request => { - const {clientId, conversationId, action} = request.params - - return sign( (timestamp, nonce) => [APP_ID, clientId, conversationId || '', '', timestamp, nonce, action]) -}) - -AV.Cloud.define('signBlockClient', async request => { - const {clientId, conversationId, action} = request.params - const members = request.params.members || [] - - return sign( (timestamp, nonce) => [APP_ID, clientId, conversationId || '', members.sort().join(':'), timestamp, nonce, action]) -}) - -// func: (timestamp, nonce) -> parts -function sign(func) { - const timestamp = Math.round(Date.now() / 1000) - const nonce = getNonce(5) - const parts = func(timestamp, nonce) - const msg = parts.filter( part => part != null ).join(':') - const signature = signSha1(msg, MASTER_KEY) - return {timestamp, nonce, signature, msg} -} - -function signSha1(text, key) { - return crypto.createHmac('sha1', key).update(text).digest('hex') -} - -function getNonce(chars){ - const d = [] - for (let i = 0; i < chars; i++) { - d.push(Math.round(Math.random() * 10)) - } - return d.join('') -} diff --git a/functions/todos.js b/functions/todos.js deleted file mode 100644 index 9c12d58..0000000 --- a/functions/todos.js +++ /dev/null @@ -1,67 +0,0 @@ -const AV = require('leanengine') - -/* - * 在云引擎中已客户端的权限来操作云存储 - * - * 安装依赖: - * - * npm install lodash bluebird - * - */ - -const Todo = AV.Object.extend('Todo') - -// 获取所有 Todo 列表 -AV.Cloud.define('getAllTodos', async request => { - const query = new AV.Query(Todo) - query.equalTo('status', request.params.status || 0) - query.include('author') - query.descending('updatedAt') - query.limit(50) - - try { - return await query.find({ - // 使用客户端发来的 sessionToken 进行查询 - sessionToken: request.sessionToken - }) - } catch (err) { - if (err.code === 101) { - // 该错误的信息为:{ code: 101, message: 'Class or object doesn\'t exists.' },说明 Todo 数据表还未创建,所以返回空的 Todo 列表。 - // 具体的错误代码详见:https://leancloud.cn/docs/error_code.html - return [] - } else { - throw err - } - } -}) - -// 创建新的 Todo -AV.Cloud.define('createTodo', async request => { - const todo = new Todo() - - todo.set('content', request.params.content) - todo.set('status', 0) - - if (request.currentUser) { - // 如果客户端已登录(发送了 sessionToken),将 Todo 的作者设置为登录用户 - todo.set('author', request.currentUser) - // 设置 ACL,可以使该 todo 只允许创建者修改,其他人只读 - const acl = new AV.ACL(request.currentUser) - acl.setPublicWriteAccess(false) - todo.setACL(acl) - } - - return todo.save(null, {sessionToken: request.sessionToken}) -}) - -// 删除指定 Todo -AV.Cloud.define('deleteTodo', async request => { - const todo = AV.Object.createWithoutData('Todo', request.params.id) - todo.destroy({sessionToken: request.sessionToken}) -}) - -// 将 Todo 标记为已完成 -AV.Cloud.define('setTodoToDone', async request => { - const todo = AV.Object.createWithoutData('Todo', request.params.id) - todo.save({status: 1}, {sessionToken: request.sessionToken}) -}) diff --git a/functions/weapp-decrypt.js b/functions/weapp-decrypt.js deleted file mode 100644 index 147396b..0000000 --- a/functions/weapp-decrypt.js +++ /dev/null @@ -1,40 +0,0 @@ -const AV = require('leanengine') -const crypto = require('crypto') - -/* - * 解密微信小程序用户加密数据 - * - * 该云函数会使用云存储用户信息中的 sessionKey(`authData.lc_weapp.session_key`,需要用 `AV.User.loginWithWeapp` 登录后才会有) - * 来对 `wx.getUserInfo` 中的 encryptedData 进行解密,返回解密后的完整信息。 - * - * 设置环境变量: - * - * env WEAPP_APPID # 微信小程序 App ID(选填,会检查数据是否属于当前小程序) - * - */ - -/* 参数:encryptedData、iv */ -AV.Cloud.define('decryptWeappData', async request => { - const {currentUser} = request - - if (currentUser && currentUser.get('authData') && currentUser.get('authData').lc_weapp) { - const encryptedData = Buffer.from(request.params.encryptedData, 'base64') - const iv = Buffer.from(request.params.iv, 'base64') - const sessionKey = Buffer.from(currentUser.get('authData').lc_weapp.session_key, 'base64') - - const decipher = crypto.createDecipheriv('aes-128-cbc', sessionKey, iv) - - decipher.setAutoPadding(true) - - const decrypted = decipher.update(encryptedData, 'binary', 'utf8') + decipher.final('utf8') - const parsed = JSON.parse(decrypted) - - if (process.env.WEAPP_APPID && parsed.watermark.appid !== process.env.WEAPP_APPID) { - throw new AV.Cloud.Error('加密数据不属于该小程序应用') - } - - return parsed - } else { - throw new AV.Cloud.Error('用户未登录或未关联微信小程序', {status: 401}) - } -}) diff --git a/functions/xml.js b/functions/xml.js deleted file mode 100644 index 90db265..0000000 --- a/functions/xml.js +++ /dev/null @@ -1,29 +0,0 @@ -const AV = require('leanengine') -const xml2js = require('xml2js') - -/* - * 使用云函数序列化 XML 对象 - * - * 安装依赖: - * - * npm install xml2js - * - */ - -AV.Cloud.define('xmlBuildObject', async request => { - const builder = new xml2js.Builder() - - const data = { - xml: { - ToUserName: 'leancloud', - FromUserName: 'guest', - CreateTime: 1462767983071, - MsgType: 'text', - Content: '谢谢你,第44位点赞者!' - } - } - - return { - xml: builder.buildObject(data) - } -}) diff --git a/leanengine.yaml b/leanengine.yaml index e689b56..2c17431 100644 --- a/leanengine.yaml +++ b/leanengine.yaml @@ -1,3 +1,3 @@ systemDependencies: - - imagemagick - - ffmpeg + - chrome-headless + - fonts-wqy diff --git a/nodemon.json b/nodemon.json deleted file mode 100644 index d264715..0000000 --- a/nodemon.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "ignore": ["views/*", "public/*"] -} diff --git a/package.json b/package.json index 1c02487..5003faf 100644 --- a/package.json +++ b/package.json @@ -1,48 +1,8 @@ { - "name": "leanengine-nodejs-demos", - "version": "1.0.0", - "private": true, - "scripts": { - "start": "node server.js", - "test": "mocha" + "engines": { + "node": "8" }, "dependencies": { - "bluebird": "^3.5.1", - "body-parser": "^1.12.2", - "captchapng": "0.0.1", - "cheerio": "^1.0.0-rc.2", - "connect-timeout": "^1.9.0", - "cookie-parser": "^1.4.4", - "debug": "^4.1.1", - "ejs": "^2.5.7", - "express": "^4.12.3", - "express-ws": "^4.0.0", - "fluent-ffmpeg": "^2.1.2", - "gm": "^1.23.1", - "ioredis": "^4.9.0", - "jade": "^1.9.2", - "leancloud-storage": "^3.0.0", - "leanengine": "^3.3.3", - "lodash": "^4.17.11", - "marked": "^0.6.2", - "measured": "^1.1.0", - "moment": "^2.13.0", - "nanoid": "^3.1.10", - "promise-memoize": "^1.2.1", - "promise-queue": "^2.2.5", - "request": "^2.83.0", - "request-promise": "^4.2.2", - "underscore": "^1.8.3", - "wechat": "^2.1.0", - "xml2js": "^0.4.19" - }, - "devDependencies": { - "nodemon": "^1.11.0", - "should": "^13.2.3", - "supertest": "^4.0.2", - "supertest-as-promised": "^4.0.2" - }, - "engines": { - "node": "10.x" + "puppeteer": "^1.2.0" } } diff --git a/public/leanstorage.png b/public/leanstorage.png deleted file mode 100644 index f1f364c..0000000 Binary files a/public/leanstorage.png and /dev/null differ diff --git a/redis.js b/redis.js deleted file mode 100644 index 6f99fb9..0000000 --- a/redis.js +++ /dev/null @@ -1,18 +0,0 @@ -const Redis = require('ioredis') - -function createClient() { - // 本地环境下此环境变量为 undefined, 会链接到默认的 127.0.0.1:6379, - // 你需要将 `demos` 修改为你的 LeanCache 实例名称 - const redisClient = new Redis(process.env['REDIS_URL_demos']) - - redisClient.on('error', function(err) { - console.error('redisClient error', err) - }) - - return redisClient -} - -module.exports = { - redisClient: createClient(), - createClient: createClient -} diff --git a/routes/cookie-session.js b/routes/cookie-session.js deleted file mode 100644 index 9e46a82..0000000 --- a/routes/cookie-session.js +++ /dev/null @@ -1,53 +0,0 @@ -const {Router} = require('express') -const AV = require('leanengine') - -const router = module.exports = new Router - -/* - * 使用 Cookie Session 示例 - * - * 注意检查 app.js 中需要有 `app.use(AV.Cloud.CookieSession({ secret: 'randomString', maxAge: 3600000, fetchUser: true }))` - * - */ - -/* 对于已登录的用户会返回用户信息,未登录的用户会返回空 */ -router.get('/', (req, res) => { - res.json(req.currentUser) -}) - -/* 注册用户并自动登录 */ -router.post('/register', async (req, res, next) => { - const {username, password} = req.body - - const user = new AV.User() - - user.set('username', username) - user.set('password', password) - - try { - await user.signUp() - res.saveCurrentUser(user) - res.json(user) - } catch (err) { - next(err) - } -}) - -/* 登录用户 */ -router.post('/login', async (req, res, next) => { - const {username, password} = req.body - - try { - const user = await AV.User.logIn(username, password) - res.saveCurrentUser(user) - res.json(user) - } catch (err) { - next(err) - } -}) - -/* 登出用户 */ -router.post('/logout', (req, res) => { - res.clearCurrentUser() - res.sendStatus(204) -}) diff --git a/routes/markdown.js b/routes/markdown.js deleted file mode 100644 index af8608e..0000000 --- a/routes/markdown.js +++ /dev/null @@ -1,22 +0,0 @@ -const {Router} = require('express') -const marked = require('marked') -const fs = require('fs').promises - -const router = module.exports = new Router - -/* - * 项目主页,将 README.md 渲染成 HTML 显示在页面上 - * - * 安装依赖: - * - * npm install marked - * - */ - -router.get('/', async (req, res, next) => { - try { - res.send(marked(await fs.readFile('README.md', 'utf8'))) - } catch (err) { - next(err) - } -}) diff --git a/routes/render-ejs.js b/routes/render-ejs.js deleted file mode 100644 index 4b66763..0000000 --- a/routes/render-ejs.js +++ /dev/null @@ -1,29 +0,0 @@ -const {Router} = require('express') -const AV = require('leanengine') - -const router = module.exports = new Router - -/* - * 使用 EJS 渲染 HTML 页面 - * - * 安装依赖: - * - * npm install ejs - * - * 注意检查 app.js 中需要有: - * - * app.set('views', path.join(__dirname, 'views')) - * app.set('view engine', 'ejs') - * - */ - -router.get('/', async (req, res) => { - const todos = await new AV.Query('Todo').include('user').descending('updatedAt').find() - - console.log(todos) - - res.render('todos', { - title: 'TODO 列表', - todos: todos - }) -}) diff --git a/routes/websocket.js b/routes/websocket.js deleted file mode 100644 index 248d372..0000000 --- a/routes/websocket.js +++ /dev/null @@ -1,42 +0,0 @@ -const {Router} = require('express') - -const router = module.exports = new Router - -/* - * WebSocket 示例 - * - * 注意检查 app.js 中需要有 `require('express-ws')(app)` - * - * 可以使用 wscat 或 websocat 来对 WebSocket API 进行测试 - * https://github.com/websockets/wscat - * https://github.com/vi/websocat - * - * 安装依赖: - * - * npm install express-ws - * - */ - -/* - * 将客户端发来的消息原样发回客户端 - * wscat -c ws://localhost:3000/websocket/echo -*/ -router.ws('/echo', (ws, req) => { - ws.on('message', (msg) => { - ws.send(msg) - }) -}) - -/* - * 每隔一秒向客户端发送一条消息 - * wscat -c ws://localhost:3000/websocket/timer -*/ -router.ws('/timer', (ws, req) => { - const intervalId = setInterval( () => { - ws.send('Hello') - }, 1000) - - ws.on('close', (msg) => { - clearInterval(intervalId) - }) -}) diff --git a/routes/wechat-message-callback.js b/routes/wechat-message-callback.js deleted file mode 100644 index b074215..0000000 --- a/routes/wechat-message-callback.js +++ /dev/null @@ -1,94 +0,0 @@ -const {Router} = require('express') -const wechat = require('wechat') - -/* - * 接受并自动回复微信公众平台的用户消息回调 - * - * 安装依赖: - * - * npm install wechat - * - * 设置环境变量: - * - * env WECHAT_APPID # 微信公众平台应用 ID(必填) - * env WECHAT_TOKEN # 微信公众平台 Key(必填) - * env encodingAESKey # 微信公众平台 AES 密钥(必填) - * - */ - -const wechatConfig = { - token: process.env.WECHAT_TOKEN, - appid: process.env.WECHAT_APPID, - encodingAESKey: process.env.WECHAT_ENCODING_AES_KEY -} - -const router = module.exports = new Router - -router.use('/', wechat(wechatConfig) - .text( (message, req, res, _next) => { - if (message.Content === '你好') { - res.reply({ - type: 'text', - content: '你好!' - }) - } else {r - res.reply({ - type: 'text', - content: '抱歉,请对我说「你好」' - }) - } - }) - .image( (message, req, res, _next) => { - res.reply({ - type: 'text', - content: JSON.stringify(message) - }) - }) - .voice( (message, req, res, _next) => { - res.reply({ - type: 'text', - content: JSON.stringify(message) - }) - }) - .video( (message, req, res, _next) => { - res.reply({ - type: 'text', - content: JSON.stringify(message) - }) - }) - .shortvideo( (message, req, res, _next) => { - res.reply({ - type: 'text', - content: JSON.stringify(message) - }) - }) - .location( (message, req, res, _next) => { - res.reply({ - type: 'text', - content: JSON.stringify(message) - }) - }) - .link( (message, req, res, _next) => { - res.reply({ - type: 'text', - content: JSON.stringify(message) - }) - }) - .event( (message, req, res, _next) => { - res.reply({ - type: 'text', - content: JSON.stringify(message) - }) - }).device_text( (message, req, res, _next) => { - res.reply({ - type: 'text', - content: JSON.stringify(message) - }) - }) - .device_event( (message, req, res, _next) => { - res.reply({ - type: 'text', - content: JSON.stringify(message) - }) - }) - .middlewarify()) diff --git a/server.js b/server.js index 335d15a..7976075 100644 --- a/server.js +++ b/server.js @@ -1,26 +1,58 @@ -const AV = require('leanengine') +var fs = require('fs'); +var url = require('url'); +const puppeteer = require('puppeteer'); -AV.init({ - appId: process.env.LEANCLOUD_APP_ID, - appKey: process.env.LEANCLOUD_APP_KEY, - masterKey: process.env.LEANCLOUD_APP_MASTER_KEY -}) +require('http').createServer(function(req, res) { + const urlInfo = url.parse(req.url, true) -const app = require('./app') + if (urlInfo.pathname !== '/') { + res.statusCode = 404; + return res.end(); + } -// 端口一定要从环境变量 `LEANCLOUD_APP_PORT` 中获取。 -// LeanEngine 运行时会分配端口并赋值到该变量。 -const PORT = parseInt(process.env.LEANCLOUD_APP_PORT || process.env.PORT || 3000) + if (urlInfo.query.url) { + makeScreenshot(urlInfo.query.url).then( (filename) => { + fs.readFile(filename, function(err, buffer) { + if (err) { + res.end(err.message); + } else { + res.setHeader('Content-Type', 'image/png'); + res.end(buffer); + } + }); + }).catch( err => { + console.log(err) + res.end(err.message); + }); + } else { + res.end('You can visit https://snapcat.leanapp.cn/?url=https://leancloud.cn/docs'); + } +}).listen(3000); -app.listen(PORT, function (err) { - console.log('Node app is running on port:', PORT) +var counter = 0; - // 注册全局未捕获异常处理器 - process.on('uncaughtException', function(err) { - console.error('Caught exception:', err.stack) - }) +async function makeScreenshot(url) { + const filename = `./${counter++}.png`; - process.on('unhandledRejection', function(reason, p) { - console.error('Unhandled Rejection at: Promise ', p, ' reason: ', reason.stack) - }) -}) + const browser = await puppeteer.launch({ + executablePath: '/usr/bin/google-chrome', + args: ['--no-sandbox', '--disable-setuid-sandbox'] + }); + + const page = await browser.newPage(); + await page.goto(url); + + await page.setViewport({ + width: 1440, + height: 900 + }); + + await page.screenshot({ + fullPage: true, + path: filename + }); + + await browser.close(); + + return filename; +} diff --git a/test/amr-transcoding.js b/test/amr-transcoding.js deleted file mode 100644 index 5dc7d90..0000000 --- a/test/amr-transcoding.js +++ /dev/null @@ -1,13 +0,0 @@ -const AV = require('leanengine') - -require('../server') - -describe('amr-transcoding', () => { - it('amrToMp3', async () => { - const amrFile = await new AV.Query('_File').get('5d9d8c087b968a008bb1a12c') - - const result = await AV.Cloud.run('amrToMp3', {file: amrFile}) - - result.get('name').should.be.equal('test.mp3') - }) -}) diff --git a/test/captcha-storage.js b/test/captcha-storage.js deleted file mode 100644 index 179678a..0000000 --- a/test/captcha-storage.js +++ /dev/null @@ -1,93 +0,0 @@ -const AV = require('leanengine') -const Promise = require('bluebird') - -require('../server') - -describe('captcha-storage', () => { - let captchaId - const mobilePhoneNumber = '18888888888' - - describe('getCaptchaImageStorage', () => { - it('response have captchaId and imageUrl', async () => { - const result = await AV.Cloud.run('getCaptchaImageStorage') - - result.should.have.properties(['captchaId', 'imageUrl']) - - captchaId = result.captchaId - - await new AV.Query('Captcha').find().then( captchas => { - // 因为设置了 ACL,所以非特殊账号无法查询到 captcha 对象 - captchas.length.should.equal(0) - }) - }) - }) - - describe('requestMobilePhoneVerifyStorage', () => { - it('captcha id mismatch', async () => { - try { - await AV.Cloud.run('requestMobilePhoneVerifyStorage', { - captchaId: 'noThisId', - captchaCode: '0000', - mobilePhoneNumber - }) - - throw new Error('should throw') - } catch (err) { - err.status.should.be.equal(401) - } - }) - - it('captcha code mismatch', async () => { - try { - await AV.Cloud.run('requestMobilePhoneVerifyStorage', { - captchaId, - captchaCode: '0000', - mobilePhoneNumber - }) - - throw new Error('should throw') - } catch (err) { - err.status.should.be.equal(401) - } - }) - - it('captcha code timeout', async () => { - // 将超时时间设置为 1 毫秒,强制过期 - process.env.CAPTCHA_TTL = '1' - - await Promise.delay(1500) - - const captchaObj = await new AV.Query('Captcha').get(captchaId, {useMasterKey: true}) - - try { - await AV.Cloud.run('requestMobilePhoneVerifyStorage', { - captchaId, - captchaCode: '' + captchaObj.get('code'), - mobilePhoneNumber - }) - - throw new Error('should throw') - } catch (err) { - err.status.should.be.equal(401) - } - }) - - it('ok', async () => { - delete process.env.CAPTCHA_TTL - - const captchaObj = await new AV.Query('Captcha').get(captchaId, {useMasterKey: true}) - - try { - await AV.Cloud.run('requestMobilePhoneVerifyStorage', { - captchaId, - captchaCode: '' + captchaObj.get('code'), - mobilePhoneNumber - }) - - throw new Error('should throw') - } catch (err) { - err.message.should.match(/phone number was not found/) - } - }) - }) -}) diff --git a/test/imagemagick.js b/test/imagemagick.js deleted file mode 100644 index 629a4f4..0000000 --- a/test/imagemagick.js +++ /dev/null @@ -1,11 +0,0 @@ -const AV = require('leanengine') - -require('../server') - -describe('imagemagick', () => { - it('imageMagicResize', async () => { - const result = await AV.Cloud.run('imageMagicResize') - - result.should.have.properties(['imageUrl']) - }) -}) diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index 0b219bc..0000000 --- a/test/mocha.opts +++ /dev/null @@ -1 +0,0 @@ --r should diff --git a/test/rtm-signature.js b/test/rtm-signature.js deleted file mode 100644 index 495181a..0000000 --- a/test/rtm-signature.js +++ /dev/null @@ -1,70 +0,0 @@ -const AV = require('leanengine') -const {Realtime} = require('leancloud-realtime') - -require('../server') - -describe('rtm-signature', () => { - let client, conversation - - const realtime = new Realtime({ - appId: process.env.LEANCLOUD_APP_ID, - appKey: process.env.LEANCLOUD_APP_KEY - }) - - const signLogin = clientId => { - return AV.Cloud.run('signLogin', {clientId}) - } - - const signStartConversation = async (conversationId, clientId, members, action) => { - if (action === 'create') { - return AV.Cloud.run('signStartConversation', { - conversationId, clientId, members, action - }) - } else { - const actionMapping = { - add: 'invite', - remove: 'kick' - } - - return AV.Cloud.run('signOperateConversation', { - conversationId, clientId, members, - action: actionMapping[action] - }) - } - } - - after( () => { - if (client) { - return client.close() - } - }) - - it('should failed without signature', async () => { - try { - await realtime.createIMClient('should-failed') - } catch (err) { - err.message.should.be.equal('SIGNATURE_FAILED') - return - } - - throw new Error('should failed without signature') - }) - - it('login with signature', async () => { - client = await realtime.createIMClient('signature-test', { - signatureFactory: signLogin, - conversationSignatureFactory: signStartConversation - }) - }) - - it('start conversation with signature', async () => { - conversation = await client.createConversation({ - members: ['signature-test'], - transient: true - }) - }) - - it('invite to conversation with signature', async () => { - await conversation.add(['Tom']) - }) -}) diff --git a/test/weapp-decrypt.js b/test/weapp-decrypt.js deleted file mode 100644 index 0a0a44e..0000000 --- a/test/weapp-decrypt.js +++ /dev/null @@ -1,17 +0,0 @@ -const AV = require('leanengine') - -require('../server') - -describe('weapp-decrypt', () => { - it('should success', async () => { - // appId uhx9ou96070bts3emcu6vyaxngnybkq07s6smzws3xp0ej4c - // userId 587207a91b69e6005ca6b05b - - const result = await AV.Cloud.run('decryptWeappData', { - encryptedData: 'dMZcop0wE2EONNFpSOWNKEjfc1LtABBG2I5Fno83Zt/jcIgbbrQzOWjmv9z+yZVmZi8YZntQ8CemE6jjsh/BIJa020IJ/afJtqc0lcrdPQ9YD/Bb176qrdZSajiM7lNtR33avYknP0zQ1APtNfDyiKehilTihfWYMUUcnKyaSihoye868MuOHa8SJEHvXxpeicq1j1op39nQEpX/9NnMjmWOgqmL1uvYyRgHOG7Kgs7D5mhpDs58q/fWGLav6d22WIQEcJmEDvKwe39CV6/9O1fyoNiHUGTUYbg5aarsM/4sG/bMD/Tw+YoIAC0n/xSYFH6Kk/jw3vubsW/AtBPeh3pQgl3gXo2ClxYc3OnJEx60kbzc9kw736NAQBxTurR0EAmHHKZagxPaaqGpFCJUwrHaEWIeiuVac3YejJ4TcxyFVWjKFsXWciNWWzXzXlsAZ8nTJJ7zQfnfH3QKnXRmMxA/7Rz04qtG52KT6u8thoYHmC2RHNWSb9h4EH8OdvDPD86ivTH6gnTfuK3HrDugOw==', - iv: '3J0zu/VXwbZsaoQiBS8pkA==' - }, {sessionToken: 'lfhz8hyibkzv5ujeiz4mrm6na'}) - - result.city.should.be.equal('Suzhou') - }) -}) diff --git a/views/todos.ejs b/views/todos.ejs deleted file mode 100644 index c3a1e9a..0000000 --- a/views/todos.ejs +++ /dev/null @@ -1,29 +0,0 @@ - - - - <%= title %> - - - - - - <%= title %> - - - - - 描述 - 所有者 - - - <% todos.forEach( todo => { %> - - <%= todo.get('content') %> - <%= todo.get('user') == null ? '' : todo.get('user').get('username') %> - - <% }) %> - - - - -