Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions miniprogram/message/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,25 @@
DataTypeJSON = "json"
)

const (
//EventSubscribePopup 用户操作订阅通知弹窗事件推送,用户在图文等场景内订阅通知的操作
EventSubscribePopup EventType = "subscribe_msg_popup_event"
//EventSubscribeChange 用户管理订阅通知,用户在服务通知管理页面做通知管理时的操作
EventSubscribeChange = "subscribe_msg_change_event"
//EventSubscribeSent 发送订阅通知,调用 bizsend 接口发送通知
EventSubscribeSent = "subscribe_msg_sent_event"
)

const (
InfoTypeAcceptSubscribeMessage InfoType = "accept"

Check failure on line 41 in miniprogram/message/consts.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.19)

exported const InfoTypeAcceptSubscribeMessage should have comment (or a comment on this block) or be unexported (golint)
InfoTypeRejectSubscribeMessage = "reject"
)

// CommonToken 消息中通用的结构
type CommonToken struct {
XMLName xml.Name `xml:"xml"`
ToUserName string `xml:"ToUserName"`
FromUserName string `xml:"FromUserName"`
ToUserName CDATA `xml:"ToUserName"`
FromUserName CDATA `xml:"FromUserName"`
CreateTime int64 `xml:"CreateTime"`
MsgType MsgType `xml:"MsgType"`
}
Expand Down Expand Up @@ -60,4 +74,36 @@
// 进入会话事件
Event string `xml:"Event"`
SessionFrom string `xml:"SessionFrom"`

// 用户操作订阅通知弹窗消息回调
SubscribeMsgPopupEvent struct {
List []SubscribeMessageList `xml:"List"`
} `xml:"SubscribeMsgPopupEvent"`

// 用户管理订阅通知回调
SubscribeMsgChangeEvent struct {
List []SubscribeMessageList `xml:"List"`
} `xml:"SubscribeMsgChangeEvent"`

// 用户发送订阅通知回调
SubscribeMsgSentEvent struct {
List []SubscribeMessageList `xml:"List"`
} `xml:"SubscribeMsgSentEvent"`
}

// SubscribeMessageList 订阅消息事件列表
type SubscribeMessageList struct {
TemplateId string `xml:"TemplateId"`

Check failure on line 96 in miniprogram/message/consts.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.19)

struct field `TemplateId` should be `TemplateID` (golint)
SubscribeStatusString string `xml:"SubscribeStatusString"`
PopupScene string `xml:"PopupScene"`
}

// EncryptedXMLMsg 安全模式下的消息体
type EncryptedXMLMsg struct {
XMLName struct{} `xml:"xml" json:"-"`
ToUserName string `xml:"ToUserName" json:"toUserName"`
EncryptedMsg string `xml:"Encrypt" json:"Encrypt"`
}

// CDATA 使用这种类型,在序列化 xml 时文本会被解析器忽略
type CDATA string
15 changes: 15 additions & 0 deletions miniprogram/message/reply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package message

import "errors"

//ErrInvalidReply 无效的回复

Check failure on line 5 in miniprogram/message/reply.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.19)

File is not `gofmt`-ed with `-s` (gofmt)
var ErrInvalidReply = errors.New("无效的回复信息")

//ErrUnsupportedReply 不支持的回复类型
var ErrUnsupportedReply = errors.New("不支持的回复消息")

//Reply 消息回复
type Reply struct {
MsgType MsgType
MsgData interface{}
}
10 changes: 10 additions & 0 deletions miniprogram/miniprogram.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package miniprogram

import (

Check failure on line 3 in miniprogram/miniprogram.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.19)

File is not `goimports`-ed (goimports)
"github.com/silenceper/wechat/v2/credential"
"github.com/silenceper/wechat/v2/internal/openapi"
"github.com/silenceper/wechat/v2/miniprogram/analysis"
Expand All @@ -15,6 +15,7 @@
"github.com/silenceper/wechat/v2/miniprogram/order"
"github.com/silenceper/wechat/v2/miniprogram/privacy"
"github.com/silenceper/wechat/v2/miniprogram/qrcode"
"github.com/silenceper/wechat/v2/miniprogram/server"
"github.com/silenceper/wechat/v2/miniprogram/riskcontrol"
"github.com/silenceper/wechat/v2/miniprogram/security"
"github.com/silenceper/wechat/v2/miniprogram/shortlink"
Expand All @@ -24,6 +25,7 @@
"github.com/silenceper/wechat/v2/miniprogram/urlscheme"
"github.com/silenceper/wechat/v2/miniprogram/virtualpayment"
"github.com/silenceper/wechat/v2/miniprogram/werun"
"net/http"
)

