chore: 首次提交 - 关联 GitHub fnvtk/MBTI_wang

Made-with: Cursor
This commit is contained in:
卡若
2026-03-17 12:39:38 +08:00
commit eb510304c0
259 changed files with 60464 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
// utils/descriptions.js - 测试结果描述
// MBTI类型描述
const mbtiDescriptions = {
ISTJ: { type: "ISTJ", name: "检查员", category: "守护者", description: "务实、负责、可靠的传统主义者", strengths: ["可靠负责", "注重细节", "有条理"], weaknesses: ["可能过于固执", "不喜变化"], careers: ["会计师", "审计师", "项目经理"] },
ISFJ: { type: "ISFJ", name: "保护者", category: "守护者", description: "安静、友好、负责任的守护者", strengths: ["忠诚体贴", "观察力强", "耐心"], weaknesses: ["不善拒绝", "过于谦虚"], careers: ["护士", "教师", "行政"] },
INFJ: { type: "INFJ", name: "提倡者", category: "理想主义者", description: "寻求意义和联系的理想主义者", strengths: ["洞察力强", "有远见", "坚定"], weaknesses: ["过于理想化", "容易疲惫"], careers: ["心理咨询", "作家", "人力资源"] },
INTJ: { type: "INTJ", name: "建筑师", category: "理想主义者", description: "独立、有战略眼光的思考者", strengths: ["战略思维", "独立自信", "意志坚定"], weaknesses: ["可能傲慢", "过于苛刻"], careers: ["战略顾问", "科学家", "架构师"] },
ISTP: { type: "ISTP", name: "鉴赏家", category: "探险家", description: "灵活、务实的问题解决者", strengths: ["适应力强", "动手能力", "冷静"], weaknesses: ["可能冷漠", "不善表达"], careers: ["工程师", "技术员", "飞行员"] },
ISFP: { type: "ISFP", name: "探险家", category: "探险家", description: "温和、敏感的艺术家", strengths: ["创造力", "同理心", "灵活"], weaknesses: ["过于敏感", "避免冲突"], careers: ["设计师", "艺术家", "摄影师"] },
INFP: { type: "INFP", name: "调停者", category: "理想主义者", description: "理想主义、忠诚的调解者", strengths: ["创造力", "同理心", "真诚"], weaknesses: ["过于理想化", "情绪化"], careers: ["作家", "心理咨询", "社工"] },
INTP: { type: "INTP", name: "逻辑学家", category: "理想主义者", description: "创新、逻辑的思考者", strengths: ["逻辑思维", "创新能力", "客观"], weaknesses: ["可能孤僻", "忽视情感"], careers: ["程序员", "研究员", "分析师"] },
ESTP: { type: "ESTP", name: "企业家", category: "探险家", description: "精力充沛、务实的行动者", strengths: ["果断", "务实", "善于应变"], weaknesses: ["可能冲动", "缺乏耐心"], careers: ["销售", "企业家", "运动员"] },
ESFP: { type: "ESFP", name: "表演者", category: "探险家", description: "热情、友好的社交达人", strengths: ["热情友好", "乐观", "灵活"], weaknesses: ["可能肤浅", "容易分心"], careers: ["演员", "销售", "主持人"] },
ENFP: { type: "ENFP", name: "竞选者", category: "理想主义者", description: "热情、有创造力的社交者", strengths: ["创造力", "热情", "善于沟通"], weaknesses: ["可能不切实际", "缺乏专注"], careers: ["市场营销", "记者", "顾问"] },
ENTP: { type: "ENTP", name: "辩论家", category: "理想主义者", description: "聪明、好奇的思想家", strengths: ["创新能力", "辩论能力", "适应力"], weaknesses: ["可能争辩", "不善执行"], careers: ["律师", "企业家", "咨询"] },
ESTJ: { type: "ESTJ", name: "总经理", category: "守护者", description: "务实、果断的组织者", strengths: ["领导力", "组织能力", "务实"], weaknesses: ["可能专制", "不够灵活"], careers: ["管理者", "项目经理", "律师"] },
ESFJ: { type: "ESFJ", name: "执政官", category: "守护者", description: "热心、合作的支持者", strengths: ["关心他人", "负责任", "善于合作"], weaknesses: ["过于在意评价", "不善拒绝"], careers: ["教师", "护士", "人力资源"] },
ENFJ: { type: "ENFJ", name: "主人公", category: "理想主义者", description: "有魅力、鼓舞人心的领导者", strengths: ["领导力", "同理心", "说服力"], weaknesses: ["过于理想化", "过度付出"], careers: ["培训师", "顾问", "教师"] },
ENTJ: { type: "ENTJ", name: "指挥官", category: "理想主义者", description: "大胆、有远见的领导者", strengths: ["领导力", "战略思维", "果断"], weaknesses: ["可能专制", "不耐烦"], careers: ["CEO", "企业家", "律师"] }
}
// DISC类型描述
const discDescriptions = {
D: { type: "D型", title: "支配型", color: "#EF4444", description: "天生的领导者,注重结果和效率", strengths: ["决断力强", "目标导向", "行动迅速"], weaknesses: ["可能过于强势", "缺乏耐心"], careers: ["企业高管", "创业者", "项目经理"] },
I: { type: "I型", title: "影响型", color: "#F59E0B", description: "热情友好,善于社交和表达", strengths: ["善于沟通", "乐观积极", "创意丰富"], weaknesses: ["可能过于乐观", "注意力分散"], careers: ["市场营销", "公关", "培训师"] },
S: { type: "S型", title: "稳健型", color: "#10B981", description: "稳重可靠,注重团队和谐", strengths: ["可靠稳定", "团队协作", "耐心倾听"], weaknesses: ["可能抗拒变化", "决策较慢"], careers: ["人力资源", "客户服务", "行政"] },
C: { type: "C型", title: "谨慎型", color: "#3B82F6", description: "注重细节和准确性,追求完美", strengths: ["分析能力强", "注重细节", "准确严谨"], weaknesses: ["可能过于完美主义", "决策较慢"], careers: ["数据分析", "工程师", "会计师"] }
}
// PDP类型描述
const pdpDescriptions = {
Tiger: { type: "老虎型", emoji: "🐅", title: "支配者", color: "#F59E0B", description: "天生的领导者,具有强烈的目标导向和执行力", strengths: ["决断力强", "目标导向", "行动迅速"], weaknesses: ["可能过于强势", "缺乏耐心"], careers: ["企业高管", "创业者", "项目经理"], teamRole: "适合担任领导者角色" },
Peacock: { type: "孔雀型", emoji: "🦚", title: "表现者", color: "#8B5CF6", description: "热情友好,善于表达和社交", strengths: ["善于沟通", "乐观积极", "影响力强"], weaknesses: ["可能过于乐观", "注意力分散"], careers: ["市场营销", "公关", "培训师"], teamRole: "适合担任激励者和调解者角色" },
Koala: { type: "考拉型", emoji: "🐨", title: "支持者", color: "#10B981", description: "稳重可靠,注重团队和谐", strengths: ["可靠稳定", "团队协作", "善解人意"], weaknesses: ["可能抗拒变化", "决策较慢"], careers: ["人力资源", "客户服务", "教师"], teamRole: "适合担任协调者和支持者角色" },
Owl: { type: "猫头鹰型", emoji: "🦉", title: "分析者", color: "#3B82F6", description: "注重细节和准确性,追求完美", strengths: ["分析能力强", "注重细节", "系统思维"], weaknesses: ["可能过于完美主义", "不善社交"], careers: ["数据分析师", "工程师", "研究员"], teamRole: "适合担任专家和质量控制者角色" },
Chameleon: { type: "变色龙型", emoji: "🦎", title: "整合者", color: "#06B6D4", description: "适应能力强,能在不同情境中灵活调整", strengths: ["适应力强", "灵活多变", "平衡能力"], weaknesses: ["可能缺乏主见", "身份认同模糊"], careers: ["咨询顾问", "协调员", "自由职业者"], teamRole: "适合担任协调者和多面手角色" }
}
module.exports = {
mbtiDescriptions,
discDescriptions,
pdpDescriptions
}

