mirror of
https://github.com/silenceper/wechat.git
synced 2026-02-04 21:02:25 +08:00
Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b578ebd3c | ||
|
|
843ed3fa8a | ||
|
|
8762af2441 | ||
|
|
bfd23056eb | ||
|
|
fedcd371d0 | ||
|
|
6569d47301 | ||
|
|
05e23e0d88 | ||
|
|
6279dadd29 | ||
|
|
a357c82080 | ||
|
|
ae271960e2 | ||
|
|
45522f003f | ||
|
|
a264ce4266 | ||
|
|
3c881e3885 | ||
|
|
3740bb55c3 | ||
|
|
c42d799367 | ||
|
|
798c5b081c | ||
|
|
491ee80136 | ||
|
|
a54b03a918 | ||
|
|
eec3233134 | ||
|
|
587ce04b5f | ||
|
|
ada9c1ff61 | ||
|
|
9e58e097cb | ||
|
|
8b6147c3ec | ||
|
|
0071852c75 | ||
|
|
f4491193cb | ||
|
|
576a898c0f | ||
|
|
76fde58ad9 | ||
|
|
903dadc260 | ||
|
|
1efbf27bde | ||
|
|
99a2eb659c | ||
|
|
9b06954b10 | ||
|
|
431f7d3a9f | ||
|
|
8e24b47a70 | ||
|
|
83bd282760 | ||
|
|
bf167d959c | ||
|
|
08b69d9419 | ||
|
|
ab39ec00d4 | ||
|
|
cafb84d6da | ||
|
|
d46df74eee | ||
|
|
96678d2279 | ||
|
|
6e5bb2553d | ||
|
|
1dbe3f60ea | ||
|
|
0f99e2e34a | ||
|
|
7ea817a7c6 | ||
|
|
de140f1037 | ||
|
|
d91e82c183 | ||
|
|
3111e12c00 | ||
|
|
9c8717afba | ||
|
|
d7f371cb65 | ||
|
|
39e81b45bf | ||
|
|
733c53a044 | ||
|
|
5617c9512d | ||
|
|
d806d1c968 | ||
|
|
254ac9d7a6 | ||
|
|
19e3174107 | ||
|
|
0dbc292f44 | ||
|
|
33d00f45c5 | ||
|
|
c4a361bbf6 | ||
|
|
f4e58b0712 | ||
|
|
5b307df969 | ||
|
|
b16d231a29 | ||
|
|
c30319c74c | ||
|
|
76c1832798 | ||
|
|
f6d07aa714 | ||
|
|
1475417a64 | ||
|
|
bf456aa77b | ||
|
|
934ca61b3b | ||
|
|
00ca733814 | ||
|
|
2e796b21d3 | ||
|
|
412c2f0ea9 | ||
|
|
8cb724ece0 | ||
|
|
13facb6df8 | ||
|
|
55615762eb | ||
|
|
1fc4cc70ec | ||
|
|
e09031b58c | ||
|
|
279ff79406 | ||
|
|
7bde39a634 | ||
|
|
1711aeb46d | ||
|
|
87470f143d | ||
|
|
900a54ee06 | ||
|
|
7170f6ef32 | ||
|
|
0c9dd16f1f | ||
|
|
f68f9d6f5e | ||
|
|
09dabb232d | ||
|
|
3ea624f832 | ||
|
|
546dce2396 | ||
|
|
abd7f512ba | ||
|
|
b8239ef9a9 | ||
|
|
0dffcde475 | ||
|
|
4e6fd625da | ||
|
|
453089e83e | ||
|
|
bb97bddc08 | ||
|
|
54e2c82fff | ||
|
|
fab09a0bbe | ||
|
|
f8ab592606 | ||
|
|
fcf289cfe3 | ||
|
|
c458f44917 | ||
|
|
688bca7436 | ||
|
|
1a9600b49f | ||
|
|
dbb43ac7ad | ||
|
|
79ff0321e3 | ||
|
|
44dae6e950 | ||
|
|
894b1972d7 | ||
|
|
6c1ed39487 | ||
|
|
529323e6b5 | ||
|
|
9a4d41563e | ||
|
|
81f26cd6dc | ||
|
|
eae6caadb2 | ||
|
|
fdd9768a96 | ||
|
|
42332eca27 | ||
|
|
b614b55cdf | ||
|
|
3fc556c425 | ||
|
|
3005852946 | ||
|
|
5652af6aab | ||
|
|
57bc7aabba | ||
|
|
3be94cd80d | ||
|
|
58d6810432 | ||
|
|
0871e2f8ed | ||
|
|
6e1ec1f00c | ||
|
|
823c54fda5 | ||
|
|
e66652f4b5 | ||
|
|
f4f1860e67 | ||
|
|
02b3fcc648 | ||
|
|
4f5945fb0f | ||
|
|
b0f1f71f37 | ||
|
|
963a2d39bd | ||
|
|
862e546367 | ||
|
|
5677b60759 | ||
|
|
61476d351d | ||
|
|
eda287070d | ||
|
|
443435343c | ||
|
|
036183e5ff | ||
|
|
593df23c46 | ||
|
|
50c490df31 | ||
|
|
7a19587f6a | ||
|
|
91d1c77abc | ||
|
|
4607ef001e | ||
|
|
fedd5a96ca | ||
|
|
3984f13c76 | ||
|
|
2da9755c58 | ||
|
|
d5302dbfdc | ||
|
|
a6b1c56c25 | ||
|
|
388fd9ec07 | ||
|
|
7163fc80c9 | ||
|
|
d67206b106 | ||
|
|
efdf09e133 | ||
|
|
9a34dca9a1 | ||
|
|
546ffb9155 | ||
|
|
f6b2887cee | ||
|
|
ebf6158b7c | ||
|
|
3e3cb594a3 |
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: https://silenceper.com/img/wechat-pay.jpeg
|
||||
2
.github/ISSUE_TEMPLATE.md
vendored
Normal file
2
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
## 问题及现象
|
||||
<!-- 描述你的问题现象,报错**贴截图**粘贴或者贴具体信息,提供**必要的代码段**
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -24,5 +24,6 @@ _testmain.go
|
||||
*.prof
|
||||
.DS_Store
|
||||
.vscode/
|
||||
vendor/*/
|
||||
vendor
|
||||
.idea/
|
||||
examples/tcb/*
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.13.x
|
||||
- 1.12.x
|
||||
- 1.11.x
|
||||
- 1.10.x
|
||||
- 1.9.x
|
||||
- 1.8.x
|
||||
|
||||
services:
|
||||
- memcached
|
||||
@@ -12,7 +11,7 @@ services:
|
||||
|
||||
before_script:
|
||||
- GO_FILES=$(find . -iname '*.go' -type f | grep -v /vendor/)
|
||||
- go get github.com/golang/lint/golint
|
||||
- go get golang.org/x/lint/golint
|
||||
|
||||
script:
|
||||
- go test -v -race ./...
|
||||
|
||||
136
README.md
136
README.md
@@ -1,9 +1,10 @@
|
||||
## 📢 注意: 此分支为v1版本,已不再维护更新,请切换至 [v2](https://github.com/silenceper/wechat/tree/release-2.0)!
|
||||
|
||||
# WeChat SDK for Go
|
||||
[](https://travis-ci.org/silenceper/wechat)
|
||||
[](https://goreportcard.com/report/github.com/silenceper/wechat)
|
||||
[](http://godoc.org/github.com/silenceper/wechat)
|
||||
|
||||
|
||||
使用Golang开发的微信SDK,简单、易用。
|
||||
|
||||
## 快速开始
|
||||
@@ -96,6 +97,8 @@ Cache主要用来保存全局access_token以及js-sdk中的ticket:
|
||||
- 检验access_token是否有效
|
||||
- 获取js-sdk配置
|
||||
- [素材管理](#素材管理)
|
||||
- [小程序开发](#小程序开发)
|
||||
- [小程序-云开发](./tcb)
|
||||
|
||||
## 消息管理
|
||||
|
||||
@@ -282,8 +285,8 @@ type Reply struct {
|
||||
#### 回复图片消息
|
||||
```go
|
||||
//mediaID 可通过素材管理-上上传多媒体文件获得
|
||||
image :=message.NewVideo("mediaID")
|
||||
return &message.Reply{message.MsgTypeVideo, image}
|
||||
image :=message.NewImage("mediaID")
|
||||
return &message.Reply{message.MsgTypeImage, image}
|
||||
```
|
||||
#### 回复视频消息
|
||||
```go
|
||||
@@ -335,14 +338,14 @@ Url :点击图文消息跳转链接
|
||||
|
||||
## 自定义菜单
|
||||
|
||||
通过` wechat.GetMenu(req, writer)`获取menu的实例
|
||||
通过` wechat.GetMenu()`获取menu的实例
|
||||
|
||||
### 自定义菜单创建接口
|
||||
|
||||
以下是一个创建二级菜单的例子
|
||||
|
||||
```go
|
||||
mu := wc.GetMenu(c.Request, c.Writer)
|
||||
mu := wc.GetMenu()
|
||||
|
||||
buttons := make([]*menu.Button, 1)
|
||||
btn := new(menu.Button)
|
||||
@@ -402,7 +405,7 @@ func (btn *Button) SetViewLimitedButton(name, mediaID string) {
|
||||
### 自定义菜单查询接口
|
||||
|
||||
```go
|
||||
mu := wc.GetMenu(c.Request, c.Writer)
|
||||
mu := wc.GetMenu()
|
||||
resMenu,err:=mu.GetMenu()
|
||||
```
|
||||
>返回结果 resMenu 结构参考 ./menu/menu.go 中ResMenu 结构体
|
||||
@@ -410,7 +413,7 @@ resMenu,err:=mu.GetMenu()
|
||||
### 自定义菜单删除接口
|
||||
|
||||
```go
|
||||
mu := wc.GetMenu(c.Request, c.Writer)
|
||||
mu := wc.GetMenu()
|
||||
err:=mu.DeleteMenu()
|
||||
```
|
||||
|
||||
@@ -458,7 +461,7 @@ func (menu *Menu) GetCurrentSelfMenuInfo() (resSelfMenuInfo ResSelfMenuInfo, err
|
||||
**1.发起授权**
|
||||
|
||||
```go
|
||||
oauth := wc.GetOauth(c.Request, c.Writer)
|
||||
oauth := wc.GetOauth()
|
||||
err := oauth.Redirect("跳转的绝对地址", "snsapi_userinfo", "123dd123")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
@@ -505,7 +508,7 @@ func (oauth *Oauth) CheckAccessToken(accessToken, openID string) (b bool, err er
|
||||
### 获取js-sdk配置
|
||||
|
||||
```go
|
||||
js := wc.GetJs(c.Request, c.Writer)
|
||||
js := wc.GetJs()
|
||||
cfg, err := js.GetConfig("传入需要的调用js-sdk的url地址")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
@@ -529,6 +532,121 @@ type Config struct {
|
||||
|
||||
[素材管理API](https://godoc.org/github.com/silenceper/wechat/material#Material)
|
||||
|
||||
### 批量获取永久素材
|
||||
|
||||
```go
|
||||
list, err := wc.GetMaterial().BatchGetMaterial(material.PermanentMaterialTypeNews, 0, 10)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(list)
|
||||
```
|
||||
|
||||
## 小程序开发
|
||||
|
||||
获取小程序操作对象
|
||||
|
||||
``` go
|
||||
memCache=cache.NewMemcache("127.0.0.1:11211")
|
||||
config := &wechat.Config{
|
||||
AppID: "xxx",
|
||||
AppSecret: "xxx",
|
||||
Cache: memCache=cache.NewMemcache("127.0.0.1:11211"),
|
||||
}
|
||||
wc := wechat.NewWechat(config)
|
||||
|
||||
wxa := wc.GetMiniProgram()
|
||||
```
|
||||
|
||||
### 小程序登录凭证校验
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) Code2Session(jsCode string) (result ResCode2Session, err error)
|
||||
```
|
||||
|
||||
### 小程序数据统计
|
||||
|
||||
**获取用户访问小程序日留存**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisDailyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error)
|
||||
```
|
||||
|
||||
**获取用户访问小程序月留存**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisMonthlyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error)
|
||||
```
|
||||
|
||||
**获取用户访问小程序周留存**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisWeeklyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error)
|
||||
```
|
||||
|
||||
**获取用户访问小程序数据概况**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisDailySummary(beginDate, endDate string) (result ResAnalysisDailySummary, err error)
|
||||
```
|
||||
|
||||
**获取用户访问小程序数据日趋势**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisDailyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error)
|
||||
```
|
||||
|
||||
**获取用户访问小程序数据月趋势**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisMonthlyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error)
|
||||
```
|
||||
|
||||
**获取用户访问小程序数据周趋势**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisWeeklyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error)
|
||||
```
|
||||
|
||||
**获取小程序新增或活跃用户的画像分布数据**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisUserPortrait(beginDate, endDate string) (result ResAnalysisUserPortrait, err error)
|
||||
```
|
||||
|
||||
**获取用户小程序访问分布数据**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisVisitDistribution(beginDate, endDate string) (result ResAnalysisVisitDistribution, err error)
|
||||
```
|
||||
|
||||
**获取小程序页面访问数据**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisVisitPage(beginDate, endDate string) (result ResAnalysisVisitPage, err error)
|
||||
```
|
||||
|
||||
### 小程序二维码生成
|
||||
|
||||
**获取小程序二维码,适用于需要的码数量较少的业务场景**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) CreateWXAQRCode(coderParams QRCoder) (response []byte, err error)
|
||||
```
|
||||
|
||||
**获取小程序码,适用于需要的码数量较少的业务场景**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetWXACode(coderParams QRCoder) (response []byte, err error)
|
||||
```
|
||||
|
||||
**获取小程序码,适用于需要的码数量极多的业务场景**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetWXACodeUnlimit(coderParams QRCoder) (response []byte, err error)
|
||||
```
|
||||
|
||||
|
||||
更多API使用请参考 godoc :
|
||||
[https://godoc.org/github.com/silenceper/wechat](https://godoc.org/github.com/silenceper/wechat)
|
||||
|
||||
74
cache/memory.go
vendored
Normal file
74
cache/memory.go
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
//Memory struct contains *memcache.Client
|
||||
type Memory struct {
|
||||
sync.Mutex
|
||||
|
||||
data map[string]*data
|
||||
}
|
||||
|
||||
type data struct {
|
||||
Data interface{}
|
||||
Expired time.Time
|
||||
}
|
||||
|
||||
//NewMemory create new memcache
|
||||
func NewMemory() *Memory {
|
||||
return &Memory{
|
||||
data: map[string]*data{},
|
||||
}
|
||||
}
|
||||
|
||||
//Get return cached value
|
||||
func (mem *Memory) Get(key string) interface{} {
|
||||
if ret, ok := mem.data[key]; ok {
|
||||
if ret.Expired.Before(time.Now()) {
|
||||
mem.deleteKey(key)
|
||||
return nil
|
||||
}
|
||||
return ret.Data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsExist check value exists in memcache.
|
||||
func (mem *Memory) IsExist(key string) bool {
|
||||
if ret, ok := mem.data[key]; ok {
|
||||
if ret.Expired.Before(time.Now()) {
|
||||
mem.deleteKey(key)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//Set cached value with key and expire time.
|
||||
func (mem *Memory) Set(key string, val interface{}, timeout time.Duration) (err error) {
|
||||
mem.Lock()
|
||||
defer mem.Unlock()
|
||||
|
||||
mem.data[key] = &data{
|
||||
Data: val,
|
||||
Expired: time.Now().Add(timeout),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//Delete delete value in memcache.
|
||||
func (mem *Memory) Delete(key string) error {
|
||||
return mem.deleteKey(key)
|
||||
}
|
||||
|
||||
// deleteKey
|
||||
func (mem *Memory) deleteKey(key string) error {
|
||||
mem.Lock()
|
||||
defer mem.Unlock()
|
||||
delete(mem.data, key)
|
||||
return nil
|
||||
}
|
||||
53
cloudbase/README.md
Normal file
53
cloudbase/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# 小程序-云开发 SDK
|
||||
|
||||
[云开发(CloudBase)](https://www.cloudbase.net/)是基于Serverless架构构建的一站式后端云服务,涵盖函数、数据库、存储、CDN等服务,免后端运维,支持小程序、Web和APP开发。 其中,小程序·云开发是微信和腾讯云联合推出的云端一体化解决方案,基于云开发可以免鉴权调用微信所有开放能力,在微信开发者工具中即可开通使用。
|
||||
|
||||
|
||||
|
||||
## 使用说明
|
||||
**引入依赖**
|
||||
>推荐使用go module 进行管理
|
||||
|
||||
```
|
||||
go get github.com/silenceper/wechat@v1.2.3
|
||||
```
|
||||
|
||||
**初始化配置**
|
||||
|
||||
```golang
|
||||
//使用memcache保存access_token,也可选择redis或自定义cache
|
||||
memCache=cache.NewMemcache("127.0.0.1:11211")
|
||||
|
||||
//配置小程序参数
|
||||
config := &wechat.Config{
|
||||
AppID: "your app id",
|
||||
AppSecret: "your app secret",
|
||||
Cache: memCache,
|
||||
}
|
||||
wc := wechat.NewWechat(config)
|
||||
wcTcb := wc.GetTcb()
|
||||
```
|
||||
|
||||
### 使用API
|
||||
|
||||
#### 触发云函数
|
||||
```golang
|
||||
res, err := wcTcb.InvokeCloudFunction("test-xxxx", "add", `{"a":1,"b":2}`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
更多使用方法参考[pkg.go.dev](https://pkg.go.dev/github.com/silenceper/wechat@v1.2.3/tcb?tab=doc#Tcb)
|
||||
|
||||
## Demo
|
||||
### 使用wechat sdk开发一个留言板
|
||||
|
||||
这是一个使用wechat sdk来完成一个留言板的例子,使用到了云开发中的云函数,数据库,存储API:
|
||||
|
||||
- [起步:项目搭建](./guestbook-demo/start.md)
|
||||
- [数据库:调用云开发数据库实现文本保存](./guestbook-demo/database.md)
|
||||
- [云函数:调用云函数实现文本过滤](./guestbook-demo/cloudfunctions.md)
|
||||
- [云开发存储:实现留言本附件上传](./guestbook-demo/storage.md)
|
||||
|
||||
以上文中的所有代码都上传在 [https://github.com/go-demo/guestbook](https://github.com/go-demo/guestbook)
|
||||
100
cloudbase/guestbook-demo/cloudfunctions.md
Normal file
100
cloudbase/guestbook-demo/cloudfunctions.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 云开发存储:实现留言本附件上传
|
||||
## API说明
|
||||
云开发中云函数[文档说明](https://developers.weixin.qq.com/minigame/dev/wxcloud/reference-http-api/functions/),可以先阅读原始http api需要的参数以及说明
|
||||
|
||||
**基本流程:**
|
||||
|
||||
1. 创建云函数
|
||||
1. 通过微信开发者工具编写云函数
|
||||
1. 利用SDK实现云函数的调用
|
||||
|
||||
云函数调用主要使用到了sdk中 `InvokeCloudFunction` 方法的使用:
|
||||
|
||||
```go
|
||||
func (tcb *Tcb) InvokeCloudFunction(env, name, args string) (*InvokeCloudFunctionRes, error)
|
||||
```
|
||||
**参数说明:**<br />1、第一个参数为云开发的环境<br />2、第二个参数为云函数名称<br />3、第三个参数为需要传入的参数,这里传入一个json,方便在云函数中接收并处理,函数的返回值也是json<br />**<br />**返回结果:**
|
||||
|
||||
```go
|
||||
type InvokeCloudFunctionRes struct {
|
||||
util.CommonError
|
||||
RespData string `json:"resp_data"` //云函数返回的buffer
|
||||
}
|
||||
```
|
||||
|
||||
> util.CommonError :包含了errcode和errmsg字段,因为微信http api中的返回结果都会包含这两个字段,所以作为了一个公共的struct
|
||||
|
||||
|
||||
这里演示如何通过云函数实现对文本内容的过滤,比如对关键字的过滤。
|
||||
<a name="s4cYj"></a>
|
||||
## 创建一个云函数
|
||||
打开微信开发者工具,在cloudfunctions中创建一个filterText云函数:<br />
|
||||
|
||||
其中index.js文本内容实现了对关键字的替换,内容如下:
|
||||
|
||||
```javascript
|
||||
// 云函数入口文件
|
||||
//敏感词
|
||||
var keywords = ["色情"]
|
||||
|
||||
// 云函数入口函数
|
||||
exports.main = async(event, context) => {
|
||||
let {
|
||||
text
|
||||
} = event
|
||||
keywords.map(word => {
|
||||
let regExp = new RegExp(word, 'g')
|
||||
text = text.replace(regExp, "****")
|
||||
})
|
||||
return {
|
||||
text
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里实现了对关键字 `色情` 替换为 `****` 。
|
||||
<a name="MIN25"></a>
|
||||
## 调用云函数
|
||||
|
||||
在feedbackService中创建FilterText函数实现对云函数的调用,传入原始文本内容,返回最终过滤之后的内容。
|
||||
|
||||
```go
|
||||
//FilterRes 过滤文件的结果
|
||||
type FilterRes struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
//FilterText 调用云函数过滤文本
|
||||
func (svc *FeedbackService) FilterText(text string) (string, error) {
|
||||
res, err := getTcb().InvokeCloudFunction(getConfig().TcbEnv, "filterText", fmt.Sprintf(`{"text":"%s"}`, text))
|
||||
//返回的是json
|
||||
filterRes := &FilterRes{}
|
||||
err = json.Unmarshal([]byte(res.RespData), filterRes)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return filterRes.Text, nil
|
||||
}
|
||||
```
|
||||
这里将云函数调用的返回值保存在FilterRes struct中。
|
||||
|
||||
最后再 feedbackService中的 `Save` 对Content内容进行替换:
|
||||
|
||||
```go
|
||||
//Save 保存内容
|
||||
func (svc *FeedbackService) Save(feedback *Feedback) error {
|
||||
.....
|
||||
//content 调用云函数过滤
|
||||
var err error
|
||||
feedback.Content, err = svc.FilterText(feedback.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
....
|
||||
}
|
||||
```
|
||||
|
||||
最终的效果如下,当我们输入了含有关键字的留言内容最终就会被替换为****:<br />
|
||||
|
||||
|
||||
306
cloudbase/guestbook-demo/database.md
Normal file
306
cloudbase/guestbook-demo/database.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# 数据库:调用云开发数据库实现文本保存
|
||||
|
||||
在这一节,我们主要描述如何利用`wechat sdk`将留言的内容保存在云开发数据库中。
|
||||
|
||||
<a name="RjeGP"></a>
|
||||
## API说明
|
||||
参考微信云开发文档 [数据库篇](https://developers.weixin.qq.com/minigame/dev/wxcloud/reference-http-api/database/#%E6%95%B0%E6%8D%AE%E5%BA%93),可以先阅读其原始的api提供的方法和说明,在[SDK DOC](https://pkg.go.dev/github.com/silenceper/wechat/tcb?tab=doc#Tcb.DatabaseAdd)中都可以找到对应的方法以及参数。
|
||||
|
||||
主要利用到[SDK DOC](https://pkg.go.dev/github.com/silenceper/wechat/tcb?tab=doc#Tcb.DatabaseAdd)中的如下方法,其他方法可在文档中找到,sdk文档中以 `Database` 开头的方法即为数据库相关的方法调用。
|
||||
```go
|
||||
func (tcb *Tcb) DatabaseAdd(env, query string) (*DatabaseAddRes, error) //数据库内容保存
|
||||
func (tcb *Tcb) DatabaseCount(env, query string) (*DatabaseCountRes, error)//数据库计数
|
||||
func (tcb *Tcb) DatabaseQuery(env, query string) (*DatabaseQueryRes, error)//数据库内容查询
|
||||
```
|
||||
返回结果对应字段说明: [`DatabaseAddRes`](https://pkg.go.dev/github.com/silenceper/wechat/tcb?tab=doc#DatabaseAddRes) , [`DatabaseCountRes`](https://pkg.go.dev/github.com/silenceper/wechat/tcb?tab=doc#DatabaseCountRes) , [`DatabaseQueryRes`](https://pkg.go.dev/github.com/silenceper/wechat/tcb?tab=doc#DatabaseQueryRes)
|
||||
|
||||
<a name="VKuDQ"></a>
|
||||
## 包引入
|
||||
本例中引入的WeChat sdk版本为v1.2.3版本,通过如下方法引入
|
||||
```bash
|
||||
go get github.com/silenceper/wechat@v1.2.3
|
||||
```
|
||||
可以在go.mod文件中看到引入的包以及对应的版本:
|
||||
```go
|
||||
module github.com/go-demo/guestbook
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.5.0
|
||||
github.com/silenceper/wechat v1.2.3 // indirect
|
||||
)
|
||||
```
|
||||
|
||||
<a name="m7jnm"></a>
|
||||
## 保存至云开发数据库
|
||||
<a name="a095b5de"></a>
|
||||
### WeChat SDK配置
|
||||
为了方便在其他方法中调用<br />创建config.go用于解析云开发对应的配置参数,appkey,app_secret等:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/silenceper/wechat"
|
||||
"github.com/silenceper/wechat/cache"
|
||||
"github.com/silenceper/wechat/tcb"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
//Config 配置信息
|
||||
type Config struct {
|
||||
TcbEnv string `yaml:"tcb_env"`
|
||||
AppID string `yaml:"app_id"`
|
||||
AppSecret string `yaml:"app_secret"`
|
||||
}
|
||||
|
||||
var cfg *Config
|
||||
var _ = getConfig()
|
||||
|
||||
//通过getConfig方法获取配置参数
|
||||
func getConfig() *Config {
|
||||
if cfg != nil {
|
||||
return cfg
|
||||
}
|
||||
data, err := ioutil.ReadFile("./config.yaml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cfg = &Config{}
|
||||
err = yaml.Unmarshal(data, cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
var wechatTcb *tcb.Tcb
|
||||
var _ = getTcb()
|
||||
//通过getTcb获取wechat sdk的配置参数
|
||||
func getTcb() *tcb.Tcb {
|
||||
if wechatTcb != nil {
|
||||
return wechatTcb
|
||||
}
|
||||
memCache := cache.NewMemory()
|
||||
|
||||
//配置小程序参数
|
||||
config := &wechat.Config{
|
||||
AppID: getConfig().AppID,
|
||||
AppSecret: getConfig().AppSecret,
|
||||
Cache: memCache,
|
||||
}
|
||||
wc := wechat.NewWechat(config)
|
||||
wechatTcb = wc.GetTcb()
|
||||
return wechatTcb
|
||||
}
|
||||
```
|
||||
|
||||
其中config.yaml写入三个配置参数:
|
||||
|
||||
```yaml
|
||||
tcb_env: test-6ku2s //云开发环境
|
||||
app_id: xxxxxx //云开发appid
|
||||
app_secret: xxxxxxxxx //云开发对应的app secret
|
||||
```
|
||||
|
||||
<a name="VbDg9"></a>
|
||||
### 调用API
|
||||
为了方便在其他方法中方便调用sdk中的方法,这里新建一个 `feedbackService` struct,创建对应的save方法用于保存留言, `feedback.go` :
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
//FeedbackService service
|
||||
type FeedbackService struct {
|
||||
}
|
||||
|
||||
//NewFeedbackService new
|
||||
func NewFeedbackService() *FeedbackService {
|
||||
return &FeedbackService{}
|
||||
}
|
||||
|
||||
//Feedback 留言记录
|
||||
type Feedback struct {
|
||||
Username string `form:"username",json:"username"`
|
||||
Content string `form:"content",json:"content"`
|
||||
FilePath string `json:"filePath"`//文件路径
|
||||
FileID string `json:"fileId"` //存放文件
|
||||
CreateTime string `json:"createTime"`
|
||||
}
|
||||
|
||||
func (svc *FeedbackService) Save(feedback *Feedback) error {
|
||||
if feedback.Username == "" || feedback.Content == "" {
|
||||
return fmt.Errorf("用户名或留言内容不能为空")
|
||||
}
|
||||
query := `db.collection(\"%s\").add({
|
||||
data: [{
|
||||
username: \"%s\",
|
||||
content: \"%s\",
|
||||
filePath: \"%s\",
|
||||
fileId: \"%s\",
|
||||
createTime: \"%s\",
|
||||
}]
|
||||
})`
|
||||
feedback.CreateTime = time.Now().Format("2006-01-02 15:04:05")
|
||||
query = fmt.Sprintf(query, "guestbook", feedback.Username, feedback.Content, feedback.FilePath, feedback.FileID, feedback.CreateTime)
|
||||
_, err := getTcb().DatabaseAdd(getConfig().TcbEnv, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
其中对于数据库中的query语句可以参考[https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/database/add.html](https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/database/add.html)
|
||||
|
||||
这里调用 `DatabaseAdd` 方法实现内容的保存。
|
||||
|
||||
<a name="8db0f827"></a>
|
||||
## 接收表单提交内容
|
||||
将表单提交的内容提交到 `/feedback` 路由中,并创建 `feedback`方法接收表单提交的参数,
|
||||
|
||||
```go
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
//包含html模板
|
||||
r.LoadHTMLGlob("./template/*")
|
||||
//渲染留言页面
|
||||
r.GET("/",index)
|
||||
//提交留言
|
||||
r.POST("/feedback", feedback)
|
||||
|
||||
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
|
||||
}
|
||||
```
|
||||
|
||||
其中feedback方法如下:
|
||||
|
||||
```go
|
||||
//接收提交的内容
|
||||
func feedback(c *gin.Context) {
|
||||
//1、接收提交参数
|
||||
feedback := &Feedback{}
|
||||
err := c.Bind(feedback)
|
||||
if err != nil {
|
||||
showError(c, err)
|
||||
return
|
||||
}
|
||||
feedbackService := NewFeedbackService()
|
||||
//保存内容
|
||||
err = feedbackService.Save(feedback)
|
||||
if err != nil {
|
||||
showError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusMovedPermanently, "/")
|
||||
}
|
||||
```
|
||||
|
||||
这里通过c.Bind方法将form表单中的内容绑定到 `Feedback` stuct中,再通过调用feedbackService中的Save方法对文本内容进行保存。
|
||||
<a name="DlBj4"></a>
|
||||
## 展示留言内容
|
||||
留言内容的展示主要分为两步,一先从数据库展示出来,二是将留言内容展示在页面:<br />查询的sql语句为:
|
||||
```go
|
||||
db.collection("guestbook").orderBy('createTime','desc').skip(0).limit(10).get()
|
||||
```
|
||||
|
||||
在 `feedback.go` 中新增List方法,其中参数传入skip和limit参数用于分页
|
||||
|
||||
```go
|
||||
//List 文本列表
|
||||
func (svc *FeedbackService) List(skip, limit int) ([]*Feedback, error) {
|
||||
query := fmt.Sprintf("db.collection(\"guestbook\").orderBy('createTime','desc').skip(%d).limit(%d).get()", skip, limit)
|
||||
data, err := getTcb().DatabaseQuery(getConfig().TcbEnv, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
feedbacks := make([]*Feedback, 0, len(data.Data))
|
||||
for _, v := range data.Data {
|
||||
feedbackItem := &Feedback{}
|
||||
err := json.Unmarshal([]byte(v), feedbackItem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
feedbacks = append(feedbacks, feedbackItem)
|
||||
|
||||
}
|
||||
//fmt.Println(data.Pager)
|
||||
return feedbacks, nil
|
||||
}
|
||||
```
|
||||
|
||||
这里主要调用 `DatabaseQuery` 方法对db进行查询。
|
||||
|
||||
在main.go中的index方法在从数据中获取的数据取出并渲染在index.html中:
|
||||
|
||||
```go
|
||||
//首页
|
||||
func index(c *gin.Context) {
|
||||
page := c.Query("page")
|
||||
//获取记录数量
|
||||
feedbackService := NewFeedbackService()
|
||||
count, err := feedbackService.Count()
|
||||
if err != nil {
|
||||
showError(c, err)
|
||||
return
|
||||
}
|
||||
limit := 10
|
||||
totalPage := math.Ceil(float64(count) / float64(limit))
|
||||
totalPageInt := int(totalPage)
|
||||
pageInt, _ := strconv.Atoi(page)
|
||||
if pageInt > totalPageInt {
|
||||
pageInt = totalPageInt
|
||||
}
|
||||
if pageInt < 1 {
|
||||
pageInt = 1
|
||||
}
|
||||
|
||||
//展示留言列表
|
||||
skip := (pageInt - 1) * limit
|
||||
list, err := feedbackService.List(skip, limit)
|
||||
if err != nil {
|
||||
showError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": "留言板",
|
||||
"list": list,
|
||||
"prevPage": pageInt - 1,
|
||||
"nextPage": pageInt + 1,
|
||||
"page": pageInt,
|
||||
"totalPage": totalPageInt,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
其中index.html通过go template语法对内容进行渲染
|
||||
|
||||
```html
|
||||
<div class="list-group list-group-flush">
|
||||
{{range .list}}
|
||||
<div class="list-group-item">
|
||||
<div><span><b>{{.Username}} 在 {{.CreateTime}} 说:</b></span></div>
|
||||
<div><p>{{.Content}}</p></div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div>
|
||||
<span>第{{.page}}页</span>
|
||||
{{if gt .page 1}}<a href="/?page={{.prevPage}}">上一页</a>{{end}}
|
||||
{{if lt .page .totalPage}} <a href="/?page={{.nextPage}}">下一页</a>{{end}}
|
||||
</div>
|
||||
```
|
||||
|
||||
这样就实现了对文本内容的保存
|
||||
|
||||
185
cloudbase/guestbook-demo/start.md
Normal file
185
cloudbase/guestbook-demo/start.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# 起步:项目搭建
|
||||
|
||||
<a name="8MeIi"></a>
|
||||
## 目标
|
||||
通过完成一个留言板应用来熟悉云开发中go sdk中的使用,主要分为以下三个内容
|
||||
|
||||
1. 如何利用云开发中的[数据库](https://developers.weixin.qq.com/minigame/dev/wxcloud/reference-http-api/database/#%E6%95%B0%E6%8D%AE%E5%BA%93)进行留言内容的保存
|
||||
1. 使用[云函数](https://developers.weixin.qq.com/minigame/dev/wxcloud/reference-http-api/functions/)进行文本内容的过滤
|
||||
1. 使用[云开发存储](https://developers.weixin.qq.com/minigame/dev/wxcloud/reference-http-api/storage/)能力进行附件的保存
|
||||
|
||||
<a name="Nq75E"></a>
|
||||
## 环境介绍
|
||||
<a name="Kxmk0"></a>
|
||||
### Golang 1.13
|
||||
项目中使用Golang 1.13版本进行开发,并且使用go module 进行依赖管理
|
||||
<a name="H0cFe"></a>
|
||||
### 编辑器:Goland
|
||||
代码编辑工具
|
||||
<a name="PsxLG"></a>
|
||||
### 热编译工具:Gowatch
|
||||
Go 程序热编译工具,提升开发效率<br />官网地址: [https://github.com/silenceper/gowatch](https://github.com/silenceper/gowatch)<br />**快速安装:**
|
||||
```basic
|
||||
go get -u github.com/silenceper/gowatch
|
||||
```
|
||||
|
||||
<a name="tNyRl"></a>
|
||||
### web开发框架-Gin
|
||||
一个web开发框架,方便快速构建一个web应用<br />官网: [https://github.com/gin-gonic/gin](https://github.com/gin-gonic/gin)
|
||||
<a name="MzLXD"></a>
|
||||
### Wechat SDK For Go
|
||||
使用Golang对微信公众号,小程序,云开发等API进行封装,使得Go项目中可以方便上手<br />官网: [https://github.com/silenceper/wechat/](https://github.com/silenceper/wechat/) <br />文档:[https://pkg.go.dev/github.com/silenceper/wechat/tcb?tab=doc](https://pkg.go.dev/github.com/silenceper/wechat/tcb?tab=doc)
|
||||
<a name="pNPPj"></a>
|
||||
### 云开发
|
||||
集成数据库,存储,云函数等功能的平台<br />使用文档:[https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/](https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/)<br />在开始开发前,请注册一个小程序获取 `app_id` , `app_secret`参数,并开启云开发功能。
|
||||
|
||||
<a name="qyAfI"></a>
|
||||
## 初始化项目
|
||||
> 本项目中使用go module进行依赖管理
|
||||
|
||||
在工作目录创建一个项目`guestbook`,并使用`go mod init github.com/go-demo/guestbook`进行初始化,后面接的是`import path`。
|
||||
|
||||
```bash
|
||||
mkdir guestbook
|
||||
cd guestbook
|
||||
go mod init github.com/go-demo/guestbook
|
||||
```
|
||||
|
||||
<a name="JvK7M"></a>
|
||||
### 编写main.go文件
|
||||
使用goland编辑器中打开这个项目,并创建一个`main.go`文件,内容如下:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"message": "pong",
|
||||
})
|
||||
})
|
||||
r.Run() // listen and serve on 0.0.0.0:8080
|
||||
}
|
||||
```
|
||||
|
||||
<a name="q2Vtn"></a>
|
||||
### 编译并运行
|
||||
我们可以在Goland编辑器中Terminal面板中进入项目目录,使用`gowatch`命令对该项目进行热编译,看到图片中的log输出表示已经启动成功:
|
||||
> gowatch会监听项目中文件的变化,当进行变化后,对项目进行build 和run,这样我们就可以在一边修改代码一边对项目进行编译及时发现错误,是不是效率提升了呢 :>
|
||||
|
||||
|
||||
<br />(初次build会通过go module自动下载依赖,请注意开启go module功能)
|
||||
|
||||
我们通过访问`127.0.0.1:8080/ping`就可以看到页面上输出`{"message":"pong"}`说明服务启动成功。
|
||||
|
||||
<a name="yXtGW"></a>
|
||||
## 渲染留言页面
|
||||
我们可以先规划我们的UI是怎么样子?
|
||||
|
||||
包含两部分:
|
||||
|
||||
- 留言框:包含留言内容,附件上传,用户名,提交按钮
|
||||
- 内容展示:展示留言内容,附件以及留言者和留言日期
|
||||
|
||||
界面展示如下:<br />
|
||||
<a name="2vKHj"></a>
|
||||
### 创建模板文件
|
||||
对应的html代码如下,我们保存在项目中的template/index.html文件中:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>留言板</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.4.1/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
|
||||
</head>
|
||||
<body class="container-md">
|
||||
<h3>留言板</h3>
|
||||
<div>
|
||||
<form action="/feedback" method="post" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<textarea class="form-control" name="content" id="content" cols="50" rows="5"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="file">附件</label>
|
||||
<input type="file" class="form-control-file" name="file" id="">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="username">名字</label>
|
||||
<input type="text" name="username" class="form-control"></div>
|
||||
<div class="form-group"><input type="submit" value="提交" class="btn btn-primary"></div>
|
||||
</form>
|
||||
<h2>内容</h2>
|
||||
<div class="list-group list-group-flush">
|
||||
<div class="list-group-item">
|
||||
<div><span><b>silenceper 在 2020-01-21 12:33:45 说:</b></span></div>
|
||||
<div><p>留言板内容</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>第1页</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
这里引入了bootstrap样式文件,不需要自己写太多前端样式了,出来的UI也不会太难看。
|
||||
|
||||
<a name="MLcML"></a>
|
||||
### 通过gin渲染模板
|
||||
我们想要通过访问`127.0.0.1:8080`直接访问到这个留言页面,main.go中代码如下:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
//包含html模板
|
||||
r.LoadHTMLGlob("./template/*")
|
||||
//渲染留言页面,GET请求,通过根路径可以直接访问
|
||||
//当路径匹配成功后,进入index访问进行处理
|
||||
r.GET("/",index)
|
||||
|
||||
|
||||
r.Run() // listen and serve on 0.0.0.0:8080
|
||||
}
|
||||
|
||||
//渲染留言板首页
|
||||
func index(c *gin.Context) {
|
||||
//返回200,并渲染index.html页面
|
||||
c.HTML(http.StatusOK,"index.html",gin.H{
|
||||
"title":"留言板",
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
其中`r.LoadHTMLGlob("./template/*") `指定了html模板的位置,这样在使用进行`c.HTML`进行渲染的时候就知道到哪个位置进行查找了。
|
||||
|
||||
**c.HTML说明:**
|
||||
|
||||
- 第一个参数:http状态码
|
||||
- 第二个参数:需要渲染的模板
|
||||
- 第三个参数:需要传递的值(`gin.H`其实是一个`map[string]interface{}`的别名)
|
||||
|
||||
这里`c.HTML`渲染了`index.html`,并以`200`状态码输出,第三个参数`gin.H`,传入`key:value` ,就可以在`index.html`页面中使用go-template语法进行值的替换,语法格式:
|
||||
|
||||
`{{.title}}`
|
||||
|
||||
这里可以查阅gin文档:[如何进行html渲染](https://github.com/gin-gonic/gin#html-rendering)
|
||||
|
||||
<a name="XXc0s"></a>
|
||||
## 代码地址
|
||||
本文中所有代码都上传在 [https://github.com/go-demo/guestbook](https://github.com/go-demo/guestbook)
|
||||
|
||||
144
cloudbase/guestbook-demo/storage.md
Normal file
144
cloudbase/guestbook-demo/storage.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# 云开发存储:实现留言本附件上传
|
||||
## API说明
|
||||
官方文档:[微信云开发存储文档](https://developers.weixin.qq.com/minigame/dev/wxcloud/reference-http-api/storage/),主要提供了三个API(上传,下载,删除),可以先分别看下参数
|
||||
|
||||
在这一节中主要利用到了sdk中的附件上传和下载的方法,方法如下:
|
||||
```go
|
||||
func (tcb *Tcb) UploadFile(env, path string) (*UploadFileRes, error) {
|
||||
func (tcb *Tcb) BatchDownloadFile(env string, fileList []*DownloadFile) (*BatchDownloadFileRes, error) {
|
||||
```
|
||||
|
||||
返回对应的返回结果在这里可以查看,[UploadFileRes](https://pkg.go.dev/github.com/silenceper/wechat/tcb?tab=doc#UploadFileRes),[BatchDownloadFileRes](https://pkg.go.dev/github.com/silenceper/wechat/tcb?tab=doc#BatchDownloadFileRes)
|
||||
|
||||
<a name="264b81c5"></a>
|
||||
## 接收附件上传内容
|
||||
1、保存index.html中form表单中使用的是post方法并且enctype为 `multipart/form-data` :
|
||||
|
||||
```html
|
||||
<form action="/feedback" method="post" enctype="multipart/form-data">
|
||||
```
|
||||
2、在main.go中的feedback方法中获取上传的文件
|
||||
|
||||
```go
|
||||
//2、文件上传
|
||||
fileHeader, err := c.FormFile("file")
|
||||
if err != nil && err != http.ErrMissingFile {
|
||||
showError(c, err)
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
<a name="186268da"></a>
|
||||
## 将附件保存在云开发中
|
||||
在feedbackService中创建 `UploadFile` 方法,调用sdk中文件上传方法实现对文件的上传:<br />UploadFile 第一个参数为云开发环境,第二个参数为需要附件需要保存的路径
|
||||
> 注意,这里path应该为相对路径。不能为绝对路径比如 /guestbook 应该为 guestbook 否则会报错。
|
||||
|
||||
|
||||
```go
|
||||
|
||||
//UploadFile 上传文件
|
||||
func (svc *FeedbackService) UploadFile(path string, file io.Reader) (string, error) {
|
||||
//获取文件上传链接
|
||||
uploadRes, err := getTcb().UploadFile(getConfig().TcbEnv, path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data := make(map[string]io.Reader)
|
||||
data["key"] = strings.NewReader(path)
|
||||
data["Signature"] = strings.NewReader(uploadRes.Authorization)
|
||||
data["x-cos-security-token"] = strings.NewReader(uploadRes.Token)
|
||||
data["x-cos-meta-fileid"] = strings.NewReader(uploadRes.CosFileID)
|
||||
data["file"] = file
|
||||
|
||||
//上传文件
|
||||
_, err = goutils.PostFormWithFile(&http.Client{}, uploadRes.URL, data)
|
||||
return uploadRes.FileID, err
|
||||
}
|
||||
```
|
||||
|
||||
其中 `PostFormWithFile` 方法是对 `mime/multipart` 方法的一个封装,用于将附件内容上传到指定的url中,在 `github.com/silenceper/goutils` 包中。
|
||||
|
||||
并在main.go中feedback方法调用并将返回的field和Filename保存在db中
|
||||
|
||||
```go
|
||||
//2、文件上传
|
||||
fileHeader, err := c.FormFile("file")
|
||||
if err != nil && err != http.ErrMissingFile {
|
||||
showError(c, err)
|
||||
return
|
||||
}
|
||||
feedbackService := NewFeedbackService()
|
||||
if fileHeader != nil {
|
||||
path := fmt.Sprintf("guestbook/%s", fileHeader.Filename)
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
showError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
fileID, err := feedbackService.UploadFile(path, file)
|
||||
if err != nil {
|
||||
showError(c, err)
|
||||
return
|
||||
}
|
||||
feedback.FilePath = fileHeader.Filename
|
||||
feedback.FileID = fileID
|
||||
}
|
||||
```
|
||||
|
||||
最终附件可以在index方法通过数据库查询在html中展示出来
|
||||
|
||||
<a name="786a132e"></a>
|
||||
## 附件下载
|
||||
这里创建一个单独的路由,用于对附件进行下载,通过在get参数中传入fileId,定位到附件,并打开附件路径:
|
||||
|
||||
```go
|
||||
r.GET("/file", downloadFile)
|
||||
```
|
||||
|
||||
```go
|
||||
//附件下载
|
||||
func downloadFile(c *gin.Context) {
|
||||
fileID := c.Query("id")
|
||||
if fileID == "" {
|
||||
showError(c, fmt.Errorf("fileID为空"))
|
||||
return
|
||||
}
|
||||
downLoadURL, err := NewFeedbackService().DownloadFile(fileID)
|
||||
if err != nil {
|
||||
showError(c, err)
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusMovedPermanently, downLoadURL)
|
||||
}
|
||||
```
|
||||
|
||||
在feedbackService中创建DownloadFile方法,返回真实下载路径,并跳转:
|
||||
|
||||
```go
|
||||
|
||||
//DownloadFile 获取下载链接
|
||||
func (svc *FeedbackService) DownloadFile(id string) (string, error) {
|
||||
files := []*tcb.DownloadFile{&tcb.DownloadFile{
|
||||
FileID: id,
|
||||
MaxAge: 100,
|
||||
}}
|
||||
res, err := getTcb().BatchDownloadFile(getConfig().TcbEnv, files)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(res.FileList) >= 1 {
|
||||
return res.FileList[0].DownloadURL, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
```
|
||||
|
||||
这里BatchDownloadFile方法第一个参数是云开发环境,第二个参数接收的 `tcb.DownloadFile` 数组,指定fileID和下载链接的有效期。
|
||||
|
||||
最终完成的效果如下:<br />
|
||||
|
||||
|
||||
本文中所有代码都上传在 [https://github.com/go-demo/guestbook](https://github.com/go-demo/guestbook)
|
||||
|
||||
@@ -22,16 +22,27 @@ type ResAccessToken struct {
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
//GetAccessTokenFunc 获取 access token 的函数签名
|
||||
type GetAccessTokenFunc func(ctx *Context) (accessToken string, err error)
|
||||
|
||||
//SetAccessTokenLock 设置读写锁(一个appID一个读写锁)
|
||||
func (ctx *Context) SetAccessTokenLock(l *sync.RWMutex) {
|
||||
ctx.accessTokenLock = l
|
||||
}
|
||||
|
||||
//SetGetAccessTokenFunc 设置自定义获取accessToken的方式, 需要自己实现缓存
|
||||
func (ctx *Context) SetGetAccessTokenFunc(f GetAccessTokenFunc) {
|
||||
ctx.accessTokenFunc = f
|
||||
}
|
||||
|
||||
//GetAccessToken 获取access_token
|
||||
func (ctx *Context) GetAccessToken() (accessToken string, err error) {
|
||||
ctx.accessTokenLock.Lock()
|
||||
defer ctx.accessTokenLock.Unlock()
|
||||
|
||||
if ctx.accessTokenFunc != nil {
|
||||
return ctx.accessTokenFunc(ctx)
|
||||
}
|
||||
accessTokenCacheKey := fmt.Sprintf("access_token_%s", ctx.AppID)
|
||||
val := ctx.Cache.Get(accessTokenCacheKey)
|
||||
if val != nil {
|
||||
|
||||
30
context/access_token_test.go
Normal file
30
context/access_token_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestContext_SetCustomAccessTokenFunc(t *testing.T) {
|
||||
ctx := Context{
|
||||
accessTokenLock: new(sync.RWMutex),
|
||||
}
|
||||
f := func(ctx *Context) (accessToken string, err error) {
|
||||
return "fake token", nil
|
||||
}
|
||||
ctx.SetGetAccessTokenFunc(f)
|
||||
res, err := ctx.GetAccessToken()
|
||||
if res != "fake token" || err != nil {
|
||||
t.Error("expect fake token but error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContext_NoSetCustomAccessTokenFunc(t *testing.T) {
|
||||
ctx := Context{
|
||||
accessTokenLock: new(sync.RWMutex),
|
||||
}
|
||||
|
||||
if ctx.accessTokenFunc != nil {
|
||||
t.Error("error accessTokenFunc")
|
||||
}
|
||||
}
|
||||
221
context/component_access_token.go
Normal file
221
context/component_access_token.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
componentAccessTokenURL = "https://api.weixin.qq.com/cgi-bin/component/api_component_token"
|
||||
getPreCodeURL = "https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token=%s"
|
||||
queryAuthURL = "https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=%s"
|
||||
refreshTokenURL = "https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=%s"
|
||||
getComponentInfoURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=%s"
|
||||
getComponentConfigURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option?component_access_token=%s"
|
||||
)
|
||||
|
||||
// ComponentAccessToken 第三方平台
|
||||
type ComponentAccessToken struct {
|
||||
AccessToken string `json:"component_access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
// GetComponentAccessToken 获取 ComponentAccessToken
|
||||
func (ctx *Context) GetComponentAccessToken() (string, error) {
|
||||
accessTokenCacheKey := fmt.Sprintf("component_access_token_%s", ctx.AppID)
|
||||
val := ctx.Cache.Get(accessTokenCacheKey)
|
||||
if val == nil {
|
||||
return "", fmt.Errorf("cann't get component access token")
|
||||
}
|
||||
return val.(string), nil
|
||||
}
|
||||
|
||||
// SetComponentAccessToken 通过component_verify_ticket 获取 ComponentAccessToken
|
||||
func (ctx *Context) SetComponentAccessToken(verifyTicket string) (*ComponentAccessToken, error) {
|
||||
body := map[string]string{
|
||||
"component_appid": ctx.AppID,
|
||||
"component_appsecret": ctx.AppSecret,
|
||||
"component_verify_ticket": verifyTicket,
|
||||
}
|
||||
respBody, err := util.PostJSON(componentAccessTokenURL, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
at := &ComponentAccessToken{}
|
||||
if err := json.Unmarshal(respBody, at); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessTokenCacheKey := fmt.Sprintf("component_access_token_%s", ctx.AppID)
|
||||
expires := at.ExpiresIn - 1500
|
||||
ctx.Cache.Set(accessTokenCacheKey, at.AccessToken, time.Duration(expires)*time.Second)
|
||||
return at, nil
|
||||
}
|
||||
|
||||
// GetPreCode 获取预授权码
|
||||
func (ctx *Context) GetPreCode() (string, error) {
|
||||
cat, err := ctx.GetComponentAccessToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req := map[string]string{
|
||||
"component_appid": ctx.AppID,
|
||||
}
|
||||
uri := fmt.Sprintf(getPreCodeURL, cat)
|
||||
body, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var ret struct {
|
||||
PreCode string `json:"pre_auth_code"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &ret); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return ret.PreCode, nil
|
||||
}
|
||||
|
||||
// ID 微信返回接口中各种类型字段
|
||||
type ID struct {
|
||||
ID int `json:"id"`
|
||||
}
|
||||
|
||||
// AuthBaseInfo 授权的基本信息
|
||||
type AuthBaseInfo struct {
|
||||
AuthrAccessToken
|
||||
FuncInfo []AuthFuncInfo `json:"func_info"`
|
||||
}
|
||||
|
||||
// AuthFuncInfo 授权的接口内容
|
||||
type AuthFuncInfo struct {
|
||||
FuncscopeCategory ID `json:"funcscope_category"`
|
||||
}
|
||||
|
||||
// AuthrAccessToken 授权方AccessToken
|
||||
type AuthrAccessToken struct {
|
||||
Appid string `json:"authorizer_appid"`
|
||||
AccessToken string `json:"authorizer_access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
RefreshToken string `json:"authorizer_refresh_token"`
|
||||
}
|
||||
|
||||
// QueryAuthCode 使用授权码换取公众号或小程序的接口调用凭据和授权信息
|
||||
func (ctx *Context) QueryAuthCode(authCode string) (*AuthBaseInfo, error) {
|
||||
cat, err := ctx.GetComponentAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := map[string]string{
|
||||
"component_appid": ctx.AppID,
|
||||
"authorization_code": authCode,
|
||||
}
|
||||
uri := fmt.Sprintf(queryAuthURL, cat)
|
||||
body, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ret struct {
|
||||
Info *AuthBaseInfo `json:"authorization_info"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret.Info, nil
|
||||
}
|
||||
|
||||
// RefreshAuthrToken 获取(刷新)授权公众号或小程序的接口调用凭据(令牌)
|
||||
func (ctx *Context) RefreshAuthrToken(appid, refreshToken string) (*AuthrAccessToken, error) {
|
||||
cat, err := ctx.GetComponentAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := map[string]string{
|
||||
"component_appid": ctx.AppID,
|
||||
"authorizer_appid": appid,
|
||||
"authorizer_refresh_token": refreshToken,
|
||||
}
|
||||
uri := fmt.Sprintf(refreshTokenURL, cat)
|
||||
body, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := &AuthrAccessToken{}
|
||||
if err := json.Unmarshal(body, ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authrTokenKey := "authorizer_access_token_" + appid
|
||||
ctx.Cache.Set(authrTokenKey, ret.AccessToken, time.Minute*80)
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// GetAuthrAccessToken 获取授权方AccessToken
|
||||
func (ctx *Context) GetAuthrAccessToken(appid string) (string, error) {
|
||||
authrTokenKey := "authorizer_access_token_" + appid
|
||||
val := ctx.Cache.Get(authrTokenKey)
|
||||
if val == nil {
|
||||
return "", fmt.Errorf("cannot get authorizer %s access token", appid)
|
||||
}
|
||||
return val.(string), nil
|
||||
}
|
||||
|
||||
// AuthorizerInfo 授权方详细信息
|
||||
type AuthorizerInfo struct {
|
||||
NickName string `json:"nick_name"`
|
||||
HeadImg string `json:"head_img"`
|
||||
ServiceTypeInfo ID `json:"service_type_info"`
|
||||
VerifyTypeInfo ID `json:"verify_type_info"`
|
||||
UserName string `json:"user_name"`
|
||||
PrincipalName string `json:"principal_name"`
|
||||
BusinessInfo struct {
|
||||
OpenStore string `json:"open_store"`
|
||||
OpenScan string `json:"open_scan"`
|
||||
OpenPay string `json:"open_pay"`
|
||||
OpenCard string `json:"open_card"`
|
||||
OpenShake string `json:"open_shake"`
|
||||
}
|
||||
Alias string `json:"alias"`
|
||||
QrcodeURL string `json:"qrcode_url"`
|
||||
}
|
||||
|
||||
// GetAuthrInfo 获取授权方的帐号基本信息
|
||||
func (ctx *Context) GetAuthrInfo(appid string) (*AuthorizerInfo, *AuthBaseInfo, error) {
|
||||
cat, err := ctx.GetComponentAccessToken()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req := map[string]string{
|
||||
"component_appid": ctx.AppID,
|
||||
"authorizer_appid": appid,
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(getComponentInfoURL, cat)
|
||||
body, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var ret struct {
|
||||
AuthorizerInfo *AuthorizerInfo `json:"authorizer_info"`
|
||||
AuthorizationInfo *AuthBaseInfo `json:"authorization_info"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &ret); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return ret.AuthorizerInfo, ret.AuthorizationInfo, nil
|
||||
}
|
||||
19
context/component_test.go
Normal file
19
context/component_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testdata = `{"authorizer_info":{"nick_name":"就爱浪","head_img":"http:\/\/wx.qlogo.cn\/mmopen\/xPKCxELaaj6hiaTZGv19oQPBJibb7hBoKmNOjQibCNOUycE8iaBhiaHOA6eC8hadQSAUZTuHUJl4qCIbCQGjSWialicfzWh4mdxuejY\/0","service_type_info":{"id":1},"verify_type_info":{"id":-1},"user_name":"gh_dcdbaa6f1687","alias":"ckeyer","qrcode_url":"http:\/\/mmbiz.qpic.cn\/mmbiz_jpg\/FribWCoIzQbAX7R1PQ8iaxGonqKp0doYD2ibhC0uhx11LrRcblASiazsbQJTJ4icQnMzfH7G0SUPuKbibTA8Cs4uk5WQ\/0","business_info":{"open_pay":0,"open_shake":0,"open_scan":0,"open_card":0,"open_store":0},"idc":1,"principal_name":"个人","signature":"不折腾会死。"},"authorization_info":{"authorizer_appid":"yyyyy","authorizer_refresh_token":"xxxx","func_info":[{"funcscope_category":{"id":1}},{"funcscope_category":{"id":15}},{"funcscope_category":{"id":4}},{"funcscope_category":{"id":7}},{"funcscope_category":{"id":2}},{"funcscope_category":{"id":3}},{"funcscope_category":{"id":11}},{"funcscope_category":{"id":6}},{"funcscope_category":{"id":5}},{"funcscope_category":{"id":8}},{"funcscope_category":{"id":13}},{"funcscope_category":{"id":9}},{"funcscope_category":{"id":12}},{"funcscope_category":{"id":22}},{"funcscope_category":{"id":23}},{"funcscope_category":{"id":24},"confirm_info":{"need_confirm":0,"already_confirm":0,"can_confirm":0}},{"funcscope_category":{"id":26}},{"funcscope_category":{"id":27},"confirm_info":{"need_confirm":0,"already_confirm":0,"can_confirm":0}},{"funcscope_category":{"id":33},"confirm_info":{"need_confirm":0,"already_confirm":0,"can_confirm":0}},{"funcscope_category":{"id":35}}]}}`
|
||||
|
||||
// TestDecode
|
||||
func TestDecode(t *testing.T) {
|
||||
var ret struct {
|
||||
AuthorizerInfo *AuthorizerInfo `json:"authorizer_info"`
|
||||
AuthorizationInfo *AuthBaseInfo `json:"authorization_info"`
|
||||
}
|
||||
json.Unmarshal([]byte(testdata), &ret)
|
||||
t.Logf("%+v", ret.AuthorizerInfo)
|
||||
t.Logf("%+v", ret.AuthorizationInfo)
|
||||
}
|
||||
@@ -27,6 +27,9 @@ type Context struct {
|
||||
|
||||
//jsAPITicket 读写锁 同一个AppID一个
|
||||
jsAPITicketLock *sync.RWMutex
|
||||
|
||||
//accessTokenFunc 自定义获取 access token 的方法
|
||||
accessTokenFunc GetAccessTokenFunc
|
||||
}
|
||||
|
||||
// Query returns the keyed url query value if it exists
|
||||
|
||||
116
device/authorize.go
Normal file
116
device/authorize.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// DeviceAdd 添加设备标识
|
||||
DeviceAdd = iota
|
||||
// DeviceUpgrade 更新设备标识
|
||||
DeviceUpgrade
|
||||
)
|
||||
|
||||
type reqDeviceAuthorize struct {
|
||||
// 设备id的个数
|
||||
DeviceNum string `json:"device_num"`
|
||||
// 设备id的列表,json的array格式,其size必须等于device_num
|
||||
DeviceList []ReqDevice `json:"device_list"`
|
||||
// 请求操作的类型,限定取值为:0:设备授权(缺省值为0) 1:设备更新(更新已授权设备的各属性值)
|
||||
OpType string `json:"op_type,omitempty"`
|
||||
// 设备的产品编号(由微信硬件平台分配)。可在公众号设备功能管理页面查询。
|
||||
//当 op_type 为‘0’,product_id 为‘1’时,不要填写 product_id 字段(会引起不必要错误);
|
||||
//当 op_typy 为‘0’,product_id 不为‘1’时,必须填写 product_id 字段;
|
||||
//当 op_type 为 1 时,不要填写 product_id 字段。
|
||||
ProductID string `json:"product_id,omitempty"`
|
||||
}
|
||||
|
||||
//ReqDevice 设备授权实体
|
||||
type ReqDevice struct {
|
||||
// 设备的 device id
|
||||
ID string `json:"id"`
|
||||
// 设备的mac地址 格式采用16进制串的方式(长度为12字节),
|
||||
// 不需要0X前缀,如: 1234567890AB
|
||||
Mac string `json:"mac"`
|
||||
// 支持以下四种连接协议:
|
||||
// android classic bluetooth – 1
|
||||
// ios classic bluetooth – 2
|
||||
// ble – 3
|
||||
// wifi -- 4
|
||||
// 一个设备可以支持多种连接类型,用符号"|"做分割,客户端优先选择靠前的连接方式(优先级按|关系的排序依次降低),举例:
|
||||
// 1:表示设备仅支持andiod classic bluetooth 1|2:表示设备支持andiod 和ios 两种classic bluetooth,但是客户端优先选择andriod classic bluetooth 协议,如果andriod classic bluetooth协议连接失败,再选择ios classic bluetooth协议进行连接
|
||||
// (注:安卓平台不同时支持BLE和classic类型)
|
||||
ConnectProtocol string `json:"connect_protocol"`
|
||||
//auth及通信的加密key,第三方需要将key烧制在设备上(128bit),格式采用16进制串的方式(长度为32字节),不需要0X前缀,如: 1234567890ABCDEF1234567890ABCDEF
|
||||
AuthKey string `json:"auth_key"`
|
||||
// 断开策略,目前支持: 1:退出公众号页面时即断开连接 2:退出公众号之后保持连接不断开
|
||||
CloseStrategy string `json:"close_strategy"`
|
||||
//连接策略,32位整型,按bit位置位,目前仅第1bit和第3bit位有效(bit置0为无效,1为有效;第2bit已被废弃),且bit位可以按或置位(如1|4=5),各bit置位含义说明如下:
|
||||
//1:(第1bit置位)在公众号对话页面,不停的尝试连接设备
|
||||
//4:(第3bit置位)处于非公众号页面(如主界面等),微信自动连接。当用户切换微信到前台时,可能尝试去连接设备,连上后一定时间会断开
|
||||
ConnStrategy string `json:"conn_strategy"`
|
||||
// auth version,设备和微信进行auth时,会根据该版本号来确认auth buf和auth key的格式(各version对应的auth buf及key的具体格式可以参看“客户端蓝牙外设协议”),该字段目前支持取值:
|
||||
// 0:不加密的version
|
||||
// 1:version 1
|
||||
AuthVer string `json:"auth_ver"`
|
||||
// 表示mac地址在厂商广播manufature data里含有mac地址的偏移,取值如下:
|
||||
// -1:在尾部、
|
||||
// -2:表示不包含mac地址 其他:非法偏移
|
||||
ManuMacPos string `json:"manu_mac_pos"`
|
||||
// 表示mac地址在厂商serial number里含有mac地址的偏移,取值如下:
|
||||
// -1:表示在尾部
|
||||
// -2:表示不包含mac地址 其他:非法偏移
|
||||
SerMacPost string `json:"ser_mac_post"`
|
||||
// 精简协议类型,取值如下:计步设备精简协议:1 (若该字段填1,connect_protocol 必须包括3。非精简协议设备切勿填写该字段)
|
||||
BleSimpleProtocol string `json:"ble_simple_protocol,omitempty"`
|
||||
}
|
||||
|
||||
//ResBaseInfo 授权回调实体
|
||||
type ResBaseInfo struct {
|
||||
BaseInfo struct {
|
||||
DeviceType string `json:"device_type"`
|
||||
DeviceID string `json:"device_id"`
|
||||
} `json:"base_info"`
|
||||
}
|
||||
|
||||
// 授权回调根信息
|
||||
type resDeviceAuthorize struct {
|
||||
util.CommonError
|
||||
Resp []ResBaseInfo `json:"resp"`
|
||||
}
|
||||
|
||||
// DeviceAuthorize 设备授权
|
||||
func (d *Device) DeviceAuthorize(devices []ReqDevice, opType int, product string) (res []ResBaseInfo, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = d.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", uriAuthorize, accessToken)
|
||||
req := reqDeviceAuthorize{
|
||||
DeviceNum: fmt.Sprintf("%d", len(devices)),
|
||||
DeviceList: devices,
|
||||
OpType: fmt.Sprintf("%d", opType),
|
||||
ProductID: product,
|
||||
}
|
||||
var response []byte
|
||||
response, err = util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result resDeviceAuthorize
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("DeviceAuthorize Error , errcode=%d , errmsg=%s", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
res = result.Resp
|
||||
return
|
||||
}
|
||||
106
device/bind.go
Normal file
106
device/bind.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
// ReqBind 设备绑定解绑共通实体
|
||||
type ReqBind struct {
|
||||
Ticket string `json:"ticket,omitempty"`
|
||||
DeviceID string `json:"device_id"`
|
||||
OpenID string `json:"openid"`
|
||||
}
|
||||
type resBind struct {
|
||||
BaseResp util.CommonError `json:"base_resp"`
|
||||
}
|
||||
|
||||
// Bind 设备绑定
|
||||
func (d *Device) Bind(req ReqBind) (err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = d.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", uriBind, accessToken)
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(uri, req); err != nil {
|
||||
return
|
||||
}
|
||||
var result resBind
|
||||
if err = json.Unmarshal(response, &result); err != nil {
|
||||
return
|
||||
}
|
||||
if result.BaseResp.ErrCode != 0 {
|
||||
err = fmt.Errorf("DeviceBind Error , errcode=%d , errmsg=%s", result.BaseResp.ErrCode, result.BaseResp.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Unbind 设备解绑
|
||||
func (d *Device) Unbind(req ReqBind) (err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = d.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", uriUnbind, accessToken)
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(uri, req); err != nil {
|
||||
return
|
||||
}
|
||||
var result resBind
|
||||
if err = json.Unmarshal(response, &result); err != nil {
|
||||
return
|
||||
}
|
||||
if result.BaseResp.ErrCode != 0 {
|
||||
err = fmt.Errorf("DeviceBind Error , errcode=%d , errmsg=%s", result.BaseResp.ErrCode, result.BaseResp.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CompelBind 强制绑定用户和设备
|
||||
func (d *Device) CompelBind(req ReqBind) (err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = d.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", uriCompelBind, accessToken)
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(uri, req); err != nil {
|
||||
return
|
||||
}
|
||||
var result resBind
|
||||
if err = json.Unmarshal(response, &result); err != nil {
|
||||
return
|
||||
}
|
||||
if result.BaseResp.ErrCode != 0 {
|
||||
err = fmt.Errorf("DeviceBind Error , errcode=%d , errmsg=%s", result.BaseResp.ErrCode, result.BaseResp.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CompelUnbind 强制解绑用户和设备
|
||||
func (d *Device) CompelUnbind(req ReqBind) (err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = d.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", uriCompelUnbind, accessToken)
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(uri, req); err != nil {
|
||||
return
|
||||
}
|
||||
var result resBind
|
||||
if err = json.Unmarshal(response, &result); err != nil {
|
||||
return
|
||||
}
|
||||
if result.BaseResp.ErrCode != 0 {
|
||||
err = fmt.Errorf("DeviceBind Error , errcode=%d , errmsg=%s", result.BaseResp.ErrCode, result.BaseResp.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
60
device/device.go
Normal file
60
device/device.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
uriAuthorize = "https://api.weixin.qq.com/device/authorize_device"
|
||||
uriQRCode = "https://api.weixin.qq.com/device/create_qrcode"
|
||||
uriVerifyQRCode = "https://api.weixin.qq.com/device/verify_qrcode"
|
||||
uriBind = "https://api.weixin.qq.com/device/bind"
|
||||
uriUnbind = "https://api.weixin.qq.com/device/unbind"
|
||||
uriCompelBind = "https://api.weixin.qq.com/device/compel_bind"
|
||||
uriCompelUnbind = "https://api.weixin.qq.com/device/compel_unbind"
|
||||
uriState = "https://api.weixin.qq.com/device/get_stat"
|
||||
)
|
||||
|
||||
//Device struct
|
||||
type Device struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
//NewDevice 实例
|
||||
func NewDevice(context *context.Context) *Device {
|
||||
device := new(Device)
|
||||
device.Context = context
|
||||
return device
|
||||
}
|
||||
|
||||
// ResDeviceState 设备状态响应实体
|
||||
type ResDeviceState struct {
|
||||
util.CommonError
|
||||
Status int `json:"status"`
|
||||
StatusInfo string `json:"status_info"`
|
||||
}
|
||||
|
||||
// State 设备状态查询
|
||||
func (d *Device) State(device string) (res ResDeviceState, err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = d.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s&device_id=%s", uriState, accessToken, device)
|
||||
var response []byte
|
||||
if response, err = util.HTTPGet(uri); err != nil {
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal(response, &res); err != nil {
|
||||
return
|
||||
}
|
||||
if res.ErrCode != 0 {
|
||||
err = fmt.Errorf("DeviceState Error , errcode=%d , errmsg=%s", res.ErrCode, res.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
9
device/message.go
Normal file
9
device/message.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package device
|
||||
|
||||
//MsgDevice 设备消息响应
|
||||
type MsgDevice struct {
|
||||
DeviceType string
|
||||
DeviceID string
|
||||
SessionID string
|
||||
OpenID string
|
||||
}
|
||||
76
device/qrcode.go
Normal file
76
device/qrcode.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
//ResCreateQRCode 获取二维码的返回实体
|
||||
type ResCreateQRCode struct {
|
||||
util.CommonError
|
||||
DeviceNum int `json:"device_num"`
|
||||
CodeList []struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
Ticket string `json:"ticket"`
|
||||
} `json:"code_list"`
|
||||
}
|
||||
|
||||
// CreateQRCode 获取设备二维码
|
||||
func (d *Device) CreateQRCode(devices []string) (res ResCreateQRCode, err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = d.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", uriQRCode, accessToken)
|
||||
req := map[string]interface{}{
|
||||
"device_num": len(devices),
|
||||
"device_id_list": devices,
|
||||
}
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(uri, req); err != nil {
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal(response, &res); err != nil {
|
||||
return
|
||||
}
|
||||
if res.ErrCode != 0 {
|
||||
err = fmt.Errorf("DeviceCreateQRCode Error , errcode=%d , errmsg=%s", res.ErrCode, res.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//ResVerifyQRCode 验证授权结果实体
|
||||
type ResVerifyQRCode struct {
|
||||
util.CommonError
|
||||
DeviceType string `json:"device_type"`
|
||||
DeviceID string `json:"device_id"`
|
||||
Mac string `json:"mac"`
|
||||
}
|
||||
|
||||
// VerifyQRCode 验证设备二维码
|
||||
func (d *Device) VerifyQRCode(ticket string) (res ResVerifyQRCode, err error) {
|
||||
var accessToken string
|
||||
if accessToken, err = d.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", uriVerifyQRCode, accessToken)
|
||||
req := map[string]interface{}{
|
||||
"ticket": ticket,
|
||||
}
|
||||
fmt.Println(req)
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(uri, req); err != nil {
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal(response, &res); err != nil {
|
||||
return
|
||||
}
|
||||
if res.ErrCode != 0 {
|
||||
err = fmt.Errorf("DeviceCreateQRCode Error , errcode=%d , errmsg=%s", res.ErrCode, res.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
22
go.mod
Normal file
22
go.mod
Normal file
@@ -0,0 +1,22 @@
|
||||
module github.com/silenceper/wechat
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/astaxie/beego v1.7.1
|
||||
github.com/bradfitz/gomemcache v0.0.0-20160117192205-fb1f79c6b65a
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/gin-gonic/gin v1.1.4
|
||||
github.com/golang/protobuf v0.0.0-20161117033126-8ee79997227b // indirect
|
||||
github.com/gomodule/redigo v1.8.1
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 // indirect
|
||||
github.com/mattn/go-isatty v0.0.0-20161123143637-30a891c33c7c // indirect
|
||||
github.com/spf13/cast v1.3.1
|
||||
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
|
||||
)
|
||||
50
go.sum
Normal file
50
go.sum
Normal file
@@ -0,0 +1,50 @@
|
||||
github.com/astaxie/beego v1.7.1 h1:TuqX4F9e3ujVEycudgWrwUj11WMppLZyunJKIBoxTFw=
|
||||
github.com/astaxie/beego v1.7.1/go.mod h1:0R4++1tUqERR0WYFWdfkcrsyoVBCG4DgpDGokT3yb+U=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20160117192205-fb1f79c6b65a h1:k5TuEkqEYCRs8+66WdOkswWOj+L/YbP5ruainvn94wg=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20160117192205-fb1f79c6b65a/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/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 v1.8.1 h1:Abmo0bI7Xf0IhdIPc7HZQzZcShdnmxeoVuDDtIQp8N8=
|
||||
github.com/gomodule/redigo v1.8.1/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
|
||||
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/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
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/net v0.0.0-20191125084936-ffdde1057850 h1:Vq85/r8R9IdcUHmZ0/nQlUg1v15rzvQ2sHdnZAj/x7s=
|
||||
golang.org/x/net v0.0.0-20191125084936-ffdde1057850/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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/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=
|
||||
@@ -10,9 +10,25 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
addNewsURL = "https://api.weixin.qq.com/cgi-bin/material/add_news"
|
||||
addMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/add_material"
|
||||
delMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/del_material"
|
||||
addNewsURL = "https://api.weixin.qq.com/cgi-bin/material/add_news"
|
||||
addMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/add_material"
|
||||
delMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/del_material"
|
||||
getMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/get_material"
|
||||
batchGetMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/batchget_material"
|
||||
)
|
||||
|
||||
//PermanentMaterialType 永久素材类型
|
||||
type PermanentMaterialType string
|
||||
|
||||
const (
|
||||
//PermanentMaterialTypeImage 永久素材图片类型(image)
|
||||
PermanentMaterialTypeImage PermanentMaterialType = "image"
|
||||
//PermanentMaterialTypeVideo 永久素材视频类型(video)
|
||||
PermanentMaterialTypeVideo PermanentMaterialType = "video"
|
||||
//PermanentMaterialTypeVoice 永久素材语音类型 (voice)
|
||||
PermanentMaterialTypeVoice PermanentMaterialType = "voice"
|
||||
//PermanentMaterialTypeNews 永久素材图文类型(news)
|
||||
PermanentMaterialTypeNews PermanentMaterialType = "news"
|
||||
)
|
||||
|
||||
//Material 素材管理
|
||||
@@ -31,11 +47,42 @@ func NewMaterial(context *context.Context) *Material {
|
||||
type Article struct {
|
||||
Title string `json:"title"`
|
||||
ThumbMediaID string `json:"thumb_media_id"`
|
||||
ThumbURL string `json:"thumb_url"`
|
||||
Author string `json:"author"`
|
||||
Digest string `json:"digest"`
|
||||
ShowCoverPic int `json:"show_cover_pic"`
|
||||
Content string `json:"content"`
|
||||
ContentSourceURL string `json:"content_source_url"`
|
||||
URL string `json:"url"`
|
||||
DownURL string `json:"down_url"`
|
||||
}
|
||||
|
||||
// GetNews 获取/下载永久素材
|
||||
func (material *Material) GetNews(id string) ([]*Article, error) {
|
||||
accessToken, err := material.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getMaterialURL, accessToken)
|
||||
|
||||
var req struct {
|
||||
MediaID string `json:"media_id"`
|
||||
}
|
||||
req.MediaID = id
|
||||
responseBytes, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res struct {
|
||||
NewsItem []*Article `json:"news_item"`
|
||||
}
|
||||
err = json.Unmarshal(responseBytes, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.NewsItem, nil
|
||||
}
|
||||
|
||||
//reqArticles 永久性图文素材请求信息
|
||||
@@ -62,8 +109,12 @@ func (material *Material) AddNews(articles []*Article) (mediaID string, err erro
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", addNewsURL, accessToken)
|
||||
responseBytes, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var res resArticles
|
||||
err = json.Unmarshal(responseBytes, res)
|
||||
err = json.Unmarshal(responseBytes, &res)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -83,6 +134,7 @@ type resAddMaterial struct {
|
||||
func (material *Material) AddMaterial(mediaType MediaType, filename string) (mediaID string, url string, err error) {
|
||||
if mediaType == MediaTypeVideo {
|
||||
err = errors.New("永久视频素材上传使用 AddVideo 方法")
|
||||
return
|
||||
}
|
||||
var accessToken string
|
||||
accessToken, err = material.GetAccessToken()
|
||||
@@ -138,11 +190,11 @@ func (material *Material) AddVideo(filename, title, introduction string) (mediaI
|
||||
fields := []util.MultipartFormField{
|
||||
{
|
||||
IsFile: true,
|
||||
Fieldname: "video",
|
||||
Fieldname: "media",
|
||||
Filename: filename,
|
||||
},
|
||||
{
|
||||
IsFile: true,
|
||||
IsFile: false,
|
||||
Fieldname: "description",
|
||||
Value: fieldValue,
|
||||
},
|
||||
@@ -187,3 +239,59 @@ func (material *Material) DeleteMaterial(mediaID string) error {
|
||||
|
||||
return util.DecodeWithCommonError(response, "DeleteMaterial")
|
||||
}
|
||||
|
||||
//ArticleList 永久素材列表
|
||||
type ArticleList struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ItemCount int64 `json:"item_count"`
|
||||
Item []ArticleListItem `json:"item"`
|
||||
}
|
||||
|
||||
//ArticleListItem 用于ArticleList的item节点
|
||||
type ArticleListItem struct {
|
||||
MediaID string `json:"media_id"`
|
||||
Content ArticleListContent `json:"content"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
UpdateTime int64 `json:"update_time"`
|
||||
}
|
||||
|
||||
//ArticleListContent 用于ArticleListItem的content节点
|
||||
type ArticleListContent struct {
|
||||
NewsItem []Article `json:"news_item"`
|
||||
UpdateTime int64 `json:"update_time"`
|
||||
CreateTime int64 `json:"create_time"`
|
||||
}
|
||||
|
||||
//reqBatchGetMaterial BatchGetMaterial请求参数
|
||||
type reqBatchGetMaterial struct {
|
||||
Type PermanentMaterialType `json:"type"`
|
||||
Count int64 `json:"count"`
|
||||
Offset int64 `json:"offset"`
|
||||
}
|
||||
|
||||
// BatchGetMaterial 批量获取永久素材
|
||||
//reference:https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Get_materials_list.html
|
||||
func (material *Material) BatchGetMaterial(permanentMaterialType PermanentMaterialType, offset, count int64) (list ArticleList, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = material.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", batchGetMaterialURL, accessToken)
|
||||
|
||||
req := reqBatchGetMaterial{
|
||||
Type: permanentMaterialType,
|
||||
Offset: offset,
|
||||
Count: count,
|
||||
}
|
||||
|
||||
var response []byte
|
||||
response, err = util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = util.DecodeWithError(response, &list, "BatchGetMaterial")
|
||||
return
|
||||
}
|
||||
|
||||
160
message/customer_message.go
Normal file
160
message/customer_message.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
customerSendMessage = "https://api.weixin.qq.com/cgi-bin/message/custom/send"
|
||||
)
|
||||
|
||||
//Manager 消息管理者,可以发送消息
|
||||
type Manager struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
//NewMessageManager 实例化消息管理者
|
||||
func NewMessageManager(context *context.Context) *Manager {
|
||||
return &Manager{
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
//CustomerMessage 客服消息
|
||||
type CustomerMessage struct {
|
||||
ToUser string `json:"touser"` //接受者OpenID
|
||||
Msgtype MsgType `json:"msgtype"` //客服消息类型
|
||||
Text *MediaText `json:"text,omitempty"` //可选
|
||||
Image *MediaResource `json:"image,omitempty"` //可选
|
||||
Voice *MediaResource `json:"voice,omitempty"` //可选
|
||||
Video *MediaVideo `json:"video,omitempty"` //可选
|
||||
Music *MediaMusic `json:"music,omitempty"` //可选
|
||||
News *MediaNews `json:"news,omitempty"` //可选
|
||||
Mpnews *MediaResource `json:"mpnews,omitempty"` //可选
|
||||
Wxcard *MediaWxcard `json:"wxcard,omitempty"` //可选
|
||||
Msgmenu *MediaMsgmenu `json:"msgmenu,omitempty"` //可选
|
||||
Miniprogrampage *MediaMiniprogrampage `json:"miniprogrampage,omitempty"` //可选
|
||||
}
|
||||
|
||||
//NewCustomerTextMessage 文本消息结构体构造方法
|
||||
func NewCustomerTextMessage(toUser, text string) *CustomerMessage {
|
||||
return &CustomerMessage{
|
||||
ToUser: toUser,
|
||||
Msgtype: MsgTypeText,
|
||||
Text: &MediaText{
|
||||
text,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//NewCustomerImgMessage 图片消息的构造方法
|
||||
func NewCustomerImgMessage(toUser, mediaID string) *CustomerMessage {
|
||||
return &CustomerMessage{
|
||||
ToUser: toUser,
|
||||
Msgtype: MsgTypeImage,
|
||||
Image: &MediaResource{
|
||||
mediaID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//NewCustomerVoiceMessage 语音消息的构造方法
|
||||
func NewCustomerVoiceMessage(toUser, mediaID string) *CustomerMessage {
|
||||
return &CustomerMessage{
|
||||
ToUser: toUser,
|
||||
Msgtype: MsgTypeVoice,
|
||||
Voice: &MediaResource{
|
||||
mediaID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//MediaText 文本消息的文字
|
||||
type MediaText struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
//MediaResource 消息使用的永久素材id
|
||||
type MediaResource struct {
|
||||
MediaID string `json:"media_id"`
|
||||
}
|
||||
|
||||
//MediaVideo 视频消息包含的内容
|
||||
type MediaVideo struct {
|
||||
MediaID string `json:"media_id"`
|
||||
ThumbMediaID string `json:"thumb_media_id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
//MediaMusic 音乐消息包括的内容
|
||||
type MediaMusic struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Musicurl string `json:"musicurl"`
|
||||
Hqmusicurl string `json:"hqmusicurl"`
|
||||
ThumbMediaID string `json:"thumb_media_id"`
|
||||
}
|
||||
|
||||
//MediaNews 图文消息的内容
|
||||
type MediaNews struct {
|
||||
Articles []MediaArticles `json:"articles"`
|
||||
}
|
||||
|
||||
//MediaArticles 图文消息的内容的文章列表中的单独一条
|
||||
type MediaArticles struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
URL string `json:"url"`
|
||||
Picurl string `json:"picurl"`
|
||||
}
|
||||
|
||||
//MediaMsgmenu 菜单消息的内容
|
||||
type MediaMsgmenu struct {
|
||||
HeadContent string `json:"head_content"`
|
||||
List []MsgmenuItem `json:"list"`
|
||||
TailContent string `json:"tail_content"`
|
||||
}
|
||||
|
||||
//MsgmenuItem 菜单消息的菜单按钮
|
||||
type MsgmenuItem struct {
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
//MediaWxcard 卡券的id
|
||||
type MediaWxcard struct {
|
||||
CardID string `json:"card_id"`
|
||||
}
|
||||
|
||||
//MediaMiniprogrampage 小程序消息
|
||||
type MediaMiniprogrampage struct {
|
||||
Title string `json:"title"`
|
||||
Appid string `json:"appid"`
|
||||
Pagepath string `json:"pagepath"`
|
||||
ThumbMediaID string `json:"thumb_media_id"`
|
||||
}
|
||||
|
||||
//Send 发送客服消息
|
||||
func (manager *Manager) Send(msg *CustomerMessage) error {
|
||||
accessToken, err := manager.Context.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", customerSendMessage, accessToken)
|
||||
response, err := util.PostJSON(uri, msg)
|
||||
var result util.CommonError
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("customer msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package message
|
||||
|
||||
import "encoding/xml"
|
||||
import (
|
||||
"encoding/xml"
|
||||
|
||||
"github.com/silenceper/wechat/device"
|
||||
)
|
||||
|
||||
// MsgType 基本消息类型
|
||||
type MsgType string
|
||||
@@ -8,6 +12,9 @@ type MsgType string
|
||||
// EventType 事件类型
|
||||
type EventType string
|
||||
|
||||
// InfoType 第三方平台授权事件类型
|
||||
type InfoType string
|
||||
|
||||
const (
|
||||
//MsgTypeText 表示文本消息
|
||||
MsgTypeText MsgType = "text"
|
||||
@@ -60,6 +67,19 @@ const (
|
||||
EventLocationSelect = "location_select"
|
||||
//EventTemplateSendJobFinish 发送模板消息推送通知
|
||||
EventTemplateSendJobFinish = "TEMPLATESENDJOBFINISH"
|
||||
//EventWxaMediaCheck 异步校验图片/音频是否含有违法违规内容推送事件
|
||||
EventWxaMediaCheck = "wxa_media_check"
|
||||
)
|
||||
|
||||
const (
|
||||
// InfoTypeVerifyTicket 返回ticket
|
||||
InfoTypeVerifyTicket InfoType = "component_verify_ticket"
|
||||
// InfoTypeAuthorized 授权
|
||||
InfoTypeAuthorized = "authorized"
|
||||
// InfoTypeUnauthorized 取消授权
|
||||
InfoTypeUnauthorized = "unauthorized"
|
||||
// InfoTypeUpdateAuthorized 更新授权
|
||||
InfoTypeUpdateAuthorized = "updateauthorized"
|
||||
)
|
||||
|
||||
//MixMessage 存放所有微信发送过来的消息和事件
|
||||
@@ -110,6 +130,35 @@ type MixMessage struct {
|
||||
Label string `xml:"Label"`
|
||||
Poiname string `xml:"Poiname"`
|
||||
}
|
||||
|
||||
// 第三方平台相关
|
||||
InfoType InfoType `xml:"InfoType"`
|
||||
AppID string `xml:"AppId"`
|
||||
ComponentVerifyTicket string `xml:"ComponentVerifyTicket"`
|
||||
AuthorizerAppid string `xml:"AuthorizerAppid"`
|
||||
AuthorizationCode string `xml:"AuthorizationCode"`
|
||||
AuthorizationCodeExpiredTime int64 `xml:"AuthorizationCodeExpiredTime"`
|
||||
PreAuthCode string `xml:"PreAuthCode"`
|
||||
|
||||
// 卡券相关
|
||||
CardID string `xml:"CardId"`
|
||||
RefuseReason string `xml:"RefuseReason"`
|
||||
IsGiveByFriend int32 `xml:"IsGiveByFriend"`
|
||||
FriendUserName string `xml:"FriendUserName"`
|
||||
UserCardCode string `xml:"UserCardCode"`
|
||||
OldUserCardCode string `xml:"OldUserCardCode"`
|
||||
OuterStr string `xml:"OuterStr"`
|
||||
IsRestoreMemberCard int32 `xml:"IsRestoreMemberCard"`
|
||||
UnionID string `xml:"UnionId"`
|
||||
|
||||
// 内容审核相关
|
||||
IsRisky bool `xml:"isrisky"`
|
||||
ExtraInfoJSON string `xml:"extra_info_json"`
|
||||
TraceID string `xml:"trace_id"`
|
||||
StatusCode int `xml:"status_code"`
|
||||
|
||||
//设备相关
|
||||
device.MsgDevice
|
||||
}
|
||||
|
||||
//EventPic 发图事件推送
|
||||
@@ -133,22 +182,32 @@ type ResponseEncryptedXMLMsg struct {
|
||||
Nonce string `xml:"Nonce" json:"Nonce"`
|
||||
}
|
||||
|
||||
// CDATA 使用该类型,在序列化为 xml 文本时文本会被解析器忽略
|
||||
type CDATA string
|
||||
|
||||
// MarshalXML 实现自己的序列化方法
|
||||
func (c CDATA) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
return e.EncodeElement(struct {
|
||||
string `xml:",cdata"`
|
||||
}{string(c)}, start)
|
||||
}
|
||||
|
||||
// CommonToken 消息中通用的结构
|
||||
type CommonToken struct {
|
||||
XMLName xml.Name `xml:"xml"`
|
||||
ToUserName string `xml:"ToUserName"`
|
||||
FromUserName string `xml:"FromUserName"`
|
||||
ToUserName CDATA `xml:"ToUserName"`
|
||||
FromUserName CDATA `xml:"FromUserName"`
|
||||
CreateTime int64 `xml:"CreateTime"`
|
||||
MsgType MsgType `xml:"MsgType"`
|
||||
}
|
||||
|
||||
//SetToUserName set ToUserName
|
||||
func (msg *CommonToken) SetToUserName(toUserName string) {
|
||||
func (msg *CommonToken) SetToUserName(toUserName CDATA) {
|
||||
msg.ToUserName = toUserName
|
||||
}
|
||||
|
||||
//SetFromUserName set FromUserName
|
||||
func (msg *CommonToken) SetFromUserName(fromUserName string) {
|
||||
func (msg *CommonToken) SetFromUserName(fromUserName CDATA) {
|
||||
msg.FromUserName = fromUserName
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package template
|
||||
package message
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -3,12 +3,12 @@ package message
|
||||
//Text 文本消息
|
||||
type Text struct {
|
||||
CommonToken
|
||||
Content string `xml:"Content"`
|
||||
Content CDATA `xml:"Content"`
|
||||
}
|
||||
|
||||
//NewText 初始化文本消息
|
||||
func NewText(content string) *Text {
|
||||
text := new(Text)
|
||||
text.Content = content
|
||||
text.Content = CDATA(content)
|
||||
return text
|
||||
}
|
||||
|
||||
305
miniprogram/analysis.go
Normal file
305
miniprogram/analysis.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package miniprogram
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// 获取用户访问小程序日留存
|
||||
getAnalysisDailyRetainURL = "https://api.weixin.qq.com/datacube/getweanalysisappiddailyretaininfo?access_token=%s"
|
||||
// 获取用户访问小程序月留存
|
||||
getAnalysisMonthlyRetainURL = "https://api.weixin.qq.com/datacube/getweanalysisappidmonthlyretaininfo?access_token=%s"
|
||||
// 获取用户访问小程序周留存
|
||||
getAnalysisWeeklyRetainURL = "https://api.weixin.qq.com/datacube/getweanalysisappidweeklyretaininfo?access_token=%s"
|
||||
// 获取用户访问小程序数据概况
|
||||
getAnalysisDailySummaryURL = "https://api.weixin.qq.com/datacube/getweanalysisappiddailysummarytrend?access_token=%s"
|
||||
// 获取用户访问小程序数据日趋势
|
||||
getAnalysisDailyVisitTrendURL = "https://api.weixin.qq.com/datacube/getweanalysisappiddailyvisittrend?access_token=%s"
|
||||
// 获取用户访问小程序数据月趋势
|
||||
getAnalysisMonthlyVisitTrendURL = "https://api.weixin.qq.com/datacube/getweanalysisappidmonthlyvisittrend?access_token=%s"
|
||||
// 获取用户访问小程序数据周趋势
|
||||
getAnalysisWeeklyVisitTrendURL = "https://api.weixin.qq.com/datacube/getweanalysisappidweeklyvisittrend?access_token=%s"
|
||||
// 获取小程序新增或活跃用户的画像分布数据
|
||||
getAnalysisUserPortraitURL = "https://api.weixin.qq.com/datacube/getweanalysisappiduserportrait?access_token=%s"
|
||||
// 获取用户小程序访问分布数据
|
||||
getAnalysisVisitDistributionURL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitdistribution?access_token=%s"
|
||||
// 访问页面
|
||||
getAnalysisVisitPageURL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitpage?access_token=%s"
|
||||
)
|
||||
|
||||
// fetchData 拉取统计数据
|
||||
func (wxa *MiniProgram) fetchData(urlStr string, body interface{}) (response []byte, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = wxa.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
urlStr = fmt.Sprintf(urlStr, accessToken)
|
||||
response, err = util.PostJSON(urlStr, body)
|
||||
return
|
||||
}
|
||||
|
||||
// AnalysisRetainItem 留存项结构
|
||||
type AnalysisRetainItem struct {
|
||||
Key int `json:"key"` // 标识,0开始表示当天,1表示1甜后,以此类推
|
||||
Value int `json:"value"` // key对应日期的新增用户数/活跃用户数(key=0时)或留存用户数(k>0时)
|
||||
}
|
||||
|
||||
// ResAnalysisRetain 小程序留存数据返回
|
||||
type ResAnalysisRetain struct {
|
||||
util.CommonError
|
||||
RefDate string `json:"ref_date"` // 日期
|
||||
VisitUVNew []AnalysisRetainItem `json:"visit_uv_new"` // 新增用户留存
|
||||
VisitUV []AnalysisRetainItem `json:"visit_uv"` // 活跃用户留存
|
||||
}
|
||||
|
||||
// getAnalysisRetain 获取用户访问小程序留存数据(日、月、周)
|
||||
func (wxa *MiniProgram) getAnalysisRetain(urlStr string, beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||
body := map[string]string{
|
||||
"begin_date": beginDate,
|
||||
"end_date": endDate,
|
||||
}
|
||||
response, err := wxa.fetchData(urlStr, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("getAnalysisRetain error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetAnalysisDailyRetain 获取用户访问小程序日留存
|
||||
func (wxa *MiniProgram) GetAnalysisDailyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||
return wxa.getAnalysisRetain(getAnalysisDailyRetainURL, beginDate, endDate)
|
||||
}
|
||||
|
||||
// GetAnalysisMonthlyRetain 获取用户访问小程序月留存
|
||||
func (wxa *MiniProgram) GetAnalysisMonthlyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||
return wxa.getAnalysisRetain(getAnalysisMonthlyRetainURL, beginDate, endDate)
|
||||
}
|
||||
|
||||
// GetAnalysisWeeklyRetain 获取用户访问小程序周留存
|
||||
func (wxa *MiniProgram) GetAnalysisWeeklyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||
return wxa.getAnalysisRetain(getAnalysisWeeklyRetainURL, beginDate, endDate)
|
||||
}
|
||||
|
||||
// ResAnalysisDailySummary 小程序访问数据概况
|
||||
type ResAnalysisDailySummary struct {
|
||||
util.CommonError
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"` // 日期
|
||||
VisitTotal int `json:"visit_total"` // 累计用户数
|
||||
SharePV int `json:"share_pv"` // 转发次数
|
||||
ShareUV int `json:"share_uv"` // 转发人数
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
// GetAnalysisDailySummary 获取用户访问小程序数据概况
|
||||
func (wxa *MiniProgram) GetAnalysisDailySummary(beginDate, endDate string) (result ResAnalysisDailySummary, err error) {
|
||||
body := map[string]string{
|
||||
"begin_date": beginDate,
|
||||
"end_date": endDate,
|
||||
}
|
||||
response, err := wxa.fetchData(getAnalysisDailySummaryURL, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Println(string(response))
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("GetAnalysisDailySummary error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ResAnalysisVisitTrend 小程序访问数据趋势(日、月、周)
|
||||
type ResAnalysisVisitTrend struct {
|
||||
util.CommonError
|
||||
List []struct {
|
||||
RefDate string `json:"ref_date"` // 日期
|
||||
SessionCnt int `json:"session_cnt"` // 打开次数
|
||||
VisitPV int `json:"visit_pv"` // 访问次数
|
||||
VisitUV int `json:"visit_uv"` // 访问人数
|
||||
VisitUVNew int `json:"visit_uv_new"` // 新用户数
|
||||
StayTimeUV float64 `json:"stay_time_uv"` // 人均停留时长
|
||||
StayTimeSession float64 `json:"stay_time_session"` // 次均停留时常
|
||||
VisitDepth float64 `json:"visit_depth"` // 平均访问深度
|
||||
} `json:"list"`
|
||||
}
|
||||
|
||||
// getAnalysisRetain 获取小程序访问数据趋势(日、月、周)
|
||||
func (wxa *MiniProgram) getAnalysisVisitTrend(urlStr string, beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||
body := map[string]string{
|
||||
"begin_date": beginDate,
|
||||
"end_date": endDate,
|
||||
}
|
||||
response, err := wxa.fetchData(urlStr, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("getAnalysisVisitTrend error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetAnalysisDailyVisitTrend 获取用户访问小程序数据日趋势
|
||||
func (wxa *MiniProgram) GetAnalysisDailyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||
return wxa.getAnalysisVisitTrend(getAnalysisDailyVisitTrendURL, beginDate, endDate)
|
||||
}
|
||||
|
||||
// GetAnalysisMonthlyVisitTrend 获取用户访问小程序数据月趋势
|
||||
func (wxa *MiniProgram) GetAnalysisMonthlyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||
return wxa.getAnalysisVisitTrend(getAnalysisMonthlyVisitTrendURL, beginDate, endDate)
|
||||
}
|
||||
|
||||
// GetAnalysisWeeklyVisitTrend 获取用户访问小程序数据周趋势
|
||||
func (wxa *MiniProgram) GetAnalysisWeeklyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||
return wxa.getAnalysisVisitTrend(getAnalysisWeeklyVisitTrendURL, beginDate, endDate)
|
||||
}
|
||||
|
||||
// UserPortraitItem 用户画像项目
|
||||
type UserPortraitItem struct {
|
||||
ID int `json:"id"` // 属性值id
|
||||
Name string `json:"name"` // 属性值名称
|
||||
AccessSourceVisitUV int `json:"access_source_visit_uv"` // 该场景访问uv
|
||||
}
|
||||
|
||||
// UserPortrait 用户画像
|
||||
type UserPortrait struct {
|
||||
Index int `json:"index"` // 分布类型
|
||||
Province []UserPortraitItem `json:"province"` // 省份,如北京、广东等
|
||||
City []UserPortraitItem `json:"city"` // 城市,如北京、广州等
|
||||
Genders []UserPortraitItem `json:"genders"` // 性别,包括男、女、未知
|
||||
Platforms []UserPortraitItem `json:"platforms"` // 终端类型,包括iPhone, android, 其他
|
||||
Devices []UserPortraitItem `json:"devices"` // 机型,如苹果iPhone 6, OPPO R9等
|
||||
Ages []UserPortraitItem `json:"ages"` // 年龄,包括17岁以下、18-24对等区间
|
||||
}
|
||||
|
||||
// ResAnalysisUserPortrait 小程序新增或活跃用户的画像分布数据返回
|
||||
type ResAnalysisUserPortrait struct {
|
||||
util.CommonError
|
||||
RefDate string `json:"ref_date"` // 日期
|
||||
VisitUVNew UserPortrait `json:"visit_uv_new"` // 新用户画像
|
||||
VisitUV UserPortrait `json:"visit_uv"` // 活跃用户画像
|
||||
}
|
||||
|
||||
// GetAnalysisUserPortrait 获取小程序新增或活跃用户的画像分布数据
|
||||
func (wxa *MiniProgram) GetAnalysisUserPortrait(beginDate, endDate string) (result ResAnalysisUserPortrait, err error) {
|
||||
body := map[string]string{
|
||||
"begin_date": beginDate,
|
||||
"end_date": endDate,
|
||||
}
|
||||
response, err := wxa.fetchData(getAnalysisUserPortraitURL, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("GetAnalysisUserPortrait error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// VisitDistributionIndexItem 访问分数数据结构
|
||||
type VisitDistributionIndexItem struct {
|
||||
Key int `json:"key"` // 场景id
|
||||
Value int `json:"value"` // 该场景id访问pv
|
||||
AccessSourceVisitUV int `json:"access_source_visit_uv"` // 该场景id访问uv
|
||||
}
|
||||
|
||||
// VisitDistributionIndex 访问分布单分布类型数据
|
||||
type VisitDistributionIndex struct {
|
||||
Index string `json:"index"` // 分布类型
|
||||
ItemList []VisitDistributionIndexItem `json:"item_list"` // 分布数据列表
|
||||
}
|
||||
|
||||
// ResAnalysisVisitDistribution 小程序访问分布数据返回
|
||||
type ResAnalysisVisitDistribution struct {
|
||||
util.CommonError
|
||||
RefDate string `json:"ref_date"` // 日期
|
||||
List []VisitDistributionIndex `json:"list"` // 数据列表
|
||||
}
|
||||
|
||||
// GetAnalysisVisitDistribution 获取用户小程序访问分布数据
|
||||
func (wxa *MiniProgram) GetAnalysisVisitDistribution(beginDate, endDate string) (result ResAnalysisVisitDistribution, err error) {
|
||||
body := map[string]string{
|
||||
"begin_date": beginDate,
|
||||
"end_date": endDate,
|
||||
}
|
||||
response, err := wxa.fetchData(getAnalysisVisitDistributionURL, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("GetAnalysisVisitDistribution error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// VisitPageItem 访问单个页面的数据结构
|
||||
type VisitPageItem struct {
|
||||
PagePath string `json:"page_path"` // 页面路径
|
||||
PageVisitPV int `json:"page_visit_pv"` // 访问次数
|
||||
PageVisitUV int `json:"page_visit_uv"` // 访问人数
|
||||
PageStaytimePV float64 `json:"page_staytime_pv"` // 次均停留时常
|
||||
EntrypagePV int `json:"entrypage_pv"` // 进入页次数
|
||||
ExitpagePV int `json:"exitpage_pv"` // 退出页次数
|
||||
PageSharePV int `json:"page_share_pv"` // 转发次数
|
||||
PageShareUV int `json:"page_share_uv"` // 转发人数
|
||||
}
|
||||
|
||||
// ResAnalysisVisitPage 访问小程序页面访问数据返回
|
||||
type ResAnalysisVisitPage struct {
|
||||
util.CommonError
|
||||
RefDate string `json:"ref_date"` // 日期
|
||||
List []VisitPageItem `json:"list"` // 数据列表
|
||||
}
|
||||
|
||||
// GetAnalysisVisitPage 获取小程序页面访问数据
|
||||
func (wxa *MiniProgram) GetAnalysisVisitPage(beginDate, endDate string) (result ResAnalysisVisitPage, err error) {
|
||||
body := map[string]string{
|
||||
"begin_date": beginDate,
|
||||
"end_date": endDate,
|
||||
}
|
||||
response, err := wxa.fetchData(getAnalysisVisitPageURL, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("GetAnalysisVisitPage error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
130
miniprogram/decrypt.go
Normal file
130
miniprogram/decrypt.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package miniprogram
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrAppIDNotMatch appid不匹配
|
||||
ErrAppIDNotMatch = errors.New("app id not match")
|
||||
// ErrInvalidBlockSize block size不合法
|
||||
ErrInvalidBlockSize = errors.New("invalid block size")
|
||||
// ErrInvalidPKCS7Data PKCS7数据不合法
|
||||
ErrInvalidPKCS7Data = errors.New("invalid PKCS7 data")
|
||||
// ErrInvalidPKCS7Padding 输入padding失败
|
||||
ErrInvalidPKCS7Padding = errors.New("invalid padding on input")
|
||||
)
|
||||
|
||||
// UserInfo 用户信息
|
||||
type UserInfo struct {
|
||||
OpenID string `json:"openId"`
|
||||
UnionID string `json:"unionId"`
|
||||
NickName string `json:"nickName"`
|
||||
Gender int `json:"gender"`
|
||||
City string `json:"city"`
|
||||
Province string `json:"province"`
|
||||
Country string `json:"country"`
|
||||
AvatarURL string `json:"avatarUrl"`
|
||||
Language string `json:"language"`
|
||||
Watermark struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
AppID string `json:"appid"`
|
||||
} `json:"watermark"`
|
||||
}
|
||||
|
||||
// PhoneInfo 用户手机号
|
||||
type PhoneInfo struct {
|
||||
PhoneNumber string `json:"phoneNumber"`
|
||||
PurePhoneNumber string `json:"purePhoneNumber"`
|
||||
CountryCode string `json:"countryCode"`
|
||||
Watermark struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
AppID string `json:"appid"`
|
||||
} `json:"watermark"`
|
||||
}
|
||||
|
||||
// pkcs7Unpad returns slice of the original data without padding
|
||||
func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) {
|
||||
if blockSize <= 0 {
|
||||
return nil, ErrInvalidBlockSize
|
||||
}
|
||||
if len(data)%blockSize != 0 || len(data) == 0 {
|
||||
return nil, ErrInvalidPKCS7Data
|
||||
}
|
||||
c := data[len(data)-1]
|
||||
n := int(c)
|
||||
if n == 0 || n > len(data) {
|
||||
return nil, ErrInvalidPKCS7Padding
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
if data[len(data)-n+i] != c {
|
||||
return nil, ErrInvalidPKCS7Padding
|
||||
}
|
||||
}
|
||||
return data[:len(data)-n], nil
|
||||
}
|
||||
|
||||
// getCipherText returns slice of the cipher text
|
||||
func getCipherText(sessionKey, encryptedData, iv string) ([]byte, error) {
|
||||
aesKey, err := base64.StdEncoding.DecodeString(sessionKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cipherText, err := base64.StdEncoding.DecodeString(encryptedData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ivBytes, err := base64.StdEncoding.DecodeString(iv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
block, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mode := cipher.NewCBCDecrypter(block, ivBytes)
|
||||
mode.CryptBlocks(cipherText, cipherText)
|
||||
cipherText, err = pkcs7Unpad(cipherText, block.BlockSize())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cipherText, nil
|
||||
}
|
||||
|
||||
// Decrypt 解密数据
|
||||
func (wxa *MiniProgram) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo, error) {
|
||||
cipherText, err := getCipherText(sessionKey, encryptedData, iv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var userInfo UserInfo
|
||||
err = json.Unmarshal(cipherText, &userInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if userInfo.Watermark.AppID != wxa.AppID {
|
||||
return nil, ErrAppIDNotMatch
|
||||
}
|
||||
return &userInfo, nil
|
||||
}
|
||||
|
||||
// DecryptPhone 解密数据(手机)
|
||||
func (wxa *MiniProgram) DecryptPhone(sessionKey, encryptedData, iv string) (*PhoneInfo, error) {
|
||||
cipherText, err := getCipherText(sessionKey, encryptedData, iv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var phoneInfo PhoneInfo
|
||||
err = json.Unmarshal(cipherText, &phoneInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if phoneInfo.Watermark.AppID != wxa.AppID {
|
||||
return nil, ErrAppIDNotMatch
|
||||
}
|
||||
return &phoneInfo, nil
|
||||
}
|
||||
17
miniprogram/miniprogram.go
Normal file
17
miniprogram/miniprogram.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package miniprogram
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/context"
|
||||
)
|
||||
|
||||
// MiniProgram struct extends context
|
||||
type MiniProgram struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewMiniProgram 实例化小程序接口
|
||||
func NewMiniProgram(context *context.Context) *MiniProgram {
|
||||
miniProgram := new(MiniProgram)
|
||||
miniProgram.Context = context
|
||||
return miniProgram
|
||||
}
|
||||
91
miniprogram/qrcode.go
Normal file
91
miniprogram/qrcode.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package miniprogram
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
createWXAQRCodeURL = "https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token=%s"
|
||||
getWXACodeURL = "https://api.weixin.qq.com/wxa/getwxacode?access_token=%s"
|
||||
getWXACodeUnlimitURL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%s"
|
||||
)
|
||||
|
||||
// QRCoder 小程序码参数
|
||||
type QRCoder struct {
|
||||
// page 必须是已经发布的小程序存在的页面,根路径前不要填加 /,不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面
|
||||
Page string `json:"page,omitempty"`
|
||||
// path 扫码进入的小程序页面路径
|
||||
Path string `json:"path,omitempty"`
|
||||
// width 图片宽度
|
||||
Width int `json:"width,omitempty"`
|
||||
// scene 最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~,其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式)
|
||||
Scene string `json:"scene,omitempty"`
|
||||
// autoColor 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调
|
||||
AutoColor bool `json:"auto_color,omitempty"`
|
||||
// lineColor AutoColor 为 false 时生效,使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"},十进制表示
|
||||
LineColor Color `json:"line_color,omitempty"`
|
||||
// isHyaline 是否需要透明底色
|
||||
IsHyaline bool `json:"is_hyaline,omitempty"`
|
||||
}
|
||||
|
||||
// Color QRCode color
|
||||
type Color struct {
|
||||
R string `json:"r"`
|
||||
G string `json:"g"`
|
||||
B string `json:"b"`
|
||||
}
|
||||
|
||||
// fetchCode 请求并返回二维码二进制数据
|
||||
func (wxa *MiniProgram) fetchCode(urlStr string, body interface{}) (response []byte, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = wxa.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
urlStr = fmt.Sprintf(urlStr, accessToken)
|
||||
var contentType string
|
||||
response, contentType, err = util.PostJSONWithRespContentType(urlStr, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(contentType, "application/json") {
|
||||
// 返回错误信息
|
||||
var result util.CommonError
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err == nil && result.ErrCode != 0 {
|
||||
err = fmt.Errorf("fetchCode error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||
return nil, err
|
||||
}
|
||||
} else if contentType == "image/jpeg" {
|
||||
// 返回文件
|
||||
return response, nil
|
||||
} else {
|
||||
err = fmt.Errorf("fetchCode error : unknown response content type - %v", contentType)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CreateWXAQRCode 获取小程序二维码,适用于需要的码数量较少的业务场景
|
||||
// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/createWXAQRCode.html
|
||||
func (wxa *MiniProgram) CreateWXAQRCode(coderParams QRCoder) (response []byte, err error) {
|
||||
return wxa.fetchCode(createWXAQRCodeURL, coderParams)
|
||||
}
|
||||
|
||||
// GetWXACode 获取小程序码,适用于需要的码数量较少的业务场景
|
||||
// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/getWXACode.html
|
||||
func (wxa *MiniProgram) GetWXACode(coderParams QRCoder) (response []byte, err error) {
|
||||
return wxa.fetchCode(getWXACodeURL, coderParams)
|
||||
}
|
||||
|
||||
// GetWXACodeUnlimit 获取小程序码,适用于需要的码数量极多的业务场景
|
||||
// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/getWXACodeUnlimit.html
|
||||
func (wxa *MiniProgram) GetWXACodeUnlimit(coderParams QRCoder) (response []byte, err error) {
|
||||
return wxa.fetchCode(getWXACodeUnlimitURL, coderParams)
|
||||
}
|
||||
40
miniprogram/sns.go
Normal file
40
miniprogram/sns.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package miniprogram
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
code2SessionURL = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
|
||||
)
|
||||
|
||||
// ResCode2Session 登录凭证校验的返回结果
|
||||
type ResCode2Session struct {
|
||||
util.CommonError
|
||||
|
||||
OpenID string `json:"openid"` // 用户唯一标识
|
||||
SessionKey string `json:"session_key"` // 会话密钥
|
||||
UnionID string `json:"unionid"` // 用户在开放平台的唯一标识符,在满足UnionID下发条件的情况下会返回
|
||||
}
|
||||
|
||||
// Code2Session 登录凭证校验
|
||||
func (wxa *MiniProgram) Code2Session(jsCode string) (result ResCode2Session, err error) {
|
||||
urlStr := fmt.Sprintf(code2SessionURL, wxa.AppID, wxa.AppSecret, jsCode)
|
||||
var response []byte
|
||||
response, err = util.HTTPGet(urlStr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("Code2Session error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -11,11 +11,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
redirectOauthURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect"
|
||||
accessTokenURL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"
|
||||
refreshAccessTokenURL = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%s"
|
||||
userInfoURL = "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN"
|
||||
checkAccessTokenURL = "https://api.weixin.qq.com/sns/auth?access_token=%s&openid=%s"
|
||||
redirectOauthURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect"
|
||||
webAppRedirectOauthURL = "https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect"
|
||||
accessTokenURL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"
|
||||
refreshAccessTokenURL = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%s"
|
||||
userInfoURL = "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN"
|
||||
checkAccessTokenURL = "https://api.weixin.qq.com/sns/auth?access_token=%s&openid=%s"
|
||||
)
|
||||
|
||||
//Oauth 保存用户授权信息
|
||||
@@ -37,6 +38,12 @@ func (oauth *Oauth) GetRedirectURL(redirectURI, scope, state string) (string, er
|
||||
return fmt.Sprintf(redirectOauthURL, oauth.AppID, urlStr, scope, state), nil
|
||||
}
|
||||
|
||||
//GetWebAppRedirectURL 获取网页应用跳转的url地址
|
||||
func (oauth *Oauth) GetWebAppRedirectURL(redirectURI, scope, state string) (string, error) {
|
||||
urlStr := url.QueryEscape(redirectURI)
|
||||
return fmt.Sprintf(webAppRedirectOauthURL, oauth.AppID, urlStr, scope, state), nil
|
||||
}
|
||||
|
||||
//Redirect 跳转到网页授权
|
||||
func (oauth *Oauth) Redirect(writer http.ResponseWriter, req *http.Request, redirectURI, scope, state string) error {
|
||||
location, err := oauth.GetRedirectURL(redirectURI, scope, state)
|
||||
@@ -56,6 +63,10 @@ type ResAccessToken struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
OpenID string `json:"openid"`
|
||||
Scope string `json:"scope"`
|
||||
|
||||
// UnionID 只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。
|
||||
// 公众号文档 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
|
||||
UnionID string `json:"unionid"`
|
||||
}
|
||||
|
||||
// GetUserAccessToken 通过网页授权的code 换取access_token(区别于context中的access_token)
|
||||
|
||||
100
pay/notify_result.go
Normal file
100
pay/notify_result.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package pay
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/fatih/structs"
|
||||
"github.com/silenceper/wechat/util"
|
||||
"github.com/spf13/cast"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// doc: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7&index=8
|
||||
|
||||
// NotifyResult 下单回调
|
||||
type NotifyResult struct {
|
||||
ReturnCode *string `xml:"return_code"`
|
||||
ReturnMsg *string `xml:"return_msg"`
|
||||
|
||||
AppID *string `xml:"appid" json:"appid"`
|
||||
MchID *string `xml:"mch_id"`
|
||||
DeviceInfo *string `xml:"device_info"`
|
||||
NonceStr *string `xml:"nonce_str"`
|
||||
Sign *string `xml:"sign"`
|
||||
SignType *string `xml:"sign_type"`
|
||||
ResultCode *string `xml:"result_code"`
|
||||
ErrCode *string `xml:"err_code"`
|
||||
ErrCodeDes *string `xml:"err_code_des"`
|
||||
OpenID *string `xml:"openid"`
|
||||
IsSubscribe *string `xml:"is_subscribe"`
|
||||
TradeType *string `xml:"trade_type"`
|
||||
BankType *string `xml:"bank_type"`
|
||||
TotalFee *int `xml:"total_fee"`
|
||||
SettlementTotalFee *int `xml:"settlement_total_fee"`
|
||||
FeeType *string `xml:"fee_type"`
|
||||
CashFee *string `xml:"cash_fee"`
|
||||
CashFeeType *string `xml:"cash_fee_type"`
|
||||
CouponFee *int `xml:"coupon_fee"`
|
||||
CouponCount *int `xml:"coupon_count"`
|
||||
|
||||
// coupon_type_$n 这里只声明 3 个,如果有更多的可以自己组合
|
||||
CouponType0 *string `xml:"coupon_type_0"`
|
||||
CouponType1 *string `xml:"coupon_type_1"`
|
||||
CouponType2 *string `xml:"coupon_type_2"`
|
||||
CouponID0 *string `xml:"coupon_id_0"`
|
||||
CouponID1 *string `xml:"coupon_id_1"`
|
||||
CouponID2 *string `xml:"coupon_id_2"`
|
||||
CouponFeed0 *string `xml:"coupon_fee_0"`
|
||||
CouponFeed1 *string `xml:"coupon_fee_1"`
|
||||
CouponFeed2 *string `xml:"coupon_fee_2"`
|
||||
|
||||
TransactionID *string `xml:"transaction_id"`
|
||||
OutTradeNo *string `xml:"out_trade_no"`
|
||||
Attach *string `xml:"attach"`
|
||||
TimeEnd *string `xml:"time_end"`
|
||||
}
|
||||
|
||||
// NotifyResp 消息通知返回
|
||||
type NotifyResp struct {
|
||||
ReturnCode string `xml:"return_code"`
|
||||
ReturnMsg string `xml:"return_msg"`
|
||||
}
|
||||
|
||||
// VerifySign 验签
|
||||
func (pcf *Pay) VerifySign(notifyRes NotifyResult) bool {
|
||||
// STEP1, 转换 struct 为 map,并对 map keys 做排序
|
||||
resMap := structs.Map(notifyRes)
|
||||
|
||||
sortedKeys := make([]string, 0, len(resMap))
|
||||
for k := range resMap {
|
||||
sortedKeys = append(sortedKeys, k)
|
||||
}
|
||||
sort.Strings(sortedKeys)
|
||||
|
||||
// STEP2, 对key=value的键值对用&连接起来,略过空值 & sign
|
||||
var signStrings string
|
||||
for _, k := range sortedKeys {
|
||||
value := fmt.Sprintf("%v", cast.ToString(resMap[k]))
|
||||
if value != "" && strings.ToLower(k) != "sign" {
|
||||
signStrings = signStrings + getTagKeyName(k, ¬ifyRes) + "=" + value + "&"
|
||||
}
|
||||
}
|
||||
|
||||
// STEP3, 在键值对的最后加上key=API_KEY
|
||||
signStrings = signStrings + "key=" + pcf.PayKey
|
||||
|
||||
// STEP4, 进行MD5签名并且将所有字符转为大写.
|
||||
sign := util.MD5Sum(signStrings)
|
||||
if sign != *notifyRes.Sign {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getTagKeyName(key string, notifyRes *NotifyResult) string {
|
||||
s := reflect.TypeOf(notifyRes).Elem()
|
||||
f, _ := s.FieldByName(key)
|
||||
name := f.Tag.Get("xml")
|
||||
return name
|
||||
}
|
||||
238
pay/pay.go
238
pay/pay.go
@@ -1,9 +1,18 @@
|
||||
package pay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
@@ -24,19 +33,26 @@ type Params struct {
|
||||
Body string
|
||||
OutTradeNo string
|
||||
OpenID string
|
||||
TradeType string
|
||||
SignType string
|
||||
Detail string
|
||||
Attach string
|
||||
GoodsTag string
|
||||
NotifyURL string
|
||||
}
|
||||
|
||||
// Config 是传出用于 jsdk 用的参数
|
||||
// Config 是传出用于 js sdk 用的参数
|
||||
type Config struct {
|
||||
Timestamp int64
|
||||
NonceStr string
|
||||
PrePayID string
|
||||
SignType string
|
||||
Sign string
|
||||
Timestamp string `json:"timestamp"`
|
||||
NonceStr string `json:"nonceStr"`
|
||||
PrePayID string `json:"prePayId"`
|
||||
SignType string `json:"signType"`
|
||||
Package string `json:"package"`
|
||||
PaySign string `json:"paySign"`
|
||||
}
|
||||
|
||||
// payResult 是 unifie order 接口的返回
|
||||
type payResult struct {
|
||||
// PreOrder 是 unifie order 接口的返回
|
||||
type PreOrder struct {
|
||||
ReturnCode string `xml:"return_code"`
|
||||
ReturnMsg string `xml:"return_msg"`
|
||||
AppID string `xml:"appid,omitempty"`
|
||||
@@ -51,7 +67,7 @@ type payResult struct {
|
||||
ErrCodeDes string `xml:"err_code_des,omitempty"`
|
||||
}
|
||||
|
||||
//payRequest 接口请求参数
|
||||
// payRequest 接口请求参数
|
||||
type payRequest struct {
|
||||
AppID string `xml:"appid"`
|
||||
MchID string `xml:"mch_id"`
|
||||
@@ -61,20 +77,20 @@ type payRequest struct {
|
||||
SignType string `xml:"sign_type,omitempty"`
|
||||
Body string `xml:"body"`
|
||||
Detail string `xml:"detail,omitempty"`
|
||||
Attach string `xml:"attach,omitempty"` //附加数据
|
||||
OutTradeNo string `xml:"out_trade_no"` //商户订单号
|
||||
FeeType string `xml:"fee_type,omitempty"` //标价币种
|
||||
TotalFee string `xml:"total_fee"` //标价金额
|
||||
SpbillCreateIP string `xml:"spbill_create_ip"` //终端IP
|
||||
TimeStart string `xml:"time_start,omitempty"` //交易起始时间
|
||||
TimeExpire string `xml:"time_expire,omitempty"` //交易结束时间
|
||||
GoodsTag string `xml:"goods_tag,omitempty"` //订单优惠标记
|
||||
NotifyURL string `xml:"notify_url"` //通知地址
|
||||
TradeType string `xml:"trade_type"` //交易类型
|
||||
ProductID string `xml:"product_id,omitempty"` //商品ID
|
||||
Attach string `xml:"attach,omitempty"` // 附加数据
|
||||
OutTradeNo string `xml:"out_trade_no"` // 商户订单号
|
||||
FeeType string `xml:"fee_type,omitempty"` // 标价币种
|
||||
TotalFee string `xml:"total_fee"` // 标价金额
|
||||
SpbillCreateIP string `xml:"spbill_create_ip"` // 终端IP
|
||||
TimeStart string `xml:"time_start,omitempty"` // 交易起始时间
|
||||
TimeExpire string `xml:"time_expire,omitempty"` // 交易结束时间
|
||||
GoodsTag string `xml:"goods_tag,omitempty"` // 订单优惠标记
|
||||
NotifyURL string `xml:"notify_url"` // 通知地址
|
||||
TradeType string `xml:"trade_type"` // 交易类型
|
||||
ProductID string `xml:"product_id,omitempty"` // 商品ID
|
||||
LimitPay string `xml:"limit_pay,omitempty"` //
|
||||
OpenID string `xml:"openid,omitempty"` //用户标识
|
||||
SceneInfo string `xml:"scene_info,omitempty"` //场景信息
|
||||
OpenID string `xml:"openid,omitempty"` // 用户标识
|
||||
SceneInfo string `xml:"scene_info,omitempty"` // 场景信息
|
||||
}
|
||||
|
||||
// NewPay return an instance of Pay package
|
||||
@@ -83,12 +99,75 @@ func NewPay(ctx *context.Context) *Pay {
|
||||
return &pay
|
||||
}
|
||||
|
||||
// PrePayID will request wechat merchant api and request for a pre payment order id
|
||||
func (pcf *Pay) PrePayID(p *Params) (prePayID string, err error) {
|
||||
// BridgeConfig get js bridge config
|
||||
func (pcf *Pay) BridgeConfig(p *Params) (cfg Config, err error) {
|
||||
var (
|
||||
buffer strings.Builder
|
||||
h hash.Hash
|
||||
timestamp = strconv.FormatInt(time.Now().Unix(), 10)
|
||||
)
|
||||
order, err := pcf.PrePayOrder(p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
buffer.WriteString("appId=")
|
||||
buffer.WriteString(order.AppID)
|
||||
buffer.WriteString("&nonceStr=")
|
||||
buffer.WriteString(order.NonceStr)
|
||||
buffer.WriteString("&package=")
|
||||
buffer.WriteString("prepay_id=" + order.PrePayID)
|
||||
buffer.WriteString("&signType=")
|
||||
buffer.WriteString(p.SignType)
|
||||
buffer.WriteString("&timeStamp=")
|
||||
buffer.WriteString(timestamp)
|
||||
buffer.WriteString("&key=")
|
||||
buffer.WriteString(pcf.PayKey)
|
||||
if p.SignType == "MD5" {
|
||||
h = md5.New()
|
||||
} else {
|
||||
h = hmac.New(sha256.New, []byte(pcf.PayKey))
|
||||
}
|
||||
h.Write([]byte(buffer.String()))
|
||||
// 签名
|
||||
cfg.PaySign = strings.ToUpper(hex.EncodeToString(h.Sum(nil)))
|
||||
cfg.NonceStr = order.NonceStr
|
||||
cfg.Timestamp = timestamp
|
||||
cfg.PrePayID = order.PrePayID
|
||||
cfg.SignType = p.SignType
|
||||
cfg.Package = "prepay_id=" + order.PrePayID
|
||||
return
|
||||
}
|
||||
|
||||
// PrePayOrder return data for invoke wechat payment
|
||||
func (pcf *Pay) PrePayOrder(p *Params) (payOrder PreOrder, err error) {
|
||||
nonceStr := util.RandomStr(32)
|
||||
tradeType := "JSAPI"
|
||||
template := "appid=%s&body=%s&mch_id=%s&nonce_str=%s¬ify_url=%s&openid=%s&out_trade_no=%s&spbill_create_ip=%s&total_fee=%s&trade_type=%s&key=%s"
|
||||
str := fmt.Sprintf(template, pcf.AppID, p.Body, pcf.PayMchID, nonceStr, pcf.PayNotifyURL, p.OpenID, p.OutTradeNo, p.CreateIP, p.TotalFee, tradeType, pcf.PayKey)
|
||||
notifyURL := pcf.PayNotifyURL
|
||||
// 签名类型
|
||||
if p.SignType == "" {
|
||||
p.SignType = "MD5"
|
||||
}
|
||||
// 通知地址
|
||||
if p.NotifyURL != "" {
|
||||
notifyURL = p.NotifyURL
|
||||
}
|
||||
param := make(map[string]interface{})
|
||||
param["appid"] = pcf.AppID
|
||||
param["body"] = p.Body
|
||||
param["mch_id"] = pcf.PayMchID
|
||||
param["nonce_str"] = nonceStr
|
||||
param["out_trade_no"] = p.OutTradeNo
|
||||
param["spbill_create_ip"] = p.CreateIP
|
||||
param["total_fee"] = p.TotalFee
|
||||
param["trade_type"] = p.TradeType
|
||||
param["openid"] = p.OpenID
|
||||
param["sign_type"] = p.SignType
|
||||
param["detail"] = p.Detail
|
||||
param["attach"] = p.Attach
|
||||
param["goods_tag"] = p.GoodsTag
|
||||
param["notify_url"] = notifyURL
|
||||
|
||||
bizKey := "&key=" + pcf.PayKey
|
||||
str := orderParam(param, bizKey)
|
||||
sign := util.MD5Sum(str)
|
||||
request := payRequest{
|
||||
AppID: pcf.AppID,
|
||||
@@ -99,25 +178,104 @@ func (pcf *Pay) PrePayID(p *Params) (prePayID string, err error) {
|
||||
OutTradeNo: p.OutTradeNo,
|
||||
TotalFee: p.TotalFee,
|
||||
SpbillCreateIP: p.CreateIP,
|
||||
NotifyURL: pcf.PayNotifyURL,
|
||||
TradeType: tradeType,
|
||||
NotifyURL: notifyURL,
|
||||
TradeType: p.TradeType,
|
||||
OpenID: p.OpenID,
|
||||
SignType: p.SignType,
|
||||
Detail: p.Detail,
|
||||
Attach: p.Attach,
|
||||
GoodsTag: p.GoodsTag,
|
||||
}
|
||||
rawRet, err := util.PostXML(payGateway, request)
|
||||
if err != nil {
|
||||
return "", errors.New(err.Error() + " parameters : " + str)
|
||||
return
|
||||
}
|
||||
payRet := payResult{}
|
||||
err = xml.Unmarshal(rawRet, &payRet)
|
||||
err = xml.Unmarshal(rawRet, &payOrder)
|
||||
if err != nil {
|
||||
return "", errors.New(err.Error())
|
||||
return
|
||||
}
|
||||
if payRet.ReturnCode == "SUCCESS" {
|
||||
//pay success
|
||||
if payRet.ResultCode == "SUCCESS" {
|
||||
return payRet.PrePayID, nil
|
||||
if payOrder.ReturnCode == "SUCCESS" {
|
||||
// pay success
|
||||
if payOrder.ResultCode == "SUCCESS" {
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
return "", errors.New(payRet.ErrCode + payRet.ErrCodeDes)
|
||||
err = errors.New(payOrder.ErrCode + payOrder.ErrCodeDes)
|
||||
return
|
||||
}
|
||||
return "", errors.New("[msg : xmlUnmarshalError] [rawReturn : " + string(rawRet) + "] [params : " + str + "] [sign : " + sign + "]")
|
||||
err = errors.New("[msg : xmlUnmarshalError] [rawReturn : " + string(rawRet) + "] [params : " + str + "] [sign : " + sign + "]")
|
||||
return
|
||||
}
|
||||
|
||||
// PrePayID will request wechat merchant api and request for a pre payment order id
|
||||
func (pcf *Pay) PrePayID(p *Params) (prePayID string, err error) {
|
||||
order, err := pcf.PrePayOrder(p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if order.PrePayID == "" {
|
||||
err = errors.New("empty prepayid")
|
||||
}
|
||||
prePayID = order.PrePayID
|
||||
return
|
||||
}
|
||||
|
||||
// order params
|
||||
func orderParam(source interface{}, bizKey string) (returnStr string) {
|
||||
switch v := source.(type) {
|
||||
case map[string]string:
|
||||
keys := make([]string, 0, len(v))
|
||||
for k := range v {
|
||||
if k == "sign" {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var buf bytes.Buffer
|
||||
for _, k := range keys {
|
||||
if v[k] == "" {
|
||||
continue
|
||||
}
|
||||
if buf.Len() > 0 {
|
||||
buf.WriteByte('&')
|
||||
}
|
||||
buf.WriteString(k)
|
||||
buf.WriteByte('=')
|
||||
buf.WriteString(v[k])
|
||||
}
|
||||
buf.WriteString(bizKey)
|
||||
returnStr = buf.String()
|
||||
case map[string]interface{}:
|
||||
keys := make([]string, 0, len(v))
|
||||
for k := range v {
|
||||
if k == "sign" {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var buf bytes.Buffer
|
||||
for _, k := range keys {
|
||||
if v[k] == "" {
|
||||
continue
|
||||
}
|
||||
if buf.Len() > 0 {
|
||||
buf.WriteByte('&')
|
||||
}
|
||||
buf.WriteString(k)
|
||||
buf.WriteByte('=')
|
||||
switch vv := v[k].(type) {
|
||||
case string:
|
||||
buf.WriteString(vv)
|
||||
case int:
|
||||
buf.WriteString(strconv.FormatInt(int64(vv), 10))
|
||||
default:
|
||||
panic("params type not supported")
|
||||
}
|
||||
}
|
||||
buf.WriteString(bizKey)
|
||||
returnStr = buf.String()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
109
pay/refund.go
Normal file
109
pay/refund.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package pay
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
var refundGateway = "https://api.mch.weixin.qq.com/secapi/pay/refund"
|
||||
|
||||
//RefundParams 调用参数
|
||||
type RefundParams struct {
|
||||
TransactionID string
|
||||
OutRefundNo string
|
||||
TotalFee string
|
||||
RefundFee string
|
||||
RefundDesc string
|
||||
RootCa string //ca证书
|
||||
}
|
||||
|
||||
//refundRequest 接口请求参数
|
||||
type refundRequest struct {
|
||||
AppID string `xml:"appid"`
|
||||
MchID string `xml:"mch_id"`
|
||||
NonceStr string `xml:"nonce_str"`
|
||||
Sign string `xml:"sign"`
|
||||
SignType string `xml:"sign_type,omitempty"`
|
||||
TransactionID string `xml:"transaction_id"`
|
||||
OutRefundNo string `xml:"out_refund_no"`
|
||||
TotalFee string `xml:"total_fee"`
|
||||
RefundFee string `xml:"refund_fee"`
|
||||
RefundDesc string `xml:"refund_desc,omitempty"`
|
||||
//NotifyUrl string `xml:"notify_url,omitempty"`
|
||||
}
|
||||
|
||||
//RefundResponse 接口返回
|
||||
type RefundResponse struct {
|
||||
ReturnCode string `xml:"return_code"`
|
||||
ReturnMsg string `xml:"return_msg"`
|
||||
AppID string `xml:"appid,omitempty"`
|
||||
MchID string `xml:"mch_id,omitempty"`
|
||||
NonceStr string `xml:"nonce_str,omitempty"`
|
||||
Sign string `xml:"sign,omitempty"`
|
||||
ResultCode string `xml:"result_code,omitempty"`
|
||||
ErrCode string `xml:"err_code,omitempty"`
|
||||
ErrCodeDes string `xml:"err_code_des,omitempty"`
|
||||
TransactionID string `xml:"transaction_id,omitempty"`
|
||||
OutTradeNo string `xml:"out_trade_no,omitempty"`
|
||||
OutRefundNo string `xml:"out_refund_no,omitempty"`
|
||||
RefundID string `xml:"refund_id,omitempty"`
|
||||
RefundFee string `xml:"refund_fee,omitempty"`
|
||||
SettlementRefundFee string `xml:"settlement_refund_fee,omitempty"`
|
||||
TotalFee string `xml:"total_fee,omitempty"`
|
||||
SettlementTotalFee string `xml:"settlement_total_fee,omitempty"`
|
||||
FeeType string `xml:"fee_type,omitempty"`
|
||||
CashFee string `xml:"cash_fee,omitempty"`
|
||||
CashFeeType string `xml:"cash_fee_type,omitempty"`
|
||||
}
|
||||
|
||||
//Refund 退款申请
|
||||
func (pcf *Pay) Refund(p *RefundParams) (rsp RefundResponse, err error) {
|
||||
nonceStr := util.RandomStr(32)
|
||||
param := make(map[string]interface{})
|
||||
param["appid"] = pcf.AppID
|
||||
param["mch_id"] = pcf.PayMchID
|
||||
param["nonce_str"] = nonceStr
|
||||
param["out_refund_no"] = p.OutRefundNo
|
||||
param["refund_desc"] = p.RefundDesc
|
||||
param["refund_fee"] = p.RefundFee
|
||||
param["total_fee"] = p.TotalFee
|
||||
param["sign_type"] = "MD5"
|
||||
param["transaction_id"] = p.TransactionID
|
||||
|
||||
bizKey := "&key=" + pcf.PayKey
|
||||
str := orderParam(param, bizKey)
|
||||
sign := util.MD5Sum(str)
|
||||
request := refundRequest{
|
||||
AppID: pcf.AppID,
|
||||
MchID: pcf.PayMchID,
|
||||
NonceStr: nonceStr,
|
||||
Sign: sign,
|
||||
SignType: "MD5",
|
||||
TransactionID: p.TransactionID,
|
||||
OutRefundNo: p.OutRefundNo,
|
||||
TotalFee: p.TotalFee,
|
||||
RefundFee: p.RefundFee,
|
||||
RefundDesc: p.RefundDesc,
|
||||
}
|
||||
rawRet, err := util.PostXMLWithTLS(refundGateway, request, p.RootCa, pcf.PayMchID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = xml.Unmarshal(rawRet, &rsp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if rsp.ReturnCode == "SUCCESS" {
|
||||
if rsp.ResultCode == "SUCCESS" {
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("refund error, errcode=%s,errmsg=%s", rsp.ErrCode, rsp.ErrCodeDes)
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("[msg : xmlUnmarshalError] [rawReturn : %s] [params : %s] [sign : %s]",
|
||||
string(rawRet), str, sign)
|
||||
return
|
||||
}
|
||||
122
qr/qr.go
Normal file
122
qr/qr.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package qr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
qrCreateURL = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s"
|
||||
getQRImgURL = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=%s"
|
||||
)
|
||||
|
||||
const (
|
||||
actionID = "QR_SCENE"
|
||||
actionStr = "QR_STR_SCENE"
|
||||
|
||||
actionLimitID = "QR_LIMIT_SCENE"
|
||||
actionLimitStr = "QR_LIMIT_STR_SCENE"
|
||||
)
|
||||
|
||||
// QR 二维码
|
||||
type QR struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
//NewQR 二维码实例
|
||||
func NewQR(context *context.Context) *QR {
|
||||
q := new(QR)
|
||||
q.Context = context
|
||||
return q
|
||||
}
|
||||
|
||||
// Request 临时二维码
|
||||
type Request struct {
|
||||
ExpireSeconds int64 `json:"expire_seconds,omitempty"`
|
||||
ActionName string `json:"action_name"`
|
||||
ActionInfo struct {
|
||||
Scene struct {
|
||||
SceneStr string `json:"scene_str,omitempty"`
|
||||
SceneID int `json:"scene_id,omitempty"`
|
||||
} `json:"scene"`
|
||||
} `json:"action_info"`
|
||||
}
|
||||
|
||||
// Ticket 二维码ticket
|
||||
type Ticket struct {
|
||||
util.CommonError `json:",inline"`
|
||||
Ticket string `json:"ticket"`
|
||||
ExpireSeconds int64 `json:"expire_seconds"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// GetQRTicket 获取二维码 Ticket
|
||||
func (q *QR) GetQRTicket(tq *Request) (t *Ticket, err error) {
|
||||
accessToken, err := q.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(qrCreateURL, accessToken)
|
||||
response, err := util.PostJSON(uri, tq)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("get qr ticket failed, %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
t = new(Ticket)
|
||||
err = json.Unmarshal(response, &t)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ShowQRCode 通过ticket换取二维码
|
||||
func ShowQRCode(tk *Ticket) string {
|
||||
return fmt.Sprintf(getQRImgURL, tk.Ticket)
|
||||
}
|
||||
|
||||
// NewTmpQrRequest 新建临时二维码请求实例
|
||||
func NewTmpQrRequest(exp time.Duration, scene interface{}) *Request {
|
||||
tq := &Request{
|
||||
ExpireSeconds: int64(exp.Seconds()),
|
||||
}
|
||||
switch reflect.ValueOf(scene).Kind() {
|
||||
case reflect.String:
|
||||
tq.ActionName = actionStr
|
||||
tq.ActionInfo.Scene.SceneStr = scene.(string)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16,
|
||||
reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16,
|
||||
reflect.Uint32, reflect.Uint64:
|
||||
tq.ActionName = actionID
|
||||
tq.ActionInfo.Scene.SceneID = scene.(int)
|
||||
}
|
||||
|
||||
return tq
|
||||
}
|
||||
|
||||
// NewLimitQrRequest 新建永久二维码请求实例
|
||||
func NewLimitQrRequest(scene interface{}) *Request {
|
||||
tq := &Request{}
|
||||
switch reflect.ValueOf(scene).Kind() {
|
||||
case reflect.String:
|
||||
tq.ActionName = actionLimitStr
|
||||
tq.ActionInfo.Scene.SceneStr = scene.(string)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16,
|
||||
reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16,
|
||||
reflect.Uint32, reflect.Uint64:
|
||||
tq.ActionName = actionLimitID
|
||||
tq.ActionInfo.Scene.SceneID = scene.(int)
|
||||
}
|
||||
|
||||
return tq
|
||||
}
|
||||
@@ -65,7 +65,9 @@ func (srv *Server) Serve() error {
|
||||
}
|
||||
|
||||
//debug
|
||||
//fmt.Println("request msg = ", string(srv.requestRawXMLMsg))
|
||||
if srv.debug {
|
||||
fmt.Println("request msg = ", string(srv.requestRawXMLMsg))
|
||||
}
|
||||
|
||||
return srv.buildResponse(response)
|
||||
}
|
||||
|
||||
32
tcb/README.md
Normal file
32
tcb/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 小程序-云开发 SDK
|
||||
|
||||
Tencent Cloud Base [文档](https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/)
|
||||
|
||||
## 使用说明
|
||||
|
||||
**初始化配置**
|
||||
|
||||
```golang
|
||||
//使用memcache保存access_token,也可选择redis或自定义cache
|
||||
memCache=cache.NewMemcache("127.0.0.1:11211")
|
||||
|
||||
//配置小程序参数
|
||||
config := &wechat.Config{
|
||||
AppID: "your app id",
|
||||
AppSecret: "your app secret",
|
||||
Cache: memCache,
|
||||
}
|
||||
wc := wechat.NewWechat(config)
|
||||
wcTcb := wc.GetTcb()
|
||||
```
|
||||
|
||||
### 举例
|
||||
#### 触发云函数
|
||||
```golang
|
||||
res, err := wcTcb.InvokeCloudFunction("test-xxxx", "add", `{"a":1,"b":2}`)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
更多使用方法参考[GODOC](https://godoc.org/github.com/silenceper/wechat/tcb)
|
||||
35
tcb/cloudfunction.go
Normal file
35
tcb/cloudfunction.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package tcb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
//触发云函数
|
||||
invokeCloudFunctionURL = "https://api.weixin.qq.com/tcb/invokecloudfunction"
|
||||
)
|
||||
|
||||
//InvokeCloudFunctionRes 云函数调用返回结果
|
||||
type InvokeCloudFunctionRes struct {
|
||||
util.CommonError
|
||||
RespData string `json:"resp_data"` //云函数返回的buffer
|
||||
}
|
||||
|
||||
//InvokeCloudFunction 云函数调用
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/functions/invokeCloudFunction.html
|
||||
func (tcb *Tcb) InvokeCloudFunction(env, name, args string) (*InvokeCloudFunctionRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s&env=%s&name=%s", invokeCloudFunctionURL, accessToken, env, name)
|
||||
response, err := util.HTTPPost(uri, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invokeCloudFunctionRes := &InvokeCloudFunctionRes{}
|
||||
err = util.DecodeWithError(response, invokeCloudFunctionRes, "InvokeCloudFunction")
|
||||
return invokeCloudFunctionRes, err
|
||||
}
|
||||
418
tcb/database.go
Normal file
418
tcb/database.go
Normal file
@@ -0,0 +1,418 @@
|
||||
package tcb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
//数据库导入
|
||||
databaseMigrateImportURL = "https://api.weixin.qq.com/tcb/databasemigrateimport"
|
||||
//数据库导出
|
||||
databaseMigrateExportURL = "https://api.weixin.qq.com/tcb/databasemigrateexport"
|
||||
//数据库迁移状态查询
|
||||
databaseMigrateQueryInfoURL = "https://api.weixin.qq.com/tcb/databasemigratequeryinfo"
|
||||
//变更数据库索引
|
||||
updateIndexURL = "https://api.weixin.qq.com/tcb/updateindex"
|
||||
//新增集合
|
||||
databaseCollectionAddURL = "https://api.weixin.qq.com/tcb/databasecollectionadd"
|
||||
//删除集合
|
||||
databaseCollectionDeleteURL = "https://api.weixin.qq.com/tcb/databasecollectiondelete"
|
||||
//获取特定云环境下集合信息
|
||||
databaseCollectionGetURL = "https://api.weixin.qq.com/tcb/databasecollectionget"
|
||||
//数据库插入记录
|
||||
databaseAddURL = "https://api.weixin.qq.com/tcb/databaseadd"
|
||||
//数据库删除记录
|
||||
databaseDeleteURL = "https://api.weixin.qq.com/tcb/databasedelete"
|
||||
//数据库更新记录
|
||||
databaseUpdateURL = "https://api.weixin.qq.com/tcb/databaseupdate"
|
||||
//数据库查询记录
|
||||
databaseQueryURL = "https://api.weixin.qq.com/tcb/databasequery"
|
||||
//统计集合记录数或统计查询语句对应的结果记录数
|
||||
databaseCountURL = "https://api.weixin.qq.com/tcb/databasecount"
|
||||
|
||||
//ConflictModeInster 冲突处理模式 插入
|
||||
ConflictModeInster ConflictMode = 1
|
||||
//ConflictModeUpsert 冲突处理模式 更新
|
||||
ConflictModeUpsert ConflictMode = 2
|
||||
|
||||
//FileTypeJSON 的合法值 json
|
||||
FileTypeJSON FileType = 1
|
||||
//FileTypeCsv 的合法值 csv
|
||||
FileTypeCsv FileType = 2
|
||||
)
|
||||
|
||||
//ConflictMode 冲突处理模式
|
||||
type ConflictMode int
|
||||
|
||||
//FileType 文件上传和导出的允许文件类型
|
||||
type FileType int
|
||||
|
||||
//ValidDirections 合法的direction值
|
||||
var ValidDirections = []string{"1", "-1", "2dsphere"}
|
||||
|
||||
//DatabaseMigrateExportReq 数据库出 请求参数
|
||||
type DatabaseMigrateExportReq struct {
|
||||
Env string `json:"env,omitempty"` //云环境ID
|
||||
FilePath string `json:"file_path,omitempty"` //导出文件路径(导入文件需先上传到同环境的存储中,可使用开发者工具或 HTTP API的上传文件 API上传)
|
||||
FileType FileType `json:"file_type,omitempty"` //导出文件类型,文件格式参考数据库导入指引中的文件格式部分 1:json 2:csv
|
||||
Query string `json:"query,omitempty"` //导出条件
|
||||
}
|
||||
|
||||
//DatabaseMigrateExportRes 数据库导出 返回结果
|
||||
type DatabaseMigrateExportRes struct {
|
||||
util.CommonError
|
||||
JobID int64 `json:"job_id"` //导出任务ID,可使用数据库迁移进度查询 API 查询导入进度及结果
|
||||
}
|
||||
|
||||
//DatabaseMigrateImportReq 数据库导入 请求参数
|
||||
type DatabaseMigrateImportReq struct {
|
||||
Env string `json:"env,omitempty"` //云环境ID
|
||||
CollectionName string `json:"collection_name,omitempty"` //集合名称
|
||||
FilePath string `json:"file_path,omitempty"` //导出文件路径(文件会导出到同环境的云存储中,可使用获取下载链接 API 获取下载链接)
|
||||
FileType FileType `json:"file_type,omitempty"` //导入文件类型,文件格式参考数据库导入指引中的文件格式部分 1:json 2:csv
|
||||
StopOnError bool `json:"stop_on_error,omitempty"` //是否在遇到错误时停止导入
|
||||
ConflictMode ConflictMode `json:"conflict_mode,omitempty"` //冲突处理模式 1:inster 2:UPSERT
|
||||
}
|
||||
|
||||
//DatabaseMigrateImportRes 数据库导入 返回结果
|
||||
type DatabaseMigrateImportRes struct {
|
||||
util.CommonError
|
||||
JobID int64 `json:"job_id"` //导入任务ID,可使用数据库迁移进度查询 API 查询导入进度及结果
|
||||
}
|
||||
|
||||
//DatabaseMigrateQueryInfoRes 数据库迁移状态查询
|
||||
type DatabaseMigrateQueryInfoRes struct {
|
||||
util.CommonError
|
||||
Status string `json:"status"` //导出状态
|
||||
RecordSuccess int64 `json:"record_success"` //导出成功记录数
|
||||
RecordFail int64 `json:"record_fail"` //导出失败记录数
|
||||
ErrMsg string `json:"err_msg"` //导出错误信息
|
||||
FileURL string `json:"file_url"` //导出文件下载地址
|
||||
}
|
||||
|
||||
//UpdateIndexReq 变更数据库索引 请求参数
|
||||
type UpdateIndexReq struct {
|
||||
Env string `json:"env,omitempty"` //云环境ID
|
||||
CollectionName string `json:"collection_name,omitempty"` //集合名称
|
||||
CreateIndexes []CreateIndex `json:"create_indexes,omitempty"` //新增索引
|
||||
DropIndexes []DropIndex `json:"drop_indexes,omitempty"` //删除索引
|
||||
}
|
||||
|
||||
//CreateIndex 新增索引
|
||||
type CreateIndex struct {
|
||||
Name string `json:"name,omitempty"` //索引名
|
||||
Unique bool `json:"unique,omitempty"` //是否唯一
|
||||
Keys []CreateIndexKey `json:"keys,omitempty"` //索引字段
|
||||
}
|
||||
|
||||
//CreateIndexKey create index key
|
||||
type CreateIndexKey struct {
|
||||
Name string `json:"name,omitempty"` //字段名
|
||||
Direction string `json:"direction,omitempty"` //字段排序
|
||||
}
|
||||
|
||||
//DropIndex 删除索引
|
||||
type DropIndex struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
//DatabaseCollectionReq 新增/删除集合请求参数
|
||||
type DatabaseCollectionReq struct {
|
||||
Env string `json:"env,omitempty"` //云环境ID
|
||||
CollectionName string `json:"collection_name,omitempty"` //集合名称
|
||||
}
|
||||
|
||||
//DatabaseCollectionGetReq 获取特定云环境下集合信息请求
|
||||
type DatabaseCollectionGetReq struct {
|
||||
Env string `json:"env,omitempty"` //云环境ID
|
||||
Limit int64 `json:"limit,omitempty"` //获取数量限制
|
||||
Offset int64 `json:"offset,omitempty"` //偏移量
|
||||
}
|
||||
|
||||
//DatabaseCollectionGetRes 获取特定云环境下集合信息结果
|
||||
type DatabaseCollectionGetRes struct {
|
||||
util.CommonError
|
||||
Pager struct {
|
||||
Limit int64 `json:"limit"` //单次查询限制
|
||||
Offset int64 `json:"offset"` //偏移量
|
||||
Total int64 `json:"total"` //符合查询条件的记录总数
|
||||
} `json:"pager"`
|
||||
Collections []struct {
|
||||
Name string `json:"name"` //集合名
|
||||
Count int64 `json:"count"` //表中文档数量
|
||||
Size int64 `json:"size"` //表的大小(即表中文档总大小),单位:字节
|
||||
IndexCount int64 `json:"index_count"` //索引数量
|
||||
IndexSize int64 `json:"index_size"` //索引占用大小,单位:字节
|
||||
} `json:"collections"`
|
||||
}
|
||||
|
||||
//DatabaseReq 数据库插入/删除/更新/查询/统计记录请求参数
|
||||
type DatabaseReq struct {
|
||||
Env string `json:"env,omitempty"` //云环境ID
|
||||
Query string `json:"query,omitempty"` //数据库操作语句
|
||||
}
|
||||
|
||||
//DatabaseAddRes 数据库插入记录返回结果
|
||||
type DatabaseAddRes struct {
|
||||
util.CommonError
|
||||
IDList []string `json:"id_list"` //插入成功的数据集合主键_id。
|
||||
}
|
||||
|
||||
//DatabaseDeleteRes 数据库删除记录返回结果
|
||||
type DatabaseDeleteRes struct {
|
||||
util.CommonError
|
||||
Deleted int64 `json:"deleted"` //删除记录数量
|
||||
}
|
||||
|
||||
//DatabaseUpdateRes 数据库更新记录返回结果
|
||||
type DatabaseUpdateRes struct {
|
||||
util.CommonError
|
||||
Matched int64 `json:"matched"` //更新条件匹配到的结果数
|
||||
Modified int64 `json:"modified"` //修改的记录数,注意:使用set操作新插入的数据不计入修改数目
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
//DatabaseQueryRes 数据库查询记录 返回结果
|
||||
type DatabaseQueryRes struct {
|
||||
util.CommonError
|
||||
Pager struct {
|
||||
Limit int64 `json:"limit"` //单次查询限制
|
||||
Offset int64 `json:"offset"` //偏移量
|
||||
Total int64 `json:"total"` //符合查询条件的记录总数
|
||||
} `json:"pager"`
|
||||
Data []string `json:"data"`
|
||||
}
|
||||
|
||||
//DatabaseCountRes 统计集合记录数或统计查询语句对应的结果记录数 返回结果
|
||||
type DatabaseCountRes struct {
|
||||
util.CommonError
|
||||
Count int64 `json:"count"` //记录数量
|
||||
}
|
||||
|
||||
//DatabaseMigrateImport 数据库导入
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseMigrateImport.html
|
||||
func (tcb *Tcb) DatabaseMigrateImport(req *DatabaseMigrateImportReq) (*DatabaseMigrateImportRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseMigrateImportURL, accessToken)
|
||||
response, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
databaseMigrateImportRes := &DatabaseMigrateImportRes{}
|
||||
err = util.DecodeWithError(response, databaseMigrateImportRes, "DatabaseMigrateImport")
|
||||
return databaseMigrateImportRes, err
|
||||
}
|
||||
|
||||
//DatabaseMigrateExport 数据库导出
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseMigrateExport.html
|
||||
func (tcb *Tcb) DatabaseMigrateExport(req *DatabaseMigrateExportReq) (*DatabaseMigrateExportRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseMigrateExportURL, accessToken)
|
||||
response, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
databaseMigrateExportRes := &DatabaseMigrateExportRes{}
|
||||
err = util.DecodeWithError(response, databaseMigrateExportRes, "DatabaseMigrateExport")
|
||||
return databaseMigrateExportRes, err
|
||||
}
|
||||
|
||||
//DatabaseMigrateQueryInfo 数据库迁移状态查询
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseMigrateQueryInfo.html
|
||||
func (tcb *Tcb) DatabaseMigrateQueryInfo(env string, jobID int64) (*DatabaseMigrateQueryInfoRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseMigrateQueryInfoURL, accessToken)
|
||||
response, err := util.PostJSON(uri, map[string]interface{}{
|
||||
"env": env,
|
||||
"job_id": jobID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
databaseMigrateQueryInfoRes := &DatabaseMigrateQueryInfoRes{}
|
||||
err = util.DecodeWithError(response, databaseMigrateQueryInfoRes, "DatabaseMigrateQueryInfo")
|
||||
return databaseMigrateQueryInfoRes, err
|
||||
}
|
||||
|
||||
//UpdateIndex 变更数据库索引
|
||||
//https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/updateIndex.html
|
||||
func (tcb *Tcb) UpdateIndex(req *UpdateIndexReq) error {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", updateIndexURL, accessToken)
|
||||
response, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "UpdateIndex")
|
||||
}
|
||||
|
||||
//DatabaseCollectionAdd 新增集合
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCollectionAdd.html
|
||||
func (tcb *Tcb) DatabaseCollectionAdd(env, collectionName string) error {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseCollectionAddURL, accessToken)
|
||||
response, err := util.PostJSON(uri, &DatabaseCollectionReq{
|
||||
Env: env,
|
||||
CollectionName: collectionName,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "DatabaseCollectionAdd")
|
||||
}
|
||||
|
||||
//DatabaseCollectionDelete 删除集合
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCollectionDelete.html
|
||||
func (tcb *Tcb) DatabaseCollectionDelete(env, collectionName string) error {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseCollectionDeleteURL, accessToken)
|
||||
response, err := util.PostJSON(uri, &DatabaseCollectionReq{
|
||||
Env: env,
|
||||
CollectionName: collectionName,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "DatabaseCollectionDelete")
|
||||
}
|
||||
|
||||
//DatabaseCollectionGet 获取特定云环境下集合信息
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCollectionGet.html
|
||||
func (tcb *Tcb) DatabaseCollectionGet(env string, limit, offset int64) (*DatabaseCollectionGetRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseCollectionGetURL, accessToken)
|
||||
response, err := util.PostJSON(uri, &DatabaseCollectionGetReq{
|
||||
Env: env,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
databaseCollectionGetRes := &DatabaseCollectionGetRes{}
|
||||
err = util.DecodeWithError(response, databaseCollectionGetRes, "DatabaseCollectionGet")
|
||||
return databaseCollectionGetRes, err
|
||||
}
|
||||
|
||||
//DatabaseAdd 数据库插入记录
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseAdd.html
|
||||
func (tcb *Tcb) DatabaseAdd(env, query string) (*DatabaseAddRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseAddURL, accessToken)
|
||||
response, err := util.PostJSON(uri, &DatabaseReq{
|
||||
Env: env,
|
||||
Query: query,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
databaseAddRes := &DatabaseAddRes{}
|
||||
err = util.DecodeWithError(response, databaseAddRes, "DatabaseAdd")
|
||||
return databaseAddRes, err
|
||||
}
|
||||
|
||||
//DatabaseDelete 数据库插入记录
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseDelete.html
|
||||
func (tcb *Tcb) DatabaseDelete(env, query string) (*DatabaseDeleteRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseDeleteURL, accessToken)
|
||||
response, err := util.PostJSON(uri, &DatabaseReq{
|
||||
Env: env,
|
||||
Query: query,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
databaseDeleteRes := &DatabaseDeleteRes{}
|
||||
err = util.DecodeWithError(response, databaseDeleteRes, "DatabaseDelete")
|
||||
return databaseDeleteRes, err
|
||||
}
|
||||
|
||||
//DatabaseUpdate 数据库插入记录
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseUpdate.html
|
||||
func (tcb *Tcb) DatabaseUpdate(env, query string) (*DatabaseUpdateRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseUpdateURL, accessToken)
|
||||
response, err := util.PostJSON(uri, &DatabaseReq{
|
||||
Env: env,
|
||||
Query: query,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
databaseUpdateRes := &DatabaseUpdateRes{}
|
||||
err = util.DecodeWithError(response, databaseUpdateRes, "DatabaseUpdate")
|
||||
return databaseUpdateRes, err
|
||||
}
|
||||
|
||||
//DatabaseQuery 数据库查询记录
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseQuery.html
|
||||
func (tcb *Tcb) DatabaseQuery(env, query string) (*DatabaseQueryRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseQueryURL, accessToken)
|
||||
response, err := util.PostJSON(uri, &DatabaseReq{
|
||||
Env: env,
|
||||
Query: query,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
databaseQueryRes := &DatabaseQueryRes{}
|
||||
err = util.DecodeWithError(response, databaseQueryRes, "DatabaseQuery")
|
||||
return databaseQueryRes, err
|
||||
}
|
||||
|
||||
//DatabaseCount 统计集合记录数或统计查询语句对应的结果记录数
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCount.html
|
||||
func (tcb *Tcb) DatabaseCount(env, query string) (*DatabaseCountRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", databaseCountURL, accessToken)
|
||||
response, err := util.PostJSON(uri, &DatabaseReq{
|
||||
Env: env,
|
||||
Query: query,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
databaseCountRes := &DatabaseCountRes{}
|
||||
err = util.DecodeWithError(response, databaseCountRes, "DatabaseCount")
|
||||
return databaseCountRes, err
|
||||
}
|
||||
134
tcb/file.go
Normal file
134
tcb/file.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package tcb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
//获取文件上传链接
|
||||
uploadFilePathURL = "https://api.weixin.qq.com/tcb/uploadfile"
|
||||
//获取文件下载链接
|
||||
batchDownloadFileURL = "https://api.weixin.qq.com/tcb/batchdownloadfile"
|
||||
//删除文件链接
|
||||
batchDeleteFileURL = "https://api.weixin.qq.com/tcb/batchdeletefile"
|
||||
)
|
||||
|
||||
//UploadFileReq 上传文件请求值
|
||||
type UploadFileReq struct {
|
||||
Env string `json:"env,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
//UploadFileRes 上传文件返回结果
|
||||
type UploadFileRes struct {
|
||||
util.CommonError
|
||||
URL string `json:"url"` //上传url
|
||||
Token string `json:"token"` //token
|
||||
Authorization string `json:"authorization"` //authorization
|
||||
FileID string `json:"file_id"` //文件ID
|
||||
CosFileID string `json:"cos_file_id"` //cos文件ID
|
||||
}
|
||||
|
||||
//BatchDownloadFileReq 上传文件请求值
|
||||
type BatchDownloadFileReq struct {
|
||||
Env string `json:"env,omitempty"`
|
||||
FileList []*DownloadFile `json:"file_list,omitempty"`
|
||||
}
|
||||
|
||||
//DownloadFile 文件信息
|
||||
type DownloadFile struct {
|
||||
FileID string `json:"fileid"` //文件ID
|
||||
MaxAge int64 `json:"max_age"` //下载链接有效期
|
||||
}
|
||||
|
||||
//BatchDownloadFileRes 上传文件返回结果
|
||||
type BatchDownloadFileRes struct {
|
||||
util.CommonError
|
||||
FileList []struct {
|
||||
FileID string `json:"file_id"` //文件ID
|
||||
DownloadURL string `json:"download_url"` //下载链接
|
||||
Status int64 `json:"status"` //状态码
|
||||
ErrMsg string `json:"errmsg"` //该文件错误信息
|
||||
} `json:"file_list"`
|
||||
}
|
||||
|
||||
//BatchDeleteFileReq 批量删除文件请求参数
|
||||
type BatchDeleteFileReq struct {
|
||||
Env string `json:"env,omitempty"`
|
||||
FileIDList []string `json:"fileid_list,omitempty"`
|
||||
}
|
||||
|
||||
//BatchDeleteFileRes 批量删除文件返回结果
|
||||
type BatchDeleteFileRes struct {
|
||||
util.CommonError
|
||||
DeleteList []struct {
|
||||
FileID string `json:"fileid"`
|
||||
Status int64 `json:"status"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
} `json:"delete_list"`
|
||||
}
|
||||
|
||||
//UploadFile 上传文件
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/storage/uploadFile.html
|
||||
func (tcb *Tcb) UploadFile(env, path string) (*UploadFileRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", uploadFilePathURL, accessToken)
|
||||
req := &UploadFileReq{
|
||||
Env: env,
|
||||
Path: path,
|
||||
}
|
||||
response, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uploadFileRes := &UploadFileRes{}
|
||||
err = util.DecodeWithError(response, uploadFileRes, "UploadFile")
|
||||
return uploadFileRes, err
|
||||
}
|
||||
|
||||
//BatchDownloadFile 获取文件下载链接
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/storage/batchDownloadFile.html
|
||||
func (tcb *Tcb) BatchDownloadFile(env string, fileList []*DownloadFile) (*BatchDownloadFileRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", batchDownloadFileURL, accessToken)
|
||||
req := &BatchDownloadFileReq{
|
||||
Env: env,
|
||||
FileList: fileList,
|
||||
}
|
||||
response, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
batchDownloadFileRes := &BatchDownloadFileRes{}
|
||||
err = util.DecodeWithError(response, batchDownloadFileRes, "BatchDownloadFile")
|
||||
return batchDownloadFileRes, err
|
||||
}
|
||||
|
||||
//BatchDeleteFile 批量删除文件
|
||||
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/storage/batchDeleteFile.html
|
||||
func (tcb *Tcb) BatchDeleteFile(env string, fileIDList []string) (*BatchDeleteFileRes, error) {
|
||||
accessToken, err := tcb.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", batchDeleteFileURL, accessToken)
|
||||
req := &BatchDeleteFileReq{
|
||||
Env: env,
|
||||
FileIDList: fileIDList,
|
||||
}
|
||||
response, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
batchDeleteFileRes := &BatchDeleteFileRes{}
|
||||
err = util.DecodeWithError(response, batchDeleteFileRes, "BatchDeleteFile")
|
||||
return batchDeleteFileRes, nil
|
||||
}
|
||||
16
tcb/tcb.go
Normal file
16
tcb/tcb.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package tcb
|
||||
|
||||
import "github.com/silenceper/wechat/context"
|
||||
|
||||
//Tcb Tencent Cloud Base
|
||||
type Tcb struct{
|
||||
*context.Context
|
||||
}
|
||||
|
||||
//NewTcb new Tencent Cloud Base
|
||||
func NewTcb(context *context.Context)*Tcb{
|
||||
return &Tcb{
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
92
user/user.go
92
user/user.go
@@ -3,6 +3,7 @@ package user
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
const (
|
||||
userInfoURL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN"
|
||||
updateRemarkURL = "https://api.weixin.qq.com/cgi-bin/user/info/updateremark?access_token=%s"
|
||||
userListURL = "https://api.weixin.qq.com/cgi-bin/user/get"
|
||||
)
|
||||
|
||||
//User 用户管理
|
||||
@@ -29,20 +31,33 @@ func NewUser(context *context.Context) *User {
|
||||
type Info struct {
|
||||
util.CommonError
|
||||
|
||||
Subscribe int32 `json:"subscribe"`
|
||||
OpenID string `json:"openid"`
|
||||
Nickname string `json:"nickname"`
|
||||
Sex int32 `json:"sex"`
|
||||
City string `json:"city"`
|
||||
Country string `json:"country"`
|
||||
Province string `json:"province"`
|
||||
Language string `json:"language"`
|
||||
Headimgurl string `json:"headimgurl"`
|
||||
SubscribeTime int32 `json:"subscribe_time"`
|
||||
UnionID string `json:"unionid"`
|
||||
Remark string `json:"remark"`
|
||||
GroupID int32 `json:"groupid"`
|
||||
TagidList []int32 `json:"tagid_list"`
|
||||
Subscribe int32 `json:"subscribe"`
|
||||
OpenID string `json:"openid"`
|
||||
Nickname string `json:"nickname"`
|
||||
Sex int32 `json:"sex"`
|
||||
City string `json:"city"`
|
||||
Country string `json:"country"`
|
||||
Province string `json:"province"`
|
||||
Language string `json:"language"`
|
||||
Headimgurl string `json:"headimgurl"`
|
||||
SubscribeTime int32 `json:"subscribe_time"`
|
||||
UnionID string `json:"unionid"`
|
||||
Remark string `json:"remark"`
|
||||
GroupID int32 `json:"groupid"`
|
||||
TagidList []int32 `json:"tagid_list"`
|
||||
SubscribeScene string `json:"subscribe_scene"`
|
||||
QrScene int `json:"qr_scene"`
|
||||
QrSceneStr string `json:"qr_scene_str"`
|
||||
}
|
||||
|
||||
// OpenidList 用户列表
|
||||
type OpenidList struct {
|
||||
Total int `json:"total"`
|
||||
Count int `json:"count"`
|
||||
Data struct {
|
||||
OpenIDs []string `json:"openid"`
|
||||
} `json:"data"`
|
||||
NextOpenID string `json:"next_openid"`
|
||||
}
|
||||
|
||||
//GetUserInfo 获取用户基本信息
|
||||
@@ -88,3 +103,52 @@ func (user *User) UpdateRemark(openID, remark string) (err error) {
|
||||
|
||||
return util.DecodeWithCommonError(response, "UpdateRemark")
|
||||
}
|
||||
|
||||
// ListUserOpenIDs 返回用户列表
|
||||
func (user *User) ListUserOpenIDs(nextOpenid ...string) (*OpenidList, error) {
|
||||
accessToken, err := user.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri, _ := url.Parse(userListURL)
|
||||
q := uri.Query()
|
||||
q.Set("access_token", accessToken)
|
||||
if len(nextOpenid) > 0 && nextOpenid[0] != "" {
|
||||
q.Set("next_openid", nextOpenid[0])
|
||||
}
|
||||
uri.RawQuery = q.Encode()
|
||||
|
||||
response, err := util.HTTPGet(uri.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userlist := new(OpenidList)
|
||||
err = json.Unmarshal(response, userlist)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return userlist, nil
|
||||
}
|
||||
|
||||
// ListAllUserOpenIDs 返回所有用户OpenID列表
|
||||
func (user *User) ListAllUserOpenIDs() ([]string, error) {
|
||||
nextOpenid := ""
|
||||
openids := []string{}
|
||||
count := 0
|
||||
for {
|
||||
ul, err := user.ListUserOpenIDs(nextOpenid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
openids = append(openids, ul.Data.OpenIDs...)
|
||||
count += ul.Count
|
||||
if ul.Total > count {
|
||||
nextOpenid = ul.NextOpenID
|
||||
} else {
|
||||
return openids, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package util
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// CommonError 微信返回的通用错误json
|
||||
@@ -23,3 +24,28 @@ func DecodeWithCommonError(response []byte, apiName string) (err error) {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecodeWithError 将返回值按照解析
|
||||
func DecodeWithError(response []byte, obj interface{}, apiName string) error {
|
||||
err := json.Unmarshal(response, obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("json Unmarshal Error, err=%v", err)
|
||||
}
|
||||
responseObj := reflect.ValueOf(obj)
|
||||
if !responseObj.IsValid() {
|
||||
return fmt.Errorf("obj is invalid")
|
||||
}
|
||||
commonError := responseObj.Elem().FieldByName("CommonError")
|
||||
if !commonError.IsValid() || commonError.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("commonError is invalid or not struct")
|
||||
}
|
||||
errCode := commonError.FieldByName("ErrCode")
|
||||
errMsg := commonError.FieldByName("ErrMsg")
|
||||
if !errCode.IsValid() || !errMsg.IsValid() {
|
||||
return fmt.Errorf("errcode or errmsg is invalid")
|
||||
}
|
||||
if errCode.Int() != 0 {
|
||||
return fmt.Errorf("%s Error , errcode=%d , errmsg=%s", apiName, errCode.Int(), errMsg.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
113
util/http.go
113
util/http.go
@@ -2,14 +2,19 @@ package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"golang.org/x/crypto/pkcs12"
|
||||
)
|
||||
|
||||
//HTTPGet get 请求
|
||||
@@ -26,17 +31,30 @@ func HTTPGet(uri string) ([]byte, error) {
|
||||
return ioutil.ReadAll(response.Body)
|
||||
}
|
||||
|
||||
//HTTPPost post 请求
|
||||
func HTTPPost(uri string, data string) ([]byte, error) {
|
||||
body := bytes.NewBuffer([]byte(data))
|
||||
response, err := http.Post(uri, "", body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("http get error : uri=%v , statusCode=%v", uri, response.StatusCode)
|
||||
}
|
||||
return ioutil.ReadAll(response.Body)
|
||||
}
|
||||
|
||||
//PostJSON post json 数据请求
|
||||
func PostJSON(uri string, obj interface{}) ([]byte, error) {
|
||||
jsonData, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jsonData = bytes.Replace(jsonData, []byte("\\u003c"), []byte("<"), -1)
|
||||
jsonData = bytes.Replace(jsonData, []byte("\\u003e"), []byte(">"), -1)
|
||||
jsonData = bytes.Replace(jsonData, []byte("\\u0026"), []byte("&"), -1)
|
||||
|
||||
body := bytes.NewBuffer(jsonData)
|
||||
response, err := http.Post(uri, "application/json;charset=utf-8", body)
|
||||
if err != nil {
|
||||
@@ -50,6 +68,32 @@ func PostJSON(uri string, obj interface{}) ([]byte, error) {
|
||||
return ioutil.ReadAll(response.Body)
|
||||
}
|
||||
|
||||
// PostJSONWithRespContentType post json数据请求,且返回数据类型
|
||||
func PostJSONWithRespContentType(uri string, obj interface{}) ([]byte, string, error) {
|
||||
jsonData, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
jsonData = bytes.Replace(jsonData, []byte("\\u003c"), []byte("<"), -1)
|
||||
jsonData = bytes.Replace(jsonData, []byte("\\u003e"), []byte(">"), -1)
|
||||
jsonData = bytes.Replace(jsonData, []byte("\\u0026"), []byte("&"), -1)
|
||||
|
||||
body := bytes.NewBuffer(jsonData)
|
||||
response, err := http.Post(uri, "application/json;charset=utf-8", body)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, "", fmt.Errorf("http get error : uri=%v , statusCode=%v", uri, response.StatusCode)
|
||||
}
|
||||
responseData, err := ioutil.ReadAll(response.Body)
|
||||
contentType := response.Header.Get("Content-Type")
|
||||
return responseData, contentType, err
|
||||
}
|
||||
|
||||
//PostFile 上传文件
|
||||
func PostFile(fieldname, filename, uri string) ([]byte, error) {
|
||||
fields := []MultipartFormField{
|
||||
@@ -141,3 +185,68 @@ func PostXML(uri string, obj interface{}) ([]byte, error) {
|
||||
}
|
||||
return ioutil.ReadAll(response.Body)
|
||||
}
|
||||
|
||||
//httpWithTLS CA证书
|
||||
func httpWithTLS(rootCa, key string) (*http.Client, error) {
|
||||
var client *http.Client
|
||||
certData, err := ioutil.ReadFile(rootCa)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to find cert path=%s, error=%v", rootCa, err)
|
||||
}
|
||||
cert := pkcs12ToPem(certData, key)
|
||||
config := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: config,
|
||||
DisableCompression: true,
|
||||
}
|
||||
client = &http.Client{Transport: tr}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
//pkcs12ToPem 将Pkcs12转成Pem
|
||||
func pkcs12ToPem(p12 []byte, password string) tls.Certificate {
|
||||
blocks, err := pkcs12.ToPEM(p12, password)
|
||||
defer func() {
|
||||
if x := recover(); x != nil {
|
||||
log.Print(x)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var pemData []byte
|
||||
for _, b := range blocks {
|
||||
pemData = append(pemData, pem.EncodeToMemory(b)...)
|
||||
}
|
||||
cert, err := tls.X509KeyPair(pemData, pemData)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cert
|
||||
}
|
||||
|
||||
//PostXMLWithTLS perform a HTTP/POST request with XML body and TLS
|
||||
func PostXMLWithTLS(uri string, obj interface{}, ca, key string) ([]byte, error) {
|
||||
xmlData, err := xml.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body := bytes.NewBuffer(xmlData)
|
||||
client, err := httpWithTLS(ca, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response, err := client.Post(uri, "application/xml;charset=utf-8", body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("http code error : uri=%v , statusCode=%v", uri, response.StatusCode)
|
||||
}
|
||||
return ioutil.ReadAll(response.Body)
|
||||
}
|
||||
|
||||
120
vendor/vendor.json
vendored
120
vendor/vendor.json
vendored
@@ -1,120 +0,0 @@
|
||||
{
|
||||
"comment": "",
|
||||
"ignore": "test",
|
||||
"package": [
|
||||
{
|
||||
"checksumSHA1": "ZZ4FL7s5f8QK4RysjZObSBYGOLY=",
|
||||
"path": "github.com/astaxie/beego",
|
||||
"revision": "2d87d4feafeea0a133d217a82e6e02df0348fed5",
|
||||
"revisionTime": "2016-09-22T15:18:45Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "LwEiQ/Hyb7Ul28TSlwowN9cpWDY=",
|
||||
"path": "github.com/astaxie/beego/config",
|
||||
"revision": "2d87d4feafeea0a133d217a82e6e02df0348fed5",
|
||||
"revisionTime": "2016-09-22T15:18:45Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "s+gj1rES9SvvCIyF8W2tzlziSPE=",
|
||||
"path": "github.com/astaxie/beego/context",
|
||||
"revision": "2d87d4feafeea0a133d217a82e6e02df0348fed5",
|
||||
"revisionTime": "2016-09-22T15:18:45Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "PDNn16w89zWODshT9zlPzSmWZFA=",
|
||||
"path": "github.com/astaxie/beego/grace",
|
||||
"revision": "2d87d4feafeea0a133d217a82e6e02df0348fed5",
|
||||
"revisionTime": "2016-09-22T15:18:45Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "Iz/p1UTvFNe5HFeohX7cvKEOQW0=",
|
||||
"path": "github.com/astaxie/beego/logs",
|
||||
"revision": "2d87d4feafeea0a133d217a82e6e02df0348fed5",
|
||||
"revisionTime": "2016-09-22T15:18:45Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "R797q1pCbp086SraUETxX1rsJYw=",
|
||||
"path": "github.com/astaxie/beego/session",
|
||||
"revision": "2d87d4feafeea0a133d217a82e6e02df0348fed5",
|
||||
"revisionTime": "2016-09-22T15:18:45Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "rxxln8GHFToVnaEJz4JMv0WbaKc=",
|
||||
"path": "github.com/astaxie/beego/toolbox",
|
||||
"revision": "2d87d4feafeea0a133d217a82e6e02df0348fed5",
|
||||
"revisionTime": "2016-09-22T15:18:45Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "fRJk3RZPPz6ovbautfsfxAk+CrI=",
|
||||
"path": "github.com/astaxie/beego/utils",
|
||||
"revision": "2d87d4feafeea0a133d217a82e6e02df0348fed5",
|
||||
"revisionTime": "2016-09-22T15:18:45Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "fNAC4qgZDqF3kxq74/yyk3PWdy8=",
|
||||
"path": "github.com/bradfitz/gomemcache/memcache",
|
||||
"revision": "fb1f79c6b65acda83063cbc69f6bba1522558bfc",
|
||||
"revisionTime": "2016-01-17T19:21:50Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "RsNwOto8G8aXIiRrlFn4dtU9q/g=",
|
||||
"path": "github.com/gin-gonic/gin",
|
||||
"revision": "e2212d40c62a98b388a5eb48ecbdcf88534688ba",
|
||||
"revisionTime": "2016-12-04T22:13:08Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "UsILDoIB2S7ra+w2fMdb85mX3HM=",
|
||||
"path": "github.com/gin-gonic/gin/binding",
|
||||
"revision": "e2212d40c62a98b388a5eb48ecbdcf88534688ba",
|
||||
"revisionTime": "2016-12-04T22:13:08Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "PHv9FNb7YavJWtAHcY6ZgXmkmHs=",
|
||||
"path": "github.com/gin-gonic/gin/render",
|
||||
"revision": "e2212d40c62a98b388a5eb48ecbdcf88534688ba",
|
||||
"revisionTime": "2016-12-04T22:13:08Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "kBeNcaKk56FguvPSUCEaH6AxpRc=",
|
||||
"path": "github.com/golang/protobuf/proto",
|
||||
"revision": "8ee79997227bf9b34611aee7946ae64735e6fd93",
|
||||
"revisionTime": "2016-11-17T03:31:26Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "b0T0Hzd+zYk+OCDTFMps+jwa/nY=",
|
||||
"path": "github.com/manucorporat/sse",
|
||||
"revision": "ee05b128a739a0fb76c7ebd3ae4810c1de808d6d",
|
||||
"revisionTime": "2016-01-26T18:01:36Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "xZuhljnmBysJPta/lMyYmJdujCg=",
|
||||
"path": "github.com/mattn/go-isatty",
|
||||
"revision": "30a891c33c7cde7b02a981314b4228ec99380cca",
|
||||
"revisionTime": "2016-11-23T14:36:37Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "pancewZW3HwGvpDwfH5Imrbadc4=",
|
||||
"path": "golang.org/x/net/context",
|
||||
"revision": ""
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "8fD/im5Kwvy3JgmxulDTambmE8w=",
|
||||
"path": "golang.org/x/sys/unix",
|
||||
"revision": "a646d33e2ee3172a661fc09bca23bb4889a41bc8",
|
||||
"revisionTime": "2016-07-15T05:43:45Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "39V1idWER42Lmcmg2Uy40wMzOlo=",
|
||||
"path": "gopkg.in/go-playground/validator.v8",
|
||||
"revision": "5f57d2222ad794d0dffb07e664ea05e2ee07d60c",
|
||||
"revisionTime": "2016-07-18T13:41:25Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "12GqsW8PiRPnezDDy0v4brZrndM=",
|
||||
"path": "gopkg.in/yaml.v2",
|
||||
"revision": "a5b47d31c556af34a302ce5d659e6fea44d90de0",
|
||||
"revisionTime": "2016-09-28T15:37:09Z"
|
||||
}
|
||||
],
|
||||
"rootPath": "github.com/silenceper/wechat"
|
||||
}
|
||||
30
wechat.go
30
wechat.go
@@ -6,13 +6,17 @@ import (
|
||||
|
||||
"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/oauth"
|
||||
"github.com/silenceper/wechat/pay"
|
||||
"github.com/silenceper/wechat/qr"
|
||||
"github.com/silenceper/wechat/server"
|
||||
"github.com/silenceper/wechat/template"
|
||||
"github.com/silenceper/wechat/tcb"
|
||||
"github.com/silenceper/wechat/user"
|
||||
)
|
||||
|
||||
@@ -91,11 +95,31 @@ func (wc *Wechat) GetUser() *user.User {
|
||||
}
|
||||
|
||||
// GetTemplate 模板消息接口
|
||||
func (wc *Wechat) GetTemplate() *template.Template {
|
||||
return template.NewTemplate(wc.Context)
|
||||
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 获取小程序的实例
|
||||
func (wc *Wechat) GetMiniProgram() *miniprogram.MiniProgram {
|
||||
return miniprogram.NewMiniProgram(wc.Context)
|
||||
}
|
||||
|
||||
// GetDevice 获取智能设备的实例
|
||||
func (wc *Wechat) GetDevice() *device.Device {
|
||||
return device.NewDevice(wc.Context)
|
||||
}
|
||||
|
||||
// GetTcb 获取小程序-云开发的实例
|
||||
func (wc *Wechat) GetTcb() *tcb.Tcb {
|
||||
return tcb.NewTcb(wc.Context)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user