企業微信腳手架(企業內部)

在這裏插入圖片描述
在這裏插入圖片描述

開發文檔地址:https://work.weixin.qq.com/api/doc/90000/90003/90556

企業微信後臺:https://work.weixin.qq.com/
更新日誌:
1、2.25:添加Redis緩存(下面有教程)


一、前言

1.企業微信於2016年4月上線,是騰訊微信團隊打造的以辦公溝通工具爲主打定位的移動辦公平臺,它的slogan:讓每個企業都有自己的微信。
2.企業微信提供了通訊錄管理、應用管理、消息推送、身份驗證、移動端SDK、素材、OA數據接口、企業支付、電子發票等API,管理員可以使用這些API,爲企業接入更多個性化的辦公應用。
3.企業微信也是一個平臺,是一個統一的辦公入口,可以集成公司內部的系統(OA系統、HR系統、ERP系統、CRM系統等),直接在企業微信手機端就可以接收內部系統的消息和通知。
4.企業微信與微信企業號區別:其實兩個產品最大的其別就是微信企業號是基於微信,而企業微信是一個獨立app。企業微信,傾向於將工作和生活完全分開,以獨立app的形式去使用,更有着豐富的辦公應用,如預設打卡、審批、日報、公告等OA應用,如果你對這些應用不滿足,還可以通過API接入和第三方應用滿足更多個性需求。有一種說法: 微信企業號要合併到企業微信,然後慢慢淡化微信企業號的概念。

源碼下載:下載
在這裏插入圖片描述


二、腳手架預覽

2.1 項目結構

在這裏插入圖片描述


2.1 自建內部應用

在這裏插入圖片描述
在這裏插入圖片描述

2.2 消息接收

在這裏插入圖片描述


2.3 拆分鏈接,授權

在這裏插入圖片描述
在這裏插入圖片描述


2.4 Js-Jdk測試

在這裏插入圖片描述


2.5 聊天側邊欄

在這裏插入圖片描述
在這裏插入圖片描述


2.6 各種事件監聽

我們可以監聽事件,比如實現添加好友自動發送消息,聯繫人變動事件,掃碼事件,訂閱取消訂閱事件…
在這裏插入圖片描述


三、開發步驟

corpid:每個企業都有唯一的corpid:我的企業–企業信息
userid:每個成員都有唯一的userid(賬號):通訊錄–成員詳情頁
部門id:每個部門都有唯一的id:通訊錄-組織架構-部門右邊的小圓點
tagid:每個標籤都有唯一的標籤id:通訊錄-標籤
agentid:每個應用唯一的id:應用與小程序-應用詳情頁
secret:企業應用中用於保障數據安全的鑰匙【跟agentid配套】
—自建應用secret:應用與小程序–應用–自建–某應用
—基礎應用secret:【如審批,打卡等應用】企業與小程序–應用–基礎–某應用–點開API小按鈕
—通訊錄管理secret:通訊錄同步【需開啓api接口同步】
—外部聯繫人管理secret:外部聯繫人–點開API小按鈕
—access_token:是企業後臺去企業微信的後臺獲取信息時的重要 票據,

由corpid和secret產生。所有接口在通信時都需要攜帶access_token用於驗證接口的訪問權限。
在這裏插入圖片描述

企業微信的開發大體可分爲以下幾步:
(1)封裝實體類
參考官方文檔給出的請求包、回包(即響應包),封裝對應的java實體類。
(2)java對象的序列化
將java對象序列化爲json格式的字符串
(3)獲取AccessToken,拼接請求接口url
憑證的獲取方式有兩種(此處暫時存疑,以待勘誤):
通訊錄AccessToken:CorpId+通訊錄密鑰
其他AccessToken:CorpId+應用密鑰
(4)調用接口發送http請求
封裝好http請求方法:httpRequest(請求url, 請求方法POST/GET, 請求包);


3.1 申請企業微信

在這裏插入圖片描述

3.2 創建應用

在這裏插入圖片描述

3.3 處理消息

點擊剛剛創建的應用,點擊【接收消息】-【設置API接收】,在URL處填寫我方的地址,例如:http://xxxxxx/wx/cp/portal/1000004,負責接收微信發送的消息。
在這裏插入圖片描述
token,和encodingAESKey需要在服務端裏面進行配置,然後啓動服務,在花生殼映射好端口,點擊保存,成功了就配置好了。

驗證:在應用裏面發送消息,看是否會回覆。如果回覆了,就成功配置完成。


