Java微信公众号开发
我们平常在微信应用上会看到有很多的公众号,但是各自并不一样,公众号也分很多种类型,不过最常见的就是服务号和订阅号了。下面我们来看一下他们的区别: 订阅号和服务号有一个比较明显的区别就是:订阅号都是存放在一个名叫订阅号的文件夹中,点开才能看到所有关注过的订阅号,但是服务号却和好友一样直接就显示在聊天列表中。这个大家打开微信客户端便能看到 以注册订阅号为例,访问注册地址:https://mp.weixin.qq.com/cgi-bin/registermidpage?action=index...\_CN&token= 填写基本信息,验证邮箱 选择账号类型 账号主体类型选择个人,填写信息 填写公众号信息 创建成功后,可以进入公众号的管理界面 主要针对非编程人员及信息发布类公众帐号 使用 。开启该模式后,可以方便地通过界面配置“自定义菜单”和“自动回复的消息”。好处是可视化界面配置,操作简单,快捷,但是功能有限 我们可以给自己的公众号设置关键词回复,收到已关注用户发送的消息,匹配到你好时回复一个你好~~ 主要针对具备开发能力的人使用。开启该模式后,能够使用微信公众平台开放的接口,但是编辑模式的设置会失效,比如“自定义菜单”和“自动回复的消息”功能。通过编程方式可以实现更多复杂的功能,提供个性化服务。 总的来说,编辑模式就是为所有人提供的,如果你的需求仅仅只是最常见的菜单,自动回复等,使用编辑模式已经满足,但是如果你需求的功能比较复杂,比如需要从自己的业务系统数据库中查询数据返回给微信用户,就需要使用到开发模式。 接下来我们介绍开发模式的各种功能使用步骤。出于安全考虑,普通的订阅号权限非常有限,可以调用的接口非常有限,不便于测试 所以,为了帮助开发者快速了解和上手微信公众号开发,熟悉各个接口的调用,微信推出了公众帐号测试号,无需公众帐号、快速申请接口测试号,通过手机微信扫描 二维码 即可获得,利用测试号我们可以体验和测试更多高级功能。测试号申请地址:https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login 我们首先了解下微信与我们的服务器交互的过程: 当我们在微信app上,给公众号发送一条内容的时候,实际会发送到微信的服务器上,此时微信的服务器就会对内容进行封装成某种格式的数据比如xml格式,再转发到我们配置好的某个URL上,所以该URL实际就是我们处理数据的一个请求路径。所以该URL必须是能暴露给外界访问的一个公网地址,不能使用内网地址,生产环境可以申请腾讯云,阿里云服务器等,但是在开发环境中可以暂时利用一些软件来完成内网穿透 在进行和微信公众号服务器交互之前,需要进行接入验证。接入验证涉及的2个关键参数如下: 接入验证的大致流程如下: 开发者通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下: 根据上述验证流程,我们创建一个Springboot应用,编写验证接口 利用内网穿透工具,将我们的本地应用地址映射到公网域名 然后在测试号页面填写回调url和正确的token,即可接入成功。如果token填错,将接入失败 特别注意: 接入成功以后,我们就可以利用微信提供的接口实现各种功能。首先来看一下基本的消息接收和回复,官方文档位置如下 当关注了公众号的用户向公众号发送消息时,微信服务器将POST消息的XML数据包到开发者填写的URL上。所以我们要在Controller中新建一个处理方法 微信会将用户发送的消息信息封装到请求体的xml中,根据消息类型的不同,xml的格式也有所不同: 我们对比一下不同类型的xml数据包中的参数, 接下来,我们可以创建一个封装消息的 实体类 ,把所有可接收到的参数都放进入,其他类型的暂时不演示,所以只在最后加入了文本和图片的参数。 这时候大家可能会有个疑问,为什么字段名称都是大写开头呢?因为微信服务器传过来的xml数据包中的xml元素都是大写开头的,如下所示: 因为xml解析是大小写敏感的,所以为了方便封装,我直接把字段名设置为大写开头。当然,如果还是想要小写开头的字段,也是可以的,我们待会再说处理方式。 实体已经建好之后,我们就可以开始接收微信传过来的xml数据了。 第1步:在handleMessage方法的形参上添加消息实体类型的参数: 第2步:需要配合JAXB的注解来解析xml。在消息实体类上添加以下两个注解: 这2个注解的含义如下: 之前我们为了方便解析,将消息实体类的属性命名成和xml中的节点一样都是大写开头,现在我们优化一下,按照java规范命名属性小写开头,然后利用 然后我们在Controller接收消息的方法中设置断点,关注测试公众号,给它发送一个文本消息"你好",就可以接收到了 官方文档:https://developers.weixin.qq.com/ doc /offiaccount/Message\_Management/Passive\_user\_reply\_message.html 当用户发送消息给公众号时(或某些特定的用户操作引发的 事件 推送时),会产生一个POST请求,开发者可以在响应包(Get)中返回特定XML结构,来对该消息进行响应(现支持回复文本、图片、图文、语音、视频、音乐)。严格来说,发送被动响应消息其实并不是一种接口,而是对微信服务器发过来消息的一次回复。 微信服务器在将用户的消息发给公众号的开发者服务器地址(开发者中心处配置)后,微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次,如果在调试中,发现用户无法收到响应的消息,可以检查是否消息处理超时。关于重试的消息排重,有msgid的消息推荐使用msgid排重。事件类型消息推荐使用FromUserName + CreateTime 排重。 如果开发者希望增强安全性,可以在开发者中心处开启消息加密,这样,用户发给公众号的消息以及公众号被动回复用户消息都会继续加密。 假如服务器无法保证在五秒内处理并回复,必须做出下述回复,这样微信服务器才不会对此作任何处理,并且不会发起重试(这种情况下,可以使用客服消息接口进行异步回复),否则,将出现严重的错误提示: 一旦遇到以下情况,微信都会在公众号会话中,向用户下发系统提示“该公众号暂时无法提供服务,请稍后再试”: 另外,请注意,回复图片(不支持gif动图)等多媒体消息时需要预先通过素材管理接口上传临时素材到微信服务器,可以使用素材管理中的临时素材,也可以使用永久素材。 各种类型的响应消息格式如下: 我们可以看到回复的格式中除了都没有MsgId,只有普通文本和接收的格式基本是一样的,图片消息或其他类型的消息与接收到的格式相比,多了xml元素的嵌套,所以原先定义的接收消息实体类无法复用,我们再封装一个响应消息的实体类,这里暂时只考虑文本和图片类型 现在我们来实现一下简单的回复消息:用户给我们发什么,我们就回复什么,只需要把接收到 测试效果如下 在开发模式下要实现关键字回复非常简单,只要判断发送过来的文本消息中是否包含关键字,然后给予相应的回复即可 官方文档:https://developers.weixin.qq.com/doc/offiaccount/Message\_Management/Receiving\_event\_pushes.html 我们之前的案例都是用户发送信息过来,我们才回复的。但是,如果是关注的时候需要马上回复,就要使用到事件消息,实际上,微信已经提供给我们很多的事件 在微信用户和公众号产生交互的过程中,用户的某些操作会使得微信服务器通过事件推送的形式通知到开发者在开发者中心处设置的服务器地址,从而开发者可以获取到该信息。其中,某些事件推送在发生后,是允许开发者回复用户的,某些则不允许 用户在关注与取消关注公众号时,微信会把这个事件推送到开发者填写的URL。方便开发者给用户下发欢迎消息或者做帐号的解绑。为保护用户数据隐私,开发者收到用户取消关注事件时需要删除该用户的所有信息。 微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。 关于重试的消息排重,推荐使用FromUserName + CreateTime 排重。 假如服务器无法保证在五秒内处理并回复,可以直接回复空串,微信服务器不会对此作任何处理,并且不会发起重试。 推送XML数据包示例: 用户扫描带场景值二维码时,可能推送以下两种事件: 这个事件仅用于服务号,订阅号不行。用户同意上报地理位置后,每次进入公众号会话时,都会在进入时上报地理位置,或在进入会话后每5秒上报一次地理位置,公众号可以在公众平台网站中修改以上设置。上报地理位置时,微信会将上报地理位置事件推送到开发者填写的URL。 推送XML数据包示例: 用户点击自定义菜单后,微信会把点击事件推送给开发者,请注意,点击菜单弹出子菜单,不会产生上报。 点击菜单拉取消息时的事件推送 点击菜单跳转链接时的事件推送 我们来实现一下用户关注公众号时接收推送消息并自动回复的功能 事件和消息都是推送到我们的URL上,怎么区分他们也很简单,通过 然后在Controller处理方法中添加对应逻辑 然后取消关注后再关注,即可展现效果 官方文档:https://developers.weixin.qq.com/doc/offiaccount/Custom\_Menus/Creating\_Custom-Defined\_Menu.html 自定义菜单能够帮助公众号丰富界面,让用户更好更快地理解公众号的功能。开启自定义菜单后,公众号界面如图所示: 请注意: 自定义菜单接口可实现多种类型按钮,如下: 如果我们要给公众号创建自定义菜单,需要发送下列请求 http请求方式:POST(请使用https协议) https://api.weixin.qq.com/cgi-bin/menu/create?access\_token=ACCESS\_TOKEN 请求体示例如下: 正确时的返回 JSON 数据包如下: 错误时的返回JSON数据包如下(示例为无效菜单名长度): 其他类型的菜单示例如下 在我们调用创建菜单接口之前,必须先获取调用微信接口的 公众平台的API调用所需的access\_token的使用及生成方式说明: 公众号和小程序均可以使用AppID和AppSecret调用本接口来获取access\_token。AppID和AppSecret可在“微信公众平台-开发-基本配置”页中获得(需要已经成为开发者,且帐号没有异常状态)。**调用接口时,请登录“微信公众平台-开发-基本配置”提前将服务器IP地址添加到IP白名单中,否则将无法调用成功。**小程序无需配置IP白名单。 获取 https请求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant\_type=client\_credential&appid=APPID&secret=APPSECRET 正常情况下,微信会返回下述JSON数据包给公众号: 错误时微信会返回错误码等信息,JSON数据包示例如下(该示例为AppID无效错误): 在Controller中创建获取 在Controller中新增方法,发送创建菜单的请求 发送请求后返回结果 创建菜单后效果如下 官方文档:https://developers.weixin.qq.com/doc/offiaccount/Message\_Management/Template\_Message\_Interface.html 在我们的生活中,无论是微商城消费,还是日常生活消费,都可能收到这种提示,比如订单通知,快递状态通知,银行卡支付通知,都属于业务通知,很多公众号也都实现了这种功能,当触发了某种行为或状态改变,就会发送这么一个消息给你,因为这种消息都是按照一定的的格式来编辑,所以也叫模板消息 我们要发送模板消息,第一步是需要创建一个模板,有了模板之后,我们才能填充内容来进行发送。创建模板不需要调用接口,在公众号后台即可设置 现在我们来按照下面案例来新建一个模板。但是模板的内容是有一定的规则的,不能随便添加: 保存之后,微信会给该模板分配一个ID,待我们要发送模板消息的时候就需要用到这个ID了 发送模板消息需要如下请求: http请求方式: POST https://api.weixin.qq.com/cgi-bin/message/template/send?access\_token=ACCESS\_TOKEN 请求体示例如下 在调用模板消息接口后,会返回JSON数据包。正常时的返回JSON数据包示例:微信公众号介绍
1.公众号的分类


