今天实现了一下微信公众号的消息群发功能,整个过程还是比较麻烦的,而且有些坑。记录一下。
微信官方文档–高级群发接口
在公众平台网站上,为订阅号提供了每天一条的群发权限,为服务号提供每月(自然月)4条的群发权限。而对于某些具备开发能力的公众号运营者,可以通过高级群发接口,实现更灵活的群发能力。
由于群发的次数非常有限。这里是以预览的方式发送给自己。
先看效果
发送成功的返回结果
{"errcode":0,"errmsg":"preview success"}
这个是预览,群发和预览差不多。
正式环境的效果(2016年9月21日补图)
微信的正确思路
我的具体做法
// 图文内的图片地址获取接口地址
private String uploadImageUrl = "https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=ACCESS_TOKEN";
// 图文封面图片获取接口地址
private String postImageMediaUrl = "http://file.api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=image";
// 图文素材上传接口地址
private String postNewsUrl = "https://api.weixin.qq.com/cgi-bin/media/uploadnews?access_token=ACCESS_TOKEN";
// 群发接口地址
private String sendToAllUrl = "https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_token=ACCESS_TOKEN";
// 预览接口地址
private String sendToPreviewUrl = "https://api.weixin.qq.com/cgi-bin/message/mass/preview?access_token=ACCESS_TOKEN";
@Test
public void testSendToAll() {
String accessToken = accessTokenUtil.getAccessToken();
// 获取详情的新闻列表
List articleList = new ArrayList<>();
// 1.原始的新闻列表
List newsList = getArticleList();
if(newsList.size()==0){
logger.info("----------今天没有发文章,任务结束----------");
return;
}
// 2.封装新闻详情,并得到新闻列表
for (NewsArticle article : newsList) {
NewsArticle articleNew = getArticleDetial(article.getUrl());
try {
// 下载封面
String localImage = imageService.saveImageToDisk(articleNew.getImagUrl(), imageSavePath);
// 上传封面
String jsonStr = HttpUtil.sendPost(postImageMediaUrl.replace("ACCESS_TOKEN", accessToken),
new File(localImage));
JSONObject object = new JSONObject();
logger.info("----------上传封面返回结果:{}----------" + jsonStr);
try {
object = new JSONObject(jsonStr);
logger.info("----------得到的图片media_id:{}----------", object.get("media_id"));
articleNew.setImageMediaId((String) object.get("media_id"));
} catch (Exception e) {
logger.error("----------上传封面发生错误:{}", e.getMessage());
}
} catch (Exception e) {
logger.error("----------下载封面上传封面过程发生错误:{}", e.getMessage());
}
articleList.add(articleNew);
}
logger.info("----------最终图文列表:{}----------", articleList);
// 3.群发图文消息封装
List articles = new ArrayList<>();
for (NewsArticle NewsArticle : articleList) {
GroupActicle article = new GroupActicle();
article.setThumb_media_id(NewsArticle.getImageMediaId());
article.setAuthor(NewsArticle.getAuthor());
article.setTitle(NewsArticle.getTitle());
article.setContent_source_url(NewsArticle.getUrl());
article.setContent(NewsArticle.getContent());
article.setShow_cover_pic(0);
articles.add(article);
}
logger.info("----------图文消息:{}----------", articles);
Map jsonMap = new HashMap<>();
jsonMap.put("articles", articles);
logger.info("----------最终的图文消息json:{}----------", gson.toJson(jsonMap));
// 4.上传群发图文素材
String postNewsResult = HttpUtil.sendPost(postNewsUrl.replace("ACCESS_TOKEN", accessToken),
gson.toJson(jsonMap));
logger.info("----------上传图文素材的返回结果:{}----------", postNewsResult);
JSONObject object = new JSONObject();
String mediaId = "";
try {
object = new JSONObject(postNewsResult);
logger.info("----------得到的media_id:{}----------", object.get("media_id"));
mediaId = (String) object.get("media_id");
} catch (Exception e) {
logger.error("----------上传图文消息发生错误:{}----------", e.getMessage());
}
// 5.选择发送的用户并发送
if (StringUtils.isNotEmpty(mediaId)) {
Filter filter = new Filter(true);
Mpnews mpnews = new Mpnews(mediaId);
SendToAllNews sendAll = new SendToAllNews();
sendAll.setFilter(filter);
sendAll.setMpnews(mpnews);
sendAll.setMsgtype("mpnews");
// TODO 正式环境用sendToAll
SendToOpenIdPreview sendToPreview = new SendToOpenIdPreview("omSsruLEtGPgxjaJTyurGQwQNh8Q");
sendToPreview.setMpnews(mpnews);
sendToPreview.setMsgtype("mpnews");
logger.info("----------预览的json:{}----------", gson.toJson(sendToPreview));
// TODO 正式环境换成sendToAllUrl
String sentAllResult = HttpUtil.sendPost(sendToPreviewUrl.replace("ACCESS_TOKEN", accessToken),
gson.toJson(sendToPreview));
logger.info("----------预览结果:{}----------", sentAllResult);
}
}
微信群发接口图文消息里面包含图片地址,群发之后图文消息内图片无法显示
请注意,在图文消息的具体内容中,将过滤外部的图片链接,开发者可以通过下述接口上传图片得到URL,放到图文内容中使用。
上传图文消息内的图片获取URL 请注意,本接口所上传的图片不占用公众号的素材库中图片数量的5000个的限制。图片仅支持jpg/png格式,大小必须在1MB以下。
所以图文消息中图片的src属性必须是腾讯的域名
也就是说必须是以
http://mmbiz.qpic.cn/
开头的,这个需要我们调用如下接口地址
// 图文内的图片上传接口地址
private String uploadImageUrl = "https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=ACCESS_TOKEN";
如果遇到如下错误
{"errcode":40007,"errmsg":"invalid media_id hint: [51TBDa0350sz63]"}
上传图片的接口地址为下面的
// 图文封面图片上传接口地址
private String postImageMediaUrl = "http://file.api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=image";
参考下面的文章
微信上传图文消息invalid media_id hint,thumb_media_id怎么获取
今天我的群发消息遇到问题了,好久没有整理了,好多人都说群发消息有问题,我今天把我的代码贴出来供大家参考,敏感信息我已经屏蔽,有些实体类也没给出,只是一个思路,仅供参考
package com.jrbac.service.wechat;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONException;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.jrbac.dao.MessagePushDao;
import com.jrbac.model.wechat.message.GroupSendMessage;
import com.jrbac.model.wechat.messsend.CdptsArticle;
import com.jrbac.model.wechat.messsend.SendToAllNews;
import com.jrbac.model.wechat.messsend.SendToOpenIdPreview;
import com.jrbac.util.HttpUtil;
import com.jrbac.util.ImageUtil;
import com.jrbac.util.PropertiesUtil;
import com.jrbac.util.wechat.AccessTokenUtil;
@Service
public class SendToAllService {
// 获取accessToken的
@Autowired
private AccessTokenUtil accessTokenUtil;
// 读取配置文件的
@Autowired
private PropertiesUtil prop;
// 将图片下载到本地的
@Autowired
private WechatImageDownloadService imageService;
// 已群发的消息
@Autowired
private MessagePushDao messagePush;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final Gson gson = new GsonBuilder().disableHtmlEscaping().create();
// 图文内的图片上传接口地址
private String uploadImageUrl = "https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=ACCESS_TOKEN";
// 图文封面图片上传接口地址
private String postImageMediaUrl = "http://file.api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=image";
// 图文素材上传接口地址
private String postNewsUrl = "https://api.weixin.qq.com/cgi-bin/media/uploadnews?access_token=ACCESS_TOKEN";
// 群发接口地址
private String sendToAllUrl = "https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_token=ACCESS_TOKEN";
// 群发预览接口地址
private String sendToPreviewUrl = "https://api.weixin.qq.com/cgi-bin/message/mass/preview?access_token=ACCESS_TOKEN";
// 照片下载路径
private String imageSavePath = "/usr/tmp/images/";
public void doSend() {
List toSendNewsList = new ArrayList<>();
// 1.获取新闻列表
toSendNewsList = getArticleListFromCdpts();
// 再去获取一次,有时候第一次获取不到,不知道为什么
if (toSendNewsList.size() == 0) {
toSendNewsList = getArticleListFromCdpts();
}
// 2.移除已发送的新闻
toSendNewsList = removeAlreadySentArticle(toSendNewsList);
if (toSendNewsList.size() == 0) {
logger.info("----------没有文章需要群发----------");
return;
}
// 最多群发8条消息
if (toSendNewsList.size() > 8) {
toSendNewsList = toSendNewsList.subList(0, 8);
}
logger.info("----------群发的消息----------");
for (CdptsArticle article : toSendNewsList) {
logger.info("----------{}----------", article.getTitle());
}
// 3.上传并设置封面图片
toSendNewsList = setCoverImage(toSendNewsList);
// 4.群发图文消息json封装
String groupSendMessageJsonStr = getGroupSendMessageJsonStr(toSendNewsList);
// 5.上传群发图文消息
String mediaId = uploadGroupSendMessage(groupSendMessageJsonStr);
// 5.群发
if (StringUtils.isNotEmpty(mediaId)) {
String mode = prop.get("active");
if (StringUtils.isNotBlank(mode) && mode.equals("dev")) {
logger.info("测试环境,预览");
String openId = "你的测试openid";
sendToPreview(toSendNewsList, openId, mediaId);
} else {
logger.info("正式环境,群发");
sendToAll(toSendNewsList, mediaId);
}
}
}
public void sendToPreview(List toSendNewsList, String openId, String mediaId) {
String accessToken = accessTokenUtil.getAccessToken();
SendToOpenIdPreview sendToPreview = new SendToOpenIdPreview(openId, mediaId);
String sendToPreviewJsonStr = gson.toJson(sendToPreview);
logger.info("----------预览的的json:{}----------", sendToPreviewJsonStr);
String sentToPreviewResult = HttpUtil.sendPost(sendToPreviewUrl.replace("ACCESS_TOKEN", accessToken),
sendToPreviewJsonStr);
logger.info("----------预览发送结果:{}----------", sentToPreviewResult);
checkSendResult(toSendNewsList, sentToPreviewResult);
}
public void sendToAll(List toSendNewsList, String mediaId) {
String accessToken = accessTokenUtil.getAccessToken();
SendToAllNews sendToAll = new SendToAllNews(mediaId);
String sendToAllJsonStr = gson.toJson(sendToAll);
logger.info("----------群发的json数据:{}----------", sendToAllJsonStr);
String sentToAllResult = HttpUtil.sendPost(sendToAllUrl.replace("ACCESS_TOKEN", accessToken), sendToAllJsonStr);
logger.info("----------群发结果:{}----------", sentToAllResult);
checkSendResult(toSendNewsList, sentToAllResult);
}
/**
* 查看消息群发结果,并保存群发数据
*
* @param toSendNewsList
* @param sentToAllResult
*/
private void checkSendResult(List toSendNewsList, String sentToAllResult) {
try {
JSONObject object = new JSONObject(sentToAllResult);
int errorCode = (Integer) object.get("errcode");
if (errorCode == 0) {
for (CdptsArticle articleSend : toSendNewsList) {
messagePush.addArticleToPushed(articleSend);
}
} else {
logger.error("---------群发发生了异常{},---------", sentToAllResult);
}
} catch (JSONException e) {
logger.error("---------群发结果json解析异常:{}\n----------jsonStr:{}---------", e.getMessage(), sentToAllResult);
}
}
private String uploadGroupSendMessage(String groupSendMessageJsonStr) {
String accessToken = accessTokenUtil.getAccessToken();
String postNewsResult = HttpUtil.sendPost(postNewsUrl.replace("ACCESS_TOKEN", accessToken),
groupSendMessageJsonStr);
logger.info("----------上传群发图文消息的返回结果:{}----------", postNewsResult);
String mediaId = "";
try {
JSONObject object = new JSONObject(postNewsResult);
mediaId = (String) object.get("media_id");
} catch (JSONException e) {
logger.error("上传群发图文消息发生错误{}", postNewsResult);
}
return mediaId;
}
private String getGroupSendMessageJsonStr(List toSendNewsList) {
List groupSendMessageList = new ArrayList<>();
for (CdptsArticle cdptsArticle : toSendNewsList) {
GroupSendMessage groupSendMessage = new GroupSendMessage();
groupSendMessage.setThumb_media_id(cdptsArticle.getImageMediaId());
groupSendMessage.setAuthor(cdptsArticle.getAuthor());
groupSendMessage.setTitle(cdptsArticle.getTitle());
groupSendMessage.setContent_source_url(cdptsArticle.getUrl());
groupSendMessage.setContent(cdptsArticle.getContent());
groupSendMessage.setShow_cover_pic(0);
groupSendMessageList.add(groupSendMessage);
}
logger.info("----------最终封装的图文消息列表:{}----------", groupSendMessageList);
Map jsonMap = new HashMap<>();
jsonMap.put("articles", groupSendMessageList);
String groupSendMessageJsonStr = gson.toJson(jsonMap);
logger.info("----------最终要发送的图文消息json数据:{}----------", groupSendMessageJsonStr);
return groupSendMessageJsonStr;
}
/**
* 设置图文消息的封面图片
*
* @param newsList
* @return
*/
private List setCoverImage(List newsList) {
String accessToken = accessTokenUtil.getAccessToken();
// 3.将待发送的新闻填充
List detialArticleList = new ArrayList<>();
// 4.封装新闻详情,并得到新闻列表
for (CdptsArticle article : newsList) {
CdptsArticle articleWithDetial = getArticleDetialAndReplaceImage(article.getUrl());
try {
// 下载封面图片到本地
String localImage = imageService.saveImageToDisk(articleWithDetial.getImageUrl(), imageSavePath);
ImageUtil.thumbImage(localImage);
// 上传封面到微信
String jsonStr = HttpUtil.sendPost(postImageMediaUrl.replace("ACCESS_TOKEN", accessToken),
new File(localImage));
logger.info("----------上传封面图片返回结果:{}----------" + jsonStr);
try {
JSONObject object = new JSONObject(jsonStr);
logger.info("----------得到的封面图片media_id:{}----------", object.get("media_id"));
articleWithDetial.setImageMediaId((String) object.get("media_id"));
} catch (Exception e) {
logger.error("----------上传封面发生错误:{}", e.getMessage());
}
} catch (Exception e) {
logger.error("----------下载封面,上传封面到微信过程发生错误:{}", e.getMessage());
}
detialArticleList.add(articleWithDetial);
}
logger.info("----------最终群发图文列表:{}----------", detialArticleList);
return detialArticleList;
}
/**
* 从新闻列表中删除已发送的文章
*
* @return
*/
public List removeAlreadySentArticle(List articleList) {
List messagePushed = messagePush.queryMessagePushed();
// 已发送的
List urlPushedList = new ArrayList<>();
for (CdptsArticle article : messagePushed) {
urlPushedList.add(article.getUrl());
}
// 删除已发送的
Iterator iterator = articleList.iterator();
while (iterator.hasNext()) {
CdptsArticle article = iterator.next();
if (urlPushedList.contains(article.getUrl())) {
iterator.remove();
}
}
return articleList;
}
/**
* 官网获取新闻列表
*
* @return
*/
public List getArticleListFromCdpts() {
// 新闻列表地址
String url = prop.get("allNewsHome");
logger.info("----------获取群发消息列表地址:{}----------", url);
List articleList = new ArrayList<>();
// 得到页面
String result = HttpUtil.sendGet(url);
Document doc = Jsoup.parse(result);
String weiSchoolPrefix = prop.get("weischool");
// 内容页面
Elements elements = doc.select("div.m_right");
for (Element element : elements) {
Elements contentElements = element.getElementsByTag("ul");
for (Element contents : contentElements) {
// System.out.println("内容:");
Elements liContent = contents.getElementsByTag("li");
for (Element content : liContent) {
CdptsArticle article = new CdptsArticle();
// System.out.println(content);
String time = content.getElementsByTag("span").first().text();
// System.out.println(content.getElementsByTag("span").first().text());
article.setTime(time);
String title = content.getElementsByTag("a").first().text();
article.setTitle(title);
String originalUrl = content.select("a[href]").attr("href");
article.setUrl(weiSchoolPrefix + originalUrl);
articleList.add(article);
}
}
}
logger.info("----------获取到的新闻列表:{}----------", articleList);
return articleList;
}
// 图片校验
private static boolean isImage(String imageSrc) {
String reg = ".+(.JPEG|.jpeg|.JPG|.jpg|.GIF|.gif|.BMP|.bmp|.PNG|.png)$";
Pattern pattern = Pattern.compile(reg);
Matcher matcher = pattern.matcher(imageSrc.toLowerCase());
return (matcher.find());
}
/**
* 获取新闻详情并替换图片为微信的
*
* @param articleUrl
* @return
*/
private CdptsArticle getArticleDetialAndReplaceImage(String articleUrl) {
String accessToken = accessTokenUtil.getAccessToken();
CdptsArticle article = new CdptsArticle();
article.setUrl(articleUrl);
// 具体页面
String remoteUrl = prop.get("remoteDomain") + "/Article/Index/" + articleUrl.split("/Article/Index/")[1];
logger.info("----------获取页面详情url:{}----------", remoteUrl);
// 得到页面
String result = HttpUtil.sendGet(remoteUrl);
Document doc = Jsoup.parse(result);
// 内容页面
Elements elements = doc.select("div.m_right");
for (Element element : elements) {
// System.out.println("原始内容:\n" + element);
// 作者信息
Element pageInfo = element.select("p.D_infor").remove(0);
// 标题
Element titleInfo = element.select("h1.D_title").remove(0);
article.setTitle(titleInfo.text());
String page = pageInfo.toString();
// System.out.println("作者相关信息:\n" + page);
// 所在板块
String module = element.getElementsByTag("h2").first().select("a[href]").text();
// 页面内容
String content = element.select("div.r_cont").html();
// 替换所有的图片路径
Elements images = element.select("div.r_cont").select("img");
for (Element image : images) {
String imageUrl = image.attr("src");
if(!isImage(imageUrl)){// 不是图片格式,直接删掉
// image.attr("src", prop.get("localhost") + "/assets/image/school/bg.jpg");
image.remove();
continue;
}else{
if (imageUrl.startsWith("http://")) {
continue;
} else if (imageUrl.startsWith("/Upload")) {
image.attr("src", prop.get("remoteDomain") + image.attr("src"));
} else {
image.attr("src", prop.get("localhost") + "/assets/image/school/bg.jpg");
}
}
// if (!image.attr("src").startsWith("http://")) {
// image.attr("src", prop.get("remoteDomain") +
// image.attr("src"));
// }
}
content = element.select("div.r_cont").html();
// 作者相关
// 作者相关
Elements info = element.getElementsByTag("p").first().getElementsByTag("span");
article.setTime(info.get(0).text().replace("发表时间: ", ""));
article.setAuthor(info.get(1).text().replace("发布人:", ""));
article.setCount(info.get(2).text().replace("点击率:", ""));
article.setModule(module);
// System.out.println("[所在板块]\t" + module);
String contents = content.replace(page, "").replace(titleInfo.toString(), "");
// System.out.println("[页面内容]\t" + contents);
Document docImage = Jsoup.parse(contents);
Elements elementImage = docImage.select("img");
if (elementImage.size() == 0) {
article.setImageUrl(prop.get("localhost") + "/assets/image/school/bg.jpg");
} else {
article.setImageUrl(elementImage.get(0).attr("src"));
}
for (Element imageElement : elementImage) {
try {
// 下载文件
logger.info("----------下载页面中的图片src:{}----------", imageElement.attr("src"));
String imageRealPath = imageService.saveImageToDisk(imageElement.attr("src"), imageSavePath);
logger.info("----------图片保存路径:{}----------", imageRealPath);
// 压缩处理
ImageUtil.thumbImage(imageRealPath);
// 上传到微信
logger.info("----------上传到微信----------");
String responseStr = HttpUtil.sendPost(uploadImageUrl.replace("ACCESS_TOKEN", accessToken),
new File(imageRealPath));
logger.info("----------上传结果:{}----------", responseStr);
JSONObject object = new JSONObject();
try {
object = new JSONObject(responseStr);
logger.info("----------得到上传图片的url:{}----------", object.get("url"));
imageElement.attr("src", (String) object.get("url"));
} catch (Exception e) {
logger.error("----------上传图片到微信发生了错误:{}" + responseStr);
}
} catch (Exception e) {
logger.error("----------下载图片并上传到微信过程发生了错误:{}" + e.getMessage());
}
}
String contentFinal = docImage.select("div.D_cont").html();
article.setContent(contentFinal);
}
logger.info("----------获取页面详情返回结果:{}----------", article);
return article;
}
}
微信开发文档–群发接口
微信上传图文消息invalid media_id hint,thumb_media_id怎么获取