Files
MBTI_wang/miniprogram/pages/purchase/index.js
卡若 ce55c48c64 feat: 同步今日小程序与后台迭代版本
集中提交今日 API、管理端、小程序与部署文档调整,确保 Gitea 主分支与本地最新开发版本一致。

Made-with: Cursor
2026-04-20 11:07:55 +08:00

428 lines
14 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/purchase/index.js - 开通会员(深度服务价格:个人/企业区分,类目由后端配置可新增)
// 禁止模块顶层 getApp():上传/首屏偶发 App 未就绪会导致空白页
const payment = require('../../utils/payment')
const { hasPhone, bindPhoneByCode } = require('../../utils/phoneAuth.js')
const {
buildDeepHeroDesc,
filterCategories
} = require('../../utils/deepPricingFilter.js')
function appSafe() {
try {
return getApp()
} catch (e) {
return { globalData: {} }
}
}
/** runtime 内嵌的深度类目(与 deep-pricing 同源) */
function embeddedDeepCategories(scope) {
const gd = appSafe().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
}
/**
* 合并接口与 runtime 类目:缺条、仅 1 条而 runtime 更多时以 runtime 为准;否则按 id/productKey 并集补全(防漏档)
*/
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
}
Page({
data: {
activeTab: 'personal',
personalCategories: [],
enterpriseCategories: [],
deepHeroDesc: '',
loading: true,
loadError: false,
loadErrorMsg: '',
purchasing: false,
hasPhone: false,
successModal: {
visible: false,
title: '',
content: ''
}
},
retryLoad() {
this.loadDeepPricing()
},
/** 提审模式:深度套餐属虚拟商品,关闭本页并返回 */
_exitPurchaseForAudit() {
if (this._purchaseAuditExitScheduled) return
this._purchaseAuditExitScheduled = true
wx.showToast({ title: '版本审核中暂不可用', icon: 'none' })
this.setData({ loading: false, loadError: false, loadErrorMsg: '' })
setTimeout(() => {
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/profile/index' }) })
}, 400)
},
onLoad(options) {
this._purchaseAuditExitScheduled = false
this._deepPersonalRecheckScheduled = false
const app = appSafe()
const tab = (options && options.tab === 'enterprise') ? 'enterprise' : 'personal'
this.setData({
activeTab: tab,
deepHeroDesc: buildDeepHeroDesc(app.globalData.enterprisePermissions)
})
wx.setNavigationBarTitle({ title: '了解自己' })
this.loadDeepPricing()
},
/** 企业 permissions 在 runtime 返回后可能与首屏不同onShow / 拉价完成后重算 */
_reapplyPermissionFilter() {
const app = appSafe()
const rawP = this._rawPersonalCategories
const rawE = this._rawEnterpriseCategories
if ((!rawP || !rawP.length) && (!rawE || !rawE.length)) return
const perms = app.globalData.enterprisePermissions
this.setData({
personalCategories: filterCategories(rawP || [], perms),
enterpriseCategories: filterCategories(rawE || [], perms),
deepHeroDesc: buildDeepHeroDesc(perms)
})
},
onShow() {
// 不在此页强制跳转资料页:避免与付费按钮上的手机号授权打架导致死循环;未绑手机由按钮 open-type 引导
this.setData({ hasPhone: hasPhone() })
if (appSafe().globalData.miniprogramAuditMode) {
this._exitPurchaseForAudit()
return
}
this._reapplyPermissionFilter()
// 网关/缓存曾只返回 1 条时runtime 稍后就绪:首屏仅一条则静默再拉一次,便于露出 VMP 第二档
const raw = this._rawPersonalCategories
if (!this._deepPersonalRecheckScheduled && Array.isArray(raw) && raw.length === 1) {
this._deepPersonalRecheckScheduled = true
setTimeout(() => {
this.loadDeepPricing()
}, 500)
}
},
loadDeepPricing() {
const app = appSafe()
const apiBase = app.globalData.apiBase || ''
if (!apiBase) {
this.setData({ loading: false, loadError: true, loadErrorMsg: '服务地址未配置' })
return
}
this.setData({ loading: true, loadError: false, loadErrorMsg: '' })
const preRuntime =
app.getRuntimeConfig && typeof app.getRuntimeConfig === 'function'
? app.getRuntimeConfig().catch(() => null)
: Promise.resolve(null)
preRuntime.then(() => {
if (appSafe().globalData.miniprogramAuditMode) {
this._exitPurchaseForAudit()
return
}
return Promise.all([this.requestDeepPricing('personal'), this.requestDeepPricing('enterprise')])
}).then((pair) => {
if (!pair) return
const [personal, enterprise] = pair
const pErr = personal && personal.__error
const eErr = enterprise && enterprise.__error
let pList = Array.isArray(personal) ? personal : []
let eList = Array.isArray(enterprise) ? enterprise : []
if (pErr && embeddedDeepCategories('personal')) {
pList = mergeDeepPricingFromRuntime('personal', [])
personal = pList
}
if (eErr && embeddedDeepCategories('enterprise')) {
eList = mergeDeepPricingFromRuntime('enterprise', [])
enterprise = eList
}
if (!pErr && pList.length) {
pList = mergeDeepPricingFromRuntime('personal', pList)
}
if (!eErr && eList.length) {
eList = mergeDeepPricingFromRuntime('enterprise', eList)
}
const bothFailed = pErr && eErr && !pList.length && !eList.length
const bothEmpty = !pList.length && !eList.length
if (bothFailed || bothEmpty) {
const msg = pErr ? (personal.__errorMsg || '网络异常') : (eErr ? (enterprise.__errorMsg || '网络异常') : '暂无可购买方案')
this._rawPersonalCategories = []
this._rawEnterpriseCategories = []
this.setData({
personalCategories: [],
enterpriseCategories: [],
deepHeroDesc: buildDeepHeroDesc(null),
loading: false,
loadError: true,
loadErrorMsg: msg
})
return
}
this._rawPersonalCategories = pList
this._rawEnterpriseCategories = eList
const perms = appSafe().globalData.enterprisePermissions
this.setData({
personalCategories: filterCategories(pList, perms),
enterpriseCategories: filterCategories(eList, perms),
deepHeroDesc: buildDeepHeroDesc(perms),
loading: false,
loadError: false,
loadErrorMsg: ''
})
}).catch((err) => {
this.setData({
loading: false,
loadError: true,
loadErrorMsg: (err && err.message) || '加载失败,请检查网络后重试'
})
})
},
requestDeepPricing(scope) {
const app = appSafe()
return new Promise((resolve) => {
wx.request({
url: `${app.globalData.apiBase.replace(/\/$/, '')}/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)) {
resolve(mergeDeepPricingFromRuntime(scope, res.data.data.categories))
} 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) || '网络异常' })
}
})
})
},
switchTab(e) {
const tab = e.currentTarget.dataset.tab
if (tab !== 'personal' && tab !== 'enterprise') return
this.setData({ activeTab: tab })
wx.setNavigationBarTitle({ title: '了解自己' })
},
// 无需再次授权时,直接点击按钮执行购买/咨询
handlePurchaseTap(e) {
const tab = e.currentTarget.dataset.tab
const index = e.currentTarget.dataset.index
this.handlePurchase(tab, index)
},
// 实际执行购买/咨询逻辑(手机号由外层 getPhoneNumber 或 hasPhone 分支保证)
handlePurchase(tab, index) {
if (index === undefined || index === null) return
const list = tab === 'enterprise' ? this.data.enterpriseCategories : this.data.personalCategories
const category = list[index]
if (!category) return
if (category.actionType === 'buy' && category.productKey) {
this.purchasePersonal(category)
} else {
this.applyConsult(category)
}
},
// 购买/企业咨询按钮:就地触发微信系统手机号授权,然后执行 handlePurchase
onGetPhoneNumberForPurchase(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.handlePurchase(tab, index)
return
}
if (!code) {
if (hasPhone()) {
this.handlePurchase(tab, index)
} else {
wx.showToast({ title: '获取手机号失败', icon: 'none' })
}
return
}
bindPhoneByCode(code)
.then(() => {
this.setData({ hasPhone: true })
this.handlePurchase(tab, index)
})
.catch(() => {
// 失败时只提示,不阻塞后续再次点击
})
},
purchasePersonal(category) {
if (this.data.purchasing) return
this.setData({ purchasing: 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({ purchasing: false })
this._reportCrmLead(category, 'buy')
const successMsg = (category.successMessage || '购买成功!我们的顾问会尽快与您联系,为您提供专属深度解读服务。').trim()
this._showSuccessModal('购买成功', successMsg)
},
fail: () => {
wx.hideLoading()
this.setData({ purchasing: false })
}
})
},
applyConsult(category) {
const successMsg = (category.successMessage || '感谢您的申请,我们的顾问会尽快与您联系!').trim()
wx.showLoading({ title: '提交中...', mask: true })
this._reportCrmLead(category, 'consult', () => {
wx.hideLoading()
this._showSuccessModal('申请成功', successMsg)
})
},
_showSuccessModal(title, content) {
this.setData({
successModal: {
visible: true,
title: title || '成功',
content: content || ''
}
})
},
catchTap() {},
closeSuccessModal() {
this.setData({ 'successModal.visible': false })
},
/**
* 向后端上报线索:存客宝(有 KEY 时)+ 飞书群(申请咨询且 deepConsult
* @param {Function} [onDone] 请求结束回调(含失败)
*/
_reportCrmLead(category, actionType, onDone) {
const app = appSafe()
const apiKey = (category.consultWechat || '').trim()
const apiBase = (app.globalData.apiBase || '').replace(/\/$/, '')
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.activeTab === '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(res) {
console.log('[CRM] 线索上报结果', res.data)
},
fail(err) {
console.warn('[CRM] 线索上报请求失败', err)
},
complete: finish
})
},
onShareAppMessage() {
const { getSharePath } = require('../../utils/share')
return { title: '神仙团队性格测试 - 发现你的内在潜能', path: getSharePath('/pages/purchase/index') }
},
onShareTimeline() {
const { buildShareQuery } = require('../../utils/share')
return {
title: '神仙团队性格测试 - 发现你的内在潜能',
query: buildShareQuery()
}
}
})