java微信公众号发送现金红包,包括查询红包记录(子商户模式)

发布时间:2019-05-20
技术:springboot2

概述

本demo,实现通过调用微信api接口发送公众号现金红包以及查询红包状态功能,应用场景是服务商替子商户发送现金红包(子商户模式)。普通商户模式也能使用,接口一致,参数调整一下就可以。所有功能代码都已封装好,替换完参数就可以直接运行。

详细

一、前言

微信红包分为普通红包和裂变红包,发放方式分为普通商户模式以及子商户模式,下面代码是以子商户模式发放普通红包写的。具体怎么选择看业务场景,代码原理是一样的,只是接口不一致,传参不同。

微信红包默认为1到200元,场景下为1-499元(调接口时参数必须传场景id),这些只需要平台设置,不需要申请。

其最小额度为0.3元,最大额度为4999元,这些需要向微信申请。(不一定能申请下来)

读代码之前最好对服务商,普通商户,特约商户等有个基本概念。最好先研究一遍文档,微信的文档写的还是很详细的。实在不明白可以打客服电话。


二、先看效果图

      1558080844676079745.jpg            1558081042149001839.jpg

三、准备工作


开始前一定要准备好测试条件:

1、已有服务商账号,必须已经开通了服务商现金红包功能。产品中心--特约商户授权产品--运营工具--服务商现金红包

image.png

2、已申请特约商户,必须已经开通了现金红包功能。产品中心--我的产品--运营工具--现金红包

image.png

3、服务商和特约商户完成绑定授权

详细流程见开发文档: https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon_sl.php?chapter=13_3&index=2

4、获取重要参数

包括:服务商id、服务商对应公众号appid、api密钥(服务商平台--账户中心--api安全--api密钥)、

api证书、特约商户号(子商户号)、子商户绑定的公众号appid、用户openid(子商户公众号下的)

1558077560606087518.png

1558077671674044105.png

关于准备工作的注意事项:

1、特约商户的申请以及授权绑定都需要审核,大概一到三天。

2、特约商户开通现金红包功能是有要求的,T+7账户能立即开通,其余的需要有90天的入驻以及30天的连续流水交易才能开通

3、特约商户绑定公众号时,两者主体必须一致,如果公众号绑定过其他商户,则特约商户的费率必须和这个商户一致

4、用户必须关注特约商户绑定的公众号,红包也是从这个公众号发出,钱是从特约商户账户出,红包发送记录是在服务商后台查

四、程序实现

项目代码截图:

1558078599730006502.png

以下摘部分重要代码说明过程:

1、服务商相关配置 application.yml

wx:
  # 服务商id
  mch_id: "153xxx9191"
  # 服务商appid(公众号id)
  wxappid: "wx3bcxxxxxx9d4163b"
  #调用接口的机器IP地址(真实ip  百度搜出来的ip)
  client_ip: "61.xxx.xxx.178"
  # api秘钥 微信商户平台-->账户设置-->API安全-->密钥设置
api-key: "zxcvbnmasdfghjklqwertyuiop123456"
# API证书
certFile: "wx_153xxx9191_cert.p12"

2、服务商对应的配置类

package com.sylujia.wxpay;

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

/**
 * @Author jia
 * @Date 2019/5/17 12:20
 * @Description
 */
@Data
@Component
public class WxConfig {

    /**
     * 服务商id
     */
    @Value("${wx.mch_id}")
    private String mchId;

    /**
     * 服务商appid
     */
    @Value("${wx.wxappid}")
    private String wxappid;

    /**
     * 调用接口的机器IP地址
     */
    @Value("${wx.client_ip}")
    private String clientIp;

    /**
     * api秘钥 微信商户平台-->账户设置-->API安全-->密钥设置
     */
    @Value("${wx.api-key}")
    private String apiKey;

    /**
     * API证书
     */
    @Value("${wx.certFile}")
    private String certFile;

}

3、调用测试类

package com.sylujia.wxpay;

