第一章 微信公众号-订阅号发送群发消息

文章目录

  • 第一章 微信公众号-订阅号发送群发消息
  • 前言
  • 一、微信官方文档介绍
  • 二、使用步骤
    • 1.获取 微信公共参数 access_token
    • 2. 上传素材,获取缩略媒资id thumb_media_id
    • 3、上传图文信息,获取素材media_id
    • 4、发布消息
      • 1、文档内容截取
      • 2、代码示例
      • 3、业务调取 封装接口 代码示例
  • 总结

前言

在公众平台网站上,为订阅号提供了每天一条的群发权限,为服务号提供每月(自然月)4条的群发权限。而对于某些具备开发能力的公众号运营者,可以通过高级群发接口,实现更灵活的群发能力。

很多开发者开发时云里雾里,这里总结一下 公众号-订阅号群发功能的开发实现过程。。。


一、微信官方文档介绍

示例:接口文档地址:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Batch_Sends_and_Originality_Checks.html#0

在公众平台网站上,为订阅号提供了每天一条的群发权限,为服务号提供每月(自然月)4条的群发权限。而对于某些具备开发能力的公众号运营者,可以通过高级群发接口,实现更灵活的群发能力。

请注意:

对于认证订阅号,群发接口每天可成功调用1次,此次群发可选择发送给全部用户或某个标签;
对于认证服务号虽然开发者使用高级群发接口的每日调用限制为100次,但是用户每月只能接收4条,无论在公众平台网站上,还是使用接口群发,用户每月只能接收4条群发消息,多于4条的群发将对该用户发送失败;
开发者可以使用预览接口校对消息样式和排版,通过预览接口可发送编辑好的消息给指定用户校验效果;
群发过程中,微信后台会自动进行图文消息原创校验,请提前设置好相关参数(send_ignore等);
开发者可以主动设置 clientmsgid 来避免重复推送。
群发接口每分钟限制请求60次,超过限制的请求会被拒绝。
图文消息正文中插入自己帐号和其他公众号已群发文章链接的能力。
对于已开启 API 群发保护的账号,群发全部用户时需要等待管理员进行确认,如管理员拒绝或30分钟内没有确认,该次群发失败。用户可通过“设置 - 安全中心 - 风险操作保护”中关闭 API 群发保护功能。
群发图文消息的过程如下:

首先,预先将图文消息中需要用到的图片,使用上传图文消息内图片接口,上传成功并获得图片 URL;
上传图文消息素材,需要用到图片时,请使用上一步获取的图片 URL;
使用对用户标签的群发,或对 OpenID 列表的群发,将图文消息群发出去,群发时微信会进行原创校验,并返回群发操作结果;
在上述过程中,如果需要,还可以预览图文消息、查询群发状态,或删除已群发的消息等。
群发图片、文本等其他消息类型的过程如下:

如果是群发文本消息,则直接根据下面的接口说明进行群发即可;
如果是群发图片、视频等消息,则需要预先通过素材管理接口准备好 mediaID。
关于群发时使用is_to_all为 true 使其进入公众号在微信客户端的历史消息列表:

使用is_to_all为 true 且成功群发,会使得此次群发进入历史消息列表,群发成功后 media_id 会失效,后台草稿也会被自动删除。
为防止异常,认证订阅号在一天内,只能使用is_to_all为 true 进行群发一次,或者在公众平台官网群发(不管本次群发是对全体还是对某个分组)一次。以避免一天内有2条群发进入历史消息列表。
类似地,服务号在一个月内,使用is_to_all为 true 群发的次数,加上公众平台官网群发(不管本次群发是对全体还是对某个分组)的次数,最多只能是4次。
设置is_to_all为 false 时是可以多次群发的,但每个用户只会收到最多4条,且这些群发不会进入历史消息列表。
另外,请开发者注意,本接口中所有使用到media_id的地方,现在都可以使用素材管理中的永久素材media_id了。请但注意,使用同一个素材群发出去的链接是一样的,这意味着,删除某一次群发,会导致整个链接失效。

此外,对于群发和预览接口中的图文消息 (mpnews) ,请使用通过 “草稿箱 / 新建草稿” 接口获得的 media_id

二、使用步骤

1.获取 微信公共参数 access_token

微信公众平台登录地址:https://mp.weixin.qq.com/cgi-bin/home?t=home/index&lang=zh_CN
前置条件
1、要拿到公众号的 appId 和 AppSecret

2、配置公众号IP白名单

获取access_token 代码(示例):

