网上那些都是零零碎碎的,不完整,重新整理下,代码可直接使用。
微信公众号消息推送大致分为两类,一是文本推送,二是带图片/视频推送。
文本推送很好理解,可以用模板消息以及自定义消息推送。
图文/视频推送就稍微麻烦些步骤分为 上传素材到临时/永久库->上传图文消息->消息推送。
贴几个官方文档,有总比没有好。
群发推送官网文档:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Batch_Sends_and_Originality_Checks.html
上传素材官方文档:https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/New_temporary_materials.html
官方素材上传调试平台:https://mp.weixin.qq.com/debug/cgi-bin/apiinfo?t=index&type=%E5%9F%BA%E7%A1%80%E6%94%AF%E6%8C%81&form=%E5%A4%9A%E5%AA%92%E4%BD%93%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E6%8E%A5%E5%8F%A3%20/media/upload
一、文本推送
这里文本推送,可以采取模板和自定义推送内容。以下是模板方式推送,在图片/视频推送中会使用自定义内容推送演示。
首先需要在微信公众号上把测试的环境弄好。
点击开发者工具->公众平台测试账号。进去创建好对应的消息模板以及关注该测试的公众号。里面会有appID/appsecret,用户,模板以及能体验接口的信息,没有认证的微信号,有些接口是没有权限的,而且部分接口在没有认证的情况下每天都会有调用次数限制。
添加依赖,因为项目里面用了自己的http封装类,需替换下
dependency>
groupId>com.squareup.okhttp3/groupId>
artifactId>okhttp/artifactId>
version>5.0.0-alpha.14/version>
/dependency>
dependency>
groupId>com.squareup.okio/groupId>
artifactId>okio/artifactId>
version>3.6.0/version>
/dependency>
WxToken
用于生成请求接口token
/**
* 存储微信公众号Token的POJO类
*
* @author zjw
* @description
*/
public class WxToken {
// 存储token信息
private String accessToken;
// 10:00:00
// 12:00:00
// 存储当前的token有效期到什么时间点
private Long expire;
public String getAccessToken() {
// 获取token之前,需要先判断生存时间到了没
return expire == null || expire (System.currentTimeMillis() / 1000) ? null : this.accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public Long getExpire() {
return expire;
}
public void setExpire(Long expire) {
this.expire = System.currentTimeMillis() / 1000 + expire;
}
}
WxMagPushReq
这里要注意下模板的填充值data的格式,keyword1就是你在模板里面所需要替换的参数名称,后面value就是参数的值,模板里面的参数是和传入的参数需一一对应
@Data
@Schema(description = "微信消息推送部分用户实体")
@JsonInclude(JsonInclude.Include.NON_NULL) //这个注解是用于实体转JSON的时候,空值就在转换的时候排除掉
public class WxMagPushReq {
@Schema(description = "用户openid")
private ListString> openIdList;
@NotBlank(message = "模板id不能为空")
@Schema(description = "模板id")
private String templateId;
@Schema(description = "模板需填充的值,keyword1就是模板里面需替换的参数名 如:{" +
" "keyword1":{n" +
" "value":"巧克力"n" +
" },n" +
" "keyword2": {n" +
" "value":"39.8元"n" +
" },n"+
" }")
@NotBlank(message = "模板需填充的值不能为空")
private String data;
}
controller
Result 是自定义的返回类,换成自己的即可
/**
* 微信公众号消息推送--需选用户openId发送
*
* @return
*/
@Operation(summary = "微信公众号消息推送--需选用户openId发送")
@PostMapping("/wxMsgPush")
public Result wxMsgPush(@RequestBody @Valid WxMagPushReq wxMagPushReq) {
return messageService.wxMsgPush(wxMagPushReq);
}
service
Result wxMsgPush(WxMagPushReq wxMagPushReq);
serviceImpl
因为未认证的微信群发接口无法请求,采用循环发送的方式。
@Value("${weixin.msg.secret}")
private String secret;
@Value("${weixin.msg.appid}")
private String appid;
private static WxToken wxToken = new WxToken();
@Override
public Result wxMsgPush(WxMagPushReq wxMagPushReq) {
//1、拿到请求路径
String url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" + getTokenString();
if (wxMagPushReq == null || wxMagPushReq.getOpenIdList().size() == 0 || StringUtils.isEmpty(wxMagPushReq.getData()) || StringUtils.isEmpty(wxMagPushReq.getTemplateId())) {
throw ServiceException.error(ErrorCode.PARAM_EXCEPTION, "参数为空");
}
//装推送失败的openId
ListString> list = new LinkedList>();
//传入的openId去重
ListString> collect = wxMagPushReq.getOpenIdList().stream().distinct().collect(Collectors.toList());
for (String openId : collect) {
//2、请求参数
String params = "{n" +
" "touser":"" + openId + "",n" +
" "template_id":"" + wxMagPushReq.getTemplateId() + "",n" +
" "data":" + wxMagPushReq.getData() +
// " "data":{n" +
// " "tel":{n" +
// " "value":"18700000000"n" +
// " }n" +
// " }n" +
" }";
HttpRequest request = HttpUtil.createPost(url);
request.body(params);
String str = request.execute().body();
JSONObject json = JSONObject.parseObject(str);
Integer errcode = json.getInteger("errcode");
if (0 != errcode) {
list.add(openId);
}
}
return Result.ok(list);
}
//用于生成认证token
private String getToken() {
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appid + "&secret=" + secret;
//2、基于doGet方法,调用地址获取Token
HttpRequest request = HttpUtil.createGet(url);
String resultJSON = request.execute().body();
JSONObject jsonObject = JSONObject.parseObject(resultJSON);
String accessToken = jsonObject.getString("access_token");
Long expiresIn = jsonObject.getLong("expires_in");
//3、存储到WxToken对象里
wxToken.setAccessToken(accessToken);
wxToken.setExpire(expiresIn);
//4、返回Token
return wxToken.getAccessToken();
}
public String getTokenString() {
// 从对象中获取accessToken
String accessToken = wxToken.getAccessToken();
// 获取的accessToken为null,可能之前没获取,可能过期了
if (accessToken == null) {
// 加锁
synchronized (wxToken) {
// 再次判断
if (wxToken.getAccessToken() == null) {
getToken();
}
}
}
return wxToken.getAccessToken();
}
代码中注释掉的tel就是模板中对应的参数名称。
启动项目就可以测试了,请求成功,所关注的公众号会发一条推送的信息过来。
二、图文推送
把图片/视频称为素材,带素材推送步骤。上传素材到临时/永久库->上传图文消息->消息推送。
之前看社区说永久的素材库不能使用,下面的示例采用的是临时库。
注:上传素材的时候会返回media_id这个ID在上传图文消息的时候需要用到,上传图文消息的时候也会返回media_id,这个ID在消息推送的时候也会用到。
WxMagPushAllReq
@Data
@Schema(description = "微信消息推送全部用户实体")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class WxMagPushAllReq {
@Schema(description = "1-文本,2图文,当发送图文消息时,mediaId不能为空")
private String type;
// @NotBlank(message = "消息内容不能为空")
@Schema(description = "图文mediaId")
private String mediaId;
// @NotBlank(message = "消息内容不能为空")
@Schema(description = "消息内容")
private String content;
}
WxTuwen
@Data
public class WxTuwen {
//图片media_id
@Schema(description = "图片media_id")
private String thumb_media_id;
@Schema(description = "图文消息的作者")
//图文消息的作者
private String author;
@Schema(description = "标题")
//标题
private String title;
@Schema(description = "在图文消息页面点击“阅读原文”后的页面,受安全限制,如需跳转Appstore,可以使用itun.es或appsto.re的短链服务,并在短链后增加 #wechat_redirect 后缀")
//在图文消息页面点击“阅读原文”后的页面,受安全限制,如需跳转Appstore,可以使用itun.es或appsto.re的短链服务,并在短链后增加 #wechat_redirect 后缀。
private String content_source_url;
@Schema(description = "图文消息页面的内容")
//图文消息页面的内容,支持HTML标签。
private String content;
@Schema(description = "图文消息的描述")
//图文消息的描述,如本字段为空
private String digest;
@Schema(description = "是否显示封面,1为显示,0为不显示")
//是否显示封面,1为显示,0为不显示
private Integer show_cover_pic;
}
controller
@Operation(summary = "微信公众号消息推送--推送全部用户")
@PostMapping("/wxMsgPushAll")
public Result wxMsgPushAll(@RequestBody @Valid WxMagPushAllReq wxMagPushAllReq) {
return messageService.wxMsgPushAll(wxMagPushAllReq);
}
@Operation(summary = "微信公众号消息推送--上传临时素材")
@PostMapping("/addMaterial")
public Result addMaterial(@RequestParam("media") MultipartFile media, @RequestParam("type") String type) {
return messageService.addMaterial(media,type);
}
@Operation(summary = "微信公众号消息推送--上传图文消息素材")
@PostMapping("/uploadnews")
public Result uploadnews(@RequestBody @Valid WxTuwen wxTuwen) {
return messageService.uploadnews(wxTuwen);
}
serveice
Result wxMsgPushAll(WxMagPushAllReq wxMagPushAllReq);
Result addMaterial(MultipartFile media, String type);
Result uploadnews(WxTuwen wxTuwen);
serviceImpl
@Value("${weixin.msg.secret}")
private String secret;
@Value("${weixin.msg.appid}")
private String appid;
private static WxToken wxToken = new WxToken();
@Override
public Result wxMsgPushAll(WxMagPushAllReq wxMagPushAllReq) {
String list = getWxUserOpenid(getTokenString(), "", "");
if (StringUtils.isEmpty(list)) {
return Result.ok();
}
String url = "https://api.weixin.qq.com/cgi-bin/message/mass/send?access_token=" + getTokenString();
//预览接口,可以看推送的效果
// String url = " https://api.weixin.qq.com/cgi-bin/message/mass/preview?access_token=" + getTokenString();
String params = "";
if ("1".equals(wxMagPushAllReq.getType())) {
//自定义推送内容
params = "{n" +
" "touser":[" + list.substring(0, list.length() - 1) + "],n" +
" "msgtype": "text",n" +
" "text": { "content": "" + wxMagPushAllReq.getContent() + ""}" +
" }";
} else {
//带图文推送消息
params = "{n" +
" "touser":[" + list.substring(0, list.length() - 1) + "],n" +
" "mpnews":{n" +
" "media_id":"" + wxMagPushAllReq.getMediaId() + ""n" +
" },n" +
" "msgtype":"mpnews",n" +
" "send_ignore_reprint":0n" +
"}";
}
HttpRequest request = HttpUtil.createPost(url);
request.body(params);
String str = request.execute().body();
System.out.println(str);
return Result.ok(str);
}
@Override
public Result addMaterial(MultipartFile media, String type) {
try {
String mediaId = uploadFile(transferToFile(media), getTokenString(), type);
return Result.ok(mediaId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public Result uploadnews(WxTuwen wxTuwen) {
//如需处理多个图文,修改为循环处理
String url = "https://api.weixin.qq.com/cgi-bin/media/uploadnews?access_token=" + getTokenString();
String str = JSONObject.toJSONString(wxTuwen);
str = "{" + ""articles":[" + str + "]" + "}";
HttpRequest request = HttpUtil.createPost(url);
request.body(str);
String body = request.execute().body();
JSONObject jsonObject = JSONObject.parseObject(body);
return Result.ok(jsonObject.getString("media_id"));
}
//将类型MultipartFile转为file类型
public File transferToFile(MultipartFile file) {
try {
File convFile = new File(file.getOriginalFilename());
convFile.createNewFile();
InputStream in = file.getInputStream();
OutputStream out = new FileOutputStream(convFile);
byte[] bytes = new byte[1024];
int read;
while ((read = in.read(bytes)) != -1) {
out.write(bytes, 0, read);
}
return convFile;
} catch (Exception e) {
throw new RuntimeException();
}
}
//上传到临时库,返回一个ID
public String uploadFile(File file, String accessToken, String type) throws Exception {
//临时素材地址
String url1 = "https://api.weixin.qq.com/cgi-bin/media/upload?access_token=" + accessToken + "&type=" + type;
//永久素材的地址
// String url1 = "https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=" + accessToken + "&type=" + type;
if (!file.exists() || !file.isFile()) {
throw new IOException("文件不存在!");
}
URL urlObj = new URL(url1);
//连接
HttpURLConnection conn = (HttpURLConnection) urlObj.openConnection();
conn.setRequestMethod("POST");
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);
//请求头
conn.setRequestProperty("Connection", "Keep-Alive");
conn.setRequestProperty("Charset", "UTF-8");
//conn.setRequestProperty("Content-Type","multipart/form-data;");
//设置边界
String BOUNDARY = "----------" + System.currentTimeMillis();
conn.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + BOUNDARY);
StringBuilder sb = new StringBuilder();
sb.append("--");
sb.append(BOUNDARY);
sb.append("rn");
sb.append("Content-Disposition:form-data;name="file";filename="" + file.getName() + ""rn");
sb.append("Content-Type:application/octet-streamrnrn");
System.out.println(sb);
byte[] head = sb.toString().getBytes("UTF-8");
//输出流
OutputStream out = new DataOutputStream(conn.getOutputStream());
out.write(head);
//文件正文部分
DataInputStream in = new DataInputStream(new FileInputStream(file));
int bytes = 0;
byte[] bufferOut = new byte[1024];
while ((bytes = in.read(bufferOut)) != -1) {
out.write(bufferOut, 0, bytes);
}
in.close();
//结尾
byte[] foot = ("rn--" + BOUNDARY + "--rn").getBytes("utf-8");
out.write(foot);
out.flush();
out.close();
//获取响应
StringBuffer buffer = new StringBuffer();
BufferedReader reader = null;
String result = null;
reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = null;
while ((line = reader.readLine()) != null) {
buffer.append(line);
}
if (result == null) {
result = buffer.toString();
}
reader.close();
//需要添加json-lib jar包
JSONObject json = JSONObject.parseObject(result);
System.out.println(json);
String mediaId = json.getString("thumb_media_id");
return result;
}
//根据appid和secretaccess_token
private String getToken() {
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appid + "&secret=" + secret;
//2、基于doGet方法,调用地址获取Token
HttpRequest request = HttpUtil.createGet(url);
String resultJSON = request.execute().body();
JSONObject jsonObject = JSONObject.parseObject(resultJSON);
String accessToken = jsonObject.getString("access_token");
Long expiresIn = jsonObject.getLong("expires_in");
//3、存储到WxToken对象里
wxToken.setAccessToken(accessToken);
wxToken.setExpire(expiresIn);
//4、返回Token
return wxToken.getAccessToken();
}
public String getTokenString() {
// 从对象中获取accessToken
String accessToken = wxToken.getAccessToken();
// 获取的accessToken为null,可能之前没获取,可能过期了
if (accessToken == null) {
// 加锁
synchronized (wxToken) {
// 再次判断
if (wxToken.getAccessToken() == null) {
getToken();
}
}
}
return wxToken.getAccessToken();
}