企业微信开发(二):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盲盒。
目录
相关文章
|
3天前
|
API 开发工具 数据库
开发一份API接口,需要注意这些,看你做到了几项
本文介绍了设计API接口时需注意的关键点,包括数字签名、敏感数据加密与脱敏、限流、参数校验、统一返回与异常处理、请求日志记录、幂等设计、数据量限制、异步处理、参数定义、完整文档及开发者对接SDK等内容,旨在帮助开发者设计出安全、稳定、易维护的API接口。
33 6
开发一份API接口,需要注意这些,看你做到了几项
|
10天前
|
JSON 安全 API
如何使用Python开发API接口?
在现代软件开发中,API(应用程序编程接口)用于不同软件组件之间的通信和数据交换,实现系统互操作性。Python因其简单易用和强大功能,成为开发API的热门选择。本文详细介绍了Python开发API的基础知识、优势、实现方式(如Flask和Django框架)、实战示例及注意事项,帮助读者掌握高效、安全的API开发技巧。
37 3
如何使用Python开发API接口?
|
6天前
|
存储 SQL API
探索后端开发:构建高效API与数据库交互
【10月更文挑战第36天】在数字化时代,后端开发是连接用户界面和数据存储的桥梁。本文深入探讨如何设计高效的API以及如何实现API与数据库之间的无缝交互,确保数据的一致性和高性能。我们将从基础概念出发,逐步深入到实战技巧,为读者提供一个清晰的后端开发路线图。
|
5天前
|
JSON 前端开发 API
后端开发中的API设计与文档编写指南####
本文探讨了后端开发中API设计的重要性,并详细阐述了如何编写高效、可维护的API接口。通过实际案例分析,文章强调了清晰的API设计对于前后端分离项目的关键作用,以及良好的文档习惯如何促进团队协作和提升开发效率。 ####
|
3天前
|
JSON API 数据格式
如何使用Python开发1688商品详情API接口?
本文介绍了如何使用Python开发1688商品详情API接口,获取商品的标题、价格、销量和评价等详细信息。主要内容包括注册1688开放平台账号、安装必要Python模块、了解API接口、生成签名、编写Python代码、解析返回数据以及错误处理和日志记录。通过这些步骤,开发者可以轻松地集成1688商品数据到自己的应用中。
16 1
|
4天前
|
JSON API 数据格式
淘宝 / 天猫官方商品 / 订单订单 API 接口丨商品上传接口对接步骤
要对接淘宝/天猫官方商品或订单API,需先注册淘宝开放平台账号,创建应用获取App Key和App Secret。之后,详细阅读API文档,了解接口功能及权限要求,编写认证、构建请求、发送请求和处理响应的代码。最后,在沙箱环境中测试与调试,确保API调用的正确性和稳定性。
|
6天前
|
监控 搜索推荐 安全
探究亚马逊详情API接口:开发与应用
在数字化时代,亚马逊作为全球领先的电商平台,为商家和消费者提供了丰富的商品信息和便捷的购物体验。本文深入探讨了亚马逊详情API接口的获取与运用,帮助开发者和商家实时监控商品数据、分析市场趋势、优化价格策略、分析竞争对手、构建推荐系统及自动化营销工具,从而在竞争中占据优势。文章还提供了Python调用示例和注意事项,确保API使用的安全与高效。
27 3
|
10天前
|
缓存 监控 Java
如何运用JAVA开发API接口?
本文详细介绍了如何使用Java开发API接口,涵盖创建、实现、测试和部署接口的关键步骤。同时,讨论了接口的安全性设计和设计原则,帮助开发者构建高效、安全、易于维护的API接口。
32 4
|
9天前
|
XML JSON API
【PHP开发专栏】PHP RESTful API设计与开发
随着互联网技术的发展,前后端分离成为Web开发的主流模式。本文介绍RESTful API的基本概念、设计原则及在PHP中的实现方法。RESTful API是一种轻量级、无状态的接口设计风格,通过HTTP方法(GET、POST、PUT、DELETE)操作资源,使用JSON或XML格式传输数据。在PHP中,通过定义路由、创建控制器、处理HTTP请求和响应等步骤实现RESTful API,并强调了安全性的重要性。
16 2
|
16天前
|
前端开发 关系型数据库 API
深入浅出后端开发——从零到一构建RESTful API
本文旨在为初学者提供一个关于后端开发的全面指南,特别是如何从零开始构建一个RESTful API。我们将探讨后端开发的基本概念、所需技术栈、以及通过实际案例展示如何设计和实现一个简单的RESTful API。无论你是完全的新手还是有一定编程基础的开发者,这篇文章都将为你提供实用的知识和技巧,帮助你在后端开发的道路上迈出坚实的一步。