// MiniProgram 微信小程序相关 API
Expand Down Expand Up @@ -101,6 +103,14 @@
return werun.NewWeRun(miniProgram.ctx)
}

// GetServer 小程序微信回调处理,接收事件,回复消息管理
func (miniProgram *MiniProgram) GetServer(req *http.Request, write http.ResponseWriter) *server.Server {
srv := server.NewServer(miniProgram.ctx)
srv.Request = req
srv.Write = write
return srv
}

// GetContentSecurity 内容安全接口
func (miniProgram *MiniProgram) GetContentSecurity() *content.Content {
return content.NewContent(miniProgram.ctx)
Expand Down
199 changes: 199 additions & 0 deletions miniprogram/server/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package server

import (
"encoding/xml"
"errors"
"fmt"
"github.com/silenceper/wechat/v2/miniprogram/context"

Check failure on line 7 in miniprogram/server/server.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.19)

File is not `goimports`-ed (goimports)
"github.com/silenceper/wechat/v2/miniprogram/message"
"github.com/silenceper/wechat/v2/util"
"io/ioutil"
"net/http"
"reflect"
"runtime/debug"
"strconv"
)

// Server struct
type Server struct {
*context.Context
Write http.ResponseWriter
Request *http.Request
skipValidate bool
openID string

messageHandler func(mixMessage *message.MiniProgramMixMessage) *message.Reply

RequestRawXMLMsg []byte
RequestMsg *message.MiniProgramMixMessage

ResponseRawXMLMsg []byte
ResponseMsg interface{}

isSafeMode bool
random []byte
nonce string
timestamp int64
}

func NewServer(context *context.Context) *Server {

Check failure on line 39 in miniprogram/server/server.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.19)

exported function `NewServer` should have comment or be unexported (golint)
srv := new(Server)
srv.Context = context
return srv
}

func (srv *Server) Server() error {

Check failure on line 45 in miniprogram/server/server.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.19)

exported method `Server.Server` should have comment or be unexported (golint)
if !srv.Validate() {
return fmt.Errorf("请求签名校验失败")
}
echoStr := srv.Query("echostr")
if echoStr != "" {
srv.SetResponseWrite(echoStr)
return nil
}

response, err := srv.handleRequest()
if err != nil {
return err
}

return srv.buildResponse(response)

}

// SkipValidate 设置跳过签名校验
func (srv *Server) SkipValidate(skip bool) {
srv.skipValidate = skip
}

// Validate 校验请求是否合法
func (srv *Server) Validate() bool {
if srv.skipValidate {
return true
}
timestamp := srv.Query("timestamp")
nonce := srv.Query("nonce")
signature := srv.Query("signature")
return signature == util.Signature(srv.Token, timestamp, nonce)
}

func (srv *Server) handleRequest() (reply *message.Reply, err error) {
//set isSafeMode
srv.isSafeMode = false
encryptType := srv.Query("encrypt_type")
if encryptType == "aes" {
srv.isSafeMode = true
}
//set openID
srv.openID = srv.Query("openid")

var msg interface{}
msg, err = srv.getMessage()
if err != nil {
return
}
mixMessage, success := msg.(*message.MiniProgramMixMessage)
if !success {
err = errors.New("消息类型转换失败")
}
srv.RequestMsg = mixMessage
reply = srv.messageHandler(mixMessage)
return
}

//GetOpenID return openID

Check failure on line 104 in miniprogram/server/server.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.19)

File is not `gofmt`-ed with `-s` (gofmt)
func (srv *Server) GetOpenID() string {
return srv.openID
}

