開發文檔地址: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