1. 程式人生 > 實用技巧 >第三方通過微信APP支付流程

第三方通過微信APP支付流程

本章文獻基本都來源於微信支付平臺,詳情請看微信官方文件:APP支付

系統互動圖

文件位置:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_3APP支付-業務流程

根據文件內容,服務端只要做好獲取 prepay_id 和 sign 傳送給客戶端,並做好回撥接收處理就行

服務端demo

APP支付文件裡面的demo,主要是供客戶端使用的,對後臺來說,基本沒什麼用。

不過,依舊有我們後端可以直接拿來使用的demo:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=11_1

這個demo主要用於付款碼/掃碼/H5支付使用,但裡面提供了很多方便的工具類。

這裡我們引用demo裡面的工具類並繼承WXPayConfig和實現IWXPayDomain的抽象介面。


  1. public class WxPayConfigImpl extends WXPayConfig{

  2. // 設定應用Id

  3. public static String appid = "2019102168481752";

  4. // 設定商戶號

  5. public static String mch_id = "1230000109";

  6. // 設定裝置號(終端裝置號(門店號或收銀裝置ID),預設請傳"WEB",非必需)

  7. public static String device_info = "WEB";

  8. // 設定字符集

  9. public static String key = "192006250b4c09247ec02edce69f6a2d";

  10. private static WxPayConfigImpl INSTANCE;

  11. public static WxPayConfigImpl getInstance() throws Exception {

  12. if (INSTANCE == null) {

  13. synchronized (WxPayConfigImpl.class) {

  14. if (INSTANCE == null) {

  15. INSTANCE = new WxPayConfigImpl();

  16. }

  17. }

  18. }

  19. return INSTANCE;

  20. }

  21. @Override

  22. public String getAppID() {

  23. return appid;

  24. }

  25. @Override

  26. public String getMchID() {

  27. return mch_id;

  28. }

  29. @Override

  30. public String getKey() {

  31. return key;

  32. }

  33. @Override

  34. public InputStream getCertStream() {

  35. //這個可以看我另一篇“支付寶:APP支付介面2.0(alipay.trade.app.pay)”

  36. //裡的“使用demo”步驟,有講解,如果是個springboot構成的jar,如何設定證書路徑

  37. //文章連結:https://blog.csdn.net/u014799292/article/details/102680149

  38. String fileUrl = "證書路徑";

  39. InputStream certStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(fileUrl);

  40. byte[] certData = null;

  41. try {

  42. certData = IOUtils.toByteArray(certStream);

  43. } catch (IOException e) {

  44. // TODO Auto-generated catch block

  45. e.printStackTrace();

  46. }finally{

  47. if(certStream != null){

  48. try {

  49. certStream.close();

  50. } catch (IOException e) {

  51. // TODO Auto-generated catch block

  52. e.printStackTrace();

  53. }

  54. }

  55. }

  56. return new ByteArrayInputStream(certData);

  57. }

  58. @Override

  59. public IWXPayDomain getWXPayDomain() {

  60. return WxPayDomainImpl.instance();

  61. }

  62. }


  1. public class WxPayDomainImpl implements IWXPayDomain {

  2. private final int MIN_SWITCH_PRIMARY_MSEC = 3 * 60 * 1000; //3 minutes

  3. private long switchToAlternateDomainTime = 0;

  4. private Map<String, DomainStatics> domainData = new HashMap<String, DomainStatics>();

  5. private WxPayDomainImpl(){

  6. }

  7. private static class WxpayDomainHolder{

  8. private static IWXPayDomain holder = new WxPayDomainImpl();

  9. }

  10. public static IWXPayDomain instance(){

  11. return WxpayDomainHolder.holder;

  12. }

  13. @Override

  14. public void report(String domain, long elapsedTimeMillis, Exception ex) {

  15. DomainStatics info = domainData.get(domain);

  16. if(info == null){

  17. info = new DomainStatics(domain);

  18. domainData.put(domain, info);

  19. }

  20. if(ex == null){ //success

  21. if(info.succCount >= 2){ //continue succ, clear error count

  22. info.connectTimeoutCount = info.dnsErrorCount = info.otherErrorCount = 0;

  23. }else{

  24. ++info.succCount;

  25. }

  26. }else if(ex instanceof ConnectTimeoutException){

  27. info.succCount = info.dnsErrorCount = 0;

  28. ++info.connectTimeoutCount;

  29. }else if(ex instanceof UnknownHostException){

  30. info.succCount = 0;

  31. ++info.dnsErrorCount;

  32. }else{

  33. info.succCount = 0;

  34. ++info.otherErrorCount;

  35. }

  36. }

  37. @Override

  38. public DomainInfo getDomain(WXPayConfig config) {

  39. DomainStatics primaryDomain = domainData.get(WXPayConstants.DOMAIN_API);

  40. if(primaryDomain == null ||

  41. primaryDomain.isGood()) {

  42. return new DomainInfo(WXPayConstants.DOMAIN_API, true);

  43. }

  44. long now = System.currentTimeMillis();

  45. if(switchToAlternateDomainTime == 0){ //first switch

  46. switchToAlternateDomainTime = now;

  47. return new DomainInfo(WXPayConstants.DOMAIN_API2, false);

  48. }else if(now - switchToAlternateDomainTime < MIN_SWITCH_PRIMARY_MSEC){

  49. DomainStatics alternateDomain = domainData.get(WXPayConstants.DOMAIN_API2);

  50. if(alternateDomain == null ||

  51. alternateDomain.isGood() ||

  52. alternateDomain.badCount() < primaryDomain.badCount()){

  53. return new DomainInfo(WXPayConstants.DOMAIN_API2, false);

  54. }else{

  55. return new DomainInfo(WXPayConstants.DOMAIN_API, true);

  56. }

  57. }else{ //force switch back

  58. switchToAlternateDomainTime = 0;

  59. primaryDomain.resetCount();

  60. DomainStatics alternateDomain = domainData.get(WXPayConstants.DOMAIN_API2);

  61. if(alternateDomain != null)

  62. alternateDomain.resetCount();

  63. return new DomainInfo(WXPayConstants.DOMAIN_API, true);

  64. }

  65. }

  66. static class DomainStatics {

  67. final String domain;

  68. int succCount = 0;

  69. int connectTimeoutCount = 0;

  70. int dnsErrorCount =0;

  71. int otherErrorCount = 0;

  72. DomainStatics(String domain) {

  73. this.domain = domain;

  74. }

  75. void resetCount(){

  76. succCount = connectTimeoutCount = dnsErrorCount = otherErrorCount = 0;

  77. }

  78. boolean isGood(){ return connectTimeoutCount <= 2 && dnsErrorCount <= 2; }

  79. int badCount(){

  80. return connectTimeoutCount + dnsErrorCount * 5 + otherErrorCount / 4;

  81. }

  82. }

  83. }

