java微信公众号发送现金红包,包括查询红包记录(子商户模式)
概述
详细
一、前言
微信红包分为普通红包和裂变红包,发放方式分为普通商户模式以及子商户模式,下面代码是以子商户模式发放普通红包写的。具体怎么选择看业务场景,代码原理是一样的,只是接口不一致,传参不同。
微信红包默认为1到200元,场景下为1-499元(调接口时参数必须传场景id),这些只需要平台设置,不需要申请。
其最小额度为0.3元,最大额度为4999元,这些需要向微信申请。(不一定能申请下来)
读代码之前最好对服务商,普通商户,特约商户等有个基本概念。最好先研究一遍文档,微信的文档写的还是很详细的。实在不明白可以打客服电话。
二、先看效果图
三、准备工作
开始前一定要准备好测试条件:
1、已有服务商账号,必须已经开通了服务商现金红包功能。产品中心--特约商户授权产品--运营工具--服务商现金红包
2、已申请特约商户,必须已经开通了现金红包功能。产品中心--我的产品--运营工具--现金红包
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(子商户公众号下的)
关于准备工作的注意事项:
1、特约商户的申请以及授权绑定都需要审核,大概一到三天。
2、特约商户开通现金红包功能是有要求的,T+7账户能立即开通,其余的需要有90天的入驻以及30天的连续流水交易才能开通
3、特约商户绑定公众号时,两者主体必须一致,如果公众号绑定过其他商户,则特约商户的费率必须和这个商户一致
4、用户必须关注特约商户绑定的公众号,红包也是从这个公众号发出,钱是从特约商户账户出,红包发送记录是在服务商后台查
四、程序实现
项目代码截图:
以下摘部分重要代码说明过程:
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小时没领取会退回),对于发送失败的订单是否要使用原订单号重发(防止资金重复发放)等。