uniapp —— 小程序使用百度云OCR鉴别身份证和营业执照

开发上传卡证功能时,必须要鉴别上传的图片,否则功能相当于废的;至于为什么选择百度云,那是因为阿里云又贵又难用,百度云的免费额度更多,并且更成熟专业;且针对于用户的角度,两者没有太大的使用区别,只需要追求一个稳定的服务即可。
开发场景:uniapp -> 微信小程序

零、我先把封装好的代码和步骤放出来,方便下文理解

  1. 获取百度云 access_token
  2. 根据获取到的 token 调用鉴别接口;
  3. 鉴别错误处理;
  4. 鉴别成功,上传图片。
/* 引入封装接口 */
import { user_profile_baidu } from '@/utils/request/api.js';

/**
 * 定义百度云账号数据,用于获取鉴权 Token
 * @BAIDU_PROFILE {
	 @grant_type 必填项,值为 client_credentials
	 @client_id 必填项,值为百度云应用的 Api-Key
	 @client_secret 必填项,值为百度云应用的 Secret-Key
 }
 */
const BAIDU_PROFILE = { grant_type: 'client_credentials', client_id: '你的应用 API-Key', client_secret: '你的应用 Secret-Key' }

/**
 * 定义百度云鉴别种类接口
 * @BAIDU_REQUEST_API 百度云鉴别请求接口
 * @BAIDU_LICENSE_API 营业执照鉴别接口
 * @BAIDU_IDCARD_API 身份证鉴别接口
 */
let BAIDU_REQUEST_API
const BAIDU_LICENSE_API = 'https://aip.baidubce.com/rest/2.0/ocr/v1/business_license'
const BAIDU_IDCARD_API = 'https://aip.baidubce.com/rest/2.0/ocr/v1/idcard'

/**
 * 定义百度云鉴别接口参数
 * @BAIDU_BUSSINESS_PARAMS Base64 | 营业执照鉴别参数 {
	 image: 二进制文件流
 }
 * @BAIDU_IDCARD_PARAMS Base64 && String | 身份证鉴别参数 {
	 image: 二进制文件流
	 id_card_side: 'front'-正面, 'back'-反面
 }
 */
const BAIDU_BUSSINESS_PARAMS = { image: '' }
const BAIDU_IDCARD_PARAMS = { image: '', id_card_side: '' }

/**
 * 设置缓存时间
 * @params {
	 @key String | 缓存键
	 @value Object | 缓存值
	 @seconds Number | 缓存时间
 }
 */
const Storage = (key, value, seconds = 3600 * 24 * 29) => {
	let nowTime = Date.parse(new Date()) / 1000;
	if (key && value) {
		let expire = nowTime + Number(seconds);
		uni.setStorageSync(key, value + '|' +expire)
	} else if (key && !value) {
		let val = uni.getStorageSync(key);
		if (val) {
			// 缓存存在,判断是否过期
			let temp = val.split('|')
			if (!temp[1] || temp[1] <= nowTime) {
				uni.removeStorageSync(key)
				return '';
			} else {
				return temp[0];
			}
		}
	}
}

/**
 * 错误处理函数
 * @params {
	 @type String | 识别类型 {
		'license'-营业执照, 'card'-身份证正面, 'card2'-身份证反面
	 }
	 @err Object | 判断的数据源
 }
 */
const ErrExcept = (type, err) => {
	// 判断数据错误情况
	switch(type) {
		case 'license':
			if (err.data.error_code) {
				// 判断常规错误情况
				switch(err.data.error_code) {
					case 1,2,4:
						uni.$u.toast('识别失败,服务器错误')
					break;
					case 17,18,19:
						uni.$u.toast('识别失败,资源超额')
					break;
					default:
						uni.$u.toast('识别失败,请检查图片')
					break;
				}
				throw new Error('server error')
			}
		break;
		case 'card':
			if (err.data.image_status != 'normal') {
				uni.$u.toast('识别失败,请重新上传正确的证件照!')
				throw new Error('image error')
			}
		break;
		case 'card2':
			if (err.data.image_status != 'normal') {
				uni.$u.toast('识别失败,请重新上传正确的证件照!')
				throw new Error('image error')
			}
		break;
		default:
			uni.$u.toast('上传成功');
	}
}

