微信支付(app內(nèi)) java后端

官方文檔

https://pay.weixin.qq.com/static/product/product_intro.shtml?name=app

開(kāi)發(fā)之前建議先梳理下官方文檔提供的業(yè)務(wù)流程

業(yè)務(wù)流程圖

開(kāi)發(fā)前準(zhǔn)備

appId:應(yīng)用id
mchId:商戶號(hào)
apiKey:api密鑰
以上參數(shù)參考https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_5_1.shtml
如需退款等接口參考https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_5_3.shtml

1.創(chuàng)建訂單

1.1支付config

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;



/**
 * 微信支付配置類
 */
@Component
@PropertySource(value = {"classpath:wechatpay.properties"})
@Data
public class WeChatPayConfig  {

    /**
     * 應(yīng)用ID
     */
    @Value("${wxappId}")
    public String appId;


    /**
     * 商戶號(hào)
     */
    @Value("${mchId}")
    public String mchId;


    /**
     * api 密鑰
     */
    @Value("${apiKey}")
    public String apiKey;

    /**
     * 異步通知地址
     */
    @Value("${wxnotifyUrl}")
    public String notifyUrl;

    @Value("${placeUrl}")
    public String placeUrl;

}

1.2 工具類

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClients;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;

import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;

public class WeChatUtil {



    /**
     * 第一次簽名
     *
     * @param parameters 數(shù)據(jù)為服務(wù)器生成,下單時(shí)必須的字段排序簽名
     * @param key
     * @return
     */
    public static String createSign(SortedMap<String, Object> parameters, String key) {
        StringBuffer sb = new StringBuffer();
        Set es = parameters.entrySet();//所有參與傳參的參數(shù)按照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=" + key);
        return encodeMD5(sb.toString());
    }

