80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
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)
|
||
}
|