第一章 微信公众号-订阅号发送群发消息
文章目录
前言
在公众平台网站上,为订阅号提供了每天一条的群发权限,为服务号提供每月(自然月)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)");
// 设置 POST
connection.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 {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
@Override
public 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_id
public static String getThumbMediaId(MultipartFile media,String accessToken) throws Exception {
if (StringUtils.isEmpty(accessToken)){
accessToken = getWxAccessToken();
}
// 压缩素材文件 thumb_media_id
String 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;
//判断是否为null
if (multipartFile.equals("") || multipartFile.getSize() <= 0) {
return file;
}
//MultipartFile转换为File
InputStream 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();
}
@Override
public 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_TOKEN
POST数据说明
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);
// 发送消息使用的 ID
String 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_TOKEN
POST数据说明
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())){
// 获取 accessToken
String accessToken = getWxAccessToken();
// 压缩素材文件 thumb_media_id
article.setThumb_media_id(article.getThumb_media_id());
PushMessageDto pushMessageDto = createArticles(article);
// 上传图文素材信息 获取图文信息媒资id
String 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 {
// 校验参数
//群发的消息类型,图文消息为mpnews
weChatMessage.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();
// 图文素材资源Id
mpnews.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());
//图文消息的原文地址,即点击“阅读原文”后的URL
news1.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();
}
@Override
public 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")
@Override
public 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")
@Override
public 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请求工具类封装了几个不同的工具,时间关系,未进行统一处理。代码内存在未使用的方法,忽略即可。
以上就是今天要讲的内容,本文仅仅简单介绍 公众号 -订阅号群发 的实现,详细文档请访问微信官方文档进行查看。