2.公众号注册流程







3.公众号的管理模式
3.1 编辑模式


编辑模式只能预先自定义一些固定的规则和数据,这些数据会保存在微信服务器,只能完成一些简单的功能。如果要完成更复杂的功能,比如根据用户输入信息动态获取数据返回,则需要使用开发模式
3.2 开发模式



但测试号也不是万能的,部分高级功能,如微信支付,卡券功能等也是不开放的。如果要实现支付功能还是得去注册个正式的公众号。本文后续所有测试都基于测试公众号
接入开发模式


参数 描述 signature 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。 timestamp 时间戳 nonce 随机数 echostr 随机字符串 注意到微信发送的GET请求中并没有携带我们填写的token,而签名
signature的生成过程中token是一个入参,所以我们填写的url处理程序中,校验signature时也需要token,这就要求我们在url对应的后端程序中定义token,要和通过微信页面填写的token一致。这样做是为了安全性,只有知道token才可以接入成功,避免了他人盗用公众号做操作
@RestController
@RequestMapping("wechat/publicAccount")
public class WechatPublicAccountController {
// 微信页面填写的token,必须保密
private static final String TOKEN = "";
@GetMapping("validate")
public String validate(String signature,String timestamp,String nonce,String echostr){
// 1. 将token、timestamp、nonce三个参数进行字典序排序
String[] arr = {timestamp, nonce, TOKEN};
Arrays.sort(arr);
// 2. 将三个参数字符串拼接成一个字符串进行sha1加密
StringBuilder sb = new StringBuilder();
for (String temp : arr) {
sb.append(temp);
}
// 这里利用了hutool的加密工具类
String sha1 = SecureUtil.sha1(sb.toString());
// 3. 加密后的字符串与signature对比,如果相同则该请求来源于微信,原样返回echostr
if (sha1.equals(signature)){
return echostr;
}
// 接入失败
return null;
}
}

