企业微信开发(二):API对接及Demo程序

简介: API对接及Demo程序

在上一篇文章《企来微信开发(一):开通及调研》中,介绍了进行企业微信开发背景,及一些调研的过程,本篇文章针对API的对接,及Demo程序的开发来进行介绍,这个过程中遇到了挺多坑的。

一、企业微信SDK下载及API介绍

从官网的技术文档的连接:企业微信 - 获取会话内容 中,可以下载到SDK,本文下载的是Linux SDK: 下载 SDK v1.2 , 解压后的文件如下:

下载的sdk中,提供了java/c的sdk包,本公司的项目都是基于java的,所以使用java_sdk。在文件列表中:

  • sdkdemo.java : 官方提供的java调用dll(.so)的例子。
  • libWeWorkFinanceSdk_Java.so : 官方封装好的dll(.so)文件,需要放到Java的环境目录下/或者绝对目录下。
  • com目录 : 这个目录是官方生成的java调.so的API的类文件,这个包结构是不能改变的(要原封不动地放到项目中),只有一个类:com.tencent.wework Finance.java

二、搭建Demo项目及API对接

1、准备工作

新建一个项目,比如ewxchat-demo,这是我的项目名称,将上面的sdkdemo.java,及com整个目录拷到项目中,如下图:
image.png
image.png

sdkdemo中的例子代码,他是通过命令行的方式来交互的。在实际的开发中,我并没有使用sdkdemo中的代码,重新写了另外的单元测试类。

2、 准备RSA的私钥,公钥

在项目中,我是使用openssl来生成的,网上也有其他工具可以生成。

  • 用 openssl genrsa -out private.pem 2048 来生成私钥,并保存。
  • 用 openssl rsa -in private.pem -pubout -out public.pem 从私钥来产生公钥,并保存,需要配置到企业微信上(在前面的文章中有介绍配置)。

3、 pom.xml配置

项目中主要引入的jar,要按如下来来配置,网上及官方有提供,但针对不同版本的jdk,都可能存在问题,我使用的jdk是1.8版本,用这些jar测试是没有问题。

<dependency>
   <groupId>commons-codec</groupId>
   <artifactId>commons-codec</artifactId>
</dependency>
<dependency>
   <groupId>org.json</groupId>
   <artifactId>json</artifactId>
   <version>20210307</version>
</dependency>
      <dependency>
          <groupId>org.jdom</groupId>
          <artifactId>jdom</artifactId>
          <version>2.0.2</version>
      </dependency>
      <dependency>
          <groupId>org.bouncycastle</groupId>
          <artifactId>bcpg-jdk15on</artifactId>
          <version>1.64</version>
      </dependency>
      <dependency>
          <groupId>org.bouncycastle</groupId>
          <artifactId>bcpkix-jdk15on</artifactId>
          <version>1.64</version>
      </dependency>
      <dependency>
          <groupId>org.bouncycastle</groupId>
          <artifactId>bcprov-jdk15on</artifactId>
          <version>1.64</version>
      </dependency>

4、 准备RSA 工个类

public static String decryptRSA(String str, String privateKey) throws Exception {
   
   
    Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
    //此处的"RSA/ECB/PKCS1Padding", "BC"不可以改变,改变会导致解密乱码
    Cipher rsa = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC");
    rsa.init(Cipher.DECRYPT_MODE, getPrivateKey(privateKey));
    byte[] utf8 = rsa.doFinal(Base64.decodeBase64(str));
    String result = new String(utf8,"UTF-8");
    return result;
}
public static PrivateKey getPrivateKey (String privateKey) throws Exception {
   
   
    Reader privateKeyReader = new StringReader(privateKey);
    PEMParser privatePemParser = new PEMParser(privateKeyReader);
    Object privateObject = privatePemParser.readObject();
    if (privateObject instanceof PEMKeyPair) {
   
   
        PEMKeyPair pemKeyPair = (PEMKeyPair) privateObject;
        JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
        PrivateKey privKey = converter.getPrivateKey(pemKeyPair.getPrivateKeyInfo());
        return privKey;
    }
    return null;
}

