Merge branch 'feat-vip' into test

This commit is contained in:
2026-07-03 13:56:06 +08:00
6 changed files with 107 additions and 79 deletions
+11
View File
@@ -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) => { export const payOrderAPI = (id) => {
return request("POST", "/user/order/pay", { return request("POST", "/user/order/pay", {
id, id,
+1 -1
View File
@@ -143,7 +143,7 @@ const openCoachComment = () => {
}}</text }}</text
>环的成绩所有箭支上靶后的平均点间距为<text >环的成绩所有箭支上靶后的平均点间距为<text
:style="{ color: '#fed847' }" :style="{ color: '#fed847' }"
>{{ Number((result.average_distance || 0).toFixed(2)) }}</text >{{ Number((result?.interpretation?.spreadStability || 0).toFixed(2)) }}</text
>{{ >{{
result.spreadEvaluation === "Dispersed" result.spreadEvaluation === "Dispersed"
? "还需要持续改进哦~" ? "还需要持续改进哦~"
+5
View File
@@ -29,6 +29,7 @@ import { storeToRefs } from "pinia";
const store = useStore(); const store = useStore();
const { user } = storeToRefs(store); const { user } = storeToRefs(store);
const scores = ref([]); const scores = ref([]);
const isSvip = ref(false);
const step = ref(0); const step = ref(0);
const total = 12; const total = 12;
const stepButtonTexts = [ const stepButtonTexts = [
@@ -114,6 +115,7 @@ const onOver = async () => {
async function onReceiveMessage(msg) { async function onReceiveMessage(msg) {
if (msg.type === MESSAGETYPESV2.ShootResult) { if (msg.type === MESSAGETYPESV2.ShootResult) {
isSvip.value = msg.sVip === true;
scores.value = msg.details; scores.value = msg.details;
} else if (msg.type === MESSAGETYPESV2.BattleEnd) { } else if (msg.type === MESSAGETYPESV2.BattleEnd) {
setTimeout(onOver, 1500); setTimeout(onOver, 1500);
@@ -204,6 +206,7 @@ const nextStep = async () => {
title.value = "小试牛刀"; title.value = "小试牛刀";
await startPractiseAPI(); await startPractiseAPI();
scores.value = []; scores.value = [];
isSvip.value = false;
step.value = 5; step.value = 5;
start.value = true; start.value = true;
setTimeout(() => { setTimeout(() => {
@@ -230,6 +233,7 @@ const onClose = async () => {
practiseResult.value = {}; practiseResult.value = {};
start.value = false; start.value = false;
scores.value = []; scores.value = [];
isSvip.value = false;
step.value = 4; step.value = 4;
const result = await createPractiseAPI(total, 120); const result = await createPractiseAPI(total, 120);
if (result) practiseId.value = result.id; if (result) practiseId.value = result.id;
@@ -343,6 +347,7 @@ const onClose = async () => {
:currentRound="step === 5 ? scores.length : 0" :currentRound="step === 5 ? scores.length : 0"
:totalRound="step === 5 ? total : 0" :totalRound="step === 5 ? total : 0"
:scores="scores" :scores="scores"
:isSvip="isSvip"
/> />
<ScorePanel <ScorePanel
v-if="step === 5" v-if="step === 5"
+78 -64
View File
@@ -1,10 +1,10 @@
<script setup> <script setup>
import { computed, ref, onBeforeUnmount } from "vue"; import { computed, ref } from "vue";
import { onShow } from "@dcloudio/uni-app"; import { onShow } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue"; import Container from "@/components/Container.vue";
import Signin from "@/components/Signin.vue"; import Signin from "@/components/Signin.vue";
import { createOrderAPI, getAppConfig, getHomeData } from "@/apis"; import { virtualPayOrderAPI, getAppConfig, getHomeData, getOrderDetailAPI } from "@/apis";
import { capsuleHeight } from "@/util"; import { capsuleHeight, wxLogin } from "@/util";
import useStore from "@/store"; import useStore from "@/store";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
@@ -18,9 +18,6 @@ const showModal = ref(false);
const loadingConfig = ref(false); const loadingConfig = ref(false);
const paying = ref(false); const paying = ref(false);
const refreshing = ref(false); const refreshing = ref(false);
const timer = ref(null);
const lastDate = ref(user.value.expiredAt || 0);
const maxRefreshTimes = 12;
// 会员页核心展示数据:视觉、权益、套餐均按蓝湖当前两张设计稿拆分。 // 会员页核心展示数据:视觉、权益、套餐均按蓝湖当前两张设计稿拆分。
const memberTypes = [ const memberTypes = [
@@ -100,16 +97,6 @@ const getVipExpiredValue = (type, source = user.value) => {
return type.key === "super" ? source.superVipExpiredAt : source.normalVipExpiredAt; 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) => { const isVipActive = (type) => {
return toTimestamp(getVipExpiredValue(type)) > Date.now(); return toTimestamp(getVipExpiredValue(type)) > Date.now();
@@ -263,41 +250,38 @@ const loadVipConfig = async () => {
} }
}; };
const clearRefreshTimer = () => { const refreshUserAfterPay = async () => {
if (timer.value) {
clearInterval(timer.value);
timer.value = null;
}
};
const refreshUserAfterPay = () => {
clearRefreshTimer();
let refreshTimes = 0;
// 先记录支付前的最大到期时间,轮询到更大的值就认为会员状态已刷新。
lastDate.value = getLatestVipExpiredTime();
refreshing.value = true; refreshing.value = true;
timer.value = setInterval(async () => {
refreshTimes += 1;
try { try {
const result = await getHomeData(); const result = await getHomeData();
const latestExpiredAt = getLatestVipExpiredTime(result.user); if (result.user) {
if (result.user && latestExpiredAt > lastDate.value) {
lastDate.value = latestExpiredAt;
updateUser(result.user); updateUser(result.user);
clearRefreshTimer();
refreshing.value = false;
} else if (refreshTimes >= maxRefreshTimes) {
clearRefreshTimer();
refreshing.value = false;
} }
} catch (error) { } catch (error) {
console.log("refresh user after pay error", error); console.log("refresh user after pay error", error);
if (refreshTimes >= maxRefreshTimes) { } finally {
clearRefreshTimer();
refreshing.value = false; refreshing.value = false;
} }
};
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;
} }
}, 1000); 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 () => { const onPay = async () => {
@@ -317,12 +301,22 @@ const onPay = async () => {
}); });
return; return;
} }
if (typeof wx === "undefined" || !wx.requestVirtualPayment) {
uni.showToast({
title: "当前环境不支持微信虚拟支付",
icon: "none",
});
return;
}
paying.value = true; paying.value = true;
let waitingPayment = false; let waitingPayment = false;
let payToast = null;
try { try {
const result = await createOrderAPI(vipId); // 微信虚拟支付创建订单前需要登录 code,服务端用它换取本次支付签名参数。
const params = result?.pay?.order?.jsApi?.params; const wxResult = await wxLogin();
if (!params?.timeStamp || !params?.nonceStr || !params?.package || !params?.paySign) { const result = await virtualPayOrderAPI(vipId, wxResult.code);
const finalSignData = getVirtualPaySignData(result);
if (!finalSignData || !result?.paySig || !result?.signature) {
uni.showToast({ uni.showToast({
title: "支付参数生成失败", title: "支付参数生成失败",
icon: "none", icon: "none",
@@ -330,34 +324,57 @@ const onPay = async () => {
refreshing.value = false; refreshing.value = false;
return; return;
} }
waitingPayment = true; wx.requestVirtualPayment({
wx.requestPayment({ signData: finalSignData,
timeStamp: params.timeStamp, // 微信支付时间戳 paySig: result.paySig,
nonceStr: params.nonceStr, // 微信支付随机串 signature: result.signature,
package: params.package, // 预支付交易会话标识 mode: "short_series_goods",
paySign: params.paySign, // 微信支付签名 async success() {
signType: params.signType || "RSA", payToast = {
success() {
uni.showToast({
title: "支付成功", 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(); refreshUserAfterPay();
}, },
fail(res) { fail(res) {
console.log("pay error", res); console.log("virtual pay error", res);
if (res.errMsg && res.errMsg.indexOf("cancel") !== -1) return; if (isVirtualPayCancel(res)) {
uni.showToast({ payToast = {
title: "支付失败,请稍后重试", title: "支付已取消",
icon: "none", icon: "none",
}); };
return;
}
payToast = {
title: res.message || "支付失败,请稍后重试",
icon: "none",
};
}, },
complete() { complete() {
paying.value = false; paying.value = false;
if (payToast) {
setTimeout(() => {
uni.showToast(payToast);
}, 200);
}
}, },
}); });
waitingPayment = true;
} catch (error) { } 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 { } finally {
if (!waitingPayment) paying.value = false; if (!waitingPayment) paying.value = false;
} }
@@ -365,9 +382,6 @@ const onPay = async () => {
onShow(loadVipConfig); onShow(loadVipConfig);
onBeforeUnmount(() => {
clearRefreshTimer();
});
</script> </script>
<template> <template>
+3 -5
View File
@@ -97,12 +97,10 @@ const cancelOrder = async () => {
>复制</text >复制</text
> >
</view> </view>
<text>下单时间{{ data.vipCreateAt }}</text> <text>创建时间{{ data.orderCreateAt }}</text>
<text <text v-if="data.orderStatus === 4">支付时间{{
>支付时间{{
data.orderStatus === 4 ? data.paymentTime : "" data.orderStatus === 4 ? data.paymentTime : ""
}}</text }}</text>
>
<text>金额{{ data.total }} </text> <text>金额{{ data.total }} </text>
<text>支付方式微信</text> <text>支付方式微信</text>
</view> </view>
+2 -2
View File
@@ -13,7 +13,7 @@ import Container from "@/components/Container.vue";
<view class="section"> <view class="section">
<view class="title">段位体系概述</view> <view class="title">段位体系概述</view>
<view class="text"> <view class="text">
我们的段位体系分为多个等级从低到高依次为倔强青铜秩序白银黄金王者永恒钻石最强王者非凡王者无双王者绝世王者至圣王者荣耀王者和传奇王者每个大段位下又分为若干小段位玩家需要通过积累积分来提升段位 我们的段位体系分为多个等级从低到高依次为倔强青铜秩序白银荣耀黄金永恒钻石最强王者非凡王者无双王者绝世王者至圣王者荣耀王者和传奇王者每个大段位下又分为若干小段位玩家需要通过积累积分来提升段位
</view> </view>
</view> </view>
@@ -79,7 +79,7 @@ import Container from "@/components/Container.vue";
<text>每个小段位需要满 3颗星才能晋升到下一个段位共9颗星</text> <text>每个小段位需要满 3颗星才能晋升到下一个段位共9颗星</text>
</view> </view>
<view class="table-row"> <view class="table-row">
<text>黄金王者</text> <text>荣耀黄金</text>
<view> <view>
<text>黄金1*</text> <text>黄金1*</text>
<text>黄金2*</text> <text>黄金2*</text>