1.前置教程参考(给用户推送模板消息,需要全部看完在进行接下来得给指定人员推送模板消息业务)
Java微信公众号发送消息-保姆级教程附源码_java微信公众号发消息给用户-CSDN博客
2.针对指定人员推送模板消息代码修改后展示
主要改的地方是这个从官方得获取公众号下得所有粉丝得openid,改为从库里拿取。
需要利用官方文档得 网页授权 接口拿到openid,我这边实现的是通过手机号查到这个人的openid。
/**
* 发送模板消息
*/
@PostMapping("/message")
public ResponseEntity
// String token = getToken();
// String url = "https://api.weixin.qq.com/cgi-bin/user/get?" + "access_token=" + token;
// // 获取 openid 数组
// List
//数据库查用户openid
String openId = wxUserService.getOpenIdByPhone(sendMessage.getPhone());
if (StringUtils.isBlank(openId)) {
return ResponseEntity.ok("未查询到该用户的信息");
}
// 主要的业务逻辑:
TemplateMessage templateMessage = new TemplateMessage();
templateMessage.setTouser(openId);
templateMessage.setTemplate_id(messageTemplateId);
templateMessage.setTopcolor("#FF0000");
// key对应创建模板内容中的形参
// WeChatTemplateMsg对应实参和字体颜色
Map
data.put("title", new WeChatTemplateMsg("你有一条新的消息", "#173177"));
data.put("username", new WeChatTemplateMsg(sendMessage.getPatientName(), "#173177"));
data.put("docker", new WeChatTemplateMsg(sendMessage.getDockerName(), "#173177"));
templateMessage.setData(data);
System.out.println(templateMessage);
WXPublicAccountHttpUtil.sendMessage(getToken(), templateMessage);
return ResponseEntity.ok("通知成功");
}
@Data
public class SendMessage {
/**
* 患者手机号
*/
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1\\d{10}$", message = "手机号格式不正确")
private String phone;
/*
* 医生名
*/
@NotBlank(message = "医生不能为空")
private String dockerName;
/*
* 患者名
*/
@NotBlank(message = "患者不能为空")
private String patientName;
}
接下来就是如何将公众号下的关注人员信息映射到表里。
我们用回调url(post请求)可以捕获到用户得关注和取消关注事件,然后逻辑里构建xml消息,当然这个xml消息需要返回最好是图文,这样用户点击图文就可以跳转到网页授权接口,无感授权用户信息
/**
* 接收事件推送 (POST请求)
*/
@PostMapping("")
public ResponseEntity
// 解析XML
Document document = DocumentHelper.parseText(xmlData);
Element root = document.getRootElement();
// 获取关键字段
String msgType = root.elementText("MsgType");
String event = root.elementText("Event");
String fromUser = root.elementText("FromUserName");
String toUser = root.elementText("ToUserName");
// 处理关注事件
if ("event".equals(msgType) && "subscribe".equals(event)) {
return ResponseEntity.ok(buildWelcomeMessage(fromUser, toUser));
}
// 处理取消关注事件
if ("event".equals(msgType) && "unsubscribe".equals(event)) {
wxUserService.deleteByOpenId(fromUser);
return ResponseEntity.ok("success");
}
return ResponseEntity.ok("success");
}
/**
* 构建欢迎消息XML
*/
private String buildWelcomeMessage(String fromUser, String toUser) {
String s = wxAuthController.redirectToWechatAuth();
return String.format(
"
"
"
"
"
"
"
"
"
"
"
"
"" +
"" +
"",
fromUser, toUser, Calendar.getInstance().getTimeInMillis() / 1000,s
);
}
上面这个post请求以及上个文档说的get请求需要绕过权限认证,不然需要携带token
小流程:
网页授权: 用户访问自动回复得授权图文进入授权链接并同时跳转到前端页面并返回code值。 用户完成授权后,前端调用/wx/callback并携带code和state。获取到access_token和openid。 然后用户填写相关信息后提交调用后端 存用户saveWxUser得接口 若为snspai_userinfo,继续调用接口获取用户信息。 完成业务逻辑
需要一个h5页面,这个页面上应有用户得手机号填写,用户的一些信息填写,然后页面把这些数据回传给后端然后入库。
/**
* 存储微信用户信息
*/
@PostMapping ("saveWxUser")
public ResponseEntity
//存储逻辑
wxUserService.saveWxUser(wxUser1);
return ResponseEntity.ok(ResultUtil.successJson("success"));
}
然后调用推送模板http://你自己的域名/wx/message传入用户信息进行推送
然后在公众号查看
整体流程:
用户关注公众号->公众号返回授权图文->用户点击授权图文->跳转至h5页面用户填写相关信息->提交后用户信息以及openid入库->调用通知接口携带用户手机号相关信息->公众号向用户推送消息
整体代码:
WxSendController
import com.alibaba.fastjson.JSONObject;
import com.geeke.utils.StringUtils;
import com.geeke.wx.dto.SendMessage;
import com.geeke.wx.dto.TemplateMessage;
import com.geeke.wx.dto.WeChatTemplateMsg;
import com.geeke.wx.service.WxUserService;
import com.geeke.wx.utils.AccessToken;
import com.geeke.wx.utils.WXPublicAccountHttpUtil;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
/**
* 微信服务调用接口
*/
@Controller
@RequestMapping("/wx")
public class WxSendController {
@Value("${wx.token}")
private String wxToken;
@Value("${wx.appid}")
private String appid;
@Value("${wx.secret}")
private String secret;
@Value("${wx.templateId}")
private String messageTemplateId;
@Autowired
private WxUserService wxUserService;
@Autowired
private WxAuthController wxAuthController;
/**
* 全局AccessToken
*/
private AccessToken at;
/**
* 校验配置URL服务器的合法性
*/
@GetMapping("")
public void doGetWx(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
// 将微信echostr返回给微信服务器
try (OutputStream os = response.getOutputStream()) {
String sha1 = getSHA1(wxToken, timestamp, nonce, "");
// 和signature进行对比
if (sha1.equals(signature)) {
// 返回echostr给微信
os.write(URLEncoder.encode(echostr, "UTF-8").getBytes());
os.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 接收事件推送 (POST请求)
*/
@PostMapping("")
public ResponseEntity
// 解析XML
Document document = DocumentHelper.parseText(xmlData);
Element root = document.getRootElement();
// 获取关键字段
String msgType = root.elementText("MsgType");
String event = root.elementText("Event");
String fromUser = root.elementText("FromUserName");
String toUser = root.elementText("ToUserName");
// 处理关注事件
if ("event".equals(msgType) && "subscribe".equals(event)) {
return ResponseEntity.ok(buildWelcomeMessage(fromUser, toUser));
}
// 处理取消关注事件
if ("event".equals(msgType) && "unsubscribe".equals(event)) {
wxUserService.deleteByOpenId(fromUser);
return ResponseEntity.ok("success");
}
return ResponseEntity.ok("success");
}
/**
* 构建欢迎消息XML
*/
private String buildWelcomeMessage(String fromUser, String toUser) {
String s = wxAuthController.redirectToWechatAuth();
return String.format(
"
"
"
"
"
"
"
"
"
"
"
"
"" +
"" +
"",
fromUser, toUser, Calendar.getInstance().getTimeInMillis() / 1000,s
);
}
/**
* 发送模板消息
*/
@PostMapping("/message")
public ResponseEntity
// String token = getToken();
// String url = "https://api.weixin.qq.com/cgi-bin/user/get?" + "access_token=" + token;
// // 获取 openid 数组
// List
//数据库查用户openid
String openId = wxUserService.getOpenIdByPhone(sendMessage.getPhone());
if (StringUtils.isBlank(openId)) {
return ResponseEntity.ok("未查询到该用户的信息");
}
// 主要的业务逻辑:
TemplateMessage templateMessage = new TemplateMessage();
templateMessage.setTouser(openId);
templateMessage.setTemplate_id(messageTemplateId);
templateMessage.setTopcolor("#FF0000");
// key对应创建模板内容中的形参
// WeChatTemplateMsg对应实参和字体颜色
Map
data.put("title", new WeChatTemplateMsg("你有一条新的消息", "#173177"));
data.put("username", new WeChatTemplateMsg(sendMessage.getPatientName(), "#173177"));
data.put("docker", new WeChatTemplateMsg(sendMessage.getDockerName(), "#173177"));
templateMessage.setData(data);
System.out.println(templateMessage);
WXPublicAccountHttpUtil.sendMessage(getToken(), templateMessage);
return ResponseEntity.ok("通知成功");
}
/**
* 获取token, 本地缓存有就直接返回,没有就发送请求获取(wx官方api获取token每天有限制,因此需做缓存)
*/
public String getToken() {
if (at == null || at.isExpired()) {
getAccessToken();
}
return at.getAccessToken();
}
/**
* 给AccessToken赋值
*/
private void getAccessToken() {
// 发送请求获取token
String token = null;
try {
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential" + "&appid=" + appid + "&secret=" + secret;
token = WXPublicAccountHttpUtil.get(url);
} catch (Exception e) {
e.printStackTrace();
}
JSONObject jsonObject = JSONObject.parseObject(token);
String accessToken = (String) jsonObject.get("access_token");
Integer expiresIn = (Integer) jsonObject.get("expires_in");
// 创建token对象,并存储
at = new AccessToken(accessToken, String.valueOf(expiresIn));
System.out.println(token);
}
/**
* 用SHA1算法生成安全签名
*
* @param token 票据
* @param timestamp 时间戳
* @param nonce 随机字符串
* @param encrypt 密文
* @return 安全签名
*/
public String getSHA1(String token, String timestamp, String nonce, String encrypt) throws Exception {
try {
String[] array = new String[]{token, timestamp, nonce, encrypt};
StringBuffer sb = new StringBuffer();
// 字符串排序
Arrays.sort(array);
for (int i = 0; i < 4; 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();
}
return "";
}
}
wx:
token: 需与后台页面得token保持一直
appid: 开发者appid
secret: 开发者密码
templateId: 模板id
redirectUri: http://你的域名/wxAuth/callback #需要重定向到前端页面地址 然后前端需要调用这个后端接口
WxAuthController
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONObject;
import com.geeke.utils.ResultUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@Controller
@RequestMapping("/wxAuth")
public class WxAuthController {
@Value("${wx.appid}")
private String appId;
@Value("${wx.secret}")
private String secret;
@Value("${wx.redirectUri}")
private String redirectUri;
private final RestTemplate restTemplate = new RestTemplate();
/**
* 引导用户进入授权页面
*/
@GetMapping("/auth")
public String redirectToWechatAuth() {
// 授权作用域,snsapi_base 静默授权,snsapi_userinfo 需要用户手动同意
String scope = "snsapi_base";
// 重定向URI需要进行URL编码
String encodedRedirectUri = null;
try {
encodedRedirectUri = URLEncoder.encode(redirectUri, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
// 构建微信授权URL
String authUrl = "https://open.weixin.qq.com/connect/oauth2/authorize" +
"?appid=" + appId +
"&response_type=code" +
"&scope=" + scope +
"&redirect_uri=" + encodedRedirectUri +
"&state=123#wechat_redirect";
// 重定向到微信授权页面
return authUrl;
}
/**
* 微信回调地址,处理授权码并获取用户信息
*/
@GetMapping("/callback")
public ResponseEntity
@RequestParam(value = "state", required = false) String state) {
try {
// 通过code换取access_token和openid
Map
// 返回用户信息页面
return ResponseEntity.ok(ResultUtil.successJson(userInfo));
} catch (Exception e) {
return ResponseEntity.ok(ResultUtil.errorJson(null,"获取用户信息失败"));
}
}
/**
* 通过code换取access_token和用户信息
*/
private Map
Map
// 1. 通过code换取access_token
String tokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
"?appid=" + appId +
"&secret=" + secret +
"&code=" + code +
"&grant_type=authorization_code";
String tokenResponse = HttpUtil.get(tokenUrl);
JSONObject tokenJson = JSONObject.parseObject(tokenResponse);
// 检查是否有错误
if (tokenJson.containsKey("errcode")) {
throw new Exception("获取access_token失败: " + tokenJson.getString("errmsg"));
}
String accessToken = tokenJson.getString("access_token");
String openid = tokenJson.getString("openid");
// 2. 拉取用户信息
String userInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
"?access_token=" + accessToken +
"&openid=" + openid +
"&lang=zh_CN";
String userInfoResponse = HttpUtil.get(userInfoUrl);
JSONObject userInfoJson = JSONObject.parseObject(userInfoResponse);
// 检查是否有错误
if (userInfoJson.containsKey("errcode")) {
throw new Exception("获取用户信息失败: " + userInfoJson.getString("errmsg"));
}
// 将用户信息添加到结果中
result.put("openid", openid);
result.put("nickname", userInfoJson.getString("nickname"));
result.put("headimgurl", userInfoJson.getString("headimgurl"));
result.put("unionid", userInfoJson.getString("unionid"));
return result;
}
/**
* 刷新access_token
*/
@GetMapping("/refreshToken")
public String refreshToken(@RequestParam("refreshToken") String refreshToken, Model model) {
try {
String refreshUrl = "https://api.weixin.qq.com/sns/oauth2/refresh_token" +
"?appid=" + appId +
"&grant_type=refresh_token" +
"&refresh_token=" + refreshToken;
Map
if (tokenResponse == null || tokenResponse.containsKey("errcode")) {
model.addAttribute("error", "刷新access_token失败");
return "error";
}
model.addAttribute("tokenInfo", tokenResponse);
return "tokenInfo";
} catch (Exception e) {
model.addAttribute("error", "刷新token过程发生异常: " + e.getMessage());
return "error";
}
}
/**
* 检验授权凭证(access_token)是否有效
*/
@GetMapping("/checkToken")
public String checkToken(@RequestParam("accessToken") String accessToken,
@RequestParam("openid") String openid, Model model) {
try {
String checkUrl = "https://api.weixin.qq.com/sns/auth" +
"?access_token=" + accessToken +
"&openid=" + openid;
Map
if (checkResponse == null) {
model.addAttribute("error", "验证请求失败");
return "error";
}
Integer errcode = (Integer) checkResponse.get("errcode");
model.addAttribute("valid", errcode != null && errcode == 0);
model.addAttribute("checkInfo", checkResponse);
return "checkResult";
} catch (Exception e) {
model.addAttribute("error", "验证token过程发生异常: " + e.getMessage());
return "error";
}
}
}
坑点:
网页授权需填写域名,这个域名是纯域名不包含http://以及/wxAuth/callback。只填写域名。
但代码得回调uri redirectUri需填写完整得http://域名/前端页面地址
加密方式(回调接口)
/**
* 接收事件推送 (POST请求)
*/
@PostMapping("")
public ResponseEntity
@RequestParam("msg_signature") String msgSignature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestBody String xmlData) throws Exception {
// 1. 初始化加密工具类(替换为你的实际配置)
WXBizMsgCrypt wxBizMsgCrypt = new WXBizMsgCrypt(wxToken, encodingAesKey, appid);
// 2. 解密消息
String decryptedXml = wxBizMsgCrypt.decryptMsg(msgSignature, timestamp, nonce, xmlData);
// 3. 解析解密后的XML
Document document = DocumentHelper.parseText(decryptedXml);
// Document document = DocumentHelper.parseText(xmlData);
Element root = document.getRootElement();
// 4. 获取关键字段
String msgType = root.elementText("MsgType");
String event = root.elementText("Event");
String fromUser = root.elementText("FromUserName");
String toUser = root.elementText("ToUserName");
// 处理关注事件
if ("event".equals(msgType) && "subscribe".equals(event)) {
// return ResponseEntity.ok(buildWelcomeMessage(fromUser, toUser));
return ResponseEntity.ok(wxBizMsgCrypt.encryptMsg(buildWelcomeMessage(fromUser, toUser), timestamp, nonce));
}
// 处理取消关注事件
if ("event".equals(msgType) && "unsubscribe".equals(event)) {
wxUserService.deleteByOpenId(fromUser);
return ResponseEntity.ok("success");
}
return ResponseEntity.ok("success");
}
网页授权域名(官方提供得文件如何保存到根目录)
添加网页授权域名
仔细阅读弹窗的文案,得出的结论是:先下载那个 *.txt 文件,然后经过一定配置后,当访问 http://你的域名/*.txt 时,返回的结果是该文件里边的内容。 下载txt文件 首先,把 *.txt 文件下载到本地,如果打开该文件,可以看到只有一串字符串,如下: *.txt文件内容 配置nginx 因为服务器使用 nginx 做代理,所以直接添加几行配置,即可达到目的。
配置nginx 配置如下:
server { listen 80; server_name 域名;
location /txt文件名 { default_type text/html; return 200 'txt文件内容'; } } 上面配置的意思为:监听80端口,如果有请求进来,且域名为目标域名,然后请求的 uri 为 txt文件名,那么返回 txt 文件内容。