signature、timestamp、nonce这3个参数,我们每次接收微信消息时都要跟初始接入一样去校验signature,以确保接收到的消息是微信发过来的,而不是其他人发过来的echostr,后续所有微信发过来的消息不会携带echostr,所以可以根据请求是否携带了echostr来判断是否是接入消息。接入消息除了校验signature外要原样返回echostr,其他后续消息只需校验signature即可消息接收与响应
1.接收消息


<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位整型 <xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1348831860</CreateTime>
<MsgType><![CDATA[image]]></MsgType>
<PicUrl><![CDATA[this is a url]]></PicUrl>
<MediaId><![CDATA[media_id]]></MediaId>
<MsgId>1234567890123456</MsgId>
</xml>参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 消息类型,图片为image PicUrl 图片链接(由系统生成) MediaId 图片消息媒体id,可以调用获取临时素材接口拉取数据。 MsgId 消息id,64位整型 <xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1357290913</CreateTime>
<MsgType><![CDATA[voice]]></MsgType>
<MediaId><![CDATA[media_id]]></MediaId>
<Format><![CDATA[Format]]></Format>
<MsgId>1234567890123456</MsgId>
</xml>参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 语音为voice MediaId 语音消息媒体id,可以调用获取临时素材接口拉取数据。 Format 语音格式,如amr,speex等 MsgId 消息id,64位整型 <xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1357290913</CreateTime>
<MsgType><![CDATA[video]]></MsgType>
<MediaId><![CDATA[media_id]]></MediaId>
<ThumbMediaId><![CDATA[thumb_media_id]]></ThumbMediaId>
<MsgId>1234567890123456</MsgId>
</xml>参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 视频为video MediaId 视频消息媒体id,可以调用获取临时素材接口拉取数据。 ThumbMediaId 视频消息缩略图的媒体id,可以调用多媒体文件下载接口拉取数据。 MsgId 消息id,64位整型 <xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1357290913</CreateTime>
<MsgType><![CDATA[shortvideo]]></MsgType>
<MediaId><![CDATA[media_id]]></MediaId>
<ThumbMediaId><![CDATA[thumb_media_id]]></ThumbMediaId>
<MsgId>1234567890123456</MsgId>
</xml>参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 小视频为shortvideo MediaId 视频消息媒体id,可以调用获取临时素材接口拉取数据。 ThumbMediaId 视频消息缩略图的媒体id,可以调用获取临时素材接口拉取数据。 MsgId 消息id,64位整型 <xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1351776360</CreateTime>
<MsgType><![CDATA[location]]></MsgType>
<Location_X>23.134521</Location_X>
<Location_Y>113.358803</Location_Y>
<Scale>20</Scale>
<Label><![CDATA[位置信息]]></Label>
<MsgId>1234567890123456</MsgId>
</xml>参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 消息类型,地理位置为location Location\_X 地理位置纬度 Location\_Y 地理位置经度 Scale 地图缩放大小 Label 地理位置信息 MsgId 消息id,64位整型 <xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1351776360</CreateTime>
<MsgType><![CDATA[link]]></MsgType>
<Title><![CDATA[公众平台官网链接]]></Title>
<Description><![CDATA[公众平台官网链接]]></Description>
<Url><![CDATA[url]]></Url>
<MsgId>1234567890123456</MsgId>
</xml>参数 描述 ToUserName 接收方微信号 FromUserName 发送方微信号,若为普通用户,则是一个OpenID CreateTime 消息创建时间 MsgType 消息类型,链接为link Title 消息标题 Description 消息描述 Url 消息链接 MsgId 消息id,64位整型 ToUserName,FromUserName,CreateTime,MsgType,MsgId这五个是公共的,所有类型都会带上这些参数。接下来,我们需要来了解这5个参数的具体意义:ToUserName:文档上描述的是开发者微信号,实际上,直接把它当做你的公众号的微信号即可,表示的是发到那个公众号的意思。FromUserName:与ToUserName相反,这是代表是由哪个用户发过来的,同一个用户发多条信息过来,FromUserName都是不变的。但这并不是用户的微信号,而是一个OpenID。那什么是OpenID呢:当用户和公众号发生了交互,微信服务器会为每个用户针对每个公众号产生一个OpenID(也就是指该OpenID是利用两个因素:用户和公众号来产生的,也就意味着如果该用户跟另外一个公众号交互,产生的OpenID也是不同的,这样安全性会比较高),如果一个公司有多个公众号,并且需要在多公众号、移动应用之间做用户共通,则需要使用UnionID,前往微信开放平台,将这些公众号和应用绑定到一个开放平台账号下,绑定后,一个用户虽然对多个公众号和应用有多个不同的OpenID,但他对所有这些同一开放平台账号下的公众号和应用,只有一个UnionID,可以在用户管理-获取用户基本信息(UnionID机制)文档了解详情。CreateTime:消息创建时间MsgType:用户发送的消息的类型,如text代表文本消息,image代表图片消息等。MsgId:用户发送的每个消息都有自己的id,可以用于消息排重,比如微信服务器把xml消息包发送到URL了,但是五秒内微信服务器没有收到我们的响应,则会重新发起请求,总共重试三次。如果不做消息排重,那么用户可能就收到多条相同的响应消息了。微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。假如服务器无法保证在五秒内处理并回复,可以直接回复空串,微信服务器不会对此作任何处理,并且不会发起重试
@Data
@AllArgsConstructor
@NoArgsConstructor
public class InMessage {
// 开发者微信号
protected String FromUserName;
// 发送方帐号(一个OpenID)
protected String ToUserName;
// 消息创建时间
protected Long CreateTime;
/**
* 消息类型
* text 文本消息
* image 图片消息
* voice 语音消息
* video 视频消息
* music 音乐消息
*/
protected String MsgType;
// 消息id
protected Long MsgId;
// 文本内容
private String Content;
// 图片链接(由系统生成)
private String PicUrl;
// 图片消息媒体id,可以调用多媒体文件下载接口拉取数据
private String MediaId;
}<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>

