公众号实现指定用户消息长期推送"/>
spring boot +微信小程序项目,通过微信公众号实现指定用户消息长期推送
流程
用户登录小程序,后台记录用户的小程序openId和用户唯一的UnionId。然后用户触发公众号事件(关注公众号或者发送指定消息),后台获取到用户公众号的openId,再调用接口通过公众号的openId查询用户的UnionId,再和数据库里的UnionId进行匹配,将用户的公众号openId存入数据库。此后即可通过userId找到公众号openId 实现公众号消息推送。
1.开通公众号
这个直接操作就好了
2.配置公众号验签接口
添加验签接口
/*** 服务器有效性验证*/@GetMapping("connect")public String verifyToken(HttpServletRequest request) {String signature = request.getParameter("signature");String timestamp = request.getParameter("timestamp");String nonce = request.getParameter("nonce");String echostr = request.getParameter("echostr");log.info("signature: {} nonce: {} echostr: {} timestamp: {}", signature, nonce, echostr, timestamp);if (this.checkSignature(signature, timestamp, nonce)) {log.info("token ok");return echostr;}return echostr;}
将接口配置到公众号上
3.设置用户关注、取关以及发送消息事件处理
接口名称和验签接口相同 只是验签是GET请求 事件处理是POST请求
/*** 用户公众号关注/取关事件回调*/@PostMapping(value = "/connect")public void getXmlInfo(HttpServletRequest req, HttpServletResponse resp) throws Exception {req.setCharacterEncoding("UTF-8"); // 接收请求时的编码。resp.setCharacterEncoding("UTF-8"); // 响应给浏览器的编码。Map<String, String> map = XMLUtil.parseXml(req);log.info(JSONObject.toJSONString(map));if ( map.get("MsgType").equals("text")){if (map.get("Content").equals("求购留言")){String result = wechatOfficialAccountService.replySubscribeInfo(map);resp.setCharacterEncoding("UTF-8");resp.getWriter().println(result);}}else {if (map.get("Event").equals("subscribe")){wechatOfficialAccountService.subscribeInfo(map);} else if (map.containsKey("Event") && map.get("Event").equals("unsubscribe")) {wechatOfficialAccountService.unSubscribeInfo(map);}}}
关注时给用户绑定openId
public void subscribeInfo(Map<String, String> map){WeChatEventInfo eventInfo = new WeChatEventInfo().setToUserName(map.get("ToUserName")).setFromUserName(map.get("FromUserName")).setCreateTime(map.get("CreateTime")).setMsgType(map.get("MsgType")).setEvent(map.get("Event"));String accessToken = getToken();log.info("accessToken:{}",accessToken);String unionId = getUserInfo(accessToken,eventInfo.getFromUserName());if(StrUtil.isNotBlank(unionId)){wxCustomerService.bindOfficialAccountOpenId(unionId,eventInfo.getFromUserName());}}
回复指定消息时给用户绑定openId
/*** 回复求购留言进行关注*/public String replySubscribeInfo(Map<String, String> map) throws Exception {WeChatEventInfo eventInfo = new WeChatEventInfo().setToUserName(map.get("ToUserName")).setFromUserName(map.get("FromUserName")).setCreateTime(map.get("CreateTime")).setMsgType(map.get("MsgType")).setContent(map.get("content"));String accessToken = getToken();log.info("accessToken:{}",accessToken);String unionId = getUserInfo(accessToken,eventInfo.getFromUserName());ReplyMessage result = new ReplyMessage().setToUserName(eventInfo.getFromUserName()).setFromUserName(eventInfo.getToUserName()).setCreateTime(System.currentTimeMillis()).setMsgType("text");if(StrUtil.isNotBlank(unionId)){wxCustomerService.bindOfficialAccountOpenId(unionId,eventInfo.getFromUserName());result.setContent("已为您开通求购留言通知");}else {result.setContent("开通失败,暂未获取到您的用户信息");}return XMLUtil.textMessageToXml(result);}
取关时解绑用户openId
public void unSubscribeInfo(Map<String, String> map){WeChatEventInfo eventInfo = new WeChatEventInfo().setToUserName(map.get("ToUserName")).setFromUserName(map.get("FromUserName")).setCreateTime(map.get("CreateTime")).setMsgType(map.get("MsgType")).setEvent(map.get("Event"));wxCustomerService.unSubscribeInfo(eventInfo.getFromUserName());}
4.申请消息模板
5.给指定用户发送消息
前三步代码
依赖
<dependency><groupId>com.github.binarywang</groupId><artifactId>weixin-java-mp</artifactId><version>4.0.0</version></dependency><dependency><groupId>dom4j</groupId><artifactId>dom4j</artifactId><version>1.1</version></dependency><!-- java对象转换为xml字符串 --><dependency><groupId>com.thoughtworks.xstream</groupId><artifactId>xstream</artifactId><version>1.4.19</version></dependency>
接口层
接口需要放行
@RestController
@RequestMapping("wechat")
@Slf4j
public class WeChatController {private final WechatOfficialAccountService wechatOfficialAccountService;private static final String TOKEN = "platformYz";public WeChatController(WechatOfficialAccountService wechatOfficialAccountService) {this.wechatOfficialAccountService = wechatOfficialAccountService;}/*** 服务器有效性验证*/@GetMapping("connect")public String verifyToken(HttpServletRequest request) {String signature = request.getParameter("signature");String timestamp = request.getParameter("timestamp");String nonce = request.getParameter("nonce");String echostr = request.getParameter("echostr");log.info("signature: {} nonce: {} echostr: {} timestamp: {}", signature, nonce, echostr, timestamp);if (this.checkSignature(signature, timestamp, nonce)) {log.info("token ok");return echostr;}return echostr;}/*** 用户公众号关注/取关事件回调*/@PostMapping(value = "/connect")public void getXmlInfo(HttpServletRequest req, HttpServletResponse resp) throws Exception {req.setCharacterEncoding("UTF-8"); // 接收请求时的编码。resp.setCharacterEncoding("UTF-8"); // 响应给浏览器的编码。Map<String, String> map = XMLUtil.parseXml(req);log.info(JSONObject.toJSONString(map));if ( map.get("MsgType").equals("text")){if (map.get("Content").equals("求购留言")){String result = wechatOfficialAccountService.replySubscribeInfo(map);resp.setCharacterEncoding("UTF-8");resp.getWriter().println(result);}}else {if (map.get("Event").equals("subscribe")){wechatOfficialAccountService.subscribeInfo(map);} else if (map.containsKey("Event") && map.get("Event").equals("unsubscribe")) {wechatOfficialAccountService.unSubscribeInfo(map);}}}private boolean checkSignature(String signature, String timestamp, String nonce) {String[] str = new String[]{TOKEN, timestamp, nonce};//排序Arrays.sort(str);//拼接字符串StringBuffer buffer = new StringBuffer();for (String s : str) {buffer.append(s);}//进行sha1加密String temp = SHA1.encode(buffer.toString());//与微信提供的signature进行匹对return signature.equals(temp);}
}
service层
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Optional;@Service
@Slf4j
public class WechatOfficialAccountService {private final WxCustomerService wxCustomerService;private final RestTemplate restTemplate;private final String appId;private final String appSecret;private static final String USER_INFO_URL = "=%s&openid=%s&lang=zh_CN";private static final String TOKEN_URL = "";public WechatOfficialAccountService(WxCustomerService wxCustomerService, RestTemplate restTemplate,@Value("${wx.gzh.appId}") String appId,@Value("${wx.gzh.secret}") String appSecret) {this.wxCustomerService = wxCustomerService;this.restTemplate = restTemplate;this.appId = appId;this.appSecret = appSecret;}public void subscribeInfo(Map<String, String> map){WeChatEventInfo eventInfo = new WeChatEventInfo().setToUserName(map.get("ToUserName")).setFromUserName(map.get("FromUserName")).setCreateTime(map.get("CreateTime")).setMsgType(map.get("MsgType")).setEvent(map.get("Event"));String accessToken = getToken();log.info("accessToken:{}",accessToken);String unionId = getUserInfo(accessToken,eventInfo.getFromUserName());if(StrUtil.isNotBlank(unionId)){wxCustomerService.bindOfficialAccountOpenId(unionId,eventInfo.getFromUserName());}}/*** 回复求购留言进行关注*/public String replySubscribeInfo(Map<String, String> map) throws Exception {WeChatEventInfo eventInfo = new WeChatEventInfo().setToUserName(map.get("ToUserName")).setFromUserName(map.get("FromUserName")).setCreateTime(map.get("CreateTime")).setMsgType(map.get("MsgType")).setContent(map.get("content"));String accessToken = getToken();log.info("accessToken:{}",accessToken);String unionId = getUserInfo(accessToken,eventInfo.getFromUserName());ReplyMessage result = new ReplyMessage().setToUserName(eventInfo.getFromUserName()).setFromUserName(eventInfo.getToUserName()).setCreateTime(System.currentTimeMillis()).setMsgType("text");if(StrUtil.isNotBlank(unionId)){wxCustomerService.bindOfficialAccountOpenId(unionId,eventInfo.getFromUserName());result.setContent("已为您开通求购留言通知");}else {result.setContent("开通失败,暂未获取到您在玉农智链中的用户信息");}return XMLUtil.textMessageToXml(result);}private String getUserInfo(String accessToken,String openId) {String url = String.format(USER_INFO_URL, accessToken, openId);String json = restTemplate.getForObject(url, String.class);JSONObject jsonObject = JSONObject.parseObject(json);log.info("json:{}",json);return jsonObject.getString("unionid");}private String getToken() {ResponseEntity<JSONObject> responseEntity = restTemplate.postForEntity(TOKEN_URL,new JSONObject().fluentPut("grant_type","client_credential").fluentPut("appid",appId).fluentPut("secret",appSecret).toJSONString(), JSONObject.class);JSONObject json = Optional.ofNullable(responseEntity.getBody()).orElse(new JSONObject());restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));log.info("access_token_json:{}",json.toJSONString());return json.getString("access_token");}public void unSubscribeInfo(Map<String, String> map){WeChatEventInfo eventInfo = new WeChatEventInfo().setToUserName(map.get("ToUserName")).setFromUserName(map.get("FromUserName")).setCreateTime(map.get("CreateTime")).setMsgType(map.get("MsgType")).setEvent(map.get("Event"));wxCustomerService.unSubscribeInfo(eventInfo.getFromUserName());}
}
工具类
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.core.util.QuickWriter;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
import com.thoughtworks.xstream.io.xml.XppDriver;
import lombok.extern.slf4j.Slf4j;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
public class XMLUtil {private static final String MESSAGE_TYPE_TEXT = "text";public static Map<String, String> parseXml(HttpServletRequest request) {Map<String, String> map = new HashMap<>();try(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());}} catch (IOException |DocumentException e) {log.info("xml信息解析失败");}return map;}/*** 文本消息对象转换成xml** @param textMessage 文本消息对象* @return xml*/public static String textMessageToXml(ReplyMessage textMessage) {XSTREAM.alias("xml", textMessage.getClass());return XSTREAM.toXML(textMessage);}/*** 扩展xstream,使其支持CDATA块*/private static final XStream XSTREAM = new XStream(new XppDriver() {@Overridepublic HierarchicalStreamWriter createWriter(Writer out) {return new PrettyPrintWriter(out) {// 对所有xml节点的转换都增加CDATA标记final boolean cdata = true;@Overrideprotected void writeText(QuickWriter writer, String text) {if (cdata) {writer.write("<![CDATA[");writer.write(text);writer.write("]]>");} else {writer.write(text);}}};}});/*** 获取默认文本消息** @param receiver 接收人* @param officialWxid 官方微信id* @return 文本消息*/public static ReplyMessage getDefaultReplyMessage(String receiver, String officialWxid) {ReplyMessage textMessage = new ReplyMessage();textMessage.setToUserName(receiver);textMessage.setFromUserName(officialWxid);textMessage.setCreateTime(System.currentTimeMillis());textMessage.setMsgType(MESSAGE_TYPE_TEXT);return textMessage;}}
模板消息发送服务类
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Optional;@Component
@Slf4j
public class WechatOfficialAccountMessageServer {private final String appId;private final String miniAppId;private final String appSecret;private final RestTemplate restTemplate;private String accessToken;private static final String SEND_URL = "=%s";private static final String TOKEN_URL = "";private WechatOfficialAccountMessageServer(@Value("${wx.gzh.appId}") String appId,@Value("${wx.xcx.appId}") String miniAppId,@Value("${wx.gzh.secret}")String appSecret) {this.appId = appId;this.appSecret = appSecret;this.miniAppId = miniAppId;this.restTemplate = new RestTemplate();}/*** 消息推送** @param templateId 消息模板id* @param openId 用户openId* @param param 推送对象*/public void pushMessage(String templateId, String openId, WechatOfficialAccountMessageParam param) {getInstance();param.getMessageDataList().forEach(item->{pushMessage(templateId,openId,item,param.getPage());});}private void getInstance(){//获取access_tokenResponseEntity<JSONObject> responseEntity = restTemplate.postForEntity(TOKEN_URL,new JSONObject().fluentPut("grant_type","client_credential").fluentPut("appid",appId).fluentPut("secret",appSecret).toJSONString(), JSONObject.class);JSONObject json = Optional.ofNullable(responseEntity.getBody()).orElse(new JSONObject());restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));this.accessToken = json.getString("access_token");}/*** 消息推送** @param templateId 消息模板id* @param openId 用户openId* @param messageData 消息体* @param page*/private void pushMessage(String templateId, String openId, JSONObject messageData, String page) {String url = String.format(SEND_URL, this.accessToken);//拼接推送的模版SendTemplateRequest request = new SendTemplateRequest().setTouser(openId).setTemplateId(templateId).setData(messageData);if (StrUtil.isNotBlank(page)){JSONObject miniProgram = new JSONObject().fluentPut("appid", miniAppId).fluentPut("pagepath", page);request.setMiniProgram(miniProgram);}ResponseEntity<SendTemplateResponse> responseEntity = restTemplate.postForEntity(url, JSONObject.toJSONString(request), SendTemplateResponse.class);SendTemplateResponse response = Optional.ofNullable(responseEntity.getBody()).orElse(SendTemplateResponse.getInstance());if (Objects.requireNonNull(response).isSuccess()) {log.info("公众号推送成功");return;}log.error("公众号推送失败:{} {}", response.getErrcode(), response.getErrmsg());throw new DefaultException("公众号推送失败:" + response.getErrcode() + response.getErrmsg(), ResponseEnum.OPERATE_FAIL);}
}
相关实体类
import lombok.Data;
import lombok.experimental.Accessors;/*** 微信公众号关注取消事件信息*/
@Data
@Accessors(chain = true)
public class WeChatEventInfo {private String toUserName;private String fromUserName;private String createTime;private String msgType;private String event;private String content;private String msgId;
}
@Data
@Accessors(chain = true)
public class ReplyMessage {private String ToUserName;private String FromUserName;private Long CreateTime;private String MsgType;private String Content;
}
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import lombok.experimental.Accessors;@Data
@Accessors(chain = true)
class SendTemplateRequest {/*** 接收者(用户)的 openid*/private String touser;/*** 所需下发的模板消息的id*/@JSONField(name = "template_id")private String templateId;/*** 模板跳转链接(海外账号没有跳转能力)*/@JSONField(name = "url")private String url;/*** 跳小程序所需数据,不需跳小程序可不用传该数据* "miniprogram":{* "appid":"xiaochengxuappid12345",* "pagepath":"index?foo=bar"* }* appid 必填 所需跳转到的小程序appid(该小程序appid必须与发模板消息的公众号是绑定关联关系,暂不支持小游戏)* pagepath 非必填 所需跳转到小程序的具体页面路径,支持带参数,(示例index?foo=bar),要求该小程序已发布,暂不支持小游戏*/@JSONField(name = "miniprogram")private Object miniProgram;/*** 模板数据*/private Object data;/*** 防重入id。对于同一个openid + client_msg_id, 只发送一条消息,10分钟有效,超过10分钟不保证效果。若无防重入需求,可不填*/@JSONField(name = "client_msg_id")private String clientMsgId;
}
import lombok.Data;
import lombok.experimental.Accessors;/*** 消息推送响应对象** @author fenglifei*/
@Data
@Accessors(chain = true)
public class SendTemplateResponse {/*** 错误码* 0 ok* 43116 该模板因滥用被滥用过多,已被限制下发*/private long errcode;/*** 错误信息*/private String errmsg;/*** 错误信息*/private String msgid;public static SendTemplateResponse getInstance(){return new SendTemplateResponse().setErrcode(404L).setErrmsg("");}public boolean isSuccess() {return this.errcode == 0;}
}
import com.alibaba.fastjson.JSONObject;
import lombok.Data;import java.util.List;@Data
public class WechatOfficialAccountMessageParam {private String page;private List<JSONObject> messageDataList;
}
更多推荐
spring boot +微信小程序项目,通过微信公众号实现指定用户消息长期推送
发布评论