golang:后端实现微信订阅消息发送功能
在日常开发中,难免会遇到需要微信订阅消息发送的需求,大部分的消息订阅发送使用的模板都是一次性发送的模板(因为长期订阅的消息模板申请时,需要服务性质是政务民生、医疗、交通、金融、教育等线下公共服务),而一次性的消息模板需要每次发送订阅消息后,用户再次订阅。我的办法是用户每次进入特定的页面后,让用户再次订阅。如下图:用户有了订阅次数,会将订阅次数和对应的模板Id存储在微信后端,需要注意,当次数用完后,
前言
在日常开发中,难免会遇到需要微信订阅消息发送的需求,大部分的消息订阅发送使用的模板都是一次性发送的模板(因为长期订阅的消息模板申请时,需要服务性质是政务民生、医疗、交通、金融、教育等线下公共服务),而一次性的消息模板需要每次发送订阅消息后,用户再次订阅。我的办法是用户每次进入特定的页面后,让用户再次订阅。如下
用户有了订阅次数,会将订阅次数和对应的模板Id存储在微信后端,需要注意,当次数用完后,调用发送订阅消息接口会报错,如下图:
下面,我们开始主要逻辑部分。
业务流程
订阅消息的发送主要分为两个部分,一是用户的订阅,二是真正给用户发送订阅消息。
一、用户的订阅
(1)openId的获取
用户首次订阅时,需要将用户的openId存储在数据库中,前端调用存储openId接口,入参是用户在系统的唯一标识,由前端提供,后端拿到code后,通过调用 “api.weixin.qq.com/sns/jscode2session” 接口获取openId,拿到openId后,需要将openId和userId形成关联关系并存储在数据库中,方便下次发送订阅消息时,拿到对应用户的openId。
需用到的接口是api.weixin.qq.com/sns/jscode2session,get请求,入参是code,appId,appSecret,code是前端获取到的用户的唯一标识,appId和appSecret是开发者在微信开发者平台拿到的小程序标识,最终发送的路径大致为“https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code”。
(2)openId的存储
拿到openId后,我们需要去系统里查看,当前登录人是否已经存储过对应的openId,如果已经存储过,可以不在做处理,否则需要将当前登陆人的userId和openId绑定在一起,存储到数据库当中,表结构可以设计的简单一些,总共就三个字段,id,userId,openId,实际情况根据业务而定。
二、发送订阅消息
(1)获取accessToken
在发送订阅消息前,需要先获取accessToken,微信对于accessToken有严格的要求,一个小程序生成一次accessToken的有效时间是2小时,而且微信对于“api.weixin.qq.com/cgi-bin/token”接口的调用次数有限制,不能频繁的调用,所以我采用的方法是将accessToken存入redis当中,设置过期时间为2小时,当accessToken过期,我们从redis里面获取accessToken时也正好获取不到,这个时候,再次调用“api.weixin.qq.com/cgi-bin/token”接口获取accessToken。
(2)组装入参
订阅消息的消息模板一般都有一些提示信息,这些提示信息需要在发送时组装好,我的消息模板大概是:
组装的结构如下:
type SubscribeMessageData struct {
Value string `json:"value"`
}
data := make(map[string]SubscribeMessageData)
data["thing1"] = SubscribeMessageData{Value: "维修厂-平"}
data["time2"] = SubscribeMessageData{Value: "2024年01月11日"}
data["thing3"] = SubscribeMessageData{Value: "车辆 皖A666661,报修人待确认"}
data["thing4"] = SubscribeMessageData{Value: "车辆总成"}
效果如下图:
(3)发送通知
目前,已经拿到,openId,accessToken,templateId,以及需要组装的参数。
根据微信开发文档的描述,我们已经足够调用“api.weixin.qq.com/cgi-bin/message/subscribe/send”接口了,需要注意的是有几个必传的参数:
- access_token
- touser:接收者(用户)的 openid
- template_id: 所需下发的订阅模板id
- page:小程序跳转链接,仅限本小程序内的页面。
- miniprogram_state:订阅消息跳转小程序类型 developer为开发版;trial为体验版;formal为正式版;默认为正式版
- lang:进入小程序查看”的语言类型,支持zh_CN(简体中文)、en_US(英文)、zh_HK(繁体中文)、zh_TW(繁体中文),默认为zh_CN 返回参数
- data:模板内容,格式形如 { “key1”: { “value”: any }, “key2”: { “value”: any } }的object
尤其是page字段需要注意,该字段不传,则模板无跳转,我就在上面吃过亏!!!虽然默认跳转到首页,但还是需要传这个字段,那怕只传一个空字符,否则现象如下:
传参时需要注意,不同的类型传入的参数是有限制的,
本次模版用到的是date,name,thing和phrase,使用方式如下:
- date.DATA:年月日格式(支持+24小时制时间),支持填时间段,两个时间点之间用“~”符号连接,例如:2019年10月1日,或:2019年10月1日 15:01
- name.DATA:10个以内纯汉字或20个以内纯字母或符号,中文名10个汉字内;纯英文名20个字母内;中文和字母混合按中文名算,10个字内
- thing.DATA:20个以内字符,可汉字、数字、字母或符号组合
- phrase.DATA:5个以内汉字,5个以内纯汉字,例如:配送中
对应的json如下:
{
"touser": "OPENID",
"template_id": "TEMPLATE_ID",
"page": "index",
"data": {
"name01": {
"value": "某某"
},
"phrase01": {
"value": "配送中"
},
"thing01": {
"value": "广州至北京"
} ,
"date01": {
"value": "2018-01-01"
}
}
}
返回的errcode也有多种情况,例如前言所说的,43101的错误情况,代表用户没有订阅次数,具体可以参考文档:
https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-message-management/subscribe-message/sendMessage.html
代码处理
一、获取openId
code := "??" // 用户在系统的唯一标识
appId := "??" // 小程序appId
appSecret := "??" // 小程序密钥
url := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", appId, appSecret, code)
resp, err := http.Get(url)
if err != nil {
fmt.Println("失败")
}
body, err := io.ReadAll(resp.Body)
// 解析JSON响应
var response map[string]interface{}
json.Unmarshal([]byte(body), &response)
// 获取openId
openId := response["openid"].(string)
fmt.Println(openId)
二、绑定userId,并存储到数据库
appId := util.WeChatAppId// 小程序appId
appSecret := util.WeChatAppSecret // 小程序密钥
url := "https://api.weixin.qq.com/sns/jscode2session?appid=" + appId + "&secret=" + appSecret + "&js_code=" + req.Code + "&grant_type=authorization_code"
resp, err := http.Get(url)
if err != nil {
return err
}
body, err := io.ReadAll(resp.Body)
// 解析JSON响应
var response map[string]interface{}
json.Unmarshal([]byte(body), &response)
if response["errcode"] != nil && response["errcode"].(float64) != 0 {
return errors.New(response["errmsg"].(string))
}
// 获取openId
openId := response["openid"].(string)
var open adminModels.SysUserWechat
err = e.Orm.Table("sys_user_wechat").
Where("open_id =?", openId).
Where("user_id =?", req.Uid).
First(&open).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
// 如果已经存储过,则不再存储
if open.Id > 0 {
return nil
}
open.OpenId = openId
open.UserId = req.Uid
err = e.Orm.Create(&open).Error
return err
数据库中,SysUserWechat对应的表结构是:
// SysUserWechat 用户微信关联表
type SysUserWechat struct {
models.Model
UserId int `json:"userId" gorm:"column:user_id"`
OpenId string `json:"openId" gorm:"column:open_id"`
}
三、获取accessToken
// 查看redis中是否有access_token
redisClient := GetRedisClient()
ctx := context.Background()
accessToken, _ := redisClient.Get(ctx, "ACCESS_TOKEN_HCM_REPAIR").Result() // 从redis中获取accessToekn
if accessToken == "" {
appId := WeChatAppId
appSecret := WeChatAppSecret
apiURL := "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appId + "&secret=" + appSecret
resp, err := http.Get(apiURL)
if err != nil {
return "", err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var response map[string]interface{}
json.Unmarshal([]byte(body), &response)
accessToken = response["access_token"].(string)
redisClient.Set(ctx, "ACCESS_TOKEN_HCM_REPAIR", accessToken, time.Second*60*60*2) // 过期时间为2小时
四、发送订阅消息
url := "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" + accessToken
message := adminModels.SubscribeMessage{
ToUser: openId, // 接收者(用户)的 openid
TemplateID: templateId, // 所需下发的订阅模板id
MiniprogramState: MiniprogramState, // 订阅消息跳转小程序类型 developer为开发版;trial为体验版;formal为正式版;默认为正式版
Page: Page, // 小程序跳转链接,仅限本小程序内的页面。
Lang: "zh_CN",// 进入小程序查看”的语言类型,支持zh_CN(简体中文)、en_US(英文)、zh_HK(繁体中文)、zh_TW(繁体中文),默认为zh_CN 返回参数
Data: data,// 需要插入的变量值
}
jsonStr, err := json.Marshal(message)
if err != nil {
return err
}
req, err := http.NewRequest("POST", url, strings.NewReader(string(jsonStr)))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
// 解析JSON响应
var response map[string]interface{}
all, _ := io.ReadAll(resp.Body)
json.Unmarshal(all, &response)
if int(response["errcode"].(float64)) != 0 {
log.Printf("errcode: %v, errmsg: %v", response["errcode"], response["errmsg"])
}
return nil
总结
在发送订阅消息时,不同的业务场景需要的代码肯定有所差别,但是万变不离其宗,需要的主要步骤就是上面这些,只要跟着上面的步骤,一步一步来,最后肯定会实现效果。最后祝看到这篇文章的家人们升官发财,万事如意!!
更多推荐
所有评论(0)