- 关于云开发数据库的使用经验和建议
作者:布道师 shaohui_xia 小程序·云开发是微信团队联合腾讯云推出的专业的小程序开发服务。 开发者可以使用云开发快速开发小程序、小游戏、公众号网页等,并且原生打通微信开放能力。 开发者无需搭建服务器,可免鉴权直接使用平台提供的 API 进行业务开发。 数据库的上手、初始化等可参看官方链接:小程序·云开发 二、使用经验直接使用云开发API场景:页面或方法的逻辑简单,关联一个数据库,无联表查询 例子: db.collection('todos').doc('todo-identifiant-aleatoire').get({ success: function(res) { // res.data 包含该记录的数据 console.log(res.data) } }) 使用数据聚合能力场景:页面或方法的逻辑中等,关联多个数据库,可能存在联表查询或数据处理 例子: const db = wx.cloud.database() const $ = db.command.aggregate db.collection('books').aggregate() .group({ // 按 category 字段分组 _id: '$category', // 让输出的每组记录有一个 avgSales 字段,其值是组内所有记录的 sales 字段的平均值 avgSales: $.avg('$sales') }) .end() 借助promise,async等场景:页面或方法的逻辑较为复杂,关联多个数据库,可能存在多次查询以及云函数或https请求 以下是对云开发CMS导出数据的扩展案例 其中整合了上述的几种方式 例子: const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) var xlsx = require('node-xlsx'); const db = cloud.database(); const MAX_LIMIT = 100; const _ = db.command; exports.main = async (event, context) => { console.log(event) event.queryStringParameters = event.queryStringParameters||{}; const collection = event.collection || event.queryStringParameters.collection; const params = event.params || event.queryStringParameters.params || {}; // const acceptType = ["String", "Tel", "Array", "Number", "Connect", "Boolean", "Enum", "Date", "DateTime"]; //"File","Image" const unacceptType = ["File", "Image"]; const schemasRes = await db.collection("tcb-ext-cms-schemas").where({ collectionName: collection }).get(); const schemas = schemasRes.data[0]; let connectList = []; const title = event.title || event.queryStringParameters.title || schemas.displayName || "数据"; // 先取出集合记录总数 const countRes = await db.collection(collection).where(params).count(); const fields = schemas.fields.filter(function (schemas) { return unacceptType.indexOf(schemas.type) == -1 && (!schemas.isHidden); }); const connectResourcenList = []; fields.forEach(field => { if (field.type == "Connect") { connectList.push(field); connectResourcenList.push(field.connectResource) } }); const schemasListRes = await db.collection("tcb-ext-cms-schemas").where({ _id: _.in(connectResourcenList) }).limit(MAX_LIMIT).get(); const schemasList = schemasListRes.data || []; // console.log("fields==============================") console.log(schemasList) const total = countRes.total // 计算需分几次取 const batchTimes = Math.ceil(total / MAX_LIMIT) // 承载所有读操作的 promise 的数组 const tasks = [] for (let i = 0; i < batchTimes; i++) { //console.log(connectList.length) if (connectList.length > 0) { let lookupList = []; connectList.forEach(connect => { const connectschemas = schemasList.filter(function (schemas) { return schemas._id == connect.connectResource; })[0]; lookupList.push({ from: connectschemas.collectionName, localField: connect.name, foreignField: '_id', as: "connect" + connect.name }) }); let aggregate = db.collection(collection).aggregate().match(params).skip(i * MAX_LIMIT).limit(MAX_LIMIT); for (let index = 0; index < connectList.length; index++) { aggregate = aggregate.lookup(lookupList[index]); } aggregate = aggregate.end(); tasks.push(aggregate) } else { const promise = db.collection(collection).where(params).skip(i * MAX_LIMIT).limit(MAX_LIMIT).get(); tasks.push(promise) } } console.log(tasks) // 等待所有 let recordRes = (await Promise.all(tasks)).reduce((acc, cur) => { return { list: (acc.list || []).concat(cur.list || []), data: (acc.data || []).concat(cur.data || []), } }) let records = (recordRes.list || []).concat(recordRes.data || []) || []; //1.定义表格名 let dataCVS = title + '.xlsx'; let excelData = []; let row = []; fields.forEach(field => { row.push(field.displayName) }); excelData.push(row); records.forEach(record => { let arr = []; fields.forEach(field => { if (!record.hasOwnProperty(field.name)) { arr.push("") } else { switch (field.type) { case "Connect": arr.push(join2Str(record["connect" + field.name], field.connectField)) break; case "DateTime": arr.push(formatDateTime(record[field.name])) break; case "Date": arr.push(formatDate(record[field.name])) break; case "Boolean": arr.push(record[field.name] ? "是" : "否") break; case "Enum": let enumElements = field.enumElements; let enumElement= enumElements.find(function(item){ return item.value = record[field.name]; }) arr.push(enumElement.label) break; default: arr.push(record[field.name]) break; } } }); excelData.push(arr); }); //3,把数据保存到excel里 var buffer = await xlsx.build([{ name: title, data: excelData }]); //4,把excel文件保存到云存储里 const excelFileIdRes = await cloud.uploadFile({ cloudPath: dataCVS, fileContent: buffer, //excel二进制文件 }); return await cloud.getTempFileURL({ fileList: [excelFileIdRes.fileID] }).then(function (res) { return res.fileList[0].tempFileURL }) } function join2Str(obj, fieldName) { if (Object.prototype.toString.call(obj) == "[object Array]") { let resultArr = []; obj.forEach(item => { if (item.hasOwnProperty(fieldName)) resultArr.push(item[fieldName]) }); return resultArr.join(",") } else { if (obj.hasOwnProperty(fieldName)) return obj[fieldName] } } function formatDateTime(inputTime) { var date = new Date(inputTime); var y = date.getFullYear(); var m = date.getMonth() + 1; m = m < 10 ? ('0' + m) : m; var d = date.getDate(); d = d < 10 ? ('0' + d) : d; var h = date.getHours(); h = h < 10 ? ('0' + h) : h; var minute = date.getMinutes(); var second = date.getSeconds(); minute = minute < 10 ? ('0' + minute) : minute; second = second < 10 ? ('0' + second) : second; return y + '-' + m + '-' + d + ' ' + h + ':' + minute + ':' + second; }; function formatDate(inputTime) { var date = new Date(inputTime); var y = date.getFullYear(); var m = date.getMonth() + 1; m = m < 10 ? ('0' + m) : m; var d = date.getDate(); d = d < 10 ? ('0' + d) : d; return y + '-' + m + '-' + d; }; 整合数据库框架场景:小程序或APP的业务逻辑复杂,模板页面的开发,组件的开发和统一异常处理 例子: 以下例子引用了wxboot的小程序框架 //app.js // const {WXBoot} = require('wxbootstart'); require('./lib-webpack/wxboot'); import login from "./login/login" import utils from "./utils/utils" import constants from "./constants/constants" App.A({ config: { initCloud:{ // env: '', traceUser: true,}, route: '/pages/$page/$page', pageApi: utils, consts: constants, updata:{ arrObjPath:false, arrCover:false }, mixins:[login,App.A.Options] , }, getOpenidFunc: function(){ return this.cloud.callFunction({ name:"getWXContext" }).then(res=>{ return res.result.openid; }).catch(err=>{ console.error(err) return "" }) }, onLaunch: function (opts) { App.A.on('some_message', function (msg) { console.log('Receive message:', msg) }) console.log('APP is Running', opts) }, store: { id: 0 }, auth:{ canUseXXX:false }, globalData: { version: "v1.0.0", id: 0, userInfo: null, addressInfo: null, sessionKey: null, loginTime: 0, openid: "", theme: { color: "#FFFFFF" }, share: { title: "开启一天好运", imageUrl: "https://XXX.jpg", path: "/pages/index/index" }, settings: null }, onAwake: function (time) { console.log('onAwake, after', time, 'ms') }, onShow: function () { console.log('App onShow') }, /*小程序主动更新 */ updateManager() { if (!wx.canIUse('getUpdateManager')) { return false; } const updateManager = wx.getUpdateManager(); updateManager.onCheckForUpdate(function (res) {}); updateManager.onUpdateReady(function () { wx.showModal({ title: '有新版本', content: '新版本已经准备好,即将重启', showCancel: false, success(res) { if (res.confirm) { updateManager.applyUpdate() } } }); }); updateManager.onUpdateFailed(function () { wx.showModal({ title: '更新提示', content: '新版本下载失败', showCancel: false }) }); }, "navigateToMiniProgramAppIdList": [ "wx8abaf00ee8c3202e" ] }) 全局封装增删改 ,我们更专注的关注于业务逻辑,统一异常处理 module.exports = { $callFun: callFunction, $add: add, $get: get, $update: update, $remove: remove, $count:count } //取数据库实例。一个数据库对应一个实例 /** * 封装查询操作 * 增 查 改 删 * */ //增 async function add(collectionName, data, openParse = false) { if (openParse) { data = await parseQuery(data, this) } return this.$collection(collectionName).add({ data }).then(res => { return res._id }).catch(res => { return "" }) } //查询 //对应id取不到的时候,返回{} async function get(collectionName, query, openParse = false) { switch (type(query)) { case "string": return this.$collection(collectionName).doc(query).get().then(res => { return res.data }).catch(res => { console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`) return {} }) case "object": const defaultOptions = { where: null, order: null, skip: 0, limit: 20, field: null, pageIndex: 1 } const parsequery = setDefaultOptions(query, defaultOptions); let { where, order, skip, limit, field, pageIndex } = parsequery; let collectionGet = this.$collection(collectionName); if (where != null) { if (openParse) { where = await parseQuery(where, this) } collectionGet = collectionGet.where(where) } if (order != null) { if (type(order) == "object") { collectionGet = collectionGet.orderBy(order.name, order.value); } if (type(order) == "array") { order.forEach(orderItem => { collectionGet = collectionGet.orderBy(orderItem.name, orderItem.value); }); } } if (field) { collectionGet = collectionGet.field(field); } if (pageIndex > 1) { collectionGet = collectionGet.skip((pageIndex - 1) * limit).limit(limit); } else { collectionGet = collectionGet.skip(skip).limit(limit); } return collectionGet.get().then(res => { return res.data }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return [] }) default: console.warn(`"query":参数类型错误不存在`) return null; } } async function count(collectionName, query, openParse = false) { switch (type(query)) { case "object": let collectionUpdate = this.$collection(collectionName); if (openParse) { query = await parseQuery(query, this) } collectionUpdate = collectionUpdate.where(query) return collectionUpdate.count().then(res => { return res.total }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return 0 }) default: return this.$collection(collectionName).count().then(res => { return res.total }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return 0 }) } } //修改 async function update(collectionName, query, updata, openParse = false) { switch (type(query)) { case "string": return this.$collection(collectionName).doc(query).update({ data: updata }).then(res => { return res.stats.updated }).catch(res => { console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`) return 0 }) case "object": let collectionUpdate = this.$collection(collectionName); if (openParse) { query = await parseQuery(query, this) } collectionUpdate = collectionUpdate.where(query) return collectionUpdate.update({ data: updata }).then(res => { return res.stats.updated }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return 0 }) default: console.warn(`"query":参数类型错误不存在`) return 0 } } //删除 async function remove(collectionName, query, openParse=false) { switch (type(query)) { case "string": return this.$collection(collectionName).doc(query).remove().then(res => { return res }).catch(res => { console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`) return {} }) case "object": let collectionRemove = this.$collection(collectionName); if (openParse) { query = await parseQuery(query, this) } collectionRemove = collectionRemove.where(query) return collectionRemove.remove().then(res => { return res }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return [] }) default: console.warn(`"query":参数类型错误不存在`) return 0 } } function setDefaultOptions(options = {}, defaultOptions = {}) { return Object.assign(defaultOptions, options); } function promisify(api) { return (options, ...query) => { return new Promise((resolve, reject) => { api(Object.assign({}, options, { success: resolve, fail: reject }), ...query); }) } } async function callFunction(options) { return await this.cloud.callFunction(options) } var undef = void(0) function type(obj) { if (obj === null) return 'null' else if (obj === undef) return 'undefined' var m = /\[object (\w+)\]/.exec(Object.prototype.toString.call(obj)) return m ? m[1].toLowerCase() : '' } async function parseQuery(query, self) { let queryStr = JSON.stringify(query); if (queryStr.indexOf("{openid}") > -1) { let openid = await self.$getOpenid(); return JSON.parse(queryStr.replace(/{openid}/g, openid)); } else { return query } } 高级用法,结合云函数和https 以及封装api ,实现统一对外接口,对接其他语言场景:多项目,多后台,多端打通,数据迁移等 // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db = cloud.database(); // 云函数入口函数 exports.main = async (event, context) => { let body = event.body; let cloudParams = urlToObj(decodeURIComponent(body)); let { cloudType, collectionName } = cloudParams; let data = JSON.parse(cloudParams.data || "{}"); let query = JSON.parse(cloudParams.query || "{}"); if(type(query)=="object"){ query.where = JSON.parse(query.where ||"{}" ); if(query.field) query.field = JSON.parse(query.field ||"{}" ); } console.log(query) let promise = null; switch (cloudType) { case "ADD": promise = add(collectionName, data); break; case "GET": promise = get(collectionName, query) break; case "UPDATE": promise = update(collectionName, query, data) break; case "REMOVE": promise = remove(collectionName, query) break; case "COUNT": let countquery = null; if (type(query) == "string") { countquery = query } else { countquery = query.where || null } promise = count(collectionName, countquery) break; default: break; } return promise; } function urlToObj(str) { var obj = {}; var arr2 = str.split("&"); for (var i = 0; i < arr2.length; i++) { var res = arr2[i].split("="); obj[res[0]] = res[1] || ""; } return obj; } //增 async function add(collectionName, data, openParse = false) { if (openParse) { data = await parseQuery(data) } return db.collection(collectionName).add({ data }).then(res => { return res._ids || res._id; }).catch(res => { return "" }) } //查询 //对应id取不到的时候,返回{} async function get(collectionName, query, openParse = false) { if (query.limit && query.limit == "all") { let countquery = null; if (type(query) == "string") { countquery = query } else { countquery = query.where || null } // 先取出集合记录总数 const total = await count(collectionName, countquery); // 计算需分几次取 const batchTimes = Math.ceil(total / 20) // 承载所有读操作的 promise 的数组 const tasks = [] for (let i = 0; i < batchTimes; i++) { query.limit = 20; query.pageIndex = i + 1; const promise = get(collectionName, query); tasks.push(promise) } // 等待所有 return (await Promise.all(tasks)).reduce((acc, cur) => { acc = acc || []; cur = cur || []; return acc.concat(cur); }) } switch (type(query)) { case "string": return db.collection(collectionName).doc(query).get().then(res => { return res.data }).catch(res => { console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`) return {} }) case "object": const defaultOptions = { where: null, order: null, skip: 0, limit: 20, field: null, pageIndex: 1 } const parsequery = setDefaultOptions(query, defaultOptions); let { where, order, skip, limit, field, pageIndex } = parsequery; let collectionGet = db.collection(collectionName); if (where != null) { if (openParse) { where = await parseQuery(where) } collectionGet = collectionGet.where(where) } if (order != null) { if (type(order) == "object") { collectionGet = collectionGet.orderBy(order.name, order.value); } if (type(order) == "array") { order.forEach(orderItem => { collectionGet = collectionGet.orderBy(orderItem.name, orderItem.value); }); } } if (field) { collectionGet = collectionGet.field(field); } if (pageIndex > 1) { collectionGet = collectionGet.skip((pageIndex - 1) * limit).limit(limit); } else { collectionGet = collectionGet.skip(skip).limit(limit); } return collectionGet.get().then(res => { return res.data }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return [] }) default: console.warn(`"query":参数类型错误不存在`) return null; } } async function count(collectionName, query, openParse = false) { switch (type(query)) { case "object": let collectionUpdate = db.collection(collectionName); if (openParse) { query = await parseQuery(query) } collectionUpdate = collectionUpdate.where(query) return collectionUpdate.count().then(res => { return res.total }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return 0 }) default: return db.collection(collectionName).count().then(res => { return res.total }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return 0 }) } } //修改 async function update(collectionName, query, updata, openParse = false) { switch (type(query)) { case "string": return db.collection(collectionName).doc(query).update({ data: updata }).then(res => { return res.stats.updated }).catch(res => { console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`) return 0 }) case "object": let collectionUpdate = db.collection(collectionName); if (openParse) { query = await parseQuery(query) } collectionUpdate = collectionUpdate.where(query) return collectionUpdate.update({ data: updata }).then(res => { return res.stats.updated }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return 0 }) default: console.warn(`"query":参数类型错误不存在`) return 0 } } //删除 async function remove(collectionName, query, openParse = false) { switch (type(query)) { case "string": return db.collection(collectionName).doc(query).remove().then(res => { return res }).catch(res => { console.warn(`"collection":"${collectionName}","_id":"${query}"不存在`) return {} }) case "object": let collectionRemove = db.collection(collectionName); if (openParse) { query = await parseQuery(query) } collectionRemove = collectionRemove.where(query) return collectionRemove.remove().then(res => { return res }).catch(res => { console.warn(`"collection":"${collectionName}"不存在`) return [] }) default: console.warn(`"query":参数类型错误不存在`) return 0 } } function setDefaultOptions(options = {}, defaultOptions = {}) { return Object.assign(defaultOptions, options); } function promisify(api) { return (options, ...query) => { return new Promise((resolve, reject) => { api(Object.assign({}, options, { success: resolve, fail: reject }), ...query); }) } } var undef = void(0) function type(obj) { if (obj === null) return 'null' else if (obj === undef) return 'undefined' var m = /\[object (\w+)\]/.exec(Object.prototype.toString.call(obj)) return m ? m[1].toLowerCase() : '' } async function parseQuery(query) { let queryStr = JSON.stringify(query); if (queryStr.indexOf("{openid}") > -1) { let openid = cloud.getWXContext().OPENID; return JSON.parse(queryStr.replace(/{openid}/g, openid)); } else { return query } } 三、建议云开发是主要是类似mongdb的非关系数据库,可以保存json的数据,我们可以多直接保存复杂的值尝试使用自己封装的业务逻辑来全局控制异常等数据库的权限、索引等可以对数据库检索性能进一步优化产品介绍云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等serverless化能力,可用于云端一体化开发多种端应用(小程序,公众号,Web 应用,Flutter 客户端等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。 开通云开发:https://console.cloud.tencent.com/tcb?tdl_anchor=techsite 产品文档:https://cloud.tencent.com/product/tcb?from=12763 技术文档:https://cloudbase.net?from=10004 技术交流群、最新资讯关注微信公众号【腾讯云云开发】
2021-06-10 - 微信支付用云函数实现notify_url
没有自己的服务器,怎么用云函数来接收微信支付成功异步通知呢。 步骤如下: (因论坛审核机制,就不上图片了,否则可能触发人工审核) 打开小程序开发工具; 1、云控制台中,点击:设置--环境设置--充值与账单; 此时会跳到腾讯云; 2、点击:账号中心--访问管理; 此时会跳到腾讯云控制台; 3、在左上角点击:云产品--找到:云开发CloudBase; 此时出现小程序的云环境列表 4、选择某个云环境; 5、点击:访问服务; 6、新建一个HTTP访问服务;将一个公网域名URL与一个云函数关联; 比如云函数名:pay_notify与https://<http访问服务的默认域名>/pay_notify关联。 7、在统一下单里,将notify_url设为:https://<http访问服务的默认域名或自定义域名>/pay_notify 此时,可以在pay_notify中处理来自微信支付的异步通知了; 那这个云函数的入口参数是什么样的呢?接口文档: https://cloud.tencent.com/document/product/876/41776 8、固定IP的配置:可以在此处腾讯云中配置,也可以在小程序开发工具的云控制台配置:某云函数--配置--高级设置--固定IP。 9、pay_notify的代码实例:以微信支付V2为例。 const cloud = require('wx-server-sdk') const xml2js = require('xml2js') const crypto = require('crypto') const config = require('./config.js') const key = config.key cloud.init({env: cloud.DYNAMIC_CURRENT_ENV}) const db = cloud.database() const _ = db.command const col = db.collection('payments') exports.main = async event => { let xml = Buffer.from(event.body, 'base64').toString() let payment = await parseXML(xml) if(signVerify(payment)){}else return 'denied' await onPayment(payment) //业务处理 return `` } 至此,不用服务器,通过云函数,就实现了微信支付notify_url的全部功能。
2021-04-28 - 一次更新多条记录,中间失败如休退回原来的状态?
我要一次更新三个集合,三条更新语句,如果中间一条失败,如何让第一条已经更新的记录返回原来的状态,我考虑用starttransaction, 但是方档说只能对一条记录使用,而我在筛选是用了where语句,哪个前辈有好的办法解决,先在这里跪谢了
2020-04-27