SpringBoot集成支付宝 - 少走弯路就看这篇
最近在做一个网站,后端采用了SpringBoot,需要集成支付宝进行线上支付,在这个过程中研究了大量支付宝的集成资料,也走了一些弯路,现在总结出来,相信你读完也能轻松集成支付宝支付。
在开始集成支付宝支付之前,我们需要准备一个支付宝商家账户,如果是个人开发者,可以通过注册公司或者让有公司资质的单位进行授权,后续在集成相关API的时候需要提供这些信息。
下面我以电脑网页端在线支付为例,介绍整个从集成、测试到上线的具体流程。
1. 预期效果展示
在开始之前我们先看下我们要达到的最后效果,具体如下:
- 前端点击支付跳转到支付宝界面
- 支付宝界面展示付款二维码
- 用户手机端支付
- 完成支付,支付宝回调开发者指定的url。
2. 开发流程
2.1 沙盒调试
支付宝为我们准备了完善的沙盒开发环境,我们可以先在沙盒环境调试好程序,后续新建好应用并成功上线后,把程序中对应的参数替换为线上参数即可。
1. 创建沙盒应用
直接进入 https://open.alipay.com/develop/sandbox/app 创建沙盒应用即可,
这里因为是测试环境,我们就选择系统默认密钥就行了,下面选择公钥模式,另外应用网关地址就是用户完成支付之后,支付宝会回调的url。在开发环境中,我们可以采用内网穿透的方式,将我们本机的端口暴露在某个公网地址上,这里推荐 https://natapp.cn/ ,可以免费注册使用。
2. SpringBoot代码实现
在创建好沙盒应用,获取到密钥,APPID,商家账户PID等信息之后,就可以在测试环境开发集成对应的API了。这里我以电脑端支付API为例,介绍如何进行集成。
关于电脑网站支付的详细产品介绍和API接入文档可以参考:https://opendocs.alipay.com/open/repo-0038oa?ref=api 和 https://opendocs.alipay.com/open/270/01didh?ref=api
- 步骤1, 添加alipay sdk对应的Maven依赖。
<!-- alipay -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.35.132.ALL</version>
</dependency>
- 步骤2,添加支付宝下单、支付成功后同步调用和异步调用的接口。
这里需要注意,同步接口是用户完成支付后会自动跳转的地址,因此需要是Get请求。异步接口,是用户完成支付之后,支付宝会回调来通知支付结果的地址,所以是POST请求。
@RestController
@RequestMapping("/alipay")
public class AliPayController {
@Autowired
AliPayService aliPayService;
@PostMapping("/order")
public GenericResponse<Object> placeOrderForPCWeb(@RequestBody AliPayRequest aliPayRequest) {
try {
return aliPayService.placeOrderForPCWeb(aliPayRequest);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@PostMapping("/callback/async")
public String asyncCallback(HttpServletRequest request) {
return aliPayService.orderCallbackInAsync(request);
}
@GetMapping("/callback/sync")
public void syncCallback(HttpServletRequest request, HttpServletResponse response) {
aliPayService.orderCallbackInSync(request, response);
}
}
- 步骤3,实现Service层代码
这里针对上面controller中的三个接口,分别完成service层对应的方法。下面是整个支付的核心流程,其中有些地方需要根据你自己的实际情况进行保存订单到DB或者检查订单状态的操作,这个可以根据实际业务需求进行设计。
public class AliPayService {
@Autowired
AliPayHelper aliPayHelper;
@Resource
AlipayConfig alipayConfig;
@Transactional(rollbackFor = Exception.class)
public GenericResponse<Object> placeOrderForPCWeb(AliPayRequest aliPayRequest) throws IOException {
log.info("【请求开始-在线购买-交易创建】*********统一下单开始*********");
String tradeNo = aliPayHelper.generateTradeNumber();
String subject = "购买套餐1";
Map<String, Object> map = aliPayHelper.placeOrderAndPayForPCWeb(tradeNo, 100, subject);
if (Boolean.parseBoolean(String.valueOf(map.get("isSuccess")))) {
log.info("【请求开始-在线购买-交易创建】统一下单成功,开始保存订单数据");
//保存订单信息
// 添加你自己的业务逻辑,主要是保存订单数据
log.info("【请求成功-在线购买-交易创建】*********统一下单结束*********");
return new GenericResponse<>(ResponseCode.SUCCESS, map.get("body"));
}else{
log.info("【失败:请求失败-在线购买-交易创建】*********统一下单结束*********");
return new GenericResponse<>(ResponseCode.INTERNAL_ERROR, String.valueOf(map.get("subMsg")));
}
}
// sync return page
public void orderCallbackInSync(HttpServletRequest request, HttpServletResponse response) {
try {
OutputStream outputStream = response.getOutputStream();
//通过设置响应头控制浏览器以UTF-8的编码显示数据,如果不加这句话,那么浏览器显示的将是乱码
response.setHeader("content-type", "text/html;charset=UTF-8");
String outputData = "支付成功,请返回网站并刷新页面。";
/**
* data.getBytes()是一个将字符转换成字节数组的过程,这个过程中一定会去查码表,
* 如果是中文的操作系统环境,默认就是查找查GB2312的码表,
*/
byte[] dataByteArr = outputData.getBytes("UTF-8");//将字符转换成字节数组,指定以UTF-8编码进行转换
outputStream.write(dataByteArr);//使用OutputStream流向客户端输出字节数组
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public String orderCallbackInAsync(HttpServletRequest request) {
try {
Map<String, String> map = aliPayHelper.paramstoMap(request);
String tradeNo = map.get("out_trade_no");
String sign = map.get("sign");
String content = AlipaySignature.getSignCheckContentV1(map);
boolean signVerified = aliPayHelper.CheckSignIn(sign, content);
// check order status
// 这里在DB中检查order的状态,如果已经支付成功,无需再次验证。
if(从DB中拿到order,并且判断order是否支付成功过){
log.info("订单:" + tradeNo + " 已经支付成功,无需再次验证。");
return "success";
}
//验证业务数据是否一致
if(!checkData(map, order)){
log.error("返回业务数据验证失败,订单:" + tradeNo );
return "返回业务数据验证失败";
}
//签名验证成功
if(signVerified){
log.info("支付宝签名验证成功,订单:" + tradeNo);
// 验证支付状态
String tradeStatus = request.getParameter("trade_status");
if(tradeStatus.equals("TRADE_SUCCESS")){
log.info("支付成功,订单:"+tradeNo);
// 更新订单状态,执行一些业务逻辑
return "success";
}else{
System.out.println("支付失败,订单:" + tradeNo );
return "支付失败";
}
}else{
log.error("签名验证失败,订单:" + tradeNo );
return "签名验证失败.";
}
} catch (IOException e) {
log.error("IO exception happened ", e);
throw new RuntimeException(ResponseCode.INTERNAL_ERROR, e.getMessage());
}
}
public boolean checkData(Map<String, String> map, OrderInfo order) {
log.info("【请求开始-交易回调-订单确认】*********校验订单确认开始*********");
//验证订单号是否准确,并且订单状态为待支付
if(验证订单号是否准确,并且订单状态为待支付){
float amount1 = Float.parseFloat(map.get("total_amount"));
float amount2 = (float) order.getOrderAmount();
//判断金额是否相等
if(amount1 == amount2){
//验证收款商户id是否一致
if(map.get("seller_id").equals(alipayConfig.getPid())){
//判断appid是否一致
if(map.get("app_id").equals(alipayConfig.getAppid())){
log.info("【成功:请求开始-交易回调-订单确认】*********校验订单确认成功*********");
return true; }
}
}
}
log.info("【失败:请求开始-交易回调-订单确认】*********校验订单确认失败*********");
return false; }
}
- 步骤4,实现alipayHelper类。这个类里面对支付宝的接口进行封装。
public class AliPayHelper {
@Resource
private AlipayConfig alipayConfig;
//返回数据格式
private static final String FORMAT = "json";
//编码类型
private static final String CHART_TYPE = "utf-8";
//签名类型
private static final String SIGN_TYPE = "RSA2";
/*支付销售产品码,目前支付宝只支持FAST_INSTANT_TRADE_PAY*/
public static final String PRODUCT_CODE = "FAST_INSTANT_TRADE_PAY";
private static AlipayClient alipayClient = null;
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmssSSS");
private static final Random random = new Random();
@PostConstruct
public void init(){
alipayClient = new DefaultAlipayClient(
alipayConfig.getGateway(),
alipayConfig.getAppid(),
alipayConfig.getPrivateKey(),
FORMAT,
CHART_TYPE,
alipayConfig.getPublicKey(),
SIGN_TYPE);
};
/*================PC网页支付====================*/
/**
* 统一下单并调用支付页面接口
* @param outTradeNo
* @param totalAmount
* @param subject
* @return
*/
public Map<String, Object> placeOrderAndPayForPCWeb(String outTradeNo, float totalAmount, String subject){
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setNotifyUrl(alipayConfig.getNotifyUrl());
request.setReturnUrl(alipayConfig.getReturnUrl());
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", outTradeNo);
bizContent.put("total_amount", totalAmount);
bizContent.put("subject", subject);
bizContent.put("product_code", PRODUCT_CODE);
request.setBizContent(bizContent.toString());
AlipayTradePagePayResponse response = null;
try {
response = alipayClient.pageExecute(request);
} catch (AlipayApiException e) {
e.printStackTrace();
}
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("isSuccess", response.isSuccess());
if(response.isSuccess()){
log.info("调用成功");
log.info(JSON.toJSONString(response));
resultMap.put("body", response.getBody());
} else {
log.error("调用失败");
log.error(response.getSubMsg());
resultMap.put("subMsg", response.getSubMsg());
}
return resultMap;
}
/**
* 交易订单查询
* @param out_trade_no
* @return
*/
public Map<String, Object> tradeQueryForPCWeb(String out_trade_no){
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("trade_no", out_trade_no);
request.setBizContent(bizContent.toString());
AlipayTradeQueryResponse response = null;
try {
response = alipayClient.execute(request);
} catch (AlipayApiException e) {
e.printStackTrace();
}
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("isSuccess", response.isSuccess());
if(response.isSuccess()){
System.out.println("调用成功");
System.out.println(JSON.toJSONString(response));
resultMap.put("status", response.getTradeStatus());
} else {
System.out.println("调用失败");
System.out.println(response.getSubMsg());
resultMap.put("subMsg", response.getSubMsg());
}
return resultMap;
}
/**
* 验证签名是否正确
* @param sign
* @param content
* @return
*/
public boolean CheckSignIn(String sign, String content){
try {
return AlipaySignature.rsaCheck(content, sign, alipayConfig.getPublicKey(), CHART_TYPE, SIGN_TYPE);
} catch (AlipayApiException e) {
e.printStackTrace();
}
return false;
}
/**
* 将异步通知的参数转化为Map
* @return
*/
public Map<String, String> paramstoMap(HttpServletRequest request) throws UnsupportedEncodingException {
Map<String, String> params = new HashMap<String, String>();
Map<String, String[]> requestParams = request.getParameterMap();
for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ",";
}
// 乱码解决,这段代码在出现乱码时使用。
// valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
params.put(name, valueStr);
}
return params;
}
}
- 步骤5,封装config类,用于存放所有的配置属性。
@Data
@Component
@ConfigurationProperties(prefix = "alipay")
public class AlipayConfig {
private String gateway;
private String appid;
private String pid;
private String privateKey;
private String publicKey;
private String returnUrl;
private String notifyUrl;
}
另外需要在application.properties中,准备好上述对应的属性。
# alipay config
alipay.gateway=https://openapi.alipaydev.com/gateway.do
alipay.appid=your_appid
alipay.pid=your_pid
alipay.privatekey=your_private_key
alipay.publickey=your_public_key
alipay.returnurl=完成支付后的同步跳转地址
alipay.notifyurl=完成支付后,支付宝会异步回调的地址
3. 前端代码实现
前端代码只需要完成两个功能,
- 根据用户的请求向后端发起支付请求。
- 直接提交返回数据完成跳转。
下面的例子中,我用typescript实现了用户点击支付之后的功能,
async function onPositiveClick() {
paymentLoading.value = true
const { data } = await placeAlipayOrder<string>({
//你的一些请求参数,例如金额等等
})
const div = document.createElement('divform')
div.innerHTML = data
document.body.appendChild(div)
document.forms[0].setAttribute('target', '_blank')
document.forms[0].submit()
showModal.value = false
paymentLoading.value = false
}
2.2 创建并上线APP
完成沙盒调试没问题之后,我们需要创建对应的支付宝网页应用并上线。
登录 https://open.alipay.com/develop/manage 并选择创建网页应用,
填写应用相关信息:
创建好应用之后,首先在开发设置中,设置好接口加签方式以及应用网关。
注意密钥选择RSA2,其他按照上面的操作指南一步步走即可,注意保管好自己的私钥和公钥。
之后在产品绑定页,绑定对应的API,比如我们这里是PC网页端支付,找到对应的API绑定就可以了。如果第一次绑定,可能需要填写相关的信息进行审核,按需填写即可,一般审核一天就通过了。
最后如果一切就绪,我们就可以把APP提交上线了,上线成功之后,我们需要把下面SpringBoot中的properties替换为线上APP的信息,然后就可以在生产环境调用支付宝的接口进行支付了。
# alipay config
alipay.gateway=https://openapi.alipaydev.com/gateway.do
alipay.appid=your_appid
alipay.pid=your_pid
alipay.privatekey=your_private_key
alipay.publickey=your_public_key
alipay.returnurl=完成支付后的同步跳转地址
alipay.notifyurl=完成支付后,支付宝会异步回调的地址
参考:
- https://blog.csdn.net/xqnode/article/details/124457790
- https://blog.51cto.com/u_15754099/5585676
- https://zhuanlan.zhihu.com/p/596771147
- https://segmentfault.com/a/1190000041974184
欢迎关注公众号【码老思】,只讲最通俗易懂的原创技术干货。
SpringBoot集成支付宝 - 少走弯路就看这篇的更多相关文章
- springboot集成支付宝的支付(easy版)
SpringBoot对接支付宝 需要先注册账号 到支付宝开发者平台创建网页支付应用 启用公钥模式 需要使用到appId和下面的两个秘钥 写配置信息的代码 1.引入依赖 <dependency&g ...
- springboot集成支付宝的支付(通用版)
[1.引依赖] <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sd ...
- Android App集成支付宝
原地址:http://blog.csdn.net/wenbingoon/article/details/7933078 手机的在线支付,被认为是2012年最看好的功能,我个人认为这也是移动互联网较传统 ...
- ***CodeIgniter框架集成支付宝即时到账支付SDK
本文为CI集成支付宝即时到账支付接口 1.下载支付宝官方demo ;即时到账交易接口(create_direct_pay_by_user)(DEMO下载) 原文地址:https://doc.open. ...
- springboot集成Guava缓存
很久没有写博客了,这段时间一直忙于看论文,写论文,简直头大,感觉还是做项目比较舒服,呵呵,闲话不多说,今天学习了下Guava缓存,这跟Redis类似的,但是适用的场景不一样,学习下吧.今天我们主要是s ...
- 0120 springboot集成Mybatis和代码生成器
在日常开发中,数据持久技术使用的架子使用频率最高的有3个,即spring-jdbc , spring-jpa, spring-mybatis.详情可以看我之前的一篇文章spring操作数据库的3个架子 ...
- iOS开发——高级篇——如何集成支付宝SDK
一.什么是支付宝 第三方支付平台 和内购非常相似内购是用户将钱付款给苹果,之后苹果分成给商户支付宝是用户将钱付款给支付宝,之后支付宝将钱转入我们的账户 使用支付宝前提购买的物品必须是和应用程序无关的. ...
- iOS - (集成支付宝SDK大坑总结)
其实集成支付宝相对于集成微信支付来说,支付宝算是简单的了,后续有空再去研究微信支付,现目前先总结一下集成支付宝所遇到的坑,其实支付宝的坑也不算太多,细算下来大概5-6个左右,但是其报错方式有点恶心,不 ...
- 【springBoot】springBoot集成redis的key,value序列化的相关问题
使用的是maven工程 springBoot集成redis默认使用的是注解,在官方文档中只需要2步; 1.在pom文件中引入即可 <dependency> <groupId>o ...
- 集成“支付宝” -b
大致步骤 1.与支付宝签约获取相关参数 合作者身份 ID 与安全校验码 key2.下载需要导入的文件,做相应设置3.在自己的项目中集成支付的方法代码 详细步骤 1.获取合作者身份 ID 与安全校验码 ...
随机推荐
- python3各数据类型的常用方法
python3数据类型包括: 数字.字符串str.列表list.元组tuple.字典dict.集合set.布尔bool 1.字符串(str)-可变-用"".''定义 (1)uppe ...
- 被吐槽 GitHub仓 库太大,直接 600M 瘦身到 6M,这下舒服了
大家好,我是小富- 前言 忙里偷闲学习了点技术写了点demo代码,打算提交到我那 2000Star 的Github仓库上,居然发现有5个Issues,最近的一条日期已经是2022/8/1了,以前我还真 ...
- day120:MoFang:修复宠物喂食饱食度不增加的BUG&修复宠物死亡导致数据错乱的BUG
目录 BUG1:修复宠物喂食饱食度未增加的BUG BUG2:修复当用户拥有2个宠物时,如果第1个宠物挂了,会出现第二个宠物变成第1个宠物的情况,会导致数据发生混乱出现bug BUG1:修复宠物喂食饱食 ...
- MySQL(八)哈希索引、AVL树、B树与B+树的比较
Hash索引 简介 这部分略了 Hash索引效率高,为什么还要设计索引结构为树形结构? Hash索引仅能满足 =.<>和IN查询,如果进行范围查询,哈希的索引会退化成O(n):而树型的 ...
- Linux(二)文件权限和压缩
1 搜索查找类 1.1 查找定位文件 find <搜索范围.路径> <选项> find将从指定目录下递归地遍历其各个子目录,将满足条件的文件显示在终端. 选项说明 -name: ...
- TEC-6计算机组成原理实验(简图)
TEC-6计算机组成原理实验
- 容器云平台监控告警体系(五)—— Prometheus发送告警机制
1.概述 在Prometheus的架构中告警被划分为两个部分,在Prometheus Server中定义告警规则以及产生告警,Alertmanager组件则用于处理这些由Prometheus产生的告警 ...
- C# 控制系统任务栏的显示与隐藏
[DllImport("user32.dll")] public static extern int FindWindow(string lpClassName, string l ...
- Spring注解@Conditional相关用法
1.@Conditional注解 @Conditional 是Spring4新提供的注解. 它的作用是按照一定的条件进行判断,满足条件给容器注册bean,否则不注入. 可以作用在方法上,也可以作用在类 ...
- Java双向链表实现队列
将双向链表做简单的改造,即可实现一个FIFO(First Input First Out)队列, 该队列只在头节点出队,尾节点入队. 一般来说定义节点类只需一个后驱节点next即可. 这里保留pre节 ...