@XmlRootElement:是一个类级别注解,主要属性为name,意为指定根节点的名字。往上面看前面举了个微信传过来的xml数据的例子里,里面的根节点就是"xml",所以这里就直接设置name=“xml”@XmlAccessorType:用于定义这个类中的何种类型需要映射到XML中XmlAccessType.PROPERTY:代表映射这个类中的属性(get/set方法)到XMLXmlAccessType.FIELD:代表映射这个类中的所有字段到XML@XmlElement给每个属性标注对应的xml节点名称@Data
@AllArgsConstructor
@NoArgsConstructor
@XmlRootElement(name = "xml")
@XmlAccessorType(XmlAccessType.FIELD)
public class InMessage {
/**
* 开发者微信号
*/
@XmlElement(name="FromUserName")
protected String fromUserName;
/**
* 发送方帐号(一个OpenID)
*/
@XmlElement(name="ToUserName")
protected String toUserName;
/**
* 消息创建时间
*/
@XmlElement(name="CreateTime")
protected Long createTime;
/**
* 消息类型
* text 文本消息
* image 图片消息
* voice 语音消息
* video 视频消息
* music 音乐消息
*/
@XmlElement(name="MsgType")
protected String msgType;
/**
* 消息id
*/
@XmlElement(name="MsgId")
protected Long msgId;
/**
* 文本内容
*/
@XmlElement(name="Content")
private String content;
/**
* 图片链接(由系统生成)
*/
@XmlElement(name="PicUrl")
private String picUrl;
/**
* 图片消息媒体id,可以调用多媒体文件下载接口拉取数据
*/
@XmlElement(name="MediaId")
private String mediaId;
}
2.响应消息
<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中能够换行,微信客户端就支持换行显示) <xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType><![CDATA[image]]></MsgType>
<Image>
<MediaId><![CDATA[media_id]]></MediaId>
</Image>
</xml>参数 是否必须 说明 ToUserName 是 接收方帐号(收到的OpenID) FromUserName 是 开发者微信号 CreateTime 是 消息创建时间 (整型) MsgType 是 消息类型,图片为image MediaId 是 通过素材管理中的接口上传多媒体文件,得到的id。 <xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType><![CDATA[voice]]></MsgType>
<Voice>
<MediaId><![CDATA[media_id]]></MediaId>
</Voice>
</xml>参数 是否必须 说明 ToUserName 是 接收方帐号(收到的OpenID) FromUserName 是 开发者微信号 CreateTime 是 消息创建时间戳 (整型) MsgType 是 消息类型,语音为voice MediaId 是 通过素材管理中的接口上传多媒体文件,得到的id <xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType><![CDATA[video]]></MsgType>
<Video>
<MediaId><![CDATA[media_id]]></MediaId>
<Title><![CDATA[title]]></Title>
<Description><![CDATA[description]]></Description>
</Video>
</xml>参数 是否必须 说明 ToUserName 是 接收方帐号(收到的OpenID) FromUserName 是 开发者微信号 CreateTime 是 消息创建时间 (整型) MsgType 是 消息类型,视频为video MediaId 是 通过素材管理中的接口上传多媒体文件,得到的id Title 否 视频消息的标题 Description 否 视频消息的描述 <xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType><![CDATA[music]]></MsgType>
<Music>
<Title><![CDATA[TITLE]]></Title>
<Description><![CDATA[DESCRIPTION]]></Description>
<MusicUrl><![CDATA[MUSIC_Url]]></MusicUrl>
<HQMusicUrl><![CDATA[HQ_MUSIC_Url]]></HQMusicUrl>
<ThumbMediaId><![CDATA[media_id]]></ThumbMediaId>
</Music>
</xml>参数 是否必须 说明 ToUserName 是 接收方帐号(收到的OpenID) FromUserName 是 开发者微信号 CreateTime 是 消息创建时间 (整型) MsgType 是 消息类型,音乐为music Title 否 音乐标题 Description 否 音乐描述 MusicURL 否 音乐链接 HQMusicUrl 否 高质量音乐链接,WIFI环境优先使用该链接播放音乐 ThumbMediaId 是 缩略图的媒体id,通过素材管理中的接口上传多媒体文件,得到的id <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格式,较好的效果为大图360*200,小图200*200 Url 是 点击图文消息跳转链接 @Data
@AllArgsConstructor
@NoArgsConstructor
@XmlRootElement(name = "xml")
@XmlAccessorType(XmlAccessType.FIELD)
public class OutMessage {
/**
* 开发者微信号
*/
@XmlElement(name="FromUserName")
protected String fromUserName;
/**
* 发送方帐号(一个OpenID)
*/
@XmlElement(name="ToUserName")
protected String toUserName;
/**
* 消息创建时间
*/
@XmlElement(name="CreateTime")
protected Long createTime;
/**
* 消息类型
* text 文本消息
* image 图片消息
* voice 语音消息
* video 视频消息
* music 音乐消息
*/
@XmlElement(name="MsgType")
protected String msgType;
/**
* 文本内容
*/
@XmlElement(name="Content")
private String content;
/**
* 图片的媒体id列表
*/
@XmlElementWrapper(name="Image") // 表示MediaId属性内嵌于<Image>标签
@XmlElement(name = "MediaId")
private String[] mediaId;
}@XmlElementWrapper注解可以在原xml结点上再包装一层xml,但仅允许出现在数组或集合属性上InMessage的内容设置到OutMessage上,并且把ToUserName与FromUserName的值设置为相反即可@PostMapping(value = "validate", produces = "application/xml;charset=UTF-8")
public Object handleMessage(@RequestBody InMessage inMessage){
// 创建响应消息实体对象
OutMessage outMessage = new OutMessage();
// 把原来的接收方设置为发送方
outMessage.setFromUserName(inMessage.getToUserName());
// 把原来的发送方设置为接收方
outMessage.setToUserName(inMessage.getFromUserName());
// 设置消息类型
outMessage.setMsgType(inMessage.getMsgType());
// 设置消息时间
outMessage.setCreateTime(System.currentTimeMillis());
// 根据接收到消息类型,响应对应的消息内容
if ("text".equals(inMessage.getMsgType())){
// 文本
outMessage.setContent(inMessage.getContent());
}else if ("image".equals(inMessage.getMsgType())){
// 图片
outMessage.setMediaId(new String[]{inMessage.getMediaId()});
}
return outMessage;
}注意:这里要在接口的
@PostMapping中指定produces = "application/xml;charset=UTF-8",表示返回的数据格式是xml,否则将默认以json格式返回
关键字回复
@PostMapping(value = "validate", produces = "application/xml;charset=UTF-8")
public Object handleMessage(@RequestBody InMessage inMessage){
// 创建响应消息实体对象
OutMessage outMessage = new OutMessage();
// 把原来的接收方设置为发送方
outMessage.setFromUserName(inMessage.getToUserName());
// 把原来的发送方设置为接收方
outMessage.setToUserName(inMessage.getFromUserName());
// 设置消息类型
outMessage.setMsgType(inMessage.getMsgType());
// 设置消息时间
outMessage.setCreateTime(System.currentTimeMillis() / 1000);
// 根据接收到消息类型,响应对应的消息内容
if ("text".equals(inMessage.getMsgType())){
// 根据不同的关键字回复消息
String inContent = inMessage.getContent();
if (inContent.contains("游戏")){
outMessage.setContent("仙剑");
}else if (inContent.contains("动漫")){
outMessage.setContent("进击的巨人");
}else {
outMessage.setContent(inContent);
}
}else if ("image".equals(inMessage.getMsgType())){
// 图片
outMessage.setMediaId(new String[]{inMessage.getMediaId()});
}
return outMessage;
}
事件推送
1.关注/取消关注事件
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe]]></Event>
</xml>参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 消息类型,event Event 事件类型,subscribe(订阅)、unsubscribe(取消订阅) 2.扫描带参数二维码事件
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe]]></Event>
<EventKey><![CDATA[qrscene_123123]]></EventKey>
<Ticket><![CDATA[TICKET]]></Ticket>
</xml>参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 消息类型,event Event 事件类型,subscribe EventKey 事件KEY值,qrscene\_为前缀,后面为二维码的参数值 Ticket 二维码的ticket,可用来换取二维码图片 <xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[SCAN]]></Event>
<EventKey><![CDATA[SCENE_VALUE]]></EventKey>
<Ticket><![CDATA[TICKET]]></Ticket>
</xml> 参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 消息类型,event Event 事件类型,SCAN EventKey 事件KEY值,是一个32位无符号整数,即创建二维码时的二维码scene\_id Ticket 二维码的ticket,可用来换取二维码图片 3.上报地理位置事件
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[LOCATION]]></Event>
<Latitude>23.137466</Latitude>
<Longitude>113.352425</Longitude>
<Precision>119.385040</Precision>
</xml>参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 消息类型,event Event 事件类型,LOCATION Latitude 地理位置纬度 Longitude 地理位置经度 Precision 地理位置精度 4.自定义菜单事件
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[CLICK]]></Event>
<EventKey><![CDATA[EVENTKEY]]></EventKey>
</xml>参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 消息类型,event Event 事件类型,CLICK EventKey 事件KEY值,与自定义菜单接口中KEY值对应 <xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[VIEW]]></Event>
<EventKey><![CDATA[www.qq.com]]></EventKey>
</xml>参数 描述 ToUserName 开发者微信号 FromUserName 发送方帐号(一个OpenID) CreateTime 消息创建时间 (整型) MsgType 消息类型,event Event 事件类型,VIEW EventKey 事件KEY值,设置的跳转URL 事件推送案例演示
MsgType这个属性,那么进一步再区分是关注还是取消关注,根据Event属性即可。所以,我们在原来的InMessage类,再添加一个Event属性


