需求:系統(tǒng)A對(duì)接支付寶,實(shí)現(xiàn)支持用戶掃碼支付
1、支付方式選擇
對(duì)接的API文檔:https://open.alipay.com/api
可選的支付方式有:
- 掃碼付:出示付款碼或者用戶掃碼付款
- APP支付:在APP中喚起支付寶
- 手機(jī)網(wǎng)站支付:在移動(dòng)端網(wǎng)頁(yè)中喚起支付寶 App 或支付寶網(wǎng)頁(yè)
- 電腦網(wǎng)站支付:在PC端喚起支付寶App或者網(wǎng)頁(yè)登錄支付寶賬戶
- 刷臉付:需硬件支持
- 商家扣款:類似每月會(huì)員扣款
- 預(yù)授權(quán)支付:凍結(jié)對(duì)應(yīng)額度,交易完成后給商家
- JSAPI支付:小程序
這里選擇掃碼付的方式,點(diǎn)擊下單后,返回支付二維碼,用戶掃碼支付。
2、交互流程
畫(huà)個(gè)下單流程的時(shí)序圖:
大致流程:
- 用戶下單,系統(tǒng)A組裝信息后(訂單信息、回調(diào)地址、簽名),調(diào)用支付寶預(yù)下單接口,返回二維碼鏈接
- 系統(tǒng)A將二維碼鏈接轉(zhuǎn)二維碼圖片
- 用戶掃碼,喚醒本地支付寶,完成支付
- 支付寶返回支付成功信息給用戶
- 支付寶異步通知系統(tǒng)A支付成功的消息(回調(diào)地址),如果用戶支付成功,支付寶就調(diào)用回調(diào)地址的API,回調(diào)接口中自然是系統(tǒng)A收到用戶支付成功消息后的動(dòng)作
- 上一步如果通知失敗,比如網(wǎng)絡(luò)異?;蛑Ц秾氄{(diào)用異步通知接口時(shí)系統(tǒng)A正好掛了 ? 可主動(dòng)調(diào)支付寶提供的查詢支付結(jié)果接口,或者加定時(shí)任務(wù)輪詢來(lái)查詢交易狀態(tài),如3s-5s
- 還可以考慮在第一步請(qǐng)求支付寶接口時(shí)加上二維碼的有效時(shí)間,過(guò)期就重新發(fā)起
查詢支付結(jié)果流程:
退款流程同上查詢支付結(jié)果。PS:注意下單、退款過(guò)程中,相關(guān)訂單的業(yè)務(wù)數(shù)據(jù)落庫(kù)到系統(tǒng)A。
3、對(duì)接準(zhǔn)備
1)加密解密 + 簽名驗(yàn)簽
支付信息不能在網(wǎng)絡(luò)上明文傳輸,以防被篡改。系統(tǒng)A到支付寶的方向,采用:
- 支付寶公鑰加密 + 系統(tǒng)A的私鑰簽名(系統(tǒng)A做的事)
- 支付寶私鑰解密 + 系統(tǒng)A的公鑰驗(yàn)簽(收到信息后,支付寶做的事)
同理,支付寶返回支付結(jié)果時(shí),就是在支付寶中用系統(tǒng)A的公鑰加密+支付寶的私鑰簽名,傳輸?shù)较到y(tǒng)A后,則是先用支付寶的公鑰驗(yàn)簽,再用系統(tǒng)A的私鑰解密支付結(jié)果
2)沙箱環(huán)境
調(diào)試過(guò)程中,可采用支付寶提供的沙箱環(huán)境,點(diǎn)擊右上角控制臺(tái),登錄后選擇沙箱:
這里有一套可調(diào)試的APPID、系統(tǒng)A的公鑰、密鑰、支付寶的公鑰、支付寶的網(wǎng)關(guān)地址,以及商家賬戶和用戶賬戶(用于后續(xù)登錄沙箱版本支付寶APP完成支付)
點(diǎn)擊【沙箱工具】側(cè)邊欄,下載沙箱版支付寶APP,等于上面的買家賬戶。
3)內(nèi)網(wǎng)穿透
前面提到,用戶支付成功后,支付寶需要回調(diào)系統(tǒng)A接口來(lái)通知系統(tǒng)A,但我的開(kāi)發(fā)環(huán)境在內(nèi)網(wǎng),支付寶訪問(wèn)不到,考慮做內(nèi)網(wǎng)穿透,讓支付寶通知到一個(gè)中轉(zhuǎn)地址,再由中轉(zhuǎn)地址到我的內(nèi)網(wǎng)。穿透工具選擇cpolar,下載地址 https://dashboard.cpolar.com/get-started,下載后,解壓并安裝msi包
雙擊exe文件,執(zhí)行認(rèn)證:
cpolar authtoken xxxx
創(chuàng)建隧道,建立鏈接:
cpolar http 9527
//返回結(jié)果
Forwarding http://maggie.cpolar.io -> localhost:9527
Forwarding https://maggie.cpolar.io -> localhost:9527
轉(zhuǎn)發(fā)成功。此時(shí),給支付寶訪問(wèn)forward的地址即可,比如系統(tǒng)A的異步通知接口:
localhost:9527/notify
那就是:
http://maggie.cpolar.io/notify
4、二維碼
二維碼是消息的載體。平時(shí)玩可直接在草料二維碼UI頁(yè)面,這里需要給系統(tǒng)A的訂單服務(wù)用代碼生成二維碼。二維碼中的信息自然是支付寶預(yù)下單返回的url。Java生成二維碼可集成zxing庫(kù),但這樣得自己兩層for填充方格子:【SpringBoot整合ZXing創(chuàng)建二維碼和條形碼】 ,這里選擇hutool工具類庫(kù)(對(duì)zxing的二次封裝),引入依賴:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.22</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.1</version>
</dependency>
調(diào)用方式:
//生成直到url對(duì)應(yīng)的二維碼,寬高均300像素,可到路徑,也可到Http響應(yīng)
QrCodeUtil.generate("https://url/path", 300, 300, "png", httpServletResponse.getOutPutStream());
也可引入QrConfig對(duì)象,設(shè)置其他屬性:
QrConfig config = new QrConfig(300, 300);
//糾錯(cuò)級(jí)別
config.setErrorCorrection(ErrorCorrectionLevel.H);
//二維碼顏色
config.setBackColor(Color.BLUE);
QrCodeUtil.generate("https://www.baidu.com", config, new File("D:\\code.png"));
5、下單
支付寶提供的SDK 中已經(jīng)對(duì)加簽驗(yàn)簽邏輯做了封裝,使用 SDK 時(shí)傳入支付寶公鑰等內(nèi)容可直接通過(guò) SDK 自動(dòng)進(jìn)行加驗(yàn)簽。 SDK文檔地址:https://opendocs.alipay.com/open/54/103419?pathHash=d6bc7c2b 。支付寶提供了兩種SDK:
- 通用版SDK
- 簡(jiǎn)易版SDK
官網(wǎng)有通用版的API代碼示例,這里走簡(jiǎn)易版的。引入簡(jiǎn)易版SDK的依賴:
<!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-easysdk -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-easysdk</artifactId>
<version>2.2.0</version>
</dependency>
在application.yml配置文件中統(tǒng)一寫(xiě)密鑰、通知地址等(生產(chǎn)環(huán)境不要將私鑰信息配置在源碼中,例如配置為常量或儲(chǔ)存在配置文件中,這樣源碼一丟,這些保密信息都泄漏了,放安全區(qū)域或服務(wù)器,運(yùn)行時(shí)讀取即可)
alipay:
easy:
protocol: https
gatewayHost: openapi-sandbox.dl.alipaydev.com
signType: RSA2
appId: 9021000133624745
merchantPrivateKey: MIIEvQIBADANBgkqhkiG9w0B
alipayPublicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOC
notifyUrl: http://maggie.cpolar.io/notify
server:
port: 9527
@ConfigurationProperties注解統(tǒng)一讀到:
@Configuration
@Data
@ConfigurationProperties(prefix = "alipay.easy")
public class AliPayConfigInfo {
/**
* 請(qǐng)求協(xié)議
*/
private String protocol;
/**
* 請(qǐng)求網(wǎng)關(guān)
*/
private String gatewayHost;
/**
* 簽名類型
*/
private String signType;
/**
* 應(yīng)用ID(來(lái)自支付寶申請(qǐng))
*/
private String appId;
/**
* 應(yīng)用秘鑰
*/
private String merchantPrivateKey;
/**
* 支付寶公鑰
*/
private String alipayPublicKey;
/**
* 支付結(jié)果異步通知的地址
*/
private String notifyUrl;
/**
* 設(shè)施AES秘鑰
*/
private String encryptKey;
}
將配置處理成Config類型的Bean,方便后面?zhèn)魅隒onfig對(duì)象:
@Configuration
public class AliPayConfig {
@Bean
public Config config(AliPayConfigInfo configInfo){
Config config = new Config();
config.protocol = configInfo.getProtocol();
config.gatewayHost = configInfo.getGatewayHost();
config.signType = configInfo.getSignType();
config.appId = configInfo.getAppId();
config.merchantPrivateKey = configInfo.getMerchantPrivateKey();
config.alipayPublicKey = configInfo.getAlipayPublicKey();
config.notifyUrl = configInfo.getNotifyUrl();
config.encryptKey = "";
return config;
}
}
寫(xiě)下單接口,響應(yīng)一個(gè)二維碼給前端,這里業(yè)務(wù)數(shù)據(jù)、訂單編號(hào)直接寫(xiě)死,只做示意:
@RestController
@Slf4j
public class PayController {
@Resource
private Config config;
/**
* 收銀臺(tái)點(diǎn)擊結(jié)賬
* 發(fā)起下單請(qǐng)求
*/
@GetMapping("/pay")
public void pay(HttpServletRequest request, HttpServletResponse response) throws Exception {
Factory.setOptions(config);
//調(diào)用支付寶的接口
AlipayTradePrecreateResponse payResponse = Factory.Payment.FaceToFace().preCreate("訂單主題:Mac筆記本", "LS123qwe123", "19999");
//參照官方文檔響應(yīng)示例,解析返回結(jié)果
String httpBodyStr = payResponse.getHttpBody();
JSONObject jsonObject = JSONObject.parseObject(httpBodyStr);
String qrUrl = jsonObject.getJSONObject("alipay_trade_precreate_response").get("qr_code").toString();
QrCodeUtil.generate(qrUrl, 300, 300, "png", response.getOutputStream());
}
}
6、異步通知回調(diào)
異步回調(diào)參考文檔:https://opendocs.alipay.com/open/194/103296?pathHash=e43f422e&ref=api,實(shí)現(xiàn)先全放Controller層了:
@RestController
@Slf4j
public class PayController {
@Resource
private Config config;
/**
* 給支付寶的回調(diào)接口
*/
@PostMapping("/notify")
public void notify(HttpServletRequest request, HttpServletResponse response) throws Exception {
Map<String, String> params = new HashMap<>();
//獲取支付寶POST過(guò)來(lái)反饋信息,將異步通知中收到的待驗(yàn)證所有參數(shù)都存放到map中
Map<String, String[]> parameterMap = request.getParameterMap();
for (String name : parameterMap.keySet()) {
String[] values = parameterMap.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);
}
//驗(yàn)簽
Boolean signResult = Factory.Payment.Common().verifyNotify(params);
if (signResult) {
log.info("收到支付寶發(fā)送的支付結(jié)果通知");
String out_trade_no = request.getParameter("out_trade_no");
log.info("交易流水號(hào):{}", out_trade_no);
//交易狀態(tài)
String trade_status = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"), "UTF-8");
//交易成功
switch (trade_status) {
case "TRADE_SUCCESS":
//支付成功的業(yè)務(wù)邏輯,比如落庫(kù),開(kāi)vip權(quán)限等
log.info("訂單:{} 交易成功", out_trade_no);
break;
case "TRADE_FINISHED":
log.info("交易結(jié)束,不可退款");
//其余業(yè)務(wù)邏輯
break;
case "TRADE_CLOSED":
log.info("超時(shí)未支付,交易已關(guān)閉,或支付完成后全額退款");
//其余業(yè)務(wù)邏輯
break;
case "WAIT_BUYER_PAY":
log.info("交易創(chuàng)建,等待買家付款");
//其余業(yè)務(wù)邏輯
break;
}
response.getWriter().write("success"); //返回success給支付寶,表示消息我已收到,不用重調(diào)
} else {
response.getWriter().write("fail"); ///返回fail給支付寶,表示消息我沒(méi)收到,請(qǐng)重試
}
}
}
到此,看下效果,請(qǐng)求下單接口:
用沙箱版app掃碼:
支付,查看余額:
7、查詢支付結(jié)果
主動(dòng)查詢用戶的支付結(jié)果,訂單編號(hào)依然寫(xiě)死:
@RestController
@Slf4j
public class PayController {
@Resource
private Config config;
@GetMapping("/query")
public String query() throws Exception {
Factory.setOptions(config);
AlipayTradeQueryResponse result = Factory.Payment.Common().query("LS123qwe123");
return result.getHttpBody();
}
}
8、退款
退款操作:
@RestController
@Slf4j
public class PayController {
@Resource
private Config config;
@GetMapping("/refund")
public String refund() throws Exception {
Factory.setOptions(config);
AlipayTradeRefundResponse refundResponse = Factory.Payment.Common().refund("LS123qwe123", "19999");
return refundResponse.getHttpBody();
}
}
9、通用版SDK
官方文檔就是以這個(gè)SDK為例的,貼個(gè)代碼示例:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-769178.html
private static final String GATEWAY_URL = "https://openapi.alipaydev.com/gateway.do";
private static final String FORMAT = "JSON";
private static final String CHARSET = "UTF-8";
//簽名方式
private static final String SIGN_TYPE = "RSA2";
@Resource
private AliPayConfig aliPayConfig;
@Resource
private OrdersMapper ordersMapper;
@GetMapping("/pay") // &subject=xxx&traceNo=xxx&totalAmount=xxx
public void pay(AliPay aliPay, HttpServletResponse httpResponse) throws Exception {
// 1. 創(chuàng)建Client,通用SDK提供的Client,負(fù)責(zé)調(diào)用支付寶的API
AlipayClient alipayClient = new DefaultAlipayClient(GATEWAY_URL, aliPayConfig.getAppId(),
aliPayConfig.getAppPrivateKey(), FORMAT, CHARSET, aliPayConfig.getAlipayPublicKey(), SIGN_TYPE);
// 2. 創(chuàng)建 Request并設(shè)置Request參數(shù)
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); // 發(fā)送請(qǐng)求的 Request類
request.setNotifyUrl(aliPayConfig.getNotifyUrl());
JSONObject bizContent = new JSONObject();
bizContent.set("out_trade_no", aliPay.getTraceNo()); // 我們自己生成的訂單編號(hào)
bizContent.set("total_amount", aliPay.getTotalAmount()); // 訂單的總金額
bizContent.set("subject", aliPay.getSubject()); // 支付的名稱
bizContent.set("product_code", "FAST_INSTANT_TRADE_PAY"); // 固定配置
request.setBizContent(bizContent.toString());
// 執(zhí)行請(qǐng)求,拿到響應(yīng)的結(jié)果,返回給瀏覽器
String form = "";
try {
form = alipayClient.pageExecute(request).getBody(); // 調(diào)用SDK生成表單
} catch (AlipayApiException e) {
e.printStackTrace();
}
httpResponse.setContentType("text/html;charset=" + CHARSET);
httpResponse.getWriter().write(form);// 直接將完整的表單html輸出到頁(yè)面
httpResponse.getWriter().flush();
httpResponse.getWriter().close();
}
具體有業(yè)務(wù)數(shù)據(jù)邏輯的對(duì)接支付寶接口,可跳轉(zhuǎn)【支付寶業(yè)務(wù)對(duì)接】文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-769178.html
到了這里,關(guān)于SpringBoot對(duì)接支付寶完成掃碼支付的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!