微信小程序 | 人脸识别的最终解决方案

Posted 陶人超有料

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了微信小程序 | 人脸识别的最终解决方案相关的知识,希望对你有一定的参考价值。

📌个人主页个人主页
​🧀 推荐专栏小程序开发成神之路 --(这是一个为想要入门和进阶小程序开发专门开启的精品专栏!从个人到商业的全套开发教程,实打实的干货分享,确定不来看看? 😻😻)
📝作者简介:一个读研中创业、打工中学习的能搞全栈、也搞算法、目前在搞大数据的奋斗者。
⭐️您的小小关注是我持续输出的动力!⭐️



干货内容推荐

🥇入门和进阶小程序开发,不可错误的精彩内容🥇 :


一、人脸识别功能现状

1.1 微信原生接口篇


微信原生的人脸识别接口可以完美的结合小程序所采集到的图像,可以达到实时帧级别的识别,是高效开发人脸识别功能的首选!

但是,由于人脸数据是个人的敏感数据,微信在开放该接口出来时,就是为了方便各行各业的业务开展。所以,我们要用它,就必须具备相应的资质。后续才能通过审核并发布上线。

资质说明链接:微信开放能力文档


1.2 百度接口篇

百度人脸识别SDK的服务模式是用户在平台开通好相应的权限以及获取到appId等一系列操作之后,再到我们开发端是采用对百度人脸验证平台的相应接口进行发送验证请求才能实现服务的调用。

也就是说:百度已经人脸验证服务帮你全部搭建好了,你只需要发起请求进行调即可。

百度人脸识别服务地址

可以按量收费,目前根据你的业务量的QPS进行计费统计。

如果您的业务并发支持要求较高,免费测试 QPS 不能满足,您可以随时购买扩充 QPS ,QPS 可包月购买,也可按天购买,灵活多样,适应多场景需求。


1.3 虹软接口篇

平台链接地址:虹软接口平台

虹软 和 百度 这两者之间的区别在于:

  • 百度直接替你部署好了识别用的服务,你只需要按照文档指引向其特定的 接口发送数据即可获得你的结果,在某些主前端开发轻后端服务的应用来说还是很有优势的。
  • 虹软所走的模式是不管你是离线还是在线都能让你运行,他把搭建服务端的工作留给你自己去做。在这样的模式下,你可以实时把控用户的人脸数据,对其进行自定义操作,从而构造更为灵活的人脸设别方式,对业务场景的开发也可以有更多操作的空间!

二、人脸识别功能流程


三、微信接口解析

3.1 微信摄像头组件的使用

  • 小程序中要使用到摄像头的功能在于使用<camera>标签:
<camera v-if='isAuthCamera' device-position="front" class="camera" flash="off" resolution='high' />

对于<camera>标签核心的参数如下:

属性类型必填说明可选值默认值
modestring应用模式,只在初始化时有效,不能动态变更normal: 相机模式 】【scanCode:扫码模式normal
属性类型必填说明可选值默认值
resolutionstring分辨率,不支持动态修改low 低 】【medium 中】【high 高medium
属性类型必填说明可选值默认值
device-positionstring摄像头朝向front 前置 】【back 后置back
属性类型必填说明可选值默认值
flashstring闪光灯,值为 auto , on, offauto 自动 】【on 打开】【off 关闭】【torch 常亮auto

3.2 微信小程序实时视频帧获取

对如何调用和开启微信小程序中的摄像头摄像和拍照功能进行学习:微信小程序–摄像头接口详解

  • 首先需要调用CameraContext wx.createCameraContext()方法创建camera操作对象。
  • 然后在camera对象之后调用onCameraFrame(function callback),该方法用于实时获取摄像头所捕捉的相片帧,从而用于与后端人脸识别引擎进行交互。
const context = wx.createCameraContext()
const listener = context.onCameraFrame((frame) => 
 	 console.log(frame.data instanceof ArrayBuffer, frame.width, frame.height)
)
listener.start()

四、工具包的准备

