APP調起微信支付,JAVA服務端統一下單

原理概述:

微信支付分爲公衆號發起,PC網頁掃碼,APP用於APP端調起的支付
這篇文章主要討論APP掉起支付,包括統一下單,微信接口回調,以及一些注意事項

微信官方文檔:

https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_1

這裏有詳細的入參列表,調用地址,但是我表示,看完文檔依舊寫不出可以馬上可用的接口

源碼詳述:

nonce_str參數:

生成隨機數算法,用於統一下單的nonce_str參數
public static String getRandomStringByLength(int length) {  
        String base = "abcdefghijklmnopqrstuvwxyz0123456789";  
        Random random = new Random();  
        StringBuffer sb = new StringBuffer();  
        for (int i = 0; i < length; i++) {  
            int number = random.nextInt(base.length());  
            sb.append(base.charAt(number));  
        }  
        return sb.toString();  
    }  

獲取統一下單接口地址:

https://api.mch.weixin.qq.com/pay/unifiedorder

從微信頒發的參數:

應用ID:appid ; 商戶號:mch_id ;微信支付key:用戶自己設置的支付key(我的程序裏叫wx_key)
以上三個參數,來自微信商戶,登錄商戶賬號,可以找到,在支付時直接把參數寫進去就行

需要客戶端必須傳遞給服務端的參數:

商戶訂單號:out_trade_no 這個參數是微信自己掉起支付生成的訂單號,服務器需要這個參數去微信服務器下單
總金額:total_fee  這個參數如果客戶端轉化成分,那麼服務端就不用轉換了,總之最後要傳分爲單位的數值
關於商品屬性相關的比如商品詳情detail ;商品描述body ;附加數據 attach ;訂單優惠標記 :goods_tag,可以服務寫死,也可以客戶端傳遞過來

微信回調地址:

這個參數是微信在接收到支付前款後,主動調用你的應用服務器,用來通知支付成功的
這個地址是需要你自己來寫一個controler,不要限制請求,不要攜帶參數,並且微信支付需要時80端口
一下爲我寫的一個回調的源碼
/**
     * 微信支付回調函數
     * @param request
     * @param response
     * @throws IOException
     */
    @RequestMapping(value = "/callback")
    public void callBack(HttpServletRequest request,HttpServletResponse response) throws IOException {
        InputStream is = request.getInputStream();
        HashMap<String, String> map = new HashMap<String, String>();
        SAXReader reader = new SAXReader();
        Document document = null;
        try {
            document = reader.read(is);
        } catch (DocumentException e1) {
            e1.printStackTrace();
        }
        String out_trade_no = "";//訂單ID
        String total_fee = "";   //訂單金額
        Element root = document.getRootElement();
        List<Element> list = root.elements();
        // 獲取微信返回值信息
        for (Element e : list) {
        	 map.put(e.getName().trim(), e.getText().trim());
            if (e.getName().trim().equals("out_trade_no")) {
            	out_trade_no = e.getText().trim();
            } else if (e.getName().trim().equals("cash_fee")) {
            	total_fee = e.getText().trim();
            }  
        }
        is.close();
        
        // 克隆傳入的信息並進行驗籤,建議一定要驗證簽名,防止返回值被篡改
        HashMap<String, String> signMap = (HashMap<String, String>) map.clone();
        signMap.remove("sign");
        // 這裏的wx_key 是用戶自定義的支付key
        String key= PropertiesUtil.getValue("wechat.properties","wx_key");
        String sign = SignatureUtils.signature(signMap,key);
        
        if (!sign.equals(map.get("sign"))) {
           
            return;
        }
        // 信息處理
        String result_code = map.get("result_code");
        try {
            if ("SUCCESS".equals(result_code)) {
                //由於微信後臺會同時回調多次,所以需要做防止重複提交操作的判斷
                //此處放防止重複提交操作
            } else if ("FAIL".equals(result_code)) {
            }
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }
        
		//這裏是驗證返回值沒問題了,可以寫具體的支付成功的邏輯

        // 返回信息,防止微信重複發送報文
        String result = "<xml>"
                + "<return_code><![CDATA[SUCCESS]]></return_code>"
                + "<return_msg><![CDATA[OK]]></return_msg>" + "</xml>";
        PrintWriter out = new PrintWriter(response.getOutputStream());
        out.print(result);
        out.flush();
        out.close();
    }

驗籤源碼:(用於統一下單,微信回調)

這個代碼生成的值用於同一下單的 簽名:sign 參數,也用於驗證微信回調的返回值
提供一個微信工具,用來計算簽名的,可以本地計算完和他比對,實際開發過程中會出現各種情況的簽名錯誤,用這個你可以很快找到問題:
https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=20_1
源碼:
/**
	 * 微信支付加密工具
	 */
	public static String signature(Map<String, String> map, String key) {
		Set<String> keySet = map.keySet();
		String[] str = new String[map.size()];
		StringBuilder tmp = new StringBuilder();
		// 進行字典排序
		str = keySet.toArray(str);
		Arrays.sort(str);
		for (int i = 0; i < str.length; i++) {
			String t = str[i] + "=" + map.get(str[i]) + "&";
			tmp.append(t);
		}
		if (StringUtils.isNotBlank(key)) {
			tmp.append("key=" + key);
		}
		String tosend = tmp.toString();
		MessageDigest md = null;
		byte[] bytes = null;
		try {

			md = MessageDigest.getInstance("MD5");
			bytes = md.digest(tosend.getBytes("utf-8"));
		} catch (Exception e) {
			e.printStackTrace();
		}

		String singe = byteToStr(bytes);
		return singe.toUpperCase();

	}