import cn.com.refratechnik.common.constant.CommonConstant;
import cn.com.refratechnik.common.constant.NumberConstants;
import cn.com.refratechnik.common.dto.PushMessageDto;
import cn.com.refratechnik.common.entity.*;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.druid.util.StringUtils;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;import java.io.File;
import java.util.ArrayList;
import java.util.List;@Slf4j
public class WeChatUtils {/*** 获取Access token* access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token* @return* @throws Exception*/public static String getWxAccessToken() throws Exception {String ACCESS_TOKEN_API = "https://api.weixin.qq.com/cgi-bin/token";String wx_appid = "你的appId";String wx_secret = "你的wx_secret";String getAccessTokenParam = "appid=" + wx_appid + "&secret=" + wx_secret + "&grant_type=client_credential";// 获取凭证String wechatResult = HttpUtils.get(ACCESS_TOKEN_API, getAccessTokenParam);JSONObject jsonObj = JSONObject.parseObject(wechatResult);// 解析微信返回消息String token = null;if (jsonObj != null) {token = (String) jsonObj.get("access_token");}if (StringUtils.isEmpty(token)) {System.err.println("******************** getWxAccessToken ********************");System.err.println("**   获取微信凭证失败");System.err.println("**   wx_appid:" + wx_appid);System.err.println("**   wx_secret:" + wx_secret);System.err.println("**   ACCESS_TOKEN_API:" + ACCESS_TOKEN_API);System.err.println("**   请求参数:" + getAccessTokenParam);System.err.println("**   返回结果:" + wechatResult);throw new Exception("调用微信接口,取 AccessToken 失败,返回结果:" + wechatResult);}return token;}
}

HttpUtils 工具类 代码(示例 ):

package cn.com.aaa.common.util;import com.alibaba.druid.util.StringUtils;
import com.google.common.base.Throwables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import javax.net.ssl.*;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;public class HttpUtils {private static Logger logger = LoggerFactory.getLogger(HttpUtils.class);/*** 向指定URL发送GET方法的请求** @param url   发送请求的URL* @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。* @return URL 所代表远程资源的响应结果*/public static String get(String url, String param) {String result = "";BufferedReader in = null;try {String api = "";if (StringUtils.isEmpty(param)) {api = url;} else {api = url + "?" + param;}URL realUrl = new URL(api);// 打开和URL之间的连接if (api.startsWith("https://")) {HttpsURLConnection httpConn = (HttpsURLConnection) realUrl.openConnection();SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");//第一个参数为 返回实现指定安全套接字协议的SSLContext对象。第二个为提供者TrustManager[] tm = {new MyX509TrustManager()};sslContext.init(null, tm, new SecureRandom());SSLSocketFactory ssf = sslContext.getSocketFactory();httpConn.setSSLSocketFactory(ssf);// 设置通用的请求属性httpConn.setRequestProperty("accept", "*/*");httpConn.setRequestProperty("connection", "Keep-Alive");httpConn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");// 建立实际的连接httpConn.connect();// 获取所有响应头字段Map<String, List<String>> map = httpConn.getHeaderFields();// 定义 BufferedReader输入流来读取URL的响应in = new BufferedReader(new InputStreamReader(httpConn.getInputStream()));String line;while ((line = in.readLine()) != null) {result += line;}} else {URLConnection httpConn = realUrl.openConnection();// 设置通用的请求属性httpConn.setRequestProperty("accept", "*/*");httpConn.setRequestProperty("connection", "Keep-Alive");httpConn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");// 建立实际的连接httpConn.connect();// 获取所有响应头字段Map<String, List<String>> map = httpConn.getHeaderFields();// 遍历所有的响应头字段for (String key : map.keySet()) {System.out.println(key + "--->" + map.get(key));}// 定义 BufferedReader输入流来读取URL的响应in = new BufferedReader(new InputStreamReader(httpConn.getInputStream()));String line;while ((line = in.readLine()) != null) {result += line;}}} catch (Exception e) {System.out.println("发送GET请求出现异常!" + e);e.printStackTrace();}// 使用finally块来关闭输入流finally {try {if (in != null) {in.close();}} catch (Exception e2) {e2.printStackTrace();}}return result;}/*** POST 请求** @param url    http请求地址* @param params http请求参数* @return String*/public static ApiResponseFile sendPost(String url, String params) {PrintWriter pw = null;BufferedReader br = null;ApiResponseFile response = new ApiResponseFile();try {HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();// 设置请求头connection.setRequestProperty("accept", "*/*");connection.setRequestProperty("connection", "Keep-Alive");connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");// 设置 POSTconnection.setRequestMethod("POST");connection.setDoOutput(true);connection.setDoInput(true);// Post 请求不能使用缓存connection.setUseCaches(false);connection.setConnectTimeout(5000);// 获取URL的输出流, 发送请求参数pw = new PrintWriter(connection.getOutputStream());pw.print(params);pw.flush();// 获取请求头字段Map<String, List<String>> header = connection.getHeaderFields();// 获取URL的输入流,读取请求响应String body = readString(connection.getInputStream());response.setHeader(header);response.setBody(body);} catch (Exception e) {logger.error("发送POST请求出现异常!, cause;{}", Throwables.getStackTraceAsString(e));} finally {try {if (pw != null) {pw.close();}} catch (Exception e) {e.printStackTrace();}}return response;}private static String readString(InputStream is) {BufferedReader br = null;String content = "";try {br = new BufferedReader(new InputStreamReader(is, "utf-8"));String line;while ((line = br.readLine()) != null) {content += line;}} catch (IOException e) {e.printStackTrace();} finally {try {if (br != null) {br.close();}} catch (Exception e) {e.printStackTrace();}}return content;}}class MyX509TrustManager implements X509TrustManager {@Overridepublic void checkClientTrusted(X509Certificate[] chain, String authType)throws CertificateException {}@Overridepublic void checkServerTrusted(X509Certificate[] chain, String authType)throws CertificateException {}@Overridepublic X509Certificate[] getAcceptedIssuers() {return null;}
}
/*** Http 请求响应*/
class ApiResponseFile {private Map header;private Object body;public ApiResponseFile() {this.header = new TreeMap();this.body = "";}public ApiResponseFile(Map header, String body) {this.header = header;this.body = body;}public Map getHeader() {return this.header;}public void setHeader(Map header) {this.header = header;}public Object getBody() {return this.body;}public void setBody(Object body) {this.body = body;}
}

2. 上传素材,获取缩略媒资id thumb_media_id

图文消息缩略图的media_id,可以在素材管理 - 新增临时素材中获得

*** 新增临时素材 官方文档 ***

公众号经常有需要用到一些临时性的多媒体素材的场景,例如在使用接口特别是发送消息时,对多媒体文件、多媒体消息的获取和调用等操作,是通过media_id来进行的。素材管理接口对所有认证的订阅号和服务号开放。通过本接口,公众号可以新增临时素材(即上传临时多媒体文件)。使用接口过程中有任何问题,可以前往微信开放社区 #公众号 专区发帖交流注意点:1、临时素材media_id是可复用的。2、媒体文件在微信后台保存时间为3天,即3天后media_id失效。3、上传临时素材的格式、大小限制与公众平台官网一致。图片(image): 10M,支持PNG\JPEG\JPG\GIF格式语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式视频(video):10MB,支持MP4格式缩略图(thumb):64KB,支持 JPG 格式4、需使用 https 调用本接口。接口调用请求说明http请求方式:POST/FORM,使用https https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE 调用示例(使用 curl 命令,用 FORM 表单方式上传一个多媒体文件): curl -F media=@test.jpg "https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE"参数说明参数    是否必须    说明
access_token    是   调用接口凭证
type    是   媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb)
media   是   form-data中媒体文件标识,有filename、filelength、content-type等信息
返回说明正确情况下的返回 JSON 数据包结果如下:{"type":"TYPE","media_id":"MEDIA_ID","created_at":123456789}
参数  描述
type    媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb,主要用于视频与音乐格式的缩略图)
media_id    媒体文件上传后,获取标识
created_at  媒体文件上传时间戳
错误情况下的返回 JSON 数据包示例如下(示例为无效媒体类型错误):{"errcode":40004,"errmsg":"invalid media type"}
使用网页调试工具调试该接口

上传素材获取压缩媒资id 工具类 代码(示例 ):


import cn.com.refratechnik.common.constant.CommonConstant;
import cn.com.refratechnik.common.constant.NumberConstants;
import cn.com.refratechnik.common.dto.PushMessageDto;
import cn.com.refratechnik.common.entity.*;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.druid.util.StringUtils;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;import java.io.File;
import java.util.ArrayList;
import java.util.List;/*** @author wangxiaodong* @description: 微信订阅号发送一次性订阅消息工具类* @date 2022/03/25 15:13*/
@Slf4j
public class WeChatUtils {// 上传订阅号素材 获取 thumb_media_idpublic static String getThumbMediaId(MultipartFile media,String accessToken) throws Exception {if (StringUtils.isEmpty(accessToken)){accessToken = getWxAccessToken();}// 压缩素材文件 thumb_media_idString thumb_media_id = "";// 调取 上传素材接口Result<MdlUpload> result=FileUpload .Upload(accessToken,"thumb", media);if (ObjectUtil.isNotNull(result.getObj())){thumb_media_id = result.getObj().getThumb_media_id();}return thumb_media_id;}}

发送请求工具类 (代码示例)

package cn.com.refratechnik.common.util;
import java.io.*;import cn.com.refratechnik.common.constant.CommonConstant;
import cn.com.refratechnik.common.entity.MdlUpload;
import cn.com.refratechnik.common.entity.PushMpNews;
import cn.com.refratechnik.common.entity.Result;
import cn.com.refratechnik.common.entity.SendMpNews;
import cn.com.refratechnik.framework.exception.BusinessException;
import cn.hutool.http.HttpRequest;
import net.sf.json.JSONObject;
import org.springframework.http.MediaType;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.util.UriComponentsBuilder;/**** @author Sunlight**/
public class FileUpload {private static final String upload_url = "https://api.weixin.qq.com/cgi-bin/media/upload?";/*** 上传文件** @param accessToken* @param type* @param file* @return*/public static Result<MdlUpload> Upload(String accessToken, String type, MultipartFile file) {Result<MdlUpload> result = new Result<MdlUpload>();String url = handUrlParam(upload_url , accessToken ,type) ;JSONObject jsonObject;try {File media = MultipartFileToFile(file);HttpPostUtil post = new HttpPostUtil(url);post.addParameter("media", media);String s = post.send();jsonObject = JSONObject.fromObject(s);if (jsonObject.containsKey("thumb_media_id")) {MdlUpload upload=new MdlUpload();upload.setThumb_media_id(jsonObject.getString("thumb_media_id"));upload.setType(jsonObject.getString("type"));upload.setCreated_at(jsonObject.getString("created_at"));result.setObj(upload);result.setErrmsg("success");result.setErrcode("0");} else {result.setErrmsg(jsonObject.getString("errmsg"));result.setErrcode(jsonObject.getString("errcode"));}} catch (Exception e) {e.printStackTrace();result.setErrmsg("Upload Exception:"+e.toString());}return result;}/*** 上传图文素材接口* @param accessToken token* @param jsonNews 入参* @return 返回参数*/public static Result<PushMpNews> pushMpNews(String accessToken, String jsonNews) {Result<PushMpNews> result = new Result<PushMpNews>();String url = CommonConstant.TAG_SEND_MESSAGE_UPLOADNEWS + accessToken;JSONObject jsonObject;try {String s = HttpRequest.post(url).timeout(60000).body(jsonNews, MediaType.APPLICATION_JSON_UTF8_VALUE).execute().body();jsonObject = JSONObject.fromObject(s);if (jsonObject.containsKey("media_id")) {PushMpNews pushMpNews = new PushMpNews();pushMpNews.setMedia_id(jsonObject.getString("media_id"));pushMpNews.setType(jsonObject.getString("type"));pushMpNews.setCreated_at(jsonObject.getString("created_at"));result.setObj(pushMpNews);result.setErrmsg("success");result.setErrcode("0");} else {result.setErrmsg(jsonObject.getString("errmsg"));result.setErrcode(jsonObject.getString("errcode"));}} catch (Exception e) {e.printStackTrace();result.setErrmsg("Upload Exception:"+e.toString());}return result;}/*** 推送订阅群发消息* @param accessToken token* @param jsonNews 入参* @return 返回参数*/public static Result<SendMpNews> sendMpNews(String accessToken, String jsonNews) {Result<SendMpNews> result = new Result<SendMpNews>();String url = CommonConstant.TAG_SEND_MESSAGE + accessToken;JSONObject jsonObject;try {String s = HttpRequest.post(url).timeout(60000).body(jsonNews, MediaType.APPLICATION_JSON_UTF8_VALUE).execute().body();jsonObject = JSONObject.fromObject(s);if (jsonObject.containsKey("msg_id")) {SendMpNews sendMpNews = new SendMpNews();sendMpNews.setMsg_data_id(jsonObject.getString("msg_data_id"));sendMpNews.setMsg_id(jsonObject.getString("msg_id"));sendMpNews.setErrmsg(jsonObject.getString("errmsg"));result.setObj(sendMpNews);result.setErrmsg("success");result.setErrcode("0");} else {result.setErrmsg(jsonObject.getString("errmsg"));result.setErrcode(jsonObject.getString("errcode"));}} catch (Exception e) {e.printStackTrace();result.setErrmsg("Upload Exception:"+e.toString());}return result;}/*** 拼接url请求参数** @param httpUrl      请求url* @param accessToken  调用接口凭证* @param type 获取的部门id* @return*/public static String handUrlParam(String httpUrl, String accessToken, String type ) {UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(httpUrl).queryParam("access_token", accessToken).queryParam("type", type);return builder.build().encode().toUriString();}/*** MultipartFile 转 File** @param multipartFile* @throws Exception*/public static File MultipartFileToFile(MultipartFile multipartFile) {File file = null;//判断是否为nullif (multipartFile.equals("") || multipartFile.getSize() <= 0) {return file;}//MultipartFile转换为FileInputStream ins = null;OutputStream os = null;try {ins = multipartFile.getInputStream();file = new File(multipartFile.getOriginalFilename());os = new FileOutputStream(file);int bytesRead = 0;byte[] buffer = new byte[8192];while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) {os.write(buffer, 0, bytesRead);}} catch (IOException e) {e.printStackTrace();}finally {if(os != null){try {os.close();} catch (IOException e) {e.printStackTrace();}}if(ins != null){try {ins.close();} catch (IOException e) {e.printStackTrace();}}}return file;}}

thumb_media_id 获取 返回参数封装 类 (代码示例)

/*** Copyright (c) 2017-2020 Htwins* https://www.htwins.com.cn* All rights reserved.*/
package cn.com.refratechnik.common.entity;public class MdlUpload {private String type;private String thumb_media_id;private String created_at;public String getType() {return type;}public void setType(String type) {this.type = type;}public String getThumb_media_id() {return thumb_media_id;}public void setThumb_media_id(String mediaId) {thumb_media_id = mediaId;}public String getCreated_at() {return created_at;}public void setCreated_at(String createdAt) {created_at = createdAt;}public MdlUpload() {super();}@Overridepublic String toString() {return "MdlUpload [created_at=" + created_at + ", thumb_media_id=" + thumb_media_id + ", type=" + type + "]";}}

3、上传图文信息,获取素材media_id

*** 官方文档 ***
地址:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Batch_Sends_and_Originality_Checks.html#0

接口文档节选内容

上传图文消息素材【订阅号与服务号认证后均可用】
接口调用请求说明http请求方式: POST https://api.weixin.qq.com/cgi-bin/media/uploadnews?access_token=ACCESS_TOKENPOST数据说明POST数据示例如下:{"articles": [   {"thumb_media_id":"qI6_Ze_6PtV7svjolgs-rN6stStuHIjs9_DidOHaj0Q-mwvBelOXCFZiq2OsIU-p","author":"xxx",       "title":"Happy Day",         "content_source_url":"www.qq.com",     "content":"content",         "digest":"digest","show_cover_pic":1,"need_open_comment":1,"only_fans_can_comment":1},    {"thumb_media_id":"qI6_Ze_6PtV7svjolgs-rN6stStuHIjs9_DidOHaj0Q-mwvBelOXCFZiq2OsIU-p","author":"xxx",       "title":"Happy Day",         "content_source_url":"www.qq.com",     "content":"content",         "digest":"digest","show_cover_pic":0,"need_open_comment":1,"only_fans_can_comment":1}]
}
参数  是否必须    说明
Articles    是   图文消息,一个图文消息支持1到8条图文
thumb_media_id  是   图文消息缩略图的media_id,可以在素材管理 - 新增临时素材中获得
author  否   图文消息的作者
title   是   图文消息的标题
content_source_url  否   在图文消息页面点击“阅读原文”后的页面,受安全限制,如需跳转Appstore,可以使用 itun.es 或appsto.re的短链服务,并在短链后增加 #wechat_redirect 后缀。
content 是   图文消息页面的内容,支持 HTML 标签。具备微信支付权限的公众号,可以使用 a 标签,其他公众号不能使用,如需插入小程序卡片,可参考下文。
digest  否   图文消息的描述,如本字段为空,则默认抓取正文前64个字
show_cover_pic  否   是否显示封面,1为显示,0为不显示
need_open_comment   否   Uint32 是否打开评论,0不打开,1打开
only_fans_can_comment   否   Uint32 是否粉丝才可评论,0所有人可评论,1粉丝才可评论
如果需要在群发图文中插入小程序,则在调用上传图文消息素材接口时,需在 content 字段中添加小程序跳转链接,有以下三种样式的可供选择。小程序卡片跳转小程序,代码示例:<mp-miniprogram data-miniprogram-appid="wx123123123" data-miniprogram-path="pages/index/index" data-miniprogram-title="小程序示例" data-miniprogram-imageurl="http://example.com/demo.jpg"></mp-miniprogram>
文字跳转小程序,代码示例:<p><a data-miniprogram-appid="wx123123123" data-miniprogram-path="pages/index" href="">点击文字跳转小程序</a></p>
图片跳转小程序,代码示例:<p><a data-miniprogram-appid="wx123123123" data-miniprogram-path="pages/index" href=""><img src="https://mmbiz.qpic.cn/mmbiz_jpg/demo/0?wx_fmt=jpg" alt="" data-width="null" data-ratio="NaN"></a></p>
参数说明参数  是否必须    说明
data-miniprogram-appid  是   小程序的AppID
data-miniprogram-path   是   小程序要打开的路径
data-miniprogram-title  是   小程序卡片的标题,不超过35个字
data-miniprogram-imageurl   是   小程序卡片的封面图链接,图片必须为1080*864像素
返回说明返回数据示例(正确时的 JSON 返回结果):{"type":"news","media_id":"CsEf3ldqkAYJAU6EJeIkStVDSvffUJ54vqbThMgplD-VJXXof6ctX5fI6-aYyUiQ","created_at":1391857799
}
参数  说明
type    媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb),图文消息(news)
media_id    媒体文件/图文消息上传后获取的唯一标识
created_at  媒体文件上传时间

上传素材 获取media_id 工具类 代码示例


import cn.com.refratechnik.common.constant.CommonConstant;
import cn.com.refratechnik.common.constant.NumberConstants;
import cn.com.refratechnik.common.dto.PushMessageDto;
import cn.com.refratechnik.common.entity.*;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.druid.util.StringUtils;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;import java.io.File;
import java.util.ArrayList;
import java.util.List;/*** @author wangxiaodong* @description: 微信订阅号发送一次性订阅消息工具类* @date 2022/03/25 15:13*/
@Slf4j
public class WeChatUtils {/*** 上传图文消息素材【订阅号与服务号认证后均可用】 请求地址*/public static final String TAG_SEND_MESSAGE_UPLOADNEWS = "https://api.weixin.qq.com/cgi-bin/media/uploadnews?access_token=";/***上传图文消息素材*/public static String uploadNews(String accessToken, PushMessageDto articles) throws Exception {if (accessToken != null) {String url = CommonConstant.TAG_SEND_MESSAGE_UPLOADNEWS + accessToken;log.info("UPLOAD_NEWS_URL:{}", url);//将菜单对象转换成JSON字符串String jsonNews = JSONObject.toJSONString(articles);log.info("JSONNEWS:{}",jsonNews);// 发送消息使用的 IDString media_id = "";// 调取 上传素材接口 ( 调取前面提供的 FileUpload 工具类 )Result<PushMpNews> result=FileUpload .pushMpNews(accessToken, jsonNews);if (ObjectUtil.isNotNull(result.getObj())){media_id = result.getObj().getMedia_id();}return media_id;}return null;}

上传素材 获取media_id 工具类 入参构造 代码示例

/**
* Copyright (c) 2017-2020 Htwins
* https://www.htwins.com.cn
* All rights reserved.
*/
package cn.com.refratechnik.common.dto;import cn.com.refratechnik.common.entity.Article;
import io.swagger.annotations.ApiModel;
import lombok.Data;import java.io.Serializable;
import java.util.List;/**
* @description: 后端用户表DTO
* @author Chengyu.yang@htwins.com.cn
* @date 2022/04/12
*/
@Data
@ApiModel
public class PushMessageDto implements Serializable {private List<Article> articles;
}
/*** Copyright (c) 2017-2020 Htwins* https://www.htwins.com.cn* All rights reserved.*/
package cn.com.refratechnik.common.entity;import io.swagger.annotations.ApiModel;
import lombok.Data;import javax.validation.constraints.NotNull;
import java.io.Serializable;/*** 上传图文消息素材* 类名: Mpnews.java</br>* 描述: 凭证</br>*/
@Data
@ApiModel
public class Article implements Serializable {/*** 图文消息缩略图的media_id,可以在素材管理 - 新增临时素材中获得*/@NotNull(message = "图文消息缩略图的media_id 不能为空")private String thumb_media_id;/*** 图文消息的作者*/private String author;/*** 图文消息的标题*/@NotNull(message = "图文消息的标题不能为空")private String title;/*** 在图文消息页面点击“阅读原文”后的页面,受安全限制,如需跳转Appstore,可以使用 itun.es 或appsto.re的短链服务,并在短链后增加 #wechat_redirect 后缀。*/private String content_source_url;/*** 图文消息页面的内容,支持 HTML 标签。具备微信支付权限的公众号,可以使用 a 标签,其他公众号不能使用,如需插入小程序卡片,可参考下文。*/@NotNull( message = "图文消息页面的内容不能为空")private String content;/*** 图文消息的描述,如本字段为空,则默认抓取正文前64个字*/private String digest;/*** 是否显示封面,1为显示,0为不显示*/private Integer show_cover_pic;/*** Uint32 是否打开评论,0不打开,1打开*/private Integer need_open_comment;/*** Uint32 是否粉丝才可评论,0所有人可评论,1粉丝才可评论*/private Integer only_fans_can_comment;}

4、发布消息

1、文档内容截取

错误时微信会返回错误码等信息,请根据错误码查询错误信息图文消息群发前将进行原创校验一、群发接口新增原创校验流程开发者调用群发接口进行图文消息的群发时,微信会将开发者准备群发的文章,与公众平台原创库中的文章进行比较,校验结果分为以下几种:当前准备群发的文章,未命中原创库中的文章,则可以群发。当前准备群发的文章,已命中原创库中的文章,则:2.1 若原创作者允许转载该文章,则可以进行群发。群发时,会自动替换成原文的样式,且会自动将文章注明为转载并显示来源。若希望修改原文内容或样式,或群发时不显示转载来源,可自行与原创公众号作者联系并获得授权之后再进行群发。2.2 若原创作者禁止转载该文章,则不能进行群发。若希望转载该篇文章,可自行与原创公众号作者联系并获得授权之后再进行群发。二、群发接口新增 send_ignore_reprint 参数群发接口新增 send_ignore_reprint 参数,开发者可以对群发接口的 send_ignore_reprint 参数进行设置,指定待群发的文章被判定为转载时,是否继续群发。当 send_ignore_reprint 参数设置为1时,文章被判定为转载时,且原创文允许转载时,将继续进行群发操作。当 send_ignore_reprint 参数设置为0时,文章被判定为转载时,将停止群发操作。send_ignore_reprint 默认为0。群发操作的相关返回码,可以参考全局返回码说明文档。根据标签进行群发【订阅号与服务号认证后均可用】
接口调用请求说明http请求方式: POST https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_token=ACCESS_TOKENPOST数据说明POST数据示例如下:图文消息(注意图文消息的media_id需要通过上述方法,或通过 “草稿箱 / 新建草稿” 接口来得到,海外微信公众号仅支持发送图文(mpnews)消息):{"filter":{"is_to_all":false,"tag_id":2},"mpnews":{"media_id":"123dsdajkasd231jhksad"},"msgtype":"mpnews","send_ignore_reprint":0
}
文本:{"filter":{"is_to_all":false,"tag_id":2},"text":{"content":"CONTENT"},"msgtype":"text"
}
语音/音频(注意此处media_id需通过素材管理->新增素材来得到):{"filter":{"is_to_all":false,"tag_id":2},"voice":{"media_id":"123dsdajkasd231jhksad"},"msgtype":"voice"
}
图片(注意此处media_id需通过素材管理->新增素材来得到):{"filter":{"is_to_all":false,"tag_id":2},"images": {"media_ids": ["aaa","bbb","ccc"],"recommend": "xxx","need_open_comment": 1,"only_fans_can_comment": 0},"msgtype":"image"
}
视频请注意,此处视频的media_id需通过 POST 请求到下述接口特别地得到:https://api.weixin.qq.com/cgi-bin/media/uploadvideo?access_token=ACCESS_TOKEN POST数据如下(此处media_id需通过素材管理->新增素材来得到):{"media_id": "rF4UdIMfYK3efUfyoddYRMU50zMiRmmt_l0kszupYh_SzrcW5Gaheq05p_lHuOTQ","title": "TITLE","description": "Description"
}
返回将为{"type":"video","media_id":"IhdaAQXuvJtGzwwc0abfXnzeezfO0NgPK6AQYShD8RQYMTtfzbLdBIQkQziv2XJc","created_at":1398848981
}
然后,POST下述数据(将media_id改为上一步中得到的media_id),即可进行发送{"filter":{"is_to_all":false,"tag_id":2},"mpvideo":{"media_id":"IhdaAQXuvJtGzwwc0abfXnzeezfO0NgPK6AQYShD8RQYMTtfzbLdBIQkQziv2XJc"},"msgtype":"mpvideo"
}
卡券消息(注意图文消息的media_id需要通过上述方法来得到):{"filter":{"is_to_all":false,"tag_id":"2"},"wxcard":{              "card_id":"123dsdajkasd231jhksad"         },"msgtype":"wxcard"
}
参数  是否必须    说明
filter  是   用于设定图文消息的接收者
is_to_all   否   用于设定是否向全部用户发送,值为 true 或false,选择 true 该消息群发给所有用户,选择 false 可根据tag_id发送给指定群组的用户
tag_id  否   群发到的标签的tag_id,参见用户管理中用户分组接口,若is_to_all值为true,可不填写tag_id
mpnews  是   用于设定即将发送的图文消息
media_id    是   用于群发的消息的media_id
recommend   否   推荐语,不填则默认为“分享图片”
msgtype 是   群发的消息类型,图文消息为mpnews,文本消息为text,语音为voice,音乐为music,图片为image,视频为video,卡券为wxcard
title   否   消息的标题
description 否   消息的描述
thumb_media_id  是   视频缩略图的媒体ID
send_ignore_reprint 是   图文消息被判定为转载时,是否继续群发。 1为继续群发(转载),0为停止群发。 该参数默认为0。
返回说明返回数据示例(正确时的 JSON 返回结果):{"errcode":0,"errmsg":"send job submission success","msg_id":34182, "msg_data_id": 206227730
}
参数  说明
type    媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb),图文消息为news
errcode 错误码
errmsg  错误信息
msg_id  消息发送任务的ID
msg_data_id 消息的数据ID,该字段只有在群发图文消息时,才会出现。可以用于在图文分析数据接口中,获取到对应的图文消息的数据,是图文分析数据接口中的 msgid 字段中的前半部分,详见图文分析数据接口中的 msgid 字段的介绍。
请注意:在返回成功时,意味着群发任务提交成功,并不意味着此时群发已经结束,所以,仍有可能在后续的发送过程中出现异常情况导致用户未收到消息,如消息有时会进行审核、服务器不稳定等。此外,群发任务一般需要较长的时间才能全部发送完毕,请耐心等待。错误时微信会返回错误码等信息,请根据错误码查询错误信息

2、代码示例

发布群发消息 工具类 代码示例

import cn.com.refratechnik.common.constant.CommonConstant;
import cn.com.refratechnik.common.constant.NumberConstants;
import cn.com.refratechnik.common.dto.PushMessageDto;
import cn.com.refratechnik.common.entity.*;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.druid.util.StringUtils;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;import java.io.File;
import java.util.ArrayList;
import java.util.List;/*** @author wangxiaodong* @description: 微信订阅号发送一次性订阅消息工具类* @date 2022/03/25 15:13*/
@Slf4j
public class WeChatUtils {/*** 发送公众号消息** @throws Exception*/public static Result<SendMpNews> pushWxMessage(Article article) throws Exception {// 校验参数Result<SendMpNews> result = new Result<>();if (ObjectUtil.isNotNull(article) && ObjectUtil.isNotNull(article.getThumb_media_id())){// 获取 accessTokenString accessToken =  getWxAccessToken();// 压缩素材文件 thumb_media_idarticle.setThumb_media_id(article.getThumb_media_id());PushMessageDto pushMessageDto = createArticles(article);// 上传图文素材信息 获取图文信息媒资idString media_id = uploadNews(accessToken,pushMessageDto);// 发布消息result = sendWxMessage(accessToken,new WeChatMessage(),media_id);}return result ;}/*** 发布订阅群发消息* @param accessToken token* @param weChatMessage 订阅消息入参* @param media_id 图文素材Id* @return 返回结果* @throws Exception*/public static Result<SendMpNews> sendWxMessage( String accessToken, WeChatMessage weChatMessage,String media_id ) throws Exception {// 校验参数//群发的消息类型,图文消息为mpnewsweChatMessage.setMsgtype("mpnews");// 图文消息被判定为转载时,是否继续群发。 1为继续群发(转载),0为停止群发。 该参数默认为0。weChatMessage.setSend_ignore_reprint(NumberConstants.ONE_INTEGER);// 过滤发布用户类Filter filter = new Filter();// 用于设定是否向全部用户发送,值为 true 或false,选择 true 该消息群发给所有用户,选择 false 可根据tag_id发送给指定群组的用户filter.setIs_to_all(true);// 插入过滤用户 全员发送weChatMessage.setFilter(filter);// 图文消息类Mpnews mpnews = new Mpnews();// 图文素材资源Idmpnews.setMedia_id(media_id);weChatMessage.setMpnews(mpnews);// 发布消息 请求地址String url = CommonConstant.TAG_SEND_MESSAGE + accessToken;//将菜单对象转换成JSON字符串String jsonNews = JSONObject.toJSONString(weChatMessage);// 调取 推送消息接口Result<SendMpNews> result=FileUpload.sendMpNews(accessToken, jsonNews);return result;}/*** 构造参数 * @param article * @return 返回结果* @throws PushMessageDto */private static PushMessageDto createArticles(Article article) {PushMessageDto articles = new PushMessageDto();List<Article> dataList = new ArrayList<>();Article  news1 = new Article();news1.setTitle(article.getTitle());// 图文消息缩略图的media_id,可以在素材管理 - 新增临时素材中获得news1.setThumb_media_id(article.getThumb_media_id());// 作者news1.setAuthor(article.getAuthor());// 图文消息的描述,如本字段为空,则默认抓取正文前64个字news1.setDigest(article.getDigest());//显示封面news1.setShow_cover_pic(NumberConstants.ONE_INTEGER);// 文章内容 不能为空 图文消息的具体内容,支持HTML标签,必须少于2万字符,小于1M,且此处会去除JS,涉及图片url必须来源接口获取。外部图片url将被过滤。"news1.setContent(article.getContent());//图文消息的原文地址,即点击“阅读原文”后的URLnews1.setContent_source_url(article.getContent_source_url());//Uint32  是否打开评论,0不打开,1打开news1.setNeed_open_comment(NumberConstants.ONE_INTEGER);//Uint32 是否粉丝才可评论,0所有人可评论,1粉丝才可评论news1.setOnly_fans_can_comment(NumberConstants.ONE_INTEGER);dataList.add(news1);articles.setArticles(dataList);return articles;}
}

群发消息接口 返回参数 构造 代码示例

/*** Copyright (c) 2017-2020 Htwins* https://www.htwins.com.cn* All rights reserved.*/
package cn.com.refratechnik.common.entity;
import lombok.Data;@Data
public class SendMpNews {private String errcode;private String errmsg;private String msg_id;private String msg_data_id;public SendMpNews() {super();}@Overridepublic String toString() {return "MdlUpload [errcode=" + errcode + ", errmsg=" + errmsg + ", msg_id=" + msg_id + ", msg_data_id=" + msg_data_id + "]";}
}

群发消息接口 入参构造 代码示例

/*** Copyright (c) 2017-2020 Htwins* https://www.htwins.com.cn* All rights reserved.*/
package cn.com.refratechnik.common.entity;import io.swagger.annotations.ApiModel;
import lombok.Data;import java.io.Serializable;/*** 消息发送类* 类名: WeChatMessage.java</br>* 描述: 凭证</br>*/
@Data
@ApiModel
public class WeChatMessage implements Serializable {/*** 群发的消息类型,图文消息为mpnews,文本消息为text,语音为voice,音乐为music,图片为image,视频为video,卡券为wxcard*/private String msgtype;/*** 图文消息被判定为转载时,是否继续群发。 1为继续群发(转载),0为停止群发。 该参数默认为0。*/private int send_ignore_reprint;/*** 用于设定图文消息的接收者*/private Filter filter;/*** 用于设定即将发送的图文消息*/private Mpnews mpnews;}
/*** Copyright (c) 2017-2020 Htwins* https://www.htwins.com.cn* All rights reserved.*/
package cn.com.refratechnik.common.entity;import io.swagger.annotations.ApiModel;
import lombok.Data;import java.io.Serializable;/*** 用于设定图文消息的接收者* 类名: Filter.java</br>* 描述: 凭证</br>*/
@Data
@ApiModel
public class Filter implements Serializable {/*** 用于设定是否向全部用户发送,值为 true 或false,选择 true 该消息群发给所有用户,选择 false 可根据tag_id发送给指定群组的用户*/private Boolean is_to_all;/*** 群发到的标签的tag_id,参见用户管理中用户分组接口,若is_to_all值为true,可不填写tag_id*/private int tag_id;}
/*** Copyright (c) 2017-2020 Htwins* https://www.htwins.com.cn* All rights reserved.*/
package cn.com.refratechnik.common.entity;import io.swagger.annotations.ApiModel;
import lombok.Data;import java.io.Serializable;/*** 用于设定即将发送的图文消息* 类名: Mpnews.java</br>* 描述: 凭证</br>*/
@Data
@ApiModel
public class Mpnews implements Serializable {/*** 用于群发的消息的media_id*/private String media_id;}

3、业务调取 封装接口 代码示例