4.1 将帧数据转为Base64

  • 针对onCameraFrame()方法,获取到的是相机所捕捉的图片帧数据,这个时候我们需要将其转为Base64格式的图片数据,这是为了让其传输到后端能够被人脸识别引擎所使用!
  • 在微信所开放的用于将帧数据转化为base 64接口中wx.arrayBufferToBase64(已弃用),项目中需要用摄像头获取人脸并将获取的ArrayBuffer数据转化为base64,就需要经过一下流程:

    代码如下:
	let pngData = ToPNG.encode([frame.data], frame.width, frame.height),  
			base64 = Base64Util.arrayBufferToBase64(pngData)
  • 所以先准备ArrayBuffer数据转化为PNG数据的工具包ToPNG.js
import pako from 'pako'
var UPNG = ;



UPNG.toRGBA8 = function (out) 
    var w = out.width,
        h = out.height;
    if (out.tabs.acTL == null) return [UPNG.toRGBA8.decodeImage(out.data, w, h, out).buffer];

    var frms = [];
    if (out.frames[0].data == null) out.frames[0].data = out.data;

    var len = w * h * 4,
        img = new Uint8Array(len),
        empty = new Uint8Array(len),
        prev = new Uint8Array(len);
    for (var i = 0; i < out.frames.length; i++) 
        var frm = out.frames[i];
        var fx = frm.rect.x,
            fy = frm.rect.y,
            fw = frm.rect.width,
            fh = frm.rect.height;
        var fdata = UPNG.toRGBA8.decodeImage(frm.data, fw, fh, out);

        if (i != 0)
            for (var j = 0; j < len; j++) prev[j] = img[j];

        if (frm.blend == 0) UPNG._copyTile(fdata, fw, fh, img, w, h, fx, fy, 0);
        else if (frm.blend == 1) UPNG._copyTile(fdata, fw, fh, img, w, h, fx, fy, 1);

        frms.push(img.buffer.slice(0));

        if (frm.dispose == 0)  else if (frm.dispose == 1) UPNG._copyTile(empty, fw, fh, img, w, h, fx, fy, 0);
        else if (frm.dispose == 2)
            for (var j = 0; j < len; j++) img[j] = prev[j];
    
    return frms;

