Skip to content

支付宝支付接入

🏷️ 支付宝

最近几天给快应用接入了支付宝支付,记录一下做个备忘。

使用的是 Alipay SDK Java 版的证书方式调用。

流程图:

1. 创建订单

服务端调用支付宝接口创建订单,代码直接参考官方 Alipay SDK 文档即可。

java
//构造 client
CertAlipayRequest certAlipayRequest = new CertAlipayRequest();
//设置网关地址
certAlipayRequest.setServerUrl("https://openapi.alipay.com/gateway.do");
//设置应用 Id
certAlipayRequest.setAppId(app_id);
//设置应用私钥
certAlipayRequest.setPrivateKey(privateKey);
//设置请求格式,固定值 json
certAlipayRequest.setFormat("json");
//设置字符集
certAlipayRequest.setCharset(charset);
//设置签名类型
certAlipayRequest.setSignType(sign_type);
//设置应用公钥证书路径
certAlipayRequest.setCertPath(app_cert_path);
//设置支付宝公钥证书路径
certAlipayRequest.setAlipayPublicCertPath(alipay_cert_path);
//设置支付宝根证书路径
certAlipayRequest.setRootCertPath(alipay_root_cert_path);

//构造 client
AlipayClient alipayClient = new DefaultAlipayClient(certAlipayRequest);
//构造 API 请求
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
//发送请求
AlipayTradeQueryResponse response = alipayClient.certificateExecute(request);

比较奇怪的是返回的响应中,除了 body 字段外,其它的都是 null

响应内容:

json
{
    "body": "alipay_root_cert_sn=687b59193f3f462dd5336e5abf83c5d8_02941eef3187dddf3d3b83462e1dfcf6&alipay_sdk=alipay-sdk-java-dynamicVersionNo&app_cert_sn=7eb546098e0ad9016f69143243868330&app_id=2021002100000000&biz_content=%7B%22body%22%3A%22%22%2C%22goods_type%22%3A%220%22%2C%22out_trade_no%22%3A%22P219200688300000000%22%2C%22product_code%22%3A%22QUICK_MSECURITY_PAY%22%2C%22subject%22%3A%22%E5%85%850.01%E5%BE%9720000%E4%B9%A6%E5%B8%81%22%2C%22timeout_express%22%3A%2230m%22%2C%22total_amount%22%3A%220.01%22%7D&charset=UTF-8&format=json&method=alipay.trade.app.pay¬ify_url=https%3A%2F%2Ftest.hyperion-novel-qa-app.mokamrp.com%2Fali%2Fpay%2Fnotify%2Forder%2F2021002100000000&sign=lfiiodhqqp1pwrwioo7olb1e8ftsmhewhziezsycanis75jo9yna4k6czh%2b2ezmo9ebsqfb3o%2fug7ga22b37qoff6pgkklhwjdxpunkqqhbyagkalc5l9zyf6u2s7czdcryt%2flvba3crdt339mdlebfqivzx%2bb2u6loss%2fqwbmfanba0ztq%2bd%2frydc%2bvvleftdrojvycqi8dcqbcfdp%2djcn71j3iywredjsu8htjoqbdorl7le46sdvaav0o%2bh%2f%2bo7vov6dgke31%2bqegokiwqp5bq%2b1nsc2jzd6tkrnlzqc30qijnr0myb%2bk%2bpvrqzsq7lqa4vxbebo6cetsh5ifwg%3d%3D&sign_type=RSA2×tamp=2021-11-26+13%3A49%3A45&version=1.0"
}

2. 查询订单

查询订单代码参考alipay.trade.query 。需要注意的是,如果使用证书的方式,应调用 certificateExecute 方法,否则会报错。

java
AlipayClient alipayClient = new DefaultAlipayClient("https://openapi.alipay.com/gateway.do","app_id","your private_key","json","GBK","alipay_public_key","RSA2");

AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
request.setBizContent("{" +
"  \"out_trade_no\":\"20150320010101001\"," +
"  \"trade_no\":\"2014112611001004680 073956707\"," +
"  \"query_options\":[" +
"    \"trade_settle_info\"" +
"  ]" +
"}");

