【Java】2021年Java对接APP支付宝API笔记
|字数总计:3.5k|阅读时长:13分钟|阅读量:|
在图标的设计上,微信和支付宝都选择超出背景部分。
## 前言
由于工作需求,需要添加统一支付功能,微信已经有人对接过,因此我主要负责对接支付宝,本文主要记录Java对接支付宝的步骤,以及服务改造。
支付宝
API文档
确保支付所需的证书和参数正确下,我第一步选择查看官方API文档:https://opendocs.alipay.com/apis
由于项目是APP项目,且只涉及到支付与退款,因此我选择查看 alipay.trade.app.pay(app支付接口2.0)
和 alipay.trade.refund(统一收单交易退款接口)
。
APP支付的含义是:外部商户APP唤起快捷SDK创建订单并支付。因此,我们得知,是APP通过阿里SDK调起支付宝,对于后端而言,我们只需要生成 符合规则的订单串
给前端,由前端唤起即可。
在查看支付宝官方的请求示例后,我并没有直接动手按照请求示例的代码开始编写测试,而是选择先Google下 ,有没有更好的服务端SDK供我们使用,果不其然,我发现了支付宝官方升级版SDK, Alipay Easy SDK:https://github.com/alipay/alipay-easysdk
对比
Alipay Easy SDK |
Alipay SDK |
极简代码风格,更贴近自然语言阅读习惯 |
传统代码风格,需要多行代码完成一个接口的调用 |
Factory单例全局任何地方都可直接引用 |
AlipayClient实例需自行创建并在上下文中传递 |
API中只保留高频场景下的必备参数,同时提供低频可选参数的装配能力 |
没有区分高低频参数,单API最多可达数十个入参,对普通开发者的干扰较大 |
引入所需的 Gradle (Maven请自行搜索)
1
| implementation group: 'com.alipay.sdk', name: 'alipay-easysdk', version: '2.2.0'
|
证书参数注入
第一步,我先根据SDK和文档,定义 公共请求参数
(可自行声明为 Java Class)
1 2 3 4 5 6 7 8 9 10 11 12 13
| message AlipayParam { string appId = 1; string privateKey = 2; string publicKey = 3; string appCertPath = 4; string aliPayCertPath = 5; string aliPayRootCertPath = 6; string notifyUrl = 7; string encryptKey = 8; string protocol = 9; string gatewayHost = 10; string signType = 11; };
|
第二步,我需要注入支付宝所需要的参数和证书等文件,我整合成了 Util 方便日后使用(或者用启动注入的方式)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| import com.alipay.easysdk.factory.Factory; import com.alipay.easysdk.kernel.Config; import org.slf4j.Logger; import org.slf4j.LoggerFactory;
public class ConfigUtil {
private static final Logger logger = LoggerFactory.getLogger(ConfigUtil.class);
private static final String INIT_PROTOCOL = "https"; private static final String INIT_GATEWAY_HOST = "openapi.alipay.com"; private static final String INIT_SIGN_TYPE = "RSA2";
public static void run(AlipayParam AlipayParam) { logger.info("注入证书 -> {}", AlipayParam); String protocol = AlipayParam.getProtocol(); String gatewayHost = AlipayParam.getGatewayHost(); String signType = AlipayParam.getSignType();
Config config = new Config(); config.protocol = ObjectUtils.isEmpty(protocol) ? INIT_PROTOCOL : protocol; config.gatewayHost = ObjectUtils.isEmpty(gatewayHost) ? INIT_GATEWAY_HOST : gatewayHost; config.signType = ObjectUtils.isEmpty(signType) ? INIT_SIGN_TYPE : signType; config.appId = AlipayParam.getAppId();
config.merchantPrivateKey = AlipayParam.getPrivateKey();
config.merchantCertPath = AlipayParam.getAppCertPath(); config.alipayCertPath = AlipayParam.getAliPayCertPath(); config.alipayRootCertPath = AlipayParam.getAliPayRootCertPath();
config.notifyUrl = AlipayParam.getNotifyUrl();
config.encryptKey = AlipayParam.getEncryptKey(); Factory.setOptions(config); }
}
|
统一下单
紧接着,我着手于这个SDK中的APP支付接口 pay(subject: string, outTradeNo: string, totalAmount: string)
,但很快,我就遇到了瓶颈。为什么呢?
因为这个参数的入参只有 订单标题
、金额
和 商户订单号
,而我需要 订单超时时间
和本系统流转的 公用回传参数
,也就是说,我需要设定最晚支付时间和本系统的订单号(我传给支付宝,它再回传给我),而SDK中的方法显然不能满足我的需求。查看源码发现,batchOptional(java.util.Map<String, Object> optionalArgs)
可批量设置API入参中没有的其他可选业务请求参数(biz_content下的字段),伪码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ConfigUtil.run(alipayParam);
String timeOutExpressStr = String.format("%sm", timeoutExpress); AlipayTradeAppPayResponse alipayTradeAppPayResponse = Factory.Payment.App() .batchOptional(BeanUtil.beanToMap(new UnifiedOrderDto(subject, outTradeNo, totalAmount, timeOutExpressStr, URLEncoder.encode(passBackParams, StandardCharsets.UTF_8)), true, true)) .pay(subject, outTradeNo, totalAmount);
|
由于 batchOptional
方法的入参是 Map
,因此我整合下单所需要的 biz_content
,并声明为 UnifiedOrderDto
,通过 BeanUtil.beanToMap()
方法,驼峰命名的属性自动替换为下划线,并转换成Map,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
| import com.google.common.base.Objects;
import java.io.UnsupportedEncodingException;
public class UnifiedOrderDto {
private String subject;
private String outTradeNo;
private String totalAmount;
private String timeoutExpress;
private String passbackParams;
public UnifiedOrderDto(String subject, String outTradeNo, String totalAmount, String timeoutExpress, String passbackParams) throws UnsupportedEncodingException { this.subject = subject; this.outTradeNo = outTradeNo; this.totalAmount = totalAmount; this.timeoutExpress = timeoutExpress; this.passbackParams = passbackParams; }
public String getPassbackParams() { return passbackParams; }
public void setPassbackParams(String passbackParams) { this.passbackParams = passbackParams; }
public String getSubject() { return subject; }
public void setSubject(String subject) { this.subject = subject; }
public String getOutTradeNo() { return outTradeNo; }
public void setOutTradeNo(String outTradeNo) { this.outTradeNo = outTradeNo; }
public String getTotalAmount() { return totalAmount; }
public void setTotalAmount(String totalAmount) { this.totalAmount = totalAmount; }
public String getTimeoutExpress() { return timeoutExpress; }
public void setTimeoutExpress(String timeoutExpress) { this.timeoutExpress = timeoutExpress; }
@Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof UnifiedOrderDto)) return false; UnifiedOrderDto that = (UnifiedOrderDto) o; return Objects.equal(subject, that.subject) && Objects.equal(outTradeNo, that.outTradeNo) && Objects.equal(totalAmount, that.totalAmount) && Objects.equal(timeoutExpress, that.timeoutExpress) && Objects.equal(passbackParams, that.passbackParams); }
@Override public int hashCode() { return Objects.hashCode(subject, outTradeNo, totalAmount, timeoutExpress, passbackParams); }
@Override public String toString() { return "UnifiedOrderDto{" + "subject='" + subject + '\'' + ", outTradeNo='" + outTradeNo + '\'' + ", " + "totalAmount='" + totalAmount + '\'' + ", timeoutExpress='" + timeoutExpress + '\'' + ", " + "passbackParams='" + passbackParams + '\'' + '}'; } }
|
接下来我们需要验证生成的 alipayTradeAppPayResponse
是否正确,伪码如下:
1 2 3
| if (ResponseChecker.success(alipayTradeAppPayResponse)) { response = alipayTradeAppPayResponse.getBody(); }
|
此时,又出现了个疑问,支付宝APP支付接口2.0文档中,公共响应参数
是5个,而我怎么尝试上方代码,都无法返回 code
状态码,后来发现,这是APP移动端支付,仅提供证书下生成对应的串,生成串这个步骤其实并没有访问支付宝的网关,完全是本地自己生成的,因此这个方法也不存在 code
的概念。
前端通过SDK附带此串,唤起支付宝,无报错,能支付,即可。
P.S. 有一次报错是调起支付宝后,说我生成的串不对,仔细发现,有两个证书传反了……
统一退款
统一退款的原理和下单是一个套路,伪码如下:
1 2 3 4 5 6 7 8 9
| ConfigUtil.run(alipayParam);
Map<String, Object> stringObjectMap = BeanUtil.beanToMap( new RefundParamDto(outTradeNo, outRequestNo, refundAmount, refundRoyaltyDto), true, true); AlipayTradeRefundResponse alipayTradeRefundResponse = Factory.Payment.Common().batchOptional(stringObjectMap)
|
验签
1 2 3 4 5 6 7 8
| Map<String, String> parametersMap = request.getParametersMap();
ConfigUtil.run(alipayParam);
Boolean flag = Factory.Payment.Common().verifyNotify(parametersMap);
|
回调/异步通知
支付宝异步通知文档:https://opensupport.alipay.com/support/helpcenter/193/201602472200?ant_source=opendoc
正常的支付流程:
用户下单
-> 后端调用支付宝统一下单接口
->后端生成订单串,返回前端
->前端根据订单串唤起支付宝
->支付宝根据订单串支付
->支付成功
->支付宝发送下单的异步通知给后端
->后端解析异步通知的参数
->后端调用支付宝验签接口
->验签成功,执行本系统其他业务逻辑(已支付),通知支付宝接收异步回调成功
->支付宝收到成功message,不再发送下单的异步通知
三个月后:
支付宝发送订单关闭(禁止退款)的异步通知给后端
->后端调用支付宝验签接口
->验签成功,执行本系统其他业务逻辑(禁止申请退款),通知支付宝接收异步回调成功
->支付宝收到成功message,不再发送订单关闭的异步通知
支付宝常量类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
|
public class AliPayConstant {
public static final String CALL_BACK_SUCCESS = "success";
public static final String CALL_BACK_FAILED = "fail";
public static final String TRADE_SUCCESS = "TRADE_SUCCESS";
public static final String TRADE_CLOSED = "TRADE_CLOSED";
public static final String TRADE_FINISHED = "TRADE_FINISHED";
@Deprecated public static final String CALLBACK_PATH = "你的notifyUrl";
public static final String CODE_SUCCESS = "10000";
}
|
回调伪码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
|
@PostMapping(value = "你的notifyUrl", consumes = "application/x-www-form-urlencoded") public String verifyNotify(HttpServletRequest request) { Map<String, String[]> parameterMap = request.getParameterMap(); Map<String, Object> map = new HashMap<>(16); parameterMap.forEach((s1, s2) -> map.put(s1,s2.length > 1 ? Arrays.asList(s2): s2[0])); Map<String, String> paramMap = mapObject2String(map); Boolean flag = Factory.Payment.Common().verifyNotify(paramMap); if (!flag) { return "success"; } String tradeStatus = paramMap.get("trade_status"); switch (tradeStatus){ case "TRADE_SUCCESS" -> { String invoiceAmount = paramMap.get("invoice_amount"); if (!ObjectUtils.isEmpty(invoiceAmount)) { logger.info("回调类型:TRADE_SUCCESS 交易支付成功"); } else { String refundFee = paramMap.get("refund_fee"); logger.info("回调类型:TRADE_SUCCESS 部分退款成功"); } } case "TRADE_CLOSED" -> { if (!ObjectUtils.isEmpty(paramMap.get("out_biz_no")) && !ObjectUtils.isEmpty(paramMap.get("refund_fee")) && !ObjectUtils.isEmpty(paramMap.get("gmt_refund"))){ logger.info("回调类型:TRADE_CLOSED 支付完成后全额退款"); } else { logger.info("回调类型:TRADE_CLOSED 未付款交易超时关闭"); } } default -> { logger.info("回调类型:TRADE_FINISHED 交易结束不可退款"); } } return "success"; }
private Map<String,String> mapObject2String(Map<String,Object> map) { Map<String,String> returnMap = new HashMap<>(32); if (!ObjectUtils.isEmpty(map)){ map.forEach((k, v) -> returnMap.put(k,String.valueOf(v))); } return returnMap; }
|
反思
反思一下,这样设计的下单、退款、验签和回调有怎样的局限性?
- 证书参数注入是有状态的,如果多套证书参数怎么办?一直在配置文件中写吗?
- 下单和退款是有状态的,如果我想剥离支付宝的下单、退款和验签为一个微服务,那多套证书又该如何注入呢?一直在配置文件中写吗?一直维护多个服务的配置文件吗?
思路:
- 支付宝单独一个微服务(无状态),支付功能一个微服务(有状态)。
- 证书参数由
支付功能的微服务
(有状态)流式发送到支付宝微服务
(无状态),其余参数存到数据库中。
- 支付回调写在
支付功能的微服务
中(有状态)
- 在支付宝下单时记录日志,记录读取的参数是哪套证书相关的,并将日志id发送给支付宝,让它在回调时在发送给我们,我们在回调中收到日志id,根据此id查询下单时的证书参数,进行验签。
P.S. :微信回调由于参数解析时就需要验签,因此我们可以在回调地址上做手脚,比如@PostMapping("/xxx/xxx/{logId}/{systemId}")
。