赞
踩
做全球性的支付,选用paypal!为什么选择paypal? 因为paypal是目前全球最大的在线支付工具,就像国内的支付宝一样,是一个基于买卖双方的第三方平台。买家只需知道你的paypal账号,即可在线直接把钱汇入你的账户,即时到账,简单方便快捷。
查看进入支付页面的历史记录:
在集成paypal支付接口之前,首先要有一系列的准备,开发者账号啊、sdk、测试环境等等先要有,然后再码代码。集成的步骤如下:
PayPal开发者:https://developer.paypal.com/dashboard/accounts
sandbox测试账号登录的测试网址:https://www.sandbox.paypal.com
一、环境准备
注册paypal账号
注册paypal开发者账号
创建两个测试用户
创建应用,生成用于测试的clientID 和 密钥
二、代码集成
springboot环境
pom引进paypal-sdk的jar包
码代码
测试
目录
现在开始
(1)在浏览器输入“安全海淘国际支付平台_安全收款外贸平台-PayPal CN” 跳转到如下界面,点击右上角的注册
(2)选择,”创建商家用户”,根据要求填写信息,一分钟的事,注册完得去邮箱激活
(1)在浏览器输入“https://developer.paypal.com”,点击右上角的“Log into Dashboard”,用上一步创建好的账号登录
(1)登录成功后,在左边的导航栏中点击 Sandbox 下的 Accounts
(2)进入Acccouts界面后,可以看到系统有两个已经生成好的测试账号,但是我们不要用系统给的测试账号,很卡的,自己创建两个
(3)点击右上角的“Create Account”,创建测试用户
<1> 先创建一个“ PERSONAL”类型的用户,国家一定要选“China”,账户余额自己填写
<2> 接着创建一个“BUSINESS”类型的用户,国家一定要选“China”,账户余额自己填写
<3>创建好之后可以点击测试账号下的”Profile“,可以查看信息,如果没加载出来,刷新
<4>用测试账号登录测试网站查看,注意!这跟paypal官网不同!不是同一个地址,在浏览器输入:安全海淘国际支付平台_安全收款外贸平台-PayPal CN 在这里登陆测试账户
(1)点击左边导航栏Dashboard下的My Apps & Credentials,创建一个Live账号,下图是我已经创建好的
(2)然后再到下边创建App
这是我创建好的“Test”App
(3)点击刚刚创建好的App“Test”,注意看到”ClientID“ 和”Secret“(Secret如果没显示,点击下面的show就会看到,点击后show变为hide)
(1)新建几个包,和目录,项目结构如下
- <dependency>
- <groupId>com.paypal.sdk</groupId>
- <artifactId>rest-api-sdk</artifactId>
- <version>1.4.2</version>
- </dependency>
- <dependency>
- <groupId>com.paypal.sdk</groupId>
- <artifactId>rest-api-sdk</artifactId>
- <version>1.4.2</version>
- </dependency>
- <dependency>
- <groupId>com.paypal.sdk</groupId>
- <artifactId>checkout-sdk</artifactId>
- <version>1.0.2</version>
- </dependency>
- package com.masasdani.paypal;
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
- import org.springframework.context.annotation.ComponentScan;
- import org.springframework.context.annotation.Configuration;
-
- @EnableAutoConfiguration
- @Configuration
- @ComponentScan
- public class Application {
-
- public static void main(String[] args) {
- SpringApplication.run(Application.class, args);
- }
- }
- package com.masasdani.paypal.config;
- import java.util.HashMap;import java.util.Map;
- import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;
- import com.paypal.base.rest.APIContext;import com.paypal.base.rest.OAuthTokenCredential;import com.paypal.base.rest.PayPalRESTException;
-
- @Configurationpublic class PaypalConfig {
-
- @Value("${paypal.client.app}")
- private String clientId;
-
- @Value("${paypal.client.secret}")
- private String clientSecret;
-
- @Value("${paypal.mode}")
- private String mode;
-
- @Bean
- public Map<String, String> paypalSdkConfig(){
- Map<String, String> sdkConfig = new HashMap<>();
- sdkConfig.put("mode", mode);
- return sdkConfig;
- }
-
-
- @Bean
- public OAuthTokenCredential authTokenCredential(){
- return new OAuthTokenCredential(clientId, clientSecret, paypalSdkConfig());
- }
-
-
- @Bean
- public APIContext apiContext() throws PayPalRESTException{
- APIContext apiContext = new APIContext(authTokenCredential().getAccessToken());
- apiContext.setConfigurationMap(paypalSdkConfig());
- return apiContext;
- }
-
- }
在 PayPal API 中,PaypalPaymentIntent 表示支付的意图或目的。
以下是 PayPal Payment Intent 的四种可能取值:
- package com.masasdani.paypal.config;
- public enum PaypalPaymentIntent {
- sale, authorize, order
- }
在 PayPal API 中,PaypalPaymentMethod 表示支付所使用的支付方式。
以下是 PayPal Payment Method 的一些可能取值:
- package com.masasdani.paypal.config;
- public enum PaypalPaymentMethod {
- credit_card, paypal
- }
- package com.masasdani.paypal.controller;
- import javax.servlet.http.HttpServletRequest;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RequestMethod;
- import org.springframework.web.bind.annotation.RequestParam;
- import com.masasdani.paypal.config.PaypalPaymentIntent;
- import com.masasdani.paypal.config.PaypalPaymentMethod;
- import com.masasdani.paypal.service.PaypalService;
- import com.masasdani.paypal.util.URLUtils;
- import com.paypal.api.payments.Links;
- import com.paypal.api.payments.Payment;
- import com.paypal.base.rest.PayPalRESTException;
-
- @Controller
- @RequestMapping("/")
- public class PaymentController {
-
- public static final String PAYPAL_SUCCESS_URL = "pay/success";
-
- public static final String PAYPAL_CANCEL_URL = "pay/cancel";
-
- private Logger log = LoggerFactory.getLogger(getClass());
-
- @Autowired
- private PaypalService paypalService;
-
- @RequestMapping(method = RequestMethod.GET)
- public String index(){
- return "index";
- }
-
- @RequestMapping(method = RequestMethod.POST, value = "pay")
- public String pay(HttpServletRequest request){ // RestFul风格里也能传入HttpServletRequest
-
- String cancelUrl = URLUtils.getBaseURl(request) + "/" + PAYPAL_CANCEL_URL;
- String successUrl = URLUtils.getBaseURl(request) + "/" + PAYPAL_SUCCESS_URL;
-
- try {
- Payment payment = paypalService.createPayment(
- 500.00,
- "USD",
- PaypalPaymentMethod.paypal,
- PaypalPaymentIntent.sale,
- "payment description",
- cancelUrl,
- successUrl);
-
- for(Links links : payment.getLinks()){
-
- if(links.getRel().equals("approval_url")){
- // 输出跳转到paypal支付页面的网站,eg:https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=EC-8HL7026676482401S
- logger.info("links is {}", links.getHref());
- return "redirect:" + links.getHref();
- }
- }
- } catch (PayPalRESTException e) {
- // PayPalRESTException是PayPal的REST API客户端中的一个异常类,它用于处理与PayPal REST API相关的异常情况
- // 当使用 PayPal 的 REST API 进行付款处理、交易查询、退款请求等操作时,如果发生错误或异常情况,PayPalRestException 将被抛出,以便开发者可以捕获并处理这些异常
- log.error(e.getMessage());
- }
- return "redirect:/";
- }
-
- @RequestMapping(method = RequestMethod.GET, value = PAYPAL_CANCEL_URL)
- public String cancelPay(){
- return "cancel";
- }
-
-
- @RequestMapping(method = RequestMethod.GET, value = PAYPAL_SUCCESS_URL)
- public String successPay(@RequestParam("paymentId") String paymentId, @RequestParam("PayerID") String payerId){ // 一定是PayerID,PayPal通常使用"PayerID"(ID和P都大小写)作为参数名称
-
- try {
-
- Payment payment = paypalService.executePayment(paymentId, payerId);
- if(payment.getState().equals("approved")){
- log.info("支付成功Payment:" + payment);
- return "success";
- }
-
- } catch (PayPalRESTException e) {
- log.error(e.getMessage());
- }
- return "redirect:/";
- }
- }
PayPalRESTException
类中包含以下常见的属性:
getMessage()
: 返回异常的详细错误消息。getResponseCode()
: 返回 HTTP 响应的状态码。getDetails()
: 返回一个 ErrorDetails
对象,其中包含有关异常的更多详细信息。getInformationLink()
: 返回一个 URL,提供有关异常的更多信息和解决方案的链接。PayPalRESTException
是 PayPal REST API SDK 中的一个异常类,用于处理与 PayPal REST API 请求和响应相关的错误和异常情况。这个异常类通常用于开发者在使用 PayPal REST API 时捕获和处理错误,包括但不限于如下作用:
认证错误:当提供的 PayPal 客户端ID和秘密无效或过期时,可能会引发异常。
请求错误:如果请求的数据不符合 PayPal REST API 的要求,如无效的金额、货币或请求格式,也可能引发异常。
支付处理错误:在创建支付、执行支付或查询支付状态时,可能会出现与支付相关的问题,例如支付已取消或已拒绝等情况。
通信问题:如果与 PayPal 服务器之间的通信中断或出现问题,也可以引发异常。
认证错误:
无效的客户端ID或客户端秘密:如果你提供的 PayPal 客户端ID或客户端秘密无效,可能会抛出认证错误。
PayPalRESTException: Authentication failed with the provided credentials.
请求错误:
PayPalRESTException: Request failed with status code 400 and message: Currency is not supported.
无效的金额:如果请求中的金额不合法,也可能引发请求错误。
PayPalRESTException: Request failed with status code 400 and message: Invalid total amount.
支付处理错误:
PayPalRESTException: Payment has been canceled by the user.
PayPalRESTException: Payment has been declined by PayPal.
网络问题:
网络连接问题:当发生网络连接问题时,可能会引发网络异常。
PayPalRESTException: Network error: Connection timed out.
服务器通信错误:如果 PayPal 服务器无法响应请求,也可能引发异常。
PayPalRESTException: Network error: Unable to communicate with PayPal servers.
注意:以下是两种不同的处理异常的方式!
1. 使用 try-catch 块:
· 作用:try-catch
块用于捕获和处理可能发生的异常,以便在异常发生时采取适当的措施,如记录错误、向用户提供错误消息或执行其他操作。它允许你在同一方法内部处理异常,避免异常的传播到上层调用层次。
· 使用场景:使用 try-catch
块来处理已知的、可以预测的异常,以确保程序能够正常继续执行,而不会中断。适用于在方法内部处理异常并采取特定的操作。
- 对于try { } catch (Exception e) { } 的 catch 中的处理方式有两种:e.printStackTrace() 与 log.error(e.getMessage()):
- e.printStackTrace()
是异常对象 e
的一个方法,它用于将异常的堆栈跟踪信息打印到标准错误流(通常是控制台)。
这种方式通常用于调试目的,以便开发者可以看到详细的异常信息,包括异常发生的位置和方法调用链。这会输出异常的完整堆栈跟踪,包括方法名、类名、行号等信息。
- log.error()
是一种记录日志的方式,通常使用日志框架(如Log4j、Logback、SLF4J等)提供的方法来记录异常信息。
e.getMessage()
只获取异常的消息部分,通常包含异常的简要说明。这种方式更适用于生产环境,以便将异常信息记录到日志文件中,以便后续的故障排除和监控。
- Logger log = LoggerFactory.getLogger(getClass());
- 或者:
- Logger log = LoggerFactory.getLogger(YourClass.class);
2. 使用 throw 关键字抛出异常:
· 作用:throw
关键字用于主动抛出异常,将异常传递到方法的调用者,以便在上层调用层次中进行处理。它用于表示方法无法处理异常,需要由调用者来处理。(用于将异常传播到调用方的方式,而不是用于捕获和处理异常)
· 使用场景:使用 throw
来通知调用者方法内部发生了异常,并将异常传递给调用者,由调用者负责处理异常。适用于方法内部无法处理的异常,或者需要将异常传递给上层调用层次的情况。
PayPalRESTException
提供了错误的详细信息,包括错误消息、错误代码、响应状态等,开发者可以使用这些信息来识别问题的性质和原因,并采取适当的措施来处理异常情况。
PayPalRESTException的使用:
- import com.paypal.api.payments.*;
- import com.paypal.base.rest.APIContext;
- import com.paypal.base.rest.PayPalRESTException;
-
- public class PayPalExample {
- public static void main(String[] args) {
- // 设置 PayPal REST API 的认证信息
- String clientId = "YOUR_CLIENT_ID";
- String clientSecret = "YOUR_CLIENT_SECRET";
- APIContext apiContext = new APIContext(clientId, clientSecret, "sandbox");
-
- // 创建一个 PayPal 支付对象
- Payment payment = new Payment();
- // 设置支付相关信息,例如总金额、货币等
- Amount amount = new Amount();
- amount.setCurrency("USD");
- amount.setTotal("100.00");
- payment.setAmount(amount);
-
- // 设置支付的执行链接
- RedirectUrls redirectUrls = new RedirectUrls();
- redirectUrls.setReturnUrl("http://example.com/return");
- redirectUrls.setCancelUrl("http://example.com/cancel");
- payment.setRedirectUrls(redirectUrls);
-
- // 设置支付方式为PayPal
- payment.setIntent("sale");
- payment.setPayer(new Payer("paypal"));
-
- try {
- Payment createdPayment = payment.create(apiContext);
- // 创建支付请求,并处理响应
- } catch (PayPalRESTException e) {
- // 处理异常情况
- System.err.println(e.getDetails());
- }
- }
- }
如何根据异常的消息不同返回不同的文字给 ResponseEntity :
- import org.springframework.http.ResponseEntity;
- import org.springframework.http.HttpStatus;
- import com.paypal.base.rest.PayPalRESTException;
-
- public ResponseEntity<String> processPayPalPayment() {
- try {
- // 执行 PayPal 支付操作
- // ...
-
- // 如果一切正常,返回成功响应
- return ResponseEntity.status(HttpStatus.OK).body("支付成功");
- } catch (PayPalRESTException e) {
- String errorMessage = e.getMessage();
-
- if (errorMessage.contains("Authentication failed with the provided credentials")) {
- // 无效的客户端ID或客户端秘密
- return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("认证失败");
- } else if (errorMessage.contains("Currency is not supported")) {
- // 无效的货币
- return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("无效的货币");
- } else if (errorMessage.contains("Invalid total amount")) {
- // 无效的金额
- return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("无效的金额");
- } else if (errorMessage.contains("Payment has been canceled by the user")) {
- // 支付已取消
- return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body("支付已取消");
- } else if (errorMessage.contains("Payment has been declined by PayPal")) {
- // 支付已拒绝
- return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body("支付已拒绝");
- } else if (errorMessage.contains("Network error: Connection timed out")) {
- // 网络连接问题
- return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body("网络连接超时");
- } else {
- // 其他异常情况
- return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("支付失败,请联系客服");
- }
- }
- }
HttpStatus 是 Spring Framework 提供了一组常用的 HTTP 状态码,以便在构建 RESTful API 或 Web应用程序时使用。这些常用的状态码位于 org.springframework.http.HttpStatus
枚举中,包括 200 OK、201 CREATED、400 BAD_REQUEST、401 UNAUTHORIZED、404 NOT_FOUND、500 INTERNAL_SERVER_ERROR 等等。你可以直接使用这些常量来设置响应的状态码,而无需自行定义。也可以直接:
return new CommonResult(e.getResponsecode(), e.getMessage(), null);
- package com.masasdani.paypal.service;
- import java.util.ArrayList;
- import java.util.List;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
- import com.masasdani.paypal.config.PaypalPaymentIntent;
- import com.masasdani.paypal.config.PaypalPaymentMethod;
- import com.paypal.api.payments.Amount;
- import com.paypal.api.payments.Payer;
- import com.paypal.api.payments.Payment; // 并不是自己写的实体类Payment
- import com.paypal.api.payments.PaymentExecution;
- import com.paypal.api.payments.RedirectUrls;
- import com.paypal.api.payments.Transaction;
- import com.paypal.base.rest.APIContext;
- import com.paypal.base.rest.PayPalRESTException;
-
- @Service
- public class PaypalService {
-
- @Autowired
- private APIContext apiContext;
-
- // 创建支付
- public Payment createPayment(
- Double total,
- String currency,
- PaypalPaymentMethod method,
- PaypalPaymentIntent intent,
- String description,
- String cancelUrl,
- String successUrl) throws PayPalRESTException{
- // 接受参数包括总金额(total)、货币类型(currency)、支付方法(method)、支付意图(intent)、描述(description)、取消 URL(cancelUrl)和成功 URL(successUrl)。在方法内部,它使用这些参数创建一个支付请求,并返回创建的 Payment 对象
-
- Amount amount = new Amount();
- amount.setCurrency(currency);
- amount.setTotal(String.format("%.2f", total));
-
- Transaction transaction = new Transaction();
- transaction.setDescription(description);
- transaction.setAmount(amount);
-
- List<Transaction> transactions = new ArrayList<>();
- transactions.add(transaction);
-
- Payer payer = new Payer();
- payer.setPaymentMethod(method.toString());
-
- Payment payment = new Payment();
- payment.setIntent(intent.toString());
- payment.setPayer(payer);
- payment.setTransactions(transactions);
-
- RedirectUrls redirectUrls = new RedirectUrls();
-
- // Paypal取消支付回调链接
- redirectUrls.setCancelUrl(cancelUrl);
-
- // Paypal付完款回调链接
- redirectUrls.setReturnUrl(successUrl);
-
- // Paypal付完款回调链接:如果要其他数据作为参数传递给成功支付后的回调URL即控制器类中的successPay方法,则对回调URL进行拼接:redirectUrls.setReturnUrl(successUrl + "?param1=" + param1 + "¶m2=" + param2 + "¶m3=" + paaram3);
- redirectUrls.setReturnUrl(successUrl + "?userId=" + entity.getUserId() + "&totalFee=" + entity.getTotalFee() + "&payFrom=" + entity.getPayFrom());
-
- payment.setRedirectUrls(redirectUrls);
-
- return payment.create(apiContext);
- }
-
- // 执行支付
- public Payment executePayment(String paymentId, String payerId) throws PayPalRESTException{
-
- // 接受支付 ID(paymentId)和付款人 ID(payerId)作为参数。在方法内部,它使用这些参数创建一个 PaymentExecution 对象,并使用支付 ID 和付款人 ID 执行支付请求,返回执行支付后的 Payment 对象
- Payment payment = new Payment();
- payment.setId(paymentId);
-
- PaymentExecution paymentExecute = new PaymentExecution();
- paymentExecute.setPayerId(payerId);
-
- return payment.execute(apiContext, paymentExecute);
- }
- }
- package com.masasdani.paypal.util;
- import javax.servlet.http.HttpServletRequest;
- public class URLUtils {
-
- public static String getBaseURl(HttpServletRequest request) {
-
- String scheme = request.getScheme(); // 获取请求的协议,如 "http" 或 "https"
- String serverName = request.getServerName(); // 获取服务器名称
- int serverPort = request.getServerPort();// 获取服务器端口
- String contextPath = request.getContextPath();// 获取上下文路径
-
- // 建议在此处插入以下代码查看上下文路径是否符合期望,因为有时候可能部署之类导致不对
- System.out.println("contextPanth: " + contextPath);
- StringBuffer url = new StringBuffer();
- url.append(scheme).append("://").append(serverName);
-
- // 判断服务器端口是否为标准的 HTTP(80)或 HTTPS(443)端口,决定是否将端口号添加到 URL 中
-
- // 当浏览器发起 HTTP 请求时,通常会使用默认的端口号,即 HTTP 使用 80 端口,HTTPS 使用 443 端口。这些端口号是默认的,因此在 URL 中不需要显式指定
- // 然而,如果应用程序部署在非默认的端口上,例如使用自定义的端口号(例如 8080、3000 等),则需要将该端口号包含在 URL 中,以确保浏览器能够正确地访问应用程序
-
- if ((serverPort != 80) && (serverPort != 443)) {
- url.append(":").append(serverPort);
- }
- url.append(contextPath);
-
- if(url.toString().endsWith("/")){ // URL 是否以斜杠结尾,如果不是,则添加斜杠
- url.append("/");
- }
- return url.toString(); // 获取当前请求的完整 URL
- }
- }
比如我的controller中的cancelUrl与successUrl就需要改成这样,因为在上面方法这种获取到的context为空,重定向之后就会报404路径不存在,可能是其他配置问题:
先生成预订单,将 url 返回给前端,前端进行跳转,之后即可获得取消/成功回调。
- package com.harmony.supreme.modular.paypal.controller;
- import com.harmony.supreme.modular.paypal.entity.PaypalOrderParam;
- import com.harmony.supreme.modular.paypal.service.PaypalService;
- import com.harmony.supreme.modular.paypal.util.URLUtils;
- import com.paypal.api.payments.Links;
- import com.paypal.api.payments.Payment;
- import com.paypal.base.rest.PayPalRESTException;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.web.bind.annotation.*;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import java.io.IOException;
- import java.util.Date;
-
-
- @RestController
- @RequestMapping("/marketing")
- public class PaypalController {
-
- public static final String PAYPAL_SUCCESS_URL = "/marketing/paypal/success";
- public static final String PAYPAL_CANCEL_URL = "/marketing/paypal/cancel";
-
- private Logger logger = LoggerFactory.getLogger(getClass());
-
- @Autowired
- private PaypalService paypalService;
-
- /**
- * 生成订单
- */
- @PostMapping("/paypal")
-
- public CommonResult<String> paypal(@RequestBody PaypalOrderParam entity, HttpServletRequest request, HttpServletResponse response) {
-
-
- String cancelUrl = URLUtils.getBaseUrl(request) + '/' +PAYPAL_CANCEL_URL;
- logger.info("cancelUrl is {}", cancelUrl); // 日志打印
- String successUrl = URLUtils.getBaseUrl(request) + '/' +PAYPAL_SUCCESS_URL;
- logger.info("successUrl is {}", successUrl); // 日志打印
-
- try {
- //Payment payment = paypalService.createPayment(entity.getTotalFee(), entity.getUserId(), entity.getContext(),
- Payment payment = paypalService.createPayment(5.0, "1686590877167329281", "视频",
- new Date(), cancelUrl, successUrl);
- for (Links links : payment.getLinks()) {
- if (links.getRel().equals("approval_url")) {
- logger.info("links.getHref() is {}", links.getHref());
- logger.info("支付订单返回paymentId:" + payment.getId());
- logger.info("支付订单状态state:" + payment.getState());
- logger.info("支付订单创建时间:" + payment.getCreateTime());
-
- String href = links.getHref();
- return CommonResult.data(href);
- }
- }
- } catch (PayPalRESTException e) {
- logger.error(e.getMessage());
- return new CommonResult<>(e.getResponsecode(), e.getMessage(), null);
- }
- return CommonResult.error("错误");
- }
-
- /**
- * 取消支付
- */
- @GetMapping("/paypal/cancel")
- public CommonResult<String> cancelPay(){
- return CommonResult.data("用户取消支付");
- }
-
- /**
- * 支付操作
- */
- @GetMapping("/paypal/success")
- public CommonResult<String> successPay(@RequestParam("paymentId") String paymentId, @RequestParam("PayerID") String payerId) { // 一定是PayerID,PayPal通常使用"PayerID"(ID和P都大小写)作为参数名称
- try {
- Payment payment = paypalService.executePayment(paymentId, payerId);
- // 支付成功
- if(payment.getState().equals("approved")){
- // 订单号
- String saleId = payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getId();
- paypalService.addPay(request, saleId);
- logger.info("PDT通知:交易成功回调");
- logger.info("付款人账户:"+payment.getPayer().getPayerInfo().getEmail());
- logger.info("支付订单Id: {}",paymentId);
- logger.info("支付订单状态state:" + payment.getState());
- logger.info("交易订单Id: {}",saleId);
- logger.info("交易订单状态state:"+payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getState());
- logger.info("交易订单支付时间:"+payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getCreateTime());
- return CommonResult.data("支付成功");
- }
- } catch (PayPalRESTException e) {
- logger.info(e.getMessage());
- return new CommonResult(e.getResponsecode(), e.getMessage(), null);
- }
- return CommonResult.data("支付失败");
- }
- }
最后支付成功后返回的Payment数据如下:
- 支付成功Payment:{
- "id": "PAYID-MUY7X3I47036021V43838732",
- "intent": "sale",
- "payer": {
- "payment_method": "paypal",
- "status": "VERIFIED",
- "payer_info": {
- "email": "sb-kuob927781461@personal.example.com",
- "first_name": "John",
- "last_name": "Doe",
- "payer_id": "PRKXLARWJVWQL",
- "country_code": "C2",
- "shipping_address": {
- "recipient_name": "Doe John",
- "line1": "NO 1 Nan Jin Road",
- "city": "Shanghai",
- "country_code": "C2",
- "postal_code": "200000",
- "state": "Shanghai"
- }
- }
- },
- "cart": "92948520FV7438203",
- "transactions": [
- {
- "transactions": [],
- "related_resources": [
- {
- "sale": {
- "id": "9GJ58424N8173751G",
- "amount": {
- "currency": "USD",
- "total": "5.00",
- "details": {
- "shipping": "0.00",
- "subtotal": "5.00",
- "handling_fee": "0.00",
- "insurance": "0.00",
- "shipping_discount": "0.00"
- }
- },
- "payment_mode": "INSTANT_TRANSFER",
- "state": "completed",
- "protection_eligibility": "ELIGIBLE",
- "protection_eligibility_type": "ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE",
- "transaction_fee": {
- "currency": "USD",
- "value": "0.47"
- },
- "parent_payment": "PAYID-MUY7X3I47036021V43838732",
- "create_time": "2023-10-20T07:04:41Z",
- "update_time": "2023-10-20T07:04:41Z",
- "links": [
- {
- "href": "https://api.sandbox.paypal.com/v1/payments/sale/9GJ58424N8173751G",
- "rel": "self",
- "method": "GET"
- },
- {
- "href": "https://api.sandbox.paypal.com/v1/payments/sale/9GJ58424N8173751G/refund",
- "rel": "refund",
- "method": "POST"
- },
- {
- "href": "https://api.sandbox.paypal.com/v1/payments/payment/PAYID-MUY7X3I47036021V43838732",
- "rel": "parent_payment",
- "method": "GET"
- }
- ]
- }
- }
- ],
- "amount": {
- "currency": "USD",
- "total": "5.00",
- "details": {
- "shipping": "0.00",
- "subtotal": "5.00",
- "handling_fee": "0.00",
- "insurance": "0.00",
- "shipping_discount": "0.00"
- }
- },
- "payee": {
- "email": "sb-gdewt15359677@business.example.com",
- "merchant_id": "AZ5ZMSER4CFS2"
- },
- "description": "视频",
- "item_list": {
- "items": [],
- "shipping_address": {
- "recipient_name": "Doe John",
- "line1": "NO 1 Nan Jin Road",
- "city": "Shanghai",
- "country_code": "C2",
- "postal_code": "200000",
- "state": "Shanghai"
- }
- }
- }
- ],
- "failed_transactions": [],
- "state": "approved",
- "create_time": "2023-10-20T04:02:53Z",
- "update_time": "2023-10-20T07:04:41Z",
- "links": [
- {
- "href": "https://api.sandbox.paypal.com/v1/payments/payment/PAYID-MUY7X3I47036021V43838732",
- "rel": "self",
- "method": "GET"
- }
- ]
- }
或者:
- 支付成功Payment:{
- "id": "PAYID-MUZC3FI3NX78464UJ4562005",
- "intent": "sale",
- "payer": {
- "payment_method": "paypal",
- "status": "VERIFIED",
- "payer_info": {
- "email": "sb-kuob927781461@personal.example.com",
- "first_name": "John",
- "last_name": "Doe",
- "payer_id": "PRKXLARWJVWQL",
- "country_code": "C2",
- "shipping_address": {
- "recipient_name": "Doe John",
- "line1": "NO 1 Nan Jin Road",
- "city": "Shanghai",
- "country_code": "C2",
- "postal_code": "200000",
- "state": "Shanghai"
- }
- }
- },
- "cart": "8HL7026676482401S",
- "transactions": [
- {
- "transactions": [],
- "related_resources": [
- {
- "sale": {
- "id": "0KC07001909543205",
- "amount": {
- "currency": "USD",
- "total": "5.00",
- "details": {
- "shipping": "0.00",
- "subtotal": "5.00",
- "handling_fee": "0.00",
- "insurance": "0.00",
- "shipping_discount": "0.00"
- }
- },
- "payment_mode": "INSTANT_TRANSFER",
- "state": "completed",
- "protection_eligibility": "ELIGIBLE",
- "protection_eligibility_type": "ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE",
- "transaction_fee": {
- "currency": "USD",
- "value": "0.47"
- },
- "parent_payment": "PAYID-MUZC3FI3NX78464UJ4562005",
- "create_time": "2023-10-20T07:35:21Z",
- "update_time": "2023-10-20T07:35:21Z",
- "links": [
- {
- "href": "https://api.sandbox.paypal.com/v1/payments/sale/0KC07001909543205",
- "rel": "self",
- "method": "GET"
- },
- {
- "href": "https://api.sandbox.paypal.com/v1/payments/sale/0KC07001909543205/refund",
- "rel": "refund",
- "method": "POST"
- },
- {
- "href": "https://api.sandbox.paypal.com/v1/payments/payment/PAYID-MUZC3FI3NX78464UJ4562005",
- "rel": "parent_payment",
- "method": "GET"
- }
- ]
- }
- }
- ],
- "amount": {
- "currency": "USD",
- "total": "5.00",
- "details": {
- "shipping": "0.00",
- "subtotal": "5.00",
- "handling_fee": "0.00",
- "insurance": "0.00",
- "shipping_discount": "0.00"
- }
- },
- "payee": {
- "email": "sb-gdewt15359677@business.example.com",
- "merchant_id": "AZ5ZMSER4CFS2"
- },
- "description": "合恕支付充值",
- "item_list": {
- "items": [],
- "shipping_address": {
- "recipient_name": "Doe John",
- "line1": "NO 1 Nan Jin Road",
- "city": "Shanghai",
- "country_code": "C2",
- "postal_code": "200000",
- "state": "Shanghai"
- }
- }
- }
- ],
- "failed_transactions": [],
- "state": "approved",
- "create_time": "2023-10-20T07:34:44Z",
- "update_time": "2023-10-20T07:35:21Z",
- "links": [
- {
- "href": "https://api.sandbox.paypal.com/v1/payments/payment/PAYID-MUZC3FI3NX78464UJ4562005",
- "rel": "self",
- "method": "GET"
- }
- ]
- }
在PayPal的Payment对象中,两种状态:支付状态(Payment Status)、交易状态(Transaction Status)。
支付状态(Payment Status):
取值:payment.getId()
交易状态(Transaction Status):
取值:payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getId()
(其中:PaypalPaymentIntent有order、sale、authorize三种状态,所以获取时分别将getSale()换为getOrder()、getAuthorize()即可)
交易订单id为:payment.getTransactions().get(0).getRelatedResources().get(0).getSale().getId()或修改Sale为其他两种状态。
- <!DOCTYPE html><html><head><meta charset="UTF-8" />
- <title>Insert title here</title>
- </head>
- <body>
- <h1>Canceled by user</h1>
- </body>
- </html>
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8" />
- <title>Insert title here</title>
- </head>
- <body>
- <form method="post" th:action="@{/pay}">
- <button type="submit">
- <img src="images/paypal.jpg" width="100px;" height="30px;"/>
- </button>
- </form>
- </body>
- </html>
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="UTF-8" />
- <title>Insert title here</title>
- </head>
- <body>
- <h1>Payment Success</h1>
- </body>
- </html>
paypal.client.app是App的CilentID, paypal.client.secret是Secret
- server.port: 8088
- spring.thymeleaf.cache=false
-
- paypal.mode=sandbox
- paypal.client.app=AeVqmY_pxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxKYniPwzfL1jGR
- paypal.client.secret=ELibZhExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxUsWOA_-
- import cn.dev33.satoken.stp.StpUtil;
- import com.alibaba.excel.util.StringUtils;
- import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
- import com.github.xiaoymin.knife4j.annotations.ApiSupport;
- import com.paypal.api.payments.Payment;
- import com.paypal.base.rest.PayPalRESTException;
- import com.paypal.http.HttpResponse;
- import com.paypal.orders.*;
- import io.swagger.annotations.Api;
- import io.swagger.annotations.ApiOperation;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.validation.annotation.Validated;
- import org.springframework.web.bind.annotation.*;
-
- import javax.annotation.Resource;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import java.io.IOException;
-
-
- @RestController
- public class PaypalV2Controller {
-
- @Value("${paypal.client.mode}")
- private String mode;
-
- @Value("${paypal.client.app}")
- private String clientId;
-
- @Value("${paypal.client.secret}")
- private String secret;
-
-
- @Resource
- private PaypalV2Service paypalV2Service;
-
- /**
- * 生成订单
- */
- @ApiOperationSupport(order = 1)
- @ApiOperation("生成订单")
- @PostMapping("/pay/paypal")
- public CommonResult<String> paypalV2(@RequestBody PaypalOrderParam entity, HttpServletRequest request, HttpServletResponse response) {
-
- // 在哪里部署就写哪个域名xxxxx,因为在服务器上如按PayPal V1中所写的方法结果xxxxx还是localhost,此路径在服务器上将报错
- String cancelUrl = "http://xxxxx:xxxx/pay/cancel";
- String successUrl = "http://xxxxx:xxxx/pay/success";
-
- if (StringUtils.isBlank(entity.getUserId())) {
- entity.setUserId(StpUtil.getLoginIdAsString());
- }
-
- try {
- String href = paypalV2Service.createPayment(entity, cancelUrl, successUrl);
- return CommonResult.data(href);
- } catch (PayPalRESTException e) {
- return new CommonResult<>(e.getResponsecode(), e.getMessage(), null);
- }
- }
-
- /**
- * 取消支付
- */
- @ApiOperationSupport(order = 2)
- @ApiOperation("取消支付")
- @GetMapping("/pay/cancel")
- public String cancelPay(){
- return "已取消支付,请返回上一级页面";
- }
-
- /**
- * 支付成功
- */
- @ApiOperationSupport(order = 3)
- @ApiOperation("支付成功")
- @GetMapping("/pay/success")
- public String successPay(@RequestParam("token") String token, @RequestParam("userId") String userId, @RequestParam("beanNum") String beanNum) {
-
- //捕获订单 进行支付
- HttpResponse<Order> response = null;
- OrdersCaptureRequest ordersCaptureRequest = new OrdersCaptureRequest(token);
- ordersCaptureRequest.requestBody(new OrderRequest());
-
- PayPalClient payPalClient = new PayPalClient();
- try {
- //环境判定sandbox 或 live
- response = payPalClient.client(mode, clientId, secret).execute(ordersCaptureRequest);
-
- for (PurchaseUnit purchaseUnit : response.result().purchaseUnits()) {
- for (Capture capture : purchaseUnit.payments().captures()) {
- if ("COMPLETED".equals(capture.status())) {
- //支付成功
- // 订单号
- String saleId = capture.id();
- String fee = capture.amount().value();
- paypalV2Service.addPay(fee, saleId, userId, beanNum);
- return "支付成功,请返回上一级页面";
- }
- }
- }
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- return "支付失败,请返回上一级页面";
- }
- }
- public interface PaypalV2Service {
-
- /**
- * 创建支付:实体类、取消支付时的重定向 URL、支付成功时的重定向 URL
- */
- public String createPayment(PaypalOrderParam entity, String cancelUrl, String successUrl) throws PayPalRESTException;
-
- /**
- * 支付成功生成订单
- */
- void addPay(String fee, String saleId, String userId, String beanNum);
-
- }
-
-
-
- import cn.dev33.satoken.stp.StpUtil;
- import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
- import com.fhs.common.utils.StringUtil;
- import com.paypal.core.PayPalHttpClient;
- import com.paypal.http.HttpResponse;
- import com.paypal.orders.*;
- import com.paypal.orders.Order;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.stereotype.Service;
- import javax.annotation.Resource;
- import javax.servlet.http.HttpServletRequest;
- import java.io.IOException;
- import java.math.BigDecimal;
- import java.util.*;
-
- @Service
- public class PaypalV2ServiceImpl implements PaypalV2Service {
-
- @Value("${paypal.client.mode}")
- private String mode;
-
- @Value("${paypal.client.app}")
- private String clientId;
-
- @Value("${paypal.client.secret}")
- private String secret;
-
- @Resource
- private UserPayService userPayService;
-
- @Resource
- private UserOrderService userOrderService;
-
- @Resource
- private SysRechargePlanService sysRechargePlanService;
-
- @Override
- public String createPayment(PaypalOrderParam entity, String cancelUrl, String successUrl) {
-
- PayPalClient payPalClient = new PayPalClient();
- // 设置环境沙盒或生产
- PayPalHttpClient client = payPalClient.client(mode, clientId, secret);
-
- //回调参数(支付成功success路径所携带的参数)
- Map<String, String> sParaTemp = new HashMap<String, String>();
-
- BigDecimal bigDecimal = new BigDecimal("100");
- String totalMoney = String.valueOf(entity.getTotalFee().divide(bigDecimal));
- // 回调的参数可以多设置几个
- // 插入用户USERID
- if (StringUtil.isEmpty(entity.getUserId())) {
- entity.setUserId(StpUtil.getLoginIdAsString());
- }
- sParaTemp.put("userId", entity.getUserId());
-
- // 根据价钱查找方案
- QueryWrapper<SysRechargePlan> wrapper = new QueryWrapper<>();
- wrapper.eq("TYPE", "BALANCE")
- .eq("FEE", Float.valueOf(totalMoney));
- SysRechargePlan one = sysRechargePlanService.getOne(wrapper);
- // 插入所得金豆数
- sParaTemp.put("beanNum", String.valueOf(one.getUnit()));
-
- String url = successUrl + paramsConvertUrl(sParaTemp);
-
- // eg:回调链接:http://localhost:xxxx/pay/success?beanNum=150&userId=1655776615868264450
- System.out.println("回调链接:"+url);
-
- // 配置请求参数
- OrderRequest orderRequest = new OrderRequest();
- orderRequest.checkoutPaymentIntent("CAPTURE");
- List<PurchaseUnitRequest> purchaseUnits = new ArrayList<>();
- purchaseUnits.add(new PurchaseUnitRequest().amountWithBreakdown(new AmountWithBreakdown().currencyCode("USD").value(totalMoney)));
- orderRequest.purchaseUnits(purchaseUnits);
- orderRequest.applicationContext(new ApplicationContext().returnUrl(url).cancelUrl(cancelUrl));
- OrdersCreateRequest request = new OrdersCreateRequest().requestBody(orderRequest);
-
- HttpResponse<Order> response;
- try {
- response = client.execute(request);
- Order order = response.result();
- String payHref = null;
- String status = order.status();
- if (status.equals("CREATED")) {
- List<LinkDescription> links = order.links();
- for (LinkDescription linkDescription : links) {
- if (linkDescription.rel().equals("approve")) {
- payHref = linkDescription.href();
- }
- }
- }
- return payHref;
- } catch (IOException e) {
- e.printStackTrace();
- }
- return null;
- }
-
- private static String paramsConvertUrl(Map<String, String> params) {
- StringBuilder urlParams = new StringBuilder("?");
- Set<Map.Entry<String, String>> entries = params.entrySet();
- for (Map.Entry<String, String> entry : params.entrySet()) {
- urlParams.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
- }
- String urlParamsStr = urlParams.toString();
- return urlParamsStr.substring(0, urlParamsStr.length()-1);
- }
-
- @Override
- public void addPay(String fee, String saleId, String userId, String beanNum) {
- UserPay userPay = new UserPay();
- userPay.setUserId(userId);
- userPay.setOrderNo(saleId);
- userPay.setPayFrom("PayPal");
- userPay.setPayType("YES");
- userPay.setTotalFee(new BigDecimal(beanNum));
- userPay.setCreateTime(new Date());
- userPay.setSuccessTime(new Date());
- userPay.setCreateUser(userId);
- userPayService.addPay(userPay);
-
- // 金豆
- UserOrder userOrder = new UserOrder();
- userOrder.setUserId(userId);
- userOrder.setOrderNo(saleId);
- userOrder.setPaySource("COST");
- userOrder.setPayStatus("INCOME");
- userOrder.setType("BALANCE");
- userOrder.setUnit(new BigDecimal(beanNum));
- userOrder.setCreateUser(userId);
- userOrderService.addPaypal(userOrder);
- }
- }
- import com.paypal.core.PayPalEnvironment;
- import com.paypal.core.PayPalHttpClient;
-
- public class PayPalClient {
-
- public PayPalHttpClient client(String mode, String clientId, String clientSecret) {
- PayPalEnvironment environment = mode.equals("live") ? new PayPalEnvironment.Live(clientId, clientSecret) : new PayPalEnvironment.Sandbox(clientId, clientSecret);
- return new PayPalHttpClient(environment);
- }
- }
- // 将控制器方法中的返回值类型设置为 String ,将 return 中的内容修改为如下:
- // 即成功后访问页面,1秒后跳转回上一级(前端)页面
-
- // 支付成功(返回一级后将返回到支付链接:https://www.sandbox.paypal.com/checkoutnow?token=7LK561281D3524115,此时会显示PayPal支付结果
- return "<script>window.onload=function(){setTimeout(function(){history.go(-1);},1000);}</script>支付成功,返回上一级页面";
- // 支付失败(返回一级后将返回到支付链接:https://www.sandbox.paypal.com/checkoutnow?token=7LK561281D3524115,此时会显示PayPal支付结果
- return "<script>window.onload=function(){setTimeout(function(){history.go(-1);},1000);}</script>支付失败,返回上一级页面";
-
- // 上一级:即跳转后“支付成功,返回上一级页面”页面/“支付失败,返回上一级页面”页面的上一级
- // 上几级就负几,-2、-3...
(1)启动项目
(2)在浏览器输入localhost:8088
(3)点击paypal后,会跳到paypal的登录界面,登录测试账号(PRESONAL)后点击继续即可扣费,扣500$(具体数额可在controller中自定义)
- Payment payment = paypalService.createPayment(
- 500.00,
- "USD",
- PaypalPaymentMethod.paypal,
- PaypalPaymentIntent.sale,
- "payment description",
- cancelUrl,
- successUrl);
(4)到安全海淘国际支付平台_安全收款外贸平台-PayPal CN 登录测试账号看看余额有没有变化
Error code : 400 with response : {"name":"DUPLICATE_REQUEST_ID","message":"The value of PayPal-Request-Id header has already been used","information_link":"https://developer.paypal.com/docs/api/payments/v1/#error-DUPLICATE_REQUEST_ID","debug_id":"a3d876b7ebd44"}
服务器未知异常:response-code: 400 details: name: DUPLICATE_REQUEST_ID message: The value of PayPal-Request-Id header has already been used details: null debug-id: a3d876b7ebd44 information-link: https://developer.paypal.com/docs/api/payments/v1/#error-DUPLICATE_REQUEST_ID, 请求地址:http://localhost:82/marketing/paypal/success
原因:
报错的原因是请求中的 PayPal-Request-Id 标头的值已经被使用过,导致请求被认为是重复的。
PayPal-Request-Id 是一个用于标识 PayPal API 请求的唯一标识符。每次进行 PayPal API 请求时,应该使用一个新的、唯一的 PayPal-Request-Id 值。如果重复使用相同的 PayPal-Request-Id 值进行请求,PayPal 服务器会将其视为重复请求,并返回 DUPLICATE_REQUEST_ID 错误。
解决:
修改PaypalConfig:
- @Configuration
- public class PaypalConfig {
-
- @Value("${paypal.client.app}")
- private String clientId;
- @Value("${paypal.client.secret}")
- private String clientSecret;
- @Value("${paypal.client.mode}")
- private String mode;
-
- @Bean
- public Map<String, String> paypalSdkConfig() {
- Map<String, String> sdkConfig = new HashMap<>();
- sdkConfig.put("mode", mode);
- return sdkConfig;
- }
-
- public OAuthTokenCredential authTokenCredential() {
- return new OAuthTokenCredential(clientId, clientSecret, paypalSdkConfig());
- }
-
- public APIContext apiContext() throws PayPalRESTException {
- APIContext apiContext = new APIContext(authTokenCredential().getAccessToken());
- apiContext.setConfigurationMap(paypalSdkConfig());
- return apiContext;
- }
- }
修改PaypalService,每次每次使用apiContext时生成新的requestId:
- @Service
- public class PaypalServiceImpl implements PaypalService {
-
- @Resource
- private PaypalConfig paypalConfig;
-
- @Resource
- private UserPayService userPayService;
-
- @Resource
- private UserOrderService userOrderService;
-
- @Override
- public Payment createPayment(PaypalOrderParam entity, String cancelUrl, String successUrl) throws PayPalRESTException {
-
- Amount amount = new Amount();
- amount.setCurrency("USD"); // 美金
- // total 是以分为单位,所以得除以100
- BigDecimal divisor = new BigDecimal("100");
- amount.setTotal(String.format("%.2f", entity.getTotalFee().divide(divisor)));
-
- Transaction transaction = new Transaction();
- transaction.setAmount(amount);
- transaction.setDescription(StringUtils.isBlank(entity.getContext())?"合恕支付充值":entity.getContext());
-
- List<Transaction> transactionList = new ArrayList<>();
- transactionList.add(transaction);
-
- Payer payer = new Payer();
- payer.setPaymentMethod("paypal"); // paypal购买
-
- Payment payment = new Payment();
- payment.setIntent("sale");// 直接购买
- payment.setPayer(payer);
- payment.setTransactions(transactionList);
-
- RedirectUrls redirectUrls = new RedirectUrls();
- redirectUrls.setCancelUrl(cancelUrl);
- redirectUrls.setReturnUrl(successUrl + "?userId="+entity.getUserId()+"&totalFee="+entity.getTotalFee()+"&payFrom="+ entity.getPayFrom());
- payment.setRedirectUrls(redirectUrls);
-
- // 每次使用apiContext时生成新的requestId
- APIContext apiContext = paypalConfig.apiContext();
-
- return payment.create(apiContext);
- }
-
- @Override
- public Payment executePayment(String paymentId, String payerId) throws PayPalRESTException {
- Payment payment = new Payment();
- payment.setId(paymentId);
- PaymentExecution paymentExecution = new PaymentExecution();
- paymentExecution.setPayerId(payerId);
-
- // 每次使用apiContext时生成新的requestId
- APIContext apiContext = paypalConfig.apiContext();
- return payment.execute(apiContext, paymentExecution);
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。