如何”加快“内容安全接口imgSecCheck的云调用速度
发布于 5 年前 作者 gang99 4811 次浏览 来自 分享

简介

使用云开发来实现内容安全检测比较方便,并且对个人小程序来说是零成本。但是也有一些问题

问题一:通过云函数调用图片安全接口时,如果直接传入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日志和一周内上传至云储存的图片留作证据

回到顶部