四、怎麼快速把項目跑起來

4.1 修改配置 application.xml(支持多個應用)
logging:
  level:
    org.springframework.web: info
    com.lxh.cp: debug
    cn.binarywang.wx.cp: debug

server:
  port: 80
  servlet:
    context-path: /

wx:
  cp:
    corpId: wwd276de90ff8xxxxxx
    configs:
      - agentId: 1000004
        secret: Mygabj9Vz7q0Z0cxliNVrr0numw_HFyExxxxxxxx
        token: Gp38xxxxxxx
        aesKey: rWKTBto89QjWxL423Cej4vaSptcmZlxxxxx
      - agentId: 100
        secret: XXXX
        token: XXXX
        aesKey: XXXX

4.2 啓動服務

在這裏插入圖片描述

到這裏基本上就搞定了。


4.3 添加redis緩存支持

自己在項目裏面配置redis,提供可用的JedisPool,然後注入到配置中。如果redis不可以,走降級方案,內存中緩存。

  @PostConstruct
    public void initServices() {
        // 默認緩存在內存中,配置redis就放入redis中
        cpServices = this.properties.getConfigs().stream().map(a -> {
            if (redisIsOk()){
                val configStorage = new WxCpRedisConfigImpl(jedisPool);
                configStorage.setCorpId(this.properties.getCorpId());
                configStorage.setAgentId(a.getAgentId());
                configStorage.setCorpSecret(a.getSecret());
                configStorage.setToken(a.getToken());
                configStorage.setAesKey(a.getAesKey());
                val service = new WxCpServiceImpl();
                service.setWxCpConfigStorage(configStorage);
                routers.put(a.getAgentId(), this.newRouter(service));
                return service;
            }else {
                val configStorage = new WxCpDefaultConfigImpl();
                configStorage.setAgentId(a.getAgentId());
                configStorage.setCorpId(this.properties.getCorpId());
                configStorage.setToken(a.getToken());
                configStorage.setCorpSecret(a.getSecret());
                configStorage.setAesKey(a.getAesKey());
                val service = new WxCpServiceImpl();
                service.setWxCpConfigStorage(configStorage);
                routers.put(a.getAgentId(), this.newRouter(service));
                return service;
            }
        }).collect(Collectors.toMap(service -> service.getWxCpConfigStorage().getAgentId(), a -> a));
    }


 /**
     * redis是否可用
     * @return
     */
    private boolean redisIsOk(){
        try {
            Jedis jedis = jedisPool.getResource();
            jedis.ping();
            return true;
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }

在這裏插入圖片描述


五、核心代碼

5.1 WxCpConfiguration.java
package com.lxh.cp.config;

import com.google.common.collect.Maps;
import com.lxh.cp.handler.*;
import lombok.val;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl;
import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
import me.chanjar.weixin.cp.constant.WxCpConsts;
import me.chanjar.weixin.cp.message.WxCpMessageRouter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * created by [email protected] on 2020/2/23
 */
@Configuration
@EnableConfigurationProperties(WxCpProperties.class)
public class WxCpConfiguration {
    private static final Logger logger = LoggerFactory.getLogger(WxCpConfiguration.class);

    private static Map<Integer/*agentId*/, WxCpMessageRouter> routers = Maps.newHashMap();
    private static Map<Integer/*agentId*/, WxCpService> cpServices = Maps.newHashMap();

    private WxCpProperties properties;

    private LogHandler logHandler;
    private NullHandler nullHandler;
    private ScanHandler scanHandler;
    private LocationHandler locationHandler;
    private MenuHandler menuHandler;
    private MsgHandler msgHandler;
    private UnsubscribeHandler unsubscribeHandler;
    private SubscribeHandler subscribeHandler;

    @Autowired
    public WxCpConfiguration(LogHandler logHandler, NullHandler nullHandler, LocationHandler locationHandler,
                             MenuHandler menuHandler, MsgHandler msgHandler, UnsubscribeHandler unsubscribeHandler,
                             SubscribeHandler subscribeHandler, WxCpProperties properties) {
        this.logHandler = logHandler;
        this.nullHandler = nullHandler;
        this.locationHandler = locationHandler;
        this.menuHandler = menuHandler;
        this.msgHandler = msgHandler;
        this.unsubscribeHandler = unsubscribeHandler;
        this.subscribeHandler = subscribeHandler;
        this.properties = properties;
    }

    public static Map<Integer, WxCpMessageRouter> getRouters() {
        return routers;
    }

    public static WxCpMessageRouter getRouter(Integer agentId) {
        return routers.get(agentId);
    }

    public static WxCpService getCpService(Integer agentId) {
        return cpServices.get(agentId);
    }


    /**
     * 初始化服務
     */
    @PostConstruct
    public void initServices() {
        cpServices = this.properties.getConfigs().stream().map(a -> {
            val configStorage = new WxCpDefaultConfigImpl();
            configStorage.setCorpId(this.properties.getCorpId());
            configStorage.setAgentId(a.getAgentId());
            configStorage.setCorpSecret(a.getSecret());
            configStorage.setToken(a.getToken());
            configStorage.setAesKey(a.getAesKey());
            val service = new WxCpServiceImpl();
            service.setWxCpConfigStorage(configStorage);
            routers.put(a.getAgentId(), this.newRouter(service));
            return service;
        }).collect(Collectors.toMap(service -> service.getWxCpConfigStorage().getAgentId(), a -> a));
    }


    /**
     * 消息路由處理
     * @param wxCpService
     * @return
     */
    private WxCpMessageRouter newRouter(WxCpService wxCpService) {
        final val newRouter = new WxCpMessageRouter(wxCpService);

        // 記錄所有事件的日誌 (異步執行)
        newRouter.rule().handler(this.logHandler).next();
        // 自定義菜單事件
        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
                .event(WxConsts.MenuButtonType.CLICK).handler(this.menuHandler).end();
        // 點擊菜單鏈接事件(這裏使用了一個空的處理器,可以根據自己需要進行擴展)
        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
                .event(WxConsts.MenuButtonType.VIEW).handler(this.nullHandler).end();
        // 關注事件
        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
                .event(WxConsts.EventType.SUBSCRIBE).handler(this.subscribeHandler)
                .end();
        // 取消關注事件
        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
                .event(WxConsts.EventType.UNSUBSCRIBE)
                .handler(this.unsubscribeHandler).end();
        // 上報地理位置事件
        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
                .event(WxConsts.EventType.LOCATION).handler(this.locationHandler)
                .end();
        // 接收地理位置消息
        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.LOCATION)
                .handler(this.locationHandler).end();
        // 掃碼事件(這裏使用了一個空的處理器,可以根據自己需要進行擴展)
        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
                .event(WxConsts.EventType.SCAN).handler(this.scanHandler).end();
        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
                .event(WxCpConsts.EventType.CHANGE_CONTACT).handler(new ContactChangeHandler()).end();
        newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
                .event(WxCpConsts.EventType.ENTER_AGENT).handler(new EnterAgentHandler()).end();

        // 默認
        newRouter.rule().async(false).handler(this.msgHandler).end();
        return newRouter;
    }
}


