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

Compare commits

...

53 Commits

Author SHA1 Message Date
silenceper
fcf289cfe3 Merge pull request #140 from akikistyle/add_member_card_field
Add member card field
2019-08-06 20:36:24 +08:00
sunyaqiu
c458f44917 Add member card field 2019-08-06 15:14:10 +08:00
silenceper
1a9600b49f go fmt 2019-07-15 18:25:01 +08:00
silenceper
dbb43ac7ad fix Unmarshal 2019-07-15 18:24:02 +08:00
silenceper
79ff0321e3 Update .travis.yml 2019-07-15 18:18:12 +08:00
silenceper
44dae6e950 Update .travis.yml 2019-07-15 18:14:53 +08:00
silenceper
894b1972d7 Merge pull request #124 from zdpdpdp/master
add: 自定义获取 access token 方法
2019-05-22 22:33:04 +08:00
silenceper
6c1ed39487 Merge pull request #126 from wispedia/master
修改文档中一些错误示例
2019-05-22 14:34:31 +08:00
任冠弛
529323e6b5 fix README😔 2019-05-22 11:51:24 +08:00
zdpdpdp
9a4d41563e add: 自定义获取 access token 方法 2019-05-21 13:03:07 +08:00
zdpdpdp
81f26cd6dc add: 自定义获取 access token 方法 2019-05-21 12:47:31 +08:00
silenceper
eae6caadb2 Merge pull request #122 from zdpdpdp/master
add: user.info subscribe info
2019-05-19 20:59:53 +08:00
zdpdpdp
fdd9768a96 add: user.info subscribe info 2019-05-16 15:27:26 +08:00
silenceper
42332eca27 Merge pull request #62 from MrSong0607/master
微信开放平台网页应用获取oauth回调地址
2019-04-23 21:28:39 +08:00
silenceper
b614b55cdf Merge pull request #119 from silenceper/fix-import
fix import path
2019-04-23 21:27:21 +08:00
silenceper
3fc556c425 add vendor 2019-04-23 21:21:31 +08:00
silenceper
3005852946 fix import path 2019-04-23 21:19:12 +08:00
silenceper
5652af6aab Merge pull request #103 from ckeyer/support_qr
生成带参数的二维码
2019-04-23 21:13:51 +08:00
silenceper
57bc7aabba Merge branch 'master' into support_qr 2019-04-23 21:13:38 +08:00
silenceper
3be94cd80d Merge pull request #118 from akikistyle/add-refund
添加退款接口,util http增加CA证书
2019-04-23 21:12:06 +08:00
silenceper
0871e2f8ed Merge pull request #104 from ckeyer/add_unionid
网页授权返回,添加`unionid`
2019-04-23 21:10:52 +08:00
silenceper
6e1ec1f00c Merge pull request #108 from JefferyWang/master
增加部分小程序接口
2019-04-23 21:09:56 +08:00
JefferyWang
823c54fda5 增加小程序用户数据解密 2019-04-10 17:26:21 +08:00
sunyaqiu
e66652f4b5 fix comment 2019-04-06 15:28:31 +08:00
sunyaqiu
f4f1860e67 fix some comment and struct field 2019-04-06 15:24:40 +08:00
sunyaqiu
02b3fcc648 add refund 2019-04-06 14:59:38 +08:00
sunyaqiu
4f5945fb0f add refund 2019-04-06 14:52:05 +08:00
silenceper
963a2d39bd Merge pull request #110 from silenceper/fix-import-path
Update component_access_token.go
2019-02-25 14:14:02 +08:00
silenceper
862e546367 Update component_access_token.go
update import path
2019-02-25 14:07:27 +08:00
jefferwang(王俊锋)
5677b60759 update readme and some comments 2019-02-19 16:41:15 +08:00
jefferwang(王俊锋)
61476d351d add PostJSONWithRespContentType 2019-02-19 15:11:45 +08:00
jefferwang(王俊锋)
eda287070d 增加部分小程序接口 2019-02-14 16:07:24 +08:00
Chuanjian Wang
443435343c add unionid 2018-12-31 11:24:23 +08:00
Chuanjian Wang
036183e5ff rcode 2018-12-23 16:25:25 +08:00
silenceper
593df23c46 Merge pull request #80 from ckeyer/suport_user_list
add list user openids
2018-12-21 13:39:36 +08:00
silenceper
50c490df31 Merge pull request #102 from silenceper/develop
Merge Develop
2018-12-21 13:37:07 +08:00
silenceper
7a19587f6a Merge pull request #101 from ckeyer/support_wechat_third_platform
微信第三方平台的授权流程
2018-12-21 13:31:41 +08:00
silenceper
91d1c77abc Merge pull request #100 from aikangs/develop
Develop
2018-12-21 13:31:04 +08:00
silenceper
4607ef001e Merge pull request #79 from ckeyer/add_memary_cache
add memory cache
2018-12-21 13:29:06 +08:00
Chuanjian Wang
fedd5a96ca add list user openids 2018-12-19 20:21:30 +08:00
Chuanjian Wang
3984f13c76 add memory cache 2018-12-19 20:01:23 +08:00
Chuanjian Wang
2da9755c58 add component funcs 2018-12-19 19:45:53 +08:00
song kang
d5302dbfdc 修复统一下单接口签名问题:如果参数值为空值,则不参与签名; 2018-12-19 18:58:51 +08:00
song kang
a6b1c56c25 统一下单接口修改tradeType:可以从param中指定使用什么tradeType;方便使用者根据不同使用场景调用 2018-12-19 17:59:17 +08:00
silenceper
388fd9ec07 Merge pull request #99 from ckeyer/support_wechat_third_platform
支持微信第三方平台相关接口
2018-12-19 14:14:43 +08:00
Chuanjian Wang
7163fc80c9 add infoType for message 2018-12-18 16:18:37 +08:00
silenceper
d67206b106 Update .travis.yml 2018-12-06 10:52:01 +08:00
silenceper
efdf09e133 Merge pull request #98 from silenceper/develop
merge to master
2018-12-06 10:47:20 +08:00
silenceper
9a34dca9a1 Merge pull request #91 from airylinus/master
统一下单提供返回所有结果的方法
2018-11-05 22:40:15 +08:00
zuomang
546ffb9155 修改统一下单接口的返回,提供全部返回,不仅仅是 prepay order 2018-11-01 14:47:26 +08:00
Mongo
f6b2887cee Merge pull request #3 from silenceper/master
Master
2018-11-01 14:30:58 +08:00
aris song
ebf6158b7c 微信开放平台网页应用获取oauth回调地址 2018-04-15 13:06:14 +08:00
Mongo
3e3cb594a3 Merge pull request #2 from silenceper/master
update
2018-03-14 00:38:45 +08:00
23 changed files with 1615 additions and 47 deletions

View File

@@ -1,10 +1,9 @@
language: go language: go
go: go:
- 1.12.x
- 1.11.x - 1.11.x
- 1.10.x - 1.10.x
- 1.9.x
- 1.8.x
services: services:
- memcached - memcached
@@ -12,7 +11,7 @@ services:
before_script: before_script:
- GO_FILES=$(find . -iname '*.go' -type f | grep -v /vendor/) - GO_FILES=$(find . -iname '*.go' -type f | grep -v /vendor/)
- go get github.com/golang/lint/golint - go get golang.org/x/lint/golint
script: script:
- go test -v -race ./... - go test -v -race ./...

117
README.md
View File

