微信公众号消息接入

  • 一、公众号普通消息
  • 1、实现目标
  • 2、消息接入
  • 2.1、公众号服务器配置
  • 2.2、验证来自微信服务器消息
  • 2.3、消息接收
  • 3、配置内网穿透
  • 3.1、注册用户
  • 3.2、实名认证
  • 3.3、开通隧道
  • 3.4、启动隧道
  • 3.5 测试
  • 4、消息业务的实现
  • 4.1、service_vod模块创建接口
  • 4.2、创建模块定义接口
  • 4.3、service_wechat引入依赖
  • 4.4、service_wechat模块实现方法
  • 4.5、更改MessageController方法
  • 5、测试公众号消息
  • 二、公众号模板消息
  • 1、实现目标
  • 2、模板消息实现
  • 3、申请模板消息
  • 4、添加模板消息
  • 5、公众号测试号申请模板消息
  • 5.1、新增测试模板
  • 5.2、填写信息
  • 6、模板消息接口封装
  • 6.1、MessageController
  • 6.2、service接口
  • 6.3、service接口实现
  • 6.4 swagger测试

服务号只能是企业申请,这里我用测试号复现所有功能。(测试号不能做支付功能)

一、公众号普通消息

1、实现目标

  1、“硅谷课堂”公众号实现根据关键字搜索相关课程,如:输入“java”,可返回java相关的一个课程;

  2、“硅谷课堂”公众号点击菜单“关于我们”,返回关于我们的介绍

  3、关注或取消关注等

2、消息接入

参考文档:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html

  接入微信公众平台开发,开发者需要按照如下步骤完成:

  1、填写服务器配置

  2、验证服务器地址的有效性

  3、依据接口文档实现业务逻辑

2.1、公众号服务器配置

  在测试管理 -> 接口配置信息,点击“修改”按钮,填写服务器地址(URL)和Token,其中URL是开发者用来接收微信消息和事件的接口URL。Token可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性)

说明:本地测试,url改为内网穿透地址

wxjava公众号发消息 公众号 发信息_服务器

2.2、验证来自微信服务器消息

(1)概述

  开发者提交信息后,微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带参数如下表所示:

wxjava公众号发消息 公众号 发信息_微信_02

  开发者通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:

  1、将token、timestamp、nonce三个参数进行字典序排序

  2、将三个参数字符串拼接成一个字符串进行sha1加密

  3、开发者获得加密后的字符串可与signature对比,标识该请求来源于微信

(2)代码实现

  创建MessageController

@Slf4j
@RestController
@RequestMapping("/api/wechat/message")
public class MessageController {
    private static final String token = "ggkt";

    @Autowired
    private MessageService messageService;

    //订单支付成功模板消息测试
    @GetMapping("/pushPayMessage")
    public Result pushPayMessage() throws WxErrorException {
        messageService.pushPayMessage(1L);
        return Result.ok(null);
    }

    /**
     * 服务器有效性验证
     * @param request
     * @return
     */
    @GetMapping
    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;
    }

    /**
     * 接收微信服务器发送来的消息
     * @param request
     * @return
     * @throws Exception
     */
    @PostMapping
    public String receiveMessage(HttpServletRequest request) throws Exception {

        WxMpXmlMessage wxMpXmlMessage = WxMpXmlMessage.fromXml(request.getInputStream());
        System.out.println(JSONObject.toJSONString(wxMpXmlMessage));
        //Map<String, String> param = this.parseXml(request);
        //String message=messageService.receiveMessage(param);
        //return message;
        return "success";
    }

    private Map<String, String> parseXml(HttpServletRequest request) throws Exception {
        Map<String, String> map = new HashMap<String, String>();
        InputStream inputStream = request.getInputStream();
        SAXReader reader = new SAXReader();
        Document document = reader.read(inputStream);
        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;
    }

    private boolean checkSignature(String signature, String timestamp, String nonce) {
        String[] str = new String[]{token, timestamp, nonce};
        //排序
        Arrays.sort(str);
        //拼接字符串
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < str.length; i++) {
            buffer.append(str[i]);
        }
        //进行sha1加密
        String temp = SHA1.encode(buffer.toString());
        //与微信提供的signature进行匹对
        return signature.equals(temp);
    }
}

  完成之后,我们的校验接口就算是开发完成了。接下来就可以开发消息接收接口了。