5.2 回覆消息類型構造器

在這裏插入圖片描述

package com.lxh.cp.builder;

import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.WxCpXmlMessage;
import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * created by [email protected] on 2020/2/23
 */
public abstract class AbstractBuilder {
    protected final Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 構建消息類型
     * @param content
     * @param wxMessage
     * @param service
     * @return
     */
    public abstract WxCpXmlOutMessage build(String content, WxCpXmlMessage wxMessage, WxCpService service);
}

5.3 各種事件Hadler

在這裏插入圖片描述

package com.lxh.cp.handler;

import me.chanjar.weixin.cp.message.WxCpMessageHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * created by [email protected] on 2020/2/23
 */
public abstract class AbstractHandler implements WxCpMessageHandler {
    protected Logger logger = LoggerFactory.getLogger(getClass());
}


5.4 WxPortalController.java
package com.lxh.cp.controller;

import com.lxh.cp.config.WxCpConfiguration;
import com.lxh.cp.utils.JsonUtils;
import com.lxh.cp.utils.WxCpUtils;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.WxCpXmlMessage;
import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage;
import me.chanjar.weixin.cp.message.WxCpMessageRouter;
import me.chanjar.weixin.cp.util.crypto.WxCpCryptUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;

/**
 * created by [email protected] on 2020/2/23
 * 、企業微信內部應用開發
 */
@RestController
@RequestMapping("/wx/cp/portal/{agentId}")
public class WxPortalController {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    private WxCpService wxCpService;


