支付寶簡介文檔
(適用于ydm-java接口與后臺,如有誤入,但愿也能給您帶來幫助)
此文檔寫于2017年3月,只能說明此時該文檔適用。使用前請查看以下接口支付寶是否提供。
(鏈接如有發(fā)生變化,請在官方文檔中尋找此產(chǎn)品,一般情況下,產(chǎn)品名不會發(fā)生改變)
1. App支付產(chǎn)品
通俗上講就是在App中使用支付寶付款,流程上就是:App請求接口(服務端),哪一個用戶準備要買什么產(chǎn)品或者是要充多少錢,然后服務端拼接一些必要的參數(shù)返回給它,App端通過集成支付寶的SDK,根據(jù)接口返回的值去喚醒支付寶進行支付;與此同時,支付寶會異步通知服務端,哪一筆訂單支付成功,服務端對充值后的邏輯做判斷。(代碼目前的做法)
見圖
alipay
看完讀可知,13,14步驟之前有10,圖中的做法是在App端喚醒支付寶支付完成后的回調(diào)里請求接口,然后接口去請求支付寶,校驗這筆訂單完成與否,13,14的步驟為了完成服務端的邏輯。
支付寶sdk對商戶的請求支付數(shù)據(jù)處理完成后,會將結(jié)果同步反饋給商戶app端。
同步返回的數(shù)據(jù),商戶可以按照下文描述的方式在服務端驗證,驗證通過后,可以認為本次用戶付款成功。有些時候會出現(xiàn)商戶app在支付寶付款階段被關閉導致無法正確收到同步結(jié)果,此時支付結(jié)果可以完全依賴服務端的異步通知。
由于同步通知和異步通知都可以作為支付完成的憑證,且異步通知支付寶一定會確保發(fā)送給商戶服務端。為了簡化集成流程,商戶可以將同步結(jié)果僅僅作為一個支付結(jié)束的通知(忽略執(zhí)行校驗),實際支付是否成功,完全依賴服務端異步通知。
2. 批量付款到支付寶賬戶
即提現(xiàn);商戶(業(yè)主)用自己的支付寶轉(zhuǎn)給別人的支付寶賬戶。
很多應用中涉及到提現(xiàn),支付寶提現(xiàn)是一種普遍做法(知聊、美麗約等),但是該接口屬于支付寶的歷史接口,在很長的時間內(nèi)沒有更新,曾經(jīng)問過客服,在不確定的某天,會將其重寫。
采用接口與后臺相配合實現(xiàn)提現(xiàn)的功能,App端提交提現(xiàn)申請,后臺(服務端)處理數(shù)據(jù),跳往支付寶的邏輯,支付寶處理完畢后,同樣給后臺反饋,只會給成功與失敗的反饋,這個地方有很多坑,具體請參見下面的描述。
強烈建議你(您),從官方的Api里尋求了解,獲取幫助,再結(jié)合鄙人粗糙的代碼進行完善或使用
3. App支付詳解
1. 前期業(yè)主申請操作:官方說明
準入條件
申請前必須擁有經(jīng)過實名認證的支付寶賬戶;
企業(yè)或個體工商戶可申請;(只涉及過企業(yè)的)
需提供真實有效的營業(yè)執(zhí)照,且支付寶賬戶名稱需與營業(yè)執(zhí)照主體一致;
網(wǎng)站能正常訪問且頁面顯示完整,網(wǎng)站需要明確經(jīng)營內(nèi)容且有完整的商品信息;
網(wǎng)站必須通過ICP備案。如為個體工商戶,網(wǎng)站備案主體需要與支付寶賬戶主體名稱一致;
如為個體工商戶,則團購不開放,且古玩、珠寶等奢侈品、投資類行業(yè)無法申請本產(chǎn)品。
計費模式
費率按單筆計算;
一般行業(yè)費率:0.6%;特殊行業(yè)費率:1.2%,特殊行業(yè)范圍包括:手機、通訊設備銷售;家用電器;數(shù)碼產(chǎn)品及配件;休閑游戲;網(wǎng)絡游戲點卡、渠道代理;游戲系統(tǒng)商;網(wǎng)游周邊服務、交易平臺;網(wǎng)游運營商(含網(wǎng)頁游戲)。
創(chuàng)建應用
在開放平臺創(chuàng)建應用
配置應用
給應用添加App支付功能,這樣就可以在你的應用里使用App支付能力。
在使用這些能力的時候,需要在開放平臺里進行簽約,這時候約定的合同就生效了。也可以代替商戶簽約。
2. 程序員進行配置
上面提及的操作均應由業(yè)主申請完成,推薦他看文檔即可,在必要時加以幫助,但一定要讓業(yè)主自己操作(把前期的鍋先由業(yè)主背負,微信支付也是同樣)
App支付系統(tǒng)架構(gòu)
alipay流程
安全設計
采用HTTPS協(xié)議傳輸交易數(shù)據(jù),防止數(shù)據(jù)被截獲,解密;
采用RSA非對稱密鑰,明確交易雙方的身份,保證交易主體的正確性和唯一性。
言歸正傳
先登錄業(yè)主的支付寶開放平臺,點擊開發(fā)者中心-->左側(cè)菜單欄應用,如下圖:理想情況下是有一個默認的應用2.0(默認存在,不用管),以及業(yè)主創(chuàng)建好的所開發(fā)的應用。點進去查看。

支付寶開放平臺
配置RSA2密鑰或者RSA密鑰,2選1即可,17年支付寶新增的RSA2加密方式,但當時前端不能配合這種方式,所以你(您)看到的項目都是RSA方式進行的。公鑰放到支付寶平臺上,私鑰自己保存好,Java采用PCKS8編碼的私鑰,項目中是放在ALiPayConfig.java下的。

