package com.grea.qz.controller.other;
import org.apache.log4j.Logger;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.Date;
@RestController
@RequestMapping("/officialAccounts")
public class OfficialAccountsController {
private static Logger logger = Logger.getLogger(OfficialAccountsController.class);
/**
* 验证微信公众号接入
* @param signature 签名
* @param timestamp 时间戳
* @param nonce 随机数
* @param echostr 随机字符串
* @param response
*/
@GetMapping(value = "verify")
public void verify(String signature, String timestamp, String nonce, String echostr, HttpServletResponse response) {
try {
if (SignUtil.checkSignature(signature, timestamp, nonce)) {
PrintWriter out = response.getWriter();
out.print(echostr);
out.close();
} else {
logger.error("这里存在非法请求!");
}
} catch (Exception e) {
logger.error(e, e);
}
}
/**
* 接受用户发送的消息
* @param officialAccountsMessageVo 解析传递回来的xml
* @param request
* @param response
*/
@PostMapping(value = "verify")
public Object verify(@RequestBody OfficialAccountsMessageVo officialAccountsMessageVo, HttpServletRequest request, HttpServletResponse response) {
Object messageVo = new Object();
String msgType = officialAccountsMessageVo.getMsgType();
//根据类型设置不同的消息数据
if("text".equals(msgType)){
// messageVo = setTextMessage("文本内容", officialAccountsMessageVo);
}else if("image".equals(msgType)){
messageVo = setPictureMessage(officialAccountsMessageVo);
} else if("subscribe".equals(msgType)){
// messageVo = setTextMessage("感谢你的关注!", officialAccountsMessageVo);
}
return messageVo;
}
/**
* 具体返回的内容根据实际业务处理
* @param officialAccountsMessageVo
* @return
*/
public OfficialAccountsSendPictureMessageVo setPictureMessage(OfficialAccountsMessageVo officialAccountsMessageVo) {
OfficialAccountsSendPictureMessageVo messageVo = new OfficialAccountsSendPictureMessageVo(); //创建消息响应对象
messageVo.setToUserName(officialAccountsMessageVo.getFromUserName());
messageVo.setFromUserName(officialAccountsMessageVo.getToUserName());
messageVo.setMsgType(officialAccountsMessageVo.getMsgType());
messageVo.setCreateTime(new Date().getTime());
messageVo.setMediaId(new String[]{officialAccountsMessageVo.getMediaId()});
messageVo.setMediaId();
return messageVo;
}
// public OfficialAccountsSendTextMessageVo setTextMessage(String content, OfficialAccountsMessageVo officialAccountsMessageVo) {
// OfficialAccountsSendTextMessageVo messageVo = new OfficialAccountsSendTextMessageVo(); //创建消息响应对象
// messageVo.setToUserName(officialAccountsMessageVo.getFromUserName());
// messageVo.setFromUserName(officialAccountsMessageVo.getToUserName());
// messageVo.setMsgType(officialAccountsMessageVo.getMsgType());
// messageVo.setCreateTime(new Date().getTime());
// messageVo.setContent(content);
// return messageVo;
// }
}
当普通微信用户向公众账号发消息时,微信服务器将POST消息的XML数据包到开发者填写的URL(验证接口)上
关于重试的消息排重,推荐使用msgid排重
微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。假如服务器无法保证在五秒内处理并回复,可以直接回复空串(被动回复消息),微信服务器不会对此作任何处理,并且不会发起重试。
消息样例
<xml>
<ToUserName>ToUserName>
<FromUserName>FromUserName>
<CreateTime>1348831860CreateTime>
<MsgType>MsgType>
<Content>Content>
<MsgId>1234567890123456MsgId>
xml>
消息类型
公众号消息Vo
package com.grea.qz.controller.other;
import lombok.Data;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* 公众号消息Vo
* 可以抽象一个父类,定义多种不同类型的子类Vo
*/
@Data
@XmlRootElement(name="xml") //用于知名xml的根元素
@XmlAccessorType(XmlAccessType.FIELD) //映射所有字段到XML
public class OfficialAccountsMessageVo {
///事件&内容通用部分//
// 开发者微信号
@XmlElement(name="FromUserName") //如果定义的字段名和xml的元素名不一致,可以使用此注解
protected String FromUserName;
// 发送方帐号(一个OpenID)
protected String ToUserName;
// 消息创建时间
protected Long CreateTime;
/**
* 消息类型
* text 文本消息
* image 图片消息
* voice 语音消息
* video 视频消息
* music 音乐消息
*
* 事件的类型
* subscribe 订阅
* unsubscribe 取消订阅
* subscribe 未关注扫描带参数的二维码
* SCAN 已关注扫描带参数的二维
* LOCATION 报地理位置
* CLICK 点击菜单拉取消息时的事件推送
* VIEW 点击菜单跳转链接时的事件推送
*/
protected String MsgType;
///内容部分
// 消息id
protected Long MsgId;
// 文本内容
private String Content;
// 图片链接(由系统生成)
private String PicUrl;
// 图片(语音、视频)消息媒体id,可以调用获取临时素材接口拉取数据
private String MediaId;
//语音格式,如amr,speex等
private String Format;
//语音识别结果,UTF8编码
private String Recognition;
//视频消息缩略图的媒体id,可以调用多媒体文件下载接口拉取数据
private String ThumbMediaId;
//地理位置纬度
private String Location_X;
//地理位置经度
private String Location_Y;
//地图缩放大小
private String Scale;
//地理位置信息
private String Label;
//消息标题
private String Title;
//消息描述
private String Description;
//消息链接
private String Url;
事件部分///
/**
* 事件类型
* subscribe(订阅)
* unsubscribe(取消订阅)
* subscribe(未关注扫描带参数的二维码)
* SCAN(已关注扫描带参数的二维码)
* LOCATION(上报地理位置)
* CLICK(点击菜单拉取消息时的事件推送)
* VIEW(点击菜单跳转链接时的事件推送)
*/
private String Event;
//1、用户未关:事件KEY值,qrscene_为前缀,后面为二维码的参数值
//2、用户已关注:事件KEY值,是一个32位无符号整数,即创建二维码时的二维码scene_id
//3、点击菜单拉取消息:事件KEY值,与自定义菜单接口中KEY值对应
//4、点击菜单跳转链接:事件KEY值,设置的跳转URL
private String EventKey;
//二维码的ticket,可用来换取二维码图片
private String Ticket;
//地理位置纬度
private String Latitude;
//地理位置经度
private String Longitude;
//地理位置精度
private String Precision;
}
公众号签名验证
package com.grea.qz.controller.other;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
/**
* @author zzx
* @version date:2021年1月22日 下午2:50:43
* @description :公众号签名验证
*/
public class SignUtil {
// 与接口配置信息中的 Token 要一致
private final static String token = "zzx";
/**
* 验证签名
*
* @param signature
* @param timestamp
* @param nonce
* @return
*/
public static boolean checkSignature(String signature, String timestamp, String nonce) throws NoSuchAlgorithmException {
String[] arr = new String[]{token, timestamp, nonce};
// 将 token、timestamp、nonce 三个参数进行字典序排序
Arrays.sort(arr);
StringBuilder content = new StringBuilder();
for (int i = 0; i < arr.length; i++) {
content.append(arr[i]);
}
MessageDigest md;
String tmpStr = null;
md = MessageDigest.getInstance("SHA-1");
// 将三个参数字符串拼接成一个字符串进行 sha1 加密
byte[] digest = md.digest(content.toString().getBytes());
tmpStr = byteToStr(digest);
// 将 sha1 加密后的字符串可与 signature 对比,标识该请求来源于微信
return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
}
/**
* 将字节数组转换为十六进制字符串
*
* @param byteArray
* @return
*/
private static String byteToStr(byte[] byteArray) {
String strDigest = "";
for (int i = 0; i < byteArray.length; i++) {
strDigest += byteToHexStr(byteArray[i]);
}
return strDigest;
}
/**
* 将字节转换为十六进制字符串
*
* @param mByte
* @return
*/
private static String byteToHexStr(byte mByte) {
char[] Digit = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
char[] tempArr = new char[2];
tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
tempArr[1] = Digit[mByte & 0X0F];
String s = new String(tempArr);
return s;
}
}
公用的参数
回复文本消息
回复图片消息
<xml>
<ToUserName>ToUserName>
<FromUserName>FromUserName>
<CreateTime>12345678CreateTime>
<MsgType>MsgType>
<Image>
<MediaId>MediaId>
Image>
xml>
回复语音消息
回复视频消息
回复音乐消息
回复图文消息
说明
设置所属行业
获取设置的行业信息
获得模板ID
获取模板列表
http请求方式:GET https://api.weixin.qq.com/cgi-bin/template/get_all_private_template?access_token=ACCESS_TOKEN
{
"template_list": [{
"template_id": "iPk5sOIt5X_flOVKn5GrTFpncEYTojx6ddbt8WYoV5s",
"title": "恭喜你购买成功!",
"primary_industry": "消费品",
"deputy_industry": "消费品",
"content": "{{first.value}}\n名称: {{keyword1.value}}\n消费金额: {{keyword2.value}}\n购买时间: {{keyword3.value}}\n{{remark.value}}",
"example": "恭喜你购买成功!\n名称:巧克力\n消费金额:39.8元\n购买时间:2013-10-10 12:22:22\n欢迎再次购买!"
}]
}
删除模板
发送模板消息
{
"touser":"OPENID",
"template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY",
"url":"http://weixin.qq.com/download",
"miniprogram":{
"appid":"xiaochengxuappid12345",
"pagepath":"index?foo=bar"
},
"data":{
"first": {
"value":"恭喜你购买成功!",
"color":"#173177"
},
"keyword1":{
"value":"巧克力",
"color":"#173177"
},
"keyword2": {
"value":"39.8元",
"color":"#173177"
},
"keyword3": {
"value":"2013-10-10 12:22:22",
"color":"#173177"
},
"remark":{
"value":"欢迎再次购买!",
"color":"#173177"
}
}
}
Articles:图文消息,一个图文消息支持1到8条图文
thumb_media_id:图文消息缩略图的media_id,可以在素材管理-新增素材中获得
author:图文消息的作者
title:图文消息的标题
content_source_url:在图文消息页面点击“阅读原文”后的页面,受安全限制,如需跳转Appstore,可以使用itun.es或appsto.re的短链服务,并在短链后增加 #wechat_redirect 后缀。
content:图文消息页面的内容,支持HTML标签、插入小程序卡片,具备微信支付权限的公众号,可以使用a标签
digest:图文消息的描述,如本字段为空,则默认抓取正文前64个字
show_cover_pic:是否显示封面,1为显示,0为不显示
need_open_comment:Uint32 是否打开评论,0不打开,1打开
only_fans_can_comment:Uint32 是否粉丝才可评论,0所有人可评论,1粉丝才可评论
MediaIdGenerateUtil
package com.grea.qz.controller.other;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import javax.net.ssl.HttpsURLConnection;
import java.io.*;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 公众号发送的图片、视频信息所需要的media的id
*/
public class MediaIdGenerateUtil {
/**
* 上传永久图文素材
* Map map = new HashMap<>();
* map.put("title", "标题");
* map.put("thumb_media_id", "J49eq_VE823b_wZH3Op4DFkLa4Lm4jkTSxX_VbiBWhY"); //上传图片中获取到的media_id
* map.put("author", "作者");
* map.put("digest", "图文消息的摘要,仅有单图文消息才有摘要,多图文此处为空。如果本字段为没有填写,则默认抓取正文前64个字。");
* map.put("show_cover_pic", "1");//显示封面 0/1
* map.put("content", "\"图文消息的具体内容,支持HTML标签,必须少于2万字符,小于1M," +
* "且此处会去除JS,涉及图片url必须来源 \"上传图文消息内的图片获取URL\"接口获取。" +
* "外部图片url将被过滤。\"");
* map.put("content_source_url", "https://www.baidu.com/");//图文消息的原文地址,即点击“阅读原文”后的URL
* map.put("need_open_comment", "1");//Uint32 是否打开评论,0不打开,1打开
* map.put("only_fans_can_comment", "1");//Uint32 是否粉丝才可评论,0所有人可评论,1粉丝才可评论
* @param articles 内容体
* @param accessToken
* @param spammers true:群发 false:上传素材
* @return
* @throws Exception
*/
public static String uploadPermanentMaterial(List<Map<String, Object>> articles, String accessToken, boolean spammers) throws Exception {
Map<String, Object> body = new HashMap<>();
body.put("articles", articles);
String url = "https://api.weixin.qq.com/cgi-bin/material/add_news?access_token=%s";
if(spammers) {
url = "https://api.weixin.qq.com/cgi-bin/media/uploadnews?access_token=%s";
}
url = String.format(url, accessToken);
String result = HttpUtil.post(url, body);
return result;
}
/**
* 上传图文消息内的图片获取URL
* 可用于后续群发中,放置到图文消息中
*
* @param filePath 文件路径
* @param accessToken
* @return
* @throws Exception
*/
public static String uploadMaterial(String filePath, String accessToken) throws Exception {
String urlStr = "https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=%s";
urlStr = String.format(urlStr, accessToken);
return getUploadResult(filePath, urlStr, false);
}
/**
* 上传素材
*
* @param filePath 文件路径
* 媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb)
* @param type 素材类型
* @param accessToken
* @param temp 临时素材
* @return
* @throws Exception
*/
public static String uploadMaterial(String filePath, String type, String accessToken, Boolean temp) throws Exception {
String urlStr = "https://api.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s";
if(!temp) {
urlStr = "https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=%s&type=%s";
if("video".equals(type)) {
urlStr = String.format(urlStr, accessToken, type);
return getUploadResult(filePath, urlStr, true);
}
}
urlStr = String.format(urlStr, accessToken, type);
return getUploadResult(filePath, urlStr, false);
}
private static String getUploadResult(String filePath, String urlStr, boolean PermanentVideo) throws Exception {
String result = null;
File file = new File(filePath);
if (!file.exists() || !file.isFile()) throw new IOException("文件不存在");
URL url = new URL(urlStr);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setRequestMethod("POST");//以POST方式提交表单
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);//POST方式不能使用缓存
//设置请求头信息
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");
if(PermanentVideo) {
sb.append("Content-Disposition: form-data; name=\"description\";\r\n\r\n");
//title:视频素材的标题 introduction:视频素材的描述
sb.append(String.format("{\"title\":\"%s\", \"introduction\":\"%s\"}","title", "introduction"));
sb.append(("\r\n--" + boundary + "--\r\n\r\n"));
}
//获得输出流
OutputStream out = new DataOutputStream(conn.getOutputStream());
//输出表头
out.write(sb.toString().getBytes("UTF-8"));
//文件正文部分
//把文件以流的方式 推送道URL中
DataInputStream din = new DataInputStream(new FileInputStream(file));
int bytes = 0;
byte[] buffer = new byte[1024];
while ((bytes = din.read(buffer)) != -1) {
out.write(buffer, 0, bytes);
}
din.close();
//结尾部分
byte[] foot = ("\r\n--" + boundary + "--\r\n").getBytes("UTF-8");//定义数据最后分割线
out.write(foot);
out.flush();
out.close();
if (HttpsURLConnection.HTTP_OK == conn.getResponseCode()) {
StringBuffer strBuffer = new StringBuffer();
BufferedReader reader = null;
String lineString;
try {
reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
while ((lineString = reader.readLine()) != null) {
strBuffer.append(lineString);
}
if (result == null) {
result = strBuffer.toString();
System.out.println("result:" + result);
}
} catch (IOException e) {
System.out.println("发送POST请求出现异常!" + e);
e.printStackTrace();
} finally {
if (reader != null) {
reader.close();
}
}
}
return result;
}
/**
* 获取临时素材
*
* @param accessToken
* @param type 媒体类型
* 媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb)
* @param mediaId 素材id
* @return
*/
public static String getMaterial(String accessToken, String type, String mediaId) {
String url = "https://api.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s";
if ("voice".equals(type)) {
url = "https://api.weixin.qq.com/cgi-bin/media/get/jssdk?access_token=%s&media_id=%s";
}
url = String.format(url, accessToken, mediaId);
String response = HttpUtil.get(url, 60000);
JSONObject object = JSON.parseObject(response);
return object.toString();
}
/**
* 获取永久素材
*
* @param accessToken
* @param mediaId 素材id
* @return
*/
public static String getPermanentMaterial(String accessToken, String mediaId) {
String url = "https://api.weixin.qq.com/cgi-bin/material/get_material?access_token=%s";
url = String.format(url, accessToken);
Map<String, Object> map = new HashMap<>();
map.put("media_id", mediaId);
String response = HttpUtil.post(url, map);
JSONObject object = JSON.parseObject(response);
return object.toString();
}
/**
* 删除永久素材
*
* @param accessToken
* @param mediaId 素材id
* @return
*/
public static String delMaterial(String accessToken, String mediaId) {
String url = "https://api.weixin.qq.com/cgi-bin/material/del_material?access_token=%s";
url = String.format(url, accessToken);
Map<String, Object> map = new HashMap<>();
map.put("media_id", mediaId);
String response = HttpUtil.post(url, map);
JSONObject object = JSON.parseObject(response);
return object.toString();
}
}