1
0
mirror of https://github.com/silenceper/wechat.git synced 2026-02-04 12:52:27 +08:00

群发消息接口 (#259)

* 添加TODO:待完善接口

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

* 【群发消息】基本框架

* 群发消息-基本方法

* fix golint

* fix:SendWxCard log
This commit is contained in:
silenceper
2020-05-29 23:17:04 +08:00
committed by GitHub
parent 880ab20a6b
commit 5e8e16444c
10 changed files with 363 additions and 17 deletions

View File

@@ -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
cache/redis_test.go vendored
View File

@@ -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
go.sum
View File

@@ -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=

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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"`

View File

@@ -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
}

View File

@@ -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 第三方平台

View File

@@ -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)
}