小程序提交
This commit is contained in:
44
miniprogram/utils/descriptions.js
Normal file
44
miniprogram/utils/descriptions.js
Normal 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
|
||||
}
|
||||
628
miniprogram/utils/payment.js
Normal file
628
miniprogram/utils/payment.js
Normal 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
|
||||
}
|
||||
113
miniprogram/utils/phoneAuth.js
Normal file
113
miniprogram/utils/phoneAuth.js
Normal 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,
|
||||
}
|
||||
117
miniprogram/utils/questions.js
Normal file
117
miniprogram/utils/questions.js
Normal 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
|
||||
}
|
||||
109
miniprogram/utils/request.js
Normal file
109
miniprogram/utils/request.js
Normal 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.request,url 可为相对路径(自动加 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
|
||||
}
|
||||
95
miniprogram/utils/resultFormat.js
Normal file
95
miniprogram/utils/resultFormat.js
Normal 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
|
||||
}
|
||||
60
miniprogram/utils/share.js
Normal file
60
miniprogram/utils/share.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 分享参数工具
|
||||
* - uid:当前登录用户 ID(始终携带,用于邀请统计)
|
||||
* - eid:企业 ID(仅 appScope=enterprise 时携带)
|
||||
*
|
||||
* 小程序好友分享(onShareAppMessage):path 带完整查询串
|
||||
* 朋友圈分享(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 }
|
||||
Reference in New Issue
Block a user