統一下單源碼:

注意,統一下單並不是支付成功,他只是吧這個訂單號在微信服務那裏生成了預支付信息,真正的支付成功還要看微信回調
public static synchronized JSONObject createOrder(String detail, String desc, String openid, String ip, String goodSn, String orderSn, String amount, String type,String randomNum) {
        JSONObject result = new JSONObject();
        double relAmount = 0;// 對應微信支付的真實數目
        try {
        	//微信接收的金額單位是分,如果客戶端不轉換,服務端要講金額轉換成分
            relAmount = Double.parseDouble(amount) * 100;
        } catch (Exception e) {
            return result;
        }

        if (relAmount == 0) {
		   //微信支付的支付金額必須爲大於0的int類型,單位爲分
            return result;
        }
		
        if (!("JSAPI".equalsIgnoreCase(type) || "NATIVE".equalsIgnoreCase(type) || "APP".equalsIgnoreCase(type))) {

            return result;
        }

        // 獲取系統配置信息
        String wx_order = PropertiesUtil.getValue("wechat.properties", "wx_order");//獲取統一下單接口地址
        String mchappid = PropertiesUtil.getValue("wechat.properties", "mchappid");// 商戶appid
        String mchid = PropertiesUtil.getValue("wechat.properties", "mchid");// 商戶ID
        String wx_callback = PropertiesUtil.getValue("wechat.properties", "wx_callback");// 獲取微信支付回調接口
        String wx_key = PropertiesUtil.getValue("wechat.properties", "wx_key");//微信商戶後臺設置的key
        String app_mchid = PropertiesUtil.getValue("wechat.properties", "app_mchid");//APP調起微信支付的商戶ID
        String app_mchappid = PropertiesUtil.getValue("wechat.properties", "app_mchappid");//APP調起微信的APPID

        if (StringUtils.isBlank(wx_order) || StringUtils.isBlank(mchappid)|| StringUtils.isBlank(mchid) || StringUtils.isBlank(wx_callback)) {
            return result;
        }
		
        // 發送報文模板,其中部分字段是可選字段
        String xml = "" +
                "<xml>" +
                "<appid>APPID</appid>" +//公衆號ID
                "<device_info>WEB</device_info>" +//設備信息
                "<detail>DETAIL</detail>" +//商品詳情
                "<body>BODY</body>" +//商品描述
                "<mch_id>MERCHANT</mch_id>" +//微信給的商戶ID
                "<nonce_str>FFHH</nonce_str>" +//32位隨機字符串
                "<notify_url><![CDATA[URL_TO]]></notify_url>" +//微信回調信息通知頁面
                "<openid>UserFrom</openid>" +//支付的用戶ID
                "<fee_type>CNY</fee_type>" +//支付貨幣
                "<spbill_create_ip>IP</spbill_create_ip>" +//用戶IP
                "<time_start>START</time_start>" +//訂單開始時間
                "<time_expire>STOP</time_expire>" +//訂單結束時間
                "<goods_tag>WXG</goods_tag>" +//商品標記
                "<product_id>GOODID</product_id>" +//商品ID
                "<limit_pay>no_credit</limit_pay>" +//支付範圍,默認不支持信用卡支付
                "<out_trade_no>PAY_NO</out_trade_no>" +//商城生成的訂單號
                "<total_fee>TOTAL</total_fee>" +//總金額
                "<trade_type>TYPE</trade_type>" +//交易類型,JSAPI,NATIVE,APP,WAP
                "<sign>SIGN</sign>" +//加密字符串
                "</xml>";

        //生成訂單起始時間,訂單7天內有效
        DateFormat df = new SimpleDateFormat("yyyyMMddhhmmss");
        String start_time = df.format(new Date());
        String stop_time = df.format(new Date().getTime() + 7 * 24 * 60 * 60 * 1000);

        //xml數據封裝

        //APP調起的時候,可能和公衆號調起的商戶號是不同的,所以需要分開設置
        if ("APP".equalsIgnoreCase(type)) {
            xml = xml.replace("MERCHANT", app_mchid);
            xml = xml.replace("APPID", app_mchappid);
        } else {
            xml = xml.replace("MERCHANT", mchid);
            xml = xml.replace("APPID", mchappid);
        }
        xml = xml.replace("FFHH", randomNum);
        xml = xml.replace("DETAIL", detail);
        xml = xml.replace("BODY", desc);
        xml = xml.replace("URL_TO", wx_callback);
        xml = xml.replace("IP", ip);
        xml = xml.replace("START", start_time);
        xml = xml.replace("STOP", stop_time);
        xml = xml.replace("GOODID", goodSn);
        xml = xml.replace("PAY_NO", orderSn);
        xml = xml.replace("TOTAL", (int) relAmount + "");
        xml = xml.replace("TYPE", type);
        if ("NATIVE".equalsIgnoreCase(type) || "APP".equalsIgnoreCase(type)) {
            xml = xml.replace("<openid>UserFrom</openid>", openid);
        } else {
            xml = xml.replace("UserFrom", openid);
        }
        // 4、加密
        Map<String, String> map = new HashMap<String, String>();
        map.put("device_info", "WEB");
        map.put("detail", detail);
        map.put("body", desc);
        if ("APP".equalsIgnoreCase(type)) {
            map.put("mch_id", app_mchid);
            map.put("appid", app_mchappid);
        } else {

            map.put("mch_id", mchid);
            map.put("appid", mchappid);
        }

        map.put("nonce_str", randomNum);
        map.put("notify_url", wx_callback);
        map.put("fee_type", "CNY");
        map.put("spbill_create_ip", ip);
        map.put("time_start", start_time);
        map.put("time_expire", stop_time);
        map.put("goods_tag", "WXG");
        map.put("product_id", goodSn);
        map.put("limit_pay", "no_credit");
        map.put("out_trade_no", orderSn);
        map.put("total_fee", (int) relAmount + "");
        map.put("trade_type", type);

        String sign = SignatureUtils.signature(map, wx_key);
        xml = xml.replace("SIGN", sign);

        // 請求
        String response = "";
        try {
		    //注意,此處的httputil一定發送請求的時候一定要注意中文亂碼問題,中文亂碼問題會導致在客戶端加密是正確的,可是微信端返回的是加密錯誤
            response = HttpUtils.post(wx_order, xml);
        } catch (Exception e) {
            return result;
        }
        //處理請求結果
        XStream s = new XStream(new DomDriver());
        s.alias("xml", WechatOrder.class);
        WechatOrder order = (WechatOrder) s.fromXML(response);

        if ("SUCCESS".equals(order.getReturn_code()) && "SUCCESS".equals(order.getResult_code())) {
            //支付成功的處理邏輯
        } else {
            //支付失敗的處理邏輯
        }
		
        HashMap<String, String> back = new HashMap<String, String>();

        //生成客戶端調時需要的信息對象
       
        //APP調起的時候,請注意,安卓端不能用駝峯法,所有的key必須使用小寫
            String time = Long.toString(System.currentTimeMillis());
            back.put("appid", app_mchappid);
            back.put("timestamp", time);
            back.put("partnerid", app_mchid);
            back.put("noncestr", "5K8264ILTKCH16CQ2502SI8ZNMTM67VS");
            back.put("prepayid", order.getPrepay_id());
            back.put("package", "Sign=WXPay");
            String sign2 = SignatureUtils.signature(back, wx_key);

            JSONObject jsonObject = new JSONObject();
            jsonObject.put("appid", app_mchappid);
            jsonObject.put("timestamp", time);
            jsonObject.put("partnerid", app_mchid);
            jsonObject.put("noncestr", "5K8264ILTKCH16CQ2502SI8ZNMTM67VS");
            jsonObject.put("prepayid", order.getPrepay_id());
            //jsonObject.put("package", "Sign=WXPay");
            jsonObject.put("sign", sign2);
            result.put("status", "success");
            result.put("msg", "下單成功");
            result.put("obj", jsonObject);
            
	    return result;

    }