給WXPayUtil再新增幾個時間工具方法(看自己需求定製),專案會在最後附連結。

appid & 商戶號位置

登入商戶平臺-產品中心-賬號關聯(AppID繫結),進入授權申請頁面;

金鑰key位置

登入商戶平臺-->>賬戶中心-->>API安全-->>設定API金鑰(32位,下面給出生成方式,然後複製貼上到祕鑰位置,記得保留,之後無法檢視,只能再次生成,測試環境隨便改,正式服記得最好固定一次,修改可能會引起服務端資料錯誤)


  1. private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

  2. private static final Random RANDOM = new SecureRandom();

  3. /**

  4. * 獲取隨機字串 Nonce Str

  5. *

  6. * @return String 隨機字串

  7. */

  8. public static String generateNonceStr() {

  9. char[] nonceChars = new char[32];

  10. for (int index = 0; index < nonceChars.length; ++index) {

  11. nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));

  12. }

  13. return new String(nonceChars);

  14. }

再來寫一個請求類,處理微信訂單(訂單--統一下單/調起支付介面/支付結果通知/查詢訂單/關閉訂單)


  1. /**

  2. * 訂單--統一下單/調起支付介面/退款/結果通知/查詢訂單/關閉訂單

  3. * 借鑑:https://blog.csdn.net/asd54090/article/details/81028323

  4. */

  5. public class WxAppPayRequest {

  6. private static final Logger logger = LoggerFactory.getLogger(WxAppPayRequest.class);

  7. private WxPayConfigImpl config;

  8. private WXPay wxpay;

  9. /**

  10. * 微信支付請求

  11. */

  12. public WxAppPayRequest() {

  13. try {

  14. config = WxPayConfigImpl.getInstance();

  15. wxpay = new WXPay(config);

  16. } catch (Exception e) {

  17. e.printStackTrace();

  18. logger.error("微信配置初始化錯誤", e);

  19. }

  20. }

  21. /**

  22. * APP支付訂單請求

  23. *

  24. * @param body

  25. * 格式:APP名字-實際商品名稱,如:天天愛消除-遊戲充值

  26. * @param attach

  27. * 附加資料,在查詢API和支付通知中原樣返回

  28. * @param outTradeNo

  29. * 商戶訂單號

  30. * @param totalFee

  31. * 總金額

  32. * @param startTime

  33. * 訂單開始時間String格式: yyyy-MM-dd HH:mm:ss

  34. * @param expireMinute

  35. * 有效時間(分鐘)

  36. * @param notifyUrl

  37. * 微信支付非同步通知回撥地址

  38. * @return

  39. */

  40. private WxAppPayResponseCode getOrderSign(String body, String attach, String outTradeNo, BigDecimal totalFee,

  41. String startTime, int expireMinute, String notifyUrl) {

  42. // 準備好請求引數

  43. Map<String, String> map = new HashMap<String, String>();

  44. map.put("device_info", WxPayConfigImpl.device_info);

  45. map.put("body", body);

  46. if (attach != null && !attach.isEmpty()) {

  47. map.put("attach", attach);

  48. }

  49. map.put("out_trade_no", outTradeNo);

  50. map.put("total_fee", totalFee.toString());

  51. map.put("spbill_create_ip", WXPayUtil.getLocalAddress());

  52. map.put("time_start", WXPayUtil.getFormatTime(startTime));

  53. String endTime = WXPayUtil.getNSecondTime(startTime, expireMinute);

  54. map.put("time_expire", WXPayUtil.getFormatTime(endTime));

  55. map.put("notify_url", notifyUrl);

  56. map.put("trade_type", "APP");

  57. // 生成帶sign的xml字串

  58. Map<String, String> unifiedOrderMap = null;

  59. try {

  60. unifiedOrderMap = wxpay.unifiedOrder(map);

  61. if (unifiedOrderMap == null || (unifiedOrderMap != null

  62. && WxAppPayResponseCode.FAIL.code().equals(unifiedOrderMap.get("return_code")))) {

  63. String errorMsg = "呼叫微信“統一下單”獲取prepayid 失敗...";

  64. logger.info("getOrderSign --unifiedOrder: 呼叫微信“統一下單”獲取prepayid 失敗.");

  65. logger.info("getOrderSign --unifiedOrder: 請求引數:" + map.toString());

  66. logger.info("getOrderSign --unifiedOrder: 返回Map:" + unifiedOrderMap);

  67. if (unifiedOrderMap != null) {

  68. errorMsg += " 異常資訊為:" + unifiedOrderMap.get("return_msg");

  69. }

  70. WxAppPayResponseCode error = WxAppPayResponseCode.ERROR;

  71. error.setAlias(errorMsg);

  72. return error;

  73. }

  74. } catch (Exception e) {

  75. // TODO Auto-generated catch block

  76. e.printStackTrace();

  77. logger.error("getOrderSign : 呼叫微信“統一下單”失敗 。", e);

  78. WxAppPayResponseCode error = WxAppPayResponseCode.ERROR;

  79. error.setAlias("呼叫微信“統一下單”失敗 。" + e.toString());

  80. return error;

  81. }

  82. // 呼叫微信請求成功,但響應失敗

  83. String resultCode = unifiedOrderMap.get("result_code");

  84. if (WxAppPayResponseCode.FAIL.code().equals(resultCode)) {

  85. WxAppPayResponseCode error = WxAppPayResponseCode.findCode(unifiedOrderMap.get("err_code"));

  86. return error;

  87. }

  88. return parseWXOrderResponse(unifiedOrderMap);

  89. }

  90. /**

  91. * 將map轉成客戶端訂單用的封裝體

  92. *

  93. * @param map

  94. * map

  95. * @return 使用者端用的封裝體

  96. */

  97. private WxAppPayResponseCode parseWXOrderResponse(Map<String, String> map) {

  98. WxOrderResponse response = new WxOrderResponse();

  99. response.setAppid(map.get("appid"));

  100. response.setPartnerid(map.get("partnerid"));

  101. response.setPrepayid(map.get("prepay_id"));

  102. response.setPack("Sign=WXPay");

  103. response.setNoncestr(map.get("noncestr"));

  104. String timestamp = WXPayUtil.getCurrentTimestamp() + "";

  105. response.setTimestamp(timestamp);

  106. // 前人踩坑,咱們乘涼

  107. // sgin(簽名),不是拿微信“統一下單”返回的sgin,而是自己再籤一次,返回給客戶端

  108. // 簽名的引數拿的不是“統一下單”,而是拿“調起支付介面”裡面的引數,這步API文件寫的是客戶端生成,不過咱們服務端就幫他做了

  109. // 注意:map的key不能是大寫

  110. Map<String, String> params = new HashMap<>();

  111. params.put("appid", map.get("appid"));

  112. params.put("partnerid", map.get("partnerid"));

  113. params.put("prepayid", map.get("prepay_id"));

  114. params.put("package", "Sign=WXPay");

  115. params.put("noncestr", map.get("nonce_str"));

  116. params.put("timestamp", timestamp);

  117. try {

  118. // 這個sign是移動端要請求微信服務端的,也是我們要儲存後面校驗的

  119. String sgin = WXPayUtil.generateSignature(params, config.getKey());

  120. response.setSign(sgin);

  121. } catch (Exception e) {

  122. e.printStackTrace();

  123. logger.error("parseWXOrderResponse : 訂單第二次供客戶端簽名信息失敗 。");

  124. logger.error("parseWXOrderResponse : 請求引數:" + params.toString());

  125. logger.error("parseWXOrderResponse : 返回錯誤資訊:", e);

  126. WxAppPayResponseCode errorData = WxAppPayResponseCode.ERROR;

  127. errorData.setAlias("呼叫支付介面生成簽名sign失敗!");

  128. return errorData;

  129. }

  130. WxAppPayResponseCode successData = WxAppPayResponseCode.SUCCESS;

  131. successData.setAlias(JSONObject.toJSONString(response));

  132. return successData;

  133. }

  134. /**

  135. * 查微信訂單

  136. *

  137. * @param outTradeNo

  138. * 訂單號

  139. */

  140. public WxAppPayResponseCode queryOrderByOutTradeNo(String outTradeNo) {

  141. logger.info("查詢微信支付訂單資訊,訂單號為:" + outTradeNo);

  142. HashMap<String, String> data = new HashMap<String, String>();

  143. data.put("out_trade_no", outTradeNo);

  144. try {

  145. Map<String, String> orderQueryMap = wxpay.orderQuery(data);

  146. return disposeReturnInfo(orderQueryMap);

  147. } catch (Exception e) {

  148. e.printStackTrace();

  149. logger.error("queryOrderByOutTradeNo : 查詢微信訂單支付資訊失敗 。訂單號:" + outTradeNo);

  150. logger.error("queryOrderByOutTradeNo : 返回錯誤資訊:", e);

  151. WxAppPayResponseCode errorData = WxAppPayResponseCode.ERROR;

  152. errorData.setAlias("呼叫查微信訂單支付資訊介面失敗!");

  153. return errorData;

  154. }

  155. }

  156. /**

  157. * 關閉訂單(剛剛生成的訂單不能立馬關閉,要間隔5分鐘,請自行做好判斷)

  158. *

  159. * @param outTradeNo

  160. * 訂單號

  161. * @return

  162. */

  163. public WxAppPayResponseCode closeOrder(String outTradeNo) {

  164. logger.info("關閉微信支付訂單資訊,訂單號為:" + outTradeNo);

  165. HashMap<String, String> data = new HashMap<>();

  166. data.put("out_trade_no", outTradeNo);

  167. try {

  168. Map<String, String> closeOrderMap = wxpay.closeOrder(data);

  169. return disposeReturnInfo(closeOrderMap);

  170. } catch (Exception e) {

  171. e.printStackTrace();

  172. logger.error("closeOrder : 微信關閉訂單失敗 。訂單號:" + outTradeNo);

  173. logger.error("closeOrder : 返回錯誤資訊:", e);

  174. WxAppPayResponseCode errorData = WxAppPayResponseCode.ERROR;

  175. errorData.setAlias("呼叫查微信訂單支付資訊介面失敗!");

  176. return errorData;

  177. }

  178. }

  179. /**

  180. * 微信退款申請

  181. *

  182. * @param outTradeNo

  183. * 商戶訂單號

  184. * @param amount

  185. * 金額

  186. * @param refund_desc

  187. * 退款原因(可空)

  188. * @param notifyUrl

  189. * 退款非同步通知連結

  190. *

  191. * @return 返回map(已做過簽名驗證),具體資料參見微信退款API

  192. */

  193. public WxAppPayResponseCode refundOrder(String outTradeNo, BigDecimal amount, String refundDesc, String notifyUrl)

  194. throws Exception {

  195. Map<String, String> data = new HashMap<String, String>();

  196. data.put("out_trade_no", outTradeNo);

  197. data.put("out_refund_no", outTradeNo);

  198. data.put("total_fee", amount + "");

  199. data.put("refund_fee", amount + "");

  200. data.put("refund_fee_type", "CNY");

  201. data.put("refund_desc", refundDesc);

  202. data.put("notifyUrl", notifyUrl);

  203. try {

  204. Map<String, String> refundOrderMap = wxpay.refund(data);

  205. return disposeReturnInfo(refundOrderMap);

  206. } catch (Exception e) {

  207. e.printStackTrace();

  208. logger.error("closeOrder : 微信退款申請失敗 。訂單號:" + outTradeNo);

  209. logger.error("closeOrder : 返回錯誤資訊:", e);

  210. WxAppPayResponseCode errorData = WxAppPayResponseCode.ERROR;

  211. errorData.setAlias("呼叫微信退款申請資訊介面失敗!");

  212. return errorData;

  213. }

  214. }

  215. /**

  216. * 查微信退款訂單 注:如果單個支付訂單部分退款次數超過20次請使用退款單號查詢

  217. *

  218. * @param outTradeNo

  219. * 訂單號

  220. */

  221. public WxAppPayResponseCode queryRefundOrderByOutTradeNo(String outTradeNo) {

  222. logger.info("查詢微信支付訂單資訊,訂單號為:" + outTradeNo);

  223. HashMap<String, String> data = new HashMap<String, String>();

  224. data.put("out_trade_no", outTradeNo);

  225. try {

  226. Map<String, String> refundQueryMap = wxpay.refundQuery(data);

  227. return disposeReturnInfo(refundQueryMap);

  228. } catch (Exception e) {

  229. e.printStackTrace();

  230. logger.error("queryRefundOrderByOutTradeNo : 查詢微信退款訂單資訊失敗 。訂單號:" + outTradeNo);

  231. logger.error("queryRefundOrderByOutTradeNo : 返回錯誤資訊:", e);

  232. WxAppPayResponseCode errorData = WxAppPayResponseCode.ERROR;

  233. errorData.setAlias("呼叫查微信退款訂單介面失敗!");

  234. return errorData;

  235. }

  236. }

  237. /**

  238. * 對介面接收成功後的返回進行處理

  239. *

  240. * @param resultMap

  241. * @return

  242. */

  243. private WxAppPayResponseCode disposeReturnInfo(Map<String, String> resultMap) {

  244. if (resultMap == null

  245. || (resultMap != null && WxAppPayResponseCode.FAIL.code().equals(resultMap.get("return_code")))) {

  246. WxAppPayResponseCode errorData = WxAppPayResponseCode.ERROR;

  247. errorData.setAlias("呼叫微信介面失敗!返回資料為 : " + resultMap);

  248. return errorData;

  249. }

  250. if (WxAppPayResponseCode.FAIL.code().equals(resultMap.get("result_code"))) {

  251. WxAppPayResponseCode errorData = WxAppPayResponseCode.findCode(resultMap.get("err_code"));

  252. return errorData;

  253. }

  254. WxAppPayResponseCode successData = WxAppPayResponseCode.SUCCESS;

  255. successData.setAlias(JSONObject.toJSONString(resultMap));

  256. return successData;

  257. }

  258. /**

  259. * 是否成功接收微信支付回撥 用於回覆微信,否則微信回預設為商戶後端沒有收到回撥

  260. *

  261. * @return

  262. */

  263. public String returnWXPayVerifyMsg() {

  264. return "<xml>\n" + "\n" + " <return_code><![CDATA[SUCCESS]]></return_code>\n"

  265. + " <return_msg><![CDATA[OK]]></return_msg>\n" + "</xml>";

  266. }

  267. public static void main(String[] args) {

  268. WxAppPayRequest pay = new WxAppPayRequest();

  269. WxAppPayResponseCode orderSign = pay.getOrderSign("APP-商品訂單支付", "data1;data2", "212458542512542542",

  270. new BigDecimal("100.12"), "2019-01-02 23:55:14", 10, "http://XXX");

  271. System.out.println(orderSign);

  272. }

  273. }

OK,基本搞定,構建成一個處理微信訂單功能的JAR。

專案已推送github:https://github.com/leopardF/wxpay

最後,再次附上本次借鑑的文章:

微信APP支付-API列表

微信支付SDK與DEMO下載

微信APP支付-JAVA

如果問題,請提醒修正。