spring boot +微信小程序项目,通过微信公众号实现指定用户消息长期推送

本文介绍了如何在微信小程序中实现用户登录后的公众号绑定,包括获取UnionId,处理关注、取关及消息发送事件,配置验签接口,以及使用模板消息发送功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

流程

用户登录小程序,后台记录用户的小程序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 = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN";
    private static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/stable_token";
    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() {
        @Override
        public HierarchicalStreamWriter createWriter(Writer out) {
            return new PrettyPrintWriter(out) {
                // 对所有xml节点的转换都增加CDATA标记
                final boolean cdata = true;

                @Override
                protected 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 = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=%s";
    private static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/stable_token";

    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_token
        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));
        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;
}

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值