    @DuplicateSubmitDefense@ApiOperation("发送群发消息接口")@PostMapping("/send-mpnews")@Overridepublic Result<SendMpNews> sendMpNews ( @RequestBody Article article ) {Result<SendMpNews> result = new Result<>();article.setAuthor(UserUtils.currentUsername());try {result = WeChatUtils.pushWxMessage(article);} catch (Exception e) {e.printStackTrace();}return result;}@DuplicateSubmitDefense@ApiOperation("获取图文消息缩略图的media_id")@PostMapping("/get-media-id")@Overridepublic R<String> getThumbMediaId(@RequestBody MultipartFile file) {R<String> stringR = new R<>();try {stringR = R.<String>ok().data(WeChatUtils.getThumbMediaId(file,null));} catch (Exception e) {e.printStackTrace();}return stringR;}

总结

总结集合坑:

1.获取图文信息缩略图的media_id 接口封装 入参 使用 MultipartFile 类型接受,http 访问时 笔者是将其转换为File类型进行传递的。时间关系 没有进行优化。。。
2. http post请求工具类封装了几个不同的工具,时间关系,未进行统一处理。代码内存在未使用的方法,忽略即可。

以上就是今天要讲的内容,本文仅仅简单介绍 公众号 -订阅号群发 的实现,详细文档请访问微信官方文档进行查看。

【微信公众号-订阅号发送群发消息】相关推荐

