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