/* 获取 access_token */
export const GET_ACCESS_TOKEN = () => {
	return new Promise((resolve, reject) => {
		try{
			let access_token = Storage('access_token');
			if (!access_token || access_token == '') {
				user_profile_baidu(BAIDU_PROFILE).then(res => {
					access_token = res.data.access_token;
					Storage('access_token', access_token)
					resolve(access_token)
				})
			} else {
				resolve(access_token)
			}
		}catch(e){
			//TODO handle the exception
			reject(e)
		}
	})
}

/**
 * 百度云识别证件函数
 * @params {
	 @type Number | 本地上传图片的途径 {
		 0-拍照, 1-相册 
	 }
	 @apiName String | 识别证件类型 {
		 'license'-营业执照, 'card'-身份证正面, 'card2'-身份证反面
	 }
	 @token | access_token
 }
 */
export const BAIDU_OCR = (type, apiName, token) => {
	return new Promise((resolve, reject) => {
		let pic = uni.chooseImage({
			sizeType: ['compressed'],
			count: 1,
			sourceType: type == 0 ? 'camera' : 'album',
			success(res) {
				uni.getFileSystemManager().readFile({
					filePath: res.tempFilePaths[0],
					encoding: 'base64',
					success(file) {
						uni.showLoading({title: '识别中'})
						let baidu_data; // 定义请求的数据
						if (apiName == 'license') {
							BAIDU_REQUEST_API = BAIDU_LICENSE_API
							baidu_data = BAIDU_BUSSINESS_PARAMS
						} else {
							BAIDU_REQUEST_API = BAIDU_IDCARD_API
							baidu_data = BAIDU_IDCARD_PARAMS
							if (apiName == 'card') {
								baidu_data.id_card_side = 'front'
							} else {
								baidu_data.id_card_side = 'back'
							}
						}
						baidu_data.image = file.data
						uni.request({
							url: `${BAIDU_REQUEST_API}?access_token=${token}`,
							method: 'POST',
							header: {
								'content-type': 'application/x-www-form-urlencoded'
							},
							data: baidu_data,
							success(baidu_res) {
								uni.hideLoading()
								ErrExcept(apiName, baidu_res)
								resolve(res.tempFilePaths[0])
							},
							fail(baidu_res_reject) {
								uni.hideLoading()
								uni.$u.toast('识别失败,请重新上传')
								reject(baidu_res_reject)
							}
						})
					},
					fail(baidu_reject) {
						uni.$u.toast('读取失败,请重新上传')
						reject(baidu_reject)
					}
				})
			},
			fail: (err) => {
				uni.$u.toast('获取照片失败,请重新上传')
				reject(err)
			}
		})
	})
}

一、注册百度云开发者平台

  1. 注册开发者平台账户《传送地址》
  2. 建议进行企业实名认证(可以拥有更多的免费鉴别卡证次数,且不仅是文字识别,其他各项服务也会赠送免费的调用接口次数);文字识别OCR的免费资源情况如下:
个人企业
200次/月2000次/月
  1. 开通文字识别服务《传送地址》
    选择文字识别
  2. 领取免费资源(建议每一项都勾选全选);
    领取免费资源
    领取免费资源
  3. 创建应用,以获取API KeySecret Key
    创建应用,选中功能
    创建应用

二、获取 access_token (最好是由后端获取递给前端)

access_token 是百度云服务器返回给你代码的鉴权加密串,需要将上文获取到的API KeySecret Keyget明文请求方式发送,我们知道get请求会把参数拼接在请求路径里面,容易导致密钥信息泄露;另外,前端获取加密串的时候,会经常性的提示 access_token 已过期,也无法经常保证程序的可用性,用户体验极差。所以为了保持密钥信息的安全性,最好是交由后端请求获取 access_token,处理完之后再递给前端。
access_token 有效期是30天,我们封装一个本地存储函数,定时清除 access_token。

/* 引入封装接口 */
import { user_profile_baidu } from '@/utils/request/api.js';

/**
 * 定义百度云账号数据,用于获取鉴权 Token
 * @BAIDU_PROFILE {
	 @grant_type 必填项,值为 client_credentials
	 @client_id 必填项,值为百度云应用的 Api-Key
	 @client_secret 必填项,值为百度云应用的 Secret-Key
 }
 */
const BAIDU_PROFILE = { grant_type: 'client_credentials', client_id: '你的应用 API-Key', client_secret: '你的应用 Secret-Key' }

/**
 * 设置缓存时间
 * @params {
	 @key String | 缓存键
	 @value Object | 缓存值
	 @seconds Number | 缓存时间
 }
 */
