支付寶對(duì)接學(xué)習(xí)筆記:
功能介紹:
- 支付寶對(duì)接
- 支付寶回調(diào)
- 查詢支付狀態(tài)(略過(guò)不講)
要求:
- 熟悉支付寶對(duì)接核心文檔井厌,調(diào)通支付寶官方Demo
- 解析支付寶SDK對(duì)接源碼
- RSA1和RSA2驗(yàn)證簽名及加解密
- 避免支付寶的重復(fù)通知而加數(shù)據(jù)校驗(yàn)(略)
技巧:
- ngrok 外網(wǎng)穿透
- 生成二維碼并持久化到圖片服務(wù)器
調(diào)試完demo后,集合到開(kāi)發(fā)項(xiàng)目。
把支付寶依賴的jar寶按照提供版本要求導(dǎo)入寻馏,sdk則放在web下lib文件夾下。然后在module的依賴中導(dǎo)入lib下的本地jar包(坑V壮啊滑燃!不然會(huì)報(bào)紅)
那么為什么不統(tǒng)一使用pom導(dǎo)入呢往果?原因就是阿里沒(méi)有提供該jar包的線上導(dǎo)入疆液,只能本地導(dǎo)入。為了統(tǒng)一jar地址陕贮,所以必須先配置sdk的jar包的位置堕油。(在這之前還要配置一個(gè)maven插件以加載本地jar包).
接下來(lái)簡(jiǎn)單梳理一遍流程:
一、登錄進(jìn)入螞蟻金服
本次使用沙箱環(huán)境下進(jìn)行整合肮之,沙箱環(huán)境開(kāi)發(fā)上線流程差別不大馍迄,和正式幾乎是一致的,只是切換不同的APPID和支付寶網(wǎng)關(guān)局骤。
二、下載官方的demo
這里選中java版的demo
選中idea導(dǎo)入暴凑。先在本地調(diào)通再集成到系統(tǒng)中去峦甩。
右鍵運(yùn)行主函數(shù)會(huì)發(fā)現(xiàn)運(yùn)行不了,那是因?yàn)槲覀冞€沒(méi)有修改配置文件中設(shè)置现喳。
對(duì)應(yīng)配置如下凯傲。
那么問(wèn)題來(lái)了怎么生成這些公鑰私鑰呢?前往這里根據(jù)系統(tǒng)下載對(duì)應(yīng)的工具嗦篱。
接著:
配置好配置文件后冰单,運(yùn)行一下:
運(yùn)行沒(méi)有問(wèn)題,證明已經(jīng)調(diào)通灸促。下載沙箱版的支付寶诫欠,登錄沙箱提供的買(mǎi)家賬戶,復(fù)制當(dāng)面付二維碼找一個(gè)二維碼生成工具掃描支付看能不能成功浴栽。
掃描支付后:
到此為止本地支付寶已經(jīng)調(diào)通荒叼,這個(gè)還是相對(duì)來(lái)說(shuō)比較簡(jiǎn)單的。從demo的項(xiàng)目結(jié)構(gòu)來(lái)看典鸡,這是一個(gè)web項(xiàng)目被廓,可以自行配置運(yùn)行環(huán)境再運(yùn)行,測(cè)試會(huì)更加方便一點(diǎn)萝玷,如果沒(méi)有出錯(cuò)的話就會(huì)出現(xiàn)下圖:
三嫁乘、系統(tǒng)對(duì)接支付寶支付接口
雖然官網(wǎng)已經(jīng)寫(xiě)得很清楚了,但是第一次對(duì)接還是很吃力球碉,這里寫(xiě)一下思路:
1蜓斧、先把demo中的aplipay那個(gè)包及配置文件復(fù)制放到需要集成項(xiàng)目的類路徑下:
2、把支付寶依賴的jar寶按照提供版本要求導(dǎo)入汁尺,sdk則放在web下lib文件夾下法精。然后在module的依賴中導(dǎo)入lib下的本地jar包(坑!!不然會(huì)報(bào)紅)
那么為什么不統(tǒng)一使用pom導(dǎo)入呢搂蜓?原因就是阿里沒(méi)有提供該jar包的線上導(dǎo)入狼荞,只能本地導(dǎo)入。為了統(tǒng)一jar地址帮碰,所以必須先配置sdk的jar包的位置相味。
還要配置一個(gè)maven插件以加載本地jar包.
3、運(yùn)行下主函數(shù)沒(méi)有報(bào)錯(cuò)就是初步導(dǎo)入成功殉挽。
四丰涉、對(duì)接支付寶支付接口
1、這里是整個(gè)過(guò)程中最難的部分斯碌。
從下訂單到支付到支付完成一死,省去下訂單的接口,支付過(guò)程需要用到兩個(gè)接口傻唾,一個(gè)是支付接口投慈,一個(gè)是給支付寶授權(quán)回調(diào)接口。訂單這里采用模擬數(shù)據(jù)冠骄。
2伪煤、首先支付接口
掃碼支付調(diào)用流程:
官方文檔參數(shù)描述:
因此先查詢數(shù)據(jù)組裝支付寶要求的參數(shù)值:
Map<String, String> resultMap = Maps.newHashMap();
Order order = orderMapper.selectByUserAndOrderNo(userId, orderNo);
if (order == null) {
return ServerRespond.createByErrorMessage("用戶沒(méi)有該訂單");
}
resultMap.put("orderNo", String.valueOf(order.getOrderNo()));
// (必填) 商戶網(wǎng)站訂單系統(tǒng)中唯一訂單號(hào),64個(gè)字符以內(nèi)凛辣,只能包含字母抱既、數(shù)字、下劃線扁誓,
// 需保證商戶系統(tǒng)端不能重復(fù)防泵,建議通過(guò)數(shù)據(jù)庫(kù)sequence生成,
String outTradeNo = order.getOrderNo().toString();
// (必填) 訂單標(biāo)題跋理,粗略描述用戶的支付目的择克。如“xxx品牌xxx門(mén)店當(dāng)面付掃碼消費(fèi)”
String subject = new StringBuilder().append("寸金在線商城,訂單號(hào):").append(outTradeNo).toString();
// (必填) 訂單總金額前普,單位為元肚邢,不能超過(guò)1億元
// 如果同時(shí)傳入了【打折金額】,【不可打折金額】,【訂單總金額】三者,則必須滿足如下條件:【訂單總金額】=【打折金額】+【不可打折金額】
String totalAmount = order.getPayment().toString();
// (可選) 訂單不可打折金額,可以配合商家平臺(tái)配置折扣活動(dòng)拭卿,如果酒水不參與打折骡湖,則將對(duì)應(yīng)金額填寫(xiě)至此字段
// 如果該值未傳入,但傳入了【訂單總金額】,【打折金額】,則該值默認(rèn)為【訂單總金額】-【打折金額】
String undiscountableAmount = "0";
// 賣(mài)家支付寶賬號(hào)ID,用于支持一個(gè)簽約賬號(hào)下支持打款到不同的收款賬號(hào)峻厚,(打款到sellerId對(duì)應(yīng)的支付寶賬號(hào))
// 如果該字段為空响蕴,則默認(rèn)為與支付寶簽約的商戶的PID,也就是appid對(duì)應(yīng)的PID
String sellerId = "";
// 訂單描述惠桃,可以對(duì)交易或商品進(jìn)行一個(gè)詳細(xì)地描述浦夷,比如填寫(xiě)"購(gòu)買(mǎi)商品2件共15.00元"
String body = new StringBuilder().append("訂單").append(outTradeNo).append("購(gòu)買(mǎi)商品共").append(totalAmount).append("元").toString();
// 商戶操作員編號(hào)辖试,添加此參數(shù)可以為商戶操作員做銷售統(tǒng)計(jì)
String operatorId = "test_operator_id";
// (必填) 商戶門(mén)店編號(hào),通過(guò)門(mén)店號(hào)和商家后臺(tái)可以配置精準(zhǔn)到門(mén)店的折扣信息劈狐,詳詢支付寶技術(shù)支持
String storeId = "test_store_id";
// 業(yè)務(wù)擴(kuò)展參數(shù)罐孝,目前可添加由支付寶分配的系統(tǒng)商編號(hào)(通過(guò)setSysServiceProviderId方法),詳情請(qǐng)咨詢支付寶技術(shù)支持
ExtendParams extendParams = new ExtendParams();
extendParams.setSysServiceProviderId("2088100200300400500");
// 支付超時(shí)肥缔,定義為120分鐘
String timeoutExpress = "120m";
// 商品明細(xì)列表莲兢,需填寫(xiě)購(gòu)買(mǎi)商品詳細(xì)信息,
List<GoodsDetail> goodsDetailList = new ArrayList<GoodsDetail>();
List<OrderItem> orderItemList = orderItemMapper.getByOrderNoUserId(orderNo, userId);
System.out.println(orderItemList.get(0));
for (OrderItem orderItem : orderItemList) {
GoodsDetail goods = GoodsDetail.newInstance(orderItem.getProductId().toString(), orderItem.getProductName().toString(),
BigDecimalUtil.mul(orderItem.getCurrentUnitPrice().doubleValue(), new Double(100).doubleValue()).longValue(), orderItem.getQuantity());
goodsDetailList.add(goods);
}
// // 創(chuàng)建一個(gè)商品信息续膳,參數(shù)含義分別為商品id(使用國(guó)標(biāo))改艇、名稱、單價(jià)(單位為分)坟岔、數(shù)量谒兄,如果需要添加商品類別,詳見(jiàn)GoodsDetail
// GoodsDetail goods1 = GoodsDetail.newInstance("goods_id001", "xxx小面包", 1000, 1);
// // 創(chuàng)建好一個(gè)商品后添加至商品明細(xì)列表
// goodsDetailList.add(goods1);
//
// // 繼續(xù)創(chuàng)建并添加第一條商品信息社付,用戶購(gòu)買(mǎi)的產(chǎn)品為“黑人牙刷”舵变,單價(jià)為5.00元,購(gòu)買(mǎi)了兩件
// GoodsDetail goods2 = GoodsDetail.newInstance("goods_id002", "xxx牙刷", 500, 2);
// goodsDetailList.add(goods2);
// 創(chuàng)建掃碼支付請(qǐng)求builder瘦穆,設(shè)置請(qǐng)求參數(shù)
AlipayTradePrecreateRequestBuilder builder = new AlipayTradePrecreateRequestBuilder()
.setSubject(subject).setTotalAmount(totalAmount).setOutTradeNo(outTradeNo)
.setUndiscountableAmount(undiscountableAmount).setSellerId(sellerId).setBody(body)
.setOperatorId(operatorId).setStoreId(storeId).setExtendParams(extendParams)
.setTimeoutExpress(timeoutExpress)
.setNotifyUrl(PropertiesUtil.getProperty("alipay.callback.url"))//支付寶服務(wù)器主動(dòng)通知商戶服務(wù)器里指定的頁(yè)面http路徑,根據(jù)需要設(shè)置
.setGoodsDetailList(goodsDetailList);
/** 一定要在創(chuàng)建AlipayTradeService之前調(diào)用Configs.init()設(shè)置默認(rèn)參數(shù)
* Configs會(huì)讀取classpath下的zfbinfo.properties文件配置信息,如果找不到該文件則確認(rèn)該文件是否在classpath目錄
*/
Configs.init("zfbinfo.properties");
/** 使用Configs提供的默認(rèn)參數(shù)
* AlipayTradeService可以使用單例或者為靜態(tài)成員對(duì)象赊豌,不需要反復(fù)new
*/
AlipayTradeService tradeService = new AlipayTradeServiceImpl.ClientBuilder().build();
AlipayF2FPrecreateResult result = tradeService.tradePrecreate(builder);
接著就是出參扛或,二維碼的生成,并展示給用戶支付碘饼。圖片展示通過(guò)上傳到圖片服務(wù)器的方式熙兔。所以前提得已經(jīng)有一個(gè)ftp服務(wù)器和連接服務(wù)器的ftp工具類。
switch (result.getTradeStatus()) {
case SUCCESS:
log.info("支付寶預(yù)下單成功: )");
AlipayTradePrecreateResponse response = result.getResponse();
dumpResponse(response);
// 關(guān)鍵部分艾恼,把生成二維碼上傳到圖片服務(wù)器
File folder = new File(path);
if (!folder.exists()) {
folder.setWritable(true);
folder.mkdirs();
}
// 需要修改為運(yùn)行機(jī)器上的路徑
//替換s占位符
String QRPath = String.format(path + "/qr-%s.png",
response.getOutTradeNo());
String qrFileName = String.format("qr-%s.png", response.getOutTradeNo());
//支付寶調(diào)用guava生成二維碼
ZxingUtils.getQRCodeImge(response.getQrCode(), 256, QRPath);
File targetFile = new File(path, qrFileName);
try {
FTPUtil.uploadFile(Lists.newArrayList(targetFile));
} catch (IOException e) {
log.error("上傳二維碼異常", e);
}
log.info("QRPath:" + QRPath);
String qrUrl = PropertiesUtil.getProperty("ftp.server.http.prefix") + targetFile.getName();
resultMap.put("qrUrl", qrUrl);
return ServerRespond.createBySuccess(resultMap);
case FAILED:
log.error("支付寶預(yù)下單失敗!!!");
return ServerRespond.createByErrorMessage("支付寶預(yù)下單失敗");
case UNKNOWN:
log.error("系統(tǒng)異常住涉,預(yù)下單狀態(tài)未知!!!");
return ServerRespond.createByErrorMessage("系統(tǒng)異常,預(yù)下單狀態(tài)未知!!!");
default:
log.error("不支持的交易狀態(tài)钠绍,交易返回異常!!!");
return ServerRespond.createByErrorMessage("不支持的交易狀態(tài)舆声,交易返回異常!!!");
}
3、支付寶回調(diào)接口
這個(gè)授權(quán)支付寶調(diào)用的接口柳爽,所以不能是本地ip媳握,必須得有一個(gè)外網(wǎng)ip,最直接的方式是服務(wù)器上操作磷脯,但顯然現(xiàn)在是沒(méi)辦法這樣做的蛾找,于是采用了內(nèi)網(wǎng)穿透的辦法,內(nèi)網(wǎng)穿透工具我采用ngrok赵誓,缺點(diǎn)是不能綁定固定域名打毛。
授權(quán)回調(diào)接口:
public ServerRespond aliCallback(Map<String, String> params) {
//處理回調(diào)數(shù)據(jù)
Long orderNo = Long.parseLong(params.get("out_trade_no"));
String tradeNo = params.get("trade_no");
String tradeStatus = params.get("trade_status");
Order order = orderMapper.selectByOrderNo(orderNo);
if (order == null) {
return ServerRespond.createByErrorMessage("寸金商場(chǎng)訂單柿赊,回調(diào)忽略");
}
if (order.getStatus() >= Const.OrderStatus.PAID.getCode()) {
return ServerRespond.createBySuccess("支付寶重復(fù)調(diào)用");
}
if (Const.alipayCallback.TRADE_STATUS_TRADE_SUCCESS.equals(tradeStatus)) {
order.setPaymentTime(DateTimeUtil.strToDate(params.get("gmt_payment")));
order.setStatus(Const.OrderStatus.PAID.getCode());
orderMapper.updateByPrimaryKeySelective(order);
}
PayInfo payInfo = new PayInfo();
payInfo.setUserId(order.getUserId());
payInfo.setOrderNo(order.getOrderNo());
payInfo.setPayPlatform(Const.PayPlatFormEnum.ALIPAY.getCode());
payInfo.setPlatformNumber(tradeNo);
payInfo.setPlatformStatus(tradeStatus);
payInfoMapper.insert(payInfo);
return ServerRespond.createBySuccess();
}
關(guān)于回調(diào)接口可以看看文檔。
第一步: 在通知返回參數(shù)列表中幻枉,除去sign碰声、sign_type兩個(gè)參數(shù)外,凡是通知返回回來(lái)的參數(shù)皆是待驗(yàn)簽的參數(shù)展辞。
這一步很重要奥邮,不然沒(méi)辦法驗(yàn)簽,看源碼,便可知sdk已經(jīng)做了罗珍,接著組裝StringBuffer洽腺,因?yàn)镾tringBuffer是線程安全的,可以以應(yīng)付高并發(fā)操作覆旱。
第三步: 將簽名參數(shù)(sign)使用base64解碼為字節(jié)碼串蘸朋。
這一步sdk也做了。
第四步: 使用RSA的驗(yàn)簽方法扣唱,通過(guò)簽名字符串藕坯、簽名參數(shù)(經(jīng)過(guò)base64解碼)及支付寶公鑰驗(yàn)證簽名。
然而需要注意的是上面這個(gè)方法實(shí)際上是不ok的噪沙,因?yàn)樗乃惴ㄕ?qǐng)求類型跟配置中的不一致炼彪。我們的請(qǐng)求算法類型是RSA2不是SHA1WithRSA。
然而還有一個(gè)函數(shù)重載允許多了一個(gè)可以選擇加密類型的參數(shù)正歼。
點(diǎn)擊rsaCheck辐马,當(dāng)signType的值equal不同的值調(diào)用不同的方法,很明顯第二個(gè)就是我們要的局义。
于是在控制器中就得這樣寫(xiě):
五喜爷、對(duì)接測(cè)試
完成接口編寫(xiě)后就是接口測(cè)試
從數(shù)據(jù)庫(kù)中提一個(gè)未付款的訂單號(hào)做測(cè)試
成功的話會(huì)返回一個(gè)付款二維碼
打開(kāi)該二維碼:
如圖則已經(jīng)對(duì)接成功:
以上就是支付寶集成的主要過(guò)程,代碼只是貼了一部分具體可以看這里萄唇。
六檩帐、總結(jié)
雖然官方的說(shuō)明已經(jīng)夠詳細(xì)了,但是真正入手去做還是有很多坑另萤,此次對(duì)接過(guò)程中學(xué)習(xí)很多湃密,其中尤其要注意的是因?yàn)楣俜降年P(guān)系,sdk必須放在lib下四敞,為了打包時(shí)能夠同其他依賴包一起打包勾缭,還需配置好對(duì)sdk的打包插件,其他支付寶需要的依賴包如果用maven引入的話盡量保持版本一致或者一起跟支付寶sdk一同從Demo中復(fù)制過(guò)來(lái)放在lib包下目养。