UPNG.toRGBA8.decodeImage = function (data, w, h, out) 
    var area = w * h,
        bpp = UPNG.decode._getBPP(out);
    var bpl = Math.ceil(w * bpp / 8); // bytes per line

    var bf = new Uint8Array(area * 4),
        bf32 = new Uint32Array(bf.buffer);
    var ctype = out.ctype,
        depth = out.depth;
    var rs = UPNG._bin.readUshort;

    //console.log(ctype, depth);
    var time = Date.now();

    if (ctype == 6)  // RGB + alpha
        var qarea = area << 2;
        if (depth == 8)
            for (var i = 0; i < qarea; i += 4) 
                bf[i] = data[i];
                bf[i + 1] = data[i + 1];
                bf[i + 2] = data[i + 2];
                bf[i + 3] = data[i + 3];
            
        if (depth == 16)
            for (var i = 0; i < qarea; i++) 
                bf[i] = data[i << 1];
            
     else if (ctype == 2)  // RGB
        var ts = out.tabs["tRNS"];
        if (ts == null) 
            if (depth == 8)
                for (var i = 0; i < area; i++) 
                    var ti = i * 3;
                    bf32[i] = (255 << 24) | (data[ti + 2] << 16) | (data[ti + 1] << 8) | data[ti];
                
            if (depth == 16)
                for (var i = 0; i < area; i++) 
                    var ti = i * 6;
                    bf32[i] = (255 << 24) | (data[ti + 4] << 16) | (data[ti + 2] << 8) | data[ti];
                
         else 
            var tr = ts[0],
                tg = ts[1],
                tb = ts[2];
            if (depth == 8)
                for (var i = 0; i < area; i++) 
                    var qi = i << 2,
                        ti = i * 3;
                    bf32[i] = (255 << 24) | (data[ti + 2] << 16) | (data[ti + 1] << 8) | data[ti];
                    if (data[ti] == tr && data[ti + 1] == tg && data[ti + 2] == tb) bf[qi + 3] = 0;
                
            if (depth == 16)
                for (var i = 0; i < area; i++) 
                    var qi = i << 2,
                        ti = i * 6;
                    bf32[i] = (255 << 24) | (data[ti + 4] << 16) | (data[ti + 2] << 8) | data[ti];
                    if (rs(data, ti) == tr && rs(data, ti + 2) == tg && rs(data, ti + 4) == tb) bf[qi + 3] = 0;
                
        
     else if (ctype == 3)  // palette
        var p = out.tabs["PLTE"],
            ap = out.tabs["tRNS"],
            tl = ap ? ap.length : 0;
        //console.log(p, ap);
        if (depth == 1)
            for (var y = 0; y < h; y++) 
                var s0 = y * bpl,
                    t0 = y * w;
                for (var i = 0; i < w; i++) 
                    var qi = (t0 + i) << 2,
                        j = ((data[s0 + (i >> 3)] >> (7 - ((i & 7) << 0))) & 1),
                        cj = 3 * j;
                    bf[qi] = p[cj];
                    bf[qi + 1] = p[cj + 1];
                    bf[qi + 2] = p[cj + 2];
                    bf[qi + 3] = (j < tl) ? ap[j] : 255;
                
            
        if (depth == 2)
            for (var y = 0; y < h; y++) 
                var s0 = y * bpl,
                    t0 = y * w;
                for (var i = 0; i < w; i++) 
                    var qi = (t0 + i) << 2,
                        j = ((data[s0 + (i >> 2)] >> (6 - ((i & 3) << 1))) & 3),
                        cj = 3 * j;
                    bf[qi] = p[cj];
                    bf[qi + 1] = p[cj + 1];
                    bf[qi + 2] = p[cj + 2];
                    bf[qi + 3] = (j < tl) ? ap[j] : 255;
                
            
        if (depth == 4)
            for (var y = 0; y < h; y++) 
                var s0 = y * bpl,
                    t0 = y * w;
                for (var i = 0; i < w; i++) 
                    var qi = (t0 + i) << 2,
                        j = ((data[s0 + (i >> 1)] >> (4 - ((i & 1) << 2))) & 15),
                        cj = 3 * j;
                    bf[qi] = p[cj];
                    bf[qi + 1] = p[cj + 1];
                    bf[qi + 2] = p[cj + 2];
                    bf[qi + 3] = (j < tl) ? ap[j] : 255;
                
            
        if (depth == 8)
            for (var i = 0; i < area; i++) 
                var qi = i << 2,
                    j = data[i],
                    cj = 3 * j;
                bf[qi] = p[cj];
                bf[qi + 1] = p[cj + 1];
                bf[qi + 2] = p[cj + 2];
                bf[qi + 3] = (j < tl) ? ap[j] : 255;
            
     else if (ctype == 4)  // gray + alpha
        if (depth == 8)
            for (var i = 0; i < area; i++) 
                var qi 
目录

一、背景

在小程序的一些应用场景中,会有语音转文字的需求。原有的做法一般是先通过小程序的录音功能录下语音文件,然后再通过调用语音智能识别WebApi(比如百度云AI平台,科大讯飞平台)将语音文件转成文字信息,以上的做法比较繁琐且用户的体验性较差。
为解决此问题,微信直接开放了同声传译的插件,小程序作者可以直接使用该插件进行语音同声传译的开发。此文章将通过前后端整合应用的完整案例完成语音的实时转换,并将语音上传到服务端后台备份。

二、同声传译插件介绍

微信同声传译由微信智聆语音团队、微信翻译团队与公众平台联合推出的同传开放接口,首期开放语音转文字、文本翻译、语音合成接口,为开发者赋能。

1、 微信小程序后台添加插件