5、 Xml工具类

public static JSONObject xml2Json(String xmlStr) throws JDOMException, IOException {
   
   
    if (!StringUtils.hasLength(xmlStr)) {
   
   
        return null;
    }
    xmlStr = xmlStr.replaceAll("\\\n", "");
    byte[] xml = xmlStr.getBytes("UTF-8");
    //JSONObject json = new JSONObject();
    InputStream is = new ByteArrayInputStream(xml);
    SAXBuilder sb = new SAXBuilder();
    Document doc = sb.build(is);
    Element root = doc.getRootElement();
    //json.put(root.getName(), iterateElement(root));
    return iterateElement(root);
}
private static JSONObject iterateElement(Element element) {
   
   
    List<Element> node = element.getChildren();
    JSONObject obj = new JSONObject();
    List list = null;
    for (Element child : node) {
   
   
        list = new LinkedList();
        String text = child.getTextTrim();
        if (!StringUtils.hasLength(text)) {
   
   
            if (child.getChildren().size() == 0) {
   
   
                continue;
            }
            if (obj.has(child.getName())) {
   
   
                list = (List) obj.get(child.getName());
            }
            list.add(iterateElement(child)); //遍历child的子节点 
           obj.put(child.getName(), list);
        } else {
   
   
            if (obj.has(child.getName())) {
   
   
                Object value = obj.get(child.getName());
                try {
   
   
                    list = (List) value;
                } catch (ClassCastException e) {
   
   
                    list.add(value);
                }
            }
            if (child.getChildren().size() == 0) {
   
    //child无子节点时直接设置text
                obj.put(child.getName(), text);
            } else {
   
   
                list.add(text);
                obj.put(child.getName(), list);
            }
        }
    }
    return obj;
}
 public static void main(String[] args){
   
   
    String str = "<xml><ToUserName><![CDATA[ww97e7f6721fd99ce9]]></ToUserName><FromUserName><![CDATA[sys]]></FromUserName><CreateTime>1654915333</CreateTime><MsgType><![CDATA[event]]></MsgType><AgentID>2000004</AgentID><Event><![CDATA[msgaudit_notify]]></Event></xml>";
    try {
   
   
        JSONObject out = XmlUtils.xml2Json(str);
        System.out.println(out);
    } catch (JDOMException e) {
   
   
        e.printStackTrace();
    } catch (IOException e) {
   
   
        e.printStackTrace();
    }
}

6、拉取会话的实现

WxchatMsgPullService.java

