| @@ -93,6 +93,7 @@ | |||
| <module>weixin-java-cp</module> | |||
| <module>weixin-java-mp</module> | |||
| <module>weixin-java-pay</module> | |||
| <module>weixin-java-miniapp</module> | |||
| <!--module>weixin-java-osgi</module--> | |||
| </modules> | |||
| @@ -0,0 +1,84 @@ | |||
| <?xml version="1.0"?> | |||
| <project | |||
| xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" | |||
| xmlns="http://maven.apache.org/POM/4.0.0"> | |||
| <modelVersion>4.0.0</modelVersion> | |||
| <parent> | |||
| <groupId>com.github.binarywang</groupId> | |||
| <artifactId>weixin-java-parent</artifactId> | |||
| <version>2.6.3.BETA</version> | |||
| </parent> | |||
| <artifactId>weixin-java-miniapp</artifactId> | |||
| <name>WeiXin Java Tools - MiniApp</name> | |||
| <description>微信小程序Java SDK</description> | |||
| <dependencies> | |||
| <dependency> | |||
| <groupId>com.github.binarywang</groupId> | |||
| <artifactId>weixin-java-common</artifactId> | |||
| <version>${project.version}</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.jodd</groupId> | |||
| <artifactId>jodd-http</artifactId> | |||
| <scope>provided</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>com.squareup.okhttp3</groupId> | |||
| <artifactId>okhttp</artifactId> | |||
| <scope>provided</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.testng</groupId> | |||
| <artifactId>testng</artifactId> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>ch.qos.logback</groupId> | |||
| <artifactId>logback-classic</artifactId> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>com.google.inject</groupId> | |||
| <artifactId>guice</artifactId> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.eclipse.jetty</groupId> | |||
| <artifactId>jetty-server</artifactId> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.eclipse.jetty</groupId> | |||
| <artifactId>jetty-servlet</artifactId> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>joda-time</groupId> | |||
| <artifactId>joda-time</artifactId> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>redis.clients</groupId> | |||
| <artifactId>jedis</artifactId> | |||
| </dependency> | |||
| </dependencies> | |||
| <build> | |||
| <plugins> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-surefire-plugin</artifactId> | |||
| <configuration> | |||
| <suiteXmlFiles> | |||
| <suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile> | |||
| </suiteXmlFiles> | |||
| </configuration> | |||
| </plugin> | |||
| </plugins> | |||
| </build> | |||
| </project> | |||
| @@ -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; | |||
| /** | |||
| * <pre> | |||
| * 临时素材接口 | |||
| * Created by Binary Wang on 2016/7/21. | |||
| * </pre> | |||
| * | |||
| * @author <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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"; | |||
| /** | |||
| * <pre> | |||
| * 新增临时素材 | |||
| * 小程序可以使用本接口把媒体文件(目前仅支持图片)上传到微信服务器,用户发送客服消息或被动回复用户消息。 | |||
| * 详情请见: <a href="https://mp.weixin.qq.com/debug/wxadoc/dev/api/custommsg/material.html#新增临时素材">新增临时素材</a> | |||
| * 接口url格式:https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE | |||
| * </pre> | |||
| * | |||
| * @param mediaType 媒体类型, | |||
| * @param file 文件对象 | |||
| * @see #uploadMedia(String, String, InputStream) | |||
| */ | |||
| WxMediaUploadResult uploadMedia(String mediaType, File file) throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| * 新增临时素材 | |||
| * 小程序可以使用本接口把媒体文件(目前仅支持图片)上传到微信服务器,用户发送客服消息或被动回复用户消息。 | |||
| * | |||
| * 详情请见: <a href="https://mp.weixin.qq.com/debug/wxadoc/dev/api/custommsg/material.html#新增临时素材">新增临时素材</a> | |||
| * 接口url格式:https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE | |||
| * </pre> | |||
| * | |||
| * @param mediaType 媒体类型 | |||
| * @param fileType 文件类型 | |||
| * @param inputStream 输入流 | |||
| * @see #uploadMedia(java.lang.String, java.io.File) | |||
| */ | |||
| WxMediaUploadResult uploadMedia(String mediaType, String fileType, InputStream inputStream) throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| * 获取临时素材 | |||
| * 小程序可以使用本接口获取客服消息内的临时素材(即下载临时的多媒体文件)。目前小程序仅支持下载图片文件。 | |||
| * | |||
| * 详情请见: <a href="https://mp.weixin.qq.com/debug/wxadoc/dev/api/custommsg/material.html#获取临时素材">获取临时素材</a> | |||
| * 接口url格式:https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID | |||
| * </pre> | |||
| * | |||
| * @param mediaId 媒体Id | |||
| * @return 保存到本地的临时文件 | |||
| */ | |||
| File getMedia(String mediaId) throws WxErrorException; | |||
| } | |||
| @@ -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; | |||
| /** | |||
| * <pre> | |||
| * 消息发送接口 | |||
| * </pre> | |||
| * | |||
| * @author <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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"; | |||
| /** | |||
| * <pre> | |||
| * 发送客服消息 | |||
| * 详情请见: <a href="http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140547&token=&lang=zh_CN">发送客服消息</a> | |||
| * 接口url格式:https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN | |||
| * </pre> | |||
| */ | |||
| boolean sendKefuMsg(WxMaKefuMessage message) throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| * 发送模板消息 | |||
| * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1433751277&token=&lang=zh_CN | |||
| * </pre> | |||
| * | |||
| * @return 消息Id | |||
| */ | |||
| String sendTemplateMsg(WxMaTemplateMessage templateMessage) throws WxErrorException; | |||
| } | |||
| @@ -0,0 +1,30 @@ | |||
| package cn.binarywang.wx.miniapp.api; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import java.io.File; | |||
| /** | |||
| * <pre> | |||
| * 二维码相关操作接口 | |||
| * 文档地址:https://mp.weixin.qq.com/debug/wxadoc/dev/api/qrcode.html | |||
| * </pre> | |||
| * | |||
| * @author <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| public interface WxMaQrcodeService { | |||
| /** | |||
| * <pre> | |||
| * 获取小程序页面二维码 | |||
| * 适用于需要的码数量较少的业务场景 | |||
| * 通过该接口,仅能生成已发布的小程序的二维码。 | |||
| * 可以在开发者工具预览时生成开发版的带参二维码。 | |||
| * 带参二维码只有 100000 个,请谨慎调用。 | |||
| * </pre> | |||
| * | |||
| * @param path 不能为空,最大长度 128 字节 | |||
| * @param width 默认430 二维码的宽度 | |||
| */ | |||
| File createQrcode(String path, int width) throws WxErrorException; | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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"; | |||
| /** | |||
| * <pre> | |||
| * 验证消息的确来自微信服务器 | |||
| * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319&token=&lang=zh_CN | |||
| * </pre> | |||
| */ | |||
| boolean checkSignature(String timestamp, String nonce, String signature); | |||
| /** | |||
| * 获取access_token, 不强制刷新access_token | |||
| * | |||
| * @see #getAccessToken(boolean) | |||
| */ | |||
| String getAccessToken() throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| * 获取access_token,本方法线程安全 | |||
| * 且在多线程同时刷新时只刷新一次,避免超出2000次/日的调用次数上限 | |||
| * | |||
| * 另:本service的所有方法都会在access_token过期是调用此方法 | |||
| * | |||
| * 程序员在非必要情况下尽量不要主动调用此方法 | |||
| * | |||
| * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183&token=&lang=zh_CN | |||
| * </pre> | |||
| * | |||
| * @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; | |||
| /** | |||
| * <pre> | |||
| * Service没有实现某个API的时候,可以用这个, | |||
| * 比{@link #get}和{@link #post}方法更灵活,可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型。 | |||
| * 可以参考,{@link MediaUploadRequestExecutor}的实现方法 | |||
| * </pre> | |||
| */ | |||
| <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| * 设置当微信系统响应系统繁忙时,要等待多少 retrySleepMillis(ms) * 2^(重试次数 - 1) 再发起重试 | |||
| * 默认:1000ms | |||
| * </pre> | |||
| */ | |||
| void setRetrySleepMillis(int retrySleepMillis); | |||
| /** | |||
| * <pre> | |||
| * 设置当微信系统响应系统繁忙时,最大重试次数 | |||
| * 默认:5次 | |||
| * </pre> | |||
| */ | |||
| 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(); | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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); | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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<File, String> 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()); | |||
| } | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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)); | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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)); | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| public class WxMaServiceImpl implements WxMaService, RequestHttp<CloseableHttpClient, HttpHost> { | |||
| 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, E> T execute(RequestExecutor<T, E> 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, E> T executeInternal(RequestExecutor<T, E> 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; | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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<String, String> 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); | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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); | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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); | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| @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; | |||
| } | |||
| } | |||
| @@ -0,0 +1,44 @@ | |||
| package cn.binarywang.wx.miniapp.bean; | |||
| import cn.binarywang.wx.miniapp.util.json.WxMaGsonBuilder; | |||
| import java.io.Serializable; | |||
| /** | |||
| * @author <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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); | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| public class WxMaTemplateMessage implements Serializable { | |||
| private static final long serialVersionUID = 5063374783759519418L; | |||
| /** | |||
| * touser 是 接收者(用户)的 openid | |||
| */ | |||
| private String toUser; | |||
| /** | |||
| * template_id 是 所需下发的模板消息的id | |||
| */ | |||
| private String templateId; | |||
| /** | |||
| * <pre> | |||
| * page 否 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转。 | |||
| * </pre> | |||
| */ | |||
| private String page; | |||
| /** | |||
| * form_id 是 表单提交场景下,为 submit 事件带上的 formId;支付场景下,为本次支付的 prepay_id | |||
| */ | |||
| private String formId; | |||
| /** | |||
| * data 是 模板内容,不填则下发空模板 | |||
| */ | |||
| private List<Data> 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<Data> getData() { | |||
| return data; | |||
| } | |||
| public void setData(List<Data> 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> 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> 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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| package cn.binarywang.wx.miniapp.builder; | |||
| import cn.binarywang.wx.miniapp.bean.WxMaKefuMessage; | |||
| /** | |||
| * @author <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| public class BaseBuilder<T> { | |||
| 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; | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| public final class ImageBuilder extends BaseBuilder<ImageBuilder> { | |||
| 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; | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| public final class TextBuilder extends BaseBuilder<TextBuilder> { | |||
| 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; | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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(); | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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; | |||
| } | |||
| } | |||
| @@ -0,0 +1,26 @@ | |||
| package cn.binarywang.wx.miniapp.constant; | |||
| /** | |||
| * <pre> | |||
| * 小程序常量 | |||
| * </pre> | |||
| * | |||
| * @author <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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";//图片消息 | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| public interface WxMaMessageHandler { | |||
| void handle(WxMaMessage message, Map<String, Object> context, | |||
| WxMaService service, WxSessionManager sessionManager) throws WxErrorException; | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| public interface WxMaMessageInterceptor { | |||
| /** | |||
| * 拦截微信消息 | |||
| * | |||
| * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||
| * @return true代表OK,false代表不OK | |||
| */ | |||
| boolean intercept(WxMaMessage wxMessage, | |||
| Map<String, Object> context, | |||
| WxMaService wxMaService, | |||
| WxSessionManager sessionManager) throws WxErrorException; | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| package cn.binarywang.wx.miniapp.message; | |||
| import cn.binarywang.wx.miniapp.bean.WxMaMessage; | |||
| /** | |||
| * 消息匹配器,用在消息路由的时候 | |||
| * | |||
| * @author <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| public interface WxMaMessageMatcher { | |||
| /** | |||
| * 消息是否匹配某种模式 | |||
| */ | |||
| boolean match(WxMaMessage message); | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| public class WxMaMessageRouter { | |||
| private static final int DEFAULT_THREAD_POOL_SIZE = 100; | |||
| private final Logger log = LoggerFactory.getLogger(WxMaMessageRouter.class); | |||
| private final List<WxMaMessageRouterRule> 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(); | |||
| } | |||
| /** | |||
| * <pre> | |||
| * 设置自定义的 {@link ExecutorService} | |||
| * 如果不调用该方法,默认使用 Executors.newFixedThreadPool(100) | |||
| * </pre> | |||
| */ | |||
| public void setExecutorService(ExecutorService executorService) { | |||
| this.executorService = executorService; | |||
| } | |||
| /** | |||
| * <pre> | |||
| * 设置自定义的 {@link me.chanjar.weixin.common.api.WxMessageDuplicateChecker} | |||
| * 如果不调用该方法,默认使用 {@link me.chanjar.weixin.common.api.WxMessageInMemoryDuplicateChecker} | |||
| * </pre> | |||
| */ | |||
| public void setMessageDuplicateChecker(WxMessageDuplicateChecker messageDuplicateChecker) { | |||
| this.messageDuplicateChecker = messageDuplicateChecker; | |||
| } | |||
| /** | |||
| * <pre> | |||
| * 设置自定义的{@link me.chanjar.weixin.common.session.WxSessionManager} | |||
| * 如果不调用该方法,默认使用 {@link me.chanjar.weixin.common.session.StandardSessionManager} | |||
| * </pre> | |||
| */ | |||
| public void setSessionManager(WxSessionManager sessionManager) { | |||
| this.sessionManager = sessionManager; | |||
| } | |||
| /** | |||
| * <pre> | |||
| * 设置自定义的{@link me.chanjar.weixin.common.api.WxErrorExceptionHandler} | |||
| * 如果不调用该方法,默认使用 {@link me.chanjar.weixin.common.util.LogExceptionHandler} | |||
| * </pre> | |||
| */ | |||
| public void setExceptionHandler(WxErrorExceptionHandler exceptionHandler) { | |||
| this.exceptionHandler = exceptionHandler; | |||
| } | |||
| List<WxMaMessageRouterRule> getRules() { | |||
| return this.rules; | |||
| } | |||
| /** | |||
| * 开始一个新的Route规则 | |||
| */ | |||
| public WxMaMessageRouterRule rule() { | |||
| return new WxMaMessageRouterRule(this); | |||
| } | |||
| /** | |||
| * 处理微信消息 | |||
| */ | |||
| public void route(final WxMaMessage wxMessage, final Map<String, Object> context) { | |||
| final List<WxMaMessageRouterRule> 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<Future<?>> 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<String, Object>()); | |||
| } | |||
| /** | |||
| * 对session的访问结束 | |||
| */ | |||
| protected void sessionEndAccess(WxMaMessage wxMessage) { | |||
| InternalSession session = ((InternalSessionManager) this.sessionManager).findSession(wxMessage.getFromUser()); | |||
| if (session != null) { | |||
| session.endAccess(); | |||
| } | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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<WxMaMessageHandler> handlers = new ArrayList<>(); | |||
| private List<WxMaMessageInterceptor> 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<String, Object> 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<WxMaMessageHandler> getHandlers() { | |||
| return this.handlers; | |||
| } | |||
| public void setHandlers(List<WxMaMessageHandler> handlers) { | |||
| this.handlers = handlers; | |||
| } | |||
| public List<WxMaMessageInterceptor> getInterceptors() { | |||
| return this.interceptors; | |||
| } | |||
| public void setInterceptors(List<WxMaMessageInterceptor> interceptors) { | |||
| this.interceptors = interceptors; | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| public class QrCodeRequestExecutor implements RequestExecutor<File, WxMaQrcode> { | |||
| protected RequestHttp<CloseableHttpClient, HttpHost> 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(); | |||
| } | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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(); | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| public class WxMaKefuMessageGsonAdapter implements JsonSerializer<WxMaKefuMessage> { | |||
| @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; | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| public class WxMaTemplateMessageGsonAdapter implements JsonSerializer<WxMaTemplateMessage> { | |||
| @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; | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| public class XStreamTransformer { | |||
| private static final Map<Class<?>, XStream> CLASS_2_XSTREAM_INSTANCE = new HashMap<>(); | |||
| static { | |||
| registerClass(WxMaMessage.class); | |||
| } | |||
| /** | |||
| * xml -> pojo | |||
| */ | |||
| @SuppressWarnings("unchecked") | |||
| public static <T> T fromXml(Class<T> clazz, String xml) { | |||
| T object = (T) CLASS_2_XSTREAM_INSTANCE.get(clazz).fromXML(xml); | |||
| return object; | |||
| } | |||
| @SuppressWarnings("unchecked") | |||
| public static <T> T fromXml(Class<T> clazz, InputStream is) { | |||
| T object = (T) CLASS_2_XSTREAM_INSTANCE.get(clazz).fromXML(is); | |||
| return object; | |||
| } | |||
| /** | |||
| * pojo -> xml | |||
| */ | |||
| public static <T> String toXml(Class<T> 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<Class<?>> 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]); | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| @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()); | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| @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超链接:<a href=\"http://www.baidu.com\">Hello World</a>"); | |||
| 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); | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| @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); | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| @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)); | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| @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")); | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| @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\"}}"); | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| @Test | |||
| public class WxMaMessageTest { | |||
| public void testFromXml() { | |||
| String xml = "<xml>" | |||
| + "<ToUserName><![CDATA[toUser]]></ToUserName>" | |||
| + "<FromUserName><![CDATA[fromUser]]></FromUserName> " | |||
| + "<CreateTime>1348831860</CreateTime>" | |||
| + "<MsgDataFormat><![CDATA[text]]></MsgDataFormat>" | |||
| + "<Content><![CDATA[this is a test]]></Content>" | |||
| + "<MsgId>1234567890123456</MsgId>" | |||
| + "<PicUrl><![CDATA[this is a url]]></PicUrl>" | |||
| + "<MediaId><![CDATA[media_id]]></MediaId>" | |||
| + "<Format><![CDATA[Format]]></Format>" | |||
| + "<ThumbMediaId><![CDATA[thumb_media_id]]></ThumbMediaId>" | |||
| + "<Location_X>23.134521</Location_X>" | |||
| + "<Location_Y>113.358803</Location_Y>" | |||
| + "<Scale>20</Scale>" | |||
| + "<Label><![CDATA[位置信息]]></Label>" | |||
| + "<Description><![CDATA[公众平台官网链接]]></Description>" | |||
| + "<Url><![CDATA[url]]></Url>" | |||
| + "<Title><![CDATA[公众平台官网链接]]></Title>" | |||
| + "<Event><![CDATA[subscribe]]></Event>" | |||
| + "<EventKey><![CDATA[qrscene_123123]]></EventKey>" | |||
| + "<Ticket><![CDATA[TICKET]]></Ticket>" | |||
| + "<Latitude>23.137466</Latitude>" | |||
| + "<Longitude>113.352425</Longitude>" | |||
| + "<Precision>119.385040</Precision>" | |||
| + "<ScanCodeInfo>" | |||
| + " <ScanType><![CDATA[qrcode]]></ScanType>" | |||
| + " <ScanResult><![CDATA[1]]></ScanResult>" | |||
| + "</ScanCodeInfo>" | |||
| + "<SendPicsInfo>" | |||
| + " <Count>1</Count>\n" | |||
| + " <PicList>" | |||
| + " <item>" | |||
| + " <PicMd5Sum><![CDATA[1b5f7c23b5bf75682a53e7b6d163e185]]></PicMd5Sum>" | |||
| + " </item>" | |||
| + " </PicList>" | |||
| + "</SendPicsInfo>" | |||
| + "<SendLocationInfo>" | |||
| + " <Location_X><![CDATA[23]]></Location_X>\n" | |||
| + " <Location_Y><![CDATA[113]]></Location_Y>\n" | |||
| + " <Scale><![CDATA[15]]></Scale>\n" | |||
| + " <Label><![CDATA[ 广州市海珠区客村艺苑路 106号]]></Label>\n" | |||
| + " <Poiname><![CDATA[wo de poi]]></Poiname>\n" | |||
| + "</SendLocationInfo>" | |||
| + "</xml>"; | |||
| 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 = "<xml>" | |||
| + "<ToUserName><![CDATA[toUser]]></ToUserName>" | |||
| + "<FromUserName><![CDATA[fromUser]]></FromUserName> " | |||
| + "<CreateTime>1348831860</CreateTime>" | |||
| + "<MsgDataFormat><![CDATA[text]]></MsgDataFormat>" | |||
| + "<Content><![CDATA[this is a test]]></Content>" | |||
| + "<MsgID>1234567890123456</MsgID>" | |||
| + "<PicUrl><![CDATA[this is a url]]></PicUrl>" | |||
| + "<MediaId><![CDATA[media_id]]></MediaId>" | |||
| + "<Format><![CDATA[Format]]></Format>" | |||
| + "<ThumbMediaId><![CDATA[thumb_media_id]]></ThumbMediaId>" | |||
| + "<Location_X>23.134521</Location_X>" | |||
| + "<Location_Y>113.358803</Location_Y>" | |||
| + "<Scale>20</Scale>" | |||
| + "<Label><![CDATA[位置信息]]></Label>" | |||
| + "<Description><![CDATA[公众平台官网链接]]></Description>" | |||
| + "<Url><![CDATA[url]]></Url>" | |||
| + "<Title><![CDATA[公众平台官网链接]]></Title>" | |||
| + "<Event><![CDATA[subscribe]]></Event>" | |||
| + "<EventKey><![CDATA[qrscene_123123]]></EventKey>" | |||
| + "<Ticket><![CDATA[TICKET]]></Ticket>" | |||
| + "<Latitude>23.137466</Latitude>" | |||
| + "<Longitude>113.352425</Longitude>" | |||
| + "<Precision>119.385040</Precision>" | |||
| + "<ScanCodeInfo>" | |||
| + " <ScanType><![CDATA[qrcode]]></ScanType>" | |||
| + " <ScanResult><![CDATA[1]]></ScanResult>" | |||
| + "</ScanCodeInfo>" | |||
| + "<SendPicsInfo>" | |||
| + " <Count>1</Count>\n" | |||
| + " <PicList>" | |||
| + " <item>" | |||
| + " <PicMd5Sum><![CDATA[1b5f7c23b5bf75682a53e7b6d163e185]]></PicMd5Sum>" | |||
| + " </item>" | |||
| + " </PicList>" | |||
| + "</SendPicsInfo>" | |||
| + "<SendLocationInfo>" | |||
| + " <Location_X><![CDATA[23]]></Location_X>\n" | |||
| + " <Location_Y><![CDATA[113]]></Location_Y>\n" | |||
| + " <Scale><![CDATA[15]]></Scale>\n" | |||
| + " <Label><![CDATA[ 广州市海珠区客村艺苑路 106号]]></Label>\n" | |||
| + " <Poiname><![CDATA[wo de poi]]></Poiname>\n" | |||
| + "</SendLocationInfo>" | |||
| + "</xml>"; | |||
| 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"); | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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\"}}}"); | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| public class WxMaDemoServer { | |||
| private static final WxMaMessageHandler logHandler = new WxMaMessageHandler() { | |||
| @Override | |||
| public void handle(WxMaMessage wxMessage, Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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(); | |||
| } | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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("不可识别的加密类型"); | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| 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(); | |||
| } | |||
| } | |||
| } | |||
| @@ -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 <a href="https://github.com/binarywang">Binary Wang</a> | |||
| */ | |||
| @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; | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| <configuration> | |||
| <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> | |||
| <encoder> | |||
| <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %replace(%caller{1}){'Caller', ''} - %msg%n</pattern> | |||
| </encoder> | |||
| </appender> | |||
| <root level="info"> | |||
| <appender-ref ref="STDOUT"/> | |||
| </root> | |||
| <logger name="cn.binarywang.wx.miniapp" level="debug"/> | |||
| </configuration> | |||
| @@ -0,0 +1,11 @@ | |||
| <xml> | |||
| <msgDataFormat>JSON或者XML</msgDataFormat> | |||
| <appid>appid</appid> | |||
| <secret>secret</secret> | |||
| <token>Token</token> | |||
| <aesKey>EncodingAESKey</aesKey> | |||
| <accessToken>可以不填写</accessToken> | |||
| <expiresTime>可以不填写</expiresTime> | |||
| <openid>某个用户的openId</openid> | |||
| <templateId>模版消息的模版ID</templateId> | |||
| </xml> | |||