javascript
SpringBoot集成支付宝沙箱手机网站支付详细流程和踩坑分享
描述
本文主要講解SpringBoot集成支付寶沙箱手機網站支付,即網頁點擊按鈕發起支付,跳轉到沙箱app付款
由于其他博客的流程大多籠統,有時候并不能找到正確的集成方式,本文盡可能詳細的闡述付款,異步通知,驗簽,退款的全部流程以及踩坑的分享,希望可以幫助你們少走彎路
必要的準備工作如公私鑰,沙箱申請詳見??支付寶沙箱簡單集成
編譯環境:IDEA
支付寶手機支付Demo
地址:https://docs.open.alipay.com/203/105910/
其中僅有一個信息配置類,其他的邏輯都集成在了,jsp頁面中,個人感覺不是太友好
所以本文把邏輯集成到了service和controller中
流程
1. 添加Maven依賴
//支付寶SDK <dependency><groupId>com.alipay.sdk</groupId><artifactId>alipay-sdk-java</artifactId><version>4.5.0.ALL</version> </dependency> //不用寫get和set方法的輔助依賴,可以根據需要選擇不添加,那就要手動添加get()和set()方法 //若僅添加依賴還是報錯,需要在File-settings-Plugins中搜索添加,也可百度詳細安裝方式 <dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId> </dependency>2. 添加配置文件
為了實現模塊化,看上去更專業(嘿嘿),在根目錄(與Application同級)建立config文件夾,該文件夾下建立AlipayConfig.java文件
import lombok.Data; import org.springframework.stereotype.Component;@Data @Component public class AlipayConfig {// 沙箱appidpublic static String APPID = "2016101400683082";// 私鑰 pkcs8格式的public static String RSA_PRIVATE_KEY = "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";// 請求網關 固定public static String URL = "https://openapi.alipaydev.com/gateway.do";//異步通知地址public static String notify_url = "http://xxx/alipay/notify";//同步地址 // public static String return_url = "http://xxx/alipay/return";// 編碼public static String CHARSET = "UTF-8";// 返回格式public static String FORMAT = "json";// 支付寶公鑰public static String ALIPAY_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjrEVFMOSiNJXaRNKicQuQdsREraftDA9Tua3WNZwcpeXeh8Wrt+V9JilLqSa7N7sVqwpvv8zWChgXhX/A96hEg97Oxe6GKUmzaZRNh0cZZ88vpkn5tlgL4mH/dhSr3Ip00kvM4rHq9PwuT4k7z1DpZAf1eghK8Q5BgxL88d0X07m9X96Ijd0yMkXArzD7jg+noqfbztEKoH3kPMRJC2w4ByVdweWUT2PwrlATpZZtYLmtDvUKG/sOkNAIKEMg3Rut1oKWpjyYanzDgS7Cg3awr1KPTl9rHCazk15aNYowmYtVabKwbGVToCAGK+qQ1gT3ELhkGnf3+h53fukNqRH+wIDAQAB";// 沙箱支付寶公鑰public static String ZHIFUBAO_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArS0SLdzGCafgBwLv3IXIrr7cXKk2pzC5fgQxVz1M9F05ReSXQOfWfjDstNrToiI3kwY3XRGI/ULzywbpKwTm/IzOULxSnexCCJRxQonmQcV1C3ixsQi9rqkL0XaV7YBQl0DfmZoHKbbpmfj/7Uv9hQ2viJ/3n844bhaIwYhR7+Smu8xk+hbT0DEpp75cJV9pt+ngCHj6x3vkGHPj7w70JGKY73wkT6wBD0A7vz/cHHkMH6EeIkus1R5umd2rGXE/8zaPFRpysNKiys4ujAW7tOwCkqiuaon3AxGdQrHom3Twp+cpm7gkzc5v/p4qHgVUyZVKjrj6VJTRIgEQOegDtQIDAQAB";// RSA2public static String SIGNTYPE = "RSA2"; }這里說明一下
1.同步地址是支付成功后跳轉的地址,由于我的業務邏輯是前端傳給后臺要跳轉的地址,所以在這并未設置,根據需要配置
2.為什么有支付寶公鑰和沙箱支付寶公鑰那,demo中自身寫的是支付寶公鑰,然而和我的沙箱中寫的支付寶公鑰并不一樣。但是使用demo的公鑰下單并沒有問題,但是在驗簽和退款時都會出錯,要使用沙箱中的支付寶公鑰。并沒有嘗試下單時也使用沙箱支付寶公鑰,諸位可以試一試。這兩個字符串長得很像,我一開始以為是一個!!!
3. 創建service層
同樣根目錄創建文件夾,后創建 AlipayService接口,分別寫創建訂單,異步通知和退款三個接口方法
public interface AlipayService {String create(String orderId, String returnUrl);//異步通知會返回一個requestvoid notify(HttpServletRequest request);void refund(String orderId); }后創建Impl實現類? AlipayServiceImpl.java
import com.alipay.api.AlipayApiException; import com.alipay.api.AlipayClient; import com.alipay.api.DefaultAlipayClient; import com.alipay.api.domain.AlipayTradeRefundModel; import com.alipay.api.domain.AlipayTradeWapPayModel; import com.alipay.api.internal.util.AlipaySignature; import com.alipay.api.request.AlipayTradeRefundRequest; import com.alipay.api.request.AlipayTradeWapPayRequest; import com.imooc.config.AlipayConfig; import com.imooc.service.AlipayService; import com.imooc.utils.JsonUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Iterator; import java.util.Map;/*** @Author Sakura* @Date 9/9/2019**/ @Service @Slf4j public class AlipayServiceImpl implements AlipayService {@Autowiredprivate AlipayConfig alipayConfig;//訂單名稱private static final String ORDER_NAME = "自定義訂單名稱";//和支付寶簽約的產品碼 固定值private static final String PRODUCTCODE = "QUICK_WAP_WAY";//支付成功標識(可退款的簽約是TRADE_SUCCESS,不可退款的簽約是TRADE_FINISHED)private static final String TRADE_SUCCESS = "TRADE_SUCCESS";@Overridepublic String create(String orderId, String returnUrl) {AlipayClient client = new DefaultAlipayClient(alipayConfig.URL, alipayConfig.APPID, alipayConfig.RSA_PRIVATE_KEY, alipayConfig.FORMAT, alipayConfig.CHARSET, alipayConfig.ALIPAY_PUBLIC_KEY,alipayConfig.SIGNTYPE);AlipayTradeWapPayRequest alipay_request=new AlipayTradeWapPayRequest();// 封裝請求支付信息AlipayTradeWapPayModel model= new AlipayTradeWapPayModel();//訂單編號,不可重復model.setOutTradeNo(orderId);//訂單名稱model.setSubject(ORDER_NAME);//訂單金額model.setTotalAmount("0.01");//產品嗎model.setProductCode(PRODUCTCODE);alipay_request.setBizModel(model);//支付成功后跳轉的地址alipay_request.setReturnUrl(returnUrl);//異步通知地址alipay_request.setNotifyUrl(alipayConfig.notify_url);// form表單生產String result = "";try {// 調用SDK生成表單result = client.pageExecute(alipay_request).getBody();} catch (AlipayApiException e) {log.info("【支付寶支付】支付失敗 error={}", e);}return result;}@Overridepublic void notify(HttpServletRequest request) {Map<String, String> map = new HashMap<>();Map<String, String[]> requestParams = request.getParameterMap();for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext(); ) {String name = iter.next();String[] values = requestParams.get(name);String valueStr = "";for (int i = 0; i < values.length; i++) {valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ",";}map.put(name, valueStr);}//驗證簽名boolean signVerified = false;try {signVerified = AlipaySignature.rsaCheckV1(map, alipayConfig.ZHIFUBAO_PUBLIC_KEY, alipayConfig.CHARSET, alipayConfig.SIGNTYPE);} catch (com.alipay.api.AlipayApiException e) {log.info("[支付驗證] 異常={}", JsonUtil.toJson(e));return;}if (signVerified) {//處理自己的業務邏輯}// log.info("[支付驗證] 驗證結果={}", signVerified);return;}@Overridepublic void refund(String orderId) {AlipayClient client = new DefaultAlipayClient(alipayConfig.URL, alipayConfig.APPID, alipayConfig.RSA_PRIVATE_KEY, alipayConfig.FORMAT, alipayConfig.CHARSET, alipayConfig.ZHIFUBAO_PUBLIC_KEY,alipayConfig.SIGNTYPE);AlipayTradeRefundRequest alipay_request = new AlipayTradeRefundRequest();AlipayTradeRefundModel model=new AlipayTradeRefundModel();//退款的訂單Id,也可以設置流水號model.setOutTradeNo(orderId);//退款金額model.setRefundAmount("0.01");alipay_request.setBizModel(model);String alipay_response = "";try {alipay_response = client.execute(alipay_request).getBody();} catch (AlipayApiException e) {log.info("【支付寶支付】退款失敗 error={}", e);} // log.info("[支付退款] response={}", alipay_response);} }日志我使用的是Slf4j,也可以直接System.out.println()
以上代碼下單,退款的部分是demo中的代碼,拷貝改了一下,具體的支付等的注意事項在編寫controller再詳細闡述
這里重點說一下異步通知? ?官方文檔:https://docs.open.alipay.com/203/105286/
異步通知的主要作用就是驗證付款之后,修改數據庫的支付信息等
支付成功后跳轉return_url的同時回將支付的信息異步發送到notify_url,可以是一個網頁,但最好是controller中的一個路徑,便于業務處理
跳轉url以 http://xxx/?...的方式返回,?之后是攜帶的信息,完整的是
https://api.xx.com/receive_notify.htm?total_amount=2.00&buyer_id=2088102116773037&body=大樂透2.1&trade_no=2016071921001003030200089909&refund_fee=0.00¬ify_time=2016-07-19 14:10:49&subject=大樂透2.1&sign_type=RSA2&charset=utf-8¬ify_type=trade_status_sync&out_trade_no=0719141034-6418&gmt_close=2016-07-19 14:10:46&gmt_payment=2016-07-19 14:10:47&trade_status=TRADE_SUCCESS&version=1.0&sign=kPbQIjX+xQc8F0/A6/AocELIjhhZnGbcBN6G4MM/HmfWL4ZiHM6fWl5NQhzXJusaklZ1LFuMo+lHQUELAYeugH8LYFvxnNajOvZhuxNFbN2LhF0l/KL8ANtj8oyPM4NN7Qft2kWJTDJUpQOzCzNnV9hDxh5AaT9FPqRS6ZKxnzM=&gmt_create=2016-07-19 14:10:44&app_id=2015102700040153&seller_id=2088102119685838¬ify_id=4a91b7a78a503640467525113fb7d8bg8e所以我們用一個Map把key-value存儲起來,也可以使用
@RequestParam("total_amout") String total_amout的方式僅接收自己所需要的參數,因為很多參數都是多余的,看個人,閑麻煩就用Map
比較重要的信息有??
out_trade_no 訂單號 trade_status支付狀態 total_amount訂單金額一般驗證付款的嚴格流程是
- 使用AlipaySignature.rsaCheckV1驗證訂單是否正確
- out_trade_no訂單號是否是后臺支付時的訂單號
- 判斷total_amount付款金額是否等于后臺應付金額
- 驗證付款方是否是下單方
這里第四條看自己業務能不能支持他人代付
附我的map中的信息
4.創建controller
根目錄創建controller文件夾,內創建 AlipayController.java
import com.imooc.exception.SellException; import com.imooc.service.AlipayService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest; import java.util.Map;/*** @Author Sakura* @Date 9/9/2019**/ @Controller @RequestMapping("/alipay") public class AlipayController {@Autowiredprivate AlipayService alipayService;@GetMapping("/create")@ResponseBodypublic String create(@RequestParam("orderId") String orderId,@RequestParam("returnUrl") String returnUrl) {//發起支付String payUrl = alipayService.create(orderId,returnUrl);return payUrl;}/*** 支付寶異步通知*/@PostMapping("/notify")public void notify(HttpServletRequest request) {alipayService.notify(request);} }1. 創建訂單時,一定要使用? @ResponseBody ,并return 通過service返回的結果,因為前端點擊按鈕發起支付時,會先跳轉至付款頁面,后打開沙箱app付款
2. 退款的業務邏輯, 一般是自己的service中先進行數據庫的退款信息更改,然后
@Autowired private AlipayService alipayService;調用refund方法傳入訂單編號
踩坑分享
1. 前端點擊按鈕發起支付無法跳轉頁面支付
controller中創建訂單一定要使用? @ResponseBody ,并return 通過service返回的結果,使返回結果是一個頁面
2. 驗簽,退款異常
new?DefaultAlipayClient() 時,其中傳遞的時沙箱中支付寶公鑰而不是應用公鑰(通過開發助手生成的),也不是demo中自帶的支付寶公鑰
3. 支付寶網關地址是固定的,一定不要改
有不妥之處,歡迎交流~~
總結
以上是生活随笔為你收集整理的SpringBoot集成支付宝沙箱手机网站支付详细流程和踩坑分享的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 立统 视频防泄密系统 技术白皮书2021
- 下一篇: JS之使用Echarts画出人际关系图