1
0
mirror of https://github.com/silenceper/wechat.git synced 2026-02-04 21:02:25 +08:00

Merge pull request #3 from silenceper/master

Master
This commit is contained in:
Mongo
2018-11-01 14:30:58 +08:00
committed by GitHub
15 changed files with 288 additions and 84 deletions

1
.gitignore vendored
View File

@@ -25,3 +25,4 @@ _testmain.go
.DS_Store
.vscode/
vendor/*/
.idea/

View File

@@ -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

View File

@@ -272,7 +272,7 @@ type Reply struct {
}
```
注意:`retrun nil`表示什么也不做
注意:`return nil`表示什么也不做
#### 回复文本消息
```go

7
cache/redis.go vendored
View File

@@ -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()

View 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
}

View File

@@ -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")
}

View File

@@ -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 临时素材上传

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
View 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
}

View File

@@ -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")

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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)
}
}