    /**
     * MD5 簽名
     *
     * @param str
     * @return 簽名后的字符串信息
     */
    public static String encodeMD5(String str) {
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            byte[] inputByteArray = (str).getBytes();
            messageDigest.update(inputByteArray);
            byte[] resultByteArray = messageDigest.digest();
            return byteArrayToHex(resultByteArray);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    // 輔助 encodeMD5 方法實(shí)現(xiàn)
    private static String byteArrayToHex(byte[] byteArray) {
        char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
        char[] resultCharArray = new char[byteArray.length * 2];
        int index = 0;
        for (byte b : byteArray) {
            resultCharArray[index++] = hexDigits[b >>> 4 & 0xf];
            resultCharArray[index++] = hexDigits[b & 0xf];
        }
        // 字符數(shù)組組合成字符串返回
        return new String(resultCharArray);
    }

    /**
     * 執(zhí)行 POST 方法的 HTTP 請(qǐng)求
     *
     * @param url
     * @param parameters
     * @return
     * @throws IOException
     */
    public static String executeHttpPost(String url, SortedMap<String, Object> parameters) throws IOException {
        HttpClient client = HttpClients.createDefault();
        HttpPost request = new HttpPost(url);
        request.setHeader("Content-type", "application/xml");
        request.setHeader("Accept", "application/xml");
        request.setEntity(new StringEntity(transferMapToXml(parameters), "UTF-8"));
        HttpResponse response = client.execute(request);
        return readResponse(response);
    }

    /**
     * 將 Map 轉(zhuǎn)化為 XML
     *
     * @param map
     * @return
     */
    public static String transferMapToXml(SortedMap<String, Object> map) {
        StringBuffer sb = new StringBuffer();
        sb.append("<xml>");
        for (String key : map.keySet()) {
            sb.append("<").append(key).append(">")
                    .append(map.get(key))
                    .append("</").append(key).append(">");
        }
        return sb.append("</xml>").toString();
    }

    /**
     * 讀取 response body 內(nèi)容為字符串
     */
    public static String readResponse(HttpResponse response) throws IOException {
        BufferedReader in = new BufferedReader(
                new InputStreamReader(response.getEntity().getContent()));
        String result = new String();
        String line;
        while ((line = in.readLine()) != null) {
            result += line;
        }
        return result;
    }

    /**
     * 第二次簽名
     *
     * @param result 數(shù)據(jù)為微信返回給服務(wù)器的數(shù)據(jù)(XML 的 String)墩弯,再次簽名后傳回給客戶端(APP)使用
     * @param key    密鑰
     * @return
     * @throws IOException
     */
    public static Map createSign2(String result, String key) throws IOException {
        SortedMap<String, Object> map = new TreeMap<>(transferXmlToMap(result));
        Map app = new HashMap();
        app.put("appid", map.get("appid"));
        app.put("partnerid", map.get("mch_id"));
        app.put("prepayid", map.get("prepay_id"));
        app.put("package", "Sign=WXPay");                   // 固定字段,保留,不可修改
        app.put("noncestr", map.get("nonce_str"));
        app.put("timestamp", new Date().getTime() / 1000);  // 時(shí)間為秒瘩例,JDK 生成的是毫秒,故除以 1000
        app.put("sign", createSign(new TreeMap<>(app), key));
        return app;
    }

    /**
     * 將 XML 轉(zhuǎn)化為 map
     *
     * @param strxml
     * @return
     * @throws JDOMException
     * @throws IOException
     */
    public static Map transferXmlToMap(String strxml) throws IOException {
        strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
        if (null == strxml || "".equals(strxml)) {
            return null;
        }
        Map m = new HashMap();
        InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
        SAXBuilder builder = new SAXBuilder();
        Document doc = null;
        try {
            doc = builder.build(in);
        } catch (JDOMException e) {
            throw new IOException(e.getMessage()); // 統(tǒng)一轉(zhuǎn)化為 IO 異常輸出
        }
        // 解析 DOM
        Element root = doc.getRootElement();
        List list = root.getChildren();
        Iterator it = list.iterator();
        while (it.hasNext()) {
            Element e = (Element) it.next();
            String k = e.getName();
            String v = "";
            List children = e.getChildren();
            if (children.isEmpty()) {
                v = e.getTextNormalize();
            } else {
                v = getChildrenText(children);
            }
            m.put(k, v);
        }
        //關(guān)閉流
        in.close();
        return m;
    }
    // 輔助 transferXmlToMap 方法遞歸提取子節(jié)點(diǎn)數(shù)據(jù)
    private static String getChildrenText(List<Element> children) {
        StringBuffer sb = new StringBuffer();
        if (!children.isEmpty()) {
            Iterator<Element> it = children.iterator();
            while (it.hasNext()) {
                Element e = (Element) it.next();
                String name = e.getName();
                String value = e.getTextNormalize();
                List<Element> list = e.getChildren();
                sb.append("<" + name + ">");
                if (!list.isEmpty()) {
                    sb.append(getChildrenText(list));
                }
                sb.append(value);
                sb.append("</" + name + ">");
            }
        }
        return sb.toString();
    }

    /**
     * 驗(yàn)證簽名是否正確
     *
     * @return boolean
     * @throws Exception
     */
    public static boolean checkSign(SortedMap<String, Object> parameters, String key) throws Exception {
        String signWx = parameters.get("sign").toString();
        if (signWx == null) return false;
        parameters.remove("sign"); // 需要去掉原 map 中包含的 sign 字段再進(jìn)行簽名
        String signMe = createSign(parameters, key);
        return signWx.equals(signMe);
    }

    /**
     * 讀取 request body 內(nèi)容作為字符串
     *
     * @param request
     * @return
     * @throws IOException
     */
    public static String readRequest(HttpServletRequest request) throws IOException {
        InputStream inputStream;
        StringBuffer sb = new StringBuffer();
        inputStream = request.getInputStream();
        String str;
        BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
        while ((str = in.readLine()) != null) {
            sb.append(str);
        }
        in.close();
        inputStream.close();
        return sb.toString();
    }


    /**
     * 微信回調(diào)接口成功返回值
     * @return
     */
    public static String success(){
        return "<xml>\n" +
                "  <return_code><![CDATA[SUCCESS]]></return_code>\n" +
                "  <return_msg><![CDATA[OK]]></return_msg>\n" +
                "</xml>";
    }

    /**
     * 微信回調(diào)接口失敗返回值
     * @return
     */
    public static String fail(){
        return "<xml>\n" +
                "  <return_code><![CDATA[FAIL]]></return_code>\n" +
                "  <return_msg><![CDATA[]]></return_msg>\n" +
                "</xml>";
    }

1.3創(chuàng)建訂單

import com.alibaba.fastjson.JSON;
import com.smt.amblyopia.nd.app.util.CommonUtil;
import com.smt.amblyopia.nd.app.util.WeChatUtil;
import com.smt.amblyopia.nd.comm.util.RandomUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.SortedMap;
import java.util.TreeMap;

@Component
@Slf4j
public class WeChatPay {

    @Autowired
    private WeChatPayConfig weChatPayConfig;


    /**
     *
     * @param body 購(gòu)買的商品名稱 如:商品1  商品2
     * @param outtradeno  訂單號(hào)
     * @param amount 商品價(jià)格
     * @param request
     * @return
     * @throws IOException
     */
    public String weChatCreateOrder(String body, String outtradeno, BigDecimal amount, HttpServletRequest request) throws IOException {
        String ip = CommonUtil.getIpAddr(request);
        SortedMap<String, Object> data = new TreeMap<>();
        data.put("appid", weChatPayConfig.getAppId());//應(yīng)用ID
        data.put("mch_id", weChatPayConfig.getMchId());//商戶號(hào)
        data.put("device_info", "WEB"); // 默認(rèn) "WEB"
        data.put("body", body);
        data.put("nonce_str", RandomUtils.getRandomStr(10));//隨機(jī)字符串data.put("trade_type" , "APP");//支付類型
        data.put("notify_url", weChatPayConfig.getNotifyUrl());
        data.put("out_trade_no", outtradeno);
        data.put("total_fee", amount.longValue() + ""); //分
        data.put("spbill_create_ip", ip);
        data.put("trade_type", "APP");//支付類型
        data.put("sign", WeChatUtil.createSign(data, weChatPayConfig.apiKey));//支付類型   ;
        String result = WeChatUtil.executeHttpPost(weChatPayConfig.getPlaceUrl(), data); // 執(zhí)行 HTTP 請(qǐng)求,獲取接收的字符串(一段 XML)
        String resp = JSON.toJSONString(WeChatUtil.createSign2(result, weChatPayConfig.apiKey));
        return resp;
    }


1.4 本地訂單處理

訂單創(chuàng)建成功庶诡,本地需要記錄原始訂單,根據(jù)回調(diào)接口的返回值咆课,改變本地訂單狀態(tài)末誓;

2.支付回調(diào)接口

2.1微信支付回調(diào)

import com.alibaba.fastjson.JSON;
import com.smt.amblyopia.nd.app.util.WeChatUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

@Component
@Slf4j
public class WeChatPay {

   @Autowired
   private WeChatPayConfig weChatPayConfig;


   
   public Map<String, String> weChatCallBack(HttpServletRequest request, HttpServletResponse response) throws Exception {
       Map<String, String> result = new HashMap<>();
       // 預(yù)先設(shè)定返回的 response 類型為 xml
       response.setHeader("Content-type", "application/xml");
       // 讀取參數(shù),解析Xml為map
       Map<String, String> map = WeChatUtil.transferXmlToMap(WeChatUtil.readRequest(request));
       log.info("微信返回值轉(zhuǎn)map:{}" , JSON.toJSONString(map));
       // 轉(zhuǎn)換為有序 map书蚪,判斷簽名是否正確
       boolean isSignSuccess = WeChatUtil.checkSign(new TreeMap<String, Object>(map), weChatPayConfig.apiKey);
       if (isSignSuccess) {
           log.info("校驗(yàn)簽名成功 isSignSuccess:{}" , isSignSuccess);
           // 簽名校驗(yàn)成功喇澡,說(shuō)明是微信服務(wù)器發(fā)出的數(shù)據(jù)
           String orderId = map.get("out_trade_no");
           String returnCode = map.get("return_code");
           result.put("orderId", orderId);
           result.put("returnCode", returnCode);
           //map.get("return_code").equals("SUCCESS")
           return result;
       }else{
           return  null;
       }

   }

2.2本地訂單更新


通知應(yīng)答
根據(jù)returnCode的值,更新本地訂單狀態(tài)善炫;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末撩幽,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌窜醉,老刑警劉巖宪萄,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異榨惰,居然都是意外死亡拜英,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門琅催,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)居凶,“玉大人,你說(shuō)我怎么就攤上這事藤抡∠辣蹋” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵缠黍,是天一觀的道長(zhǎng)弄兜。 經(jīng)常有香客問(wèn)我,道長(zhǎng)瓷式,這世上最難降的妖魔是什么替饿? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮贸典,結(jié)果婚禮上视卢,老公的妹妹穿的比我還像新娘。我一直安慰自己廊驼,他們只是感情好据过,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著妒挎,像睡著了一般蝶俱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上饥漫,一...
    開(kāi)封第一講書(shū)人閱讀 51,182評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音罗标,去河邊找鬼庸队。 笑死,一個(gè)胖子當(dāng)著我的面吹牛闯割,可吹牛的內(nèi)容都是我干的彻消。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼宙拉,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼宾尚!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤煌贴,失蹤者是張志新(化名)和其女友劉穎御板,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體牛郑,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡怠肋,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了淹朋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片笙各。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖础芍,靈堂內(nèi)的尸體忽然破棺而出杈抢,到底是詐尸還是另有隱情,我是刑警寧澤仑性,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布惶楼,位于F島的核電站,受9級(jí)特大地震影響虏缸,放射性物質(zhì)發(fā)生泄漏鲫懒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一刽辙、第九天 我趴在偏房一處隱蔽的房頂上張望窥岩。 院中可真熱鬧,春花似錦宰缤、人聲如沸颂翼。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)朦乏。三九已至,卻和暖如春氧骤,著一層夾襖步出監(jiān)牢的瞬間呻疹,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工筹陵, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留刽锤,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓朦佩,卻偏偏與公主長(zhǎng)得像并思,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子语稠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容