mirror of
https://github.com/silenceper/wechat.git
synced 2026-02-04 12:52:27 +08:00
Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69d0b94fdf | ||
|
|
c14c020a3c | ||
|
|
fe31f04640 | ||
|
|
2ccc15b050 | ||
|
|
3d7d60644f | ||
|
|
5d9705ddc8 | ||
|
|
63f2226974 | ||
|
|
3a1221e7ed | ||
|
|
6c06c05233 | ||
|
|
2af3f42055 | ||
|
|
c4cb394d80 | ||
|
|
681e86e3d4 | ||
|
|
6fe797765f | ||
|
|
74e965b207 | ||
|
|
0e23fa3fee | ||
|
|
351cf65621 | ||
|
|
f3e3564178 | ||
|
|
5e8e16444c | ||
|
|
880ab20a6b | ||
|
|
66369c9541 | ||
|
|
58092a21de | ||
|
|
01f83c20d5 | ||
|
|
972dec0406 | ||
|
|
b599e93c5b | ||
|
|
f77ee8dd2e | ||
|
|
c89d24990c | ||
|
|
e6a61a5ec7 | ||
|
|
4326efae54 | ||
|
|
5d3f4be9a2 | ||
|
|
9b5bb79a72 | ||
|
|
a4a388aef7 | ||
|
|
7a4230414d | ||
|
|
239fe1d758 | ||
|
|
3f7690cf24 | ||
|
|
2e94f61043 | ||
|
|
a3ce22df79 | ||
|
|
f9a2c15361 | ||
|
|
3360ee7752 | ||
|
|
9c0cde64e5 | ||
|
|
748fe17e72 | ||
|
|
340e1a3c5b | ||
|
|
165d8f4d96 | ||
|
|
5557975960 | ||
|
|
7842eed32f | ||
|
|
d69578bedc | ||
|
|
fad1b1839b | ||
|
|
a8abc23893 | ||
|
|
c372d7a83c | ||
|
|
85b0a114dc | ||
|
|
ea1b2d92d0 | ||
|
|
d91d1dd630 | ||
|
|
b147370c1d | ||
|
|
293d47c6fb | ||
|
|
aa75de8144 | ||
|
|
2423b58855 | ||
|
|
2c7d3aaf3e | ||
|
|
ebf85640f7 | ||
|
|
b6bba6ab66 | ||
|
|
50588e3d93 | ||
|
|
9084163132 | ||
|
|
51cbfab087 | ||
|
|
7e8c94a6c4 | ||
|
|
15ebd71a04 | ||
|
|
9b06954b10 | ||
|
|
431f7d3a9f | ||
|
|
8e24b47a70 | ||
|
|
83bd282760 | ||
|
|
bf167d959c | ||
|
|
08b69d9419 | ||
|
|
ab39ec00d4 | ||
|
|
cafb84d6da | ||
|
|
d46df74eee | ||
|
|
96678d2279 | ||
|
|
6e5bb2553d | ||
|
|
1dbe3f60ea | ||
|
|
0f99e2e34a | ||
|
|
7ea817a7c6 | ||
|
|
de140f1037 | ||
|
|
688bca7436 | ||
|
|
58d6810432 | ||
|
|
b0f1f71f37 |
50
.github/workflows/go.yml
vendored
Normal file
50
.github/workflows/go.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master,release-* ]
|
||||
pull_request:
|
||||
branches: [ master,release-* ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: --entrypoint redis-server
|
||||
memcached:
|
||||
image: memcached
|
||||
ports:
|
||||
- 11211:11211
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.13
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go get -v -t -d ./...
|
||||
if [ -f Gopkg.toml ]; then
|
||||
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
|
||||
dep ensure
|
||||
fi
|
||||
|
||||
- name: Lint Go Code
|
||||
run: |
|
||||
export PATH=$PATH:$(go env GOPATH)/bin # temporary fix. See https://github.com/actions/setup-go/issues/14
|
||||
go get -u golang.org/x/lint/golint
|
||||
go vet ./...
|
||||
golint -set_exit_status $(go list ./...)
|
||||
|
||||
- name: Test
|
||||
run: go test -v -race ./...
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -24,5 +24,6 @@ _testmain.go
|
||||
*.prof
|
||||
.DS_Store
|
||||
.vscode/
|
||||
vendor/*/
|
||||
vendor
|
||||
.idea/
|
||||
example/*
|
||||
20
.travis.yml
20
.travis.yml
@@ -1,20 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.13.x
|
||||
- 1.12.x
|
||||
- 1.11.x
|
||||
- 1.10.x
|
||||
|
||||
services:
|
||||
- memcached
|
||||
- redis-server
|
||||
|
||||
before_script:
|
||||
- GO_FILES=$(find . -iname '*.go' -type f | grep -v /vendor/)
|
||||
- go get golang.org/x/lint/golint
|
||||
|
||||
script:
|
||||
- go test -v -race ./...
|
||||
- go vet ./...
|
||||
- golint -set_exit_status $(go list ./...)
|
||||
666
README.md
666
README.md
@@ -1,642 +1,72 @@
|
||||
# WeChat SDK for Go
|
||||
[](https://travis-ci.org/silenceper/wechat)
|
||||

|
||||
[](https://goreportcard.com/report/github.com/silenceper/wechat)
|
||||
[](http://godoc.org/github.com/silenceper/wechat)
|
||||
|
||||
[](https://pkg.go.dev/github.com/silenceper/wechat/v2?tab=doc)
|
||||
|
||||
使用Golang开发的微信SDK,简单、易用。
|
||||
>当前版本为2.0版本
|
||||
|
||||
|
||||
## 文档 && 例子
|
||||
[Wechat SDK 2.0 文档](http://silenceper.com/wechat)
|
||||
|
||||
[Wechat SDK 2.0 例子](https://github.com/gowechat/example)
|
||||
|
||||
## 快速开始
|
||||
|
||||
以下是一个处理消息接收以及回复的例子:
|
||||
以下是一个微信公众号处理消息接收以及回复的例子:
|
||||
|
||||
```go
|
||||
//使用memcache保存access_token,也可选择redis或自定义cache
|
||||
memCache=cache.NewMemcache("127.0.0.1:11211")
|
||||
|
||||
//配置微信参数
|
||||
config := &wechat.Config{
|
||||
AppID: "xxxx",
|
||||
AppSecret: "xxxx",
|
||||
Token: "xxxx",
|
||||
EncodingAESKey: "xxxx",
|
||||
Cache: memCache
|
||||
wc := wechat.NewWechat()
|
||||
memory := cache.NewMemory()
|
||||
cfg := &offConfig.Config{
|
||||
AppID: "xxx",
|
||||
AppSecret: "xxx",
|
||||
Token: "xxx",
|
||||
//EncodingAESKey: "xxxx",
|
||||
Cache: memory,
|
||||
}
|
||||
wc := wechat.NewWechat(config)
|
||||
officialAccount := wc.GetOfficialAccount(cfg)
|
||||
|
||||
// 传入request和responseWriter
|
||||
server := wc.GetServer(request, responseWriter)
|
||||
server := officialAccount.GetServer(req, rw)
|
||||
//设置接收消息的处理方法
|
||||
server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {
|
||||
|
||||
//回复消息:演示回复用户发送的消息
|
||||
text := message.NewText(msg.Content)
|
||||
return &message.Reply{message.MsgTypeText, text}
|
||||
//回复消息:演示回复用户发送的消息
|
||||
text := message.NewText(msg.Content)
|
||||
return &message.Reply{MsgType: message.MsgTypeText, MsgData: text}
|
||||
})
|
||||
|
||||
server.Serve()
|
||||
//处理消息接收以及回复
|
||||
err := server.Serve()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
//发送回复的消息
|
||||
server.Send()
|
||||
|
||||
```
|
||||
完整代码:[examples/http/http.go](./examples/http/http.go)
|
||||
|
||||
#### 和主流框架配合使用
|
||||
|
||||
主要是request和responseWriter在不同框架中获取方式可能不一样:
|
||||
|
||||
- Beego: [./examples/beego/beego.go](./examples/beego/beego.go)
|
||||
- Gin Framework: [./examples/gin/gin.go](./examples/gin/gin.go)
|
||||
|
||||
## 基本配置
|
||||
|
||||
```go
|
||||
memcache := cache.NewMemcache("127.0.0.1:11211")
|
||||
|
||||
wcConfig := &wechat.Config{
|
||||
AppID: cfg.AppID,
|
||||
AppSecret: cfg.AppSecret,
|
||||
Token: cfg.Token,
|
||||
EncodingAESKey: cfg.EncodingAESKey,//消息加解密时用到
|
||||
Cache: memcache,
|
||||
}
|
||||
```
|
||||
|
||||
**Cache 设置**
|
||||
|
||||
Cache主要用来保存全局access_token以及js-sdk中的ticket:
|
||||
默认采用memcache存储。当然也可以直接实现`cache/cache.go`中的接口
|
||||
|
||||
|
||||
## 基本API使用
|
||||
|
||||
- [消息管理](#消息管理)
|
||||
- 接收普通消息
|
||||
- 接收事件推送
|
||||
- 被动回复消息
|
||||
- 回复文本消息
|
||||
- 回复图片消息
|
||||
- 回复视频消息
|
||||
- 回复音乐消息
|
||||
- 回复图文消息
|
||||
- [自定义菜单](#自定义菜单)
|
||||
- 自定义菜单创建接口
|
||||
- 自定义菜单查询接口
|
||||
- 自定义菜单删除接口
|
||||
- 自定义菜单事件推送
|
||||
- 个性化菜单接口
|
||||
- 添加个性化菜单
|
||||
- 删除个性化菜单
|
||||
- 测试个性化菜单匹配结果
|
||||
- 获取公众号菜单配置
|
||||
- [微信网页开发](#微信网页开发)
|
||||
- Oauth2 授权
|
||||
- 发起授权
|
||||
- 通过code换取access_token
|
||||
- 拉取用户信息
|
||||
- 刷新access_token
|
||||
- 检验access_token是否有效
|
||||
- 获取js-sdk配置
|
||||
- [素材管理](#素材管理)
|
||||
- [小程序开发](#小程序开发)
|
||||
|
||||
## 消息管理
|
||||
|
||||
通过`wechat.GetServer(request,responseWriter)`获取到server对象之后
|
||||
|
||||
调用`SetMessageHandler(func(msg message.MixMessage){})`设置消息的处理函数,函数参数为message.MixMessage 结构如下:
|
||||
|
||||
```go
|
||||
//MixMessage 存放所有微信发送过来的消息和事件
|
||||
type MixMessage struct {
|
||||
CommonToken
|
||||
|
||||
//基本消息
|
||||
MsgID int64 `xml:"MsgId"`
|
||||
Content string `xml:"Content"`
|
||||
PicURL string `xml:"PicUrl"`
|
||||
MediaID string `xml:"MediaId"`
|
||||
Format string `xml:"Format"`
|
||||
ThumbMediaID string `xml:"ThumbMediaId"`
|
||||
LocationX float64 `xml:"Location_X"`
|
||||
LocationY float64 `xml:"Location_Y"`
|
||||
Scale float64 `xml:"Scale"`
|
||||
Label string `xml:"Label"`
|
||||
Title string `xml:"Title"`
|
||||
Description string `xml:"Description"`
|
||||
URL string `xml:"Url"`
|
||||
|
||||
//事件相关
|
||||
Event string `xml:"Event"`
|
||||
EventKey string `xml:"EventKey"`
|
||||
Ticket string `xml:"Ticket"`
|
||||
Latitude string `xml:"Latitude"`
|
||||
Longitude string `xml:"Longitude"`
|
||||
Precision string `xml:"Precision"`
|
||||
|
||||
MenuID string `xml:"MenuId"`
|
||||
|
||||
//扫码事件
|
||||
ScanCodeInfo struct {
|
||||
ScanType string `xml:"ScanType"`
|
||||
ScanResult string `xml:"ScanResult"`
|
||||
} `xml:"ScanCodeInfo"`
|
||||
|
||||
//发图事件
|
||||
SendPicsInfo struct {
|
||||
Count int32 `xml:"Count"`
|
||||
PicList []EventPic `xml:"PicList>item"`
|
||||
} `xml:"SendPicsInfo"`
|
||||
|
||||
//发送地理位置事件
|
||||
SendLocationInfo struct {
|
||||
LocationX float64 `xml:"Location_X"`
|
||||
LocationY float64 `xml:"Location_Y"`
|
||||
Scale float64 `xml:"Scale"`
|
||||
Label string `xml:"Label"`
|
||||
Poiname string `xml:"Poiname"`
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
具体参数请参考微信文档:[接收普通消息
|
||||
](http://mp.weixin.qq.com/wiki/17/f298879f8fb29ab98b2f2971d42552fd.html)
|
||||
|
||||
### 接收普通消息
|
||||
```go
|
||||
server.SetMessageHandler(func(v message.MixMessage) *message.Reply {
|
||||
switch v.MsgType {
|
||||
//文本消息
|
||||
case message.MsgTypeText:
|
||||
//do something
|
||||
|
||||
//图片消息
|
||||
case message.MsgTypeImage:
|
||||
//do something
|
||||
|
||||
//语音消息
|
||||
case message.MsgTypeVoice:
|
||||
//do something
|
||||
|
||||
//视频消息
|
||||
case message.MsgTypeVideo:
|
||||
//do something
|
||||
|
||||
//小视频消息
|
||||
case message.MsgTypeShortVideo:
|
||||
//do something
|
||||
|
||||
//地理位置消息
|
||||
case message.MsgTypeLocation:
|
||||
//do something
|
||||
|
||||
//链接消息
|
||||
case message.MsgTypeLink:
|
||||
//do something
|
||||
|
||||
//事件推送消息
|
||||
case message.MsgTypeEvent:
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
### 接收事件推送
|
||||
```go
|
||||
//事件推送消息
|
||||
case message.MsgTypeEvent:
|
||||
switch v.Event {
|
||||
//EventSubscribe 订阅
|
||||
case message.EventSubscribe:
|
||||
//do something
|
||||
|
||||
//取消订阅
|
||||
case message.EventUnsubscribe:
|
||||
//do something
|
||||
|
||||
//用户已经关注公众号,则微信会将带场景值扫描事件推送给开发者
|
||||
case message.EventScan:
|
||||
//do something
|
||||
|
||||
// 上报地理位置事件
|
||||
case message.EventLocation:
|
||||
//do something
|
||||
|
||||
// 点击菜单拉取消息时的事件推送
|
||||
case message.EventClick:
|
||||
//do something
|
||||
|
||||
// 点击菜单跳转链接时的事件推送
|
||||
case message.EventView:
|
||||
//do something
|
||||
|
||||
// 扫码推事件的事件推送
|
||||
case message.EventScancodePush:
|
||||
//do something
|
||||
|
||||
// 扫码推事件且弹出“消息接收中”提示框的事件推送
|
||||
case message.EventScancodeWaitmsg:
|
||||
//do something
|
||||
|
||||
// 弹出系统拍照发图的事件推送
|
||||
case message.EventPicSysphoto:
|
||||
//do something
|
||||
|
||||
// 弹出拍照或者相册发图的事件推送
|
||||
case message.EventPicPhotoOrAlbum:
|
||||
//do something
|
||||
|
||||
// 弹出微信相册发图器的事件推送
|
||||
case message.EventPicWeixin:
|
||||
//do something
|
||||
|
||||
// 弹出地理位置选择器的事件推送
|
||||
case message.EventLocationSelect:
|
||||
//do something
|
||||
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
### 被动回复消息
|
||||
|
||||
回复消息需要返回 `*message.Reply` 对象结构体如下:
|
||||
|
||||
```go
|
||||
type Reply struct {
|
||||
MsgType MsgType //消息类型
|
||||
MsgData interface{} //消息结构
|
||||
}
|
||||
|
||||
```
|
||||
注意:`return nil`表示什么也不做
|
||||
|
||||
#### 回复文本消息
|
||||
```go
|
||||
text := message.NewText("回复文本消息")
|
||||
return &message.Reply{message.MsgTypeText, text}
|
||||
```
|
||||
#### 回复图片消息
|
||||
```go
|
||||
//mediaID 可通过素材管理-上上传多媒体文件获得
|
||||
image :=message.NewImage("mediaID")
|
||||
return &message.Reply{message.MsgTypeImage, image}
|
||||
```
|
||||
#### 回复视频消息
|
||||
```go
|
||||
video := message.NewVideo("mediaID", "视频标题", "视频描述")
|
||||
return &message.Reply{message.MsgTypeVideo, video}
|
||||
```
|
||||
#### 回复音乐消息
|
||||
```go
|
||||
music := message.NewMusic("title", "description", "musicURL", "hQMusicURL", "thumbMediaID")
|
||||
return &message.Reply{message.MsgTypeMusic,music}
|
||||
```
|
||||
**字段说明:**
|
||||
|
||||
Title:音乐标题
|
||||
|
||||
Description:音乐描述
|
||||
|
||||
MusicURL:音乐链接
|
||||
|
||||
HQMusicUrl:高质量音乐链接,WIFI环境优先使用该链接播放音乐
|
||||
|
||||
ThumbMediaId:缩略图的媒体id,通过素材管理接口上传多媒体文件,得到的id
|
||||
|
||||
#### 回复图文消息
|
||||
|
||||
```go
|
||||
articles := make([]*message.Article, 1)
|
||||
|
||||
article := new(message.Article)
|
||||
article.Title = "标题"
|
||||
article.Description = "描述信息信息信息"
|
||||
article.PicURL = "http://ww1.sinaimg.cn/large/65209136gw1f7vhjw95eqj20wt0zk40z.jpg"
|
||||
article.URL = "https://github.com/silenceper/wechat"
|
||||
articles[0] = article
|
||||
|
||||
news := message.NewNews(articles)
|
||||
return &message.Reply{message.MsgTypeNews,news}
|
||||
```
|
||||
**字段说明:**
|
||||
|
||||
Title:图文消息标题
|
||||
|
||||
Description:图文消息描述
|
||||
|
||||
PicUrl :图片链接,支持JPG、PNG格式,较好的效果为大图360*200,小图200*200
|
||||
|
||||
Url :点击图文消息跳转链接
|
||||
|
||||
|
||||
## 自定义菜单
|
||||
|
||||
通过` wechat.GetMenu()`获取menu的实例
|
||||
|
||||
### 自定义菜单创建接口
|
||||
|
||||
以下是一个创建二级菜单的例子
|
||||
|
||||
```go
|
||||
mu := wc.GetMenu()
|
||||
|
||||
buttons := make([]*menu.Button, 1)
|
||||
btn := new(menu.Button)
|
||||
|
||||
//创建click类型菜单
|
||||
btn.SetClickButton("name", "key123")
|
||||
buttons[0] = btn
|
||||
|
||||
//设置btn为二级菜单
|
||||
btn2 := new(menu.Button)
|
||||
btn2.SetSubButton("subButton", buttons)
|
||||
|
||||
buttons2 := make([]*menu.Button, 1)
|
||||
buttons2[0] = btn2
|
||||
|
||||
//发送请求
|
||||
err := mu.SetMenu(buttons2)
|
||||
if err != nil {
|
||||
fmt.Printf("err= %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**创建其他类型的菜单:**
|
||||
|
||||
```go
|
||||
//SetViewButton view类型
|
||||
func (btn *Button) SetViewButton(name, url string)
|
||||
|
||||
// SetScanCodePushButton 扫码推事件
|
||||
func (btn *Button) SetScanCodePushButton(name, key string)
|
||||
|
||||
//SetScanCodeWaitMsgButton 设置 扫码推事件且弹出"消息接收中"提示框
|
||||
func (btn *Button) SetScanCodeWaitMsgButton(name, key string)
|
||||
|
||||
//SetPicSysPhotoButton 设置弹出系统拍照发图按钮
|
||||
func (btn *Button) SetPicSysPhotoButton(name, key string)
|
||||
|
||||
//SetPicPhotoOrAlbumButton 设置弹出拍照或者相册发图类型按钮
|
||||
func (btn *Button) SetPicPhotoOrAlbumButton(name, key string) {
|
||||
|
||||
// SetPicWeixinButton 设置弹出微信相册发图器类型按钮
|
||||
func (btn *Button) SetPicWeixinButton(name, key string)
|
||||
|
||||
// SetLocationSelectButton 设置 弹出地理位置选择器 类型按钮
|
||||
func (btn *Button) SetLocationSelectButton(name, key string)
|
||||
|
||||
//SetMediaIDButton 设置 下发消息(除文本消息) 类型按钮
|
||||
func (btn *Button) SetMediaIDButton(name, mediaID string)
|
||||
|
||||
//SetViewLimitedButton 设置 跳转图文消息URL 类型按钮
|
||||
func (btn *Button) SetViewLimitedButton(name, mediaID string) {
|
||||
|
||||
```
|
||||
|
||||
### 自定义菜单查询接口
|
||||
|
||||
```go
|
||||
mu := wc.GetMenu()
|
||||
resMenu,err:=mu.GetMenu()
|
||||
```
|
||||
>返回结果 resMenu 结构参考 ./menu/menu.go 中ResMenu 结构体
|
||||
|
||||
### 自定义菜单删除接口
|
||||
|
||||
```go
|
||||
mu := wc.GetMenu()
|
||||
err:=mu.DeleteMenu()
|
||||
```
|
||||
|
||||
### 自定义菜单事件推送
|
||||
|
||||
请参考 消息管理 - 事件推送
|
||||
|
||||
### 个性化菜单接口
|
||||
**添加个性化菜单**
|
||||
|
||||
```go
|
||||
|
||||
func (menu *Menu) AddConditional(buttons []*Button, matchRule *MatchRule) error
|
||||
```
|
||||
|
||||
**删除个性化菜单**
|
||||
|
||||
```go
|
||||
//删除个性化菜单
|
||||
func (menu *Menu) DeleteConditional(menuID int64) error
|
||||
|
||||
```
|
||||
**测试个性化菜单匹配结果**
|
||||
|
||||
```go
|
||||
//菜单匹配
|
||||
func (menu *Menu) MenuTryMatch(userID string) (buttons []Button, err error) {
|
||||
|
||||
```
|
||||
|
||||
### 获取公众号菜单配置
|
||||
|
||||
```go
|
||||
//获取自定义菜单配置接口
|
||||
func (menu *Menu) GetCurrentSelfMenuInfo() (resSelfMenuInfo ResSelfMenuInfo, err error)
|
||||
|
||||
```
|
||||
|
||||
## 微信网页开发
|
||||
|
||||
### Oauth2 授权
|
||||
|
||||
具体授权流程请参考微信文档:[网页授权](http://mp.weixin.qq.com/wiki/4/9ac2e7b1f1d22e9e57260f6553822520.html)
|
||||
|
||||
**1.发起授权**
|
||||
|
||||
```go
|
||||
oauth := wc.GetOauth()
|
||||
err := oauth.Redirect("跳转的绝对地址", "snsapi_userinfo", "123dd123")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
```
|
||||
> 如果不希望直接跳转,可通过 oauth.GetRedirectURL 获取跳转的url
|
||||
|
||||
**2.通过code换取access_token**
|
||||
|
||||
```go
|
||||
code := c.Query("code")
|
||||
resToken, err := oauth.GetUserAccessToken(code)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
```
|
||||
**3.拉取用户信息(需scope为 snsapi_userinfo)**
|
||||
|
||||
```go
|
||||
//getUserInfo
|
||||
userInfo, err := oauth.GetUserInfo(resToken.AccessToken, resToken.OpenID)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(userInfo)
|
||||
|
||||
```
|
||||
**刷新access_token**
|
||||
|
||||
```go
|
||||
func (oauth *Oauth) RefreshAccessToken(refreshToken string) (result ResAccessToken, err error)
|
||||
|
||||
```
|
||||
**检验access_token是否有效**
|
||||
|
||||
```go
|
||||
func (oauth *Oauth) CheckAccessToken(accessToken, openID string) (b bool, err error)
|
||||
```
|
||||
|
||||
### 获取js-sdk配置
|
||||
|
||||
```go
|
||||
js := wc.GetJs()
|
||||
cfg, err := js.GetConfig("传入需要的调用js-sdk的url地址")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(cfg)
|
||||
```
|
||||
其中返回的cfg结构体如下:
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
AppID string `json:"app_id"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
NonceStr string `json:"nonce_str"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 素材管理
|
||||
|
||||
[素材管理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)
|
||||
## 目录说明
|
||||
- officialaccount: 微信公众号API
|
||||
- miniprogram: 小程序API
|
||||
- minigame:小游戏API
|
||||
- pay:微信支付API
|
||||
- opernplatform:开放平台API
|
||||
- work:企业微信
|
||||
- aispeech:智能对话
|
||||
|
||||
## 贡献
|
||||
- 提交issue,描述需要贡献的内容
|
||||
- 完成更改后,提交PR
|
||||
|
||||
感谢以下成员贡献.
|
||||
<a href="https://github.com/silenceper/wechat/graphs/contributors"><img src="https://opencollective.com/gowechat/contributors.svg?width=890&button=false" /></a>
|
||||
## 公众号
|
||||

|
||||
|
||||
## License
|
||||
|
||||
|
||||
5
aispeech/README.md
Normal file
5
aispeech/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 智能对话
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/doc/aispeech/platform/INTERFACEDOCUMENT.html)
|
||||
|
||||
## 快速入门
|
||||
16
cache/memcache_test.go
vendored
16
cache/memcache_test.go
vendored
@@ -3,6 +3,9 @@ package cache
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bradfitz/gomemcache/memcache"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMemcache(t *testing.T) {
|
||||
@@ -16,13 +19,22 @@ func TestMemcache(t *testing.T) {
|
||||
if !mem.IsExist("username") {
|
||||
t.Error("IsExist Error")
|
||||
}
|
||||
exists := mem.IsExist("unknown-key")
|
||||
assert.Equal(t, false, exists)
|
||||
|
||||
name := mem.Get("username").(string)
|
||||
if name != "silenceper" {
|
||||
t.Error("get Error")
|
||||
if name != "" {
|
||||
if name != "silenceper" {
|
||||
t.Error("get Error")
|
||||
}
|
||||
}
|
||||
data := mem.Get("unknown-key")
|
||||
assert.Nil(t, data)
|
||||
|
||||
if err = mem.Delete("username"); err != nil {
|
||||
t.Errorf("delete Error , err=%v", err)
|
||||
}
|
||||
|
||||
err = mem.Delete("unknown-key")
|
||||
assert.Equal(t, memcache.ErrCacheMiss, err)
|
||||
}
|
||||
|
||||
7
cache/redis.go
vendored
7
cache/redis.go
vendored
@@ -19,7 +19,7 @@ type RedisOpts struct {
|
||||
Database int `yml:"database" json:"database"`
|
||||
MaxIdle int `yml:"max_idle" json:"max_idle"`
|
||||
MaxActive int `yml:"max_active" json:"max_active"`
|
||||
IdleTimeout int32 `yml:"idle_timeout" json:"idle_timeout"` //second
|
||||
IdleTimeout int `yml:"idle_timeout" json:"idle_timeout"` //second
|
||||
}
|
||||
|
||||
//NewRedis 实例化
|
||||
@@ -45,6 +45,11 @@ func NewRedis(opts *RedisOpts) *Redis {
|
||||
return &Redis{pool}
|
||||
}
|
||||
|
||||
//SetRedisPool 设置redis连接池
|
||||
func (r *Redis) SetRedisPool(pool *redis.Pool) {
|
||||
r.conn = pool
|
||||
}
|
||||
|
||||
//SetConn 设置conn
|
||||
func (r *Redis) SetConn(conn *redis.Pool) {
|
||||
r.conn = conn
|
||||
|
||||
1
cache/redis_test.go
vendored
1
cache/redis_test.go
vendored
@@ -10,6 +10,7 @@ func TestRedis(t *testing.T) {
|
||||
Host: "127.0.0.1:6379",
|
||||
}
|
||||
redis := NewRedis(opts)
|
||||
redis.SetConn(redis.conn)
|
||||
var err error
|
||||
timeoutDuration := 1 * time.Second
|
||||
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
//AccessTokenURL 获取access_token的接口
|
||||
AccessTokenURL = "https://api.weixin.qq.com/cgi-bin/token"
|
||||
)
|
||||
|
||||
//ResAccessToken struct
|
||||
type ResAccessToken struct {
|
||||
util.CommonError
|
||||
|
||||
AccessToken string `json:"access_token"`
|
||||
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 {
|
||||
accessToken = val.(string)
|
||||
return
|
||||
}
|
||||
|
||||
//从微信服务器获取
|
||||
var resAccessToken ResAccessToken
|
||||
resAccessToken, err = ctx.GetAccessTokenFromServer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
accessToken = resAccessToken.AccessToken
|
||||
return
|
||||
}
|
||||
|
||||
//GetAccessTokenFromServer 强制从微信服务器获取token
|
||||
func (ctx *Context) GetAccessTokenFromServer() (resAccessToken ResAccessToken, err error) {
|
||||
url := fmt.Sprintf("%s?grant_type=client_credential&appid=%s&secret=%s", AccessTokenURL, ctx.AppID, ctx.AppSecret)
|
||||
var body []byte
|
||||
body, err = util.HTTPGet(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(body, &resAccessToken)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if resAccessToken.ErrMsg != "" {
|
||||
err = fmt.Errorf("get access_token error : errcode=%v , errormsg=%v", resAccessToken.ErrCode, resAccessToken.ErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
accessTokenCacheKey := fmt.Sprintf("access_token_%s", ctx.AppID)
|
||||
expires := resAccessToken.ExpiresIn - 1500
|
||||
err = ctx.Cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
||||
return
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/silenceper/wechat/cache"
|
||||
)
|
||||
|
||||
// Context struct
|
||||
type Context struct {
|
||||
AppID string
|
||||
AppSecret string
|
||||
Token string
|
||||
EncodingAESKey string
|
||||
PayMchID string
|
||||
PayNotifyURL string
|
||||
PayKey string
|
||||
|
||||
Cache cache.Cache
|
||||
|
||||
Writer http.ResponseWriter
|
||||
Request *http.Request
|
||||
|
||||
//accessTokenLock 读写锁 同一个AppID一个
|
||||
accessTokenLock *sync.RWMutex
|
||||
|
||||
//jsAPITicket 读写锁 同一个AppID一个
|
||||
jsAPITicketLock *sync.RWMutex
|
||||
|
||||
//accessTokenFunc 自定义获取 access token 的方法
|
||||
accessTokenFunc GetAccessTokenFunc
|
||||
}
|
||||
|
||||
// Query returns the keyed url query value if it exists
|
||||
func (ctx *Context) Query(key string) string {
|
||||
value, _ := ctx.GetQuery(key)
|
||||
return value
|
||||
}
|
||||
|
||||
// GetQuery is like Query(), it returns the keyed url query value
|
||||
func (ctx *Context) GetQuery(key string) (string, bool) {
|
||||
req := ctx.Request
|
||||
if values, ok := req.URL.Query()[key]; ok && len(values) > 0 {
|
||||
return values[0], true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// SetJsAPITicketLock 设置jsAPITicket的lock
|
||||
func (ctx *Context) SetJsAPITicketLock(lock *sync.RWMutex) {
|
||||
ctx.jsAPITicketLock = lock
|
||||
}
|
||||
|
||||
// GetJsAPITicketLock 获取jsAPITicket 的lock
|
||||
func (ctx *Context) GetJsAPITicketLock() *sync.RWMutex {
|
||||
return ctx.jsAPITicketLock
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
//qyAccessTokenURL 获取access_token的接口
|
||||
qyAccessTokenURL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s"
|
||||
)
|
||||
|
||||
//ResQyAccessToken struct
|
||||
type ResQyAccessToken struct {
|
||||
util.CommonError
|
||||
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
//SetQyAccessTokenLock 设置读写锁(一个appID一个读写锁)
|
||||
func (ctx *Context) SetQyAccessTokenLock(l *sync.RWMutex) {
|
||||
ctx.accessTokenLock = l
|
||||
}
|
||||
|
||||
//GetQyAccessToken 获取access_token
|
||||
func (ctx *Context) GetQyAccessToken() (accessToken string, err error) {
|
||||
ctx.accessTokenLock.Lock()
|
||||
defer ctx.accessTokenLock.Unlock()
|
||||
|
||||
accessTokenCacheKey := fmt.Sprintf("qy_access_token_%s", ctx.AppID)
|
||||
val := ctx.Cache.Get(accessTokenCacheKey)
|
||||
if val != nil {
|
||||
accessToken = val.(string)
|
||||
return
|
||||
}
|
||||
|
||||
//从微信服务器获取
|
||||
var resQyAccessToken ResQyAccessToken
|
||||
resQyAccessToken, err = ctx.GetQyAccessTokenFromServer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
accessToken = resQyAccessToken.AccessToken
|
||||
return
|
||||
}
|
||||
|
||||
//GetQyAccessTokenFromServer 强制从微信服务器获取token
|
||||
func (ctx *Context) GetQyAccessTokenFromServer() (resQyAccessToken ResQyAccessToken, err error) {
|
||||
log.Printf("GetQyAccessTokenFromServer")
|
||||
url := fmt.Sprintf(qyAccessTokenURL, ctx.AppID, ctx.AppSecret)
|
||||
var body []byte
|
||||
body, err = util.HTTPGet(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(body, &resQyAccessToken)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if resQyAccessToken.ErrCode != 0 {
|
||||
err = fmt.Errorf("get qy_access_token error : errcode=%v , errormsg=%v", resQyAccessToken.ErrCode, resQyAccessToken.ErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
qyAccessTokenCacheKey := fmt.Sprintf("qy_access_token_%s", ctx.AppID)
|
||||
expires := resQyAccessToken.ExpiresIn - 1500
|
||||
err = ctx.Cache.Set(qyAccessTokenCacheKey, resQyAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
||||
return
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var xmlContentType = []string{"application/xml; charset=utf-8"}
|
||||
var plainContentType = []string{"text/plain; charset=utf-8"}
|
||||
|
||||
//Render render from bytes
|
||||
func (ctx *Context) Render(bytes []byte) {
|
||||
//debug
|
||||
//fmt.Println("response msg = ", string(bytes))
|
||||
ctx.Writer.WriteHeader(200)
|
||||
_, err := ctx.Writer.Write(bytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
//String render from string
|
||||
func (ctx *Context) String(str string) {
|
||||
writeContextType(ctx.Writer, plainContentType)
|
||||
ctx.Render([]byte(str))
|
||||
}
|
||||
|
||||
//XML render to xml
|
||||
func (ctx *Context) XML(obj interface{}) {
|
||||
writeContextType(ctx.Writer, xmlContentType)
|
||||
bytes, err := xml.Marshal(obj)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ctx.Render(bytes)
|
||||
}
|
||||
|
||||
func writeContextType(w http.ResponseWriter, value []string) {
|
||||
header := w.Header()
|
||||
if val := header["Content-Type"]; len(val) == 0 {
|
||||
header["Content-Type"] = value
|
||||
}
|
||||
}
|
||||
6
credential/access_token.go
Normal file
6
credential/access_token.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package credential
|
||||
|
||||
//AccessTokenHandle AccessToken 接口
|
||||
type AccessTokenHandle interface {
|
||||
GetAccessToken() (accessToken string, err error)
|
||||
}
|
||||
99
credential/default_access_token.go
Normal file
99
credential/default_access_token.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package credential
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/v2/cache"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
//AccessTokenURL 获取access_token的接口
|
||||
accessTokenURL = "https://api.weixin.qq.com/cgi-bin/token"
|
||||
//CacheKeyOfficialAccountPrefix 微信公众号cache key前缀
|
||||
CacheKeyOfficialAccountPrefix = "gowechat_officialaccount_"
|
||||
//CacheKeyMiniProgramPrefix 小程序cache key前缀
|
||||
CacheKeyMiniProgramPrefix = "gowechat_miniprogram_"
|
||||
)
|
||||
|
||||
//DefaultAccessToken 默认AccessToken 获取
|
||||
type DefaultAccessToken struct {
|
||||
appID string
|
||||
appSecret string
|
||||
cacheKeyPrefix string
|
||||
cache cache.Cache
|
||||
accessTokenLock *sync.Mutex
|
||||
}
|
||||
|
||||
//NewDefaultAccessToken new DefaultAccessToken
|
||||
func NewDefaultAccessToken(appID, appSecret, cacheKeyPrefix string, cache cache.Cache) AccessTokenHandle {
|
||||
if cache == nil {
|
||||
panic("cache is ineed")
|
||||
}
|
||||
return &DefaultAccessToken{
|
||||
appID: appID,
|
||||
appSecret: appSecret,
|
||||
cache: cache,
|
||||
cacheKeyPrefix: cacheKeyPrefix,
|
||||
accessTokenLock: new(sync.Mutex),
|
||||
}
|
||||
}
|
||||
|
||||
//ResAccessToken struct
|
||||
type ResAccessToken struct {
|
||||
util.CommonError
|
||||
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
//GetAccessToken 获取access_token,先从cache中获取,没有则从服务端获取
|
||||
func (ak *DefaultAccessToken) GetAccessToken() (accessToken string, err error) {
|
||||
//加上lock,是为了防止在并发获取token时,cache刚好失效,导致从微信服务器上获取到不同token
|
||||
ak.accessTokenLock.Lock()
|
||||
defer ak.accessTokenLock.Unlock()
|
||||
|
||||
accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", ak.cacheKeyPrefix, ak.appID)
|
||||
val := ak.cache.Get(accessTokenCacheKey)
|
||||
if val != nil {
|
||||
accessToken = val.(string)
|
||||
return
|
||||
}
|
||||
|
||||
//cache失效,从微信服务器获取
|
||||
var resAccessToken ResAccessToken
|
||||
resAccessToken, err = GetTokenFromServer(ak.appID, ak.appSecret)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
expires := resAccessToken.ExpiresIn - 1500
|
||||
err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
accessToken = resAccessToken.AccessToken
|
||||
return
|
||||
}
|
||||
|
||||
//GetTokenFromServer 强制从微信服务器获取token
|
||||
func GetTokenFromServer(appID, appSecret string) (resAccessToken ResAccessToken, err error) {
|
||||
url := fmt.Sprintf("%s?grant_type=client_credential&appid=%s&secret=%s", accessTokenURL, appID, appSecret)
|
||||
var body []byte
|
||||
body, err = util.HTTPGet(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(body, &resAccessToken)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if resAccessToken.ErrMsg != "" {
|
||||
err = fmt.Errorf("get access_token error : errcode=%v , errormsg=%v", resAccessToken.ErrCode, resAccessToken.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
18
credential/default_access_token_test.go
Normal file
18
credential/default_access_token_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package credential
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
func TestGetTicketFromServer(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.New(getTicketURL).Reply(200).JSON(&ResTicket{Ticket: "mock-ticket", ExpiresIn: 10})
|
||||
ticket, err := GetTicketFromServer("arg-ak")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(0), ticket.ErrCode)
|
||||
assert.Equal(t, "mock-ticket", ticket.Ticket, "they should be equal")
|
||||
assert.Equal(t, int64(10), ticket.ExpiresIn, "they should be equal")
|
||||
}
|
||||
80
credential/default_js_ticket.go
Normal file
80
credential/default_js_ticket.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package credential
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/v2/cache"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
//获取ticket的url
|
||||
const getTicketURL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi"
|
||||
|
||||
//DefaultJsTicket 默认获取js ticket方法
|
||||
type DefaultJsTicket struct {
|
||||
appID string
|
||||
cacheKeyPrefix string
|
||||
cache cache.Cache
|
||||
//jsAPITicket 读写锁 同一个AppID一个
|
||||
jsAPITicketLock *sync.Mutex
|
||||
}
|
||||
|
||||
//NewDefaultJsTicket new
|
||||
func NewDefaultJsTicket(appID string, cacheKeyPrefix string, cache cache.Cache) JsTicketHandle {
|
||||
return &DefaultJsTicket{
|
||||
appID: appID,
|
||||
cache: cache,
|
||||
cacheKeyPrefix: cacheKeyPrefix,
|
||||
jsAPITicketLock: new(sync.Mutex),
|
||||
}
|
||||
}
|
||||
|
||||
// ResTicket 请求jsapi_tikcet返回结果
|
||||
type ResTicket struct {
|
||||
util.CommonError
|
||||
|
||||
Ticket string `json:"ticket"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
//GetTicket 获取jsapi_ticket
|
||||
func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err error) {
|
||||
js.jsAPITicketLock.Lock()
|
||||
defer js.jsAPITicketLock.Unlock()
|
||||
|
||||
//先从cache中取
|
||||
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", js.cacheKeyPrefix, js.appID)
|
||||
val := js.cache.Get(jsAPITicketCacheKey)
|
||||
if val != nil {
|
||||
ticketStr = val.(string)
|
||||
return
|
||||
}
|
||||
var ticket ResTicket
|
||||
ticket, err = GetTicketFromServer(accessToken)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
expires := ticket.ExpiresIn - 1500
|
||||
err = js.cache.Set(jsAPITicketCacheKey, ticket.Ticket, time.Duration(expires)*time.Second)
|
||||
ticketStr = ticket.Ticket
|
||||
return
|
||||
}
|
||||
|
||||
//GetTicketFromServer 从服务器中获取ticket
|
||||
func GetTicketFromServer(accessToken string) (ticket ResTicket, err error) {
|
||||
var response []byte
|
||||
url := fmt.Sprintf(getTicketURL, accessToken)
|
||||
response, err = util.HTTPGet(url)
|
||||
err = json.Unmarshal(response, &ticket)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if ticket.ErrCode != 0 {
|
||||
err = fmt.Errorf("getTicket Error : errcode=%d , errmsg=%s", ticket.ErrCode, ticket.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
7
credential/js_ticket.go
Normal file
7
credential/js_ticket.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package credential
|
||||
|
||||
//JsTicketHandle js ticket获取
|
||||
type JsTicketHandle interface {
|
||||
//GetTicket 获取ticket
|
||||
GetTicket(accessToken string) (ticket string, err error)
|
||||
}
|
||||
23
doc.go
23
doc.go
@@ -3,29 +3,6 @@ Package wechat provide wechat sdk for go
|
||||
|
||||
使用Golang开发的微信SDK,简单、易用。
|
||||
|
||||
以下是一个处理消息接收以及回复的例子:
|
||||
|
||||
//配置微信参数
|
||||
config := &wechat.Config{
|
||||
AppID: "xxxx",
|
||||
AppSecret: "xxxx",
|
||||
Token: "xxxx",
|
||||
EncodingAESKey: "xxxx",
|
||||
}
|
||||
wc := wechat.NewWechat(config)
|
||||
|
||||
// 传入request和responseWriter
|
||||
server := wc.GetServer(request, responseWriter)
|
||||
server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {
|
||||
|
||||
//回复消息:演示回复用户发送的消息
|
||||
text := message.NewText(msg.Content)
|
||||
return &message.Reply{message.MsgText, text}
|
||||
})
|
||||
|
||||
server.Serve()
|
||||
server.Send()
|
||||
|
||||
|
||||
更多信息:https://github.com/silenceper/wechat
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
"github.com/astaxie/beego/context"
|
||||
"github.com/silenceper/wechat"
|
||||
"github.com/silenceper/wechat/message"
|
||||
)
|
||||
|
||||
func hello(ctx *context.Context) {
|
||||
//配置微信参数
|
||||
config := &wechat.Config{
|
||||
AppID: "your app id",
|
||||
AppSecret: "your app secret",
|
||||
Token: "your token",
|
||||
EncodingAESKey: "your encoding aes key",
|
||||
}
|
||||
wc := wechat.NewWechat(config)
|
||||
|
||||
// 传入request和responseWriter
|
||||
server := wc.GetServer(ctx.Request, ctx.ResponseWriter)
|
||||
//设置接收消息的处理方法
|
||||
server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {
|
||||
|
||||
//回复消息:演示回复用户发送的消息
|
||||
text := message.NewText(msg.Content)
|
||||
return &message.Reply{MsgType: message.MsgTypeText, MsgData: text}
|
||||
})
|
||||
|
||||
//处理消息接收以及回复
|
||||
err := server.Serve()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
//发送回复的消息
|
||||
server.Send()
|
||||
}
|
||||
|
||||
func main() {
|
||||
beego.Any("/", hello)
|
||||
beego.Run(":8001")
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/silenceper/wechat"
|
||||
"github.com/silenceper/wechat/message"
|
||||
)
|
||||
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
|
||||
router.Any("/", hello)
|
||||
router.Run(":8001")
|
||||
}
|
||||
|
||||
func hello(c *gin.Context) {
|
||||
|
||||
//配置微信参数
|
||||
config := &wechat.Config{
|
||||
AppID: "your app id",
|
||||
AppSecret: "your app secret",
|
||||
Token: "your token",
|
||||
EncodingAESKey: "your encoding aes key",
|
||||
}
|
||||
wc := wechat.NewWechat(config)
|
||||
|
||||
// 传入request和responseWriter
|
||||
server := wc.GetServer(c.Request, c.Writer)
|
||||
//设置接收消息的处理方法
|
||||
server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {
|
||||
|
||||
//回复消息:演示回复用户发送的消息
|
||||
text := message.NewText(msg.Content)
|
||||
return &message.Reply{MsgType: message.MsgTypeText, MsgData: text}
|
||||
})
|
||||
|
||||
//处理消息接收以及回复
|
||||
err := server.Serve()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
//发送回复的消息
|
||||
server.Send()
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/silenceper/wechat"
|
||||
"github.com/silenceper/wechat/message"
|
||||
)
|
||||
|
||||
func hello(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
//配置微信参数
|
||||
config := &wechat.Config{
|
||||
AppID: "your app id",
|
||||
AppSecret: "your app secret",
|
||||
Token: "your token",
|
||||
EncodingAESKey: "your encoding aes key",
|
||||
}
|
||||
wc := wechat.NewWechat(config)
|
||||
|
||||
// 传入request和responseWriter
|
||||
server := wc.GetServer(req, rw)
|
||||
//设置接收消息的处理方法
|
||||
server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {
|
||||
|
||||
//回复消息:演示回复用户发送的消息
|
||||
text := message.NewText(msg.Content)
|
||||
return &message.Reply{MsgType: message.MsgTypeText, MsgData: text}
|
||||
})
|
||||
|
||||
//处理消息接收以及回复
|
||||
err := server.Serve()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
//发送回复的消息
|
||||
server.Send()
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", hello)
|
||||
err := http.ListenAndServe(":8001", nil)
|
||||
if err != nil {
|
||||
fmt.Printf("start server error , err=%v", err)
|
||||
}
|
||||
}
|
||||
14
go.mod
Normal file
14
go.mod
Normal file
@@ -0,0 +1,14 @@
|
||||
module github.com/silenceper/wechat/v2
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/gomodule/redigo v1.8.1
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
github.com/spf13/cast v1.3.1
|
||||
github.com/stretchr/testify v1.5.1
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
|
||||
gopkg.in/h2non/gock.v1 v1.0.15
|
||||
)
|
||||
43
go.sum
Normal file
43
go.sum
Normal file
@@ -0,0 +1,43 @@
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/gomodule/redigo v1.8.1 h1:Abmo0bI7Xf0IhdIPc7HZQzZcShdnmxeoVuDDtIQp8N8=
|
||||
github.com/gomodule/redigo v1.8.1/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0=
|
||||
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
109
js/js.go
109
js/js.go
@@ -1,109 +0,0 @@
|
||||
package js
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const getTicketURL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi"
|
||||
|
||||
// Js struct
|
||||
type Js struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// Config 返回给用户jssdk配置信息
|
||||
type Config struct {
|
||||
AppID string `json:"app_id"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
NonceStr string `json:"nonce_str"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
// resTicket 请求jsapi_tikcet返回结果
|
||||
type resTicket struct {
|
||||
util.CommonError
|
||||
|
||||
Ticket string `json:"ticket"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
//NewJs init
|
||||
func NewJs(context *context.Context) *Js {
|
||||
js := new(Js)
|
||||
js.Context = context
|
||||
return js
|
||||
}
|
||||
|
||||
//GetConfig 获取jssdk需要的配置参数
|
||||
//uri 为当前网页地址
|
||||
func (js *Js) GetConfig(uri string) (config *Config, err error) {
|
||||
config = new(Config)
|
||||
var ticketStr string
|
||||
ticketStr, err = js.GetTicket()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
nonceStr := util.RandomStr(16)
|
||||
timestamp := util.GetCurrTs()
|
||||
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
||||
sigStr := util.Signature(str)
|
||||
|
||||
config.AppID = js.AppID
|
||||
config.NonceStr = nonceStr
|
||||
config.Timestamp = timestamp
|
||||
config.Signature = sigStr
|
||||
return
|
||||
}
|
||||
|
||||
//GetTicket 获取jsapi_ticket
|
||||
func (js *Js) GetTicket() (ticketStr string, err error) {
|
||||
js.GetJsAPITicketLock().Lock()
|
||||
defer js.GetJsAPITicketLock().Unlock()
|
||||
|
||||
//先从cache中取
|
||||
jsAPITicketCacheKey := fmt.Sprintf("jsapi_ticket_%s", js.AppID)
|
||||
val := js.Cache.Get(jsAPITicketCacheKey)
|
||||
if val != nil {
|
||||
ticketStr = val.(string)
|
||||
return
|
||||
}
|
||||
var ticket resTicket
|
||||
ticket, err = js.getTicketFromServer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ticketStr = ticket.Ticket
|
||||
return
|
||||
}
|
||||
|
||||
//getTicketFromServer 强制从服务器中获取ticket
|
||||
func (js *Js) getTicketFromServer() (ticket resTicket, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = js.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
url := fmt.Sprintf(getTicketURL, accessToken)
|
||||
response, err = util.HTTPGet(url)
|
||||
err = json.Unmarshal(response, &ticket)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if ticket.ErrCode != 0 {
|
||||
err = fmt.Errorf("getTicket Error : errcode=%d , errmsg=%s", ticket.ErrCode, ticket.ErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
jsAPITicketCacheKey := fmt.Sprintf("jsapi_ticket_%s", js.AppID)
|
||||
expires := ticket.ExpiresIn - 1500
|
||||
err = js.Cache.Set(jsAPITicketCacheKey, ticket.Ticket, time.Duration(expires)*time.Second)
|
||||
return
|
||||
}
|
||||
8
minigame/README.md
Normal file
8
minigame/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# 微信小游戏
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/minigame/dev/api-backend/)
|
||||
|
||||
## 快速入门
|
||||
```go
|
||||
|
||||
```
|
||||
20
miniprogram/README.md
Normal file
20
miniprogram/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 微信小程序
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/miniprogram/dev/framework/)
|
||||
|
||||
|
||||
## 包说明
|
||||
- analysis 数据分析相关API
|
||||
|
||||
## 快速入门
|
||||
```go
|
||||
wc := wechat.NewWechat()
|
||||
memory := cache.NewMemory()
|
||||
cfg := &miniConfig.Config{
|
||||
AppID: "xxx",
|
||||
AppSecret: "xxx",
|
||||
Cache: memory,
|
||||
}
|
||||
miniprogram := wc.GetMiniProgram(cfg)
|
||||
miniprogram.GetAnalysis().GetAnalysisDailyRetain()
|
||||
```
|
||||
@@ -1,10 +1,12 @@
|
||||
package miniprogram
|
||||
package analysis
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -30,10 +32,20 @@ const (
|
||||
getAnalysisVisitPageURL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitpage?access_token=%s"
|
||||
)
|
||||
|
||||
//Analysis analyis 数据分析
|
||||
type Analysis struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
//NewAnalysis new
|
||||
func NewAnalysis(ctx *context.Context) *Analysis {
|
||||
return &Analysis{ctx}
|
||||
}
|
||||
|
||||
// fetchData 拉取统计数据
|
||||
func (wxa *MiniProgram) fetchData(urlStr string, body interface{}) (response []byte, err error) {
|
||||
func (analysis *Analysis) fetchData(urlStr string, body interface{}) (response []byte, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = wxa.GetAccessToken()
|
||||
accessToken, err = analysis.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -42,8 +54,8 @@ func (wxa *MiniProgram) fetchData(urlStr string, body interface{}) (response []b
|
||||
return
|
||||
}
|
||||
|
||||
// AnalysisRetainItem 留存项结构
|
||||
type AnalysisRetainItem struct {
|
||||
// RetainItem 留存项结构
|
||||
type RetainItem struct {
|
||||
Key int `json:"key"` // 标识,0开始表示当天,1表示1甜后,以此类推
|
||||
Value int `json:"value"` // key对应日期的新增用户数/活跃用户数(key=0时)或留存用户数(k>0时)
|
||||
}
|
||||
@@ -51,18 +63,18 @@ type AnalysisRetainItem struct {
|
||||
// ResAnalysisRetain 小程序留存数据返回
|
||||
type ResAnalysisRetain struct {
|
||||
util.CommonError
|
||||
RefDate string `json:"ref_date"` // 日期
|
||||
VisitUVNew []AnalysisRetainItem `json:"visit_uv_new"` // 新增用户留存
|
||||
VisitUV []AnalysisRetainItem `json:"visit_uv"` // 活跃用户留存
|
||||
RefDate string `json:"ref_date"` // 日期
|
||||
VisitUVNew []RetainItem `json:"visit_uv_new"` // 新增用户留存
|
||||
VisitUV []RetainItem `json:"visit_uv"` // 活跃用户留存
|
||||
}
|
||||
|
||||
// getAnalysisRetain 获取用户访问小程序留存数据(日、月、周)
|
||||
func (wxa *MiniProgram) getAnalysisRetain(urlStr string, beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||
func (analysis *Analysis) 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)
|
||||
response, err := analysis.fetchData(urlStr, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -78,18 +90,18 @@ func (wxa *MiniProgram) getAnalysisRetain(urlStr string, beginDate, endDate stri
|
||||
}
|
||||
|
||||
// GetAnalysisDailyRetain 获取用户访问小程序日留存
|
||||
func (wxa *MiniProgram) GetAnalysisDailyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||
return wxa.getAnalysisRetain(getAnalysisDailyRetainURL, beginDate, endDate)
|
||||
func (analysis *Analysis) GetAnalysisDailyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||
return analysis.getAnalysisRetain(getAnalysisDailyRetainURL, beginDate, endDate)
|
||||
}
|
||||
|
||||
// GetAnalysisMonthlyRetain 获取用户访问小程序月留存
|
||||
func (wxa *MiniProgram) GetAnalysisMonthlyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||
return wxa.getAnalysisRetain(getAnalysisMonthlyRetainURL, beginDate, endDate)
|
||||
func (analysis *Analysis) GetAnalysisMonthlyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||
return analysis.getAnalysisRetain(getAnalysisMonthlyRetainURL, beginDate, endDate)
|
||||
}
|
||||
|
||||
// GetAnalysisWeeklyRetain 获取用户访问小程序周留存
|
||||
func (wxa *MiniProgram) GetAnalysisWeeklyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||
return wxa.getAnalysisRetain(getAnalysisWeeklyRetainURL, beginDate, endDate)
|
||||
func (analysis *Analysis) GetAnalysisWeeklyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||
return analysis.getAnalysisRetain(getAnalysisWeeklyRetainURL, beginDate, endDate)
|
||||
}
|
||||
|
||||
// ResAnalysisDailySummary 小程序访问数据概况
|
||||
@@ -104,16 +116,16 @@ type ResAnalysisDailySummary struct {
|
||||
}
|
||||
|
||||
// GetAnalysisDailySummary 获取用户访问小程序数据概况
|
||||
func (wxa *MiniProgram) GetAnalysisDailySummary(beginDate, endDate string) (result ResAnalysisDailySummary, err error) {
|
||||
func (analysis *Analysis) GetAnalysisDailySummary(beginDate, endDate string) (result ResAnalysisDailySummary, err error) {
|
||||
body := map[string]string{
|
||||
"begin_date": beginDate,
|
||||
"end_date": endDate,
|
||||
}
|
||||
response, err := wxa.fetchData(getAnalysisDailySummaryURL, body)
|
||||
response, err := analysis.fetchData(getAnalysisDailySummaryURL, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(string(response))
|
||||
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -141,12 +153,12 @@ type ResAnalysisVisitTrend struct {
|
||||
}
|
||||
|
||||
// getAnalysisRetain 获取小程序访问数据趋势(日、月、周)
|
||||
func (wxa *MiniProgram) getAnalysisVisitTrend(urlStr string, beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||
func (analysis *Analysis) 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)
|
||||
response, err := analysis.fetchData(urlStr, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -162,18 +174,18 @@ func (wxa *MiniProgram) getAnalysisVisitTrend(urlStr string, beginDate, endDate
|
||||
}
|
||||
|
||||
// GetAnalysisDailyVisitTrend 获取用户访问小程序数据日趋势
|
||||
func (wxa *MiniProgram) GetAnalysisDailyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||
return wxa.getAnalysisVisitTrend(getAnalysisDailyVisitTrendURL, beginDate, endDate)
|
||||
func (analysis *Analysis) GetAnalysisDailyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||
return analysis.getAnalysisVisitTrend(getAnalysisDailyVisitTrendURL, beginDate, endDate)
|
||||
}
|
||||
|
||||
// GetAnalysisMonthlyVisitTrend 获取用户访问小程序数据月趋势
|
||||
func (wxa *MiniProgram) GetAnalysisMonthlyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||
return wxa.getAnalysisVisitTrend(getAnalysisMonthlyVisitTrendURL, beginDate, endDate)
|
||||
func (analysis *Analysis) GetAnalysisMonthlyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||
return analysis.getAnalysisVisitTrend(getAnalysisMonthlyVisitTrendURL, beginDate, endDate)
|
||||
}
|
||||
|
||||
// GetAnalysisWeeklyVisitTrend 获取用户访问小程序数据周趋势
|
||||
func (wxa *MiniProgram) GetAnalysisWeeklyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||
return wxa.getAnalysisVisitTrend(getAnalysisWeeklyVisitTrendURL, beginDate, endDate)
|
||||
func (analysis *Analysis) GetAnalysisWeeklyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||
return analysis.getAnalysisVisitTrend(getAnalysisWeeklyVisitTrendURL, beginDate, endDate)
|
||||
}
|
||||
|
||||
// UserPortraitItem 用户画像项目
|
||||
@@ -203,12 +215,12 @@ type ResAnalysisUserPortrait struct {
|
||||
}
|
||||
|
||||
// GetAnalysisUserPortrait 获取小程序新增或活跃用户的画像分布数据
|
||||
func (wxa *MiniProgram) GetAnalysisUserPortrait(beginDate, endDate string) (result ResAnalysisUserPortrait, err error) {
|
||||
func (analysis *Analysis) GetAnalysisUserPortrait(beginDate, endDate string) (result ResAnalysisUserPortrait, err error) {
|
||||
body := map[string]string{
|
||||
"begin_date": beginDate,
|
||||
"end_date": endDate,
|
||||
}
|
||||
response, err := wxa.fetchData(getAnalysisUserPortraitURL, body)
|
||||
response, err := analysis.fetchData(getAnalysisUserPortraitURL, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -244,12 +256,12 @@ type ResAnalysisVisitDistribution struct {
|
||||
}
|
||||
|
||||
// GetAnalysisVisitDistribution 获取用户小程序访问分布数据
|
||||
func (wxa *MiniProgram) GetAnalysisVisitDistribution(beginDate, endDate string) (result ResAnalysisVisitDistribution, err error) {
|
||||
func (analysis *Analysis) GetAnalysisVisitDistribution(beginDate, endDate string) (result ResAnalysisVisitDistribution, err error) {
|
||||
body := map[string]string{
|
||||
"begin_date": beginDate,
|
||||
"end_date": endDate,
|
||||
}
|
||||
response, err := wxa.fetchData(getAnalysisVisitDistributionURL, body)
|
||||
response, err := analysis.fetchData(getAnalysisVisitDistributionURL, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -284,12 +296,12 @@ type ResAnalysisVisitPage struct {
|
||||
}
|
||||
|
||||
// GetAnalysisVisitPage 获取小程序页面访问数据
|
||||
func (wxa *MiniProgram) GetAnalysisVisitPage(beginDate, endDate string) (result ResAnalysisVisitPage, err error) {
|
||||
func (analysis *Analysis) GetAnalysisVisitPage(beginDate, endDate string) (result ResAnalysisVisitPage, err error) {
|
||||
body := map[string]string{
|
||||
"begin_date": beginDate,
|
||||
"end_date": endDate,
|
||||
}
|
||||
response, err := wxa.fetchData(getAnalysisVisitPageURL, body)
|
||||
response, err := analysis.fetchData(getAnalysisVisitPageURL, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -1,16 +1,27 @@
|
||||
package miniprogram
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
code2SessionURL = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
|
||||
)
|
||||
|
||||
//Auth 登录/用户信息
|
||||
type Auth struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
//NewAuth new auth
|
||||
func NewAuth(ctx *context.Context) *Auth {
|
||||
return &Auth{ctx}
|
||||
}
|
||||
|
||||
// ResCode2Session 登录凭证校验的返回结果
|
||||
type ResCode2Session struct {
|
||||
util.CommonError
|
||||
@@ -20,9 +31,9 @@ type ResCode2Session struct {
|
||||
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)
|
||||
//Code2Session 登录凭证校验。
|
||||
func (auth *Auth) Code2Session(jsCode string) (result ResCode2Session, err error) {
|
||||
urlStr := fmt.Sprintf(code2SessionURL, auth.AppID, auth.AppSecret, jsCode)
|
||||
var response []byte
|
||||
response, err = util.HTTPGet(urlStr)
|
||||
if err != nil {
|
||||
@@ -38,3 +49,8 @@ func (wxa *MiniProgram) Code2Session(jsCode string) (result ResCode2Session, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//GetPaidUnionID 用户支付完成后,获取该用户的 UnionId,无需用户授权
|
||||
func (auth *Auth) GetPaidUnionID() {
|
||||
//TODO
|
||||
}
|
||||
12
miniprogram/config/config.go
Normal file
12
miniprogram/config/config.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/v2/cache"
|
||||
)
|
||||
|
||||
//Config config for 小程序
|
||||
type Config struct {
|
||||
AppID string `json:"app_id"` //appid
|
||||
AppSecret string `json:"app_secret"` //appsecret
|
||||
Cache cache.Cache
|
||||
}
|
||||
12
miniprogram/context/context.go
Normal file
12
miniprogram/context/context.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/v2/credential"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/config"
|
||||
)
|
||||
|
||||
// Context struct
|
||||
type Context struct {
|
||||
*config.Config
|
||||
credential.AccessTokenHandle
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package miniprogram
|
||||
package encryptor
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
@@ -6,8 +6,23 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
)
|
||||
|
||||
//Encryptor struct
|
||||
type Encryptor struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
//NewEncryptor 实例
|
||||
func NewEncryptor(context *context.Context) *Encryptor {
|
||||
basic := new(Encryptor)
|
||||
basic.Context = context
|
||||
return basic
|
||||
}
|
||||
|
||||
|
||||
var (
|
||||
// ErrAppIDNotMatch appid不匹配
|
||||
ErrAppIDNotMatch = errors.New("app id not match")
|
||||
@@ -19,8 +34,8 @@ var (
|
||||
ErrInvalidPKCS7Padding = errors.New("invalid padding on input")
|
||||
)
|
||||
|
||||
// UserInfo 用户信息
|
||||
type UserInfo struct {
|
||||
// PlainData 用户信息/手机号信息
|
||||
type PlainData struct {
|
||||
OpenID string `json:"openId"`
|
||||
UnionID string `json:"unionId"`
|
||||
NickName string `json:"nickName"`
|
||||
@@ -30,18 +45,10 @@ type UserInfo struct {
|
||||
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 {
|
||||
Watermark struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
AppID string `json:"appid"`
|
||||
} `json:"watermark"`
|
||||
@@ -96,35 +103,18 @@ func getCipherText(sessionKey, encryptedData, iv string) ([]byte, error) {
|
||||
}
|
||||
|
||||
// Decrypt 解密数据
|
||||
func (wxa *MiniProgram) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo, error) {
|
||||
func (encryptor *Encryptor) Decrypt(sessionKey, encryptedData, iv string) (*PlainData, error) {
|
||||
cipherText, err := getCipherText(sessionKey, encryptedData, iv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var userInfo UserInfo
|
||||
err = json.Unmarshal(cipherText, &userInfo)
|
||||
var plainData PlainData
|
||||
err = json.Unmarshal(cipherText, &plainData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if userInfo.Watermark.AppID != wxa.AppID {
|
||||
if plainData.Watermark.AppID != encryptor.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
|
||||
return &plainData, nil
|
||||
}
|
||||
@@ -1,17 +1,68 @@
|
||||
package miniprogram
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/v2/credential"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/analysis"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/auth"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/config"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/encryptor"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/qrcode"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/subscribe"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/tcb"
|
||||
)
|
||||
|
||||
// MiniProgram struct extends context
|
||||
//MiniProgram 微信小程序相关API
|
||||
type MiniProgram struct {
|
||||
*context.Context
|
||||
ctx *context.Context
|
||||
}
|
||||
|
||||
// NewMiniProgram 实例化小程序接口
|
||||
func NewMiniProgram(context *context.Context) *MiniProgram {
|
||||
miniProgram := new(MiniProgram)
|
||||
miniProgram.Context = context
|
||||
return miniProgram
|
||||
//NewMiniProgram 实例化小程序API
|
||||
func NewMiniProgram(cfg *config.Config) *MiniProgram {
|
||||
defaultAkHandle := credential.NewDefaultAccessToken(cfg.AppID, cfg.AppSecret, credential.CacheKeyMiniProgramPrefix, cfg.Cache)
|
||||
ctx := &context.Context{
|
||||
Config: cfg,
|
||||
AccessTokenHandle: defaultAkHandle,
|
||||
}
|
||||
return &MiniProgram{ctx}
|
||||
}
|
||||
|
||||
//SetAccessTokenHandle 自定义access_token获取方式
|
||||
func (miniProgram *MiniProgram) SetAccessTokenHandle(accessTokenHandle credential.AccessTokenHandle) {
|
||||
miniProgram.ctx.AccessTokenHandle = accessTokenHandle
|
||||
}
|
||||
|
||||
// GetContext get Context
|
||||
func (miniProgram *MiniProgram) GetContext() *context.Context {
|
||||
return miniProgram.ctx
|
||||
}
|
||||
|
||||
// GetEncryptor 小程序加解密
|
||||
func (miniProgram *MiniProgram) GetEncryptor() *encryptor.Encryptor {
|
||||
return encryptor.NewEncryptor(miniProgram.ctx)
|
||||
}
|
||||
|
||||
//GetAuth 登录/用户信息相关接口
|
||||
func (miniProgram *MiniProgram) GetAuth() *auth.Auth {
|
||||
return auth.NewAuth(miniProgram.ctx)
|
||||
}
|
||||
|
||||
//GetAnalysis 数据分析
|
||||
func (miniProgram *MiniProgram) GetAnalysis() *analysis.Analysis {
|
||||
return analysis.NewAnalysis(miniProgram.ctx)
|
||||
}
|
||||
|
||||
//GetQRCode 小程序码相关API
|
||||
func (miniProgram *MiniProgram) GetQRCode() *qrcode.QRCode {
|
||||
return qrcode.NewQRCode(miniProgram.ctx)
|
||||
}
|
||||
|
||||
//GetTcb 小程序云开发API
|
||||
func (miniProgram *MiniProgram) GetTcb() *tcb.Tcb {
|
||||
return tcb.NewTcb(miniProgram.ctx)
|
||||
}
|
||||
|
||||
//GetSubscribe 小程序订阅消息
|
||||
func (miniProgram *MiniProgram) GetSubscribe() *subscribe.Subscribe {
|
||||
return subscribe.NewSubscribe(miniProgram.ctx)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package miniprogram
|
||||
package qrcode
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -14,6 +15,25 @@ const (
|
||||
getWXACodeUnlimitURL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%s"
|
||||
)
|
||||
|
||||
//QRCode struct
|
||||
type QRCode struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
//NewQRCode 实例
|
||||
func NewQRCode(context *context.Context) *QRCode {
|
||||
qrCode := new(QRCode)
|
||||
qrCode.Context = context
|
||||
return qrCode
|
||||
}
|
||||
|
||||
// Color QRCode color
|
||||
type Color struct {
|
||||
R string `json:"r"`
|
||||
G string `json:"g"`
|
||||
B string `json:"b"`
|
||||
}
|
||||
|
||||
// QRCoder 小程序码参数
|
||||
type QRCoder struct {
|
||||
// page 必须是已经发布的小程序存在的页面,根路径前不要填加 /,不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面
|
||||
@@ -32,17 +52,10 @@ type QRCoder struct {
|
||||
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) {
|
||||
func (qrCode *QRCode) fetchCode(urlStr string, body interface{}) (response []byte, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = wxa.GetAccessToken()
|
||||
accessToken, err = qrCode.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -74,18 +87,18 @@ func (wxa *MiniProgram) fetchCode(urlStr string, body interface{}) (response []b
|
||||
|
||||
// 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)
|
||||
func (qrCode *QRCode) CreateWXAQRCode(coderParams QRCoder) (response []byte, err error) {
|
||||
return qrCode.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)
|
||||
func (qrCode *QRCode) GetWXACode(coderParams QRCoder) (response []byte, err error) {
|
||||
return qrCode.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)
|
||||
func (qrCode *QRCode) GetWXACodeUnlimit(coderParams QRCoder) (response []byte, err error) {
|
||||
return qrCode.fetchCode(getWXACodeUnlimitURL, coderParams)
|
||||
}
|
||||
52
miniprogram/subscribe/subscribe.go
Normal file
52
miniprogram/subscribe/subscribe.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package subscribe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
//发送订阅消息
|
||||
//https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html
|
||||
subscribeSendURL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send"
|
||||
)
|
||||
|
||||
// Subscribe 订阅消息
|
||||
type Subscribe struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewSubscribe 实例化
|
||||
func NewSubscribe(ctx *context.Context) *Subscribe {
|
||||
return &Subscribe{Context: ctx}
|
||||
}
|
||||
|
||||
// Message 订阅消息请求参数
|
||||
type Message struct {
|
||||
ToUser string `json:"touser"` //必选,接收者(用户)的 openid
|
||||
TemplateID string `json:"template_id"` //必选,所需下发的订阅模板id
|
||||
Page string `json:"page"` //可选,点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转。
|
||||
Data map[string]*DataItem `json:"data"` //必选, 模板内容
|
||||
MiniprogramState string `json:"miniprogram_state"` //可选,跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版
|
||||
Lang string `json:"lang"` //入小程序查看”的语言类型,支持zh_CN(简体中文)、en_US(英文)、zh_HK(繁体中文)、zh_TW(繁体中文),默认为zh_CN
|
||||
}
|
||||
|
||||
//DataItem 模版内某个 .DATA 的值
|
||||
type DataItem struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// Send 发送订阅消息
|
||||
func (s *Subscribe) Send(msg *Message) (err error) {
|
||||
var accessToken string
|
||||
accessToken, err = s.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", subscribeSendURL, accessToken)
|
||||
response, err := util.PostJSON(uri, msg)
|
||||
|
||||
return util.DecodeWithCommonError(response, "Send")
|
||||
}
|
||||
32
miniprogram/tcb/README.md
Normal file
32
miniprogram/tcb/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 小程序-云开发 SDK
|
||||
|
||||
Tencent Cloud Base [文档](https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/)
|
||||
|
||||
## 使用说明
|
||||
|
||||
**初始化配置**
|
||||
|
||||
```golang
|
||||
//使用memcache保存access_token,也可选择redis或自定义cache
|
||||
memCache=cache.NewMemcache("127.0.0.1:11211")
|
||||
|
||||
//配置小程序参数
|
||||
config := &wechat.Config{
|
||||
AppID: "your app id",
|
||||
AppSecret: "your app secret",
|
||||
Cache: memCache,
|
||||
}
|
||||
wc := wechat.NewWechat(config)
|
||||
wcTcb := wc.GetTcb()
|
||||
```
|
||||
|
||||
### 举例
|
||||
#### 触发云函数
|
||||
```golang
|
||||
res, err := wcTcb.InvokeCloudFunction("test-xxxx", "add", `{"a":1,"b":2}`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
更多使用方法参考[PKG.DEV](https://pkg.go.dev/github.com/silenceper/wechat/v2/miniprogram/tcb)
|
||||
35
miniprogram/tcb/cloudfunction.go
Normal file
35
miniprogram/tcb/cloudfunction.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package tcb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
//触发云函数
|
||||
invokeCloudFunctionURL = "https://api.weixin.qq.com/tcb/invokecloudfunction"
|
||||
)
|
||||
|
||||
//InvokeCloudFunctionRes 云函数调用返回结果
|
||||
type InvokeCloudFunctionRes struct {
|
||||
util.CommonError
|
||||
RespData string `json:"resp_data"` //云函数返回的buffer
|
||||
}
|
||||
|
||||
//InvokeCloudFunction 云函数调用
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/functions/invokeCloudFunction.html
|
||||
func (tcb *Tcb) InvokeCloudFunction(env, name, args string) (*InvokeCloudFunctionRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s&env=%s&name=%s", invokeCloudFunctionURL, accessToken, env, name)
|
||||
response, err := util.HTTPPost(uri, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invokeCloudFunctionRes := &InvokeCloudFunctionRes{}
|
||||
err = util.DecodeWithError(response, invokeCloudFunctionRes, "InvokeCloudFunction")
|
||||
return invokeCloudFunctionRes, err
|
||||
}
|
||||
418
miniprogram/tcb/database.go
Normal file
418
miniprogram/tcb/database.go
Normal file
@@ -0,0 +1,418 @@
|
||||
package tcb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
//数据库导入
|
||||
databaseMigrateImportURL = "https://api.weixin.qq.com/tcb/databasemigrateimport"
|
||||
//数据库导出
|
||||
databaseMigrateExportURL = "https://api.weixin.qq.com/tcb/databasemigrateexport"
|
||||
//数据库迁移状态查询
|
||||
databaseMigrateQueryInfoURL = "https://api.weixin.qq.com/tcb/databasemigratequeryinfo"
|
||||
//变更数据库索引
|
||||
updateIndexURL = "https://api.weixin.qq.com/tcb/updateindex"
|
||||
//新增集合
|
||||
databaseCollectionAddURL = "https://api.weixin.qq.com/tcb/databasecollectionadd"
|
||||
//删除集合
|
||||
databaseCollectionDeleteURL = "https://api.weixin.qq.com/tcb/databasecollectiondelete"
|
||||
//获取特定云环境下集合信息
|
||||
databaseCollectionGetURL = "https://api.weixin.qq.com/tcb/databasecollectionget"
|
||||
//数据库插入记录
|
||||
databaseAddURL = "https://api.weixin.qq.com/tcb/databaseadd"
|
||||
//数据库删除记录
|
||||
databaseDeleteURL = "https://api.weixin.qq.com/tcb/databasedelete"
|
||||
//数据库更新记录
|
||||
databaseUpdateURL = "https://api.weixin.qq.com/tcb/databaseupdate"
|
||||
//数据库查询记录
|
||||
databaseQueryURL = "https://api.weixin.qq.com/tcb/databasequery"
|
||||
//统计集合记录数或统计查询语句对应的结果记录数
|
||||
databaseCountURL = "https://api.weixin.qq.com/tcb/databasecount"
|
||||
|
||||
//ConflictModeInster 冲突处理模式 插入
|
||||
ConflictModeInster ConflictMode = 1
|
||||
//ConflictModeUpsert 冲突处理模式 更新
|
||||
ConflictModeUpsert ConflictMode = 2
|
||||
|
||||
//FileTypeJSON 的合法值 json
|
||||
FileTypeJSON FileType = 1
|
||||
//FileTypeCsv 的合法值 csv
|
||||
FileTypeCsv FileType = 2
|
||||
)
|
||||
|
||||
//ConflictMode 冲突处理模式
|
||||
type ConflictMode int
|
||||
|
||||
//FileType 文件上传和导出的允许文件类型
|
||||
type FileType int
|
||||
|
||||
//ValidDirections 合法的direction值
|
||||
var ValidDirections = []string{"1", "-1", "2dsphere"}
|
||||
|
||||
//DatabaseMigrateExportReq 数据库出 请求参数
|
||||
type DatabaseMigrateExportReq struct {
|
||||
Env string `json:"env,omitempty"` //云环境ID
|
||||
FilePath string `json:"file_path,omitempty"` //导出文件路径(导入文件需先上传到同环境的存储中,可使用开发者工具或 HTTP API的上传文件 API上传)
|
||||
FileType FileType `json:"file_type,omitempty"` //导出文件类型,文件格式参考数据库导入指引中的文件格式部分 1:json 2:csv
|
||||
Query string `json:"query,omitempty"` //导出条件
|
||||
}
|
||||
|
||||
//DatabaseMigrateExportRes 数据库导出 返回结果
|
||||
type DatabaseMigrateExportRes struct {
|
||||
util.CommonError
|
||||
JobID int64 `json:"job_id"` //导出任务ID,可使用数据库迁移进度查询 API 查询导入进度及结果
|
||||
}
|
||||
|
||||
//DatabaseMigrateImportReq 数据库导入 请求参数
|
||||
type DatabaseMigrateImportReq struct {
|
||||
Env string `json:"env,omitempty"` //云环境ID
|
||||
CollectionName string `json:"collection_name,omitempty"` //集合名称
|
||||
FilePath string `json:"file_path,omitempty"` //导出文件路径(文件会导出到同环境的云存储中,可使用获取下载链接 API 获取下载链接)
|
||||
FileType FileType `json:"file_type,omitempty"` //导入文件类型,文件格式参考数据库导入指引中的文件格式部分 1:json 2:csv
|
||||
StopOnError bool `json:"stop_on_error,omitempty"` //是否在遇到错误时停止导入
|
||||
ConflictMode ConflictMode `json:"conflict_mode,omitempty"` //冲突处理模式 1:inster 2:UPSERT
|
||||
}
|
||||
|
||||
//DatabaseMigrateImportRes 数据库导入 返回结果
|
||||
type DatabaseMigrateImportRes struct {
|
||||
util.CommonError
|
||||
JobID int64 `json:"job_id"` //导入任务ID,可使用数据库迁移进度查询 API 查询导入进度及结果
|
||||
}
|
||||
|
||||
//DatabaseMigrateQueryInfoRes 数据库迁移状态查询
|
||||
type DatabaseMigrateQueryInfoRes struct {
|
||||
util.CommonError
|
||||
Status string `json:"status"` //导出状态
|
||||
RecordSuccess int64 `json:"record_success"` //导出成功记录数
|
||||
RecordFail int64 `json:"record_fail"` //导出失败记录数
|
||||
ErrMsg string `json:"err_msg"` //导出错误信息
|
||||
FileURL string `json:"file_url"` //导出文件下载地址
|
||||
}
|
||||
|
||||
//UpdateIndexReq 变更数据库索引 请求参数
|
||||
type UpdateIndexReq struct {
|
||||
Env string `json:"env,omitempty"` //云环境ID
|
||||
CollectionName string `json:"collection_name,omitempty"` //集合名称
|
||||
CreateIndexes []CreateIndex `json:"create_indexes,omitempty"` //新增索引
|
||||
DropIndexes []DropIndex `json:"drop_indexes,omitempty"` //删除索引
|
||||
}
|
||||
|
||||
//CreateIndex 新增索引
|
||||
type CreateIndex struct {
|
||||
Name string `json:"name,omitempty"` //索引名
|
||||
Unique bool `json:"unique,omitempty"` //是否唯一
|
||||
Keys []CreateIndexKey `json:"keys,omitempty"` //索引字段
|
||||
}
|
||||
|
||||
//CreateIndexKey create index key
|
||||
type CreateIndexKey struct {
|
||||
Name string `json:"name,omitempty"` //字段名
|
||||
Direction string `json:"direction,omitempty"` //字段排序
|
||||
}
|
||||
|
||||
//DropIndex 删除索引
|
||||
type DropIndex struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
//DatabaseCollectionReq 新增/删除集合请求参数
|
||||
type DatabaseCollectionReq struct {
|
||||
Env string `json:"env,omitempty"` //云环境ID
|
||||
CollectionName string `json:"collection_name,omitempty"` //集合名称
|
||||
}
|
||||
|
||||
//DatabaseCollectionGetReq 获取特定云环境下集合信息请求
|
||||
type DatabaseCollectionGetReq struct {
|
||||
Env string `json:"env,omitempty"` //云环境ID
|
||||
Limit int64 `json:"limit,omitempty"` //获取数量限制
|
||||
Offset int64 `json:"offset,omitempty"` //偏移量
|
||||
}
|
||||
|
||||
//DatabaseCollectionGetRes 获取特定云环境下集合信息结果
|
||||
type DatabaseCollectionGetRes struct {
|
||||
util.CommonError
|
||||
Pager struct {
|
||||
Limit int64 `json:"limit"` //单次查询限制
|
||||
Offset int64 `json:"offset"` //偏移量
|
||||
Total int64 `json:"total"` //符合查询条件的记录总数
|
||||
} `json:"pager"`
|
||||
Collections []struct {
|
||||
Name string `json:"name"` //集合名
|
||||
Count int64 `json:"count"` //表中文档数量
|
||||
Size int64 `json:"size"` //表的大小(即表中文档总大小),单位:字节
|
||||
IndexCount int64 `json:"index_count"` //索引数量
|
||||
IndexSize int64 `json:"index_size"` //索引占用大小,单位:字节
|
||||
} `json:"collections"`
|
||||
}
|
||||
|
||||
//DatabaseReq 数据库插入/删除/更新/查询/统计记录请求参数
|
||||
type DatabaseReq struct {
|
||||
Env string `json:"env,omitempty"` //云环境ID
|
||||
Query string `json:"query,omitempty"` //数据库操作语句
|
||||
}
|
||||
|
||||
//DatabaseAddRes 数据库插入记录返回结果
|
||||
type DatabaseAddRes struct {
|
||||
util.CommonError
|
||||
IDList []string `json:"id_list"` //插入成功的数据集合主键_id。
|
||||
}
|
||||
|
||||
//DatabaseDeleteRes 数据库删除记录返回结果
|
||||
type DatabaseDeleteRes struct {
|
||||
util.CommonError
|
||||
Deleted int64 `json:"deleted"` //删除记录数量
|
||||
}
|
||||
|
||||
//DatabaseUpdateRes 数据库更新记录返回结果
|
||||
type DatabaseUpdateRes struct {
|
||||
util.CommonError
|
||||
Matched int64 `json:"matched"` //更新条件匹配到的结果数
|
||||
Modified int64 `json:"modified"` //修改的记录数,注意:使用set操作新插入的数据不计入修改数目
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
//DatabaseQueryRes 数据库查询记录 返回结果
|
||||
type DatabaseQueryRes struct {
|
||||
util.CommonError
|
||||
Pager struct {
|
||||
Limit int64 `json:"limit"` //单次查询限制
|
||||
Offset int64 `json:"offset"` //偏移量
|
||||
Total int64 `json:"total"` //符合查询条件的记录总数
|
||||
} `json:"pager"`
|
||||
Data []string `json:"data"`
|
||||
}
|
||||
|
||||
//DatabaseCountRes 统计集合记录数或统计查询语句对应的结果记录数 返回结果
|
||||
type DatabaseCountRes struct {
|
||||
util.CommonError
|
||||
Count int64 `json:"count"` //记录数量
|
||||
}
|
||||
|
||||
//DatabaseMigrateImport 数据库导入
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseMigrateImport.html
|
||||
func (tcb *Tcb) DatabaseMigrateImport(req *DatabaseMigrateImportReq) (*DatabaseMigrateImportRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseMigrateImportURL, accessToken)
|
||||
response, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
databaseMigrateImportRes := &DatabaseMigrateImportRes{}
|
||||
err = util.DecodeWithError(response, databaseMigrateImportRes, "DatabaseMigrateImport")
|
||||
return databaseMigrateImportRes, err
|
||||
}
|
||||
|
||||
//DatabaseMigrateExport 数据库导出
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseMigrateExport.html
|
||||
func (tcb *Tcb) DatabaseMigrateExport(req *DatabaseMigrateExportReq) (*DatabaseMigrateExportRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseMigrateExportURL, accessToken)
|
||||
response, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
databaseMigrateExportRes := &DatabaseMigrateExportRes{}
|
||||
err = util.DecodeWithError(response, databaseMigrateExportRes, "DatabaseMigrateExport")
|
||||
return databaseMigrateExportRes, err
|
||||
}
|
||||
|
||||
//DatabaseMigrateQueryInfo 数据库迁移状态查询
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseMigrateQueryInfo.html
|
||||
func (tcb *Tcb) DatabaseMigrateQueryInfo(env string, jobID int64) (*DatabaseMigrateQueryInfoRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseMigrateQueryInfoURL, accessToken)
|
||||
response, err := util.PostJSON(uri, map[string]interface{}{
|
||||
"env": env,
|
||||
"job_id": jobID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
databaseMigrateQueryInfoRes := &DatabaseMigrateQueryInfoRes{}
|
||||
err = util.DecodeWithError(response, databaseMigrateQueryInfoRes, "DatabaseMigrateQueryInfo")
|
||||
return databaseMigrateQueryInfoRes, err
|
||||
}
|
||||
|
||||
//UpdateIndex 变更数据库索引
|
||||
//https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/updateIndex.html
|
||||
func (tcb *Tcb) UpdateIndex(req *UpdateIndexReq) error {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", updateIndexURL, accessToken)
|
||||
response, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "UpdateIndex")
|
||||
}
|
||||
|
||||
//DatabaseCollectionAdd 新增集合
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCollectionAdd.html
|
||||
func (tcb *Tcb) DatabaseCollectionAdd(env, collectionName string) error {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseCollectionAddURL, accessToken)
|
||||
response, err := util.PostJSON(uri, &DatabaseCollectionReq{
|
||||
Env: env,
|
||||
CollectionName: collectionName,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "DatabaseCollectionAdd")
|
||||
}
|
||||
|
||||
//DatabaseCollectionDelete 删除集合
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCollectionDelete.html
|
||||
func (tcb *Tcb) DatabaseCollectionDelete(env, collectionName string) error {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseCollectionDeleteURL, accessToken)
|
||||
response, err := util.PostJSON(uri, &DatabaseCollectionReq{
|
||||
Env: env,
|
||||
CollectionName: collectionName,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "DatabaseCollectionDelete")
|
||||
}
|
||||
|
||||
//DatabaseCollectionGet 获取特定云环境下集合信息
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCollectionGet.html
|
||||
func (tcb *Tcb) DatabaseCollectionGet(env string, limit, offset int64) (*DatabaseCollectionGetRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseCollectionGetURL, accessToken)
|
||||
response, err := util.PostJSON(uri, &DatabaseCollectionGetReq{
|
||||
Env: env,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
databaseCollectionGetRes := &DatabaseCollectionGetRes{}
|
||||
err = util.DecodeWithError(response, databaseCollectionGetRes, "DatabaseCollectionGet")
|
||||
return databaseCollectionGetRes, err
|
||||
}
|
||||
|
||||
//DatabaseAdd 数据库插入记录
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseAdd.html
|
||||
func (tcb *Tcb) DatabaseAdd(env, query string) (*DatabaseAddRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseAddURL, accessToken)
|
||||
response, err := util.PostJSON(uri, &DatabaseReq{
|
||||
Env: env,
|
||||
Query: query,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
databaseAddRes := &DatabaseAddRes{}
|
||||
err = util.DecodeWithError(response, databaseAddRes, "DatabaseAdd")
|
||||
return databaseAddRes, err
|
||||
}
|
||||
|
||||
//DatabaseDelete 数据库插入记录
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseDelete.html
|
||||
func (tcb *Tcb) DatabaseDelete(env, query string) (*DatabaseDeleteRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseDeleteURL, accessToken)
|
||||
response, err := util.PostJSON(uri, &DatabaseReq{
|
||||
Env: env,
|
||||
Query: query,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
databaseDeleteRes := &DatabaseDeleteRes{}
|
||||
err = util.DecodeWithError(response, databaseDeleteRes, "DatabaseDelete")
|
||||
return databaseDeleteRes, err
|
||||
}
|
||||
|
||||
//DatabaseUpdate 数据库插入记录
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseUpdate.html
|
||||
func (tcb *Tcb) DatabaseUpdate(env, query string) (*DatabaseUpdateRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseUpdateURL, accessToken)
|
||||
response, err := util.PostJSON(uri, &DatabaseReq{
|
||||
Env: env,
|
||||
Query: query,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
databaseUpdateRes := &DatabaseUpdateRes{}
|
||||
err = util.DecodeWithError(response, databaseUpdateRes, "DatabaseUpdate")
|
||||
return databaseUpdateRes, err
|
||||
}
|
||||
|
||||
//DatabaseQuery 数据库查询记录
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseQuery.html
|
||||
func (tcb *Tcb) DatabaseQuery(env, query string) (*DatabaseQueryRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseQueryURL, accessToken)
|
||||
response, err := util.PostJSON(uri, &DatabaseReq{
|
||||
Env: env,
|
||||
Query: query,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
databaseQueryRes := &DatabaseQueryRes{}
|
||||
err = util.DecodeWithError(response, databaseQueryRes, "DatabaseQuery")
|
||||
return databaseQueryRes, err
|
||||
}
|
||||
|
||||
//DatabaseCount 统计集合记录数或统计查询语句对应的结果记录数
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCount.html
|
||||
func (tcb *Tcb) DatabaseCount(env, query string) (*DatabaseCountRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseCountURL, accessToken)
|
||||
response, err := util.PostJSON(uri, &DatabaseReq{
|
||||
Env: env,
|
||||
Query: query,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
databaseCountRes := &DatabaseCountRes{}
|
||||
err = util.DecodeWithError(response, databaseCountRes, "DatabaseCount")
|
||||
return databaseCountRes, err
|
||||
}
|
||||
134
miniprogram/tcb/file.go
Normal file
134
miniprogram/tcb/file.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package tcb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
//获取文件上传链接
|
||||
uploadFilePathURL = "https://api.weixin.qq.com/tcb/uploadfile"
|
||||
//获取文件下载链接
|
||||
batchDownloadFileURL = "https://api.weixin.qq.com/tcb/batchdownloadfile"
|
||||
//删除文件链接
|
||||
batchDeleteFileURL = "https://api.weixin.qq.com/tcb/batchdeletefile"
|
||||
)
|
||||
|
||||
//UploadFileReq 上传文件请求值
|
||||
type UploadFileReq struct {
|
||||
Env string `json:"env,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
//UploadFileRes 上传文件返回结果
|
||||
type UploadFileRes struct {
|
||||
util.CommonError
|
||||
URL string `json:"url"` //上传url
|
||||
Token string `json:"token"` //token
|
||||
Authorization string `json:"authorization"` //authorization
|
||||
FileID string `json:"file_id"` //文件ID
|
||||
CosFileID string `json:"cos_file_id"` //cos文件ID
|
||||
}
|
||||
|
||||
//BatchDownloadFileReq 上传文件请求值
|
||||
type BatchDownloadFileReq struct {
|
||||
Env string `json:"env,omitempty"`
|
||||
FileList []*DownloadFile `json:"file_list,omitempty"`
|
||||
}
|
||||
|
||||
//DownloadFile 文件信息
|
||||
type DownloadFile struct {
|
||||
FileID string `json:"fileid"` //文件ID
|
||||
MaxAge int64 `json:"max_age"` //下载链接有效期
|
||||
}
|
||||
|
||||
//BatchDownloadFileRes 上传文件返回结果
|
||||
type BatchDownloadFileRes struct {
|
||||
util.CommonError
|
||||
FileList []struct {
|
||||
FileID string `json:"file_id"` //文件ID
|
||||
DownloadURL string `json:"download_url"` //下载链接
|
||||
Status int64 `json:"status"` //状态码
|
||||
ErrMsg string `json:"errmsg"` //该文件错误信息
|
||||
} `json:"file_list"`
|
||||
}
|
||||
|
||||
//BatchDeleteFileReq 批量删除文件请求参数
|
||||
type BatchDeleteFileReq struct {
|
||||
Env string `json:"env,omitempty"`
|
||||
FileIDList []string `json:"fileid_list,omitempty"`
|
||||
}
|
||||
|
||||
//BatchDeleteFileRes 批量删除文件返回结果
|
||||
type BatchDeleteFileRes struct {
|
||||
util.CommonError
|
||||
DeleteList []struct {
|
||||
FileID string `json:"fileid"`
|
||||
Status int64 `json:"status"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
} `json:"delete_list"`
|
||||
}
|
||||
|
||||
//UploadFile 上传文件
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/storage/uploadFile.html
|
||||
func (tcb *Tcb) UploadFile(env, path string) (*UploadFileRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", uploadFilePathURL, accessToken)
|
||||
req := &UploadFileReq{
|
||||
Env: env,
|
||||
Path: path,
|
||||
}
|
||||
response, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uploadFileRes := &UploadFileRes{}
|
||||
err = util.DecodeWithError(response, uploadFileRes, "UploadFile")
|
||||
return uploadFileRes, err
|
||||
}
|
||||
|
||||
//BatchDownloadFile 获取文件下载链接
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/storage/batchDownloadFile.html
|
||||
func (tcb *Tcb) BatchDownloadFile(env string, fileList []*DownloadFile) (*BatchDownloadFileRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", batchDownloadFileURL, accessToken)
|
||||
req := &BatchDownloadFileReq{
|
||||
Env: env,
|
||||
FileList: fileList,
|
||||
}
|
||||
response, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
batchDownloadFileRes := &BatchDownloadFileRes{}
|
||||
err = util.DecodeWithError(response, batchDownloadFileRes, "BatchDownloadFile")
|
||||
return batchDownloadFileRes, err
|
||||
}
|
||||
|
||||
//BatchDeleteFile 批量删除文件
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/storage/batchDeleteFile.html
|
||||
func (tcb *Tcb) BatchDeleteFile(env string, fileIDList []string) (*BatchDeleteFileRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", batchDeleteFileURL, accessToken)
|
||||
req := &BatchDeleteFileReq{
|
||||
Env: env,
|
||||
FileIDList: fileIDList,
|
||||
}
|
||||
response, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
batchDeleteFileRes := &BatchDeleteFileRes{}
|
||||
err = util.DecodeWithError(response, batchDeleteFileRes, "BatchDeleteFile")
|
||||
return batchDeleteFileRes, nil
|
||||
}
|
||||
15
miniprogram/tcb/tcb.go
Normal file
15
miniprogram/tcb/tcb.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package tcb
|
||||
|
||||
import "github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
|
||||
//Tcb Tencent Cloud Base
|
||||
type Tcb struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
//NewTcb new Tencent Cloud Base
|
||||
func NewTcb(context *context.Context) *Tcb {
|
||||
return &Tcb{
|
||||
context,
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
var (
|
||||
qyRedirectOauthURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&agentid=%s&state=%s#wechat_redirect"
|
||||
qyUserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=%s&code=%s"
|
||||
qyUserDetailURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserdetail"
|
||||
)
|
||||
|
||||
//GetQyRedirectURL 获取企业微信跳转的url地址
|
||||
func (oauth *Oauth) GetQyRedirectURL(redirectURI, agentid, scope, state string) (string, error) {
|
||||
//url encode
|
||||
urlStr := url.QueryEscape(redirectURI)
|
||||
return fmt.Sprintf(qyRedirectOauthURL, oauth.AppID, urlStr, scope, agentid, state), nil
|
||||
}
|
||||
|
||||
//QyUserInfo 用户授权获取到用户信息
|
||||
type QyUserInfo struct {
|
||||
util.CommonError
|
||||
|
||||
UserID string `json:"UserId"`
|
||||
DeviceID string `json:"DeviceId"`
|
||||
UserTicket string `json:"user_ticket"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
//GetQyUserInfoByCode 根据code获取企业user_info
|
||||
func (oauth *Oauth) GetQyUserInfoByCode(code string) (result QyUserInfo, err error) {
|
||||
qyAccessToken, e := oauth.GetQyAccessToken()
|
||||
if e != nil {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
urlStr := fmt.Sprintf(qyUserInfoURL, qyAccessToken, code)
|
||||
var response []byte
|
||||
response, err = util.HTTPGet(urlStr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("GetQyUserInfoByCode error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//QyUserDetail 到用户详情
|
||||
type QyUserDetail struct {
|
||||
util.CommonError
|
||||
|
||||
UserID string `json:"UserId"`
|
||||
Name string `json:"name"`
|
||||
Mobile string `json:"mobile"`
|
||||
Gender string `json:"gender"`
|
||||
Email string `json:"email"`
|
||||
Avatar string `json:"avatar"`
|
||||
QrCode string `json:"qr_code"`
|
||||
}
|
||||
|
||||
//GetQyUserDetailUserTicket 根据user_ticket获取到用户详情
|
||||
func (oauth *Oauth) GetQyUserDetailUserTicket(userTicket string) (result QyUserDetail, err error) {
|
||||
var qyAccessToken string
|
||||
qyAccessToken, err = oauth.GetQyAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", qyUserDetailURL, qyAccessToken)
|
||||
var response []byte
|
||||
response, err = util.PostJSON(uri, map[string]string{
|
||||
"user_ticket": userTicket,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("GetQyUserDetailUserTicket Error , errcode=%d , errmsg=%s", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
5
officialaccount/README.md
Normal file
5
officialaccount/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 微信公众号
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html)
|
||||
|
||||
## 快速入门
|
||||
84
officialaccount/basic/basic.go
Normal file
84
officialaccount/basic/basic.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
var (
|
||||
//获取微信服务器IP地址
|
||||
//文档:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_the_WeChat_server_IP_address.html
|
||||
getCallbackIPURL = "https://api.weixin.qq.com/cgi-bin/getcallbackip"
|
||||
getAPIDomainIPURL = "https://api.weixin.qq.com/cgi-bin/get_api_domain_ip"
|
||||
|
||||
//清理接口调用次数
|
||||
clearQuotaURL = "https://api.weixin.qq.com/cgi-bin/clear_quota"
|
||||
)
|
||||
|
||||
//Basic struct
|
||||
type Basic struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
//NewBasic 实例
|
||||
func NewBasic(context *context.Context) *Basic {
|
||||
basic := new(Basic)
|
||||
basic.Context = context
|
||||
return basic
|
||||
}
|
||||
|
||||
//IPListRes 获取微信服务器IP地址 返回结果
|
||||
type IPListRes struct {
|
||||
util.CommonError
|
||||
IPList []string `json:"ip_list"`
|
||||
}
|
||||
|
||||
//GetCallbackIP 获取微信callback IP地址
|
||||
func (basic *Basic) GetCallbackIP() ([]string, error) {
|
||||
ak, err := basic.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := fmt.Sprintf("%s?access_token=%s", getCallbackIPURL, ak)
|
||||
data, err := util.HTTPGet(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ipListRes := &IPListRes{}
|
||||
err = util.DecodeWithError(data, ipListRes, "GetCallbackIP")
|
||||
return ipListRes.IPList, err
|
||||
}
|
||||
|
||||
//GetAPIDomainIP 获取微信API接口 IP地址
|
||||
func (basic *Basic) GetAPIDomainIP() ([]string, error) {
|
||||
ak, err := basic.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := fmt.Sprintf("%s?access_token=%s", getAPIDomainIPURL, ak)
|
||||
data, err := util.HTTPGet(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ipListRes := &IPListRes{}
|
||||
err = util.DecodeWithError(data, ipListRes, "GetAPIDomainIP")
|
||||
return ipListRes.IPList, err
|
||||
}
|
||||
|
||||
//ClearQuota 清理接口调用次数
|
||||
func (basic *Basic) ClearQuota() error {
|
||||
ak, err := basic.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := fmt.Sprintf("%s?access_token=%s", clearQuotaURL, ak)
|
||||
data, err := util.PostJSON(url, map[string]string{
|
||||
"appid": basic.AppID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(data, "ClearQuota")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package qr
|
||||
package basic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -6,8 +6,7 @@ import (
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -23,18 +22,6 @@ const (
|
||||
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"`
|
||||
@@ -56,8 +43,8 @@ type Ticket struct {
|
||||
}
|
||||
|
||||
// GetQRTicket 获取二维码 Ticket
|
||||
func (q *QR) GetQRTicket(tq *Request) (t *Ticket, err error) {
|
||||
accessToken, err := q.GetAccessToken()
|
||||
func (basic *Basic) GetQRTicket(tq *Request) (t *Ticket, err error) {
|
||||
accessToken, err := basic.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
279
officialaccount/broadcast/broadcast.go
Normal file
279
officialaccount/broadcast/broadcast.go
Normal file
@@ -0,0 +1,279 @@
|
||||
package broadcast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
sendURLByTag = "https://api.weixin.qq.com/cgi-bin/message/mass/sendall"
|
||||
sendURLByOpenID = "https://api.weixin.qq.com/cgi-bin/message/mass/send"
|
||||
deleteSendURL ="https://api.weixin.qq.com/cgi-bin/message/mass/delete"
|
||||
)
|
||||
|
||||
//MsgType 发送消息类型
|
||||
type MsgType string
|
||||
|
||||
const (
|
||||
//MsgTypeNews 图文消息
|
||||
MsgTypeNews MsgType = "mpnews"
|
||||
//MsgTypeText 文本
|
||||
MsgTypeText MsgType = "text"
|
||||
//MsgTypeVoice 语音/音频
|
||||
MsgTypeVoice MsgType = "voice"
|
||||
//MsgTypeImage 图片
|
||||
MsgTypeImage MsgType = "image"
|
||||
//MsgTypeVideo 视频
|
||||
MsgTypeVideo MsgType = "mpvideo"
|
||||
//MsgTypeWxCard 卡券
|
||||
MsgTypeWxCard MsgType = "wxcard"
|
||||
)
|
||||
|
||||
//Broadcast 群发消息
|
||||
type Broadcast struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
//NewBroadcast new
|
||||
func NewBroadcast(ctx *context.Context) *Broadcast {
|
||||
return &Broadcast{ctx}
|
||||
}
|
||||
|
||||
//User 发送的用户
|
||||
type User struct {
|
||||
TagID int64
|
||||
OpenID []string
|
||||
}
|
||||
|
||||
//Result 群发返回结果
|
||||
type Result struct {
|
||||
util.CommonError
|
||||
MsgID int64 `json:"msg_id"`
|
||||
MsgDataID int64 `json:"msg_data_id"`
|
||||
}
|
||||
|
||||
//sendRequest 发送请求的数据
|
||||
type sendRequest struct {
|
||||
//根据tag获全部发送
|
||||
Filter map[string]interface{} `json:"filter,omitempty"`
|
||||
//根据OpenID发送
|
||||
ToUser interface{} `json:"touser,omitempty"`
|
||||
//发送文本
|
||||
Text map[string]interface{} `json:"text,omitempty"`
|
||||
//发送图文消息
|
||||
Mpnews map[string]interface{} `json:"mpnews,omitempty"`
|
||||
//发送语音
|
||||
Voice map[string]interface{} `json:"voice,omitempty"`
|
||||
//发送图片
|
||||
Images *Image `json:"images,omitempty"`
|
||||
//发送卡券
|
||||
WxCard map[string]interface{} `json:"wxcard,omitempty"`
|
||||
MsgType MsgType `json:"msgtype"`
|
||||
SendIgnoreReprint int32 `json:"send_ignore_reprint,omitempty"`
|
||||
}
|
||||
|
||||
//Image 发送图片
|
||||
type Image struct{
|
||||
MediaIDs []string `json:"media_ids"`
|
||||
Recommend string `json:"recommend"`
|
||||
NeedOpenComment int32 `json:"need_open_comment"`
|
||||
OnlyFansCanComment int32 `json:"only_fans_can_comment"`
|
||||
}
|
||||
|
||||
//SendText 群发文本
|
||||
//user 为nil,表示全员发送
|
||||
//&User{TagID:2} 根据tag发送
|
||||
//&User{OpenID:[]string("xxx","xxx")} 根据openid发送
|
||||
func (broadcast *Broadcast) SendText(user *User, content string) (*Result, error) {
|
||||
ak, err := broadcast.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := &sendRequest{
|
||||
ToUser: nil,
|
||||
MsgType: MsgTypeText,
|
||||
}
|
||||
req.Text=map[string]interface{}{
|
||||
"content":content,
|
||||
}
|
||||
req,sendURL:=broadcast.chooseTagOrOpenID(user,req)
|
||||
url := fmt.Sprintf("%s?access_token=%s", sendURL, ak)
|
||||
data, err := util.PostJSON(url, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := &Result{}
|
||||
err = util.DecodeWithError(data, res, "SendText")
|
||||
return res, err
|
||||
}
|
||||
|
||||
//SendNews 发送图文
|
||||
func (broadcast *Broadcast) SendNews(user *User, mediaID string,ignoreReprint bool) (*Result, error) {
|
||||
ak, err := broadcast.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := &sendRequest{
|
||||
ToUser: nil,
|
||||
MsgType: MsgTypeNews,
|
||||
}
|
||||
if ignoreReprint{
|
||||
req.SendIgnoreReprint=1
|
||||
}
|
||||
req.Mpnews=map[string]interface{}{
|
||||
"media_id":mediaID,
|
||||
}
|
||||
req,sendURL:=broadcast.chooseTagOrOpenID(user,req)
|
||||
url := fmt.Sprintf("%s?access_token=%s", sendURL, ak)
|
||||
data, err := util.PostJSON(url, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := &Result{}
|
||||
err = util.DecodeWithError(data, res, "SendNews")
|
||||
return res, err
|
||||
}
|
||||
|
||||
|
||||
//SendVoice 发送语音
|
||||
func (broadcast *Broadcast) SendVoice(user *User, mediaID string) (*Result, error) {
|
||||
ak, err := broadcast.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := &sendRequest{
|
||||
ToUser: nil,
|
||||
MsgType: MsgTypeVoice,
|
||||
}
|
||||
req.Voice=map[string]interface{}{
|
||||
"media_id":mediaID,
|
||||
}
|
||||
req,sendURL:=broadcast.chooseTagOrOpenID(user,req)
|
||||
url := fmt.Sprintf("%s?access_token=%s", sendURL, ak)
|
||||
data, err := util.PostJSON(url, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := &Result{}
|
||||
err = util.DecodeWithError(data, res, "SendVoice")
|
||||
return res, err
|
||||
}
|
||||
|
||||
//SendImage 发送图片
|
||||
func (broadcast *Broadcast) SendImage(user *User, images *Image) (*Result, error) {
|
||||
ak, err := broadcast.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := &sendRequest{
|
||||
ToUser: nil,
|
||||
MsgType: MsgTypeImage,
|
||||
}
|
||||
req.Images=images
|
||||
req,sendURL:=broadcast.chooseTagOrOpenID(user,req)
|
||||
url := fmt.Sprintf("%s?access_token=%s", sendURL, ak)
|
||||
data, err := util.PostJSON(url, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := &Result{}
|
||||
err = util.DecodeWithError(data, res, "SendImage")
|
||||
return res, err
|
||||
}
|
||||
|
||||
|
||||
//SendVideo 发送视频
|
||||
func (broadcast *Broadcast) SendVideo(user *User, mediaID string,title,description string) (*Result, error) {
|
||||
ak, err := broadcast.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := &sendRequest{
|
||||
ToUser: nil,
|
||||
MsgType: MsgTypeVideo,
|
||||
}
|
||||
req.Voice=map[string]interface{}{
|
||||
"media_id":mediaID,
|
||||
"title":title,
|
||||
"description":description,
|
||||
}
|
||||
req,sendURL:=broadcast.chooseTagOrOpenID(user,req)
|
||||
url := fmt.Sprintf("%s?access_token=%s", sendURL, ak)
|
||||
data, err := util.PostJSON(url, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := &Result{}
|
||||
err = util.DecodeWithError(data, res, "SendVideo")
|
||||
return res, err
|
||||
}
|
||||
|
||||
|
||||
//SendWxCard 发送卡券
|
||||
func (broadcast *Broadcast) SendWxCard(user *User, cardID string) (*Result, error) {
|
||||
ak, err := broadcast.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := &sendRequest{
|
||||
ToUser: nil,
|
||||
MsgType: MsgTypeWxCard,
|
||||
}
|
||||
req.WxCard=map[string]interface{}{
|
||||
"card_id":cardID,
|
||||
}
|
||||
req,sendURL:=broadcast.chooseTagOrOpenID(user,req)
|
||||
url := fmt.Sprintf("%s?access_token=%s", sendURL, ak)
|
||||
data, err := util.PostJSON(url, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := &Result{}
|
||||
err = util.DecodeWithError(data, res, "SendWxCard")
|
||||
return res, err
|
||||
}
|
||||
//Delete 删除群发消息
|
||||
func (broadcast *Broadcast) Delete(msgID int64 ,articleIDx int64) error {
|
||||
ak, err := broadcast.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := map[string]interface{}{
|
||||
"msg_id": msgID,
|
||||
"article_idx": articleIDx,
|
||||
}
|
||||
url := fmt.Sprintf("%s?access_token=%s", deleteSendURL, ak)
|
||||
data, err := util.PostJSON(url, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(data, "Delete")
|
||||
}
|
||||
|
||||
|
||||
//TODO 发送预览,群发消息状态,发送速度
|
||||
|
||||
func (broadcast *Broadcast) chooseTagOrOpenID(user *User,req *sendRequest)(ret *sendRequest,url string){
|
||||
sendURL:=""
|
||||
if user == nil {
|
||||
req.Filter=map[string]interface{}{
|
||||
"is_to_all":true,
|
||||
}
|
||||
sendURL=sendURLByTag
|
||||
} else {
|
||||
if user.TagID != 0 {
|
||||
req.Filter=map[string]interface{}{
|
||||
"is_to_all":false,
|
||||
"tag_id":user.TagID,
|
||||
}
|
||||
sendURL=sendURLByTag
|
||||
}
|
||||
if len(user.OpenID) != 0 {
|
||||
req.ToUser = user.OpenID
|
||||
sendURL=sendURLByOpenID
|
||||
}
|
||||
}
|
||||
return req,sendURL
|
||||
}
|
||||
14
officialaccount/config/config.go
Normal file
14
officialaccount/config/config.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/v2/cache"
|
||||
)
|
||||
|
||||
//Config config for 微信公众号
|
||||
type Config struct {
|
||||
AppID string `json:"app_id"` //appid
|
||||
AppSecret string `json:"app_secret"` //appsecret
|
||||
Token string `json:"token"` //token
|
||||
EncodingAESKey string `json:"encoding_aes_key"` //EncodingAESKey
|
||||
Cache cache.Cache
|
||||
}
|
||||
12
officialaccount/context/context.go
Normal file
12
officialaccount/context/context.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/v2/credential"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/config"
|
||||
)
|
||||
|
||||
// Context struct
|
||||
type Context struct {
|
||||
*config.Config
|
||||
credential.AccessTokenHandle
|
||||
}
|
||||
270
officialaccount/datacube/broadcast.go
Normal file
270
officialaccount/datacube/broadcast.go
Normal file
@@ -0,0 +1,270 @@
|
||||
package datacube
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
getArticleSummary = "https://api.weixin.qq.com/datacube/getarticlesummary"
|
||||
getArticleTotal = "https://api.weixin.qq.com/datacube/getarticletotal"
|
||||
getUserRead = "https://api.weixin.qq.com/datacube/getuserread"
|
||||
getUserReadHour = "https://api.weixin.qq.com/datacube/getuserreadhour"
|
||||
getUserShare = "https://api.weixin.qq.com/datacube/getusershare"
|
||||
getUserShareHour = "https://api.weixin.qq.com/datacube/getusersharehour"
|
||||
)
|
||||
|
||||
//ResArticleSummary 获取图文群发每日数据响应
|
||||
type ResArticleSummary struct {
|
||||
util.CommonError
|
||||
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"`
|
||||
MsgID string `json:"msgid"`
|
||||
Title string `json:"title"`
|
||||
IntPageReadUser int `json:"int_page_read_user"`
|
||||
IntPageReadCount int `json:"int_page_read_count"`
|
||||
OriPageReadUser int `json:"ori_page_read_user"`
|
||||
OriPageReadCount int `json:"ori_page_read_count"`
|
||||
ShareUser int `json:"share_user"`
|
||||
ShareCount int `json:"share_count"`
|
||||
AddToFavUser int `json:"add_to_fav_user"`
|
||||
AddToFavCount int `json:"add_to_fav_count"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
//ResArticleTotal 获取图文群发总数据响应
|
||||
type ResArticleTotal struct {
|
||||
util.CommonError
|
||||
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"`
|
||||
MsgID string `json:"msgid"`
|
||||
Title string `json:"title"`
|
||||
Details []ArticleTotalDetails `json:"details"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
//ArticleTotalDetails 获取图文群发总数据响应文字详情
|
||||
type ArticleTotalDetails struct {
|
||||
StatDate string `json:"stat_date"`
|
||||
TargetUser int `json:"target_user"`
|
||||
IntPageReadUser int `json:"int_page_read_user"`
|
||||
IntPageReadCount int `json:"int_page_read_count"`
|
||||
OriPageReadUser int `json:"ori_page_read_user"`
|
||||
OriPageReadCount int `json:"ori_page_read_count"`
|
||||
ShareUser int `json:"share_user"`
|
||||
ShareCount int `json:"share_count"`
|
||||
AddToFavUser int `json:"add_to_fav_user"`
|
||||
AddToFavCount int `json:"add_to_fav_count"`
|
||||
IntPageFromSessionReadUser int `json:"int_page_from_session_read_user"`
|
||||
IntPageFromSessionReadCount int `json:"int_page_from_session_read_count"`
|
||||
IntPageFromHistMsgReadUser int `json:"int_page_from_hist_msg_read_user"`
|
||||
IntPageFromHistMsgReadCount int `json:"int_page_from_hist_msg_read_count"`
|
||||
IntPageFromFeedReadUser int `json:"int_page_from_feed_read_user"`
|
||||
IntPageFromFeedReadCount int `json:"int_page_from_feed_read_count"`
|
||||
IntPageFromFriendsReadUser int `json:"int_page_from_friends_read_user"`
|
||||
IntPageFromFriendsReadCount int `json:"int_page_from_friends_read_count"`
|
||||
IntPageFromOtherReadUser int `json:"int_page_from_other_read_user"`
|
||||
IntPageFromOtherReadCount int `json:"int_page_from_other_read_count"`
|
||||
FeedShareFromSessionUser int `json:"feed_share_from_session_user"`
|
||||
FeedShareFromSessionCnt int `json:"feed_share_from_session_cnt"`
|
||||
FeedShareFromFeedUser int `json:"feed_share_from_feed_user"`
|
||||
FeedShareFromFeedCnt int `json:"feed_share_from_feed_cnt"`
|
||||
FeedShareFromOtherUser int `json:"feed_share_from_other_user"`
|
||||
FeedShareFromOtherCnt int `json:"feed_share_from_other_cnt"`
|
||||
}
|
||||
|
||||
//ResUserRead 获取图文统计数据响应
|
||||
type ResUserRead struct {
|
||||
util.CommonError
|
||||
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"`
|
||||
UserSource int `json:"user_source"`
|
||||
IntPageReadUser int `json:"int_page_read_user"`
|
||||
IntPageReadCount int `json:"int_page_read_count"`
|
||||
OriPageReadUser int `json:"ori_page_read_user"`
|
||||
OriPageReadCount int `json:"ori_page_read_count"`
|
||||
ShareUser int `json:"share_user"`
|
||||
ShareCount int `json:"share_count"`
|
||||
AddToFavUser int `json:"add_to_fav_user"`
|
||||
AddToFavCount int `json:"add_to_fav_count"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
//ResUserReadHour 获取图文统计分时数据
|
||||
type ResUserReadHour struct {
|
||||
util.CommonError
|
||||
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"`
|
||||
RefHour int `json:"ref_hour"`
|
||||
UserSource int `json:"user_source"`
|
||||
IntPageReadUser int `json:"int_page_read_user"`
|
||||
IntPageReadCount int `json:"int_page_read_count"`
|
||||
OriPageReadUser int `json:"ori_page_read_user"`
|
||||
OriPageReadCount int `json:"ori_page_read_count"`
|
||||
ShareUser int `json:"share_user"`
|
||||
ShareCount int `json:"share_count"`
|
||||
AddToFavUser int `json:"add_to_fav_user"`
|
||||
AddToFavCount int `json:"add_to_fav_count"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
//ResUserShare 获取图文分享转发数据
|
||||
type ResUserShare struct {
|
||||
util.CommonError
|
||||
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"`
|
||||
ShareScene int `json:"share_scene"`
|
||||
ShareCount int `json:"share_count"`
|
||||
ShareUser int `json:"share_user"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
//ResUserShareHour 获取图文分享转发分时数据
|
||||
type ResUserShareHour struct {
|
||||
util.CommonError
|
||||
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"`
|
||||
RefHour int `json:"ref_hour"`
|
||||
ShareScene int `json:"share_scene"`
|
||||
ShareCount int `json:"share_count"`
|
||||
ShareUser int `json:"share_user"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
//GetArticleSummary 获取图文群发每日数据
|
||||
func (cube *DataCube) GetArticleSummary(s string, e string) (resArticleSummary ResArticleSummary, err error) {
|
||||
accessToken, err := cube.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getArticleSummary, accessToken)
|
||||
reqDate := &reqDate{
|
||||
BeginDate: s,
|
||||
EndDate: e,
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(uri, reqDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resArticleSummary, "GetArticleSummary")
|
||||
return
|
||||
}
|
||||
|
||||
//GetArticleTotal 获取图文群发总数据
|
||||
func (cube *DataCube) GetArticleTotal(s string, e string) (resArticleTotal ResArticleTotal, err error) {
|
||||
accessToken, err := cube.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getArticleTotal, accessToken)
|
||||
reqDate := &reqDate{
|
||||
BeginDate: s,
|
||||
EndDate: e,
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(uri, reqDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resArticleTotal, "GetArticleTotal")
|
||||
return
|
||||
}
|
||||
|
||||
//GetUserRead 获取图文统计数据
|
||||
func (cube *DataCube) GetUserRead(s string, e string) (resUserRead ResUserRead, err error) {
|
||||
accessToken, err := cube.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getUserRead, accessToken)
|
||||
reqDate := &reqDate{
|
||||
BeginDate: s,
|
||||
EndDate: e,
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(uri, reqDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resUserRead, "GetUserRead")
|
||||
return
|
||||
}
|
||||
|
||||
//GetUserReadHour 获取图文统计分时数据
|
||||
func (cube *DataCube) GetUserReadHour(s string, e string) (resUserReadHour ResUserReadHour, err error) {
|
||||
accessToken, err := cube.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getUserReadHour, accessToken)
|
||||
reqDate := &reqDate{
|
||||
BeginDate: s,
|
||||
EndDate: e,
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(uri, reqDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resUserReadHour, "GetUserReadHour")
|
||||
return
|
||||
}
|
||||
|
||||
//GetUserShare 获取图文分享转发数据
|
||||
func (cube *DataCube) GetUserShare(s string, e string) (resUserShare ResUserShare, err error) {
|
||||
accessToken, err := cube.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getUserShare, accessToken)
|
||||
reqDate := &reqDate{
|
||||
BeginDate: s,
|
||||
EndDate: e,
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(uri, reqDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resUserShare, "GetUserShare")
|
||||
return
|
||||
}
|
||||
|
||||
//GetUserShareHour 获取图文分享转发分时数据
|
||||
func (cube *DataCube) GetUserShareHour(s string, e string) (resUserShareHour ResUserShareHour, err error) {
|
||||
accessToken, err := cube.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getUserShareHour, accessToken)
|
||||
reqDate := &reqDate{
|
||||
BeginDate: s,
|
||||
EndDate: e,
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(uri, reqDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resUserShareHour, "GetUserShareHour")
|
||||
return
|
||||
}
|
||||
22
officialaccount/datacube/datacube.go
Normal file
22
officialaccount/datacube/datacube.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package datacube
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
)
|
||||
|
||||
type reqDate struct {
|
||||
BeginDate string `json:"begin_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
}
|
||||
|
||||
//DataCube 数据统计
|
||||
type DataCube struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
//NewCube 数据统计
|
||||
func NewCube(context *context.Context) *DataCube {
|
||||
dataCube := new(DataCube)
|
||||
dataCube.Context = context
|
||||
return dataCube
|
||||
}
|
||||
82
officialaccount/datacube/interface.go
Normal file
82
officialaccount/datacube/interface.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package datacube
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
getInterfaceSummary = "https://api.weixin.qq.com/datacube/getinterfacesummary"
|
||||
getInterfaceSummaryHour = "https://api.weixin.qq.com/datacube/getinterfacesummaryhour"
|
||||
)
|
||||
|
||||
//ResInterfaceSummary 接口分析数据响应
|
||||
type ResInterfaceSummary struct {
|
||||
util.CommonError
|
||||
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"`
|
||||
CallbackCount int `json:"callback_count"`
|
||||
FailCount int `json:"fail_count"`
|
||||
TotalTimeCost int `json:"total_time_cost"`
|
||||
MaxTimeCost int `json:"max_time_cost"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
//ResInterfaceSummaryHour 接口分析分时数据响应
|
||||
type ResInterfaceSummaryHour struct {
|
||||
util.CommonError
|
||||
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"`
|
||||
RefHour int `json:"ref_hour"`
|
||||
CallbackCount int `json:"callback_count"`
|
||||
FailCount int `json:"fail_count"`
|
||||
TotalTimeCost int `json:"total_time_cost"`
|
||||
MaxTimeCost int `json:"max_time_cost"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
//GetInterfaceSummary 获取接口分析数据
|
||||
func (cube *DataCube) GetInterfaceSummary(s string, e string) (resInterfaceSummary ResInterfaceSummary, err error) {
|
||||
accessToken, err := cube.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getInterfaceSummary, accessToken)
|
||||
reqDate := &reqDate{
|
||||
BeginDate: s,
|
||||
EndDate: e,
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(uri, reqDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resInterfaceSummary, "GetInterfaceSummary")
|
||||
return
|
||||
}
|
||||
|
||||
//GetInterfaceSummaryHour 获取接口分析分时数据
|
||||
func (cube *DataCube) GetInterfaceSummaryHour(s string, e string) (resInterfaceSummaryHour ResInterfaceSummaryHour, err error) {
|
||||
accessToken, err := cube.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getInterfaceSummaryHour, accessToken)
|
||||
reqDate := &reqDate{
|
||||
BeginDate: s,
|
||||
EndDate: e,
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(uri, reqDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resInterfaceSummaryHour, "GetInterfaceSummaryHour")
|
||||
return
|
||||
}
|
||||
252
officialaccount/datacube/message.go
Normal file
252
officialaccount/datacube/message.go
Normal file
@@ -0,0 +1,252 @@
|
||||
package datacube
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
getUpstreamMsg = "https://api.weixin.qq.com/datacube/getupstreammsg"
|
||||
getUpstreamMsgHour = "https://api.weixin.qq.com/datacube/getupstreammsghour"
|
||||
getUpstreamMsgWeek = "https://api.weixin.qq.com/datacube/getupstreammsgweek"
|
||||
getUpstreamMsgMonth = "https://api.weixin.qq.com/datacube/getupstreammsgmonth"
|
||||
getUpstreamMsgDist = "https://api.weixin.qq.com/datacube/getupstreammsgdist"
|
||||
getUpstreamMsgDistWeek = "https://api.weixin.qq.com/datacube/getupstreammsgdistweek"
|
||||
getUpstreamMsgDistMonth = "https://api.weixin.qq.com/datacube/getupstreammsgdistmonth"
|
||||
)
|
||||
|
||||
//ResUpstreamMsg 获取消息发送概况数据响应
|
||||
type ResUpstreamMsg struct {
|
||||
util.CommonError
|
||||
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"`
|
||||
MsgType int `json:"msg_type"`
|
||||
MsgUser int `json:"msg_user"`
|
||||
MsgCount int `json:"msg_count"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
//ResUpstreamMsgHour 获取消息分送分时数据响应
|
||||
type ResUpstreamMsgHour struct {
|
||||
util.CommonError
|
||||
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"`
|
||||
RefHour int `json:"ref_hour"`
|
||||
MsgType int `json:"msg_type"`
|
||||
MsgUser int `json:"msg_user"`
|
||||
MsgCount int `json:"msg_count"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
//ResUpstreamMsgWeek 获取消息发送周数据响应
|
||||
type ResUpstreamMsgWeek struct {
|
||||
util.CommonError
|
||||
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"`
|
||||
MsgType int `json:"msg_type"`
|
||||
MsgUser int `json:"msg_user"`
|
||||
MsgCount int `json:"msg_count"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
//ResUpstreamMsgMonth 获取消息发送月数据响应
|
||||
type ResUpstreamMsgMonth struct {
|
||||
util.CommonError
|
||||
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"`
|
||||
MsgType int `json:"msg_type"`
|
||||
MsgUser int `json:"msg_user"`
|
||||
MsgCount int `json:"msg_count"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
//ResUpstreamMsgDist 获取消息发送分布数据响应
|
||||
type ResUpstreamMsgDist struct {
|
||||
util.CommonError
|
||||
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"`
|
||||
CountInterval int `json:"count_interval"`
|
||||
MsgUser int `json:"msg_user"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
//ResUpstreamMsgDistWeek 获取消息发送分布周数据响应
|
||||
type ResUpstreamMsgDistWeek struct {
|
||||
util.CommonError
|
||||
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"`
|
||||
CountInterval int `json:"count_interval"`
|
||||
MsgUser int `json:"msg_user"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
//ResUpstreamMsgDistMonth 获取消息发送分布月数据响应
|
||||
type ResUpstreamMsgDistMonth struct {
|
||||
util.CommonError
|
||||
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"`
|
||||
CountInterval int `json:"count_interval"`
|
||||
MsgUser int `json:"msg_user"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
//GetUpstreamMsg 获取消息发送概况数据
|
||||
func (cube *DataCube) GetUpstreamMsg(s string, e string) (resUpstreamMsg ResUpstreamMsg, err error) {
|
||||
accessToken, err := cube.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getUpstreamMsg, accessToken)
|
||||
reqDate := &reqDate{
|
||||
BeginDate: s,
|
||||
EndDate: e,
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(uri, reqDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resUpstreamMsg, "GetUpstreamMsg")
|
||||
return
|
||||
}
|
||||
|
||||
//GetUpstreamMsgHour 获取消息分送分时数据
|
||||
func (cube *DataCube) GetUpstreamMsgHour(s string, e string) (resUpstreamMsgHour ResUpstreamMsgHour, err error) {
|
||||
accessToken, err := cube.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getUpstreamMsgHour, accessToken)
|
||||
reqDate := &reqDate{
|
||||
BeginDate: s,
|
||||
EndDate: e,
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(uri, reqDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resUpstreamMsgHour, "GetUpstreamMsgHour")
|
||||
return
|
||||
}
|
||||
|
||||
//GetUpstreamMsgWeek 获取消息发送周数据
|
||||
func (cube *DataCube) GetUpstreamMsgWeek(s string, e string) (resUpstreamMsgWeek ResUpstreamMsgWeek, err error) {
|
||||
accessToken, err := cube.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getUpstreamMsgWeek, accessToken)
|
||||
reqDate := &reqDate{
|
||||
BeginDate: s,
|
||||
EndDate: e,
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(uri, reqDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resUpstreamMsgWeek, "GetUpstreamMsgWeek")
|
||||
return
|
||||
}
|
||||
|
||||
//GetUpstreamMsgMonth 获取消息发送月数据
|
||||
func (cube *DataCube) GetUpstreamMsgMonth(s string, e string) (resUpstreamMsgMonth ResUpstreamMsgMonth, err error) {
|
||||
accessToken, err := cube.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getUpstreamMsgMonth, accessToken)
|
||||
reqDate := &reqDate{
|
||||
BeginDate: s,
|
||||
EndDate: e,
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(uri, reqDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resUpstreamMsgMonth, "GetUpstreamMsgMonth")
|
||||
return
|
||||
}
|
||||
|
||||
//GetUpstreamMsgDist 获取消息发送分布数据
|
||||
func (cube *DataCube) GetUpstreamMsgDist(s string, e string) (resUpstreamMsgDist ResUpstreamMsgDist, err error) {
|
||||
accessToken, err := cube.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getUpstreamMsgDist, accessToken)
|
||||
reqDate := &reqDate{
|
||||
BeginDate: s,
|
||||
EndDate: e,
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(uri, reqDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resUpstreamMsgDist, "GetUpstreamMsgDist")
|
||||
return
|
||||
}
|
||||
|
||||
//GetUpstreamMsgDistWeek 获取消息发送分布周数据
|
||||
func (cube *DataCube) GetUpstreamMsgDistWeek(s string, e string) (resUpstreamMsgDistWeek ResUpstreamMsgDistWeek, err error) {
|
||||
accessToken, err := cube.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getUpstreamMsgDistWeek, accessToken)
|
||||
reqDate := &reqDate{
|
||||
BeginDate: s,
|
||||
EndDate: e,
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(uri, reqDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resUpstreamMsgDistWeek, "GetUpstreamMsgDistWeek")
|
||||
return
|
||||
}
|
||||
|
||||
//GetUpstreamMsgDistMonth 获取消息发送分布月数据
|
||||
func (cube *DataCube) GetUpstreamMsgDistMonth(s string, e string) (resUpstreamMsgDistMonth ResUpstreamMsgDistMonth, err error) {
|
||||
accessToken, err := cube.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getUpstreamMsgDistMonth, accessToken)
|
||||
reqDate := &reqDate{
|
||||
BeginDate: s,
|
||||
EndDate: e,
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(uri, reqDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resUpstreamMsgDistMonth, "GetUpstreamMsgDistMonth")
|
||||
return
|
||||
}
|
||||
306
officialaccount/datacube/publisher.go
Normal file
306
officialaccount/datacube/publisher.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package datacube
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
//AdSlot 广告位类型
|
||||
type AdSlot string
|
||||
|
||||
const (
|
||||
//SlotIDBizBottom 公众号底部广告
|
||||
SlotIDBizBottom AdSlot = "SLOT_ID_BIZ_BOTTOM"
|
||||
//SlotIDBizMidContext 公众号文中广告
|
||||
SlotIDBizMidContext AdSlot = "SLOT_ID_BIZ_MID_CONTEXT"
|
||||
//SlotIDBizVideoEnd 公众号视频后贴
|
||||
SlotIDBizVideoEnd AdSlot = "SLOT_ID_BIZ_VIDEO_END"
|
||||
//SlotIDBizSponsor 公众号互选广告
|
||||
SlotIDBizSponsor AdSlot = "SLOT_ID_BIZ_SPONSOR"
|
||||
//SlotIDBizCps 公众号返佣商品
|
||||
SlotIDBizCps AdSlot = "SLOT_ID_BIZ_CPS"
|
||||
//SlotIDWeappBanner 小程序banner
|
||||
SlotIDWeappBanner AdSlot = "SLOT_ID_WEAPP_BANNER"
|
||||
//SlotIDWeappRewardVideo 小程序激励视频
|
||||
SlotIDWeappRewardVideo AdSlot = "SLOT_ID_WEAPP_REWARD_VIDEO"
|
||||
//SlotIDWeappInterstitial 小程序插屏广告
|
||||
SlotIDWeappInterstitial AdSlot = "SLOT_ID_WEAPP_INTERSTITIAL"
|
||||
//SlotIDWeappVideoFeeds 小程序视频广告
|
||||
SlotIDWeappVideoFeeds AdSlot = "SLOT_ID_WEAPP_VIDEO_FEEDS"
|
||||
//SlotIDWeappVideoBegin 小程序视频前贴
|
||||
SlotIDWeappVideoBegin AdSlot = "SLOT_ID_WEAPP_VIDEO_BEGIN"
|
||||
//SlotIDWeappBox 小程序格子广告
|
||||
SlotIDWeappBox AdSlot = "SLOT_ID_WEAPP_BOX"
|
||||
)
|
||||
|
||||
const (
|
||||
publisherURL = "https://api.weixin.qq.com/publisher/stat"
|
||||
)
|
||||
|
||||
const (
|
||||
actionPublisherAdPosGeneral = "publisher_adpos_general"
|
||||
actionPublisherCpsGeneral = "publisher_cps_general"
|
||||
actionPublisherSettlement = "publisher_settlement"
|
||||
)
|
||||
|
||||
//BaseResp 错误信息
|
||||
type BaseResp struct {
|
||||
ErrMsg string `json:"err_msg"`
|
||||
Ret int `json:"ret"`
|
||||
}
|
||||
|
||||
//ResPublisherAdPos 公众号分广告位数据响应
|
||||
type ResPublisherAdPos struct {
|
||||
util.CommonError
|
||||
BaseResp
|
||||
|
||||
Base BaseResp `json:"base_resp"`
|
||||
List []ResAdPosList `json:"list"`
|
||||
Summary ResAdPosSummary `json:"summary"`
|
||||
TotalNum int `json:"total_num"`
|
||||
}
|
||||
|
||||
//ResAdPosList 公众号分广告位列表
|
||||
type ResAdPosList struct {
|
||||
SlotID int64 `json:"slot_id"`
|
||||
AdSlot string `json:"ad_slot"`
|
||||
Date string `json:"date"`
|
||||
ReqSuccCount int `json:"req_succ_count"`
|
||||
ExposureCount int `json:"exposure_count"`
|
||||
ExposureRate float64 `json:"exposure_rate"`
|
||||
ClickCount int `json:"click_count"`
|
||||
ClickRate float64 `json:"click_rate"`
|
||||
Income int `json:"income"`
|
||||
Ecpm float64 `json:"ecpm"`
|
||||
}
|
||||
|
||||
//ResAdPosSummary 公众号分广告位概览
|
||||
type ResAdPosSummary struct {
|
||||
ReqSuccCount int `json:"req_succ_count"`
|
||||
ExposureCount int `json:"exposure_count"`
|
||||
ExposureRate float64 `json:"exposure_rate"`
|
||||
ClickCount int `json:"click_count"`
|
||||
ClickRate float64 `json:"click_rate"`
|
||||
Income int `json:"income"`
|
||||
Ecpm float64 `json:"ecpm"`
|
||||
}
|
||||
|
||||
//ResPublisherCps 公众号返佣商品数据响应
|
||||
type ResPublisherCps struct {
|
||||
util.CommonError
|
||||
BaseResp
|
||||
|
||||
Base BaseResp `json:"base_resp"`
|
||||
List []ResCpsList `json:"list"`
|
||||
Summary ResCpsSummary `json:"summary"`
|
||||
TotalNum int `json:"total_num"`
|
||||
}
|
||||
|
||||
//ResCpsList 公众号返佣商品列表
|
||||
type ResCpsList struct {
|
||||
Date string `json:"date"`
|
||||
ExposureCount int `json:"exposure_count"`
|
||||
ClickCount int `json:"click_count"`
|
||||
ClickRate float64 `json:"click_rate"`
|
||||
OrderCount int `json:"order_count"`
|
||||
OrderRate float64 `json:"order_rate"`
|
||||
TotalFee int `json:"total_fee"`
|
||||
TotalCommission int `json:"total_commission"`
|
||||
}
|
||||
|
||||
//ResCpsSummary 公众号返佣概览
|
||||
type ResCpsSummary struct {
|
||||
ExposureCount int `json:"exposure_count"`
|
||||
ClickCount int `json:"click_count"`
|
||||
ClickRate float64 `json:"click_rate"`
|
||||
OrderCount int `json:"order_count"`
|
||||
OrderRate float64 `json:"order_rate"`
|
||||
TotalFee int `json:"total_fee"`
|
||||
TotalCommission int `json:"total_commission"`
|
||||
}
|
||||
|
||||
//ResPublisherSettlement 公众号结算收入数据及结算主体信息响应
|
||||
type ResPublisherSettlement struct {
|
||||
util.CommonError
|
||||
BaseResp
|
||||
|
||||
Base BaseResp `json:"base_resp"`
|
||||
Body string `json:"body"`
|
||||
PenaltyAll int `json:"penalty_all"`
|
||||
RevenueAll int64 `json:"revenue_all"`
|
||||
SettledRevenueAll int64 `json:"settled_revenue_all"`
|
||||
SettlementList []SettlementList `json:"settlement_list"`
|
||||
TotalNum int `json:"total_num"`
|
||||
}
|
||||
|
||||
//SettlementList 结算单列表
|
||||
type SettlementList struct {
|
||||
Date string `json:"date"`
|
||||
Zone string `json:"zone"`
|
||||
Month string `json:"month"`
|
||||
Order int `json:"order"`
|
||||
SettStatus int `json:"sett_status"`
|
||||
SettledRevenue int `json:"settled_revenue"`
|
||||
SettNo string `json:"sett_no"`
|
||||
MailSendCnt string `json:"mail_send_cnt"`
|
||||
SlotRevenue []SlotRevenue `json:"slot_revenue"`
|
||||
}
|
||||
|
||||
//SlotRevenue 产生收入的广告
|
||||
type SlotRevenue struct {
|
||||
SlotID string `json:"slot_id"`
|
||||
SlotSettledRevenue int `json:"slot_settled_revenue"`
|
||||
}
|
||||
|
||||
//ParamsPublisher 拉取数据参数
|
||||
type ParamsPublisher struct {
|
||||
Action string `json:"action"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
AdSlot AdSlot `json:"ad_slot"`
|
||||
}
|
||||
|
||||
// fetchData 拉取统计数据
|
||||
func (cube *DataCube) fetchData(params ParamsPublisher) (response []byte, err error) {
|
||||
accessToken, err := cube.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
v := url.Values{}
|
||||
v.Add("action", params.Action)
|
||||
v.Add("access_token", accessToken)
|
||||
v.Add("page", strconv.Itoa(params.Page))
|
||||
v.Add("page_size", strconv.Itoa(params.PageSize))
|
||||
v.Add("start_date", params.StartDate)
|
||||
v.Add("end_date", params.EndDate)
|
||||
if params.AdSlot != "" {
|
||||
v.Add("ad_slot", string(params.AdSlot))
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?%s", publisherURL, v.Encode())
|
||||
|
||||
response, err = util.HTTPGet(uri)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//GetPublisherAdPosGeneral 获取公众号分广告位数据
|
||||
func (cube *DataCube) GetPublisherAdPosGeneral(startDate, endDate string, page, pageSize int, adSlot AdSlot) (resPublisherAdPos ResPublisherAdPos, err error) {
|
||||
params := ParamsPublisher{
|
||||
Action: actionPublisherAdPosGeneral,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
AdSlot: adSlot,
|
||||
}
|
||||
|
||||
response, err := cube.fetchData(params)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(response, &resPublisherAdPos)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if resPublisherAdPos.CommonError.ErrCode != 0 {
|
||||
err = fmt.Errorf("GetPublisherAdPosGeneral Error , errcode=%d , errmsg=%s", resPublisherAdPos.CommonError.ErrCode, resPublisherAdPos.CommonError.ErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
if resPublisherAdPos.BaseResp.Ret != 0 {
|
||||
err = fmt.Errorf("GetPublisherAdPosGeneral Error , errcode=%d , errmsg=%s", resPublisherAdPos.BaseResp.Ret, resPublisherAdPos.BaseResp.ErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
if resPublisherAdPos.Base.Ret != 0 {
|
||||
err = fmt.Errorf("GetPublisherAdPosGeneral Error , errcode=%d , errmsg=%s", resPublisherAdPos.Base.Ret, resPublisherAdPos.Base.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//GetPublisherCpsGeneral 获取公众号返佣商品数据
|
||||
func (cube *DataCube) GetPublisherCpsGeneral(startDate, endDate string, page, pageSize int) (resPublisherCps ResPublisherCps, err error) {
|
||||
params := ParamsPublisher{
|
||||
Action: actionPublisherCpsGeneral,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}
|
||||
|
||||
response, err := cube.fetchData(params)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(response, &resPublisherCps)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if resPublisherCps.CommonError.ErrCode != 0 {
|
||||
err = fmt.Errorf("GetPublisherCpsGeneral Error , errcode=%d , errmsg=%s", resPublisherCps.CommonError.ErrCode, resPublisherCps.CommonError.ErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
if resPublisherCps.BaseResp.Ret != 0 {
|
||||
err = fmt.Errorf("GetPublisherCpsGeneral Error , errcode=%d , errmsg=%s", resPublisherCps.BaseResp.Ret, resPublisherCps.BaseResp.ErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
if resPublisherCps.Base.Ret != 0 {
|
||||
err = fmt.Errorf("GetPublisherCpsGeneral Error , errcode=%d , errmsg=%s", resPublisherCps.Base.Ret, resPublisherCps.Base.ErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//GetPublisherSettlement 获取公众号结算收入数据及结算主体信息
|
||||
func (cube *DataCube) GetPublisherSettlement(startDate, endDate string, page, pageSize int) (resPublisherSettlement ResPublisherSettlement, err error) {
|
||||
params := ParamsPublisher{
|
||||
Action: actionPublisherSettlement,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}
|
||||
|
||||
response, err := cube.fetchData(params)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(response, &resPublisherSettlement)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if resPublisherSettlement.CommonError.ErrCode != 0 {
|
||||
err = fmt.Errorf("GetPublisherSettlement Error , errcode=%d , errmsg=%s", resPublisherSettlement.CommonError.ErrCode, resPublisherSettlement.CommonError.ErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
if resPublisherSettlement.BaseResp.Ret != 0 {
|
||||
err = fmt.Errorf("GetPublisherSettlement Error , errcode=%d , errmsg=%s", resPublisherSettlement.BaseResp.Ret, resPublisherSettlement.BaseResp.ErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
if resPublisherSettlement.Base.Ret != 0 {
|
||||
err = fmt.Errorf("GetPublisherSettlement Error , errcode=%d , errmsg=%s", resPublisherSettlement.Base.Ret, resPublisherSettlement.Base.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
77
officialaccount/datacube/user.go
Normal file
77
officialaccount/datacube/user.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package datacube
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
getUserSummary = "https://api.weixin.qq.com/datacube/getusersummary"
|
||||
getUserAccumulate = "https://api.weixin.qq.com/datacube/getusercumulate"
|
||||
)
|
||||
|
||||
//ResUserSummary 获取用户增减数据响应
|
||||
type ResUserSummary struct {
|
||||
util.CommonError
|
||||
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"`
|
||||
UserSource int `json:"user_source"`
|
||||
NewUser int `json:"new_user"`
|
||||
CancelUser int `json:"cancel_user"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
//ResUserAccumulate 获取累计用户数据响应
|
||||
type ResUserAccumulate struct {
|
||||
util.CommonError
|
||||
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"`
|
||||
CumulateUser int `json:"cumulate_user"`
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
//GetUserSummary 获取用户增减数据
|
||||
func (cube *DataCube) GetUserSummary(s string, e string) (resUserSummary ResUserSummary, err error) {
|
||||
accessToken, err := cube.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getUserSummary, accessToken)
|
||||
reqDate := &reqDate{
|
||||
BeginDate: s,
|
||||
EndDate: e,
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(uri, reqDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resUserSummary, "GetUserSummary")
|
||||
return
|
||||
}
|
||||
|
||||
//GetUserAccumulate 获取累计用户数据
|
||||
func (cube *DataCube) GetUserAccumulate(s string, e string) (resUserAccumulate ResUserAccumulate, err error) {
|
||||
accessToken, err := cube.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getUserAccumulate, accessToken)
|
||||
reqDate := &reqDate{
|
||||
BeginDate: s,
|
||||
EndDate: e,
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(uri, reqDate)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &resUserAccumulate, "GetUserAccumulate")
|
||||
return
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
// ReqBind 设备绑定解绑共通实体
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
//ResCreateQRCode 获取二维码的返回实体
|
||||
@@ -60,7 +60,7 @@ func (d *Device) VerifyQRCode(ticket string) (res ResVerifyQRCode, err error) {
|
||||
req := map[string]interface{}{
|
||||
"ticket": ticket,
|
||||
}
|
||||
fmt.Println(req)
|
||||
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(uri, req); err != nil {
|
||||
return
|
||||
64
officialaccount/js/js.go
Normal file
64
officialaccount/js/js.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package js
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/credential"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
// Js struct
|
||||
type Js struct {
|
||||
*context.Context
|
||||
credential.JsTicketHandle
|
||||
}
|
||||
|
||||
// Config 返回给用户jssdk配置信息
|
||||
type Config struct {
|
||||
AppID string `json:"app_id"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
NonceStr string `json:"nonce_str"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
//NewJs init
|
||||
func NewJs(context *context.Context) *Js {
|
||||
js := new(Js)
|
||||
js.Context = context
|
||||
jsTicketHandle := credential.NewDefaultJsTicket(context.AppID, credential.CacheKeyOfficialAccountPrefix, context.Cache)
|
||||
js.SetJsTicketHandle(jsTicketHandle)
|
||||
return js
|
||||
}
|
||||
|
||||
//SetJsTicketHandle 自定义js ticket取值方式
|
||||
func (js *Js) SetJsTicketHandle(ticketHandle credential.JsTicketHandle) {
|
||||
js.JsTicketHandle = ticketHandle
|
||||
}
|
||||
|
||||
//GetConfig 获取jssdk需要的配置参数
|
||||
//uri 为当前网页地址
|
||||
func (js *Js) GetConfig(uri string) (config *Config, err error) {
|
||||
config = new(Config)
|
||||
var accessToken string
|
||||
accessToken, err = js.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var ticketStr string
|
||||
ticketStr, err = js.GetTicket(accessToken)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
nonceStr := util.RandomStr(16)
|
||||
timestamp := util.GetCurrTs()
|
||||
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
||||
sigStr := util.Signature(str)
|
||||
|
||||
config.AppID = js.AppID
|
||||
config.NonceStr = nonceStr
|
||||
config.Timestamp = timestamp
|
||||
config.Signature = sigStr
|
||||
return
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -14,8 +14,22 @@ const (
|
||||
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"
|
||||
batchGetMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/batchget_material"
|
||||
)
|
||||
|
||||
//PermanentMaterialType 永久素材类型
|
||||
type PermanentMaterialType string
|
||||
|
||||
const (
|
||||
//PermanentMaterialTypeImage 永久素材图片类型(image)
|
||||
PermanentMaterialTypeImage PermanentMaterialType = "image"
|
||||
//PermanentMaterialTypeVideo 永久素材视频类型(video)
|
||||
PermanentMaterialTypeVideo PermanentMaterialType = "video"
|
||||
//PermanentMaterialTypeVoice 永久素材语音类型 (voice)
|
||||
PermanentMaterialTypeVoice PermanentMaterialType = "voice"
|
||||
//PermanentMaterialTypeNews 永久素材图文类型(news)
|
||||
PermanentMaterialTypeNews PermanentMaterialType = "news"
|
||||
)
|
||||
//Material 素材管理
|
||||
type Material struct {
|
||||
*context.Context
|
||||
@@ -32,6 +46,7 @@ func NewMaterial(context *context.Context) *Material {
|
||||
type Article struct {
|
||||
Title string `json:"title"`
|
||||
ThumbMediaID string `json:"thumb_media_id"`
|
||||
ThumbURL string `json:"thumb_url"`
|
||||
Author string `json:"author"`
|
||||
Digest string `json:"digest"`
|
||||
ShowCoverPic int `json:"show_cover_pic"`
|
||||
@@ -54,6 +69,9 @@ func (material *Material) GetNews(id string) ([]*Article, error) {
|
||||
}
|
||||
req.MediaID = id
|
||||
responseBytes, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res struct {
|
||||
NewsItem []*Article `json:"news_item"`
|
||||
@@ -90,6 +108,9 @@ 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)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var res resArticles
|
||||
err = json.Unmarshal(responseBytes, &res)
|
||||
if err != nil {
|
||||
@@ -111,6 +132,7 @@ type resAddMaterial struct {
|
||||
func (material *Material) AddMaterial(mediaType MediaType, filename string) (mediaID string, url string, err error) {
|
||||
if mediaType == MediaTypeVideo {
|
||||
err = errors.New("永久视频素材上传使用 AddVideo 方法")
|
||||
return
|
||||
}
|
||||
var accessToken string
|
||||
accessToken, err = material.GetAccessToken()
|
||||
@@ -215,3 +237,59 @@ func (material *Material) DeleteMaterial(mediaID string) error {
|
||||
|
||||
return util.DecodeWithCommonError(response, "DeleteMaterial")
|
||||
}
|
||||
|
||||
//ArticleList 永久素材列表
|
||||
type ArticleList struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ItemCount int64 `json:"item_count"`
|
||||
Item []ArticleListItem `json:"item"`
|
||||
}
|
||||
|
||||
//ArticleListItem 用于ArticleList的item节点
|
||||
type ArticleListItem struct {
|
||||
MediaID string `json:"media_id"`
|
||||
Content ArticleListContent `json:"content"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
UpdateTime int64 `json:"update_time"`
|
||||
}
|
||||
|
||||
//ArticleListContent 用于ArticleListItem的content节点
|
||||
type ArticleListContent struct {
|
||||
NewsItem []Article `json:"news_item"`
|
||||
UpdateTime int64 `json:"update_time"`
|
||||
CreateTime int64 `json:"create_time"`
|
||||
}
|
||||
|
||||
//reqBatchGetMaterial BatchGetMaterial请求参数
|
||||
type reqBatchGetMaterial struct {
|
||||
Type PermanentMaterialType `json:"type"`
|
||||
Count int64 `json:"count"`
|
||||
Offset int64 `json:"offset"`
|
||||
}
|
||||
|
||||
// BatchGetMaterial 批量获取永久素材
|
||||
//reference:https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Get_materials_list.html
|
||||
func (material *Material) BatchGetMaterial(permanentMaterialType PermanentMaterialType, offset, count int64) (list ArticleList, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = material.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", batchGetMaterialURL, accessToken)
|
||||
|
||||
req := reqBatchGetMaterial{
|
||||
Type: permanentMaterialType,
|
||||
Offset: offset,
|
||||
Count: count,
|
||||
}
|
||||
|
||||
var response []byte
|
||||
response, err = util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &list, "BatchGetMaterial")
|
||||
return
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
//MediaType 媒体文件类型
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -102,12 +102,12 @@ type ButtonNew struct {
|
||||
|
||||
//MatchRule 个性化菜单规则
|
||||
type MatchRule struct {
|
||||
GroupID int32 `json:"group_id,omitempty"`
|
||||
Sex int32 `json:"sex,omitempty"`
|
||||
GroupID string `json:"group_id,omitempty"`
|
||||
Sex string `json:"sex,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
Province string `json:"province,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
ClientPlatformType int32 `json:"client_platform_type,omitempty"`
|
||||
ClientPlatformType string `json:"client_platform_type,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
}
|
||||
|
||||
@@ -138,6 +138,24 @@ func (menu *Menu) SetMenu(buttons []*Button) error {
|
||||
return util.DecodeWithCommonError(response, "SetMenu")
|
||||
}
|
||||
|
||||
|
||||
//SetMenuByJSON 设置按钮
|
||||
func (menu *Menu) SetMenuByJSON(jsonInfo string) error {
|
||||
accessToken, err := menu.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", menuCreateURL, accessToken)
|
||||
|
||||
response, err := util.PostJSON(uri, jsonInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.DecodeWithCommonError(response, "SetMenu")
|
||||
}
|
||||
|
||||
//GetMenu 获取菜单配置
|
||||
func (menu *Menu) GetMenu() (resMenu ResMenu, err error) {
|
||||
var accessToken string
|
||||
@@ -198,6 +216,23 @@ func (menu *Menu) AddConditional(buttons []*Button, matchRule *MatchRule) error
|
||||
return util.DecodeWithCommonError(response, "AddConditional")
|
||||
}
|
||||
|
||||
|
||||
//AddConditionalByJSON 添加个性化菜单
|
||||
func (menu *Menu) AddConditionalByJSON(jsonInfo string) error {
|
||||
accessToken, err := menu.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", menuAddConditionalURL, accessToken)
|
||||
response, err := util.PostJSON(uri, jsonInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.DecodeWithCommonError(response, "AddConditional")
|
||||
}
|
||||
|
||||
//DeleteConditional 删除个性化菜单
|
||||
func (menu *Menu) DeleteConditional(menuID int64) error {
|
||||
accessToken, err := menu.GetAccessToken()
|
||||
@@ -3,8 +3,9 @@ package message
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -3,7 +3,7 @@ package message
|
||||
import (
|
||||
"encoding/xml"
|
||||
|
||||
"github.com/silenceper/wechat/device"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/device"
|
||||
)
|
||||
|
||||
// MsgType 基本消息类型
|
||||
@@ -72,6 +72,8 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
//微信开放平台需要用到
|
||||
|
||||
// InfoTypeVerifyTicket 返回ticket
|
||||
InfoTypeVerifyTicket InfoType = "component_verify_ticket"
|
||||
// InfoTypeAuthorized 授权
|
||||
@@ -87,20 +89,21 @@ type MixMessage struct {
|
||||
CommonToken
|
||||
|
||||
//基本消息
|
||||
MsgID int64 `xml:"MsgId"`
|
||||
Content string `xml:"Content"`
|
||||
Recognition string `xml:"Recognition"`
|
||||
PicURL string `xml:"PicUrl"`
|
||||
MediaID string `xml:"MediaId"`
|
||||
Format string `xml:"Format"`
|
||||
ThumbMediaID string `xml:"ThumbMediaId"`
|
||||
LocationX float64 `xml:"Location_X"`
|
||||
LocationY float64 `xml:"Location_Y"`
|
||||
Scale float64 `xml:"Scale"`
|
||||
Label string `xml:"Label"`
|
||||
Title string `xml:"Title"`
|
||||
Description string `xml:"Description"`
|
||||
URL string `xml:"Url"`
|
||||
MsgID int64 `xml:"MsgId"` //其他消息推送过来是MsgId
|
||||
TemplateMsgID int64 `xml:"MsgID"` //模板消息推送成功的消息是MsgID
|
||||
Content string `xml:"Content"`
|
||||
Recognition string `xml:"Recognition"`
|
||||
PicURL string `xml:"PicUrl"`
|
||||
MediaID string `xml:"MediaId"`
|
||||
Format string `xml:"Format"`
|
||||
ThumbMediaID string `xml:"ThumbMediaId"`
|
||||
LocationX float64 `xml:"Location_X"`
|
||||
LocationY float64 `xml:"Location_Y"`
|
||||
Scale float64 `xml:"Scale"`
|
||||
Label string `xml:"Label"`
|
||||
Title string `xml:"Title"`
|
||||
Description string `xml:"Description"`
|
||||
URL string `xml:"Url"`
|
||||
|
||||
//事件相关
|
||||
Event EventType `xml:"Event"`
|
||||
@@ -1,11 +1,11 @@
|
||||
package template
|
||||
package message
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -24,13 +24,13 @@ func NewTemplate(context *context.Context) *Template {
|
||||
return tpl
|
||||
}
|
||||
|
||||
//Message 发送的模板消息内容
|
||||
type Message struct {
|
||||
ToUser string `json:"touser"` // 必须, 接受者OpenID
|
||||
TemplateID string `json:"template_id"` // 必须, 模版ID
|
||||
URL string `json:"url,omitempty"` // 可选, 用户点击后跳转的URL, 该URL必须处于开发者在公众平台网站中设置的域中
|
||||
Color string `json:"color,omitempty"` // 可选, 整个消息的颜色, 可以不设置
|
||||
Data map[string]*DataItem `json:"data"` // 必须, 模板数据
|
||||
//TemplateMessage 发送的模板消息内容
|
||||
type TemplateMessage struct {
|
||||
ToUser string `json:"touser"` // 必须, 接受者OpenID
|
||||
TemplateID string `json:"template_id"` // 必须, 模版ID
|
||||
URL string `json:"url,omitempty"` // 可选, 用户点击后跳转的URL, 该URL必须处于开发者在公众平台网站中设置的域中
|
||||
Color string `json:"color,omitempty"` // 可选, 整个消息的颜色, 可以不设置
|
||||
Data map[string]*TemplateDataItem `json:"data"` // 必须, 模板数据
|
||||
|
||||
MiniProgram struct {
|
||||
AppID string `json:"appid"` //所需跳转到的小程序appid(该小程序appid必须与发模板消息的公众号是绑定关联关系)
|
||||
@@ -38,8 +38,8 @@ type Message struct {
|
||||
} `json:"miniprogram"` //可选,跳转至小程序地址
|
||||
}
|
||||
|
||||
//DataItem 模版内某个 .DATA 的值
|
||||
type DataItem struct {
|
||||
//TemplateDataItem 模版内某个 .DATA 的值
|
||||
type TemplateDataItem struct {
|
||||
Value string `json:"value"`
|
||||
Color string `json:"color,omitempty"`
|
||||
}
|
||||
@@ -51,7 +51,7 @@ type resTemplateSend struct {
|
||||
}
|
||||
|
||||
//Send 发送模板消息
|
||||
func (tpl *Template) Send(msg *Message) (msgID int64, err error) {
|
||||
func (tpl *Template) Send(msg *TemplateMessage) (msgID int64, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = tpl.GetAccessToken()
|
||||
if err != nil {
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
109
officialaccount/officialaccount.go
Normal file
109
officialaccount/officialaccount.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package officialaccount
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/v2/officialaccount/datacube"
|
||||
"net/http"
|
||||
|
||||
"github.com/silenceper/wechat/v2/credential"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/basic"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/broadcast"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/config"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/device"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/js"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/material"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/menu"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/message"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/oauth"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/server"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/user"
|
||||
)
|
||||
|
||||
//OfficialAccount 微信公众号相关API
|
||||
type OfficialAccount struct {
|
||||
ctx *context.Context
|
||||
}
|
||||
|
||||
//NewOfficialAccount 实例化公众号API
|
||||
func NewOfficialAccount(cfg *config.Config) *OfficialAccount {
|
||||
defaultAkHandle := credential.NewDefaultAccessToken(cfg.AppID, cfg.AppSecret, credential.CacheKeyOfficialAccountPrefix, cfg.Cache)
|
||||
ctx := &context.Context{
|
||||
Config: cfg,
|
||||
AccessTokenHandle: defaultAkHandle,
|
||||
}
|
||||
return &OfficialAccount{ctx: ctx}
|
||||
}
|
||||
|
||||
//SetAccessTokenHandle 自定义access_token获取方式
|
||||
func (officialAccount *OfficialAccount) SetAccessTokenHandle(accessTokenHandle credential.AccessTokenHandle) {
|
||||
officialAccount.ctx.AccessTokenHandle = accessTokenHandle
|
||||
}
|
||||
|
||||
// GetContext get Context
|
||||
func (officialAccount *OfficialAccount) GetContext() *context.Context {
|
||||
return officialAccount.ctx
|
||||
}
|
||||
|
||||
// GetBasic qr/url 相关配置
|
||||
func (officialAccount *OfficialAccount) GetBasic() *basic.Basic {
|
||||
return basic.NewBasic(officialAccount.ctx)
|
||||
}
|
||||
|
||||
// GetMenu 菜单管理接口
|
||||
func (officialAccount *OfficialAccount) GetMenu() *menu.Menu {
|
||||
return menu.NewMenu(officialAccount.ctx)
|
||||
}
|
||||
|
||||
// GetServer 消息管理:接收事件,被动回复消息管理
|
||||
func (officialAccount *OfficialAccount) GetServer(req *http.Request, writer http.ResponseWriter) *server.Server {
|
||||
srv := server.NewServer(officialAccount.ctx)
|
||||
srv.Request = req
|
||||
srv.Writer = writer
|
||||
return srv
|
||||
}
|
||||
|
||||
//GetAccessToken 获取access_token
|
||||
func (officialAccount *OfficialAccount) GetAccessToken() (string, error) {
|
||||
return officialAccount.ctx.GetAccessToken()
|
||||
}
|
||||
|
||||
// GetOauth oauth2网页授权
|
||||
func (officialAccount *OfficialAccount) GetOauth() *oauth.Oauth {
|
||||
return oauth.NewOauth(officialAccount.ctx)
|
||||
}
|
||||
|
||||
// GetMaterial 素材管理
|
||||
func (officialAccount *OfficialAccount) GetMaterial() *material.Material {
|
||||
return material.NewMaterial(officialAccount.ctx)
|
||||
}
|
||||
|
||||
// GetJs js-sdk配置
|
||||
func (officialAccount *OfficialAccount) GetJs() *js.Js {
|
||||
return js.NewJs(officialAccount.ctx)
|
||||
}
|
||||
|
||||
// GetUser 用户管理接口
|
||||
func (officialAccount *OfficialAccount) GetUser() *user.User {
|
||||
return user.NewUser(officialAccount.ctx)
|
||||
}
|
||||
|
||||
// GetTemplate 模板消息接口
|
||||
func (officialAccount *OfficialAccount) GetTemplate() *message.Template {
|
||||
return message.NewTemplate(officialAccount.ctx)
|
||||
}
|
||||
|
||||
// GetDevice 获取智能设备的实例
|
||||
func (officialAccount *OfficialAccount) GetDevice() *device.Device {
|
||||
return device.NewDevice(officialAccount.ctx)
|
||||
}
|
||||
|
||||
//GetBroadcast 群发消息
|
||||
//TODO 待完善
|
||||
func (officialAccount *OfficialAccount) GetBroadcast() *broadcast.Broadcast {
|
||||
return broadcast.NewBroadcast(officialAccount.ctx)
|
||||
}
|
||||
|
||||
//GetDataCube 数据统计
|
||||
func (officialAccount *OfficialAccount) GetDataCube() *datacube.DataCube {
|
||||
return datacube.NewCube(officialAccount.ctx)
|
||||
}
|
||||
@@ -5,29 +5,34 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/message"
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/message"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
//Server struct
|
||||
type Server struct {
|
||||
*context.Context
|
||||
Writer http.ResponseWriter
|
||||
Request *http.Request
|
||||
|
||||
debug bool
|
||||
skipValidate bool
|
||||
|
||||
openID string
|
||||
|
||||
messageHandler func(message.MixMessage) *message.Reply
|
||||
|
||||
requestRawXMLMsg []byte
|
||||
requestMsg message.MixMessage
|
||||
responseRawXMLMsg []byte
|
||||
responseMsg interface{}
|
||||
RequestRawXMLMsg []byte
|
||||
RequestMsg message.MixMessage
|
||||
ResponseRawXMLMsg []byte
|
||||
ResponseMsg interface{}
|
||||
|
||||
isSafeMode bool
|
||||
random []byte
|
||||
@@ -42,14 +47,15 @@ func NewServer(context *context.Context) *Server {
|
||||
return srv
|
||||
}
|
||||
|
||||
// SetDebug set debug field
|
||||
func (srv *Server) SetDebug(debug bool) {
|
||||
srv.debug = debug
|
||||
// SkipValidate set skip validate
|
||||
func (srv *Server) SkipValidate(skip bool) {
|
||||
srv.skipValidate = skip
|
||||
}
|
||||
|
||||
//Serve 处理微信的请求消息
|
||||
func (srv *Server) Serve() error {
|
||||
if !srv.Validate() {
|
||||
log.Error("Validate Signature Failed.")
|
||||
return fmt.Errorf("请求校验失败")
|
||||
}
|
||||
|
||||
@@ -64,22 +70,21 @@ func (srv *Server) Serve() error {
|
||||
return err
|
||||
}
|
||||
|
||||
//debug
|
||||
if srv.debug {
|
||||
fmt.Println("request msg = ", string(srv.requestRawXMLMsg))
|
||||
}
|
||||
//debug print request msg
|
||||
log.Debugf("request msg =%s", string(srv.RequestRawXMLMsg))
|
||||
|
||||
return srv.buildResponse(response)
|
||||
}
|
||||
|
||||
//Validate 校验请求是否合法
|
||||
func (srv *Server) Validate() bool {
|
||||
if srv.debug {
|
||||
if srv.skipValidate {
|
||||
return true
|
||||
}
|
||||
timestamp := srv.Query("timestamp")
|
||||
nonce := srv.Query("nonce")
|
||||
signature := srv.Query("signature")
|
||||
log.Debugf("validate signature, timestamp=%s, nonce=%s", timestamp, nonce)
|
||||
return signature == util.Signature(srv.Token, timestamp, nonce)
|
||||
}
|
||||
|
||||
@@ -104,7 +109,7 @@ func (srv *Server) handleRequest() (reply *message.Reply, err error) {
|
||||
if !success {
|
||||
err = errors.New("消息类型转换失败")
|
||||
}
|
||||
srv.requestMsg = mixMessage
|
||||
srv.RequestMsg = mixMessage
|
||||
reply = srv.messageHandler(mixMessage)
|
||||
return
|
||||
}
|
||||
@@ -150,7 +155,7 @@ func (srv *Server) getMessage() (interface{}, error) {
|
||||
}
|
||||
}
|
||||
|
||||
srv.requestRawXMLMsg = rawXMLMsgBytes
|
||||
srv.RequestRawXMLMsg = rawXMLMsgBytes
|
||||
|
||||
return srv.parseRequestMessage(rawXMLMsgBytes)
|
||||
}
|
||||
@@ -199,10 +204,10 @@ func (srv *Server) buildResponse(reply *message.Reply) (err error) {
|
||||
}
|
||||
|
||||
params := make([]reflect.Value, 1)
|
||||
params[0] = reflect.ValueOf(srv.requestMsg.FromUserName)
|
||||
params[0] = reflect.ValueOf(srv.RequestMsg.FromUserName)
|
||||
value.MethodByName("SetToUserName").Call(params)
|
||||
|
||||
params[0] = reflect.ValueOf(srv.requestMsg.ToUserName)
|
||||
params[0] = reflect.ValueOf(srv.RequestMsg.ToUserName)
|
||||
value.MethodByName("SetFromUserName").Call(params)
|
||||
|
||||
params[0] = reflect.ValueOf(msgType)
|
||||
@@ -211,18 +216,19 @@ func (srv *Server) buildResponse(reply *message.Reply) (err error) {
|
||||
params[0] = reflect.ValueOf(util.GetCurrTs())
|
||||
value.MethodByName("SetCreateTime").Call(params)
|
||||
|
||||
srv.responseMsg = msgData
|
||||
srv.responseRawXMLMsg, err = xml.Marshal(msgData)
|
||||
srv.ResponseMsg = msgData
|
||||
srv.ResponseRawXMLMsg, err = xml.Marshal(msgData)
|
||||
return
|
||||
}
|
||||
|
||||
//Send 将自定义的消息发送
|
||||
func (srv *Server) Send() (err error) {
|
||||
replyMsg := srv.responseMsg
|
||||
replyMsg := srv.ResponseMsg
|
||||
log.Debugf("response msg =%+v", replyMsg)
|
||||
if srv.isSafeMode {
|
||||
//安全模式下对消息进行加密
|
||||
var encryptedMsg []byte
|
||||
encryptedMsg, err = util.EncryptMsg(srv.random, srv.responseRawXMLMsg, srv.AppID, srv.EncodingAESKey)
|
||||
encryptedMsg, err = util.EncryptMsg(srv.random, srv.ResponseRawXMLMsg, srv.AppID, srv.EncodingAESKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
58
officialaccount/server/util.go
Normal file
58
officialaccount/server/util.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var xmlContentType = []string{"application/xml; charset=utf-8"}
|
||||
var plainContentType = []string{"text/plain; charset=utf-8"}
|
||||
|
||||
func writeContextType(w http.ResponseWriter, value []string) {
|
||||
header := w.Header()
|
||||
if val := header["Content-Type"]; len(val) == 0 {
|
||||
header["Content-Type"] = value
|
||||
}
|
||||
}
|
||||
|
||||
//Render render from bytes
|
||||
func (srv *Server) Render(bytes []byte) {
|
||||
//debug
|
||||
//fmt.Println("response msg = ", string(bytes))
|
||||
srv.Writer.WriteHeader(200)
|
||||
_, err := srv.Writer.Write(bytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
//String render from string
|
||||
func (srv *Server) String(str string) {
|
||||
writeContextType(srv.Writer, plainContentType)
|
||||
srv.Render([]byte(str))
|
||||
}
|
||||
|
||||
//XML render to xml
|
||||
func (srv *Server) XML(obj interface{}) {
|
||||
writeContextType(srv.Writer, xmlContentType)
|
||||
bytes, err := xml.Marshal(obj)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
srv.Render(bytes)
|
||||
}
|
||||
|
||||
// Query returns the keyed url query value if it exists
|
||||
func (srv *Server) Query(key string) string {
|
||||
value, _ := srv.GetQuery(key)
|
||||
return value
|
||||
}
|
||||
|
||||
// GetQuery is like Query(), it returns the keyed url query value
|
||||
func (srv *Server) GetQuery(key string) (string, bool) {
|
||||
req := srv.Request
|
||||
if values, ok := req.URL.Query()[key]; ok && len(values) > 0 {
|
||||
return values[0], true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -44,7 +44,7 @@ type Info struct {
|
||||
UnionID string `json:"unionid"`
|
||||
Remark string `json:"remark"`
|
||||
GroupID int32 `json:"groupid"`
|
||||
TagidList []int32 `json:"tagid_list"`
|
||||
TagIDList []int32 `json:"tagid_list"`
|
||||
SubscribeScene string `json:"subscribe_scene"`
|
||||
QrScene int `json:"qr_scene"`
|
||||
QrSceneStr string `json:"qr_scene_str"`
|
||||
63
openplatform/README.md
Normal file
63
openplatform/README.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# 微信开放平台
|
||||
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Third_party_platform_appid.html)
|
||||
|
||||
## 快速入门
|
||||
|
||||
### 服务端处理
|
||||
```go
|
||||
wc := wechat.NewWechat()
|
||||
memory := cache.NewMemory()
|
||||
cfg := &openplatform.Config{
|
||||
AppID: "xxx",
|
||||
AppSecret: "xxx",
|
||||
Token: "xxx",
|
||||
EncodingAESKey: "xxx",
|
||||
Cache: memory,
|
||||
}
|
||||
|
||||
|
||||
openPlatform := wc.GetOpenPlatform(cfg)
|
||||
// 传入request和responseWriter
|
||||
server := openPlatform.GetServer(req, rw)
|
||||
//设置接收消息的处理方法
|
||||
server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {
|
||||
if msg.InfoType == message.InfoTypeVerifyTicket {
|
||||
componentVerifyTicket, err := openPlatform.SetComponentAccessToken(msg.ComponentVerifyTicket)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil
|
||||
}
|
||||
//debug
|
||||
fmt.Println(componentVerifyTicket)
|
||||
rw.Write([]byte("success"))
|
||||
return nil
|
||||
}
|
||||
//handle other message
|
||||
//
|
||||
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
//处理消息接收以及回复
|
||||
err := server.Serve()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
//发送回复的消息
|
||||
server.Send()
|
||||
|
||||
|
||||
```
|
||||
### 待授权处理消息
|
||||
```go
|
||||
|
||||
//授权的第三方公众号的appID
|
||||
appID := "xxx"
|
||||
openPlatform := wc.GetOpenPlatform(cfg)
|
||||
openPlatform.GetOfficialAccount(appID)
|
||||
|
||||
```
|
||||
34
openplatform/account/account.go
Normal file
34
openplatform/account/account.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package account
|
||||
|
||||
import "github.com/silenceper/wechat/v2/openplatform/context"
|
||||
|
||||
//Account 开放平台张哈管理
|
||||
//TODO 实现方法
|
||||
type Account struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
//NewAccount new
|
||||
func NewAccount(ctx *context.Context) *Account {
|
||||
return &Account{ctx}
|
||||
}
|
||||
|
||||
//Create 创建开放平台帐号并绑定公众号/小程序
|
||||
func (account *Account) Create(appID string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
//Bind 将公众号/小程序绑定到开放平台帐号下
|
||||
func (account *Account) Bind(appID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//Unbind 将公众号/小程序从开放平台帐号下解绑
|
||||
func (account *Account) Unbind(appID string, openAppID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//Get 获取公众号/小程序所绑定的开放平台帐号
|
||||
func (account *Account) Get(appID string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
14
openplatform/config/config.go
Normal file
14
openplatform/config/config.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/v2/cache"
|
||||
)
|
||||
|
||||
//Config config for 微信开放平台
|
||||
type Config struct {
|
||||
AppID string `json:"app_id"` //appid
|
||||
AppSecret string `json:"app_secret"` //appsecret
|
||||
Token string `json:"token"` //token
|
||||
EncodingAESKey string `json:"encoding_aes_key"` //EncodingAESKey
|
||||
Cache cache.Cache
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -14,7 +14,10 @@ const (
|
||||
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"
|
||||
//TODO 获取授权方选项信息
|
||||
getComponentConfigURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option?component_access_token=%s"
|
||||
//TODO 获取已授权的账号信息
|
||||
getuthorizerListURL = "POST https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_list?component_access_token=%s"
|
||||
)
|
||||
|
||||
// ComponentAccessToken 第三方平台
|
||||
@@ -52,7 +55,9 @@ func (ctx *Context) SetComponentAccessToken(verifyTicket string) (*ComponentAcce
|
||||
|
||||
accessTokenCacheKey := fmt.Sprintf("component_access_token_%s", ctx.AppID)
|
||||
expires := at.ExpiresIn - 1500
|
||||
ctx.Cache.Set(accessTokenCacheKey, at.AccessToken, time.Duration(expires)*time.Second)
|
||||
if err := ctx.Cache.Set(accessTokenCacheKey, at.AccessToken, time.Duration(expires)*time.Second); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return at, nil
|
||||
}
|
||||
|
||||
@@ -157,8 +162,9 @@ func (ctx *Context) RefreshAuthrToken(appid, refreshToken string) (*AuthrAccessT
|
||||
}
|
||||
|
||||
authrTokenKey := "authorizer_access_token_" + appid
|
||||
ctx.Cache.Set(authrTokenKey, ret.AccessToken, time.Minute*80)
|
||||
|
||||
if err := ctx.Cache.Set(authrTokenKey, ret.AccessToken, time.Minute*80); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
10
openplatform/context/context.go
Normal file
10
openplatform/context/context.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/v2/openplatform/config"
|
||||
)
|
||||
|
||||
// Context struct
|
||||
type Context struct {
|
||||
*config.Config
|
||||
}
|
||||
52
openplatform/miniprogram/basic/basic.go
Normal file
52
openplatform/miniprogram/basic/basic.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
openContext "github.com/silenceper/wechat/v2/openplatform/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
getAccountBasicInfoURL = "https://api.weixin.qq.com/cgi-bin/account/getaccountbasicinfo"
|
||||
)
|
||||
|
||||
//Basic 基础信息设置
|
||||
type Basic struct {
|
||||
*openContext.Context
|
||||
appID string
|
||||
}
|
||||
|
||||
//NewBasic new
|
||||
func NewBasic(opContext *openContext.Context, appID string) *Basic {
|
||||
return &Basic{Context: opContext, appID: appID}
|
||||
}
|
||||
|
||||
//AccountBasicInfo 基础信息
|
||||
type AccountBasicInfo struct {
|
||||
util.CommonError
|
||||
}
|
||||
|
||||
//GetAccountBasicInfo 获取小程序基础信息
|
||||
//reference:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Mini_Programs/Mini_Program_Information_Settings.html
|
||||
func (basic *Basic) GetAccountBasicInfo() (*AccountBasicInfo, error) {
|
||||
ak, err := basic.GetAuthrAccessToken(basic.AppID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := fmt.Sprintf("%s?access_token=%s", getAccountBasicInfoURL, ak)
|
||||
data, err := util.HTTPGet(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &AccountBasicInfo{}
|
||||
if err := util.DecodeWithError(data, result, "account/getaccountbasicinfo"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
//modify_domain设置服务器域名
|
||||
//TODO
|
||||
//func (encryptor *Basic) modifyDomain() {
|
||||
//}
|
||||
69
openplatform/miniprogram/component/component.go
Normal file
69
openplatform/miniprogram/component/component.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package component
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
openContext "github.com/silenceper/wechat/v2/openplatform/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
fastregisterweappURL = "https://api.weixin.qq.com/cgi-bin/component/fastregisterweapp"
|
||||
)
|
||||
|
||||
//Component 快速创建小程序
|
||||
type Component struct {
|
||||
*openContext.Context
|
||||
}
|
||||
|
||||
//NewComponent new
|
||||
func NewComponent(opContext *openContext.Context) *Component {
|
||||
return &Component{opContext}
|
||||
}
|
||||
|
||||
//RegisterMiniProgramParam 快速注册小程序参数
|
||||
type RegisterMiniProgramParam struct {
|
||||
Name string `json:"name"` //企业名
|
||||
Code string `json:"code"` //企业代码
|
||||
CodeType string `json:"code_type"` //企业代码类型 1:统一社会信用代码(18 位) 2:组织机构代码(9 位 xxxxxxxx-x) 3:营业执照注册号(15 位)
|
||||
LegalPersonaWechat string `json:"legal_persona_wechat"` //法人微信号
|
||||
LegalPersonaName string `json:"legal_persona_name"` //法人姓名(绑定银行卡)
|
||||
ComponentPhone string `json:"component_phone"` //第三方联系电话(方便法人与第三方联系)
|
||||
}
|
||||
|
||||
//RegisterMiniProgram 快速创建小程
|
||||
//reference: https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Mini_Programs/Fast_Registration_Interface_document.html
|
||||
func (component *Component) RegisterMiniProgram(param *RegisterMiniProgramParam) error {
|
||||
componentAK, err := component.GetComponentAccessToken()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
url := fmt.Sprintf(fastregisterweappURL+"?action=create&component_access_token=%s", componentAK)
|
||||
data, err := util.PostJSON(url, param)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(data, "component/fastregisterweapp?action=create")
|
||||
}
|
||||
|
||||
//GetRegistrationStatusParam 查询任务创建状态
|
||||
type GetRegistrationStatusParam struct {
|
||||
Name string `json:"name"` //企业名
|
||||
LegalPersonaWechat string `json:"legal_persona_wechat"` //法人微信号
|
||||
LegalPersonaName string `json:"legal_persona_name"` //法人姓名(绑定银行卡)
|
||||
|
||||
}
|
||||
|
||||
//GetRegistrationStatus 查询创建任务状态.
|
||||
func (component *Component) GetRegistrationStatus(param *GetRegistrationStatusParam) error {
|
||||
componentAK, err := component.GetComponentAccessToken()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
url := fmt.Sprintf(fastregisterweappURL+"?action=search&component_access_token=%s", componentAK)
|
||||
data, err := util.PostJSON(url, param)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(data, "component/fastregisterweapp?action=search")
|
||||
}
|
||||
32
openplatform/miniprogram/miniprogram.go
Normal file
32
openplatform/miniprogram/miniprogram.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package miniprogram
|
||||
|
||||
import (
|
||||
openContext "github.com/silenceper/wechat/v2/openplatform/context"
|
||||
"github.com/silenceper/wechat/v2/openplatform/miniprogram/basic"
|
||||
"github.com/silenceper/wechat/v2/openplatform/miniprogram/component"
|
||||
)
|
||||
|
||||
//MiniProgram 代小程序实现业务
|
||||
type MiniProgram struct {
|
||||
AppID string
|
||||
openContext *openContext.Context
|
||||
}
|
||||
|
||||
//NewMiniProgram 实例化
|
||||
func NewMiniProgram(opCtx *openContext.Context, appID string) *MiniProgram {
|
||||
return &MiniProgram{
|
||||
openContext: opCtx,
|
||||
AppID: appID,
|
||||
}
|
||||
}
|
||||
|
||||
//GetComponent get component
|
||||
//快速注册小程序相关
|
||||
func (miniProgram *MiniProgram) GetComponent() *component.Component {
|
||||
return component.NewComponent(miniProgram.openContext)
|
||||
}
|
||||
|
||||
//GetBasic 基础信息设置
|
||||
func (miniProgram *MiniProgram) GetBasic() *basic.Basic {
|
||||
return basic.NewBasic(miniProgram.openContext, miniProgram.AppID)
|
||||
}
|
||||
49
openplatform/officialaccount/officialaccount.go
Normal file
49
openplatform/officialaccount/officialaccount.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package officialaccount
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/v2/credential"
|
||||
"github.com/silenceper/wechat/v2/officialaccount"
|
||||
offConfig "github.com/silenceper/wechat/v2/officialaccount/config"
|
||||
opContext "github.com/silenceper/wechat/v2/openplatform/context"
|
||||
)
|
||||
|
||||
//OfficialAccount 代公众号实现业务
|
||||
type OfficialAccount struct {
|
||||
//授权的公众号的appID
|
||||
appID string
|
||||
*officialaccount.OfficialAccount
|
||||
opContext *opContext.Context
|
||||
}
|
||||
|
||||
//NewOfficialAccount 实例化
|
||||
//appID :为授权方公众号 APPID,非开放平台第三方平台 APPID
|
||||
func NewOfficialAccount(opCtx *opContext.Context, appID string) *OfficialAccount {
|
||||
officialAccount := officialaccount.NewOfficialAccount(&offConfig.Config{
|
||||
AppID: opCtx.AppID,
|
||||
EncodingAESKey: opCtx.EncodingAESKey,
|
||||
Token: opCtx.Token,
|
||||
Cache: opCtx.Cache,
|
||||
})
|
||||
//设置获取access_token的函数
|
||||
officialAccount.SetAccessTokenHandle(NewDefaultAuthrAccessToken(opCtx, appID))
|
||||
return &OfficialAccount{appID: appID, OfficialAccount: officialAccount}
|
||||
}
|
||||
|
||||
//DefaultAuthrAccessToken 默认获取授权ak的方法
|
||||
type DefaultAuthrAccessToken struct {
|
||||
opCtx *opContext.Context
|
||||
appID string
|
||||
}
|
||||
|
||||
//NewDefaultAuthrAccessToken New
|
||||
func NewDefaultAuthrAccessToken(opCtx *opContext.Context, appID string) credential.AccessTokenHandle {
|
||||
return &DefaultAuthrAccessToken{
|
||||
opCtx: opCtx,
|
||||
appID: appID,
|
||||
}
|
||||
}
|
||||
|
||||
//GetAccessToken 获取ak
|
||||
func (ak *DefaultAuthrAccessToken) GetAccessToken() (string, error) {
|
||||
return ak.opCtx.GetAuthrAccessToken(ak.appID)
|
||||
}
|
||||
50
openplatform/openplatform.go
Normal file
50
openplatform/openplatform.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package openplatform
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/silenceper/wechat/v2/officialaccount/server"
|
||||
"github.com/silenceper/wechat/v2/openplatform/account"
|
||||
"github.com/silenceper/wechat/v2/openplatform/config"
|
||||
"github.com/silenceper/wechat/v2/openplatform/context"
|
||||
"github.com/silenceper/wechat/v2/openplatform/miniprogram"
|
||||
"github.com/silenceper/wechat/v2/openplatform/officialaccount"
|
||||
)
|
||||
|
||||
//OpenPlatform 微信开放平台相关api
|
||||
type OpenPlatform struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
//NewOpenPlatform new openplatform
|
||||
func NewOpenPlatform(cfg *config.Config) *OpenPlatform {
|
||||
if cfg.Cache == nil {
|
||||
panic("cache 未设置")
|
||||
}
|
||||
ctx := &context.Context{
|
||||
Config: cfg,
|
||||
}
|
||||
return &OpenPlatform{ctx}
|
||||
}
|
||||
|
||||
//GetServer get server
|
||||
func (openPlatform *OpenPlatform) GetServer(req *http.Request, writer http.ResponseWriter) *server.Server {
|
||||
off := officialaccount.NewOfficialAccount(openPlatform.Context, "")
|
||||
return off.GetServer(req, writer)
|
||||
}
|
||||
|
||||
//GetOfficialAccount 公众号代处理
|
||||
func (openPlatform *OpenPlatform) GetOfficialAccount(appID string) *officialaccount.OfficialAccount {
|
||||
return officialaccount.NewOfficialAccount(openPlatform.Context, appID)
|
||||
}
|
||||
|
||||
//GetMiniProgram 小程序代理
|
||||
func (openPlatform *OpenPlatform) GetMiniProgram(appID string) *miniprogram.MiniProgram {
|
||||
return miniprogram.NewMiniProgram(openPlatform.Context, appID)
|
||||
}
|
||||
|
||||
//GetAccountManager 账号管理
|
||||
//TODO
|
||||
func (openPlatform *OpenPlatform) GetAccountManager() *account.Account {
|
||||
return account.NewAccount(openPlatform.Context)
|
||||
}
|
||||
5
pay/README.md
Normal file
5
pay/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 微信支付
|
||||
|
||||
[官方文档](https://pay.weixin.qq.com/wiki/doc/api/index.html)
|
||||
|
||||
## 快速入门
|
||||
9
pay/config/config.go
Normal file
9
pay/config/config.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
//Config config for pay
|
||||
type Config struct {
|
||||
AppID string `json:"app_id"`
|
||||
MchID string `json:"mch_id"`
|
||||
Key string `json:"key"`
|
||||
NotifyURL string `json:"notify_url"`
|
||||
}
|
||||
15
pay/notify/notify.go
Normal file
15
pay/notify/notify.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/v2/pay/config"
|
||||
)
|
||||
|
||||
//Notify 回调
|
||||
type Notify struct {
|
||||
*config.Config
|
||||
}
|
||||
|
||||
//NewNotify new
|
||||
func NewNotify(cfg *config.Config) *Notify {
|
||||
return &Notify{cfg}
|
||||
}
|
||||
108
pay/notify/paid.go
Normal file
108
pay/notify/paid.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// doc: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7&index=8
|
||||
|
||||
// PaidResult 下单回调
|
||||
type PaidResult struct {
|
||||
ReturnCode *string `xml:"return_code"`
|
||||
ReturnMsg *string `xml:"return_msg"`
|
||||
|
||||
AppID *string `xml:"appid" json:"appid"`
|
||||
MchID *string `xml:"mch_id"`
|
||||
DeviceInfo *string `xml:"device_info"`
|
||||
NonceStr *string `xml:"nonce_str"`
|
||||
Sign *string `xml:"sign"`
|
||||
SignType *string `xml:"sign_type"`
|
||||
ResultCode *string `xml:"result_code"`
|
||||
ErrCode *string `xml:"err_code"`
|
||||
ErrCodeDes *string `xml:"err_code_des"`
|
||||
OpenID *string `xml:"openid"`
|
||||
IsSubscribe *string `xml:"is_subscribe"`
|
||||
TradeType *string `xml:"trade_type"`
|
||||
BankType *string `xml:"bank_type"`
|
||||
TotalFee *int `xml:"total_fee"`
|
||||
SettlementTotalFee *int `xml:"settlement_total_fee"`
|
||||
FeeType *string `xml:"fee_type"`
|
||||
CashFee *string `xml:"cash_fee"`
|
||||
CashFeeType *string `xml:"cash_fee_type"`
|
||||
CouponFee *int `xml:"coupon_fee"`
|
||||
CouponCount *int `xml:"coupon_count"`
|
||||
|
||||
// coupon_type_$n 这里只声明 3 个,如果有更多的可以自己组合
|
||||
CouponType0 *string `xml:"coupon_type_0"`
|
||||
CouponType1 *string `xml:"coupon_type_1"`
|
||||
CouponType2 *string `xml:"coupon_type_2"`
|
||||
CouponID0 *string `xml:"coupon_id_0"`
|
||||
CouponID1 *string `xml:"coupon_id_1"`
|
||||
CouponID2 *string `xml:"coupon_id_2"`
|
||||
CouponFeed0 *string `xml:"coupon_fee_0"`
|
||||
CouponFeed1 *string `xml:"coupon_fee_1"`
|
||||
CouponFeed2 *string `xml:"coupon_fee_2"`
|
||||
|
||||
TransactionID *string `xml:"transaction_id"`
|
||||
OutTradeNo *string `xml:"out_trade_no"`
|
||||
Attach *string `xml:"attach"`
|
||||
TimeEnd *string `xml:"time_end"`
|
||||
}
|
||||
|
||||
// PaidResp 消息通知返回
|
||||
type PaidResp struct {
|
||||
ReturnCode string `xml:"return_code"`
|
||||
ReturnMsg string `xml:"return_msg"`
|
||||
}
|
||||
|
||||
// PaidVerifySign 支付成功结果验签
|
||||
func (notify *Notify) PaidVerifySign(notifyRes PaidResult) bool {
|
||||
// STEP1, 转换 struct 为 map,并对 map keys 做排序
|
||||
resMap := structs.Map(notifyRes)
|
||||
|
||||
sortedKeys := make([]string, 0, len(resMap))
|
||||
for k := range resMap {
|
||||
sortedKeys = append(sortedKeys, k)
|
||||
}
|
||||
sort.Strings(sortedKeys)
|
||||
|
||||
// STEP2, 对key=value的键值对用&连接起来,略过空值 & sign
|
||||
var signStrings string
|
||||
for _, k := range sortedKeys {
|
||||
value := fmt.Sprintf("%v", cast.ToString(resMap[k]))
|
||||
if value != "" && strings.ToLower(k) != "sign" {
|
||||
signStrings = signStrings + getTagKeyName(k, ¬ifyRes) + "=" + value + "&"
|
||||
}
|
||||
}
|
||||
|
||||
// STEP3, 在键值对的最后加上key=API_KEY
|
||||
signStrings = signStrings + "key=" + notify.Key
|
||||
|
||||
// STEP4, 根据SignType计算出签名
|
||||
var signType string
|
||||
if notifyRes.SignType != nil {
|
||||
signType = *notifyRes.SignType
|
||||
}
|
||||
sign, err := util.CalculateSign(signStrings, signType, notify.Key)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if sign != *notifyRes.Sign {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getTagKeyName(key string, notifyRes *PaidResult) string {
|
||||
s := reflect.TypeOf(notifyRes).Elem()
|
||||
f, _ := s.FieldByName(key)
|
||||
name := f.Tag.Get("xml")
|
||||
return name
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
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
|
||||
}
|
||||
216
pay/order/pay.go
Normal file
216
pay/order/pay.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/v2/pay/config"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
//https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
|
||||
var payGateway = "https://api.mch.weixin.qq.com/pay/unifiedorder"
|
||||
|
||||
// Order struct extends context
|
||||
type Order struct {
|
||||
*config.Config
|
||||
}
|
||||
|
||||
// NewOrder return an instance of order package
|
||||
func NewOrder(cfg *config.Config) *Order {
|
||||
order := Order{cfg}
|
||||
return &order
|
||||
}
|
||||
|
||||
// Params was NEEDED when request unifiedorder
|
||||
// 传入的参数,用于生成 prepay_id 的必需参数
|
||||
type Params struct {
|
||||
TotalFee string
|
||||
CreateIP string
|
||||
Body string
|
||||
OutTradeNo string
|
||||
OpenID string
|
||||
TradeType string
|
||||
SignType string
|
||||
Detail string
|
||||
Attach string
|
||||
GoodsTag string
|
||||
NotifyURL string
|
||||
}
|
||||
|
||||
// Config 是传出用于 js sdk 用的参数
|
||||
type Config struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
NonceStr string `json:"nonceStr"`
|
||||
PrePayID string `json:"prePayId"`
|
||||
SignType string `json:"signType"`
|
||||
Package string `json:"package"`
|
||||
PaySign string `json:"paySign"`
|
||||
}
|
||||
|
||||
// PreOrder 是 unifie order 接口的返回
|
||||
type PreOrder 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"`
|
||||
TradeType string `xml:"trade_type,omitempty"`
|
||||
PrePayID string `xml:"prepay_id,omitempty"`
|
||||
CodeURL string `xml:"code_url,omitempty"`
|
||||
ErrCode string `xml:"err_code,omitempty"`
|
||||
ErrCodeDes string `xml:"err_code_des,omitempty"`
|
||||
}
|
||||
|
||||
// payRequest 接口请求参数
|
||||
type payRequest struct {
|
||||
AppID string `xml:"appid"`
|
||||
MchID string `xml:"mch_id"`
|
||||
DeviceInfo string `xml:"device_info,omitempty"`
|
||||
NonceStr string `xml:"nonce_str"`
|
||||
Sign string `xml:"sign"`
|
||||
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
|
||||
LimitPay string `xml:"limit_pay,omitempty"` //
|
||||
OpenID string `xml:"openid,omitempty"` // 用户标识
|
||||
SceneInfo string `xml:"scene_info,omitempty"` // 场景信息
|
||||
|
||||
XMLName struct{} `xml:"xml"`
|
||||
}
|
||||
|
||||
// BridgeConfig get js bridge config
|
||||
func (o *Order) BridgeConfig(p *Params) (cfg Config, err error) {
|
||||
var (
|
||||
buffer strings.Builder
|
||||
timestamp = strconv.FormatInt(time.Now().Unix(), 10)
|
||||
)
|
||||
order, err := o.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(o.Key)
|
||||
|
||||
sign, err := util.CalculateSign(buffer.String(), p.SignType, o.Key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 签名
|
||||
cfg.PaySign = sign
|
||||
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 (o *Order) PrePayOrder(p *Params) (payOrder PreOrder, err error) {
|
||||
nonceStr := util.RandomStr(32)
|
||||
notifyURL := o.NotifyURL
|
||||
// 签名类型
|
||||
if p.SignType == "" {
|
||||
p.SignType = util.SignTypeMD5
|
||||
}
|
||||
// 通知地址
|
||||
if p.NotifyURL != "" {
|
||||
notifyURL = p.NotifyURL
|
||||
}
|
||||
param := make(map[string]string)
|
||||
param["appid"] = o.AppID
|
||||
param["body"] = p.Body
|
||||
param["mch_id"] = o.MchID
|
||||
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
|
||||
|
||||
sign, err := util.ParamSign(param, o.Key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
request := payRequest{
|
||||
AppID: o.AppID,
|
||||
MchID: o.MchID,
|
||||
NonceStr: nonceStr,
|
||||
Sign: sign,
|
||||
Body: p.Body,
|
||||
OutTradeNo: p.OutTradeNo,
|
||||
TotalFee: p.TotalFee,
|
||||
SpbillCreateIP: p.CreateIP,
|
||||
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
|
||||
}
|
||||
err = xml.Unmarshal(rawRet, &payOrder)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if payOrder.ReturnCode == "SUCCESS" {
|
||||
// pay success
|
||||
if payOrder.ResultCode == "SUCCESS" {
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
err = errors.New(payOrder.ErrCode + payOrder.ErrCodeDes)
|
||||
return
|
||||
}
|
||||
err = errors.New("[msg : xmlUnmarshalError] [rawReturn : " + string(rawRet) + "] [sign : " + sign + "]")
|
||||
return
|
||||
}
|
||||
|
||||
// PrePayID will request wechat merchant api and request for a pre payment order id
|
||||
func (o *Order) PrePayID(p *Params) (prePayID string, err error) {
|
||||
order, err := o.PrePayOrder(p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if order.PrePayID == "" {
|
||||
err = errors.New("empty prepayid")
|
||||
}
|
||||
prePayID = order.PrePayID
|
||||
return
|
||||
}
|
||||
282
pay/pay.go
282
pay/pay.go
@@ -1,281 +1,27 @@
|
||||
package pay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"hash"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/silenceper/wechat/v2/pay/config"
|
||||
"github.com/silenceper/wechat/v2/pay/notify"
|
||||
"github.com/silenceper/wechat/v2/pay/order"
|
||||
)
|
||||
|
||||
var payGateway = "https://api.mch.weixin.qq.com/pay/unifiedorder"
|
||||
|
||||
// Pay struct extends context
|
||||
//Pay 微信支付相关API
|
||||
type Pay struct {
|
||||
*context.Context
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// Params was NEEDED when request unifiedorder
|
||||
// 传入的参数,用于生成 prepay_id 的必需参数
|
||||
type Params struct {
|
||||
TotalFee string
|
||||
CreateIP string
|
||||
Body string
|
||||
OutTradeNo string
|
||||
OpenID string
|
||||
TradeType string
|
||||
SignType string
|
||||
Detail string
|
||||
Attach string
|
||||
GoodsTag string
|
||||
NotifyURL string
|
||||
//NewPay 实例化微信支付相关API
|
||||
func NewPay(cfg *config.Config) *Pay {
|
||||
return &Pay{cfg}
|
||||
}
|
||||
|
||||
// Config 是传出用于 js sdk 用的参数
|
||||
type Config struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
NonceStr string `json:"nonceStr"`
|
||||
PrePayID string `json:"prePayId"`
|
||||
SignType string `json:"signType"`
|
||||
Package string `json:"package"`
|
||||
PaySign string `json:"paySign"`
|
||||
// GetOrder 下单
|
||||
func (pay *Pay) GetOrder() *order.Order {
|
||||
return order.NewOrder(pay.cfg)
|
||||
}
|
||||
|
||||
// PreOrder 是 unifie order 接口的返回
|
||||
type PreOrder 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"`
|
||||
TradeType string `xml:"trade_type,omitempty"`
|
||||
PrePayID string `xml:"prepay_id,omitempty"`
|
||||
CodeURL string `xml:"code_url,omitempty"`
|
||||
ErrCode string `xml:"err_code,omitempty"`
|
||||
ErrCodeDes string `xml:"err_code_des,omitempty"`
|
||||
}
|
||||
|
||||
// payRequest 接口请求参数
|
||||
type payRequest struct {
|
||||
AppID string `xml:"appid"`
|
||||
MchID string `xml:"mch_id"`
|
||||
DeviceInfo string `xml:"device_info,omitempty"`
|
||||
NonceStr string `xml:"nonce_str"`
|
||||
Sign string `xml:"sign"`
|
||||
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
|
||||
LimitPay string `xml:"limit_pay,omitempty"` //
|
||||
OpenID string `xml:"openid,omitempty"` // 用户标识
|
||||
SceneInfo string `xml:"scene_info,omitempty"` // 场景信息
|
||||
}
|
||||
|
||||
// NewPay return an instance of Pay package
|
||||
func NewPay(ctx *context.Context) *Pay {
|
||||
pay := Pay{Context: ctx}
|
||||
return &pay
|
||||
}
|
||||
|
||||
// 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)
|
||||
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,
|
||||
MchID: pcf.PayMchID,
|
||||
NonceStr: nonceStr,
|
||||
Sign: sign,
|
||||
Body: p.Body,
|
||||
OutTradeNo: p.OutTradeNo,
|
||||
TotalFee: p.TotalFee,
|
||||
SpbillCreateIP: p.CreateIP,
|
||||
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
|
||||
}
|
||||
err = xml.Unmarshal(rawRet, &payOrder)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if payOrder.ReturnCode == "SUCCESS" {
|
||||
// pay success
|
||||
if payOrder.ResultCode == "SUCCESS" {
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
err = errors.New(payOrder.ErrCode + payOrder.ErrCodeDes)
|
||||
return
|
||||
}
|
||||
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
|
||||
// GetNotify 通知
|
||||
func (pay *Pay) GetNotify() *notify.Notify {
|
||||
return notify.NewNotify(pay.cfg)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
package pay
|
||||
package refund
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/silenceper/wechat/v2/pay/config"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
var refundGateway = "https://api.mch.weixin.qq.com/secapi/pay/refund"
|
||||
|
||||
//RefundParams 调用参数
|
||||
type RefundParams struct {
|
||||
// Refund struct extends context
|
||||
type Refund struct {
|
||||
*config.Config
|
||||
}
|
||||
|
||||
// NewRefund return an instance of refund package
|
||||
func NewRefund(cfg *config.Config) *Refund {
|
||||
refund := Refund{cfg}
|
||||
return &refund
|
||||
}
|
||||
|
||||
//Params 调用参数
|
||||
type Params struct {
|
||||
TransactionID string
|
||||
OutRefundNo string
|
||||
TotalFee string
|
||||
@@ -19,8 +31,8 @@ type RefundParams struct {
|
||||
RootCa string //ca证书
|
||||
}
|
||||
|
||||
//refundRequest 接口请求参数
|
||||
type refundRequest struct {
|
||||
//request 接口请求参数
|
||||
type request struct {
|
||||
AppID string `xml:"appid"`
|
||||
MchID string `xml:"mch_id"`
|
||||
NonceStr string `xml:"nonce_str"`
|
||||
@@ -34,8 +46,8 @@ type refundRequest struct {
|
||||
//NotifyUrl string `xml:"notify_url,omitempty"`
|
||||
}
|
||||
|
||||
//RefundResponse 接口返回
|
||||
type RefundResponse struct {
|
||||
//Response 接口返回
|
||||
type Response struct {
|
||||
ReturnCode string `xml:"return_code"`
|
||||
ReturnMsg string `xml:"return_msg"`
|
||||
AppID string `xml:"appid,omitempty"`
|
||||
@@ -59,35 +71,37 @@ type RefundResponse struct {
|
||||
}
|
||||
|
||||
//Refund 退款申请
|
||||
func (pcf *Pay) Refund(p *RefundParams) (rsp RefundResponse, err error) {
|
||||
func (refund *Refund) Refund(p *Params) (rsp Response, err error) {
|
||||
nonceStr := util.RandomStr(32)
|
||||
param := make(map[string]interface{})
|
||||
param["appid"] = pcf.AppID
|
||||
param["mch_id"] = pcf.PayMchID
|
||||
param := make(map[string]string)
|
||||
param["appid"] = refund.AppID
|
||||
param["mch_id"] = refund.MchID
|
||||
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["sign_type"] = util.SignTypeMD5
|
||||
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,
|
||||
sign, err := util.ParamSign(param, refund.Key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
request := request{
|
||||
AppID: refund.AppID,
|
||||
MchID: refund.MchID,
|
||||
NonceStr: nonceStr,
|
||||
Sign: sign,
|
||||
SignType: "MD5",
|
||||
SignType: util.SignTypeMD5,
|
||||
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)
|
||||
rawRet, err := util.PostXMLWithTLS(refundGateway, request, p.RootCa, refund.MchID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -103,7 +117,6 @@ func (pcf *Pay) Refund(p *RefundParams) (rsp RefundResponse, err error) {
|
||||
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)
|
||||
err = fmt.Errorf("[msg : xmlUnmarshalError] [rawReturn : %s] [sign : %s]", string(rawRet), sign)
|
||||
return
|
||||
}
|
||||
@@ -1,14 +1,23 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 微信签名算法方式
|
||||
const (
|
||||
SignTypeMD5 = `MD5`
|
||||
SignTypeHMACSHA256 = `HMAC-SHA256`
|
||||
)
|
||||
|
||||
//EncryptMsg 加密消息
|
||||
@@ -186,14 +195,35 @@ func decodeNetworkByteOrder(orderBytes []byte) (n uint32) {
|
||||
uint32(orderBytes[3])
|
||||
}
|
||||
|
||||
// MD5Sum 计算 32 位长度的 MD5 sum
|
||||
func MD5Sum(txt string) (sum string) {
|
||||
h := md5.New()
|
||||
buf := bufio.NewWriterSize(h, 128)
|
||||
buf.WriteString(txt)
|
||||
buf.Flush()
|
||||
sign := make([]byte, hex.EncodedLen(h.Size()))
|
||||
hex.Encode(sign, h.Sum(nil))
|
||||
sum = string(bytes.ToUpper(sign))
|
||||
return
|
||||
// CalculateSign 计算签名
|
||||
func CalculateSign(content, signType, key string) (string, error) {
|
||||
var h hash.Hash
|
||||
if signType == SignTypeHMACSHA256 {
|
||||
h = hmac.New(sha256.New, []byte(key))
|
||||
} else {
|
||||
h = md5.New()
|
||||
}
|
||||
|
||||
if _, err := h.Write([]byte(content)); err != nil {
|
||||
return ``, err
|
||||
}
|
||||
return strings.ToUpper(hex.EncodeToString(h.Sum(nil))), nil
|
||||
}
|
||||
|
||||
// ParamSign 计算所传参数的签名
|
||||
func ParamSign(p map[string]string, key string) (string, error) {
|
||||
bizKey := "&key=" + key
|
||||
str := OrderParam(p, bizKey)
|
||||
|
||||
var signType string
|
||||
switch p["sign_type"] {
|
||||
case SignTypeMD5, SignTypeHMACSHA256:
|
||||
signType = p["sign_type"]
|
||||
case ``:
|
||||
signType = SignTypeMD5
|
||||
default:
|
||||
return ``, errors.New(`invalid sign_type`)
|
||||
}
|
||||
|
||||
return CalculateSign(str, signType, key)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package util
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// CommonError 微信返回的通用错误json
|
||||
@@ -23,3 +24,28 @@ func DecodeWithCommonError(response []byte, apiName string) (err error) {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecodeWithError 将返回值按照解析
|
||||
func DecodeWithError(response []byte, obj interface{}, apiName string) error {
|
||||
err := json.Unmarshal(response, obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("json Unmarshal Error, err=%v", err)
|
||||
}
|
||||
responseObj := reflect.ValueOf(obj)
|
||||
if !responseObj.IsValid() {
|
||||
return fmt.Errorf("obj is invalid")
|
||||
}
|
||||
commonError := responseObj.Elem().FieldByName("CommonError")
|
||||
if !commonError.IsValid() || commonError.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("commonError is invalid or not struct")
|
||||
}
|
||||
errCode := commonError.FieldByName("ErrCode")
|
||||
errMsg := commonError.FieldByName("ErrMsg")
|
||||
if !errCode.IsValid() || !errMsg.IsValid() {
|
||||
return fmt.Errorf("errcode or errmsg is invalid")
|
||||
}
|
||||
if errCode.Int() != 0 {
|
||||
return fmt.Errorf("%s Error , errcode=%d , errmsg=%s", apiName, errCode.Int(), errMsg.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user