1.背景
實際開發中用到微信支付的概率非常大,
至於為什么這里不必要我多少......
微信支付大體需要對接的核心接口有
其實大部分支付都是這些,就像上一節我們講的支付寶支付一樣
這里以常用的H5支付為例,其他的都是差不多的....
值得注意的時候,微信支付最常用的就是H5支付和JSAPI支付,這兩者的主要區別在於
H5支付必須在非微信瀏覽器打開;
JSAPI支付只能在微信的瀏覽器打開;
微信支付官方文檔:https://pay.weixin.qq.com/wiki/doc/api/index.html
1.統一下單
2.查詢訂單
3.支付結果通知
4.申請退款
5.查詢退款
6.下載對賬單
2.需要准備的環境
微信支付目前還沒有沙箱測試環境,要開開發支付功能必須要有企業資質申請公眾號和商戶號才可以
主要的是:
公眾號appid
商戶號
商戶號的apiSercet密碼
商戶號的證書
商戶號上綁定好的回調域名
3.開發步驟
3.1.統一下單
代碼如下:

package com.ldp.user.service.impl; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpUtil; import com.alibaba.fastjson.JSON; import com.ldp.user.common.constant.PayConstant; import com.ldp.user.common.constant.PayEnum; import com.ldp.user.common.constant.TestData; import com.ldp.user.common.constant.WeChatConstant; import com.ldp.user.common.exception.ParamException; import com.ldp.user.common.exception.WeChatException; import com.ldp.user.common.util.wechat.WXPayUtil; import com.ldp.user.entity.bo.PayBO; import com.ldp.user.entity.vo.PayVO; import com.ldp.user.service.IPayService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * @Copyright (C) XXXXXX科技股份技有限公司 * @Author: lidongping * @Date: 2021-01-07 18:47 * @Description: */ @Service @Slf4j public class WeChatPayService implements IPayService { @Override public String getPayType() { return PayConstant.WECHAT; } @Override public PayVO getPayInfo(PayBO payBO) throws Exception { // 根據訂單號查詢訂單數據 和 支付賬號數據 // 這里測試 直接將數據寫在代碼里 payBO.setAppId(TestData.WX_APP_ID) .setMerchantId(TestData.WX_MERCHANT_ID) .setApiSecretKey(TestData.WX_SECRET) .setPayTitle("測試支付") .setPayMoney(0.01) .setProductNo("P001") .setAsynchronousNotifyUrl("http://lidongping.free.idcfengye.com/api/async/wechat/payment") .setOpenId("ozwLvwah3xtA93csvZTWr0wON_IU"); // 以下是封裝微信支付數據 Map dic = new HashMap<String, String>(); dic.put("appid", payBO.getAppId()); dic.put("mch_id", payBO.getMerchantId()); String nonceStr = RandomUtil.randomString(30); dic.put("nonce_str", nonceStr); dic.put("body", payBO.getPayTitle()); dic.put("out_trade_no", payBO.getOrderNo()); String amount = NumberUtil.roundStr(payBO.getPayMoney() * 100, 0); dic.put("total_fee", amount); dic.put("spbill_create_ip", payBO.getUserIp()); dic.put("notify_url", payBO.getAsynchronousNotifyUrl()); // 生成的支付信息一小時內有效 dic.put("time_expire", DateUtil.format(DateUtil.offsetHour(new Date(), 1), "yyyyMMddHHmmss")); // 根據類型請求支付 PayEnum payType = PayEnum.valueOf(payBO.getPayType()); switch (payType) { case WECHAT_QR: dic.put("trade_type", "NATIVE"); dic.put("product_id", payBO.getProductNo()); break; case WECHAT_JSAPI: dic.put("trade_type", "JSAPI"); if (StrUtil.isEmpty(payBO.getOpenId())) { throw new ParamException("openid 為空"); } dic.put("openid", payBO.getOpenId()); break; case WECHAT_H5: dic.put("trade_type", "MWEB"); break; default: throw new ParamException("沒有對應的支付方式"); } log.info("簽名前:" + JSON.toJSONString(dic)); dic.put("sign", WXPayUtil.generateSignature(dic, payBO.getApiSecretKey())); String dataXml = WXPayUtil.mapToXml(dic); log.info("微信請求下單url:" + WeChatConstant.PAY_URL); log.info("微信請求下單參數:" + dataXml); String resp = HttpUtil.post(WeChatConstant.PAY_URL, dataXml, 60 * 1000); log.info("微信請求下單返回:" + resp); Map<String, String> resMap = WXPayUtil.xmlToMap(resp); // 請求失敗 if (!resMap.get("return_code").equalsIgnoreCase(WeChatConstant.SUCCESS)) { throw new WeChatException(resMap.get("return_msg")); } // 下單失敗 if (!resMap.get("result_code").equalsIgnoreCase(WeChatConstant.SUCCESS)) { throw new WeChatException(resMap.get("err_code_des")); } // 校驗支付返回結果簽名 if (!checkSign(resMap, payBO.getApiSecretKey())) { throw new WeChatException("驗證簽名失敗"); } // 處理返回結果 PayVO payVO = new PayVO(); payVO.setOrderNo(payBO.getOrderNo()); switch (payType) { case WECHAT_QR: payVO.setPayInfo(resMap.get("code_url")); break; case WECHAT_JSAPI: Map jsapi = new HashMap<String, String>(); jsapi.put("appId", payBO.getAppId()); // 當前時間秒 jsapi.put("timeStamp", System.currentTimeMillis() / 1000 + ""); jsapi.put("nonceStr", nonceStr); jsapi.put("package", "prepay_id=" + resMap.get("prepay_id")); jsapi.put("signType", "MD5"); jsapi.put("paySign", WXPayUtil.generateSignature(jsapi, payBO.getApiSecretKey())); payVO.setPayInfo(jsapi); break; default: payVO.setPayInfo(resMap.get("mweb_url")); } return payVO; } /** * 微信響應數據簽名檢查 * * @param map * @param key * @return * @throws Exception */ private static boolean checkSign(Map<String, String> map, String key) throws Exception { String sign = map.get("sign"); map.remove("sign"); String orign = WXPayUtil.generateSignature(map, key); return orign.equalsIgnoreCase(sign); } }
3.2.異步通知
代碼如下:

package com.ldp.user.controller; import cn.hutool.extra.servlet.ServletUtil; import com.alibaba.fastjson.JSON; import com.ldp.user.common.constant.TestData; import com.ldp.user.common.constant.WeChatConstant; import com.ldp.user.common.util.RedisUtil; import com.ldp.user.common.util.wechat.WXPayUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import java.util.Map; /** * @author 姿勢帝-博客園 * @address https://www.cnblogs.com/newAndHui/ * @WeChat 851298348 * @create 01/10 11:18 * @description */ @RestController @Slf4j public class WeChatController { /** * 注意: * 1.這里是只支付寶微信xml的參數接收,微信最近也提供了json的方式,本質上都是一樣的 * 2.xml參數回調文檔:https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=9_7&index=8 * * @param request * @return * @throws Exception */ @PostMapping("/async/wechat/payment") public String notifyPaymentResult(HttpServletRequest request) throws Exception { log.info("wechat異步回調...."); String paramXml = ServletUtil.getBody(request); Map<String, String> paramsMap = WXPayUtil.xmlToMap(paramXml); //解析參數 String returnCode = paramsMap.get("return_code"); String tradeStatus = paramsMap.get("result_code"); String appid = paramsMap.get("appid"); String totalFee = paramsMap.get("total_fee"); String orderNo = paramsMap.get("out_trade_no"); if (!WeChatConstant.SUCCESS.equals(returnCode)) { log.error("回調狀態錯誤:returnCode={}", returnCode); return WeChatConstant.RETURN_FAIL; } // TODO 這里實際開發時根據訂單號獲取apiSecret秘鑰 String apiSecret = TestData.WX_SECRET; // 驗簽 if (!WXPayUtil.isSignatureValid(paramsMap, apiSecret)) { log.error("驗簽失敗"); return WeChatConstant.RETURN_FAIL; } if (!WeChatConstant.SUCCESS.equals(tradeStatus)) { log.error("支付狀態失敗:tradeStatus={}", tradeStatus); return WeChatConstant.RETURN_FAIL; } // TODO 更新數據庫訂單數據和緩存 ... 這里略 // 這里只是模擬方放入緩存,便於前端查詢訂單 60 * 60 * 2L 表示緩存2小時 RedisUtil.set(orderNo, JSON.toJSON(paramsMap), 60 * 60 * 2L); log.info("異步處理成功............."); return WeChatConstant.RETURN_SUCCESS; } }
3.3.測試
獲取微信支付信息

@Test void getPayInfo() { String url = urlLocal + "/userOrder/payInfo"; System.out.println("請求地址:" + url); HttpRequest request = HttpUtil.createRequest(Method.GET, url); Map<String, Object> map = new HashMap<>(); map.put("orderNo", "NO" + System.currentTimeMillis()); // 微信支付 wechat 支付寶 alipay map.put("payCategory", "wechat"); // 201 jsapi支付 , 202 微信H5 101支付寶H5 map.put("payType", "202"); request.form(map); System.out.println("請求參數:" + map); request.header("X-Real_IP", "192.168.5.195"); request.setConnectionTimeout(60 * 1000); String respone = request.execute().body(); System.out.println("響應結果:" + respone); }
測試結果
請求地址:http://localhost:8080/api/userOrder/payInfo 請求參數:{payCategory=wechat, orderNo=NO1610250837438, payType=202} 響應結果:{"message":"success","code":100,"data":{"orderNo":"NO1610250837438","payInfo":"https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx10115509234051b333adbe01d032b80000&package=2821154835"}}
從響應的結果看,我們已經拿到了payInfo,也就是跳轉到微信的url
正常情況下,只要在非微信瀏覽器打開就可以了,但是微信那邊對請求來源的域名做了校驗,我們這里測試不是很方便,就不繼續演示下去了
因為實際開發中只要走到這一步,對於開發人員來說代碼就已經合格了,如果只會支付不了,那多半是微信商戶號上的配置有問題....
學習我們就到這里,如果在實際開發中有問題可以咨詢我,共同交流技術!
4.總結
1.這里只是給大家演示了統一下單和支付回調的代碼,也是微信支付的核心代碼,相信只要這調通了,其他的接口類似!
2.為了更好讓大家快速掌握微信支付,這篇博客已經做了配套的視頻講解,大家可以結合視頻講解學習,或者單獨問我!