items;
+
+ public static WxOcrCommResult fromJson(String json) {
+ return WxGsonBuilder.create().fromJson(json, WxOcrCommResult.class);
+ }
+
+ @Override
+ public String toString() {
+ return WxGsonBuilder.create().toJson(this);
+ }
+
+ @Data
+ public static class Items implements Serializable {
+ private static final long serialVersionUID = 3066181677009102791L;
+
+ @SerializedName("text")
+ private String text;
+ @SerializedName("pos")
+ private WxOcrPos pos;
+
+ @Override
+ public String toString() {
+ return WxGsonBuilder.create().toJson(this);
+ }
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrDrivingLicenseResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrDrivingLicenseResult.java
new file mode 100644
index 0000000000..c9306200de
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrDrivingLicenseResult.java
@@ -0,0 +1,80 @@
+package me.chanjar.weixin.common.bean.ocr;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+
+import java.io.Serializable;
+
+/**
+ * @author Theo Nie
+ */
+@Data
+public class WxOcrDrivingLicenseResult implements Serializable {
+ private static final long serialVersionUID = -6984670645802585738L;
+
+ /**
+ * 证号
+ */
+ @SerializedName("id_num")
+ private String idNum;
+ /**
+ * 姓名
+ */
+ @SerializedName("name")
+ private String name;
+ /**
+ * 性别
+ */
+ @SerializedName("sex")
+ private String sex;
+ /**
+ * 国籍
+ */
+ @SerializedName("nationality")
+ private String nationality;
+ /**
+ * 住址
+ */
+ @SerializedName("address")
+ private String address;
+ /**
+ * 出生日期
+ */
+ @SerializedName("birth_date")
+ private String birthDate;
+ /**
+ * 初次领证日期
+ */
+ @SerializedName("issue_date")
+ private String issueDate;
+ /**
+ * 准驾车型
+ */
+ @SerializedName("car_class")
+ private String carClass;
+ /**
+ * 有效期限起始日
+ */
+ @SerializedName("valid_from")
+ private String validFrom;
+ /**
+ * 有效期限终止日
+ */
+ @SerializedName("valid_to")
+ private String validTo;
+ /**
+ * 印章文字
+ */
+ @SerializedName("official_seal")
+ private String officialSeal;
+
+ @Override
+ public String toString() {
+ return WxGsonBuilder.create().toJson(this);
+ }
+
+ public static WxOcrDrivingLicenseResult fromJson(String json) {
+ return WxGsonBuilder.create().fromJson(json, WxOcrDrivingLicenseResult.class);
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrDrivingResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrDrivingResult.java
new file mode 100644
index 0000000000..b486baf1c4
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrDrivingResult.java
@@ -0,0 +1,133 @@
+package me.chanjar.weixin.common.bean.ocr;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+
+import java.io.Serializable;
+
+/**
+ * @author Theo Nie
+ */
+@Data
+public class WxOcrDrivingResult implements Serializable {
+ private static final long serialVersionUID = -7477484374200211303L;
+
+ /**
+ * 车牌号码
+ */
+ @SerializedName("plate_num")
+ private String plateNum;
+ /**
+ * 车辆类型
+ */
+ @SerializedName("vehicle_type")
+ private String vehicleType;
+ /**
+ * 所有人
+ */
+ @SerializedName("owner")
+ private String owner;
+ /**
+ * 住址
+ */
+ @SerializedName("addr")
+ private String addr;
+ /**
+ * 使用性质
+ */
+ @SerializedName("use_character")
+ private String useCharacter;
+ /**
+ * 品牌型号
+ */
+ @SerializedName("model")
+ private String model;
+ /**
+ * 车辆识别代码
+ */
+ @SerializedName("vin")
+ private String vin;
+ /**
+ * 发动机号码
+ */
+ @SerializedName("engine_num")
+ private String engineNum;
+ /**
+ * 注册日期
+ */
+ @SerializedName("register_date")
+ private String registerDate;
+ /**
+ * 发证日期
+ */
+ @SerializedName("issue_date")
+ private String issueDate;
+ /**
+ * 车牌号码
+ */
+ @SerializedName("plate_num_b")
+ private String plateNumB;
+ /**
+ * 号牌
+ */
+ @SerializedName("record")
+ private String record;
+ /**
+ * 核定载人数
+ */
+ @SerializedName("passengers_num")
+ private String passengersNum;
+ /**
+ * 总质量
+ */
+ @SerializedName("total_quality")
+ private String totalQuality;
+ /**
+ * 整备质量
+ */
+ @SerializedName("prepare_quality")
+ private String prepareQuality;
+ /**
+ * 外廓尺寸
+ */
+ @SerializedName("overall_size")
+ private String overallSize;
+ /**
+ * 卡片正面位置(检测到卡片正面才会返回)
+ */
+ @SerializedName("card_position_front")
+ private CardPosition cardPositionFront;
+ /**
+ * 卡片反面位置(检测到卡片反面才会返回)
+ */
+ @SerializedName("card_position_back")
+ private CardPosition cardPositionBack;
+ /**
+ * 图片大小
+ */
+ @SerializedName("img_size")
+ private WxOcrImgSize imgSize;
+
+ @Data
+ public static class CardPosition implements Serializable {
+ private static final long serialVersionUID = 2884515165228160517L;
+
+ @SerializedName("pos")
+ private WxOcrPos pos;
+
+ @Override
+ public String toString() {
+ return WxGsonBuilder.create().toJson(this);
+ }
+ }
+
+ public static WxOcrDrivingResult fromJson(String json) {
+ return WxGsonBuilder.create().fromJson(json, WxOcrDrivingResult.class);
+ }
+
+ @Override
+ public String toString() {
+ return WxGsonBuilder.create().toJson(this);
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrIdCardResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrIdCardResult.java
new file mode 100644
index 0000000000..0b1e0ff838
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrIdCardResult.java
@@ -0,0 +1,32 @@
+package me.chanjar.weixin.common.bean.ocr;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+
+import java.io.Serializable;
+
+/**
+ * OCR身份证识别结果.
+ *
+ * @author Binary Wang
+ * @date 2019-06-23
+ */
+@Data
+public class WxOcrIdCardResult implements Serializable {
+ private static final long serialVersionUID = 8184352486986729980L;
+
+ @SerializedName("type")
+ private String type;
+ @SerializedName("name")
+ private String name;
+ @SerializedName("id")
+ private String id;
+ @SerializedName("valid_date")
+ private String validDate;
+
+ public static WxOcrIdCardResult fromJson(String json) {
+ return WxGsonBuilder.create().fromJson(json, WxOcrIdCardResult.class);
+ }
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrImgSize.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrImgSize.java
new file mode 100644
index 0000000000..f5446ab405
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrImgSize.java
@@ -0,0 +1,25 @@
+package me.chanjar.weixin.common.bean.ocr;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+
+import java.io.Serializable;
+
+/**
+ * @author Theo Nie
+ */
+@Data
+public class WxOcrImgSize implements Serializable {
+ private static final long serialVersionUID = 5234409123551074168L;
+
+ @SerializedName("w")
+ private int w;
+ @SerializedName("h")
+ private int h;
+
+ @Override
+ public String toString() {
+ return WxGsonBuilder.create().toJson(this);
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrPos.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrPos.java
new file mode 100644
index 0000000000..54089f3235
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrPos.java
@@ -0,0 +1,43 @@
+package me.chanjar.weixin.common.bean.ocr;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+
+import java.io.Serializable;
+
+/**
+ * @author Theo Nie
+ */
+@Data
+public class WxOcrPos implements Serializable {
+ private static final long serialVersionUID = 4204160206873907920L;
+
+ @SerializedName("left_top")
+ private Coordinate leftTop;
+ @SerializedName("right_top")
+ private Coordinate rightTop;
+ @SerializedName("right_bottom")
+ private Coordinate rightBottom;
+ @SerializedName("left_bottom")
+ private Coordinate leftBottom;
+
+ @Override
+ public String toString() {
+ return WxGsonBuilder.create().toJson(this);
+ }
+
+ @Data
+ public static class Coordinate implements Serializable {
+ private static final long serialVersionUID = 8675059935386304399L;
+ @SerializedName("x")
+ private int x;
+ @SerializedName("y")
+ private int y;
+
+ @Override
+ public String toString() {
+ return WxGsonBuilder.create().toJson(this);
+ }
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxError.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxError.java
deleted file mode 100644
index 46c0ae89be..0000000000
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxError.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package me.chanjar.weixin.common.bean.result;
-
-import me.chanjar.weixin.common.util.json.WxGsonBuilder;
-
-import java.io.Serializable;
-
-/**
- * 微信错误码说明,请阅读: 全局返回码说明
- *
- * @author Daniel Qian
- */
-public class WxError implements Serializable {
-
- private static final long serialVersionUID = 7869786563361406291L;
-
- private int errorCode;
-
- private String errorMsg;
-
- private String json;
-
- public static WxError fromJson(String json) {
- return WxGsonBuilder.create().fromJson(json, WxError.class);
- }
-
- public static Builder newBuilder() {
- return new Builder();
- }
-
- public int getErrorCode() {
- return this.errorCode;
- }
-
- public void setErrorCode(int errorCode) {
- this.errorCode = errorCode;
- }
-
- public String getErrorMsg() {
- return this.errorMsg;
- }
-
- public void setErrorMsg(String errorMsg) {
- this.errorMsg = errorMsg;
- }
-
- public String getJson() {
- return this.json;
- }
-
- public void setJson(String json) {
- this.json = json;
- }
-
- @Override
- public String toString() {
- if (this.json != null) {
- return this.json;
- }
- return "错误: Code=" + this.errorCode + ", Msg=" + this.errorMsg;
- }
-
- public static class Builder {
- private int errorCode;
- private String errorMsg;
-
- public Builder setErrorCode(int errorCode) {
- this.errorCode = errorCode;
- return this;
- }
-
- public Builder setErrorMsg(String errorMsg) {
- this.errorMsg = errorMsg;
- return this;
- }
-
- public WxError build() {
- WxError wxError = new WxError();
- wxError.setErrorCode(this.errorCode);
- wxError.setErrorMsg(this.errorMsg);
- return wxError;
- }
-
- }
-}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxMediaUploadResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxMediaUploadResult.java
index 09901c3e0e..a62bf3c605 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxMediaUploadResult.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxMediaUploadResult.java
@@ -1,12 +1,19 @@
package me.chanjar.weixin.common.bean.result;
-import me.chanjar.weixin.common.util.json.WxGsonBuilder;
-
import java.io.Serializable;
+import lombok.Data;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+
+/**
+ *
+ * @author Daniel Qian
+ */
+@Data
public class WxMediaUploadResult implements Serializable {
private static final long serialVersionUID = 330834334738622341L;
+ private String url;
private String type;
private String mediaId;
private String thumbMediaId;
@@ -16,42 +23,9 @@ public static WxMediaUploadResult fromJson(String json) {
return WxGsonBuilder.create().fromJson(json, WxMediaUploadResult.class);
}
- public String getType() {
- return this.type;
- }
-
- public void setType(String type) {
- this.type = type;
- }
-
- public String getMediaId() {
- return this.mediaId;
- }
-
- public void setMediaId(String mediaId) {
- this.mediaId = mediaId;
- }
-
- public long getCreatedAt() {
- return this.createdAt;
- }
-
- public void setCreatedAt(long createdAt) {
- this.createdAt = createdAt;
- }
-
- public String getThumbMediaId() {
- return this.thumbMediaId;
- }
-
- public void setThumbMediaId(String thumbMediaId) {
- this.thumbMediaId = thumbMediaId;
- }
-
@Override
public String toString() {
- return "WxUploadResult [type=" + this.type + ", media_id=" + this.mediaId + ", thumb_media_id=" + this.thumbMediaId
- + ", created_at=" + this.createdAt + "]";
+ return WxGsonBuilder.create().toJson(this);
}
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/enums/TicketType.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/enums/TicketType.java
new file mode 100644
index 0000000000..afbd1ec382
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/enums/TicketType.java
@@ -0,0 +1,35 @@
+package me.chanjar.weixin.common.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ *
+ * ticket类型枚举
+ * Created by Binary Wang on 2018/11/18.
+ *
+ *
+ * @author Binary Wang
+ */
+@Getter
+@RequiredArgsConstructor
+public enum TicketType {
+ /**
+ * jsapi
+ */
+ JSAPI("jsapi"),
+ /**
+ * sdk
+ */
+ SDK("2"),
+ /**
+ * 微信卡券
+ */
+ WX_CARD("wx_card");
+
+ /**
+ * type代码
+ */
+ private final String code;
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/enums/WxType.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/enums/WxType.java
new file mode 100644
index 0000000000..de047beb1d
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/enums/WxType.java
@@ -0,0 +1,32 @@
+package me.chanjar.weixin.common.enums;
+
+/**
+ *
+ * 微信类型枚举.
+ * Created by BinaryWang on 2018/5/14.
+ *
+ *
+ * @author Binary Wang
+ */
+public enum WxType {
+ /**
+ * 企业微信.
+ */
+ CP,
+ /**
+ * 微信公众号.
+ */
+ MP,
+ /**
+ * 微信小程序.
+ */
+ MiniApp,
+ /**
+ * 微信开放平台.
+ */
+ Open,
+ /**
+ * 微信支付.
+ */
+ Pay;
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxCpErrorMsgEnum.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxCpErrorMsgEnum.java
new file mode 100644
index 0000000000..c742959bb6
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxCpErrorMsgEnum.java
@@ -0,0 +1,1137 @@
+package me.chanjar.weixin.common.error;
+
+import lombok.Getter;
+
+/**
+ *
+ * 企业微信全局错误码.
+ * 参考文档:企业微信全局错误码
+ * Created by Binary Wang on 2018/5/13.
+ *
+ *
+ * @author Binary Wang
+ */
+@Getter
+public enum WxCpErrorMsgEnum {
+ /**
+ * 系统繁忙;服务器暂不可用,建议稍候重试。建议重试次数不超过3次.
+ */
+ CODE_1(-1, "系统繁忙;服务器暂不可用,建议稍候重试。建议重试次数不超过3次。"),
+ /**
+ * 请求成功;接口调用成功.
+ */
+ CODE_0(0, "请求成功;接口调用成功"),
+ /**
+ * 不合法的secret参数;secret在应用详情/通讯录管理助手可查看.
+ */
+ CODE_40001(40001, "不合法的secret参数;secret在应用详情/通讯录管理助手可查看"),
+ /**
+ * 无效的UserID.
+ */
+ CODE_40003(40003, "无效的UserID"),
+ /**
+ * 不合法的媒体文件类型;不满足系统文件要求。参考:上传的媒体文件限制.
+ */
+ CODE_40004(40004, "不合法的媒体文件类型;不满足系统文件要求。参考:上传的媒体文件限制"),
+ /**
+ * 不合法的type参数;合法的type取值,参考:上传临时素材.
+ */
+ CODE_40005(40005, "不合法的type参数;合法的type取值,参考:上传临时素材"),
+ /**
+ * 不合法的文件大小;系统文件要求,参考:上传的媒体文件限制.
+ */
+ CODE_40006(40006, "不合法的文件大小;系统文件要求,参考:上传的媒体文件限制"),
+ /**
+ * 不合法的media_id参数.
+ */
+ CODE_40007(40007, "不合法的media_id参数"),
+ /**
+ * 不合法的msgtype参数;合法的msgtype取值,参考:消息类型.
+ */
+ CODE_40008(40008, "不合法的msgtype参数;合法的msgtype取值,参考:消息类型"),
+ /**
+ * 上传图片大小不是有效值;图片大小的系统限制,参考上传的媒体文件限制.
+ */
+ CODE_40009(40009, "上传图片大小不是有效值;图片大小的系统限制,参考上传的媒体文件限制"),
+ /**
+ * 上传视频大小不是有效值;视频大小的系统限制,参考上传的媒体文件限制.
+ */
+ CODE_40011(40011, "上传视频大小不是有效值;视频大小的系统限制,参考上传的媒体文件限制"),
+ /**
+ * 不合法的CorpID;需确认CorpID是否填写正确,在 web管理端-设置 可查看.
+ */
+ CODE_40013(40013, "不合法的CorpID;需确认CorpID是否填写正确,在 web管理端-设置 可查看"),
+ /**
+ * 不合法的access_token.
+ */
+ CODE_40014(40014, "不合法的access_token"),
+ /**
+ * 不合法的按钮个数;菜单按钮1-3个.
+ */
+ CODE_40016(40016, "不合法的按钮个数;菜单按钮1-3个"),
+ /**
+ * 不合法的按钮类型;支持的类型,参考:按钮类型.
+ */
+ CODE_40017(40017, "不合法的按钮类型;支持的类型,参考:按钮类型"),
+ /**
+ * 不合法的按钮名字长度;长度应不超过16个字节.
+ */
+ CODE_40018(40018, "不合法的按钮名字长度;长度应不超过16个字节"),
+ /**
+ * 不合法的按钮KEY长度;长度应不超过128字节.
+ */
+ CODE_40019(40019, "不合法的按钮KEY长度;长度应不超过128字节"),
+ /**
+ * 不合法的按钮URL长度;长度应不超过1024字节.
+ */
+ CODE_40020(40020, "不合法的按钮URL长度;长度应不超过1024字节"),
+ /**
+ * 不合法的子菜单级数;只能包含一级菜单和二级菜单.
+ */
+ CODE_40022(40022, "不合法的子菜单级数;只能包含一级菜单和二级菜单"),
+ /**
+ * 不合法的子菜单按钮个数;子菜单按钮1-5个.
+ */
+ CODE_40023(40023, "不合法的子菜单按钮个数;子菜单按钮1-5个"),
+ /**
+ * 不合法的子菜单按钮类型;支持的类型,参考:按钮类型.
+ */
+ CODE_40024(40024, "不合法的子菜单按钮类型;支持的类型,参考:按钮类型"),
+ /**
+ * 不合法的子菜单按钮名字长度;支持的类型,参考:按钮类型.
+ */
+ CODE_40025(40025, "不合法的子菜单按钮名字长度;支持的类型,参考:按钮类型"),
+ /**
+ * 不合法的子菜单按钮KEY长度;长度应不超过60个字节.
+ */
+ CODE_40026(40026, "不合法的子菜单按钮KEY长度;长度应不超过60个字节"),
+ /**
+ * 不合法的子菜单按钮URL长度;长度应不超过1024字节.
+ */
+ CODE_40027(40027, "不合法的子菜单按钮URL长度;长度应不超过1024字节"),
+ /**
+ * 不合法的oauth_code.
+ */
+ CODE_40029(40029, "不合法的oauth_code"),
+ /**
+ * 不合法的UserID列表;指定的UserID列表,至少存在一个UserID不在通讯录中.
+ */
+ CODE_40031(40031, "不合法的UserID列表;指定的UserID列表,至少存在一个UserID不在通讯录中"),
+ /**
+ * 不合法的UserID列表长度.
+ */
+ CODE_40032(40032, "不合法的UserID列表长度"),
+ /**
+ * 不合法的请求字符;不能包含\\uxxxx格式的字符.
+ */
+ CODE_40033(40033, "不合法的请求字符;不能包含\\uxxxx格式的字符"),
+ /**
+ * 不合法的参数.
+ */
+ CODE_40035(40035, "不合法的参数"),
+ /**
+ * chatid不存在;会话需要先创建后,才可修改会话详情或者发起聊天.
+ */
+ CODE_40050(40050, "chatid不存在;会话需要先创建后,才可修改会话详情或者发起聊天"),
+ /**
+ * 不合法的子菜单url域名.
+ */
+ CODE_40054(40054, "不合法的子菜单url域名"),
+ /**
+ * 不合法的菜单url域名.
+ */
+ CODE_40055(40055, "不合法的菜单url域名"),
+ /**
+ * 不合法的agentid.
+ */
+ CODE_40056(40056, "不合法的agentid"),
+ /**
+ * 不合法的callbackurl或者callbackurl验证失败;可自助到开发调试工具重现.
+ */
+ CODE_40057(40057, "不合法的callbackurl或者callbackurl验证失败;可自助到开发调试工具重现"),
+ /**
+ * 不合法的参数;传递参数不符合系统要求,需要参照具体API接口说明.
+ */
+ CODE_40058(40058, "不合法的参数;传递参数不符合系统要求,需要参照具体API接口说明"),
+ /**
+ * 不合法的上报地理位置标志位;开关标志位只能填 0 或者 1.
+ */
+ CODE_40059(40059, "不合法的上报地理位置标志位;开关标志位只能填 0 或者 1"),
+ /**
+ * 参数为空.
+ */
+ CODE_40063(40063, "参数为空"),
+ /**
+ * 不合法的部门列表;部门列表为空,或者至少存在一个部门ID不存在于通讯录中.
+ */
+ CODE_40066(40066, "不合法的部门列表;部门列表为空,或者至少存在一个部门ID不存在于通讯录中"),
+ /**
+ * 不合法的标签ID;标签ID未指定,或者指定的标签ID不存在.
+ */
+ CODE_40068(40068, "不合法的标签ID;标签ID未指定,或者指定的标签ID不存在"),
+ /**
+ * 指定的标签范围结点全部无效.
+ */
+ CODE_40070(40070, "指定的标签范围结点全部无效"),
+ /**
+ * 不合法的标签名字;标签名字已经存在.
+ */
+ CODE_40071(40071, "不合法的标签名字;标签名字已经存在"),
+ /**
+ * 不合法的标签名字长度;不允许为空,最大长度限制为32个字(汉字或英文字母).
+ */
+ CODE_40072(40072, "不合法的标签名字长度;不允许为空,最大长度限制为32个字(汉字或英文字母)"),
+ /**
+ * 不合法的openid;openid不存在,需确认获取来源.
+ */
+ CODE_40073(40073, "不合法的openid;openid不存在,需确认获取来源"),
+ /**
+ * news消息不支持保密消息类型;图文消息支持保密类型需改用mpnews.
+ */
+ CODE_40074(40074, "news消息不支持保密消息类型;图文消息支持保密类型需改用mpnews"),
+ /**
+ * 不合法的pre_auth_code参数;预授权码不存在,参考:获取预授权码.
+ */
+ CODE_40077(40077, "不合法的pre_auth_code参数;预授权码不存在,参考:获取预授权码"),
+ /**
+ * 不合法的auth_code参数;需确认获取来源,并且只能消费一次.
+ */
+ CODE_40078(40078, "不合法的auth_code参数;需确认获取来源,并且只能消费一次"),
+ /**
+ * 不合法的suite_secret;套件secret可在第三方管理端套件详情查看.
+ */
+ CODE_40080(40080, "不合法的suite_secret;套件secret可在第三方管理端套件详情查看"),
+ /**
+ * 不合法的suite_token.
+ */
+ CODE_40082(40082, "不合法的suite_token"),
+ /**
+ * 不合法的suite_id;suite_id不存在.
+ */
+ CODE_40083(40083, "不合法的suite_id;suite_id不存在"),
+ /**
+ * 不合法的permanent_code参数.
+ */
+ CODE_40084(40084, "不合法的permanent_code参数"),
+ /**
+ * 不合法的的suite_ticket参数;suite_ticket不存在或者已失效.
+ */
+ CODE_40085(40085, "不合法的的suite_ticket参数;suite_ticket不存在或者已失效"),
+ /**
+ * 不合法的第三方应用appid;至少有一个不存在应用id.
+ */
+ CODE_40086(40086, "不合法的第三方应用appid;至少有一个不存在应用id"),
+ /**
+ * jobid不存在;请检查 jobid 来源.
+ */
+ CODE_40088(40088, "jobid不存在;请检查 jobid 来源"),
+ /**
+ * 批量任务的结果已清理;系统仅保存最近5次批量任务的结果。可在通讯录查看实际导入情况.
+ */
+ CODE_40089(40089, "批量任务的结果已清理;系统仅保存最近5次批量任务的结果。可在通讯录查看实际导入情况"),
+ /**
+ * secret不合法;可能用了别的企业的secret.
+ */
+ CODE_40091(40091, "secret不合法;可能用了别的企业的secret"),
+ /**
+ * 导入文件存在不合法的内容.
+ */
+ CODE_40092(40092, "导入文件存在不合法的内容"),
+ /**
+ * 不合法的jsapi_ticket参数;ticket已失效,或者拼写错误.
+ */
+ CODE_40093(40093, "不合法的jsapi_ticket参数;ticket已失效,或者拼写错误"),
+ /**
+ * 不合法的URL;缺少主页URL参数,或者URL不合法(链接需要带上协议头,以 http:// 或者 https:// 开头).
+ */
+ CODE_40094(40094, "不合法的URL;缺少主页URL参数,或者URL不合法(链接需要带上协议头,以 http:// 或者 https:// 开头)"),
+ /**
+ * 缺少access_token参数.
+ */
+ CODE_41001(41001, "缺少access_token参数"),
+ /**
+ * 缺少corpid参数.
+ */
+ CODE_41002(41002, "缺少corpid参数"),
+ /**
+ * 缺少secret参数.
+ */
+ CODE_41004(41004, "缺少secret参数"),
+ /**
+ * 缺少media_id参数;media_id为调用接口必填参数,请确认是否有传递.
+ */
+ CODE_41006(41006, "缺少media_id参数;media_id为调用接口必填参数,请确认是否有传递"),
+ /**
+ * 缺少auth code参数.
+ */
+ CODE_41008(41008, "缺少auth code参数"),
+ /**
+ * 缺少userid参数.
+ */
+ CODE_41009(41009, "缺少userid参数"),
+ /**
+ * 缺少url参数.
+ */
+ CODE_41010(41010, "缺少url参数"),
+ /**
+ * 缺少agentid参数.
+ */
+ CODE_41011(41011, "缺少agentid参数"),
+ /**
+ * 缺少 description 参数;发送文本卡片消息接口,description 是必填字段.
+ */
+ CODE_41033(41033, "缺少 description 参数;发送文本卡片消息接口,description 是必填字段"),
+ /**
+ * 缺少title参数;发送图文消息,标题是必填参数。请确认参数是否有传递.
+ */
+ CODE_41016(41016, "缺少title参数;发送图文消息,标题是必填参数。请确认参数是否有传递。"),
+ /**
+ * 缺少 department 参数.
+ */
+ CODE_41019(41019, "缺少 department 参数"),
+ /**
+ * 缺少tagid参数.
+ */
+ CODE_41017(41017, "缺少tagid参数"),
+ /**
+ * 缺少suite_id参数.
+ */
+ CODE_41021(41021, "缺少suite_id参数"),
+ /**
+ * 缺少suite_access_token参数.
+ */
+ CODE_41022(41022, "缺少suite_access_token参数"),
+ /**
+ * 缺少suite_ticket参数.
+ */
+ CODE_41023(41023, "缺少suite_ticket参数"),
+ /**
+ * 缺少secret参数.
+ */
+ CODE_41024(41024, "缺少secret参数"),
+ /**
+ * 缺少permanent_code参数.
+ */
+ CODE_41025(41025, "缺少permanent_code参数"),
+ /**
+ * access_token已过期;access_token有时效性,需要重新获取一次.
+ */
+ CODE_42001(42001, "access_token已过期;access_token有时效性,需要重新获取一次"),
+ /**
+ * pre_auth_code已过期;pre_auth_code有时效性,需要重新获取一次.
+ */
+ CODE_42007(42007, "pre_auth_code已过期;pre_auth_code有时效性,需要重新获取一次"),
+ /**
+ * suite_access_token已过期;suite_access_token有时效性,需要重新获取一次.
+ */
+ CODE_42009(42009, "suite_access_token已过期;suite_access_token有时效性,需要重新获取一次"),
+ /**
+ * 指定的userid未绑定微信或未关注微信插件;需要成员使用微信登录企业微信或者关注微信插件才能获取openid.
+ */
+ CODE_43004(43004, "指定的userid未绑定微信或未关注微信插件;需要成员使用微信登录企业微信或者关注微信插件才能获取openid"),
+ /**
+ * 多媒体文件为空;上传格式参考:上传临时素材,确认header和body的内容正确.
+ */
+ CODE_44001(44001, "多媒体文件为空;上传格式参考:上传临时素材,确认header和body的内容正确。"),
+ /**
+ * 文本消息content参数为空;发文本消息content为必填参数,且不能为空.
+ */
+ CODE_44004(44004, "文本消息content参数为空;发文本消息content为必填参数,且不能为空"),
+ /**
+ * 多媒体文件大小超过限制;图片不可超过5M;音频不可超过5M;文件不可超过20M.
+ */
+ CODE_45001(45001, "多媒体文件大小超过限制;图片不可超过5M;音频不可超过5M;文件不可超过20M"),
+ /**
+ * 消息内容大小超过限制.
+ */
+ CODE_45002(45002, "消息内容大小超过限制"),
+ /**
+ * 应用description参数长度不符合系统限制;设置应用若带有description参数,则长度必须为4至120个字符.
+ */
+ CODE_45004(45004, "应用description参数长度不符合系统限制;设置应用若带有description参数,则长度必须为4至120个字符"),
+ /**
+ * 语音播放时间超过限制;语音播放时长不能超过60秒.
+ */
+ CODE_45007(45007, "语音播放时间超过限制;语音播放时长不能超过60秒"),
+ /**
+ * 图文消息的文章数量不符合系统限制;图文消息的文章数量不能超过8条.
+ */
+ CODE_45008(45008, "图文消息的文章数量不符合系统限制;图文消息的文章数量不能超过8条"),
+ /**
+ * 接口调用超过限制.
+ */
+ CODE_45009(45009, "接口调用超过限制"),
+ /**
+ * 应用name参数长度不符合系统限制;设置应用若带有name参数,则不允许为空,且不超过32个字符.
+ */
+ CODE_45022(45022, "应用name参数长度不符合系统限制;设置应用若带有name参数,则不允许为空,且不超过32个字符"),
+ /**
+ * 帐号数量超过上限.
+ */
+ CODE_45024(45024, "帐号数量超过上限"),
+ /**
+ * 触发删除用户数的保护;限制参考:全量覆盖成员.
+ */
+ CODE_45026(45026, "触发删除用户数的保护;限制参考:全量覆盖成员"),
+ /**
+ * 图文消息author参数长度超过限制;最长64个字节.
+ */
+ CODE_45032(45032, "图文消息author参数长度超过限制;最长64个字节"),
+ /**
+ * 接口并发调用超过限制.
+ */
+ CODE_45033(45033, "接口并发调用超过限制"),
+ /**
+ * 菜单未设置;菜单需发布后才能获取到数据.
+ */
+ CODE_46003(46003, "菜单未设置;菜单需发布后才能获取到数据"),
+ /**
+ * 指定的用户不存在;需要确认指定的用户存在于通讯录中.
+ */
+ CODE_46004(46004, "指定的用户不存在;需要确认指定的用户存在于通讯录中"),
+ /**
+ * API接口无权限调用.
+ */
+ CODE_48002(48002, "API接口无权限调用"),
+ /**
+ * 不合法的suite_id;确认suite_access_token由指定的suite_id生成.
+ */
+ CODE_48003(48003, "不合法的suite_id;确认suite_access_token由指定的suite_id生成"),
+ /**
+ * 授权关系无效;可能是无授权或授权已被取消.
+ */
+ CODE_48004(48004, "授权关系无效;可能是无授权或授权已被取消"),
+ /**
+ * API接口已废弃;接口已不再支持,建议改用新接口或者新方案.
+ */
+ CODE_48005(48005, "API接口已废弃;接口已不再支持,建议改用新接口或者新方案"),
+ /**
+ * redirect_url未登记可信域名.
+ */
+ CODE_50001(50001, "redirect_url未登记可信域名"),
+ /**
+ * 成员不在权限范围;请检查应用或管理组的权限范围.
+ */
+ CODE_50002(50002, "成员不在权限范围;请检查应用或管理组的权限范围"),
+ /**
+ * 应用已禁用;禁用的应用无法使用API接口。可在”管理端-企业应用”启用应用.
+ */
+ CODE_50003(50003, "应用已禁用;禁用的应用无法使用API接口。可在”管理端-企业应用”启用应用"),
+ /**
+ * 部门长度不符合限制;部门名称不能为空且长度不能超过32个字.
+ */
+ CODE_60001(60001, "部门长度不符合限制;部门名称不能为空且长度不能超过32个字"),
+ /**
+ * 部门ID不存在;需要确认部门ID是否有带,并且存在通讯录中.
+ */
+ CODE_60003(60003, "部门ID不存在;需要确认部门ID是否有带,并且存在通讯录中"),
+ /**
+ * 父部门不存在;需要确认父亲部门ID是否有带,并且存在通讯录中.
+ */
+ CODE_60004(60004, "父部门不存在;需要确认父亲部门ID是否有带,并且存在通讯录中"),
+ /**
+ * 部门下存在成员;不允许删除有成员的部门.
+ */
+ CODE_60005(60005, "部门下存在成员;不允许删除有成员的部门"),
+ /**
+ * 部门下存在子部门;不允许删除有子部门的部门.
+ */
+ CODE_60006(60006, "部门下存在子部门;不允许删除有子部门的部门"),
+ /**
+ * 不允许删除根部门.
+ */
+ CODE_60007(60007, "不允许删除根部门"),
+ /**
+ * 部门已存在;部门ID或者部门名称已存在.
+ */
+ CODE_60008(60008, "部门已存在;部门ID或者部门名称已存在"),
+ /**
+ * 部门名称含有非法字符;不能含有 \\:?*“< >| 等字符.
+ */
+ CODE_60009(60009, "部门名称含有非法字符;不能含有 \\ :?*“< >| 等字符"),
+ /**
+ * 部门存在循环关系.
+ */
+ CODE_60010(60010, "部门存在循环关系"),
+ /**
+ * 指定的成员/部门/标签参数无权限.
+ */
+ CODE_60011(60011, "指定的成员/部门/标签参数无权限"),
+ /**
+ * 不允许删除默认应用;默认应用的id为0.
+ */
+ CODE_60012(60012, "不允许删除默认应用;默认应用的id为0"),
+ /**
+ * 访问ip不在白名单之中;请确认访问ip是否在服务商白名单IP列表.
+ */
+ CODE_60020(60020, "访问ip不在白名单之中;请确认访问ip是否在服务商白名单IP列表"),
+ /**
+ * 不允许修改第三方应用的主页 URL;第三方应用类型,不允许通过接口修改该应用的主页 URL.
+ */
+ CODE_60028(60028, "不允许修改第三方应用的主页 URL;第三方应用类型,不允许通过接口修改该应用的主页 URL"),
+ /**
+ * UserID已存在.
+ */
+ CODE_60102(60102, "UserID已存在"),
+ /**
+ * 手机号码不合法;长度不超过32位,字符仅支持数字,加号和减号.
+ */
+ CODE_60103(60103, "手机号码不合法;长度不超过32位,字符仅支持数字,加号和减号"),
+ /**
+ * 手机号码已存在;同一个企业内,成员的手机号不能重复。建议更换手机号,或者更新已有的手机记录.
+ */
+ CODE_60104(60104, "手机号码已存在;同一个企业内,成员的手机号不能重复。建议更换手机号,或者更新已有的手机记录。"),
+ /**
+ * 邮箱不合法;长度不超过64位,且为有效的email格式.
+ */
+ CODE_60105(60105, "邮箱不合法;长度不超过64位,且为有效的email格式"),
+ /**
+ * 邮箱已存在;同一个企业内,成员的邮箱不能重复。建议更换邮箱,或者更新已有的邮箱记录.
+ */
+ CODE_60106(60106, "邮箱已存在;同一个企业内,成员的邮箱不能重复。建议更换邮箱,或者更新已有的邮箱记录。"),
+ /**
+ * 微信号不合法;微信号格式由字母、数字、”-“、”_“组成,长度为 3-20 字节,首字符必须是字母或”-“或”_“.
+ */
+ CODE_60107(60107, "微信号不合法;微信号格式由字母、数字、”-“、”_“组成,长度为 3-20 字节,首字符必须是字母或”-“或”_“"),
+ /**
+ * 用户所属部门数量超过限制;用户同时归属部门不超过20个.
+ */
+ CODE_60110(60110, "用户所属部门数量超过限制;用户同时归属部门不超过20个"),
+ /**
+ * UserID不存在;UserID参数为空,或者不存在通讯录中.
+ */
+ CODE_60111(60111, "UserID不存在;UserID参数为空,或者不存在通讯录中"),
+ /**
+ * 成员name参数不合法;不能为空,且不能超过64字符.
+ */
+ CODE_60112(60112, "成员name参数不合法;不能为空,且不能超过64字符"),
+ /**
+ * 无效的部门id;部门不存在通讯录中.
+ */
+ CODE_60123(60123, "无效的部门id;部门不存在通讯录中"),
+ /**
+ * 无效的父部门id;父部门不存在通讯录中.
+ */
+ CODE_60124(60124, "无效的父部门id;父部门不存在通讯录中"),
+ /**
+ * 非法部门名字;不能为空,且不能超过64字节,且不能含有\\:*?”< >|等字符.
+ */
+ CODE_60125(60125, "非法部门名字;不能为空,且不能超过64字节,且不能含有\\:*?”< >|等字符"),
+ /**
+ * 缺少department参数.
+ */
+ CODE_60127(60127, "缺少department参数"),
+ /**
+ * 成员手机和邮箱都为空;成员手机和邮箱至少有个非空.
+ */
+ CODE_60129(60129, "成员手机和邮箱都为空;成员手机和邮箱至少有个非空"),
+ /**
+ * 发票已被其他公众号锁定.
+ */
+ CODE_72023(72023, "发票已被其他公众号锁定"),
+ /**
+ * 发票状态错误;reimburse_status状态错误,参考:更新发票状态.
+ */
+ CODE_72024(72024, "发票状态错误;reimburse_status状态错误,参考:更新发票状态"),
+ /**
+ * 存在发票不属于该用户;只能批量更新该openid的发票,参考:批量更新发票状态.
+ */
+ CODE_72037(72037, "存在发票不属于该用户;只能批量更新该openid的发票,参考:批量更新发票状态"),
+ /**
+ * 可信域名不正确,或者无ICP备案.
+ */
+ CODE_80001(80001, "可信域名不正确,或者无ICP备案"),
+ /**
+ * 部门下的结点数超过限制(3W).
+ */
+ CODE_81001(81001, "部门下的结点数超过限制(3W)"),
+ /**
+ * 部门最多15层.
+ */
+ CODE_81002(81002, "部门最多15层"),
+ /**
+ * 无权限操作标签.
+ */
+ CODE_81011(81011, "无权限操作标签"),
+ /**
+ * UserID、部门ID、标签ID全部非法或无权限.
+ */
+ CODE_81013(81013, "UserID、部门ID、标签ID全部非法或无权限"),
+ /**
+ * 标签添加成员,单次添加user或party过多.
+ */
+ CODE_81014(81014, "标签添加成员,单次添加user或party过多"),
+ /**
+ * 指定的成员/部门/标签全部无效.
+ */
+ CODE_82001(82001, "指定的成员/部门/标签全部无效"),
+ /**
+ * 不合法的PartyID列表长度;发消息,单次不能超过100个部门.
+ */
+ CODE_82002(82002, "不合法的PartyID列表长度;发消息,单次不能超过100个部门"),
+ /**
+ * 不合法的TagID列表长度;发消息,单次不能超过100个标签.
+ */
+ CODE_82003(82003, "不合法的TagID列表长度;发消息,单次不能超过100个标签"),
+ /**
+ * 成员票据过期.
+ */
+ CODE_84014(84014, "成员票据过期"),
+ /**
+ * 成员票据无效;确认user_ticket参数来源是否正确。参考接口:根据code获取成员信息.
+ */
+ CODE_84015(84015, "成员票据无效;确认user_ticket参数来源是否正确。参考接口:根据code获取成员信息"),
+ /**
+ * 缺少templateid参数.
+ */
+ CODE_84019(84019, "缺少templateid参数"),
+ /**
+ * templateid不存在;确认参数是否有带,并且已创建.
+ */
+ CODE_84020(84020, "templateid不存在;确认参数是否有带,并且已创建"),
+ /**
+ * 缺少register_code参数.
+ */
+ CODE_84021(84021, "缺少register_code参数"),
+ /**
+ * 无效的register_code参数.
+ */
+ CODE_84022(84022, "无效的register_code参数"),
+ /**
+ * 不允许调用设置通讯录同步完成接口.
+ */
+ CODE_84023(84023, "不允许调用设置通讯录同步完成接口"),
+ /**
+ * 无注册信息.
+ */
+ CODE_84024(84024, "无注册信息"),
+ /**
+ * 不符合的state参数;必须是[a-zA-Z0-9]的参数值,长度不可超过128个字节.
+ */
+ CODE_84025(84025, "不符合的state参数;必须是[a-zA-Z0-9]的参数值,长度不可超过128个字节"),
+ /**
+ * 缺少caller参数.
+ */
+ CODE_84052(84052, "缺少caller参数"),
+ /**
+ * 缺少callee参数.
+ */
+ CODE_84053(84053, "缺少callee参数"),
+ /**
+ * 缺少auth_corpid参数.
+ */
+ CODE_84054(84054, "缺少auth_corpid参数"),
+ /**
+ * 超过拨打公费电话频率。排查方法:同一个客服5秒内只能调用api拨打一次公费电话
+ */
+ CODE_84055(84055, "超过拨打公费电话频率。排查方法:同一个客服5秒内只能调用api拨打一次公费电话"),
+ /**
+ * 被拨打用户安装应用时未授权拨打公费电话权限.
+ */
+ CODE_84056(84056, "被拨打用户安装应用时未授权拨打公费电话权限"),
+ /**
+ * 公费电话余额不足.
+ */
+ CODE_84057(84057, "公费电话余额不足"),
+ /**
+ * caller
+ */
+ CODE_84058(84058, "caller 呼叫号码不支持"),
+ /**
+ * 号码非法.
+ */
+ CODE_84059(84059, "号码非法"),
+ /**
+ * callee
+ */
+ CODE_84060(84060, "callee 呼叫号码不支持"),
+ /**
+ * 不存在外部联系人的关系.
+ */
+ CODE_84061(84061, "不存在外部联系人的关系"),
+ /**
+ * 未开启公费电话应用.
+ */
+ CODE_84062(84062, "未开启公费电话应用"),
+ /**
+ * caller不存在.
+ */
+ CODE_84063(84063, "caller不存在"),
+ /**
+ * callee不存在.
+ */
+ CODE_84064(84064, "callee不存在"),
+ /**
+ * caller跟callee电话号码一致。排查方法:不允许自己拨打给自己
+ */
+ CODE_84065(84065, "caller跟callee电话号码一致。排查方法:不允许自己拨打给自己"),
+ /**
+ * 服务商拨打次数超过限制。排查方法:单个企业管理员,在一天(以上午10
+ */
+ CODE_84066(84066, "服务商拨打次数超过限制。排查方法:单个企业管理员,在一天(以上午10:00为起始时间)内,对应单个服务商,只能被呼叫【4】次。"),
+ /**
+ * 管理员收到的服务商公费电话个数超过限制。排查方法:单个企业管理员,在一天(以上午10
+ */
+ CODE_84067(84067, "管理员收到的服务商公费电话个数超过限制。排查方法:单个企业管理员,在一天(以上午10:00为起始时间)内,一共只能被【3】个服务商成功呼叫。"),
+ /**
+ * 拨打方被限制拨打公费电话.
+ */
+ CODE_84069(84069, "拨打方被限制拨打公费电话"),
+ /**
+ * 不支持的电话号码。排查方法:拨打方或者被拨打方电话号码不支持
+ */
+ CODE_84070(84070, "不支持的电话号码。排查方法:拨打方或者被拨打方电话号码不支持"),
+ /**
+ * 不合法的外部联系人授权码。排查方法:非法或者已经消费过
+ */
+ CODE_84071(84071, "不合法的外部联系人授权码。排查方法:非法或者已经消费过"),
+ /**
+ * 应用未配置客服.
+ */
+ CODE_84072(84072, "应用未配置客服"),
+ /**
+ * 客服userid不在应用配置的客服列表中.
+ */
+ CODE_84073(84073, "客服userid不在应用配置的客服列表中"),
+ /**
+ * 没有外部联系人权限.
+ */
+ CODE_84074(84074, "没有外部联系人权限"),
+ /**
+ * 不合法或过期的authcode.
+ */
+ CODE_84075(84075, "不合法或过期的authcode"),
+ /**
+ * 缺失authcode.
+ */
+ CODE_84076(84076, "缺失authcode"),
+ /**
+ * 订单价格过高,无法受理.
+ */
+ CODE_84077(84077, "订单价格过高,无法受理"),
+ /**
+ * 购买人数不正确.
+ */
+ CODE_84078(84078, "购买人数不正确"),
+ /**
+ * 价格策略不存在.
+ */
+ CODE_84079(84079, "价格策略不存在"),
+ /**
+ * 订单不存在.
+ */
+ CODE_84080(84080, "订单不存在"),
+ /**
+ * 存在未支付订单.
+ */
+ CODE_84081(84081, "存在未支付订单"),
+ /**
+ * 存在申请退款中的订单.
+ */
+ CODE_84082(84082, "存在申请退款中的订单"),
+ /**
+ * 非服务人员.
+ */
+ CODE_84083(84083, "非服务人员"),
+ /**
+ * 非跟进用户.
+ */
+ CODE_84084(84084, "非跟进用户"),
+ /**
+ * 应用已下架.
+ */
+ CODE_84085(84085, "应用已下架"),
+ /**
+ * 订单人数超过可购买最大人数.
+ */
+ CODE_84086(84086, "订单人数超过可购买最大人数"),
+ /**
+ * 打开订单支付前禁止关闭订单.
+ */
+ CODE_84087(84087, "打开订单支付前禁止关闭订单"),
+ /**
+ * 禁止关闭已支付的订单.
+ */
+ CODE_84088(84088, "禁止关闭已支付的订单"),
+ /**
+ * 订单已支付.
+ */
+ CODE_84089(84089, "订单已支付"),
+ /**
+ * 缺失user_ticket.
+ */
+ CODE_84090(84090, "缺失user_ticket"),
+ /**
+ * 订单价格不可低于下限.
+ */
+ CODE_84091(84091, "订单价格不可低于下限"),
+ /**
+ * 无法发起代下单操作.
+ */
+ CODE_84092(84092, "无法发起代下单操作"),
+ /**
+ * 代理关系已占用,无法代下单.
+ */
+ CODE_84093(84093, "代理关系已占用,无法代下单"),
+ /**
+ * 该应用未配置代理分润规则,请先联系应用服务商处理.
+ */
+ CODE_84094(84094, "该应用未配置代理分润规则,请先联系应用服务商处理"),
+ /**
+ * 免费试用版,无法扩容.
+ */
+ CODE_84095(84095, "免费试用版,无法扩容"),
+ /**
+ * 免费试用版,无法续期.
+ */
+ CODE_84096(84096, "免费试用版,无法续期"),
+ /**
+ * 当前企业有未处理订单.
+ */
+ CODE_84097(84097, "当前企业有未处理订单"),
+ /**
+ * 固定总量,无法扩容.
+ */
+ CODE_84098(84098, "固定总量,无法扩容"),
+ /**
+ * 非购买状态,无法扩容.
+ */
+ CODE_84099(84099, "非购买状态,无法扩容"),
+ /**
+ * 未购买过此应用,无法续期.
+ */
+ CODE_84100(84100, "未购买过此应用,无法续期"),
+ /**
+ * 企业已试用付费版本,无法全新购买.
+ */
+ CODE_84101(84101, "企业已试用付费版本,无法全新购买"),
+ /**
+ * 企业当前应用状态已过期,无法扩容.
+ */
+ CODE_84102(84102, "企业当前应用状态已过期,无法扩容"),
+ /**
+ * 仅可修改未支付订单.
+ */
+ CODE_84103(84103, "仅可修改未支付订单"),
+ /**
+ * 订单已支付,无法修改.
+ */
+ CODE_84104(84104, "订单已支付,无法修改"),
+ /**
+ * 订单已被取消,无法修改.
+ */
+ CODE_84105(84105, "订单已被取消,无法修改"),
+ /**
+ * 企业含有该应用的待支付订单,无法代下单.
+ */
+ CODE_84106(84106, "企业含有该应用的待支付订单,无法代下单"),
+ /**
+ * 企业含有该应用的退款中订单,无法代下单.
+ */
+ CODE_84107(84107, "企业含有该应用的退款中订单,无法代下单"),
+ /**
+ * 企业含有该应用的待生效订单,无法代下单.
+ */
+ CODE_84108(84108, "企业含有该应用的待生效订单,无法代下单"),
+ /**
+ * 订单定价不能未0.
+ */
+ CODE_84109(84109, "订单定价不能未0"),
+ /**
+ * 新安装应用不在试用状态,无法升级为付费版.
+ */
+ CODE_84110(84110, "新安装应用不在试用状态,无法升级为付费版"),
+ /**
+ * 无足够可用优惠券.
+ */
+ CODE_84111(84111, "无足够可用优惠券"),
+ /**
+ * 无法关闭未支付订单.
+ */
+ CODE_84112(84112, "无法关闭未支付订单"),
+ /**
+ * 无付费信息.
+ */
+ CODE_84113(84113, "无付费信息"),
+ /**
+ * 虚拟版本不支持下单.
+ */
+ CODE_84114(84114, "虚拟版本不支持下单"),
+ /**
+ * 虚拟版本不支持扩容.
+ */
+ CODE_84115(84115, "虚拟版本不支持扩容"),
+ /**
+ * 虚拟版本不支持续期.
+ */
+ CODE_84116(84116, "虚拟版本不支持续期"),
+ /**
+ * 在虚拟正式版期内不能扩容.
+ */
+ CODE_84117(84117, "在虚拟正式版期内不能扩容"),
+ /**
+ * 虚拟正式版期内不能变更版本.
+ */
+ CODE_84118(84118, "虚拟正式版期内不能变更版本"),
+ /**
+ * 当前企业未报备,无法进行代下单.
+ */
+ CODE_84119(84119, "当前企业未报备,无法进行代下单"),
+ /**
+ * 当前应用版本已删除.
+ */
+ CODE_84120(84120, "当前应用版本已删除"),
+ /**
+ * 应用版本已删除,无法扩容.
+ */
+ CODE_84121(84121, "应用版本已删除,无法扩容"),
+ /**
+ * 应用版本已删除,无法续期.
+ */
+ CODE_84122(84122, "应用版本已删除,无法续期"),
+ /**
+ * 非虚拟版本,无法升级.
+ */
+ CODE_84123(84123, "非虚拟版本,无法升级"),
+ /**
+ * 非行业方案订单,不能添加部分应用版本的订单.
+ */
+ CODE_84124(84124, "非行业方案订单,不能添加部分应用版本的订单"),
+ /**
+ * 购买人数不能少于最少购买人数.
+ */
+ CODE_84125(84125, "购买人数不能少于最少购买人数"),
+ /**
+ * 购买人数不能多于最大购买人数.
+ */
+ CODE_84126(84126, "购买人数不能多于最大购买人数"),
+ /**
+ * 无应用管理权限.
+ */
+ CODE_84127(84127, "无应用管理权限"),
+ /**
+ * 无该行业方案下全部应用的管理权限.
+ */
+ CODE_84128(84128, "无该行业方案下全部应用的管理权限"),
+ /**
+ * 付费策略已被删除,无法下单.
+ */
+ CODE_84129(84129, "付费策略已被删除,无法下单"),
+ /**
+ * 订单生效时间不合法.
+ */
+ CODE_84130(84130, "订单生效时间不合法"),
+ /**
+ * 文件转译解析错误。排查方法:只支持utf8文件转译,可能是不支持的文件类型或者格式
+ */
+ CODE_84200(84200, "文件转译解析错误。排查方法:只支持utf8文件转译,可能是不支持的文件类型或者格式"),
+ /**
+ * 包含不合法的词语.
+ */
+ CODE_85002(85002, "包含不合法的词语"),
+ /**
+ * 每企业每个月设置的可信域名不可超过20个.
+ */
+ CODE_85004(85004, "每企业每个月设置的可信域名不可超过20个"),
+ /**
+ * 可信域名未通过所有权校验.
+ */
+ CODE_85005(85005, "可信域名未通过所有权校验"),
+ /**
+ * 参数 chatid 不合法.
+ */
+ CODE_86001(86001, "参数 chatid 不合法"),
+ /**
+ * 参数 chatid 不存在.
+ */
+ CODE_86003(86003, "参数 chatid 不存在"),
+ /**
+ * 参数 群名不合法.
+ */
+ CODE_86004(86004, "参数 群名不合法"),
+ /**
+ * 参数 群主不合法.
+ */
+ CODE_86005(86005, "参数 群主不合法"),
+ /**
+ * 群成员数过多或过少.
+ */
+ CODE_86006(86006, "群成员数过多或过少"),
+ /**
+ * 不合法的群成员.
+ */
+ CODE_86007(86007, "不合法的群成员"),
+ /**
+ * 非法操作非自己创建的群.
+ */
+ CODE_86008(86008, "非法操作非自己创建的群"),
+ /**
+ * 存在非法会话成员ID.
+ */
+ CODE_86216(86216, "存在非法会话成员ID"),
+ /**
+ * 会话发送者不在会话成员列表中;会话的发送者,必须是会话的成员列表之一.
+ */
+ CODE_86217(86217, "会话发送者不在会话成员列表中;会话的发送者,必须是会话的成员列表之一"),
+ /**
+ * 指定的会话参数不合法.
+ */
+ CODE_86220(86220, "指定的会话参数不合法"),
+ /**
+ * 未认证摇一摇周边.
+ */
+ CODE_90001(90001, "未认证摇一摇周边"),
+ /**
+ * 缺少摇一摇周边ticket参数.
+ */
+ CODE_90002(90002, "缺少摇一摇周边ticket参数"),
+ /**
+ * 摇一摇周边ticket参数不合法.
+ */
+ CODE_90003(90003, "摇一摇周边ticket参数不合法"),
+ /**
+ * 非法的对外属性类型.
+ */
+ CODE_90100(90100, "非法的对外属性类型"),
+ /**
+ * 对外属性:文本类型长度不合法;文本长度不可超过12个UTF8字符.
+ */
+ CODE_90101(90101, "对外属性:文本类型长度不合法;文本长度不可超过12个UTF8字符"),
+ /**
+ * 对外属性:网页类型标题长度不合法;标题长度不可超过12个UTF8字符.
+ */
+ CODE_90102(90102, "对外属性:网页类型标题长度不合法;标题长度不可超过12个UTF8字符"),
+ /**
+ * 对外属性:网页url不合法.
+ */
+ CODE_90103(90103, "对外属性:网页url不合法"),
+ /**
+ * 对外属性:小程序类型标题长度不合法;标题长度不可超过12个UTF8字符.
+ */
+ CODE_90104(90104, "对外属性:小程序类型标题长度不合法;标题长度不可超过12个UTF8字符"),
+ /**
+ * 对外属性:小程序类型pagepath不合法.
+ */
+ CODE_90105(90105, "对外属性:小程序类型pagepath不合法"),
+ /**
+ * 对外属性:请求参数不合法.
+ */
+ CODE_90106(90106, "对外属性:请求参数不合法"),
+ /**
+ * 获取ticket的类型无效.
+ */
+ CODE_91040(91040, "获取ticket的类型无效"),
+ /**
+ * 无权限操作指定的应用.
+ */
+ CODE_301002(301002, "无权限操作指定的应用"),
+ /**
+ * 不允许删除创建者;创建者不允许从通讯录中删除。如果需要删除该成员,需要先在WEB管理端转移创建者身份.
+ */
+ CODE_301005(301005, "不允许删除创建者;创建者不允许从通讯录中删除。如果需要删除该成员,需要先在WEB管理端转移创建者身份。"),
+ /**
+ * 参数 position 不合法;长度不允许超过128个字符.
+ */
+ CODE_301012(301012, "参数 position 不合法;长度不允许超过128个字符"),
+ /**
+ * 参数 telephone 不合法;telephone必须由1-32位的纯数字或’-‘号组成.
+ */
+ CODE_301013(301013, "参数 telephone 不合法;telephone必须由1-32位的纯数字或’-‘号组成。"),
+ /**
+ * 参数 english_name 不合法;参数如果有传递,不允许为空字符串,同时不能超过64字节,只能是由字母、数字、点(.)、减号(-)、空格或下划线(_)组成.
+ */
+ CODE_301014(301014, "参数 english_name 不合法;参数如果有传递,不允许为空字符串,同时不能超过64字节,只能是由字母、数字、点(.)、减号(-)、空格或下划线(_)组成"),
+ /**
+ * 参数 mediaid 不合法;请检查 mediaid 来源,应该通过上传临时素材的图片类型获得mediaid.
+ */
+ CODE_301015(301015, "参数 mediaid 不合法;请检查 mediaid 来源,应该通过上传临时素材的图片类型获得mediaid"),
+ /**
+ * 上传语音文件不符合系统要求;语音文件的系统限制,参考上传的媒体文件限制.
+ */
+ CODE_301016(301016, "上传语音文件不符合系统要求;语音文件的系统限制,参考上传的媒体文件限制"),
+ /**
+ * 上传语音文件仅支持AMR格式;语音文件的系统限制,参考上传的媒体文件限制.
+ */
+ CODE_301017(301017, "上传语音文件仅支持AMR格式;语音文件的系统限制,参考上传的媒体文件限制"),
+ /**
+ * 参数 userid 无效;至少有一个userid不存在于通讯录中.
+ */
+ CODE_301021(301021, "参数 userid 无效;至少有一个userid不存在于通讯录中"),
+ /**
+ * 获取打卡数据失败;系统失败,可重试处理.
+ */
+ CODE_301022(301022, "获取打卡数据失败;系统失败,可重试处理"),
+ /**
+ * useridlist非法或超过限额;列表数量不能为0且不超过100.
+ */
+ CODE_301023(301023, "useridlist非法或超过限额;列表数量不能为0且不超过100"),
+ /**
+ * 获取打卡记录时间间隔超限;保证开始时间大于0 且结束时间大于 0 且结束时间大于开始时间,且间隔少于93天.
+ */
+ CODE_301024(301024, "获取打卡记录时间间隔超限;保证开始时间大于0 且结束时间大于 0 且结束时间大于开始时间,且间隔少于93天"),
+ /**
+ * 提交审批单请求参数错误
+ */
+ CODE_301025(301025,"提交审批单请求参数错误"),
+ /**
+ * 不允许更新该用户的userid.
+ */
+ CODE_301036(301036, "不允许更新该用户的userid"),
+ /**
+ * 无审批应用权限,或者提单者不在审批应用/自建应用的可见范围
+ */
+ CODE_301055(301055,"无审批应用权限,或者提单者不在审批应用/自建应用的可见范围"),
+ /**
+ * 审批应用已停用
+ */
+ CODE_301056(301056,"审批应用已停用"),
+ /**
+ * 通用错误码,提交审批单内部接口失败
+ */
+ CODE_301057(301057,"通用错误码,提交审批单内部接口失败"),
+ /**
+ * 批量导入任务的文件中userid有重复.
+ */
+ CODE_302003(302003, "批量导入任务的文件中userid有重复"),
+ /**
+ * 组织架构不合法(1不是一棵树,2 多个一样的partyid,3 partyid空,4 partyid name 空,5 同一个父节点下有两个子节点 部门名字一样 可能是以上情况,请一一排查).
+ */
+ CODE_302004(302004, "组织架构不合法(1不是一棵树,2 多个一样的partyid,3 partyid空,4 partyid name 空,5 同一个父节点下有两个子节点 部门名字一样 可能是以上情况,请一一排查)"),
+ /**
+ * 批量导入系统失败,请重新尝试导入.
+ */
+ CODE_302005(302005, "批量导入系统失败,请重新尝试导入"),
+ /**
+ * 批量导入任务的文件中partyid有重复.
+ */
+ CODE_302006(302006, "批量导入任务的文件中partyid有重复"),
+ /**
+ * 批量导入任务的文件中,同一个部门下有两个子部门名字一样.
+ */
+ CODE_302007(302007, "批量导入任务的文件中,同一个部门下有两个子部门名字一样"),
+ /**
+ * CorpId参数无效;指定的CorpId不存在.
+ */
+ CODE_2000002(2000002, "CorpId参数无效;指定的CorpId不存在");
+
+ private int code;
+ private String msg;
+
+ WxCpErrorMsgEnum(int code, String msg) {
+ this.code = code;
+ this.msg = msg;
+ }
+
+ /**
+ * 通过错误代码查找其中文含义..
+ */
+ public static String findMsgByCode(int code) {
+ for (WxCpErrorMsgEnum value : WxCpErrorMsgEnum.values()) {
+ if (value.code == code) {
+ return value.msg;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxError.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxError.java
new file mode 100644
index 0000000000..f1be843827
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxError.java
@@ -0,0 +1,94 @@
+package me.chanjar.weixin.common.error;
+
+import lombok.Builder;
+import lombok.Data;
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.Serializable;
+
+/**
+ * 微信错误码.
+ * 请阅读:
+ * 公众平台:全局返回码说明
+ * 企业微信:全局错误码
+ *
+ * @author Daniel Qian & Binary Wang
+ */
+@Data
+@Builder
+public class WxError implements Serializable {
+ private static final long serialVersionUID = 7869786563361406291L;
+
+ /**
+ * 微信错误代码.
+ */
+ private int errorCode;
+
+ /**
+ * 微信错误信息.
+ * (如果可以翻译为中文,就为中文)
+ */
+ private String errorMsg;
+
+ /**
+ * 微信接口返回的错误原始信息(英文).
+ */
+ private String errorMsgEn;
+
+ private String json;
+
+ public static WxError fromJson(String json) {
+ return fromJson(json, null);
+ }
+
+ public static WxError fromJson(String json, WxType type) {
+ final WxError wxError = WxGsonBuilder.create().fromJson(json, WxError.class);
+ if (wxError.getErrorCode() == 0 || type == null) {
+ return wxError;
+ }
+
+ if (StringUtils.isNotEmpty(wxError.getErrorMsg())) {
+ wxError.setErrorMsgEn(wxError.getErrorMsg());
+ }
+
+ switch (type) {
+ case MP: {
+ final String msg = WxMpErrorMsgEnum.findMsgByCode(wxError.getErrorCode());
+ if (msg != null) {
+ wxError.setErrorMsg(msg);
+ }
+ break;
+ }
+ case CP: {
+ final String msg = WxCpErrorMsgEnum.findMsgByCode(wxError.getErrorCode());
+ if (msg != null) {
+ wxError.setErrorMsg(msg);
+ }
+ break;
+ }
+ case MiniApp: {
+ final String msg = WxMaErrorMsgEnum.findMsgByCode(wxError.getErrorCode());
+ if (msg != null) {
+ wxError.setErrorMsg(msg);
+ }
+ break;
+ }
+ default:
+ return wxError;
+ }
+
+ return wxError;
+ }
+
+ @Override
+ public String toString() {
+ if (this.json == null) {
+ return "错误代码:" + this.errorCode + ", 错误信息:" + this.errorMsg;
+ }
+
+ return "错误代码:" + this.errorCode + ", 错误信息:" + this.errorMsg + ",微信原始报文:" + this.json;
+ }
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/exception/WxErrorException.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxErrorException.java
similarity index 81%
rename from weixin-java-common/src/main/java/me/chanjar/weixin/common/exception/WxErrorException.java
rename to weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxErrorException.java
index 4038e60185..6e9a2c538d 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/exception/WxErrorException.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxErrorException.java
@@ -1,9 +1,9 @@
-package me.chanjar.weixin.common.exception;
-
-import me.chanjar.weixin.common.bean.result.WxError;
+package me.chanjar.weixin.common.error;
+/**
+ * @author Daniel Qian
+ */
public class WxErrorException extends Exception {
-
private static final long serialVersionUID = -6357149550353160810L;
private WxError error;
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnum.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnum.java
new file mode 100644
index 0000000000..eced6027e9
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnum.java
@@ -0,0 +1,490 @@
+package me.chanjar.weixin.common.error;
+
+import lombok.Getter;
+
+/**
+ * 微信小程序错误码
+ *
+ * @author biggates
+ */
+@Getter
+public enum WxMaErrorMsgEnum {
+ /**
+ *
+ * 获取 access_token 时 AppSecret 错误,
+ * 或者 access_token 无效。请开发者认真比对 AppSecret 的正确性,或查看是否正在为恰当的小程序调用接口
+ * 对应操作:sendCustomerMessage
+ * 对应地址:
+ * POST https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/sendCustomerMessage.html
+ *
+ */
+ CODE_40001(40001, "access_token 无效或 AppSecret 错误"),
+ /**
+ *
+ * 不合法的凭证类型
+ * 对应操作:sendCustomerMessage
+ * 对应地址:
+ * POST https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/sendCustomerMessage.html
+ *
+ */
+ CODE_40002(40002, "不合法的凭证类型"),
+ /**
+ *
+ * touser不是正确的openid.
+ * 对应操作:sendCustomerMessage, sendUniformMessage
+ * 对应地址:
+ * POST https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
+ * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/sendCustomerMessage.html
+ * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html
+ *
+ */
+ CODE_40003(40003, "openid 不正确"),
+ /**
+ *
+ * 无效媒体文件类型
+ * 对应操作:uploadTempMedia
+ * 对应地址:
+ * POST https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/uploadTempMedia.html
+ *
+ */
+ CODE_40004(40004, "无效媒体文件类型"),
+ /**
+ *
+ * 无效媒体文件 ID.
+ * 对应操作:getTempMedia
+ * 对应地址:
+ * GET https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/getTempMedia.html
+ *
+ */
+ CODE_40007(40007, "无效媒体文件 ID"),
+ /**
+ *
+ * appid不正确,或者不符合绑定关系要求.
+ * 对应操作:sendUniformMessage
+ * 对应地址:
+ * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html
+ *
+ */
+ CODE_40013(40013, "appid不正确,或者不符合绑定关系要求"),
+ /**
+ *
+ * template_id 不正确.
+ * 对应操作:sendUniformMessage, sendTemplateMessage
+ * 对应地址:
+ * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN
+ * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html
+ * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/template-message/sendTemplateMessage.html
+ *
+ */
+ CODE_40037(40037, "template_id 不正确"),
+ /**
+ *
+ * form_id不正确,或者过期.
+ * 对应操作:sendUniformMessage, sendTemplateMessage
+ * 对应地址:
+ * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN
+ * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html
+ * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/template-message/sendTemplateMessage.html
+ *
+ */
+ CODE_41028(41028, "form_id 不正确,或者过期"),
+ /**
+ *
+ * code 或 template_id 不正确.
+ * 对应操作:code2Session, sendUniformMessage, sendTemplateMessage
+ * 对应地址:
+ * GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
+ * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN
+ * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/code2Session.html
+ * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html
+ * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/template-message/sendTemplateMessage.html
+ *
+ */
+ CODE_41029(41029, "请求的参数不正确"),
+ /**
+ *
+ * form_id 已被使用,或者所传page页面不存在,或者小程序没有发布
+ * 对应操作:sendUniformMessage, getWXACodeUnlimit
+ * 对应地址:
+ * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN
+ * POST https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html
+ * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/qr-code/getWXACodeUnlimit.html
+ *
+ */
+ CODE_41030(41030, "请求的参数不正确"),
+ /**
+ *
+ * 调用分钟频率受限.
+ * 对应操作:getWXACodeUnlimit, sendUniformMessage, sendTemplateMessage
+ * 对应地址:
+ * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN
+ * POST https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN
+ * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html
+ * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/qr-code/getWXACodeUnlimit.html
+ *
+ */
+ CODE_45009(45009, "调用分钟频率受限"),
+ /**
+ *
+ * 频率限制,每个用户每分钟100次.
+ * 对应操作:code2Session
+ * 对应地址:
+ * GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/code2Session.html
+ *
+ */
+ CODE_45011(45011, "频率限制,每个用户每分钟100次"),
+ /**
+ *
+ * 回复时间超过限制.
+ * 对应操作:sendCustomerMessage
+ * 对应地址:
+ * POST https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/sendCustomerMessage.html
+ *
+ */
+ CODE_45015(45015, "回复时间超过限制"),
+ /**
+ *
+ * 接口调用超过限额, 或生成码个数总和到达最大个数限制.
+ * 对应操作:createWXAQRCode, sendTemplateMessage
+ * 对应地址:
+ * POST https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token=ACCESS_TOKEN
+ * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/qr-code/getWXACode.html
+ * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/template-message/sendTemplateMessage.html
+ *
+ */
+ CODE_45029(45029, "接口调用超过限额"),
+ /**
+ *
+ * 客服接口下行条数超过上限.
+ * 对应操作:sendCustomerMessage
+ * 对应地址:
+ * POST https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/sendCustomerMessage.html
+ *
+ */
+ CODE_45047(45047, "客服接口下行条数超过上限"),
+ /**
+ *
+ * command字段取值不对
+ * 对应操作:customerTyping
+ * 对应地址:
+ * POST https://api.weixin.qq.com/cgi-bin/message/custom/typing?access_token=ACCESS_TOKEN
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/customerTyping.html
+ *
+ */
+ CODE_45072(45072, "command字段取值不对"),
+ /**
+ *
+ * 下发输入状态,需要之前30秒内跟用户有过消息交互.
+ * 对应操作:customerTyping
+ * 对应地址:
+ * POST https://api.weixin.qq.com/cgi-bin/message/custom/typing?access_token=ACCESS_TOKEN
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/customerTyping.html
+ */
+ CODE_45080(45080, "下发输入状态,需要之前30秒内跟用户有过消息交互"),
+ /**
+ *
+ * 已经在输入状态,不可重复下发.
+ * 对应操作:customerTyping
+ * 对应地址:
+ * POST https://api.weixin.qq.com/cgi-bin/message/custom/typing?access_token=ACCESS_TOKEN
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/customerTyping.html
+ *
+ */
+ CODE_45081(45081, "已经在输入状态,不可重复下发"),
+ /**
+ *
+ * API 功能未授权,请确认小程序已获得该接口.
+ * 对应操作:sendCustomerMessage
+ * 对应地址:
+ * POST https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/sendCustomerMessage.html
+ *
+ */
+ CODE_48001(48001, "API 功能未授权"),
+ /**
+ *
+ * 内容含有违法违规内容.
+ * 对应操作:imgSecCheck, msgSecCheck
+ * 对应地址:
+ * POST https://api.weixin.qq.com/wxa/img_sec_check?access_token=ACCESS_TOKEN
+ * POST https://api.weixin.qq.com/wxa/msg_sec_check?access_token=ACCESS_TOKEN
+ * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/sec-check/imgSecCheck.html
+ * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/sec-check/msgSecCheck.html
+ *
+ */
+ CODE_87014(87014, "内容含有违法违规内容"),
+ /**
+ * 系统繁忙,此时请开发者稍候再试.
+ */
+ CODE_MINUS_1(-1, "系统繁忙,此时请开发者稍候再试"),
+ /**
+ * code 无效.
+ */
+ CODE_40029(40029, "code 无效"),
+ /**
+ * access_token 过期.
+ */
+ CODE_42001(42001, "access_token 过期"),
+ /**
+ * post 数据为空.
+ */
+ CODE_44002(44002, "post 数据为空"),
+ /**
+ * post 数据中参数缺失.
+ */
+ CODE_47001(47001, "post 数据中参数缺失"),
+ /**
+ * 参数 activity_id 错误.
+ */
+ CODE_47501(47501, "参数 activity_id 错误"),
+ /**
+ * 参数 target_state 错误.
+ */
+ CODE_47502(47502, "参数 target_state 错误"),
+ /**
+ * 参数 version_type 错误.
+ */
+ CODE_47503(47503, "参数 version_type 错误"),
+ /**
+ * activity_id 过期.
+ */
+ CODE_47504(47504, "activity_id 过期"),
+ /**
+ * 没有绑定开放平台帐号.
+ */
+ CODE_89002(89002, "没有绑定开放平台帐号"),
+ /**
+ * 订单无效.
+ */
+ CODE_89300(89300, "订单无效"),
+
+ /**
+ * 代小程序实现业务的错误码,部分和小程序业务一致
+ * https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Mini_Programs/Intro.html
+ */
+ CODE_85060(85060, "无效的taskid"),
+
+ CODE_85027(85027, "身份证绑定管理员名额达到上限"),
+
+ CODE_85061(85061, "手机号绑定管理员名额达到上限"),
+
+ CODE_85026(85026, "微信号绑定管理员名额达到上限"),
+
+ CODE_85063(85063, "身份证黑名单"),
+
+ CODE_85062(85062, "手机号黑名单"),
+
+ CODE_85016(85016, "域名数量超过限制"),
+
+ CODE_85017(85017, "没有新增域名,请确认小程序已经添加了域名或该域名是否没有在第三方平台添加"),
+
+ CODE_85018(85018, "域名没有在第三方平台设置"),
+
+ CODE_89019(89019, "业务域名无更改,无需重复设置"),
+
+ CODE_89020(89020, "尚未设置小程序业务域名,请先在第三方平台中设置小程序业务域名后在调用本接口"),
+
+ CODE_89021(89021, "请求保存的域名不是第三方平台中已设置的小程序业务域名或子域名"),
+
+ CODE_89029(89029, "业务域名数量超过限制"),
+
+ CODE_89231(89231, "个人小程序不支持调用 setwebviewdomain 接口"),
+
+ CODE_91001(91001, "不是公众号快速创建的小程序"),
+
+ CODE_91002(91002, "小程序发布后不可改名"),
+
+ CODE_91003(91003, "改名状态不合法"),
+
+ CODE_91004(91004, "昵称不合法"),
+
+ CODE_91005(91005, "昵称 15 天主体保护"),
+
+ CODE_91006(91006, "昵称命中微信号"),
+
+ CODE_91007(91007, "昵称已被占用"),
+
+ CODE_91008(91008, "昵称命中 7 天侵权保护期"),
+
+ CODE_91009(91009, "需要提交材料"),
+
+ CODE_91010(91010, "其他错误"),
+
+ CODE_91011(91011, "查不到昵称修改审核单信息"),
+
+ CODE_91012(91012, "其他错误"),
+
+ CODE_91013(91013, "占用名字过多"),
+
+ CODE_91014(91014, "+号规则 同一类型关联名主体不一致"),
+
+ CODE_91015(91015, "原始名不同类型主体不一致"),
+
+ CODE_91016(91016, "名称占用者 ≥2"),
+
+ CODE_91017(91017, "+号规则 不同类型关联名主体不一致"),
+
+ CODE_40097(40097, "参数错误"),
+
+ CODE_41006(41006, "media_id 不能为空"),
+
+ CODE_46001(46001, "media_id 不存在"),
+
+ CODE_40009(40009, "图片尺寸太大"),
+
+ CODE_53202(53202, "本月头像修改次数已用完"),
+
+ CODE_53200(53200, "本月功能介绍修改次数已用完"),
+
+ CODE_53201(53201, "功能介绍内容命中黑名单关键字"),
+
+ CODE_85083(85083, "搜索标记位被封禁,无法修改"),
+
+ CODE_85084(85084, "非法的 status 值,只能填 0 或者 1"),
+
+ CODE_85013(85013, "无效的自定义配置"),
+
+ CODE_85014(85014, "无效的模版编号"),
+
+ CODE_85043(85043, "模版错误"),
+
+ CODE_85044(85044, "代码包超过大小限制"),
+
+ CODE_85045(85045, "ext_json 有不存在的路径"),
+
+ CODE_85046(85046, "tabBar 中缺少 path"),
+
+ CODE_85047(85047, "pages 字段为空"),
+
+ CODE_85048(85048, "ext_json 解析失败"),
+
+ CODE_80082(80082, "没有权限使用该插件"),
+
+ CODE_80067(80067, "找不到使用的插件"),
+
+ CODE_80066(80066, "非法的插件版本"),
+
+ CODE_86000(86000, "不是由第三方代小程序进行调用"),
+
+ CODE_86001(86001, "不存在第三方的已经提交的代码"),
+
+ CODE_85006(85006, "标签格式错误"),
+
+ CODE_85007(85007, "页面路径错误"),
+
+ CODE_85008(85008, "类目填写错误"),
+
+ CODE_85009(85009, "已经有正在审核的版本"),
+
+ CODE_85010(85010, "item_list 有项目为空"),
+
+ CODE_85011(85011, "标题填写错误"),
+
+ CODE_85023(85023, "审核列表填写的项目数不在 1-5 以内"),
+
+ CODE_85077(85077, "小程序类目信息失效(类目中含有官方下架的类目,请重新选择类目)"),
+
+ CODE_86002(86002, "小程序还未设置昵称、头像、简介。请先设置完后再重新提交"),
+
+ CODE_85085(85085, "近 7 天提交审核的小程序数量过多,请耐心等待审核完毕后再次提交"),
+
+ CODE_85086(85086, "提交代码审核之前需提前上传代码"),
+
+ CODE_85087(85087, "小程序已使用 api navigateToMiniProgram,请声明跳转 appid 列表后再次提交"),
+
+ CODE_85012(85012, "无效的审核 id"),
+
+ CODE_87013(87013, "撤回次数达到上限(每天一次,每个月 10 次)"),
+
+ CODE_85019(85019, "没有审核版本"),
+
+ CODE_85020(85020, "审核状态未满足发布"),
+
+ CODE_87011(87011, "现网已经在灰度发布,不能进行版本回退"),
+
+ CODE_87012(87012, "该版本不能回退,可能的原因:1:无上一个线上版用于回退 2:此版本为已回退版本,不能回退 3:此版本为回退功能上线之前的版本,不能回退"),
+
+ CODE_85079(85079, "小程序没有线上版本,不能进行灰度"),
+
+ CODE_85080(85080, "小程序提交的审核未审核通过"),
+
+ CODE_85081(85081, "无效的发布比例"),
+
+ CODE_85082(85082, "当前的发布比例需要比之前设置的高"),
+
+ CODE_85021(85021, "状态不可变"),
+
+ CODE_85022(85022, "action 非法"),
+
+ CODE_89401(89401, "系统不稳定,请稍后再试,如多次失败请通过社区反馈"),
+
+ CODE_89402(89402, "该审核单不在待审核队列,请检查是否已提交审核或已审完"),
+
+ CODE_89403(89403, "本单属于平台不支持加急种类,请等待正常审核流程"),
+
+ CODE_89404(89404, "本单已加速成功,请勿重复提交"),
+
+ CODE_89405(89405, "本月加急额度不足,请提升提审质量以获取更多额度"),
+
+ CODE_85064(85064, "找不到模版/草稿"),
+
+ CODE_85065(85065, "模版库已满"),
+
+ /**
+ * 小程序订阅消息错误码
+ * https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html
+ */
+ CODE_43101(43101, "用户拒绝接受消息,如果用户之前曾经订阅过,则表示用户取消了订阅关系"),
+
+ CODE_47003(47003, "模板参数不准确,可能为空或者不满足规则,errmsg会提示具体是哪个字段出错"),
+
+ /**
+ * 小程序绑定体验者
+ */
+ CODE_85001(85001, "微信号不存在或微信号设置为不可搜索"),
+
+ CODE_85002(85002, "小程序绑定的体验者数量达到上限"),
+
+ CODE_85003(85003, "微信号绑定的小程序体验者达到上限"),
+
+ CODE_85004(85004, "微信号已经绑定"),
+
+// CODE_504002(-504002, "云函数未找到 Function not found"),
+ ;
+
+ private int code;
+ private String msg;
+
+ WxMaErrorMsgEnum(int code, String msg) {
+ this.code = code;
+ this.msg = msg;
+ }
+
+ /**
+ * 通过错误代码查找其中文含义.
+ */
+ public static String findMsgByCode(int code) {
+ for (WxMaErrorMsgEnum value : WxMaErrorMsgEnum.values()) {
+ if (value.code == code) {
+ return value.msg;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMpErrorMsgEnum.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMpErrorMsgEnum.java
new file mode 100644
index 0000000000..486791986b
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMpErrorMsgEnum.java
@@ -0,0 +1,667 @@
+package me.chanjar.weixin.common.error;
+
+import lombok.Getter;
+
+/**
+ *
+ * 微信公众平台全局返回码.
+ * 参考文档:公众平台全局返回码
+ * Created by Binary Wang on 2018/5/13.
+ *
+ *
+ * @author Binary Wang
+ */
+@Getter
+public enum WxMpErrorMsgEnum {
+ /**
+ * 系统繁忙,此时请开发者稍候再试.
+ */
+ CODE_1(-1, "系统繁忙,此时请开发者稍候再试"),
+ /**
+ * 请求成功.
+ */
+ CODE_0(0, "请求成功"),
+ /**
+ * 获取 access_token 时 AppSecret 错误,或者 access_token 无效。请开发者认真比对 AppSecret 的正确性,或查看是否正在为恰当的公众号调用接口.
+ */
+ CODE_40001(40001, "获取 access_token 时 AppSecret 错误,或者 access_token 无效。请开发者认真比对 AppSecret 的正确性,或查看是否正在为恰当的公众号调用接口"),
+ /**
+ * 不合法的凭证类型.
+ */
+ CODE_40002(40002, "不合法的凭证类型"),
+ /**
+ * 不合法的 OpenID ,请开发者确认 OpenID (该用户)是否已关注公众号,或是否是其他公众号的 OpenID.
+ */
+ CODE_40003(40003, "不合法的 OpenID ,请开发者确认 OpenID (该用户)是否已关注公众号,或是否是其他公众号的 OpenID"),
+ /**
+ * 不合法的媒体文件类型.
+ */
+ CODE_40004(40004, "不合法的媒体文件类型"),
+ /**
+ * 不合法的文件类型.
+ */
+ CODE_40005(40005, "不合法的文件类型"),
+ /**
+ * 不合法的文件大小.
+ */
+ CODE_40006(40006, "不合法的文件大小"),
+ /**
+ * 不合法的媒体文件 id.
+ */
+ CODE_40007(40007, "不合法的媒体文件 id"),
+ /**
+ * 不合法的消息类型.
+ */
+ CODE_40008(40008, "不合法的消息类型"),
+ /**
+ * 不合法的图片文件大小.
+ */
+ CODE_40009(40009, "不合法的图片文件大小"),
+ /**
+ * 不合法的语音文件大小.
+ */
+ CODE_40010(40010, "不合法的语音文件大小"),
+ /**
+ * 不合法的视频文件大小.
+ */
+ CODE_40011(40011, "不合法的视频文件大小"),
+ /**
+ * 不合法的缩略图文件大小.
+ */
+ CODE_40012(40012, "不合法的缩略图文件大小"),
+ /**
+ * 不合法的 AppID ,请开发者检查 AppID 的正确性,避免异常字符,注意大小写.
+ */
+ CODE_40013(40013, "不合法的 AppID ,请开发者检查 AppID 的正确性,避免异常字符,注意大小写"),
+ /**
+ * 不合法的 access_token ,请开发者认真比对 access_token 的有效性(如是否过期),或查看是否正在为恰当的公众号调用接口.
+ */
+ CODE_40014(40014, "不合法的 access_token ,请开发者认真比对 access_token 的有效性(如是否过期),或查看是否正在为恰当的公众号调用接口"),
+ /**
+ * 不合法的菜单类型.
+ */
+ CODE_40015(40015, "不合法的菜单类型"),
+ /**
+ * 不合法的按钮个数.
+ */
+ CODE_40016(40016, "不合法的按钮个数"),
+ /**
+ * 不合法的按钮类型.
+ */
+ CODE_40017(40017, "不合法的按钮类型"),
+ /**
+ * 不合法的按钮名字长度.
+ */
+ CODE_40018(40018, "不合法的按钮名字长度"),
+ /**
+ * 不合法的按钮 KEY 长度.
+ */
+ CODE_40019(40019, "不合法的按钮 KEY 长度"),
+ /**
+ * 不合法的按钮 URL 长度.
+ */
+ CODE_40020(40020, "不合法的按钮 URL 长度"),
+ /**
+ * 不合法的菜单版本号.
+ */
+ CODE_40021(40021, "不合法的菜单版本号"),
+ /**
+ * 不合法的子菜单级数.
+ */
+ CODE_40022(40022, "不合法的子菜单级数"),
+ /**
+ * 不合法的子菜单按钮个数.
+ */
+ CODE_40023(40023, "不合法的子菜单按钮个数"),
+ /**
+ * 不合法的子菜单按钮类型.
+ */
+ CODE_40024(40024, "不合法的子菜单按钮类型"),
+ /**
+ * 不合法的子菜单按钮名字长度.
+ */
+ CODE_40025(40025, "不合法的子菜单按钮名字长度"),
+ /**
+ * 不合法的子菜单按钮 KEY 长度.
+ */
+ CODE_40026(40026, "不合法的子菜单按钮 KEY 长度"),
+ /**
+ * 不合法的子菜单按钮 URL 长度.
+ */
+ CODE_40027(40027, "不合法的子菜单按钮 URL 长度"),
+ /**
+ * 不合法的自定义菜单使用用户.
+ */
+ CODE_40028(40028, "不合法的自定义菜单使用用户"),
+ /**
+ * 不合法的 oauth_code.
+ */
+ CODE_40029(40029, "不合法的 oauth_code"),
+ /**
+ * 不合法的 refresh_token.
+ */
+ CODE_40030(40030, "不合法的 refresh_token"),
+ /**
+ * 不合法的 openid 列表.
+ */
+ CODE_40031(40031, "不合法的 openid 列表"),
+ /**
+ * 不合法的 openid 列表长度.
+ */
+ CODE_40032(40032, "不合法的 openid 列表长度"),
+ /**
+ * 不合法的请求字符,不能包含\\uxxxx 格式的字符.
+ */
+ CODE_40033(40033, "不合法的请求字符,不能包含\\uxxxx 格式的字符"),
+ /**
+ * 不合法的参数.
+ */
+ CODE_40035(40035, "不合法的参数"),
+ /**
+ * 不合法的请求格式.
+ */
+ CODE_40038(40038, "不合法的请求格式"),
+ /**
+ * 不合法的 URL 长度.
+ */
+ CODE_40039(40039, "不合法的 URL 长度"),
+ /**
+ * 不合法的分组 id.
+ */
+ CODE_40050(40050, "不合法的分组 id"),
+ /**
+ * 分组名字不合法.
+ */
+ CODE_40051(40051, "分组名字不合法"),
+ /**
+ * 删除单篇图文时,指定的 article_idx 不合法.
+ */
+ CODE_40060(40060, "删除单篇图文时,指定的 article_idx 不合法"),
+ /**
+ * 分组名字不合法.
+ */
+ CODE_40117(40117, "分组名字不合法"),
+ /**
+ * media_id 大小不合法.
+ */
+ CODE_40118(40118, "media_id 大小不合法"),
+ /**
+ * button 类型错误.
+ */
+ CODE_40119(40119, "button 类型错误"),
+ /**
+ * button 类型错误.
+ */
+ CODE_40120(40120, "button 类型错误"),
+ /**
+ * 不合法的 media_id 类型.
+ */
+ CODE_40121(40121, "不合法的 media_id 类型"),
+ /**
+ * 微信号不合法.
+ */
+ CODE_40132(40132, "微信号不合法"),
+ /**
+ * 不支持的图片格式.
+ */
+ CODE_40137(40137, "不支持的图片格式"),
+ /**
+ * 请勿添加其他公众号的主页链接.
+ */
+ CODE_40155(40155, "请勿添加其他公众号的主页链接"),
+ /**
+ * 缺少 access_token 参数.
+ */
+ CODE_41001(41001, "缺少 access_token 参数"),
+ /**
+ * 缺少 appid 参数.
+ */
+ CODE_41002(41002, "缺少 appid 参数"),
+ /**
+ * 缺少 refresh_token 参数.
+ */
+ CODE_41003(41003, "缺少 refresh_token 参数"),
+ /**
+ * 缺少 secret 参数.
+ */
+ CODE_41004(41004, "缺少 secret 参数"),
+ /**
+ * 缺少多媒体文件数据.
+ */
+ CODE_41005(41005, "缺少多媒体文件数据"),
+ /**
+ * 缺少 media_id 参数.
+ */
+ CODE_41006(41006, "缺少 media_id 参数"),
+ /**
+ * 缺少子菜单数据.
+ */
+ CODE_41007(41007, "缺少子菜单数据"),
+ /**
+ * 缺少 oauth code.
+ */
+ CODE_41008(41008, "缺少 oauth code"),
+ /**
+ * 缺少 openid.
+ */
+ CODE_41009(41009, "缺少 openid"),
+ /**
+ * access_token 超时,请检查 access_token 的有效期,请参考基础支持 - 获取 access_token 中,对 access_token 的详细机制说明.
+ */
+ CODE_42001(42001, "access_token 超时,请检查 access_token 的有效期,请参考基础支持 - 获取 access_token 中,对 access_token 的详细机制说明"),
+ /**
+ * refresh_token 超时.
+ */
+ CODE_42002(42002, "refresh_token 超时"),
+ /**
+ * oauth_code 超时.
+ */
+ CODE_42003(42003, "oauth_code 超时"),
+ /**
+ * 用户修改微信密码, accesstoken 和 refreshtoken 失效,需要重新授权.
+ */
+ CODE_42007(42007, "用户修改微信密码, accesstoken 和 refreshtoken 失效,需要重新授权"),
+ /**
+ * 需要 GET 请求.
+ */
+ CODE_43001(43001, "需要 GET 请求"),
+ /**
+ * 需要 POST 请求.
+ */
+ CODE_43002(43002, "需要 POST 请求"),
+ /**
+ * 需要 HTTPS 请求.
+ */
+ CODE_43003(43003, "需要 HTTPS 请求"),
+ /**
+ * 需要接收者关注.
+ */
+ CODE_43004(43004, "需要接收者关注"),
+ /**
+ * 需要好友关系.
+ */
+ CODE_43005(43005, "需要好友关系"),
+ /**
+ * 需要将接收者从黑名单中移除.
+ */
+ CODE_43019(43019, "需要将接收者从黑名单中移除"),
+ /**
+ * 多媒体文件为空.
+ */
+ CODE_44001(44001, "多媒体文件为空"),
+ /**
+ * POST 的数据包为空.
+ */
+ CODE_44002(44002, "POST 的数据包为空"),
+ /**
+ * 图文消息内容为空.
+ */
+ CODE_44003(44003, "图文消息内容为空"),
+ /**
+ * 文本消息内容为空.
+ */
+ CODE_44004(44004, "文本消息内容为空"),
+ /**
+ * 多媒体文件大小超过限制.
+ */
+ CODE_45001(45001, "多媒体文件大小超过限制"),
+ /**
+ * 消息内容超过限制.
+ */
+ CODE_45002(45002, "消息内容超过限制"),
+ /**
+ * 标题字段超过限制.
+ */
+ CODE_45003(45003, "标题字段超过限制"),
+ /**
+ * 描述字段超过限制.
+ */
+ CODE_45004(45004, "描述字段超过限制"),
+ /**
+ * 链接字段超过限制.
+ */
+ CODE_45005(45005, "链接字段超过限制"),
+ /**
+ * 图片链接字段超过限制.
+ */
+ CODE_45006(45006, "图片链接字段超过限制"),
+ /**
+ * 语音播放时间超过限制.
+ */
+ CODE_45007(45007, "语音播放时间超过限制"),
+ /**
+ * 图文消息超过限制.
+ */
+ CODE_45008(45008, "图文消息超过限制"),
+ /**
+ * 接口调用超过限制.
+ */
+ CODE_45009(45009, "接口调用超过限制"),
+ /**
+ * 创建菜单个数超过限制.
+ */
+ CODE_45010(45010, "创建菜单个数超过限制"),
+ /**
+ * API 调用太频繁,请稍候再试.
+ */
+ CODE_45011(45011, "API 调用太频繁,请稍候再试"),
+ /**
+ * 回复时间超过限制.
+ */
+ CODE_45015(45015, "回复时间超过限制"),
+ /**
+ * 系统分组,不允许修改.
+ */
+ CODE_45016(45016, "系统分组,不允许修改"),
+ /**
+ * 分组名字过长.
+ */
+ CODE_45017(45017, "分组名字过长"),
+ /**
+ * 分组数量超过上限.
+ */
+ CODE_45018(45018, "分组数量超过上限"),
+ /**
+ * 客服接口下行条数超过上限.
+ */
+ CODE_45047(45047, "客服接口下行条数超过上限"),
+ /**
+ * 非法的tag_id.
+ */
+ CODE_45159(45159, "非法的tag_id"),
+ /**
+ * 不存在媒体数据.
+ */
+ CODE_46001(46001, "不存在媒体数据"),
+ /**
+ * 不存在的菜单版本.
+ */
+ CODE_46002(46002, "不存在的菜单版本"),
+ /**
+ * 不存在的菜单数据.
+ */
+ CODE_46003(46003, "不存在的菜单数据"),
+ /**
+ * 不存在的用户.
+ */
+ CODE_46004(46004, "不存在的用户"),
+ /**
+ * 解析 JSON/XML 内容错误.
+ */
+ CODE_47001(47001, "解析 JSON/XML 内容错误"),
+ /**
+ * api 功能未授权,请确认公众号已获得该接口,可以在公众平台官网 - 开发者中心页中查看接口权限.
+ */
+ CODE_48001(48001, "api 功能未授权,请确认公众号已获得该接口,可以在公众平台官网 - 开发者中心页中查看接口权限"),
+ /**
+ * 粉丝拒收消息(粉丝在公众号选项中,关闭了 “ 接收消息 ” ).
+ */
+ CODE_48002(48002, "粉丝拒收消息(粉丝在公众号选项中,关闭了 “ 接收消息 ” )"),
+ /**
+ * api 接口被封禁,请登录 mp.weixin.qq.com 查看详情.
+ */
+ CODE_48004(48004, "api 接口被封禁,请登录 mp.weixin.qq.com 查看详情"),
+ /**
+ * api 禁止删除被自动回复和自定义菜单引用的素材.
+ */
+ CODE_48005(48005, "api 禁止删除被自动回复和自定义菜单引用的素材"),
+ /**
+ * api 禁止清零调用次数,因为清零次数达到上限.
+ */
+ CODE_48006(48006, "api 禁止清零调用次数,因为清零次数达到上限"),
+ /**
+ * 没有该类型消息的发送权限.
+ */
+ CODE_48008(48008, "没有该类型消息的发送权限"),
+ /**
+ * 用户未授权该 api.
+ */
+ CODE_50001(50001, "用户未授权该 api"),
+ /**
+ * 用户受限,可能是违规后接口被封禁.
+ */
+ CODE_50002(50002, "用户受限,可能是违规后接口被封禁"),
+ /**
+ * 用户未关注公众号.
+ */
+ CODE_50005(50005, "用户未关注公众号"),
+ /**
+ * 参数错误 (invalid parameter).
+ */
+ CODE_61451(61451, "参数错误 (invalid parameter)"),
+ /**
+ * 无效客服账号 (invalid kf_account).
+ */
+ CODE_61452(61452, "无效客服账号 (invalid kf_account)"),
+ /**
+ * 客服帐号已存在 (kf_account exsited).
+ */
+ CODE_61453(61453, "客服帐号已存在 (kf_account exsited)"),
+ /**
+ * 客服帐号名长度超过限制 ( 仅允许 10 个英文字符,不包括 @ 及 @ 后的公众号的微信号 )(invalid kf_acount length).
+ */
+ CODE_61454(61454, "客服帐号名长度超过限制 ( 仅允许 10 个英文字符,不包括 @ 及 @ 后的公众号的微信号 )(invalid kf_acount length)"),
+ /**
+ * 客服帐号名包含非法字符 ( 仅允许英文 + 数字 )(illegal character in kf_account).
+ */
+ CODE_61455(61455, "客服帐号名包含非法字符 ( 仅允许英文 + 数字 )(illegal character in kf_account)"),
+ /**
+ * 客服帐号个数超过限制 (10 个客服账号 )(kf_account count exceeded).
+ */
+ CODE_61456(61456, "客服帐号个数超过限制 (10 个客服账号 )(kf_account count exceeded)"),
+ /**
+ * 无效头像文件类型 (invalid file type).
+ */
+ CODE_61457(61457, "无效头像文件类型 (invalid file type)"),
+ /**
+ * 系统错误 (system error).
+ */
+ CODE_61450(61450, "系统错误 (system error)"),
+ /**
+ * 日期格式错误.
+ */
+ CODE_61500(61500, "日期格式错误"),
+ /**
+ * 不存在此 menuid 对应的个性化菜单.
+ */
+ CODE_65301(65301, "不存在此 menuid 对应的个性化菜单"),
+ /**
+ * 没有相应的用户.
+ */
+ CODE_65302(65302, "没有相应的用户"),
+ /**
+ * 没有默认菜单,不能创建个性化菜单.
+ */
+ CODE_65303(65303, "没有默认菜单,不能创建个性化菜单"),
+ /**
+ * MatchRule 信息为空.
+ */
+ CODE_65304(65304, "MatchRule 信息为空"),
+ /**
+ * 个性化菜单数量受限.
+ */
+ CODE_65305(65305, "个性化菜单数量受限"),
+ /**
+ * 不支持个性化菜单的帐号.
+ */
+ CODE_65306(65306, "不支持个性化菜单的帐号"),
+ /**
+ * 个性化菜单信息为空.
+ */
+ CODE_65307(65307, "个性化菜单信息为空"),
+ /**
+ * 包含没有响应类型的 button.
+ */
+ CODE_65308(65308, "包含没有响应类型的 button"),
+ /**
+ * 个性化菜单开关处于关闭状态.
+ */
+ CODE_65309(65309, "个性化菜单开关处于关闭状态"),
+ /**
+ * 填写了省份或城市信息,国家信息不能为空.
+ */
+ CODE_65310(65310, "填写了省份或城市信息,国家信息不能为空"),
+ /**
+ * 填写了城市信息,省份信息不能为空.
+ */
+ CODE_65311(65311, "填写了城市信息,省份信息不能为空"),
+ /**
+ * 不合法的国家信息.
+ */
+ CODE_65312(65312, "不合法的国家信息"),
+ /**
+ * 不合法的省份信息.
+ */
+ CODE_65313(65313, "不合法的省份信息"),
+ /**
+ * 不合法的城市信息.
+ */
+ CODE_65314(65314, "不合法的城市信息"),
+ /**
+ * 该公众号的菜单设置了过多的域名外跳(最多跳转到 3 个域名的链接).
+ */
+ CODE_65316(65316, "该公众号的菜单设置了过多的域名外跳(最多跳转到 3 个域名的链接)"),
+ /**
+ * 不合法的 URL.
+ */
+ CODE_65317(65317, "不合法的 URL"),
+ /**
+ * POST 数据参数不合法.
+ */
+ CODE_9001001(9001001, "POST 数据参数不合法"),
+ /**
+ * 远端服务不可用.
+ */
+ CODE_9001002(9001002, "远端服务不可用"),
+ /**
+ * Ticket 不合法.
+ */
+ CODE_9001003(9001003, "Ticket 不合法"),
+ /**
+ * 获取摇周边用户信息失败.
+ */
+ CODE_9001004(9001004, "获取摇周边用户信息失败"),
+ /**
+ * 获取商户信息失败.
+ */
+ CODE_9001005(9001005, "获取商户信息失败"),
+ /**
+ * 获取 OpenID 失败.
+ */
+ CODE_9001006(9001006, "获取 OpenID 失败"),
+ /**
+ * 上传文件缺失.
+ */
+ CODE_9001007(9001007, "上传文件缺失"),
+ /**
+ * 上传素材的文件类型不合法.
+ */
+ CODE_9001008(9001008, "上传素材的文件类型不合法"),
+ /**
+ * 上传素材的文件尺寸不合法.
+ */
+ CODE_9001009(9001009, "上传素材的文件尺寸不合法"),
+ /**
+ * 上传失败.
+ */
+ CODE_9001010(9001010, "上传失败"),
+ /**
+ * 帐号不合法.
+ */
+ CODE_9001020(9001020, "帐号不合法"),
+ /**
+ * 已有设备激活率低于 50% ,不能新增设备.
+ */
+ CODE_9001021(9001021, "已有设备激活率低于 50% ,不能新增设备"),
+ /**
+ * 设备申请数不合法,必须为大于 0 的数字.
+ */
+ CODE_9001022(9001022, "设备申请数不合法,必须为大于 0 的数字"),
+ /**
+ * 已存在审核中的设备 ID 申请.
+ */
+ CODE_9001023(9001023, "已存在审核中的设备 ID 申请"),
+ /**
+ * 一次查询设备 ID 数量不能超过 50.
+ */
+ CODE_9001024(9001024, "一次查询设备 ID 数量不能超过 50"),
+ /**
+ * 设备 ID 不合法.
+ */
+ CODE_9001025(9001025, "设备 ID 不合法"),
+ /**
+ * 页面 ID 不合法.
+ */
+ CODE_9001026(9001026, "页面 ID 不合法"),
+ /**
+ * 页面参数不合法.
+ */
+ CODE_9001027(9001027, "页面参数不合法"),
+ /**
+ * 一次删除页面 ID 数量不能超过 10.
+ */
+ CODE_9001028(9001028, "一次删除页面 ID 数量不能超过 10"),
+ /**
+ * 页面已应用在设备中,请先解除应用关系再删除.
+ */
+ CODE_9001029(9001029, "页面已应用在设备中,请先解除应用关系再删除"),
+ /**
+ * 一次查询页面 ID 数量不能超过 50.
+ */
+ CODE_9001030(9001030, "一次查询页面 ID 数量不能超过 50"),
+ /**
+ * 时间区间不合法.
+ */
+ CODE_9001031(9001031, "时间区间不合法"),
+ /**
+ * 保存设备与页面的绑定关系参数错误.
+ */
+ CODE_9001032(9001032, "保存设备与页面的绑定关系参数错误"),
+ /**
+ * 门店 ID 不合法.
+ */
+ CODE_9001033(9001033, "门店 ID 不合法"),
+ /**
+ * 设备备注信息过长.
+ */
+ CODE_9001034(9001034, "设备备注信息过长"),
+ /**
+ * 设备申请参数不合法.
+ */
+ CODE_9001035(9001035, "设备申请参数不合法"),
+ /**
+ * 查询起始值 begin 不合法.
+ */
+ CODE_9001036(9001036, "查询起始值 begin 不合法"),
+
+ /**
+ * 设置的 speed 参数不在0到4的范围内
+ */
+ CODE_45083(45083, "设置的 speed 参数不在0到4的范围内"),
+
+ /**
+ * 没有设置 speed 参数
+ */
+ CODE_45084(45084, "没有设置 speed 参数");
+
+ private int code;
+ private String msg;
+
+ WxMpErrorMsgEnum(int code, String msg) {
+ this.code = code;
+ this.msg = msg;
+ }
+
+ /**
+ * 通过错误代码查找其中文含义..
+ */
+ public static String findMsgByCode(int code) {
+ for (WxMpErrorMsgEnum value : WxMpErrorMsgEnum.values()) {
+ if (value.code == code) {
+ return value.msg;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/BaseWxRedisOps.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/BaseWxRedisOps.java
new file mode 100644
index 0000000000..17e992ab25
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/BaseWxRedisOps.java
@@ -0,0 +1,37 @@
+package me.chanjar.weixin.common.redis;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * 微信redis操作基本类
+ *
+ * 非内置实现redis相关操作, 请实现该类
+ */
+public abstract class BaseWxRedisOps implements WxRedisOps {
+
+ @Override
+ public String getValue(String key) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setValue(String key, String value, int expire, TimeUnit timeUnit) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Long getExpire(String key) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void expire(String key, int expire, TimeUnit timeUnit) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Lock getLock(String key) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/JedisWxRedisOps.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/JedisWxRedisOps.java
new file mode 100644
index 0000000000..2b9849ff92
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/JedisWxRedisOps.java
@@ -0,0 +1,51 @@
+package me.chanjar.weixin.common.redis;
+
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.util.locks.JedisDistributedLock;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.util.Pool;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+
+@RequiredArgsConstructor
+public class JedisWxRedisOps implements WxRedisOps {
+ private final Pool jedisPool;
+
+ @Override
+ public String getValue(String key) {
+ try (Jedis jedis = this.jedisPool.getResource()) {
+ return jedis.get(key);
+ }
+ }
+
+ @Override
+ public void setValue(String key, String value, int expire, TimeUnit timeUnit) {
+ try (Jedis jedis = this.jedisPool.getResource()) {
+ if (expire <= 0) {
+ jedis.set(key, value);
+ } else {
+ jedis.psetex(key, timeUnit.toMillis(expire), value);
+ }
+ }
+ }
+
+ @Override
+ public Long getExpire(String key) {
+ try (Jedis jedis = this.jedisPool.getResource()) {
+ return jedis.ttl(key);
+ }
+ }
+
+ @Override
+ public void expire(String key, int expire, TimeUnit timeUnit) {
+ try (Jedis jedis = this.jedisPool.getResource()) {
+ jedis.pexpire(key, timeUnit.toMillis(expire));
+ }
+ }
+
+ @Override
+ public Lock getLock(String key) {
+ return new JedisDistributedLock(jedisPool, key);
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOps.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOps.java
new file mode 100644
index 0000000000..19d4046c92
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOps.java
@@ -0,0 +1,44 @@
+package me.chanjar.weixin.common.redis;
+
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.util.locks.RedisTemplateSimpleDistributedLock;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+
+@RequiredArgsConstructor
+public class RedisTemplateWxRedisOps implements WxRedisOps {
+
+ private final StringRedisTemplate redisTemplate;
+
+ @Override
+ public String getValue(String key) {
+ return redisTemplate.opsForValue().get(key);
+ }
+
+ @Override
+ public void setValue(String key, String value, int expire, TimeUnit timeUnit) {
+ if (expire <= 0) {
+ redisTemplate.opsForValue().set(key, value);
+ } else {
+ redisTemplate.opsForValue().set(key, value, expire, timeUnit);
+ }
+ }
+
+ @Override
+ public Long getExpire(String key) {
+ return redisTemplate.getExpire(key);
+ }
+
+ @Override
+ public void expire(String key, int expire, TimeUnit timeUnit) {
+ redisTemplate.expire(key, expire, timeUnit);
+ }
+
+ @Override
+ public Lock getLock(@NonNull String key) {
+ return new RedisTemplateSimpleDistributedLock(redisTemplate, key, 60 * 1000);
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedissonWxRedisOps.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedissonWxRedisOps.java
new file mode 100644
index 0000000000..d51cd3e1ad
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedissonWxRedisOps.java
@@ -0,0 +1,47 @@
+package me.chanjar.weixin.common.redis;
+
+import lombok.RequiredArgsConstructor;
+import org.redisson.api.RedissonClient;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+
+@RequiredArgsConstructor
+public class RedissonWxRedisOps implements WxRedisOps {
+
+ private final RedissonClient redissonClient;
+
+ @Override
+ public String getValue(String key) {
+ Object value = redissonClient.getBucket(key).get();
+ return value == null ? null : value.toString();
+ }
+
+ @Override
+ public void setValue(String key, String value, int expire, TimeUnit timeUnit) {
+ if (expire <= 0) {
+ redissonClient.getBucket(key).set(value);
+ } else {
+ redissonClient.getBucket(key).set(value, expire, timeUnit);
+ }
+ }
+
+ @Override
+ public Long getExpire(String key) {
+ long expire = redissonClient.getBucket(key).remainTimeToLive();
+ if (expire > 0) {
+ expire = expire / 1000;
+ }
+ return expire;
+ }
+
+ @Override
+ public void expire(String key, int expire, TimeUnit timeUnit) {
+ redissonClient.getBucket(key).expire(expire, timeUnit);
+ }
+
+ @Override
+ public Lock getLock(String key) {
+ return redissonClient.getLock(key);
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/WxRedisOps.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/WxRedisOps.java
new file mode 100644
index 0000000000..5489165e74
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/WxRedisOps.java
@@ -0,0 +1,27 @@
+package me.chanjar.weixin.common.redis;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * 微信Redis相关操作
+ *
+ * 该接口不承诺稳定, 外部实现请继承{@link BaseWxRedisOps}
+ *
+ * @see BaseWxRedisOps 实现需要继承该类
+ * @see JedisWxRedisOps jedis实现
+ * @see RedissonWxRedisOps redisson实现
+ * @see RedisTemplateWxRedisOps redisTemplate实现
+ */
+public interface WxRedisOps {
+
+ String getValue(String key);
+
+ void setValue(String key, String value, int expire, TimeUnit timeUnit);
+
+ Long getExpire(String key);
+
+ void expire(String key, int expire, TimeUnit timeUnit);
+
+ Lock getLock(String key);
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/requestexecuter/ocr/OcrDiscernApacheHttpRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/requestexecuter/ocr/OcrDiscernApacheHttpRequestExecutor.java
new file mode 100644
index 0000000000..2a84ac0e8b
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/requestexecuter/ocr/OcrDiscernApacheHttpRequestExecutor.java
@@ -0,0 +1,57 @@
+package me.chanjar.weixin.common.requestexecuter.ocr;
+
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+import me.chanjar.weixin.common.util.http.apache.Utf8ResponseHandler;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHost;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.mime.HttpMultipartMode;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * .
+ *
+ * @author : zhayueran
+ * @date 2019/6/27 14:06
+ */
+public class OcrDiscernApacheHttpRequestExecutor extends OcrDiscernRequestExecutor {
+ public OcrDiscernApacheHttpRequestExecutor(RequestHttp requestHttp) {
+ super(requestHttp);
+ }
+
+ @Override
+ public String execute(String uri, File file, WxType wxType) throws WxErrorException, IOException {
+ HttpPost httpPost = new HttpPost(uri);
+ if (requestHttp.getRequestHttpProxy() != null) {
+ RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build();
+ httpPost.setConfig(config);
+ }
+ if (file != null) {
+ HttpEntity entity = MultipartEntityBuilder
+ .create()
+ .addBinaryBody("file", file)
+ .setMode(HttpMultipartMode.RFC6532)
+ .build();
+ httpPost.setEntity(entity);
+ }
+ try (CloseableHttpResponse response = requestHttp.getRequestHttpClient().execute(httpPost)) {
+ String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
+ WxError error = WxError.fromJson(responseContent, wxType);
+ if (error.getErrorCode() != 0) {
+ throw new WxErrorException(error);
+ }
+ return responseContent;
+ } finally {
+ httpPost.releaseConnection();
+ }
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/requestexecuter/ocr/OcrDiscernRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/requestexecuter/ocr/OcrDiscernRequestExecutor.java
new file mode 100644
index 0000000000..38926e72e5
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/requestexecuter/ocr/OcrDiscernRequestExecutor.java
@@ -0,0 +1,38 @@
+package me.chanjar.weixin.common.requestexecuter.ocr;
+
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.RequestExecutor;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+import me.chanjar.weixin.common.util.http.ResponseHandler;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * .
+ *
+ * @author zhayueran
+ * @date 2019/6/27 15:06
+ */
+public abstract class OcrDiscernRequestExecutor implements RequestExecutor {
+ protected RequestHttp requestHttp;
+
+ public OcrDiscernRequestExecutor(RequestHttp requestHttp) {
+ this.requestHttp = requestHttp;
+ }
+
+ @Override
+ public void execute(String uri, File data, ResponseHandler handler, WxType wxType) throws WxErrorException, IOException {
+ handler.handle(this.execute(uri, data, wxType));
+ }
+
+ public static RequestExecutor create(RequestHttp requestHttp) {
+ switch (requestHttp.getRequestType()) {
+ case APACHE_HTTP:
+ return new OcrDiscernApacheHttpRequestExecutor(requestHttp);
+ default:
+ return null;
+ }
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxService.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxService.java
new file mode 100644
index 0000000000..fc49bfd9ce
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxService.java
@@ -0,0 +1,41 @@
+package me.chanjar.weixin.common.service;
+
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 微信服务接口.
+ *
+ * @author Binary Wang
+ * @date 2020-04-25
+ */
+public interface WxService {
+ /**
+ * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的GET请求.
+ *
+ * @param queryParam 参数
+ * @param url 请求接口地址
+ * @return 接口响应字符串
+ * @throws WxErrorException 异常
+ */
+ String get(String url, String queryParam) throws WxErrorException;
+
+ /**
+ * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求.
+ *
+ * @param postData 请求参数json值
+ * @param url 请求接口地址
+ * @return 接口响应字符串
+ * @throws WxErrorException 异常
+ */
+ String post(String url, String postData) throws WxErrorException;
+
+ /**
+ * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求.
+ *
+ * @param url 请求接口地址
+ * @param obj 请求对象
+ * @return 接口响应字符串
+ * @throws WxErrorException 异常
+ */
+ String post(String url, Object obj) throws WxErrorException;
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/Constants.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/Constants.java
index f30a0ae0d0..98d45e31d4 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/Constants.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/Constants.java
@@ -5,9 +5,9 @@
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -23,9 +23,6 @@
*
* @author Craig R. McClanahan
*/
-
public class Constants {
-
- public static final String Package = "me.chanjar.weixin.common.session";
-
+ public static final String PACKAGE = "me.chanjar.weixin.common.session";
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/InternalSession.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/InternalSession.java
index 77d4d28291..05cb41363f 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/InternalSession.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/InternalSession.java
@@ -1,5 +1,9 @@
package me.chanjar.weixin.common.session;
+/**
+ *
+ * @author Daniel Qian
+ */
public interface InternalSession {
/**
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/InternalSessionManager.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/InternalSessionManager.java
index a92e107154..e3d9ab8351 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/InternalSessionManager.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/InternalSessionManager.java
@@ -1,5 +1,8 @@
package me.chanjar.weixin.common.session;
+/**
+ * @author Daniel Qian
+ */
public interface InternalSessionManager {
/**
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/StandardSession.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/StandardSession.java
index f9d61707e3..3c4ec20c8d 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/StandardSession.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/StandardSession.java
@@ -1,23 +1,29 @@
package me.chanjar.weixin.common.session;
-import me.chanjar.weixin.common.util.res.StringManager;
-
-import java.util.*;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
+import me.chanjar.weixin.common.util.res.StringManager;
+
+/**
+ * @author Daniel Qian
+ */
public class StandardSession implements WxSession, InternalSession {
/**
* The string manager for this package.
*/
- protected static final StringManager sm = StringManager.getManager(Constants.Package);
+ protected static final StringManager SM = StringManager.getManager(Constants.PACKAGE);
/**
* Type array.
*/
private static final String[] EMPTY_ARRAY = new String[0];
- // ------------------------------ WxSession
protected Map attributes = new ConcurrentHashMap<>();
/**
* The session identifier of this Session.
@@ -73,7 +79,7 @@ public Object getAttribute(String name) {
if (!isValidInternal()) {
throw new IllegalStateException
- (sm.getString("sessionImpl.getAttribute.ise"));
+ (SM.getString("sessionImpl.getAttribute.ise"));
}
if (name == null) {
@@ -86,7 +92,7 @@ public Object getAttribute(String name) {
@Override
public Enumeration getAttributeNames() {
if (!isValidInternal()) {
- throw new IllegalStateException(sm.getString("sessionImpl.getAttributeNames.ise"));
+ throw new IllegalStateException(SM.getString("sessionImpl.getAttributeNames.ise"));
}
Set names = new HashSet<>();
@@ -98,7 +104,7 @@ public Enumeration getAttributeNames() {
public void setAttribute(String name, Object value) {
// Name cannot be null
if (name == null) {
- throw new IllegalArgumentException(sm.getString("sessionImpl.setAttribute.namenull"));
+ throw new IllegalArgumentException(SM.getString("sessionImpl.setAttribute.namenull"));
}
// Null value is the same as removeAttribute()
@@ -109,7 +115,7 @@ public void setAttribute(String name, Object value) {
// Validate our current state
if (!isValidInternal()) {
- throw new IllegalStateException(sm.getString("sessionImpl.setAttribute.ise", getIdInternal()));
+ throw new IllegalStateException(SM.getString("sessionImpl.setAttribute.ise", getIdInternal()));
}
this.attributes.put(name, value);
@@ -123,8 +129,9 @@ public void removeAttribute(String name) {
@Override
public void invalidate() {
- if (!isValidInternal())
- throw new IllegalStateException(sm.getString("sessionImpl.invalidate.ise"));
+ if (!isValidInternal()) {
+ throw new IllegalStateException(SM.getString("sessionImpl.invalidate.ise"));
+ }
// Cause this session to expire
expire();
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/StandardSessionFacade.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/StandardSessionFacade.java
index e449308961..aa9f877136 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/StandardSessionFacade.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/StandardSessionFacade.java
@@ -2,6 +2,9 @@
import java.util.Enumeration;
+/**
+ * @author Daniel Qian
+ */
public class StandardSessionFacade implements WxSession {
/**
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/StandardSessionManager.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/StandardSessionManager.java
index 7e6c95a28f..591b7025dd 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/StandardSessionManager.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/StandardSessionManager.java
@@ -9,12 +9,12 @@
import java.util.concurrent.atomic.AtomicBoolean;
/**
- * 基于内存的session manager
+ * 基于内存的session manager.
+ *
+ * @author Daniel Qian
*/
public class StandardSessionManager implements WxSessionManager, InternalSessionManager {
-
- protected static final StringManager sm =
- StringManager.getManager(Constants.Package);
+ protected static final StringManager SM = StringManager.getManager(Constants.PACKAGE);
/**
* The descriptive name of this Manager implementation (for logging).
*/
@@ -49,7 +49,9 @@ public class StandardSessionManager implements WxSessionManager, InternalSession
*/
protected int maxInactiveInterval = 30 * 60;
- // Number of sessions created by this manager
+ /**
+ * Number of sessions created by this manager
+ */
protected long sessionCounter = 0;
protected volatile int maxActive = 0;
@@ -82,7 +84,7 @@ public WxSession getSession(String sessionId) {
public WxSession getSession(String sessionId, boolean create) {
if (sessionId == null) {
throw new IllegalStateException
- (sm.getString("sessionManagerImpl.getSession.ise"));
+ (SM.getString("sessionManagerImpl.getSession.ise"));
}
InternalSession session = findSession(sessionId);
@@ -124,26 +126,25 @@ public void remove(InternalSession session, boolean update) {
@Override
public InternalSession findSession(String id) {
-
- if (id == null)
+ if (id == null) {
return (null);
+ }
return this.sessions.get(id);
-
}
@Override
public InternalSession createSession(String sessionId) {
if (sessionId == null) {
throw new IllegalStateException
- (sm.getString("sessionManagerImpl.createSession.ise"));
+ (SM.getString("sessionManagerImpl.createSession.ise"));
}
if ((this.maxActiveSessions >= 0) &&
- (getActiveSessions() >= this.maxActiveSessions)) {
+ (getActiveSessions() >= this.maxActiveSessions)) {
this.rejectedSessions++;
throw new TooManyActiveSessionsException(
- sm.getString("sessionManagerImpl.createSession.tmase"),
- this.maxActiveSessions);
+ SM.getString("sessionManagerImpl.createSession.tmase"),
+ this.maxActiveSessions);
}
// Recycle or create a Session instance
@@ -153,12 +154,10 @@ public InternalSession createSession(String sessionId) {
session.setValid(true);
session.setCreationTime(System.currentTimeMillis());
session.setMaxInactiveInterval(this.maxInactiveInterval);
- String id = sessionId;
- session.setId(id);
+ session.setId(sessionId);
this.sessionCounter++;
- return (session);
-
+ return session;
}
@@ -180,10 +179,8 @@ protected InternalSession getNewSession() {
return new StandardSession(this);
}
-
@Override
public void add(InternalSession session) {
-
// 当第一次有session创建的时候,开启session清理线程
if (!this.backgroundProcessStarted.getAndSet(true)) {
Thread t = new Thread(new Runnable() {
@@ -192,9 +189,10 @@ public void run() {
while (true) {
try {
// 每秒清理一次
- Thread.sleep(StandardSessionManager.this.backgroundProcessorDelay * 1000l);
+ Thread.sleep(StandardSessionManager.this.backgroundProcessorDelay * 1000L);
backgroundProcess();
} catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
StandardSessionManager.this.log.error("SessionManagerImpl.backgroundProcess error", e);
}
}
@@ -230,8 +228,9 @@ public InternalSession[] findSessions() {
@Override
public void backgroundProcess() {
this.count = (this.count + 1) % this.processExpiresFrequency;
- if (this.count == 0)
+ if (this.count == 0) {
processExpires();
+ }
}
/**
@@ -243,16 +242,18 @@ public void processExpires() {
InternalSession sessions[] = findSessions();
int expireHere = 0;
- if (this.log.isDebugEnabled())
+ if (this.log.isDebugEnabled()) {
this.log.debug("Start expire sessions {} at {} sessioncount {}", getName(), timeNow, sessions.length);
- for (int i = 0; i < sessions.length; i++) {
- if (sessions[i] != null && !sessions[i].isValid()) {
+ }
+ for (InternalSession session : sessions) {
+ if (session != null && !session.isValid()) {
expireHere++;
}
}
long timeEnd = System.currentTimeMillis();
- if (this.log.isDebugEnabled())
+ if (this.log.isDebugEnabled()) {
this.log.debug("End expire sessions {} processingTime {} expired sessions: {}", getName(), timeEnd - timeNow, expireHere);
+ }
this.processingTime += (timeEnd - timeNow);
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/TooManyActiveSessionsException.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/TooManyActiveSessionsException.java
index 72a2b4c416..114dd1c4ed 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/TooManyActiveSessionsException.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/TooManyActiveSessionsException.java
@@ -19,9 +19,11 @@
/**
* An exception that indicates the maximum number of active sessions has been
* reached and the server is refusing to create any new sessions.
+ *
+ * @author Daniel Qian
*/
public class TooManyActiveSessionsException
- extends IllegalStateException {
+ extends IllegalStateException {
private static final long serialVersionUID = 1L;
/**
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/WxSession.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/WxSession.java
index 25bed2d274..3aa79f9ad2 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/WxSession.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/WxSession.java
@@ -2,6 +2,9 @@
import java.util.Enumeration;
+/**
+ * @author Daniel Qian
+ */
public interface WxSession {
Object getAttribute(String name);
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/WxSessionManager.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/WxSessionManager.java
index c966ddab28..789e272875 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/WxSessionManager.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/WxSessionManager.java
@@ -1,5 +1,8 @@
package me.chanjar.weixin.common.session;
+/**
+ * @author Daniel Qian
+ */
public interface WxSessionManager {
/**
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/BeanUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/BeanUtils.java
index 630821e954..768f2e5324 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/BeanUtils.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/BeanUtils.java
@@ -1,11 +1,9 @@
package me.chanjar.weixin.common.util;
import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.thoughtworks.xstream.annotations.XStreamAlias;
import me.chanjar.weixin.common.annotation.Required;
-import me.chanjar.weixin.common.bean.result.WxError;
-import me.chanjar.weixin.common.exception.WxErrorException;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -14,14 +12,14 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import java.util.Map;
/**
*
* bean操作的一些工具类
* Created by Binary Wang on 2016-10-21.
- * @author binarywang(Binary Wang)
*
+ *
+ * @author binarywang(Binary Wang)
*/
public class BeanUtils {
private static Logger log = LoggerFactory.getLogger(BeanUtils.class);
@@ -30,7 +28,6 @@ public class BeanUtils {
* 检查bean里标记为@Required的field是否为空,为空则抛异常
*
* @param bean 要检查的bean对象
- * @throws WxErrorException
*/
public static void checkRequiredFields(Object bean) throws WxErrorException {
List requiredFields = Lists.newArrayList();
@@ -42,55 +39,27 @@ public static void checkRequiredFields(Object bean) throws WxErrorException {
boolean isAccessible = field.isAccessible();
field.setAccessible(true);
if (field.isAnnotationPresent(Required.class)) {
- if (field.get(bean) == null || (field.get(bean) instanceof String && StringUtils.isBlank(field.get(bean).toString()))) {
- //两种情况,一种是值为null,另外一种情况是类型为字符串,但是字符串内容为空的,都认为是没有提供值
+ // 两种情况,一种是值为null,
+ // 另外一种情况是类型为字符串,但是字符串内容为空的,都认为是没有提供值
+ boolean isRequiredMissing = field.get(bean) == null
+ || (field.get(bean) instanceof String
+ && StringUtils.isBlank(field.get(bean).toString())
+ );
+ if (isRequiredMissing) {
requiredFields.add(field.getName());
}
}
field.setAccessible(isAccessible);
- } catch (SecurityException | IllegalArgumentException
- | IllegalAccessException e) {
- e.printStackTrace();
+ } catch (SecurityException | IllegalArgumentException | IllegalAccessException e) {
+ log.error(e.getMessage(), e);
}
}
if (!requiredFields.isEmpty()) {
- String msg = "必填字段 " + requiredFields + " 必须提供值";
+ String msg = String.format("必填字段【%s】必须提供值!", requiredFields);
log.debug(msg);
- throw new WxErrorException(WxError.newBuilder().setErrorMsg(msg).build());
+ throw new WxErrorException(WxError.builder().errorMsg(msg).build());
}
}
- /**
- * 将bean按照@XStreamAlias标识的字符串内容生成以之为key的map对象
- *
- * @param bean 包含@XStreamAlias的xml bean对象
- * @return map对象
- */
- public static Map xmlBean2Map(Object bean) {
- Map result = Maps.newHashMap();
- List fields = new ArrayList<>(Arrays.asList(bean.getClass().getDeclaredFields()));
- fields.addAll(Arrays.asList(bean.getClass().getSuperclass().getDeclaredFields()));
- for (Field field : fields) {
- try {
- boolean isAccessible = field.isAccessible();
- field.setAccessible(true);
- if (field.get(bean) == null) {
- field.setAccessible(isAccessible);
- continue;
- }
-
- if (field.isAnnotationPresent(XStreamAlias.class)) {
- result.put(field.getAnnotation(XStreamAlias.class).value(), field.get(bean).toString());
- }
-
- field.setAccessible(isAccessible);
- } catch (SecurityException | IllegalArgumentException | IllegalAccessException e) {
- e.printStackTrace();
- }
-
- }
-
- return result;
- }
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/DataUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/DataUtils.java
new file mode 100644
index 0000000000..983d9a668f
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/DataUtils.java
@@ -0,0 +1,24 @@
+package me.chanjar.weixin.common.util;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ *
+ * 数据处理工具类
+ * Created by BinaryWang on 2018/5/8.
+ *
+ *
+ * @author Binary Wang
+ */
+public class DataUtils {
+ /**
+ * 将数据中包含的secret字符使用星号替换,防止日志打印时被输出
+ */
+ public static E handleDataWithSecret(E data) {
+ E dataForLog = data;
+ if(data instanceof String && StringUtils.contains((String)data, "&secret=")){
+ dataForLog = (E) StringUtils.replaceAll((String)data,"&secret=\\w+&","&secret=******&");
+ }
+ return dataForLog;
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/LogExceptionHandler.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/LogExceptionHandler.java
index 7ad976abd8..7487a0fe29 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/LogExceptionHandler.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/LogExceptionHandler.java
@@ -1,7 +1,7 @@
package me.chanjar.weixin.common.util;
import me.chanjar.weixin.common.api.WxErrorExceptionHandler;
-import me.chanjar.weixin.common.exception.WxErrorException;
+import me.chanjar.weixin.common.error.WxErrorException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/SignUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/SignUtils.java
new file mode 100644
index 0000000000..fc3579d45c
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/SignUtils.java
@@ -0,0 +1,42 @@
+package me.chanjar.weixin.common.util;
+
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.commons.codec.binary.Hex;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ *
+ * 签名工具类
+ * Created by BinaryWang on 2018/7/11.
+ *
+ *
+ * @author Binary Wang
+ */
+@Slf4j
+public class SignUtils {
+ /**
+ * HmacSHA256 签名算法
+ *
+ * @param message 签名数据
+ * @param key 签名密钥
+ */
+ public static String createHmacSha256Sign(String message, String key) {
+ try {
+ Mac sha256 = Mac.getInstance("HmacSHA256");
+ SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
+ sha256.init(secretKeySpec);
+ byte[] bytes = sha256.doFinal(message.getBytes(StandardCharsets.UTF_8));
+ return Hex.encodeHexString(bytes).toUpperCase();
+ } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+ SignUtils.log.error(e.getMessage(), e);
+ }
+
+ return null;
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/ToStringUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/ToStringUtils.java
deleted file mode 100644
index 0fa38391c0..0000000000
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/ToStringUtils.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package me.chanjar.weixin.common.util;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-
-/**
- *
- * 自定义的ToString方法,用于产生去掉空值属性的字符串
- * Created by Binary Wang on 2016-10-27.
- * @author binarywang(Binary Wang)
- *
- */
-public class ToStringUtils {
- public static final ToStringStyle THE_STYLE = new SimpleMultiLineToStringStyle();
- private static class SimpleMultiLineToStringStyle extends ToStringStyle {
- private static final long serialVersionUID = 4645306494220335355L;
- private static final String LINE_SEPARATOR = "\n";
- private static final String NULL_TEXT = "";
-
- public SimpleMultiLineToStringStyle() {
- super();
- this.setContentStart("[");
- this.setFieldSeparator(LINE_SEPARATOR + " ");
- this.setFieldSeparatorAtStart(true);
- this.setContentEnd(LINE_SEPARATOR + "]");
- this.setNullText(NULL_TEXT);
- this.setUseShortClassName(true);
- this.setUseIdentityHashCode(false);
- }
- }
-
- /**
- * 用于产生去掉空值属性并以换行符分割各属性键值的toString字符串
- * @param obj
- */
- public static String toSimpleString(Object obj) {
- String toStringResult = ToStringBuilder.reflectionToString(obj, THE_STYLE);
- String[] split = toStringResult.split(SimpleMultiLineToStringStyle.LINE_SEPARATOR);
- StringBuilder result = new StringBuilder();
- for (String string : split) {
- if (string.endsWith(SimpleMultiLineToStringStyle.NULL_TEXT)) {
- continue;
- }
-
- result.append(string + SimpleMultiLineToStringStyle.LINE_SEPARATOR);
- }
-
- if (result.length() == 0) {
- return "";
- }
-
- //如果没有非空的属性,就输出
- if (StringUtils.countMatches(result, SimpleMultiLineToStringStyle.LINE_SEPARATOR) == 2) {
- return result.toString().split(SimpleMultiLineToStringStyle.LINE_SEPARATOR)[0]
- + "]";
- }
-
- return result.deleteCharAt(result.length() - 1).toString();
- }
-}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/XmlUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/XmlUtils.java
new file mode 100644
index 0000000000..c2ffdb001b
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/XmlUtils.java
@@ -0,0 +1,101 @@
+package me.chanjar.weixin.common.util;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.dom4j.Document;
+import org.dom4j.DocumentException;
+import org.dom4j.Element;
+import org.dom4j.Node;
+import org.dom4j.io.SAXReader;
+import org.dom4j.tree.DefaultText;
+import org.xml.sax.SAXException;
+
+import java.io.StringReader;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ *
+ * XML转换工具类.
+ * Created by Binary Wang on 2018/11/4.
+ *
+ *
+ * @author Binary Wang
+ */
+public class XmlUtils {
+
+ public static Map xml2Map(String xmlString) {
+ Map map = new HashMap<>(16);
+ try {
+ SAXReader saxReader = new SAXReader();
+ saxReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+ saxReader.setFeature("http://javax.xml.XMLConstants/feature/secure-processing", true);
+ saxReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+ saxReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
+ saxReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+ Document doc = saxReader.read(new StringReader(xmlString));
+ Element root = doc.getRootElement();
+ List elements = root.elements();
+ for (Element element : elements) {
+ map.put(element.getName(), element2MapOrString(element));
+ }
+ } catch (DocumentException | SAXException e) {
+ throw new RuntimeException(e);
+ }
+
+ return map;
+ }
+
+ private static Object element2MapOrString(Element element) {
+ Map result = Maps.newHashMap();
+
+ final List content = element.content();
+ if (content.size() <= 1) {
+ return element.getText();
+ }
+
+ final Set names = names(content);
+ if (names.size() == 1) {
+ // 说明是个列表,各个子对象是相同的name
+ List list = Lists.newArrayList();
+ for (Node node : content) {
+ if (node instanceof DefaultText) {
+ continue;
+ }
+
+ if (node instanceof Element) {
+ list.add(element2MapOrString((Element) node));
+ }
+ }
+
+ result.put(names.iterator().next(), list);
+ } else {
+ for (Node node : content) {
+ if (node instanceof DefaultText) {
+ continue;
+ }
+
+ if (node instanceof Element) {
+ result.put(node.getName(), element2MapOrString((Element) node));
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private static Set names(List nodes) {
+ Set names = Sets.newHashSet();
+ for (Node node : nodes) {
+ if (node instanceof DefaultText) {
+ continue;
+ }
+ names.add(node.getName());
+ }
+
+ return names;
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/ByteGroup.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/ByteGroup.java
index b12ec65555..bff08d680d 100755
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/ByteGroup.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/ByteGroup.java
@@ -1,26 +1,26 @@
-package me.chanjar.weixin.common.util.crypto;
-
-import java.util.ArrayList;
-
-public class ByteGroup {
- ArrayList byteContainer = new ArrayList<>();
-
- public byte[] toBytes() {
- byte[] bytes = new byte[this.byteContainer.size()];
- for (int i = 0; i < this.byteContainer.size(); i++) {
- bytes[i] = this.byteContainer.get(i);
- }
- return bytes;
- }
-
- public ByteGroup addBytes(byte[] bytes) {
- for (byte b : bytes) {
- this.byteContainer.add(b);
- }
- return this;
- }
-
- public int size() {
- return this.byteContainer.size();
- }
-}
+package me.chanjar.weixin.common.util.crypto;
+
+import java.util.ArrayList;
+
+public class ByteGroup {
+ ArrayList byteContainer = new ArrayList<>();
+
+ public byte[] toBytes() {
+ byte[] bytes = new byte[this.byteContainer.size()];
+ for (int i = 0; i < this.byteContainer.size(); i++) {
+ bytes[i] = this.byteContainer.get(i);
+ }
+ return bytes;
+ }
+
+ public ByteGroup addBytes(byte[] bytes) {
+ for (byte b : bytes) {
+ this.byteContainer.add(b);
+ }
+ return this;
+ }
+
+ public int size() {
+ return this.byteContainer.size();
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/PKCS7Encoder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/PKCS7Encoder.java
index 145896a577..efe7867baf 100755
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/PKCS7Encoder.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/PKCS7Encoder.java
@@ -1,68 +1,65 @@
-/**
- * 对公众平台发送给公众账号的消息加解密示例代码.
- *
- * @copyright Copyright (c) 1998-2014 Tencent Inc.
- */
-
-// ------------------------------------------------------------------------
-
-package me.chanjar.weixin.common.util.crypto;
-
-import java.nio.charset.Charset;
-import java.util.Arrays;
-
-/**
- * 提供基于PKCS7算法的加解
- */
-public class PKCS7Encoder {
-
- private static final Charset CHARSET = Charset.forName("utf-8");
- private static final int BLOCK_SIZE = 32;
-
- /**
- * 获得对明文进行补位填充的字节.
- *
- * @param count 需要进行填充补位操作的明文字节个数
- * @return 补齐用的字节数组
- */
- public static byte[] encode(int count) {
- // 计算需要填充的位数
- int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
- if (amountToPad == 0) {
- amountToPad = BLOCK_SIZE;
- }
- // 获得补位所用的字符
- char padChr = chr(amountToPad);
- String tmp = new String();
- for (int index = 0; index < amountToPad; index++) {
- tmp += padChr;
- }
- return tmp.getBytes(CHARSET);
- }
-
- /**
- * 删除解密后明文的补位字符
- *
- * @param decrypted 解密后的明文
- * @return 删除补位字符后的明文
- */
- public static byte[] decode(byte[] decrypted) {
- int pad = decrypted[decrypted.length - 1];
- if (pad < 1 || pad > 32) {
- pad = 0;
- }
- return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
- }
-
- /**
- * 将数字转化成ASCII码对应的字符,用于对明文进行补码
- *
- * @param a 需要转化的数字
- * @return 转化得到的字符
- */
- public static char chr(int a) {
- byte target = (byte) (a & 0xFF);
- return (char) target;
- }
-
-}
+/*
+ * 对公众平台发送给公众账号的消息加解密示例代码.
+ *
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+package me.chanjar.weixin.common.util.crypto;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+/**
+ * 提供基于PKCS7算法的加解.
+ *
+ * @author tencent
+ */
+public class PKCS7Encoder {
+ private static final Charset CHARSET = StandardCharsets.UTF_8;
+ private static final int BLOCK_SIZE = 32;
+
+ /**
+ * 获得对明文进行补位填充的字节.
+ *
+ * @param count 需要进行填充补位操作的明文字节个数
+ * @return 补齐用的字节数组
+ */
+ public static byte[] encode(int count) {
+ // 计算需要填充的位数
+ int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
+ // 获得补位所用的字符
+ char padChr = chr(amountToPad);
+ StringBuilder tmp = new StringBuilder();
+ for (int index = 0; index < amountToPad; index++) {
+ tmp.append(padChr);
+ }
+ return tmp.toString().getBytes(CHARSET);
+ }
+
+ /**
+ * 删除解密后明文的补位字符.
+ *
+ * @param decrypted 解密后的明文
+ * @return 删除补位字符后的明文
+ */
+ public static byte[] decode(byte[] decrypted) {
+ int pad = decrypted[decrypted.length - 1];
+ if (pad < 1 || pad > 32) {
+ pad = 0;
+ }
+ return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
+ }
+
+ /**
+ * 将数字转化成ASCII码对应的字符,用于对明文进行补码.
+ *
+ * @param a 需要转化的数字
+ * @return 转化得到的字符
+ */
+ private static char chr(int a) {
+ byte target = (byte) (a & 0xFF);
+ return (char) target;
+ }
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/SHA1.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/SHA1.java
index 683f2ecfc3..c82f94d871 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/SHA1.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/SHA1.java
@@ -1,18 +1,25 @@
package me.chanjar.weixin.common.util.crypto;
-import java.util.Arrays;
-
import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Arrays;
/**
- * Created by Daniel Qian on 14/10/19.
+ *
+ * @author Daniel Qian
+ * @date 14/10/19
*/
public class SHA1 {
/**
- * 串接arr参数,生成sha1 digest
+ * 串接arr参数,生成sha1 digest.
*/
public static String gen(String... arr) {
+ if (StringUtils.isAnyEmpty(arr)) {
+ throw new IllegalArgumentException("非法请求参数,有部分参数为空 : " + Arrays.toString(arr));
+ }
+
Arrays.sort(arr);
StringBuilder sb = new StringBuilder();
for (String a : arr) {
@@ -22,9 +29,13 @@ public static String gen(String... arr) {
}
/**
- * 用&串接arr参数,生成sha1 digest
+ * 用&串接arr参数,生成sha1 digest.
*/
public static String genWithAmple(String... arr) {
+ if (StringUtils.isAnyEmpty(arr)) {
+ throw new IllegalArgumentException("非法请求参数,有部分参数为空 : " + Arrays.toString(arr));
+ }
+
Arrays.sort(arr);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < arr.length; i++) {
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java
index a809fe3b1b..0103fc7f06 100755
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java
@@ -1,27 +1,10 @@
-/**
- * 对公众平台发送给公众账号的消息加解密示例代码.
- *
- * @copyright Copyright (c) 1998-2014 Tencent Inc.
- *
- * 针对org.apache.commons.codec.binary.Base64,
- * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本)
- * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi
- */
-
-// ------------------------------------------------------------------------
-
-/**
- * 针对org.apache.commons.codec.binary.Base64,
- * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本)
- * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi
- */
package me.chanjar.weixin.common.util.crypto;
import java.io.StringReader;
import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Random;
-
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
@@ -29,21 +12,37 @@
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
+import com.google.common.base.CharMatcher;
+import com.google.common.io.BaseEncoding;
import org.apache.commons.codec.binary.Base64;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;
+/**
+ *
+ * 对公众平台发送给公众账号的消息加解密示例代码.
+ * Copyright (c) 1998-2014 Tencent Inc.
+ * 针对org.apache.commons.codec.binary.Base64,
+ * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本)
+ * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi
+ *
+ *
+ * @author Tencent
+ */
public class WxCryptUtil {
- private static final Base64 base64 = new Base64();
- private static final Charset CHARSET = Charset.forName("utf-8");
+ private static final Base64 BASE64 = new Base64();
+ private static final Charset CHARSET = StandardCharsets.UTF_8;
- private static final ThreadLocal builderLocal = new ThreadLocal() {
+ private static final ThreadLocal BUILDER_LOCAL = new ThreadLocal() {
@Override
protected DocumentBuilder initialValue() {
try {
- return DocumentBuilderFactory.newInstance().newDocumentBuilder();
+ final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setExpandEntityReferences(false);
+ factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+ return factory.newDocumentBuilder();
} catch (ParserConfigurationException exc) {
throw new IllegalArgumentException(exc);
}
@@ -55,26 +54,24 @@ protected DocumentBuilder initialValue() {
protected String appidOrCorpid;
public WxCryptUtil() {
- super();
}
/**
- * 构造函数
+ * 构造函数.
*
- * @param token 公众平台上,开发者设置的token
- * @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey
- * @param appidOrCorpid 公众平台appid/corpid
+ * @param token 公众平台上,开发者设置的token
+ * @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey
+ * @param appidOrCorpid 公众平台appid/corpid
*/
- public WxCryptUtil(String token, String encodingAesKey,
- String appidOrCorpid) {
+ public WxCryptUtil(String token, String encodingAesKey, String appidOrCorpid) {
this.token = token;
this.appidOrCorpid = appidOrCorpid;
- this.aesKey = Base64.decodeBase64(encodingAesKey + "=");
+ this.aesKey = BaseEncoding.base64().decode(CharMatcher.whitespace().removeFrom(encodingAesKey));
}
- static String extractEncryptPart(String xml) {
+ private static String extractEncryptPart(String xml) {
try {
- DocumentBuilder db = builderLocal.get();
+ DocumentBuilder db = BUILDER_LOCAL.get();
Document document = db.parse(new InputSource(new StringReader(xml)));
Element root = document.getDocumentElement();
@@ -84,6 +81,61 @@ static String extractEncryptPart(String xml) {
}
}
+ /**
+ * 将一个数字转换成生成4个字节的网络字节序bytes数组.
+ */
+ private static byte[] number2BytesInNetworkOrder(int number) {
+ byte[] orderBytes = new byte[4];
+ orderBytes[3] = (byte) (number & 0xFF);
+ orderBytes[2] = (byte) (number >> 8 & 0xFF);
+ orderBytes[1] = (byte) (number >> 16 & 0xFF);
+ orderBytes[0] = (byte) (number >> 24 & 0xFF);
+ return orderBytes;
+ }
+
+ /**
+ * 4个字节的网络字节序bytes数组还原成一个数字.
+ */
+ private static int bytesNetworkOrder2Number(byte[] bytesInNetworkOrder) {
+ int sourceNumber = 0;
+ for (int i = 0; i < 4; i++) {
+ sourceNumber <<= 8;
+ sourceNumber |= bytesInNetworkOrder[i] & 0xff;
+ }
+ return sourceNumber;
+ }
+
+ /**
+ * 随机生成16位字符串.
+ */
+ private static String genRandomStr() {
+ String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ Random random = new Random();
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < 16; i++) {
+ int number = random.nextInt(base.length());
+ sb.append(base.charAt(number));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 生成xml消息.
+ *
+ * @param encrypt 加密后的消息密文
+ * @param signature 安全签名
+ * @param timestamp 时间戳
+ * @param nonce 随机字符串
+ * @return 生成的xml字符串
+ */
+ private static String generateXml(String encrypt, String signature, String timestamp, String nonce) {
+ String format = "\n" + " \n"
+ + " \n"
+ + "%3$s \n" + " \n"
+ + " ";
+ return String.format(format, encrypt, signature, timestamp, nonce);
+ }
+
/**
* 将公众平台回复用户的消息加密打包.
*
@@ -100,12 +152,11 @@ public String encrypt(String plainText) {
String encryptedXml = encrypt(genRandomStr(), plainText);
// 生成安全签名
- String timeStamp = Long.toString(System.currentTimeMillis() / 1000l);
+ String timeStamp = Long.toString(System.currentTimeMillis() / 1000L);
String nonce = genRandomStr();
String signature = SHA1.gen(this.token, timeStamp, nonce, encryptedXml);
- String result = generateXml(encryptedXml, signature, timeStamp, nonce);
- return result;
+ return generateXml(encryptedXml, signature, timeStamp, nonce);
}
/**
@@ -118,8 +169,7 @@ protected String encrypt(String randomStr, String plainText) {
ByteGroup byteCollector = new ByteGroup();
byte[] randomStringBytes = randomStr.getBytes(CHARSET);
byte[] plainTextBytes = plainText.getBytes(CHARSET);
- byte[] bytesOfSizeInNetworkOrder = number2BytesInNetworkOrder(
- plainTextBytes.length);
+ byte[] bytesOfSizeInNetworkOrder = number2BytesInNetworkOrder(plainTextBytes.length);
byte[] appIdBytes = this.appidOrCorpid.getBytes(CHARSET);
// randomStr + networkBytesOrder + text + appid
@@ -146,9 +196,7 @@ protected String encrypt(String randomStr, String plainText) {
byte[] encrypted = cipher.doFinal(unencrypted);
// 使用BASE64对加密后的字符串进行编码
- String base64Encrypted = base64.encodeToString(encrypted);
-
- return base64Encrypted;
+ return BASE64.encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -168,8 +216,7 @@ protected String encrypt(String randomStr, String plainText) {
* @param encryptedXml 密文,对应POST请求的数据
* @return 解密后的原文
*/
- public String decrypt(String msgSignature, String timeStamp, String nonce,
- String encryptedXml) {
+ public String decrypt(String msgSignature, String timeStamp, String nonce, String encryptedXml) {
// 密钥,公众账号的app corpSecret
// 提取密文
String cipherText = extractEncryptPart(encryptedXml);
@@ -181,8 +228,7 @@ public String decrypt(String msgSignature, String timeStamp, String nonce,
}
// 解密
- String result = decrypt(cipherText);
- return result;
+ return decrypt(cipherText);
}
/**
@@ -196,10 +242,9 @@ public String decrypt(String cipherText) {
try {
// 设置解密模式为AES的CBC模式
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
- SecretKeySpec key_spec = new SecretKeySpec(this.aesKey, "AES");
- IvParameterSpec iv = new IvParameterSpec(
- Arrays.copyOfRange(this.aesKey, 0, 16));
- cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);
+ SecretKeySpec keySpec = new SecretKeySpec(this.aesKey, "AES");
+ IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(this.aesKey, 0, 16));
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
// 使用BASE64对密文进行解码
byte[] encrypted = Base64.decodeBase64(cipherText);
@@ -210,7 +255,8 @@ public String decrypt(String cipherText) {
throw new RuntimeException(e);
}
- String xmlContent, from_appid;
+ String xmlContent;
+ String fromAppid;
try {
// 去除补位字符
byte[] bytes = PKCS7Encoder.decode(original);
@@ -220,81 +266,19 @@ public String decrypt(String cipherText) {
int xmlLength = bytesNetworkOrder2Number(networkOrder);
- xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength),
- CHARSET);
- from_appid = new String(
- Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), CHARSET);
+ xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
+ fromAppid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), CHARSET);
} catch (Exception e) {
throw new RuntimeException(e);
}
- // appid不相同的情况
- if (!from_appid.equals(this.appidOrCorpid)) {
- throw new RuntimeException("AppID不正确");
- }
+ // appid不相同的情况 暂时忽略这段判断
+// if (!fromAppid.equals(this.appidOrCorpid)) {
+// throw new RuntimeException("AppID不正确,请核实!");
+// }
return xmlContent;
}
- /**
- * 将一个数字转换成生成4个字节的网络字节序bytes数组
- *
- * @param number
- */
- private static byte[] number2BytesInNetworkOrder(int number) {
- byte[] orderBytes = new byte[4];
- orderBytes[3] = (byte) (number & 0xFF);
- orderBytes[2] = (byte) (number >> 8 & 0xFF);
- orderBytes[1] = (byte) (number >> 16 & 0xFF);
- orderBytes[0] = (byte) (number >> 24 & 0xFF);
- return orderBytes;
- }
-
- /**
- * 4个字节的网络字节序bytes数组还原成一个数字
- *
- * @param bytesInNetworkOrder
- */
- private static int bytesNetworkOrder2Number(byte[] bytesInNetworkOrder) {
- int sourceNumber = 0;
- for (int i = 0; i < 4; i++) {
- sourceNumber <<= 8;
- sourceNumber |= bytesInNetworkOrder[i] & 0xff;
- }
- return sourceNumber;
- }
-
- /**
- * 随机生成16位字符串
- */
- private static String genRandomStr() {
- String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
- Random random = new Random();
- StringBuffer sb = new StringBuffer();
- for (int i = 0; i < 16; i++) {
- int number = random.nextInt(base.length());
- sb.append(base.charAt(number));
- }
- return sb.toString();
- }
-
- /**
- * 生成xml消息
- *
- * @param encrypt 加密后的消息密文
- * @param signature 安全签名
- * @param timestamp 时间戳
- * @param nonce 随机字符串
- * @return 生成的xml字符串
- */
- private static String generateXml(String encrypt, String signature,
- String timestamp, String nonce) {
- String format = "\n" + " \n"
- + " \n"
- + "%3$s \n" + " \n"
- + " ";
- return String.format(format, encrypt, signature, timestamp, nonce);
- }
-
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/fs/FileUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/fs/FileUtils.java
index 9ff25ebc10..a00c9cbade 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/fs/FileUtils.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/fs/FileUtils.java
@@ -1,52 +1,66 @@
package me.chanjar.weixin.common.util.fs;
import java.io.File;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.Base64;
public class FileUtils {
-
/**
- * 创建临时文件
+ * 创建临时文件.
*
- * @param inputStream
+ * @param inputStream 输入文件流
* @param name 文件名
* @param ext 扩展名
* @param tmpDirFile 临时文件夹目录
*/
public static File createTmpFile(InputStream inputStream, String name, String ext, File tmpDirFile) throws IOException {
- File tmpFile;
- if (tmpDirFile == null) {
- tmpFile = File.createTempFile(name, '.' + ext);
- } else {
- tmpFile = File.createTempFile(name, '.' + ext, tmpDirFile);
- }
-
- tmpFile.deleteOnExit();
-
- try (FileOutputStream fos = new FileOutputStream(tmpFile)) {
- int read = 0;
- byte[] bytes = new byte[1024 * 100];
- while ((read = inputStream.read(bytes)) != -1) {
- fos.write(bytes, 0, read);
- }
+ File resultFile = File.createTempFile(name, '.' + ext, tmpDirFile);
- fos.flush();
- return tmpFile;
- }
+ resultFile.deleteOnExit();
+ org.apache.commons.io.FileUtils.copyToFile(inputStream, resultFile);
+ return resultFile;
}
/**
- * 创建临时文件
+ * 创建临时文件.
*
- * @param inputStream
+ * @param inputStream 输入文件流
* @param name 文件名
* @param ext 扩展名
*/
public static File createTmpFile(InputStream inputStream, String name, String ext) throws IOException {
- return createTmpFile(inputStream, name, ext, null);
+ return createTmpFile(inputStream, name, ext, Files.createTempDirectory("weixin-java-tools-temp").toFile());
+ }
+
+ /**
+ * 文件流生成base64
+ *
+ * @param in 文件流
+ * @return base64编码
+ */
+ public static String imageToBase64ByStream(InputStream in) {
+ byte[] data = null;
+ // 读取图片字节数组
+ try {
+ data = new byte[in.available()];
+ in.read(data);
+ // 返回Base64编码过的字节数组字符串
+ return Base64.getEncoder().encodeToString(data);
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ return null;
}
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/BaseMediaDownloadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/BaseMediaDownloadRequestExecutor.java
new file mode 100644
index 0000000000..ed5ec17bc9
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/BaseMediaDownloadRequestExecutor.java
@@ -0,0 +1,46 @@
+package me.chanjar.weixin.common.util.http;
+
+import java.io.File;
+import java.io.IOException;
+
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.apache.ApacheMediaDownloadRequestExecutor;
+import me.chanjar.weixin.common.util.http.jodd.JoddHttpMediaDownloadRequestExecutor;
+import me.chanjar.weixin.common.util.http.okhttp.OkHttpMediaDownloadRequestExecutor;
+
+/**
+ * 下载媒体文件请求执行器.
+ * 请求的参数是String, 返回的结果是File
+ * 视频文件不支持下载
+ *
+ * @author Daniel Qian
+ */
+public abstract class BaseMediaDownloadRequestExecutor implements RequestExecutor {
+ protected RequestHttp requestHttp;
+ protected File tmpDirFile;
+
+ public BaseMediaDownloadRequestExecutor(RequestHttp requestHttp, File tmpDirFile) {
+ this.requestHttp = requestHttp;
+ this.tmpDirFile = tmpDirFile;
+ }
+
+ @Override
+ public void execute(String uri, String data, ResponseHandler handler, WxType wxType) throws WxErrorException, IOException {
+ handler.handle(this.execute(uri, data, wxType));
+ }
+
+ public static RequestExecutor create(RequestHttp requestHttp, File tmpDirFile) {
+ switch (requestHttp.getRequestType()) {
+ case APACHE_HTTP:
+ return new ApacheMediaDownloadRequestExecutor(requestHttp, tmpDirFile);
+ case JODD_HTTP:
+ return new JoddHttpMediaDownloadRequestExecutor(requestHttp, tmpDirFile);
+ case OK_HTTP:
+ return new OkHttpMediaDownloadRequestExecutor(requestHttp, tmpDirFile);
+ default:
+ return null;
+ }
+ }
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpResponseProxy.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpResponseProxy.java
new file mode 100644
index 0000000000..0d68518849
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpResponseProxy.java
@@ -0,0 +1,90 @@
+package me.chanjar.weixin.common.util.http;
+
+import jodd.http.HttpResponse;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import okhttp3.Response;
+import org.apache.http.Header;
+import org.apache.http.client.methods.CloseableHttpResponse;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ *
+ * 三种http框架的response代理类,方便提取公共方法
+ * Created by Binary Wang on 2017-8-3.
+ *
+ *
+ * @author Binary Wang
+ */
+public class HttpResponseProxy {
+ private static final Pattern PATTERN = Pattern.compile(".*filename=\"(.*)\"");
+
+ private CloseableHttpResponse apacheHttpResponse;
+ private HttpResponse joddHttpResponse;
+ private Response okHttpResponse;
+
+ public HttpResponseProxy(CloseableHttpResponse apacheHttpResponse) {
+ this.apacheHttpResponse = apacheHttpResponse;
+ }
+
+ public HttpResponseProxy(HttpResponse joddHttpResponse) {
+ this.joddHttpResponse = joddHttpResponse;
+ }
+
+ public HttpResponseProxy(Response okHttpResponse) {
+ this.okHttpResponse = okHttpResponse;
+ }
+
+ public String getFileName() throws WxErrorException {
+ //由于对象只能由一个构造方法实现,因此三个response对象必定且只有一个不为空
+ if (this.apacheHttpResponse != null) {
+ return this.getFileName(this.apacheHttpResponse);
+ }
+
+ if (this.joddHttpResponse != null) {
+ return this.getFileName(this.joddHttpResponse);
+ }
+
+ if (this.okHttpResponse != null) {
+ return this.getFileName(this.okHttpResponse);
+ }
+
+ //cannot happen
+ return null;
+ }
+
+ private String getFileName(CloseableHttpResponse response) throws WxErrorException {
+ Header[] contentDispositionHeader = response.getHeaders("Content-disposition");
+ if (contentDispositionHeader == null || contentDispositionHeader.length == 0) {
+ throw new WxErrorException(WxError.builder().errorMsg("无法获取到文件名").errorCode(99999).build());
+ }
+
+ return this.extractFileNameFromContentString(contentDispositionHeader[0].getValue());
+ }
+
+ private String getFileName(HttpResponse response) throws WxErrorException {
+ String content = response.header("Content-disposition");
+ return this.extractFileNameFromContentString(content);
+ }
+
+ private String getFileName(Response response) throws WxErrorException {
+ String content = response.header("Content-disposition");
+ return this.extractFileNameFromContentString(content);
+ }
+
+ private String extractFileNameFromContentString(String content) throws WxErrorException {
+ if (content == null || content.length() == 0) {
+ throw new WxErrorException(WxError.builder().errorMsg("无法获取到文件名").errorCode(99999).build());
+ }
+
+ Matcher m = PATTERN.matcher(content);
+ if (m.matches()) {
+ return m.group(1);
+ }
+
+ throw new WxErrorException(WxError.builder().errorMsg("无法获取到文件名").errorCode(99999).build());
+ }
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpType.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpType.java
new file mode 100644
index 0000000000..eff5907f7a
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpType.java
@@ -0,0 +1,19 @@
+package me.chanjar.weixin.common.util.http;
+
+/**
+ * Created by ecoolper on 2017/4/28.
+ */
+public enum HttpType {
+ /**
+ * jodd-http.
+ */
+ JODD_HTTP,
+ /**
+ * apache httpclient.
+ */
+ APACHE_HTTP,
+ /**
+ * okhttp.
+ */
+ OK_HTTP
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaDownloadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaDownloadRequestExecutor.java
deleted file mode 100644
index 57024f5001..0000000000
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaDownloadRequestExecutor.java
+++ /dev/null
@@ -1,93 +0,0 @@
-package me.chanjar.weixin.common.util.http;
-
-import me.chanjar.weixin.common.bean.result.WxError;
-import me.chanjar.weixin.common.exception.WxErrorException;
-import me.chanjar.weixin.common.util.fs.FileUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.http.Header;
-import org.apache.http.HttpHost;
-import org.apache.http.client.config.RequestConfig;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.entity.ContentType;
-import org.apache.http.impl.client.CloseableHttpClient;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * 下载媒体文件请求执行器,请求的参数是String, 返回的结果是File
- * 视频文件不支持下载
- * @author Daniel Qian
- */
-public class MediaDownloadRequestExecutor implements RequestExecutor {
-
- private File tmpDirFile;
-
- public MediaDownloadRequestExecutor() {
- }
-
- public MediaDownloadRequestExecutor(File tmpDirFile) {
- this.tmpDirFile = tmpDirFile;
- }
-
- @Override
- public File execute(CloseableHttpClient httpclient, HttpHost httpProxy, String uri, String queryParam) throws WxErrorException, IOException {
- if (queryParam != null) {
- if (uri.indexOf('?') == -1) {
- uri += '?';
- }
- uri += uri.endsWith("?") ? queryParam : '&' + queryParam;
- }
-
- HttpGet httpGet = new HttpGet(uri);
- if (httpProxy != null) {
- RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build();
- httpGet.setConfig(config);
- }
-
- try (CloseableHttpResponse response = httpclient.execute(httpGet);
- InputStream inputStream = InputStreamResponseHandler.INSTANCE
- .handleResponse(response)) {
-
- Header[] contentTypeHeader = response.getHeaders("Content-Type");
- if (contentTypeHeader != null && contentTypeHeader.length > 0) {
- if (contentTypeHeader[0].getValue().startsWith(ContentType.APPLICATION_JSON.getMimeType())) {
- // application/json; encoding=utf-8 下载媒体文件出错
- String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
- throw new WxErrorException(WxError.fromJson(responseContent));
- }
- }
-
- String fileName = getFileName(response);
- if (StringUtils.isBlank(fileName)) {
- return null;
- }
-
- String[] nameAndExt = fileName.split("\\.");
- return FileUtils.createTmpFile(inputStream, nameAndExt[0], nameAndExt[1], this.tmpDirFile);
-
- } finally {
- httpGet.releaseConnection();
- }
-
- }
-
- private String getFileName(CloseableHttpResponse response) throws WxErrorException {
- Header[] contentDispositionHeader = response.getHeaders("Content-disposition");
- if(contentDispositionHeader == null || contentDispositionHeader.length == 0){
- throw new WxErrorException(WxError.newBuilder().setErrorMsg("无法获取到文件名").build());
- }
-
- Pattern p = Pattern.compile(".*filename=\"(.*)\"");
- Matcher m = p.matcher(contentDispositionHeader[0].getValue());
- if(m.matches()){
- return m.group(1);
- }
- throw new WxErrorException(WxError.newBuilder().setErrorMsg("无法获取到文件名").build());
- }
-
-}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaUploadRequestExecutor.java
index 54ded05a7a..14724412f1 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaUploadRequestExecutor.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaUploadRequestExecutor.java
@@ -1,53 +1,43 @@
package me.chanjar.weixin.common.util.http;
-import me.chanjar.weixin.common.bean.result.WxError;
-import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
-import me.chanjar.weixin.common.exception.WxErrorException;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpHost;
-import org.apache.http.client.config.RequestConfig;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.entity.ContentType;
-import org.apache.http.entity.mime.HttpMultipartMode;
-import org.apache.http.entity.mime.MultipartEntityBuilder;
-import org.apache.http.impl.client.CloseableHttpClient;
-
import java.io.File;
import java.io.IOException;
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.apache.ApacheMediaUploadRequestExecutor;
+import me.chanjar.weixin.common.util.http.jodd.JoddHttpMediaUploadRequestExecutor;
+import me.chanjar.weixin.common.util.http.okhttp.OkHttpMediaUploadRequestExecutor;
+
/**
- * 上传媒体文件请求执行器,请求的参数是File, 返回的结果是String
+ * 上传媒体文件请求执行器.
+ * 请求的参数是File, 返回的结果是String
*
* @author Daniel Qian
*/
-public class MediaUploadRequestExecutor implements RequestExecutor {
+public abstract class MediaUploadRequestExecutor implements RequestExecutor {
+ protected RequestHttp requestHttp;
+
+ public MediaUploadRequestExecutor(RequestHttp requestHttp) {
+ this.requestHttp = requestHttp;
+ }
@Override
- public WxMediaUploadResult execute(CloseableHttpClient httpclient, HttpHost httpProxy, String uri, File file) throws WxErrorException, IOException {
- HttpPost httpPost = new HttpPost(uri);
- if (httpProxy != null) {
- RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build();
- httpPost.setConfig(config);
- }
- if (file != null) {
- HttpEntity entity = MultipartEntityBuilder
- .create()
- .addBinaryBody("media", file)
- .setMode(HttpMultipartMode.RFC6532)
- .build();
- httpPost.setEntity(entity);
- httpPost.setHeader("Content-Type", ContentType.MULTIPART_FORM_DATA.toString());
- }
- try (CloseableHttpResponse response = httpclient.execute(httpPost)) {
- String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
- WxError error = WxError.fromJson(responseContent);
- if (error.getErrorCode() != 0) {
- throw new WxErrorException(error);
- }
- return WxMediaUploadResult.fromJson(responseContent);
- } finally {
- httpPost.releaseConnection();
+ public void execute(String uri, File data, ResponseHandler handler, WxType wxType) throws WxErrorException, IOException {
+ handler.handle(this.execute(uri, data, wxType));
+ }
+
+ public static RequestExecutor create(RequestHttp requestHttp) {
+ switch (requestHttp.getRequestType()) {
+ case APACHE_HTTP:
+ return new ApacheMediaUploadRequestExecutor(requestHttp);
+ case JODD_HTTP:
+ return new JoddHttpMediaUploadRequestExecutor(requestHttp);
+ case OK_HTTP:
+ return new OkHttpMediaUploadRequestExecutor(requestHttp);
+ default:
+ return null;
}
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/RequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/RequestExecutor.java
index 6d2dbe9ab8..da1292ba62 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/RequestExecutor.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/RequestExecutor.java
@@ -1,27 +1,40 @@
package me.chanjar.weixin.common.util.http;
-import me.chanjar.weixin.common.exception.WxErrorException;
-import org.apache.http.HttpHost;
-import org.apache.http.impl.client.CloseableHttpClient;
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.error.WxErrorException;
import java.io.IOException;
/**
- * http请求执行器
+ * http请求执行器.
*
* @param 返回值类型
* @param 请求参数类型
+ * @author Daniel Qian
*/
public interface RequestExecutor {
/**
- * @param httpclient 传入的httpClient
- * @param httpProxy http代理对象,如果没有配置代理则为空
- * @param uri uri
- * @param data 数据
- * @throws WxErrorException
- * @throws IOException
+ * 执行http请求.
+ *
+ * @param uri uri
+ * @param data 数据
+ * @param wxType 微信模块类型
+ * @return 响应结果
+ * @throws WxErrorException 自定义异常
+ * @throws IOException io异常
*/
- T execute(CloseableHttpClient httpclient, HttpHost httpProxy, String uri, E data) throws WxErrorException, IOException;
+ T execute(String uri, E data, WxType wxType) throws WxErrorException, IOException;
+ /**
+ * 执行http请求.
+ *
+ * @param uri uri
+ * @param data 数据
+ * @param handler http响应处理器
+ * @param wxType 微信模块类型
+ * @throws WxErrorException 自定义异常
+ * @throws IOException io异常
+ */
+ void execute(String uri, E data, ResponseHandler handler, WxType wxType) throws WxErrorException, IOException;
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/RequestHttp.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/RequestHttp.java
new file mode 100644
index 0000000000..b7bc850f8f
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/RequestHttp.java
@@ -0,0 +1,31 @@
+package me.chanjar.weixin.common.util.http;
+
+/**
+ * Created by ecoolper on 2017/4/22.
+ *
+ * @author ecoolper
+ */
+public interface RequestHttp {
+
+ /**
+ * 返回httpClient.
+ *
+ * @return 返回httpClient
+ */
+ H getRequestHttpClient();
+
+ /**
+ * 返回httpProxy.
+ *
+ * @return 返回httpProxy
+ */
+ P getRequestHttpProxy();
+
+ /**
+ * 返回HttpType.
+ *
+ * @return HttpType
+ */
+ HttpType getRequestType();
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/ResponseHandler.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/ResponseHandler.java
new file mode 100644
index 0000000000..1571764284
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/ResponseHandler.java
@@ -0,0 +1,19 @@
+package me.chanjar.weixin.common.util.http;
+
+/**
+ *
+ * http请求响应回调处理接口.
+ * Created by Binary Wang on 2018/12/8.
+ *
+ *
+ * @param 返回值类型
+ * @author Binary Wang
+ */
+public interface ResponseHandler {
+ /**
+ * 响应结果处理.
+ *
+ * @param t 要处理的对象
+ */
+ void handle(T t);
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimpleGetRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimpleGetRequestExecutor.java
index ec221e28b8..a0ce7a17e2 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimpleGetRequestExecutor.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimpleGetRequestExecutor.java
@@ -1,46 +1,51 @@
package me.chanjar.weixin.common.util.http;
-import me.chanjar.weixin.common.bean.result.WxError;
-import me.chanjar.weixin.common.exception.WxErrorException;
-import org.apache.http.HttpHost;
-import org.apache.http.client.config.RequestConfig;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.impl.client.CloseableHttpClient;
-
import java.io.IOException;
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.apache.ApacheSimpleGetRequestExecutor;
+import me.chanjar.weixin.common.util.http.jodd.JoddHttpSimpleGetRequestExecutor;
+import me.chanjar.weixin.common.util.http.okhttp.OkHttpSimpleGetRequestExecutor;
+
/**
- * 简单的GET请求执行器,请求的参数是String, 返回的结果也是String
+ * 简单的GET请求执行器.
+ * 请求的参数是String, 返回的结果也是String
*
* @author Daniel Qian
*/
-public class SimpleGetRequestExecutor implements RequestExecutor {
+public abstract class SimpleGetRequestExecutor implements RequestExecutor {
+ protected RequestHttp requestHttp;
+
+ public SimpleGetRequestExecutor(RequestHttp requestHttp) {
+ this.requestHttp = requestHttp;
+ }
@Override
- public String execute(CloseableHttpClient httpclient, HttpHost httpProxy, String uri, String queryParam) throws WxErrorException, IOException {
- if (queryParam != null) {
- if (uri.indexOf('?') == -1) {
- uri += '?';
- }
- uri += uri.endsWith("?") ? queryParam : '&' + queryParam;
- }
- HttpGet httpGet = new HttpGet(uri);
- if (httpProxy != null) {
- RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build();
- httpGet.setConfig(config);
- }
+ public void execute(String uri, String data, ResponseHandler handler, WxType wxType) throws WxErrorException, IOException {
+ handler.handle(this.execute(uri, data, wxType));
+ }
- try (CloseableHttpResponse response = httpclient.execute(httpGet)) {
- String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
- WxError error = WxError.fromJson(responseContent);
- if (error.getErrorCode() != 0) {
- throw new WxErrorException(error);
- }
- return responseContent;
- } finally {
- httpGet.releaseConnection();
+ public static RequestExecutor create(RequestHttp requestHttp) {
+ switch (requestHttp.getRequestType()) {
+ case APACHE_HTTP:
+ return new ApacheSimpleGetRequestExecutor(requestHttp);
+ case JODD_HTTP:
+ return new JoddHttpSimpleGetRequestExecutor(requestHttp);
+ case OK_HTTP:
+ return new OkHttpSimpleGetRequestExecutor(requestHttp);
+ default:
+ throw new IllegalArgumentException("非法请求参数");
}
}
+ protected String handleResponse(WxType wxType, String responseContent) throws WxErrorException {
+ WxError error = WxError.fromJson(responseContent, wxType);
+ if (error.getErrorCode() != 0) {
+ throw new WxErrorException(error);
+ }
+
+ return responseContent;
+ }
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimplePostRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimplePostRequestExecutor.java
index bdea8573c0..c0952b32d4 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimplePostRequestExecutor.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimplePostRequestExecutor.java
@@ -1,59 +1,61 @@
package me.chanjar.weixin.common.util.http;
-import java.io.IOException;
-
-import org.apache.http.Consts;
-import org.apache.http.HttpHost;
-import org.apache.http.client.config.RequestConfig;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.impl.client.CloseableHttpClient;
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.apache.ApacheSimplePostRequestExecutor;
+import me.chanjar.weixin.common.util.http.jodd.JoddHttpSimplePostRequestExecutor;
+import me.chanjar.weixin.common.util.http.okhttp.OkHttpSimplePostRequestExecutor;
+import org.jetbrains.annotations.NotNull;
-import me.chanjar.weixin.common.bean.result.WxError;
-import me.chanjar.weixin.common.exception.WxErrorException;
+import java.io.IOException;
/**
* 简单的POST请求执行器,请求的参数是String, 返回的结果也是String
*
* @author Daniel Qian
*/
-public class SimplePostRequestExecutor implements RequestExecutor {
+public abstract class SimplePostRequestExecutor implements RequestExecutor {
+ protected RequestHttp requestHttp;
+
+ public SimplePostRequestExecutor(RequestHttp requestHttp) {
+ this.requestHttp = requestHttp;
+ }
@Override
- public String execute(CloseableHttpClient httpclient, HttpHost httpProxy, String uri, String postEntity) throws WxErrorException, IOException {
- HttpPost httpPost = new HttpPost(uri);
- if (httpProxy != null) {
- RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build();
- httpPost.setConfig(config);
+ public void execute(String uri, String data, ResponseHandler handler, WxType wxType)
+ throws WxErrorException, IOException {
+ handler.handle(this.execute(uri, data, wxType));
+ }
+
+ public static RequestExecutor create(RequestHttp requestHttp) {
+ switch (requestHttp.getRequestType()) {
+ case APACHE_HTTP:
+ return new ApacheSimplePostRequestExecutor(requestHttp);
+ case JODD_HTTP:
+ return new JoddHttpSimplePostRequestExecutor(requestHttp);
+ case OK_HTTP:
+ return new OkHttpSimplePostRequestExecutor(requestHttp);
+ default:
+ throw new IllegalArgumentException("非法请求参数");
}
+ }
- if (postEntity != null) {
- StringEntity entity = new StringEntity(postEntity, Consts.UTF_8);
- httpPost.setEntity(entity);
+ @NotNull
+ public String handleResponse(WxType wxType, String responseContent) throws WxErrorException {
+ if (responseContent.isEmpty()) {
+ throw new WxErrorException(WxError.builder().errorCode(9999).errorMsg("无响应内容").build());
}
- try (CloseableHttpResponse response = httpclient.execute(httpPost)) {
- String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
- if (responseContent.isEmpty()) {
- throw new WxErrorException(
- WxError.newBuilder().setErrorCode(9999).setErrorMsg("无响应内容")
- .build());
- }
-
- if (responseContent.startsWith("")) {
- //xml格式输出直接返回
- return responseContent;
- }
-
- WxError error = WxError.fromJson(responseContent);
- if (error.getErrorCode() != 0) {
- throw new WxErrorException(error);
- }
+ if (responseContent.startsWith("")) {
+ //xml格式输出直接返回
return responseContent;
- } finally {
- httpPost.releaseConnection();
}
- }
+ WxError error = WxError.fromJson(responseContent, wxType);
+ if (error.getErrorCode() != 0) {
+ throw new WxErrorException(error);
+ }
+ return responseContent;
+ }
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/URIUtil.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/URIUtil.java
index 7e42aba72e..4e45c65d69 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/URIUtil.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/URIUtil.java
@@ -1,9 +1,9 @@
package me.chanjar.weixin.common.util.http;
-import org.apache.commons.lang3.StringUtils;
+import java.nio.charset.StandardCharsets;
-import java.io.UnsupportedEncodingException;
+import org.apache.commons.lang3.StringUtils;
public class URIUtil {
@@ -16,27 +16,22 @@ public static String encodeURIComponent(String input) {
int l = input.length();
StringBuilder o = new StringBuilder(l * 3);
- try {
- for (int i = 0; i < l; i++) {
- String e = input.substring(i, i + 1);
- if (ALLOWED_CHARS.indexOf(e) == -1) {
- byte[] b = e.getBytes("utf-8");
- o.append(getHex(b));
- continue;
- }
- o.append(e);
+ for (int i = 0; i < l; i++) {
+ String e = input.substring(i, i + 1);
+ if (!ALLOWED_CHARS.contains(e)) {
+ byte[] b = e.getBytes(StandardCharsets.UTF_8);
+ o.append(getHex(b));
+ continue;
}
- return o.toString();
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
+ o.append(e);
}
- return input;
+ return o.toString();
}
- private static String getHex(byte buf[]) {
+ private static String getHex(byte[] buf) {
StringBuilder o = new StringBuilder(buf.length * 3);
- for (int i = 0; i < buf.length; i++) {
- int n = buf[i] & 0xff;
+ for (byte aBuf : buf) {
+ int n = aBuf & 0xff;
o.append("%");
if (n < 0x10) {
o.append("0");
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/WxDnsResolver.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/WxDnsResolver.java
new file mode 100644
index 0000000000..6c6137089a
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/WxDnsResolver.java
@@ -0,0 +1,61 @@
+package me.chanjar.weixin.common.util.http;
+
+import org.apache.http.conn.DnsResolver;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 微信DNS域名解析器,将微信域名绑定到指定IP
+ * --------------------------------------------
+ * 适用于服务器端调用微信服务器需要开通出口防火墙情况
+ *
+ * Created by Andy Huo on 17/03/28.
+ */
+public class WxDnsResolver implements DnsResolver {
+
+ private final static String WECHAT_API_URL = "api.weixin.qq.com";
+ private static Map MAPPINGS = new HashMap();
+ protected final Logger log = LoggerFactory.getLogger(WxDnsResolver.class);
+ private String wxApiIp;
+
+ public WxDnsResolver(String ip) {
+
+ this.wxApiIp = ip;
+ this.init();
+ }
+
+ private void init() {
+ if (log.isDebugEnabled()) {
+ log.debug("init wechat dns config with ip {}", wxApiIp);
+ }
+ try {
+ MAPPINGS.put(WECHAT_API_URL, new InetAddress[]{InetAddress.getByName(wxApiIp)});
+ } catch (UnknownHostException e) {
+ //如果初始化DNS配置失败则使用默认配置,不影响服务的启动
+ log.error("init WxDnsResolver error", e);
+ MAPPINGS = new HashMap();
+ }
+
+ }
+
+ @Override
+ public InetAddress[] resolve(String host) throws UnknownHostException {
+
+
+ return MAPPINGS.containsKey(host) ? MAPPINGS.get(host) : new InetAddress[0];
+ }
+
+ public String getWxApiIp() {
+ return wxApiIp;
+ }
+
+ public void setWxApiIp(String wxApiIp) {
+ this.wxApiIp = wxApiIp;
+ this.init();
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/ApacheHttpClientBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpClientBuilder.java
similarity index 63%
rename from weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/ApacheHttpClientBuilder.java
rename to weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpClientBuilder.java
index c932445d33..fcd56c48a7 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/ApacheHttpClientBuilder.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpClientBuilder.java
@@ -1,53 +1,44 @@
-package me.chanjar.weixin.common.util.http;
+package me.chanjar.weixin.common.util.http.apache;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
/**
- * httpclient build interface
+ * httpclient build interface.
+ *
* @author kakotor
*/
public interface ApacheHttpClientBuilder {
/**
- * 构建httpclient实例
+ * 构建httpclient实例.
*
* @return new instance of CloseableHttpClient
*/
CloseableHttpClient build();
/**
- * 代理服务器地址
- *
- * @param httpProxyHost
+ * 代理服务器地址.
*/
ApacheHttpClientBuilder httpProxyHost(String httpProxyHost);
/**
- * 代理服务器端口
- *
- * @param httpProxyPort
+ * 代理服务器端口.
*/
ApacheHttpClientBuilder httpProxyPort(int httpProxyPort);
/**
- * 代理服务器用户名
- *
- * @param httpProxyUsername
+ * 代理服务器用户名.
*/
ApacheHttpClientBuilder httpProxyUsername(String httpProxyUsername);
/**
- * 代理服务器密码
- *
- * @param httpProxyPassword
+ * 代理服务器密码.
*/
ApacheHttpClientBuilder httpProxyPassword(String httpProxyPassword);
/**
- * ssl连接socket工厂
- *
- * @param sslConnectionSocketFactory
+ * ssl连接socket工厂.
*/
ApacheHttpClientBuilder sslConnectionSocketFactory(SSLConnectionSocketFactory sslConnectionSocketFactory);
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/DefaultApacheHttpClientBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpDnsClientBuilder.java
similarity index 71%
rename from weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/DefaultApacheHttpClientBuilder.java
rename to weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpDnsClientBuilder.java
index 030fe98fe2..fe5472f3c0 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/DefaultApacheHttpClientBuilder.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpDnsClientBuilder.java
@@ -1,6 +1,11 @@
-package me.chanjar.weixin.common.util.http;
+package me.chanjar.weixin.common.util.http.apache;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.lang3.StringUtils;
+import org.apache.http.HttpHost;
import org.apache.http.annotation.NotThreadSafe;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
@@ -10,6 +15,7 @@
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.config.SocketConfig;
+import org.apache.http.conn.DnsResolver;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
@@ -23,18 +29,15 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.IOException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-
/**
- * httpclient 连接管理器
+ * httpclient 连接管理器 自带DNS解析.
+ * 大部分代码拷贝自:DefaultApacheHttpClientBuilder
*
- * @author kakotor
+ * @author Andy.Huo
*/
@NotThreadSafe
-public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder {
- protected final Logger log = LoggerFactory.getLogger(DefaultApacheHttpClientBuilder.class);
+public class ApacheHttpDnsClientBuilder implements ApacheHttpClientBuilder {
+ protected final Logger log = LoggerFactory.getLogger(ApacheHttpDnsClientBuilder.class);
private final AtomicBoolean prepared = new AtomicBoolean(false);
private int connectionRequestTimeout = 3000;
private int connectionTimeout = 5000;
@@ -44,6 +47,9 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder {
private int maxConnPerHost = 10;
private int maxTotalConn = 50;
private String userAgent;
+
+ private DnsResolver dnsResover;
+
private HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() {
@Override
public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
@@ -56,17 +62,18 @@ public boolean retryRequest(IOException exception, int executionCount, HttpConte
private int httpProxyPort;
private String httpProxyUsername;
private String httpProxyPassword;
+
/**
- * 闲置连接监控线程
+ * 闲置连接监控线程.
*/
private IdleConnectionMonitorThread idleConnectionMonitorThread;
private HttpClientBuilder httpClientBuilder;
- private DefaultApacheHttpClientBuilder() {
+ private ApacheHttpDnsClientBuilder() {
}
- public static DefaultApacheHttpClientBuilder get() {
- return new DefaultApacheHttpClientBuilder();
+ public static ApacheHttpDnsClientBuilder get() {
+ return new ApacheHttpDnsClientBuilder();
}
@Override
@@ -102,8 +109,7 @@ public ApacheHttpClientBuilder sslConnectionSocketFactory(SSLConnectionSocketFac
/**
* 获取链接的超时时间设置,默认3000ms
*
- * 设置为零时不超时,一直等待.
- * 设置为负数是使用系统默认设置(非上述的3000ms的默认值,而是httpclient的默认设置).
+ * 设置为零时不超时,一直等待. 设置为负数是使用系统默认设置(非上述的3000ms的默认值,而是httpclient的默认设置).
*
*
* @param connectionRequestTimeout 获取链接的超时时间设置(单位毫秒),默认3000ms
@@ -115,8 +121,7 @@ public void setConnectionRequestTimeout(int connectionRequestTimeout) {
/**
* 建立链接的超时时间,默认为5000ms.由于是在链接池获取链接,此设置应该并不起什么作用
*
- * 设置为零时不超时,一直等待.
- * 设置为负数是使用系统默认设置(非上述的5000ms的默认值,而是httpclient的默认设置).
+ * 设置为零时不超时,一直等待. 设置为负数是使用系统默认设置(非上述的5000ms的默认值,而是httpclient的默认设置).
*
*
* @param connectionTimeout 建立链接的超时时间设置(单位毫秒),默认5000ms
@@ -157,7 +162,7 @@ public void setCheckWaitTime(int checkWaitTime) {
}
/**
- * 每路的最大链接数,默认10
+ * 每路的最大链接数,默认10.
*
* @param maxConnPerHost 每路的最大链接数,默认10
*/
@@ -166,7 +171,7 @@ public void setMaxConnPerHost(int maxConnPerHost) {
}
/**
- * 最大总连接数,默认50
+ * 最大总连接数,默认50.
*
* @param maxTotalConn 最大总连接数,默认50
*/
@@ -175,7 +180,7 @@ public void setMaxTotalConn(int maxTotalConn) {
}
/**
- * 自定义httpclient的User Agent
+ * 自定义httpclient的User Agent.
*
* @param userAgent User Agent
*/
@@ -188,50 +193,54 @@ public IdleConnectionMonitorThread getIdleConnectionMonitorThread() {
}
private synchronized void prepare() {
- if(prepared.get()){
+ if (prepared.get()) {
return;
}
- Registry registry = RegistryBuilder.create()
- .register("http", this.plainConnectionSocketFactory)
- .register("https", this.sslConnectionSocketFactory)
- .build();
+
+ Registry registry =
+ RegistryBuilder.create()
+ .register("http", this.plainConnectionSocketFactory)
+ .register("https", this.sslConnectionSocketFactory)
+ .build();
@SuppressWarnings("resource")
- PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
+ PoolingHttpClientConnectionManager connectionManager;
+ if (dnsResover != null) {
+ if (log.isDebugEnabled()) {
+ log.debug("specified dns resolver.");
+ }
+ connectionManager = new PoolingHttpClientConnectionManager(registry, dnsResover);
+ } else {
+ if (log.isDebugEnabled()) {
+ log.debug("Not specified dns resolver.");
+ }
+ connectionManager = new PoolingHttpClientConnectionManager(registry);
+ }
+
connectionManager.setMaxTotal(this.maxTotalConn);
connectionManager.setDefaultMaxPerRoute(this.maxConnPerHost);
- connectionManager.setDefaultSocketConfig(
- SocketConfig.copy(SocketConfig.DEFAULT)
- .setSoTimeout(this.soTimeout)
- .build()
- );
+ connectionManager
+ .setDefaultSocketConfig(SocketConfig.copy(SocketConfig.DEFAULT).setSoTimeout(this.soTimeout).build());
this.idleConnectionMonitorThread = new IdleConnectionMonitorThread(
connectionManager, this.idleConnTimeout, this.checkWaitTime);
this.idleConnectionMonitorThread.setDaemon(true);
this.idleConnectionMonitorThread.start();
- this.httpClientBuilder = HttpClients.custom()
- .setConnectionManager(connectionManager)
+ this.httpClientBuilder = HttpClients.custom().setConnectionManager(connectionManager)
.setConnectionManagerShared(true)
- .setDefaultRequestConfig(
- RequestConfig.custom()
- .setSocketTimeout(this.soTimeout)
- .setConnectTimeout(this.connectionTimeout)
- .setConnectionRequestTimeout(this.connectionRequestTimeout)
- .build()
- )
+ .setDefaultRequestConfig(RequestConfig.custom().setSocketTimeout(this.soTimeout)
+ .setConnectTimeout(this.connectionTimeout)
+ .setConnectionRequestTimeout(this.connectionRequestTimeout).build())
.setRetryHandler(this.httpRequestRetryHandler);
- if (StringUtils.isNotBlank(this.httpProxyHost)
- && StringUtils.isNotBlank(this.httpProxyUsername)) {
+ if (StringUtils.isNotBlank(this.httpProxyHost) && StringUtils.isNotBlank(this.httpProxyUsername)) {
// 使用代理服务器 需要用户认证的代理服务器
CredentialsProvider provider = new BasicCredentialsProvider();
- provider.setCredentials(
- new AuthScope(this.httpProxyHost, this.httpProxyPort),
- new UsernamePasswordCredentials(this.httpProxyUsername,
- this.httpProxyPassword));
+ provider.setCredentials(new AuthScope(this.httpProxyHost, this.httpProxyPort),
+ new UsernamePasswordCredentials(this.httpProxyUsername, this.httpProxyPassword));
this.httpClientBuilder.setDefaultCredentialsProvider(provider);
+ this.httpClientBuilder.setProxy(new HttpHost(this.httpProxyHost, this.httpProxyPort));
}
if (StringUtils.isNotBlank(this.userAgent)) {
@@ -242,18 +251,29 @@ private synchronized void prepare() {
@Override
public CloseableHttpClient build() {
- if(!prepared.get()){
+ if (!prepared.get()) {
prepare();
}
return this.httpClientBuilder.build();
}
+ public DnsResolver getDnsResover() {
+ return dnsResover;
+ }
+
+ public void setDnsResover(DnsResolver dnsResover) {
+ this.dnsResover = dnsResover;
+ }
+
public static class IdleConnectionMonitorThread extends Thread {
private final HttpClientConnectionManager connMgr;
private final int idleConnTimeout;
private final int checkWaitTime;
private volatile boolean shutdown;
+ /**
+ * 构造方法.
+ */
public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr, int idleConnTimeout, int checkWaitTime) {
super("IdleConnectionMonitorThread");
this.connMgr = connMgr;
@@ -268,20 +288,26 @@ public void run() {
synchronized (this) {
wait(this.checkWaitTime);
this.connMgr.closeExpiredConnections();
- this.connMgr.closeIdleConnections(this.idleConnTimeout,
- TimeUnit.MILLISECONDS);
+ this.connMgr.closeIdleConnections(this.idleConnTimeout, TimeUnit.MILLISECONDS);
}
}
} catch (InterruptedException ignore) {
+ Thread.currentThread().interrupt();
}
}
+ /**
+ * 触发.
+ */
public void trigger() {
synchronized (this) {
notifyAll();
}
}
+ /**
+ * 关闭.
+ */
public void shutdown() {
this.shutdown = true;
synchronized (this) {
@@ -289,4 +315,5 @@ public void shutdown() {
}
}
}
+
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaDownloadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaDownloadRequestExecutor.java
new file mode 100644
index 0000000000..5bb4aeba90
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaDownloadRequestExecutor.java
@@ -0,0 +1,79 @@
+package me.chanjar.weixin.common.util.http.apache;
+
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.fs.FileUtils;
+import me.chanjar.weixin.common.util.http.BaseMediaDownloadRequestExecutor;
+import me.chanjar.weixin.common.util.http.HttpResponseProxy;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.Header;
+import org.apache.http.HttpHost;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.entity.ContentType;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * .
+ *
+ * @author ecoolper
+ * @date 2017/5/5
+ */
+public class ApacheMediaDownloadRequestExecutor extends BaseMediaDownloadRequestExecutor {
+ public ApacheMediaDownloadRequestExecutor(RequestHttp requestHttp, File tmpDirFile) {
+ super(requestHttp, tmpDirFile);
+ }
+
+ @Override
+ public File execute(String uri, String queryParam, WxType wxType) throws WxErrorException, IOException {
+ if (queryParam != null) {
+ if (uri.indexOf('?') == -1) {
+ uri += '?';
+ }
+ uri += uri.endsWith("?") ? queryParam : '&' + queryParam;
+ }
+
+ HttpGet httpGet = new HttpGet(uri);
+ if (requestHttp.getRequestHttpProxy() != null) {
+ RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build();
+ httpGet.setConfig(config);
+ }
+
+ try (CloseableHttpResponse response = requestHttp.getRequestHttpClient().execute(httpGet);
+ InputStream inputStream = InputStreamResponseHandler.INSTANCE.handleResponse(response)) {
+ Header[] contentTypeHeader = response.getHeaders("Content-Type");
+ if (contentTypeHeader != null && contentTypeHeader.length > 0) {
+ if (contentTypeHeader[0].getValue().startsWith(ContentType.APPLICATION_JSON.getMimeType())) {
+ // application/json; encoding=utf-8 下载媒体文件出错
+ String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
+ throw new WxErrorException(WxError.fromJson(responseContent, wxType));
+ }
+ }
+
+ String fileName = new HttpResponseProxy(response).getFileName();
+ if (StringUtils.isBlank(fileName)) {
+ fileName = String.valueOf(System.currentTimeMillis());
+ }
+
+ String baseName = FilenameUtils.getBaseName(fileName);
+ if (StringUtils.isBlank(fileName) || baseName.length() < 3) {
+ baseName = String.valueOf(System.currentTimeMillis());
+ }
+
+ return FileUtils.createTmpFile(inputStream, baseName, FilenameUtils.getExtension(fileName),
+ super.tmpDirFile);
+
+ } finally {
+ httpGet.releaseConnection();
+ }
+ }
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaUploadRequestExecutor.java
new file mode 100644
index 0000000000..ca33b8641f
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaUploadRequestExecutor.java
@@ -0,0 +1,55 @@
+package me.chanjar.weixin.common.util.http.apache;
+
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHost;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.mime.HttpMultipartMode;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Created by ecoolper on 2017/5/5.
+ */
+public class ApacheMediaUploadRequestExecutor extends MediaUploadRequestExecutor {
+ public ApacheMediaUploadRequestExecutor(RequestHttp requestHttp) {
+ super(requestHttp);
+ }
+
+ @Override
+ public WxMediaUploadResult execute(String uri, File file, WxType wxType) throws WxErrorException, IOException {
+ HttpPost httpPost = new HttpPost(uri);
+ if (requestHttp.getRequestHttpProxy() != null) {
+ RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build();
+ httpPost.setConfig(config);
+ }
+ if (file != null) {
+ HttpEntity entity = MultipartEntityBuilder
+ .create()
+ .addBinaryBody("media", file)
+ .setMode(HttpMultipartMode.RFC6532)
+ .build();
+ httpPost.setEntity(entity);
+ }
+ try (CloseableHttpResponse response = requestHttp.getRequestHttpClient().execute(httpPost)) {
+ String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
+ WxError error = WxError.fromJson(responseContent, wxType);
+ if (error.getErrorCode() != 0) {
+ throw new WxErrorException(error);
+ }
+ return WxMediaUploadResult.fromJson(responseContent);
+ } finally {
+ httpPost.releaseConnection();
+ }
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheSimpleGetRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheSimpleGetRequestExecutor.java
new file mode 100644
index 0000000000..6b365edd09
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheSimpleGetRequestExecutor.java
@@ -0,0 +1,48 @@
+package me.chanjar.weixin.common.util.http.apache;
+
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor;
+import org.apache.http.HttpHost;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import java.io.IOException;
+
+/**
+ * .
+ *
+ * @author ecoolper
+ * @date 2017/5/4
+ */
+public class ApacheSimpleGetRequestExecutor extends SimpleGetRequestExecutor {
+ public ApacheSimpleGetRequestExecutor(RequestHttp requestHttp) {
+ super(requestHttp);
+ }
+
+ @Override
+ public String execute(String uri, String queryParam, WxType wxType) throws WxErrorException, IOException {
+ if (queryParam != null) {
+ if (uri.indexOf('?') == -1) {
+ uri += '?';
+ }
+ uri += uri.endsWith("?") ? queryParam : '&' + queryParam;
+ }
+ HttpGet httpGet = new HttpGet(uri);
+ if (requestHttp.getRequestHttpProxy() != null) {
+ RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build();
+ httpGet.setConfig(config);
+ }
+
+ try (CloseableHttpResponse response = requestHttp.getRequestHttpClient().execute(httpGet)) {
+ String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
+ return handleResponse(wxType, responseContent);
+ } finally {
+ httpGet.releaseConnection();
+ }
+ }
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheSimplePostRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheSimplePostRequestExecutor.java
new file mode 100644
index 0000000000..4e6f31ec67
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheSimplePostRequestExecutor.java
@@ -0,0 +1,50 @@
+package me.chanjar.weixin.common.util.http.apache;
+
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor;
+import org.apache.http.Consts;
+import org.apache.http.HttpHost;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import java.io.IOException;
+
+/**
+ * .
+ *
+ * @author ecoolper
+ * @date 2017/5/4
+ */
+public class ApacheSimplePostRequestExecutor extends SimplePostRequestExecutor {
+ public ApacheSimplePostRequestExecutor(RequestHttp requestHttp) {
+ super(requestHttp);
+ }
+
+ @Override
+ public String execute(String uri, String postEntity, WxType wxType) throws WxErrorException, IOException {
+ HttpPost httpPost = new HttpPost(uri);
+ if (requestHttp.getRequestHttpProxy() != null) {
+ RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build();
+ httpPost.setConfig(config);
+ }
+
+ if (postEntity != null) {
+ StringEntity entity = new StringEntity(postEntity, Consts.UTF_8);
+ entity.setContentType("application/json; charset=utf-8");
+ httpPost.setEntity(entity);
+ }
+
+ try (CloseableHttpResponse response = requestHttp.getRequestHttpClient().execute(httpPost)) {
+ String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
+ return this.handleResponse(wxType, responseContent);
+ } finally {
+ httpPost.releaseConnection();
+ }
+ }
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilder.java
new file mode 100644
index 0000000000..3fb08ab2c6
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilder.java
@@ -0,0 +1,288 @@
+package me.chanjar.weixin.common.util.http.apache;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.HttpHost;
+import org.apache.http.annotation.NotThreadSafe;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.HttpRequestRetryHandler;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.config.Registry;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.config.SocketConfig;
+import org.apache.http.conn.HttpClientConnectionManager;
+import org.apache.http.conn.socket.ConnectionSocketFactory;
+import org.apache.http.conn.socket.PlainConnectionSocketFactory;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustStrategy;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.ssl.SSLContexts;
+
+import javax.net.ssl.SSLContext;
+import java.io.IOException;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * httpclient 连接管理器
+ *
+ * @author kakotor
+ */
+@Slf4j
+@Data
+@NotThreadSafe
+public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder {
+ private final AtomicBoolean prepared = new AtomicBoolean(false);
+
+ /**
+ * 获取链接的超时时间设置
+ *
+ * 设置为零时不超时,一直等待.
+ * 设置为负数是使用系统默认设置(非3000ms的默认值,而是httpClient的默认设置).
+ *
+ */
+ private int connectionRequestTimeout = -1;
+
+ /**
+ * 建立链接的超时时间,默认为5000ms.由于是在链接池获取链接,此设置应该并不起什么作用
+ *
+ * 设置为零时不超时,一直等待.
+ * 设置为负数是使用系统默认设置(非上述的5000ms的默认值,而是httpclient的默认设置).
+ *
+ */
+ private int connectionTimeout = 5000;
+ /**
+ * 默认NIO的socket超时设置,默认5000ms.
+ */
+ private int soTimeout = 5000;
+ /**
+ * 空闲链接的超时时间,默认60000ms.
+ *
+ * 超时的链接将在下一次空闲链接检查是被销毁
+ *
+ */
+ private int idleConnTimeout = 60000;
+ /**
+ * 检查空间链接的间隔周期,默认60000ms.
+ */
+ private int checkWaitTime = 60000;
+ /**
+ * 每路的最大链接数,默认10
+ */
+ private int maxConnPerHost = 10;
+ /**
+ * 最大总连接数,默认50
+ */
+ private int maxTotalConn = 50;
+ /**
+ * 自定义httpclient的User Agent
+ */
+ private String userAgent;
+
+ private final HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() {
+ @Override
+ public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
+ return false;
+ }
+ };
+ private SSLConnectionSocketFactory sslConnectionSocketFactory = SSLConnectionSocketFactory.getSocketFactory();
+ private final PlainConnectionSocketFactory plainConnectionSocketFactory = PlainConnectionSocketFactory.getSocketFactory();
+ private String httpProxyHost;
+ private int httpProxyPort;
+ private String httpProxyUsername;
+ private String httpProxyPassword;
+ /**
+ * 闲置连接监控线程
+ */
+ private IdleConnectionMonitorThread idleConnectionMonitorThread;
+ /**
+ * 持有client对象,仅初始化一次,避免多service实例的时候造成重复初始化的问题
+ */
+ private CloseableHttpClient closeableHttpClient;
+
+ private DefaultApacheHttpClientBuilder() {
+ }
+
+ public static DefaultApacheHttpClientBuilder get() {
+ return DefaultApacheHttpClientBuilder.SingletonHolder.INSTANCE;
+ }
+
+ @Override
+ public ApacheHttpClientBuilder httpProxyHost(String httpProxyHost) {
+ this.httpProxyHost = httpProxyHost;
+ return this;
+ }
+
+ @Override
+ public ApacheHttpClientBuilder httpProxyPort(int httpProxyPort) {
+ this.httpProxyPort = httpProxyPort;
+ return this;
+ }
+
+ @Override
+ public ApacheHttpClientBuilder httpProxyUsername(String httpProxyUsername) {
+ this.httpProxyUsername = httpProxyUsername;
+ return this;
+ }
+
+ @Override
+ public ApacheHttpClientBuilder httpProxyPassword(String httpProxyPassword) {
+ this.httpProxyPassword = httpProxyPassword;
+ return this;
+ }
+
+ @Override
+ public ApacheHttpClientBuilder sslConnectionSocketFactory(SSLConnectionSocketFactory sslConnectionSocketFactory) {
+ this.sslConnectionSocketFactory = sslConnectionSocketFactory;
+ return this;
+ }
+
+ public IdleConnectionMonitorThread getIdleConnectionMonitorThread() {
+ return this.idleConnectionMonitorThread;
+ }
+
+ private synchronized void prepare() {
+ if (prepared.get()) {
+ return;
+ }
+ Registry registry = RegistryBuilder.create()
+ .register("http", this.plainConnectionSocketFactory)
+ .register("https", this.sslConnectionSocketFactory)
+ .build();
+
+ @SuppressWarnings("resource")
+ PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
+ connectionManager.setMaxTotal(this.maxTotalConn);
+ connectionManager.setDefaultMaxPerRoute(this.maxConnPerHost);
+ connectionManager.setDefaultSocketConfig(
+ SocketConfig.copy(SocketConfig.DEFAULT)
+ .setSoTimeout(this.soTimeout)
+ .build()
+ );
+
+ this.idleConnectionMonitorThread = new IdleConnectionMonitorThread(
+ connectionManager, this.idleConnTimeout, this.checkWaitTime);
+ this.idleConnectionMonitorThread.setDaemon(true);
+ this.idleConnectionMonitorThread.start();
+
+ HttpClientBuilder httpClientBuilder = HttpClients.custom()
+ .setConnectionManager(connectionManager)
+ .setConnectionManagerShared(true)
+ .setSSLSocketFactory(this.buildSSLConnectionSocketFactory())
+ .setDefaultRequestConfig(RequestConfig.custom()
+ .setSocketTimeout(this.soTimeout)
+ .setConnectTimeout(this.connectionTimeout)
+ .setConnectionRequestTimeout(this.connectionRequestTimeout)
+ .build()
+ ).setRetryHandler(this.httpRequestRetryHandler);
+
+ if (StringUtils.isNotBlank(this.httpProxyHost) && StringUtils.isNotBlank(this.httpProxyUsername)) {
+ // 使用代理服务器 需要用户认证的代理服务器
+ CredentialsProvider provider = new BasicCredentialsProvider();
+ provider.setCredentials(new AuthScope(this.httpProxyHost, this.httpProxyPort),
+ new UsernamePasswordCredentials(this.httpProxyUsername, this.httpProxyPassword));
+ httpClientBuilder.setDefaultCredentialsProvider(provider);
+ httpClientBuilder.setProxy(new HttpHost(this.httpProxyHost, this.httpProxyPort));
+ }
+
+ if (StringUtils.isNotBlank(this.userAgent)) {
+ httpClientBuilder.setUserAgent(this.userAgent);
+ }
+
+ this.closeableHttpClient = httpClientBuilder.build();
+ prepared.set(true);
+ }
+
+ private SSLConnectionSocketFactory buildSSLConnectionSocketFactory() {
+ try {
+ SSLContext sslcontext = SSLContexts.custom()
+ //忽略掉对服务器端证书的校验
+ .loadTrustMaterial(new TrustStrategy() {
+ @Override
+ public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ return true;
+ }
+ }).build();
+
+ return new SSLConnectionSocketFactory(
+ sslcontext,
+ new String[]{"TLSv1"},
+ null,
+ SSLConnectionSocketFactory.getDefaultHostnameVerifier());
+ } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
+ log.error("构建SSL连接工厂时发生异常!", e);
+ }
+
+ return null;
+ }
+
+ @Override
+ public CloseableHttpClient build() {
+ if (!prepared.get()) {
+ prepare();
+ }
+ return this.closeableHttpClient;
+ }
+
+ /**
+ * DefaultApacheHttpClientBuilder 改为单例模式,并持有唯一的CloseableHttpClient(仅首次调用创建)
+ */
+ private static class SingletonHolder {
+ private static final DefaultApacheHttpClientBuilder INSTANCE = new DefaultApacheHttpClientBuilder();
+ }
+
+ public static class IdleConnectionMonitorThread extends Thread {
+ private final HttpClientConnectionManager connMgr;
+ private final int idleConnTimeout;
+ private final int checkWaitTime;
+ private volatile boolean shutdown;
+
+ public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr, int idleConnTimeout, int checkWaitTime) {
+ super("IdleConnectionMonitorThread");
+ this.connMgr = connMgr;
+ this.idleConnTimeout = idleConnTimeout;
+ this.checkWaitTime = checkWaitTime;
+ }
+
+ @Override
+ public void run() {
+ try {
+ while (!this.shutdown) {
+ synchronized (this) {
+ wait(this.checkWaitTime);
+ this.connMgr.closeExpiredConnections();
+ this.connMgr.closeIdleConnections(this.idleConnTimeout,
+ TimeUnit.MILLISECONDS);
+ }
+ }
+ } catch (InterruptedException ignore) {
+ }
+ }
+
+ public void trigger() {
+ synchronized (this) {
+ notifyAll();
+ }
+ }
+
+ public void shutdown() {
+ this.shutdown = true;
+ synchronized (this) {
+ notifyAll();
+ }
+ }
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/InputStreamResponseHandler.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/InputStreamResponseHandler.java
similarity index 80%
rename from weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/InputStreamResponseHandler.java
rename to weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/InputStreamResponseHandler.java
index 0cc1562b59..5c72744cb0 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/InputStreamResponseHandler.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/InputStreamResponseHandler.java
@@ -1,4 +1,7 @@
-package me.chanjar.weixin.common.util.http;
+package me.chanjar.weixin.common.util.http.apache;
+
+import java.io.IOException;
+import java.io.InputStream;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
@@ -7,18 +10,20 @@
import org.apache.http.client.ResponseHandler;
import org.apache.http.util.EntityUtils;
-import java.io.IOException;
-import java.io.InputStream;
-
+/**
+ * 输入流响应处理器.
+ *
+ * @author Daniel Qian
+ */
public class InputStreamResponseHandler implements ResponseHandler {
-
public static final ResponseHandler INSTANCE = new InputStreamResponseHandler();
+ private static final int STATUS_CODE_300 = 300;
@Override
public InputStream handleResponse(final HttpResponse response) throws IOException {
final StatusLine statusLine = response.getStatusLine();
final HttpEntity entity = response.getEntity();
- if (statusLine.getStatusCode() >= 300) {
+ if (statusLine.getStatusCode() >= STATUS_CODE_300) {
EntityUtils.consume(entity);
throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/Utf8ResponseHandler.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/Utf8ResponseHandler.java
similarity index 92%
rename from weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/Utf8ResponseHandler.java
rename to weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/Utf8ResponseHandler.java
index 148ef9650d..035726d44f 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/Utf8ResponseHandler.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/Utf8ResponseHandler.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.common.util.http;
+package me.chanjar.weixin.common.util.http.apache;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
@@ -25,7 +25,7 @@ public String handleResponse(final HttpResponse response) throws IOException {
final HttpEntity entity = response.getEntity();
if (statusLine.getStatusCode() >= 300) {
EntityUtils.consume(entity);
- throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
+ throw new HttpResponseException(statusLine.getStatusCode(), statusLine.toString());
}
return entity == null ? null : EntityUtils.toString(entity, Consts.UTF_8);
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaDownloadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaDownloadRequestExecutor.java
new file mode 100644
index 0000000000..5c67cbffe1
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaDownloadRequestExecutor.java
@@ -0,0 +1,77 @@
+package me.chanjar.weixin.common.util.http.jodd;
+
+import jodd.http.HttpConnectionProvider;
+import jodd.http.HttpRequest;
+import jodd.http.HttpResponse;
+import jodd.http.ProxyInfo;
+import jodd.util.StringPool;
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.fs.FileUtils;
+import me.chanjar.weixin.common.util.http.BaseMediaDownloadRequestExecutor;
+import me.chanjar.weixin.common.util.http.HttpResponseProxy;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * .
+ *
+ * @author ecoolper
+ * @date 2017/5/5
+ */
+public class JoddHttpMediaDownloadRequestExecutor extends BaseMediaDownloadRequestExecutor {
+ public JoddHttpMediaDownloadRequestExecutor(RequestHttp requestHttp, File tmpDirFile) {
+ super(requestHttp, tmpDirFile);
+ }
+
+ @Override
+ public File execute(String uri, String queryParam, WxType wxType) throws WxErrorException, IOException {
+ if (queryParam != null) {
+ if (uri.indexOf('?') == -1) {
+ uri += '?';
+ }
+ uri += uri.endsWith("?") ? queryParam : '&' + queryParam;
+ }
+
+ HttpRequest request = HttpRequest.get(uri);
+ if (requestHttp.getRequestHttpProxy() != null) {
+ requestHttp.getRequestHttpClient().useProxy(requestHttp.getRequestHttpProxy());
+ }
+ request.withConnectionProvider(requestHttp.getRequestHttpClient());
+
+ HttpResponse response = request.send();
+ response.charset(StringPool.UTF_8);
+
+ String contentType = response.header("Content-Type");
+ if (contentType != null && contentType.startsWith("application/json")) {
+ // application/json; encoding=utf-8 下载媒体文件出错
+ throw new WxErrorException(WxError.fromJson(response.bodyText(), wxType));
+ }
+
+ String fileName = new HttpResponseProxy(response).getFileName();
+ if (StringUtils.isBlank(fileName)) {
+ return null;
+ }
+
+ String baseName = FilenameUtils.getBaseName(fileName);
+ if (StringUtils.isBlank(fileName) || baseName.length() < 3) {
+ baseName = String.valueOf(System.currentTimeMillis());
+ }
+
+ try (InputStream inputStream = new ByteArrayInputStream(response.bodyBytes())) {
+ return FileUtils.createTmpFile(inputStream,
+ baseName,
+ FilenameUtils.getExtension(fileName),
+ super.tmpDirFile);
+ }
+ }
+
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaUploadRequestExecutor.java
new file mode 100644
index 0000000000..93f25fb740
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaUploadRequestExecutor.java
@@ -0,0 +1,47 @@
+package me.chanjar.weixin.common.util.http.jodd;
+
+import jodd.http.HttpConnectionProvider;
+import jodd.http.HttpRequest;
+import jodd.http.HttpResponse;
+import jodd.http.ProxyInfo;
+import jodd.util.StringPool;
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * .
+ *
+ * @author ecoolper
+ * @date 2017/5/5
+ */
+public class JoddHttpMediaUploadRequestExecutor extends MediaUploadRequestExecutor {
+ public JoddHttpMediaUploadRequestExecutor(RequestHttp requestHttp) {
+ super(requestHttp);
+ }
+
+ @Override
+ public WxMediaUploadResult execute(String uri, File file, WxType wxType) throws WxErrorException, IOException {
+ HttpRequest request = HttpRequest.post(uri);
+ if (requestHttp.getRequestHttpProxy() != null) {
+ requestHttp.getRequestHttpClient().useProxy(requestHttp.getRequestHttpProxy());
+ }
+ request.withConnectionProvider(requestHttp.getRequestHttpClient());
+ request.form("media", file);
+ HttpResponse response = request.send();
+ response.charset(StringPool.UTF_8);
+
+ String responseContent = response.bodyText();
+ WxError error = WxError.fromJson(responseContent, wxType);
+ if (error.getErrorCode() != 0) {
+ throw new WxErrorException(error);
+ }
+ return WxMediaUploadResult.fromJson(responseContent);
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimpleGetRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimpleGetRequestExecutor.java
new file mode 100644
index 0000000000..e8adad6b66
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimpleGetRequestExecutor.java
@@ -0,0 +1,46 @@
+package me.chanjar.weixin.common.util.http.jodd;
+
+import jodd.http.HttpConnectionProvider;
+import jodd.http.HttpRequest;
+import jodd.http.HttpResponse;
+import jodd.http.ProxyInfo;
+import jodd.util.StringPool;
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor;
+
+import java.io.IOException;
+
+/**
+ * .
+ *
+ * @author ecoolper
+ * @date 2017/5/4
+ */
+public class JoddHttpSimpleGetRequestExecutor extends SimpleGetRequestExecutor {
+ public JoddHttpSimpleGetRequestExecutor(RequestHttp requestHttp) {
+ super(requestHttp);
+ }
+
+ @Override
+ public String execute(String uri, String queryParam, WxType wxType) throws WxErrorException, IOException {
+ if (queryParam != null) {
+ if (uri.indexOf('?') == -1) {
+ uri += '?';
+ }
+ uri += uri.endsWith("?") ? queryParam : '&' + queryParam;
+ }
+
+ HttpRequest request = HttpRequest.get(uri);
+ if (requestHttp.getRequestHttpProxy() != null) {
+ requestHttp.getRequestHttpClient().useProxy(requestHttp.getRequestHttpProxy());
+ }
+ request.withConnectionProvider(requestHttp.getRequestHttpClient());
+ HttpResponse response = request.send();
+ response.charset(StringPool.UTF_8);
+
+ return handleResponse(wxType, response.bodyText());
+ }
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimplePostRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimplePostRequestExecutor.java
new file mode 100644
index 0000000000..7b2f5f61ef
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimplePostRequestExecutor.java
@@ -0,0 +1,45 @@
+package me.chanjar.weixin.common.util.http.jodd;
+
+import jodd.http.HttpConnectionProvider;
+import jodd.http.HttpRequest;
+import jodd.http.HttpResponse;
+import jodd.http.ProxyInfo;
+import jodd.util.StringPool;
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor;
+
+import java.io.IOException;
+
+/**
+ * .
+ *
+ * @author ecoolper
+ * @date 2017/5/4
+ */
+public class JoddHttpSimplePostRequestExecutor extends SimplePostRequestExecutor {
+ public JoddHttpSimplePostRequestExecutor(RequestHttp requestHttp) {
+ super(requestHttp);
+ }
+
+ @Override
+ public String execute(String uri, String postEntity, WxType wxType) throws WxErrorException, IOException {
+ HttpConnectionProvider provider = requestHttp.getRequestHttpClient();
+ ProxyInfo proxyInfo = requestHttp.getRequestHttpProxy();
+
+ HttpRequest request = HttpRequest.post(uri);
+ if (proxyInfo != null) {
+ provider.useProxy(proxyInfo);
+ }
+ request.withConnectionProvider(provider);
+ if (postEntity != null) {
+ request.bodyText(postEntity);
+ }
+ HttpResponse response = request.send();
+ response.charset(StringPool.UTF_8);
+
+ return this.handleResponse(wxType, response.bodyText());
+ }
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaDownloadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaDownloadRequestExecutor.java
new file mode 100644
index 0000000000..ff0cea1c2b
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaDownloadRequestExecutor.java
@@ -0,0 +1,76 @@
+package me.chanjar.weixin.common.util.http.okhttp;
+
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.BaseMediaDownloadRequestExecutor;
+import me.chanjar.weixin.common.util.http.HttpResponseProxy;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okio.BufferedSink;
+import okio.Okio;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ *.
+ * @author ecoolper
+ * @date 2017/5/5
+ */
+@Slf4j
+public class OkHttpMediaDownloadRequestExecutor extends BaseMediaDownloadRequestExecutor {
+ public OkHttpMediaDownloadRequestExecutor(RequestHttp requestHttp, File tmpDirFile) {
+ super(requestHttp, tmpDirFile);
+ }
+
+ @Override
+ public File execute(String uri, String queryParam, WxType wxType) throws WxErrorException, IOException {
+ if (queryParam != null) {
+ if (uri.indexOf('?') == -1) {
+ uri += '?';
+ }
+ uri += uri.endsWith("?") ? queryParam : '&' + queryParam;
+ }
+
+ //得到httpClient
+ OkHttpClient client = requestHttp.getRequestHttpClient();
+
+ Request request = new Request.Builder().url(uri).get().build();
+
+ Response response = client.newCall(request).execute();
+
+ String contentType = response.header("Content-Type");
+ if (contentType != null && contentType.startsWith("application/json")) {
+ // application/json; encoding=utf-8 下载媒体文件出错
+ throw new WxErrorException(WxError.fromJson(response.body().string(), wxType));
+ }
+
+ String fileName = new HttpResponseProxy(response).getFileName();
+ if (StringUtils.isBlank(fileName)) {
+ return null;
+ }
+
+ String baseName = FilenameUtils.getBaseName(fileName);
+ if (StringUtils.isBlank(fileName) || baseName.length() < 3) {
+ baseName = String.valueOf(System.currentTimeMillis());
+ }
+
+ File file = File.createTempFile(
+ baseName, "." + FilenameUtils.getExtension(fileName), super.tmpDirFile
+ );
+
+ try (BufferedSink sink = Okio.buffer(Okio.sink(file))) {
+ sink.writeAll(response.body().source());
+ }
+
+ file.deleteOnExit();
+ return file;
+ }
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaUploadRequestExecutor.java
new file mode 100644
index 0000000000..6d2602d3df
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaUploadRequestExecutor.java
@@ -0,0 +1,45 @@
+package me.chanjar.weixin.common.util.http.okhttp;
+
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+import okhttp3.*;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * .
+ *
+ * @author ecoolper
+ * @date 2017/5/5
+ */
+public class OkHttpMediaUploadRequestExecutor extends MediaUploadRequestExecutor {
+ public OkHttpMediaUploadRequestExecutor(RequestHttp requestHttp) {
+ super(requestHttp);
+ }
+
+ @Override
+ public WxMediaUploadResult execute(String uri, File file, WxType wxType) throws WxErrorException, IOException {
+
+ RequestBody body = new MultipartBody.Builder()
+ .setType(MediaType.parse("multipart/form-data"))
+ .addFormDataPart("media",
+ file.getName(),
+ RequestBody.create(MediaType.parse("application/octet-stream"), file))
+ .build();
+ Request request = new Request.Builder().url(uri).post(body).build();
+
+ Response response = requestHttp.getRequestHttpClient().newCall(request).execute();
+ String responseContent = response.body().string();
+ WxError error = WxError.fromJson(responseContent, wxType);
+ if (error.getErrorCode() != 0) {
+ throw new WxErrorException(error);
+ }
+ return WxMediaUploadResult.fromJson(responseContent);
+ }
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpProxyInfo.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpProxyInfo.java
new file mode 100644
index 0000000000..eb9f7ac908
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpProxyInfo.java
@@ -0,0 +1,116 @@
+package me.chanjar.weixin.common.util.http.okhttp;
+
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+
+/**
+ * Created by ecoolper on 2017/4/26.
+ * Proxy information.
+ */
+public class OkHttpProxyInfo {
+ private final String proxyAddress;
+ private final int proxyPort;
+ private final String proxyUsername;
+ private final String proxyPassword;
+ private final ProxyType proxyType;
+
+ public OkHttpProxyInfo(ProxyType proxyType, String proxyHost, int proxyPort, String proxyUser, String proxyPassword) {
+ this.proxyType = proxyType;
+ this.proxyAddress = proxyHost;
+ this.proxyPort = proxyPort;
+ this.proxyUsername = proxyUser;
+ this.proxyPassword = proxyPassword;
+ }
+
+ /**
+ * Creates directProxy.
+ */
+ public static OkHttpProxyInfo directProxy() {
+ return new OkHttpProxyInfo(ProxyType.NONE, null, 0, null, null);
+ }
+
+ // ---------------------------------------------------------------- factory
+
+ /**
+ * Creates SOCKS4 proxy.
+ */
+ public static OkHttpProxyInfo socks4Proxy(String proxyAddress, int proxyPort, String proxyUser) {
+ return new OkHttpProxyInfo(ProxyType.SOCKS4, proxyAddress, proxyPort, proxyUser, null);
+ }
+
+ /**
+ * Creates SOCKS5 proxy.
+ */
+ public static OkHttpProxyInfo socks5Proxy(String proxyAddress, int proxyPort, String proxyUser, String proxyPassword) {
+ return new OkHttpProxyInfo(ProxyType.SOCKS5, proxyAddress, proxyPort, proxyUser, proxyPassword);
+ }
+
+ /**
+ * Creates HTTP proxy.
+ */
+ public static OkHttpProxyInfo httpProxy(String proxyAddress, int proxyPort, String proxyUser, String proxyPassword) {
+ return new OkHttpProxyInfo(ProxyType.HTTP, proxyAddress, proxyPort, proxyUser, proxyPassword);
+ }
+
+ /**
+ * Returns proxy type.
+ */
+ public ProxyType getProxyType() {
+ return proxyType;
+ }
+
+ // ---------------------------------------------------------------- getter
+
+ /**
+ * Returns proxy address.
+ */
+ public String getProxyAddress() {
+ return proxyAddress;
+ }
+
+ /**
+ * Returns proxy port.
+ */
+ public int getProxyPort() {
+ return proxyPort;
+ }
+
+ /**
+ * Returns proxy user name or null if
+ * no authentication required.
+ */
+ public String getProxyUsername() {
+ return proxyUsername;
+ }
+
+ /**
+ * Returns proxy password or null.
+ */
+ public String getProxyPassword() {
+ return proxyPassword;
+ }
+
+ /**
+ * 返回 java.net.Proxy
+ */
+ public Proxy getProxy() {
+ Proxy proxy = null;
+ if (getProxyType().equals(ProxyType.SOCKS5)) {
+ proxy = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(getProxyAddress(), getProxyPort()));
+ } else if (getProxyType().equals(ProxyType.SOCKS4)) {
+ proxy = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(getProxyAddress(), getProxyPort()));
+ } else if (getProxyType().equals(ProxyType.HTTP)) {
+ proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(getProxyAddress(), getProxyPort()));
+ } else if (getProxyType().equals(ProxyType.NONE)) {
+ proxy = new Proxy(Proxy.Type.DIRECT, new InetSocketAddress(getProxyAddress(), getProxyPort()));
+ }
+ return proxy;
+ }
+
+ /**
+ * Proxy types.
+ */
+ public enum ProxyType {
+ NONE, HTTP, SOCKS4, SOCKS5
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpSimpleGetRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpSimpleGetRequestExecutor.java
new file mode 100644
index 0000000000..9be073e38a
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpSimpleGetRequestExecutor.java
@@ -0,0 +1,40 @@
+package me.chanjar.weixin.common.util.http.okhttp;
+
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+import java.io.IOException;
+
+/**
+ * .
+ *
+ * @author ecoolper
+ * @date 2017/5/4
+ */
+public class OkHttpSimpleGetRequestExecutor extends SimpleGetRequestExecutor {
+ public OkHttpSimpleGetRequestExecutor(RequestHttp requestHttp) {
+ super(requestHttp);
+ }
+
+ @Override
+ public String execute(String uri, String queryParam, WxType wxType) throws WxErrorException, IOException {
+ if (queryParam != null) {
+ if (uri.indexOf('?') == -1) {
+ uri += '?';
+ }
+ uri += uri.endsWith("?") ? queryParam : '&' + queryParam;
+ }
+
+ //得到httpClient
+ OkHttpClient client = requestHttp.getRequestHttpClient();
+ Request request = new Request.Builder().url(uri).build();
+ Response response = client.newCall(request).execute();
+ return this.handleResponse(wxType, response.body().string());
+ }
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpSimplePostRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpSimplePostRequestExecutor.java
new file mode 100644
index 0000000000..788ea59260
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpSimplePostRequestExecutor.java
@@ -0,0 +1,33 @@
+package me.chanjar.weixin.common.util.http.okhttp;
+
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor;
+import okhttp3.*;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * .
+ *
+ * @author ecoolper
+ * @date 2017/5/4
+ */
+@Slf4j
+public class OkHttpSimplePostRequestExecutor extends SimplePostRequestExecutor {
+ public OkHttpSimplePostRequestExecutor(RequestHttp requestHttp) {
+ super(requestHttp);
+ }
+
+ @Override
+ public String execute(String uri, String postEntity, WxType wxType) throws WxErrorException, IOException {
+ RequestBody body = RequestBody.Companion.create(postEntity, MediaType.parse("text/plain; charset=utf-8"));
+ Request request = new Request.Builder().url(uri).post(body).build();
+ Response response = requestHttp.getRequestHttpClient().newCall(request).execute();
+ return this.handleResponse(wxType, Objects.requireNonNull(response.body()).string());
+ }
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonHelper.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonHelper.java
index 6166da9e04..882853945a 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonHelper.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonHelper.java
@@ -1,14 +1,5 @@
-/*
- * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
- *
- * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
- * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
- * arose from modification of the original source, or other redistribution of this source
- * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
- */
package me.chanjar.weixin.common.util.json;
-
import com.google.common.collect.Lists;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
@@ -16,7 +7,6 @@
import java.util.List;
-
public class GsonHelper {
public static boolean isNull(JsonElement element) {
@@ -77,7 +67,7 @@ public static Long getAsLong(JsonElement element) {
public static long getAsPrimitiveLong(JsonElement element) {
Long r = getAsLong(element);
- return r == null ? 0l : r;
+ return r == null ? 0L : r;
}
public static Integer getAsInteger(JsonElement element) {
@@ -95,7 +85,7 @@ public static Boolean getAsBoolean(JsonElement element) {
public static boolean getAsPrimitiveBool(JsonElement element) {
Boolean r = getAsBoolean(element);
- return r != null && r.booleanValue();
+ return r != null && r;
}
public static Double getAsDouble(JsonElement element) {
@@ -130,6 +120,20 @@ public static Integer[] getIntArray(JsonObject o, String string) {
return result.toArray(new Integer[0]);
}
+ public static String[] getStringArray(JsonObject o, String string) {
+ JsonArray jsonArray = getAsJsonArray(o.getAsJsonArray(string));
+ if (jsonArray == null) {
+ return null;
+ }
+
+ List result = Lists.newArrayList();
+ for (int i = 0; i < jsonArray.size(); i++) {
+ result.add(jsonArray.get(i).getAsString());
+ }
+
+ return result.toArray(new String[0]);
+ }
+
public static Long[] getLongArray(JsonObject o, String string) {
JsonArray jsonArray = getAsJsonArray(o.getAsJsonArray(string));
if (jsonArray == null) {
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonParser.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonParser.java
new file mode 100644
index 0000000000..53f51e0f3e
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonParser.java
@@ -0,0 +1,27 @@
+package me.chanjar.weixin.common.util.json;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.stream.JsonReader;
+import lombok.NoArgsConstructor;
+
+import java.io.Reader;
+
+/**
+ * @author niefy
+ */
+public class GsonParser {
+ private static final JsonParser JSON_PARSER = new JsonParser();
+
+ public static JsonObject parse(String json) {
+ return JSON_PARSER.parse(json).getAsJsonObject();
+ }
+
+ public static JsonObject parse(Reader json) {
+ return JSON_PARSER.parse(json).getAsJsonObject();
+ }
+
+ public static JsonObject parse(JsonReader json) {
+ return JSON_PARSER.parse(json).getAsJsonObject();
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxAccessTokenAdapter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxAccessTokenAdapter.java
index e7d700e9d1..4d19ec3ad9 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxAccessTokenAdapter.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxAccessTokenAdapter.java
@@ -1,11 +1,3 @@
-/*
- * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
- *
- * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
- * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
- * arose from modification of the original source, or other redistribution of this source
- * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
- */
package me.chanjar.weixin.common.util.json;
import com.google.gson.*;
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxBooleanTypeAdapter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxBooleanTypeAdapter.java
new file mode 100644
index 0000000000..3fbbef4a43
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxBooleanTypeAdapter.java
@@ -0,0 +1,47 @@
+package me.chanjar.weixin.common.util.json;
+
+import com.google.gson.JsonParseException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import org.apache.commons.lang3.BooleanUtils;
+
+import java.io.IOException;
+
+/**
+ *
+ * Gson 布尔类型类型转换器
+ * Created by Binary Wang on 2017-7-8.
+ *
+ *
+ * @author Binary Wang
+ */
+public class WxBooleanTypeAdapter extends TypeAdapter {
+ @Override
+ public void write(JsonWriter out, Boolean value) throws IOException {
+ if (value == null) {
+ out.nullValue();
+ } else {
+ out.value(value);
+ }
+ }
+
+ @Override
+ public Boolean read(JsonReader in) throws IOException {
+ JsonToken peek = in.peek();
+ switch (peek) {
+ case BOOLEAN:
+ return in.nextBoolean();
+ case NULL:
+ in.nextNull();
+ return null;
+ case NUMBER:
+ return BooleanUtils.toBoolean(in.nextInt());
+ case STRING:
+ return BooleanUtils.toBoolean(in.nextString());
+ default:
+ throw new JsonParseException("Expected BOOLEAN or NUMBER but was " + peek);
+ }
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxDateTypeAdapter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxDateTypeAdapter.java
new file mode 100644
index 0000000000..fd54cf3f43
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxDateTypeAdapter.java
@@ -0,0 +1,43 @@
+package me.chanjar.weixin.common.util.json;
+
+import com.google.gson.JsonParseException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+
+import java.io.IOException;
+import java.util.Date;
+
+/**
+ *
+ * Gson 日期类型转换器
+ * Created by Binary Wang on 2017-7-8.
+ *
+ *
+ * @author Binary Wang
+ */
+public class WxDateTypeAdapter extends TypeAdapter {
+ @Override
+ public void write(JsonWriter out, Date value) throws IOException {
+ if (value == null) {
+ out.nullValue();
+ } else {
+ out.value(value.getTime() / 1000);
+ }
+ }
+
+ @Override
+ public Date read(JsonReader in) throws IOException {
+ JsonToken peek = in.peek();
+ switch (peek) {
+ case NULL:
+ in.nextNull();
+ return null;
+ case NUMBER:
+ return new Date(in.nextInt() * 1000);
+ default:
+ throw new JsonParseException("Expected NUMBER but was " + peek);
+ }
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxErrorAdapter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxErrorAdapter.java
index cc426bc8ca..0ea52b9a86 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxErrorAdapter.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxErrorAdapter.java
@@ -1,36 +1,31 @@
-/*
- * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
- *
- * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
- * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
- * arose from modification of the original source, or other redistribution of this source
- * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
- */
package me.chanjar.weixin.common.util.json;
import com.google.gson.*;
-import me.chanjar.weixin.common.bean.result.WxError;
+import me.chanjar.weixin.common.error.WxError;
import java.lang.reflect.Type;
/**
- * @author Daniel Qian
+ * @author Daniel Qian.
*/
public class WxErrorAdapter implements JsonDeserializer {
@Override
- public WxError deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
- WxError wxError = new WxError();
+ public WxError deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
+ WxError.WxErrorBuilder errorBuilder = WxError.builder();
JsonObject wxErrorJsonObject = json.getAsJsonObject();
if (wxErrorJsonObject.get("errcode") != null && !wxErrorJsonObject.get("errcode").isJsonNull()) {
- wxError.setErrorCode(GsonHelper.getAsPrimitiveInt(wxErrorJsonObject.get("errcode")));
+ errorBuilder.errorCode(GsonHelper.getAsPrimitiveInt(wxErrorJsonObject.get("errcode")));
}
if (wxErrorJsonObject.get("errmsg") != null && !wxErrorJsonObject.get("errmsg").isJsonNull()) {
- wxError.setErrorMsg(GsonHelper.getAsString(wxErrorJsonObject.get("errmsg")));
+ errorBuilder.errorMsg(GsonHelper.getAsString(wxErrorJsonObject.get("errmsg")));
}
- wxError.setJson(json.toString());
- return wxError;
+
+ errorBuilder.json(json.toString());
+
+ return errorBuilder.build();
}
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxGsonBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxGsonBuilder.java
index 100ef52303..0624923508 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxGsonBuilder.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxGsonBuilder.java
@@ -2,15 +2,18 @@
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
-
import me.chanjar.weixin.common.bean.WxAccessToken;
+import me.chanjar.weixin.common.bean.WxNetCheckResult;
import me.chanjar.weixin.common.bean.menu.WxMenu;
-import me.chanjar.weixin.common.bean.result.WxError;
+import me.chanjar.weixin.common.error.WxError;
import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
+/**
+ * .
+ * @author chanjarster
+ */
public class WxGsonBuilder {
-
- public static final GsonBuilder INSTANCE = new GsonBuilder();
+ private static final GsonBuilder INSTANCE = new GsonBuilder();
static {
INSTANCE.disableHtmlEscaping();
@@ -18,6 +21,8 @@ public class WxGsonBuilder {
INSTANCE.registerTypeAdapter(WxError.class, new WxErrorAdapter());
INSTANCE.registerTypeAdapter(WxMenu.class, new WxMenuGsonAdapter());
INSTANCE.registerTypeAdapter(WxMediaUploadResult.class, new WxMediaUploadResultAdapter());
+ INSTANCE.registerTypeAdapter(WxNetCheckResult.class, new WxNetCheckResultGsonAdapter());
+
}
public static Gson create() {
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxMediaUploadResultAdapter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxMediaUploadResultAdapter.java
index 662b744274..3cb32c5ede 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxMediaUploadResultAdapter.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxMediaUploadResultAdapter.java
@@ -1,18 +1,14 @@
-/*
- * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
- *
- * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
- * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
- * arose from modification of the original source, or other redistribution of this source
- * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
- */
package me.chanjar.weixin.common.util.json;
-import com.google.gson.*;
-import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
-
import java.lang.reflect.Type;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
+
/**
* @author Daniel Qian
*/
@@ -20,22 +16,25 @@ public class WxMediaUploadResultAdapter implements JsonDeserializer {
+
+
+ @Override
+ public WxNetCheckResult deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+ WxNetCheckResult result = new WxNetCheckResult();
+
+ JsonArray dnssJson = json.getAsJsonObject().get("dns").getAsJsonArray();
+ List dnsInfoList = new ArrayList<>();
+ if (dnssJson != null && dnssJson.size() > 0) {
+ for (int i = 0; i < dnssJson.size(); i++) {
+ JsonObject buttonJson = dnssJson.get(i).getAsJsonObject();
+ WxNetCheckResult.WxNetCheckDnsInfo dnsInfo = new WxNetCheckResult.WxNetCheckDnsInfo();
+ dnsInfo.setIp(GsonHelper.getString(buttonJson, "ip"));
+ dnsInfo.setRealOperator(GsonHelper.getString(buttonJson, "real_operator"));
+ dnsInfoList.add(dnsInfo);
+ }
+ }
+
+ JsonArray pingsJson = json.getAsJsonObject().get("ping").getAsJsonArray();
+ List pingInfoList = new ArrayList<>();
+ if (pingsJson != null && pingsJson.size() > 0) {
+ for (int i = 0; i < pingsJson.size(); i++) {
+ JsonObject pingJson = pingsJson.get(i).getAsJsonObject();
+ WxNetCheckResult.WxNetCheckPingInfo pingInfo = new WxNetCheckResult.WxNetCheckPingInfo();
+ pingInfo.setIp(GsonHelper.getString(pingJson, "ip"));
+ pingInfo.setFromOperator(GsonHelper.getString(pingJson, "from_operator"));
+ pingInfo.setPackageLoss(GsonHelper.getString(pingJson, "package_loss"));
+ pingInfo.setTime(GsonHelper.getString(pingJson, "time"));
+ pingInfoList.add(pingInfo);
+ }
+ }
+ result.setDnsInfos(dnsInfoList);
+ result.setPingInfos(pingInfoList);
+ return result;
+ }
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/JedisDistributedLock.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/JedisDistributedLock.java
new file mode 100644
index 0000000000..074f9d9351
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/JedisDistributedLock.java
@@ -0,0 +1,73 @@
+package me.chanjar.weixin.common.util.locks;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+
+import com.github.jedis.lock.JedisLock;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.util.Pool;
+
+/**
+ * JedisPool 分布式锁
+ *
+ * @author 007
+ */
+public class JedisDistributedLock implements Lock {
+ private final Pool jedisPool;
+ private final JedisLock lock;
+
+ public JedisDistributedLock(Pool jedisPool, String key){
+ this.jedisPool = jedisPool;
+ this.lock = new JedisLock(key);
+ }
+
+ @Override
+ public void lock() {
+ try (Jedis jedis = jedisPool.getResource()) {
+ if (!lock.acquire(jedis)) {
+ throw new RuntimeException("acquire timeouted");
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException("lock failed", e);
+ }
+ }
+
+ @Override
+ public void lockInterruptibly() throws InterruptedException {
+ try (Jedis jedis = jedisPool.getResource()) {
+ if (!lock.acquire(jedis)) {
+ throw new RuntimeException("acquire timeouted");
+ }
+ }
+ }
+
+ @Override
+ public boolean tryLock() {
+ try (Jedis jedis = jedisPool.getResource()) {
+ return lock.acquire(jedis);
+ } catch (InterruptedException e) {
+ throw new RuntimeException("lock failed", e);
+ }
+ }
+
+ @Override
+ public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
+ try (Jedis jedis = jedisPool.getResource()) {
+ return lock.acquire(jedis);
+ }
+ }
+
+ @Override
+ public void unlock() {
+ try (Jedis jedis = jedisPool.getResource()) {
+ lock.release(jedis);
+ }
+ }
+
+ @Override
+ public Condition newCondition() {
+ throw new RuntimeException("unsupported method");
+ }
+
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java
new file mode 100644
index 0000000000..dfac1c28fb
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java
@@ -0,0 +1,126 @@
+package me.chanjar.weixin.common.util.locks;
+
+import lombok.Getter;
+import lombok.NonNull;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.dao.DataAccessException;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.connection.RedisStringCommands;
+import org.springframework.data.redis.core.RedisCallback;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.core.script.RedisScript;
+import org.springframework.data.redis.core.types.Expiration;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * 实现简单的redis分布式锁, 支持重入, 不是红锁
+ *
+ * @see reids distlock
+ */
+public class RedisTemplateSimpleDistributedLock implements Lock {
+
+ @Getter
+ private final StringRedisTemplate redisTemplate;
+ @Getter
+ private final String key;
+ @Getter
+ private final int leaseMilliseconds;
+
+ private final ThreadLocal valueThreadLocal = new ThreadLocal<>();
+
+ public RedisTemplateSimpleDistributedLock(@NonNull StringRedisTemplate redisTemplate, int leaseMilliseconds) {
+ this(redisTemplate, "lock:" + UUID.randomUUID().toString(), leaseMilliseconds);
+ }
+
+ public RedisTemplateSimpleDistributedLock(@NonNull StringRedisTemplate redisTemplate, @NonNull String key, int leaseMilliseconds) {
+ if (leaseMilliseconds <= 0) {
+ throw new IllegalArgumentException("Parameter 'leaseMilliseconds' must grate then 0: " + leaseMilliseconds);
+ }
+ this.redisTemplate = redisTemplate;
+ this.key = key;
+ this.leaseMilliseconds = leaseMilliseconds;
+ }
+
+ @Override
+ public void lock() {
+ while (!tryLock()) {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+ }
+
+ @Override
+ public void lockInterruptibly() throws InterruptedException {
+ while (!tryLock()) {
+ Thread.sleep(1000);
+ }
+ }
+
+ @Override
+ public boolean tryLock() {
+ String value = valueThreadLocal.get();
+ if (value == null || value.length() == 0) {
+ value = UUID.randomUUID().toString();
+ valueThreadLocal.set(value);
+ }
+ final byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
+ final byte[] valueBytes = value.getBytes(StandardCharsets.UTF_8);
+ List redisResults = redisTemplate.executePipelined(new RedisCallback() {
+ @Override
+ public String doInRedis(RedisConnection connection) throws DataAccessException {
+ connection.set(keyBytes, valueBytes, Expiration.milliseconds(leaseMilliseconds), RedisStringCommands.SetOption.SET_IF_ABSENT);
+ connection.get(keyBytes);
+ return null;
+ }
+ });
+ Object currentLockSecret = redisResults.size() > 1 ? redisResults.get(1) : redisResults.get(0);
+ return currentLockSecret != null && currentLockSecret.toString().equals(value);
+ }
+
+ @Override
+ public boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedException {
+ long waitMs = unit.toMillis(time);
+ boolean locked = tryLock();
+ while (!locked && waitMs > 0) {
+ long sleep = waitMs < 1000 ? waitMs : 1000;
+ Thread.sleep(sleep);
+ waitMs -= sleep;
+ locked = tryLock();
+ }
+ return locked;
+ }
+
+ @Override
+ public void unlock() {
+ if (valueThreadLocal.get() != null) {
+ // 提示: 必须指定returnType, 类型: 此处必须为Long, 不能是Integer
+ RedisScript script = new DefaultRedisScript("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", Long.class);
+ redisTemplate.execute(script, Arrays.asList(key), valueThreadLocal.get());
+ valueThreadLocal.remove();
+ }
+ }
+
+ @Override
+ public Condition newCondition() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * 获取当前锁的值
+ * return 返回null意味着没有加锁, 但是返回非null值并不以为着当前加锁成功(redis中key可能自动过期)
+ */
+ public String getLockSecretValue() {
+ return valueThreadLocal.get();
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/res/StringManager.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/res/StringManager.java
index 32f4e42aaa..e5bdb38804 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/res/StringManager.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/res/StringManager.java
@@ -19,6 +19,7 @@
import java.text.MessageFormat;
import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
/**
* An internationalization / localization helper class which reduces
@@ -46,8 +47,7 @@
*/
public class StringManager {
- private static final Map> managers =
- new Hashtable<>();
+ private static final Map> MANAGERS = new ConcurrentHashMap<>();
private static int LOCALE_CACHE_SIZE = 10;
/**
* The ResourceBundle for this StringManager.
@@ -103,7 +103,7 @@ private StringManager(String packageName, Locale locale) {
* @param packageName The package name
*/
public static final synchronized StringManager getManager(
- String packageName) {
+ String packageName) {
return getManager(packageName, Locale.getDefault());
}
@@ -116,28 +116,28 @@ public static final synchronized StringManager getManager(
* @param locale The Locale
*/
public static final synchronized StringManager getManager(
- String packageName, Locale locale) {
+ String packageName, Locale locale) {
- Map map = managers.get(packageName);
+ Map map = MANAGERS.get(packageName);
if (map == null) {
- /*
- * Don't want the HashMap to be expanded beyond LOCALE_CACHE_SIZE.
- * Expansion occurs when size() exceeds capacity. Therefore keep
- * size at or below capacity.
- * removeEldestEntry() executes after insertion therefore the test
- * for removal needs to use one less than the maximum desired size
- *
- */
+ /*
+ * Don't want the HashMap to be expanded beyond LOCALE_CACHE_SIZE.
+ * Expansion occurs when size() exceeds capacity. Therefore keep
+ * size at or below capacity.
+ * removeEldestEntry() executes after insertion therefore the test
+ * for removal needs to use one less than the maximum desired size
+ *
+ */
map = new LinkedHashMap(LOCALE_CACHE_SIZE, 1, true) {
private static final long serialVersionUID = 1L;
@Override
protected boolean removeEldestEntry(
- Map.Entry eldest) {
+ Map.Entry eldest) {
return size() > (LOCALE_CACHE_SIZE - 1);
}
};
- managers.put(packageName, map);
+ MANAGERS.put(packageName, map);
}
StringManager mgr = map.get(locale);
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/IntegerArrayConverter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/IntegerArrayConverter.java
new file mode 100644
index 0000000000..3a82b213ca
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/IntegerArrayConverter.java
@@ -0,0 +1,37 @@
+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;
+
+/**
+ * Integer型数组转换器.
+ *
+ * @author Binary Wang
+ * @date 2019-08-22
+ */
+public class IntegerArrayConverter extends StringConverter {
+ @Override
+ public boolean canConvert(Class type) {
+ return type == Integer[].class;
+ }
+
+ @Override
+ public String toString(Object obj) {
+ return "";
+ }
+
+ @Override
+ public Object fromString(String str) {
+ final Iterable iterable = Splitter.on(",").split(str);
+ final String[] strings = Iterables.toArray(iterable, String.class);
+ Integer[] result = new Integer[strings.length];
+ int index = 0;
+ for (String string : strings) {
+ result[index++] = Integer.parseInt(string);
+ }
+
+ return result;
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/LongArrayConverter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/LongArrayConverter.java
new file mode 100644
index 0000000000..a383c59674
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/LongArrayConverter.java
@@ -0,0 +1,37 @@
+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;
+
+/**
+ * Long型数组转换器.
+ *
+ * @author Binary Wang
+ * @date 2019-08-22
+ */
+public class LongArrayConverter extends StringConverter {
+ @Override
+ public boolean canConvert(Class type) {
+ return type == Long[].class;
+ }
+
+ @Override
+ public String toString(Object obj) {
+ return "";
+ }
+
+ @Override
+ public Object fromString(String str) {
+ final Iterable iterable = Splitter.on(",").split(str);
+ final String[] strings = Iterables.toArray(iterable, String.class);
+ Long[] result = new Long[strings.length];
+ int index = 0;
+ for (String string : strings) {
+ result[index++] = Long.parseLong(string);
+ }
+
+ return result;
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamCDataConverter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamCDataConverter.java
index aa948b34b5..ab1b168831 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamCDataConverter.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamCDataConverter.java
@@ -2,6 +2,11 @@
import com.thoughtworks.xstream.converters.basic.StringConverter;
+/**
+ * CDATA 内容转换器,加上CDATA标签.
+ *
+ * @author Daniel Qian
+ */
public class XStreamCDataConverter extends StringConverter {
@Override
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamInitializer.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamInitializer.java
index ba49528aaf..5fd7ceb2cb 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamInitializer.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamInitializer.java
@@ -1,51 +1,80 @@
package me.chanjar.weixin.common.util.xml;
-import java.io.Writer;
-
import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.converters.basic.*;
+import com.thoughtworks.xstream.converters.collections.CollectionConverter;
+import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;
+import com.thoughtworks.xstream.converters.reflection.ReflectionConverter;
import com.thoughtworks.xstream.core.util.QuickWriter;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
import com.thoughtworks.xstream.io.xml.XppDriver;
-import com.thoughtworks.xstream.security.NullPermission;
-import com.thoughtworks.xstream.security.PrimitiveTypePermission;
+import com.thoughtworks.xstream.security.NoTypePermission;
+import com.thoughtworks.xstream.security.WildcardTypePermission;
+
+import java.io.Writer;
public class XStreamInitializer {
+ private static final XppDriver XPP_DRIVER = new XppDriver() {
+ @Override
+ public HierarchicalStreamWriter createWriter(Writer out) {
+ return new PrettyPrintWriter(out, getNameCoder()) {
+ private static final String PREFIX_CDATA = "";
+ private static final String PREFIX_MEDIA_ID = "";
+ private static final String SUFFIX_MEDIA_ID = " ";
- public static XStream getInstance() {
- XStream xstream = new XStream(new XppDriver() {
+ @Override
+ protected void writeText(QuickWriter writer, String text) {
+ if (text.startsWith(PREFIX_CDATA) && text.endsWith(SUFFIX_CDATA)) {
+ writer.write(text);
+ } else if (text.startsWith(PREFIX_MEDIA_ID) && text.endsWith(SUFFIX_MEDIA_ID)) {
+ writer.write(text);
+ } else {
+ super.writeText(writer, text);
+ }
- @Override
- public HierarchicalStreamWriter createWriter(Writer out) {
- return new PrettyPrintWriter(out, getNameCoder()) {
- protected String PREFIX_CDATA = "";
- protected String PREFIX_MEDIA_ID = "";
- protected String SUFFIX_MEDIA_ID = " ";
-
- @Override
- protected void writeText(QuickWriter writer, String text) {
- if (text.startsWith(this.PREFIX_CDATA) && text.endsWith(this.SUFFIX_CDATA)) {
- writer.write(text);
- } else if (text.startsWith(this.PREFIX_MEDIA_ID) && text.endsWith(this.SUFFIX_MEDIA_ID)) {
- writer.write(text);
- } else {
- super.writeText(writer, text);
- }
+ }
- }
+ @Override
+ public String encodeNode(String name) {
+ //防止将_转换成__
+ return name;
+ }
+ };
+ }
+ };
- @Override
- public String encodeNode(String name) {
- return name;//防止将_转换成__
- }
- };
+ public static XStream getInstance() {
+ XStream xstream = new XStream(new PureJavaReflectionProvider(), XPP_DRIVER) {
+ // only register the converters we need; other converters generate a private access warning in the console on Java9+...
+ @Override
+ protected void setupConverters() {
+ registerConverter(new NullConverter(), PRIORITY_VERY_HIGH);
+ registerConverter(new IntConverter(), PRIORITY_NORMAL);
+ registerConverter(new FloatConverter(), PRIORITY_NORMAL);
+ registerConverter(new DoubleConverter(), PRIORITY_NORMAL);
+ registerConverter(new LongConverter(), PRIORITY_NORMAL);
+ registerConverter(new ShortConverter(), PRIORITY_NORMAL);
+ registerConverter(new BooleanConverter(), PRIORITY_NORMAL);
+ registerConverter(new ByteConverter(), PRIORITY_NORMAL);
+ registerConverter(new StringConverter(), PRIORITY_NORMAL);
+ registerConverter(new DateConverter(), PRIORITY_NORMAL);
+ registerConverter(new CollectionConverter(getMapper()), PRIORITY_NORMAL);
+ registerConverter(new ReflectionConverter(getMapper(), getReflectionProvider()), PRIORITY_VERY_LOW);
}
- });
+ };
xstream.ignoreUnknownElements();
xstream.setMode(XStream.NO_REFERENCES);
- xstream.addPermission(NullPermission.NULL);
- xstream.addPermission(PrimitiveTypePermission.PRIMITIVES);
+ XStream.setupDefaultSecurity(xstream);
+ xstream.autodetectAnnotations(true);
+
+ // setup proper security by limiting which classes can be loaded by XStream
+ xstream.addPermission(NoTypePermission.NONE);
+ xstream.addPermission(new WildcardTypePermission(new String[]{
+ "me.chanjar.weixin.**", "cn.binarywang.wx.**", "com.github.binarywang.**"
+ }));
+ xstream.setClassLoader(Thread.currentThread().getContextClassLoader());
return xstream;
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/cp/bean/article/MpnewsArticle.java b/weixin-java-common/src/main/java/me/chanjar/weixin/cp/bean/article/MpnewsArticle.java
deleted file mode 100644
index 7904da202c..0000000000
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/cp/bean/article/MpnewsArticle.java
+++ /dev/null
@@ -1,139 +0,0 @@
-package me.chanjar.weixin.cp.bean.article;
-
-/**
- *
- * Created by BinaryWang on 2017/3/27.
- *
- * @author Binary Wang
- */
-public class MpnewsArticle {
- private String title;
- private String thumbMediaId;
- private String author;
- private String contentSourceUrl;
- private String content;
- private String digest;
- private String showCoverPic;
-
- private MpnewsArticle(Builder builder) {
- setTitle(builder.title);
- setThumbMediaId(builder.thumbMediaId);
- setAuthor(builder.author);
- setContentSourceUrl(builder.contentSourceUrl);
- setContent(builder.content);
- setDigest(builder.digest);
- setShowCoverPic(builder.showCoverPic);
- }
-
- public static Builder newBuilder() {
- return new Builder();
- }
-
- public String getTitle() {
- return title;
- }
-
- public void setTitle(String title) {
- this.title = title;
- }
-
- public String getThumbMediaId() {
- return thumbMediaId;
- }
-
- public void setThumbMediaId(String thumbMediaId) {
- this.thumbMediaId = thumbMediaId;
- }
-
- public String getAuthor() {
- return author;
- }
-
- public void setAuthor(String author) {
- this.author = author;
- }
-
- public String getContentSourceUrl() {
- return contentSourceUrl;
- }
-
- public void setContentSourceUrl(String contentSourceUrl) {
- this.contentSourceUrl = contentSourceUrl;
- }
-
- public String getContent() {
- return content;
- }
-
- public void setContent(String content) {
- this.content = content;
- }
-
- public String getDigest() {
- return digest;
- }
-
- public void setDigest(String digest) {
- this.digest = digest;
- }
-
- public String getShowCoverPic() {
- return showCoverPic;
- }
-
- public void setShowCoverPic(String showCoverPic) {
- this.showCoverPic = showCoverPic;
- }
-
- public static final class Builder {
- private String title;
- private String thumbMediaId;
- private String author;
- private String contentSourceUrl;
- private String content;
- private String digest;
- private String showCoverPic;
-
- private Builder() {
- }
-
- public Builder title(String title) {
- this.title = title;
- return this;
- }
-
- public Builder thumbMediaId(String thumbMediaId) {
- this.thumbMediaId = thumbMediaId;
- return this;
- }
-
- public Builder author(String author) {
- this.author = author;
- return this;
- }
-
- public Builder contentSourceUrl(String contentSourceUrl) {
- this.contentSourceUrl = contentSourceUrl;
- return this;
- }
-
- public Builder content(String content) {
- this.content = content;
- return this;
- }
-
- public Builder digest(String digest) {
- this.digest = digest;
- return this;
- }
-
- public Builder showCoverPic(String showCoverPic) {
- this.showCoverPic = showCoverPic;
- return this;
- }
-
- public MpnewsArticle build() {
- return new MpnewsArticle(this);
- }
- }
-}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/cp/bean/article/NewArticle.java b/weixin-java-common/src/main/java/me/chanjar/weixin/cp/bean/article/NewArticle.java
deleted file mode 100644
index d4c056d5ad..0000000000
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/cp/bean/article/NewArticle.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package me.chanjar.weixin.cp.bean.article;
-
-/**
- *
- * Created by BinaryWang on 2017/3/27.
- *
- *
- * @author Binary Wang
- */
-public class NewArticle {
-
- private String title;
- private String description;
- private String url;
- private String picUrl;
-
- public String getTitle() {
- return this.title;
- }
-
- public void setTitle(String title) {
- this.title = title;
- }
-
- public String getDescription() {
- return this.description;
- }
-
- public void setDescription(String description) {
- this.description = description;
- }
-
- public String getUrl() {
- return this.url;
- }
-
- public void setUrl(String url) {
- this.url = url;
- }
-
- public String getPicUrl() {
- return this.picUrl;
- }
-
- public void setPicUrl(String picUrl) {
- this.picUrl = picUrl;
- }
-
-}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/WxMessageInMemoryDuplicateCheckerTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/api/WxMessageInMemoryDuplicateCheckerTest.java
similarity index 52%
rename from weixin-java-common/src/test/java/me/chanjar/weixin/common/util/WxMessageInMemoryDuplicateCheckerTest.java
rename to weixin-java-common/src/test/java/me/chanjar/weixin/common/api/WxMessageInMemoryDuplicateCheckerTest.java
index a3a243a428..fd8819272c 100644
--- a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/WxMessageInMemoryDuplicateCheckerTest.java
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/api/WxMessageInMemoryDuplicateCheckerTest.java
@@ -1,34 +1,37 @@
-package me.chanjar.weixin.common.util;
+package me.chanjar.weixin.common.api;
-import me.chanjar.weixin.common.api.WxMessageInMemoryDuplicateChecker;
-import org.testng.Assert;
import org.testng.annotations.Test;
+import java.util.concurrent.TimeUnit;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
@Test
public class WxMessageInMemoryDuplicateCheckerTest {
+ private WxMessageInMemoryDuplicateChecker checker = new WxMessageInMemoryDuplicateChecker(2000L, 1000L);
public void test() throws InterruptedException {
- Long[] msgIds = new Long[]{1l, 2l, 3l, 4l, 5l, 6l, 7l, 8l};
- WxMessageInMemoryDuplicateChecker checker = new WxMessageInMemoryDuplicateChecker(2000l, 1000l);
+ Long[] msgIds = new Long[]{1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L};
// 第一次检查
for (Long msgId : msgIds) {
boolean result = checker.isDuplicate(String.valueOf(msgId));
- Assert.assertFalse(result);
+ assertFalse(result);
}
// 过1秒再检查
- Thread.sleep(1000l);
+ TimeUnit.SECONDS.sleep(1);
for (Long msgId : msgIds) {
boolean result = checker.isDuplicate(String.valueOf(msgId));
- Assert.assertTrue(result);
+ assertTrue(result);
}
// 过1.5秒再检查
- Thread.sleep(1500l);
+ TimeUnit.MILLISECONDS.sleep(1500L);
for (Long msgId : msgIds) {
boolean result = checker.isDuplicate(String.valueOf(msgId));
- Assert.assertFalse(result);
+ assertFalse(result);
}
}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxAccessTokenTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxAccessTokenTest.java
index b2bc7fe7d1..1f88df29bb 100644
--- a/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxAccessTokenTest.java
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxAccessTokenTest.java
@@ -1,7 +1,7 @@
package me.chanjar.weixin.common.bean;
-import org.testng.Assert;
-import org.testng.annotations.Test;
+import org.testng.*;
+import org.testng.annotations.*;
@Test
public class WxAccessTokenTest {
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxErrorTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxErrorTest.java
deleted file mode 100644
index 3fe40795f3..0000000000
--- a/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxErrorTest.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package me.chanjar.weixin.common.bean;
-
-import me.chanjar.weixin.common.bean.result.WxError;
-import org.testng.Assert;
-import org.testng.annotations.Test;
-
-@Test
-public class WxErrorTest {
-
- public void testFromJson() {
-
- String json = "{ \"errcode\": 40003, \"errmsg\": \"invalid openid\" }";
- WxError wxError = WxError.fromJson(json);
- Assert.assertTrue(wxError.getErrorCode() == 40003);
- Assert.assertEquals(wxError.getErrorMsg(), "invalid openid");
-
- }
-
- public void testFromBadJson1() {
-
- String json = "{ \"errcode\": 40003, \"errmsg\": \"invalid openid\", \"media_id\": \"12323423dsfafsf232f\" }";
- WxError wxError = WxError.fromJson(json);
- Assert.assertTrue(wxError.getErrorCode() == 40003);
- Assert.assertEquals(wxError.getErrorMsg(), "invalid openid");
-
- }
-
- public void testFromBadJson2() {
-
- String json = "{\"access_token\":\"ACCESS_TOKEN\",\"expires_in\":7200}";
- WxError wxError = WxError.fromJson(json);
- Assert.assertTrue(wxError.getErrorCode() == 0);
- Assert.assertEquals(wxError.getErrorMsg(), null);
-
- }
-
-}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxMenuTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxMenuTest.java
index e2a0c2c1f8..506b2dbaca 100644
--- a/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxMenuTest.java
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxMenuTest.java
@@ -1,12 +1,10 @@
package me.chanjar.weixin.common.bean;
-import org.testng.Assert;
-import org.testng.annotations.DataProvider;
-import org.testng.annotations.Test;
-
import me.chanjar.weixin.common.bean.menu.WxMenu;
import me.chanjar.weixin.common.bean.menu.WxMenuButton;
import me.chanjar.weixin.common.bean.menu.WxMenuRule;
+import org.testng.*;
+import org.testng.annotations.*;
@Test
public class WxMenuTest {
@@ -87,75 +85,75 @@ public Object[][] wxReturnMenu() {
Object[][] res = menuJson();
String json = "{ \"menu\" : " + res[0][0] + " }";
return new Object[][]{
- new Object[]{json}
+ new Object[]{json}
};
}
@DataProvider(name = "wxPushMenu")
public Object[][] menuJson() {
String json =
- "{"
- + "\"button\":["
- + "{"
- + "\"type\":\"click\","
- + "\"name\":\"今日歌曲\","
- + "\"key\":\"V1001_TODAY_MUSIC\""
- + "},"
- + "{"
- + "\"type\":\"click\","
- + "\"name\":\"歌手简介\","
- + "\"key\":\"V1001_TODAY_SINGER\""
- + "},"
- + "{"
- + "\"name\":\"菜单\","
- + "\"sub_button\":["
- + "{"
- + "\"type\":\"view\","
- + "\"name\":\"搜索\","
- + "\"url\":\"http://www.soso.com/\""
- + "},"
- + "{"
- + "\"type\":\"view\","
- + "\"name\":\"视频\","
- + "\"url\":\"http://v.qq.com/\""
- + "},"
- + "{"
- + "\"type\":\"click\","
- + "\"name\":\"赞一下我们\","
- + "\"key\":\"V1001_GOOD\""
- + "}"
- + "]"
- + "}"
- + "]"
- + "}";
+ "{"
+ + "\"button\":["
+ + "{"
+ + "\"type\":\"click\","
+ + "\"name\":\"今日歌曲\","
+ + "\"key\":\"V1001_TODAY_MUSIC\""
+ + "},"
+ + "{"
+ + "\"type\":\"click\","
+ + "\"name\":\"歌手简介\","
+ + "\"key\":\"V1001_TODAY_SINGER\""
+ + "},"
+ + "{"
+ + "\"name\":\"菜单\","
+ + "\"sub_button\":["
+ + "{"
+ + "\"type\":\"view\","
+ + "\"name\":\"搜索\","
+ + "\"url\":\"http://www.soso.com/\""
+ + "},"
+ + "{"
+ + "\"type\":\"view\","
+ + "\"name\":\"视频\","
+ + "\"url\":\"http://v.qq.com/\""
+ + "},"
+ + "{"
+ + "\"type\":\"click\","
+ + "\"name\":\"赞一下我们\","
+ + "\"key\":\"V1001_GOOD\""
+ + "}"
+ + "]"
+ + "}"
+ + "]"
+ + "}";
return new Object[][]{
- new Object[]{json}
+ new Object[]{json}
};
}
@DataProvider(name = "wxAddConditionalMenu")
public Object[][] addConditionalMenuJson() {
String json =
- "{"
- + "\"button\":["
- + "{"
- + "\"type\":\"click\","
- + "\"name\":\"今日歌曲\","
- + "\"key\":\"V1001_TODAY_MUSIC\""
- + "}"
- + "],"
- + "\"matchrule\":{"
- + "\"group_id\":\"2\","
- + "\"sex\":\"1\","
- + "\"country\":\"中国\","
- + "\"province\":\"广东\","
- + "\"city\":\"广州\","
- + "\"client_platform_type\":\"2\","
- + "\"language\":\"zh_CN\""
- + "}"
- + "}";
+ "{"
+ + "\"button\":["
+ + "{"
+ + "\"type\":\"click\","
+ + "\"name\":\"今日歌曲\","
+ + "\"key\":\"V1001_TODAY_MUSIC\""
+ + "}"
+ + "],"
+ + "\"matchrule\":{"
+ + "\"group_id\":\"2\","
+ + "\"sex\":\"1\","
+ + "\"country\":\"中国\","
+ + "\"province\":\"广东\","
+ + "\"city\":\"广州\","
+ + "\"client_platform_type\":\"2\","
+ + "\"language\":\"zh_CN\""
+ + "}"
+ + "}";
return new Object[][]{
- new Object[]{json}
+ new Object[]{json}
};
}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxNetCheckResultTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxNetCheckResultTest.java
new file mode 100644
index 0000000000..3f08b20bff
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxNetCheckResultTest.java
@@ -0,0 +1,58 @@
+package me.chanjar.weixin.common.bean;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ *
+ * @author Binary Wang
+ * @date 2020-06-06
+ */
+public class WxNetCheckResultTest {
+
+ @Test
+ public void testFromJson() {
+ String json = "{\n" +
+ " \"dns\": [\n" +
+ " {\n" +
+ " \"ip\": \"111.161.64.40\", \n" +
+ " \"real_operator\": \"UNICOM\"\n" +
+ " }, \n" +
+ " {\n" +
+ " \"ip\": \"111.161.64.48\", \n" +
+ " \"real_operator\": \"UNICOM\"\n" +
+ " }\n" +
+ " ], \n" +
+ " \"ping\": [\n" +
+ " {\n" +
+ " \"ip\": \"111.161.64.40\", \n" +
+ " \"from_operator\": \"UNICOM\"," +
+ " \"package_loss\": \"0%\", \n" +
+ " \"time\": \"23.079ms\"\n" +
+ " }, \n" +
+ " {\n" +
+ " \"ip\": \"111.161.64.48\", \n" +
+ " \"from_operator\": \"UNICOM\", \n" +
+ " \"package_loss\": \"0%\", \n" +
+ " \"time\": \"21.434ms\"\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+ WxNetCheckResult result = WxNetCheckResult.fromJson(json);
+ Assert.assertNotNull(result);
+ Assert.assertNotNull(result.getDnsInfos());
+
+ WxNetCheckResult.WxNetCheckDnsInfo dnsInfo = new WxNetCheckResult.WxNetCheckDnsInfo();
+ dnsInfo.setIp("111.161.64.40");
+ dnsInfo.setRealOperator("UNICOM");
+ Assert.assertEquals(result.getDnsInfos().get(0), dnsInfo);
+
+ WxNetCheckResult.WxNetCheckPingInfo pingInfo = new WxNetCheckResult.WxNetCheckPingInfo();
+ pingInfo.setTime("21.434ms");
+ pingInfo.setFromOperator("UNICOM");
+ pingInfo.setIp("111.161.64.48");
+ pingInfo.setPackageLoss("0%");
+ Assert.assertEquals(result.getPingInfos().get(1), pingInfo);
+
+ }
+}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/error/WxErrorTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/error/WxErrorTest.java
new file mode 100644
index 0000000000..456a58ad76
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/error/WxErrorTest.java
@@ -0,0 +1,35 @@
+package me.chanjar.weixin.common.error;
+
+import me.chanjar.weixin.common.enums.WxType;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+
+@Test
+public class WxErrorTest {
+ public void testFromJson() {
+ String json = "{ \"errcode\": 40003, \"errmsg\": \"invalid openid\" }";
+ WxError wxError = WxError.fromJson(json, WxType.MP);
+ assertEquals(40003, wxError.getErrorCode());
+ assertEquals(wxError.getErrorMsgEn(), "invalid openid");
+
+ }
+
+ public void testFromBadJson1() {
+ String json = "{ \"errcode\": 40003, \"errmsg\": \"invalid openid\", \"media_id\": \"12323423dsfafsf232f\" }";
+ WxError wxError = WxError.fromJson(json, WxType.MP);
+ assertEquals(40003, wxError.getErrorCode());
+ assertEquals(wxError.getErrorMsgEn(), "invalid openid");
+
+ }
+
+ public void testFromBadJson2() {
+ String json = "{\"access_token\":\"ACCESS_TOKEN\",\"expires_in\":7200}";
+ WxError wxError = WxError.fromJson(json, WxType.MP);
+ assertEquals(0, wxError.getErrorCode());
+ assertNull(wxError.getErrorMsg());
+
+ }
+
+}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/CommonWxRedisOpsTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/CommonWxRedisOpsTest.java
new file mode 100644
index 0000000000..96ba20ba2b
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/CommonWxRedisOpsTest.java
@@ -0,0 +1,51 @@
+package me.chanjar.weixin.common.redis;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.util.concurrent.TimeUnit;
+
+public class CommonWxRedisOpsTest {
+
+ protected WxRedisOps wxRedisOps;
+ private String key = "access_token";
+ private String value = String.valueOf(System.currentTimeMillis());
+
+ @Test
+ public void testGetValue() {
+ wxRedisOps.setValue(key, value, 3, TimeUnit.SECONDS);
+ Assert.assertEquals(wxRedisOps.getValue(key), value);
+ }
+
+ @Test
+ public void testSetValue() {
+ String key = "access_token", value = String.valueOf(System.currentTimeMillis());
+ wxRedisOps.setValue(key, value, -1, TimeUnit.SECONDS);
+ wxRedisOps.setValue(key, value, 0, TimeUnit.SECONDS);
+ wxRedisOps.setValue(key, value, 1, TimeUnit.SECONDS);
+ }
+
+ @Test
+ public void testGetExpire() {
+ String key = "access_token", value = String.valueOf(System.currentTimeMillis());
+ wxRedisOps.setValue(key, value, -1, TimeUnit.SECONDS);
+ Assert.assertTrue(wxRedisOps.getExpire(key) < 0);
+ wxRedisOps.setValue(key, value, 4, TimeUnit.SECONDS);
+ Long expireSeconds = wxRedisOps.getExpire(key);
+ Assert.assertTrue(expireSeconds <= 4 && expireSeconds >= 0);
+ }
+
+ @Test
+ public void testExpire() {
+ String key = "access_token", value = String.valueOf(System.currentTimeMillis());
+ wxRedisOps.setValue(key, value, -1, TimeUnit.SECONDS);
+ wxRedisOps.expire(key, 4, TimeUnit.SECONDS);
+ Long expireSeconds = wxRedisOps.getExpire(key);
+ Assert.assertTrue(expireSeconds <= 4 && expireSeconds >= 0);
+ }
+
+ @Test
+ public void testGetLock() {
+ Assert.assertNotNull(wxRedisOps.getLock("access_token_lock"));
+ }
+}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/JedisWxRedisOpsTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/JedisWxRedisOpsTest.java
new file mode 100644
index 0000000000..2ff2c37b81
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/JedisWxRedisOpsTest.java
@@ -0,0 +1,21 @@
+package me.chanjar.weixin.common.redis;
+
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import redis.clients.jedis.JedisPool;
+
+public class JedisWxRedisOpsTest extends CommonWxRedisOpsTest {
+
+ JedisPool jedisPool;
+
+ @BeforeTest
+ public void init() {
+ this.jedisPool = new JedisPool("127.0.0.1", 6379);
+ this.wxRedisOps = new JedisWxRedisOps(jedisPool);
+ }
+
+ @AfterTest
+ public void destroy() {
+ this.jedisPool.close();
+ }
+}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOpsTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOpsTest.java
new file mode 100644
index 0000000000..bf3b35a7cc
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOpsTest.java
@@ -0,0 +1,26 @@
+package me.chanjar.weixin.common.redis;
+
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+
+public class RedisTemplateWxRedisOpsTest extends CommonWxRedisOpsTest {
+
+ StringRedisTemplate redisTemplate;
+
+ @BeforeTest
+ public void init() {
+ JedisConnectionFactory connectionFactory = new JedisConnectionFactory();
+ connectionFactory.setHostName("127.0.0.1");
+ connectionFactory.setPort(6379);
+ connectionFactory.afterPropertiesSet();
+ StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory);
+ this.redisTemplate = redisTemplate;
+ this.wxRedisOps = new RedisTemplateWxRedisOps(this.redisTemplate);
+ }
+
+ @AfterTest
+ public void destroy() {
+ }
+}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedissonWxRedisOpsTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedissonWxRedisOpsTest.java
new file mode 100644
index 0000000000..48cf7b29be
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedissonWxRedisOpsTest.java
@@ -0,0 +1,27 @@
+package me.chanjar.weixin.common.redis;
+
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.redisson.config.TransportMode;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+
+public class RedissonWxRedisOpsTest extends CommonWxRedisOpsTest {
+
+ RedissonClient redissonClient;
+
+ @BeforeTest
+ public void init() {
+ Config config = new Config();
+ config.useSingleServer().setAddress("redis://127.0.0.1:6379");
+ config.setTransportMode(TransportMode.NIO);
+ this.redissonClient = Redisson.create(config);
+ this.wxRedisOps = new RedissonWxRedisOps(this.redissonClient);
+ }
+
+ @AfterTest
+ public void destroy() {
+ this.redissonClient.shutdown();
+ }
+}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/session/SessionTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/session/SessionTest.java
index e0248c7960..84d80eab0d 100644
--- a/weixin-java-common/src/test/java/me/chanjar/weixin/common/session/SessionTest.java
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/session/SessionTest.java
@@ -1,8 +1,7 @@
package me.chanjar.weixin.common.session;
-import org.testng.Assert;
-import org.testng.annotations.DataProvider;
-import org.testng.annotations.Test;
+import org.testng.*;
+import org.testng.annotations.*;
@Test
public class SessionTest {
@@ -11,7 +10,7 @@ public class SessionTest {
public Object[][] getSessionManager() {
return new Object[][]{
- new Object[]{new StandardSessionManager()}
+ new Object[]{new StandardSessionManager()}
};
}
@@ -81,7 +80,7 @@ public void testBackgroundProcess(WxSessionManager sessionManager) throws Interr
InternalSession abc = ism.createSession("abc");
abc.endAccess();
- Thread.sleep(2000l);
+ Thread.sleep(2000);
Assert.assertEquals(ism.getActiveSessions(), 0);
}
@@ -100,7 +99,7 @@ public void testBackgroundProcess2(WxSessionManager sessionManager) throws Inter
abc.setMaxInactiveInterval(1);
abc.endAccess();
- Thread.sleep(2000l);
+ Thread.sleep(2000);
Assert.assertEquals(ism.getActiveSessions(), 0);
}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/DataUtilsTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/DataUtilsTest.java
new file mode 100644
index 0000000000..f5732d9a0b
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/DataUtilsTest.java
@@ -0,0 +1,22 @@
+package me.chanjar.weixin.common.util;
+
+import org.testng.annotations.*;
+
+import static org.testng.Assert.*;
+
+/**
+ *
+ * Created by BinaryWang on 2018/5/8.
+ *
+ *
+ * @author Binary Wang
+ */
+public class DataUtilsTest {
+
+ @Test
+ public void testHandleDataWithSecret() {
+ String data = "js_code=001tZveq0SMoiq1AEXeq0ECJeq0tZveZ&secret=5681022fa1643845392367ea88888888&grant_type=authorization_code&appid=wxe156d4848d999999";
+ final String s = DataUtils.handleDataWithSecret(data);
+ assertTrue(s.contains("&secret=******&"));
+ }
+}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/XmlUtilsTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/XmlUtilsTest.java
new file mode 100644
index 0000000000..7b6bb536f4
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/XmlUtilsTest.java
@@ -0,0 +1,89 @@
+package me.chanjar.weixin.common.util;
+
+import org.testng.annotations.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ *
+ *
+ * Created by Binary Wang on 2018/11/4.
+ *
+ *
+ * @author Binary Wang
+ */
+public class XmlUtilsTest {
+
+ @Test(expectedExceptions = {RuntimeException.class})
+ public void testXml2Map_xxe() {
+ String xml = "\n" +
+ "\n" +
+ "\n" +
+ "]>\n" +
+ " ";
+ XmlUtils.xml2Map(xml);
+ }
+
+ @Test
+ public void testXml2Map() {
+ String xml = "\n" +
+ "\n" +
+ "2 \n" +
+ "\n" +
+ "- \n" +
+ "
1 \n" +
+ "0 \n" +
+ "2 \n" +
+ " \n" +
+ "1 \n" +
+ "1 \n" +
+ "1 \n" +
+ "1 \n" +
+ " \n" +
+ "- \n" +
+ "
2 \n" +
+ "0 \n" +
+ "2 \n" +
+ " \n" +
+ "1 \n" +
+ "1 \n" +
+ "1 \n" +
+ "1 \n" +
+ " \n" +
+ " \n" +
+ "2 \n" +
+ " \n" +
+ " ";
+
+ final Map map = XmlUtils.xml2Map(xml);
+ assertThat(map).isNotNull();
+ final Map copyrightCheckResult = (Map) map.get("CopyrightCheckResult");
+ List> resultList = (List>) ((Map) copyrightCheckResult.get("ResultList")).get("item");
+ assertThat(copyrightCheckResult).isNotNull();
+
+ assertThat(copyrightCheckResult.get("Count")).isEqualTo("2");
+ assertThat(copyrightCheckResult.get("CheckState")).isEqualTo("2");
+
+ assertThat(resultList.get(0).get("ArticleIdx")).isEqualTo("1");
+ assertThat(resultList.get(0).get("UserDeclareState")).isEqualTo("0");
+ assertThat(resultList.get(0).get("AuditState")).isEqualTo("2");
+ assertThat(resultList.get(0).get("OriginalArticleUrl")).isEqualTo("Url_1");
+ assertThat(resultList.get(0).get("OriginalArticleType")).isEqualTo("1");
+ assertThat(resultList.get(0).get("CanReprint")).isEqualTo("1");
+ assertThat(resultList.get(0).get("NeedReplaceContent")).isEqualTo("1");
+ assertThat(resultList.get(0).get("NeedShowReprintSource")).isEqualTo("1");
+
+ assertThat(resultList.get(1).get("ArticleIdx")).isEqualTo("2");
+ assertThat(resultList.get(1).get("UserDeclareState")).isEqualTo("0");
+ assertThat(resultList.get(1).get("AuditState")).isEqualTo("2");
+ assertThat(resultList.get(1).get("OriginalArticleUrl")).isEqualTo("Url_2");
+ assertThat(resultList.get(1).get("OriginalArticleType")).isEqualTo("1");
+ assertThat(resultList.get(1).get("CanReprint")).isEqualTo("1");
+ assertThat(resultList.get(1).get("NeedReplaceContent")).isEqualTo("1");
+ assertThat(resultList.get(1).get("NeedShowReprintSource")).isEqualTo("1");
+ }
+}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/crypto/SHA1Test.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/crypto/SHA1Test.java
new file mode 100644
index 0000000000..ffdb613128
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/crypto/SHA1Test.java
@@ -0,0 +1,36 @@
+package me.chanjar.weixin.common.util.crypto;
+
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
+/**
+ *
+ * Created by BinaryWang on 2017/6/10.
+ *
+ *
+ * @author Binary Wang
+ */
+public class SHA1Test {
+ @Test
+ public void testGen() throws Exception {
+ final String result = SHA1.gen("123", "345");
+ assertNotNull(result);
+ assertEquals(result,"9f537aeb751ec72605f57f94a2f6dc3e3958e1dd");
+ }
+
+ @Test(expectedExceptions = {IllegalArgumentException.class})
+ public void testGen_illegalArguments() {
+ final String result = SHA1.gen(null, "", "345");
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testGenWithAmple() throws Exception {
+ final String result = SHA1.genWithAmple("123", "345");
+ assertNotNull(result);
+ assertEquals(result,"20b896ccbd5a72dde5dbe0878ff985e4069771c6");
+ }
+
+}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/crypto/WxCryptUtilTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/crypto/WxCryptUtilTest.java
index 91095a20e9..82cfa9d2d6 100755
--- a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/crypto/WxCryptUtilTest.java
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/crypto/WxCryptUtilTest.java
@@ -1,20 +1,19 @@
package me.chanjar.weixin.common.util.crypto;
-import org.testng.annotations.Test;
+import java.io.IOException;
+import java.io.StringReader;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.testng.annotations.*;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-import java.io.IOException;
-import java.io.StringReader;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.fail;
+import static org.testng.Assert.*;
@Test
public class WxCryptUtilTest {
@@ -40,6 +39,8 @@ public void testNormal() throws ParserConfigurationException, SAXException, IOEx
System.out.println(encryptedXml);
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
+ documentBuilderFactory.setExpandEntityReferences(false);
+ documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = documentBuilder.parse(new InputSource(new StringReader(encryptedXml)));
@@ -77,11 +78,14 @@ public void testAesEncrypt2() {
}
public void testValidateSignatureError() throws ParserConfigurationException, SAXException,
- IOException {
+ IOException {
try {
WxCryptUtil pc = new WxCryptUtil(this.token, this.encodingAesKey, this.appId);
String afterEncrpt = pc.encrypt(this.replyMsg);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+ dbf.setExpandEntityReferences(false);
+ dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+
DocumentBuilder db = dbf.newDocumentBuilder();
StringReader sr = new StringReader(afterEncrpt);
InputSource is = new InputSource(sr);
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilderTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilderTest.java
new file mode 100644
index 0000000000..24a45eea09
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilderTest.java
@@ -0,0 +1,64 @@
+package me.chanjar.weixin.common.util.http.apache;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class DefaultApacheHttpClientBuilderTest {
+ @Test
+ public void testBuild() throws Exception {
+ DefaultApacheHttpClientBuilder builder1 = DefaultApacheHttpClientBuilder.get();
+ DefaultApacheHttpClientBuilder builder2 = DefaultApacheHttpClientBuilder.get();
+ Assert.assertSame(builder1, builder2, "DefaultApacheHttpClientBuilder为单例,获取到的对象应该相同");
+ List threadList = new ArrayList<>(10);
+ for (int i = 0; i < 10; i++) {
+ TestThread thread = new TestThread();
+ thread.start();
+ threadList.add(thread);
+ }
+ for (TestThread testThread : threadList) {
+ testThread.join();
+ Assert.assertNotEquals(-1,testThread.getRespState(),"请求响应code不应为-1");
+ }
+
+ for (int i = 1; i < threadList.size(); i++) {
+ TestThread thread1 = threadList.get(i - 1);
+ TestThread thread2 = threadList.get(i);
+ Assert.assertSame(
+ thread1.getClient(),
+ thread2.getClient(),
+ "DefaultApacheHttpClientBuilder为单例,并持有了相同的HttpClient"
+ );
+ }
+ }
+
+
+ public static class TestThread extends Thread {
+ private CloseableHttpClient client;
+ private int respState = -1;
+
+ @Override
+ public void run() {
+ client = DefaultApacheHttpClientBuilder.get().build();
+ HttpGet httpGet = new HttpGet("http://www.sina.com.cn/");
+ try (CloseableHttpResponse resp = client.execute(httpGet)){
+ respState = resp.getStatusLine().getStatusCode();
+ } catch (IOException ignored) {
+ }
+ }
+
+ public CloseableHttpClient getClient() {
+ return client;
+ }
+
+ public int getRespState() {
+ return respState;
+ }
+ }
+}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockTest.java
new file mode 100644
index 0000000000..50a17ed94b
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockTest.java
@@ -0,0 +1,79 @@
+package me.chanjar.weixin.common.util.locks;
+
+import lombok.SneakyThrows;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.testng.Assert.*;
+
+@Test(enabled = false)
+public class RedisTemplateSimpleDistributedLockTest {
+
+ RedisTemplateSimpleDistributedLock redisLock;
+
+ StringRedisTemplate redisTemplate;
+
+ AtomicInteger lockCurrentExecuteCounter;
+
+ @BeforeTest
+ public void init() {
+ JedisConnectionFactory connectionFactory = new JedisConnectionFactory();
+ connectionFactory.setHostName("127.0.0.1");
+ connectionFactory.setPort(6379);
+ connectionFactory.afterPropertiesSet();
+ StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory);
+ this.redisTemplate = redisTemplate;
+ this.redisLock = new RedisTemplateSimpleDistributedLock(redisTemplate, 60000);
+ this.lockCurrentExecuteCounter = new AtomicInteger(0);
+ }
+
+ @Test(description = "多线程测试锁排他性")
+ public void testLockExclusive() throws InterruptedException {
+ int threadSize = 100;
+ final CountDownLatch startLatch = new CountDownLatch(threadSize);
+ final CountDownLatch endLatch = new CountDownLatch(threadSize);
+
+ for (int i = 0; i < threadSize; i++) {
+ new Thread(new Runnable() {
+ @SneakyThrows
+ @Override
+ public void run() {
+ startLatch.await();
+
+ redisLock.lock();
+ assertEquals(lockCurrentExecuteCounter.incrementAndGet(), 1, "临界区同时只能有一个线程执行");
+ lockCurrentExecuteCounter.decrementAndGet();
+ redisLock.unlock();
+
+ endLatch.countDown();
+ }
+ }).start();
+ startLatch.countDown();
+ }
+ endLatch.await();
+ }
+
+ @Test
+ public void testTryLock() throws InterruptedException {
+ assertTrue(redisLock.tryLock(3, TimeUnit.SECONDS), "第一次加锁应该成功");
+ assertNotNull(redisLock.getLockSecretValue());
+ String redisValue = this.redisTemplate.opsForValue().get(redisLock.getKey());
+ assertEquals(redisValue, redisLock.getLockSecretValue());
+
+ redisLock.unlock();
+ assertNull(redisLock.getLockSecretValue());
+ redisValue = this.redisTemplate.opsForValue().get(redisLock.getKey());
+ assertNull(redisValue, "释放锁后key会被删除");
+
+ redisLock.unlock();
+ }
+
+
+}
+
diff --git a/weixin-java-common/src/test/resources/logback-test.xml b/weixin-java-common/src/test/resources/logback-test.xml
index 2c421e49ab..9a6fe3eea1 100644
--- a/weixin-java-common/src/test/resources/logback-test.xml
+++ b/weixin-java-common/src/test/resources/logback-test.xml
@@ -1,17 +1,12 @@
-
-
-
- %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+ %d{HH:mm:ss.SSS} [%thread] %-5level %replace(%caller{1}){'Caller', ''} - %msg%n
-
-
+
+
-
diff --git a/weixin-java-common/src/test/resources/testng.xml b/weixin-java-common/src/test/resources/testng.xml
index f2222275b2..9eeba0df4c 100644
--- a/weixin-java-common/src/test/resources/testng.xml
+++ b/weixin-java-common/src/test/resources/testng.xml
@@ -7,8 +7,8 @@
-
-
+
+
diff --git a/weixin-java-cp/build.gradle b/weixin-java-cp/build.gradle
deleted file mode 100644
index 85d7c4bba2..0000000000
--- a/weixin-java-cp/build.gradle
+++ /dev/null
@@ -1,12 +0,0 @@
-
-description = 'WeiXin Java Tools - CP'
-dependencies {
- compile project(':weixin-java-common')
- testCompile group: 'junit', name: 'junit', version:'4.11'
- testCompile group: 'org.testng', name: 'testng', version:'6.8.7'
- testCompile group: 'org.mockito', name: 'mockito-all', version:'1.9.5'
- testCompile group: 'com.google.inject', name: 'guice', version:'3.0'
- testCompile group: 'org.eclipse.jetty', name: 'jetty-server', version:'9.3.0.RC0'
- testCompile group: 'org.eclipse.jetty', name: 'jetty-servlet', version:'9.3.0.RC0'
-}
-test.useTestNG()
diff --git a/weixin-java-cp/pom.xml b/weixin-java-cp/pom.xml
index 88f83f0f24..5d9fdae34c 100644
--- a/weixin-java-cp/pom.xml
+++ b/weixin-java-cp/pom.xml
@@ -1,17 +1,18 @@
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+ xmlns="http://maven.apache.org/POM/4.0.0">
4.0.0
com.github.binarywang
- weixin-java-parent
- 2.6.0
+ wx-java
+ 3.9.0
weixin-java-cp
- WeiXin Java Tools - CP
- 微信企业号Java SDK
+ WxJava - CP Java SDK
+ 微信企业号/企业微信 Java SDK
@@ -19,10 +20,29 @@
weixin-java-common
${project.version}
+
+ org.jodd
+ jodd-http
+ provided
+
+
+ com.squareup.okhttp3
+ okhttp
+ provided
+
redis.clients
jedis
+
+ org.slf4j
+ slf4j-api
+
+
+
+ org.redisson
+ redisson
+
org.testng
testng
@@ -48,6 +68,21 @@
jetty-servlet
test
+
+ ch.qos.logback
+ logback-classic
+ test
+
+
+ org.projectlombok
+ lombok
+
+
+
+ org.assertj
+ assertj-guava
+ test
+
@@ -64,4 +99,36 @@
+
+
+ native-image
+
+ false
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.5.1
+
+
+ cn.binarywang.wx.graal.GraalProcessor,lombok.launch.AnnotationProcessorHider$AnnotationProcessor,lombok.launch.AnnotationProcessorHider$ClaimingProcessor
+
+
+
+ com.github.binarywang
+ weixin-graal
+ ${project.version}
+
+
+
+
+
+
+
+
+
+
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpAgentService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpAgentService.java
new file mode 100644
index 0000000000..d57ca56c21
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpAgentService.java
@@ -0,0 +1,51 @@
+package me.chanjar.weixin.cp.api;
+
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.bean.WxCpAgent;
+
+import java.util.List;
+
+/**
+ *
+ * 管理企业号应用
+ * 文档地址:https://work.weixin.qq.com/api/doc#10087
+ * Created by huansinho on 2018/4/13.
+ *
+ *
+ * @author huansinho
+ */
+public interface WxCpAgentService {
+ /**
+ *
+ * 获取企业号应用信息
+ * 该API用于获取企业号某个应用的基本信息,包括头像、昵称、帐号类型、认证类型、可见范围等信息
+ * 详情请见: https://work.weixin.qq.com/api/doc#10087
+ *
+ *
+ * @param agentId 企业应用的id
+ * @return 部门id
+ */
+ WxCpAgent get(Integer agentId) throws WxErrorException;
+
+ /**
+ *
+ * 设置应用.
+ * 仅企业可调用,可设置当前凭证对应的应用;第三方不可调用。
+ * 详情请见: https://work.weixin.qq.com/api/doc#10088
+ *
+ *
+ * @param agentInfo 应用信息
+ */
+ void set(WxCpAgent agentInfo) throws WxErrorException;
+
+ /**
+ *
+ * 获取应用列表.
+ * 企业仅可获取当前凭证对应的应用;第三方仅可获取被授权的应用。
+ * 详情请见: https://work.weixin.qq.com/api/doc#11214
+ *
+ *
+ */
+ List list() throws WxErrorException;
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpChatService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpChatService.java
new file mode 100644
index 0000000000..462ec75071
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpChatService.java
@@ -0,0 +1,59 @@
+package me.chanjar.weixin.cp.api;
+
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.bean.WxCpAppChatMessage;
+import me.chanjar.weixin.cp.bean.WxCpChat;
+
+import java.util.List;
+
+/**
+ * 群聊服务.
+ *
+ * @author gaigeshen
+ */
+public interface WxCpChatService {
+ /**
+ * 创建群聊会话,注意:刚创建的群,如果没有下发消息,在企业微信不会出现该群.
+ *
+ * @param name 群聊名,最多50个utf8字符,超过将截断
+ * @param owner 指定群主的id。如果不指定,系统会随机从userlist中选一人作为群主
+ * @param users 群成员id列表。至少2人,至多500人
+ * @param chatId 群聊的唯一标志,不能与已有的群重复;字符串类型,最长32个字符。只允许字符0-9及字母a-zA-Z。如果不填,系统会随机生成群id
+ * @return 创建的群聊会话chatId
+ * @throws WxErrorException 异常
+ */
+ String create(String name, String owner, List users, String chatId) throws WxErrorException;
+
+ /**
+ * 修改群聊会话.
+ *
+ * @param chatId 群聊id
+ * @param name 新的群聊名。若不需更新,请忽略此参数(null or empty)。最多50个utf8字符,超过将截断
+ * @param owner 新群主的id。若不需更新,请忽略此参数(null or empty)
+ * @param usersToAdd 添加成员的id列表,若不需要更新,则传递空对象或者空集合
+ * @param usersToDelete 踢出成员的id列表,若不需要更新,则传递空对象或者空集合
+ * @throws WxErrorException 异常
+ */
+ void update(String chatId, String name, String owner, List usersToAdd, List usersToDelete) throws WxErrorException;
+
+ /**
+ * 获取群聊会话.
+ *
+ * @param chatId 群聊编号
+ * @return 群聊会话
+ * @throws WxErrorException 异常
+ */
+ WxCpChat get(String chatId) throws WxErrorException;
+
+ /**
+ * 应用支持推送文本、图片、视频、文件、图文等类型.
+ * 请求方式: POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/appchat/send?access_token=ACCESS_TOKEN
+ * 文档地址:https://work.weixin.qq.com/api/doc#90000/90135/90248
+ *
+ * @param message 要发送的消息内容对象
+ * @throws WxErrorException 异常
+ */
+ void sendMsg(WxCpAppChatMessage message) throws WxErrorException;
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpConfigStorage.java
deleted file mode 100644
index eb1e57cafa..0000000000
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpConfigStorage.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package me.chanjar.weixin.cp.api;
-
-import me.chanjar.weixin.common.bean.WxAccessToken;
-import me.chanjar.weixin.common.util.http.ApacheHttpClientBuilder;
-
-import java.io.File;
-
-/**
- * 微信客户端配置存储
- *
- * @author Daniel Qian
- */
-public interface WxCpConfigStorage {
-
- String getAccessToken();
-
- boolean isAccessTokenExpired();
-
- /**
- * 强制将access token过期掉
- */
- void expireAccessToken();
-
- void updateAccessToken(WxAccessToken accessToken);
-
- void updateAccessToken(String accessToken, int expiresIn);
-
- String getJsapiTicket();
-
- boolean isJsapiTicketExpired();
-
- /**
- * 强制将jsapi ticket过期掉
- */
- void expireJsapiTicket();
-
- /**
- * 应该是线程安全的
- *
- * @param jsapiTicket
- */
- void updateJsapiTicket(String jsapiTicket, int expiresInSeconds);
-
- String getCorpId();
-
- String getCorpSecret();
-
- Integer getAgentId();
-
- String getToken();
-
- String getAesKey();
-
- long getExpiresTime();
-
- String getOauth2redirectUri();
-
- String getHttpProxyHost();
-
- int getHttpProxyPort();
-
- String getHttpProxyUsername();
-
- String getHttpProxyPassword();
-
- File getTmpDirFile();
-
- /**
- * http client builder
- *
- * @return ApacheHttpClientBuilder
- */
- ApacheHttpClientBuilder getApacheHttpClientBuilder();
-}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpDepartmentService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpDepartmentService.java
new file mode 100644
index 0000000000..c86816b7f2
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpDepartmentService.java
@@ -0,0 +1,66 @@
+package me.chanjar.weixin.cp.api;
+
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.bean.WxCpDepart;
+
+import java.util.List;
+
+/**
+ *
+ * 部门管理接口
+ * Created by BinaryWang on 2017/6/24.
+ *
+ *
+ * @author Binary Wang
+ */
+public interface WxCpDepartmentService {
+
+ /**
+ *
+ * 部门管理接口 - 创建部门.
+ * 最多支持创建500个部门
+ * 详情请见: https://work.weixin.qq.com/api/doc#90000/90135/90205
+ *
+ *
+ * @param depart 部门
+ * @return 部门id
+ * @throws WxErrorException 异常
+ */
+ Long create(WxCpDepart depart) throws WxErrorException;
+
+ /**
+ *
+ * 部门管理接口 - 获取部门列表.
+ * 详情请见: https://work.weixin.qq.com/api/doc#90000/90135/90208
+ *
+ *
+ * @param id 部门id。获取指定部门及其下的子部门。非必需,可为null
+ * @return 获取的部门列表
+ * @throws WxErrorException 异常
+ */
+ List list(Long id) throws WxErrorException;
+
+ /**
+ *
+ * 部门管理接口 - 更新部门.
+ * 详情请见: https://work.weixin.qq.com/api/doc#90000/90135/90206
+ * 如果id为0(未部门),1(黑名单),2(星标组),或者不存在的id,微信会返回系统繁忙的错误
+ *
+ *
+ * @param group 要更新的group,group的id,name必须设置
+ * @throws WxErrorException 异常
+ */
+ void update(WxCpDepart group) throws WxErrorException;
+
+ /**
+ *
+ * 部门管理接口 - 删除部门.
+ * 详情请见: https://work.weixin.qq.com/api/doc#90000/90135/90207
+ * 应用须拥有指定部门的管理权限
+ *
+ *
+ * @param departId 部门id
+ * @throws WxErrorException 异常
+ */
+ void delete(Long departId) throws WxErrorException;
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExternalContactService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExternalContactService.java
new file mode 100644
index 0000000000..a386b0ead2
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExternalContactService.java
@@ -0,0 +1,359 @@
+package me.chanjar.weixin.cp.api;
+
+import lombok.NonNull;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.bean.WxCpBaseResp;
+import me.chanjar.weixin.cp.bean.external.*;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ *
+ * 外部联系人管理接口,企业微信的外部联系人的接口和通讯录接口已经拆离
+ * Created by Joe Cao on 2019/6/14
+ *
+ *
+ * @author JoeCao
+ */
+public interface WxCpExternalContactService {
+
+ /**
+ * 配置客户联系「联系我」方式
+ *
+ * 企业可以在管理后台-客户联系中配置成员的「联系我」的二维码或者小程序按钮,客户通过扫描二维码或点击小程序上的按钮,即可获取成员联系方式,主动联系到成员。
+ * 企业可通过此接口为具有客户联系功能的成员生成专属的「联系我」二维码或者「联系我」按钮。
+ * 如果配置的是「联系我」按钮,需要开发者的小程序接入小程序插件。
+ *
+ * 注意:
+ * 通过API添加的「联系我」不会在管理端进行展示,每个企业可通过API最多配置50万个「联系我」。
+ * 用户需要妥善存储返回的config_id,config_id丢失可能导致用户无法编辑或删除「联系我」。
+ * 临时会话模式不占用「联系我」数量,但每日最多添加10万个,并且仅支持单人。
+ * 临时会话模式的二维码,添加好友完成后该二维码即刻失效。
+ *
+ *
+ * @param info 客户联系「联系我」方式
+ * @return wx cp contact way result
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpContactWayResult addContactWay(@NonNull WxCpContactWayInfo info) throws WxErrorException;
+
+ /**
+ * 获取企业已配置的「联系我」方式
+ *
+ *
+ * 批量 获取企业配置的「联系我」二维码和「联系我」小程序按钮。
+ *
+ *
+ * @param configId 联系方式的配置id,必填
+ * @return contact way
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpContactWayInfo getContactWay(@NonNull String configId) throws WxErrorException;
+
+ /**
+ * 更新企业已配置的「联系我」方式
+ *
+ *
+ * 更新企业配置的「联系我」二维码和「联系我」小程序按钮中的信息,如使用人员和备注等。
+ *
+ *
+ * @param info 客户联系「联系我」方式
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp updateContactWay(@NonNull WxCpContactWayInfo info) throws WxErrorException;
+
+ /**
+ * 删除企业已配置的「联系我」方式
+ *
+ *
+ * 删除一个已配置的「联系我」二维码或者「联系我」小程序按钮。
+ *
+ *
+ * @param configId 企业联系方式的配置id,必填
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp deleteContactWay(@NonNull String configId) throws WxErrorException;
+
+ /**
+ * 结束临时会话
+ *
+ *
+ * 将指定的企业成员和客户之前的临时会话断开,断开前会自动下发已配置的结束语。
+ *
+ * 注意:请保证传入的企业成员和客户之间有仍然有效的临时会话, 通过其他方式的添加外部联系人无法通过此接口关闭会话 。
+ *
+ *
+ * @param userId the user id
+ * @param externalUserId the external user id
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp closeTempChat(@NonNull String userId, @NonNull String externalUserId) throws WxErrorException;
+
+
+ /**
+ * 获取外部联系人详情.
+ *
+ * 企业可通过此接口,根据外部联系人的userid,拉取外部联系人详情。权限说明:
+ * 企业需要使用外部联系人管理secret所获取的accesstoken来调用
+ * 第三方应用需拥有“企业客户”权限。
+ * 第三方应用调用时,返回的跟进人follow_user仅包含应用可见范围之内的成员。
+ *
+ *
+ * @param userId 外部联系人的userid
+ * @return . external contact
+ * @throws WxErrorException the wx error exception
+ * @deprecated 建议使用 {@link #getContactDetail(String)}
+ */
+ @Deprecated
+ WxCpUserExternalContactInfo getExternalContact(String userId) throws WxErrorException;
+
+ /**
+ * 获取客户详情.
+ *
+ *
+ * 企业可通过此接口,根据外部联系人的userid(如何获取?),拉取客户详情。
+ *
+ * 请求方式:GET(HTTPS)
+ * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get?access_token=ACCESS_TOKEN&external_userid=EXTERNAL_USERID
+ *
+ * 权限说明:
+ *
+ * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?);
+ * 第三方/自建应用调用时,返回的跟进人follow_user仅包含应用可见范围之内的成员。
+ *
+ *
+ * @param userId 外部联系人的userid,注意不是企业成员的帐号
+ * @return . contact detail
+ * @throws WxErrorException .
+ */
+ WxCpUserExternalContactInfo getContactDetail(String userId) throws WxErrorException;
+
+ /**
+ * 获取客户列表.
+ *
+ * 企业可通过此接口获取指定成员添加的客户列表。客户是指配置了客户联系功能的成员所添加的外部联系人。没有配置客户联系功能的成员,所添加的外部联系人将不会作为客户返回。
+ *
+ * 请求方式:GET(HTTPS)
+ * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list?access_token=ACCESS_TOKEN&userid=USERID
+ *
+ * 权限说明:
+ *
+ * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?);
+ * 第三方应用需拥有“企业客户”权限。
+ * 第三方/自建应用只能获取到可见范围内的配置了客户联系功能的成员。
+ *
+ *
+ * @param userId 企业成员的userid
+ * @return List of External wx id
+ * @throws WxErrorException .
+ */
+ List listExternalContacts(String userId) throws WxErrorException;
+
+ /**
+ * 企业和第三方服务商可通过此接口获取配置了客户联系功能的成员(Customer Contact)列表。
+ *
+ * 企业需要使用外部联系人管理secret所获取的accesstoken来调用(accesstoken如何获取?);
+ * 第三方应用需拥有“企业客户”权限。
+ * 第三方应用只能获取到可见范围内的配置了客户联系功能的成员
+ *
+ *
+ * @return List of CpUser id
+ * @throws WxErrorException .
+ */
+ List listFollowers() throws WxErrorException;
+
+ /**
+ * 企业和第三方可通过此接口,获取所有离职成员的客户列表,并可进一步调用离职成员的外部联系人再分配接口将这些客户重新分配给其他企业成员。
+ *
+ * @param page the page
+ * @param pageSize the page size
+ * @return wx cp user external unassign list
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpUserExternalUnassignList listUnassignedList(Integer page, Integer pageSize) throws WxErrorException;
+
+ /**
+ * 企业可通过此接口,将已离职成员的外部联系人分配给另一个成员接替联系。
+ *
+ * @param externalUserid the external userid
+ * @param handOverUserid the hand over userid
+ * @param takeOverUserid the take over userid
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp transferExternalContact(String externalUserid, String handOverUserid, String takeOverUserid) throws WxErrorException;
+
+ /**
+ *
+ * 该接口用于获取配置过客户群管理的客户群列表。
+ * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+ * 暂不支持第三方调用。
+ * 微信文档:https://work.weixin.qq.com/api/doc/90000/90135/92119
+ *
+ *
+ * @param pageIndex the page index
+ * @param pageSize the page size
+ * @param status the status
+ * @param userIds the user ids
+ * @param partyIds the party ids
+ * @return the wx cp user external group chat list
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpUserExternalGroupChatList listGroupChat(Integer pageIndex, Integer pageSize, int status, String[] userIds, String[] partyIds) throws WxErrorException;
+
+ /**
+ *
+ * 通过客户群ID,获取详情。包括群名、群成员列表、群成员入群时间、入群方式。(客户群是由具有客户群使用权限的成员创建的外部群)
+ * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+ * 暂不支持第三方调用。
+ * 微信文档:https://work.weixin.qq.com/api/doc/90000/90135/92122
+ *
+ *
+ * @param chatId the chat id
+ * @return group chat
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpUserExternalGroupChatInfo getGroupChat(String chatId) throws WxErrorException;
+
+ /**
+ *
+ * 企业可通过此接口获取成员联系客户的数据,包括发起申请数、新增客户数、聊天数、发送消息数和删除/拉黑成员的客户数等指标。
+ * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+ * 第三方应用需拥有“企业客户”权限。
+ * 第三方/自建应用调用时传入的userid和partyid要在应用的可见范围内;
+ *
+ *
+ * @param startTime the start time
+ * @param endTime the end time
+ * @param userIds the user ids
+ * @param partyIds the party ids
+ * @return user behavior statistic
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpUserExternalUserBehaviorStatistic getUserBehaviorStatistic(Date startTime, Date endTime, String[] userIds, String[] partyIds) throws WxErrorException;
+
+ /**
+ *
+ * 获取指定日期全天的统计数据。注意,企业微信仅存储60天的数据。
+ * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+ * 暂不支持第三方调用。
+ *
+ *
+ * @param startTime the start time
+ * @param orderBy the order by
+ * @param orderAsc the order asc
+ * @param pageIndex the page index
+ * @param pageSize the page size
+ * @param userIds the user ids
+ * @param partyIds the party ids
+ * @return group chat statistic
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpUserExternalGroupChatStatistic getGroupChatStatistic(Date startTime, Integer orderBy, Integer orderAsc, Integer pageIndex, Integer pageSize, String[] userIds, String[] partyIds) throws WxErrorException;
+
+ /**
+ * 添加企业群发消息任务
+ * 企业可通过此接口添加企业群发消息的任务并通知客服人员发送给相关客户或客户群。(注:企业微信终端需升级到2.7.5版本及以上)
+ * 注意:调用该接口并不会直接发送消息给客户/客户群,需要相关的客服人员操作以后才会实际发送(客服人员的企业微信需要升级到2.7.5及以上版本)
+ * 同一个企业每个自然月内仅可针对一个客户/客户群发送4条消息,超过限制的用户将会被忽略。
+ *
+ * 请求方式: POST(HTTP)
+ *
+ * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_msg_template?access_token=ACCESS_TOKEN
+ *
+ * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/92135
+ *
+ * @param wxCpMsgTemplate the wx cp msg template
+ * @return the wx cp msg template add result
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpMsgTemplateAddResult addMsgTemplate(WxCpMsgTemplate wxCpMsgTemplate) throws WxErrorException;
+
+ /**
+ * 发送新客户欢迎语
+ *
+ * 企业微信在向企业推送添加外部联系人事件时,会额外返回一个welcome_code,企业以此为凭据调用接口,即可通过成员向新添加的客户发送个性化的欢迎语。
+ * 为了保证用户体验以及避免滥用,企业仅可在收到相关事件后20秒内调用,且只可调用一次。
+ * 如果企业已经在管理端为相关成员配置了可用的欢迎语,则推送添加外部联系人事件时不会返回welcome_code。
+ * 每次添加新客户时可能有多个企业自建应用/第三方应用收到带有welcome_code的回调事件,但仅有最先调用的可以发送成功。后续调用将返回41051(externaluser has started chatting)错误,请用户根据实际使用需求,合理设置应用可见范围,避免冲突。
+ * 请求方式: POST(HTTP)
+ *
+ * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/send_welcome_msg?access_token=ACCESS_TOKEN
+ *
+ * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/92137
+ *
+ *
+ * @param msg .
+ * @throws WxErrorException .
+ */
+ void sendWelcomeMsg(WxCpWelcomeMsg msg) throws WxErrorException;
+
+ /**
+ *
+ * 企业可通过此接口获取企业客户标签详情。
+ *
+ *
+ * @param tagId the tag id
+ * @return corp tag list
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpUserExternalTagGroupList getCorpTagList(String[] tagId) throws WxErrorException;
+
+ /**
+ *
+ * 企业可通过此接口向客户标签库中添加新的标签组和标签,每个企业最多可配置3000个企业标签。
+ * 暂不支持第三方调用。
+ *
+ *
+ * @param tagGroup the tag group
+ * @return wx cp user external tag group info
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpUserExternalTagGroupInfo addCorpTag(WxCpUserExternalTagGroupInfo tagGroup) throws WxErrorException;
+
+ /**
+ *
+ * 企业可通过此接口编辑客户标签/标签组的名称或次序值。
+ * 暂不支持第三方调用。
+ *
+ *
+ * @param id the id
+ * @param name the name
+ * @param order the order
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp editCorpTag(String id, String name, Integer order) throws WxErrorException;
+
+ /**
+ *
+ * 企业可通过此接口删除客户标签库中的标签,或删除整个标签组。
+ * 暂不支持第三方调用。
+ *
+ *
+ * @param tagId the tag id
+ * @param groupId the group id
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp delCorpTag(String[] tagId, String[] groupId) throws WxErrorException;
+
+ /**
+ *
+ * 企业可通过此接口为指定成员的客户添加上由企业统一配置的标签。
+ * https://work.weixin.qq.com/api/doc/90000/90135/92117
+ *
+ *
+ * @param userid the userid
+ * @param externalUserid the external userid
+ * @param addTag the add tag
+ * @param removeTag the remove tag
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp markTag(String userid, String externalUserid, String[] addTag, String[] removeTag) throws WxErrorException;
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpGroupRobotService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpGroupRobotService.java
new file mode 100644
index 0000000000..007dff78fc
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpGroupRobotService.java
@@ -0,0 +1,52 @@
+package me.chanjar.weixin.cp.api;
+
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.bean.article.NewArticle;
+
+import java.util.List;
+
+/**
+ * 微信群机器人消息发送api
+ * 文档地址:https://work.weixin.qq.com/help?doc_id=13376
+ * 调用地址:https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=
+ *
+ * @author yr
+ * @date 2020-8-20
+ */
+public interface WxCpGroupRobotService {
+
+ /**
+ * 发送text类型的消息
+ *
+ * @param content 文本内容,最长不超过2048个字节,必须是utf8编码
+ * @param mentionedList userId的列表,提醒群中的指定成员(@某个成员),@all表示提醒所有人,如果开发者获取不到userId,可以使用mentioned_mobile_list
+ * @param mobileList 手机号列表,提醒手机号对应的群成员(@某个成员),@all表示提醒所有人
+ * @throws WxErrorException 异常
+ */
+ void sendText(String content, List mentionedList, List mobileList) throws WxErrorException;
+
+ /**
+ * 发送markdown类型的消息
+ *
+ * @param content markdown内容,最长不超过4096个字节,必须是utf8编码
+ * @throws WxErrorException 异常
+ */
+ void sendMarkDown(String content) throws WxErrorException;
+
+ /**
+ * 发送image类型的消息
+ *
+ * @param base64 图片内容的base64编码
+ * @param md5 图片内容(base64编码前)的md5值
+ * @throws WxErrorException 异常
+ */
+ void sendImage(String base64, String md5) throws WxErrorException;
+
+ /**
+ * 发送news类型的消息
+ *
+ * @param articleList 图文消息,支持1到8条图文
+ * @throws WxErrorException 异常
+ */
+ void sendNews(List articleList) throws WxErrorException;
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpInMemoryConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpInMemoryConfigStorage.java
deleted file mode 100644
index 867dd6bb68..0000000000
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpInMemoryConfigStorage.java
+++ /dev/null
@@ -1,224 +0,0 @@
-package me.chanjar.weixin.cp.api;
-
-import me.chanjar.weixin.common.bean.WxAccessToken;
-import me.chanjar.weixin.common.util.ToStringUtils;
-import me.chanjar.weixin.common.util.http.ApacheHttpClientBuilder;
-
-import java.io.File;
-
-/**
- * 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化
- *
- * @author Daniel Qian
- */
-public class WxCpInMemoryConfigStorage implements WxCpConfigStorage {
-
- protected volatile String corpId;
- protected volatile String corpSecret;
-
- protected volatile String token;
- protected volatile String accessToken;
- protected volatile String aesKey;
- protected volatile Integer agentId;
- protected volatile long expiresTime;
-
- protected volatile String oauth2redirectUri;
-
- protected volatile String httpProxyHost;
- protected volatile int httpProxyPort;
- protected volatile String httpProxyUsername;
- protected volatile String httpProxyPassword;
-
- protected volatile String jsapiTicket;
- protected volatile long jsapiTicketExpiresTime;
-
- protected volatile File tmpDirFile;
-
- private volatile ApacheHttpClientBuilder apacheHttpClientBuilder;
-
- @Override
- public String getAccessToken() {
- return this.accessToken;
- }
-
- public void setAccessToken(String accessToken) {
- this.accessToken = accessToken;
- }
-
- @Override
- public boolean isAccessTokenExpired() {
- return System.currentTimeMillis() > this.expiresTime;
- }
-
- @Override
- public void expireAccessToken() {
- this.expiresTime = 0;
- }
-
- @Override
- public synchronized void updateAccessToken(WxAccessToken accessToken) {
- updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
- }
-
- @Override
- public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) {
- this.accessToken = accessToken;
- this.expiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000l;
- }
-
- @Override
- public String getJsapiTicket() {
- return this.jsapiTicket;
- }
-
- public void setJsapiTicket(String jsapiTicket) {
- this.jsapiTicket = jsapiTicket;
- }
-
- public long getJsapiTicketExpiresTime() {
- return this.jsapiTicketExpiresTime;
- }
-
- public void setJsapiTicketExpiresTime(long jsapiTicketExpiresTime) {
- this.jsapiTicketExpiresTime = jsapiTicketExpiresTime;
- }
-
- @Override
- public boolean isJsapiTicketExpired() {
- return System.currentTimeMillis() > this.jsapiTicketExpiresTime;
- }
-
- @Override
- public synchronized void updateJsapiTicket(String jsapiTicket, int expiresInSeconds) {
- this.jsapiTicket = jsapiTicket;
- // 预留200秒的时间
- this.jsapiTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000l;
- }
-
- @Override
- public void expireJsapiTicket() {
- this.jsapiTicketExpiresTime = 0;
- }
-
- @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;
- }
-
- @Override
- public String getToken() {
- return this.token;
- }
-
- 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;
- }
-
- public void setAesKey(String aesKey) {
- this.aesKey = aesKey;
- }
-
- @Override
- public Integer getAgentId() {
- return this.agentId;
- }
-
- public void setAgentId(Integer agentId) {
- this.agentId = agentId;
- }
-
- @Override
- public String getOauth2redirectUri() {
- return this.oauth2redirectUri;
- }
-
- public void setOauth2redirectUri(String oauth2redirectUri) {
- this.oauth2redirectUri = oauth2redirectUri;
- }
-
- @Override
- public String getHttpProxyHost() {
- return this.httpProxyHost;
- }
-
- public void setHttpProxyHost(String httpProxyHost) {
- this.httpProxyHost = httpProxyHost;
- }
-
- @Override
- public int getHttpProxyPort() {
- return this.httpProxyPort;
- }
-
- public void setHttpProxyPort(int httpProxyPort) {
- this.httpProxyPort = httpProxyPort;
- }
-
- @Override
- public String getHttpProxyUsername() {
- return this.httpProxyUsername;
- }
-
- public void setHttpProxyUsername(String httpProxyUsername) {
- this.httpProxyUsername = httpProxyUsername;
- }
-
- @Override
- public String getHttpProxyPassword() {
- return this.httpProxyPassword;
- }
-
- public void setHttpProxyPassword(String httpProxyPassword) {
- this.httpProxyPassword = httpProxyPassword;
- }
-
- @Override
- public String toString() {
- return ToStringUtils.toSimpleString(this);
- }
-
- @Override
- public File getTmpDirFile() {
- return this.tmpDirFile;
- }
-
- public void setTmpDirFile(File tmpDirFile) {
- this.tmpDirFile = tmpDirFile;
- }
-
- @Override
- public ApacheHttpClientBuilder getApacheHttpClientBuilder() {
- return this.apacheHttpClientBuilder;
- }
-
- public void setApacheHttpClientBuilder(ApacheHttpClientBuilder apacheHttpClientBuilder) {
- this.apacheHttpClientBuilder = apacheHttpClientBuilder;
- }
-}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java
new file mode 100644
index 0000000000..a51e04e175
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java
@@ -0,0 +1,87 @@
+package me.chanjar.weixin.cp.api;
+
+import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ *
+ * 媒体管理接口.
+ * Created by BinaryWang on 2017/6/24.
+ *
+ *
+ * @author Binary Wang
+ */
+public interface WxCpMediaService {
+
+ /**
+ *
+ * 上传多媒体文件.
+ * 上传的多媒体文件有格式和大小限制,如下:
+ * 图片(image): 1M,支持JPG格式
+ * 语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式
+ * 视频(video):10MB,支持MP4格式
+ * 缩略图(thumb):64KB,支持JPG格式
+ * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件
+ *
+ *
+ * @param mediaType 媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts}
+ * @param fileType 文件类型,请看{@link me.chanjar.weixin.common.api.WxConsts}
+ * @param inputStream 输入流,需要调用方控制关闭该输入流
+ */
+ WxMediaUploadResult upload(String mediaType, String fileType, InputStream inputStream)
+ throws WxErrorException, IOException;
+
+ /**
+ * 上传多媒体文件.
+ *
+ * @param mediaType 媒体类型
+ * @param file 文件对象
+ * @see #upload(String, String, InputStream)
+ */
+ WxMediaUploadResult upload(String mediaType, File file) throws WxErrorException;
+
+ /**
+ *
+ * 下载多媒体文件.
+ * 根据微信文档,视频文件下载不了,会返回null
+ * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件
+ *
+ *
+ * @param mediaId 媒体id
+ * @return 保存到本地的临时文件
+ */
+ File download(String mediaId) throws WxErrorException;
+
+ /**
+ *
+ * 获取高清语音素材.
+ * 可以使用本接口获取从JSSDK的uploadVoice接口上传的临时语音素材,格式为speex,16K采样率。该音频比上文的临时素材获取接口(格式为amr,8K采样率)更加清晰,适合用作语音识别等对音质要求较高的业务。
+ * 请求方式:GET(HTTPS)
+ * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/media/get/jssdk?access_token=ACCESS_TOKEN&media_id=MEDIA_ID
+ * 仅企业微信2.4及以上版本支持。
+ * 文档地址:https://work.weixin.qq.com/api/doc#90000/90135/90255
+ *
+ *
+ * @param mediaId 媒体id
+ * @return 保存到本地的临时文件
+ */
+ File getJssdkFile(String mediaId) throws WxErrorException;
+
+ /**
+ *
+ * 上传图片.
+ * 上传图片得到图片URL,该URL永久有效
+ * 返回的图片URL,仅能用于图文消息(mpnews)正文中的图片展示;若用于非企业微信域名下的页面,图片将被屏蔽。
+ * 每个企业每天最多可上传100张图片
+ * 接口url格式:https://qyapi.weixin.qq.com/cgi-bin/media/uploadimg?access_token=ACCESS_TOKEN
+ *
+ *
+ * @param file 上传的文件对象
+ * @return 返回图片url
+ */
+ String uploadImg(File file) throws WxErrorException;
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMenuService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMenuService.java
new file mode 100644
index 0000000000..309b981211
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMenuService.java
@@ -0,0 +1,92 @@
+package me.chanjar.weixin.cp.api;
+
+import me.chanjar.weixin.common.bean.menu.WxMenu;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ *
+ * 菜单管理相关接口
+ * Created by BinaryWang on 2017/6/24.
+ *
+ *
+ * @author Binary Wang
+ */
+public interface WxCpMenuService {
+
+ /**
+ *
+ * 自定义菜单创建接口
+ * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口
+ *
+ * 注意: 这个方法使用WxCpConfigStorage里的agentId
+ *
+ *
+ * @param menu 菜单对象
+ * @see #create(Integer, WxMenu)
+ */
+ void create(WxMenu menu) throws WxErrorException;
+
+ /**
+ *
+ * 自定义菜单创建接口
+ * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口
+ *
+ * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
+ *
+ *
+ * @param agentId 企业号应用的id
+ * @param menu 菜单对象
+ * @see #create(me.chanjar.weixin.common.bean.menu.WxMenu)
+ */
+ void create(Integer agentId, WxMenu menu) throws WxErrorException;
+
+ /**
+ *
+ * 自定义菜单删除接口
+ * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单删除接口
+ *
+ * 注意: 这个方法使用WxCpConfigStorage里的agentId
+ *
+ *
+ * @see #delete(Integer)
+ */
+ void delete() throws WxErrorException;
+
+ /**
+ *
+ * 自定义菜单删除接口
+ * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单删除接口
+ *
+ * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
+ *
+ *
+ * @param agentId 企业号应用的id
+ * @see #delete()
+ */
+ void delete(Integer agentId) throws WxErrorException;
+
+ /**
+ *
+ * 自定义菜单查询接口
+ * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单查询接口
+ *
+ * 注意: 这个方法使用WxCpConfigStorage里的agentId
+ *
+ *
+ * @see #get(Integer)
+ */
+ WxMenu get() throws WxErrorException;
+
+ /**
+ *
+ * 自定义菜单查询接口
+ * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单查询接口
+ *
+ * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
+ *
+ *
+ * @param agentId 企业号应用的id
+ * @see #get()
+ */
+ WxMenu get(Integer agentId) throws WxErrorException;
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOAuth2Service.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOAuth2Service.java
new file mode 100644
index 0000000000..7c42ea63fc
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOAuth2Service.java
@@ -0,0 +1,104 @@
+package me.chanjar.weixin.cp.api;
+
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.bean.WxCpOauth2UserInfo;
+import me.chanjar.weixin.cp.bean.WxCpUserDetail;
+
+/**
+ *
+ * OAuth2相关管理接口.
+ * Created by BinaryWang on 2017/6/24.
+ *
+ *
+ * @author Binary Wang
+ */
+public interface WxCpOAuth2Service {
+
+ /**
+ *
+ * 构造oauth2授权的url连接.
+ *
+ *
+ * @param state 状态码
+ * @return url
+ */
+ String buildAuthorizationUrl(String state);
+
+ /**
+ *
+ * 构造oauth2授权的url连接.
+ * 详情请见: http://qydev.weixin.qq.com/wiki/index.php?title=企业获取code
+ *
+ *
+ * @param redirectUri 跳转链接地址
+ * @param state 状态码
+ * @return url
+ */
+ String buildAuthorizationUrl(String redirectUri, String state);
+
+ /**
+ *
+ * 构造oauth2授权的url连接
+ * 详情请见: http://qydev.weixin.qq.com/wiki/index.php?title=企业获取code
+ *
+ *
+ * @param redirectUri 跳转链接地址
+ * @param state 状态码
+ * @param scope 取值参考me.chanjar.weixin.common.api.WxConsts.OAuth2Scope类
+ * @return url
+ */
+ String buildAuthorizationUrl(String redirectUri, String state, String scope);
+
+ /**
+ *
+ * 用oauth2获取用户信息
+ * http://qydev.weixin.qq.com/wiki/index.php?title=根据code获取成员信息
+ * 因为企业号oauth2.0必须在应用设置里设置通过ICP备案的可信域名,所以无法测试,因此这个方法很可能是坏的。
+ *
+ * 注意: 这个方法使用WxCpConfigStorage里的agentId
+ *
+ *
+ * @param code 微信oauth授权返回的代码
+ * @return WxCpOauth2UserInfo
+ * @throws WxErrorException 异常
+ * @see #getUserInfo(Integer, String)
+ */
+ WxCpOauth2UserInfo getUserInfo(String code) throws WxErrorException;
+
+ /**
+ *
+ * 根据code获取成员信息
+ * http://qydev.weixin.qq.com/wiki/index.php?title=根据code获取成员信息
+ * https://work.weixin.qq.com/api/doc#10028/根据code获取成员信息
+ * https://work.weixin.qq.com/api/doc#90000/90135/91023 获取访问用户身份
+ * 因为企业号oauth2.0必须在应用设置里设置通过ICP备案的可信域名,所以无法测试,因此这个方法很可能是坏的。
+ *
+ * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
+ *
+ *
+ * @param agentId 企业号应用的id
+ * @param code 通过成员授权获取到的code,最大为512字节。每次成员授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。
+ * @return WxCpOauth2UserInfo
+ * @throws WxErrorException 异常
+ * @see #getUserInfo(String)
+ */
+ WxCpOauth2UserInfo getUserInfo(Integer agentId, String code) throws WxErrorException;
+
+ /**
+ *
+ * 使用user_ticket获取成员详情.
+ *
+ * 文档地址:https://work.weixin.qq.com/api/doc#10028/%E4%BD%BF%E7%94%A8user_ticket%E8%8E%B7%E5%8F%96%E6%88%90%E5%91%98%E8%AF%A6%E6%83%85
+ * 请求方式:POST(HTTPS)
+ * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/getuserdetail?access_token=ACCESS_TOKEN
+ *
+ * 权限说明:
+ * 需要有对应应用的使用权限,且成员必须在授权应用的可见范围内。
+ *
+ *
+ * @param userTicket 成员票据
+ * @return WxCpUserDetail
+ * @throws WxErrorException 异常
+ */
+ WxCpUserDetail getUserDetail(String userTicket) throws WxErrorException;
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaService.java
new file mode 100644
index 0000000000..1e9c6dd5e9
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaService.java
@@ -0,0 +1,135 @@
+package me.chanjar.weixin.cp.api;
+
+import lombok.NonNull;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.bean.oa.*;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 企业微信OA相关接口.
+ *
+ * @author Element
+ * @date 2019-04-06 10:52
+ */
+public interface WxCpOaService {
+
+ /**
+ * 提交审批申请
+ * 调试工具
+ * 企业可通过审批应用或自建应用Secret调用本接口,代应用可见范围内员工在企业微信“审批应用”内提交指定类型的审批申请。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent?access_token=ACCESS_TOKEN
+ * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/91853
+ *
+ *
+ * @param request 请求
+ * @return 表单提交成功后,返回的表单编号
+ * @throws WxErrorException .
+ */
+ String apply(WxCpOaApplyEventRequest request) throws WxErrorException;
+
+ /**
+ *
+ * 获取打卡数据
+ * API doc : https://work.weixin.qq.com/api/doc#90000/90135/90262
+ *
+ *
+ * @param openCheckinDataType 打卡类型。1:上下班打卡;2:外出打卡;3:全部打卡
+ * @param startTime 获取打卡记录的开始时间
+ * @param endTime 获取打卡记录的结束时间
+ * @param userIdList 需要获取打卡记录的用户列表
+ * @return 打卡数据列表
+ * @throws WxErrorException 异常
+ */
+ List getCheckinData(Integer openCheckinDataType, Date startTime, Date endTime,
+ List userIdList) throws WxErrorException;
+
+ /**
+ *
+ * 获取打卡规则
+ * API doc : https://work.weixin.qq.com/api/doc#90000/90135/90263
+ *
+ *
+ * @param datetime 需要获取规则的当天日期
+ * @param userIdList 需要获取打卡规则的用户列表
+ * @return 打卡规则列表
+ * @throws WxErrorException .
+ */
+ List getCheckinOption(Date datetime, List userIdList) throws WxErrorException;
+
+ /**
+ *
+ *
+ * 批量获取审批单号
+ *
+ * 审批应用及有权限的自建应用,可通过Secret调用本接口,以获取企业一段时间内企业微信“审批应用”单据的审批编号,支持按模板类型、申请人、部门、申请单审批状态等条件筛选。
+ * 自建应用调用此接口,需在“管理后台-应用管理-审批-API-审批数据权限”中,授权应用允许提交审批单据。
+ *
+ * 一次拉取调用最多拉取100个审批记录,可以通过多次拉取的方式来满足需求,但调用频率不可超过600次/分。
+ *
+ * API doc : https://work.weixin.qq.com/api/doc/90000/90135/91816
+ *
+ *
+ * @param startTime 开始时间
+ * @param endTime 结束时间
+ * @param cursor 分页查询游标,默认为0,后续使用返回的next_cursor进行分页拉取
+ * @param size 一次请求拉取审批单数量,默认值为100,上限值为100
+ * @param filters 筛选条件,可对批量拉取的审批申请设置约束条件,支持设置多个条件,nullable
+ * @return WxCpApprovalInfo
+ * @throws WxErrorException .
+ */
+ WxCpApprovalInfo getApprovalInfo(@NonNull Date startTime, @NonNull Date endTime, Integer cursor, Integer size,
+ List filters) throws WxErrorException;
+
+ /**
+ * short method
+ *
+ * @param startTime 开始时间
+ * @param endTime 结束时间
+ * @return WxCpApprovalInfo
+ * @throws WxErrorException .
+ * @see me.chanjar.weixin.cp.api.WxCpOaService#getApprovalInfo
+ */
+ WxCpApprovalInfo getApprovalInfo(@NonNull Date startTime, @NonNull Date endTime) throws WxErrorException;
+
+ /**
+ *
+ * 获取审批申请详情
+ *
+ * 企业可通过审批应用或自建应用Secret调用本接口,根据审批单号查询企业微信“审批应用”的审批申请详情。
+ *
+ * API Doc : https://work.weixin.qq.com/api/doc/90000/90135/91983
+ *
+ *
+ * @param spNo 审批单编号。
+ * @return WxCpApprovaldetail
+ * @throws WxErrorException .
+ */
+ WxCpApprovalDetailResult getApprovalDetail(@NonNull String spNo) throws WxErrorException;
+
+ /**
+ * 获取公费电话拨打记录
+ *
+ * @param startTime 查询的起始时间戳
+ * @param endTime 查询的结束时间戳
+ * @param offset 分页查询的偏移量
+ * @param limit 分页查询的每页大小,默认为100条,如该参数大于100则按100处理
+ * @return .
+ * @throws WxErrorException .
+ */
+ List getDialRecord(Date startTime, Date endTime, Integer offset,
+ Integer limit) throws WxErrorException;
+
+ /**
+ * 获取审批模板详情
+ *
+ * @param templateId 模板ID
+ * @return .
+ * @throws WxErrorException .
+ */
+ WxCpTemplateResult getTemplateDetail(@NonNull String templateId) throws WxErrorException;
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java
index bd0b322f72..036265815b 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java
@@ -1,27 +1,24 @@
package me.chanjar.weixin.cp.api;
import me.chanjar.weixin.common.bean.WxJsapiSignature;
-import me.chanjar.weixin.common.bean.menu.WxMenu;
-import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
-import me.chanjar.weixin.common.exception.WxErrorException;
+import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.session.WxSession;
import me.chanjar.weixin.common.session.WxSessionManager;
+import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor;
import me.chanjar.weixin.common.util.http.RequestExecutor;
-import me.chanjar.weixin.cp.bean.WxCpDepart;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+import me.chanjar.weixin.cp.bean.WxCpMaJsCode2SessionResult;
import me.chanjar.weixin.cp.bean.WxCpMessage;
-import me.chanjar.weixin.cp.bean.WxCpTag;
-import me.chanjar.weixin.cp.bean.WxCpUser;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.List;
+import me.chanjar.weixin.cp.bean.WxCpMessageSendResult;
+import me.chanjar.weixin.cp.bean.WxCpProviderToken;
+import me.chanjar.weixin.cp.config.WxCpConfigStorage;
/**
- * 微信API的Service
+ * 微信API的Service.
+ *
+ * @author chanjaster
*/
public interface WxCpService {
-
/**
*
* 验证推送过来的消息的正确性
@@ -35,16 +32,6 @@ public interface WxCpService {
*/
boolean checkSignature(String msgSignature, String timestamp, String nonce, String data);
- /**
- *
- * 用在二次验证的时候
- * 企业在员工验证成功后,调用本方法告诉企业号平台该员工关注成功。
- *
- *
- * @param userId 用户id
- */
- void userAuthenticated(String userId) throws WxErrorException;
-
/**
* 获取access_token, 不强制刷新access_token
*
@@ -85,469 +72,291 @@ public interface WxCpService {
String getJsapiTicket(boolean forceRefresh) throws WxErrorException;
/**
- *
- * 创建调用jsapi时所需要的签名
- *
- * 详情请见:http://qydev.weixin.qq.com/wiki/index.php?title=微信JS接口#.E9.99.84.E5.BD.951-JS-SDK.E4.BD.BF.E7.94.A8.E6.9D.83.E9.99.90.E7.AD.BE.E5.90.8D.E7.AE.97.E6.B3.95
- *
+ * 获得jsapi_ticket,不强制刷新jsapi_ticket
+ * 应用的jsapi_ticket用于计算agentConfig(参见“通过agentConfig注入应用的权限”)的签名,签名计算方法与上述介绍的config的签名算法完全相同,但需要注意以下区别:
+ *
+ * 签名的jsapi_ticket必须使用以下接口获取。且必须用wx.agentConfig中的agentid对应的应用secret去获取access_token。
+ * 签名用的noncestr和timestamp必须与wx.agentConfig中的nonceStr和timestamp相同。
*
- * @param url url
+ * @see #getJsapiTicket(boolean)
*/
- WxJsapiSignature createJsapiSignature(String url) throws WxErrorException;
+ String getAgentJsapiTicket() throws WxErrorException;
/**
*
- * 上传多媒体文件
- * 上传的多媒体文件有格式和大小限制,如下:
- * 图片(image): 1M,支持JPG格式
- * 语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式
- * 视频(video):10MB,支持MP4格式
- * 缩略图(thumb):64KB,支持JPG格式
- * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件
- *
+ * 获取应用的jsapi_ticket
+ * 应用的jsapi_ticket用于计算agentConfig(参见“通过agentConfig注入应用的权限”)的签名,签名计算方法与上述介绍的config的签名算法完全相同,但需要注意以下区别:
*
- * @param mediaType 媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts}
- * @param fileType 文件类型,请看{@link me.chanjar.weixin.common.api.WxConsts}
- * @param inputStream 输入流
- */
- WxMediaUploadResult mediaUpload(String mediaType, String fileType, InputStream inputStream)
- throws WxErrorException, IOException;
-
- /**
- * @param mediaType 媒体类型
- * @param file 文件对象
- * @see #mediaUpload(String, String, InputStream)
- */
- WxMediaUploadResult mediaUpload(String mediaType, File file) throws WxErrorException;
-
- /**
- *
- * 下载多媒体文件
- * 根据微信文档,视频文件下载不了,会返回null
- * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件
- *
+ * 签名的jsapi_ticket必须使用以下接口获取。且必须用wx.agentConfig中的agentid对应的应用secret去获取access_token。
+ * 签名用的noncestr和timestamp必须与wx.agentConfig中的nonceStr和timestamp相同。
*
- * @param mediaId 媒体id
- * @return 保存到本地的临时文件
- */
- File mediaDownload(String mediaId) throws WxErrorException;
-
- /**
- *
- * 发送消息
- * 详情请见: http://qydev.weixin.qq.com/wiki/index.php?title=%E5%8F%91%E9%80%81%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
- *
- *
- * @param message 要发送的消息对象
- */
- void messageSend(WxCpMessage message) throws WxErrorException;
-
- /**
- *
- * 自定义菜单创建接口
- * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口
+ * 获得时会检查jsapiToken是否过期,如果过期了,那么就刷新一下,否则就什么都不干
*
- * 注意: 这个方法使用WxCpConfigStorage里的agentId
+ * 详情请见:https://work.weixin.qq.com/api/doc#10029/%E8%8E%B7%E5%8F%96%E5%BA%94%E7%94%A8%E7%9A%84jsapi_ticket
*
*
- * @param menu 菜单对象
- * @see #menuCreate(Integer, WxMenu)
+ * @param forceRefresh 强制刷新
*/
- void menuCreate(WxMenu menu) throws WxErrorException;
+ String getAgentJsapiTicket(boolean forceRefresh) throws WxErrorException;
/**
*
- * 自定义菜单创建接口
- * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口
+ * 创建调用jsapi时所需要的签名
*
- * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
+ * 详情请见:http://qydev.weixin.qq.com/wiki/index.php?title=微信JS接口#.E9.99.84.E5.BD.951-JS-SDK.E4.BD.BF.E7.94.A8.E6.9D.83.E9.99.90.E7.AD.BE.E5.90.8D.E7.AE.97.E6.B3.95
*
*
- * @param agentId 企业号应用的id
- * @param menu 菜单对象
- * @see #menuCreate(me.chanjar.weixin.common.bean.menu.WxMenu)
+ * @param url url
*/
- void menuCreate(Integer agentId, WxMenu menu) throws WxErrorException;
+ WxJsapiSignature createJsapiSignature(String url) throws WxErrorException;
/**
*
- * 自定义菜单删除接口
- * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单删除接口
- *
- * 注意: 这个方法使用WxCpConfigStorage里的agentId
+ * 发送消息
+ * 详情请见: http://qydev.weixin.qq.com/wiki/index.php?title=%E5%8F%91%E9%80%81%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
*
*
- * @see #menuDelete(Integer)
+ * @param message 要发送的消息对象
*/
- void menuDelete() throws WxErrorException;
+ WxCpMessageSendResult messageSend(WxCpMessage message) throws WxErrorException;
/**
- *
- * 自定义菜单删除接口
- * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单删除接口
- *
- * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
- *
+ * 小程序登录凭证校验
*
- * @param agentId 企业号应用的id
- * @see #menuDelete()
+ * @param jsCode 登录时获取的 code
*/
- void menuDelete(Integer agentId) throws WxErrorException;
+ WxCpMaJsCode2SessionResult jsCode2Session(String jsCode) throws WxErrorException;
/**
*
- * 自定义菜单查询接口
- * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单查询接口
- *
- * 注意: 这个方法使用WxCpConfigStorage里的agentId
+ * 获取微信服务器的ip段
+ * http://qydev.weixin.qq.com/wiki/index.php?title=回调模式#.E8.8E.B7.E5.8F.96.E5.BE.AE.E4.BF.A1.E6.9C.8D.E5.8A.A1.E5.99.A8.E7.9A.84ip.E6.AE.B5
*
*
- * @see #menuGet(Integer)
+ * @return { "ip_list": ["101.226.103.*", "101.226.62.*"] }
*/
- WxMenu menuGet() throws WxErrorException;
+ String[] getCallbackIp() throws WxErrorException;
/**
*
- * 自定义菜单查询接口
- * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单查询接口
- *
- * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
+ * 获取服务商凭证
+ * 文档地址:https://work.weixin.qq.com/api/doc#90001/90143/91200
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/service/get_provider_token
*
*
- * @param agentId 企业号应用的id
- * @see #menuGet()
+ * @param corpId 服务商的corpid
+ * @param providerSecret 服务商的secret,在服务商管理后台可见
+ * @return {
+ * "errcode":0 ,
+ * "errmsg":"ok" ,
+ * "provider_access_token":"enLSZ5xxxxxxJRL",
+ * "expires_in":7200
+ * }
+ * @throws WxErrorException .
*/
- WxMenu menuGet(Integer agentId) throws WxErrorException;
+ WxCpProviderToken getProviderToken(String corpId, String providerSecret) throws WxErrorException;
/**
- *
- * 部门管理接口 - 创建部门
- * 最多支持创建500个部门
- * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=部门管理接口
- *
+ * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的GET请求
*
- * @param depart 部门
- * @return 部门id
+ * @param url 接口地址
+ * @param queryParam 请求参数
*/
- Integer departCreate(WxCpDepart depart) throws WxErrorException;
+ String get(String url, String queryParam) throws WxErrorException;
/**
- *
- * 部门管理接口 - 查询所有部门
- * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=部门管理接口
- *
+ * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求
+ *
+ * @param url 接口地址
+ * @param postData 请求body字符串
*/
- List departGet() throws WxErrorException;
+ String post(String url, String postData) throws WxErrorException;
/**
- *
- * 部门管理接口 - 修改部门名
- * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=部门管理接口
- * 如果id为0(未部门),1(黑名单),2(星标组),或者不存在的id,微信会返回系统繁忙的错误
- *
+ * 当不需要自动带accessToken的时候,可以用这个发起post请求
*
- * @param group 要更新的group,group的id,name必须设置
+ * @param url 接口地址
+ * @param postData 请求body字符串
*/
- void departUpdate(WxCpDepart group) throws WxErrorException;
+ String postWithoutToken(String url, String postData) throws WxErrorException;
/**
*
- * 部门管理接口 - 删除部门
+ * Service没有实现某个API的时候,可以用这个,
+ * 比{@link #get}和{@link #post}方法更灵活,可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型。
+ * 可以参考,{@link MediaUploadRequestExecutor}的实现方法
*
*
- * @param departId 部门id
+ * @param executor 执行器
+ * @param uri 请求地址
+ * @param data 参数
+ * @param 请求值类型
+ * @param 返回值类型
*/
- void departDelete(Integer departId) throws WxErrorException;
+ T execute(RequestExecutor executor, String uri, E data) throws WxErrorException;
/**
*
- * 获取部门成员(详情)
- *
- * http://qydev.weixin.qq.com/wiki/index.php?title=管理成员#.E8.8E.B7.E5.8F.96.E9.83.A8.E9.97.A8.E6.88.90.E5.91.98.28.E8.AF.A6.E6.83.85.29
+ * 设置当微信系统响应系统繁忙时,要等待多少 retrySleepMillis(ms) * 2^(重试次数 - 1) 再发起重试
+ * 默认:1000ms
*
*
- * @param departId 必填。部门id
- * @param fetchChild 非必填。1/0:是否递归获取子部门下面的成员
- * @param status 非必填。0获取全部员工,1获取已关注成员列表,2获取禁用成员列表,4获取未关注成员列表。status可叠加
+ * @param retrySleepMillis 重试休息时间
*/
- List userList(Integer departId, Boolean fetchChild, Integer status) throws WxErrorException;
+ void setRetrySleepMillis(int retrySleepMillis);
/**
*
- * 获取部门成员
- *
- * http://qydev.weixin.qq.com/wiki/index.php?title=管理成员#.E8.8E.B7.E5.8F.96.E9.83.A8.E9.97.A8.E6.88.90.E5.91.98
+ * 设置当微信系统响应系统繁忙时,最大重试次数
+ * 默认:5次
*
*
- * @param departId 必填。部门id
- * @param fetchChild 非必填。1/0:是否递归获取子部门下面的成员
- * @param status 非必填。0获取全部员工,1获取已关注成员列表,2获取禁用成员列表,4获取未关注成员列表。status可叠加
+ * @param maxRetryTimes 最大重试次数
*/
- List departGetUsers(Integer departId, Boolean fetchChild, Integer status) throws WxErrorException;
+ void setMaxRetryTimes(int maxRetryTimes);
/**
- * 新建用户
+ * 获取某个sessionId对应的session,如果sessionId没有对应的session,则新建一个并返回。
*
- * @param user 用户对象
+ * @param id id可以为任意字符串,建议使用FromUserName作为id
*/
- void userCreate(WxCpUser user) throws WxErrorException;
+ WxSession getSession(String id);
/**
- * 更新用户
+ * 获取某个sessionId对应的session,如果sessionId没有对应的session,若create为true则新建一个,否则返回null。
*
- * @param user 用户对象
+ * @param id id可以为任意字符串,建议使用FromUserName作为id
+ * @param create 是否新建
*/
- void userUpdate(WxCpUser user) throws WxErrorException;
+ WxSession getSession(String id, boolean create);
/**
- * 删除用户
+ * 获取WxSessionManager 对象
*
- * @param userid 用户id
+ * @return WxSessionManager
*/
- void userDelete(String userid) throws WxErrorException;
+ WxSessionManager getSessionManager();
/**
*
- * 批量删除成员
- * http://qydev.weixin.qq.com/wiki/index.php?title=管理成员#.E6.89.B9.E9.87.8F.E5.88.A0.E9.99.A4.E6.88.90.E5.91.98
+ * 设置WxSessionManager,只有当需要使用个性化的WxSessionManager的时候才需要调用此方法,
+ * WxCpService默认使用的是{@link me.chanjar.weixin.common.session.StandardSessionManager}
*
*
- * @param userids 员工UserID列表。对应管理端的帐号
- */
- void userDelete(String[] userids) throws WxErrorException;
-
- /**
- * 获取用户
- *
- * @param userid 用户id
+ * @param sessionManager 会话管理器
*/
- WxCpUser userGet(String userid) throws WxErrorException;
+ void setSessionManager(WxSessionManager sessionManager);
/**
- * 创建标签
+ * 上传部门列表覆盖企业号上的部门信息
*
- * @param tagName 标签名
+ * @param mediaId 媒体id
*/
- String tagCreate(String tagName) throws WxErrorException;
+ String replaceParty(String mediaId) throws WxErrorException;
/**
- * 更新标签
+ * 上传用户列表覆盖企业号上的用户信息
*
- * @param tagId 标签id
- * @param tagName 标签名
+ * @param mediaId 媒体id
*/
- void tagUpdate(String tagId, String tagName) throws WxErrorException;
+ String replaceUser(String mediaId) throws WxErrorException;
/**
- * 删除标签
- *
- * @param tagId 标签id
+ * 获取异步任务结果
*/
- void tagDelete(String tagId) throws WxErrorException;
+ String getTaskResult(String joinId) throws WxErrorException;
/**
- * 获得标签列表
+ * 初始化http请求对象
*/
- List tagGet() throws WxErrorException;
+ void initHttp();
/**
- * 获取标签成员
+ * 获取WxMpConfigStorage 对象
*
- * @param tagId 标签ID
+ * @return WxMpConfigStorage
*/
- List tagGetUsers(String tagId) throws WxErrorException;
+ WxCpConfigStorage getWxCpConfigStorage();
/**
- * 增加标签成员
+ * 注入 {@link WxCpConfigStorage} 的实现
*
- * @param tagId 标签id
- * @param userIds 用户ID 列表
+ * @param wxConfigProvider 配置对象
*/
- void tagAddUsers(String tagId, List userIds, List partyIds) throws WxErrorException;
+ void setWxCpConfigStorage(WxCpConfigStorage wxConfigProvider);
/**
- *
- * 构造oauth2授权的url连接
- *
- *
- * @param state 状态码
- * @return url
+ * 获取部门相关接口的服务类对象
*/
- String oauth2buildAuthorizationUrl(String state);
+ WxCpDepartmentService getDepartmentService();
/**
- *
- * 构造oauth2授权的url连接
- * 详情请见: http://qydev.weixin.qq.com/wiki/index.php?title=企业获取code
- *
- *
- * @param redirectUri 跳转链接地址
- * @param state 状态码
- * @return url
+ * 获取媒体相关接口的服务类对象
*/
- String oauth2buildAuthorizationUrl(String redirectUri, String state);
+ WxCpMediaService getMediaService();
/**
- *
- * 用oauth2获取用户信息
- * http://qydev.weixin.qq.com/wiki/index.php?title=根据code获取成员信息
- * 因为企业号oauth2.0必须在应用设置里设置通过ICP备案的可信域名,所以无法测试,因此这个方法很可能是坏的。
- *
- * 注意: 这个方法使用WxCpConfigStorage里的agentId
- *
- *
- * @param code 微信oauth授权返回的代码
- * @return [userid, deviceid]
- * @see #oauth2getUserInfo(Integer, String)
+ * 获取菜单相关接口的服务类对象
*/
- String[] oauth2getUserInfo(String code) throws WxErrorException;
+ WxCpMenuService getMenuService();
/**
- *
- * 用oauth2获取用户信息
- * http://qydev.weixin.qq.com/wiki/index.php?title=根据code获取成员信息
- * 因为企业号oauth2.0必须在应用设置里设置通过ICP备案的可信域名,所以无法测试,因此这个方法很可能是坏的。
- *
- * 注意: 这个方法不使用WxCpConfigStorage里的agentId,需要开发人员自己给出
- *
- *
- * @param agentId 企业号应用的id
- * @param code 微信oauth授权返回的代码
- * @return [userid, deviceid]
- * @see #oauth2getUserInfo(String)
+ * 获取Oauth2相关接口的服务类对象
*/
- String[] oauth2getUserInfo(Integer agentId, String code) throws WxErrorException;
-
+ WxCpOAuth2Service getOauth2Service();
/**
- * 移除标签成员
- *
- * @param tagId 标签id
- * @param userIds 用户id列表
+ * 获取标签相关接口的服务类对象
*/
- void tagRemoveUsers(String tagId, List userIds) throws WxErrorException;
+ WxCpTagService getTagService();
/**
- *
- * 邀请成员关注
- * http://qydev.weixin.qq.com/wiki/index.php?title=管理成员#.E9.82.80.E8.AF.B7.E6.88.90.E5.91.98.E5.85.B3.E6.B3.A8
- *
- *
- * @param userId 用户的userid
- * @param inviteTips 推送到微信上的提示语(只有认证号可以使用)。当使用微信推送时,该字段默认为“请关注XXX企业号”,邮件邀请时,该字段无效。
- * @return 1:微信邀请 2.邮件邀请
+ * 获取用户相关接口的服务类对象
*/
- int invite(String userId, String inviteTips) throws WxErrorException;
+ WxCpUserService getUserService();
- /**
- *
- * 获取微信服务器的ip段
- * http://qydev.weixin.qq.com/wiki/index.php?title=回调模式#.E8.8E.B7.E5.8F.96.E5.BE.AE.E4.BF.A1.E6.9C.8D.E5.8A.A1.E5.99.A8.E7.9A.84ip.E6.AE.B5
- *
- *
- * @return { "ip_list": ["101.226.103.*", "101.226.62.*"] }
- */
- String[] getCallbackIp() throws WxErrorException;
+ WxCpExternalContactService getExternalContactService();
/**
- * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的GET请求
+ * 获取群聊服务
*
- * @param url 接口地址
- * @param queryParam 请求参数
+ * @return 群聊服务
*/
- String get(String url, String queryParam) throws WxErrorException;
+ WxCpChatService getChatService();
/**
- * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求
+ * 获取任务卡片服务
*
- * @param url 接口地址
- * @param postData 请求body字符串
+ * @return 任务卡片服务
*/
- String post(String url, String postData) throws WxErrorException;
+ WxCpTaskCardService getTaskCardService();
- /**
- *
- * Service没有实现某个API的时候,可以用这个,
- * 比{@link #get}和{@link #post}方法更灵活,可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型。
- * 可以参考,{@link me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor}的实现方法
- *
- *
- * @param executor 执行器
- * @param uri 请求地址
- * @param data 参数
- * @param 请求值类型
- * @param 返回值类型
- */
- T execute(RequestExecutor executor, String uri, E data) throws WxErrorException;
+ WxCpAgentService getAgentService();
- /**
- * 注入 {@link WxCpConfigStorage} 的实现
- *
- * @param wxConfigProvider 配置对象
- */
- void setWxCpConfigStorage(WxCpConfigStorage wxConfigProvider);
+ WxCpOaService getOAService();
/**
- *
- * 设置当微信系统响应系统繁忙时,要等待多少 retrySleepMillis(ms) * 2^(重试次数 - 1) 再发起重试
- * 默认:1000ms
- *
+ * 获取群机器人消息推送服务
*
- * @param retrySleepMillis 重试休息时间
+ * @return 群机器人消息推送服务
*/
- void setRetrySleepMillis(int retrySleepMillis);
+ WxCpGroupRobotService getGroupRobotService();
/**
- *
- * 设置当微信系统响应系统繁忙时,最大重试次数
- * 默认:5次
- *
- *
- * @param maxRetryTimes 最大重试次数
+ * http请求对象
*/
- void setMaxRetryTimes(int maxRetryTimes);
+ RequestHttp, ?> getRequestHttp();
- /**
- * 获取某个sessionId对应的session,如果sessionId没有对应的session,则新建一个并返回。
- *
- * @param id id可以为任意字符串,建议使用FromUserName作为id
- */
- WxSession getSession(String id);
+ void setUserService(WxCpUserService userService);
- /**
- * 获取某个sessionId对应的session,如果sessionId没有对应的session,若create为true则新建一个,否则返回null。
- *
- * @param id id可以为任意字符串,建议使用FromUserName作为id
- * @param create 是否新建
- */
- WxSession getSession(String id, boolean create);
+ void setDepartmentService(WxCpDepartmentService departmentService);
- /**
- *
- * 设置WxSessionManager,只有当需要使用个性化的WxSessionManager的时候才需要调用此方法,
- * WxCpService默认使用的是{@link me.chanjar.weixin.common.session.StandardSessionManager}
- *
- *
- * @param sessionManager 会话管理器
- */
- void setSessionManager(WxSessionManager sessionManager);
+ void setMediaService(WxCpMediaService mediaService);
- /**
- * 上传部门列表覆盖企业号上的部门信息
- *
- * @param mediaId 媒体id
- */
- String replaceParty(String mediaId) throws WxErrorException;
+ void setMenuService(WxCpMenuService menuService);
- /**
- * 上传用户列表覆盖企业号上的用户信息
- *
- * @param mediaId 媒体id
- */
- String replaceUser(String mediaId) throws WxErrorException;
+ void setOauth2Service(WxCpOAuth2Service oauth2Service);
- /**
- * 获取异步任务结果
- */
- String getTaskResult(String joinId) throws WxErrorException;
+ void setTagService(WxCpTagService tagService);
}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpServiceImpl.java
deleted file mode 100644
index 0105e78d52..0000000000
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpServiceImpl.java
+++ /dev/null
@@ -1,697 +0,0 @@
-package me.chanjar.weixin.cp.api;
-
-import com.google.gson.*;
-import com.google.gson.reflect.TypeToken;
-import me.chanjar.weixin.common.bean.WxAccessToken;
-import me.chanjar.weixin.common.bean.WxJsapiSignature;
-import me.chanjar.weixin.common.bean.menu.WxMenu;
-import me.chanjar.weixin.common.bean.result.WxError;
-import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
-import me.chanjar.weixin.common.exception.WxErrorException;
-import me.chanjar.weixin.common.session.StandardSessionManager;
-import me.chanjar.weixin.common.session.WxSession;
-import me.chanjar.weixin.common.session.WxSessionManager;
-import me.chanjar.weixin.common.util.RandomUtils;
-import me.chanjar.weixin.common.util.crypto.SHA1;
-import me.chanjar.weixin.common.util.fs.FileUtils;
-import me.chanjar.weixin.common.util.http.*;
-import me.chanjar.weixin.common.util.json.GsonHelper;
-import me.chanjar.weixin.cp.bean.WxCpDepart;
-import me.chanjar.weixin.cp.bean.WxCpMessage;
-import me.chanjar.weixin.cp.bean.WxCpTag;
-import me.chanjar.weixin.cp.bean.WxCpUser;
-import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.http.HttpHost;
-import org.apache.http.client.config.RequestConfig;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.impl.client.BasicResponseHandler;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.List;
-import java.util.UUID;
-
-public class WxCpServiceImpl implements WxCpService {
-
- protected final Logger log = LoggerFactory.getLogger(WxCpServiceImpl.class);
-
- /**
- * 全局的是否正在刷新access token的锁
- */
- protected final Object globalAccessTokenRefreshLock = new Object();
-
- /**
- * 全局的是否正在刷新jsapi_ticket的锁
- */
- protected final Object globalJsapiTicketRefreshLock = new Object();
-
- protected WxCpConfigStorage configStorage;
-
- protected CloseableHttpClient httpClient;
-
- protected HttpHost httpProxy;
- protected WxSessionManager sessionManager = new StandardSessionManager();
- /**
- * 临时文件目录
- */
- protected File tmpDirFile;
- private int retrySleepMillis = 1000;
- private int maxRetryTimes = 5;
-
- @Override
- public boolean checkSignature(String msgSignature, String timestamp, String nonce, String data) {
- try {
- return SHA1.gen(this.configStorage.getToken(), timestamp, nonce, data)
- .equals(msgSignature);
- } catch (Exception e) {
- return false;
- }
- }
-
- @Override
- public void userAuthenticated(String userId) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/user/authsucc?userid=" + userId;
- get(url, null);
- }
-
- @Override
- public String getAccessToken() throws WxErrorException {
- return getAccessToken(false);
- }
-
- @Override
- public String getAccessToken(boolean forceRefresh) throws WxErrorException {
- if (forceRefresh) {
- this.configStorage.expireAccessToken();
- }
- if (this.configStorage.isAccessTokenExpired()) {
- synchronized (this.globalAccessTokenRefreshLock) {
- if (this.configStorage.isAccessTokenExpired()) {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?"
- + "&corpid=" + this.configStorage.getCorpId()
- + "&corpsecret=" + this.configStorage.getCorpSecret();
- try {
- HttpGet httpGet = new HttpGet(url);
- if (this.httpProxy != null) {
- RequestConfig config = RequestConfig.custom()
- .setProxy(this.httpProxy).build();
- httpGet.setConfig(config);
- }
- String resultContent = null;
- try (CloseableHttpClient httpclient = getHttpclient();
- CloseableHttpResponse response = httpclient.execute(httpGet)) {
- resultContent = new BasicResponseHandler().handleResponse(response);
- } finally {
- httpGet.releaseConnection();
- }
- WxError error = WxError.fromJson(resultContent);
- if (error.getErrorCode() != 0) {
- throw new WxErrorException(error);
- }
- WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
- this.configStorage.updateAccessToken(
- accessToken.getAccessToken(), accessToken.getExpiresIn());
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
- }
- }
- return this.configStorage.getAccessToken();
- }
-
- @Override
- public String getJsapiTicket() throws WxErrorException {
- return getJsapiTicket(false);
- }
-
- @Override
- public String getJsapiTicket(boolean forceRefresh) throws WxErrorException {
- if (forceRefresh) {
- this.configStorage.expireJsapiTicket();
- }
- if (this.configStorage.isJsapiTicketExpired()) {
- synchronized (this.globalJsapiTicketRefreshLock) {
- if (this.configStorage.isJsapiTicketExpired()) {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket";
- String responseContent = execute(new SimpleGetRequestExecutor(), url, null);
- JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
- JsonObject tmpJsonObject = tmpJsonElement.getAsJsonObject();
- String jsapiTicket = tmpJsonObject.get("ticket").getAsString();
- int expiresInSeconds = tmpJsonObject.get("expires_in").getAsInt();
- this.configStorage.updateJsapiTicket(jsapiTicket,
- expiresInSeconds);
- }
- }
- }
- return this.configStorage.getJsapiTicket();
- }
-
- @Override
- public WxJsapiSignature createJsapiSignature(String url) throws WxErrorException {
- long timestamp = System.currentTimeMillis() / 1000;
- String noncestr = RandomUtils.getRandomStr();
- String jsapiTicket = getJsapiTicket(false);
- String signature = SHA1.genWithAmple(
- "jsapi_ticket=" + jsapiTicket,
- "noncestr=" + noncestr,
- "timestamp=" + timestamp,
- "url=" + url
- );
- WxJsapiSignature jsapiSignature = new WxJsapiSignature();
- jsapiSignature.setTimestamp(timestamp);
- jsapiSignature.setNonceStr(noncestr);
- jsapiSignature.setUrl(url);
- jsapiSignature.setSignature(signature);
-
- // Fixed bug
- jsapiSignature.setAppId(this.configStorage.getCorpId());
-
- return jsapiSignature;
- }
-
- @Override
- public void messageSend(WxCpMessage message) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/message/send";
- post(url, message.toJson());
- }
-
- @Override
- public void menuCreate(WxMenu menu) throws WxErrorException {
- menuCreate(this.configStorage.getAgentId(), menu);
- }
-
- @Override
- public void menuCreate(Integer agentId, WxMenu menu) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/menu/create?agentid="
- + this.configStorage.getAgentId();
- post(url, menu.toJson());
- }
-
- @Override
- public void menuDelete() throws WxErrorException {
- menuDelete(this.configStorage.getAgentId());
- }
-
- @Override
- public void menuDelete(Integer agentId) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/menu/delete?agentid=" + agentId;
- get(url, null);
- }
-
- @Override
- public WxMenu menuGet() throws WxErrorException {
- return menuGet(this.configStorage.getAgentId());
- }
-
- @Override
- public WxMenu menuGet(Integer agentId) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/menu/get?agentid=" + agentId;
- try {
- String resultContent = get(url, null);
- return WxMenu.fromJson(resultContent);
- } catch (WxErrorException e) {
- // 46003 不存在的菜单数据
- if (e.getError().getErrorCode() == 46003) {
- return null;
- }
- throw e;
- }
- }
-
- @Override
- public WxMediaUploadResult mediaUpload(String mediaType, String fileType, InputStream inputStream)
- throws WxErrorException, IOException {
- return mediaUpload(mediaType, FileUtils.createTmpFile(inputStream, UUID.randomUUID().toString(), fileType));
- }
-
- @Override
- public WxMediaUploadResult mediaUpload(String mediaType, File file) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?type=" + mediaType;
- return execute(new MediaUploadRequestExecutor(), url, file);
- }
-
- @Override
- public File mediaDownload(String mediaId) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/media/get";
- return execute(
- new MediaDownloadRequestExecutor(
- this.configStorage.getTmpDirFile()),
- url, "media_id=" + mediaId);
- }
-
-
- @Override
- public Integer departCreate(WxCpDepart depart) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/department/create";
- String responseContent = execute(
- new SimplePostRequestExecutor(),
- url,
- depart.toJson());
- JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
- return GsonHelper.getAsInteger(tmpJsonElement.getAsJsonObject().get("id"));
- }
-
- @Override
- public void departUpdate(WxCpDepart group) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/department/update";
- post(url, group.toJson());
- }
-
- @Override
- public void departDelete(Integer departId) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/department/delete?id=" + departId;
- get(url, null);
- }
-
- @Override
- public List departGet() throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/department/list";
- String responseContent = get(url, null);
- /*
- * 操蛋的微信API,创建时返回的是 { group : { id : ..., name : ...} }
- * 查询时返回的是 { groups : [ { id : ..., name : ..., count : ... }, ... ] }
- */
- JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
- return WxCpGsonBuilder.INSTANCE.create()
- .fromJson(
- tmpJsonElement.getAsJsonObject().get("department"),
- new TypeToken>() {
- }.getType()
- );
- }
-
- @Override
- public void userCreate(WxCpUser user) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/user/create";
- post(url, user.toJson());
- }
-
- @Override
- public void userUpdate(WxCpUser user) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/user/update";
- post(url, user.toJson());
- }
-
- @Override
- public void userDelete(String userid) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/user/delete?userid=" + userid;
- get(url, null);
- }
-
- @Override
- public void userDelete(String[] userids) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/user/batchdelete";
- JsonObject jsonObject = new JsonObject();
- JsonArray jsonArray = new JsonArray();
- for (String userid : userids) {
- jsonArray.add(new JsonPrimitive(userid));
- }
- jsonObject.add("useridlist", jsonArray);
- post(url, jsonObject.toString());
- }
-
- @Override
- public WxCpUser userGet(String userid) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/user/get?userid=" + userid;
- String responseContent = get(url, null);
- return WxCpUser.fromJson(responseContent);
- }
-
- @Override
- public List userList(Integer departId, Boolean fetchChild, Integer status) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/user/list?department_id=" + departId;
- String params = "";
- if (fetchChild != null) {
- params += "&fetch_child=" + (fetchChild ? "1" : "0");
- }
- if (status != null) {
- params += "&status=" + status;
- } else {
- params += "&status=0";
- }
-
- String responseContent = get(url, params);
- JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
- return WxCpGsonBuilder.INSTANCE.create()
- .fromJson(
- tmpJsonElement.getAsJsonObject().get("userlist"),
- new TypeToken>() {
- }.getType()
- );
- }
-
- @Override
- public List departGetUsers(Integer departId, Boolean fetchChild, Integer status) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?department_id=" + departId;
- String params = "";
- if (fetchChild != null) {
- params += "&fetch_child=" + (fetchChild ? "1" : "0");
- }
- if (status != null) {
- params += "&status=" + status;
- } else {
- params += "&status=0";
- }
-
- String responseContent = get(url, params);
- JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
- return WxCpGsonBuilder.INSTANCE.create()
- .fromJson(
- tmpJsonElement.getAsJsonObject().get("userlist"),
- new TypeToken>() {
- }.getType()
- );
- }
-
- @Override
- public String tagCreate(String tagName) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/create";
- JsonObject o = new JsonObject();
- o.addProperty("tagname", tagName);
- String responseContent = post(url, o.toString());
- JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
- return tmpJsonElement.getAsJsonObject().get("tagid").getAsString();
- }
-
- @Override
- public void tagUpdate(String tagId, String tagName) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/update";
- JsonObject o = new JsonObject();
- o.addProperty("tagid", tagId);
- o.addProperty("tagname", tagName);
- post(url, o.toString());
- }
-
- @Override
- public void tagDelete(String tagId) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/delete?tagid=" + tagId;
- get(url, null);
- }
-
- @Override
- public List tagGet() throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/list";
- String responseContent = get(url, null);
- JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
- return WxCpGsonBuilder.INSTANCE.create()
- .fromJson(
- tmpJsonElement.getAsJsonObject().get("taglist"),
- new TypeToken>() {
- }.getType()
- );
- }
-
- @Override
- public List tagGetUsers(String tagId) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/get?tagid=" + tagId;
- String responseContent = get(url, null);
- JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
- return WxCpGsonBuilder.INSTANCE.create()
- .fromJson(
- tmpJsonElement.getAsJsonObject().get("userlist"),
- new TypeToken>() {
- }.getType()
- );
- }
-
- @Override
- public void tagAddUsers(String tagId, List userIds, List partyIds) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/addtagusers";
- JsonObject jsonObject = new JsonObject();
- jsonObject.addProperty("tagid", tagId);
- if (userIds != null) {
- JsonArray jsonArray = new JsonArray();
- for (String userId : userIds) {
- jsonArray.add(new JsonPrimitive(userId));
- }
- jsonObject.add("userlist", jsonArray);
- }
- if (partyIds != null) {
- JsonArray jsonArray = new JsonArray();
- for (String userId : partyIds) {
- jsonArray.add(new JsonPrimitive(userId));
- }
- jsonObject.add("partylist", jsonArray);
- }
- post(url, jsonObject.toString());
- }
-
- @Override
- public void tagRemoveUsers(String tagId, List userIds) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/tag/deltagusers";
- JsonObject jsonObject = new JsonObject();
- jsonObject.addProperty("tagid", tagId);
- JsonArray jsonArray = new JsonArray();
- for (String userId : userIds) {
- jsonArray.add(new JsonPrimitive(userId));
- }
- jsonObject.add("userlist", jsonArray);
- post(url, jsonObject.toString());
- }
-
- @Override
- public String oauth2buildAuthorizationUrl(String state) {
- return this.oauth2buildAuthorizationUrl(
- this.configStorage.getOauth2redirectUri(),
- state
- );
- }
-
- @Override
- public String oauth2buildAuthorizationUrl(String redirectUri, String state) {
- String url = "https://open.weixin.qq.com/connect/oauth2/authorize?";
- url += "appid=" + this.configStorage.getCorpId();
- url += "&redirect_uri=" + URIUtil.encodeURIComponent(redirectUri);
- url += "&response_type=code";
- url += "&scope=snsapi_base";
- if (state != null) {
- url += "&state=" + state;
- }
- url += "#wechat_redirect";
- return url;
- }
-
- @Override
- public String[] oauth2getUserInfo(String code) throws WxErrorException {
- return oauth2getUserInfo(this.configStorage.getAgentId(), code);
- }
-
- @Override
- public String[] oauth2getUserInfo(Integer agentId, String code) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?"
- + "code=" + code
- + "&agentid=" + agentId;
- String responseText = get(url, null);
- JsonElement je = new JsonParser().parse(responseText);
- JsonObject jo = je.getAsJsonObject();
- return new String[]{GsonHelper.getString(jo, "UserId"), GsonHelper.getString(jo, "DeviceId"), GsonHelper.getString(jo, "OpenId")};
- }
-
- @Override
- public int invite(String userId, String inviteTips) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/invite/send";
- JsonObject jsonObject = new JsonObject();
- jsonObject.addProperty("userid", userId);
- if (StringUtils.isNotEmpty(inviteTips)) {
- jsonObject.addProperty("invite_tips", inviteTips);
- }
- String responseContent = post(url, jsonObject.toString());
- JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
- return tmpJsonElement.getAsJsonObject().get("type").getAsInt();
- }
-
- @Override
- public String[] getCallbackIp() throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/getcallbackip";
- String responseContent = get(url, null);
- JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
- JsonArray jsonArray = tmpJsonElement.getAsJsonObject().get("ip_list").getAsJsonArray();
- String[] ips = new String[jsonArray.size()];
- for (int i = 0; i < jsonArray.size(); i++) {
- ips[i] = jsonArray.get(i).getAsString();
- }
- return ips;
- }
-
- @Override
- public String get(String url, String queryParam) throws WxErrorException {
- return execute(new SimpleGetRequestExecutor(), url, queryParam);
- }
-
- @Override
- public String post(String url, String postData) throws WxErrorException {
- return execute(new SimplePostRequestExecutor(), url, postData);
- }
-
- /**
- * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求
- */
- @Override
- public T execute(RequestExecutor executor, String uri, E data) throws WxErrorException {
- int retryTimes = 0;
- do {
- try {
- T result = this.executeInternal(executor, uri, data);
- this.log.debug("\n[URL]: {}\n[PARAMS]: {}\n[RESPONSE]: {}",uri, data, result);
- return result;
- } catch (WxErrorException e) {
- if (retryTimes + 1 > this.maxRetryTimes) {
- this.log.warn("重试达到最大次数【{}】", this.maxRetryTimes);
- //最后一次重试失败后,直接抛出异常,不再等待
- throw new RuntimeException("微信服务端异常,超出重试次数");
- }
-
- WxError error = e.getError();
- /*
- * -1 系统繁忙, 1000ms后重试
- */
- if (error.getErrorCode() == -1) {
- int sleepMillis = this.retrySleepMillis * (1 << retryTimes);
- try {
- this.log.debug("微信系统繁忙,{} ms 后重试(第{}次)", sleepMillis, retryTimes + 1);
- Thread.sleep(sleepMillis);
- } catch (InterruptedException e1) {
- throw new RuntimeException(e1);
- }
- } else {
- throw e;
- }
- }
- } while (retryTimes++ < this.maxRetryTimes);
-
- this.log.warn("重试达到最大次数【{}】", this.maxRetryTimes);
- throw new RuntimeException("微信服务端异常,超出重试次数");
- }
-
- protected synchronized T executeInternal(RequestExecutor executor, String uri, E data) throws WxErrorException {
- if (uri.contains("access_token=")) {
- throw new IllegalArgumentException("uri参数中不允许有access_token: " + uri);
- }
- String accessToken = getAccessToken(false);
-
- String uriWithAccessToken = uri;
- uriWithAccessToken += uri.indexOf('?') == -1 ? "?access_token=" + accessToken : "&access_token=" + accessToken;
-
- try {
- return executor.execute(getHttpclient(), this.httpProxy, uriWithAccessToken, data);
- } catch (WxErrorException e) {
- WxError error = e.getError();
- /*
- * 发生以下情况时尝试刷新access_token
- * 40001 获取access_token时AppSecret错误,或者access_token无效
- * 42001 access_token超时
- */
- if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001) {
- // 强制设置wxCpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token
- this.configStorage.expireAccessToken();
- return execute(executor, uri, data);
- }
-
- if (error.getErrorCode() != 0) {
- this.log.error("\n[URL]: {}\n[PARAMS]: {}\n[RESPONSE]: {}", uri, data, error);
- throw new WxErrorException(error);
- }
- return null;
- } catch (IOException e) {
- this.log.error("\n[URL]: {}\n[PARAMS]: {}\n[EXCEPTION]: {}", uri, data, e.getMessage());
- throw new RuntimeException(e);
- }
- }
-
- protected CloseableHttpClient getHttpclient() {
- return this.httpClient;
- }
-
- @Override
- public void setWxCpConfigStorage(WxCpConfigStorage wxConfigProvider) {
- this.configStorage = wxConfigProvider;
- ApacheHttpClientBuilder apacheHttpClientBuilder = this.configStorage
- .getApacheHttpClientBuilder();
- if (null == apacheHttpClientBuilder) {
- apacheHttpClientBuilder = DefaultApacheHttpClientBuilder.get();
- }
-
- apacheHttpClientBuilder.httpProxyHost(this.configStorage.getHttpProxyHost())
- .httpProxyPort(this.configStorage.getHttpProxyPort())
- .httpProxyUsername(this.configStorage.getHttpProxyUsername())
- .httpProxyPassword(this.configStorage.getHttpProxyPassword());
-
- if (this.configStorage.getHttpProxyHost() != null && this.configStorage.getHttpProxyPort() > 0) {
- this.httpProxy = new HttpHost(this.configStorage.getHttpProxyHost(), this.configStorage.getHttpProxyPort());
- }
-
- this.httpClient = apacheHttpClientBuilder.build();
- }
-
- @Override
- public void setRetrySleepMillis(int retrySleepMillis) {
- this.retrySleepMillis = retrySleepMillis;
- }
-
-
- @Override
- public void setMaxRetryTimes(int maxRetryTimes) {
- this.maxRetryTimes = maxRetryTimes;
- }
-
- @Override
- public WxSession getSession(String id) {
- if (this.sessionManager == null) {
- return null;
- }
- return this.sessionManager.getSession(id);
- }
-
- @Override
- public WxSession getSession(String id, boolean create) {
- if (this.sessionManager == null) {
- return null;
- }
- return this.sessionManager.getSession(id, create);
- }
-
-
- @Override
- public void setSessionManager(WxSessionManager sessionManager) {
- this.sessionManager = sessionManager;
- }
-
- @Override
- public String replaceParty(String mediaId) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/batch/replaceparty";
- JsonObject jsonObject = new JsonObject();
- jsonObject.addProperty("media_id", mediaId);
- return post(url, jsonObject.toString());
- }
-
- @Override
- public String replaceUser(String mediaId) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/batch/replaceuser";
- JsonObject jsonObject = new JsonObject();
- jsonObject.addProperty("media_id", mediaId);
- return post(url, jsonObject.toString());
- }
-
- @Override
- public String getTaskResult(String joinId) throws WxErrorException {
- String url = "https://qyapi.weixin.qq.com/cgi-bin/batch/getresult?jobid=" + joinId;
- return get(url, null);
- }
-
- public File getTmpDirFile() {
- return this.tmpDirFile;
- }
-
- public void setTmpDirFile(File tmpDirFile) {
- this.tmpDirFile = tmpDirFile;
- }
-
-
-}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTagService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTagService.java
new file mode 100644
index 0000000000..045264f7d0
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTagService.java
@@ -0,0 +1,100 @@
+package me.chanjar.weixin.cp.api;
+
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.bean.WxCpTag;
+import me.chanjar.weixin.cp.bean.WxCpTagAddOrRemoveUsersResult;
+import me.chanjar.weixin.cp.bean.WxCpTagGetResult;
+import me.chanjar.weixin.cp.bean.WxCpUser;
+
+import java.util.List;
+
+/**
+ *
+ * 标签管理接口.
+ * Created by BinaryWang on 2017/6/24.
+ *
+ *
+ * @author Binary Wang
+ */
+public interface WxCpTagService {
+ /**
+ * 创建标签.
+ *
+ * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/tag/create?access_token=ACCESS_TOKEN
+ * 文档地址:https://work.weixin.qq.com/api/doc#90000/90135/90210
+ *
+ *
+ * @param name 标签名称,长度限制为32个字以内(汉字或英文字母),标签名不可与其他标签重名。
+ * @param id 标签id,非负整型,指定此参数时新增的标签会生成对应的标签id,不指定时则以目前最大的id自增。
+ * @return 标签id
+ * @throws WxErrorException .
+ */
+ String create(String name, Integer id) throws WxErrorException;
+
+ /**
+ * 更新标签.
+ *
+ * @param tagId 标签id
+ * @param tagName 标签名
+ * @throws WxErrorException .
+ */
+ void update(String tagId, String tagName) throws WxErrorException;
+
+ /**
+ * 删除标签.
+ *
+ * @param tagId 标签id
+ * @throws WxErrorException .
+ */
+ void delete(String tagId) throws WxErrorException;
+
+ /**
+ * 获得标签列表.
+ *
+ * @return 标签列表
+ * @throws WxErrorException .
+ */
+ List listAll() throws WxErrorException;
+
+ /**
+ * 获取标签成员.
+ *
+ * @param tagId 标签ID
+ * @return 成员列表
+ * @throws WxErrorException .
+ */
+ List listUsersByTagId(String tagId) throws WxErrorException;
+
+ /**
+ * 获取标签成员.
+ * 对应: http://qydev.weixin.qq.com/wiki/index.php?title=管理标签 中的get接口
+ *
+ * @param tagId 标签id
+ * @return .
+ * @throws WxErrorException .
+ */
+ WxCpTagGetResult get(String tagId) throws WxErrorException;
+
+ /**
+ * 增加标签成员.
+ *
+ * @param tagId 标签id
+ * @param userIds 用户ID 列表
+ * @param partyIds 企业部门ID列表
+ * @return .
+ * @throws WxErrorException .
+ */
+ WxCpTagAddOrRemoveUsersResult addUsers2Tag(String tagId, List userIds, List partyIds) throws WxErrorException;
+
+ /**
+ * 移除标签成员.
+ *
+ * @param tagId 标签id
+ * @param userIds 用户id列表
+ * @param partyIds 企业部门ID列表
+ * @return .
+ * @throws WxErrorException .
+ */
+ WxCpTagAddOrRemoveUsersResult removeUsersFromTag(String tagId, List userIds, List partyIds) throws WxErrorException;
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTaskCardService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTaskCardService.java
new file mode 100644
index 0000000000..5bf50d36dc
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTaskCardService.java
@@ -0,0 +1,31 @@
+package me.chanjar.weixin.cp.api;
+
+import me.chanjar.weixin.common.error.WxErrorException;
+
+import java.util.List;
+
+/**
+ *
+ * 任务卡片管理接口.
+ * Created by Jeff on 2019-05-16.
+ *
+ *
+ * @author Jeff
+ * @date 2019-05-16
+ */
+public interface WxCpTaskCardService {
+
+ /**
+ *
+ * 更新任务卡片消息状态
+ * 详情请见: https://work.weixin.qq.com/api/doc#90000/90135/91579
+ *
+ * 注意: 这个方法使用WxCpConfigStorage里的agentId
+ *
+ *
+ * @param userIds 企业的成员ID列表
+ * @param taskId 任务卡片ID
+ * @param clickedKey 已点击按钮的Key
+ */
+ void update(List userIds, String taskId, String clickedKey) throws WxErrorException;
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTpService.java
new file mode 100644
index 0000000000..ad2f403af6
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTpService.java
@@ -0,0 +1,208 @@
+package me.chanjar.weixin.cp.api;
+
+import me.chanjar.weixin.common.bean.WxAccessToken;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor;
+import me.chanjar.weixin.common.util.http.RequestExecutor;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+import me.chanjar.weixin.cp.bean.WxCpMaJsCode2SessionResult;
+import me.chanjar.weixin.cp.bean.WxCpTpAuthInfo;
+import me.chanjar.weixin.cp.bean.WxCpTpCorp;
+import me.chanjar.weixin.cp.bean.WxCpTpPermanentCodeInfo;
+import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
+
+/**
+ * 微信第三方应用API的Service.
+ *
+ * @author zhenjun cai
+ */
+public interface WxCpTpService {
+ /**
+ *
+ * 验证推送过来的消息的正确性
+ * 详情请见: https://work.weixin.qq.com/api/doc#90000/90139/90968/消息体签名校验
+ *
+ *
+ * @param msgSignature 消息签名
+ * @param timestamp 时间戳
+ * @param nonce 随机数
+ * @param data 微信传输过来的数据,有可能是echoStr,有可能是xml消息
+ */
+ boolean checkSignature(String msgSignature, String timestamp, String nonce, String data);
+
+ /**
+ * 获取suite_access_token, 不强制刷新suite_access_token
+ *
+ * @see #getSuiteAccessToken(boolean)
+ */
+ String getSuiteAccessToken() throws WxErrorException;
+
+ /**
+ *
+ * 获取suite_access_token,本方法线程安全
+ * 且在多线程同时刷新时只刷新一次,避免超出2000次/日的调用次数上限
+ * 另:本service的所有方法都会在suite_access_token过期是调用此方法
+ * 程序员在非必要情况下尽量不要主动调用此方法
+ * 详情请见: https://work.weixin.qq.com/api/doc#90001/90143/90600
+ *
+ *
+ * @param forceRefresh 强制刷新
+ */
+ String getSuiteAccessToken(boolean forceRefresh) throws WxErrorException;
+
+ /**
+ * 获得suite_ticket,不强制刷新suite_ticket
+ *
+ * @see #getSuiteTicket(boolean)
+ */
+ String getSuiteTicket() throws WxErrorException;
+
+ /**
+ *
+ * 获得suite_ticket
+ * 由于suite_ticket是微信服务器定时推送(每10分钟),不能主动获取,如果碰到过期只能抛异常
+ *
+ * 详情请见:https://work.weixin.qq.com/api/doc#90001/90143/90628
+ *
+ *
+ * @param forceRefresh 强制刷新
+ */
+ String getSuiteTicket(boolean forceRefresh) throws WxErrorException;
+
+ /**
+ * 小程序登录凭证校验
+ *
+ * @param jsCode 登录时获取的 code
+ */
+ WxCpMaJsCode2SessionResult jsCode2Session(String jsCode) throws WxErrorException;
+
+ /**
+ * 获取企业凭证
+ *
+ * @param authCorpid 授权方corpid
+ * @param permanentCode 永久授权码,通过get_permanent_code获取
+ */
+ WxAccessToken getCorpToken(String authCorpid, String permanentCode) throws WxErrorException;
+
+ /**
+ * 获取企业永久授权码 .
+ *
+ * @param authCode .
+ * @return .
+ */
+ @Deprecated
+ WxCpTpCorp getPermanentCode(String authCode) throws WxErrorException;
+
+ /**
+ * 获取企业永久授权码信息
+ *
+ * 原来的方法实现不全
+ *
+ *
+ * @param authCode
+ * @return
+ *
+ * @author yuan
+ * @since 2020-03-18
+ *
+ * @throws WxErrorException
+ */
+ WxCpTpPermanentCodeInfo getPermanentCodeInfo(String authCode) throws WxErrorException;
+
+ /**
+ *
+ * 获取预授权链接
+ *
+ * @param redirectUri 授权完成后的回调网址
+ * @param state a-zA-Z0-9的参数值(不超过128个字节),用于第三方自行校验session,防止跨域攻击
+ * @return
+ * @throws WxErrorException
+ */
+ String getPreAuthUrl(String redirectUri,String state) throws WxErrorException;
+
+ /**
+ * 获取企业的授权信息
+ *
+ * @param authCorpId 授权企业的corpId
+ * @param permanentCode 授权企业的永久授权码
+ * @return
+ * @throws WxErrorException
+ */
+ WxCpTpAuthInfo getAuthInfo(String authCorpId,String permanentCode) throws WxErrorException;
+
+ /**
+ * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的GET请求.
+ *
+ * @param url 接口地址
+ * @param queryParam 请求参数
+ */
+ String get(String url, String queryParam) throws WxErrorException;
+
+ /**
+ * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求.
+ *
+ * @param url 接口地址
+ * @param postData 请求body字符串
+ */
+ String post(String url, String postData) throws WxErrorException;
+
+ /**
+ *
+ * Service没有实现某个API的时候,可以用这个,
+ * 比{@link #get}和{@link #post}方法更灵活,可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型。
+ * 可以参考,{@link MediaUploadRequestExecutor}的实现方法
+ *
+ *
+ * @param executor 执行器
+ * @param uri 请求地址
+ * @param data 参数
+ * @param 请求值类型
+ * @param 返回值类型
+ */
+ T execute(RequestExecutor executor, String uri, E data) throws WxErrorException;
+
+ /**
+ *
+ * 设置当微信系统响应系统繁忙时,要等待多少 retrySleepMillis(ms) * 2^(重试次数 - 1) 再发起重试.
+ * 默认:1000ms
+ *
+ *
+ * @param retrySleepMillis 重试休息时间
+ */
+ void setRetrySleepMillis(int retrySleepMillis);
+
+ /**
+ *
+ * 设置当微信系统响应系统繁忙时,最大重试次数.
+ * 默认:5次
+ *
+ *
+ * @param maxRetryTimes 最大重试次数
+ */
+ void setMaxRetryTimes(int maxRetryTimes);
+
+ /**
+ * 初始化http请求对象
+ */
+ void initHttp();
+
+ /**
+ * 获取WxMpConfigStorage 对象.
+ *
+ * @return WxMpConfigStorage
+ */
+ WxCpTpConfigStorage getWxCpTpConfigStorage();
+
+ /**
+ * 注入 {@link WxCpTpConfigStorage} 的实现.
+ *
+ * @param wxConfigProvider 配置对象
+ */
+ void setWxCpTpConfigStorage(WxCpTpConfigStorage wxConfigProvider);
+
+ /**
+ * http请求对象.
+ */
+ RequestHttp, ?> getRequestHttp();
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpUserService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpUserService.java
new file mode 100644
index 0000000000..8cf5670f9b
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpUserService.java
@@ -0,0 +1,173 @@
+package me.chanjar.weixin.cp.api;
+
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.bean.WxCpInviteResult;
+import me.chanjar.weixin.cp.bean.WxCpUser;
+import me.chanjar.weixin.cp.bean.external.WxCpUserExternalContactInfo;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ *
+ * 用户管理接口
+ * Created by BinaryWang on 2017/6/24.
+ *
+ *
+ * @author Binary Wang
+ */
+public interface WxCpUserService {
+
+ /**
+ *
+ * 用在二次验证的时候.
+ * 企业在员工验证成功后,调用本方法告诉企业号平台该员工关注成功。
+ *
+ *
+ * @param userId 用户id
+ */
+ void authenticate(String userId) throws WxErrorException;
+
+ /**
+ *
+ * 获取部门成员(详情).
+ *
+ * http://qydev.weixin.qq.com/wiki/index.php?title=管理成员#.E8.8E.B7.E5.8F.96.E9.83.A8.E9.97.A8.E6.88.90.E5.91.98.28.E8.AF.A6.E6.83.85.29
+ *
+ *
+ * @param departId 必填。部门id
+ * @param fetchChild 非必填。1/0:是否递归获取子部门下面的成员
+ * @param status 非必填。0获取全部员工,1获取已关注成员列表,2获取禁用成员列表,4获取未关注成员列表。status可叠加
+ */
+ List listByDepartment(Long departId, Boolean fetchChild, Integer status) throws WxErrorException;
+
+ /**
+ *
+ * 获取部门成员.
+ *
+ * http://qydev.weixin.qq.com/wiki/index.php?title=管理成员#.E8.8E.B7.E5.8F.96.E9.83.A8.E9.97.A8.E6.88.90.E5.91.98
+ *
+ *
+ * @param departId 必填。部门id
+ * @param fetchChild 非必填。1/0:是否递归获取子部门下面的成员
+ * @param status 非必填。0获取全部员工,1获取已关注成员列表,2获取禁用成员列表,4获取未关注成员列表。status可叠加
+ */
+ List listSimpleByDepartment(Long departId, Boolean fetchChild, Integer status) throws WxErrorException;
+
+ /**
+ * 新建用户.
+ *
+ * @param user 用户对象
+ */
+ void create(WxCpUser user) throws WxErrorException;
+
+ /**
+ * 更新用户.
+ *
+ * @param user 用户对象
+ */
+ void update(WxCpUser user) throws WxErrorException;
+
+ /**
+ *
+ * 删除用户/批量删除成员.
+ * http://qydev.weixin.qq.com/wiki/index.php?title=管理成员#.E6.89.B9.E9.87.8F.E5.88.A0.E9.99.A4.E6.88.90.E5.91.98
+ *
+ *
+ * @param userIds 员工UserID列表。对应管理端的帐号
+ */
+ void delete(String... userIds) throws WxErrorException;
+
+ /**
+ * 获取用户.
+ *
+ * @param userid 用户id
+ */
+ WxCpUser getById(String userid) throws WxErrorException;
+
+ /**
+ *
+ * 邀请成员.
+ * 企业可通过接口批量邀请成员使用企业微信,邀请后将通过短信或邮件下发通知。
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/batch/invite?access_token=ACCESS_TOKEN
+ * 文档地址:https://work.weixin.qq.com/api/doc#12543
+ *
+ *
+ * @param userIds 成员ID列表, 最多支持1000个。
+ * @param partyIds 部门ID列表,最多支持100个。
+ * @param tagIds 标签ID列表,最多支持100个。
+ */
+ WxCpInviteResult invite(List userIds, List partyIds, List tagIds) throws WxErrorException;
+
+ /**
+ *
+ * userid转openid.
+ * 该接口使用场景为微信支付、微信红包和企业转账。
+ *
+ * 在使用微信支付的功能时,需要自行将企业微信的userid转成openid。
+ * 在使用微信红包功能时,需要将应用id和userid转成appid和openid才能使用。
+ * 注:需要成员使用微信登录企业微信或者关注微信插件才能转成openid
+ *
+ * 文档地址:https://work.weixin.qq.com/api/doc#11279
+ *
+ *
+ * @param userId 企业内的成员id
+ * @param agentId 非必填,整型,仅用于发红包。其它场景该参数不要填,如微信支付、企业转账、电子发票
+ * @return map对象,可能包含以下值:
+ * - openid 企业微信成员userid对应的openid,若有传参agentid,则是针对该agentid的openid。否则是针对企业微信corpid的openid
+ * - appid 应用的appid,若请求包中不包含agentid则不返回appid。该appid在使用微信红包时会用到
+ */
+ Map userId2Openid(String userId, Integer agentId) throws WxErrorException;
+
+ /**
+ *
+ * openid转userid.
+ *
+ * 该接口主要应用于使用微信支付、微信红包和企业转账之后的结果查询。
+ * 开发者需要知道某个结果事件的openid对应企业微信内成员的信息时,可以通过调用该接口进行转换查询。
+ * 权限说明:
+ * 管理组需对openid对应的企业微信成员有查看权限。
+ *
+ * 文档地址:https://work.weixin.qq.com/api/doc#11279
+ *
+ *
+ * @param openid 在使用微信支付、微信红包和企业转账之后,返回结果的openid
+ * @return userid 该openid在企业微信对应的成员userid
+ */
+ String openid2UserId(String openid) throws WxErrorException;
+
+ /**
+ *
+ *
+ * 通过手机号获取其所对应的userid。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/getuserid?access_token=ACCESS_TOKEN
+ *
+ * 文档地址:https://work.weixin.qq.com/api/doc#90001/90143/91693
+ *
+ *
+ * @param mobile 手机号码。长度为5~32个字节
+ * @return userid mobile对应的成员userid
+ * @throws WxErrorException .
+ */
+ String getUserId(String mobile) throws WxErrorException;
+
+ /**
+ * 获取外部联系人详情.
+ *
+ * 企业可通过此接口,根据外部联系人的userid,拉取外部联系人详情。权限说明:
+ * 企业需要使用外部联系人管理secret所获取的accesstoken来调用
+ * 第三方应用需拥有“企业客户”权限。
+ * 第三方应用调用时,返回的跟进人follow_user仅包含应用可见范围之内的成员。
+ *
+ *
+ * @param userId 外部联系人的userid
+ * @return 联系人详情
+ * @throws WxErrorException .
+ */
+ WxCpUserExternalContactInfo getExternalContact(String userId) throws WxErrorException;
+
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java
new file mode 100644
index 0000000000..52d88e4564
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java
@@ -0,0 +1,492 @@
+package me.chanjar.weixin.cp.api.impl;
+
+import com.google.common.base.Joiner;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.api.WxConsts;
+import me.chanjar.weixin.common.bean.WxJsapiSignature;
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.session.StandardSessionManager;
+import me.chanjar.weixin.common.session.WxSession;
+import me.chanjar.weixin.common.session.WxSessionManager;
+import me.chanjar.weixin.common.util.DataUtils;
+import me.chanjar.weixin.common.util.RandomUtils;
+import me.chanjar.weixin.common.util.crypto.SHA1;
+import me.chanjar.weixin.common.util.http.RequestExecutor;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor;
+import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor;
+import me.chanjar.weixin.common.util.json.GsonParser;
+import me.chanjar.weixin.cp.api.*;
+import me.chanjar.weixin.cp.bean.WxCpMaJsCode2SessionResult;
+import me.chanjar.weixin.cp.bean.WxCpMessage;
+import me.chanjar.weixin.cp.bean.WxCpMessageSendResult;
+import me.chanjar.weixin.cp.bean.WxCpProviderToken;
+import me.chanjar.weixin.cp.config.WxCpConfigStorage;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.*;
+
+/**
+ * .
+ *
+ * @author chanjarster
+ */
+@Slf4j
+public abstract class BaseWxCpServiceImpl implements WxCpService, RequestHttp {
+ private WxCpUserService userService = new WxCpUserServiceImpl(this);
+ private WxCpChatService chatService = new WxCpChatServiceImpl(this);
+ private WxCpDepartmentService departmentService = new WxCpDepartmentServiceImpl(this);
+ private WxCpMediaService mediaService = new WxCpMediaServiceImpl(this);
+ private WxCpMenuService menuService = new WxCpMenuServiceImpl(this);
+ private WxCpOAuth2Service oauth2Service = new WxCpOAuth2ServiceImpl(this);
+ private WxCpTagService tagService = new WxCpTagServiceImpl(this);
+ private WxCpAgentService agentService = new WxCpAgentServiceImpl(this);
+ private WxCpOaService oaService = new WxCpOaServiceImpl(this);
+ private WxCpTaskCardService taskCardService = new WxCpTaskCardServiceImpl(this);
+ private WxCpExternalContactService externalContactService = new WxCpExternalContactServiceImpl(this);
+ private WxCpGroupRobotService groupRobotService = new WxCpGroupRobotServiceImpl(this);
+
+ /**
+ * 全局的是否正在刷新access token的锁.
+ */
+ protected final Object globalAccessTokenRefreshLock = new Object();
+
+ /**
+ * 全局的是否正在刷新jsapi_ticket的锁.
+ */
+ protected final Object globalJsapiTicketRefreshLock = new Object();
+
+ /**
+ * 全局的是否正在刷新agent的jsapi_ticket的锁.
+ */
+ protected final Object globalAgentJsapiTicketRefreshLock = new Object();
+
+ protected WxCpConfigStorage configStorage;
+
+ private WxSessionManager sessionManager = new StandardSessionManager();
+
+ /**
+ * 临时文件目录.
+ */
+ private File tmpDirFile;
+ private int retrySleepMillis = 1000;
+ private int maxRetryTimes = 5;
+
+ @Override
+ public boolean checkSignature(String msgSignature, String timestamp, String nonce, String data) {
+ try {
+ return SHA1.gen(this.configStorage.getToken(), timestamp, nonce, data)
+ .equals(msgSignature);
+ } catch (Exception e) {
+ log.error("Checking signature failed, and the reason is :" + e.getMessage());
+ return false;
+ }
+ }
+
+ @Override
+ public String getAccessToken() throws WxErrorException {
+ return getAccessToken(false);
+ }
+
+ @Override
+ public String getAgentJsapiTicket() throws WxErrorException {
+ return this.getAgentJsapiTicket(false);
+ }
+
+ @Override
+ public String getAgentJsapiTicket(boolean forceRefresh) throws WxErrorException {
+ if (forceRefresh) {
+ this.configStorage.expireAgentJsapiTicket();
+ }
+
+ if (this.configStorage.isAgentJsapiTicketExpired()) {
+ synchronized (this.globalAgentJsapiTicketRefreshLock) {
+ if (this.configStorage.isAgentJsapiTicketExpired()) {
+ String responseContent = this.get(this.configStorage.getApiUrl(GET_AGENT_CONFIG_TICKET), null);
+ JsonObject jsonObject = GsonParser.parse(responseContent);
+ this.configStorage.updateAgentJsapiTicket(jsonObject.get("ticket").getAsString(),
+ jsonObject.get("expires_in").getAsInt());
+ }
+ }
+ }
+
+ return this.configStorage.getAgentJsapiTicket();
+ }
+
+ @Override
+ public String getJsapiTicket() throws WxErrorException {
+ return getJsapiTicket(false);
+ }
+
+ @Override
+ public String getJsapiTicket(boolean forceRefresh) throws WxErrorException {
+ if (forceRefresh) {
+ this.configStorage.expireJsapiTicket();
+ }
+
+ if (this.configStorage.isJsapiTicketExpired()) {
+ synchronized (this.globalJsapiTicketRefreshLock) {
+ if (this.configStorage.isJsapiTicketExpired()) {
+ String responseContent = this.get(this.configStorage.getApiUrl(GET_JSAPI_TICKET), null);
+ JsonObject tmpJsonObject = GsonParser.parse(responseContent);
+ this.configStorage.updateJsapiTicket(tmpJsonObject.get("ticket").getAsString(),
+ tmpJsonObject.get("expires_in").getAsInt());
+ }
+ }
+ }
+
+ return this.configStorage.getJsapiTicket();
+ }
+
+ @Override
+ public WxJsapiSignature createJsapiSignature(String url) throws WxErrorException {
+ long timestamp = System.currentTimeMillis() / 1000;
+ String noncestr = RandomUtils.getRandomStr();
+ String jsapiTicket = getJsapiTicket(false);
+ String signature = SHA1.genWithAmple(
+ "jsapi_ticket=" + jsapiTicket,
+ "noncestr=" + noncestr,
+ "timestamp=" + timestamp,
+ "url=" + url
+ );
+ WxJsapiSignature jsapiSignature = new WxJsapiSignature();
+ jsapiSignature.setTimestamp(timestamp);
+ jsapiSignature.setNonceStr(noncestr);
+ jsapiSignature.setUrl(url);
+ jsapiSignature.setSignature(signature);
+
+ // Fixed bug
+ jsapiSignature.setAppId(this.configStorage.getCorpId());
+
+ return jsapiSignature;
+ }
+
+ @Override
+ public WxCpMessageSendResult messageSend(WxCpMessage message) throws WxErrorException {
+ Integer agentId = message.getAgentId();
+ if (null == agentId) {
+ message.setAgentId(this.getWxCpConfigStorage().getAgentId());
+ }
+
+ return WxCpMessageSendResult.fromJson(this.post(this.configStorage.getApiUrl(MESSAGE_SEND), message.toJson()));
+ }
+
+ @Override
+ public WxCpMaJsCode2SessionResult jsCode2Session(String jsCode) throws WxErrorException {
+ Map params = new HashMap<>(2);
+ params.put("js_code", jsCode);
+ params.put("grant_type", "authorization_code");
+
+ final String url = this.configStorage.getApiUrl(JSCODE_TO_SESSION);
+ return WxCpMaJsCode2SessionResult.fromJson(this.get(url, Joiner.on("&").withKeyValueSeparator("=").join(params)));
+ }
+
+ @Override
+ public String[] getCallbackIp() throws WxErrorException {
+ String responseContent = get(this.configStorage.getApiUrl(GET_CALLBACK_IP), null);
+ JsonObject tmpJsonObject = GsonParser.parse(responseContent);
+ JsonArray jsonArray = tmpJsonObject.get("ip_list").getAsJsonArray();
+ String[] ips = new String[jsonArray.size()];
+ for (int i = 0; i < jsonArray.size(); i++) {
+ ips[i] = jsonArray.get(i).getAsString();
+ }
+ return ips;
+ }
+
+ @Override
+ public WxCpProviderToken getProviderToken(String corpId, String providerSecret) throws WxErrorException {
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("corpid", corpId);
+ jsonObject.addProperty("provider_secret", providerSecret);
+ return WxCpProviderToken.fromJson(this.post(this.configStorage.getApiUrl(Tp.GET_PROVIDER_TOKEN), jsonObject.toString()));
+ }
+
+ @Override
+ public String get(String url, String queryParam) throws WxErrorException {
+ return execute(SimpleGetRequestExecutor.create(this), url, queryParam);
+ }
+
+ @Override
+ public String post(String url, String postData) throws WxErrorException {
+ return execute(SimplePostRequestExecutor.create(this), url, postData);
+ }
+
+ @Override
+ public String postWithoutToken(String url, String postData) throws WxErrorException {
+ return this.executeNormal(SimplePostRequestExecutor.create(this), url, postData);
+ }
+
+ /**
+ * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求.
+ */
+ @Override
+ public T execute(RequestExecutor executor, String uri, E data) throws WxErrorException {
+ int retryTimes = 0;
+ do {
+ try {
+ return this.executeInternal(executor, uri, data);
+ } catch (WxErrorException e) {
+ if (retryTimes + 1 > this.maxRetryTimes) {
+ log.warn("重试达到最大次数【{}】", this.maxRetryTimes);
+ //最后一次重试失败后,直接抛出异常,不再等待
+ throw new RuntimeException("微信服务端异常,超出重试次数");
+ }
+
+ WxError error = e.getError();
+ /*
+ * -1 系统繁忙, 1000ms后重试
+ */
+ if (error.getErrorCode() == -1) {
+ int sleepMillis = this.retrySleepMillis * (1 << retryTimes);
+ try {
+ log.debug("微信系统繁忙,{} ms 后重试(第{}次)", sleepMillis, retryTimes + 1);
+ Thread.sleep(sleepMillis);
+ } catch (InterruptedException e1) {
+ Thread.currentThread().interrupt();
+ }
+ } else {
+ throw e;
+ }
+ }
+ } while (retryTimes++ < this.maxRetryTimes);
+
+ log.warn("重试达到最大次数【{}】", this.maxRetryTimes);
+ throw new RuntimeException("微信服务端异常,超出重试次数");
+ }
+
+ protected T executeInternal(RequestExecutor executor, String uri, E data) throws WxErrorException {
+ E dataForLog = DataUtils.handleDataWithSecret(data);
+
+ if (uri.contains("access_token=")) {
+ throw new IllegalArgumentException("uri参数中不允许有access_token: " + uri);
+ }
+ String accessToken = getAccessToken(false);
+
+ String uriWithAccessToken = uri + (uri.contains("?") ? "&" : "?") + "access_token=" + accessToken;
+
+ try {
+ T result = executor.execute(uriWithAccessToken, data, WxType.CP);
+ log.debug("\n【请求地址】: {}\n【请求参数】:{}\n【响应数据】:{}", uriWithAccessToken, dataForLog, result);
+ return result;
+ } catch (WxErrorException e) {
+ WxError error = e.getError();
+
+ if (WxConsts.ACCESS_TOKEN_ERROR_CODES.contains(error.getErrorCode())) {
+ // 强制设置wxCpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token
+ this.configStorage.expireAccessToken();
+ if (this.getWxCpConfigStorage().autoRefreshToken()) {
+ log.warn("即将重新获取新的access_token,错误代码:{},错误信息:{}", error.getErrorCode(), error.getErrorMsg());
+ return this.execute(executor, uri, data);
+ }
+ }
+
+ if (error.getErrorCode() != 0) {
+ log.error("\n【请求地址】: {}\n【请求参数】:{}\n【错误信息】:{}", uriWithAccessToken, dataForLog, error);
+ throw new WxErrorException(error, e);
+ }
+ return null;
+ } catch (IOException e) {
+ log.error("\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uriWithAccessToken, dataForLog, e.getMessage());
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 普通请求,不自动带accessToken
+ */
+ private T executeNormal(RequestExecutor executor, String uri, E data) throws WxErrorException {
+ try {
+ T result = executor.execute(uri, data, WxType.CP);
+ log.debug("\n【请求地址】: {}\n【请求参数】:{}\n【响应数据】:{}", uri, data, result);
+ return result;
+ } catch (WxErrorException e) {
+ WxError error = e.getError();
+ if (error.getErrorCode() != 0) {
+ log.error("\n【请求地址】: {}\n【请求参数】:{}\n【错误信息】:{}", uri, data, error);
+ throw new WxErrorException(error, e);
+ }
+ return null;
+ } catch (IOException e) {
+ log.error("\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uri, data, e.getMessage());
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void setWxCpConfigStorage(WxCpConfigStorage wxConfigProvider) {
+ this.configStorage = wxConfigProvider;
+ this.initHttp();
+ }
+
+ @Override
+ public void setRetrySleepMillis(int retrySleepMillis) {
+ this.retrySleepMillis = retrySleepMillis;
+ }
+
+
+ @Override
+ public void setMaxRetryTimes(int maxRetryTimes) {
+ this.maxRetryTimes = maxRetryTimes;
+ }
+
+ @Override
+ public WxSession getSession(String id) {
+ if (this.sessionManager == null) {
+ return null;
+ }
+ return this.sessionManager.getSession(id);
+ }
+
+ @Override
+ public WxSession getSession(String id, boolean create) {
+ if (this.sessionManager == null) {
+ return null;
+ }
+ return this.sessionManager.getSession(id, create);
+ }
+
+ @Override
+ public void setSessionManager(WxSessionManager sessionManager) {
+ this.sessionManager = sessionManager;
+ }
+
+ @Override
+ public WxSessionManager getSessionManager() {
+ return this.sessionManager;
+ }
+
+ @Override
+ public String replaceParty(String mediaId) throws WxErrorException {
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("media_id", mediaId);
+ return post(this.configStorage.getApiUrl(BATCH_REPLACE_PARTY), jsonObject.toString());
+ }
+
+ @Override
+ public String replaceUser(String mediaId) throws WxErrorException {
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("media_id", mediaId);
+ return post(this.configStorage.getApiUrl(BATCH_REPLACE_USER), jsonObject.toString());
+ }
+
+ @Override
+ public String getTaskResult(String joinId) throws WxErrorException {
+ String url = this.configStorage.getApiUrl(BATCH_GET_RESULT + joinId);
+ return get(url, null);
+ }
+
+ public File getTmpDirFile() {
+ return this.tmpDirFile;
+ }
+
+ public void setTmpDirFile(File tmpDirFile) {
+ this.tmpDirFile = tmpDirFile;
+ }
+
+ @Override
+ public WxCpDepartmentService getDepartmentService() {
+ return departmentService;
+ }
+
+ @Override
+ public WxCpMediaService getMediaService() {
+ return mediaService;
+ }
+
+ @Override
+ public WxCpMenuService getMenuService() {
+ return menuService;
+ }
+
+ @Override
+ public WxCpOAuth2Service getOauth2Service() {
+ return oauth2Service;
+ }
+
+ @Override
+ public WxCpTagService getTagService() {
+ return tagService;
+ }
+
+ @Override
+ public WxCpUserService getUserService() {
+ return userService;
+ }
+
+ @Override
+ public WxCpExternalContactService getExternalContactService() {
+ return externalContactService;
+ }
+
+ @Override
+ public WxCpChatService getChatService() {
+ return chatService;
+ }
+
+ @Override
+ public WxCpOaService getOAService() {
+ return oaService;
+ }
+
+ @Override
+ public WxCpGroupRobotService getGroupRobotService() {
+ return groupRobotService;
+ }
+
+ @Override
+ public WxCpTaskCardService getTaskCardService() {
+ return taskCardService;
+ }
+
+ @Override
+ public RequestHttp, ?> getRequestHttp() {
+ return this;
+ }
+
+ @Override
+ public void setUserService(WxCpUserService userService) {
+ this.userService = userService;
+ }
+
+ @Override
+ public void setDepartmentService(WxCpDepartmentService departmentService) {
+ this.departmentService = departmentService;
+ }
+
+ @Override
+ public void setMediaService(WxCpMediaService mediaService) {
+ this.mediaService = mediaService;
+ }
+
+ @Override
+ public void setMenuService(WxCpMenuService menuService) {
+ this.menuService = menuService;
+ }
+
+ @Override
+ public void setOauth2Service(WxCpOAuth2Service oauth2Service) {
+ this.oauth2Service = oauth2Service;
+ }
+
+ @Override
+ public void setTagService(WxCpTagService tagService) {
+ this.tagService = tagService;
+ }
+
+ @Override
+ public WxCpAgentService getAgentService() {
+ return agentService;
+ }
+
+ public void setAgentService(WxCpAgentService agentService) {
+ this.agentService = agentService;
+ }
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpTpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpTpServiceImpl.java
new file mode 100644
index 0000000000..191bfec0d8
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpTpServiceImpl.java
@@ -0,0 +1,276 @@
+package me.chanjar.weixin.cp.api.impl;
+
+import com.google.common.base.Joiner;
+import com.google.gson.JsonObject;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.bean.WxAccessToken;
+import me.chanjar.weixin.common.error.WxCpErrorMsgEnum;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.DataUtils;
+import me.chanjar.weixin.common.util.crypto.SHA1;
+import me.chanjar.weixin.common.util.http.RequestExecutor;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor;
+import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor;
+import me.chanjar.weixin.common.util.json.GsonParser;
+import me.chanjar.weixin.cp.api.WxCpTpService;
+import me.chanjar.weixin.cp.bean.*;
+import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.Map;
+
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Tp.*;
+
+/**
+ * .
+ *
+ * @author zhenjun cai
+ */
+@Slf4j
+public abstract class BaseWxCpTpServiceImpl implements WxCpTpService, RequestHttp {
+
+ /**
+ * 全局的是否正在刷新access token的锁.
+ */
+ protected final Object globalSuiteAccessTokenRefreshLock = new Object();
+
+ /**
+ * 全局的是否正在刷新jsapi_ticket的锁.
+ */
+ protected final Object globalSuiteTicketRefreshLock = new Object();
+
+ protected WxCpTpConfigStorage configStorage;
+
+ /**
+ * 临时文件目录.
+ */
+ private File tmpDirFile;
+ private int retrySleepMillis = 1000;
+ private int maxRetryTimes = 5;
+
+ @Override
+ public boolean checkSignature(String msgSignature, String timestamp, String nonce, String data) {
+ try {
+ return SHA1.gen(this.configStorage.getToken(), timestamp, nonce, data)
+ .equals(msgSignature);
+ } catch (Exception e) {
+ log.error("Checking signature failed, and the reason is :" + e.getMessage());
+ return false;
+ }
+ }
+
+ @Override
+ public String getSuiteAccessToken() throws WxErrorException {
+ return getSuiteAccessToken(false);
+ }
+
+ @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);
+ throw new WxErrorException(wxError);
+ }
+ return this.configStorage.getSuiteTicket();
+ }
+
+
+ @Override
+ public WxCpMaJsCode2SessionResult jsCode2Session(String jsCode) throws WxErrorException {
+ Map params = new HashMap<>(2);
+ params.put("js_code", jsCode);
+ params.put("grant_type", "authorization_code");
+
+ final String url = configStorage.getApiUrl(JSCODE_TO_SESSION);
+ return WxCpMaJsCode2SessionResult.fromJson(this.get(url, Joiner.on("&").withKeyValueSeparator("=").join(params)));
+ }
+
+
+ @Override
+ public WxAccessToken getCorpToken(String authCorpid, String permanentCode) throws WxErrorException {
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("auth_corpid", authCorpid);
+ jsonObject.addProperty("permanent_code", permanentCode);
+ String result = post(configStorage.getApiUrl(GET_CORP_TOKEN), jsonObject.toString());
+
+ return WxAccessToken.fromJson(result);
+ }
+
+ @Override
+ public WxCpTpCorp getPermanentCode(String authCode) throws WxErrorException {
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("auth_code", authCode);
+
+ String result = post(configStorage.getApiUrl(GET_PERMANENT_CODE), jsonObject.toString());
+ jsonObject = GsonParser.parse(result);
+ WxCpTpCorp wxCpTpCorp = WxCpTpCorp.fromJson(jsonObject.get("auth_corp_info").getAsJsonObject().toString());
+ wxCpTpCorp.setPermanentCode(jsonObject.get("permanent_code").getAsString());
+ return wxCpTpCorp;
+ }
+
+ @Override
+ public WxCpTpPermanentCodeInfo getPermanentCodeInfo(String authCode) throws WxErrorException{
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("auth_code", authCode);
+ String result = post(configStorage.getApiUrl(GET_PERMANENT_CODE), jsonObject.toString());
+ return WxCpTpPermanentCodeInfo.fromJson(result);
+ }
+
+ @Override
+ @SneakyThrows
+ public String getPreAuthUrl(String redirectUri,String state) throws WxErrorException{
+ String result = get(configStorage.getApiUrl(GET_PREAUTH_CODE),null);
+ WxCpTpPreauthCode preauthCode = WxCpTpPreauthCode.fromJson(result);
+ String preAuthUrl = "https://open.work.weixin.qq.com/3rdapp/install?suite_id="+configStorage.getSuiteId()+
+ "&pre_auth_code="+preauthCode.getPreAuthCode()+"&redirect_uri="+ URLEncoder.encode(redirectUri,"utf-8");
+ if(StringUtils.isNotBlank(state))
+ preAuthUrl += "&state="+state;
+ return preAuthUrl;
+ }
+
+ @Override
+ public WxCpTpAuthInfo getAuthInfo(String authCorpId, String permanentCode) throws WxErrorException{
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("auth_corpid", authCorpId);
+ jsonObject.addProperty("permanent_code", permanentCode);
+ String result = post(configStorage.getApiUrl(GET_AUTH_INFO), jsonObject.toString());
+ return WxCpTpAuthInfo.fromJson(result);
+ }
+
+ @Override
+ public String get(String url, String queryParam) throws WxErrorException {
+ return execute(SimpleGetRequestExecutor.create(this), url, queryParam);
+ }
+
+ @Override
+ public String post(String url, String postData) throws WxErrorException {
+ return execute(SimplePostRequestExecutor.create(this), url, postData);
+ }
+
+ /**
+ * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求.
+ */
+ @Override
+ public T execute(RequestExecutor executor, String uri, E data) throws WxErrorException {
+ int retryTimes = 0;
+ do {
+ try {
+ return this.executeInternal(executor, uri, data);
+ } catch (WxErrorException e) {
+ if (retryTimes + 1 > this.maxRetryTimes) {
+ log.warn("重试达到最大次数【{}】", this.maxRetryTimes);
+ //最后一次重试失败后,直接抛出异常,不再等待
+ throw new RuntimeException("微信服务端异常,超出重试次数");
+ }
+
+ WxError error = e.getError();
+ /*
+ * -1 系统繁忙, 1000ms后重试
+ */
+ if (error.getErrorCode() == -1) {
+ int sleepMillis = this.retrySleepMillis * (1 << retryTimes);
+ try {
+ log.debug("微信系统繁忙,{} ms 后重试(第{}次)", sleepMillis, retryTimes + 1);
+ Thread.sleep(sleepMillis);
+ } catch (InterruptedException e1) {
+ Thread.currentThread().interrupt();
+ }
+ } else {
+ throw e;
+ }
+ }
+ } while (retryTimes++ < this.maxRetryTimes);
+
+ log.warn("重试达到最大次数【{}】", this.maxRetryTimes);
+ throw new RuntimeException("微信服务端异常,超出重试次数");
+ }
+
+ protected T executeInternal(RequestExecutor executor, String uri, E data) throws WxErrorException {
+ E dataForLog = DataUtils.handleDataWithSecret(data);
+
+ if (uri.contains("suite_access_token=")) {
+ throw new IllegalArgumentException("uri参数中不允许有suite_access_token: " + uri);
+ }
+ String suiteAccessToken = getSuiteAccessToken(false);
+
+ String uriWithAccessToken = uri + (uri.contains("?") ? "&" : "?") + "suite_access_token=" + suiteAccessToken;
+
+ try {
+ T result = executor.execute(uriWithAccessToken, data, WxType.CP);
+ log.debug("\n【请求地址】: {}\n【请求参数】:{}\n【响应数据】:{}", uriWithAccessToken, dataForLog, result);
+ return result;
+ } catch (WxErrorException e) {
+ WxError error = e.getError();
+ /*
+ * 发生以下情况时尝试刷新suite_access_token
+ * 42009 suite_access_token已过期
+ */
+ if (error.getErrorCode() == WxCpErrorMsgEnum.CODE_42009.getCode()) {
+ // 强制设置wxCpTpConfigStorage它的suite access token过期了,这样在下一次请求里就会刷新suite access token
+ this.configStorage.expireSuiteAccessToken();
+ if (this.getWxCpTpConfigStorage().autoRefreshToken()) {
+ log.warn("即将重新获取新的access_token,错误代码:{},错误信息:{}", error.getErrorCode(), error.getErrorMsg());
+ return this.execute(executor, uri, data);
+ }
+ }
+
+ if (error.getErrorCode() != 0) {
+ log.error("\n【请求地址】: {}\n【请求参数】:{}\n【错误信息】:{}", uriWithAccessToken, dataForLog, error);
+ throw new WxErrorException(error, e);
+ }
+ return null;
+ } catch (IOException e) {
+ log.error("\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uriWithAccessToken, dataForLog, e.getMessage());
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void setWxCpTpConfigStorage(WxCpTpConfigStorage wxConfigProvider) {
+ this.configStorage = wxConfigProvider;
+ this.initHttp();
+ }
+
+ @Override
+ public void setRetrySleepMillis(int retrySleepMillis) {
+ this.retrySleepMillis = retrySleepMillis;
+ }
+
+
+ @Override
+ public void setMaxRetryTimes(int maxRetryTimes) {
+ this.maxRetryTimes = maxRetryTimes;
+ }
+
+ public File getTmpDirFile() {
+ return this.tmpDirFile;
+ }
+
+ public void setTmpDirFile(File tmpDirFile) {
+ this.tmpDirFile = tmpDirFile;
+ }
+
+ @Override
+ public RequestHttp, ?> getRequestHttp() {
+ return this;
+ }
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpAgentServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpAgentServiceImpl.java
new file mode 100644
index 0000000000..4dd661fbf0
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpAgentServiceImpl.java
@@ -0,0 +1,67 @@
+package me.chanjar.weixin.cp.api.impl;
+
+import com.google.gson.JsonObject;
+import com.google.gson.reflect.TypeToken;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.enums.WxType;
+import me.chanjar.weixin.common.error.WxError;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.json.GsonParser;
+import me.chanjar.weixin.cp.api.WxCpAgentService;
+import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.bean.WxCpAgent;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.util.List;
+
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Agent.*;
+
+
+/**
+ *
+ * 管理企业号应用
+ * Created by huansinho on 2018/4/13.
+ *
+ *
+ * @author huansinho
+ */
+@RequiredArgsConstructor
+public class WxCpAgentServiceImpl implements WxCpAgentService {
+
+
+ private final WxCpService mainService;
+
+ @Override
+ public WxCpAgent get(Integer agentId) throws WxErrorException {
+ if (agentId == null) {
+ throw new IllegalArgumentException("缺少agentid参数");
+ }
+
+ final String url = String.format(this.mainService.getWxCpConfigStorage().getApiUrl(AGENT_GET), agentId);
+ return WxCpAgent.fromJson(this.mainService.get(url, null));
+ }
+
+ @Override
+ public void set(WxCpAgent agentInfo) throws WxErrorException {
+ String url = this.mainService.getWxCpConfigStorage().getApiUrl(AGENT_SET);
+ String responseContent = this.mainService.post(url, agentInfo.toJson());
+ JsonObject jsonObject = GsonParser.parse(responseContent);
+ if (jsonObject.get("errcode").getAsInt() != 0) {
+ throw new WxErrorException(WxError.fromJson(responseContent, WxType.CP));
+ }
+ }
+
+ @Override
+ public List list() throws WxErrorException {
+ String url = this.mainService.getWxCpConfigStorage().getApiUrl(AGENT_LIST);
+ String responseContent = this.mainService.get(url, null);
+ JsonObject jsonObject = GsonParser.parse(responseContent);
+ if (jsonObject.get("errcode").getAsInt() != 0) {
+ throw new WxErrorException(WxError.fromJson(responseContent, WxType.CP));
+ }
+
+ return WxCpGsonBuilder.create().fromJson(jsonObject.get("agentlist").toString(), new TypeToken>() {
+ }.getType());
+ }
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpChatServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpChatServiceImpl.java
new file mode 100644
index 0000000000..10af36afe6
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpChatServiceImpl.java
@@ -0,0 +1,86 @@
+package me.chanjar.weixin.cp.api.impl;
+
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.json.GsonParser;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+import me.chanjar.weixin.cp.api.WxCpChatService;
+import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.bean.WxCpAppChatMessage;
+import me.chanjar.weixin.cp.bean.WxCpChat;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Chat.*;
+
+/**
+ * 群聊服务实现.
+ *
+ * @author gaigeshen
+ */
+@RequiredArgsConstructor
+public class WxCpChatServiceImpl implements WxCpChatService {
+ private final WxCpService cpService;
+
+ @Override
+ public String create(String name, String owner, List users, String chatId) throws WxErrorException {
+ Map data = new HashMap<>(4);
+ if (StringUtils.isNotBlank(name)) {
+ data.put("name", name);
+ }
+ if (StringUtils.isNotBlank(owner)) {
+ data.put("owner", owner);
+ }
+ if (users != null) {
+ data.put("userlist", users);
+ }
+ if (StringUtils.isNotBlank(chatId)) {
+ data.put("chatid", chatId);
+ }
+ final String url = this.cpService.getWxCpConfigStorage().getApiUrl(APPCHAT_CREATE);
+ String result = this.cpService.post(url, WxGsonBuilder.create().toJson(data));
+ return GsonParser.parse(result).get("chatid").getAsString();
+ }
+
+ @Override
+ public void update(String chatId, String name, String owner, List usersToAdd, List usersToDelete)
+ throws WxErrorException {
+ Map data = new HashMap<>(5);
+ if (StringUtils.isNotBlank(chatId)) {
+ data.put("chatid", chatId);
+ }
+ if (StringUtils.isNotBlank(name)) {
+ data.put("name", name);
+ }
+ if (StringUtils.isNotBlank(owner)) {
+ data.put("owner", owner);
+ }
+ if (usersToAdd != null && !usersToAdd.isEmpty()) {
+ data.put("add_user_list", usersToAdd);
+ }
+ if (usersToDelete != null && !usersToDelete.isEmpty()) {
+ data.put("del_user_list", usersToDelete);
+ }
+
+ final String url = this.cpService.getWxCpConfigStorage().getApiUrl(APPCHAT_UPDATE);
+ this.cpService.post(url, WxGsonBuilder.create().toJson(data));
+ }
+
+ @Override
+ public WxCpChat get(String chatId) throws WxErrorException {
+ final String url = this.cpService.getWxCpConfigStorage().getApiUrl(APPCHAT_GET_CHATID + chatId);
+ String result = this.cpService.get(url, null);
+ final String chatInfo = GsonParser.parse(result).getAsJsonObject("chat_info").toString();
+ return WxCpGsonBuilder.create().fromJson(chatInfo, WxCpChat.class);
+ }
+
+ @Override
+ public void sendMsg(WxCpAppChatMessage message) throws WxErrorException {
+ this.cpService.post(this.cpService.getWxCpConfigStorage().getApiUrl(APPCHAT_SEND), message.toJson());
+ }
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpDepartmentServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpDepartmentServiceImpl.java
new file mode 100644
index 0000000000..3a5ef87985
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpDepartmentServiceImpl.java
@@ -0,0 +1,65 @@
+package me.chanjar.weixin.cp.api.impl;
+
+import com.google.gson.JsonObject;
+import com.google.gson.reflect.TypeToken;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.json.GsonHelper;
+import me.chanjar.weixin.common.util.json.GsonParser;
+import me.chanjar.weixin.cp.api.WxCpDepartmentService;
+import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.bean.WxCpDepart;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.util.List;
+
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Department.*;
+
+/**
+ *
+ * 部门管理接口
+ * Created by BinaryWang on 2017/6/24.
+ *
+ *
+ * @author Binary Wang
+ */
+@RequiredArgsConstructor
+public class WxCpDepartmentServiceImpl implements WxCpDepartmentService {
+ private final WxCpService mainService;
+
+ @Override
+ public Long create(WxCpDepart depart) throws WxErrorException {
+ String url = this.mainService.getWxCpConfigStorage().getApiUrl(DEPARTMENT_CREATE);
+ String responseContent = this.mainService.post(url, depart.toJson());
+ JsonObject tmpJsonObject = GsonParser.parse(responseContent);
+ return GsonHelper.getAsLong(tmpJsonObject.get("id"));
+ }
+
+ @Override
+ public void update(WxCpDepart group) throws WxErrorException {
+ String url = this.mainService.getWxCpConfigStorage().getApiUrl(DEPARTMENT_UPDATE);
+ this.mainService.post(url, group.toJson());
+ }
+
+ @Override
+ public void delete(Long departId) throws WxErrorException {
+ String url = String.format(this.mainService.getWxCpConfigStorage().getApiUrl(DEPARTMENT_DELETE), departId);
+ this.mainService.get(url, null);
+ }
+
+ @Override
+ public List list(Long id) throws WxErrorException {
+ String url = this.mainService.getWxCpConfigStorage().getApiUrl(DEPARTMENT_LIST);
+ if (id != null) {
+ url += "?id=" + id;
+ }
+
+ String responseContent = this.mainService.get(url, null);
+ JsonObject tmpJsonObject = GsonParser.parse(responseContent);
+ return WxCpGsonBuilder.create()
+ .fromJson(tmpJsonObject.get("department"),
+ new TypeToken>() {
+ }.getType()
+ );
+ }
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpExternalContactServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpExternalContactServiceImpl.java
new file mode 100644
index 0000000000..b64ec0e870
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpExternalContactServiceImpl.java
@@ -0,0 +1,298 @@
+package me.chanjar.weixin.cp.api.impl;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.error.WxCpErrorMsgEnum;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.api.WxCpExternalContactService;
+import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.bean.*;
+import me.chanjar.weixin.cp.bean.external.*;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.ExternalContact.*;
+
+/**
+ * @author 曹祖鹏 & yuanqixun
+ */
+@RequiredArgsConstructor
+public class WxCpExternalContactServiceImpl implements WxCpExternalContactService {
+ private final WxCpService mainService;
+
+ @Override
+ public WxCpContactWayResult addContactWay(@NonNull WxCpContactWayInfo info) throws WxErrorException {
+
+ if (info.getContactWay().getUsers() != null && info.getContactWay().getUsers().size() > 100) {
+ throw new RuntimeException("「联系我」使用人数默认限制不超过100人(包括部门展开后的人数)");
+ }
+
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(ADD_CONTACT_WAY);
+ String responseContent = this.mainService.post(url, info.getContactWay().toJson());
+
+ return WxCpContactWayResult.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpContactWayInfo getContactWay(@NonNull String configId) throws WxErrorException {
+ JsonObject json = new JsonObject();
+ json.addProperty("config_id", configId);
+
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_CONTACT_WAY);
+ String responseContent = this.mainService.post(url, json.toString());
+ return WxCpContactWayInfo.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp updateContactWay(@NonNull WxCpContactWayInfo info) throws WxErrorException {
+ if (StringUtils.isBlank(info.getContactWay().getConfigId())) {
+ throw new RuntimeException("更新「联系我」方式需要指定configId");
+ }
+ if (info.getContactWay().getUsers() != null && info.getContactWay().getUsers().size() > 100) {
+ throw new RuntimeException("「联系我」使用人数默认限制不超过100人(包括部门展开后的人数)");
+ }
+
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(UPDATE_CONTACT_WAY);
+ String responseContent = this.mainService.post(url, info.getContactWay().toJson());
+
+ return WxCpBaseResp.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp deleteContactWay(@NonNull String configId) throws WxErrorException {
+ JsonObject json = new JsonObject();
+ json.addProperty("config_id",configId);
+
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(DEL_CONTACT_WAY);
+ String responseContent = this.mainService.post(url, json.toString());
+
+ return WxCpBaseResp.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp closeTempChat(@NonNull String userId, @NonNull String externalUserId) throws WxErrorException {
+
+ JsonObject json = new JsonObject();
+ json.addProperty("userid",userId);
+ json.addProperty("external_userid",externalUserId);
+
+
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(CLOSE_TEMP_CHAT);
+ String responseContent = this.mainService.post(url, json.toString());
+
+ return WxCpBaseResp.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpUserExternalContactInfo getExternalContact(String userId) throws WxErrorException {
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_EXTERNAL_CONTACT + userId);
+ String responseContent = this.mainService.get(url, null);
+ return WxCpUserExternalContactInfo.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpUserExternalContactInfo getContactDetail(String userId) throws WxErrorException {
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_CONTACT_DETAIL + userId);
+ String responseContent = this.mainService.get(url, null);
+ return WxCpUserExternalContactInfo.fromJson(responseContent);
+ }
+
+ @Override
+ public List listExternalContacts(String userId) throws WxErrorException {
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(LIST_EXTERNAL_CONTACT + userId);
+ try {
+ String responseContent = this.mainService.get(url, null);
+ return WxCpUserExternalContactList.fromJson(responseContent).getExternalUserId();
+ } catch (WxErrorException e) {
+ // not external contact,无客户则返回空列表
+ if (e.getError().getErrorCode() == WxCpErrorMsgEnum.CODE_84061.getCode()) {
+ return Collections.emptyList();
+ }
+ throw e;
+ }
+ }
+
+ @Override
+ public List listFollowers() throws WxErrorException {
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_FOLLOW_USER_LIST);
+ String responseContent = this.mainService.get(url, null);
+ return WxCpUserWithExternalPermission.fromJson(responseContent).getFollowers();
+ }
+
+ @Override
+ public WxCpUserExternalUnassignList listUnassignedList(Integer pageIndex, Integer pageSize) throws WxErrorException {
+ JsonObject json = new JsonObject();
+ json.addProperty("page_id", pageIndex == null ? 0 : pageIndex);
+ json.addProperty("page_size", pageSize == null ? 100 : pageSize);
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(LIST_UNASSIGNED_CONTACT);
+ final String result = this.mainService.post(url, json.toString());
+ return WxCpUserExternalUnassignList.fromJson(result);
+ }
+
+ @Override
+ public WxCpBaseResp transferExternalContact(String externalUserid, String handOverUserid, String takeOverUserid) throws WxErrorException {
+ JsonObject json = new JsonObject();
+ json.addProperty("external_userid", externalUserid);
+ json.addProperty("handover_userid", handOverUserid);
+ json.addProperty("takeover_userid", takeOverUserid);
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(TRANSFER_UNASSIGNED_CONTACT);
+ final String result = this.mainService.post(url, json.toString());
+ return WxCpBaseResp.fromJson(result);
+ }
+
+ @Override
+ public WxCpUserExternalGroupChatList listGroupChat(Integer pageIndex, Integer pageSize, int status, String[] userIds, String[] partyIds) throws WxErrorException {
+ JsonObject json = new JsonObject();
+ json.addProperty("offset", pageIndex == null ? 0 : pageIndex);
+ json.addProperty("limit", pageSize == null ? 100 : pageSize);
+ json.addProperty("status_filter", status);
+ if (ArrayUtils.isNotEmpty(userIds) || ArrayUtils.isNotEmpty(partyIds)) {
+ JsonObject ownerFilter = new JsonObject();
+ if (ArrayUtils.isNotEmpty(userIds)) {
+ json.add("userid_list", new Gson().toJsonTree(userIds).getAsJsonArray());
+ }
+ if (ArrayUtils.isNotEmpty(partyIds)) {
+ json.add("partyid_list", new Gson().toJsonTree(partyIds).getAsJsonArray());
+ }
+ json.add("owner_filter", ownerFilter);
+ }
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GROUP_CHAT_LIST);
+ final String result = this.mainService.post(url, json.toString());
+ return WxCpUserExternalGroupChatList.fromJson(result);
+ }
+
+ @Override
+ public WxCpUserExternalGroupChatInfo getGroupChat(String chatId) throws WxErrorException {
+ JsonObject json = new JsonObject();
+ json.addProperty("chat_id", chatId);
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GROUP_CHAT_INFO);
+ final String result = this.mainService.post(url, json.toString());
+ return WxCpUserExternalGroupChatInfo.fromJson(result);
+ }
+
+ @Override
+ public WxCpUserExternalUserBehaviorStatistic getUserBehaviorStatistic(Date startTime, Date endTime, String[] userIds, String[] partyIds) throws WxErrorException {
+ JsonObject json = new JsonObject();
+ json.addProperty("start_time", startTime.getTime() / 1000);
+ json.addProperty("end_time", endTime.getTime() / 1000);
+ if (ArrayUtils.isNotEmpty(userIds) || ArrayUtils.isNotEmpty(partyIds)) {
+ if (ArrayUtils.isNotEmpty(userIds)) {
+ json.add("userid", new Gson().toJsonTree(userIds).getAsJsonArray());
+ }
+ if (ArrayUtils.isNotEmpty(partyIds)) {
+ json.add("partyid", new Gson().toJsonTree(partyIds).getAsJsonArray());
+ }
+ }
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(LIST_USER_BEHAVIOR_DATA);
+ final String result = this.mainService.post(url, json.toString());
+ return WxCpUserExternalUserBehaviorStatistic.fromJson(result);
+ }
+
+ @Override
+ public WxCpUserExternalGroupChatStatistic getGroupChatStatistic(Date startTime, Integer orderBy, Integer orderAsc, Integer pageIndex, Integer pageSize, String[] userIds, String[] partyIds) throws WxErrorException {
+ JsonObject json = new JsonObject();
+ json.addProperty("day_begin_time", startTime.getTime() / 1000);
+ json.addProperty("order_by", orderBy == null ? 1 : orderBy);
+ json.addProperty("order_asc", orderAsc == null ? 0 : orderAsc);
+ json.addProperty("offset", pageIndex == null ? 0 : pageIndex);
+ json.addProperty("limit", pageSize == null ? 500 : pageSize);
+ if (ArrayUtils.isNotEmpty(userIds) || ArrayUtils.isNotEmpty(partyIds)) {
+ JsonObject ownerFilter = new JsonObject();
+ if (ArrayUtils.isNotEmpty(userIds)) {
+ json.add("userid_list", new Gson().toJsonTree(userIds).getAsJsonArray());
+ }
+ if (ArrayUtils.isNotEmpty(partyIds)) {
+ json.add("partyid_list", new Gson().toJsonTree(partyIds).getAsJsonArray());
+ }
+ json.add("owner_filter", ownerFilter);
+ }
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(LIST_GROUP_CHAT_DATA);
+ final String result = this.mainService.post(url, json.toString());
+ return WxCpUserExternalGroupChatStatistic.fromJson(result);
+ }
+
+ @Override
+ public WxCpMsgTemplateAddResult addMsgTemplate(WxCpMsgTemplate wxCpMsgTemplate) throws WxErrorException {
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(ADD_MSG_TEMPLATE);
+ final String result = this.mainService.post(url, wxCpMsgTemplate.toJson());
+ return WxCpMsgTemplateAddResult.fromJson(result);
+ }
+
+ @Override
+ public void sendWelcomeMsg(WxCpWelcomeMsg msg) throws WxErrorException {
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(SEND_WELCOME_MSG);
+ this.mainService.post(url, msg.toJson());
+ }
+
+ @Override
+ public WxCpUserExternalTagGroupList getCorpTagList(String[] tagId) throws WxErrorException {
+ JsonObject json = new JsonObject();
+ if(ArrayUtils.isNotEmpty(tagId)){
+ json.add("tag_id",new Gson().toJsonTree(tagId).getAsJsonArray());
+ }
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_CORP_TAG_LIST);
+ final String result = this.mainService.post(url,json.toString());
+ return WxCpUserExternalTagGroupList.fromJson(result);
+ }
+
+ @Override
+ public WxCpUserExternalTagGroupInfo addCorpTag(WxCpUserExternalTagGroupInfo tagGroup) throws WxErrorException{
+
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(ADD_CORP_TAG);
+ final String result = this.mainService.post(url,tagGroup.getTagGroup().toJson());
+ return WxCpUserExternalTagGroupInfo.fromJson(result);
+ }
+
+ @Override
+ public WxCpBaseResp editCorpTag(String id, String name, Integer order) throws WxErrorException{
+
+ JsonObject json = new JsonObject();
+ json.addProperty("id",id);
+ json.addProperty("name",name);
+ json.addProperty("order",order);
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(EDIT_CORP_TAG);
+ final String result = this.mainService.post(url,json.toString());
+ return WxCpBaseResp.fromJson(result);
+ }
+
+ @Override
+ public WxCpBaseResp delCorpTag(String[] tagId, String[] groupId) throws WxErrorException{
+ JsonObject json = new JsonObject();
+ if(ArrayUtils.isNotEmpty(tagId)){
+ json.add("tag_id",new Gson().toJsonTree(tagId).getAsJsonArray());
+ }
+ if(ArrayUtils.isNotEmpty(groupId)){
+ json.add("group_id",new Gson().toJsonTree(groupId).getAsJsonArray());
+ }
+
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(DEL_CORP_TAG);
+ final String result = this.mainService.post(url,json.toString());
+ return WxCpBaseResp.fromJson(result);
+ }
+
+ @Override
+ public WxCpBaseResp markTag(String userid, String externalUserid, String[] addTag, String[] removeTag)throws WxErrorException{
+
+
+ JsonObject json = new JsonObject();
+ json.addProperty("userid",userid);
+ json.addProperty("external_userid",externalUserid);
+
+ if(ArrayUtils.isNotEmpty(addTag)){
+ json.add("add_tag",new Gson().toJsonTree(addTag).getAsJsonArray());
+ }
+ if(ArrayUtils.isNotEmpty(removeTag)){
+ json.add("remove_tag",new Gson().toJsonTree(removeTag).getAsJsonArray());
+ }
+
+ final String url = this.mainService.getWxCpConfigStorage().getApiUrl(MARK_TAG);
+ final String result = this.mainService.post(url,json.toString());
+ return WxCpBaseResp.fromJson(result);
+ }
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpGroupRobotServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpGroupRobotServiceImpl.java
new file mode 100644
index 0000000000..ed4d8a108e
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpGroupRobotServiceImpl.java
@@ -0,0 +1,65 @@
+package me.chanjar.weixin.cp.api.impl;
+
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.api.WxConsts;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.api.WxCpGroupRobotService;
+import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.bean.WxCpGroupRobotMessage;
+import me.chanjar.weixin.cp.bean.article.NewArticle;
+import me.chanjar.weixin.cp.config.WxCpConfigStorage;
+import me.chanjar.weixin.cp.constant.WxCpApiPathConsts;
+
+import java.util.List;
+
+/**
+ * 微信群机器人消息发送api 实现
+ *
+ * @author yr
+ * @date 2020-08-20
+ */
+@RequiredArgsConstructor
+public class WxCpGroupRobotServiceImpl implements WxCpGroupRobotService {
+ private final WxCpService cpService;
+
+ private String getApiUrl() {
+ WxCpConfigStorage wxCpConfigStorage = cpService.getWxCpConfigStorage();
+ return wxCpConfigStorage.getApiUrl(WxCpApiPathConsts.WEBHOOK_SEND) + wxCpConfigStorage.getWebhookKey();
+ }
+
+ @Override
+ public void sendText(String content, List mentionedList, List mobileList) throws WxErrorException {
+ WxCpGroupRobotMessage message = new WxCpGroupRobotMessage()
+ .setMsgType(WxConsts.GroupRobotMsgType.TEXT)
+ .setContent(content)
+ .setMentionedList(mentionedList)
+ .setMentionedMobileList(mobileList);
+ cpService.postWithoutToken(this.getApiUrl(), message.toJson());
+ }
+
+ @Override
+ public void sendMarkDown(String content) throws WxErrorException {
+ WxCpGroupRobotMessage message = new WxCpGroupRobotMessage()
+ .setMsgType(WxConsts.GroupRobotMsgType.MARKDOWN)
+ .setContent(content);
+ cpService.postWithoutToken(this.getApiUrl(), message.toJson());
+ }
+
+ @Override
+ public void sendImage(String base64, String md5) throws WxErrorException {
+ WxCpGroupRobotMessage message = new WxCpGroupRobotMessage()
+ .setMsgType(WxConsts.GroupRobotMsgType.IMAGE)
+ .setBase64(base64)
+ .setMd5(md5);
+ cpService.postWithoutToken(this.getApiUrl(), message.toJson());
+ }
+
+ @Override
+ public void sendNews(List articleList) throws WxErrorException {
+ WxCpGroupRobotMessage message = new WxCpGroupRobotMessage()
+ .setMsgType(WxConsts.GroupRobotMsgType.NEWS)
+ .setArticles(articleList);
+ cpService.postWithoutToken(this.getApiUrl(), message.toJson());
+ }
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImpl.java
new file mode 100644
index 0000000000..b83b6d39ab
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImpl.java
@@ -0,0 +1,65 @@
+package me.chanjar.weixin.cp.api.impl;
+
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.fs.FileUtils;
+import me.chanjar.weixin.common.util.http.BaseMediaDownloadRequestExecutor;
+import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor;
+import me.chanjar.weixin.cp.api.WxCpMediaService;
+import me.chanjar.weixin.cp.api.WxCpService;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.UUID;
+
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Media.*;
+
+/**
+ *