#193 增加小程序模块weixin-java-miniapp,支持小程序后台开发

This commit is contained in:
Binary Wang
2017-06-16 00:02:45 +08:00
parent 39e79a2c00
commit 1bd9b15c98
50 changed files with 3641 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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());
}
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

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

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();
}

View File

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

View File

@@ -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";//图片消息
}
}

View File

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

View File

@@ -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代表OKfalse代表不OK
*/
boolean intercept(WxMaMessage wxMessage,
Map<String, Object> context,
WxMaService wxMaService,
WxSessionManager sessionManager) throws WxErrorException;
}

View File

@@ -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);
}

View File

@@ -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();
}
}
}

View File

@@ -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 代表继续执行别的routerfalse 代表停止执行别的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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}

View File

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

View File

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

View File

@@ -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]);
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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"));
}
}

View File

@@ -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\"}}");
}
}

View File

@@ -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");
}
}

View File

@@ -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\"}}}");
}
}

View File

@@ -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();
}
}
}

View File

@@ -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("不可识别的加密类型");
}
}

View File

@@ -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();
}
}
}

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB