feat(recharge): add full virtual recharge flow for aigc credits
- Replace legacy recharge API endpoints with new dedicated VirtualRechargeApi with sensitive request logging - Implement pending order tracking, automatic payment reconciliation, and recovery for interrupted payments - Overhaul recharge component logic to support both pre-defined package and custom amount recharge - Fix currency price formatting to consistently display 2 decimal places for all monetary values - Remove unused xiaoqi-avatar asset and related component imports - Simplify Vite build asset naming configuration - Switch application to test development mode
This commit is contained in:
@@ -51,19 +51,40 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { onHide, onLoad, onShow, onUnload } from "@dcloudio/uni-app";
|
||||
import TopNavBar from "@/components/TopNavBar/index.vue";
|
||||
import { useCurrentCredit } from "@/pages-aigc/hooks/useCurrentCredit.js";
|
||||
import {
|
||||
getPointsTopUpAgreement,
|
||||
getRechargeOptions,
|
||||
rechargeCredit,
|
||||
} from "@/request/api/AigcApi.js";
|
||||
import AgreePopup from "@/pages/login/components/AgreePopup/index.vue";
|
||||
import { getPointsTopUpAgreement } from "@/request/api/AigcApi.js";
|
||||
import {
|
||||
confirmVirtualRechargeOrder,
|
||||
createVirtualRechargeOrder,
|
||||
getVirtualRechargeOptions,
|
||||
getVirtualRechargeOrder,
|
||||
} from "@/request/api/VirtualRechargeApi.js";
|
||||
import BalanceCard from "./components/BalanceCard/index.vue";
|
||||
import CustomAmountCard from "./components/CustomAmountCard/index.vue";
|
||||
import RechargeFooter from "./components/RechargeFooter/index.vue";
|
||||
import RechargePackageList from "./components/RechargePackageList/index.vue";
|
||||
import {
|
||||
getPendingVirtualRechargeOrderNo,
|
||||
removePendingVirtualRechargeOrderNo,
|
||||
setPendingVirtualRechargeOrderNo,
|
||||
} from "./services/pendingVirtualRecharge.js";
|
||||
import {
|
||||
getWechatClientPlatform,
|
||||
getWechatLoginCode,
|
||||
requestWechatVirtualPayment,
|
||||
} from "./services/wechatVirtualPayment.js";
|
||||
import {
|
||||
buildVirtualRechargeOrderPayload,
|
||||
classifyVirtualRechargeStatus,
|
||||
normalizeVirtualPaymentOrder,
|
||||
VIRTUAL_RECHARGE_STATUS_KIND,
|
||||
} from "./utils/virtualRecharge.js";
|
||||
|
||||
const ORDER_QUERY_INTERVAL = 2000;
|
||||
const MAX_ORDER_QUERY_ATTEMPTS = 5;
|
||||
|
||||
const { pointBalance: balancePoints, fetchCurrentCredit } = useCurrentCredit();
|
||||
const rechargeOptions = ref([]);
|
||||
@@ -71,51 +92,78 @@ const selectedRechargeOption = ref(null);
|
||||
const customAmount = ref("");
|
||||
const rechargeMode = ref("package");
|
||||
const paying = ref(false);
|
||||
const reconciling = ref(false);
|
||||
const agreementAccepted = ref(false);
|
||||
const agreementContent = ref("");
|
||||
const agreementLoading = ref(false);
|
||||
const agreementVisible = ref(false);
|
||||
|
||||
let orderQueryTimer = null;
|
||||
let orderQueryResolver = null;
|
||||
let pageVisible = false;
|
||||
|
||||
const agreementReady = computed(() => Boolean(agreementContent.value.trim()));
|
||||
|
||||
const getRechargeOptionKey = (option) => {
|
||||
if (!option) return "";
|
||||
|
||||
return `${option.rechargeAmount ?? ""}-${option.rechargePoints ?? ""}`;
|
||||
};
|
||||
|
||||
const selectedRechargeKey = computed(() => {
|
||||
if (rechargeMode.value !== "package") return "";
|
||||
|
||||
return getRechargeOptionKey(selectedRechargeOption.value);
|
||||
return String(selectedRechargeOption.value?.optionCode || "");
|
||||
});
|
||||
|
||||
const payPrice = computed(() => {
|
||||
const amount =
|
||||
rechargeMode.value === "custom"
|
||||
? Number.parseInt(customAmount.value || "0", 10)
|
||||
: Number(selectedRechargeOption.value?.rechargeAmount);
|
||||
return Number.isFinite(amount) ? amount : 0;
|
||||
if (rechargeMode.value === "custom") {
|
||||
const amount = Number(customAmount.value);
|
||||
return Number.isInteger(amount) ? amount : 0;
|
||||
}
|
||||
|
||||
const amountFen = Number(selectedRechargeOption.value?.expectedAmountFen);
|
||||
return Number.isInteger(amountFen) && amountFen > 0 ? amountFen / 100 : 0;
|
||||
});
|
||||
|
||||
const showToast = (title) => {
|
||||
uni.showToast({
|
||||
title,
|
||||
icon: "none",
|
||||
const showToast = (title, icon = "none") => {
|
||||
uni.showToast({ title, icon });
|
||||
};
|
||||
|
||||
const clearOrderQueryTimer = () => {
|
||||
if (orderQueryTimer) {
|
||||
clearTimeout(orderQueryTimer);
|
||||
orderQueryTimer = null;
|
||||
}
|
||||
if (orderQueryResolver) {
|
||||
orderQueryResolver(false);
|
||||
orderQueryResolver = null;
|
||||
}
|
||||
};
|
||||
|
||||
const waitForNextOrderQuery = () => {
|
||||
if (!pageVisible) return Promise.resolve(false);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
orderQueryResolver = resolve;
|
||||
orderQueryTimer = setTimeout(() => {
|
||||
orderQueryTimer = null;
|
||||
orderQueryResolver = null;
|
||||
resolve(pageVisible);
|
||||
}, ORDER_QUERY_INTERVAL);
|
||||
});
|
||||
};
|
||||
|
||||
const fetchRechargeOptions = async () => {
|
||||
try {
|
||||
const res = await getRechargeOptions();
|
||||
const res = await getVirtualRechargeOptions();
|
||||
if (res?.code === 0 && Array.isArray(res.data)) {
|
||||
rechargeOptions.value = res.data;
|
||||
if (rechargeMode.value === "package") {
|
||||
selectedRechargeOption.value = res.data[0] || null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
rechargeOptions.value = [];
|
||||
selectedRechargeOption.value = null;
|
||||
} catch (error) {
|
||||
console.error("获取积分充值档位失败", error);
|
||||
console.error("获取虚拟支付充值档位失败", error);
|
||||
rechargeOptions.value = [];
|
||||
selectedRechargeOption.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -133,16 +181,12 @@ const fetchPointsTopUpAgreement = async (showError = false) => {
|
||||
|
||||
agreementContent.value = "";
|
||||
agreementAccepted.value = false;
|
||||
if (showError) {
|
||||
showToast(res?.msg || "协议加载失败,请重试");
|
||||
}
|
||||
if (showError) showToast(res?.msg || "协议加载失败,请重试");
|
||||
} catch (error) {
|
||||
console.error("获取积分充值协议失败", error);
|
||||
agreementContent.value = "";
|
||||
agreementAccepted.value = false;
|
||||
if (showError) {
|
||||
showToast("协议加载失败,请重试");
|
||||
}
|
||||
if (showError) showToast("协议加载失败,请重试");
|
||||
} finally {
|
||||
agreementLoading.value = false;
|
||||
}
|
||||
@@ -150,21 +194,129 @@ const fetchPointsTopUpAgreement = async (showError = false) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
onLoad(() => {
|
||||
agreementAccepted.value = false;
|
||||
fetchRechargeOptions();
|
||||
fetchPointsTopUpAgreement();
|
||||
});
|
||||
const finishOrderByStatus = async (orderData) => {
|
||||
const statusKind = classifyVirtualRechargeStatus(orderData?.status);
|
||||
|
||||
onShow(() => {
|
||||
fetchCurrentCredit();
|
||||
});
|
||||
if (statusKind === VIRTUAL_RECHARGE_STATUS_KIND.PAID) {
|
||||
removePendingVirtualRechargeOrderNo();
|
||||
await fetchCurrentCredit();
|
||||
showToast("充值成功", "success");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (statusKind === VIRTUAL_RECHARGE_STATUS_KIND.REFUNDED) {
|
||||
removePendingVirtualRechargeOrderNo();
|
||||
await fetchCurrentCredit();
|
||||
showToast("充值订单已退款");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (statusKind === VIRTUAL_RECHARGE_STATUS_KIND.FAILED) {
|
||||
removePendingVirtualRechargeOrderNo();
|
||||
showToast("充值订单异常,请重新发起支付");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const getPaymentErrorMessage = (error) => {
|
||||
switch (Number(error?.errCode)) {
|
||||
case -2:
|
||||
return "已取消支付";
|
||||
case -4:
|
||||
return "支付被微信风控拦截";
|
||||
case -15007:
|
||||
return "微信登录状态已过期,请重新发起支付";
|
||||
default:
|
||||
return "支付失败,请重试";
|
||||
}
|
||||
};
|
||||
|
||||
const reconcileVirtualRechargeOrder = async (
|
||||
rechargeOrderNo,
|
||||
{ clientPaymentError = null, showPendingToast = true } = {}
|
||||
) => {
|
||||
if (reconciling.value || !rechargeOrderNo) return "skipped";
|
||||
|
||||
reconciling.value = true;
|
||||
let latestOrder = null;
|
||||
|
||||
try {
|
||||
let confirmArgs = {};
|
||||
try {
|
||||
const wxLoginCode = await getWechatLoginCode();
|
||||
confirmArgs = { wxLoginCode };
|
||||
} catch (error) {
|
||||
console.warn("刷新微信登录凭证失败,将使用现有服务端会话确认订单", error);
|
||||
}
|
||||
|
||||
try {
|
||||
const confirmRes = await confirmVirtualRechargeOrder(
|
||||
rechargeOrderNo,
|
||||
confirmArgs
|
||||
);
|
||||
if (confirmRes?.code === 0 && confirmRes.data) {
|
||||
latestOrder = confirmRes.data;
|
||||
if (await finishOrderByStatus(latestOrder)) return "terminal";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("服务端确认虚拟支付订单失败", error);
|
||||
}
|
||||
|
||||
if (Number(clientPaymentError?.errCode) === -2 && latestOrder?.status === "UNPAID") {
|
||||
removePendingVirtualRechargeOrderNo();
|
||||
showToast("已取消支付");
|
||||
return "cancelled";
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt < MAX_ORDER_QUERY_ATTEMPTS; attempt += 1) {
|
||||
const shouldContinue = await waitForNextOrderQuery();
|
||||
if (!shouldContinue) return "paused";
|
||||
|
||||
try {
|
||||
const queryRes = await getVirtualRechargeOrder(rechargeOrderNo);
|
||||
if (queryRes?.code === 0 && queryRes.data) {
|
||||
latestOrder = queryRes.data;
|
||||
if (await finishOrderByStatus(latestOrder)) return "terminal";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("查询虚拟支付充值订单失败", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (clientPaymentError && latestOrder?.status === "UNPAID") {
|
||||
removePendingVirtualRechargeOrderNo();
|
||||
showToast(getPaymentErrorMessage(clientPaymentError));
|
||||
return "client-failed";
|
||||
}
|
||||
|
||||
if (showPendingToast) {
|
||||
showToast("支付结果确认中,请稍后返回查看");
|
||||
}
|
||||
return "pending";
|
||||
} finally {
|
||||
reconciling.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const recoverPendingVirtualRechargeOrder = async () => {
|
||||
const rechargeOrderNo = getPendingVirtualRechargeOrderNo();
|
||||
if (!rechargeOrderNo || paying.value || reconciling.value) return;
|
||||
|
||||
paying.value = true;
|
||||
uni.showLoading({ title: "正在确认支付结果...", mask: true });
|
||||
try {
|
||||
await reconcileVirtualRechargeOrder(rechargeOrderNo);
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
paying.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack();
|
||||
}
|
||||
if (pages.length > 1) uni.navigateBack();
|
||||
};
|
||||
|
||||
const handleOpenLedger = () => {
|
||||
@@ -177,6 +329,7 @@ const handleOpenLedger = () => {
|
||||
const handleSelectRechargeOption = (option) => {
|
||||
rechargeMode.value = "package";
|
||||
selectedRechargeOption.value = option;
|
||||
customAmount.value = "";
|
||||
};
|
||||
|
||||
const handleSelectCustomAmount = () => {
|
||||
@@ -196,53 +349,31 @@ const handleViewAgreement = async () => {
|
||||
}
|
||||
|
||||
const loaded = await fetchPointsTopUpAgreement(true);
|
||||
if (loaded) {
|
||||
agreementVisible.value = true;
|
||||
}
|
||||
if (loaded) agreementVisible.value = true;
|
||||
};
|
||||
|
||||
const requestWechatPay = (payData) => {
|
||||
const { nonceStr, packageVal, paySign, signType, timeStamp } = payData || {};
|
||||
|
||||
if (!nonceStr || !packageVal || !paySign || !signType || !timeStamp) {
|
||||
showToast("支付参数错误,请重试");
|
||||
const validateRechargeSelection = () => {
|
||||
if (rechargeMode.value === "package") {
|
||||
if (!String(selectedRechargeOption.value?.optionCode || "").trim()) {
|
||||
throw new Error("请选择充值档位");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.requestPayment({
|
||||
provider: "wxpay",
|
||||
timeStamp: String(timeStamp),
|
||||
nonceStr: String(nonceStr),
|
||||
package: String(packageVal),
|
||||
signType: String(signType),
|
||||
paySign: String(paySign),
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: "支付成功",
|
||||
icon: "success",
|
||||
});
|
||||
fetchCurrentCredit();
|
||||
},
|
||||
fail: () => {
|
||||
showToast("支付失败,请重试");
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
const amountText = String(customAmount.value || "").trim();
|
||||
const amount = Number(amountText);
|
||||
if (!/^\d{1,6}$/.test(amountText) || amount < 1 || amount > 999999) {
|
||||
throw new Error("请输入1至999999的整数充值金额");
|
||||
}
|
||||
};
|
||||
|
||||
const handlePay = async () => {
|
||||
if (paying.value) return;
|
||||
|
||||
if (rechargeMode.value === "package" && !selectedRechargeOption.value) {
|
||||
showToast("请选择充值档位");
|
||||
return;
|
||||
}
|
||||
|
||||
const rechargeAmount = payPrice.value;
|
||||
if (!Number.isInteger(rechargeAmount) || rechargeAmount <= 0) {
|
||||
const message = rechargeMode.value === "custom" ? "请输入正确的充值金额" : "充值档位金额错误,请重试";
|
||||
showToast(message);
|
||||
try {
|
||||
validateRechargeSelection();
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -250,43 +381,81 @@ const handlePay = async () => {
|
||||
showToast("协议加载中,请稍候");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!agreementReady.value) {
|
||||
showToast("协议加载失败,请重试");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!agreementAccepted.value) {
|
||||
showToast("请先阅读并同意积分充值协议");
|
||||
return;
|
||||
}
|
||||
|
||||
paying.value = true;
|
||||
uni.showLoading({ title: "正在发起支付..." });
|
||||
clearOrderQueryTimer();
|
||||
uni.showLoading({ title: "正在发起支付...", mask: true });
|
||||
|
||||
try {
|
||||
const res = await rechargeCredit({
|
||||
rechargeAmount,
|
||||
payWay: "0",
|
||||
paySource: "1",
|
||||
const clientPlatform = getWechatClientPlatform();
|
||||
const wxLoginCode = await getWechatLoginCode();
|
||||
const createOrderPayload = buildVirtualRechargeOrderPayload({
|
||||
mode: rechargeMode.value,
|
||||
rechargeOptionCode: selectedRechargeOption.value?.optionCode,
|
||||
rechargeAmount: customAmount.value,
|
||||
wxLoginCode,
|
||||
clientPlatform,
|
||||
});
|
||||
const createRes = await createVirtualRechargeOrder(createOrderPayload);
|
||||
|
||||
uni.hideLoading();
|
||||
|
||||
if (res?.code === 0 && res.data) {
|
||||
requestWechatPay(res.data);
|
||||
return;
|
||||
if (createRes?.code !== 0 || !createRes.data) {
|
||||
throw new Error(createRes?.msg || "充值下单失败,请重试");
|
||||
}
|
||||
|
||||
showToast(res?.msg || "充值下单失败,请重试");
|
||||
} catch (error) {
|
||||
const paymentOrder = normalizeVirtualPaymentOrder(createRes.data);
|
||||
setPendingVirtualRechargeOrderNo(paymentOrder.rechargeOrderNo);
|
||||
uni.hideLoading();
|
||||
console.error("充值下单失败", error);
|
||||
showToast("充值下单失败,请重试");
|
||||
|
||||
let clientPaymentError = null;
|
||||
try {
|
||||
await requestWechatVirtualPayment(paymentOrder);
|
||||
} catch (error) {
|
||||
clientPaymentError = error;
|
||||
}
|
||||
|
||||
pageVisible = true;
|
||||
uni.showLoading({ title: "正在确认支付结果...", mask: true });
|
||||
await reconcileVirtualRechargeOrder(paymentOrder.rechargeOrderNo, {
|
||||
clientPaymentError,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("虚拟支付积分充值失败", error);
|
||||
showToast(error?.message || "充值下单失败,请重试");
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
paying.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onLoad(() => {
|
||||
agreementAccepted.value = false;
|
||||
fetchRechargeOptions();
|
||||
fetchPointsTopUpAgreement();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
pageVisible = true;
|
||||
fetchCurrentCredit();
|
||||
recoverPendingVirtualRechargeOrder();
|
||||
});
|
||||
|
||||
onHide(() => {
|
||||
pageVisible = false;
|
||||
clearOrderQueryTimer();
|
||||
});
|
||||
|
||||
onUnload(() => {
|
||||
pageVisible = false;
|
||||
clearOrderQueryTimer();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
Reference in New Issue
Block a user