AlipayTradeQueryResponse response = alipayClient.execute(request);

if(response.isSuccess()){
    System.out.println("调用成功");
} else {
    System.out.println("调用失败");
}

2.1 ACQ.TRADE_NOT_EXIST 交易不存在

支付宝在客户端未支付的情况下(如未能拉起支付宝 APP、未付款、付款失败),使用 out_trade_no 查询订单时,返回的代码为 40004,错误代码为 ACQ.TRADE_NOT_EXIST (交易不存在)。

这个流程使用起来感觉有些怪异,调用创建订单接口时支付宝的服务端应该是会保存订单信息的,结果却返回交易不存在。不知道是不是此时真的没有创建订单,还是因为别的什么原因,故意显示成这样的。

这导致开发者在同步订单状态时有点麻烦,好在创建订单接口提供了一个订单相对超时时间参数(timeoutExpress),官方示例中使用的是 30 分钟。还有另外一个订单绝对超时时间(timeExpire),代码备注中显示推荐使用这个参数。

java
model.setTimeoutExpress("30m");

客户端未支付时查询订单返回的响应内容如下:

json
{
    "msg": "Business Failed",
    "pointAmount": "0.00",
    "code": "40004",
    "invoiceAmount": "0.00",
    "body": "{\"alipay_trade_query_response\":{\"code\":\"40004\",\"msg\":\"Business Failed\",\"sub_code\":\"ACQ.TRADE_NOT_EXIST\",\"sub_msg\":\"交易不存在\",\"buyer_pay_amount\":\"0.00\",\"invoice_amount\":\"0.00\",\"out_trade_no\":\"P219200688300000000\",\"point_amount\":\"0.00\",\"receipt_amount\":\"0.00\"},\"alipay_cert_sn\":\"78aa9258336cb49f0000000000000000\",\"sign\":\"nxrfqos5/dvvgq5qhpaaqp8uqkxsfugqlh2veggpjxcg0yopo12pitd4jrcvvduh7tfedk/rdd5o7bkrdcqt0h8a6aaocnu7wbhjim9ptlrhcr3ilostjwt7bpstfadlfw108dfbpvxbcz7c3j1ahk+om0hmtxjccr+qfqj2+j3jfjebhvpgbpl9jsiu3frgbysddvqu20iszet6fvufhwsfnkjnbkq+gmjlkujhxxgmavam/dk0mh9eddqxtsxnbggr1d6nnbwcagaaukky/+9zk9wkac5zhzcuxhj820vyumkhz3ojccyhfagfs544rs+iwquoey8ccla0rcimta==\"}",
    "params": {
        "biz_content": "{\"out_trade_no\":\"P219200688300000000\"}"
    },
    "receiptAmount": "0.00",
    "subCode": "ACQ.TRADE_NOT_EXIST",
    "outTradeNo": "P219200688300000000",
    "buyerPayAmount": "0.00",
    "subMsg": "交易不存在"
}

2.2 交易成功

2.2.1 支付成功异步通知

客户端付款成功后,支付宝会发送异步通知到创建订单时指定的 notifyUrl 异步通知地址。

代码参考官方文档中的服务端验证异步通知信息参数示例进行验签。接收参数是个 Map<String, String> 型的实例,使用起来不太方便,设计成这样估计是为了兼容性。

java
//获取支付宝 POST 过来反馈信息 
Map<String,String> params = new HashMap<String,String>();
Map requestParams = request.getParameterMap();
for (Iterator 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);
}
//切记 alipayPublicCertPath 是应用公钥证书路径,请去 open.alipay.com 对应应用下载。 
//boolean AlipaySignature.rsaCertCheckV1(Map<String, String> params, String publicKeyCertPath, String charset,String signType) 
boolean flag = AlipaySignature.rsaCertCheckV1(params,alipayPublicCertPath,charset,"RSA2");

请求 Body 如下(为方便查看,添加了换行,实际的请求体中没有换行):