View File

@@ -0,0 +1,628 @@
// miniprogram/utils/payment.js
// 微信支付工具类 - 复刻自Soul项目
const app = getApp()
/**
* 生成符合微信规则的订单号
* 规则示例FACE20260226151900001前缀+时间戳+随机数最长不超过32位
* @param {String} productType - 业务类型,如 face/mbti/disc/pdp/report/team_analysis/vip 等
*/
function generateOrderId(productType) {
const now = new Date()
const pad = (n, len = 2) => String(n).padStart(len, '0')
const prefixMap = {
face: 'FACE',
mbti: 'MBTI',
disc: 'DISC',
pdp: 'PDP',
report: 'REPT',
team_analysis: 'TEAM',
vip: 'VIP',
test_count: 'TNUM',
single_test: 'TSGL',
recharge: 'RCG',
deep_personal: 'DPER',
deep_team: 'DTEAM'
}
const prefix = prefixMap[productType] || (productType || 'ORD').toUpperCase().slice(0, 6)
const dateStr =
now.getFullYear().toString() +
pad(now.getMonth() + 1) +
pad(now.getDate()) +
pad(now.getHours()) +
pad(now.getMinutes()) +
pad(now.getSeconds())
// 3位随机数避免重复如 001~999
const rand = pad(Math.floor(Math.random() * 1000), 3)
const raw = `${prefix}${dateStr}${rand}` // 示例FACE20260226151900001
// 微信 out_trade_no 最长 32 字节,这里兜底截断
return raw.length > 32 ? raw.slice(0, 32) : raw
}
/**
* 发起微信支付
* @param {Object} options - 支付选项
* @param {String} options.orderId - 订单ID
* @param {Number} [options.amount] - 支付金额(分),部分产品使用(如充值/VIP/测试次数等);深度服务与按配置定价的产品传 0 或不传
* @param {String} options.description - 商品描述
* @param {String} options.productType - 商品类型: 'vip' | 'test_count' | 'single_test' | 'face' | 'mbti' 等
* @param {Number} [options.testResultId] - 可选,对应 mbti_test_results.id用于精确关联测试记录
* @param {String} [options.deepProductId] - 可选深度服务对应的套餐ID/产品Key用于后台从 categories 中选中具体价格
* @param {Function} options.success - 成功回调
* @param {Function} options.fail - 失败回调
*/
function wxPay(options) {
const { orderId, amount = 0, description, productType, testResultId, deepProductId, enterpriseId, success, fail } = options
wx.showLoading({
title: '正在支付...',
mask: true
})
// 1. 调用后端创建支付订单
wx.request({
url: `${app.globalData.apiBase}/api/payment/create`,
method: 'POST',
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`,
'Content-Type': 'application/json'
},
data: {
orderId,
amount,
description,
productType,
paymentMethod: 'wechat',
openId: app.globalData.openId || '',
enterpriseId: enterpriseId || 0,
// 创建订单时将本次测试记录ID传给后端避免每次都只更新“最新一条”
testResultId: testResultId || 0,
// 深度服务使用的具体套餐ID/产品Key用于从 categories 中选择价格)
deepProductId: deepProductId || ''
},
success: (res) => {
wx.hideLoading()
if (res.statusCode === 200 && res.data.code === 200) {
const paymentData = res.data.data
// 2. 调起微信支付
wx.requestPayment({
timeStamp: paymentData.timeStamp,
nonceStr: paymentData.nonceStr,
package: paymentData.package,
signType: paymentData.signType || 'MD5',
paySign: paymentData.paySign,
success: (payRes) => {
console.log('支付成功', payRes)
// 按照微信文档推荐,仅依赖查询接口确认支付结果,不做本地立即标记
// 支付成功后轮询后端订单状态 3~5 次,确保通过微信订单查询接口确认成功
pollOrderStatus(orderId, 5, 1000, (ok, order) => {
if (ok) {
wx.showToast({
title: '支付成功',
icon: 'success',
duration: 2000
})
// 将本地订单信息一并透传给业务方
success && success({ payRes, order })
} else {
// 落库状态暂未确认,但微信侧已成功,一般稍后会自动对齐
wx.showToast({
title: '支付结果处理中,请稍后在历史中查看',
icon: 'none',
duration: 2500
})
success && success({ payRes, order: null })
}
})
},
fail: (payErr) => {
console.error('支付失败', payErr)
if (payErr.errMsg.indexOf('cancel') !== -1) {
wx.showToast({
title: '支付已取消',
icon: 'none'
})
} else {
wx.showToast({
title: '支付失败',
icon: 'none'
})
}
fail && fail(payErr)
}
})
} else {
wx.showToast({
title: res.data.message || '创建订单失败',
icon: 'none'
})
fail && fail(res)
}
},
fail: (err) => {
wx.hideLoading()
console.error('请求失败', err)
wx.showToast({
title: '网络请求失败',
icon: 'none'
})
fail && fail(err)
}
})
}
/**
* 通知后端支付成功
*/
function notifyPaymentSuccess(orderId, prepayId) {
wx.request({
url: `${app.globalData.apiBase}/api/payment/notify`,
method: 'POST',
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`,
'Content-Type': 'application/json'
},
data: {
orderId,
prepayId,
status: 'success'
},
success: (res) => {
console.log('支付通知成功', res)
},
fail: (err) => {
console.error('支付通知失败', err)
}
})
}
/**
* 查询订单状态
*/
function queryOrderStatus(orderId, callback) {
wx.request({
url: `${app.globalData.apiBase}/api/payment/query`,
method: 'GET',
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`
},
data: { orderId },
success: (res) => {
if (res.statusCode === 200 && res.data.code === 200) {
callback && callback(true, res.data.data)
} else {
callback && callback(false, null)
}
},
fail: () => {
callback && callback(false, null)
}
})
}
/**
* 按照微信文档建议:支付成功后,基于商户订单号轮询查询订单状态(本项目由后端代理查询)
* @param {String} orderId 商户订单号(如 FACE20260226151900001
* @param {Number} maxAttempts 最大轮询次数
* @param {Number} intervalMs 间隔毫秒
* @param {Function} done 回调 (ok:boolean, order?:object)
*/
function pollOrderStatus(orderId, maxAttempts = 5, intervalMs = 1000, done) {
let attempts = 0
const tick = () => {
attempts += 1
queryOrderStatus(orderId, (ok, order) => {
if (ok && order && (order.status === 'paid' || order.status === 'completed')) {
done && done(true, order)
return
}
if (attempts >= maxAttempts) {
done && done(false, order || null)
return
}
setTimeout(tick, intervalMs)
})
}
tick()
}
/**
* 购买VIP会员
* @param {String} vipType - 'month' | 'quarter' | 'year' | 'lifetime'
*/
function purchaseVIP(vipType, success, fail) {
const prices = {
month: 1990, // 19.9元
quarter: 4990, // 49.9元
year: 9900, // 99元
lifetime: 19900, // 199元
personal_insight: 19800 // 198元 - 个人深度洞察版
}
const names = {
month: '月度VIP会员',
quarter: '季度VIP会员',
year: '年度VIP会员',
lifetime: '终身VIP会员',
personal_insight: '个人深度洞察版'
}
const orderId = generateOrderId('vip')
wxPay({
orderId,
amount: prices[vipType],
description: `MBTI性格测试 - ${names[vipType]}`,
productType: 'vip',
success: (res) => {
// 更新VIP状态
updateVIPStatus(vipType)
success && success(res)
},
fail
})
}
/**
* 购买测试次数
* @param {Number} count - 购买次数
*/
function purchaseTestCount(count, success, fail) {
// 单次价格 3.9元10次29元50次99元
let price = count * 390
if (count >= 10) price = Math.floor(count * 290)
if (count >= 50) price = Math.floor(count * 198)
const orderId = generateOrderId('test_count')
wxPay({
orderId,
amount: price,
description: `MBTI性格测试 - ${count}次测试次数`,
productType: 'test_count',
success: (res) => {
// 更新测试次数
addTestCount(count)
success && success(res)
},
fail
})
}
/**
* 购买单次测试
* @param {String} testType - 'mbti' | 'disc' | 'pdp' | 'ai'
*/
function purchaseSingleTest(testType, success, fail) {
const prices = {
mbti: 990, // 9.9元
disc: 690, // 6.9元
pdp: 690, // 6.9元
ai: 1990 // 19.9元
}
const names = {
mbti: 'MBTI性格测试',
disc: 'DISC行为风格测试',
pdp: 'PDP动物性格测试',
ai: 'AI人脸性格分析'
}
const orderId = generateOrderId(`single_${testType}`)
wxPay({
orderId,
amount: prices[testType],
description: names[testType],
productType: 'single_test',
success: (res) => {
// 解锁该测试
unlockTest(testType)
success && success(res)
},
fail
})
}
/**
* 使用后台定价购买单项测试/报告
* 支持:人脸测试/MBTI测试/DISC测试/PDP测试/完整报告/团队分析
* @param {String} productType - 'face' | 'mbti' | 'disc' | 'pdp' | 'report' | 'team_analysis'
* @param {String} description - 商品描述
* @param {Function|Object} extra - 兼容两种调用方式:
* - purchaseByPricing('face', 'xxx', success, fail)
* - purchaseByPricing('face', 'xxx', { testResultId, success, fail })
*/
function purchaseByPricing(productType, description, extra, maybeFail) {
let opts = {}
if (typeof extra === 'function' || extra === undefined) {
opts.success = extra
opts.fail = maybeFail
} else {
opts = extra || {}
}
const { testResultId, success, fail } = opts
const orderId = generateOrderId(productType)
wxPay({
orderId,
amount: 0, // 金额交由后端根据定价配置计算personal/enterprise
description,
productType,
testResultId,
success,
fail
})
}
// 人脸测试完整报告
// 兼容两种调用方式:
// - purchaseFaceTest(success, fail)
// - purchaseFaceTest({ testResultId, success, fail })
function purchaseFaceTest(arg1, arg2) {
let opts = {}
if (typeof arg1 === 'function' || arg1 === undefined) {
opts.success = arg1
opts.fail = arg2
} else {
opts = arg1 || {}
}
const { testResultId, success, fail } = opts
purchaseByPricing('face', 'AI人脸性格分析完整报告', { testResultId, success, fail })
}
// MBTI测试付费版支持 purchaseMbtiTest({ testResultId, success, fail })
function purchaseMbtiTest(arg1, arg2) {
const opts = typeof arg1 === 'function' || arg1 == null ? { success: arg1, fail: arg2 } : (arg1 || {})
const { testResultId, success, fail } = opts
purchaseByPricing('mbti', 'MBTI性格测试付费版', { testResultId, success, fail })
}
// DISC测试付费版
function purchaseDiscTest(arg1, arg2) {
const opts = typeof arg1 === 'function' || arg1 == null ? { success: arg1, fail: arg2 } : (arg1 || {})
const { testResultId, success, fail } = opts
purchaseByPricing('disc', 'DISC行为风格测试付费版', { testResultId, success, fail })
}
// PDP测试付费版
function purchasePdpTest(arg1, arg2) {
const opts = typeof arg1 === 'function' || arg1 == null ? { success: arg1, fail: arg2 } : (arg1 || {})
const { testResultId, success, fail } = opts
purchaseByPricing('pdp', 'PDP动物性格测试付费版', { testResultId, success, fail })
}
// 简历综合分析付费版(支持 purchaseResumeAnalysis({ testResultId, success, fail })
function purchaseResumeAnalysis(arg1, arg2) {
const opts = typeof arg1 === 'function' || arg1 == null ? { success: arg1, fail: arg2 } : (arg1 || {})
const { testResultId, success, fail } = opts
purchaseByPricing('resume', '简历综合分析付费版', { testResultId, success, fail })
}
// 完整报告整合MBTI/DISC/PDP/AI人脸等深度解读
function purchaseFullReport(success, fail) {
purchaseByPricing('report', '完整人格与职业发展报告', success, fail)
}
// 团队分析服务
function purchaseTeamAnalysis(success, fail) {
purchaseByPricing('team_analysis', '团队性格组合与冲突分析服务', success, fail)
}
/**
* 企业充值
* 支持两种调用:
* - recharge(100, success, fail)
* - recharge({ amountYuan: 100, enterpriseId: 6, success, fail })
*/
function recharge(arg1, arg2, arg3) {
const opts = (typeof arg1 === 'object' && arg1 !== null)
? arg1
: { amountYuan: arg1, success: arg2, fail: arg3 }
const safeAmount = Number(opts.amountYuan) || 0
const amount = Math.round(safeAmount * 100)
const orderId = generateOrderId('recharge')
wxPay({
orderId,
amount,
description: `账户充值 ¥${safeAmount.toFixed(2)}`,
productType: 'recharge',
enterpriseId: opts.enterpriseId || 0,
success: opts.success,
fail: opts.fail
})
}
/**
* 个人深度服务(价格从 PricingConfig deep_personal.categories 中读取)
* 支持两种调用方式:
* - purchasePersonalDeepService(deepProductId, success, fail)
* - purchasePersonalDeepService({ deepProductId, description, success, fail })
* @param {String|Object} arg1
* @param {Function} [arg2]
* @param {Function} [arg3]
*/
function purchasePersonalDeepService(arg1, arg2, arg3) {
let deepProductId = ''
let description = ''
let success
let fail
if (typeof arg1 === 'object' && arg1 !== null) {
deepProductId = arg1.deepProductId || ''
description = arg1.description || ''
success = arg1.success
fail = arg1.fail
} else {
deepProductId = typeof arg1 === 'string' ? arg1 : ''
success = arg2
fail = arg3
}
// 默认标题兜底
const desc = description || '个人深度服务1v1深度解读'
const orderId = generateOrderId('deep_personal')
wxPay({
orderId,
amount: 0,
description: desc,
productType: 'deep_personal',
deepProductId,
success,
fail
})
}
/**
* 团队深度服务(价格从 PricingConfig.deep.team 读取)
*/
function purchaseTeamDeepService(success, fail) {
const orderId = generateOrderId('deep_team')
wxPay({
orderId,
amount: 0,
description: '团队深度服务(团队画像+策略)',
productType: 'deep_team',
success,
fail
})
}
/**
* 更新VIP状态
*/
function updateVIPStatus(vipType) {
const durations = {
month: 30,
quarter: 90,
year: 365,
lifetime: 36500 // 100年
}
const expireDate = new Date()
expireDate.setDate(expireDate.getDate() + durations[vipType])
const vipInfo = {
isVIP: true,
vipType,
expireDate: expireDate.toISOString(),
purchaseDate: new Date().toISOString()
}
wx.setStorageSync('vipInfo', vipInfo)
app.globalData.vipInfo = vipInfo
}
/**
* 增加测试次数
*/
function addTestCount(count) {
const currentCount = wx.getStorageSync('testCount') || 0
const newCount = currentCount + count
wx.setStorageSync('testCount', newCount)
app.globalData.testCount = newCount
}
/**
* 解锁单次测试
*/
function unlockTest(testType) {
const unlockedTests = wx.getStorageSync('unlockedTests') || []
if (!unlockedTests.includes(testType)) {
unlockedTests.push(testType)
wx.setStorageSync('unlockedTests', unlockedTests)
}
app.globalData.unlockedTests = unlockedTests
}
/**
* 检查是否是VIP
*/
function checkVIP() {
const vipInfo = wx.getStorageSync('vipInfo')
if (!vipInfo || !vipInfo.isVIP) return false
const expireDate = new Date(vipInfo.expireDate)
return expireDate > new Date()
}
/**
* 检查测试是否可用VIP或已解锁或有测试次数
*/
function canTakeTest(testType) {
// 临时策略:所有测试免费开放,直接返回 true
// 保留原有支付与权益逻辑,后续若恢复收费可还原为 VIP/解锁/次数判断
return true
}
/**
* 消耗一次测试次数
*/
function consumeTestCount() {
const testCount = wx.getStorageSync('testCount') || 0
if (testCount > 0) {
wx.setStorageSync('testCount', testCount - 1)
app.globalData.testCount = testCount - 1
return true
}
return false
}
/**
* 获取用户权益信息
*/
function getUserBenefits() {
return {
isVIP: checkVIP(),
vipInfo: wx.getStorageSync('vipInfo') || null,
testCount: wx.getStorageSync('testCount') || 0,
unlockedTests: wx.getStorageSync('unlockedTests') || []
}
}
module.exports = {
wxPay,
queryOrderStatus,
pollOrderStatus,
generateOrderId,
purchaseVIP,
purchaseTestCount,
purchaseSingleTest,
// 基于后台定价的购买入口(测试/报告/团队分析/深度服务/充值)
purchaseFaceTest,
purchaseMbtiTest,
purchaseDiscTest,
purchasePdpTest,
purchaseResumeAnalysis,
purchaseFullReport,
purchaseTeamAnalysis,
recharge,
purchasePersonalDeepService,
purchaseTeamDeepService,
checkVIP,
canTakeTest,
consumeTestCount,
getUserBenefits,
updateVIPStatus,
addTestCount,
unlockTest
}

View File

@@ -0,0 +1,113 @@
/**
* 手机号授权工具:基于微信 getPhoneNumber + 服务器端换取手机号接口
* 个人资料完整性检查:头像、昵称、手机号为必填,生日和性别选填
*/
/**
* 个人资料是否已完善(头像、昵称、手机号必填,生日和性别选填)
* @returns {boolean}
*/
function isProfileComplete() {
const app = getApp()
const user = app.globalData.userInfo || wx.getStorageSync('userInfo')
if (!user) return false
const avatar = (user.avatar || user.avatarUrl || '').trim()
const nickname = (user.nickname || user.nickName || '').trim()
const phone = (user.phone || user.phoneNumber || '').trim()
return avatar.length > 0 && nickname.length > 0 && phone.length > 0
}
/**
* 若资料未完善则跳转到个人资料页,需登录
* @returns {boolean} true=已完善可继续false=已跳转
*/
function ensureProfileCompleteAndRedirect() {
const app = getApp()
const token = app.globalData.token || wx.getStorageSync('token')
if (!token) return true
if (isProfileComplete()) return true
wx.showToast({ title: '请先完善个人资料', icon: 'none' })
wx.navigateTo({ url: '/pages/user-profile/index' })
return false
}
/**
* 当前用户是否已有手机号(从 globalData.userInfo 或 storage 读取)
* @returns {boolean}
*/
function hasPhone() {
const app = getApp()
const user = app.globalData.userInfo || wx.getStorageSync('userInfo')
const phone = (user && (user.phone || user.phoneNumber)) ? String(user.phone || user.phoneNumber).trim() : ''
return phone.length > 0
}
/**
* 使用 getPhoneNumber 回调里的 code 调用后端接口换取手机号,并写回 userInfo
* @param {string} code
* @returns {Promise<object>} resolve 为更新后的 userInfo
*/
function bindPhoneByCode(code) {
return new Promise((resolve, reject) => {
if (!code) {
wx.showToast({ title: '获取手机号失败', icon: 'none' })
reject(new Error('empty code'))
return
}
const app = getApp()
const token = app.globalData.token || wx.getStorageSync('token')
if (!token) {
wx.showToast({ title: '请先登录', icon: 'none' })
reject(new Error('no token'))
return
}
const apiBase = app.globalData.apiBase || ''
if (!apiBase) {
wx.showToast({ title: '服务未配置', icon: 'none' })
reject(new Error('no api base'))
return
}
wx.showLoading({ title: '处理中...', mask: true })
wx.request({
url: `${apiBase.replace(/\/$/, '')}/api/auth/wechat/phone`,
method: 'POST',
header: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
},
data: { code },
success: (res) => {
wx.hideLoading()
if (res.statusCode === 200 && res.data && res.data.code === 200) {
const data = res.data.data || {}
const user = data.user || app.globalData.userInfo || {}
const phone = data.phone || user.phone || ''
const newUser = { ...user, phone }
app.globalData.userInfo = newUser
wx.setStorageSync('userInfo', newUser)
wx.showToast({ title: '授权成功', icon: 'success' })
resolve(newUser)
} else {
const msg = res.data && res.data.message ? res.data.message : '获取手机号失败'
wx.showToast({ title: msg, icon: 'none' })
reject(new Error(msg))
}
},
fail: () => {
wx.hideLoading()
wx.showToast({ title: '网络请求失败', icon: 'none' })
reject(new Error('network error'))
},
})
})
}
module.exports = {
hasPhone,
bindPhoneByCode,
isProfileComplete,
ensureProfileCompleteAndRedirect,
}

View File

@@ -0,0 +1,117 @@
// utils/questions.js - 测试题库
// MBTI测试题精简版30题每个维度约7-8题
const mbtiQuestions = [
// E vs I 维度 (8题)
{ id: 1, question: "在社交场合中,您通常会:", dimension: "EI", options: [{ value: "E", text: "认识新朋友,扩大社交圈" }, { value: "I", text: "与已认识的朋友交流" }] },
{ id: 2, question: "您更喜欢:", dimension: "EI", options: [{ value: "E", text: "在团队中工作" }, { value: "I", text: "独立工作" }] },
{ id: 3, question: "当您需要充电时,您会选择:", dimension: "EI", options: [{ value: "E", text: "与朋友聚会" }, { value: "I", text: "独处休息" }] },
{ id: 4, question: "在会议中,您通常:", dimension: "EI", options: [{ value: "E", text: "积极发言" }, { value: "I", text: "先思考再发表" }] },
{ id: 5, question: "您更倾向于:", dimension: "EI", options: [{ value: "E", text: "认识很多人" }, { value: "I", text: "与少数人深交" }] },
{ id: 6, question: "面对问题时,您更倾向于:", dimension: "EI", options: [{ value: "E", text: "与他人讨论" }, { value: "I", text: "独自思考" }] },
{ id: 7, question: "您更喜欢的休闲活动是:", dimension: "EI", options: [{ value: "E", text: "团体活动" }, { value: "I", text: "个人爱好" }] },
{ id: 8, question: "在陌生环境中,您通常会:", dimension: "EI", options: [{ value: "E", text: "主动与人交谈" }, { value: "I", text: "观察周围环境" }] },
// S vs N 维度 (7题)
{ id: 9, question: "您更关注:", dimension: "SN", options: [{ value: "S", text: "具体的细节和事实" }, { value: "N", text: "整体概念和可能性" }] },
{ id: 10, question: "您更信任:", dimension: "SN", options: [{ value: "S", text: "实际经验" }, { value: "N", text: "直觉和灵感" }] },
{ id: 11, question: "您更喜欢:", dimension: "SN", options: [{ value: "S", text: "按步骤执行" }, { value: "N", text: "创造性解决" }] },
{ id: 12, question: "学习新事物时,您更喜欢:", dimension: "SN", options: [{ value: "S", text: "实际操作" }, { value: "N", text: "理论学习" }] },
{ id: 13, question: "您更喜欢的工作类型是:", dimension: "SN", options: [{ value: "S", text: "明确的任务" }, { value: "N", text: "创新的项目" }] },
{ id: 14, question: "您描述事物时更倾向于:", dimension: "SN", options: [{ value: "S", text: "具体描述" }, { value: "N", text: "使用比喻" }] },
{ id: 15, question: "您更关心:", dimension: "SN", options: [{ value: "S", text: "当下的现实" }, { value: "N", text: "未来的可能" }] },
// T vs F 维度 (8题)
{ id: 16, question: "做决定时,您更依赖:", dimension: "TF", options: [{ value: "T", text: "逻辑分析" }, { value: "F", text: "个人价值" }] },
{ id: 17, question: "在争论中,您更看重:", dimension: "TF", options: [{ value: "T", text: "事实真相" }, { value: "F", text: "和谐关系" }] },
{ id: 18, question: "给予反馈时,您更注重:", dimension: "TF", options: [{ value: "T", text: "直接指出问题" }, { value: "F", text: "考虑对方感受" }] },
{ id: 19, question: "评估方案时,您更关注:", dimension: "TF", options: [{ value: "T", text: "效率和结果" }, { value: "F", text: "对人的影响" }] },
{ id: 20, question: "当朋友遇到困难时,您更倾向于:", dimension: "TF", options: [{ value: "T", text: "分析问题提供建议" }, { value: "F", text: "倾听并给予支持" }] },
{ id: 21, question: "您更欣赏的品质是:", dimension: "TF", options: [{ value: "T", text: "理性客观" }, { value: "F", text: "善解人意" }] },
{ id: 22, question: "您认为好的决定应该:", dimension: "TF", options: [{ value: "T", text: "基于客观分析" }, { value: "F", text: "考虑各方感受" }] },
{ id: 23, question: "当与他人意见不同时,您更倾向于:", dimension: "TF", options: [{ value: "T", text: "坚持正确观点" }, { value: "F", text: "寻求共识" }] },
// J vs P 维度 (7题)
{ id: 24, question: "您更喜欢的工作方式是:", dimension: "JP", options: [{ value: "J", text: "有计划地进行" }, { value: "P", text: "随机应变" }] },
{ id: 25, question: "对于截止日期,您通常:", dimension: "JP", options: [{ value: "J", text: "提前完成" }, { value: "P", text: "在最后完成" }] },
{ id: 26, question: "您的生活方式更倾向于:", dimension: "JP", options: [{ value: "J", text: "有条理有规律" }, { value: "P", text: "灵活随意" }] },
{ id: 27, question: "面对选择时,您更倾向于:", dimension: "JP", options: [{ value: "J", text: "快速做出决定" }, { value: "P", text: "保持开放选项" }] },
{ id: 28, question: "您的桌面通常是:", dimension: "JP", options: [{ value: "J", text: "整洁有序" }, { value: "P", text: "创意性混乱" }] },
{ id: 29, question: "计划改变时,您的反应是:", dimension: "JP", options: [{ value: "J", text: "感到不安" }, { value: "P", text: "觉得有趣" }] },
{ id: 30, question: "您更喜欢:", dimension: "JP", options: [{ value: "J", text: "事先规划" }, { value: "P", text: "即兴发挥" }] }
]
// DISC测试题 (20题)
const discQuestions = [
{ id: 1, question: "在团队中,您更倾向于:", options: [{ value: "D", text: "主导决策,带领团队" }, { value: "I", text: "活跃气氛,激励成员" }, { value: "S", text: "支持他人,确保和谐" }, { value: "C", text: "分析数据,确保质量" }] },
{ id: 2, question: "面对挑战时,您的第一反应是:", options: [{ value: "D", text: "立即行动" }, { value: "I", text: "寻找支持" }, { value: "S", text: "冷静思考" }, { value: "C", text: "收集信息" }] },
{ id: 3, question: "您在工作中最看重的是:", options: [{ value: "D", text: "成果和效率" }, { value: "I", text: "认可和赞赏" }, { value: "S", text: "稳定和安全" }, { value: "C", text: "准确和质量" }] },
{ id: 4, question: "与他人沟通时,您通常:", options: [{ value: "D", text: "直接了当" }, { value: "I", text: "热情友好" }, { value: "S", text: "耐心倾听" }, { value: "C", text: "逻辑清晰" }] },
{ id: 5, question: "压力之下,您会:", options: [{ value: "D", text: "更加强势" }, { value: "I", text: "寻求鼓励" }, { value: "S", text: "保持冷静" }, { value: "C", text: "更加谨慎" }] },
{ id: 6, question: "您认为自己的优势是:", options: [{ value: "D", text: "决断力强" }, { value: "I", text: "人际关系好" }, { value: "S", text: "可靠稳定" }, { value: "C", text: "分析能力强" }] },
{ id: 7, question: "在会议中,您通常扮演:", options: [{ value: "D", text: "主导者" }, { value: "I", text: "激励者" }, { value: "S", text: "调和者" }, { value: "C", text: "分析者" }] },
{ id: 8, question: "您最不喜欢的工作环境是:", options: [{ value: "D", text: "进展缓慢" }, { value: "I", text: "被孤立" }, { value: "S", text: "变化太快" }, { value: "C", text: "混乱无序" }] },
{ id: 9, question: "做决定时,您更依赖:", options: [{ value: "D", text: "直觉经验" }, { value: "I", text: "他人意见" }, { value: "S", text: "过去经验" }, { value: "C", text: "数据事实" }] },
{ id: 10, question: "您的工作风格是:", options: [{ value: "D", text: "快速高效" }, { value: "I", text: "灵活多变" }, { value: "S", text: "稳定持续" }, { value: "C", text: "严谨细致" }] },
{ id: 11, question: "遇到冲突时,您会:", options: [{ value: "D", text: "直面解决" }, { value: "I", text: "调解双方" }, { value: "S", text: "避免冲突" }, { value: "C", text: "分析原因" }] },
{ id: 12, question: "您期望的领导风格是:", options: [{ value: "D", text: "给予挑战" }, { value: "I", text: "认可表扬" }, { value: "S", text: "稳定支持" }, { value: "C", text: "明确指导" }] },
{ id: 13, question: "处理任务时,您更注重:", options: [{ value: "D", text: "速度效率" }, { value: "I", text: "创意新颖" }, { value: "S", text: "过程协作" }, { value: "C", text: "质量准确" }] },
{ id: 14, question: "您的社交方式是:", options: [{ value: "D", text: "目的明确" }, { value: "I", text: "广泛社交" }, { value: "S", text: "深度交往" }, { value: "C", text: "选择性社交" }] },
{ id: 15, question: "您理想的工作节奏是:", options: [{ value: "D", text: "快节奏" }, { value: "I", text: "灵活多变" }, { value: "S", text: "稳定有序" }, { value: "C", text: "有条理" }] },
{ id: 16, question: "面对变化,您的态度是:", options: [{ value: "D", text: "主动拥抱" }, { value: "I", text: "积极适应" }, { value: "S", text: "需要时间" }, { value: "C", text: "谨慎评估" }] },
{ id: 17, question: "您的时间管理风格是:", options: [{ value: "D", text: "高效利用" }, { value: "I", text: "灵活安排" }, { value: "S", text: "按部就班" }, { value: "C", text: "精确规划" }] },
{ id: 18, question: "激励您的是:", options: [{ value: "D", text: "成就控制" }, { value: "I", text: "认可社交" }, { value: "S", text: "稳定归属" }, { value: "C", text: "正确标准" }] },
{ id: 19, question: "您处理细节的方式是:", options: [{ value: "D", text: "关注大局" }, { value: "I", text: "可能忽略" }, { value: "S", text: "认真对待" }, { value: "C", text: "极度重视" }] },
{ id: 20, question: "您对规则的态度是:", options: [{ value: "D", text: "灵活打破" }, { value: "I", text: "灵活运用" }, { value: "S", text: "遵守维护" }, { value: "C", text: "严格遵守" }] }
]
// PDP测试题 (20题)
const pdpQuestions = [
{ id: 1, question: "面对紧急任务,您的第一反应是:", options: [{ value: "Tiger", text: "立即行动" }, { value: "Peacock", text: "召集团队" }, { value: "Koala", text: "冷静分析" }, { value: "Owl", text: "仔细规划" }, { value: "Chameleon", text: "灵活应对" }] },
{ id: 2, question: "在社交场合,您通常会:", options: [{ value: "Tiger", text: "主导话题" }, { value: "Peacock", text: "活跃气氛" }, { value: "Koala", text: "安静倾听" }, { value: "Owl", text: "观察分析" }, { value: "Chameleon", text: "根据对象调整" }] },
{ id: 3, question: "您最看重工作中的:", options: [{ value: "Tiger", text: "权力和成就" }, { value: "Peacock", text: "认可和赞赏" }, { value: "Koala", text: "稳定和和谐" }, { value: "Owl", text: "准确和质量" }, { value: "Chameleon", text: "平衡和适应" }] },
{ id: 4, question: "处理冲突时,您倾向于:", options: [{ value: "Tiger", text: "直接解决" }, { value: "Peacock", text: "调解双方" }, { value: "Koala", text: "避免冲突" }, { value: "Owl", text: "理性处理" }, { value: "Chameleon", text: "视情况而定" }] },
{ id: 5, question: "您的决策风格是:", options: [{ value: "Tiger", text: "果断迅速" }, { value: "Peacock", text: "直觉判断" }, { value: "Koala", text: "深思熟虑" }, { value: "Owl", text: "数据分析" }, { value: "Chameleon", text: "灵活决策" }] },
{ id: 6, question: "面对压力,您会:", options: [{ value: "Tiger", text: "更加强势" }, { value: "Peacock", text: "寻求支持" }, { value: "Koala", text: "保持冷静" }, { value: "Owl", text: "更加谨慎" }, { value: "Chameleon", text: "调整策略" }] },
{ id: 7, question: "您的领导风格是:", options: [{ value: "Tiger", text: "指挥型" }, { value: "Peacock", text: "激励型" }, { value: "Koala", text: "支持型" }, { value: "Owl", text: "专家型" }, { value: "Chameleon", text: "教练型" }] },
{ id: 8, question: "您处理细节的方式是:", options: [{ value: "Tiger", text: "关注大局" }, { value: "Peacock", text: "可能忽略" }, { value: "Koala", text: "认真对待" }, { value: "Owl", text: "极度重视" }, { value: "Chameleon", text: "视情况决定" }] },
{ id: 9, question: "您的沟通方式是:", options: [{ value: "Tiger", text: "直接简短" }, { value: "Peacock", text: "热情生动" }, { value: "Koala", text: "温和耐心" }, { value: "Owl", text: "逻辑清晰" }, { value: "Chameleon", text: "根据对象调整" }] },
{ id: 10, question: "您对变化的态度是:", options: [{ value: "Tiger", text: "主动推动" }, { value: "Peacock", text: "积极拥抱" }, { value: "Koala", text: "需要适应" }, { value: "Owl", text: "谨慎评估" }, { value: "Chameleon", text: "随机应变" }] },
{ id: 11, question: "您的时间管理风格是:", options: [{ value: "Tiger", text: "追求速度" }, { value: "Peacock", text: "灵活安排" }, { value: "Koala", text: "稳定执行" }, { value: "Owl", text: "精确规划" }, { value: "Chameleon", text: "根据情况调整" }] },
{ id: 12, question: "您被什么激励:", options: [{ value: "Tiger", text: "成就权力" }, { value: "Peacock", text: "认可赞赏" }, { value: "Koala", text: "安全归属" }, { value: "Owl", text: "正确标准" }, { value: "Chameleon", text: "多样平衡" }] },
{ id: 13, question: "您的学习方式是:", options: [{ value: "Tiger", text: "边做边学" }, { value: "Peacock", text: "互动讨论" }, { value: "Koala", text: "循序渐进" }, { value: "Owl", text: "深入研究" }, { value: "Chameleon", text: "多种结合" }] },
{ id: 14, question: "您对规则的态度是:", options: [{ value: "Tiger", text: "灵活打破" }, { value: "Peacock", text: "不拘一格" }, { value: "Koala", text: "遵守维护" }, { value: "Owl", text: "严格遵守" }, { value: "Chameleon", text: "灵活处理" }] },
{ id: 15, question: "您在团队中的角色是:", options: [{ value: "Tiger", text: "领导者" }, { value: "Peacock", text: "激励者" }, { value: "Koala", text: "协调者" }, { value: "Owl", text: "专家" }, { value: "Chameleon", text: "多面手" }] },
{ id: 16, question: "您的工作节奏是:", options: [{ value: "Tiger", text: "快节奏" }, { value: "Peacock", text: "充满活力" }, { value: "Koala", text: "稳定有序" }, { value: "Owl", text: "有条理" }, { value: "Chameleon", text: "灵活调整" }] },
{ id: 17, question: "您最大的优势是:", options: [{ value: "Tiger", text: "执行力" }, { value: "Peacock", text: "影响力" }, { value: "Koala", text: "可靠性" }, { value: "Owl", text: "准确性" }, { value: "Chameleon", text: "适应力" }] },
{ id: 18, question: "您的人际关系特点是:", options: [{ value: "Tiger", text: "目标导向" }, { value: "Peacock", text: "朋友众多" }, { value: "Koala", text: "关系稳定" }, { value: "Owl", text: "志同道合" }, { value: "Chameleon", text: "灵活建立" }] },
{ id: 19, question: "面对批评,您的反应是:", options: [{ value: "Tiger", text: "可能反驳" }, { value: "Peacock", text: "可能受伤" }, { value: "Koala", text: "接受思考" }, { value: "Owl", text: "分析合理性" }, { value: "Chameleon", text: "灵活调整" }] },
{ id: 20, question: "您的理想工作环境是:", options: [{ value: "Tiger", text: "充满挑战" }, { value: "Peacock", text: "互动频繁" }, { value: "Koala", text: "稳定和谐" }, { value: "Owl", text: "有序规范" }, { value: "Chameleon", text: "灵活多变" }] }
]
/**
* Fisher-Yates 洗牌:随机打乱题目顺序,同时随机打乱每题的选项顺序
* 不修改原数组,返回深拷贝后的新数组
* @param {Array} questions - 原题目数组
* @returns {Array} 打乱后的题目数组
*/
function shuffleQuestions(questions) {
// 深拷贝,避免污染原始数组
const arr = questions.map(q => ({
...q,
options: q.options.slice().sort(() => Math.random() - 0.5)
}))
// Fisher-Yates 打乱题目顺序
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]]
}
return arr
}
module.exports = {
mbtiQuestions,
discQuestions,
pdpQuestions,
shuffleQuestions
}

View File

@@ -0,0 +1,109 @@
/**
* 统一请求封装:自动拼接 baseURL、携带 token、处理 401
* 使用前需已执行 App(),否则 getApp() 在 require 时可能未就绪,这里在请求时再取 app
*/
function getAppSafe() {
try {
return getApp()
} catch (e) {
return null
}
}
function getToken() {
const app = getAppSafe()
return (app && app.globalData && app.globalData.token) || wx.getStorageSync('token') || ''
}
function getApiBase() {
const app = getAppSafe()
return (app && app.globalData && app.globalData.apiBase) || ''
}
/**
* 清除登录态401 或主动退出时调用)
*/
function clearLoginState() {
const app = getAppSafe()
if (app && app.globalData) {
app.globalData.token = null
app.globalData.userInfo = null
app.globalData.openId = null
}
try {
wx.removeStorageSync('token')
wx.removeStorageSync('userInfo')
} catch (e) {}
}
/**
* 发起请求
* @param {Object} options - 同 wx.requesturl 可为相对路径(自动加 apiBase
* @param {boolean} options.needAuth - 是否携带 Authorization默认 true
* @param {boolean} options.allow401 - 401 时是否静默清除登录态而不 fail默认 true
*/
function request(options) {
const apiBase = getApiBase()
const url = options.url
const fullUrl = url.startsWith('http') ? url : `${apiBase.replace(/\/$/, '')}${url.startsWith('/') ? '' : '/'}${url}`
const needAuth = options.needAuth !== false
const allow401 = options.allow401 !== false
const header = {
'Content-Type': 'application/json',
...(options.header || {})
}
if (needAuth) {
const token = getToken()
if (token) header['Authorization'] = `Bearer ${token}`
}
const success = options.success
const fail = options.fail
const complete = options.complete
return wx.request({
...options,
url: fullUrl,
header,
success(res) {
if (res.statusCode === 401 && allow401) {
clearLoginState()
}
if (success) success(res)
},
fail(err) {
if (fail) fail(err)
},
complete(res) {
if (complete) complete(res)
}
})
}
/**
* Promise 版 request便于 async/await
*/
function requestPromise(options) {
return new Promise((resolve, reject) => {
request({
...options,
success(res) {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res)
} else {
reject(new Error(res.data && res.data.message ? res.data.message : '请求失败'))
}
},
fail: reject
})
})
}
module.exports = {
request,
requestPromise,
getToken,
getApiBase,
clearLoginState
}

View File

@@ -0,0 +1,95 @@
/**
* 测试结果摘要格式化:类型 + 整数百分比(与管理后台一致)
*/
function toIntPercent(value) {
if (value == null) return 0
const n = typeof value === 'number' ? value : Number(value)
return Number.isFinite(n) ? Math.round(n) : 0
}
/**
* 根据测试类型和原始结果,生成带整数百分比的摘要文案
* @param {object} data - 单条测试结果mbtiResult / discResult / pdpResult
* @param {string} testType - 'mbti' | 'disc' | 'pdp'
* @returns {string}
*/
function formatTestSummary(data, testType) {
if (!data || typeof data !== 'object') return ''
const t = (testType || '').toLowerCase()
if (t === 'mbti') {
const label = data.mbtiType || data.type || data.result || ''
const dims = data.dimensionScores
if (dims && typeof dims === 'object') {
const parts = []
const order = ['EI', 'SN', 'TF', 'JP']
for (const key of order) {
const pct = dims[key] && (dims[key].percentage != null ? dims[key].percentage : dims[key].dominant)
if (pct != null) parts.push(toIntPercent(pct) + '%')
}
if (parts.length) return label + ' (' + parts.join(' ') + ')'
}
return String(label)
}
if (t === 'disc') {
const desc = data.description && data.description.type
const label = (typeof desc === 'string' && desc) ? desc : ((data.dominantType ? data.dominantType + '型' : '') || (data.disc || ''))
const pct = data.percentages
if (pct && typeof pct === 'object') {
const d = toIntPercent(pct.D != null ? pct.D : pct.d)
const i = toIntPercent(pct.I != null ? pct.I : pct.i)
const s = toIntPercent(pct.S != null ? pct.S : pct.s)
const c = toIntPercent(pct.C != null ? pct.C : pct.c)
return label + ' D:' + d + '% I:' + i + '% S:' + s + '% C:' + c + '%'
}
return label || ''
}
if (t === 'pdp') {
const desc = data.description && data.description.type
const label = (typeof desc === 'string' && desc) ? desc : (data.dominantType || data.pdp || '')
const pct = data.percentages
if (pct && typeof pct === 'object') {
const names = { Tiger: '老虎', Peacock: '孔雀', Owl: '猫头鹰', Koala: '考拉', Chameleon: '变色龙' }
const parts = []
for (const [key, name] of Object.entries(names)) {
const v = pct[key] != null ? pct[key] : pct[key.toLowerCase()]
if (v != null) parts.push(name + ':' + toIntPercent(v) + '%')
}
if (parts.length) return label + ' ' + parts.join(' ')
}
return label || ''
}
return ''
}
/**
* 仅返回类型标签(无百分比),用于列表、个人中心等
*/
function getTypeOnly(data, testType) {
if (!data || typeof data !== 'object') return ''
const t = (testType || '').toLowerCase()
if (t === 'mbti') return String(data.mbtiType ?? data.type ?? data.result ?? '')
if (t === 'disc') {
const desc = data.description?.type
if (typeof desc === 'string' && desc) return desc
if (data.dominantType) return String(data.dominantType) + '型'
return String(data.disc ?? '')
}
if (t === 'pdp') {
const desc = data.description?.type
if (typeof desc === 'string' && desc) return desc
if (data.dominantType) return String(data.dominantType)
return String(data.pdp ?? '')
}
return ''
}
module.exports = {
toIntPercent,
formatTestSummary,
getTypeOnly
}

View File

@@ -0,0 +1,60 @@
/**
* 分享参数工具
* - uid当前登录用户 ID始终携带用于邀请统计
* - eid企业 ID仅 appScope=enterprise 时携带)
*
* 小程序好友分享onShareAppMessagepath 带完整查询串
* 朋友圈分享onShareTimeline返回 query 字符串(不含 ?
* 接收方(落地页 onLoad options直接读 options.uid / options.eid
*/
function getApp_() {
try { return getApp() } catch (e) { return null }
}
/**
* 构建分享查询串,例如 "uid=1&eid=6" 或 "uid=1"
*/
function buildShareQuery() {
try {
const app = getApp_()
const userInfo = (app && app.globalData && app.globalData.userInfo) || wx.getStorageSync('userInfo') || {}
const scope = (app && app.globalData && app.globalData.appScope) || 'personal'
const uid = userInfo.id || ''
const parts = []
if (uid) parts.push('uid=' + uid)
if (scope === 'enterprise') {
const eid = (app && app.globalData && app.globalData.enterpriseIdFromScene)
|| userInfo.enterpriseId
|| ''
if (eid) parts.push('eid=' + eid)
}
return parts.join('&')
} catch (e) {
return ''
}
}
/**
* 返回带查询参数的完整落地页路径
* @param {string} basePath 例如 '/pages/index/index'
* @param {boolean} forceEnterprise 强制使用企业版落地页(传 true 时 basePath 无效)
*/
function getSharePath(basePath) {
const query = buildShareQuery()
return query ? basePath + '?' + query : basePath
}
/**
* 根据当前 scope 决定落地页路径
* - 企业版 → /pages/enterprise/index?uid=X&eid=Y
* - 个人版 → basePath?uid=X basePath 默认 /pages/index/index
*/
function getSharePathByScope(personalBasePath) {
const app = getApp_()
const scope = (app && app.globalData && app.globalData.appScope) || 'personal'
const base = scope === 'enterprise' ? '/pages/enterprise/index' : (personalBasePath || '/pages/index/index')
return getSharePath(base)
}
module.exports = { buildShareQuery, getSharePath, getSharePathByScope }