Files
MBTI_wang/miniprogram/pages/index/result.js

546 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// pages/index/result.js - AI分析结果页按旧版模板重构
const app = getApp()
const payment = require('../../utils/payment')
const { hasPhone, bindPhoneByCode, ensureProfileCompleteAndRedirect } = require('../../utils/phoneAuth.js')
Page({
data: {
isAnalyzing: true,
showResult: false,
hasError: false,
errorMessage: '',
noFaceError: false,
noFaceMessage: '',
progress: 0,
analyzingTip: '正在识别面部特征...',
activeTab: 'face', // 'face' | 'bone'
// 报告付费信息
payInfo: {
requiresPayment: false, // 是否需要付费才解锁完整报告
isPaid: false, // 当前这次是否已解锁
amountYuan: 0 // 人脸报告价格(元)
},
result: {
mbti: '',
title: '',
summary: '',
pdp: '',
pdpAux: '',
pdpEmoji: '',
disc: '',
discAux: '',
traits: [],
faceAnalysisText: '',
boneAnalysisText: '',
careers: [],
relationship: '',
gallupTop3: [],
// 完整面相/骨相object 才用,字符串走 *Text 字段)
faceAnalysis: null,
boneAnalysis: null,
// 企业版职业画像、HR视角、老板视角、简历亮点
portrait: null,
hrView: null,
bossView: null,
resumeHighlights: ''
},
// 当前这次AI分析对应的测试记录IDmbti_test_results.id
testResultId: null,
// 支付后是否已经触发过一次“刷新完整报告”,避免重复刷新
hasReloadedAfterPay: false,
// 是否已在本地拥有手机号(决定是否还需要弹出微信手机号授权)
hasPhone: false,
analyzingTitle: '正在分析中',
reportTitle: '分析报告',
aiAnalysisText: '智能分析',
reviewMode: false
},
onLoad(options) {
const id = options && options.id
const type = options && options.type
// 同步审核模式
this.setData({ reviewMode: !!app.globalData.reviewMode })
// 加载文案配置(分析中提示、报告标题等)
const tc = app.globalData.textConfig
if (tc) {
this.setData({
analyzingTitle: tc.analyzingTitle || '正在分析中',
reportTitle: tc.reportTitle || '分析报告',
aiAnalysisText: tc.aiAnalysisText || '智能分析'
})
if (tc.reportTitle) wx.setNavigationBarTitle({ title: tc.reportTitle })
} else {
app.getRuntimeConfig().then((cfg) => {
if (cfg && cfg.textConfig) {
app.globalData.textConfig = cfg.textConfig
const t = cfg.textConfig
this.setData({
analyzingTitle: t.analyzingTitle || '正在分析中',
reportTitle: t.reportTitle || '分析报告',
aiAnalysisText: t.aiAnalysisText || '智能分析'
})
if (t.reportTitle) wx.setNavigationBarTitle({ title: t.reportTitle })
}
}).catch(() => {})
}
// 从历史记录进入根据ID从后端读取数据库中的结果
if (id && type === 'ai') {
const token = app.globalData.token || wx.getStorageSync('token')
const apiBase = app.globalData.apiBase
if (!token || !apiBase) {
wx.showToast({ title: '未登录,无法读取历史记录', icon: 'none' })
setTimeout(() => wx.navigateBack(), 1500)
return
}
wx.showLoading({ title: '加载历史记录...' })
wx.request({
url: `${apiBase}/api/test/detail`,
method: 'GET',
header: {
'Authorization': `Bearer ${token}`
},
data: { id },
success: (res) => {
wx.hideLoading()
if (res.statusCode === 200 && res.data && res.data.data) {
const payload = res.data.data
const apiData = payload.data || payload
// 历史详情场景下记录当前测试记录ID
this.setData({ testResultId: id })
this.processResult(apiData)
// 从历史进入:金额与是否需付款以 test_results 的 paidAmount/needPaymentToUnlock 为准
this.initPayInfoFromRuntime(
!!payload.requiresPayment,
!!payload.isPaid,
payload
)
} else {
wx.showToast({ title: res.data?.message || '加载失败', icon: 'none' })
setTimeout(() => wx.navigateBack(), 1500)
}
},
fail: () => {
wx.hideLoading()
wx.showToast({ title: '网络错误,加载失败', icon: 'none' })
setTimeout(() => wx.navigateBack(), 1500)
}
})
return
}
// 正常从拍照流程进入:调用 /api/analyze
this.startAnalysis()
},
onShow() {
if (!ensureProfileCompleteAndRedirect()) return
this.setData({ hasPhone: hasPhone() })
const tc = app.globalData.textConfig
if (tc) {
this.setData({
analyzingTitle: tc.analyzingTitle || '正在分析中',
reportTitle: tc.reportTitle || '分析报告',
aiAnalysisText: tc.aiAnalysisText || '智能分析'
})
}
},
// 面相/骨相 Tab 切换
switchAnalysisTab(e) {
this.setData({ activeTab: e.currentTarget.dataset.tab })
},
// 调用后端AI分析APIaiPhotos 应为上传后的 URL 数组,由拍照页上传后写入)
startAnalysis() {
let photos = wx.getStorageSync('aiPhotos') || []
if (!Array.isArray(photos)) photos = []
const isUrls = photos.length > 0 && photos.every(p => typeof p === 'string' && (p.startsWith('http://') || p.startsWith('https://')))
if (!isUrls || photos.length === 0) {
this.setData({ progress: 100 })
setTimeout(() => {
wx.showToast({ title: '请先完成拍照并上传', icon: 'none' })
wx.navigateBack({ delta: 1 })
}, 300)
return
}
const tips = [
'正在识别面部特征...',
'分析眉眼特征...',
'结合《冰鉴》分析骨形...',
'匹配MBTI/PDP/DISC...',
'生成综合报告...'
]
let progress = 0
let tipIndex = 0
// 进度动画
const timer = setInterval(() => {
progress += 3
if (progress > 95) progress = 95
if (progress > (tipIndex + 1) * 18 && tipIndex < tips.length - 1) tipIndex++
this.setData({ progress: Math.floor(progress), analyzingTip: tips[tipIndex] })
}, 200)
// 调用后端APIappScope='enterprise' 时才传 enterpriseId个人版不传
const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo') || {}
const scope = (app.globalData && app.globalData.appScope) || 'personal'
const enterpriseId = scope === 'enterprise'
? (app.globalData.enterpriseIdFromScene || userInfo.enterpriseId || null)
: null
wx.request({
url: `${app.globalData.apiBase}/api/analyze`,
method: 'POST',
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${wx.getStorageSync('token') || ''}`
},
data: {
photoUrls: photos,
userId: app.globalData.openId || '',
...(enterpriseId ? { enterpriseId: Number(enterpriseId) } : {})
},
success: (res) => {
clearInterval(timer)
if (res.statusCode === 200 && res.data) {
const bodyCode = res.data.code
// 后端 error() 始终返回 HTTP 200业务错误码放在 data.code 中
if (bodyCode === 422) {
// AI 未检测到人脸
const msg = res.data.message || '图片中未检测到人脸,请确保拍摄时面部清晰可见'
this.setData({ progress: 100 })
setTimeout(() => this.showNoFaceError(msg), 300)
} else if (bodyCode === 200) {
const apiData = res.data.data || res.data
this.setData({ progress: 100, analyzingTip: '分析完成!' })
setTimeout(() => {
this.processResult(apiData)
}, 500)
} else {
console.error('API返回错误:', res)
this.setData({ progress: 100 })
const msg = (res.data && (res.data.message || res.data.msg)) || '分析失败,请稍后重试'
setTimeout(() => this.showAnalyzeError(msg), 300)
}
} else {
console.error('API返回错误:', res)
this.setData({ progress: 100 })
setTimeout(() => this.showAnalyzeError('分析失败,请稍后重试'), 300)
}
},
fail: (err) => {
clearInterval(timer)
console.error('API调用失败:', err)
this.setData({ progress: 100 })
setTimeout(() => this.showAnalyzeError('网络异常,请检查网络后重试'), 300)
}
})
},
// 处理API返回结果
processResult(apiData) {
const result = {
mbti: apiData.mbti?.type || '',
title: apiData.mbti?.title || '',
summary: apiData.personalitySummary || apiData.overview || '',
pdp: apiData.pdp?.primary || '',
pdpAux: apiData.pdp?.secondary || '',
pdpEmoji: this.getPDPEmoji(apiData.pdp?.primary),
disc: apiData.disc?.primary || '',
discAux: apiData.disc?.secondary || '',
traits: Array.isArray(apiData.advantages) ? apiData.advantages : [],
faceAnalysisText: typeof apiData.faceAnalysis === 'string' ? apiData.faceAnalysis : '',
boneAnalysisText: typeof apiData.boneAnalysis === 'string' ? apiData.boneAnalysis : '',
careers: Array.isArray(apiData.careers) ? apiData.careers : [],
relationship: apiData.relationship || '',
gallupTop3: Array.isArray(apiData.gallupTop3) ? apiData.gallupTop3 : [],
faceAnalysis: null,
boneAnalysis: null,
portrait: apiData.portrait || null,
hrView: apiData.hrView || null,
bossView: apiData.bossView || null,
resumeHighlights: apiData.resumeHighlights || '',
timestamp: Date.now()
}
wx.setStorageSync('aiResult', result)
if (app.globalData) app.globalData.aiResult = result
// 测试记录已由 /api/analyze 顺带写入,无需再调 /api/test/submit
const updates = {
isAnalyzing: false,
showResult: true,
hasError: false,
result
}
// 记录本次测试记录ID由 /api/analyze 返回)
if (apiData && apiData._testResultId) {
updates.testResultId = apiData._testResultId
}
this.setData(updates)
// 优先使用后端 /api/analyze 返回的价格信息,避免二次请求
if (apiData._payment) {
const p = apiData._payment || {}
this.setData({
payInfo: {
requiresPayment: !!p.requiresPayment,
isPaid: false,
amountYuan: typeof p.amountYuan === 'number'
? p.amountYuan
: (p.amountFen ? (p.amountFen / 100) : 0)
}
})
}
// 无 _payment 时(如从历史详情进入)不在这里调 initPayInfoFromRuntime避免异步 getRuntimeConfig 后覆盖详情接口返回的 needPaymentToUnlock由调用方用 detail 的 payload 单独设置 payInfo
},
// PDP类型对应emoji
getPDPEmoji(type) {
const map = {
'老虎': '🐅', '孔雀': '🦚', '无尾熊': '🐨', '考拉': '🐨',
'猫头鹰': '🦉', '变色龙': '🦎'
}
return map[type] || '🐅'
},
/**
* 初始化付费信息:结合运行配置 + 可选的数据库记录状态
* @param {boolean|null} recordRequires 是否从记录中读到需要付费(可为空)
* @param {boolean} recordIsPaid 记录是否已付费
*/
initPayInfoFromRuntime(recordRequires = null, recordIsPaid = false, detailPayload = null) {
// 从历史/详情进入:优先用 test_results 的 paidAmount需付款但金额为0 则直接可查看
if (detailPayload && (detailPayload.paidAmount != null || detailPayload.amountYuan != null || detailPayload.needPaymentToUnlock != null)) {
const paidAmount = Number(detailPayload.paidAmount ?? 0)
const amountYuan = detailPayload.amountYuan != null ? Number(detailPayload.amountYuan) : (paidAmount > 0 ? paidAmount / 100 : 0)
const needPay = detailPayload.needPaymentToUnlock === true || (!!detailPayload.requiresPayment && !detailPayload.isPaid && paidAmount > 0)
this.setData({
payInfo: {
requiresPayment: needPay,
isPaid: !!detailPayload.isPaid,
amountYuan: needPay ? amountYuan : 0
}
})
return
}
app.getRuntimeConfig()
.then((cfg) => {
const pricing = cfg.pricing || {}
const reportRequires = cfg.reportRequiresPayment || {}
const facePriceRaw = pricing.face
const facePrice = typeof facePriceRaw === 'number'
? facePriceRaw
: Number(facePriceRaw || 0)
const requiresByConfig = !!(reportRequires && reportRequires.face)
const requiresPayment =
typeof recordRequires === 'boolean' ? recordRequires : requiresByConfig
// 系统设置需付款但金额为0 则直接可查看,不展示付费墙
const needPay = requiresPayment && facePrice > 0
const amountYuan = facePrice > 0 ? facePrice : 0
this.setData({
payInfo: {
requiresPayment: needPay,
isPaid: !!recordIsPaid,
amountYuan
}
})
})
.catch(() => {
this.setData({
payInfo: {
requiresPayment: false,
isPaid: !!recordIsPaid,
amountYuan: 0
}
})
})
},
// 解锁完整报告:发起人脸测试付费
unlockFullReport() {
const { payInfo, testResultId, hasReloadedAfterPay } = this.data
if (!payInfo.requiresPayment || payInfo.isPaid) {
return
}
app.ensureLogin().then((logged) => {
if (!logged) {
wx.showToast({ title: '请先登录后再解锁', icon: 'none' })
return
}
payment.purchaseFaceTest({
testResultId,
success: () => {
wx.showToast({ title: '已解锁完整报告', icon: 'success' })
// 本地先标记已付费,避免按钮仍然提示“需要解锁”
this.setData({
'payInfo.isPaid': true
})
// 避免重复触发刷新:只在还没刷新的情况下,延迟 0.5s 拉一次详情
if (testResultId && !hasReloadedAfterPay) {
this.setData({ hasReloadedAfterPay: true })
setTimeout(() => {
this.reloadFullDetail(testResultId)
}, 500)
}
},
fail: () => {
// 支付失败或取消,这里不做额外处理
}
})
})
},
// AI结果页付费按钮就地触发微信手机号授权然后调用 unlockFullReport
onGetPhoneNumberForFacePay(e) {
if (!ensureProfileCompleteAndRedirect()) return
const { code, errMsg } = e.detail || {}
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
if (!hasPhone()) {
wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' })
return
}
this.unlockFullReport()
return
}
if (!code) {
if (hasPhone()) {
this.unlockFullReport()
} else {
wx.showToast({ title: '获取手机号失败', icon: 'none' })
}
return
}
bindPhoneByCode(code)
.then(() => {
this.setData({ hasPhone: true })
this.unlockFullReport()
})
.catch(() => {
// 保持在当前页,等待用户重新点击
})
},
// 解锁成功后根据测试记录ID重新拉取完整详情
reloadFullDetail(id) {
if (!id) return
const app = getApp()
const apiBase = app.globalData?.apiBase || ''
wx.showLoading({ title: '加载完整报告...' })
wx.request({
url: `${apiBase}/api/test/detail`,
method: 'GET',
header: {
'Authorization': app.globalData?.token ? `Bearer ${app.globalData.token}` : ''
},
data: { id },
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.data) {
const payload = res.data.data
const apiData = payload.data || payload
// 详情接口在后台会根据 isPaid 返回完整结构,这里直接复用已有处理逻辑
this.processResult(apiData)
// 同步数据库中的付费状态与金额test_results.paidAmount
const recordRequires =
typeof payload.requiresPayment === 'boolean' || typeof payload.requiresPayment === 'number'
? !!payload.requiresPayment
: null
const recordIsPaid = !!payload.isPaid
this.initPayInfoFromRuntime(recordRequires, recordIsPaid, payload)
} else {
console.error('刷新完整报告失败', res)
}
},
fail: (err) => {
console.error('请求完整报告失败', err)
},
complete: () => {
wx.hideLoading()
}
})
},
// 无人脸错误:显示提示并提供重新拍摄入口
showNoFaceError(message) {
this.setData({
isAnalyzing: false,
showResult: false,
noFaceError: true,
noFaceMessage: message || '图片中未检测到人脸,请重新拍摄清晰的正面照片'
})
},
// 分析失败:不使用任何本地模拟数据
showAnalyzeError(message) {
this.setData({
isAnalyzing: false,
showResult: true,
hasError: true,
errorMessage: message || '分析失败,请稍后重试'
})
},
// 跳转到详情性格测试选择页MBTI / PDP / DISC 三选一)
goToMBTI() {
wx.navigateTo({ url: '/pages/test-select/index' })
},
shareResult() {
wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] })
},
retake() {
// camera 是 tabBar 页面,必须用 switchTab
wx.switchTab({ url: '/pages/index/camera' })
},
goHome() {
// 根据当前 scope 跳对应首页
const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal'
if (scope === 'enterprise') {
wx.navigateTo({ url: '/pages/enterprise/index' })
} else {
wx.switchTab({ url: '/pages/index/index' })
}
},
onShareAppMessage() {
const r = this.data.result
const rm = this.data.reviewMode
const t = rm ? '测试结果' : (this.data.aiAnalysisText || '智能分析')
const { getSharePathByScope } = require('../../utils/share')
return {
title: `${t}:我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`,
path: getSharePathByScope('/pages/index/index')
}
},
onShareTimeline() {
const r = this.data.result
const rm = this.data.reviewMode
const t = rm ? '测试结果' : (this.data.aiAnalysisText || '智能分析')
const { buildShareQuery } = require('../../utils/share')
return {
title: `${t}:我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`,
query: buildShareQuery()
}
}
})