HttpUtils.post源碼:

public static String post(String url, String str)throws Exception {
		// 處理請求地址
		URI uri = new URI(url);
		HttpPost post = new HttpPost(uri);
		post.setEntity(new StringEntity(str,"utf-8"));
		// 執行請求
		HttpResponse response = client.execute(post);

		if (response.getStatusLine().getStatusCode() == 200) {
			// 處理請求結果
			StringBuffer buffer = new StringBuffer();
			InputStream in = null;
			try {
				in = response.getEntity().getContent();
				BufferedReader reader = new BufferedReader(
						new InputStreamReader(in));
				String line = null;
				while ((line = reader.readLine()) != null) {
					buffer.append(line);
				}

			} finally {
				// 關閉流
				if (in != null)
					in.close();
			}

			return buffer.toString();
		} else {
			return null;
		}

	}
PS:
微信回調,如果嚴謹的話可以加上金額的匹配,也就是除了驗證簽名,訂單號,還有金額,總之約多約嚴謹
簽名錯誤:如果報簽名錯誤,除了按照上面微信提供的驗證簽名工具,如果都一樣還報錯,一般通過在商戶平臺重置支付key 會解決
微信執行回調函數:服務器一定要是80端口,給微信的url地址不能包含參數,不用限制get還是post請求
在初期調試時,建議多打日誌信息,看看錯誤出在哪裏,畢竟需要和客戶端聯調,這樣會減少排錯時間













發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章