feat: 管理端与用户端批量更新(订单/邀请双码/tabBar/审核与分销等)
- 管理端:用户详情、企业看板双小程序码、超管设置与用户列表等 - 后端:订单接口、邀请码企业版+个人版、分析与测试相关调整 - 微信小程序/抖音:自定义 tabBar 居中与拍摄钮上移、相机页与结果页、订单页、支付与统计 - 新增 faceResultDetail 工具与 mp 分析迁移脚本 Made-with: Cursor
This commit is contained in:
@@ -1,6 +1,36 @@
|
||||
// app.js - MBTI小程序主入口
|
||||
const { request } = require('./utils/request.js')
|
||||
|
||||
// 全局注入:所有页面 onShow 自动上报 page_view;onShareAppMessage 自动上报 share
|
||||
const _OrigPage = Page
|
||||
Page = function (pageConfig) {
|
||||
const origOnShow = pageConfig.onShow
|
||||
pageConfig.onShow = function () {
|
||||
try {
|
||||
const { reportPageView } = require('./utils/analytics.js')
|
||||
reportPageView()
|
||||
} catch (e) {}
|
||||
if (typeof origOnShow === 'function') {
|
||||
origOnShow.call(this)
|
||||
}
|
||||
}
|
||||
if (typeof pageConfig.onShareAppMessage === 'function') {
|
||||
const origShare = pageConfig.onShareAppMessage
|
||||
pageConfig.onShareAppMessage = function (res) {
|
||||
try { require('./utils/analytics.js').reportShare('friend') } catch (e) {}
|
||||
return origShare.call(this, res)
|
||||
}
|
||||
}
|
||||
if (typeof pageConfig.onShareTimeline === 'function') {
|
||||
const origTimeline = pageConfig.onShareTimeline
|
||||
pageConfig.onShareTimeline = function () {
|
||||
try { require('./utils/analytics.js').reportShare('timeline') } catch (e) {}
|
||||
return origTimeline.call(this)
|
||||
}
|
||||
}
|
||||
return _OrigPage(pageConfig)
|
||||
}
|
||||
|
||||
App({
|
||||
globalData: {
|
||||
userInfo: null,
|
||||
@@ -17,8 +47,8 @@ App({
|
||||
// 超管配置的默认企业 ID(无 scene/eid 等入口参数时回落)
|
||||
defaultEnterpriseId: null,
|
||||
// API基础地址(开发时用本地,生产环境替换为实际域名)
|
||||
apiBase: 'https://mbtiapi.quwanzhi.com',
|
||||
//apiBase: 'http://mbti.com',
|
||||
//apiBase: 'https://mbtiapi.quwanzhi.com',
|
||||
apiBase: 'http://mbti.com',
|
||||
// VIP信息
|
||||
vipInfo: null,
|
||||
// 测试次数
|
||||
@@ -32,13 +62,22 @@ App({
|
||||
aiResult: null
|
||||
},
|
||||
|
||||
onLaunch() {
|
||||
onLaunch(launchOptions) {
|
||||
// 记录场景值供埋点使用
|
||||
this.globalData.scene = launchOptions && launchOptions.scene ? launchOptions.scene : ''
|
||||
|
||||
// 加载本地存储的数据
|
||||
this.loadStoredData()
|
||||
|
||||
|
||||
// 静默登录获取openId
|
||||
this.silentLogin()
|
||||
|
||||
// 上报应用启动事件
|
||||
try {
|
||||
const analytics = require('./utils/analytics.js')
|
||||
analytics.reportAppLaunch(launchOptions)
|
||||
} catch (e) {}
|
||||
|
||||
// 预加载站点/小程序名称、维护模式与面相审核模式(camera/首页文案)
|
||||
this.getRuntimeConfig().then((cfg) => {
|
||||
if (cfg) {
|
||||
@@ -121,11 +160,17 @@ App({
|
||||
return
|
||||
}
|
||||
const url = `${this.globalData.apiBase}/api/auth/wechat`
|
||||
const loginData = { code: res.code }
|
||||
try {
|
||||
const { getEffectiveEnterpriseId } = require('./utils/enterpriseContext.js')
|
||||
const eid = getEffectiveEnterpriseId()
|
||||
if (eid) loginData.enterpriseId = eid
|
||||
} catch (e) {}
|
||||
wx.request({
|
||||
url,
|
||||
method: 'POST',
|
||||
header: { 'Content-Type': 'application/json' },
|
||||
data: { code: res.code },
|
||||
data: loginData,
|
||||
success: (response) => {
|
||||
if (response.statusCode === 200 && response.data && response.data.code === 200) {
|
||||
const data = response.data.data || {}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"pages/profile/index",
|
||||
"pages/user-profile/index",
|
||||
"pages/history/index",
|
||||
"pages/order/index",
|
||||
"pages/phone-auth/index",
|
||||
"pages/promo/index",
|
||||
"pages/promo/poster",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
.tab-bar-inner {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 120rpx;
|
||||
padding: 12rpx 12rpx 10rpx;
|
||||
@@ -31,8 +31,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 4rpx;
|
||||
justify-content: center;
|
||||
padding-bottom: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -60,19 +60,19 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 4rpx;
|
||||
justify-content: center;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* 浮钮:略上移即可,避免压住首页主按钮 */
|
||||
/* 浮钮:整体上移,更突出圆形快门、减少与底栏顶边重叠感 */
|
||||
.middle-fab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
transform: translateY(-10rpx);
|
||||
margin-bottom: -6rpx;
|
||||
transform: translateY(-26rpx);
|
||||
margin-bottom: -10rpx;
|
||||
}
|
||||
|
||||
.center-circle {
|
||||
|
||||
@@ -132,6 +132,7 @@ Page({
|
||||
this.cameraContext.takePhoto({
|
||||
quality: 'high',
|
||||
success: (res) => {
|
||||
try { require('../../utils/analytics').track('take_photo', { index: this.data.photos.length + 1 }) } catch (e) {}
|
||||
const photos = [...this.data.photos, res.tempImagePath]
|
||||
const photoIndex = photos.length
|
||||
const guideText = this.data.guideTexts[photoIndex] || '拍摄完成'
|
||||
@@ -231,11 +232,13 @@ Page({
|
||||
wx.hideLoading()
|
||||
wx.setStorageSync('aiPhotos', urls)
|
||||
this.setData({ uploading: false })
|
||||
try { require('../../utils/analytics').track('photo_upload_success', { count: urls.length }) } catch (e) {}
|
||||
wx.navigateTo({ url: '/pages/index/result' })
|
||||
})
|
||||
.catch((err) => {
|
||||
wx.hideLoading()
|
||||
this.setData({ uploading: false })
|
||||
try { require('../../utils/analytics').track('photo_upload_fail', { error: (err && err.message) || '' }) } catch (e) {}
|
||||
wx.showToast({ title: err.message || '上传失败', icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
@@ -102,6 +102,7 @@ Page({
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
this._payInfoSetByDetail = false
|
||||
const id = options && options.id
|
||||
const type = options && options.type
|
||||
|
||||
@@ -154,13 +155,14 @@ Page({
|
||||
const apiData = payload.data || payload
|
||||
// 历史详情场景下,记录当前测试记录ID
|
||||
this.setData({ testResultId: id })
|
||||
this.processResult(apiData)
|
||||
// 从历史进入:金额与是否需付款以 test_results 的 paidAmount/needPaymentToUnlock 为准
|
||||
// 先确定付费状态(设置 _payInfoSetByDetail 标记),再渲染结果
|
||||
// 避免 processResult 内部异步拉全局配置覆盖掉数据库级别的付费判定
|
||||
this.initPayInfoFromRuntime(
|
||||
!!payload.requiresPayment,
|
||||
!!payload.isPaid,
|
||||
payload
|
||||
)
|
||||
this.processResult(apiData)
|
||||
} else {
|
||||
wx.showToast({ title: res.data?.message || '加载失败', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 1500)
|
||||
@@ -254,6 +256,7 @@ Page({
|
||||
setTimeout(() => this.showNoFaceError(msg), 300)
|
||||
} else if (bodyCode === 200) {
|
||||
const apiData = res.data.data || res.data
|
||||
try { require('../../utils/analytics').track('ai_analysis_complete', { mbti: (apiData.mbti && apiData.mbti.type) || '' }) } catch (e) {}
|
||||
this.setData({ progress: 100, analyzingTip: '分析完成!' })
|
||||
setTimeout(() => {
|
||||
this.processResult(apiData)
|
||||
@@ -342,8 +345,9 @@ Page({
|
||||
amountYuan
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 拍照直连分析未带 _payment 时,拉 runtime 定价(与历史详情入口互不覆盖)
|
||||
} else if (!this._payInfoSetByDetail) {
|
||||
// 拍照直连分析未带 _payment 时,拉 runtime 定价
|
||||
// 历史详情入口已由 initPayInfoFromRuntime(detailPayload) 处理过,不再覆盖
|
||||
this.initPayInfoFromRuntime()
|
||||
}
|
||||
},
|
||||
@@ -369,6 +373,8 @@ Page({
|
||||
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)
|
||||
// 标记:历史详情已确定付费状态,processResult 中不再用全局配置覆盖
|
||||
this._payInfoSetByDetail = true
|
||||
this.setData({
|
||||
payInfo: {
|
||||
requiresPayment: needPay,
|
||||
@@ -430,6 +436,7 @@ Page({
|
||||
|
||||
/** 付费墙主按钮:先支付,支付后再引导手机号与资料 */
|
||||
onTapUnlockPay() {
|
||||
try { require('../../utils/analytics').track('tap_unlock_pay', { amount: this.data.payInfo.amountYuan, testResultId: this.data.testResultId }) } catch (e) {}
|
||||
this.unlockFullReport()
|
||||
},
|
||||
|
||||
@@ -534,16 +541,14 @@ Page({
|
||||
const payload = res.data.data
|
||||
const apiData = payload.data || payload
|
||||
|
||||
// 详情接口在后台会根据 isPaid 返回完整结构,这里直接复用已有处理逻辑
|
||||
this.processResult(apiData)
|
||||
|
||||
// 同步数据库中的付费状态与金额(test_results.paidAmount)
|
||||
// 先确定付费状态,再渲染结果(避免 processResult 异步覆盖)
|
||||
const recordRequires =
|
||||
typeof payload.requiresPayment === 'boolean' || typeof payload.requiresPayment === 'number'
|
||||
? !!payload.requiresPayment
|
||||
: null
|
||||
const recordIsPaid = !!payload.isPaid
|
||||
this.initPayInfoFromRuntime(recordRequires, recordIsPaid, payload)
|
||||
this.processResult(apiData)
|
||||
} else {
|
||||
console.error('刷新完整报告失败', res)
|
||||
}
|
||||
@@ -577,11 +582,6 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 跳转到详情性格测试选择页(MBTI / PDP / DISC 三选一)
|
||||
goToMBTI() {
|
||||
wx.navigateTo({ url: '/pages/test-select/index' })
|
||||
},
|
||||
|
||||
shareResult() {
|
||||
wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] })
|
||||
},
|
||||
|
||||
@@ -74,15 +74,10 @@
|
||||
class="card personality-card {{(payInfo.requiresPayment && !payInfo.isPaid) ? 'personality-card--locked' : ''}}"
|
||||
>
|
||||
<text class="card-title">性格类型分析</text>
|
||||
<text class="personality-card-sub">MBTI · PDP · DISC 联合速览</text>
|
||||
|
||||
<view class="personality-type">
|
||||
<text class="type-code">{{result.mbti}}</text>
|
||||
<text class="type-name">{{result.title}}</text>
|
||||
<view class="type-chip-row" wx:if="{{result.pdp || result.disc}}">
|
||||
<text class="type-chip" wx:if="{{result.pdp}}">{{result.pdpEmoji}}{{result.pdp}}</text>
|
||||
<text class="type-chip type-chip--disc" wx:if="{{result.disc}}">DISC {{result.disc}}</text>
|
||||
</view>
|
||||
<text class="type-desc">{{result.summary}}</text>
|
||||
</view>
|
||||
|
||||
@@ -110,10 +105,6 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="next-btn" bindtap="goToMBTI">
|
||||
<text class="next-btn-text">下一步:详细性格测试 →</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
|
||||
@@ -323,11 +323,11 @@
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
/* 约前 40% 区域保持清晰预览,以下渐强为「约 60% 可读」观感 */
|
||||
/* 约前 50% 区域保持清晰预览,以下渐强变白 */
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0) 38%,
|
||||
rgba(255, 255, 255, 0) 50%,
|
||||
rgba(255, 255, 255, 0.2) 52%,
|
||||
rgba(255, 255, 255, 0.55) 68%,
|
||||
rgba(255, 255, 255, 0.82) 88%,
|
||||
|
||||
81
miniprogram/pages/order/index.js
Normal file
81
miniprogram/pages/order/index.js
Normal file
@@ -0,0 +1,81 @@
|
||||
// pages/order/index.js - 我的订单(支付记录)
|
||||
const app = getApp()
|
||||
const { request } = require('../../utils/request')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
list: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
hasMore: false,
|
||||
loading: false,
|
||||
empty: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadList(true)
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.loadList(true)
|
||||
.then(() => wx.stopPullDownRefresh())
|
||||
.catch(() => wx.stopPullDownRefresh())
|
||||
},
|
||||
|
||||
loadList(reset) {
|
||||
const token = app.globalData.token || wx.getStorageSync('token')
|
||||
if (!token) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const page = reset ? 1 : this.data.page
|
||||
if (this.data.loading) return Promise.resolve()
|
||||
this.setData({ loading: true })
|
||||
|
||||
return new Promise((resolve) => {
|
||||
request({
|
||||
url: `/api/orders?page=${page}&pageSize=${this.data.pageSize}`,
|
||||
method: 'GET',
|
||||
needAuth: true,
|
||||
success: (res) => {
|
||||
const body = res.data
|
||||
if (res.statusCode === 200 && body && body.code === 200 && body.data) {
|
||||
const d = body.data
|
||||
const raw = Array.isArray(d.list) ? d.list : []
|
||||
const list = reset ? raw : this.data.list.concat(raw)
|
||||
this.setData({
|
||||
list,
|
||||
total: d.total || 0,
|
||||
page: d.page || page,
|
||||
hasMore: !!d.hasMore,
|
||||
empty: list.length === 0,
|
||||
loading: false
|
||||
})
|
||||
} else {
|
||||
this.setData({
|
||||
list: reset ? [] : this.data.list,
|
||||
empty: reset && (!this.data.list || this.data.list.length === 0),
|
||||
loading: false
|
||||
})
|
||||
if (body && body.message) wx.showToast({ title: body.message, icon: 'none' })
|
||||
}
|
||||
resolve()
|
||||
},
|
||||
fail: () => {
|
||||
this.setData({ loading: false })
|
||||
wx.showToast({ title: '加载失败', icon: 'none' })
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
if (!this.data.hasMore || this.data.loading) return
|
||||
this.setData({ page: this.data.page + 1 }, () => {
|
||||
this.loadList(false)
|
||||
})
|
||||
}
|
||||
})
|
||||
7
miniprogram/pages/order/index.json
Normal file
7
miniprogram/pages/order/index.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationBarTitleText": "我的订单",
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"navigationBarTextStyle": "black",
|
||||
"enablePullDownRefresh": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
32
miniprogram/pages/order/index.wxml
Normal file
32
miniprogram/pages/order/index.wxml
Normal file
@@ -0,0 +1,32 @@
|
||||
<!--pages/order/index.wxml-->
|
||||
<view class="page">
|
||||
<view class="loading-wrap" wx:if="{{loading && list.length === 0}}">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view class="empty" wx:elif="{{empty && !loading}}">
|
||||
<text class="empty-icon">🧾</text>
|
||||
<text class="empty-title">暂无订单</text>
|
||||
<text class="empty-desc">购买专业报告或测试服务后,订单会显示在这里</text>
|
||||
</view>
|
||||
|
||||
<view class="list" wx:else>
|
||||
<view class="order-card" wx:for="{{list}}" wx:key="id">
|
||||
<view class="card-head">
|
||||
<text class="title">{{item.productTitle}}</text>
|
||||
<text class="status status-{{item.status}}">{{item.statusText}}</text>
|
||||
</view>
|
||||
<text class="order-no">单号 {{item.orderNo}}</text>
|
||||
<view class="card-foot">
|
||||
<text class="time">{{item.payTimeStr || item.createdAtStr}}</text>
|
||||
<text class="amount">¥{{item.amountYuan}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="load-more" wx:if="{{hasMore}}" bindtap="loadMore">
|
||||
<text class="load-more-text">{{loading ? '加载中...' : '加载更多'}}</text>
|
||||
</view>
|
||||
<view class="list-end" wx:if="{{!hasMore && list.length > 0}}">
|
||||
<text class="list-end-text">没有更多了</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
134
miniprogram/pages/order/index.wxss
Normal file
134
miniprogram/pages/order/index.wxss
Normal file
@@ -0,0 +1,134 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 24rpx;
|
||||
padding-bottom: 48rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.loading-wrap {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 160rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 100rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 32rpx;
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: #9ca3af;
|
||||
text-align: center;
|
||||
padding: 0 48rpx;
|
||||
}
|
||||
|
||||
.order-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 28rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 24rpx;
|
||||
flex-shrink: 0;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: #d97706;
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.status-paid,
|
||||
.status-completed {
|
||||
color: #059669;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
color: #6b7280;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.order-no {
|
||||
font-size: 24rpx;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 16rpx;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.card-foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 24rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
text-align: center;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.load-more-text {
|
||||
font-size: 28rpx;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.list-end {
|
||||
text-align: center;
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.list-end-text {
|
||||
font-size: 24rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
@@ -278,6 +278,14 @@ Page({
|
||||
try { require('../../utils/analytics').track('tap_deep_service', {}) } catch (e) {}
|
||||
wx.navigateTo({ url: '/pages/purchase/index' })
|
||||
},
|
||||
goToOrders() {
|
||||
if (!this.data.hasLogin) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
try { require('../../utils/analytics').track('tap_my_orders', {}) } catch (e) {}
|
||||
wx.navigateTo({ url: '/pages/order/index' })
|
||||
},
|
||||
goToPurchase() { wx.navigateTo({ url: '/pages/purchase/index' }) },
|
||||
goToPurchasePersonal() { wx.navigateTo({ url: '/pages/purchase/index?tab=personal' }) },
|
||||
goToPurchaseEnterprise() { wx.navigateTo({ url: '/pages/purchase/index?tab=enterprise' }) },
|
||||
@@ -316,10 +324,10 @@ Page({
|
||||
if (id) wx.navigateTo({ url: `/pages/index/result?id=${id}&type=ai` })
|
||||
else wx.switchTab({ url: '/pages/index/camera' })
|
||||
},
|
||||
/** 动作测试:进入底部「拍摄」页 */
|
||||
goToActionTest() {
|
||||
try { require('../../utils/analytics').track('tap_action_test_camera', {}) } catch (e) {}
|
||||
wx.switchTab({ url: '/pages/index/camera' })
|
||||
/** 详细性格测试:MBTI / PDP / DISC 选择页 */
|
||||
goToTestSelect() {
|
||||
try { require('../../utils/analytics').track('tap_test_select_from_profile', {}) } catch (e) {}
|
||||
wx.navigateTo({ url: '/pages/test-select/index' })
|
||||
},
|
||||
|
||||
logout() {
|
||||
|
||||
@@ -129,20 +129,20 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="depth-empty-hint depth-empty-hint--compact">
|
||||
<text>灰色为未完成项,点击即可进入测评或拍摄;右上方可查看全部历史。</text>
|
||||
<text>灰色为未完成项,点击即可进入测评;右上方可查看全部历史。</text>
|
||||
</view>
|
||||
<view class="depth-inner-divider"></view>
|
||||
<view class="menu-item menu-item--flat" bindtap="goToActionTest" wx:if="{{!reviewMode}}">
|
||||
<view class="menu-item menu-item--flat" bindtap="goToTestSelect">
|
||||
<view class="menu-icon-wrap menu-icon-amber">
|
||||
<text class="menu-icon">📷</text>
|
||||
<text class="menu-icon">🧠</text>
|
||||
</view>
|
||||
<view class="menu-content">
|
||||
<text class="menu-title">动作测试(拍摄)</text>
|
||||
<text class="menu-sub">拍摄人像照片,生成面相、骨相与盖洛普等解析</text>
|
||||
<text class="menu-title">详细性格测试</text>
|
||||
<text class="menu-sub">MBTI、PDP、DISC 三套问卷,任选一项开始</text>
|
||||
</view>
|
||||
<text class="menu-chevron">›</text>
|
||||
</view>
|
||||
<view class="menu-divider menu-divider--in-card" wx:if="{{!reviewMode}}"></view>
|
||||
<view class="menu-divider menu-divider--in-card"></view>
|
||||
<view class="menu-item menu-item--flat" bindtap="goToDeepService">
|
||||
<view class="menu-icon-wrap menu-icon-purple">
|
||||
<text class="menu-icon">✨</text>
|
||||
@@ -153,6 +153,17 @@
|
||||
</view>
|
||||
<text class="menu-chevron">›</text>
|
||||
</view>
|
||||
<view class="menu-divider menu-divider--in-card"></view>
|
||||
<view class="menu-item menu-item--flat" bindtap="goToOrders">
|
||||
<view class="menu-icon-wrap menu-icon-emerald">
|
||||
<text class="menu-icon">🧾</text>
|
||||
</view>
|
||||
<view class="menu-content">
|
||||
<text class="menu-title">我的订单</text>
|
||||
<text class="menu-sub">支付记录与订单状态</text>
|
||||
</view>
|
||||
<text class="menu-chevron">›</text>
|
||||
</view>
|
||||
<view class="menu-divider menu-divider--in-card" wx:if="{{hasEnterprise}}"></view>
|
||||
<view class="menu-item menu-item--flat" wx:if="{{hasEnterprise}}" bindtap="goToMyResume">
|
||||
<view class="menu-icon-wrap menu-icon-indigo">
|
||||
|
||||
@@ -568,6 +568,7 @@ custom-tab-bar {
|
||||
.menu-icon-red { background: #FEF2F2; }
|
||||
.menu-icon-indigo { background: #EEF2FF; }
|
||||
.menu-icon-amber { background: #FFFBEB; }
|
||||
.menu-icon-emerald { background: #ECFDF5; }
|
||||
.menu-icon-purple { background: #EDE9FE; }
|
||||
|
||||
.menu-icon {
|
||||
|
||||
@@ -1,157 +1,157 @@
|
||||
// pages/result/disc.js - DISC结果页(支持付费墙 + 历史详情拉取)
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
|
||||
function toIntPercent(v) {
|
||||
if (v == null) return 0
|
||||
const n = typeof v === 'number' ? v : Number(v)
|
||||
return Number.isFinite(n) ? Math.round(n) : 0
|
||||
}
|
||||
|
||||
function withPercentagesInt(data) {
|
||||
if (!data) return data
|
||||
const p = data.percentages || {}
|
||||
const out = { ...data }
|
||||
out.percentagesInt = {
|
||||
D: toIntPercent(p.D ?? p.d),
|
||||
I: toIntPercent(p.I ?? p.i),
|
||||
S: toIntPercent(p.S ?? p.s),
|
||||
C: toIntPercent(p.C ?? p.c)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
result: null,
|
||||
typeList: [
|
||||
{ type: 'D', label: 'D型 - 支配型', colorClass: 'fill-d' },
|
||||
{ type: 'I', label: 'I型 - 影响型', colorClass: 'fill-i' },
|
||||
{ type: 'S', label: 'S型 - 稳健型', colorClass: 'fill-s' },
|
||||
{ type: 'C', label: 'C型 - 谨慎型', colorClass: 'fill-c' }
|
||||
],
|
||||
payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 },
|
||||
testResultId: null,
|
||||
hasReloadedAfterPay: false
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const id = options && options.id
|
||||
const type = options && options.type
|
||||
if (id && type === 'disc') {
|
||||
this.setData({ testResultId: id })
|
||||
this.loadDetail(id)
|
||||
return
|
||||
}
|
||||
const result = wx.getStorageSync('discResult')
|
||||
if (result) {
|
||||
this.setData({ result: withPercentagesInt(result) })
|
||||
this.initPayInfoFromRuntime('disc')
|
||||
} else {
|
||||
wx.showToast({ title: '暂无测试结果', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 1500)
|
||||
}
|
||||
},
|
||||
|
||||
loadDetail(id) {
|
||||
const apiBase = app.globalData?.apiBase || ''
|
||||
const token = app.globalData?.token || wx.getStorageSync('token') || ''
|
||||
if (!apiBase) { wx.showToast({ title: '配置异常', icon: 'none' }); return }
|
||||
wx.showLoading({ title: '加载中...' })
|
||||
wx.request({
|
||||
url: `${apiBase}/api/test/detail`,
|
||||
method: 'GET',
|
||||
header: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
data: { id },
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200 && res.data && res.data.code === 200) {
|
||||
const payload = res.data.data || {}
|
||||
const data = payload.data || payload
|
||||
const isPaid = !!payload.isPaid
|
||||
const paidAmount = payload.paidAmount != null ? Number(payload.paidAmount) : 0
|
||||
const amountYuan = payload.amountYuan != null ? Number(payload.amountYuan) : (paidAmount > 0 ? paidAmount / 100 : 0)
|
||||
const needPaymentToUnlock = payload.needPaymentToUnlock === true || (!!payload.requiresPayment && !isPaid && paidAmount > 0)
|
||||
this.setData({ result: withPercentagesInt(data) })
|
||||
const payInfo = {
|
||||
requiresPayment: needPaymentToUnlock,
|
||||
isPaid,
|
||||
amountYuan: needPaymentToUnlock ? amountYuan : 0
|
||||
}
|
||||
this.setData({ payInfo })
|
||||
} else {
|
||||
wx.showToast({ title: res.data?.message || '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
fail: () => wx.showToast({ title: '网络错误', icon: 'none' }),
|
||||
complete: () => wx.hideLoading()
|
||||
})
|
||||
},
|
||||
|
||||
initPayInfoFromRuntime(testType) {
|
||||
app.getRuntimeConfig()
|
||||
.then((cfg) => {
|
||||
const reportRequires = cfg.reportRequiresPayment || {}
|
||||
const pricing = cfg.pricing || {}
|
||||
const requiresPayment = !!(reportRequires && reportRequires[testType])
|
||||
const amountYuan = Number(pricing[testType]) || (requiresPayment ? 1.98 : 0)
|
||||
this.setData({
|
||||
payInfo: { requiresPayment, isPaid: false, amountYuan }
|
||||
})
|
||||
})
|
||||
.catch(() => this.setData({ payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 } }))
|
||||
},
|
||||
|
||||
unlockFullReport() {
|
||||
const { payInfo, testResultId, hasReloadedAfterPay } = this.data
|
||||
if (!payInfo.requiresPayment || payInfo.isPaid) return
|
||||
app.ensureLogin && app.ensureLogin().then((logged) => {
|
||||
if (!logged) { wx.showToast({ title: '请先登录', icon: 'none' }); return }
|
||||
payment.purchaseDiscTest({
|
||||
testResultId: testResultId || undefined,
|
||||
success: () => {
|
||||
wx.showToast({ title: '已解锁完整报告', icon: 'success' })
|
||||
this.setData({ 'payInfo.isPaid': true })
|
||||
if (testResultId && !hasReloadedAfterPay) {
|
||||
this.setData({ hasReloadedAfterPay: true })
|
||||
setTimeout(() => this.loadDetail(testResultId), 500)
|
||||
}
|
||||
},
|
||||
fail: () => {}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
retakeTest() {
|
||||
if (!this.data.testResultId) {
|
||||
wx.removeStorageSync('discResult')
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/test/disc' })
|
||||
},
|
||||
|
||||
goHome() {
|
||||
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 result = this.data.result
|
||||
const { getSharePathByScope } = require('../../utils/share')
|
||||
return {
|
||||
title: `我的DISC类型是${result?.dominantType}型(${result?.description?.title}),来测测你的吧!`,
|
||||
path: getSharePathByScope('/pages/index/index')
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const result = this.data.result
|
||||
const { buildShareQuery } = require('../../utils/share')
|
||||
return {
|
||||
title: `我的DISC类型是${result?.dominantType}型(${result?.description?.title}),来测测你的吧!`,
|
||||
query: buildShareQuery()
|
||||
}
|
||||
}
|
||||
})
|
||||
// pages/result/disc.js - DISC结果页(支持付费墙 + 历史详情拉取)
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
|
||||
function toIntPercent(v) {
|
||||
if (v == null) return 0
|
||||
const n = typeof v === 'number' ? v : Number(v)
|
||||
return Number.isFinite(n) ? Math.round(n) : 0
|
||||
}
|
||||
|
||||
function withPercentagesInt(data) {
|
||||
if (!data) return data
|
||||
const p = data.percentages || {}
|
||||
const out = { ...data }
|
||||
out.percentagesInt = {
|
||||
D: toIntPercent(p.D ?? p.d),
|
||||
I: toIntPercent(p.I ?? p.i),
|
||||
S: toIntPercent(p.S ?? p.s),
|
||||
C: toIntPercent(p.C ?? p.c)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
result: null,
|
||||
typeList: [
|
||||
{ type: 'D', label: 'D型 - 支配型', colorClass: 'fill-d' },
|
||||
{ type: 'I', label: 'I型 - 影响型', colorClass: 'fill-i' },
|
||||
{ type: 'S', label: 'S型 - 稳健型', colorClass: 'fill-s' },
|
||||
{ type: 'C', label: 'C型 - 谨慎型', colorClass: 'fill-c' }
|
||||
],
|
||||
payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 },
|
||||
testResultId: null,
|
||||
hasReloadedAfterPay: false
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const id = options && options.id
|
||||
const type = options && options.type
|
||||
if (id && type === 'disc') {
|
||||
this.setData({ testResultId: id })
|
||||
this.loadDetail(id)
|
||||
return
|
||||
}
|
||||
const result = wx.getStorageSync('discResult')
|
||||
if (result) {
|
||||
this.setData({ result: withPercentagesInt(result) })
|
||||
this.initPayInfoFromRuntime('disc')
|
||||
} else {
|
||||
wx.showToast({ title: '暂无测试结果', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 1500)
|
||||
}
|
||||
},
|
||||
|
||||
loadDetail(id) {
|
||||
const apiBase = app.globalData?.apiBase || ''
|
||||
const token = app.globalData?.token || wx.getStorageSync('token') || ''
|
||||
if (!apiBase) { wx.showToast({ title: '配置异常', icon: 'none' }); return }
|
||||
wx.showLoading({ title: '加载中...' })
|
||||
wx.request({
|
||||
url: `${apiBase}/api/test/detail`,
|
||||
method: 'GET',
|
||||
header: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
data: { id },
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200 && res.data && res.data.code === 200) {
|
||||
const payload = res.data.data || {}
|
||||
const data = payload.data || payload
|
||||
const isPaid = !!payload.isPaid
|
||||
const paidAmount = payload.paidAmount != null ? Number(payload.paidAmount) : 0
|
||||
const amountYuan = payload.amountYuan != null ? Number(payload.amountYuan) : (paidAmount > 0 ? paidAmount / 100 : 0)
|
||||
const needPaymentToUnlock = payload.needPaymentToUnlock === true || (!!payload.requiresPayment && !isPaid && paidAmount > 0)
|
||||
this.setData({ result: withPercentagesInt(data) })
|
||||
const payInfo = {
|
||||
requiresPayment: needPaymentToUnlock,
|
||||
isPaid,
|
||||
amountYuan: needPaymentToUnlock ? amountYuan : 0
|
||||
}
|
||||
this.setData({ payInfo })
|
||||
} else {
|
||||
wx.showToast({ title: res.data?.message || '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
fail: () => wx.showToast({ title: '网络错误', icon: 'none' }),
|
||||
complete: () => wx.hideLoading()
|
||||
})
|
||||
},
|
||||
|
||||
initPayInfoFromRuntime(testType) {
|
||||
app.getRuntimeConfig()
|
||||
.then((cfg) => {
|
||||
const reportRequires = cfg.reportRequiresPayment || {}
|
||||
const pricing = cfg.pricing || {}
|
||||
const requiresPayment = !!(reportRequires && reportRequires[testType])
|
||||
const amountYuan = Number(pricing[testType]) || (requiresPayment ? 1.98 : 0)
|
||||
this.setData({
|
||||
payInfo: { requiresPayment, isPaid: false, amountYuan }
|
||||
})
|
||||
})
|
||||
.catch(() => this.setData({ payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 } }))
|
||||
},
|
||||
|
||||
unlockFullReport() {
|
||||
const { payInfo, testResultId, hasReloadedAfterPay } = this.data
|
||||
if (!payInfo.requiresPayment || payInfo.isPaid) return
|
||||
app.ensureLogin && app.ensureLogin().then((logged) => {
|
||||
if (!logged) { wx.showToast({ title: '请先登录', icon: 'none' }); return }
|
||||
payment.purchaseDiscTest({
|
||||
testResultId: testResultId || undefined,
|
||||
success: () => {
|
||||
wx.showToast({ title: '已解锁完整报告', icon: 'success' })
|
||||
this.setData({ 'payInfo.isPaid': true })
|
||||
if (testResultId && !hasReloadedAfterPay) {
|
||||
this.setData({ hasReloadedAfterPay: true })
|
||||
setTimeout(() => this.loadDetail(testResultId), 500)
|
||||
}
|
||||
},
|
||||
fail: () => {}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
retakeTest() {
|
||||
if (!this.data.testResultId) {
|
||||
wx.removeStorageSync('discResult')
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/test/disc' })
|
||||
},
|
||||
|
||||
goHome() {
|
||||
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 result = this.data.result
|
||||
const { getSharePathByScope } = require('../../utils/share')
|
||||
return {
|
||||
title: `我的DISC类型是${result?.dominantType}型(${result?.description?.title}),来测测你的吧!`,
|
||||
path: getSharePathByScope('/pages/index/index')
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const result = this.data.result
|
||||
const { buildShareQuery } = require('../../utils/share')
|
||||
return {
|
||||
title: `我的DISC类型是${result?.dominantType}型(${result?.description?.title}),来测测你的吧!`,
|
||||
query: buildShareQuery()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="action-section">
|
||||
<!-- <view class="action-section">
|
||||
<button class="btn btn-primary" open-type="share">
|
||||
<text class="btn-text">分享结果</text>
|
||||
</button>
|
||||
@@ -75,7 +75,7 @@
|
||||
<view class="btn btn-outline" bindtap="goHome">
|
||||
<text class="btn-text-outline">返回首页</text>
|
||||
</view>
|
||||
</view>
|
||||
</view> -->
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
<text class="relationship-text">{{mbtiDesc.relationships}}</text>
|
||||
</view>
|
||||
|
||||
<view class="action-section">
|
||||
<!-- <view class="action-section">
|
||||
<button class="btn btn-primary" open-type="share">
|
||||
<text class="btn-text">分享结果</text>
|
||||
</button>
|
||||
@@ -101,7 +101,7 @@
|
||||
<view class="btn btn-outline" bindtap="goHome">
|
||||
<text class="btn-text-outline">返回首页</text>
|
||||
</view>
|
||||
</view>
|
||||
</view> -->
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
@@ -1,157 +1,157 @@
|
||||
// pages/result/pdp.js - PDP结果页(支持付费墙 + 历史详情拉取)
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
|
||||
const PDP_KEYS = ['Tiger', 'Peacock', 'Koala', 'Owl', 'Chameleon']
|
||||
|
||||
function toIntPercent(v) {
|
||||
if (v == null) return 0
|
||||
const n = typeof v === 'number' ? v : Number(v)
|
||||
return Number.isFinite(n) ? Math.round(n) : 0
|
||||
}
|
||||
|
||||
function withPercentagesInt(data) {
|
||||
if (!data) return data
|
||||
const p = data.percentages || {}
|
||||
const out = { ...data }
|
||||
const ints = {}
|
||||
PDP_KEYS.forEach((k) => { ints[k] = toIntPercent(p[k] ?? p[k.toLowerCase()]) })
|
||||
out.percentagesInt = ints
|
||||
return out
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
result: null,
|
||||
typeList: [
|
||||
{ type: 'Tiger', emoji: '🐅', label: '老虎型', colorClass: 'fill-tiger' },
|
||||
{ type: 'Peacock', emoji: '🦚', label: '孔雀型', colorClass: 'fill-peacock' },
|
||||
{ type: 'Koala', emoji: '🐨', label: '考拉型', colorClass: 'fill-koala' },
|
||||
{ type: 'Owl', emoji: '🦉', label: '猫头鹰型', colorClass: 'fill-owl' },
|
||||
{ type: 'Chameleon', emoji: '🦎', label: '变色龙型', colorClass: 'fill-chameleon' }
|
||||
],
|
||||
payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 },
|
||||
testResultId: null,
|
||||
hasReloadedAfterPay: false
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const id = options && options.id
|
||||
const type = options && options.type
|
||||
if (id && type === 'pdp') {
|
||||
this.setData({ testResultId: id })
|
||||
this.loadDetail(id)
|
||||
return
|
||||
}
|
||||
const result = wx.getStorageSync('pdpResult')
|
||||
if (result) {
|
||||
this.setData({ result: withPercentagesInt(result) })
|
||||
this.initPayInfoFromRuntime('pdp')
|
||||
} else {
|
||||
wx.showToast({ title: '暂无测试结果', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 1500)
|
||||
}
|
||||
},
|
||||
|
||||
loadDetail(id) {
|
||||
const apiBase = app.globalData?.apiBase || ''
|
||||
const token = app.globalData?.token || wx.getStorageSync('token') || ''
|
||||
if (!apiBase) { wx.showToast({ title: '配置异常', icon: 'none' }); return }
|
||||
wx.showLoading({ title: '加载中...' })
|
||||
wx.request({
|
||||
url: `${apiBase}/api/test/detail`,
|
||||
method: 'GET',
|
||||
header: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
data: { id },
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200 && res.data && res.data.code === 200) {
|
||||
const payload = res.data.data || {}
|
||||
const data = payload.data || payload
|
||||
const isPaid = !!payload.isPaid
|
||||
const paidAmount = payload.paidAmount != null ? Number(payload.paidAmount) : 0
|
||||
const amountYuan = payload.amountYuan != null ? Number(payload.amountYuan) : (paidAmount > 0 ? paidAmount / 100 : 0)
|
||||
const needPaymentToUnlock = payload.needPaymentToUnlock === true || (!!payload.requiresPayment && !isPaid && paidAmount > 0)
|
||||
this.setData({ result: withPercentagesInt(data) })
|
||||
const payInfo = {
|
||||
requiresPayment: needPaymentToUnlock,
|
||||
isPaid,
|
||||
amountYuan: needPaymentToUnlock ? amountYuan : 0
|
||||
}
|
||||
this.setData({ payInfo })
|
||||
} else {
|
||||
wx.showToast({ title: res.data?.message || '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
fail: () => wx.showToast({ title: '网络错误', icon: 'none' }),
|
||||
complete: () => wx.hideLoading()
|
||||
})
|
||||
},
|
||||
|
||||
initPayInfoFromRuntime(testType) {
|
||||
app.getRuntimeConfig()
|
||||
.then((cfg) => {
|
||||
const reportRequires = cfg.reportRequiresPayment || {}
|
||||
const pricing = cfg.pricing || {}
|
||||
const requiresPayment = !!(reportRequires && reportRequires[testType])
|
||||
const amountYuan = Number(pricing[testType]) || (requiresPayment ? 1.98 : 0)
|
||||
this.setData({
|
||||
payInfo: { requiresPayment, isPaid: false, amountYuan }
|
||||
})
|
||||
})
|
||||
.catch(() => this.setData({ payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 } }))
|
||||
},
|
||||
|
||||
unlockFullReport() {
|
||||
const { payInfo, testResultId, hasReloadedAfterPay } = this.data
|
||||
if (!payInfo.requiresPayment || payInfo.isPaid) return
|
||||
app.ensureLogin && app.ensureLogin().then((logged) => {
|
||||
if (!logged) { wx.showToast({ title: '请先登录', icon: 'none' }); return }
|
||||
payment.purchasePdpTest({
|
||||
testResultId: testResultId || undefined,
|
||||
success: () => {
|
||||
wx.showToast({ title: '已解锁完整报告', icon: 'success' })
|
||||
this.setData({ 'payInfo.isPaid': true })
|
||||
if (testResultId && !hasReloadedAfterPay) {
|
||||
this.setData({ hasReloadedAfterPay: true })
|
||||
setTimeout(() => this.loadDetail(testResultId), 500)
|
||||
}
|
||||
},
|
||||
fail: () => {}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
retakeTest() {
|
||||
if (!this.data.testResultId) {
|
||||
wx.removeStorageSync('pdpResult')
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/test/pdp' })
|
||||
},
|
||||
|
||||
goHome() {
|
||||
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 result = this.data.result
|
||||
const { getSharePathByScope } = require('../../utils/share')
|
||||
return {
|
||||
title: `我的PDP类型是${result?.description?.type}${result?.description?.emoji},来测测你的吧!`,
|
||||
path: getSharePathByScope('/pages/index/index')
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const result = this.data.result
|
||||
const { buildShareQuery } = require('../../utils/share')
|
||||
return {
|
||||
title: `我的PDP类型是${result?.description?.type}${result?.description?.emoji},来测测你的吧!`,
|
||||
query: buildShareQuery()
|
||||
}
|
||||
}
|
||||
})
|
||||
// pages/result/pdp.js - PDP结果页(支持付费墙 + 历史详情拉取)
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
|
||||
const PDP_KEYS = ['Tiger', 'Peacock', 'Koala', 'Owl', 'Chameleon']
|
||||
|
||||
function toIntPercent(v) {
|
||||
if (v == null) return 0
|
||||
const n = typeof v === 'number' ? v : Number(v)
|
||||
return Number.isFinite(n) ? Math.round(n) : 0
|
||||
}
|
||||
|
||||
function withPercentagesInt(data) {
|
||||
if (!data) return data
|
||||
const p = data.percentages || {}
|
||||
const out = { ...data }
|
||||
const ints = {}
|
||||
PDP_KEYS.forEach((k) => { ints[k] = toIntPercent(p[k] ?? p[k.toLowerCase()]) })
|
||||
out.percentagesInt = ints
|
||||
return out
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
result: null,
|
||||
typeList: [
|
||||
{ type: 'Tiger', emoji: '🐅', label: '老虎型', colorClass: 'fill-tiger' },
|
||||
{ type: 'Peacock', emoji: '🦚', label: '孔雀型', colorClass: 'fill-peacock' },
|
||||
{ type: 'Koala', emoji: '🐨', label: '考拉型', colorClass: 'fill-koala' },
|
||||
{ type: 'Owl', emoji: '🦉', label: '猫头鹰型', colorClass: 'fill-owl' },
|
||||
{ type: 'Chameleon', emoji: '🦎', label: '变色龙型', colorClass: 'fill-chameleon' }
|
||||
],
|
||||
payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 },
|
||||
testResultId: null,
|
||||
hasReloadedAfterPay: false
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const id = options && options.id
|
||||
const type = options && options.type
|
||||
if (id && type === 'pdp') {
|
||||
this.setData({ testResultId: id })
|
||||
this.loadDetail(id)
|
||||
return
|
||||
}
|
||||
const result = wx.getStorageSync('pdpResult')
|
||||
if (result) {
|
||||
this.setData({ result: withPercentagesInt(result) })
|
||||
this.initPayInfoFromRuntime('pdp')
|
||||
} else {
|
||||
wx.showToast({ title: '暂无测试结果', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 1500)
|
||||
}
|
||||
},
|
||||
|
||||
loadDetail(id) {
|
||||
const apiBase = app.globalData?.apiBase || ''
|
||||
const token = app.globalData?.token || wx.getStorageSync('token') || ''
|
||||
if (!apiBase) { wx.showToast({ title: '配置异常', icon: 'none' }); return }
|
||||
wx.showLoading({ title: '加载中...' })
|
||||
wx.request({
|
||||
url: `${apiBase}/api/test/detail`,
|
||||
method: 'GET',
|
||||
header: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
data: { id },
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200 && res.data && res.data.code === 200) {
|
||||
const payload = res.data.data || {}
|
||||
const data = payload.data || payload
|
||||
const isPaid = !!payload.isPaid
|
||||
const paidAmount = payload.paidAmount != null ? Number(payload.paidAmount) : 0
|
||||
const amountYuan = payload.amountYuan != null ? Number(payload.amountYuan) : (paidAmount > 0 ? paidAmount / 100 : 0)
|
||||
const needPaymentToUnlock = payload.needPaymentToUnlock === true || (!!payload.requiresPayment && !isPaid && paidAmount > 0)
|
||||
this.setData({ result: withPercentagesInt(data) })
|
||||
const payInfo = {
|
||||
requiresPayment: needPaymentToUnlock,
|
||||
isPaid,
|
||||
amountYuan: needPaymentToUnlock ? amountYuan : 0
|
||||
}
|
||||
this.setData({ payInfo })
|
||||
} else {
|
||||
wx.showToast({ title: res.data?.message || '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
fail: () => wx.showToast({ title: '网络错误', icon: 'none' }),
|
||||
complete: () => wx.hideLoading()
|
||||
})
|
||||
},
|
||||
|
||||
initPayInfoFromRuntime(testType) {
|
||||
app.getRuntimeConfig()
|
||||
.then((cfg) => {
|
||||
const reportRequires = cfg.reportRequiresPayment || {}
|
||||
const pricing = cfg.pricing || {}
|
||||
const requiresPayment = !!(reportRequires && reportRequires[testType])
|
||||
const amountYuan = Number(pricing[testType]) || (requiresPayment ? 1.98 : 0)
|
||||
this.setData({
|
||||
payInfo: { requiresPayment, isPaid: false, amountYuan }
|
||||
})
|
||||
})
|
||||
.catch(() => this.setData({ payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 } }))
|
||||
},
|
||||
|
||||
unlockFullReport() {
|
||||
const { payInfo, testResultId, hasReloadedAfterPay } = this.data
|
||||
if (!payInfo.requiresPayment || payInfo.isPaid) return
|
||||
app.ensureLogin && app.ensureLogin().then((logged) => {
|
||||
if (!logged) { wx.showToast({ title: '请先登录', icon: 'none' }); return }
|
||||
payment.purchasePdpTest({
|
||||
testResultId: testResultId || undefined,
|
||||
success: () => {
|
||||
wx.showToast({ title: '已解锁完整报告', icon: 'success' })
|
||||
this.setData({ 'payInfo.isPaid': true })
|
||||
if (testResultId && !hasReloadedAfterPay) {
|
||||
this.setData({ hasReloadedAfterPay: true })
|
||||
setTimeout(() => this.loadDetail(testResultId), 500)
|
||||
}
|
||||
},
|
||||
fail: () => {}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
retakeTest() {
|
||||
if (!this.data.testResultId) {
|
||||
wx.removeStorageSync('pdpResult')
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/test/pdp' })
|
||||
},
|
||||
|
||||
goHome() {
|
||||
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 result = this.data.result
|
||||
const { getSharePathByScope } = require('../../utils/share')
|
||||
return {
|
||||
title: `我的PDP类型是${result?.description?.type}${result?.description?.emoji},来测测你的吧!`,
|
||||
path: getSharePathByScope('/pages/index/index')
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const result = this.data.result
|
||||
const { buildShareQuery } = require('../../utils/share')
|
||||
return {
|
||||
title: `我的PDP类型是${result?.description?.type}${result?.description?.emoji},来测测你的吧!`,
|
||||
query: buildShareQuery()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="action-section">
|
||||
<!-- <view class="action-section">
|
||||
<button class="btn btn-primary" open-type="share">
|
||||
<text class="btn-text">分享结果</text>
|
||||
</button>
|
||||
@@ -81,7 +81,7 @@
|
||||
<view class="btn btn-outline" bindtap="goHome">
|
||||
<text class="btn-text-outline">返回首页</text>
|
||||
</view>
|
||||
</view>
|
||||
</view> -->
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
@@ -23,10 +23,15 @@ Page({
|
||||
onLoad() {
|
||||
const questions = shuffleQuestions(discQuestions)
|
||||
this.setData({ questions, currentQuestion: questions[0] })
|
||||
try { require('../../utils/analytics').track('test_start', { type: 'disc', total: questions.length }) } catch (e) {}
|
||||
this.startTimer()
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
if (this._advanceTimer) {
|
||||
clearTimeout(this._advanceTimer)
|
||||
this._advanceTimer = null
|
||||
}
|
||||
if (this.timer) clearInterval(this.timer)
|
||||
},
|
||||
|
||||
@@ -48,28 +53,48 @@ Page({
|
||||
},
|
||||
|
||||
selectAnswer(e) {
|
||||
if (this._advanceTimer) {
|
||||
clearTimeout(this._advanceTimer)
|
||||
this._advanceTimer = null
|
||||
}
|
||||
const value = e.currentTarget.dataset.value
|
||||
const questionId = this.data.currentQuestion.id
|
||||
let answers = { ...this.data.answers }
|
||||
answers[questionId] = value
|
||||
|
||||
this.setData({
|
||||
selectedAnswer: value,
|
||||
answers: answers,
|
||||
answeredCount: Object.keys(answers).length,
|
||||
progress: (Object.keys(answers).length / this.data.total) * 100
|
||||
})
|
||||
const cq = this.data.currentQuestion
|
||||
if (value == null || !cq || cq.id == null) return
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.data.currentIndex < this.data.total - 1) {
|
||||
this.nextQuestion()
|
||||
} else {
|
||||
this.submitTest()
|
||||
const questionId = cq.id
|
||||
const idx = this.data.currentIndex
|
||||
const tot = this.data.total
|
||||
const answers = { ...this.data.answers }
|
||||
answers[questionId] = value
|
||||
const answeredCount = Object.keys(answers).length
|
||||
|
||||
this.setData(
|
||||
{
|
||||
selectedAnswer: value,
|
||||
answers,
|
||||
answeredCount,
|
||||
progress: tot ? (answeredCount / tot) * 100 : 0
|
||||
},
|
||||
() => {
|
||||
this._advanceTimer = setTimeout(() => {
|
||||
this._advanceTimer = null
|
||||
const d = this.data
|
||||
if (d.currentIndex !== idx || !d.currentQuestion || d.currentQuestion.id !== questionId) return
|
||||
if (idx < tot - 1) {
|
||||
this.nextQuestion()
|
||||
} else {
|
||||
this.submitTest()
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
}, 300)
|
||||
)
|
||||
},
|
||||
|
||||
prevQuestion() {
|
||||
if (this._advanceTimer) {
|
||||
clearTimeout(this._advanceTimer)
|
||||
this._advanceTimer = null
|
||||
}
|
||||
if (this.data.currentIndex > 0) {
|
||||
const newIndex = this.data.currentIndex - 1
|
||||
const newQuestion = this.data.questions[newIndex]
|
||||
@@ -131,6 +156,7 @@ Page({
|
||||
|
||||
// 本地缓存 + 全局缓存
|
||||
wx.setStorageSync('discResult', resultData)
|
||||
try { require('../../utils/analytics').track('test_complete', { type: 'disc', result: dominantType, duration: resultData.testDuration }) } catch (e) {}
|
||||
if (app && typeof app.saveTestResult === 'function') {
|
||||
app.saveTestResult('disc', resultData)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ Page({
|
||||
total,
|
||||
progress: total ? Math.round((1 / total) * 100) : 0
|
||||
})
|
||||
try { require('../../utils/analytics').track('test_start', { type: 'mbti', total }) } catch (e) {}
|
||||
this.startTimer()
|
||||
},
|
||||
|
||||
@@ -43,6 +44,10 @@ Page({
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
if (this._advanceTimer) {
|
||||
clearTimeout(this._advanceTimer)
|
||||
this._advanceTimer = null
|
||||
}
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer)
|
||||
}
|
||||
@@ -66,36 +71,53 @@ Page({
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
// 选择答案(用闭包保存题号,避免 setTimeout 与 setData 时序导致最后一题未提交)
|
||||
// 选择答案:防抖 + 过期回调校验,避免连点导致重复 next 漏题
|
||||
selectAnswer(e) {
|
||||
if (this._advanceTimer) {
|
||||
clearTimeout(this._advanceTimer)
|
||||
this._advanceTimer = null
|
||||
}
|
||||
const value = e.currentTarget.dataset.value
|
||||
const questionId = this.data.currentQuestion.id
|
||||
const cq = this.data.currentQuestion
|
||||
if (value == null || !cq || cq.id == null) return
|
||||
|
||||
const questionId = cq.id
|
||||
const idx = this.data.currentIndex
|
||||
const tot = this.data.total
|
||||
|
||||
let answers = { ...this.data.answers }
|
||||
const answers = { ...this.data.answers }
|
||||
answers[questionId] = value
|
||||
const answeredCount = Object.keys(answers).length
|
||||
const progress = tot ? Math.round(((idx + 1) / tot) * 100) : 0
|
||||
|
||||
this.setData({
|
||||
selectedAnswer: value,
|
||||
answers,
|
||||
answeredCount,
|
||||
progress
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
if (idx < tot - 1) {
|
||||
this.nextQuestion()
|
||||
} else {
|
||||
this.submitTest()
|
||||
this.setData(
|
||||
{
|
||||
selectedAnswer: value,
|
||||
answers,
|
||||
answeredCount,
|
||||
progress
|
||||
},
|
||||
() => {
|
||||
this._advanceTimer = setTimeout(() => {
|
||||
this._advanceTimer = null
|
||||
const d = this.data
|
||||
if (d.currentIndex !== idx || !d.currentQuestion || d.currentQuestion.id !== questionId) return
|
||||
if (idx < tot - 1) {
|
||||
this.nextQuestion()
|
||||
} else {
|
||||
this.submitTest()
|
||||
}
|
||||
}, 320)
|
||||
}
|
||||
}, 320)
|
||||
)
|
||||
},
|
||||
|
||||
// 上一题
|
||||
prevQuestion() {
|
||||
if (this._advanceTimer) {
|
||||
clearTimeout(this._advanceTimer)
|
||||
this._advanceTimer = null
|
||||
}
|
||||
if (this.data.currentIndex > 0) {
|
||||
const newIndex = this.data.currentIndex - 1
|
||||
const newQuestion = this.data.questions[newIndex]
|
||||
@@ -181,6 +203,7 @@ Page({
|
||||
}
|
||||
wx.setStorageSync('mbtiResult', resultData)
|
||||
app.saveTestResult('mbti', resultData)
|
||||
try { require('../../utils/analytics').track('test_complete', { type: 'mbti', result: result.mbtiType, confidence: result.confidence, duration: resultData.testDuration }) } catch (e) {}
|
||||
|
||||
wx.redirectTo({
|
||||
url: '/pages/result/mbti'
|
||||
|
||||
@@ -23,10 +23,15 @@ Page({
|
||||
onLoad() {
|
||||
const questions = shuffleQuestions(pdpQuestions)
|
||||
this.setData({ questions, currentQuestion: questions[0] })
|
||||
try { require('../../utils/analytics').track('test_start', { type: 'pdp', total: questions.length }) } catch (e) {}
|
||||
this.startTimer()
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
if (this._advanceTimer) {
|
||||
clearTimeout(this._advanceTimer)
|
||||
this._advanceTimer = null
|
||||
}
|
||||
if (this.timer) clearInterval(this.timer)
|
||||
},
|
||||
|
||||
@@ -53,28 +58,48 @@ Page({
|
||||
},
|
||||
|
||||
selectAnswer(e) {
|
||||
if (this._advanceTimer) {
|
||||
clearTimeout(this._advanceTimer)
|
||||
this._advanceTimer = null
|
||||
}
|
||||
const value = e.currentTarget.dataset.value
|
||||
const questionId = this.data.currentQuestion.id
|
||||
let answers = { ...this.data.answers }
|
||||
answers[questionId] = value
|
||||
|
||||
this.setData({
|
||||
selectedAnswer: value,
|
||||
answers: answers,
|
||||
answeredCount: Object.keys(answers).length,
|
||||
progress: (Object.keys(answers).length / this.data.total) * 100
|
||||
})
|
||||
const cq = this.data.currentQuestion
|
||||
if (value == null || !cq || cq.id == null) return
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.data.currentIndex < this.data.total - 1) {
|
||||
this.nextQuestion()
|
||||
} else {
|
||||
this.submitTest()
|
||||
const questionId = cq.id
|
||||
const idx = this.data.currentIndex
|
||||
const tot = this.data.total
|
||||
const answers = { ...this.data.answers }
|
||||
answers[questionId] = value
|
||||
const answeredCount = Object.keys(answers).length
|
||||
|
||||
this.setData(
|
||||
{
|
||||
selectedAnswer: value,
|
||||
answers,
|
||||
answeredCount,
|
||||
progress: tot ? (answeredCount / tot) * 100 : 0
|
||||
},
|
||||
() => {
|
||||
this._advanceTimer = setTimeout(() => {
|
||||
this._advanceTimer = null
|
||||
const d = this.data
|
||||
if (d.currentIndex !== idx || !d.currentQuestion || d.currentQuestion.id !== questionId) return
|
||||
if (idx < tot - 1) {
|
||||
this.nextQuestion()
|
||||
} else {
|
||||
this.submitTest()
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
}, 300)
|
||||
)
|
||||
},
|
||||
|
||||
prevQuestion() {
|
||||
if (this._advanceTimer) {
|
||||
clearTimeout(this._advanceTimer)
|
||||
this._advanceTimer = null
|
||||
}
|
||||
if (this.data.currentIndex > 0) {
|
||||
const newIndex = this.data.currentIndex - 1
|
||||
const newQuestion = this.data.questions[newIndex]
|
||||
@@ -134,6 +159,7 @@ Page({
|
||||
|
||||
// 本地缓存 + 全局缓存
|
||||
wx.setStorageSync('pdpResult', resultData)
|
||||
try { require('../../utils/analytics').track('test_complete', { type: 'pdp', result: dominantType, duration: resultData.testDuration }) } catch (e) {}
|
||||
if (app && typeof app.saveTestResult === 'function') {
|
||||
app.saveTestResult('pdp', resultData)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,52 @@
|
||||
/**
|
||||
* 小程序埋点:批量上报 /api/analytics/events(可选登录,便于超管后台关联用户)
|
||||
* 小程序埋点:批量上报 /api/analytics/events
|
||||
* - session_id 区分每次启动
|
||||
* - 自动采集平台、网络、设备、场景值
|
||||
* - 定时 flush + 页面隐藏 flush
|
||||
*/
|
||||
const { request } = require('./request.js')
|
||||
|
||||
const MAX_BATCH = 30
|
||||
const FLUSH_INTERVAL = 10000
|
||||
const queue = []
|
||||
let lastPageReport = { path: '', t: 0 }
|
||||
|
||||
function getAppSafe() {
|
||||
const sessionId = 'wx_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
|
||||
|
||||
let _deviceInfo = null
|
||||
function getDeviceInfo() {
|
||||
if (_deviceInfo) return _deviceInfo
|
||||
try {
|
||||
return getApp()
|
||||
const sys = wx.getSystemInfoSync()
|
||||
_deviceInfo = {
|
||||
brand: sys.brand,
|
||||
model: sys.model,
|
||||
system: sys.system,
|
||||
platform: sys.platform,
|
||||
version: sys.version,
|
||||
SDKVersion: sys.SDKVersion,
|
||||
screenWidth: sys.screenWidth,
|
||||
screenHeight: sys.screenHeight,
|
||||
pixelRatio: sys.pixelRatio
|
||||
}
|
||||
} catch (e) {
|
||||
return null
|
||||
_deviceInfo = {}
|
||||
}
|
||||
return _deviceInfo
|
||||
}
|
||||
|
||||
let _networkType = 'unknown'
|
||||
function refreshNetwork() {
|
||||
try {
|
||||
wx.getNetworkType({
|
||||
success(res) { _networkType = res.networkType || 'unknown' }
|
||||
})
|
||||
} catch (e) {}
|
||||
}
|
||||
refreshNetwork()
|
||||
|
||||
function getAppSafe() {
|
||||
try { return getApp() } catch (e) { return null }
|
||||
}
|
||||
|
||||
function getCurrentRoute() {
|
||||
@@ -21,19 +55,27 @@ function getCurrentRoute() {
|
||||
return p && p.route ? p.route : ''
|
||||
}
|
||||
|
||||
function getScene() {
|
||||
const app = getAppSafe()
|
||||
return (app && app.globalData && app.globalData.scene) || ''
|
||||
}
|
||||
|
||||
function track(eventName, props) {
|
||||
if (!eventName || typeof eventName !== 'string') return
|
||||
const app = getAppSafe()
|
||||
const openId =
|
||||
app && app.globalData
|
||||
? (app.globalData.openId || (app.globalData.userInfo && (app.globalData.userInfo.openid || app.globalData.userInfo.openId)) || '')
|
||||
: ''
|
||||
const openId = app && app.globalData
|
||||
? (app.globalData.openId || (app.globalData.userInfo && (app.globalData.userInfo.openid || app.globalData.userInfo.openId)) || '')
|
||||
: ''
|
||||
queue.push({
|
||||
event_name: eventName,
|
||||
page_path: getCurrentRoute(),
|
||||
props: props && typeof props === 'object' ? props : {},
|
||||
client_ts: Date.now(),
|
||||
openid: openId || undefined
|
||||
openid: openId || undefined,
|
||||
session_id: sessionId,
|
||||
platform: 'wechat',
|
||||
network: _networkType,
|
||||
scene: getScene()
|
||||
})
|
||||
if (queue.length >= MAX_BATCH) {
|
||||
flush()
|
||||
@@ -41,15 +83,13 @@ function track(eventName, props) {
|
||||
}
|
||||
|
||||
/** 页面曝光(防抖:同路径 2 秒内只记一次) */
|
||||
function reportPageView() {
|
||||
function reportPageView(extraProps) {
|
||||
const path = getCurrentRoute()
|
||||
if (!path) return
|
||||
const now = Date.now()
|
||||
if (path === lastPageReport.path && now - lastPageReport.t < 2000) {
|
||||
return
|
||||
}
|
||||
if (path === lastPageReport.path && now - lastPageReport.t < 2000) return
|
||||
lastPageReport = { path, t: now }
|
||||
track('page_view', { path })
|
||||
track('page_view', { path, ...(extraProps || {}) })
|
||||
}
|
||||
|
||||
function flush() {
|
||||
@@ -59,16 +99,57 @@ function flush() {
|
||||
url: '/api/analytics/events',
|
||||
method: 'POST',
|
||||
needAuth: true,
|
||||
data: { events },
|
||||
data: { events, device: getDeviceInfo() },
|
||||
allow401: false,
|
||||
success() {},
|
||||
fail() {}
|
||||
})
|
||||
}
|
||||
|
||||
/** 上报应用启动(首次 onLaunch 调用一次) */
|
||||
function reportAppLaunch(launchOptions) {
|
||||
const opts = launchOptions || {}
|
||||
track('app_launch', {
|
||||
scene: opts.scene,
|
||||
query: opts.query || {},
|
||||
referrerInfo: opts.referrerInfo || {},
|
||||
device: getDeviceInfo()
|
||||
})
|
||||
}
|
||||
|
||||
/** 上报分享行为 */
|
||||
function reportShare(channel, extra) {
|
||||
track('share', { channel: channel || 'friend', ...(extra || {}) })
|
||||
flush()
|
||||
}
|
||||
|
||||
/** 上报支付结果 */
|
||||
function reportPayResult(success, extra) {
|
||||
track(success ? 'pay_success' : 'pay_fail', extra || {})
|
||||
flush()
|
||||
}
|
||||
|
||||
let _flushTimer = null
|
||||
function startAutoFlush() {
|
||||
if (_flushTimer) return
|
||||
_flushTimer = setInterval(flush, FLUSH_INTERVAL)
|
||||
}
|
||||
function stopAutoFlush() {
|
||||
if (_flushTimer) {
|
||||
clearInterval(_flushTimer)
|
||||
_flushTimer = null
|
||||
}
|
||||
}
|
||||
startAutoFlush()
|
||||
|
||||
module.exports = {
|
||||
track,
|
||||
flush,
|
||||
reportPageView,
|
||||
getCurrentRoute
|
||||
reportAppLaunch,
|
||||
reportShare,
|
||||
reportPayResult,
|
||||
getCurrentRoute,
|
||||
startAutoFlush,
|
||||
stopAutoFlush
|
||||
}
|
||||
|
||||
@@ -130,15 +130,15 @@ function wxPay(options) {
|
||||
// 支付成功后轮询后端订单状态 3~5 次,确保通过微信订单查询接口确认成功
|
||||
pollOrderStatus(orderId, 5, 1000, (ok, order) => {
|
||||
if (ok) {
|
||||
try { require('./analytics').reportPayResult(true, { productType: productType || '', orderId, amount }) } catch (e) {}
|
||||
wx.showToast({
|
||||
title: '支付成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
// 将本地订单信息一并透传给业务方
|
||||
success && success({ payRes, order })
|
||||
} else {
|
||||
// 落库状态暂未确认,但微信侧已成功,一般稍后会自动对齐
|
||||
try { require('./analytics').reportPayResult(true, { productType: productType || '', orderId, amount, note: 'poll_pending' }) } catch (e) {}
|
||||
wx.showToast({
|
||||
title: '支付结果处理中,请稍后在历史中查看',
|
||||
icon: 'none',
|
||||
@@ -150,8 +150,9 @@ function wxPay(options) {
|
||||
},
|
||||
fail: (payErr) => {
|
||||
console.error('支付失败', payErr)
|
||||
|
||||
if (payErr.errMsg.indexOf('cancel') !== -1) {
|
||||
const isCancel = payErr.errMsg && payErr.errMsg.indexOf('cancel') !== -1
|
||||
try { require('./analytics').reportPayResult(false, { productType: productType || '', orderId, reason: isCancel ? 'cancel' : 'fail' }) } catch (e) {}
|
||||
if (isCancel) {
|
||||
wx.showToast({
|
||||
title: '支付已取消',
|
||||
icon: 'none'
|
||||
|
||||
Reference in New Issue
Block a user