2.3、消息接收

  https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html

  接下来我们来开发消息接收接口,消息接收接口和上面的服务器校验接口地址是一样的,都是我们一开始在公众号后台配置的地址。只不过消息接收接口是一个 POST 请求。

  在公众号后台配置的时候,消息加解密方式选择了明文模式,这样在后台收到的消息直接就可以处理了。微信服务器给我发来的普通文本消息格式如下:

<xml>
    <ToUserName><![CDATA[toUser]]></ToUserName>
    <FromUserName><![CDATA[fromUser]]></FromUserName>
    <CreateTime>1348831860</CreateTime>
    <MsgType><![CDATA[text]]></MsgType>
    <Content><![CDATA[this is a test]]></Content>
    <MsgId>1234567890123456</MsgId>
</xml>

wxjava公众号发消息 公众号 发信息_服务器_03

  看到这里,大家心里大概就有数了,当我们收到微信服务器发来的消息之后,我们就进行 XML 解析,提取出来我们需要的信息,去做相关的查询操作,再将查到的结果返回给微信服务器。

  这里我们先来个简单的,我们将收到的消息解析并打印出来

/**
     * 接收微信服务器发送来的消息
     * @param request
     * @return
     * @throws Exception
     */
    @PostMapping
    public String receiveMessage(HttpServletRequest request) throws Exception {

        WxMpXmlMessage wxMpXmlMessage = WxMpXmlMessage.fromXml(request.getInputStream());
        System.out.println(JSONObject.toJSONString(wxMpXmlMessage));
        return "success";
    }

    private Map<String, String> parseXml(HttpServletRequest request) throws Exception {
        Map<String, String> map = new HashMap<String, String>();
        InputStream inputStream = request.getInputStream();
        SAXReader reader = new SAXReader();
        Document document = reader.read(inputStream);
        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;
    }

3、配置内网穿透

3.1、注册用户

  网址:https://ngrok.cc/login/register

wxjava公众号发消息 公众号 发信息_公众号_04

3.2、实名认证

  (1)注册成功之后,登录系统,进行实名认证,认证费2元,认证通过后才能开通隧道

wxjava公众号发消息 公众号 发信息_微信_05

3.3、开通隧道

(1)选择隧道管理 -> 开通隧道

  最好一个是免费服务器,建议选择付费服务器,10元/月,因为免费服务器使用人数很多,经常掉线

wxjava公众号发消息 公众号 发信息_服务器_06

wxjava公众号发消息 公众号 发信息_微信_07

  上面这张图是视频中的配置,我的配置稍微有点差别,不过本地映射的端口都是一样的。

(3)开通成功后,查看开通的隧道

  这里开通了两个隧道,一个用于后端接口调用,一个用于公众号前端调用

wxjava公众号发消息 公众号 发信息_微信_08

3.4、启动隧道

(1)下载客户端工具

wxjava公众号发消息 公众号 发信息_公众号_09

wxjava公众号发消息 公众号 发信息_微信_10

(3)解压,找到bat文件,双击启动

wxjava公众号发消息 公众号 发信息_微信_11

(4)输入隧道id,多个使用逗号隔开,最后回车就可以启动

wxjava公众号发消息 公众号 发信息_公众号_12

这张图是视频中的id,已经过期了,用你自己的id。

wxjava公众号发消息 公众号 发信息_公众号_13

  注意,测试号中的配置可以在这个时候添加。如下图

wxjava公众号发消息 公众号 发信息_微信_14

   先添加下面的JS接口安全域名,再配置上面的接口。

3.5 测试

  启动服务后,在公众号发送文本消息

wxjava公众号发消息 公众号 发信息_微信_15

  消息就会显示在后台日志中:

wxjava公众号发消息 公众号 发信息_服务器_16

4、消息业务的实现

4.1、service_vod模块创建接口

