Quellcode durchsuchen

Feature: improve code message subscribe (#739)

* fix

* feat(miniapp): 小程序订阅消息 (#429)

1. 用户订阅消息服务端回调处理
2. 用户订阅消息订阅通知事件推送

Co-authored-by: houseme <qzg40737@163.com>

* feat: improve subscribe msg

* feat: add v1.21 and feature branch

* feat: improve code for subscribe

* test

* test

* fix

* fix

* improve comment

* improve code for message

* improve code for unmarshal message

* improve code for message

---------

Co-authored-by: Ralph Maas <stuchilde@outlook.com>
houseme vor 2 Jahren
Ursprung
Commit
56a3734546

+ 4 - 4
.github/workflows/go.yml

@@ -2,15 +2,15 @@ name: Go
 
 on:
   push:
-    branches: [ master,release-*,v2 ]
+    branches: [ master,release-*,v2,feature/** ]
   pull_request:
-    branches: [ master,release-*,v2 ]
+    branches: [ master,release-*,v2,feature/** ]
 
 jobs:
   golangci:
     strategy:
       matrix:
-        go-version: [ '1.16','1.17','1.18','1.19','1.20' ]
+        go-version: [ '1.16','1.17','1.18','1.19','1.20','1.21' ]
     name: golangci-lint
     runs-on: ubuntu-latest
     steps:
@@ -42,7 +42,7 @@ jobs:
     # strategy set
     strategy:
       matrix:
-        go: [ '1.16','1.17','1.18','1.19','1.20' ]
+        go: [ '1.16','1.17','1.18','1.19','1.20','1.21' ]
 
     steps:
       - uses: actions/checkout@v3

+ 10 - 10
domain/openapi/mgnt.go

@@ -2,12 +2,12 @@ package openapi
 
 import "github.com/silenceper/wechat/v2/util"
 
-// GetAPIQuotaParams 查询API调用额度参数
+// GetAPIQuotaParams 查询 API 调用额度参数
 type GetAPIQuotaParams struct {
-	CgiPath string `json:"cgi_path"` // api的请求地址,例如"/cgi-bin/message/custom/send";不要前缀“https://api.weixin.qq.com” ,也不要漏了"/",否则都会76003的报错
+	CgiPath string `json:"cgi_path"` // api 的请求地址,例如"/cgi-bin/message/custom/send";不要前缀“https://api.weixin.qq.com” ,也不要漏了"/",否则都会 76003 的报错
 }
 
-// APIQuota API调用额度
+// APIQuota API 调用额度
 type APIQuota struct {
 	util.CommonError
 	Quota struct {
@@ -17,20 +17,20 @@ type APIQuota struct {
 	} `json:"quota"` // 详情
 }
 
-// GetRidInfoParams 查询rid信息参数
+// GetRidInfoParams 查询 rid 信息参数
 type GetRidInfoParams struct {
-	Rid string `json:"rid"` // 调用接口报错返回的rid
+	Rid string `json:"rid"` // 调用接口报错返回的 rid
 }
 
-// RidInfo rid信息
+// RidInfo rid 信息
 type RidInfo struct {
 	util.CommonError
 	Request struct {
 		InvokeTime   int64  `json:"invoke_time"`   // 发起请求的时间戳
 		CostInMs     int64  `json:"cost_in_ms"`    // 请求毫秒级耗时
-		RequestURL   string `json:"request_url"`   // 请求的URL参数
-		RequestBody  string `json:"request_body"`  // post请求的请求参数
+		RequestURL   string `json:"request_url"`   // 请求的 URL 参数
+		RequestBody  string `json:"request_body"`  // post 请求的请求参数
 		ResponseBody string `json:"response_body"` // 接口请求返回参数
-		ClientIP     string `json:"client_ip"`     // 接口请求的客户端ip
-	} `json:"request"` // 该rid对应的请求详情
+		ClientIP     string `json:"client_ip"`     // 接口请求的客户端 ip
+	} `json:"request"` // 该 rid 对应的请求详情
 }

+ 2 - 2
miniprogram/message/consts.go

@@ -22,9 +22,9 @@ const (
 	MsgTypeMiniProgramPage = "miniprogrampage"
 	// MsgTypeEvent 事件
 	MsgTypeEvent MsgType = "event"
-	// DataTypeXML XML格式数据
+	// DataTypeXML XML 格式数据
 	DataTypeXML = "xml"
-	// DataTypeJSON JSON格式数据
+	// DataTypeJSON JSON 格式数据
 	DataTypeJSON = "json"
 )
 

+ 2 - 2
miniprogram/message/customer_message.go

@@ -28,7 +28,7 @@ type MediaText struct {
 	Content string `json:"content"`
 }
 
-// MediaResource  消息使用的临时素材id
+// MediaResource  消息使用的临时素材 id
 type MediaResource struct {
 	MediaID string `json:"media_id"`
 }
@@ -51,7 +51,7 @@ type MediaLink struct {
 
 // CustomerMessage  客服消息
 type CustomerMessage struct {
-	ToUser          string                `json:"touser"`                    // 接受者OpenID
+	ToUser          string                `json:"touser"`                    // 接受者 OpenID
 	Msgtype         MsgType               `json:"msgtype"`                   // 客服消息类型
 	Text            *MediaText            `json:"text,omitempty"`            // 可选
 	Image           *MediaResource        `json:"image,omitempty"`           // 可选

+ 234 - 30
miniprogram/message/message.go

@@ -9,6 +9,8 @@ import (
 	"sort"
 	"strings"
 
+	"github.com/tidwall/gjson"
+
 	"github.com/silenceper/wechat/v2/miniprogram/context"
 	"github.com/silenceper/wechat/v2/miniprogram/security"
 	"github.com/silenceper/wechat/v2/util"
@@ -18,11 +20,11 @@ import (
 type ConfirmReceiveMethod int8
 
 const (
-	// EventTypeTradeManageRemindAccessAPI 提醒接入发货信息管理服务API
+	// EventTypeTradeManageRemindAccessAPI 提醒接入发货信息管理服务 API
 	// 小程序完成账期授权时/小程序产生第一笔交易时/已产生交易但从未发货的小程序,每天一次
 	EventTypeTradeManageRemindAccessAPI EventType = "trade_manage_remind_access_api"
 	// EventTypeTradeManageRemindShipping 提醒需要上传发货信息
-	// 曾经发过货的小程序,订单超过48小时未发货时
+	// 曾经发过货的小程序,订单超过 48 小时未发货时
 	EventTypeTradeManageRemindShipping EventType = "trade_manage_remind_shipping"
 	// EventTypeTradeManageOrderSettlement 订单将要结算或已经结算
 	// 订单完成发货时/订单结算时
@@ -39,14 +41,27 @@ const (
 	EventTypeXpayGoodsDeliverNotify EventType = "xpay_goods_deliver_notify"
 	// EventTypeXpayCoinPayNotify 代币支付推送事件
 	EventTypeXpayCoinPayNotify EventType = "xpay_coin_pay_notify"
+	// EventSubscribePopup 用户操作订阅通知弹窗事件推送,用户在图文等场景内订阅通知的操作
+	EventSubscribePopup EventType = "subscribe_msg_popup_event"
+	// EventSubscribeMsgChange 用户管理订阅通知,用户在服务通知管理页面做通知管理时的操作
+	EventSubscribeMsgChange EventType = "subscribe_msg_change_event"
+	// EventSubscribeMsgSent 发送订阅通知,调用 bizsend 接口发送通知
+	EventSubscribeMsgSent EventType = "subscribe_msg_sent_event"
 	// ConfirmReceiveMethodAuto 自动确认收货
 	ConfirmReceiveMethodAuto ConfirmReceiveMethod = 1
 	// ConfirmReceiveMethodManual 手动确认收货
 	ConfirmReceiveMethodManual ConfirmReceiveMethod = 2
 )
 
+const (
+	// InfoTypeAcceptSubscribeMessage 接受订阅通知
+	InfoTypeAcceptSubscribeMessage InfoType = "accept"
+	// InfoTypeRejectSubscribeMessage 拒绝订阅通知
+	InfoTypeRejectSubscribeMessage InfoType = "reject"
+)
+
 // PushReceiver 接收消息推送
-// 暂仅支付Aes加密方式
+// 暂仅支付 Aes 加密方式
 type PushReceiver struct {
 	*context.Context
 }
@@ -58,16 +73,16 @@ func NewPushReceiver(ctx *context.Context) *PushReceiver {
 	}
 }
 
-// GetMsg 获取接收到的消息(如果是加密的返回解密数据)
+// GetMsg 获取接收到的消息 (如果是加密的返回解密数据)
 func (receiver *PushReceiver) GetMsg(r *http.Request) (string, []byte, error) {
 	// 判断请求格式
 	var dataType string
 	contentType := r.Header.Get("Content-Type")
 	if strings.HasPrefix(contentType, "text/xml") {
-		// xml格式
+		// xml 格式
 		dataType = DataTypeXML
 	} else {
-		// json格式
+		// json 格式
 		dataType = DataTypeJSON
 	}
 
@@ -108,7 +123,7 @@ func (receiver *PushReceiver) GetMsg(r *http.Request) (string, []byte, error) {
 	return dataType, byteData, err
 }
 
-// GetMsgData 获取接收到的消息(解密数据)
+// GetMsgData 获取接收到的消息 (解密数据)
 func (receiver *PushReceiver) GetMsgData(r *http.Request) (MsgType, EventType, PushData, error) {
 	dataType, decryptMsg, err := receiver.GetMsg(r)
 	if err != nil {
@@ -144,7 +159,7 @@ func (receiver *PushReceiver) GetMsgData(r *http.Request) (MsgType, EventType, P
 func (receiver *PushReceiver) getEvent(dataType string, eventType EventType, decryptMsg []byte) (PushData, error) {
 	switch eventType {
 	case EventTypeTradeManageRemindAccessAPI:
-		// 提醒接入发货信息管理服务API
+		// 提醒接入发货信息管理服务 API
 		var pushData PushDataRemindAccessAPI
 		err := receiver.unmarshal(dataType, decryptMsg, &pushData)
 		return &pushData, err
@@ -188,25 +203,104 @@ func (receiver *PushReceiver) getEvent(dataType string, eventType EventType, dec
 		var pushData PushDataXpayCoinPayNotify
 		err := receiver.unmarshal(dataType, decryptMsg, &pushData)
 		return &pushData, err
+	case EventSubscribePopup:
+		// 用户操作订阅通知弹窗事件推送
+		return receiver.unmarshalSubscribePopup(dataType, decryptMsg)
+	case EventSubscribeMsgChange:
+		// 用户管理订阅通知事件推送
+		return receiver.unmarshalSubscribeMsgChange(dataType, decryptMsg)
+	case EventSubscribeMsgSent:
+		// 用户发送订阅通知事件推送
+		return receiver.unmarshalSubscribeMsgSent(dataType, decryptMsg)
 	}
 	// 暂不支持其他事件类型,直接返回解密后的数据,由调用方处理
 	return decryptMsg, nil
 }
 
 // unmarshal 解析推送的数据
-func (receiver *PushReceiver) unmarshal(dateType string, decryptMsg []byte, pushData interface{}) error {
-	if dateType == DataTypeXML {
+func (receiver *PushReceiver) unmarshal(dataType string, decryptMsg []byte, pushData interface{}) error {
+	if dataType == DataTypeXML {
 		return xml.Unmarshal(decryptMsg, pushData)
 	}
 	return json.Unmarshal(decryptMsg, pushData)
 }
 
+// unmarshalSubscribePopup
+func (receiver *PushReceiver) unmarshalSubscribePopup(dataType string, decryptMsg []byte) (PushData, error) {
+	var pushData PushDataSubscribePopup
+	err := receiver.unmarshal(dataType, decryptMsg, &pushData)
+	if err == nil {
+		listData := gjson.Get(string(decryptMsg), "List")
+		if listData.IsObject() {
+			listItem := SubscribeMsgPopupEventList{}
+			if parseErr := json.Unmarshal([]byte(listData.Raw), &listItem); parseErr != nil {
+				return &pushData, parseErr
+			}
+			pushData.SetSubscribeMsgPopupEvents([]SubscribeMsgPopupEventList{listItem})
+		} else if listData.IsArray() {
+			listItems := make([]SubscribeMsgPopupEventList, 0)
+			if parseErr := json.Unmarshal([]byte(listData.Raw), &listItems); parseErr != nil {
+				return &pushData, parseErr
+			}
+			pushData.SetSubscribeMsgPopupEvents(listItems)
+		}
+	}
+
+	return &pushData, err
+}
+
+// unmarshalSubscribeMsgChange 解析用户管理订阅通知事件推送
+func (receiver *PushReceiver) unmarshalSubscribeMsgChange(dataType string, decryptMsg []byte) (PushData, error) {
+	var pushData PushDataSubscribeMsgChange
+	err := receiver.unmarshal(dataType, decryptMsg, &pushData)
+	if err == nil {
+		listData := gjson.Get(string(decryptMsg), "List")
+		if listData.IsObject() {
+			listItem := SubscribeMsgChangeList{}
+			if parseErr := json.Unmarshal([]byte(listData.Raw), &listItem); parseErr != nil {
+				return &pushData, parseErr
+			}
+			pushData.SetSubscribeMsgChangeEvents([]SubscribeMsgChangeList{listItem})
+		} else if listData.IsArray() {
+			listItems := make([]SubscribeMsgChangeList, 0)
+			if parseErr := json.Unmarshal([]byte(listData.Raw), &listItems); parseErr != nil {
+				return &pushData, parseErr
+			}
+			pushData.SetSubscribeMsgChangeEvents(listItems)
+		}
+	}
+	return &pushData, err
+}
+
+// unmarshalSubscribeMsgSent 解析用户发送订阅通知事件推送
+func (receiver *PushReceiver) unmarshalSubscribeMsgSent(dataType string, decryptMsg []byte) (PushData, error) {
+	var pushData PushDataSubscribeMsgSent
+	err := receiver.unmarshal(dataType, decryptMsg, &pushData)
+	if err == nil {
+		listData := gjson.Get(string(decryptMsg), "List")
+		if listData.IsObject() {
+			listItem := SubscribeMsgSentList{}
+			if parseErr := json.Unmarshal([]byte(listData.Raw), &listItem); parseErr != nil {
+				return &pushData, parseErr
+			}
+			pushData.SetSubscribeMsgSentEvents([]SubscribeMsgSentList{listItem})
+		} else if listData.IsArray() {
+			listItems := make([]SubscribeMsgSentList, 0)
+			if parseErr := json.Unmarshal([]byte(listData.Raw), &listItems); parseErr != nil {
+				return &pushData, parseErr
+			}
+			pushData.SetSubscribeMsgSentEvents(listItems)
+		}
+	}
+	return &pushData, err
+}
+
 // DataReceived 接收到的数据
 type DataReceived struct {
 	Encrypt string `json:"Encrypt" xml:"Encrypt"` // 加密的消息体
 }
 
-// PushData 推送的数据(已转对应的结构体)
+// PushData 推送的数据 (已转对应的结构体)
 type PushData interface{}
 
 // CommonPushData 推送数据通用部分
@@ -216,7 +310,7 @@ type CommonPushData struct {
 	Event        EventType `json:"Event" xml:"Event"`               // 事件类型
 	ToUserName   string    `json:"ToUserName" xml:"ToUserName"`     // 小程序的原始 ID
 	FromUserName string    `json:"FromUserName" xml:"FromUserName"` // 发送方账号(一个 OpenID,此时发送方是系统账号)
-	CreateTime   int64     `json:"CreateTime" xml:"CreateTime"`     // 消息创建时间 (整型),时间戳
+	CreateTime   int64     `json:"CreateTime" xml:"CreateTime"`     // 消息创建时间(整型),时间戳
 }
 
 // MediaCheckAsyncData 媒体内容安全异步审查结果通知
@@ -272,7 +366,7 @@ type PushDataRemindShipping struct {
 	Msg             string `json:"msg" xml:"msg"`                             // 消息文本内容
 }
 
-// PushDataRemindAccessAPI 提醒接入发货信息管理服务API信息
+// PushDataRemindAccessAPI 提醒接入发货信息管理服务 API 信息
 type PushDataRemindAccessAPI struct {
 	CommonPushData
 	Msg string `json:"msg" xml:"msg"` // 消息文本内容
@@ -281,9 +375,9 @@ type PushDataRemindAccessAPI struct {
 // PushDataAddExpressPath 运单轨迹更新信息
 type PushDataAddExpressPath struct {
 	CommonPushData
-	DeliveryID string                          `json:"DeliveryID" xml:"DeliveryID"` // 快递公司ID
-	WayBillID  string                          `json:"WaybillId" xml:"WaybillId"`   // 运单ID
-	OrderID    string                          `json:"OrderId" xml:"OrderId"`       // 订单ID
+	DeliveryID string                          `json:"DeliveryID" xml:"DeliveryID"` // 快递公司 ID
+	WayBillID  string                          `json:"WaybillId" xml:"WaybillId"`   // 运单 ID
+	OrderID    string                          `json:"OrderId" xml:"OrderId"`       // 订单 ID
 	Version    int                             `json:"Version" xml:"Version"`       // 轨迹版本号(整型)
 	Count      int                             `json:"Count" xml:"Count"`           // 轨迹节点数(整型)
 	Actions    []*PushDataAddExpressPathAction `json:"Actions" xml:"Actions"`       // 轨迹节点列表
@@ -304,10 +398,10 @@ type PushDataSecVodUpload struct {
 
 // SecVodUploadEvent 短剧媒资上传完成事件
 type SecVodUploadEvent struct {
-	MediaID       string `json:"media_id" xml:"media_id"`             // 媒资id
+	MediaID       string `json:"media_id" xml:"media_id"`             // 媒资 id
 	SourceContext string `json:"source_context" xml:"source_context"` // 透传上传接口中开发者设置的值。
-	Errcode       int    `json:"errcode" xml:"errcode"`               // 错误码,上传失败时该值非
-	Errmsg        string `json:"errmsg" xml:"errmsg"`                 // 错误提示
+	ErrCode       int    `json:"errcode" xml:"errcode"`               // 错误码,上传失败时该值非
+	ErrMsg        string `json:"errmsg" xml:"errmsg"`                 // 错误提示
 }
 
 // PushDataSecVodAudit 短剧媒资审核状态
@@ -318,14 +412,14 @@ type PushDataSecVodAudit struct {
 
 // SecVodAuditEvent 短剧媒资审核状态事件
 type SecVodAuditEvent struct {
-	DramaID       string           `json:"drama_id" xml:"drama_id"`             // 剧目id
+	DramaID       string           `json:"drama_id" xml:"drama_id"`             // 剧目 id
 	SourceContext string           `json:"source_context" xml:"source_context"` // 透传上传接口中开发者设置的值
-	AuditDetail   DramaAuditDetail `json:"audit_detail" xml:"audit_detail"`     // 剧目审核结果,单独每一集的审核结果可以根据drama_id查询剧集详情得到
+	AuditDetail   DramaAuditDetail `json:"audit_detail" xml:"audit_detail"`     // 剧目审核结果,单独每一集的审核结果可以根据 drama_id 查询剧集详情得到
 }
 
 // DramaAuditDetail 剧目审核结果
 type DramaAuditDetail struct {
-	Status     int   `json:"status" xml:"status"`           // 审核状态,0为无效值;1为审核中;2为最终失败;3为审核通过;4为驳回重填
+	Status     int   `json:"status" xml:"status"`           // 审核状态,0 为无效值;1 为审核中;2 为最终失败;3 为审核通过;4 为驳回重填
 	CreateTime int64 `json:"create_time" xml:"create_time"` // 提审时间戳
 	AuditTime  int64 `json:"audit_time" xml:"audit_time"`   // 审核时间戳
 }
@@ -333,9 +427,9 @@ type DramaAuditDetail struct {
 // PushDataXpayGoodsDeliverNotify 道具发货推送
 type PushDataXpayGoodsDeliverNotify struct {
 	CommonPushData
-	OpenID        string        `json:"OpenId" xml:"OpenId"`               // 用户openid
+	OpenID        string        `json:"OpenId" xml:"OpenId"`               // 用户 openid
 	OutTradeNo    string        `json:"OutTradeNo" xml:"OutTradeNo"`       // 业务订单号
-	Env           int           `json:"Env" xml:"Env"`                     //,环境配置 0:现网环境(也叫正式环境)1:沙箱环境
+	Env           int           `json:"Env" xml:"Env"`                     // ,环境配置 0:现网环境(也叫正式环境)1:沙箱环境
 	WeChatPayInfo WeChatPayInfo `json:"WeChatPayInfo" xml:"WeChatPayInfo"` // 微信支付信息 非微信支付渠道可能没有
 	GoodsInfo     GoodsInfo     `json:"GoodsInfo" xml:"GoodsInfo"`         // 道具参数信息
 }
@@ -344,14 +438,14 @@ type PushDataXpayGoodsDeliverNotify struct {
 type WeChatPayInfo struct {
 	MchOrderNo    string `json:"MchOrderNo" xml:"MchOrderNo"`       // 微信支付商户单号
 	TransactionID string `json:"TransactionId" xml:"TransactionId"` // 交易单号(微信支付订单号)
-	PaidTime      int64  `json:"PaidTime" xml:"PaidTime"`           // 用户支付时间,Linux秒级时间戳
+	PaidTime      int64  `json:"PaidTime" xml:"PaidTime"`           // 用户支付时间,Linux 秒级时间戳
 }
 
 // GoodsInfo 道具参数信息
 type GoodsInfo struct {
-	ProductID   string `json:"ProductId" xml:"ProductId"`     // 道具ID
+	ProductID   string `json:"ProductId" xml:"ProductId"`     // 道具 ID
 	Quantity    int    `json:"Quantity" xml:"Quantity"`       // 数量
-	OrigPrice   int64  `json:"OrigPrice" xml:"OrigPrice"`     // 物品原始价格 (单位:分)
+	OrigPrice   int64  `json:"OrigPrice" xml:"OrigPrice"`     // 物品原始价格(单位:分)
 	ActualPrice int64  `json:"ActualPrice" xml:"ActualPrice"` // 物品实际支付价格(单位:分)
 	Attach      string `json:"Attach" xml:"Attach"`           // 透传信息
 }
@@ -359,9 +453,9 @@ type GoodsInfo struct {
 // PushDataXpayCoinPayNotify 代币支付推送
 type PushDataXpayCoinPayNotify struct {
 	CommonPushData
-	OpenID        string        `json:"OpenId" xml:"OpenId"`               // 用户openid
+	OpenID        string        `json:"OpenId" xml:"OpenId"`               // 用户 openid
 	OutTradeNo    string        `json:"OutTradeNo" xml:"OutTradeNo"`       // 业务订单号
-	Env           int           `json:"Env" xml:"Env"`                     //,环境配置 0:现网环境(也叫正式环境)1:沙箱环境
+	Env           int           `json:"Env" xml:"Env"`                     // ,环境配置 0:现网环境(也叫正式环境)1:沙箱环境
 	WeChatPayInfo WeChatPayInfo `json:"WeChatPayInfo" xml:"WeChatPayInfo"` // 微信支付信息 非微信支付渠道可能没有
 	CoinInfo      CoinInfo      `json:"CoinInfo" xml:"CoinInfo"`           // 代币参数信息
 }
@@ -369,7 +463,117 @@ type PushDataXpayCoinPayNotify struct {
 // CoinInfo 代币参数信息
 type CoinInfo struct {
 	Quantity    int    `json:"Quantity" xml:"Quantity"`       // 数量
-	OrigPrice   int64  `json:"OrigPrice" xml:"OrigPrice"`     // 物品原始价格 (单位:分)
+	OrigPrice   int64  `json:"OrigPrice" xml:"OrigPrice"`     // 物品原始价格(单位:分)
 	ActualPrice int64  `json:"ActualPrice" xml:"ActualPrice"` // 物品实际支付价格(单位:分)
 	Attach      string `json:"Attach" xml:"Attach"`           // 透传信息
 }
+
+// PushDataSubscribePopup 用户操作订阅通知弹窗事件推送
+type PushDataSubscribePopup struct {
+	CommonPushData
+	subscribeMsgPopupEventList []SubscribeMsgPopupEventList `json:"-"`
+	SubscribeMsgPopupEvent     SubscribeMsgPopupEvent       `xml:"SubscribeMsgPopupEvent"`
+}
+
+// SubscribeMsgPopupEvent 用户操作订阅通知弹窗消息回调
+type SubscribeMsgPopupEvent struct {
+	List []SubscribeMsgPopupEventList `xml:"List"`
+}
+
+// SubscribeMsgPopupEventList 订阅消息事件列表
+type SubscribeMsgPopupEventList struct {
+	TemplateID            string `xml:"TemplateId" json:"TemplateId"`
+	SubscribeStatusString string `xml:"SubscribeStatusString" json:"SubscribeStatusString"`
+	PopupScene            string `xml:"PopupScene" json:"PopupScene"`
+}
+
+// SetSubscribeMsgPopupEvents 设置订阅消息事件
+func (s *PushDataSubscribePopup) SetSubscribeMsgPopupEvents(list []SubscribeMsgPopupEventList) {
+	s.subscribeMsgPopupEventList = list
+}
+
+// GetSubscribeMsgPopupEvents 获取订阅消息事件数据
+func (s *PushDataSubscribePopup) GetSubscribeMsgPopupEvents() []SubscribeMsgPopupEventList {
+	if s.subscribeMsgPopupEventList != nil {
+		return s.subscribeMsgPopupEventList
+	}
+
+	if s.SubscribeMsgPopupEvent.List == nil || len(s.SubscribeMsgPopupEvent.List) < 1 {
+		return nil
+	}
+	return s.SubscribeMsgPopupEvent.List
+}
+
+// PushDataSubscribeMsgChange 用户管理订阅通知事件推送
+type PushDataSubscribeMsgChange struct {
+	CommonPushData
+	SubscribeMsgChangeEvent SubscribeMsgChangeEvent  `xml:"SubscribeMsgChangeEvent"`
+	subscribeMsgChangeList  []SubscribeMsgChangeList `json:"-"`
+}
+
+// SubscribeMsgChangeEvent 用户管理订阅通知回调
+type SubscribeMsgChangeEvent struct {
+	List []SubscribeMsgChangeList `xml:"List" json:"List"`
+}
+
+// SubscribeMsgChangeList 订阅消息事件列表
+type SubscribeMsgChangeList struct {
+	TemplateID            string `xml:"TemplateId" json:"TemplateId"`
+	SubscribeStatusString string `xml:"SubscribeStatusString" json:"SubscribeStatusString"`
+}
+
+// SetSubscribeMsgChangeEvents 设置订阅消息事件
+func (s *PushDataSubscribeMsgChange) SetSubscribeMsgChangeEvents(list []SubscribeMsgChangeList) {
+	s.subscribeMsgChangeList = list
+}
+
+// GetSubscribeMsgChangeEvents 获取订阅消息事件数据
+func (s *PushDataSubscribeMsgChange) GetSubscribeMsgChangeEvents() []SubscribeMsgChangeList {
+	if s.subscribeMsgChangeList != nil {
+		return s.subscribeMsgChangeList
+	}
+
+	if s.SubscribeMsgChangeEvent.List == nil || len(s.SubscribeMsgChangeEvent.List) < 1 {
+		return nil
+	}
+
+	return s.SubscribeMsgChangeEvent.List
+}
+
+// PushDataSubscribeMsgSent 用户发送订阅通知事件推送
+type PushDataSubscribeMsgSent struct {
+	CommonPushData
+	SubscribeMsgSentEvent     SubscribeMsgSentEvent  `xml:"SubscribeMsgSentEvent"`
+	subscribeMsgSentEventList []SubscribeMsgSentList `json:"-"`
+}
+
+// SubscribeMsgSentEvent 用户发送订阅通知回调
+type SubscribeMsgSentEvent struct {
+	List []SubscribeMsgSentList `xml:"List" json:"List"`
+}
+
+// SubscribeMsgSentList 订阅消息事件列表
+type SubscribeMsgSentList struct {
+	TemplateID  string `xml:"TemplateId" json:"TemplateId"`
+	MsgID       string `xml:"MsgID" json:"MsgID"`
+	ErrorCode   int    `xml:"ErrorCode" json:"ErrorCode"`
+	ErrorStatus string `xml:"ErrorStatus" json:"ErrorStatus"`
+}
+
+// SetSubscribeMsgSentEvents 设置订阅消息事件
+func (s *PushDataSubscribeMsgSent) SetSubscribeMsgSentEvents(list []SubscribeMsgSentList) {
+	s.subscribeMsgSentEventList = list
+}
+
+// GetSubscribeMsgSentEvents 获取订阅消息事件数据
+func (s *PushDataSubscribeMsgSent) GetSubscribeMsgSentEvents() []SubscribeMsgSentList {
+	if s.subscribeMsgSentEventList != nil {
+		return s.subscribeMsgSentEventList
+	}
+
+	if s.SubscribeMsgSentEvent.List == nil || len(s.SubscribeMsgSentEvent.List) < 1 {
+		return nil
+	}
+
+	return s.SubscribeMsgSentEvent.List
+}

+ 15 - 0
miniprogram/message/reply.go

@@ -0,0 +1,15 @@
+package message
+
+import "errors"
+
+// ErrInvalidReply 无效的回复
+var ErrInvalidReply = errors.New("无效的回复信息")
+
+// ErrUnsupportedReply 不支持的回复类型
+var ErrUnsupportedReply = errors.New("不支持的回复消息")
+
+// Reply 消息回复
+type Reply struct {
+	MsgType MsgType
+	MsgData interface{}
+}

+ 22 - 22
officialaccount/message/message.go

@@ -27,15 +27,15 @@ const (
 	MsgTypeVideo MsgType = "video"
 	// MsgTypeMiniprogrampage 表示小程序卡片消息
 	MsgTypeMiniprogrampage MsgType = "miniprogrampage"
-	// MsgTypeShortVideo 表示短视频消息[限接收]
+	// MsgTypeShortVideo 表示短视频消息 [限接收]
 	MsgTypeShortVideo MsgType = "shortvideo"
-	// MsgTypeLocation 表示坐标消息[限接收]
+	// MsgTypeLocation 表示坐标消息 [限接收]
 	MsgTypeLocation MsgType = "location"
-	// MsgTypeLink 表示链接消息[限接收]
+	// MsgTypeLink 表示链接消息 [限接收]
 	MsgTypeLink MsgType = "link"
-	// MsgTypeMusic 表示音乐消息[限回复]
+	// MsgTypeMusic 表示音乐消息 [限回复]
 	MsgTypeMusic MsgType = "music"
-	// MsgTypeNews 表示图文消息[限回复]
+	// MsgTypeNews 表示图文消息 [限回复]
 	MsgTypeNews MsgType = "news"
 	// MsgTypeTransfer 表示消息消息转发到客服
 	MsgTypeTransfer MsgType = "transfer_customer_service"
@@ -91,7 +91,7 @@ const (
 const (
 	// 微信开放平台需要用到
 
-	// InfoTypeVerifyTicket 返回ticket
+	// InfoTypeVerifyTicket 返回 ticket
 	InfoTypeVerifyTicket InfoType = "component_verify_ticket"
 	// InfoTypeAuthorized 授权
 	InfoTypeAuthorized InfoType = "authorized"
@@ -108,8 +108,8 @@ type MixMessage struct {
 	CommonToken
 
 	// 基本消息
-	MsgID         int64   `xml:"MsgId"` // 其他消息推送过来是MsgId
-	TemplateMsgID int64   `xml:"MsgID"` // 模板消息推送成功的消息是MsgID
+	MsgID         int64   `xml:"MsgId"` // 其他消息推送过来是 MsgId
+	TemplateMsgID int64   `xml:"MsgID"` // 模板消息推送成功的消息是 MsgID
 	Content       string  `xml:"Content"`
 	Recognition   string  `xml:"Recognition"`
 	PicURL        string  `xml:"PicUrl"`
@@ -166,17 +166,17 @@ type MixMessage struct {
 
 	// 事件相关:发布能力
 	PublishEventInfo struct {
-		PublishID     int64                     `xml:"publish_id"`     // 发布任务id
+		PublishID     int64                     `xml:"publish_id"`     // 发布任务 id
 		PublishStatus freepublish.PublishStatus `xml:"publish_status"` // 发布状态
-		ArticleID     string                    `xml:"article_id"`     // 当发布状态为0时(即成功)时,返回图文的 article_id,可用于“客服消息”场景
+		ArticleID     string                    `xml:"article_id"`     // 当发布状态为 0 时(即成功)时,返回图文的 article_id,可用于“客服消息”场景
 		ArticleDetail struct {
 			Count uint `xml:"count"` // 文章数量
 			Item  []struct {
 				Index      uint   `xml:"idx"`         // 文章对应的编号
 				ArticleURL string `xml:"article_url"` // 图文的永久链接
 			} `xml:"item"`
-		} `xml:"article_detail"` // 当发布状态为0时(即成功)时,返回内容
-		FailIndex []uint `xml:"fail_idx"` // 当发布状态为2或4时,返回不通过的文章编号,第一篇为 1;其他发布状态则为空
+		} `xml:"article_detail"` // 当发布状态为 0 时(即成功)时,返回内容
+		FailIndex []uint `xml:"fail_idx"` // 当发布状态为 2  4 时,返回不通过的文章编号,第一篇为 1;其他发布状态则为空
 	} `xml:"PublishEventInfo"`
 
 	// 第三方平台相关
@@ -222,19 +222,19 @@ type MixMessage struct {
 	TraceID       string `xml:"trace_id"`
 	StatusCode    int    `xml:"status_code"`
 
-	//小程序名称审核结果事件推送
-	Ret      int32  `xml:"ret"`      //审核结果 2:失败,3:成功
-	NickName string `xml:"nickname"` //小程序昵称
+	// 小程序名称审核结果事件推送
+	Ret      int32  `xml:"ret"`      // 审核结果 2:失败,3:成功
+	NickName string `xml:"nickname"` // 小程序昵称
 
 	// 设备相关
 	device.MsgDevice
 
-	//小程序审核通知
-	SuccTime   int    `xml:"SuccTime"`   //审核成功时的时间戳
-	FailTime   int    `xml:"FailTime"`   //审核不通过的时间戳
-	DelayTime  int    `xml:"DelayTime"`  //审核延后时的时间戳
-	Reason     string `xml:"Reason"`     //审核不通过的原因
-	ScreenShot string `xml:"ScreenShot"` //审核不通过的截图示例。用 | 分隔的 media_id 的列表,可通过获取永久素材接口拉取截图内容
+	// 小程序审核通知
+	SuccTime   int    `xml:"SuccTime"`   // 审核成功时的时间戳
+	FailTime   int    `xml:"FailTime"`   // 审核不通过的时间戳
+	DelayTime  int    `xml:"DelayTime"`  // 审核延后时的时间戳
+	Reason     string `xml:"Reason"`     // 审核不通过的原因
+	ScreenShot string `xml:"ScreenShot"` // 审核不通过的截图示例。用 | 分隔的 media_id 的列表,可通过获取永久素材接口拉取截图内容
 }
 
 // SubscribeMsgPopupEvent 订阅通知事件推送的消息体
@@ -282,7 +282,7 @@ type ResponseEncryptedXMLMsg struct {
 	Nonce        string   `xml:"Nonce"        json:"Nonce"`
 }
 
-// CDATA  使用该类型,在序列化为 xml 文本时文本会被解析器忽略
+// CDATA  使用该类型在序列化为 xml 文本时文本会被解析器忽略
 type CDATA string
 
 // MarshalXML 实现自己的序列化方法

+ 3 - 3
officialaccount/server/server.go

@@ -73,7 +73,7 @@ func (srv *Server) Serve() error {
 	if err != nil {
 		return err
 	}
-	// 非安全模式下,请求处理方法返回为nil则直接回复success给微信服务器
+	// 非安全模式下,请求处理方法返回为 nil 则直接回复 success 给微信服务器
 	if response == nil && !srv.isSafeMode {
 		srv.String("success")
 		return nil
@@ -198,7 +198,7 @@ func (srv *Server) parseRequestMessage(rawXMLMsgBytes []byte) (msg *message.MixM
 	if err != nil {
 		return
 	}
-	// nonstandard json, 目前小程序订阅消息返回数据格式不标准,订阅消息模板单个List返回是对象,多个List返回是数组。
+	// nonstandard json, 目前小程序订阅消息返回数据格式不标准,订阅消息模板单个 List 返回是对象,多个 List 返回是数组。
 	if msg.MsgType == message.MsgTypeEvent {
 		listData := gjson.Get(string(rawXMLMsgBytes), "List")
 		if listData.IsObject() {
@@ -284,7 +284,7 @@ func (srv *Server) Send() (err error) {
 		if err != nil {
 			return
 		}
-		// TODO 如果获取不到timestamp nonce 则自己生成
+		// TODO 如果获取不到 timestamp nonce 则自己生成
 		timestamp := srv.timestamp
 		timestampStr := strconv.FormatInt(timestamp, 10)
 		msgSignature := util.Signature(srv.Token, timestampStr, srv.nonce, string(encryptedMsg))