- 适配隐私保护协议接口
1、相关api说明 适配过程中主要是用到了以下几个接口 1. wx.onNeedPrivacyAuthorization:用于监听隐私接口需要用户授权事件,只有当隐私协议需要用户授权时才会由平台触发此事件,然后开发者需要弹窗显示隐私协议说明。如果用户拒绝授权,则隐私接口调用失败,在下次调用到隐私接口时还会继续弹。 2. wx.requirePrivacyAuthorize:用于模拟隐私接口调用,并触发隐私弹窗逻辑,也就是会触发wx.onNeedPrivacyAuthorization,但如果用户之前已经同意过隐私授权,会立即返回success回调,不会触发 wx.onNeedPrivacyAuthorization 。这个api的使用场景目前我是用在获取用户昵称时,在下一篇文章会进行说明。 3. wx.openPrivacyContract:用于打开隐私协议页面。 其他api大家可以根据具体情况选择使用。 2、弹窗思路 其实就是写一个自定义组件,然后在有调用到隐私接口的页面引入,利用自定义组件的attached方法,把各个页面的隐私弹窗组件的显示、隐藏方法保存到自定义组件的全局变量中,当用户点击隐私协议弹窗的同意、拒绝按钮时调用resolve方法,将对应的参数通知给平台。 3、注意点 隐私接口在隐私保护指引中有声明才能调用基础库版本2.32.3及以上开始支持2023.9.15号之前,在 app.json 中配置 __usePrivacyCheck__: true 后,会启用隐私相关功能,如果不配置或者配置为 false 则不会启用。2023.9.15号之后,不论 app.json 中是否有配置 __usePrivacyCheck__,隐私相关功能都会启用wx.onNeedPrivacyAuthorization 是覆盖式注册监听,若重复注册监听,则只有最后一次注册会生效同意授权后如果想取消授权,在开发工具上 清缓存->清除模拟器缓存->清除授权数据,在手机上删除小程序即可隐私弹窗的z-index要设置成最大,避免被其他遮罩挡住导致无法点击,另外如果有调到官方的api例如wx.showLoading,也要注意是否设置了mask,避免在loading的时候弹出隐私弹窗,导致弹窗无法点击,而loading又要等弹窗关闭才会消失的尴尬情况4、代码如下 4.1、app.json "usingComponents": { //全局自定义组件 "privacyPopup": "/components/privacy/privacyPopup" }, //开启隐私相关功能 "__usePrivacyCheck__": true 4.2、自定义组件privacyPopup 4.2.1、privacyPopup.wxml <view class="container" wx:if="{{show}}"> <view class="cover {{showCoverAnimation?'cover-fade-in':''}}" catch:touchmove="return"></view> <view class="privacy-box {{showBoxAnimation?'slade-in':''}} {{device.isPhoneX? 'phx_68':''}}" catch:touchmove="return"> <view class="title flex-start-horizontal"> <view class="logo" wx:if="{{privacyConfig.icon}}"> <image class="icon" src="{{privacyConfig.icon}}"></image> </view> <view class="mini-name">{{privacyConfig.name || '小程序'}}</view> </view> <view class="tips"> <view class="privacy-content"> <view class="start">{{privacyConfig.content.start}}</view> <view class="link" bindtap="openPrivacyContract"> {{privacyConfig.content.mid}} </view> <view class="end">{{privacyConfig.content.end}}</view> </view> </view> <view class="buttons flex-center"> <button class="cancel reset-btn" bindtap="disagree">拒绝</button> <button class="save reset-btn" id="agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="agree">同意</button> </view> </view> </view> 4.2.2、privacyPopup.wxss @import "/app.wxss"; .cover{ background-color: #111; opacity: 0; position: fixed; left: 0; top: 0; right: 0; bottom: 0; z-index: 50000; transition: opacity .3s; } .privacy-box{ position: fixed; left: 0rpx; right: 0rpx; bottom: 0rpx; z-index: 51000; background-color: #fff; box-sizing: border-box; padding-top: 60rpx; padding-left: 50rpx; padding-right: 50rpx; border-radius: 30rpx 30rpx 0 0; transform: translateY(100%); transition: transform .3s, bottom .3s; } .cover-fade-in{ opacity: 0.7; } .slade-in{ transform: translateY(0); bottom: 0rpx; } .privacy-box .title{ font-size: 32rpx; font-weight: 500; text-align: center; margin-bottom: 40rpx; color: #161616; } .privacy-box .title .icon{ width: 46rpx; height: 46rpx; margin-right: 8rpx; vertical-align: bottom; border-radius: 50%; } .privacy-box .title .mini-name{ /*margin-bottom: 2rpx;*/ } .privacy-box .tips{ margin-bottom: 20rpx; } .privacy-box .tips .privacy-content{ color: #606266; font-size: 30rpx; margin-bottom: 20rpx; line-height: 1.6; } .privacy-box .tips .privacy-content .start, .privacy-box .tips .privacy-content .link, .privacy-box .tips .privacy-content .end { display: inline; } .privacy-box .tips .privacy-content .link{ color: #1890FF; } .privacy-box .buttons{ margin-bottom: 40rpx; margin-top: 50rpx; text-align: center; font-size: 34rpx; font-weight: 550; } .privacy-box .buttons .save{ width: 220rpx !important; height: 90rpx; line-height: 90rpx; border-radius: 14rpx; color: #fff; /*background:#fff linear-gradient(90deg,rgba(246, 120, 121,.9) 10%, rgb(246, 120, 121));*/ background-color: #07c160; margin-left: -50rpx; } .privacy-box .buttons .cancel{ width: 220rpx !important; height: 90rpx; line-height: 90rpx; border-radius: 14rpx; color: #07c160; background-color: #F2F2F2; } 4.2.3、privacyPopup.js //获取应用实例 const tabbar = require("../../utils/tabbar.js"); const app = getApp(); //用来保存各个页面注册的隐私协议回调事件 let privacyHooks = {}; if (wx.onNeedPrivacyAuthorization) { console.warn("当前基础库支持api wx.onNeedPrivacyAuthorization"); wx.onNeedPrivacyAuthorization(resolve => { console.warn("需要隐私协议弹窗:", resolve); const pages = getCurrentPages(); const route = pages[pages.length - 1].route; const hook = privacyHooks[route]; if (hook) { hook.resolve = resolve; hook.show(resolve); } else { console.error("当前页面没有注册隐藏协议弹窗回调:", route); } }) } else { console.warn("当前基础库不支持api wx.onNeedPrivacyAuthorization"); } Component({ data: { show: false, showCoverAnimation: false,//显示类别选择窗口动画 showBoxAnimation: false,//显示类别选择窗口动画 }, lifetimes: { //在组件实例进入页面节点树时执行 attached: function () { console.warn("privacyPopup attached"); const pages = getCurrentPages(); privacyHooks[pages[pages.length - 1].route] = { show: resolve => { this.show(resolve); }, close: () => { this.hide(); } } }, //在组件实例被从页面节点树移除时执行 detached: function () { console.warn("privacyPopup detached"); } }, methods: { /** * 同意隐私协议 * @param e */ agree(e) { Object.values(privacyHooks).forEach(hook => { hook.close(); hook.resolve && hook.resolve({ event: "agree", buttonId: "agree-btn" }); }); }, /** * 不同意隐私协议 * @param e */ disagree(e) { Object.values(privacyHooks).forEach(hook => { hook.close(); hook.resolve && hook.resolve({ event: "disagree" }); }); }, /** * 显示隐私协议授权弹窗 */ show(resolve) { app.fillConfig(this, ["privacyConfig"], data => { console.log("显示隐私协议弹窗"); const device = app.getSystemInfo(); this.setData({ show: true, device, }, () => { this.setData({ showCoverAnimation: true, showBoxAnimation: true }); //自定义隐私弹窗曝光告知平台 resolve({event: "exposureAuthorization"}); }); tabbar.hideTabByPrivacy(this); }); }, /** * 关闭隐私协议授权弹窗 */ hide() { console.log("隐藏隐私协议弹窗"); this.setData({ showCoverAnimation: false, showBoxAnimation: false }, () => { const that = this; setTimeout(function () { that.setData({ show: false }) }, 300) }) tabbar.showTabByPrivacy(this); }, /** * 打开隐私协议 */ openPrivacyContract() { wx.openPrivacyContract({ success: res => { console.log("openPrivacyContract success") }, fail: res => { console.error("openPrivacyContract fail", res) } }) } } }) 5、弹窗效果 [图片]
2023-09-13 - Skyline|原生级卡片转场,小程序轻松实现
在上一篇文章《在小程序中实现原生相册》中,我们学习了自定义路由搭配共享元素实现的原生相册效果,共享元素可以让用户在体验小程序时视觉关联性更强。 除了相册实现之外,常见的卡片转场也非常适合。 [图片] ⬆️ 演示效果:默认动画 vs 卡片转场动画 👇 下面我们来看看卡片转场中通过 共享元素 + 自定义路由 来实现无痕跳转。 [图片] 这里的转场稍微有点复杂,涉及到以下 3 个点 旧卡片:图片放大、内容渐隐新页面:按比例放大、页面渐显手势搭配1、旧卡片:图片放大、内容渐隐 在本示例中,列表页采用的是 scroll-view 瀑布流布局的实现。 [图片] 这里我们的共享元素是卡片,即 grid-view 中的内容 card,卡片包括 图片、内容描述。 [图片] 默认情况下,共享元素是整个节点进行飞跃的,由于前后页面的图片元素一致但文本内容不一致, 导致在第一帧或者最后一帧会有跳动的效果。 为了让转场动画更加自然,我们需要在飞跃的过程中渐隐旧卡片的内容描述。 [图片] 在这里,我们需要先用 this.applyAnimatedStyle 来给对应的节点绑定 worklet 驱动动画。 .card_wrap 节点:整个卡片按比例放大.card_desc 节点:内容描述渐隐[图片] 关于动画执行的时机,我们可以通过配置项修改。 immediate:设置是否立即执行驱动动画flush:shareValue 更新时,applyAnimatedStyle 的 updater 函数刷新时机在本例中,需要保证共享元素的图片与目标页面图片位置重叠,所以 flush 设置 sync 在当前时间片刷新。 [图片] 绑定完驱动动画之后,我们需要给共享元素绑定帧回调事件,根据当前动画进度改变共享变量的值来驱动共享动画 [图片] 2、新页面:按比例放大、页面渐显 新页面在路由中的动画,需要在自定义路由中进行配置。关于自定义路由的更多介绍,可参考《小程序页面转场动画》 在路由动画过程中,我们将上一步的共享元素帧回调拿到 begin、end 的值,然后结合动画进度 t 计算得出新页面的位置、缩放比例。 还有根据动画进度,设置页面渐显,与前面的卡片渐隐承接。 [图片] 3、手势搭配 学习过我们前面的文章的同学都知道,自定义路由经常需要结合页面手势,来实现手势返回,关于手势的基础知识可参考《小程序页面转场动画》 [图片] 这里我们希望手势缩小整个当前页面,所以这里手势返回时只在当前页面做手势动画即可。 在页面详情页的最外层,嵌套一个手势组件 pan-gesture-handler,当手势拖动时根据手势的位置改变整个页面(通过 #fake-host 控制)的位置和大小来达到拖动的效果。 [图片] 同样绑定页面驱动动画,通过 applyAnimatedStyle 给 #fake-host 绑定驱动动画,当共享变量 transX、transY 等变化时则自动改变 transform 来驱动 #fake-host 缩小。 [图片] 接着绑定手势事件,根据手势拖动时拿到位置信息改变共享变量 transX、transY 的值。 [图片] 最后我们需要设置背景颜色透明,来达到类似把卡片拖回列表的视觉效果,更好的减少页面切换感~ [图片] 一个自定义路由的页面会有 3 层可以设置到背景色,要做到透明的效果需要将 3 个背景色都设置为透明。更多自定义路由背景色的详情参考官方文档。 [图片] 想要试试卡片转场的无恒效果~扫描 ⬇️ 下方小程序码即可体验。 如果你也想在小程序中实现卡片转场动画,mark 下这个 源码 直接接到到你的小程序吧~ [图片]
2023-08-03 - Skyline|在小程序实现原生相册的效果
相册在日常生活中经常使用到,如手机自带相册、朋友圈、商品展示图、评论贴图等等,都经常用到相册的能力。 👇下面演示 iOS 原生相册、朋友圈等相册使用效果,我们可以看到图片切换非常顺滑,视觉焦点不变。 [图片] 😭 但是在小程序中,页面切换会有明显的切换感。用户焦点会丢失,缺少视觉关联性。 [图片] 共享元素🔥 为了丰富用户交互效果、提升用户体验、增强视觉关联性,小程序支持了页面间的共享元素 下图展示有无共享元素的页面切换效果,可以看出使用共享元素之后,转场动画更灵活 [图片] 共享元素 经常作用在图片上,例如上面示例中的相册效果,是那么共享元素动画要怎么实现呢? 在页面跳转时,两个页面 key 相同的 share-element 组件则会产生飞跃的过渡效果 [图片] 在上一篇文章中,我们学习了 页面转场动画,共享元素动画跟页面转场动画是类似的,同样是在页面切换间的动画。 动画进度、时间 与 路由进度、时间保持一致(非自定义路由也支持共享元素动画) 在共享元素飞跃的过程中,前后页面图片的裁剪方式(mode) 可能不一致 这种情况下容易导致图片突然跳变,所以我们需要在飞跃的过程中改变图片的大小来保证平滑飞跃 [图片] 在共享元素动画进行的过程中,share-element 可以收到 onFrame 表示动画帧回调 我们可以在帧回调中处理内部元素的显示 例如:我们这里通过在帧回调中改变图片宽高来达到平滑飞跃的效果 // .wxml // .js // 初始化 attached() { this.aspectRatio = shared(0) this.curRect = shared(undefined) // 绑定 worklet 动画 this.applyAnimatedStyle('.img', () => { 'worklet' const curRect = this.curRect.value return { left: `${curRect.left}px`, top: `${curRect.top}px`, width: `${curRect.width}px`, height: `${curRect.height}px` } }) }, // 获取图片初始宽高比 onImageLoad(e) { const { width, height } = e.detail this.aspectRatio.value = width / height }, // 动画帧回调,调整图片大小 onFrame(data) { 'worklet' // 当前帧容器的宽高、进度等信息 const { begin, end, progress, direction } = data ... // 根据图片初始宽高比、共享元素容器、动画进度等计算出变化过程中的值 this.curRect.value = { left = lerp(begin.left, end.left, t), top = lerp(begin.top, end.top, t), width = lerp(begin.width, end.width, t), height = lerp(begin.height, end.height, t), } } 更多共享元素动画原理请查看 官方文档 手势搭配打开图片之后,我们经常需要用到手势来操作图片,如缩放、移动、双击等等 [图片] 我们上次学过的 手势系统 又派上用场啦 通过监听手势事件配合 worklet 函数即可在小程序实现图片预览效果 👇 下面演示缩放手势的处理,除了缩放之外,相册在手势处理上还有很多复杂的逻辑,包括惯性、边界逻辑判断等 点击查看更多相册相关的手势操作 // .wxml // 绑定缩放手势 let sharedValues = this.sharedValues ?? [] // .js // 绑定缩放 this.applyAnimatedStyle('#image', () => { 'worklet' // worklet 函数,sharedValues 变化时,函数会立即执行 return { transform: `scale(${sharedValues[SCALE].value})` } }) // 监听缩放 onScale(evt) { 'worklet' // 连续的手势状态 && 双指放缩 if (evt.state === GestureState.ACTIVE && evt.pointerCount === 2) { // 计算出当前真正的缩放值 sharedValues[SCALE].value = evt.scale / sharedValues[TEMP_LAST_SCALE].value sharedValues[TEMP_LAST_SCALE].value = evt.scale } } 最后,我们来看下小程序实现出来的相册跟原生相册的使用对比,在小程序也可以顺滑的实现类原生的效果啦~ [图片] 目前,同程旅行 已经上线了共享元素结合手势的相册效果,mark这个 相册源码 直接接入到你的小程序吧~ [视频]
2023-08-03 - 小程序云开发获取并保存用户IP属地
现在各大平台发表文章、评论等内容都显示出了用户的IP属地,现在来探讨一下小程序使用云开发怎么获取并保存用户IP属地。 1、获取到用户ip,这里演示使用云函数获取。 2、使用腾讯位置服务的WebService API的IP定位接口,获取归属地。 响应示例: { "status": 0, "message": "Success", "result": { "ip": "111.206.145.41", "location": { "lat": 39.90469, "lng": 116.40717 }, "ad_info": { "nation": "中国", "province": "北京市", "city": "北京市", "district": "", "adcode": 110000 } } } 演示代码: // 云函数入口文件 const cloud = require('wx-server-sdk') const axios = require('axios') cloud.init() // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext(); var ip = wxContext.CLIENTIP ? wxContext.CLIENTIP : wxContext.CLIENTIPV6; if (ip) { const res = await axios.get("https://apis.map.qq.com/ws/location/v1/ip", { params: { ip: ip, key: "xxx" // 使用腾讯WebService API:https://lbs.qq.com/service/webService/webServiceGuide/webServiceIp } }); return res; } return null; }
2022-05-11 - 小程序公众号干货运营之注销篇
各位亲,面对帐号注销是不是束手无策呢?帐号如何注销,怎么注销,注销需要提供什么信息内容呢?请仔细往下看看 小程序 关于小程序注销的条件,若未冻结的个人帐号和组织类帐号就不 一 一 细讲,详情请参腾讯客服文档:https://kf.qq.com/product/wx_xcx.html#hid=2826 1:小程序注销之政府无对公账户: 详细流程请参考:https://kf.qq.com/faq/190104YnQbYN190104RzaYba.html 政府的有一致主体是提供一致的主体证件和公章,如果有变更请提供4项材料:因机构改革、单位合并、撤一建一等情况,导致机构主体名称有变更,提供以下材料申请注销: 1、更名相关的红头文件(有鲜章); 2、主体名称变更情况说明书(加盖新主体公章); 3、变更后新主体的主体证件;(原件拍照或加盖公章的复印件) 4、注销申请函(加盖新主体公章); 2:小程序注销之个体工商户 若个体工商户存在对公账户,请使用对公账户小额打款注销 若个体工商户类型无对公账户注销小程序工单指引流程如下 工单所需材料 1、小程序绑定邮箱/原始ID: 2、主体证件材料(营业执照/组织机构代码证等): 3、小程序绑定的法人身份证原件正反面的清晰扫描件或照片: 4、小程序的注销书面申请,申请书必须加盖公章。(若个体户没有公章可支持法人手写签名) 附注:注销申请书模板(https://kf.qq.com/faq/200306R7N3mI200306I3aEBz.html) 材料提交链接:https://kf.qq.com/touch/bill/200306selfqaafe6c551.html(手机端打开) 3:小程序注销之帐号主体已注销 主体已注销小程序工单指引流程如下, 1、小程序绑定邮箱/原始ID: 2、主体注销证明: 3、小程序绑定的法人身份证原件正反面的清晰扫描件或照片: 4、小程序的注销书面申请,企业账号的申请必须有加盖公章的函件(公章被收的请法人手写签字)附注:注销申请书模板(https://kf.qq.com/faq/200306R7N3mI200306I3aEBz.html) 材料提交链接:https://kf.qq.com/touch/bill/200306selfqaafe6c551.html(手机端打开) 4:小程序注销之门店小程序 门店小程序依附于公众号,不支持单独注销,公众号注销门店小程序才支持注销 5:公众号正常运营,门店小程序如何释放昵称 如果需要释放该小店小程序昵称,发送邮件到“miniprogram@tencent.com”,标题格式【关于XXX名称释放请求】,需提供以下材料: 1、小程序帐号(原始ID); 2、绑定的管理员微信号; 3、小程序主体营业执照等主体证件; 4、小程序所有者的书面申请,申请书需加盖小程序主体公章;(个体户无公章:申请书需要加上法人签名); 邮件内容:需包含背景、释放请求原因。 6:复用公众号资质快速注册的小程序如何注销 复用资质申请的小程序是独立存在的,请按照正常流程注销即可 7:注册小程序选择微信认证,若未完成微信认证如何注销呢? 小程序30天未认证或认证失败且7天内未发起认证不会释放邮箱,但该邮箱支持重新注册小程序,会释放主体信息、管理员信息、昵称。 公众号 关于公众号若未冻结的个人帐号和组织类帐号就不一一细讲,详情请参考腾讯客服文档:https://kf.qq.com/product/weixinmp.html#hid=2267 1:公众号注销之政府无对公账户: 详细流程请参考:https://kf.qq.com/faq/190531qyuuiY190531BjyyEv.html 政府的有一致主体是提供一致的主体证件和公章,如果有变更请提供4项材料:因机构改革、单位合并、撤一建一等情况,导致机构主体名称有变更,提供以下材料申请注销: 1、更名相关的红头文件(有鲜章); 2、主体名称变更情况说明书(加盖新主体公章); 3、变更后新主体的主体证件;(原件拍照或加盖公章的复印件) 4、注销申请函(加盖新主体公章); 2:公众号注销之个体工商户 若个体工商户存在对公账户,请使用对公账户小额打款注销 若个体工商户类型无对公账户,请使用法人扫脸注销公众号 详情请参考:https://kf.qq.com/faq/220309bUvmIB220309BbAjMz.html 3:公众号注销之帐号主体已注销 主体已注销公众号工单指引流程如下, 1、公众号绑定邮箱/原始id/微信号: 2、主体注销证明: 3、公众号绑定的法人身份证原件正反面的清晰扫描件或照片: 4、公众号的注销书面申请,企业账号的申请必须有加盖公章的函件(公章被收的请法人手写签字) 附注:注销申请书模板(http://kf.qq.com/faq/171018R3IVBF171018INjUvA.html ) 材料提交链接:https://kf.qq.com/touch/bill/180227selfqa9ab6ac55.html(手机端打开) 4:未注册成功的帐号如何注销 若帐号当时没有走完注册流程且长期没有登录该帐号,到期会被系统注销。没有走完注册流程的帐号不占用个人信息,也不支持找回,建议重新注册 5:注册公众号选择微信认证,若未完成微信认证如何注销呢? 若公众号注册时选择微信认证,自注册日起30天内未进行认证(第30天仍在认证中不算),点击“重新提交材料”,帐号角色变为注册失败,不会释放帐号邮箱,但该邮箱支持重新注册公众号,会释放主体信息、管理员信息、昵称, 6:小程序公众号注销确认期 注销确认期的7天内每天会发送一次确认注销的通知,若管理员一直未点击确认注销则默认取消注销,注销失败。因此管理员请关注公众平台安全助手!!!
2022-04-08 - 微信开发者工具使用的域名列表
登录相关 https://mp.weixin.qq.com https://open.weixin.qq.com https://long.open.weixin.qq.com https://lp.open.weixin.qq.com 主服务器 https://servicewechat.com CDN https://dldir1.qq.com https://res.wx.qq.com https://res.servicewechat.com (基础库下载地址) 云开发相关 https://tcb.cloud.tencent.com https://scf.tencentcloudapi.com https://flexdb.tencentcloudapi.com https://tcb.tencentcloudapi.com 真机调试 wss://wxagame.weixin.qq.com 帧同步服务器 https://long.wxagame.weixin.qq.com 上报相关 https://cube.weixinbridge.com https://repot.url.cn
2023-05-23 - 小程序客服回复消息返回文字链
请教下,官方文档说返回文本消息时,内容可以是跳转小程序的a链,但是我怎么配置都不管用,这个应该怎么配置? 发送文本消息时,支持添加可跳转小程序的文字链 [代码]文本内容....<a href="http://www.qq.com" data-miniprogram-appid="appid" data-miniprogram-path="pages/index/index">点击跳小程序</a>[代码]
2018-09-19 - 云开发云函数中使用Redis的最佳实践,包括五种常用数据结构和分布式全局锁
Redis因其拥有丰富的数据结构、基于单线程模型可以实现简易的分布式锁、单分片5w+ ops的超强性能等等特点,成为了大家处理高并发问题的最常用的缓存中间件。 那么云开发能不能使用Redis呢?答案是肯定的。 下面我介绍下云开发中Redis使用的最佳实践: 第一步、购买Redis,安装Redis扩展 参见官方文档:https://developers.weixin.qq.com/community/develop/article/doc/000a4446518488b6002c9fa3651813 吐槽一下,写这篇文章的原因之一就是上面的官方文档中的示例代码是在不堪入目,希望这篇文章能让小伙伴少踩些坑。 第二步、创建并部署测试云函数,配置云函数的网络环境 [图片] 第三步、编写代码 cache.js const Redis = require('ioredis') const redis = new Redis({ port: 6379, host: '1.1.1.1', family: 4, password: 'password', db: 0 }) exports.redis = redis /** * 加redis全局锁 * @param {锁的key} lockKey * @param {锁的值} lockValue * @param {持续时间,单位s} duration */ exports.lock = async function(lockKey, lockValue, duration) { const lockSuccess = await redis.set(lockKey, lockValue, 'EX', duration, 'NX') if (lockSuccess) { return true } else { return false } } /** * 解redis全局锁 * @param {锁的key} lockKey * @param {锁的值} lockValue */ exports.unlock = async function (lockKey, lockValue) { const existValue = await redis.get(lockKey) if (existValue == lockValue) { await redis.del(lockKey) } } 上面是操作redis的工具方法,可以打包放到云函数的层管理中,方便其他云函数引用。层管理使用方式参见官方文档:https://cloud.tencent.com/document/product/876/50940 index.js const cloud = require("wx-server-sdk") const cache = require('/opt/utils/cache.js') // 使用到了云函数的层管理 cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) global.cloud = cloud global.db = cloud.database() global._ = db.command global.$ = _.aggregate exports.main = async (event, context) => { context.callbackWaitsForEmptyEventLoop = false const wxContext = cloud.getWXContext() let appId = wxContext.APPID if (wxContext.FROM_APPID) { appId = wxContext.FROM_APPID } let unionId = wxContext.UNIONID if (wxContext.FROM_UNIONID) { unionId = wxContext.FROM_UNIONID } let openId = wxContext.OPENID if (wxContext.FROM_OPENID) { openId = wxContext.FROM_OPENID } // redis五种常用数据结构 // 字符串 await cache.redis.set('hello', 'world') // 无过期时间 await cache.redis.set('hello', 'world', 'EX', 60) // 过期时间60s let stringValue = await cache.redis.get('hello') console.log('string: ', stringValue) // hash await cache.redis.hset('hash', 'hello', 'world') let hashValue = await cache.redis.hget('hash', 'hello') console.log('hash: ', hashValue) // list await cache.redis.lpush('list', 'hello', 'world') let listList = await cache.redis.lrange('list', 0, -1) // 读取队列所有元素 await cache.redis.ltrim('list', 1, 0) // 清空队列 console.log('listList: ', listList) // set await cache.redis.sadd('set', 'hello', 'world') let setExist = await cache.redis.sismember('set', 'hello') // 检查元素是否在集合中 console.log('set: ', setExist) // zset await cache.redis.zadd('zset', 1, 'hello', 2, 'world') let zsetList = await cache.redis.zrange('zset', 0, -1, 'WITHSCORES') console.log('zsetList: ', zsetList) // redis实现分布式全局锁 // 加全局锁,锁的过期时间应根据实际业务调整 const createOrderLock = `createOrderLock:${unionId}` const ts = Date.now() if (!(await cache.lock(createOrderLock, ts, 3))) { return { code: 4, msg: '操作太频繁了' } } // 这边写全局互斥的业务逻辑代码 // 比如创建订单,一个用户同时只能并发创建一个订单 // 解全局锁 await cache.unlock(createOrderLock, ts) return { code: 0, data: {} } } 上面是测试云函数的入口文件,演示了redis五种常用数据结构和redis全局锁的使用方法。 最后还有个小tips,所有引用到cache.js的云函数需要安装ioredis的依赖,进入云函数目录,使用如下命令: npm install ioredis
2021-06-17 - 关于云开发的一次性订阅消息
前段时间看到了这位老哥的一篇关于订阅消息的文章:https://developers.weixin.qq.com/community/develop/article/doc/0008802e8381e0eeabb92c9975b013 这篇文章对于程序员来说非常直观的说明了一次性订阅消息的逻辑:订阅1次,可以收到订阅消息一次,订阅10次,可以收到订阅消息10次。 但是我觉得这个方案对于一个普通用户来说,并不够友好,如果我是一个不懂订阅消息的普通用户,我根本不会花时间去点这样一个点1加1的订阅消息。我觉得对于开发者来说,用户能够点一次允许并且勾上不在询问就已经很不错了,剩下的完全可以交给程序来处理。下面是我的方案。让一次性订阅消息达到长期订阅的效果。 首先明确以下逻辑: 通过 wx.getSetting({ withSubscriptions: true }) 的 success 回调 res 可以得出订阅消息的以下5种状态[图片]当用户勾选了“不在询问”之后,不管你后面怎么调 wx.requestSubscribeMessage ,订阅消息的弹窗都是不会弹起的wx.requestSubscribeMessage 需要用户手动点击触发当得到以上几种状态之后,接下来就可以根据需要做自己想要做的操作 如我的小程序首页是一个版本列表 [图片] 我在列表的头部设计了一个跟小程序同风格的授权卡片,这样不会显得突兀同时告诉用户点击授权并且勾选“不在询问”,并告诉用户这样做的目的是什么。 然后根据上面得到的不同状态来显示不同的提示语: 总开关关闭了: [图片] 勾选了“不在询问”并且选项是取消 [图片] 接下来就是实现订阅消息+1的步骤,上面提到了当用户勾选了“不在询问”之后,不管你后面怎么调 wx.requestSubscribeMessage ,订阅消息的弹窗都是不会弹起的 这时在用户点击你应用中必点的操作时,比如知乎微博的点击列表进入详情,或者我这个小程序点击版本列表进入版本详情时就可以根据以上得到的状态来判断:当授权状态是“选择了不在询问并且选项是允许” 时,直接调用 wx.requestSubscribeMessage ,这时 wx.requestSubscribeMessage 的回调必定是success,而且不会出现授权弹窗,自然也就实现了+1效果。 最后把订阅次数+1记录到数据库,推送时推送订阅次数大于0的就ok了 [图片] 这样一个普通用户需要做的操作就只有点击授权-勾选不在询问-允许 这样一个步骤,同时就实现了无形中增加订阅次数的效果,替代让用户手动去点+1增加订阅消息的操作。 另外不用担心这种操作会使用户感觉像垃圾广告一样一直被推送,因为不管是在服务通知页面,还是在设置页面,用户都是可以很轻松的一键关闭通知。 [图片] 然后说下订阅消息的几个特殊情况: 1.当你的账号在开发者工具上面点过允许或取消的时候,wx.getSetting({ withSubscriptions: true }) 的 success 回调结果是这样的 [图片] 手机上的设置界面是这样的 [图片] 回调的itemSettings属性消失了,界面上有订阅消息的开关,但是订阅消息的选项却没了,正常情况应该是这样的 [图片] 这样的 [图片] 2.当用户点了“不在询问并允许”但是又手动通过服务通知页面,或者设置页面关闭了消息通知,这时就算该用户之前已经订阅过了很多次,都会被系统自动清0,这时你的数据库可能存的该用户还有比如5次订阅消息,但是通过cloud.openapi.subscribeMessage.send推送消息的时候,会进catch,errCode是43101。 3.当用户手速过快连续点击了授权按钮触发wx.requestSubscribeMessage时,会进入fail回调,errMsg是 requestSubscribeMessage:fail last call,这个文档是没写的。 最后可以扫码体验一下: [图片]
2020-07-14 - 用云开发Cloudbase,实现小程序多图片内容安全监测
作者介绍 随笔川迹: 一个靠前排的90后具有情怀的技匠,路上正追逐斜杠青年的践行者,人人领读发起人。 小程序云开发工作者,致力于小程序云开发研究,持续分享传播小程序云开发过程中遇到的一些坑以及知识体系,希望能帮助更多的小程序云开发者,与开发者们一起成长。 前言相比于文本的安全检测,图片的安全检测要稍微略复杂一些,当您读完本篇,将get到 图片安全检测的应用场景解决图片的安全校验的方式使用云调用方式对图片进行检测如何对上传图片大小进行限制如何解决多图上传覆盖问题 示例效果当用户上传敏感违规图片时,禁止用户上传发布,并且做出相对应的用户友好提示: [图片] 应用场景通常,在校验一张图片是否含有违法违规内容相比于文本安全的校验,同样重要,有如下应用: 图片智能鉴黄:涉及拍照的工具类应用(如美拍,识图类应用)用户拍照上传检测;电商类商品上架图片检测;媒体类用户文章里的图片检测等。敏感人脸识别:用户头像;媒体类用户文章里的图片检测;社交类用户上传的图片检测等,凡是有用户自发生产内容的都应当提前做检测。 解决图片的安全手段在小程序开发中,提供了两种方式 HTTPS调用云调用HTTPS 调用的请求接口地址为: https://api.weixin.qq.com/wxa/img_sec_check?access_token=ACCESS_TOKEN 检测图片审核,根据官方文档得知,需要两个必传的参数:分别是:access_token(接口调用凭证),media(要检测的图片文件) 对于HTTPS调用方式,愿意折腾的小伙伴可以参考文本内容安全检测(上篇)的处理方式,处理大同小异,本篇主要以云开发的云调用为主。 功能实现:小程序端逻辑对于wxml与wxss,大家可以自行任意修改,本文重点在于图片安全的校验。 <view class="image-list"> <!-- 显示图片 --> <block wx:for="{{images}}" wx:key="*this"><view class="image-wrap"> <image class="image" src="{{item}}" mode="aspectFill" bind:tap="onPreviewImage" data-imgsrc="{{item}}"></image><i class="iconfont icon-shanchu" bind:tap="onDelImage" data-index="{{index}}"></i></view> </block> <!-- 选择图片 --> <view class="image-wrap selectphoto" hidden="{{!selectPhoto}}" bind:tap="onChooseImage"><i class="iconfont icon-add"></i></view> </view> <view class="footer"><button class="send-btn" bind:tap="send">发布</button> </view> 对应的wxss代码: .footer { display: flex; align-items: center; width: 100%; box-sizing: border-box; background: #34bfa3; } .send-btn { width: 100%; color: #fff; font-size: 32rpx; background: #34bfa3; } button { border-radius: 0rpx; } button::after { border-radius: 0rpx !important; } /* 图片样式 */ .image-list { display: flex; flex-wrap: wrap; margin-top: 20rpx; } .image-wrap { width: 220rpx; height: 220rpx; margin-right: 10rpx; margin-bottom: 10rpx; position: relative; overflow: hidden; text-align: center; } .image { width: 100%; height: 100%; } .icon-shanchu { position: absolute; top: 0; right: 0; width: 40rpx; height: 40rpx; background-color: #000; opacity: 0.4; color: #fff; text-align: center; line-height: 40rpx; font-size: 38rpx; font-weight: bolder; } .selectphoto { border: 2rpx dashed #cbd1d7; position: relative; } .icon-add { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #cbd1d7; font-size: 60rpx; } 最终呈现的UI,由于只是用于图片检测演示,UI方面可忽略,如下所示: [图片] 对应的JS代码: /* * 涉及到的API:wx.chooseImage 从本地相册选择图片或使用相机拍照 *(https://developers.weixin.qq.com/miniprogram/dev/api/media/image/wx.chooseImage.html) * * */// 最大上传图片数量 const MAX_IMG_NUM = 9; const db = wx.cloud.database(); // 初始化云数据库 Page({ /** * 页面的初始数据 */ data: { images: [], // 把上传的图片存放在一个数组对象里面 selectPhoto: true, // 添加+icon元素是否显示 }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { }, // 选择图片 onChooseImage() { // 还能再选几张图片,初始值设置最大的数量-当前的图片的长度 let max = MAX_IMG_NUM - this.data.images.length; wx.chooseImage({ count: max, // count表示最多可以选择的图片张数 sizeType: ['original', 'compressed'], // 所选的图片的尺寸 sourceType: ['album', 'camera'], // 选择图片的来源 success: (res) => { // 接口调用成功的回调函数console.log(res) this.setData({ // tempFilePath可以作为img标签的src属性显示图片,下面是将后添加的图片与之前的图片给追加起来 images: this.data.images.concat(res.tempFilePaths) }) // 还能再选几张图片 max = MAX_IMG_NUM - this.data.images.length this.setData({ selectPhoto: max <= 0 ? false : true // 当超过9张时,加号隐藏 }) }, }) }, // 点击右上方删除图标,删除图片操作 onDelImage(event) { const index = event.target.dataset.index; // 点击删除当前图片,用splice方法,删除一张,从数组中移除一个 this.data.images.splice(index, 1) this.setData({ images: this.data.images }) // 当添加的图片达到设置最大的数量时,添加按钮隐藏,不让新添加图片 if (this.data.images.length == MAX_IMG_NUM - 1) { this.setData({ selectPhoto: true, }) } }, }) 最终实现的前端UI效果如下所示: [图片] 您现在看到的效果,没有任何云函数代码,只是前端的纯静态展示,对于一些涉嫌敏感图片,是有必要进行做过滤处理的。 功能实现:云函数侧逻辑在cloudfunctions目录文件夹下创建云函数imgSecCheck: [图片] 并在该目录下创建config.json,配置参数如下所示: { "permissions": { "openapi": [ "security.imgSecCheck" ] } } 配置完后,在主入口index.js中,如下所示,通过security.imgSecCheck接口,并传入media对象: // 云函数入口文件 const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async (event, context) => { const wxContext = cloud.getWXContext() try { const result = await cloud.openapi.security.imgSecCheck({ media: { contentType: 'image/png', value: Buffer.from(event.img) // 这里必须要将小程序端传过来的进行Buffer转化,否则就会报错,接口异常 } }) if (result && result.errCode.toString() === '87014') { return { code: 500, msg: '内容含有违法违规内容', data: result } } else { return { code: 200, msg: '内容ok', data: result } } } catch (err) { // 错误处理 if (err.errCode.toString() === '87014') { return { code: 500, msg: '内容含有违法违规内容', data: err } } return { code: 502, msg: '调用imgSecCheck接口异常', data: err } } } 您会发现在云函数端,就这么几行代码,就完成了图片安全校验! 而在小程序端,代码如下所示: // miniprogram/pages/imgSecCheck/imgSecCheck.js // 最大上传图片数量 const MAX_IMG_NUM = 9; const db = wx.cloud.database() Page({ /** * 页面的初始数据 */ data: { images: [], selectPhoto: true, // 添加图片元素是否显示 }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { }, // 选择图片 onChooseImage() { // const that = this; // 如果下面用了箭头函数,那么这行代码是不需要的,直接用this就可以了的// 还能再选几张图片,初始值设置最大的数量-当前的图片的长度 let max = MAX_IMG_NUM - this.data.images.length; wx.chooseImage({ count: max, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success: (res) => { // 这里若不是箭头函数,那么下面的this.setData的this要换成that上面的临时变量,作用域的问题,不清楚的,可以看下this指向相关的知识 console.log(res) // tempFilePath可以作为img标签的src属性显示图片 const tempFiles = res.tempFiles; this.setData({ images: this.data.images.concat(res.tempFilePaths) }) // 在选择图片时,对本地临时存储的图片,这个时候,进行图片的校验,当然你放在最后点击发布时,进行校验也是可以的,只不过是一个前置校验和后置校验的问题,我个人倾向于在选择图片时就进行校验的,选择一些照片时,就应该在选择时阶段做安全判断的, 小程序端请求云函数方式// 图片转化buffer后,调用云函数 console.log(tempFiles); tempFiles.forEach(items => { console.log(items); // 图片转化buffer后,调用云函数 wx.getFileSystemManager().readFile({ filePath: items.path, success: res => { console.log(res); wx.cloud.callFunction({ // 小程序端请求imgSecCheck云函数,并传递img参数进行检验 name: 'imgSecCheck', data: { img: res.data } }) .then(res => { console.log(res); let { errCode } = res.result.data; switch(errCode) { case 87014: this.setData({ resultText: '内容含有违法违规内容' }) break; case 0: this.setData({ resultText: '内容OK' }) break; default: break; } }) .catch(err => { console.error(err); }) }, fail: err => { console.error(err); } }) }) // 还能再选几张图片 max = MAX_IMG_NUM - this.data.images.length this.setData({ selectPhoto: max <= 0 ? false : true // 当超过9张时,加号隐藏 }) }, }) }, // 删除图片 onDelImage(event) { const index = event.target.dataset.index; // 点击删除当前图片,用splice方法,删除一张,从数组中移除一个 this.data.images.splice(index, 1); this.setData({ images: this.data.images }) // 当添加的图片达到设置最大的数量时,添加按钮隐藏,不让新添加图片 if (this.data.images.length == MAX_IMG_NUM - 1) { this.setData({ selectPhoto: true, }) } }, }) 示例效果如下所示: [图片] 至此,关于图片安全检测就已经完成了,您只需要根据检测的结果,做一些友好的用户提示,或者做一些自己的业务逻辑判断即可。 常见问题1.如何对上传的图片大小进行限制有时候,您需要对用户上传图片的大小进行限制,限制用户任意上传超大图片,那怎么处理呢,在微信小程序里面,主要借助的是 wx.chooseImage 这个接口成功返回后临时路径的 res.tempFiles 中的 size 大小判断即可进行处理。 [图片] 具体实例代码如下所示: // 选择图片 onChooseImage() { // 还能再选几张图片,初始值设置最大的数量-当前的图片的长度 let max = MAX_IMG_NUM - this.data.images.length; wx.chooseImage({ count: max, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success: (res) => { console.log(res) const tempFiles = res.tempFiles; this.setData({ images: this.data.images.concat(res.tempFilePaths) // tempFilePath可以作为img标签的src属性显示图片 }) // 在选择图片时,对本地临时存储的图片,这个时候,进行图片的校验,当然你放在最后点击发布时,进行校验也是可以的,只不过是一个前置校验和后置校验的问题,我个人倾向于在选择图片时就进行校验的,选择一些照片时,就应该在选择时阶段做安全判断的, 小程序端请求云函数方式// 图片转化buffer后,调用云函数 console.log(tempFiles); tempFiles.forEach(items => { if (items && items.size > 1 * (1024 * 1024)) { // 限制图片的大小 wx.showToast({ icon: 'none', title: '上传的图片超过1M,禁止用户上传', duration: 4000 }) // 超过1M的图片,禁止用户上传 } console.log(items); // 图片转化buffer后,调用云函数 wx.getFileSystemManager().readFile({ filePath: items.path, success: res => { console.log(res); wx.cloud.callFunction({ // 请求调用云函数imgSecCheck name: 'imgSecCheck', data: { img: res.data } }) .then(res => { console.log(res); let { errCode } = res.result.data; switch(errCode) { case 87014: this.setData({ resultText: '内容含有违法违规内容' }) break; case 0: this.setData({ resultText: '内容OK' }) break; default: break; } }) .catch(err => { console.error(err); }) }, fail: err => { console.error(err); } }) }) // 还能再选几张图片 max = MAX_IMG_NUM - this.data.images.length this.setData({ selectPhoto: max <= 0 ? false : true // 当超过9张时,加号隐藏 }) }, }) }, [图片] 注意: 使用微信官方的图片内容安全接口进行校验,限制图片大小限制:1M,否则的话就会报错 [图片] 也就是说,对于超过1M大小的违规图片,微信官方提供的这个图片安全接口是无法进行校验的。 这个根据自己的业务而定,小程序端对用户上传图片的大小是进行了限制的。如果您觉得微信官方提供的图片安全接口满足不了自己的业务需求,那么可以选择一些其他的图片内容安全校验接口。 图片安全校验是非常有必要的,用户一旦上传非法图片,一旦通过网络进行传播,产生了社会影响,平台将会被追究责任,我们要吸取前车之鉴带给我的教训。 2.如何解决多图上传覆盖的问题对于上传图片来说,这个wx.cloud.uploadFileAPI接口只能上传一张图片,但是很多时候,是需要上传多张图片到云存储当中的,当点击发布的时候,我们是希望将多张图片都上传到云存储当中。 这个API虽然只能每次上传一张,但您可以循环遍历多张图片,然后一张一张上传。 在cloudPath上传文件的参数当中,它的值:需要注意:文件的名称。 那如何保证上传的图片不被覆盖呢?其实文件不重名的情况下就不会被覆盖,而在选择图片的时候,不应该上传,因为用户可能有删除等操作,如果直接上传的话会造成资源的浪费,应该在点发布按钮的时候,才执行上传操作,文件不重名覆盖的示例代码如下所示: let promiseArr = [] let fileIds = [] // 将图片的fileId存放到一个数组中 let imgLength = this.data.images.length; // 图片上传 for (let i = 0; i < imgLength; i++) { let p = new Promise((resolve, reject) => { let item = this.data.images[i] // 文件扩展名 let suffix = /\.\w+$/.exec(item)[0]; // 取文件后拓展名 wx.cloud.uploadFile({ // 利用官方提供的上传接口 cloudPath: 'blog/' + Date.now() + '-' + Math.random() * 1000000 + suffix, // 云存储路径,您也可以使用es6中的模板字符串进行拼接的 filePath: item, // 要上传文件资源的路径 success: (res) => { console.log(res); console.log(res.fileID) fileIds = fileIds.concat(res.fileID) // 将新上传的与之前上传的给拼接起来 resolve() }, fail: (err) => { console.error(err) reject() } }) }) promiseArr.push(p) } // 存入到云数据库,其中这个Promise.all(),等待里面所有的任务都执行之后,在去执行后面的任务,也就是等待上传所有的图片上传完后,才能把相对应的数据存到数据库当中,具体与promise相关问题,可自行查漏 Promise.all(promiseArr).then((res) => { db.collection('blog').add({ // 查找blog集合,将img,时间等数据添加到这个集合当中 data: { img: fileIds, createTime: db.serverDate(), // 服务端的时间 } }).then((res) => { console.log(res); this._hideToastTip(); this._successTip(); }) }) .catch((err) => { // 发布失败console.error(err); }) 上面通过利用当前时间+随机数的方式进行了一个区分,规避了上传文件同名的问题: 因为这个上传接口,一次性只能上传一张图片,所以需要循环遍历图片,然后一张张的上传。 一个是上传到云存储中,另一个是添加到云数据库集合当中,要分别注意下这两个操作,云数据库中的图片是从云存储中拿到的,然后再添加到云数据库当中去的。 示例效果如下所示: [图片] [图片] 将上传的图片存储到云数据库中: [图片] 注意:添加数据到云数据库中,需要手动创建集合,不然是无法上传不到云数据库当中的,会报错 至此,关于敏感图片的检测,以及多图片的上传到这里就已经完成了! 如下是完整的小程序端逻辑示例代码: // miniprogram/pages/imgSecCheck/imgSecCheck.js // 最大上传图片数量 const MAX_IMG_NUM = 9; const db = wx.cloud.database() Page({ /** * 页面的初始数据 */ data: { images: [], selectPhoto: true, // 添加图片元素是否显示 }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { }, // 选择图片 onChooseImage() { // 还能再选几张图片,初始值设置最大的数量-当前的图片的长度 let max = MAX_IMG_NUM - this.data.images.length; wx.chooseImage({ count: max, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success: (res) => { console.log(res) const tempFiles = res.tempFiles; this.setData({ images: this.data.images.concat(res.tempFilePaths) // tempFilePath可以作为img标签的src属性显示图片 }) // 在选择图片时,对本地临时存储的图片,这个时候,进行图片的校验,当然你放在最后点击发布时,进行校验也是可以的,只不过是一个前置校验和后置校验的问题,我个人倾向于在选择图片时就进行校验的,选择一些照片时,就应该在选择时阶段做安全判断的, 小程序端请求云函数方式 // 图片转化buffer后,调用云函数 console.log(tempFiles); tempFiles.forEach(items => { if (items && items.size > 1 * (1024 * 1024)) { wx.showToast({ icon: 'none', title: '上传的图片超过1M,禁止用户上传', duration: 4000 }) // 超过1M的图片,禁止上传 } console.log(items); // 图片转化buffer后,调用云函数 wx.getFileSystemManager().readFile({ filePath: items.path, success: res => { console.log(res); this._checkImgSafe(res.data); // 检测图片安全校验 }, fail: err => { console.error(err); } }) }) // 还能再选几张图片 max = MAX_IMG_NUM - this.data.images.length this.setData({ selectPhoto: max <= 0 ? false : true // 当超过9张时,加号隐藏 }) }, }) }, // 删除图片 onDelImage(event) { const index = event.target.dataset.index; // 点击删除当前图片,用splice方法,删除一张,从数组中移除一个 this.data.images.splice(index, 1); this.setData({ images: this.data.images }) // 当添加的图片达到设置最大的数量时,添加按钮隐藏,不让新添加图片 if (this.data.images.length == MAX_IMG_NUM - 1) { this.setData({ selectPhoto: true, }) } }, // 点击发布按钮,将图片上传到云数据库当中 send() { const images = this.data.images.length; if (images) { this._showToastTip(); let promiseArr = [] let fileIds = [] let imgLength = this.data.images.length; // 图片上传 for (let i = 0; i < imgLength; i++) { let p = new Promise((resolve, reject) => { let item = this.data.images[i] // 文件扩展名 let suffix = /\.\w+$/.exec(item)[0]; // 取文件后拓展名 wx.cloud.uploadFile({ // 上传图片至云存储,循环遍历,一张张的上传 cloudPath: 'blog/' + Date.now() + '-' + Math.random() * 1000000 + suffix, filePath: item, success: (res) => { console.log(res); console.log(res.fileID) fileIds = fileIds.concat(res.fileID) resolve() }, fail: (err) => { console.error(err) reject() } }) }) promiseArr.push(p) } // 存入到云数据库 Promise.all(promiseArr).then((res) => { db.collection('blog').add({ // 查找blog集合,将数据添加到这个集合当中 data: { img: fileIds, createTime: db.serverDate(), // 服务端的时间 } }).then((res) => { console.log(res); this._hideToastTip(); this._successTip(); }) }) .catch((err) => { // 发布失败 console.error(err); }) } else { wx.showToast({ icon: 'none', title: '没有选择任何图片,发布不了', }) } }, // 校验图片的安全 _checkImgSafe(data) { wx.cloud.callFunction({ name: 'imgSecCheck', data: { img: data } }) .then(res => { console.log(res); let { errCode } = res.result.data; switch (errCode) { case 87014: this.setData({ resultText: '内容含有违法违规内容' }) break; case 0: this.setData({ resultText: '内容OK' }) break; default: break; } }) .catch(err => { console.error(err); }) }, _showToastTip() { wx.showToast({ icon: 'none', title: '发布中...', }) }, _hideToastTip() { wx.hideLoading(); }, _successTip() { wx.showToast({ icon: 'none', title: '发布成功', }) }, }) 完整的示例wxml,如下所示: <view class="image-list"> <!-- 显示图片 --> <block wx:for="{{images}}" wx:key="*this"> <view class="image-wrap"><image class="image" src="{{item}}" mode="aspectFill" bind:tap="onPreviewImage" data-imgsrc="{{item}}"></image><i class="iconfont icon-shanchu" bind:tap="onDelImage" data-index="{{index}}"></i> </view> </block> <!-- 选择图片 --> <view class="image-wrap selectphoto" hidden="{{!selectPhoto}}" bind:tap="onChooseImage"><i class="iconfont icon-add"></i></view> </view> <view class="footer"> <button class="send-btn" bind:tap="send">发布</button> </view> <view> 检测结果显示: {{ resultText }} </view> 您可以根据自己的业务逻辑需要,一旦检测到图片违规时,禁用按钮状态,或者给一些用户提示,都是可以的;在发布之前或者点击发布时,进行图片内容安全的校验都可以,一旦发现图片有违规时,就禁止继续后面的操作。 结语本文主要通过借助官方提供的图片:security.imgSecCheck 接口,实现了对图片安全的校验,实现起来,是相当方便的,对于基础性的校验,利用官方提供的这个接口,已经够用了的,但是如果想要更加严格的检测,可以引入一些第三方的内容安全强强校验,确保内容的安全。 实现了如何对上传的图片大小进行限制,以及解决同名图片上传覆盖的问题。 如果大家对文本内容安全校验以及图片安全校验仍然有什么问题,可以在下方留言,一起探讨。
2020-09-14 - 如何关注公众号以后自动推送小程序?
如何关注公众号以后自动推送小程序? 这个问题是不是觉得没什么难度?打开配置界面一看你就傻眼了。 [图片] 怎么自动回复里没有跳转小程序选项?自定义菜单里面是有的呀!:( [图片] 遇到困难先不要慌,办法总是有的! 不过要做一点点准备工作,首先绑定要跳转的小程序,然后准备好小程序的APPID和跳转路径。 然后,一行代码搞定: <a data-miniprogram-appid="小程序APPID" data-miniprogram-path="跳转路径">点我跳转到小程序</a> 上面准备的小程序APPID和跳转路径编辑一下即可。 <a data-miniprogram-appid="wx3fa5ddf638c664d8" data-miniprogram-path="page/tabBar/index/index">点我跳转到小程序</a> [图片] [图片] 想体验效果,搜索:来一间 公众号体验。关键词自动回复也可以用这个方法配置哦! [图片] 补充一点: 如何获取小程序页面路径的方法:https://developers.weixin.qq.com/community/develop/article/doc/0008627017cf104da879c3dd25b813
2020-07-14 - 云数据库如何根据数组字段长度筛选?
比如有这个表 {"_id":1, options:[1,2,3]} {"_id":2, options:[1,2]} 现在我要查询所有options.length>2的记录, 该如何构建呢?翻阅了参考手册, 实在找不到解决方案.
2020-05-01