微信公众号开发-Java版学习笔记

写在前面

  • 为什么学习微信公众号开发

    在我使用Java写了一个简单的web网站----任务墙,每天需要进行任务打卡,就要进入网站系统后台。在PC端操作还行,但是在移动端操作时,由于系统后台页面没有做响应式,所以非常不方便。主要是想更加方便、简单、高效进行任务打卡,起初是想写个微信机器人,但是需要登录一个微信于是无意间想到了微信公众平台开发。说干就干,就找了教程开始学习。任务墙地址:任务墙 (task.imyjs.cn)

  • 学习过程与感受

    在整个学习过程是根据罗召勇老师录制的教程学习的,本计划学习周期是两天,但是由于刚回到家,每天都有一些事情要去做,没有完整的学习时间,就断断续续的持续了四天,微信公众号开发整体不难,主要是熟悉微信公众号常用的一些接口文档,然后会一门后端语言(比如java)即可。整体学习下来,其实就是调用各种接口,难度不大,但是值得学习的地方是在整个过程中,用到了许多面向对象的开发思想,我感觉这是学下来收获最大之一。

  • 关于本篇文章

    由于时间仓促,加上整体内容难度不大,于是没有自己独立去整理学习笔记,在教程评论下发现了一位小伙伴整理的笔记,整体思路清晰,所以本篇文章是基于他的文章修改整理的。文章原文地址: https://heliufang.gitee.io/2021/08/14/1426556608602836992/images/坚决支持作者原创!侵删!

  • 学习参考内容

    罗召勇老师教程:微信公众号开发-Java版(蓝桥罗召勇)

    微信公众号文档:微信公众号官方文档(核心参考文档)

    Java版学习笔记:微信公众号开发

    本文源码已上传至我的码云:yjs0612 (yjs0612) - Gitee.com

  • 我的站点
  • 联系我

    🐧:2410685763

    WeChat:Super_Baby_00

    公众号:编程那点事儿

1 微信公众号介绍

账号分为服务号订阅号、小程序

服务号和订阅号开发类似,但是申请服务号必须是企业,所以学习的话申请一个订阅号+测试账号即可。为啥要申请测试账号呢?因为订阅号的接口功能有限,为了学习开发以及熟悉更多的接口,所以还需要申请一个测试号

2 注册订阅号

第一步:访问:https://mp.weixin.qq.com/ 点击立即注册按钮

第二步:注册类型页面选择订阅号

第三步:填写相关信息,点击注册即可

3 注册测试号

因为订阅号的接口权限是有限的,为了熟悉更多的微信公众号接口,所以需要申请一个测试号。

第一步:用注册的订阅号登录

第二步:在目录中【设置与开发】—>【开发者工具】下选择公众平台测试账号,点击进入后申请即可。

或者直接输入网址进入

微信公众平台 (qq.com)

申请成功之后,就可以配置相关信息进行开发了,具体怎么配置后面再解释

4 程序运行流程

  • 用户在公众号发送请求到微信服务器
  • 微信服务器将请求转发到我们自己的服务器
  • 我们自己的服务器处理完之后再把结果发送到微信服务器
  • 最后微信服务器再把结果响应给客户

image-20210808114624893

5 搭建开发环境

罗老师用的是eclipse并且没有用maven环境,我用的是eclipse+maven+jdk8+tomcat8.5。maven的话可以兼容idea,而且下载依赖方便。

5.1建项目导依赖

新建一个名为weixin的maven项目(这个项目名字任意都行),pom.xml的依赖如下:

<dependencies>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <!--
        此处为什么需要添加该标签?
        provided指的是在编译和测试过程中有效,最后生成的war包时不会加入
         因为Tomcat的lib目录中已经有servlet-api这个jar包,如果在生成war包的时		候生效就会和Tomcat中的jar包冲突,导致报错
      -->
      <scope>provided</scope>
    </dependency>
    <!-- xml操作相关依赖 -->
    <dependency>
      <groupId>com.thoughtworks.xstream</groupId>
      <artifactId>xstream</artifactId>
      <version>1.4.10</version>
    </dependency>
    <dependency>
      <groupId>org.dom4j</groupId>
      <artifactId>dom4j</artifactId>
      <version>2.0.0</version>
    </dependency>
    <!-- 阿里json解析 -->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.47</version>
    </dependency>
    <!-- 这个是编码解码的 -->
    <dependency>
      <groupId>commons-codec</groupId>
      <artifactId>commons-codec</artifactId>
      <version>1.6</version>
    </dependency>
    <!-- 这个是测试的 -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
    <!--百度AI 这里用于图片识别-->
    <dependency>
      <groupId>com.baidu.aip</groupId>
      <artifactId>java-sdk</artifactId>
      <version>4.16.0</version>
    </dependency>
  </dependencies>


<!-- 阿里云小蜜-自动回复机器人  这里我没有实现
		<dependency>
			<groupId>com.aliyun</groupId>
			<artifactId>aliyun-java-sdk-chatbot</artifactId>
			<version>1.0.0</version>
		</dependency>
		<dependency>
			<groupId>com.aliyun</groupId>
			<artifactId>aliyun-java-sdk-core</artifactId>
			<version>4.5.2</version>
		</dependency>
-->

5.2编写测试的servlet

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/test") 
public class TestServlet extends HttpServlet{

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		System.out.println("请求到达了");
		resp.getWriter().write("hello weixin");
	}

	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		
	}

}

启动项目访问:http://localhost:8080/weixin/test

6 内外网穿透

外网默认是访问不到自己电脑上的项目的,为了让外网能够访问,所以需要做内外网穿透.这个不需要自己实现,可以借助一些工具,如花生壳、ngrok.这里用的是ngrok

ngrok文档

第一步:访问ngrok官网,注册ngrok账号。

第二步:使用注册的账号登录

第三步:【隧道管理—>开通隧道】立即购买,可以购买最后那个免费的,也可以花10块钱买一个。免费的有时候不稳定,可以买一个10块。

开通之后在隧道管理下就可以看到刚刚开通的隧道

第四步:下载客户端工具,我电脑是windows的所以下载windows版

各版本工具下载地址:https://www.ngrok.cc/download.html

第五步:启动ngrok客户端工具,运行bat,输入隧道id,回车

看到这个状态为【online】表示启动成功

7 开发接入

接入之后微信服务器和我们自己的项目就接通了。那么如何接入呢?

接入的官方文档

image-20210808213637686

上图中的url就是自己电脑的项目

点击上图的提交按钮之后,微信会向上图中的url发送一个get请求,请求参数如下:

参数 描述
signature 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
timestamp 时间戳
nonce 随机数
echostr 随机字符串
  • 第二步:编写代码校验,用代码实现下面的逻辑

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

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

    3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信,如果比对成功,请原样返回echostr参数内容

在之前搭建的名为weixin的项目中新建一个【WxServlet.java】

package cn.imyjs.servlet;

import cn.imyjs.service.WxService;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

@WebServlet("/wx")
public class WxServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        String signature = req.getParameter("signature");
        String timestamp = req.getParameter("timestamp");
        String nonce = req.getParameter("nonce");
        String echostr = req.getParameter("echostr");

        if (WxService.check(signature,timestamp,nonce)){
            System.out.println("接入成功");
            resp.getWriter().write(echostr);
        } else {
            System.out.println("接入失败");
        }
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//        ServletInputStream inputStream = req.getInputStream();
//        byte[] bytes = new byte[1024];
//        int len = 0;
//        StringBuilder stringBuilder = new StringBuilder();
//        while ((len=inputStream.read(bytes)) != -1){
//            stringBuilder.append(new String(bytes,0,len));
//        }
//        System.out.println(stringBuilder.toString());
        // 防止乱码
        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");
        Map<String, String> requestMap = WxService.parseRequest(req.getInputStream());
        System.out.println(requestMap);
        // 准备回复的XML数据包
        String respXml = WxService.getRespMsg(requestMap);
        System.out.println(respXml);
        if (respXml != null)
            resp.getWriter().write(respXml);
    }
}

