Browse Source

群发消息接口 (#259)

* 添加TODO:待完善接口

* 【模板消息】将message.DataItem改为message.TemplateDataItem

* 【群发消息】基本框架

* 群发消息-基本方法

* fix golint

* fix:SendWxCard log
silenceper 6 years ago
parent
commit
5e8e16444c

+ 14 - 2
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)
 }

+ 1 - 0
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
 

+ 3 - 0
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=

+ 279 - 0
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
+}

+ 10 - 10
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 {

+ 8 - 1
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)
+}

+ 1 - 1
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"`

+ 34 - 0
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
+}

+ 4 - 1
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 第三方平台

+ 9 - 2
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)
 }