diff --git a/src/apis.js b/src/apis.js index 5e38eb0..de645e1 100644 --- a/src/apis.js +++ b/src/apis.js @@ -366,6 +366,17 @@ export const createOrderAPI = (vipId) => { }); }; +export const virtualPayOrderAPI = (vipId = 0, code = "") => { + return request("POST", "/user/virtualPay/createOrder", { + vipId, + code, + }); +}; + +export const getOrderDetailAPI = (orderId) => { + return request("GET", `/user/order/detail?orderId=${encodeURIComponent(orderId)}`); +}; + export const payOrderAPI = (id) => { return request("POST", "/user/order/pay", { id, diff --git a/src/components/ScoreResult.vue b/src/components/ScoreResult.vue index 3464483..c08381f 100644 --- a/src/components/ScoreResult.vue +++ b/src/components/ScoreResult.vue @@ -143,7 +143,7 @@ const openCoachComment = () => { }}环的成绩,所有箭支上靶后的平均点间距为{{ Number((result.average_distance || 0).toFixed(2)) }}{{ Number((result?.interpretation?.spreadStability || 0).toFixed(2)) }},{{ result.spreadEvaluation === "Dispersed" ? "还需要持续改进哦~" diff --git a/src/pages/first-try.vue b/src/pages/first-try.vue index cc40e37..9eef54c 100644 --- a/src/pages/first-try.vue +++ b/src/pages/first-try.vue @@ -29,6 +29,7 @@ import { storeToRefs } from "pinia"; const store = useStore(); const { user } = storeToRefs(store); const scores = ref([]); +const isSvip = ref(false); const step = ref(0); const total = 12; const stepButtonTexts = [ @@ -114,6 +115,7 @@ const onOver = async () => { async function onReceiveMessage(msg) { if (msg.type === MESSAGETYPESV2.ShootResult) { + isSvip.value = msg.sVip === true; scores.value = msg.details; } else if (msg.type === MESSAGETYPESV2.BattleEnd) { setTimeout(onOver, 1500); @@ -204,6 +206,7 @@ const nextStep = async () => { title.value = "小试牛刀"; await startPractiseAPI(); scores.value = []; + isSvip.value = false; step.value = 5; start.value = true; setTimeout(() => { @@ -230,6 +233,7 @@ const onClose = async () => { practiseResult.value = {}; start.value = false; scores.value = []; + isSvip.value = false; step.value = 4; const result = await createPractiseAPI(total, 120); if (result) practiseId.value = result.id; @@ -343,6 +347,7 @@ const onClose = async () => { :currentRound="step === 5 ? scores.length : 0" :totalRound="step === 5 ? total : 0" :scores="scores" + :isSvip="isSvip" /> -import { computed, ref, onBeforeUnmount } from "vue"; +import { computed, ref } from "vue"; import { onShow } from "@dcloudio/uni-app"; import Container from "@/components/Container.vue"; import Signin from "@/components/Signin.vue"; -import { createOrderAPI, getAppConfig, getHomeData } from "@/apis"; -import { capsuleHeight } from "@/util"; +import { virtualPayOrderAPI, getAppConfig, getHomeData, getOrderDetailAPI } from "@/apis"; +import { capsuleHeight, wxLogin } from "@/util"; import useStore from "@/store"; import { storeToRefs } from "pinia"; @@ -18,9 +18,6 @@ const showModal = ref(false); const loadingConfig = ref(false); const paying = ref(false); const refreshing = ref(false); -const timer = ref(null); -const lastDate = ref(user.value.expiredAt || 0); -const maxRefreshTimes = 12; // 会员页核心展示数据:视觉、权益、套餐均按蓝湖当前两张设计稿拆分。 const memberTypes = [ @@ -100,16 +97,6 @@ const getVipExpiredValue = (type, source = user.value) => { return type.key === "super" ? source.superVipExpiredAt : source.normalVipExpiredAt; }; -// 支付后轮询需要比较最新会员到期时间,兼容旧字段 expiredAt。 -const getLatestVipExpiredTime = (source = user.value) => { - if (!source) return 0; - return Math.max( - toTimestamp(source.normalVipExpiredAt), - toTimestamp(source.superVipExpiredAt), - toTimestamp(source.expiredAt) - ); -}; - // 未过期才展示“会员生效中”样式,已过期或无值继续展示未开通样式。 const isVipActive = (type) => { return toTimestamp(getVipExpiredValue(type)) > Date.now(); @@ -263,41 +250,38 @@ const loadVipConfig = async () => { } }; -const clearRefreshTimer = () => { - if (timer.value) { - clearInterval(timer.value); - timer.value = null; +const refreshUserAfterPay = async () => { + refreshing.value = true; + try { + const result = await getHomeData(); + if (result.user) { + updateUser(result.user); + } + } catch (error) { + console.log("refresh user after pay error", error); + } finally { + refreshing.value = false; } }; -const refreshUserAfterPay = () => { - clearRefreshTimer(); - let refreshTimes = 0; - // 先记录支付前的最大到期时间,轮询到更大的值就认为会员状态已刷新。 - lastDate.value = getLatestVipExpiredTime(); - refreshing.value = true; - timer.value = setInterval(async () => { - refreshTimes += 1; - try { - const result = await getHomeData(); - const latestExpiredAt = getLatestVipExpiredTime(result.user); - if (result.user && latestExpiredAt > lastDate.value) { - lastDate.value = latestExpiredAt; - updateUser(result.user); - clearRefreshTimer(); - refreshing.value = false; - } else if (refreshTimes >= maxRefreshTimes) { - clearRefreshTimer(); - refreshing.value = false; - } - } catch (error) { - console.log("refresh user after pay error", error); - if (refreshTimes >= maxRefreshTimes) { - clearRefreshTimer(); - refreshing.value = false; - } - } - }, 1000); +const getVirtualPaySignData = (result) => { + // 后端可能返回字符串或对象,这里统一转成微信虚拟支付需要的 JSON 字符串。 + const signDataObj = typeof result?.signData === "string" ? JSON.parse(result.signData) : result?.signData; + if (!signDataObj) return ""; + + // 微信虚拟支付短剧/虚拟商品模式需要购买数量;后端未返回时使用订单数量兜底。 + if (!signDataObj.buyQuantity) { + signDataObj.buyQuantity = result.quantity || 1; + } + return JSON.stringify(signDataObj); +}; + +const isVirtualPayCancel = (res = {}) => { + const message = String(res.errMsg || res.message || "").toLowerCase(); + const errCode = Number(res.errCode); + const errno = Number(res.errno); + + return message.includes("cancel") || errCode === -23 || errno === -2; }; const onPay = async () => { @@ -317,12 +301,22 @@ const onPay = async () => { }); return; } + if (typeof wx === "undefined" || !wx.requestVirtualPayment) { + uni.showToast({ + title: "当前环境不支持微信虚拟支付", + icon: "none", + }); + return; + } paying.value = true; let waitingPayment = false; + let payToast = null; try { - const result = await createOrderAPI(vipId); - const params = result?.pay?.order?.jsApi?.params; - if (!params?.timeStamp || !params?.nonceStr || !params?.package || !params?.paySign) { + // 微信虚拟支付创建订单前需要登录 code,服务端用它换取本次支付签名参数。 + const wxResult = await wxLogin(); + const result = await virtualPayOrderAPI(vipId, wxResult.code); + const finalSignData = getVirtualPaySignData(result); + if (!finalSignData || !result?.paySig || !result?.signature) { uni.showToast({ title: "支付参数生成失败", icon: "none", @@ -330,34 +324,57 @@ const onPay = async () => { refreshing.value = false; return; } - waitingPayment = true; - wx.requestPayment({ - timeStamp: params.timeStamp, // 微信支付时间戳 - nonceStr: params.nonceStr, // 微信支付随机串 - package: params.package, // 预支付交易会话标识 - paySign: params.paySign, // 微信支付签名 - signType: params.signType || "RSA", - success() { - uni.showToast({ + wx.requestVirtualPayment({ + signData: finalSignData, + paySig: result.paySig, + signature: result.signature, + mode: "short_series_goods", + async success() { + payToast = { title: "支付成功", - icon: "none", - }); + icon: "success", + }; + if (result?.outTradeNo) { + try { + const orderDetail = await getOrderDetailAPI(result.outTradeNo); + console.log("virtual pay order detail", orderDetail); + } catch (error) { + console.log("virtual pay order detail error", error); + } + } + // 客户端支付成功后,会员是否真正生效仍以服务端用户信息刷新结果为准。 refreshUserAfterPay(); }, fail(res) { - console.log("pay error", res); - if (res.errMsg && res.errMsg.indexOf("cancel") !== -1) return; - uni.showToast({ - title: "支付失败,请稍后重试", + console.log("virtual pay error", res); + if (isVirtualPayCancel(res)) { + payToast = { + title: "支付已取消", + icon: "none", + }; + return; + } + payToast = { + title: res.message || "支付失败,请稍后重试", icon: "none", - }); + }; }, complete() { paying.value = false; + if (payToast) { + setTimeout(() => { + uni.showToast(payToast); + }, 200); + } }, }); + waitingPayment = true; } catch (error) { - console.log("create vip order error", error); + console.log("create virtual pay order error", error); + uni.showToast({ + title: error.message || "下单失败", + icon: "none", + }); } finally { if (!waitingPayment) paying.value = false; } @@ -365,9 +382,6 @@ const onPay = async () => { onShow(loadVipConfig); -onBeforeUnmount(() => { - clearRefreshTimer(); -});