diff --git a/pom.xml b/pom.xml
index ac44c2fc..86e9c096 100644
--- a/pom.xml
+++ b/pom.xml
@@ -93,6 +93,7 @@
weixin-java-cp
weixin-java-mp
weixin-java-pay
+ weixin-java-miniapp
diff --git a/weixin-java-miniapp/pom.xml b/weixin-java-miniapp/pom.xml
new file mode 100644
index 00000000..1dbf7835
--- /dev/null
+++ b/weixin-java-miniapp/pom.xml
@@ -0,0 +1,84 @@
+
+
+ 4.0.0
+
+ com.github.binarywang
+ weixin-java-parent
+ 2.6.3.BETA
+
+ weixin-java-miniapp
+ WeiXin Java Tools - MiniApp
+ 微信小程序Java SDK
+
+
+
+ com.github.binarywang
+ weixin-java-common
+ ${project.version}
+
+
+
+ org.jodd
+ jodd-http
+ provided
+
+
+ com.squareup.okhttp3
+ okhttp
+ provided
+
+
+
+ org.testng
+ testng
+ test
+
+
+ ch.qos.logback
+ logback-classic
+ test
+
+
+ com.google.inject
+ guice
+ test
+
+
+ org.eclipse.jetty
+ jetty-server
+ test
+
+
+ org.eclipse.jetty
+ jetty-servlet
+ test
+
+
+ joda-time
+ joda-time
+ test
+
+
+ redis.clients
+ jedis
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ src/test/resources/testng.xml
+
+
+
+
+
+
+
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaMediaService.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaMediaService.java
new file mode 100644
index 00000000..09c8aa5b
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaMediaService.java
@@ -0,0 +1,65 @@
+package cn.binarywang.wx.miniapp.api;
+
+import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
+import me.chanjar.weixin.common.exception.WxErrorException;
+
+import java.io.File;
+import java.io.InputStream;
+
+/**
+ *
+ * 临时素材接口
+ * Created by Binary Wang on 2016/7/21.
+ *
+ *
+ * @author Binary Wang
+ */
+public interface WxMaMediaService {
+ String MEDIA_UPLOAD_URL = "https://api.weixin.qq.com/cgi-bin/media/upload?type=%s";
+ String MEDIA_GET_URL = "https://api.weixin.qq.com/cgi-bin/media/get";
+
+ /**
+ *
+ * 新增临时素材
+ * 小程序可以使用本接口把媒体文件(目前仅支持图片)上传到微信服务器,用户发送客服消息或被动回复用户消息。
+ * 详情请见: 新增临时素材
+ * 接口url格式:https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE
+ *
+ *
+ * @param mediaType 媒体类型,
+ * @param file 文件对象
+ * @see #uploadMedia(String, String, InputStream)
+ */
+ WxMediaUploadResult uploadMedia(String mediaType, File file) throws WxErrorException;
+
+ /**
+ *
+ * 新增临时素材
+ * 小程序可以使用本接口把媒体文件(目前仅支持图片)上传到微信服务器,用户发送客服消息或被动回复用户消息。
+ *
+ * 详情请见: 新增临时素材
+ * 接口url格式:https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE
+ *
+ *
+ * @param mediaType 媒体类型
+ * @param fileType 文件类型
+ * @param inputStream 输入流
+ * @see #uploadMedia(java.lang.String, java.io.File)
+ */
+ WxMediaUploadResult uploadMedia(String mediaType, String fileType, InputStream inputStream) throws WxErrorException;
+
+ /**
+ *
+ * 获取临时素材
+ * 小程序可以使用本接口获取客服消息内的临时素材(即下载临时的多媒体文件)。目前小程序仅支持下载图片文件。
+ *
+ * 详情请见: 获取临时素材
+ * 接口url格式:https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID
+ *
+ *
+ * @param mediaId 媒体Id
+ * @return 保存到本地的临时文件
+ */
+ File getMedia(String mediaId) throws WxErrorException;
+
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaMsgService.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaMsgService.java
new file mode 100644
index 00000000..040e81e5
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaMsgService.java
@@ -0,0 +1,36 @@
+package cn.binarywang.wx.miniapp.api;
+
+import cn.binarywang.wx.miniapp.bean.WxMaKefuMessage;
+import cn.binarywang.wx.miniapp.bean.WxMaTemplateMessage;
+import me.chanjar.weixin.common.exception.WxErrorException;
+
+/**
+ *
+ * 消息发送接口
+ *
+ *
+ * @author Binary Wang
+ */
+public interface WxMaMsgService {
+ String KEFU_MESSAGE_SEND_URL = "https://api.weixin.qq.com/cgi-bin/message/custom/send";
+ String TEMPLATE_MSG_SEND_URL = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send";
+
+ /**
+ *
+ * 发送客服消息
+ * 详情请见: 发送客服消息
+ * 接口url格式:https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
+ *
+ */
+ boolean sendKefuMsg(WxMaKefuMessage message) throws WxErrorException;
+
+ /**
+ *
+ * 发送模板消息
+ * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1433751277&token=&lang=zh_CN
+ *
+ *
+ * @return 消息Id
+ */
+ String sendTemplateMsg(WxMaTemplateMessage templateMessage) throws WxErrorException;
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaQrcodeService.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaQrcodeService.java
new file mode 100644
index 00000000..822f5de8
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaQrcodeService.java
@@ -0,0 +1,30 @@
+package cn.binarywang.wx.miniapp.api;
+
+import me.chanjar.weixin.common.exception.WxErrorException;
+
+import java.io.File;
+
+/**
+ *
+ * 二维码相关操作接口
+ * 文档地址:https://mp.weixin.qq.com/debug/wxadoc/dev/api/qrcode.html
+ *
+ *
+ * @author Binary Wang
+ */
+public interface WxMaQrcodeService {
+
+ /**
+ *
+ * 获取小程序页面二维码
+ * 适用于需要的码数量较少的业务场景
+ * 通过该接口,仅能生成已发布的小程序的二维码。
+ * 可以在开发者工具预览时生成开发版的带参二维码。
+ * 带参二维码只有 100000 个,请谨慎调用。
+ *
+ *
+ * @param path 不能为空,最大长度 128 字节
+ * @param width 默认430 二维码的宽度
+ */
+ File createQrcode(String path, int width) throws WxErrorException;
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaService.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaService.java
new file mode 100644
index 00000000..9098d4ac
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaService.java
@@ -0,0 +1,135 @@
+package cn.binarywang.wx.miniapp.api;
+
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import me.chanjar.weixin.common.exception.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;
+
+/**
+ * @author Binary Wang
+ */
+public interface WxMaService {
+ /**
+ * 获取access_token
+ */
+ String GET_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
+
+ /**
+ *
+ * 验证消息的确来自微信服务器
+ * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319&token=&lang=zh_CN
+ *
+ */
+ boolean checkSignature(String timestamp, String nonce, String signature);
+
+ /**
+ * 获取access_token, 不强制刷新access_token
+ *
+ * @see #getAccessToken(boolean)
+ */
+ String getAccessToken() throws WxErrorException;
+
+ /**
+ *
+ * 获取access_token,本方法线程安全
+ * 且在多线程同时刷新时只刷新一次,避免超出2000次/日的调用次数上限
+ *
+ * 另:本service的所有方法都会在access_token过期是调用此方法
+ *
+ * 程序员在非必要情况下尽量不要主动调用此方法
+ *
+ * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183&token=&lang=zh_CN
+ *
+ *
+ * @param forceRefresh 强制刷新
+ */
+ String getAccessToken(boolean forceRefresh) throws WxErrorException;
+
+ /**
+ * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的GET请求
+ */
+ String get(String url, String queryParam) throws WxErrorException;
+
+ /**
+ * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求
+ */
+ String post(String url, String postData) throws WxErrorException;
+
+ /**
+ *
+ * Service没有实现某个API的时候,可以用这个,
+ * 比{@link #get}和{@link #post}方法更灵活,可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型。
+ * 可以参考,{@link MediaUploadRequestExecutor}的实现方法
+ *
+ */
+ T execute(RequestExecutor executor, String uri, E data) throws WxErrorException;
+
+ /**
+ *
+ * 设置当微信系统响应系统繁忙时,要等待多少 retrySleepMillis(ms) * 2^(重试次数 - 1) 再发起重试
+ * 默认:1000ms
+ *
+ */
+ void setRetrySleepMillis(int retrySleepMillis);
+
+ /**
+ *
+ * 设置当微信系统响应系统繁忙时,最大重试次数
+ * 默认:5次
+ *
+ */
+ void setMaxRetryTimes(int maxRetryTimes);
+
+ /**
+ * 获取WxMaConfig 对象
+ *
+ * @return WxMaConfig
+ */
+ WxMaConfig getWxMaConfig();
+
+ /**
+ * 注入 {@link WxMaConfig} 的实现
+ */
+ void setWxMaConfig(WxMaConfig wxConfigProvider);
+
+ /**
+ * 返回消息(客服消息和模版消息)发送接口方法实现类,以方便调用其各个接口
+ *
+ * @return WxMaMsgService
+ */
+ WxMaMsgService getMsgService();
+
+ /**
+ * 返回素材相关接口方法的实现类对象,以方便调用其各个接口
+ *
+ * @return WxMaMediaService
+ */
+ WxMaMediaService getMediaService();
+
+ /**
+ * 返回用户相关接口方法的实现类对象,以方便调用其各个接口
+ *
+ * @return WxMaUserService
+ */
+ WxMaUserService getUserService();
+
+ /**
+ * 返回二维码相关接口方法的实现类对象,以方便调用其各个接口
+ *
+ * @return WxMaQrcodeService
+ */
+ WxMaQrcodeService getQrcodeService();
+
+ /**
+ * 初始化http请求对象
+ */
+ void initHttp();
+
+ /**
+ * 请求http请求相关信息
+ */
+ RequestHttp getRequestHttp();
+
+
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaUserService.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaUserService.java
new file mode 100644
index 00000000..84b17378
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/WxMaUserService.java
@@ -0,0 +1,39 @@
+package cn.binarywang.wx.miniapp.api;
+
+import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
+import cn.binarywang.wx.miniapp.bean.WxMaUserInfo;
+import me.chanjar.weixin.common.exception.WxErrorException;
+
+/**
+ * 用户信息相关操作接口
+ *
+ * @author Binary Wang
+ */
+public interface WxMaUserService {
+ String JSCODE_TO_SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session";
+
+ /**
+ * 获取登录后的session信息
+ *
+ * @param jsCode 登录时获取的 code
+ */
+ WxMaJscode2SessionResult getSessionInfo(String jsCode) throws WxErrorException;
+
+ /**
+ * 解密用户敏感数据
+ *
+ * @param sessionKey 会话密钥
+ * @param encryptedData 消息密文
+ * @param ivStr 加密算法的初始向量
+ */
+ WxMaUserInfo getUserInfo(String sessionKey, String encryptedData, String ivStr);
+
+ /**
+ * 验证用户信息完整性
+ *
+ * @param sessionKey 会话密钥
+ * @param rawData 微信用户基本信息
+ * @param signature 数据签名
+ */
+ boolean checkUserInfo(String sessionKey, String rawData, String signature);
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaMediaServiceImpl.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaMediaServiceImpl.java
new file mode 100644
index 00000000..66ad3c2a
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaMediaServiceImpl.java
@@ -0,0 +1,61 @@
+package cn.binarywang.wx.miniapp.api.impl;
+
+import cn.binarywang.wx.miniapp.api.WxMaMediaService;
+import cn.binarywang.wx.miniapp.api.WxMaService;
+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.util.fs.FileUtils;
+import me.chanjar.weixin.common.util.http.MediaDownloadRequestExecutor;
+import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor;
+import me.chanjar.weixin.common.util.http.RequestExecutor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.UUID;
+
+/**
+ * @author Binary Wang
+ */
+public class WxMaMediaServiceImpl implements WxMaMediaService {
+ private final Logger log = LoggerFactory.getLogger(this.getClass());
+
+ private WxMaService wxMaService;
+
+ public WxMaMediaServiceImpl(WxMaService wxMaService) {
+ this.wxMaService = wxMaService;
+ }
+
+ @Override
+ public WxMediaUploadResult uploadMedia(String mediaType, String fileType, InputStream inputStream) throws WxErrorException {
+ try {
+ return this.uploadMedia(mediaType, FileUtils.createTmpFile(inputStream, UUID.randomUUID().toString(), fileType));
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new WxErrorException(WxError.newBuilder().setErrorMsg(e.getMessage()).build());
+ }
+ }
+
+ @Override
+ public WxMediaUploadResult uploadMedia(String mediaType, File file) throws WxErrorException {
+ String url = String.format(MEDIA_UPLOAD_URL, mediaType);
+ return this.wxMaService.execute(MediaUploadRequestExecutor.create(this.wxMaService.getRequestHttp()), url, file);
+ }
+
+ @Override
+ public File getMedia(String mediaId) throws WxErrorException {
+ try {
+ RequestExecutor executor = MediaDownloadRequestExecutor
+ .create(this.wxMaService.getRequestHttp(), Files.createTempDirectory("wxma").toFile());
+ return this.wxMaService.execute(executor, MEDIA_GET_URL, "media_id=" + mediaId);
+ } catch (IOException e) {
+ this.log.error(e.getMessage(), e);
+ throw new WxErrorException(WxError.newBuilder().setErrorMsg(e.getMessage()).build());
+ }
+ }
+
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaMsgServiceImpl.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaMsgServiceImpl.java
new file mode 100644
index 00000000..238bbe0e
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaMsgServiceImpl.java
@@ -0,0 +1,40 @@
+package cn.binarywang.wx.miniapp.api.impl;
+
+import cn.binarywang.wx.miniapp.api.WxMaMsgService;
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.bean.WxMaKefuMessage;
+import cn.binarywang.wx.miniapp.bean.WxMaTemplateMessage;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import me.chanjar.weixin.common.bean.result.WxError;
+import me.chanjar.weixin.common.exception.WxErrorException;
+
+/**
+ * @author Binary Wang
+ */
+public class WxMaMsgServiceImpl implements WxMaMsgService {
+ private static final JsonParser JSON_PARSER = new JsonParser();
+ private WxMaService wxMaService;
+
+ public WxMaMsgServiceImpl(WxMaService wxMaService) {
+ this.wxMaService = wxMaService;
+ }
+
+ @Override
+ public boolean sendKefuMsg(WxMaKefuMessage message) throws WxErrorException {
+ String responseContent = this.wxMaService.post(KEFU_MESSAGE_SEND_URL, message.toJson());
+ return responseContent != null;
+ }
+
+ @Override
+ public String sendTemplateMsg(WxMaTemplateMessage templateMessage) throws WxErrorException {
+ String responseContent = this.wxMaService.post(TEMPLATE_MSG_SEND_URL, templateMessage.toJson());
+ JsonObject jsonObject = JSON_PARSER.parse(responseContent).getAsJsonObject();
+ if (jsonObject.get("errcode").getAsInt() == 0) {
+ return jsonObject.get("msgid").getAsString();
+ }
+
+ throw new WxErrorException(WxError.fromJson(responseContent));
+ }
+
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaQrcodeServiceImpl.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaQrcodeServiceImpl.java
new file mode 100644
index 00000000..f0fc36af
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaQrcodeServiceImpl.java
@@ -0,0 +1,28 @@
+package cn.binarywang.wx.miniapp.api.impl;
+
+import cn.binarywang.wx.miniapp.api.WxMaQrcodeService;
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.bean.WxMaQrcode;
+import cn.binarywang.wx.miniapp.util.http.QrCodeRequestExecutor;
+import me.chanjar.weixin.common.exception.WxErrorException;
+
+import java.io.File;
+
+/**
+ * @author Binary Wang
+ */
+public class WxMaQrcodeServiceImpl implements WxMaQrcodeService {
+ private WxMaService wxMaService;
+
+ public WxMaQrcodeServiceImpl(WxMaService wxMaService) {
+ this.wxMaService = wxMaService;
+ }
+
+ @Override
+ public File createQrcode(String path, int width) throws WxErrorException {
+ String url = "https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode";
+ return this.wxMaService.execute(new QrCodeRequestExecutor(this.wxMaService.getRequestHttp()),
+ url, new WxMaQrcode(path, width));
+ }
+
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaServiceImpl.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaServiceImpl.java
new file mode 100644
index 00000000..16734126
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaServiceImpl.java
@@ -0,0 +1,265 @@
+package cn.binarywang.wx.miniapp.api.impl;
+
+import cn.binarywang.wx.miniapp.api.*;
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import com.google.gson.JsonParser;
+import me.chanjar.weixin.common.bean.WxAccessToken;
+import me.chanjar.weixin.common.bean.result.WxError;
+import me.chanjar.weixin.common.exception.WxErrorException;
+import me.chanjar.weixin.common.util.crypto.SHA1;
+import me.chanjar.weixin.common.util.http.*;
+import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
+import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder;
+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.IOException;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * @author Binary Wang
+ */
+public class WxMaServiceImpl implements WxMaService, RequestHttp {
+ private static final JsonParser JSON_PARSER = new JsonParser();
+ private final Logger log = LoggerFactory.getLogger(this.getClass());
+
+ private CloseableHttpClient httpClient;
+ private HttpHost httpProxy;
+ private WxMaConfig wxMaConfig;
+
+ private WxMaMsgService kefuService = new WxMaMsgServiceImpl(this);
+ private WxMaMediaService materialService = new WxMaMediaServiceImpl(this);
+ private WxMaUserService userService = new WxMaUserServiceImpl(this);
+ private WxMaQrcodeService qrCodeService = new WxMaQrcodeServiceImpl(this);
+
+ private int retrySleepMillis = 1000;
+ private int maxRetryTimes = 5;
+
+ @Override
+ public CloseableHttpClient getRequestHttpClient() {
+ return httpClient;
+ }
+
+ @Override
+ public HttpHost getRequestHttpProxy() {
+ return httpProxy;
+ }
+
+ @Override
+ public HttpType getRequestType() {
+ return HttpType.APACHE_HTTP;
+ }
+
+ @Override
+ public void initHttp() {
+ WxMaConfig configStorage = this.getWxMaConfig();
+ ApacheHttpClientBuilder apacheHttpClientBuilder = configStorage.getApacheHttpClientBuilder();
+ if (null == apacheHttpClientBuilder) {
+ apacheHttpClientBuilder = DefaultApacheHttpClientBuilder.get();
+ }
+
+ apacheHttpClientBuilder.httpProxyHost(configStorage.getHttpProxyHost())
+ .httpProxyPort(configStorage.getHttpProxyPort())
+ .httpProxyUsername(configStorage.getHttpProxyUsername())
+ .httpProxyPassword(configStorage.getHttpProxyPassword());
+
+ if (configStorage.getHttpProxyHost() != null && configStorage.getHttpProxyPort() > 0) {
+ this.httpProxy = new HttpHost(configStorage.getHttpProxyHost(), configStorage.getHttpProxyPort());
+ }
+
+ this.httpClient = apacheHttpClientBuilder.build();
+ }
+
+ @Override
+ public RequestHttp getRequestHttp() {
+ return this;
+ }
+
+ @Override
+ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
+ Lock lock = this.getWxMaConfig().getAccessTokenLock();
+ try {
+ lock.lock();
+
+ if (forceRefresh) {
+ this.getWxMaConfig().expireAccessToken();
+ }
+
+ if (this.getWxMaConfig().isAccessTokenExpired()) {
+ String url = String.format(WxMaService.GET_ACCESS_TOKEN_URL, this.getWxMaConfig().getAppid(),
+ this.getWxMaConfig().getSecret());
+ try {
+ HttpGet httpGet = new HttpGet(url);
+ if (this.getRequestHttpProxy() != null) {
+ RequestConfig config = RequestConfig.custom().setProxy(this.getRequestHttpProxy()).build();
+ httpGet.setConfig(config);
+ }
+ try (CloseableHttpResponse response = getRequestHttpClient().execute(httpGet)) {
+ String resultContent = new BasicResponseHandler().handleResponse(response);
+ WxError error = WxError.fromJson(resultContent);
+ if (error.getErrorCode() != 0) {
+ throw new WxErrorException(error);
+ }
+ WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+ this.getWxMaConfig().updateAccessToken(accessToken.getAccessToken(),
+ accessToken.getExpiresIn());
+ } finally {
+ httpGet.releaseConnection();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ } finally {
+ lock.unlock();
+ }
+
+ return this.getWxMaConfig().getAccessToken();
+ }
+
+ @Override
+ public boolean checkSignature(String timestamp, String nonce, String signature) {
+ try {
+ return SHA1.gen(this.getWxMaConfig().getToken(), timestamp, nonce).equals(signature);
+ } catch (Exception e) {
+ this.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 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过期时才去刷新,然后重新执行请求,而不是全局定时请求
+ */
+ 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) {
+ this.log.warn("重试达到最大次数【{}】", maxRetryTimes);
+ //最后一次重试失败后,直接抛出异常,不再等待
+ throw new RuntimeException("微信服务端异常,超出重试次数");
+ }
+
+ WxError error = e.getError();
+ // -1 系统繁忙, 1000ms后重试
+ if (error.getErrorCode() == -1) {
+ int sleepMillis = this.retrySleepMillis * (1 << retryTimes);
+ try {
+ this.log.warn("微信系统繁忙,{} 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("微信服务端异常,超出重试次数");
+ }
+
+ public 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 + (uri.contains("?") ? "&" : "?") + "access_token=" + accessToken;
+
+ try {
+ T result = executor.execute(uriWithAccessToken, data);
+ this.log.debug("\n[URL]: {}\n[PARAMS]: {}\n[RESPONSE]: {}", uriWithAccessToken, data, result);
+ return result;
+ } catch (WxErrorException e) {
+ WxError error = e.getError();
+ /*
+ * 发生以下情况时尝试刷新access_token
+ * 40001 获取access_token时AppSecret错误,或者access_token无效
+ * 42001 access_token超时
+ * 40014 不合法的access_token,请开发者认真比对access_token的有效性(如是否过期)
+ */
+ if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001 || error.getErrorCode() == 40014) {
+ // 强制设置wxMpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token
+ this.getWxMaConfig().expireAccessToken();
+ if (this.getWxMaConfig().autoRefreshToken()) {
+ return this.execute(executor, uri, data);
+ }
+ }
+
+ if (error.getErrorCode() != 0) {
+ this.log.error("\n[URL]: {}\n[PARAMS]: {}\n[RESPONSE]: {}", uriWithAccessToken, data, error);
+ throw new WxErrorException(error);
+ }
+ return null;
+ } catch (IOException e) {
+ this.log.error("\n[URL]: {}\n[PARAMS]: {}\n[EXCEPTION]: {}", uriWithAccessToken, data, e.getMessage());
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public WxMaConfig getWxMaConfig() {
+ return this.wxMaConfig;
+ }
+
+ @Override
+ public void setWxMaConfig(WxMaConfig wxConfigProvider) {
+ this.wxMaConfig = wxConfigProvider;
+ this.initHttp();
+ }
+
+ @Override
+ public void setRetrySleepMillis(int retrySleepMillis) {
+ this.retrySleepMillis = retrySleepMillis;
+ }
+
+ @Override
+ public void setMaxRetryTimes(int maxRetryTimes) {
+ this.maxRetryTimes = maxRetryTimes;
+ }
+
+ @Override
+ public WxMaMsgService getMsgService() {
+ return this.kefuService;
+ }
+
+ @Override
+ public WxMaMediaService getMediaService() {
+ return this.materialService;
+ }
+
+ @Override
+ public WxMaUserService getUserService() {
+ return this.userService;
+ }
+
+ @Override
+ public WxMaQrcodeService getQrcodeService() {
+ return this.qrCodeService;
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaUserServiceImpl.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaUserServiceImpl.java
new file mode 100644
index 00000000..e2fdb9dc
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/api/impl/WxMaUserServiceImpl.java
@@ -0,0 +1,51 @@
+package cn.binarywang.wx.miniapp.api.impl;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.api.WxMaUserService;
+import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
+import cn.binarywang.wx.miniapp.bean.WxMaUserInfo;
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import cn.binarywang.wx.miniapp.util.crypt.WxMaCryptUtils;
+import com.google.common.base.Joiner;
+import me.chanjar.weixin.common.exception.WxErrorException;
+import org.apache.commons.codec.digest.DigestUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Binary Wang
+ */
+public class WxMaUserServiceImpl implements WxMaUserService {
+ private WxMaService service;
+
+ WxMaUserServiceImpl(WxMaService service) {
+ this.service = service;
+ }
+
+ @Override
+ public WxMaJscode2SessionResult getSessionInfo(String jsCode) throws WxErrorException {
+ final WxMaConfig config = service.getWxMaConfig();
+ Map params = new HashMap<>();
+ params.put("appid", config.getAppid());
+ params.put("secret", config.getSecret());
+ params.put("js_code", jsCode);
+ params.put("grant_type", "authorization_code");
+
+ String result = this.service.get(JSCODE_TO_SESSION_URL, Joiner.on("&").withKeyValueSeparator("=").join(params));
+ return WxMaJscode2SessionResult.fromJson(result);
+ }
+
+ @Override
+ public WxMaUserInfo getUserInfo(String sessionKey, String encryptedData, String ivStr) {
+ return WxMaUserInfo.fromJson(WxMaCryptUtils.decrypt(sessionKey, encryptedData, ivStr));
+ }
+
+ @Override
+ public boolean checkUserInfo(String sessionKey, String rawData, String signature) {
+ final String generatedSignature = DigestUtils.sha1Hex(rawData + sessionKey);
+ System.out.println(generatedSignature);
+ return generatedSignature.equals(signature);
+ }
+
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaJscode2SessionResult.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaJscode2SessionResult.java
new file mode 100644
index 00000000..4b9cc93d
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaJscode2SessionResult.java
@@ -0,0 +1,49 @@
+package cn.binarywang.wx.miniapp.bean;
+
+import cn.binarywang.wx.miniapp.util.json.WxMaGsonBuilder;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * {"session_key":"nzoqhc3OnwHzeTxJs+inbQ==","expires_in":2592000,"openid":"oVBkZ0aYgDMDIywRdgPW8-joxXc4"}
+ *
+ * @author Binary Wang
+ */
+public class WxMaJscode2SessionResult {
+ @SerializedName("session_key")
+ private String sessionKey;
+
+ @SerializedName("expires_in")
+ private Integer expiresin;
+
+ @SerializedName("openid")
+ private String openid;
+
+ public String getSessionKey() {
+ return sessionKey;
+ }
+
+ public void setSessionKey(String sessionKey) {
+ this.sessionKey = sessionKey;
+ }
+
+ public Integer getExpiresin() {
+ return expiresin;
+ }
+
+ public void setExpiresin(Integer expiresin) {
+ this.expiresin = expiresin;
+ }
+
+ public String getOpenid() {
+ return openid;
+ }
+
+ public void setOpenid(String openid) {
+ this.openid = openid;
+ }
+
+ public static WxMaJscode2SessionResult fromJson(String json) {
+ return WxMaGsonBuilder.create().fromJson(json, WxMaJscode2SessionResult.class);
+ }
+
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaKefuMessage.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaKefuMessage.java
new file mode 100644
index 00000000..d75fbf12
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaKefuMessage.java
@@ -0,0 +1,99 @@
+package cn.binarywang.wx.miniapp.bean;
+
+import cn.binarywang.wx.miniapp.builder.ImageBuilder;
+import cn.binarywang.wx.miniapp.builder.TextBuilder;
+import cn.binarywang.wx.miniapp.util.json.WxMaGsonBuilder;
+
+import java.io.Serializable;
+
+/**
+ * 客服消息
+ *
+ * @author Binary Wang
+ */
+public class WxMaKefuMessage implements Serializable {
+ private static final long serialVersionUID = -9196732086954365246L;
+
+ private String toUser;
+ private String msgType;
+ private String content;
+ private String mediaId;
+ private String thumbMediaId;
+ private String title;
+ private String description;
+
+ /**
+ * 获得文本消息builder
+ */
+ public static TextBuilder TEXT() {
+ return new TextBuilder();
+ }
+
+ /**
+ * 获得图片消息builder
+ */
+ public static ImageBuilder IMAGE() {
+ return new ImageBuilder();
+ }
+
+ public String getToUser() {
+ return this.toUser;
+ }
+
+ public void setToUser(String toUser) {
+ this.toUser = toUser;
+ }
+
+ public String getMsgType() {
+ return this.msgType;
+ }
+
+ public void setMsgType(String msgType) {
+ this.msgType = msgType;
+ }
+
+ public String getContent() {
+ return this.content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ public String getMediaId() {
+ return this.mediaId;
+ }
+
+ public void setMediaId(String mediaId) {
+ this.mediaId = mediaId;
+ }
+
+ public String getThumbMediaId() {
+ return this.thumbMediaId;
+ }
+
+ public void setThumbMediaId(String thumbMediaId) {
+ this.thumbMediaId = thumbMediaId;
+ }
+
+ 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 toJson() {
+ return WxMaGsonBuilder.create().toJson(this);
+ }
+
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaMessage.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaMessage.java
new file mode 100644
index 00000000..4b201da1
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaMessage.java
@@ -0,0 +1,236 @@
+package cn.binarywang.wx.miniapp.bean;
+
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import cn.binarywang.wx.miniapp.util.crypt.WxMaCryptUtils;
+import cn.binarywang.wx.miniapp.util.json.WxMaGsonBuilder;
+import cn.binarywang.wx.miniapp.util.xml.XStreamTransformer;
+import com.google.gson.annotations.SerializedName;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamConverter;
+import me.chanjar.weixin.common.util.ToStringUtils;
+import me.chanjar.weixin.common.util.xml.XStreamCDataConverter;
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Serializable;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * @author Binary Wang
+ */
+@XStreamAlias("xml")
+public class WxMaMessage implements Serializable {
+ private static final long serialVersionUID = -3586245291677274914L;
+
+ @SerializedName("Encrypt")
+ @XStreamAlias("Encrypt")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private String encrypt;
+
+ @SerializedName("ToUserName")
+ @XStreamAlias("ToUserName")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private String toUser;
+
+ @SerializedName("FromUserName")
+ @XStreamAlias("FromUserName")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private String fromUser;
+
+ @SerializedName("CreateTime")
+ @XStreamAlias("CreateTime")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private Integer createTime;
+
+ @SerializedName("MsgDataFormat")
+ @XStreamAlias("MsgDataFormat")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private String msgType;
+
+ // 文本消息
+ @SerializedName("Content")
+ @XStreamAlias("Content")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private String content;
+
+ @SerializedName("MsgId")
+ @XStreamAlias("MsgId")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private Long msgId;
+
+ // 图片消息
+ @SerializedName("PicUrl")
+ @XStreamAlias("PicUrl")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private String picUrl;
+
+ @SerializedName("MediaId")
+ @XStreamAlias("MediaId")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private String mediaId;
+
+ // 事件消息
+ @SerializedName("Event")
+ @XStreamAlias("Event")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private String event;
+
+ @SerializedName("SessionFrom")
+ @XStreamAlias("SessionFrom")
+ @XStreamConverter(value = XStreamCDataConverter.class)
+ private String sessionFrom;
+
+ public static WxMaMessage fromXml(String xml) {
+ return XStreamTransformer.fromXml(WxMaMessage.class, xml);
+ }
+
+ public static WxMaMessage fromXml(InputStream is) {
+ return XStreamTransformer.fromXml(WxMaMessage.class, is);
+ }
+
+ /**
+ * 从加密字符串转换
+ *
+ * @param encryptedXml 密文
+ * @param wxMaConfig 配置存储器对象
+ * @param timestamp 时间戳
+ * @param nonce 随机串
+ * @param msgSignature 签名串
+ */
+ public static WxMaMessage fromEncryptedXml(String encryptedXml,
+ WxMaConfig wxMaConfig, String timestamp, String nonce,
+ String msgSignature) {
+ String plainText = new WxMaCryptUtils(wxMaConfig).decrypt(msgSignature, timestamp, nonce, encryptedXml);
+ return fromXml(plainText);
+ }
+
+ public static WxMaMessage fromEncryptedXml(InputStream is, WxMaConfig wxMaConfig, String timestamp,
+ String nonce, String msgSignature) {
+ try {
+ return fromEncryptedXml(IOUtils.toString(is, StandardCharsets.UTF_8), wxMaConfig,
+ timestamp, nonce, msgSignature);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static WxMaMessage fromJson(String json) {
+ return WxMaGsonBuilder.create().fromJson(json, WxMaMessage.class);
+ }
+
+ public static WxMaMessage fromEncryptedJson(String encryptedJson, WxMaConfig config) {
+ try {
+ WxMaMessage encryptedMessage = fromJson(encryptedJson);
+ String plainText = new WxMaCryptUtils(config).decrypt(encryptedMessage.getEncrypt());
+ return fromJson(plainText);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static WxMaMessage fromEncryptedJson(InputStream inputStream, WxMaConfig config) {
+ try {
+ return fromEncryptedJson(IOUtils.toString(inputStream, StandardCharsets.UTF_8), config);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return ToStringUtils.toSimpleString(this);
+ }
+
+ public String toJson() {
+ return WxMaGsonBuilder.create().toJson(this);
+ }
+
+ public String getToUser() {
+ return toUser;
+ }
+
+ public void setToUser(String toUser) {
+ this.toUser = toUser;
+ }
+
+ public String getFromUser() {
+ return fromUser;
+ }
+
+ public void setFromUser(String fromUser) {
+ this.fromUser = fromUser;
+ }
+
+ public Integer getCreateTime() {
+ return createTime;
+ }
+
+ public void setCreateTime(Integer createTime) {
+ this.createTime = createTime;
+ }
+
+ public String getMsgType() {
+ return msgType;
+ }
+
+ public void setMsgType(String msgType) {
+ this.msgType = msgType;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ public Long getMsgId() {
+ return msgId;
+ }
+
+ public void setMsgId(Long msgId) {
+ this.msgId = msgId;
+ }
+
+ public String getPicUrl() {
+ return picUrl;
+ }
+
+ public void setPicUrl(String picUrl) {
+ this.picUrl = picUrl;
+ }
+
+ public String getMediaId() {
+ return mediaId;
+ }
+
+ public void setMediaId(String mediaId) {
+ this.mediaId = mediaId;
+ }
+
+ public String getEvent() {
+ return event;
+ }
+
+ public void setEvent(String event) {
+ this.event = event;
+ }
+
+ public String getSessionFrom() {
+ return sessionFrom;
+ }
+
+ public void setSessionFrom(String sessionFrom) {
+ this.sessionFrom = sessionFrom;
+ }
+
+ public String getEncrypt() {
+ return encrypt;
+ }
+
+ public void setEncrypt(String encrypt) {
+ this.encrypt = encrypt;
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaQrcode.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaQrcode.java
new file mode 100644
index 00000000..83050f06
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaQrcode.java
@@ -0,0 +1,44 @@
+package cn.binarywang.wx.miniapp.bean;
+
+import cn.binarywang.wx.miniapp.util.json.WxMaGsonBuilder;
+
+import java.io.Serializable;
+
+/**
+ * @author Binary Wang
+ */
+public class WxMaQrcode implements Serializable {
+ private static final long serialVersionUID = 5777119669111011584L;
+ private String path;
+ private int width = 430;
+
+ public WxMaQrcode(String path, int width) {
+ this.path = path;
+ this.width = width;
+ }
+
+ public static WxMaQrcode fromJson(String json) {
+ return WxMaGsonBuilder.create().fromJson(json, WxMaQrcode.class);
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public void setWidth(int width) {
+ this.width = width;
+ }
+
+ @Override
+ public String toString() {
+ return WxMaGsonBuilder.create().toJson(this);
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaTemplateMessage.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaTemplateMessage.java
new file mode 100644
index 00000000..7a89dfa8
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaTemplateMessage.java
@@ -0,0 +1,214 @@
+package cn.binarywang.wx.miniapp.bean;
+
+import cn.binarywang.wx.miniapp.util.json.WxMaGsonBuilder;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 参考 https://mp.weixin.qq.com/debug/wxadoc/dev/api/notice.html#接口说明 模板消息部分
+ *
+ * @author Binary Wang
+ */
+public class WxMaTemplateMessage implements Serializable {
+ private static final long serialVersionUID = 5063374783759519418L;
+ /**
+ * touser 是 接收者(用户)的 openid
+ */
+ private String toUser;
+ /**
+ * template_id 是 所需下发的模板消息的id
+ */
+ private String templateId;
+ /**
+ *
+ * page 否 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转。
+ *
+ */
+ private String page;
+ /**
+ * form_id 是 表单提交场景下,为 submit 事件带上的 formId;支付场景下,为本次支付的 prepay_id
+ */
+ private String formId;
+ /**
+ * data 是 模板内容,不填则下发空模板
+ */
+ private List data = new ArrayList<>();
+ /**
+ * color 否 模板内容字体的颜色,不填默认黑色
+ */
+ private String color;
+ /**
+ * emphasis_keyword 否 模板需要放大的关键词,不填则默认无放大
+ */
+ private String emphasisKeyword;
+
+ private WxMaTemplateMessage(Builder builder) {
+ setToUser(builder.toUser);
+ setTemplateId(builder.templateId);
+ setPage(builder.page);
+ setFormId(builder.formId);
+ setData(builder.data);
+ setColor(builder.color);
+ setEmphasisKeyword(builder.emphasisKeyword);
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public String toJson() {
+ return WxMaGsonBuilder.create().toJson(this);
+ }
+
+ public String getToUser() {
+ return toUser;
+ }
+
+ public void setToUser(String toUser) {
+ this.toUser = toUser;
+ }
+
+ public String getTemplateId() {
+ return templateId;
+ }
+
+ public void setTemplateId(String templateId) {
+ this.templateId = templateId;
+ }
+
+ public String getPage() {
+ return page;
+ }
+
+ public void setPage(String page) {
+ this.page = page;
+ }
+
+ public String getFormId() {
+ return formId;
+ }
+
+ public void setFormId(String formId) {
+ this.formId = formId;
+ }
+
+ public List getData() {
+ return data;
+ }
+
+ public void setData(List data) {
+ this.data = data;
+ }
+
+ public String getColor() {
+ return color;
+ }
+
+ public void setColor(String color) {
+ this.color = color;
+ }
+
+ public String getEmphasisKeyword() {
+ return emphasisKeyword;
+ }
+
+ public void setEmphasisKeyword(String emphasisKeyword) {
+ this.emphasisKeyword = emphasisKeyword;
+ }
+
+ public static class Data {
+ private String name;
+ private String value;
+ private String color;
+
+ public Data(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public Data(String name, String value, String color) {
+ this.name = name;
+ this.value = value;
+ this.color = color;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getValue() {
+ return this.value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ public String getColor() {
+ return this.color;
+ }
+
+ public void setColor(String color) {
+ this.color = color;
+ }
+
+ }
+
+ public static final class Builder {
+ private String toUser;
+ private String templateId;
+ private String page;
+ private String formId;
+ private List data;
+ private String color;
+ private String emphasisKeyword;
+
+ private Builder() {
+ }
+
+ public Builder toUser(String toUser) {
+ this.toUser = toUser;
+ return this;
+ }
+
+ public Builder templateId(String templateId) {
+ this.templateId = templateId;
+ return this;
+ }
+
+ public Builder page(String page) {
+ this.page = page;
+ return this;
+ }
+
+ public Builder formId(String formId) {
+ this.formId = formId;
+ return this;
+ }
+
+ public Builder data(List data) {
+ this.data = data;
+ return this;
+ }
+
+ public Builder color(String color) {
+ this.color = color;
+ return this;
+ }
+
+ public Builder emphasisKeyword(String emphasisKeyword) {
+ this.emphasisKeyword = emphasisKeyword;
+ return this;
+ }
+
+ public WxMaTemplateMessage build() {
+ return new WxMaTemplateMessage(this);
+ }
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaUserInfo.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaUserInfo.java
new file mode 100644
index 00000000..e3cc69d9
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/bean/WxMaUserInfo.java
@@ -0,0 +1,131 @@
+package cn.binarywang.wx.miniapp.bean;
+
+import cn.binarywang.wx.miniapp.util.json.WxMaGsonBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * @author Binary Wang
+ */
+public class WxMaUserInfo {
+ private String openId;
+ private String nickName;
+ private String gender;
+ private String language;
+ private String city;
+ private String province;
+ private String country;
+ private String avatarUrl;
+ private String unionId;
+ private Watermark watermark;
+
+ public static WxMaUserInfo fromJson(String json) {
+ return WxMaGsonBuilder.create().fromJson(json, WxMaUserInfo.class);
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+ }
+
+ public String getOpenId() {
+ return openId;
+ }
+
+ public void setOpenId(String openId) {
+ this.openId = openId;
+ }
+
+ public String getNickName() {
+ return nickName;
+ }
+
+ public void setNickName(String nickName) {
+ this.nickName = nickName;
+ }
+
+ public String getGender() {
+ return gender;
+ }
+
+ public void setGender(String gender) {
+ this.gender = gender;
+ }
+
+ public String getLanguage() {
+ return language;
+ }
+
+ public void setLanguage(String language) {
+ this.language = language;
+ }
+
+ public String getCity() {
+ return city;
+ }
+
+ public void setCity(String city) {
+ this.city = city;
+ }
+
+ public String getProvince() {
+ return province;
+ }
+
+ public void setProvince(String province) {
+ this.province = province;
+ }
+
+ public String getCountry() {
+ return country;
+ }
+
+ public void setCountry(String country) {
+ this.country = country;
+ }
+
+ public String getAvatarUrl() {
+ return avatarUrl;
+ }
+
+ public void setAvatarUrl(String avatarUrl) {
+ this.avatarUrl = avatarUrl;
+ }
+
+ public String getUnionId() {
+ return unionId;
+ }
+
+ public void setUnionId(String unionId) {
+ this.unionId = unionId;
+ }
+
+ public Watermark getWatermark() {
+ return watermark;
+ }
+
+ public void setWatermark(Watermark watermark) {
+ this.watermark = watermark;
+ }
+
+ public static class Watermark {
+ private String timestamp;
+ private String appid;
+
+ public String getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(String timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public String getAppid() {
+ return appid;
+ }
+
+ public void setAppid(String appid) {
+ this.appid = appid;
+ }
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/builder/BaseBuilder.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/builder/BaseBuilder.java
new file mode 100644
index 00000000..70d7cf4b
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/builder/BaseBuilder.java
@@ -0,0 +1,24 @@
+package cn.binarywang.wx.miniapp.builder;
+
+import cn.binarywang.wx.miniapp.bean.WxMaKefuMessage;
+
+/**
+ * @author Binary Wang
+ */
+public class BaseBuilder {
+ protected String msgType;
+ protected String toUser;
+
+ @SuppressWarnings("unchecked")
+ public T toUser(String toUser) {
+ this.toUser = toUser;
+ return (T) this;
+ }
+
+ public WxMaKefuMessage build() {
+ WxMaKefuMessage m = new WxMaKefuMessage();
+ m.setMsgType(this.msgType);
+ m.setToUser(this.toUser);
+ return m;
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/builder/ImageBuilder.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/builder/ImageBuilder.java
new file mode 100644
index 00000000..219e3fd4
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/builder/ImageBuilder.java
@@ -0,0 +1,27 @@
+package cn.binarywang.wx.miniapp.builder;
+
+import cn.binarywang.wx.miniapp.bean.WxMaKefuMessage;
+import cn.binarywang.wx.miniapp.constant.WxMaConstants;
+
+/**
+ * @author Binary Wang
+ */
+public final class ImageBuilder extends BaseBuilder {
+ private String mediaId;
+
+ public ImageBuilder() {
+ this.msgType = WxMaConstants.KefuMsgType.IMAGE;
+ }
+
+ public ImageBuilder mediaId(String media_id) {
+ this.mediaId = media_id;
+ return this;
+ }
+
+ @Override
+ public WxMaKefuMessage build() {
+ WxMaKefuMessage m = super.build();
+ m.setMediaId(this.mediaId);
+ return m;
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/builder/TextBuilder.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/builder/TextBuilder.java
new file mode 100644
index 00000000..35c58a67
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/builder/TextBuilder.java
@@ -0,0 +1,27 @@
+package cn.binarywang.wx.miniapp.builder;
+
+import cn.binarywang.wx.miniapp.bean.WxMaKefuMessage;
+import cn.binarywang.wx.miniapp.constant.WxMaConstants;
+
+/**
+ * @author Binary Wang
+ */
+public final class TextBuilder extends BaseBuilder {
+ private String content;
+
+ public TextBuilder() {
+ this.msgType = WxMaConstants.KefuMsgType.TEXT;
+ }
+
+ public TextBuilder content(String content) {
+ this.content = content;
+ return this;
+ }
+
+ @Override
+ public WxMaKefuMessage build() {
+ WxMaKefuMessage m = super.build();
+ m.setContent(this.content);
+ return m;
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/config/WxMaConfig.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/config/WxMaConfig.java
new file mode 100644
index 00000000..df72bb2c
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/config/WxMaConfig.java
@@ -0,0 +1,73 @@
+package cn.binarywang.wx.miniapp.config;
+
+import me.chanjar.weixin.common.bean.WxAccessToken;
+import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
+
+import java.util.concurrent.locks.Lock;
+
+/**
+ * 小程序配置
+ *
+ * @author Binary Wang
+ */
+public interface WxMaConfig {
+
+ String getAccessToken();
+
+ Lock getAccessTokenLock();
+
+ boolean isAccessTokenExpired();
+
+ /**
+ * 强制将access token过期掉
+ */
+ void expireAccessToken();
+
+ /**
+ * 应该是线程安全的
+ *
+ * @param accessToken 要更新的WxAccessToken对象
+ */
+ void updateAccessToken(WxAccessToken accessToken);
+
+ /**
+ * 应该是线程安全的
+ *
+ * @param accessToken 新的accessToken值
+ * @param expiresInSeconds 过期时间,以秒为单位
+ */
+ void updateAccessToken(String accessToken, int expiresInSeconds);
+
+ String getAppid();
+
+ String getSecret();
+
+ String getToken();
+
+ String getAesKey();
+
+ String getMsgDataFormat();
+
+ long getExpiresTime();
+
+ String getHttpProxyHost();
+
+ int getHttpProxyPort();
+
+ String getHttpProxyUsername();
+
+ String getHttpProxyPassword();
+
+ /**
+ * http client builder
+ *
+ * @return ApacheHttpClientBuilder
+ */
+ ApacheHttpClientBuilder getApacheHttpClientBuilder();
+
+ /**
+ * 是否自动刷新token
+ */
+ boolean autoRefreshToken();
+
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/config/WxMaInMemoryConfig.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/config/WxMaInMemoryConfig.java
new file mode 100644
index 00000000..735a52b5
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/config/WxMaInMemoryConfig.java
@@ -0,0 +1,185 @@
+package cn.binarywang.wx.miniapp.config;
+
+import me.chanjar.weixin.common.bean.WxAccessToken;
+import me.chanjar.weixin.common.util.ToStringUtils;
+import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
+
+import java.io.File;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化
+ *
+ * @author Binary Wang
+ */
+public class WxMaInMemoryConfig implements WxMaConfig {
+ protected volatile String msgDataFormat;
+ protected volatile String appid;
+ protected volatile String secret;
+ protected volatile String token;
+ protected volatile String accessToken;
+ protected volatile String aesKey;
+ protected volatile long expiresTime;
+
+ protected volatile String httpProxyHost;
+ protected volatile int httpProxyPort;
+ protected volatile String httpProxyUsername;
+ protected volatile String httpProxyPassword;
+
+ protected Lock accessTokenLock = new ReentrantLock();
+
+ /**
+ * 临时文件目录
+ */
+ protected volatile File tmpDirFile;
+
+ protected volatile ApacheHttpClientBuilder apacheHttpClientBuilder;
+
+ @Override
+ public String getAccessToken() {
+ return this.accessToken;
+ }
+
+ public void setAccessToken(String accessToken) {
+ this.accessToken = accessToken;
+ }
+
+ @Override
+ public Lock getAccessTokenLock() {
+ return this.accessTokenLock;
+ }
+
+ public void setAccessTokenLock(Lock accessTokenLock) {
+ this.accessTokenLock = accessTokenLock;
+ }
+
+ @Override
+ public boolean isAccessTokenExpired() {
+ return System.currentTimeMillis() > this.expiresTime;
+ }
+
+ @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 void expireAccessToken() {
+ this.expiresTime = 0;
+ }
+
+ @Override
+ public String getSecret() {
+ return this.secret;
+ }
+
+ public void setSecret(String secret) {
+ this.secret = secret;
+ }
+
+ @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 String getMsgDataFormat() {
+ return this.msgDataFormat;
+ }
+
+ public void setMsgDataFormat(String msgDataFormat) {
+ this.msgDataFormat = msgDataFormat;
+ }
+
+ @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 ApacheHttpClientBuilder getApacheHttpClientBuilder() {
+ return this.apacheHttpClientBuilder;
+ }
+
+ public void setApacheHttpClientBuilder(ApacheHttpClientBuilder apacheHttpClientBuilder) {
+ this.apacheHttpClientBuilder = apacheHttpClientBuilder;
+ }
+
+ @Override
+ public boolean autoRefreshToken() {
+ return true;
+ }
+
+ public String getAppid() {
+ return appid;
+ }
+
+ public void setAppid(String appid) {
+ this.appid = appid;
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaConstants.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaConstants.java
new file mode 100644
index 00000000..2ec71869
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/constant/WxMaConstants.java
@@ -0,0 +1,26 @@
+package cn.binarywang.wx.miniapp.constant;
+
+/**
+ *
+ * 小程序常量
+ *
+ *
+ * @author Binary Wang
+ */
+public class WxMaConstants {
+ /**
+ * 消息格式
+ */
+ public static class MsgDataFormat {
+ public static final String XML = "XML";
+ public static final String JSON = "JSON";
+ }
+
+ /**
+ * 客服消息的消息类型
+ */
+ public static class KefuMsgType {
+ public static final String TEXT = "text";//文本消息
+ public static final String IMAGE = "image";//图片消息
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/message/WxMaMessageHandler.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/message/WxMaMessageHandler.java
new file mode 100644
index 00000000..794d60e9
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/message/WxMaMessageHandler.java
@@ -0,0 +1,20 @@
+package cn.binarywang.wx.miniapp.message;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.bean.WxMaMessage;
+import me.chanjar.weixin.common.exception.WxErrorException;
+import me.chanjar.weixin.common.session.WxSessionManager;
+
+import java.util.Map;
+
+/**
+ * 处理小程序推送消息的处理器接口
+ *
+ * @author Binary Wang
+ */
+public interface WxMaMessageHandler {
+
+ void handle(WxMaMessage message, Map context,
+ WxMaService service, WxSessionManager sessionManager) throws WxErrorException;
+
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/message/WxMaMessageInterceptor.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/message/WxMaMessageInterceptor.java
new file mode 100644
index 00000000..1b220a0b
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/message/WxMaMessageInterceptor.java
@@ -0,0 +1,28 @@
+package cn.binarywang.wx.miniapp.message;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.bean.WxMaMessage;
+import me.chanjar.weixin.common.exception.WxErrorException;
+import me.chanjar.weixin.common.session.WxSessionManager;
+
+import java.util.Map;
+
+/**
+ * 微信消息拦截器,可以用来做验证
+ *
+ * @author Binary Wang
+ */
+public interface WxMaMessageInterceptor {
+
+ /**
+ * 拦截微信消息
+ *
+ * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个
+ * @return true代表OK,false代表不OK
+ */
+ boolean intercept(WxMaMessage wxMessage,
+ Map context,
+ WxMaService wxMaService,
+ WxSessionManager sessionManager) throws WxErrorException;
+
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/message/WxMaMessageMatcher.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/message/WxMaMessageMatcher.java
new file mode 100644
index 00000000..9d4e54f8
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/message/WxMaMessageMatcher.java
@@ -0,0 +1,17 @@
+package cn.binarywang.wx.miniapp.message;
+
+import cn.binarywang.wx.miniapp.bean.WxMaMessage;
+
+/**
+ * 消息匹配器,用在消息路由的时候
+ *
+ * @author Binary Wang
+ */
+public interface WxMaMessageMatcher {
+
+ /**
+ * 消息是否匹配某种模式
+ */
+ boolean match(WxMaMessage message);
+
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/message/WxMaMessageRouter.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/message/WxMaMessageRouter.java
new file mode 100644
index 00000000..cbe42b1f
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/message/WxMaMessageRouter.java
@@ -0,0 +1,176 @@
+package cn.binarywang.wx.miniapp.message;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.bean.WxMaMessage;
+import me.chanjar.weixin.common.api.WxErrorExceptionHandler;
+import me.chanjar.weixin.common.api.WxMessageDuplicateChecker;
+import me.chanjar.weixin.common.api.WxMessageInMemoryDuplicateChecker;
+import me.chanjar.weixin.common.session.InternalSession;
+import me.chanjar.weixin.common.session.InternalSessionManager;
+import me.chanjar.weixin.common.session.StandardSessionManager;
+import me.chanjar.weixin.common.session.WxSessionManager;
+import me.chanjar.weixin.common.util.LogExceptionHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+/**
+ * @author Binary Wang
+ */
+public class WxMaMessageRouter {
+ private static final int DEFAULT_THREAD_POOL_SIZE = 100;
+ private final Logger log = LoggerFactory.getLogger(WxMaMessageRouter.class);
+ private final List rules = new ArrayList<>();
+
+ private final WxMaService wxMaService;
+
+ private ExecutorService executorService;
+
+ private WxMessageDuplicateChecker messageDuplicateChecker;
+
+ private WxSessionManager sessionManager;
+
+ private WxErrorExceptionHandler exceptionHandler;
+
+ public WxMaMessageRouter(WxMaService wxMaService) {
+ this.wxMaService = wxMaService;
+ this.executorService = Executors.newFixedThreadPool(DEFAULT_THREAD_POOL_SIZE);
+ this.messageDuplicateChecker = new WxMessageInMemoryDuplicateChecker();
+ this.sessionManager = new StandardSessionManager();
+ this.exceptionHandler = new LogExceptionHandler();
+ }
+
+ /**
+ *
+ * 设置自定义的 {@link ExecutorService}
+ * 如果不调用该方法,默认使用 Executors.newFixedThreadPool(100)
+ *
+ */
+ public void setExecutorService(ExecutorService executorService) {
+ this.executorService = executorService;
+ }
+
+ /**
+ *
+ * 设置自定义的 {@link me.chanjar.weixin.common.api.WxMessageDuplicateChecker}
+ * 如果不调用该方法,默认使用 {@link me.chanjar.weixin.common.api.WxMessageInMemoryDuplicateChecker}
+ *
+ */
+ public void setMessageDuplicateChecker(WxMessageDuplicateChecker messageDuplicateChecker) {
+ this.messageDuplicateChecker = messageDuplicateChecker;
+ }
+
+ /**
+ *
+ * 设置自定义的{@link me.chanjar.weixin.common.session.WxSessionManager}
+ * 如果不调用该方法,默认使用 {@link me.chanjar.weixin.common.session.StandardSessionManager}
+ *
+ */
+ public void setSessionManager(WxSessionManager sessionManager) {
+ this.sessionManager = sessionManager;
+ }
+
+ /**
+ *
+ * 设置自定义的{@link me.chanjar.weixin.common.api.WxErrorExceptionHandler}
+ * 如果不调用该方法,默认使用 {@link me.chanjar.weixin.common.util.LogExceptionHandler}
+ *
+ */
+ public void setExceptionHandler(WxErrorExceptionHandler exceptionHandler) {
+ this.exceptionHandler = exceptionHandler;
+ }
+
+ List getRules() {
+ return this.rules;
+ }
+
+ /**
+ * 开始一个新的Route规则
+ */
+ public WxMaMessageRouterRule rule() {
+ return new WxMaMessageRouterRule(this);
+ }
+
+ /**
+ * 处理微信消息
+ */
+ public void route(final WxMaMessage wxMessage, final Map context) {
+ final List matchRules = new ArrayList<>();
+ // 收集匹配的规则
+ for (final WxMaMessageRouterRule rule : this.rules) {
+ if (rule.test(wxMessage)) {
+ matchRules.add(rule);
+ if (!rule.isReEnter()) {
+ break;
+ }
+ }
+ }
+
+ if (matchRules.size() == 0) {
+ return;
+ }
+
+ final List> futures = new ArrayList<>();
+ for (final WxMaMessageRouterRule rule : matchRules) {
+ // 返回最后一个非异步的rule的执行结果
+ if (rule.isAsync()) {
+ futures.add(
+ this.executorService.submit(new Runnable() {
+ @Override
+ public void run() {
+ rule.service(wxMessage, context, WxMaMessageRouter.this.wxMaService, WxMaMessageRouter.this.sessionManager, WxMaMessageRouter.this.exceptionHandler);
+ }
+ })
+ );
+ } else {
+ rule.service(wxMessage, context, this.wxMaService, this.sessionManager, this.exceptionHandler);
+ // 在同步操作结束,session访问结束
+ this.log.debug("End session access: async=false, sessionId={}", wxMessage.getFromUser());
+ sessionEndAccess(wxMessage);
+ }
+ }
+
+ if (futures.size() > 0) {
+ this.executorService.submit(new Runnable() {
+ @Override
+ public void run() {
+ for (Future> future : futures) {
+ try {
+ future.get();
+ WxMaMessageRouter.this.log.debug("End session access: async=true, sessionId={}", wxMessage.getFromUser());
+ // 异步操作结束,session访问结束
+ sessionEndAccess(wxMessage);
+ } catch (InterruptedException | ExecutionException e) {
+ WxMaMessageRouter.this.log.error("Error happened when wait task finish", e);
+ }
+ }
+ }
+ });
+ }
+
+ }
+
+ public void route(final WxMaMessage wxMessage) {
+ this.route(wxMessage, new HashMap());
+ }
+
+ /**
+ * 对session的访问结束
+ */
+ protected void sessionEndAccess(WxMaMessage wxMessage) {
+
+ InternalSession session = ((InternalSessionManager) this.sessionManager).findSession(wxMessage.getFromUser());
+ if (session != null) {
+ session.endAccess();
+ }
+
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/message/WxMaMessageRouterRule.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/message/WxMaMessageRouterRule.java
new file mode 100644
index 00000000..9fce8b1a
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/message/WxMaMessageRouterRule.java
@@ -0,0 +1,316 @@
+package cn.binarywang.wx.miniapp.message;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.bean.WxMaMessage;
+import me.chanjar.weixin.common.api.WxErrorExceptionHandler;
+import me.chanjar.weixin.common.exception.WxErrorException;
+import me.chanjar.weixin.common.session.WxSessionManager;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * @author Binary Wang
+ */
+public class WxMaMessageRouterRule {
+
+ private final WxMaMessageRouter routerBuilder;
+
+ private boolean async = true;
+
+ private String fromUser;
+
+ private String msgType;
+
+ private String event;
+
+ private String eventKey;
+
+ private String content;
+
+ private String rContent;
+
+ private WxMaMessageMatcher matcher;
+
+ private boolean reEnter = false;
+
+ private List handlers = new ArrayList<>();
+
+ private List interceptors = new ArrayList<>();
+
+ public WxMaMessageRouterRule(WxMaMessageRouter routerBuilder) {
+ this.routerBuilder = routerBuilder;
+ }
+
+ /**
+ * 设置是否异步执行,默认是true
+ */
+ public WxMaMessageRouterRule async(boolean async) {
+ this.async = async;
+ return this;
+ }
+
+ /**
+ * 如果msgType等于某值
+ */
+ public WxMaMessageRouterRule msgType(String msgType) {
+ this.msgType = msgType;
+ return this;
+ }
+
+ /**
+ * 如果event等于某值
+ */
+ public WxMaMessageRouterRule event(String event) {
+ this.event = event;
+ return this;
+ }
+
+ /**
+ * 如果eventKey等于某值
+ */
+ public WxMaMessageRouterRule eventKey(String eventKey) {
+ this.eventKey = eventKey;
+ return this;
+ }
+
+ /**
+ * 如果content等于某值
+ */
+ public WxMaMessageRouterRule content(String content) {
+ this.content = content;
+ return this;
+ }
+
+ /**
+ * 如果content匹配该正则表达式
+ */
+ public WxMaMessageRouterRule rContent(String regex) {
+ this.rContent = regex;
+ return this;
+ }
+
+ /**
+ * 如果fromUser等于某值
+ */
+ public WxMaMessageRouterRule fromUser(String fromUser) {
+ this.fromUser = fromUser;
+ return this;
+ }
+
+ /**
+ * 如果消息匹配某个matcher,用在用户需要自定义更复杂的匹配规则的时候
+ */
+ public WxMaMessageRouterRule matcher(WxMaMessageMatcher matcher) {
+ this.matcher = matcher;
+ return this;
+ }
+
+ /**
+ * 设置微信消息拦截器
+ */
+ public WxMaMessageRouterRule interceptor(WxMaMessageInterceptor interceptor) {
+ return interceptor(interceptor, (WxMaMessageInterceptor[]) null);
+ }
+
+ /**
+ * 设置微信消息拦截器
+ */
+ public WxMaMessageRouterRule interceptor(WxMaMessageInterceptor interceptor, WxMaMessageInterceptor... otherInterceptors) {
+ this.interceptors.add(interceptor);
+ if (otherInterceptors != null && otherInterceptors.length > 0) {
+ for (WxMaMessageInterceptor i : otherInterceptors) {
+ this.interceptors.add(i);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * 设置微信消息处理器
+ */
+ public WxMaMessageRouterRule handler(WxMaMessageHandler handler) {
+ return handler(handler, (WxMaMessageHandler[]) null);
+ }
+
+ /**
+ * 设置微信消息处理器
+ */
+ public WxMaMessageRouterRule handler(WxMaMessageHandler handler, WxMaMessageHandler... otherHandlers) {
+ this.handlers.add(handler);
+ if (otherHandlers != null && otherHandlers.length > 0) {
+ for (WxMaMessageHandler i : otherHandlers) {
+ this.handlers.add(i);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * 规则结束,代表如果一个消息匹配该规则,那么它将不再会进入其他规则
+ */
+ public WxMaMessageRouter end() {
+ this.routerBuilder.getRules().add(this);
+ return this.routerBuilder;
+ }
+
+ /**
+ * 规则结束,但是消息还会进入其他规则
+ */
+ public WxMaMessageRouter next() {
+ this.reEnter = true;
+ return end();
+ }
+
+ /**
+ * 将微信自定义的事件修正为不区分大小写,
+ * 比如框架定义的事件常量为click,但微信传递过来的却是CLICK
+ */
+ protected boolean test(WxMaMessage wxMessage) {
+ return
+ (this.fromUser == null || this.fromUser.equals(wxMessage.getFromUser()))
+ &&
+ (this.msgType == null || this.msgType.toLowerCase().equals((wxMessage.getMsgType() == null ? null : wxMessage.getMsgType().toLowerCase())))
+ &&
+ (this.event == null || this.event.toLowerCase().equals((wxMessage.getEvent() == null ? null : wxMessage.getEvent().toLowerCase())))
+ &&
+ (this.content == null || this.content
+ .equals(wxMessage.getContent() == null ? null : wxMessage.getContent().trim()))
+ &&
+ (this.rContent == null || Pattern
+ .matches(this.rContent, wxMessage.getContent() == null ? "" : wxMessage.getContent().trim()))
+ &&
+ (this.matcher == null || this.matcher.match(wxMessage))
+ ;
+ }
+
+ /**
+ * 处理微信推送过来的消息
+ *
+ * @return true 代表继续执行别的router,false 代表停止执行别的router
+ */
+ protected void service(WxMaMessage wxMessage,
+ Map context,
+ WxMaService wxMaService,
+ WxSessionManager sessionManager,
+ WxErrorExceptionHandler exceptionHandler) {
+ if (context == null) {
+ context = new HashMap<>();
+ }
+
+ try {
+ // 如果拦截器不通过
+ for (WxMaMessageInterceptor interceptor : this.interceptors) {
+ if (!interceptor.intercept(wxMessage, context, wxMaService, sessionManager)) {
+ return;
+ }
+ }
+
+ // 交给handler处理
+ for (WxMaMessageHandler handler : this.handlers) {
+ // 返回最后handler的结果
+ if (handler == null) {
+ continue;
+ }
+ handler.handle(wxMessage, context, wxMaService, sessionManager);
+ }
+ } catch (WxErrorException e) {
+ exceptionHandler.handle(e);
+ }
+ }
+
+ public WxMaMessageRouter getRouterBuilder() {
+ return this.routerBuilder;
+ }
+
+ public boolean isAsync() {
+ return this.async;
+ }
+
+ public void setAsync(boolean async) {
+ this.async = async;
+ }
+
+ public String getFromUser() {
+ return this.fromUser;
+ }
+
+ public void setFromUser(String fromUser) {
+ this.fromUser = fromUser;
+ }
+
+ public String getMsgType() {
+ return this.msgType;
+ }
+
+ public void setMsgType(String msgType) {
+ this.msgType = msgType;
+ }
+
+ public String getEvent() {
+ return this.event;
+ }
+
+ public void setEvent(String event) {
+ this.event = event;
+ }
+
+ public String getEventKey() {
+ return this.eventKey;
+ }
+
+ public void setEventKey(String eventKey) {
+ this.eventKey = eventKey;
+ }
+
+ public String getContent() {
+ return this.content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ public String getrContent() {
+ return this.rContent;
+ }
+
+ public void setrContent(String rContent) {
+ this.rContent = rContent;
+ }
+
+ public WxMaMessageMatcher getMatcher() {
+ return this.matcher;
+ }
+
+ public void setMatcher(WxMaMessageMatcher matcher) {
+ this.matcher = matcher;
+ }
+
+ public boolean isReEnter() {
+ return this.reEnter;
+ }
+
+ public void setReEnter(boolean reEnter) {
+ this.reEnter = reEnter;
+ }
+
+ public List getHandlers() {
+ return this.handlers;
+ }
+
+ public void setHandlers(List handlers) {
+ this.handlers = handlers;
+ }
+
+ public List getInterceptors() {
+ return this.interceptors;
+ }
+
+ public void setInterceptors(List interceptors) {
+ this.interceptors = interceptors;
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtils.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtils.java
new file mode 100644
index 00000000..e05bac5f
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/crypt/WxMaCryptUtils.java
@@ -0,0 +1,43 @@
+package cn.binarywang.wx.miniapp.util.crypt;
+
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import me.chanjar.weixin.common.util.crypto.PKCS7Encoder;
+import org.apache.commons.codec.binary.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.AlgorithmParameters;
+
+/**
+ * @author Binary Wang
+ */
+public class WxMaCryptUtils extends me.chanjar.weixin.common.util.crypto.WxCryptUtil {
+ public WxMaCryptUtils(WxMaConfig config) {
+ this.appidOrCorpid = config.getAppid();
+ this.token = config.getToken();
+ this.aesKey = Base64.decodeBase64(config.getAesKey() + "=");
+ }
+
+ /**
+ * AES解密
+ *
+ * @param encryptedData 消息密文
+ * @param ivStr iv字符串
+ */
+ public static String decrypt(String sessionKey, String encryptedData, String ivStr) {
+ try {
+ AlgorithmParameters params = AlgorithmParameters.getInstance("AES");
+ params.init(new IvParameterSpec(Base64.decodeBase64(ivStr)));
+
+ Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+ cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(Base64.decodeBase64(sessionKey), "AES"), params);
+
+ return new String(PKCS7Encoder.decode(cipher.doFinal(Base64.decodeBase64(encryptedData))), StandardCharsets.UTF_8);
+ } catch (Exception e) {
+ throw new RuntimeException("AES解密失败", e);
+ }
+ }
+
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/http/QrCodeRequestExecutor.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/http/QrCodeRequestExecutor.java
new file mode 100644
index 00000000..7883ffda
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/http/QrCodeRequestExecutor.java
@@ -0,0 +1,61 @@
+package cn.binarywang.wx.miniapp.util.http;
+
+import cn.binarywang.wx.miniapp.bean.WxMaQrcode;
+import me.chanjar.weixin.common.bean.result.WxError;
+import me.chanjar.weixin.common.exception.WxErrorException;
+import me.chanjar.weixin.common.util.fs.FileUtils;
+import me.chanjar.weixin.common.util.http.RequestExecutor;
+import me.chanjar.weixin.common.util.http.RequestHttp;
+import me.chanjar.weixin.common.util.http.apache.InputStreamResponseHandler;
+import me.chanjar.weixin.common.util.http.apache.Utf8ResponseHandler;
+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.HttpPost;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.UUID;
+
+/**
+ * @author Binary Wang
+ */
+public class QrCodeRequestExecutor implements RequestExecutor {
+ protected RequestHttp requestHttp;
+
+ public QrCodeRequestExecutor(RequestHttp requestHttp) {
+ this.requestHttp = requestHttp;
+ }
+
+ @Override
+ public File execute(String uri, WxMaQrcode ticket) throws WxErrorException, IOException {
+ HttpPost httpPost = new HttpPost(uri);
+ if (requestHttp.getRequestHttpProxy() != null) {
+ httpPost
+ .setConfig(RequestConfig.custom()
+ .setProxy(requestHttp.getRequestHttpProxy())
+ .build()
+ );
+ }
+ httpPost.setEntity(new StringEntity(ticket.toString()));
+
+ try (CloseableHttpResponse response = requestHttp.getRequestHttpClient().execute(httpPost);
+ InputStream inputStream = InputStreamResponseHandler.INSTANCE.handleResponse(response);) {
+ Header[] contentTypeHeader = response.getHeaders("Content-Type");
+ if (contentTypeHeader != null && contentTypeHeader.length > 0
+ && ContentType.TEXT_PLAIN.getMimeType().equals(contentTypeHeader[0].getValue())) {
+ String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response);
+ throw new WxErrorException(WxError.fromJson(responseContent));
+ }
+
+ return FileUtils.createTmpFile(inputStream, UUID.randomUUID().toString(), "jpg");
+ } finally {
+ httpPost.releaseConnection();
+ }
+ }
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/json/WxMaGsonBuilder.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/json/WxMaGsonBuilder.java
new file mode 100644
index 00000000..4eb3a8ad
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/json/WxMaGsonBuilder.java
@@ -0,0 +1,24 @@
+package cn.binarywang.wx.miniapp.util.json;
+
+import cn.binarywang.wx.miniapp.bean.WxMaKefuMessage;
+import cn.binarywang.wx.miniapp.bean.WxMaTemplateMessage;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * @author Binary Wang
+ */
+public class WxMaGsonBuilder {
+ private static final GsonBuilder INSTANCE = new GsonBuilder();
+
+ static {
+ INSTANCE.disableHtmlEscaping();
+ INSTANCE.registerTypeAdapter(WxMaKefuMessage.class, new WxMaKefuMessageGsonAdapter());
+ INSTANCE.registerTypeAdapter(WxMaTemplateMessage.class, new WxMaTemplateMessageGsonAdapter());
+ }
+
+ public static Gson create() {
+ return INSTANCE.create();
+ }
+
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/json/WxMaKefuMessageGsonAdapter.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/json/WxMaKefuMessageGsonAdapter.java
new file mode 100644
index 00000000..33c46d63
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/json/WxMaKefuMessageGsonAdapter.java
@@ -0,0 +1,47 @@
+/*
+ * 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 cn.binarywang.wx.miniapp.util.json;
+
+import cn.binarywang.wx.miniapp.bean.WxMaKefuMessage;
+import cn.binarywang.wx.miniapp.constant.WxMaConstants;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import org.apache.commons.lang3.StringUtils;
+
+import java.lang.reflect.Type;
+
+/**
+ * @author Binary Wang
+ */
+public class WxMaKefuMessageGsonAdapter implements JsonSerializer {
+
+ @Override
+ public JsonElement serialize(WxMaKefuMessage message, Type typeOfSrc, JsonSerializationContext context) {
+ JsonObject messageJson = new JsonObject();
+ messageJson.addProperty("touser", message.getToUser());
+ messageJson.addProperty("msgtype", message.getMsgType());
+
+ if (WxMaConstants.KefuMsgType.TEXT.equals(message.getMsgType())) {
+ JsonObject text = new JsonObject();
+ text.addProperty("content", message.getContent());
+ messageJson.add("text", text);
+ }
+
+ if (WxMaConstants.KefuMsgType.IMAGE.equals(message.getMsgType())) {
+ JsonObject image = new JsonObject();
+ image.addProperty("media_id", message.getMediaId());
+ messageJson.add("image", image);
+ }
+
+ return messageJson;
+ }
+
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/json/WxMaTemplateMessageGsonAdapter.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/json/WxMaTemplateMessageGsonAdapter.java
new file mode 100644
index 00000000..0b4edee2
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/json/WxMaTemplateMessageGsonAdapter.java
@@ -0,0 +1,60 @@
+package cn.binarywang.wx.miniapp.util.json;
+
+import cn.binarywang.wx.miniapp.bean.WxMaTemplateMessage;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+import java.lang.reflect.Type;
+
+/**
+ * @author Binary Wang
+ */
+public class WxMaTemplateMessageGsonAdapter implements JsonSerializer {
+
+ @Override
+ public JsonElement serialize(WxMaTemplateMessage message, Type typeOfSrc, JsonSerializationContext context) {
+ JsonObject messageJson = new JsonObject();
+ messageJson.addProperty("touser", message.getToUser());
+ messageJson.addProperty("template_id", message.getTemplateId());
+ if (message.getPage() != null) {
+ messageJson.addProperty("page", message.getPage());
+ }
+
+ if (message.getFormId() != null) {
+ messageJson.addProperty("form_id", message.getFormId());
+ }
+
+ if (message.getPage() != null) {
+ messageJson.addProperty("page", message.getPage());
+ }
+
+ if (message.getColor() != null) {
+ messageJson.addProperty("color", message.getColor());
+ }
+
+ if (message.getEmphasisKeyword() != null) {
+ messageJson.addProperty("emphasis_keyword", message.getEmphasisKeyword());
+ }
+
+ JsonObject data = new JsonObject();
+ messageJson.add("data", data);
+
+ if (message.getData() == null) {
+ return messageJson;
+ }
+
+ for (WxMaTemplateMessage.Data datum : message.getData()) {
+ JsonObject dataJson = new JsonObject();
+ dataJson.addProperty("value", datum.getValue());
+ if (datum.getColor() != null) {
+ dataJson.addProperty("color", datum.getColor());
+ }
+ data.add(datum.getName(), dataJson);
+ }
+
+ return messageJson;
+ }
+
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/xml/XStreamTransformer.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/xml/XStreamTransformer.java
new file mode 100644
index 00000000..f85c716a
--- /dev/null
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/util/xml/XStreamTransformer.java
@@ -0,0 +1,88 @@
+package cn.binarywang.wx.miniapp.util.xml;
+
+import cn.binarywang.wx.miniapp.bean.WxMaMessage;
+import com.thoughtworks.xstream.XStream;
+import me.chanjar.weixin.common.util.xml.XStreamInitializer;
+
+import java.io.InputStream;
+import java.util.*;
+
+/**
+ * @author Binary Wang
+ */
+public class XStreamTransformer {
+ private static final Map, XStream> CLASS_2_XSTREAM_INSTANCE = new HashMap<>();
+
+ static {
+ registerClass(WxMaMessage.class);
+ }
+
+ /**
+ * xml -> pojo
+ */
+ @SuppressWarnings("unchecked")
+ public static T fromXml(Class clazz, String xml) {
+ T object = (T) CLASS_2_XSTREAM_INSTANCE.get(clazz).fromXML(xml);
+ return object;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static T fromXml(Class clazz, InputStream is) {
+ T object = (T) CLASS_2_XSTREAM_INSTANCE.get(clazz).fromXML(is);
+ return object;
+ }
+
+ /**
+ * pojo -> xml
+ */
+ public static String toXml(Class clazz, T object) {
+ return CLASS_2_XSTREAM_INSTANCE.get(clazz).toXML(object);
+ }
+
+ /**
+ * 注册扩展消息的解析器
+ *
+ * @param clz 类型
+ * @param xStream xml解析器
+ */
+ private static void register(Class> clz, XStream xStream) {
+ CLASS_2_XSTREAM_INSTANCE.put(clz, xStream);
+ }
+
+ /**
+ * 会自动注册该类及其子类
+ *
+ * @param clz 要注册的类
+ */
+ private static void registerClass(Class> clz) {
+ XStream xstream = XStreamInitializer.getInstance();
+ xstream.processAnnotations(clz);
+ xstream.processAnnotations(getInnerClasses(clz));
+ if (clz.equals(WxMaMessage.class)) {
+ // 操蛋的微信,模板消息推送成功的消息是MsgID,其他消息推送过来是MsgId
+ xstream.aliasField("MsgID", WxMaMessage.class, "msgId");
+ }
+
+ register(clz, xstream);
+ }
+
+ private static Class>[] getInnerClasses(Class> clz) {
+ Class>[] innerClasses = clz.getClasses();
+ if (innerClasses == null) {
+ return null;
+ }
+
+ List> result = new ArrayList<>();
+ result.addAll(Arrays.asList(innerClasses));
+ for (Class> inner : innerClasses) {
+ Class>[] innerClz = getInnerClasses(inner);
+ if (innerClz == null) {
+ continue;
+ }
+
+ result.addAll(Arrays.asList(innerClz));
+ }
+
+ return result.toArray(new Class>[0]);
+ }
+}
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaMediaServiceImplTest.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaMediaServiceImplTest.java
new file mode 100644
index 00000000..825ad05f
--- /dev/null
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaMediaServiceImplTest.java
@@ -0,0 +1,52 @@
+package cn.binarywang.wx.miniapp.api.impl;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.test.ApiTestModule;
+import com.google.inject.Inject;
+import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
+import me.chanjar.weixin.common.exception.WxErrorException;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+/**
+ * 临时素材接口的测试
+ *
+ * @author Binary Wang
+ */
+@Test
+@Guice(modules = ApiTestModule.class)
+public class WxMaMediaServiceImplTest {
+ @Inject
+ protected WxMaService wxService;
+
+ private String mediaId;
+
+ @Test
+ public void testUploadMedia() throws WxErrorException, IOException {
+ String mediaType = "image";
+ String fileType = "png";
+ String fileName = "tmp.png";
+ try (InputStream inputStream = ClassLoader.getSystemResourceAsStream(fileName)) {
+ WxMediaUploadResult res = this.wxService.getMediaService().uploadMedia(mediaType, fileType, inputStream);
+ assertNotNull(res.getType());
+ assertNotNull(res.getCreatedAt());
+ assertTrue(res.getMediaId() != null || res.getThumbMediaId() != null);
+ this.mediaId = res.getMediaId();
+ System.out.println(res);
+ }
+ }
+
+ @Test(dependsOnMethods = {"testUploadMedia"})
+ public void testGetMedia() throws WxErrorException {
+ File file = this.wxService.getMediaService().getMedia(this.mediaId);
+ assertNotNull(file);
+ System.out.println(file.getAbsolutePath());
+ }
+}
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaMsgServiceImplTest.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaMsgServiceImplTest.java
new file mode 100644
index 00000000..c5d8f1e8
--- /dev/null
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaMsgServiceImplTest.java
@@ -0,0 +1,76 @@
+package cn.binarywang.wx.miniapp.api.impl;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.bean.WxMaKefuMessage;
+import cn.binarywang.wx.miniapp.bean.WxMaTemplateMessage;
+import cn.binarywang.wx.miniapp.test.ApiTestModule;
+import cn.binarywang.wx.miniapp.test.TestConfig;
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+import me.chanjar.weixin.common.api.WxConsts;
+import me.chanjar.weixin.common.exception.WxErrorException;
+import org.testng.Assert;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * 测试客服相关接口
+ *
+ * @author Binary Wang
+ */
+@Test
+@Guice(modules = ApiTestModule.class)
+public class WxMaMsgServiceImplTest {
+
+ @Inject
+ protected WxMaService wxService;
+
+ public void testSendKefuMpNewsMessage() throws WxErrorException {
+ TestConfig configStorage = (TestConfig) this.wxService
+ .getWxMaConfig();
+ WxMaKefuMessage message = new WxMaKefuMessage();
+ message.setMsgType(WxConsts.CUSTOM_MSG_MPNEWS);
+ message.setToUser(configStorage.getOpenid());
+
+ this.wxService.getMsgService().sendKefuMsg(message);
+ }
+
+ public void testSendKefuMessage() throws WxErrorException {
+ TestConfig config = (TestConfig) this.wxService
+ .getWxMaConfig();
+ WxMaKefuMessage message = new WxMaKefuMessage();
+ message.setMsgType(WxConsts.CUSTOM_MSG_TEXT);
+ message.setToUser(config.getOpenid());
+ message.setContent(
+ "欢迎欢迎,热烈欢迎\n换行测试\n超链接:Hello World");
+
+ this.wxService.getMsgService().sendKefuMsg(message);
+ }
+
+ @Test(invocationCount = 5, threadPoolSize = 3)
+ public void testSendTemplateMsg() throws WxErrorException {
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+ TestConfig config = (TestConfig) this.wxService.getWxMaConfig();
+
+ WxMaTemplateMessage templateMessage = WxMaTemplateMessage.newBuilder()
+ .toUser(config.getOpenid())
+ .formId("FORMID")
+ .page("index")
+ .data(Lists.newArrayList(
+ new WxMaTemplateMessage.Data("keyword1", "339208499", "#173177"),
+ new WxMaTemplateMessage.Data("keyword2", dateFormat.format(new Date()), "#173177"),
+ new WxMaTemplateMessage.Data("keyword3", "粤海喜来登酒店", "#173177"),
+ new WxMaTemplateMessage.Data("keyword4", "广州市天河区天河路208号", "#173177")))
+ .templateId(config.getTemplateId())
+ .emphasisKeyword("keyword1.DATA")
+ .build();
+
+ String msgId = this.wxService.getMsgService().sendTemplateMsg(templateMessage);
+ Assert.assertNotNull(msgId);
+ System.out.println(msgId);
+ }
+
+}
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaQrcodeServiceImplTest.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaQrcodeServiceImplTest.java
new file mode 100644
index 00000000..48dad936
--- /dev/null
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaQrcodeServiceImplTest.java
@@ -0,0 +1,26 @@
+package cn.binarywang.wx.miniapp.api.impl;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.test.ApiTestModule;
+import com.google.inject.Inject;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import java.io.File;
+
+/**
+ * @author Binary Wang
+ */
+@Test
+@Guice(modules = ApiTestModule.class)
+public class WxMaQrcodeServiceImplTest {
+ @Inject
+ protected WxMaService wxService;
+
+ @Test
+ public void testCreateQrCode() throws Exception {
+ final File qrCode = this.wxService.getQrcodeService().createQrcode("111", 122);
+ System.out.println(qrCode);
+ }
+
+}
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaServiceImplTest.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaServiceImplTest.java
new file mode 100644
index 00000000..1c99530d
--- /dev/null
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaServiceImplTest.java
@@ -0,0 +1,35 @@
+package cn.binarywang.wx.miniapp.api.impl;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import cn.binarywang.wx.miniapp.test.ApiTestModule;
+import com.google.inject.Inject;
+import me.chanjar.weixin.common.exception.WxErrorException;
+import org.apache.commons.lang3.StringUtils;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertTrue;
+
+/**
+ * @author Binary Wang
+ */
+@Test
+@Guice(modules = ApiTestModule.class)
+public class WxMaServiceImplTest {
+
+ @Inject
+ private WxMaService wxService;
+
+ public void testRefreshAccessToken() throws WxErrorException {
+ WxMaConfig configStorage = this.wxService.getWxMaConfig();
+ String before = configStorage.getAccessToken();
+ this.wxService.getAccessToken(false);
+
+ String after = configStorage.getAccessToken();
+ assertNotEquals(before, after);
+ assertTrue(StringUtils.isNotBlank(after));
+ }
+
+}
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaUserServiceImplTest.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaUserServiceImplTest.java
new file mode 100644
index 00000000..a1969276
--- /dev/null
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/api/impl/WxMaUserServiceImplTest.java
@@ -0,0 +1,46 @@
+package cn.binarywang.wx.miniapp.api.impl;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.bean.WxMaUserInfo;
+import cn.binarywang.wx.miniapp.test.ApiTestModule;
+import com.google.inject.Inject;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+/**
+ * 测试用户相关的接口
+ *
+ * @author Binary Wang
+ */
+@Test
+@Guice(modules = ApiTestModule.class)
+public class WxMaUserServiceImplTest {
+
+ @Inject
+ private WxMaService wxService;
+
+ @Test
+ public void testGetSessionKey() throws Exception {
+ assertNotNull(this.wxService.getUserService().getSessionInfo("aaa"));
+ }
+
+ @Test
+ public void testGetUserInfo() throws Exception {
+ WxMaUserInfo userInfo = this.wxService.getUserService().getUserInfo("tiihtNczf5v6AKRyjwEUhQ==",
+ "CiyLU1Aw2KjvrjMdj8YKliAjtP4gsMZMQmRzooG2xrDcvSnxIMXFufNstNGTyaGS9uT5geRa0W4oTOb1WT7fJlAC+oNPdbB+3hVbJSRgv+4lGOETKUQz6OYStslQ142dNCuabNPGBzlooOmB231qMM85d2/fV6ChevvXvQP8Hkue1poOFtnEtpyxVLW1zAo6/1Xx1COxFvrc2d7UL/lmHInNlxuacJXwu0fjpXfz/YqYzBIBzD6WUfTIF9GRHpOn/Hz7saL8xz+W//FRAUid1OksQaQx4CMs8LOddcQhULW4ucetDf96JcR3g0gfRK4PC7E/r7Z6xNrXd2UIeorGj5Ef7b1pJAYB6Y5anaHqZ9J6nKEBvB4DnNLIVWSgARns/8wR2SiRS7MNACwTyrGvt9ts8p12PKFdlqYTopNHR1Vf7XjfhQlVsAJdNiKdYmYVoKlaRv85IfVunYzO0IKXsyl7JCUjCpoG20f0a04COwfneQAGGwd5oa+T8yO5hzuyDb/XcxxmK01EpqOyuxINew==",
+ "r7BXXKkLb8qrSNn05n0qiA==");
+ assertNotNull(userInfo);
+ System.out.println(userInfo.toString());
+ }
+
+ @Test
+ public void testCheckUserInfo() throws Exception {
+ assertTrue(this.wxService.getUserService().checkUserInfo("HyVFkGl5F5OQWJZZaNzBBg==",
+ "{\"nickName\":\"Band\",\"gender\":1,\"language\":\"zh_CN\",\"city\":\"Guangzhou\",\"province\":\"Guangdong\",\"country\":\"CN\",\"avatarUrl\":\"http://wx.qlogo.cn/mmopen/vi_32/1vZvI39NWFQ9XM4LtQpFrQJ1xlgZxx3w7bQxKARol6503Iuswjjn6nIGBiaycAjAtpujxyzYsrztuuICqIM5ibXQ/0\"}",
+ "75e81ceda165f4ffa64f4068af58c64b8f54b88c"));
+ }
+
+}
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaKefuMessageTest.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaKefuMessageTest.java
new file mode 100644
index 00000000..f2ef8bfa
--- /dev/null
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaKefuMessageTest.java
@@ -0,0 +1,40 @@
+package cn.binarywang.wx.miniapp.bean;
+
+import me.chanjar.weixin.common.api.WxConsts;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * @author Binary Wang
+ */
+@Test
+public class WxMaKefuMessageTest {
+
+ public void testTextReply() {
+ WxMaKefuMessage reply = new WxMaKefuMessage();
+ reply.setToUser("OPENID");
+ reply.setMsgType(WxConsts.CUSTOM_MSG_TEXT);
+ reply.setContent("sfsfdsdf");
+ Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"text\",\"text\":{\"content\":\"sfsfdsdf\"}}");
+ }
+
+ public void testTextBuild() {
+ WxMaKefuMessage reply = WxMaKefuMessage.TEXT().toUser("OPENID").content("sfsfdsdf").build();
+ Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"text\",\"text\":{\"content\":\"sfsfdsdf\"}}");
+ }
+
+ public void testImageReply() {
+ WxMaKefuMessage reply = new WxMaKefuMessage();
+ reply.setToUser("OPENID");
+ reply.setMsgType(WxConsts.CUSTOM_MSG_IMAGE);
+ reply.setMediaId("MEDIA_ID");
+ Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"image\",\"image\":{\"media_id\":\"MEDIA_ID\"}}");
+ }
+
+ public void testImageBuild() {
+ WxMaKefuMessage reply = WxMaKefuMessage.IMAGE().toUser("OPENID").mediaId("MEDIA_ID").build();
+ Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"image\",\"image\":{\"media_id\":\"MEDIA_ID\"}}");
+ }
+
+
+}
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaMessageTest.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaMessageTest.java
new file mode 100644
index 00000000..881091a1
--- /dev/null
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaMessageTest.java
@@ -0,0 +1,130 @@
+package cn.binarywang.wx.miniapp.bean;
+
+import cn.binarywang.wx.miniapp.bean.WxMaMessage;
+import me.chanjar.weixin.common.api.WxConsts;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+
+/**
+ * @author Binary Wang
+ */
+@Test
+public class WxMaMessageTest {
+
+ public void testFromXml() {
+ String xml = ""
+ + ""
+ + " "
+ + "1348831860"
+ + ""
+ + ""
+ + "1234567890123456"
+ + ""
+ + ""
+ + ""
+ + ""
+ + "23.134521"
+ + "113.358803"
+ + "20"
+ + ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + "23.137466"
+ + "113.352425"
+ + "119.385040"
+ + ""
+ + " "
+ + " "
+ + ""
+ + ""
+ + " 1\n"
+ + " "
+ + " - "
+ + " "
+ + "
"
+ + " "
+ + ""
+ + ""
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + ""
+ + "";
+ WxMaMessage wxMessage = WxMaMessage.fromXml(xml);
+ assertEquals(wxMessage.getToUser(), "toUser");
+ assertEquals(wxMessage.getFromUser(), "fromUser");
+ assertEquals(wxMessage.getCreateTime(), new Long(1348831860L));
+ assertEquals(wxMessage.getMsgType(), WxConsts.XML_MSG_TEXT);
+ assertEquals(wxMessage.getContent(), "this is a test");
+ assertEquals(wxMessage.getMsgId(), new Long(1234567890123456L));
+ assertEquals(wxMessage.getPicUrl(), "this is a url");
+ assertEquals(wxMessage.getMediaId(), "media_id");
+ assertEquals(wxMessage.getEvent(), "subscribe");
+ }
+
+ public void testFromXml2() {
+
+ String xml = ""
+ + ""
+ + " "
+ + "1348831860"
+ + ""
+ + ""
+ + "1234567890123456"
+ + ""
+ + ""
+ + ""
+ + ""
+ + "23.134521"
+ + "113.358803"
+ + "20"
+ + ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + "23.137466"
+ + "113.352425"
+ + "119.385040"
+ + ""
+ + " "
+ + " "
+ + ""
+ + ""
+ + " 1\n"
+ + " "
+ + " - "
+ + " "
+ + "
"
+ + " "
+ + ""
+ + ""
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + ""
+ + "";
+ WxMaMessage wxMessage = WxMaMessage.fromXml(xml);
+ assertEquals(wxMessage.getToUser(), "toUser");
+ assertEquals(wxMessage.getFromUser(), "fromUser");
+ assertEquals(wxMessage.getCreateTime(), new Integer(1348831860));
+ assertEquals(wxMessage.getMsgType(), WxConsts.XML_MSG_TEXT);
+ assertEquals(wxMessage.getContent(), "this is a test");
+ assertEquals(wxMessage.getMsgId(), new Long(1234567890123456L));
+ assertEquals(wxMessage.getPicUrl(), "this is a url");
+ assertEquals(wxMessage.getMediaId(), "media_id");
+ assertEquals(wxMessage.getEvent(), "subscribe");
+ }
+
+}
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaTemplateMessageTest.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaTemplateMessageTest.java
new file mode 100644
index 00000000..ad6e62c2
--- /dev/null
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/bean/WxMaTemplateMessageTest.java
@@ -0,0 +1,31 @@
+package cn.binarywang.wx.miniapp.bean;
+
+import com.google.common.collect.Lists;
+import org.testng.annotations.Test;
+
+import static org.testng.AssertJUnit.assertEquals;
+
+/**
+ * @author Binary Wang
+ */
+public class WxMaTemplateMessageTest {
+ @Test
+ public void testToJson() throws Exception {
+ WxMaTemplateMessage tm = WxMaTemplateMessage.newBuilder()
+ .toUser("OPENID")
+ //.color("aaaaa")
+ .formId("FORMID")
+ .page("index")
+ .data(Lists.newArrayList(
+ new WxMaTemplateMessage.Data("keyword1", "339208499", "#173177"),
+ new WxMaTemplateMessage.Data("keyword2", "2015年01月05日12:30", "#173177"),
+ new WxMaTemplateMessage.Data("keyword3", "粤海喜来登酒店", "#173177"),
+ new WxMaTemplateMessage.Data("keyword4", "广州市天河区天河路208号", "#173177")))
+ .templateId("TEMPLATE_ID")
+ .emphasisKeyword("keyword1.DATA")
+ .build();
+
+ assertEquals(tm.toJson(), "{\"touser\":\"OPENID\",\"template_id\":\"TEMPLATE_ID\",\"page\":\"index\",\"form_id\":\"FORMID\",\"emphasis_keyword\":\"keyword1.DATA\",\"data\":{\"keyword1\":{\"value\":\"339208499\",\"color\":\"#173177\"},\"keyword2\":{\"value\":\"2015年01月05日12:30\",\"color\":\"#173177\"},\"keyword3\":{\"value\":\"粤海喜来登酒店\",\"color\":\"#173177\"},\"keyword4\":{\"value\":\"广州市天河区天河路208号\",\"color\":\"#173177\"}}}");
+ }
+
+}
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/demo/WxMaDemoServer.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/demo/WxMaDemoServer.java
new file mode 100644
index 00000000..62847ab7
--- /dev/null
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/demo/WxMaDemoServer.java
@@ -0,0 +1,144 @@
+package cn.binarywang.wx.miniapp.demo;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
+import cn.binarywang.wx.miniapp.bean.WxMaMessage;
+import cn.binarywang.wx.miniapp.bean.WxMaKefuMessage;
+import cn.binarywang.wx.miniapp.bean.WxMaTemplateMessage;
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import cn.binarywang.wx.miniapp.message.WxMaMessageHandler;
+import cn.binarywang.wx.miniapp.message.WxMaMessageRouter;
+import cn.binarywang.wx.miniapp.test.TestConfig;
+import com.google.common.collect.Lists;
+import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
+import me.chanjar.weixin.common.exception.WxErrorException;
+import me.chanjar.weixin.common.session.WxSessionManager;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.servlet.ServletHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * @author Binary Wang
+ */
+public class WxMaDemoServer {
+
+ private static final WxMaMessageHandler logHandler = new WxMaMessageHandler() {
+ @Override
+ public void handle(WxMaMessage wxMessage, Map context,
+ WxMaService service, WxSessionManager sessionManager) throws WxErrorException {
+ System.out.println("收到消息:" + wxMessage.toString());
+ service.getMsgService().sendKefuMsg(WxMaKefuMessage.TEXT().content("收到信息为:" + wxMessage.toJson())
+ .toUser(wxMessage.getFromUser()).build());
+ }
+ };
+ private static final WxMaMessageHandler textHandler = new WxMaMessageHandler() {
+ @Override
+ public void handle(WxMaMessage wxMessage, Map context,
+ WxMaService service, WxSessionManager sessionManager)
+ throws WxErrorException {
+ service.getMsgService().sendKefuMsg(WxMaKefuMessage.TEXT().content("回复文本消息")
+ .toUser(wxMessage.getFromUser()).build());
+ }
+
+ };
+ private static final WxMaMessageHandler picHandler = new WxMaMessageHandler() {
+ @Override
+ public void handle(WxMaMessage wxMessage, Map context,
+ WxMaService service, WxSessionManager sessionManager) throws WxErrorException {
+ try {
+ WxMediaUploadResult uploadResult = service.getMediaService()
+ .uploadMedia("image", "png",
+ ClassLoader.getSystemResourceAsStream("tmp.png"));
+ service.getMsgService().sendKefuMsg(
+ WxMaKefuMessage
+ .IMAGE()
+ .mediaId(uploadResult.getMediaId())
+ .toUser(wxMessage.getFromUser())
+ .build());
+ } catch (WxErrorException e) {
+ e.printStackTrace();
+ }
+ }
+ };
+ private static final WxMaMessageHandler qrcodeHandler = new WxMaMessageHandler() {
+ @Override
+ public void handle(WxMaMessage wxMessage, Map context,
+ WxMaService service, WxSessionManager sessionManager) throws WxErrorException {
+ try {
+ final File file = service.getQrcodeService().createQrcode("123", 430);
+ WxMediaUploadResult uploadResult = service.getMediaService().uploadMedia("image", file);
+ service.getMsgService().sendKefuMsg(
+ WxMaKefuMessage
+ .IMAGE()
+ .mediaId(uploadResult.getMediaId())
+ .toUser(wxMessage.getFromUser())
+ .build());
+ } catch (WxErrorException e) {
+ e.printStackTrace();
+ }
+ }
+ };
+
+ private static final WxMaMessageHandler templateMsgHandler = new WxMaMessageHandler() {
+ @Override
+ public void handle(WxMaMessage wxMessage, Map context,
+ WxMaService service, WxSessionManager sessionManager)
+ throws WxErrorException {
+ service.getMsgService().sendTemplateMsg(WxMaTemplateMessage.newBuilder()
+ .templateId(templateId).data(Lists.newArrayList(
+ new WxMaTemplateMessage.Data("keyword1", "339208499", "#173177")))
+ .toUser(wxMessage.getFromUser())
+ .formId("自己替换可用的formid")
+ .build());
+ }
+
+ };
+
+ private static WxMaConfig config;
+ private static WxMaService service;
+ private static WxMaMessageRouter router;
+ private static String templateId;
+
+ public static void main(String[] args) throws Exception {
+ init();
+
+ Server server = new Server(8080);
+
+ ServletHandler servletHandler = new ServletHandler();
+ server.setHandler(servletHandler);
+
+ ServletHolder endpointServletHolder = new ServletHolder(new WxMaPortalServlet(config, service, router));
+ servletHandler.addServletWithMapping(endpointServletHolder, "/*");
+
+ server.start();
+ server.join();
+ }
+
+ private static void init() {
+ try (InputStream is1 = ClassLoader.getSystemResourceAsStream("test-config.xml")) {
+ TestConfig config = TestConfig.fromXml(is1);
+ config.setAccessTokenLock(new ReentrantLock());
+ templateId = config.getTemplateId();
+
+ WxMaDemoServer.config = config;
+ service = new WxMaServiceImpl();
+ service.setWxMaConfig(config);
+
+ router = new WxMaMessageRouter(service);
+
+ router.rule().handler(logHandler).next()
+ .rule().async(false).content("模板").handler(templateMsgHandler).end()
+ .rule().async(false).content("文本").handler(textHandler).end()
+ .rule().async(false).content("图片").handler(picHandler).end()
+ .rule().async(false).content("二维码").handler(qrcodeHandler).end();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/demo/WxMaPortalServlet.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/demo/WxMaPortalServlet.java
new file mode 100644
index 00000000..5cadb689
--- /dev/null
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/demo/WxMaPortalServlet.java
@@ -0,0 +1,92 @@
+package cn.binarywang.wx.miniapp.demo;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.bean.WxMaMessage;
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import cn.binarywang.wx.miniapp.constant.WxMaConstants;
+import cn.binarywang.wx.miniapp.message.WxMaMessageRouter;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+/**
+ * @author Binary Wang
+ */
+public class WxMaPortalServlet extends HttpServlet {
+ private static final long serialVersionUID = 1L;
+
+ private WxMaConfig wxMaConfig;
+ private WxMaService wxMaService;
+ private WxMaMessageRouter wxMaMessageRouter;
+
+ WxMaPortalServlet(WxMaConfig wxMaConfig, WxMaService wxMaService,
+ WxMaMessageRouter wxMaMessageRouter) {
+ this.wxMaConfig = wxMaConfig;
+ this.wxMaService = wxMaService;
+ this.wxMaMessageRouter = wxMaMessageRouter;
+ }
+
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ response.setContentType("text/html;charset=utf-8");
+ response.setStatus(HttpServletResponse.SC_OK);
+
+ String signature = request.getParameter("signature");
+ String nonce = request.getParameter("nonce");
+ String timestamp = request.getParameter("timestamp");
+
+ if (!this.wxMaService.checkSignature(timestamp, nonce, signature)) {
+ // 消息签名不正确,说明不是公众平台发过来的消息
+ response.getWriter().println("非法请求");
+ return;
+ }
+
+ String echoStr = request.getParameter("echostr");
+ if (StringUtils.isNotBlank(echoStr)) {
+ // 说明是一个仅仅用来验证的请求,回显echostr
+ response.getWriter().println(echoStr);
+ return;
+ }
+
+ String encryptType = request.getParameter("encrypt_type");
+ final boolean isJson = Objects.equals(this.wxMaConfig.getMsgDataFormat(), WxMaConstants.MsgDataFormat.JSON);
+ if (StringUtils.isBlank(encryptType)) {
+ // 明文传输的消息
+ WxMaMessage inMessage;
+ if (isJson) {
+ inMessage = WxMaMessage.fromJson(IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8));
+ } else {//xml
+ inMessage = WxMaMessage.fromXml(request.getInputStream());
+ }
+
+ this.wxMaMessageRouter.route(inMessage);
+ response.getWriter().write("success");
+ return;
+ }
+
+ if ("aes".equals(encryptType)) {
+ // 是aes加密的消息
+ String msgSignature = request.getParameter("msg_signature");
+ WxMaMessage inMessage;
+ if (isJson) {
+ inMessage = WxMaMessage.fromEncryptedJson(request.getInputStream(), this.wxMaConfig);
+ } else {//xml
+ inMessage = WxMaMessage.fromEncryptedXml(request.getInputStream(), this.wxMaConfig, timestamp, nonce, msgSignature);
+ }
+
+ this.wxMaMessageRouter.route(inMessage);
+ response.getWriter().write("success");
+ return;
+ }
+
+ response.getWriter().println("不可识别的加密类型");
+ }
+
+}
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/test/ApiTestModule.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/test/ApiTestModule.java
new file mode 100644
index 00000000..3b569e1b
--- /dev/null
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/test/ApiTestModule.java
@@ -0,0 +1,33 @@
+package cn.binarywang.wx.miniapp.test;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.config.WxMaConfig;
+import com.google.inject.Binder;
+import com.google.inject.Module;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * @author Binary Wang
+ */
+public class ApiTestModule implements Module {
+
+ @Override
+ public void configure(Binder binder) {
+ try (InputStream inputStream = ClassLoader.getSystemResourceAsStream("test-config.xml")) {
+ TestConfig config = TestConfig.fromXml(inputStream);
+ config.setAccessTokenLock(new ReentrantLock());
+
+ WxMaService wxService = new cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl();
+ wxService.setWxMaConfig(config);
+
+ binder.bind(WxMaService.class).toInstance(wxService);
+ binder.bind(WxMaConfig.class).toInstance(config);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/test/TestConfig.java b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/test/TestConfig.java
new file mode 100644
index 00000000..ee941ef3
--- /dev/null
+++ b/weixin-java-miniapp/src/test/java/cn/binarywang/wx/miniapp/test/TestConfig.java
@@ -0,0 +1,61 @@
+package cn.binarywang.wx.miniapp.test;
+
+import cn.binarywang.wx.miniapp.config.WxMaInMemoryConfig;
+import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import me.chanjar.weixin.common.util.xml.XStreamInitializer;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+
+import java.io.InputStream;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * @author Binary Wang
+ */
+@XStreamAlias("xml")
+public class TestConfig extends WxMaInMemoryConfig {
+
+ private String openid;
+ private String kfAccount;
+ private String templateId;
+
+ public static TestConfig fromXml(InputStream is) {
+ XStream xstream = XStreamInitializer.getInstance();
+ xstream.processAnnotations(TestConfig.class);
+ return (TestConfig) xstream.fromXML(is);
+ }
+
+ public String getOpenid() {
+ return this.openid;
+ }
+
+ public void setOpenid(String openid) {
+ this.openid = openid;
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this);
+ }
+
+ public String getKfAccount() {
+ return this.kfAccount;
+ }
+
+ public void setKfAccount(String kfAccount) {
+ this.kfAccount = kfAccount;
+ }
+
+ public String getTemplateId() {
+ return this.templateId;
+ }
+
+ public void setTemplateId(String templateId) {
+ this.templateId = templateId;
+ }
+
+ public void setAccessTokenLock(Lock lock) {
+ super.accessTokenLock = lock;
+ }
+
+}
diff --git a/weixin-java-miniapp/src/test/resources/logback-test.xml b/weixin-java-miniapp/src/test/resources/logback-test.xml
new file mode 100644
index 00000000..14d6a9a7
--- /dev/null
+++ b/weixin-java-miniapp/src/test/resources/logback-test.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %replace(%caller{1}){'Caller', ''} - %msg%n
+
+
+
+
+
+
+
+
+
diff --git a/weixin-java-miniapp/src/test/resources/test-config.sample.xml b/weixin-java-miniapp/src/test/resources/test-config.sample.xml
new file mode 100644
index 00000000..1f6cc388
--- /dev/null
+++ b/weixin-java-miniapp/src/test/resources/test-config.sample.xml
@@ -0,0 +1,11 @@
+
+ JSON或者XML
+ appid
+ secret
+ Token
+ EncodingAESKey
+ 可以不填写
+ 可以不填写
+ 某个用户的openId
+ 模版消息的模版ID
+
diff --git a/weixin-java-miniapp/src/test/resources/tmp.png b/weixin-java-miniapp/src/test/resources/tmp.png
new file mode 100644
index 00000000..5cb4a406
Binary files /dev/null and b/weixin-java-miniapp/src/test/resources/tmp.png differ