(1)创建CourseApiController方法,根据课程关键字查询课程信息

wxjava公众号发消息 公众号 发信息_公众号_17

@ApiOperation("根据关键字查询课程")
    @GetMapping("inner/findByKeyword/{keyword}")
    public List<Course> findByKeyword(
            @ApiParam(value = "关键字", required = true)
            @PathVariable String keyword){
        QueryWrapper<Course> queryWrapper = new QueryWrapper();
        queryWrapper.like("title", keyword);
        List<Course> list = courseService.list(queryWrapper);
        return list;
    }

4.2、创建模块定义接口

  这里是负责service_wechat远程调用service_vod

(1)service_client下创建子模块service_course_client

wxjava公众号发消息 公众号 发信息_服务器_18

(2)定义根据关键字查询课程接口

@FeignClient(value = "service-vod")
public interface CourseFeignClient {

    @ApiOperation("根据关键字查询课程")
    @GetMapping("/api/vod/course/inner/findByKeyword/{keyword}")
    List<Course> findByKeyword(@PathVariable String keyword);
}

4.3、service_wechat引入依赖

<dependency>
    <groupId>com.atguigu</groupId>
    <artifactId>service_course_client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

4.4、service_wechat模块实现方法

(1)MessageService

public interface MessageService {
//    接收微信服务器发送来的消息
    String receiveMessage(Map<String, String> param);

    //订单支付成功
    void pushPayMessage(long id);
}

(2)MessageServiceImpl

@Service
public class MessageServiceImpl implements MessageService {

    @Autowired
    private CourseFeignClient courseFeignClient;

    @Autowired
    private WxMpService wxMpService;


//    接收微信服务器发送来的消息
    @Override
    public String receiveMessage(Map<String, String> param) {
        String content="";
        String msgType = param.get("MsgType");
        //判断什么类型消息
        switch (msgType){
            case "text":    //普通文本类型,输入关键字java、mysql等
                content=this.search(param);
                break;
            case "event":   //关注、取消关注、点击关于我们
                String event = param.get("Event");
                String eventKey = param.get("EventKey");
                //关注
                if("subscribe".equals(event)){  //关注
                    content=this.subscribe(param);
                }else if("unsubscribe".equals(event)) { //取消关注
                    content=this.unsubscribe(param);
                }else if("CLICK".equals(event) && "aboutUs".equals(eventKey)){  //关于我们
                    content=this.aboutUs(param);
                }else{
                    content="success";
                }
                break;
            default:    //其他情况
                content = "success";
        }
        return content;
    }

    @Override
    public void pushPayMessage(long id) {
        //微信openid
        String openid = "你的openid";
        WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
                .toUser(openid)//要推送的用户openid
                .templateId("8iVYZuYXGfaK2Irvdrj_sgHtOOA49QBoXRr6_97U448")//模板id
                .url("http://ggkt2.vipgz1.91tunnel.com/#/pay/"+id)//点击模板消息要访问的网址
                .build();
        //3,如果是正式版发送消息,,这里需要配置你的信息
        templateMessage.addData(new WxMpTemplateData("first", "亲爱的用户:您有一笔订单支付成功。", "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword1", "1314520", "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword2", "java基础课程", "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword3", "2022-01-11", "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword4", "100", "#272727"));
        templateMessage.addData(new WxMpTemplateData("remark", "感谢你购买课程,如有疑问,随时咨询!", "#272727"));
        String msg = null;
        try {
            msg = wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
        } catch (WxErrorException e) {
            e.printStackTrace();
        }
        System.out.println(msg);
    }

    //关于我们
    private String aboutUs(Map<String, String> param) {
        return this.text(param, "硅谷课堂现开设Java、HTML5前端+全栈、大数据、" +
                "全链路UI/UE设计、人工智能、大数据运维+Python自动化、Android+HTML5混合开" +
                "发等多门课程;同时,通过视频分享、谷粒学苑在线课堂、大厂学苑直播课堂等多种" +
                "方式,满足了全国编程爱好者对多样化学习场景的需求,已经为行业输送了大量" +
                "IT技术人才。").toString();
    }

