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); - String getSuiteAccessToken(); - - boolean isSuiteAccessTokenExpired(); - /** - * 强制将suite access token过期掉. + * 第三方应用的suite access token相关 */ + String getSuiteAccessToken(); + boolean isSuiteAccessTokenExpired(); + //强制将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过期掉. + void expireSuiteTicket(); + //应该是线程安全的 + void updateSuiteTicket(String suiteTicket, int expiresInSeconds); /** - * 强制将suite ticket过期掉. + * 第三方应用的其他配置,来自于企微配置 */ - void expireSuiteTicket(); + String getSuiteId(); + String getSuiteSecret(); + // 第三方应用的token,用来检查应用的签名 + String getToken(); + //第三方应用的EncodingAESKey,用来检查签名 + String getAesKey(); /** - * 应该是线程安全的. + * 企微服务商企业ID & 企业secret */ - void updateSuiteTicket(String suiteTicket, int expiresInSeconds); - String getCorpId(); - String getCorpSecret(); - String getSuiteId(); - - String getSuiteSecret(); - - String getToken(); + /** + * 授权企业的access token相关 + */ + String getAccessToken(String authCorpId); + boolean isAccessTokenExpired(String authCorpId); + void updateAccessToken(String authCorpId, String accessToken, int expiredInSeconds); - String getAesKey(); + /** + * 授权企业的js api ticket相关 + */ + String getAuthCorpJsApiTicket(String authCorpId); + boolean isAuthCorpJsApiTicketExpired(String authCorpId); + void updateAuthCorpJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds); - long getExpiresTime(); + /** + * 授权企业的第三方应用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(); - - File getTmpDirFile(); - - /** - * http client builder. - * - * @return ApacheHttpClientBuilder - */ 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 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 void setSuiteAccessToken(String suiteAccessToken) { @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,66 +94,58 @@ public synchronized void updateSuiteAccessToken(WxAccessToken suiteAccessToken) @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; + @Deprecated + public void setSuiteAccessTokenExpiresTime(long suiteAccessTokenExpiresTime) { + this.suiteAccessTokenExpiresTime = suiteAccessTokenExpiresTime; } - public void setCorpId(String corpId) { - this.corpId = corpId; + @Override + public String getSuiteTicket() { + return this.suiteTicket; } @Override - public String getCorpSecret() { - return this.corpSecret; + public boolean isSuiteTicketExpired() { + return System.currentTimeMillis() > this.suiteTicketExpiresTime; } - public void setCorpSecret(String corpSecret) { - this.corpSecret = corpSecret; + @Override + public void expireSuiteTicket() { + this.suiteTicketExpiresTime = 0; } @Override - public String getSuiteTicket() { - return this.suiteTicket; + public synchronized void updateSuiteTicket(String suiteTicket, int expiresInSeconds) { + this.suiteTicket = suiteTicket; + // 预留200秒的时间 + this.suiteTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L; } + + @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 - public boolean isSuiteTicketExpired() { - return System.currentTimeMillis() > this.suiteTicketExpiresTime; - } - - @Override - public synchronized void updateSuiteTicket(String suiteTicket, int expiresInSeconds) { - this.suiteTicket = suiteTicket; - // 预留200秒的时间 - this.suiteTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L; - } - - @Override - public void expireSuiteTicket() { - this.suiteTicketExpiresTime = 0; - } - @Override public String getSuiteId() { return this.suiteId; } + @Deprecated public void setSuiteId(String corpId) { this.suiteId = corpId; } @@ -152,6 +155,7 @@ public String getSuiteSecret() { return this.suiteSecret; } + @Deprecated public void setSuiteSecret(String corpSecret) { this.suiteSecret = corpSecret; } @@ -161,26 +165,107 @@ public String getToken() { return this.token; } + @Deprecated public void setToken(String token) { this.token = token; } @Override - public long getExpiresTime() { - return this.expiresTime; + public String getAesKey() { + return this.aesKey; + } + + @Deprecated + public void setAesKey(String aesKey) { + this.aesKey = aesKey; + } + + + @Override + public String getCorpId() { + return this.corpId; } - public void setExpiresTime(long expiresTime) { - this.expiresTime = expiresTime; + @Deprecated + public void setCorpId(String corpId) { + this.corpId = corpId; } @Override - public String getAesKey() { - return this.aesKey; + public String getCorpSecret() { + return this.corpSecret; } - public void setAesKey(String aesKey) { - this.aesKey = aesKey; + @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) { 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 static class Tp { 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 @@ 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 @@ 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.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 @@ /** *
  * 微信消息路由器,通过代码化的配置,把来自微信的消息交给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 WxCpXmlOutMessage route(final WxCpTpXmlMessage wxMessage, final Map {
-            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 WxCpXmlOutMessage route(final WxCpTpXmlMessage wxMessage) {
 
   private boolean isMsgDuplicated(WxCpTpXmlMessage wxMessage) {
     StringBuilder messageId = new StringBuilder();
-    if (StringUtils.isNotEmpty(wxMessage.getSuiteId())) {
-      messageId.append("-").append(wxMessage.getSuiteId());
-    }
-
-    if (StringUtils.isNotEmpty(wxMessage.getInfoType())) {
-      messageId.append("-").append(wxMessage.getInfoType());
-    }
+      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.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.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 WxCpTpMessageRouterRule infoType(String infoType) {
     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 WxCpTpMessageRouter next() {
   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,11 +45,17 @@ public abstract class BaseWxCpTpServiceImpl implements WxCpTpService, Requ */ protected final Object globalSuiteAccessTokenRefreshLock = new Object(); + /** - * 全局的是否正在刷新jsapi_ticket的锁. + * 全局刷新suite ticket的锁 */ protected final Object globalSuiteTicketRefreshLock = new Object(); + /** + * 全局的是否正在刷新jsapi_ticket的锁. + */ + protected final Object globalJsApiTicketRefreshLock = new Object(); + protected WxCpTpConfigStorage configStorage; private WxSessionManager sessionManager = new StandardSessionManager(); @@ -79,7 +85,12 @@ public String getSuiteAccessToken() throws WxErrorException { @Override public String getSuiteTicket() throws WxErrorException { - return getSuiteTicket(false); + if (this.configStorage.isSuiteTicketExpired()) { + // 本地suite ticket 不存在或者过期 + WxError wxError = WxError.fromJson("{\"errcode\":40085, \"errmsg\":\"invaild suite ticket\"}", WxType.CP); + throw new WxErrorException(wxError); + } + return this.configStorage.getSuiteTicket(); } @Override @@ -88,15 +99,62 @@ public String getSuiteTicket(boolean forceRefresh) throws WxErrorException { // if (forceRefresh) { // this.configStorage.expireSuiteTicket(); // } + return getSuiteTicket(); + } - if (this.configStorage.isSuiteTicketExpired()) { - // 本地suite ticket 不存在或者过期 - WxError wxError = WxError.fromJson("{\"errcode\":40085, \"errmsg\":\"invaild suite ticket\"}", WxType.CP); - throw new WxErrorException(wxError); + @Override + public void setSuiteTicket(String suiteTicket) throws WxErrorException { + synchronized (globalSuiteTicketRefreshLock) { + this.configStorage.updateSuiteTicket(suiteTicket, 10 * 60); } - return this.configStorage.getSuiteTicket(); } + @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)); + } +}