进入微信小程序后台-->进入设置-->第三方设置-->添加插件->搜索同声传译-->完成添加。
技术图片
技术图片

2、 微信小程序启用插件

在小程序app.json文件中增加插件版本等信息:

"plugins": {
    "WechatSI": {
      "version": "0.3.3",
      "provider": "wx069ba97219f66d99"
    }
  },

在页面程序文件中引入插件:

/* index.js */

const plugin = requirePlugin("WechatSI")

// 获取**全局唯一**的语音识别管理器**recordRecoManager**
const manager = plugin.getRecordRecognitionManager()

recordRecoManager 对象的方法列表:

方法 参数 说明
start options 开始识别
stop 结束识别
onStart callback 正常开始录音识别时会调用此事件
onRecognize callback 有新的识别内容返回,则会调用此事件
onStop callback 识别结束事件
onError callback 识别错误事件

官方开发文档:插件的语音识别管理器

三、语音同步转换的前端实现

1、界面UI与操作

UI参考微信官方的DEMO:长按按钮进行录音,松开按钮实时将录音转换为文字。
技术图片

用户可对同步转换的文字进行编辑,同时可将原始语音文件与文字上传后台服务端。
技术图片

2、代码实现

语音同步转换的主要代码:

//导入插件
const plugin = requirePlugin("WechatSI");
// 获取**全局唯一**的语音识别管理器**recordRecoManager**
const manager = plugin.getRecordRecognitionManager();

/**
   * 加载进行初始化
   */
 onLoad: function () {
 	//获取录音权限
	app.getRecordAuth();
	//初始化语音识别回调
    this.initRecord();
  },

 ...
 
/**
   * 初始化语音识别回调
   * 绑定语音播放开始事件
   */
  initRecord: function () {
    //有新的识别内容返回,则会调用此事件
    manager.onRecognize = (res) => {
      let currentData = Object.assign({}, this.data.currentTranslate, {
        text: res.result,
      });
      this.setData({
        currentTranslate: currentData,
      });
      this.scrollToNew();
    };

    // 识别结束事件
    manager.onStop = (res) => {
      let text = res.result;

      console.log(res.tempFilePath);

      if (text == "") {
        this.showRecordEmptyTip();
        return;
      }

      let lastId = this.data.lastId + 1;

      let currentData = Object.assign({}, this.data.currentTranslate, {
        text: res.result,
        translateText: "正在识别中",
        id: lastId,
        voicePath: res.tempFilePath,
        duration: res.duration
      });

      this.setData({
        currentTranslate: currentData,
        recordStatus: 1,
        lastId: lastId,
      });
      //将当前识别内容与语音文件加入列表
      this.addRecordFile(currentData, this.data.dialogList.length);
      //刷新列表
	  this.scrollToNew();
    };

    // 识别错误事件
    manager.onError = (res) => {
      this.setData({
        recording: false,
        bottomButtonDisabled: false,
      });
    };

  },

  /**
   * 按住按钮开始语音识别
   */
  streamRecord: function (e) {
    let detail = e.detail || {};
    let buttonItem = detail.buttonItem || {};
    //开始中文录音
    manager.start({
      lang: buttonItem.lang,
    });

    this.setData({
      recordStatus: 0,
      recording: true,
      currentTranslate: {
        // 当前语音输入内容
        create: util.recordTime(new Date()),
        text: "正在聆听中",
        lfrom: buttonItem.lang,
        lto: buttonItem.lto,
      },
    });
    //刷新列表
    this.scrollToNew();
  },

  /**
   * 松开按钮结束语音识别
   */
  streamRecordEnd: function (e) {
    let detail = e.detail || {}; // 自定义组件触发事件时提供的detail对象
    let buttonItem = detail.buttonItem || {};

    // 防止重复触发stop函数
    if (!this.data.recording || this.data.recordStatus != 0) {
      console.warn("has finished!");
      return;
    }

    manager.stop();

    this.setData({
      bottomButtonDisabled: true,
    });
  },

