首先上效果图
可以看出这是公众号推送的消息,下面还有小程序的链接,可以直接跳转到小程序,这个可以根据发送的消息类型和业务决定是否让跳转到小程序。
业务需求
(都知道小程序无法直接做消息提醒,所以使用公众号来做提醒,还有一种提醒的方式不需要通过公众号就可以完成,就是微信的服务通知,但是这种每次都需要订阅后才可以给你发送通知,只有一些行业的小程序才可以让永久订阅。这种服务通知的教程在: https://blog.csdn.net/weixin_44467567/article/details/111942539)
本来我也想用这种方法,但是我们的小程序没有办法申请永久订阅,如果每次都让销售点击允许本次订阅是不现实的,所以就改用公众号的方式了。
详细步骤
提前准备:微信小程序,微信公众号(服务号),微信开放平台
微信小程序不用说啦,肯定要有的。
微信公众号(服务号)是需要有企业资质才可以申请的,个人无法申请,认证费300RMB
微信开放平台申请,这个需要企业资质,认证费用300RMB
开发前关系绑定:
在公众号后台绑定小程序,原因:公众号绑定了小程序可以再直接跳转到小程序中,位置再公众号后台左侧菜单栏 -> 小程序 -> 小程序管理中。一个公众号可以绑定多个小程序。
把小程序和公众号都绑定到微信开放平台,原因:为了获取unionid,unionid是什么?简单来说,就是开放平台用于区分是否是同一用户的标识,你把多个小程序,公众号,第三方平台等都绑定到开放平台上,取到的unionid都是一样的,这样就可以知道多个不同程序之间的用户是否是同一用户。
我们这里获取unionid就是为了知道使用我们小程序的销售和关注了公众号的销售是不是同一个销售,从而给该销售发送提醒通知,我们在小程序端获取销售的openId和unionid,在公众号端通过关注也获取销售的openId和unionid,其中openId是不同的,unionid是相同的。我们就可以通过小程序的unionid找到公众号的openid。通过openId就知道给谁发送通知。(openId就是小程序或公众号的唯一标识)
公众号白名单配置和服务器配置
白名单配置: 在公众号后台进行配置,开发 -> 基本配置 -> IP白名单。把部署的服务器IP配置进来就可以
服务器配置: 在公众号后台进行配置,开发 -> 基本配置 -> 服务器配置
官方文档:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html
URL: 用户微信服务器调用进行验证和发送信息使用。
Token: 可以随便写,但是要和代码中一致,才可以验证过去
验证服务器配置接口: 要先把接口写好放到服务器中,服务器配置才可以成功。
//这个token要与公众平台服务器配置填写的token一致
private final static String TOKEN = "xxxxxx";
//该接口就是在公众号后台配置的服务器URL
@GetMapping("/mp/serverCheck")
public void doGet(HttpServletRequest request, HttpServletResponse response) throws AesException, IOException {
// 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
String signature = request.getParameter("signature");
// 时间戳
String timestamp = request.getParameter("timestamp");
// 随机数
String nonce = request.getParameter("nonce");
// 随机字符串
String echostr = request.getParameter("echostr");
log.info("\n接收到来自微信服务器的认证消息:[{}, {}, {}, {}]", signature, timestamp, nonce, echostr);
if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
throw new IllegalArgumentException("请求参数非法,请核实!");
}
// 将token、timestamp、nonce三个参数进行字典序排序 2)将三个参数字符串拼接成一个字符串进行sha1加密 3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
String signatureCheck = getSHA1(TOKEN, timestamp, nonce);
log.info("\n加密后的signatureCheck = {}", signatureCheck);
if (signatureCheck.equals(signature)) {
log.info("\n接入成功");
PrintWriter out = response.getWriter();
//原样返回echostr参数
out.print(echostr);
out.flush();
out.close();
} else {
throw new AesException(AesException.ValidateSignatureError);
}
}
SHA1加密方法
/**
* 用SHA1算法验证Token
*
* @param token 票据
* @param timestamp 时间戳
* @param nonce 随机字符串
* @return 安全签名
* @throws AesException
*/
public static String getSHA1(String token, String timestamp, String nonce) throws AesException {
try {
String[] array = new String[]{token, timestamp, nonce};
StringBuffer sb = new StringBuffer();
// 字符串排序
Arrays.sort(array);
for (int i = 0; i < 3; i++) {
sb.append(array[i]);
}
String str = sb.toString();
// SHA1签名生成
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(str.getBytes());
byte[] digest = md.digest();
StringBuffer hexstr = new StringBuffer();
String shaHex = "";
for (int i = 0; i < digest.length; i++) {
shaHex = Integer.toHexString(digest[i] & 0xFF);
if (shaHex.length() < 2) {
hexstr.append(0);
}
hexstr.append(shaHex);
}
return hexstr.toString();
} catch (Exception e) {
e.printStackTrace();
throw new AesException(AesException.ComputeSignatureError);
}
}
获取公众号用户的openId 和 unionId
我这里就不写获取小程序的openId 和 unionId和用户信息了,获取小程序的代码有点杂,是以前写的,不会的可以上网找一下如何获取,如果不绑定开放平台时获取不到unionId的,可以看一下官方文档https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/union-id.html
下面是获取公众号的openId和unionId代码:根据关注的用户进行获取
这个接口就是服务器配置的URL,上面验证的时候也使用过一次,不过验证时get请求,这次是post请求。
Controller层
@Autowired
private WeChatMPService weChatMPService;
@ApiOperation(value = "处理微信服务器发来的消息", notes = "处理微信服务器发来的消息")
@PostMapping("/mp/serverCheck")
public String doPost(HttpServletRequest request, HttpServletResponse response) {
// 调用核心服务类接收处理请求
return weChatMPService.processRequest(request);
}
Service接口层
String processRequest(HttpServletRequest request, String projectId);
ServiceImpl实现类
import com.minapp.management.config.WeChatContant;
import com.minapp.management.service.TdSysMpStaffLoginService;
import com.minapp.management.service.WeChatMPService;
import com.minapp.management.utils.WeChatUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* @ClassName: WeChatServiceImpl
* @Description: 可以结合官网的api看是什么意思 ↓ 消息管理 -> 接受事件推送
* https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html
* @Authror: XQD
* @Date: 2021/1/4 15:42
*/
@Slf4j
@Service
public class WeChatMPServiceImpl implements WeChatMPService {
@Resource
private TdSysMpStaffLoginService mpStaffLoginService;
@Override
public String processRequest(HttpServletRequest request) {
// xml格式的消息数据
String respXml = null;
// 默认返回的文本消息内容
String respContent;
try {
// 调用parseXml方法解析请求消息
Map<String, String> requestMap = WeChatUtil.parseXml(request);
// 消息类型
String msgType = requestMap.get(WeChatContant.MsgType);
log.info("\n消息类型:{}", msgType);
String mes = null;
// 文本消息
if (msgType.equals("text")) {
respContent = "您发送的是文本消息!";
respXml = WeChatUtil.sendTextMsg(requestMap, respContent);
}
// 图片消息
else if (msgType.equals("image")) {
respContent = "您发送的是图片消息!";
respXml = WeChatUtil.sendTextMsg(requestMap, respContent);
}
// 语音消息
else if (msgType.equals("voice")) {
respContent = "您发送的是语音消息!";
respXml = WeChatUtil.sendTextMsg(requestMap, respContent);
}
// 视频消息
else if (msgType.equals("video")) {
respContent = "您发送的是视频消息!";
respXml = WeChatUtil.sendTextMsg(requestMap, respContent);
}
// 地理位置消息
else if (msgType.equals("location")) {
respContent = "您发送的是地理位置消息!";
respXml = WeChatUtil.sendTextMsg(requestMap, respContent);
}
// 链接消息
else if (msgType.equals("link")) {
respContent = "您发送的是链接消息!";
respXml = WeChatUtil.sendTextMsg(requestMap, respContent);
}
// 事件推送
else if (msgType.equals("event")) {
// 事件类型
String eventType = requestMap.get("Event");
log.info("\n事件类型为:{}", eventType);
// 关注
if (eventType.equals("subscribe")) {
mpStaffLoginService.subscribeMPUserInfo(requestMap.get(WeChatContant.FromUserName));
respContent = "谢谢您的关注!";
respXml = WeChatUtil.sendTextMsg(requestMap, respContent);
}
// 取消关注
else if (eventType.equals("unsubscribe")) {
mpStaffLoginService.unsubscribeMPUserInfo(requestMap.get(WeChatContant.FromUserName));
// TODO 取消订阅后用户不会再收到公众账号发送的消息,因此不需要回复
}
// 扫描带参数二维码
else if (eventType.equals("SCAN")) {
// TODO 处理扫描带参数二维码事件
}
// 上报地理位置
else if (eventType.equals("LOCATION")) {
// TODO 处理上报地理位置事件
}
// 自定义菜单
else if (eventType.equals("CLICK")) {
// TODO 处理菜单点击事件
}
}
mes = mes == null ? "不知道你在干嘛" : mes;
if (respXml == null) {
respXml = WeChatUtil.sendTextMsg(requestMap, mes);
}
log.info("\n"+respXml);
return respXml;
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
}
WeChatUtil工具类
package com.minapp.management.utils;
import com.minapp.management.config.WeChatContant;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
/**
* @ClassName: WeChatUtil
* @Description: 请求校验工具类
* @Authror: XQD
* @Date: 2021/1/4 15:35
*/
public class WeChatUtil {
/**
* 将字节数组转换为十六进制字符串
*
* @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;
}
private static void sort(String a[]) {
for (int i = 0; i < a.length - 1; i++) {
for (int j = i + 1; j < a.length; j++) {
if (a[j].compareTo(a[i]) < 0) {
String temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
}
/**
* 解析微信发来的请求(xml)
*
* @param request
* @return
* @throws Exception
*/
@SuppressWarnings({ "unchecked"})
public static Map<String,String> parseXml(HttpServletRequest request) throws Exception {
// 将解析结果存储在HashMap中
Map<String,String> map = new HashMap<String,String>();
// 从request中取得输入流
InputStream inputStream = request.getInputStream();
// 读取输入流
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子节点
List<Element> elementList = root.elements();
// 遍历所有子节点
for (Element e : elementList){
map.put(e.getName(), e.getText());
}
// 释放资源
inputStream.close();
inputStream = null;
return map;
}
public static String mapToXML(Map map) {
StringBuffer sb = new StringBuffer();
sb.append("" );
mapToXML2(map, sb);
sb.append("");
try {
return sb.toString();
} catch (Exception e) {
}
return null;
}
private static void mapToXML2(Map map, StringBuffer sb) {
Set set = map.keySet();
for (Iterator it = set.iterator(); it.hasNext();) {
String key = (String) it.next();
Object value = map.get(key);
if (null == value){
value = "";
}
if (value.getClass().getName().equals("java.util.ArrayList")) {
ArrayList list = (ArrayList) map.get(key);
sb.append("<" + key + ">");
for (int i = 0; i < list.size(); i++) {
HashMap hm = (HashMap) list.get(i);
mapToXML2(hm, sb);
}
sb.append(" + key + ">");
} else {
if (value instanceof HashMap) {
sb.append("<" + key + ">");
mapToXML2((HashMap) value, sb);
sb.append(" + key + ">");
} else {
sb.append("<" + key + "> + value + "]]> + key + ">");
}
}
}
}
/**
* 回复文本消息
* @param requestMap
* @param content
* @return
*/
public static String sendTextMsg(Map<String,String> requestMap,String content){
Map<String,Object> map=new HashMap<String, Object>();
map.put("ToUserName", requestMap.get(WeChatContant.FromUserName));
map.put("FromUserName", requestMap.get(WeChatContant.ToUserName));
map.put("MsgType", WeChatContant.RESP_MESSAGE_TYPE_TEXT);
map.put("CreateTime", System.currentTimeMillis());
map.put("Content", content);
return mapToXML(map);
}
}
在用户关注了,会触发关注事件,微信服务器通过接口给你发送上面的一段xml信息,里面包括了openId和一些其他信息,解析出来后,可以通过openId 和access_token (access_token获取方法在我推送服务通知的博客里面) 获取到用户的昵称,头像,unionId 等等一些信息,把这些信息存入数据库中就可以。代码如下
service接口
import com.alibaba.fastjson.JSONObject;
import com.minapp.management.entity.TdSysMpStaffLogin;
import com.baomidou.mybatisplus.extension.service.IService;
/**
*
* 服务类
*
*
* @author XQD
* @since 2021-01-04
*/
public interface TdSysMpStaffLoginService extends IService<TdSysMpStaffLogin> {
// 关注公众号事件调用的接口
public void subscribeMPUserInfo(String openId);
// 取消关注公众号事件调用的接口
public void unsubscribeMPUserInfo(String openId);
// 发送模板信息接口
boolean sendTemplateMsg(String staffIds, String messageType, JSONObject param);
}
ServiceImpl实现类
@Resource
private ObjectMapper objectMapper;
/**
* @Description: 订阅的公众号用户信息存入数据库
* @Param: [projectId, openId]
* @return: void
* @Author: XQD
* @Date:2021/1/4 17:45
*/
@Override
public void subscribeMPUserInfo(String openId) {
String url = "https://api.weixin.qq.com/cgi-bin/user/info?access_token="+getAccessToken()+"&openid="+openId+"&lang=zh_CN";
String mpUserInfo = HttpClientUtil.get(url);
Map<String, Object> map = null;
try {
map = objectMapper.readValue(mpUserInfo, Map.class);
} catch (IOException e) {
log.error("公众号异常通知-获取用户信息转化异常", e);
}
TdSysMpStaffLogin mpStaffLogin = new TdSysMpStaffLogin();
mpStaffLogin.setId(GeneratorIDUtil.generatorId())
.setOpenId(openId)
.setProjectId(projectId)
.setNickName((String) map.get("nickname"))
.setHeadImageUrl((String) map.get("headimgurl"))
.setSex((Integer) map.get("sex"))
.setSubscribe((Integer) map.get("subscribe"))
.setUnionId((String) map.get("unionid"))
.setCountry((String) map.get("country"))
.setProvince((String) map.get("province"))
.setCity((String) map.get("city"))
.setSubscribeScene((String) map.get("subscribe_scene"));
UpdateWrapper<TdSysMpStaffLogin> mpStaffLoginUpdateWrapper = new UpdateWrapper<>();
mpStaffLoginUpdateWrapper.set("subscribe",1)
.eq("open_id",openId);
//数据库操作
if (mpStaffLoginService.saveOrUpdate(mpStaffLogin, mpStaffLoginUpdateWrapper)){
log.info("\n关注的用户信息添加成功 openId = {}", openId);
}else {
log.info("\n关注的用户信息添加失败 openId = {}", openId);
}
}
/**
* @Description: 取消订阅用户
* @Param: [projectId, openId]
* @return: void
* @Author: XQD
* @Date:2021/1/4 17:45
*/
@Override
public void unsubscribeMPUserInfo(String openId) {
// 数据库操作,就是把关注的状态改一下
UpdateWrapper<TdSysMpStaffLogin> mpStaffLoginUpdateWrapper = new UpdateWrapper<>();
mpStaffLoginUpdateWrapper.set("subscribe",0)
.eq("open_id",openId);
if (mpStaffLoginService.update(mpStaffLoginUpdateWrapper)){
log.info("\n取消关注的用户操作成功 openId = {}", openId);
}else {
log.info("\n取消关注的用户操作失败 openId = {}", openId);
}
}
HttpClientUtil类就是用来调用外部接口的,就不贴出来了,