1. 程式人生 > >微信支付-App支付服務端詳解

微信支付-App支付服務端詳解

微信App支付服務端詳解

引言

主要實現app支付統一下單、非同步通知、調起支付介面、支付訂單查詢、申請退款、查詢退款功能;封裝了https對發起退款的證書校驗、簽名、xml解析等。

支付流程

具體支付流程參考“微信APP”文件,文件地址

APP支付:APP端點選下單—-服務端生成訂單,並調起“統一下單”,返回app支付所需引數—–APP端“調起支付介面“,發起支付—-微信伺服器端呼叫服務端回撥地址—–服務端按照“支付結果通知”,處理支付結果

app查詢:調起“查詢訂單”

APP退款:發起退款請求,呼叫“申請退款”,發起退款,需雙向證書驗證

APP退款查詢:調起“查詢退款”

APP支付Api列表

支付程式碼實現

程式碼實現簽名、證書校驗、http和https封裝等,專案結構如下:

專案結構

支付程式碼

包含支付、支付查詢、非同步通知、退款申請、退款查詢

package org.andy.wxpay.controller;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet
.http.HttpServletResponse; import org.andy.wxpay.model.JsonResult; import org.andy.wxpay.model.ResponseData; import org.andy.wxpay.utils.CollectionUtil; import org.andy.wxpay.utils.ConfigUtil; import org.andy.wxpay.utils.FileUtil; import org.andy.wxpay.utils.HttpUtils; import org.andy.wxpay.utils.PayUtil
; import org.andy.wxpay.utils.SerializerFeatureUtil; import org.andy.wxpay.utils.StringUtil; import org.andy.wxpay.utils.WebUtil; import org.andy.wxpay.utils.XmlUtil; import org.apache.log4j.Logger; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import com.alibaba.fastjson.JSON; /** * 建立時間:2016年11月2日 下午4:16:32 * * @author andy * @version 2.2 */ @Controller @RequestMapping("/order") public class PayController { private static final Logger LOG = Logger.getLogger(PayController.class); private static final String ORDER_PAY = "https://api.mch.weixin.qq.com/pay/unifiedorder"; // 統一下單 private static final String ORDER_PAY_QUERY = "https://api.mch.weixin.qq.com/pay/orderquery"; // 支付訂單查詢 private static final String ORDER_REFUND = "https://api.mch.weixin.qq.com/secapi/pay/refund"; // 申請退款 private static final String ORDER_REFUND_QUERY = "https://api.mch.weixin.qq.com/pay/refundquery"; // 申請退款 private static final String APP_ID = ConfigUtil.getProperty("wx.appid"); private static final String MCH_ID = ConfigUtil.getProperty("wx.mchid"); private static final String API_SECRET = ConfigUtil.getProperty("wx.api.secret"); /** * 支付下訂單 * * @param request * @param response * @param cashnum * 支付金額 * @param mercid * 商品id * @param callback */ @RequestMapping(value = "/pay", method = RequestMethod.POST) public void orderPay(HttpServletRequest request, HttpServletResponse response, @RequestParam(required = false, defaultValue = "0") Double cashnum, String mercid, String callback) { LOG.info("[/order/pay]"); if (!"001".equals(mercid)) { WebUtil.response(response, WebUtil.packJsonp(callback, JSON .toJSONString(new JsonResult(-1, "商品不存在", new ResponseData()), SerializerFeatureUtil.FEATURES))); } Map<String, String> restmap = null; boolean flag = true; // 是否訂單建立成功 try { String total_fee = BigDecimal.valueOf(cashnum).multiply(BigDecimal.valueOf(100)) .setScale(0, BigDecimal.ROUND_HALF_UP).toString(); Map<String, String> parm = new HashMap<String, String>(); parm.put("appid", APP_ID); parm.put("mch_id", MCH_ID); parm.put("device_info", "WEB"); parm.put("nonce_str", PayUtil.getNonceStr()); parm.put("body", "測試付費"); parm.put("attach", "Andy"); parm.put("out_trade_no", PayUtil.getTradeNo()); parm.put("total_fee", total_fee); parm.put("spbill_create_ip", PayUtil.getRemoteAddrIp(request)); parm.put("notify_url", "https://www.andy.org/wxpay/order/pay/notify.shtml"); parm.put("trade_type", "APP"); parm.put("sign", PayUtil.getSign(parm, API_SECRET)); String restxml = HttpUtils.post(ORDER_PAY, XmlUtil.xmlFormat(parm, false)); restmap = XmlUtil.xmlParse(restxml); } catch (Exception e) { LOG.error(e.getMessage(), e); } Map<String, String> payMap = new HashMap<String, String>(); if (CollectionUtil.isNotEmpty(restmap) && "SUCCESS".equals(restmap.get("result_code"))) { payMap.put("appid", APP_ID); payMap.put("partnerid", MCH_ID); payMap.put("prepayid", restmap.get("prepay_id")); payMap.put("package", "Sign=WXPay"); payMap.put("noncestr", PayUtil.getNonceStr()); payMap.put("timestamp", PayUtil.payTimestamp()); try { payMap.put("sign", PayUtil.getSign(payMap, API_SECRET)); } catch (Exception e) { flag = false; } } if (flag) { WebUtil.response(response, WebUtil.packJsonp(callback, JSON.toJSONString(new JsonResult(1, "訂單獲取成功", new ResponseData(null, payMap)), SerializerFeatureUtil.FEATURES))); } else { if (CollectionUtil.isNotEmpty(restmap)) { LOG.info("訂單建立失敗:" + restmap.get("err_code") + ":" + restmap.get("err_code_des")); } WebUtil.response(response, WebUtil.packJsonp(callback, JSON .toJSONString(new JsonResult(-1, "訂單獲取失敗", new ResponseData()), SerializerFeatureUtil.FEATURES))); } } /** * 查詢支付結果 * * @param request * @param response * @param tradeid 微信交易訂單號 * @param tradeno 商品訂單號 * @param callback */ @RequestMapping(value = "/pay/query", method = RequestMethod.POST) public void orderPayQuery(HttpServletRequest request, HttpServletResponse response, String tradeid, String tradeno, String callback) { LOG.info("[/order/pay/query]"); if (StringUtil.isEmpty(tradeno) && StringUtil.isEmpty(tradeid)) { WebUtil.response(response, WebUtil.packJsonp(callback, JSON .toJSONString(new JsonResult(-1, "訂單號不能為空", new ResponseData()), SerializerFeatureUtil.FEATURES))); } Map<String, String> restmap = null; try { Map<String, String> parm = new HashMap<String, String>(); parm.put("appid", APP_ID); parm.put("mch_id", MCH_ID); parm.put("transaction_id", tradeid); parm.put("out_trade_no", tradeno); parm.put("nonce_str", PayUtil.getNonceStr()); parm.put("sign", PayUtil.getSign(parm, API_SECRET)); String restxml = HttpUtils.post(ORDER_PAY_QUERY, XmlUtil.xmlFormat(parm, false)); restmap = XmlUtil.xmlParse(restxml); } catch (Exception e) { LOG.error(e.getMessage(), e); } if (CollectionUtil.isNotEmpty(restmap) && "SUCCESS".equals(restmap.get("result_code"))) { // 訂單查詢成功 處理業務邏輯 LOG.info("訂單查詢:訂單" + restmap.get("out_trade_no") + "支付成功"); WebUtil.response(response, WebUtil.packJsonp(callback, JSON .toJSONString(new JsonResult(1, "訂單支付成功", new ResponseData()), SerializerFeatureUtil.FEATURES))); } else { if (CollectionUtil.isNotEmpty(restmap)) { LOG.info("訂單支付失敗:" + restmap.get("err_code") + ":" + restmap.get("err_code_des")); } WebUtil.response(response, WebUtil.packJsonp(callback, JSON .toJSONString(new JsonResult(-1, "訂單支付失敗", new ResponseData()), SerializerFeatureUtil.FEATURES))); } } /** * 訂單支付微信伺服器非同步通知 * * @param request * @param response */ @RequestMapping("/pay/notify") public void orderPayNotify(HttpServletRequest request, HttpServletResponse response) { LOG.info("[/order/pay/notify]"); response.setCharacterEncoding("UTF-8"); response.setContentType("text/xml"); try { ServletInputStream in = request.getInputStream(); String resxml = FileUtil.readInputStream2String(in); Map<String, String> restmap = XmlUtil.xmlParse(resxml); LOG.info("支付結果通知:" + restmap); if ("SUCCESS".equals(restmap.get("result_code"))) { // 訂單支付成功 業務處理 String out_trade_no = restmap.get("out_trade_no"); // 商戶訂單號 // 通過商戶訂單判斷是否該訂單已經處理 如果處理跳過 如果未處理先校驗sign簽名 再進行訂單業務相關的處理 String sing = restmap.get("sign"); // 返回的簽名 restmap.remove("sign"); String signnow = PayUtil.getSign(restmap, API_SECRET); if (signnow.equals(sing)) { // 進行業務處理 LOG.info("訂單支付通知: 支付成功,訂單號" + out_trade_no); // 處理成功後相應給響應xml Map<String, String> respMap = new HashMap<>(); respMap = new HashMap<String, String>(); respMap.put("return_code", "SUCCESS"); //相應給微信伺服器 respMap.put("return_msg", "OK"); String resXml = XmlUtil.xmlFormat(restmap, true); response.getWriter().write(resXml); } else { LOG.info("訂單支付通知:簽名錯誤"); } } else { LOG.info("訂單支付通知:支付失敗," + restmap.get("err_code") + ":" + restmap.get("err_code_des")); } } catch (Exception e) { LOG.error(e.getMessage(), e); } } /** * 訂單退款 需要雙向證書驗證 * * @param request * @param response * @param tradeno 微信訂單號 * @param orderno 商家訂單號 * @param callback */ @RequestMapping(value = "/pay/refund", method = RequestMethod.POST) public void orderPayRefund(HttpServletRequest request, HttpServletResponse response, String tradeno, String orderno, String callback) { LOG.info("[/pay/refund]"); if (StringUtil.isEmpty(tradeno) && StringUtil.isEmpty(orderno)) { WebUtil.response(response, WebUtil.packJsonp(callback, JSON .toJSONString(new JsonResult(-1, "訂單號不能為空", new ResponseData()), SerializerFeatureUtil.FEATURES))); } Map<String, String> restmap = null; try { Map<String, String> parm = new HashMap<String, String>(); parm.put("appid", APP_ID); parm.put("mch_id", MCH_ID); parm.put("nonce_str", PayUtil.getNonceStr()); parm.put("transaction_id", tradeno); parm.put("out_trade_no", orderno);//訂單號 parm.put("out_refund_no", PayUtil.getRefundNo()); //退款單號 parm.put("total_fee", "10"); // 訂單總金額 從業務邏輯獲取 parm.put("refund_fee", "10"); // 退款金額 parm.put("op_user_id", MCH_ID); parm.put("refund_account", "REFUND_SOURCE_RECHARGE_FUNDS");//退款方式 parm.put("sign", PayUtil.getSign(parm, API_SECRET)); //String restxml = HttpUtils.posts(ORDER_REFUND, XmlUtil.xmlFormat(parm, false)); String restxml = HttpUtils.posts(ORDER_REFUND, XmlUtil.xmlFormat(parm, false)); restmap = XmlUtil.xmlParse(restxml); } catch (Exception e) { LOG.error(e.getMessage(), e); } Map<String, String> refundMap = new HashMap<>(); if (CollectionUtil.isNotEmpty(restmap) && "SUCCESS".equals(restmap.get("result_code"))) { refundMap.put("transaction_id", restmap.get("transaction_id")); refundMap.put("out_trade_no", restmap.get("out_trade_no")); refundMap.put("refund_id", restmap.get("refund_id")); refundMap.put("out_refund_no", restmap.get("out_refund_no")); LOG.info("訂單退款:訂單" + restmap.get("out_trade_no") + "退款成功,商戶退款單號" + restmap.get("out_refund_no") + ",微信退款單號" + restmap.get("refund_id")); WebUtil.response(response, WebUtil.packJsonp(callback, JSON.toJSONString(new JsonResult(1, "訂單獲取成功", new ResponseData(null, refundMap)), SerializerFeatureUtil.FEATURES))); } else { if (CollectionUtil.isNotEmpty(restmap)) { LOG.info("訂單退款失敗:" + restmap.get("err_code") + ":" + restmap.get("err_code_des")); } WebUtil.response(response, WebUtil.packJsonp(callback, JSON .toJSONString(new JsonResult(-1, "訂單退款失敗", new ResponseData()), SerializerFeatureUtil.FEATURES))); } } /** * 訂單退款查詢 * @param request * @param response * @param tradeid 微信訂單號 * @param tradeno 商戶訂單號 * @param refundid 微信退款號 * @param refundno 商家退款號 * @param callback */ @RequestMapping(value = "/pay/refund/query", method = RequestMethod.POST) public void orderPayRefundQuery(HttpServletRequest request, HttpServletResponse response, String refundid, String refundno, String tradeid, String tradeno, String callback) { LOG.info("[/pay/refund/query]"); if (StringUtil.isEmpty(tradeid) && StringUtil.isEmpty(tradeno) && StringUtil.isEmpty(refundno) && StringUtil.isEmpty(refundid)) { WebUtil.response(response, WebUtil.packJsonp(callback, JSON .toJSONString(new JsonResult(-1, "退單號或訂單號不能為空", new ResponseData()), SerializerFeatureUtil.FEATURES))); } Map<String, String> restmap = null; try { Map<String, String> parm = new HashMap<String, String>(); parm.put("appid", APP_ID); parm.put("mch_id", MCH_ID); parm.put("transaction_id", tradeid); parm.put("out_trade_no", tradeno); parm.put("refund_id", refundid); parm.put("out_refund_no", refundno); parm.put("nonce_str", PayUtil.getNonceStr()); parm.put("sign", PayUtil.getSign(parm, API_SECRET)); String restxml = HttpUtils.post(ORDER_REFUND_QUERY, XmlUtil.xmlFormat(parm, false)); restmap = XmlUtil.xmlParse(restxml); } catch (Exception e) { LOG.error(e.getMessage(), e); } Map<String, String> refundMap = new HashMap<>(); if (CollectionUtil.isNotEmpty(restmap) && "SUCCESS".equals(restmap.get("result_code")) && "SUCCESS".equals(restmap.get("result_code"))) { // 訂單退款查詢成功 處理業務邏輯 LOG.info("退款訂單查詢:訂單" + restmap.get("out_trade_no") + "退款成功,退款狀態"+ restmap.get("refund_status_0")); refundMap.put("transaction_id", restmap.get("transaction_id")); refundMap.put("out_trade_no", restmap.get("out_trade_no")); refundMap.put("refund_id", restmap.get("refund_id_0")); refundMap.put("refund_no", restmap.get("out_refund_no_0")); refundMap.put("refund_status", restmap.get("refund_status_0")); WebUtil.response(response, WebUtil.packJsonp(callback, JSON .toJSONString(new JsonResult(1, "訂單退款成功", new ResponseData(null, refundMap)), SerializerFeatureUtil.FEATURES))); } else { if (CollectionUtil.isNotEmpty(restmap)) { LOG.info("訂單退款失敗:" + restmap.get("err_code") + ":" + restmap.get("err_code_des")); } WebUtil.response(response, WebUtil.packJsonp(callback, JSON .toJSONString(new JsonResult(-1, "訂單退款失敗", new ResponseData()), SerializerFeatureUtil.FEATURES))); } } }

微信支付介面引數含義具體參考微信APP支付文件。

微信支付工具類

包含簽名、訂單號、退單號、隨機串、伺服器ip地址、客戶端ip地址等方法。

package org.andy.wxpay.utils;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;

/**
 * 建立時間:2016年11月2日 下午7:12:44
 * 
 * @author andy
 * @version 2.2
 */

public class PayUtil {

    /**
     * 生成訂單號
     * 
     * @return
     */
    public static String getTradeNo() {
        // 自增8位數 00000001
        return "TNO" + DatetimeUtil.formatDate(new Date(), DatetimeUtil.TIME_STAMP_PATTERN) + "00000001";
    }

    /**
     * 退款單號
     * 
     * @return
     */
    public static String getRefundNo() {
        // 自增8位數 00000001
        return "RNO" + DatetimeUtil.formatDate(new Date(), DatetimeUtil.TIME_STAMP_PATTERN) + "00000001";
    }

    /**
     * 退款單號
     * 
     * @return
     */
    public static String getTransferNo() {
        // 自增8位數 00000001
        return "TNO" + DatetimeUtil.formatDate(new Date(), DatetimeUtil.TIME_STAMP_PATTERN) + "00000001";
    }

    /**
     * 返回客戶端ip
     * 
     * @param request
     * @return
     */
    public static String getRemoteAddrIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (StringUtil.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
            // 多次反向代理後會有多個ip值,第一個ip才是真實ip
            int index = ip.indexOf(",");
            if (index != -1) {
                return ip.substring(0, index);
            } else {
                return ip;
            }
        }
        ip = request.getHeader("X-Real-IP");
        if (StringUtil.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
            return ip;
        }
        return request.getRemoteAddr();
    }

    /**
     * 獲取伺服器的ip地址
     * 
     * @param request
     * @return
     */
    public static String getLocalIp(HttpServletRequest request) {
        return request.getLocalAddr();
    }

    public static String getSign(Map<String, String> params, String paternerKey) throws UnsupportedEncodingException {
        return MD5Utils.getMD5(createSign(params, false) + "&key=" + paternerKey).toUpperCase();
    }

    /**
     * 構造簽名
     * 
     * @param params
     * @param encode
     * @return
     * @throws UnsupportedEncodingException
     */
    public static String createSign(Map<String, String> params, boolean encode) throws UnsupportedEncodingException {
        Set<String> keysSet = params.keySet();
        Object[] keys = keysSet.toArray();
        Arrays.sort(keys);
        StringBuffer temp = new StringBuffer();
        boolean first = true;
        for (Object key : keys) {
            if (key == null || StringUtil.isEmpty(params.get(key))) // 引數為空不參與簽名
                continue;
            if (first) {
                first = false;
            } else {
                temp.append("&");
            }
            temp.append(key).append("=");
            Object value = params.get(key);
            String valueStr = "";
            if (null != value) {
                valueStr = value.toString();
            }
            if (encode) {
                temp.append(URLEncoder.encode(valueStr, "UTF-8"));
            } else {
                temp.append(valueStr);
            }
        }
        return temp.toString();
    }

    /**
     * 建立支付隨機字串
     * @return
     */
    public static String getNonceStr(){
        return RandomUtil.randomString(RandomUtil.LETTER_NUMBER_CHAR, 32);
    }

    /**
     * 支付時間戳
     * @return
     */
    public static String payTimestamp() {
        return Long.toString(System.currentTimeMillis() / 1000);
    }
}

其他所需工具類參考專案原始碼

支付結果

支付結果

APP支付測試完成