浏览代码

微信添加JS接口

zhanghongbo 9 年之前
父节点
当前提交
89ee3d79c4
共有 2 个文件被更改,包括 364 次插入61 次删除
  1. 21 0
      weixin/src/qfw/weixin/rpc/jssdk.go
  2. 343 61
      weixin/src/qfw/weixin/weixinsdk.go

+ 21 - 0
weixin/src/qfw/weixin/rpc/jssdk.go

@@ -0,0 +1,21 @@
+package rpc
+
+//JS 微信接口
+import (
+	"fmt"
+	"qfw/util"
+	wf "qfw/weixinconfig"
+	"time"
+)
+
+//取得JS接口参数
+func (wxrpc *WeiXinRpc) GetJSInterfaceParam(currenturl /*当前页面的URL一定要真实*/ string, ret *[]string) (err error) {
+	timestamp := time.Now().Unix()
+	noncestr := util.Uuid(32)
+	signature, _ := wxrpc.wx.JsSignature(currenturl, timestamp, noncestr)
+	*ret = append(*ret, wf.SysConfig.Appid)
+	*ret = append(*ret, fmt.Sprintf("%d", timestamp))
+	*ret = append(*ret, noncestr)
+	*ret = append(*ret, signature)
+	return nil
+}

+ 343 - 61
weixin/src/qfw/weixin/weixinsdk.go