const Storage = (key, value, seconds = 3600 * 24 * 29) => {
	let nowTime = Date.parse(new Date()) / 1000;
	if (key && value) {
		let expire = nowTime + Number(seconds);
		uni.setStorageSync(key, value + '|' +expire)
	} else if (key && !value) {
		let val = uni.getStorageSync(key);
		if (val) {
			// 缓存存在,判断是否过期
			let temp = val.split('|')
			if (!temp[1] || temp[1] <= nowTime) {
				uni.removeStorageSync(key)
				return '';
			} else {
				return temp[0];
			}
		}
	}
}

/* 获取 access_token,这里 export 出去是因为 access_token在别处还有用到 */
export const GET_ACCESS_TOKEN = () => {
	return new Promise((resolve, reject) => {
		try{
			let access_token = Storage('access_token');
			if (!access_token || access_token == '') {
			    // user_profile_baidu是自封装请求函数
				user_profile_baidu(BAIDU_PROFILE).then(res => {
					access_token = res.data.access_token;
					Storage('access_token', access_token)
					resolve(access_token)
				})
			} else {
				resolve(access_token)
			}
		}catch(e){
			//TODO handle the exception
			reject(e)
		}
	})
}

三、调用鉴别接口

在应用列表页点击管理,进入详情页,点击展开,获取功能接口:
获取功能接口

/**
 * 定义百度云鉴别种类接口
 * @BAIDU_REQUEST_API 百度云鉴别请求接口
 * @BAIDU_LICENSE_API 营业执照鉴别接口
 * @BAIDU_IDCARD_API 身份证鉴别接口
 */
let BAIDU_REQUEST_API
const BAIDU_LICENSE_API = 'https://aip.baidubce.com/rest/2.0/ocr/v1/business_license'
const BAIDU_IDCARD_API = 'https://aip.baidubce.com/rest/2.0/ocr/v1/idcard'

/**
 * 定义百度云鉴别接口参数
 * @BAIDU_BUSSINESS_PARAMS Base64 | 营业执照鉴别参数 {
	 image: 二进制文件流
 }
 * @BAIDU_IDCARD_PARAMS Base64 && String | 身份证鉴别参数 {
	 image: 二进制文件流
	 id_card_side: 'front'-正面, 'back'-反面
 }
 */
const BAIDU_BUSSINESS_PARAMS = { image: '' }
const BAIDU_IDCARD_PARAMS = { image: '', id_card_side: '' }

/**
 * 错误处理函数
 * @params {
	 @type String | 识别类型 {
		'license'-营业执照, 'card'-身份证正面, 'card2'-身份证反面
	 }
	 @err Object | 判断的数据源
 }
 */
const ErrExcept = (type, err) => {
	// 判断数据错误情况
	switch(type) {
		case 'license':
			if (err.data.error_code) {
				// 判断常规错误情况
				switch(err.data.error_code) {
					case 1,2,4:
						uni.$u.toast('识别失败,服务器错误')
					break;
					case 17,18,19:
						uni.$u.toast('识别失败,资源超额')
					break;
					default:
						uni.$u.toast('识别失败,请检查图片')
					break;
				}
				throw new Error('server error')
			}
		break;
		case 'card':
			if (err.data.image_status != 'normal') {
				uni.$u.toast('识别失败,请重新上传正确的证件照!')
				throw new Error('image error')
			}
		break;
		case 'card2':
			if (err.data.image_status != 'normal') {
				uni.$u.toast('识别失败,请重新上传正确的证件照!')
				throw new Error('image error')
			}
		break;
		default:
			uni.$u.toast('上传成功');
	}
}

/**
 * 百度云识别证件函数
 * @params {
	 @type Number | 本地上传图片的途径 {
		 0-拍照, 1-相册 
	 }
	 @apiName String | 识别证件类型 {
		 'license'-营业执照, 'card'-身份证正面, 'card2'-身份证反面
	 }
	 @token | access_token
 }
 */
