之前一直没有写这篇文章,是觉得企微的服务商应用相对简单。第二个原因是最近在弄钉钉的ISV上架,所以时间不是很充足。正题开始……
1、使用管理员登录服务商管理后台
企业微信-服务商后台-登录地址https://open.work.weixin.qq.com/wwopen/login2、输入基本信息及认证


1、登录后,直接创建创建网页应用,如下图。

2、应用详情-使用配置-参照下面的教程。主页地址是固定的,只需要写入自己的appid和redirect_uri的地址就行。这里需要注意,可信域名必须配置一下,需要注意的是这个一级域名一旦使用的服务商应用,那么自建应用是无法在使用这个域名的,即便是不同的二级域名也不可以。这个还是比较坑,导致我们重新申请了一个一级域名来服务自建应用。

3、可信域名配置,使用配置-点击【编辑】,然后点击【校验可信域名归属】,然后下载这个文件到nginx配置的域名文件夹下,只要通过步骤2的地址可以访问到就算验证通过。nginx的配置参照下面的这个文章。

4、数据回调的配置

1、pom.xml中添加解析XML格式内容
-
-
- <dependency>
- <groupId>org.jdomgroupId>
- <artifactId>jdom2artifactId>
- <version>2.0.6version>
- dependency>
- <dependency>
- <groupId>commons-codecgroupId>
- <artifactId>commons-codecartifactId>
- <version>1.15version>
- dependency>
2、properties文件,不需要那么多,命名更具自己的喜好,这一看就是参照了gitee的binarywang/weixin-java-cp-demo,这个demo如果初学者可以看看,然后自己封装。

