写在前面
针对互联网上已有的人脸识别小程序项目,很多只是基于手动拍照,然后上传到SDK进行识别。这一过程完全脱离实际场景!无法直接使用!
本文项目是基于微信摄像头中的实时视频帧数据
,通过实时动态识别小程序端所获取的照片帧,从而自动触发人脸识别和登录!完全贴合实际,真正的拿来即用!
一、需求背景
人脸识别是一个老生常谈的问题了,再继小程序的逐步普及化后,人脸识别的功能也变为了各大业务的家常菜。目前市面上已有的解决方案,一方面除了自研以外,对于本来就是小成本进行创业或者开发的个人以及初创型公司来说,接入一些专业的第三方的人脸识别接口不妨是一个优秀的解决方案。目前市面上常用的第三方支持技术有哪些,可以阅读本专栏的文章: 微信小程序 | 人脸识别的最终解决方案 – 里面做了详细的介绍和解读,相信能带你弄请这个方向的技术行情。
为此,这一次我们自己动手来实现一次全流程的人脸识别小程序
。 没有基础不用担心,本文就是带领大家从零到一开始搭建,细致到每个网页功能如何选择、代码如何编写,前后端的逻辑如何编写。只会前端或者后端的小伙伴也不用担心,本文提供全流程的操作指引,让零基础的前/后端小白都能轻松上手。 废话不多说相信看到上面酷炫的效果大家应该都按捺不住了,那就让我们开整把。
二、系统架构
2.1 系统技术栈
前端 | 后端 |
---|---|
- 语言:vue 2.0 | - 框架 : uni-app | - UI组件: uview | - 语言:python | - 框架: Flask |
2.2 系统架构图
2.3 系统请求时序图
2.4 详细接口请求时序图
三、项目实现
要使用百度智能云的api接口,就需要在控制台创建你的项目应用,通过项目应用去获取API Key``Secret Key ``Access_token
,这三个参数是后续调用api接口的必填参数,缺一不可!
3.1 开通百度智能云API
- 登录到百度智能云人脸识别控制台
- 选择相应的API接口并进行购买操作即可(其中我们选择的是
人脸比对
),其实为了方便我们可以一次性将所有的人脸识别相关的产品全部都进行一次购买,反正都是按量计费,不进行调用也就不用收钱了。
3.2 并获取API Key
-在开通了服务的基础上,由于要获取三大认证参数,再调用接口之前我们仍需要进行应用的创建。
- 由此即可获取到三大参数
3.3 搭建项目调试环境
本项目是继续uniapp
框架进行开发(采用uniapp框架开发的好处在于:在面对众多的小程序平台,我们只需要编写这一套代码就可以实现多平台的运行,极大程度的降低了开发成本!可以说是只要会一套小程序开发的流程,基本每个平台你都能拿下。
)
- 详细的开发环境搭建过程,可以参考:从零开始搭建uni-app框架的小程序开发环境
3.4 参考百度云SDK文档
- 人脸识别接口Pthon SDK文档:人脸接口文档地址
3.5 人脸对比接口详解
接口能力
- 两张人脸图片相似度对比:比对两张图片中人脸的相似度,并返回相似度分值;
- 多种图片类型:支持生活照、证件照、身份证芯片照、带网纹照四种类型的人脸对比;
- 活体检测:基于图片中的破绽分析,判断其中的人脸是否为二次翻拍(举例:如用户A用手机拍摄了一张包含人脸的图片一,用户B翻拍了图片一得到了图片二,并用图片二伪造成用户A去进行识别操作,这种情况普遍发生在金融开户、实名认证等环节。);
- 质量检测:返回模糊、光照等质量检测信息,用于辅助判断图片是否符合识别要求;
业务应用
用于比对多张图片中的人脸相似度并返回两两比对的得分,可用于判断两张脸是否是同一人的可能性大小。
典型应用场景:如人证合一验证,用户认证等,可与您现有的人脸库进行比对验证。
result = client.match([
{
'image': base64.b64encode(open('1.jpg', 'rb').read()).decode(),
'image_type': 'BASE64',
},
{
'image': base64.b64encode(open('2.jpg', 'rb').read()).decode(),
'image_type': 'BASE64',
}
])
请求参数
参数 | 必选 | 类型 | 说明 |
---|---|---|---|
image | 是 | string | 图片信息(总数据大小应小于10M),图片上传方式根据image_type来判断。 两张图片通过json格式上传,格式参考表格下方示例 |
image_type | 是 | string | 图片类型 BASE64:图片的base64值,base64编码后的图片数据,编码后的图片大小不超过2M;URL:图片的 URL地址( 可能由于网络等原因导致下载图片时间过长);FACE_TOKEN: 人脸图片的唯一标识,调用人脸检测接口时,会为每个人脸图片赋予一个唯一的FACE_TOKEN,同一张图片多次检测得到的FACE_TOKEN是同一个。 |
face_type | 否 | string | 人脸的类型LIVE表示生活照:通常为手机、相机拍摄的人像图片、或从网络获取的人像图片等,IDCARD表示身份证芯片照:二代身份证内置芯片中的人像照片, WATERMARK表示带水印证件照:一般为带水印的小图,如公安网小图 CERT表示证件照片:如拍摄的身份证、工卡、护照、学生证等证件图片 默认LIVE |
quality_control | 否 | string | 图片质量控制 NONE: 不进行控制 LOW:较低的质量要求 NORMAL: 一般的质量要求 HIGH: 较高的质量要求 默认 NONE |
liveness_control | 否 | string | 活体检测控制 NONE: 不进行控制 LOW:较低的活体要求(高通过率 低攻击拒绝率) NORMAL: 一般的活体要求(平衡的攻击拒绝率, 通过率) HIGH: 较高的活体要求(高攻击拒绝率 低通过率) 默认NONE |
返回参数
参数名 | 必选 | 类型 | 说明 |
---|---|---|---|
score | 是 | float | 人脸相似度得分 |
face_list | 是 | array | 人脸信息列表 |
+face_token | 是 | string | 人脸的唯一标志 |
返回示例
{
"score": 44.3,
"face_list": [ //返回的顺序与传入的顺序保持一致
{
"face_token": "fid1"
},
{
"face_token": "fid2"
}
]
}
3.6 人脸注册到人脸库功能实现
为了实现保存用户的人脸信息,用于验证人脸请求。一般都是有两种方案:
- 其一,是我们在自己的后台服务维护一套用户人脸信息库,然后在每次前端请求登入时进行读库操作,同时再调用
client.match()
方法对用户的登录信息进行比对。这样的做法可以实现高度自定义化,但是维护成本相对较高,主要包括数据的存储和可视化的展示
- 其二,百度SDK平台给我们提供了一套完整的用于维护用户人脸信息库API,而且还以
Group
分组的形式进行用户数据管理,这就使得对于用户的所归宿的不同组的权限进行区分。同样地,借助组的字段概念可以与业务系统打通,进而更为方便的管理用户。
3.6.1 前端代码实现
微信摄像头实时帧的获取与数据处理
要实现小程序端自动采集人脸数据并进行接口验证,就必须使用微信摄像头图像帧数据的采集与监听功能。
- 官方接口说明如下:
获取 Camera 实时帧数据文档地址
- 程序中实现思路如下:
1、调用
onCameraFrame
接口,生成帧数据监听器,然后使用listener.start()
启动监听服务。
2、在listen接口
中设置一个setInterVal()
时间循环任务,从而实现一个循环获取帧数据并循环上报并处理接口返回数据的任务,保证人脸登录的实时性!
3、特别地,对于获取到的帧数据,仍需要进行处理:将其转为Base64格式数据从而进行数据传输。
- 代码如下:
startFaceCapture(){
console.log('=====show==facelogin=====')
var that = this
this.isAuthCamera = true
var i = 1;
const context = wx.createCameraContext()
console.log('=====load===')
const listener = context.onCameraFrame((frame) => {
// console.log('==获取帧动画===',that.flag)
if (frame && that.flag) {
i++;
that.frameQueue.push(frame)
// console.log('==push任务',that.frameQueue.length)
that.flag = false
// console.log(i++, frame.data, frame.width, frame.height)
}
})
listener.start({
success: function(res) {
console.log("开始监听");
let task = setInterval(function() {
var timeStart = Date.now();
//在此处处理store[0](图像的数据);
// store.shift();
var frame = that.frameQueue.shift()
console.log("开始运行===",frame,that.flag);
that.flag = true;
if(frame != undefined){
let pngData = UPNG.encode([frame.data], frame.width, frame.height),
base64 = Base64Util.arrayBufferToBase64(pngData)
uni.request({
url: 'http://127.0.0.1:8099/faceAuth' ,
method: 'post',
data: {openId:uni.getStorageSync("openId"),base64Img:base64} ,
dataType:'json',
header: {
'content-type':'application/json'//自定义请求头信息
},
success:(res)=>{
console.log('===result===',res)
clearInterval(task)
this.$refs.uToast.show({
...failTips,
complete() {
}
})
},
fail:(err)=>{
console.log("====执行失败===",err)
clearInterval(task)
that.isAuthCamera = false
uni.navigateTo({
url:'./login'
})
}
})
}
// if(i==2){
// clearInterval(task)
// that.isAuthCamera = false
// }
}, 2000);
}
})
},
3.6.1.1 首页登录界面源代码
<template>
<view>
<view class="content cover" :animation="animationBack">
<!-- <view class="bg"></view>
<view class="bg2"></view>
<view class="tips">
<text class="title">登录</text>
<text class="subtitle">欢迎再次回来~</text>
</view>
<view class="form-box">
<view class="btn"style="margin-top:10px;" @click='rotateFn(2)'>密码登录</view>
<view class="other">
<text>找回密码</text>
<text style="color:#00c6fc;">录入人脸</text>
</view>
</view> -->
<capture ref="faceLogin" @changeLoginMode="rotateFn(2)"></capture>
</view>
<view class="content cover" :animation="animationMain">
<view class="bg"></view>
<view class="bg2"></view>
<view class="tips">
<text class="title">登录</text>
<text class="subtitle">欢迎再次回来~</text>
</view>
<view class="form-box">
<view class="input-box">
<image class="left"
src="https://6e69-niew6-1302638010.tcb.qcloud.la/%E7%99%BB%E5%BD%95%E6%A0%B7%E5%BC%8F/%E7%99%BB%E5%BD%959/%E8%B4%A6%E5%8F%B7.png">
</image>
<input placeholder="请输入账号" />
<image class="right"
src="">
</image>
</view>
<view class="input-box">
<image class="left"
src="">
</image>
<input placeholder="请输入密码" />
<image class="right"
src="">
</image>
</view>
<view class="btn">登录</view>
<view class="btn" style="margin-top:10px;" @click='login()'>刷脸登录</view>
<view class="other">
<text>找回密码</text>
<text style="color:#00c6fc;">快速注册</text>
</view>
</view>
</view>
</view>
</template>
<script>
import capture from './capture.vue'
export default {
components: {
capture
},
data() {
return {
animationMain: null, //正
animationBack: null, //反
}
},
methods: {
rotateFn(e) {
this.animation_main = uni.createAnimation({
duration: 400,
timingFunction: 'linear'
})
this.animation_back = uni.createAnimation({
duration: 400,
timingFunction: 'linear'
})
// 点击正面
if (e == 1) {
this.animation_main.rotateY(180).step()
this.animation_back.rotateY(0).step()
this.animationMain = this.animation_main.export()
this.animationBack = this.animation_back.export()
this.$refs.faceLogin.startFaceCapture()
} else {
this.animation_main.rotateY(0).step()
this.animation_back.rotateY(-180).step()
this.animationMain = this.animation_main.export()
this.animationBack = this.animation_back.export()
}
},
login() {
var that = this;
if (uni.getStorageSync("openId") == undefined) {
wx.login({
success(res) {
console.log('===触发登录====', res)
if (res.code) {
uni.request({
url: 'http://127.0.0.1:8000/miniapp/faceEngine/login/' + res.code,
method: 'get',
dataType: 'json',
success: (res) => {
console.log("====执行成功===", res)
that.rotateFn(1)
uni.setStorageSync("authed", true)
uni.setStorageSync("openId", res.data.openid)
},
fail: (err) => {
console.log("====执行失败===", err)
}
})
} else {
console.log('登录失败!')
}
}
})
} else {
that.rotateFn(1)
}
},
}
}
</script>
<style lang="scss">
page {
margin: 0;
padding: 0;
min-height: 100vh;
position: relative;
background-color: rgb(118, 218, 255);
overflow: hidden;
}
page::before,
page::after {
content: '';
position: absolute;
top: 90vh;
min-width: 300vw;
min-height: 300vw;
background-color: #76daff;
animation: rote 10s linear infinite;
}
page::before {
left: -95%;
border-radius: 45%;
opacity: .5;
}
page::after {
left: -95%;
border-radius: 46%;
}
@keyframes rote {
from {
transform: rotateZ(0);
}
to {
transform: rotateZ(360deg);
}
}
.cover {
position: absolute;
top: 0;
left: 0;
}
.content {
width: 100vw;
height: 100vh;
background-color: #ffffff;
transition: all 1s;
backface-visibility: hidden;
.tips {
padding-top: 200rpx;
padding-left: 80rpx;
display: flex;
flex-direction: column;
.title {
line-height: 70rpx;
font-weight: bold;
font-size: 50rpx;
}
.subtitle {
line-height: 70rpx;
font-size: 35rpx;
font-weight: bold;
color: #b0b0b1;
}
}
.bg {
position: fixed;
top: -250rpx;
right: -250rpx;
width: 600rpx;
height: 600rpx;
border-radius: 100%;
background-color: #00baef;
z-index: 2
}
.bg2 {
position: fixed;
top: -150rpx;
right: -300rpx;
width: 600rpx;
height: 600rpx;
border-radius: 100%;
background-color: #ade8f9;
z-index: 1;
}
.form-box {
padding-top: 180rpx;
padding-left: 70rpx;
width: 610rpx;
.input-box {
margin: 40rpx 0;
display: flex;
justify-content: flex-start;
align-items: center;
height: 100rpx;
background-color: #f5f5f5;
border-radius: 100rpx;
width: 100%;
input {
flex: 1;
height: 100%;
font-size: 30rpx;
}
.left {
padding: 0 30rpx;
width: 35rpx;
height: 35rpx;
}
.right {
padding: 0 30rpx;
width: 40rpx;
height: 40rpx;
}
}
.btn {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100rpx;
border-radius: 100rpx;
color: #FFFFFF;
background: linear-gradient(to right, #00c6fc, #9adcf1);
}
.other {
display: flex;
justify-content: space-between;
text {
line-height: 80rpx;
font-size: 28rpx;
}
}
}
}
</style>
3.6.1.2人脸采集界面代码
<template>
<view class="page-content">
<view class="containerV">
<!-- 标题 -->
<view class="headerV">
<view class="top-tips1">
<view>{{tipsText}}</view>
</view>
</view>
<u-toast ref="uToast"></u-toast>
<!-- 拍摄区域 -->
<view class="contentV">
<view class="mark"></view>
<image class="image" v-if="tempImg" mode="widthFix" :src="tempImg" />
<camera v-if='isAuthCamera' device-position="front" class="camera" flash="off" resolution='high' />
</view>
<!-- 操作区域 -->
<view class="footerV">
<view style="width: 100%;">
<view v-if="!tempImg" style="width: 100%;">
<view class="take-photo-bgV">
<!-- 图片上传 -->
<view v-show="true" class="btn-change-upload" @click="handleChooseImage" />
<!--拍照-->
<view class="btn-take-photo" @click="handleTakePhotoClick" />
<!-- 切换镜头 -->
<view class="btn-change-camera" @click="handleChangeCameraClick" />
</view>
</view>
<view class="confirmV" v-else>
<view class="btn-cancel" @click="handleCancelClick">
取消
</view>
<view class="btn-ok" @click="handleOkClick">
确定
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
// import crudFace from '@/api/face.js'
import Base64Util from '@/util/Base64Util.js'
import UPNG from '@/util/UPNG.js'
export default {
components: {},
data() {
return {
frameQueue: [],
flag: false,
tipsText: '请将脸部移入框内', // 错误文案提示
tempImg: '', // 本地图片路径
cameraEngine: null, // 相机引擎
devicePosition: true, // 摄像头朝向
isAuthCamera: false, // 是否拥有相机权限
cameraListen: '',
failTips: {
type: 'error',
message: "请勿遮挡人脸!",
iconUrl: 'https://cdn.uviewui.com/uview/demo/toast/error.png'
},
successTips: {
type: 'success',
message: "识别成功!",
iconUrl: 'https://www.yuucn.com/wp-content/uploads/2023/04/1681863493-92ddda8be70d273.png'
}
};
},
created() {
},
onLoad() {
// this.init();
const context = wx.createCameraContext()
console.log('=====load===')
const listener = context.onCameraFrame((frame) => {
console.log(frame.data instanceof ArrayBuffer, frame.width, frame.height)
})
this.cameraListen = listener
// listener.start()
},
onShow() {
console.log('=====show===', )
},
methods: {
startFaceCapture(){
console.log('=====show==facelogin=====')
var that = this
this.isAuthCamera = true
var i = 1;
const context = wx.createCameraContext()
console.log('=====load===')
const listener = context.onCameraFrame((frame) => {
// console.log('==获取帧动画===',that.flag)
if (frame && that.flag) {
i++;
that.frameQueue.push(frame)
// console.log('==push任务',that.frameQueue.length)
that.flag = false
// console.log(i++, frame.data, frame.width, frame.height)
}
})
listener.start({
success: function(res) {
console.log("开始监听");
let task = setInterval(function() {
var timeStart = Date.now();
//在此处处理store[0](图像的数据);
// store.shift();
var frame = that.frameQueue.shift()
console.log("开始运行===",frame,that.flag);
that.flag = true;
if(frame != undefined){
let pngData = UPNG.encode([frame.data], frame.width, frame.height),
base64 = Base64Util.arrayBufferToBase64(pngData)
// that.$refs.uToast.show({
// ...that.successTips,
// complete() {
// }
// })
// clearInterval(task)
// setTimeout(function(){
// uni.navigateTo({
// url:'./home'
// })
// },1000)
uni.request({
url: 'http://127.0.0.1:8099/faceAuth' ,
method: 'post',
data: {openId:uni.getStorageSync("openId"),base64Img:base64} ,
dataType:'json',
header: {
'content-type':'application/json'//自定义请求头信息
},
success:(res)=>{
console.log('===result===',res)
clearInterval(task)
this.$refs.uToast.show({
...failTips,
complete() {
}
})
},
fail:(err)=>{
console.log("====执行失败===",err)
clearInterval(task)
that.isAuthCamera = false
uni.navigateTo({
url:'./login'
})
}
})
}
// if(i==2){
// clearInterval(task)
// that.isAuthCamera = false
// }
}, 2000);
}
})
},
handleChangeCameraClick() { // 切换设备镜头
this.devicePosition = !this.devicePosition;
this.$emit('changeLoginMode')
},
handleChooseImage() { // 图片上传
uni.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album'],
success: (res) => {
if (res.errMsg === 'chooseImage:ok') {
this.handleOkClick(res.tempFilePaths[0])
}
},
fail: (res) => {
},
});
},
handleTakePhotoClick() { // 拍照点击
listener.stop()
console.log('===to TaskPhoto====')
if (this.tipsText != "" && this.tipsText != "请拍照") {
return;
}
uni.getSetting({
success: (res) => {
if (!res.authSetting['scope.camera']) {
this.isAuthCamera = false
uni.openSetting({
success: (res) => {
if (res.authSetting['scope.camera']) {
this.isAuthCamera = true;
}
}
})
}
}
})
this.cameraEngine.takePhoto({
quality: "high",
success: ({
tempImagePath
}) => {
this.handleOkClick(tempImagePath)
}
})
},
async handleOkClick(filePath) { // 点击确定上传
uni.showLoading({
title: '上传中...',
mask: true
})
let img = await this.$util.uploadopt.image(filePath, 2)
uni.$emit('onface', img);
uni.navigateBack();
},
handleCancelClick() { // 点击 - 取消上传
this.tempImg = ''
},
}
}
</script>
<style lang="scss">
page {
height: 100%;
width: 100%;
}
.page-content {
width: 100%;
height: 100%;
.containerV {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
/* 标题 */
.headerV {
padding: 163rpx 0 56rpx;
text-align: center;
.top-tips1 {
color: #333333;
font-size: 42rpx;
}
}
/* 拍摄区域 */
.contentV {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 564rpx;
height: 564rpx;
border-radius: 50%;
margin: 0 auto;
border: 10rpx solid #1568E0;
background-color: #d8d8d8;
.camera {
width: 100%;
height: 100%;
border-radius: 50%;
}
.mark {
position: absolute;
left: 0;
top: 0;
z-index: 1;
width: 100%;
height: 100%;
border-radius: 50%;
}
.image {
position: absolute;
width: 100%;
height: 100%;
z-index: 3;
border-radius: 50%;
}
}
/* 操作按钮 */
.footerV {
width: 100%;
flex-grow: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.privacyV {
padding-top: 30rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.text {
font-size: 30rpx;
color: #1C1C1C;
text-align: center;
line-height: 42rpx;
margin-left: 15rpx;
text {
font-size: 30rpx;
color: #00AAFF;
text-align: center;
line-height: 42rpx;
}
}
}
.bottom-tips-2 {
margin-top: 20rpx;
color: #999999;
text-align: center;
font-size: 26rpx;
}
.take-photo-bgV {
width: 100%;
margin-top: 30rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
// 由于左边没有按钮,所以左边要便宜更大,以便是拍照按钮居中
.btn-take-photo {
margin: 0rpx 80rpx 0rpx 80rpx;
width: 196rpx;
height: 196rpx;
background: url("https://www.hujinqiang.com/anjiantong/face/photo.png") no-repeat;
background-size: 100% auto;
}
.btn-change-upload {
left: 130rpx;
width: 80rpx;
height: 80rpx;
background: url("https://www.yuucn.com/wp-content/uploads/2023/04/1681863499-92ddda8be70d273.png") no-repeat;
background-size: 100% auto;
}
.btn-change-camera {
right: 130rpx;
width: 80rpx;
height: 80rpx;
background: url("https://www.yuucn.com/wp-content/uploads/2023/04/1681863505-92ddda8be70d273.png") no-repeat;
background-size: 100% auto;
}
}
.confirmV {
margin: 200rpx 100rpx 0rpx 100rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.btn-cancel {
font-size: 32rpx;
color: #1C1C1C;
}
.btn-ok {
font-size: 32rpx;
color: #00AAFF;
}
}
}
}
}
</style>
3.6.1.3 个人中心界面代码
<template>
<view>
<!-- #ifndef H5 -->
<view class="ns">我的账户</view>
<!-- #endif -->
<view class="flexView">
<view class="scrollView">
<view class="head-read">
<view class="flex">
<image class="read-img" :src="userData.headimgurl" mode="aspectFill" />
<view class="flex-box">
<view class="flex-box-text">姓名:{{ userData.name }}({{ userData.id }})</view>
<view class="flex-box-text">手机号:{{ userData.phone }}</view>
</view>
<view class="arrow arrow-one" @click="showModal = true"><span>名词解释</span></view>
</view>
</view>
<view class="white-box">
<view class="tx">
<view class="tx-grid">
<view class="tx-grid-text">
<view class="title">可提现余额</view>
<view class="money">
<text>{{ userData.withdrawable }}</text>
<text class="money-b">元</text>
</view>
</view>
</view>
<view class="tx-grid" @click="navTo('/pages/withdraw/index')"><span class="tx-grid-comm-sign">立即提现</span></view>
</view>
<view class="palace palace-one">
<view class="palace-grid">
<view class="palace-grid-text">
<view class="palace-grid-text-data">
<text>{{ userData.coming }}</text>
<text class="palace-grid-text-data-b">元</text>
</view>
<view class="palace-grid-text-name">即将到账</view>
</view>
</view>
<view class="palace-grid">
<view class="palace-grid-text">
<view class="palace-grid-text-data">
<text>{{ userData.came }}</text>
<text class="palace-grid-text-data-b">元</text>
</view>
<view class="palace-grid-text-name">累计到账</view>
</view>
</view>
<view class="palace-grid">
<view class="palace-grid-text">
<view class="palace-grid-text-data">
<text>{{ userData.withdrawed }}</text>
<text class="palace-grid-text-data-b">元</text>
</view>
<view class="palace-grid-text-name">累计提现</view>
</view>
</view>
</view>
</view>
<view class="top">
<tui-tabs
:tabs="tabs"
:height="88"
:currentTab="currentTab"
:sliderWidth="150"
:sliderHeight="60"
bottom="50%"
color="#888"
selectedColor="#fff"
sliderBgColor="#ff8a4a"
@change="changeTab"
></tui-tabs>
</view>
<view class="list-view">
<view class="list-item" v-for="(item, index) in list" :key="index" hover-class="hover" :hover-stay-time="150" bindtap="detail">
<view class="content-box">
<view class="des-box">
<view class="tit">{{ currentTab == 3 ? '流水号:' + item.extract_no : '订单号:' + item.order_no }}</view>
<view v-if="currentTab == 3" class="source" :style="{ color: item.status == 1 ? '#4caf50' : item.status == 2 ? '#ff1e0f' : '#00b7ff' }">
提现{{ item.status == 1 ? '成功' : item.status == 2 ? '失败' : '处理中' }}
</view>
<view class="time">{{ item.create_time }}</view>
</view>
</view>
<view class="money" :class="{ less: is_withdraw }">{{ is_withdraw ? '-' : '+' }}{{ currentTab == 3 ? item.real_money : item.money }}</view>
</view>
</view>
<view class="tip">仅显示近半年内的收支记录</view>
</view>
<view class="cu-modal" :class="showModal ? 'show' : ''">
<view class="cu-dialog">
<view class="cu-bar bg-white justify-end">
<view class="content">名词解释</view>
<view class="action" @tap="showModal = false"><text class="cuIcon-close text-red"></text></view>
</view>
<view class="padding-xl" style="text-align: left;">
<view>
<text class="text-red">可提现余额:</text>
<text>当前您可以提现的金额</text>
</view>
<view class="margin-top-sm">
<text class="text-red">即将到账:</text>
<text>交易中的金额,交易成功后可提现</text>
</view>
<view class="margin-top-sm">
<text class="text-red">累计到账:</text>
<text>累计交易成功的金额</text>
</view>
<view class="margin-top-sm">
<text class="text-red">累计提现:</text>
<text>累计提现成功的金额</text>
</view>
<view class="margin-top-sm"><text class="text-red">*注:所有金额币种均为人民币,单位为元,符号¥</text></view>
</view>
<view class="cu-bar bg-white justify-end">
<view class="action"><button class="cu-btn bg-green margin-left" @tap="showModal = false">我已知晓</button></view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import tuiTabs from '@/components/tui-tabs/tui-tabs';
export default {
components: {
tuiTabs
},
data() {
return {
is_withdraw: false,
list: [],
userData: {name:'陶人',id:12,phone:18648759961,withdrawable:98989,coming:288,came:770,withdrawed:8888289},
showModal: false,
date: 'incomeMonth',
currentTab: 0,
tabs: [
{
name: '本月收入'
},
{
name: '今日收入'
},
{
name: '昨日收入'
},
{
name: '提现记录'
}
]
};
},
onLoad() {
this.$api.loading(true);
this.loadData();
setTimeout(() => {
this.$api.loading(false);
}, 500);
},
methods: {
async loadData() {
this.userData = await this.$api.json('userData');
this.list = await this.$api.json('incomeMonth');
},
async getFundList() {
if (this.currentTab == 3) {
this.list = await this.$api.json('extractList');
} else {
this.list = await this.$api.json(this.date);
}
},
changeTab(e) {
this.currentTab = e.index;
this.list = [];
if (this.currentTab == 3) {
this.is_withdraw = true;
} else if (this.currentTab == 0) {
this.date = 'incomeMonth';
this.is_withdraw = false;
} else if (this.currentTab == 1) {
this.date = 'incomeToday';
this.is_withdraw = false;
} else if (this.currentTab == 2) {
this.date = 'incomeYesterday';
this.is_withdraw = false;
}
this.$api.loading(true);
this.getFundList();
setTimeout(() => {
this.$api.loading(false);
}, 500);
},
navTo(url) {
uni.navigateTo({
url
});
}
},
onPullDownRefresh() {
this.loadData();
setTimeout(function() {
uni.stopPullDownRefresh();
}, 500);
}
};
</script>
<style lang="scss" scoped>
page {
background-color: #fff;
}
.ns {
width: 100%;
height: 60px;
text-align: center;
line-height: 200rpx;
color: white;
font-size: 34rpx;
font-weight: bold;
// background: linear-gradient(to right, #ff8440, #ff1e0f);
background: linear-gradient(to right, #7CF7FF , #4B73FF );
}
.top {
margin-top: 20rpx;
}
.flexView {
width: 100%;
height: 100%;
margin: 0 auto;
display: flex;
flex-direction: column;
.scrollView {
width: 100%;
height: 100%;
flex: 1;
overflow-y: auto;
overflow-x: hidden;
position: relative;
padding-bottom: 116rpx;
.head-read {
// background: linear-gradient(to right, #ff8440, #ff1e0f);
background: linear-gradient(to right, #7CF7FF , #4B73FF );
background-color: #4B73FF;
padding-bottom: 100rpx;
.flex {
display: flex;
align-items: center;
padding: 50rpx;
position: relative;
.read-img {
width: 120rpx;
height: 120rpx;
border-radius: 100%;
overflow: hidden;
margin-right: 20rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
}
.flex-box {
flex: 1;
min-width: 0;
font-size: 26rpx;
color: #333;
&-text {
margin: 10rpx 0;
color: #f5f5f5;
font-weight: normal;
}
}
.arrow {
position: relative;
padding-right: 30rpx;
span {
font-size: 28rpx;
color: white;
}
&:after {
content: ' ';
display: inline-block;
height: 12rpx;
width: 12rpx;
border-width: 4rpx 4rpx 0 0;
border-color: #848484;
border-style: solid;
transform: matrix(0.71, 0.71, -0.71, 0.71, 0, 0);
position: relative;
top: -4rpx;
position: absolute;
top: 50%;
margin-top: -8rpx;
right: 4rpx;
border-radius: 2rpx;
}
}
.arrow-one:after {
border-color: white;
margin-top: -6rpx;
}
}
}
.white-box {
width: 94%;
background: white;
border-radius: 10rpx;
margin: -120rpx auto 20rpx;
box-shadow: 0 6rpx 20rpx #e7e7e7;
.tx {
padding-top: 26rpx;
overflow: hidden;
display: flex;
justify-content: space-between;
align-content: center;
&-grid {
box-sizing: border-box;
&:first-child {
margin-left: 40rpx;
}
&-comm-sign {
display: block;
border-radius: 40rpx 0 0 40rpx;
font-size: 26rpx;
padding: 16rpx 44rpx;
background: linear-gradient(to right, #fef082, #ffc551);
background-color: #fef082;
color: #4B73FF;
}
&-text {
display: block;
color: #333;
font-size: 26rpx;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
.title {
font-size: 26rpx;
font-weight: normal;
color: #ff6423;
}
.money {
font-size: 60rpx;
color: #ff6423;
letter-spacing: 2rpx;
margin-bottom: 10rpx;
&-b {
font-size: 28rpx;
}
}
}
}
}
.palace {
padding-bottom: 20rpx;
overflow: hidden;
display: flex;
justify-content: center;
&-grid {
flex: 1;
position: relative;
box-sizing: border-box;
&:not(:last-child) {
&:after {
width: 1rpx;
height: 80rpx;
background: #fddece;
content: ' ';
display: inline-block;
position: absolute;
top: 0;
right: 0;
}
}
&-text {
display: block;
text-align: center;
color: #333;
font-size: 32rpx;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
&-name {
font-size: 26rpx;
font-weight: normal;
color: #ff8a4a;
}
&-data {
font-size: 32rpx;
color: #ff8a4a;
letter-spacing: 2rpx;
margin-bottom: 5rpx;
&-b {
font-size: 20rpx;
}
}
}
}
}
}
}
}
.list-view {
position: relative;
width: 100%;
overflow: hidden;
}
.list-item {
width: 100%;
padding: 30rpx 28rpx;
box-sizing: border-box;
background: #fff;
display: flex;
align-items: flex-start;
justify-content: space-between;
border-bottom: 1rpx solid #eaeef1;
}
.item-last::after {
left: 0 !important;
}
.content-box {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.des-box {
min-height: 80rpx;
padding-left: 28rpx;
box-sizing: border-box;
vertical-align: top;
color: #333;
font-size: 24rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.tit {
font-size: 32rpx;
max-width: 420rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.source {
margin: 12rpx 0;
}
.time {
color: #888;
}
.money {
font-size: 38rpx;
font-weight: 500;
color: #ff1e0f;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-left: 20rpx;
}
.less {
color: #4caf50 !important;
}
.tip {
margin-top: 50rpx;
display: flex;
justify-content: center;
align-content: center;
font-size: 24rpx;
color: #888;
}
</style>
3.6.2 后端代码实现
from flask import Flask, jsonify, request
import re,os
from aip import AipFace
basedir = os.path.abspath(os.path.dirname(__file__)) # 定义一个根目录 用于保存图片用
import base64
from io import BytesIO
from PIL import Image
imageType = "BASE64"
""" 你的 APPID AK SK """
APP_ID = '***'
API_KEY = '***'
SECRET_KEY = '***'
client = AipFace(APP_ID, API_KEY, SECRET_KEY)
app = Flask(__name__)
@app.route('/faceRegister', methods=['GET', 'POST'])
def faceRegister():
# 获取图片文件 name = upload
user_data = request.get_json()
print('==user--data==',user_data['base64Img'])
image = user_data['base64Img'].split(',')[1]
userId = user_data['openId']
# 将base64字符串转换为图像对象
img_data = base64.b64decode(user_data['base64Img'].split(',')[1])
img = Image.open(BytesIO(img_data))
# 将图像对象保存为文件
img.save('face.png')
""" 调用人脸检测 : 校验摄像头中是否检测到人脸 """
detect_res = client.detect(image, imageType)
print(detect_res)
if detect_res['error_code'] == 222203 :
return jsonify({"code": 101, "data": "无法解析人脸", "msg": "验证失败"})
if detect_res['face_num'] != 0 :
return jsonify({"code": 101, "data": "解析人脸失败!", "msg": "验证失败"})
groupId = "vip"
""" 调用人脸注册 : 将人脸数据注册到人脸库中 """
client.addUser(image, imageType, groupId, userId);
""" 调用人脸搜索 """
client.search(image, imageType, groupId);
print(detect_res['face_list'][0]['quality']['occlusion'])
return detect_res
@app.route('/faceAuth', methods=['GET', 'POST'])
def faceAuth():
user_data = request.get_json()
groupId = "vip"
image = user_data['base64Img'].split(',')[1]
userId = user_data['openId']
options = {}
options["user_id"] = userId
""" 调用人脸搜索 """
auth_result = client.search(image, imageType, groupId,options)
# {
# "face_token": "fid",
# "user_list": [
# {
# "group_id": "test1",
# "user_id": "u333333",
# "user_info": "Test User",
# "score": 99.3
# }
# ]
# }
if len(auth_result['user_list'])>0:
return jsonify({"code": 200, "msg": "登录成功!"})
else:
return jsonify({"code": 102, "msg": "用户尚未注册!"})
if __name__ == '__main__':
app.run(host="0.0.0.0", port=int("8099"), debug=True)
四、推荐阅读
🥇入门和进阶小程序开发,不可错误的精彩内容🥇 :
- 《小程序开发必备功能的吐血整理【个人中心界面样式大全】》
- 《微信小程序 | 动手实现双十一红包雨》
- 《微信小程序 | 人脸识别的最终解决方案》
- 《来接私活吧?小程序接私活必备功能-婚恋交友【附完整代码】》
- 《吐血整理的几十款小程序登陆界面【附完整代码】》