Go 上传文件到微信图库

228 min read
package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	weixin "go-weixin"
	"net/http"
	"os"
	"time"
	"github.com/gin-contrib/cors"
)

// 单文件上传
func fileUpload(c *gin.Context) {
	file, err := c.FormFile("file")

	if err != nil {
		c.String(http.StatusBadRequest, "请求失败")
		return
	}

	//SaveUploadedFile(文件头,保存路径)
	if err := c.SaveUploadedFile(file, file.Filename); err != nil {
		c.String(http.StatusBadRequest, "保存失败 Error:%s", err.Error())
		return
	}

	img, err := os.Open(file.Filename)
	if err != nil {
		return
	}

	url, err := weixin.UploadImg(img)
	if err != nil {
		return
	}

	fmt.Printf("url")

	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"msg": "file upload err.",
		})
	} else {
		c.String(http.StatusOK, url)
	}

}

func getToken(c *gin.Context) {
	c.String(200, weixin.AccessToken())
}

func main() {
	appId := "wx642f9b***f3fd6"
	appSecret := "ecbf2**ef43aa8554438b51e"
	weixin.RefreshAccessToken(appId, appSecret)

	time.Sleep(3 * time.Second)

	engine := gin.Default()
  engine.Use(cors.Default())
	engine.POST("/upload", fileUpload)
	engine.GET("/token", getToken)

	engine.Run(":8880")
}

  • weixin.RefreshAccessToken 获取操作的token,参数为appId,appSecret,这两个参数需要在微信公众号后台获取
  • github.com/gin-contrib/cors 解决前端上传的跨域问题
  • c.FormFile("file") 获取前端上传的文件
  • os.Open(file.Filename) 获取File对象

accesstoken.go 后台获取token对象

package go_weixin

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"sync"
	"time"
)

// tick := time.Tick(7 * time.Second)
const refreshTimeout = 30 * time.Minute
const tokenURL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s"

type accessToken struct {
	AccessToken string `json:"access_token"` //	获取到的凭证
	ExpiresIn   int    `json:"expires_in"`   //	凭证有效时间,单位:秒
	mutex       sync.RWMutex
}

// AccessToken 取最新的 access_token,必须使用这个接口取,内部已经加锁
var AccessToken func() string

// RefreshAccessToken 定时刷新 access_token
func RefreshAccessToken(appId, appSecret string) {
	// 内部变量,外部不可以调用
	var _token = &accessToken{}

	AccessToken = func() string {
		_token.mutex.RLock()
		defer _token.mutex.RUnlock()

		return _token.AccessToken
	}

	go func() {
		url := fmt.Sprintf(tokenURL, appId, appSecret)

		tick := time.Tick(refreshTimeout)
		for {
			new := refresh(url)

			//fmt.Printf("old access token %+v", _token)
			fmt.Printf("new access token %+v", new)

			_token.mutex.Lock()
			_token.AccessToken = new.AccessToken
			_token.ExpiresIn = new.ExpiresIn
			_token.mutex.Unlock()

			<-tick // 等待下一个时钟周期到来
		}
	}()
}

func refresh(url string, ns ...int) (new *accessToken) {
	n := 0
	if len(ns) > 0 {
		n = ns[0]
	}

	var err error
	defer func() {
		if err != nil {
			fmt.Println(err)
			time.Sleep(3 * time.Minute)
			if n < 9 {
				n++
				new = refresh(url, n)
			}
		}
	}()

	resp, err := http.Get(url)
	if err != nil {
		return
	}

	body, err := ioutil.ReadAll(resp.Body)

	if err != nil {
		return
	}
	resp.Body.Close()

	new = &accessToken{}
	err = json.Unmarshal(body, new)
	if err != nil {
		return
	}

	return new
}

package go_weixin

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
)

