Java实现微信公众号开发
=============
基于微信公众号的功能开发,是很多企业对外宣传推广的必要手段,企业的公众号就像是企业的一个微网站一样,是企业对外的重要窗口,如何用 Java 实现微信公众号的接口开发,V 哥这里将详细介绍。
一、概述
1.微信公众平台开发概述
微信公众平台是运营者通过公众号为微信用户提供资讯和服务的平台,而公众平台开放接口则是提供服务的基础,开发者在公众平台网站中创建公众号、获取接口权限后,可以通过阅读本接口文档来帮助开发。
为了识别用户,每个用户针对每个公众号会产生一个安全的OpenID,如果需要在多公众号、移动应用之间做用户共通,则需前往微信开放平台,将这些公众号和应用绑定到一个开放平台账号下,绑定后,一个用户虽然对多个公众号和应用有多个不同的OpenID,但他对所有这些同一开放平台账号下的公众号和应用,只有一个UnionID,可以在用户管理 – 获取用户基本信息(UnionID机制)文档了解详情。
请开发者注意:
- 微信公众平台开发是指为微信公众号进行业务开发,为移动应用、PC端网站、公众号第三方平台(为各行各业公众号运营者提供服务)的开发,请前往微信开放平台接入。
- 在申请到认证公众号之前,你可以先通过测试号申请系统,快速申请一个接口测试号,立即开始接口测试开发。
- 在开发过程中,可以使用接口调试工具来在线调试某些接口。
- 每个接口都有每日接口调用频次限制,可以在公众平台官网 – 开发者中心处查看具体频次。
- 在开发出现问题时,可以通过接口调用的返回码,以及报警排查指引(在公众平台官网 – 开发者中心处可以设置接口报警),来发现和解决问题。
- 公众平台以access_token为接口调用凭据,来调用接口,所有接口的调用需要先获取access_token,access_token在2小时内有效,过期需要重新获取,但1天内获取次数有限,开发者需自行存储,详见获取接口调用凭据(access_token)文档。
公众号主要通过公众号消息会话和公众号内网页来为用户提供服务的,下面分别介绍这两种情况:
- 公众号消息会话
公众号是以微信用户的一个联系人形式存在的,消息会话是公众号与用户交互的基础。目前公众号内主要有这样几类消息服务的类型,分别用于不同的场景。
1)群发消息:公众号可以以一定频次(订阅号为每天1次,服务号为每月4次),向用户群发消息,包括文字消息、图文消息、图片、视频、语音等。
2)被动回复消息:在用户给公众号发消息后,微信服务器会将消息发到开发者预先在开发者中心设置的服务器地址(开发者需要进行消息真实性验证),公众号可以在5秒内做出回复,可以回复一个消息,也可以回复命令告诉微信服务器这条消息暂不回复。被动回复消息可以设置加密(在公众平台官网的开发者中心处设置,设置后,按照消息加解密文档来进行处理。其他3种消息的调用因为是 API 调用而不是对请求的返回,所以不需要加解密)。
3)客服消息:用户在公众号内发消息/触发特定行为后,公众号可以给用户发消息。
4)模板消息:在需要对用户发送服务通知(如刷卡提醒、服务预约成功通知等)时,公众号可以用特定内容模板,主动向用户发送消息。
- 公众号内网页
许多复杂的业务场景,需要通过网页形式来提供服务,这时需要用到:
1)网页授权获取用户基本信息:通过该接口,可以获取用户的基本信息(获取用户的 OpenID 是无需用户同意的,获取用户的基本信息则需用户同意)
2)微信JS-SDK:是开发者在网页上通过 JavaScript 代码使用微信原生功能的工具包,开发者可以使用它在网页上录制和播放微信语音、监听微信分享、上传手机本地图片、拍照等许多能力。
2.开启公众号开发者模式
公众平台技术文档的目的是为了简明扼要的说明接口的使用,语句难免苦涩难懂,甚至对于不同的读者,有语意歧义。万事皆是入门难,对于刚入门的开发者讲,更是难上加难。
为了降低门槛,弥补不足,我们编写了《开发者指引》来讲解微信开放平台的基础常见功能,旨在帮助大家入门微信开放平台的开发者模式。
已熟知接口使用或有一定公众平台开发经验的开发者,请直接跳过本文。这篇文章不会给你带来厉害的编码技巧亦或接口的深层次讲解。
3.申请公众号
邮箱激活后,选择公众号类型。不同的公众号拥有不同的能力,详情请见wiki:公众号接口权限说明,当然,服务号、企业号需要一定的证件和相关资料填写,如果证件一时不能准备好,没关系,公众号其实已注册,下次可以根据此邮箱&密码登录再选择。
4.开发者基本配置
1) 公众平台官网登录之后,找到“基本配置”菜单栏
2) 填写配置 url填写:http://外网IP/wx 。外网 IP 请到腾讯云购买成功处查询。 http的端口号固定使用80,不可填写其他。 Token:自主设置,这个 token 与公众平台 wiki 中常提的access_token不是一回事。这个 token 只用于验证开发者服务器。
二、接入准备
1.搭建内网穿透环境
微信需要访问我们项目的web接口,在开发阶段可以把项目部署在公网能访问的云服务器上,也可以使用内网穿透工具来访问我们自己电脑上运行的测试接口。
这里我们使用natapp作为内网穿透工具。
- 首先在本站注册账号:natapp.cn/register
- 登录后,点击左边 购买隧道,免费/付费均可
- 根据需要选择隧道协议,这里以web演示,购买隧道
- 本机建立web服务,使用Ideal启动springboot项目,确保 http://127.0.0.1 可以访问,如localhost:8080
- 在 natapp.cn 根据本机操作系统下载对应的客户端
- 下载之后,解压至任意目录,得到natapp.exe (linux下无需解压,直接 wget)
- 取得authtoken 在网站后台,我的隧道处,可以看到刚才购买的隧道
点击复制,即可得到 authtoken 这个authtoken便是您的隧道登录凭证.如这里得到的authtoken为9ab6b9040a624f40
- 运行natapp .
windows下,直接双击natapp.exe 即可 。在Linux/Mac 下 需要先给执行权限
chmod a+x natapp
然后再运行:
windows ,点击开始->运行->命令行提示符 后进入 natapp.exe的目录运行
natapp -authtoken=9ab6b9040a624f40
linux ,同样给予可执行权限之后,运行
./natapp -authtoken=9ab6b9040a624f40
注意参数输入正确性,不要有多余的空格等!
- 运行成功,都可以得到如下界面:
2.验证服务器url
开发者提交信息后,微信服务器将发送 GET 请求到填写的服务器地址 URL 上,GET请求携带参数如下表所示:
| 参数 | 描述 |
| — | — |
| signature | 微信加密签名,signature结合了开发者填写的 token 参数和请求中的 timestamp 参数、nonce参数。 |
| timestamp | 时间戳 |
| nonce | 随机数 |
| echostr | 随机字符串 |
开发者通过检验 signature 对请求进行校验(下面有校验方式)。若确认此次 GET 请求来自微信服务器,请原样返回 echostr 参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:
1)将token、timestamp、nonce三个参数进行字典序排序 2)将三个参数字符串拼接成一个字符串进行sha1加密 3)开发者获得加密后的字符串可与 signature 对比,标识该请求来源于微信
检验 signature 的Java示例代码:
/**
* 消息验证
*
* @param signature
* @param timestamp
* @param nonce
* @param echostr
* @return
*/
@GetMapping("/")
public String check(String signature,
String timestamp,
String nonce,
String echostr) {
// 1)将token、timestamp、nonce三个参数进行字典序排序
String token = "qfjava";
List<String> list = Arrays.asList(token, timestamp, nonce);
//排序
Collections.sort(list);
// 2)将三个参数字符串拼接成一个字符串进行sha1加密
StringBuilder stringBuilder = new StringBuilder();
for (String s : list) {
stringBuilder.append(s);
}
//加密
try {
MessageDigest instance = MessageDigest.getInstance("sha1");
//使用sha1进行加密,获得byte数组
byte[] digest = instance.digest(stringBuilder.toString().getBytes());
StringBuilder sum = new StringBuilder();
for (byte b : digest) {
sum.append(Integer.toHexString((b >> 4) & 15));
sum.append(Integer.toHexString(b & 15));
}
// 3)开发者获得加密后的字符串可与 signature 对比,标识该请求来源于微信
if (!StringUtils.isEmpty(signature) && signature.equals(sum.toString())) {
return echostr;
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
三、消息处理
1.接收用户消息
当普通微信用户向公众账号发消息时,微信服务器将 POST 消息的 XML 数据包到开发者填写的 URL 上。
请注意:
- 关于重试的消息排重,推荐使用 msgid 排重。
- 微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。假如服务器无法保证在五秒内处理并回复,可以直接回复空串,微信服务器不会对此作任何处理,并且不会发起重试。详情请见“发送消息 – 被动回复消息”。
- 如果开发者需要对用户消息在5秒内立即做出回应,即使用“发送消息 – 被动回复消息”接口向用户被动回复消息时,可以在
公众平台官网的开发者中心处设置消息加密。开启加密后,用户发来的消息和开发者回复的消息都会被加密(但开发者通过客服接口等 API 调用形式向用户发送消息,则不受影响)。关于消息加解密的详细说明,请见“发送消息 – 被动回复消息加解密说明”。 各消息类型的推送 XML 数据包结构如下:
@PostMapping("/")
public String receiveMessage(HttpServletRequest request) throws IOException {
ServletInputStream inputStream = request.getInputStream();
Map<String,String> map = new HashMap<>();
SAXReader reader = new SAXReader();
try {
//读取request输入流,获得Document对象
Document document = reader.read(inputStream);
//获得root节点
Element root = document.getRootElement();
//获取所有的子节点
List<Element> elements = root.elements();
for (Element element : elements) {
map.put(element.getName(),element.getStringValue());
}
} catch (DocumentException e) {
e.printStackTrace();
}
System.out.println(map);
return message;
}
2.回复用户消息
当用户发送消息给公众号时(或某些特定的用户操作引发的事件推送时),会产生一个 POST 请求,开发者可以在响应包(Get)中返回特定 XML 结构,来对该消息进行响应(现支持回复文本、图片、图文、语音、视频、音乐)。严格来说,发送被动响应消息其实并不是一种接口,而是对微信服务器发过来消息的一次回复。
微信服务器在将用户的消息发给公众号的开发者服务器地址(开发者中心处配置)后,微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次,如果在调试中,发现用户无法收到响应的消息,可以检查是否消息处理超时。关于重试的消息排重,有 msgid 的消息推荐使用 msgid 排重。事件类型消息推荐使用FromUserName + CreateTime 排重。
@PostMapping("/")
public String receiveMessage(HttpServletRequest request) throws IOException {
ServletInputStream inputStream = request.getInputStream();
Map<String,String> map = new HashMap<>();
SAXReader reader = new SAXReader();
try {
//读取request输入流,获得Document对象
Document document = reader.read(inputStream);
//获得root节点
Element root = document.getRootElement();
//获取所有的子节点
List<Element> elements = root.elements();
for (Element element : elements) {
map.put(element.getName(),element.getStringValue());
}
} catch (DocumentException e) {
e.printStackTrace();
}
System.out.println(map);
//回复消息
String message = "<xml><ToUserName><![CDATA[ozsXm5rMF8KMGrwh2Ulzkj9jSzMA]]></ToUserName><FromUserName><![CDATA[gh_e2226aee29e5]]></FromUserName><CreateTime>1669797622</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[你好]]></Content></xml>";
return message;
}
3.封装消息
将回复的消息XML内容封装为Java对象进行操作,更有利于项目开发。
package com.qf.wx.common.message;
import com.thoughtworks.xstream.annotations.XStreamAlias;
/**
* @author Thor
* @公众号 Java架构栈
*/
@XStreamAlias("xml")
public class TextMessage implements BaseMessage {
@XStreamAlias("ToUserName")
private String toUserName;
@XStreamAlias("FromUserName")
private String fromUserName;
@XStreamAlias("CreateTime")
private long createTime;
@XStreamAlias("MsgType")
private String msgType;
@XStreamAlias("Content")
private String content;
public String getToUserName() {
return toUserName;
}
public void setToUserName(String toUserName) {
this.toUserName = toUserName;
}
public String getFromUserName() {
return fromUserName;
}
public void setFromUserName(String fromUserName) {
this.fromUserName = fromUserName;
}
public long getCreateTime() {
return createTime;
}
public void setCreateTime(long createTime) {
this.createTime = createTime;
}
public String getMsgType() {
return msgType;
}
public void setMsgType(String msgType) {
this.msgType = msgType;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public String toString() {
return "TextMessage{" +
"toUserName='" + toUserName + '\'' +
", fromUserName='" + fromUserName + '\'' +
", createTime=" + createTime +
", msgType='" + msgType + '\'' +
", content='" + content + '\'' +
'}';
}
}
4.消息处理案例-接入第三方服务接口
- 引入依赖
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.2.3</version>
<classifier>jdk15</classifier>
</dependency>
- 编写接口工具类
package com.qf.wx.util;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
/**
* @author Thor
* @公众号 Java架构栈
*/
public class WordUtil {
//随机笑话
public static final String WORD_URL = "http://apis.juhe.cn/tyfy/query?key=%s";
//申请接口的请求key
// TODO: 您需要改为自己的请求key
public static final String KEY = "d45d162569497be89293291fc543d184";
public static String getWords(String word) {
//发送http请求的url
String url = String.format(WORD_URL, KEY);
final String response = doPost(url,"word="+word);
System.out.println("接口返回:" + response);
try {
JSONObject jsonObject = JSONObject.fromObject(response);
int error_code = jsonObject.getInt("error_code");
if (error_code == 0) {
System.out.println("调用接口成功");
JSONObject result = jsonObject.getJSONObject("result");
JSONArray words = result.getJSONArray("words");
StringBuilder stringBuilder = new StringBuilder();
words.stream().forEach(w->stringBuilder.append(w+" "));
System.out.println(stringBuilder);
return stringBuilder.toString();
} else {
System.out.println("调用接口失败:" + jsonObject.getString("reason"));
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* get方式的http请求
*
* @param httpUrl 请求地址
* @return 返回结果
*/
public static String doGet(String httpUrl) {
HttpURLConnection connection = null;
InputStream inputStream = null;
BufferedReader bufferedReader = null;
String result = null;// 返回结果字符串
try {
// 创建远程url连接对象
URL url = new URL(httpUrl);
// 通过远程url连接对象打开一个连接,强转成httpURLConnection类
connection = (HttpURLConnection) url.openConnection();
// 设置连接方式:get
connection.setRequestMethod("GET");
// 设置连接主机服务器的超时时间:15000毫秒
connection.setConnectTimeout(15000);
// 设置读取远程返回的数据时间:60000毫秒
connection.setReadTimeout(60000);
// 发送请求
connection.connect();
// 通过connection连接,获取输入流
if (connection.getResponseCode() == 200) {
inputStream = connection.getInputStream();
// 封装输入流,并指定字符集
bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
// 存放数据
StringBuilder sbf = new StringBuilder();
String temp;
while ((temp = bufferedReader.readLine()) != null) {
sbf.append(temp);
sbf.append(System.getProperty("line.separator"));
}
result = sbf.toString();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (null != bufferedReader) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (connection != null) {
connection.disconnect();// 关闭远程连接
}
}
return result;
}
/**
* post方式的http请求
*
* @param httpUrl 请求地址
* @param param 请求参数
* @return 返回结果
*/
public static String doPost(String httpUrl, String param) {
HttpURLConnection connection = null;
InputStream inputStream = null;
OutputStream outputStream = null;
BufferedReader bufferedReader = null;
String result = null;
try {
java.net.URL url = new URL(httpUrl);
// 通过远程url连接对象打开连接
connection = (HttpURLConnection) url.openConnection();
// 设置连接请求方式
connection.setRequestMethod("POST");
// 设置连接主机服务器超时时间:15000毫秒
connection.setConnectTimeout(15000);
// 设置读取主机服务器返回数据超时时间:60000毫秒
connection.setReadTimeout(60000);
// 默认值为:false,当向远程服务器传送数据/写数据时,需要设置为true
connection.setDoOutput(true);
// 设置传入参数的格式:请求参数应该是 name1=value1&name2=value2 的形式。
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
// 通过连接对象获取一个输出流
outputStream = connection.getOutputStream();
// 通过输出流对象将参数写出去/传输出去,它是通过字节数组写出的
outputStream.write(param.getBytes());
// 通过连接对象获取一个输入流,向远程读取
if (connection.getResponseCode() == 200) {
inputStream = connection.getInputStream();
// 对输入流对象进行包装:charset根据工作项目组的要求来设置
bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
StringBuilder sbf = new StringBuilder();
String temp;
// 循环遍历一行一行读取数据
while ((temp = bufferedReader.readLine()) != null) {
sbf.append(temp);
sbf.append(System.getProperty("line.separator"));
}
result = sbf.toString();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (null != bufferedReader) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != outputStream) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (connection != null) {
connection.disconnect();
}
}
return result;
}
}
- 处理消息结果
/**
* 获得同义词
* @param map
* @return xml格式的字符串
*/
private String getReplyMessageByWord(Map<String, String> map) {
TextMessage textMessage = new TextMessage();
textMessage.setToUserName(map.get("FromUserName"));
textMessage.setFromUserName(map.get("ToUserName"));
textMessage.setMsgType("text");
String content = WordUtil.getWords(map.get("Content"));
textMessage.setContent(content);
textMessage.setCreateTime(System.currentTimeMillis()/1000);
//XStream将Java对象转换成xml字符串
XStream xStream = new XStream();
xStream.processAnnotations(TextMessage.class);
String xml = xStream.toXML(textMessage);
return xml;
}
5.回复图文消息
回复带图片和文字的消息,需要使用如下参数生成的xml:
<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 | 是 | 点击图文消息跳转链接 |
相应的代码如下:
package com.qf.wx.message;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import java.util.List;
/**
* @author Thor
* @公众号 Java架构栈
*/
@XStreamAlias("xml")
public class NewsMessage {
@XStreamAlias("ToUserName")
private String toUserName;
@XStreamAlias("FromUserName")
private String fromUserName;
@XStreamAlias("CreateTime")
private long createTime;
@XStreamAlias("MsgType")
private String msgType;
@XStreamAlias("ArticleCount")
private int articleCount;
@XStreamAlias("Articles")
private List<Article> articles;
public String getToUserName() {
return toUserName;
}
public void setToUserName(String toUserName) {
this.toUserName = toUserName;
}
public String getFromUserName() {
return fromUserName;
}
public void setFromUserName(String fromUserName) {
this.fromUserName = fromUserName;
}
public long getCreateTime() {
return createTime;
}
public void setCreateTime(long createTime) {
this.createTime = createTime;
}
public String getMsgType() {
return msgType;
}
public void setMsgType(String msgType) {
this.msgType = msgType;
}
public int getArticleCount() {
return articleCount;
}
public void setArticleCount(int articleCount) {
this.articleCount = articleCount;
}
public List<Article> getArticles() {
return articles;
}
public void setArticles(List<Article> articles) {
this.articles = articles;
}
}
生成图文消息:
private String getReplyNewsMessage(Map<String, String> map) {
NewsMessage newsMessage = new NewsMessage();
newsMessage.setToUserName(map.get("FromUserName"));
newsMessage.setFromUserName(map.get("ToUserName"));
newsMessage.setMsgType("news");
newsMessage.setCreateTime(System.currentTimeMillis()/1000);
newsMessage.setArticleCount(1);
List<Article> articles = new ArrayList<>();
Article article = new Article();
article.setTitle("千锋Java微信公众号开发教程");
article.setDescription("来自于千锋java,详细的微信公众号开发教程");
article.setUrl("http://www.qfedu.com");
article.setPicUrl("http://mmbiz.qpic.cn/mmbiz_jpg/djXAWeX3AxUBTA5ZXIrbtQgUdt2DjhZOkd7aje7pp9yfOUU9NwKPyQJzeduaO6iaCa3NSyTW0GJWONp5IRVnYvg/0");
articles.add(article);
newsMessage.setArticles(articles);
//XStream将Java对象转换成xml字符串
XStream xStream = new XStream();
xStream.processAnnotations(NewsMessage.class);
String xml = xStream.toXML(newsMessage);
return xml;
}
四、功能开发
1.获得Access_token
access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。
公众平台的 API 调用所需的access_token的使用及生成方式说明:
1)建议公众号开发者使用中控服务器统一获取和刷新access_token,其他业务逻辑服务器所使用的access_token均来自于该中控服务器,不应该各自去刷新,否则容易造成冲突,导致access_token覆盖而影响业务;
2)目前access_token的有效期通过返回的expires_in来传达,目前是7200秒之内的值。中控服务器需要根据这个有效时间提前去刷新新access_token。在刷新过程中,中控服务器可对外继续输出的老access_token,此时公众平台后台会保证在5分钟内,新老access_token都可用,这保证了第三方业务的平滑过渡;
3)access_token的有效时间可能会在未来有调整,所以中控服务器不仅需要内部定时主动刷新,还需要提供被动刷新access_token的接口,这样便于业务服务器在 API 调用获知access_token已超时的情况下,可以触发access_token的刷新流程。
- 创建HttpUtil
package com.qf.wx.util;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
/**
* @author Thor
* @公众号 Java架构栈
*/
public class HttpUtil {
/**
* get方式的http请求
*
* @param httpUrl 请求地址
* @return 返回结果
*/
public static String doGet(String httpUrl) {
HttpURLConnection connection = null;
InputStream inputStream = null;
BufferedReader bufferedReader = null;
String result = null;// 返回结果字符串
try {
// 创建远程url连接对象
URL url = new URL(httpUrl);
// 通过远程url连接对象打开一个连接,强转成httpURLConnection类
connection = (HttpURLConnection) url.openConnection();
// 设置连接方式:get
connection.setRequestMethod("GET");
// 设置连接主机服务器的超时时间:15000毫秒
connection.setConnectTimeout(15000);
// 设置读取远程返回的数据时间:60000毫秒
connection.setReadTimeout(60000);
// 发送请求
connection.connect();
// 通过connection连接,获取输入流
if (connection.getResponseCode() == 200) {
inputStream = connection.getInputStream();
// 封装输入流,并指定字符集
bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
// 存放数据
StringBuilder sbf = new StringBuilder();
String temp;
while ((temp = bufferedReader.readLine()) != null) {
sbf.append(temp);
sbf.append(System.getProperty("line.separator"));
}
result = sbf.toString();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (null != bufferedReader) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (connection != null) {
connection.disconnect();// 关闭远程连接
}
}
return result;
}
/**
* post方式的http请求
*
* @param httpUrl 请求地址
* @param param 请求参数
* @return 返回结果
*/
public static String doPost(String httpUrl, String param) {
HttpURLConnection connection = null;
InputStream inputStream = null;
OutputStream outputStream = null;
BufferedReader bufferedReader = null;
String result = null;
try {
java.net.URL url = new URL(httpUrl);
// 通过远程url连接对象打开连接
connection = (HttpURLConnection) url.openConnection();
// 设置连接请求方式
connection.setRequestMethod("POST");
// 设置连接主机服务器超时时间:15000毫秒
connection.setConnectTimeout(15000);
// 设置读取主机服务器返回数据超时时间:60000毫秒
connection.setReadTimeout(60000);
// 默认值为:false,当向远程服务器传送数据/写数据时,需要设置为true
connection.setDoOutput(true);
// 设置传入参数的格式:请求参数应该是 name1=value1&name2=value2 的形式。
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
// 通过连接对象获取一个输出流
outputStream = connection.getOutputStream();
// 通过输出流对象将参数写出去/传输出去,它是通过字节数组写出的
outputStream.write(param.getBytes());
// 通过连接对象获取一个输入流,向远程读取
if (connection.getResponseCode() == 200) {
inputStream = connection.getInputStream();
// 对输入流对象进行包装:charset根据工作项目组的要求来设置
bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
StringBuilder sbf = new StringBuilder();
String temp;
// 循环遍历一行一行读取数据
while ((temp = bufferedReader.readLine()) != null) {
sbf.append(temp);
sbf.append(System.getProperty("line.separator"));
}
result = sbf.toString();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (null != bufferedReader) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != outputStream) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != inputStream) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (connection != null) {
connection.disconnect();
}
}
return result;
}
}
- 创建AccessToken类
package com.qf.wx.token;
/**
* @author Thor
* @公众号 Java架构栈
*/
public class AccessToken {
private String token;
private long expireTime;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public long getExpireTime() {
return expireTime;
}
public void setExpireTime(long expireIn) {
this.expireTime = System.currentTimeMillis() + expireIn * 1000;
}
/**
* 判断是否超时
* @return
*/
public boolean isExpired(){
return System.currentTimeMillis()>this.expireTime;
}
}
- 创建TokenUtil
package com.qf.wx.token;
import com.qf.wx.util.HttpUtil;
import net.sf.json.JSONObject;
/**
* @author Thor
* @公众号 Java架构栈
*/
public class TokenUtil {
private static final String APP_ID = "wx9083c3c195116d94";
private static final String APP_SECRET = "fdf3a4e12343d4a2b871e8b2aa467d7a";
private static AccessToken accessToken = new AccessToken();
public static void main(String[] args) {
//{"access_token":"63_8R2EcPuM3dz_D81Q2FBiSfgrlwokafQloAU33iFhHIbjabRFtC_thRqk7VOkMbarQ8lA9yyq2pgwh4pc6P-5qQutc6WWMLwFafIR6ZaLkB299OJU78npFt--I0ACXCiACAHCH","expires_in":7200}
System.out.println(getAccessToken());
System.out.println(getAccessToken());
}
private static void getToken(){
String url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
APP_ID,
APP_SECRET);
String result = HttpUtil.doGet(url);
JSONObject jsonObject = JSONObject.fromObject(result);
String token = jsonObject.getString("acces s_token");
long expiresIn = jsonObject.getLong("expires_in");
accessToken.setToken(token);
accessToken.setExpireTime(expiresIn);
}
/**
* 获取AccessToken
* @return
*/
public static String getAccessToken(){
if(accessToken == null || accessToken.isExpired()){
getToken();
}
return accessToken.getToken();
}
}
2.自定义菜单
自定义菜单能够帮助公众号丰富界面,让用户更好更快地理解公众号的功能。开启自定义菜单后,公众号界面如图所示:
请注意:
- 自定义菜单最多包括3个一级菜单,每个一级菜单最多包含5个二级菜单。
- 一级菜单最多4个汉字,二级菜单最多8个汉字,多出来的部分将会以“…”代替。
- 创建自定义菜单后,菜单的刷新策略是,在用户进入公众号会话页或公众号 profile 页时,如果发现上一次拉取菜单的请求在5分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。
自定义菜单接口可实现多种类型按钮,如下:
- click:点击推事件用户点击 click 类型按钮后,微信服务器会通过消息接口推送消息类型为 event 的结构给开发者(参考消息接口指南),并且带上按钮中开发者填写的 key 值,开发者可以通过自定义的 key 值与用户进行交互;
- view:跳转 URL 用户点击 view 类型按钮后,微信客户端将会打开开发者在按钮中填写的网页URL,可与网页授权获取用户基本信息接口结合,获得用户基本信息。
- scancode_push:扫码推事件用户点击按钮后,微信客户端将调起扫一扫工具,完成扫码操作后显示扫描结果(如果是URL,将进入URL),且会将扫码的结果传给开发者,开发者可以下发消息。
- scancode_waitmsg:扫码推事件且弹出“消息接收中”提示框用户点击按钮后,微信客户端将调起扫一扫工具,完成扫码操作后,将扫码的结果传给开发者,同时收起扫一扫工具,然后弹出“消息接收中”提示框,随后可能会收到开发者下发的消息。
- pic_sysphoto:弹出系统拍照发图用户点击按钮后,微信客户端将调起系统相机,完成拍照操作后,会将拍摄的相片发送给开发者,并推送事件给开发者,同时收起系统相机,随后可能会收到开发者下发的消息。
- pic_photo_or_album:弹出拍照或者相册发图用户点击按钮后,微信客户端将弹出选择器供用户选择“拍照”或者“从手机相册选择”。用户选择后即走其他两种流程。
- pic_weixin:弹出微信相册发图器用户点击按钮后,微信客户端将调起微信相册,完成选择操作后,将选择的相片发送给开发者的服务器,并推送事件给开发者,同时收起相册,随后可能会收到开发者下发的消息。
- location_select:弹出地理位置选择器用户点击按钮后,微信客户端将调起地理位置选择工具,完成选择操作后,将选择的地理位置发送给开发者的服务器,同时收起位置选择工具,随后可能会收到开发者下发的消息。
- media_id:下发消息(除文本消息)用户点击media_id类型按钮后,微信服务器会将开发者填写的永久素材 id 对应的素材下发给用户,永久素材类型可以是图片、音频、视频 、图文消息。请注意:永久素材 id 必须是在“素材管理/新增永久素材”接口上传后获得的合法id。
- view_limited:跳转图文消息 URL 用户点击view_limited类型按钮后,微信客户端将打开开发者在按钮中填写的永久素材 id 对应的图文消息URL,永久素材类型只支持图文消息。请注意:永久素材 id 必须是在“素材管理/新增永久素材”接口上传后获得的合法id。
- article_id:用户点击 article_id 类型按钮后,微信客户端将会以卡片形式,下发开发者在按钮中填写的图文消息
- article_view_limited:类似 view_limited,但不使用 media_id 而使用 article_id
注意: 草稿接口灰度完成后,将不再支持图文信息类型的 media_id 和 view_limited,有需要的,请使用 article_id 和 article_view_limited 代替
请注意,3到8的所有事件,仅支持微信iPhone5.4.1以上版本,和Android5.4以上版本的微信用户,旧版本微信用户点击后将没有回应,开发者也不能正常接收到事件推送。9~12,是专���给第三方平台旗下未微信认证(具体而言,是资质认证未通过)的订阅号准备的事件类型,它们是没有事件推送的,能力相对受限,其他类型的公众号不必使用。
接口调用请求说明
http请求方式:POST(请使用 https 协议) api.weixin.qq.com/cgi-bin/men…
click和 view 的请求示例
{
"button":[
{
"type":"click",
"name":"今日歌曲",
"key":"V1001_TODAY_MUSIC"
},
{
"name":"菜单",
"sub_button":[
{
"type":"view",
"name":"搜索",
"url":"http://www.soso.com/"
},
{
"type":"miniprogram",
"name":"wxa",
"url":"http://mp.weixin.qq.com",
"appid":"wx286b93c14bbf93aa",
"pagepath":"pages/lunar/index"
},
{
"type":"click",
"name":"赞一下我们",
"key":"V1001_GOOD"
}]
}]
}
其他新增按钮类型的请求示例
{
"button": [
{
"name": "扫码",
"sub_button": [
{
"type": "scancode_waitmsg",
"name": "扫码带提示",
"key": "rselfmenu_0_0",
"sub_button": [ ]
},
{
"type": "scancode_push",
"name": "扫码推事件",
"key": "rselfmenu_0_1",
"sub_button": [ ]
}
]
},
{
"name": "发图",
"sub_button": [
{
"type": "pic_sysphoto",
"name": "系统拍照发图",
"key": "rselfmenu_1_0",
"sub_button": [ ]
},
{
"type": "pic_photo_or_album",
"name": "拍照或者相册发图",
"key": "rselfmenu_1_1",
"sub_button": [ ]
},
{
"type": "pic_weixin",
"name": "微信相册发图",
"key": "rselfmenu_1_2",
"sub_button": [ ]
}
]
},
{
"name": "发送位置",
"type": "location_select",
"key": "rselfmenu_2_0"
},
{
"type": "media_id",
"name": "图片",
"media_id": "MEDIA_ID1"
},
{
"type": "view_limited",
"name": "图文消息",
"media_id": "MEDIA_ID2"
},
{
"type": "article_id",
"name": "发布后的图文消息",
"article_id": "ARTICLE_ID1"
},
{
"type": "article_view_limited",
"name": "发布后的图文消息",
"article_id": "ARTICLE_ID2"
}
]
}
参数说明
| 参数 | 是否必须 | 说明 |
| — | — | — |
| 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类型必须 | 小程序的页面路径 |
| article_id | article_id类型和article_view_limited类型必须 | 发布后获得的合法 article_id |
返回结果
正确时的返回 JSON 数据包如下:
{"errcode":0,"errmsg":"ok"}
错误时的返回 JSON 数据包如下(示例为无效菜单名长度):
{"errcode":40018,"errmsg":"invalid button name size"}
- 封装菜单类
package com.qf.wx.button;
/**
* @author Thor
* @公众号 Java架构栈
*/
public class PhotoOrAlbumButton extends AbstractButton{
public PhotoOrAlbumButton(String name,String key) {
super(name);
this.type = "pic_photo_or_album";
this.key = key;
}
private String type;
private String key;
public String getType() {
return type;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
- 实现自定义菜单
package com.qf.wx.button;
import com.qf.wx.token.TokenUtil;
import com.qf.wx.util.HttpUtil;
import net.sf.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
/**
* @author Thor
* @公众号 Java架构栈
*/
public class TestButton {
public static void main(String[] args) {
//创建一级菜单
Button button = new Button();
List<AbstractButton> buttons = new ArrayList<>();
//一级菜单中的第一个按钮
ClickButton clickButton = new ClickButton("千锋");
clickButton.setKey("1");
//一级菜单中的第二个按钮
ViewButton viewButton = new ViewButton("baidu","http://www.baidu.com");
//一级菜单中的第三个按钮(二级菜单)
SubButton subButton = new SubButton("更多");
List<AbstractButton> subButtons = new ArrayList<>();
//二级菜单的第一个按钮
subButtons.add(new ViewButton("千锋","http://www.qfedu.cn"));
//二级菜单的第二个按钮
subButtons.add(new PhotoOrAlbumButton("上传图片","2"));
subButton.setSub_button(subButtons);
//把一级菜单中的三个按钮添加进集合
buttons.add(clickButton);
buttons.add(viewButton);
buttons.add(subButton);
//把集合添加到一级菜单中
button.setButton(buttons);
//转换成json字符串
JSONObject jsonObject = JSONObject.fromObject(button);
String json = jsonObject.toString();
String url = String.format("https://api.weixin.qq.com/cgi-bin/menu/create?access_token=%s", TokenUtil.getAccessToken());
//发送请求
String result = HttpUtil.doPostByButton(url, json);
System.out.println(result);
}
}
- 处理自定义菜单事件
/**
* 处理事件推送
* @param map
* @return
*/
private String handleEvent(Map<String, String> map) {
String event = map.get("Event");
switch (event){
case "CLICK":
if("1".equals(map.get("EventKey"))){
TextMessage textMessage = new TextMessage();
textMessage.setToUserName(map.get("FromUserName"));
textMessage.setFromUserName(map.get("ToUserName"));
textMessage.setMsgType("text");
textMessage.setContent("你点击了event key是1的按钮");
textMessage.setCreateTime(System.currentTimeMillis()/1000);
//XStream将Java对象转换成xml字符串
XStream xStream = new XStream();
xStream.processAnnotations(TextMessage.class);
String xml = xStream.toXML(textMessage);
return xml;
}
break;
case "VIEW":
System.out.println("view");
break;
default: break;
}
return null;
}
3.文字识别功能
使用百度AI服务,实现图片的文字识别功能
- 引入依赖
<dependency>
<groupId>com.baidu.aip</groupId>
<artifactId>java-sdk</artifactId>
<version>4.16.12</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
</exclusions>
</dependency>
- 实现图片识别
public static final String APP_ID = "28840928";
public static final String API_KEY = "8GFDlXxjO9cX5PvZ1rWVQTeS";
public static final String SECRET_KEY = "qXHkpRbEFvUreLDyFu5MB8nx6xgGy3d7";
private String handleImage(Map<String, String> map) {
// 初始化一个AipOcr
AipOcr client = new AipOcr(APP_ID, API_KEY, SECRET_KEY);
// 网络图片文字识别, 图片参数为远程url图片
String url = map.get("PicUrl");
JSONObject res = client.webImageUrl(url, new HashMap<String,String>());
System.out.println(res.toString(2));
TextMessage textMessage = new TextMessage();
textMessage.setToUserName(map.get("FromUserName"));
textMessage.setFromUserName(map.get("ToUserName"));
textMessage.setMsgType("text");
JSONArray wordsResult = res.getJSONArray("words_result");
StringBuilder stringBuilder = new StringBuilder();
Iterator<Object> iterator = wordsResult.iterator();
while(iterator.hasNext()){
JSONObject next = (JSONObject) iterator.next();
stringBuilder.append(next.getString("words"));
}
textMessage.setContent(stringBuilder.toString());
textMessage.setCreateTime(System.currentTimeMillis()/1000);
//XStream将Java对象转换成xml字符串
XStream xStream = new XStream();
xStream.processAnnotations(TextMessage.class);
String xml = xStream.toXML(textMessage);
return xml;
}
4.模版消息
模板消息仅用于公众号向用户发送重要的服务通知,只能用于符合其要求的服务场景中,如信用卡刷卡通知,商品购买成功通知等。不支持广告等营销类消息以及其它所有可能对用户造成骚扰的消息。
关于使用规则,请注意:
- 所有服务号都可以在功能->添加功能插件处看到申请模板消息功能的入口,但只有认证后的服务号才可以申请模板消息的使用权限并获得该权限;
- 需要选择公众账号服务所处的2个行业,每月可更改1次所选行业;
- 在所选择行业的模板库中选用已有的模板进行调用;
- 每个账号可以同时使用25个模板。
- 当前每个账号的模板消息的日调用上限为10万次,单个模板没有特殊限制。【2014年11月18日将接口调用频率从默认的日1万次提升为日10万次,可在 MP 登录后的开发者中心查看】。当账号粉丝数超过10W/100W/1000W时,模板消息的日调用上限会相应提升,以公众号 MP 后台开发者中心页面中标明的数字为准。
关于接口文档,请注意:
- 模板消息调用时主要需要模板 ID 和模板中各参数的赋值内容;
- 模板中参数内容必须以”.DATA”结尾,否则视为保留字;
- 模板保留符号””。
1)设置行业
设置行业可在微信公众平台后台完成,每月可修改行业1次,帐号仅可使用所属行业中相关的模板,为方便第三方开发者,提供通过接口调用的方式来修改账号所属行业,具体如下:
接口调用请求说明
http请求方式: POST api.weixin.qq.com/cgi-bin/tem…
POST数据说明
POST数据示例如下:
{
"industry_id1":"1",
"industry_id2":"4"
}
参数说明
| 参数 | 是否必须 | 说明 |
| — | — | — |
| access_token | 是 | 接口调用凭证 |
| industry_id1 | 是 | 公众号模板消息所属行业编号,查看官方文档的行业编号 |
| industry_id2 | 是 | 公众号模板消息所属行业编号,查看官方文档的行业编号 |
2)创建模版
在微信公众平台根据行业模版案例创建消息模版。
3)发送模版消息
接口调用请求说明
http请求方式: POST api.weixin.qq.com/cgi-bin/mes…
POST数据说明
POST数据示例如下:
{
"touser":"OPENID",
"template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY",
"url":"http://weixin.qq.com/download",
"miniprogram":{
"appid":"xiaochengxuappid12345",
"pagepath":"index?foo=bar"
},
"client_msg_id":"MSG_000001",
"data":{
"first": {
"value":"恭喜你购买成功!",
"color":"#173177"
},
"keyword1":{
"value":"巧克力",
"color":"#173177"
},
"keyword2": {
"value":"39.8元",
"color":"#173177"
},
"keyword3": {
"value":"2014年9月22日",
"color":"#173177"
},
"remark":{
"value":"欢迎再次购买!",
"color":"#173177"
}
}
}
参数说明
| 参数 | 是否必填 | 说明 |
| — | — | — |
| touser | 是 | 接收者openid |
| template_id | 是 | 模板ID |
| url | 否 | 模板跳转链接(海外帐号没有跳转能力) |
| miniprogram | 否 | 跳小程序所需数据,不需跳小程序可不用传该数据 |
| appid | 是 | 所需跳转到的小程序appid(该小程序 appid 必须与发模板消息的公众号是绑定关联关系,暂不支持小游戏) |
| pagepath | 否 | 所需跳转到小程序的具体页面路径,支持带参数,(示例index?foo=bar),要求该小程序已发布,暂不支持小游戏 |
| data | 是 | 模板数据 |
| color | 否 | 模板内容字体颜色,不填默认为黑色 |
| client_msg_id | 否 | 防重入id。对于同一个openid + client_msg_id, 只发送一条消息,10分钟有效,超过10分钟不保证效果。若无防重入需求,可不填 |
注:url和 miniprogram 都是非必填字段,若都不传则模板无跳转;若都传,会优先跳转至小程序。开发者可根据实际需要选择其中一种跳转方式即可。当用户的微信客户端版本不支持跳小程序时,将会跳转至url。
返回码说明
在调用模板消息接口后,会返回 JSON 数据包。正常时的返回 JSON 数据包示例:
{
"errcode":0,
"errmsg":"ok",
"msgid":200228332
}
使用效果
5.临时素材管理
公众号经常有需要用到一些临时性的多媒体素材的场景,例如在使用接口特别是发送消息时,对多媒体文件、多媒体消息的获取和调用等操作,是通过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 格式
- 引入依赖
<!--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>
- HttpUtil中添加携带素材发送post请求的方法
//httpClient发送携带文件的post请求
public static String doPostByFile(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();
builder.addPart(fileParamName, bin);
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;
}
- 新增临时素材
//上传临时素材
@Test
public void testImage(){
String url = String.format("https://api.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s",
TokenUtil.getAccessToken(),
"image");
String result = HttpUtil.doPostByFile(url, null, "/Users/zeleishi/Documents/code/my-qf-wx-demo/src/main/resources/static/1663580417771.jpeg", "");
System.out.println(result);
}
- 获取临时素材
//获得临时素材
@Test
public void testGetImage(){
String mediaId = "h7mvkpNYOsmyPtPxnvwh7hopFlQ2LLBalyiCXcfGG2p943IqKHrrSTYdIDMbeRID";
String url = String.format("https://api.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s",
TokenUtil.getAccessToken(),
mediaId);
String result = HttpUtil.doGet(url);
System.out.println(result);
}
总结:
微信公众号开发主要涉及服务端开发和前端页面开发。服务端开发主要使用Java等语言,负责处理用户发送的消息,与微信服务器进行交互等。前端页面开发主要使用HTML、CSS、JavaScript等,负责将服务端返回的消息展示给用户。通过以上步骤,可以实现一个简单的微信公众号开发。
原文链接: https://juejin.cn/post/7369211590364020747
文章收集整理于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除,如若转载,请注明出处:http://www.cxyroad.com/17839.html