提交 ac245c59 编写于 作者: crlfe's avatar crlfe 😲

feat: 实人认证

上级 38204308
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
module.exports = {
extends: "standard",
};
{
"name" : "hello uni-id-pages",
"appid" : "__UNI__17D54C2",
"appid" : "__UNI__1108543",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"uni-app-x" : {},
"uni-app-x" : {
"modules" : {
"FacialRecognitionVerify" : {}
}
},
/* 快应用特有相关 */
"quickapp" : {},
/* 小程序特有相关 */
......
......@@ -3,13 +3,13 @@ export default {
"agreements":{
"serviceUrl": "https://uniapp.dcloud.io/", // 用户服务协议链接
"privacyUrl": "https://uniapp.dcloud.io/", // 隐私政策条款链接
/*
/*
* 哪些场景下显示
* 1. register注册(包括登录并注册,如:短信验证码登录,一键登录)
* 2. login登录(如:用户名密码登录,短信验证码登录)
*/
"scopeList": [
"register","login"
"register","login","realNameVerify"
]
},
"needLogin":[
......
function checkIdCard (idCardNumber: string): boolean {
if (typeof idCardNumber !== 'string' || idCardNumber.length !== 18) return false
const coefficient = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
const checkCode = [1, 0, 'x', 9, 8, 7, 6, 5, 4, 3, 2]
const code = idCardNumber.substring(17)
let sum = 0
for (let i = 0; i < 17; i++) {
sum += parseInt(idCardNumber.charAt(i)) * coefficient[i]
}
return checkCode[sum % 11].toString() == code.toLowerCase()
}
export default checkIdCard
<template>
<view>
<template v-if="isCertify">
<view class="list">
<view class="list__item">
<view class="list__title">姓名</view>
<view class="list__content">
<text class="value">{{realnameInfo['realName']}}</text>
</view>
</view>
<view class="list__item">
<view class="list__title">身份证号码</view>
<view class="list__content">
<text class="value">{{realnameInfo['identity']}}</text>
</view>
</view>
</view>
</template>
<template v-else>
<template v-if="verifyFail">
<view class="fail-tip-content">
<view class="face-icon">
<image src="@/uni_modules/uni-id-pages-x/static/face-verify-icon.png" class="face-icon-image"/>
</view>
<view class="error-title">
<text class="text">{{verifyFailTitle}}</text>
</view>
<view class="error-description">
<text class="text">{{verifyFailContent}}</text>
</view>
<button type="primary" @click="retry" v-if="verifyFailCode != 10013">重新开始验证</button>
<button type="primary" @click="retry" v-else>返回</button>
<view class="dev-tip" v-if="isDev">请在控制台查看详细错误(此提示仅在开发环境展示)</view>
</view>
</template>
<template v-else>
<uni-id-pages-x-input class="input" v-model=" realName as string" placeholder="姓名" :focus="true"
:maxlength="25"></uni-id-pages-x-input>
<uni-id-pages-x-input class="input" v-model="idCard as string" placeholder="身份证号码"
:maxlength="25"></uni-id-pages-x-input>
<uni-id-pages-x-agreements scope="realNameVerify" ref="agreements"
style="margin: 0 10px 10px;"></uni-id-pages-x-agreements>
<button type="primary" :disabled="!certifyIdNext" @click="getCertifyId" style="margin: 0 10px;">确定
</button>
</template>
</template>
</view>
</template>
<script lang="uts">
import {state, mutations} from '@/uni_modules/uni-id-pages-x/store.uts';
import checkIdCard from "@/uni_modules/uni-id-pages-x/lib/check-id-card.uts";
const uniIdCo = uniCloud.importObject('uni-id-co')
const tempFrvInfoKey = 'uni-id-pages-temp-frv'
export default {
data() {
return {
realName: '',
idCard: '',
certifyId: '',
verifyFail: false,
verifyFailCode: 0,
verifyFailTitle: '',
verifyFailContent: ''
};
},
computed: {
userInfo(): UTSJSONObject {
return state.userInfo
},
realnameInfo(): UTSJSONObject {
return this.userInfo.getJSON('realNameInfo') ?? {}
},
certifyIdNext(): boolean {
return this.realName !== '' && this.idCard !== '' && !state.pendingAgreements
},
isCertify(): boolean {
return this.realnameInfo.getNumber('authStatus') == 2
},
isDev(): boolean {
return process.env.NODE_ENV == 'development'
}
},
methods: {
async getCertifyId(): Promise<void> {
if (!this.certifyIdNext) return
if (!checkIdCard(this.idCard)) {
uni.showToast({
title: "身份证不合法",
icon: "none"
})
return
}
const reg = new RegExp('^[\u4e00-\u9fa5]{1,10}(·?[\u4e00-\u9fa5]{1,10}){0,5}', 'g')
if (
typeof this.realName !== 'string' ||
this.realName.length < 2 ||
!reg.test(this.realName)
) {
uni.showToast({
title: "姓名只能是汉字",
icon: "none"
})
return
}
uni.setStorage({
key: tempFrvInfoKey,
data: {
realName: this.realName,
idCard: this.idCard
}
});
const metaInfo = uni.getFacialRecognitionMetaInfo()
const res: UTSJSONObject = await uniIdCo.getFrvCertifyId({
realName: this.realName,
idCard: this.idCard,
metaInfo
})
if (res.getNumber('errCode') == 0) {
return uni.showToast({
title: res.getString('errMsg') as string,
icon: "none"
})
}
this.certifyId = res.getString('certifyId') as string
this.startFacialRecognitionVerify()
},
startFacialRecognitionVerify() {
uni.startFacialRecognitionVerify({
certifyId: this.certifyId,
progressBarColor: "#2979ff",
success: () => {
this.verifyFail = false
this.getFrvAuthResult()
},
fail: (e: IFacialRecognitionVerifyError) => {
let title = "验证失败"
let content: string
console.log(
`[frv-debug] certifyId auth error: certifyId -> ${this.certifyId}, error -> ${JSON.stringify(e)}`
)
switch (e.errCode) {
case 10001:
content = '认证ID为空'
break
case 10010:
title = '刷脸异常'
content = e.cause?.message ?? '错误代码: 10010'
break
case 10011:
title = '验证中断'
content = e.cause?.message ?? '错误代码: 10011'
break
case 10012:
content = '网络异常'
break
case 10020:
content = '设备设置时间异常'
break
case 10013:
this.verifyFailCode = e.errCode
this.verifyFailContent = e.cause?.message ?? '错误代码: 10013'
this.getFrvAuthResult()
console.log(
`[frv-debug] 刷脸失败, certifyId -> ${this.certifyId}, 如在开发环境请检查用户的姓名、身份证号与刷脸用户是否为同一用户。如遇到认证ID已使用请检查opendb-frv-logs表中certifyId状态`
)
return
default:
title = ''
content = `验证未知错误 (${e.errCode})`
break
}
this.verifyFail = true
this.verifyFailCode = e.errCode
this.verifyFailTitle = title
this.verifyFailContent = content
}
})
},
async getFrvAuthResult(): Promise<void> {
const uniIdCo = uniCloud.importObject('uni-id-co', {
customUI: true
})
try {
uni.showLoading({
title: "验证中...",
mask: false
})
const res: UTSJSONObject = await uniIdCo.getFrvAuthResult({
certifyId: this.certifyId
})
if (this.verifyFailContent != '') {
console.log(`[frv-debug] 客户端刷脸失败,由实人认证服务查询具体原因,原因:${this.verifyFailContent}`)
}
uni.showModal({
content: "实名认证成功",
showCancel: false,
success: () => {
mutations.updateUserInfo({
realNameInfo: res
})
this.verifyFail = false
}
})
uni.removeStorage({
key: tempFrvInfoKey
})
} catch (e: UniCloudError) {
this.verifyFail = true
this.verifyFailTitle = e.errMsg
console.error(JSON.stringify(e))
}
uni.hideLoading()
},
retry() {
if (this.verifyFailCode != 10013) {
this.getCertifyId()
} else {
this.verifyFail = false
}
},
}
}
</script>
<style lang="scss">
@import url("/uni_modules/uni-id-pages-x/common/common.scss");
.list {
position: relative;
background-color: #fff;
margin-top: 15px;
&__item {
border-bottom: 1px solid #f5f5f5;
flex-direction: row;
align-items: center;
padding: 15px 15px;
}
&__title {
flex: 1;
}
&__text {
font-size: 14px;
color: #333;
}
&__content {
flex-direction: row;
justify-content: center;
align-items: center;
.value {
color: #999;
font-size: 12px;
}
.unset {
color: #aaa;
font-size: 12px;
}
}
}
.input {
background-color: #FFF;
}
.uni-label-pointer {
align-items: center;
display: flex;
flex-direction: row;
}
.item {
flex-direction: row;
}
.text {
line-height: 26px;
}
.agreements {
margin-bottom: 20px;
}
.fail-tip-content {
padding: 20px;
}
.face-icon {
width: 100px;
height: 100px;
margin: 50px auto 30px;
}
.face-icon-image {
width: 100%;
height: 100%;
}
.error-title {
.text {
font-size: 18px;
text-align: center;
font-weight: bold;
}
}
.error-description {
margin: 10px 0 20px;
.text {
font-size: 13px;
color: #999999;
text-align: center;
}
}
.dev-tip {
margin-top: 20px;
font-size: 13px;
color: #999;
text-align: center;
}
</style>
<template>
<view class="uni-content">
<view class="list">
<view class="item">
<text class="title">头像</text>
<uni-id-pages-x-avatar ref="avatar" width="60px" height="60px" :border="false"></uni-id-pages-x-avatar>
</view>
<view class="item">
<text class="title">昵称</text>
<view @click="setNickname" class="content">
<text class="unset" v-if="!isLogin">未登录</text>
<text class="value"
v-else-if="userInfo['nickname'] != null && userInfo['nickname'] != ''">{{userInfo['nickname']}}</text>
<text class="unset" v-else>未设置</text>
<uni-id-pages-x-icons v-if="isLogin" :size="16" type="right-arrow"
class="link-icon"></uni-id-pages-x-icons>
</view>
</view>
<view class="item">
<text class="title">手机号码</text>
<view @click="bindmobile" class="content">
<text class="value" v-if="userInfo['mobile'] != null">{{userInfo['mobile']}}</text>
<text class="unset" v-else>未绑定</text>
<uni-id-pages-x-icons :size="16" type="right-arrow" class="link-icon"></uni-id-pages-x-icons>
</view>
</view>
</view>
<view class="list">
<view @click="deactivate" class="item">
<text class="title">注销账号</text>
<uni-id-pages-x-icons :size="16" type="right-arrow" class="link-icon"></uni-id-pages-x-icons>
</view>
<view class="item" v-if="isLogin && showLoginManage" @click="logout">
<text class="title">退出登录</text>
<uni-id-pages-x-icons :size="16" type="right-arrow" class="link-icon"></uni-id-pages-x-icons>
</view>
<view class="mask" v-if="!isLogin" @click="login"></view>
</view>
<view class="btn-list" v-if="showLoginManage">
<!-- <button v-if="isLogin" @click="logout" class="logout">退出登录</button> -->
<button v-if="!isLogin" @click="login" type="primary" class="login">去登录</button>
</view>
<!-- 弹出对话框组件 -->
<uni-id-pages-x-popup-dialog ref="popup-dialog" />
</view>
<view class="uni-content">
<view class="list">
<view class="item">
<text class="title">头像</text>
<uni-id-pages-x-avatar ref="avatar" width="60px" height="60px" :border="false"></uni-id-pages-x-avatar>
</view>
<view class="item">
<text class="title">昵称</text>
<view @click="setNickname" class="content">
<text class="unset" v-if="!isLogin">未登录</text>
<text class="value"
v-else-if="userInfo['nickname'] != null && userInfo['nickname'] != ''">{{userInfo['nickname']}}
</text>
<text class="unset" v-else>未设置</text>
<uni-id-pages-x-icons v-if="isLogin" :size="16" type="right-arrow"
class="link-icon"></uni-id-pages-x-icons>
</view>
</view>
<view class="item">
<text class="title">手机号码</text>
<view @click="bindmobile" class="content">
<text class="value" v-if="userInfo['mobile'] != null">{{userInfo['mobile']}}</text>
<text class="unset" v-else>未绑定</text>
<uni-id-pages-x-icons :size="16" type="right-arrow" class="link-icon"></uni-id-pages-x-icons>
</view>
</view>
</view>
<view class="list">
<!-- 如未开通实人认证服务,可以将实名认证入口注释 -->
<view @click="realNameVerify" class="item">
<text class="title">实名认证</text>
<view class="content">
<text class="value" v-if="realNameStatus == 2">已认证</text>
<text class="unset" v-else>未认证</text>
<uni-id-pages-x-icons :size="16" type="right-arrow" class="link-icon"></uni-id-pages-x-icons>
</view>
</view>
</view>
<view class="list">
<view @click="deactivate" class="item">
<text class="title">注销账号</text>
<uni-id-pages-x-icons :size="16" type="right-arrow" class="link-icon"></uni-id-pages-x-icons>
</view>
<view class="item" v-if="isLogin && showLoginManage" @click="logout">
<text class="title">退出登录</text>
<uni-id-pages-x-icons :size="16" type="right-arrow" class="link-icon"></uni-id-pages-x-icons>
</view>
<view class="mask" v-if="!isLogin" @click="login"></view>
</view>
<view class="btn-list" v-if="showLoginManage">
<!-- <button v-if="isLogin" @click="logout" class="logout">退出登录</button> -->
<button v-if="!isLogin" @click="login" type="primary" class="login">去登录</button>
</view>
<!-- 弹出对话框组件 -->
<uni-id-pages-x-popup-dialog ref="popup-dialog"/>
</view>
</template>
<script>
import { state, mutations } from '@/uni_modules/uni-id-pages-x/store.uts';
import { logout } from '@/uni_modules/uni-id-pages-x/common/common.uts';
export default {
data() {
return {
}
},
computed: {
userInfo() : UTSJSONObject {
return state.userInfo
},
isLogin() : boolean {
return state.isLogin
}
},
onShow() {
},
props: {
showLoginManage: {
type: Boolean,
default: true
}
},
methods: {
setNickname() {
uni.navigateTo({
"url": "/uni_modules/uni-id-pages-x/pages/userinfo/setNickname/setNickname"
})
},
bindmobile() {
uni.navigateTo({
url: "/uni_modules/uni-id-pages-x/pages/userinfo/bindMobile/bindMobile",
complete(_) {
// console.log('e', e);
}
})
},
deactivate() {
uni.navigateTo({
url: "/uni_modules/uni-id-pages-x/pages/userinfo/deactivate/deactivate",
complete(_) {
// console.log('e', e);
}
})
},
logout() {
logout()
},
login() {
uni.navigateTo({
url: "/uni_modules/uni-id-pages-x/pages/login/login",
// complete(e){
// console.log('e',e);
// }
})
}
}
}
import {state, mutations} from '@/uni_modules/uni-id-pages-x/store.uts';
import {logout} from '@/uni_modules/uni-id-pages-x/common/common.uts';
export default {
data() {
return {}
},
computed: {
userInfo(): UTSJSONObject {
return state.userInfo
},
isLogin(): boolean {
return state.isLogin
},
realNameStatus (): number {
const realNameAuth = this.userInfo.getJSON('realNameInfo')
const authStatus = realNameAuth?.getNumber('authStatus')
if (realNameAuth == null || authStatus == null) {
return 0
}
return authStatus as number
}
},
onShow() {
},
props: {
showLoginManage: {
type: Boolean,
default: true
}
},
methods: {
setNickname() {
uni.navigateTo({
"url": "/uni_modules/uni-id-pages-x/pages/userinfo/setNickname/setNickname"
})
},
bindmobile() {
uni.navigateTo({
url: "/uni_modules/uni-id-pages-x/pages/userinfo/bindMobile/bindMobile",
complete(_) {
// console.log('e', e);
}
})
},
deactivate() {
uni.navigateTo({
url: "/uni_modules/uni-id-pages-x/pages/userinfo/deactivate/deactivate",
complete(_) {
// console.log('e', e);
}
})
},
logout() {
logout()
},
login() {
uni.navigateTo({
url: "/uni_modules/uni-id-pages-x/pages/login/login",
// complete(e){
// console.log('e',e);
// }
})
},
realNameVerify() {
uni.navigateTo({
"url": "/uni_modules/uni-id-pages-x/pages/userinfo/realnameAuth/realnameAuth"
})
}
}
}
</script>
<style>
.uni-content {
width: 750rpx;
background-color: #f5f5f5;
flex: 1;
}
.avatar {
border: 1px solid #000;
align-items: center;
}
.list {
position: relative;
background-color: #fff;
margin-top: 15px;
}
.mask {
position: absolute;
width: 750rpx;
height: 100%;
}
.list .item {
border-bottom: 1px solid #f5f5f5;
flex-direction: row;
align-items: center;
padding: 15px 15px;
}
.list .item.deactivate {
margin-top: 20px;
border-top: 0.1px solid #EEE;
}
.list .item .title {
font-size: 14px;
flex: 1;
color: #333;
}
.list .item .content {
flex-direction: row;
justify-content: center;
align-items: center;
}
.list .item .content .value {
color: #999;
font-size: 12px;
}
.list .item .content .unset {
color: #aaa;
font-size: 12px;
}
.link-icon {
margin-left: 5px;
width: 13px;
}
.btn-list {
margin-top: 30px;
padding: 30px 150rpx;
}
.logout {
color: #888;
border-style: none;
}
</style>
\ No newline at end of file
.uni-content {
width: 750rpx;
background-color: #f5f5f5;
flex: 1;
}
.avatar {
border: 1px solid #000;
align-items: center;
}
.list {
position: relative;
background-color: #fff;
margin-top: 15px;
}
.mask {
position: absolute;
width: 750rpx;
height: 100%;
}
.list .item {
border-bottom: 1px solid #f5f5f5;
flex-direction: row;
align-items: center;
padding: 15px 15px;
}
.list .item.deactivate {
margin-top: 20px;
border-top: 0.1px solid #EEE;
}
.list .item .title {
font-size: 14px;
flex: 1;
color: #333;
}
.list .item .content {
flex-direction: row;
justify-content: center;
align-items: center;
}
.list .item .content .value {
color: #999;
font-size: 12px;
}
.list .item .content .unset {
color: #aaa;
font-size: 12px;
}
.link-icon {
margin-left: 5px;
width: 13px;
}
.btn-list {
margin-top: 30px;
padding: 30px 150rpx;
}
.logout {
color: #888;
border-style: none;
}
</style>
export type State = {
pendingAgreements : boolean,
isLogin : boolean,
userInfo : UTSJSONObject
pendingAgreements: boolean,
isLogin: boolean,
userInfo: UTSJSONObject
}
// 实例化为state
export const state = reactive({
pendingAgreements: false,
userInfo: {
"_id": null,
"avatar_file": null,
"nickname": null,
"mobile": null,
} as UTSJSONObject,
isLogin: false
pendingAgreements: false,
userInfo: {
"_id": null,
"avatar_file": null,
"nickname": null,
"mobile": null,
} as UTSJSONObject,
isLogin: false
} as State)
function initState() {
try {
let userInfo = uni.getStorageSync('uni-id-pages-x-userInfo')
if (userInfo instanceof UTSJSONObject) {
state.userInfo = userInfo
// console.log('init userInfo',userInfo);
}
} catch (e) {
console.error('init userInfo error', e);
}
state.isLogin = uniCloud.getCurrentUserInfo().tokenExpired > Date.now()
try {
let userInfo = uni.getStorageSync('uni-id-pages-x-userInfo')
if (userInfo instanceof UTSJSONObject) {
state.userInfo = userInfo
// console.log('init userInfo',userInfo);
}
} catch (e) {
console.error('init userInfo error', e);
}
state.isLogin = uniCloud.getCurrentUserInfo().tokenExpired > Date.now()
};
initState()
type Mutations = {
updateUserInfo(param : null | UTSJSONObject) : void
updateUserInfo(param: null | UTSJSONObject): void
}
export const mutations = {
updateUserInfo(param : null | UTSJSONObject) {
// console.log('updateUserInfo', param); // param为 null 时从云端获取数据更新,为UTSJSONObject时直接根据传入的值来更新
function afterUpdateUserInfo() {
// console.log('afterUpdateUserInfo', state.userInfo);
uni.setStorageSync('uni-id-pages-x-userInfo', state.userInfo)
}
if (param == null) {
const db = uniCloud.databaseForJQL()
const user_id = uniCloud.getCurrentUserInfo().uid
// console.log('user_id', user_id);
if (user_id != null) {
db.collection('uni-id-users')
.doc(user_id)
.field('_id,username,nickname,avatar_file,mobile')
.get()
.then<void>(res => {
// console.log("get cloud userinfo", res);
state.userInfo = res.data[0]
afterUpdateUserInfo()
})
.catch<void>((err : any | null) => {
const error = err as UniCloudError
console.error(error.errMsg, '错误')
})
}
} else {
param.toMap().forEach((value, key) => {
// console.log("updateUserInfo.", key, value)
state.userInfo.set(key, value)
})
afterUpdateUserInfo()
}
}
} as Mutations;
\ No newline at end of file
updateUserInfo(param: null | UTSJSONObject) {
// console.log('updateUserInfo', param); // param为 null 时从云端获取数据更新,为UTSJSONObject时直接根据传入的值来更新
function afterUpdateUserInfo() {
// console.log('afterUpdateUserInfo', state.userInfo);
uni.setStorageSync('uni-id-pages-x-userInfo', state.userInfo)
}
if (param == null) {
const db = uniCloud.databaseForJQL()
const user_id = uniCloud.getCurrentUserInfo().uid
// console.log('user_id', user_id);
if (user_id != null) {
db.collection('uni-id-users')
.doc(user_id)
.field('_id,username,nickname,avatar_file,mobile')
.get()
.then<void>(res => {
const uniIdCo = uniCloud.importObject('uni-id-co', {
customUI: true
})
uniIdCo
.getRealNameInfo()
.then((realNameInfo) => {
state.userInfo = Object.assign(res.data[0], {
realNameInfo
})
afterUpdateUserInfo()
})
})
.catch<void>((err: any | null) => {
const error = err as UniCloudError
console.error(error.errMsg, '错误')
})
}
} else {
param.toMap().forEach((value, key) => {
// console.log("updateUserInfo.", key, value)
state.userInfo.set(key, value)
})
afterUpdateUserInfo()
}
}
} as Mutations;
......@@ -45,11 +45,9 @@ module.exports = async function (params) {
// 查询已经使用同一个身份证认证的账号数量,如果超过限制则不能认证
const idCardAccount = await userCollection.where({
realname_auth: {
type: 0, // 用户认证状态是个人
auth_status: REAL_NAME_STATUS.CERTIFIED, // 认证状态为已认证
identity: idCard // 身份证号码和传入参数的身份证号码相同
}
'realname_auth.type': 0,
'realname_auth.auth_status': REAL_NAME_STATUS.CERTIFIED,
'realname_auth.identity': idCard
}).get()
if (idCardAccount.data.length >= idCardCertifyLimit) {
throw {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册