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 }