2
0

feat: paypal支付

This commit is contained in:
caiyuchao
2025-04-23 16:00:21 +08:00
parent 092864e661
commit 3cbaebb5d3
9 changed files with 167 additions and 0 deletions

1
.env
View File

@@ -35,3 +35,4 @@ VITE_SERVICE_EXPIRED_TOKEN_CODES=403
VITE_SERVICE_SERVER_ERROR_CODE=500
VITE_PAYPAL_CLIENT_ID=AfPgwFAmo9K7KCqiiGpNRCyQMSxI6V33eH-nEMnVndJNVEYOEOEn5wSPkHUybfzcjDLnBejt-RKnIfqX

View File

@@ -14,6 +14,7 @@
"dependencies": {
"@better-scroll/core": "2.5.1",
"@iconify/vue": "4.1.2",
"@paypal/paypal-js": "^8.2.0",
"@sa/axios": "workspace:*",
"@sa/color-palette": "workspace:*",
"@sa/hooks": "workspace:*",

15
pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ importers:
'@iconify/vue':
specifier: 4.1.2
version: 4.1.2(vue@3.4.27(typescript@5.4.5))
'@paypal/paypal-js':
specifier: ^8.2.0
version: 8.2.0
'@sa/axios':
specifier: workspace:*
version: link:packages/axios
@@ -730,6 +733,9 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@paypal/paypal-js@8.2.0':
resolution: {integrity: sha512-hLg5wNORW3WiyMiRNJOm6cN2IqjPlClpxd971bEdm0LNpbbejQZYtesb0/0arTnySSbGcxg7MxjkZ/N5Z5qBNQ==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@@ -3263,6 +3269,9 @@ packages:
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
engines: {node: '>=0.4.0'}
promise-polyfill@8.3.0:
resolution: {integrity: sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==}
prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
@@ -4633,6 +4642,10 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1
'@paypal/paypal-js@8.2.0':
dependencies:
promise-polyfill: 8.3.0
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -7553,6 +7566,8 @@ snapshots:
progress@2.0.3: {}
promise-polyfill@8.3.0: {}
prompts@2.4.2:
dependencies:
kleur: 3.0.3

View File

@@ -3,6 +3,7 @@ import { defineProps, defineEmits, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { AlipayOutlined, WechatOutlined, WalletOutlined } from '@ant-design/icons-vue';
import { Modal } from 'ant-design-vue';
import PaypalButton from '@/components/payment/paypal-button.vue';
const { t } = useI18n();
@@ -128,6 +129,9 @@ const handleCancel = () => {
<WechatOutlined class="payment-icon wxpay-icon" />
<span>{{ t('page.order.wxpay') }}</span>
</div>
<PaypalButton
:order-info="props.orderInfo"
/>
</div>
</div>
</a-spin>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { constructPaypalBtn } from '@/utils/paypal';
interface Props {
orderInfo: {
orderId: string;
orderType: number; // 0: 购买套餐, 1: 余额充值
orderAmount: number;
};
}
const props = defineProps<Props>();
onMounted(async()=>{
await constructPaypalBtn(props.orderInfo.orderId, "USD");
})
</script>
<template>
<div id="paypal-buttons"></div>
</template>
<style scoped>
</style>

View File

@@ -37,3 +37,20 @@ export function payBalance(params: { orderId: string }) {
}
});
}
/** Paypal createOrder */
export function payPalOrders(params: {orderId: number}) {
return request({
url: '/payment/paypal/orders',
method: 'post',
params
});
}
/** Paypal captureOrder */
export function payPalCapture(paypalOrderId: string, orderId: number) {
return request({
url: `/payment/paypal/orders/${paypalOrderId}/capture/${orderId}`,
method: 'post'
});
}

View File

@@ -30,6 +30,7 @@ declare global {
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const constructPaypalBtn: typeof import('../utils/paypal')['constructPaypalBtn']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
@@ -180,6 +181,8 @@ declare global {
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const payBalance: typeof import('../service/api/payment')['payBalance']
const payPalCapture: typeof import('../service/api/payment')['payPalCapture']
const payPalOrders: typeof import('../service/api/payment')['payPalOrders']
const pick: typeof import('lodash-es')['pick']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']

View File

@@ -80,6 +80,7 @@ declare module 'vue' {
LookForward: typeof import('./../components/custom/look-forward.vue')['default']
MenuToggler: typeof import('./../components/common/menu-toggler.vue')['default']
OrderConfirmModal: typeof import('./../components/order-confirm/orderConfirmModal.vue')['default']
PaypalButton: typeof import('./../components/payment/paypal-button.vue')['default']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']

99
src/utils/paypal.ts Normal file
View File

@@ -0,0 +1,99 @@
import { loadScript } from "@paypal/paypal-js";
import { payPalOrders, payPalCapture } from '@/service/api/payment';
export const constructPaypalBtn = async(orderId: number, currency: string) =>{
loadScript({ clientId: import.meta.env.VITE_PAYPAL_CLIENT_ID, disableFunding: ["paylater"], currency })
.then((paypal: any) => {
paypal
.Buttons({
style: {
shape: "rect",
layout: "vertical",
color: "white",
label: "paypal",
},
message: {
},
async createOrder() {
try {
const response = await payPalOrders({orderId: orderId});
const orderData = response.data;
if (orderData.id) {
return orderData.id;
}
const errorDetail = orderData?.details?.[0];
const errorMessage = errorDetail
? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})`
: JSON.stringify(orderData);
throw new Error(errorMessage);
} catch (error) {
console.error(error);
// resultMessage(`Could not initiate PayPal Checkout...<br><br>${error}`);
}
},
async onApprove(data, actions) {
try {
const response = await payPalCapture(data.orderID, orderId)
const orderData = response.data
// Three cases to handle:
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
// (2) Other non-recoverable errors -> Show a failure message
// (3) Successful transaction -> Show confirmation or thank you message
const errorDetail = orderData?.details?.[0];
if (errorDetail?.issue === "INSTRUMENT_DECLINED") {
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
// recoverable state, per
// https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/
return actions.restart();
} else if (errorDetail) {
// (2) Other non-recoverable errors -> Show a failure message
throw new Error(`${errorDetail.description} (${orderData.debug_id})`);
} else if (!orderData.purchase_units) {
throw new Error(JSON.stringify(orderData));
} else {
// (3) Successful transaction -> Show confirmation or thank you message
// Or go to another URL: actions.redirect('thank_you.html');
const transaction =
orderData?.purchase_units?.[0]?.payments?.captures?.[0] ||
orderData?.purchase_units?.[0]?.payments?.authorizations?.[0];
// resultMessage(
// `Transaction ${transaction.status}: ${transaction.id}<br>
//<br>See console for all available details`
//);
console.log(
"Capture result",
orderData,
JSON.stringify(orderData, null, 2)
);
}
} catch (error) {
console.error(error);
// resultMessage(
// `Sorry, your transaction could not be processed...<br><br>${error}`
//);
}
},
})
.render("#paypal-buttons")
.catch((error) => {
console.error("failed to render the PayPal Buttons", error);
});
})
.catch((error) => {
console.error("failed to load the PayPal JS SDK script", error);
});
}
// Example function to show a result to the user. Your site's UI library can be used instead.
function resultMessage(message: any) {
const container: any = document.querySelector("#result-message");
container.innerHTML = message;
}