@@ -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> |