需求
要实现借助公众号给系统中的用户发送通知,则至关重要的一步就是将公众号用户与系统用户绑定起来。这样在系统中需要发送通知的时候,就可以知道对哪个关注了公众号的用户发送通知。
1、接口测试号
1.1、登录微信公众平台测试号接口
1.2、填写接口配置信息
该步骤需要使用到内网穿透工具https://www.yuque.com/xihuanxiaorang/ng3te7/delw5o。提交配置的时候会把这个token发送到微信平台,然后微信平台会请求此URL调用开发的微信服务,验证服务的可用性和合法性。
URL:http://intelliws.vaiwan.com/api/wx/portal/appID
Token:intelliws
1.3、开发环境准备
1.3.1、引入 wx-java-mp-spring-boot-starter
依赖
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>4.1.5.B</version>
</dependency>
1.3.2、配置文件
wx.mp.app-id=wxe04ed9ab275e518f
wx.mp.secret=dc8ab44fbd2be76a1690f9acedbe1c18
wx.mp.token=intelliws
1.3.3、编写controller
@RestController
@RequestMapping("/api/wx/portal/{appid}")
public class WeiXinMpController {
private static final Logger logger = LoggerFactory.getLogger(WeiXinMpController.class);
private final WxMpService wxMpService;
public WeiXinMpController(WxMpService wxMpService) {
this.wxMpService = wxMpService;
}
@GetMapping
public void authGet(@PathVariable String appid,
@RequestParam(name = "signature", required = false) String signature,
@RequestParam(name = "timestamp", required = false) String timestamp,
@RequestParam(name = "nonce", required = false) String nonce,
@RequestParam(name = "echostr", required = false) String echostr, HttpServletResponse response)
throws IOException {
logger.info("\n接收到来自微信服务器的认证消息:[{}, {}, {}, {}]", signature, timestamp, nonce, echostr);
if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
throw new IllegalArgumentException("请求参数非法,请核实!");
}
if (!this.wxMpService.switchover(appid)) {
throw new IllegalArgumentException(String.format("未找到对应appid=[%s]的配置,请核实!", appid));
}
PrintWriter out = response.getWriter();
if (wxMpService.checkSignature(timestamp, nonce, signature)) {
out.write(echostr);
} else {
out.write("非法请求!");
}
out.flush();
out.close();
}
}
1.3.4、启动内网穿透,点击页面提交
2、接收与响应消息
2.1、消息处理器接口
WxJava 为了对不同类型的微信消息进行分类处理,用户必须自己实现不同类型的消息处理器,而消息处理器必须实现 WxMpMessageHandler
接口。
/**
* 处理微信推送消息的处理器接口.
*
*/
public interface WxMpMessageHandler {
/**
* 处理微信推送消息.
*
* @param wxMessage 微信推送消息
* @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个
* @param wxMpService 服务类
* @param sessionManager session管理器
* @return xml格式的消息,如果在异步规则里处理的话,可以返回null
* @throws WxErrorException 异常
*/
WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
Map<String, Object> context,
WxMpService wxMpService,
WxSessionManager sessionManager) throws WxErrorException;
}
2.2、关注与取消关注事件
2.2.1、实现消息处理器
实现一个接收关注、取消关注事件推送的处理。首先定义关注和取消关注的消息处理器存入容器。
@Component
public class WxMpSubscribeHandler implements WxMpMessageHandler {
private static final Logger logger = LoggerFactory.getLogger(WxMpSubscribeHandler.class);
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService,
WxSessionManager sessionManager) throws WxErrorException {
logger.info("新关注用户: {}", wxMessage.getFromUser());
// 获取微信用户基本信息
try {
WxMpUser userWxInfo = wxMpService.getUserService().userInfo(wxMessage.getFromUser(), null);
if (userWxInfo != null) {
// TODO 可以添加关注用户到本地数据库
logger.info("用户信息: {}", userWxInfo);
}
return WxMpXmlOutMessage.TEXT().fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.content("欢迎关注!").build();
} catch (WxErrorException e) {
if (e.getError().getErrorCode() == 48001) {
logger.info("该公众号没有获取用户信息权限!");
}
}
return null;
}
}
@Component
public class WxMpUnSubscribeHandler implements WxMpMessageHandler {
private static final Logger logger = LoggerFactory.getLogger(WxMpUnSubscribeHandler.class);
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService,
WxSessionManager sessionManager) throws WxErrorException {
logger.info("用户取消关注: {}", wxMessage.getFromUser());
// TODO 可以更新本地数据库为取消关注状态
// 因为已经取消关注,所以即使回复消息也收不到
return WxMpXmlOutMessage.TEXT().fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser()).content("请别离开我")
.build();
}
}
2.2.2、指定消息路由规则
@Configuration
public class WeiXinMpConfig {
private final WxMpService wxMpService;
private final WxMpSubscribeHandler wxMpSubscribeHandler;
private final WxMpUnSubscribeHandler wxMpUnSubscribeHandler;
public WeiXinMpConfig(WxMpService wxMpService, WxMpSubscribeHandler wxMpSubscribeHandler,
WxMpUnSubscribeHandler wxMpUnSubscribeHandler) {
this.wxMpService = wxMpService;
this.wxMpSubscribeHandler = wxMpSubscribeHandler;
this.wxMpUnSubscribeHandler = wxMpUnSubscribeHandler;
}
@Bean
public WxMpMessageRouter wxMpMessageRouter() {
final WxMpMessageRouter router = new WxMpMessageRouter(wxMpService);
router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT).event(WxConsts.EventType.SUBSCRIBE)
.handler(wxMpSubscribeHandler).end();
router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT).event(WxConsts.EventType.UNSUBSCRIBE)
.handler(wxMpUnSubscribeHandler).end();
return router;
}
}
2.2.3、编写controller
@PostMapping(produces = "application/xml; charset=UTF-8")
public String post(@PathVariable String appid, @RequestBody String requestBody,
@RequestParam("signature") String signature, @RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce, @RequestParam("openid") String openid,
@RequestParam(name = "encrypt_type", required = false) String encType,
@RequestParam(name = "msg_signature", required = false) String msgSignature) {
logger.info(
"\n接收微信请求:[appid=[{}], openid=[{}], [signature=[{}], encType=[{}], msgSignature=[{}],"
+ " timestamp=[{}], nonce=[{}], requestBody=[\n{}\n] ",
appid, openid, signature, encType, msgSignature, timestamp, nonce, requestBody);
if (!this.wxMpService.switchover(appid)) {
throw new IllegalArgumentException(String.format("未找到对应appid=[%s]的配置,请核实!", appid));
}
if (!wxMpService.checkSignature(timestamp, nonce, signature)) {
throw new IllegalArgumentException("非法请求,可能属于伪造的请求!");
}
String out = null;
if (encType == null) {
// 明文传输的消息
WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(requestBody);
WxMpXmlOutMessage outMessage = this.route(inMessage);
if (outMessage == null) {
return "";
}
out = outMessage.toXml();
} else if ("aes".equalsIgnoreCase(encType)) {
// aes加密的消息
WxMpXmlMessage inMessage = WxMpXmlMessage.fromEncryptedXml(requestBody, wxMpService.getWxMpConfigStorage(),
timestamp, nonce, msgSignature);
logger.debug("\n消息解密后内容为:\n{} ", inMessage.toString());
WxMpXmlOutMessage outMessage = this.route(inMessage);
if (outMessage == null) {
return "";
}
out = outMessage.toEncryptedXml(wxMpService.getWxMpConfigStorage());
}
logger.debug("\n组装回复信息:{}", out);
return out;
}
private WxMpXmlOutMessage route(WxMpXmlMessage message) {
try {
return this.wxMpMessageRouter.route(message);
} catch (Exception e) {
logger.error("路由消息时出现异常!", e);
}
return null;
}
2.2.4、用户扫码关注
2.2.5、用户取消关注
3、公众号用户与网站用户绑定重要
其实在微信公众号文档中已经给出了答案,为了满足用户渠道推广分析和用户帐号绑定等场景的需要,公众平台提供了生成带参数二维码的接口。使用该接口可以获得多个带不同场景值的二维码,用户扫描后,公众号可以接收到事件推送。
3.1、流程
一次完整的绑定流程如下:
-
用户登录网站,进入用户管理列表,点击用户上的"绑定微信账户"按钮;
-
后台使用微信接口,生成二维码链接返回给前端弹框显示,并带上场景值(即当前绑定的用户编号);
-
如果用户还未关注公众号,用户扫描二维码,并点击关注微信公众号;后台接收微信服务器推送的关注事件,拿到场景值;
-
如果用户已经关注公众号,用户扫描二维码,直接进入公众号会话;后台接收微信服务器推送的扫描事件,拿到场景值;
-
后台将场景值(即当前绑定的用户编号)与微信用户的openId绑定起来;
-
给微信公众号返回"绑定成功"的提示;
-
通知网站前台页面,提示"绑定成功",刷新页面,并返回一些微信用户信息。
3.2、二维码类型
目前有2种类型的二维码:
- 临时二维码,是有过期时间的,最长可以设置为在二维码生成后的30天(即2592000秒)后过期,但能够生成较多数量。临时二维码主要用于帐号绑定等不要求二维码永久保存的业务场景
- 永久二维码,是无过期时间的,但数量较少(目前为最多10万个)。永久二维码主要用于适用于帐号绑定、用户来源统计等场景。
3.3、事件推送类型
用户扫描带场景值二维码时,可能推送以下两种事件:
- 如果用户还未关注公众号,则用户可以关注公众号,关注后微信会将带场景值的关注事件推送给开发者
- 如果用户已经关注公众号,在用户扫描后会自动进入会话,微信也会将带场景值的扫描事件推送给开发者
3.4、生成二维码步骤
3.4.1、在你的网站页面上生成一个带场景值的二维码,其中的场景值为当前需要绑定的系统用户编号
3.4.2、创建二维码ticket,每次创建二维码ticket的时候需要提供一个开发者自行设定的参数(scene_id)
- 临时二维码请求说明:
http请求方式: POST URL: https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN
POST数据格式:json POST数据例子:{"expire_seconds": 604800, "action_name": "QR_SCENE", "action_info": {"scene": {"scene_id": 123}}}
或者也可以使用以下POST数据创建字符串形式的二维码参数:{"expire_seconds": 604800, "action_name": "QR_STR_SCENE", "action_info": {"scene": {"scene_str": "test"}}}
- 永久二维码请求说明:
http请求方式: POST URL: https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN
POST数据格式:json POST数据例子:{"action_name": "QR_LIMIT_SCENE", "action_info": {"scene": {"scene_id": 123}}}
或者也可以使用以下POST数据创建字符串形式的二维码参数: {"action_name": "QR_LIMIT_STR_SCENE", "action_info": {"scene": {"scene_str": "test"}}}
参数说明:
参数 | 说明 |
---|---|
expire_seconds | 该二维码有效时间,以秒为单位。 最大不超过2592000(即30天),此字段如果不填,则默认有效期为30秒。 |
action_name | 二维码类型,QR_SCENE为临时的整型参数值,QR_STR_SCENE为临时的字符串参数值,QR_LIMIT_SCENE为永久的整型参数值,QR_LIMIT_STR_SCENE为永久的字符串参数值 |
action_info | 二维码详细信息 |
scene_id | 场景值ID,临时二维码时为32位非0整型,永久二维码时最大值为100000(目前参数只支持1--100000) |
scene_str | 场景值ID(字符串形式的ID),字符串类型,长度限制为1到64 |
返回结果:
参数说明:
参数 | 说明 |
---|---|
ticket | 获取的二维码ticket,凭借此ticket可以在有效时间内换取二维码。 |
expire_seconds | 该二维码有效时间,以秒为单位。 最大不超过2592000(即30天)。 |
url | 二维码图片解析后的地址,开发者可根据该地址自行生成需要的二维码图片 |
3.4.3、通过ticket换取二维码
获取二维码ticket后,开发者可用ticket换取二维码图片。请注意,本接口无须登录态即可调用。
请求说明:
HTTP GET请求(请使用https协议)https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET 提醒:TICKET记得进行UrlEncode
返回结果:二维码图片地址就是下面返回的请求地址。
3.4.4、代码
- 生成带场景值的二维码
/**
* 生成带场景值二维码
*
* @return 二维码url
*/
@GetMapping("qr-code/{userNo}")
public String createQrCode(@PathVariable String userNo) throws WxErrorException {
logger.info("绑定用户账号为: {}", userNo);
// 获取ticket,时间不填默认30秒,最大30天
WxMpQrCodeTicket ticket =
this.wxMpService.getQrcodeService().qrCodeCreateTmpTicket(userNo, null);
// 根据ticket创建临时二维码
return this.wxMpService.getQrcodeService().qrCodePictureUrl(ticket.getTicket());
}
网站显示二维码图片
- 修改消息路由规则
router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT).event(WxConsts.EventType.SCAN).handler(wxMpScanHandler).end();
- 添加用于处理扫描的消息处理器
@Component
public class WxMpScanHandler implements WxMpMessageHandler {
private static final Logger logger = LoggerFactory.getLogger(WxMpScanHandler.class);
@Override
public WxMpXmlOutMessage handle(
WxMpXmlMessage wxMessage,
Map<String, Object> map,
WxMpService wxMpService,
WxSessionManager wxSessionManager)
throws WxErrorException {
logger.info("系统用户账号为:{}", wxMessage.getEventKey());
logger.info("openId: {}", wxMessage.getFromUser());
return WxMpXmlOutMessage.TEXT()
.content("绑定系统用户成功!")
.fromUser(wxMessage.getToUser())
.toUser(wxMessage.getFromUser())
.build();
}
}
- 修改关注事件消息处理器
@Component
public class WxMpSubscribeHandler implements WxMpMessageHandler {
private static final Logger logger = LoggerFactory.getLogger(WxMpSubscribeHandler.class);
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService,
WxSessionManager sessionManager) throws WxErrorException {
if (StringUtils.hasText(wxMessage.getEventKey())) {
// 通过扫描带场景值二维码关注的用户,用于系统绑定用户
logger.info("用户账号为:{}", wxMessage.getEventKey().split("_")[1]);
}
logger.info("新用户关注 OPENID: {}", wxMessage.getFromUser());
String uri = "http://intelliws.vaiwan.com/api/wx/portal/APPID/callback";
uri = uri.replace("APPID", wxMpService.getWxMpConfigStorage().getAppId());
String href = "欢迎关注!<a href=\"" + wxMpService.getOAuth2Service().buildAuthorizationUrl(uri,
WxConsts.OAuth2Scope.SNSAPI_USERINFO, wxMpService.getWxMpConfigStorage().getToken())
+ "\">请点击此处进行网页授权,测试用!!!</a>";
return WxMpXmlOutMessage.TEXT().content(href).fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.build();
}
}
到此,公众号用户与系统用户绑定的流程就完成了。
4、模板消息
4.1、使用规则
-
所有服务号都可以在功能->添加功能插件处看到申请模板消息功能的入口,但只有认证后的服务号才可以申请模板消息的使用权限并获得该权限;
-
需要选择公众账号服务所处的2个行业,每月可更改1次所选行业;
-
在所选择行业的模板库中选用已有的模板进行调用;
-
每个账号可以同时使用25个模板。
-
当前每个账号的模板消息的日调用上限为10万次,单个模板没有特殊限制。【2014年11月18日将接口调用频率从默认的日1万次提升为日10万次,可在MP登录后的开发者中心查看】。当账号粉丝数超过10W/100W/1000W时,模板消息的日调用上限会相应提升,以公众号MP后台开发者中心页面中标明的数字为准。
4.2、模板消息接口
4.2.1、注意点:
-
模板消息调用时主要需要模板ID和模板中各参数的赋值内容;
-
模板中参数内容必须以".DATA"结尾,否则视为保留字;
-
模板保留符号""。
4.2.2、发送模板消息
4.2.2.1、接口调用请求说明
http请求方式: POST https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN
4.2.2.2、POST数据说明
{
"touser":"OPENID",
"template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY",
"url":"http://weixin.qq.com/download",
"miniprogram":{
"appid":"xiaochengxuappid12345",
"pagepath":"index?foo=bar"
},
"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"
}
}
}
参数说明;
注:url和miniprogram都是非必填字段,若都不传则模板无跳转;若都传,会优先跳转至小程序。开发者可根据实际需要选择其中一种跳转方式即可。当用户的微信客户端版本不支持跳小程序时,将会跳转至url。
4.3、添加模板
如果是认证过后的服务号,可以登录微信公众号后台管理,从模板库中添加,如果找不到适合的模板,还可以申请新模板(一个月只可以申请三个模板);现在我们可以先在测试号中手动添加模板。
在测试号中手动添加:
{{first.DATA}} 商家名称:{{keyword1.DATA}} 商家电话:{{keyword2.DATA}} 订单号:{{keyword3.DATA}} 状态:{{keyword4.DATA}} 总价:{{keyword5.DATA}} {{remark.DATA}}
4.4、代码
/**
* 发送模板消息
*
* @return
* @throws WxErrorException
*/
@GetMapping("send")
public String sendTemplateMessage() throws WxErrorException {
logger.info(wxMpService.getAccessToken());
// 发送模板消息接口
WxMpTemplateMessage templateMessage =
WxMpTemplateMessage.builder()
// 接收者openid
.toUser("openId")
// 模板id
.templateId("templateId")
// 模板跳转链接
.url("http://www.baidu.com")
.build();
// 添加模板数据
templateMessage
.addData(new WxMpTemplateData("first", "用餐愉快哦", "#FF00FF"))
.addData(new WxMpTemplateData("keyword1", "微信点餐", "#A9A9A9"))
.addData(new WxMpTemplateData("keyword2", "13826913333", "#FF00FF"))
.addData(new WxMpTemplateData("keyword3", "2021081722150001", "#FF00FF"))
.addData(new WxMpTemplateData("keyword4", "¥56.5", "#FF00FF"))
.addData(new WxMpTemplateData("remark", "用餐愉快哦", "#000000"));
String msgId = null;
try {
// 发送模板消息
msgId = wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
logger.info(wxMpService.getAccessToken());
logger.warn("·==++--·推送微信模板信息:{}·--++==·", "成功");
} catch (WxErrorException e) {
System.out.println(wxMpService.getAccessToken());
logger.warn("·==++--·推送微信模板信息:{}·--++==·", "失败");
e.printStackTrace();
}
return msgId;
}
其中,如果想使用redis保存accessToken的话,可以在配置文件中配置并且还要加入redis的依赖:
wx:
mp:
app-id: xxxxxxxx
secret: xxxxxxxx
token: xxxxxxxx
config-storage:
type: redistemplate
spring:
redis:
host: xxx.xx.xxx.xxx
<!--wx-java-mp的依赖 -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>4.1.5.B</version>
</dependency>
<!--redis的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
代码仓库 https://e.coding.net/start520823/notes/wx-mp.git
5、公司服务号
经过如下配置后,就和我们使用测试号就是一样的效果了。
至此,大功告成!撒花❀❀❀