遇到的坑要写下来。
企业微信接口定义:
企业微信AP地址:https://work.weixin.qq.com/api/doc/90000/90135/90275
请求方式:POST(HTTPS)
请求地址:https://api.mch.weixin.qq.com/mmpaymkttransfers/sendworkwxredpack
是否需要证书:是(注:这里的是否需要证书指的是在post请求中需要携带证书,证书最好按照企业微信的建议放在一个有读取权限的位置)
数据格式:xml
证书使用详见:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=4_3
请求示例:
<xml>
<nonce_str>5K8264ILTKCH16CQ2502SI8ZNMTM67VS</nonce_str>
<sign>C380BEC2BFD727A4B6845133519F3AD6</sign>
<mch_billno>123456</mch_billno>
<mch_id>10000098</mch_id>
<wxappid>wx8888888888888888</wxappid>
<sender_name>XX活动</sender_name>
<sender_header_media_id>1G6nrLmr5EC3MMb_-zK1dDdzmd0p7cNliYu9V5w7o8K0</sender_header_media_id>
<re_openid>oxTWIuGaIt6gTKsQRLau2M0yL16E</re_openid>
<total_amount>1000</total_amount>
<wishing>感谢您参加猜灯谜活动,祝您元宵节快乐!</wishing>
<act_name>猜灯谜抢红包活动</act_name>
<remark>猜越多得越多,快来抢!</remark>
<workwx_sign>99BCDAFF065A4B95628E3DB468A874A8</workwx_sign>
</xml>
参数说明:
字段名 | 字段 | 必填 | 示例值 | 类型 | 说明 |
随机字符串 | nonce_str | 是 | 5K8264ILTKCH16CQ2502SI8ZNMTM67VS | String(32) | 随机字符串,不长于32位 |
微信支付签名 | sign | 是 | C380BEC2BFD727A4B6845133519F3AD6 | String(32) | 参见“签名算法” |
商户订单号 | mch_billno | 是 | 123456 | String(28) | 商户订单号(每个订单号必须唯一。取值范围:0~9,a~z,A~Z).接口根据商户订单号支持重入,如出现超时可再调用。组成参考:mch_id+yyyymmdd+10位一天内不能重复的数字 |
商户号 | mch_id | 是 | 10000098 | String(32) | 微信支付分配的商户号 |
公众账号appid | wxappid | 是 | wx8888888888888888 | String(32) | 微信分配的公众账号ID(企业微信corpid即为此appId)。接口传入的所有appid应该为公众号的appid(在mp.weixin.qq.com申请的),不能为APP的appid(在open.weixin.qq.com申请的)。 |
发送者名称 | sender_name | 否 | XX活动 | String(128) | 以个人名义发红包,红包发送者名称(需要utf-8格式)。与agentid互斥,二者只能填一个。 |
发送红包的应用id | agentid | 否 | 1 | unsigned int | 以企业应用的名义发红包,企业应用id,整型,可在企业微信管理端应用的设置页面查看。与sender_name互斥,二者只能填一个。 |
发送者头像 | sender_header_media_id | 否 | 1G6nrLmr5EC3MMb_-zK1dDdzmd0p7cNliYu9V5w7o8K0 | String(128) | 发送者头像素材id,通过企业微信开放上传素材接口获取 |
用户openid | re_openid | 是 | oxTWIuGaIt6gTKsQRLau2M0yL16E | String(32) | 接受红包的用户.用户在wxappid下的openid。获取用户openid参见:http://work.weixin.qq.com/api/doc#11279 |
金额 | total_amount | 是 | 1000 | int | 金额,单位分,单笔最小金额默认为1元 |
红包祝福语 | wishing | 是 | 感谢您参加猜灯谜活动,祝您元宵节快乐! | String(128) | 红包祝福语 |
项目名称 | act_name | 是 | 猜灯谜抢红包活动 | String(32) | 项目名称 |
备注 | remark | 是 | 猜越多得越多,快来抢! | String(256) | 备注信息 |
场景 | scene_id | 否 | PRODUCT_1 | String(32) | 发放红包使用场景,红包金额大于200或者小于1元时必传 PRODUCT_1:商品促销 PRODUCT_2:抽奖 PRODUCT_3:虚拟物品兑奖 PRODUCT_4:企业内部福利 PRODUCT_5:渠道分润 PRODUCT_6:保险回馈 PRODUCT_7:彩票派奖 PRODUCT_8:税务刮奖 |
企业微信签名 | workwx_sign | 是 | 企业微信签名 | String(32) | 参见“签名算法” |
代码实现:
单元测试主类:
package com.cth.src;
import com.cth.src.work.request.EmployeeGetOpenIdRequest;
import com.cth.src.work.response.EmployeeGetOpenIdResponse;
import com.cth.src.work.response.MediaUploadResponse;
import com.cth.src.work.utils.IamMultipartFile;
import com.cth.src.work.utils.ScrmStrinUtils;
import com.cth.src.work.weComService.EmployeeService;
import com.cth.src.work.weComService.MediaService;
import com.cth.src.work.weComService.SendRedPacketService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author chengtonghua
* @date 2020-11-17
*/
@SpringBootTest(classes = TopKApplication.class)
@Slf4j
public class PayTest {
@Autowired
private SendRedPacketService sendRedPacketService;
@Test
public void sendworkwxredpack() {
//构建企业微信签名
try {
Map<String, String> paramsMap = new HashMap<>();
//项目名称
paramsMap.put("act_name", "猜灯谜抢红包活动");
//商户订单号
paramsMap.put("mch_billno", UUID.randomUUID().toString().replace("-", "").substring(0,28));
//商户号微信商户平台获取
paramsMap.put("mch_id", "");
//随机字符串
paramsMap.put("nonce_str", ScrmStrinUtils.generateNonceStr());
//用户openid(根据应用类型,调用企业微信接口使用客户userId换取客户的openId)
paramsMap.put("re_openid", "");
//金额 单位分,单笔最小金额默认为1元
paramsMap.put("total_amount", "1");
//公众账号appid 天坑 此处一定要配置微信商户平台的appID
paramsMap.put("wxappid", "你的微信商户平添appId");
//放这的原因是以上字段为企业微信签名固定字段
//此处的secret是 企业微信应用中的企业支付应用的Secret(文档并没有阐明,坑)
String secret = "企业微信应用中的企业支付应用的Secret";
String workwxSign = ScrmStrinUtils.generateSignature(paramsMap,secret, "qywx");
//企业微信签名
paramsMap.put("workwx_sign", workwxSign);
//发送者名称
// paramsMap.put("sender_name", "ss");
//企业微信中发送红包的应用ID
paramsMap.put("agentid", "应用ID");
//发送者头像((需要调用素材库上传素材接口)上传用户头衔到临时素材库中换取头像的素材ID)
// paramsMap.put("sender_header_media_id", "素材Id");
//场景 --发放红包使用场景,红包金额大于200或者小于1元时必传
paramsMap.put("scene_id", "PRODUCT_1");
//红包祝福语
paramsMap.put("wishing", "测试通过");
//备注
paramsMap.put("remark", "");
//构建微信支付签名
String key = "此处的key为微信商户平台的api密钥";
String sign = ScrmStrinUtils.generateSignature(paramsMap, key, "wx");
//微信支付签名 需要将企业微信签名也包含进来
paramsMap.put("sign", sign);
String play = sendRedPacketService.sendworkwxredpack(ScrmStrinUtils.mapToXml(paramsMap));
//注意 ,此处只是简单拿到企业微信返回值而已,如果要使用在生产环境一定要校验企业微信返回值内的sign进行验签操作
System.out.println(play);
} catch (Exception e) {
log.info("报错信息" + e);
}
}
@Test
public void queryworkwxredpack() {
//构建企业微信签名
try {
Map<String, String> paramsMap = new HashMap<>();
//商户订单号
paramsMap.put("mch_billno", "");
//商户号
paramsMap.put("mch_id", "");
//随机字符串
paramsMap.put("nonce_str", ScrmStrinUtils.generateNonceStr());
//公众账号appid
paramsMap.put("appid", "");
//构建微信支付签名
String key = "此处的key为微信商户平台的api密钥";
String sign = ScrmStrinUtils.generateSignature(paramsMap, key, "wx");
//微信支付签名
paramsMap.put("sign", sign);
String play = sendRedPacketService.queryworkwxredpack(ScrmStrinUtils.mapToXml(paramsMap));
System.out.println(play);
} catch (Exception e) {
log.info("报错信息" + e);
}
}
}
SendRedPacketService:
package com.cth.src.work.weComService;
import com.cth.src.work.config.WebbankConfiguration;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* @author chengtonghua
* @date 2020-11-18
*此处是思想是将企业微信作为一个的单独微服务,使用Feign的方式进行调用。
* WebbankConfiguration 作为发送红包接口的一个config配置,里面在请求中注入了企业微信请求需要的证书信息
*/
@FeignClient(name = "weComService", url = "https://api.mch.weixin.qq.com/mmpaymkttransfers", contextId = "sendService",configuration = WebbankConfiguration.class)
public interface SendRedPacketService {
/**
* 调用企业微信红包接口
* @param play
* @return
*/
@PostMapping(value = "/sendworkwxredpack", consumes = MediaType.APPLICATION_XML_VALUE, produces = MediaType.APPLICATION_XML_VALUE)
String sendworkwxredpack(@RequestBody String play);
/**
* 查询红包记录
* @param play
* @return
*/
@PostMapping(value = "/queryworkwxredpack", consumes = MediaType.APPLICATION_XML_VALUE, produces = MediaType.APPLICATION_XML_VALUE)
String queryworkwxredpack(@RequestBody String play);
}
ScrmStrinUtils:
package com.cth.src.work.utils;
import org.w3c.dom.Document;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.StringWriter;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Map;
import java.util.Random;
import java.util.Set;
/**
* @author chengtonghua
* @date 2020-11-17
*/
public class ScrmStrinUtils {
private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final Random RANDOM = new SecureRandom();
/**
* 获取随机字符串 Nonce Str
*
* @return String 随机字符串
*/
public static String generateNonceStr() {
char[] nonceChars = new char[32];
for (int index = 0; index < nonceChars.length; ++index) {
nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
}
return new String(nonceChars);
}
/**
* 生成签名
* @param data 待签名数据
* @param key API密钥
* @return 签名
*/
public static String generateSignature(final Map<String, String> data, String key,String type) throws Exception {
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (k.equals("sign")) {
continue;
}
if (data.get(k).trim().length() > 0) {
// 参数值为空,则不参与签名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
}
if("wx".equals(type)){
sb.append("key=").append(key);
}else{
sb.append("secret=").append(key);
}
String sign;
try {
sign = MD5(sb.toString()).toUpperCase();
}catch (Exception e){
throw new Exception(String.format("Invalid sign_type: %s", e));
}
return sign;
}
/**
* 生成 MD5
*
* @param data 待处理数据
* @return MD5结果
*/
public static String MD5(String data) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
/**
* 将Map转换为XML格式的字符串
*
* @param data Map类型数据
* @return XML格式的字符串
* @throws Exception
*/
public static String mapToXml(Map<String, String> data) throws Exception {
Document document = newDocument();
org.w3c.dom.Element root = document.createElement("xml");
document.appendChild(root);
for (String key: data.keySet()) {
String value = data.get(key);
if (value == null) {
value = "";
}
value = value.trim();
org.w3c.dom.Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
root.appendChild(filed);
}
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
DOMSource source = new DOMSource(document);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
try {
writer.close();
}
catch (Exception ex) {
}
return output;
}
public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
documentBuilderFactory.setXIncludeAware(false);
documentBuilderFactory.setExpandEntityReferences(false);
return documentBuilderFactory.newDocumentBuilder();
}
public static Document newDocument() throws ParserConfigurationException {
return newDocumentBuilder().newDocument();
}
}
WebbankConfiguration:
package com.cth.src.work.config;
import feign.Client;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.ssl.SSLContexts;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import java.io.FileInputStream;
import java.security.*;
@Configuration
@Slf4j
public class WebbankConfiguration {
@Bean
public Client feignClient() throws Exception {
Client trustSSLSockets = new Client.Default(getSSLSocketFactory(), new NoopHostnameVerifier());
log.info("feignClient called");
return trustSSLSockets;
}
//增加SSL
public static SSLSocketFactory getSSLSocketFactory() throws Exception {
//TODO Exception 需要抛出异常
//商户id,证书的默认密码
String mchId = "";
KeyStore keyStore = KeyStore.getInstance("PKCS12");
//本地证书
ClassPathResource resource = new ClassPathResource("微信支付需要的证书路径");
FileInputStream instream = new FileInputStream(resource.getFile());
keyStore.load(instream, mchId.toCharArray());
SSLContext sslContext = null;
try {
sslContext = SSLContexts.custom().loadKeyMaterial(keyStore, mchId.toCharArray()).build();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (UnrecoverableKeyException e) {
e.printStackTrace();
}
return sslContext.getSocketFactory();
}
}
常见错误处理:
- 签名错误,这个错误最经常出现,但是呢情况很多所以不容易排查。这个时候就需要按照官方文档内的要求核对参数的顺序以及数量。
- 企业微信的签名一定要在微信支付签名之前执行,因为企业微信签名需要参与到微信支付的签名中去。(api地址:https://work.weixin.qq.com/api/doc/90000/90135/90281)
- wxappid是微信商户平台的appID
- 企业微信签名所使用的Secret是来源于企业微信应用中的企业支付应用的Secret
- 微信支付签名中使用的key为微信商户平台api设置中的key值
- 商户平台的证书和密码是否正确
- 签名的处理
- 千万不要删除或者增加字段,否则会报签名失败
企业微信签名字段说明:
发红包api固定如下几个字段参与签名:
act_name
mch_billno
mch_id
nonce_str
re_openid
total_amount
wxappid
付款api固定如下几个字段参与签名:
amount
appid
desc
mch_id
nonce_str
openid
partner_trade_no
ww_msg_type
- 关键参数来源截图
- 企业微信参与签名的应用secret和应用ID
3. 微信商户平台的appid
登录商户平台后->产品中心->AppID账号管理中即可看到。
4. 微信支付key以及证书(需要注意的是:key一旦获取后需要妥善保存)
补充一个比较狗的但是能在企业微信外部联系人群发送红包的方法:就是创建了一个外部联系人群之后,你把自己的微信账号也拉入群中,这样你自己就可以在微信客户端发送客户群红包了,产生的消费可以自己找公司财务报销。