| @@ -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) | |||
| @@ -85,6 +85,8 @@ public interface WxMpConfigStorage { | |||
| String getAesKey(); | |||
| String getTemplateId(); | |||
| long getExpiresTime(); | |||
| String getOauth2redirectUri(); | |||
| @@ -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; | |||
| @@ -363,6 +363,13 @@ public interface WxMpService { | |||
| */ | |||
| 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 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<H, P> implements WxMpService, Requ | |||
| return this.templateMsgService; | |||
| } | |||
| @Override | |||
| public WxMpSubscribeMsgService getSubscribeMsgService() { | |||
| return this.subscribeMsgService; | |||
| } | |||
| @Override | |||
| public WxMpDeviceService getDeviceService() { | |||
| 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.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()); | |||
| @@ -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 | |||
| 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 + "]"; | |||
| } | |||
| } | |||
| @@ -342,6 +342,11 @@ public class WxOpenInMemoryConfigStorage implements WxOpenConfigStorage { | |||
| return wxOpenConfigStorage.getComponentToken(); | |||
| } | |||
| @Override | |||
| public String getTemplateId() { | |||
| return null; | |||
| } | |||
| @Override | |||
| public long getExpiresTime() { | |||
| return 0; | |||