SSM實現(xiàn)微信JSAPI支付以及退款

??微信支付方式有很多,這里主要記錄網(wǎng)頁微信支付喚起以及退款的主要流程澎迎,微信JSAPI支付必傳openid乱投,這里需要實現(xiàn)網(wǎng)頁授權(quán)獲取,網(wǎng)頁授權(quán)先獲取用戶授權(quán)的code铛嘱,然后拿code去換取access_token暖释,基本實現(xiàn)可以參考PHP實現(xiàn)獲取微信網(wǎng)頁授權(quán),獲取用戶信息

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

1.微信公眾號后臺配置業(yè)務(wù)域名和網(wǎng)頁授權(quán)域名


配置業(yè)務(wù)域名和網(wǎng)頁授權(quán)域名

2.微信支付平臺的產(chǎn)品中心綁定APPID和開發(fā)配置域名


綁定APPID和開發(fā)配置域名

3.下載微信退款的證書工具
微信退款的校驗證書

4.使用證書工具生成相應(yīng)證書(1年有效期墨吓,注意及時更換)


證書文件

5.下載Java的SDK與DEMO下載
SDK與DEMO下載

以下附上開發(fā)代碼

前端默認(rèn)頁面訪問路徑

https://open.weixin.qq.com/connect/oauth2/authorize?appid=你的appId&redirect_uri=URLEncoder.encode(你的網(wǎng)頁地址&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect

前端獲取url中的code通過ajax來換取openId
    /**
     *  code換取用戶的open_id
     */
    @RequestMapping(value = "getOpenid.htm")
    @ResponseBody
    public JSONObject getOpenid(HttpServletRequest request){
        String code = request.getParameter("code");
        String user_id = request.getParameter("user_id");
        JSONObject jsonObject = new JSONObject();
        if (RedisUtil.getString("is_web_access_token_valid" + user_id) != null) {
            jsonObject = JSONObject.parseObject(RedisUtil.getString("is_web_access_token_valid" + user_id));
        }else{
            String app_secrect = WechatUtil.app_secrect;
            String app_id = WechatUtil.app_id;
            String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
            jsonObject = HttpUtil.doGet(url.replace("APPID", app_id).replace("SECRET", app_secrect).replace("CODE", code));
            RedisUtil.setString("is_web_access_token_valid" + user_id, jsonObject.toJSONString(), 7000);
        }

        return jsonObject;
    }
微信繼承SDK的工共配置文件
package com.shadmin.common.util;

import com.shadmin.common.util.wxpay.IWXPayDomain;
import com.shadmin.common.util.wxpay.WXPayConfig;
import com.shadmin.common.util.wxpay.WXPayConstants;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.math.BigDecimal;

public class WechatPayUtil extends WXPayConfig {

    private byte[] certData;

    public WechatPayUtil() throws Exception {
        // 獲取證書存放的路徑(放在SSM項目根目錄)
        String certPath =  Thread.currentThread().getContextClassLoader().getResource("1283062301_20181206_cert.p12").toString().replace("file:","");
        // certPath = "/E:/myjava/sy_shadmin/target/sy_shadmin/WEB-INF/classes/1283062301_20181206_cert.p12";
        File file = new File(certPath);
        InputStream certStream = new FileInputStream(file);
        this.certData = new byte[(int) file.length()];
        certStream.read(this.certData);
        certStream.close();
    }

    public String getAppID() {
        return "你的AppID";
    }

    public String getMchID() {
        return "你的商戶ID";
    }

    public String getKey() {
        return "你的商戶支付密鑰";
    }

