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" + + "" + + "" + + "" + + "<![CDATA[公众平台官网链接]]>" + + "" + + "" + + "" + + "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" + + "" + + "" + + "" + + "<![CDATA[公众平台官网链接]]>" + + "" + + "" + + "" + + "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