@@ -13,6 +13,7 @@ import (
 	"log"
 	"mime/multipart"
 	"net/http"
+	"net/url"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -23,45 +24,71 @@ import (
 
 const (
 	// Event type
-	msgEvent         = "event"
-	EventSubscribe   = "subscribe"
-	EventUnsubscribe = "unsubscribe"
-	EventScan        = "scan"
-	EventClick       = "CLICK"
+	msgEvent          = "event"
+	EventSubscribe    = "subscribe"
+	EventUnsubscribe  = "unsubscribe"
+	EventScan         = "scan"
+	EventView         = "VIEW"
+	EventClick        = "CLICK"
+	EventLocation     = "LOCATION"
+	EventTemplateSent = "TEMPLATESENDJOBFINISH"
+
 	// Message type
-	MsgTypeDefault          = ".*"
-	MsgTypeText             = "text"
-	MsgTypeImage            = "image"
-	MsgTypeVoice            = "voice"
-	MsgTypeVideo            = "video"
-	MsgTypeLocation         = "location"
-	MsgTypeLink             = "link"
-	MsgTypeEvent            = msgEvent + ".*"
-	MsgTypeEventSubscribe   = msgEvent + "\\." + EventSubscribe
-	MsgTypeEventUnsubscribe = msgEvent + "\\." + EventUnsubscribe
-	MsgTypeEventScan        = msgEvent + "\\." + EventScan
-	MsgTypeEventClick       = msgEvent + "\\." + EventClick
+	MsgTypeDefault           = ".*"
+	MsgTypeText              = "text"
+	MsgTypeImage             = "image"
+	MsgTypeVoice             = "voice"
+	MsgTypeVideo             = "video"
+	MsgTypeShortVideo        = "shortvideo"
+	MsgTypeLocation          = "location"
+	MsgTypeLink              = "link"
+	MsgTypeEvent             = msgEvent + ".*"
+	MsgTypeEventSubscribe    = msgEvent + "\\." + EventSubscribe
+	MsgTypeEventUnsubscribe  = msgEvent + "\\." + EventUnsubscribe
+	MsgTypeEventScan         = msgEvent + "\\." + EventScan
+	MsgTypeEventView         = msgEvent + "\\." + EventView
+	MsgTypeEventClick        = msgEvent + "\\." + EventClick
+	MsgTypeEventLocation     = msgEvent + "\\." + EventLocation
+	MsgTypeEventTemplateSent = msgEvent + "\\." + EventTemplateSent
+
 	// Media type
 	MediaTypeImage = "image"
 	MediaTypeVoice = "voice"
 	MediaTypeVideo = "video"
 	MediaTypeThumb = "thumb"
 	// Button type
-	MenuButtonTypeKey        = "click"
-	MenuButtonTypeUrl        = "view"
-	MenuButtonTypeMedia      = "media_id"
-	MenuButtonTypePicOrAlbum = "pic_photo_or_album"
+	MenuButtonTypeKey             = "click"
+	MenuButtonTypeUrl             = "view"
+	MenuButtonTypeScancodePush    = "scancode_push"
+	MenuButtonTypeScancodeWaitmsg = "scancode_waitmsg"
+	MenuButtonTypePicSysphoto     = "pic_sysphoto"
+	MenuButtonTypePicPhotoOrAlbum = "pic_photo_or_album"
+	MenuButtonTypePicWeixin       = "pic_weixin"
+	MenuButtonTypeLocationSelect  = "location_select"
+	MenuButtonTypeMediaId         = "media_id"
+	MenuButtonTypeViewLimited     = "view_limited"
+	// Template Status
+	TemplateSentStatusSuccess      = "success"
+	TemplateSentStatusUserBlock    = "failed:user block"
+	TemplateSentStatusSystemFailed = "failed:system failed"
+	// Redirect Scope
+	RedirectURLScopeBasic    = "snsapi_base"
+	RedirectURLScopeUserInfo = "snsapi_userinfo"
 	// Weixin host URL
-	weixinHost        = "https://api.weixin.qq.com/cgi-bin"
-	weixinQRScene     = "https://api.weixin.qq.com/cgi-bin/qrcode"
-	weixinShowQRScene = "https://mp.weixin.qq.com/cgi-bin/showqrcode"
-	weixinFileURL     = "http://file.api.weixin.qq.com/cgi-bin/media"
-	weixinUserInfo    = "https://api.weixin.qq.com/cgi-bin/user/info"
+	weixinHost               = "https://api.weixin.qq.com/cgi-bin"
+	weixinQRScene            = "https://api.weixin.qq.com/cgi-bin/qrcode"
+	weixinShowQRScene        = "https://mp.weixin.qq.com/cgi-bin/showqrcode"
+	weixinShortURL           = "https://api.weixin.qq.com/cgi-bin/shorturl"
+	weixinUserInfo           = "https://api.weixin.qq.com/cgi-bin/user/info"
+	weixinFileURL            = "http://file.api.weixin.qq.com/cgi-bin/media"
+	weixinTemplate           = "https://api.weixin.qq.com/cgi-bin/template"
+	weixinRedirectURL        = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect"
+	weixinUserAccessTokenURL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"
+	weixinJsApiTicketURL     = "https://api.weixin.qq.com/cgi-bin/ticket/getticket"
 	// Max retry count
 	retryMaxN = 3
 	// Reply format
 	replyText               = "<xml>%s<MsgType><![CDATA[text]]></MsgType><Content><![CDATA[%s]]></Content></xml>"
-	reply2CustomerService   = "<xml>%s<MsgType><![CDATA[transfer_customer_service]]></MsgType></xml>"
 	replyImage              = "<xml>%s<MsgType><![CDATA[image]]></MsgType><Image><MediaId><![CDATA[%s]]></MediaId></Image></xml>"
 	replyVoice              = "<xml>%s<MsgType><![CDATA[voice]]></MsgType><Voice><MediaId><![CDATA[%s]]></MediaId></Voice></xml>"
 	replyVideo              = "<xml>%s<MsgType><![CDATA[video]]></MsgType><Video><MediaId><![CDATA[%s]]></MediaId><Title><![CDATA[%s]]></Title><Description><![CDATA[%s]]></Description></Video></xml>"
@@ -70,10 +97,11 @@ const (
 	replyHeader             = "<ToUserName><![CDATA[%s]]></ToUserName><FromUserName><![CDATA[%s]]></FromUserName><CreateTime>%d</CreateTime>"
 	replyArticle            = "<item><Title><![CDATA[%s]]></Title> <Description><![CDATA[%s]]></Description><PicUrl><![CDATA[%s]]></PicUrl><Url><![CDATA[%s]]></Url></item>"
 	transferCustomerService = "<xml>" + replyHeader + "<MsgType><![CDATA[transfer_customer_service]]></MsgType></xml>"
+	reply2CustomerService   = "<xml>%s<MsgType><![CDATA[transfer_customer_service]]></MsgType></xml>"
 
 	// QR scene request
-	requestQRScene      = "{\"expire_seconds\":%d,\"action_name\":\"QR_SCENE\",\"action_info\":{\"scene\":{\"scene_id\":%d}}}"
-	requestQRLimitScene = "{\"action_name\":\"QR_LIMIT_SCENE\",\"action_info\":{\"scene\":{\"scene_id\":%d}}}"
+	requestQRScene      = `{"expire_seconds":%d,"action_name":"QR_SCENE","action_info":{"scene":{"scene_id":%d}}}`
+	requestQRLimitScene = `{"action_name":"QR_LIMIT_SCENE","action_info":{"scene":{"scene_id":%d}}}`
 )
 
 // Common message header
@@ -107,6 +135,7 @@ type Request struct {
 	Longitude    float32
 	Precision    float32
 	Recognition  string
+	Status       string
 }
 
 // Use to reply music message
@@ -142,10 +171,35 @@ type MenuButton struct {
 	Type       string       `json:"type,omitempty"`
 	Key        string       `json:"key,omitempty"`
 	Url        string       `json:"url,omitempty"`
-	Mediaid    int          `json:"media_id"`
+	MediaId    string       `json:"media_id,omitempty"`
 	SubButtons []MenuButton `json:"sub_button,omitempty"`
 }
 
+type UserAccessToken struct {
+	AccessToken   string `json:"access_token"`
+	RefreshToken  string `json:"refresh_token"`
+	ExpireSeconds int    `json:"expires_in"`
+	OpenId        string `json:"openid"`
+	Scope         string `json:"scope"`
+	UnionId       string `json:"unionid,omitempty"`
+}
+
+type UserInfo struct {
+	Subscribe     int    `json:"subscribe,omitempty"`
+	Language      string `json:"language,omitempty"`
+	OpenId        string `json:"openid,omitempty"`
+	UnionId       string `json:"unionid,omitempty"`
+	Nickname      string `json:"nickname,omitempty"`
+	Sex           int    `json:"sex,omitempty"`
+	City          string `json:"city,omitempty"`
+	Country       string `json:"country,omitempty"`
+	Province      string `json:"province,omitempty"`
+	HeadImageUrl  string `json:"headimgurl,omitempty"`
+	SubscribeTime int64  `json:"subscribe_time,omitempty"`
+	Remark        string `json:"remark,omitempty"`
+	GroupId       int    `json:"groupid,omitempty"`
+}
+
 // Use to output reply
 type ResponseWriter interface {
 	// Get weixin
@@ -168,6 +222,7 @@ type ResponseWriter interface {
 	PostVideo(mediaId string, title string, description string) error
 	PostMusic(music *Music) error
 	PostNews(articles []Article) error
+	PostTemplateMessage(templateid string, url string, data interface{}) (string, error)
 	// Media operator
 	UploadMediaFromFile(mediaType string, filepath string) (string, error)
 	DownloadMediaToFile(mediaId string, filepath string) error
@@ -201,11 +256,19 @@ type accessToken struct {
 	expires time.Time
 }
 
+type jsApiTicket struct {
+	ticket  string
+	expires time.Time
+}
+
 type Weixin struct {
-	token     string
-	routes    []*route
-	tokenChan chan accessToken
-	userData  interface{}
+	token      string
+	routes     []*route
+	tokenChan  chan accessToken
+	ticketChan chan jsApiTicket
+	userData   interface{}
+	appId      string
+	appSecret  string
 }
 
 //
@@ -219,9 +282,13 @@ func (qr *QRScene) ToURL() string {
 func New(token string, appid string, secret string) *Weixin {
 	wx := &Weixin{}
 	wx.token = token
+	wx.appId = appid
+	wx.appSecret = secret
 	if len(appid) > 0 && len(secret) > 0 {
 		wx.tokenChan = make(chan accessToken)
 		go createAccessToken(wx.tokenChan, appid, secret)
+		wx.ticketChan = make(chan jsApiTicket)
+		go createJsApiTicket(wx.tokenChan, wx.ticketChan)
 	}
 	return wx
 }
@@ -232,6 +299,14 @@ func NewWithUserData(token string, appid string, secret string, userData interfa
 	return wx
 }
 
+func (wx *Weixin) GetAppId() string {
+	return wx.appId
+}
+
+func (wx *Weixin) GetAppSecret() string {
+	return wx.appSecret
+}
+
 // Register request callback.
 func (wx *Weixin) HandleFunc(pattern string, handler HandlerFunc) {
 	regex, err := regexp.Compile(pattern)
@@ -387,6 +462,21 @@ func (wx *Weixin) DownloadMedia(mediaId string, writer io.Writer) error {
 	return downloadMedia(wx.tokenChan, mediaId, writer)
 }
 
+// Get ip list
+func (wx *Weixin) GetIpList() ([]string, error) {
+	reply, err := sendGetRequest(weixinHost+"/getcallbackip?access_token=", wx.tokenChan)
+	if err != nil {
+		return nil, err
+	}
+	var result struct {
+		IpList []string `json:"ip_list"`
+	}
+	if err := json.Unmarshal(reply, &result); err != nil {
+		return nil, err
+	}
+	return result.IpList, nil
+}
+
 // Create QR scene
 func (wx *Weixin) CreateQRScene(sceneId int, expires int) (*QRScene, error) {
 	reply, err := postRequest(weixinQRScene+"/create?access_token=", wx.tokenChan, []byte(fmt.Sprintf(requestQRScene, expires, sceneId)))
@@ -413,6 +503,31 @@ func (wx *Weixin) CreateQRLimitScene(sceneId int) (*QRScene, error) {
 	return &qr, nil
 }
 
+// Long url to short url
+func (wx *Weixin) ShortURL(url string) (string, error) {
+	var request struct {
+		Action  string `json:"action"`
+		LongUrl string `json:"long_url"`
+	}
+	request.Action = "long2short"
+	request.LongUrl = url
+	data, err := marshal(request)
+	if err != nil {
+		return "", err
+	}
+	reply, err := postRequest(weixinShortURL+"?access_token=", wx.tokenChan, data)
+	if err != nil {
+		return "", err
+	}
+	var shortUrl struct {
+		Url string `json:"short_url"`
+	}
+	if err := json.Unmarshal(reply, &shortUrl); err != nil {
+		return "", err
+	}
+	return shortUrl.Url, nil
+}
+
 // Custom menu
 func (wx *Weixin) CreateMenu(menu *Menu) error {
 	data, err := json.Marshal(menu)
@@ -431,16 +546,14 @@ func (wx *Weixin) GetMenu() (*Menu, error) {
 	reply, err := sendGetRequest(weixinHost+"/menu/get?access_token=", wx.tokenChan)
 	if err != nil {
 		return nil, err
-	} else {
-		var result struct {
-			MenuCtx *Menu `json:"menu"`
-		}
-		if err := json.Unmarshal(reply, &result); err != nil {
-			return nil, err
-		} else {
-			return result.MenuCtx, nil
-		}
 	}
+	var result struct {
+		MenuCtx *Menu `json:"menu"`
+	}
+	if err := json.Unmarshal(reply, &result); err != nil {
+		return nil, err
+	}
+	return result.MenuCtx, nil
 }
 
 func (wx *Weixin) DeleteMenu() error {
@@ -448,6 +561,129 @@ func (wx *Weixin) DeleteMenu() error {
 	return err
 }
 
+// Template
+func (wx *Weixin) SetTemplateIndustry(id1 string, id2 string) error {
+	var industry struct {
+		Id1 string `json:"industry_id1,omitempty"`
+		Id2 string `json:"industry_id2,omitempty"`
+	}
+	industry.Id1 = id1
+	industry.Id2 = id2
+	data, err := marshal(industry)
+	if err != nil {
+		return err
+	}
+	_, err = postRequest(weixinTemplate+"/api_set_industry?access_token=", wx.tokenChan, data)
+	return err
+}
+
+func (wx *Weixin) AddTemplate(shortid string) (string, error) {
+	var request struct {
+		Shortid string `json:"template_id_short,omitempty"`
+	}
+	request.Shortid = shortid
+	data, err := marshal(request)
+	if err != nil {
+		return "", err
+	}
+	reply, err := postRequest(weixinTemplate+"/api_set_industry?access_token=", wx.tokenChan, data)
+	if err != nil {
+		return "", err
+	}
+	var templateId struct {
+		Id string `json:"template_id,omitempty"`
+	}
+	if err := json.Unmarshal(reply, &templateId); err != nil {
+		return "", err
+	}
+	return templateId.Id, nil
+}
+
+func (wx *Weixin) PostTemplateMessage(touser string, templateid string, url string, data interface{}) (string, error) {
+	var msg struct {
+		ToUser     string      `json:"touser"`
+		TemplateId string      `json:"template_id"`
+		Url        string      `json:"url,omitempty"`
+		Data       interface{} `json:"data,omitempty"`
+	}
+	msg.ToUser = touser
+	msg.TemplateId = templateid
+	msg.Url = url
+	msg.Data = data
+	msgStr, err := marshal(msg)
+	if err != nil {
+		return "", err
+	}
+	reply, err := postRequest(weixinHost+"/message/template/send?access_token=", wx.tokenChan, msgStr)
+	if err != nil {
+		return "", err
+	}
+	var resp struct {
+		MsgId string `json:"msgid,omitempty"`
+	}
+	if err := json.Unmarshal(reply, &resp); err != nil {
+		return "", err
+	}
+	return resp.MsgId, nil
+}
+
+// Create redirect url
+func (wx *Weixin) CreateRedirectURL(urlStr string, scope string, state string) string {
+	return fmt.Sprintf(weixinRedirectURL, wx.appId, url.QueryEscape(urlStr), scope, state)
+}
+
+// Get open id
+func (wx *Weixin) GetUserAccessToken(code string) (*UserAccessToken, error) {
+	resp, err := http.Get(fmt.Sprintf(weixinUserAccessTokenURL, wx.appId, wx.appSecret, code))
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	var res UserAccessToken
+	if err := json.Unmarshal(body, &res); err != nil {
+		return nil, err
+	}
+	return &res, nil
+}
+
+// Get user info
+func (wx *Weixin) GetUserInfo(openid string) (*UserInfo, error) {
+	reply, err := sendGetRequest(fmt.Sprintf("%s?openid=%s&lang=zh_CN&access_token=", weixinUserInfo, openid), wx.tokenChan)
+	if err != nil {
+		return nil, err
+	}
+	var result UserInfo
+	if err := json.Unmarshal(reply, &result); err != nil {
+		return nil, err
+	}
+	return &result, nil
+}
+
+func (wx *Weixin) GetJsApiTicket() (string, error) {
+	for i := 0; i < retryMaxN; i++ {
+		ticket := <-wx.ticketChan
+		if time.Since(ticket.expires).Seconds() < 0 {
+			return ticket.ticket, nil
+		}
+	}
+	return "", errors.New("Get JsApi Ticket Timeout")
+}
+
+func (wx *Weixin) JsSignature(url string, timestamp int64, noncestr string) (string, error) {
+	ticket, err := wx.GetJsApiTicket()
+	if err != nil {
+		return "", err
+	}
+	h := sha1.New()
+	h.Write([]byte(fmt.Sprintf("jsapi_ticket=%s&noncestr=%s&timestamp=%d&url=%s",
+		ticket, noncestr, timestamp, url)))
+	return fmt.Sprintf("%x", h.Sum(nil)), nil
+}
+
 // Create handler func
 func (wx *Weixin) CreateHandlerFunc(w http.ResponseWriter, r *http.Request) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
@@ -505,6 +741,16 @@ func (wx *Weixin) routeRequest(w http.ResponseWriter, r *Request) {
 	return
 }
 
+func marshal(v interface{}) ([]byte, error) {
+	data, err := json.Marshal(v)
+	if err == nil {
+		data = bytes.Replace(data, []byte("\\u003c"), []byte("<"), -1)
+		data = bytes.Replace(data, []byte("\\u003e"), []byte(">"), -1)
+		data = bytes.Replace(data, []byte("\\u0026"), []byte("&"), -1)
+	}
+	return data, err
+}
+
 func checkSignature(t string, w http.ResponseWriter, r *http.Request) bool {
 	r.ParseForm()
 	var signature string = r.FormValue("signature")
@@ -547,6 +793,25 @@ func authAccessToken(appid string, secret string) (string, time.Duration) {
 	return "", 0
 }
 
+func getJsApiTicket(c chan accessToken) (*jsApiTicket, error) {
+	reply, err := sendGetRequest(weixinJsApiTicketURL+"?type=jsapi&access_token=", c)
+	if err != nil {
+		return nil, err
+	}
+	var res struct {
+		Ticket    string `json:"ticket"`
+		ExpiresIn int64  `json:"expires_in"`
+	}
+	if err := json.Unmarshal(reply, &res); err != nil {
+		return nil, err
+	}
+	var ticket jsApiTicket
+	ticket.ticket = res.Ticket
+	ticket.expires = time.Now().Add(time.Duration(res.ExpiresIn * 1000 * 1000 * 1000))
+	return &ticket, nil
+
+}
+
 func createAccessToken(c chan accessToken, appid string, secret string) {
 	token := accessToken{"", time.Now()}
 	c <- token
@@ -560,6 +825,20 @@ func createAccessToken(c chan accessToken, appid string, secret string) {
 	}
 }
 
+func createJsApiTicket(cin chan accessToken, c chan jsApiTicket) {
+	ticket := jsApiTicket{"", time.Now()}
+	c <- ticket
+	for {
+		if time.Since(ticket.expires).Seconds() >= 0 {
+			t, err := getJsApiTicket(cin)
+			if err == nil {
+				ticket = *t
+			}
+		}
+		c <- ticket
+	}
+}
+
 func sendGetRequest(reqURL string, c chan accessToken) ([]byte, error) {
 	for i := 0; i < retryMaxN; i++ {
 		token := <-c
@@ -698,24 +977,22 @@ func downloadMedia(c chan accessToken, mediaId string, writer io.Writer) error {
 			if r.Header.Get("Content-Type") != "text/plain" {
 				_, err := io.Copy(writer, r.Body)
 				return err
-			} else {
-				reply, err := ioutil.ReadAll(r.Body)
-				if err != nil {
-					return err
-				}
-				var result response
-				if err := json.Unmarshal(reply, &result); err != nil {
-					return err
-				} else {
-					switch result.ErrorCode {
-					case 0:
-						return nil
-					case 42001: // access_token timeout and retry
-						continue
-					default:
-						return errors.New(fmt.Sprintf("WeiXin download[%d]: %s", result.ErrorCode, result.ErrorMessage))
-					}
-				}
+			}
+			reply, err := ioutil.ReadAll(r.Body)
+			if err != nil {
+				return err
+			}
+			var result response
+			if err := json.Unmarshal(reply, &result); err != nil {
+				return err
+			}
+			switch result.ErrorCode {
+			case 0:
+				return nil
+			case 42001: // access_token timeout and retry
+				continue
+			default:
+				return errors.New(fmt.Sprintf("WeiXin download[%d]: %s", result.ErrorCode, result.ErrorMessage))
 			}
 		}
 	}
@@ -823,6 +1100,11 @@ func (w responseWriter) PostNews(articles []Article) error {
 	return w.wx.PostNews(w.toUserName, articles)
 }
 
+// Post template message
+func (w responseWriter) PostTemplateMessage(templateid string, url string, data interface{}) (string, error) {
+	return w.wx.PostTemplateMessage(w.toUserName, templateid, url, data)
+}
+
 // Upload media from local file
 func (w responseWriter) UploadMediaFromFile(mediaType string, filepath string) (string, error) {
 	return w.wx.UploadMediaFromFile(mediaType, filepath)