⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7346763303459635251 「luhumu」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

前置需求

支付宝沙箱配置

需要有支付宝沙箱:提供一个虚拟的支付环境,用于测验调试,点击这里进入沙箱环境:

https://openhome.alipay.com/platform/appDaily.htm

因为是沙箱默认的密钥即可,这其中我们需要如下数据

  • 支付宝的公钥和私钥
  • 支付的网关
  • 支付的APPID
  • 支付宝sdk

内网穿透

系统启动只是在本地,如何让不同ip都能访问呢?内网穿透

内网穿透是一种技术手段,它允许外部网络的用户访问内部网络中的设备或服务。这项技术通常用于远程访问、建站、调试和游戏联机等场景,使得本地开发或维护的工作可以不受地理位置的限制。

为什么需要内网穿透呢?因为本质上调用人家的服务,当请求以后,支付宝需要返回给你信息,这就需要你的地址。这样才能找到我们的接口,我们才能根据他返回的信息做出一些业务处理。

免费即可,第一次需要实名认证

设置免费隧道,

我的隧道进行查询 以下红框中的码需要用到,

下载pc端app,解压出来,只有一个exe文件

这里68cvzs地址就是我本地映射到公网上的地址。

可以启动本地的9090服务,使用公网地址进行测试,试一试能不能访问成功

成功,则内网穿透成功。

具体开发

sdk

<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.9.28.ALL</version>
</dependency>

配置

相当于设置收钱的人是谁

server:
port: 9090

