02_微信扫码支付接口开发

@Author wangwangjie
转载请标明出处~~~

1. 微信扫码支付快速接入

01.png
微信支付接入网址:https://pay.weixin.qq.com/index.php/partner/public/home
1. 与支付宝类似,微信扫码支付开发也需要进行申请,审核通过,微信会下发一个公众号appID与一个商户号mchID。
2. 生成一个32位的随机密码作为微信的秘钥,然后登陆微信公众号商户平台(就是上面的链接)设置API密钥。例如:057B07F6A839420C8110D60F728CB92
3. 有了appid,mchID,及key就可以进行开发了。
    微信支付开发者文档链接:https://pay.weixin.qq.com/wiki/doc/api/index.html

    3.1 下载微信支付的sdk:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=11_1,在上面开发者文档中都能找到.
        将sdk加入到自己的项目中。

    3.2 下面就可以参照sdk中的demo进行开发

这里主要列出支付下单的流程及代码:

微信支付业务流程时序图:
02.png
业务流程说明:
(1)商户后台系统根据用户选购的商品生成订单。
(2)用户确认支付后调用微信支付【统一下单API】生成预支付交易;
(3)微信支付系统收到请求后生成预支付交易单,并返回交易会话的二维码链接code_url。
(4)商户后台系统根据返回的code_url生成二维码。
(5)用户打开微信“扫一扫”扫描二维码,微信客户端将扫码内容发送到微信支付系统。
(6)微信支付系统收到客户端请求,验证链接有效性后发起用户支付,要求用户授权。
(7)用户在微信客户端输入密码,确认支付后,微信客户端提交授权。
(8)微信支付系统根据用户授权完成支付交易。
(9)微信支付系统完成支付交易后给微信客户端返回交易结果,并将交易结果通过短信、微信消息提示用户。微信客户端展示支付交易结果页面。
(10)微信支付系统通过发送异步消息通知商户后台系统支付结果。商户后台系统需回复接收情况,通知微信后台系统不再发送该单的支付通知。
(11)未收到支付通知的情况,商户后台系统调用【查询订单API】。
(12)商户确认订单已支付后给用户发货。


-------------------------------------------------------------------------------------------------------------

具体代码:

application.properties:
    #微信配置项
    wx_appID=wx123456789
    wx_mchID=xxxxxxx
    #微信支付秘钥,API根据key默认以MD5生成sign,预创建成功返回return_code为success时以key与默认MD5校验key
    wx_key=xxxxxxxxxxxxxxxxxxxxxxxxxx(32位随机数密钥)
    wx_notify_url=http://xx.com.cn/pay/wxNotify
    wx_spbill_create_ip=127.0.0.1
    #微信支付请求的主域名
    wx_domain=api.mch.weixin.qq.com
    #该域名是否为主域名
    wx_primaryDomain=true


自定义WXConfig继承WXPayConfig:
@Component
public class WXconfig extends WXPayConfig {
    @Value("${wx_appID}")
    private String appID;
    @Value("${wx_mchID}")
    private String mchID;
    @Value("${wx_key}")
    private String key;

    //这里要设置domain和primaryDomain,不设置跑起来会报空指针,并且对其源码里面的实现要做一部分变动,后面再详细说明
    @Value("${wx_domain}")
    public String domain;// 支付请求的域名
    @Value("${wx_primaryDomain}")
    private boolean primaryDomain;// 该支付请求域名是否是主域名