新建一个【WxService.java】并添加一个check工具方法

public class WxService {
    private static final String TOKEN = "XXXX"; //在微信配置界面自定义的token
    //登录测试号管理界面-测试号信息下面可以得到你的APPID和APPSECRET
    public static String APPID = "wx5d50022255560";
    public static String APPSECRET = "2df2cbc64466b80153e358aea14";
/**
     * 验证签名
     *
     * @param signature
     * @param timestamp
     * @param nonce
     * @return
     */
    public static boolean check(String signature, String timestamp, String nonce) {
        // 1)将token、timestamp、nonce三个参数进行字典序排序
        String strs[] = new String[]{TOKEN, timestamp, nonce};
        Arrays.sort(strs);
        // 2)将三个参数字符串拼接成一个字符串进行sha1加密
        String str1 = strs[0] + strs[1] + strs[2];
        // 3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
        String mysig = sha1(str1);
        // System.out.println(mysig);
        // System.out.println(signature);
        return signature.equals(mysig);
    }

    /**
     * Sha1加密
     *
     * @param str
     * @return
     */
    private static String sha1(String str) {

        MessageDigest sha1 = null;
        try {
            // 获取加密对象
            sha1 = MessageDigest.getInstance("sha1");
            char[] chars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
            StringBuilder sb = new StringBuilder();
            // 进行加密
            byte[] digest = sha1.digest(str.getBytes());
            for (byte b : digest) {
                sb.append(chars[(b >> 4) & 15]);
                sb.append(chars[b & 15]);
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }
}

启动项目,点击提交按钮,出现下面这个代表接入成功。

image-20210808220055857

8 接收用户消息

官方文档:接受普通消息

当普通微信用户向公众账号发消息时,微信服务器将POST消息的XML数据包到开发者填写的URL上。

也就是说用户发消息给微信服务器,微信服务器会发送post请求到我们自己的服务器,并且传送一个xml的数据给我们自己的服务器。

例如文本消息是这样的

<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>
参数 描述
ToUserName 开发者微信号
FromUserName 发送方帐号(一个OpenID)
CreateTime 消息创建时间 (整型)
MsgType 消息类型,文本为text
Content 文本消息内容
MsgId 消息id,64位整型

目前消息类型有以下几种:

 

java中这样的数据读取并不方便。可以转换一下,先通过dom4j这个包转成dom对象,再把标签名和对应的标签的值保存到HashMap集合中,这样后面处理数据就很方便了,具体代码实现如下:

在【WxServlet】中编写doPost方法,在测试号管理界面,扫码关注测试公众号

@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		Map<String,String> map = WxService.parseRequest(req.getInputStream());
		System.out.println(map);//关注测试号,给测试公众号发消息,就可以看到打印结果了
	}

在【WxService】中添加parseRequest方法

/**
	 * 将接受到的消息转化成map
	 * @param req
	 * @return
	 */
	public static  Map<String, String> parseRequest(InputStream is) {
		Map<String,String> map = new HashMap<String,String>();
		//1.通过io流得到文档对象
		SAXReader saxReader = new SAXReader();
		Document document = null;
		try {
			document = saxReader.read(is);
		} catch (DocumentException e) {
			e.printStackTrace();
		}
		//2.通过文档对象得到根节点对象
		Element root = document.getRootElement();
		//3.通过根节点对象获取所有子节点对象
		List<Element> elements = root.elements();
		//4.将所有节点放入map
		for (Element element : elements) {
			map.put(element.getName(), element.getStringValue());
		}
		return map;
	}

9 回复用户消息封装

官方文档:被动回复用户消息

当用户发送消息给公众号时(或某些特定的用户操作引发的事件推送时),会产生一个POST请求,开发者可以在响应包(Get)中返回特定XML结构,来对该消息进行响应(现支持回复文本、图片、图文、语音、视频、音乐)。严格来说,发送被动响应消息其实并不是一种接口,而是对微信服务器发过来消息的一次回复。

一旦遇到以下情况,微信都会在公众号会话中,向用户下发系统提示“该公众号暂时无法提供服务,请稍后再试”:

1、开发者在5秒内未回复任何内容 2、开发者回复了异常数据,比如JSON数据等

上面这段文字来自官方,可以看出

  • 回复必须是xml的类型
  • 可以回复多种类型的xml(文本、图片、图文、语音、视频、音乐)
  • 接收到消息没有做出响应就会抛出:该公众号暂时无法提供服务,请稍后再试

9.1 回复消息入门demo

这个demo就是给用户回复一个文本消息

回复的xml格式如下:

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[fromUser]]></FromUserName>
  <CreateTime>12345678</CreateTime>
  <MsgType><![CDATA[text]]></MsgType>
  <Content><![CDATA[你好]]></Content>
</xml>
参数 是否必须 描述
ToUserName 接收方帐号(收到的OpenID)
FromUserName 开发者微信号
CreateTime 消息创建时间 (整型)
MsgType 消息类型,文本为text
Content 回复的消息内容(换行:在content中能够换行,微信客户端就支持换行显示)

在wxservlet中doPost编写如下代码

@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		//设置编码格式,不然中文会乱码
		req.setCharacterEncoding("UTF-8");
		resp.setCharacterEncoding("UTF-8");
        //将请求中的xml参数转成map
		Map<String,String> map = WxService.parseRequest(req.getInputStream());
		System.out.println(map);
		//回复消息
		String textMsg = "<xml><ToUserName><!						[CDATA["+map.get("FromUserName")+"]]></ToUserName><FromUserName><![CDATA["+map.get("ToUserName")+"]]></FromUserName><CreateTime>12345678</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[你好]]></Content></xml>";
		resp.getWriter().print(textMsg);
	}

然后用测试号发消息,公众号都会回复一个 【你好】

这样写代码功能是可以实现,但是这样拼接字符串,再回复消息很不方便.然后自然就想到可以用java类来封装消息,响应的时候将java类转成xml(通过xstream这个工具包实现)。下面就以文本消息和图文消息为例进行封装,其它消息类似。使用面向对象思想进行开发

9.2 基础消息类的封装

把公共的属性放到基础消息类中,然后其它消息类继承即可。

@XStreamAlias 这个注解配置的就是转成xml时对应的节点名字

public class BaseMsg {
	@XStreamAlias("ToUserName")
	private String toUserName;//接收方的账号(收到的openid)
	@XStreamAlias("FromUserName")
	private String fromUserName;//开发者的微信号
	@XStreamAlias("CreateTime")
	private String createTime;//消息创建时间
	@XStreamAlias("MsgType")
	private String msgType;//消息类型

	public BaseMsg(Map<String,String> requestMap) {
		super();
		this.toUserName = requestMap.get("FromUserName");
		this.fromUserName = requestMap.get("ToUserName");
		this.createTime = requestMap.get("CreateTime");
	}
    
    //get and set ...
}

9.3 文本消息类封装

回复的xml的格式说明可以参考9.1入门demo.回复文本的封装类如下:

@XStreamAlias("xml") //xml指的就是xml这个根节点名称
public class TextMsg extends BaseMsg {
	@XStreamAlias("Content")
	private String content;//回复的文本内容
	
	public TextMsg(Map<String,String> requestMap,String content) {
		super(requestMap);
		this.setMsgType("text");
		this.content = content;
	}
    
    //get and set ...
}