import com.sylujia.wxpay.entity.WxPayReqBO;
import com.sylujia.wxpay.service.WxPayService;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class WxpayRedpackageApplicationTests {

    @Autowired
    private WxPayService wxPayService;

    @Test
    public void sendRedPackTest() {

        WxPayReqBO wxPayReqBO = new WxPayReqBO();

        //微信公众号下的用户唯一标示(msgAppid公众号下对应的用户openid)
        wxPayReqBO.setReOpenid("osuL9szZugvO1vkwJGKNYGb9cNT8");
        //子商户号(特约商户)
        wxPayReqBO.setSubMchId("153xxx651");
        //子商户绑定的公众号APPID
        wxPayReqBO.setMsgAppid("wx3d9xxxxxx5741bf4");
        //发送方名字
        wxPayReqBO.setSendName("返现测试");
        //红包金额 单位分
        wxPayReqBO.setTotalAmount(100);
        //红包总数
        wxPayReqBO.setTotalNum(1);
        //红包祝福语
        wxPayReqBO.setWishing("红包祝福语");
        //活动名称
        wxPayReqBO.setActName("活动");
        //备注
        wxPayReqBO.setRemark("红包备注");
        //使用原订单号重发
//        wxPayReqBO.setMchBillNo("1557727546317cfysmet0xptt9sp");

        String res = wxPayService.sendRedPack(wxPayReqBO);
        log.info("红包发送返回:{}",res);
    }



    @Test
    public void queryRedPackInfoTest(){
        String resXml = wxPayService.queryRedPackInfo("1558073129334pqbc9nki4tvr9jl");
        log.info("查询红包记录返回:{}", resXml);
    }



}

4、服务实现类,包含发送红包服务、以及红包状态查询服务

package com.sylujia.wxpay.service.impl;

import com.sylujia.wxpay.WxConfig;
import com.sylujia.wxpay.entity.RedPackQueryEntity;
import com.sylujia.wxpay.entity.RedPackSendEntity;
import com.sylujia.wxpay.entity.WxPayReqBO;
import com.sylujia.wxpay.service.WxPayService;
import com.sylujia.wxpay.utils.HttpClientSSL;
import com.sylujia.wxpay.utils.WeiXinUtil;
import com.sylujia.wxpay.utils.XmlUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author jia
 * @createTime 2019-05-09
 * @description 微信支付服务实现
 */
@Slf4j
@Service
public class WxPayServiceImpl implements WxPayService {

    @Autowired
    private HttpClientSSL httpClientSSL;

    @Autowired
    private WxConfig wxConfig;

    /**
     * 红包发送url
     */
    private final String SEND_URL = "https://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack";
    /**
     * 查询红包记录url
     */
    private final String QUERY_URL = "https://api.mch.weixin.qq.com/mmpaymkttransfers/gethbinfo";

    @Override
    public String sendRedPack(WxPayReqBO wxPayReqBO) {
        log.info("红包发送实体,wxPayReqBO:{}", wxPayReqBO);
        try {
            /** 封装发送红包实体**/
            RedPackSendEntity redPackageEntity = transferRedPackSendEntity(wxPayReqBO);

            /**获取xml格式请求体**/
            XmlUtils.xstream().autodetectAnnotations(true);
            XmlUtils.xstream().alias("xml", redPackageEntity.getClass());
            String xmlData = XmlUtils.xstream().toXML(redPackageEntity);
            /**http请求加证书 发送请求**/
            String resXml = httpClientSSL.sendWX(SEND_URL, xmlData, httpClientSSL.defaultSSLClientFile());
            log.info("sendRedPack() 红包发送结果:{}", resXml);
            return resXml;
        } catch (Exception e) {
            log.error("sendRedPack() 红包发送异常", e);
            throw new RuntimeException("红包发送异常");
        }
    }