  1. 微信公众平台订阅号如何升级转换为服务号?

    很多用户开通微信公众平台时选择了订阅号,但是后来又想用微信支付.小店等功能,就需要把订阅号变成服务号.因微信官方政策限制,目前订阅号改为服务号的方法是进行账号迁移,这也是唯一的方法了. 微信公众平台订 ...

  2. 微信公众平台订阅号和服务号和企业号的区别

    为了帮助网友解决"微信公众平台订阅号和服务号和企业号的区别"相关的问题,中国学网通过互联网对"微信公众平台订阅号和服务号和企业号的区别"相关的解决方案进行了整理 ...

  3. 微信公众平台订阅号运营11个秘决

    据媒体报道微信用户数已经突破6亿!相信在不久的将来微信必将成为重要营销渠道,很多企业都开通了微信号,但是99%微信号都没有专人负责运营,爱煮饭负责运营企业微信订阅号大概有半年多的时间,从0增加到180 ...

  4. 微信公众平台订阅号和服务号的区别详解

    什么是订阅号? 订阅号:为媒体和个人提供一种新的信息传播方式,主要功能是在微信侧给用户传达资讯:(功能类似报纸杂志,提供新闻信息或娱乐趣事) 适用人群:个人.媒体.企业.政府或其他组织. 什么是服务号 ...

