企业微信开发(二):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天前
|
算法 Java API
如何使用Java开发获得淘宝商品描述API接口?
本文详细介绍如何使用Java开发调用淘宝商品描述API接口,涵盖从注册淘宝开放平台账号、阅读平台规则、创建应用并申请接口权限,到安装开发工具、配置开发环境、获取访问令牌,以及具体的Java代码实现和注意事项。通过遵循这些步骤,开发者可以高效地获取商品详情、描述及图片等信息,为项目和业务增添价值。
33 10
|
3天前
|
存储 API 数据库
使用Python开发获取商品销量详情API接口
本文介绍了使用Python开发获取商品销量详情的API接口方法,涵盖API接口概述、技术选型(Flask与FastAPI)、环境准备、API接口创建及调用淘宝开放平台API等内容。通过示例代码,详细说明了如何构建和调用API,以及开发过程中需要注意的事项,如数据库连接、API权限、错误处理、安全性和性能优化等。
36 5
|
2天前
|
数据可视化 搜索推荐 API
速卖通获得aliexpress商品详情API接口的开发、应用与收益。
速卖通(AliExpress)作为阿里巴巴旗下的跨境电商平台,为全球消费者提供丰富商品。其开放平台提供的API接口支持开发者获取商品详情等信息,本文探讨了速卖通商品详情API的开发流程、应用场景及潜在收益,包括提高运营效率、降低成本、增加收入和提升竞争力等方面。
13 1
|
17天前
|
搜索推荐 数据挖掘 API
淘宝商品API接口的对接及收益
淘宝商品API接口是淘宝开放平台提供的数据服务,支持商品搜索、详情、评价等功能,帮助开发者快速获取商品数据,提升用户体验,降低运营成本,增强市场竞争力,并支持数据分析和业务决策。对接流程包括注册账号、获取API密钥、阅读文档、编写请求代码、发送请求、解析数据、错误处理和日志记录等步骤。通过实际案例,如价格比较网站、库存管理工具、个性化推荐系统等,展示了API接口的应用价值。
45 5
|
17天前
|
JSON API 数据格式
探索后端开发:从零构建简易RESTful API
在数字时代的浪潮中,后端开发如同搭建一座桥梁,连接着用户界面与数据世界。本文将引导读者步入后端开发的殿堂,通过构建一个简易的RESTful API,揭示其背后的逻辑与魅力。我们将从基础概念出发,逐步深入到实际操作,不仅分享代码示例,更探讨如何思考和解决问题,让每一位读者都能在后端开发的道路上迈出坚实的一步。
|
21天前
|
缓存 前端开发 API
深入浅出:后端开发中的RESTful API设计原则
【10月更文挑战第43天】在数字化浪潮中,后端开发如同搭建梦想的脚手架,而RESTful API则是连接梦想与现实的桥梁。本文将带你领略API设计的哲学之美,探索如何通过简洁明了的设计,提升开发效率与用户体验。从资源定位到接口约束,从状态转换到性能优化,我们将一步步构建高效、易用、可维护的后端服务。无论你是初涉后端的新手,还是寻求进阶的开发者,这篇文章都将为你的开发之路提供指引。让我们一起走进RESTful API的世界,解锁后端开发的新篇章。
|
20天前
|
缓存 API 数据库
Python哪个框架合适开发速卖通商品详情api?
在跨境电商平台速卖通的商品详情数据获取与整合中,Python 语言及其多种框架(如 Flask、Django、Tornado 和 FastAPI)提供了高效解决方案。Flask 简洁灵活,适合快速开发;Django 功能全面,适用于大型项目;Tornado 性能卓越,擅长处理高并发;FastAPI 结合类型提示和异步编程,开发体验优秀。选择合适的框架需综合考虑项目规模、性能要求和团队技术栈。
25 2
|
19天前
|
存储 API 数据安全/隐私保护
Python开发淘宝详情API的深入探索
通过Python开发淘宝详情API,你可以高效地获取商品信息,为电商运营和市场分析提供强有力的数据支持。本文详细介绍了注册开发者账号、获取API密钥、构建请求、解析响应数据等步骤,并探讨了相关的注意事项和最佳实践。希望这些内容能够帮助你更好地理解和使用淘宝开放平台的API接口,实现你的业务需求。
28 1
|
14天前
|
安全 测试技术 API
构建高效RESTful API:后端开发的艺术与实践####
在现代软件开发的浩瀚星空中,RESTful API如同一座桥梁,连接着前端世界的绚丽多彩与后端逻辑的深邃复杂。本文旨在探讨如何精心打造一款既高效又易于维护的RESTful API,通过深入浅出的方式,剖析其设计原则、实现技巧及最佳实践,为后端开发者提供一份实用的指南。我们不深入晦涩的理论,只聚焦于那些能够即刻提升API品质与开发效率的关键点,让你的API在众多服务中脱颖而出。 ####
24 0
|
15天前
|
JSON API 数据格式
如何使用Python开发天猫获得淘宝买家秀API接口?
本文介绍了如何使用Python开发天猫和淘宝买家秀API接口,包括注册开放平台账号、创建应用获取API权限、构建请求URL、发送请求获取响应及解析数据等步骤,帮助开发者高效获取和处理商品信息与用户评价数据。
20 0