| @@ -93,6 +93,7 @@ | |||||
| <module>weixin-java-cp</module> | <module>weixin-java-cp</module> | ||||
| <module>weixin-java-mp</module> | <module>weixin-java-mp</module> | ||||
| <module>weixin-java-pay</module> | <module>weixin-java-pay</module> | ||||
| <module>weixin-java-miniapp</module> | |||||
| <!--module>weixin-java-osgi</module--> | <!--module>weixin-java-osgi</module--> | ||||
| </modules> | </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> | |||||