微信商家红包发放接入
微信红包
准备工作
微信商家开通支付功能、微信公众号、开通红包功能
相关网站
- 微信支付: https://pay.weixin.qq.com/
- 签名生成算法:https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=4_3
- 发放红包接口:https://pay.weixin.qq.com/wiki/doc/api/tools/cash_coupon.php?chapter=13_4&index=3
需要资料
- API证书(apiclient_cert.p12):微信商户平台(pay.weixin.qq.com)-->账户中心-->账户设置-->API安全
- API支付秘钥:商户平台设置的密钥key 32 位
- openId:用户微信公众授权获取到得 openId
注意事项
- 现金红包接口目前微信还没升级,使用的v2版本,所以提交数据和返回数据采用的是xml格式
- 付款金额,单位分
- 红包发放成功,会发放到公众号中,需要用户主动点击领取
- 测试过程中保证商户运营账号有余额
- 微信红包-产品设置-添加测试地址ip白名单,不知道自己外网ip地址 百度搜索 ip 即可
代码示例
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.HttpClientBuilder;
import org.apache.http.ssl.SSLContexts;
import org.jeecg.common.util.Md5Util;
import org.jeecg.modules.video.utitls.Constants;
import org.springframework.core.io.ClassPathResource;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.net.ssl.SSLContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.security.*;
import java.util.*;
/**
* @description:
* @author: Mr.Fang
* @create: 2023-05-26
**/
@Slf4j
public class WxTest {
public static void main(String[] args) throws Exception {
// 1、参数封装
TreeMap<String, Object> map = new TreeMap<>();
map.put("nonce_str", UUID.randomUUID().toString().replaceAll("-", "")); // 随机字符串
map.put("mch_billno", "202302260000001");// "商家订单编号"
map.put("mch_id", Constants.WX_MERCHANT_ID); // 商户号
map.put("wxappid", Constants.WX_APP_ID); // 公众号 APPID
map.put("send_name", "我发的");
map.put("re_openid", "123456"); // openid
map.put("total_amount", "100"); // 金额
map.put("total_num", 1); // 数量
map.put("wishing", "恭喜发财");
map.put("client_ip", "127.0.0.1");
map.put("act_name", "任务分佣");
map.put("remark", "无");
// 2、参数拼接 k=v 格式
List<String> list = new ArrayList<>();
for (Map.Entry<String, Object> entry : map.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
list.add(String.format("%s=%s", key, value.toString()));
}
String join = String.join("&", list);
log.debug("拼接参数结果:{}", join);
// 3、参数+&key=API支付秘钥 md5 签名
String sign = Md5Util.md5Encode(join + "&key=" + Constants.WX_MERCHANT_API_SECRET, "UTF-8");
log.debug("拼接参数md5:{}", sign.toUpperCase(Locale.ROOT));
map.put("sign", sign.toUpperCase(Locale.ROOT));
log.debug("请求参数Map:{}", map);
// 4、获取 API 证书,当前证书是放在 resource 下的
ClassPathResource resource = new ClassPathResource("apiclient_cert.p12");
InputStream inputStream = null;
try {
inputStream = resource.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
// FIXME: 2023/5/26 也可以这样
// inputStream = new FileInputStream("证书路径");
// 5、加载 SSL 证书
String pass =Constants.WX_MERCHANT_ID; // 密码商户号
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(inputStream, pass.toCharArray()); // 证书默认密码是商户号码
SSLContext sslcontext = SSLContexts.custom().loadTrustMaterial((chain, authType) -> true).loadKeyMaterial(keyStore, pass.toCharArray()).build();
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslcontext, new String[]{"TLSv1"}, null, SSLConnectionSocketFactory.getDefaultHostnameVerifier());
// 6、创建 httpClient 客户端 并设置 ssl 对象
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
httpClientBuilder.setSSLSocketFactory(sslConnectionSocketFactory);
CloseableHttpClient build = httpClientBuilder.build();
// 7、创建 post 请求
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack");
String xml = parseMapToXml(map);
log.debug("请求体xml:{}", xml);
StringEntity stringEntity = new StringEntity(xml, "UTF-8");
stringEntity.setContentEncoding("UTF-8");
httpPost.setEntity(stringEntity);
// 8、发起请求
CloseableHttpResponse response = build.execute(httpPost);
Map<String, Object> toMap = parseXMLToMap(response.getEntity().getContent());
log.info("mchRedEnvelope:{}", toMap);
}
/**
* 解析 xml
*
* @param map
* @return
*/
public static String parseMapToXml(Map<String, ? extends Object> map) {
StringBuilder sb = new StringBuilder("<xml>");
if (null != map && map.size() > 0) {
Set<? extends Map.Entry<String, ?>> entries = map.entrySet();
String key;
for (Iterator iterator = entries.iterator(); iterator.hasNext(); sb.append("</" + key + ">")) {
Map.Entry next = (Map.Entry) iterator.next();
key = (String) next.getKey();
Object value = next.getValue();
sb.append("<" + key + ">");
if (Objects.nonNull(value)) {
sb.append("<![CDATA[" + value + "]]>");
}
}
}
sb.append("</xml>");
return sb.toString();
}
/**
* xml 转 map
* @param inputStream
* @return
*/
public static Map<String, Object> parseXMLToMap(InputStream inputStream) {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(inputStream);
Element documentElement = document.getDocumentElement();
Map<String, Object> map = recursiveFindXmlElement(documentElement);
return map;
} catch (ParserConfigurationException | SAXException | IOException e) {
e.printStackTrace();
}
return null;
}
private static Map<String, Object> recursiveFindXmlElement(Element element) {
if (Objects.nonNull(element)) {
HashMap<String, Object> map = new HashMap();
NodeList childNodes = element.getChildNodes();
for(int i = 0; i < childNodes.getLength(); ++i) {
Node item = childNodes.item(i);
short nodeType = item.getNodeType();
if (Objects.equals(nodeType, (short) 1)) {
String nodeName = item.getNodeName();
Element childElement = (Element)item;
Map<String, Object> childMap = recursiveFindXmlElement(childElement);
if (null != childMap && childMap.size() > 0) {
map.put(nodeName, childMap);
} else {
String nodeValue = StringUtils.isBlank(item.getTextContent()) ? null : item.getTextContent().trim();
map.put(nodeName, nodeValue);
}
}
}
if (map.size() > 0) {
return map;
}
}
return null;
}
}
响应结果
展示参数进行处理,不代表真实发送数据内容
拼接参数结果:
act_name=任务分佣&client_ip=127.0.0.1&mch_billno=202302260000001&mch_id=1299535101&nonce_str=c3dad56d4bc74f9d910a6210edaddd0a&re_openid=1111111&remark=无&send_name=我发的&total_amount=100&total_num=1&wishing=恭喜发财&wxappid=1111111
拼接参数md5:
426a84083ea5adc67398a7918490adeb
请求参数Map:
{act_name=任务分佣, client_ip=127.0.0.1, mch_billno=202302260000001, mch_id=1111111, nonce_str=c3dad56d4bc74f9d910a6210edaddd0a, re_openid=1111111, remark=无, send_name=恭喜发财, sign=426a84083eadadc67398a7918490adeb, total_amount=100, total_num=1, wishing=恭喜发财, wxappid=1111111}
请求体xml
<xml>
<act_name><![CDATA[任务分佣]]></act_name>
<client_ip><![CDATA[127.0.0.1]]></client_ip>
<mch_billno><![CDATA[202302260000001]]></mch_billno>
<mch_id><![CDATA[1111111]]></mch_id>
<nonce_str><![CDATA[c3dad56d4bc74f9d910a6210edaddd0a]]></nonce_str>
<re_openid><![CDATA[1111111]]></re_openid>
<remark><![CDATA[无]]></remark>
<send_name><![CDATA[33333]]></send_name>
<sign><![CDATA[426a84083eadadc67398a7918490adeb]]></sign>
<total_amount><![CDATA[100]]></total_amount>
<total_num><![CDATA[1]]></total_num>
<wishing><![CDATA[恭喜发财]]></wishing>
<wxappid><![CDATA[1111111]]></wxappid>
</xml>
哇!又赚了一天人民币