@@ -96,6 +96,7 @@ Cache主要用来保存全局access_token以及js-sdk中的ticket
- 检验access_token是否有效 - 检验access_token是否有效
- 获取js-sdk配置 - 获取js-sdk配置
- [素材管理](#素材管理) - [素材管理](#素材管理)
- [小程序开发](#小程序开发)
## 消息管理 ## 消息管理
@@ -335,14 +336,14 @@ Url :点击图文消息跳转链接
## 自定义菜单 ## 自定义菜单
通过` wechat.GetMenu(req, writer)`获取menu的实例 通过` wechat.GetMenu()`获取menu的实例
### 自定义菜单创建接口 ### 自定义菜单创建接口
以下是一个创建二级菜单的例子 以下是一个创建二级菜单的例子
```go ```go
mu := wc.GetMenu(c.Request, c.Writer) mu := wc.GetMenu()
buttons := make([]*menu.Button, 1) buttons := make([]*menu.Button, 1)
btn := new(menu.Button) btn := new(menu.Button)
@@ -402,7 +403,7 @@ func (btn *Button) SetViewLimitedButton(name, mediaID string) {
### 自定义菜单查询接口 ### 自定义菜单查询接口
```go ```go
mu := wc.GetMenu(c.Request, c.Writer) mu := wc.GetMenu()
resMenu,err:=mu.GetMenu() resMenu,err:=mu.GetMenu()
``` ```
>返回结果 resMenu 结构参考 ./menu/menu.go 中ResMenu 结构体 >返回结果 resMenu 结构参考 ./menu/menu.go 中ResMenu 结构体
@@ -410,7 +411,7 @@ resMenu,err:=mu.GetMenu()
### 自定义菜单删除接口 ### 自定义菜单删除接口
```go ```go
mu := wc.GetMenu(c.Request, c.Writer) mu := wc.GetMenu()
err:=mu.DeleteMenu() err:=mu.DeleteMenu()
``` ```
@@ -458,7 +459,7 @@ func (menu *Menu) GetCurrentSelfMenuInfo() (resSelfMenuInfo ResSelfMenuInfo, err
**1.发起授权** **1.发起授权**
```go ```go
oauth := wc.GetOauth(c.Request, c.Writer) oauth := wc.GetOauth()
err := oauth.Redirect("跳转的绝对地址", "snsapi_userinfo", "123dd123") err := oauth.Redirect("跳转的绝对地址", "snsapi_userinfo", "123dd123")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@@ -505,7 +506,7 @@ func (oauth *Oauth) CheckAccessToken(accessToken, openID string) (b bool, err er
### 获取js-sdk配置 ### 获取js-sdk配置
```go ```go
js := wc.GetJs(c.Request, c.Writer) js := wc.GetJs()
cfg, err := js.GetConfig("传入需要的调用js-sdk的url地址") cfg, err := js.GetConfig("传入需要的调用js-sdk的url地址")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@@ -529,6 +530,110 @@ type Config struct {
[素材管理API](https://godoc.org/github.com/silenceper/wechat/material#Material) [素材管理API](https://godoc.org/github.com/silenceper/wechat/material#Material)
## 小程序开发
获取小程序操作对象
``` go
memCache=cache.NewMemcache("127.0.0.1:11211")
config := &wechat.Config{
AppID: "xxx",
AppSecret: "xxx",
Cache: memCache=cache.NewMemcache("127.0.0.1:11211"),
}
wc := wechat.NewWechat(config)
wxa := wc.GetMiniProgram()
```
### 小程序登录凭证校验
``` go
func (wxa *MiniProgram) Code2Session(jsCode string) (result ResCode2Session, err error)
```
### 小程序数据统计
**获取用户访问小程序日留存**
``` go
func (wxa *MiniProgram) GetAnalysisDailyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error)
```
**获取用户访问小程序月留存**
``` go
func (wxa *MiniProgram) GetAnalysisMonthlyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error)
```
**获取用户访问小程序周留存**
``` go
func (wxa *MiniProgram) GetAnalysisWeeklyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error)
```
**获取用户访问小程序数据概况**
``` go
func (wxa *MiniProgram) GetAnalysisDailySummary(beginDate, endDate string) (result ResAnalysisDailySummary, err error)
```
**获取用户访问小程序数据日趋势**
``` go
func (wxa *MiniProgram) GetAnalysisDailyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error)
```
**获取用户访问小程序数据月趋势**
``` go
func (wxa *MiniProgram) GetAnalysisMonthlyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error)
```
**获取用户访问小程序数据周趋势**
``` go
func (wxa *MiniProgram) GetAnalysisWeeklyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error)
```
**获取小程序新增或活跃用户的画像分布数据**
``` go
func (wxa *MiniProgram) GetAnalysisUserPortrait(beginDate, endDate string) (result ResAnalysisUserPortrait, err error)
```
**获取用户小程序访问分布数据**
``` go
func (wxa *MiniProgram) GetAnalysisVisitDistribution(beginDate, endDate string) (result ResAnalysisVisitDistribution, err error)
```
**获取小程序页面访问数据**
``` go
func (wxa *MiniProgram) GetAnalysisVisitPage(beginDate, endDate string) (result ResAnalysisVisitPage, err error)
```
### 小程序二维码生成
**获取小程序二维码,适用于需要的码数量较少的业务场景**
``` go
func (wxa *MiniProgram) CreateWXAQRCode(coderParams QRCoder) (response []byte, err error)
```
**获取小程序码,适用于需要的码数量较少的业务场景**
``` go
func (wxa *MiniProgram) GetWXACode(coderParams QRCoder) (response []byte, err error)
```
**获取小程序码,适用于需要的码数量极多的业务场景**
``` go
func (wxa *MiniProgram) GetWXACodeUnlimit(coderParams QRCoder) (response []byte, err error)
```
更多API使用请参考 godoc 更多API使用请参考 godoc
[https://godoc.org/github.com/silenceper/wechat](https://godoc.org/github.com/silenceper/wechat) [https://godoc.org/github.com/silenceper/wechat](https://godoc.org/github.com/silenceper/wechat)

74
cache/memory.go vendored Normal file
View File

@@ -0,0 +1,74 @@
package cache
import (
"sync"
"time"
)
//Memory struct contains *memcache.Client
type Memory struct {
sync.Mutex
data map[string]*data
}
type data struct {
Data interface{}
Expired time.Time
}
//NewMemory create new memcache
func NewMemory() *Memory {
return &Memory{
data: map[string]*data{},
}
}
//Get return cached value
func (mem *Memory) Get(key string) interface{} {
if ret, ok := mem.data[key]; ok {
if ret.Expired.Before(time.Now()) {
mem.deleteKey(key)
return nil
}
return ret.Data
}
return nil
}
// IsExist check value exists in memcache.
func (mem *Memory) IsExist(key string) bool {
if ret, ok := mem.data[key]; ok {
if ret.Expired.Before(time.Now()) {
mem.deleteKey(key)
return false
}
return true
}
return false
}
//Set cached value with key and expire time.
func (mem *Memory) Set(key string, val interface{}, timeout time.Duration) (err error) {
mem.Lock()
defer mem.Unlock()
mem.data[key] = &data{
Data: val,
Expired: time.Now().Add(timeout),
}
return nil
}
//Delete delete value in memcache.
func (mem *Memory) Delete(key string) error {
return mem.deleteKey(key)
}
// deleteKey
func (mem *Memory) deleteKey(key string) error {
mem.Lock()
defer mem.Unlock()
delete(mem.data, key)
return nil
}

View File

@@ -22,16 +22,27 @@ type ResAccessToken struct {
ExpiresIn int64 `json:"expires_in"` ExpiresIn int64 `json:"expires_in"`
} }
//GetAccessTokenFunc 获取 access token 的函数签名
type GetAccessTokenFunc func(ctx *Context) (accessToken string, err error)
//SetAccessTokenLock 设置读写锁一个appID一个读写锁 //SetAccessTokenLock 设置读写锁一个appID一个读写锁
func (ctx *Context) SetAccessTokenLock(l *sync.RWMutex) { func (ctx *Context) SetAccessTokenLock(l *sync.RWMutex) {
ctx.accessTokenLock = l ctx.accessTokenLock = l
} }
//SetGetAccessTokenFunc 设置自定义获取accessToken的方式, 需要自己实现缓存
func (ctx *Context) SetGetAccessTokenFunc(f GetAccessTokenFunc) {
ctx.accessTokenFunc = f
}
//GetAccessToken 获取access_token //GetAccessToken 获取access_token
func (ctx *Context) GetAccessToken() (accessToken string, err error) { func (ctx *Context) GetAccessToken() (accessToken string, err error) {
ctx.accessTokenLock.Lock() ctx.accessTokenLock.Lock()
defer ctx.accessTokenLock.Unlock() defer ctx.accessTokenLock.Unlock()
if ctx.accessTokenFunc != nil {
return ctx.accessTokenFunc(ctx)
}
accessTokenCacheKey := fmt.Sprintf("access_token_%s", ctx.AppID) accessTokenCacheKey := fmt.Sprintf("access_token_%s", ctx.AppID)
val := ctx.Cache.Get(accessTokenCacheKey) val := ctx.Cache.Get(accessTokenCacheKey)
if val != nil { if val != nil {

View File

@@ -0,0 +1,30 @@
package context
import (
"sync"
"testing"
)
func TestContext_SetCustomAccessTokenFunc(t *testing.T) {
ctx := Context{
accessTokenLock: new(sync.RWMutex),
}
f := func(ctx *Context) (accessToken string, err error) {
return "fake token", nil
}
ctx.SetGetAccessTokenFunc(f)
res, err := ctx.GetAccessToken()
if res != "fake token" || err != nil {
t.Error("expect fake token but error")
}
}
func TestContext_NoSetCustomAccessTokenFunc(t *testing.T) {
ctx := Context{
accessTokenLock: new(sync.RWMutex),
}
if ctx.accessTokenFunc != nil {
t.Error("error accessTokenFunc")
}
}

View File

@@ -0,0 +1,221 @@
package context
import (
"encoding/json"
"fmt"
"time"
"github.com/silenceper/wechat/util"
)
const (
componentAccessTokenURL = "https://api.weixin.qq.com/cgi-bin/component/api_component_token"
getPreCodeURL = "https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=%s"
queryAuthURL = "https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=%s"
refreshTokenURL = "https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=%s"
getComponentInfoURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=%s"
getComponentConfigURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option?component_access_token=%s"
)
// ComponentAccessToken 第三方平台
type ComponentAccessToken struct {
AccessToken string `json:"component_access_token"`
ExpiresIn int64 `json:"expires_in"`
}
// GetComponentAccessToken 获取 ComponentAccessToken
func (ctx *Context) GetComponentAccessToken() (string, error) {
accessTokenCacheKey := fmt.Sprintf("component_access_token_%s", ctx.AppID)
val := ctx.Cache.Get(accessTokenCacheKey)
if val == nil {
return "", fmt.Errorf("cann't get component access token")
}
return val.(string), nil
}
// SetComponentAccessToken 通过component_verify_ticket 获取 ComponentAccessToken
func (ctx *Context) SetComponentAccessToken(verifyTicket string) (*ComponentAccessToken, error) {
body := map[string]string{
"component_appid": ctx.AppID,
"component_appsecret": ctx.AppSecret,
"component_verify_ticket": verifyTicket,
}
respBody, err := util.PostJSON(componentAccessTokenURL, body)
if err != nil {
return nil, err
}
at := &ComponentAccessToken{}
if err := json.Unmarshal(respBody, at); err != nil {
return nil, err
}
accessTokenCacheKey := fmt.Sprintf("component_access_token_%s", ctx.AppID)
expires := at.ExpiresIn - 1500
ctx.Cache.Set(accessTokenCacheKey, at.AccessToken, time.Duration(expires)*time.Second)
return at, nil
}
// GetPreCode 获取预授权码
func (ctx *Context) GetPreCode() (string, error) {
cat, err := ctx.GetComponentAccessToken()
if err != nil {
return "", err
}
req := map[string]string{
"component_appid": ctx.AppID,
}
uri := fmt.Sprintf(getPreCodeURL, cat)
body, err := util.PostJSON(uri, req)
if err != nil {
return "", err
}
var ret struct {
PreCode string `json:"pre_auth_code"`
}
if err := json.Unmarshal(body, &ret); err != nil {
return "", err
}
return ret.PreCode, nil
}
// ID 微信返回接口中各种类型字段
type ID struct {
ID int `json:"id"`
}
// AuthBaseInfo 授权的基本信息
type AuthBaseInfo struct {
AuthrAccessToken
FuncInfo []AuthFuncInfo `json:"func_info"`
}
// AuthFuncInfo 授权的接口内容
type AuthFuncInfo struct {
FuncscopeCategory ID `json:"funcscope_category"`
}
// AuthrAccessToken 授权方AccessToken
type AuthrAccessToken struct {
Appid string `json:"authorizer_appid"`
AccessToken string `json:"authorizer_access_token"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"authorizer_refresh_token"`
}
// QueryAuthCode 使用授权码换取公众号或小程序的接口调用凭据和授权信息
func (ctx *Context) QueryAuthCode(authCode string) (*AuthBaseInfo, error) {
cat, err := ctx.GetComponentAccessToken()
if err != nil {
return nil, err
}
req := map[string]string{
"component_appid": ctx.AppID,
"authorization_code": authCode,
}
uri := fmt.Sprintf(queryAuthURL, cat)
body, err := util.PostJSON(uri, req)
if err != nil {
return nil, err
}
var ret struct {
Info *AuthBaseInfo `json:"authorization_info"`
}
if err := json.Unmarshal(body, &ret); err != nil {
return nil, err
}
return ret.Info, nil
}
// RefreshAuthrToken 获取(刷新)授权公众号或小程序的接口调用凭据(令牌)
func (ctx *Context) RefreshAuthrToken(appid, refreshToken string) (*AuthrAccessToken, error) {
cat, err := ctx.GetComponentAccessToken()
if err != nil {
return nil, err
}
req := map[string]string{
"component_appid": ctx.AppID,
"authorizer_appid": appid,
"authorizer_refresh_token": refreshToken,
}
uri := fmt.Sprintf(refreshTokenURL, cat)
body, err := util.PostJSON(uri, req)
if err != nil {
return nil, err
}
ret := &AuthrAccessToken{}
if err := json.Unmarshal(body, ret); err != nil {
return nil, err
}
authrTokenKey := "authorizer_access_token_" + appid
ctx.Cache.Set(authrTokenKey, ret.AccessToken, time.Minute*80)
return ret, nil
}
// GetAuthrAccessToken 获取授权方AccessToken
func (ctx *Context) GetAuthrAccessToken(appid string) (string, error) {
authrTokenKey := "authorizer_access_token_" + appid
val := ctx.Cache.Get(authrTokenKey)
if val == nil {
return "", fmt.Errorf("cannot get authorizer %s access token", appid)
}
return val.(string), nil
}
// AuthorizerInfo 授权方详细信息
type AuthorizerInfo struct {
NickName string `json:"nick_name"`
HeadImg string `json:"head_img"`
ServiceTypeInfo ID `json:"service_type_info"`
VerifyTypeInfo ID `json:"verify_type_info"`
UserName string `json:"user_name"`
PrincipalName string `json:"principal_name"`
BusinessInfo struct {
OpenStore string `json:"open_store"`
OpenScan string `json:"open_scan"`
OpenPay string `json:"open_pay"`
OpenCard string `json:"open_card"`
OpenShake string `json:"open_shake"`
}
Alias string `json:"alias"`
QrcodeURL string `json:"qrcode_url"`
}
// GetAuthrInfo 获取授权方的帐号基本信息
func (ctx *Context) GetAuthrInfo(appid string) (*AuthorizerInfo, *AuthBaseInfo, error) {
cat, err := ctx.GetComponentAccessToken()
if err != nil {
return nil, nil, err
}
req := map[string]string{
"component_appid": ctx.AppID,
"authorizer_appid": appid,
}
uri := fmt.Sprintf(getComponentInfoURL, cat)
body, err := util.PostJSON(uri, req)
if err != nil {
return nil, nil, err
}
var ret struct {
AuthorizerInfo *AuthorizerInfo `json:"authorizer_info"`
AuthorizationInfo *AuthBaseInfo `json:"authorization_info"`
}
if err := json.Unmarshal(body, &ret); err != nil {
return nil, nil, err
}
return ret.AuthorizerInfo, ret.AuthorizationInfo, nil
}

19
context/component_test.go Normal file
View File

@@ -0,0 +1,19 @@
package context
import (
"encoding/json"
"testing"
)
var testdata = `{"authorizer_info":{"nick_name":"就爱浪","head_img":"http:\/\/wx.qlogo.cn\/mmopen\/xPKCxELaaj6hiaTZGv19oQPBJibb7hBoKmNOjQibCNOUycE8iaBhiaHOA6eC8hadQSAUZTuHUJl4qCIbCQGjSWialicfzWh4mdxuejY\/0","service_type_info":{"id":1},"verify_type_info":{"id":-1},"user_name":"gh_dcdbaa6f1687","alias":"ckeyer","qrcode_url":"http:\/\/mmbiz.qpic.cn\/mmbiz_jpg\/FribWCoIzQbAX7R1PQ8iaxGonqKp0doYD2ibhC0uhx11LrRcblASiazsbQJTJ4icQnMzfH7G0SUPuKbibTA8Cs4uk5WQ\/0","business_info":{"open_pay":0,"open_shake":0,"open_scan":0,"open_card":0,"open_store":0},"idc":1,"principal_name":"个人","signature":"不折腾会死。"},"authorization_info":{"authorizer_appid":"yyyyy","authorizer_refresh_token":"xxxx","func_info":[{"funcscope_category":{"id":1}},{"funcscope_category":{"id":15}},{"funcscope_category":{"id":4}},{"funcscope_category":{"id":7}},{"funcscope_category":{"id":2}},{"funcscope_category":{"id":3}},{"funcscope_category":{"id":11}},{"funcscope_category":{"id":6}},{"funcscope_category":{"id":5}},{"funcscope_category":{"id":8}},{"funcscope_category":{"id":13}},{"funcscope_category":{"id":9}},{"funcscope_category":{"id":12}},{"funcscope_category":{"id":22}},{"funcscope_category":{"id":23}},{"funcscope_category":{"id":24},"confirm_info":{"need_confirm":0,"already_confirm":0,"can_confirm":0}},{"funcscope_category":{"id":26}},{"funcscope_category":{"id":27},"confirm_info":{"need_confirm":0,"already_confirm":0,"can_confirm":0}},{"funcscope_category":{"id":33},"confirm_info":{"need_confirm":0,"already_confirm":0,"can_confirm":0}},{"funcscope_category":{"id":35}}]}}`
// TestDecode
func TestDecode(t *testing.T) {
var ret struct {
AuthorizerInfo *AuthorizerInfo `json:"authorizer_info"`
AuthorizationInfo *AuthBaseInfo `json:"authorization_info"`
}
json.Unmarshal([]byte(testdata), &ret)
t.Logf("%+v", ret.AuthorizerInfo)
t.Logf("%+v", ret.AuthorizationInfo)
}

View File

@@ -27,6 +27,9 @@ type Context struct {
//jsAPITicket 读写锁 同一个AppID一个 //jsAPITicket 读写锁 同一个AppID一个
jsAPITicketLock *sync.RWMutex jsAPITicketLock *sync.RWMutex
//accessTokenFunc 自定义获取 access token 的方法
accessTokenFunc GetAccessTokenFunc
} }
// Query returns the keyed url query value if it exists // Query returns the keyed url query value if it exists

View File

@@ -63,7 +63,7 @@ func (material *Material) AddNews(articles []*Article) (mediaID string, err erro
uri := fmt.Sprintf("%s?access_token=%s", addNewsURL, accessToken) uri := fmt.Sprintf("%s?access_token=%s", addNewsURL, accessToken)
responseBytes, err := util.PostJSON(uri, req) responseBytes, err := util.PostJSON(uri, req)
var res resArticles var res resArticles
err = json.Unmarshal(responseBytes, res) err = json.Unmarshal(responseBytes, &res)
if err != nil { if err != nil {
return return
} }

View File

@@ -8,6 +8,9 @@ type MsgType string
// EventType 事件类型 // EventType 事件类型
type EventType string type EventType string
// InfoType 第三方平台授权事件类型
type InfoType string
const ( const (
//MsgTypeText 表示文本消息 //MsgTypeText 表示文本消息
MsgTypeText MsgType = "text" MsgTypeText MsgType = "text"
@@ -62,6 +65,17 @@ const (
EventTemplateSendJobFinish = "TEMPLATESENDJOBFINISH" EventTemplateSendJobFinish = "TEMPLATESENDJOBFINISH"
) )
const (
// InfoTypeVerifyTicket 返回ticket
InfoTypeVerifyTicket InfoType = "component_verify_ticket"
// InfoTypeAuthorized 授权
InfoTypeAuthorized = "authorized"
// InfoTypeUnauthorized 取消授权
InfoTypeUnauthorized = "unauthorized"
// InfoTypeUpdateAuthorized 更新授权
InfoTypeUpdateAuthorized = "updateauthorized"
)
//MixMessage 存放所有微信发送过来的消息和事件 //MixMessage 存放所有微信发送过来的消息和事件
type MixMessage struct { type MixMessage struct {
CommonToken CommonToken
@@ -110,6 +124,26 @@ type MixMessage struct {
Label string `xml:"Label"` Label string `xml:"Label"`
Poiname string `xml:"Poiname"` Poiname string `xml:"Poiname"`
} }
// 第三方平台相关
InfoType InfoType `xml:"InfoType"`
AppID string `xml:"AppId"`
ComponentVerifyTicket string `xml:"ComponentVerifyTicket"`
AuthorizerAppid string `xml:"AuthorizerAppid"`
AuthorizationCode string `xml:"AuthorizationCode"`
AuthorizationCodeExpiredTime int64 `xml:"AuthorizationCodeExpiredTime"`
PreAuthCode string `xml:"PreAuthCode"`
// 卡券相关
CardID string `xml:"CardId"`
RefuseReason string `xml:"RefuseReason"`
IsGiveByFriend int32 `xml:"IsGiveByFriend"`
FriendUserName string `xml:"FriendUserName"`
UserCardCode string `xml:"UserCardCode"`
OldUserCardCode string `xml:"OldUserCardCode"`
OuterStr string `xml:"OuterStr"`
IsRestoreMemberCard int32 `xml:"IsRestoreMemberCard"`
UnionID string `xml:"UnionId"`
} }
//EventPic 发图事件推送 //EventPic 发图事件推送

305
miniprogram/analysis.go Normal file
View File

@@ -0,0 +1,305 @@
package miniprogram
import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/util"
)
const (
// 获取用户访问小程序日留存
getAnalysisDailyRetainURL = "https://api.weixin.qq.com/datacube/getweanalysisappiddailyretaininfo?access_token=%s"
// 获取用户访问小程序月留存
getAnalysisMonthlyRetainURL = "https://api.weixin.qq.com/datacube/getweanalysisappidmonthlyretaininfo?access_token=%s"
// 获取用户访问小程序周留存
getAnalysisWeeklyRetainURL = "https://api.weixin.qq.com/datacube/getweanalysisappidweeklyretaininfo?access_token=%s"
// 获取用户访问小程序数据概况
getAnalysisDailySummaryURL = "https://api.weixin.qq.com/datacube/getweanalysisappiddailysummarytrend?access_token=%s"
// 获取用户访问小程序数据日趋势
getAnalysisDailyVisitTrendURL = "https://api.weixin.qq.com/datacube/getweanalysisappiddailyvisittrend?access_token=%s"
// 获取用户访问小程序数据月趋势
getAnalysisMonthlyVisitTrendURL = "https://api.weixin.qq.com/datacube/getweanalysisappidmonthlyvisittrend?access_token=%s"
// 获取用户访问小程序数据周趋势
getAnalysisWeeklyVisitTrendURL = "https://api.weixin.qq.com/datacube/getweanalysisappidweeklyvisittrend?access_token=%s"
// 获取小程序新增或活跃用户的画像分布数据
getAnalysisUserPortraitURL = "https://api.weixin.qq.com/datacube/getweanalysisappiduserportrait?access_token=%s"
// 获取用户小程序访问分布数据
getAnalysisVisitDistributionURL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitdistribution?access_token=%s"
// 访问页面
getAnalysisVisitPageURL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitpage?access_token=%s"
)
// fetchData 拉取统计数据
func (wxa *MiniProgram) fetchData(urlStr string, body interface{}) (response []byte, err error) {
var accessToken string
accessToken, err = wxa.GetAccessToken()
if err != nil {
return
}
urlStr = fmt.Sprintf(urlStr, accessToken)
response, err = util.PostJSON(urlStr, body)
return
}
// AnalysisRetainItem 留存项结构
type AnalysisRetainItem struct {
Key int `json:"key"` // 标识0开始表示当天1表示1甜后以此类推
Value int `json:"value"` // key对应日期的新增用户数/活跃用户数key=0时或留存用户数k>0时
}
// ResAnalysisRetain 小程序留存数据返回
type ResAnalysisRetain struct {
util.CommonError
RefDate string `json:"ref_date"` // 日期
VisitUVNew []AnalysisRetainItem `json:"visit_uv_new"` // 新增用户留存
VisitUV []AnalysisRetainItem `json:"visit_uv"` // 活跃用户留存
}
// getAnalysisRetain 获取用户访问小程序留存数据(日、月、周)
func (wxa *MiniProgram) getAnalysisRetain(urlStr string, beginDate, endDate string) (result ResAnalysisRetain, err error) {
body := map[string]string{
"begin_date": beginDate,
"end_date": endDate,
}
response, err := wxa.fetchData(urlStr, body)
if err != nil {
return
}
err = json.Unmarshal(response, &result)
if err != nil {
return
}
if result.ErrCode != 0 {
err = fmt.Errorf("getAnalysisRetain error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
return
}
return
}
// GetAnalysisDailyRetain 获取用户访问小程序日留存
func (wxa *MiniProgram) GetAnalysisDailyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
return wxa.getAnalysisRetain(getAnalysisDailyRetainURL, beginDate, endDate)
}
// GetAnalysisMonthlyRetain 获取用户访问小程序月留存
func (wxa *MiniProgram) GetAnalysisMonthlyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
return wxa.getAnalysisRetain(getAnalysisMonthlyRetainURL, beginDate, endDate)
}
// GetAnalysisWeeklyRetain 获取用户访问小程序周留存
func (wxa *MiniProgram) GetAnalysisWeeklyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
return wxa.getAnalysisRetain(getAnalysisWeeklyRetainURL, beginDate, endDate)
}
// ResAnalysisDailySummary 小程序访问数据概况
type ResAnalysisDailySummary struct {
util.CommonError
List []struct {
RefDate string `json:"ref_date"` // 日期
VisitTotal int `json:"visit_total"` // 累计用户数
SharePV int `json:"share_pv"` // 转发次数
ShareUV int `json:"share_uv"` // 转发人数
} `json:"list"`
}
// GetAnalysisDailySummary 获取用户访问小程序数据概况
func (wxa *MiniProgram) GetAnalysisDailySummary(beginDate, endDate string) (result ResAnalysisDailySummary, err error) {
body := map[string]string{
"begin_date": beginDate,
"end_date": endDate,
}
response, err := wxa.fetchData(getAnalysisDailySummaryURL, body)
if err != nil {
return
}
fmt.Println(string(response))
err = json.Unmarshal(response, &result)
if err != nil {
return
}
if result.ErrCode != 0 {
err = fmt.Errorf("GetAnalysisDailySummary error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
return
}
return
}
// ResAnalysisVisitTrend 小程序访问数据趋势(日、月、周)
type ResAnalysisVisitTrend struct {
util.CommonError
List []struct {
RefDate string `json:"ref_date"` // 日期
SessionCnt int `json:"session_cnt"` // 打开次数
VisitPV int `json:"visit_pv"` // 访问次数
VisitUV int `json:"visit_uv"` // 访问人数
VisitUVNew int `json:"visit_uv_new"` // 新用户数
StayTimeUV float64 `json:"stay_time_uv"` // 人均停留时长
StayTimeSession float64 `json:"stay_time_session"` // 次均停留时常
VisitDepth float64 `json:"visit_depth"` // 平均访问深度
} `json:"list"`
}
// getAnalysisRetain 获取小程序访问数据趋势(日、月、周)
func (wxa *MiniProgram) getAnalysisVisitTrend(urlStr string, beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
body := map[string]string{
"begin_date": beginDate,
"end_date": endDate,
}
response, err := wxa.fetchData(urlStr, body)
if err != nil {
return
}
err = json.Unmarshal(response, &result)
if err != nil {
return
}
if result.ErrCode != 0 {
err = fmt.Errorf("getAnalysisVisitTrend error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
return
}
return
}
// GetAnalysisDailyVisitTrend 获取用户访问小程序数据日趋势
func (wxa *MiniProgram) GetAnalysisDailyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
return wxa.getAnalysisVisitTrend(getAnalysisDailyVisitTrendURL, beginDate, endDate)
}
// GetAnalysisMonthlyVisitTrend 获取用户访问小程序数据月趋势
func (wxa *MiniProgram) GetAnalysisMonthlyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
return wxa.getAnalysisVisitTrend(getAnalysisMonthlyVisitTrendURL, beginDate, endDate)
}
// GetAnalysisWeeklyVisitTrend 获取用户访问小程序数据周趋势
func (wxa *MiniProgram) GetAnalysisWeeklyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
return wxa.getAnalysisVisitTrend(getAnalysisWeeklyVisitTrendURL, beginDate, endDate)
}
// UserPortraitItem 用户画像项目
type UserPortraitItem struct {
ID int `json:"id"` // 属性值id
Name string `json:"name"` // 属性值名称
AccessSourceVisitUV int `json:"access_source_visit_uv"` // 该场景访问uv
}
// UserPortrait 用户画像
type UserPortrait struct {
Index int `json:"index"` // 分布类型
Province []UserPortraitItem `json:"province"` // 省份,如北京、广东等
City []UserPortraitItem `json:"city"` // 城市,如北京、广州等
Genders []UserPortraitItem `json:"genders"` // 性别,包括男、女、未知
Platforms []UserPortraitItem `json:"platforms"` // 终端类型包括iPhone, android, 其他
Devices []UserPortraitItem `json:"devices"` // 机型如苹果iPhone 6, OPPO R9等
Ages []UserPortraitItem `json:"ages"` // 年龄包括17岁以下、18-24对等区间
}
// ResAnalysisUserPortrait 小程序新增或活跃用户的画像分布数据返回
type ResAnalysisUserPortrait struct {
util.CommonError
RefDate string `json:"ref_date"` // 日期
VisitUVNew UserPortrait `json:"visit_uv_new"` // 新用户画像
VisitUV UserPortrait `json:"visit_uv"` // 活跃用户画像
}
// GetAnalysisUserPortrait 获取小程序新增或活跃用户的画像分布数据
func (wxa *MiniProgram) GetAnalysisUserPortrait(beginDate, endDate string) (result ResAnalysisUserPortrait, err error) {
body := map[string]string{
"begin_date": beginDate,
"end_date": endDate,
}
response, err := wxa.fetchData(getAnalysisUserPortraitURL, body)
if err != nil {
return
}
err = json.Unmarshal(response, &result)
if err != nil {
return
}
if result.ErrCode != 0 {
err = fmt.Errorf("GetAnalysisUserPortrait error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
return
}
return
}
// VisitDistributionIndexItem 访问分数数据结构
type VisitDistributionIndexItem struct {
Key int `json:"key"` // 场景id
Value int `json:"value"` // 该场景id访问pv
AccessSourceVisitUV int `json:"access_source_visit_uv"` // 该场景id访问uv
}
// VisitDistributionIndex 访问分布单分布类型数据
type VisitDistributionIndex struct {
Index string `json:"index"` // 分布类型
ItemList []VisitDistributionIndexItem `json:"item_list"` // 分布数据列表
}
// ResAnalysisVisitDistribution 小程序访问分布数据返回
type ResAnalysisVisitDistribution struct {
util.CommonError
RefDate string `json:"ref_date"` // 日期
List []VisitDistributionIndex `json:"list"` // 数据列表
}
// GetAnalysisVisitDistribution 获取用户小程序访问分布数据
func (wxa *MiniProgram) GetAnalysisVisitDistribution(beginDate, endDate string) (result ResAnalysisVisitDistribution, err error) {
body := map[string]string{
"begin_date": beginDate,
"end_date": endDate,
}
response, err := wxa.fetchData(getAnalysisVisitDistributionURL, body)
if err != nil {
return
}
err = json.Unmarshal(response, &result)
if err != nil {
return
}
if result.ErrCode != 0 {
err = fmt.Errorf("GetAnalysisVisitDistribution error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
return
}
return
}
// VisitPageItem 访问单个页面的数据结构
type VisitPageItem struct {
PagePath string `json:"page_path"` // 页面路径
PageVisitPV int `json:"page_visit_pv"` // 访问次数
PageVisitUV int `json:"page_visit_uv"` // 访问人数
PageStaytimePV float64 `json:"page_staytime_pv"` // 次均停留时常
EntrypagePV int `json:"entrypage_pv"` // 进入页次数
ExitpagePV int `json:"exitpage_pv"` // 退出页次数
PageSharePV int `json:"page_share_pv"` // 转发次数
PageShareUV int `json:"page_share_uv"` // 转发人数
}
// ResAnalysisVisitPage 访问小程序页面访问数据返回
type ResAnalysisVisitPage struct {
util.CommonError
RefDate string `json:"ref_date"` // 日期
List []VisitPageItem `json:"list"` // 数据列表
}
// GetAnalysisVisitPage 获取小程序页面访问数据
func (wxa *MiniProgram) GetAnalysisVisitPage(beginDate, endDate string) (result ResAnalysisVisitPage, err error) {
body := map[string]string{
"begin_date": beginDate,
"end_date": endDate,
}
response, err := wxa.fetchData(getAnalysisVisitPageURL, body)
if err != nil {
return
}
err = json.Unmarshal(response, &result)
if err != nil {
return
}
if result.ErrCode != 0 {
err = fmt.Errorf("GetAnalysisVisitPage error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
return
}
return
}

93
miniprogram/decrypt.go Normal file
View File

@@ -0,0 +1,93 @@
package miniprogram
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/json"
"errors"
)
var (
// ErrAppIDNotMatch appid不匹配
ErrAppIDNotMatch = errors.New("app id not match")
// ErrInvalidBlockSize block size不合法
ErrInvalidBlockSize = errors.New("invalid block size")
// ErrInvalidPKCS7Data PKCS7数据不合法
ErrInvalidPKCS7Data = errors.New("invalid PKCS7 data")
// ErrInvalidPKCS7Padding 输入padding失败
ErrInvalidPKCS7Padding = errors.New("invalid padding on input")
)
// UserInfo 用户信息
type UserInfo struct {
OpenID string `json:"openId"`
UnionID string `json:"unionId"`
NickName string `json:"nickName"`
Gender int `json:"gender"`
City string `json:"city"`
Province string `json:"province"`
Country string `json:"country"`
AvatarURL string `json:"avatarUrl"`
Language string `json:"language"`
Watermark struct {
Timestamp int64 `json:"timestamp"`
AppID string `json:"appid"`
} `json:"watermark"`
}
// pkcs7Unpad returns slice of the original data without padding
func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) {
if blockSize <= 0 {
return nil, ErrInvalidBlockSize
}
if len(data)%blockSize != 0 || len(data) == 0 {
return nil, ErrInvalidPKCS7Data
}
c := data[len(data)-1]
n := int(c)
if n == 0 || n > len(data) {
return nil, ErrInvalidPKCS7Padding
}
for i := 0; i < n; i++ {
if data[len(data)-n+i] != c {
return nil, ErrInvalidPKCS7Padding
}
}
return data[:len(data)-n], nil
}
// Decrypt 解密数据
func (wxa *MiniProgram) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo, error) {
aesKey, err := base64.StdEncoding.DecodeString(sessionKey)
if err != nil {
return nil, err
}
cipherText, err := base64.StdEncoding.DecodeString(encryptedData)
if err != nil {
return nil, err
}
ivBytes, err := base64.StdEncoding.DecodeString(iv)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(aesKey)
if err != nil {
return nil, err
}
mode := cipher.NewCBCDecrypter(block, ivBytes)
mode.CryptBlocks(cipherText, cipherText)
cipherText, err = pkcs7Unpad(cipherText, block.BlockSize())
if err != nil {
return nil, err
}
var userInfo UserInfo
err = json.Unmarshal(cipherText, &userInfo)
if err != nil {
return nil, err
}
if userInfo.Watermark.AppID != wxa.AppID {
return nil, ErrAppIDNotMatch
}
return &userInfo, nil
}

View File

@@ -0,0 +1,17 @@
package miniprogram
import (
"github.com/silenceper/wechat/context"
)
// MiniProgram struct extends context
type MiniProgram struct {
*context.Context
}
// NewMiniProgram 实例化小程序接口
func NewMiniProgram(context *context.Context) *MiniProgram {
miniProgram := new(MiniProgram)
miniProgram.Context = context
return miniProgram
}

91
miniprogram/qrcode.go Normal file
View File

@@ -0,0 +1,91 @@
package miniprogram
import (
"encoding/json"
"fmt"
"strings"
"github.com/silenceper/wechat/util"
)
const (
createWXAQRCodeURL = "https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token=%s"
getWXACodeURL = "https://api.weixin.qq.com/wxa/getwxacode?access_token=%s"
getWXACodeUnlimitURL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%s"
)
// QRCoder 小程序码参数
type QRCoder struct {
// page 必须是已经发布的小程序存在的页面,根路径前不要填加 /,不能携带参数参数请放在scene字段里如果不填写这个字段默认跳主页面
Page string `json:"page,omitempty"`
// path 扫码进入的小程序页面路径
Path string `json:"path,omitempty"`
// width 图片宽度
Width int `json:"width,omitempty"`
// scene 最大32个可见字符只支持数字大小写英文以及部分特殊字符!#$&'()*+,/:;=?@-._~,其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式)
Scene string `json:"scene,omitempty"`
// autoColor 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调
AutoColor bool `json:"auto_color,omitempty"`
// lineColor AutoColor 为 false 时生效,使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"},十进制表示
LineColor Color `json:"line_color,omitempty"`
// isHyaline 是否需要透明底色
IsHyaline bool `json:"is_hyaline,omitempty"`
}
// Color QRCode color
type Color struct {
R string `json:"r"`
G string `json:"g"`
B string `json:"b"`
}
// fetchCode 请求并返回二维码二进制数据
func (wxa *MiniProgram) fetchCode(urlStr string, body interface{}) (response []byte, err error) {
var accessToken string
accessToken, err = wxa.GetAccessToken()
if err != nil {
return
}
urlStr = fmt.Sprintf(urlStr, accessToken)
var contentType string
response, contentType, err = util.PostJSONWithRespContentType(urlStr, body)
if err != nil {
return
}
if strings.HasPrefix(contentType, "application/json") {
// 返回错误信息
var result util.CommonError
err = json.Unmarshal(response, &result)
if err == nil && result.ErrCode != 0 {
err = fmt.Errorf("fetchCode error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
return nil, err
}
} else if contentType == "image/jpeg" {
// 返回文件
return response, nil
} else {
err = fmt.Errorf("fetchCode error : unknown response content type - %v", contentType)
return nil, err
}
return
}
// CreateWXAQRCode 获取小程序二维码,适用于需要的码数量较少的业务场景
// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/createWXAQRCode.html
func (wxa *MiniProgram) CreateWXAQRCode(coderParams QRCoder) (response []byte, err error) {
return wxa.fetchCode(createWXAQRCodeURL, coderParams)
}
// GetWXACode 获取小程序码,适用于需要的码数量较少的业务场景
// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/getWXACode.html
func (wxa *MiniProgram) GetWXACode(coderParams QRCoder) (response []byte, err error) {
return wxa.fetchCode(getWXACodeURL, coderParams)
}
// GetWXACodeUnlimit 获取小程序码,适用于需要的码数量极多的业务场景
// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/getWXACodeUnlimit.html
func (wxa *MiniProgram) GetWXACodeUnlimit(coderParams QRCoder) (response []byte, err error) {
return wxa.fetchCode(getWXACodeUnlimitURL, coderParams)
}

40
miniprogram/sns.go Normal file
View File

@@ -0,0 +1,40 @@
package miniprogram
import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/util"
)
const (
code2SessionURL = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
)
// ResCode2Session 登录凭证校验的返回结果
type ResCode2Session struct {
util.CommonError
OpenID string `json:"openid"` // 用户唯一标识
SessionKey string `json:"session_key"` // 会话密钥
UnionID string `json:"unionid"` // 用户在开放平台的唯一标识符在满足UnionID下发条件的情况下会返回
}
// Code2Session 登录凭证校验
func (wxa *MiniProgram) Code2Session(jsCode string) (result ResCode2Session, err error) {
urlStr := fmt.Sprintf(code2SessionURL, wxa.AppID, wxa.AppSecret, jsCode)
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("Code2Session error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
return
}
return
}

View File

@@ -11,11 +11,12 @@ import (
) )
const ( const (
redirectOauthURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect" redirectOauthURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect"
accessTokenURL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code" webAppRedirectOauthURL = "https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect"
refreshAccessTokenURL = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%s" accessTokenURL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"
userInfoURL = "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN" refreshAccessTokenURL = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%s"
checkAccessTokenURL = "https://api.weixin.qq.com/sns/auth?access_token=%s&openid=%s" userInfoURL = "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN"
checkAccessTokenURL = "https://api.weixin.qq.com/sns/auth?access_token=%s&openid=%s"
) )
//Oauth 保存用户授权信息 //Oauth 保存用户授权信息
@@ -37,6 +38,12 @@ func (oauth *Oauth) GetRedirectURL(redirectURI, scope, state string) (string, er
return fmt.Sprintf(redirectOauthURL, oauth.AppID, urlStr, scope, state), nil return fmt.Sprintf(redirectOauthURL, oauth.AppID, urlStr, scope, state), nil
} }
//GetWebAppRedirectURL 获取网页应用跳转的url地址
func (oauth *Oauth) GetWebAppRedirectURL(redirectURI, scope, state string) (string, error) {
urlStr := url.QueryEscape(redirectURI)
return fmt.Sprintf(webAppRedirectOauthURL, oauth.AppID, urlStr, scope, state), nil
}
//Redirect 跳转到网页授权 //Redirect 跳转到网页授权
func (oauth *Oauth) Redirect(writer http.ResponseWriter, req *http.Request, redirectURI, scope, state string) error { func (oauth *Oauth) Redirect(writer http.ResponseWriter, req *http.Request, redirectURI, scope, state string) error {
location, err := oauth.GetRedirectURL(redirectURI, scope, state) location, err := oauth.GetRedirectURL(redirectURI, scope, state)
@@ -56,6 +63,10 @@ type ResAccessToken struct {
RefreshToken string `json:"refresh_token"` RefreshToken string `json:"refresh_token"`
OpenID string `json:"openid"` OpenID string `json:"openid"`
Scope string `json:"scope"` Scope string `json:"scope"`
// UnionID 只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。
// 公众号文档 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
UnionID string `json:"unionid"`
} }
// GetUserAccessToken 通过网页授权的code 换取access_token(区别于context中的access_token) // GetUserAccessToken 通过网页授权的code 换取access_token(区别于context中的access_token)

View File

@@ -1,9 +1,11 @@
package pay package pay
import ( import (
"bytes"
"encoding/xml" "encoding/xml"
"errors" "errors"
"fmt" "sort"
"strconv"
"github.com/silenceper/wechat/context" "github.com/silenceper/wechat/context"
"github.com/silenceper/wechat/util" "github.com/silenceper/wechat/util"
@@ -24,6 +26,7 @@ type Params struct {
Body string Body string
OutTradeNo string OutTradeNo string
OpenID string OpenID string
TradeType string
} }
// Config 是传出用于 jsdk 用的参数 // Config 是传出用于 jsdk 用的参数
@@ -35,8 +38,8 @@ type Config struct {
Sign string Sign string
} }
// payResult 是 unifie order 接口的返回 // PreOrder 是 unifie order 接口的返回
type payResult struct { type PreOrder struct {
ReturnCode string `xml:"return_code"` ReturnCode string `xml:"return_code"`
ReturnMsg string `xml:"return_msg"` ReturnMsg string `xml:"return_msg"`
AppID string `xml:"appid,omitempty"` AppID string `xml:"appid,omitempty"`
@@ -83,12 +86,23 @@ func NewPay(ctx *context.Context) *Pay {
return &pay return &pay
} }
// PrePayID will request wechat merchant api and request for a pre payment order id // PrePayOrder return data for invoke wechat payment
func (pcf *Pay) PrePayID(p *Params) (prePayID string, err error) { func (pcf *Pay) PrePayOrder(p *Params) (payOrder PreOrder, err error) {
nonceStr := util.RandomStr(32) nonceStr := util.RandomStr(32)
tradeType := "JSAPI" param := make(map[string]interface{})
template := "appid=%s&body=%s&mch_id=%s&nonce_str=%s&notify_url=%s&openid=%s&out_trade_no=%s&spbill_create_ip=%s&total_fee=%s&trade_type=%s&key=%s" param["appid"] = pcf.AppID
str := fmt.Sprintf(template, pcf.AppID, p.Body, pcf.PayMchID, nonceStr, pcf.PayNotifyURL, p.OpenID, p.OutTradeNo, p.CreateIP, p.TotalFee, tradeType, pcf.PayKey) param["body"] = p.Body
param["mch_id"] = pcf.PayMchID
param["nonce_str"] = nonceStr
param["notify_url"] = pcf.PayNotifyURL
param["out_trade_no"] = p.OutTradeNo
param["spbill_create_ip"] = p.CreateIP
param["total_fee"] = p.TotalFee
param["trade_type"] = p.TradeType
param["openid"] = p.OpenID
bizKey := "&key=" + pcf.PayKey
str := orderParam(param, bizKey)
sign := util.MD5Sum(str) sign := util.MD5Sum(str)
request := payRequest{ request := payRequest{
AppID: pcf.AppID, AppID: pcf.AppID,
@@ -100,24 +114,99 @@ func (pcf *Pay) PrePayID(p *Params) (prePayID string, err error) {
TotalFee: p.TotalFee, TotalFee: p.TotalFee,
SpbillCreateIP: p.CreateIP, SpbillCreateIP: p.CreateIP,
NotifyURL: pcf.PayNotifyURL, NotifyURL: pcf.PayNotifyURL,
TradeType: tradeType, TradeType: p.TradeType,
OpenID: p.OpenID, OpenID: p.OpenID,
} }
rawRet, err := util.PostXML(payGateway, request) rawRet, err := util.PostXML(payGateway, request)
if err != nil { if err != nil {
return "", errors.New(err.Error() + " parameters : " + str) return
} }
payRet := payResult{} err = xml.Unmarshal(rawRet, &payOrder)
err = xml.Unmarshal(rawRet, &payRet)
if err != nil { if err != nil {
return "", errors.New(err.Error()) return
} }
if payRet.ReturnCode == "SUCCESS" { if payOrder.ReturnCode == "SUCCESS" {
//pay success //pay success
if payRet.ResultCode == "SUCCESS" { if payOrder.ResultCode == "SUCCESS" {
return payRet.PrePayID, nil err = nil
return
} }
return "", errors.New(payRet.ErrCode + payRet.ErrCodeDes) err = errors.New(payOrder.ErrCode + payOrder.ErrCodeDes)
return
} }
return "", errors.New("[msg : xmlUnmarshalError] [rawReturn : " + string(rawRet) + "] [params : " + str + "] [sign : " + sign + "]") err = errors.New("[msg : xmlUnmarshalError] [rawReturn : " + string(rawRet) + "] [params : " + str + "] [sign : " + sign + "]")
return
}
// PrePayID will request wechat merchant api and request for a pre payment order id
func (pcf *Pay) PrePayID(p *Params) (prePayID string, err error) {
order, err := pcf.PrePayOrder(p)
if err != nil {
return
}
if order.PrePayID == "" {
err = errors.New("empty prepayid")
}
prePayID = order.PrePayID
return
}
// order params
func orderParam(source interface{}, bizKey string) (returnStr string) {
switch v := source.(type) {
case map[string]string:
keys := make([]string, 0, len(v))
for k := range v {
if k == "sign" {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
var buf bytes.Buffer
for _, k := range keys {
if v[k] == "" {
continue
}
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(k)
buf.WriteByte('=')
buf.WriteString(v[k])
}
buf.WriteString(bizKey)
returnStr = buf.String()
case map[string]interface{}:
keys := make([]string, 0, len(v))
for k := range v {
if k == "sign" {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
var buf bytes.Buffer
for _, k := range keys {
if v[k] == "" {
continue
}
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(k)
buf.WriteByte('=')
switch vv := v[k].(type) {
case string:
buf.WriteString(vv)
case int:
buf.WriteString(strconv.FormatInt(int64(vv), 10))
default:
panic("params type not supported")
}
}
buf.WriteString(bizKey)
returnStr = buf.String()
}
return
} }

109
pay/refund.go Normal file
View File

@@ -0,0 +1,109 @@
package pay
import (
"encoding/xml"
"fmt"
"github.com/silenceper/wechat/util"
)
var refundGateway = "https://api.mch.weixin.qq.com/secapi/pay/refund"
//RefundParams 调用参数
type RefundParams struct {
TransactionID string
OutRefundNo string
TotalFee string
RefundFee string
RefundDesc string
RootCa string //ca证书
}
//refundRequest 接口请求参数
type refundRequest struct {
AppID string `xml:"appid"`
MchID string `xml:"mch_id"`
NonceStr string `xml:"nonce_str"`
Sign string `xml:"sign"`
SignType string `xml:"sign_type,omitempty"`
TransactionID string `xml:"transaction_id"`
OutRefundNo string `xml:"out_refund_no"`
TotalFee string `xml:"total_fee"`
RefundFee string `xml:"refund_fee"`
RefundDesc string `xml:"refund_desc,omitempty"`
//NotifyUrl string `xml:"notify_url,omitempty"`
}
//RefundResponse 接口返回
type RefundResponse struct {
ReturnCode string `xml:"return_code"`
ReturnMsg string `xml:"return_msg"`
AppID string `xml:"appid,omitempty"`
MchID string `xml:"mch_id,omitempty"`
NonceStr string `xml:"nonce_str,omitempty"`
Sign string `xml:"sign,omitempty"`
ResultCode string `xml:"result_code,omitempty"`
ErrCode string `xml:"err_code,omitempty"`
ErrCodeDes string `xml:"err_code_des,omitempty"`
TransactionID string `xml:"transaction_id,omitempty"`
OutTradeNo string `xml:"out_trade_no,omitempty"`
OutRefundNo string `xml:"out_refund_no,omitempty"`
RefundID string `xml:"refund_id,omitempty"`
RefundFee string `xml:"refund_fee,omitempty"`
SettlementRefundFee string `xml:"settlement_refund_fee,omitempty"`
TotalFee string `xml:"total_fee,omitempty"`
SettlementTotalFee string `xml:"settlement_total_fee,omitempty"`
FeeType string `xml:"fee_type,omitempty"`
CashFee string `xml:"cash_fee,omitempty"`
CashFeeType string `xml:"cash_fee_type,omitempty"`
}
//Refund 退款申请
func (pcf *Pay) Refund(p *RefundParams) (rsp RefundResponse, err error) {
nonceStr := util.RandomStr(32)
param := make(map[string]interface{})
param["appid"] = pcf.AppID
param["mch_id"] = pcf.PayMchID
param["nonce_str"] = nonceStr
param["out_refund_no"] = p.OutRefundNo
param["refund_desc"] = p.RefundDesc
param["refund_fee"] = p.RefundFee
param["total_fee"] = p.TotalFee
param["sign_type"] = "MD5"
param["transaction_id"] = p.TransactionID
bizKey := "&key=" + pcf.PayKey
str := orderParam(param, bizKey)
sign := util.MD5Sum(str)
request := refundRequest{
AppID: pcf.AppID,
MchID: pcf.PayMchID,
NonceStr: nonceStr,
Sign: sign,
SignType: "MD5",
TransactionID: p.TransactionID,
OutRefundNo: p.OutRefundNo,
TotalFee: p.TotalFee,
RefundFee: p.RefundFee,
RefundDesc: p.RefundDesc,
}
rawRet, err := util.PostXMLWithTLS(refundGateway, request, p.RootCa, pcf.PayMchID)
if err != nil {
return
}
err = xml.Unmarshal(rawRet, &rsp)
if err != nil {
return
}
if rsp.ReturnCode == "SUCCESS" {
if rsp.ResultCode == "SUCCESS" {
err = nil
return
}
err = fmt.Errorf("refund error, errcode=%s,errmsg=%s", rsp.ErrCode, rsp.ErrCodeDes)
return
}
err = fmt.Errorf("[msg : xmlUnmarshalError] [rawReturn : %s] [params : %s] [sign : %s]",
string(rawRet), str, sign)
return
}

122
qr/qr.go Normal file
View File

@@ -0,0 +1,122 @@
package qr
import (
"encoding/json"
"fmt"
"reflect"
"time"
"github.com/silenceper/wechat/context"
"github.com/silenceper/wechat/util"
)
const (
qrCreateURL = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s"
getQRImgURL = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=%s"
)
const (
actionID = "QR_SCENE"
actionStr = "QR_STR_SCENE"
actionLimitID = "QR_LIMIT_SCENE"
actionLimitStr = "QR_LIMIT_STR_SCENE"
)
// QR 二维码
type QR struct {
*context.Context
}
//NewQR 二维码实例
func NewQR(context *context.Context) *QR {
q := new(QR)
q.Context = context
return q
}
// Request 临时二维码
type Request struct {
ExpireSeconds int64 `json:"expire_seconds,omitempty"`
ActionName string `json:"action_name"`
ActionInfo struct {
Scene struct {
SceneStr string `json:"scene_str,omitempty"`
SceneID int `json:"scene_id,omitempty"`
} `json:"scene"`
} `json:"action_info"`
}
// Ticket 二维码ticket
type Ticket struct {
util.CommonError `json:",inline"`
Ticket string `json:"ticket"`
ExpireSeconds int64 `json:"expire_seconds"`
URL string `json:"url"`
}
// GetQRTicket 获取二维码 Ticket
func (q *QR) GetQRTicket(tq *Request) (t *Ticket, err error) {
accessToken, err := q.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(qrCreateURL, accessToken)
response, err := util.PostJSON(uri, tq)
if err != nil {
err = fmt.Errorf("get qr ticket failed, %s", err)
return
}
t = new(Ticket)
err = json.Unmarshal(response, &t)
if err != nil {
return
}
return
}
// ShowQRCode 通过ticket换取二维码
func ShowQRCode(tk *Ticket) string {
return fmt.Sprintf(getQRImgURL, tk.Ticket)
}
// NewTmpQrRequest 新建临时二维码请求实例
func NewTmpQrRequest(exp time.Duration, scene interface{}) *Request {
tq := &Request{
ExpireSeconds: int64(exp.Seconds()),
}
switch reflect.ValueOf(scene).Kind() {
case reflect.String:
tq.ActionName = actionStr
tq.ActionInfo.Scene.SceneStr = scene.(string)
case reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64:
tq.ActionName = actionID
tq.ActionInfo.Scene.SceneID = scene.(int)
}
return tq
}
// NewLimitQrRequest 新建永久二维码请求实例
func NewLimitQrRequest(scene interface{}) *Request {
tq := &Request{}
switch reflect.ValueOf(scene).Kind() {
case reflect.String:
tq.ActionName = actionLimitStr
tq.ActionInfo.Scene.SceneStr = scene.(string)
case reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64:
tq.ActionName = actionLimitID
tq.ActionInfo.Scene.SceneID = scene.(int)
}
return tq
}

View File

@@ -3,6 +3,7 @@ package user
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/url"
"github.com/silenceper/wechat/context" "github.com/silenceper/wechat/context"
"github.com/silenceper/wechat/util" "github.com/silenceper/wechat/util"
@@ -11,6 +12,7 @@ import (
const ( const (
userInfoURL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN" 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" updateRemarkURL = "https://api.weixin.qq.com/cgi-bin/user/info/updateremark?access_token=%s"
userListURL = "https://api.weixin.qq.com/cgi-bin/user/get"
) )
//User 用户管理 //User 用户管理
@@ -29,20 +31,33 @@ func NewUser(context *context.Context) *User {
type Info struct { type Info struct {
util.CommonError util.CommonError
Subscribe int32 `json:"subscribe"` Subscribe int32 `json:"subscribe"`
OpenID string `json:"openid"` OpenID string `json:"openid"`
Nickname string `json:"nickname"` Nickname string `json:"nickname"`
Sex int32 `json:"sex"` Sex int32 `json:"sex"`
City string `json:"city"` City string `json:"city"`
Country string `json:"country"` Country string `json:"country"`
Province string `json:"province"` Province string `json:"province"`
Language string `json:"language"` Language string `json:"language"`
Headimgurl string `json:"headimgurl"` Headimgurl string `json:"headimgurl"`
SubscribeTime int32 `json:"subscribe_time"` SubscribeTime int32 `json:"subscribe_time"`
UnionID string `json:"unionid"` UnionID string `json:"unionid"`
Remark string `json:"remark"` Remark string `json:"remark"`
GroupID int32 `json:"groupid"` GroupID int32 `json:"groupid"`
TagidList []int32 `json:"tagid_list"` TagidList []int32 `json:"tagid_list"`
SubscribeScene string `json:"subscribe_scene"`
QrScene int `json:"qr_scene"`
QrSceneStr string `json:"qr_scene_str"`
}
// OpenidList 用户列表
type OpenidList struct {
Total int `json:"total"`
Count int `json:"count"`
Data struct {
OpenIDs []string `json:"openid"`
} `json:"data"`
NextOpenID string `json:"next_openid"`
} }
//GetUserInfo 获取用户基本信息 //GetUserInfo 获取用户基本信息
@@ -88,3 +103,52 @@ func (user *User) UpdateRemark(openID, remark string) (err error) {
return util.DecodeWithCommonError(response, "UpdateRemark") return util.DecodeWithCommonError(response, "UpdateRemark")
} }
// ListUserOpenIDs 返回用户列表
func (user *User) ListUserOpenIDs(nextOpenid ...string) (*OpenidList, error) {
accessToken, err := user.GetAccessToken()
if err != nil {
return nil, err
}
uri, _ := url.Parse(userListURL)
q := uri.Query()
q.Set("access_token", accessToken)
if len(nextOpenid) > 0 && nextOpenid[0] != "" {
q.Set("next_openid", nextOpenid[0])
}
uri.RawQuery = q.Encode()
response, err := util.HTTPGet(uri.String())
if err != nil {
return nil, err
}
userlist := new(OpenidList)
err = json.Unmarshal(response, userlist)
if err != nil {
return nil, err
}
return userlist, nil
}
// ListAllUserOpenIDs 返回所有用户OpenID列表
func (user *User) ListAllUserOpenIDs() ([]string, error) {
nextOpenid := ""
openids := []string{}
count := 0
for {
ul, err := user.ListUserOpenIDs(nextOpenid)
if err != nil {
return nil, err
}
openids = append(openids, ul.Data.OpenIDs...)
count += ul.Count
if ul.Total > count {
nextOpenid = ul.NextOpenID
} else {
return openids, nil
}
}
}

View File

@@ -2,11 +2,15 @@ package util
import ( import (
"bytes" "bytes"
"crypto/tls"
"encoding/json" "encoding/json"
"encoding/pem"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"golang.org/x/crypto/pkcs12"
"io" "io"
"io/ioutil" "io/ioutil"
"log"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os" "os"
@@ -50,6 +54,32 @@ func PostJSON(uri string, obj interface{}) ([]byte, error) {
return ioutil.ReadAll(response.Body) return ioutil.ReadAll(response.Body)
} }
// PostJSONWithRespContentType post json数据请求且返回数据类型
func PostJSONWithRespContentType(uri string, obj interface{}) ([]byte, string, error) {
jsonData, err := json.Marshal(obj)
if err != nil {
return nil, "", err
}
jsonData = bytes.Replace(jsonData, []byte("\\u003c"), []byte("<"), -1)
jsonData = bytes.Replace(jsonData, []byte("\\u003e"), []byte(">"), -1)
jsonData = bytes.Replace(jsonData, []byte("\\u0026"), []byte("&"), -1)
body := bytes.NewBuffer(jsonData)
response, err := http.Post(uri, "application/json;charset=utf-8", body)
if err != nil {
return nil, "", err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, "", fmt.Errorf("http get error : uri=%v , statusCode=%v", uri, response.StatusCode)
}
responseData, err := ioutil.ReadAll(response.Body)
contentType := response.Header.Get("Content-Type")
return responseData, contentType, err
}
//PostFile 上传文件 //PostFile 上传文件
func PostFile(fieldname, filename, uri string) ([]byte, error) { func PostFile(fieldname, filename, uri string) ([]byte, error) {
fields := []MultipartFormField{ fields := []MultipartFormField{
@@ -141,3 +171,68 @@ func PostXML(uri string, obj interface{}) ([]byte, error) {
} }
return ioutil.ReadAll(response.Body) return ioutil.ReadAll(response.Body)
} }
//httpWithTLS CA证书
func httpWithTLS(rootCa, key string) (*http.Client, error) {
var client *http.Client
certData, err := ioutil.ReadFile(rootCa)
if err != nil {
return nil, fmt.Errorf("unable to find cert path=%s, error=%v", rootCa, err)
}
cert := pkcs12ToPem(certData, key)
config := &tls.Config{
Certificates: []tls.Certificate{cert},
}
tr := &http.Transport{
TLSClientConfig: config,
DisableCompression: true,
}
client = &http.Client{Transport: tr}
return client, nil
}
//pkcs12ToPem 将Pkcs12转成Pem
func pkcs12ToPem(p12 []byte, password string) tls.Certificate {
blocks, err := pkcs12.ToPEM(p12, password)
defer func() {
if x := recover(); x != nil {
log.Print(x)
}
}()
if err != nil {
panic(err)
}
var pemData []byte
for _, b := range blocks {
pemData = append(pemData, pem.EncodeToMemory(b)...)
}
cert, err := tls.X509KeyPair(pemData, pemData)
if err != nil {
panic(err)
}
return cert
}
//PostXMLWithTLS perform a HTTP/POST request with XML body and TLS
func PostXMLWithTLS(uri string, obj interface{}, ca, key string) ([]byte, error) {
xmlData, err := xml.Marshal(obj)
if err != nil {
return nil, err
}
body := bytes.NewBuffer(xmlData)
client, err := httpWithTLS(ca, key)
if err != nil {
return nil, err
}
response, err := client.Post(uri, "application/xml;charset=utf-8", body)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("http code error : uri=%v , statusCode=%v", uri, response.StatusCode)
}
return ioutil.ReadAll(response.Body)
}

24
vendor/vendor.json vendored
View File

@@ -80,6 +80,18 @@
"revision": "8ee79997227bf9b34611aee7946ae64735e6fd93", "revision": "8ee79997227bf9b34611aee7946ae64735e6fd93",
"revisionTime": "2016-11-17T03:31:26Z" "revisionTime": "2016-11-17T03:31:26Z"
}, },
{
"checksumSHA1": "w3QCCIYHgZzIXQ+xTl7oLfFrXHs=",
"path": "github.com/gomodule/redigo/internal",
"revision": "2cd21d9966bf7ff9ae091419744f0b3fb0fecace",
"revisionTime": "2018-06-27T14:45:07Z"
},
{
"checksumSHA1": "To/N5YA/FD0Rrs6r2OOmHXgxYwI=",
"path": "github.com/gomodule/redigo/redis",
"revision": "2cd21d9966bf7ff9ae091419744f0b3fb0fecace",
"revisionTime": "2018-06-27T14:45:07Z"
},
{ {
"checksumSHA1": "b0T0Hzd+zYk+OCDTFMps+jwa/nY=", "checksumSHA1": "b0T0Hzd+zYk+OCDTFMps+jwa/nY=",
"path": "github.com/manucorporat/sse", "path": "github.com/manucorporat/sse",
@@ -92,6 +104,18 @@
"revision": "30a891c33c7cde7b02a981314b4228ec99380cca", "revision": "30a891c33c7cde7b02a981314b4228ec99380cca",
"revisionTime": "2016-11-23T14:36:37Z" "revisionTime": "2016-11-23T14:36:37Z"
}, },
{
"checksumSHA1": "PJY7uCr3UnX4/Mf/RoWnbieSZ8o=",
"path": "golang.org/x/crypto/pkcs12",
"revision": "9f005a07e0d31d45e6656d241bb5c0f2efd4bc94",
"revisionTime": "2017-09-21T17:41:56Z"
},
{
"checksumSHA1": "iVJcif9M9uefvvqHCNR9VQrjc/s=",
"path": "golang.org/x/crypto/pkcs12/internal/rc2",
"revision": "9f005a07e0d31d45e6656d241bb5c0f2efd4bc94",
"revisionTime": "2017-09-21T17:41:56Z"
},
{ {
"checksumSHA1": "pancewZW3HwGvpDwfH5Imrbadc4=", "checksumSHA1": "pancewZW3HwGvpDwfH5Imrbadc4=",
"path": "golang.org/x/net/context", "path": "golang.org/x/net/context",

View File

@@ -9,8 +9,10 @@ import (
"github.com/silenceper/wechat/js" "github.com/silenceper/wechat/js"
"github.com/silenceper/wechat/material" "github.com/silenceper/wechat/material"
"github.com/silenceper/wechat/menu" "github.com/silenceper/wechat/menu"
"github.com/silenceper/wechat/miniprogram"
"github.com/silenceper/wechat/oauth" "github.com/silenceper/wechat/oauth"
"github.com/silenceper/wechat/pay" "github.com/silenceper/wechat/pay"
"github.com/silenceper/wechat/qr"
"github.com/silenceper/wechat/server" "github.com/silenceper/wechat/server"
"github.com/silenceper/wechat/template" "github.com/silenceper/wechat/template"
"github.com/silenceper/wechat/user" "github.com/silenceper/wechat/user"
@@ -99,3 +101,13 @@ func (wc *Wechat) GetTemplate() *template.Template {
func (wc *Wechat) GetPay() *pay.Pay { func (wc *Wechat) GetPay() *pay.Pay {
return pay.NewPay(wc.Context) return pay.NewPay(wc.Context)
} }
// GetQR 返回二维码的实例
func (wc *Wechat) GetQR() *qr.QR {
return qr.NewQR(wc.Context)
}
// GetMiniProgram 获取小程序的实例
func (wc *Wechat) GetMiniProgram() *miniprogram.MiniProgram {
return miniprogram.NewMiniProgram(wc.Context)
}