9.4 图文消息封装

图文消息格式说明

<xml>
  <ToUserName><![CDATA[toUser]]></ToUserName>
  <FromUserName><![CDATA[fromUser]]></FromUserName>
  <CreateTime>12345678</CreateTime>
  <MsgType><![CDATA[news]]></MsgType>
  <ArticleCount>1</ArticleCount>
  <Articles>
    <item>
      <Title><![CDATA[title1]]></Title>
      <Description><![CDATA[description1]]></Description>
      <PicUrl><![CDATA[picurl]]></PicUrl>
      <Url><![CDATA[url]]></Url>
    </item>
  </Articles>
</xml>
参数 是否必须 说明
ToUserName 接收方帐号(收到的OpenID)
FromUserName 开发者微信号
CreateTime 消息创建时间 (整型)
MsgType 消息类型,图文为news
ArticleCount 图文消息个数;当用户发送文本、图片、语音、视频、图文、地理位置这六种消息时,开发者只能回复1条图文消息;其余场景最多可回复8条图文消息
Articles 图文消息信息,注意,如果图文数超过限制,则将只发限制内的条数
Title 图文消息标题
Description 图文消息描述
PicUrl 图片链接,支持JPG、PNG格式,较好的效果为大图360200,小图200200
Url 点击图文消息跳转链接

发现这类消息包含有二级节点标签,首先封装一个article类,对应就是xml中的item这个节点

@XStreamAlias("item")//映射到xml中的item这个节点
public class Article {
	@XStreamAlias("Title")
	private String title;//图文消息标题
	@XStreamAlias("Description")
	private String description;//图文消息描述
	@XStreamAlias("PicUrl")
	private String picUrl;//图片链接
	@XStreamAlias("Url")
	private String url;//点击图文消息跳转链接
    
    //get and set ...
}

然后再封装一个图文消息类

@XStreamAlias("xml")
public class NewsMsg extends BaseMsg {
	
	@XStreamAlias("ArticleCount")
	private String articleCount;//图文消息个数
	@XStreamAlias("Articles")
	private List<Article> articles;

	public NewsMsg(Map<String, String> requestMap,List<Article> articles) {
		super(requestMap);
		this.setMsgType("news");
		this.articles = articles;
		this.setArticleCount(this.articles.size()+"");
	}
    //get and set ...
}

其余几种消息和这中类似!

9.5 测试

前面已经将基础消息和图文消息封装好了,现在用封装好的消息类来回复

第一步:将wxservletdoPost方法改成如下

@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		//设置编码格式,不然中文会乱码
		req.setCharacterEncoding("UTF-8");
		resp.setCharacterEncoding("UTF-8");
        //将请求中的xml参数转成map
		Map<String,String> map = WxService.parseRequest(req.getInputStream());
		System.out.println(map);
        //处理完将响应一个xml给微信
		String respXml = WxService.getRespose(map);
		System.out.println(respXml);
		resp.getWriter().print(respXml);
	}

第二步:WxService添加如下方法:

	/**
	 * 事件消息回复
	 */
	public static String getRespose(Map<String, String> requestMap) {
		BaseMsg msg = null;
		// 根据用户发送消息的类型,做不同的处理
		String msgType = requestMap.get("MsgType");
		switch (msgType) {
		case "text":
			msg = dealTextMsg(requestMap);
			break;
		case "news":
			break;
		default:
			break;
		}
		// System.out.println(msg);
		// 将处理结果转化成xml的字符串返回
		if (null != msg) {
			return beanToXml(msg);
		}
		return null;
	}

	/**
	 * 将回复的消息类转成xml字符串
	 * 
	 * @param msg
	 * @return
	 */
	public static String beanToXml(BaseMsg msg) {
		XStream stream = new XStream();
		stream.processAnnotations(TextMsg.class);
		stream.processAnnotations(NewsMsg.class);
		String xml = stream.toXML(msg);
		return xml;
	}

	/**
	 * 当用户发送是文本消息的处理逻辑
	 * 
	 * @param map
	 * @return
	 */
	private static BaseMsg dealTextMsg(Map<String, String> requestMap) {
		// 获取用户发送的消息内容
		String msg = requestMap.get("Content");
		// 如果是图文回复一个图文消息
		if (msg.equals("图文")) {
			List<Article> articles = new ArrayList<Article>();
			articles.add(new Article("码云博客", "这个是我个人的码云博客,基于hexo搭建,里面的文章都是使用markdown编写",
					"https://heliufang.gitee.io/uploads/banner.jpg", "https://yjs0612.gitee.io/"));
			return new NewsMsg(requestMap, articles);
		}
		//否则回复一个文本消息,文本内容为'当前时间+你好'
        //当然这个内容可以自定义,在这里也可以接入自动回复机器人
		TextMsg textMsg = new TextMsg(requestMap, new Date(System.currentTimeMillis()).toLocaleString() + "你好");
		return textMsg;
	}

然后分别给公众号发一个1和图文

9.6 自动回复机器人

罗老师教程中的图灵机器人已经要收费.我使用的是阿里云的阿里云小蜜这个机器人来做的回复.

阿里云小蜜机器人可以免费体验三个月。

具体代码可以查看阿里云小蜜的文档:阿里云产品服务协议(云小蜜)

10 ★access token的获取

access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存.access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效

access token文档

目前access_token的有效期通过返回的expire_in来传达,目前是7200秒之内的值。中控服务器需要根据这个有效时间提前去刷新新access_token

总结:调用很多接口需要access_token,获取access_token之后需要保存起来,过期了再重新获取,而不是每次都重新获取。

接口调用请求说明

参数说明

参数 是否必须 说明
grant_type 获取access_token填写client_credential
appid 第三方用户唯一凭证
secret 第三方用户唯一凭证密钥,即appsecret

返回说明

正常情况下,微信会返回下述JSON数据包给公众号:

{"access_token":"ACCESS_TOKEN","expires_in":7200}

参数说明

参数 说明
access_token 获取到的凭证
expires_in 凭证有效时间,单位:秒

10.1 ★封装请求工具类

HttpClient

因为需要发送请求给微信服务器,所以需要有请求的工具类。罗老师用的是java自带的请求类,相对来说比较繁琐。所以我这里采用的是Apache HttpClient,这个用起来更加的简单。

第一步:pom.xml中导入依赖

<!--httpClient需要的依赖-->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.2</version>
</dependency>
<!--//httpclient缓存-->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient-cache</artifactId>
    <version>4.5</version>
</dependency>
<!--//http的mime类型都在这里面-->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpmime</artifactId>
    <version>4.3.2</version>
</dependency>

第二步:基于Apache HttpClient封装HttpUtils工具类,我封装了4个方法,可以支持get请求和post请求。后面很多需要用的地方直接调用即可。

可以参考这个博客:HttpClient发送get/post请求

public class HttpUtils {

	public static void main(String[] args) {
		// 1.测试get请求
		/*
		 String getUrl = "http://localhost:8080/user/searchPage?pageNum=1&pageSize=2";
		 System.out.println(sendGet(getUrl));
		 */
		
		// 2.测试post请求 携带x-www-form-urlencoded数据格式
		/*String postUrlForm = "http://localhost:8080/user";
		Map paramMap = new HashMap();
		paramMap.put("name", "杰克");
		paramMap.put("age", "20");
		paramMap.put("gender", "1");
		System.out.println(sendPost(postUrlForm, paramMap));*/
		
		//3.测试post请求 携带json数据格式
		/*String postUrlJson = "http://localhost:8080/user";
		String jsonParam = "{\"name\":\"jack\",\"age\":\"18\",\"gender\":\"2\"}";
		System.out.println(sendPost(postUrlJson,jsonParam));*/
		
		//4 测试post 携带文件
		String postUrlFile = "http://localhost:8080/user/upload";
		Map paramMap = new HashMap();
		paramMap.put("name", "tom");
		String localFile = "d:\\logo.png";
		String fileParamName = "file";
		System.out.println(sendPost(postUrlFile, paramMap,localFile,fileParamName));
	}