自定义菜单
1.自定义菜单简介

请注意,3到8的所有事件,仅支持微信iPhone5.4.1以上版本,和Android5.4以上版本的微信用户,旧版本微信用户点击后将没有回应,开发者也不能正常接收到事件推送。9和10,是专门给第三方平台旗下未微信认证(具体而言,是资质认证未通过)的订阅号准备的事件类型,它们是没有事件推送的,能力相对受限,其他类型的公众号不必使用。
{
"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 是 一级菜单数组,个数应为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类型必须 小程序的页面路径 {"errcode":0,"errmsg":"ok"}{"errcode":40018,"errmsg":"invalid button name size"}{
"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"
}
]
}2.获取access\_token
access_token。access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access\_token的存储至少要保留512个字符空间。access\_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access\_token失效。access_token需要调用的接口如下参数 是否必须 说明 grant\_type 是 获取access\_token填写client\_credential appid 是 公众号唯一凭证,注册成功后由微信提供 secret 是 公众号唯一凭证密钥,注册成功后由微信提供 {"access_token":"ACCESS_TOKEN","expires_in":7200}参数 说明 access\_token 获取到的凭证 expires\_in 凭证有效时间,单位:秒 {"errcode":40013,"errmsg":"invalid appid"}返回码 说明 -1 系统繁忙,此时请开发者稍候再试 0 请求成功 40001 AppSecret错误或者AppSecret不属于这个公众号,请开发者确认AppSecret的正确性 40002 请确保grant\_type字段值为client\_credential 40164 调用接口的IP地址不在白名单中,请在接口IP白名单中进行设置。(小程序及小游戏调用不要求IP地址在白名单内。) 89503 此IP调用需要管理员确认,请联系管理员 89501 此IP正在等待管理员确认,请联系管理员 89506 24小时内该IP被管理员拒绝调用两次,24小时内不可再使用该IP调用 89507 1小时内该IP被管理员拒绝调用一次,1小时内不可再使用该IP调用 access_token的方法,获取成功后将其保存到redis中@GetMapping("getAccessToken")
public String getAccessToken(){
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + APPID +
"&secret=" + APPSECRET;
// 利用hutool的http工具类请求获取access_token
String result = HttpUtil.get(url);
// 将结果解析为json
JSONObject jsonObject = JSONUtil.parseObj(result);
// 获取access_token
String accessToken = jsonObject.getStr("access_token");
if (!StringUtils.isEmpty(accessToken)){
// 将access_token存入redis
stringRedisTemplate.opsForValue().set("access_token", accessToken);
}
return accessToken;
}3.创建自定义菜单
@GetMapping("createMenu")
public String createMenu(){
// 从redis中取出access_token
String accessToken = stringRedisTemplate.opsForValue().get("access_token");
String url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=" + accessToken;
// 创建菜单的请求体
String body = "{\n" +
" \"button\":[\n" +
" {\t\n" +
" \"type\":\"click\",\n" +
" \"name\":\"位置\",\n" +
" \"key\":\"button_location\"\n" +
" }]}";
return HttpUtil.post(url, body);
}{"errcode":0,"errmsg":"ok"}
发送模板消息

认证后的服务号才可以申请模板消息的使用权限并获得该权限,否则就只能使用测试号


{
"touser":"OPENID",
"template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY",
"url":"http://weixin.qq.com/download",
"miniprogram":{
"appid":"xiaochengxuappid12345",
"pagepath":"index?foo=bar"
},
"data":{
"goodsName":{
"value":"巧克力",
"color":"#173177"
},
"price": {
"value":"39.8元",
"color":"#173177"
},
"time": {
"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 否 模板内容字体颜色,不填默认为黑色 {
"errcode":0,
"errmsg":"ok",
"msgid":200228332
}