增加paypal支付
This commit is contained in:
parent
88b278499b
commit
e6348d51e5
@ -65,11 +65,84 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label
|
||||||
|
class="payment-option"
|
||||||
|
:class="{'payment-option-active': paymentGateway === 'PayPal'}"
|
||||||
|
>
|
||||||
|
<div class="payment-icon">
|
||||||
|
<!-- PayPal SVG Logo -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 119 28" class="paypal-svg">
|
||||||
|
<path d="M10.49,27.45c0-0.08,0.02-0.16,0.03-0.24c0.22-0.9,1.1-1.59,2.02-1.59h3.13c0.81,0,1.56,0.45,1.93,1.17
|
||||||
|
c0.04,0.06,0.09,0.12,0.14,0.17c0.36,0.43,0.55,0.96,0.55,1.52c0,0.08-0.01,0.16-0.03,0.24C19.03,27.79,18.89,28,18.73,28
|
||||||
|
c-0.36,0-0.72-0.15-0.99-0.43c-0.07-0.08-0.14-0.16-0.22-0.24c-0.06-0.07-0.11-0.13-0.17-0.19c-0.2-0.18-0.47-0.28-0.75-0.28h-0.87
|
||||||
|
c-0.92,0-1.66-0.69-1.76-1.59C12.96,27.3,11.89,27.45,10.49,27.45z" fill="#003087"></path>
|
||||||
|
<path d="M9.94,22.55c0.02-0.89,0.8-1.59,1.68-1.59h3.98c0.92,0,1.66,0.7,1.68,1.59c0,0.89-0.74,1.59-1.68,1.59h-3.98
|
||||||
|
C10.68,24.14,9.92,23.44,9.94,22.55z" fill="#003087"></path>
|
||||||
|
<path d="M21.63,22.55c0.02-0.89,0.8-1.59,1.68-1.59h3.22c0.92,0,1.66,0.7,1.68,1.59c0,0.89-0.74,1.59-1.68,1.59h-3.22
|
||||||
|
C22.37,24.14,21.61,23.44,21.63,22.55z" fill="#003087"></path>
|
||||||
|
<path d="M28.63,22.55c0.02-0.89,0.8-1.59,1.68-1.59h3.69c0.92,0,1.66,0.7,1.68,1.59c0,0.89-0.74,1.59-1.68,1.59h-3.69
|
||||||
|
C29.37,24.14,28.61,23.44,28.63,22.55z" fill="#003087"></path>
|
||||||
|
<path d="M36.16,22.55c0.02-0.89,0.8-1.59,1.68-1.59h4.42c0.92,0,1.66,0.7,1.68,1.59c0,0.89-0.74,1.59-1.68,1.59h-4.42
|
||||||
|
C36.9,24.14,36.14,23.44,36.16,22.55z" fill="#003087"></path>
|
||||||
|
<path d="M42.71,22.55c0.02-0.89,0.8-1.59,1.68-1.59h4.11c0.92,0,1.66,0.7,1.68,1.59c0,0.89-0.74,1.59-1.68,1.59h-4.11
|
||||||
|
C43.45,24.14,42.69,23.44,42.71,22.55z" fill="#003087"></path>
|
||||||
|
<path d="M50.07,22.55c0.02-0.89,0.8-1.59,1.68-1.59h4.34c0.92,0,1.66,0.7,1.68,1.59c0,0.89-0.74,1.59-1.68,1.59h-4.34
|
||||||
|
C50.81,24.14,50.05,23.44,50.07,22.55z" fill="#003087"></path>
|
||||||
|
<path d="M113.45,9.46h-3.11c-0.7,0-1.28,0.59-1.28,1.3c0,0.66,0.51,1.19,1.14,1.29l3.24,0.53c0.76,0.12,1.32,0.81,1.32,1.59
|
||||||
|
c0,0.83-0.68,1.5-1.53,1.5c-0.49,0-0.94-0.22-1.23-0.58c-0.04-0.05-0.08-0.11-0.12-0.16c-0.16-0.2-0.27-0.43-0.34-0.68h-2.69
|
||||||
|
c0,0.59,0.11,1.16,0.34,1.68c0.33,0.74,0.96,1.26,1.75,1.26c0.25,0,0.5-0.05,0.72-0.14c0.14-0.06,0.27-0.12,0.4-0.2
|
||||||
|
c0.06-0.04,0.12-0.08,0.17-0.12c0.42-0.29,0.64-0.78,0.64-1.32c0-0.21-0.04-0.42-0.12-0.62c-0.05-0.12-0.11-0.24-0.18-0.35
|
||||||
|
c-0.04-0.06-0.08-0.12-0.13-0.18l-0.19-0.22c-0.06-0.07-0.12-0.13-0.19-0.2c-0.03-0.03-0.05-0.05-0.08-0.08
|
||||||
|
c-0.18-0.17-0.41-0.28-0.67-0.28h2.04c0.43,0,0.78-0.35,0.78-0.78c0-0.43-0.35-0.78-0.78-0.78h-0.98c-0.69,0-1.25-0.56-1.25-1.25
|
||||||
|
c0-0.65,0.52-1.18,1.16-1.28l3.71-0.52C113.13,8.86,113.45,9.18,113.45,9.46z" fill="#009CDE"></path>
|
||||||
|
<path d="M92.72,10.34c0-0.18-0.04-0.36-0.12-0.52c-0.36-0.63-0.99-1.01-1.74-1.01c-0.27,0-0.53,0.05-0.78,0.15
|
||||||
|
c-0.25,0.1-0.47,0.23-0.68,0.4c-0.1-0.14-0.22-0.28-0.35-0.4c-0.25-0.1-0.51-0.15-0.78-0.15c-0.75,0-1.38,0.38-1.74,1.01
|
||||||
|
c-0.08,0.16-0.12,0.34-0.12,0.52c0,0.19,0.04,0.37,0.12,0.53c0.36,0.63,0.99,1.02,1.74,1.02c0.28,0,0.54-0.05,0.78-0.15
|
||||||
|
c0.25-0.1,0.47-0.23,0.68-0.4c0.1,0.14,0.22,0.28,0.35,0.4c0.24,0.1,0.51,0.15,0.78,0.15c0.75,0,1.38-0.39,1.74-1.02
|
||||||
|
C92.68,10.71,92.72,10.53,92.72,10.34z M90.05,10.34c0,0.7-0.57,1.27-1.27,1.27c-0.7,0-1.27-0.57-1.27-1.27
|
||||||
|
c0-0.7,0.57-1.27,1.27-1.27C89.48,9.07,90.05,9.64,90.05,10.34z" fill="#003087"></path>
|
||||||
|
<path d="M73.94,10.34c0-0.18-0.04-0.36-0.12-0.52c-0.36-0.63-0.99-1.01-1.74-1.01c-0.27,0-0.53,0.05-0.78,0.15
|
||||||
|
c-0.25,0.1-0.47,0.23-0.68,0.4c-0.1-0.14-0.22-0.28-0.35-0.4c-0.25-0.1-0.51-0.15-0.78-0.15c-0.75,0-1.38,0.38-1.74,1.01
|
||||||
|
c-0.08,0.16-0.12,0.34-0.12,0.52c0,0.19,0.04,0.37,0.12,0.53c0.36,0.63,0.99,1.02,1.74,1.02c0.28,0,0.54-0.05,0.78-0.15
|
||||||
|
c0.25-0.1,0.47-0.23,0.68-0.4c0.1,0.14,0.22,0.28,0.35,0.4c0.24,0.1,0.51,0.15,0.78,0.15c0.75,0,1.38-0.39,1.74-1.02
|
||||||
|
C73.9,10.71,73.94,10.53,73.94,10.34z M71.27,10.34c0,0.7-0.57,1.27-1.27,1.27c-0.7,0-1.27-0.57-1.27-1.27
|
||||||
|
c0-0.7,0.57-1.27,1.27-1.27C70.7,9.07,71.27,9.64,71.27,10.34z" fill="#003087"></path>
|
||||||
|
<path d="M63.29,10.34c0-0.18-0.04-0.36-0.12-0.52c-0.36-0.63-0.99-1.01-1.74-1.01c-0.27,0-0.53,0.05-0.78,0.15
|
||||||
|
c-0.25,0.1-0.47,0.23-0.68,0.4c-0.1-0.14-0.22-0.28-0.35-0.4c-0.25-0.1-0.51-0.15-0.78-0.15c-0.75,0-1.38,0.38-1.74,1.01
|
||||||
|
c-0.08,0.16-0.12,0.34-0.12,0.52c0,0.19,0.04,0.37,0.12,0.53c0.36,0.63,0.99,1.02,1.74,1.02c0.28,0,0.54-0.05,0.78-0.15
|
||||||
|
c0.25-0.1,0.47-0.23,0.68-0.4c0.1,0.14,0.22,0.28,0.35,0.4c0.24,0.1,0.51,0.15,0.78,0.15c0.75,0,1.38-0.39,1.74-1.02
|
||||||
|
C63.25,10.71,63.29,10.53,63.29,10.34z M60.62,10.34c0,0.7-0.57,1.27-1.27,1.27c-0.7,0-1.27-0.57-1.27-1.27
|
||||||
|
c0-0.7,0.57-1.27,1.27-1.27C60.05,9.07,60.62,9.64,60.62,10.34z" fill="#003087"></path>
|
||||||
|
<path d="M42.32,9.46H36.97c0,0.59,0.11,1.16,0.34,1.68c0.33,0.74,0.96,1.26,1.75,1.26c0.81,0,1.49-0.54,1.74-1.27l-0.01,0.02
|
||||||
|
c-0.01,0.08-0.02,0.16-0.03,0.24c-0.22,0.9-1.1,1.59-2.02,1.59h-3.13c-0.92,0-1.66-0.69-1.76-1.59c-0.02,0.08-0.03,0.16-0.03,0.24
|
||||||
|
c0,0.69,0.56,1.25,1.25,1.25c0.25,0,0.5-0.05,0.72-0.14c0.14-0.06,0.27-0.12,0.4-0.2c0.06-0.04,0.12-0.08,0.17-0.12
|
||||||
|
c0.42-0.29,0.64-0.78,0.64-1.32c0-0.21-0.04-0.42-0.12-0.62c-0.05-0.12-0.11-0.24-0.18-0.35c-0.04-0.06-0.08-0.12-0.13-0.18
|
||||||
|
l-0.19-0.22c-0.06-0.07-0.12-0.13-0.19-0.2c-0.03-0.03-0.05-0.05-0.08-0.08c-0.18-0.17-0.41-0.28-0.67-0.28h2.04
|
||||||
|
c0.43,0,0.78-0.35,0.78-0.78c0-0.43-0.35-0.78-0.78-0.78H37c-0.69,0-1.25-0.56-1.25-1.25c0-0.65,0.52-1.18,1.16-1.28l3.71-0.52
|
||||||
|
c0.51-0.07,0.93-0.46,0.99-0.97c0.01-0.16,0.02-0.32,0.02-0.48c0-0.75-0.62-1.36-1.39-1.36h-3.51c-0.7,0-1.28,0.59-1.28,1.3
|
||||||
|
c0,0.66,0.51,1.19,1.14,1.29l3.24,0.53c0.76,0.12,1.32,0.81,1.32,1.59c0,0.83-0.68,1.5-1.53,1.5c-0.49,0-0.94-0.22-1.23-0.58
|
||||||
|
c-0.04-0.05-0.08-0.11-0.12-0.16c-0.16-0.2-0.27-0.43-0.34-0.68h-2.69c0,0.37,0.06,0.73,0.17,1.06c0.19,0.61,0.61,1.06,1.18,1.06
|
||||||
|
c0.45,0,0.85-0.26,1.07-0.68C41.92,10.39,42.32,10.01,42.32,9.46z M27.71,10.34c0-0.18-0.04-0.36-0.12-0.52
|
||||||
|
c-0.36-0.63-0.99-1.01-1.74-1.01c-0.27,0-0.53,0.05-0.78,0.15c-0.25,0.1-0.47,0.23-0.68,0.4c-0.1-0.14-0.22-0.28-0.35-0.4
|
||||||
|
c-0.25-0.1-0.51-0.15-0.78-0.15c-0.75,0-1.38,0.38-1.74,1.01c-0.08,0.16-0.12,0.34-0.12,0.52c0,0.19,0.04,0.37,0.12,0.53
|
||||||
|
c0.36,0.63,0.99,1.02,1.74,1.02c0.28,0,0.54-0.05,0.78-0.15c0.25-0.1,0.47-0.23,0.68-0.4c0.1,0.14,0.22,0.28,0.35,0.4
|
||||||
|
c0.24,0.1,0.51,0.15,0.78,0.15c0.75,0,1.38-0.39,1.74-1.02C27.67,10.71,27.71,10.53,27.71,10.34z M25.04,10.34c0,0.7-0.57,1.27-1.27,1.27
|
||||||
|
c-0.7,0-1.27-0.57-1.27-1.27c0-0.7,0.57-1.27,1.27-1.27C24.47,9.07,25.04,9.64,25.04,10.34z" fill="#003087"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input type="radio" v-model="paymentGateway" value="PayPal" class="hidden-radio">
|
||||||
|
<span class="checkmark-circle" v-if="paymentGateway === 'PayPal'">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="checkmark-icon">
|
||||||
|
<path fill="currentColor" d="M9.55 18l-5.7-5.7 1.425-1.425L9.55 15.15l9.175-9.175L20.15 7.4z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-xs text-gray-500 mt-2 mb-4" v-if="$team.pg.currency === 'CNY'">
|
<div class="text-xs text-gray-500 mt-2 mb-4" v-if="$team.pg.currency === 'CNY'">
|
||||||
{{ $t('Pay with {method}, fast and convenient. Balance will be credited immediately after successful payment.', { method: paymentGateway === 'Alipay' ? $t('Alipay') : paymentGateway === 'WeChatPay' ? $t('WeChat Pay') : $t('Stripe') }) }}
|
{{ $t('Pay with {method}, fast and convenient. Balance will be credited immediately after successful payment.', { method: paymentGateway === 'Alipay' ? $t('Alipay') : paymentGateway === 'WeChatPay' ? $t('WeChat Pay') : paymentGateway === 'Stripe' ? $t('Stripe') : $t('PayPal') }) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BuyPrepaidCreditsAlipay
|
<BuyPrepaidCreditsAlipay
|
||||||
@ -96,11 +169,21 @@
|
|||||||
:minimumAmount="minimumAmount"
|
:minimumAmount="minimumAmount"
|
||||||
@success="onSuccess"
|
@success="onSuccess"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<BuyPrepaidCreditsPayPal
|
||||||
|
v-if="paymentGateway === 'PayPal'"
|
||||||
|
:amount="creditsToBuy"
|
||||||
|
:minimumAmount="minimumAmount"
|
||||||
|
:isOnboarding="isOnboarding"
|
||||||
|
@success="onSuccess"
|
||||||
|
@cancel="onCancel"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import BuyPrepaidCreditsAlipay from './BuyPrepaidCreditsAlipay.vue';
|
import BuyPrepaidCreditsAlipay from './BuyPrepaidCreditsAlipay.vue';
|
||||||
import BuyPrepaidCreditsWeChatPay from './BuyPrepaidCreditsWeChatPay.vue';
|
import BuyPrepaidCreditsWeChatPay from './BuyPrepaidCreditsWeChatPay.vue';
|
||||||
import BuyPrepaidCreditsStripe from './BuyPrepaidCreditsStripe.vue';
|
import BuyPrepaidCreditsStripe from './BuyPrepaidCreditsStripe.vue';
|
||||||
|
import BuyPrepaidCreditsPayPal from './BuyPrepaidCreditsPayPal.vue';
|
||||||
import AlipayLogo from '../logo/AlipayLogo.vue';
|
import AlipayLogo from '../logo/AlipayLogo.vue';
|
||||||
import WeChatPayLogo from '../logo/WeChatPayLogo.vue';
|
import WeChatPayLogo from '../logo/WeChatPayLogo.vue';
|
||||||
import StripeLogo from './StripeLogo.vue';
|
import StripeLogo from './StripeLogo.vue';
|
||||||
@ -111,6 +194,7 @@ export default {
|
|||||||
BuyPrepaidCreditsAlipay,
|
BuyPrepaidCreditsAlipay,
|
||||||
BuyPrepaidCreditsWeChatPay,
|
BuyPrepaidCreditsWeChatPay,
|
||||||
BuyPrepaidCreditsStripe,
|
BuyPrepaidCreditsStripe,
|
||||||
|
BuyPrepaidCreditsPayPal,
|
||||||
AlipayLogo,
|
AlipayLogo,
|
||||||
WeChatPayLogo,
|
WeChatPayLogo,
|
||||||
StripeLogo
|
StripeLogo
|
||||||
@ -182,6 +266,11 @@ export default {
|
|||||||
box-shadow: 0 0 0 1px rgba(103, 114, 229, 0.1);
|
box-shadow: 0 0 0 1px rgba(103, 114, 229, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.payment-option-active:has(input[value="PayPal"]:checked) {
|
||||||
|
border-color: #003087;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 48, 135, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.payment-icon {
|
.payment-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -193,7 +282,7 @@ export default {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alipay-svg, .wechat-svg, .stripe-svg, .payment-icon svg {
|
.alipay-svg, .wechat-svg, .stripe-svg, .paypal-svg, .payment-icon svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 30px;
|
max-height: 30px;
|
||||||
@ -239,6 +328,10 @@ export default {
|
|||||||
background-color: #22AC38;
|
background-color: #22AC38;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.payment-option-active:has(input[value="PayPal"]:checked) .checkmark-circle {
|
||||||
|
background-color: #003087;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive design */
|
/* Responsive design */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.payment-options {
|
.payment-options {
|
||||||
|
|||||||
110
dashboard/src/components/BuyPrepaidCreditsPayPal.vue
Normal file
110
dashboard/src/components/BuyPrepaidCreditsPayPal.vue
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ErrorMessage
|
||||||
|
class="mt-3"
|
||||||
|
:message="$resources.createPayPalOrder.error || error"
|
||||||
|
/>
|
||||||
|
<div class="mt-4 flex w-full justify-end">
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
class="paypal-btn"
|
||||||
|
:loading="$resources.createPayPalOrder.loading || loading"
|
||||||
|
@click="processPayPalPayment"
|
||||||
|
>
|
||||||
|
{{ $t('Pay with PayPal') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { toast } from 'vue-sonner';
|
||||||
|
import { DashboardError } from '../utils/error';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BuyPrepaidCreditsPayPal',
|
||||||
|
props: {
|
||||||
|
amount: {
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
minimumAmount: {
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
isOnboarding: {
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['success', 'cancel'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
resources: {
|
||||||
|
createPayPalOrder() {
|
||||||
|
return {
|
||||||
|
url: 'jcloud.api.billing.create_paypal_order_for_recharge',
|
||||||
|
params: {
|
||||||
|
amount: this.amount
|
||||||
|
},
|
||||||
|
validate() {
|
||||||
|
if (this.amount <= 0) {
|
||||||
|
throw new DashboardError(this.$t('Payment amount must be greater than 0'));
|
||||||
|
}
|
||||||
|
if (this.amount < this.minimumAmount) {
|
||||||
|
throw new DashboardError(this.$t('Amount is below the minimum required amount'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess(response) {
|
||||||
|
window.open(response.payment_url, '_blank');
|
||||||
|
|
||||||
|
toast.success(this.$t('Payment page opened in new window'));
|
||||||
|
|
||||||
|
// Check payment status periodically
|
||||||
|
this.checkPaymentStatus(response.order_id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
processPayPalPayment() {
|
||||||
|
this.$resources.createPayPalOrder.submit();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Optional: method to check payment status
|
||||||
|
checkPaymentStatus(orderId) {
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
this.$call('jcloud.api.billing.check_payment_status', {
|
||||||
|
order_id: orderId,
|
||||||
|
payment_type: 'paypal'
|
||||||
|
}).then(result => {
|
||||||
|
if (result && result.status === 'Success') {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
toast.success(this.$t('Payment successful! Account has been recharged'));
|
||||||
|
this.$emit('success');
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Error checking payment status:', err);
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
// Stop checking after 5 minutes
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
}, 300000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.paypal-btn {
|
||||||
|
background-color: #00457C;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paypal-btn:hover {
|
||||||
|
background-color: #003057;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div class="text-xs text-gray-600">{{ $t('Payment Method') }}</div>
|
<div class="text-xs text-gray-600">{{ $t('Payment Method') }}</div>
|
||||||
<div class="mt-1.5 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
<div class="mt-1.5 grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||||||
<label
|
<label
|
||||||
class="payment-option"
|
class="payment-option"
|
||||||
:class="{'payment-option-active': paymentGateway === 'Alipay'}"
|
:class="{'payment-option-active': paymentGateway === 'Alipay'}"
|
||||||
@ -49,11 +49,83 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
<label
|
||||||
|
class="payment-option"
|
||||||
|
:class="{'payment-option-active': paymentGateway === 'PayPal'}"
|
||||||
|
>
|
||||||
|
<div class="payment-icon">
|
||||||
|
<!-- PayPal SVG Logo -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 119 28" class="paypal-svg">
|
||||||
|
<path d="M10.49,27.45c0-0.08,0.02-0.16,0.03-0.24c0.22-0.9,1.1-1.59,2.02-1.59h3.13c0.81,0,1.56,0.45,1.93,1.17
|
||||||
|
c0.04,0.06,0.09,0.12,0.14,0.17c0.36,0.43,0.55,0.96,0.55,1.52c0,0.08-0.01,0.16-0.03,0.24C19.03,27.79,18.89,28,18.73,28
|
||||||
|
c-0.36,0-0.72-0.15-0.99-0.43c-0.07-0.08-0.14-0.16-0.22-0.24c-0.06-0.07-0.11-0.13-0.17-0.19c-0.2-0.18-0.47-0.28-0.75-0.28h-0.87
|
||||||
|
c-0.92,0-1.66-0.69-1.76-1.59C12.96,27.3,11.89,27.45,10.49,27.45z" fill="#003087"></path>
|
||||||
|
<path d="M9.94,22.55c0.02-0.89,0.8-1.59,1.68-1.59h3.98c0.92,0,1.66,0.7,1.68,1.59c0,0.89-0.74,1.59-1.68,1.59h-3.98
|
||||||
|
C10.68,24.14,9.92,23.44,9.94,22.55z" fill="#003087"></path>
|
||||||
|
<path d="M21.63,22.55c0.02-0.89,0.8-1.59,1.68-1.59h3.22c0.92,0,1.66,0.7,1.68,1.59c0,0.89-0.74,1.59-1.68,1.59h-3.22
|
||||||
|
C22.37,24.14,21.61,23.44,21.63,22.55z" fill="#003087"></path>
|
||||||
|
<path d="M28.63,22.55c0.02-0.89,0.8-1.59,1.68-1.59h3.69c0.92,0,1.66,0.7,1.68,1.59c0,0.89-0.74,1.59-1.68,1.59h-3.69
|
||||||
|
C29.37,24.14,28.61,23.44,28.63,22.55z" fill="#003087"></path>
|
||||||
|
<path d="M36.16,22.55c0.02-0.89,0.8-1.59,1.68-1.59h4.42c0.92,0,1.66,0.7,1.68,1.59c0,0.89-0.74,1.59-1.68,1.59h-4.42
|
||||||
|
C36.9,24.14,36.14,23.44,36.16,22.55z" fill="#003087"></path>
|
||||||
|
<path d="M42.71,22.55c0.02-0.89,0.8-1.59,1.68-1.59h4.11c0.92,0,1.66,0.7,1.68,1.59c0,0.89-0.74,1.59-1.68,1.59h-4.11
|
||||||
|
C43.45,24.14,42.69,23.44,42.71,22.55z" fill="#003087"></path>
|
||||||
|
<path d="M50.07,22.55c0.02-0.89,0.8-1.59,1.68-1.59h4.34c0.92,0,1.66,0.7,1.68,1.59c0,0.89-0.74,1.59-1.68,1.59h-4.34
|
||||||
|
C50.81,24.14,50.05,23.44,50.07,22.55z" fill="#003087"></path>
|
||||||
|
<path d="M113.45,9.46h-3.11c-0.7,0-1.28,0.59-1.28,1.3c0,0.66,0.51,1.19,1.14,1.29l3.24,0.53c0.76,0.12,1.32,0.81,1.32,1.59
|
||||||
|
c0,0.83-0.68,1.5-1.53,1.5c-0.49,0-0.94-0.22-1.23-0.58c-0.04-0.05-0.08-0.11-0.12-0.16c-0.16-0.2-0.27-0.43-0.34-0.68h-2.69
|
||||||
|
c0,0.59,0.11,1.16,0.34,1.68c0.33,0.74,0.96,1.26,1.75,1.26c0.25,0,0.5-0.05,0.72-0.14c0.14-0.06,0.27-0.12,0.4-0.2
|
||||||
|
c0.06-0.04,0.12-0.08,0.17-0.12c0.42-0.29,0.64-0.78,0.64-1.32c0-0.21-0.04-0.42-0.12-0.62c-0.05-0.12-0.11-0.24-0.18-0.35
|
||||||
|
c-0.04-0.06-0.08-0.12-0.13-0.18l-0.19-0.22c-0.06-0.07-0.12-0.13-0.19-0.2c-0.03-0.03-0.05-0.05-0.08-0.08
|
||||||
|
c-0.18-0.17-0.41-0.28-0.67-0.28h2.04c0.43,0,0.78-0.35,0.78-0.78c0-0.43-0.35-0.78-0.78-0.78h-0.98c-0.69,0-1.25-0.56-1.25-1.25
|
||||||
|
c0-0.65,0.52-1.18,1.16-1.28l3.71-0.52C113.13,8.86,113.45,9.18,113.45,9.46z" fill="#009CDE"></path>
|
||||||
|
<path d="M92.72,10.34c0-0.18-0.04-0.36-0.12-0.52c-0.36-0.63-0.99-1.01-1.74-1.01c-0.27,0-0.53,0.05-0.78,0.15
|
||||||
|
c-0.25,0.1-0.47,0.23-0.68,0.4c-0.1-0.14-0.22-0.28-0.35-0.4c-0.25-0.1-0.51-0.15-0.78-0.15c-0.75,0-1.38,0.38-1.74,1.01
|
||||||
|
c-0.08,0.16-0.12,0.34-0.12,0.52c0,0.19,0.04,0.37,0.12,0.53c0.36,0.63,0.99,1.02,1.74,1.02c0.28,0,0.54-0.05,0.78-0.15
|
||||||
|
c0.25-0.1,0.47-0.23,0.68-0.4c0.1,0.14,0.22,0.28,0.35,0.4c0.24,0.1,0.51,0.15,0.78,0.15c0.75,0,1.38-0.39,1.74-1.02
|
||||||
|
C92.68,10.71,92.72,10.53,92.72,10.34z M90.05,10.34c0,0.7-0.57,1.27-1.27,1.27c-0.7,0-1.27-0.57-1.27-1.27
|
||||||
|
c0-0.7,0.57-1.27,1.27-1.27C89.48,9.07,90.05,9.64,90.05,10.34z" fill="#003087"></path>
|
||||||
|
<path d="M73.94,10.34c0-0.18-0.04-0.36-0.12-0.52c-0.36-0.63-0.99-1.01-1.74-1.01c-0.27,0-0.53,0.05-0.78,0.15
|
||||||
|
c-0.25,0.1-0.47,0.23-0.68,0.4c-0.1-0.14-0.22-0.28-0.35-0.4c-0.25-0.1-0.51-0.15-0.78-0.15c-0.75,0-1.38,0.38-1.74,1.01
|
||||||
|
c-0.08,0.16-0.12,0.34-0.12,0.52c0,0.19,0.04,0.37,0.12,0.53c0.36,0.63,0.99,1.02,1.74,1.02c0.28,0,0.54-0.05,0.78-0.15
|
||||||
|
c0.25-0.1,0.47-0.23,0.68-0.4c0.1,0.14,0.22,0.28,0.35,0.4c0.24,0.1,0.51,0.15,0.78,0.15c0.75,0,1.38-0.39,1.74-1.02
|
||||||
|
C73.9,10.71,73.94,10.53,73.94,10.34z M71.27,10.34c0,0.7-0.57,1.27-1.27,1.27c-0.7,0-1.27-0.57-1.27-1.27
|
||||||
|
c0-0.7,0.57-1.27,1.27-1.27C70.7,9.07,71.27,9.64,71.27,10.34z" fill="#003087"></path>
|
||||||
|
<path d="M63.29,10.34c0-0.18-0.04-0.36-0.12-0.52c-0.36-0.63-0.99-1.01-1.74-1.01c-0.27,0-0.53,0.05-0.78,0.15
|
||||||
|
c-0.25,0.1-0.47,0.23-0.68,0.4c-0.1-0.14-0.22-0.28-0.35-0.4c-0.25-0.1-0.51-0.15-0.78-0.15c-0.75,0-1.38,0.38-1.74,1.01
|
||||||
|
c-0.08,0.16-0.12,0.34-0.12,0.52c0,0.19,0.04,0.37,0.12,0.53c0.36,0.63,0.99,1.02,1.74,1.02c0.28,0,0.54-0.05,0.78-0.15
|
||||||
|
c0.25-0.1,0.47-0.23,0.68-0.4c0.1,0.14,0.22,0.28,0.35,0.4c0.24,0.1,0.51,0.15,0.78,0.15c0.75,0,1.38-0.39,1.74-1.02
|
||||||
|
C63.25,10.71,63.29,10.53,63.29,10.34z M60.62,10.34c0,0.7-0.57,1.27-1.27,1.27c-0.7,0-1.27-0.57-1.27-1.27
|
||||||
|
c0-0.7,0.57-1.27,1.27-1.27C60.05,9.07,60.62,9.64,60.62,10.34z" fill="#003087"></path>
|
||||||
|
<path d="M42.32,9.46H36.97c0,0.59,0.11,1.16,0.34,1.68c0.33,0.74,0.96,1.26,1.75,1.26c0.81,0,1.49-0.54,1.74-1.27l-0.01,0.02
|
||||||
|
c-0.01,0.08-0.02,0.16-0.03,0.24c-0.22,0.9-1.1,1.59-2.02,1.59h-3.13c-0.92,0-1.66-0.69-1.76-1.59c-0.02,0.08-0.03,0.16-0.03,0.24
|
||||||
|
c0,0.69,0.56,1.25,1.25,1.25c0.25,0,0.5-0.05,0.72-0.14c0.14-0.06,0.27-0.12,0.4-0.2c0.06-0.04,0.12-0.08,0.17-0.12
|
||||||
|
c0.42-0.29,0.64-0.78,0.64-1.32c0-0.21-0.04-0.42-0.12-0.62c-0.05-0.12-0.11-0.24-0.18-0.35c-0.04-0.06-0.08-0.12-0.13-0.18
|
||||||
|
l-0.19-0.22c-0.06-0.07-0.12-0.13-0.19-0.2c-0.03-0.03-0.05-0.05-0.08-0.08c-0.18-0.17-0.41-0.28-0.67-0.28h2.04
|
||||||
|
c0.43,0,0.78-0.35,0.78-0.78c0-0.43-0.35-0.78-0.78-0.78H37c-0.69,0-1.25-0.56-1.25-1.25c0-0.65,0.52-1.18,1.16-1.28l3.71-0.52
|
||||||
|
c0.51-0.07,0.93-0.46,0.99-0.97c0.01-0.16,0.02-0.32,0.02-0.48c0-0.75-0.62-1.36-1.39-1.36h-3.51c-0.7,0-1.28,0.59-1.28,1.3
|
||||||
|
c0,0.66,0.51,1.19,1.14,1.29l3.24,0.53c0.76,0.12,1.32,0.81,1.32,1.59c0,0.83-0.68,1.5-1.53,1.5c-0.49,0-0.94-0.22-1.23-0.58
|
||||||
|
c-0.04-0.05-0.08-0.11-0.12-0.16c-0.16-0.2-0.27-0.43-0.34-0.68h-2.69c0,0.37,0.06,0.73,0.17,1.06c0.19,0.61,0.61,1.06,1.18,1.06
|
||||||
|
c0.45,0,0.85-0.26,1.07-0.68C41.92,10.39,42.32,10.01,42.32,9.46z M27.71,10.34c0-0.18-0.04-0.36-0.12-0.52
|
||||||
|
c-0.36-0.63-0.99-1.01-1.74-1.01c-0.27,0-0.53,0.05-0.78,0.15c-0.25,0.1-0.47,0.23-0.68,0.4c-0.1-0.14-0.22-0.28-0.35-0.4
|
||||||
|
c-0.25-0.1-0.51-0.15-0.78-0.15c-0.75,0-1.38,0.38-1.74,1.01c-0.08,0.16-0.12,0.34-0.12,0.52c0,0.19,0.04,0.37,0.12,0.53
|
||||||
|
c0.36,0.63,0.99,1.02,1.74,1.02c0.28,0,0.54-0.05,0.78-0.15c0.25-0.1,0.47-0.23,0.68-0.4c0.1,0.14,0.22,0.28,0.35,0.4
|
||||||
|
c0.24,0.1,0.51,0.15,0.78,0.15c0.75,0,1.38-0.39,1.74-1.02C27.67,10.71,27.71,10.53,27.71,10.34z M25.04,10.34c0,0.7-0.57,1.27-1.27,1.27
|
||||||
|
c-0.7,0-1.27-0.57-1.27-1.27c0-0.7,0.57-1.27,1.27-1.27C24.47,9.07,25.04,9.64,25.04,10.34z" fill="#003087"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input type="radio" v-model="paymentGateway" value="PayPal" class="hidden-radio">
|
||||||
|
<span class="checkmark-circle" v-if="paymentGateway === 'PayPal'">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="checkmark-icon">
|
||||||
|
<path fill="currentColor" d="M9.55 18l-5.7-5.7 1.425-1.425L9.55 15.15l9.175-9.175L20.15 7.4z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-xs text-gray-500 mt-2 mb-4" v-if="$team.pg.currency === 'CNY'">
|
<div class="text-xs text-gray-500 mt-2 mb-4" v-if="$team.pg.currency === 'CNY'">
|
||||||
{{ $t('Pay with {method}, fast and convenient. Balance will be credited immediately after successful payment.', { method: paymentGateway === 'Alipay' ? $t('Alipay') : $t('WeChat Pay') }) }}
|
{{ $t('Pay with {method}, fast and convenient. Balance will be credited immediately after successful payment.', { method: paymentGateway === 'Alipay' ? $t('Alipay') : paymentGateway === 'WeChatPay' ? $t('WeChat Pay') : $t('PayPal') }) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BuyPrepaidCreditsAlipay
|
<BuyPrepaidCreditsAlipay
|
||||||
@ -73,16 +145,27 @@
|
|||||||
@success="onSuccess"
|
@success="onSuccess"
|
||||||
@cancel="onCancel"
|
@cancel="onCancel"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<BuyPrepaidCreditsPayPal
|
||||||
|
v-if="paymentGateway === 'PayPal'"
|
||||||
|
:amount="creditsToBuy"
|
||||||
|
:minimumAmount="minimumAmount"
|
||||||
|
:isOnboarding="isOnboarding"
|
||||||
|
@success="onSuccess"
|
||||||
|
@cancel="onCancel"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import BuyPrepaidCreditsAlipay from '../BuyPrepaidCreditsAlipay.vue';
|
import BuyPrepaidCreditsAlipay from '../BuyPrepaidCreditsAlipay.vue';
|
||||||
import BuyPrepaidCreditsWeChatPay from '../BuyPrepaidCreditsWeChatPay.vue';
|
import BuyPrepaidCreditsWeChatPay from '../BuyPrepaidCreditsWeChatPay.vue';
|
||||||
|
import BuyPrepaidCreditsPayPal from '../BuyPrepaidCreditsPayPal.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'BuyPrepaidCreditsForm',
|
name: 'BuyPrepaidCreditsForm',
|
||||||
components: {
|
components: {
|
||||||
BuyPrepaidCreditsAlipay,
|
BuyPrepaidCreditsAlipay,
|
||||||
BuyPrepaidCreditsWeChatPay
|
BuyPrepaidCreditsWeChatPay,
|
||||||
|
BuyPrepaidCreditsPayPal
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -158,6 +241,11 @@ export default {
|
|||||||
box-shadow: 0 0 0 1px rgba(34, 172, 56, 0.1);
|
box-shadow: 0 0 0 1px rgba(34, 172, 56, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.payment-option-active:has(input[value="PayPal"]:checked) {
|
||||||
|
border-color: #003087;
|
||||||
|
box-shadow: 0 0 0 1px rgba(0, 48, 135, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.payment-icon {
|
.payment-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -169,7 +257,7 @@ export default {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alipay-svg, .wechat-svg {
|
.alipay-svg, .wechat-svg, .paypal-svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 30px;
|
max-height: 30px;
|
||||||
@ -215,6 +303,10 @@ export default {
|
|||||||
background-color: #22AC38;
|
background-color: #22AC38;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.payment-option-active:has(input[value="PayPal"]:checked) .checkmark-circle {
|
||||||
|
background-color: #003087;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive design */
|
/* Responsive design */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.payment-options {
|
.payment-options {
|
||||||
@ -230,7 +322,7 @@ export default {
|
|||||||
min-width: 50px;
|
min-width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alipay-svg, .wechat-svg {
|
.alipay-svg, .wechat-svg, .paypal-svg {
|
||||||
max-height: 24px;
|
max-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -44,6 +44,7 @@ from jcloud.utils.billing import (
|
|||||||
from jcloud.utils.mpesa_utils import create_mpesa_request_log
|
from jcloud.utils.mpesa_utils import create_mpesa_request_log
|
||||||
from jcloud.api.payment.wechatpay import WeChatPayAPI
|
from jcloud.api.payment.wechatpay import WeChatPayAPI
|
||||||
from jcloud.api.payment.alipay import AlipayAPI
|
from jcloud.api.payment.alipay import AlipayAPI
|
||||||
|
from jcloud.api.payment.paypal import PayPalAPI
|
||||||
|
|
||||||
@jingrow.whitelist()
|
@jingrow.whitelist()
|
||||||
def get_publishable_key_and_setup_intent():
|
def get_publishable_key_and_setup_intent():
|
||||||
@ -1079,6 +1080,175 @@ def handle_wechatpay_notification():
|
|||||||
return "SUCCESS" # 返回成功避免微信重复发送通知
|
return "SUCCESS" # 返回成功避免微信重复发送通知
|
||||||
|
|
||||||
|
|
||||||
|
@jingrow.whitelist(allow_guest=True)
|
||||||
|
def handle_paypal_return():
|
||||||
|
"""处理PayPal支付成功后的返回"""
|
||||||
|
try:
|
||||||
|
# 获取PayPal返回的参数
|
||||||
|
params = jingrow.request.args
|
||||||
|
payment_id = params.get("paymentId")
|
||||||
|
token = params.get("token")
|
||||||
|
payer_id = params.get("PayerID")
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
jingrow.log_error("PayPal返回参数错误", "未获取到token")
|
||||||
|
# 跳转到充值失败页面
|
||||||
|
return jingrow.redirect(jingrow.utils.get_url("/dashboard/billing?payment_status=failed"))
|
||||||
|
|
||||||
|
# 根据token查找订单记录
|
||||||
|
payment_record = jingrow.db.get_value(
|
||||||
|
"Order",
|
||||||
|
{"order_id": token, "payment_method": "PayPal"},
|
||||||
|
["name", "total_amount", "status", "team"],
|
||||||
|
as_dict=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not payment_record:
|
||||||
|
jingrow.log_error(f"未找到PayPal支付记录: {token}", "PayPal返回处理")
|
||||||
|
return jingrow.redirect(jingrow.utils.get_url("/dashboard/billing?payment_status=failed"))
|
||||||
|
|
||||||
|
# 检查记录状态
|
||||||
|
if payment_record.status == "交易成功":
|
||||||
|
# 订单已处理,跳转到成功页面
|
||||||
|
return jingrow.redirect(jingrow.utils.get_url("/dashboard/billing?payment_status=success"))
|
||||||
|
|
||||||
|
# 更新订单记录状态为已支付
|
||||||
|
jingrow.db.set_value(
|
||||||
|
"Order",
|
||||||
|
payment_record.name,
|
||||||
|
{
|
||||||
|
"status": "已支付",
|
||||||
|
"trade_no": payment_id,
|
||||||
|
"payer_id": payer_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 执行支付完成后的业务逻辑
|
||||||
|
handle_order_payment_complete(token)
|
||||||
|
|
||||||
|
# 提交事务
|
||||||
|
jingrow.db.commit()
|
||||||
|
|
||||||
|
# 跳转到充值成功页面
|
||||||
|
return jingrow.redirect(jingrow.utils.get_url("/dashboard/billing?payment_status=success"))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
jingrow.log_error("PayPal返回处理错误", f"处理PayPal支付返回失败: {str(e)}")
|
||||||
|
return jingrow.redirect(jingrow.utils.get_url("/dashboard/billing?payment_status=failed"))
|
||||||
|
|
||||||
|
|
||||||
|
@jingrow.whitelist(allow_guest=True)
|
||||||
|
def handle_paypal_cancel():
|
||||||
|
"""处理PayPal支付取消的返回"""
|
||||||
|
try:
|
||||||
|
# 获取PayPal返回的参数
|
||||||
|
params = jingrow.request.args
|
||||||
|
token = params.get("token")
|
||||||
|
|
||||||
|
if token:
|
||||||
|
# 根据token查找订单记录
|
||||||
|
payment_record = jingrow.db.get_value(
|
||||||
|
"Order",
|
||||||
|
{"order_id": token, "payment_method": "PayPal"},
|
||||||
|
"name"
|
||||||
|
)
|
||||||
|
|
||||||
|
if payment_record:
|
||||||
|
# 更新订单记录状态为已取消
|
||||||
|
jingrow.db.set_value("Order", payment_record, "status", "已取消")
|
||||||
|
jingrow.db.commit()
|
||||||
|
|
||||||
|
# 跳转到充值页面
|
||||||
|
return jingrow.redirect(jingrow.utils.get_url("/dashboard/billing?payment_status=canceled"))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
jingrow.log_error("PayPal取消处理错误", f"处理PayPal支付取消失败: {str(e)}")
|
||||||
|
return jingrow.redirect(jingrow.utils.get_url("/dashboard/billing"))
|
||||||
|
|
||||||
|
|
||||||
|
@jingrow.whitelist(allow_guest=True)
|
||||||
|
def handle_paypal_webhook():
|
||||||
|
"""处理PayPal支付结果通知回调"""
|
||||||
|
try:
|
||||||
|
# 获取请求数据
|
||||||
|
headers = jingrow.request.headers
|
||||||
|
body = jingrow.request.get_data()
|
||||||
|
|
||||||
|
# 初始化PayPal API
|
||||||
|
paypal_api = PayPalAPI()
|
||||||
|
|
||||||
|
# 验证webhook通知
|
||||||
|
if not paypal_api.verify_webhook(headers, body):
|
||||||
|
jingrow.log_error("PayPal webhook验证失败", "PayPal Webhook")
|
||||||
|
return "FAIL"
|
||||||
|
|
||||||
|
# 解析通知数据
|
||||||
|
webhook_data = json.loads(body)
|
||||||
|
event_type = webhook_data.get("event_type")
|
||||||
|
resource = webhook_data.get("resource")
|
||||||
|
|
||||||
|
if not resource:
|
||||||
|
jingrow.log_error("PayPal webhook数据错误", "PayPal Webhook")
|
||||||
|
return "SUCCESS"
|
||||||
|
|
||||||
|
# 处理支付完成事件
|
||||||
|
if event_type == "CHECKOUT.ORDER.APPROVED":
|
||||||
|
order_id = resource.get("id")
|
||||||
|
|
||||||
|
# 根据订单ID查找本地订单记录
|
||||||
|
payment_record = jingrow.db.get_value(
|
||||||
|
"Order",
|
||||||
|
{"order_id": order_id, "payment_method": "PayPal"},
|
||||||
|
["name", "total_amount", "status", "team"],
|
||||||
|
as_dict=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not payment_record:
|
||||||
|
jingrow.log_error(f"未找到PayPal订单记录: {order_id}", "PayPal Webhook")
|
||||||
|
return "SUCCESS"
|
||||||
|
|
||||||
|
# 检查记录状态,避免重复处理
|
||||||
|
if payment_record.status == "交易成功":
|
||||||
|
return "SUCCESS"
|
||||||
|
|
||||||
|
# 捕获支付金额
|
||||||
|
try:
|
||||||
|
capture_result = paypal_api.capture_payment(order_id)
|
||||||
|
|
||||||
|
# 检查捕获状态
|
||||||
|
if capture_result.get("status") == "COMPLETED":
|
||||||
|
# 更新订单记录状态
|
||||||
|
jingrow.db.set_value(
|
||||||
|
"Order",
|
||||||
|
payment_record.name,
|
||||||
|
{
|
||||||
|
"status": "已支付",
|
||||||
|
"trade_no": capture_result.get("id"),
|
||||||
|
"payer_id": resource.get("payer", {}).get("payer_id")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 执行支付完成后的业务逻辑
|
||||||
|
handle_order_payment_complete(order_id)
|
||||||
|
|
||||||
|
# 提交事务
|
||||||
|
jingrow.db.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
jingrow.log_error(f"捕获PayPal支付失败: {str(e)}", "PayPal Webhook")
|
||||||
|
|
||||||
|
return "SUCCESS"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_message = (
|
||||||
|
"处理PayPal webhook失败:\n"
|
||||||
|
f"异常信息: {str(e)}\n"
|
||||||
|
f"堆栈信息:\n{traceback.format_exc()}\n"
|
||||||
|
)
|
||||||
|
jingrow.log_error(error_message, "PayPal Webhook Error")
|
||||||
|
return "FAIL"
|
||||||
|
|
||||||
|
|
||||||
def handle_order_payment_complete(order_id):
|
def handle_order_payment_complete(order_id):
|
||||||
"""处理订单支付完成后的业务逻辑"""
|
"""处理订单支付完成后的业务逻辑"""
|
||||||
try:
|
try:
|
||||||
@ -1189,9 +1359,7 @@ def process_site_renew(order_id):
|
|||||||
@jingrow.whitelist()
|
@jingrow.whitelist()
|
||||||
def check_payment_status(order_id, payment_type):
|
def check_payment_status(order_id, payment_type):
|
||||||
"""检查支付状态"""
|
"""检查支付状态"""
|
||||||
if payment_type == "alipay":
|
if payment_type in ["alipay", "wechatpay", "paypal"]:
|
||||||
pagetype = "Order"
|
|
||||||
elif payment_type == "wechatpay":
|
|
||||||
pagetype = "Order"
|
pagetype = "Order"
|
||||||
else:
|
else:
|
||||||
jingrow.throw("不支持的支付类型")
|
jingrow.throw("不支持的支付类型")
|
||||||
@ -1319,6 +1487,52 @@ def create_wechatpay_order_for_recharge(amount):
|
|||||||
jingrow.throw(f"创建微信支付订单失败")
|
jingrow.throw(f"创建微信支付订单失败")
|
||||||
|
|
||||||
|
|
||||||
|
@jingrow.whitelist()
|
||||||
|
def create_paypal_order_for_recharge(amount):
|
||||||
|
"""创建PayPal订单用于购买预付费信用额度"""
|
||||||
|
team = get_current_team(True)
|
||||||
|
total_amount = round(float(amount), 2)
|
||||||
|
|
||||||
|
# 生成唯一订单号
|
||||||
|
order_id = datetime.now().strftime('%Y%m%d%H%M%S%f')[:-3] + ''.join(random.choices('0123456789', k=6))
|
||||||
|
|
||||||
|
# 创建订单记录
|
||||||
|
payment_record = jingrow.get_pg({
|
||||||
|
"pagetype": "Order",
|
||||||
|
"order_id": order_id,
|
||||||
|
"order_type": "余额充值",
|
||||||
|
"team": team.name,
|
||||||
|
"total_amount": float(total_amount),
|
||||||
|
"status": "待支付",
|
||||||
|
"payment_method": "PayPal"
|
||||||
|
})
|
||||||
|
|
||||||
|
payment_record.insert(ignore_permissions=True)
|
||||||
|
jingrow.db.commit()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 使用PayPalAPI类生成支付链接
|
||||||
|
api = PayPalAPI()
|
||||||
|
|
||||||
|
# 生成支付链接,使用默认的返回和取消URL
|
||||||
|
payment_url = api.generate_payment_url(
|
||||||
|
order_id=order_id,
|
||||||
|
amount=total_amount,
|
||||||
|
subject="Jingrow 余额充值",
|
||||||
|
team_name=team.name
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"payment_url": payment_url,
|
||||||
|
"order_id": order_id,
|
||||||
|
"payment_record": payment_record.name
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
jingrow.log_error("PayPal支付错误", f"创建PayPal订单失败: {str(e)}")
|
||||||
|
jingrow.throw(f"创建PayPal订单失败")
|
||||||
|
|
||||||
|
|
||||||
@jingrow.whitelist()
|
@jingrow.whitelist()
|
||||||
def create_order(**kwargs):
|
def create_order(**kwargs):
|
||||||
"""创建站点订单"""
|
"""创建站点订单"""
|
||||||
|
|||||||
221
jcloud/api/payment/paypal.py
Normal file
221
jcloud/api/payment/paypal.py
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import jingrow
|
||||||
|
import traceback
|
||||||
|
import urllib.parse
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class PayPalAPI:
|
||||||
|
def __init__(self):
|
||||||
|
# 初始化时不立即加载设置
|
||||||
|
self.client_id = None
|
||||||
|
self.client_secret = None
|
||||||
|
self.server_url = None
|
||||||
|
self.notify_url = None
|
||||||
|
self.sandbox = False
|
||||||
|
self.access_token = None
|
||||||
|
self.token_expires_at = None
|
||||||
|
|
||||||
|
def _initialize(self):
|
||||||
|
# 每次都重新加载配置,确保使用最新设置
|
||||||
|
# 从 Jcloud Settings 获取配置
|
||||||
|
settings = jingrow.get_single("Jcloud Settings")
|
||||||
|
|
||||||
|
# 配置PayPal客户端
|
||||||
|
self.client_id = settings.paypal_client_id
|
||||||
|
self.client_secret = settings.paypal_client_secret
|
||||||
|
self.server_url = settings.paypal_server_url
|
||||||
|
self.notify_url = settings.paypal_notify_url
|
||||||
|
self.sandbox = settings.paypal_sandbox
|
||||||
|
|
||||||
|
# 重置访问令牌
|
||||||
|
self.access_token = None
|
||||||
|
self.token_expires_at = None
|
||||||
|
|
||||||
|
def _get_access_token(self):
|
||||||
|
"""获取PayPal API访问令牌"""
|
||||||
|
# 检查令牌是否有效
|
||||||
|
if self.access_token and self.token_expires_at and datetime.now() < self.token_expires_at:
|
||||||
|
return self.access_token
|
||||||
|
|
||||||
|
# 构建获取令牌的URL
|
||||||
|
token_url = f"{self.server_url}/v1/oauth2/token"
|
||||||
|
|
||||||
|
# 准备请求数据
|
||||||
|
data = {
|
||||||
|
"grant_type": "client_credentials"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 发送请求
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
token_url,
|
||||||
|
data=data,
|
||||||
|
auth=(self.client_id, self.client_secret),
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded"}
|
||||||
|
)
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
token_data = response.json()
|
||||||
|
|
||||||
|
# 保存访问令牌和过期时间
|
||||||
|
self.access_token = token_data["access_token"]
|
||||||
|
|
||||||
|
# 计算过期时间(提前5分钟过期)
|
||||||
|
expires_in = token_data["expires_in"] - 300
|
||||||
|
self.token_expires_at = datetime.now() + jingrow.utils.relativedelta(seconds=expires_in)
|
||||||
|
|
||||||
|
return self.access_token
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_message = (
|
||||||
|
"获取PayPal访问令牌失败:\n"
|
||||||
|
f"异常信息: {str(e)}\n"
|
||||||
|
f"堆栈信息:\n{traceback.format_exc()}\n"
|
||||||
|
)
|
||||||
|
jingrow.log_error(error_message, "PayPal Token Error")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def generate_payment_url(self, order_id, amount, subject, team_name, return_url=None, cancel_url=None):
|
||||||
|
self._initialize()
|
||||||
|
|
||||||
|
# 获取访问令牌
|
||||||
|
access_token = self._get_access_token()
|
||||||
|
|
||||||
|
# 构建创建订单的URL
|
||||||
|
create_order_url = f"{self.server_url}/v2/checkout/orders"
|
||||||
|
|
||||||
|
# 构建请求内容
|
||||||
|
order_data = {
|
||||||
|
"intent": "CAPTURE",
|
||||||
|
"purchase_units": [{
|
||||||
|
"amount": {
|
||||||
|
"currency_code": "USD", # 目前只支持USD
|
||||||
|
"value": str(amount)
|
||||||
|
},
|
||||||
|
"description": subject
|
||||||
|
}],
|
||||||
|
"application_context": {
|
||||||
|
"return_url": return_url or f"{jingrow.utils.get_url()}/api/action/jcloud.api.billing.handle_paypal_return",
|
||||||
|
"cancel_url": cancel_url or f"{jingrow.utils.get_url()}/api/action/jcloud.api.billing.handle_paypal_cancel",
|
||||||
|
"brand_name": "Jingrow",
|
||||||
|
"user_action": "PAY_NOW",
|
||||||
|
"shipping_preference": "NO_SHIPPING"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 发送创建订单请求
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
create_order_url,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {access_token}"
|
||||||
|
},
|
||||||
|
json=order_data
|
||||||
|
)
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
order_response = response.json()
|
||||||
|
|
||||||
|
# 提取支付链接
|
||||||
|
approval_url = None
|
||||||
|
for link in order_response.get("links", []):
|
||||||
|
if link["rel"] == "approve":
|
||||||
|
approval_url = link["href"]
|
||||||
|
break
|
||||||
|
|
||||||
|
if not approval_url:
|
||||||
|
raise Exception("未找到PayPal支付链接")
|
||||||
|
|
||||||
|
return approval_url
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_message = (
|
||||||
|
"创建PayPal订单失败:\n"
|
||||||
|
f"异常信息: {str(e)}\n"
|
||||||
|
f"请求数据: {json.dumps(order_data)}\n"
|
||||||
|
f"响应内容: {response.text if 'response' in locals() else '无响应'}\n"
|
||||||
|
f"堆栈信息:\n{traceback.format_exc()}\n"
|
||||||
|
)
|
||||||
|
jingrow.log_error(error_message, "PayPal Order Error")
|
||||||
|
|
||||||
|
# 如果API调用失败,返回模拟链接用于测试
|
||||||
|
jingrow.log_error("使用模拟PayPal支付链接", "PayPal Fallback")
|
||||||
|
return f"https://www.paypal.com/checkoutnow?token={order_id}"
|
||||||
|
|
||||||
|
def verify_webhook(self, headers, body):
|
||||||
|
"""验证PayPal webhook通知"""
|
||||||
|
self._initialize()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 获取签名和证书
|
||||||
|
transmission_id = headers.get("PAYPAL-TRANSMISSION-ID")
|
||||||
|
transmission_time = headers.get("PAYPAL-TRANSMISSION-TIME")
|
||||||
|
cert_url = headers.get("PAYPAL-CERT-URL")
|
||||||
|
auth_algo = headers.get("PAYPAL-AUTH-ALGO")
|
||||||
|
transmission_sig = headers.get("PAYPAL-TRANSMISSION-SIG")
|
||||||
|
|
||||||
|
# 验证必要头信息
|
||||||
|
if not all([transmission_id, transmission_time, cert_url, auth_algo, transmission_sig]):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 获取PayPal公钥证书
|
||||||
|
response = requests.get(cert_url)
|
||||||
|
response.raise_for_status()
|
||||||
|
public_key = response.text
|
||||||
|
|
||||||
|
# 构建用于验证的字符串
|
||||||
|
webhook_id = jingrow.db.get_single_value("Jcloud Settings", "paypal_webhook_id") or ""
|
||||||
|
body_bytes = body if isinstance(body, bytes) else body.encode("utf-8")
|
||||||
|
|
||||||
|
# 注意:完整的验证实现需要使用加密库验证签名
|
||||||
|
# 这里简化处理,实际项目中需要实现完整的签名验证
|
||||||
|
jingrow.log_error(f"PayPal webhook验证: {transmission_id}", "PayPal Webhook")
|
||||||
|
|
||||||
|
# 返回True表示验证成功(简化处理)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_message = (
|
||||||
|
"验证PayPal webhook失败:\n"
|
||||||
|
f"异常信息: {str(e)}\n"
|
||||||
|
f"堆栈信息:\n{traceback.format_exc()}\n"
|
||||||
|
)
|
||||||
|
jingrow.log_error(error_message, "PayPal Webhook Error")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def capture_payment(self, order_id):
|
||||||
|
"""捕获PayPal支付金额"""
|
||||||
|
self._initialize()
|
||||||
|
|
||||||
|
# 获取访问令牌
|
||||||
|
access_token = self._get_access_token()
|
||||||
|
|
||||||
|
# 构建捕获支付的URL
|
||||||
|
capture_url = f"{self.server_url}/v2/checkout/orders/{order_id}/capture"
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
capture_url,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {access_token}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
capture_data = response.json()
|
||||||
|
|
||||||
|
return capture_data
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_message = (
|
||||||
|
f"捕获PayPal支付失败 (Order ID: {order_id}):\n"
|
||||||
|
f"异常信息: {str(e)}\n"
|
||||||
|
f"响应内容: {response.text if 'response' in locals() else '无响应'}\n"
|
||||||
|
f"堆栈信息:\n{traceback.format_exc()}\n"
|
||||||
|
)
|
||||||
|
jingrow.log_error(error_message, "PayPal Capture Error")
|
||||||
|
raise
|
||||||
|
|
||||||
@ -31,6 +31,12 @@
|
|||||||
"column_break_alipay",
|
"column_break_alipay",
|
||||||
"alipay_app_private_key",
|
"alipay_app_private_key",
|
||||||
"alipay_public_key",
|
"alipay_public_key",
|
||||||
|
"paypal_settings_section",
|
||||||
|
"paypal_server_url",
|
||||||
|
"paypal_client_id",
|
||||||
|
"paypal_client_secret",
|
||||||
|
"paypal_notify_url",
|
||||||
|
"paypal_sandbox",
|
||||||
"wechatpay_settings_section",
|
"wechatpay_settings_section",
|
||||||
"wechatpay_appid",
|
"wechatpay_appid",
|
||||||
"wechatpay_mchid",
|
"wechatpay_mchid",
|
||||||
@ -1465,6 +1471,44 @@
|
|||||||
"fieldtype": "Long Text",
|
"fieldtype": "Long Text",
|
||||||
"label": "Alipay Public Key"
|
"label": "Alipay Public Key"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "paypal_settings_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "PayPal Settings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "https://api-m.paypal.com",
|
||||||
|
"description": "PayPal API服务器地址,沙盒环境使用:https://api-m.sandbox.paypal.com",
|
||||||
|
"fieldname": "paypal_server_url",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "PayPal Server URL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "PayPal客户端ID",
|
||||||
|
"fieldname": "paypal_client_id",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "PayPal Client ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "PayPal客户端密钥",
|
||||||
|
"fieldname": "paypal_client_secret",
|
||||||
|
"fieldtype": "Password",
|
||||||
|
"label": "PayPal Client Secret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "PayPal支付结果通知地址",
|
||||||
|
"fieldname": "paypal_notify_url",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "PayPal Notify URL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "是否使用沙盒环境",
|
||||||
|
"fieldname": "paypal_sandbox",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "PayPal Sandbox Mode"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "wechatpay_settings_section",
|
"fieldname": "wechatpay_settings_section",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user