编辑识别文字并完上传的主要代码:

 /**
   * 页面的初始数据
   */
  data: {
    edit_text_max: 200,
    remain_length: 200,
    edit_text: "",
    is_focus: false,
    tips: "",
    index: -1,
    voicePath: "",
    
  },

/**
   * 加载初始化
   */
 onLoad: function (options) {
    //根据传入的文字内容填充编辑框
    this.setEditText(options.content)
    
    this.setData({
        index: index,
        oldText:options.content,
        voicePath: options.voicePath
    })
    
  },

 /**
   * 编辑文字
   */
  editInput: function (event) {
    console.log(event)
    if (event.detail.value.length > this.getEditTextMax()) {

    } else {
      this.data.edit_text = event.detail.value
      this.updateRemainLength(this.data.edit_text)
    }
  },

 /**
   * 上传文字与语音文件
   */
  editConfirm: function (event) {
    let json=this.data.edit_text
    //调用微信上传文件api将信息上传至服务端webApi
    wx.uploadFile({
      url: api.wxFileUploadUrl,
      filePath: this.data.voicePath,
      name: "file",
      header: {
        Authorization: wx.getStorageSync("loginFlag"),
        "Content-Type": "multipart/form-data",
      },
      formData: {
        openId: app.globalData.userInfo.openId,
        realName: "语音文件",
        json: JSON.stringify(json),
      },
      success: (result) => {
        console.log("success:", result);
        if (result.statusCode == "200") {
          let data = JSON.parse(result.data);
          console.log("data", data);
          if (data.success == true) {
            let module = data.module;
            console.log("module", module);
            app.showInfo("上传成功");            
            setTimeout( ()=>{
              wx.navigateBack();
            }, 2000)
                      
          } else {
            app.showInfo("异常错误" + data.errMsg + ",请重新进入");
            wx.navigateTo({
              url: "/pages/index/index",
            });
          }
        } else {
          app.showInfo("访问后台异常,重新进入系统");
          wx.navigateTo({
            url: "/pages/index/index",
          });
        }
      },
      fail: (result) => {
        console.log("fail", result);
        wx.navigateTo({
          url: "/pages/index/index",
        });
      },
      complete: () => {},
    });

  },

四、后端SpringBoot实现语音文件上传webApi

1、SpringBoot项目API相关结构树

技术图片

2、文件上传工具类的实现

tools工具类包中主要存文件通用的文件上传工具类,该工具类会将文件上传至配置指定的文件夹下,并将文件信息写入upload_file表中。

  • 文件信息实体类:与数据库中表upload_file对应;
  • 文件存储仓库类:通过Spring Data JPA接口实现数据的CRUD;
  • 文件上传工具接口:对外统一封装文件上传方法;
  • 文件上传工具实现类:实现文件上传方法接口。

文件信息实体类:UploadFile.java

/**
 * 文件信息表
 *
 * @author zhuhuix
 * @date 2020-04-20
 */
@Entity
@Getter
@Setter
@Table(name = "upload_file")
public class UploadFile {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @NotNull(groups = Update.class)
    private Long id;

    /**
     * 文件实际名称
     */
    @Column(name = "real_name")
    private String realName;

    /**
     * 文件名
     */
    @NotNull
    @Column(name = "file_name")
    private String fileName;

    /**
     * 文件主名称
     */
    @NotNull
    @Column(name = "primary_name")
    private String primaryName;

    /**
     * 文件扩展名
     */
    @NotNull
    private String extension;

    /**
     * 存放路径
     */
    @NotNull
    private String path;

    /**
     * 文件类型
     */
    private String type;

    /**
     * 文件大小
     */
    private Long size;

    /**
     * 上传人
     */
    private String uploader;

    @JsonIgnore
    @Column(name = "create_time")
    @CreationTimestamp
    private Timestamp createTime;

