// pages/result/resume.js - 简历综合分析结果页 const app = getApp() const payment = require('../../utils/payment') const { getEnterpriseIdForApiPayload } = require('../../utils/enterpriseContext.js') const { triggerTestResultCompleted } = require('../../utils/pushHook') Page({ data: { loading: true, error: '', content: '', sections: [], resumeData: null, fileUrl: '', progress: 0, analyzingTip: '正在准备分析数据...', analyzingTitle: '综合分析生成中', // 支付相关 payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 }, testResultId: 0, paying: false, profileGate: false }, onLoad(options) { const fileUrl = options.fileUrl ? decodeURIComponent(options.fileUrl) : '' this.setData({ fileUrl }) const run = () => { if (options.id && options.type === 'resume') { this.loadFromHistory(options.id) } else { this.fetchResumeAnalysis() } } // 分享直达:需 token 调 detail / analyze,等静默登录完成再请求,避免白屏 app.ensureLogin() .then((ok) => { if (!ok) { this.setData({ loading: false, error: '请先登录后查看报告;若已登录请下拉重试或返回重进。' }) return } run() }) .catch(() => { this.setData({ loading: false, error: '登录失败,请返回重试。' }) }) }, onShow() { const id = this.data.testResultId if (id && this.data.profileGate) { this.loadFromHistory(String(id)) } }, goCompleteProfile() { wx.navigateTo({ url: '/pages/user-profile/index' }) }, loadFromHistory(id) { this.setData({ loading: true, error: '', content: '', sections: [], resumeData: null, profileGate: false }) const apiBase = (app.globalData && app.globalData.apiBase) || wx.getStorageSync('apiBase') || '' const token = (app.globalData && app.globalData.token) || wx.getStorageSync('token') || '' wx.request({ url: `${apiBase}/api/test/detail?id=${id}`, method: 'GET', timeout: 15000, header: { 'Authorization': token ? `Bearer ${token}` : '' }, success: (res) => { if (res.statusCode === 200 && res.data && res.data.data) { const payload = res.data.data let data = payload.data || payload // 兼容:历史数据可能是字符串,或包在 content 里的 JSON 字符串 data = this.normalizeStructuredData(data) const profileIncomplete = payload.profileIncomplete === true const locked = !!(data && data.locked) const structured = !!data && data._structured === true const requiresPayment = !!payload.requiresPayment const isPaid = !!payload.isPaid const amountYuan = payload.paidAmount ? payload.paidAmount / 100 : 0 const payWall = requiresPayment && !isPaid const profileGate = !payWall && (profileIncomplete || locked) let content = '' let sections = [] let resumeData = null if (payWall || profileGate) { content = '' sections = [] resumeData = null } else if (structured) { const built = this.buildSectionsFromStructured(data) content = built.content sections = built.sections resumeData = data } else { const raw = data.content || '' content = raw sections = this.parseContent(raw) } this.setData({ loading: false, testResultId: payload.id || parseInt(id) || 0, content, sections, resumeData, profileGate, payInfo: { requiresPayment: payWall, isPaid, amountYuan } }) } else { this.setData({ loading: false, error: '加载历史记录失败,请返回重试。' }) } }, fail: () => { this.setData({ loading: false, error: '网络请求失败,请检查网络后重试。' }) } }) }, fetchResumeAnalysis() { this.setData({ loading: true, error: '', content: '', sections: [], resumeData: null, progress: 0 }) const tips = [ '正在汇总各项测评结果...', '正在解析简历内容...', 'AI 正在进行深度匹配分析...', '正在生成综合评估报告...', '分析即将完成,请稍候...' ] let progress = 0 let tipIndex = 0 // 进度动画 const timer = setInterval(() => { progress += 2 if (progress > 98) progress = 98 if (progress > (tipIndex + 1) * 18 && tipIndex < tips.length - 1) tipIndex++ this.setData({ progress: Math.floor(progress), analyzingTip: tips[tipIndex] }) }, 200) const apiBase = (app.globalData && app.globalData.apiBase) || wx.getStorageSync('apiBase') || '' const token = (app.globalData && app.globalData.token) || wx.getStorageSync('token') || '' const enterpriseId = getEnterpriseIdForApiPayload() const postData = {} if (this.data.fileUrl) postData.fileUrl = this.data.fileUrl if (enterpriseId) postData.enterpriseId = enterpriseId wx.request({ url: `${apiBase}/api/resume/analyze`, method: 'POST', timeout: 120000, header: { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '' }, data: postData, success: (res) => { clearInterval(timer) if (res.statusCode === 200 && res.data) { if (res.data.code === 200) { let d = res.data.data || {} const p = d._payment || {} const requiresPayment = !!p.requiresPayment const amountYuan = p.amountYuan || 0 const resultId = d._testResultId || 0 if (resultId) { triggerTestResultCompleted(resultId) } // 兼容:API 直接返回结构化对象,或把 JSON 放在 content 里 d = this.normalizeStructuredData(d) const locked = !!(d && d.locked) const structured = !!d && d._structured === true let content = '' let sections = [] let resumeData = null let profileGate = false if (requiresPayment) { profileGate = false } else if (locked) { profileGate = true } else if (structured) { const built = this.buildSectionsFromStructured(d) content = built.content sections = built.sections resumeData = d } else { const raw = d.content || '' content = raw || '分析已完成,但未返回可展示的内容。' sections = this.parseContent(raw) } this.setData({ loading: false, progress: 100, testResultId: resultId, content: requiresPayment ? '' : content, sections: requiresPayment ? [] : sections, resumeData, profileGate, payInfo: { requiresPayment, isPaid: !requiresPayment, amountYuan } }) } else { this.setData({ loading: false, error: res.data.message || res.data.msg || '综合分析失败,请稍后重试。' }) } } else { this.setData({ loading: false, error: '服务器返回异常,请稍后重试。' }) } }, fail: (err) => { clearInterval(timer) const isTimeout = err && (err.errMsg || '').indexOf('timeout') !== -1 this.setData({ loading: false, error: isTimeout ? 'AI 分析耗时较长,请点击「重新生成」再试。' : '网络请求失败,请检查网络后重试。' }) } }) }, // 支付解锁 doPay() { if (this.data.paying) return this.setData({ paying: true }) const testResultId = this.data.testResultId || 0 payment.purchaseResumeAnalysis({ testResultId: testResultId > 0 ? testResultId : undefined, success: () => { this.setData({ paying: false, 'payInfo.requiresPayment': false, 'payInfo.isPaid': true }) // 支付成功后重新拉取完整内容 if (testResultId > 0) { this.loadFromHistory(testResultId) } }, fail: (err) => { this.setData({ paying: false }) wx.showToast({ title: (err && err.message) || '支付失败,请重试', icon: 'none' }) } }) }, goHome() { const scope = (app.globalData && app.globalData.appScope) || 'personal' if (scope === 'enterprise') { wx.navigateTo({ url: '/pages/enterprise/index' }) } else { wx.switchTab({ url: '/pages/index/index' }) } }, onShareAppMessage() { return { title: '我刚刚完成了一份人才简历综合分析报告,快来看看吧!', path: '/pages/index/index' } }, onPullDownRefresh() { this.fetchResumeAnalysis() wx.stopPullDownRefresh() }, // 智能解析 AI 输出的长文本,按标题分块 parseContent(content) { if (!content) return []; // 1. 预处理:去掉首尾空行 content = content.trim(); // 2. 尝试分割 // 使用正则匹配头部标记:## Title 或 ### Title 或 **一、Title** 或 一、**Title** const parts = content.split(/\n(?=#{2,4}\s+|\*\*?[\d一二三四五六七八九十]+[、\.].*?\*\*?|【.*?】)/); const sections = []; parts.forEach(part => { let title = ''; let body = ''; // 匹配 ## 标题 const hMatch = part.match(/^(?:#{2,4}\s+)(.*?)\n([\s\S]*)$/m); // 匹配 **一、标题** 或 一、**标题** const bMatch = part.match(/^(?:\*\*?[\d一二三四五六七八九十]+[、\.].*?\*\*?)([\s\S]*)$/m); // 匹配 【标题】 const kMatch = part.match(/^(?:【(.*?)】)\n?([\s\S]*)$/m); if (hMatch) { title = hMatch[1].replace(/[\*#]/g, '').trim(); body = hMatch[2].trim(); } else if (kMatch) { title = kMatch[1].trim(); body = kMatch[2].trim(); } else if (bMatch) { const rawTitleMatch = part.match(/^(\*\*?.*?\*\*?)/); title = rawTitleMatch ? rawTitleMatch[1].replace(/[\*#]/g, '').trim() : ''; body = bMatch[1].trim(); } else { body = part.trim(); } // 清洗正文中的 Markdown 符号 body = this.cleanMarkdown(body); if (title || body) { sections.push({ title: title || '报告详情', icon: this.getSectionIcon(title), body: body }); } }); return sections; }, // 兼容各种后端返回形态,统一整理为结构化对象或普通文本 normalizeStructuredData(raw) { let data = raw // 字符串:尝试直接当 JSON 解析 if (typeof data === 'string') { try { const parsed = JSON.parse(data) if (parsed && typeof parsed === 'object') { data = parsed } } catch (e) {} } // 包在 content 里的 JSON 字符串 if (data && typeof data === 'object' && typeof data.content === 'string') { const text = data.content.trim() if (text.startsWith('{')) { try { const parsedInner = JSON.parse(text) if (parsedInner && typeof parsedInner === 'object') { // 把外层 fileUrl 等透传下去 if (data.fileUrl && !parsedInner.fileUrl) { parsedInner.fileUrl = data.fileUrl } data = parsedInner } } catch (e) {} } } // 标记是否为结构化 JSON if ( data && typeof data === 'object' && (data.version === 2 || !!data.overview || !!data.portrait || !!data.hrView || !!data.bossView) ) { data._structured = true } return data }, // 从结构化 JSON 构建前端展示区块 buildSectionsFromStructured(data) { const sections = [] if (data.overview) { sections.push({ title: '整体人才画像', icon: '👤', body: this.cleanMarkdown(data.overview) }) } if (data.resumeHighlights) { sections.push({ title: '简历要点', icon: '📄', body: this.cleanMarkdown(data.resumeHighlights) }) } const portrait = data.portrait || {} if ( (portrait.coreStrengths && portrait.coreStrengths.length) || (portrait.coreRisks && portrait.coreRisks.length) || portrait.workStyle ) { const lines = [] if (portrait.coreStrengths && portrait.coreStrengths.length) { lines.push('【核心优势】') lines.push(...portrait.coreStrengths.map((s) => '- ' + s)) } if (portrait.coreRisks && portrait.coreRisks.length) { lines.push('') lines.push('【潜在风险】') lines.push(...portrait.coreRisks.map((s) => '- ' + s)) } if (portrait.workStyle) { lines.push('') lines.push('【工作风格】') lines.push(portrait.workStyle) } sections.push({ title: '人才画像', icon: '🌈', body: this.cleanMarkdown(lines.join('\n')) }) } const hrView = data.hrView || {} const role = hrView.roleRecommend || {} if ((role.bestFit && role.bestFit.length) || (role.notSuitable && role.notSuitable.length)) { const lines = [] if (role.bestFit && role.bestFit.length) { lines.push('【推荐岗位】') lines.push(...role.bestFit.map((s) => '- ' + s)) } if (role.notSuitable && role.notSuitable.length) { lines.push('') lines.push('【不适合场景】') lines.push(...role.notSuitable.map((s) => '- ' + s)) } sections.push({ title: '岗位匹配建议(HR视角)', icon: '🧭', body: this.cleanMarkdown(lines.join('\n')) }) } if (hrView.lifecycle) { const lc = hrView.lifecycle const parts = [] if (lc.onboarding) parts.push('【入职】' + lc.onboarding) if (lc.probation) parts.push('【试用期】' + lc.probation) if (lc.growth) parts.push('【成长】' + lc.growth) if (lc.retention) parts.push('【留存】' + lc.retention) if (parts.length) { sections.push({ title: '员工全生命周期预测', icon: '📈', body: this.cleanMarkdown(parts.join('\n\n')) }) } } if (hrView.performance || hrView.teamFit || hrView.complianceRisk) { const lines = [] if (hrView.performance) { lines.push('【绩效潜力】' + (hrView.performance.potential || '')) if (hrView.performance.drivers && hrView.performance.drivers.length) { lines.push('驱动因素:' + hrView.performance.drivers.join(';')) } if (hrView.performance.risks && hrView.performance.risks.length) { lines.push('风险提示:' + hrView.performance.risks.join(';')) } lines.push('') } if (hrView.teamFit) { if (hrView.teamFit.bestTeam) { lines.push('【适配团队】' + hrView.teamFit.bestTeam) } if (hrView.teamFit.manageAdvice) { lines.push('【管理建议】' + hrView.teamFit.manageAdvice) } lines.push('') } if (hrView.complianceRisk) { const level = hrView.complianceRisk.level || '' const notes = hrView.complianceRisk.notes || '' lines.push('【合规风险】' + level + (notes ? ';' + notes : '')) } if (lines.length) { sections.push({ title: '绩效 · 团队 · 合规', icon: '📊', body: this.cleanMarkdown(lines.join('\n')) }) } } const bossView = data.bossView || {} if (bossView.headline || (bossView.metrics && bossView.metrics.length) || bossView.costInsight) { const lines = [] if (bossView.headline) { lines.push('【一句话结论】' + bossView.headline) } if (bossView.metrics && bossView.metrics.length) { lines.push('') lines.push('【关键指标】') bossView.metrics.forEach((m) => { if (!m) return lines.push(`- ${m.label}:${m.value || ''}`) }) } if (bossView.costInsight) { lines.push('') lines.push('【用人成本与产出】' + bossView.costInsight) } sections.push({ title: '给老板看的摘要', icon: '💼', body: this.cleanMarkdown(lines.join('\n')) }) } const mainContent = data.overview || data.resumeHighlights || (data.portrait && data.portrait.workStyle) || '' return { content: this.cleanMarkdown(mainContent), sections } }, // 清洗正文中的 Markdown 符号 cleanMarkdown(text) { if (!text) return ''; return text .replace(/\*\*\*(.+?)\*\*\*/g, '$1') // ***bold italic*** .replace(/\*\*(.+?)\*\*/g, '$1') // **bold** .replace(/\*(.+?)\*/g, '$1') // *italic* .replace(/^#{1,6}\s+/gm, '') // ## 标题符号 .replace(/^[-*]\s+/gm, '• ') // - 无序列表 → 圆点 .replace(/^\d+\.\s+/gm, (m) => m) // 保留有序列表编号 .replace(/`(.+?)`/g, '$1') // `code` .replace(/\[(.+?)\]\(.+?\)/g, '$1') // [链接](url) → 链接文字 .trim(); }, // 根据标题关键词匹配图标 getSectionIcon(title) { if (!title) return '📝'; const map = { '评估': '📊', '综述': '📊', '评价': '📊', '总评': '📊', '优势': '🌟', '特质': '🌟', '性格': '🌟', '岗位': '🎯', '适配': '🎯', '匹配': '🎯', '核心': '🎯', '职业': '💼', '发展': '💼', '建议': '💡', '提升': '💡', '面试': '🗣️', '录用': '✅', '风险': '⚠️' }; for (let key in map) { if (title.indexOf(key) !== -1) return map[key]; } return '📝'; } })