mirror of
https://github.com/silenceper/wechat.git
synced 2026-02-04 12:52:27 +08:00
add: 增加公众号「发布能力」接口(#530) (#531)
* add: 增加公众号「发布能力」接口(#530) * fix: go lint * fix: 修复响应结构体及解析方式 * add: 增加公众号「草稿箱」接口(#530) * fix: text * mod: 增加 发布任务完成 消息事件 Co-authored-by: luoyu <luoyu@medlinker.com>
This commit is contained in:
228
officialaccount/draft/draft.go
Normal file
228
officialaccount/draft/draft.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package draft
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
addURL = "https://api.weixin.qq.com/cgi-bin/draft/add" // 新建草稿
|
||||
getURL = "https://api.weixin.qq.com/cgi-bin/draft/get" // 获取草稿
|
||||
deleteURL = "https://api.weixin.qq.com/cgi-bin/draft/delete" // 删除草稿
|
||||
updateURL = "https://api.weixin.qq.com/cgi-bin/draft/update" // 修改草稿
|
||||
countURL = "https://api.weixin.qq.com/cgi-bin/draft/count" // 获取草稿总数
|
||||
paginateURL = "https://api.weixin.qq.com/cgi-bin/draft/batchget" // 获取草稿列表
|
||||
)
|
||||
|
||||
// Draft 草稿箱
|
||||
type Draft struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewDraft init
|
||||
func NewDraft(ctx *context.Context) *Draft {
|
||||
return &Draft{
|
||||
Context: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// Article 草稿
|
||||
type Article struct {
|
||||
Title string `json:"title"` // 标题
|
||||
Author string `json:"author"` // 作者
|
||||
Digest string `json:"digest"` // 图文消息的摘要,仅有单图文消息才有摘要,多图文此处为空。
|
||||
Content string `json:"content"` // 图文消息的具体内容,支持HTML标签,必须少于2万字符,小于1M,且去除JS
|
||||
ContentSourceURL string `json:"content_source_url"` // 图文消息的原文地址,即点击“阅读原文”后的URL
|
||||
ThumbMediaID string `json:"thumb_media_id"` // 图文消息的封面图片素材id(必须是永久MediaID)
|
||||
ShowCoverPic uint `json:"show_cover_pic"` // 是否显示封面,0为false,即不显示,1为true,即显示(默认)
|
||||
NeedOpenComment uint `json:"need_open_comment"` // 是否打开评论,0不打开(默认),1打开
|
||||
OnlyFansCanComment uint `json:"only_fans_can_comment"` // 是否粉丝才可评论,0所有人可评论(默认),1粉丝才可评论
|
||||
}
|
||||
|
||||
// AddDraft 新建草稿
|
||||
func (draft *Draft) AddDraft(articles []*Article) (mediaID string, err error) {
|
||||
accessToken, err := draft.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Articles []*Article `json:"articles"`
|
||||
}
|
||||
req.Articles = articles
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", addURL, accessToken)
|
||||
response, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var res struct {
|
||||
util.CommonError
|
||||
MediaID string `json:"media_id"`
|
||||
}
|
||||
err = util.DecodeWithError(response, &res, "AddDraft")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
mediaID = res.MediaID
|
||||
return
|
||||
}
|
||||
|
||||
// GetDraft 获取草稿
|
||||
func (draft *Draft) GetDraft(mediaID string) (articles []*Article, err error) {
|
||||
accessToken, err := draft.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
MediaID string `json:"media_id"`
|
||||
}
|
||||
req.MediaID = mediaID
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getURL, accessToken)
|
||||
response, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var res struct {
|
||||
util.CommonError
|
||||
NewsItem []*Article `json:"news_item"`
|
||||
}
|
||||
err = util.DecodeWithError(response, &res, "GetDraft")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
articles = res.NewsItem
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteDraft 删除草稿
|
||||
func (draft *Draft) DeleteDraft(mediaID string) (err error) {
|
||||
accessToken, err := draft.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
MediaID string `json:"media_id"`
|
||||
}
|
||||
req.MediaID = mediaID
|
||||
|
||||
var response []byte
|
||||
uri := fmt.Sprintf("%s?access_token=%s", deleteURL, accessToken)
|
||||
response, err = util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithCommonError(response, "DeleteDraft")
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateDraft 修改草稿
|
||||
// index 要更新的文章在图文消息中的位置(多图文消息时,此字段才有意义),第一篇为0
|
||||
func (draft *Draft) UpdateDraft(article *Article, mediaID string, index uint) (err error) {
|
||||
accessToken, err := draft.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
MediaID string `json:"media_id"`
|
||||
Index uint `json:"index"`
|
||||
Article *Article `json:"articles"`
|
||||
}
|
||||
req.MediaID = mediaID
|
||||
req.Index = index
|
||||
req.Article = article
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", updateURL, accessToken)
|
||||
var response []byte
|
||||
response, err = util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithCommonError(response, "UpdateDraft")
|
||||
return
|
||||
}
|
||||
|
||||
// CountDraft 获取草稿总数
|
||||
func (draft *Draft) CountDraft() (total uint, err error) {
|
||||
accessToken, err := draft.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
uri := fmt.Sprintf("%s?access_token=%s", countURL, accessToken)
|
||||
response, err = util.HTTPGet(uri)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var res struct {
|
||||
util.CommonError
|
||||
Total uint `json:"total_count"`
|
||||
}
|
||||
err = util.DecodeWithError(response, &res, "CountDraft")
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
total = res.Total
|
||||
return
|
||||
}
|
||||
|
||||
// ArticleList 草稿列表
|
||||
type ArticleList struct {
|
||||
util.CommonError
|
||||
TotalCount int64 `json:"total_count"` // 草稿素材的总数
|
||||
ItemCount int64 `json:"item_count"` // 本次调用获取的素材的数量
|
||||
Item []ArticleListItem `json:"item"`
|
||||
}
|
||||
|
||||
// ArticleListItem 用于 ArticleList 的 item 节点
|
||||
type ArticleListItem struct {
|
||||
MediaID string `json:"media_id"` // 图文消息的id
|
||||
Content ArticleListContent `json:"content"` // 内容
|
||||
UpdateTime int64 `json:"update_time"` // 这篇图文消息素材的最后更新时间
|
||||
}
|
||||
|
||||
// ArticleListContent 用于 ArticleListItem 的 content 节点
|
||||
type ArticleListContent struct {
|
||||
NewsItem []Article `json:"news_item"` // 这篇图文消息素材的内容
|
||||
}
|
||||
|
||||
// PaginateDraft 获取草稿列表
|
||||
func (draft *Draft) PaginateDraft(offset, count int64, noReturnContent bool) (list ArticleList, err error) {
|
||||
accessToken, err := draft.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Count int64 `json:"count"`
|
||||
Offset int64 `json:"offset"`
|
||||
NoReturnContent bool `json:"no_content"`
|
||||
}
|
||||
req.Count = count
|
||||
req.Offset = offset
|
||||
req.NoReturnContent = noReturnContent
|
||||
|
||||
var response []byte
|
||||
uri := fmt.Sprintf("%s?access_token=%s", paginateURL, accessToken)
|
||||
response, err = util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &list, "PaginateDraft")
|
||||
return
|
||||
}
|
||||
248
officialaccount/freepublish/freepublish.go
Normal file
248
officialaccount/freepublish/freepublish.go
Normal file
@@ -0,0 +1,248 @@
|
||||
package freepublish
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
publishURL = "https://api.weixin.qq.com/cgi-bin/freepublish/submit" // 发布接口
|
||||
selectStateURL = "https://api.weixin.qq.com/cgi-bin/freepublish/get" // 发布状态轮询接口
|
||||
deleteURL = "https://api.weixin.qq.com/cgi-bin/freepublish/delete" // 删除发布
|
||||
firstArticleURL = "https://api.weixin.qq.com/cgi-bin/freepublish/getarticle" // 通过 article_id 获取已发布文章
|
||||
paginateURL = "https://api.weixin.qq.com/cgi-bin/freepublish/batchget" // 获取成功发布列表
|
||||
)
|
||||
|
||||
// PublishStatus 发布状态
|
||||
type PublishStatus uint
|
||||
|
||||
const (
|
||||
// PublishStatusSuccess 0:成功
|
||||
PublishStatusSuccess PublishStatus = iota
|
||||
// PublishStatusPublishing 1:发布中
|
||||
PublishStatusPublishing
|
||||
// PublishStatusOriginalFail 2:原创失败
|
||||
PublishStatusOriginalFail
|
||||
// PublishStatusFail 3:常规失败
|
||||
PublishStatusFail
|
||||
// PublishStatusAuditRefused 4:平台审核不通过
|
||||
PublishStatusAuditRefused
|
||||
// PublishStatusUserDeleted 5:成功后用户删除所有文章
|
||||
PublishStatusUserDeleted
|
||||
// PublishStatusSystemBanned 6:成功后系统封禁所有文章
|
||||
PublishStatusSystemBanned
|
||||
)
|
||||
|
||||
// FreePublish 发布能力
|
||||
type FreePublish struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewFreePublish init
|
||||
func NewFreePublish(ctx *context.Context) *FreePublish {
|
||||
return &FreePublish{
|
||||
Context: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// Publish 发布接口。需要先将图文素材以草稿的形式保存(见“草稿箱/新建草稿”,
|
||||
// 如需从已保存的草稿中选择,见“草稿箱/获取草稿列表”),选择要发布的草稿 media_id 进行发布
|
||||
func (freePublish *FreePublish) Publish(mediaID string) (publishID int64, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = freePublish.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
MediaID string `json:"media_id"`
|
||||
}
|
||||
req.MediaID = mediaID
|
||||
|
||||
var response []byte
|
||||
uri := fmt.Sprintf("%s?access_token=%s", publishURL, accessToken)
|
||||
response, err = util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var res struct {
|
||||
util.CommonError
|
||||
PublishID int64 `json:"publish_id"`
|
||||
}
|
||||
err = util.DecodeWithError(response, &res, "SubmitFreePublish")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
publishID = res.PublishID
|
||||
return
|
||||
}
|
||||
|
||||
// PublishStatusList 发布任务状态列表
|
||||
type PublishStatusList struct {
|
||||
util.CommonError
|
||||
PublishID int64 `json:"publish_id"` // 发布任务id
|
||||
PublishStatus PublishStatus `json:"publish_status"` // 发布状态
|
||||
ArticleID string `json:"article_id"` // 当发布状态为0时(即成功)时,返回图文的 article_id,可用于“客服消息”场景
|
||||
ArticleDetail PublishArticleDetail `json:"article_detail"` // 发布任务文章成功状态详情
|
||||
FailIndex []uint `json:"fail_idx"` // 当发布状态为2或4时,返回不通过的文章编号,第一篇为 1;其他发布状态则为空
|
||||
}
|
||||
|
||||
// PublishArticleDetail 发布任务成功详情
|
||||
type PublishArticleDetail struct {
|
||||
Count uint `json:"count"` // 当发布状态为0时(即成功)时,返回文章数量
|
||||
Items []PublishArticleItem `json:"item"`
|
||||
}
|
||||
|
||||
// PublishArticleItem 发布任务成功的文章内容
|
||||
type PublishArticleItem struct {
|
||||
Index uint `json:"idx"` // 当发布状态为0时(即成功)时,返回文章对应的编号
|
||||
ArticleURL string `json:"article_url"` // 当发布状态为0时(即成功)时,返回图文的永久链接
|
||||
}
|
||||
|
||||
// SelectStatus 发布状态轮询接口
|
||||
func (freePublish *FreePublish) SelectStatus(publishID int64) (list PublishStatusList, err error) {
|
||||
accessToken, err := freePublish.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
PublishID int64 `json:"publish_id"`
|
||||
}
|
||||
req.PublishID = publishID
|
||||
|
||||
var response []byte
|
||||
uri := fmt.Sprintf("%s?access_token=%s", selectStateURL, accessToken)
|
||||
response, err = util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &list, "SelectStatusFreePublish")
|
||||
return
|
||||
}
|
||||
|
||||
// Delete 删除发布。
|
||||
// index 要删除的文章在图文消息中的位置,第一篇编号为1,该字段不填或填0会删除全部文章
|
||||
// !!!此操作不可逆,请谨慎操作!!!删除后微信公众号后台仍然会有记录!!!
|
||||
func (freePublish *FreePublish) Delete(articleID string, index uint) (err error) {
|
||||
accessToken, err := freePublish.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req struct {
|
||||
ArticleID string `json:"article_id"`
|
||||
Index uint `json:"index"`
|
||||
}
|
||||
req.ArticleID = articleID
|
||||
req.Index = index
|
||||
|
||||
var response []byte
|
||||
uri := fmt.Sprintf("%s?access_token=%s", deleteURL, accessToken)
|
||||
response, err = util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.DecodeWithCommonError(response, "DeleteFreePublish")
|
||||
}
|
||||
|
||||
// Article 图文信息内容
|
||||
type Article struct {
|
||||
Title string `json:"title"` // 标题
|
||||
Author string `json:"author"` // 作者
|
||||
Digest string `json:"digest"` // 图文消息的摘要,仅有单图文消息才有摘要,多图文此处为空
|
||||
Content string `json:"content"` // 图文消息的具体内容,支持HTML标签,必须少于2万字符,小于1M,且此处会去除JS
|
||||
ContentSourceURL string `json:"content_source_url"` // 图文消息的原文地址,即点击“阅读原文”后的URL
|
||||
ThumbMediaID string `json:"thumb_media_id"` // 图文消息的封面图片素材id(一定是永久MediaID)
|
||||
ShowCoverPic uint `json:"show_cover_pic"` // 是否显示封面,0为false,即不显示,1为true,即显示(默认)
|
||||
NeedOpenComment uint `json:"need_open_comment"` // 是否打开评论,0不打开(默认),1打开
|
||||
OnlyFansCanComment uint `json:"only_fans_can_comment"` // 是否粉丝才可评论,0所有人可评论(默认),1粉丝才可评论
|
||||
URL string `json:"url"` // 图文消息的URL
|
||||
IsDeleted bool `json:"is_deleted"` // 该图文是否被删除
|
||||
}
|
||||
|
||||
// First 通过 article_id 获取已发布文章
|
||||
func (freePublish *FreePublish) First(articleID string) (list []Article, err error) {
|
||||
accessToken, err := freePublish.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
ArticleID string `json:"article_id"`
|
||||
}
|
||||
req.ArticleID = articleID
|
||||
|
||||
var response []byte
|
||||
uri := fmt.Sprintf("%s?access_token=%s", firstArticleURL, accessToken)
|
||||
response, err = util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var res struct {
|
||||
util.CommonError
|
||||
NewsItem []Article `json:"news_item"`
|
||||
}
|
||||
err = util.DecodeWithError(response, &res, "FirstFreePublish")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
list = res.NewsItem
|
||||
return
|
||||
}
|
||||
|
||||
// ArticleList 发布列表
|
||||
type ArticleList struct {
|
||||
util.CommonError
|
||||
TotalCount int64 `json:"total_count"` // 成功发布素材的总数
|
||||
ItemCount int64 `json:"item_count"` // 本次调用获取的素材的数量
|
||||
Item []ArticleListItem `json:"item"`
|
||||
}
|
||||
|
||||
// ArticleListItem 用于 ArticleList 的 item 节点
|
||||
type ArticleListItem struct {
|
||||
ArticleID string `json:"article_id"` // 成功发布的图文消息id
|
||||
Content ArticleListContent `json:"content"` // 内容
|
||||
UpdateTime int64 `json:"update_time"` // 这篇图文消息素材的最后更新时间
|
||||
}
|
||||
|
||||
// ArticleListContent 用于 ArticleListItem 的 content 节点
|
||||
type ArticleListContent struct {
|
||||
NewsItem []Article `json:"news_item"` // 这篇图文消息素材的内容
|
||||
}
|
||||
|
||||
// Paginate 获取成功发布列表
|
||||
func (freePublish *FreePublish) Paginate(offset, count int64, noReturnContent bool) (list ArticleList, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = freePublish.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Count int64 `json:"count"`
|
||||
Offset int64 `json:"offset"`
|
||||
NoReturnContent bool `json:"no_content"`
|
||||
}
|
||||
req.Count = count
|
||||
req.Offset = offset
|
||||
req.NoReturnContent = noReturnContent
|
||||
|
||||
var response []byte
|
||||
uri := fmt.Sprintf("%s?access_token=%s", paginateURL, accessToken)
|
||||
response, err = util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &list, "PaginateFreePublish")
|
||||
return
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/xml"
|
||||
|
||||
"github.com/silenceper/wechat/v2/officialaccount/device"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/freepublish"
|
||||
)
|
||||
|
||||
// MsgType 基本消息类型
|
||||
@@ -75,6 +76,8 @@ const (
|
||||
EventWxaMediaCheck EventType = "wxa_media_check"
|
||||
// EventSubscribeMsgPopupEvent 订阅通知事件推送
|
||||
EventSubscribeMsgPopupEvent EventType = "subscribe_msg_popup_event"
|
||||
// EventPublishJobFinish 发布任务完成
|
||||
EventPublishJobFinish EventType = "PUBLISHJOBFINISH"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -150,6 +153,21 @@ type MixMessage struct {
|
||||
List SubscribeMsgPopupEvent `xml:"List"`
|
||||
} `xml:"SubscribeMsgPopupEvent"`
|
||||
|
||||
// 事件相关:发布能力
|
||||
PublishEventInfo struct {
|
||||
PublishID int64 `xml:"publish_id"` // 发布任务id
|
||||
PublishStatus freepublish.PublishStatus `xml:"publish_status"` // 发布状态
|
||||
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:"PublishEventInfo"`
|
||||
|
||||
// 第三方平台相关
|
||||
InfoType InfoType `xml:"InfoType"`
|
||||
AppID string `xml:"AppId"`
|
||||
|
||||
@@ -3,6 +3,8 @@ package officialaccount
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/silenceper/wechat/v2/officialaccount/draft"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/freepublish"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/ocr"
|
||||
|
||||
"github.com/silenceper/wechat/v2/officialaccount/datacube"
|
||||
@@ -80,6 +82,16 @@ func (officialAccount *OfficialAccount) GetMaterial() *material.Material {
|
||||
return material.NewMaterial(officialAccount.ctx)
|
||||
}
|
||||
|
||||
// GetDraft 草稿箱
|
||||
func (officialAccount *OfficialAccount) GetDraft() *draft.Draft {
|
||||
return draft.NewDraft(officialAccount.ctx)
|
||||
}
|
||||
|
||||
// GetFreePublish 发布能力
|
||||
func (officialAccount *OfficialAccount) GetFreePublish() *freepublish.FreePublish {
|
||||
return freepublish.NewFreePublish(officialAccount.ctx)
|
||||
}
|
||||
|
||||
// GetJs js-sdk配置
|
||||
func (officialAccount *OfficialAccount) GetJs() *js.Js {
|
||||
return js.NewJs(officialAccount.ctx)
|
||||
|
||||
Reference in New Issue
Block a user