Files
zhiyin/zhiyin-app/src/composables/useGravityPurchase.ts
T
yuzhiran 52e5350ba0 补充 VP 模块注册 + 前端 API 封装
- app.module.ts: 注册 VirtualPaymentModule
- config.ts + api.ts: 添加虚拟支付 API 端点
- useGravityPurchase.ts: VP 购买逻辑
- interview.vue / user.vue: 适配 VP 调用
- admin.controller.ts: 管理端虚拟支付相关调整
- payment.controller.spec.ts: 测试适配
2026-06-24 10:35:25 +08:00

204 lines
6.9 KiB
TypeScript

import { ref, computed, nextTick } from 'vue'
import { api } from '../config'
export function useGravityPurchase(onPaymentSuccess?: () => void) {
const showQuantityModal = ref(false)
const buyQuantity = ref(1)
const products = ref<any[]>([])
const buyProductType = ref('interview')
const payLoading = ref(false)
const payError = ref('')
const showPayModal = ref(false)
const payCodeUrl = ref('')
const currentOutTradeNo = ref('')
const isMp = ref(false)
const paySuccess = ref(false)
const unitPrice = computed(() => {
const p = products.value.find(p => p.type === buyProductType.value)
return p?.price || 0
})
const totalPrice = computed(() => unitPrice.value * buyQuantity.value / 100)
const buyGravityPerUnit = computed(() => {
const p = products.value.find(p => p.type === buyProductType.value)
return p?.gravity || 0
})
const changeQty = (delta: number) => {
const next = buyQuantity.value + delta
if (next >= 1 && next <= 99) buyQuantity.value = next
}
const clampQty = () => {
if (buyQuantity.value < 1) buyQuantity.value = 1
if (buyQuantity.value > 99) buyQuantity.value = 99
}
const loadProducts = async () => {
try {
const res = await uni.request({ url: api('/member/plans'), method: 'GET' })
if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.products) {
const prodList: any[] = []
for (const [key, val] of Object.entries(res.data.products as Record<string, any>)) {
if (val?.price > 0) prodList.push({ type: key, ...val })
}
products.value = prodList
}
} catch (e) { /* silent */ }
}
const openGravityPurchase = async (productType = 'interview') => {
buyProductType.value = productType
buyQuantity.value = 1
// #ifdef MP-WEIXIN
isMp.value = true
// #endif
// 加载产品信息
await loadProducts()
showQuantityModal.value = true
}
const cancelPay = () => {
showPayModal.value = false
payCodeUrl.value = ''
payLoading.value = false
payError.value = ''
}
const confirmProductBuy = () => {
showQuantityModal.value = false
startProductPay(buyProductType.value, buyQuantity.value)
}
const startProductPay = async (type: string, quantity = 1) => {
const token = uni.getStorageSync('token') || ''
if (!token) {
uni.showToast({ title: '请先登录', icon: 'none' })
return
}
showPayModal.value = true
payLoading.value = true
payError.value = ''
if (isMp.value) {
try {
const loginRes = await new Promise<{ code: string }>((resolve, reject) => {
uni.login({ provider: 'weixin', success: r => resolve(r), fail: reject })
})
if (!loginRes.code) {
payLoading.value = false
payError.value = '微信登录失败,请重试'
uni.showToast({ title: '微信登录失败', icon: 'none' })
return
}
const res = await uni.request({
url: api('/virtual-payment/create'), method: 'POST',
data: { type, quantity, wxCode: loginRes.code },
header: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
timeout: 30000,
})
payLoading.value = false
if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.paySig) {
const vp = res.data as any
currentOutTradeNo.value = vp.outTradeNo
;(wx as any).requestVirtualPayment({
env: vp.env,
offerId: vp.offerId,
signData: vp.signData,
paySig: vp.paySig,
signature: vp.signature,
success: () => {
const no = currentOutTradeNo.value || vp.outTradeNo
pollPayResult(no)
},
fail: (err: any) => {
payError.value = '支付未完成'
uni.showToast({ title: err?.errMsg || '支付未完成', icon: 'none' })
},
})
} else if (!res.statusCode || res.statusCode === 0) {
payError.value = '网络连接失败,请检查网络后重试'
uni.showToast({ title: '网络连接失败', icon: 'none' })
} else {
const errMsg = res.data?.message || '购买失败'
payError.value = errMsg
uni.showToast({ title: errMsg, icon: 'none' })
}
} catch (e: any) {
payLoading.value = false
payError.value = e?.errMsg || '网络错误,请重试'
uni.showToast({ title: '网络错误', icon: 'none' })
}
} else {
try {
const res = await uni.request({
url: api('/payment/create-product'), method: 'POST',
data: { type, quantity },
header: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
})
payLoading.value = false
if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.codeUrl) {
payCodeUrl.value = res.data.codeUrl
currentOutTradeNo.value = res.data.outTradeNo
// 页面需要自己渲染二维码(依赖 uqrcode)
pollPayResult(res.data.outTradeNo)
} else if (!res.statusCode || res.statusCode === 0) {
payError.value = '网络连接失败,请检查网络后重试'
uni.showToast({ title: '网络连接失败', icon: 'none' })
} else {
payError.value = res.data?.message || '购买失败'
uni.showToast({ title: res.data?.message || '购买失败', icon: 'none' })
}
} catch (e) {
payLoading.value = false
payError.value = '网络错误,请重试'
uni.showToast({ title: '网络错误', icon: 'none' })
}
}
}
const pollPayResult = async (outTradeNo: string) => {
if (!outTradeNo) return
const maxAttempts = 30
let attempts = 0
const token = uni.getStorageSync('token') || ''
const poll = async () => {
attempts++
try {
const res = await uni.request({
url: api(`/payment/check/${outTradeNo}`), method: 'GET',
header: { 'Authorization': `Bearer ${token}` },
})
if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.status === 'success') {
paySuccess.value = true
showPayModal.value = false
uni.showToast({ title: '充值成功!', icon: 'success' })
onPaymentSuccess?.()
return
}
} catch (e) { /* ignore */ }
if (attempts < maxAttempts) {
setTimeout(poll, 2000)
} else {
payError.value = '支付结果查询超时,请联系客服'
uni.showToast({ title: '支付查询超时', icon: 'none' })
}
}
setTimeout(poll, 2000)
}
return {
showQuantityModal, buyQuantity, products, buyProductType,
unitPrice, totalPrice, buyGravityPerUnit,
payLoading, payError, showPayModal, payCodeUrl, paySuccess,
isMp,
changeQty, clampQty, loadProducts, openGravityPurchase,
confirmProductBuy, cancelPay,
}
}