支付宝提供了统一支付回调接口,用于接收支付宝支付结果的通知。通过配置回调接口,支付宝在用户完成支付后会向该接口发送异步通知,通知商户支付结果。支付宝统一支付回调接口的一般流程如下:
- 商户服务器搭建接收回调的HTTP接口:商户需要在自己的服务器上搭建一个接收支付宝回调通知的HTTP接口。
- 配置支付宝回调地址:商户在支付宝开放平台或商户后台配置支付宝回调地址,将接收回调通知的接口地址提供给支付宝。
- 接收支付宝回调通知:支付宝在用户完成支付后,会向商户提供的回调接口发送POST请求,携带支付结果相关参数。商户服务器需要处理该请求,并验证通知的合法性。
- 验证回调通知的合法性:商户在接收到支付宝回调通知后,需要进行回调通知的合法性验证,以确保该通知是由支付宝发送的,而不是伪造的通知。验证过程包括验证通知签名、验证通知来源等。
- 处理支付结果:验证回调通知的合法性后,商户可以根据通知中的支付结果信息,更新订单状态、进行业务处理等。
在沙箱环境
获取AppId、商户PID、支付私钥/公钥等信息,不需要自己创建应用。
地址:https://openhome.alipay.com/develop/sandbox/app
AppId、商户PID:
公钥、私钥信息:
<!--支付宝SDK -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.31.48.ALL</version>
</dependency>
<!-- 支付宝SDK依赖的日志-->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
alipay:
# appid
appId:
# 商户PID,卖家支付宝账号ID
sellerId:
# 私钥 pkcs8格式的,rsc中的私钥:https://openhome.alipay.com/platform/appDaily.htm?tab=info
privateKey:
# 支付宝公钥:https://openhome.alipay.com/platform/appDaily.htm?tab=info
publicKey:
# 服务器异步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
notifyUrl: http://zhbexg.natappfree.cc/alipay/notify
# 页面跳转同步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
returnUrl: https://zhbexg.natappfree.cc/alipay/return
# 请求网关地址
# 正式为:"https://openapi.alipay.com/gateway.do"
serverUrl: https://openapi.alipaydev.com/gateway.do
AlipayConfig:
@Component
@Data
@ConfigurationProperties(prefix = "alipay")
public class AlipayConfig {
/**
* 商户appid
*/
private String appId;
/**
* 商户PID,卖家支付宝账号ID
*/
private String sellerId;
/**
* 私钥 pkcs8格式的,rsc中的私钥
*/
private String privateKey;
/**
* 支付宝公钥
*/
private String publicKey;
/**
* 请求网关地址
*/
private String serverUrl;
/**
* 页面跳转同步通知(可以直接返回前端页面、或者通过后端进行跳转)
*/
private String returnUrl;
/**
* 服务器异步通知
*/
private String notifyUrl;
/**
* 获得初始化的AlipayClient
*
* @return
*/
@Bean
public AlipayClient alipayClient() {
// 获得初始化的AlipayClient
return new DefaultAlipayClient(serverUrl, appId, privateKey,
AlipayConstants.FORMAT_JSON, AlipayConstants.CHARSET_UTF8,
publicKey, AlipayConstants.SIGN_TYPE_RSA2);
}
}
需要在支付时,直到回调地址,回调接口不区分支付或者退款回调;一切回调都会触发该接口;
NotifyController :
@Api(tags = "回调接口(API3)")
@RestController
@Slf4j
public class NotifyController {
private final ReentrantLock lock = new ReentrantLock();
@Resource
private AlipayConfig alipayConfig;
/**
* 服务器异步通知页面路径,接收支付宝回调后,再封装页面数据,直接返回相应页面到前端
* 支付、退款都会通过此回调接口
*
* @param request
* @param response
* @throws IOException
* @deprecated API地址:https://opendocs.alipay.com/open/203/105286
*/
@PostMapping("/notify")
public void notifyUrl(HttpServletRequest request, HttpServletResponse response) throws Exception {
log.info("异步通知");
PrintWriter out = response.getWriter();
if (lock.tryLock()) {
try {
//乱码解决,这段代码在出现乱码时使用
request.setCharacterEncoding("utf-8");
//获取支付宝POST过来反馈信息
Map<String, String> params = new HashMap<>(8);
Map<String, String[]> requestParams = request.getParameterMap();
for (Map.Entry<String, String[]> stringEntry : requestParams.entrySet()) {
String[] values = stringEntry.getValue();
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
params.put(stringEntry.getKey(), valueStr);
}
//调用SDK验证签名
boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayConfig.getPublicKey(), AlipayConstants.CHARSET_UTF8, AlipayConstants.SIGN_TYPE_RSA2);
if (!signVerified) {
log.error("验签失败");
out.print("fail");
return;
}
log.warn("=========== 在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱 ===========");
// 通知ID
String notifyId = new String(params.get("notify_id").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
// 通知时间
String notifyTime = new String(params.get("notify_time").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
//商户订单号,之前生成的带用户ID的订单号
String outTradeNo = new String(params.get("out_trade_no").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
//支付宝交易号
String tradeNo = new String(params.get("trade_no").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
//付款金额
String totalAmount = new String(params.get("total_amount").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
//交易状态
String tradeStatus = new String(params.get("trade_status").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
/*
* 交易状态
* TRADE_SUCCESS 交易完成
* TRADE_FINISHED 支付成功
* WAIT_BUYER_PAY 交易创建
* TRADE_CLOSED 交易关闭
*/
log.info("tradeStatus:" + tradeStatus);
if ("TRADE_FINISHED".equals(tradeStatus)) {
/*此处可自由发挥*/
//判断该笔订单是否在商户网站中已经做过处理
//如果没有做过处理,根据订单号(out_trade_no)在商户网站的订单系统中查到该笔订单的详细,并执行商户的业务程序
//如果有做过处理,不执行商户的业务程序
//注意:
//退款日期超过可退款期限后(如三个月可退款),支付宝系统发送该交易状态通知
} else if ("TRADE_SUCCESS".equals(tradeStatus)) {
// TODO 根据订单号,做幂等处理,并且在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱
log.warn("=========== 根据订单号,做幂等处理 ===========");
//判断该笔订单是否在商户网站中已经做过处理
//如果没有做过处理,根据订单号(out_trade_no在商户网站的订单系统中查到该笔订单的详细,并执行商户的业务程序
//如果有做过处理,不执行商户的业务程序
// 此处代表交易已经成果,编写实际页面代码
// 比如:重置成功,那么往数据库中写入重置金额
log.info("notifyId:" + notifyId);
log.info("notifyTime:" + notifyTime);
log.info("trade_no:" + tradeNo);
log.info("outTradeNo:" + outTradeNo);
log.info("totalAmount:" + totalAmount);
}
} finally {
//要主动释放锁
lock.unlock();
}
}
out.print("success");
}
/**
* 完成支付后的同步通知
* 页面跳转同步通知页面路径,接收支付宝回调后,再封装页面数据,直接返回相应页面到前端
*
* @param request
* @param response
* @throws IOException
*/
@GetMapping("/return")
public String returnUrl(HttpServletRequest request, HttpServletResponse response) throws Exception {
log.info("同步通知");
//乱码解决,这段代码在出现乱码时使用
request.setCharacterEncoding("utf-8");
//获取支付宝GET过来反馈信息
Map<String, String> params = new HashMap<>(8);
Map<String, String[]> requestParams = request.getParameterMap();
for (Map.Entry<String, String[]> stringEntry : requestParams.entrySet()) {
String[] values = stringEntry.getValue();
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
params.put(stringEntry.getKey(), valueStr);
}
// 通知ID
String notifyId = new String(params.get("notify_id").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
// 通知时间
String notifyTime = new String(params.get("notify_time").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
//商户订单号,之前生成的带用户ID的订单号
String outTradeNo = new String(params.get("out_trade_no").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
//支付宝交易号
String tradeNo = new String(params.get("trade_no").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
//付款金额
String totalAmount = new String(params.get("total_amount").getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
log.info("notifyId:" + notifyId);
log.info("notifyTime:" + notifyTime);
log.info("trade_no:" + tradeNo);
log.info("outTradeNo:" + outTradeNo);
log.info("totalAmount:" + totalAmount);
//调用SDK验证签名
boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayConfig.getPublicKey(), AlipayConstants.CHARSET_UTF8, AlipayConstants.SIGN_TYPE_RSA2);![在这里插入图片描述](https://img-blog.csdnimg.cn/79915c5b8acf49cda4bcfe6758b589ac.jpeg#pic_center)
if (signVerified) {
System.out.println("验签成功-跳转到成功后页面");
//跳转至支付成功后的页面,
return "redirect:https://baidu.com";
} else {
System.out.println("验签失败-跳转到充值页面让用户重新充值");
return "redirect:https://google.com";
}
}
}
文章浏览阅读2.6w次。文件夹权限问题Linux、Fedora、Ubuntu修改文件、文件夹权限的方法差不多。很多人开始接触Linux时都很头痛Linux的文件权限问题。这里告诉大家如何修改Linux文件-文件夹权限。以主文件夹下的一个名为cc的文件夹为例。 下面一步一步介绍如何修改权限: 1.打开终端。输入su(没 Linux、Fedora、Ubuntu修改文件、文件夹权限的方法差不多。很多人开始接触Lin_linux 文件夹权限 影响ftp listfles
文章浏览阅读186次。JavaScript调用Python程序_javascript 调用python
文章浏览阅读300次。生物信息学习的正确姿势NGS系列文章包括NGS基础、在线绘图、转录组分析(Nature重磅综述|关于RNA-seq你想知道的全在这)、ChIP-seq分析(ChIP-seq基本分析流..._bash教程 国外
文章浏览阅读4.3k次,点赞11次,收藏65次。首先看张图:对于一个数据库系统来说,假设这个系统没有运行,我们所能看到的和这个数据库相关的无非就是几个基于操作系统的物理文件,这是从静态的角度来看,如果从动态的角度来看呢,也就是说这个数据库系统运行起来了,能够对外提供服务了,那就意外着数据库系统启动了自己的一个实例,综合以上2个角度,Oracle如何定义上述描述呢?我们来引入第一个概念,Oracle服务器,所谓Oracle服务器是一个数据库管理系统,它包括一个Oracle实例(动态)和一个Oracle数据库(静态)。Oracle实例是一个运行的概念(_oracle
文章浏览阅读1.3k次,点赞2次,收藏14次。基于模拟集成电路仿真软件cadence零基础入门的一点学习记录_layout如何提取参数
文章浏览阅读1.7k次。Notion与Obsidian是目前市场上功能最强大,使用用户最多,门槛也最高的两款笔记软件。本文不会对Notion和Obsidian两款笔记软件的优劣进行对比,但从两者的存储方式,我们可以看到Notion和Obsidian是两个极端——前者只支持云存储,后者只支持本地存储。_obsidian同步方案
文章浏览阅读2.2k次。最近使用java写了个爬虫,可能我对java比较熟悉,所以相对于python来说,我觉得用java写更得心应手些。我采用的是java的jsoup,以及解析用到的json先放上学校教务系统的url http://222.200.98.147首先可以看到,这里是需要验证码输入的,所以我使用了以下的思路:第一步,先访问验证码所在的url,把图片下载到本地,然后保存cookie_java实现从学校教务网上爬取数据(
文章浏览阅读5.6k次。1. MySQL5.7.21启动时报错:[ERROR] Can't find error-message file '/data/mysql/3307/share/errmsg.sys'. Check error-message file location and 'lc-messages-dir' configuration directive.2. 登录MySQL查看系统全局参数:..._can't find error-message file '/data/mysql/share/errmsg.sys'. check error-me
文章浏览阅读2.5w次,点赞58次,收藏419次。文章目录1. 前言1.1. 设计要求2. 硬件原理2.1. 时钟信号(晶振)2.2. 按键开关2.3. 数码管显示3. 原理图3.1. 仿真原理图3.2. AD原理图3.3. PCB图4. 软件设计4.1. 初版代码(无年月日)4.2. 终版代码5. 元器件清单5.1. 仿真软件5.2. 实物1. 前言在此之前我们已经学习了单片机的定时器、中断、数码管。这篇文章主要讲述如何用上述的知识自己制作一个基于51单片机的数字时钟。1.1. 设计要求(1)主电路由秒信号发生器、“时、分、秒”计数器_51单片机 时钟
文章浏览阅读3.4k次。任意两个给定区间的交集,称为公共区间(如:[1,2],[2,3]的公共区间为[2,2],[3,5],[3,6]的公共区间为[3,5])。公共区间之间若存在交集,则需要合并(如:[1,3],[3,5]区间存在交集[3,3],需合并为[1,5])。区间元素为 X: -10000_给定一组闭区间,其中部分区间存在交集,任意两个给定区间的交集,称为公共区间
文章浏览阅读1.7k次。根据官网说明配置的yum源,今天用yum下载Zabbix时莫名的报错,经过几番折腾,找到了解决方法。一、报错如下:二、 解决方法:[root@VM_0_6_centos ~]# cat /etc/yum.repos.d/zabbix.repo # 这个yum源是官方地址,服务器应该是在国外的,下载的时候报错[zabbix] ..._warning: failed loading '/etc/yum.repos.d/zabbix.repo', skipping.
文章浏览阅读6.4k次,点赞2次,收藏10次。5.9.1实验目的本章实验的目的是:1)了解和熟悉人机界面设计过程管理的相关知识;2)了解和评价游戏软件的人机交互设计,提高自己的评价能力,提高自己对设计水平的鉴赏能力。5.9.2工具/准备工作在开始本实验之前,请回顾课文的相关内容。需要准备一台能够访问因特网的计算机。5.9.3实验内容与步骤1.概念理解1)成功的用户界面开发有4个支柱,它们能够帮助用户界面架构师将好的思想转化为成功的系统。经验表明,每个支柱都能在此过程中产生..._人机交互界面设计演练