Files
MBTI_wang/douyin-miniprogram/utils/payment.js
卡若 aca2e263bb feat: 管理端聚合页、小程序/抖音埋点与统计、飞书线索 webhook、API 迁移与路由
- admin:OrdersHub/UsersHub、Commerce/Ops/Enterprise Hub、MpAnalytics、Feishu/小程序配置面板、鉴权存储
- api:Analytics、DataMigration、FeishuLeadWebhook、mp 事件迁移 SQL
- 微信/抖音小程序:analytics 上报与相关页面调整
- 开发文档与 scripts 补充

Made-with: Cursor
2026-03-27 17:22:04 +08:00

547 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// douyin-miniprogram/utils/payment.js
// 抖音支付工具类 - 从微信版移植
// 后端需实现 /api/payment/create 返回 { order_id, order_token } 用于 tt.pay
const app = getApp()
/**
* 生成订单号
* 规则:前缀+时间戳+随机数最长64位抖音限制比微信宽松
*/
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())
const rand = pad(Math.floor(Math.random() * 1000), 3)
const raw = `${prefix}${dateStr}${rand}`
return raw.length > 64 ? raw.slice(0, 64) : raw
}
/**
* 发起抖音支付
* 流程:后端创建订单 → 返回 order_id + order_token → tt.pay 调起收银台
* 后端需实现 /api/payment/create 接口,当 paymentMethod='douyin' 时返回:
* { order_id: 'xxx', order_token: 'xxx' }
*/
function douyinPay(options) {
const { orderId, amount = 0, description, productType, testResultId, deepProductId, enterpriseId, success, fail } = options
try {
const analyticsMod = require('./analytics')
if (analyticsMod && typeof analyticsMod.track === 'function') {
if (productType === 'recharge') {
analyticsMod.track('click_recharge', { action: '点击充值并发起支付', productType: 'recharge' })
} else {
analyticsMod.track('click_pay', { action: '发起支付', productType: productType || '' })
}
if (typeof analyticsMod.flush === 'function') {
analyticsMod.flush()
}
}
} catch (e) {}
tt.showLoading({
title: '正在支付...',
mask: true
})
tt.request({
url: `${app.globalData.apiBase}/api/payment/create`,
method: 'POST',
header: {
'Authorization': `Bearer ${tt.getStorageSync('token')}`,
'Content-Type': 'application/json'
},
data: {
orderId,
amount,
description,
productType,
paymentMethod: 'douyin',
openId: app.globalData.openId || '',
enterpriseId: enterpriseId || 0,
testResultId: testResultId || 0,
deepProductId: deepProductId || ''
},
success: (res) => {
tt.hideLoading()
if (res.statusCode === 200 && res.data.code === 200) {
const paymentData = res.data.data
tt.pay({
orderInfo: {
order_id: paymentData.order_id,
order_token: paymentData.order_token
},
service: 5,
success: (payRes) => {
console.log('抖音支付回调', payRes)
if (payRes.code === 0) {
pollOrderStatus(orderId, 5, 1000, (ok, order) => {
if (ok) {
tt.showToast({
title: '支付成功',
icon: 'success',
duration: 2000
})
success && success({ payRes, order })
} else {
tt.showToast({
title: '支付结果处理中,请稍后查看',
icon: 'none',
duration: 2500
})
success && success({ payRes, order: null })
}
})
} else if (payRes.code === 4) {
tt.showToast({
title: '支付已取消',
icon: 'none'
})
fail && fail(payRes)
} else if (payRes.code === 9) {
pollOrderStatus(orderId, 5, 1500, (ok, order) => {
if (ok) {
tt.showToast({ title: '支付成功', icon: 'success', duration: 2000 })
success && success({ payRes, order })
} else {
tt.showToast({ title: '支付结果待确认,请稍后查看', icon: 'none', duration: 2500 })
fail && fail(payRes)
}
})
} else {
tt.showToast({
title: '支付失败',
icon: 'none'
})
fail && fail(payRes)
}
},
fail: (payErr) => {
console.error('抖音支付失败', payErr)
tt.showToast({
title: '支付失败',
icon: 'none'
})
fail && fail(payErr)
}
})
} else {
tt.showToast({
title: res.data.message || '创建订单失败',
icon: 'none'
})
fail && fail(res)
}
},
fail: (err) => {
tt.hideLoading()
console.error('请求失败', err)
tt.showToast({
title: '网络请求失败',
icon: 'none'
})
fail && fail(err)
}
})
}
function notifyPaymentSuccess(orderId, prepayId) {
tt.request({
url: `${app.globalData.apiBase}/api/payment/notify`,
method: 'POST',
header: {
'Authorization': `Bearer ${tt.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) {
tt.request({
url: `${app.globalData.apiBase}/api/payment/query`,
method: 'GET',
header: {
'Authorization': `Bearer ${tt.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)
}
})
}
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()
}
function purchaseVIP(vipType, success, fail) {
const prices = {
month: 1990,
quarter: 4990,
year: 9900,
lifetime: 19900,
personal_insight: 19800
}
const names = {
month: '月度VIP会员',
quarter: '季度VIP会员',
year: '年度VIP会员',
lifetime: '终身VIP会员',
personal_insight: '个人深度洞察版'
}
const orderId = generateOrderId('vip')
douyinPay({
orderId,
amount: prices[vipType],
description: `MBTI性格测试 - ${names[vipType]}`,
productType: 'vip',
success: (res) => {
updateVIPStatus(vipType)
success && success(res)
},
fail
})
}
function purchaseTestCount(count, success, fail) {
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')
douyinPay({
orderId,
amount: price,
description: `MBTI性格测试 - ${count}次测试次数`,
productType: 'test_count',
success: (res) => {
addTestCount(count)
success && success(res)
},
fail
})
}
function purchaseSingleTest(testType, success, fail) {
const prices = {
mbti: 990,
disc: 690,
pdp: 690,
ai: 1990
}
const names = {
mbti: 'MBTI性格测试',
disc: 'DISC行为风格测试',
pdp: 'PDP动物性格测试',
ai: '性格分析'
}
const orderId = generateOrderId(`single_${testType}`)
douyinPay({
orderId,
amount: prices[testType],
description: names[testType],
productType: 'single_test',
success: (res) => {
unlockTest(testType)
success && success(res)
},
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)
douyinPay({
orderId,
amount: 0,
description,
productType,
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', '性格分析完整报告', { 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 })
}
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 })
}
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 })
}
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 })
}
function purchaseFullReport(success, fail) {
purchaseByPricing('report', '完整人格与职业发展报告', success, fail)
}
function purchaseTeamAnalysis(success, fail) {
purchaseByPricing('team_analysis', '团队性格组合与冲突分析服务', 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')
douyinPay({
orderId,
amount,
description: `账户充值 ¥${safeAmount.toFixed(2)}`,
productType: 'recharge',
enterpriseId: opts.enterpriseId || 0,
success: opts.success,
fail: opts.fail
})
}
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')
douyinPay({
orderId,
amount: 0,
description: desc,
productType: 'deep_personal',
deepProductId,
success,
fail
})
}
function purchaseTeamDeepService(success, fail) {
const orderId = generateOrderId('deep_team')
douyinPay({
orderId,
amount: 0,
description: '团队深度服务(团队画像+策略)',
productType: 'deep_team',
success,
fail
})
}
function updateVIPStatus(vipType) {
const durations = {
month: 30,
quarter: 90,
year: 365,
lifetime: 36500
}
const expireDate = new Date()
expireDate.setDate(expireDate.getDate() + durations[vipType])
const vipInfo = {
isVIP: true,
vipType,
expireDate: expireDate.toISOString(),
purchaseDate: new Date().toISOString()
}
tt.setStorageSync('vipInfo', vipInfo)
app.globalData.vipInfo = vipInfo
}
function addTestCount(count) {
const currentCount = tt.getStorageSync('testCount') || 0
const newCount = currentCount + count
tt.setStorageSync('testCount', newCount)
app.globalData.testCount = newCount
}
function unlockTest(testType) {
const unlockedTests = tt.getStorageSync('unlockedTests') || []
if (!unlockedTests.includes(testType)) {
unlockedTests.push(testType)
tt.setStorageSync('unlockedTests', unlockedTests)
}
app.globalData.unlockedTests = unlockedTests
}
function checkVIP() {
const vipInfo = tt.getStorageSync('vipInfo')
if (!vipInfo || !vipInfo.isVIP) return false
const expireDate = new Date(vipInfo.expireDate)
return expireDate > new Date()
}
function canTakeTest(testType) {
return true
}
function consumeTestCount() {
const testCount = tt.getStorageSync('testCount') || 0
if (testCount > 0) {
tt.setStorageSync('testCount', testCount - 1)
app.globalData.testCount = testCount - 1
return true
}
return false
}
function getUserBenefits() {
return {
isVIP: checkVIP(),
vipInfo: tt.getStorageSync('vipInfo') || null,
testCount: tt.getStorageSync('testCount') || 0,
unlockedTests: tt.getStorageSync('unlockedTests') || []
}
}
module.exports = {
douyinPay,
queryOrderStatus,
pollOrderStatus,
generateOrderId,
purchaseVIP,
purchaseTestCount,
purchaseSingleTest,
purchaseFaceTest,
purchaseMbtiTest,
purchaseDiscTest,
purchasePdpTest,
purchaseResumeAnalysis,
purchaseFullReport,
purchaseTeamAnalysis,
recharge,
purchasePersonalDeepService,
purchaseTeamDeepService,
checkVIP,
canTakeTest,
consumeTestCount,
getUserBenefits,
updateVIPStatus,
addTestCount,
unlockTest
}