From a2074a4d391144ded325529c20567b560a663501 Mon Sep 17 00:00:00 2001 From: xworks Date: Thu, 12 Nov 2020 17:30:27 +0800 Subject: [PATCH] =?UTF-8?q?:art:=20#1867=E3=80=90=E4=BC=81=E4=B8=9A?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E3=80=91=E4=BC=98=E5=8C=96=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E7=AC=AC=E4=B8=89=E6=96=B9=E5=BA=94=E7=94=A8=E7=9A=84=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/util/xml/StringArrayConverter.java | 30 ++ .../cp/bean/message/WxCpTpXmlMessage.java | 357 ++++++++++++++++++ .../weixin/cp/config/WxCpTpConfigStorage.java | 93 ++--- .../config/impl/WxCpTpDefaultConfigImpl.java | 179 ++++++--- .../config/impl/WxCpTpRedissonConfigImpl.java | 278 ++++++++++++++ .../weixin/cp/constant/WxCpApiPathConsts.java | 2 + .../weixin/cp/constant/WxCpTpConsts.java | 52 +++ .../cp/tp/message/WxCpTpMessageHandler.java | 1 - .../tp/message/WxCpTpMessageInterceptor.java | 1 - .../cp/tp/message/WxCpTpMessageMatcher.java | 3 +- .../cp/tp/message/WxCpTpMessageRouter.java | 55 +-- .../tp/message/WxCpTpMessageRouterRule.java | 60 ++- .../weixin/cp/tp/service/WxCpTpService.java | 33 ++ .../service/impl/BaseWxCpTpServiceImpl.java | 80 +++- .../cp/api/WxCpTpMessageRouterTest.java | 57 +++ .../cp/bean/message/WxCpTpXmlMessageTest.java | 234 ++++++++++++ 16 files changed, 1385 insertions(+), 130 deletions(-) create mode 100644 weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/StringArrayConverter.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpTpRedissonConfigImpl.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpTpConsts.java create mode 100644 weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpTpMessageRouterTest.java create mode 100644 weixin-java-cp/src/test/java/me/chanjar/weixin/cp/bean/message/WxCpTpXmlMessageTest.java diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/StringArrayConverter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/StringArrayConverter.java new file mode 100644 index 000000000..44d2926f4 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/StringArrayConverter.java @@ -0,0 +1,30 @@ +package me.chanjar.weixin.common.util.xml; + +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.collect.Iterables; +import com.thoughtworks.xstream.converters.basic.StringConverter; + + +/** + * String 数组转换 + * @author chily.lin + */ +public class StringArrayConverter extends StringConverter { + @Override + public boolean canConvert(Class type) { + return type == String[].class; + } + + @Override + public String toString(Object obj) { + return ""; + } + + @Override + public Object fromString(String str) { + final Iterable iterable = Splitter.on(",").split(str); + String[] results = Iterables.toArray(iterable, String.class); + return results; + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/message/WxCpTpXmlMessage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/message/WxCpTpXmlMessage.java index 6ee139538..c8273e9a9 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/message/WxCpTpXmlMessage.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/message/WxCpTpXmlMessage.java @@ -1,13 +1,17 @@ package me.chanjar.weixin.cp.bean.message; import java.io.Serializable; +import java.util.List; import java.util.Map; import com.thoughtworks.xstream.annotations.XStreamAlias; import com.thoughtworks.xstream.annotations.XStreamConverter; +import com.thoughtworks.xstream.converters.basic.IntConverter; import lombok.Data; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.util.XmlUtils; +import me.chanjar.weixin.common.util.xml.IntegerArrayConverter; +import me.chanjar.weixin.common.util.xml.StringArrayConverter; import me.chanjar.weixin.common.util.xml.XStreamCDataConverter; import me.chanjar.weixin.cp.util.xml.XStreamTransformer; @@ -52,6 +56,359 @@ public class WxCpTpXmlMessage implements Serializable { @XStreamConverter(value = XStreamCDataConverter.class) protected String authCorpId; + @XStreamAlias("ChangeType") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String changeType; + + @XStreamAlias("UserID") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String userID; + + @XStreamAlias("Department") + @XStreamConverter(value = IntegerArrayConverter.class) + protected Integer[] department; + + @XStreamAlias("MainDepartment") + @XStreamConverter(value = IntConverter.class) + protected Integer mainDepartment; + + @XStreamAlias("IsLeaderInDept") + @XStreamConverter(value = IntegerArrayConverter.class) + protected Integer[] isLeaderInDept; + + @XStreamAlias("Mobile") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String mobile; + + @XStreamAlias("Position") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String position; + + @XStreamAlias("Gender") + @XStreamConverter(value = IntConverter.class) + protected Integer gender; + + @XStreamAlias("Email") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String email; + + @XStreamAlias("Status") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String status; + + @XStreamAlias("Avatar") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String avatar; + + @XStreamAlias("Alias") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String alias; + + @XStreamAlias("Telephone") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String telephone; + + @XStreamAlias("Id") + @XStreamConverter(value = IntConverter.class) + protected Integer id; + + @XStreamAlias("Name") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String name; + + @XStreamAlias("ParentId") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String parentId; + + @XStreamAlias("Order") + @XStreamConverter(value = XStreamCDataConverter.class) + protected Integer order; + + @XStreamAlias("TagId") + @XStreamConverter(value = IntConverter.class) + protected Integer tagId; + + @XStreamAlias("AddUserItems") + @XStreamConverter(value = StringArrayConverter.class) + protected String[] addUserItems; + + @XStreamAlias("DelUserItems") + @XStreamConverter(value = StringArrayConverter.class) + protected String[] delUserItems; + + @XStreamAlias("AddPartyItems") + @XStreamConverter(value = IntegerArrayConverter.class) + protected Integer[] addPartyItems; + + @XStreamAlias("DelPartyItems") + @XStreamConverter(value = IntegerArrayConverter.class) + protected Integer[] delPartyItems; + + //ref: https://work.weixin.qq.com/api/doc/90001/90143/90585 + @XStreamAlias("ServiceCorpId") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String serviceCorpId; + + @XStreamAlias("RegisterCode") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String registerCode; + + @XStreamAlias("ContactSync") + protected ContactSync contactSync; + + @XStreamAlias("AuthUserInfo") + protected AuthUserInfo authUserInfo; + + @XStreamAlias("TemplateId") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String templateId; + + @XStreamAlias("CreateTime") + protected Long createTime; + + @XStreamAlias("ToUserName") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String toUserName; + + @XStreamAlias("FromUserName") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String fromUserName; + + @XStreamAlias("MsgType") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String msgType; + + @XStreamAlias("Event") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String event; + + @XStreamAlias("BatchJob") + protected BatchJob batchJob; + + @XStreamAlias("ExternalUserID") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String externalUserID; + + @XStreamAlias("WelcomeCode") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String welcomeCode; + + @XStreamAlias("FromUser") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String fromUser; + + @XStreamAlias("Content") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String content; + + @XStreamAlias("MsgId") + protected String msgId; + + @XStreamAlias("AgentID") + protected Integer agentID; + + @XStreamAlias("PicUrl") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String picUrl; + + @XStreamAlias("MediaId") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String mediaId; + + @XStreamAlias("Format") + @XStreamConverter(value = XStreamCDataConverter.class) + private String format; + + @XStreamAlias("ThumbMediaId") + @XStreamConverter(value = XStreamCDataConverter.class) + private String thumbMediaId; + + @XStreamAlias("Location_X") + private Double locationX; + + @XStreamAlias("Location_Y") + private Double locationY; + + @XStreamAlias("Scale") + private Double scale; + + @XStreamAlias("Label") + @XStreamConverter(value = XStreamCDataConverter.class) + private String label; + + @XStreamAlias("Title") + @XStreamConverter(value = XStreamCDataConverter.class) + private String title; + + @XStreamAlias("Description") + @XStreamConverter(value = XStreamCDataConverter.class) + private String description; + + @XStreamAlias("Url") + @XStreamConverter(value = XStreamCDataConverter.class) + private String url; + + @XStreamAlias("EventKey") + @XStreamConverter(value = XStreamCDataConverter.class) + private String eventKey; + + @XStreamAlias("Latitude") + private Double latitude; + + @XStreamAlias("Longitude") + private Double longitude; + + @XStreamAlias("Precision") + private Double precision; + + @XStreamAlias("AppType") + @XStreamConverter(value = XStreamCDataConverter.class) + private String appType; + + @XStreamAlias("ScanCodeInfo") + private WxCpXmlMessage.ScanCodeInfo scanCodeInfo = new WxCpXmlMessage.ScanCodeInfo(); + + @XStreamAlias("SendPicsInfo") + private WxCpXmlMessage.SendPicsInfo sendPicsInfo = new WxCpXmlMessage.SendPicsInfo(); + + @XStreamAlias("SendLocationInfo") + private WxCpXmlMessage.SendLocationInfo sendLocationInfo = new WxCpXmlMessage.SendLocationInfo(); + + @XStreamAlias("ApprovalInfo") + private ApprovalInfo approvalInfo = new ApprovalInfo(); + + @XStreamAlias("TaskId") + @XStreamConverter(value = XStreamCDataConverter.class) + private String taskId; + + @Data + @XStreamAlias("ContactSync") + public static class ContactSync { + @XStreamAlias("AccessToken") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String accessToken; + + @XStreamAlias("ExpiresIn") + protected Integer expiresIn; + } + + @Data + @XStreamAlias("AuthUserInfo") + public static class AuthUserInfo { + @XStreamAlias("UserId") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String userId; + } + + @Data + @XStreamAlias("BatchJob") + public static class BatchJob { + @XStreamAlias("JobId") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String JobId; + + @XStreamAlias("JobType") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String jobType; + + @XStreamAlias("ErrCode") + @XStreamConverter(value = IntConverter.class) + protected Integer errCode; + + @XStreamAlias("ErrMsg") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String errMsg; + } + + @Data + @XStreamAlias("ApprovalInfo") + public static class ApprovalInfo { + @XStreamAlias("ThirdNo") + protected Long thirdNo; + + @XStreamAlias("OpenSpName") + protected String openSpName; + + @XStreamAlias("OpenTemplateId") + protected Integer openTemplateId; + + @XStreamAlias("OpenSpStatus") + protected Integer openSpStatus; + + @XStreamAlias("ApplyTime") + protected Long applyTime; + + @XStreamAlias("ApplyUserName") + protected String applyUserName; + + @XStreamAlias("ApplyUserId") + protected Integer applyUserId; + + @XStreamAlias("ApplyUserParty") + protected String applyUserParty; + + @XStreamAlias("ApplyUserImage") + protected String applyUserImage; + + @XStreamAlias("ApprovalNodes") + protected List approvalNodes; + + @XStreamAlias("NotifyNodes") + protected List notifyNodes; + + @XStreamAlias("approverstep") + protected Integer approverstep; + + //自建/第三方应用调用审批流程引擎,状态通知 + //ref: https://work.weixin.qq.com/api/doc/90001/90143/90376#审批状态通知事件 + //1.自建/第三方应用调用审批流程引擎发起申请之后,审批状态发生变化时 + //2.自建/第三方应用调用审批流程引擎发起申请之后,在“审批中”状态,有任意审批人进行审批操作时 + @Data + @XStreamAlias("ApprovalNode") + public static class ApprovalNode { + @XStreamAlias("NodeStatus") + protected Integer nodeStatus; + + @XStreamAlias("NodeAttr") + protected Integer nodeAttr; + + @XStreamAlias("NodeType") + protected Integer nodeType; + + @XStreamAlias("Items") + protected List items; + + @Data + @XStreamAlias("Item") + public static class Item { + @XStreamAlias("ItemName") + protected String itemName; + @XStreamAlias("ItemUserId") + protected Integer itemUserId; + @XStreamAlias("ItemImage") + protected String itemImage; + @XStreamAlias("ItemStatus") + protected Integer itemStatus; + @XStreamAlias("ItemSpeech") + protected String itemSpeech; + @XStreamAlias("ItemOpTime") + protected Long itemOpTime; + } + } + + @Data + @XStreamAlias("NotifyNode") + public static class NotifyNode { + @XStreamAlias("ItemName") + protected String itemName; + @XStreamAlias("ItemUserId") + protected Integer itemUserId; + @XStreamAlias("ItemImage") + protected String itemImage; + } + } + + public static WxCpTpXmlMessage fromXml(String xml) { //修改微信变态的消息内容格式,方便解析 //xml = xml.replace("", ""); diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpTpConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpTpConfigStorage.java index 40c29ed0c..0fda37663 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpTpConfigStorage.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpTpConfigStorage.java @@ -26,67 +26,76 @@ public interface WxCpTpConfigStorage { */ String getApiUrl(String path); + /** + * 第三方应用的suite access token相关 + */ String getSuiteAccessToken(); - boolean isSuiteAccessTokenExpired(); - - /** - * 强制将suite access token过期掉. - */ + //强制将suite access token过期掉. void expireSuiteAccessToken(); - void updateSuiteAccessToken(WxAccessToken suiteAccessToken); + void updateSuiteAccessToken(String suiteAccessToken, int expiresInSeconds); - void updateSuiteAccessToken(String suiteAccessToken, int expiresIn); - + /** + * 第三方应用的suite ticket相关 + */ String getSuiteTicket(); - boolean isSuiteTicketExpired(); - - /** - * 强制将suite ticket过期掉. - */ + //强制将suite ticket过期掉. void expireSuiteTicket(); - - /** - * 应该是线程安全的. - */ + //应该是线程安全的 void updateSuiteTicket(String suiteTicket, int expiresInSeconds); - String getCorpId(); - - String getCorpSecret(); - + /** + * 第三方应用的其他配置,来自于企微配置 + */ String getSuiteId(); - String getSuiteSecret(); - + // 第三方应用的token,用来检查应用的签名 String getToken(); - + //第三方应用的EncodingAESKey,用来检查签名 String getAesKey(); - long getExpiresTime(); - - String getHttpProxyHost(); - - int getHttpProxyPort(); - - String getHttpProxyUsername(); - - String getHttpProxyPassword(); - - File getTmpDirFile(); + /** + * 企微服务商企业ID & 企业secret + */ + String getCorpId(); + String getCorpSecret(); /** - * http client builder. - * - * @return ApacheHttpClientBuilder + * 授权企业的access token相关 */ + String getAccessToken(String authCorpId); + boolean isAccessTokenExpired(String authCorpId); + void updateAccessToken(String authCorpId, String accessToken, int expiredInSeconds); + + /** + * 授权企业的js api ticket相关 + */ + String getAuthCorpJsApiTicket(String authCorpId); + boolean isAuthCorpJsApiTicketExpired(String authCorpId); + void updateAuthCorpJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds); + + /** + * 授权企业的第三方应用js api ticket相关 + */ + String getAuthSuiteJsApiTicket(String authCorpId); + boolean isAuthSuiteJsApiTicketExpired(String authCorpId); + void updateAuthSuiteJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds);; + + /** + * 网络代理相关 + */ + String getHttpProxyHost(); + int getHttpProxyPort(); + String getHttpProxyUsername(); + String getHttpProxyPassword(); ApacheHttpClientBuilder getApacheHttpClientBuilder(); - /** - * 是否自动刷新token - * @return . - */ boolean autoRefreshToken(); + + // 毫无相关性的代码 + @Deprecated + File getTmpDirFile(); + } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpTpDefaultConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpTpDefaultConfigImpl.java index be4b046a4..a748e301d 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpTpDefaultConfigImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpTpDefaultConfigImpl.java @@ -7,6 +7,8 @@ import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; import java.io.File; import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; /** * 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化. @@ -24,25 +26,34 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl private volatile String token; private volatile String suiteAccessToken; + private volatile long suiteAccessTokenExpiresTime; private volatile String aesKey; - private volatile long expiresTime; + private volatile String suiteTicket; + private volatile long suiteTicketExpiresTime; private volatile String oauth2redirectUri; + private volatile Map authCorpAccessTokenMap = new HashMap<>(); + private volatile Map authCorpAccessTokenExpireTimeMap = new HashMap<>(); + + private volatile Map authCorpJsApiTicketMap = new HashMap<>(); + private volatile Map authCorpJsApiTicketExpireTimeMap = new HashMap<>(); + + private volatile Map authSuiteJsApiTicketMap = new HashMap<>(); + private volatile Map authSuiteJsApiTicketExpireTimeMap = new HashMap<>(); + private volatile String httpProxyHost; private volatile int httpProxyPort; private volatile String httpProxyUsername; private volatile String httpProxyPassword; - private volatile String suiteTicket; - private volatile long suiteTicketExpiresTime; - private volatile File tmpDirFile; private volatile ApacheHttpClientBuilder apacheHttpClientBuilder; private volatile String baseApiUrl; + @Override public void setBaseApiUrl(String baseUrl) { this.baseApiUrl = baseUrl; @@ -67,12 +78,12 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl @Override public boolean isSuiteAccessTokenExpired() { - return System.currentTimeMillis() > this.expiresTime; + return System.currentTimeMillis() > this.suiteAccessTokenExpiresTime; } @Override public void expireSuiteAccessToken() { - this.expiresTime = 0; + this.suiteAccessTokenExpiresTime = 0; } @Override @@ -83,25 +94,12 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl @Override public synchronized void updateSuiteAccessToken(String suiteAccessToken, int expiresInSeconds) { this.suiteAccessToken = suiteAccessToken; - this.expiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L; + this.suiteAccessTokenExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L; } - @Override - public String getCorpId() { - return this.corpId; - } - - public void setCorpId(String corpId) { - this.corpId = corpId; - } - - @Override - public String getCorpSecret() { - return this.corpSecret; - } - - public void setCorpSecret(String corpSecret) { - this.corpSecret = corpSecret; + @Deprecated + public void setSuiteAccessTokenExpiresTime(long suiteAccessTokenExpiresTime) { + this.suiteAccessTokenExpiresTime = suiteAccessTokenExpiresTime; } @Override @@ -109,23 +107,16 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl return this.suiteTicket; } - public void setSuiteTicket(String suiteTicket) { - this.suiteTicket = suiteTicket; - } - - public long getSuiteTicketExpiresTime() { - return this.suiteTicketExpiresTime; - } - - public void setSuiteTicketExpiresTime(long suiteTicketExpiresTime) { - this.suiteTicketExpiresTime = suiteTicketExpiresTime; - } - @Override public boolean isSuiteTicketExpired() { return System.currentTimeMillis() > this.suiteTicketExpiresTime; } + @Override + public void expireSuiteTicket() { + this.suiteTicketExpiresTime = 0; + } + @Override public synchronized void updateSuiteTicket(String suiteTicket, int expiresInSeconds) { this.suiteTicket = suiteTicket; @@ -133,9 +124,20 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl this.suiteTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L; } - @Override - public void expireSuiteTicket() { - this.suiteTicketExpiresTime = 0; + + @Deprecated + public void setSuiteTicket(String suiteTicket) { + this.suiteTicket = suiteTicket; + } + + @Deprecated + public long getSuiteTicketExpiresTime() { + return this.suiteTicketExpiresTime; + } + + @Deprecated + public void setSuiteTicketExpiresTime(long suiteTicketExpiresTime) { + this.suiteTicketExpiresTime = suiteTicketExpiresTime; } @Override @@ -143,6 +145,7 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl return this.suiteId; } + @Deprecated public void setSuiteId(String corpId) { this.suiteId = corpId; } @@ -152,6 +155,7 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl return this.suiteSecret; } + @Deprecated public void setSuiteSecret(String corpSecret) { this.suiteSecret = corpSecret; } @@ -161,28 +165,109 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl return this.token; } + @Deprecated public void setToken(String token) { this.token = token; } - @Override - public long getExpiresTime() { - return this.expiresTime; - } - - public void setExpiresTime(long expiresTime) { - this.expiresTime = expiresTime; - } - @Override public String getAesKey() { return this.aesKey; } + @Deprecated public void setAesKey(String aesKey) { this.aesKey = aesKey; } + + @Override + public String getCorpId() { + return this.corpId; + } + + @Deprecated + public void setCorpId(String corpId) { + this.corpId = corpId; + } + + @Override + public String getCorpSecret() { + return this.corpSecret; + } + + @Deprecated + public void setCorpSecret(String corpSecret) { + this.corpSecret = corpSecret; + } + + + @Override + public String getAccessToken(String authCorpId) { + return authCorpAccessTokenMap.get(authCorpId); + } + + @Override + public boolean isAccessTokenExpired(String authCorpId) { + return System.currentTimeMillis() > authCorpAccessTokenExpireTimeMap.get(authCorpId); + } + + @Override + public void updateAccessToken(String authCorpId, String accessToken, int expiredInSeconds) { + authCorpAccessTokenMap.put(authCorpId, accessToken); + // 预留200秒的时间 + authCorpAccessTokenExpireTimeMap.put(authCorpId, System.currentTimeMillis() + (expiredInSeconds - 200) * 1000L); + } + + + @Override + public String getAuthCorpJsApiTicket(String authCorpId) { + return this.authCorpJsApiTicketMap.get(authCorpId); + } + + @Override + public boolean isAuthCorpJsApiTicketExpired(String authCorpId) { + Long t = this.authCorpJsApiTicketExpireTimeMap.get(authCorpId); + if (t == null) { + return System.currentTimeMillis() > t; + } + else { + return true; + } + } + + @Override + public void updateAuthCorpJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds) { + // 应该根据不同的授权企业做区分 + authCorpJsApiTicketMap.put(authCorpId, jsApiTicket); + // 预留200秒的时间 + authCorpJsApiTicketExpireTimeMap.put(authCorpId, System.currentTimeMillis() + (expiredInSeconds - 200) * 1000L); + } + + @Override + public String getAuthSuiteJsApiTicket(String authCorpId) { + return authSuiteJsApiTicketMap.get(authCorpId); + } + + @Override + public boolean isAuthSuiteJsApiTicketExpired(String authCorpId) { + Long t = authSuiteJsApiTicketExpireTimeMap.get(authCorpId); + if (t == null) { + return System.currentTimeMillis() > t; + } + else { + return true; + } + } + + @Override + public void updateAuthSuiteJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds) { + // 应该根据不同的授权企业做区分 + authSuiteJsApiTicketMap.put(authCorpId, jsApiTicket); + // 预留200秒的时间 + authSuiteJsApiTicketExpireTimeMap.put(authCorpId, System.currentTimeMillis() + (expiredInSeconds - 200) * 1000L); + } + public void setOauth2redirectUri(String oauth2redirectUri) { this.oauth2redirectUri = oauth2redirectUri; } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpTpRedissonConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpTpRedissonConfigImpl.java new file mode 100644 index 000000000..3b1414d9b --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpTpRedissonConfigImpl.java @@ -0,0 +1,278 @@ +package me.chanjar.weixin.cp.config.impl; + + +import lombok.Builder; +import lombok.NonNull; +import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.redis.WxRedisOps; +import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; +import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.io.File; +import java.io.Serializable; +import java.util.concurrent.TimeUnit; + +/** + * 企业微信各种固定、授权配置的Redisson存储实现 + */ +@Builder +public class WxCpTpRedissonConfigImpl implements WxCpTpConfigStorage, Serializable { + + @NonNull + private final WxRedisOps wxRedisOps; + + //redis里面key的统一前缀 + private final String keyPrefix = ""; + + private final String suiteAccessTokenKey = ":suiteAccessTokenKey:"; + + private final String suiteTicketKey = ":suiteTicketKey:"; + + private final String accessTokenKey = ":accessTokenKey:"; + + private final String authCorpJsApiTicketKey = ":authCorpJsApiTicketKey:"; + + private final String authSuiteJsApiTicketKey = ":authSuiteJsApiTicketKey:"; + + private volatile String baseApiUrl; + private volatile String httpProxyHost; + private volatile int httpProxyPort; + private volatile String httpProxyUsername; + private volatile String httpProxyPassword; + private volatile ApacheHttpClientBuilder apacheHttpClientBuilder; + private volatile File tmpDirFile; + + /** + * 第三方应用的其他配置,来自于企微配置 + */ + private volatile String suiteId; + private volatile String suiteSecret; + // 第三方应用的token,用来检查应用的签名 + private volatile String token; + //第三方应用的EncodingAESKey,用来检查签名 + private volatile String aesKey; + + /** + * 企微服务商企业ID & 企业secret,来自于企微配置 + */ + private volatile String corpId; + private volatile String corpSecret; + + @Override + public void setBaseApiUrl(String baseUrl) { + this.baseApiUrl = baseUrl; + } + + @Override + public String getApiUrl(String path) { + if (baseApiUrl == null) { + baseApiUrl = "https://qyapi.weixin.qq.com"; + } + return baseApiUrl + path; } + + + /** + * 第三方应用的suite access token相关 + */ + @Override + public String getSuiteAccessToken() { + return wxRedisOps.getValue(keyWithPrefix(suiteAccessTokenKey)); + } + + @Override + public boolean isSuiteAccessTokenExpired() { + //remain time to live in seconds, or key not exist + return wxRedisOps.getExpire(keyWithPrefix(suiteAccessTokenKey)) == 0L || wxRedisOps.getExpire(keyWithPrefix(suiteAccessTokenKey)) == -2; + } + + @Override + public void expireSuiteAccessToken() { + wxRedisOps.expire(keyWithPrefix(suiteAccessTokenKey), 0, TimeUnit.SECONDS); + } + + @Override + public void updateSuiteAccessToken(WxAccessToken suiteAccessToken) { + updateSuiteAccessToken(suiteAccessToken.getAccessToken(), suiteAccessToken.getExpiresIn()); + } + + @Override + public void updateSuiteAccessToken(String suiteAccessToken, int expiresInSeconds) { + wxRedisOps.setValue(keyWithPrefix(suiteAccessTokenKey), suiteAccessToken, expiresInSeconds, TimeUnit.SECONDS); + } + + /** + * 第三方应用的suite ticket相关 + */ + @Override + public String getSuiteTicket() { + return wxRedisOps.getValue(keyWithPrefix(suiteTicketKey)); + } + + @Override + public boolean isSuiteTicketExpired() { + //remain time to live in seconds, or key not exist + return wxRedisOps.getExpire(keyWithPrefix(suiteTicketKey)) == 0L || wxRedisOps.getExpire(keyWithPrefix(suiteTicketKey)) == -2; + } + + @Override + public void expireSuiteTicket() { + wxRedisOps.expire(keyWithPrefix(suiteTicketKey), 0, TimeUnit.SECONDS); + } + + @Override + public void updateSuiteTicket(String suiteTicket, int expiresInSeconds) { + wxRedisOps.setValue(keyWithPrefix(suiteTicketKey), suiteTicket, expiresInSeconds, TimeUnit.SECONDS); + } + + /** + * 第三方应用的其他配置,来自于企微配置 + */ + @Override + public String getSuiteId() { + return suiteId; + } + + @Override + public String getSuiteSecret() { + return suiteSecret; + } + + // 第三方应用的token,用来检查应用的签名 + @Override + public String getToken() { + return token; + } + + //第三方应用的EncodingAESKey,用来检查签名 + @Override + public String getAesKey() { + return aesKey; + } + + + /** + * 企微服务商企业ID & 企业secret, 来自于企微配置 + */ + @Override + public String getCorpId() { + return corpId; + } + + @Override + public String getCorpSecret() { + return corpSecret; + } + + + /** + * 授权企业的access token相关 + */ + @Override + public String getAccessToken(String authCorpId) { + return wxRedisOps.getValue(keyWithPrefix(authCorpId) + accessTokenKey); + } + + @Override + public boolean isAccessTokenExpired(String authCorpId) { + //没有设置或者TTL为0,都是过期 + return wxRedisOps.getExpire(keyWithPrefix(authCorpId) + accessTokenKey) == 0L + || wxRedisOps.getExpire(keyWithPrefix(authCorpId) + accessTokenKey) == -2; + } + + @Override + public void updateAccessToken(String authCorpId, String accessToken, int expiredInSeconds) { + wxRedisOps.setValue(keyWithPrefix(authCorpId) + accessTokenKey, accessToken, expiredInSeconds, TimeUnit.SECONDS); + } + + + /** + * 授权企业的js api ticket相关 + */ + @Override + public String getAuthCorpJsApiTicket(String authCorpId) { + return wxRedisOps.getValue(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey); + } + + @Override + public boolean isAuthCorpJsApiTicketExpired(String authCorpId) { + //没有设置或TTL为0,都是过期 + return wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey) == 0L + || wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey) == -2; + } + + @Override + public void updateAuthCorpJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds) { + wxRedisOps.setValue(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey, jsApiTicket, expiredInSeconds, TimeUnit.SECONDS); + } + + + /** + * 授权企业的第三方应用js api ticket相关 + */ + @Override + public String getAuthSuiteJsApiTicket(String authCorpId) { + return wxRedisOps.getValue(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey); + } + + @Override + public boolean isAuthSuiteJsApiTicketExpired(String authCorpId) { + //没有设置或者TTL为0,都是过期 + return wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey) == 0L + || wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey) == -2; + } + + @Override + public void updateAuthSuiteJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds) { + wxRedisOps.setValue(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey, jsApiTicket, expiredInSeconds, TimeUnit.SECONDS); + } + + + /** + * 网络代理相关 + */ + @Override + public String getHttpProxyHost() { + return this.httpProxyHost; + } + + @Override + public int getHttpProxyPort() { + return this.httpProxyPort; + } + + @Override + public String getHttpProxyUsername() { + return this.httpProxyUsername; + } + + @Override + public String getHttpProxyPassword() { + return this.httpProxyPassword; + } + + @Override + public File getTmpDirFile() { + return tmpDirFile; + } + + @Override + public ApacheHttpClientBuilder getApacheHttpClientBuilder() { + return this.apacheHttpClientBuilder; + } + + @Override + public boolean autoRefreshToken() { + return false; + } + + @Override + public String toString() { + //TODO: + return WxCpGsonBuilder.create().toJson(this); + } + + private String keyWithPrefix(String key) { + return keyPrefix + key; + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java index 97e30f034..00d88940f 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java @@ -140,6 +140,8 @@ public final class WxCpApiPathConsts { public static final String GET_PROVIDER_TOKEN = "/cgi-bin/service/get_provider_token"; public static final String GET_PREAUTH_CODE = "/cgi-bin/service/get_pre_auth_code"; public static final String GET_AUTH_INFO = "/cgi-bin/service/get_auth_info"; + public static final String GET_AUTH_CORP_JSAPI_TICKET = "/cgi-bin/get_jsapi_ticket"; + public static final String GET_SUITE_JSAPI_TICKET = "/cgi-bin/ticket/get"; } @UtilityClass diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpTpConsts.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpTpConsts.java new file mode 100644 index 000000000..40270270c --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpTpConsts.java @@ -0,0 +1,52 @@ +package me.chanjar.weixin.cp.constant; + +import lombok.experimental.UtilityClass; + +public class WxCpTpConsts { + + + @UtilityClass + public static class InfoType { + /** + * 推送更新suite_ticket + */ + public static final String SUITE_TICKET = "suite_ticket"; + + /** + * 从企业微信应用市场发起授权时,授权成功通知 + */ + public static final String CREATE_AUTH = "create_auth"; + + /** + * 从企业微信应用市场发起授权时,变更授权通知 + */ + public static final String CHANGE_AUTH = "change_auth"; + + /** + * 从企业微信应用市场发起授权时,取消授权通知 + */ + public static final String CANCEL_AUTH = "cancel_auth"; + + /** + * 通讯录变更通知 + */ + public static final String CHANGE_CONTACT = "change_contact"; + + /** + * 用户进行企业微信的注册,注册完成回调通知 + */ + public static final String REGISTER_CORP = "register_corp"; + + /** + * 异步任务回调通知 + */ + public static final String BATCH_JOB_RESULT = "batch_job_result"; + + /** + * 外部联系人变更通知 + */ + public static final String CHANGE_EXTERNAL_CONTACT = "change_external_contact"; + + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageHandler.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageHandler.java index eea6bd966..639a74335 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageHandler.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageHandler.java @@ -3,7 +3,6 @@ package me.chanjar.weixin.cp.tp.message; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.cp.bean.message.WxCpTpXmlMessage; -import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage; import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage; import me.chanjar.weixin.cp.tp.service.WxCpTpService; diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageInterceptor.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageInterceptor.java index 62975951b..feac10dbb 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageInterceptor.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageInterceptor.java @@ -3,7 +3,6 @@ package me.chanjar.weixin.cp.tp.message; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.cp.bean.message.WxCpTpXmlMessage; -import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage; import me.chanjar.weixin.cp.tp.service.WxCpTpService; import java.util.Map; diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageMatcher.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageMatcher.java index 8f7decf4b..57e35f194 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageMatcher.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageMatcher.java @@ -1,5 +1,6 @@ package me.chanjar.weixin.cp.tp.message; +import me.chanjar.weixin.cp.bean.message.WxCpTpXmlMessage; import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage; /** @@ -15,6 +16,6 @@ public interface WxCpTpMessageMatcher { * @param message the message * @return the boolean */ - boolean match(WxCpXmlMessage message); + boolean match(WxCpTpXmlMessage message); } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageRouter.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageRouter.java index ad9d0ff0a..5b045082a 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageRouter.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageRouter.java @@ -10,9 +10,7 @@ import me.chanjar.weixin.common.session.InternalSessionManager; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.common.util.LogExceptionHandler; import me.chanjar.weixin.cp.bean.message.WxCpTpXmlMessage; -import me.chanjar.weixin.cp.bean.message.WxCpXmlMessage; import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage; -import me.chanjar.weixin.cp.message.WxCpMessageRouterRule; import me.chanjar.weixin.cp.tp.service.WxCpTpService; import org.apache.commons.lang3.StringUtils; @@ -25,20 +23,22 @@ import java.util.concurrent.*; /** *
  * 微信消息路由器,通过代码化的配置,把来自微信的消息交给handler处理