    //取消关注
    private String unsubscribe(Map<String, String> param) {
        return "success";
    }

    //关注
    private String subscribe(Map<String, String> param) {
        return this.text(param,"感谢你关注“硅谷课堂”,可以根据关键字" +
                "搜索您想看的视频教程,如:JAVA基础、Spring boot、大数据等")
                .toString();
    }


    /**
     * 处理关键字搜索事件
     * 图文消息个数;当用户发送文本、图片、语音、视频、图文、地理位置这六种消息时,开发者只能回复1条图文消息;其余场景最多可回复8条图文消息
     * @param param
     * @return
     */
    private String search(Map<String, String> param) {
        String fromusername = param.get("FromUserName");
        String tousername = param.get("ToUserName");
        String content = param.get("Content");
        //单位为秒,不是毫秒
        Long createTime = new Date().getTime() / 1000;
        StringBuffer text = new StringBuffer();
        List<Course> courseList = courseFeignClient.findByKeyword(content);
        if(CollectionUtils.isEmpty(courseList)) {
            text = this.text(param, "请重新输入关键字,没有匹配到相关视频课程");
        } else {
            //一次只能返回一个
            Random random = new Random();
            int num = random.nextInt(courseList.size());
            Course course = courseList.get(num);
            StringBuffer articles = new StringBuffer();
            articles.append("<item>");
            articles.append("<Title><![CDATA["+course.getTitle()+"]]></Title>");
            articles.append("<Description><![CDATA["+course.getTitle()+"]]></Description>");
            articles.append("<PicUrl><![CDATA["+course.getCover()+"]]></PicUrl>");
            articles.append("<Url><![CDATA[http://glkt.atguigu.cn/#/liveInfo/"+course.getId()+"]]></Url>");
            articles.append("</item>");

            text.append("<xml>");
            text.append("<ToUserName><![CDATA["+fromusername+"]]></ToUserName>");
            text.append("<FromUserName><![CDATA["+tousername+"]]></FromUserName>");
            text.append("<CreateTime><![CDATA["+createTime+"]]></CreateTime>");
            text.append("<MsgType><![CDATA[news]]></MsgType>");
            text.append("<ArticleCount><![CDATA[1]]></ArticleCount>");
            text.append("<Articles>");
            text.append(articles);
            text.append("</Articles>");
            text.append("</xml>");
        }
        return text.toString();
    }

    /**
     * 回复文本
     * @param param
     * @param content
     * @return
     */
    private StringBuffer text(Map<String, String> param, String content) {
        String fromusername = param.get("FromUserName");
        String tousername = param.get("ToUserName");
        //单位为秒,不是毫秒
        Long createTime = new Date().getTime() / 1000;
        StringBuffer text = new StringBuffer();
        text.append("<xml>");
        text.append("<ToUserName><![CDATA["+fromusername+"]]></ToUserName>");
        text.append("<FromUserName><![CDATA["+tousername+"]]></FromUserName>");
        text.append("<CreateTime><![CDATA["+createTime+"]]></CreateTime>");
        text.append("<MsgType><![CDATA[text]]></MsgType>");
        text.append("<Content><![CDATA["+content+"]]></Content>");
        text.append("</xml>");
        return text;
    }
}

  上面的openid如下图:

wxjava公众号发消息 公众号 发信息_wxjava公众号发消息_19

4.5、更改MessageController方法

@PostMapping
    public String receiveMessage(HttpServletRequest request) throws Exception {

//        WxMpXmlMessage wxMpXmlMessage = WxMpXmlMessage.fromXml(request.getInputStream());
//        System.out.println(JSONObject.toJSONString(wxMpXmlMessage));
        Map<String, String> param = this.parseXml(request);
        String message=messageService.receiveMessage(param);
        return message;
//        return "success";
    }

5、测试公众号消息

(1)点击个人 -> 关于我们,返回关于我们的介绍

wxjava公众号发消息 公众号 发信息_公众号_20

wxjava公众号发消息 公众号 发信息_公众号_21

(2)在公众号输入关键字,返回搜索的课程信息

