需求
要實現借助公眾號給系統中的用戶發送通知,則至關重要的一步就是將公眾號用戶與系統用戶綁定起來。這樣在系統中需要發送通知的時候,就可以知道對哪個關注了公眾號的用戶發送通知。
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、公司服務號
經過如下配置后,就和我們使用測試號就是一樣的效果了。
至此,大功告成!撒花❀❀❀