應用配置
接著我們看看AlipayConfig這個類:
前幾個獲取沒什么難度,商戶的私鑰這里對應上圖你的配置,你配置了RSA就用RSA的,配置了RSA2就用RSA2的,支付寶的公鑰也是要對應好,包括pay_sign_type這個值也要和你的選擇對應好,RSA就填RSA,RSA2就填RSA2;私鑰什么的要用pck格式的,不需要加空格,完整的一行復制下來;接著就剩下填寫回調(diào)地址了,在本地的情況下,你可以參考下方注釋提供的外網(wǎng)映射,大體意思就是配置一下,訪問一個虛擬的域名能指向自己的Controller,把這個提供給支付寶,就能在本地測試支付了。正式發(fā)布的時候,讀取域名即可。
理想情況下,只需要更改這里的配置,就可以完成支付的需求,當然如果后期表結(jié)構(gòu)有變化,小幅度改改也是可以用的。
publicclassAlipayConfig{// 合作身份者ID,以2088開頭由16位純數(shù)字組成的字符串publicstaticStringpartner ="";//appid? ? 在開放平臺的應用里可以得到? 一般是YYYYDDMM等publicstaticStringappid ="";//商戶支付寶賬號publicstaticStringseller_email="1234567@qq.com";//商戶真實姓名publicstaticStringaccount_name ="XXXX科技有限公司";// 商戶的私鑰//RSA2public static String private_key = "一長串放在這里";//RSApublicstaticStringprivate_key ="";//支付寶的公鑰? RSA2//public static String ali_public_key = "";//支付寶的公鑰? RSApublicstaticStringali_public_key ="";//接口名稱publicstaticStringmethod ="alipay.trade.app.pay";//調(diào)用的接口版本,固定為:1.0publicstaticStringversion ="1.0";/**
* 支付寶服務器主動通知商戶服務器里指定的頁面http/https路徑。建議商戶使用https
* 實際上https很貴。沒有用過https.
* 這里需要測試的話,需要用外網(wǎng)測試。https://www.ngrok.cc/? 這里有免費和付費的,實際上,免費用一下就可以了。
*/publicstaticStringnotify_url = BaseHandler.PATHS+"/LiaoBanApi/ALiPay/AfterPayNotify";//銷售產(chǎn)品碼,商家和支付寶簽約的產(chǎn)品碼,為固定值QUICK_MSECURITY_PAYpublicstaticStringproduct_code ="QUICK_MSECURITY_PAY";//商品的標題/交易標題/訂單標題/訂單關鍵字等。publicstaticStringsubject ="XXX-金幣充值";//↑↑↑↑↑↑↑↑↑↑請在這里配置您的基本信息↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑// 調(diào)試用,創(chuàng)建TXT日志文件夾路徑publicstaticStringlog_path ="D:\\";// 字符編碼格式 目前支持 gbk 或 utf-8publicstaticStringinput_charset ="utf-8";// 簽名方式 (支付回調(diào)簽名方式)publicstaticStringpay_sign_type ="RSA";// 簽名方式 不需修改(退款簽名方式)publicstaticStringsign_type ="MD5";? }
如果配置了以上的東西還不能用,那很好,證明你(您)可以重寫這一塊了233;不知大體有沒有說明白,支付寶支付就是APP走一下接口,告訴接口,誰要花多少錢,然后接口根據(jù)一堆配置信息(里面有賬戶的一些信息,私鑰),按照支付寶的規(guī)矩拼接參數(shù),加簽,然后扔給App端,App端拿著它就能調(diào)起支付寶客戶端了,支付成功后呢,支付寶會告訴咱們給它寫的回調(diào)地址,在回調(diào)的Controller里完成給用戶支付成功后的邏輯。在ALiPayService里大多完成業(yè)務所需的,然后按照支付寶規(guī)矩辦事,在業(yè)務那一塊你(您)看到的應該是對于金幣的一些處理,這個還需要根據(jù)業(yè)務進行部分的梳理。
ALiPayController、ALiPayService詳解:
在Controller對參數(shù)做了簡單的判斷,傳入的參數(shù)為用戶id,充值套餐id,用戶自定義充值的錢數(shù)(用戶選擇套餐就不傳自定義金額,傳自定義金額就不傳套餐id),這里還是根據(jù)自己業(yè)務所需進行設計吧,跳入Service層。
/**? * 返回App端調(diào)起支付寶的值? * 用戶選擇自定義充值 token_package_id? 不傳值? * 用戶選擇充值套餐? ? money 不傳值? *@parampersion_id? ? 充值用戶的id? *@paramtoken_package_id? ? ? ? 套餐id? *@parammoney? ? ? ? 用戶選擇自定義充值金額? *@return*/@ResponseBody@RequestMapping("getaliorder")publicObjectgetAliOrder(Integer persion_id,Integer token_package_id,Integer money){//驗證參數(shù)if(ObjectUtil.isPassInteger(persion_id)){if(ObjectUtil.isPassInteger(token_package_id) || ObjectUtil.isPassInteger(money)){returnaLiPayService.getAliOrder(persion_id, token_package_id, money);? ? ? ? ? }else{returnnewResultData(404,"客戶端傳參有誤。",null);? ? ? ? ? }? ? ? }else{returnnewResultData(404,"客戶端傳參有誤。",null);? ? ? }? }
Service層就顯得很冗余了,完成一些自己的業(yè)務邏輯,然后根據(jù)支付寶的要求拼接參數(shù);
首先在充值表里記錄了這次行為的訂單號(具有唯一性)以及充了多少錢或者選了哪個套餐,為后期回調(diào)的時候做準備;
然后拼接參數(shù),這些參數(shù)在官方文檔里有說明,把表格里要求必填的參數(shù)(在ALiPayConfig文件里)都給支付寶準備好了,閱讀文檔可知,還需要你按照請求示例來進行拼接,經(jīng)歷九九八十一難后最終扔給App端一個字符串......
請求參數(shù)組裝分下列3步,以最后第三步獲取到的請求為準 :
1 請求參數(shù)按照key=value&key=value方式拼接的未簽名原始字符串;(展開:肯定不想一個一個的拼,就按照map的方式進行處理;這里業(yè)務參數(shù) biz_content這里有個坑->所有的value就算是字符串也要加上雙引號,類似于這樣{"key":"vaule"},即使value是個字符串,拼好業(yè)務參數(shù)后,也放入map里)
2 再對原始字符串進行簽名,參考簽名規(guī)則;(展開:實際上支付寶提供了加簽的jar包,項目已經(jīng)集成好,主要是alipay-sdk-java20170209153223.jar這個以及它的源碼jar,它本身就需要你傳一個map,商戶自己的私鑰以及編碼格式;這里的jar包暫不清楚是否已經(jīng)支持了RSA2的方式,因為到離開時,只測試了RSA的方式)
3 最后對請求字符串的所有一級value(biz_content作為一個value)進行encode,編碼格式按請求串中的charset為準,沒傳charset按UTF-8處理,獲得最終的請求字符串。(展開:將得到的sign也放到map里,新創(chuàng)建一個新的map,用來存放進行encode之后的參數(shù);處理之后專門把sign參數(shù)拿出來的原因是因為支付寶給的示例里把sign參數(shù)在最后面,當初為了避嫌,就這么做了,現(xiàn)在看起來沒什么影響。調(diào)用了AliPayCore里的一個方法是為了把map轉(zhuǎn)成key=value&形式的字符串。實際上AliPayCore這個類是支付寶給的舊的示例,為了讓它有用,這里用了一下)
4 到這里把字符串扔給前端讓調(diào)支付寶就好了。(之前那個biz_content(寫成了{"key":value}因為那個時候value是String類型的,以為不用加雙引號)的坑時,給Android沒問題,給iOS就死活不行,而且還是10.0之后的支付寶版本,糾結(jié)了幾天,還驚動了支付寶那邊的技術)
publicObject getAliOrder(Integer persion_id,Integer token_package_id,Integer money){? ? ? Recharge recharge =newRecharge();//充值實體類//生成自己的訂單號recharge.setOrdersn(DateUtil.getDays() + UUIDHashCode.getOrderIdByUUId());//處理些自己的業(yè)務? ? ? --------? ? startrecharge.setRecharge_type(1);//充值類型為支付寶充值recharge.setPersion_id(persion_id);//誰充的錢recharge.setCreate_date(newDate());//用戶點擊支付的時間DecimalFormat? df =newDecimalFormat("#0.00");//用來格式化金額Stringtotal_fee ="";if(token_package_id ==null&& money !=null){//用戶自定義充值金額recharge.setToken_package(0);//用戶自定義金額,套餐id為0total_fee =String.valueOf(df.format(money));? ? ? }elseif(token_package_id !=null&& money ==null){//用戶選擇充值套餐//驗證套餐TokenPackage tokenPackage = tp_dao.selectByKey(token_package_id);if(tokenPackage !=null){if(tokenPackage.getMoney() >0){? ? ? ? ? ? ? ? ? total_fee =String.valueOf(df.format(tokenPackage.getMoney()));? ? ? ? ? ? ? ? ? recharge.setToken_package(token_package_id);? ? ? ? ? ? ? }? ? ? ? ? }else{returnnewResultData(404,"請重新選擇后重試。","充值套餐不存在。");? ? ? ? ? }? ? ? }else{returnnewResultData(404,"您操作有誤!","用戶充值時,兩者都選擇了,或者都沒有選擇。");? ? ? }? ? ? recharge.setAmount(Double.parseDouble(total_fee));//放入用戶充值的金額//申請支付recharge.setState(1);//支付狀態(tài)? 1為申請支付recharge.setType(1);//這個值默認為1,沒有意義,后期設計時可以去掉//保存一條充值記錄int count = recharge_dao.insert(recharge);//處理些自己的業(yè)務? ? ? --------? ? end/**
* 拼接公共參數(shù),一般情況下無需修改
*/if(count ==1){//生成待簽名字符串1HashMap waitSignStr =newHashMap();/**
* 公共參數(shù)
*/waitSignStr.put("app_id", ALiPayConfig.appid);//支付寶分配給開發(fā)者的應用IDwaitSignStr.put("method", ALiPayConfig.method);//接口名稱waitSignStr.put("format","JSON");//僅支持JSONwaitSignStr.put("charset", ALiPayConfig.input_charset);//商戶網(wǎng)站使用的編碼格式,固定為UTF-8waitSignStr.put("sign_type", ALiPayConfig.pay_sign_type);//簽名類型,目前僅支持RSA、RSA2。waitSignStr.put("version", ALiPayConfig.version);//調(diào)用的接口版本,固定為:1.0waitSignStr.put("timestamp",newSimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(newDate()));//發(fā)送請求的時間waitSignStr.put("notify_url", ALiPayConfig.notify_url);//支付寶服務器主動通知商戶網(wǎng)站里指定的頁面http路徑。/**
* 業(yè)務參數(shù)
*/Stringbiz_content ="{\"subject\":\""+ALiPayConfig.subject+"\",\"out_trade_no\":\""+recharge.getOrdersn()+"\",\"total_amount\":\""+total_fee+"\",\"product_code\":\"QUICK_MSECURITY_PAY\"}";? ? ? ? ? waitSignStr.put("biz_content", biz_content);//業(yè)務參數(shù)Stringsign ="";try{? ? ? ? ? ? ? sign = AlipaySignature.rsaSign(waitSignStr, ALiPayConfig.private_key, ALiPayConfig.input_charset);? ? ? ? ? }catch(Exception e) {? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? ? log.error("出錯了?。。。。。。。?!~~~~~~~~~~");? ? ? ? ? }? ? ? ? ? waitSignStr.put("sign", sign);? ? ? ? ? log.info(ALiPayCore.createLinkString(waitSignStr));//新建一個map用于存放將URL進行編碼。HashMapnewWaitSign=newHashMap();//循環(huán)mapSet sets = waitSignStr.keySet();for(Stringstring :sets) {try{newWaitSign.put(string,URLEncoder.encode(waitSignStr.get(string),"UTF-8"));? ? ? ? ? ? ? }catch(Exception e) {? ? ? ? ? ? ? ? ? log.error("*********************拼接支付寶參數(shù)進行url編碼出錯。**********************");? ? ? ? ? ? ? }? ? ? ? ? }StringurlSign =newWaitSign.get("sign");newWaitSign.remove("sign");StringnewSign= ALiPayCore.createLinkString(newWaitSign) +"&sign="+ urlSign;? ? ? ? ? log.info(newSign);? ? ? ? ? HashMap ordermap =newHashMap();? ? ? ? ? ordermap.put("orderString",newSign);returnnewResultData(1,"success", ordermap);? ? ? }else{returnnewResultData(0,"請重新進入充值界面后再試。","save_error");//保存訂單信息異常}? }
接下來就是回調(diào)了,回調(diào)地址取決于ALiPayConfig中notify_url,要測試時一定要配好外網(wǎng)映射或者在服務器上進行,測試金額靈活一些,0.01就可以。支付寶回調(diào)說明
值得關注的有兩個方面:
一,得到異步通知,處理完自己的邏輯后,必須給支付寶反饋success,不帶引號,否則支付寶會認為你沒有收到,會按照一定時間策略反復通知你;
二,驗簽(支付寶的jar提供方法)后,還需要判斷一些條件,判斷訂單號、金額、商戶id等是不是自己的,防止別人惡意攻擊。
在第二步以后,就可以給訂單進行業(yè)務的處理了,加金幣啊什么的,或者發(fā)貨什么的;記得給支付寶說success(out.print("success");)
/**
* 處理支付寶給的回調(diào)
* @param request
* @param out
*/@TransactionalpublicvoidAliPayAfter(HttpServletRequest request,PrintWriter out){//獲取支付寶POST過來反饋信息Map params = GetInfoFromALiPay(request);//1.驗證簽名booleanoneStep =false;try{? ? ? ? ? oneStep = AlipaySignature.rsaCheckV1(params, ALiPayConfig.ali_public_key,"UTF-8");? ? ? }catch(AlipayApiException e) {? ? ? ? ? e.printStackTrace();log.error("驗簽出錯了...........");? ? ? }//第一步if(oneStep){//驗證out_trade_no、total_amount、seller_id 是否是自己的if(ALiPayConfig.partner.equals(params.get("seller_id"))){? ? ? ? ? ? ? Recharge recharge = recharge_dao.selectByOrderSn(params.get("out_trade_no"));if(recharge !=null){intcount = recharge.getAmount().compareTo(Double.parseDouble(params.get("total_amount")));if(count ==0){if(ALiPayConfig.appid.equals(params.get("app_id"))){//支付成功的通知if("TRADE_FINISHED".equals(params.get("trade_status")) ||"TRADE_SUCCESS".equals(params.get("trade_status"))){//設置支付寶訂單號recharge.setAlipayordersn(params.get("trade_no"));? ? ? ? ? ? ? ? ? ? ? ? ? ? ? recharge.setState(2);//狀態(tài)為成功inta = recharge_dao.updateByAlipayordersn(recharge);if(a ==1){/**
* 支付成功,以下內(nèi)容為走自己的業(yè)務了
*///用戶的金幣增加Persion persion =newPersion();? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? persion.setId(recharge.getPersion_id());if(recharge.getToken_package() !=null&& recharge.getToken_package() ==0){//用戶自定義充值金額log.info("**************************用戶自定義充值金額**************************");? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? BigDecimal rmb =newBigDecimal(recharge.getAmount());//充值的RMB//金幣與RMB兌換比例Config config = configMapper.queryConfigById(1);//換算成金幣persion.setToken(newBigDecimal(config.getConfig_value()).multiply(rmb).intValue());? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? }elseif(ObjectUtil.isPassInteger(recharge.getToken_package())){log.info("**************************用戶選擇充值套餐**************************");? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? TokenPackage tp = tp_dao.selectByKey(recharge.getToken_package());? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? persion.setToken(tp.getToken_count() + tp.getGive());? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? persion.setVersion(recharge.getPversion());//樂觀鎖//給用戶加錢inttokenAdd = persion_dao.addPersionToken(persion);if(tokenAdd ==1){log.info("支付寶支付回調(diào),增加用戶金幣成功!");? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? out.println("success");//記得處理完自己的業(yè)務后告訴支付寶做完了,必須返回success,不然支付寶會一直請求。}else{? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? tokenAdd =1/0;log.error("支付寶支付回調(diào),增加用戶金幣失?。?);? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? ? ? ? ? ? }else{log.error("支付寶回調(diào)更新訂單狀態(tài)成功。-----失敗。");? ? ? ? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? ? ? ? }else{//支付失敗recharge.setAlipayordersn(params.get("trade_no"));? ? ? ? ? ? ? ? ? ? ? ? ? ? ? recharge.setState(3);inta =recharge_dao.updateByAlipayordersn(recharge);if(a ==1){log.info("支付寶回調(diào)更新訂單狀態(tài)失敗。-----成功。");? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? out.println("success");? ? ? ? ? ? ? ? ? ? ? ? ? ? ? }else{log.error("支付寶回調(diào)更新訂單狀態(tài)失敗。-----失敗。");? ? ? ? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? ? }else{log.error("支付寶支付回調(diào)驗證失敗,回調(diào)中app_id驗證失敗。");? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? }else{log.error("支付寶支付回調(diào)驗證失敗,回調(diào)中total_amount驗證失敗。");? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? }else{log.error("支付寶支付回調(diào)驗證失敗,回調(diào)中out_trade_no驗證失敗。");? ? ? ? ? ? ? }? ? ? ? ? }else{log.error("支付寶支付回調(diào)驗證失敗,回調(diào)中seller_id驗證失敗。");? ? ? ? ? }? ? ? }else{log.error("支付寶支付回調(diào)驗證失敗,驗證是否是支付寶發(fā)來的通知,失?。?);? ? ? }? }? @SuppressWarnings("unchecked")publicMap GetInfoFromALiPay(HttpServletRequest request) {? ? ? Map params =newHashMap();? ? ? Map requestParams = request.getParameterMap();for(Iterator iter = requestParams.keySet().iterator(); iter.hasNext();) {Stringname = (String) iter.next();String[] values = (String[]) requestParams.get(name);StringvalueStr ="";for(inti =0; i < values.length; i++) {? ? ? ? ? ? ? valueStr = (i == values.length -1) ? valueStr + values[i] : valueStr + values[i] +",";? ? ? ? ? }//亂碼解決,這段代碼在出現(xiàn)亂碼時使用。如果mysign和sign不相等也可以使用這段代碼轉(zhuǎn)化//valueStr = new String(valueStr.getBytes("ISO-8859-1"), "gbk");params.put(name, valueStr);? ? ? }returnparams;? }
4. 批量付款到支付寶賬戶詳解
1. 前期業(yè)主申請操作:
找文檔
這個藏得很深的,因為在支付寶的明面上看不到它,不像App支付,在類似產(chǎn)品大全的頁面能找到;首先進入支付寶的文檔中心在左側(cè)找到歷史接口,但愿你看到的還是這個樣子,如果在那一欄底下也標注了新的接口,那么你就重寫它吧(笑)。

歷史接口
業(yè)主簽約
由于屬于歷史接口,支付寶的考量可能是不建議新商家接入,所以簽約的方式比較繁雜,需要App支付產(chǎn)品簽約成功后,再找人工客服簽約。
人工客服可以通過登錄支付寶開放平臺,如圖1所示的地方喚醒;喚醒的是一只機器人,和它聊三句以上的話,會提醒接入人工,如圖2。然后就可以和人工說我需要接入批量付款到支付寶賬戶這個有密接口,跟著她的節(jié)奏來進行了。這一步也是需要業(yè)主完成的,因為有業(yè)主需要考量的東西,程序員做不了主。

圖1

圖2
人工客服大概會說,該接口每筆會收取0.5%的手續(xù)費,不滿1元按1元算,最高不會超過25元,您同意嗎?,同意就簽約了。一般這個費用是轉(zhuǎn)嫁給用戶的,看業(yè)主如何考量了。
2.程序員進行配置、抒寫:
獲取PID、MD5Key
pid就不說了,以2088開頭的數(shù)字;
配置密鑰,參照文檔上指引的地方,把MD5那個key保存下來,用于請求數(shù)據(jù)的簽名和支付寶返回數(shù)據(jù)的驗簽(后期才注意到,在“提現(xiàn)”這一模塊,簽名是用MD5簽名的)。這里就不放圖了,支付寶的網(wǎng)頁跳來跳去,而且經(jīng)常改動,無法確切它的位置,文檔里倒是更新開很快,這點比某信要強多了。
簡單綜述一下:
用戶,在App中填寫好自己的支付寶真實姓名以及對應的支付寶賬戶,然后在提現(xiàn)那個地方,輸上提現(xiàn)的金額,點擊提交,接口就接收了這個數(shù)據(jù),做一定的處理(比如轉(zhuǎn)嫁手續(xù)費什么的),在提現(xiàn)表里有一個狀態(tài)標識是提現(xiàn)申請、處理中、成功、失??;后臺處理申請的提現(xiàn),拼接一些參數(shù)給支付寶,處理成功后,支付寶給回饋,成功的成功,失敗的失敗。成功,失敗拿系統(tǒng)消息作為提示,失敗之后返回給用戶金錢,可以重新提交申請。失敗的原因可能是用戶支付寶填的不正確,正常情況下是這樣的。但是有很多意外情況,也就帶來了很多坑,下面會拿粗體涉及。
App接口處理方面
對簡單的參數(shù)做驗證后,查看用戶是否綁定了支付寶賬戶信息,以及用戶的提現(xiàn)的錢是否符合夠用(項目里是最低標準是5元)。
/**? * 提現(xiàn)申請? *@return*/@ResponseBody@RequestMapping("applicationWithdraw")publicObject applicationWithdraw(Integer pid, BigDecimal money){//驗證參數(shù)if(ObjectUtil.isPassInteger(pid) && money !=null&& money.compareTo(new BigDecimal(5)) !=-1){if(aliacount_dao.selectByPersionId(pid).size()!=0){//用戶已綁定支付寶Persion persion = persion_dao.queryPersionByIdForIM(pid);if(money.compareTo(new BigDecimal(persion.getCapitalBalance())) !=1){// 申請?zhí)岈F(xiàn)業(yè)務boolean flag = wiService.insertApplication(persion, money);returnflag==true?"success":"error";? ? ? ? ? ? ? }else{return"error";//用戶金額不足}? ? ? ? ? }else{//用戶未綁定支付寶return"fail";? ? ? ? ? }? ? ? }else{return"error";? ? ? }? }
下面這塊給拼接了一個流水號,在后面會有介紹;然后轉(zhuǎn)嫁手續(xù)費到用戶身上,插記錄,扣錢;以事務的方式進行控制。關于用戶手續(xù)費方面,其實還可以更加靈活一些,無非就是多提少收,少提多收;這個也得業(yè)主考量了。
**? * 申請?zhí)岈F(xiàn)業(yè)務? *@return*/@Transactionalpublic boolean insertApplication(Persion persion,BigDecimal money){StringonceId = UUIDHashCode.getOrderIdByUUId();//獲得一個隨機唯一標識//扣除支付寶的手續(xù)費 0.5% 最高不會超過25元 最低不會超過1元BigDecimal platform_cost =? money.multiply(newBigDecimal(0.005)).setScale(2, BigDecimal.ROUND_HALF_UP);if(platform_cost.compareTo(newBigDecimal(25)) ==1){? ? ? ? ? platform_cost =newBigDecimal(25);? ? ? }elseif(platform_cost.compareTo(newBigDecimal(1)) ==-1){? ? ? ? ? platform_cost =newBigDecimal(1);? ? ? }? ? ? BigDecimal getMoney = money.subtract(platform_cost);//扣除手續(xù)費之后的金額intnum=0;if(getMoney.compareTo(newBigDecimal(0)) ==1){// 新增一條提現(xiàn)申請記錄num= withMa.insertSelective(newWithdraw(onceId,persion.getId(),getMoney,1,platform_cost));? ? ? }if(num==1){// 修改該用戶信息中可提現(xiàn)金額信息persion.setCapitalBalance(money.doubleValue());? ? ? ? ? Persion queryPersion = perMa.queryPersionByIdForIM(persion.getId());if(queryPersion !=null){? ? ? ? ? ? ? persion.setVersion(queryPersion.getVersion());intwithdr = perMa.cutPersionCap(persion);returnwithdr>0?true:false;? ? ? ? ? }else{num=1/0;returnfalse;? ? ? ? ? }? ? ? }else{num=1/0;returnfalse;? ? ? }? }
后臺進行處理
因為這個提現(xiàn)還是需要人工操作的,比如輸支付密碼什么的,支付寶的考量也可能是為了安全吧,所以需要有后臺的一系列邏輯。像App支付一樣,先從基本信息配置入手。
AlipayConfig這里關鍵在于私鑰和公鑰,可以看一看阮一峰的數(shù)字簽名是什么支付寶App支付的公鑰和這里提現(xiàn)所用的公鑰是不一致的,對應的RSA的與RSA2的公鑰也是不同的。之前不了解公鑰的意義,這里的公鑰和App支付的共用了,也算是無知的坑吧。商戶安全校驗碼就是上面讓保存的那個MD5的key值。
publicclassAlipayConfig{//↓↓↓↓↓↓↓↓↓↓請在這里配置您的基本信息↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓// 合作身份者ID,以2088開頭由16位純數(shù)字組成的字符串publicstaticStringpartner ="2088xxxxxxx";//商戶支付寶賬號publicstaticStringseller_email ="";//商戶真實姓名publicstaticStringaccount_name =" XXXXX科技有限公司";// 支付寶用于提現(xiàn)的公鑰,一般情況下無需修改該值? (提現(xiàn)),也可與支付寶map網(wǎng)關產(chǎn)品密鑰的支付寶公鑰做對比,應該是一致的。publicstaticStringali_public_key ="MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCnxj/9qwVfgoUh/y2W89L6BkRAFljhNhgPdyPuBV64bfQNN1PjbCzkIM6qRdKBoLPXmKKMiFYnkd6rAoprih3/PrQEB/VsW8OoM8fxn67UDYuyBTqA23MML9q1+ilIZwBC2AQ2UBVOrFXfFl75p6/B5KsiNG9zpgmLCUYuLkxpLQIDAQAB";//商戶安全校驗碼publicstaticStringkey ="MD5key";//支付寶異步通知地址? ? 需http://格式的完整路徑,不允許加?id=123這類自定義參數(shù)publicstaticStringnotify_url = PathUtil.GetDemain() +"/WoBanAdmin/ALiPay/TransNotify";//public static String notify_url = "http://xiaofanfight.viphk.ngrok.org/WoBanAdmin/ALiPay/TransNotify";//↑↑↑↑↑↑↑↑↑↑請在這里配置您的基本信息↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑// 調(diào)試用,創(chuàng)建TXT日志文件夾路徑publicstaticStringlog_path ="D:\\";// 字符編碼格式 目前支持 gbk 或 utf-8publicstaticStringinput_charset ="utf-8";// 簽名方式 不需修改(退款簽名方式)publicstaticStringsign_type ="MD5";}
代碼里有很多是運用支付寶提供的Demo,支付寶并沒有對這個接口提供jar包,公司的前輩也對此進行了開創(chuàng)性的研究,而我只是完善了一下,并寫成了拙劣的文檔。
后臺的流程及頁面展示:
頁面是位于pages - recharge 里的。

提現(xiàn)申請列表 money_drawing.jsp
這里每進一次頁面會有提示,因為很重要,所以每次都提醒;提示框的藍色按鈕可以跳往支付寶對證書的提示,它會說,目前只支持IE 32位部分瀏覽器以及UC瀏覽器,Mac下只支持safari;但我實際測試的時候,只支持火狐......支付寶需要證書是為了后面你輸入支付寶支付密碼時候的安全。

提現(xiàn)申請詳情 BalanceDetail.jsp
一個的時候展示一個的詳情,多人的時候展示多人的信息,點擊結(jié)算后的頁面是以彈出層展示的

點擊結(jié)算后 HandleClearing.jsp
這里的話其實不加驗證也可以的,畢竟支付寶那邊還要驗證支付密碼,或者這里也可以結(jié)合一下管理員的手機號,進行處理。
輸入密碼,點擊確定后,就走Controller了,從控制層里拼接一些參數(shù),跳往支付寶頁面。下面詳述ALiPayController
實際上,在上一張圖我們可知,能傳過來的數(shù)據(jù)無非就幾個:
1.哪個管理員操作的;
2.哪些人申請的提現(xiàn)。
因為像申請的錢什么的是依賴表的而不是頁面上的數(shù)據(jù)。
參數(shù)ids實際上控制著三個選擇,全部處理、按照選擇處理、單個處理;然后一些基礎信息的取得,計算提現(xiàn)的總額(依靠SQL),最為關鍵就是拼接參數(shù),按照文檔進行,參數(shù)中最為關鍵的就是付款詳細數(shù)據(jù)以及批次號和流水號,批次號每次都是隨機生成的,因為支付寶將相同的批次號視作同一筆申請,而流水號在App端提交的時候就做了處理;這里不需要關注一些細節(jié),在支付寶提供的Demo里有涉及,有興趣可以點進去看一看。
/**
*
* Method ALiPayTrans
* 方法作用:請求批量轉(zhuǎn)賬前一步
* 適用條件:提現(xiàn)
* 使用方法:請參照最新的支付寶批量轉(zhuǎn)賬有密接口文檔
* @param aid? ? 管理員id
* @param ids? ? ? 提現(xiàn)申請人id的集合
* @param out? ? ? 用于給支付寶回饋(大概吧)
* @since Met 2.0
*/@RequestMapping(value ="ALiPayTrans", method = RequestMethod.GET)publicvoidALiPayTrans(Integer aid,Stringids,PrintWriter out) {? ? ? DecimalFormat df =newDecimalFormat(".00");//將double類型的數(shù)據(jù)保留兩位數(shù)List wlist =newArrayList();if(ids!=null && !ids.equals("")){if(ids.equals("all")){//全部處理wlist = withdrawMapper.selectWithdraw(newWithdraw());? ? ? ? ? }else{if(ids.contains(",")){StringIds=ids.trim().substring(0, ids.length() -1);? ? ? ? ? ? ? ? ? Withdraw withdraw =newWithdraw();? ? ? ? ? ? ? ? ? withdraw.setId(0);? ? ? ? ? ? ? ? ? withdraw.setBatchNo(Ids);? ? ? ? ? ? ? ? ? wlist = withdrawMapper.selectWithdraw(withdraw);? ? ? ? ? ? ? }else{//單個流水wlist = withdrawMapper.selectWithdraw(newWithdraw(Integer.parseInt(ids)));? ? ? ? ? ? ? }? ? ? ? ? }//服務器異步通知頁面路徑? 通知提現(xiàn)成功與失敗Stringnotify_url = AlipayConfig.notify_url;//付款賬號Stringemail = AlipayConfig.seller_email;//付款賬戶名Stringaccount_name = AlipayConfig.account_name;//必填,個人支付寶賬號是真實姓名公司支付寶賬號是公司名稱//付款當天日期Stringpay_date = DateUtil.getDays();//必填,格式:年[4位]月[2位]日[2位],如:20100801//批次號Stringbatch_no = DateUtil.getDays() + DateUtil.getThree() + DateUtil.getThree();//必填,格式:當天日期[8位]+序列號[3至16位],如:201008010000001//付款總金額//計算總額BigDecimal summoney = withdrawMapper.querySumByGet(wlist);if(summoney != null && summoney.compareTo(newBigDecimal(10000000)) ==-1){Stringbatch_fee = df.format(summoney).toString();//必填,即參數(shù)detail_data的值中所有金額的總和//付款筆數(shù)Integer num = wlist.size();Stringbatch_num = num.toString();//必填,即參數(shù)detail_data的值中,“|”字符出現(xiàn)的數(shù)量加1,最大支持1000筆(即“|”字符出現(xiàn)的數(shù)量999個)/*
* 下列付款詳細數(shù)據(jù)說明以及示例
* String detail_data = batch_no + "^" + "zhangsan@qq.com" + "^" + "張三" + "^" + batch_fee + "^備注說明";
* 解釋:其中batch_no為上面生成的轉(zhuǎn)賬批次號;zhangsan@qq.com為需要轉(zhuǎn)賬的支付寶賬戶;張三為轉(zhuǎn)賬支付寶賬戶的真實姓名;
* batch_fee為轉(zhuǎn)賬金額,最后的參數(shù)為附加參數(shù),可以對本次轉(zhuǎn)賬備注說明,只要是字符串就可以,但長度不宜過長。請根據(jù)需要以此替換
*///必填,即參數(shù)detail_data的值中,“|”字符出現(xiàn)的數(shù)量加1,最大支持1000筆(即“|”字符出現(xiàn)的數(shù)量999個)//付款詳細數(shù)據(jù)Stringdetail_data ="";for(inti=0;i sParaTemp =newHashMap();? ? ? ? ? ? ? sParaTemp.put("service","batch_trans_notify");? ? ? ? ? ? ? sParaTemp.put("partner", AlipayConfig.partner);? ? ? ? ? ? ? sParaTemp.put("_input_charset", AlipayConfig.input_charset);? ? ? ? ? ? ? sParaTemp.put("notify_url", notify_url);? ? ? ? ? ? ? sParaTemp.put("email", email);? ? ? ? ? ? ? sParaTemp.put("account_name", account_name);? ? ? ? ? ? ? sParaTemp.put("pay_date", pay_date);? ? ? ? ? ? ? sParaTemp.put("batch_no", batch_no);? ? ? ? ? ? ? sParaTemp.put("batch_fee", batch_fee);? ? ? ? ? ? ? sParaTemp.put("batch_num", batch_num);? ? ? ? ? ? ? sParaTemp.put("detail_data", detail_data);//建立請求//Log log =newLog(aid,"處理提現(xiàn)申請操作,處理額度為"+ summoney +"元");? ? ? ? ? ? ? logMa.insert(log);StringsHtmlText = AlipaySubmit.buildRequest(sParaTemp,"post","確認");? ? ? ? ? ? ? out.println(sHtmlText);? ? ? ? ? }else{? ? ? ? ? ? ? out.print("alert('處理批次額度超限。最高不超過1000萬元。');");? ? ? ? ? }? ? ? }? }
順利就跳到了以下的頁面,值得注意的是,在這個Controller內(nèi),將這筆提現(xiàn)申請改成了處理中,避免其他人會重復處理。請注意這個狀態(tài)。

支付寶頁面
支付寶提示當前操作環(huán)境不支持支付寶控件,因為我是谷歌瀏覽器打開的,這種情況支付寶不會給異步通知,因為它不知道是這種問題,它會通知的情況只有兩種:
1.支付成功;
2.支付失敗,給轉(zhuǎn)賬的支付寶用戶信息不正確。
所以處理中的作用就是存放意外情況下的申請,管理員可以在合適的環(huán)境下,將處理中的申請再次轉(zhuǎn)化為申請中,再次提交。

申請中列表

支付失敗之一 收款支付寶賬戶信息校驗不通過
以下為出現(xiàn)意外的情況截圖:

不會通知的情況 未安裝證書

上一步安裝了之后有刷新選項,但是支付寶會這樣說,也不需要管,因為剛剛那筆訂單已經(jīng)在提現(xiàn)申請中里了

不會通知的情況 安裝了證書之后未安裝電子證書
正常情況下:

安裝了電子證書 正常的情況1
下面這個圖就說明了,支付寶會給打款成功的回調(diào)。

安裝了電子證書 正常的情況2

支付寶的異步通知
剛剛問了半天客服,客服也無法說清楚,建議是在IE8 32位瀏覽器下進行的,可是IE8會和layui會有沖突,這就有點尷尬了。以上的截圖是在火狐下進行的??头膊恢每煞?。
完成了支付寶付款的邏輯,就剩下接收回調(diào)的處理了,也在Controller層,對于成功走成功邏輯,失敗走失敗邏輯。
這里關注一下支付寶的說明即可:如果成功的信息為空,證明都失敗了,反之;如果兩者都不為空,就需要各自走各自的邏輯了,根據(jù)流水號查出提現(xiàn)的詳情,成功推送信息,失敗返回資金并推送。

回調(diào)處理的關鍵
@RequestMapping(value ="TransNotify", method = RequestMethod.POST)/*** 批量付款數(shù)據(jù)中轉(zhuǎn)賬成功的詳細信息 String success_details* 批量付款數(shù)據(jù)中轉(zhuǎn)賬失敗的詳細信息 String fail_details* 批量付款數(shù)據(jù)中轉(zhuǎn)賬批次號 String batch_no*/publicvoidTransNotify(HttpServletRequest request,Stringsuccess_details,Stringbatch_no,Stringfail_details, PrintWriter out) {//獲取支付寶POST過來反饋信息Map params = GetInfoFromALiPay(request);? ? ? ? boolean flag =true;if(AlipayNotify.verify(params)){//驗證成功//判斷是否在商戶網(wǎng)站中已經(jīng)做過了這次通知返回的處理;如果沒有做過處理,那么執(zhí)行商戶的業(yè)務程序;如果有做過處理,那么不執(zhí)行商戶的業(yè)務程序//可以判斷success_details是否為null來標識轉(zhuǎn)賬是否成功,支付寶方面明確說明如果轉(zhuǎn)賬成功success_details不為null,fail_details則//為null;若轉(zhuǎn)賬失敗success_details為null而fail_details不為null,同樣根據(jù)batch_no來查詢轉(zhuǎn)賬對象并更新轉(zhuǎn)賬狀態(tài)if(fail_details ==null){//提現(xiàn)全部成功 處理相關業(yè)務 看是否已經(jīng)處理過了 改狀態(tài)? 推送withdrawService.getMoneySuccess(batch_no);? ? ? ? ? ? }elseif(success_details ==null){//提現(xiàn)全部失敗//返還未提現(xiàn)用戶的金幣 推送Stringinfo = withdrawService.getMoneyError(batch_no);if("error".equals(info)){? ? ? ? ? ? ? ? ? ? flag =false;//自己sql出錯,請求支付寶再次發(fā)送驗證}? ? ? ? ? ? }else{//轉(zhuǎn)賬部分成功/部分失敗Stringinfo = withdrawService.getMoneySuccessOrError(batch_no, success_details, fail_details);if("error".equals(info)){? ? ? ? ? ? ? ? ? ? flag =false;//自己sql出錯,請求支付寶再次發(fā)送驗證}? ? ? ? ? ? }if(flag){? ? ? ? ? ? ? ? out.println("success");//請勿修改該值!}else{? ? ? ? ? ? ? ? out.println("fail");//自己sql出錯,請求支付寶再次發(fā)送驗證}? ? ? ? }else{//驗證失敗//程序執(zhí)行完后必須打印輸出“success”(不包含引號)。如果商戶反饋給支付寶的字符不是success這7個字符,支付寶服務器會不斷重發(fā)通知,直到超過24小時22分鐘out.println("fail");? ? ? ? }? ? }@SuppressWarnings("unchecked")? ? publicMap GetInfoFromALiPay(HttpServletRequest request) {Map params =newHashMap();MaprequestParams = request.getParameterMap();for(Iteratoriter = requestParams.keySet().iterator(); iter.hasNext();) {Stringname = (String) iter.next();String[] values = (String[]) requestParams.get(name);StringvalueStr ="";for(inti =0; i < values.length; i++) {? ? ? ? ? ? ? ? valueStr = (i == values.length -1) ? valueStr + values[i] : valueStr + values[i]? ? ? ? ? ? ? ? ? ? ? ? +",";? ? ? ? ? ? }//亂碼解決,這段代碼在出現(xiàn)亂碼時使用。如果mysign和sign不相等也可以使用這段代碼轉(zhuǎn)化//valueStr = new String(valueStr.getBytes("ISO-8859-1"), "gbk");params.put(name, valueStr);? ? ? ? }returnparams;? ? }
5. 結(jié)束
第一次總結(jié),文字還是很冗余的。在這里還是需要感謝一下ydm公司的前輩。寫于2017-03-17。