+ * 和WxCpMessageRouter的rule相比,多了infoType和changeType维度的匹配
  *
  * 说明:
  * 1. 配置路由规则时要按照从细到粗的原则,否则可能消息可能会被提前处理
- * 2. 默认情况下消息只会被处理一次,除非使用 {@link WxCpMessageRouterRule#next()}
- * 3. 规则的结束必须用{@link WxCpMessageRouterRule#end()}或者{@link WxCpMessageRouterRule#next()},否则不会生效
+ * 2. 默认情况下消息只会被处理一次,除非使用 {@link WxCpTpMessageRouterRule#next()}
+ * 3. 规则的结束必须用{@link WxCpTpMessageRouterRule#end()}或者{@link WxCpTpMessageRouterRule#next()},否则不会生效
  *
  * 使用方法:
- * WxCpMessageRouter router = new WxCpMessageRouter();
+ * WxCpTpMessageRouter router = new WxCpTpMessageRouter();
  * router
  *   .rule()
  *       .msgType("MSG_TYPE").event("EVENT").eventKey("EVENT_KEY").content("CONTENT")
  *       .interceptor(interceptor, ...).handler(handler, ...)
  *   .end()
  *   .rule()
+ *       .infoType("INFO_TYPE").changeType("CHANGE_TYPE")
  *       // 另外一个匹配规则
  *   .end()
  * ;
@@ -55,7 +55,7 @@ public class WxCpTpMessageRouter {
   private static final int DEFAULT_THREAD_POOL_SIZE = 100;
   private final List rules = new ArrayList<>();
 
-  private final WxCpTpService wxCpService;
+  private final WxCpTpService wxCpTpService;
 
   private ExecutorService executorService;
 
@@ -68,13 +68,13 @@ public class WxCpTpMessageRouter {
   /**
    * 构造方法.
    */
-  public WxCpTpMessageRouter(WxCpTpService wxCpService) {
-    this.wxCpService = wxCpService;
+  public WxCpTpMessageRouter(WxCpTpService wxCpTpService) {
+    this.wxCpTpService = wxCpTpService;
     ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("WxCpTpMessageRouter-pool-%d").build();
     this.executorService = new ThreadPoolExecutor(DEFAULT_THREAD_POOL_SIZE, DEFAULT_THREAD_POOL_SIZE,
       0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), namedThreadFactory);
     this.messageDuplicateChecker = new WxMessageInMemoryDuplicateChecker();
-    this.sessionManager = wxCpService.getSessionManager();
+    this.sessionManager = wxCpTpService.getSessionManager();
     this.exceptionHandler = new LogExceptionHandler();
   }
 
@@ -160,11 +160,11 @@ public class WxCpTpMessageRouter {
       if (rule.isAsync()) {
         futures.add(
           this.executorService.submit(() -> {
-            rule.service(wxMessage, context, WxCpTpMessageRouter.this.wxCpService, WxCpTpMessageRouter.this.sessionManager, WxCpTpMessageRouter.this.exceptionHandler);
+            rule.service(wxMessage, context, WxCpTpMessageRouter.this.wxCpTpService, WxCpTpMessageRouter.this.sessionManager, WxCpTpMessageRouter.this.exceptionHandler);
           })
         );
       } else {
-        res = rule.service(wxMessage, context, this.wxCpService, this.sessionManager, this.exceptionHandler);
+        res = rule.service(wxMessage, context, this.wxCpTpService, this.sessionManager, this.exceptionHandler);
         // 在同步操作结束,session访问结束
         log.debug("End session access: async=false, sessionId={}", wxMessage.getSuiteId());
         sessionEndAccess(wxMessage);
@@ -200,17 +200,30 @@ public class WxCpTpMessageRouter {
 
   private boolean isMsgDuplicated(WxCpTpXmlMessage wxMessage) {
     StringBuilder messageId = new StringBuilder();
-    if (StringUtils.isNotEmpty(wxMessage.getSuiteId())) {
-      messageId.append("-").append(wxMessage.getSuiteId());
-    }
+      if (wxMessage.getInfoType() != null) {
+        messageId.append(wxMessage.getInfoType())
+          .append("-").append(StringUtils.trimToEmpty(wxMessage.getSuiteId()))
+          .append("-").append(wxMessage.getTimeStamp())
+          .append("-").append(StringUtils.trimToEmpty(wxMessage.getAuthCorpId()))
+          .append("-").append(StringUtils.trimToEmpty(wxMessage.getUserID()))
+          .append("-").append(StringUtils.trimToEmpty(wxMessage.getChangeType()))
+          .append("-").append(StringUtils.trimToEmpty(wxMessage.getServiceCorpId()));
+      }
 
-    if (StringUtils.isNotEmpty(wxMessage.getInfoType())) {
-      messageId.append("-").append(wxMessage.getInfoType());
-    }
-
-    if (StringUtils.isNotEmpty(wxMessage.getTimeStamp())) {
-      messageId.append("-").append(wxMessage.getTimeStamp());
-    }
+      if (wxMessage.getMsgType() != null) {
+        if (wxMessage.getMsgId() != null) {
+          messageId.append(wxMessage.getMsgId())
+            .append("-").append(wxMessage.getCreateTime())
+            .append("-").append(wxMessage.getFromUserName());
+        }
+        else {
+          messageId.append(wxMessage.getMsgType())
+            .append("-").append(wxMessage.getCreateTime())
+            .append("-").append(wxMessage.getFromUserName())
+            .append("-").append(StringUtils.trimToEmpty(wxMessage.getEvent()))
+            .append("-").append(StringUtils.trimToEmpty(wxMessage.getEventKey()));
+        }
+      }
 
     return this.messageDuplicateChecker.isDuplicate(messageId.toString());
   }
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageRouterRule.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageRouterRule.java
index 257cc1212..1b7d7fbf7 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageRouterRule.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/message/WxCpTpMessageRouterRule.java
@@ -6,10 +6,11 @@ import me.chanjar.weixin.common.error.WxErrorException;
 import me.chanjar.weixin.common.session.WxSessionManager;
 import me.chanjar.weixin.cp.bean.message.WxCpTpXmlMessage;
 import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage;
-import me.chanjar.weixin.cp.message.WxCpMessageMatcher;
 import me.chanjar.weixin.cp.tp.service.WxCpTpService;
+import org.apache.commons.lang3.StringUtils;
 
 import java.util.*;
+import java.util.regex.Pattern;
 
 /**
  * The type Wx cp message router rule.
@@ -22,15 +23,34 @@ public class WxCpTpMessageRouterRule {
 
   private boolean async = true;
 
-  private WxCpMessageMatcher matcher;
+  private String fromUser;
+
+  private String msgType;
+
+  private String event;
+
+  private String eventKey;
+
+  private String eventKeyRegex;
+
+  private String content;
+
+  private String rContent;
+
+  private WxCpTpMessageMatcher matcher;
 
   private boolean reEnter = false;
 
+  private Integer agentId;
+
+  private String infoType;
+
+  private String changeType;
+
   private List handlers = new ArrayList<>();
 
   private List interceptors = new ArrayList<>();
   private String suiteId;
-  private String infoType;
   private String authCode;
   private String suiteTicket;
 
@@ -64,13 +84,24 @@ public class WxCpTpMessageRouterRule {
     return this;
   }
 
+  /**
+   * 如果changeType等于这个type,符合rule的条件之一
+   * @param changeType
+   * @return
+   */
+  public WxCpTpMessageRouterRule changeType(String changeType) {
+    this.changeType = changeType;
+    return this;
+  }
+
+
   /**
    * 如果消息匹配某个matcher,用在用户需要自定义更复杂的匹配规则的时候
    *
    * @param matcher the matcher
    * @return the wx cp message router rule
    */
-  public WxCpTpMessageRouterRule matcher(WxCpMessageMatcher matcher) {
+  public WxCpTpMessageRouterRule matcher(WxCpTpMessageMatcher matcher) {
     this.matcher = matcher;
     return this;
   }
@@ -154,13 +185,30 @@ public class WxCpTpMessageRouterRule {
   protected boolean test(WxCpTpXmlMessage wxMessage) {
     return
       (this.suiteId == null || this.suiteId.equals(wxMessage.getSuiteId()))
+        &&
+        (this.fromUser == null || this.fromUser.equals(wxMessage.getFromUserName()))
+        &&
+        (this.agentId == null || this.agentId.equals(wxMessage.getAgentID()))
+        &&
+        (this.msgType == null || this.msgType.equalsIgnoreCase(wxMessage.getMsgType()))
         &&
         (this.infoType == null || this.infoType.equals(wxMessage.getInfoType()))
         &&
         (this.suiteTicket == null || this.suiteTicket.equalsIgnoreCase(wxMessage.getSuiteTicket()))
         &&
-        (this.authCode == null || this.authCode.equalsIgnoreCase(wxMessage.getAuthCode()))
-      ;
+        (this.eventKeyRegex == null || Pattern.matches(this.eventKeyRegex, StringUtils.trimToEmpty(wxMessage.getEventKey())))
+        &&
+        (this.content == null || this.content.equals(StringUtils.trimToNull(wxMessage.getContent())))
+        &&
+        (this.rContent == null || Pattern.matches(this.rContent, StringUtils.trimToEmpty(wxMessage.getContent())))
+        &&
+        (this.infoType == null || this.infoType.equals(wxMessage.getInfoType()))
+        &&
+        (this.changeType == null || this.changeType.equals(wxMessage.getChangeType()))
+        &&
+        (this.matcher == null || this.matcher.match(wxMessage))
+        &&
+        (this.authCode == null || this.authCode.equalsIgnoreCase(wxMessage.getAuthCode()));
   }
 
   /**
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java
index 26da1ddd4..8e8cbd6fa 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java
@@ -73,12 +73,35 @@ public interface WxCpTpService {
    * 详情请见:https://work.weixin.qq.com/api/doc#90001/90143/90628
    * 
* + * @Deprecated 由于无法主动刷新,所以这个接口实际已经没有意义,需要在接收企业微信的主动推送后,保存这个ticket + * @see #setSuiteTicket(String) + * * @param forceRefresh 强制刷新 * @return the suite ticket * @throws WxErrorException the wx error exception */ + @Deprecated String getSuiteTicket(boolean forceRefresh) throws WxErrorException; + /** + *
+   * 保存企业微信定时推送的suite_ticket,(每10分钟)
+   * 详情请见:https://work.weixin.qq.com/api/doc#90001/90143/90628
+   * 
+ * + * @param suiteTicket + * @throws WxErrorException + */ + void setSuiteTicket(String suiteTicket) throws WxErrorException; + + /** + * 获取应用的 jsapi ticket + * + * @param authCorpId 授权企业的cropId + * @return jsapi ticket + */ + String getSuiteJsApiTicket(String authCorpId) throws WxErrorException; + /** * 小程序登录凭证校验 * @@ -144,6 +167,14 @@ public interface WxCpTpService { */ WxCpTpAuthInfo getAuthInfo(String authCorpId, String permanentCode) throws WxErrorException; + /** + * 获取授权企业的 jsapi ticket + * + * @param authCorpId 授权企业的cropId + * @return jsapi ticket + */ + String getAuthCorpJsApiTicket(String authCorpId) throws WxErrorException; + /** * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的GET请求. * @@ -209,8 +240,10 @@ public interface WxCpTpService { /** * 获取WxMpConfigStorage 对象. * + * @Deprecated storage应该在service内部使用,提供这个接口,容易破坏这个封装 * @return WxMpConfigStorage wx cp tp config storage */ + @Deprecated WxCpTpConfigStorage getWxCpTpConfigStorage(); /** diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java index a0c0b2649..6c2399067 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java @@ -45,10 +45,16 @@ public abstract class BaseWxCpTpServiceImpl implements WxCpTpService, Requ */ protected final Object globalSuiteAccessTokenRefreshLock = new Object(); + + /** + * 全局刷新suite ticket的锁 + */ + protected final Object globalSuiteTicketRefreshLock = new Object(); + /** * 全局的是否正在刷新jsapi_ticket的锁. */ - protected final Object globalSuiteTicketRefreshLock = new Object(); + protected final Object globalJsApiTicketRefreshLock = new Object(); protected WxCpTpConfigStorage configStorage; @@ -79,16 +85,6 @@ public abstract class BaseWxCpTpServiceImpl implements WxCpTpService, Requ @Override public String getSuiteTicket() throws WxErrorException { - return getSuiteTicket(false); - } - - @Override - public String getSuiteTicket(boolean forceRefresh) throws WxErrorException { -// suite ticket由微信服务器推送,不能强制刷新 -// if (forceRefresh) { -// this.configStorage.expireSuiteTicket(); -// } - if (this.configStorage.isSuiteTicketExpired()) { // 本地suite ticket 不存在或者过期 WxError wxError = WxError.fromJson("{\"errcode\":40085, \"errmsg\":\"invaild suite ticket\"}", WxType.CP); @@ -97,6 +93,68 @@ public abstract class BaseWxCpTpServiceImpl implements WxCpTpService, Requ return this.configStorage.getSuiteTicket(); } + @Override + public String getSuiteTicket(boolean forceRefresh) throws WxErrorException { +// suite ticket由微信服务器推送,不能强制刷新 +// if (forceRefresh) { +// this.configStorage.expireSuiteTicket(); +// } + return getSuiteTicket(); + } + + @Override + public void setSuiteTicket(String suiteTicket) throws WxErrorException { + synchronized (globalSuiteTicketRefreshLock) { + this.configStorage.updateSuiteTicket(suiteTicket, 10 * 60); + } + } + + @Override + public String getSuiteJsApiTicket(String authCorpId) throws WxErrorException { + if (this.configStorage.isSuiteAccessTokenExpired()) { + + String resp = get(configStorage.getApiUrl(GET_SUITE_JSAPI_TICKET), + "type=agent_config&access_token=" + this.configStorage.getAccessToken(authCorpId)); + + JsonObject jsonObject = GsonParser.parse(resp); + if (jsonObject.get("errcode").getAsInt() == 0) { + String jsApiTicket = jsonObject.get("ticket").getAsString(); + int expiredInSeconds = jsonObject.get("expires_in").getAsInt(); + synchronized (globalJsApiTicketRefreshLock) { + configStorage.updateAuthSuiteJsApiTicket(authCorpId, jsApiTicket, expiredInSeconds); + } + } + else { + throw new WxErrorException(WxError.fromJson(resp)); + } + } + + return configStorage.getSuiteAccessToken(); + } + + @Override + public String getAuthCorpJsApiTicket(String authCorpId) throws WxErrorException { + if (this.configStorage.isSuiteAccessTokenExpired()) { + + String resp = get(configStorage.getApiUrl(GET_AUTH_CORP_JSAPI_TICKET), + "access_token=" + this.configStorage.getAccessToken(authCorpId)); + + JsonObject jsonObject = GsonParser.parse(resp); + if (jsonObject.get("errcode").getAsInt() == 0) { + String jsApiTicket = jsonObject.get("ticket").getAsString(); + int expiredInSeconds = jsonObject.get("expires_in").getAsInt(); + + synchronized (globalJsApiTicketRefreshLock) { + configStorage.updateAuthCorpJsApiTicket(authCorpId, jsApiTicket, expiredInSeconds); + } + } + else { + throw new WxErrorException(WxError.fromJson(resp)); + } + } + + return configStorage.getSuiteAccessToken(); + } @Override public WxCpMaJsCode2SessionResult jsCode2Session(String jsCode) throws WxErrorException { diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpTpMessageRouterTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpTpMessageRouterTest.java new file mode 100644 index 000000000..f6ab29df7 --- /dev/null +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpTpMessageRouterTest.java @@ -0,0 +1,57 @@ +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.cp.bean.message.WxCpTpXmlMessage; +import me.chanjar.weixin.cp.bean.message.WxCpXmlOutMessage; +import me.chanjar.weixin.cp.tp.message.WxCpTpMessageHandler; +import me.chanjar.weixin.cp.tp.message.WxCpTpMessageRouter; +import me.chanjar.weixin.cp.tp.service.WxCpTpService; +import me.chanjar.weixin.cp.tp.service.impl.WxCpTpServiceImpl; +import org.testng.annotations.Test; + +import java.util.Map; + +import static org.testng.Assert.assertNotNull; +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertNull; + +public class WxCpTpMessageRouterTest { + + + @Test + public void testMessageRouter() { + WxCpTpService service = new WxCpTpServiceImpl(); + WxCpTpMessageRouter router = new WxCpTpMessageRouter(service); + + String xml = "\n" + + " \n" + + " \n" + + " \n" + + " 1403610513\n" + + " \n" + + " 1\n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + WxCpTpXmlMessage wxXmlMessage = WxCpTpXmlMessage.fromXml(xml); + + router.rule().infoType("change_contact").changeType("update_tag").handler(new WxCpTpMessageHandler() { + @Override + public WxCpXmlOutMessage handle(WxCpTpXmlMessage wxMessage, Map context, WxCpTpService wxCpService, WxSessionManager sessionManager) throws WxErrorException { + System.out.println("handler enter"); + assertNotNull(wxCpService); + return null; + } + }).end(); + + assertNull(router.route(wxXmlMessage)); + + + System.out.println("over"); + } + +} diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/bean/message/WxCpTpXmlMessageTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/bean/message/WxCpTpXmlMessageTest.java new file mode 100644 index 000000000..1e4a1450a --- /dev/null +++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/bean/message/WxCpTpXmlMessageTest.java @@ -0,0 +1,234 @@ +package me.chanjar.weixin.cp.bean.message; + +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +public class WxCpTpXmlMessageTest { + + @Test + public void testUserNotifyXML() { + String xml = "\n" + + " \n" + + " \n" + + " \n" + + " 1403610513\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 1\n" + + " \n" + + " \n" + + " \n" + + " 1\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 0\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 1\n" + + " \n" + + " <![CDATA[企业微信]]>\n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + WxCpTpXmlMessage wxXmlMessage = WxCpTpXmlMessage.fromXml(xml); + assertEquals(wxXmlMessage.getSuiteId(), "ww4asffe99e54c0f4c"); + assertEquals(wxXmlMessage.getPosition(), "产品经理"); + assertEquals(wxXmlMessage.getGender(), Integer.valueOf(1)); + assertEquals(wxXmlMessage.getTelephone(), "020-111111"); + } + + + @Test + public void testRegisterCorp() { + String xml = "\n" + + " \n" + + " \n" + + " 1502682173\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 1800\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + WxCpTpXmlMessage wxXmlMessage = WxCpTpXmlMessage.fromXml(xml); + assertEquals(wxXmlMessage.getServiceCorpId(), "wwddddccc7775555aab"); + assertEquals(wxXmlMessage.getInfoType(), "register_corp"); + assertEquals(wxXmlMessage.getRegisterCode(), "pIKi3wRPNWCGF-pyP-YU5KWjDDD"); + assertNotNull(wxXmlMessage.getContactSync()); + assertEquals(wxXmlMessage.getContactSync().getAccessToken(), "accesstoken000001"); + assertEquals(wxXmlMessage.getContactSync().getExpiresIn(), Integer.valueOf(1800)); + assertNotNull(wxXmlMessage.getAuthUserInfo()); + assertEquals(wxXmlMessage.getAuthUserInfo().getUserId(), "zhangshan"); + assertEquals(wxXmlMessage.getTemplateId(), "tpl1test"); + } + + @Test + public void tagNotifyTest() { + String xml = "\n" + + " \n" + + " \n" + + " \n" + + " 1403610513\n" + + " \n" + + " 1\n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + WxCpTpXmlMessage wxXmlMessage = WxCpTpXmlMessage.fromXml(xml); + + assertEquals(wxXmlMessage.getTagId(), Integer.valueOf(1)); + assertNotNull(wxXmlMessage.getAddUserItems()); + assertEquals(wxXmlMessage.getAddUserItems()[0], "zhangsan"); + assertEquals(wxXmlMessage.getAddUserItems()[1], "lisi"); + + assertNotNull(wxXmlMessage.getDelUserItems()); + assertNotNull(wxXmlMessage.getDelUserItems()[0], "zhangsan1"); + assertNotNull(wxXmlMessage.getDelUserItems()[0], "lisi1"); + + assertNotNull(wxXmlMessage.getAddPartyItems()); + assertEquals(wxXmlMessage.getAddPartyItems()[0], Integer.valueOf(1)); + assertEquals(wxXmlMessage.getAddPartyItems()[1], Integer.valueOf(2)); + + } + + + @Test + public void enterAppTest() { + String xml = "\n" + + "\n" + + "1408091189\n" + + "\n" + + "\n" + + "\n" + + "1\n" + + ""; + + WxCpTpXmlMessage wxXmlMessage = WxCpTpXmlMessage.fromXml(xml); + assertEquals(wxXmlMessage.getToUserName(), "toUser"); + assertEquals(wxXmlMessage.getFromUserName(), "FromUser"); + assertEquals(wxXmlMessage.getCreateTime(), Long.valueOf(1408091189)); + assertEquals(wxXmlMessage.getEvent(), "enter_agent"); + assertEquals(wxXmlMessage.getEventKey(), ""); + assertEquals(wxXmlMessage.getAgentID(), Integer.valueOf(1)); + } + + @Test + public void textMessageTest() { + String xml = "\n" + + " \n" + + " \n" + + " 1348831860\n" + + " \n" + + " \n" + + " 1234567890123456\n" + + " 1\n" + + ""; + + WxCpTpXmlMessage wxXmlMessage = WxCpTpXmlMessage.fromXml(xml); + assertEquals(wxXmlMessage.getToUserName(), "toUser"); + assertEquals(wxXmlMessage.getFromUserName(), "fromUser"); + assertEquals(wxXmlMessage.getCreateTime(), Long.valueOf(1348831860)); + assertEquals(wxXmlMessage.getMsgType(), "text"); + assertEquals(wxXmlMessage.getMsgId(), "1234567890123456"); + } + + @Test + public void ApprovalInfoTest() { + String xml = "\n" + + " wwddddccc7775555aaa \n" + + " sys \n" + + " 1527838022 \n" + + " event \n" + + " open_approval_change\n" + + " 1\n" + + " \n" + + " 201806010001 \n" + + " 付款 \n" + + " 1234567890 \n" + + " 1 \n" + + " 1527837645 \n" + + " xiaoming \n" + + " 1 \n" + + " 产品部 \n" + + " http://www.qq.com/xxx.png \n" + + " \n" + + " \n" + + " 1 \n" + + " 1 \n" + + " 1 \n" + + " \n" + + " \n" + + " xiaohong \n" + + " 2 \n" + + " http://www.qq.com/xxx.png \n" + + " 1 \n" + + " \n" + + " 0 \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " xiaogang \n" + + " 3 \n" + + " http://www.qq.com/xxx.png \n" + + " \n" + + " \n" + + " 0 \n" + + " \n" + + ""; + + WxCpTpXmlMessage wxXmlMessage = WxCpTpXmlMessage.fromXml(xml); + assertEquals(wxXmlMessage.getToUserName(), "wwddddccc7775555aaa"); + assertEquals(wxXmlMessage.getFromUserName(), "sys"); + assertEquals(wxXmlMessage.getCreateTime(), Long.valueOf(1527838022)); + assertEquals(wxXmlMessage.getEvent(), "open_approval_change"); + + assertNotNull(wxXmlMessage.getApprovalInfo()); + assertEquals(wxXmlMessage.getApprovalInfo().getThirdNo(), Long.valueOf(201806010001L)); + assertEquals(wxXmlMessage.getApprovalInfo().getOpenSpName(), "付款"); + assertEquals(wxXmlMessage.getApprovalInfo().getThirdNo(), Long.valueOf(201806010001L)); + assertEquals(wxXmlMessage.getApprovalInfo().getApplyTime(), Long.valueOf(1527837645)); + assertEquals(wxXmlMessage.getApprovalInfo().getApplyUserName(), "xiaoming"); + + assertNotNull(wxXmlMessage.getApprovalInfo().getApprovalNodes()); + assertNotNull(wxXmlMessage.getApprovalInfo().getApprovalNodes().get(0)); + assertEquals(wxXmlMessage.getApprovalInfo().getApprovalNodes().get(0).getNodeAttr(), Integer.valueOf(1)); + assertEquals(wxXmlMessage.getApprovalInfo().getApprovalNodes().get(0).getNodeType(), Integer.valueOf(1)); + + assertNotNull(wxXmlMessage.getApprovalInfo().getApprovalNodes().get(0).getItems()); + assertEquals(wxXmlMessage.getApprovalInfo().getApprovalNodes().get(0).getItems().get(0).getItemName(), "xiaohong"); + assertEquals(wxXmlMessage.getApprovalInfo().getApprovalNodes().get(0).getItems().get(0).getItemOpTime(), Long.valueOf(0)); + + assertNotNull(wxXmlMessage.getApprovalInfo().getNotifyNodes().get(0)); + assertEquals(wxXmlMessage.getApprovalInfo().getNotifyNodes().get(0).getItemImage(), "http://www.qq.com/xxx.png"); + assertEquals(wxXmlMessage.getApprovalInfo().getNotifyNodes().get(0).getItemUserId(), Integer.valueOf(3)); + } +}