mirror of
https://github.com/silenceper/wechat.git
synced 2026-02-04 12:52:27 +08:00
规范目录
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -26,4 +26,4 @@ _testmain.go
|
|||||||
.vscode/
|
.vscode/
|
||||||
vendor
|
vendor
|
||||||
.idea/
|
.idea/
|
||||||
examples/tcb/*
|
example/*
|
||||||
649
README.md
649
README.md
@@ -1,647 +1,66 @@
|
|||||||
# WeChat SDK for Go
|
# WeChat SDK for Go
|
||||||
[](https://travis-ci.org/silenceper/wechat)
|
[](https://travis-ci.org/silenceper/wechat)
|
||||||
[](https://goreportcard.com/report/github.com/silenceper/wechat)
|
[](https://goreportcard.com/report/github.com/silenceper/wechat)
|
||||||
[](http://godoc.org/github.com/silenceper/wechat)
|
[](http://godoc.org/github.com/silenceper/wechat)
|
||||||
|
|
||||||
使用Golang开发的微信SDK,简单、易用。
|
使用Golang开发的微信SDK,简单、易用。
|
||||||
|
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
以下是一个处理消息接收以及回复的例子:
|
以下是一个微信公众号处理消息接收以及回复的例子:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
//使用memcache保存access_token,也可选择redis或自定义cache
|
//使用memcache保存access_token,也可选择redis或自定义cache
|
||||||
memCache=cache.NewMemcache("127.0.0.1:11211")
|
wc := wechat.NewWechat()
|
||||||
|
memory := cache.NewMemory()
|
||||||
//配置微信参数
|
cfg := &offConfig.Config{
|
||||||
config := &wechat.Config{
|
AppID: "xxx",
|
||||||
AppID: "xxxx",
|
AppSecret: "xxx",
|
||||||
AppSecret: "xxxx",
|
Token: "xxx",
|
||||||
Token: "xxxx",
|
//EncodingAESKey: "xxxx",
|
||||||
EncodingAESKey: "xxxx",
|
Cache: memory,
|
||||||
Cache: memCache
|
|
||||||
}
|
}
|
||||||
wc := wechat.NewWechat(config)
|
officialAccount := wc.GetOfficialAccount(cfg)
|
||||||
|
|
||||||
// 传入request和responseWriter
|
// 传入request和responseWriter
|
||||||
server := wc.GetServer(request, responseWriter)
|
server := officialAccount.GetServer(req, rw)
|
||||||
|
//设置接收消息的处理方法
|
||||||
server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {
|
server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {
|
||||||
|
|
||||||
//回复消息:演示回复用户发送的消息
|
//回复消息:演示回复用户发送的消息
|
||||||
text := message.NewText(msg.Content)
|
text := message.NewText(msg.Content)
|
||||||
return &message.Reply{message.MsgTypeText, text}
|
return &message.Reply{MsgType: message.MsgTypeText, MsgData: text}
|
||||||
})
|
})
|
||||||
|
|
||||||
server.Serve()
|
//处理消息接收以及回复
|
||||||
|
err := server.Serve()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//发送回复的消息
|
||||||
server.Send()
|
server.Send()
|
||||||
|
|
||||||
```
|
```
|
||||||
完整代码:[examples/http/http.go](./examples/http/http.go)
|
|
||||||
|
|
||||||
#### 和主流框架配合使用
|
|
||||||
|
|
||||||
主要是request和responseWriter在不同框架中获取方式可能不一样:
|
## 参与贡献
|
||||||
|
|
||||||
|
### 目录说明
|
||||||
|
- officialaccount: 微信公众号API
|
||||||
|
- miniprogram: 小程序API
|
||||||
|
- minigame:小游戏API
|
||||||
|
- pay:微信支付API
|
||||||
|
- opernplatform:开放平台API
|
||||||
|
- work:企业微信
|
||||||
|
- aispeech:智能对话
|
||||||
|
|
||||||
- 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配置
|
|
||||||
- [素材管理](#素材管理)
|
|
||||||
- [小程序开发](#小程序开发)
|
|
||||||
- [小程序-云开发](./tcb)
|
|
||||||
|
|
||||||
## 消息管理
|
|
||||||
|
|
||||||
通过`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)
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Apache License, Version 2.0
|
Apache License, Version 2.0
|
||||||
|
|||||||
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)
|
||||||
|
|
||||||
|
## 快速入门
|
||||||
@@ -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,221 +0,0 @@
|
|||||||
package context
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/silenceper/wechat/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
componentAccessTokenURL = "https://api.weixin.qq.com/cgi-bin/component/api_component_token"
|
|
||||||
getPreCodeURL = "https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=%s"
|
|
||||||
queryAuthURL = "https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=%s"
|
|
||||||
refreshTokenURL = "https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=%s"
|
|
||||||
getComponentInfoURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=%s"
|
|
||||||
getComponentConfigURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option?component_access_token=%s"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ComponentAccessToken 第三方平台
|
|
||||||
type ComponentAccessToken struct {
|
|
||||||
AccessToken string `json:"component_access_token"`
|
|
||||||
ExpiresIn int64 `json:"expires_in"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetComponentAccessToken 获取 ComponentAccessToken
|
|
||||||
func (ctx *Context) GetComponentAccessToken() (string, error) {
|
|
||||||
accessTokenCacheKey := fmt.Sprintf("component_access_token_%s", ctx.AppID)
|
|
||||||
val := ctx.Cache.Get(accessTokenCacheKey)
|
|
||||||
if val == nil {
|
|
||||||
return "", fmt.Errorf("cann't get component access token")
|
|
||||||
}
|
|
||||||
return val.(string), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetComponentAccessToken 通过component_verify_ticket 获取 ComponentAccessToken
|
|
||||||
func (ctx *Context) SetComponentAccessToken(verifyTicket string) (*ComponentAccessToken, error) {
|
|
||||||
body := map[string]string{
|
|
||||||
"component_appid": ctx.AppID,
|
|
||||||
"component_appsecret": ctx.AppSecret,
|
|
||||||
"component_verify_ticket": verifyTicket,
|
|
||||||
}
|
|
||||||
respBody, err := util.PostJSON(componentAccessTokenURL, body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
at := &ComponentAccessToken{}
|
|
||||||
if err := json.Unmarshal(respBody, at); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
accessTokenCacheKey := fmt.Sprintf("component_access_token_%s", ctx.AppID)
|
|
||||||
expires := at.ExpiresIn - 1500
|
|
||||||
ctx.Cache.Set(accessTokenCacheKey, at.AccessToken, time.Duration(expires)*time.Second)
|
|
||||||
return at, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPreCode 获取预授权码
|
|
||||||
func (ctx *Context) GetPreCode() (string, error) {
|
|
||||||
cat, err := ctx.GetComponentAccessToken()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
req := map[string]string{
|
|
||||||
"component_appid": ctx.AppID,
|
|
||||||
}
|
|
||||||
uri := fmt.Sprintf(getPreCodeURL, cat)
|
|
||||||
body, err := util.PostJSON(uri, req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var ret struct {
|
|
||||||
PreCode string `json:"pre_auth_code"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(body, &ret); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret.PreCode, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID 微信返回接口中各种类型字段
|
|
||||||
type ID struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthBaseInfo 授权的基本信息
|
|
||||||
type AuthBaseInfo struct {
|
|
||||||
AuthrAccessToken
|
|
||||||
FuncInfo []AuthFuncInfo `json:"func_info"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthFuncInfo 授权的接口内容
|
|
||||||
type AuthFuncInfo struct {
|
|
||||||
FuncscopeCategory ID `json:"funcscope_category"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthrAccessToken 授权方AccessToken
|
|
||||||
type AuthrAccessToken struct {
|
|
||||||
Appid string `json:"authorizer_appid"`
|
|
||||||
AccessToken string `json:"authorizer_access_token"`
|
|
||||||
ExpiresIn int64 `json:"expires_in"`
|
|
||||||
RefreshToken string `json:"authorizer_refresh_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryAuthCode 使用授权码换取公众号或小程序的接口调用凭据和授权信息
|
|
||||||
func (ctx *Context) QueryAuthCode(authCode string) (*AuthBaseInfo, error) {
|
|
||||||
cat, err := ctx.GetComponentAccessToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req := map[string]string{
|
|
||||||
"component_appid": ctx.AppID,
|
|
||||||
"authorization_code": authCode,
|
|
||||||
}
|
|
||||||
uri := fmt.Sprintf(queryAuthURL, cat)
|
|
||||||
body, err := util.PostJSON(uri, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var ret struct {
|
|
||||||
Info *AuthBaseInfo `json:"authorization_info"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(body, &ret); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret.Info, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RefreshAuthrToken 获取(刷新)授权公众号或小程序的接口调用凭据(令牌)
|
|
||||||
func (ctx *Context) RefreshAuthrToken(appid, refreshToken string) (*AuthrAccessToken, error) {
|
|
||||||
cat, err := ctx.GetComponentAccessToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req := map[string]string{
|
|
||||||
"component_appid": ctx.AppID,
|
|
||||||
"authorizer_appid": appid,
|
|
||||||
"authorizer_refresh_token": refreshToken,
|
|
||||||
}
|
|
||||||
uri := fmt.Sprintf(refreshTokenURL, cat)
|
|
||||||
body, err := util.PostJSON(uri, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ret := &AuthrAccessToken{}
|
|
||||||
if err := json.Unmarshal(body, ret); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
authrTokenKey := "authorizer_access_token_" + appid
|
|
||||||
ctx.Cache.Set(authrTokenKey, ret.AccessToken, time.Minute*80)
|
|
||||||
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAuthrAccessToken 获取授权方AccessToken
|
|
||||||
func (ctx *Context) GetAuthrAccessToken(appid string) (string, error) {
|
|
||||||
authrTokenKey := "authorizer_access_token_" + appid
|
|
||||||
val := ctx.Cache.Get(authrTokenKey)
|
|
||||||
if val == nil {
|
|
||||||
return "", fmt.Errorf("cannot get authorizer %s access token", appid)
|
|
||||||
}
|
|
||||||
return val.(string), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthorizerInfo 授权方详细信息
|
|
||||||
type AuthorizerInfo struct {
|
|
||||||
NickName string `json:"nick_name"`
|
|
||||||
HeadImg string `json:"head_img"`
|
|
||||||
ServiceTypeInfo ID `json:"service_type_info"`
|
|
||||||
VerifyTypeInfo ID `json:"verify_type_info"`
|
|
||||||
UserName string `json:"user_name"`
|
|
||||||
PrincipalName string `json:"principal_name"`
|
|
||||||
BusinessInfo struct {
|
|
||||||
OpenStore string `json:"open_store"`
|
|
||||||
OpenScan string `json:"open_scan"`
|
|
||||||
OpenPay string `json:"open_pay"`
|
|
||||||
OpenCard string `json:"open_card"`
|
|
||||||
OpenShake string `json:"open_shake"`
|
|
||||||
}
|
|
||||||
Alias string `json:"alias"`
|
|
||||||
QrcodeURL string `json:"qrcode_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAuthrInfo 获取授权方的帐号基本信息
|
|
||||||
func (ctx *Context) GetAuthrInfo(appid string) (*AuthorizerInfo, *AuthBaseInfo, error) {
|
|
||||||
cat, err := ctx.GetComponentAccessToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req := map[string]string{
|
|
||||||
"component_appid": ctx.AppID,
|
|
||||||
"authorizer_appid": appid,
|
|
||||||
}
|
|
||||||
|
|
||||||
uri := fmt.Sprintf(getComponentInfoURL, cat)
|
|
||||||
body, err := util.PostJSON(uri, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var ret struct {
|
|
||||||
AuthorizerInfo *AuthorizerInfo `json:"authorizer_info"`
|
|
||||||
AuthorizationInfo *AuthBaseInfo `json:"authorization_info"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(body, &ret); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret.AuthorizerInfo, ret.AuthorizationInfo, nil
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
23
doc.go
23
doc.go
@@ -3,29 +3,6 @@ Package wechat provide wechat sdk for go
|
|||||||
|
|
||||||
使用Golang开发的微信SDK,简单、易用。
|
使用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
|
更多信息: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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
18
go.mod
18
go.mod
@@ -3,19 +3,7 @@ module github.com/silenceper/wechat
|
|||||||
go 1.13
|
go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/astaxie/beego v1.7.1
|
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20160117192205-fb1f79c6b65a
|
github.com/gomodule/redigo v2.0.0+incompatible
|
||||||
github.com/gin-gonic/gin v1.1.4
|
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad
|
||||||
github.com/golang/protobuf v0.0.0-20161117033126-8ee79997227b // indirect
|
|
||||||
github.com/gomodule/redigo v2.0.1-0.20180627144507-2cd21d9966bf+incompatible
|
|
||||||
github.com/kr/pretty v0.1.0
|
|
||||||
github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.0-20161123143637-30a891c33c7c // indirect
|
|
||||||
github.com/stretchr/testify v1.4.0 // indirect
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
|
|
||||||
golang.org/x/net v0.0.0-20191125084936-ffdde1057850 // indirect
|
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
|
||||||
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
|
||||||
gopkg.in/go-playground/validator.v8 v8.18.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
51
go.sum
51
go.sum
@@ -1,44 +1,13 @@
|
|||||||
github.com/astaxie/beego v1.7.1 h1:TuqX4F9e3ujVEycudgWrwUj11WMppLZyunJKIBoxTFw=
|
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
|
||||||
github.com/astaxie/beego v1.7.1/go.mod h1:0R4++1tUqERR0WYFWdfkcrsyoVBCG4DgpDGokT3yb+U=
|
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20160117192205-fb1f79c6b65a h1:k5TuEkqEYCRs8+66WdOkswWOj+L/YbP5ruainvn94wg=
|
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20160117192205-fb1f79c6b65a/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
|
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||||
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/gin-gonic/gin v1.1.4 h1:XLaCFbU39SSGRQrEeP7Z7mM3lvRqC4vE5tEaVdLDdSE=
|
|
||||||
github.com/gin-gonic/gin v1.1.4/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
|
|
||||||
github.com/golang/protobuf v0.0.0-20161117033126-8ee79997227b h1:fE/yi9pibxGEc0gSJuEShcsBXE2d5FW3OudsjE9tKzQ=
|
|
||||||
github.com/golang/protobuf v0.0.0-20161117033126-8ee79997227b/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/gomodule/redigo v2.0.1-0.20180627144507-2cd21d9966bf+incompatible h1:QJ4V3LjaRe/6NKoaaj2QzQZcezt5gNXdPv0axxS4VNA=
|
|
||||||
github.com/gomodule/redigo v2.0.1-0.20180627144507-2cd21d9966bf+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 h1:ykXz+pRRTibcSjG1yRhpdSHInF8yZY/mfn+Rz2Nd1rE=
|
|
||||||
github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739/go.mod h1:zUx1mhth20V3VKgL5jbd1BSQcW4Fy6Qs4PZvQwRFwzM=
|
|
||||||
github.com/mattn/go-isatty v0.0.0-20161123143637-30a891c33c7c h1:YHHK/dEmr2Jo1cWD1VMB2waEeHJhHFp3CEylwWy/VcY=
|
|
||||||
github.com/mattn/go-isatty v0.0.0-20161123143637-30a891c33c7c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
|
||||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/net v0.0.0-20191125084936-ffdde1057850 h1:Vq85/r8R9IdcUHmZ0/nQlUg1v15rzvQ2sHdnZAj/x7s=
|
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg=
|
||||||
golang.org/x/net v0.0.0-20191125084936-ffdde1057850/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
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/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
|
|
||||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
|
||||||
gopkg.in/go-playground/validator.v8 v8.18.1 h1:F8SLY5Vqesjs1nI1EL4qmF1PQZ1sitsmq0rPYXLyfGU=
|
|
||||||
gopkg.in/go-playground/validator.v8 v8.18.1/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
|
|||||||
5
minigame/README.md
Normal file
5
minigame/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 微信小游戏
|
||||||
|
|
||||||
|
[官方文档](https://developers.weixin.qq.com/minigame/dev/api-backend/)
|
||||||
|
|
||||||
|
## 快速入门
|
||||||
5
miniprogram/README.md
Normal file
5
miniprogram/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 微信小程序
|
||||||
|
|
||||||
|
[官方文档](https://developers.weixin.qq.com/miniprogram/dev/framework/)
|
||||||
|
|
||||||
|
## 快速入门
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
package miniprogram
|
package analysis
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/miniprogram/context"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/util"
|
"github.com/silenceper/wechat/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,10 +32,20 @@ const (
|
|||||||
getAnalysisVisitPageURL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitpage?access_token=%s"
|
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 拉取统计数据
|
// 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
|
var accessToken string
|
||||||
accessToken, err = wxa.GetAccessToken()
|
accessToken, err = analysis.GetAccessToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -42,8 +54,8 @@ func (wxa *MiniProgram) fetchData(urlStr string, body interface{}) (response []b
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// AnalysisRetainItem 留存项结构
|
// RetainItem 留存项结构
|
||||||
type AnalysisRetainItem struct {
|
type RetainItem struct {
|
||||||
Key int `json:"key"` // 标识,0开始表示当天,1表示1甜后,以此类推
|
Key int `json:"key"` // 标识,0开始表示当天,1表示1甜后,以此类推
|
||||||
Value int `json:"value"` // key对应日期的新增用户数/活跃用户数(key=0时)或留存用户数(k>0时)
|
Value int `json:"value"` // key对应日期的新增用户数/活跃用户数(key=0时)或留存用户数(k>0时)
|
||||||
}
|
}
|
||||||
@@ -51,18 +63,18 @@ type AnalysisRetainItem struct {
|
|||||||
// ResAnalysisRetain 小程序留存数据返回
|
// ResAnalysisRetain 小程序留存数据返回
|
||||||
type ResAnalysisRetain struct {
|
type ResAnalysisRetain struct {
|
||||||
util.CommonError
|
util.CommonError
|
||||||
RefDate string `json:"ref_date"` // 日期
|
RefDate string `json:"ref_date"` // 日期
|
||||||
VisitUVNew []AnalysisRetainItem `json:"visit_uv_new"` // 新增用户留存
|
VisitUVNew []RetainItem `json:"visit_uv_new"` // 新增用户留存
|
||||||
VisitUV []AnalysisRetainItem `json:"visit_uv"` // 活跃用户留存
|
VisitUV []RetainItem `json:"visit_uv"` // 活跃用户留存
|
||||||
}
|
}
|
||||||
|
|
||||||
// getAnalysisRetain 获取用户访问小程序留存数据(日、月、周)
|
// 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{
|
body := map[string]string{
|
||||||
"begin_date": beginDate,
|
"begin_date": beginDate,
|
||||||
"end_date": endDate,
|
"end_date": endDate,
|
||||||
}
|
}
|
||||||
response, err := wxa.fetchData(urlStr, body)
|
response, err := analysis.fetchData(urlStr, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -78,18 +90,18 @@ func (wxa *MiniProgram) getAnalysisRetain(urlStr string, beginDate, endDate stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAnalysisDailyRetain 获取用户访问小程序日留存
|
// GetAnalysisDailyRetain 获取用户访问小程序日留存
|
||||||
func (wxa *MiniProgram) GetAnalysisDailyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
func (analysis *Analysis) GetAnalysisDailyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||||
return wxa.getAnalysisRetain(getAnalysisDailyRetainURL, beginDate, endDate)
|
return analysis.getAnalysisRetain(getAnalysisDailyRetainURL, beginDate, endDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAnalysisMonthlyRetain 获取用户访问小程序月留存
|
// GetAnalysisMonthlyRetain 获取用户访问小程序月留存
|
||||||
func (wxa *MiniProgram) GetAnalysisMonthlyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
func (analysis *Analysis) GetAnalysisMonthlyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||||
return wxa.getAnalysisRetain(getAnalysisMonthlyRetainURL, beginDate, endDate)
|
return analysis.getAnalysisRetain(getAnalysisMonthlyRetainURL, beginDate, endDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAnalysisWeeklyRetain 获取用户访问小程序周留存
|
// GetAnalysisWeeklyRetain 获取用户访问小程序周留存
|
||||||
func (wxa *MiniProgram) GetAnalysisWeeklyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
func (analysis *Analysis) GetAnalysisWeeklyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||||
return wxa.getAnalysisRetain(getAnalysisWeeklyRetainURL, beginDate, endDate)
|
return analysis.getAnalysisRetain(getAnalysisWeeklyRetainURL, beginDate, endDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResAnalysisDailySummary 小程序访问数据概况
|
// ResAnalysisDailySummary 小程序访问数据概况
|
||||||
@@ -104,12 +116,12 @@ type ResAnalysisDailySummary struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAnalysisDailySummary 获取用户访问小程序数据概况
|
// 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{
|
body := map[string]string{
|
||||||
"begin_date": beginDate,
|
"begin_date": beginDate,
|
||||||
"end_date": endDate,
|
"end_date": endDate,
|
||||||
}
|
}
|
||||||
response, err := wxa.fetchData(getAnalysisDailySummaryURL, body)
|
response, err := analysis.fetchData(getAnalysisDailySummaryURL, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -141,12 +153,12 @@ type ResAnalysisVisitTrend struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getAnalysisRetain 获取小程序访问数据趋势(日、月、周)
|
// 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{
|
body := map[string]string{
|
||||||
"begin_date": beginDate,
|
"begin_date": beginDate,
|
||||||
"end_date": endDate,
|
"end_date": endDate,
|
||||||
}
|
}
|
||||||
response, err := wxa.fetchData(urlStr, body)
|
response, err := analysis.fetchData(urlStr, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -162,18 +174,18 @@ func (wxa *MiniProgram) getAnalysisVisitTrend(urlStr string, beginDate, endDate
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAnalysisDailyVisitTrend 获取用户访问小程序数据日趋势
|
// GetAnalysisDailyVisitTrend 获取用户访问小程序数据日趋势
|
||||||
func (wxa *MiniProgram) GetAnalysisDailyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
func (analysis *Analysis) GetAnalysisDailyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||||
return wxa.getAnalysisVisitTrend(getAnalysisDailyVisitTrendURL, beginDate, endDate)
|
return analysis.getAnalysisVisitTrend(getAnalysisDailyVisitTrendURL, beginDate, endDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAnalysisMonthlyVisitTrend 获取用户访问小程序数据月趋势
|
// GetAnalysisMonthlyVisitTrend 获取用户访问小程序数据月趋势
|
||||||
func (wxa *MiniProgram) GetAnalysisMonthlyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
func (analysis *Analysis) GetAnalysisMonthlyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||||
return wxa.getAnalysisVisitTrend(getAnalysisMonthlyVisitTrendURL, beginDate, endDate)
|
return analysis.getAnalysisVisitTrend(getAnalysisMonthlyVisitTrendURL, beginDate, endDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAnalysisWeeklyVisitTrend 获取用户访问小程序数据周趋势
|
// GetAnalysisWeeklyVisitTrend 获取用户访问小程序数据周趋势
|
||||||
func (wxa *MiniProgram) GetAnalysisWeeklyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
func (analysis *Analysis) GetAnalysisWeeklyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||||
return wxa.getAnalysisVisitTrend(getAnalysisWeeklyVisitTrendURL, beginDate, endDate)
|
return analysis.getAnalysisVisitTrend(getAnalysisWeeklyVisitTrendURL, beginDate, endDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserPortraitItem 用户画像项目
|
// UserPortraitItem 用户画像项目
|
||||||
@@ -203,12 +215,12 @@ type ResAnalysisUserPortrait struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAnalysisUserPortrait 获取小程序新增或活跃用户的画像分布数据
|
// 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{
|
body := map[string]string{
|
||||||
"begin_date": beginDate,
|
"begin_date": beginDate,
|
||||||
"end_date": endDate,
|
"end_date": endDate,
|
||||||
}
|
}
|
||||||
response, err := wxa.fetchData(getAnalysisUserPortraitURL, body)
|
response, err := analysis.fetchData(getAnalysisUserPortraitURL, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -244,12 +256,12 @@ type ResAnalysisVisitDistribution struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAnalysisVisitDistribution 获取用户小程序访问分布数据
|
// 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{
|
body := map[string]string{
|
||||||
"begin_date": beginDate,
|
"begin_date": beginDate,
|
||||||
"end_date": endDate,
|
"end_date": endDate,
|
||||||
}
|
}
|
||||||
response, err := wxa.fetchData(getAnalysisVisitDistributionURL, body)
|
response, err := analysis.fetchData(getAnalysisVisitDistributionURL, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -284,12 +296,12 @@ type ResAnalysisVisitPage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetAnalysisVisitPage 获取小程序页面访问数据
|
// 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{
|
body := map[string]string{
|
||||||
"begin_date": beginDate,
|
"begin_date": beginDate,
|
||||||
"end_date": endDate,
|
"end_date": endDate,
|
||||||
}
|
}
|
||||||
response, err := wxa.fetchData(getAnalysisVisitPageURL, body)
|
response, err := analysis.fetchData(getAnalysisVisitPageURL, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
package miniprogram
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/miniprogram/context"
|
||||||
"github.com/silenceper/wechat/util"
|
"github.com/silenceper/wechat/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,6 +12,16 @@ const (
|
|||||||
code2SessionURL = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
|
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 登录凭证校验的返回结果
|
// ResCode2Session 登录凭证校验的返回结果
|
||||||
type ResCode2Session struct {
|
type ResCode2Session struct {
|
||||||
util.CommonError
|
util.CommonError
|
||||||
@@ -20,9 +31,9 @@ type ResCode2Session struct {
|
|||||||
UnionID string `json:"unionid"` // 用户在开放平台的唯一标识符,在满足UnionID下发条件的情况下会返回
|
UnionID string `json:"unionid"` // 用户在开放平台的唯一标识符,在满足UnionID下发条件的情况下会返回
|
||||||
}
|
}
|
||||||
|
|
||||||
// Code2Session 登录凭证校验
|
//Code2Session 登录凭证校验。
|
||||||
func (wxa *MiniProgram) Code2Session(jsCode string) (result ResCode2Session, err error) {
|
func (auth *Auth) Code2Session(jsCode string) (result ResCode2Session, err error) {
|
||||||
urlStr := fmt.Sprintf(code2SessionURL, wxa.AppID, wxa.AppSecret, jsCode)
|
urlStr := fmt.Sprintf(code2SessionURL, auth.AppID, auth.AppSecret, jsCode)
|
||||||
var response []byte
|
var response []byte
|
||||||
response, err = util.HTTPGet(urlStr)
|
response, err = util.HTTPGet(urlStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -38,3 +49,8 @@ func (wxa *MiniProgram) Code2Session(jsCode string) (result ResCode2Session, err
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//GetPaidUnionID 用户支付完成后,获取该用户的 UnionId,无需用户授权
|
||||||
|
func (auth *Auth) GetPaidUnionID() {
|
||||||
|
//TODO
|
||||||
|
}
|
||||||
15
miniprogram/basic/basic.go
Normal file
15
miniprogram/basic/basic.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package basic
|
||||||
|
|
||||||
|
import "github.com/silenceper/wechat/miniprogram/context"
|
||||||
|
|
||||||
|
//Basic struct
|
||||||
|
type Basic struct {
|
||||||
|
*context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewBasic 实例
|
||||||
|
func NewBasic(context *context.Context) *Basic {
|
||||||
|
basic := new(Basic)
|
||||||
|
basic.Context = context
|
||||||
|
return basic
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package miniprogram
|
package basic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
@@ -96,7 +96,7 @@ func getCipherText(sessionKey, encryptedData, iv string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt 解密数据
|
// Decrypt 解密数据
|
||||||
func (wxa *MiniProgram) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo, error) {
|
func (basic *Basic) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo, error) {
|
||||||
cipherText, err := getCipherText(sessionKey, encryptedData, iv)
|
cipherText, err := getCipherText(sessionKey, encryptedData, iv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -106,14 +106,14 @@ func (wxa *MiniProgram) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if userInfo.Watermark.AppID != wxa.AppID {
|
if userInfo.Watermark.AppID != basic.AppID {
|
||||||
return nil, ErrAppIDNotMatch
|
return nil, ErrAppIDNotMatch
|
||||||
}
|
}
|
||||||
return &userInfo, nil
|
return &userInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecryptPhone 解密数据(手机)
|
// DecryptPhone 解密数据(手机)
|
||||||
func (wxa *MiniProgram) DecryptPhone(sessionKey, encryptedData, iv string) (*PhoneInfo, error) {
|
func (basic *Basic) DecryptPhone(sessionKey, encryptedData, iv string) (*PhoneInfo, error) {
|
||||||
cipherText, err := getCipherText(sessionKey, encryptedData, iv)
|
cipherText, err := getCipherText(sessionKey, encryptedData, iv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -123,7 +123,7 @@ func (wxa *MiniProgram) DecryptPhone(sessionKey, encryptedData, iv string) (*Pho
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if phoneInfo.Watermark.AppID != wxa.AppID {
|
if phoneInfo.Watermark.AppID != basic.AppID {
|
||||||
return nil, ErrAppIDNotMatch
|
return nil, ErrAppIDNotMatch
|
||||||
}
|
}
|
||||||
return &phoneInfo, nil
|
return &phoneInfo, nil
|
||||||
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/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Config config for 小程序
|
||||||
|
type Config struct {
|
||||||
|
AppID string `json:"app_id"` //appid
|
||||||
|
AppSecret string `json:"app_secret"` //appsecret
|
||||||
|
Cache cache.Cache
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
const (
|
const (
|
||||||
//AccessTokenURL 获取access_token的接口
|
//AccessTokenURL 获取access_token的接口
|
||||||
AccessTokenURL = "https://api.weixin.qq.com/cgi-bin/token"
|
AccessTokenURL = "https://api.weixin.qq.com/cgi-bin/token"
|
||||||
|
//CacheKeyPrefix cache前缀
|
||||||
|
CacheKeyPrefix = "gowechat_miniprogram_"
|
||||||
)
|
)
|
||||||
|
|
||||||
//ResAccessToken struct
|
//ResAccessToken struct
|
||||||
@@ -43,7 +45,7 @@ func (ctx *Context) GetAccessToken() (accessToken string, err error) {
|
|||||||
if ctx.accessTokenFunc != nil {
|
if ctx.accessTokenFunc != nil {
|
||||||
return ctx.accessTokenFunc(ctx)
|
return ctx.accessTokenFunc(ctx)
|
||||||
}
|
}
|
||||||
accessTokenCacheKey := fmt.Sprintf("access_token_%s", ctx.AppID)
|
accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", CacheKeyPrefix, ctx.AppID)
|
||||||
val := ctx.Cache.Get(accessTokenCacheKey)
|
val := ctx.Cache.Get(accessTokenCacheKey)
|
||||||
if val != nil {
|
if val != nil {
|
||||||
accessToken = val.(string)
|
accessToken = val.(string)
|
||||||
@@ -78,7 +80,7 @@ func (ctx *Context) GetAccessTokenFromServer() (resAccessToken ResAccessToken, e
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
accessTokenCacheKey := fmt.Sprintf("access_token_%s", ctx.AppID)
|
accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", CacheKeyPrefix, ctx.AppID)
|
||||||
expires := resAccessToken.ExpiresIn - 1500
|
expires := resAccessToken.ExpiresIn - 1500
|
||||||
err = ctx.Cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
err = ctx.Cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
||||||
return
|
return
|
||||||
18
miniprogram/context/context.go
Normal file
18
miniprogram/context/context.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/miniprogram/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context struct
|
||||||
|
type Context struct {
|
||||||
|
*config.Config
|
||||||
|
|
||||||
|
//accessTokenLock 读写锁 同一个AppID一个
|
||||||
|
accessTokenLock *sync.RWMutex
|
||||||
|
|
||||||
|
//accessTokenFunc 自定义获取 access token 的方法
|
||||||
|
accessTokenFunc GetAccessTokenFunc
|
||||||
|
}
|
||||||
@@ -1,17 +1,60 @@
|
|||||||
package miniprogram
|
package miniprogram
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/silenceper/wechat/context"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/miniprogram/analysis"
|
||||||
|
"github.com/silenceper/wechat/miniprogram/auth"
|
||||||
|
"github.com/silenceper/wechat/miniprogram/basic"
|
||||||
|
"github.com/silenceper/wechat/miniprogram/config"
|
||||||
|
"github.com/silenceper/wechat/miniprogram/context"
|
||||||
|
"github.com/silenceper/wechat/miniprogram/qrcode"
|
||||||
|
"github.com/silenceper/wechat/miniprogram/tcb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MiniProgram struct extends context
|
//MiniProgram 微信小程序相关API
|
||||||
type MiniProgram struct {
|
type MiniProgram struct {
|
||||||
*context.Context
|
ctx *context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMiniProgram 实例化小程序接口
|
//NewMiniProgram 实例化小程序API
|
||||||
func NewMiniProgram(context *context.Context) *MiniProgram {
|
func NewMiniProgram(cfg *config.Config) *MiniProgram {
|
||||||
miniProgram := new(MiniProgram)
|
if cfg.Cache == nil {
|
||||||
miniProgram.Context = context
|
panic("cache未设置")
|
||||||
return miniProgram
|
}
|
||||||
|
ctx := &context.Context{
|
||||||
|
Config: cfg,
|
||||||
|
}
|
||||||
|
ctx.SetAccessTokenLock(new(sync.RWMutex))
|
||||||
|
return &MiniProgram{ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetContext get Context
|
||||||
|
func (miniProgram *MiniProgram) GetContext() *context.Context {
|
||||||
|
return miniProgram.ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBasic 基础接口(小程序加解密)
|
||||||
|
func (miniProgram *MiniProgram) GetBasic() *basic.Basic {
|
||||||
|
return basic.NewBasic(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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package miniprogram
|
package qrcode
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/miniprogram/context"
|
||||||
"github.com/silenceper/wechat/util"
|
"github.com/silenceper/wechat/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,6 +15,25 @@ const (
|
|||||||
getWXACodeUnlimitURL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%s"
|
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 小程序码参数
|
// QRCoder 小程序码参数
|
||||||
type QRCoder struct {
|
type QRCoder struct {
|
||||||
// page 必须是已经发布的小程序存在的页面,根路径前不要填加 /,不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面
|
// page 必须是已经发布的小程序存在的页面,根路径前不要填加 /,不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面
|
||||||
@@ -32,17 +52,10 @@ type QRCoder struct {
|
|||||||
IsHyaline bool `json:"is_hyaline,omitempty"`
|
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 请求并返回二维码二进制数据
|
// 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
|
var accessToken string
|
||||||
accessToken, err = wxa.GetAccessToken()
|
accessToken, err = qrCode.GetAccessToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -74,18 +87,18 @@ func (wxa *MiniProgram) fetchCode(urlStr string, body interface{}) (response []b
|
|||||||
|
|
||||||
// CreateWXAQRCode 获取小程序二维码,适用于需要的码数量较少的业务场景
|
// CreateWXAQRCode 获取小程序二维码,适用于需要的码数量较少的业务场景
|
||||||
// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/createWXAQRCode.html
|
// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/createWXAQRCode.html
|
||||||
func (wxa *MiniProgram) CreateWXAQRCode(coderParams QRCoder) (response []byte, err error) {
|
func (qrCode *QRCode) CreateWXAQRCode(coderParams QRCoder) (response []byte, err error) {
|
||||||
return wxa.fetchCode(createWXAQRCodeURL, coderParams)
|
return qrCode.fetchCode(createWXAQRCodeURL, coderParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWXACode 获取小程序码,适用于需要的码数量较少的业务场景
|
// GetWXACode 获取小程序码,适用于需要的码数量较少的业务场景
|
||||||
// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/getWXACode.html
|
// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/getWXACode.html
|
||||||
func (wxa *MiniProgram) GetWXACode(coderParams QRCoder) (response []byte, err error) {
|
func (qrCode *QRCode) GetWXACode(coderParams QRCoder) (response []byte, err error) {
|
||||||
return wxa.fetchCode(getWXACodeURL, coderParams)
|
return qrCode.fetchCode(getWXACodeURL, coderParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWXACodeUnlimit 获取小程序码,适用于需要的码数量极多的业务场景
|
// GetWXACodeUnlimit 获取小程序码,适用于需要的码数量极多的业务场景
|
||||||
// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/getWXACodeUnlimit.html
|
// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/getWXACodeUnlimit.html
|
||||||
func (wxa *MiniProgram) GetWXACodeUnlimit(coderParams QRCoder) (response []byte, err error) {
|
func (qrCode *QRCode) GetWXACodeUnlimit(coderParams QRCoder) (response []byte, err error) {
|
||||||
return wxa.fetchCode(getWXACodeUnlimitURL, coderParams)
|
return qrCode.fetchCode(getWXACodeUnlimitURL, coderParams)
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
package tcb
|
package tcb
|
||||||
|
|
||||||
import "github.com/silenceper/wechat/context"
|
import "github.com/silenceper/wechat/miniprogram/context"
|
||||||
|
|
||||||
//Tcb Tencent Cloud Base
|
//Tcb Tencent Cloud Base
|
||||||
type Tcb struct{
|
type Tcb struct {
|
||||||
*context.Context
|
*context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
//NewTcb new Tencent Cloud Base
|
//NewTcb new Tencent Cloud Base
|
||||||
func NewTcb(context *context.Context)*Tcb{
|
func NewTcb(context *context.Context) *Tcb {
|
||||||
return &Tcb{
|
return &Tcb{
|
||||||
context,
|
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)
|
||||||
|
|
||||||
|
## 快速入门
|
||||||
15
officialaccount/basic/basic.go
Normal file
15
officialaccount/basic/basic.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package basic
|
||||||
|
|
||||||
|
import "github.com/silenceper/wechat/officialaccount/context"
|
||||||
|
|
||||||
|
//Basic struct
|
||||||
|
type Basic struct {
|
||||||
|
*context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewBasic 实例
|
||||||
|
func NewBasic(context *context.Context) *Basic {
|
||||||
|
basic := new(Basic)
|
||||||
|
basic.Context = context
|
||||||
|
return basic
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package qr
|
package basic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/context"
|
|
||||||
"github.com/silenceper/wechat/util"
|
"github.com/silenceper/wechat/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,18 +22,6 @@ const (
|
|||||||
actionLimitStr = "QR_LIMIT_STR_SCENE"
|
actionLimitStr = "QR_LIMIT_STR_SCENE"
|
||||||
)
|
)
|
||||||
|
|
||||||
// QR 二维码
|
|
||||||
type QR struct {
|
|
||||||
*context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
//NewQR 二维码实例
|
|
||||||
func NewQR(context *context.Context) *QR {
|
|
||||||
q := new(QR)
|
|
||||||
q.Context = context
|
|
||||||
return q
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request 临时二维码
|
// Request 临时二维码
|
||||||
type Request struct {
|
type Request struct {
|
||||||
ExpireSeconds int64 `json:"expire_seconds,omitempty"`
|
ExpireSeconds int64 `json:"expire_seconds,omitempty"`
|
||||||
@@ -56,8 +43,8 @@ type Ticket struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetQRTicket 获取二维码 Ticket
|
// GetQRTicket 获取二维码 Ticket
|
||||||
func (q *QR) GetQRTicket(tq *Request) (t *Ticket, err error) {
|
func (basic *Basic) GetQRTicket(tq *Request) (t *Ticket, err error) {
|
||||||
accessToken, err := q.GetAccessToken()
|
accessToken, err := basic.GetAccessToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
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/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
|
||||||
|
}
|
||||||
87
officialaccount/context/access_token.go
Normal file
87
officialaccount/context/access_token.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
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"
|
||||||
|
//CacheKeyPrefix 微信公众号cache key前缀
|
||||||
|
CacheKeyPrefix = "gowechat_officialaccount_"
|
||||||
|
)
|
||||||
|
|
||||||
|
//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("%s_access_token_%s", CacheKeyPrefix, 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("%s_access_token_%s", CacheKeyPrefix, ctx.AppID)
|
||||||
|
expires := resAccessToken.ExpiresIn - 1500
|
||||||
|
err = ctx.Cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
||||||
|
return
|
||||||
|
}
|
||||||
31
officialaccount/context/context.go
Normal file
31
officialaccount/context/context.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/officialaccount/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context struct
|
||||||
|
type Context struct {
|
||||||
|
*config.Config
|
||||||
|
|
||||||
|
//accessTokenLock 读写锁 同一个AppID一个
|
||||||
|
accessTokenLock *sync.RWMutex
|
||||||
|
|
||||||
|
//jsAPITicket 读写锁 同一个AppID一个
|
||||||
|
jsAPITicketLock *sync.RWMutex
|
||||||
|
|
||||||
|
//accessTokenFunc 自定义获取 access token 的方法
|
||||||
|
accessTokenFunc GetAccessTokenFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/context"
|
"github.com/silenceper/wechat/officialaccount/context"
|
||||||
"github.com/silenceper/wechat/util"
|
"github.com/silenceper/wechat/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/context"
|
"github.com/silenceper/wechat/officialaccount/context"
|
||||||
"github.com/silenceper/wechat/util"
|
"github.com/silenceper/wechat/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ func (js *Js) GetTicket() (ticketStr string, err error) {
|
|||||||
defer js.GetJsAPITicketLock().Unlock()
|
defer js.GetJsAPITicketLock().Unlock()
|
||||||
|
|
||||||
//先从cache中取
|
//先从cache中取
|
||||||
jsAPITicketCacheKey := fmt.Sprintf("jsapi_ticket_%s", js.AppID)
|
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", context.CacheKeyPrefix, js.AppID)
|
||||||
val := js.Cache.Get(jsAPITicketCacheKey)
|
val := js.Cache.Get(jsAPITicketCacheKey)
|
||||||
if val != nil {
|
if val != nil {
|
||||||
ticketStr = val.(string)
|
ticketStr = val.(string)
|
||||||
@@ -102,7 +102,7 @@ func (js *Js) getTicketFromServer() (ticket resTicket, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jsAPITicketCacheKey := fmt.Sprintf("jsapi_ticket_%s", js.AppID)
|
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", context.CacheKeyPrefix, js.AppID)
|
||||||
expires := ticket.ExpiresIn - 1500
|
expires := ticket.ExpiresIn - 1500
|
||||||
err = js.Cache.Set(jsAPITicketCacheKey, ticket.Ticket, time.Duration(expires)*time.Second)
|
err = js.Cache.Set(jsAPITicketCacheKey, ticket.Ticket, time.Duration(expires)*time.Second)
|
||||||
return
|
return
|
||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/context"
|
"github.com/silenceper/wechat/officialaccount/context"
|
||||||
"github.com/silenceper/wechat/util"
|
"github.com/silenceper/wechat/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/context"
|
"github.com/silenceper/wechat/officialaccount/context"
|
||||||
"github.com/silenceper/wechat/util"
|
"github.com/silenceper/wechat/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3,7 +3,8 @@ package message
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/silenceper/wechat/context"
|
|
||||||
|
"github.com/silenceper/wechat/officialaccount/context"
|
||||||
"github.com/silenceper/wechat/util"
|
"github.com/silenceper/wechat/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ package message
|
|||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/device"
|
"github.com/silenceper/wechat/officialaccount/device"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MsgType 基本消息类型
|
// MsgType 基本消息类型
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/context"
|
"github.com/silenceper/wechat/officialaccount/context"
|
||||||
"github.com/silenceper/wechat/util"
|
"github.com/silenceper/wechat/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/context"
|
"github.com/silenceper/wechat/officialaccount/context"
|
||||||
"github.com/silenceper/wechat/util"
|
"github.com/silenceper/wechat/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
94
officialaccount/officialaccount.go
Normal file
94
officialaccount/officialaccount.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package officialaccount
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/officialaccount/basic"
|
||||||
|
"github.com/silenceper/wechat/officialaccount/config"
|
||||||
|
"github.com/silenceper/wechat/officialaccount/context"
|
||||||
|
"github.com/silenceper/wechat/officialaccount/device"
|
||||||
|
"github.com/silenceper/wechat/officialaccount/js"
|
||||||
|
"github.com/silenceper/wechat/officialaccount/material"
|
||||||
|
"github.com/silenceper/wechat/officialaccount/menu"
|
||||||
|
"github.com/silenceper/wechat/officialaccount/message"
|
||||||
|
"github.com/silenceper/wechat/officialaccount/oauth"
|
||||||
|
"github.com/silenceper/wechat/officialaccount/server"
|
||||||
|
"github.com/silenceper/wechat/officialaccount/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
//OfficialAccount 微信公众号相关API
|
||||||
|
type OfficialAccount struct {
|
||||||
|
ctx *context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewOfficialAccount 实例化公众号API
|
||||||
|
func NewOfficialAccount(cfg *config.Config) *OfficialAccount {
|
||||||
|
if cfg.Cache == nil {
|
||||||
|
panic("cache未设置")
|
||||||
|
}
|
||||||
|
ctx := &context.Context{
|
||||||
|
Config: cfg,
|
||||||
|
}
|
||||||
|
ctx.SetAccessTokenLock(new(sync.RWMutex))
|
||||||
|
ctx.SetJsAPITicketLock(new(sync.RWMutex))
|
||||||
|
return &OfficialAccount{ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
@@ -5,18 +5,22 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/context"
|
"github.com/silenceper/wechat/officialaccount/context"
|
||||||
"github.com/silenceper/wechat/message"
|
"github.com/silenceper/wechat/officialaccount/message"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/util"
|
"github.com/silenceper/wechat/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
//Server struct
|
//Server struct
|
||||||
type Server struct {
|
type Server struct {
|
||||||
*context.Context
|
*context.Context
|
||||||
|
Writer http.ResponseWriter
|
||||||
|
Request *http.Request
|
||||||
|
|
||||||
debug bool
|
debug bool
|
||||||
|
|
||||||
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,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/context"
|
"github.com/silenceper/wechat/officialaccount/context"
|
||||||
"github.com/silenceper/wechat/util"
|
"github.com/silenceper/wechat/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
5
openplatform/README.md
Normal file
5
openplatform/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 微信开放平台
|
||||||
|
|
||||||
|
[官方文档](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Third_party_platform_appid.html)
|
||||||
|
|
||||||
|
## 快速入门
|
||||||
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/pay/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Notify 回调
|
||||||
|
type Notify struct {
|
||||||
|
*config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewNotify new
|
||||||
|
func NewNotify(cfg *config.Config) *Notify {
|
||||||
|
return &Notify{cfg}
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
package pay
|
package notify
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/silenceper/wechat/util"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Base 公用参数
|
// Base 公用参数
|
||||||
@@ -14,8 +15,8 @@ type Base struct {
|
|||||||
Sign string `xml:"sign"`
|
Sign string `xml:"sign"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyResult 下单回调
|
// PaidResult 下单回调
|
||||||
type NotifyResult struct {
|
type PaidResult struct {
|
||||||
Base
|
Base
|
||||||
ReturnCode string `xml:"return_code"`
|
ReturnCode string `xml:"return_code"`
|
||||||
ReturnMsg string `xml:"return_msg"`
|
ReturnMsg string `xml:"return_msg"`
|
||||||
@@ -34,14 +35,14 @@ type NotifyResult struct {
|
|||||||
TimeEnd string `xml:"time_end"`
|
TimeEnd string `xml:"time_end"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyResp 消息通知返回
|
// PaidResp 消息通知返回
|
||||||
type NotifyResp struct {
|
type PaidResp struct {
|
||||||
ReturnCode string `xml:"return_code"`
|
ReturnCode string `xml:"return_code"`
|
||||||
ReturnMsg string `xml:"return_msg"`
|
ReturnMsg string `xml:"return_msg"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifySign 验签
|
// PaidVerifySign 支付成功结果验签
|
||||||
func (pcf *Pay) VerifySign(notifyRes NotifyResult) bool {
|
func (notify *Notify) PaidVerifySign(notifyRes PaidResult) bool {
|
||||||
// 封装map 请求过来的 map
|
// 封装map 请求过来的 map
|
||||||
resMap := make(map[string]interface{})
|
resMap := make(map[string]interface{})
|
||||||
// base
|
// base
|
||||||
@@ -77,7 +78,7 @@ func (pcf *Pay) VerifySign(notifyRes NotifyResult) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// STEP3, 在键值对的最后加上key=API_KEY
|
// STEP3, 在键值对的最后加上key=API_KEY
|
||||||
signStrings = signStrings + "key=" + pcf.PayKey
|
signStrings = signStrings + "key=" + notify.Key
|
||||||
// STEP4, 进行MD5签名并且将所有字符转为大写.
|
// STEP4, 进行MD5签名并且将所有字符转为大写.
|
||||||
sign := util.MD5Sum(signStrings)
|
sign := util.MD5Sum(signStrings)
|
||||||
if sign != notifyRes.Sign {
|
if sign != notifyRes.Sign {
|
||||||
220
pay/order/pay.go
Normal file
220
pay/order/pay.go
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
package order
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"hash"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/pay/config"
|
||||||
|
"github.com/silenceper/wechat/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"` // 场景信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// BridgeConfig get js bridge config
|
||||||
|
func (o *Order) BridgeConfig(p *Params) (cfg Config, err error) {
|
||||||
|
var (
|
||||||
|
buffer strings.Builder
|
||||||
|
h hash.Hash
|
||||||
|
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)
|
||||||
|
if p.SignType == "MD5" {
|
||||||
|
h = md5.New()
|
||||||
|
} else {
|
||||||
|
h = hmac.New(sha256.New, []byte(o.Key))
|
||||||
|
}
|
||||||
|
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 (o *Order) PrePayOrder(p *Params) (payOrder PreOrder, err error) {
|
||||||
|
nonceStr := util.RandomStr(32)
|
||||||
|
notifyURL := o.NotifyURL
|
||||||
|
// 签名类型
|
||||||
|
if p.SignType == "" {
|
||||||
|
p.SignType = "MD5"
|
||||||
|
}
|
||||||
|
// 通知地址
|
||||||
|
if p.NotifyURL != "" {
|
||||||
|
notifyURL = p.NotifyURL
|
||||||
|
}
|
||||||
|
param := make(map[string]interface{})
|
||||||
|
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
|
||||||
|
|
||||||
|
bizKey := "&key=" + o.Key
|
||||||
|
str := util.OrderParam(param, bizKey)
|
||||||
|
sign := util.MD5Sum(str)
|
||||||
|
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) + "] [params : " + str + "] [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
|
package pay
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"github.com/silenceper/wechat/pay/config"
|
||||||
"crypto/hmac"
|
"github.com/silenceper/wechat/pay/notify"
|
||||||
"crypto/md5"
|
"github.com/silenceper/wechat/pay/order"
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"hash"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/silenceper/wechat/context"
|
|
||||||
"github.com/silenceper/wechat/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var payGateway = "https://api.mch.weixin.qq.com/pay/unifiedorder"
|
//Pay 微信支付相关API
|
||||||
|
|
||||||
// Pay struct extends context
|
|
||||||
type Pay struct {
|
type Pay struct {
|
||||||
*context.Context
|
cfg *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// Params was NEEDED when request unifiedorder
|
//NewPay 实例化微信支付相关API
|
||||||
// 传入的参数,用于生成 prepay_id 的必需参数
|
func NewPay(cfg *config.Config) *Pay {
|
||||||
type Params struct {
|
return &Pay{cfg}
|
||||||
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 用的参数
|
// GetOrder 下单
|
||||||
type Config struct {
|
func (pay *Pay) GetOrder() *order.Order {
|
||||||
Timestamp string `json:"timestamp"`
|
return order.NewOrder(pay.cfg)
|
||||||
NonceStr string `json:"nonceStr"`
|
|
||||||
PrePayID string `json:"prePayId"`
|
|
||||||
SignType string `json:"signType"`
|
|
||||||
Package string `json:"package"`
|
|
||||||
PaySign string `json:"paySign"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PreOrder 是 unifie order 接口的返回
|
// GetNotify 通知
|
||||||
type PreOrder struct {
|
func (pay *Pay) GetNotify() *notify.Notify {
|
||||||
ReturnCode string `xml:"return_code"`
|
return notify.NewNotify(pay.cfg)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
package pay
|
package refund
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/pay/config"
|
||||||
"github.com/silenceper/wechat/util"
|
"github.com/silenceper/wechat/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var refundGateway = "https://api.mch.weixin.qq.com/secapi/pay/refund"
|
var refundGateway = "https://api.mch.weixin.qq.com/secapi/pay/refund"
|
||||||
|
|
||||||
//RefundParams 调用参数
|
// Refund struct extends context
|
||||||
type RefundParams struct {
|
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
|
TransactionID string
|
||||||
OutRefundNo string
|
OutRefundNo string
|
||||||
TotalFee string
|
TotalFee string
|
||||||
@@ -19,8 +31,8 @@ type RefundParams struct {
|
|||||||
RootCa string //ca证书
|
RootCa string //ca证书
|
||||||
}
|
}
|
||||||
|
|
||||||
//refundRequest 接口请求参数
|
//request 接口请求参数
|
||||||
type refundRequest struct {
|
type request struct {
|
||||||
AppID string `xml:"appid"`
|
AppID string `xml:"appid"`
|
||||||
MchID string `xml:"mch_id"`
|
MchID string `xml:"mch_id"`
|
||||||
NonceStr string `xml:"nonce_str"`
|
NonceStr string `xml:"nonce_str"`
|
||||||
@@ -34,8 +46,8 @@ type refundRequest struct {
|
|||||||
//NotifyUrl string `xml:"notify_url,omitempty"`
|
//NotifyUrl string `xml:"notify_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
//RefundResponse 接口返回
|
//Response 接口返回
|
||||||
type RefundResponse struct {
|
type Response struct {
|
||||||
ReturnCode string `xml:"return_code"`
|
ReturnCode string `xml:"return_code"`
|
||||||
ReturnMsg string `xml:"return_msg"`
|
ReturnMsg string `xml:"return_msg"`
|
||||||
AppID string `xml:"appid,omitempty"`
|
AppID string `xml:"appid,omitempty"`
|
||||||
@@ -59,11 +71,11 @@ type RefundResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Refund 退款申请
|
//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)
|
nonceStr := util.RandomStr(32)
|
||||||
param := make(map[string]interface{})
|
param := make(map[string]interface{})
|
||||||
param["appid"] = pcf.AppID
|
param["appid"] = refund.AppID
|
||||||
param["mch_id"] = pcf.PayMchID
|
param["mch_id"] = refund.MchID
|
||||||
param["nonce_str"] = nonceStr
|
param["nonce_str"] = nonceStr
|
||||||
param["out_refund_no"] = p.OutRefundNo
|
param["out_refund_no"] = p.OutRefundNo
|
||||||
param["refund_desc"] = p.RefundDesc
|
param["refund_desc"] = p.RefundDesc
|
||||||
@@ -72,12 +84,12 @@ func (pcf *Pay) Refund(p *RefundParams) (rsp RefundResponse, err error) {
|
|||||||
param["sign_type"] = "MD5"
|
param["sign_type"] = "MD5"
|
||||||
param["transaction_id"] = p.TransactionID
|
param["transaction_id"] = p.TransactionID
|
||||||
|
|
||||||
bizKey := "&key=" + pcf.PayKey
|
bizKey := "&key=" + refund.Key
|
||||||
str := orderParam(param, bizKey)
|
str := util.OrderParam(param, bizKey)
|
||||||
sign := util.MD5Sum(str)
|
sign := util.MD5Sum(str)
|
||||||
request := refundRequest{
|
request := request{
|
||||||
AppID: pcf.AppID,
|
AppID: refund.AppID,
|
||||||
MchID: pcf.PayMchID,
|
MchID: refund.MchID,
|
||||||
NonceStr: nonceStr,
|
NonceStr: nonceStr,
|
||||||
Sign: sign,
|
Sign: sign,
|
||||||
SignType: "MD5",
|
SignType: "MD5",
|
||||||
@@ -87,7 +99,7 @@ func (pcf *Pay) Refund(p *RefundParams) (rsp RefundResponse, err error) {
|
|||||||
RefundFee: p.RefundFee,
|
RefundFee: p.RefundFee,
|
||||||
RefundDesc: p.RefundDesc,
|
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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
67
util/param.go
Normal file
67
util/param.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OrderParam 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
|
||||||
|
}
|
||||||
119
wechat.go
119
wechat.go
@@ -1,125 +1,42 @@
|
|||||||
package wechat
|
package wechat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/silenceper/wechat/cache"
|
|
||||||
"github.com/silenceper/wechat/context"
|
|
||||||
"github.com/silenceper/wechat/device"
|
|
||||||
"github.com/silenceper/wechat/js"
|
|
||||||
"github.com/silenceper/wechat/material"
|
|
||||||
"github.com/silenceper/wechat/menu"
|
|
||||||
"github.com/silenceper/wechat/message"
|
|
||||||
"github.com/silenceper/wechat/miniprogram"
|
"github.com/silenceper/wechat/miniprogram"
|
||||||
"github.com/silenceper/wechat/oauth"
|
miniConfig "github.com/silenceper/wechat/miniprogram/config"
|
||||||
|
payConfig "github.com/silenceper/wechat/pay/config"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/officialaccount"
|
||||||
|
offConfig "github.com/silenceper/wechat/officialaccount/config"
|
||||||
"github.com/silenceper/wechat/pay"
|
"github.com/silenceper/wechat/pay"
|
||||||
"github.com/silenceper/wechat/qr"
|
|
||||||
"github.com/silenceper/wechat/server"
|
|
||||||
"github.com/silenceper/wechat/tcb"
|
|
||||||
"github.com/silenceper/wechat/user"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Wechat struct
|
// Wechat struct
|
||||||
type Wechat struct {
|
type Wechat struct {
|
||||||
Context *context.Context
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config for user
|
// Config for user
|
||||||
type Config struct {
|
type Config struct {
|
||||||
AppID string
|
PayMchID string //支付 - 商户 ID
|
||||||
AppSecret string
|
PayNotifyURL string //支付 - 接受微信支付结果通知的接口地址
|
||||||
Token string
|
PayKey string //支付 - 商户后台设置的支付 key
|
||||||
EncodingAESKey string
|
|
||||||
PayMchID string //支付 - 商户 ID
|
|
||||||
PayNotifyURL string //支付 - 接受微信支付结果通知的接口地址
|
|
||||||
PayKey string //支付 - 商户后台设置的支付 key
|
|
||||||
Cache cache.Cache
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWechat init
|
// NewWechat init
|
||||||
func NewWechat(cfg *Config) *Wechat {
|
func NewWechat() *Wechat {
|
||||||
context := new(context.Context)
|
return &Wechat{}
|
||||||
copyConfigToContext(cfg, context)
|
|
||||||
return &Wechat{context}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyConfigToContext(cfg *Config, context *context.Context) {
|
//GetOfficialAccount 获取微信公众号实例
|
||||||
context.AppID = cfg.AppID
|
func (wc *Wechat) GetOfficialAccount(cfg *offConfig.Config) *officialaccount.OfficialAccount {
|
||||||
context.AppSecret = cfg.AppSecret
|
return officialaccount.NewOfficialAccount(cfg)
|
||||||
context.Token = cfg.Token
|
|
||||||
context.EncodingAESKey = cfg.EncodingAESKey
|
|
||||||
context.PayMchID = cfg.PayMchID
|
|
||||||
context.PayKey = cfg.PayKey
|
|
||||||
context.PayNotifyURL = cfg.PayNotifyURL
|
|
||||||
context.Cache = cfg.Cache
|
|
||||||
context.SetAccessTokenLock(new(sync.RWMutex))
|
|
||||||
context.SetJsAPITicketLock(new(sync.RWMutex))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetServer 消息管理
|
|
||||||
func (wc *Wechat) GetServer(req *http.Request, writer http.ResponseWriter) *server.Server {
|
|
||||||
wc.Context.Request = req
|
|
||||||
wc.Context.Writer = writer
|
|
||||||
return server.NewServer(wc.Context)
|
|
||||||
}
|
|
||||||
|
|
||||||
//GetAccessToken 获取access_token
|
|
||||||
func (wc *Wechat) GetAccessToken() (string, error) {
|
|
||||||
return wc.Context.GetAccessToken()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOauth oauth2网页授权
|
|
||||||
func (wc *Wechat) GetOauth() *oauth.Oauth {
|
|
||||||
return oauth.NewOauth(wc.Context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMaterial 素材管理
|
|
||||||
func (wc *Wechat) GetMaterial() *material.Material {
|
|
||||||
return material.NewMaterial(wc.Context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetJs js-sdk配置
|
|
||||||
func (wc *Wechat) GetJs() *js.Js {
|
|
||||||
return js.NewJs(wc.Context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMenu 菜单管理接口
|
|
||||||
func (wc *Wechat) GetMenu() *menu.Menu {
|
|
||||||
return menu.NewMenu(wc.Context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUser 用户管理接口
|
|
||||||
func (wc *Wechat) GetUser() *user.User {
|
|
||||||
return user.NewUser(wc.Context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTemplate 模板消息接口
|
|
||||||
func (wc *Wechat) GetTemplate() *message.Template {
|
|
||||||
return message.NewTemplate(wc.Context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPay 返回支付消息的实例
|
|
||||||
func (wc *Wechat) GetPay() *pay.Pay {
|
|
||||||
return pay.NewPay(wc.Context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetQR 返回二维码的实例
|
|
||||||
func (wc *Wechat) GetQR() *qr.QR {
|
|
||||||
return qr.NewQR(wc.Context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMiniProgram 获取小程序的实例
|
// GetMiniProgram 获取小程序的实例
|
||||||
func (wc *Wechat) GetMiniProgram() *miniprogram.MiniProgram {
|
func (wc *Wechat) GetMiniProgram(cfg *miniConfig.Config) *miniprogram.MiniProgram {
|
||||||
return miniprogram.NewMiniProgram(wc.Context)
|
return miniprogram.NewMiniProgram(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDevice 获取智能设备的实例
|
// GetPay 获取微信支付的实例
|
||||||
func (wc *Wechat) GetDevice() *device.Device {
|
func (wc *Wechat) GetPay(cfg *payConfig.Config) *pay.Pay {
|
||||||
return device.NewDevice(wc.Context)
|
return pay.NewPay(cfg)
|
||||||
}
|
|
||||||
|
|
||||||
// GetTcb 获取小程序-云开发的实例
|
|
||||||
func (wc *Wechat) GetTcb() *tcb.Tcb {
|
|
||||||
return tcb.NewTcb(wc.Context)
|
|
||||||
}
|
}
|
||||||
|
|||||||
5
work/README.md
Normal file
5
work/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 企业微信
|
||||||
|
|
||||||
|
[官方文档](https://work.weixin.qq.com/api/doc)
|
||||||
|
|
||||||
|
## 快速入门
|
||||||
Reference in New Issue
Block a user