    public InputStream getCertStream() {
        ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);
        return certBis;
    }

    public int getHttpConnectTimeoutMs() {
        return 8000;
    }

    public int getHttpReadTimeoutMs() {
        return 10000;
    }

    @Override
    protected IWXPayDomain getWXPayDomain() {
        IWXPayDomain iwxPayDomain = new IWXPayDomain() {
            @Override
            public void report(String domain, long elapsedTimeMillis, Exception ex) {

            }
            @Override
            public DomainInfo getDomain(WXPayConfig config) {
                return new IWXPayDomain.DomainInfo(WXPayConstants.DOMAIN_API, true);
            }
        };
        return iwxPayDomain;
    }


    /**
     * 元轉(zhuǎn)分(微信支付以分為單位)
     * @param yuan
     * @return
     */
    public static String Yuan2Fen(String yuan) {
        return new BigDecimal(yuan).movePointRight(2).toString();
    }

    /**
     * 分轉(zhuǎn)元
     * @param fen
     * @return
     */
    public static String Fen2Yuan(String fen) {
        return new BigDecimal(fen).movePointLeft(2).toString();
    }
}
微信統(tǒng)一下單
/**
     * 獲取微信支付參數(shù)
     */
    public JSONObject getWechatPayData(JSONObject param) throws Exception {
        WechatPayUtil wechatPayUtil = new WechatPayUtil();
        WXPay wxpay = new WXPay(wechatPayUtil);
        Map<String, String> data = new HashMap<String, String>();
        data.put("body", param.getString("goods_name"));
        data.put("out_trade_no", System.currentTimeMillis() + UUID.randomUUID().toString().replace("-", "").substring(0,5));
        data.put("device_info", param.getString("user_id"));
        data.put("fee_type", "CNY");
        data.put("total_fee", WechatPayUtil.Yuan2Fen(param.getString("total_fee")));
        data.put("spbill_create_ip", param.getString("spbill_create_ip"));
        data.put("notify_url", "你的微信支付服務(wù)器回調(diào)地址");
        data.put("trade_type", "JSAPI");  // 此處指定為JSAPI支付
        data.put("openid", param.getString("open_id"));
        Map<String, String> resp = wxpay.unifiedOrder(data);
        JSONObject jsonObject = JSONObject.parseObject(JSONObject.toJSONString(resp));
        //新增返回值球匕,方便模擬微信回調(diào)
        jsonObject.put("out_trade_no", data.get("out_trade_no"));
        jsonObject.put("spbill_create_ip", data.get("spbill_create_ip"));
        //新增支付等待有效期,方便定時任務(wù)掃描濾除緩存中過期未支付訂單
        jsonObject.put("time_expire",System.currentTimeMillis());
        //新增訂單編號帖烘,方便定時任務(wù)掃描濾除數(shù)據(jù)庫中過期未支付訂單
        jsonObject.put("out_trade_no", data.get("out_trade_no"));
        return jsonObject;
    }
后臺將微信統(tǒng)一下單的返回值拼接成前段調(diào)用的參數(shù)(注意:微信統(tǒng)一下單的sign并不等同于前端微信支付喚起的paySign亮曹,否則會報錯chooseWXPay:fail the permission value is offline verifying)
    /**
     * 查詢是否有支付資格
     * @param request
     * @return
     */
    @RequestMapping(value = "checkIsPaying.htm")
    @ResponseBody
    public JSONObject checkIsPaying(HttpServletRequest request) throws Exception {

        String user_id = request.getParameter("user_id");
        JSONObject jsonObject = new JSONObject();
        //獲取搶購的庫存總數(shù)
        Object store_num_object = RedisUtil.getMapKey("second_kill_activity_goods_info", "store_num");
        int store_num = (int) ((store_num_object !=null)? store_num_object :0);
        //判斷是否搶購失敗
        if (RedisUtil.getMapSize("second_kill_order") < store_num) {
            //從redis的order_to_pay的map中讀取對應(yīng)的值
            if (RedisUtil.getMapKey("second_kill_pay", user_id) != null) {
                JSONObject pay_detail = JSONObject.parseObject(RedisUtil.getMapKey("second_kill_pay", user_id).toString());
                //根據(jù)微信支付參數(shù)增添返回值
                HashMap<String, String> map = new HashMap<>();
                map.put("appId", pay_detail.getString("appid"));
                map.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
                map.put("nonceStr", pay_detail.getString("nonce_str"));
                map.put("package", "prepay_id=" + pay_detail.getString("prepay_id"));
                map.put("signType", "MD5");
                //調(diào)用微信SDK自帶的生成簽名的方法生成paySign
                WechatPayUtil payUtil = new WechatPayUtil();
                map.put("paySign", WXPayUtil.generateSignature(map, payUtil.getKey()));
                map.put("out_trade_no", pay_detail.getString("out_trade_no"));
                jsonObject.put("code", 1);
                jsonObject.put("data", map);
                jsonObject.put("message", "訂單已生成,請支付秘症!");
            } else {
                jsonObject.put("code", 0);
                jsonObject.put("message", "搶購排隊中照卦,請稍后嘗試!");
            }
        }else{
            jsonObject.put("code", -1);
            jsonObject.put("message", "搶購失敗乡摹,搶購已結(jié)束役耕!");
        }

        return jsonObject;
    }
獲取微信支付設(shè)備的ip地址
package com.shadmin.common.util;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;


/**
 * 工具類
 * @author Administrator
 *
 */
@SuppressWarnings("all")
public class ToolUtil {

    /**
     * 獲取客戶端IP
     * @param request
     * @return
     */
    public static String getIpAddr(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }

        if (ip.equals("127.0.0.1") || ip.endsWith("0:0:0:0:0:0:0:1")) {
            ip = "127.0.0.1";
        }

