之前做项目有就接触微信公众号的接入,但没有将过程记录成笔记,这几天在做的项目也是需要集成微信公众号,正好将在做的过程记录成笔记
文章目录
总概
1、要想微信公众号接入到自己的项目中,那么项目中需要有微信公众号的配置,公众号平台也要有后端的配置,这样才能使项目和公众号具有关联性;
2、需要拥有一个域名,公众号调用后端接口只能通过域名的方式调用,无法通过ip+端口直接访问
3、想要定位到当前操作的用户,需要网页开启授权,通过提供的code值来获取到openid
0、准备工作
配置:在项目中配置公众号信息,在公众号平台绑定项目路径
一、公众号平台
在开发阶段可以使用测试号来调试开发,网站地址:微信公众平台
扫码登录进入
1、参数解释
- 测试号信息
appID和appsercret相当于是公众号的唯一凭证,用来验证匹配公众号的
- 接口配置信息修改
URL:接入公众号的项目后端接入公众号接口的地址
注意:
1、接口地址必须是域名+接口路径
的形式,形如:http://xxx.xx.com/xxx/access
2、这个URL可以晚些再写,因为点击提交时平台会调用填写的URL里面的接口来验证后端是否成功接入
所以需要有个域名,获取域名方式
Token:这个命名可以随意命名,但要保证与后端配置中保持一致
- js接口安全域名修改
域名:域名地址
注意:不要有http://,就是单纯的域名url
2、获取域名
市面上有很多内网穿透的软件工具,我使用的是natapp
里面有免费版的收费的,免费的域名会随机分配,并不定时变动,我去年用免费版的是可以调试接入公众号的,但现在用免费版的接口根本调不通了,无法白嫖了😔
收费的一个月十来块钱,是可以调通接口的,可以申请一个月玩玩
网址:https://natapp.cn/
申请完成后,留意authtoken信息
并下载他的软件
下载解压之后,双击exe文件打开
输入命令:
natapp -authtoken 自己的authtoken
回车打开
成功会形如:
这时,生成的域名便映射到localhost:8081这个地址上了,域名获取成功
注意:在后端调试过程中,这个cmd窗口不能关闭
二、后端配置
需要把微信公众号上的参数绑定到后端项目上,参数包括appid,appsecret,token,域名地址
注意:参数名称命名没有要求,只要保证对应的参数值一致就行
形如:
#微信公众号
##测试号
wxToken=xbwangxxx
appId=wxc1xxx
appsecret=a09c2118xxxxxxxxxx
### 域名
DNS_URl=http://xxx.natapp1.cc
准备工作完成后,便可以开始接入微信公众号了
第一阶段-公众号接入
1、接入过程描述
在后端编写一个接入接口,然后微信平台去调用这个接口,如果调通,则接入成功
2、实现过程
在准备工作开始时,配置了接口配置信息,现在可以开始URL的接口编写了
创建接口
controller层
/**
* 微信公众号接口
* @author xbwang
* @date 2023/9/05 11:14
**/
@Controller
@RequestMapping("/wechat")
public class WeChatController {
@Resource
private WeChatService weChatService;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 微信公众号接入
* @param request
* @param response
*/
@GetMapping("/access")
public void wxlogin(HttpServletRequest request, HttpServletResponse response) {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
PrintWriter out=null;
try {
out = response.getWriter();
} catch (IOException e) {
logger.error("获取PrintWriter异常");
}
if (SignUtil.checkSignature(signature, timestamp, nonce)) {
logger.debug("微信公众号接入成功!");
out.print(echostr);
}
out.close();
}
对应的校验签名工具类
public class SignUtil{
/**
* 校验签名
* @param signature 微信加密签名.
* @param timestamp 时间戳.
* @param nonce 随机数.
* @return
*/
public static boolean checkSignature(String signature, String timestamp, String nonce) {
// 对token、timestamp、和nonce按字典排序.
String[] paramArr = new String[] {PropertiesUtil.getProperty("wxToken"), timestamp, nonce};
Arrays.sort(paramArr);
// 将排序后的结果拼接成一个字符串.
String content = paramArr[0].concat(paramArr[1]).concat(paramArr[2]);
String ciphertext = null;
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
// 对拼接后的字符串进行sha1加密.
byte[] digest = md.digest(content.toString().getBytes());
ciphertext = byteToStr(digest);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
// 将sha1加密后的字符串与signature进行对比.
return ciphertext != null ? ciphertext.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;
}
}
编写好后,运行后端项目,URL信息就可以填写上了,形如 http://域名/(项目虚拟路径)/wechat/access
在接口处打上断点,点击提交,如果进入断点,说明接入成功
第二阶段-获取access_token
很多跟微信公众号相关的接口都需要携带access_token这个值,但这个值有以下特点:
1、不是固定值,需要通过调用接口获取;
2、具有时效性,2小时后会过期;
3、获取接口每天有上限,也就是调用多了平台会设限;
获取方式:
get请求
https请求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
把APPID和APPSECRET替换掉执行接口即可
获取策略:
根据这些特点,提供一个获取策略:
存储到数据库中,需要使用时访问数据库获取
写一个定时任务,每一小时执行一次,调用获取access_token接口,取到值后,更新存储起来(redis,mysql里都可以),那么需要access_token的接口就从数据库/缓存中取access_token,这样就避免了频繁调用获取access_token的接口
第三阶段-被动回复消息
1、情景描述
给公众号发送消息时,公众号会回复相应内容,形如
上面回复的内容就是我自定义的内容
2、功能实现
controller层
注意:接口地址要跟接入的那个接口
名称一致
,可以写2个接口(一个post请求,一个get请求),也可以整合到一个接口里(即1个接口具备2个功能:1、公众号接入认证,2、消息被动回复)
/**
* 消息被动回复
*
* @param request
* @param response
*/
@ApiOperation("消息被动回复")
@PostMapping("/access")
public void feedbackMsg(HttpServletRequest request, HttpServletResponse response) {
//响应消息
PrintWriter out = null;
try {
response.setCharacterEncoding("utf-8");
out = response.getWriter();
//被动回复消息
String feedmsg = null;
feedmsg = weChatService.feedbackMsg(request, response);
logger.info("被动回复消息:{}", feedmsg);
out.print(feedmsg);
} catch (IOException e) {
logger.error("获取PrintWriter异常", e);
}
out.close();
}
service层
/**
* @author xbwang
* @date 2023/9/5 14:35
**/
@Service
public class WeChatService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource
private WxTokenMapper wxTokenMapper;
@Resource
private WxUserMapper wxUserMapper;
public Integer updateToken(WxToken wxToken){
return wxTokenMapper.updateByIdNoHis(wxToken);
}
public String feedbackMsg(HttpServletRequest req, HttpServletResponse resp) throws IOException {
req.setCharacterEncoding("UTF-8");
String message = "success";
//响应消息
PrintWriter out=null;
resp.setCharacterEncoding("utf-8");
out = resp.getWriter();
// 把微信返回的xml信息转义成map
try {
Map<String, String> xmlMessage = WxMessageUtil.xmlToMap(req);
String fromUserName = xmlMessage.get("FromUserName"); // 这个就是你关注公众号的openId
String toUserName = xmlMessage.get("ToUserName"); // 这个是用户微信的id
String msgType = xmlMessage.get("MsgType"); // 消息类型(event或者text)
String createTime = xmlMessage.get("CreateTime"); // 消息创建时间 (整型)
logger.info("消息来自(公众号的openId)=>" + fromUserName);
logger.info("用户微信的id=>" + toUserName);
logger.info("消息类型为=>" + msgType);
logger.info("消息创建时间 (整型)=>" + createTime);
if ("event".equals(msgType)) { // 如果是事件推送
String eventType = xmlMessage.get("Event"); // 事件类型
if ("subscribe".equals(eventType)) { // 如果是订阅消息
String subscribeContent = "感谢关注";
String subscribeReturnXml = WxMessageUtil.getWxReturnMsg(xmlMessage, subscribeContent);
return subscribeReturnXml;
}
if ("SCAN".equals(eventType)) { // 如果是扫码消息
String scanContent = "扫码成功";
String scanReturnXml = WxMessageUtil.getWxReturnMsg(xmlMessage, scanContent);
return scanReturnXml;
}
}
if ("text".equals(msgType)) { // 如果是文本消息推送
String content = xmlMessage.get("Content"); // 接收到的消息内容,可以对特定的内容添加定制化回复
String textReturnXml = WxMessageUtil.getWxReturnMsg(xmlMessage, content);
return textReturnXml; // 将接收到的文本消息变成xml格式再返回
}
} catch (Exception e) {
e.printStackTrace();
}
return message;
}
}
对应的工具类
/**
* 微信消息处理类(微信消息交互大部分就是xml格式交互)
*/
@Slf4j
public class WxMessageUtil {
/*
* xml转map
*/
public static Map<String, String> xmlToMap(HttpServletRequest request) throws IOException, DocumentException {
HashMap<String, String> map = new HashMap<String,String>();
SAXReader reader = new SAXReader();
InputStream ins = request.getInputStream();
Document doc = reader.read(ins);
Element root = doc.getRootElement();
@SuppressWarnings("unchecked")
List<Element> list = (List<Element>)root.elements();
for(Element e:list){
map.put(e.getName(), e.getText());
}
ins.close();
return map;
}
/**
* 获取公众号回复信息(xml格式)
*/
public static String getWxReturnMsg(Map<String, String> decryptMap, String content) throws UnsupportedEncodingException {
log.info("---开始封装xml---decryptMap:" + decryptMap.toString());
TextMessage textMessage = new TextMessage();
textMessage.setToUserName(decryptMap.get("FromUserName"));
textMessage.setFromUserName(decryptMap.get("ToUserName"));
textMessage.setCreateTime(System.currentTimeMillis());
textMessage.setMsgType("text"); // 设置回复消息类型
textMessage.setContent(content); // 设置回复内容
String xmlMsg = getXmlString(textMessage);
// 设置返回信息编码,防止中文乱码
// String encodeXmlMsg = new String(xmlMsg.getBytes(), "ISO-8859-1");
String encodeXmlMsg = new String(xmlMsg.getBytes(), "GBK");
return encodeXmlMsg;
}
/**
* 设置回复消息xml格式
*/
private static String getXmlString(TextMessage textMessage) {
String xml = "";
if (textMessage != null) {
xml = "<xml>";
xml += "<ToUserName><![CDATA[";
xml += textMessage.getToUserName();
xml += "]]></ToUserName>";
xml += "<FromUserName><![CDATA[";
xml += textMessage.getFromUserName();
xml += "]]></FromUserName>";
xml += "<CreateTime>";
xml += textMessage.getCreateTime();
xml += "</CreateTime>";
xml += "<MsgType><![CDATA[";
xml += textMessage.getMsgType();
xml += "]]></MsgType>";
xml += "<Content><![CDATA[";
xml += textMessage.getContent();
xml += "]]></Content>";
xml += "</xml>";
}
log.info("xml封装结果=>" + xml);
return xml;
}
}
pojo
import lombok.Data;
/**
* 微信消息自动回复模板类
*/
@Data
public class TextMessage {
private String toUserName;
private String fromUserName;
private Long createTime;
private String msgType;
private String content;
}
编写好后,给测试公众号发送消息,公众号会回复相同的消息给你
第四阶段-菜单栏配置
很多公众号都会有多个菜单栏,并且有些菜单栏还有几个子菜单,形如
这个实现方式可以通过接口工具调用菜单配置的接口即可,平台上也有介绍:自定义菜单
自己实现的话,最简单的方式就是用postman工具调用,具体如下
拷贝平台提供的接口地址,把ACCESS_TOKEN替换为自己的
https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN
post请求,body里用json配置菜单栏信息
json数据参考
{
"button": [
{
"type": "click",
"name": "图文获取",
"key": "图文"
},
{
"type": "view",
"name": "授权获取",
"url": "https://www.baidu.com"
},
{
"name": "乡镇治理",
"sub_button": [
{
"type": "view",
"name": "告警记录",
"url": "https://www.baidu.com"
}
]
}
]
}
发送请求,如果返回ok说明菜单栏配置成功,点击右边2个菜单可以实现页面跳转
注意:菜单栏名称命名不能太长(最多5个中文),否则会配置失败
第五阶段-模板通知推送
有2种实现方式
1、直接调用接口工具,模板通知在body里配置
2、后端代码实现
第一种方式可以快速体验效果,如果上升到生产上还是得用后端代码来实现
不管第一种还是第二种,首先都需要在平台上先配置下消息模板
配置一个就可,形如:
{{first.DATA}}
设备名称:{{keyword1.DATA}}
事件类型:{{keyword2.DATA}}
发生时间:{{keyword3.DATA}}
{{remark.DATA}}
提交会生成一个模板消息,留意左边有一个模板id,复制好调用接口的时候会用上
第一种方式
先看官方文档:模板消息
POST请求
https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN
ACCESS_TOKEN替换成自己的
json数据中,
参数解释
touser:是用户openid,即微信号,需要给谁推送消息,就把对应用户的openid写上
template_id:模板消息id
url:点击详情跳转的页面
{
"touser":"onM576KL4nxxx",
"template_id":"H_wvrVIoDdBLsBEAyLtpJVdxxx",
"url":"https://www.baidu.com",
"data":{
"first": {
"value":"秋江街道新西街",
"color":"#fa052a"
},
"keyword1":{
"value":"10001xnwmz",
"color":"#173177"
},
"keyword2": {
"value":"工程车",
"color":"#173177"
},
"remark":{
"value":"标记为真实报警",
"color":"#173177"
}
}
}
若请求成功,可得到如下效果
第二种方式
通过代码实现
思路:可以编写一块逻辑,当满足条件时,触发推送模板消息
形如:
public void method(){
//方法体
if(条件){
//满足条件,调用推送方法
sendTranTemplate()
}
}
推送方法
//模板消息
public static String sendTranTemplate(WXTransportTemplate resource,String accessToken){
//accessToken就是access_token
//发送模板消息开始
TranTemplate tranTemplate = new TranTemplate();
//设置接收司机微信ID
tranTemplate.setTouser(resource.getOpenid());
//设置模板ID
tranTemplate.setTemplate_id(resource.getTemplateId());
//详情链接
tranTemplate.setUrl(resource.getUrl());
//给模板的内容赋值
Map<String , TemplateInfo> dataMap = new HashMap<>();
TemplateInfo first = new TemplateInfo(resource.getFirst(),"#DC143C");
dataMap.put("first",first);
TemplateInfo keyword1 = new TemplateInfo(resource.getKeyword1(),"#173177");
dataMap.put("keyword1",keyword1);
TemplateInfo keyword2 = new TemplateInfo(resource.getKeyword2(),"#173177");
dataMap.put("keyword2",keyword2);
TemplateInfo keyword3 = new TemplateInfo(resource.getKeyword3(),"#173177");
dataMap.put("keyword3",keyword3);
TemplateInfo remark = new TemplateInfo(resource.getRemark(),"#DC143C");
dataMap.put("remark",remark);
tranTemplate.setData(dataMap);
JSONObject jsonObject = JSONUtil.parseObj(tranTemplate);
//将入参转为字符串
String jsonParam = jsonObject.toString();
//发起请求发送模板信息
String sendTemplateUrl = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=";
String result = HttpUtil.requestPost(sendTemplateUrl + accessToken, jsonParam);
return result;
}
pojo实体类
import lombok.Data;
//入参类
@Data
public class WXTransportTemplate {
private String templateId;//模板id
private String openid;//目标客户
private String url;//详情页
private String first;
private String keyword1;
private String keyword2;
private String keyword3;
private String remark;
}
第六阶段-菜单跳转携带openid等信息
1、如果只需要实现菜单页面跳转的功能的话,那直接在配置菜单时设置跳转前端路径即可
2、但如果希望跳转的页面携带着当前账号信息等参数信息时,那么需要写一个后端接口,根据微信提供的code来获取openid等用户信息,再通过重定向实现携带参数跳转到前端页面
接口如下:
/**
* 菜单栏页面跳转
*
* @param request
* @param response
* @return
*/
@RequestMapping("/homePage")
public Object wx2alarm(HttpServletRequest request, HttpServletResponse response, String code) {
String openid = null;
String userAgent = request.getHeader("user-agent").toLowerCase();
if (userAgent != null) {
if (userAgent.contains("android") || userAgent.contains("iphone") || userAgent.contains("wechat")) {
//手机端或者微信客户端
System.out.println(code);
//获取微信id
String requestUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid="+appId+"&secret=" +appsecret+
"&code=" + code + "&grant_type=authorization_code";
// ResponseEntity<WxToken> entity = restTemplate.getForEntity(requestUrl, WxToken.class);
String conent = restTemplate.getForObject(requestUrl, String.class);
// WxToken token = entity.getBody();
// openid = token.getOpenid();
System.out.println(openid);
logger.info("openid={}",openid);
}
}
return "redirect:" + dns_url + "/screen/html/index.html?openid=" + openid;//重定向路径改为自己需要跳转的页面路径
}
配置菜单跳转的路径设置为这个接口路径