// 素材管理
const (
	MaterialUploadTemporaryURL = "https://api.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s"
	MaterialGetTemporaryURL    = "https://api.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s"
	MaterialAddNewsURL         = "https://api.weixin.qq.com/cgi-bin/material/add_news?access_token=%s"
	MaterialUploadImg          = "https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=%s"
	MaterialUploadNewsURL      = "https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=%s&type=%s"
	MaterialGetNewsURL         = "https://api.weixin.qq.com/cgi-bin/material/get_material?access_token=%s"
	MaterialDeleteNewsURL      = "https://api.weixin.qq.com/cgi-bin/material/del_material?access_token=%s"
	MaterialUpdateNewsURL      = "https://api.weixin.qq.com/cgi-bin/material/update_news?access_token=%s"
	MaterialCountURL           = "https://api.weixin.qq.com/cgi-bin/material/get_materialcount?access_token=%s"
	MaterialBatchGetNewsURL    = "https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token=%s"
)

// MediaType 媒体文件类型
type MediaType string

// 微信支持的媒体文件类型
const (
	MediaTypeImage MediaType = "image"
	MediaTypeVoice MediaType = "voice"
	MediaTypeVideo MediaType = "video"
	MediaTypeThumb MediaType = "thumb"
)

// UploadTemporaryMaterial 新增临时素材
func UploadTemporaryMaterial(mtype MediaType, file *os.File) (mediaId string, createAt int, err error) {
	url := fmt.Sprintf(MaterialUploadTemporaryURL, AccessToken(), mtype)
	wapper := &struct {
		WXError
		Type     MediaType `json:"type"`
		MediaId  string    `json:"media_id"`
		CreateAt int       `json:"created_at"`
	}{}
	err = Upload(url, "media", file, wapper)
	return wapper.MediaId, wapper.CreateAt, err
}

// GetTemporaryMaterial 新增临时素材
func GetTemporaryMaterial(mediaId string) (filename string, body []byte, err error) {
	url := fmt.Sprintf(MaterialGetTemporaryURL, AccessToken(), mediaId)
	return Download(url)
}

// Article 永久图文素材
type Article struct {
	Title            string `json:"title"`              // 标题
	ThumbMediaId     string `json:"thumb_media_id"`     // 图文消息的封面图片素材id(必须是永久mediaID)
	Author           string `json:"author"`             // 作者
	Digest           string `json:"digest"`             // 图文消息的摘要,仅有单图文消息才有摘要,多图文此处为空
	ShowCoverPic     int    `json:"show_cover_pic"`     // 是否显示封面,0为false,即不显示,1为true,即显示
	Content          string `json:"content"`            // 图文消息的具体内容,支持HTML标签,必须少于2万字符,小于1M,且此处会去除JS
	URL              string `json:"url"`                // 图文页的URL
	ContentSourceURL string `json:"content_source_url"` // 图文消息的原文地址,即点击“阅读原文”后的URL
}

// AddNews 新增永久图文素材
func AddNews(as []Article) (mediaId string, err error) {
	if len(as) == 0 {
		return "", errors.New("articles is blank")
	}

	js, err := json.Marshal(as)
	if err != nil {
		return "", err
	}

	body := fmt.Sprintf(`{"articles":%s}`, js)
	url := fmt.Sprintf(MaterialAddNewsURL, AccessToken())
	wrapper := &struct {
		WXError
		MediaId string `json:"media_id"`
	}{}
	err = Post(url, []byte(body), wrapper)
	return wrapper.MediaId, err
}

// UploadImg 上传图文消息内的图片获取URL
func UploadImg(file *os.File) (u string, err error) {
	url := fmt.Sprintf(MaterialUploadImg, AccessToken())
	wapper := &struct {
		WXError
		URL string `json:"url"`
	}{}
	err = Upload(url, "media", file, wapper)

	return wapper.URL, err
}

// UploadNews 新增其他类型永久素材
func UploadNews(mtype MediaType, file *os.File) (mediaId, u string, err error) {
	url := fmt.Sprintf(MaterialUploadNewsURL, AccessToken(), mtype)
	wapper := &struct {
		WXError
		MediaId string `json:"media_id"`
		URL     string `json:"url"`
	}{}
	err = Upload(url, "media", file, wapper)
	return wapper.MediaId, wapper.URL, err
}