@Value("${corp_id}")
    private String corpId;
    @Value("${corp_key}")
    private String corpKey;
    @Value("${media.save.path}")
    private String mediaSavePath;
    //私钥
    private String privateKey;
    private long sdk;
    private Boolean sdkSuccess = Boolean.FALSE;


    private void init(){
   
   
        if(sdkSuccess){
   
   
            return;
        }

        //log.info("{}", System.getProperty("java.library.path"));
        this.sdk = Finance.NewSdk();
        log.info("corpId: {}, corpKey: {}", corpId, corpKey);
        long ret = Finance.Init(sdk, corpId, corpKey); // 初始化
        if(ret != 0){
   
   
            Finance.DestroySdk(sdk);
            log.error("init sdk err : {}", ret);
            throw new RuntimeException("init wxchat sdk error");
        }
        sdkSuccess = true;
    }

    /**
     * 会话消息的定义:
     * 参数    说明
     * msgid    消息id,消息的唯一标识,企业可以使用此字段进行消息去重。String类型
     * action    消息动作,目前有send(发送消息)/recall(撤回消息)/switch(切换企业日志)三种类型。String类型
     * from    消息发送方id。同一企业内容为userid,非相同企业为external_userid。消息如果是机器人发出,也为external_userid。String类型
     * tolist    消息接收方列表,可能是多个,同一个企业内容为userid,非相同企业为external_userid。数组,内容为string类型
     * roomid    群聊消息的群id。如果是单聊则为空。String类型
     * msgtime    消息发送时间戳,utc时间,ms单位。
     * msgtype    文本消息为:text。String类型
     * content    消息内容。String类型
     * @return
     */
    public String readMsgs(){
   
   
        init();

        // 从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,
        // seq为之前接口返回的最大seq值。首次使用请使用seq:0(这个值需要记录下来,以便下一次的拉去)
        int seq = 0;
        int limit = 60;
        long slice = Finance.NewSlice();
        long ret = Finance.GetChatData(sdk, seq, limit, null, null, 3, slice);
        if (ret != 0) {
   
   
            log.error("Call Finance.GetChatData error: {}", ret);
            throw new IllegalStateException("调用Finance.GetChatData错误:" + ret);
        }

        String getchatdata = Finance.GetContentFromSlice(slice);
        log.info("序号:{}, 拉去的聊天记录密文结果:{}", seq, getchatdata);

        JSONObject jo = new JSONObject(getchatdata);
        JSONArray chatdata = jo.getJSONArray("chatdata");

        log.info("消息数:{}", chatdata.length());

        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < chatdata.length(); i++) {
   
   
            JSONObject data = new JSONObject(chatdata.get(i).toString());
            String encryptRandomKey = data.getString("encrypt_random_key");
            String encryptChatMsg   = data.getString("encrypt_chat_msg");
            long msg = Finance.NewSlice();
            try {
   
   
                // 聊天记录密文解密
                String message = RSAKit.decryptRSA(encryptRandomKey, Conf.RSA_primary);
                ret = Finance.DecryptData(sdk, message, encryptChatMsg, msg);
                if (ret != 0) {
   
   
                    log.error("Finance.DecryptData error : {}", ret);
                    throw new IllegalStateException("调用Finance.DecryptData解密出错:" + ret);
                }

                String plaintext = Finance.GetContentFromSlice(msg);
                log.info("decrypt result: {}, msg: {}", ret, plaintext);
                Finance.FreeSlice(msg);

                JSONObject plaintextJson = new JSONObject(plaintext);
                //如果包含msgtype,是有效的消息,
                // 不包含msgtype,可能是一些操作类的通知
                //消息动作,目前有send(发送消息)/recall(撤回消息)/switch(切换企业日志)三种类型。String类型
                if(plaintextJson.has("msgtype")) {
   
   
                    // 拉去媒体文件解密
                    String msgtype = plaintextJson.getString("msgtype");
                    if ("mixed".equals(msgtype)) {
   
   
                        // 混合消息
                        JSONArray array = new JSONArray();
                        JSONObject mixed = new JSONObject(plaintextJson.get("mixed").toString());
                        JSONArray items = mixed.getJSONArray("item");
                        for (int j = 0; j < items.length(); j++) {
   
   
                            JSONObject item = new JSONObject(items.get(j).toString());
                            JSONObject content = new JSONObject(item.getString("content"));
                            String type = item.getString("type");
                            if ("text".equals(type)) {
   
   
                                item.put("content", content.getString("content"));
                            } else {
   
   
                                String url = pullMediaFiles(sdk, type, content);
                                item.put("content", url);
                            }
                            array.put(item);
                        }

                        JSONObject content = new JSONObject();
                        content.put(msgtype, array.toString());
                        plaintextJson.put(msgtype, content.toString());
                    }else {
   
   
                        pullMediaFiles(sdk, msgtype, plaintextJson);
                    }
                }
                // 会话内容写入数据库
                sb.append(plaintextJson.toString()).append(",");
                log.info("第{}条:{}", i, plaintextJson);
                // save(plaintextJson);
            } catch (Exception e) {
   
   
                log.error("第{}条,解密会话内容出错", i, e);
                continue;
            }
        }
        return sb.toString();
    }

    // 拉去媒体信息
    private String pullMediaFiles(long sdk, String msgtype, JSONObject plaintextJson) {
   
   
        String[] msgtypeStr = {
   
   "image", "voice", "video", "emotion", "file"};
        List<String> msgtypeList = Arrays.asList(msgtypeStr);

        if (msgtypeList.contains(msgtype)) {
   
   
            String savefileName = "";
            JSONObject file = new JSONObject();
            if (!plaintextJson.isNull("msgid")) {
   
   
                file = plaintextJson.getJSONObject(msgtype);
                savefileName = plaintextJson.getString("msgid");
            } else {
   
   
                // 混合消息
                file = plaintextJson;
                savefileName = file.getString("md5sum");
            }
            log.info("媒体文件信息:{}",  file);

            /* ============ 文件存储目录及文件名 Start ============ */
            String suffix = "";
            switch (msgtype) {
   
   
                case "image" : suffix = ".jpg"; break;
                case "voice" : suffix = ".amr"; break;
                case "video" : suffix = ".mp4"; break;
                case "emotion" :
                    int type = (int) file.get("type");
                    if (type == 1) suffix = ".gif";
                    else if (type == 2) suffix = ".png";
                    break;
                case "file" :
                    suffix = "." + file.getString("fileext");
                    break;
            }

            savefileName += suffix;
            String savefile = this.mediaSavePath + savefileName;
            File targetFile = new File(savefile);

            if (!targetFile.getParentFile().exists())
                //创建父级文件路径
                targetFile.getParentFile().mkdirs();
            /* ============ 文件存储目录及文件名 End ============ */

            /* ============ 拉去文件 Start ============ */
            int i = 0; boolean isSave = true;
            String indexbuf = "", sdkfileid = file.getString("sdkfileid");
            while (true) {
   
   
                long mediaData = Finance.NewMediaData();
                int ret = Finance.GetMediaData(sdk, indexbuf, sdkfileid, null, null, 3, mediaData);
                if (ret != 0) {
   
   
                    log.error("调用getmediadata ret: {}", ret);
                    Finance.FreeMediaData(mediaData);
                    return null;
                }
                log.info("getmediadata outindex len:{}, data_len:{}, is_finis:{}\n",
                        Finance.GetIndexLen(mediaData), Finance.GetDataLen(mediaData),
                        Finance.IsMediaDataFinish(mediaData));
                try {
   
   
                    // 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。
                    FileOutputStream outputStream = new FileOutputStream(new File(savefile), true);
                    outputStream.write(Finance.GetData(mediaData));
                    outputStream.close();
                } catch (Exception e) {
   
   
                    log.error("输出媒体文件出错", e);
                }

                if (Finance.IsMediaDataFinish(mediaData) == 1) {
   
   
                    // 已经拉取完成最后一个分片
                    Finance.FreeMediaData(mediaData);
                    break;
                } else {
   
   
                    // 获取下次拉取需要使用的indexbuf
                    indexbuf = Finance.GetOutIndexBuf(mediaData);
                    Finance.FreeMediaData(mediaData);
                }
                // 若文件大于50M则不保存
                if (++i > 100) {
   
   
                    isSave = false;
                    break;
                }
            }
            /* ============ 拉去文件 End ============ */
            if (isSave) {
   
   
                file.put("sdkfileid", savefile);
                return savefile;
            }
        }
        return "";
    }

    //注意,这里要有个方法,来释放初始化的SDK,
    //不然会一直占用在微信那边
    @PreDestroy
    public void releaseSDK(){
   
   
        Finance.DestroySdk(sdk);
    }