        if (ip != null && ip.length() > 15) { // "***.***.***.***".length()= 15
            if (ip.indexOf(",") > 0) {
                ip = ip.substring(0, ip.indexOf(","));
            }
        }
        return ip;

    }

    /**
     * 把request轉(zhuǎn)為map
     *
     * @param request
     * @return
     */
    public static Map<String, Object> getParameterMap(HttpServletRequest request) {
        // 參數(shù)Map
        Map<?, ?> properties = request.getParameterMap();
        // 返回值Map
        Map<String, Object> returnMap = new HashMap<String, Object>();
        Iterator<?> entries = properties.entrySet().iterator();

        Map.Entry<String, Object> entry;
        String name = "";
        String value = "";
        Object valueObj =null;
        while (entries.hasNext()) {
            entry = (Map.Entry<String, Object>) entries.next();
            name = (String) entry.getKey();
            valueObj = entry.getValue();
            if (null == valueObj) {
                value = "";
            } else if (valueObj instanceof String[]) {
                String[] values = (String[]) valueObj;
                for (int i = 0; i < values.length; i++) {
                    value = values[i] + ",";
                }
                value = value.substring(0, value.length() - 1);
            } else {
                value = valueObj.toString();
            }
            returnMap.put(name, value);
        }
        return returnMap;
    }
}
微信支付回調(diào)處理
    /**
     * 處理微信付款回調(diào)
     * 1.獲取微信的回調(diào)參數(shù)  return_code 返回狀態(tài)碼   SUCCESS
     * 2.獲取second_kill_pay中對應(yīng)user_id的參數(shù)
     * 3.根據(jù)回調(diào)結(jié)果判斷是否訂單成功
     * 4.成功——添加到second_kill_order表
     * 5.失敗——觸發(fā)退款機制
     * 6.刪除second_kill_pay隊列數(shù)據(jù)
     * 7.返回給微信回調(diào)應(yīng)答
     * @return
     */
    @RequestMapping("second_kill_notify.htm")
    public void second_kill_notify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //響應(yīng)微信回調(diào),默認(rèn)失敗
        String notify_xml_response = resFailXml;
        // 1.獲取微信支付回調(diào)xml
        InputStream inputStream = request.getInputStream();
        ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int length = 0;
        while ((length = inputStream.read(buffer)) != -1) {
            outSteam.write(buffer, 0, length);
        }
        outSteam.close();
        inputStream.close();
        String resultXml = new String(outSteam.toByteArray(), "utf-8");
        logger.info("微信支付回調(diào):----result----:" + resultXml);
        WechatPayUtil config = new WechatPayUtil();
        WXPay wxpay = new WXPay(config);
        Map<String, String> notifyMap = WXPayUtil.xmlToMap(resultXml);  // 轉(zhuǎn)換成map
        //  2.獲取second_kill_pay中對應(yīng)user_id的參數(shù),user_id對應(yīng)notifyMap中的device_info,下單時用device_info存放user_id
        String user_id = notifyMap.get("device_info");
        // 獲取對應(yīng)訂單out_trade_no的訂單second_order_pay值
        JSONObject pay_detail = JSONObject.parseObject(RedisUtil.getMapKey("second_kill_pay", user_id).toString());
        // 簽名正確并且是首次調(diào)用
        if (wxpay.isPayResultNotifySignatureValid(notifyMap) && pay_detail.getString("out_trade_no").equals(notifyMap.get("out_trade_no"))) {
            //獲取second_kill_activity_goods_info中的對應(yīng)的秒殺價格參數(shù)
            String total_fee = RedisUtil.getMapKey("second_kill_activity_goods_info", "sale_price").toString();
            HashMap<Object, Object> map = new HashMap<>();
            map.put("pay_amount", WechatPayUtil.Fen2Yuan(notifyMap.get("total_fee")));
            map.put("out_trade_no", notifyMap.get("out_trade_no"));
            map.put("transaction_id", notifyMap.get("transaction_id"));
            map.put("update_time", df.format(new Date()));
            //  3.根據(jù)回調(diào)結(jié)果判斷是否訂單成功(total_fee支付金額一致聪廉,bank_type銀行類型必須為建行信用卡CCB_CREDIT)
            if (notifyMap.get("bank_type").equals("CCB_CREDIT") && WechatPayUtil.Fen2Yuan(notifyMap.get("total_fee")).equals(total_fee)) {
                //  4.成功——添加到second_kill_order表,并將該訂單寫入成功隊列
                map.put("status", "1");     //status 1-支付成功
                secondKillOrderListService.updateOrderList(map);
                //  往成功訂單列表second_kill_order插入該user_id
                RedisUtil.addMap("second_kill_order", user_id, map);

            } else {
                //  5.失敗——觸發(fā)退款機制蹄葱,并將失敗訂單寫入訂單表
                Map<String, String> data = new HashMap<String, String>();
                data.put("out_trade_no", notifyMap.get("out_trade_no"));
                data.put("out_refund_no", System.currentTimeMillis() + UUID.randomUUID().toString().replace("-", "").substring(0,18));
                data.put("total_fee", WechatPayUtil.Yuan2Fen(total_fee));
                data.put("refund_fee", notifyMap.get("total_fee"));
                data.put("refund_desc", "付款金額不一致或者非建行信用卡支付!");
                Map<String, String> refund_result = wxpay.refund(data);
                map.put("status", "2");     //status 2-已退款
                map.put("out_refund_no", data.get("out_refund_no"));
                map.put("pay_amount", WechatPayUtil.Fen2Yuan(notifyMap.get("total_fee")));
                secondKillOrderListService.updateOrderList(map);
                // 失敗的是后刪除排隊列表的信息锄列,方便再次發(fā)起搶購
                RedisUtil.delMapKey("second_kill_queue_data", user_id);
                logger.info("訂單支付回調(diào)刪除second_kill_queue_data:user_id="+user_id);
            }
            //  6.無論成功與否图云,刪除second_kill_pay隊列數(shù)據(jù)
            RedisUtil.delMapKey("second_kill_pay", user_id);
            logger.info("訂單支付回調(diào)刪除second_kill_pay:user_id="+user_id);
            //  7.響應(yīng)微信回調(diào),支付成功
            notify_xml_response = resSuccessXml;
        }
        //  7.返回給微信回調(diào)應(yīng)答
        response.setContentType("text/xml");
        response.getWriter().write(notify_xml_response);
        response.flushBuffer();
    }