json
gmt_create=2021-11-26+13%3A50%3A07
&charset=UTF-8
&seller_email=ywu1112%40***.com
&subject=%E5%85%850.01%E5%BE%9720000%E4%B9%A6%E5%B8%81
&sign=oihr%2fajqzzwy2lwoih%2b%2b0tg2vlesrl5odpxpujju1jy%2bxt6ckdtzliqetz75hqzpddkckotsb5qavnzrptax1hspeqshd7unjw25cnptsr4zult9eogzzen501kbntuexhbjwnaogbxplkqskrauwrl5ngavhaxqviyftitzgjlml%2bahc2z6qptrddcbjhcwu4znvjhqfup2ppxymw8ccxwribbsrvi62iizk6r7cdc7p7swlldrdvi9djaadaa2fe4shkqsciddz6hnpolu%2fuxe94albqqeytn4%2bqwkhvzdp%2ffzus3wovgjkgldwl9cnjh9wbjj0youhwpe7sjcyq%3d%3d
&buyer_id=2088000000000000
&invoice_amount=0.01¬ify_id=2021112600222135008052701400000000
&fund_bill_list=%5B%7B%22amount%22%3A%220.01%22%2C%22fundChannel%22%3A%22ALIPAYACCOUNT%22%7D%5D¬ify_type=trade_status_sync
&trade_status=TRADE_SUCCESS
&receipt_amount=0.01
&app_id=2021002100000000
&buyer_pay_amount=0.01
&sign_type=RSA2
&seller_id=2088341000000000
&gmt_payment=2021-11-26+13%3A50%3A08¬ify_time=2021-11-26+13%3A50%3A08
&version=1.0
&out_trade_no=P219200688300000000
&total_amount=0.01
&trade_no=2021112622001452000000000000
&auth_app_id=2021002100000000
&buyer_logon_id=******%40***.com
&point_amount=0.00

2.2.2 订单查询结果

成功支付后再次查询订单,响应如下:

json
{
    "msg": "Success",
    "pointAmount": "0.00",
    "code": "10000",
    "tradeNo": "2021112622001452000000000000",
    "invoiceAmount": "0.00",
    "body": "{\"alipay_trade_query_response\":{\"code\":\"10000\",\"msg\":\"Success\",\"buyer_logon_id\":\"*********.com\",\"buyer_pay_amount\":\"0.00\",\"buyer_user_id\":\"2088000000000000\",\"invoice_amount\":\"0.00\",\"out_trade_no\":\"P219200688300000000\",\"point_amount\":\"0.00\",\"receipt_amount\":\"0.00\",\"send_pay_date\":\"2021-11-26 13:50:08\",\"total_amount\":\"0.01\",\"trade_no\":\"2021112622001452000000000000\",\"trade_status\":\"TRADE_SUCCESS\"},\"alipay_cert_sn\":\"78aa9258336cb49f0000000000000000\",\"sign\":\"ymlqcmmttjsjiuhslujxuzw2zvfi+5p47bqt7h9diclv7pgzc9qhyrnzdkthqfypnknxcum/guusmecubro8udky4bkgelkj+ep8m2iyjtx1+gujq7d2kr/owgc4amxs/vcybjp3/ymcv1/jaduyazyif+xhwif9nk4ayajixrgrdorevfkfcwtysxjasiqr4qaq/fyzgxaqdotpc4s6uczciaeafifd8hpizcejgr425wr/yrdr3kni0umehoatgxeeq1seyfk7c6d/83qcjoq7adxnmdjbgawhuv/8ph3gpbguf0/5lornb1ctd4odpjblrj6slmoehbp0dlizhg==\"}",
    "params": {
        "biz_content": "{\"out_trade_no\":\"P219200688300000000\"}"
    },
    "buyerLogonId": "*********.com",
    "receiptAmount": "0.00",
    "totalAmount": "0.01",
    "outTradeNo": "P219200688300000000",
    "tradeStatus": "TRADE_SUCCESS",
    "buyerPayAmount": "0.00",
    "buyerUserId": "2088000000000000",
    "sendPayDate": 1637905808000
}

需要吐个槽的是 tradeStatus 字段,有一个 TRADE_CLOSED 的状态,即表示 未付款交易超时关闭,又表示 支付完成后全额退款,明明是两个状态,为啥要合成一个?

3. 退款

退款代码参考统一收单交易退款接口上面的示例,同查询订单一样,如果使用证书方式,应改为调用 certificateExecute 方法。