	// 1.httpClient发送get请求
	public static String sendGet(String url) {
		String result = "";
		CloseableHttpResponse response = null;
		try {
			// 根据地址获取请求
			HttpGet request = new HttpGet(url);// 这里发送get请求
			// 获取当前客户端对象
			CloseableHttpClient httpClient = HttpClients.createDefault();
			// 通过请求对象获取响应对象
			response = httpClient.execute(request);
			// 判断网络连接状态码是否正常(0--200都数正常)
			if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
				result = EntityUtils.toString(response.getEntity(), "utf-8");
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (null != response) {
				try {
					response.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return result;
	}

	// 2.httpClient发送post请求 携带x-www-form-urlencoded数据格式
	public static String sendPost(String url, Map<String, String> map) {
		CloseableHttpResponse httpResponse = null;
		String result = "";
		try {
			// 1、创建一个httpClient客户端对象
			CloseableHttpClient httpClient = HttpClients.createDefault();
			// 2、创建一个HttpPost请求
			HttpPost httpPost = new HttpPost(url);
			// 设置请求头
			httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded"); // 设置传输的数据格式
			// 携带普通的参数params的方式
			List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
			Set<String> keys = map.keySet();
			for (String key : keys) {
				params.add(new BasicNameValuePair(key, map.get(key)));
			}
			String str = EntityUtils.toString(new UrlEncodedFormEntity(params, Consts.UTF_8));
			// 这里就是:username=kylin&password=123456
			System.out.println(str);

			// 放参数进post请求里面 从名字可以知道 这个类是专门处理x-www-form-urlencoded 添加参数的
			httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));

			// 7、执行post请求操作,并拿到结果
			httpResponse = httpClient.execute(httpPost);
			// 获取结果实体
			HttpEntity entity = httpResponse.getEntity();
			if (entity != null) {
				result = EntityUtils.toString(entity, "UTF-8");
			} else {
				EntityUtils.consume(entity);//// 如果entity为空,那么直接消化掉即可
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (null != httpResponse) {
				try {
					httpResponse.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return result;
	}

	// 3.httpClient发送post请求 携带json数据格式
	public static String sendPost(String url, String jsonStr) {
		CloseableHttpResponse httpResponse = null;
		String result = "";
		try {
			// 1.创建httpClient
			CloseableHttpClient httpClient = HttpClients.createDefault();
			// 2.创建post请求方式实例
			HttpPost httpPost = new HttpPost(url);

			// 2.1设置请求头 发送的是json数据格式
			httpPost.setHeader("Content-type", "application/json;charset=utf-8");
			httpPost.setHeader("Connection", "Close");

			// 3.设置参数---设置消息实体 也就是携带的数据
			/*
			 * 比如传递: { "username": "aries", "password": "666666" }
			 */
			//String jsonStr = " {\"username\":\"aries\",\"password\":\"666666\"}";
			StringEntity entity = new StringEntity(jsonStr.toString(), Charset.forName("UTF-8"));
			entity.setContentEncoding("UTF-8"); // 设置编码格式
			// 发送Json格式的数据请求
			entity.setContentType("application/json");
			// 把请求消息实体塞进去
			httpPost.setEntity(entity);

			// 4.执行http的post请求
			// 4.执行post请求操作,并拿到结果
			httpResponse = httpClient.execute(httpPost);
			// 获取结果实体
			HttpEntity httpEntity = httpResponse.getEntity();
			if (httpEntity != null) {
				result = EntityUtils.toString(httpEntity, "UTF-8");
			} else {
				EntityUtils.consume(httpEntity);//// 如果httpEntity为空,那么直接消化掉即可
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (null != httpResponse) {
				try {
					httpResponse.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return result;
	}

	// 4.httpClient发送post请求 携带文件
	public static String sendPost(String url, Map<String, String> map,String localFile, String fileParamName) {
		HttpPost httpPost = new HttpPost(url);
		CloseableHttpClient httpClient = HttpClients.createDefault();
        String resultString = "";
        CloseableHttpResponse response = null;
        try {
            // 把文件转换成流对象FileBody
            FileBody bin = new FileBody(new File(localFile));

            MultipartEntityBuilder builder = MultipartEntityBuilder.create();

            // 相当于<input type="file" name="fileParamName"/> 其中fileParamName以传进来的为准
            builder.addPart(fileParamName, bin);
            // 相当于<input type="text" name="userName" value=userName>
            /*builder.addPart("filesFileName",
                    new StringBody(fileParamName, ContentType.create("text/plain", Consts.UTF_8)));*/
            if (map != null) {
                for (String key : map.keySet()) {
                    builder.addPart(key,
                            new StringBody(map.get(key), ContentType.create("text/plain", Consts.UTF_8)));
                }
            }
            HttpEntity reqEntity = builder.build();
            httpPost.setEntity(reqEntity);
            // 发起请求 并返回请求的响应
            response = httpClient.execute(httpPost, HttpClientContext.create());
            resultString = EntityUtils.toString(response.getEntity(), "utf-8");
        }  catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (response != null)
                    response.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return resultString;
	}
}

原生URL对象

public class Util {
    //百度AI
    //设置APPID/AK/SK
    public static final String APP_ID = "00000";
    public static final String API_KEY = "DkvPT0pz00000wNgQsuZeGQI";
    public static final String SECRET_KEY = "5nOOLwwI0000rjIcyCZbTeW0";

    /**
     * 发送POST请求
     */
    public static String sendPost(String url, String data) {
        URL urlObj = null;
        try {
            urlObj = new URL(url);
            URLConnection urlConnection = urlObj.openConnection();
            // 设置可发送数据状态
            urlConnection.setDoOutput(true);
            // 获取输出流
            OutputStream os = urlConnection.getOutputStream();
            // 写入数据
            os.write(data.getBytes());
            // 关闭输出流
            os.close();
            // 获取返回输入流
            InputStream is = urlConnection.getInputStream();
            byte[] bytes = new byte[1024];
            int len = 0;
            StringBuilder stringBuilder = new StringBuilder();
            while ((len = is.read(bytes)) != -1) {
                stringBuilder.append(new String(bytes, 0, len));
            }
            return stringBuilder.toString();

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 发送GET请求
     */
    public static String sendGet(String url) {
        URL urlObj = null;
        try {
            urlObj = new URL(url);
            URLConnection urlConnection = urlObj.openConnection();
//            urlConnection.setRequestProperty("Charset", "utf8");
            InputStream is = urlConnection.getInputStream();
            byte[] bytes = new byte[1024];
            int len = 0;
            StringBuilder stringBuilder = new StringBuilder();
            while ((len = is.read(bytes)) != -1) {
                stringBuilder.append(new String(bytes, 0, len));
            }
            return stringBuilder.toString();

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

	/**
	* 调用百度AI通用文字识别API
	*/
    public static String ImageOCR(String picUrl) {
        // 初始化一个AipOcr
        AipOcr client = new AipOcr(APP_ID, API_KEY, SECRET_KEY);

        // 可选:设置网络连接参数
        client.setConnectionTimeoutInMillis(2000);
        client.setSocketTimeoutInMillis(60000);

        // 调用接口
        //String path = "C:\\Users\\Smile\\Desktop\\weixin\\src\\main\\webapp\\img\\1.jpg";

        //org.json.JSONObject res = client.basicGeneral(path, new HashMap<String, String>());
        org.json.JSONObject res = client.basicGeneralUrl(picUrl, new HashMap<String, String>());
        String json = res.toString(2);
        JSONObject object = JSONObject.parseObject(json);
        JSONArray words_result = object.getJSONArray("words_result");
        Iterator<Object> iterator = words_result.iterator();
        StringBuilder sb = new StringBuilder();
        while (iterator.hasNext()) {
            JSONObject jsonObject = (JSONObject) iterator.next();
            sb.append(jsonObject.getString("words"));
        }
        System.out.println(sb.toString());
        return sb.toString();
    }
}

10.2 创建AccessToken类

public class AccessToken {
	private String token;
	private long expiresTime;//过期时间
	
	public AccessToken(String token, String expiresIn) {
		super();
		this.token = token;
		//当前时间+有效期 = 过期时间
		this.expiresTime = System.currentTimeMillis()+Integer.parseInt(expiresIn);
	}
	
	/**
	 * 判断token是否过期
	 * @return
	 */
	public boolean isExpire() {
		return System.currentTimeMillis() > expiresTime;
	}
    //get and set ...
}

10.3 WxService中添加获取AccessToken的方法

private static AccessToken at;//token获取的次数有限,有效期也有限,所以需要保存起来
private static String GET_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
	
	//登录测试号管理界面-测试号信息下面可以得到你的APPID和APPSECRET
	private static String APPID = "wx7b00000050a5a";
	private static String APPSECRET = "8d9930d0000000620ad993d984d8";
/**
	 * 发送get请求获取AccessToken
	 */
	private static void getToken() {
		String url = GET_TOKEN_URL.replace("APPID", APPID).replace("APPSECRET", APPSECRET);
		String tokenStr = HttpUtils.sendGet(url);//调用工具类发get请求
		System.out.println(tokenStr);
		JSONObject jsonObject = JSONObject.parseObject(tokenStr);
		String token = jsonObject.getString("access_token");
		String expiresIn = jsonObject.getString("expires_in");
		at = new AccessToken(token, expiresIn);
	}
	
	/**
	 * 获取AccessToken  向外提供
	 */
	public static String getAccessToken() {
		//过期了或者没有值再去发送请求获取
		if(at == null || at.isExpire()) {
			getToken();
		}
		return at.getToken();
	}

11 自定义菜单

自定义菜单文档

请注意:

  1. 自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单。
  2. 一级菜单最多4个汉字,二级菜单最多8个汉字,多出来的部分将会以“…”代替。
  3. 创建自定义菜单后,菜单的刷新策略是,在用户进入公众号会话页或公众号profile页时,如果发现上一次拉取菜单的请求在5分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。

自定义菜单接口可实现多种类型按钮,如下:

  1. click:点击推事件用户点击click类型按钮后,微信服务器会通过消息接口推送消息类型为event的结构给开发者(参考消息接口指南),并且带上按钮中开发者填写的key值,开发者可以通过自定义的key值与用户进行交互;
  2. view:跳转URL用户点击view类型按钮后,微信客户端将会打开开发者在按钮中填写的网页URL,可与网页授权获取用户基本信息接口结合,获得用户基本信息。
  3. scancode_push:扫码推事件用户点击按钮后,微信客户端将调起扫一扫工具,完成扫码操作后显示扫描结果(如果是URL,将进入URL),且会将扫码的结果传给开发者,开发者可以下发消息。
  4. scancode_waitmsg:扫码推事件且弹出“消息接收中”提示框用户点击按钮后,微信客户端将调起扫一扫工具,完成扫码操作后,将扫码的结果传给开发者,同时收起扫一扫工具,然后弹出“消息接收中”提示框,随后可能会收到开发者下发的消息。
  5. pic_sysphoto:弹出系统拍照发图用户点击按钮后,微信客户端将调起系统相机,完成拍照操作后,会将拍摄的相片发送给开发者,并推送事件给开发者,同时收起系统相机,随后可能会收到开发者下发的消息。
  6. pic_photo_or_album:弹出拍照或者相册发图用户点击按钮后,微信客户端将弹出选择器供用户选择“拍照”或者“从手机相册选择”。用户选择后即走其他两种流程。
  7. pic_weixin:弹出微信相册发图器用户点击按钮后,微信客户端将调起微信相册,完成选择操作后,将选择的相片发送给开发者的服务器,并推送事件给开发者,同时收起相册,随后可能会收到开发者下发的消息。
  8. location_select:弹出地理位置选择器用户点击按钮后,微信客户端将调起地理位置选择工具,完成选择操作后,将选择的地理位置发送给开发者的服务器,同时收起位置选择工具,随后可能会收到开发者下发的消息。
  9. media_id:下发消息(除文本消息)用户点击media_id类型按钮后,微信服务器会将开发者填写的永久素材id对应的素材下发给用户,永久素材类型可以是图片、音频、视频、图文消息。请注意:永久素材id必须是在“素材管理/新增永久素材”接口上传后获得的合法id。
  10. view_limited:跳转图文消息URL用户点击view_limited类型按钮后,微信客户端将打开开发者在按钮中填写的永久素材id对应的图文消息URL,永久素材类型只支持图文消息。请注意:永久素材id必须是在“素材管理/新增永久素材”接口上传后获得的合法id。

接口调用请求说明

http请求方式:POST(请使用https协议) https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN

url中的ACCESS_TOKEN就是之前获取的,调用这个接口需要带上

请求需携带json参数

{
 "button":[
 	{
 		  "type":"click",
          "name":"一级点击",
          "key":"1"
 	},
 	{
 		  "type":"view",
          "name":"个人博客",
          "url":"https://yjs0612.gitee.io/"
 	},
 	{
          "name":"有子菜单",
          "sub_button":[
          	{
                  "type":"click",
                  "name":"三一点击",
                  "key":"31"
            },
            {
                  "type":"view",
                  "name":"码云博客",
                  "url":"https://yjs0612.gitee.io/"
            },
            {
            	"type":"pic_photo_or_album",
            	"name":"拍照或发图",
            	"key":"33"
            }
          ]
 	}
 ]
}

参数说明

参数 是否必须 说明
button 一级菜单数组,个数应为1~3个
sub_button 二级菜单数组,个数应为1~5个
type 菜单的响应动作类型,view表示网页类型,click表示点击类型,miniprogram表示小程序类型
name 菜单标题,不超过16个字节,子菜单不超过60个字节
key click等点击类型必须 菜单KEY值,用于消息接口推送,不超过128字节
url view、miniprogram类型必须 网页 链接,用户点击菜单可打开链接,不超过1024字节。 type为miniprogram时,不支持小程序的老版本客户端将打开本url。
media_id media_id类型和view_limited类型必须 调用新增永久素材接口返回的合法media_id
appid miniprogram类型必须 小程序的appid(仅认证公众号可配置)
pagepath miniprogram类型必须 小程序的页面路径

返回结果

正确时的返回JSON数据包如下:


{"errcode":0,"errmsg":"ok"}

错误时的返回JSON数据包如下(示例为无效菜单名长度):


{"errcode":40018,"errmsg":"invalid button name size"}

和前面xml的类似,我们需要对着请求的json数据封装按钮类,这样后面操作起来就比较方便,而且也方便维护。

11.1 封装菜单类

<1>AbstractButton类

//所有菜单(按钮)的父类
public abstract class AbstractButton {
	private String name;//按钮标题

	public String getName() {
		return this.name;
	}

	public void setName(final String name) {
		this.name = name;
	}

	public AbstractButton(final String name) {
		this.name = name;
	}
}

<2>Button类

//一级菜单对象
public class Button {
	private List<AbstractButton> button;

	public Button() {
		this.button = new ArrayList<AbstractButton>();
	}

	public List<AbstractButton> getButton() {
		return this.button;
	}

	public void setButton(final List<AbstractButton> button) {
		this.button = button;
	}
}

<3>ClickButton类

//点击类型的菜单
public class ClickButton extends AbstractButton {
	private String type;
	private String key;

	public String getType() {
		return this.type;
	}

	public void setType(final String type) {
		this.type = type;
	}

	public String getKey() {
		return this.key;
	}

	public void setKey(final String key) {
		this.key = key;
	}

	public ClickButton(final String name, final String key) {
		super(name);
		this.type = "click";//点击类型
		this.key = key;
	}
}

<4>ViewButton类

//网页类型的菜单
public class ViewButton extends AbstractButton {
	private String type;
	private String url;

	public String getType() {
		return this.type;
	}

	public void setType(final String type) {
		this.type = type;
	}

	public String getUrl() {
		return this.url;
	}

	public void setUrl(final String url) {
		this.url = url;
	}

	public ViewButton(final String name, final String url) {
		super(name);
		this.type = "view";//网页类型
		this.url = url;
	}
}

<5> PhotoOrAlbumButton

//拍照或传图菜单
public class PhotoOrAlbumButton extends AbstractButton{
	private String type;
	private String key;

	public PhotoOrAlbumButton(String name,String key) {
		super(name);
		this.type = "pic_photo_or_album";//拍照获取传图
		this.key = key;
	}

	public String getType() {
		return type;
	}

	public void setType(String type) {
		this.type = type;
	}

	public String getKey() {
		return key;
	}

	public void setKey(String key) {
		this.key = key;
	}
}

<6>SubButton

import java.util.ArrayList;
import java.util.List;

//二级菜单对象
public class SubButton extends AbstractButton {
	private List<AbstractButton> sub_button;

	public List<AbstractButton> getSub_button() {
		return this.sub_button;
	}

	public void setSub_button(final List<AbstractButton> sub_button) {
		this.sub_button = sub_button;
	}

	public SubButton(final String name) {
		super(name);
		this.sub_button = new ArrayList<AbstractButton>();
	}
}

11.2 测试

public class TestMenu {
    @Test
    public void testMenu(){
        //创建一级菜单
        Button button = new Button();
        //在第三个菜单中创建二级菜单
        SubButton subButton = new SubButton("有子菜单");
        List<AbstractButton> list2 = new ArrayList();
        list2.add(new ClickButton("三一点击", "31"));
        list2.add(new ViewButton("我的网站", "http://www.imyjs.cn/"));
        list2.add(new PhotoOrAlbumButton("拍照或发图","33"));
        subButton.setSub_button(list2);
        //在一级菜单中添加三个按钮,
        List<AbstractButton> list = new ArrayList();
        list.add(new ClickButton("一级点击", "1"));
        list.add(new ViewButton("个人博客", "http://www.imyjs.cn/"));
        list.add(subButton);
        button.setButton(list);
        //转成json格式字符串
        String jsonString = JSONObject.toJSONString(button);
        System.out.println(jsonString);
        //发送请求
		String url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";
		url = url.replace("ACCESS_TOKEN", WxService.getAccessToken());//把token带上
		String result = HttpUtils.sendPost(url, jsonString);
		System.out.println(result);
    }
}

 

12 设置和获取行业信息

12.1 设置行业信息

如果要发送模板消息,那么首先就得设置行业信息,如何设置和获取可以看下面接口。

模板消息文档

设置行业可在微信公众平台后台完成,每月可修改行业1次,帐号仅可使用所属行业中相关的模板,为方便第三方开发者,提供通过接口调用的方式来修改账号所属行业,具体如下:

接口调用请求说明

POST数据说明

POST数据示例如下:

{
    "industry_id1":"1",
    "industry_id2":"4"
}

参数说明

参数 是否必须 说明
access_token 接口调用凭证
industry_id1 公众号模板消息所属行业编号-主行业
industry_id2 公众号模板消息所属行业编号-副行业

行业代码查询,更多代码可以查询文档

主行业 副行业 代码
IT科技 互联网/电子商务 1
IT科技 IT软件与服务 2
IT科技 IT硬件与设备 3

编写测试代码

	@Test
	public void setIndustry() {
		String url = "https://api.weixin.qq.com/cgi-bin/template/api_set_industry?access_token=ACCESS_TOKEN";
		url = url.replace("ACCESS_TOKEN", WxService.getAccessToken());
		String jsonStr = "{\"industry_id1\":\"1\",\"industry_id2\":\"4\"}";
		String rString = HttpUtils.sendPost(url, jsonStr);
		System.out.println(rString);
	}

12.2 获取行业信息

获取帐号设置的行业信息。可登录微信公众平台,在公众号后台中查看行业信息。为方便第三方开发者,提供通过接口调用的方式来获取帐号所设置的行业信息,具体如下:

接口调用请求说明

参数说明

参数 是否必须 说明
access_token 接口调用凭证

返回说明

正确调用后的返回示例:

json

{
    "primary_industry":{"first_class":"运输与仓储","second_class":"快递"},
    "secondary_industry":{"first_class":"IT科技","second_class":"互联网|电子商务"}
}

返回参数说明

参数 是否必填 说明
access_token 接口调用凭证
primary_industry 帐号设置的主营行业
secondary_industry 帐号设置的副营行业

编写测试代码

	@Test
	public void getIndustry() {
		String url = "https://api.weixin.qq.com/cgi-bin/template/get_industry?access_token=ACCESS_TOKEN";
		url = url.replace("ACCESS_TOKEN", WxService.getAccessToken());
		String string = HttpUtils.sendGet(url);
		System.out.println(string);
	}

13 发送模板消息

模板消息接口

就是微信主动给用户推送消息,不需要像之前那样被动(用户发送之后再回复).

接口调用请求说明

POST数据如下:

{
	"touser": "oQxvI51GI5t9wBaBjmBXgJZZVM3A",
	"template_id": "tQ0G9Pmd_n_ylmplYsEnexgabkJXH1S3J7BXahK454g",
	"url": "https://heliufang.gitee.io/",
	"data": {
		"first": {
			"value": "您好!您投递的简历有新的反馈",
			"color": "#173177"
		},
		"company": {
			"value": "广州壹新网络科技有限公司",
			"color": "#173177"
		},
		"time": {
			"value": "2021-8-5 23:31:23",
			"color": "#173177"
		},
		"result": {
			"value": "已通过",
			"color": "#ff0000"
		},
		"remark": {
			"value": "带身份证",
			"color": "#173177"
		}
	}
}

参数说明

参数 是否必填 说明
touser 接收者openid
template_id 模板ID,这个需要在管理界面配置
url 模板跳转链接(海外帐号没有跳转能力)
data 模板数据
color 模板内容字体颜色,不填默认为黑色

返回码说明

在调用模板消息接口后,会返回JSON数据包。正常时的返回JSON数据包示例:

{"errcode":0,"errmsg":"ok","msgid":200228332}

★第一步:在微信测试号管理后台配置模板:

  • 模板标题: 简历反馈提醒
  • 模板内容:
{{first.DATA}} 
公司名:{{company.DATA}} 
投递时间:{{time.DATA}} 
反馈结果:{{result.DATA}} {{remark.DATA}}

创建好之后是下面这个样子

第二步:编写代码

@Test
	public void sendTemplateMsg() {
		String url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN";
		url = url.replace("ACCESS_TOKEN", WxService.getAccessToken());
		//实际开发中应封装成java类,再把java对象转成类似下面的jsonstr
		String jsonStr = "{\r\n" + 
				"	\"touser\": \"oQxvI51GI5t9wBaBjmBXgJZZVM3A\",\r\n" + 
				"	\"template_id\": \"tQ0G9Pmd_n_ylmplYsEnexgabkJXH1S3J7BXahK454g\",\r\n" + 
				"	\"url\": \"https://heliufang.gitee.io/\",\r\n" + 
				"	\"data\": {\r\n" + 
				"		\"first\": {\r\n" + 
				"			\"value\": \"您好!您投递的简历有新的反馈\",\r\n" + 
				"			\"color\": \"#173177\"\r\n" + 
				"		},\r\n" + 
				"		\"company\": {\r\n" + 
				"			\"value\": \"广州壹新网络科技有限公司\",\r\n" + 
				"			\"color\": \"#173177\"\r\n" + 
				"		},\r\n" + 
				"		\"time\": {\r\n" + 
				"			\"value\": \"2021-8-5 23:31:23\",\r\n" + 
				"			\"color\": \"#173177\"\r\n" + 
				"		},\r\n" + 
				"		\"result\": {\r\n" + 
				"			\"value\": \"已通过\",\r\n" + 
				"			\"color\": \"#ff0000\"\r\n" + 
				"		},\r\n" + 
				"		\"remark\": {\r\n" + 
				"			\"value\": \"带身份证\",\r\n" + 
				"			\"color\": \"#173177\"\r\n" + 
				"		}\r\n" + 
				"	}\r\n" + 
				"}";
		String rString = HttpUtils.sendPost(url, jsonStr);
		System.out.println(rString);
	}

测试结果如下

image-20210814171316722

14 新增和获取临时素材

公众号经常有需要用到一些临时性的多媒体素材的场景,例如在使用接口特别是发送消息时,对多媒体文件、多媒体消息的获取和调用等操作,是通过media_id来进行的。素材管理接口对所有认证的订阅号和服务号开放。

注意点:

1、临时素材media_id是可复用的。

2、媒体文件在微信后台保存时间为3天,即3天后media_id失效。

3、上传临时素材的格式、大小限制与公众平台官网一致。

图片(image): 10M,支持PNG\JPEG\JPG\GIF格式

语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式

视频(video):10MB,支持MP4格式

缩略图(thumb):64KB,支持JPG格式

14.1 新增临时素材

新增临时素材文档

罗老师用的是java自带的文件类上传,代码比较繁琐。而我使用HttpClient封装的HttpUtils上传就很简单了。

接口调用请求说明

参数说明

参数 是否必须 说明
access_token 调用接口凭证
type 媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb)
media form-data中媒体文件标识,有filename、filelength、content-type等信息

返回说明

正确情况下的返回JSON数据包结果如下:

{"type":"image","media_id":"atL80WWRNpMWhivoIGf9KTUUUO5pm6RxML8OPEUd7cbfb1Rs0kl2Yv0319KMQI-0","created_at":1628933345,"item":[]}
参数 描述
type 媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb,主要用于视频与音乐格式的缩略图)
media_id 媒体文件上传后,获取标识
created_at 媒体文件上传时间戳

编写测试代码

//上传图片
	@Test
	public void uploadMedia() {
		String url = "https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE";
		url = url.replace("ACCESS_TOKEN", WxService.getAccessToken());
		url = url.replace("TYPE", "image");
		String string = HttpUtils.sendPost(url, null, "C:\\Users\\Administrator\\Desktop\\2.jpg", "");
		System.out.println(string);
	}

不使用HttpClient代码:

/**
     * 上传临时素材 使用原生URL对象发送
     * @param path 上传素材路径
     * @param type 上传素材类型
     * 图片(image): 10M,支持PNG\JPEG\JPG\GIF格式
     * 语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式
     * 视频(video):10MB,支持MP4格式
     * 缩略图(thumb):64KB,支持JPG格式
     *
     * @return
     */
    public static String upload(String path, String type){
        // 获取文件对象
        File file = new File(path);
        // 微信上传素材API路径
        // http请求方式:POST/FORM,
        // 使用https https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE
        // 调用示例(使用curl命令,用FORM表单方式上传一个多媒体文件): curl -F media=@test.jpg "https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE"
        String url = "https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE";
        // 处理url
        url = url.replace("ACCESS_TOKEN", getAccessToken()).replace("TYPE", type);
        // 以下为设置Https 请求信息
        try {
            // 获取URL对象
            URL urlObj = new URL(url);
            // 强转为安全连接
            HttpsURLConnection conn = (HttpsURLConnection)urlObj.openConnection();
            // 设置连接的信息
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setUseCaches(false);
            // 设置请求头信息
            conn.setRequestProperty("Connection", "Keep-Alive");
            conn.setRequestProperty("Charset", "utf8");
            // 数据的边界
            String boundary = "-----" + System.currentTimeMillis();
            conn.setRequestProperty("Content-Type","multipart/form-data;boundary="+boundary);
            // 获取连接的输出流
            OutputStream out = conn.getOutputStream();
            // 创建文件的输入流
            InputStream in = new FileInputStream(file);
            // 第一部分 头部信息
            StringBuilder sb = new StringBuilder();
            sb.append("--");
            sb.append(boundary);
            sb.append("\r\n");
            sb.append("Content-Disposition:form-data;name=\"media\";filename=\"" + file.getName()+ "\"\r\n");
            sb.append("Content-Type:application/octet-stream\r\n\r\n");
            out.write(sb.toString().getBytes());
            System.out.println(sb.toString());

            // 第二部分 文件内容
            byte[] bytes = new byte[1024];
            int len;
            while ((len = in.read(bytes))!= -1) {
                out.write(bytes,0,len);
            }

            //第三部分 尾部信息
            String foot = "\r\n--" + boundary + "--\r\n";
            out.write(foot.getBytes());
            out.flush();
            out.close();

            //读取返回数据
            InputStream is = conn.getInputStream();
            StringBuilder resp = new StringBuilder();
            while ((len=is.read(bytes))!=-1){
                resp.append(new String(bytes,0,len));
            }
            return resp.toString();

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

14.2 获取临时素材

获取临时素材文档

接口调用请求说明

http请求方式: GET,https调用 https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID 请求示例(示例为通过curl命令获取多媒体文件) curl -I -G “https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID"

把ACCESS_TOKEN和MEDIA_ID替换到url的位置,然后浏览器打开就可以下载了

参数说明

参数 是否必须 说明
access_token 调用接口凭证
media_id 媒体文件ID

返回说明

正确情况下的返回HTTP头如下:

HTTP/1.1 200 OK
Connection: close
Content-Type: image/jpeg
Content-disposition: attachment; filename="MEDIA_ID.jpg"
Date: Sun, 06 Jan 2013 10:20:18 GMT
Cache-Control: no-cache, must-revalidate
Content-Length: 339721
curl -G "https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID"

15 二维码生成和扫描

15.1 生成带参数的临时二维码

为了满足用户渠道推广分析和用户帐号绑定等场景的需要,公众平台提供了生成带参数二维码的接口。使用该接口可以获得多个带不同场景值的二维码,用户扫描后,公众号可以接收到事件推送。

目前有2种类型的二维码:

1、临时二维码,是有过期时间的,最长可以设置为在二维码生成后的30天(即2592000秒)后过期,但能够生成较多数量。临时二维码主要用于帐号绑定等不要求二维码永久保存的业务场景 2、永久二维码,是无过期时间的,但数量较少(目前为最多10万个)。永久二维码主要用于适用于帐号绑定、用户来源统计等场景。

获取带参数的二维码的过程包括两步,首先创建二维码ticket,然后凭借ticket到指定URL换取二维码。

生成带参数的二维码文档

测试代码将实现下面这样一个功能,点击页面上的生成按钮,在页面展示生成好的二维码

【index.jsp】

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>二维码测试页面</title>
</head>
<body>
<button type="button">生成二维码</button><br>
<img alt="暂无图片" src="">
</body>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script>
    $("button").click(function(){
        $.ajax({
            url: "/weixin/getQrCode",
            type: "get",
            dataType: "json",
            success: function(resp){
                console.log(resp);
                //通过ticket获取图片
                var src = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket="+resp.ticket;
                $("img").attr("src",src)
            }
        })
    })
</script>
</html>

【后端servlet】

@WebServlet("/getQrCode")
public class GetQrCodeServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置编码格式,不然中文会乱码
        req.setCharacterEncoding("UTF-8");
        resp.setCharacterEncoding("UTF-8");

        String qrCodeTicket = WxService.getQrCodeTicket();
        resp.getWriter().write(qrCodeTicket);
    }


    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doGet(req,resp);
    }
}

15.2 扫描二维码

用户扫描带场景值二维码时,可能推送以下两种事件:

如果用户还未关注公众号,则用户可以关注公众号,关注后微信会将带场景值关注事件推送给开发者。

如果用户已经关注公众号,在用户扫描后会自动进入会话,微信也会将带场景值扫描事件推送给开发者

扫描临时二维码之后,会向服务器推送一个xml数据包,解析之后打印效果如下:

{
Ticket=gQFr8DwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAya1JKeDQ2M3JmOEQxOGlybk54Y08AAgS6mBdhAwRYAgAA, 
FromUserName=oQxvI51GI5t9wBaBjmBXgJZZVM3A,
EventKey=test, 
Event=SCAN, 
CreateTime=1628936703, 
ToUserName=gh_c8af0521f09a, 
MsgType=event
}

实现扫码之后给用户回复一个[你扫码了]

修改【WxService】的代码,修改getRespose方法,新增dealEvent和dealScanEvent方法

/**
	 * 事件消息回复
	 */
	public static String getRespose(Map<String, String> requestMap) {
		BaseMsg msg = null;
		// 根据用户发送消息的类型,做不同的处理
		String msgType = requestMap.get("MsgType");
		switch (msgType) {
		case "text":
			msg = dealTextMsg(requestMap);
			break;
		case "news":
			break;
		case "event":
		    //新增处理事件的方法
			msg = dealEvent(requestMap);
			break;
		default:
			break;
		}
		// System.out.println(msg);
		// 将处理结果转化成xml的字符串返回
		if (null != msg) {
			return beanToXml(msg);
		}
		return null;
	}

	//处理事件
	private static BaseMsg dealEvent(Map<String, String> requestMap) {
		String event = requestMap.get("Event");
		BaseMsg msg = null;
        //switch分发到具体事件
		switch (event) {
            case "SCAN":
                msg = dealScanEvent(requestMap);
                break;
            default:
                break;
		}
		return msg;
	}

	//处理SCAN事件
	private static BaseMsg dealScanEvent(Map<String, String> requestMap) {
		String eventKey = requestMap.get("EventKey");
		if("test".equals(eventKey)) {
			return new TextMsg(requestMap, "你扫码了");
		}
		return new TextMsg(requestMap, requestMap.toString());
	}

16 获取用户信息

一般在做网页授权的时候,会用到这个功能。

16.1 获取已关注的用户信息

获取用户基本信息(UnionID机制)

在关注者与公众号产生消息交互后,公众号可获得关注者的OpenID(加密后的微信号,每个用户对每个公众号的OpenID是唯一的。对于不同公众号,同一用户的openid不同)。公众号可通过本接口来根据OpenID获取用户基本信息,包括昵称、头像、性别、所在城市、语言和关注时间。

获取用户基本信息(包括UnionID机制)

开发者可通过OpenID来获取用户基本信息。请使用https协议。

参数说明

参数 是否必须 说明
access_token 调用接口凭证
openid 普通用户的标识,对当前公众号唯一
lang 返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语

openid可以登录测试号管理界面获取,对应关注者的微信号

测试代码

	@Test
	public void getUserInfo() {
		String url = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";
		url = url.replace("ACCESS_TOKEN", WxService.getAccessToken());
		url = url.replace("OPENID", "oQxvI51GI5t9wBaBjmBXgJZZVM3A");
		String string = HttpUtils.sendGet(url);
		System.out.println(string);//这里就可以看到打印的用户信息了
	}

16.2 网页授权

可以获取未关注的用户信息,这部分需要有域名才能测试。

网页授权

因为ngrok默认域名已经被微信封了,所以需要申请一个域名来测试

第一步:在ngrok中使用自定义域名。

第二步:微信测试号管理界面修改成自定义的域名

第三步:测试号接口权限中配置网页授权的域名

测试代码:

【WxService】修改dealTextMsg方法

private static BaseMessage dealTextMsg(Map<String, String> requestMap) {
        String msg = requestMap.get("Content");
        if ("登录".equals(msg)){
            String url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE#wechat_redirect";
            url = url.replace("APPID",APPID).replace("REDIRECT_URI",REDIRECT_URI).replace("SCOPE","snsapi_userinfo");
            return new TextMessage(requestMap,"<a href=\""+ url + "\">点击登录</a>");
        }
        TextMessage tm = new TextMessage(requestMap, "你好啊");
        return tm;
    }

新增一个【getUserInfo】的servlet,这个就是用户点击之后重定向的servlet

@WebServlet("/getuserinfo")
public class GetUserInfoServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        //1.用户同意授权,获取code
        String code = request.getParameter("code");
        //2.通过code获取网页授权的access_token
        String url = " https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
        url = url.replace("APPID",WxService.APPID).replace("CODE", code).replace("SECRET", WxService.APPSECRET);
        String string = Util.sendGet(url);
        JSONObject jsonObject = JSONObject.parseObject(string);
        String accessToken = jsonObject.getString("access_token");
        String openid = jsonObject.getString("openid");
        //3.刷新access_token(如果需要)
        //4.通过token获取用户信息
        String getUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";
        getUserInfoUrl = getUserInfoUrl.replace("ACCESS_TOKEN", accessToken).replace("OPENID", openid);
        String userInfoJsonStr = Util.sendGet(getUserInfoUrl);
        System.out.println(userInfoJsonStr);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }
}

17 微信公众号开发框架

前面的开发都是原生的写法,github上有很多现成的公众号开发框架。

比如这个基于springboot的公众号开发框架:

仓库:https://github.com/binarywang/weixin-java-mp-demo

文档:https://github.com/Wechat-Group/WxJava/wiki/%E5%85%AC%E4%BC%97%E5%8F%B7%E5%BC%80%E5%8F%91%E6%96%87%E6%A1%A3

最后多说一句只有把原生的基础打好了,才能更好的理解和使用框架,所以建议先学原生的公众号开发,再上手框架。

微信关注

WeChat

 

本站为非盈利性站点,所有资源、文章等仅供学习参考,并不贩卖软件且不存在任何商业目的及用途,如果您访问和下载某文件,表示您同意只将此文件用于参考、学习而非其他用途。
本站所发布的一切软件资源、文章内容、页面内容可能整理来自于互联网,在此郑重声明本站仅限用于学习和研究目的;并告知用户不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
如果本站相关内容有侵犯到您的合法权益,请仔细阅读本站公布的投诉指引页相关内容联系我,依法依规进行处理!
作者:理想
链接:https://www.imyjs.cn/archives/476
THE END
二维码
微信公众号开发-Java版学习笔记
写在前面 为什么学习微信公众号开发 在我使用Java……
<<上一篇
下一篇>>
文章目录
关闭
目 录