You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
403 lines
12 KiB
403 lines
12 KiB
import tui from './httpRequest'
|
|
import { verifyIeRealName, verifyIeStudentCard, getIeRealNameAuditSetting } from './ieApi.js'
|
|
|
|
function showLongToast(title, duration = 3200) {
|
|
const content = String(title || '操作未完成')
|
|
if (content.length > 18) {
|
|
uni.showModal({
|
|
title: '提示',
|
|
content,
|
|
showCancel: false,
|
|
confirmText: '知道了'
|
|
})
|
|
return
|
|
}
|
|
uni.showToast({ title: content, icon: 'none', duration })
|
|
}
|
|
|
|
function showAuthDialog(options = {}) {
|
|
return new Promise(resolve => {
|
|
let handled = false
|
|
const fallbackTimer = setTimeout(() => {
|
|
if (handled) return
|
|
uni.showModal({
|
|
title: options.title || '提示',
|
|
content: options.content || '',
|
|
confirmText: options.confirmText || '继续',
|
|
cancelText: options.cancelText || '取消',
|
|
success: res => resolve(!!res.confirm),
|
|
fail: () => resolve(false)
|
|
})
|
|
}, 80)
|
|
uni.$emit('ie-auth-dialog:show', {
|
|
...options,
|
|
handled: () => {
|
|
handled = true
|
|
clearTimeout(fallbackTimer)
|
|
},
|
|
resolve
|
|
})
|
|
})
|
|
}
|
|
|
|
function chooseImage(sourceType = ['camera'], camera = 'front') {
|
|
return new Promise((resolve, reject) => {
|
|
const success = res => {
|
|
const file = res.tempFiles && res.tempFiles[0]
|
|
resolve((file && (file.tempFilePath || file.path)) || (res.tempFilePaths && res.tempFilePaths[0]) || '')
|
|
}
|
|
if (uni.chooseMedia) {
|
|
uni.chooseMedia({
|
|
count: 1,
|
|
mediaType: ['image'],
|
|
sourceType,
|
|
camera,
|
|
success,
|
|
fail: reject
|
|
})
|
|
return
|
|
}
|
|
uni.chooseImage({
|
|
count: 1,
|
|
sourceType,
|
|
success,
|
|
fail: reject
|
|
})
|
|
})
|
|
}
|
|
|
|
function extractUploadUrl(fileObj) {
|
|
if (!fileObj) return ''
|
|
if (typeof fileObj === 'string') return fileObj
|
|
return fileObj.url || fileObj.fileUrl || fileObj.path || fileObj.fullPath || ''
|
|
}
|
|
|
|
const MAX_CERT_PICTURE_BASE64_BYTES = 50 * 1024
|
|
const STUDENT_CARD_AUTH_DISABLED = true
|
|
|
|
function confirmCameraCapture(title, content, confirmText = '开始拍摄') {
|
|
return showAuthDialog({
|
|
badge: '照片采集',
|
|
title,
|
|
content,
|
|
confirmText,
|
|
cancelText: '取消',
|
|
steps: [
|
|
'请在光线充足处拍摄',
|
|
'保持画面清晰无遮挡',
|
|
'照片仅用于本次认证'
|
|
]
|
|
})
|
|
}
|
|
|
|
async function uploadStudentCard(attempt = 0) {
|
|
const confirmed = await confirmCameraCapture(
|
|
attempt === 0 ? '拍摄学生证照片' : '重新拍摄学生证',
|
|
attempt === 0
|
|
? '请拍摄本人大学学生证照片,尽量保证证件信息清晰、无遮挡。'
|
|
: '上次未识别为大学生学生证,请重新拍摄一张清晰的学生证照片。'
|
|
)
|
|
if (!confirmed) return ''
|
|
const path = await chooseImage(['camera', 'album'])
|
|
if (!path) return ''
|
|
const uploaded = await tui.uploadFile('/upload/file', path)
|
|
return extractUploadUrl(uploaded)
|
|
}
|
|
|
|
function promptText(title, placeholder) {
|
|
return new Promise(resolve => {
|
|
uni.showModal({
|
|
title,
|
|
editable: true,
|
|
placeholderText: placeholder,
|
|
confirmText: '确定',
|
|
cancelText: '取消',
|
|
success: res => resolve(res.confirm ? String(res.content || '').trim() : ''),
|
|
fail: () => resolve('')
|
|
})
|
|
})
|
|
}
|
|
|
|
function isValidMainlandName(name) {
|
|
const value = String(name || '').trim()
|
|
if (value.length < 2 || value.length > 30) return false
|
|
if (value.indexOf('·') === 0 || value.indexOf('·') === value.length - 1 || value.indexOf('··') !== -1) return false
|
|
return /^[\u4e00-\u9fa5]+(·[\u4e00-\u9fa5]+)*$/.test(value)
|
|
}
|
|
|
|
function isValidMainlandIdCard(idCardNo) {
|
|
const id = String(idCardNo || '').trim().toUpperCase()
|
|
if (!/^\d{17}[\dX]$/.test(id)) return false
|
|
if (id.substring(0, 6) === '000000') return false
|
|
if (!isValidIdCardBirthday(id.substring(6, 14))) return false
|
|
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
|
|
const checks = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
|
|
let sum = 0
|
|
for (let i = 0; i < 17; i += 1) {
|
|
sum += Number(id.charAt(i)) * weights[i]
|
|
}
|
|
return checks[sum % 11] === id.charAt(17)
|
|
}
|
|
|
|
function isValidIdCardBirthday(birthday) {
|
|
if (!/^\d{8}$/.test(birthday)) return false
|
|
const year = Number(birthday.substring(0, 4))
|
|
const month = Number(birthday.substring(4, 6))
|
|
const day = Number(birthday.substring(6, 8))
|
|
const date = new Date(year, month - 1, day)
|
|
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) return false
|
|
const today = new Date()
|
|
return year >= 1900 && date <= today
|
|
}
|
|
|
|
function compressImage(filePath, option) {
|
|
return new Promise(resolve => {
|
|
if (!uni.compressImage) {
|
|
resolve(filePath)
|
|
return
|
|
}
|
|
const params = {
|
|
src: filePath,
|
|
quality: option.quality,
|
|
success: res => resolve(res.tempFilePath || filePath),
|
|
fail: () => resolve(filePath)
|
|
}
|
|
if (option.width) {
|
|
params.compressedWidth = option.width
|
|
params.compressWidth = option.width
|
|
}
|
|
if (option.height) {
|
|
params.compressedHeight = option.height
|
|
params.compressHeight = option.height
|
|
}
|
|
uni.compressImage(params)
|
|
})
|
|
}
|
|
|
|
function readFileBase64(filePath) {
|
|
return new Promise((resolve, reject) => {
|
|
const wxApi = typeof wx !== 'undefined' ? wx : null
|
|
if (!wxApi || !wxApi.getFileSystemManager) {
|
|
reject(new Error('当前环境不支持读取图片Base64'))
|
|
return
|
|
}
|
|
wxApi.getFileSystemManager().readFile({
|
|
filePath,
|
|
encoding: 'base64',
|
|
success: res => resolve(String(res.data || '').replace(/\s+/g, '')),
|
|
fail: reject
|
|
})
|
|
})
|
|
}
|
|
|
|
async function chooseFacePictureBase64() {
|
|
const confirmed = await confirmCameraCapture(
|
|
'拍摄实名核验照片',
|
|
'请拍摄本人清晰正脸照片用于身份证三要素核验,照片只用于本次认证并会压缩到50KB以内。'
|
|
)
|
|
if (!confirmed) return ''
|
|
const path = await chooseImage(['camera'], 'front')
|
|
if (!path) return ''
|
|
uni.showLoading({ title: '照片处理中\n正在压缩图片', mask: true })
|
|
const compressOptions = [
|
|
{ quality: 70, width: 480, height: 480 },
|
|
{ quality: 55, width: 360, height: 360 },
|
|
{ quality: 40, width: 300, height: 300 },
|
|
{ quality: 30, width: 240, height: 240 },
|
|
{ quality: 22, width: 200, height: 200 },
|
|
{ quality: 15, width: 160, height: 160 },
|
|
{ quality: 10, width: 128, height: 128 }
|
|
]
|
|
let lastBase64 = ''
|
|
try {
|
|
for (const option of compressOptions) {
|
|
const compressed = await compressImage(path, option)
|
|
const base64 = await readFileBase64(compressed)
|
|
lastBase64 = base64
|
|
if (base64.length <= MAX_CERT_PICTURE_BASE64_BYTES) {
|
|
return base64
|
|
}
|
|
}
|
|
} finally {
|
|
uni.hideLoading()
|
|
}
|
|
if (lastBase64.length > MAX_CERT_PICTURE_BASE64_BYTES) {
|
|
showLongToast('人像照片Base64不能超过50KB,请靠近拍摄清晰正脸并减少背景后重试', 3800)
|
|
return ''
|
|
}
|
|
return lastBase64
|
|
}
|
|
|
|
function confirmAuth(actionText, cancelText, dialogOptions = {}) {
|
|
return showAuthDialog({
|
|
badge: dialogOptions.badge || 'i/e 安全认证',
|
|
title: dialogOptions.title || '进阶实名认证',
|
|
content: dialogOptions.content || `为满足未成年人保护与防沉迷要求,${actionText || '继续使用'}前需要完成身份证三要素核验。`,
|
|
confirmText: '去认证',
|
|
cancelText: cancelText || '暂不继续',
|
|
steps: dialogOptions.steps || [
|
|
'填写身份证上的姓名与号码',
|
|
'拍摄本人清晰正脸照',
|
|
'通过后继续完善 i/e 身份资料'
|
|
]
|
|
})
|
|
}
|
|
|
|
function confirmStudentCardOnly(actionText, cancelText) {
|
|
return showAuthDialog({
|
|
badge: '学生身份',
|
|
title: '学生身份认证',
|
|
content: `${actionText || '继续使用'}前需要完成学生证认证。`,
|
|
confirmText: '去认证',
|
|
cancelText: cancelText || '暂不继续',
|
|
steps: [
|
|
'上传本人学生证照片',
|
|
'系统识别学生证信息',
|
|
'通过后继续 i/e 互动'
|
|
]
|
|
})
|
|
}
|
|
|
|
async function ensureStudentCardVerified(profile, options = {}) {
|
|
if (profile && profile.studentCardVerified) return true
|
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
const imageUrl = await uploadStudentCard(attempt)
|
|
if (!imageUrl) {
|
|
showLongToast('未上传学生证照片,请重新选择', 3000)
|
|
return false
|
|
}
|
|
const result = await verifyIeStudentCard({ imageUrl })
|
|
if (!result) return false
|
|
if (result.studentCardVerified) {
|
|
if (!options.skipSuccessToast) {
|
|
showLongToast('学生证认证通过', 2200)
|
|
}
|
|
return true
|
|
}
|
|
if (result.studentCardNeedRetry && attempt === 0) {
|
|
continue
|
|
}
|
|
showLongToast(result.message || '学生证认证未完成,请重新上传', 3200)
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
async function ensureAliyunRealNameVerified(profile) {
|
|
if (profile && profile.realNameVerified) return true
|
|
const regionId = getCurrentRegionId()
|
|
if (!regionId) {
|
|
showLongToast('请先选择校区后再进行实名认证', 3200)
|
|
return false
|
|
}
|
|
const certName = await promptText('填写真实姓名', '请输入身份证上的姓名')
|
|
if (!certName) {
|
|
showLongToast('未填写真实姓名,无法完成实名认证', 3200)
|
|
return false
|
|
}
|
|
if (!isValidMainlandName(certName)) {
|
|
showLongToast('真实姓名格式不正确,请填写中国大陆身份证上的中文姓名', 3600)
|
|
return false
|
|
}
|
|
const certNo = (await promptText('填写身份证号', '请输入18位中国大陆居民身份证号')).toUpperCase()
|
|
if (!certNo) {
|
|
showLongToast('未填写身份证号,无法完成实名认证', 3200)
|
|
return false
|
|
}
|
|
if (!isValidMainlandIdCard(certNo)) {
|
|
showLongToast('身份证号格式或校验位不正确,请核对后重试', 3600)
|
|
return false
|
|
}
|
|
const certPicture = await chooseFacePictureBase64()
|
|
if (!certPicture) return false
|
|
uni.showLoading({ title: '实名认证中\n正在安全核验', mask: true })
|
|
let result = null
|
|
try {
|
|
result = await verifyIeRealName({ certName, certNo, certPicture, regionId })
|
|
} finally {
|
|
uni.hideLoading()
|
|
}
|
|
if (!result || !result.realNameVerified) {
|
|
showLongToast((result && result.message) || '实名认证未完成,请稍后重试', 3200)
|
|
return false
|
|
}
|
|
showLongToast('实名认证通过', 2200)
|
|
return true
|
|
}
|
|
|
|
function getCurrentRegionId() {
|
|
const area = uni.getStorageSync('area')
|
|
if (!area) return ''
|
|
if (typeof area === 'string') {
|
|
try {
|
|
const parsed = JSON.parse(area)
|
|
return parsed && parsed.id ? parsed.id : ''
|
|
} catch (e) {
|
|
return ''
|
|
}
|
|
}
|
|
return area.id || ''
|
|
}
|
|
|
|
function needsProfileSetup(profile) {
|
|
return !profile || profile.exists === false || profile.profileCompleted !== 1
|
|
}
|
|
|
|
function goProfileSetup() {
|
|
uni.navigateTo({
|
|
url: '/package1/ieBrowser/profileSetup',
|
|
fail: () => uni.redirectTo({ url: '/package1/ieBrowser/profileSetup' })
|
|
})
|
|
}
|
|
|
|
export async function ensureIeVerifiedBeforeAction(options = {}) {
|
|
const profile = options.profile || {}
|
|
if (STUDENT_CARD_AUTH_DISABLED && profile.realNameVerified) {
|
|
if (needsProfileSetup(profile)) {
|
|
goProfileSetup()
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
if (!STUDENT_CARD_AUTH_DISABLED && profile.realNameVerified && profile.studentCardVerified) {
|
|
if (needsProfileSetup(profile)) {
|
|
goProfileSetup()
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
const setting = await getIeRealNameAuditSetting().catch(() => null)
|
|
const realNameAuditEnabled = !setting || setting.realNameAuditEnabled !== false
|
|
if (!realNameAuditEnabled) return true
|
|
const confirmed = realNameAuditEnabled
|
|
? await confirmAuth(options.actionText, options.cancelText, options.authDialog || {})
|
|
: await confirmStudentCardOnly(options.actionText, options.cancelText)
|
|
if (!confirmed) return false
|
|
try {
|
|
// 学生证认证流程临时注释保留,当前阶段只执行实名认证。
|
|
// const studentOk = await ensureStudentCardVerified(profile, { skipSuccessToast: realNameAuditEnabled })
|
|
// if (!studentOk) return false
|
|
// if (!realNameAuditEnabled) {
|
|
// if (typeof options.reload === 'function') {
|
|
// await options.reload()
|
|
// }
|
|
// return true
|
|
// }
|
|
const realNameOk = await ensureAliyunRealNameVerified(profile)
|
|
if (!realNameOk) return false
|
|
let latestProfile = null
|
|
if (typeof options.reload === 'function') {
|
|
latestProfile = await options.reload()
|
|
}
|
|
if (needsProfileSetup(latestProfile || profile)) {
|
|
goProfileSetup()
|
|
return false
|
|
}
|
|
return true
|
|
} catch (e) {
|
|
showLongToast((e && e.message) || '认证未完成,请稍后重试', 3200)
|
|
return false
|
|
}
|
|
}
|
|
|
|
export { showLongToast }
|
|
|