1. 程式人生 > >java實現微信企業付款到個人賬戶

java實現微信企業付款到個人賬戶

  • 微信官方提供了微信企業賬戶付款到微信個人零錢介面,提供企業向用戶付款的功能,支援企業通過API介面付款,或通過微信支付商戶平臺網頁功能操作付款。該介面並不是直接所有的商戶都擁有,企業要開啟必須滿足以下兩個條件:
    1、商戶號已入駐90日
    2、商戶號有30天連續正常交易
    滿足以上條件就可登入微信支付商戶平臺-產品中心,開通企業付款。
    呼叫的連結地址:介面連結:https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers
    微信官方介面文件提供的呼叫微信企業付款的引數:
    這裡寫圖片描述
    引數中最重要是獲取使用者openid和呼叫介面ip,獲取openid可以通過公眾號獲取,app端可以直接獲取。具體的程式碼實現如下:
    封裝請求微信企業付款的實體類Transfers:

    public class Transfers implements Serializable{
        private static final long serialVersionUID = 1L;
        /** 商戶賬號appid*/
        public String mch_appid;
        /** 微信支付商戶號*/
        public String mchid;
        /** 隨機串*/
        public String nonce_str;
        /** 簽名*/
        public String sign;
        /** 商戶訂單號*/
        public String partner_trade_no;
        /** 使用者id*/
        public String openid;
        /** 是否校驗使用者姓名 NO_CHECK:不校驗真實姓名  FORCE_CHECK:強校驗真實姓名*/
        public String check_name;
        /** 金額 單位:分*/
        public Integer amount;
        /** 企業付款描述資訊*/
        public String desc;
        /** ip地址*/
        public String spbill_create_ip;
        public String getMch_appid() {
            return mch_appid;
        }
        public void setMch_appid(String mch_appid) {
            this.mch_appid = mch_appid;
        }
        public String getMchid() {
            return mchid;
        }
        public void setMchid(String mchid) {
            this.mchid = mchid;
        }
        public String getNonce_str() {
            return nonce_str;
        }
        public void setNonce_str(String nonce_str) {
            this.nonce_str = nonce_str;
        }
        public String getSign() {
            return sign;
        }
        public void setSign(String sign) {
            this.sign = sign;
        }
        public String getPartner_trade_no() {
            return partner_trade_no;
        }
        public void setPartner_trade_no(String partner_trade_no) {
            this.partner_trade_no = partner_trade_no;
        }
        public String getOpenid() {
            return openid;
        }
        public void setOpenid(String openid) {
            this.openid = openid;
        }
        public String getCheck_name() {
            return check_name;
        }
        public void setCheck_name(String check_name) {
            this.check_name = check_name;
        }
        public Integer getAmount() {
            return amount;
        }
        public void setAmount(Integer amount) {
            this.amount = amount;
        }
        public String getDesc() {
            return desc;
        }
        public void setDesc(String desc) {
            this.desc = desc;
        }
        public String getSpbill_create_ip() {
            return spbill_create_ip;
        }
        public void setSpbill_create_ip(String spbill_create_ip) {
            this.spbill_create_ip = spbill_create_ip;
        }
    
    }

介面部分程式碼:

private Transfers transfers = new Transfers();
// 構造簽名的map
    private SortedMap<Object, Object> parameters = new TreeMap<Object, Object>();
