SpringBoot微信點餐專案(二)——微信內調起支付寶支付
iwehdio的部落格園:https://www.cnblogs.com/iwehdio/
1、微信部分
-
微信支付文件:【微信支付】JSAPI支付開發者文件。
-
微信公眾賬戶測試號獲取:公眾號測試號。
-
使用natapp內網穿透:基於springboot接入微信公眾號(內網穿透技術)。以下捱打url中,帶有http://xxxx.natappfree.cc格式的均為請求到的不同的穿透域名。
-
此外,還需要設定體驗介面許可權表下的網頁賬號的授權回撥頁面域名(不能填帶/的路徑)。
-
獲取openID:手工方式和SDK方式。
-
請求:
重定向到 /sell/wechat/authorize
-
引數:
returnUrl: http://xxx.com/abc //【必填】
-
返回:
http://xxx.com/abc?openid=oZxSYw5ldcxv6H0EU67GgSXOUrVg
-
-
網頁回撥手動獲取openID:微信網頁授權
-
第一步:使用者同意授權,獲取code
-
請求使用者授權,訪問:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect ---- https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=http://hpqv53.natappfree.cc/sell/auth&response_type=code&scope=snsapi_base&state=123#wechat_redirect
-
具體引數含義:
引數 是否必須 說明 appid 是 公眾號的唯一標識 redirect_uri 是 授權後重定向的回撥連結地址, 請使用 urlEncode 對連結進行處理 response_type 是 返回型別,請填寫code scope 是 應用授權作用域,snsapi_base (不彈出授權頁面,直接跳轉,只能獲取使用者openid),snsapi_userinfo (彈出授權頁面,可通過openid拿到暱稱、性別、所在地。並且, 即使在未關注的情況下,只要使用者授權,也能獲取其資訊 ) state 否 重定向後會帶上state引數,開發者可以填寫a-zA-Z0-9的引數值,最多128位元組 #wechat_redirect 是 無論直接開啟還是做頁面302重定向時候,必須帶此引數 -
如果使用者同意授權,頁面將跳轉至 redirect_uri/?code=CODE&state=STATE。
- code作為換取access_token的票據,每次使用者授權帶上的code將不一樣,code只能使用一次,5分鐘未被使用自動過期。
-
-
第二步:通過code換取網頁授權access_token
-
獲取code後,請求以下連結獲取access_token:
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code --- 改成具體的APPID和SECRETID https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRETID&code=CODE&grant_type=authorization_code
-
引數說明:
引數 是否必須 說明 appid 是 公眾號的唯一標識 secret 是 公眾號的appsecret code 是 填寫第一步獲取的code引數 grant_type 是 填寫為authorization_code -
返回格式:
//成功時 { "access_token":"ACCESS_TOKEN", "expires_in":7200, "refresh_token":"REFRESH_TOKEN", "openid":"OPENID", "scope":"SCOPE" } //錯誤時 {"errcode":40029,"errmsg":"invalid code"}
-
引數說明:
引數 描述 access_token 網頁授權介面呼叫憑證,注意:此access_token與基礎支援的access_token不同 expires_in access_token介面呼叫憑證超時時間,單位(秒) refresh_token 使用者重新整理access_token openid 使用者唯一標識,請注意,在未關注公眾號時,使用者訪問公眾號的網頁,也會產生一個使用者和公眾號唯一的OpenID scope 使用者授權的作用域,使用逗號(,)分隔
-
-
程式碼:
@RestController public class WeiXinController { private final Logger logger = LoggerFactory.getLogger(WeChatController.class); @GetMapping("/auth") public void auth(@RequestParam("code") String code) { logger.info("code={}",code); String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRETID&code="+code+"&grant_type=authorization_code"; RestTemplate restTemplate = new RestTemplate(); String access = restTemplate.getForObject(url, String.class); logger.info(access); } }
-
-
使用SDK方式:MP_OAuth2網頁授權
-
maven依賴:
<dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-mp</artifactId> <version>2.7.0</version> </dependency> <!-- Springboot的配置類 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency>
-
配置類:
@Component @ConfigurationProperties(prefix = "wechat") public class WechatAccountConfig { private String mpAppId; private String mpAppSecret; }
-
配置檔案:
wechat: mpAppId: wx4451dcc6f165f0f4 mpAppSecret: cf93b89968d2fdf90af56d733ccd1a16
-
自動配置:
@Component @Configuration public class WechatMConfig { @Autowired private WechatAccountConfig accountConfig; @Bean public WxMpService wxMPService(){ WxMpService wxMpService = new WxMpServiceImpl(); wxMpService.setWxMpConfigStorage(wxMpConfigStorage()); return wxMpService; } @Bean public WxMpConfigStorage wxMpConfigStorage(){ WxMpInMemoryConfigStorage wxMpConfigStorage = new WxMpInMemoryConfigStorage(); wxMpConfigStorage.setAppId(accountConfig.getMpAppId()); wxMpConfigStorage.setSecret(accountConfig.getMpAppSecret()); return wxMpConfigStorage; } }
-
SDK網頁授權:
- authorize方法相當於獲取code。
- userInfo方法相當於根據code獲取openid。
@Controller @RequestMapping("/wechat") public class WechatController { @Autowired private WxMpService wxMpService; @GetMapping("/authorize") public String authorize(@RequestParam("returnUrl") String returnUrl){ String url = "http://hpqv53.natappfree.cc/sell/wechat/userInfo"; String redirectUrl = wxMpService.oauth2buildAuthorizationUrl(url, WxConsts.OAUTH2_SCOPE_BASE, returnUrl); System.out.println(redirectUrl); return "redirect:"+redirectUrl; } @GetMapping("/userInfo") public String userInfo(@RequestParam("code") String code, @RequestParam("state") String returnUrl){ WxMpOAuth2AccessToken accessToken; try { accessToken = wxMpService.oauth2getAccessToken(code); } catch (WxErrorException e) { throw new SellException(ResultEnum.WECHAT_MP_ERROR,e.getError().getErrorMsg()); } String openId = accessToken.getOpenId(); return "redirect:"+returnUrl+"?openid="+openId; } }
-
測試請求以下連結:
http://hpqv53.natappfree.cc/sell/wechat/authorize?returnUrl=http://www.imooc.com
-
-
接入前端除錯:
-
首先更改前端專案中的/config/index.js中:
sellUrl: 'sell.com', openidUrl: 'http://43bwcy.natappfree.cc/sell/wechat/authorize',
-
然後npm run build編譯,編譯後的檔案在dist目錄下,再複製到Nginx部署靜態資源的位置。
-
獲取電腦和手機的IP地址。測試能不能ping通。
-
在手機上設定手動HTTP代理,地址為電腦的IP地址,埠為8888。
-
下載fiddler,設定監聽埠為8888,並且允許遠端連線。
-
2、支付部分
-
主要用到的是統一下單和支付結果通知兩個API。
-
使用SDK:支付SDK。
-
請求:
重定向 /sell/pay/create
-
引數:
orderId:16465165415452 returnUrl:http://xxx.com/abc/order/16465165415452
-
返回:
http://xxx.com/abc/order/16465165415452
-
maven依賴:
<dependency> <groupId>cn.springboot</groupId> <artifactId>best-pay-sdk</artifactId> <version>1.1.0</version> </dependency>
-
-
但是微信測試號不提供支付功能,只能用在微信內拉起支付寶的方式測試,因為支付寶提供了沙箱測試環境。開放平臺-沙箱環境 (alipay.com)。
-
使用官方的老SDK
-
引入老SDK的maven依賴:
<dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.10.167.ALL</version> </dependency>
-
根據官方的接入文件,主要是對AlipayClient和AlipayTradeWapPayRequest進行一些配置,然後用
form = alipayClient.pageExecute(alipayRequest).getBody()
獲取提交到前端的表單,用response.getWriter().write(form)
輸出。
-
-
支付寶支付相關的配置類:
-
配置類:
@Component @ConfigurationProperties(prefix = "alipay") public class AlipayConfig { String charset = "UTF-8"; String appId; String appPrivateKey; String alipayPublicKey; String serverUrl = "https://openapi.alipaydev.com/gateway.do"; String baseUrl; }
-
配置檔案:
alipay: app-id: alipay-public-key: app-private-key: base-url:
-
可以先用這種方式測試一下。
-
-
支付寶針對官方的老SDK提供了實現的Demo:
- demo中,ap.js提供了跳轉邏輯,pay.htm是提供了跳轉頁面,另外兩個是示例。使用需要注意的是要在htm檔案中正確配置ap.js的路徑。
- 以get方式為例,最終訪問的是demo_get.htm下的確認支付的a標籤中的href路徑。如果使用get方式,需要將後端的請求資料動態傳入,可以使用freemaker模板引擎。
-
freemaker模板引擎:
-
maven依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency>
-
前端使用:
- 在template目錄下建立字尾為.ftl的檔案。
- 使用
${鍵名}
的方式獲取後端傳入的資料,對於物件使用${鍵名.屬性名}
。
-
後端資料:
- 返回一個ModelAndView物件,設定viewName為.ftl檔名。
- 設定model為一個map形式的資料。
-
-
實現微信呼叫支付寶支付:
-
主要就是要傳給前端一個拼接好的url。
-
alipayClient.pageExecute(alipayRequest).getBody()
返回的String物件是一個form表單,寫到前端是用post方式。- 而沒有找到單獨的獲取url等的方法,只好用擷取form字串的方法。(其實用post的demo可能好一點?)
- 在form表單中,只有biz_content屬性是在表單屬性,其他都被拼接在了action屬性中。
-
必須呼叫
alipayClient.pageExecute(alipayRequest)
方法的原因是,需要根據支付寶公鑰和應用私鑰獲得簽名sign屬性,當然也可以自行實現。而且經過這個方法後,其中的屬性都被UrlEncode了(除了biz_content)。 -
所以解決辦法是對form字串進行擷取(其實不是一個很好的方法),然後拼接UrlEncode後的biz_content屬性。
-
測試控制器:
@Controller @RequestMapping("/pay") public class PayController { @Autowired private AlipayConfig alipayConfig; @GetMapping("/create") public ModelAndView create(@RequestParam("orderId") String orderId, @RequestParam("returnUrl") String returnUrl, HttpServletResponse response){ String CHARSET = alipayConfig.getCharset(); String APP_ID = alipayConfig.getAppId(); String APP_PRIVATE_KEY = alipayConfig.getAppPrivateKey(); String ALIPAY_PUBLIC_KEY = alipayConfig.getAlipayPublicKey(); AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig.getServerUrl(), APP_ID, APP_PRIVATE_KEY, "json", CHARSET, ALIPAY_PUBLIC_KEY, "RSA2"); //獲得初始化的AlipayClient AlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//建立API對應的request String baseUrl = alipayConfig.getBaseUrl(); alipayRequest.setReturnUrl(baseUrl+"/pay/returnUrl"); alipayRequest.setNotifyUrl(baseUrl+"/pay/notifyUrl");//在公共引數中設定回跳和通知地址 alipayRequest.setBizContent("{" + " \"out_trade_no\":\"201603200756150101002\"," + " \"total_amount\":\"88.88\"," + " \"subject\":\"Iphone6 84G\"," + " \"product_code\":\"QUICK_WAP_PAY\"" + " }"); String form = ""; try { form = alipayClient.pageExecute(alipayRequest).getBody(); } catch (AlipayApiException e) { e.printStackTrace(); } String oriUrl = StringUtils.substringBetween(form,alipayConfig.getServerUrl(),"\">"); String biz_content = ""; try { biz_content = URLEncoder.encode(alipayRequest.getBizContent(),"utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } String toUrl = oriUrl + "&biz_content=" + biz_content; toUrl = alipayConfig.getServerUrl() + toUrl; Map<String,String> map = new HashMap<>(); map.put("toUrl",toUrl); return new ModelAndView("confirm_order",map); } }
-
前端模板頁面confirm_order.ftl,其實就是把demo_get.htm中a標籤中具體的url變成了
${toUrl}
。
-
-
實現效果:
-
實現具體業務:
-
根據傳入的orderId查詢訂單詳情,獲取相關資訊並且傳到前端。
-
控制器:
@Controller @RequestMapping("/pay") public class PayController { private Logger logger = LoggerFactory.getLogger(PayController.class); @Autowired private OrderMasterService masterService; @Autowired private PayService payService; @GetMapping("/create") public ModelAndView create(@RequestParam("orderId") String orderId, @RequestParam("returnUrl") String returnUrl) { OrderDTO orderDTO = masterService.findOne(orderId); if (orderDTO == null) { logger.error("【支付出錯】訂單不存在,orderId={}",orderId); throw new SellException(ResultEnum.ORDER_NOT_EXIST); } //拼接訂單中的商品名 StringBuilder subject = new StringBuilder(); List<OrderDetail> orderDetailList = orderDTO.getOrderDetailList(); for (OrderDetail orderDetail : orderDetailList) { subject.append(orderDetail.getProductName()).append(" "); } Map<String, String> returnMap = payService.create(orderDTO); Map<String, String> map = new HashMap<>(); map.put("toUrl", returnMap.get("toUrl")); try { map.put("checkUrl", returnMap.get("checkUrl") + "&returnUrl=" + URLEncoder.encode(returnUrl,"utf-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } map.put("productName",subject.toString()); map.put("amount", orderDTO.getOrderAmount().toString()); map.put("buyerName",orderDTO.getBuyerName()); map.put("orderTime", orderDTO.getCreateTime().toString()); map.put("orderId", orderId); return new ModelAndView("confirm_order", map); } }
-
業務層:
@Service public class PayServiceImpl implements PayService { @Autowired private AlipayConfig alipayConfig; @Autowired private OrderMasterService masterService; private Logger logger = LoggerFactory.getLogger(PayServiceImpl.class); @Override public Map<String, String> create(OrderDTO orderDTO) { String subject = getProductNames(orderDTO); String CHARSET = alipayConfig.getCharset(); String APP_ID = alipayConfig.getAppId(); String APP_PRIVATE_KEY = alipayConfig.getAppPrivateKey(); String ALIPAY_PUBLIC_KEY = alipayConfig.getAlipayPublicKey(); AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig.getServerUrl(), APP_ID, APP_PRIVATE_KEY, "json", CHARSET, ALIPAY_PUBLIC_KEY, "RSA2"); //獲得初始化的AlipayClient AlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//建立API對應的request String baseUrl = alipayConfig.getBaseUrl(); alipayRequest.setReturnUrl(baseUrl + "/pay/backwechat"); alipayRequest.setNotifyUrl(baseUrl + "/pay/notifyUrl");//在公共引數中設定回跳和通知地址 alipayRequest.setBizContent("{" + " \"out_trade_no\":\"" + orderDTO.getOrderId() + "\"," + " \"total_amount\":\"" + orderDTO.getOrderAmount() + "\"," + " \"subject\":\"" + subject + "\"," + " \"product_code\":\"QUICK_WAP_PAY\"" + " }"); String form = ""; try { form = alipayClient.pageExecute(alipayRequest).getBody(); } catch (AlipayApiException e) { e.printStackTrace(); } String oriUrl = StringUtils.substringBetween(form, alipayConfig.getServerUrl(), "\">"); String biz_content = ""; try { biz_content = URLEncoder.encode(alipayRequest.getBizContent(), "utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } String toUrl = oriUrl + "&biz_content=" + biz_content; toUrl = alipayConfig.getServerUrl() + toUrl; String checkUrl = baseUrl + "/pay/checkpay?orderId=" + orderDTO.getOrderId(); Map<String, String> map = new HashMap<>(); map.put("toUrl",toUrl); map.put("checkUrl",checkUrl); return map; } private String getProductNames(OrderDTO orderDTO) { StringBuilder subject = new StringBuilder(); List<OrderDetail> orderDetailList = orderDTO.getOrderDetailList(); for (OrderDetail orderDetail : orderDetailList) { subject.append(orderDetail.getProductName()).append(" "); } return subject.toString(); } }
-
-
非同步通知:
-
驗證簽名。
-
獲取返回的支付狀態。
-
校驗支付金額。
-
全部校驗通過後,通知支付平臺停止非同步通知。
-
具體見支付寶非同步回撥文件。
-
控制器:
@PostMapping("/notifyUrl") public void notifyUrl(HttpServletRequest request, HttpServletResponse response) { boolean signVerified = payService.notifyUrl(request, response); if (signVerified) { masterService.paid(masterService.findOne(request.getParameter("out_trade_no"))); } logger.info("【非同步回撥】回撥結果{},orderId={}",signVerified,request.getParameter("out_trade_no")); }
-
業務層:
@Override public boolean notifyUrl(HttpServletRequest request, HttpServletResponse response) { response.setContentType("text/html;charset=utf-8"); Map<String, String> paramsMap = new HashMap<>(); //將非同步通知中收到的所有引數都存放到map中 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] + ","; } paramsMap.put(name, valueStr); } boolean signVerified = false; //呼叫SDK驗證簽名 try { signVerified = AlipaySignature.rsaCheckV1(paramsMap, alipayConfig.getAlipayPublicKey(), alipayConfig.getCharset(), alipayConfig.getSign_type()); } catch (AlipayApiException e) { e.printStackTrace(); } boolean check = checkBotifyRequest(paramsMap); try { PrintWriter writer = response.getWriter(); check = checkBotifyRequest(paramsMap); if(signVerified && check){ writer.write("success"); logger.info("【回撥支付成功】,orderId={}",request.getParameter("out_trade_no")); }else{ writer.write("failure"); logger.info("【回撥支付失敗】,orderId={}",request.getParameter("out_trade_no")); } response.getWriter().close(); } catch (IOException e) { e.printStackTrace(); } return signVerified && check; }
-
-
還要考慮的一點是從外部瀏覽器支付完成後如何跳轉回微信:
-
首先,直接用同步回撥returnUrl(固定為/backwechat)回到微信:
-
控制器:
@GetMapping("/backwechat") public ModelAndView backwechat(){ return new ModelAndView("jumpback"); }
-
前端:
<script> window.onload =function () { window.location = "weixin://" } </script>
-
這樣回到的就是跳轉之前的微信介面,但是現在的問題是如何進入下一步。
-
-
改造原有的跳轉頁面,增加一個支付完成的按鈕。
-
新增在pay.htm中:
<div class="wrapper buy-wrapper"> <a id="checkpay" class="J-btn-submit btn mj-submit btn-strong btn-larger btn-block">支付完成</a> </div>
var checkUrl = getQueryString(location.href, "checkUrl"); var returnUrl = getQueryString(location.href, "returnUrl"); document.querySelector("#checkpay").href = checkUrl + "&returnUrl=" + returnUrl;
-
為了能獲取checkUrl,需要對之前的前端檔案進行一些改造:
-
confir_order.ftl:
<input id="orderId" type="hidden" value=${checkUrl} />
var checkUrl = document.getElementById("orderId").value; _AP.pay(e.target.href, checkUrl);
-
ap.js:
b.pay = function(d, checkUrl) { var c = encodeURIComponent(a.encode(d)); location.href = "pay.htm?goto=" + c + "&checkUrl=" + checkUrl; };
-
-
後端的處理:
-
PayServiceImpl中拼接checkUrl,同時使用一個map返回toUrl和checkUrl:
String checkUrl = baseUrl + "/pay/checkpay?orderId=" + orderDTO.getOrderId();
-
PayController中放入map中:
map.put("checkUrl", returnMap.get("checkUrl"));
-
-
-
最後的結果:
- 如果支付前直接點支付完成,會查詢訂單是否完成。
- 支付完成後在瀏覽器中點選完成付款,會跳轉到微信中,然後點選支付完成會跳轉到最終頁面。
-
-
接入前端除錯:
-
首先更改前端專案中的/config/index.js中:
sellUrl: 'http://sell.com', openidUrl: 'http://5w3ab3.natappfree.cc/sell/wechat/authorize', wechatPayUrl: 'http://5w3ab3.natappfree.cc/sell/pay/create'
-
重新部署前端。
-
-
退款:
-
使用請求
AlipayTradeRefundRequest
,傳入orderId和退款金額。 -
業務層:
@Override public void refund(String orderId) { String CHARSET = alipayConfig.getCharset(); String APP_ID = alipayConfig.getAppId(); String APP_PRIVATE_KEY = alipayConfig.getAppPrivateKey(); String ALIPAY_PUBLIC_KEY = alipayConfig.getAlipayPublicKey(); AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig.getServerUrl(), APP_ID, APP_PRIVATE_KEY, "json", CHARSET, ALIPAY_PUBLIC_KEY, "RSA2"); //獲得初始化的AlipayClient AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();//建立API對應的request類 OrderDTO orderDTO = masterService.findOne(orderId); request.setBizContent("{" + "\"out_trade_no\":\"" + orderId + "\"," + "\"out_request_no\":\"1000001\"," + "\"refund_amount\":\"0" + orderDTO.getOrderAmount() + "\"}"); //設定業務引數 AlipayTradeRefundResponse response = null;//通過alipayClient呼叫API,獲得對應的response類 try { response = alipayClient.execute(request); } catch (AlipayApiException e) { e.printStackTrace(); } System.out.print(response.getBody()); }
-
在orderMasterService的cancel取消訂單方法中最後被呼叫。
-
iwehdio的部落格園:https://www.cnblogs.com/iwehdio/