企业微信脚手架(企业内部)

企业微信脚手架(企业内部)_第1张图片
企业微信脚手架(企业内部)_第2张图片

开发文档地址: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接入和第三方应用满足更多个性需求。有一种说法: 微信企业号要合并到企业微信,然后慢慢淡化微信企业号的概念。

源码下载:下载
企业微信脚手架(企业内部)_第3张图片


二、脚手架预览

2.1 项目结构

企业微信脚手架(企业内部)_第4张图片


2.1 自建内部应用

企业微信脚手架(企业内部)_第5张图片
企业微信脚手架(企业内部)_第6张图片

2.2 消息接收

企业微信脚手架(企业内部)_第7张图片


2.3 拆分链接,授权

企业微信脚手架(企业内部)_第8张图片
企业微信脚手架(企业内部)_第9张图片


2.4 Js-Jdk测试

企业微信脚手架(企业内部)_第10张图片


2.5 聊天侧边栏

企业微信脚手架(企业内部)_第11张图片
企业微信脚手架(企业内部)_第12张图片


2.6 各种事件监听

我们可以监听事件,比如实现添加好友自动发送消息,联系人变动事件,扫码事件,订阅取消订阅事件…
企业微信脚手架(企业内部)_第13张图片


三、开发步骤

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用于验证接口的访问权限。
企业微信脚手架(企业内部)_第14张图片

企业微信的开发大体可分为以下几步:
(1)封装实体类
参考官方文档给出的请求包、回包(即响应包),封装对应的java实体类。
(2)java对象的序列化
将java对象序列化为json格式的字符串
(3)获取AccessToken,拼接请求接口url
凭证的获取方式有两种(此处暂时存疑,以待勘误):
通讯录AccessToken:CorpId+通讯录密钥
其他AccessToken:CorpId+应用密钥
(4)调用接口发送http请求
封装好http请求方法:httpRequest(请求url, 请求方法POST/GET, 请求包);


3.1 申请企业微信

企业微信脚手架(企业内部)_第15张图片

3.2 创建应用

企业微信脚手架(企业内部)_第16张图片

3.3 处理消息

点击刚刚创建的应用,点击【接收消息】-【设置API接收】,在URL处填写我方的地址,例如:http://xxxxxx/wx/cp/portal/1000004,负责接收微信发送的消息。
企业微信脚手架(企业内部)_第17张图片
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 启动服务

企业微信脚手架(企业内部)_第18张图片

到这里基本上就搞定了。


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;
        }
    }

企业微信脚手架(企业内部)_第19张图片


五、核心代码

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 回复消息类型构造器

企业微信脚手架(企业内部)_第20张图片

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

企业微信脚手架(企业内部)_第21张图片

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

你可能感兴趣的:(微信支付宝)