    /**
     * 获取红包实体
     * @param wxPayReqBO
     * @return
     */
    private RedPackSendEntity transferRedPackSendEntity(WxPayReqBO wxPayReqBO){
        //封装发送红包实体
        RedPackSendEntity redPackEntity = new RedPackSendEntity();
        redPackEntity.setReOpenid(wxPayReqBO.getReOpenid());
        redPackEntity.setSubMchId(wxPayReqBO.getSubMchId());
        redPackEntity.setMsgAppid(wxPayReqBO.getMsgAppid());
        redPackEntity.setSendName(wxPayReqBO.getSendName());
        redPackEntity.setTotalAmount(wxPayReqBO.getTotalAmount());
        redPackEntity.setTotalNum(wxPayReqBO.getTotalNum());
        redPackEntity.setWishing(wxPayReqBO.getWishing());
        redPackEntity.setActName(wxPayReqBO.getActName());
        redPackEntity.setRemark(wxPayReqBO.getRemark());

        redPackEntity.setMchId(wxConfig.getMchId());
        redPackEntity.setWxAppid(wxConfig.getWxappid());
        redPackEntity.setClientIp(wxConfig.getClientIp());

        String nonceStr = WeiXinUtil.getUUID32Str();
        redPackEntity.setNonceStr(nonceStr);

        //不为空即使用原订单号重发
        String mchBillNo = wxPayReqBO.getMchBillNo();
        if(StringUtils.isBlank(mchBillNo)){
            mchBillNo = WeiXinUtil.getMchBillNo();
        }
        redPackEntity.setMchBillNo(mchBillNo);
        //签名
        String sign = WeiXinUtil.createRedPackOrderSign(redPackEntity , wxConfig.getApiKey());
        redPackEntity.setSign(sign);
        return redPackEntity;
    }


    @Override
    public String queryRedPackInfo(String mchBillNo) {
        log.info("查询红包发送记录 入参 mchBillNo:{}", mchBillNo);

        /**封装查询红包记录的实体**/
        RedPackQueryEntity redInfoEntity = new RedPackQueryEntity();
        redInfoEntity.setMchBillNo(mchBillNo);
        redInfoEntity.setMchId(wxConfig.getMchId());
        redInfoEntity.setAppid(wxConfig.getWxappid());
        redInfoEntity.setBillType("MCHT");
        String nonceStr = WeiXinUtil.getUUID32Str();
        redInfoEntity.setNonceStr(nonceStr);
        //签名
        String sign = WeiXinUtil.createQueryRedPackInfoSign(redInfoEntity , wxConfig.getApiKey());
        redInfoEntity.setSign(sign);
        //转换为xml格式请求体
        XmlUtils.xstream().autodetectAnnotations(true);
        XmlUtils.xstream().alias("xml", redInfoEntity.getClass());
        String xmlData = XmlUtils.xstream().toXML(redInfoEntity);

        /**http请求加证书 发送请求**/
        String resXml = httpClientSSL.sendWX(QUERY_URL, xmlData, httpClientSSL.defaultSSLClientFile());

        return resXml;

    }

}

上面代码涉及到几个工具类:

HttpClientSSL.java  http请求加微信证书,封装请求用的

WeiXinUtil.java     微信通用工具方法,生成订单号,拼接参数,生成签名等

XmlUtils.java        重写Xstream添加支持<![CDATA[]]>,封装接口的请求体使用

HttpClientSSL.java如下:

package com.sylujia.wxpay.utils;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.Charsets;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.net.ssl.SSLContext;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.*;
import java.security.cert.CertificateException;

/**
 * @Author jia
 * @Date 2019/5/7 17:12
 * @Description   http请求带证书
 */
@Slf4j
@Component
public class HttpClientSSL {

    /**
     * 证书名称
     */
    @Value("${wx.certFile}")
    private String certFile;
    /**
     * 服务商id
     */
    @Value("${wx.mch_id}")
    private String mchId;

    /**
     * 请求超时时间(毫秒) 5秒
     * 响应超时时间(毫秒) 15秒
     */
    public static RequestConfig getRequestConfig() {
        return RequestConfig.custom().setConnectTimeout(5 * 1000).setConnectionRequestTimeout(15 * 1000).build();
    }

