diff --git a/common/ieApi.js b/common/ieApi.js index d3bc82c..0f53fc0 100644 --- a/common/ieApi.js +++ b/common/ieApi.js @@ -3,7 +3,7 @@ import tui from './httpRequest' function unwrap(res) { if (!res) return null if (res.success === false) { - uni.showToast({ title: res.message || '请求失败', icon: 'none' }) + uni.showToast({ title: res.message || '请求失败', icon: 'none', duration: 3200 }) return null } return res.result !== undefined ? res.result : res @@ -16,7 +16,7 @@ function unwrapIeHome(res) { if (message.indexOf('i/e 身份已被封禁') !== -1 || message.indexOf('ie 身份已被封禁') !== -1) { return { banned: true, message } } - uni.showToast({ title: message, icon: 'none' }) + uni.showToast({ title: message, icon: 'none', duration: 3200 }) return null } return res.result !== undefined ? res.result : res @@ -50,6 +50,18 @@ export function saveIeProfile(data) { return tui.request('/app/ie/profile', 'POST', data, false, false, false).then(unwrap) } +export function verifyIeRealName(data) { + return tui.request('/app/ie/real-name/verify', 'POST', data, false, false, false).then(unwrap) +} + +export function verifyIeStudentCard(data) { + return tui.request('/app/ie/real-name/student-card/verify', 'POST', data, false, false, false).then(unwrap) +} + +export function getIeRealNameAuditSetting() { + return tui.request('/app/ie/real-name/audit-setting', 'GET', {}, false, true, true).then(unwrap) +} + export function startIeMatch(data) { return tui.request('/app/ie/match/start', 'POST', data, false, false, false).then(unwrap) } diff --git a/common/ieRealNameAuth.js b/common/ieRealNameAuth.js new file mode 100644 index 0000000..504ecd9 --- /dev/null +++ b/common/ieRealNameAuth.js @@ -0,0 +1,270 @@ +import tui from './httpRequest' +import { verifyIeRealName, verifyIeStudentCard, getIeRealNameAuditSetting } from './ieApi.js' + +function showLongToast(title, duration = 3200) { + uni.showToast({ title, icon: 'none', duration }) +} + +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 new Promise(resolve => { + uni.showModal({ + title, + content, + confirmText, + cancelText: '取消', + success: res => resolve(!!res.confirm), + fail: () => resolve(false) + }) + }) +} + +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 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 '' + 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 = '' + 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 + } + } + if (lastBase64.length > MAX_CERT_PICTURE_BASE64_BYTES) { + showLongToast('人像照片Base64不能超过50KB,请靠近拍摄清晰正脸并减少背景后重试', 3800) + return '' + } + return lastBase64 +} + +function confirmAuth(actionText, cancelText) { + return new Promise(resolve => { + uni.showModal({ + title: '进阶实名认证', + content: `为满足未成年人保护与防沉迷要求,${actionText || '继续使用'}前需要完成身份证三要素核验。`, + confirmText: '去认证', + cancelText: cancelText || '暂不继续', + success: res => resolve(!!res.confirm), + fail: () => resolve(false) + }) + }) +} + +function confirmStudentCardOnly(actionText, cancelText) { + return new Promise(resolve => { + uni.showModal({ + title: '学生身份认证', + content: `${actionText || '继续使用'}前需要完成学生证认证。`, + confirmText: '去认证', + cancelText: cancelText || '暂不继续', + success: res => resolve(!!res.confirm), + fail: () => resolve(false) + }) + }) +} + +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 certName = await promptText('填写真实姓名', '请输入身份证上的姓名') + if (!certName) { + showLongToast('未填写真实姓名,无法完成实名认证', 3200) + return false + } + const certNo = await promptText('填写身份证号', '请输入身份证号码') + if (!certNo) { + showLongToast('未填写身份证号,无法完成实名认证', 3200) + return false + } + const certPicture = await chooseFacePictureBase64() + if (!certPicture) return false + uni.showLoading({ title: '实名认证中...', mask: true }) + let result = null + try { + result = await verifyIeRealName({ certName, certNo, certPicture }) + } finally { + uni.hideLoading() + } + if (!result || !result.realNameVerified) { + showLongToast((result && result.message) || '实名认证未完成,请稍后重试', 3200) + return false + } + showLongToast('实名认证通过', 2200) + return true +} + +export async function ensureIeVerifiedBeforeAction(options = {}) { + const profile = options.profile || {} + if (STUDENT_CARD_AUTH_DISABLED && profile.realNameVerified) return true + if (!STUDENT_CARD_AUTH_DISABLED && profile.realNameVerified && profile.studentCardVerified) 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) + : 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 + if (typeof options.reload === 'function') { + await options.reload() + } + return true + } catch (e) { + showLongToast('认证未完成,请稍后重试', 3200) + return false + } +} + +export { showLongToast }