golang:后端实现微信订阅消息发送功能

前言

在日常开发中,难免会遇到需要微信订阅消息发送的需求,大部分的消息订阅发送使用的模板都是一次性发送的模板(因为长期订阅的消息模板申请时,需要服务性质是政务民生、医疗、交通、金融、教育等线下公共服务),而一次性的消息模板需要每次发送订阅消息后,用户再次订阅。我的办法是用户每次进入特定的页面后,让用户再次订阅。如下
在这里插入图片描述
用户有了订阅次数,会将订阅次数和对应的模板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”接口了,需要注意的是有几个必传的参数:

  1. access_token
  2. touser:接收者(用户)的 openid
  3. template_id: 所需下发的订阅模板id
  4. page:小程序跳转链接,仅限本小程序内的页面。
  5. miniprogram_state:订阅消息跳转小程序类型 developer为开发版;trial为体验版;formal为正式版;默认为正式版
  6. lang:进入小程序查看”的语言类型,支持zh_CN(简体中文)、en_US(英文)、zh_HK(繁体中文)、zh_TW(繁体中文),默认为zh_CN 返回参数
  7. data:模板内容,格式形如 { “key1”: { “value”: any }, “key2”: { “value”: any } }的object

尤其是page字段需要注意,该字段不传,则模板无跳转,我就在上面吃过亏!!!虽然默认跳转到首页,但还是需要传这个字段,那怕只传一个空字符,否则现象如下:
在这里插入图片描述

传参时需要注意,不同的类型传入的参数是有限制的,
本次模版用到的是date,name,thing和phrase,使用方式如下:

  1. date.DATA:年月日格式(支持+24小时制时间),支持填时间段,两个时间点之间用“~”符号连接,例如:2019年10月1日,或:2019年10月1日 15:01
  2. name.DATA:10个以内纯汉字或20个以内纯字母或符号,中文名10个汉字内;纯英文名20个字母内;中文和字母混合按中文名算,10个字内
  3. thing.DATA:20个以内字符,可汉字、数字、字母或符号组合
  4. 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

总结

在发送订阅消息时,不同的业务场景需要的代码肯定有所差别,但是万变不离其宗,需要的主要步骤就是上面这些,只要跟着上面的步骤,一步一步来,最后肯定会实现效果。最后祝看到这篇文章的家人们升官发财,万事如意!!

  • 19
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值