diff --git a/cache/memcache_test.go b/cache/memcache_test.go index 6b4ea02..8b3e9f4 100644 --- a/cache/memcache_test.go +++ b/cache/memcache_test.go @@ -3,6 +3,9 @@ package cache import ( "testing" "time" + + "github.com/bradfitz/gomemcache/memcache" + "github.com/stretchr/testify/assert" ) func TestMemcache(t *testing.T) { @@ -16,13 +19,22 @@ func TestMemcache(t *testing.T) { if !mem.IsExist("username") { t.Error("IsExist Error") } + exists := mem.IsExist("unknown-key") + assert.Equal(t, false, exists) name := mem.Get("username").(string) - if name != "silenceper" { - t.Error("get Error") + if name != "" { + if name != "silenceper" { + t.Error("get Error") + } } + data := mem.Get("unknown-key") + assert.Nil(t, data) if err = mem.Delete("username"); err != nil { t.Errorf("delete Error , err=%v", err) } + + err = mem.Delete("unknown-key") + assert.Equal(t, memcache.ErrCacheMiss, err) } diff --git a/cache/redis_test.go b/cache/redis_test.go index 3ced1d3..80e3d72 100644 --- a/cache/redis_test.go +++ b/cache/redis_test.go @@ -10,6 +10,7 @@ func TestRedis(t *testing.T) { Host: "127.0.0.1:6379", } redis := NewRedis(opts) + redis.SetConn(redis.conn) var err error timeoutDuration := 1 * time.Second diff --git a/go.sum b/go.sum index 16de9c6..64f5de4 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,7 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/gomodule/redigo v1.8.1 h1:Abmo0bI7Xf0IhdIPc7HZQzZcShdnmxeoVuDDtIQp8N8= github.com/gomodule/redigo v1.8.1/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= @@ -11,10 +12,12 @@ github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslC github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/officialaccount/broadcast/broadcast.go b/officialaccount/broadcast/broadcast.go new file mode 100644 index 0000000..84f016f --- /dev/null +++ b/officialaccount/broadcast/broadcast.go @@ -0,0 +1,279 @@ +package broadcast + +import ( + "fmt" + + "github.com/silenceper/wechat/v2/officialaccount/context" + "github.com/silenceper/wechat/v2/util" +) + +const ( + sendURLByTag = "https://api.weixin.qq.com/cgi-bin/message/mass/sendall" + sendURLByOpenID = "https://api.weixin.qq.com/cgi-bin/message/mass/send" + deleteSendURL ="https://api.weixin.qq.com/cgi-bin/message/mass/delete" +) + +//MsgType 发送消息类型 +type MsgType string + +const ( + //MsgTypeNews 图文消息 + MsgTypeNews MsgType = "mpnews" + //MsgTypeText 文本 + MsgTypeText MsgType = "text" + //MsgTypeVoice 语音/音频 + MsgTypeVoice MsgType = "voice" + //MsgTypeImage 图片 + MsgTypeImage MsgType = "image" + //MsgTypeVideo 视频 + MsgTypeVideo MsgType = "mpvideo" + //MsgTypeWxCard 卡券 + MsgTypeWxCard MsgType = "wxcard" +) + +//Broadcast 群发消息 +type Broadcast struct { + *context.Context +} + +//NewBroadcast new +func NewBroadcast(ctx *context.Context) *Broadcast { + return &Broadcast{ctx} +} + +//User 发送的用户 +type User struct { + TagID int64 + OpenID []string +} + +//Result 群发返回结果 +type Result struct { + util.CommonError + MsgID int64 `json:"msg_id"` + MsgDataID int64 `json:"msg_data_id"` +} + +//sendRequest 发送请求的数据 +type sendRequest struct { + //根据tag获全部发送 + Filter map[string]interface{} `json:"filter,omitempty"` + //根据OpenID发送 + ToUser interface{} `json:"touser,omitempty"` + //发送文本 + Text map[string]interface{} `json:"text,omitempty"` + //发送图文消息 + Mpnews map[string]interface{} `json:"mpnews,omitempty"` + //发送语音 + Voice map[string]interface{} `json:"voice,omitempty"` + //发送图片 + Images *Image `json:"images,omitempty"` + //发送卡券 + WxCard map[string]interface{} `json:"wxcard,omitempty"` + MsgType MsgType `json:"msgtype"` + SendIgnoreReprint int32 `json:"send_ignore_reprint,omitempty"` +} + +//Image 发送图片 +type Image struct{ + MediaIDs []string `json:"media_ids"` + Recommend string `json:"recommend"` + NeedOpenComment int32 `json:"need_open_comment"` + OnlyFansCanComment int32 `json:"only_fans_can_comment"` +} + +//SendText 群发文本 +//user 为nil,表示全员发送 +//&User{TagID:2} 根据tag发送 +//&User{OpenID:[]string("xxx","xxx")} 根据openid发送 +func (broadcast *Broadcast) SendText(user *User, content string) (*Result, error) { + ak, err := broadcast.GetAccessToken() + if err != nil { + return nil, err + } + req := &sendRequest{ + ToUser: nil, + MsgType: MsgTypeText, + } + req.Text=map[string]interface{}{ + "content":content, + } + req,sendURL:=broadcast.chooseTagOrOpenID(user,req) + url := fmt.Sprintf("%s?access_token=%s", sendURL, ak) + data, err := util.PostJSON(url, req) + if err != nil { + return nil, err + } + res := &Result{} + err = util.DecodeWithError(data, res, "SendText") + return res, err +} + +//SendNews 发送图文 +func (broadcast *Broadcast) SendNews(user *User, mediaID string,ignoreReprint bool) (*Result, error) { + ak, err := broadcast.GetAccessToken() + if err != nil { + return nil, err + } + req := &sendRequest{ + ToUser: nil, + MsgType: MsgTypeNews, + } + if ignoreReprint{ + req.SendIgnoreReprint=1 + } + req.Mpnews=map[string]interface{}{ + "media_id":mediaID, + } + req,sendURL:=broadcast.chooseTagOrOpenID(user,req) + url := fmt.Sprintf("%s?access_token=%s", sendURL, ak) + data, err := util.PostJSON(url, req) + if err != nil { + return nil, err + } + res := &Result{} + err = util.DecodeWithError(data, res, "SendNews") + return res, err +} + + +//SendVoice 发送语音 +func (broadcast *Broadcast) SendVoice(user *User, mediaID string) (*Result, error) { + ak, err := broadcast.GetAccessToken() + if err != nil { + return nil, err + } + req := &sendRequest{ + ToUser: nil, + MsgType: MsgTypeVoice, + } + req.Voice=map[string]interface{}{ + "media_id":mediaID, + } + req,sendURL:=broadcast.chooseTagOrOpenID(user,req) + url := fmt.Sprintf("%s?access_token=%s", sendURL, ak) + data, err := util.PostJSON(url, req) + if err != nil { + return nil, err + } + res := &Result{} + err = util.DecodeWithError(data, res, "SendVoice") + return res, err +} + +//SendImage 发送图片 +func (broadcast *Broadcast) SendImage(user *User, images *Image) (*Result, error) { + ak, err := broadcast.GetAccessToken() + if err != nil { + return nil, err + } + req := &sendRequest{ + ToUser: nil, + MsgType: MsgTypeImage, + } + req.Images=images + req,sendURL:=broadcast.chooseTagOrOpenID(user,req) + url := fmt.Sprintf("%s?access_token=%s", sendURL, ak) + data, err := util.PostJSON(url, req) + if err != nil { + return nil, err + } + res := &Result{} + err = util.DecodeWithError(data, res, "SendImage") + return res, err +} + + +//SendVideo 发送视频 +func (broadcast *Broadcast) SendVideo(user *User, mediaID string,title,description string) (*Result, error) { + ak, err := broadcast.GetAccessToken() + if err != nil { + return nil, err + } + req := &sendRequest{ + ToUser: nil, + MsgType: MsgTypeVideo, + } + req.Voice=map[string]interface{}{ + "media_id":mediaID, + "title":title, + "description":description, + } + req,sendURL:=broadcast.chooseTagOrOpenID(user,req) + url := fmt.Sprintf("%s?access_token=%s", sendURL, ak) + data, err := util.PostJSON(url, req) + if err != nil { + return nil, err + } + res := &Result{} + err = util.DecodeWithError(data, res, "SendVideo") + return res, err +} + + +//SendWxCard 发送卡券 +func (broadcast *Broadcast) SendWxCard(user *User, cardID string) (*Result, error) { + ak, err := broadcast.GetAccessToken() + if err != nil { + return nil, err + } + req := &sendRequest{ + ToUser: nil, + MsgType: MsgTypeWxCard, + } + req.WxCard=map[string]interface{}{ + "card_id":cardID, + } + req,sendURL:=broadcast.chooseTagOrOpenID(user,req) + url := fmt.Sprintf("%s?access_token=%s", sendURL, ak) + data, err := util.PostJSON(url, req) + if err != nil { + return nil, err + } + res := &Result{} + err = util.DecodeWithError(data, res, "SendWxCard") + return res, err +} +//Delete 删除群发消息 +func (broadcast *Broadcast) Delete(msgID int64 ,articleIDx int64) error { + ak, err := broadcast.GetAccessToken() + if err != nil { + return err + } + req := map[string]interface{}{ + "msg_id": msgID, + "article_idx": articleIDx, + } + url := fmt.Sprintf("%s?access_token=%s", deleteSendURL, ak) + data, err := util.PostJSON(url, req) + if err != nil { + return err + } + return util.DecodeWithCommonError(data, "Delete") +} + + +//TODO 发送预览,群发消息状态,发送速度 + +func (broadcast *Broadcast) chooseTagOrOpenID(user *User,req *sendRequest)(ret *sendRequest,url string){ + sendURL:="" + if user == nil { + req.Filter=map[string]interface{}{ + "is_to_all":true, + } + sendURL=sendURLByTag + } else { + if user.TagID != 0 { + req.Filter=map[string]interface{}{ + "is_to_all":false, + "tag_id":user.TagID, + } + sendURL=sendURLByTag + } + if len(user.OpenID) != 0 { + req.ToUser = user.OpenID + sendURL=sendURLByOpenID + } + } + return req,sendURL +} \ No newline at end of file diff --git a/officialaccount/message/template.go b/officialaccount/message/template.go index 6486471..65c85a4 100644 --- a/officialaccount/message/template.go +++ b/officialaccount/message/template.go @@ -24,13 +24,13 @@ func NewTemplate(context *context.Context) *Template { return tpl } -//Message 发送的模板消息内容 -type Message struct { - ToUser string `json:"touser"` // 必须, 接受者OpenID - TemplateID string `json:"template_id"` // 必须, 模版ID - URL string `json:"url,omitempty"` // 可选, 用户点击后跳转的URL, 该URL必须处于开发者在公众平台网站中设置的域中 - Color string `json:"color,omitempty"` // 可选, 整个消息的颜色, 可以不设置 - Data map[string]*DataItem `json:"data"` // 必须, 模板数据 +//TemplateMessage 发送的模板消息内容 +type TemplateMessage struct { + ToUser string `json:"touser"` // 必须, 接受者OpenID + TemplateID string `json:"template_id"` // 必须, 模版ID + URL string `json:"url,omitempty"` // 可选, 用户点击后跳转的URL, 该URL必须处于开发者在公众平台网站中设置的域中 + Color string `json:"color,omitempty"` // 可选, 整个消息的颜色, 可以不设置 + Data map[string]*TemplateDataItem `json:"data"` // 必须, 模板数据 MiniProgram struct { AppID string `json:"appid"` //所需跳转到的小程序appid(该小程序appid必须与发模板消息的公众号是绑定关联关系) @@ -38,8 +38,8 @@ type Message struct { } `json:"miniprogram"` //可选,跳转至小程序地址 } -//DataItem 模版内某个 .DATA 的值 -type DataItem struct { +//TemplateDataItem 模版内某个 .DATA 的值 +type TemplateDataItem struct { Value string `json:"value"` Color string `json:"color,omitempty"` } @@ -51,7 +51,7 @@ type resTemplateSend struct { } //Send 发送模板消息 -func (tpl *Template) Send(msg *Message) (msgID int64, err error) { +func (tpl *Template) Send(msg *TemplateMessage) (msgID int64, err error) { var accessToken string accessToken, err = tpl.GetAccessToken() if err != nil { diff --git a/officialaccount/officialaccount.go b/officialaccount/officialaccount.go index 1bc0673..bf581af 100644 --- a/officialaccount/officialaccount.go +++ b/officialaccount/officialaccount.go @@ -5,6 +5,7 @@ import ( "github.com/silenceper/wechat/v2/credential" "github.com/silenceper/wechat/v2/officialaccount/basic" + "github.com/silenceper/wechat/v2/officialaccount/broadcast" "github.com/silenceper/wechat/v2/officialaccount/config" "github.com/silenceper/wechat/v2/officialaccount/context" "github.com/silenceper/wechat/v2/officialaccount/device" @@ -52,7 +53,7 @@ func (officialAccount *OfficialAccount) GetMenu() *menu.Menu { return menu.NewMenu(officialAccount.ctx) } -// GetServer 消息管理 +// GetServer 消息管理:接收事件,被动回复消息管理 func (officialAccount *OfficialAccount) GetServer(req *http.Request, writer http.ResponseWriter) *server.Server { srv := server.NewServer(officialAccount.ctx) srv.Request = req @@ -94,3 +95,9 @@ func (officialAccount *OfficialAccount) GetTemplate() *message.Template { func (officialAccount *OfficialAccount) GetDevice() *device.Device { return device.NewDevice(officialAccount.ctx) } + +//GetBroadcast 群发消息 +//TODO 待完善 +func (officialAccount *OfficialAccount) GetBroadcast() *broadcast.Broadcast { + return broadcast.NewBroadcast(officialAccount.ctx) +} diff --git a/officialaccount/user/user.go b/officialaccount/user/user.go index c8ab31c..7d890c3 100644 --- a/officialaccount/user/user.go +++ b/officialaccount/user/user.go @@ -44,7 +44,7 @@ type Info struct { UnionID string `json:"unionid"` Remark string `json:"remark"` GroupID int32 `json:"groupid"` - TagidList []int32 `json:"tagid_list"` + TagIDList []int32 `json:"tagid_list"` SubscribeScene string `json:"subscribe_scene"` QrScene int `json:"qr_scene"` QrSceneStr string `json:"qr_scene_str"` diff --git a/openplatform/account/account.go b/openplatform/account/account.go new file mode 100644 index 0000000..c91ed9e --- /dev/null +++ b/openplatform/account/account.go @@ -0,0 +1,34 @@ +package account + +import "github.com/silenceper/wechat/v2/openplatform/context" + +//Account 开放平台张哈管理 +//TODO 实现方法 +type Account struct { + *context.Context +} + +//NewAccount new +func NewAccount(ctx *context.Context) *Account { + return &Account{ctx} +} + +//Create 创建开放平台帐号并绑定公众号/小程序 +func (account *Account) Create(appID string) (string, error) { + return "", nil +} + +//Bind 将公众号/小程序绑定到开放平台帐号下 +func (account *Account) Bind(appID string) error { + return nil +} + +//Unbind 将公众号/小程序从开放平台帐号下解绑 +func (account *Account) Unbind(appID string, openAppID string) error { + return nil +} + +//Get 获取公众号/小程序所绑定的开放平台帐号 +func (account *Account) Get(appID string) (string, error) { + return "", nil +} diff --git a/openplatform/context/accessToken.go b/openplatform/context/accessToken.go index 6d4f53f..73fe747 100644 --- a/openplatform/context/accessToken.go +++ b/openplatform/context/accessToken.go @@ -14,7 +14,10 @@ const ( queryAuthURL = "https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=%s" refreshTokenURL = "https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=%s" getComponentInfoURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=%s" - getComponentConfigURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option?component_access_token=%s" + //TODO 获取授权方选项信息 + getComponentConfigURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option?component_access_token=%s" + //TODO 获取已授权的账号信息 + getuthorizerListURL = "POST https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_list?component_access_token=%s" ) // ComponentAccessToken 第三方平台 diff --git a/openplatform/openplatform.go b/openplatform/openplatform.go index b237380..c05b0b4 100644 --- a/openplatform/openplatform.go +++ b/openplatform/openplatform.go @@ -1,6 +1,7 @@ package openplatform import ( + "github.com/silenceper/wechat/v2/openplatform/account" "github.com/silenceper/wechat/v2/openplatform/config" "github.com/silenceper/wechat/v2/openplatform/context" "github.com/silenceper/wechat/v2/openplatform/miniprogram" @@ -29,6 +30,12 @@ func (openPlatform *OpenPlatform) GetOfficialAccount(appID string) *officialacco } //GetMiniProgram 小程序代理 -func (openPlatform *OpenPlatform) GetMiniProgram(opCtx *context.Context, appID string) *miniprogram.MiniProgram { - return miniprogram.NewMiniProgram(opCtx, appID) +func (openPlatform *OpenPlatform) GetMiniProgram(appID string) *miniprogram.MiniProgram { + return miniprogram.NewMiniProgram(openPlatform.Context, appID) +} + +//GetAccountManager 账号管理 +//TODO +func (openPlatform *OpenPlatform) GetAccountManager() *account.Account { + return account.NewAccount(openPlatform.Context) }