// 微信的引數
    private WeixinConfigUtils config = new WeixinConfigUtils();
  /**
   * 微信提現(企業付款)
   */
    @Action("weixinWithdraw")
    public String weixinWithdraw(){
        String openId = request.getParameter("openid");
        String ip = request.getParameter("ip");
        String money = request.getParameter("money");
        String doctorId = request.getParameter("doctorId");
        if (StringUtils.isNotBlank(money) && StringUtils.isNotBlank(ip) && StringUtils.isNotBlank(openId) && StringUtils.isNotBlank(doctorId)) {
        // 引數組
        String appid = config.appid;
        String mch_id = config.mch_id;
        String nonce_str = RandCharsUtils.getRandomString(16);
        //是否校驗使用者姓名 NO_CHECK:不校驗真實姓名  FORCE_CHECK:強校驗真實姓名
        String checkName ="NO_CHECK";
        //等待確認轉賬金額,ip,openid的來源
        Integer amount = Integer.valueOf(money);
        String spbill_create_ip = ip;
        String partner_trade_no = UuIdUtils.getUUID();
        //描述
        String desc = "健康由我醫師助手提現"+amount/100+"元";
        // 引數:開始生成第一次簽名
        parameters.put("appid", appid);
        parameters.put("mch_id", mch_id);
        parameters.put("partner_trade_no", partner_trade_no);
        parameters.put("nonce_str", nonce_str);
        parameters.put("openId", openId);
        parameters.put("checkName", checkName);
        parameters.put("amount", amount);
        parameters.put("spbill_create_ip", spbill_create_ip);
        parameters.put("desc", desc);
        String sign = WXSignUtils.createSign("UTF-8", parameters);
        transfers.setAmount(amount);
        transfers.setCheck_name(checkName);
        transfers.setDesc(desc);
        transfers.setMch_appid(appid);
        transfers.setMchid(mch_id);
        transfers.setNonce_str(nonce_str);
        transfers.setOpenid(openId);
        transfers.setPartner_trade_no(partner_trade_no);
        transfers.setSign(sign);
        transfers.setSpbill_create_ip(spbill_create_ip);
        String xmlInfo = HttpXmlUtils.transferXml(transfers);
        try {
            CloseableHttpResponse response =  HttpUtil.Post(weixinConstant.WITHDRAW_URL, xmlInfo, true);
            String transfersXml = EntityUtils.toString(response.getEntity(), "utf-8");
            Map<String, String> transferMap = HttpXmlUtils.parseRefundXml(transfersXml);
            if (transferMap.size()>0) {
                if (transferMap.get("result_code").equals("SUCCESS") && transferMap.get("return_code").equals("SUCCESS")) {
                    //成功需要進行的邏輯操作,

                    }
                }
            System.out.println("成功");
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new BasicRuntimeException(this, "企業付款異常" + e.getMessage());
        }
        }else {
            System.out.println("失敗");
        }
        return NONE;
    }

產生隨機串部分程式碼:

public class RandCharsUtils {
    private static SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");

    public static String getRandomString(int length) { //length表示生成字串的長度
        String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";   
        Random random = new Random();   
        StringBuffer sb = new StringBuffer();
        int number = 0;
        for (int i = 0; i < length; i++) {   
            number = random.nextInt(base.length());   
            sb.append(base.charAt(number));   
        }   
        return sb.toString();   
    }   
    }

生成簽名:

public class WXSignUtils {
    /**
     * 微信支付簽名演算法sign
     * @param characterEncoding
     * @param parameters
     * @return
     */
    @SuppressWarnings("rawtypes")
    public static String createSign(String characterEncoding,SortedMap<Object,Object> parameters){
        StringBuffer sb = new StringBuffer();
        Set es = parameters.entrySet();//所有參與傳參的引數按照accsii排序(升序)
        Iterator it = es.iterator();
        while(it.hasNext()) {
            Map.Entry entry = (Map.Entry)it.next();
            String k = (String)entry.getKey();
            Object v = entry.getValue();
            if(null != v && !"".equals(v) 
                    && !"sign".equals(k) && !"key".equals(k)) {
                sb.append(k + "=" + v + "&");
            }
        }
        sb.append("key=" + weixinConstant.KEY);
        String sign = MD5Util.MD5Encode(sb.toString(), characterEncoding).toUpperCase();
        return sign;
    }
}

md5部分程式碼:

import java.security.MessageDigest;

public class MD5Util {

    private static String byteArrayToHexString(byte b[]) {
        StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++)
            resultSb.append(byteToHexString(b[i]));

        return resultSb.toString();
    }

    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0)
            n += 256;
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    public static String MD5Encode(String origin, String charsetname) {
        String resultString = null;
        try {
            resultString = new String(origin);
            MessageDigest md = MessageDigest.getInstance("MD5");
            if (charsetname == null || "".equals(charsetname))
                resultString = byteArrayToHexString(md.digest(resultString
                        .getBytes()));
            else
                resultString = byteArrayToHexString(md.digest(resultString
                        .getBytes(charsetname)));
        } catch (Exception exception) {
        }
        return resultString;
    }

    private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
        "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };

}

構造xml:

/**
     * 構造企業付款xml引數
     * @param xml
     * @return
     */
    public static String transferXml(Transfers transfers){
            xStream.autodetectAnnotations(true);
            xStream.alias("xml", Transfers.class);
            return xStream.toXML(transfers);
    }