    @GetMapping(produces = "text/plain;charset=utf-8")
    public String authGet(@PathVariable Integer agentId,
                          @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) {
        this.logger.info("\n接收到來自微信服務器的認證消息:signature = [{}], timestamp = [{}], nonce = [{}], echostr = [{}]",
                signature, timestamp, nonce, echostr);
        if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
            throw new IllegalArgumentException("請求參數非法,請覈實!");
        }
        wxCpService = WxCpUtils.switchover(agentId);
        if (wxCpService.checkSignature(signature, timestamp, nonce, echostr)) {
            return new WxCpCryptUtil(wxCpService.getWxCpConfigStorage()).decrypt(echostr);
        }
        return "非法請求";
    }


    @PostMapping(produces = "application/xml; charset=UTF-8")
    public String post(@PathVariable Integer agentId,
                       @RequestBody String requestBody,
                       @RequestParam("msg_signature") String signature,
                       @RequestParam("timestamp") String timestamp,
                       @RequestParam("nonce") String nonce) {
        this.logger.info("\n接收微信請求:[signature=[{}], timestamp=[{}], nonce=[{}], requestBody=[\n{}\n] ",
                signature, timestamp, nonce, requestBody);

        wxCpService = WxCpUtils.switchover(agentId);
        WxCpXmlMessage inMessage = WxCpXmlMessage.fromEncryptedXml(requestBody, wxCpService.getWxCpConfigStorage(),
                timestamp, nonce, signature);
        this.logger.debug("\n消息解密後內容爲:\n{} ", JsonUtils.toJson(inMessage));
        WxCpXmlOutMessage outMessage = this.route(agentId, inMessage);
        if (outMessage == null) {
            return "";
        }
        String out = outMessage.toEncryptedXml(wxCpService.getWxCpConfigStorage());
        this.logger.debug("\n組裝回覆信息:{}", out);
        return out;
    }


    private WxCpXmlOutMessage route(Integer agentId, WxCpXmlMessage message) {
        try {
            WxCpMessageRouter router = WxCpUtils.getRouter(agentId);
            if (router == null){
                throw new Exception("路由爲空!");
            }
            return router.route(message);
        } catch (Exception e) {
            this.logger.error(e.getMessage(), e);
        }
        return null;
    }
}


5.5 WxCpOauthController.java
package com.lxh.cp.controller;

import com.lxh.cp.utils.JsonUtils;
import com.lxh.cp.utils.WxCpUtils;
import me.chanjar.weixin.common.bean.WxJsapiSignature;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.WxCpOauth2UserInfo;
import me.chanjar.weixin.cp.bean.WxCpUser;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * created by [email protected] on 2020/2/23
 * 授權相關
 */
@RestController
@RequestMapping("/wx/oauth/{agentId}")
public class WxCpOauthController {
    private WxCpService wxCpService;
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    private String oauthUrl = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=%s#wechat_redirect";
    private String loginUrl = "http://chenxingxing.51vip.biz/wx/oauth/%s/login";


    /**
     * 拆分鏈接
     * @param agentId
     * @param url
     */
    @GetMapping("/jump")
    public void jump(@PathVariable Integer agentId,
                     String url,
                     HttpServletResponse response) throws IOException {
        if (StringUtils.isBlank(url)) {
            throw new IllegalArgumentException("url is empty");
        }
        wxCpService = WxCpUtils.switchover(agentId);
        loginUrl = String.format(loginUrl, agentId);
        oauthUrl = String.format(oauthUrl, "wwd276de90ff82e1e3", loginUrl, url);
        logger.info("跳轉url:" + oauthUrl);
        response.sendRedirect(oauthUrl);
    }


    /**
     * 授權鏈接  通過code換取用戶信息
     * https://open.weixin.qq.com/connect/oauth2/authorize?appid=CORPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect
     * 前:https://open.weixin.qq.com/connect/oauth2/authorize?appid=wwd276de90ff82e1e3&redirect_uri=https%3a%2f%2fchenxingxing.51vip.biz%2f&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect
     * 後:https://chenxingxing.51vip.biz/?code=eTrMxa-_afrQ8JsK-rnmSJdBZhCvqalYQ4ZSDZHjHP8&state=STATE
     *
     * code獲取用戶信息:響應結果
     * {
     *     "UserId":"ChenXingXing",
     *     "DeviceId":"d93b8209-bf41-4d15-9bd2-136138799a03",
     *     "errcode":0,
     *     "errmsg":"ok"
     * }
     */
    @GetMapping("/login")
    public void login(@PathVariable Integer agentId,
                      String code,
                      String state,
                      HttpServletRequest request,
                      HttpServletResponse response) {
        if (StringUtils.isBlank(code)) {
            throw new IllegalArgumentException("code is empty");
        }
        wxCpService = WxCpUtils.switchover(agentId);
        try {
            WxCpOauth2UserInfo userInfo = wxCpService.getOauth2Service().getUserInfo(code);
            this.logger.info("userInfo:"+ JsonUtils.toJson(userInfo));
            String userId = userInfo.getUserId();
            WxCpUser user = wxCpService.getUserService().getById(userId);
            logger.info("完整的user:" + JsonUtils.toJson(user));
            // TODO: 2020/2/24 處理用戶信息 
            request.getSession().setAttribute("token", user);
            response.sendRedirect(state);
        } catch (Exception e) {
            this.logger.error(e.getMessage(), e);
        }
    }