export const BAIDU_OCR = (type, apiName, token) => {
	return new Promise((resolve, reject) => {
		let pic = uni.chooseImage({
			sizeType: ['compressed'],
			count: 1,
			sourceType: type == 0 ? 'camera' : 'album',
			success(res) {
				uni.getFileSystemManager().readFile({
					filePath: res.tempFilePaths[0],
					encoding: 'base64',
					success(file) {
						uni.showLoading({title: '识别中'})
						let baidu_data; // 定义请求的数据
						if (apiName == 'license') {
							BAIDU_REQUEST_API = BAIDU_LICENSE_API
							baidu_data = BAIDU_BUSSINESS_PARAMS
						} else {
							BAIDU_REQUEST_API = BAIDU_IDCARD_API
							baidu_data = BAIDU_IDCARD_PARAMS
							if (apiName == 'card') {
								baidu_data.id_card_side = 'front'
							} else {
								baidu_data.id_card_side = 'back'
							}
						}
						baidu_data.image = file.data
						uni.request({
							url: `${BAIDU_REQUEST_API}?access_token=${token}`,
							method: 'POST',
							header: {
								'content-type': 'application/x-www-form-urlencoded'
							},
							data: baidu_data,
							success(baidu_res) {
								uni.hideLoading()
								ErrExcept(apiName, baidu_res)
								resolve(res.tempFilePaths[0])
							},
							fail(baidu_res_reject) {
								uni.hideLoading()
								uni.$u.toast('识别失败,请重新上传')
								reject(baidu_res_reject)
							}
						})
					},
					fail(baidu_reject) {
						uni.$u.toast('读取失败,请重新上传')
						reject(baidu_reject)
					}
				})
			},
			fail: (err) => {
				uni.$u.toast('获取照片失败,请重新上传')
				reject(err)
			}
		})
	})
}

四、完整应用

使用的组件是 uview2.0,需要的就替换成自己的组件。《传送地址》

<template>
	<!-- 营业执照 -->
	<view class="box_radius" style="margin-top: 20rpx;">
		<text>请点击上传您的营业执照</text>
		<view @click="bindActions('license')">
			<image style="width: 100%;margin-top: 20rpx;" :src="fileinp_yin_path || 'http://nq34.gzfloat.com/public/user/license@2x.png'" mode="widthFix"></image>
		</view>
	</view>
	<!-- 身份证正反面 -->
	<view class="box_radius" style="margin: 20rpx 0;">
		<text>请点击上传您的身份证正反面</text>
		<view>
			<view @click="bindActions('card')">
				<image style="width: 100%;margin: 20rpx 0;" :src="fileinp_zheng_path || 'http://nq34.gzfloat.com/public/user/card@2x.png'" mode="widthFix"></image>
			</view>
			<view @click="bindActions('card2')">
				<image style="width: 100%;" :src="fileinp_fan_path || 'http://nq34.gzfloat.com/public/user/card2@2x.png'" mode="widthFix"></image>
			</view>
		</view>
	</view>
	<!-- Action Sheet组件 -->
	<u-action-sheet @select="selectActions" @close="actionShow = false" :actions="actionList" :safeAreaInsetBottom="true" cancelText="取消" :closeOnClickOverlay="true" :closeOnClickAction="true"  :title="actionTitle" :show="actionShow"></u-action-sheet>
</template>

<script>
import { GET_ACCESS_TOKEN, BAIDU_OCR } from '@/utils/ocr/index.js'

export default {
    data() {
		return {
			name: '',
			actionShow: false,
			actionTitle: '上传营业执照',
			actionList: [{id: 0, name: '拍照'},{id: 1, name: '相册'}],
		    name: '',
		    actionTitle: '',
		    fileinp_yin_path: '',
			fileinp_zheng_path: '',
			fileinp_fan_path: ''
		}
	},
	methods: {
	    bindActions(name) {
			this.actionShow = true
			if (name == 'license') {
				this.name = 'license'
				this.actionTitle = '上传营业执照'
			} else if (name == 'card') {
				this.name = 'card'
				this.actionTitle = '上传身份证正面'
			} else {
				this.name = 'card2'
				this.actionTitle = '上传身份证反面'
			}
		},
		async selectActions(index) {
			// 获取鉴权 token
			const BAIDU_ACCESS_TOKEN = await GET_ACCESS_TOKEN()
			// 选择图片以及鉴别图片
			const chooseResult = await BAIDU_OCR(index.id, this.name, BAIDU_ACCESS_TOKEN)
			// 上传图片
			const result = await this.uploadFilePromise(chooseResult)
			if (this.name == 'license') {
				this.fileinp_yin_path = result
			} else if (this.name == 'card') {
				this.fileinp_zheng_path = result
			} else {
				this.fileinp_fan_path = result
			}
		}
	}
}
</script>

五、实现效果

效果
效果
效果
效果
效果

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