alipay:
# appid
appId: 9021000135634074
# 应用私钥
appPrivateKey: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8bdoXTGIiXXkt/FBg0LPoOSvQANWo4wIYKVSjodGa60y+7Dk9d6DavrcWR7xrBYaJcSt9jr35rYGZQsXLuYJya0X1bkuiE4C1B26jrTdvPMu2UXlo9dQM2XdwuAb7ohkgMdp0jkMYag2IbtfjRFv0h+jnlUNfAZjJyQ8l7TePqFnJ8S94L9jVloYIRoeoJG3vj701HJ4IccEEFzpiNoznJnw33elr9Zu5K1gZqvQxp61D2pbMJ0daiof94uh3J0XwqeyB8HNbGlZj3OIyZMibU1AZfIpE2+cYVSGYt7+JawayNybhuTlb540OZGRjFGS7KCysqvEqIH6KbHqZJpwpAgMBAAECggEAVmPLDMmBaJ4Qc+vtciXdhgfINYpcax0BFdDFNGx5bBybSCGHsM3LGt87k+R511tmLleLh/pM0U8iTwEVLG02CH10Sq0x0fI9HUJ2EGbXNpHaGBHMpzml6eo+X5iP4wiTmnYg4TVkP6pH4BhptJHf0wII3zxI8iMKSOF8p3fV9G+Ay8i4zTmJtnzTopMshmH0IjemGBsImVLPjlrGNtt28ibarFNu+y8zq82H8ZKW9Gf2aokU3juml7ztIeVuEJEJGZo9D7hv+IwjCUr4q273pNSVE/RW0Nyi2moHmlaJzvSBY9iGPegmZDZAdG2pTUvfbj7KMUHUpTATSXkOwUg8AQKBgQD7zyspR0V9XAlWIYVNNu9aNgtN6lpYtG0MlQYAREsWF9/HCRgghnSE5/TfvKtDsdk2E2nw6WTp9D9ATPFf5TXTWNCcQp2bhiZttFgr4ii+inFpOf8zNJKovuLEa8SB9ImvPQXF7H1pbvWMnDoFtVTbmtiL7RtfOYK/GaK3I3Z2gQKBgQC/kKcXdieMY0jA6Ho6+5W5LmwBWiuhp/8S+o29494JzLr9cG074q69jaXHBNaEMgLfn3oM9Te2SzaPf8dEIzmfjmOJOrfzz8WOI9vWfW14u9ev6h52p21CjKCVsaxcjle9rXWoO4L8UfPpvjA3gz+/KLFdjyz16ctIeKqLWkvhqQKBgQCxcklBGnduorf1mUOdqRO8p48JxhcKoYKjNjT3ZSjglcxdLxxwdy+PJQJb1FGAL8kYoU+rtF0nvLYB8va5lAV9PP4sz4jPOxbDgi4MJqB9vYO0GmmrROYAwht9PVBKH/ASrFbwJfounUGuZ70/nowBATqOSHVcgmOPlVoj4nqtgQKBgBJdgNdr+XCpGBF+eDFtazY8sBgVF/fAjmC4apxY2zGfUhH4FFDYc54ylUEWQqab0NC1jlLts3Cjl1B8lJMAuYaFdR9z5KzYL31oDKxsi/E2OQBhdpgzhvMJXl/bj8Wz08+YcJ43TmB4TuUK2b6Is7TU5uQsVMgiqN2Cy/7eIkY5AoGANDCwDwrSTsHPzmrdL+8ACgWNv7KIj81mjSRIxGQrYYwuxMUAKWRBzEPcgFj3c6KfbyR/PxH+4jf8weYUIMZqY16pjmTUPs1Ba2UX0C2DSIcZeF7SRTrRX3CT0e5ycRBXu1EoqGPmtmO1YFnObTsGY+ZwMUKIFSS1ip2IkPaDyvA=
# 支付宝公钥
alipayPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx8Z31UrIoS1tL3f0VVqia8tlUze7RFnEVi3VbwOzC3RV9tt/crZaR+B21DrWkzjCw36tfrlL2WKYW4dWE/4KKwBY+TB67dZmIUShLf65imxysrB5woofe00uXv5GRYNVEgF+XAyUl3lVO5CHCwr2TGnBdOFOBJzvjmBViwEXHFLqECc4UqqnxvrP0AjMggwExktBEba3Z+i14o1jNznntf52FR2A/RGXiuj9FZHXTOlsc0FHodA3uO5V7WCwMlb7QI0HUm4UvTgPziLv6L3Gee2925Mu5t3NXQuRSVrOVh/W2xUCdwoPVeXd6mNh5v25qrR8TsKbmuO556haJbbCTQIDAQAB
# 回调接口,支付宝通过什么接口通知你,这里就是填写具体接口地址,我们使用公网的地址以及接口uri /公网地址/uri
notifyUrl: http://v6tqyw.natappfree.cc/alipay/notify
/**
* 读取yml中的配置信息,自动填充到对应的属性
*/
@Data
@Component
@ConfigurationProperties(prefix = "alipay")
public class AliPayConfig {
private String appId;
private String appPrivateKey;
private String alipayPublicKey;
private String notifyUrl;

}

具体支付

步骤

  • 必须传递如下参数(支付订单号(必须唯一), 订单名称,订单金额)
  • 创建支付客户端 设定appid 公钥密钥等等信息,用于知道这个订单谁发出的,钱给到谁的账户
  • 将请求的订单号等等写入支付的请求对象中 并且请求对象设置回调接口以及支付后的接口
  • 支付客户端对象根据支付请求对象去执行,调用支付宝API
  • 这个API接口会返回一个表单页面,让用户去输入帐号密码(也就是谁来支付),成功显示金额等等信息,填写支付密码进行转账
  • 转账以后无论成功与否,支付宝都会调用你的回调接口,传入数据

代码实现

业务参数代码

/**
* 支付宝支付请求对象 所需要的参数
*/
@Data
public class PayVO {
private String out_trade_no; // 商户订单号 必填
private String subject; // 订单名称 必填
private BigDecimal total_amount; // 付款金额 必填
private String body; // 商品描述 可空
}

支付宝客户端执行

