微信一次性订阅消息
开发者可以通过一次性订阅消息授权让微信用户授权第三方移动应用(接入说明)或公众号,获得发送一次订阅消息给到授权微信用户的机会。授权微信用户可以不需要关注公众号。微信用户每授权一次,开发者可获得一次下发消息的权限。对于已关注公众号的,消息将下发到公众号会话;未关注公众号的,将下发到服务通知。
本篇文章主要讨论公众号
1、确认是否有权限
已认证的公众号即有权限,可登陆公众平台在接口权限列表处查看(如下图)。目前测试号还无法测试一次性订阅消息

2、配置相关的参数
- 查看AppId以及AppSecret (发送一次性订阅消息需要ACCESS_TOKEN、获取ACCESS_TOKEN需要使用到)
- 配置回调域名



3、授权发送一次性订阅消息
具体的参数看文档这里就不详细介绍了。
public static String getAuthorizeURL(String appId, String scene, String template_id,String redirectUri, String reserved) throws UnsupportedEncodingException {
StringBuffer sbf = new StringBuffer();
sbf.append(authorize_uri).append("&appid=").append(appId)
.append("&scene=").append(scene)
.append("&template_id=").append(template_id)
.append("&redirect_uri=").append(URLEncoder.encode(redirectUri, Charsets.UTF_8.name()).replace("+", "%20"));
if (StrKit.notBlank(reserved)) {
sbf.append("&reserved=").append(reserved);
}
sbf.append("#wechat_redirect");
return sbf.toString();
}
用户同意或取消授权后会返回相关信息
如果用户点击同意或取消授权,页面将跳转至:
redirect_url/?openid=OPENID&template_id=TEMPLATE_ID&action=ACTION&scene=SCENE

4、通过API推送订阅模板消息给到授权微信用户
http请求方式: post
https://api.weixin.qq.com/cgi-bin/message/template/subscribe?access_token=ACCESS_TOKEN
{
"touser": "OPENID",
"template_id": "TEMPLATE_ID",
"url": "URL",
"scene": "SCENE",
"title": "TITLE",
"data": {
"content": {
"value": "VALUE",
"color": "COLOR"
}
}
}

具体封装代码如下:
/**
* 发送一次性订阅消息
*
* @param jsonStr
* json字符串
* @return ApiResult
*
*/
public static ApiResult subscribe(String jsonStr) {
String jsonResult = HttpUtils.post(subscribe + AccessTokenApi.getAccessTokenStr(), jsonStr);
return new ApiResult(jsonResult);
}
public static ApiResult subscribe(SubscribeInfo subscribeInfo) {
return new ApiResult(JsonKit.toJson(subscribeInfo));
}
public static ApiResult subscribe(String openId, String templateId, String url, int scene, String title,
String value, String color) {
SubscribeInfo subscribeInfo = new SubscribeInfo.Builder()
.setTouser(openId).setTemplate_id(templateId).setUrl(url)
.setScene(String.valueOf(scene)).setTitle(title)
.setData(new Data.Builder()
.setContent(new Content.Builder()
.setColor(color).setValue(value)
.create())
.create())
.create();
System.out.println(JsonUtils.toJson(subscribeInfo));
return subscribe(JsonUtils.toJson(subscribeInfo));
}
Builder模式构建请求参数的json对象
class SubscribeInfo {
private String touser;
private String template_id;
private String url;
private String scene;
private String title;
private Data data;
public static class Builder{
private String touser;
private String template_id;
private String url;
private String scene;
private String title;
private Data data;
public Builder setTouser(String touser) {
this.touser = touser;
return this;
}
public Builder setTemplate_id(String template_id) {
this.template_id = template_id;
return this;
}
public Builder setUrl(String url) {
this.url = url;
return this;
}
public Builder setScene(String scene) {
this.scene = scene;
return this;
}
public Builder setTitle(String title) {
this.title = title;
return this;
}
public Builder setData(Data data) {
this.data = data;
return this;
}
public SubscribeInfo create(){
return new SubscribeInfo(this);
}
}
private SubscribeInfo(Builder builder) {
if (StrKit.isBlank(builder.touser)) {
throw new IllegalStateException("touser is null");
}
if (StrKit.isBlank(builder.template_id)) {
throw new IllegalStateException("template_id is null");
}
if (StrKit.isBlank(builder.url)) {
throw new IllegalStateException("url is null");
}
if (StrKit.isBlank(builder.scene)) {
throw new IllegalStateException("scene is null");
}
if (StrKit.isBlank(builder.title)) {
throw new IllegalStateException("title is null");
}
if (!StrKit.notNull(builder.data)) {
throw new IllegalStateException("data is null");
}
this.touser = builder.touser;
this.template_id = builder.template_id;
this.url = builder.url;
this.scene = builder.scene;
this.title = builder.title;
this.data = builder.data;
}
public String getTouser() {
return touser;
}
public String getTemplate_id() {
return template_id;
}
public String getUrl() {
return url;
}
public String getScene() {
return scene;
}
public String getTitle() {
return title;
}
public Data getData() {
return data;
}
}
class Data {
private Content content;
public static class Builder {
private Content content;
public Builder setContent(Content content) {
this.content = content;
return this;
}
public Data create(){
return new Data(this);
}
}
private Data(Builder builder) {
if (!StrKit.notNull(builder.content)) {
throw new IllegalStateException("content is null");
}
this.content = builder.content;
}
public Content getContent() {
return content;
}
}
class Content {
private String value;
private String color;
public static class Builder{
private String value;
private String color;
public Builder setValue(String value) {
this.value = value;
return this;
}
public Builder setColor(String color) {
this.color = color;
return this;
}
public Content create(){
return new Content(this);
}
}
private Content(Builder builder) {
if (StrKit.isBlank(builder.value)) {
throw new IllegalStateException("value is null");
}
if (StrKit.isBlank(builder.color)) {
throw new IllegalStateException("color is null");
}
this.value = builder.value;
this.color = builder.color;
}
public String getValue() {
return value;
}
public String getColor() {
return color;
}
}
5、遗留问题
1、授权后页面跳转无效redirect_url
2、发送一次性订阅消息提示没有权限(认证的服务号)
{"errcode":48001,"errmsg":"api unauthorized hint: [uAi6Za0855sz10!]"}
猜测:应该是官方接口存在问题
推荐阅读
10分钟搭建属于自己的ngork服务器,实现内网穿透
极速开发微信公众号
IJPay让支付触手可及
微信、支付宝App支付
不要让昨天的沮丧 让今天的梦想黯然失色
成功的人总是修改方法而不修改目标
微信公众号:javenlife
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 解答了困扰我五年的技术问题
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 用 C# 插值字符串处理器写一个 sscanf
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
· DeepSeek 解答了困扰我五年的技术问题。时代确实变了!
· PPT革命!DeepSeek+Kimi=N小时工作5分钟完成?
· What?废柴, 还在本地部署DeepSeek吗?Are you kidding?
· DeepSeek企业级部署实战指南:从服务器选型到Dify私有化落地
· 程序员转型AI:行业分析