java springboot调用微信公众平台(公众号)api,主要针对发布图文案例
- 微信公众平台官网地址
- 1.公众号配置服务器地址,微信验证成功后,配置白名单
- 2.application.yml配置微信的相关信息
- 3.接口和代码
- contoller类
- service接口
- serviceImpl实现类
- 工具类
- 实体model
- 4.说明:
微信公众平台官网地址
登录地址:https://mp.weixin.qq.com/ api地址:https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html
1.公众号配置服务器地址,微信验证成功后,配置白名单
注:微信验证服务器地址需要写个接口接收数据进行验证,成功后才能通过,接口已经在下面代码里有了
2.application.yml配置微信的相关信息
3.接口和代码
contoller类
package com.uniwill.manage.controller;
import com.uniwill.core.ResultMsg;
import com.uniwill.core.controller.GenericController;
import com.uniwill.manage.model.WechatDraft;
import com.uniwill.manage.model.WechatNews;
import com.uniwill.manage.service.WechatService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
/**
* @Author tianxubo
* @Date 2024-03-05 16:40:42
* @Description
*/
@Api(value = "微信公众号服务接口", description = "微信公众号服务接口")
@RestController
@RequestMapping("/wechat")
public class WechatController extends GenericController {
@Autowired
private WechatService service;
@ApiOperation(value = "验证是否微信服务推送", notes = "验证是否微信服务推送")
@RequestMapping(value = "verifyWechat", method = RequestMethod.GET)
public String verifyWechat(HttpServletRequest request) {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
return service.verifyWechat(signature, timestamp, nonce, echostr);
}
@ApiOperation(value = "获取微信AccessToken", notes = "获取微信AccessToken")
@RequestMapping(value = "getAccessToken", method = RequestMethod.GET)
public ResultMsg<?> getAccessToken() {
try {
return getSuccessResult(service.getAccessToken(), "success");
} catch (Exception e) {
return getErrorResult(e.getMessage());
}
}
@ApiOperation(value = "新加永久素材图片,只返回url", notes = "新加永久素材图片,只返回url")
@RequestMapping(value = "uploadImg", method = RequestMethod.POST)
public ResultMsg<?> uploadImg(@RequestParam String filePath) {
try {
return getSuccessResult(service.uploadImg(filePath), "success");
} catch (Exception e) {
return getErrorResult(e.getMessage());
}
}
/**
* 新增永久素材
* @param filePath 文件路径
* @param type 媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb)
* @param title 视频素材的标题
* @param introduction 视频素材的描述
* @return 返回 media_id和url(url只有是图片才返回)
*/
@ApiOperation(value = "新加永久素材,返回 media_id和url(url只有是图片才返回),支持图片(image)、语音(voice)、视频(video)和缩略图(thumb)", notes = "新加永久素材,返回 media_id和url(url只有是图片才返回),支持图片(image)、语音(voice)、视频(video)和缩略图(thumb)")
@RequestMapping(value = "addMaterial", method = RequestMethod.POST)
public ResultMsg<?> addMaterial(@RequestParam String filePath,
@RequestParam String type,
@RequestParam(required = false) String title,
@RequestParam(required = false) String introduction) {
try {
return getSuccessResult(service.addMaterial(filePath, type,title,introduction), "success");
} catch (Exception e) {
return getErrorResult(e.getMessage());
}
}
@ApiOperation(value = "删除素材", notes = "删除素材")
@RequestMapping(value = "delMaterial", method = RequestMethod.POST)
public ResultMsg<?> delMaterial(@RequestParam String mediaId) {
try {
return getSuccessResult(service.delMaterial(mediaId), "success");
} catch (Exception e) {
return getErrorResult(e.getMessage());
}
}
@ApiOperation(value = "新建草稿", notes = "新建草稿")
@RequestMapping(value = "addDraft", method = RequestMethod.POST)
public ResultMsg<?> addDraft(@RequestBody WechatDraft entity) {
try {
return getSuccessResult(service.addDraft(entity), "success");
} catch (Exception e) {
return getErrorResult(e.getMessage());
}
}
@ApiOperation(value = "新建图文消息", notes = "新建图文消息")
@RequestMapping(value = "addNewMedia", method = RequestMethod.POST)
public ResultMsg<?> addNewMedia(@RequestBody WechatNews entity) {
try {
return getSuccessResult(service.addNewMedia(entity), "success");
} catch (Exception e) {
return getErrorResult(e.getMessage());
}
}
@ApiOperation(value = "查看草稿", notes = "查看草稿")
@RequestMapping(value = "getDraft", method = RequestMethod.GET)
public ResultMsg<?> getDraft(@RequestParam String mediaId) {
try {
return getSuccessResult(service.getDraft(mediaId), "success");
} catch (Exception e) {
return getErrorResult(e.getMessage());
}
}
@ApiOperation(value = "删除草稿", notes = "删除草稿")
@RequestMapping(value = "delDraft", method = RequestMethod.POST)
public ResultMsg<?> delDraft(@RequestParam String mediaId) {
try {
return getSuccessResult(service.delDraft(mediaId), "success");
} catch (Exception e) {
return getErrorResult(e.getMessage());
}
}
@ApiOperation(value = "发布文章", notes = "发布文章")
@RequestMapping(value = "submitPublish", method = RequestMethod.POST)
public ResultMsg<?> submitPublish(@RequestParam String mediaId) {
try {
return getSuccessResult(service.submitPublish(mediaId), "success");
} catch (Exception e) {
return getErrorResult(e.getMessage());
}
}
@ApiOperation(value = "查询发布状态(当前审核状态)", notes = "查询发布状态(当前审核状态)")
@RequestMapping(value = "getPublish", method = RequestMethod.POST)
public ResultMsg<?> getPublish(@RequestParam String publishId) {
try {
return getSuccessResult(service.getPublish(publishId), "success");
} catch (Exception e) {
return getErrorResult(e.getMessage());
}
}
@ApiOperation(value = "删除发布", notes = "删除发布")
@RequestMapping(value = "delPublish", method = RequestMethod.POST)
public ResultMsg<?> delPublish(@RequestParam String articleId) {
try {
return getSuccessResult(service.delPublish(articleId), "success");
} catch (Exception e) {
return getErrorResult(e.getMessage());
}
}
@ApiOperation(value = "群发消息", notes = "群发消息")
@RequestMapping(value = "sendMessageAll", method = RequestMethod.POST)
public ResultMsg<?> sendMessageAll(@RequestParam String mediaId) {
try {
return getSuccessResult(service.sendMessageAll(mediaId), "success");
} catch (Exception e) {
return getErrorResult(e.getMessage());
}
}
}
service接口
package com.uniwill.manage.service;
import com.alibaba.fastjson.JSONObject;
import com.uniwill.manage.model.WechatDraft;
import com.uniwill.manage.model.WechatNews;
/**
* @Author tianxubo
* @Date 2024-03-05 16:39:49
* @Description 微信公众号推送开发服务步骤
* 1.验证是否微信服务推送
* 2.获取accessToken
* 3.具体操作
*/
public interface WechatService {
//验证是否微信服务推送
String verifyWechat(String signature, String timestamp, String nonce, String echostr);
//获取accesstoken
String getAccessToken();
//刷新accesstoken
String refreshAccessToken();
//上传永久素材图片 只返回url
String uploadImg(String filePath);
/**
* 新增永久素材
* @param filePath 文件路径
* @param type 媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb)
* @param title 视频素材的标题
* @param introduction 视频素材的描述
* @return 返回 media_id和url(url只有是图片才返回) 支持图片、音频、视频等
*/
JSONObject addMaterial(String filePath, String type,String title,String introduction);
//删除永久素材
boolean delMaterial(String mediaId);
//新建草稿
String addDraft(WechatDraft entity);
//新建图文消息
String addNewMedia(WechatNews entity);
JSONObject getDraft(String mediaId);
//删除草稿
boolean delDraft(String mediaId);
//发布文章
String submitPublish(String mediaId);
//查询发布状态 publishId 发布任务的id
JSONObject getPublish(String publishId);
//删除发布 articleId 当发布状态为0时(即成功)时,返回图文的 article_id,可用于“客服消息”场景
boolean delPublish(String articleId);
//群发消息
boolean sendMessageAll(String mediaId);
}
serviceImpl实现类
package com.uniwill.manage.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.uniwill.exception.BusinessMessage;
import com.uniwill.manage.model.WechatDraft;
import com.uniwill.manage.model.WechatNews;
import com.uniwill.manage.service.WechatService;
import com.uniwill.manage.tools.HttpUtils;
import com.uniwill.manage.tools.Sha1Utils;
import com.uniwill.redis.service.RedisService;
import com.uniwill.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @Author tianxubo
* @Date 2024-03-05 16:39:19
* @Description
*/
@Slf4j
@Service("weChatService")
public class WechatServiceImpl implements WechatService {
//获取access_token的url
private static final String GET_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
//上传永久素材url 返回url 只支持jpg和png
private static final String UPLOAD_MEDIA_IMG_URL = "https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=%s";
//上传永久素材url 返回mediaId和url 支持音频、图片视频等
private static final String UPLOAD_FOREVER_MEDIA_URL = "https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=%s&type=%s";
//删除永久素材url
private static final String DELETE_FOREVER_MEDIA_URL = "https://api.weixin.qq.com/cgi-bin/material/del_material?access_token=%s";
//新加草稿url
private static final String ADD_DRAFT_URL = "https://api.weixin.qq.com/cgi-bin/draft/add?access_token=%s";
//删除草稿url
private static final String DELETE_DRAFT_URL = "https://api.weixin.qq.com/cgi-bin/draft/delete?access_token=%s";
//草稿发布url
private static final String SUBMIT_FREEP_PUBLISH_URL = "https://api.weixin.qq.com/cgi-bin/freepublish/submit?access_token=%s";
//发布状态巡查
private static final String GET_FREEP_PUBLISH_URL = "https://api.weixin.qq.com/cgi-bin/freepublish/get?access_token=%s";
//删除发布url
private static final String DELETE_FREEP_PUBLISH_URL = "https://api.weixin.qq.com/cgi-bin/freepublish/delete?access_token=%s";
//新增永久素材的图文消息
private static final String ADD_MATERIAL_NEWS_URL = "https://api.weixin.qq.com/cgi-bin/material/add_news?access_token=%s";
//群发消息(所有)
private static final String SEND_MESSAGE_ALL_URL = "https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_token=%s";
//redis上AccessToken存储位置
private static final String REDIS_WECHAT_ACCESS_TOKEN = "wechat:AccessToken:";
private static final String GET_DRAFT_URL = "https://api.weixin.qq.com/cgi-bin/draft/get?access_token=%s";
@Value("${wechat.appid}")
private String appid;
@Value("${wechat.secret}")
private String secret;
@Value("${wechat.refreshExpire:120}")
private long expire; //微信token存入redis的过期时长
@Value("${wechat.token}")
private String token; //微信token存入redis的过期时长
@Autowired
private RedisService redisService;
/**
* 腾讯验证服务器
* 开发者提交信息后,微信服务器将发送GET请求到填写的服务器地址URL上,请求参数带有四个参数
*
* @param
* @return
*/
@Override
public String verifyWechat(String signature, String timestamp, String nonce, String echostr) {
try {
if (StringUtil.isEmpty(signature) || StringUtil.isEmpty(timestamp) || StringUtil.isEmpty(nonce) || StringUtil.isEmpty(echostr)) {
return "";
}
String[] array = {token, timestamp, nonce};
Arrays.sort(array);
String original = String.join("", array);
//sha1加密算法后获取到hashcode值
String hashcode = Sha1Utils.sha1(original);
//对比获取到的 hashcode 等于echostr值,证明是腾讯公众服务请求
if (hashcode.equals(signature)) {
return echostr;
} else {
return "";
}
} catch (Exception e) {
return e.getMessage();
}
}
/**
* 获取微信公众号AccessToken
* 1.判断redis最新的微信公众号AccessToken是否存在 存在-获取返回至4,不存在-至2
* 2.去微信公众平台获取(2小时有效,每天获取次数有限)
* 3.获取到存入redis
* 4.返回AccessToken
*
* @return
*/
@Override
public String getAccessToken() {
String redisKey = REDIS_WECHAT_ACCESS_TOKEN + appid;
String accessToken = "";
//1.判断redis最新的微信公众号AccessToken是否存在
if (redisService.exists(redisKey)) {
//2.存在获取返回
accessToken = redisService.get(redisKey);
} else {
//2.不存在去微信api取
accessToken = refreshAccessToken();
//3.获取到存入redis
redisService.set(redisKey, accessToken, expire * 60);
}
log.debug("accessToken:" + accessToken);
return accessToken;
}
/**
* 刷新微信公众号AccessToken
* 去微信api获取
*
* @return
*/
@Override
public String refreshAccessToken() {
String url = String.format(GET_ACCESS_TOKEN_URL, appid, secret);
String result = HttpUtils.doGetHttpRequest(url);
if (StringUtil.isNotEmpty(result)) {
JSONObject json = JSONObject.parseObject(result);
if (null != json && StringUtil.isNotEmpty(json.getString("access_token"))) {
return json.getString("access_token");
}
}
throw new BusinessMessage("获取微信公众号权限失败!");
}
/**
* 上传永久素材图片 只返回url
*
* @param filePath
* @return
*/
@Override
public String uploadImg(String filePath) {
String type = filePath.toUpperCase().substring(filePath.lastIndexOf(".") + 1);
if (!type.equals("JPG") && !type.equals("PNG") && !type.equals("JPEG") && !type.equals("GIF") && !type.equals("BMP")) {
throw new BusinessMessage("上传永久素材图片失败,图片类型必须为jpg/png/jpeg/gif/bmp");
}
String url = String.format(UPLOAD_MEDIA_IMG_URL, getAccessToken());
// String result = HttpUtils.doPostLocalUploadMedia(url, filePath,null);
String result = HttpUtils.doPostLocalUploadMedia(url, filePath, null);
log.debug("result:" + result);
if (StringUtil.isEmpty(result)) {
throw new BusinessMessage("上传永久素材图片失败");
}
JSONObject json = JSONObject.parseObject(result);
String mediaUrl = json.getString("url");
if (StringUtil.isEmpty(mediaUrl)) {
throw new BusinessMessage("上传永久素材图片失败");
}
return mediaUrl;
}
/**
* 新增永久素材
*
* @param filePath 文件路径
* @param type 媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb)
* @param title 视频素材的标题
* @param introduction 视频素材的描述
* @return 返回 media_id和url(url只有是图片才返回) 支持图片、音频、视频等
*/
@Override
public JSONObject addMaterial(String filePath, String type, String title, String introduction) {
String url = String.format(UPLOAD_FOREVER_MEDIA_URL, getAccessToken(), type);
JSONObject params = null;
if (type.equals("video")) {
params = new JSONObject();
params.put("title", title);
params.put("introduction", introduction);
}
String result = HttpUtils.doPostLocalUploadMedia(url, filePath, params);
log.debug("result:" + result);
if (StringUtil.isEmpty(result)) {
throw new BusinessMessage("上传永久素材附件失败");
}
JSONObject json = JSONObject.parseObject(result);
String mediaId = json.getString("media_id");
if (StringUtil.isEmpty(mediaId)) {
throw new BusinessMessage("上传永久素材附件失败");
}
if (type.equals("image")) {
String mediaUrl = json.getString("url"); //只有图片才有mediaUrl
if (StringUtil.isEmpty(mediaUrl)) {
throw new BusinessMessage("上传永久素材图片失败");
}
}
return json;
}
@Override
public boolean delMaterial(String mediaId) {
String url = String.format(DELETE_FOREVER_MEDIA_URL, getAccessToken());
JSONObject params = new JSONObject();
params.put("media_id", mediaId);
String result = HttpUtils.doPostHttpRequest(url, params);
log.debug("result:" + result);
if (StringUtil.isEmpty(result)) {
throw new BusinessMessage("删除永久素材附件失败");
}
JSONObject json = JSONObject.parseObject(result);
int errcode = json.getInteger("errcode");
String errmsg = json.getString("errmsg");
if (errcode != 0) {
throw new BusinessMessage("删除永久素材附件失败");
}
return true;
}
// {
// "articles": [
// {
// "title":TITLE, 标题
// "author":AUTHOR, 作者
// "digest":DIGEST, 图文消息的摘要,仅有单图文消息才有摘要,多图文此处为空。如果本字段为没有填写,则默认抓取正文前54个字。
// "content":CONTENT, 图文消息的具体内容,支持HTML标签,必须少于2万字符,小于1M,且此处会去除JS,涉及图片url必须来源 "上传图文消息内的图片获取URL"接口获取。外部图片url将被过滤。
// "content_source_url":CONTENT_SOURCE_URL, 图文消息的原文地址,即点击“阅读原文”后的URL
// "thumb_media_id":THUMB_MEDIA_ID, 图文消息的封面图片素材id(必须是永久MediaID)
// "need_open_comment":0, Uint32 是否打开评论,0不打开(默认),1打开
// "only_fans_can_comment":0, Uint32 是否粉丝才可评论,0所有人可评论(默认),1粉丝才可评论
// "pic_crop_235_1":X1_Y1_X2_Y2, 封面裁剪为2.35:1规格的坐标字段。以原始图片(thumb_media_id)左上角(0,0),右下角(1,1)建立平面坐标系,经过裁剪后的图片,其左上角所在的坐标即为(X1,Y1),右下角所在的坐标则为(X2,Y2),用分隔符_拼接为X1_Y1_X2_Y2,每个坐标值的精度为不超过小数点后6位数字。示例见下图,图中(X1,Y1) 等于(0.1945,0),(X2,Y2)等于(1,0.5236),所以请求参数值为0.1945_0_1_0.5236。
// "pic_crop_1_1":X1_Y1_X2_Y2 封面裁剪为1:1规格的坐标字段,裁剪原理同pic_crop_235_1,裁剪后的图片必须符合规格要求。
// }
// //若新增的是多图文素材,则此处应还有几段articles结构
// ]
// }
@Override
public String addDraft(WechatDraft entity) {
String url = String.format(ADD_DRAFT_URL, getAccessToken());
// entity = getTestDraft(entity.getThumb_media_id());
if (null == entity) {
throw new BusinessMessage("请求体为空!");
}
//参数
JSONObject params = new JSONObject();
List<JSONObject> lists = new ArrayList<>();
JSONObject jsonObject = (JSONObject) JSON.toJSON(entity);
lists.add(jsonObject);
params.put("articles", lists);
//请求
String result = HttpUtils.doPostHttpRequest(url, params);
log.debug(result);
if (StringUtil.isEmpty(result)) {
throw new BusinessMessage("新增草稿失败");
}
JSONObject resultJson = JSONObject.parseObject(result);
String mediaId = resultJson.getString("media_id");
if (StringUtil.isEmpty(mediaId)) {
throw new BusinessMessage("新增草稿失败");
}
return mediaId;
}
@Override
public String addNewMedia(WechatNews entity) {
String url = String.format(ADD_MATERIAL_NEWS_URL, getAccessToken());
// entity = getTestNews(entity.getThumb_media_id());
if (null == entity) {
throw new BusinessMessage("请求体为空!");
}
//参数
JSONObject params = new JSONObject();
List<JSONObject> lists = new ArrayList<>();
JSONObject jsonObject = (JSONObject) JSON.toJSON(entity);
lists.add(jsonObject);
params.put("articles", lists);
//请求
String result = HttpUtils.doPostHttpRequest(url, params);
log.debug(result);
if (StringUtil.isEmpty(result)) {
throw new BusinessMessage("新增图文消息失败");
}
JSONObject resultJson = JSONObject.parseObject(result);
String mediaId = resultJson.getString("media_id");
if (StringUtil.isEmpty(mediaId)) {
throw new BusinessMessage("新增图文消息失败");
}
return mediaId;
}
//rh5-HCuNsWky-w72HJjm9e2zprgiJfm7pGVaZ8gcT5xBED3zdrkkMkLMIrX-DA5C
public JSONObject getDraft(String mediaId) {
String url = String.format(GET_DRAFT_URL, getAccessToken());
JSONObject params = new JSONObject();
params.put("media_id", mediaId);
String result = HttpUtils.doPostHttpRequest(url, params);
log.debug("result:" + result);
if (StringUtil.isEmpty(result)) {
throw new BusinessMessage("查看草稿失败");
}
JSONObject json = JSONObject.parseObject(result);
System.out.println(json.getJSONArray("news_item").getJSONObject(0).getString("content"));
return json;
}
@Override
public boolean delDraft(String mediaId) {
String url = String.format(DELETE_DRAFT_URL, getAccessToken());
JSONObject params = new JSONObject();
params.put("media_id", mediaId);
String result = HttpUtils.doPostHttpRequest(url, params);
log.debug("result:" + result);
if (StringUtil.isEmpty(result)) {
throw new BusinessMessage("删除草稿失败");
}
JSONObject json = JSONObject.parseObject(result);
int errcode = json.getInteger("errcode");
String errmsg = json.getString("errmsg");
if (errcode != 0) {
throw new BusinessMessage("删除草稿失败");
}
return true;
}
@Override
public String submitPublish(String mediaId) {
String url = String.format(SUBMIT_FREEP_PUBLISH_URL, getAccessToken());
JSONObject params = new JSONObject();
params.put("media_id", mediaId);
String result = HttpUtils.doPostHttpRequest(url, params);
log.debug("result:" + result);
if (StringUtil.isEmpty(result)) {
throw new BusinessMessage("发布错误");
}
JSONObject json = JSONObject.parseObject(result);
int errcode = json.getInteger("errcode");
String errmsg = json.getString("errmsg");
String publishId = json.getString("publish_id"); //发布任务的id
String msgDataId = json.getString("msg_data_id"); //消息的数据ID
if (errcode != 0) {
throw new BusinessMessage("发布错误");
}
return publishId;
}
@Override
public JSONObject getPublish(String publishId) {
String url = String.format(GET_FREEP_PUBLISH_URL, getAccessToken());
JSONObject params = new JSONObject();
params.put("publish_id", publishId);
String result = HttpUtils.doPostHttpRequest(url, params);
log.debug("result:" + result);
if (StringUtil.isEmpty(result)) {
throw new BusinessMessage("查询发布状态失败");
}
JSONObject json = JSONObject.parseObject(result);
String articleId = json.getString("article_id"); //删除用的id 当发布状态为0时(即成功)时,返回图文的 article_id,可用于“客服消息”场景
int publishStatus = json.getInteger("publish_status"); //发布任务状态 0.成功 1.发布中 2.发布失败,审核不通过
return json;
}
@Override
public boolean delPublish(String articleId) {
String url = String.format(DELETE_FREEP_PUBLISH_URL, getAccessToken());
JSONObject params = new JSONObject();
params.put("article_id", articleId);
params.put("index", 1);
String result = HttpUtils.doPostHttpRequest(url, params);
log.debug("result:" + result);
if (StringUtil.isEmpty(result)) {
throw new BusinessMessage("删除发布失败");
}
JSONObject json = JSONObject.parseObject(result);
int errcode = json.getInteger("errcode");
String errmsg = json.getString("errmsg");
if (errcode != 0) {
throw new BusinessMessage("删除发布失败");
}
return true;
}
/**
* 群发消息
* {
* "filter":{ 用于设定图文消息的接收者
* "is_to_all":true, 用于设定是否向全部用户发送,值为true或false,选择true该消息群发给所有用户,选择false可根据tag_id发送给指定群组的用户
* "tag_id":2 群发到的标签的tag_id,参见用户管理中用户分组接口,若is_to_all值为true,可不填写tag_id
* },
* "mpnews":{ 用于设定即将发送的图文消息
* "media_id":"123dsdajkasd231jhksad" 用于群发的消息的media_id
* },
* "msgtype":"mpnews", 群发的消息类型,图文消息为mpnews,文本消息为text,语音为voice,音乐为music,图片为image,视频为video,卡券为wxcard
* "send_ignore_reprint":1 图文消息被判定为转载时,是否继续群发。 1为继续群发(转载),0为停止群发。 该参数默认为0。
* }
*
* @param mediaId
* @return
*/
@Override
public boolean sendMessageAll(String mediaId) {
String url = String.format(SEND_MESSAGE_ALL_URL, getAccessToken());
JSONObject params = new JSONObject();
JSONObject filter = new JSONObject(); //用于设定图文消息的接收者
filter.put("is_to_all", true); //用于设定是否向全部用户发送,值为true或false,选择true该消息群发给所有用户
JSONObject mpnews = new JSONObject(); //用于设定即将发送的图文消息
mpnews.put("media_id", mediaId); //用于群发的消息的media_id
params.put("msgtype", mpnews);
params.put("send_ignore_reprint", 1);
String result = HttpUtils.doPostHttpRequest(url, params);
log.debug("result:" + result);
if (StringUtil.isEmpty(result)) {
throw new BusinessMessage("删除发布失败");
}
JSONObject json = JSONObject.parseObject(result);
int errcode = json.getInteger("errcode");
String errmsg = json.getString("errmsg");
if (errcode != 0) {
throw new BusinessMessage("群发消息失败");
}
return true;
}
}
工具类
package com.uniwill.manage.tools;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.uniwill.exception.BusinessMessage;
import com.uniwill.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
/**
* http网络请求
*/
@Slf4j
public class HttpUtils {
/**
* http网络请求
*
* @param uristr
* @return
*/
public static String doGetHttpRequest(String uristr) {
//链接
HttpURLConnection connection = null;
InputStream is = null;
BufferedReader br = null;
StringBuffer result = new StringBuffer();
try {
//创建连接
URL url = new URL(uristr);
connection = (HttpURLConnection) url.openConnection();
//设置请求方式
connection.setRequestMethod("GET");
//设置连接超时时间
connection.setReadTimeout(15000);
//开始连接
connection.connect();
//获取响应数据
if (connection.getResponseCode() == 200) {
//获取返回的数据
is = connection.getInputStream();
if (null != is) {
br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
String temp = null;
while (null != (temp = br.readLine())) {
result.append(temp);
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != br) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != is) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//关闭远程连接
connection.disconnect();
}
return result.toString();
}
/**
* http网络请求 POST
*
* @param httpUrl
* @param param
* @return
*/
public static String doPostHttpRequest(String httpUrl, JSONObject param) {
HttpURLConnection connection = null;
InputStream is = null;
OutputStream os = null;
BufferedReader br = null;
String result = null;
try {
URL url = new URL(httpUrl);
// 通过远程url连接对象打开连接
connection = (HttpURLConnection) url.openConnection();
// 设置连接请求方式
connection.setRequestMethod("POST");
// 设置连接主机服务器超时时间:15000毫秒
connection.setConnectTimeout(15000);
// 设置读取主机服务器返回数据超时时间:60000毫秒
connection.setReadTimeout(60000);
// 默认值为:false,当向远程服务器传送数据/写数据时,需要设置为true
connection.setDoOutput(true);
// 默认值为:true,当前向远程服务读取数据时,设置为true,该参数可有可无
connection.setDoInput(true);
// 设置传入参数的格式:请求参数应该是 name1=value1&name2=value2 的形式。
connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
connection.setRequestProperty("Connection", "keep-alive");
// connection.setRequestProperty("app-secret","a6b7b560f5e5e17910d1079ababfa4f65494ac9a1da349bfb9ba2e7c83ad65eac0fd352a120c31c8c6a86b305a367d49");
// 通过连接对象获取一个输出流
os = connection.getOutputStream();
// 通过输出流对象将参数写出去/传输出去,它是通过字节数组写出的
os.write(JSON.toJSONBytes(param));
// 通过连接对象获取一个输入流,向远程读取
if (connection.getResponseCode() == 200) {
is = connection.getInputStream();
// 对输入流对象进行包装:charset根据工作项目组的要求来设置
br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
StringBuffer sbf = new StringBuffer();
String temp = null;
// 循环遍历一行一行读取数据
while ((temp = br.readLine()) != null) {
sbf.append(temp);
sbf.append("\r\n");
}
result = sbf.toString();
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (null != br) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != os) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != is) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 断开与远程地址url的连接
connection.disconnect();
}
return result;
}
/**
* 上传媒体文件到腾讯公众平台 媒体文件类型分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb)
*
* @param uri 上传路径url
* @param filePath //文件本地路径
* @param params //视频需要参数,图片和音乐不需要
* @return
*/
public static String doPostLocalUploadMedia(String uri, String filePath, JSONObject params) {
String result = null;
OutputStream output = null;
DataInputStream inputStream = null;
URL url = null;
try {
url = new URL(uri);
File file = new File(filePath);
if (!file.isFile() || !file.exists()) {
throw new IOException("file is not exist");
}
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setDoInput(true);
con.setDoOutput(true);
con.setUseCaches(false);
con.setRequestMethod("POST");
// 设置请求头信息
con.setRequestProperty("Connection", "Keep-Alive");
con.setRequestProperty("Charset", "UTF-8");
// 设置边界
String boundary = "----------" + System.currentTimeMillis();
con.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
// 请求正文信息
// 第一部分
output = new DataOutputStream(con.getOutputStream());
IOUtils.write(("--" + boundary + "\r\n").getBytes("UTF-8"), output);
IOUtils.write(("Content-Disposition: form-data;name=\"media\"; filename=\"" + file.getName() + "\"\r\n").getBytes(), output);
IOUtils.write("Content-Type: video/mp4 \r\n\r\n".getBytes(), output);
// 文件正文部分
// 把文件已流文件的方式 推入到url中
inputStream = new DataInputStream(new FileInputStream(file));
IOUtils.copy(inputStream, output);
// 结尾部分
IOUtils.write(("--" + boundary + "\r\n").getBytes("UTF-8"), output);
IOUtils.write("Content-Disposition: form-data; name=\"description\";\r\n\r\n".getBytes("UTF-8"), output);
//只有视频才需要title和introduction参数
if (null != params) {
String title = params.getString("title");
String introduction = params.getString("introduction");
if (StringUtil.isNotEmpty(title) && StringUtil.isNotEmpty(introduction)) {
IOUtils.write(("{\"title\":\"" + title + "\",\"introduction\":\"" + introduction + "\"}").getBytes("UTF-8"), output);
}
}
IOUtils.write(("\r\n--" + boundary + "--\r\n\r\n").getBytes("UTF-8"), output);
output.flush();
result = inputStreamToString(con.getInputStream());
System.out.println(result);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (ProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(output);
IOUtils.closeQuietly(inputStream);
}
return result;
}
public static String inputStreamToString(InputStream inputStream) {
StringBuilder buffer = new StringBuilder();
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line = null;
while ((line = reader.readLine()) != null) {
buffer.append(line);
}
} catch (IOException e) {
e.printStackTrace();
}
return buffer.toString();
}
/***
* * 根据图片在服务器的地址将图片、视频转成file
* @param picUrl 图片服务器地址
* @param suffix 后缀
* @return
* @throws Exception
*/
public static File getFileByUrl(String picUrl, String suffix) throws Exception {
URL imageUrl = new URL(picUrl);
HttpURLConnection conn = (HttpURLConnection) imageUrl.openConnection();
InputStream inputStream = conn.getInputStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
File file = File.createTempFile("pattern", "." + suffix);
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(outputStream.toByteArray());
inputStream.close();
outputStream.close();
fileOutputStream.close();
return file;
}
/**
* 本地文件上传 图片,音频
*
* @param url
* @param filePath
* @return
* @throws Exception
*/
public static String doPostUploadImg(String url, String filePath) {
BufferedReader reader = null;
String result = null;
URL urlObj = null;
HttpURLConnection conn = null;
InputStream in = null;
OutputStream out = null;
try {
File file = new File(filePath);
if (!file.exists() || !file.isFile()) {
throw new BusinessMessage("文件不存在!");
}
urlObj = new URL(url);
//连接
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");
//设置边界
String BOUNDARY = "----------" + System.currentTimeMillis();
conn.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + BOUNDARY);
StringBuilder sb = new StringBuilder();
sb.append("--");
sb.append(BOUNDARY);
sb.append("\r\n");
sb.append("Content-Disposition:form-data;name=\"media\";filename=\"" + file.getName() + "\"\r\n");
sb.append("Content-Type:application/octet-stream\r\n\r\n");
byte[] head = sb.toString().getBytes("utf-8");
//输出流
out = new DataOutputStream(conn.getOutputStream());
out.write(head);
//文件正文部分
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);
}
//结尾
byte[] foot = ("\r\n--" + BOUNDARY + "--\r\n").getBytes("utf-8");
out.write(foot);
out.flush();
//获取响应
StringBuffer buffer = new StringBuffer();
reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = null;
while ((line = reader.readLine()) != null) {
buffer.append(line);
}
result = buffer.toString();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
if (null != in) {
in.close();
}
if (null != out) {
out.close();
}
if (null != reader) {
reader.close();
}
if (null != conn) {
conn.disconnect();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return result;
}
}
package com.uniwill.manage.tools;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* sha1加密算法
* @Author tianxubo
* @Date 2024-03-08 15:37:49
* @Description
*/
public class Sha1Utils {
/**
* sha1加密获取到hashcode值
* @param str
* @return
* @throws NoSuchAlgorithmException
*/
public static String sha1(String str) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] hash = digest.digest(str.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}
实体model
package com.uniwill.manage.model;
import lombok.Data;
/**
* 草稿实体,,微信新建草稿所需字段
* @Author tianxubo
* @Date 2024-03-11 09:53:10
* @Description
*/
@Data
public class WechatNews {
private String title; //标题
private String author; //作者
private String digest; //图文消息的摘要,仅有单图文消息才有摘要,多图文此处为空。如果本字段为没有填写,则默认抓取正文前54个字。
private int show_cover_pic = 0; //是否显示封面,0为false,即不显示,1为true,即显示
private String content; //图文消息的具体内容,支持HTML标签,必须少于2万字符,小于1M,且此处会去除JS,涉及图片url必须来源 "上传图文消息内的图片获取URL"接口获取。外部图片url将被过滤。
private String content_source_url; //图文消息的原文地址,即点击“阅读原文”后的URL
private String thumb_media_id; //图文消息的封面图片素材id(必须是永久MediaID)
private int need_open_comment = 0; //0, Uint32 是否打开评论,0不打开(默认),1打开
private int only_fans_can_comment = 0; //0,Uint32 是否粉丝才可评论,0所有人可评论(默认),1粉丝才可评论
}
package com.uniwill.manage.model;
import lombok.Data;
/**
* 草稿实体,,微信新建草稿所需字段
* @Author tianxubo
* @Date 2024-03-11 09:53:10
* @Description
*/
@Data
public class WechatDraft {
private String title; //标题
private String author; //作者
private String digest; //图文消息的摘要,仅有单图文消息才有摘要,多图文此处为空。如果本字段为没有填写,则默认抓取正文前54个字。
private String content; //图文消息的具体内容,支持HTML标签,必须少于2万字符,小于1M,且此处会去除JS,涉及图片url必须来源 "上传图文消息内的图片获取URL"接口获取。外部图片url将被过滤。
private String content_source_url; //图文消息的原文地址,即点击“阅读原文”后的URL
private String thumb_media_id; //图文消息的封面图片素材id(必须是永久MediaID)
private int need_open_comment = 0; //0, Uint32 是否打开评论,0不打开(默认),1打开
private int only_fans_can_comment = 0; //0,Uint32 是否粉丝才可评论,0所有人可评论(默认),1粉丝才可评论
private String pic_crop_235_1; //封面裁剪为2.35:1规格的坐标字段。以原始图片(thumb_media_id)左上角(0,0),右下角(1,1)建立平面坐标系,经过裁剪后的图片,其左上角所在的坐标即为(X1,Y1),右下角所在的坐标则为(X2,Y2),用分隔符_拼接为X1_Y1_X2_Y2,每个坐标值的精度为不超过小数点后6位数字。示例见下图,图中(X1,Y1) 等于(0.1945,0),(X2,Y2)等于(1,0.5236),所以请求参数值为0.1945_0_1_0.5236。
private String pic_crop_1_1; //封面裁剪为1:1规格的坐标字段,裁剪原理同pic_crop_235_1,裁剪后的图片必须符合规格要求。
}
吐槽: 公众号的api接口文档写的简直依托答辩,返回值错误直接英译汉,而没有说明为什么发生,该怎么解决
4.说明:
1. redisService个人用自己或公司封装,或者自己去写,微信的accessToken两小时过期,具体请看公众号api
2. httpUtils里面上传附件素材含有/t /n 等不要删,删了调用接口就报错,官方api上也没说没写,个人找资料测出来的
3. 上面这些只是基础接口,如果想要自己发布内容,要自己调取相关接口
4. 公众号答辩接口对图片识别很敏感,例如网上找个webp(不支持)的格式,下载本地改名改格式为jpg,这种图微信度不支持会报错
5. 如果api调用发布一个推送(富文本中带图文),我自己写的流程是:
(1). 获取accessToken
(2). 上传永久素材(封面图)。获取封面图media_id
(3). 上传草稿。因为公众号中不识别外部图片,所以content中内图片等附件url要全部替换成腾讯的;步骤是要先获取到富文本中内容,截取富文本中所有图片附件url,然后上传获取到这些图片腾讯返回的url,替换原本的每个图片ur
(4). 草稿发布
(5). 群发消息
6. 注意新增草稿接口中,作者、标题、摘要、正文等度字数限制