微信的支付DEMO已經(jīng)很齊全了邻邮,所有微信支付開放的接口都可以參照README.MD去一一實現(xiàn)竣况,

這里重點說兩點:
1.統(tǒng)一下單接口的sign不等同于喚起微信支付時的paySign,否則手機微信端會看到微信支付閃一下就沒了筒严,微信開發(fā)者工具提示chooseWXPay:fail the permission value is offline verifying
2.JAVA的SDK支付實際下單時(區(qū)別與沙箱模式)默認(rèn)是使用HMAC-SHA256方式加密丹泉,
但是在微信支付回調(diào)時校驗回調(diào)簽名默認(rèn)使用MD5情萤,所以可以將WXPAY文件的45行改成

this.signType = SignType.MD5; // 指定為Md5加密

3.高并發(fā)生成唯一訂單號的方式

System.currentTimeMillis() + UUID.randomUUID().toString().replace("-", "").substring(0,18)

4.微信證書配置的位置在SSM項目的src根目錄下,獲取該路徑的方式

String certPath = Thread.currentThread().getContextClassLoader().getResource("1283062301_20181206_cert.p12").toString().replace("file:","");
// certPath = "/E:/myjava/sy_shadmin/target/sy_shadmin/WEB-INF/classes/1283062301_20181206_cert.p12";

以下附上參考鏈接

微信支付喚起報錯:the permission value is offline verifying
微信統(tǒng)一下單接口說明

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末摹恨,一起剝皮案震驚了整個濱河市筋岛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌晒哄,老刑警劉巖睁宰,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異寝凌,居然都是意外死亡柒傻,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門较木,熙熙樓的掌柜王于貴愁眉苦臉地迎上來红符,“玉大人,你說我怎么就攤上這事伐债≡ず睿” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵峰锁,是天一觀的道長雌桑。 經(jīng)常有香客問我,道長祖今,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任拣技,我火速辦了婚禮千诬,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘膏斤。我一直安慰自己徐绑,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布莫辨。 她就那樣靜靜地躺著傲茄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪沮榜。 梳的紋絲不亂的頭發(fā)上盘榨,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天,我揣著相機與錄音蟆融,去河邊找鬼草巡。 笑死,一個胖子當(dāng)著我的面吹牛型酥,可吹牛的內(nèi)容都是我干的山憨。 我是一名探鬼主播查乒,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼郁竟!你這毒婦竟也來了玛迄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤棚亩,失蹤者是張志新(化名)和其女友劉穎蓖议,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蔑舞,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡拒担,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了攻询。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片从撼。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖钧栖,靈堂內(nèi)的尸體忽然破棺而出低零,到底是詐尸還是另有隱情,我是刑警寧澤拯杠,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布掏婶,位于F島的核電站,受9級特大地震影響潭陪,放射性物質(zhì)發(fā)生泄漏雄妥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一依溯、第九天 我趴在偏房一處隱蔽的房頂上張望老厌。 院中可真熱鬧,春花似錦黎炉、人聲如沸枝秤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽淀弹。三九已至,卻和暖如春庆械,著一層夾襖步出監(jiān)牢的瞬間薇溃,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工缭乘, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留痊焊,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像薄啥,于是被迫代替她去往敵國和親辕羽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

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