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