    /**
     * https请求加证书
     * @return
     */
    public CloseableHttpClient defaultSSLClientFile() {
        InputStream inputStream = null;
        KeyStore keyStore;
        try {
            // ssl证书类型
            keyStore = KeyStore.getInstance("PKCS12");
            // ssl文件
            inputStream = this.getClass().getClassLoader().getResourceAsStream(certFile);
            // 设置ssl密码
            keyStore.load(inputStream, mchId.toCharArray());
        } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e1) {
           log.error("https请求加证书,获取KeyStore错误", e1);
           throw new RuntimeException("https请求加证书,获取KeyStore错误  HttpClientSSL.defaultSSLClientFile()");
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                log.error("https请求加证书,io错误", e);
                throw new RuntimeException("https请求加证书,io错误  HttpClientSSL.defaultSSLClientFile()");
            }
        }
        SSLContext sslContext;
        try {
            //创建SSL
            sslContext = SSLContexts.custom().loadKeyMaterial(keyStore,mchId.toCharArray()).build();
        } catch (UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
            log.error("https请求加证书,创建SSL错误", e);
            throw new RuntimeException("https请求加证书,创建SSL错误 HttpClientSSL.defaultSSLClientFile()");
        }

        SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(
                sslContext,
                new String[]{"TLSv1"},
                null,
                SSLConnectionSocketFactory.getDefaultHostnameVerifier());

        return HttpClients.custom().setSSLSocketFactory(factory).build();
    }

    /**
     * 封装发送请求的方法
     * @param ulr
     * @param xmlData
     * @param closeableHttpClient
     * @return
     */
    public String sendWX(String ulr, String xmlData, CloseableHttpClient closeableHttpClient) {

        StringBuffer message = new StringBuffer();

        HttpPost httpPost = new HttpPost(ulr);
        httpPost.addHeader("Connection", "keep-alive");
        httpPost.addHeader("Accept", "*/*");
        httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
        httpPost.addHeader("Host", "api.mch.weixin.qq.com");
        httpPost.addHeader("X-Requested-With", "XMLHttpRequest");
        httpPost.addHeader("Cache-Control", "max-age=0");
        httpPost.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) ");
        // 设置超时时间
        httpPost.setConfig(getRequestConfig());

        // 参数放入
        StringEntity entity = new StringEntity(xmlData, Charsets.UTF_8);
        entity.setContentType("application/xml");
        httpPost.setEntity(entity);

        try {
            //http请求加证书
            CloseableHttpClient httpClient = closeableHttpClient;
            CloseableHttpResponse response = httpClient.execute(httpPost);

            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                HttpEntity httpEntity = response.getEntity();
                if (httpEntity != null) {
                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(httpEntity.getContent(), Charsets.UTF_8));
                    String text;
                    while ((text = bufferedReader.readLine()) != null) {
                        message.append(text);
                    }
                }
            }
            if (message.length() == 0){
                throw new RuntimeException("network response error at HttpClientSSL sendWX()");
            }
        } catch (Exception e) {
            log.error("封装发送带证书的http请求错误", e);
            throw new RuntimeException("封装发送带证书的http请求错误 HttpClientSSL sendWX()");
        }
        return message.toString();

    }

}

WeiXinUtil.java如下:

package com.sylujia.wxpay.utils;

import com.sylujia.wxpay.entity.RedPackQueryEntity;
import com.sylujia.wxpay.entity.RedPackSendEntity;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;

/**
 * @Author jia
 * @Date 2019/5/7 15:43
 * @Description 微信通用工具方法
 */
public class WeiXinUtil {

    /**
     * 获取32为的随机的字符串
     * @return 去除'-'的32为字符串
     */
    public static String getUUID32Str() {
        return UUID.randomUUID().toString().replace("-", "");
    }

    /**
     * 生成商户订单号
     * @return String 商户订单号
     */
    public static String getMchBillNo() {
        String randomStr = RandomStringUtils.random(15, "abcdefghijklmnopqrstuvwxyz1234567890");
        String mchBillno = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli() + randomStr;
        return mchBillno;
    }


