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