material.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. package material
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "os"
  8. "github.com/silenceper/wechat/v2/officialaccount/context"
  9. "github.com/silenceper/wechat/v2/util"
  10. )
  11. const (
  12. addNewsURL = "https://api.weixin.qq.com/cgi-bin/material/add_news"
  13. updateNewsURL = "https://api.weixin.qq.com/cgi-bin/material/update_news"
  14. addMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/add_material"
  15. delMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/del_material"
  16. getMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/get_material"
  17. getMaterialCountURL = "https://api.weixin.qq.com/cgi-bin/material/get_materialcount"
  18. batchGetMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/batchget_material"
  19. )
  20. // PermanentMaterialType 永久素材类型
  21. type PermanentMaterialType string
  22. const (
  23. // PermanentMaterialTypeImage 永久素材图片类型(image)
  24. PermanentMaterialTypeImage PermanentMaterialType = "image"
  25. // PermanentMaterialTypeVideo 永久素材视频类型(video)
  26. PermanentMaterialTypeVideo PermanentMaterialType = "video"
  27. // PermanentMaterialTypeVoice 永久素材语音类型(voice)
  28. PermanentMaterialTypeVoice PermanentMaterialType = "voice"
  29. // PermanentMaterialTypeNews 永久素材图文类型(news)
  30. PermanentMaterialTypeNews PermanentMaterialType = "news"
  31. )
  32. // Material 素材管理
  33. type Material struct {
  34. *context.Context
  35. }
  36. // NewMaterial init
  37. func NewMaterial(context *context.Context) *Material {
  38. material := new(Material)
  39. material.Context = context
  40. return material
  41. }
  42. // Article 永久图文素材
  43. type Article struct {
  44. Title string `json:"title"`
  45. ThumbMediaID string `json:"thumb_media_id"`
  46. ThumbURL string `json:"thumb_url"`
  47. Author string `json:"author"`
  48. Digest string `json:"digest"`
  49. ShowCoverPic int `json:"show_cover_pic"`
  50. Content string `json:"content"`
  51. ContentSourceURL string `json:"content_source_url"`
  52. URL string `json:"url"`
  53. DownURL string `json:"down_url"`
  54. }
  55. // GetNews 获取/下载永久素材
  56. func (material *Material) GetNews(id string) ([]*Article, error) {
  57. accessToken, err := material.GetAccessToken()
  58. if err != nil {
  59. return nil, err
  60. }
  61. uri := fmt.Sprintf("%s?access_token=%s", getMaterialURL, accessToken)
  62. var req struct {
  63. MediaID string `json:"media_id"`
  64. }
  65. req.MediaID = id
  66. responseBytes, err := util.PostJSON(uri, req)
  67. if err != nil {
  68. return nil, err
  69. }
  70. var res struct {
  71. NewsItem []*Article `json:"news_item"`
  72. }
  73. err = json.Unmarshal(responseBytes, &res)
  74. if err != nil {
  75. return nil, err
  76. }
  77. return res.NewsItem, nil
  78. }
  79. // reqArticles 永久性图文素材请求信息
  80. type reqArticles struct {
  81. Articles []*Article `json:"articles"`
  82. }
  83. // resArticles 永久性图文素材返回结果
  84. type resArticles struct {
  85. util.CommonError
  86. MediaID string `json:"media_id"`
  87. }
  88. // AddNews 新增永久图文素材
  89. func (material *Material) AddNews(articles []*Article) (mediaID string, err error) {
  90. req := &reqArticles{articles}
  91. var accessToken string
  92. accessToken, err = material.GetAccessToken()
  93. if err != nil {
  94. return
  95. }
  96. uri := fmt.Sprintf("%s?access_token=%s", addNewsURL, accessToken)
  97. responseBytes, err := util.PostJSON(uri, req)
  98. if err != nil {
  99. return
  100. }
  101. var res resArticles
  102. err = json.Unmarshal(responseBytes, &res)
  103. if err != nil {
  104. return
  105. }
  106. if res.ErrCode != 0 {
  107. return "", fmt.Errorf("errcode=%d,errmsg=%s", res.ErrCode, res.ErrMsg)
  108. }
  109. mediaID = res.MediaID
  110. return
  111. }
  112. // reqUpdateArticle 更新永久性图文素材请求信息
  113. type reqUpdateArticle struct {
  114. MediaID string `json:"media_id"`
  115. Index int64 `json:"index"`
  116. Articles *Article `json:"articles"`
  117. }
  118. // UpdateNews 更新永久图文素材
  119. func (material *Material) UpdateNews(article *Article, mediaID string, index int64) (err error) {
  120. req := &reqUpdateArticle{mediaID, index, article}
  121. var accessToken string
  122. accessToken, err = material.GetAccessToken()
  123. if err != nil {
  124. return
  125. }
  126. uri := fmt.Sprintf("%s?access_token=%s", updateNewsURL, accessToken)
  127. var response []byte
  128. response, err = util.PostJSON(uri, req)
  129. if err != nil {
  130. return
  131. }
  132. return util.DecodeWithCommonError(response, "UpdateNews")
  133. }
  134. // resAddMaterial 永久性素材上传返回的结果
  135. type resAddMaterial struct {
  136. util.CommonError
  137. MediaID string `json:"media_id"`
  138. URL string `json:"url"`
  139. }
  140. // AddMaterialFromReader 上传永久性素材(处理视频需要单独上传),从 io.Reader 中读取
  141. func (material *Material) AddMaterialFromReader(mediaType MediaType, filename string, reader io.Reader) (mediaID string, url string, err error) {
  142. if mediaType == MediaTypeVideo {
  143. err = errors.New("永久视频素材上传使用 AddVideo 方法")
  144. return
  145. }
  146. var accessToken string
  147. accessToken, err = material.GetAccessToken()
  148. if err != nil {
  149. return
  150. }
  151. uri := fmt.Sprintf("%s?access_token=%s&type=%s", addMaterialURL, accessToken, mediaType)
  152. var response []byte
  153. response, err = util.PostFileFromReader("media", filename, uri, reader)
  154. if err != nil {
  155. return
  156. }
  157. var resMaterial resAddMaterial
  158. err = json.Unmarshal(response, &resMaterial)
  159. if err != nil {
  160. return
  161. }
  162. if resMaterial.ErrCode != 0 {
  163. err = fmt.Errorf("AddMaterial error : errcode=%v , errmsg=%v", resMaterial.ErrCode, resMaterial.ErrMsg)
  164. return
  165. }
  166. mediaID = resMaterial.MediaID
  167. url = resMaterial.URL
  168. return
  169. }
  170. // AddMaterial 上传永久性素材(处理视频需要单独上传)
  171. func (material *Material) AddMaterial(mediaType MediaType, filename string) (mediaID string, url string, err error) {
  172. f, err := os.Open(filename)
  173. if err != nil {
  174. return
  175. }
  176. defer func() { _ = f.Close() }()
  177. return material.AddMaterialFromReader(mediaType, filename, f)
  178. }
  179. type reqVideo struct {
  180. Title string `json:"title"`
  181. Introduction string `json:"introduction"`
  182. }
  183. // AddVideoFromReader 永久视频素材文件上传,从 io.Reader 中读取
  184. func (material *Material) AddVideoFromReader(filename, title, introduction string, reader io.Reader) (mediaID string, url string, err error) {
  185. var accessToken string
  186. accessToken, err = material.GetAccessToken()
  187. if err != nil {
  188. return
  189. }
  190. uri := fmt.Sprintf("%s?access_token=%s&type=video", addMaterialURL, accessToken)
  191. videoDesc := &reqVideo{
  192. Title: title,
  193. Introduction: introduction,
  194. }
  195. var fieldValue []byte
  196. fieldValue, err = json.Marshal(videoDesc)
  197. if err != nil {
  198. return
  199. }
  200. fields := []util.MultipartFormField{
  201. {
  202. IsFile: true,
  203. Fieldname: "media",
  204. Filename: filename,
  205. FileReader: reader,
  206. },
  207. {
  208. IsFile: false,
  209. Fieldname: "description",
  210. Value: fieldValue,
  211. },
  212. }
  213. var response []byte
  214. response, err = util.PostMultipartForm(fields, uri)
  215. if err != nil {
  216. return
  217. }
  218. var resMaterial resAddMaterial
  219. err = json.Unmarshal(response, &resMaterial)
  220. if err != nil {
  221. return
  222. }
  223. if resMaterial.ErrCode != 0 {
  224. err = fmt.Errorf("AddMaterial error : errcode=%v , errmsg=%v", resMaterial.ErrCode, resMaterial.ErrMsg)
  225. return
  226. }
  227. mediaID = resMaterial.MediaID
  228. url = resMaterial.URL
  229. return
  230. }
  231. // AddVideo 永久视频素材文件上传
  232. func (material *Material) AddVideo(filename, title, introduction string) (mediaID string, url string, err error) {
  233. f, err := os.Open(filename)
  234. if err != nil {
  235. return "", "", err
  236. }
  237. defer func() { _ = f.Close() }()
  238. return material.AddVideoFromReader(filename, title, introduction, f)
  239. }
  240. type reqDeleteMaterial struct {
  241. MediaID string `json:"media_id"`
  242. }
  243. // DeleteMaterial 删除永久素材
  244. func (material *Material) DeleteMaterial(mediaID string) error {
  245. accessToken, err := material.GetAccessToken()
  246. if err != nil {
  247. return err
  248. }
  249. uri := fmt.Sprintf("%s?access_token=%s", delMaterialURL, accessToken)
  250. response, err := util.PostJSON(uri, reqDeleteMaterial{mediaID})
  251. if err != nil {
  252. return err
  253. }
  254. return util.DecodeWithCommonError(response, "DeleteMaterial")
  255. }
  256. // ArticleList 永久素材列表
  257. type ArticleList struct {
  258. util.CommonError
  259. TotalCount int64 `json:"total_count"`
  260. ItemCount int64 `json:"item_count"`
  261. Item []ArticleListItem `json:"item"`
  262. }
  263. // ArticleListItem 用于 ArticleList 的 item 节点
  264. type ArticleListItem struct {
  265. MediaID string `json:"media_id"`
  266. Content ArticleListContent `json:"content"`
  267. Name string `json:"name"`
  268. URL string `json:"url"`
  269. UpdateTime int64 `json:"update_time"`
  270. }
  271. // ArticleListContent 用于 ArticleListItem 的 content 节点
  272. type ArticleListContent struct {
  273. NewsItem []Article `json:"news_item"`
  274. UpdateTime int64 `json:"update_time"`
  275. CreateTime int64 `json:"create_time"`
  276. }
  277. // reqBatchGetMaterial BatchGetMaterial 请求参数
  278. type reqBatchGetMaterial struct {
  279. Type PermanentMaterialType `json:"type"`
  280. Count int64 `json:"count"`
  281. Offset int64 `json:"offset"`
  282. }
  283. // BatchGetMaterial 批量获取永久素材
  284. //
  285. //reference:https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Get_materials_list.html
  286. func (material *Material) BatchGetMaterial(permanentMaterialType PermanentMaterialType, offset, count int64) (list ArticleList, err error) {
  287. var accessToken string
  288. accessToken, err = material.GetAccessToken()
  289. if err != nil {
  290. return
  291. }
  292. uri := fmt.Sprintf("%s?access_token=%s", batchGetMaterialURL, accessToken)
  293. req := reqBatchGetMaterial{
  294. Type: permanentMaterialType,
  295. Offset: offset,
  296. Count: count,
  297. }
  298. var response []byte
  299. response, err = util.PostJSON(uri, req)
  300. if err != nil {
  301. return
  302. }
  303. err = util.DecodeWithError(response, &list, "BatchGetMaterial")
  304. return
  305. }
  306. // ResMaterialCount 素材总数
  307. type ResMaterialCount struct {
  308. util.CommonError
  309. VoiceCount int64 `json:"voice_count"` // 语音总数量
  310. VideoCount int64 `json:"video_count"` // 视频总数量
  311. ImageCount int64 `json:"image_count"` // 图片总数量
  312. NewsCount int64 `json:"news_count"` // 图文总数量
  313. }
  314. // GetMaterialCount 获取素材总数。
  315. func (material *Material) GetMaterialCount() (res ResMaterialCount, err error) {
  316. var accessToken string
  317. accessToken, err = material.GetAccessToken()
  318. if err != nil {
  319. return
  320. }
  321. uri := fmt.Sprintf("%s?access_token=%s", getMaterialCountURL, accessToken)
  322. var response []byte
  323. response, err = util.HTTPGet(uri)
  324. if err != nil {
  325. return
  326. }
  327. err = util.DecodeWithError(response, &res, "GetMaterialCount")
  328. return
  329. }