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.公众号配置服务器地址,微信验证成功后,配置白名单

springboot 微信公众号发送消息模板_微信公众平台


注:微信验证服务器地址需要写个接口接收数据进行验证,成功后才能通过,接口已经在下面代码里有了

2.application.yml配置微信的相关信息

springboot 微信公众号发送消息模板_微信_02

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. 注意新增草稿接口中,作者、标题、摘要、正文等度字数限制