7、实现一个拉取的接口

@RestController
@RequiredArgsConstructor
public class PullMsgApi {
   
   

    private final WxchatMsgPullService wxchatMsgPullService;

    @GetMapping(value = "api/pull")
    public String pullMsg(){
   
   
        String result = wxchatMsgPullService.readMsgs();
        return result;
    }
}

8、 .so包的加载

官方提供的.so加载,是放在Finance类中,我把他迁到了启动类中,如下:

public class EwxchatDemoApplication {
   
   
   public static void main(String[] args) {
   
   
      SpringApplication.run(EwxchatDemoApplication.class, args);
   } 
   static {
   
    
       /**
         * 开发时就要调整此处的参数
         * 建议将.so包,入到jdk的lib目录下,这样可以直接使用loadLibrary
         * 不然只能用load,这要绝对路径
         */
        //System.loadLibrary("libWeWorkFinanceSdk_Java.so"); 
       //System.load("/Users/xxxx(改成目录)/Documents/dev/libWeWorkFinanceSdk_Java.so");
        System.load("/usr/xxxx/logs/ewxchat-demo/lib/libWeWorkFinanceSdk_Java.so");
   }

说明,我这里用于服务器的绝对路径来加载.so文件,你也可以放到jdk的lib目录下,使用另外一个方法来加载。

8、 结果

用工具请求这个接口:xxxx.xxxx .com/ewxchat/api/pull,得到的结果如下:

[  {
   
       "msgid": "17641547684399051526_1655110008353",    "action": "switch",    "time": 1655110008334,    "user": "zhangshan"  },  {
   
       "tolist": [      "jasontan"    ],
    "msgtime": 1655198980626,
    "msgid": "17836885138636475767_1655198980860",
    "action": "send",
    "from": "lishe",
    "text": {
   
   
      "content": "测试一下"
    },
    "msgtype": "text",
    "roomid": ""
  },
  {
   
   
    "tolist": [
      "lishe"
    ],
    "msgtime": 1655199069793,
    "msgid": "17966145653496018174_1655199070041",
    "action": "send",
    "from": "zhangshan",
    "text": {
   
   
      "content": "收到"
    },
    "msgtype": "text",
    "roomid": ""
  },
  {
   
   
    "tolist": [
      "jasontan"
    ],
    "msgtime": 1655199083555,
    "msgid": "250435355251795619_1655199083855",
    "action": "send",
    "from": "lishe",
    "text": {
   
   
      "content": "不用回"
    },
    "msgtype": "text",
    "roomid": ""
  },
  {
   
   
    "tolist": [
      "zhangshan"
    ],
    "msgtime": 1655199794237,
    "msgid": "15655799441533302234_1655199794364",
    "action": "recall",
    "revoke": {
   
   
      "pre_msgid": "17836885138636475767_1655198980860"
    },
    "from": "lishe",
    "msgtype": "revoke",
    "roomid": ""
  }
]

三、遇到的问题

1、 遇到的第一个问题

makefile

异常java.security.InvalidKeyException:illegal Key Size

这个是JCE权限策略文件的问题,需要去官方网站下载JCE无限制权限策略文件(请到官网下载对应的版本, 例如JDK8的下载地址(根据不同JDK下载):www.oracle.com/technetwork… ):下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt。

2、 环境问题

由于我用的是mac系统来开发,运行过程中,报无法加载.so文件,这个是因为官方提供的.so文件只支持 linux, window系统。

Exception in thread "main" java.lang.UnsatisfiedLinkError: /Users/xxx/Documents/dev/libWeWorkFinanceSdk_Java.so: 
dlopen(/Users/xxx/Documents/dev/libWeWorkFinanceSdk_Java.so, 1): 
no suitable image found.  Did find:
    /Users/xxx/Documents/dev/libWeWorkFinanceSdk_Java.so: unknown file type, first eight bytes: 0x7F 0x45 0x4C 0x46 0x02 0x01 0x01 0x00
    /Users/xxx/Documents/dev/libWeWorkFinanceSdk_Java.so: unknown file type, first eight bytes: 0x7F 0x45 0x4C 0x46 0x02 0x01 0x01 0x00

3、 引用包问题

bouncycastle 包引入版本不对问题,经过调试,按我上面引入的是没有问题的。

四、总结

demo程序能完成会话内容的获取,可以存储起来,方便后续对内容进行分析。
如果在调试,开发过程中遇到问题,可以找我,帮你解决。

相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
目录
相关文章
|
4天前
|
监控 供应链 搜索推荐
阿里妈妈商品详情API接口:开发、应用与收益的深度剖析
阿里妈妈是阿里巴巴旗下的数字营销平台,其商品详情API接口为开发者提供了获取淘宝、天猫等电商平台商品详细信息的工具。本文介绍了该接口的开发流程、应用场景及带来的收益,揭示了其在电商生态中的重要地位。
38 6
|
4天前
|
供应链 搜索推荐 API
1688APP原数据API接口的开发、应用与收益(一篇文章全明白)
1688作为全球知名的B2B电商平台,通过开放的原数据API接口,为开发者提供了丰富的数据资源,涵盖商品信息、交易数据、店铺信息、物流信息和用户信息等。本文将深入探讨1688 APP原数据API接口的开发、应用及其带来的商业收益,包括提升流量、优化库存管理、增强用户体验等方面。
30 6
|
6天前
|
监控 搜索推荐 API
京东商品详情API接口的开发、应用与收益探索
在数字化和互联网高速发展的时代,京东通过开放商品详情API接口,为开发者、企业和商家提供了丰富的数据源和创新空间。本文将探讨该API接口的开发背景、流程、应用场景及带来的多重收益,包括促进生态系统建设、提升数据利用效率和推动数字化转型等。
24 3
|
11天前
|
供应链 搜索推荐 API
探索1688榜单商品详细信息API接口:开发、应用与收益
本文深入探讨了1688榜单商品详细信息API接口的开发与应用,涵盖接口概述、开发条件、调用方法及数据处理等内容。该API帮助企业高效获取1688平台商品信息,应用于商品信息采集、校验、同步与数据分析等领域,有效提升了企业的运营效率、库存管理、销售转化率及市场策略制定能力,降低了采购成本,提升了客户满意度。
37 9
|
14天前
|
算法 Java API
如何使用Java开发获得淘宝商品描述API接口?
本文详细介绍如何使用Java开发调用淘宝商品描述API接口,涵盖从注册淘宝开放平台账号、阅读平台规则、创建应用并申请接口权限,到安装开发工具、配置开发环境、获取访问令牌,以及具体的Java代码实现和注意事项。通过遵循这些步骤,开发者可以高效地获取商品详情、描述及图片等信息,为项目和业务增添价值。
48 10
|
9天前
|
API 开发者
微信native支付对接案例详解
本文详细介绍了微信Native支付的对接流程,包括效果展示、产品介绍、接入前准备、开发指引、API列表、支付通知等,并强调了只有通过微信认证的服务号才能对接微信支付。每年需支付300元认证费用。
25 3
|
14天前
|
存储 API 数据库
使用Python开发获取商品销量详情API接口
本文介绍了使用Python开发获取商品销量详情的API接口方法,涵盖API接口概述、技术选型(Flask与FastAPI)、环境准备、API接口创建及调用淘宝开放平台API等内容。通过示例代码,详细说明了如何构建和调用API,以及开发过程中需要注意的事项,如数据库连接、API权限、错误处理、安全性和性能优化等。
62 5
|
13天前
|
数据可视化 搜索推荐 API
速卖通获得aliexpress商品详情API接口的开发、应用与收益。
速卖通(AliExpress)作为阿里巴巴旗下的跨境电商平台,为全球消费者提供丰富商品。其开放平台提供的API接口支持开发者获取商品详情等信息,本文探讨了速卖通商品详情API的开发流程、应用场景及潜在收益,包括提高运营效率、降低成本、增加收入和提升竞争力等方面。
31 1
|
2月前
|
JSON 小程序 JavaScript
uni-app开发微信小程序的报错[渲染层错误]排查及解决
uni-app开发微信小程序的报错[渲染层错误]排查及解决
695 7
|
2月前
|
小程序 JavaScript 前端开发
uni-app开发微信小程序:四大解决方案,轻松应对主包与vendor.js过大打包难题
uni-app开发微信小程序:四大解决方案,轻松应对主包与vendor.js过大打包难题
749 1