开发文档地址: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接入和第三方应用满足更多个性需求。有一种说法: 微信企业号要合并到企业微信,然后慢慢淡化微信企业号的概念。
我们可以监听事件,比如实现添加好友自动发送消息,联系人变动事件,扫码事件,订阅取消订阅事件…
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, 请求包);
点击刚刚创建的应用,点击【接收消息】-【设置API接收】,在URL处填写我方的地址,例如:http://xxxxxx/wx/cp/portal/1000004,负责接收微信发送的消息。
token,和encodingAESKey需要在服务端里面进行配置,然后启动服务,在花生壳映射好端口,点击保存,成功了就配置好了。
验证:在应用里面发送消息,看是否会回复。如果回复了,就成功配置完成。
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
到这里基本上就搞定了。
自己在项目里面配置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;
}
}
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;
}
}
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);
}
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());
}
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;
}
}
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