mirror of
https://github.com/silenceper/wechat.git
synced 2026-02-04 21:02:25 +08:00
1
.gitignore
vendored
1
.gitignore
vendored
@@ -25,3 +25,4 @@ _testmain.go
|
||||
.DS_Store
|
||||
.vscode/
|
||||
vendor/*/
|
||||
.idea/
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.11.x
|
||||
- 1.10.x
|
||||
- 1.9.x
|
||||
- 1.8.x
|
||||
- 1.7.x
|
||||
- 1.6.x
|
||||
|
||||
services:
|
||||
- memcached
|
||||
|
||||
@@ -272,7 +272,7 @@ type Reply struct {
|
||||
}
|
||||
|
||||
```
|
||||
注意:`retrun nil`表示什么也不做
|
||||
注意:`return nil`表示什么也不做
|
||||
|
||||
#### 回复文本消息
|
||||
```go
|
||||
|
||||
7
cache/redis.go
vendored
7
cache/redis.go
vendored
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/garyburd/redigo/redis"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
)
|
||||
|
||||
//Redis redis cache
|
||||
@@ -45,6 +45,11 @@ func NewRedis(opts *RedisOpts) *Redis {
|
||||
return &Redis{pool}
|
||||
}
|
||||
|
||||
//SetConn 设置conn
|
||||
func (r *Redis) SetConn(conn *redis.Pool) {
|
||||
r.conn = conn
|
||||
}
|
||||
|
||||
//Get 获取一个值
|
||||
func (r *Redis) Get(key string) interface{} {
|
||||
conn := r.conn.Get()
|
||||
|
||||
76
context/qy_access_token.go
Normal file
76
context/qy_access_token.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
//qyAccessTokenURL 获取access_token的接口
|
||||
qyAccessTokenURL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s"
|
||||
)
|
||||
|
||||
//ResQyAccessToken struct
|
||||
type ResQyAccessToken struct {
|
||||
util.CommonError
|
||||
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
//SetQyAccessTokenLock 设置读写锁(一个appID一个读写锁)
|
||||
func (ctx *Context) SetQyAccessTokenLock(l *sync.RWMutex) {
|
||||
ctx.accessTokenLock = l
|
||||
}
|
||||
|
||||
//GetQyAccessToken 获取access_token
|
||||
func (ctx *Context) GetQyAccessToken() (accessToken string, err error) {
|
||||
ctx.accessTokenLock.Lock()
|
||||
defer ctx.accessTokenLock.Unlock()
|
||||
|
||||
accessTokenCacheKey := fmt.Sprintf("qy_access_token_%s", ctx.AppID)
|
||||
val := ctx.Cache.Get(accessTokenCacheKey)
|
||||
if val != nil {
|
||||
accessToken = val.(string)
|
||||
return
|
||||
}
|
||||
|
||||
//从微信服务器获取
|
||||
var resQyAccessToken ResQyAccessToken
|
||||
resQyAccessToken, err = ctx.GetQyAccessTokenFromServer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
accessToken = resQyAccessToken.AccessToken
|
||||
return
|
||||
}
|
||||
|
||||
//GetQyAccessTokenFromServer 强制从微信服务器获取token
|
||||
func (ctx *Context) GetQyAccessTokenFromServer() (resQyAccessToken ResQyAccessToken, err error) {
|
||||
log.Printf("GetQyAccessTokenFromServer")
|
||||
url := fmt.Sprintf(qyAccessTokenURL, ctx.AppID, ctx.AppSecret)
|
||||
var body []byte
|
||||
body, err = util.HTTPGet(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(body, &resQyAccessToken)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if resQyAccessToken.ErrCode != 0 {
|
||||
err = fmt.Errorf("get qy_access_token error : errcode=%v , errormsg=%v", resQyAccessToken.ErrCode, resQyAccessToken.ErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
qyAccessTokenCacheKey := fmt.Sprintf("qy_access_token_%s", ctx.AppID)
|
||||
expires := resQyAccessToken.ExpiresIn - 1500
|
||||
err = ctx.Cache.Set(qyAccessTokenCacheKey, resQyAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
||||
return
|
||||
}
|
||||
@@ -184,13 +184,6 @@ func (material *Material) DeleteMaterial(mediaID string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var resDeleteMaterial util.CommonError
|
||||
err = json.Unmarshal(response, &resDeleteMaterial)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resDeleteMaterial.ErrCode != 0 {
|
||||
return fmt.Errorf("DeleteMaterial error : errcode=%v , errmsg=%v", resDeleteMaterial.ErrCode, resDeleteMaterial.ErrMsg)
|
||||
}
|
||||
return nil
|
||||
|
||||
return util.DecodeWithCommonError(response, "DeleteMaterial")
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@ const (
|
||||
type Media struct {
|
||||
util.CommonError
|
||||
|
||||
Type MediaType `json:"type"`
|
||||
MediaID string `json:"media_id"`
|
||||
ThumbMediaID string `json:"thumb_media_id"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Type MediaType `json:"type"`
|
||||
MediaID string `json:"media_id"`
|
||||
ThumbMediaID string `json:"thumb_media_id"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
//MediaUpload 临时素材上传
|
||||
|
||||
@@ -7,6 +7,8 @@ type Button struct {
|
||||
Key string `json:"key,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
MediaID string `json:"media_id,omitempty"`
|
||||
AppID string `json:"appid,omitempty"`
|
||||
PagePath string `json:"pagepath,omitempty"`
|
||||
SubButtons []*Button `json:"sub_button,omitempty"`
|
||||
}
|
||||
|
||||
@@ -126,3 +128,16 @@ func (btn *Button) SetViewLimitedButton(name, mediaID string) {
|
||||
btn.URL = ""
|
||||
btn.SubButtons = nil
|
||||
}
|
||||
|
||||
//SetMiniprogramButton 设置 跳转小程序 类型按钮 (公众号后台必须已经关联小程序)
|
||||
func (btn *Button) SetMiniprogramButton(name, url, appID, pagePath string) {
|
||||
btn.Type = "miniprogram"
|
||||
btn.Name = name
|
||||
btn.URL = url
|
||||
btn.AppID = appID
|
||||
btn.PagePath = pagePath
|
||||
|
||||
btn.Key = ""
|
||||
btn.MediaID = ""
|
||||
btn.SubButtons = nil
|
||||
}
|
||||
|
||||
45
menu/menu.go
45
menu/menu.go
@@ -134,15 +134,8 @@ func (menu *Menu) SetMenu(buttons []*Button) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var commError util.CommonError
|
||||
err = json.Unmarshal(response, &commError)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if commError.ErrCode != 0 {
|
||||
return fmt.Errorf("SetMenu Error , errcode=%d , errmsg=%s", commError.ErrCode, commError.ErrMsg)
|
||||
}
|
||||
return nil
|
||||
|
||||
return util.DecodeWithCommonError(response, "SetMenu")
|
||||
}
|
||||
|
||||
//GetMenu 获取菜单配置
|
||||
@@ -180,15 +173,8 @@ func (menu *Menu) DeleteMenu() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var commError util.CommonError
|
||||
err = json.Unmarshal(response, &commError)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if commError.ErrCode != 0 {
|
||||
return fmt.Errorf("GetMenu Error , errcode=%d , errmsg=%s", commError.ErrCode, commError.ErrMsg)
|
||||
}
|
||||
return nil
|
||||
|
||||
return util.DecodeWithCommonError(response, "GetMenu")
|
||||
}
|
||||
|
||||
//AddConditional 添加个性化菜单
|
||||
@@ -208,15 +194,8 @@ func (menu *Menu) AddConditional(buttons []*Button, matchRule *MatchRule) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var commError util.CommonError
|
||||
err = json.Unmarshal(response, &commError)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if commError.ErrCode != 0 {
|
||||
return fmt.Errorf("AddConditional Error , errcode=%d , errmsg=%s", commError.ErrCode, commError.ErrMsg)
|
||||
}
|
||||
return nil
|
||||
|
||||
return util.DecodeWithCommonError(response, "AddConditional")
|
||||
}
|
||||
|
||||
//DeleteConditional 删除个性化菜单
|
||||
@@ -235,15 +214,8 @@ func (menu *Menu) DeleteConditional(menuID int64) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var commError util.CommonError
|
||||
err = json.Unmarshal(response, &commError)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if commError.ErrCode != 0 {
|
||||
return fmt.Errorf("DeleteConditional Error , errcode=%d , errmsg=%s", commError.ErrCode, commError.ErrMsg)
|
||||
}
|
||||
return nil
|
||||
|
||||
return util.DecodeWithCommonError(response, "DeleteConditional")
|
||||
}
|
||||
|
||||
//MenuTryMatch 菜单匹配
|
||||
@@ -286,7 +258,6 @@ func (menu *Menu) GetCurrentSelfMenuInfo() (resSelfMenuInfo ResSelfMenuInfo, err
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(string(response))
|
||||
err = json.Unmarshal(response, &resSelfMenuInfo)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -69,6 +69,7 @@ type MixMessage struct {
|
||||
//基本消息
|
||||
MsgID int64 `xml:"MsgId"`
|
||||
Content string `xml:"Content"`
|
||||
Recognition string `xml:"Recognition"`
|
||||
PicURL string `xml:"PicUrl"`
|
||||
MediaID string `xml:"MediaId"`
|
||||
Format string `xml:"Format"`
|
||||
@@ -82,15 +83,15 @@ type MixMessage struct {
|
||||
URL string `xml:"Url"`
|
||||
|
||||
//事件相关
|
||||
Event EventType `xml:"Event"`
|
||||
EventKey string `xml:"EventKey"`
|
||||
Ticket string `xml:"Ticket"`
|
||||
Latitude string `xml:"Latitude"`
|
||||
Longitude string `xml:"Longitude"`
|
||||
Precision string `xml:"Precision"`
|
||||
MenuID string `xml:"MenuId"`
|
||||
Status string `xml:"Status"`
|
||||
SessionFrom string `xml:"SessionFrom"`
|
||||
Event EventType `xml:"Event"`
|
||||
EventKey string `xml:"EventKey"`
|
||||
Ticket string `xml:"Ticket"`
|
||||
Latitude string `xml:"Latitude"`
|
||||
Longitude string `xml:"Longitude"`
|
||||
Precision string `xml:"Precision"`
|
||||
MenuID string `xml:"MenuId"`
|
||||
Status string `xml:"Status"`
|
||||
SessionFrom string `xml:"SessionFrom"`
|
||||
|
||||
ScanCodeInfo struct {
|
||||
ScanType string `xml:"ScanType"`
|
||||
|
||||
95
oauth/qy_oauth.go
Normal file
95
oauth/qy_oauth.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
var (
|
||||
qyRedirectOauthURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&agentid=%s&state=%s#wechat_redirect"
|
||||
qyUserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=%s&code=%s"
|
||||
qyUserDetailURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserdetail"
|
||||
)
|
||||
|
||||
//GetQyRedirectURL 获取企业微信跳转的url地址
|
||||
func (oauth *Oauth) GetQyRedirectURL(redirectURI, agentid, scope, state string) (string, error) {
|
||||
//url encode
|
||||
urlStr := url.QueryEscape(redirectURI)
|
||||
return fmt.Sprintf(qyRedirectOauthURL, oauth.AppID, urlStr, scope, agentid, state), nil
|
||||
}
|
||||
|
||||
//QyUserInfo 用户授权获取到用户信息
|
||||
type QyUserInfo struct {
|
||||
util.CommonError
|
||||
|
||||
UserID string `json:"UserId"`
|
||||
DeviceID string `json:"DeviceId"`
|
||||
UserTicket string `json:"user_ticket"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
//GetQyUserInfoByCode 根据code获取企业user_info
|
||||
func (oauth *Oauth) GetQyUserInfoByCode(code string) (result QyUserInfo, err error) {
|
||||
qyAccessToken, e := oauth.GetQyAccessToken()
|
||||
if e != nil {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
urlStr := fmt.Sprintf(qyUserInfoURL, qyAccessToken, code)
|
||||
var response []byte
|
||||
response, err = util.HTTPGet(urlStr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("GetQyUserInfoByCode error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//QyUserDetail 到用户详情
|
||||
type QyUserDetail struct {
|
||||
util.CommonError
|
||||
|
||||
UserID string `json:"UserId"`
|
||||
Name string `json:"name"`
|
||||
Mobile string `json:"mobile"`
|
||||
Gender string `json:"gender"`
|
||||
Email string `json:"email"`
|
||||
Avatar string `json:"avatar"`
|
||||
QrCode string `json:"qr_code"`
|
||||
}
|
||||
|
||||
//GetQyUserDetailUserTicket 根据user_ticket获取到用户详情
|
||||
func (oauth *Oauth) GetQyUserDetailUserTicket(userTicket string) (result QyUserDetail, err error) {
|
||||
var qyAccessToken string
|
||||
qyAccessToken, err = oauth.GetQyAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", qyUserDetailURL, qyAccessToken)
|
||||
var response []byte
|
||||
response, err = util.PostJSON(uri, map[string]string{
|
||||
"user_ticket": userTicket,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("GetQyUserDetailUserTicket Error , errcode=%d , errmsg=%s", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
type Server struct {
|
||||
*context.Context
|
||||
|
||||
debug bool
|
||||
|
||||
openID string
|
||||
|
||||
messageHandler func(message.MixMessage) *message.Reply
|
||||
@@ -40,6 +42,11 @@ func NewServer(context *context.Context) *Server {
|
||||
return srv
|
||||
}
|
||||
|
||||
// SetDebug set debug field
|
||||
func (srv *Server) SetDebug(debug bool) {
|
||||
srv.debug = debug
|
||||
}
|
||||
|
||||
//Serve 处理微信的请求消息
|
||||
func (srv *Server) Serve() error {
|
||||
if !srv.Validate() {
|
||||
@@ -65,6 +72,9 @@ func (srv *Server) Serve() error {
|
||||
|
||||
//Validate 校验请求是否合法
|
||||
func (srv *Server) Validate() bool {
|
||||
if srv.debug {
|
||||
return true
|
||||
}
|
||||
timestamp := srv.Query("timestamp")
|
||||
nonce := srv.Query("nonce")
|
||||
signature := srv.Query("signature")
|
||||
|
||||
51
user/user.go
51
user/user.go
@@ -9,7 +9,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
userInfoURL = "https://api.weixin.qq.com/cgi-bin/user/info"
|
||||
userInfoURL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN"
|
||||
updateRemarkURL = "https://api.weixin.qq.com/cgi-bin/user/info/updateremark?access_token=%s"
|
||||
)
|
||||
|
||||
//User 用户管理
|
||||
@@ -28,20 +29,20 @@ func NewUser(context *context.Context) *User {
|
||||
type Info struct {
|
||||
util.CommonError
|
||||
|
||||
Subscribe int32 `json:"subscribe"`
|
||||
OpenID string `json:"openid"`
|
||||
Nickname string `json:"nickname"`
|
||||
Sex int32 `json:"sex"`
|
||||
City string `json:"city"`
|
||||
Country string `json:"country"`
|
||||
Province string `json:"province"`
|
||||
Language string `json:"language"`
|
||||
Headimgurl string `json:"headimgurl"`
|
||||
SubscribeTime int32 `json:"subscribe_time"`
|
||||
UnionID string `json:"unionid"`
|
||||
Remark string `json:"remark"`
|
||||
GroupID int32 `json:"groupid"`
|
||||
TagidList []string `json:"tagid_list"`
|
||||
Subscribe int32 `json:"subscribe"`
|
||||
OpenID string `json:"openid"`
|
||||
Nickname string `json:"nickname"`
|
||||
Sex int32 `json:"sex"`
|
||||
City string `json:"city"`
|
||||
Country string `json:"country"`
|
||||
Province string `json:"province"`
|
||||
Language string `json:"language"`
|
||||
Headimgurl string `json:"headimgurl"`
|
||||
SubscribeTime int32 `json:"subscribe_time"`
|
||||
UnionID string `json:"unionid"`
|
||||
Remark string `json:"remark"`
|
||||
GroupID int32 `json:"groupid"`
|
||||
TagidList []int32 `json:"tagid_list"`
|
||||
}
|
||||
|
||||
//GetUserInfo 获取用户基本信息
|
||||
@@ -52,7 +53,7 @@ func (user *User) GetUserInfo(openID string) (userInfo *Info, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s&openid=%s&lang=zh_CN", userInfoURL, accessToken, openID)
|
||||
uri := fmt.Sprintf(userInfoURL, accessToken, openID)
|
||||
var response []byte
|
||||
response, err = util.HTTPGet(uri)
|
||||
if err != nil {
|
||||
@@ -69,3 +70,21 @@ func (user *User) GetUserInfo(openID string) (userInfo *Info, err error) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateRemark 设置用户备注名
|
||||
func (user *User) UpdateRemark(openID, remark string) (err error) {
|
||||
var accessToken string
|
||||
accessToken, err = user.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(updateRemarkURL, accessToken)
|
||||
var response []byte
|
||||
response, err = util.PostJSON(uri, map[string]string{"openid": openID, "remark": remark})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return util.DecodeWithCommonError(response, "UpdateRemark")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// CommonError 微信返回的通用错误json
|
||||
type CommonError struct {
|
||||
ErrCode int64 `json:"errcode"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
}
|
||||
|
||||
// DecodeWithCommonError 将返回值按照CommonError解析
|
||||
func DecodeWithCommonError(response []byte, apiName string) (err error) {
|
||||
var commError CommonError
|
||||
err = json.Unmarshal(response, &commError)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if commError.ErrCode != 0 {
|
||||
return fmt.Errorf("%s Error , errcode=%d , errmsg=%s", apiName, commError.ErrCode, commError.ErrMsg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
10
wechat.go
10
wechat.go
@@ -10,10 +10,10 @@ import (
|
||||
"github.com/silenceper/wechat/material"
|
||||
"github.com/silenceper/wechat/menu"
|
||||
"github.com/silenceper/wechat/oauth"
|
||||
"github.com/silenceper/wechat/pay"
|
||||
"github.com/silenceper/wechat/server"
|
||||
"github.com/silenceper/wechat/template"
|
||||
"github.com/silenceper/wechat/user"
|
||||
"github.com/silenceper/wechat/pay"
|
||||
)
|
||||
|
||||
// Wechat struct
|
||||
@@ -27,9 +27,9 @@ type Config struct {
|
||||
AppSecret string
|
||||
Token string
|
||||
EncodingAESKey string
|
||||
PayMchID string //支付 - 商户 ID
|
||||
PayNotifyURL string //支付 - 接受微信支付结果通知的接口地址
|
||||
PayKey string //支付 - 商户后台设置的支付 key
|
||||
PayMchID string //支付 - 商户 ID
|
||||
PayNotifyURL string //支付 - 接受微信支付结果通知的接口地址
|
||||
PayKey string //支付 - 商户后台设置的支付 key
|
||||
Cache cache.Cache
|
||||
}
|
||||
|
||||
@@ -98,4 +98,4 @@ func (wc *Wechat) GetTemplate() *template.Template {
|
||||
// GetPay 返回支付消息的实例
|
||||
func (wc *Wechat) GetPay() *pay.Pay {
|
||||
return pay.NewPay(wc.Context)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user