功能概览
- 商户入驻(Partner Referrals)
- 保存并管理 paypal_merchant_id
- 创建订单(支持指定收款商户 + 平台抽佣)
- 引导用户跳转 PayPal 支付 + 支付结果处理
- 订单 Capture(收款)
- Webhook 验签(支付成功/退款等事件)
- 多环境(Sandbox / Live)切换
目录结构(建议)
paypal-multitenant/
├─ pom.xml
├─ src/main/java/com/example/paypal/
│ ├─ PayPalApplication.java
│ ├─ config/
│ │ ├─ AppProperties.java
│ │ └─ PayPalConfig.java
│ ├─ core/
│ │ ├─ PayPalApiClient.java
│ │ └─ WebhookVerifier.java
│ ├─ merchant/
│ │ ├─ MerchantEntity.java
│ │ ├─ MerchantRepository.java
│ │ └─ MerchantService.java
│ ├─ order/
│ │ ├─ PayOrderEntity.java
│ │ ├─ PayOrderRepository.java
│ │ ├─ OrderService.java
│ │ └─ dto/
│ │ ├─ CreateOrderRequest.java
│ │ ├─ CreateOrderResponse.java
│ │ └─ CaptureResponse.java
│ ├─ web/
│ │ ├─ OnboardingController.java
│ │ ├─ OrderController.java
│ │ └─ WebhookController.java
│ └─ util/Jsons.java
└─ src/main/resources/
├─ application.yml
└─ logback-spring.xml (可选)
PayPal 客户端 PayPalApiClient.java
package com.example.paypal.core;
import com.example.paypal.config.AppProperties;
import lombok.RequiredArgsConstructor;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@Component
@RequiredArgsConstructor
public class PayPalApiClient {
private final AppProperties props;
public String accessToken() throws Exception {
var url = props.getPaypal().getApiBase() + "/v1/oauth2/token";
try (CloseableHttpClient client = HttpClients.createDefault()) {
HttpPost post = new HttpPost(url);
String basic = Base64.getEncoder().encodeToString((
props.getPaypal().getClientId() + ":" + props.getPaypal().getClientSecret()
).getBytes(StandardCharsets.UTF_8));
post.addHeader(HttpHeaders.AUTHORIZATION, "Basic " + basic);
post.addHeader("Accept", "application/json");
post.addHeader("Accept-Language", "en_US");
post.setEntity(new StringEntity("grant_type=client_credentials", ContentType.APPLICATION_FORM_URLENCODED));
return Http.execAndRead(client, post);
}
}
public String post(String path, String jsonBody, String bearer) throws Exception {
String url = props.getPaypal().getApiBase() + path;
try (CloseableHttpClient client = HttpClients.createDefault()) {
HttpPost post = new HttpPost(url);
decorate(post, bearer);
post.setEntity(new StringEntity(jsonBody, ContentType.APPLICATION_JSON));
return Http.execAndRead(client, post);
}
}
private void decorate(HttpUriRequestBase req, String bearer) {
req.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + bearer);
req.addHeader("Content-Type", "application/json");
if (props.getPaypal().getPartnerAttributionId() != null) {
req.addHeader("PayPal-Partner-Attribution-Id", props.getPaypal().getPartnerAttributionId());
}
}
// 简化 http 读写
static class Http {
static String execAndRead(CloseableHttpClient client, HttpUriRequestBase req) throws Exception {
return client.execute(req, res -> new String(res.getEntity().getContent().readAllBytes(), StandardCharsets.UTF_8));
}
}
}