个人案例
- 微商店助手
一机在手,生意到手。
微商店助手扫码体验
- 云函数(nodejs)使用上传图片api获取MediaID
接口说明 适用对象:服务商/电商平台 请求URL:https://api.mch.weixin.qq.com/v3/merchant/media/upload 逻辑流程: 从云储存或小程序端获取图片二进制数据 使用crypto生成文件摘要 使用crypto生成签名和授权信息(需要先有文件名和文件摘要) 自定义分割字符串boundary和HTTP头Content-Type 创建传输body的头部和尾部,并与文件拼接 发送请求 1. 获取图片二进制数据 可以从云储存获取,或小程序端传参 小程序获取的是ArrayBuffer,在后续使用中需要转型成Buffer 场景1: 从云储存获取图片buffer 云函数代码 [代码]// [云储存文件id] 可以从小程序端传入 const fileRes = await cloud.downloadFile({fileID: '云储存文件id'}) const imgBuffer = fileRes.fileContent [代码] 场景2:从小程序端获取图片buffer 小程序代码 [代码]const fileSystemManager = wx.getFileSystemManager() const buffer = fileSystemManager.readFileSync('图片路径') wx.cloud.callFunction({ name: '云函数名称', data: { buffer, filename: '图片名.jpg' } }) [代码] 云函数代码 [代码]let { buffer, filename } = event const imgBuffer = Buffer.from(buffer) [代码] 2. 生成摘要、创建meta [代码]const hash = crypto.createHash('sha256'); hash.update(imgBuffer); let sha256 = hash.digest('hex') let meta = { filename, sha256 } [代码] 3. 生成签名、创建授权信息 [代码]let getAuthorization = async function(meta) { // 获取商户私钥、证书序列号、商户号 // (可以储存在云数据中,从云数据库获取) const priKey = '商户私钥' const serialNo = '商户证书序列号' const mchid= '商户号' // 生成随机序列 let nonceStr = Math.random().toString() // 获取当前时间戳(这里需要的是秒数不是毫秒,要除以1000) let timestamp = Math.floor(Date.now() / 1000) // 创建待签名文本 let signatureText = "POST\n" + "/v3/merchant/media/upload\n" + timestamp + "\n" + nonceStr + "\n" + JSON.stringify(meta)+"\n" // 生成签名 let sign = crypto.createSign('RSA-SHA256') sign.update(signatureText) let signature = sign.sign(priKey, 'base64') // 合成授权信息 let authorization = 'WECHATPAY2-SHA256-RSA2048' + ` mchid="${mchid}"` + `,nonce_str="${nonceStr}"` + `,timestamp="${timestamp}"` + `,serial_no="${serialNo}"` + `,signature="${signature}"` return authorization } [代码] 4. 自定义分割字符串boundary和HTTP头Content-Type [代码]// 自定义分割字符串(可以自行定义,和发送的内容不重复即可) let boundary = 'miwoo-boundary-' + Math.random() let contentType = 'multipart/form-data; boundary=' + boundary [代码] 5. 创建传输body的前半部分和尾部,并与文件拼接 [代码]// 创建body的前半部分并转换为buffer // 注意反引号`与单引号'的区别 // 分割符要在boundary前加-- let beginBuffer = Buffer.from( `--${boundary}\r\n` + 'Content-Disposition: form-data; name="meta";\r\n' + 'Content-Type: application/json\r\n' + '\r\n' + `${JSON.stringify(meta)}\r\n` + `--${boundary}\r\n` + `Content-Disposition: form-data; name="file"; filename="${filename}";\r\n` + 'Content-Type: image/jpg\r\n' + '\r\n' ) // 创建body尾部(第一个\r\n是接在imgBuffer后面的换行) // 结束分割符要在boundary两边加-- let endBuffer = Buffer.from(`\r\n--${boundary}--\r\n`) // 将imgBuffer加入头部与尾部,拼接成完整body(不能直接使用+号连接) let body = Buffer.concat([beginBuffer,imgBuffer,endBuffer]) [代码] 6. 发送请求 [代码]axios({ method: 'POST', url: 'https://api.mch.weixin.qq.com/v3/merchant/media/upload', headers: { 'Authorization': authorization, 'Content-Type': contentType }, data: body }) .then(res => { console.log(res.data) }) [代码] 完整流程 [代码]const cloud = require('wx-server-sdk') cloud.init() const crypto = require('crypto') //使用crypto生成文件摘要以及签名 const axios = require('axios') //使用axios发送请求(npm install axios) exports.main =async (event, context) => { // 1. 获取图片buffer const imgBuffer = '从云储存或小程序获取' // 获取文件名(请自行获取) const filename = '图片名.jpg' // 2. 生成文件摘要 const hash = crypto.createHash('sha256'); hash.update(imgBuffer); let sha256 = hash.digest('hex') let meta = { filename, sha256 } // 3. 获取签名(使用上面的签名函数) let authorization = await getAuthorization(meta) // 4. 自定义分割字符串和Content-Type let boundary = 'miwoo-boundary-' + Math.random() let contentType = 'multipart/form-data; boundary=' + boundary // 5. 创建(拼接)body let beginBuffer = Buffer.from( `--${boundary}\r\n` + 'Content-Disposition: form-data; name="meta";\r\n' + 'Content-Type: application/json\r\n' + '\r\n' + `${JSON.stringify(meta)}\r\n` + `--${boundary}\r\n` + `Content-Disposition: form-data; name="file"; filename="${filename}";\r\n` + 'Content-Type: image/jpg\r\n' + '\r\n' ) let endBuffer = Buffer.from(`\r\n--${boundary}--\r\n`) let body = Buffer.concat([beginBuffer,imgBuffer,endBuffer]) // 6. 发送请求 return await axios({ method: 'POST', url: 'https://api.mch.weixin.qq.com/v3/merchant/media/upload', headers: { 'Authorization': authorization, 'Content-Type': contentType }, data: body }) .then(res => { console.log(res.data) return res.data.media_id }) } [代码] 欢迎留言 本文为**电商平台(云开发)**的填坑之作,欢迎提出不足之处、分享云开发的经验和坑。
2021-06-02 - 小程序云开发:data exceed max size 解决方案
说现象 前两天有客服消息说我的小程序无法进行正常功能的使用了,后来定位到问题了,在 update 的时候出现错误信息:data exceed max size。 [图片] 并且这个错误还开发工具上不出现,只有在手机上才出现。原因是 update 的字段内容太大了导致无法正常修改,那么我们要做的就是让参数变小。 分析问题 [图片] 这个小程序是实现的是投票功能,有多层嵌套,之前的更新方案是直接修改 optionData 里面的所有内容。 数据结构解释: optionData 是一个数组里面存储选项数据 optionData 下面还有一个joiner数据组,这个是存放用户投票信息的。 optionData内容过大导致报错,解决方案是通过只更新局部来解决。 上代码 没改之前的代码 [代码] let vote = await db.collection('activity').doc(event.id).update({ data:{ optionData:event.optionData } }) [代码] 优化后的代码 [代码] let vote = await db.collection('activity').doc(event.id).update({ data:{ ['optionData.'+ joiner.index +'.joiner']:_.push(event.userInfo) } }) [代码] 总结 很多bug当数据量小的时候问题并不会暴露出来,数据一旦大起来问题就来了。本质上反应出一个问题就是从数据结构的设计上这个就是不合理的,不应该所有数据都存放在一个大字段里面,应该做数据拆分,然后通过联查来展示,这样更加便于后续的数据分析与统计。 最后,感谢 @老张 的指点。 后记 还有一种情况也会出现 data exceed max size ,查询数据过大。 有两种解决方案: 查询通过过滤一些无关字段。 1.1 聚合查询使用「project」进行过滤 1.2 普通查询使用「field」进行过滤 分页查询。 详细查看「云开发:实现分页功能」 以上两种方式本质上都是减少一次性的查询数据量。
2020-12-19 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有小程序码的图片,然后引导用户下载图片到本地后再分享到朋友圈。相信大家在绘制分享图中应该踩到 Canvas 的各种(坑)彩dan了吧~ 这里首先推荐一个开源的组件:painter(通过该组件目前我们已经成功在支付宝小程序上也应用上了分享图功能) 咱们不多说,直接上手就是干。 [图片] 首先我们新增一个自定义组件,在该组件的json中引入painter [代码]{ "component": true, "usingComponents": { "painter": "/painter/painter" } } [代码] 然后组件的WXML (代码片段在最后) [代码]// 将该组件定位在屏幕之外,用户查看不到。 <painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" /> [代码] 重点来了 JS (代码片段在最后) [代码]Component({ properties: { // 是否开始绘图 isCanDraw: { type: Boolean, value: false, observer(newVal) { newVal && this.handleStartDrawImg() } }, // 用户头像昵称信息 userInfo: { type: Object, value: { avatarUrl: '', nickName: '' } } }, data: { imgDraw: {}, // 绘制图片的大对象 sharePath: '' // 生成的分享图 }, methods: { handleStartDrawImg() { wx.showLoading({ title: '生成中' }) this.setData({ imgDraw: { width: '750rpx', height: '1334rpx', background: 'https://qiniu-image.qtshe.com/20190506share-bg.png', views: [ { type: 'image', url: 'https://qiniu-image.qtshe.com/1560248372315_467.jpg', css: { top: '32rpx', left: '30rpx', right: '32rpx', width: '688rpx', height: '420rpx', borderRadius: '16rpx' }, }, { type: 'image', url: this.data.userInfo.avatarUrl || 'https://qiniu-image.qtshe.com/default-avatar20170707.png', css: { top: '404rpx', left: '328rpx', width: '96rpx', height: '96rpx', borderWidth: '6rpx', borderColor: '#FFF', borderRadius: '96rpx' } }, { type: 'text', text: this.data.userInfo.nickName || '青团子', css: { top: '532rpx', fontSize: '28rpx', left: '375rpx', align: 'center', color: '#3c3c3c' } }, { type: 'text', text: `邀请您参与助力活动`, css: { top: '576rpx', left: '375rpx', align: 'center', fontSize: '28rpx', color: '#3c3c3c' } }, { type: 'text', text: `宇宙最萌蓝牙耳机测评员`, css: { top: '644rpx', left: '375rpx', maxLines: 1, align: 'center', fontWeight: 'bold', fontSize: '44rpx', color: '#3c3c3c' } }, { type: 'image', url: 'https://qiniu-image.qtshe.com/20190605index.jpg', css: { top: '834rpx', left: '470rpx', width: '200rpx', height: '200rpx' } } ] } }) }, onImgErr(e) { wx.hideLoading() wx.showToast({ title: '生成分享图失败,请刷新页面重试' }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') }, onImgOK(e) { wx.hideLoading() // 展示分享图 wx.showShareImageMenu({ path: e.detail.path, fail: err => { console.log(err) } }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') } } }) [代码] 那么我们该如何引用呢? 首先json里引用我们封装好的组件share-box [代码]{ "usingComponents": { "share-box": "/components/shareBox/index" } } [代码] 以下示例为获取用户头像昵称后再生成图。 [代码]<button class="intro" bindtap="getUserInfo">点我生成分享图</button> <share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" bind:initData="handleClose" /> [代码] 调用的地方: [代码]const app = getApp() Page({ data: { isCanDraw: false }, // 组件内部关掉或者绘制完成需重置状态 handleClose() { this.setData({ isCanDraw: !this.data.isCanDraw }) }, getUserInfo(e) { wx.getUserProfile({ desc: "获取您的头像昵称信息", success: res => { const { userInfo = {} } = res this.setData({ userInfo, isCanDraw: true // 开始绘制海报图 }) }, fail: err => { console.log(err) } }) } }) [代码] 最后绘制分享图的自定义组件就完成啦~效果图如下: [图片] tips: 文字居中实现可以看下代码片段 文字换行实现(maxLines)只需要设置宽度,maxLines如果设置为1,那么超出一行将会展示为省略号 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5 附上painter可视化编辑代码工具:点我直达,因为涉及网络图片,代码片段设置不了downloadFile合法域名,建议真机开启调试模式,开发者工具 详情里开启不校验合法域名进行代码片段的运行查看。 最后看下面大家评论问的较多的问题:downLoadFile合法域名在小程序后台 开发>开发设置里配置,域名为你图片的域名前缀 比如我文章里的图https://qiniu-image.qtshe.com/20190605index.jpg。配置域名时填写https://qiniu-image.qtshe.com即可。如果你图片cdn地址为https://aaa.com/xxx.png, 那你就配置https://aaa.com即可。
2022-01-20