wxjava公众号发消息 公众号 发信息_微信_22

二、公众号模板消息

1、实现目标

  购买课程支付成功微信推送消息

2、模板消息实现

  接口文档:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html

3、申请模板消息

  首先我们需要知道,模板消息是需要申请的。

  但是我们在申请时还是有一些东西要注意,这个在官方的文档有非常详细的说明。

  https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Operation_Specifications.html

wxjava公众号发消息 公众号 发信息_wxjava公众号发消息_23

  这个大家好好看看。选择行业的时候可要谨慎些,因为这个一个月只可以修改一次。

  下面看看在哪里申请,硅谷课堂已经申请过,忽略

wxjava公众号发消息 公众号 发信息_wxjava公众号发消息_24

wxjava公众号发消息 公众号 发信息_wxjava公众号发消息_25

4、添加模板消息

  审核通过之后,我们就可以添加模板消息,进行开发了。

  我们点击模板消息进入后,直接在模板库中选择你需要的消息模板添加就可以了,添加之后就会在我的模板中。会有一个模板id,这个模板id在我们发送消息的时候会用到。

  模板消息如下:

wxjava公众号发消息 公众号 发信息_wxjava公众号发消息_26

  我们需要模板消息:

   1、订单支付成功通知;

  模板库中没有的模板,可以自定义模板,审核通过后可以使用。

  测试号看上面这个没用,测试号看下面的部分

5、公众号测试号申请模板消息

5.1、新增测试模板

wxjava公众号发消息 公众号 发信息_服务器_27

5.2、填写信息

(1)下载示例参考

  下载地址:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Operation_Specifications.html

wxjava公众号发消息 公众号 发信息_服务器_28

(2)填写模板标题和模板内容

wxjava公众号发消息 公众号 发信息_微信_29

wxjava公众号发消息 公众号 发信息_服务器_30

6、模板消息接口封装

6.1、MessageController

  添加方法

@GetMapping("/pushPayMessage")
public Result pushPayMessage() throws WxErrorException {
    messageService.pushPayMessage(1L);
    return Result.ok();
}

6.2、service接口

  MessageService

void pushPayMessage(Long orderId);

6.3、service接口实现

  (1)MessageServiceImpl类

  (2)openid值

wxjava公众号发消息 公众号 发信息_wxjava公众号发消息_31

wxjava公众号发消息 公众号 发信息_服务器_30

  id值用你自己的,切记,老师提供的那个由于他当时申请的隧道现在已经过期了,所以用不了。

@Autowired
    private WxMpService wxMpService;
 	@Override
    public void pushPayMessage(long id) {
        //微信openid
        String openid = "用你的";
        WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
                .toUser(openid)//要推送的用户openid
                .templateId("8iVYZuYXGfaK2Irvdrj_sgHtOOA49QBoXRr6_97U448")//模板id
                .url("http://ggkt2.vipgz1.91tunnel.com/#/pay/"+id)//点击模板消息要访问的网址(这个隧道早已经过期了,用你的)
                .build();
        //3,如果是正式版发送消息,,这里需要配置你的信息
        templateMessage.addData(new WxMpTemplateData("first", "亲爱的用户:您有一笔订单支付成功。", "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword1", "1314520", "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword2", "java基础课程", "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword3", "2022-01-11", "#272727"));
        templateMessage.addData(new WxMpTemplateData("keyword4", "100", "#272727"));
        templateMessage.addData(new WxMpTemplateData("remark", "感谢你购买课程,如有疑问,随时咨询!", "#272727"));
        String msg = null;
        try {
            msg = wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
        } catch (WxErrorException e) {
            e.printStackTrace();
        }
        System.out.println(msg);
    }

6.4 swagger测试

wxjava公众号发消息 公众号 发信息_服务器_33

wxjava公众号发消息 公众号 发信息_微信_34

  可以看到,点击成功之后,公众号测试号中也给我们推送了模板消息。

  接口实现大多数都是微信官方给的示例,个人开发者没有公众号,只能在测试号中完成这些功能,为了方便直接使用内网穿透实现调用,但是测试号是不能实现微信支付的。