mirror of
https://github.com/silenceper/wechat.git
synced 2026-02-04 12:52:27 +08:00
Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d91e82c183 | ||
|
|
3111e12c00 | ||
|
|
9c8717afba | ||
|
|
d7f371cb65 | ||
|
|
39e81b45bf | ||
|
|
733c53a044 | ||
|
|
5617c9512d | ||
|
|
d806d1c968 | ||
|
|
254ac9d7a6 | ||
|
|
19e3174107 | ||
|
|
0dbc292f44 | ||
|
|
33d00f45c5 | ||
|
|
c4a361bbf6 | ||
|
|
f4e58b0712 | ||
|
|
5b307df969 | ||
|
|
b16d231a29 | ||
|
|
c30319c74c | ||
|
|
76c1832798 | ||
|
|
f6d07aa714 | ||
|
|
1475417a64 | ||
|
|
bf456aa77b | ||
|
|
934ca61b3b | ||
|
|
00ca733814 | ||
|
|
2e796b21d3 | ||
|
|
412c2f0ea9 | ||
|
|
8cb724ece0 | ||
|
|
13facb6df8 | ||
|
|
55615762eb | ||
|
|
1fc4cc70ec | ||
|
|
e09031b58c | ||
|
|
279ff79406 | ||
|
|
7bde39a634 | ||
|
|
1711aeb46d | ||
|
|
87470f143d | ||
|
|
900a54ee06 | ||
|
|
7170f6ef32 | ||
|
|
0c9dd16f1f | ||
|
|
f68f9d6f5e | ||
|
|
09dabb232d | ||
|
|
3ea624f832 | ||
|
|
546dce2396 | ||
|
|
abd7f512ba | ||
|
|
b8239ef9a9 | ||
|
|
0dffcde475 | ||
|
|
4e6fd625da | ||
|
|
453089e83e | ||
|
|
bb97bddc08 | ||
|
|
54e2c82fff | ||
|
|
fab09a0bbe | ||
|
|
f8ab592606 | ||
|
|
fcf289cfe3 | ||
|
|
c458f44917 | ||
|
|
1a9600b49f | ||
|
|
dbb43ac7ad | ||
|
|
79ff0321e3 | ||
|
|
44dae6e950 | ||
|
|
894b1972d7 | ||
|
|
6c1ed39487 | ||
|
|
529323e6b5 | ||
|
|
9a4d41563e | ||
|
|
81f26cd6dc | ||
|
|
eae6caadb2 | ||
|
|
fdd9768a96 | ||
|
|
42332eca27 | ||
|
|
b614b55cdf | ||
|
|
3fc556c425 | ||
|
|
3005852946 | ||
|
|
5652af6aab | ||
|
|
57bc7aabba | ||
|
|
3be94cd80d | ||
|
|
0871e2f8ed | ||
|
|
6e1ec1f00c | ||
|
|
823c54fda5 | ||
|
|
e66652f4b5 | ||
|
|
f4f1860e67 | ||
|
|
02b3fcc648 | ||
|
|
4f5945fb0f | ||
|
|
963a2d39bd | ||
|
|
862e546367 | ||
|
|
5677b60759 | ||
|
|
61476d351d | ||
|
|
eda287070d | ||
|
|
443435343c | ||
|
|
036183e5ff | ||
|
|
593df23c46 | ||
|
|
50c490df31 | ||
|
|
7a19587f6a | ||
|
|
91d1c77abc | ||
|
|
4607ef001e | ||
|
|
fedd5a96ca | ||
|
|
3984f13c76 | ||
|
|
2da9755c58 | ||
|
|
d5302dbfdc | ||
|
|
a6b1c56c25 | ||
|
|
388fd9ec07 | ||
|
|
7163fc80c9 | ||
|
|
d67206b106 | ||
|
|
efdf09e133 | ||
|
|
9a34dca9a1 | ||
|
|
546ffb9155 | ||
|
|
f6b2887cee | ||
|
|
ebf6158b7c | ||
|
|
3e3cb594a3 |
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: https://silenceper.com/img/wechat-pay.jpeg
|
||||
2
.github/ISSUE_TEMPLATE.md
vendored
Normal file
2
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
## 问题及现象
|
||||
<!-- 描述你的问题现象,报错**贴截图**粘贴或者贴具体信息,提供**必要的代码段**
|
||||
@@ -1,10 +1,10 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.13.x
|
||||
- 1.12.x
|
||||
- 1.11.x
|
||||
- 1.10.x
|
||||
- 1.9.x
|
||||
- 1.8.x
|
||||
|
||||
services:
|
||||
- memcached
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
|
||||
before_script:
|
||||
- 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:
|
||||
- go test -v -race ./...
|
||||
|
||||
121
README.md
121
README.md
@@ -96,6 +96,7 @@ Cache主要用来保存全局access_token以及js-sdk中的ticket:
|
||||
- 检验access_token是否有效
|
||||
- 获取js-sdk配置
|
||||
- [素材管理](#素材管理)
|
||||
- [小程序开发](#小程序开发)
|
||||
|
||||
## 消息管理
|
||||
|
||||
@@ -282,8 +283,8 @@ type Reply struct {
|
||||
#### 回复图片消息
|
||||
```go
|
||||
//mediaID 可通过素材管理-上上传多媒体文件获得
|
||||
image :=message.NewVideo("mediaID")
|
||||
return &message.Reply{message.MsgTypeVideo, image}
|
||||
image :=message.NewImage("mediaID")
|
||||
return &message.Reply{message.MsgTypeImage, image}
|
||||
```
|
||||
#### 回复视频消息
|
||||
```go
|
||||
@@ -335,14 +336,14 @@ Url :点击图文消息跳转链接
|
||||
|
||||
## 自定义菜单
|
||||
|
||||
通过` wechat.GetMenu(req, writer)`获取menu的实例
|
||||
通过` wechat.GetMenu()`获取menu的实例
|
||||
|
||||
### 自定义菜单创建接口
|
||||
|
||||
以下是一个创建二级菜单的例子
|
||||
|
||||
```go
|
||||
mu := wc.GetMenu(c.Request, c.Writer)
|
||||
mu := wc.GetMenu()
|
||||
|
||||
buttons := make([]*menu.Button, 1)
|
||||
btn := new(menu.Button)
|
||||
@@ -402,7 +403,7 @@ func (btn *Button) SetViewLimitedButton(name, mediaID string) {
|
||||
### 自定义菜单查询接口
|
||||
|
||||
```go
|
||||
mu := wc.GetMenu(c.Request, c.Writer)
|
||||
mu := wc.GetMenu()
|
||||
resMenu,err:=mu.GetMenu()
|
||||
```
|
||||
>返回结果 resMenu 结构参考 ./menu/menu.go 中ResMenu 结构体
|
||||
@@ -410,7 +411,7 @@ resMenu,err:=mu.GetMenu()
|
||||
### 自定义菜单删除接口
|
||||
|
||||
```go
|
||||
mu := wc.GetMenu(c.Request, c.Writer)
|
||||
mu := wc.GetMenu()
|
||||
err:=mu.DeleteMenu()
|
||||
```
|
||||
|
||||
@@ -458,7 +459,7 @@ func (menu *Menu) GetCurrentSelfMenuInfo() (resSelfMenuInfo ResSelfMenuInfo, err
|
||||
**1.发起授权**
|
||||
|
||||
```go
|
||||
oauth := wc.GetOauth(c.Request, c.Writer)
|
||||
oauth := wc.GetOauth()
|
||||
err := oauth.Redirect("跳转的绝对地址", "snsapi_userinfo", "123dd123")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
@@ -505,7 +506,7 @@ func (oauth *Oauth) CheckAccessToken(accessToken, openID string) (b bool, err er
|
||||
### 获取js-sdk配置
|
||||
|
||||
```go
|
||||
js := wc.GetJs(c.Request, c.Writer)
|
||||
js := wc.GetJs()
|
||||
cfg, err := js.GetConfig("传入需要的调用js-sdk的url地址")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
@@ -529,6 +530,110 @@ type Config struct {
|
||||
|
||||
[素材管理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 :
|
||||
[https://godoc.org/github.com/silenceper/wechat](https://godoc.org/github.com/silenceper/wechat)
|
||||
|
||||
74
cache/memory.go
vendored
Normal file
74
cache/memory.go
vendored
Normal 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
|
||||
}
|
||||
@@ -22,16 +22,27 @@ type ResAccessToken struct {
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
//GetAccessTokenFunc 获取 access token 的函数签名
|
||||
type GetAccessTokenFunc func(ctx *Context) (accessToken string, err error)
|
||||
|
||||
//SetAccessTokenLock 设置读写锁(一个appID一个读写锁)
|
||||
func (ctx *Context) SetAccessTokenLock(l *sync.RWMutex) {
|
||||
ctx.accessTokenLock = l
|
||||
}
|
||||
|
||||
//SetGetAccessTokenFunc 设置自定义获取accessToken的方式, 需要自己实现缓存
|
||||
func (ctx *Context) SetGetAccessTokenFunc(f GetAccessTokenFunc) {
|
||||
ctx.accessTokenFunc = f
|
||||
}
|
||||
|
||||
//GetAccessToken 获取access_token
|
||||
func (ctx *Context) GetAccessToken() (accessToken string, err error) {
|
||||
ctx.accessTokenLock.Lock()
|
||||
defer ctx.accessTokenLock.Unlock()
|
||||
|
||||
if ctx.accessTokenFunc != nil {
|
||||
return ctx.accessTokenFunc(ctx)
|
||||
}
|
||||
accessTokenCacheKey := fmt.Sprintf("access_token_%s", ctx.AppID)
|
||||
val := ctx.Cache.Get(accessTokenCacheKey)
|
||||
if val != nil {
|
||||
|
||||
30
context/access_token_test.go
Normal file
30
context/access_token_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
221
context/component_access_token.go
Normal file
221
context/component_access_token.go
Normal 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
19
context/component_test.go
Normal 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)
|
||||
}
|
||||
@@ -27,6 +27,9 @@ type Context struct {
|
||||
|
||||
//jsAPITicket 读写锁 同一个AppID一个
|
||||
jsAPITicketLock *sync.RWMutex
|
||||
|
||||
//accessTokenFunc 自定义获取 access token 的方法
|
||||
accessTokenFunc GetAccessTokenFunc
|
||||
}
|
||||
|
||||
// Query returns the keyed url query value if it exists
|
||||
|
||||
116
device/authorize.go
Normal file
116
device/authorize.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// DeviceAdd 添加设备标识
|
||||
DeviceAdd = iota
|
||||
// DeviceUpgrade 更新设备标识
|
||||
DeviceUpgrade
|
||||
)
|
||||
|
||||
type reqDeviceAuthorize struct {
|
||||
// 设备id的个数
|
||||
DeviceNum string `json:"device_num"`
|
||||
// 设备id的列表,json的array格式,其size必须等于device_num
|
||||
DeviceList []ReqDevice `json:"device_list"`
|
||||
// 请求操作的类型,限定取值为:0:设备授权(缺省值为0) 1:设备更新(更新已授权设备的各属性值)
|
||||
OpType string `json:"op_type,omitempty"`
|
||||
// 设备的产品编号(由微信硬件平台分配)。可在公众号设备功能管理页面查询。
|
||||
//当 op_type 为‘0’,product_id 为‘1’时,不要填写 product_id 字段(会引起不必要错误);
|
||||
//当 op_typy 为‘0’,product_id 不为‘1’时,必须填写 product_id 字段;
|
||||
//当 op_type 为 1 时,不要填写 product_id 字段。
|
||||
ProductID string `json:"product_id,omitempty"`
|
||||
}
|
||||
|
||||
//ReqDevice 设备授权实体
|
||||
type ReqDevice struct {
|
||||
// 设备的 device id
|
||||
ID string `json:"id"`
|
||||
// 设备的mac地址 格式采用16进制串的方式(长度为12字节),
|
||||
// 不需要0X前缀,如: 1234567890AB
|
||||
Mac string `json:"mac"`
|
||||
// 支持以下四种连接协议:
|
||||
// android classic bluetooth – 1
|
||||
// ios classic bluetooth – 2
|
||||
// ble – 3
|
||||
// wifi -- 4
|
||||
// 一个设备可以支持多种连接类型,用符号"|"做分割,客户端优先选择靠前的连接方式(优先级按|关系的排序依次降低),举例:
|
||||
// 1:表示设备仅支持andiod classic bluetooth 1|2:表示设备支持andiod 和ios 两种classic bluetooth,但是客户端优先选择andriod classic bluetooth 协议,如果andriod classic bluetooth协议连接失败,再选择ios classic bluetooth协议进行连接
|
||||
// (注:安卓平台不同时支持BLE和classic类型)
|
||||
ConnectProtocol string `json:"connect_protocol"`
|
||||
//auth及通信的加密key,第三方需要将key烧制在设备上(128bit),格式采用16进制串的方式(长度为32字节),不需要0X前缀,如: 1234567890ABCDEF1234567890ABCDEF
|
||||
AuthKey string `json:"auth_key"`
|
||||
// 断开策略,目前支持: 1:退出公众号页面时即断开连接 2:退出公众号之后保持连接不断开
|
||||
CloseStrategy string `json:"close_strategy"`
|
||||
//连接策略,32位整型,按bit位置位,目前仅第1bit和第3bit位有效(bit置0为无效,1为有效;第2bit已被废弃),且bit位可以按或置位(如1|4=5),各bit置位含义说明如下:
|
||||
//1:(第1bit置位)在公众号对话页面,不停的尝试连接设备
|
||||
//4:(第3bit置位)处于非公众号页面(如主界面等),微信自动连接。当用户切换微信到前台时,可能尝试去连接设备,连上后一定时间会断开
|
||||
ConnStrategy string `json:"conn_strategy"`
|
||||
// auth version,设备和微信进行auth时,会根据该版本号来确认auth buf和auth key的格式(各version对应的auth buf及key的具体格式可以参看“客户端蓝牙外设协议”),该字段目前支持取值:
|
||||
// 0:不加密的version
|
||||
// 1:version 1
|
||||
AuthVer string `json:"auth_ver"`
|
||||
// 表示mac地址在厂商广播manufature data里含有mac地址的偏移,取值如下:
|
||||
// -1:在尾部、
|
||||
// -2:表示不包含mac地址 其他:非法偏移
|
||||
ManuMacPos string `json:"manu_mac_pos"`
|
||||
// 表示mac地址在厂商serial number里含有mac地址的偏移,取值如下:
|
||||
// -1:表示在尾部
|
||||
// -2:表示不包含mac地址 其他:非法偏移
|
||||
SerMacPost string `json:"ser_mac_post"`
|
||||
// 精简协议类型,取值如下:计步设备精简协议:1 (若该字段填1,connect_protocol 必须包括3。非精简协议设备切勿填写该字段)
|
||||
BleSimpleProtocol string `json:"ble_simple_protocol,omitempty"`
|
||||
}
|
||||
|
||||
//ResBaseInfo 授权回调实体
|
||||
type ResBaseInfo struct {
|
||||
BaseInfo struct {
|
||||
DeviceType string `json:"device_type"`
|
||||
DeviceID string `json:"device_id"`
|
||||
} `json:"base_info"`
|
||||
}
|
||||
|
||||
// 授权回调根信息
|
||||
type resDeviceAuthorize struct {
|
||||
util.CommonError
|
||||
Resp []ResBaseInfo `json:"resp"`
|
||||
}
|
||||
|
||||
// DeviceAuthorize 设备授权
|
||||
func (d *Device) DeviceAuthorize(devices []ReqDevice, opType int, product string) (res []ResBaseInfo, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = d.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", uriAuthorize, accessToken)
|
||||
req := reqDeviceAuthorize{
|
||||
DeviceNum: fmt.Sprintf("%d", len(devices)),
|
||||
DeviceList: devices,
|
||||
OpType: fmt.Sprintf("%d", opType),
|
||||
ProductID: product,
|
||||
}
|
||||
var response []byte
|
||||
response, err = util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result resDeviceAuthorize
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("DeviceAuthorize Error , errcode=%d , errmsg=%s", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
res = result.Resp
|
||||
return
|
||||
}
|
||||
106
device/bind.go
Normal file
106
device/bind.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
// ReqBind 设备绑定解绑共通实体
|
||||
type ReqBind struct {
|
||||
Ticket string `json:"ticket,omitempty"`
|
||||
DeviceID string `json:"device_id"`
|
||||
OpenID string `json:"openid"`
|
||||
}
|
||||
type resBind struct {
|
||||
BaseResp util.CommonError `json:"base_resp"`
|
||||
}
|
||||
|
||||
// Bind 设备绑定
|
||||
func (d *Device) Bind(req ReqBind) (err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = d.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", uriBind, accessToken)
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(uri, req); err != nil {
|
||||
return
|
||||
}
|
||||
var result resBind
|
||||
if err = json.Unmarshal(response, &result); err != nil {
|
||||
return
|
||||
}
|
||||
if result.BaseResp.ErrCode != 0 {
|
||||
err = fmt.Errorf("DeviceBind Error , errcode=%d , errmsg=%s", result.BaseResp.ErrCode, result.BaseResp.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Unbind 设备解绑
|
||||
func (d *Device) Unbind(req ReqBind) (err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = d.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", uriUnbind, accessToken)
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(uri, req); err != nil {
|
||||
return
|
||||
}
|
||||
var result resBind
|
||||
if err = json.Unmarshal(response, &result); err != nil {
|
||||
return
|
||||
}
|
||||
if result.BaseResp.ErrCode != 0 {
|
||||
err = fmt.Errorf("DeviceBind Error , errcode=%d , errmsg=%s", result.BaseResp.ErrCode, result.BaseResp.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CompelBind 强制绑定用户和设备
|
||||
func (d *Device) CompelBind(req ReqBind) (err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = d.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", uriCompelBind, accessToken)
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(uri, req); err != nil {
|
||||
return
|
||||
}
|
||||
var result resBind
|
||||
if err = json.Unmarshal(response, &result); err != nil {
|
||||
return
|
||||
}
|
||||
if result.BaseResp.ErrCode != 0 {
|
||||
err = fmt.Errorf("DeviceBind Error , errcode=%d , errmsg=%s", result.BaseResp.ErrCode, result.BaseResp.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CompelUnbind 强制解绑用户和设备
|
||||
func (d *Device) CompelUnbind(req ReqBind) (err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = d.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", uriCompelUnbind, accessToken)
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(uri, req); err != nil {
|
||||
return
|
||||
}
|
||||
var result resBind
|
||||
if err = json.Unmarshal(response, &result); err != nil {
|
||||
return
|
||||
}
|
||||
if result.BaseResp.ErrCode != 0 {
|
||||
err = fmt.Errorf("DeviceBind Error , errcode=%d , errmsg=%s", result.BaseResp.ErrCode, result.BaseResp.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
60
device/device.go
Normal file
60
device/device.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
uriAuthorize = "https://api.weixin.qq.com/device/authorize_device"
|
||||
uriQRCode = "https://api.weixin.qq.com/device/create_qrcode"
|
||||
uriVerifyQRCode = "https://api.weixin.qq.com/device/verify_qrcode"
|
||||
uriBind = "https://api.weixin.qq.com/device/bind"
|
||||
uriUnbind = "https://api.weixin.qq.com/device/unbind"
|
||||
uriCompelBind = "https://api.weixin.qq.com/device/compel_bind"
|
||||
uriCompelUnbind = "https://api.weixin.qq.com/device/compel_unbind"
|
||||
uriState = "https://api.weixin.qq.com/device/get_stat"
|
||||
)
|
||||
|
||||
//Device struct
|
||||
type Device struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
//NewDevice 实例
|
||||
func NewDevice(context *context.Context) *Device {
|
||||
device := new(Device)
|
||||
device.Context = context
|
||||
return device
|
||||
}
|
||||
|
||||
// ResDeviceState 设备状态响应实体
|
||||
type ResDeviceState struct {
|
||||
util.CommonError
|
||||
Status int `json:"status"`
|
||||
StatusInfo string `json:"status_info"`
|
||||
}
|
||||
|
||||
// State 设备状态查询
|
||||
func (d *Device) State(device string) (res ResDeviceState, err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = d.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s&device_id=%s", uriState, accessToken, device)
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(uri); err != nil {
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal(response, &res); err != nil {
|
||||
return
|
||||
}
|
||||
if res.ErrCode != 0 {
|
||||
err = fmt.Errorf("DeviceState Error , errcode=%d , errmsg=%s", res.ErrCode, res.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
9
device/message.go
Normal file
9
device/message.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package device
|
||||
|
||||
//MsgDevice 设备消息响应
|
||||
type MsgDevice struct {
|
||||
DeviceType string
|
||||
DeviceID string
|
||||
SessionID string
|
||||
OpenID string
|
||||
}
|
||||
76
device/qrcode.go
Normal file
76
device/qrcode.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
//ResCreateQRCode 获取二维码的返回实体
|
||||
type ResCreateQRCode struct {
|
||||
util.CommonError
|
||||
DeviceNum int `json:"device_num"`
|
||||
CodeList []struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
Ticket string `json:"ticket"`
|
||||
} `json:"code_list"`
|
||||
}
|
||||
|
||||
// CreateQRCode 获取设备二维码
|
||||
func (d *Device) CreateQRCode(devices []string) (res ResCreateQRCode, err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = d.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", uriQRCode, accessToken)
|
||||
req := map[string]interface{}{
|
||||
"device_num": len(devices),
|
||||
"device_id_list": devices,
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(uri, req); err != nil {
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal(response, &res); err != nil {
|
||||
return
|
||||
}
|
||||
if res.ErrCode != 0 {
|
||||
err = fmt.Errorf("DeviceCreateQRCode Error , errcode=%d , errmsg=%s", res.ErrCode, res.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//ResVerifyQRCode 验证授权结果实体
|
||||
type ResVerifyQRCode struct {
|
||||
util.CommonError
|
||||
DeviceType string `json:"device_type"`
|
||||
DeviceID string `json:"device_id"`
|
||||
Mac string `json:"mac"`
|
||||
}
|
||||
|
||||
// VerifyQRCode 验证设备二维码
|
||||
func (d *Device) VerifyQRCode(ticket string) (res ResVerifyQRCode, err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = d.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", uriVerifyQRCode, accessToken)
|
||||
req := map[string]interface{}{
|
||||
"ticket": ticket,
|
||||
}
|
||||
fmt.Println(req)
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(uri, req); err != nil {
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal(response, &res); err != nil {
|
||||
return
|
||||
}
|
||||
if res.ErrCode != 0 {
|
||||
err = fmt.Errorf("DeviceCreateQRCode Error , errcode=%d , errmsg=%s", res.ErrCode, res.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -13,6 +13,7 @@ const (
|
||||
addNewsURL = "https://api.weixin.qq.com/cgi-bin/material/add_news"
|
||||
addMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/add_material"
|
||||
delMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/del_material"
|
||||
getMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/get_material"
|
||||
)
|
||||
|
||||
//Material 素材管理
|
||||
@@ -36,6 +37,33 @@ type Article struct {
|
||||
ShowCoverPic int `json:"show_cover_pic"`
|
||||
Content string `json:"content"`
|
||||
ContentSourceURL string `json:"content_source_url"`
|
||||
URL string `json:"url"`
|
||||
DownURL string `json:"down_url"`
|
||||
}
|
||||
|
||||
// GetNews 获取/下载永久素材
|
||||
func (material *Material) GetNews(id string) ([]*Article, error) {
|
||||
accessToken, err := material.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getMaterialURL, accessToken)
|
||||
|
||||
var req struct {
|
||||
MediaID string `json:"media_id"`
|
||||
}
|
||||
req.MediaID = id
|
||||
responseBytes, err := util.PostJSON(uri, req)
|
||||
|
||||
var res struct {
|
||||
NewsItem []*Article `json:"news_item"`
|
||||
}
|
||||
err = json.Unmarshal(responseBytes, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.NewsItem, nil
|
||||
}
|
||||
|
||||
//reqArticles 永久性图文素材请求信息
|
||||
@@ -63,7 +91,7 @@ func (material *Material) AddNews(articles []*Article) (mediaID string, err erro
|
||||
uri := fmt.Sprintf("%s?access_token=%s", addNewsURL, accessToken)
|
||||
responseBytes, err := util.PostJSON(uri, req)
|
||||
var res resArticles
|
||||
err = json.Unmarshal(responseBytes, res)
|
||||
err = json.Unmarshal(responseBytes, &res)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -138,11 +166,11 @@ func (material *Material) AddVideo(filename, title, introduction string) (mediaI
|
||||
fields := []util.MultipartFormField{
|
||||
{
|
||||
IsFile: true,
|
||||
Fieldname: "video",
|
||||
Fieldname: "media",
|
||||
Filename: filename,
|
||||
},
|
||||
{
|
||||
IsFile: true,
|
||||
IsFile: false,
|
||||
Fieldname: "description",
|
||||
Value: fieldValue,
|
||||
},
|
||||
|
||||
160
message/customer_message.go
Normal file
160
message/customer_message.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
customerSendMessage = "https://api.weixin.qq.com/cgi-bin/message/custom/send"
|
||||
)
|
||||
|
||||
//Manager 消息管理者,可以发送消息
|
||||
type Manager struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
//NewMessageManager 实例化消息管理者
|
||||
func NewMessageManager(context *context.Context) *Manager {
|
||||
return &Manager{
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
//CustomerMessage 客服消息
|
||||
type CustomerMessage struct {
|
||||
ToUser string `json:"touser"` //接受者OpenID
|
||||
Msgtype MsgType `json:"msgtype"` //客服消息类型
|
||||
Text *MediaText `json:"text,omitempty"` //可选
|
||||
Image *MediaResource `json:"image,omitempty"` //可选
|
||||
Voice *MediaResource `json:"voice,omitempty"` //可选
|
||||
Video *MediaVideo `json:"video,omitempty"` //可选
|
||||
Music *MediaMusic `json:"music,omitempty"` //可选
|
||||
News *MediaNews `json:"news,omitempty"` //可选
|
||||
Mpnews *MediaResource `json:"mpnews,omitempty"` //可选
|
||||
Wxcard *MediaWxcard `json:"wxcard,omitempty"` //可选
|
||||
Msgmenu *MediaMsgmenu `json:"msgmenu,omitempty"` //可选
|
||||
Miniprogrampage *MediaMiniprogrampage `json:"miniprogrampage,omitempty"` //可选
|
||||
}
|
||||
|
||||
//NewCustomerTextMessage 文本消息结构体构造方法
|
||||
func NewCustomerTextMessage(toUser, text string) *CustomerMessage {
|
||||
return &CustomerMessage{
|
||||
ToUser: toUser,
|
||||
Msgtype: MsgTypeText,
|
||||
Text: &MediaText{
|
||||
text,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//NewCustomerImgMessage 图片消息的构造方法
|
||||
func NewCustomerImgMessage(toUser, mediaID string) *CustomerMessage {
|
||||
return &CustomerMessage{
|
||||
ToUser: toUser,
|
||||
Msgtype: MsgTypeImage,
|
||||
Image: &MediaResource{
|
||||
mediaID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//NewCustomerVoiceMessage 语音消息的构造方法
|
||||
func NewCustomerVoiceMessage(toUser, mediaID string) *CustomerMessage {
|
||||
return &CustomerMessage{
|
||||
ToUser: toUser,
|
||||
Msgtype: MsgTypeVoice,
|
||||
Voice: &MediaResource{
|
||||
mediaID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//MediaText 文本消息的文字
|
||||
type MediaText struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
//MediaResource 消息使用的永久素材id
|
||||
type MediaResource struct {
|
||||
MediaID string `json:"media_id"`
|
||||
}
|
||||
|
||||
//MediaVideo 视频消息包含的内容
|
||||
type MediaVideo struct {
|
||||
MediaID string `json:"media_id"`
|
||||
ThumbMediaID string `json:"thumb_media_id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
//MediaMusic 音乐消息包括的内容
|
||||
type MediaMusic struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Musicurl string `json:"musicurl"`
|
||||
Hqmusicurl string `json:"hqmusicurl"`
|
||||
ThumbMediaID string `json:"thumb_media_id"`
|
||||
}
|
||||
|
||||
//MediaNews 图文消息的内容
|
||||
type MediaNews struct {
|
||||
Articles []MediaArticles `json:"articles"`
|
||||
}
|
||||
|
||||
//MediaArticles 图文消息的内容的文章列表中的单独一条
|
||||
type MediaArticles struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
URL string `json:"url"`
|
||||
Picurl string `json:"picurl"`
|
||||
}
|
||||
|
||||
//MediaMsgmenu 菜单消息的内容
|
||||
type MediaMsgmenu struct {
|
||||
HeadContent string `json:"head_content"`
|
||||
List []MsgmenuItem `json:"list"`
|
||||
TailContent string `json:"tail_content"`
|
||||
}
|
||||
|
||||
//MsgmenuItem 菜单消息的菜单按钮
|
||||
type MsgmenuItem struct {
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
//MediaWxcard 卡券的id
|
||||
type MediaWxcard struct {
|
||||
CardID string `json:"card_id"`
|
||||
}
|
||||
|
||||
//MediaMiniprogrampage 小程序消息
|
||||
type MediaMiniprogrampage struct {
|
||||
Title string `json:"title"`
|
||||
Appid string `json:"appid"`
|
||||
Pagepath string `json:"pagepath"`
|
||||
ThumbMediaID string `json:"thumb_media_id"`
|
||||
}
|
||||
|
||||
//Send 发送客服消息
|
||||
func (manager *Manager) Send(msg *CustomerMessage) error {
|
||||
accessToken, err := manager.Context.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", customerSendMessage, accessToken)
|
||||
response, err := util.PostJSON(uri, msg)
|
||||
var result util.CommonError
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("customer msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package message
|
||||
|
||||
import "encoding/xml"
|
||||
import (
|
||||
"encoding/xml"
|
||||
|
||||
"github.com/silenceper/wechat/device"
|
||||
)
|
||||
|
||||
// MsgType 基本消息类型
|
||||
type MsgType string
|
||||
@@ -8,6 +12,9 @@ type MsgType string
|
||||
// EventType 事件类型
|
||||
type EventType string
|
||||
|
||||
// InfoType 第三方平台授权事件类型
|
||||
type InfoType string
|
||||
|
||||
const (
|
||||
//MsgTypeText 表示文本消息
|
||||
MsgTypeText MsgType = "text"
|
||||
@@ -60,6 +67,19 @@ const (
|
||||
EventLocationSelect = "location_select"
|
||||
//EventTemplateSendJobFinish 发送模板消息推送通知
|
||||
EventTemplateSendJobFinish = "TEMPLATESENDJOBFINISH"
|
||||
//EventWxaMediaCheck 异步校验图片/音频是否含有违法违规内容推送事件
|
||||
EventWxaMediaCheck = "wxa_media_check"
|
||||
)
|
||||
|
||||
const (
|
||||
// InfoTypeVerifyTicket 返回ticket
|
||||
InfoTypeVerifyTicket InfoType = "component_verify_ticket"
|
||||
// InfoTypeAuthorized 授权
|
||||
InfoTypeAuthorized = "authorized"
|
||||
// InfoTypeUnauthorized 取消授权
|
||||
InfoTypeUnauthorized = "unauthorized"
|
||||
// InfoTypeUpdateAuthorized 更新授权
|
||||
InfoTypeUpdateAuthorized = "updateauthorized"
|
||||
)
|
||||
|
||||
//MixMessage 存放所有微信发送过来的消息和事件
|
||||
@@ -110,6 +130,35 @@ type MixMessage struct {
|
||||
Label string `xml:"Label"`
|
||||
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"`
|
||||
|
||||
// 内容审核相关
|
||||
IsRisky bool `xml:"isrisky"`
|
||||
ExtraInfoJSON string `xml:"extra_info_json"`
|
||||
TraceID string `xml:"trace_id"`
|
||||
StatusCode int `xml:"status_code"`
|
||||
|
||||
//设备相关
|
||||
device.MsgDevice
|
||||
}
|
||||
|
||||
//EventPic 发图事件推送
|
||||
@@ -133,22 +182,32 @@ type ResponseEncryptedXMLMsg struct {
|
||||
Nonce string `xml:"Nonce" json:"Nonce"`
|
||||
}
|
||||
|
||||
// CDATA 使用该类型,在序列化为 xml 文本时文本会被解析器忽略
|
||||
type CDATA string
|
||||
|
||||
// MarshalXML 实现自己的序列化方法
|
||||
func (c CDATA) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
return e.EncodeElement(struct {
|
||||
string `xml:",cdata"`
|
||||
}{string(c)}, start)
|
||||
}
|
||||
|
||||
// CommonToken 消息中通用的结构
|
||||
type CommonToken struct {
|
||||
XMLName xml.Name `xml:"xml"`
|
||||
ToUserName string `xml:"ToUserName"`
|
||||
FromUserName string `xml:"FromUserName"`
|
||||
ToUserName CDATA `xml:"ToUserName"`
|
||||
FromUserName CDATA `xml:"FromUserName"`
|
||||
CreateTime int64 `xml:"CreateTime"`
|
||||
MsgType MsgType `xml:"MsgType"`
|
||||
}
|
||||
|
||||
//SetToUserName set ToUserName
|
||||
func (msg *CommonToken) SetToUserName(toUserName string) {
|
||||
func (msg *CommonToken) SetToUserName(toUserName CDATA) {
|
||||
msg.ToUserName = toUserName
|
||||
}
|
||||
|
||||
//SetFromUserName set FromUserName
|
||||
func (msg *CommonToken) SetFromUserName(fromUserName string) {
|
||||
func (msg *CommonToken) SetFromUserName(fromUserName CDATA) {
|
||||
msg.FromUserName = fromUserName
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ package message
|
||||
//Text 文本消息
|
||||
type Text struct {
|
||||
CommonToken
|
||||
Content string `xml:"Content"`
|
||||
Content CDATA `xml:"Content"`
|
||||
}
|
||||
|
||||
//NewText 初始化文本消息
|
||||
func NewText(content string) *Text {
|
||||
text := new(Text)
|
||||
text.Content = content
|
||||
text.Content = CDATA(content)
|
||||
return text
|
||||
}
|
||||
|
||||
305
miniprogram/analysis.go
Normal file
305
miniprogram/analysis.go
Normal 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
|
||||
}
|
||||
130
miniprogram/decrypt.go
Normal file
130
miniprogram/decrypt.go
Normal file
@@ -0,0 +1,130 @@
|
||||
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"`
|
||||
}
|
||||
|
||||
// PhoneInfo 用户手机号
|
||||
type PhoneInfo struct {
|
||||
PhoneNumber string `json:"phoneNumber"`
|
||||
PurePhoneNumber string `json:"purePhoneNumber"`
|
||||
CountryCode string `json:"countryCode"`
|
||||
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
|
||||
}
|
||||
|
||||
// getCipherText returns slice of the cipher text
|
||||
func getCipherText(sessionKey, encryptedData, iv string) ([]byte, 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
|
||||
}
|
||||
return cipherText, nil
|
||||
}
|
||||
|
||||
// Decrypt 解密数据
|
||||
func (wxa *MiniProgram) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo, error) {
|
||||
cipherText, err := getCipherText(sessionKey, encryptedData, iv)
|
||||
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
|
||||
}
|
||||
|
||||
// DecryptPhone 解密数据(手机)
|
||||
func (wxa *MiniProgram) DecryptPhone(sessionKey, encryptedData, iv string) (*PhoneInfo, error) {
|
||||
cipherText, err := getCipherText(sessionKey, encryptedData, iv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var phoneInfo PhoneInfo
|
||||
err = json.Unmarshal(cipherText, &phoneInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if phoneInfo.Watermark.AppID != wxa.AppID {
|
||||
return nil, ErrAppIDNotMatch
|
||||
}
|
||||
return &phoneInfo, nil
|
||||
}
|
||||
17
miniprogram/miniprogram.go
Normal file
17
miniprogram/miniprogram.go
Normal 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
91
miniprogram/qrcode.go
Normal 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
40
miniprogram/sns.go
Normal 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
|
||||
}
|
||||
@@ -11,11 +11,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
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"
|
||||
refreshAccessTokenURL = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%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"
|
||||
redirectOauthURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect"
|
||||
webAppRedirectOauthURL = "https://open.weixin.qq.com/connect/qrconnect?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"
|
||||
refreshAccessTokenURL = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%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 保存用户授权信息
|
||||
@@ -37,6 +38,12 @@ func (oauth *Oauth) GetRedirectURL(redirectURI, scope, state string) (string, er
|
||||
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 跳转到网页授权
|
||||
func (oauth *Oauth) Redirect(writer http.ResponseWriter, req *http.Request, redirectURI, scope, state string) error {
|
||||
location, err := oauth.GetRedirectURL(redirectURI, scope, state)
|
||||
@@ -56,6 +63,10 @@ type ResAccessToken struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
OpenID string `json:"openid"`
|
||||
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)
|
||||
|
||||
84
pay/notify_result.go
Normal file
84
pay/notify_result.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package pay
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/silenceper/wechat/util"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Base 公用参数
|
||||
type Base struct {
|
||||
AppID string `xml:"appid"`
|
||||
MchID string `xml:"mch_id"`
|
||||
NonceStr string `xml:"nonce_str"`
|
||||
Sign string `xml:"sign"`
|
||||
}
|
||||
|
||||
// NotifyResult 下单回调
|
||||
type NotifyResult struct {
|
||||
Base
|
||||
ReturnCode string `xml:"return_code"`
|
||||
ReturnMsg string `xml:"return_msg"`
|
||||
ResultCode string `xml:"result_code"`
|
||||
OpenID string `xml:"openid"`
|
||||
IsSubscribe string `xml:"is_subscribe"`
|
||||
TradeType string `xml:"trade_type"`
|
||||
BankType string `xml:"bank_type"`
|
||||
TotalFee int `xml:"total_fee"`
|
||||
FeeType string `xml:"fee_type"`
|
||||
CashFee int `xml:"cash_fee"`
|
||||
CashFeeType string `xml:"cash_fee_type"`
|
||||
TransactionID string `xml:"transaction_id"`
|
||||
OutTradeNo string `xml:"out_trade_no"`
|
||||
Attach string `xml:"attach"`
|
||||
TimeEnd string `xml:"time_end"`
|
||||
}
|
||||
|
||||
// NotifyResp 消息通知返回
|
||||
type NotifyResp struct {
|
||||
ReturnCode string `xml:"return_code"`
|
||||
ReturnMsg string `xml:"return_msg"`
|
||||
}
|
||||
|
||||
// VerifySign 验签
|
||||
func (pcf *Pay) VerifySign(notifyRes NotifyResult) bool {
|
||||
// 封装map 请求过来的 map
|
||||
resMap := make(map[string]interface{})
|
||||
resMap["appid"] = notifyRes.AppID
|
||||
resMap["bank_type"] = notifyRes.BankType
|
||||
resMap["cash_fee"] = notifyRes.CashFee
|
||||
resMap["fee_type"] = notifyRes.FeeType
|
||||
resMap["is_subscribe"] = notifyRes.IsSubscribe
|
||||
resMap["mch_id"] = notifyRes.MchID
|
||||
resMap["nonce_str"] = notifyRes.NonceStr
|
||||
resMap["openid"] = notifyRes.OpenID
|
||||
resMap["out_trade_no"] = notifyRes.OutTradeNo
|
||||
resMap["result_code"] = notifyRes.ResultCode
|
||||
resMap["return_code"] = notifyRes.ReturnCode
|
||||
resMap["time_end"] = notifyRes.TimeEnd
|
||||
resMap["total_fee"] = notifyRes.TotalFee
|
||||
resMap["trade_type"] = notifyRes.TradeType
|
||||
resMap["transaction_id"] = notifyRes.TransactionID
|
||||
// 支付key
|
||||
sortedKeys := make([]string, 0, len(resMap))
|
||||
for k := range resMap {
|
||||
sortedKeys = append(sortedKeys, k)
|
||||
}
|
||||
sort.Strings(sortedKeys)
|
||||
// STEP2, 对key=value的键值对用&连接起来,略过空值
|
||||
var signStrings string
|
||||
for _, k := range sortedKeys {
|
||||
value := fmt.Sprintf("%v", resMap[k])
|
||||
if value != "" {
|
||||
signStrings = signStrings + k + "=" + value + "&"
|
||||
}
|
||||
}
|
||||
// STEP3, 在键值对的最后加上key=API_KEY
|
||||
signStrings = signStrings + "key=" + pcf.PayKey
|
||||
// STEP4, 进行MD5签名并且将所有字符转为大写.
|
||||
sign := util.MD5Sum(signStrings)
|
||||
if sign != notifyRes.Sign {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
238
pay/pay.go
238
pay/pay.go
@@ -1,9 +1,18 @@
|
||||
package pay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
@@ -24,19 +33,26 @@ type Params struct {
|
||||
Body string
|
||||
OutTradeNo string
|
||||
OpenID string
|
||||
TradeType string
|
||||
SignType string
|
||||
Detail string
|
||||
Attach string
|
||||
GoodsTag string
|
||||
NotifyURL string
|
||||
}
|
||||
|
||||
// Config 是传出用于 jsdk 用的参数
|
||||
// Config 是传出用于 js sdk 用的参数
|
||||
type Config struct {
|
||||
Timestamp int64
|
||||
NonceStr string
|
||||
PrePayID string
|
||||
SignType string
|
||||
Sign string
|
||||
Timestamp string `json:"timestamp"`
|
||||
NonceStr string `json:"nonceStr"`
|
||||
PrePayID string `json:"prePayId"`
|
||||
SignType string `json:"signType"`
|
||||
Package string `json:"package"`
|
||||
PaySign string `json:"paySign"`
|
||||
}
|
||||
|
||||
// payResult 是 unifie order 接口的返回
|
||||
type payResult struct {
|
||||
// PreOrder 是 unifie order 接口的返回
|
||||
type PreOrder struct {
|
||||
ReturnCode string `xml:"return_code"`
|
||||
ReturnMsg string `xml:"return_msg"`
|
||||
AppID string `xml:"appid,omitempty"`
|
||||
@@ -51,7 +67,7 @@ type payResult struct {
|
||||
ErrCodeDes string `xml:"err_code_des,omitempty"`
|
||||
}
|
||||
|
||||
//payRequest 接口请求参数
|
||||
// payRequest 接口请求参数
|
||||
type payRequest struct {
|
||||
AppID string `xml:"appid"`
|
||||
MchID string `xml:"mch_id"`
|
||||
@@ -61,20 +77,20 @@ type payRequest struct {
|
||||
SignType string `xml:"sign_type,omitempty"`
|
||||
Body string `xml:"body"`
|
||||
Detail string `xml:"detail,omitempty"`
|
||||
Attach string `xml:"attach,omitempty"` //附加数据
|
||||
OutTradeNo string `xml:"out_trade_no"` //商户订单号
|
||||
FeeType string `xml:"fee_type,omitempty"` //标价币种
|
||||
TotalFee string `xml:"total_fee"` //标价金额
|
||||
SpbillCreateIP string `xml:"spbill_create_ip"` //终端IP
|
||||
TimeStart string `xml:"time_start,omitempty"` //交易起始时间
|
||||
TimeExpire string `xml:"time_expire,omitempty"` //交易结束时间
|
||||
GoodsTag string `xml:"goods_tag,omitempty"` //订单优惠标记
|
||||
NotifyURL string `xml:"notify_url"` //通知地址
|
||||
TradeType string `xml:"trade_type"` //交易类型
|
||||
ProductID string `xml:"product_id,omitempty"` //商品ID
|
||||
Attach string `xml:"attach,omitempty"` // 附加数据
|
||||
OutTradeNo string `xml:"out_trade_no"` // 商户订单号
|
||||
FeeType string `xml:"fee_type,omitempty"` // 标价币种
|
||||
TotalFee string `xml:"total_fee"` // 标价金额
|
||||
SpbillCreateIP string `xml:"spbill_create_ip"` // 终端IP
|
||||
TimeStart string `xml:"time_start,omitempty"` // 交易起始时间
|
||||
TimeExpire string `xml:"time_expire,omitempty"` // 交易结束时间
|
||||
GoodsTag string `xml:"goods_tag,omitempty"` // 订单优惠标记
|
||||
NotifyURL string `xml:"notify_url"` // 通知地址
|
||||
TradeType string `xml:"trade_type"` // 交易类型
|
||||
ProductID string `xml:"product_id,omitempty"` // 商品ID
|
||||
LimitPay string `xml:"limit_pay,omitempty"` //
|
||||
OpenID string `xml:"openid,omitempty"` //用户标识
|
||||
SceneInfo string `xml:"scene_info,omitempty"` //场景信息
|
||||
OpenID string `xml:"openid,omitempty"` // 用户标识
|
||||
SceneInfo string `xml:"scene_info,omitempty"` // 场景信息
|
||||
}
|
||||
|
||||
// NewPay return an instance of Pay package
|
||||
@@ -83,12 +99,75 @@ func NewPay(ctx *context.Context) *Pay {
|
||||
return &pay
|
||||
}
|
||||
|
||||
// PrePayID will request wechat merchant api and request for a pre payment order id
|
||||
func (pcf *Pay) PrePayID(p *Params) (prePayID string, err error) {
|
||||
// BridgeConfig get js bridge config
|
||||
func (pcf *Pay) BridgeConfig(p *Params) (cfg Config, err error) {
|
||||
var (
|
||||
buffer strings.Builder
|
||||
h hash.Hash
|
||||
timestamp = strconv.FormatInt(time.Now().Unix(), 10)
|
||||
)
|
||||
order, err := pcf.PrePayOrder(p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buffer.WriteString("appId=")
|
||||
buffer.WriteString(order.AppID)
|
||||
buffer.WriteString("&nonceStr=")
|
||||
buffer.WriteString(order.NonceStr)
|
||||
buffer.WriteString("&package=")
|
||||
buffer.WriteString("prepay_id=" + order.PrePayID)
|
||||
buffer.WriteString("&signType=")
|
||||
buffer.WriteString(p.SignType)
|
||||
buffer.WriteString("&timeStamp=")
|
||||
buffer.WriteString(timestamp)
|
||||
buffer.WriteString("&key=")
|
||||
buffer.WriteString(pcf.PayKey)
|
||||
if p.SignType == "MD5" {
|
||||
h = md5.New()
|
||||
} else {
|
||||
h = hmac.New(sha256.New, []byte(pcf.PayKey))
|
||||
}
|
||||
h.Write([]byte(buffer.String()))
|
||||
// 签名
|
||||
cfg.PaySign = strings.ToUpper(hex.EncodeToString(h.Sum(nil)))
|
||||
cfg.NonceStr = order.NonceStr
|
||||
cfg.Timestamp = timestamp
|
||||
cfg.PrePayID = order.PrePayID
|
||||
cfg.SignType = p.SignType
|
||||
cfg.Package = "prepay_id=" + order.PrePayID
|
||||
return
|
||||
}
|
||||
|
||||
// PrePayOrder return data for invoke wechat payment
|
||||
func (pcf *Pay) PrePayOrder(p *Params) (payOrder PreOrder, err error) {
|
||||
nonceStr := util.RandomStr(32)
|
||||
tradeType := "JSAPI"
|
||||
template := "appid=%s&body=%s&mch_id=%s&nonce_str=%s¬ify_url=%s&openid=%s&out_trade_no=%s&spbill_create_ip=%s&total_fee=%s&trade_type=%s&key=%s"
|
||||
str := fmt.Sprintf(template, pcf.AppID, p.Body, pcf.PayMchID, nonceStr, pcf.PayNotifyURL, p.OpenID, p.OutTradeNo, p.CreateIP, p.TotalFee, tradeType, pcf.PayKey)
|
||||
notifyURL := pcf.PayNotifyURL
|
||||
// 签名类型
|
||||
if p.SignType == "" {
|
||||
p.SignType = "MD5"
|
||||
}
|
||||
// 通知地址
|
||||
if p.NotifyURL != "" {
|
||||
notifyURL = p.NotifyURL
|
||||
}
|
||||
param := make(map[string]interface{})
|
||||
param["appid"] = pcf.AppID
|
||||
param["body"] = p.Body
|
||||
param["mch_id"] = pcf.PayMchID
|
||||
param["nonce_str"] = nonceStr
|
||||
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
|
||||
param["sign_type"] = p.SignType
|
||||
param["detail"] = p.Detail
|
||||
param["attach"] = p.Attach
|
||||
param["goods_tag"] = p.GoodsTag
|
||||
param["notify_url"] = notifyURL
|
||||
|
||||
bizKey := "&key=" + pcf.PayKey
|
||||
str := orderParam(param, bizKey)
|
||||
sign := util.MD5Sum(str)
|
||||
request := payRequest{
|
||||
AppID: pcf.AppID,
|
||||
@@ -99,25 +178,104 @@ func (pcf *Pay) PrePayID(p *Params) (prePayID string, err error) {
|
||||
OutTradeNo: p.OutTradeNo,
|
||||
TotalFee: p.TotalFee,
|
||||
SpbillCreateIP: p.CreateIP,
|
||||
NotifyURL: pcf.PayNotifyURL,
|
||||
TradeType: tradeType,
|
||||
NotifyURL: notifyURL,
|
||||
TradeType: p.TradeType,
|
||||
OpenID: p.OpenID,
|
||||
SignType: p.SignType,
|
||||
Detail: p.Detail,
|
||||
Attach: p.Attach,
|
||||
GoodsTag: p.GoodsTag,
|
||||
}
|
||||
rawRet, err := util.PostXML(payGateway, request)
|
||||
if err != nil {
|
||||
return "", errors.New(err.Error() + " parameters : " + str)
|
||||
return
|
||||
}
|
||||
payRet := payResult{}
|
||||
err = xml.Unmarshal(rawRet, &payRet)
|
||||
err = xml.Unmarshal(rawRet, &payOrder)
|
||||
if err != nil {
|
||||
return "", errors.New(err.Error())
|
||||
return
|
||||
}
|
||||
if payRet.ReturnCode == "SUCCESS" {
|
||||
//pay success
|
||||
if payRet.ResultCode == "SUCCESS" {
|
||||
return payRet.PrePayID, nil
|
||||
if payOrder.ReturnCode == "SUCCESS" {
|
||||
// pay success
|
||||
if payOrder.ResultCode == "SUCCESS" {
|
||||
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
109
pay/refund.go
Normal 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
122
qr/qr.go
Normal 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
|
||||
}
|
||||
@@ -65,7 +65,9 @@ func (srv *Server) Serve() error {
|
||||
}
|
||||
|
||||
//debug
|
||||
//fmt.Println("request msg = ", string(srv.requestRawXMLMsg))
|
||||
if srv.debug {
|
||||
fmt.Println("request msg = ", string(srv.requestRawXMLMsg))
|
||||
}
|
||||
|
||||
return srv.buildResponse(response)
|
||||
}
|
||||
|
||||
92
user/user.go
92
user/user.go
@@ -3,6 +3,7 @@ package user
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
const (
|
||||
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"
|
||||
userListURL = "https://api.weixin.qq.com/cgi-bin/user/get"
|
||||
)
|
||||
|
||||
//User 用户管理
|
||||
@@ -29,20 +31,33 @@ 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 []int32 `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"`
|
||||
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 获取用户基本信息
|
||||
@@ -88,3 +103,52 @@ func (user *User) UpdateRemark(openID, remark string) (err error) {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
95
util/http.go
95
util/http.go
@@ -2,11 +2,15 @@ package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"golang.org/x/crypto/pkcs12"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -50,6 +54,32 @@ func PostJSON(uri string, obj interface{}) ([]byte, error) {
|
||||
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 上传文件
|
||||
func PostFile(fieldname, filename, uri string) ([]byte, error) {
|
||||
fields := []MultipartFormField{
|
||||
@@ -141,3 +171,68 @@ func PostXML(uri string, obj interface{}) ([]byte, error) {
|
||||
}
|
||||
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
24
vendor/vendor.json
vendored
@@ -80,6 +80,18 @@
|
||||
"revision": "8ee79997227bf9b34611aee7946ae64735e6fd93",
|
||||
"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=",
|
||||
"path": "github.com/manucorporat/sse",
|
||||
@@ -92,6 +104,18 @@
|
||||
"revision": "30a891c33c7cde7b02a981314b4228ec99380cca",
|
||||
"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=",
|
||||
"path": "golang.org/x/net/context",
|
||||
|
||||
18
wechat.go
18
wechat.go
@@ -1,6 +1,7 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/device"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
@@ -9,8 +10,10 @@ import (
|
||||
"github.com/silenceper/wechat/js"
|
||||
"github.com/silenceper/wechat/material"
|
||||
"github.com/silenceper/wechat/menu"
|
||||
"github.com/silenceper/wechat/miniprogram"
|
||||
"github.com/silenceper/wechat/oauth"
|
||||
"github.com/silenceper/wechat/pay"
|
||||
"github.com/silenceper/wechat/qr"
|
||||
"github.com/silenceper/wechat/server"
|
||||
"github.com/silenceper/wechat/template"
|
||||
"github.com/silenceper/wechat/user"
|
||||
@@ -99,3 +102,18 @@ func (wc *Wechat) GetTemplate() *template.Template {
|
||||
func (wc *Wechat) GetPay() *pay.Pay {
|
||||
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)
|
||||
}
|
||||
|
||||
// GetDevice 获取智能设备的实例
|
||||
func (wc *Wechat) GetDevice() *device.Device {
|
||||
return device.NewDevice(wc.Context)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user