Files
qitongxue-wx/src/utils/member-pay.ts
2026-09-17 17:39:06 +08:00

80 lines
2.9 KiB
TypeScript
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.
import type { MemberState } from '@/api/member'
import { apiMemberOrder, apiMemberOrderCheck } from '@/api/member'
import { checkPaySupport, requestVirtualPayment, VirtualPayError } from '@/utils/vpay'
/**
* 会员购买闭环编排(会员中心页与「功能未解锁」引导共用)
*
* 流程:终端能力校验 → wx.login 取 code → 服务端下单(返回签名后的 payData)
* → 拉起 wx.requestVirtualPayment → 轮询自己服务器的订单状态确认发货
*
* 注意:平台 success 回调可能丢失,绝不能据此发货;发货由服务端「发货推送 + query_order 查单兜底」
* 完成,前端查询接口本身也会触发一次兜底查单,所以这里轮询几次即可拿到结果。
*/
export interface BuyMembershipResult {
delivered: boolean
state?: MemberState
}
/** 轮询次数与间隔:发货推送通常在秒级到达 */
const POLL_TIMES = 5
const POLL_INTERVAL_MS = 1200
/** 前端支付流程交互超时(毫秒),用于防止 promise 悬挂 */
export const PAY_TIMEOUT_MS = 8000
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}
/** 取 wx.login 的临时凭证(服务端用它换 session_key 生成用户态签名) */
function fetchLoginCode(): Promise<string> {
return new Promise<string>((resolve, reject) => {
uni.login({
provider: 'weixin',
success: (res) => {
if (res.code)
resolve(res.code)
else
reject(new VirtualPayError('获取微信登录凭证失败,请稍后重试'))
},
fail: err => reject(new VirtualPayError(err.errMsg || '获取微信登录凭证失败')),
})
})
}
/**
* 购买会员套餐。抛出的错误统一为 VirtualPayError,message 可直接展示。
* delivered=false 表示支付流程走完但服务端尚未确认发货(少见,可稍后在订单页查看)。
*/
export async function buyMembership(planKey: string): Promise<BuyMembershipResult> {
const support = checkPaySupport()
if (!support.ok)
throw new VirtualPayError(support.reason || '当前环境不支持购买')
const code = await fetchLoginCode()
const payData = await apiMemberOrder(planKey, code)
await requestVirtualPayment({
signData: payData.signData,
paySig: payData.paySig,
signature: payData.signature,
mode: payData.mode,
})
let last: Awaited<ReturnType<typeof apiMemberOrderCheck>> | null = null
for (let i = 0; i < POLL_TIMES; i++) {
last = await apiMemberOrderCheck(payData.out_trade_no)
if (last.delivered)
return { delivered: true, state: last.state }
await sleep(POLL_INTERVAL_MS)
}
return { delivered: false, state: last?.state }
}
/** 分 → 元(保留两位,去掉无意义的小数位) */
export function fenToYuan(fen: number): string {
const yuan = fen / 100
return Number.isInteger(yuan) ? String(yuan) : yuan.toFixed(2)
}