  5. 微信公众平台订阅号、服务号和企业号三者之间的区别与联系

    现在很多人用微信营销,但是网上经常能看到有人问订阅号.服务号和企业号到底该选择哪个,下面我们会详细的讲解订阅号.服务号和企业号的区别与联系,需要的朋友可以参考下. 9月18日,微信正式开启了微信企业号 ...

  6. 微信公众平台订阅号与服务号的区别

    订阅号 主要便于为用户传达资讯(类似报纸,杂志),如果想简单的发送消息,达到宣传效果,建议可选择订阅号. 服务号 主要偏于服务交互(类似银行,114,提供服务查询),如果想进行商品营销,进行商品售卖, ...

  7. 微信公众号订阅号开发的学习(二):获取用户发送的消息、简单的自动回复、自定义菜单

    获取用户发送的消息 基础 微信服务器会发送两种类型的消息给开发者服务器. get请求 验证服务器的有效性 post请求 微信服务器会将用户发送的数据转发到开发者服务器上 实现 基于微信公众号订阅号开发 ...

  8. PHP+TP框架实现微信公众号开发之发送模板消息

    一:准备工作. 1.将需要使用到的微信配置放在config.php配置文件中方便获取.(我使用的是微信测试号) return array(//'配置项'=>'配置值''appID'=>'w ...

  9. 微信公众平台服务号、订阅号的相关说明

    一.服务号.订阅号的介绍(区别) 微信公众平台现在已分成订阅公众号和服务公众号两种类型. 公众平台服务号,是公众平台的一种帐号类型,旨在为用户提供服务. 特点:每月可群发一条信息给粉丝.群发的消息乃显 ...

  10. 微信开发专题---7微信公众号订阅号与服务号的区别

    微信公众平台现在已分成订阅公众号和服务公众号两种类型.两者的区别大致如下: 一.目的不同 1.服务号: 旨在为用户提供服务. 2.订阅号: 为用户提供信息和资讯. 二.功能不同 服务号的功能 1.1个 ...

最新文章

  1. SAP MMBE库存数量与在库序列号数量差异之处理
  2. 跨站请求伪造攻击(CSRF)
  3. ASP.NET Core 沉思录 - ServiceProvider 的二度出生
  4. 神经网络与深度学习——TensorFlow2.0实战(笔记)(四)(python异常处理)
  5. 聚合天气--ajax 通过城市名取数据
  6. mysql数据库批量修改
  7. 基于 Gitlab 交付 Go 程序的 Docker 镜像
  8. 京东金融创新”ABS云平台” 大数据提升直接融资效率
  9. Linux系统centos6.7上安装libevent
  10. Spark-SQL教程
  11. 网站克隆工具_Kali Linux工具篇十三:网站克隆技巧Httrack使用技巧
  12. python调用openapi_eleme.openapi.python.sdk · PyPI
  13. 2020届校招浦发银行 信息科技 创新岗 面经及历年汇总~
  14. 从线代角度图解:通解、特解、非齐次通解、非齐次特解、齐次通解、齐次特解
  15. 天然气故障代码大全_天然气故障码表
  16. 使用kubecm管理k8s多集群环境
  17. 【小河今学 | Bootstrap-v3+animate+wow】制作一个简单的响应式网站
  18. 朝阳医院2018年销售数据分析
  19. 第14周—项目1(3)二叉排序树
  20. 【机器人原理与实践(二)】单目摄像头标定与单目测距

热门文章

  1. QT 使用QAxWidget和QAxObject操作DOCX和EXECL文件,包括修改数据、插入图片、修改表格、打印文档、复制SHEET、修改页码数等
  2. Google Code checkout v8 方法
  3. am3352 软时钟老是漂移 rx-8025时钟 rx-8025SA时钟
  4. 网络防火墙开发二三事 转
  5. 华为RS1 企业的网络架构
  6. 微软消息队列-MicroSoft Message Queue(MSMQ)队列的C#使用
  7. 电子合同助力“在线教育”高效发展
  8. 分享常见的视频加密算法原理及其优缺点
  9. 表白网页制作_表白网页_创意表白_表白神器
  10. 如何用AutoRunner进行脚本手工编写