    /**
     * 创建发红包签名
     *
     * @param redPack 红包实体
     * @param apiKey  api密钥
     * @return String MD5加密后的值
     */
    public static String createRedPackOrderSign(RedPackSendEntity redPack, String apiKey) {

        Map<String, String> params = new HashMap<>();
        params.put("act_name", redPack.getActName());
        params.put("client_ip", redPack.getClientIp());
        params.put("mch_billno", redPack.getMchBillNo());
        params.put("mch_id", redPack.getMchId());
        params.put("msgappid", redPack.getMsgAppid());
        params.put("nonce_str", redPack.getNonceStr());
        params.put("re_openid", redPack.getReOpenid());
        params.put("remark", redPack.getRemark());
        params.put("send_name", redPack.getSendName());
        params.put("total_amount", String.valueOf(redPack.getTotalAmount()));
        params.put("total_num", String.valueOf(redPack.getTotalNum()));
        params.put("wishing", redPack.getWishing());
        params.put("wxappid", redPack.getWxAppid());
        params.put("sub_mch_id", redPack.getSubMchId());
        if(StringUtils.isNotBlank(redPack.getSceneId())){
            params.put("scene_id", redPack.getSceneId());
        }
        try {
            String url = makeUrlFormMap(params, false, false);
            url += "&key=" + apiKey;
            return DigestUtils.md5Hex(url).toUpperCase();
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * 创建查询红包记录签名
     *
     * @param redInfo 查询红包记录实体
     * @param apiKey  api密钥
     * @return String MD5加密后的值
     */
    public static String createQueryRedPackInfoSign(RedPackQueryEntity redInfo, String apiKey) {

        Map<String, String> params = new HashMap<>();
        params.put("nonce_str", redInfo.getNonceStr());
        params.put("mch_billno", redInfo.getMchBillNo());
        params.put("mch_id", redInfo.getMchId());
        params.put("appid", redInfo.getAppid());
        params.put("bill_type", redInfo.getBillType());
        params.put("mch_billno", redInfo.getMchBillNo());

        try {
            String url = makeUrlFormMap(params, false, false);
            url += "&key=" + apiKey;
            return DigestUtils.md5Hex(url).toUpperCase();
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }



    /**
     * 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序),并且生成url参数串
     *
     * @param paraMap   要排序的Map对象
     * @param urlEncode 是否需要URLENCODE
     * @return keyToLower 是否需要将Key转换为全小写  true:key转化成小写,false:不转化
     * @throws UnsupportedEncodingException 未知字符集异常
     */
    public static String makeUrlFormMap(Map<String, String> paraMap, boolean urlEncode, boolean keyToLower) throws UnsupportedEncodingException {
        StringBuffer stringBuffer = new StringBuffer();
        List<Map.Entry<String, String>> entryList = new ArrayList<>(paraMap.entrySet());
        // 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序)
        Collections.sort(entryList, Comparator.comparing(Map.Entry::getKey));
        //构造URL
        for (Map.Entry<String, String> entry: entryList) {
            if (!StringUtils.isBlank(entry.getKey())) {
                String key = entry.getKey();
                String val = entry.getValue();
                if (urlEncode) {
                    val = URLEncoder.encode(val, StandardCharsets.UTF_8.name());
                }
                if (keyToLower) {
                    stringBuffer.append(key.toLowerCase()).append("=").append(val);
                } else {
                    stringBuffer.append(key).append("=").append(val);
                }
                stringBuffer.append("&");
            }
        }
        String url = stringBuffer.toString();
        if (!url.isEmpty()) {
            url = url.substring(0, url.length() - 1);
        }
        return url;
    }

}

五、结束语

上面只是一些简单的逻辑实现,工具类不仅适用于发红包以及查红包,其他微信api也适用。

这里只是发红包以及查红包的接口实现,具体业务情景下,这些记录都是要入库的,而且对于红包的状态要考虑清楚,例如红包是否发送成功,用户是否领取成功(24小时没领取会退回),对于发送失败的订单是否要使用原订单号重发(防止资金重复发放)等。

本实例支付的费用只是购买源码的费用,如有疑问欢迎在文末留言交流,如需作者在线代码指导、定制等,在作者开启付费服务后,可以点击“购买服务”进行实时联系,请知悉,谢谢
手机上随时阅读、收藏该文章 ?请扫下方二维码