简介
使用云开发来实现内容安全检测比较方便,并且对个人小程序来说是零成本。但是也有一些问题
问题一:通过云函数调用图片安全接口时,如果直接传入Buffer类型的图片数据,<a href="https://developers.weixin.qq.com/community/develop/doc/000ec2abca002850c6b9ab5c850000?fromCreate=1" rel="noopener" target="_blank">真机可能会返回-404012错误</a>。
这个问题可以先将图片上传至<a href="https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/storage.html" rel="noopener" target="_blank">云存储</a>,再在云函数中下载图片,然后调用内容安全接口,可以避免此问题,并且实测速度较直接传入Buffer有所提升。
内容安全接口的图片大小限制为1M。
为避免出现这个问题,可以使用<a href="https://developers.weixin.qq.com/community/develop/article/doc/00062c5c7a8ec834dc692913156013" rel="noopener" target="_blank">Canvas压缩图片</a>。或调用获取本机图片的接口时默认选择压缩的图片,也可以规避大部分问题。
云函数的访问速度较慢(尤其是小程序启动后的第一次调用)。简单看了一下,用云函数调用文本安全时,一般是0.5-0.6s,还行。而调用图片安全时,用传入Buffer数据的方法检测一个500K的图片大约需__5s__;如果用先上传云存储,再调用云函数检测的方法,上传需大约0.7s,调云函数检测需2.5s,还是需要__3s+__,可能会影响一定的用户体验。
下面主要介绍一下如何“减少”内容安全的请求时间,这个方法在其他地方也可以用。
基本方法是在__用户点击提交前__进行内容安全检测,将请求结果缓存为一个promise对象,然后在用户提交时使用之前缓存的promise的then
方法获取到检测结果。如果这里看明白了,后面的就不用看了,可以自己去实现了。
有些代码没有简化了,所以看起来可能有点复杂,所以可以只看文字注释。
简单的示例代码
图片安全检测
首先将图片安全检测封装为一个Promise类型的函数
async function imgSecCheck({path}){ // async关键词,声明返回Promise
let cloudPath = "images/avatars/"+ path.replace(/[\/\\:]/g, "_");
// 首先上传云存储
let {fileID} = await wx.cloud.uploadFile({ cloudPath, filePath: path });
// 调用云函数
let res = await wx.cloud.callFunction({
// 这里的参数结构要根据你写的云函数来具体确定,
// 该示例代码所对应的云函数代码在后文有给出
name: "openapi",
data:{
name:"security.imgSecCheck",
data:{ fileID }
}
});
return res.result;
}
现在要在一个页面实现图片安全检测
在用户选择本机的图片后,获取到了图片的本地地址path
,此时调用检测函数,并缓存返回的promise对象
let cache_promise = imgSecCheck({ path })
在提交时,使用then
来获取缓存的结果,这里并不需要判断是否请求完成。像这样在用户提交保存前进行检测,可以减少一点用户的等待时间,理想状态下用户等待的检测时间可以为0s。
cache_promise.then(res=>{
if(res.errCode==87014){
// 图片有问题,提示用户
}else{
// 图片没有问题,执行其他逻辑
}
})
文本安全检测
与图片检测类似,也可以用上面相同的方法来实现“加速”
首先将文本安全检测封装为一个Promise类型的函数
async function msgSecCheck({content}){ // async关键词,声明返回Promise
let res = await wx.cloud.callFunction({
// 这里的参数结构要根据你写的云函数来具体确定,
// 该示例代码所对应的云函数代码在后文有给出
name: "openapi",
data:{
name:"security.msgSecCheck",
data:{ content }
}
});
return res.result;
}
可以在用户实时输入的时候,进行提前检测,于是将该函数绑定到input
组件的输入事件bindinput
回调上,或者绑定到输入框的失焦事件bindblur
回调上,根据不同需求来选择即可。
<!--wxml-->
<input bindinput="preCheck"/>
<!--或者用下面这个绑定到blur回调上-->
<input bindblur="preCheck"/>
// js
Page({
preCheck({detail:{value}}){
// 缓存检测结果
this.cache_promise = msgSecCheck({ content: value });
}
})
如果绑定到bindinput
,函数会在__短时间被多次回调__,此时需要对上面的msgSecCheck
进行修饰,让其两次执行间必须__间隔1s__,且__最后一次的回调一定执行__。
可以使用限频函数throttle
对其进行修饰:
msgSecCheck = throttle(msgSecCheck, 1000, {} );
这里用的的throttle
函数取自微信官方的一个代码片段
function throttle(func, wait, options) {
var context = void 0;
var args = void 0;
var result = void 0;
var timeout = null;
var previous = 0;
if (!options) options = {};
var later = function later() {
previous = options.leading === false ? 0 : Date.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function () {
var now = Date.now();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
};
附较完整的代码
图片安全检测
const app = getApp();
Page({
async _checkImg({path}){
// 上传图片至云存储
let cloudPath = "images/avatars/"+ path.replace(/[\/\\:]/g, "_");
let res = await wx.cloud.uploadFile({
cloudPath,
filePath: path
});
let {fileID} = res;
// 调用云函数的图片安全检测接口,该函数返回一个Promise
return app.openapi("security.imgSecCheck")({fileID});
},
checkImg({path}){
// 查看是否有缓存,有则直接返回缓存的promise
if(this.promise&&path==this.tmp_path)
return this.promise;
// 没有则请求,并缓存请求的结果
this.promise = this._checkImg({path});
this.tmp_path = path;
this.promise.then(res=>{
console.log(res);
if(res.errCode==87014){
wx.showToast({
title:rich_message,icon: "none",duration:5000
})
}
});
return this.promise;
},
afterSelectImg({path}){
// 选择图片后
this.checkImg({path});
},
onSubmit(){
// 提交时
this.checkImg().then(res=>{
if(res.errCode==0){
//没有问题,执行其他逻辑
}else if(res.errCode==87014){
wx.showModal({
content:“图片有敏感内容”,showCancel: false
})
}
});
}
})
文本安全检测
const app = getApp();
// 限频函数的一个实现,来自微信官方的一个代码片段
function throttle(func, wait, options) {
var context = void 0;
var args = void 0;
var result = void 0;
var timeout = null;
var previous = 0;
if (!options) options = {};
var later = function later() {
previous = options.leading === false ? 0 : Date.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function () {
var now = Date.now();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
};
function msgSecCheck({content=""}){
// 判断是否有缓存
if(this.tmp_promise&&this.tmp_content==content)
return this.tmp_promise; //有则直接返回
//没有缓存,则进行请求,并以promise类型缓存请求
this.tmp_promise = app.openapi("security.msgSecCheck")({content});
this.tmp_content = content;
this.tmp_promise.then(res=>{
if(res.errCode==87014){
wx.showToast({
title: “有敏感词汇”,icon:"none",duration:4000
})
}
})
return this.tmp_promise;
}
Page({
throttledMsgSecCheck: throttle( msgSecCheck, 1000, {} ),//被限频的函数,用于input事件的回调中
msgSecCheck,
onInput({detail:{value}}){
// input事件的回调
this.throttledMsgSecCheck({content: value});
},
onSubmit({detail:{value}}){
wx.showLoading({
title: '检查内容中...'
})
this.msgSecCheck({content: value.name}).then(res=>{
if(res.errCode==0){
wx.showLoading({
title: '正在提交'
})
//执行提交逻辑
}else{
// 检测到敏感词
wx.hideLoading();
wx.showModal({
content:“有敏感词汇”,
showCancel:false
})
}
})
}
})
本地代码的封装
/** app.js
将云调用代码简单封装了一下
**/
App({
openapi(name){
// 函数柯里化,调用时写起来方便
return ({success, fail, complete, ...data})=>{
return this.callOpenapi({name, data, success,fail, complete});
}
},
callOpenapi({name, data, success, fail, complete}){ // 调用名为openapi的云函数
return this.callCloudapi({name:"openapi", data:{name, data}, success, fail, complete});
},
callCloudapi({name, data, success, fail, complete}){
return new Promise((resolve, reject)=>{
return wx.cloud.callFunction({name, data,
success:res=>{
// 只需要 res.result 中的数据
success&&success(res.result);
resolve(res.result);
complete&&complete(res.result);
}, fail:e=>{
fail&&fail(e);
reject(e);
complete&&complete(e);
}
});
});
}
});
云函数代码
// 云函数名: openapi
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV})
exports.main = async (event, context) => {
console.log("调用云函数openapi, 参数event:", event);
let {name, data} = event;
switch (event.name) {
case "security.msgSecCheck":{
try{
console.log("检查文本安全, 参数:", data);
var res = await cloud.openapi.security.msgSecCheck(data);
console.log("返回结果: ", res);
return res;
}catch(e){
return e;
}
}
case "security.imgSecCheck":{
try{
console.log("检查图片安全, 参数:", data);
if(data.media)
var value = Buffer.from(data.media);
else if(data.fileID){
var {fileID} = data;
var res = await cloud.downloadFile({fileID});
var value = res.fileContent;
}
var res = await cloud.openapi.security.imgSecCheck({
media:{
contentType:"image/png",
value
}
});
console.log("返回结果: ", res);
return res;
}catch(e){
return e;
}
}
}
}
官方有时会检测去小程序的内容安全接口是不是完善。为了避免接口错误导致误判,可以保留上面云函数中的console日志和一周内上传至云储存的图片留作证据