向微信傳送xml請求(驗證證書)部分程式碼:

public class HttpUtil {
    /**
     * 傳送post請求
     * 
     * @param url
     *            請求地址
     * @param outputEntity
     *            傳送內容
     * @param isLoadCert
     *            是否載入證書
     */
    public static CloseableHttpResponse Post(String url, String outputEntity, boolean isLoadCert) throws Exception {
        HttpPost httpPost = new HttpPost(url);
        // 得指明使用UTF-8編碼,否則到API伺服器XML的中文不能被成功識別
        httpPost.addHeader("Content-Type", "text/xml");
        httpPost.setEntity(new StringEntity(outputEntity, "UTF-8"));
        if (isLoadCert) {
            // 載入含有證書的http請求
            return HttpClients.custom().setSSLSocketFactory(CertUtil.initCert()).build().execute(httpPost);
        } else {
            return HttpClients.custom().build().execute(httpPost);
        }
    }
}

載入證書部分程式碼:(載入證書需要注意證書放置位置在專案下的webapp中建資料夾,linux單獨防止只要地址配置正確即可。)

import java.io.File;
import java.io.FileInputStream;
import java.security.KeyStore;

import javax.net.ssl.SSLContext;

import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.ssl.SSLContexts;

/**
 * 載入證書的類
 * @author 
 * @since 2017/08/16
 */

@SuppressWarnings("deprecation")
public class CertUtil {
    private static  WeixinConfigUtils config = new WeixinConfigUtils();
    /**
     * 載入證書
     */
    public static SSLConnectionSocketFactory initCert() throws Exception {
        FileInputStream instream = null;
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        instream = new FileInputStream(new File(weixinConstant.PATH));
        keyStore.load(instream, config.mch_id.toCharArray());

        if (null != instream) {
            instream.close();
        }

        SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore,config.mch_id.toCharArray()).build();
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, new String[]{"TLSv1"}, null, SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);

        return sslsf;
    }
}

載入配置檔案部分程式碼:

@SuppressWarnings("unused")
public class WeixinConfigUtils {
    private static final Log log = LogFactory.getLog(WeixinConfigUtils.class);
    public static String appid;
    public static String mch_id;
    public static String notify_url;
    public static String order_notify_url;
    public static String doctor_notify_url;
    static {
        try{
            InputStream is = WeixinConfigUtils.class.getResourceAsStream("/weixin.properties");
            Properties properties = new Properties();
            properties.load(is);
            appid = properties.getProperty("weixin.appid");
            mch_id = properties.getProperty("weixin.mch_id");
            notify_url = properties.getProperty("weixin.notify_url");
            order_notify_url = properties.getProperty("weixin.order_notify_url");
            doctor_notify_url = properties.getProperty("weixin.doctor_notify_url");
        }catch(Exception ex){
            log.debug("載入配置檔案:"+ex.getMessage());
        }
    }
}

獲取返回的xml引數並解析為map:

/**
     * 解析申請退款之後微信返回的值並進行存庫操作
     * @throws IOException 
     * @throws JDOMException 
     */
    public static Map<String, String> parseRefundXml(String refundXml) throws JDOMException, IOException{
        ParseXMLUtils.jdomParseXml(refundXml);
        StringReader read = new StringReader(refundXml);
        // 建立新的輸入源SAX 解析器將使用 InputSource 物件來確定如何讀取 XML 輸入
        InputSource source = new InputSource(read);
        // 建立一個新的SAXBuilder
        SAXBuilder sb = new SAXBuilder();
        // 通過輸入源構造一個Document
        org.jdom.Document doc;
        doc = (org.jdom.Document) sb.build(source);
        org.jdom.Element root = doc.getRootElement();// 指向根節點
        List<org.jdom.Element> list = root.getChildren();
        Map<String, String> refundOrderMap = new HashMap<String, String>();
        if(list!=null&&list.size()>0){
            for (org.jdom.Element element : list) {
                refundOrderMap.put(element.getName(), element.getText());
            }
            return refundOrderMap;
            }
        return null;
    }

呼叫時候主要是獲取openid和調起介面的ip(ip十分重要,微信在收到xml後會校驗傳過去的ip和微信獲取的調起介面ip是否一致)
在呼叫時候當返回錯誤碼為“SYSTEMERROR”時,一定要使用原單號重試,否則可能造成重複支付等資金風險。
微信官方文件提供有相關的引數錯誤碼https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2