高考版优化

审核模式优化
This commit is contained in:
Ghost
2026-04-29 18:01:26 +08:00
parent 59419c4ce9
commit a411bf7b21
134 changed files with 19898 additions and 3371 deletions

View File

@@ -3,6 +3,10 @@ const { request } = require('../../utils/request.js')
const analytics = require('../../utils/analytics.js')
const { buildRecoMiniProgramPath } = require('../../utils/recoMiniProgram.js')
const aiChatIntent = require('../../utils/aiChatIntent.js')
const { isAuditHideAiMode, redirectIfMiniprogramAudit } = require('../../utils/miniprogramAuditGate.js')
const { buildDeepHeroDesc, filterCategories } = require('../../utils/deepPricingFilter.js')
const payment = require('../../utils/payment')
const { hasPhone, bindPhoneByCode } = require('../../utils/phoneAuth.js')
/** 功能类推荐卡「阅读数」按发布日起算时的锚点日期 */
const FEATURE_RECO_PUBLISHED_AT = '2026-01-01'
@@ -12,6 +16,45 @@ const REPORT_CTA_MIN_USER_TURNS = 10
/** 报告条仅展示一次(点过或离开带条页面后不再出现) */
const STORAGE_REPORT_CTA_DISMISSED = 'ai_chat_deep_report_cta_dismissed'
function embeddedDeepCategories(scope) {
const app = getApp()
const gd = (app && app.globalData) || {}
const list = scope === 'enterprise' ? gd.deepPricingEnterprise : gd.deepPricingPersonal
return Array.isArray(list) && list.length ? list : null
}
function rememberCategoryKeys(cat, seen) {
const id = String((cat && cat.id) || '')
const pk = String((cat && cat.productKey) || '')
if (id) seen.add('id:' + id)
if (pk) seen.add('pk:' + pk)
}
function categorySeenInSet(cat, seen) {
const id = String((cat && cat.id) || '')
const pk = String((cat && cat.productKey) || '')
if (id && seen.has('id:' + id)) return true
if (pk && seen.has('pk:' + pk)) return true
return false
}
function mergeDeepPricingFromRuntime(scope, apiList) {
let list = Array.isArray(apiList) ? apiList.slice() : []
const emb = embeddedDeepCategories(scope)
if (!emb || !emb.length) return list
if (!list.length) return emb.slice()
if (list.length === 1 && emb.length > list.length) return emb.slice()
const seen = new Set()
list.forEach((c) => rememberCategoryKeys(c, seen))
emb.forEach((c) => {
if (!categorySeenInSet(c, seen)) {
list.push(c)
rememberCategoryKeys(c, seen)
}
})
return list
}
// 兜底封面base64 SVG紫色渐变 + sparkle
const DEFAULT_COVER = 'data:image/svg+xml;utf8,' + encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 440 220">' +
@@ -63,12 +106,33 @@ Page({
inlineRecoInterval: 3,
inlineRecoRoll: 0.5,
inlineRecoIconCount: 3,
inlineRecoIconsPool: ['✨', '💬', '📌']
inlineRecoIconsPool: ['✨', '💬', '📌'],
/** 聊天区固定高度px与底部固定 composer 对齐,避免 scroll-view 被内容撑开压到输入条下 */
chatScrollHeightPx: 500,
auditPurchaseMode: false,
auditPurchaseLoading: false,
auditPurchaseError: '',
auditPurchaseTab: 'personal',
auditPurchasePersonal: [],
auditPurchaseEnterprise: [],
auditDeepHeroDesc: '',
auditHasPhone: false,
auditPurchasing: false,
auditSuccessModal: {
visible: false,
title: '',
content: ''
}
},
onLoad(options) {
const { ensureRuntimeThenGate } = require('../../utils/miniprogramAuditGate.js')
ensureRuntimeThenGate(() => {
this._fallbackChatScrollHeight()
const app = getApp()
const boot = () => {
if (isAuditHideAiMode(app && app.globalData)) {
redirectIfMiniprogramAudit('功能升级中')
return
}
const cid = parseInt(options && options.cid, 10)
if (cid > 0) {
this.setData({ conversationId: cid })
@@ -97,7 +161,12 @@ Page({
} else {
pullRuntime()
}
})
}
if (app && typeof app.getRuntimeConfig === 'function') {
app.getRuntimeConfig().then(boot).catch(boot)
} else {
boot()
}
},
/** 分享链接里带 inviterId 时,绑定分销关系(复用现有分销系统) */
@@ -143,20 +212,21 @@ Page({
tryUrl('/api/ai/my-report/latest', () => tryUrl('/api/ai/report/my-latest'))
},
onReady() {
this._measureChatScrollHeight()
},
onShow() {
const { redirectIfMiniprogramAudit } = require('../../utils/miniprogramAuditGate.js')
if (redirectIfMiniprogramAudit()) return
try {
const tb = typeof this.getTabBar === 'function' ? this.getTabBar() : null
if (tb && typeof tb.updateSelected === 'function') {
tb.updateSelected()
} else {
const c = typeof this.selectComponent === 'function' ? this.selectComponent('#custom-tab-bar') : null
if (c && typeof c.refreshFromConfig === 'function') c.refreshFromConfig()
else if (c && typeof c.updateSelected === 'function') c.updateSelected()
}
} catch (e) {}
const app = getApp()
if (isAuditHideAiMode(app && app.globalData)) {
redirectIfMiniprogramAudit('功能升级中')
return
}
if (this.data.auditPurchaseMode) {
this.setData({ auditPurchaseMode: false })
}
const tb = typeof this.getTabBar === 'function' ? this.getTabBar() : null
if (tb && typeof tb.updateSelected === 'function') tb.updateSelected()
try {
const st = wx.getStorageSync('token')
if (st && app && app.globalData && !app.globalData.token) {
@@ -166,10 +236,21 @@ Page({
this.loadQuickQuestions()
this.loadMyReportStatus()
this._refreshReportCta()
if (typeof wx.onWindowResize === 'function' && !this._chatResizeListening) {
this._winResizeHandler = () => this._scheduleMeasureChatScroll()
wx.onWindowResize(this._winResizeHandler)
this._chatResizeListening = true
}
this._scheduleMeasureChatScroll()
},
/** 离开对话页时若报告条正在展示,记为已曝光,避免反复出现 */
onHide() {
if (this._chatResizeListening && this._winResizeHandler && typeof wx.offWindowResize === 'function') {
wx.offWindowResize(this._winResizeHandler)
this._chatResizeListening = false
this._winResizeHandler = null
}
if (this.data.showReportCta) {
this._dismissReportCtaPermanently()
}
@@ -179,6 +260,246 @@ Page({
this.setData({ draft: e.detail.value })
},
onInputFocus() {
this._scheduleMeasureChatScroll()
},
onAuditPurchaseSwitchTab(e) {
const tab = (e.currentTarget.dataset.tab || '') === 'enterprise' ? 'enterprise' : 'personal'
this.setData({ auditPurchaseTab: tab })
},
_enterAuditPurchaseMode(options) {
const tab = options && options.tab === 'enterprise' ? 'enterprise' : this.data.auditPurchaseTab
const app = getApp()
const ep = (app && app.globalData && app.globalData.enterprisePermissions) || null
this.setData({
auditPurchaseMode: true,
auditPurchaseTab: tab,
auditDeepHeroDesc: buildDeepHeroDesc(ep),
auditHasPhone: hasPhone()
})
this._loadAuditPurchasePricing()
},
_loadAuditPurchasePricing() {
this.setData({ auditPurchaseLoading: true, auditPurchaseError: '' })
Promise.all([this._requestAuditDeepPricing('personal'), this._requestAuditDeepPricing('enterprise')])
.then(([personal, enterprise]) => {
const pErr = personal && personal.__error
const eErr = enterprise && enterprise.__error
const pList = Array.isArray(personal) ? personal : []
const eList = Array.isArray(enterprise) ? enterprise : []
const app = getApp()
const ep = (app && app.globalData && app.globalData.enterprisePermissions) || null
this.setData({
auditPurchasePersonal: filterCategories(pList, ep),
auditPurchaseEnterprise: filterCategories(eList, ep),
auditDeepHeroDesc: buildDeepHeroDesc(ep),
auditPurchaseLoading: false,
auditPurchaseError: pErr && eErr
? (personal.__errorMsg || enterprise.__errorMsg || '加载失败,请稍后重试')
: ((!pList.length && !eList.length) ? '暂无可展示的套餐' : '')
})
})
.catch(() => {
this.setData({
auditPurchaseLoading: false,
auditPurchaseError: '加载失败,请稍后重试'
})
})
},
_requestAuditDeepPricing(scope, triedRuntime) {
const app = getApp()
const apiBase = (((app && app.globalData && app.globalData.apiBase) || '') + '').replace(/\/$/, '')
if (!apiBase) {
if (!triedRuntime && app && typeof app.getRuntimeConfig === 'function') {
return app
.getRuntimeConfig()
.catch(() => null)
.then(() => this._requestAuditDeepPricing(scope, true))
}
return Promise.resolve({ __error: true, __errorMsg: '服务地址未配置' })
}
return new Promise((resolve) => {
wx.request({
url: `${apiBase}/api/config/deep-pricing`,
method: 'GET',
data: { scope },
timeout: 25000,
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 200 && Array.isArray(res.data.data && res.data.data.categories)) {
const merged = mergeDeepPricingFromRuntime(scope, res.data.data.categories)
resolve(merged)
} else {
const emb = embeddedDeepCategories(scope)
if (emb && emb.length) {
resolve(emb.slice())
return
}
resolve({
__error: true,
__errorMsg: (res && res.data && res.data.message) || '响应异常'
})
}
},
fail: (err) => {
const emb = embeddedDeepCategories(scope)
if (emb && emb.length) {
resolve(emb.slice())
return
}
resolve({
__error: true,
__errorMsg: (err && err.errMsg) || '网络异常'
})
}
})
})
},
handleAuditPurchaseTap(e) {
const tab = e.currentTarget.dataset.tab
const index = e.currentTarget.dataset.index
this._handleAuditPurchase(tab, index)
},
onGetPhoneNumberForAuditPurchase(e) {
const tab = e.currentTarget.dataset.tab
const index = e.currentTarget.dataset.index
const { code, errMsg } = e.detail || {}
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
if (!hasPhone()) {
wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' })
return
}
this._handleAuditPurchase(tab, index)
return
}
if (!code) {
if (hasPhone()) {
this._handleAuditPurchase(tab, index)
} else {
wx.showToast({ title: '获取手机号失败', icon: 'none' })
}
return
}
bindPhoneByCode(code)
.then(() => {
this.setData({ auditHasPhone: true })
this._handleAuditPurchase(tab, index)
})
.catch(() => {})
},
_handleAuditPurchase(tab, index) {
if (index === undefined || index === null) return
const isEnterprise = tab === 'enterprise'
const list = isEnterprise ? this.data.auditPurchaseEnterprise : this.data.auditPurchasePersonal
const category = list[index]
if (!category) return
this.setData({ auditPurchaseTab: isEnterprise ? 'enterprise' : 'personal' })
if (category.actionType === 'buy' && category.productKey) {
this._purchaseAuditPersonal(category)
} else {
this._applyAuditConsult(category)
}
},
_purchaseAuditPersonal(category) {
if (this.data.auditPurchasing) return
this.setData({ auditPurchasing: true })
wx.showLoading({ title: '处理中...', mask: true })
const deepProductId = category.id || category.productKey || ''
const title = category.title || '个人深度服务1v1深度解读'
payment.purchasePersonalDeepService({
deepProductId,
description: title,
success: () => {
wx.hideLoading()
this.setData({ auditPurchasing: false })
this._reportAuditCrmLead(category, 'buy')
const successMsg = (category.successMessage || '购买成功!我们的顾问会尽快与您联系,为您提供专属深度解读服务。').trim()
this._showAuditSuccessModal('购买成功', successMsg)
},
fail: () => {
wx.hideLoading()
this.setData({ auditPurchasing: false })
}
})
},
_applyAuditConsult(category) {
const successMsg = (category.successMessage || '感谢您的申请,我们的顾问会尽快与您联系!').trim()
wx.showLoading({ title: '提交中...', mask: true })
this._reportAuditCrmLead(category, 'consult', () => {
wx.hideLoading()
this._showAuditSuccessModal('申请成功', successMsg)
})
},
_showAuditSuccessModal(title, content) {
this.setData({
auditSuccessModal: {
visible: true,
title: title || '成功',
content: content || ''
}
})
},
catchTapAuditSuccess() {},
closeAuditSuccessModal() {
this.setData({ 'auditSuccessModal.visible': false })
},
_reportAuditCrmLead(category, actionType, onDone) {
const app = getApp()
const apiBase = (((app && app.globalData && app.globalData.apiBase) || '') + '').replace(/\/$/, '')
const apiKey = (category.consultWechat || '').trim()
const isConsult = actionType === 'consult'
const deepConsult = isConsult
const finish = () => {
if (typeof onDone === 'function') onDone()
}
if (!apiBase) {
finish()
return
}
if (!deepConsult && !apiKey) {
finish()
return
}
const isEnterprise = this.data.auditPurchaseTab === 'enterprise'
const source = (isEnterprise ? '企业深度服务' : '个人深度服务') + (category.title ? `-${category.title}` : '')
const remark =
actionType === 'buy'
? '完成付款'
: isEnterprise
? '申请咨询并降低30%成本'
: '申请咨询'
wx.request({
url: `${apiBase}/api/crm/report`,
method: 'POST',
header: {
Authorization: `Bearer ${wx.getStorageSync('token') || ''}`,
'Content-Type': 'application/json'
},
data: {
apiKey,
source,
remark,
siteTags: category.title || '',
deepConsult: deepConsult ? true : false
},
success() {},
fail() {},
complete: finish
})
},
// ---------- 数据加载 ----------
loadArticles() {
@@ -368,24 +689,32 @@ Page({
const apply = (body) => {
if (!body || body.code !== 200) return false
const d = body.data || {}
this.setData({
mbtiType: d.mbtiType || '',
nickname: d.nickname || '',
quickQuestions: this.buildQuickQuestions(d.questions || [])
})
this.setData(
{
mbtiType: d.mbtiType || '',
nickname: d.nickname || '',
quickQuestions: this.buildQuickQuestions(d.questions || [])
},
() => this._scheduleMeasureChatScroll()
)
return true
}
const applyDirect = (d) => {
if (!d || !Array.isArray(d.questions) || d.questions.length === 0) return false
this.setData({
mbtiType: d.mbtiType || '',
nickname: d.nickname || '',
quickQuestions: this.buildQuickQuestions(d.questions)
})
this.setData(
{
mbtiType: d.mbtiType || '',
nickname: d.nickname || '',
quickQuestions: this.buildQuickQuestions(d.questions)
},
() => this._scheduleMeasureChatScroll()
)
return true
}
const fallbackLocal = () => {
this.setData({ quickQuestions: this.buildQuickQuestions([]) })
this.setData({ quickQuestions: this.buildQuickQuestions([]) }, () =>
this._scheduleMeasureChatScroll()
)
}
const tryLegacy = () => {
request({
@@ -468,6 +797,8 @@ Page({
const msgs = (d.messages || []).filter(m => m.role !== 'system')
this.setData({ messages: msgs }, () => {
this.scrollToBottom()
setTimeout(() => this.scrollToBottom(), 120)
this._scheduleMeasureChatScroll()
this._refreshReportCta()
})
}
@@ -586,7 +917,7 @@ Page({
sending: true
},
() => {
this.setData({ scrollTarget: 'msg-typing' })
this.scrollToBottom()
}
)
@@ -658,6 +989,8 @@ Page({
setPayload,
() => {
this.scrollToBottom()
setTimeout(() => this.scrollToBottom(), 120)
setTimeout(() => this.scrollToBottom(), 380)
this._refreshReportCta()
}
)
@@ -895,14 +1228,77 @@ Page({
})
},
scrollToBottom() {
if (this.data.sending) {
this.setData({ scrollTarget: 'msg-typing' })
return
_fallbackChatScrollHeight() {
try {
const sys = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync()
const wh = sys.windowHeight || 600
const sw = sys.screenWidth || 375
const rpx2px = (r) => (r / 750) * sw
const inset = sys.safeAreaInsets || {}
const safeBottom = typeof inset.bottom === 'number' ? inset.bottom : 0
// Tab 区 + 固定 composer含快捷语行在 rpx 上的经验预留
const reserve = rpx2px(168 + 300) + safeBottom
const h = Math.max(200, Math.floor(wh - reserve - rpx2px(20)))
this.setData({ chatScrollHeightPx: h })
} catch (e) {
this.setData({ chatScrollHeightPx: 500 })
}
const msgs = this.data.messages
const last = msgs.length ? msgs[msgs.length - 1] : null
this.setData({ scrollTarget: last ? ('msg-' + last.id) : 'bottom' })
},
_scheduleMeasureChatScroll() {
if (this._chatLayoutTimer) clearTimeout(this._chatLayoutTimer)
this._chatLayoutTimer = setTimeout(() => {
this._chatLayoutTimer = null
this._measureChatScrollHeight()
}, 80)
},
_measureChatScrollHeight(retry) {
wx.nextTick(() => {
wx.createSelectorQuery()
.in(this)
.select('.chat-scroll')
.boundingClientRect()
.select('.composer-wrap')
.boundingClientRect()
.exec((res) => {
const scrollRect = res && res[0]
const composerRect = res && res[1]
if (!scrollRect || !composerRect || typeof composerRect.top !== 'number') {
this._fallbackChatScrollHeight()
return
}
const gapPx = 6
const h = composerRect.top - scrollRect.top - gapPx
if (h > 120) {
this.setData({ chatScrollHeightPx: Math.round(h) }, () => {
this.scrollToBottom()
})
} else if (!retry) {
setTimeout(() => this._measureChatScrollHeight(true), 200)
} else {
this._fallbackChatScrollHeight()
}
})
})
},
/**
* 滚到底部(对齐微信聊天:新发/收信后总能看到最新消息)。
* scroll-view 的 scroll-into-view 在连续设为同一 id 时不会触发,需先清空再设;
* 锚点统一用 #bottom位于打字条、报告条之后避免滚到最后一条气泡 id 仍被底部输入区挡住。
*/
scrollToBottom() {
const go = () => {
this.setData({ scrollTarget: 'bottom' })
}
this.setData({ scrollTarget: '' }, () => {
if (typeof wx !== 'undefined' && typeof wx.nextTick === 'function') {
wx.nextTick(go)
} else {
setTimeout(go, 20)
}
})
},
/** 对话内上传简历:直传 OSS 后走 /api/ai/chat由神仙 AI 结合测评与正文回复(不跳转匹配工作页) */