diff --git a/demo.md b/demo.md
index 89c61ab4..8d8f6d83 100644
--- a/demo.md
+++ b/demo.md
@@ -5,7 +5,7 @@
1. 微信支付Demo:[码云](http://gitee.com/binary/weixin-java-pay-demo)、[GitHub](http://github.com/binarywang/weixin-java-pay-demo)
1. 企业号/企业微信Demo:[码云](http://gitee.com/binary/weixin-java-cp-demo)、[GitHub](http://github.com/binarywang/weixin-java-cp-demo)
1. 微信小程序Demo:[码云](http://gitee.com/binary/weixin-java-miniapp-demo)、[GitHub](http://github.com/binarywang/weixin-java-miniapp-demo)
-1. 开放平台Demo:[码云](http://gitee.com/binary/weixin-java-open-demo)、[GitHub](http://github.com/binarywang/weixin-java-open-demo)
+1. 开放平台Demo:[码云](http://gitee.com/binary/weixin-java-open-demo)、[GitHub](http://github.com/Wechat-Group/weixin-java-open-demo)
1. 公众号Demo:
- 使用Spring MVC实现的公众号Demo:[码云](http://gitee.com/binary/weixin-java-mp-demo)、[GitHub](http://github.com/binarywang/weixin-java-mp-demo)
- 使用Spring Boot实现的公众号Demo:[码云](http://gitee.com/binary/weixin-java-mp-demo-springboot)、[GitHub](http://github.com/binarywang/weixin-java-mp-demo-springboot)
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpConfigStorage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpConfigStorage.java
index dbff2a7a..ea21c80b 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpConfigStorage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpConfigStorage.java
@@ -85,6 +85,8 @@ public interface WxMpConfigStorage {
String getAesKey();
+ String getTemplateId();
+
long getExpiresTime();
String getOauth2redirectUri();
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpInMemoryConfigStorage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpInMemoryConfigStorage.java
index e74f5188..46a22fa8 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpInMemoryConfigStorage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpInMemoryConfigStorage.java
@@ -18,6 +18,7 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage {
protected volatile String appId;
protected volatile String secret;
protected volatile String token;
+ protected volatile String templateId;
protected volatile String accessToken;
protected volatile String aesKey;
protected volatile long expiresTime;
@@ -173,6 +174,15 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage {
this.token = token;
}
+ @Override
+ public String getTemplateId() {
+ return this.templateId;
+ }
+
+ public void setTemplateId(String templateId) {
+ this.templateId = templateId;
+ }
+
@Override
public long getExpiresTime() {
return this.expiresTime;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpService.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpService.java
index 0dd5a66b..647f9132 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpService.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpService.java
@@ -363,6 +363,13 @@ public interface WxMpService {
*/
WxMpTemplateMsgService getTemplateMsgService();
+ /**
+ * 返回一次性订阅消息相关接口方法的实现类对象,以方便调用其各个接口
+ *
+ * @return WxMpSubscribeMsgService
+ */
+ WxMpSubscribeMsgService getSubscribeMsgService();
+
/**
* 返回硬件平台相关接口方法的实现类对象,以方便调用其各个接口
*
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpSubscribeMsgService.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpSubscribeMsgService.java
new file mode 100644
index 00000000..9e3b45e0
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpSubscribeMsgService.java
@@ -0,0 +1,40 @@
+package me.chanjar.weixin.mp.api;
+
+import me.chanjar.weixin.common.exception.WxErrorException;
+import me.chanjar.weixin.mp.bean.subscribe.WxMpSubscribeMessage;
+
+/**
+ *
+ * 一次性订阅消息接口
+ * https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1500374289_66bvB
+ *
+ *
+ * @author Mklaus
+ * @date 2018-01-22 上午11:07
+ */
+public interface WxMpSubscribeMsgService {
+
+ /**
+ *
+ * 构造用户订阅一条模板消息授权的url连接
+ * 详情请见: https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1500374289_66bvB
+ *
+ *
+ * @param redirectURI 用户授权完成后的重定向链接,无需urlencode, 方法内会进行encode
+ * @param scene 重定向后会带上scene参数,开发者可以填0-10000的整形值,用来标识订阅场景值
+ * @param reserved 用于保持请求和回调的状态,授权请后原样带回给第三方 (最多128字节,要求做urlencode)
+ * @return url
+ */
+ String subscribeMsgAuthorizationUrl(String redirectURI, int scene, String reserved);
+
+ /**
+ *
+ * 发送一次性订阅消息
+ * 详情请见: https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1500374289_66bvB
+ *
+ *
+ * @return 消息Id
+ */
+ boolean sendSubscribeMessage(WxMpSubscribeMessage message) throws WxErrorException;
+
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpServiceAbstractImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpServiceAbstractImpl.java
index b79a1bba..a09d8cc4 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpServiceAbstractImpl.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpServiceAbstractImpl.java
@@ -40,6 +40,7 @@ public abstract class WxMpServiceAbstractImpl implements WxMpService, Requ
private WxMpDataCubeService dataCubeService = new WxMpDataCubeServiceImpl(this);
private WxMpUserBlacklistService blackListService = new WxMpUserBlacklistServiceImpl(this);
private WxMpTemplateMsgService templateMsgService = new WxMpTemplateMsgServiceImpl(this);
+ private WxMpSubscribeMsgService subscribeMsgService = new WxMpSubscribeMsgServiceImpl(this);
private WxMpDeviceService deviceService = new WxMpDeviceServiceImpl(this);
private WxMpShakeService shakeService = new WxMpShakeServiceImpl(this);
private WxMpMemberCardService memberCardService = new WxMpMemberCardServiceImpl(this);
@@ -375,6 +376,11 @@ public abstract class WxMpServiceAbstractImpl implements WxMpService, Requ
return this.templateMsgService;
}
+ @Override
+ public WxMpSubscribeMsgService getSubscribeMsgService() {
+ return this.subscribeMsgService;
+ }
+
@Override
public WxMpDeviceService getDeviceService() {
return this.deviceService;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpSubscribeMsgServiceImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpSubscribeMsgServiceImpl.java
new file mode 100644
index 00000000..4cf17bd2
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpSubscribeMsgServiceImpl.java
@@ -0,0 +1,42 @@
+package me.chanjar.weixin.mp.api.impl;
+
+import me.chanjar.weixin.common.exception.WxErrorException;
+import me.chanjar.weixin.common.util.http.URIUtil;
+import me.chanjar.weixin.mp.api.WxMpConfigStorage;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.api.WxMpSubscribeMsgService;
+import me.chanjar.weixin.mp.bean.subscribe.WxMpSubscribeMessage;
+
+/**
+ * @author Mklaus
+ * @date 2018-01-22 上午11:19
+ */
+public class WxMpSubscribeMsgServiceImpl implements WxMpSubscribeMsgService {
+ private static final String SUBSCRIBE_MESSAGE_AUTHORIZE_URL =
+ "https://mp.weixin.qq.com/mp/subscribemsg?action=get_confirm&appid=%s&scene=%d&template_id=%s&redirect_url=%s&reserved=%s#wechat_redirect";
+ private static final String SEND_MESSAGE_URL = "https://api.weixin.qq.com/cgi-bin/message/template/subscribe";
+
+
+ private WxMpService wxMpService;
+
+ public WxMpSubscribeMsgServiceImpl(WxMpService wxMpService) {
+ this.wxMpService = wxMpService;
+ }
+
+ @Override
+ public String subscribeMsgAuthorizationUrl(String redirectURI, int scene, String reserved) {
+ WxMpConfigStorage storage = this.wxMpService.getWxMpConfigStorage();
+ return String.format(SUBSCRIBE_MESSAGE_AUTHORIZE_URL,
+ storage.getAppId(), scene, storage.getTemplateId(), URIUtil.encodeURIComponent(redirectURI), reserved);
+ }
+
+ @Override
+ public boolean sendSubscribeMessage(WxMpSubscribeMessage message) throws WxErrorException {
+ if (message.getTemplateId() == null) {
+ message.setTemplateId(this.wxMpService.getWxMpConfigStorage().getTemplateId());
+ }
+
+ String responseContent = this.wxMpService.post(SEND_MESSAGE_URL, message.toJson());
+ return responseContent != null;
+ }
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/subscribe/WxMpSubscribeMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/subscribe/WxMpSubscribeMessage.java
new file mode 100644
index 00000000..62e5e0d6
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/subscribe/WxMpSubscribeMessage.java
@@ -0,0 +1,72 @@
+package me.chanjar.weixin.mp.bean.subscribe;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.mp.bean.template.WxMpTemplateMessage;
+import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
+
+/**
+ * @author Mklaus
+ * @date 2018-01-22 下午12:18
+ */
+@Data
+@NoArgsConstructor
+@Builder
+@AllArgsConstructor
+public class WxMpSubscribeMessage {
+
+ /**
+ * 接收者openid.
+ */
+ private String toUser;
+
+ /**
+ * 模板ID.
+ */
+ private String templateId;
+
+ /**
+ * 模板跳转链接.
+ *
+ * url和miniprogram都是非必填字段,若都不传则模板无跳转;若都传,会优先跳转至小程序。
+ * 开发者可根据实际需要选择其中一种跳转方式即可。当用户的微信客户端版本不支持跳小程序时,将会跳转至url。
+ *
+ */
+ private String url;
+
+ /**
+ * 跳小程序所需数据,不需跳小程序可不用传该数据.
+ *
+ * @see #url
+ */
+ private WxMpTemplateMessage.MiniProgram miniProgram;
+
+ /**
+ * 订阅场景值
+ */
+ private String scene;
+
+ /**
+ * 消息标题 (15字以内)
+ */
+ private String title;
+
+ /**
+ * 消息内容文本 (200字以内)
+ */
+ private String contentValue;
+
+ /**
+ * 消息内容文本颜色
+ */
+ private String contentColor;
+
+
+ public String toJson() {
+ return WxMpGsonBuilder.INSTANCE.create().toJson(this);
+ }
+
+
+}
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/json/WxMpGsonBuilder.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/json/WxMpGsonBuilder.java
index 124845ed..524e1531 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/json/WxMpGsonBuilder.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/json/WxMpGsonBuilder.java
@@ -10,6 +10,7 @@ import me.chanjar.weixin.mp.bean.membercard.WxMpMemberCardUpdateResult;
import me.chanjar.weixin.mp.bean.membercard.WxMpMemberCardUserInfoResult;
import me.chanjar.weixin.mp.bean.material.*;
import me.chanjar.weixin.mp.bean.result.*;
+import me.chanjar.weixin.mp.bean.subscribe.WxMpSubscribeMessage;
import me.chanjar.weixin.mp.bean.template.WxMpTemplateIndustry;
import me.chanjar.weixin.mp.bean.template.WxMpTemplateMessage;
@@ -30,6 +31,7 @@ public class WxMpGsonBuilder {
INSTANCE.registerTypeAdapter(WxMpMassUploadResult.class, new WxMpMassUploadResultAdapter());
INSTANCE.registerTypeAdapter(WxMpQrCodeTicket.class, new WxQrCodeTicketAdapter());
INSTANCE.registerTypeAdapter(WxMpTemplateMessage.class, new WxMpTemplateMessageGsonAdapter());
+ INSTANCE.registerTypeAdapter(WxMpSubscribeMessage.class, new WxMpSubscribeMessageGsonAdapter());
INSTANCE.registerTypeAdapter(WxMpSemanticQueryResult.class, new WxMpSemanticQueryResultAdapter());
INSTANCE.registerTypeAdapter(WxMpOAuth2AccessToken.class, new WxMpOAuth2AccessTokenAdapter());
INSTANCE.registerTypeAdapter(WxDataCubeUserSummary.class, new WxMpUserSummaryGsonAdapter());
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/json/WxMpSubscribeMessageGsonAdapter.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/json/WxMpSubscribeMessageGsonAdapter.java
new file mode 100644
index 00000000..367416db
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/json/WxMpSubscribeMessageGsonAdapter.java
@@ -0,0 +1,54 @@
+package me.chanjar.weixin.mp.util.json;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import me.chanjar.weixin.mp.bean.subscribe.WxMpSubscribeMessage;
+
+import java.lang.reflect.Type;
+
+/**
+ * @author Mklaus
+ * @date 2018-01-22 下午12:31
+ */
+public class WxMpSubscribeMessageGsonAdapter implements JsonSerializer {
+
+ @Override
+ public JsonElement serialize(WxMpSubscribeMessage message, Type type, JsonSerializationContext jsonSerializationContext) {
+ JsonObject messageJson = new JsonObject();
+ messageJson.addProperty("touser", message.getToUser());
+ messageJson.addProperty("template_id", message.getTemplateId());
+
+ if (message.getUrl() != null) {
+ messageJson.addProperty("url", message.getUrl());
+ }
+
+ if (message.getMiniProgram() != null) {
+ JsonObject miniProgramJson = new JsonObject();
+ miniProgramJson.addProperty("appid", message.getMiniProgram().getAppid());
+ miniProgramJson.addProperty("pagepath", message.getMiniProgram().getPagePath());
+ messageJson.add("miniprogram", miniProgramJson);
+ }
+
+ messageJson.addProperty("scene", message.getScene());
+ messageJson.addProperty("title", message.getTitle());
+
+ JsonObject data = new JsonObject();
+ messageJson.add("data", data);
+
+ JsonObject content = new JsonObject();
+ data.add("content", content);
+
+ if (message.getContentValue() != null) {
+ content.addProperty("value", message.getContentValue());
+ }
+
+ if (message.getContentColor() != null) {
+ content.addProperty("color", message.getContentColor());
+ }
+
+ return messageJson;
+
+ }
+}
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpSubscribeMsgServiceImplTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpSubscribeMsgServiceImplTest.java
new file mode 100644
index 00000000..3db67ef6
--- /dev/null
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpSubscribeMsgServiceImplTest.java
@@ -0,0 +1,48 @@
+package me.chanjar.weixin.mp.api.impl;
+
+import com.google.inject.Inject;
+import me.chanjar.weixin.common.exception.WxErrorException;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.api.test.ApiTestModule;
+import me.chanjar.weixin.mp.api.test.TestConfigStorage;
+import me.chanjar.weixin.mp.bean.subscribe.WxMpSubscribeMessage;
+import org.testng.Assert;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+/**
+ * @author Mklaus
+ * @date 2018-01-22 下午2:02
+ */
+@Guice(modules = ApiTestModule.class)
+public class WxMpSubscribeMsgServiceImplTest {
+
+ @Inject
+ protected WxMpService wxService;
+
+ @Test
+ public void testSendSubscribeMessage() throws WxErrorException {
+ TestConfigStorage configStorage = (TestConfigStorage) this.wxService
+ .getWxMpConfigStorage();
+
+ WxMpSubscribeMessage message = WxMpSubscribeMessage.builder()
+ .title("weixin test")
+ .toUser(configStorage.getOpenid())
+ .scene("1000")
+ .contentColor("#FF0000")
+ .contentValue("Send subscribe message test")
+ .build();
+
+ try {
+ boolean send = this.wxService.getSubscribeMsgService().sendSubscribeMessage(message);
+ Assert.assertTrue(send);
+ } catch (WxErrorException e) {
+ // 当用户没有授权,获取之前的授权已使用。微信会返回错误代码 {"errcode":43101,"errmsg":"user refuse to accept the msg hint: [xxxxxxxxxxx]"}
+ if (e.getError().getErrorCode() != 43101) {
+ throw e;
+ }
+ }
+
+ }
+
+}
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/subscribe/WxMpSubscribeMessageTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/subscribe/WxMpSubscribeMessageTest.java
new file mode 100644
index 00000000..5cdc89d4
--- /dev/null
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/subscribe/WxMpSubscribeMessageTest.java
@@ -0,0 +1,46 @@
+package me.chanjar.weixin.mp.bean.subscribe;
+
+import me.chanjar.weixin.mp.bean.template.WxMpTemplateMessage;
+import org.testng.annotations.Test;
+import static org.testng.AssertJUnit.assertEquals;
+
+/**
+ * @author Mklaus
+ * @date 2018-01-22 下午1:41
+ */
+public class WxMpSubscribeMessageTest {
+ @Test
+ public void testToJson() {
+ String actual = "{" +
+ "\"touser\":\"OPENID\"," +
+ "\"template_id\":\"TEMPLATE_ID\"," +
+ "\"url\":\"URL\"," +
+ "\"miniprogram\":{" +
+ "\"appid\":\"xiaochengxuappid12345\"," +
+ "\"pagepath\":\"index?foo=bar\"" +
+ "}," +
+ "\"scene\":\"SCENE\"," +
+ "\"title\":\"TITLE\"," +
+ "\"data\":{" +
+ "\"content\":{" +
+ "\"value\":\"VALUE\"," +
+ "\"color\":\"COLOR\"" +
+ "}" +
+ "}" +
+ "}";
+
+ WxMpSubscribeMessage message = WxMpSubscribeMessage.builder()
+ .toUser("OPENID")
+ .templateId("TEMPLATE_ID")
+ .url("URL")
+ .miniProgram(new WxMpTemplateMessage.MiniProgram("xiaochengxuappid12345", "index?foo=bar"))
+ .scene("SCENE")
+ .title("TITLE")
+ .contentValue("VALUE")
+ .contentColor("COLOR")
+ .build();
+
+ assertEquals(message.toJson(), actual);
+
+ }
+}
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/WxMpDemoInMemoryConfigStorage.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/WxMpDemoInMemoryConfigStorage.java
index e42a192b..bf0c3142 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/WxMpDemoInMemoryConfigStorage.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/WxMpDemoInMemoryConfigStorage.java
@@ -22,7 +22,7 @@ class WxMpDemoInMemoryConfigStorage extends WxMpInMemoryConfigStorage {
@Override
public String toString() {
return "SimpleWxConfigProvider [appId=" + this.appId + ", secret=" + this.secret + ", accessToken=" + this.accessToken
- + ", expiresTime=" + this.expiresTime + ", token=" + this.token + ", aesKey=" + this.aesKey + "]";
+ + ", expiresTime=" + this.expiresTime + ", token=" + this.token + ", aesKey=" + this.aesKey + ", templateId=" + this.templateId + "]";
}
}
diff --git a/weixin-java-open/src/main/java/me/chanjar/weixin/open/api/impl/WxOpenInMemoryConfigStorage.java b/weixin-java-open/src/main/java/me/chanjar/weixin/open/api/impl/WxOpenInMemoryConfigStorage.java
index 3ec6d5ce..c0c65270 100644
--- a/weixin-java-open/src/main/java/me/chanjar/weixin/open/api/impl/WxOpenInMemoryConfigStorage.java
+++ b/weixin-java-open/src/main/java/me/chanjar/weixin/open/api/impl/WxOpenInMemoryConfigStorage.java
@@ -342,6 +342,11 @@ public class WxOpenInMemoryConfigStorage implements WxOpenConfigStorage {
return wxOpenConfigStorage.getComponentToken();
}
+ @Override
+ public String getTemplateId() {
+ return null;
+ }
+
@Override
public long getExpiresTime() {
return 0;