    @Override
    protected String getAppID() {
        // TODO Auto-generated method stub
        return appID;
    }
    @Override
    protected String getMchID() {
        // TODO Auto-generated method stub
        return mchID;
    }
    @Override
    protected String getKey() {
        // TODO Auto-generated method stub
        return key;
    }
    public String getDomain() {
        return domain;
    }
    public boolean isPrimaryDomain() {
        return primaryDomain;
    }
    @Override
    protected InputStream getCertStream() {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    protected IWXPayDomain getWXPayDomain() {
        return new WXPayDomain();
    }
}



@Autowired
private WXconfig wxconfig;

public DreamResponse unifiedOrder(IPayDTO dto) {
    DreamResponse res = new DreamResponse();
    CommonDTO real = (CommonDTO) dto;
    if (real.getEpcs() == null || real.getEpcs().size() == 0) {
        res.setMsg("标签列表不能为空!");
        res.setStatus(DreamStatus.FAIL);
        return res;
    }
    if (StringUtils.isEmpty(real.getMac())) {
        res.setMsg("设备地址不能为空!");
        res.setStatus(DreamStatus.FAIL);
        return res;
    }
    Map<String, String> resmap = new HashMap<String, String>();

    ===============================================微信支付部分================================================
    try {
        //构建WXPay的时候调用其构造方法,会将微信配置项wxconfig及签名类型sign_type给赋值,默认本地签名类型sign_type为:HMACSHA256
        WXPay wxpay = new WXPay(wxconfig);

        //Map集合用于存储请求参数执行微信支付,下面的value根据自己的业务设定,这里就不加以更改了
        Map<String, String> data = new HashMap<String, String>();
        StockVO stockVo = priceSearchService.getSKUPrice(real);//这里是商品集合
        data.put("body", stockVo.getListStock().get(0).getWarehouseName());
        data.put("out_trade_no", OrderUtil.generateOrderNo());
        data.put("device_info", real.getMac());
        data.put("fee_type", "CNY");

        // 注意:这里去除小数点,total_fee不支持小数点
        Double totalAmt = stockVo.getTotalAmt() * 100;// 订单总金额,单位为分
        String temp = totalAmt.toString();
        temp = temp.substring(0, temp.indexOf('.'));
        data.put("total_fee", temp);

        data.put("spbill_create_ip", wxbean.getSpbill_create_ip());
        data.put("notify_url", wxbean.getNotify_url());
        data.put("trade_type", "NATIVE"); // 此处指定为扫码支付
        // data.put("attach",wxbean.getOpenid());
        data.put("product_id", OrderUtil.generateOrderNo("", 9));// 生成一个id
        data.put("time_start", DateUtils.date2String(new Date(), "yyyyMMddHHmmss"));
        data.put("limit_pay", "no_credit");
        //data.put(WXPayConstants.FIELD_SIGN_TYPE, WXPayConstants.HMACSHA256);
        ========================================微信支付请求参数封装结束=========================================

        Map<String, Object> mapdto = new HashMap<String, Object>();
        mapdto.put("epclist", real.getEpcs());
        mapdto.put("out_trade_no", data.get("out_trade_no"));
        mapdto.put("device_info", real.getMac());
        mapdto.put("payment_type", DreamStatus.WX);
        mapdto.put("stockVo", stockVo);
        // 记录订单流水
        orderService.add(mapdto);

        ========================================发起微信支付===================================================
        logger.info("****微信下订单入参:" + new Gson().toJson(data));
        resmap = wxpay.unifiedOrder(data);
        resmap.put("out_trade_no", data.get("out_trade_no"));
        logger.info("****微信下订单返回结果:" + new Gson().toJson(resmap));
        if (DreamStatus.SUCCESS.equals(resmap.get("return_code"))
                && DreamStatus.SUCCESS.equals(resmap.get("result_code"))) {
            res.setMsg("微信支付下订单成功");
            res.setStatus(DreamStatus.SUCCESS);                 
            ......具体业务实现

        } else {
            res.setExtData(resmap);
            res.setMsg("微信支付下订单失败");
            res.setStatus(DreamStatus.FAIL);
        }
    } catch (Exception e) {
        e.printStackTrace();
        logger.warn("微信支付下订单失败", e);
        res.setExtData(resmap);
        res.setMsg("下单失败");
        res.setStatus(DreamStatus.FAIL);
    }
    return res;
}

以上,domain与primaryDomain是在程序运行起来发现问题后加入的,查看源码发现,必须给定这两个值。
并且在继承WXPayConfig的WxConfig的类中加入重写的方法:(对于getWXPayDomain还得注意修改其实现)
    public String getDomain() {
        return domain;
    }
    public boolean isPrimaryDomain() {
        return primaryDomain;
    }
    @Override
    protected InputStream getCertStream() {
        // TODO Auto-generated method stub
        return null;
    }
    @Override
    protected IWXPayDomain getWXPayDomain() {
        return new WXPayDomain();
    }


跟入源码发现,wxpay.unifiedOrder(data)底层将我作为构造参数的config内的配置项appid,mchid,key都设置入data了,并且会生成sign存入data,
并且我们无需制定访问路径,调用对应的方法,底层会指定调用的接口。

继续往下走,我们发现这里调用了我们在new WXPay(config)中config.getWXPayDomain().getDomain(config)方法。如果不加以配置就会报空指针。
03.png
继续跟入这个get方法,我们在上面return new WXPayDomain();至于为什么要这样,跟入getDomain(config)研究就可以得到。
04.png
到了这里,具体的原因就不叙述了,看代码体会,我们需要自定义一个类来继承IWXPayDomain重新其getDomain()方法。

public class WXPayDomain implements IWXPayDomain {

  @Override
  public void report(String domain, long elapsedTimeMillis, Exception ex) {
    // TODO Auto-generated method stub
  }

  @Override
  public DomainInfo getDomain(WXPayConfig config) {
    WXconfig wxConfig = (WXconfig)config;
    return new DomainInfo(wxConfig.getDomain(), wxConfig.isPrimaryDomain());
  }
}

至此,微信扫码支付开发就完成了,最后domain这里是个坑,需要看源码自己去体会。

总结:

1. 自定义WXConfig继承WXPayConfig,读取微信配置项
2. 给定支付接口所需的请求参数,用wxpay.unifiedOrder(data)进行支付
3. 对于domain要做一下几个地方的改动。
    1. 配置项的改动:这里为什么这么配通过看源码可以得到
        domain=api.mch.weixin.qq.com 
        primaryDomain=true

    2. WXConfig中读取domain,primaryDomain配置项,并重写getDomain(),isPrimaryDomain(),getWXPayDomain()方法
    3. 自定义类(我这里叫WXPayDomain继承IWXPayDomain),重写getDomain()方法

2. 微信扫码支付成功回调接口开发

与支付宝支付成功后的异步回调一样,需要登录商户平台找到开发配置,设置notify_url回调ip即可。这里要注意的是:微信支付的notify_url只支持域名。
如:http://wwj.com.cn/pay/wxNotify,不像支付宝ip也可以

注意点:
1. 微信支付回调返回的xml数据,并且是以流的方式返回的,因此我们收取数据的时候也必须以流的方式获取:
2. 微信支付返回的xml数据中没有sign_type这一字段,而在本地验签时,我们需要采用HMACSHA256(与下单时一致)进行验签,因此要手动设置到map中去进行验签。
    跟入源码发现,map有sign_type为我们需要采用HMACSHA256时验签类型才为HMACSHA256,否则就是MD5.
    但如果只是这样处理,最后会出现验签失败,之前支付宝支付也说明了验签的原理,是通过私钥与request中的参数进行签名后和原本的签名sign进行对比,
    如今你手动多加了一个字段,那么签名后肯定不同,因此要在源码中,sign_type确定后,将sign_type再重新移除掉即可(这是我想到的做法,可能不是妥当)

微信支付成功的回调代码:
public String notify() {
    Map<String, Object> map = new HashMap<String, Object>();
    Map<String, String> res = new HashMap<String, String>();

    =======================================微信支付回调======================================
    String xml = "";
    String notifyXml = "";
    // 微信支付结果参数
    Map<String, String> paramMap = null;
    WxPayDTO real = null;

    try {
        // 获取微信支付成功后回调POST反馈的信息
        logger.info("微信支付回调获取数据开始...");

        //以流的方式读取Request中的数据
        notifyXml = RequestUtil.getRequestBodyByReader(request);
        logger.debug("微信支付回调参数xml格式:" + notifyXml);

        if (StringUtils.isEmpty(notifyXml)) {
            logger.error("微信支付回调参数xml为空");
        }

        //下面这一段是将获取到的xml数据封装到实体类中,实体类WxPayDTO中对应的字段与官方API支付成功后的响应参数必须一致
        paramMap = WXPayUtil.xmlToMap(notifyXml);
        String paramJson = new Gson().toJson(paramMap);
        logger.info("微信支付回调参数json格式" + paramJson);
        real = new Gson().fromJson(paramJson, WxPayDTO.class);

        WXPay wxpay = new WXPay(wxconfig);
        logger.info("微信通知成功:" + paramJson);

        if (isPay(real.getOut_trade_no()) || null == real) {// 已通知过,无参数无需通知
            res.put("return_code", DreamStatus.SUCCESS);
            res.put("return_msg", "OK");
            xml = WXPayUtil.mapToXml(res);
            return xml;
        }

        
        //指定验签方式与下单时的提交到微信服务端的sign_type一致,为HMACSHA256
        paramMap.put(WXPayConstants.FIELD_SIGN_TYPE, WXPayConstants.HMACSHA256);
        if (wxpay.isPayResultNotifySignatureValid(paramMap)) {
            =========================微信回调基本代码到此结束,下面是具体业务的实现====================

            // 签名正确
            logger.info("微信验签成功:" + new Gson().toJson(paramMap));
            if (DreamStatus.SUCCESS.equals(real.getResult_code())
                    && DreamStatus.SUCCESS.equals(real.getReturn_code())) {
                //更新订单状态
                map.put("out_trade_no", real.getOut_trade_no());
                map.put("status", DreamStatus.SUCCESS);
                map.put("type", MessageType.PAY_OVER);
                orderService.update(map);

                res.put("return_code", DreamStatus.SUCCESS);
                res.put("return_msg", "OK");
            } else {
                res.put("return_code", DreamStatus.FAIL);
                res.put("return_msg", real.getErr_code_des());
            }
        } else {
            // 签名失败
            logger.info("微信验签失败:" + new Gson().toJson(paramMap));
            map.put("out_trade_no", real.getOut_trade_no());
            map.put("status", DreamStatus.FAIL);
            orderService.update(map);
            res.put("return_code", DreamStatus.FAIL);
            res.put("return_msg", "fail");
        }
    } catch (Exception e) {
        e.printStackTrace();
        res.put("return_code", DreamStatus.FAIL);
        res.put("return_msg", "fail");
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        logger.error("微信消息通知异常", e);
    }
    try {
        xml = WXPayUtil.mapToXml(res);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return xml;
}


/**   
 * @Title: getRequestBodyByReader   
 * @Description: TODO(获取微信回调函数参数,返回xml)   
 * @param: @param request
 * @param: @return
 * @param: @throws IOException      
 * @return: String      
 * @throws   
 */ 
public static String getRequestBodyByReader(HttpServletRequest request) throws IOException {
    String tempLine;
    String result = "";
    try {
        if(request != null) {
            while ((tempLine = request.getReader().readLine()) != null) {
                result += tempLine;
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
        throw e;
    } finally {
        try {
            if(request.getReader() != null) {
                request.getReader().close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return result;
}


用于封装Request返回参数的实体类,与API响应参数保持一致:
public class WxPayDTO implements IPayDTO, Serializable {
    private String device_info;// 设备号

    private String openid;// 用户标识

    private String appid;// 公众账号ID

    private String mch_id;// 商户号

    private String nonce_str;// 随机字符串

    private String sign;// 签名

    private String sign_type;// 签名类型

    private String result_code;// 业务结果

    private String err_code;// 错误代码

    private String err_code_des;// 错误代码描述

    private String is_subscribe;// 是否关注公众账号

    private String trade_type;// 交易类型

    private String bank_type;// 付款银行

    private int total_fee;// 订单金额

    private int settlement_total_fee;// 应结订单金额

    private int cash_fee;// 现金支付金额

    private String transaction_id;// 微信支付订单号

    private String out_trade_no;// 商户订单号

    private String time_end;// 支付完成时间

    private String return_code;// 返回状态码

    private String return_msg;// 返回信息

    ............
}   


跟入wxpay.isPayResultNotifySignatureValid(paramMap)验签方法,修改isSignatureValid()方法,移除map中的sign_type字段

/**
 * 判断签名是否正确,必须包含sign字段,否则返回false。
 *
 * @param data
 *            Map类型数据
 * @param key
 *            API密钥
 * @param signType
 *            签名方式
 * @return 签名是否正确
 * @throws Exception
 */
public static boolean isSignatureValid(Map<String, String> data, String key, SignType signType) throws Exception {
    if (!data.containsKey(WXPayConstants.FIELD_SIGN)) {
        return false;
    }
    String sign = data.get(WXPayConstants.FIELD_SIGN);
    logger.info("原签名为:" + sign + ",本地验签类型为:" + signType);
    //为了保证签名类型是HMACSHA256手动设置了sign_type,为了保证验签通过,移除sign_type
    data.remove(WXPayConstants.FIELD_SIGN_TYPE);
    String generateSignature = generateSignature(data, key, signType);
    logger.info("验签后>>>" + generateSignature);
    return generateSignature.equals(sign);
}

到此,微信支付成功回调接口开发完成,开发途中会遇到很多坑,慢慢解决就好了~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容