    /**
     * 創建js-sdk簽名
     * @param url
     * @return
     * @throws Exception
     */
    @RequestMapping("/create/jsapi_sign")
    @ResponseBody
    public Object jssdk(@PathVariable Integer agentId,
                        @RequestParam  String url) throws Exception{
        // 切換企業微信
        wxCpService = WxCpUtils.switchover(agentId);
        logger.info("url:" + url);
        WxJsapiSignature jsapiSignature = wxCpService.createJsapiSignature(url);
        logger.info("data:" + JsonUtils.toJson(jsapiSignature));
        return jsapiSignature;
    }
}


參考資料

授權鏈接:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wwd276de90ff82e1e3&redirect_uri=http://chenxingxing.51vip.biz/wx/oauth/1000004/login&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect

拆分鏈接:
hhttp://chenxingxing.51vip.biz/wx/oauth/1000004/jump?url=http://chenxingxing.51vip.biz

jsdk文檔
https://work.weixin.qq.com/api/doc/90001/90144/90547
如果checkjsApi是ok的,但是在調用接口還是說沒權限,需要檢驗域名歸屬地(一個坑)


企業微信 API文檔:https://work.weixin.qq.com/api/doc
開發時請留意企業微信與企業號的接口差異:https://work.weixin.qq.com/api/doc#12060
(1) Quick Start
https://github.com/Wechat-Group/weixin-java-tools/wiki/CP_Quick-Start
(2) 微信消息路由器
https://github.com/Wechat-Group/weixin-java-tools/wiki/CP_微信消息路由器
(3)WxCpConfigStorage
https://github.com/Wechat-Group/weixin-java-tools/wiki/CP_WxCpConfigStorage
(4)同步回覆消息
https://github.com/Wechat-Group/weixin-java-tools/wiki/CP_同步回覆消息
(5)刷新access_token
https://github.com/Wechat-Group/weixin-java-tools/wiki/CP_刷新access_token
(6)用戶身份二次驗證
https://github.com/Wechat-Group/weixin-java-tools/wiki/CP_用戶身份二次驗證
(7)主動發送消息
https://github.com/Wechat-Group/weixin-java-tools/wiki/CP_主動發送消息
(8)臨時素材(多媒體文件)管理
https://github.com/Wechat-Group/weixin-java-tools/wiki/CP_多媒體文件管理 (9)
用戶管理 https://github.com/Wechat-Group/weixin-java-tools/wiki/CP_用戶管理
(10)部門管理
https://github.com/Wechat-Group/weixin-java-tools/wiki/CP_部門管理
(11)標籤管理
https://github.com/Wechat-Group/weixin-java-tools/wiki/CP_標籤管理
(12)自定義菜單管理
https://github.com/Wechat-Group/weixin-java-tools/wiki/CP_自定義菜單管理
(13)OAuth2網頁授權
https://github.com/Wechat-Group/weixin-java-tools/wiki/CP_OAuth2網頁授權
(14)http代理支持
https://github.com/Wechat-Group/weixin-java-tools/wiki/CP_http代理支持
(15)如何調用未支持的接口
https://github.com/Wechat-Group/weixin-java-tools/wiki/CP_如何調用未支持的接口
(16)如何執行本項目單元測試
https://github.com/Wechat-Group/weixin-java-tools/wiki/CP_如何執行本項目單元測試

可以運行 demo-1 的代碼來對weixin-java-tools的有一個更好的瞭解。 項目demo-1:
https://github.com/Wechat-Group/weixin-java-tools 啓動方式:
https://github.com/Wechat-Group/weixin-java-tools/wiki/CP_demo代碼


企業微信開發異常整理:http://www.cnblogs.com/shirui/category/1053578.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章