@RestController
@RequestMapping("/alipay")
@Transactional(rollbackFor = Exception.class)
public class AliPayController {

@Resource
AliPayConfig aliPayConfig;

@Resource
private ShopOrderDao shopOrderMapper;
private static final String GATEWAY_URL = "https://openapi-sandbox.dl.alipaydev.com/gateway.do";
private static final String FORMAT = "JSON";
private static final String CHARSET = "utf-8";
private static final String SIGN_TYPE = "RSA2";

/**
* 支付接口 传入业务参数
* 支付是一个我向你要钱的过程,设置api参数就是为了,知道收钱的人是谁,
* 当执行以后支付宝会返回一个登录页面,支付的人输入帐号密码。并且确定金额输入支付密码进行支付
* @param aliPay
* @param httpResponse
* @throws Exception
*/
// 这里使用Get其实不是很恰当,应该使用post,这里为了调试方便使用Get
@GetMapping("/pay")
public void pay(PayVO aliPay, HttpServletResponse httpResponse) throws Exception {
// 1、根据支付宝的配置生成一个支付客户端 客户端用于去调用支付宝的API
// 官方写法
AlipayClient alipayClient = new DefaultAlipayClient(GATEWAY_URL, aliPayConfig.getAppId(),
aliPayConfig.getAppPrivateKey(), FORMAT, CHARSET, aliPayConfig.getAlipayPublicKey(), SIGN_TYPE);


// 2、创建一个支付请求对象
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
// 设置回调接口
request.setNotifyUrl(aliPayConfig.getNotifyUrl());
// 商户订单号,商户网站订单系统中唯一订单号,必填,支付宝不允许有两个相同的订单号
// 使用uuid生成 避免重复
aliPay.setOut_trade_no(UUID.randomUUID().toString());
String out_trade_no = aliPay.getOut_trade_no();
// 付款金额,必填
BigDecimal total_amount = aliPay.getTotal_amount();
// 订单名称,必填
String subject = aliPay.getSubject();
// 商品描述,可空
String body = aliPay.getBody();
// 设置 业务参数 是一个json对象
// 这个json对象 支付宝后台回去识别,根据这些参数进行处理,例如 金额,订单名称,商品描述
request.setBizContent("{"out_trade_no":"" + out_trade_no + "","
+ ""total_amount":"" + total_amount + "","
+ ""subject":"" + subject + "","+ ""body":""+ body +"","
+ ""product_code":"FAST_INSTANT_TRADE_PAY"}");
// 支付完以后跳转的地址
request.setReturnUrl("http://loaclhost:9090/hello/pay");

// 3. 客户端执行请求
// 客户端执行请求,拿到响应的结果,返回给浏览器
String form = "";
try {
// 调用阿里的SDK生成表单
// 会收到支付宝的响应,响应的是一个页面,一开始是登陆,然后显示金额,让用户输入密码进行付款
form = alipayClient.pageExecute(request).getBody();
} catch (AlipayApiException e) {
e.printStackTrace();
}
httpResponse.setContentType("text/html;charset=" + CHARSET);
// 直接将完整的表单html输出到页面
httpResponse.getWriter().write(form);
httpResponse.getWriter().flush();
httpResponse.getWriter().close();
}

效果

输入如下地址

http://localhost:9090/alipay/pay?subject=测试商品&total_amount=1000

支付宝返回页面

输入帐号密码(沙箱有测试的密码)

输入支付密码支付

回调接口

以下是支付宝返回给我们的信息

"gmt_create" -> "2024-03-16 22:40:26"
"charset" -> "utf-8"
"gmt_payment" -> "2024-03-16 22:40:30"
"notify_time" -> "2024-03-16 22:40:31"
"subject" -> "测试商品"
"sign" -> "XfBcgT1lIYpxYm0DzaBtLz7WjzxHxhBK4gUdmDCtD/JTAwhohqu"
"buyer_id" -> "2088722031942622"
"body" -> "null"
"invoice_amount" -> "1000.00"
"version" -> "1.0"
"notify_id" -> "2024031601222224031142620502419"
"fund_bill_list" -> "[{"amount":"1000.00","fundChannel":"ALIPAYACCOUNT"}]"
"notify_type" -> "trade_status_sync"
"out_trade_no" -> "96b14931-b0a7-49bb-aa93-498432247a4"
"total_amount" -> "1000.00"
"trade_status" -> "TRADE_SUCCESS"
"trade_no" -> "20240316220014426205023040"
"auth_app_id" -> "9021000135634074"
"receipt_amount" -> "1000.00"
"point_amount" -> "0.00"
"buyer_pay_amount" -> "1000.00"
"app_id" -> "90210001354"
"sign_type" -> "RSA2"
"seller_id" -> "20887976059"

具体实现

@PostMapping("/notify")  // 注意这里必须是POST接口
public String payNotify(HttpServletRequest request) throws Exception {
// 判断返回状态trade_status 支付成功是TRADE_SUCCESS
if (request.getParameter("trade_status").equals("TRADE_SUCCESS")) {
System.out.println("=========支付宝异步回调========");

Map<String, String> params = new HashMap<>();
// 返回的所有元素 其中有gmt_create=2024-03-16 22:26:17, charset=utf-8, gmt_payment=2024-03-16 22:26:21, notify_time=2024-03-16 22:26:23, subject=测试商品
Map<String, String[]> requestParams = request.getParameterMap();
for (String name : requestParams.keySet()) {
// servlet写法 通过key获取value
params.put(name, request.getParameter(name));
}
System.out.println(params);
System.out.println(params.size());
String tradeNo = params.get("out_trade_no");
String gmtPayment = params.get("gmt_payment");
// 支付宝验签
// 这里必须要初始化不然报错
if (Factory.Payment.Common().verifyNotify(params)) {
// 验签通过
System.out.println("交易名称: " + params.get("subject"));
System.out.println("交易状态: " + params.get("trade_status"));
System.out.println("支付宝交易凭证号: " + params.get("trade_no"));
System.out.println("商户订单号: " + params.get("out_trade_no"));
System.out.println("交易金额: " + params.get("total_amount"));
System.out.println("买家在支付宝唯一id: " + params.get("buyer_id"));
System.out.println("买家付款时间: " + params.get("gmt_payment"));
System.out.println("买家付款金额: " + params.get("buyer_pay_amount"));
// 更新订单未已支付
// 做一些业务上的处理 例如说支付成功以后 更新订单状态 改为已支付等等
ShopOrder order = new ShopOrder();
order.setId(tradeNo);
order.setStatus("1");
order.setZhhifuTime(gmtPayment);
shopOrderMapper.save(order);
}
}
return "success";
}

注意:以上代码会报错,显示空指针异常

报错信息如下

Cannot invoke "com.alipay.easysdk.kernel.Context.getCertEnvironment()" because "this.context" is null

这是因为我们并未初始化sdk基础信息

解决:

/**
* 读取yml中的配置信息,自动填充到对应的属性
*/
@Data
@Component
@ConfigurationProperties(prefix = "alipay")
public class AliPayConfig {
private String appId;
private String appPrivateKey;
private String alipayPublicKey;
private String notifyUrl;

/**
* 初始化sdk 这样回调接口的时候 才能知道sdk的基础信息
*/
@PostConstruct
public void init() {
// 设置参数(全局只需设置一次)
Config config = new Config();
config.protocol = "https";
config.gatewayHost = "openapi-sandbox.dl.alipaydev.com";
config.signType = "RSA2";
config.appId = this.appId;
config.merchantPrivateKey = this.appPrivateKey;
config.alipayPublicKey = this.alipayPublicKey;
config.notifyUrl = this.notifyUrl;
Factory.setOptions(config);
System.out.println("=======支付宝SDK初始化成功=======");
}
}

结果

文章目录
  1. 1. 前置需求
    1. 1.1. 支付宝沙箱配置
    2. 1.2. 内网穿透
  2. 2. 具体开发
    1. 2.1. sdk
    2. 2.2. 配置
    3. 2.3. 具体支付
      1. 2.3.1. 步骤
      2. 2.3.2. 代码实现
      3. 2.3.3. 效果
    4. 2.4. 回调接口
      1. 2.4.1. 具体实现