微信商家红包发放接入
微信红包
准备工作
微信商家开通支付功能、微信公众号、开通红包功能
相关网站
- 微信支付: 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>
哇!又赚了一天人民币
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库