java
AlipayClient alipayClient = new DefaultAlipayClient("https://openapi.alipay.com/gateway.do","app_id","your private_key","json","GBK","alipay_public_key","RSA2");

JSONObject bizContent = new JSONObject();
bizContent.put("trade_no", "2021081722001419121412730660");
bizContent.put("refund_amount", 0.01);
bizContent.put("out_request_no", "HZ01RF001");

//// 返回参数选项,按需传入
//JSONArray queryOptions = new JSONArray();
//queryOptions.add("refund_detail_item_list");
//bizContent.put("query_options", queryOptions);

AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
request.setBizContent(bizContent.toString());

AlipayTradeRefundResponse response = alipayClient.execute(request);

if(response.isSuccess()){
    System.out.println("调用成功");
} else {
    System.out.println("调用失败");
}

退款接口响应如下:

json
{
    "gmtRefundPay": 1637905816000,
    "msg": "Success",
    "refundFee": "0.01",
    "fundChange": "Y",
    "code": "10000",
    "tradeNo": "2021112622001452000000000000",
    "body": "{\"alipay_trade_refund_response\":{\"code\":\"10000\",\"msg\":\"Success\",\"buyer_logon_id\":\"*********.com\",\"buyer_user_id\":\"2088000000000000\",\"fund_change\":\"Y\",\"gmt_refund_pay\":\"2021-11-26 13:50:16\",\"out_trade_no\":\"P219200688300000000\",\"refund_fee\":\"0.01\",\"send_back_fee\":\"0.00\",\"trade_no\":\"2021112622001452000000000000\"},\"alipay_cert_sn\":\"78aa9258336cb49f0000000000000000\",\"sign\":\"fhpud+sccff9iltxgjz6bp8dpojauvrbpe4q2xlhpz83+3ilhrwyhhgqldu7gmx6fd7ugh9rbdguxphqrcgtjbq/hf2eqzlx+cbhtko+o4wpddaraa636fbudizerrg7+ht8uqcapx/+jr3byu7zksgdapjl7fhpntndvfkbn7thbvjbolp/nkcgo+eo8ebr/1tfff2k1smoc5otgtzqmc4wvbrom78bczrgxdbkkniq0tcvo5sr7yyxosxptfhzmw7uzjdwqzusc8rspvlx9wmdmhlc1vng1pm7mce27hdv7zwjta8afkd32cs7haftajkyk8ocov1q2womzgxpqa==\"}",
    "params": {
        "biz_content": "{\"trade_no\":\"2021112622001452000000000000\",\"refund_amount\":0.01,\"out_request_no\":\"FNR219200810000000000\"}"
    },
    "buyerLogonId": "*********.com",
    "sendBackFee": "0.00",
    "outTradeNo": "P219200688300000000",
    "buyerUserId": "2088000000000000"
}

3.1 退款异步通知

由于退款同步结果中内容可能不太准确,这边仍然使用异步通知的方式同步退款状态。退款通知的验签同支付成功时的异步通知一样。

支付宝退款接口没有 notifyUrl 参数,其异步通知仍然使用的是创建订单时 notifyUrl 参数指定的异步通知地址。

请求 Body 如下(为方便查看,添加了换行,实际的请求体中没有换行):