// UploadVideo 新增视频类型永久素材
func UploadVideo(mtype MediaType, title, desc string, file *os.File) (mediaId, u string, err error) {
	url := fmt.Sprintf(MaterialUploadNewsURL, AccessToken(), mtype)
	wapper := &struct {
		WXError
		MediaId string `json:"media_id"`
		URL     string `json:"url"`
	}{}
	desc = fmt.Sprintf(`{"title":"%s", "introduction":"%s"}`, title, desc)
	err = Upload(url, "media", file, wapper, desc)
	return wapper.MediaId, wapper.URL, err
}

// News 永久素材
type News struct {
	WXError
	Title       string    `json:"title"` // 图文消息的标题
	Description string    `json:"description"`
	DownURL     string    `json:"down_url"`
	NewsItem    []Article `json:"news_item"`
}

// GetNews 获取临时素材
func GetNews(mediaId string) (ret *News, err error) {
	url := fmt.Sprintf(MaterialGetNewsURL, AccessToken())
	body := fmt.Sprintf(`{"media_id":"%s"}`, mediaId)
	ret = &News{}
	err = Post(url, []byte(body), ret)
	return ret, err
}

// DeleteNews 删除永久素材
func DeleteNews(mediaId string) (err error) {
	url := fmt.Sprintf(MaterialDeleteNewsURL, AccessToken())
	body := fmt.Sprintf(`{"media_id":"%s"}`, mediaId)
	return Post(url, []byte(body), nil)
}

// UpdateNewsReq 修改永久图文素材
type UpdateNewsReq struct {
	MediaId  string    `json:"media_id"` // 要修改的图文消息的id
	Index    string    `json:"index"`    // 要更新的文章在图文消息中的位置(多图文消息时,此字段才有意义),第一篇为0
	Articles []Article `json:"articles"` // 要修改的图文消息的id
}

// UpdateNews 修改永久图文素材
func UpdateNews(news *UpdateNewsReq) (err error) {
	url := fmt.Sprintf(MaterialDeleteNewsURL, AccessToken())
	return Post(url, news, nil)
}

// MaterialCount 素材总数
type MaterialCount struct {
	WXError
	VoiceCount int `json:"voice_count"` // 语音总数量
	VideoCount int `json:"video_count"` // 视频总数量
	ImageCount int `json:"image_count"` // 图片总数量
	NewsCount  int `json:"news_count"`  // 图文总数量
}

// GetMaterialCount 获取素材总数
func GetMaterialCount() (mc *MaterialCount, err error) {
	url := fmt.Sprintf(MaterialCountURL, AccessToken())
	mc = &MaterialCount{}
	err = Get(url, mc)
	return mc, err
}

// NewsList 素材列表
type NewsList struct {
	WXError
	TotalCount string `json:"total_count"` // 该类型的素材的总数
	ItemCount  string `json:"item_count"`  // 本次调用获取的素材的数量
	Item       []struct {
		MediaId    string `json:"media_id"`
		UpdateTime string `json:"update_time"` // 这篇图文消息素材的最后更新时间
		Name       string `json:"name"`        // 文件名称
		URL        string `json:"url"`         // 图文页的URL,或者,当获取的列表是图片素材列表时,该字段是图片的URL
		Content    struct {
			NewsItem []Article `json:"news_item"`
		} `json:"content"` // 本次调用获取的素材的数量
	} `json:"item"` // 多图文消息会在此处有多篇文章
}

// BatchGetNews 获取素材列表
func BatchGetNews(mtype MediaType, offset int, count int) (ret *NewsList, err error) {
	url := fmt.Sprintf(MaterialBatchGetNewsURL, AccessToken())
	body := fmt.Sprintf(`{"type":%s,"offset":%d,"count":%d}`, mtype, offset, count)
	ret = &NewsList{}
	err = Post(url, []byte(body), ret)
	return ret, err
}

dockfile

FROM golang:latest
ADD . .
EXPOSE 8880
ENTRYPOINT ["./main"]

上传测试

curl -H "Expect:" -F 'file=@Users/test/Desktop/1.png' http://xxxxx:8880/upload