    public UploadFile(String realName, @NotNull String fileName, @NotNull String primaryName, @NotNull String extension, @NotNull String path, String type, Long size, String uploader) {
        this.realName = realName;
        this.fileName = fileName;
        this.primaryName = primaryName;
        this.extension = extension;
        this.path = path;
        this.type = type;
        this.size = size;
        this.uploader = uploader;
    }

    @Override
    public String toString() {
        return "UploadFile{" +
                "fileName=‘" + fileName + ‘‘‘ +
                ", uploader=‘" + uploader + ‘‘‘ +
                ", createTime=" + createTime +
                ‘}‘;
    }
}

文件存储仓库类:UploadFileRepository.java

/**
 * 上传文件DAO接口层
 *
 * @author zhuhuix
 * @date 2020-04-03
 */
public interface UploadFileRepository extends JpaRepository<UploadFile, Long>, JpaSpecificationExecutor<UploadFile> {
//该接口继承JpaRepository及CrudRepository接口,已实现了如findById,save,delete等CRUD方法
}

UploadFileRepository 接口继承JpaRepository及CrudRepository接口,已实现了如findById,save,delete等CRUD方法
技术图片
文件上传工具接口:UploadFileTool.java

/**
 * 文件上传接口定义
 *
 * @author zhuhuix
 * @date 2020-04-20
 */
public interface UploadFileTool {

    /**
     * 文件上传
     * @param multipartFile 文件
     * @return 上传信息
     */
   UploadFile upload(String uploader,String realName,MultipartFile multipartFile);
}

文件上传工具实现类:UploadFileToolImpl.java