json
gmt_create=2021-11-26+13%3A50%3A07
&charset=UTF-8
&gmt_payment=2021-11-26+13%3A50%3A08
&seller_email=ywu1112%40***.com¬ify_time=2021-11-26+13%3A50%3A16
&subject=%E5%85%850.01%E5%BE%9720000%E4%B9%A6%E5%B8%81
&gmt_refund=2021-11-26+13%3A50%3A15.858
&sign=vavtzne9tk8z7mswrzuljp4b3tvaluypc7srccwiyke0stvpmkg4lmefqzvswgfojgaxzpvtl5yvw%2fyomndl%2btt%2bujhjs%2fiz%2ff4zkbhtaw1skx5gqahcwgmjtmpo7irvvb8ioqx2dsdgjsvcfms3pfnvpuwcqmuo7%2b0toafzi6m%2bu%2bugr%2bclkhq%2frglslejaskdin%2fyapuyasgqyrnl2xf0%2f4ybehobgajdongoux2ckvs6advzvrgi16k2%2fjs4u8o984zvbhknwzynv6ahqzr9fm9nletprfuct%2bhiv61ymwd36fobh%2bhvl6c9a%2bjk0facatd6bvy9u9tihebglig%3d%3d
&buyer_id=2088000000000000
&out_biz_no=FNR219200810000000000
&version=1.0¬ify_id=2021112600222135016052000000000000¬ify_type=trade_status_sync
&out_trade_no=P219200688300000000
&total_amount=0.01
&trade_status=TRADE_CLOSED
&refund_fee=0.01
&trade_no=2021112622001452000000000000
&auth_app_id=2021002100000000
&gmt_close=2021-11-26+13%3A50%3A15
&buyer_logon_id=******%40***.com
&app_id=2021002100000000
&sign_type=RSA2
&seller_id=2088341000000000

3.2 退款查询结果

退款查询代码参照统一收单交易退款查询

java
AlipayClient alipayClient = new DefaultAlipayClient("https://openapi.alipay.com/gateway.do","app_id","your private_key","json","GBK","alipay_public_key","RSA2");

JSONObject bizContent = new JSONObject();
bizContent.put("trade_no", "2021081722001419121412730660");
bizContent.put("out_request_no", "HZ01RF001");

//// 返回参数选项,按需传入
//JSONArray queryOptions = new JSONArray();
//queryOptions.add("refund_detail_item_list");
//bizContent.put("query_options", queryOptions);


AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
request.setBizContent(bizContent.toString());

AlipayTradeFastpayRefundQueryResponse response = alipayClient.execute(request);

if(response.isSuccess()){
    System.out.println("调用成功");
} else {
    System.out.println("调用失败");
}

退款查询响应如下:

json
{
    "gmtRefundPay": 1637905816000,
    "outRequestNo": "FNR219200810000000000",
    "msg": "Success",
    "code": "10000",
    "tradeNo": "2021112622001452000000000000",
    "refundStatus": "REFUND_SUCCESS",
    "body": "{\"alipay_trade_fastpay_refund_query_response\":{\"code\":\"10000\",\"msg\":\"Success\",\"gmt_refund_pay\":\"2021-11-26 13:50:16\",\"out_request_no\":\"FNR219200810000000000\",\"out_trade_no\":\"P219200688300000000\",\"refund_amount\":\"0.01\",\"refund_detail_item_list\":[{\"amount\":\"0.01\",\"fund_channel\":\"ALIPAYACCOUNT\"}],\"refund_status\":\"REFUND_SUCCESS\",\"send_back_fee\":\"0.01\",\"total_amount\":\"0.01\",\"trade_no\":\"2021112622001452000000000000\"},\"alipay_cert_sn\":\"78aa9258336cb49f0000000000000000\",\"sign\":\"gr0izd7wohfjqi7qhiifgdqeqxvfmun7hze64/kh5yhlc/kwov7zy42nfjf0+1je0cuu9pwp1pry5vlhii04qronuo77j9i1/4aailfan9gxyckgy8j8x4bd9iweqoohruf7up/mobvowpcgullt3sudciapyaxcdj7chbnwwd7dmbivq2t57rvvers62p+cvwdrccoepge5q/ljdbx8e12h5uouijmo8r8ch5k49flxwax/rf3hjy507b4ojuxozcuij+ay2qd2+4ng8iua0hm91iv8dfeyett8/xpfnw2dnpcwivfaqypnxrd63m0gg+u8rtqzq+s77ntgxqwtsq==\"}",
    "params": {
        "biz_content": "{\"query_options\":[\"gmt_refund_pay\",\"refund_detail_item_list\"],\"trade_no\":\"2021112622001452000000000000\",\"out_request_no\":\"FNR219200810000000000\"}"
    },
    "sendBackFee": "0.01",
    "refundDetailItemList": [
        {
            "amount": "0.01",
            "fundChannel": "ALIPAYACCOUNT"
        }
    ],
    "totalAmount": "0.01",
    "outTradeNo": "P219200688300000000",
    "refundAmount": "0.01"
}