@@ -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-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-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-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: | 1. 公众号Demo: | ||||
- 使用Spring MVC实现的公众号Demo:[码云](http://gitee.com/binary/weixin-java-mp-demo)、[GitHub](http://github.com/binarywang/weixin-java-mp-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) | - 使用Spring Boot实现的公众号Demo:[码云](http://gitee.com/binary/weixin-java-mp-demo-springboot)、[GitHub](http://github.com/binarywang/weixin-java-mp-demo-springboot) | ||||
@@ -85,6 +85,8 @@ public interface WxMpConfigStorage { | |||||
String getAesKey(); | String getAesKey(); | ||||
String getTemplateId(); | |||||
long getExpiresTime(); | long getExpiresTime(); | ||||
String getOauth2redirectUri(); | String getOauth2redirectUri(); | ||||
@@ -18,6 +18,7 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||||
protected volatile String appId; | protected volatile String appId; | ||||
protected volatile String secret; | protected volatile String secret; | ||||
protected volatile String token; | protected volatile String token; | ||||
protected volatile String templateId; | |||||
protected volatile String accessToken; | protected volatile String accessToken; | ||||
protected volatile String aesKey; | protected volatile String aesKey; | ||||
protected volatile long expiresTime; | protected volatile long expiresTime; | ||||
@@ -173,6 +174,15 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||||
this.token = token; | this.token = token; | ||||
} | } | ||||
@Override | |||||
public String getTemplateId() { | |||||
return this.templateId; | |||||
} | |||||
public void setTemplateId(String templateId) { | |||||
this.templateId = templateId; | |||||
} | |||||
@Override | @Override | ||||
public long getExpiresTime() { | public long getExpiresTime() { | ||||
return this.expiresTime; | return this.expiresTime; | ||||
@@ -363,6 +363,13 @@ public interface WxMpService { | |||||
*/ | */ | ||||
WxMpTemplateMsgService getTemplateMsgService(); | WxMpTemplateMsgService getTemplateMsgService(); | ||||
/** | |||||
* 返回一次性订阅消息相关接口方法的实现类对象,以方便调用其各个接口 | |||||
* | |||||
* @return WxMpSubscribeMsgService | |||||
*/ | |||||
WxMpSubscribeMsgService getSubscribeMsgService(); | |||||
/** | /** | ||||
* 返回硬件平台相关接口方法的实现类对象,以方便调用其各个接口 | * 返回硬件平台相关接口方法的实现类对象,以方便调用其各个接口 | ||||
* | * | ||||
@@ -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; | |||||
/** | |||||
* <pre> | |||||
* 一次性订阅消息接口 | |||||
* https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1500374289_66bvB | |||||
* </pre> | |||||
* | |||||
* @author Mklaus | |||||
* @date 2018-01-22 上午11:07 | |||||
*/ | |||||
public interface WxMpSubscribeMsgService { | |||||
/** | |||||
* <pre> | |||||
* 构造用户订阅一条模板消息授权的url连接 | |||||
* 详情请见: https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1500374289_66bvB | |||||
* </pre> | |||||
* | |||||
* @param redirectURI 用户授权完成后的重定向链接,无需urlencode, 方法内会进行encode | |||||
* @param scene 重定向后会带上scene参数,开发者可以填0-10000的整形值,用来标识订阅场景值 | |||||
* @param reserved 用于保持请求和回调的状态,授权请后原样带回给第三方 (最多128字节,要求做urlencode) | |||||
* @return url | |||||
*/ | |||||
String subscribeMsgAuthorizationUrl(String redirectURI, int scene, String reserved); | |||||
/** | |||||
* <pre> | |||||
* 发送一次性订阅消息 | |||||
* 详情请见: https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1500374289_66bvB | |||||
* </pre> | |||||
* | |||||
* @return 消息Id | |||||
*/ | |||||
boolean sendSubscribeMessage(WxMpSubscribeMessage message) throws WxErrorException; | |||||
} |
@@ -40,6 +40,7 @@ public abstract class WxMpServiceAbstractImpl<H, P> implements WxMpService, Requ | |||||
private WxMpDataCubeService dataCubeService = new WxMpDataCubeServiceImpl(this); | private WxMpDataCubeService dataCubeService = new WxMpDataCubeServiceImpl(this); | ||||
private WxMpUserBlacklistService blackListService = new WxMpUserBlacklistServiceImpl(this); | private WxMpUserBlacklistService blackListService = new WxMpUserBlacklistServiceImpl(this); | ||||
private WxMpTemplateMsgService templateMsgService = new WxMpTemplateMsgServiceImpl(this); | private WxMpTemplateMsgService templateMsgService = new WxMpTemplateMsgServiceImpl(this); | ||||
private WxMpSubscribeMsgService subscribeMsgService = new WxMpSubscribeMsgServiceImpl(this); | |||||
private WxMpDeviceService deviceService = new WxMpDeviceServiceImpl(this); | private WxMpDeviceService deviceService = new WxMpDeviceServiceImpl(this); | ||||
private WxMpShakeService shakeService = new WxMpShakeServiceImpl(this); | private WxMpShakeService shakeService = new WxMpShakeServiceImpl(this); | ||||
private WxMpMemberCardService memberCardService = new WxMpMemberCardServiceImpl(this); | private WxMpMemberCardService memberCardService = new WxMpMemberCardServiceImpl(this); | ||||
@@ -375,6 +376,11 @@ public abstract class WxMpServiceAbstractImpl<H, P> implements WxMpService, Requ | |||||
return this.templateMsgService; | return this.templateMsgService; | ||||
} | } | ||||
@Override | |||||
public WxMpSubscribeMsgService getSubscribeMsgService() { | |||||
return this.subscribeMsgService; | |||||
} | |||||
@Override | @Override | ||||
public WxMpDeviceService getDeviceService() { | public WxMpDeviceService getDeviceService() { | ||||
return this.deviceService; | return this.deviceService; | ||||
@@ -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; | |||||
} | |||||
} |
@@ -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; | |||||
/** | |||||
* 模板跳转链接. | |||||
* <pre> | |||||
* url和miniprogram都是非必填字段,若都不传则模板无跳转;若都传,会优先跳转至小程序。 | |||||
* 开发者可根据实际需要选择其中一种跳转方式即可。当用户的微信客户端版本不支持跳小程序时,将会跳转至url。 | |||||
* </pre> | |||||
*/ | |||||
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); | |||||
} | |||||
} |
@@ -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.membercard.WxMpMemberCardUserInfoResult; | ||||
import me.chanjar.weixin.mp.bean.material.*; | import me.chanjar.weixin.mp.bean.material.*; | ||||
import me.chanjar.weixin.mp.bean.result.*; | 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.WxMpTemplateIndustry; | ||||
import me.chanjar.weixin.mp.bean.template.WxMpTemplateMessage; | import me.chanjar.weixin.mp.bean.template.WxMpTemplateMessage; | ||||
@@ -30,6 +31,7 @@ public class WxMpGsonBuilder { | |||||
INSTANCE.registerTypeAdapter(WxMpMassUploadResult.class, new WxMpMassUploadResultAdapter()); | INSTANCE.registerTypeAdapter(WxMpMassUploadResult.class, new WxMpMassUploadResultAdapter()); | ||||
INSTANCE.registerTypeAdapter(WxMpQrCodeTicket.class, new WxQrCodeTicketAdapter()); | INSTANCE.registerTypeAdapter(WxMpQrCodeTicket.class, new WxQrCodeTicketAdapter()); | ||||
INSTANCE.registerTypeAdapter(WxMpTemplateMessage.class, new WxMpTemplateMessageGsonAdapter()); | INSTANCE.registerTypeAdapter(WxMpTemplateMessage.class, new WxMpTemplateMessageGsonAdapter()); | ||||
INSTANCE.registerTypeAdapter(WxMpSubscribeMessage.class, new WxMpSubscribeMessageGsonAdapter()); | |||||
INSTANCE.registerTypeAdapter(WxMpSemanticQueryResult.class, new WxMpSemanticQueryResultAdapter()); | INSTANCE.registerTypeAdapter(WxMpSemanticQueryResult.class, new WxMpSemanticQueryResultAdapter()); | ||||
INSTANCE.registerTypeAdapter(WxMpOAuth2AccessToken.class, new WxMpOAuth2AccessTokenAdapter()); | INSTANCE.registerTypeAdapter(WxMpOAuth2AccessToken.class, new WxMpOAuth2AccessTokenAdapter()); | ||||
INSTANCE.registerTypeAdapter(WxDataCubeUserSummary.class, new WxMpUserSummaryGsonAdapter()); | INSTANCE.registerTypeAdapter(WxDataCubeUserSummary.class, new WxMpUserSummaryGsonAdapter()); | ||||
@@ -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<WxMpSubscribeMessage> { | |||||
@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; | |||||
} | |||||
} |
@@ -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; | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -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); | |||||
} | |||||
} |
@@ -22,7 +22,7 @@ class WxMpDemoInMemoryConfigStorage extends WxMpInMemoryConfigStorage { | |||||
@Override | @Override | ||||
public String toString() { | public String toString() { | ||||
return "SimpleWxConfigProvider [appId=" + this.appId + ", secret=" + this.secret + ", accessToken=" + this.accessToken | 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 + "]"; | |||||
} | } | ||||
} | } |
@@ -342,6 +342,11 @@ public class WxOpenInMemoryConfigStorage implements WxOpenConfigStorage { | |||||
return wxOpenConfigStorage.getComponentToken(); | return wxOpenConfigStorage.getComponentToken(); | ||||
} | } | ||||
@Override | |||||
public String getTemplateId() { | |||||
return null; | |||||
} | |||||
@Override | @Override | ||||
public long getExpiresTime() { | public long getExpiresTime() { | ||||
return 0; | return 0; | ||||