省流
- 企业微信如果已经被认证,企业可信域名必须与认证的企业有关
- 个体工商户不能成为服务商
- 企业成为服务商需要300认证
- A手上有一个个体工商和企业均是法人,个体工商为认证主体的域名可以被企业账号使用。
- 如果不想这么麻烦就让客户不要企业微信认证
🌟🌟🌟 服务商代开发不免费!!!🌟🌟🌟
- 服务商代开发接口需要收费,您有90天的免费调用开发时间。服务商代开发是按照客户用户人数收费的。
- 一个代开发模板可以给多个企业扫码授权
作者在给客户开发企业微信应用时,客户的企业微信已经通过企业认证,此时我们在填写企业信任域名的时候必须填写客户企业为认证主体或相关的域名。
当然我们还可以使用服务商代开发的方式。
了解服务商代开发流程
开始进行服务商代开发
这里我们默认认为您已经完成企业微信服务商的认证
1.登录服务商后台
企业成为服务商后可以在右上角看到服务商入口,如果您的企业还不是服务商请点击链接成为服务商。
2.创建代开发应用模板
找到入口 【服务商后台】应用管理->应用代开发->创建代开发应用模板
第一步填写logo、名称等基本信息
第二步中
Token、EncodingAESKey可以点击随机获取生成;代开发模板回调URL是我们配置回调的地址,开发环境下我们可以使用内网穿透工具在自己电脑上开发,无需上传到云服务器。
这里我使用的内网穿透工具是natapp,大家可以自行选择方案。
上面的模板回调URL我们直接配置为本地内网穿透路径
http://h5.****.top/**/auth/verify(你自己的callBack访问地址),并实现后台代码:
@PostMapping("/auth/verify")
public String authCallBack() {
return "success";
}
这边我们先直接返回"success"完成模板的创建,后续我们将继续补充回调方法。此方法后续将获取更新suiteTicket、应用secret等。
此方法用于处理回调,具体请查看官网文档。
完成模板创建后需要在代开发应用上线提交模板审核上线
模板审核我估摸着应该是ai审核,半夜也会进行。审核完之后状态会变成“待上线”,需要点击此条目在左上角点击上线。
3.模板授权回调
授权回调的请求路径和上一节的模版回调完全一致,不同的是这里的授权回调使用的GET请求,而上一章的回调是POST请求。(代码快中的authToken、authSecret、authEncodingAESKey、clientCorpId等配置请查看第5章节——服务商代开发所需的所有配置项)
// ============== Controller ===================
@GetMapping("/auth/verify")
public String authVerifyEffectiveness(
String msg_signature,
String timestamp,
String nonce throws Exception {
return weComService.authVerifyEffectiveness(msg_signature, timestamp, nonce, echostr);
}
// ============== Service ===================
public String authVerifyEffectiveness(String msgSignature, String timestamp, String nonce, String echostr) throws Exception {
WXBizJsonMsgCrypt wxcpt = new WXBizJsonMsgCrypt(authToken, authEncodingAESKey, authEncryptCorpId(clientCorpId));
return wxcpt.VerifyURL(msgSignature, timestamp, nonce, echostr);
}
public String authEncryptCorpId(String corpId) {
String url = "https://qyapi.weixin.qq.com/cgi-bin/service/corpid_to_opencorpid?provider_access_token=" + getProviderToken();
Map<String, Object> data = new HashMap<>();
data.put("corpid", corpId);
JSONObject res = restTemplate.postForObject(url, data, JSONObject.class);
if (res != null && res.getInteger("errcode") == 0) {
return res.getString("open_corpid");
} else {
throw new RuntimeException("获取加密corpId失败");
}
}
public String getProviderToken(){
String url = "https://qyapi.weixin.qq.com/cgi-bin/service/get_provider_token";
Map<String, Object> data = new HashMap<>();
data.put("corpid", authCorpId);
data.put("provider_secret", authSecret);
JSONObject res = restTemplate.postForObject(url, data, JSONObject.class);
if (res != null) {
return res.getString("provider_access_token");
} else {
throw new RuntimeException("获取providerToken失败");
}
}
代码快中的WXBizJsonMsgCrypt类可以在官网下载java库json版本。
这章节如果配置失败客户扫码时将会报错。
![]()
4.客户管理员扫码授权
模板上线后会出现一个二维码,可以点击进入详情放大。将二维码提供给客户企业微信的管理员扫码,一个模板可以给多个企业扫码。
客户扫码授权成功后会在下方显示一个“待开发”的企业
并且在客户的企业微信后台也会出现一个半透明的应用,点击进入详情,我们需要知道应用的
AgentId。
5.服务商代开发所需的所有配置项
# ========== 服务商配置 ==========
# 自主生成的Token
ww.auth.Token=
# 自主生成的EncodingAESKey
ww.auth.EncodingAESKey=
# 服务商的corpId
ww.auth.corpId=
# 服务商的secret
ww.auth.corpSecret=
# 代开发模板的id
ww.auth.templateId=
# 代开发模板的secret
ww.auth.templateSecret=
# ========== 客户配置 ==========
# 客户corpId
ww.client.corpId=
# 客户secret(没办法直接查看,需要通过上面的回调函数获取)
ww.client.secret=
# 客户应用的agentId
ww.client.agentId=
5.1 Token与EncodingAESKey
5.2 服务商的corpId与secret
服务商的corpId和secret在通用开发参数中找,这里的ProviderSecret就是配置中的
ww.auth.corpSecret。
5.3 代开发模板的id与secret
代开发模板的id与secret在代开发模版详情点击蓝色小字查看模板信息中获取。
5.4 客户corpId
客户corpId需要在客户登入的后台-我的企业中查看,这里的企业ID就是配置中的
ww.client.corpId。
5.5 客户应用agentId
应用agentId在客户后台点击新建的半透明应用,进入详情页获取。
5.6 🌟🌟🌟 客户应用secret 🌟🌟🌟
客户应用secret没办法直接查看,需要通过上面的回调函数获取。
获取secret我们先要获取到应用回调中的suite_ticket,通过suite_ticket等拿到suiteToken,再通过suiteToken和auth_code获取到客户应用secret。直接上代码:
import cn.hutool.json.XML;
import com.alibaba.fastjson.JSONObject;
import com.zhzhc.safe.admin.modules.wecom.aes.WXBizJsonMsgCrypt;
import com.zhzhc.safe.admin.modules.wecom.utils.WXBizMsgCrypt;
import me.chanjar.weixin.common.bean.WxJsapiSignature;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl;
import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Service
public class WeComService {
@Autowired
private RestTemplate restTemplate;
@Value("${ww.client.corpId}")
private String clientCorpId;
@Value("${ww.client.secret}")
private String clientSecret;
@Value("${ww.client.agentId}")
private String clientAgentId;
@Value("${ww.auth.Token}")
private String authToken;
@Value("${ww.auth.EncodingAESKey}")
private String authEncodingAESKey;
@Value("${ww.auth.corpId}")
private String authCorpId;
@Value("${ww.auth.corpSecret}")
private String authSecret;
@Value("${ww.auth.templateId}")
private String templateId;
@Value("${ww.auth.templateSecret}")
private String templateSecret;
private String suiteAccessToken = null;
private String suiteTicket = null;
private String permanentCode = null;
public String authCallBack(HttpServletRequest request, String msgSignature, String timestamp, String nonce, Map<String, Object> map) throws Exception {
try {
String xmlString = getXMLString(request);
String encryptData = XML.toJSONObject(xmlString).getJSONObject("xml").getStr("Encrypt");
System.out.println(encryptData);
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(authToken, authEncodingAESKey, templateId);
String sEchoStr = wxcpt.verifyAndGetData(msgSignature, timestamp, nonce, encryptData);
cn.hutool.json.JSONObject jsonObject = XML.toJSONObject(sEchoStr).getJSONObject("xml");
if(jsonObject.getStr("InfoType").equals("suite_ticket")){
// 更新suiteTicket
suiteTicket = jsonObject.getStr("SuiteTicket");
} else if(jsonObject.getStr("InfoType").equals("reset_permanent_code")){
String authCode = jsonObject.getStr("AuthCode");
String clientSecret = getPermanentCode(authCode);
}
} catch (Exception e) {
e.printStackTrace();
}
return "success";
}
private String getXMLString(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
BufferedReader br = null;
try {
br = request.getReader();
char[] buff = new char[1024];
int len;
while ((len = br.read(buff)) > 0) {
sb.append(buff, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
public String getSuiteToken(){
if(suiteAccessToken != null){
return suiteAccessToken;
}
String url = "https://qyapi.weixin.qq.com/cgi-bin/service/get_suite_token";
Map<String, Object> data = new HashMap<>();
data.put("suite_id", templateId);
data.put("suite_secret", templateSecret);
data.put("suite_ticket", suiteTicket);
JSONObject res = restTemplate.postForObject(url, data, JSONObject.class);
if (res != null) {
suiteAccessToken = res.getString("suite_access_token");
return suiteAccessToken;
} else {
throw new RuntimeException("获取suiteAccessToken失败");
}
}
// 获取客户应用secret
public String getPermanentCode(String authCode){
String url = "https://qyapi.weixin.qq.com/cgi-bin/service/get_permanent_code?suite_access_token=" + getSuiteToken();
Map<String, Object> data = new HashMap<>();
data.put("auth_code", authCode);
JSONObject res = restTemplate.postForObject(url, data, JSONObject.class);
if (res != null) {
permanentCode = res.getString("permanent_code");
return permanentCode;
} else {
throw new RuntimeException("获取代开发应用secret失败");
}
}
}
核心函数是getPermanentCode,通过查看官网文档后发现,结果中的permanent_code就是我们需要的代开发授权应用secret。
上述代码中的authCallBack的函数就是我们在第2章结尾模版回调函数的完整体,这里我的项目只用到了免登录和消息通知发送,所有我只需要获取到应用secret就可以了。企业微信会每十分钟向回调模版地址发送一次POST请求,我们也可以在后台手动刷新,我们在后台代开发应用模板信息中可以点击刷Ticket,此时会执行方法中的if(jsonObject.getStr("InfoType").equals("suite_ticket"))分支,执行后我们将获得模版授权Ticket,Ticket的有效期为30分钟,我们缓存后继续在代开发应用详情中点击重新获取应用Secret,secret是永久有效的除非你重新获取,请妥善保存。点击重新获取后会执行方法中的else if(jsonObject.getStr("InfoType").equals("reset_permanent_code")分支,我们可以直接从参数中拿到所需的auth_code,suiteToken的获取通过方法getSuiteToken。
模版回调可以放在自己的服务器上无需和业务代码放在一起,我这里为了节省时间直接放在本地,每次授权新企业就手动调用一次拿到应用secret在配置在properties中。
至此我们已经完成了代开发应用的主要内容获取与配置,我们需要在代开发应用上线中提交我们刚刚的应用上线即可在客户工作台可见,客户需要自行设置可见范围。
6. Ip白名单配置
我们可以在服务商信息-Ip白名单配置修改可信任的白名单
剩余代码
WXBizMsgCrypt
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Base64;
public class WXBizMsgCrypt {
byte[] aesKey;
String token;
String receiveId;
/**
* 构造函数
*
* @param token 企业微信后台,开发者设置的token
* @param encodingAesKey 企业微信后台,开发者设置的EncodingAESKey
* @param receiveId, 不同场景含义不同,详见文档
*/
public WXBizMsgCrypt(String token, String encodingAesKey, String receiveId) {
this.token = token;
this.receiveId = receiveId;
aesKey = Base64.getDecoder().decode(encodingAesKey + "=");
}
/**
* 验证并获取解密后数据
*
* @param msgSignature 签名串,对应URL参数的msg_signature
* @param timeStamp 时间戳,对应URL参数的timestamp
* @param nonce 随机串,对应URL参数的nonce
* @param echoStr 随机串,对应URL参数的echostr
* @return 解密之后的echoString
* @throws Exception 执行失败,请查看该异常的错误码和具体的错误信息
*/
public String verifyAndGetData(String msgSignature, String timeStamp, String nonce, String echoStr)
throws Exception {
String signature = getSignature(token, timeStamp, nonce, echoStr);
if (!signature.equals(msgSignature)) {
throw new Exception("参数验签不通过");
}
return decrypt(echoStr);
}
/**
* 获取参数签名
*
* @param token
* @param timestamp
* @param nonce
* @param encrypt
* @return
* @throws Exception
*/
private String getSignature(String token, String timestamp, String nonce, String encrypt) throws Exception {
String[] array = new String[]{token, timestamp, nonce, encrypt};
StringBuilder sb = new StringBuilder();
// 字符串排序
Arrays.sort(array);
for (String s : array) {
sb.append(s);
}
String str = sb.toString();
// SHA1签名生成
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(str.getBytes());
byte[] digest = md.digest();
StringBuilder hexStringBuilder = new StringBuilder();
String shaHex;
for (byte b : digest) {
shaHex = Integer.toHexString(b & 0xFF);
if (shaHex.length() < 2) {
hexStringBuilder.append(0);
}
hexStringBuilder.append(shaHex);
}
return hexStringBuilder.toString();
}
/**
* 对密文进行解密.
*
* @param text 需要解密的密文
* @return 解密得到的明文
* @throws Exception aes解密失败
*/
private String decrypt(String text) throws Exception {
// 设置解密模式为AES的CBC模式
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec secretKeySpec = new SecretKeySpec(aesKey, "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
// 使用BASE64对密文进行解码
byte[] encrypted = Base64.getDecoder().decode(text);
// 解密
byte[] original = cipher.doFinal(encrypted);
// 去除补位字符
byte[] bytes = decode(original);
// 分离16位随机字符串,网络字节序和receiveId
byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
int xmlLength = recoverNetworkBytesOrder(networkOrder);
String xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), StandardCharsets.UTF_8);
String fromReceiveId = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length),
StandardCharsets.UTF_8);
System.out.println("fromReceiveId:" + fromReceiveId);
// receiveId不相同的情况
if (!fromReceiveId.equals(receiveId)) {
throw new Exception("receiveId不相同");
}
return xmlContent;
}
/**
* 还原4个字节的网络字节序
*
* @param orderBytes
* @return
*/
int recoverNetworkBytesOrder(byte[] orderBytes) {
int sourceNumber = 0;
int length = 4;
for (int i = 0; i < length; i++) {
sourceNumber <<= 8;
sourceNumber |= orderBytes[i] & 0xff;
}
return sourceNumber;
}
/**
* 删除解密后明文的补位字符
*
* @param decrypted 解密后的明文
* @return 删除补位字符后的明文
*/
private byte[] decode(byte[] decrypted) {
int pad = decrypted[decrypted.length - 1];
int min = 1;
int max = 32;
if (pad < min || pad > max) {
pad = 0;
}
return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
}
}
完整的Service
import cn.hutool.json.XML;
import com.alibaba.fastjson.JSONObject;
import com.zhzhc.safe.admin.modules.wecom.aes.WXBizJsonMsgCrypt;
import com.zhzhc.safe.admin.modules.wecom.utils.WXBizMsgCrypt;
import me.chanjar.weixin.common.bean.WxJsapiSignature;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl;
import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Service
public class WeComService {
@Autowired
private RestTemplate restTemplate;
@Value("${ww.client.corpId}")
private String clientCorpId;
@Value("${ww.client.secret}")
private String clientSecret;
@Value("${ww.client.agentId}")
private String clientAgentId;
@Value("${ww.auth.Token}")
private String authToken;
@Value("${ww.auth.EncodingAESKey}")
private String authEncodingAESKey;
@Value("${ww.auth.corpId}")
private String authCorpId;
@Value("${ww.auth.corpSecret}")
private String authSecret;
@Value("${ww.auth.templateId}")
private String templateId;
@Value("${ww.auth.templateSecret}")
private String templateSecret;
private String accessToken = null;
private String suiteAccessToken = null;
private String suiteTicket = null;
private String permanentCode = null;
/**
* 根据access_token和code获取用户信息
*
* @param code 临时授权码,企业成员从企业微信内的网页、小程序、公众号等场景扫码授权后获得的
* @return 用户信息,封装在JSONObject中
*/
public JSONObject getUserInfo(String code) {
String url = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=" + getAccessToken() + "&code=" + code;
String resStr = restTemplate.getForObject(url, String.class);
return JSONObject.parseObject(resStr);
}
/**
* 获取访问令牌
*
* @return 返回访问令牌,如果当前访问令牌为空,则返回null
*/
public String getAccessToken() {
if(accessToken == null){
String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + clientCorpId + "&corpsecret=" + clientSecret;
String resStr = restTemplate.getForObject(url, String.class);
JSONObject jsonObject = JSONObject.parseObject(resStr);
if(jsonObject.getInteger("errcode") == 0){
accessToken = jsonObject.getString("access_token");
}
}
return accessToken;
}
/**
* 销毁访问令牌
*.
* <p>此方法用于销毁当前持有的访问令牌,确保访问令牌的安全性。
*/
public void destroyAccessToken(){
accessToken = null;
}
/**
* 向指定用户发送消息
*
* @param touser 接收消息的用户标识
* @param title 消息标题
* @param description 消息描述
* @param path 消息中链接的URL
*/
public void sendMessage(String touser,
String title,
String description,
String path){
String url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=" + getAccessToken();
Map<String, Object> textcard = new HashMap<>();
textcard.put("title", title);
textcard.put("btntxt", "立即处理");
textcard.put("url", path);
textcard.put("description", description);
Map<String, Object> data = new HashMap<>();
data.put("touser", touser);
data.put("msgtype", "textcard");
data.put("agentid", clientAgentId);
data.put("textcard", textcard);
restTemplate.postForObject(url, data, String.class);
}
public WxJsapiSignature getWxJsapiSignature(String url) throws WxErrorException {
WxCpDefaultConfigImpl config = new WxCpDefaultConfigImpl();
config.setCorpId(clientCorpId);
config.setCorpSecret(clientSecret);
config.setAgentId(Integer.valueOf(clientAgentId));
WxCpServiceImpl wxCpService = new WxCpServiceImpl();
wxCpService.setWxCpConfigStorage(config);
return wxCpService.createJsapiSignature(url);
}
// 前端jsapi_ticket签名
public String authVerifyEffectiveness(String msgSignature, String timestamp, String nonce, String echostr) throws Exception {
try {
System.out.println("clientCorpId " + clientCorpId);
WXBizJsonMsgCrypt wxcpt = new WXBizJsonMsgCrypt(authToken, authEncodingAESKey, authEncryptCorpId(clientCorpId));
return wxcpt.VerifyURL(msgSignature, timestamp, nonce, echostr);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public String authCallBack(HttpServletRequest request, String msgSignature, String timestamp, String nonce, Map<String, Object> map) throws Exception {
try {
String xmlString = getXMLString(request);
String encryptData = XML.toJSONObject(xmlString).getJSONObject("xml").getStr("Encrypt");
System.out.println(encryptData);
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(authToken, authEncodingAESKey, templateId);
String sEchoStr = wxcpt.verifyAndGetData(msgSignature, timestamp, nonce, encryptData);
cn.hutool.json.JSONObject jsonObject = XML.toJSONObject(sEchoStr).getJSONObject("xml");
if(jsonObject.getStr("InfoType").equals("suite_ticket")){
// 更新suiteTicket
suiteTicket = jsonObject.getStr("SuiteTicket");
} else if(jsonObject.getStr("InfoType").equals("reset_permanent_code")){
String authCode = jsonObject.getStr("AuthCode");
String clientSecret = getPermanentCode(authCode);
}
} catch (Exception e) {
e.printStackTrace();
}
return "success";
}
private String getXMLString(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
BufferedReader br = null;
try {
br = request.getReader();
char[] buff = new char[1024];
int len;
while ((len = br.read(buff)) > 0) {
sb.append(buff, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
public String authEncryptCorpId(String corpId) {
String url = "https://qyapi.weixin.qq.com/cgi-bin/service/corpid_to_opencorpid?provider_access_token=" + getProviderToken();
Map<String, Object> data = new HashMap<>();
data.put("corpid", corpId);
JSONObject res = restTemplate.postForObject(url, data, JSONObject.class);
if (res != null && res.getInteger("errcode") == 0) {
return res.getString("open_corpid");
} else {
throw new RuntimeException("获取加密corpId失败");
}
}
public String getProviderToken(){
String url = "https://qyapi.weixin.qq.com/cgi-bin/service/get_provider_token?debug=1";
Map<String, Object> data = new HashMap<>();
data.put("corpid", authCorpId);
data.put("provider_secret", authSecret);
data.put("debug", 1);
JSONObject res = restTemplate.postForObject(url, data, JSONObject.class);
System.out.println("===========getProviderToken=============");
System.out.println(res);
if (res != null) {
return res.getString("provider_access_token");
} else {
throw new RuntimeException("获取providerToken失败");
}
}
public void destroyProviderToken(){
suiteAccessToken = null;
}
public String getSuiteToken(){
if(suiteAccessToken != null){
return suiteAccessToken;
}
String url = "https://qyapi.weixin.qq.com/cgi-bin/service/get_suite_token";
Map<String, Object> data = new HashMap<>();
data.put("suite_id", templateId);
data.put("suite_secret", templateSecret);
data.put("suite_ticket", suiteTicket);
JSONObject res = restTemplate.postForObject(url, data, JSONObject.class);
if (res != null) {
suiteAccessToken = res.getString("suite_access_token");
return suiteAccessToken;
} else {
throw new RuntimeException("获取suiteAccessToken失败");
}
}
public String getPermanentCode(String authCode){
String url = "https://qyapi.weixin.qq.com/cgi-bin/service/get_permanent_code?suite_access_token=" + getSuiteToken();
Map<String, Object> data = new HashMap<>();
data.put("auth_code", authCode);
JSONObject res = restTemplate.postForObject(url, data, JSONObject.class);
System.out.println("======getPermanentCode=======");
System.out.println(res.toJSONString());
if (res != null) {
permanentCode = res.getString("permanent_code");
return permanentCode;
} else {
throw new RuntimeException("获取代开发应用secret失败");
}
}
public String getSuiteTicket() {
return suiteTicket;
}
public String getPermanentCode() {
return permanentCode;
}
}
作者也是第一次进行企业微信服务商代开发,如果上述内容有误,请各位大佬指点一下。
如果有疑问欢迎在评论留言或私聊我。