/**
 * 文件上传实现类
 *
 * @author zhuhuix
 * @date 2020-04-20
 */
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class UploadFileToolImpl implements UploadFileTool {

    private final UploadFileRepository uploadFileRepository;

    @Value("${uploadFile.path}")
    private String path;

    @Value("${uploadFile.maxSize}")
    private long maxSize;

    public UploadFileToolImpl(UploadFileRepository uploadFileRepository) {
        this.uploadFileRepository = uploadFileRepository;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public UploadFile upload(String uploader, String realName, MultipartFile multipartFile) {
        //检查文件大小
        if (multipartFile.getSize() > maxSize * Constant.MB) {
            throw new RuntimeException("超出文件上传大小限制" + maxSize + "MB");
        }
        //获取上传文件的主文件名与扩展名
        String primaryName = FileUtil.mainName(multipartFile.getOriginalFilename());
        String extension = FileUtil.extName(multipartFile.getOriginalFilename());
        //根据文件扩展名得到文件类型
        String type = getFileType(extension);
        //给上传的文件加上时间戳
        LocalDateTime date = LocalDateTime.now();
        DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyyMMddhhmmssS");
        String nowStr = "-" + date.format(format);
        String fileName = primaryName + nowStr + "." + extension;

        try {
            String filePath = path + type + File.separator + fileName;
            File dest = new File(filePath).getCanonicalFile();
            if (!dest.getParentFile().exists()) {
                dest.getParentFile().mkdirs();
            }
            multipartFile.transferTo(dest);
            if (ObjectUtil.isNull(dest)) {
                throw new RuntimeException("上传文件失败");
            }

            UploadFile uploadFile = new UploadFile(realName, fileName, primaryName, extension, dest.getPath(), type, multipartFile.getSize(), uploader);
            return uploadFileRepository.save(uploadFile);

        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }

    }

    /**
     * 根据文件扩展名给文件类型
     *
     * @param extension 文件扩展名
     * @return 文件类型
     */
    private static String getFileType(String extension) {
        String document = "txt doc pdf ppt pps xlsx xls docx csv";
        String music = "mp3 wav wma mpa ram ra aac aif m4a";
        String video = "avi mpg mpe mpeg asf wmv mov qt rm mp4 flv m4v webm ogv ogg";
        String image = "bmp dib pcp dif wmf gif jpg tif eps psd cdr iff tga pcd mpt png jpeg";
        if (image.contains(extension)) {
            return "image";
        } else if (document.contains(extension)) {
            return "document";
        } else if (music.contains(extension)) {
            return "music";
        } else if (video.contains(extension)) {
            return "video";
        } else {
            return "other";
        }
    }
}

注意,该程序代码中用到了@Value注解获取配置文件中的uploadFile.path及uploadFile.maxsize参数,一般在项目静态配置文件中按如下书写(yml配置文件)。

# 测试环境文件存储路径
uploadFile:
  path: C:startupfile  # 文件大小 /M
  maxSize: 50
3、小程序上传文件接口的实现

wx-miniprogram包定义了小程序CRM webApi的接口,小程序调用webApi实现文件的上传及其他功能。

  • 微信小程序 webApi:对外提供小程序上传文件webApi;
  • 微信小程序服务接口:封装小程序上传文件服务接口;
  • 微信小程序服务实现:小程序上传文件服务的实现,该服务实现中会调用tools包中的UploadFile接口进行文件的上传。

微信小程序CRM webApi:WxMiniCrmController.java

/**
 * 微信小程序Crm webApi
 *
 * @author zhuhuix
 * @date 2020-03-30
 */
@Slf4j
@RestController
@RequestMapping("/api/wx-mini")
@Api(tags = "微信小程序Crm接口")
public class WxMiniCrmController {

    private final WxMiniCrm wxMiniCrm;

    public WxMiniCrmController(WxMiniCrm wxMiniCrm) {
        this.wxMiniCrm = wxMiniCrm;
    }

    @ApiOperation(value = "微信小程序端上传文件")
    @PostMapping(value = "/fileUpload")
    public ResponseEntity fileUpload(HttpServletRequest request) {
        MultipartHttpServletRequest req = (MultipartHttpServletRequest) request;

        MultipartFile multipartFile = req.getFile("file");
        String openId = req.getParameter("openId");
        String realName = req.getParameter("realName");
        String json = req.getParameter("json");

        return ResponseEntity.ok(wxMiniCrm.uploadFile(json, openId,realName, multipartFile));

    }
}

微信小程序CRM服务接口:WxMiniCrm.java

/**
 * 微信小程序CRM服务接口定义
 *
 * @author zhuhuix
 * @date 2020-04-20
 */
public interface WxMiniCrm {

    /**
     * 将微信小程序传入的json对象写入数据库,并同时将文件上传至服务端
     *
     * @param json          微信端传入json对象
     * @param openId        上传人
     * @param realName      文件实际名称
     * @param multipartFile 上传文件
     * @return 返回上传信息
     */
    Result<UploadFile> uploadFile(String  json, String openId, String realName,MultipartFile multipartFile);
}

微信小程序CRM服务实现:WxMiniCrmImpl.java

/**
 * 微信小程序CRM实现类
 *
 * @author zhuhuix
 * @date 2020-04-20
 */
@Slf4j
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class WxMiniCrmImpl implements WxMiniCrm {

    private final UploadFileTool uploadFileTool;

    public WxMiniCrmImpl(UploadFileTool uploadFileTool) {
        this.uploadFileTool = uploadFileTool;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result<UploadFile> uploadFile(String  json, String openId,String realName, MultipartFile multipartFile) {
        return new Result<UploadFile>().ok(uploadFileTool.upload(openId,realName, multipartFile));
    }
}
4、小程序上传文件接口的查看

访问Swagger2可查看该接口,Swagger2与SpringBoot的集成可参考SpringBoot JWT认证机制项目集成Swagger2
技术图片

五、实际测试

语音测试正常
技术图片
上传文件至后台:
技术图片
上传的日志信息查看:
技术图片














以上是关于微信小程序 | 人脸识别的最终解决方案的主要内容,如果未能解决你的问题,请参考以下文章

微信小程序语音同步智能识别的实现案例

大家可有图片文字识别的好方法?

微信小程序二维码识别

微信语音红包小程序开发如何提高精准度 红包小程序语音识别精准度 微信小程序红包开发语音红包

微信小程序 人脸识别登陆

微信小程序人脸识别功能