3、核心解密controller.java
- package cn.renkai721.controller;
-
- import cn.renkai721.bean.*;
- import cn.renkai721.configuration.QywxProperties;
- import cn.renkai721.service.*;
- import cn.renkai721.util.HttpUtil;
- import cn.renkai721.util.MsgUtil;
- import cn.renkai721.util.WxUtil;
- import cn.renkai721.wechataes.WXBizMsgCrypt;
- import com.alibaba.druid.util.StringUtils;
- import com.alibaba.fastjson.JSON;
- import lombok.extern.slf4j.Slf4j;
- import org.redisson.api.RBucket;
- import org.redisson.api.RedissonClient;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.http.ResponseEntity;
- import org.springframework.scheduling.annotation.EnableAsync;
- import org.springframework.web.bind.annotation.*;
- import org.springframework.web.client.RestTemplate;
-
- import javax.annotation.Resource;
- import javax.servlet.ServletInputStream;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import java.io.BufferedReader;
- import java.io.InputStreamReader;
- import java.util.Map;
-
-
- @EnableAsync
- @RestController
- @RequestMapping("/d3f")
- @Slf4j
- public class D3f2Controller {
- @Resource
- private RedissonClient redissonClient;
- @Autowired
- private RestTemplate restTemplate;
- @Autowired
- private D3fService d3fService;
-
-
- @GetMapping(produces = "text/plain;charset=utf-8")
- public void d3fGet(@RequestParam(name = "msg_signature", required = false) String signature,
- @RequestParam(name = "timestamp", required = false) String timestamp,
- @RequestParam(name = "nonce", required = false) String nonce,
- @RequestParam(name = "echostr", required = false) String echostr,
- HttpServletResponse response) throws Exception {
- response.setContentType("text/html;charset=utf-8");
- response.setStatus(HttpServletResponse.SC_OK);
- WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(
- MsgUtil.val("wechat.cp.appConfigs[0].token"),
- MsgUtil.val("wechat.cp.appConfigs[0].aesKey"),
- MsgUtil.val("wechat.cp.corpId"));
- // 需要返回的明文
- String sEchoStr = "";
- try {
- sEchoStr = wxcpt.VerifyURL(signature, timestamp, nonce, echostr);
- log.info("resp sEchoStr={}",sEchoStr);
- response.getWriter().print(sEchoStr);
- return;
- } catch (Exception e) {
- // 验证URL失败,错误原因请查看异常
- e.printStackTrace();
- }
- response.getWriter().print("非法请求");
- return;
- }
-
- @PostMapping(produces = "application/xml; charset=UTF-8")
- public void d3fPost(@RequestParam("msg_signature") String signature,
- @RequestParam("timestamp") String timestamp,
- @RequestParam("nonce") String nonce,
- HttpServletResponse response,
- HttpServletRequest request) throws Exception {
- String success = "success";
- String type = request.getParameter("type");
- String corpid = request.getParameter("corpid");
- log.info("接收d3f post请求:[signature=[{}], timestamp=[{}], nonce=[{}], type=[{}], corpid=[{}] ]",
- signature, timestamp, nonce, type, corpid);
- try{
- response.setContentType("text/html;charset=utf-8");
- response.setStatus(HttpServletResponse.SC_OK);
- String id = "";
- // 访问应用和企业回调传不同的ID
- if("data".equals(type)){
- // 企微后台设置【数据回调URL】的链接为https://wx.naturobot.com/qywx/d3f?type=data&corpid=$CORPID$
- id = corpid;
- } else {
- id = MsgUtil.val("suite_id");
- }
- WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(MsgUtil.val(
- "wechat.cp.appConfigs[0].token"),
- MsgUtil.val("wechat.cp.appConfigs[0].aesKey"),
- id);
- // 密文,对应POST请求的数据
- String postData = "";
- // 获取加密的请求消息:使用输入流获得加密请求消息postData
- ServletInputStream in = request.getInputStream();
- BufferedReader reader = new BufferedReader(new InputStreamReader(in));
- // 作为输出字符串的临时串,用于判断是否读取完毕
- String tempStr = "";
- while(null != (tempStr=reader.readLine())){
- postData += tempStr;
- }
- String suiteXml = wxcpt.DecryptMsg(signature, timestamp, nonce, postData);
- Map suiteMap = WxUtil.transferXmlToMap(suiteXml);
- log.info("\n req map={}", suiteMap);
- if("suite_ticket".equals(suiteMap.get("InfoType"))){
- // https://developer.work.weixin.qq.com/document/10975#%E8%8E%B7%E5%8F%96%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%94%E7%94%A8%E5%87%AD%E8%AF%81
- // 主动推送SuiteTicket直接写入
- // 每十分钟更新一次
- //suite_ticket实际有效期为30分钟,
- String suite_ticket_value = (String) suiteMap.get("SuiteTicket");
- String SuiteId = (String) suiteMap.get("SuiteId");
- log.info("suite_ticket={},SuiteId={}",suite_ticket_value,SuiteId);
- RBucket
idBucket = redissonClient.getBucket(QywxProperties.suite_ticket_key); - idBucket.set(suite_ticket_value);
- // 调用企业微信接口
- d3fService.get_suite_access_token();
-
- }else if("create_auth".equals(suiteMap.get("InfoType"))){
- String authCode = (String) suiteMap.get("AuthCode");
- // SuiteId代表一个企业,相当于suite_id
- String SuiteId = (String) suiteMap.get("SuiteId");
- log.info("第三方应用测试上线,AuthCode={},SuiteId={}",authCode,SuiteId);
- RBucket
idBucket = redissonClient.getBucket(QywxProperties.authCode_key+"_"+SuiteId); - idBucket.set(authCode);
- // 获取企业永久授权码
- idBucket = redissonClient.getBucket(QywxProperties.suite_access_token_key);
- String suite_access_token = idBucket.get();
- String url1 = "https://qyapi.weixin.qq.com/cgi-bin/service/get_permanent_code?suite_access_token="+suite_access_token;
- PermanentReqBean permanentReqBean = new PermanentReqBean();
- permanentReqBean.setAuth_code(authCode);
- ResponseEntity
postForEntity1 = restTemplate.postForEntity(url1, permanentReqBean, PermanentRespBean.class); - log.info("get_permanent_code={}",postForEntity1.getBody());
- if(postForEntity1.getBody().getExpires_in() != null){
- String authCorpId = postForEntity1.getBody().getAuth_corp_info().getCorpid();
- log.info("永久授权码中获取的第三方应用的authCorpId={}",authCorpId);
- String userIdD3f = postForEntity1.getBody().getAuth_user_info().getUserid();
- // 直接取第一个
- String agentId = postForEntity1.getBody().getAuth_info().getAgent().get(0).getAgentid();
- String permanent_code_access_token = postForEntity1.getBody().getAccess_token();
- String permanent_code = postForEntity1.getBody().getPermanent_code();
- log.info("permanent_code={}",permanent_code);
- String open_userid = postForEntity1.getBody().getAuth_user_info().getOpen_userid();
- // 可以设置企业的许可自动激活状态
- // 这里面的东西需要保存下来,不然后面使用的时候没有了就完蛋了
- // 这里面的东西需要保存下来,不然后面使用的时候没有了就完蛋了
- // 这里面的东西需要保存下来,不然后面使用的时候没有了就完蛋了
- }else{
- log.error("get_permanent_code api is error");
- }
- }else if("cancel_auth".equals(suiteMap.get("InfoType"))){
- String AuthCorpId = (String) suiteMap.get("AuthCorpId");
- log.info("取消订阅cancel_auth AuthCorpId={}",AuthCorpId);
- }
-
- if("unlicensed_notify".equals(suiteMap.get("Event"))){
- // 该用户帐号未授权
- String AgentID = (String) suiteMap.get("AgentID");
- String ToUserName = (String) suiteMap.get("ToUserName");
- String FromUserName = (String) suiteMap.get("FromUserName");
- log.info("用户帐号没有开通授权,需要授权");
- }else if("change_app_admin".equals(suiteMap.get("Event"))){
- String AgentID = (String) suiteMap.get("AgentID");
- // ToUserName=corpId
- String ToUserName = (String) suiteMap.get("ToUserName");
- log.info("第三方应用change_app_admin,ToUserName={},AgentID={}",ToUserName,AgentID);
- }else if("subscribe".equals(suiteMap.get("Event"))){
- log.info("新用户关注,user={}",suiteMap);
- // 回复感谢关注
- String ToUserName = (String) suiteMap.get("ToUserName");
- String FromUserName = (String) suiteMap.get("FromUserName");
- String AgentID = (String) suiteMap.get("AgentID");
- // 获取临时授权码
- RBucket
idBucket = redissonClient.getBucket(QywxProperties.suite_access_token_key); - String suite_access_token = idBucket.get();
- String url1 = "https://qyapi.weixin.qq.com/cgi-bin/service/get_pre_auth_code?suite_access_token=" + suite_access_token;
- String postData1 = HttpUtil.sendGet(url1);
- log.info("get_pre_auth_code={}", postData1);
- String subscribe_pre_auth_code = JSON.parseObject(postData1).getString("pre_auth_code");
- String expires_in = JSON.parseObject(postData1).getString("expires_in");
- if(!StringUtils.isEmpty(expires_in)){
- // 设置授权配置
- url1 = "https://qyapi.weixin.qq.com/cgi-bin/service/set_session_info?suite_access_token=" + suite_access_token;
- SessionInfoReqBean sessionInfoReqBean = new SessionInfoReqBean();
- sessionInfoReqBean.setPre_auth_code(subscribe_pre_auth_code);
- SessionInfoBean sessionInfoBean = new SessionInfoBean();
- sessionInfoBean.setAppid(new Integer[0]);
- sessionInfoBean.setAuth_type(Integer.parseInt(MsgUtil.val("authType")));
- sessionInfoReqBean.setSession_info(sessionInfoBean);
- log.info("sessionInfoReqBean={}", JSON.toJSONString(sessionInfoReqBean));
- ResponseEntity
postForEntity = restTemplate.postForEntity(url1, sessionInfoReqBean, SessionInfoRespBean.class); - log.info("设置授权配置={}", postForEntity.getBody());
- }else{
- log.error("get_pre_auth_code api is error");
- }
- // 发送XML消息给用户
- String Title = "谢谢安装该应用";
- String Description = "我们的应用很好用,如果有问题请拨打电话021-12345";
- String Url = MsgUtil.val("poster.freeUrl");
- String PicUrl = "https://wx.naturobot.com/qywx/image/bg1.png";
- log.info("Title={},Description={},Url={},PicUrl={},",Title,Description,Url,PicUrl);
- String xmlOutMsg = wxcpt.getXmlNewsMessage(FromUserName,ToUserName,Title,Description,Url,PicUrl);
- success = wxcpt.EncryptMsg(xmlOutMsg, timestamp, nonce);
- }else if("enter_agent".equals(suiteMap.get("Event"))){
- // 用户打开应用的事件
- }
-
- if("text".equals(suiteMap.get("MsgType"))){
- // 用户发送了文本消息给应用
- String ToUserName = (String) suiteMap.get("ToUserName");
- String FromUserName = (String) suiteMap.get("FromUserName");
- String AgentID = (String) suiteMap.get("AgentID");
- String xmlOutMsg = wxcpt.getXmlTextMessage(FromUserName,ToUserName,"暂未开启聊天功能。");
- success = wxcpt.EncryptMsg(xmlOutMsg, timestamp, nonce);
- }
-
- } catch (Exception e) {
- e.printStackTrace();
- }
- response.getWriter().print(success);
- return;
- }
-
-
-
- }
4、解密工具WXBizMsgCrypt.java
- /**
- * 对企业微信发送给企业后台的消息加解密示例代码.
- *
- * @copyright Copyright (c) 1998-2014 Tencent Inc.
- */
-
- // ------------------------------------------------------------------------
-
- /**
- * 针对org.apache.commons.codec.binary.Base64,
- * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本)
- * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi
- */
- package cn.renkai721.wechataes;
-
- import org.apache.commons.codec.binary.Base64;
-
- import javax.crypto.Cipher;
- import javax.crypto.spec.IvParameterSpec;
- import javax.crypto.spec.SecretKeySpec;
- import java.nio.charset.Charset;
- import java.util.Arrays;
- import java.util.Random;
-
- /**
- * 提供接收和推送给企业微信消息的加解密接口(UTF8编码的字符串).
- *
- *
- 第三方回复加密消息给企业微信
- *
- 第三方收到企业微信发送的消息,验证消息的安全性,并对消息进行解密。
- *
- * 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案
- *
- *
- 在官方网站下载JCE无限制权限策略文件(JDK7的下载地址:
- * http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
- *
- 下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt
- *
- 如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件
- *
- 如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件
- *
- */
- public class WXBizMsgCrypt {
- static Charset CHARSET = Charset.forName("utf-8");
- Base64 base64 = new Base64();
- byte[] aesKey;
- String token;
- String receiveid;
-
- /**
- * 构造函数
- * @param token 企业微信后台,开发者设置的token
- * @param encodingAesKey 企业微信后台,开发者设置的EncodingAESKey
- * @param receiveid, 不同场景含义不同,详见文档
- *
- * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
- */
- public WXBizMsgCrypt(String token, String encodingAesKey, String receiveid) throws AesException {
- if (encodingAesKey.length() != 43) {
- throw new AesException(AesException.IllegalAesKey);
- }
-
- this.token = token;
- this.receiveid = receiveid;
- aesKey = Base64.decodeBase64(encodingAesKey + "=");
- }
-
- // 生成4个字节的网络字节序
- byte[] getNetworkBytesOrder(int sourceNumber) {
- byte[] orderBytes = new byte[4];
- orderBytes[3] = (byte) (sourceNumber & 0xFF);
- orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF);
- orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF);
- orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF);
- return orderBytes;
- }
-
- // 还原4个字节的网络字节序
- int recoverNetworkBytesOrder(byte[] orderBytes) {
- int sourceNumber = 0;
- for (int i = 0; i < 4; i++) {
- sourceNumber <<= 8;
- sourceNumber |= orderBytes[i] & 0xff;
- }
- return sourceNumber;
- }
-
- // 随机生成16位字符串
- String getRandomStr() {
- String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
- Random random = new Random();
- StringBuffer sb = new StringBuffer();
- for (int i = 0; i < 16; i++) {
- int number = random.nextInt(base.length());
- sb.append(base.charAt(number));
- }
- return sb.toString();
- }
-
- /**
- * 对明文进行加密.
- *
- * @param text 需要加密的明文
- * @return 加密后base64编码的字符串
- * @throws AesException aes加密失败
- */
- String encrypt(String randomStr, String text) throws AesException {
- ByteGroup byteCollector = new ByteGroup();
- byte[] randomStrBytes = randomStr.getBytes(CHARSET);
- byte[] textBytes = text.getBytes(CHARSET);
- byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length);
- byte[] receiveidBytes = receiveid.getBytes(CHARSET);
-
- // randomStr + networkBytesOrder + text + receiveid
- byteCollector.addBytes(randomStrBytes);
- byteCollector.addBytes(networkBytesOrder);
- byteCollector.addBytes(textBytes);
- byteCollector.addBytes(receiveidBytes);
-
- // ... + pad: 使用自定义的填充方式对明文进行补位填充
- byte[] padBytes = PKCS7Encoder.encode(byteCollector.size());
- byteCollector.addBytes(padBytes);
-
- // 获得最终的字节流, 未加密
- byte[] unencrypted = byteCollector.toBytes();
-
- try {
- // 设置加密模式为AES的CBC模式
- Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
- SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
- IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
- cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
-
- // 加密
- byte[] encrypted = cipher.doFinal(unencrypted);
-
- // 使用BASE64对加密后的字符串进行编码
- String base64Encrypted = base64.encodeToString(encrypted);
-
- return base64Encrypted;
- } catch (Exception e) {
- e.printStackTrace();
- throw new AesException(AesException.EncryptAESError);
- }
- }
-
- /**
- * 对密文进行解密.
- *
- * @param text 需要解密的密文
- * @return 解密得到的明文
- * @throws AesException aes解密失败
- */
- String decrypt(String text) throws AesException {
- byte[] original;
- try {
- // 设置解密模式为AES的CBC模式
- Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
- SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES");
- IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
- cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);
-
- // 使用BASE64对密文进行解码
- byte[] encrypted = Base64.decodeBase64(text);
-
- // 解密
- original = cipher.doFinal(encrypted);
- } catch (Exception e) {
- e.printStackTrace();
- throw new AesException(AesException.DecryptAESError);
- }
-
- String xmlContent, from_receiveid;
- try {
- // 去除补位字符
- byte[] bytes = PKCS7Encoder.decode(original);
-
- // 分离16位随机字符串,网络字节序和receiveid
- byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
-
- int xmlLength = recoverNetworkBytesOrder(networkOrder);
-
- xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
- from_receiveid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length),
- CHARSET);
- } catch (Exception e) {
- e.printStackTrace();
- throw new AesException(AesException.IllegalBuffer);
- }
-
- // receiveid不相同的情况
- System.out.println("------ from_receiveid="+from_receiveid+", receiveid="+receiveid);
- if (!from_receiveid.equals(receiveid)) {
- throw new AesException(AesException.ValidateCorpidError);
- }
- return xmlContent;
-
- }
-
- /**
- * 将企业微信回复用户的消息加密打包.
- *
- *
- 对要发送的消息进行AES-CBC加密
- *
- 生成安全签名
- *
- 将消息密文和安全签名打包成xml格式
- *
- *
- * @param replyMsg 企业微信待回复用户的消息,xml格式的字符串
- * @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp
- * @param nonce 随机串,可以自己生成,也可以用URL参数的nonce
- *
- * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串
- * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
- */
- public String EncryptMsg(String replyMsg, String timeStamp, String nonce) throws AesException {
- // 加密
- String encrypt = encrypt(getRandomStr(), replyMsg);
-
- // 生成安全签名
- if (timeStamp == "") {
- timeStamp = Long.toString(System.currentTimeMillis());
- }
-
- String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt);
-
- // System.out.println("发送给平台的签名是: " + signature[1].toString());
- // 生成发送的xml
- String result = XMLParse.generate(encrypt, signature, timeStamp, nonce);
- System.out.println("call wechat xml message=["+result+"]");
- return result;
- }
-
- /**
- * 检验消息的真实性,并且获取解密后的明文.
- *
- *
- 利用收到的密文生成安全签名,进行签名验证
- *
- 若验证通过,则提取xml中的加密消息
- *
- 对消息进行解密
- *
- *
- * @param msgSignature 签名串,对应URL参数的msg_signature
- * @param timeStamp 时间戳,对应URL参数的timestamp
- * @param nonce 随机串,对应URL参数的nonce
- * @param postData 密文,对应POST请求的数据
- *
- * @return 解密后的原文
- * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
- */
- public String DecryptMsg(String msgSignature, String timeStamp, String nonce, String postData)
- throws AesException {
-
- // 密钥,公众帐号的app secret
- // 提取密文
- Object[] encrypt = XMLParse.extract(postData);
-
- // 验证安全签名
- String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt[1].toString());
-
- // 和URL中的签名比较是否相等
- // System.out.println("第三方收到URL中的签名:" + msg_sign);
- // System.out.println("第三方校验签名:" + signature);
- if (!signature.equals(msgSignature)) {
- throw new AesException(AesException.ValidateSignatureError);
- }
-
- // 解密
- String result = decrypt(encrypt[1].toString());
- return result;
- }
-
- /**
- * 验证URL
- * @param msgSignature 签名串,对应URL参数的msg_signature
- * @param timeStamp 时间戳,对应URL参数的timestamp
- * @param nonce 随机串,对应URL参数的nonce
- * @param echoStr 随机串,对应URL参数的echostr
- *
- * @return 解密之后的echostr
- * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
- */
- public String VerifyURL(String msgSignature, String timeStamp, String nonce, String echoStr)
- throws AesException {
- String signature = SHA1.getSHA1(token, timeStamp, nonce, echoStr);
- if (!signature.equals(msgSignature)) {
- throw new AesException(AesException.ValidateSignatureError);
- }
- String result = decrypt(echoStr);
- return result;
- }
-
- public String getXmlTextMessage(String FromUserName,String ToUserName, String sendMsgText){
- // 文本消息
- String timestamp = Long.toString(System.currentTimeMillis()/1000L);
- return "
" + - "
+FromUserName+"]]> " + - "
+ToUserName+"]]> " + - "
" +timestamp+"" + - "
" + - "
+sendMsgText+"]]> " + - "";
- }
-
- public String getXmlNewsMessage(String FromUserName,String ToUserName, String Title, String Description, String Url, String PicUrl){
- // 图文消息
- String timestamp = Long.toString(System.currentTimeMillis()/1000L);
- return "
" + - "
+FromUserName+"]]> " + - "
+ToUserName+"]]> " + - "
" +timestamp+"" + - "
" + - "
" + - "
1 " + - "
" + - "
- "
+ - "
+Title+"]]> " + - "
+Description+"]]> " + - "
+Url+"]]> " + - "
+PicUrl+"]]> " + - " " +
- " " +
- "
0 " + - "";
- }
-
- }
5、WxUtil.java
- package cn.renkai721.util;
-
- import org.jdom2.Document;
- import org.jdom2.Element;
- import org.jdom2.JDOMException;
- import org.jdom2.input.SAXBuilder;
-
- import java.io.ByteArrayInputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.util.*;
-
- public class WxUtil {
- /**
- * 将 Map 转化为 XML
- *
- * @param map
- * @return
- */
- public static String transferMapToXml(SortedMap
map) { - StringBuffer sb = new StringBuffer();
- sb.append("
" ); - for (String key : map.keySet()) {
- sb.append("<").append(key).append(">")
- .append(map.get(key))
- .append("").append(key).append(">");
- }
- return sb.append("").toString();
- }
-
- /**
- * 将 XML 转化为 map
- *
- * @param strxml
- * @return
- * @throws IOException
- */
- public static Map transferXmlToMap(String strxml) throws IOException {
- strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
- if (null == strxml || "".equals(strxml)) {
- return null;
- }
- Map m = new HashMap();
- InputStream in = new ByteArrayInputStream(strxml.getBytes("UTF-8"));
- SAXBuilder builder = new SAXBuilder();
- Document doc = null;
- try {
- doc = builder.build(in);
- } catch (JDOMException e) {
- throw new IOException(e.getMessage()); // 统一转化为 IO 异常输出
- }
- // 解析 DOM
- Element root = doc.getRootElement();
- List list = root.getChildren();
- Iterator it = list.iterator();
- while (it.hasNext()) {
- Element e = (Element) it.next();
- String k = e.getName();
- String v = "";
- List children = e.getChildren();
- if (children.isEmpty()) {
- v = e.getTextNormalize();
- } else {
- v = getChildrenText(children);
- }
- m.put(k, v);
- }
- //关闭流
- in.close();
- return m;
- }
-
- // 辅助 transferXmlToMap 方法递归提取子节点数据
- private static String getChildrenText(List
children) { - StringBuffer sb = new StringBuffer();
- if (!children.isEmpty()) {
- Iterator
it = children.iterator(); - while (it.hasNext()) {
- Element e = (Element) it.next();
- String name = e.getName();
- String value = e.getTextNormalize();
- List
list = e.getChildren(); - sb.append("<" + name + ">");
- if (!list.isEmpty()) {
- sb.append(getChildrenText(list));
- }
- sb.append(value);
- sb.append("" + name + ">");
- }
- }
- return sb.toString();
- }
- }
-
6、XmlUtil.java
- package cn.renkai721.util;
-
- import javax.xml.bind.JAXBContext;
- import javax.xml.bind.Unmarshaller;
- import java.io.StringReader;
-
-
- public class XmlUtil {
-
-
- /**
- * 解析XMl内容,转换为POJO类
- *
- * @param clazz 要解析的对象
- * @param xml 解析的xml字符串
- * @return 解析完成的对象
- */
- public static Object xmlStrToObject(Class clazz, String xml) {
- Object xmlObject = null;
- try {
- JAXBContext context = JAXBContext.newInstance(clazz);
- // 进行将Xml转成对象的核心接口
- Unmarshaller unmarshaller = context.createUnmarshaller();
- StringReader sr = new StringReader(xml);
- xmlObject = unmarshaller.unmarshal(sr);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return xmlObject;
- }
-
-
- }
7、AesException.java
- package cn.renkai721.wechataes;
-
- @SuppressWarnings("serial")
- public class AesException extends Exception {
-
- public final static int OK = 0;
- public final static int ValidateSignatureError = -40001;
- public final static int ParseXmlError = -40002;
- public final static int ComputeSignatureError = -40003;
- public final static int IllegalAesKey = -40004;
- public final static int ValidateCorpidError = -40005;
- public final static int EncryptAESError = -40006;
- public final static int DecryptAESError = -40007;
- public final static int IllegalBuffer = -40008;
- //public final static int EncodeBase64Error = -40009;
- //public final static int DecodeBase64Error = -40010;
- //public final static int GenReturnXmlError = -40011;
-
- private int code;
-
- private static String getMessage(int code) {
- switch (code) {
- case ValidateSignatureError:
- return "签名验证错误";
- case ParseXmlError:
- return "xml解析失败";
- case ComputeSignatureError:
- return "sha加密生成签名失败";
- case IllegalAesKey:
- return "SymmetricKey非法";
- case ValidateCorpidError:
- return "corpid校验失败";
- case EncryptAESError:
- return "aes加密失败";
- case DecryptAESError:
- return "aes解密失败";
- case IllegalBuffer:
- return "解密后得到的buffer非法";
- // case EncodeBase64Error:
- // return "base64加密错误";
- // case DecodeBase64Error:
- // return "base64解密错误";
- // case GenReturnXmlError:
- // return "xml生成失败";
- default:
- return null; // cannot be
- }
- }
-
- public int getCode() {
- return code;
- }
-
- AesException(int code) {
- super(getMessage(code));
- this.code = code;
- }
-
- }
8、ByteGroup.java
- package cn.renkai721.wechataes;
-
- import java.util.ArrayList;
-
- public class ByteGroup {
- ArrayList
byteContainer = new ArrayList(); -
- public byte[] toBytes() {
- byte[] bytes = new byte[byteContainer.size()];
- for (int i = 0; i < byteContainer.size(); i++) {
- bytes[i] = byteContainer.get(i);
- }
- return bytes;
- }
-
- public ByteGroup addBytes(byte[] bytes) {
- for (byte b : bytes) {
- byteContainer.add(b);
- }
- return this;
- }
-
- public int size() {
- return byteContainer.size();
- }
- }
9、D3fService.java
- package cn.renkai721.biz;
-
- import cn.renkai721.bean.*;
- import cn.renkai721.configuration.QywxProperties;
- import cn.renkai721.util.HttpUtil;
- import cn.renkai721.util.MsgUtil;
- import com.alibaba.druid.util.StringUtils;
- import com.alibaba.fastjson.JSON;
- import com.alibaba.fastjson.JSONObject;
- import lombok.extern.slf4j.Slf4j;
- import org.redisson.api.RBucket;
- import org.redisson.api.RedissonClient;
- import org.springframework.http.ResponseEntity;
- import org.springframework.stereotype.Component;
- import org.springframework.web.client.RestTemplate;
-
- import javax.annotation.Resource;
- import java.util.*;
- import java.util.concurrent.TimeUnit;
-
-
- @Slf4j
- @Component
- public class D3fBiz {
-
- @Resource
- private RedissonClient redissonClient;
- @Resource
- private RestTemplate restTemplate;
-
- public String get_suite_ticket(){
- RBucket
idBucket = redissonClient.getBucket(QywxProperties.suite_ticket_key); - String get_suite_ticket = idBucket.get();
- return get_suite_ticket;
- }
-
- public String get_suite_access_token(){
- RBucket
idBucket = redissonClient.getBucket(QywxProperties.suite_access_token_key); - String suite_access_token = idBucket.get();
- log.info("suite_access_token={}",suite_access_token);
- if(StringUtils.isEmpty(suite_access_token)){
- String suite_ticket = this.get_suite_ticket();
- // 如果上线后,没有最新的suite,手动在企微控制台点击刷新ticket
- // 通过本接口获取的suite_access_token有效期为2小时,开发者需要进行缓存,不可频繁获取。
- // 参考地址=https://work.weixin.qq.com/api/doc/90001/90143/90600
- String url1= "https://qyapi.weixin.qq.com/cgi-bin/service/get_suite_token";
- Map
paramMap1 = new HashMap<>(); - paramMap1.put("suite_id", MsgUtil.val(QywxProperties.suite_id_key));
- paramMap1.put("suite_secret", MsgUtil.val(QywxProperties.suite_secret_key));
- paramMap1.put("suite_ticket", suite_ticket);
- String postData1 = HttpUtil.sendPost(url1, JSONObject.toJSONString(paramMap1));
- log.info("get_suite_token={}",postData1);
- suite_access_token = JSON.parseObject(postData1).getString(QywxProperties.suite_access_token_key);
- String expires_in = JSON.parseObject(postData1).getString("expires_in");
- if(!StringUtils.isEmpty(expires_in)){
- idBucket.set(suite_access_token,Integer.parseInt(expires_in), TimeUnit.SECONDS);
- }else{
- log.error("get_suite_token api is error");
- }
- }
- return suite_access_token;
- }
-
- public String get_access_token(String corpId){
- String suite_access_token = this.get_suite_access_token();
- RBucket
idBucket = redissonClient.getBucket(QywxProperties.corpId_suiteId_agentId+"_"+corpId); - String corpIdAndAgentId = idBucket.get();
- log.info("corpIdAndAgentId={}",corpIdAndAgentId);
- String permanent_code = corpIdAndAgentId.split(";")[3];
- Map paramMap1 = new HashMap<>();
- // 获取企业access_token
- idBucket = redissonClient.getBucket(QywxProperties.access_token_key+"_"+corpId);
- String access_token = idBucket.get();
- log.info("access_token={}",access_token);
- if(StringUtils.isEmpty(access_token)){
- String url1 = "https://qyapi.weixin.qq.com/cgi-bin/service/get_corp_token?suite_access_token="+suite_access_token;
- paramMap1 = new HashMap<>();
- paramMap1.put("auth_corpid", corpId);
- paramMap1.put("permanent_code", permanent_code);
- String postData1 = HttpUtil.sendPost(url1, JSONObject.toJSONString(paramMap1));
- log.info("get_corp_token={}",postData1);
- access_token = JSON.parseObject(postData1).getString("access_token");
- String expires_in = JSON.parseObject(postData1).getString("expires_in");
- if(!StringUtils.isEmpty(expires_in)){
- idBucket.set(access_token,Integer.parseInt(expires_in), TimeUnit.SECONDS);
- }else{
- log.error("get_corp_token is error");
- }
- }
- return access_token;
- }
-
-
- public void sendD3fTextMsg(String corpId, String toUser, String message){
- log.info("sendD3fTextMsg corpId={},toUser={},message={}"
- ,corpId,toUser,message);
- RBucket
idBucket = redissonClient.getBucket(QywxProperties.corpId_suiteId_agentId+"_"+corpId); - String corpIdAndAgentId = idBucket.get();
- log.info("corpIdAndAgentId={}",corpIdAndAgentId);
- String agentId = corpIdAndAgentId.split(";")[2];
- String access_token = this.get_access_token(corpId);
- String msgUrl = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="+access_token;
- MsgRequestDTO requestData = new MsgRequestDTO();
- requestData.setAgentid(Integer.parseInt(agentId));
- requestData.setTouser(toUser);
- requestData.setMsgtype("text");
- Map
text = new HashMap<>(); - text.put("content", message);
- requestData.setText(text);
- log.info("sendD3fTextMsg requestData={}",requestData);
- ResponseEntity
postForEntity = restTemplate.postForEntity(msgUrl, requestData, MsgResult.class); - log.info("sendD3fTextMsg postForEntity={}",postForEntity);
- }
-
- public void sendD3fNewsMsg(String corpId, String toUser, String Title,
- String Description, String Url, String PicUrl){
- log.info("sendD3fNewsMsg corpId={},toUser={},Title={},Description={},Url={},PicUrl={},"
- ,corpId,toUser,Title,Description,Url,PicUrl);
- RBucket
idBucket = redissonClient.getBucket(QywxProperties.corpId_suiteId_agentId+"_"+corpId); - String corpIdAndAgentId = idBucket.get();
- log.info("corpIdAndAgentId={}",corpIdAndAgentId);
- String agentId = corpIdAndAgentId.split(";")[2];
- String access_token = this.get_access_token(corpId);
- String msgUrl = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="+access_token;
- Map
body = new HashMap<>(); - body.put("touser",toUser);
- body.put("msgtype","news");
- body.put("agentid",Integer.parseInt(agentId));
- Map
news = new HashMap<>(); - List articles = new ArrayList();
- Map
article = new HashMap<>(); - article.put("title",Title);
- if(!StringUtils.isEmpty(Description)){
- article.put("description",Description);
- }
- if(!StringUtils.isEmpty(Url)){
- article.put("url",Url);
- }
- article.put("picurl",PicUrl);
- articles.add(article);
- news.put("articles",articles);
- body.put("news",news);
- JSONObject jsonObject = new JSONObject(body);
- log.info("sendNewsMsg body={},",jsonObject);
- ResponseEntity
postForEntity = restTemplate.postForEntity(msgUrl, jsonObject, MsgResult.class); - log.info("sendNewsMsg postForEntity={}",postForEntity);
- }
-
- public void sendMarkdownMsg(String corpId,String toUser,String message) {
- log.info("sendMarkdownMsg corpId={},toUser={},message={}"
- ,corpId,toUser,message);
- RBucket
idBucket = redissonClient.getBucket(QywxProperties.corpId_suiteId_agentId+"_"+corpId); - String corpIdAndAgentId = idBucket.get();
- log.info("corpIdAndAgentId={}",corpIdAndAgentId);
- String agentId = corpIdAndAgentId.split(";")[2];
- String access_token = this.get_access_token(corpId);
- String msgUrl = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="+access_token;
- Map
body = new HashMap<>(); - body.put("touser",toUser);
- body.put("msgtype","markdown");
- body.put("agentid",agentId);
- Map
markdown = new HashMap<>(); - markdown.put("content", message);
- body.put("markdown",markdown);
- JSONObject jsonObject = new JSONObject(body);
- log.info("sendMarkdownMsg body={},",jsonObject);
- ResponseEntity
postForEntity = restTemplate.postForEntity(msgUrl, jsonObject, MsgResult.class); - log.info("sendMarkdownMsg={}",postForEntity);
- }
-
- }
10、开发代码测试的时候,记得把服务器IP添加到白名单,使用管理员登录服务商后台,点击企业信息,然后输入IP。
1、开发结束后,登录到企业服务商管理后台,普通的管理员也可以操作。
2、【应用和模板上线】-【提交上线】-选一个要上线的应用-【确定】。

3、如果失败了,服务商后台的消息会收到通知,成功也会收到通知。
4、上线成功后,可以设置应用市场可搜索的配置,发布上线还是要填写一些东西,包括图片,需要美工制作专门格式的图片才可以。
企业微信服务商-开发前必读 - 接口文档
https://developer.work.weixin.qq.com/document/path/91201企微服务商平台收费接口对接教程_renkai721的博客-CSDN博客_企微服务商前言1、以前的流程是用户添加第三方应用,然后登录,然后操作。2、现在的流程是用户添加第三方应用,然后服务商购买账号,服务商在用户添加第三方应用时或用户登录时或接收到【unlicensed_notify】接口许可失效通知时,授权激活该用户,然后用户登录,然后操作。企微官方文档面向服务商进行平台收费模式调整的说明平台接口许可付费企微服务商后台管理操作教程1、用户在企微应用市场搜索服务商开发的第3方应用,假如应用名字【天气助手】。然后点击安装。2、这时候服务商的后台服务会收到腾讯服https://blog.csdn.net/renkai721/article/details/124970456解读:企微面向服务商进行平台收费模式调整的说明_renkai721的博客-CSDN博客前言1、以前的流程是用户添加第三方应用,然后登录,然后操作。2、现在是服务商购买账号,服务商在用户添加第三方应用时或用户登录时授权激活该用户,然后用户登录,然后操作。企微官方文档面向服务商进行平台收费模式调整的说明平台接口许可付费一、如果不购买【基础帐号】,那么【身份验证】【小程序登录】【发送应用消息】这3个接口无法调用。表现出来的场景为:1、第三方应用和小程序的用户是无法登录的。2、也不能调用接口API发送消息给用户。二、如果不购买【互通帐号】,那么【获取.
https://blog.csdn.net/renkai721/article/details/124675211