func (srv *Server) getMessage() (interface{}, error) {
var rawXMLMsgBytes []byte
var err error
if srv.isSafeMode {
var encryptedXMLMsg message.EncryptedXMLMsg
if err := xml.NewDecoder(srv.Request.Body).Decode(&encryptedXMLMsg); err != nil {
return nil, fmt.Errorf("从body中解析xml失败,err=%v", err)
}
//验证消息签名
timestamp := srv.Query("timestamp")
srv.timestamp, err = strconv.ParseInt(timestamp, 10, 32)
if err != nil {
return nil, err
}
nonce := srv.Query("nonce")
srv.nonce = nonce
msgSignature := srv.Query("msg_signature")
msgSignatureGen := util.Signature(srv.Token, timestamp, nonce, encryptedXMLMsg.EncryptedMsg)
if msgSignature != msgSignatureGen {
return nil, fmt.Errorf("消息不合法,验证签名失败")
}

// 解密
srv.random, rawXMLMsgBytes, err = util.DecryptMsg(srv.AppID, encryptedXMLMsg.EncryptedMsg, srv.EncodingAESKey)
if err != nil {
return nil, fmt.Errorf("消息解密失败, err=%v", err)
}
} else {
rawXMLMsgBytes, err = ioutil.ReadAll(srv.Request.Body)
if err != nil {
return nil, fmt.Errorf("从body中解析xml失败, err=%v", err)
}
}
srv.RequestRawXMLMsg = rawXMLMsgBytes
return srv.parseRequestMessage(rawXMLMsgBytes)
}

func (srv *Server) parseRequestMessage(rawXMLMsgBytes []byte) (msg *message.MiniProgramMixMessage, err error) {
msg = &message.MiniProgramMixMessage{}
err = xml.Unmarshal(rawXMLMsgBytes, msg)
return
}

func (srv *Server) SetMessageHandler(handler func(*message.MiniProgramMixMessage) *message.Reply) {

Check failure on line 152 in miniprogram/server/server.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.19)

exported method `Server.SetMessageHandler` should have comment or be unexported (golint)
srv.messageHandler = handler
}

func (srv *Server) buildResponse(reply *message.Reply) (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("panic error: %v\n%s", e, debug.Stack())
}
}()
if reply == nil {
return nil
}
msgType := reply.MsgType
switch msgType {
case message.MsgTypeEvent:
case message.MsgTypeImage:
case message.MsgTypeLink:
case message.MsgTypeText:
case message.MsgTypeMiniProgramPage:
default:
err = message.ErrUnsupportedReply
return
}
msgData := reply.MsgData
value := reflect.ValueOf(msgData)
//msgData must be a ptr
kind := value.Kind().String()
if kind != "ptr" {
return message.ErrUnsupportedReply
}
params := make([]reflect.Value, 1)
params[0] = reflect.ValueOf(srv.RequestMsg.FromUserName)
value.MethodByName("SetToUserName").Call(params)

params[0] = reflect.ValueOf(srv.RequestMsg.ToUserName)
value.MethodByName("SetFromUserName").Call(params)

params[0] = reflect.ValueOf(srv.RequestMsg.MsgType)
value.MethodByName("SetMsgType").Call(params)

params[0] = reflect.ValueOf(util.GetCurrTS())
value.MethodByName("SetCreateTime").Call(params)

srv.ResponseMsg = msgData
srv.ResponseRawXMLMsg, err = xml.Marshal(msgData)
return
}
29 changes: 29 additions & 0 deletions miniprogram/server/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package server

import "net/http"

var textContentType = []string{"text/plain; charset=utf-8"}

//Set http response Content-Type

Check failure on line 7 in miniprogram/server/util.go

View workflow job for this annotation

GitHub Actions / golangci-lint (1.19)

File is not `gofmt`-ed with `-s` (gofmt)
func setContentType(w http.ResponseWriter, value []string) {
header := w.Header()
if val := header["Content-Type"]; len(val) == 0 {
header["Content-Type"] = value
}
}

// Query 查询 URL query string
func (srv *Server) Query(key string) string {
req := srv.Request
return req.URL.Query().Get(key)
}

// SetResponseWrite 设置回调返回值
func (srv *Server) SetResponseWrite(str string) {
setContentType(srv.Write, textContentType)
srv.Write.WriteHeader(http.StatusOK)
_, err := srv.Write.Write([]byte(str))
if err != nil {
panic(err)
}
}
Loading