mirror of
https://github.com/silenceper/wechat.git
synced 2026-02-04 12:52:27 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ada9c1ff61 | ||
|
|
9e58e097cb | ||
|
|
8b6147c3ec | ||
|
|
0071852c75 | ||
|
|
f4491193cb | ||
|
|
576a898c0f | ||
|
|
76fde58ad9 | ||
|
|
903dadc260 | ||
|
|
1efbf27bde | ||
|
|
99a2eb659c |
1
.github/ISSUE_TEMPLATE.md
vendored
1
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,3 +1,2 @@
|
||||
## 问题及现象
|
||||
|
||||
<!-- 描述你的问题现象,报错**贴截图**粘贴或者贴具体信息,提供**必要的代码段**
|
||||
|
||||
21
.github/ISSUE_TEMPLATE/bug.md
vendored
21
.github/ISSUE_TEMPLATE/bug.md
vendored
@@ -1,21 +0,0 @@
|
||||
---
|
||||
name: 报告Bug
|
||||
about: 反馈BUG信息
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**描述**
|
||||
|
||||
**如何复现**
|
||||
步骤:
|
||||
1、
|
||||
2、
|
||||
|
||||
**关联日志信息**
|
||||
|
||||
|
||||
**使用的版本**
|
||||
- SDK版本: [比如 v0.0.0]
|
||||
15
.github/ISSUE_TEMPLATE/feature.md
vendored
15
.github/ISSUE_TEMPLATE/feature.md
vendored
@@ -1,15 +0,0 @@
|
||||
---
|
||||
name: API需求
|
||||
about: 待实现的API接口,SDK的强大离不开社区的帮助,欢迎为项目贡献PR
|
||||
title: "[Feature]"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
!!!SDK的强大离不开社区的帮助,欢迎为本项目贡献PR!!!
|
||||
-->
|
||||
**你想要实现的模块或API**
|
||||
|
||||
|
||||
15
.github/ISSUE_TEMPLATE/question.md
vendored
15
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -1,15 +0,0 @@
|
||||
---
|
||||
name: 使用咨询
|
||||
about: 关于SDK使用相关的咨询,在使用前请先阅读官方微信文档
|
||||
title: "[咨询]"
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
重要:
|
||||
1、在使用本SDK前请先阅读对应的官方微信API文档:https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html
|
||||
2、本SDK部分接口文档: https://silenceper.com/wechat/
|
||||
-->
|
||||
**请描述您的问题**
|
||||
51
.github/workflows/go.yml
vendored
51
.github/workflows/go.yml
vendored
@@ -1,51 +0,0 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master,release-*,v2 ]
|
||||
pull_request:
|
||||
branches: [ master,release-*,v2 ]
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.15.x,1.16.x]
|
||||
name: golangci-lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v2
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3.1.0
|
||||
with:
|
||||
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
||||
version: v1.31
|
||||
build:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: --entrypoint redis-server
|
||||
memcached:
|
||||
image: memcached
|
||||
ports:
|
||||
- 11211:11211
|
||||
|
||||
# strategy set
|
||||
strategy:
|
||||
matrix:
|
||||
go: ["1.14","1.15", "1.16", "1.17", "1.18"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
id: go
|
||||
- name: Test
|
||||
run: go test -v -race ./...
|
||||
29
.github/workflows/release.yml
vendored
29
.github/workflows/release.yml
vendored
@@ -1,29 +0,0 @@
|
||||
name: goreleaser
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.15
|
||||
-
|
||||
name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -26,5 +26,4 @@ _testmain.go
|
||||
.vscode/
|
||||
vendor
|
||||
.idea/
|
||||
example/*
|
||||
/test
|
||||
examples/tcb/*
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
linters:
|
||||
# please, do not use `enable-all`: it's deprecated and will be removed soon.
|
||||
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
|
||||
disable-all: true
|
||||
enable:
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- depguard
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- funlen
|
||||
- goconst
|
||||
# - gocritic
|
||||
- gocyclo
|
||||
- gofmt
|
||||
- goimports
|
||||
- golint
|
||||
- goprintffuncname
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- interfacer
|
||||
- misspell
|
||||
- nolintlint
|
||||
- rowserrcheck
|
||||
- scopelint
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- stylecheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- whitespace
|
||||
|
||||
issues:
|
||||
# Excluding configuration per-path, per-linter, per-text and per-source
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gomnd
|
||||
|
||||
# https://github.com/go-critic/go-critic/issues/926
|
||||
- linters:
|
||||
- gocritic
|
||||
text: "unnecessaryDefer:"
|
||||
|
||||
linters-settings:
|
||||
funlen:
|
||||
lines: 66
|
||||
statements: 40
|
||||
|
||||
issues:
|
||||
include:
|
||||
- EXC0002 # disable excluding of issues about comments from golint
|
||||
exclude-rules:
|
||||
- linters:
|
||||
- stylecheck
|
||||
text: "ST1000:"
|
||||
@@ -1,29 +0,0 @@
|
||||
# This is an example goreleaser.yaml file with some sane defaults.
|
||||
# Make sure to check the documentation at http://goreleaser.com
|
||||
before:
|
||||
hooks:
|
||||
# You may remove this if you don't use go modules.
|
||||
- go mod download
|
||||
# you may remove this if you don't need go generate
|
||||
- go generate ./...
|
||||
builds:
|
||||
- skip: true
|
||||
|
||||
archives:
|
||||
- replacements:
|
||||
darwin: Darwin
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
19
.travis.yml
Normal file
19
.travis.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.13.x
|
||||
- 1.12.x
|
||||
- 1.11.x
|
||||
|
||||
services:
|
||||
- memcached
|
||||
- redis-server
|
||||
|
||||
before_script:
|
||||
- GO_FILES=$(find . -iname '*.go' -type f | grep -v /vendor/)
|
||||
- go get golang.org/x/lint/golint
|
||||
|
||||
script:
|
||||
- go test -v -race ./...
|
||||
- go vet ./...
|
||||
- golint -set_exit_status $(go list ./...)
|
||||
669
README.md
669
README.md
@@ -1,83 +1,642 @@
|
||||
# WeChat SDK for Go
|
||||
|
||||

|
||||
[](https://travis-ci.org/silenceper/wechat)
|
||||
[](https://goreportcard.com/report/github.com/silenceper/wechat)
|
||||
[](https://pkg.go.dev/github.com/silenceper/wechat/v2?tab=doc)
|
||||

|
||||
[](http://godoc.org/github.com/silenceper/wechat)
|
||||
|
||||
使用Golang开发的微信SDK,简单、易用。
|
||||
> 注意:当前版本为v2版本,v1版本已废弃
|
||||
|
||||
## 文档 && 例子
|
||||
|
||||
[API列表](https://github.com/silenceper/wechat/tree/v2/doc/api)
|
||||
|
||||
[Wechat SDK 2.0 文档](https://silenceper.com/wechat)
|
||||
|
||||
[Wechat SDK 2.0 例子](https://github.com/gowechat/example)
|
||||
|
||||
## 快速开始
|
||||
|
||||
```
|
||||
import "github.com/silenceper/wechat/v2"
|
||||
```
|
||||
|
||||
以下是一个微信公众号处理消息接收以及回复的例子:
|
||||
以下是一个处理消息接收以及回复的例子:
|
||||
|
||||
```go
|
||||
//使用memcache保存access_token,也可选择redis或自定义cache
|
||||
wc := wechat.NewWechat()
|
||||
memory := cache.NewMemory()
|
||||
cfg := &offConfig.Config{
|
||||
AppID: "xxx",
|
||||
AppSecret: "xxx",
|
||||
Token: "xxx",
|
||||
//EncodingAESKey: "xxxx",
|
||||
Cache: memory,
|
||||
memCache=cache.NewMemcache("127.0.0.1:11211")
|
||||
|
||||
//配置微信参数
|
||||
config := &wechat.Config{
|
||||
AppID: "xxxx",
|
||||
AppSecret: "xxxx",
|
||||
Token: "xxxx",
|
||||
EncodingAESKey: "xxxx",
|
||||
Cache: memCache
|
||||
}
|
||||
officialAccount := wc.GetOfficialAccount(cfg)
|
||||
wc := wechat.NewWechat(config)
|
||||
|
||||
// 传入request和responseWriter
|
||||
server := officialAccount.GetServer(req, rw)
|
||||
//设置接收消息的处理方法
|
||||
server.SetMessageHandler(func(msg *message.MixMessage) *message.Reply {
|
||||
server := wc.GetServer(request, responseWriter)
|
||||
server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {
|
||||
|
||||
//回复消息:演示回复用户发送的消息
|
||||
text := message.NewText(msg.Content)
|
||||
return &message.Reply{MsgType: message.MsgTypeText, MsgData: text}
|
||||
//回复消息:演示回复用户发送的消息
|
||||
text := message.NewText(msg.Content)
|
||||
return &message.Reply{message.MsgTypeText, text}
|
||||
})
|
||||
|
||||
//处理消息接收以及回复
|
||||
err := server.Serve()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
//发送回复的消息
|
||||
server.Serve()
|
||||
server.Send()
|
||||
|
||||
```
|
||||
完整代码:[examples/http/http.go](./examples/http/http.go)
|
||||
|
||||
## 目录说明
|
||||
#### 和主流框架配合使用
|
||||
|
||||
- officialaccount: 微信公众号API
|
||||
- miniprogram: 小程序API
|
||||
- minigame:小游戏API
|
||||
- pay:微信支付API
|
||||
- openplatform:开放平台API
|
||||
- work:企业微信
|
||||
- aispeech:智能对话
|
||||
- doc: api文档
|
||||
主要是request和responseWriter在不同框架中获取方式可能不一样:
|
||||
|
||||
## 贡献
|
||||
- Beego: [./examples/beego/beego.go](./examples/beego/beego.go)
|
||||
- Gin Framework: [./examples/gin/gin.go](./examples/gin/gin.go)
|
||||
|
||||
- 在[API列表](https://github.com/silenceper/wechat/tree/v2/doc/api)中查看哪些API未实现
|
||||
- 提交issue,描述需要贡献的内容
|
||||
- 完成更改后,提交PR
|
||||
## 基本配置
|
||||
|
||||
## 公众号
|
||||
```go
|
||||
memcache := cache.NewMemcache("127.0.0.1:11211")
|
||||
|
||||

|
||||
wcConfig := &wechat.Config{
|
||||
AppID: cfg.AppID,
|
||||
AppSecret: cfg.AppSecret,
|
||||
Token: cfg.Token,
|
||||
EncodingAESKey: cfg.EncodingAESKey,//消息加解密时用到
|
||||
Cache: memcache,
|
||||
}
|
||||
```
|
||||
|
||||
**Cache 设置**
|
||||
|
||||
Cache主要用来保存全局access_token以及js-sdk中的ticket:
|
||||
默认采用memcache存储。当然也可以直接实现`cache/cache.go`中的接口
|
||||
|
||||
|
||||
## 基本API使用
|
||||
|
||||
- [消息管理](#消息管理)
|
||||
- 接收普通消息
|
||||
- 接收事件推送
|
||||
- 被动回复消息
|
||||
- 回复文本消息
|
||||
- 回复图片消息
|
||||
- 回复视频消息
|
||||
- 回复音乐消息
|
||||
- 回复图文消息
|
||||
- [自定义菜单](#自定义菜单)
|
||||
- 自定义菜单创建接口
|
||||
- 自定义菜单查询接口
|
||||
- 自定义菜单删除接口
|
||||
- 自定义菜单事件推送
|
||||
- 个性化菜单接口
|
||||
- 添加个性化菜单
|
||||
- 删除个性化菜单
|
||||
- 测试个性化菜单匹配结果
|
||||
- 获取公众号菜单配置
|
||||
- [微信网页开发](#微信网页开发)
|
||||
- Oauth2 授权
|
||||
- 发起授权
|
||||
- 通过code换取access_token
|
||||
- 拉取用户信息
|
||||
- 刷新access_token
|
||||
- 检验access_token是否有效
|
||||
- 获取js-sdk配置
|
||||
- [素材管理](#素材管理)
|
||||
- [小程序开发](#小程序开发)
|
||||
- [小程序-云开发](./tcb)
|
||||
|
||||
## 消息管理
|
||||
|
||||
通过`wechat.GetServer(request,responseWriter)`获取到server对象之后
|
||||
|
||||
调用`SetMessageHandler(func(msg message.MixMessage){})`设置消息的处理函数,函数参数为message.MixMessage 结构如下:
|
||||
|
||||
```go
|
||||
//MixMessage 存放所有微信发送过来的消息和事件
|
||||
type MixMessage struct {
|
||||
CommonToken
|
||||
|
||||
//基本消息
|
||||
MsgID int64 `xml:"MsgId"`
|
||||
Content string `xml:"Content"`
|
||||
PicURL string `xml:"PicUrl"`
|
||||
MediaID string `xml:"MediaId"`
|
||||
Format string `xml:"Format"`
|
||||
ThumbMediaID string `xml:"ThumbMediaId"`
|
||||
LocationX float64 `xml:"Location_X"`
|
||||
LocationY float64 `xml:"Location_Y"`
|
||||
Scale float64 `xml:"Scale"`
|
||||
Label string `xml:"Label"`
|
||||
Title string `xml:"Title"`
|
||||
Description string `xml:"Description"`
|
||||
URL string `xml:"Url"`
|
||||
|
||||
//事件相关
|
||||
Event string `xml:"Event"`
|
||||
EventKey string `xml:"EventKey"`
|
||||
Ticket string `xml:"Ticket"`
|
||||
Latitude string `xml:"Latitude"`
|
||||
Longitude string `xml:"Longitude"`
|
||||
Precision string `xml:"Precision"`
|
||||
|
||||
MenuID string `xml:"MenuId"`
|
||||
|
||||
//扫码事件
|
||||
ScanCodeInfo struct {
|
||||
ScanType string `xml:"ScanType"`
|
||||
ScanResult string `xml:"ScanResult"`
|
||||
} `xml:"ScanCodeInfo"`
|
||||
|
||||
//发图事件
|
||||
SendPicsInfo struct {
|
||||
Count int32 `xml:"Count"`
|
||||
PicList []EventPic `xml:"PicList>item"`
|
||||
} `xml:"SendPicsInfo"`
|
||||
|
||||
//发送地理位置事件
|
||||
SendLocationInfo struct {
|
||||
LocationX float64 `xml:"Location_X"`
|
||||
LocationY float64 `xml:"Location_Y"`
|
||||
Scale float64 `xml:"Scale"`
|
||||
Label string `xml:"Label"`
|
||||
Poiname string `xml:"Poiname"`
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
具体参数请参考微信文档:[接收普通消息
|
||||
](http://mp.weixin.qq.com/wiki/17/f298879f8fb29ab98b2f2971d42552fd.html)
|
||||
|
||||
### 接收普通消息
|
||||
```go
|
||||
server.SetMessageHandler(func(v message.MixMessage) *message.Reply {
|
||||
switch v.MsgType {
|
||||
//文本消息
|
||||
case message.MsgTypeText:
|
||||
//do something
|
||||
|
||||
//图片消息
|
||||
case message.MsgTypeImage:
|
||||
//do something
|
||||
|
||||
//语音消息
|
||||
case message.MsgTypeVoice:
|
||||
//do something
|
||||
|
||||
//视频消息
|
||||
case message.MsgTypeVideo:
|
||||
//do something
|
||||
|
||||
//小视频消息
|
||||
case message.MsgTypeShortVideo:
|
||||
//do something
|
||||
|
||||
//地理位置消息
|
||||
case message.MsgTypeLocation:
|
||||
//do something
|
||||
|
||||
//链接消息
|
||||
case message.MsgTypeLink:
|
||||
//do something
|
||||
|
||||
//事件推送消息
|
||||
case message.MsgTypeEvent:
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
### 接收事件推送
|
||||
```go
|
||||
//事件推送消息
|
||||
case message.MsgTypeEvent:
|
||||
switch v.Event {
|
||||
//EventSubscribe 订阅
|
||||
case message.EventSubscribe:
|
||||
//do something
|
||||
|
||||
//取消订阅
|
||||
case message.EventUnsubscribe:
|
||||
//do something
|
||||
|
||||
//用户已经关注公众号,则微信会将带场景值扫描事件推送给开发者
|
||||
case message.EventScan:
|
||||
//do something
|
||||
|
||||
// 上报地理位置事件
|
||||
case message.EventLocation:
|
||||
//do something
|
||||
|
||||
// 点击菜单拉取消息时的事件推送
|
||||
case message.EventClick:
|
||||
//do something
|
||||
|
||||
// 点击菜单跳转链接时的事件推送
|
||||
case message.EventView:
|
||||
//do something
|
||||
|
||||
// 扫码推事件的事件推送
|
||||
case message.EventScancodePush:
|
||||
//do something
|
||||
|
||||
// 扫码推事件且弹出“消息接收中”提示框的事件推送
|
||||
case message.EventScancodeWaitmsg:
|
||||
//do something
|
||||
|
||||
// 弹出系统拍照发图的事件推送
|
||||
case message.EventPicSysphoto:
|
||||
//do something
|
||||
|
||||
// 弹出拍照或者相册发图的事件推送
|
||||
case message.EventPicPhotoOrAlbum:
|
||||
//do something
|
||||
|
||||
// 弹出微信相册发图器的事件推送
|
||||
case message.EventPicWeixin:
|
||||
//do something
|
||||
|
||||
// 弹出地理位置选择器的事件推送
|
||||
case message.EventLocationSelect:
|
||||
//do something
|
||||
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
### 被动回复消息
|
||||
|
||||
回复消息需要返回 `*message.Reply` 对象结构体如下:
|
||||
|
||||
```go
|
||||
type Reply struct {
|
||||
MsgType MsgType //消息类型
|
||||
MsgData interface{} //消息结构
|
||||
}
|
||||
|
||||
```
|
||||
注意:`return nil`表示什么也不做
|
||||
|
||||
#### 回复文本消息
|
||||
```go
|
||||
text := message.NewText("回复文本消息")
|
||||
return &message.Reply{message.MsgTypeText, text}
|
||||
```
|
||||
#### 回复图片消息
|
||||
```go
|
||||
//mediaID 可通过素材管理-上上传多媒体文件获得
|
||||
image :=message.NewImage("mediaID")
|
||||
return &message.Reply{message.MsgTypeImage, image}
|
||||
```
|
||||
#### 回复视频消息
|
||||
```go
|
||||
video := message.NewVideo("mediaID", "视频标题", "视频描述")
|
||||
return &message.Reply{message.MsgTypeVideo, video}
|
||||
```
|
||||
#### 回复音乐消息
|
||||
```go
|
||||
music := message.NewMusic("title", "description", "musicURL", "hQMusicURL", "thumbMediaID")
|
||||
return &message.Reply{message.MsgTypeMusic,music}
|
||||
```
|
||||
**字段说明:**
|
||||
|
||||
Title:音乐标题
|
||||
|
||||
Description:音乐描述
|
||||
|
||||
MusicURL:音乐链接
|
||||
|
||||
HQMusicUrl:高质量音乐链接,WIFI环境优先使用该链接播放音乐
|
||||
|
||||
ThumbMediaId:缩略图的媒体id,通过素材管理接口上传多媒体文件,得到的id
|
||||
|
||||
#### 回复图文消息
|
||||
|
||||
```go
|
||||
articles := make([]*message.Article, 1)
|
||||
|
||||
article := new(message.Article)
|
||||
article.Title = "标题"
|
||||
article.Description = "描述信息信息信息"
|
||||
article.PicURL = "http://ww1.sinaimg.cn/large/65209136gw1f7vhjw95eqj20wt0zk40z.jpg"
|
||||
article.URL = "https://github.com/silenceper/wechat"
|
||||
articles[0] = article
|
||||
|
||||
news := message.NewNews(articles)
|
||||
return &message.Reply{message.MsgTypeNews,news}
|
||||
```
|
||||
**字段说明:**
|
||||
|
||||
Title:图文消息标题
|
||||
|
||||
Description:图文消息描述
|
||||
|
||||
PicUrl :图片链接,支持JPG、PNG格式,较好的效果为大图360*200,小图200*200
|
||||
|
||||
Url :点击图文消息跳转链接
|
||||
|
||||
|
||||
## 自定义菜单
|
||||
|
||||
通过` wechat.GetMenu()`获取menu的实例
|
||||
|
||||
### 自定义菜单创建接口
|
||||
|
||||
以下是一个创建二级菜单的例子
|
||||
|
||||
```go
|
||||
mu := wc.GetMenu()
|
||||
|
||||
buttons := make([]*menu.Button, 1)
|
||||
btn := new(menu.Button)
|
||||
|
||||
//创建click类型菜单
|
||||
btn.SetClickButton("name", "key123")
|
||||
buttons[0] = btn
|
||||
|
||||
//设置btn为二级菜单
|
||||
btn2 := new(menu.Button)
|
||||
btn2.SetSubButton("subButton", buttons)
|
||||
|
||||
buttons2 := make([]*menu.Button, 1)
|
||||
buttons2[0] = btn2
|
||||
|
||||
//发送请求
|
||||
err := mu.SetMenu(buttons2)
|
||||
if err != nil {
|
||||
fmt.Printf("err= %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**创建其他类型的菜单:**
|
||||
|
||||
```go
|
||||
//SetViewButton view类型
|
||||
func (btn *Button) SetViewButton(name, url string)
|
||||
|
||||
// SetScanCodePushButton 扫码推事件
|
||||
func (btn *Button) SetScanCodePushButton(name, key string)
|
||||
|
||||
//SetScanCodeWaitMsgButton 设置 扫码推事件且弹出"消息接收中"提示框
|
||||
func (btn *Button) SetScanCodeWaitMsgButton(name, key string)
|
||||
|
||||
//SetPicSysPhotoButton 设置弹出系统拍照发图按钮
|
||||
func (btn *Button) SetPicSysPhotoButton(name, key string)
|
||||
|
||||
//SetPicPhotoOrAlbumButton 设置弹出拍照或者相册发图类型按钮
|
||||
func (btn *Button) SetPicPhotoOrAlbumButton(name, key string) {
|
||||
|
||||
// SetPicWeixinButton 设置弹出微信相册发图器类型按钮
|
||||
func (btn *Button) SetPicWeixinButton(name, key string)
|
||||
|
||||
// SetLocationSelectButton 设置 弹出地理位置选择器 类型按钮
|
||||
func (btn *Button) SetLocationSelectButton(name, key string)
|
||||
|
||||
//SetMediaIDButton 设置 下发消息(除文本消息) 类型按钮
|
||||
func (btn *Button) SetMediaIDButton(name, mediaID string)
|
||||
|
||||
//SetViewLimitedButton 设置 跳转图文消息URL 类型按钮
|
||||
func (btn *Button) SetViewLimitedButton(name, mediaID string) {
|
||||
|
||||
```
|
||||
|
||||
### 自定义菜单查询接口
|
||||
|
||||
```go
|
||||
mu := wc.GetMenu()
|
||||
resMenu,err:=mu.GetMenu()
|
||||
```
|
||||
>返回结果 resMenu 结构参考 ./menu/menu.go 中ResMenu 结构体
|
||||
|
||||
### 自定义菜单删除接口
|
||||
|
||||
```go
|
||||
mu := wc.GetMenu()
|
||||
err:=mu.DeleteMenu()
|
||||
```
|
||||
|
||||
### 自定义菜单事件推送
|
||||
|
||||
请参考 消息管理 - 事件推送
|
||||
|
||||
### 个性化菜单接口
|
||||
**添加个性化菜单**
|
||||
|
||||
```go
|
||||
|
||||
func (menu *Menu) AddConditional(buttons []*Button, matchRule *MatchRule) error
|
||||
```
|
||||
|
||||
**删除个性化菜单**
|
||||
|
||||
```go
|
||||
//删除个性化菜单
|
||||
func (menu *Menu) DeleteConditional(menuID int64) error
|
||||
|
||||
```
|
||||
**测试个性化菜单匹配结果**
|
||||
|
||||
```go
|
||||
//菜单匹配
|
||||
func (menu *Menu) MenuTryMatch(userID string) (buttons []Button, err error) {
|
||||
|
||||
```
|
||||
|
||||
### 获取公众号菜单配置
|
||||
|
||||
```go
|
||||
//获取自定义菜单配置接口
|
||||
func (menu *Menu) GetCurrentSelfMenuInfo() (resSelfMenuInfo ResSelfMenuInfo, err error)
|
||||
|
||||
```
|
||||
|
||||
## 微信网页开发
|
||||
|
||||
### Oauth2 授权
|
||||
|
||||
具体授权流程请参考微信文档:[网页授权](http://mp.weixin.qq.com/wiki/4/9ac2e7b1f1d22e9e57260f6553822520.html)
|
||||
|
||||
**1.发起授权**
|
||||
|
||||
```go
|
||||
oauth := wc.GetOauth()
|
||||
err := oauth.Redirect("跳转的绝对地址", "snsapi_userinfo", "123dd123")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
```
|
||||
> 如果不希望直接跳转,可通过 oauth.GetRedirectURL 获取跳转的url
|
||||
|
||||
**2.通过code换取access_token**
|
||||
|
||||
```go
|
||||
code := c.Query("code")
|
||||
resToken, err := oauth.GetUserAccessToken(code)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
```
|
||||
**3.拉取用户信息(需scope为 snsapi_userinfo)**
|
||||
|
||||
```go
|
||||
//getUserInfo
|
||||
userInfo, err := oauth.GetUserInfo(resToken.AccessToken, resToken.OpenID)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(userInfo)
|
||||
|
||||
```
|
||||
**刷新access_token**
|
||||
|
||||
```go
|
||||
func (oauth *Oauth) RefreshAccessToken(refreshToken string) (result ResAccessToken, err error)
|
||||
|
||||
```
|
||||
**检验access_token是否有效**
|
||||
|
||||
```go
|
||||
func (oauth *Oauth) CheckAccessToken(accessToken, openID string) (b bool, err error)
|
||||
```
|
||||
|
||||
### 获取js-sdk配置
|
||||
|
||||
```go
|
||||
js := wc.GetJs()
|
||||
cfg, err := js.GetConfig("传入需要的调用js-sdk的url地址")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(cfg)
|
||||
```
|
||||
其中返回的cfg结构体如下:
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
AppID string `json:"app_id"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
NonceStr string `json:"nonce_str"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 素材管理
|
||||
|
||||
[素材管理API](https://godoc.org/github.com/silenceper/wechat/material#Material)
|
||||
|
||||
## 小程序开发
|
||||
|
||||
获取小程序操作对象
|
||||
|
||||
``` go
|
||||
memCache=cache.NewMemcache("127.0.0.1:11211")
|
||||
config := &wechat.Config{
|
||||
AppID: "xxx",
|
||||
AppSecret: "xxx",
|
||||
Cache: memCache=cache.NewMemcache("127.0.0.1:11211"),
|
||||
}
|
||||
wc := wechat.NewWechat(config)
|
||||
|
||||
wxa := wc.GetMiniProgram()
|
||||
```
|
||||
|
||||
### 小程序登录凭证校验
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) Code2Session(jsCode string) (result ResCode2Session, err error)
|
||||
```
|
||||
|
||||
### 小程序数据统计
|
||||
|
||||
**获取用户访问小程序日留存**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisDailyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error)
|
||||
```
|
||||
|
||||
**获取用户访问小程序月留存**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisMonthlyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error)
|
||||
```
|
||||
|
||||
**获取用户访问小程序周留存**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisWeeklyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error)
|
||||
```
|
||||
|
||||
**获取用户访问小程序数据概况**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisDailySummary(beginDate, endDate string) (result ResAnalysisDailySummary, err error)
|
||||
```
|
||||
|
||||
**获取用户访问小程序数据日趋势**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisDailyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error)
|
||||
```
|
||||
|
||||
**获取用户访问小程序数据月趋势**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisMonthlyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error)
|
||||
```
|
||||
|
||||
**获取用户访问小程序数据周趋势**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisWeeklyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error)
|
||||
```
|
||||
|
||||
**获取小程序新增或活跃用户的画像分布数据**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisUserPortrait(beginDate, endDate string) (result ResAnalysisUserPortrait, err error)
|
||||
```
|
||||
|
||||
**获取用户小程序访问分布数据**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisVisitDistribution(beginDate, endDate string) (result ResAnalysisVisitDistribution, err error)
|
||||
```
|
||||
|
||||
**获取小程序页面访问数据**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetAnalysisVisitPage(beginDate, endDate string) (result ResAnalysisVisitPage, err error)
|
||||
```
|
||||
|
||||
### 小程序二维码生成
|
||||
|
||||
**获取小程序二维码,适用于需要的码数量较少的业务场景**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) CreateWXAQRCode(coderParams QRCoder) (response []byte, err error)
|
||||
```
|
||||
|
||||
**获取小程序码,适用于需要的码数量较少的业务场景**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetWXACode(coderParams QRCoder) (response []byte, err error)
|
||||
```
|
||||
|
||||
**获取小程序码,适用于需要的码数量极多的业务场景**
|
||||
|
||||
``` go
|
||||
func (wxa *MiniProgram) GetWXACodeUnlimit(coderParams QRCoder) (response []byte, err error)
|
||||
```
|
||||
|
||||
|
||||
更多API使用请参考 godoc :
|
||||
[https://godoc.org/github.com/silenceper/wechat](https://godoc.org/github.com/silenceper/wechat)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# 智能对话
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/doc/aispeech/platform/INTERFACEDOCUMENT.html)
|
||||
|
||||
## 快速入门
|
||||
2
cache/cache.go
vendored
2
cache/cache.go
vendored
@@ -2,7 +2,7 @@ package cache
|
||||
|
||||
import "time"
|
||||
|
||||
// Cache interface
|
||||
//Cache interface
|
||||
type Cache interface {
|
||||
Get(key string) interface{}
|
||||
Set(key string, val interface{}, timeout time.Duration) error
|
||||
|
||||
10
cache/memcache.go
vendored
10
cache/memcache.go
vendored
@@ -7,18 +7,18 @@ import (
|
||||
"github.com/bradfitz/gomemcache/memcache"
|
||||
)
|
||||
|
||||
// Memcache struct contains *memcache.Client
|
||||
//Memcache struct contains *memcache.Client
|
||||
type Memcache struct {
|
||||
conn *memcache.Client
|
||||
}
|
||||
|
||||
// NewMemcache create new memcache
|
||||
//NewMemcache create new memcache
|
||||
func NewMemcache(server ...string) *Memcache {
|
||||
mc := memcache.New(server...)
|
||||
return &Memcache{mc}
|
||||
}
|
||||
|
||||
// Get return cached value
|
||||
//Get return cached value
|
||||
func (mem *Memcache) Get(key string) interface{} {
|
||||
var err error
|
||||
var item *memcache.Item
|
||||
@@ -40,7 +40,7 @@ func (mem *Memcache) IsExist(key string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Set cached value with key and expire time.
|
||||
//Set cached value with key and expire time.
|
||||
func (mem *Memcache) Set(key string, val interface{}, timeout time.Duration) (err error) {
|
||||
var data []byte
|
||||
if data, err = json.Marshal(val); err != nil {
|
||||
@@ -51,7 +51,7 @@ func (mem *Memcache) Set(key string, val interface{}, timeout time.Duration) (er
|
||||
return mem.conn.Set(item)
|
||||
}
|
||||
|
||||
// Delete delete value in memcache.
|
||||
//Delete delete value in memcache.
|
||||
func (mem *Memcache) Delete(key string) error {
|
||||
return mem.conn.Delete(key)
|
||||
}
|
||||
|
||||
16
cache/memcache_test.go
vendored
16
cache/memcache_test.go
vendored
@@ -3,9 +3,6 @@ package cache
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bradfitz/gomemcache/memcache"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMemcache(t *testing.T) {
|
||||
@@ -19,22 +16,13 @@ func TestMemcache(t *testing.T) {
|
||||
if !mem.IsExist("username") {
|
||||
t.Error("IsExist Error")
|
||||
}
|
||||
exists := mem.IsExist("unknown-key")
|
||||
assert.Equal(t, false, exists)
|
||||
|
||||
name := mem.Get("username").(string)
|
||||
if name != "" {
|
||||
if name != "silenceper" {
|
||||
t.Error("get Error")
|
||||
}
|
||||
if name != "silenceper" {
|
||||
t.Error("get Error")
|
||||
}
|
||||
data := mem.Get("unknown-key")
|
||||
assert.Nil(t, data)
|
||||
|
||||
if err = mem.Delete("username"); err != nil {
|
||||
t.Errorf("delete Error , err=%v", err)
|
||||
}
|
||||
|
||||
err = mem.Delete("unknown-key")
|
||||
assert.Equal(t, memcache.ErrCacheMiss, err)
|
||||
}
|
||||
|
||||
16
cache/memory.go
vendored
16
cache/memory.go
vendored
@@ -5,7 +5,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Memory struct contains *memcache.Client
|
||||
//Memory struct contains *memcache.Client
|
||||
type Memory struct {
|
||||
sync.Mutex
|
||||
|
||||
@@ -17,14 +17,14 @@ type data struct {
|
||||
Expired time.Time
|
||||
}
|
||||
|
||||
// NewMemory create new memcache
|
||||
//NewMemory create new memcache
|
||||
func NewMemory() *Memory {
|
||||
return &Memory{
|
||||
data: map[string]*data{},
|
||||
}
|
||||
}
|
||||
|
||||
// Get return cached value
|
||||
//Get return cached value
|
||||
func (mem *Memory) Get(key string) interface{} {
|
||||
if ret, ok := mem.data[key]; ok {
|
||||
if ret.Expired.Before(time.Now()) {
|
||||
@@ -48,7 +48,7 @@ func (mem *Memory) IsExist(key string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Set cached value with key and expire time.
|
||||
//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()
|
||||
@@ -60,15 +60,15 @@ func (mem *Memory) Set(key string, val interface{}, timeout time.Duration) (err
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete delete value in memcache.
|
||||
//Delete delete value in memcache.
|
||||
func (mem *Memory) Delete(key string) error {
|
||||
mem.deleteKey(key)
|
||||
return nil
|
||||
return mem.deleteKey(key)
|
||||
}
|
||||
|
||||
// deleteKey
|
||||
func (mem *Memory) deleteKey(key string) {
|
||||
func (mem *Memory) deleteKey(key string) error {
|
||||
mem.Lock()
|
||||
defer mem.Unlock()
|
||||
delete(mem.data, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
109
cache/redis.go
vendored
109
cache/redis.go
vendored
@@ -1,72 +1,109 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
)
|
||||
|
||||
// Redis .redis cache
|
||||
//Redis redis cache
|
||||
type Redis struct {
|
||||
ctx context.Context
|
||||
conn redis.UniversalClient
|
||||
conn *redis.Pool
|
||||
}
|
||||
|
||||
// RedisOpts redis 连接属性
|
||||
//RedisOpts redis 连接属性
|
||||
type RedisOpts struct {
|
||||
Host string `yml:"host" json:"host"`
|
||||
Password string `yml:"password" json:"password"`
|
||||
Database int `yml:"database" json:"database"`
|
||||
MaxIdle int `yml:"max_idle" json:"max_idle"`
|
||||
MaxActive int `yml:"max_active" json:"max_active"`
|
||||
IdleTimeout int `yml:"idle_timeout" json:"idle_timeout"` // second
|
||||
IdleTimeout int32 `yml:"idle_timeout" json:"idle_timeout"` //second
|
||||
}
|
||||
|
||||
// NewRedis 实例化
|
||||
func NewRedis(ctx context.Context, opts *RedisOpts) *Redis {
|
||||
conn := redis.NewUniversalClient(&redis.UniversalOptions{
|
||||
Addrs: []string{opts.Host},
|
||||
DB: opts.Database,
|
||||
Password: opts.Password,
|
||||
IdleTimeout: time.Second * time.Duration(opts.IdleTimeout),
|
||||
MinIdleConns: opts.MaxIdle,
|
||||
})
|
||||
return &Redis{ctx: ctx, conn: conn}
|
||||
//NewRedis 实例化
|
||||
func NewRedis(opts *RedisOpts) *Redis {
|
||||
pool := &redis.Pool{
|
||||
MaxActive: opts.MaxActive,
|
||||
MaxIdle: opts.MaxIdle,
|
||||
IdleTimeout: time.Second * time.Duration(opts.IdleTimeout),
|
||||
Dial: func() (redis.Conn, error) {
|
||||
return redis.Dial("tcp", opts.Host,
|
||||
redis.DialDatabase(opts.Database),
|
||||
redis.DialPassword(opts.Password),
|
||||
)
|
||||
},
|
||||
TestOnBorrow: func(conn redis.Conn, t time.Time) error {
|
||||
if time.Since(t) < time.Minute {
|
||||
return nil
|
||||
}
|
||||
_, err := conn.Do("PING")
|
||||
return err
|
||||
},
|
||||
}
|
||||
return &Redis{pool}
|
||||
}
|
||||
|
||||
// SetConn 设置conn
|
||||
func (r *Redis) SetConn(conn redis.UniversalClient) {
|
||||
//SetConn 设置conn
|
||||
func (r *Redis) SetConn(conn *redis.Pool) {
|
||||
r.conn = conn
|
||||
}
|
||||
|
||||
// SetRedisCtx 设置redis ctx 参数
|
||||
func (r *Redis) SetRedisCtx(ctx context.Context) {
|
||||
r.ctx = ctx
|
||||
}
|
||||
|
||||
// Get 获取一个值
|
||||
//Get 获取一个值
|
||||
func (r *Redis) Get(key string) interface{} {
|
||||
result, err := r.conn.Do(r.ctx, "GET", key).Result()
|
||||
if err != nil {
|
||||
conn := r.conn.Get()
|
||||
defer conn.Close()
|
||||
|
||||
var data []byte
|
||||
var err error
|
||||
if data, err = redis.Bytes(conn.Do("GET", key)); err != nil {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
var reply interface{}
|
||||
if err = json.Unmarshal(data, &reply); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return reply
|
||||
}
|
||||
|
||||
// Set 设置一个值
|
||||
func (r *Redis) Set(key string, val interface{}, timeout time.Duration) error {
|
||||
return r.conn.SetEX(r.ctx, key, val, timeout).Err()
|
||||
//Set 设置一个值
|
||||
func (r *Redis) Set(key string, val interface{}, timeout time.Duration) (err error) {
|
||||
conn := r.conn.Get()
|
||||
defer conn.Close()
|
||||
|
||||
var data []byte
|
||||
if data, err = json.Marshal(val); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = conn.Do("SETEX", key, int64(timeout/time.Second), data)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IsExist 判断key是否存在
|
||||
//IsExist 判断key是否存在
|
||||
func (r *Redis) IsExist(key string) bool {
|
||||
result, _ := r.conn.Exists(r.ctx, key).Result()
|
||||
conn := r.conn.Get()
|
||||
defer conn.Close()
|
||||
|
||||
return result > 0
|
||||
a, _ := conn.Do("EXISTS", key)
|
||||
i := a.(int64)
|
||||
if i > 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Delete 删除
|
||||
//Delete 删除
|
||||
func (r *Redis) Delete(key string) error {
|
||||
return r.conn.Del(r.ctx, key).Err()
|
||||
conn := r.conn.Get()
|
||||
defer conn.Close()
|
||||
|
||||
if _, err := conn.Do("DEL", key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
30
cache/redis_test.go
vendored
30
cache/redis_test.go
vendored
@@ -1,40 +1,32 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRedis(t *testing.T) {
|
||||
var (
|
||||
timeoutDuration = time.Second
|
||||
ctx = context.Background()
|
||||
opts = &RedisOpts{
|
||||
Host: "127.0.0.1:6379",
|
||||
}
|
||||
redis = NewRedis(ctx, opts)
|
||||
err error
|
||||
val = "silenceper"
|
||||
key = "username"
|
||||
)
|
||||
redis.SetConn(redis.conn)
|
||||
redis.SetRedisCtx(ctx)
|
||||
opts := &RedisOpts{
|
||||
Host: "127.0.0.1:6379",
|
||||
}
|
||||
redis := NewRedis(opts)
|
||||
var err error
|
||||
timeoutDuration := 1 * time.Second
|
||||
|
||||
if err = redis.Set(key, val, timeoutDuration); err != nil {
|
||||
if err = redis.Set("username", "silenceper", timeoutDuration); err != nil {
|
||||
t.Error("set Error", err)
|
||||
}
|
||||
|
||||
if !redis.IsExist(key) {
|
||||
if !redis.IsExist("username") {
|
||||
t.Error("IsExist Error")
|
||||
}
|
||||
|
||||
name := redis.Get(key).(string)
|
||||
if name != val {
|
||||
name := redis.Get("username").(string)
|
||||
if name != "silenceper" {
|
||||
t.Error("get Error")
|
||||
}
|
||||
|
||||
if err = redis.Delete(key); err != nil {
|
||||
if err = redis.Delete("username"); err != nil {
|
||||
t.Errorf("delete Error , err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
85
context/access_token.go
Normal file
85
context/access_token.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
//AccessTokenURL 获取access_token的接口
|
||||
AccessTokenURL = "https://api.weixin.qq.com/cgi-bin/token"
|
||||
)
|
||||
|
||||
//ResAccessToken struct
|
||||
type ResAccessToken struct {
|
||||
util.CommonError
|
||||
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
//GetAccessTokenFunc 获取 access token 的函数签名
|
||||
type GetAccessTokenFunc func(ctx *Context) (accessToken string, err error)
|
||||
|
||||
//SetAccessTokenLock 设置读写锁(一个appID一个读写锁)
|
||||
func (ctx *Context) SetAccessTokenLock(l *sync.RWMutex) {
|
||||
ctx.accessTokenLock = l
|
||||
}
|
||||
|
||||
//SetGetAccessTokenFunc 设置自定义获取accessToken的方式, 需要自己实现缓存
|
||||
func (ctx *Context) SetGetAccessTokenFunc(f GetAccessTokenFunc) {
|
||||
ctx.accessTokenFunc = f
|
||||
}
|
||||
|
||||
//GetAccessToken 获取access_token
|
||||
func (ctx *Context) GetAccessToken() (accessToken string, err error) {
|
||||
ctx.accessTokenLock.Lock()
|
||||
defer ctx.accessTokenLock.Unlock()
|
||||
|
||||
if ctx.accessTokenFunc != nil {
|
||||
return ctx.accessTokenFunc(ctx)
|
||||
}
|
||||
accessTokenCacheKey := fmt.Sprintf("access_token_%s", ctx.AppID)
|
||||
val := ctx.Cache.Get(accessTokenCacheKey)
|
||||
if val != nil {
|
||||
accessToken = val.(string)
|
||||
return
|
||||
}
|
||||
|
||||
//从微信服务器获取
|
||||
var resAccessToken ResAccessToken
|
||||
resAccessToken, err = ctx.GetAccessTokenFromServer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
accessToken = resAccessToken.AccessToken
|
||||
return
|
||||
}
|
||||
|
||||
//GetAccessTokenFromServer 强制从微信服务器获取token
|
||||
func (ctx *Context) GetAccessTokenFromServer() (resAccessToken ResAccessToken, err error) {
|
||||
url := fmt.Sprintf("%s?grant_type=client_credential&appid=%s&secret=%s", AccessTokenURL, ctx.AppID, ctx.AppSecret)
|
||||
var body []byte
|
||||
body, err = util.HTTPGet(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(body, &resAccessToken)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if resAccessToken.ErrMsg != "" {
|
||||
err = fmt.Errorf("get access_token error : errcode=%v , errormsg=%v", resAccessToken.ErrCode, resAccessToken.ErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
accessTokenCacheKey := fmt.Sprintf("access_token_%s", ctx.AppID)
|
||||
expires := resAccessToken.ExpiresIn - 1500
|
||||
err = ctx.Cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
||||
return
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
// Package context 开放平台相关context
|
||||
package context
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -16,17 +14,11 @@ const (
|
||||
queryAuthURL = "https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token=%s"
|
||||
refreshTokenURL = "https://api.weixin.qq.com/cgi-bin/component/api_authorizer_token?component_access_token=%s"
|
||||
getComponentInfoURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=%s"
|
||||
componentLoginURL = "https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=%s&pre_auth_code=%s&redirect_uri=%s&auth_type=%d&biz_appid=%s"
|
||||
bindComponentURL = "https://mp.weixin.qq.com/safe/bindcomponent?action=bindcomponent&auth_type=%d&no_scan=1&component_appid=%s&pre_auth_code=%s&redirect_uri=%s&biz_appid=%s#wechat_redirect"
|
||||
// TODO 获取授权方选项信息
|
||||
// getComponentConfigURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option?component_access_token=%s"
|
||||
// TODO 获取已授权的账号信息
|
||||
// getuthorizerListURL = "POST https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_list?component_access_token=%s"
|
||||
getComponentConfigURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option?component_access_token=%s"
|
||||
)
|
||||
|
||||
// ComponentAccessToken 第三方平台
|
||||
type ComponentAccessToken struct {
|
||||
util.CommonError
|
||||
AccessToken string `json:"component_access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
@@ -58,15 +50,9 @@ func (ctx *Context) SetComponentAccessToken(verifyTicket string) (*ComponentAcce
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if at.ErrCode != 0 {
|
||||
return nil, fmt.Errorf("SetComponentAccessToken Error , errcode=%d , errmsg=%s", at.ErrCode, at.ErrMsg)
|
||||
}
|
||||
|
||||
accessTokenCacheKey := fmt.Sprintf("component_access_token_%s", ctx.AppID)
|
||||
expires := at.ExpiresIn - 1500
|
||||
if err := ctx.Cache.Set(accessTokenCacheKey, at.AccessToken, time.Duration(expires)*time.Second); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
ctx.Cache.Set(accessTokenCacheKey, at.AccessToken, time.Duration(expires)*time.Second)
|
||||
return at, nil
|
||||
}
|
||||
|
||||
@@ -95,24 +81,6 @@ func (ctx *Context) GetPreCode() (string, error) {
|
||||
return ret.PreCode, nil
|
||||
}
|
||||
|
||||
// GetComponentLoginPage 获取第三方公众号授权链接(扫码授权)
|
||||
func (ctx *Context) GetComponentLoginPage(redirectURI string, authType int, bizAppID string) (string, error) {
|
||||
code, err := ctx.GetPreCode()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf(componentLoginURL, ctx.AppID, code, url.QueryEscape(redirectURI), authType, bizAppID), nil
|
||||
}
|
||||
|
||||
// GetBindComponentURL 获取第三方公众号授权链接(链接跳转,适用移动端)
|
||||
func (ctx *Context) GetBindComponentURL(redirectURI string, authType int, bizAppID string) (string, error) {
|
||||
code, err := ctx.GetPreCode()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf(bindComponentURL, authType, ctx.AppID, code, url.QueryEscape(redirectURI), bizAppID), nil
|
||||
}
|
||||
|
||||
// ID 微信返回接口中各种类型字段
|
||||
type ID struct {
|
||||
ID int `json:"id"`
|
||||
@@ -155,17 +123,13 @@ func (ctx *Context) QueryAuthCode(authCode string) (*AuthBaseInfo, error) {
|
||||
}
|
||||
|
||||
var ret struct {
|
||||
util.CommonError
|
||||
Info *AuthBaseInfo `json:"authorization_info"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ret.ErrCode != 0 {
|
||||
err = fmt.Errorf("QueryAuthCode error : errcode=%v , errmsg=%v", ret.ErrCode, ret.ErrMsg)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret.Info, nil
|
||||
}
|
||||
|
||||
@@ -193,9 +157,8 @@ func (ctx *Context) RefreshAuthrToken(appid, refreshToken string) (*AuthrAccessT
|
||||
}
|
||||
|
||||
authrTokenKey := "authorizer_access_token_" + appid
|
||||
if err := ctx.Cache.Set(authrTokenKey, ret.AccessToken, time.Minute*80); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx.Cache.Set(authrTokenKey, ret.AccessToken, time.Minute*80)
|
||||
|
||||
return ret, 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)
|
||||
}
|
||||
58
context/context.go
Normal file
58
context/context.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/silenceper/wechat/cache"
|
||||
)
|
||||
|
||||
// Context struct
|
||||
type Context struct {
|
||||
AppID string
|
||||
AppSecret string
|
||||
Token string
|
||||
EncodingAESKey string
|
||||
PayMchID string
|
||||
PayNotifyURL string
|
||||
PayKey string
|
||||
|
||||
Cache cache.Cache
|
||||
|
||||
Writer http.ResponseWriter
|
||||
Request *http.Request
|
||||
|
||||
//accessTokenLock 读写锁 同一个AppID一个
|
||||
accessTokenLock *sync.RWMutex
|
||||
|
||||
//jsAPITicket 读写锁 同一个AppID一个
|
||||
jsAPITicketLock *sync.RWMutex
|
||||
|
||||
//accessTokenFunc 自定义获取 access token 的方法
|
||||
accessTokenFunc GetAccessTokenFunc
|
||||
}
|
||||
|
||||
// Query returns the keyed url query value if it exists
|
||||
func (ctx *Context) Query(key string) string {
|
||||
value, _ := ctx.GetQuery(key)
|
||||
return value
|
||||
}
|
||||
|
||||
// GetQuery is like Query(), it returns the keyed url query value
|
||||
func (ctx *Context) GetQuery(key string) (string, bool) {
|
||||
req := ctx.Request
|
||||
if values, ok := req.URL.Query()[key]; ok && len(values) > 0 {
|
||||
return values[0], true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// SetJsAPITicketLock 设置jsAPITicket的lock
|
||||
func (ctx *Context) SetJsAPITicketLock(lock *sync.RWMutex) {
|
||||
ctx.jsAPITicketLock = lock
|
||||
}
|
||||
|
||||
// GetJsAPITicketLock 获取jsAPITicket 的lock
|
||||
func (ctx *Context) GetJsAPITicketLock() *sync.RWMutex {
|
||||
return ctx.jsAPITicketLock
|
||||
}
|
||||
76
context/qy_access_token.go
Normal file
76
context/qy_access_token.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
//qyAccessTokenURL 获取access_token的接口
|
||||
qyAccessTokenURL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s"
|
||||
)
|
||||
|
||||
//ResQyAccessToken struct
|
||||
type ResQyAccessToken struct {
|
||||
util.CommonError
|
||||
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
//SetQyAccessTokenLock 设置读写锁(一个appID一个读写锁)
|
||||
func (ctx *Context) SetQyAccessTokenLock(l *sync.RWMutex) {
|
||||
ctx.accessTokenLock = l
|
||||
}
|
||||
|
||||
//GetQyAccessToken 获取access_token
|
||||
func (ctx *Context) GetQyAccessToken() (accessToken string, err error) {
|
||||
ctx.accessTokenLock.Lock()
|
||||
defer ctx.accessTokenLock.Unlock()
|
||||
|
||||
accessTokenCacheKey := fmt.Sprintf("qy_access_token_%s", ctx.AppID)
|
||||
val := ctx.Cache.Get(accessTokenCacheKey)
|
||||
if val != nil {
|
||||
accessToken = val.(string)
|
||||
return
|
||||
}
|
||||
|
||||
//从微信服务器获取
|
||||
var resQyAccessToken ResQyAccessToken
|
||||
resQyAccessToken, err = ctx.GetQyAccessTokenFromServer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
accessToken = resQyAccessToken.AccessToken
|
||||
return
|
||||
}
|
||||
|
||||
//GetQyAccessTokenFromServer 强制从微信服务器获取token
|
||||
func (ctx *Context) GetQyAccessTokenFromServer() (resQyAccessToken ResQyAccessToken, err error) {
|
||||
log.Printf("GetQyAccessTokenFromServer")
|
||||
url := fmt.Sprintf(qyAccessTokenURL, ctx.AppID, ctx.AppSecret)
|
||||
var body []byte
|
||||
body, err = util.HTTPGet(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(body, &resQyAccessToken)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if resQyAccessToken.ErrCode != 0 {
|
||||
err = fmt.Errorf("get qy_access_token error : errcode=%v , errormsg=%v", resQyAccessToken.ErrCode, resQyAccessToken.ErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
qyAccessTokenCacheKey := fmt.Sprintf("qy_access_token_%s", ctx.AppID)
|
||||
expires := resQyAccessToken.ExpiresIn - 1500
|
||||
err = ctx.Cache.Set(qyAccessTokenCacheKey, resQyAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
||||
return
|
||||
}
|
||||
43
context/render.go
Normal file
43
context/render.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var xmlContentType = []string{"application/xml; charset=utf-8"}
|
||||
var plainContentType = []string{"text/plain; charset=utf-8"}
|
||||
|
||||
//Render render from bytes
|
||||
func (ctx *Context) Render(bytes []byte) {
|
||||
//debug
|
||||
//fmt.Println("response msg = ", string(bytes))
|
||||
ctx.Writer.WriteHeader(200)
|
||||
_, err := ctx.Writer.Write(bytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
//String render from string
|
||||
func (ctx *Context) String(str string) {
|
||||
writeContextType(ctx.Writer, plainContentType)
|
||||
ctx.Render([]byte(str))
|
||||
}
|
||||
|
||||
//XML render to xml
|
||||
func (ctx *Context) XML(obj interface{}) {
|
||||
writeContextType(ctx.Writer, xmlContentType)
|
||||
bytes, err := xml.Marshal(obj)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ctx.Render(bytes)
|
||||
}
|
||||
|
||||
func writeContextType(w http.ResponseWriter, value []string) {
|
||||
header := w.Header()
|
||||
if val := header["Content-Type"]; len(val) == 0 {
|
||||
header["Content-Type"] = value
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package credential
|
||||
|
||||
// AccessTokenHandle AccessToken 接口
|
||||
type AccessTokenHandle interface {
|
||||
GetAccessToken() (accessToken string, err error)
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
package credential
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/v2/cache"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// AccessTokenURL 获取access_token的接口
|
||||
accessTokenURL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s"
|
||||
// AccessTokenURL 企业微信获取access_token的接口
|
||||
workAccessTokenURL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s"
|
||||
// CacheKeyOfficialAccountPrefix 微信公众号cache key前缀
|
||||
CacheKeyOfficialAccountPrefix = "gowechat_officialaccount_"
|
||||
// CacheKeyMiniProgramPrefix 小程序cache key前缀
|
||||
CacheKeyMiniProgramPrefix = "gowechat_miniprogram_"
|
||||
// CacheKeyWorkPrefix 企业微信cache key前缀
|
||||
CacheKeyWorkPrefix = "gowechat_work_"
|
||||
)
|
||||
|
||||
// DefaultAccessToken 默认AccessToken 获取
|
||||
type DefaultAccessToken struct {
|
||||
appID string
|
||||
appSecret string
|
||||
cacheKeyPrefix string
|
||||
cache cache.Cache
|
||||
accessTokenLock *sync.Mutex
|
||||
}
|
||||
|
||||
// NewDefaultAccessToken new DefaultAccessToken
|
||||
func NewDefaultAccessToken(appID, appSecret, cacheKeyPrefix string, cache cache.Cache) AccessTokenHandle {
|
||||
if cache == nil {
|
||||
panic("cache is ineed")
|
||||
}
|
||||
return &DefaultAccessToken{
|
||||
appID: appID,
|
||||
appSecret: appSecret,
|
||||
cache: cache,
|
||||
cacheKeyPrefix: cacheKeyPrefix,
|
||||
accessTokenLock: new(sync.Mutex),
|
||||
}
|
||||
}
|
||||
|
||||
// ResAccessToken struct
|
||||
type ResAccessToken struct {
|
||||
util.CommonError
|
||||
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
// GetAccessToken 获取access_token,先从cache中获取,没有则从服务端获取
|
||||
func (ak *DefaultAccessToken) GetAccessToken() (accessToken string, err error) {
|
||||
// 先从cache中取
|
||||
accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", ak.cacheKeyPrefix, ak.appID)
|
||||
if val := ak.cache.Get(accessTokenCacheKey); val != nil {
|
||||
return val.(string), nil
|
||||
}
|
||||
|
||||
// 加上lock,是为了防止在并发获取token时,cache刚好失效,导致从微信服务器上获取到不同token
|
||||
ak.accessTokenLock.Lock()
|
||||
defer ak.accessTokenLock.Unlock()
|
||||
|
||||
// 双检,防止重复从微信服务器获取
|
||||
if val := ak.cache.Get(accessTokenCacheKey); val != nil {
|
||||
return val.(string), nil
|
||||
}
|
||||
|
||||
// cache失效,从微信服务器获取
|
||||
var resAccessToken ResAccessToken
|
||||
resAccessToken, err = GetTokenFromServer(fmt.Sprintf(accessTokenURL, ak.appID, ak.appSecret))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
expires := resAccessToken.ExpiresIn - 1500
|
||||
err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
accessToken = resAccessToken.AccessToken
|
||||
return
|
||||
}
|
||||
|
||||
// WorkAccessToken 企业微信AccessToken 获取
|
||||
type WorkAccessToken struct {
|
||||
CorpID string
|
||||
CorpSecret string
|
||||
cacheKeyPrefix string
|
||||
cache cache.Cache
|
||||
accessTokenLock *sync.Mutex
|
||||
}
|
||||
|
||||
// NewWorkAccessToken new WorkAccessToken
|
||||
func NewWorkAccessToken(corpID, corpSecret, cacheKeyPrefix string, cache cache.Cache) AccessTokenHandle {
|
||||
if cache == nil {
|
||||
panic("cache the not exist")
|
||||
}
|
||||
return &WorkAccessToken{
|
||||
CorpID: corpID,
|
||||
CorpSecret: corpSecret,
|
||||
cache: cache,
|
||||
cacheKeyPrefix: cacheKeyPrefix,
|
||||
accessTokenLock: new(sync.Mutex),
|
||||
}
|
||||
}
|
||||
|
||||
// GetAccessToken 企业微信获取access_token,先从cache中获取,没有则从服务端获取
|
||||
func (ak *WorkAccessToken) GetAccessToken() (accessToken string, err error) {
|
||||
// 加上lock,是为了防止在并发获取token时,cache刚好失效,导致从微信服务器上获取到不同token
|
||||
ak.accessTokenLock.Lock()
|
||||
defer ak.accessTokenLock.Unlock()
|
||||
accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", ak.cacheKeyPrefix, ak.CorpID)
|
||||
val := ak.cache.Get(accessTokenCacheKey)
|
||||
if val != nil {
|
||||
accessToken = val.(string)
|
||||
return
|
||||
}
|
||||
|
||||
// cache失效,从微信服务器获取
|
||||
var resAccessToken ResAccessToken
|
||||
resAccessToken, err = GetTokenFromServer(fmt.Sprintf(workAccessTokenURL, ak.CorpID, ak.CorpSecret))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
expires := resAccessToken.ExpiresIn - 1500
|
||||
err = ak.cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
accessToken = resAccessToken.AccessToken
|
||||
return
|
||||
}
|
||||
|
||||
// GetTokenFromServer 强制从微信服务器获取token
|
||||
func GetTokenFromServer(url string) (resAccessToken ResAccessToken, err error) {
|
||||
var body []byte
|
||||
body, err = util.HTTPGet(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(body, &resAccessToken)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if resAccessToken.ErrCode != 0 {
|
||||
err = fmt.Errorf("get access_token error : errcode=%v , errormsg=%v", resAccessToken.ErrCode, resAccessToken.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package credential
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
// TestGetTicketFromServer .
|
||||
func TestGetTicketFromServer(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.New(getTicketURL).Reply(200).JSON(&ResTicket{Ticket: "mock-ticket", ExpiresIn: 10})
|
||||
ticket, err := GetTicketFromServer("arg-ak")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(0), ticket.ErrCode)
|
||||
assert.Equal(t, "mock-ticket", ticket.Ticket, "they should be equal")
|
||||
assert.Equal(t, int64(10), ticket.ExpiresIn, "they should be equal")
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package credential
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/v2/cache"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
// getTicketURL 获取ticket的url
|
||||
const getTicketURL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi"
|
||||
|
||||
// DefaultJsTicket 默认获取js ticket方法
|
||||
type DefaultJsTicket struct {
|
||||
appID string
|
||||
cacheKeyPrefix string
|
||||
cache cache.Cache
|
||||
// jsAPITicket 读写锁 同一个AppID一个
|
||||
jsAPITicketLock *sync.Mutex
|
||||
}
|
||||
|
||||
// NewDefaultJsTicket new
|
||||
func NewDefaultJsTicket(appID string, cacheKeyPrefix string, cache cache.Cache) JsTicketHandle {
|
||||
return &DefaultJsTicket{
|
||||
appID: appID,
|
||||
cache: cache,
|
||||
cacheKeyPrefix: cacheKeyPrefix,
|
||||
jsAPITicketLock: new(sync.Mutex),
|
||||
}
|
||||
}
|
||||
|
||||
// ResTicket 请求jsapi_tikcet返回结果
|
||||
type ResTicket struct {
|
||||
util.CommonError
|
||||
|
||||
Ticket string `json:"ticket"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
// GetTicket 获取jsapi_ticket
|
||||
func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err error) {
|
||||
// 先从cache中取
|
||||
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", js.cacheKeyPrefix, js.appID)
|
||||
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
|
||||
return val.(string), nil
|
||||
}
|
||||
|
||||
js.jsAPITicketLock.Lock()
|
||||
defer js.jsAPITicketLock.Unlock()
|
||||
|
||||
// 双检,防止重复从微信服务器获取
|
||||
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
|
||||
return val.(string), nil
|
||||
}
|
||||
|
||||
var ticket ResTicket
|
||||
ticket, err = GetTicketFromServer(accessToken)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
expires := ticket.ExpiresIn - 1500
|
||||
err = js.cache.Set(jsAPITicketCacheKey, ticket.Ticket, time.Duration(expires)*time.Second)
|
||||
ticketStr = ticket.Ticket
|
||||
return
|
||||
}
|
||||
|
||||
// GetTicketFromServer 从服务器中获取ticket
|
||||
func GetTicketFromServer(accessToken string) (ticket ResTicket, err error) {
|
||||
var response []byte
|
||||
url := fmt.Sprintf(getTicketURL, accessToken)
|
||||
response, err = util.HTTPGet(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(response, &ticket)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if ticket.ErrCode != 0 {
|
||||
err = fmt.Errorf("getTicket Error : errcode=%d , errmsg=%s", ticket.ErrCode, ticket.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package credential
|
||||
|
||||
// JsTicketHandle js ticket获取
|
||||
type JsTicketHandle interface {
|
||||
// GetTicket 获取ticket
|
||||
GetTicket(accessToken string) (ticket string, err error)
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
// Package device 设备相关接口
|
||||
package device
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -23,13 +22,13 @@ type reqDeviceAuthorize struct {
|
||||
// 请求操作的类型,限定取值为: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 字段。
|
||||
//当 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 设备授权实体
|
||||
//ReqDevice 设备授权实体
|
||||
type ReqDevice struct {
|
||||
// 设备的 device id
|
||||
ID string `json:"id"`
|
||||
@@ -42,22 +41,22 @@ type ReqDevice struct {
|
||||
// ble – 3
|
||||
// wifi -- 4
|
||||
// 一个设备可以支持多种连接类型,用符号"|"做分割,客户端优先选择靠前的连接方式(优先级按|关系的排序依次降低),举例:
|
||||
// 1:表示设备仅支持andiod classic bluetooth 1|2:表示设备支持android 和ios 两种classic bluetooth,但是客户端优先选择android classic bluetooth 协议,如果android classic bluetooth协议连接失败,再选择ios classic bluetooth协议进行连接
|
||||
// 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
|
||||
//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置位)处于非公众号页面(如主界面等),微信自动连接。当用户切换微信到前台时,可能尝试去连接设备,连上后一定时间会断开
|
||||
//连接策略,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地址在厂商广播manufacture data里含有mac地址的偏移,取值如下:
|
||||
// 表示mac地址在厂商广播manufature data里含有mac地址的偏移,取值如下:
|
||||
// -1:在尾部、
|
||||
// -2:表示不包含mac地址 其他:非法偏移
|
||||
ManuMacPos string `json:"manu_mac_pos"`
|
||||
@@ -69,7 +68,7 @@ type ReqDevice struct {
|
||||
BleSimpleProtocol string `json:"ble_simple_protocol,omitempty"`
|
||||
}
|
||||
|
||||
// ResBaseInfo 授权回调实体
|
||||
//ResBaseInfo 授权回调实体
|
||||
type ResBaseInfo struct {
|
||||
BaseInfo struct {
|
||||
DeviceType string `json:"device_type"`
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
// ReqBind 设备绑定解绑共通实体
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -19,12 +19,12 @@ const (
|
||||
uriState = "https://api.weixin.qq.com/device/get_stat"
|
||||
)
|
||||
|
||||
// Device struct
|
||||
//Device struct
|
||||
type Device struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewDevice 实例
|
||||
//NewDevice 实例
|
||||
func NewDevice(context *context.Context) *Device {
|
||||
device := new(Device)
|
||||
device.Context = context
|
||||
@@ -1,6 +1,6 @@
|
||||
package device
|
||||
|
||||
// MsgDevice 设备消息响应
|
||||
//MsgDevice 设备消息响应
|
||||
type MsgDevice struct {
|
||||
DeviceType string
|
||||
DeviceID string
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
// ResCreateQRCode 获取二维码的返回实体
|
||||
//ResCreateQRCode 获取二维码的返回实体
|
||||
type ResCreateQRCode struct {
|
||||
util.CommonError
|
||||
DeviceNum int `json:"device_num"`
|
||||
@@ -42,7 +42,7 @@ func (d *Device) CreateQRCode(devices []string) (res ResCreateQRCode, err error)
|
||||
return
|
||||
}
|
||||
|
||||
// ResVerifyQRCode 验证授权结果实体
|
||||
//ResVerifyQRCode 验证授权结果实体
|
||||
type ResVerifyQRCode struct {
|
||||
util.CommonError
|
||||
DeviceType string `json:"device_type"`
|
||||
@@ -60,7 +60,7 @@ func (d *Device) VerifyQRCode(ticket string) (res ResVerifyQRCode, err error) {
|
||||
req := map[string]interface{}{
|
||||
"ticket": ticket,
|
||||
}
|
||||
|
||||
fmt.Println(req)
|
||||
var response []byte
|
||||
if response, err = util.PostJSON(uri, req); err != nil {
|
||||
return
|
||||
23
doc.go
23
doc.go
@@ -3,6 +3,29 @@ Package wechat provide wechat sdk for go
|
||||
|
||||
使用Golang开发的微信SDK,简单、易用。
|
||||
|
||||
以下是一个处理消息接收以及回复的例子:
|
||||
|
||||
//配置微信参数
|
||||
config := &wechat.Config{
|
||||
AppID: "xxxx",
|
||||
AppSecret: "xxxx",
|
||||
Token: "xxxx",
|
||||
EncodingAESKey: "xxxx",
|
||||
}
|
||||
wc := wechat.NewWechat(config)
|
||||
|
||||
// 传入request和responseWriter
|
||||
server := wc.GetServer(request, responseWriter)
|
||||
server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {
|
||||
|
||||
//回复消息:演示回复用户发送的消息
|
||||
text := message.NewText(msg.Content)
|
||||
return &message.Reply{message.MsgText, text}
|
||||
})
|
||||
|
||||
server.Serve()
|
||||
server.Send()
|
||||
|
||||
|
||||
更多信息:https://github.com/silenceper/wechat
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# API 文档
|
||||
|
||||
已完成以及未完成API列表汇总
|
||||
|
||||
如果有兴趣参与贡献,可以在具体的API表格后面标识自己为贡献者以及完成时间,例如:
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |贡献者|完成时间|
|
||||
| :---------------------: | -------- | :------------------------- | ---------- | -------- |-------- |-------- |
|
||||
| 获取公众号类目 | GET | /wxaapi/newtmpl/getcategory | NO | |silenceper| 2021-12-20|
|
||||
|
||||
- [微信公众号](./officialaccount.md)
|
||||
- [小程序](./miniprogram.md)
|
||||
- [小游戏](./minigame.md)
|
||||
- [开放平台](./oplatform.md)
|
||||
- [微信支付](./wxpay.md)
|
||||
- [企业微信](./work.md)
|
||||
- [智能对话](./aispeech.md)
|
||||
@@ -1,3 +0,0 @@
|
||||
# 智能对话
|
||||
|
||||
TODO
|
||||
@@ -1,3 +0,0 @@
|
||||
# 小游戏
|
||||
|
||||
TODO
|
||||
@@ -1,50 +0,0 @@
|
||||
# 小程序
|
||||
|
||||
## 基础接口
|
||||
|
||||
TODO
|
||||
|
||||
## 内容安全
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.mediaCheckAsync.html)
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |
|
||||
| :-------------------------------: | -------- | :------------------------------- | ---------- | -------------------------------------- |
|
||||
| 异步校验图片/音频 <sub>v1.0</sub> | POST | /wxa/media_check_async | YES | (security *Security) MediaCheckAsyncV1 |
|
||||
| 同步校验一张图片 <sub>v1.0</sub> | POST | /wxa/img_sec_check | YES | (security *Security) ImageCheckV1 |
|
||||
| 异步校验图片/音频 | POST | /wxa/media_check_async?version=2 | YES | (security *Security) MediaCheckAsync |
|
||||
| 同步检查一段文本 <sub>v1.0</sub> | POST | /wxa/msg_sec_check | YES | (security *Security) MsgCheckV1 |
|
||||
| 同步检查一段文本 | POST | /wxa/msg_sec_check?version=2 | YES | (security *Security) MsgCheck |
|
||||
|
||||
|
||||
## OCR
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/ocr/ocr.bankcard.html)
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |
|
||||
| :------------: | -------- | :--------------------- | ---------- | -------- |
|
||||
| 银行卡识别 | POST | /cv/ocr/bankcard | | |
|
||||
| 营业执照识别 | POST | /cv/ocr/bizlicense | | |
|
||||
| 驾驶证识别 | POST | /cv/ocr/drivinglicense | | |
|
||||
| 身份证识别 | POST | /cv/ocr/idcard | | |
|
||||
| 通用印刷体识别 | POST | /cv/ocr/comm | | |
|
||||
| 行驶证识别 | POST | /cv/ocr/driving | | |
|
||||
|
||||
|
||||
## 手机号
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/phonenumber/phonenumber.getPhoneNumber.html)
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |
|
||||
| :----------------: | -------- | :------------------------------- | ---------- | ----------------------------------- |
|
||||
| code换取用户手机号 | POST | /wxa/business/getuserphonenumber | YES | (business *Business) GetPhoneNumber |
|
||||
|
||||
|
||||
## 安全风控
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/safety-control-capability/riskControl.getUserRiskRank.html)
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |
|
||||
| :----------------: | -------- | :------------------- | ---------- | ------------------------------------------ |
|
||||
| 获取用户的安全等级 | POST | /wxa/getuserriskrank | YES | (riskControl *RiskControl) GetUserRiskRank |
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
# 微信公众号API列表
|
||||
|
||||
## 基础接口
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html)
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |
|
||||
| :---------------------: | -------- | :------------------------- | ---------- | -------- |
|
||||
| 获取Access token | GET | /cgi-bin/token | YES | |
|
||||
| 获取微信服务器IP地址 | GET | /cgi-bin/get_api_domain_ip | YES | |
|
||||
| 获取微信callback IP地址 | GET | /cgi-bin/getcallbackip | YES | |
|
||||
| 清理接口调用次数 | POST | /cgi-bin/clear_quota | YES | |
|
||||
|
||||
## 订阅通知
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Subscription_Messages/api.html)
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |
|
||||
| -------------------- | -------- | -------------------------------------- | ---------- | ----------------------- |
|
||||
| 选用模板 | POST | /wxaapi/newtmpl/addtemplate | YES | (tpl *Subscribe) Add |
|
||||
| 删除模板 | POST | /wxaapi/newtmpl/deltemplate | YES | (tpl *Subscribe) Delete |
|
||||
| 获取公众号类目 | GET | /wxaapi/newtmpl/getcategory | NO | |
|
||||
| 获取模板中的关键词 | GET | /wxaapi/newtmpl/getpubtemplatekeywords | NO | |
|
||||
| 获取类目下的公共模板 | GET | /wxaapi/newtmpl/getpubtemplatetitles | NO | |
|
||||
| 获取私有模板列表 | GET | /wxaapi/newtmpl/gettemplate | YES | (tpl *Subscribe) List() |
|
||||
| 发送订阅通知 | POST | /cgi-bin/message/subscribe/bizsend | YES | (tpl *Subscribe) Send |
|
||||
|
||||
## 客服消息
|
||||
|
||||
### PC 客服能力
|
||||
|
||||
#### 客服管理
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Customer_Service/Customer_Service_Management.html)
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |
|
||||
| ---------------- | --------- | -------------------------------------- | ---------- | -------- |
|
||||
| 获取客服基本信息 | GET | /cgi-bin/customservice/getkflist | NO | |
|
||||
| 添加客服帐号 | POST | /customservice/kfaccount/add | NO | |
|
||||
| 邀请绑定客服帐号 | POST | /customservice/kfaccount/inviteworker | NO | |
|
||||
| 设置客服信息 | POST | /customservice/kfaccount/update | NO | |
|
||||
| 上传客服头像 | POST/FORM | /customservice/kfaccount/uploadheadimg | NO | |
|
||||
| 删除客服帐号 | GET | /customservice/kfaccount/del | NO | |
|
||||
|
||||
#### 会话控制
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Customer_Service/Session_control.html)
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |
|
||||
| ------------------ | -------- | --------------------------------------- | ---------- | -------- |
|
||||
| 创建会话 | POST | /customservice/kfsession/create | NO | |
|
||||
| 获取客户会话状态 | GET | /customservice/kfsession/getsession | NO | |
|
||||
| 获取客服会话列表 | GET | /customservice/kfsession/getsessionlist | NO | |
|
||||
| 获取未接入会话列表 | POST | /customservice/kfsession/getwaitcase | NO | |
|
||||
|
||||
#### 获取聊天记录
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Customer_Service/Obtain_chat_transcript.html)
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |
|
||||
| ------------ | -------- | ----------------------------------- | ---------- | -------- |
|
||||
| 获取聊天记录 | POST | /customservice/msgrecord/getmsglist | NO | |
|
||||
|
||||
### 对话能力
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Shopping_Guide/guide.html)
|
||||
|
||||
#### 顾问管理
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |
|
||||
| ------------------------------ | -------- | -------------------------------------- | ---------- | -------- |
|
||||
| 添加顾问 | POST | /cgi-bin/guide/addguideacct | NO | |
|
||||
| 获取顾问信息 | POST | /cgi-bin/guide/getguideacct | NO | |
|
||||
| 修改顾问信息 | POST | /cgi-bin/guide/updateguideacct | NO | |
|
||||
| 删除顾问 | POST | /cgi-bin/guide/delguideacct | NO | |
|
||||
| 获取服务号顾问列表 | POST | /cgi-bin/guide/getguideacctlist | NO | |
|
||||
| 生成顾问二维码 | POST | /cgi-bin/guide/guidecreateqrcode | NO | |
|
||||
| 获取顾问聊天记录 | POST | /cgi-bin/guide/getguidebuyerchatrecord | NO | |
|
||||
| 设置快捷回复与关注自动回复 | POST | /cgi-bin/guide/setguideconfig | NO | |
|
||||
| 获取快捷回复与关注自动回复 | POST | /cgi-bin/guide/getguideconfig | NO | |
|
||||
| 设置敏感词与离线自动回复 | POST | /cgi-bin/guide/setguideacctconfig | NO | |
|
||||
| 获取离线自动回复与敏感词 | POST | /cgi-bin/guide/getguideacctconfig | NO | |
|
||||
| 允许微信用户复制小程序页面路径 | POST | /cgi-bin/guide/pushshowwxapathmenu | NO | |
|
||||
| 新建顾问分组 | POST | /cgi-bin/guide/newguidegroup | NO | |
|
||||
| 获取顾问分组列表 | POST | /cgi-bin/guide/getguidegrouplist | NO | |
|
||||
| 获取顾问分组信息 | POST | /cgi-bin/guide/getgroupinfo | NO | |
|
||||
| 分组内添加顾问 | POST | /cgi-bin/guide/addguide2guidegroup | NO | |
|
||||
| 分组内删除顾问 | POST | /cgi-bin/guide/delguide2guidegroup | NO | |
|
||||
| 获取顾问所在分组 | POST | /cgi-bin/guide/getgroupbyguide | NO | |
|
||||
| 删除指定顾问分组 | POST | /cgi-bin/guide/delguidegroup | NO | |
|
||||
|
||||
#### 客户管理
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |
|
||||
| ------------------------ | -------- | ------------------------------------------- | ---------- | -------- |
|
||||
| 为顾问分配客户 | POST | /cgi-bin/guide/addguidebuyerrelation | NO | |
|
||||
| 为顾问移除客户 | POST | /cgi-bin/guide/delguidebuyerrelation | NO | |
|
||||
| 获取顾问的客户列表 | POST | /cgi-bin/guide/getguidebuyerrelationlist | NO | |
|
||||
| 为客户更换顾问 | POST | /cgi-bin/guide/rebindguideacctforbuyer | NO | |
|
||||
| 修改客户昵称 | POST | /cgi-bin/guide/updateguidebuyerrelation | NO | |
|
||||
| 查询客户所属顾问 | POST | /cgi-bin/guide/getguidebuyerrelationbybuyer | NO | |
|
||||
| 查询指定顾问和客户的关系 | POST | /cgi-bin/guide/getguidebuyerrelation | NO | |
|
||||
|
||||
#### 标签管理
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |
|
||||
| ------------------ | -------- | -------------------------------------- | ---------- | -------- |
|
||||
| 新建标签类型 | POST | /cgi-bin/guide/newguidetagoption | NO | |
|
||||
| 删除标签类型 | POST | /cgi-bin/guide/delguidetagoption | NO | |
|
||||
| 为标签添加可选值 | POST | /cgi-bin/guide/addguidetagoption | NO | |
|
||||
| 获取标签和可选值 | POST | /cgi-bin/guide/getguidetagoption | NO | |
|
||||
| 为客户设置标签 | POST | /cgi-bin/guide/addguidebuyertag | NO | |
|
||||
| 查询客户标签 | POST | /cgi-bin/guide/getguidebuyertag | NO | |
|
||||
| 根据标签值筛选客户 | POST | /cgi-bin/guide/queryguidebuyerbytag | NO | |
|
||||
| 删除客户标签 | POST | /cgi-bin/guide/delguidebuyertag | NO | |
|
||||
| 设置自定义客户信息 | POST | /cgi-bin/guide/addguidebuyerdisplaytag | NO | |
|
||||
| 获取自定义客户信息 | POST | /cgi-bin/guide/getguidebuyerdisplaytag | NO | |
|
||||
|
||||
#### 素材管理
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |
|
||||
| ------------------ | -------- | ------------------------------------ | ---------- | -------- |
|
||||
| 添加小程序卡片素材 | POST | /cgi-bin/guide/setguidecardmaterial | NO | |
|
||||
| 查询小程序卡片素材 | POST | /cgi-bin/guide/getguidecardmaterial | NO | |
|
||||
| 删除小程序卡片素材 | POST | /cgi-bin/guide/delguidecardmaterial | NO | |
|
||||
| 添加图片素材 | POST | /cgi-bin/guide/setguideimagematerial | NO | |
|
||||
| 查询图片素材 | POST | /cgi-bin/guide/getguideimagematerial | NO | |
|
||||
| 删除图片素材 | POST | /cgi-bin/guide/delguideimagematerial | NO | |
|
||||
| 添加文字素材 | POST | /cgi-bin/guide/setguidewordmaterial | NO | |
|
||||
| 查询文字素材 | POST | /cgi-bin/guide/getguidewordmaterial | NO | |
|
||||
| 删除文字素材 | POST | /cgi-bin/guide/delguidewordmaterial | NO | |
|
||||
|
||||
#### 群发任务管理
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Shopping_Guide/task-account/shopping-guide.addGuideMassendJob.html)
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |
|
||||
| -------------------- | -------- | ------------------------------------- | ---------- | -------- |
|
||||
| 添加群发任务 | POST | /cgi-bin/guide/addguidemassendjob | NO | |
|
||||
| 获取群发任务列表 | POST | /cgi-bin/guide/getguidemassendjoblist | NO | |
|
||||
| 获取指定群发任务信息 | POST | /cgi-bin/guide/getguidemassendjob | NO | |
|
||||
| 修改群发任务 | POST | /cgi-bin/guide/updateguidemassendjob | NO | |
|
||||
| 取消群发任务 | POST | /cgi-bin/guide/cancelguidemassendjob | NO | |
|
||||
|
||||
## 微信网页开发
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html)
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |
|
||||
| ------------------------------------------------------------ | -------- | --------------------------------------------------- | ---------- | ----------------------------------- |
|
||||
| 获取跳转的url地址 | GET | https://open.weixin.qq.com/connect/oauth2/authorize | YES | (oauth *Oauth) GetRedirectURL |
|
||||
| 获取网页应用跳转的url地址 | GET | https://open.weixin.qq.com/connect/qrconnect | YES | (oauth *Oauth) GetWebAppRedirectURL |
|
||||
| 通过网页授权的code 换取access_token(区别于context中的access_token) | GET | /sns/oauth2/access_token | YES | (oauth *Oauth) GetUserAccessToken |
|
||||
| 刷新access_token | GET | /sns/oauth2/refresh_token? | YES | (oauth *Oauth) RefreshAccessToken |
|
||||
| 检验access_token是否有效 | GET | /sns/auth | YES | (oauth *Oauth) CheckAccessToken( |
|
||||
| 拉取用户信息(需scope为 snsapi_userinfo) | GET | /sns/userinfo | YES | (oauth *Oauth) GetUserInfo |
|
||||
| 获取jssdk需要的配置参数 | GET | /cgi-bin/ticket/getticket | YES | (js *Js) GetConfig |
|
||||
|
||||
## 素材管理
|
||||
|
||||
## 草稿箱
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Draft_Box/Add_draft.html)
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |
|
||||
| -------------------------- | -------- | ------------------------------------------------------------ | ---------- | ---------------------------- |
|
||||
| 新建草稿 | POST | /cgi-bin/draft/add | YES | (draft *Draft) AddDraft |
|
||||
| 获取草稿 | POST | /cgi-bin/draft/get | YES | (draft *Draft) GetDraft |
|
||||
| 删除草稿 | POST | /cgi-bin/draft/delete | YES | (draft *Draft) DeleteDraft |
|
||||
| 修改草稿 | POST | /cgi-bin/draft/update | YES | (draft *Draft) UpdateDraft |
|
||||
| 获取草稿总数 | GET | /cgi-bin/draft/count | YES | (draft *Draft) CountDraft |
|
||||
| 获取草稿列表 | POST | /cgi-bin/draft/batchget | YES | (draft *Draft) PaginateDraft |
|
||||
| MP端开关(仅内测期间使用) | POST | /cgi-bin/draft/switch<br />/cgi-bin/draft/switch?checkonly=1 | NO | |
|
||||
|
||||
|
||||
|
||||
## 发布能力
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Publish/Publish.html)
|
||||
|
||||
说明:「发表记录」包括群发和发布。
|
||||
|
||||
注意:该接口,只能处理 "发布" 相关的信息,无法操作和获取 "群发" 相关内容!
|
||||
|
||||
- 群发:主动推送给粉丝,历史消息可看,被搜一搜收录,可以限定部分的粉丝接收到。
|
||||
- 发布:不会主动推给粉丝,历史消息列表看不到,但是是公开给所有人的文章。也不会占用群发的次数。每天可以发布多篇内容。可以用于自动回复、自定义菜单、页面模板和话题中,发布成功时会生成一个永久链接。
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |
|
||||
| ------------------------------ | -------- | ------------------------------- | ---------- | --------------------------------------- |
|
||||
| 发布接口 | POST | /cgi-bin/freepublish/submit | YES | (freePublish *FreePublish) Publish |
|
||||
| 发布状态轮询接口 | POST | /cgi-bin/freepublish/get | YES | (freePublish *FreePublish) SelectStatus |
|
||||
| 事件推送发布结果 | | | YES | EventPublishJobFinish |
|
||||
| 删除发布 | POST | /cgi-bin/freepublish/delete | YES | (freePublish *FreePublish) Delete |
|
||||
| 通过 article_id 获取已发布文章 | POST | /cgi-bin/freepublish/getarticle | YES | (freePublish *FreePublish) First |
|
||||
| 获取成功发布列表 | POST | /cgi-bin/freepublish/batchget | YES | (freePublish *FreePublish) Paginate |
|
||||
|
||||
|
||||
## 图文消息留言管理
|
||||
|
||||
## 用户管理
|
||||
|
||||
## 账号管理
|
||||
|
||||
## 数据统计
|
||||
|
||||
## 微信卡券
|
||||
|
||||
## 微信门店
|
||||
|
||||
## 智能接口
|
||||
|
||||
## 微信设备功能
|
||||
|
||||
## 微信“一物一码”
|
||||
|
||||
## 微信发票
|
||||
|
||||
## 微信非税缴费
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# 开放平台
|
||||
@@ -1,65 +0,0 @@
|
||||
# 企业微信
|
||||
|
||||
host: https://qyapi.weixin.qq.com/
|
||||
|
||||
## 微信客服
|
||||
|
||||
[官方文档](https://work.weixin.qq.com/api/doc/90000/90135/94638)
|
||||
|
||||
### 客服账号管理
|
||||
|
||||
[官方文档](https://open.work.weixin.qq.com/api/doc/90001/90143/94684)
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |贡献者 |
|
||||
| :--------------: | -------- | :-------------------------- | ---------- | -------------------------------|------------|
|
||||
| 添加客服帐号 | POST | /cgi-bin/kf/account/add | YES | (r *Client) AccountAdd | NICEXAI |
|
||||
| 删除客服帐号 | POST | /cgi-bin/kf/account/del | YES | (r *Client) AccountDel | NICEXAI |
|
||||
| 修改客服帐号 | POST | /cgi-bin/kf/account/update | YES | (r *Client) AccountUpdate | NICEXAI |
|
||||
| 获取客服帐号列表 | GET | /cgi-bin/kf/account/list | YES | (r *Client) AccountList | NICEXAI |
|
||||
| 获取客服帐号链接 | GET | /cgi-bin/kf/add_contact_way | YES | (r *Client) AddContactWay | NICEXAI |
|
||||
|
||||
### 接待人员列表
|
||||
|
||||
[官方文档](https://open.work.weixin.qq.com/api/doc/90001/90143/94693)
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |贡献者 |
|
||||
| :--------------: | -------- | :-------------------------- | ---------- | -------------------------------|------------|
|
||||
| 添加接待人员 | POST | /cgi-bin/kf/servicer/add | YES | (r *Client) ReceptionistAdd | NICEXAI |
|
||||
| 删除接待人员 | POST | /cgi-bin/kf/servicer/del | YES | (r *Client) ReceptionistDel | NICEXAI |
|
||||
| 获取接待人员列表 | GET | /cgi-bin/kf/servicer/list | YES | (r *Client) ReceptionistList | NICEXAI |
|
||||
|
||||
### 会话分配与消息收发
|
||||
|
||||
[官方文档](https://open.work.weixin.qq.com/api/doc/90001/90143/94694)
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |贡献者 |
|
||||
| :--------------: | -------- | :-------------------------------| ---------- | ------------------------------- |------------|
|
||||
| 获取会话状态 | POST | /cgi-bin/kf/service_state/get | YES | (r *Client) ServiceStateGet | NICEXAI |
|
||||
| 变更会话状态 | POST | /cgi-bin/kf/service_state/trans | YES | (r *Client) ServiceStateTrans | NICEXAI |
|
||||
| 读取消息 | POST | /cgi-bin/kf/sync_msg | YES | (r *Client) SyncMsg | NICEXAI |
|
||||
| 发送消息 | POST | /cgi-bin/kf/send_msg | YES | (r *Client) SendMsg | NICEXAI |
|
||||
| 发送事件响应消息 | POST | /cgi-bin/kf/send_msg_on_event | YES | (r *Client) SendMsgOnEvent | NICEXAI |
|
||||
|
||||
### 升级服务配置
|
||||
|
||||
[官方文档](https://open.work.weixin.qq.com/api/doc/90001/90143/94702)
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 |贡献者 |
|
||||
| :--------------: | -------- | :-------------------------------------------------| ---------- | ------------------------------- |------------|
|
||||
| 获取配置的专员与客户群 | POST | /cgi-bin/kf/customer/get_upgrade_service_config | YES | (r *Client) UpgradeServiceConfig | NICEXAI |
|
||||
| 为客户升级为专员或客户群服务 | POST | /cgi-bin/kf/customer/upgrade_service | YES | (r *Client) UpgradeService | NICEXAI |
|
||||
| 为客户取消推荐 | POST | /cgi-bin/kf/customer/cancel_upgrade_service | YES | (r *Client) UpgradeServiceCancel | NICEXAI |
|
||||
|
||||
### 其他基础信息获取
|
||||
|
||||
[官方文档](https://open.work.weixin.qq.com/api/doc/90001/90143/95148)
|
||||
|
||||
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | 贡献者 |
|
||||
| :--------------: | -------- | :---------------------------------------| ---------- | ------------------------------- |------------|
|
||||
| 获取客户基础信息 | POST | /cgi-bin/kf/customer/batchget | YES | (r *Client) CustomerBatchGet | NICEXAI |
|
||||
| 获取视频号绑定状态 | GET | /cgi-bin/kf/get_corp_qualification | YES | (r *Client) GetCorpQualification | NICEXAI |
|
||||
|
||||
## 应用管理
|
||||
|
||||
TODO
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# 微信支付
|
||||
|
||||
TODO
|
||||
45
examples/beego/beego.go
Normal file
45
examples/beego/beego.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
"github.com/astaxie/beego/context"
|
||||
"github.com/silenceper/wechat"
|
||||
"github.com/silenceper/wechat/message"
|
||||
)
|
||||
|
||||
func hello(ctx *context.Context) {
|
||||
//配置微信参数
|
||||
config := &wechat.Config{
|
||||
AppID: "your app id",
|
||||
AppSecret: "your app secret",
|
||||
Token: "your token",
|
||||
EncodingAESKey: "your encoding aes key",
|
||||
}
|
||||
wc := wechat.NewWechat(config)
|
||||
|
||||
// 传入request和responseWriter
|
||||
server := wc.GetServer(ctx.Request, ctx.ResponseWriter)
|
||||
//设置接收消息的处理方法
|
||||
server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {
|
||||
|
||||
//回复消息:演示回复用户发送的消息
|
||||
text := message.NewText(msg.Content)
|
||||
return &message.Reply{MsgType: message.MsgTypeText, MsgData: text}
|
||||
})
|
||||
|
||||
//处理消息接收以及回复
|
||||
err := server.Serve()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
//发送回复的消息
|
||||
server.Send()
|
||||
}
|
||||
|
||||
func main() {
|
||||
beego.Any("/", hello)
|
||||
beego.Run(":8001")
|
||||
}
|
||||
47
examples/gin/gin.go
Normal file
47
examples/gin/gin.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/silenceper/wechat"
|
||||
"github.com/silenceper/wechat/message"
|
||||
)
|
||||
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
|
||||
router.Any("/", hello)
|
||||
router.Run(":8001")
|
||||
}
|
||||
|
||||
func hello(c *gin.Context) {
|
||||
|
||||
//配置微信参数
|
||||
config := &wechat.Config{
|
||||
AppID: "your app id",
|
||||
AppSecret: "your app secret",
|
||||
Token: "your token",
|
||||
EncodingAESKey: "your encoding aes key",
|
||||
}
|
||||
wc := wechat.NewWechat(config)
|
||||
|
||||
// 传入request和responseWriter
|
||||
server := wc.GetServer(c.Request, c.Writer)
|
||||
//设置接收消息的处理方法
|
||||
server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {
|
||||
|
||||
//回复消息:演示回复用户发送的消息
|
||||
text := message.NewText(msg.Content)
|
||||
return &message.Reply{MsgType: message.MsgTypeText, MsgData: text}
|
||||
})
|
||||
|
||||
//处理消息接收以及回复
|
||||
err := server.Serve()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
//发送回复的消息
|
||||
server.Send()
|
||||
}
|
||||
48
examples/http/http.go
Normal file
48
examples/http/http.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/silenceper/wechat"
|
||||
"github.com/silenceper/wechat/message"
|
||||
)
|
||||
|
||||
func hello(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
//配置微信参数
|
||||
config := &wechat.Config{
|
||||
AppID: "your app id",
|
||||
AppSecret: "your app secret",
|
||||
Token: "your token",
|
||||
EncodingAESKey: "your encoding aes key",
|
||||
}
|
||||
wc := wechat.NewWechat(config)
|
||||
|
||||
// 传入request和responseWriter
|
||||
server := wc.GetServer(req, rw)
|
||||
//设置接收消息的处理方法
|
||||
server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {
|
||||
|
||||
//回复消息:演示回复用户发送的消息
|
||||
text := message.NewText(msg.Content)
|
||||
return &message.Reply{MsgType: message.MsgTypeText, MsgData: text}
|
||||
})
|
||||
|
||||
//处理消息接收以及回复
|
||||
err := server.Serve()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
//发送回复的消息
|
||||
server.Send()
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", hello)
|
||||
err := http.ListenAndServe(":8001", nil)
|
||||
if err != nil {
|
||||
fmt.Printf("start server error , err=%v", err)
|
||||
}
|
||||
}
|
||||
28
go.mod
28
go.mod
@@ -1,15 +1,23 @@
|
||||
module github.com/silenceper/wechat/v2
|
||||
module github.com/silenceper/wechat
|
||||
|
||||
go 1.14
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d
|
||||
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/go-redis/redis/v8 v8.11.6-0.20220405070650-99c79f7041fc
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spf13/cast v1.4.1
|
||||
github.com/stretchr/testify v1.7.1
|
||||
github.com/tidwall/gjson v1.14.1
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
|
||||
gopkg.in/h2non/gock.v1 v1.1.2
|
||||
github.com/gin-gonic/gin v1.1.4
|
||||
github.com/golang/protobuf v0.0.0-20161117033126-8ee79997227b // indirect
|
||||
github.com/gomodule/redigo v2.0.1-0.20180627144507-2cd21d9966bf+incompatible
|
||||
github.com/kr/pretty v0.1.0
|
||||
github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 // indirect
|
||||
github.com/mattn/go-isatty v0.0.0-20161123143637-30a891c33c7c // indirect
|
||||
github.com/spf13/cast v1.3.1
|
||||
github.com/stretchr/testify v1.4.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
|
||||
golang.org/x/net v0.0.0-20191125084936-ffdde1057850 // indirect
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
||||
gopkg.in/go-playground/validator.v8 v8.18.1 // indirect
|
||||
)
|
||||
|
||||
171
go.sum
171
go.sum
@@ -1,147 +1,50 @@
|
||||
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/go-redis/redis/v8 v8.11.6-0.20220405070650-99c79f7041fc h1:jZY+lpZB92nvBo2f31oPC/ivGll6NcsnEOORm8Fkr4M=
|
||||
github.com/go-redis/redis/v8 v8.11.6-0.20220405070650-99c79f7041fc/go.mod h1:25mL1NKxbJhB63ihiK8MnNeTRd+xAizd6bOdydrTLUQ=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.1.3 h1:e/3Cwtogj0HA+25nMP1jCMDIf8RtRYbGwGGuBIFztkc=
|
||||
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/gin-gonic/gin v1.1.4 h1:XLaCFbU39SSGRQrEeP7Z7mM3lvRqC4vE5tEaVdLDdSE=
|
||||
github.com/gin-gonic/gin v1.1.4/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
|
||||
github.com/golang/protobuf v0.0.0-20161117033126-8ee79997227b h1:fE/yi9pibxGEc0gSJuEShcsBXE2d5FW3OudsjE9tKzQ=
|
||||
github.com/golang/protobuf v0.0.0-20161117033126-8ee79997227b/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gomodule/redigo v2.0.1-0.20180627144507-2cd21d9966bf+incompatible h1:QJ4V3LjaRe/6NKoaaj2QzQZcezt5gNXdPv0axxS4VNA=
|
||||
github.com/gomodule/redigo v2.0.1-0.20180627144507-2cd21d9966bf+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 h1:ykXz+pRRTibcSjG1yRhpdSHInF8yZY/mfn+Rz2Nd1rE=
|
||||
github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739/go.mod h1:zUx1mhth20V3VKgL5jbd1BSQcW4Fy6Qs4PZvQwRFwzM=
|
||||
github.com/mattn/go-isatty v0.0.0-20161123143637-30a891c33c7c h1:YHHK/dEmr2Jo1cWD1VMB2waEeHJhHFp3CEylwWy/VcY=
|
||||
github.com/mattn/go-isatty v0.0.0-20161123143637-30a891c33c7c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
|
||||
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
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=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
|
||||
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
|
||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
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=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
109
js/js.go
Normal file
109
js/js.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package js
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const getTicketURL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi"
|
||||
|
||||
// Js struct
|
||||
type Js struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// Config 返回给用户jssdk配置信息
|
||||
type Config struct {
|
||||
AppID string `json:"app_id"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
NonceStr string `json:"nonce_str"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
// resTicket 请求jsapi_tikcet返回结果
|
||||
type resTicket struct {
|
||||
util.CommonError
|
||||
|
||||
Ticket string `json:"ticket"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
//NewJs init
|
||||
func NewJs(context *context.Context) *Js {
|
||||
js := new(Js)
|
||||
js.Context = context
|
||||
return js
|
||||
}
|
||||
|
||||
//GetConfig 获取jssdk需要的配置参数
|
||||
//uri 为当前网页地址
|
||||
func (js *Js) GetConfig(uri string) (config *Config, err error) {
|
||||
config = new(Config)
|
||||
var ticketStr string
|
||||
ticketStr, err = js.GetTicket()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
nonceStr := util.RandomStr(16)
|
||||
timestamp := util.GetCurrTs()
|
||||
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
||||
sigStr := util.Signature(str)
|
||||
|
||||
config.AppID = js.AppID
|
||||
config.NonceStr = nonceStr
|
||||
config.Timestamp = timestamp
|
||||
config.Signature = sigStr
|
||||
return
|
||||
}
|
||||
|
||||
//GetTicket 获取jsapi_ticket
|
||||
func (js *Js) GetTicket() (ticketStr string, err error) {
|
||||
js.GetJsAPITicketLock().Lock()
|
||||
defer js.GetJsAPITicketLock().Unlock()
|
||||
|
||||
//先从cache中取
|
||||
jsAPITicketCacheKey := fmt.Sprintf("jsapi_ticket_%s", js.AppID)
|
||||
val := js.Cache.Get(jsAPITicketCacheKey)
|
||||
if val != nil {
|
||||
ticketStr = val.(string)
|
||||
return
|
||||
}
|
||||
var ticket resTicket
|
||||
ticket, err = js.getTicketFromServer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ticketStr = ticket.Ticket
|
||||
return
|
||||
}
|
||||
|
||||
//getTicketFromServer 强制从服务器中获取ticket
|
||||
func (js *Js) getTicketFromServer() (ticket resTicket, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = js.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var response []byte
|
||||
url := fmt.Sprintf(getTicketURL, accessToken)
|
||||
response, err = util.HTTPGet(url)
|
||||
err = json.Unmarshal(response, &ticket)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if ticket.ErrCode != 0 {
|
||||
err = fmt.Errorf("getTicket Error : errcode=%d , errmsg=%s", ticket.ErrCode, ticket.ErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
jsAPITicketCacheKey := fmt.Sprintf("jsapi_ticket_%s", js.AppID)
|
||||
expires := ticket.ExpiresIn - 1500
|
||||
err = js.Cache.Set(jsAPITicketCacheKey, ticket.Ticket, time.Duration(expires)*time.Second)
|
||||
return
|
||||
}
|
||||
218
material/material.go
Normal file
218
material/material.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package material
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
addNewsURL = "https://api.weixin.qq.com/cgi-bin/material/add_news"
|
||||
addMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/add_material"
|
||||
delMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/del_material"
|
||||
getMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/get_material"
|
||||
)
|
||||
|
||||
//Material 素材管理
|
||||
type Material struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
//NewMaterial init
|
||||
func NewMaterial(context *context.Context) *Material {
|
||||
material := new(Material)
|
||||
material.Context = context
|
||||
return material
|
||||
}
|
||||
|
||||
//Article 永久图文素材
|
||||
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)
|
||||
|
||||
var res struct {
|
||||
NewsItem []*Article `json:"news_item"`
|
||||
}
|
||||
err = json.Unmarshal(responseBytes, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.NewsItem, nil
|
||||
}
|
||||
|
||||
//reqArticles 永久性图文素材请求信息
|
||||
type reqArticles struct {
|
||||
Articles []*Article `json:"articles"`
|
||||
}
|
||||
|
||||
//resArticles 永久性图文素材返回结果
|
||||
type resArticles struct {
|
||||
util.CommonError
|
||||
|
||||
MediaID string `json:"media_id"`
|
||||
}
|
||||
|
||||
//AddNews 新增永久图文素材
|
||||
func (material *Material) AddNews(articles []*Article) (mediaID string, err error) {
|
||||
req := &reqArticles{articles}
|
||||
|
||||
var accessToken string
|
||||
accessToken, err = material.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", addNewsURL, accessToken)
|
||||
responseBytes, err := util.PostJSON(uri, req)
|
||||
var res resArticles
|
||||
err = json.Unmarshal(responseBytes, &res)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
mediaID = res.MediaID
|
||||
return
|
||||
}
|
||||
|
||||
//resAddMaterial 永久性素材上传返回的结果
|
||||
type resAddMaterial struct {
|
||||
util.CommonError
|
||||
|
||||
MediaID string `json:"media_id"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
//AddMaterial 上传永久性素材(处理视频需要单独上传)
|
||||
func (material *Material) AddMaterial(mediaType MediaType, filename string) (mediaID string, url string, err error) {
|
||||
if mediaType == MediaTypeVideo {
|
||||
err = errors.New("永久视频素材上传使用 AddVideo 方法")
|
||||
}
|
||||
var accessToken string
|
||||
accessToken, err = material.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s&type=%s", addMaterialURL, accessToken, mediaType)
|
||||
var response []byte
|
||||
response, err = util.PostFile("media", filename, uri)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var resMaterial resAddMaterial
|
||||
err = json.Unmarshal(response, &resMaterial)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if resMaterial.ErrCode != 0 {
|
||||
err = fmt.Errorf("AddMaterial error : errcode=%v , errmsg=%v", resMaterial.ErrCode, resMaterial.ErrMsg)
|
||||
return
|
||||
}
|
||||
mediaID = resMaterial.MediaID
|
||||
url = resMaterial.URL
|
||||
return
|
||||
}
|
||||
|
||||
type reqVideo struct {
|
||||
Title string `json:"title"`
|
||||
Introduction string `json:"introduction"`
|
||||
}
|
||||
|
||||
//AddVideo 永久视频素材文件上传
|
||||
func (material *Material) AddVideo(filename, title, introduction string) (mediaID string, url string, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = material.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s&type=video", addMaterialURL, accessToken)
|
||||
|
||||
videoDesc := &reqVideo{
|
||||
Title: title,
|
||||
Introduction: introduction,
|
||||
}
|
||||
var fieldValue []byte
|
||||
fieldValue, err = json.Marshal(videoDesc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fields := []util.MultipartFormField{
|
||||
{
|
||||
IsFile: true,
|
||||
Fieldname: "media",
|
||||
Filename: filename,
|
||||
},
|
||||
{
|
||||
IsFile: false,
|
||||
Fieldname: "description",
|
||||
Value: fieldValue,
|
||||
},
|
||||
}
|
||||
|
||||
var response []byte
|
||||
response, err = util.PostMultipartForm(fields, uri)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var resMaterial resAddMaterial
|
||||
err = json.Unmarshal(response, &resMaterial)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if resMaterial.ErrCode != 0 {
|
||||
err = fmt.Errorf("AddMaterial error : errcode=%v , errmsg=%v", resMaterial.ErrCode, resMaterial.ErrMsg)
|
||||
return
|
||||
}
|
||||
mediaID = resMaterial.MediaID
|
||||
url = resMaterial.URL
|
||||
return
|
||||
}
|
||||
|
||||
type reqDeleteMaterial struct {
|
||||
MediaID string `json:"media_id"`
|
||||
}
|
||||
|
||||
//DeleteMaterial 删除永久素材
|
||||
func (material *Material) DeleteMaterial(mediaID string) error {
|
||||
accessToken, err := material.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", delMaterialURL, accessToken)
|
||||
response, err := util.PostJSON(uri, reqDeleteMaterial{mediaID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.DecodeWithCommonError(response, "DeleteMaterial")
|
||||
}
|
||||
@@ -4,21 +4,21 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
// MediaType 媒体文件类型
|
||||
//MediaType 媒体文件类型
|
||||
type MediaType string
|
||||
|
||||
const (
|
||||
// MediaTypeImage 媒体文件:图片
|
||||
//MediaTypeImage 媒体文件:图片
|
||||
MediaTypeImage MediaType = "image"
|
||||
// MediaTypeVoice 媒体文件:声音
|
||||
MediaTypeVoice MediaType = "voice"
|
||||
// MediaTypeVideo 媒体文件:视频
|
||||
MediaTypeVideo MediaType = "video"
|
||||
// MediaTypeThumb 媒体文件:缩略图
|
||||
MediaTypeThumb MediaType = "thumb"
|
||||
//MediaTypeVoice 媒体文件:声音
|
||||
MediaTypeVoice = "voice"
|
||||
//MediaTypeVideo 媒体文件:视频
|
||||
MediaTypeVideo = "video"
|
||||
//MediaTypeThumb 媒体文件:缩略图
|
||||
MediaTypeThumb = "thumb"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -27,7 +27,7 @@ const (
|
||||
mediaGetURL = "https://api.weixin.qq.com/cgi-bin/media/get"
|
||||
)
|
||||
|
||||
// Media 临时素材上传返回信息
|
||||
//Media 临时素材上传返回信息
|
||||
type Media struct {
|
||||
util.CommonError
|
||||
|
||||
@@ -37,7 +37,7 @@ type Media struct {
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
// MediaUpload 临时素材上传
|
||||
//MediaUpload 临时素材上传
|
||||
func (material *Material) MediaUpload(mediaType MediaType, filename string) (media Media, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = material.GetAccessToken()
|
||||
@@ -62,8 +62,8 @@ func (material *Material) MediaUpload(mediaType MediaType, filename string) (med
|
||||
return
|
||||
}
|
||||
|
||||
// GetMediaURL 返回临时素材的下载地址供用户自己处理
|
||||
// NOTICE: URL 不可公开,因为含access_token 需要立即另存文件
|
||||
//GetMediaURL 返回临时素材的下载地址供用户自己处理
|
||||
//NOTICE: URL 不可公开,因为含access_token 需要立即另存文件
|
||||
func (material *Material) GetMediaURL(mediaID string) (mediaURL string, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = material.GetAccessToken()
|
||||
@@ -74,14 +74,14 @@ func (material *Material) GetMediaURL(mediaID string) (mediaURL string, err erro
|
||||
return
|
||||
}
|
||||
|
||||
// resMediaImage 图片上传返回结果
|
||||
//resMediaImage 图片上传返回结果
|
||||
type resMediaImage struct {
|
||||
util.CommonError
|
||||
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// ImageUpload 图片上传
|
||||
//ImageUpload 图片上传
|
||||
func (material *Material) ImageUpload(filename string) (url string, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = material.GetAccessToken()
|
||||
@@ -106,4 +106,5 @@ func (material *Material) ImageUpload(filename string) (url string, err error) {
|
||||
}
|
||||
url = image.URL
|
||||
return
|
||||
|
||||
}
|
||||
143
menu/button.go
Normal file
143
menu/button.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package menu
|
||||
|
||||
//Button 菜单按钮
|
||||
type Button struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
MediaID string `json:"media_id,omitempty"`
|
||||
AppID string `json:"appid,omitempty"`
|
||||
PagePath string `json:"pagepath,omitempty"`
|
||||
SubButtons []*Button `json:"sub_button,omitempty"`
|
||||
}
|
||||
|
||||
//SetSubButton 设置二级菜单
|
||||
func (btn *Button) SetSubButton(name string, subButtons []*Button) {
|
||||
btn.Name = name
|
||||
btn.SubButtons = subButtons
|
||||
btn.Type = ""
|
||||
btn.Key = ""
|
||||
btn.URL = ""
|
||||
btn.MediaID = ""
|
||||
}
|
||||
|
||||
//SetClickButton btn 为click类型
|
||||
func (btn *Button) SetClickButton(name, key string) {
|
||||
btn.Type = "click"
|
||||
btn.Name = name
|
||||
btn.Key = key
|
||||
btn.URL = ""
|
||||
btn.MediaID = ""
|
||||
btn.SubButtons = nil
|
||||
}
|
||||
|
||||
//SetViewButton view类型
|
||||
func (btn *Button) SetViewButton(name, url string) {
|
||||
btn.Type = "view"
|
||||
btn.Name = name
|
||||
btn.URL = url
|
||||
btn.Key = ""
|
||||
btn.MediaID = ""
|
||||
btn.SubButtons = nil
|
||||
}
|
||||
|
||||
// SetScanCodePushButton 扫码推事件
|
||||
func (btn *Button) SetScanCodePushButton(name, key string) {
|
||||
btn.Type = "scancode_push"
|
||||
btn.Name = name
|
||||
btn.Key = key
|
||||
btn.URL = ""
|
||||
btn.MediaID = ""
|
||||
btn.SubButtons = nil
|
||||
}
|
||||
|
||||
//SetScanCodeWaitMsgButton 设置 扫码推事件且弹出"消息接收中"提示框
|
||||
func (btn *Button) SetScanCodeWaitMsgButton(name, key string) {
|
||||
btn.Type = "scancode_waitmsg"
|
||||
btn.Name = name
|
||||
btn.Key = key
|
||||
|
||||
btn.URL = ""
|
||||
btn.MediaID = ""
|
||||
btn.SubButtons = nil
|
||||
}
|
||||
|
||||
//SetPicSysPhotoButton 设置弹出系统拍照发图按钮
|
||||
func (btn *Button) SetPicSysPhotoButton(name, key string) {
|
||||
btn.Type = "pic_sysphoto"
|
||||
btn.Name = name
|
||||
btn.Key = key
|
||||
|
||||
btn.URL = ""
|
||||
btn.MediaID = ""
|
||||
btn.SubButtons = nil
|
||||
}
|
||||
|
||||
//SetPicPhotoOrAlbumButton 设置弹出拍照或者相册发图类型按钮
|
||||
func (btn *Button) SetPicPhotoOrAlbumButton(name, key string) {
|
||||
btn.Type = "pic_photo_or_album"
|
||||
btn.Name = name
|
||||
btn.Key = key
|
||||
|
||||
btn.URL = ""
|
||||
btn.MediaID = ""
|
||||
btn.SubButtons = nil
|
||||
}
|
||||
|
||||
// SetPicWeixinButton 设置弹出微信相册发图器类型按钮
|
||||
func (btn *Button) SetPicWeixinButton(name, key string) {
|
||||
btn.Type = "pic_weixin"
|
||||
btn.Name = name
|
||||
btn.Key = key
|
||||
|
||||
btn.URL = ""
|
||||
btn.MediaID = ""
|
||||
btn.SubButtons = nil
|
||||
}
|
||||
|
||||
// SetLocationSelectButton 设置 弹出地理位置选择器 类型按钮
|
||||
func (btn *Button) SetLocationSelectButton(name, key string) {
|
||||
btn.Type = "location_select"
|
||||
btn.Name = name
|
||||
btn.Key = key
|
||||
|
||||
btn.URL = ""
|
||||
btn.MediaID = ""
|
||||
btn.SubButtons = nil
|
||||
}
|
||||
|
||||
//SetMediaIDButton 设置 下发消息(除文本消息) 类型按钮
|
||||
func (btn *Button) SetMediaIDButton(name, mediaID string) {
|
||||
btn.Type = "media_id"
|
||||
btn.Name = name
|
||||
btn.MediaID = mediaID
|
||||
|
||||
btn.Key = ""
|
||||
btn.URL = ""
|
||||
btn.SubButtons = nil
|
||||
}
|
||||
|
||||
//SetViewLimitedButton 设置 跳转图文消息URL 类型按钮
|
||||
func (btn *Button) SetViewLimitedButton(name, mediaID string) {
|
||||
btn.Type = "view_limited"
|
||||
btn.Name = name
|
||||
btn.MediaID = mediaID
|
||||
|
||||
btn.Key = ""
|
||||
btn.URL = ""
|
||||
btn.SubButtons = nil
|
||||
}
|
||||
|
||||
//SetMiniprogramButton 设置 跳转小程序 类型按钮 (公众号后台必须已经关联小程序)
|
||||
func (btn *Button) SetMiniprogramButton(name, url, appID, pagePath string) {
|
||||
btn.Type = "miniprogram"
|
||||
btn.Name = name
|
||||
btn.URL = url
|
||||
btn.AppID = appID
|
||||
btn.PagePath = pagePath
|
||||
|
||||
btn.Key = ""
|
||||
btn.MediaID = ""
|
||||
btn.SubButtons = nil
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -18,42 +18,42 @@ const (
|
||||
menuSelfMenuInfoURL = "https://api.weixin.qq.com/cgi-bin/get_current_selfmenu_info"
|
||||
)
|
||||
|
||||
// Menu struct
|
||||
//Menu struct
|
||||
type Menu struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// reqMenu 设置菜单请求数据
|
||||
//reqMenu 设置菜单请求数据
|
||||
type reqMenu struct {
|
||||
Button []*Button `json:"button,omitempty"`
|
||||
MatchRule *MatchRule `json:"matchrule,omitempty"`
|
||||
}
|
||||
|
||||
// reqDeleteConditional 删除个性化菜单请求数据
|
||||
//reqDeleteConditional 删除个性化菜单请求数据
|
||||
type reqDeleteConditional struct {
|
||||
MenuID int64 `json:"menuid"`
|
||||
}
|
||||
|
||||
// reqMenuTryMatch 菜单匹配请求
|
||||
//reqMenuTryMatch 菜单匹配请求
|
||||
type reqMenuTryMatch struct {
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
// resConditionalMenu 个性化菜单返回结果
|
||||
//resConditionalMenu 个性化菜单返回结果
|
||||
type resConditionalMenu struct {
|
||||
Button []Button `json:"button"`
|
||||
MatchRule MatchRule `json:"matchrule"`
|
||||
MenuID int64 `json:"menuid"`
|
||||
}
|
||||
|
||||
// resMenuTryMatch 菜单匹配请求结果
|
||||
//resMenuTryMatch 菜单匹配请求结果
|
||||
type resMenuTryMatch struct {
|
||||
util.CommonError
|
||||
|
||||
Button []Button `json:"button"`
|
||||
}
|
||||
|
||||
// ResMenu 查询菜单的返回数据
|
||||
//ResMenu 查询菜单的返回数据
|
||||
type ResMenu struct {
|
||||
util.CommonError
|
||||
|
||||
@@ -64,7 +64,7 @@ type ResMenu struct {
|
||||
Conditionalmenu []resConditionalMenu `json:"conditionalmenu"`
|
||||
}
|
||||
|
||||
// ResSelfMenuInfo 自定义菜单配置返回结果
|
||||
//ResSelfMenuInfo 自定义菜单配置返回结果
|
||||
type ResSelfMenuInfo struct {
|
||||
util.CommonError
|
||||
|
||||
@@ -74,7 +74,7 @@ type ResSelfMenuInfo struct {
|
||||
} `json:"selfmenu_info"`
|
||||
}
|
||||
|
||||
// SelfMenuButton 自定义菜单配置详情
|
||||
//SelfMenuButton 自定义菜单配置详情
|
||||
type SelfMenuButton struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
@@ -89,7 +89,7 @@ type SelfMenuButton struct {
|
||||
} `json:"news_info,omitempty"`
|
||||
}
|
||||
|
||||
// ButtonNew 图文消息菜单
|
||||
//ButtonNew 图文消息菜单
|
||||
type ButtonNew struct {
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
@@ -100,25 +100,25 @@ type ButtonNew struct {
|
||||
SourceURL string `json:"source_url"`
|
||||
}
|
||||
|
||||
// MatchRule 个性化菜单规则
|
||||
//MatchRule 个性化菜单规则
|
||||
type MatchRule struct {
|
||||
GroupID string `json:"group_id,omitempty"`
|
||||
Sex string `json:"sex,omitempty"`
|
||||
GroupID int32 `json:"group_id,omitempty"`
|
||||
Sex int32 `json:"sex,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
Province string `json:"province,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
ClientPlatformType string `json:"client_platform_type,omitempty"`
|
||||
ClientPlatformType int32 `json:"client_platform_type,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
}
|
||||
|
||||
// NewMenu 实例
|
||||
//NewMenu 实例
|
||||
func NewMenu(context *context.Context) *Menu {
|
||||
menu := new(Menu)
|
||||
menu.Context = context
|
||||
return menu
|
||||
}
|
||||
|
||||
// SetMenu 设置按钮
|
||||
//SetMenu 设置按钮
|
||||
func (menu *Menu) SetMenu(buttons []*Button) error {
|
||||
accessToken, err := menu.GetAccessToken()
|
||||
if err != nil {
|
||||
@@ -138,24 +138,7 @@ func (menu *Menu) SetMenu(buttons []*Button) error {
|
||||
return util.DecodeWithCommonError(response, "SetMenu")
|
||||
}
|
||||
|
||||
// SetMenuByJSON 设置按钮
|
||||
func (menu *Menu) SetMenuByJSON(jsonInfo string) error {
|
||||
accessToken, err := menu.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", menuCreateURL, accessToken)
|
||||
|
||||
response, err := util.HTTPPost(uri, jsonInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.DecodeWithCommonError(response, "SetMenuByJSON")
|
||||
}
|
||||
|
||||
// GetMenu 获取菜单配置
|
||||
//GetMenu 获取菜单配置
|
||||
func (menu *Menu) GetMenu() (resMenu ResMenu, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = menu.GetAccessToken()
|
||||
@@ -179,7 +162,7 @@ func (menu *Menu) GetMenu() (resMenu ResMenu, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteMenu 删除菜单
|
||||
//DeleteMenu 删除菜单
|
||||
func (menu *Menu) DeleteMenu() error {
|
||||
accessToken, err := menu.GetAccessToken()
|
||||
if err != nil {
|
||||
@@ -194,7 +177,7 @@ func (menu *Menu) DeleteMenu() error {
|
||||
return util.DecodeWithCommonError(response, "GetMenu")
|
||||
}
|
||||
|
||||
// AddConditional 添加个性化菜单
|
||||
//AddConditional 添加个性化菜单
|
||||
func (menu *Menu) AddConditional(buttons []*Button, matchRule *MatchRule) error {
|
||||
accessToken, err := menu.GetAccessToken()
|
||||
if err != nil {
|
||||
@@ -215,23 +198,7 @@ func (menu *Menu) AddConditional(buttons []*Button, matchRule *MatchRule) error
|
||||
return util.DecodeWithCommonError(response, "AddConditional")
|
||||
}
|
||||
|
||||
// AddConditionalByJSON 添加个性化菜单
|
||||
func (menu *Menu) AddConditionalByJSON(jsonInfo string) error {
|
||||
accessToken, err := menu.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", menuAddConditionalURL, accessToken)
|
||||
response, err := util.HTTPPost(uri, jsonInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.DecodeWithCommonError(response, "AddConditional")
|
||||
}
|
||||
|
||||
// DeleteConditional 删除个性化菜单
|
||||
//DeleteConditional 删除个性化菜单
|
||||
func (menu *Menu) DeleteConditional(menuID int64) error {
|
||||
accessToken, err := menu.GetAccessToken()
|
||||
if err != nil {
|
||||
@@ -251,7 +218,7 @@ func (menu *Menu) DeleteConditional(menuID int64) error {
|
||||
return util.DecodeWithCommonError(response, "DeleteConditional")
|
||||
}
|
||||
|
||||
// MenuTryMatch 菜单匹配
|
||||
//MenuTryMatch 菜单匹配
|
||||
func (menu *Menu) MenuTryMatch(userID string) (buttons []Button, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = menu.GetAccessToken()
|
||||
@@ -278,7 +245,7 @@ func (menu *Menu) MenuTryMatch(userID string) (buttons []Button, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// GetCurrentSelfMenuInfo 获取自定义菜单配置接口
|
||||
//GetCurrentSelfMenuInfo 获取自定义菜单配置接口
|
||||
func (menu *Menu) GetCurrentSelfMenuInfo() (resSelfMenuInfo ResSelfMenuInfo, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = menu.GetAccessToken()
|
||||
@@ -3,44 +3,43 @@ package message
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
customerSendMessage = "https://api.weixin.qq.com/cgi-bin/message/custom/send"
|
||||
)
|
||||
|
||||
// Manager 消息管理者,可以发送消息
|
||||
//Manager 消息管理者,可以发送消息
|
||||
type Manager struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewMessageManager 实例化消息管理者
|
||||
//NewMessageManager 实例化消息管理者
|
||||
func NewMessageManager(context *context.Context) *Manager {
|
||||
return &Manager{
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
// CustomerMessage 客服消息
|
||||
//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"` // 可选
|
||||
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 文本消息结构体构造方法
|
||||
//NewCustomerTextMessage 文本消息结构体构造方法
|
||||
func NewCustomerTextMessage(toUser, text string) *CustomerMessage {
|
||||
return &CustomerMessage{
|
||||
ToUser: toUser,
|
||||
@@ -51,7 +50,7 @@ func NewCustomerTextMessage(toUser, text string) *CustomerMessage {
|
||||
}
|
||||
}
|
||||
|
||||
// NewCustomerImgMessage 图片消息的构造方法
|
||||
//NewCustomerImgMessage 图片消息的构造方法
|
||||
func NewCustomerImgMessage(toUser, mediaID string) *CustomerMessage {
|
||||
return &CustomerMessage{
|
||||
ToUser: toUser,
|
||||
@@ -62,7 +61,7 @@ func NewCustomerImgMessage(toUser, mediaID string) *CustomerMessage {
|
||||
}
|
||||
}
|
||||
|
||||
// NewCustomerVoiceMessage 语音消息的构造方法
|
||||
//NewCustomerVoiceMessage 语音消息的构造方法
|
||||
func NewCustomerVoiceMessage(toUser, mediaID string) *CustomerMessage {
|
||||
return &CustomerMessage{
|
||||
ToUser: toUser,
|
||||
@@ -73,31 +72,17 @@ func NewCustomerVoiceMessage(toUser, mediaID string) *CustomerMessage {
|
||||
}
|
||||
}
|
||||
|
||||
// NewCustomerMiniprogrampageMessage 小程序卡片消息的构造方法
|
||||
func NewCustomerMiniprogrampageMessage(toUser, title, appID, pagePath, thumbMediaID string) *CustomerMessage {
|
||||
return &CustomerMessage{
|
||||
ToUser: toUser,
|
||||
Msgtype: MsgTypeMiniprogrampage,
|
||||
Miniprogrampage: &MediaMiniprogrampage{
|
||||
Title: title,
|
||||
AppID: appID,
|
||||
Pagepath: pagePath,
|
||||
ThumbMediaID: thumbMediaID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MediaText 文本消息的文字
|
||||
//MediaText 文本消息的文字
|
||||
type MediaText struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// MediaResource 消息使用的永久素材id
|
||||
//MediaResource 消息使用的永久素材id
|
||||
type MediaResource struct {
|
||||
MediaID string `json:"media_id"`
|
||||
}
|
||||
|
||||
// MediaVideo 视频消息包含的内容
|
||||
//MediaVideo 视频消息包含的内容
|
||||
type MediaVideo struct {
|
||||
MediaID string `json:"media_id"`
|
||||
ThumbMediaID string `json:"thumb_media_id"`
|
||||
@@ -105,7 +90,7 @@ type MediaVideo struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// MediaMusic 音乐消息包括的内容
|
||||
//MediaMusic 音乐消息包括的内容
|
||||
type MediaMusic struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
@@ -114,12 +99,12 @@ type MediaMusic struct {
|
||||
ThumbMediaID string `json:"thumb_media_id"`
|
||||
}
|
||||
|
||||
// MediaNews 图文消息的内容
|
||||
//MediaNews 图文消息的内容
|
||||
type MediaNews struct {
|
||||
Articles []MediaArticles `json:"articles"`
|
||||
}
|
||||
|
||||
// MediaArticles 图文消息的内容的文章列表中的单独一条
|
||||
//MediaArticles 图文消息的内容的文章列表中的单独一条
|
||||
type MediaArticles struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
@@ -127,33 +112,33 @@ type MediaArticles struct {
|
||||
Picurl string `json:"picurl"`
|
||||
}
|
||||
|
||||
// MediaMsgmenu 菜单消息的内容
|
||||
//MediaMsgmenu 菜单消息的内容
|
||||
type MediaMsgmenu struct {
|
||||
HeadContent string `json:"head_content"`
|
||||
List []MsgmenuItem `json:"list"`
|
||||
TailContent string `json:"tail_content"`
|
||||
}
|
||||
|
||||
// MsgmenuItem 菜单消息的菜单按钮
|
||||
//MsgmenuItem 菜单消息的菜单按钮
|
||||
type MsgmenuItem struct {
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// MediaWxcard 卡券的id
|
||||
//MediaWxcard 卡券的id
|
||||
type MediaWxcard struct {
|
||||
CardID string `json:"card_id"`
|
||||
}
|
||||
|
||||
// MediaMiniprogrampage 小程序消息
|
||||
//MediaMiniprogrampage 小程序消息
|
||||
type MediaMiniprogrampage struct {
|
||||
Title string `json:"title"`
|
||||
AppID string `json:"appid"`
|
||||
Appid string `json:"appid"`
|
||||
Pagepath string `json:"pagepath"`
|
||||
ThumbMediaID string `json:"thumb_media_id"`
|
||||
}
|
||||
|
||||
// Send 发送客服消息
|
||||
//Send 发送客服消息
|
||||
func (manager *Manager) Send(msg *CustomerMessage) error {
|
||||
accessToken, err := manager.Context.GetAccessToken()
|
||||
if err != nil {
|
||||
@@ -161,9 +146,6 @@ func (manager *Manager) Send(msg *CustomerMessage) error {
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", customerSendMessage, accessToken)
|
||||
response, err := util.PostJSON(uri, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var result util.CommonError
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
@@ -1,6 +1,6 @@
|
||||
package message
|
||||
|
||||
// Image 图片消息
|
||||
//Image 图片消息
|
||||
type Image struct {
|
||||
CommonToken
|
||||
|
||||
@@ -9,7 +9,7 @@ type Image struct {
|
||||
} `xml:"Image"`
|
||||
}
|
||||
|
||||
// NewImage 回复图片消息
|
||||
//NewImage 回复图片消息
|
||||
func NewImage(mediaID string) *Image {
|
||||
image := new(Image)
|
||||
image.Image.MediaID = mediaID
|
||||
222
message/message.go
Normal file
222
message/message.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
|
||||
"github.com/silenceper/wechat/device"
|
||||
)
|
||||
|
||||
// MsgType 基本消息类型
|
||||
type MsgType string
|
||||
|
||||
// EventType 事件类型
|
||||
type EventType string
|
||||
|
||||
// InfoType 第三方平台授权事件类型
|
||||
type InfoType string
|
||||
|
||||
const (
|
||||
//MsgTypeText 表示文本消息
|
||||
MsgTypeText MsgType = "text"
|
||||
//MsgTypeImage 表示图片消息
|
||||
MsgTypeImage = "image"
|
||||
//MsgTypeVoice 表示语音消息
|
||||
MsgTypeVoice = "voice"
|
||||
//MsgTypeVideo 表示视频消息
|
||||
MsgTypeVideo = "video"
|
||||
//MsgTypeShortVideo 表示短视频消息[限接收]
|
||||
MsgTypeShortVideo = "shortvideo"
|
||||
//MsgTypeLocation 表示坐标消息[限接收]
|
||||
MsgTypeLocation = "location"
|
||||
//MsgTypeLink 表示链接消息[限接收]
|
||||
MsgTypeLink = "link"
|
||||
//MsgTypeMusic 表示音乐消息[限回复]
|
||||
MsgTypeMusic = "music"
|
||||
//MsgTypeNews 表示图文消息[限回复]
|
||||
MsgTypeNews = "news"
|
||||
//MsgTypeTransfer 表示消息消息转发到客服
|
||||
MsgTypeTransfer = "transfer_customer_service"
|
||||
//MsgTypeEvent 表示事件推送消息
|
||||
MsgTypeEvent = "event"
|
||||
)
|
||||
|
||||
const (
|
||||
//EventSubscribe 订阅
|
||||
EventSubscribe EventType = "subscribe"
|
||||
//EventUnsubscribe 取消订阅
|
||||
EventUnsubscribe = "unsubscribe"
|
||||
//EventScan 用户已经关注公众号,则微信会将带场景值扫描事件推送给开发者
|
||||
EventScan = "SCAN"
|
||||
//EventLocation 上报地理位置事件
|
||||
EventLocation = "LOCATION"
|
||||
//EventClick 点击菜单拉取消息时的事件推送
|
||||
EventClick = "CLICK"
|
||||
//EventView 点击菜单跳转链接时的事件推送
|
||||
EventView = "VIEW"
|
||||
//EventScancodePush 扫码推事件的事件推送
|
||||
EventScancodePush = "scancode_push"
|
||||
//EventScancodeWaitmsg 扫码推事件且弹出“消息接收中”提示框的事件推送
|
||||
EventScancodeWaitmsg = "scancode_waitmsg"
|
||||
//EventPicSysphoto 弹出系统拍照发图的事件推送
|
||||
EventPicSysphoto = "pic_sysphoto"
|
||||
//EventPicPhotoOrAlbum 弹出拍照或者相册发图的事件推送
|
||||
EventPicPhotoOrAlbum = "pic_photo_or_album"
|
||||
//EventPicWeixin 弹出微信相册发图器的事件推送
|
||||
EventPicWeixin = "pic_weixin"
|
||||
//EventLocationSelect 弹出地理位置选择器的事件推送
|
||||
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 存放所有微信发送过来的消息和事件
|
||||
type MixMessage struct {
|
||||
CommonToken
|
||||
|
||||
//基本消息
|
||||
MsgID int64 `xml:"MsgId"`
|
||||
Content string `xml:"Content"`
|
||||
Recognition string `xml:"Recognition"`
|
||||
PicURL string `xml:"PicUrl"`
|
||||
MediaID string `xml:"MediaId"`
|
||||
Format string `xml:"Format"`
|
||||
ThumbMediaID string `xml:"ThumbMediaId"`
|
||||
LocationX float64 `xml:"Location_X"`
|
||||
LocationY float64 `xml:"Location_Y"`
|
||||
Scale float64 `xml:"Scale"`
|
||||
Label string `xml:"Label"`
|
||||
Title string `xml:"Title"`
|
||||
Description string `xml:"Description"`
|
||||
URL string `xml:"Url"`
|
||||
|
||||
//事件相关
|
||||
Event EventType `xml:"Event"`
|
||||
EventKey string `xml:"EventKey"`
|
||||
Ticket string `xml:"Ticket"`
|
||||
Latitude string `xml:"Latitude"`
|
||||
Longitude string `xml:"Longitude"`
|
||||
Precision string `xml:"Precision"`
|
||||
MenuID string `xml:"MenuId"`
|
||||
Status string `xml:"Status"`
|
||||
SessionFrom string `xml:"SessionFrom"`
|
||||
|
||||
ScanCodeInfo struct {
|
||||
ScanType string `xml:"ScanType"`
|
||||
ScanResult string `xml:"ScanResult"`
|
||||
} `xml:"ScanCodeInfo"`
|
||||
|
||||
SendPicsInfo struct {
|
||||
Count int32 `xml:"Count"`
|
||||
PicList []EventPic `xml:"PicList>item"`
|
||||
} `xml:"SendPicsInfo"`
|
||||
|
||||
SendLocationInfo struct {
|
||||
LocationX float64 `xml:"Location_X"`
|
||||
LocationY float64 `xml:"Location_Y"`
|
||||
Scale float64 `xml:"Scale"`
|
||||
Label string `xml:"Label"`
|
||||
Poiname string `xml:"Poiname"`
|
||||
}
|
||||
|
||||
// 第三方平台相关
|
||||
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 发图事件推送
|
||||
type EventPic struct {
|
||||
PicMd5Sum string `xml:"PicMd5Sum"`
|
||||
}
|
||||
|
||||
//EncryptedXMLMsg 安全模式下的消息体
|
||||
type EncryptedXMLMsg struct {
|
||||
XMLName struct{} `xml:"xml" json:"-"`
|
||||
ToUserName string `xml:"ToUserName" json:"ToUserName"`
|
||||
EncryptedMsg string `xml:"Encrypt" json:"Encrypt"`
|
||||
}
|
||||
|
||||
//ResponseEncryptedXMLMsg 需要返回的消息体
|
||||
type ResponseEncryptedXMLMsg struct {
|
||||
XMLName struct{} `xml:"xml" json:"-"`
|
||||
EncryptedMsg string `xml:"Encrypt" json:"Encrypt"`
|
||||
MsgSignature string `xml:"MsgSignature" json:"MsgSignature"`
|
||||
Timestamp int64 `xml:"TimeStamp" json:"TimeStamp"`
|
||||
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 CDATA `xml:"ToUserName"`
|
||||
FromUserName CDATA `xml:"FromUserName"`
|
||||
CreateTime int64 `xml:"CreateTime"`
|
||||
MsgType MsgType `xml:"MsgType"`
|
||||
}
|
||||
|
||||
//SetToUserName set ToUserName
|
||||
func (msg *CommonToken) SetToUserName(toUserName CDATA) {
|
||||
msg.ToUserName = toUserName
|
||||
}
|
||||
|
||||
//SetFromUserName set FromUserName
|
||||
func (msg *CommonToken) SetFromUserName(fromUserName CDATA) {
|
||||
msg.FromUserName = fromUserName
|
||||
}
|
||||
|
||||
//SetCreateTime set createTime
|
||||
func (msg *CommonToken) SetCreateTime(createTime int64) {
|
||||
msg.CreateTime = createTime
|
||||
}
|
||||
|
||||
//SetMsgType set MsgType
|
||||
func (msg *CommonToken) SetMsgType(msgType MsgType) {
|
||||
msg.MsgType = msgType
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package message
|
||||
|
||||
// Music 音乐消息
|
||||
//Music 音乐消息
|
||||
type Music struct {
|
||||
CommonToken
|
||||
|
||||
@@ -13,7 +13,7 @@ type Music struct {
|
||||
} `xml:"Music"`
|
||||
}
|
||||
|
||||
// NewMusic 回复音乐消息
|
||||
//NewMusic 回复音乐消息
|
||||
func NewMusic(title, description, musicURL, hQMusicURL, thumbMediaID string) *Music {
|
||||
music := new(Music)
|
||||
music.Music.Title = title
|
||||
@@ -1,6 +1,6 @@
|
||||
package message
|
||||
|
||||
// News 图文消息
|
||||
//News 图文消息
|
||||
type News struct {
|
||||
CommonToken
|
||||
|
||||
@@ -8,7 +8,7 @@ type News struct {
|
||||
Articles []*Article `xml:"Articles>item,omitempty"`
|
||||
}
|
||||
|
||||
// NewNews 初始化图文消息
|
||||
//NewNews 初始化图文消息
|
||||
func NewNews(articles []*Article) *News {
|
||||
news := new(News)
|
||||
news.ArticleCount = len(articles)
|
||||
@@ -16,7 +16,7 @@ func NewNews(articles []*Article) *News {
|
||||
return news
|
||||
}
|
||||
|
||||
// Article 单篇文章
|
||||
//Article 单篇文章
|
||||
type Article struct {
|
||||
Title string `xml:"Title,omitempty"`
|
||||
Description string `xml:"Description,omitempty"`
|
||||
@@ -24,7 +24,7 @@ type Article struct {
|
||||
URL string `xml:"Url,omitempty"`
|
||||
}
|
||||
|
||||
// NewArticle 初始化文章
|
||||
//NewArticle 初始化文章
|
||||
func NewArticle(title, description, picURL, url string) *Article {
|
||||
article := new(Article)
|
||||
article.Title = title
|
||||
@@ -1,23 +1,23 @@
|
||||
package message
|
||||
|
||||
// TransferCustomer 转发客服消息
|
||||
//TransferCustomer 转发客服消息
|
||||
type TransferCustomer struct {
|
||||
CommonToken
|
||||
|
||||
TransInfo *TransInfo `xml:"TransInfo,omitempty"`
|
||||
}
|
||||
|
||||
// TransInfo 转发到指定客服
|
||||
//TransInfo 转发到指定客服
|
||||
type TransInfo struct {
|
||||
KfAccount string `xml:"KfAccount"`
|
||||
}
|
||||
|
||||
// NewTransferCustomer 实例化
|
||||
func NewTransferCustomer(kfAccount string) *TransferCustomer {
|
||||
//NewTransferCustomer 实例化
|
||||
func NewTransferCustomer(KfAccount string) *TransferCustomer {
|
||||
tc := new(TransferCustomer)
|
||||
if kfAccount != "" {
|
||||
if KfAccount != "" {
|
||||
transInfo := new(TransInfo)
|
||||
transInfo.KfAccount = kfAccount
|
||||
transInfo.KfAccount = KfAccount
|
||||
tc.TransInfo = transInfo
|
||||
}
|
||||
return tc
|
||||
@@ -2,13 +2,13 @@ package message
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrInvalidReply 无效的回复
|
||||
//ErrInvalidReply 无效的回复
|
||||
var ErrInvalidReply = errors.New("无效的回复消息")
|
||||
|
||||
// ErrUnsupportReply 不支持的回复类型
|
||||
//ErrUnsupportReply 不支持的回复类型
|
||||
var ErrUnsupportReply = errors.New("不支持的回复消息")
|
||||
|
||||
// Reply 消息回复
|
||||
//Reply 消息回复
|
||||
type Reply struct {
|
||||
MsgType MsgType
|
||||
MsgData interface{}
|
||||
74
message/template.go
Normal file
74
message/template.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
templateSendURL = "https://api.weixin.qq.com/cgi-bin/message/template/send"
|
||||
)
|
||||
|
||||
//Template 模板消息
|
||||
type Template struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
//NewTemplate 实例化
|
||||
func NewTemplate(context *context.Context) *Template {
|
||||
tpl := new(Template)
|
||||
tpl.Context = context
|
||||
return tpl
|
||||
}
|
||||
|
||||
//Message 发送的模板消息内容
|
||||
type Message struct {
|
||||
ToUser string `json:"touser"` // 必须, 接受者OpenID
|
||||
TemplateID string `json:"template_id"` // 必须, 模版ID
|
||||
URL string `json:"url,omitempty"` // 可选, 用户点击后跳转的URL, 该URL必须处于开发者在公众平台网站中设置的域中
|
||||
Color string `json:"color,omitempty"` // 可选, 整个消息的颜色, 可以不设置
|
||||
Data map[string]*DataItem `json:"data"` // 必须, 模板数据
|
||||
|
||||
MiniProgram struct {
|
||||
AppID string `json:"appid"` //所需跳转到的小程序appid(该小程序appid必须与发模板消息的公众号是绑定关联关系)
|
||||
PagePath string `json:"pagepath"` //所需跳转到小程序的具体页面路径,支持带参数,(示例index?foo=bar)
|
||||
} `json:"miniprogram"` //可选,跳转至小程序地址
|
||||
}
|
||||
|
||||
//DataItem 模版内某个 .DATA 的值
|
||||
type DataItem struct {
|
||||
Value string `json:"value"`
|
||||
Color string `json:"color,omitempty"`
|
||||
}
|
||||
|
||||
type resTemplateSend struct {
|
||||
util.CommonError
|
||||
|
||||
MsgID int64 `json:"msgid"`
|
||||
}
|
||||
|
||||
//Send 发送模板消息
|
||||
func (tpl *Template) Send(msg *Message) (msgID int64, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = tpl.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", templateSendURL, accessToken)
|
||||
response, err := util.PostJSON(uri, msg)
|
||||
|
||||
var result resTemplateSend
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
msgID = result.MsgID
|
||||
return
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package message
|
||||
|
||||
// Text 文本消息
|
||||
//Text 文本消息
|
||||
type Text struct {
|
||||
CommonToken
|
||||
Content CDATA `xml:"Content"`
|
||||
}
|
||||
|
||||
// NewText 初始化文本消息
|
||||
//NewText 初始化文本消息
|
||||
func NewText(content string) *Text {
|
||||
text := new(Text)
|
||||
text.Content = CDATA(content)
|
||||
@@ -1,6 +1,6 @@
|
||||
package message
|
||||
|
||||
// Video 视频消息
|
||||
//Video 视频消息
|
||||
type Video struct {
|
||||
CommonToken
|
||||
|
||||
@@ -11,7 +11,7 @@ type Video struct {
|
||||
} `xml:"Video"`
|
||||
}
|
||||
|
||||
// NewVideo 回复图片消息
|
||||
//NewVideo 回复图片消息
|
||||
func NewVideo(mediaID, title, description string) *Video {
|
||||
video := new(Video)
|
||||
video.Video.MediaID = mediaID
|
||||
@@ -1,6 +1,6 @@
|
||||
package message
|
||||
|
||||
// Voice 语音消息
|
||||
//Voice 语音消息
|
||||
type Voice struct {
|
||||
CommonToken
|
||||
|
||||
@@ -9,7 +9,7 @@ type Voice struct {
|
||||
} `xml:"Voice"`
|
||||
}
|
||||
|
||||
// NewVoice 回复语音消息
|
||||
//NewVoice 回复语音消息
|
||||
func NewVoice(mediaID string) *Voice {
|
||||
voice := new(Voice)
|
||||
voice.Voice.MediaID = mediaID
|
||||
@@ -1,9 +0,0 @@
|
||||
# 微信小游戏
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/minigame/dev/api-backend/)
|
||||
|
||||
## 快速入门
|
||||
|
||||
```go
|
||||
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
# 微信小程序
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/miniprogram/dev/framework/)
|
||||
|
||||
## 包说明
|
||||
|
||||
- analysis 数据分析相关API
|
||||
|
||||
## 快速入门
|
||||
|
||||
```go
|
||||
wc := wechat.NewWechat()
|
||||
memory := cache.NewMemory()
|
||||
cfg := &miniConfig.Config{
|
||||
AppID: "xxx",
|
||||
AppSecret: "xxx",
|
||||
Cache: memory,
|
||||
}
|
||||
miniprogram := wc.GetMiniProgram(cfg)
|
||||
miniprogram.GetAnalysis().GetAnalysisDailyRetain()
|
||||
```
|
||||
@@ -1,12 +1,10 @@
|
||||
package analysis
|
||||
package miniprogram
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,20 +30,10 @@ const (
|
||||
getAnalysisVisitPageURL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitpage?access_token=%s"
|
||||
)
|
||||
|
||||
// Analysis analyis 数据分析
|
||||
type Analysis struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewAnalysis new
|
||||
func NewAnalysis(ctx *context.Context) *Analysis {
|
||||
return &Analysis{ctx}
|
||||
}
|
||||
|
||||
// fetchData 拉取统计数据
|
||||
func (analysis *Analysis) fetchData(urlStr string, body interface{}) (response []byte, err error) {
|
||||
func (wxa *MiniProgram) fetchData(urlStr string, body interface{}) (response []byte, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = analysis.GetAccessToken()
|
||||
accessToken, err = wxa.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -54,8 +42,8 @@ func (analysis *Analysis) fetchData(urlStr string, body interface{}) (response [
|
||||
return
|
||||
}
|
||||
|
||||
// RetainItem 留存项结构
|
||||
type RetainItem struct {
|
||||
// AnalysisRetainItem 留存项结构
|
||||
type AnalysisRetainItem struct {
|
||||
Key int `json:"key"` // 标识,0开始表示当天,1表示1甜后,以此类推
|
||||
Value int `json:"value"` // key对应日期的新增用户数/活跃用户数(key=0时)或留存用户数(k>0时)
|
||||
}
|
||||
@@ -63,18 +51,18 @@ type RetainItem struct {
|
||||
// ResAnalysisRetain 小程序留存数据返回
|
||||
type ResAnalysisRetain struct {
|
||||
util.CommonError
|
||||
RefDate string `json:"ref_date"` // 日期
|
||||
VisitUVNew []RetainItem `json:"visit_uv_new"` // 新增用户留存
|
||||
VisitUV []RetainItem `json:"visit_uv"` // 活跃用户留存
|
||||
RefDate string `json:"ref_date"` // 日期
|
||||
VisitUVNew []AnalysisRetainItem `json:"visit_uv_new"` // 新增用户留存
|
||||
VisitUV []AnalysisRetainItem `json:"visit_uv"` // 活跃用户留存
|
||||
}
|
||||
|
||||
// getAnalysisRetain 获取用户访问小程序留存数据(日、月、周)
|
||||
func (analysis *Analysis) getAnalysisRetain(urlStr string, beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||
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 := analysis.fetchData(urlStr, body)
|
||||
response, err := wxa.fetchData(urlStr, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -90,18 +78,18 @@ func (analysis *Analysis) getAnalysisRetain(urlStr string, beginDate, endDate st
|
||||
}
|
||||
|
||||
// GetAnalysisDailyRetain 获取用户访问小程序日留存
|
||||
func (analysis *Analysis) GetAnalysisDailyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||
return analysis.getAnalysisRetain(getAnalysisDailyRetainURL, beginDate, endDate)
|
||||
func (wxa *MiniProgram) GetAnalysisDailyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||
return wxa.getAnalysisRetain(getAnalysisDailyRetainURL, beginDate, endDate)
|
||||
}
|
||||
|
||||
// GetAnalysisMonthlyRetain 获取用户访问小程序月留存
|
||||
func (analysis *Analysis) GetAnalysisMonthlyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||
return analysis.getAnalysisRetain(getAnalysisMonthlyRetainURL, beginDate, endDate)
|
||||
func (wxa *MiniProgram) GetAnalysisMonthlyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||
return wxa.getAnalysisRetain(getAnalysisMonthlyRetainURL, beginDate, endDate)
|
||||
}
|
||||
|
||||
// GetAnalysisWeeklyRetain 获取用户访问小程序周留存
|
||||
func (analysis *Analysis) GetAnalysisWeeklyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||
return analysis.getAnalysisRetain(getAnalysisWeeklyRetainURL, beginDate, endDate)
|
||||
func (wxa *MiniProgram) GetAnalysisWeeklyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
|
||||
return wxa.getAnalysisRetain(getAnalysisWeeklyRetainURL, beginDate, endDate)
|
||||
}
|
||||
|
||||
// ResAnalysisDailySummary 小程序访问数据概况
|
||||
@@ -116,16 +104,16 @@ type ResAnalysisDailySummary struct {
|
||||
}
|
||||
|
||||
// GetAnalysisDailySummary 获取用户访问小程序数据概况
|
||||
func (analysis *Analysis) GetAnalysisDailySummary(beginDate, endDate string) (result ResAnalysisDailySummary, err error) {
|
||||
func (wxa *MiniProgram) GetAnalysisDailySummary(beginDate, endDate string) (result ResAnalysisDailySummary, err error) {
|
||||
body := map[string]string{
|
||||
"begin_date": beginDate,
|
||||
"end_date": endDate,
|
||||
}
|
||||
response, err := analysis.fetchData(getAnalysisDailySummaryURL, body)
|
||||
response, err := wxa.fetchData(getAnalysisDailySummaryURL, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(string(response))
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -153,12 +141,12 @@ type ResAnalysisVisitTrend struct {
|
||||
}
|
||||
|
||||
// getAnalysisRetain 获取小程序访问数据趋势(日、月、周)
|
||||
func (analysis *Analysis) getAnalysisVisitTrend(urlStr string, beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||
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 := analysis.fetchData(urlStr, body)
|
||||
response, err := wxa.fetchData(urlStr, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -174,18 +162,18 @@ func (analysis *Analysis) getAnalysisVisitTrend(urlStr string, beginDate, endDat
|
||||
}
|
||||
|
||||
// GetAnalysisDailyVisitTrend 获取用户访问小程序数据日趋势
|
||||
func (analysis *Analysis) GetAnalysisDailyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||
return analysis.getAnalysisVisitTrend(getAnalysisDailyVisitTrendURL, beginDate, endDate)
|
||||
func (wxa *MiniProgram) GetAnalysisDailyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||
return wxa.getAnalysisVisitTrend(getAnalysisDailyVisitTrendURL, beginDate, endDate)
|
||||
}
|
||||
|
||||
// GetAnalysisMonthlyVisitTrend 获取用户访问小程序数据月趋势
|
||||
func (analysis *Analysis) GetAnalysisMonthlyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||
return analysis.getAnalysisVisitTrend(getAnalysisMonthlyVisitTrendURL, beginDate, endDate)
|
||||
func (wxa *MiniProgram) GetAnalysisMonthlyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||
return wxa.getAnalysisVisitTrend(getAnalysisMonthlyVisitTrendURL, beginDate, endDate)
|
||||
}
|
||||
|
||||
// GetAnalysisWeeklyVisitTrend 获取用户访问小程序数据周趋势
|
||||
func (analysis *Analysis) GetAnalysisWeeklyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||
return analysis.getAnalysisVisitTrend(getAnalysisWeeklyVisitTrendURL, beginDate, endDate)
|
||||
func (wxa *MiniProgram) GetAnalysisWeeklyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
|
||||
return wxa.getAnalysisVisitTrend(getAnalysisWeeklyVisitTrendURL, beginDate, endDate)
|
||||
}
|
||||
|
||||
// UserPortraitItem 用户画像项目
|
||||
@@ -215,12 +203,12 @@ type ResAnalysisUserPortrait struct {
|
||||
}
|
||||
|
||||
// GetAnalysisUserPortrait 获取小程序新增或活跃用户的画像分布数据
|
||||
func (analysis *Analysis) GetAnalysisUserPortrait(beginDate, endDate string) (result ResAnalysisUserPortrait, err error) {
|
||||
func (wxa *MiniProgram) GetAnalysisUserPortrait(beginDate, endDate string) (result ResAnalysisUserPortrait, err error) {
|
||||
body := map[string]string{
|
||||
"begin_date": beginDate,
|
||||
"end_date": endDate,
|
||||
}
|
||||
response, err := analysis.fetchData(getAnalysisUserPortraitURL, body)
|
||||
response, err := wxa.fetchData(getAnalysisUserPortraitURL, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -256,12 +244,12 @@ type ResAnalysisVisitDistribution struct {
|
||||
}
|
||||
|
||||
// GetAnalysisVisitDistribution 获取用户小程序访问分布数据
|
||||
func (analysis *Analysis) GetAnalysisVisitDistribution(beginDate, endDate string) (result ResAnalysisVisitDistribution, err error) {
|
||||
func (wxa *MiniProgram) GetAnalysisVisitDistribution(beginDate, endDate string) (result ResAnalysisVisitDistribution, err error) {
|
||||
body := map[string]string{
|
||||
"begin_date": beginDate,
|
||||
"end_date": endDate,
|
||||
}
|
||||
response, err := analysis.fetchData(getAnalysisVisitDistributionURL, body)
|
||||
response, err := wxa.fetchData(getAnalysisVisitDistributionURL, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -296,12 +284,12 @@ type ResAnalysisVisitPage struct {
|
||||
}
|
||||
|
||||
// GetAnalysisVisitPage 获取小程序页面访问数据
|
||||
func (analysis *Analysis) GetAnalysisVisitPage(beginDate, endDate string) (result ResAnalysisVisitPage, err error) {
|
||||
func (wxa *MiniProgram) GetAnalysisVisitPage(beginDate, endDate string) (result ResAnalysisVisitPage, err error) {
|
||||
body := map[string]string{
|
||||
"begin_date": beginDate,
|
||||
"end_date": endDate,
|
||||
}
|
||||
response, err := analysis.fetchData(getAnalysisVisitPageURL, body)
|
||||
response, err := wxa.fetchData(getAnalysisVisitPageURL, body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
context2 "context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
code2SessionURL = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
|
||||
|
||||
checkEncryptedDataURL = "https://api.weixin.qq.com/wxa/business/checkencryptedmsg?access_token=%s"
|
||||
|
||||
getPhoneNumber = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s"
|
||||
)
|
||||
|
||||
// Auth 登录/用户信息
|
||||
type Auth struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewAuth new auth
|
||||
func NewAuth(ctx *context.Context) *Auth {
|
||||
return &Auth{ctx}
|
||||
}
|
||||
|
||||
// ResCode2Session 登录凭证校验的返回结果
|
||||
type ResCode2Session struct {
|
||||
util.CommonError
|
||||
|
||||
OpenID string `json:"openid"` // 用户唯一标识
|
||||
SessionKey string `json:"session_key"` // 会话密钥
|
||||
UnionID string `json:"unionid"` // 用户在开放平台的唯一标识符,在满足UnionID下发条件的情况下会返回
|
||||
}
|
||||
|
||||
// RspCheckEncryptedData .
|
||||
type RspCheckEncryptedData struct {
|
||||
util.CommonError
|
||||
|
||||
Vaild bool `json:"vaild"` // 是否是合法的数据
|
||||
CreateTime uint `json:"create_time"` // 加密数据生成的时间戳
|
||||
}
|
||||
|
||||
// Code2Session 登录凭证校验。
|
||||
func (auth *Auth) Code2Session(jsCode string) (result ResCode2Session, err error) {
|
||||
return auth.Code2SessionContext(context2.Background(), jsCode)
|
||||
}
|
||||
|
||||
// Code2SessionContext 登录凭证校验。
|
||||
func (auth *Auth) Code2SessionContext(ctx context2.Context, jsCode string) (result ResCode2Session, err error) {
|
||||
var response []byte
|
||||
if response, err = util.HTTPGetContext(ctx, fmt.Sprintf(code2SessionURL, auth.AppID, auth.AppSecret, jsCode)); err != nil {
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal(response, &result); err != nil {
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("Code2Session error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetPaidUnionID 用户支付完成后,获取该用户的 UnionId,无需用户授权
|
||||
func (auth *Auth) GetPaidUnionID() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// CheckEncryptedData .检查加密信息是否由微信生成(当前只支持手机号加密数据),只能检测最近3天生成的加密数据
|
||||
func (auth *Auth) CheckEncryptedData(encryptedMsgHash string) (result RspCheckEncryptedData, err error) {
|
||||
return auth.CheckEncryptedDataContext(context2.Background(), encryptedMsgHash)
|
||||
}
|
||||
|
||||
// CheckEncryptedDataContext .检查加密信息是否由微信生成(当前只支持手机号加密数据),只能检测最近3天生成的加密数据
|
||||
func (auth *Auth) CheckEncryptedDataContext(ctx context2.Context, encryptedMsgHash string) (result RspCheckEncryptedData, err error) {
|
||||
var response []byte
|
||||
var (
|
||||
at string
|
||||
)
|
||||
if at, err = auth.GetAccessToken(); err != nil {
|
||||
return
|
||||
}
|
||||
if response, err = util.HTTPPostContext(ctx, fmt.Sprintf(checkEncryptedDataURL, at), "encrypted_msg_hash="+encryptedMsgHash); err != nil {
|
||||
return
|
||||
}
|
||||
if err = util.DecodeWithError(response, &result, "CheckEncryptedDataAuth"); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetPhoneNumberResponse 新版获取用户手机号响应结构体
|
||||
type GetPhoneNumberResponse struct {
|
||||
util.CommonError
|
||||
|
||||
PhoneInfo PhoneInfo `json:"phone_info"`
|
||||
}
|
||||
|
||||
// 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"` // 数据水印
|
||||
}
|
||||
|
||||
// GetPhoneNumber 小程序通过code获取用户手机号
|
||||
func (auth *Auth) GetPhoneNumber(code string) (*GetPhoneNumberResponse, error) {
|
||||
var response []byte
|
||||
var (
|
||||
at string
|
||||
err error
|
||||
)
|
||||
if at, err = auth.GetAccessToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body := map[string]interface{}{
|
||||
"code": code,
|
||||
}
|
||||
if response, err = util.PostJSON(fmt.Sprintf(getPhoneNumber, at), body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result GetPhoneNumberResponse
|
||||
if err = util.DecodeWithError(response, &result, "phonenumber.getPhoneNumber"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package business
|
||||
|
||||
import "github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
|
||||
// Business 业务
|
||||
type Business struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewBusiness init
|
||||
func NewBusiness(ctx *context.Context) *Business {
|
||||
return &Business{ctx}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
getPhoneNumberURL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s"
|
||||
)
|
||||
|
||||
// GetPhoneNumberRequest 获取手机号请求
|
||||
type GetPhoneNumberRequest struct {
|
||||
Code string `json:"code"` // 手机号获取凭证
|
||||
}
|
||||
|
||||
// PhoneInfo 手机号信息
|
||||
type PhoneInfo struct {
|
||||
PhoneNumber string `json:"phoneNumber"` // 用户绑定的手机号(国外手机号会有区号)
|
||||
PurePhoneNumber string `json:"purePhoneNumber"` // 没有区号的手机号
|
||||
CountryCode string `json:"countryCode"` // 区号
|
||||
Watermark struct {
|
||||
AppID string `json:"appid"` // 小程序appid
|
||||
Timestamp int64 `json:"timestamp"` // 用户获取手机号操作的时间戳
|
||||
} `json:"watermark"`
|
||||
}
|
||||
|
||||
// GetPhoneNumber code换取用户手机号。 每个code只能使用一次,code的有效期为5min
|
||||
func (business *Business) GetPhoneNumber(in *GetPhoneNumberRequest) (info PhoneInfo, err error) {
|
||||
accessToken, err := business.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(getPhoneNumberURL, accessToken)
|
||||
response, err := util.PostJSON(uri, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
var resp struct {
|
||||
util.CommonError
|
||||
PhoneInfo PhoneInfo `json:"phone_info"`
|
||||
}
|
||||
err = util.DecodeWithError(response, &resp, "business.GetPhoneNumber")
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
info = resp.PhoneInfo
|
||||
return
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// Package config 小程序config配置
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/v2/cache"
|
||||
)
|
||||
|
||||
// Config .config for 小程序
|
||||
type Config struct {
|
||||
AppID string `json:"app_id"` // appid
|
||||
AppSecret string `json:"app_secret"` // appsecret
|
||||
Cache cache.Cache
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package content
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
checkTextURL = "https://api.weixin.qq.com/wxa/msg_sec_check?access_token=%s"
|
||||
checkImageURL = "https://api.weixin.qq.com/wxa/img_sec_check?access_token=%s"
|
||||
)
|
||||
|
||||
// Content 内容安全
|
||||
type Content struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewContent 内容安全接口
|
||||
func NewContent(ctx *context.Context) *Content {
|
||||
return &Content{ctx}
|
||||
}
|
||||
|
||||
// CheckText 检测文字
|
||||
// @text 需要检测的文字
|
||||
// Deprecated
|
||||
// 采用 security.MsgCheckV1 替代,返回值更加丰富
|
||||
func (content *Content) CheckText(text string) error {
|
||||
accessToken, err := content.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := util.PostJSON(
|
||||
fmt.Sprintf(checkTextURL, accessToken),
|
||||
map[string]string{
|
||||
"content": text,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "ContentCheckText")
|
||||
}
|
||||
|
||||
// CheckImage 检测图片
|
||||
// 所传参数为要检测的图片文件的绝对路径,图片格式支持PNG、JPEG、JPG、GIF, 像素不超过 750 x 1334,同时文件大小以不超过 300K 为宜,否则可能报错
|
||||
// @media 图片文件的绝对路径
|
||||
// Deprecated
|
||||
// 采用 security.ImageCheckV1 替代,返回值更加丰富
|
||||
func (content *Content) CheckImage(media string) error {
|
||||
accessToken, err := content.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := util.PostFile(
|
||||
"media",
|
||||
media,
|
||||
fmt.Sprintf(checkImageURL, accessToken),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "ContentCheckImage")
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/v2/credential"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/config"
|
||||
)
|
||||
|
||||
// Context struct
|
||||
type Context struct {
|
||||
*config.Config
|
||||
credential.AccessTokenHandle
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package encryptor
|
||||
package miniprogram
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
@@ -6,23 +6,8 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
)
|
||||
|
||||
// Encryptor struct
|
||||
type Encryptor struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewEncryptor 实例
|
||||
func NewEncryptor(context *context.Context) *Encryptor {
|
||||
basic := new(Encryptor)
|
||||
basic.Context = context
|
||||
return basic
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrAppIDNotMatch appid不匹配
|
||||
ErrAppIDNotMatch = errors.New("app id not match")
|
||||
@@ -34,20 +19,26 @@ var (
|
||||
ErrInvalidPKCS7Padding = errors.New("invalid padding on input")
|
||||
)
|
||||
|
||||
// PlainData 用户信息/手机号信息
|
||||
type PlainData 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"`
|
||||
// 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"`
|
||||
OpenGID string `json:"openGId"`
|
||||
MsgTicket string `json:"msgTicket"`
|
||||
PurePhoneNumber string `json:"purePhoneNumber"`
|
||||
CountryCode string `json:"countryCode"`
|
||||
Watermark struct {
|
||||
@@ -77,8 +68,8 @@ func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) {
|
||||
return data[:len(data)-n], nil
|
||||
}
|
||||
|
||||
// GetCipherText returns slice of the cipher text
|
||||
func GetCipherText(sessionKey, encryptedData, iv string) ([]byte, error) {
|
||||
// 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
|
||||
@@ -91,9 +82,6 @@ func GetCipherText(sessionKey, encryptedData, iv string) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ivBytes) != aes.BlockSize {
|
||||
return nil, fmt.Errorf("bad iv length %d", len(ivBytes))
|
||||
}
|
||||
block, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -108,18 +96,35 @@ func GetCipherText(sessionKey, encryptedData, iv string) ([]byte, error) {
|
||||
}
|
||||
|
||||
// Decrypt 解密数据
|
||||
func (encryptor *Encryptor) Decrypt(sessionKey, encryptedData, iv string) (*PlainData, error) {
|
||||
cipherText, err := GetCipherText(sessionKey, encryptedData, iv)
|
||||
func (wxa *MiniProgram) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo, error) {
|
||||
cipherText, err := getCipherText(sessionKey, encryptedData, iv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var plainData PlainData
|
||||
err = json.Unmarshal(cipherText, &plainData)
|
||||
var userInfo UserInfo
|
||||
err = json.Unmarshal(cipherText, &userInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if plainData.Watermark.AppID != encryptor.AppID {
|
||||
if userInfo.Watermark.AppID != wxa.AppID {
|
||||
return nil, ErrAppIDNotMatch
|
||||
}
|
||||
return &plainData, nil
|
||||
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
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package encryptor
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetCipherText_BadIV(t *testing.T) {
|
||||
keyData := base64.StdEncoding.EncodeToString([]byte("1234567890123456"))
|
||||
badData := base64.StdEncoding.EncodeToString([]byte("1"))
|
||||
_, err := GetCipherText(keyData, badData, badData)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package message
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// MsgType 基本消息类型
|
||||
type MsgType string
|
||||
|
||||
// EventType 事件类型
|
||||
type EventType string
|
||||
|
||||
// InfoType 第三方平台授权事件类型
|
||||
type InfoType string
|
||||
|
||||
const (
|
||||
// MsgTypeText 文本消息
|
||||
MsgTypeText MsgType = "text"
|
||||
// MsgTypeImage 图片消息
|
||||
MsgTypeImage = "image"
|
||||
// MsgTypeLink 图文链接
|
||||
MsgTypeLink = "link"
|
||||
// MsgTypeMiniProgramPage 小程序卡片
|
||||
MsgTypeMiniProgramPage = "miniprogrampage"
|
||||
)
|
||||
|
||||
// CommonToken 消息中通用的结构
|
||||
type CommonToken struct {
|
||||
XMLName xml.Name `xml:"xml"`
|
||||
ToUserName string `xml:"ToUserName"`
|
||||
FromUserName string `xml:"FromUserName"`
|
||||
CreateTime int64 `xml:"CreateTime"`
|
||||
MsgType MsgType `xml:"MsgType"`
|
||||
}
|
||||
|
||||
// MiniProgramMixMessage 小程序回调的消息结构
|
||||
type MiniProgramMixMessage struct {
|
||||
CommonToken
|
||||
|
||||
MsgID int64 `xml:"MsgId"`
|
||||
|
||||
// 文本消息
|
||||
Content string `xml:"Content"`
|
||||
|
||||
// 图片消息
|
||||
PicURL string `xml:"PicUrl"`
|
||||
MediaID string `xml:"MediaId"`
|
||||
|
||||
// 小程序卡片消息
|
||||
Title string `xml:"Title"`
|
||||
AppID string `xml:"AppId"`
|
||||
PagePath string `xml:"PagePath"`
|
||||
ThumbURL string `xml:"ThumbUrl"`
|
||||
ThumbMediaID string `xml:"ThumbMediaId"`
|
||||
|
||||
// 进入会话事件
|
||||
Event string `xml:"Event"`
|
||||
SessionFrom string `xml:"SessionFrom"`
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
customerSendMessage = "https://api.weixin.qq.com/cgi-bin/message/custom/send"
|
||||
)
|
||||
|
||||
// Manager 消息管理者,可以发送消息
|
||||
type Manager struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewCustomerMessageManager 实例化消息管理者
|
||||
func NewCustomerMessageManager(context *context.Context) *Manager {
|
||||
return &Manager{
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
// MediaText 文本消息的文字
|
||||
type MediaText struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// MediaResource 消息使用的临时素材id
|
||||
type MediaResource struct {
|
||||
MediaID string `json:"media_id"`
|
||||
}
|
||||
|
||||
// MediaMiniprogrampage 小程序卡片
|
||||
type MediaMiniprogrampage struct {
|
||||
Title string `json:"title"`
|
||||
Appid string `json:"appid"`
|
||||
Pagepath string `json:"pagepath"`
|
||||
ThumbMediaID string `json:"thumb_media_id"`
|
||||
}
|
||||
|
||||
// MediaLink 发送图文链接
|
||||
type MediaLink struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
URL string `json:"url"`
|
||||
ThumbURL string `json:"thumb_url"`
|
||||
}
|
||||
|
||||
// CustomerMessage 客服消息
|
||||
type CustomerMessage struct {
|
||||
ToUser string `json:"touser"` // 接受者OpenID
|
||||
Msgtype MsgType `json:"msgtype"` // 客服消息类型
|
||||
Text *MediaText `json:"text,omitempty"` // 可选
|
||||
Image *MediaResource `json:"image,omitempty"` // 可选
|
||||
Link *MediaLink `json:"link,omitempty"` // 可选
|
||||
Miniprogrampage *MediaMiniprogrampage `json:"miniprogrampage,omitempty"` // 可选
|
||||
}
|
||||
|
||||
// NewCustomerTextMessage 文本消息结构体构造方法
|
||||
func NewCustomerTextMessage(toUser, text string) *CustomerMessage {
|
||||
return &CustomerMessage{
|
||||
ToUser: toUser,
|
||||
Msgtype: MsgTypeText,
|
||||
Text: &MediaText{
|
||||
Content: text,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewCustomerImgMessage 图片消息的构造方法
|
||||
func NewCustomerImgMessage(toUser, mediaID string) *CustomerMessage {
|
||||
return &CustomerMessage{
|
||||
ToUser: toUser,
|
||||
Msgtype: MsgTypeImage,
|
||||
Image: &MediaResource{
|
||||
MediaID: mediaID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewCustomerLinkMessage 图文链接消息的构造方法
|
||||
func NewCustomerLinkMessage(toUser, title, description, url, thumbURL string) *CustomerMessage {
|
||||
return &CustomerMessage{
|
||||
ToUser: toUser,
|
||||
Msgtype: MsgTypeLink,
|
||||
Link: &MediaLink{
|
||||
Title: title,
|
||||
Description: description,
|
||||
URL: url,
|
||||
ThumbURL: thumbURL,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewCustomerMiniprogrampageMessage 小程序卡片消息的构造方法
|
||||
func NewCustomerMiniprogrampageMessage(toUser, title, pagepath, thumbMediaID string) *CustomerMessage {
|
||||
return &CustomerMessage{
|
||||
ToUser: toUser,
|
||||
Msgtype: MsgTypeMiniProgramPage,
|
||||
Miniprogrampage: &MediaMiniprogrampage{
|
||||
Title: title,
|
||||
Pagepath: pagepath,
|
||||
ThumbMediaID: thumbMediaID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return util.DecodeWithCommonError(response, "SendCustomerMessage")
|
||||
}
|
||||
@@ -1,128 +1,17 @@
|
||||
package miniprogram
|
||||
|
||||
import (
|
||||
"github.com/silenceper/wechat/v2/credential"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/analysis"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/auth"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/business"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/config"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/content"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/encryptor"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/message"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/privacy"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/qrcode"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/riskcontrol"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/security"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/shortlink"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/subscribe"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/tcb"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/urllink"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/urlscheme"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/werun"
|
||||
"github.com/silenceper/wechat/context"
|
||||
)
|
||||
|
||||
// MiniProgram 微信小程序相关API
|
||||
// MiniProgram struct extends context
|
||||
type MiniProgram struct {
|
||||
ctx *context.Context
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewMiniProgram 实例化小程序API
|
||||
func NewMiniProgram(cfg *config.Config) *MiniProgram {
|
||||
defaultAkHandle := credential.NewDefaultAccessToken(cfg.AppID, cfg.AppSecret, credential.CacheKeyMiniProgramPrefix, cfg.Cache)
|
||||
ctx := &context.Context{
|
||||
Config: cfg,
|
||||
AccessTokenHandle: defaultAkHandle,
|
||||
}
|
||||
return &MiniProgram{ctx}
|
||||
}
|
||||
|
||||
// SetAccessTokenHandle 自定义access_token获取方式
|
||||
func (miniProgram *MiniProgram) SetAccessTokenHandle(accessTokenHandle credential.AccessTokenHandle) {
|
||||
miniProgram.ctx.AccessTokenHandle = accessTokenHandle
|
||||
}
|
||||
|
||||
// GetContext get Context
|
||||
func (miniProgram *MiniProgram) GetContext() *context.Context {
|
||||
return miniProgram.ctx
|
||||
}
|
||||
|
||||
// GetEncryptor 小程序加解密
|
||||
func (miniProgram *MiniProgram) GetEncryptor() *encryptor.Encryptor {
|
||||
return encryptor.NewEncryptor(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetAuth 登录/用户信息相关接口
|
||||
func (miniProgram *MiniProgram) GetAuth() *auth.Auth {
|
||||
return auth.NewAuth(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetAnalysis 数据分析
|
||||
func (miniProgram *MiniProgram) GetAnalysis() *analysis.Analysis {
|
||||
return analysis.NewAnalysis(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetBusiness 业务接口
|
||||
func (miniProgram *MiniProgram) GetBusiness() *business.Business {
|
||||
return business.NewBusiness(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetPrivacy 小程序隐私协议相关API
|
||||
func (miniProgram *MiniProgram) GetPrivacy() *privacy.Privacy {
|
||||
return privacy.NewPrivacy(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetQRCode 小程序码相关API
|
||||
func (miniProgram *MiniProgram) GetQRCode() *qrcode.QRCode {
|
||||
return qrcode.NewQRCode(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetTcb 小程序云开发API
|
||||
func (miniProgram *MiniProgram) GetTcb() *tcb.Tcb {
|
||||
return tcb.NewTcb(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetSubscribe 小程序订阅消息
|
||||
func (miniProgram *MiniProgram) GetSubscribe() *subscribe.Subscribe {
|
||||
return subscribe.NewSubscribe(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetCustomerMessage 客服消息接口
|
||||
func (miniProgram *MiniProgram) GetCustomerMessage() *message.Manager {
|
||||
return message.NewCustomerMessageManager(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetWeRun 微信运动接口
|
||||
func (miniProgram *MiniProgram) GetWeRun() *werun.WeRun {
|
||||
return werun.NewWeRun(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetContentSecurity 内容安全接口
|
||||
func (miniProgram *MiniProgram) GetContentSecurity() *content.Content {
|
||||
return content.NewContent(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetURLLink 小程序URL Link接口
|
||||
func (miniProgram *MiniProgram) GetURLLink() *urllink.URLLink {
|
||||
return urllink.NewURLLink(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetRiskControl 安全风控接口
|
||||
func (miniProgram *MiniProgram) GetRiskControl() *riskcontrol.RiskControl {
|
||||
return riskcontrol.NewRiskControl(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetSecurity 内容安全接口
|
||||
func (miniProgram *MiniProgram) GetSecurity() *security.Security {
|
||||
return security.NewSecurity(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetShortLink 小程序短链接口
|
||||
func (miniProgram *MiniProgram) GetShortLink() *shortlink.ShortLink {
|
||||
return shortlink.NewShortLink(miniProgram.ctx)
|
||||
}
|
||||
|
||||
// GetSURLScheme 小程序URL Scheme接口
|
||||
func (miniProgram *MiniProgram) GetSURLScheme() *urlscheme.URLScheme {
|
||||
return urlscheme.NewURLScheme(miniProgram.ctx)
|
||||
// NewMiniProgram 实例化小程序接口
|
||||
func NewMiniProgram(context *context.Context) *MiniProgram {
|
||||
miniProgram := new(MiniProgram)
|
||||
miniProgram.Context = context
|
||||
return miniProgram
|
||||
}
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
package privacy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
// Privacy 小程序授权隐私设置
|
||||
type Privacy struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewPrivacy 实例化小程序隐私接口
|
||||
// 文档地址 https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/privacy_config/set_privacy_setting.html
|
||||
func NewPrivacy(context *context.Context) *Privacy {
|
||||
if context == nil {
|
||||
panic("NewPrivacy got a nil context")
|
||||
}
|
||||
return &Privacy{
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
// OwnerSetting 收集方(开发者)信息配置
|
||||
type OwnerSetting struct {
|
||||
ContactEmail string `json:"contact_email"`
|
||||
ContactPhone string `json:"contact_phone"`
|
||||
ContactQQ string `json:"contact_qq"`
|
||||
ContactWeixin string `json:"contact_weixin"`
|
||||
ExtFileMediaID string `json:"ext_file_media_id"`
|
||||
NoticeMethod string `json:"notice_method"`
|
||||
StoreExpireTimestamp string `json:"store_expire_timestamp"`
|
||||
}
|
||||
|
||||
// SettingItem 收集权限的配置
|
||||
type SettingItem struct {
|
||||
PrivacyKey string `json:"privacy_key"`
|
||||
PrivacyText string `json:"privacy_text"`
|
||||
}
|
||||
|
||||
// SetPrivacySettingRequest 设置权限的请求参数
|
||||
type SetPrivacySettingRequest struct {
|
||||
PrivacyVer int `json:"privacy_ver"`
|
||||
OwnerSetting OwnerSetting `json:"owner_setting"`
|
||||
SettingList []SettingItem `json:"setting_list"`
|
||||
}
|
||||
|
||||
const (
|
||||
setPrivacySettingURL = "https://api.weixin.qq.com/cgi-bin/component/setprivacysetting"
|
||||
getPrivacySettingURL = "https://api.weixin.qq.com/cgi-bin/component/getprivacysetting"
|
||||
uploadPrivacyExtFileURL = "https://api.weixin.qq.com/cgi-bin/component/uploadprivacyextfile"
|
||||
|
||||
// PrivacyV1 用户隐私保护指引的版本,1表示现网版本。
|
||||
PrivacyV1 = 1
|
||||
// PrivacyV2 2表示开发版。默认是2开发版。
|
||||
PrivacyV2 = 2
|
||||
)
|
||||
|
||||
// GetPrivacySettingResponse 获取权限配置的响应结果
|
||||
type GetPrivacySettingResponse struct {
|
||||
util.CommonError
|
||||
CodeExist int `json:"code_exist"`
|
||||
PrivacyList []string `json:"privacy_list"`
|
||||
SettingList []SettingResponseItem `json:"setting_list"`
|
||||
UpdateTime int64 `json:"update_time"`
|
||||
OwnerSetting OwnerSetting `json:"owner_setting"`
|
||||
PrivacyDesc DescList `json:"privacy_desc"`
|
||||
}
|
||||
|
||||
// SettingResponseItem 获取权限设置的响应明细
|
||||
type SettingResponseItem struct {
|
||||
PrivacyKey string `json:"privacy_key"`
|
||||
PrivacyText string `json:"privacy_text"`
|
||||
PrivacyLabel string `json:"privacy_label"`
|
||||
}
|
||||
|
||||
// DescList 权限列表(保持与官方一致)
|
||||
type DescList struct {
|
||||
PrivacyDescList []Desc `json:"privacy_desc_list"`
|
||||
}
|
||||
|
||||
// Desc 权限列表明细(保持与官方一致)
|
||||
type Desc struct {
|
||||
PrivacyDesc string `json:"privacy_desc"`
|
||||
PrivacyKey string `json:"privacy_key"`
|
||||
}
|
||||
|
||||
// GetPrivacySetting 获取小程序权限配置
|
||||
func (s *Privacy) GetPrivacySetting(privacyVer int) (GetPrivacySettingResponse, error) {
|
||||
accessToken, err := s.GetAccessToken()
|
||||
if err != nil {
|
||||
return GetPrivacySettingResponse{}, err
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(fmt.Sprintf("%s?access_token=%s", getPrivacySettingURL, accessToken), map[string]int{
|
||||
"privacy_ver": privacyVer,
|
||||
})
|
||||
if err != nil {
|
||||
return GetPrivacySettingResponse{}, err
|
||||
}
|
||||
// 返回错误信息
|
||||
var result GetPrivacySettingResponse
|
||||
if err = util.DecodeWithError(response, &result, "getprivacysetting"); err != nil {
|
||||
return GetPrivacySettingResponse{}, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SetPrivacySetting 更新小程序权限配置
|
||||
func (s *Privacy) SetPrivacySetting(privacyVer int, ownerSetting OwnerSetting, settingList []SettingItem) error {
|
||||
if privacyVer == PrivacyV1 && len(settingList) > 0 {
|
||||
return errors.New("当privacy_ver传2或者不传时,setting_list是必填;当privacy_ver传1时,该参数不可传")
|
||||
}
|
||||
accessToken, err := s.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(fmt.Sprintf("%s?access_token=%s", setPrivacySettingURL, accessToken), SetPrivacySettingRequest{
|
||||
PrivacyVer: privacyVer,
|
||||
OwnerSetting: ownerSetting,
|
||||
SettingList: settingList,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 返回错误信息
|
||||
if err = util.DecodeWithCommonError(response, "setprivacysetting"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// UploadPrivacyExtFileResponse 上传权限定义模板响应参数
|
||||
type UploadPrivacyExtFileResponse struct {
|
||||
util.CommonError
|
||||
ExtFileMediaID string `json:"ext_file_media_id"`
|
||||
}
|
||||
|
||||
// UploadPrivacyExtFile 上传权限定义模板
|
||||
func (s *Privacy) UploadPrivacyExtFile(fileData []byte) (UploadPrivacyExtFileResponse, error) {
|
||||
accessToken, err := s.GetAccessToken()
|
||||
if err != nil {
|
||||
return UploadPrivacyExtFileResponse{}, err
|
||||
}
|
||||
|
||||
response, err := util.PostJSON(fmt.Sprintf("%s?access_token=%s", uploadPrivacyExtFileURL, accessToken), map[string][]byte{
|
||||
"file": fileData,
|
||||
})
|
||||
if err != nil {
|
||||
return UploadPrivacyExtFileResponse{}, err
|
||||
}
|
||||
|
||||
// 返回错误信息
|
||||
var result UploadPrivacyExtFileResponse
|
||||
if err = util.DecodeWithError(response, &result, "setprivacysetting"); err != nil {
|
||||
return UploadPrivacyExtFileResponse{}, err
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
package qrcode
|
||||
package miniprogram
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -15,16 +14,22 @@ const (
|
||||
getWXACodeUnlimitURL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%s"
|
||||
)
|
||||
|
||||
// QRCode struct
|
||||
type QRCode struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewQRCode 实例
|
||||
func NewQRCode(context *context.Context) *QRCode {
|
||||
qrCode := new(QRCode)
|
||||
qrCode.Context = context
|
||||
return qrCode
|
||||
// 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
|
||||
@@ -34,32 +39,10 @@ type Color struct {
|
||||
B string `json:"b"`
|
||||
}
|
||||
|
||||
// QRCoder 小程序码参数
|
||||
type QRCoder struct {
|
||||
// page 必须是已经发布的小程序存在的页面,根路径前不要填加 /,不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面
|
||||
Page string `json:"page,omitempty"`
|
||||
// path 扫码进入的小程序页面路径
|
||||
Path string `json:"path,omitempty"`
|
||||
// checkPath 检查page 是否存在,为 true 时 page 必须是已经发布的小程序存在的页面(否则报错);为 false 时允许小程序未发布或者 page 不存在, 但page 有数量上限(60000个)请勿滥用,默认true
|
||||
CheckPath *bool `json:"check_path,omitempty"`
|
||||
// width 图片宽度
|
||||
Width int `json:"width,omitempty"`
|
||||
// scene 最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~,其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式)
|
||||
Scene string `json:"scene,omitempty"`
|
||||
// autoColor 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调,默认false
|
||||
AutoColor bool `json:"auto_color,omitempty"`
|
||||
// lineColor AutoColor 为 false 时生效,使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"},十进制表示
|
||||
LineColor *Color `json:"line_color,omitempty"`
|
||||
// isHyaline 是否需要透明底色,默认false
|
||||
IsHyaline bool `json:"is_hyaline,omitempty"`
|
||||
// envVersion 要打开的小程序版本。正式版为 "release",体验版为 "trial",开发版为 "develop"
|
||||
EnvVersion string `json:"env_version,omitempty"`
|
||||
}
|
||||
|
||||
// fetchCode 请求并返回二维码二进制数据
|
||||
func (qrCode *QRCode) fetchCode(urlStr string, body interface{}) (response []byte, err error) {
|
||||
func (wxa *MiniProgram) fetchCode(urlStr string, body interface{}) (response []byte, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = qrCode.GetAccessToken()
|
||||
accessToken, err = wxa.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -78,29 +61,31 @@ func (qrCode *QRCode) fetchCode(urlStr string, body interface{}) (response []byt
|
||||
err = fmt.Errorf("fetchCode error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if contentType == "image/jpeg" {
|
||||
} else if contentType == "image/jpeg" {
|
||||
// 返回文件
|
||||
return response, nil
|
||||
} else {
|
||||
err = fmt.Errorf("fetchCode error : unknown response content type - %v", contentType)
|
||||
return nil, err
|
||||
}
|
||||
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 (qrCode *QRCode) CreateWXAQRCode(coderParams QRCoder) (response []byte, err error) {
|
||||
return qrCode.fetchCode(createWXAQRCodeURL, coderParams)
|
||||
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 (qrCode *QRCode) GetWXACode(coderParams QRCoder) (response []byte, err error) {
|
||||
return qrCode.fetchCode(getWXACodeURL, coderParams)
|
||||
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 (qrCode *QRCode) GetWXACodeUnlimit(coderParams QRCoder) (response []byte, err error) {
|
||||
return qrCode.fetchCode(getWXACodeUnlimitURL, coderParams)
|
||||
func (wxa *MiniProgram) GetWXACodeUnlimit(coderParams QRCoder) (response []byte, err error) {
|
||||
return wxa.fetchCode(getWXACodeUnlimitURL, coderParams)
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package riskcontrol
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
getUserRiskRankURL = "https://api.weixin.qq.com/wxa/getuserriskrank?access_token=%s"
|
||||
)
|
||||
|
||||
// RiskControl 安全风控
|
||||
type RiskControl struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewRiskControl init
|
||||
func NewRiskControl(ctx *context.Context) *RiskControl {
|
||||
return &RiskControl{ctx}
|
||||
}
|
||||
|
||||
// UserRiskRankRequest 获取用户安全等级请求
|
||||
type UserRiskRankRequest struct {
|
||||
AppID string `json:"appid"` // 小程序 app id
|
||||
OpenID string `json:"openid"` // 用户的 openid
|
||||
Scene uint8 `json:"scene"` // 场景值,0:注册,1:营销作弊
|
||||
ClientIP string `json:"client_ip"` // 用户访问源ip
|
||||
|
||||
Mobile string `json:"mobile_no"` // 用户手机号
|
||||
Email string `json:"email_address"` // 用户邮箱地址
|
||||
ExtendedInfo string `json:"extended_info"` // 额外补充信息
|
||||
IsTest bool `json:"is_test"` // false:正式调用,true:测试调用
|
||||
}
|
||||
|
||||
// UserRiskRank 用户安全等级
|
||||
type UserRiskRank struct {
|
||||
util.CommonError
|
||||
UnionID int64 `json:"union_id"` // 唯一请求标识
|
||||
RiskRank uint8 `json:"risk_rank"` // 用户风险等级
|
||||
}
|
||||
|
||||
// GetUserRiskRank 根据提交的用户信息数据获取用户的安全等级 risk_rank,无需用户授权。
|
||||
func (riskControl *RiskControl) GetUserRiskRank(in *UserRiskRankRequest) (res UserRiskRank, err error) {
|
||||
accessToken, err := riskControl.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(getUserRiskRankURL, accessToken)
|
||||
response, err := util.PostJSON(uri, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &res, "GetUserRiskRank")
|
||||
return
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
mediaCheckAsyncURL = "https://api.weixin.qq.com/wxa/media_check_async?access_token=%s"
|
||||
imageCheckURL = "https://api.weixin.qq.com/wxa/img_sec_check?access_token=%s"
|
||||
msgCheckURL = "https://api.weixin.qq.com/wxa/msg_sec_check?access_token=%s"
|
||||
)
|
||||
|
||||
// Security 内容安全
|
||||
type Security struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewSecurity init
|
||||
func NewSecurity(ctx *context.Context) *Security {
|
||||
return &Security{ctx}
|
||||
}
|
||||
|
||||
// MediaCheckAsyncV1Request 图片/音频异步校验请求参数
|
||||
type MediaCheckAsyncV1Request struct {
|
||||
MediaURL string `json:"media_url"` // 要检测的图片或音频的url,支持图片格式包括jpg, jepg, png, bmp, gif(取首帧),支持的音频格式包括mp3, aac, ac3, wma, flac, vorbis, opus, wav
|
||||
MediaType uint8 `json:"media_type"` // 1:音频;2:图片
|
||||
}
|
||||
|
||||
// MediaCheckAsyncV1 异步校验图片/音频是否含有违法违规内容
|
||||
// Deprecated
|
||||
// 在2021年9月1日停止更新,请尽快更新至 2.0 接口。建议使用 MediaCheckAsync
|
||||
func (security *Security) MediaCheckAsyncV1(in *MediaCheckAsyncV1Request) (traceID string, err error) {
|
||||
accessToken, err := security.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(mediaCheckAsyncURL, accessToken)
|
||||
response, err := util.PostJSON(uri, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
var res struct {
|
||||
util.CommonError
|
||||
TraceID string `json:"trace_id"`
|
||||
}
|
||||
err = util.DecodeWithError(response, &res, "MediaCheckAsyncV1")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
traceID = res.TraceID
|
||||
return
|
||||
}
|
||||
|
||||
// MediaCheckAsyncRequest 图片/音频异步校验请求参数
|
||||
type MediaCheckAsyncRequest struct {
|
||||
MediaURL string `json:"media_url"` // 要检测的图片或音频的url,支持图片格式包括jpg, jepg, png, bmp, gif(取首帧),支持的音频格式包括mp3, aac, ac3, wma, flac, vorbis, opus, wav
|
||||
MediaType uint8 `json:"media_type"` // 1:音频;2:图片
|
||||
OpenID string `json:"openid"` // 用户的openid(用户需在近两小时访问过小程序)
|
||||
Scene uint8 `json:"scene"` // 场景枚举值(1 资料;2 评论;3 论坛;4 社交日志)
|
||||
}
|
||||
|
||||
// MediaCheckAsync 异步校验图片/音频是否含有违法违规内容
|
||||
func (security *Security) MediaCheckAsync(in *MediaCheckAsyncRequest) (traceID string, err error) {
|
||||
accessToken, err := security.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
MediaCheckAsyncRequest
|
||||
Version uint `json:"version"` // 接口版本号,2.0版本为固定值2
|
||||
}
|
||||
req.MediaCheckAsyncRequest = *in
|
||||
req.Version = 2
|
||||
|
||||
uri := fmt.Sprintf(mediaCheckAsyncURL, accessToken)
|
||||
response, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
var res struct {
|
||||
util.CommonError
|
||||
TraceID string `json:"trace_id"`
|
||||
}
|
||||
err = util.DecodeWithError(response, &res, "MediaCheckAsync")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
traceID = res.TraceID
|
||||
return
|
||||
}
|
||||
|
||||
// ImageCheckV1 校验一张图片是否含有违法违规内容(同步)
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/framework/security.imgSecCheck.html
|
||||
// Deprecated
|
||||
// 在2021年9月1日停止更新。建议使用 MediaCheckAsync
|
||||
func (security *Security) ImageCheckV1(filename string) (err error) {
|
||||
accessToken, err := security.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf(imageCheckURL, accessToken)
|
||||
response, err := util.PostFile("media", filename, uri)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
return util.DecodeWithCommonError(response, "ImageCheckV1")
|
||||
}
|
||||
|
||||
// CheckSuggest 检查建议
|
||||
type CheckSuggest string
|
||||
|
||||
const (
|
||||
// CheckSuggestRisky 违规风险建议
|
||||
CheckSuggestRisky CheckSuggest = "risky"
|
||||
// CheckSuggestPass 安全
|
||||
CheckSuggestPass CheckSuggest = "pass"
|
||||
// CheckSuggestReview 需要审查
|
||||
CheckSuggestReview CheckSuggest = "review"
|
||||
)
|
||||
|
||||
// MsgScene 文本场景
|
||||
type MsgScene uint8
|
||||
|
||||
const (
|
||||
// MsgSceneMaterial 资料文件检查场景
|
||||
MsgSceneMaterial MsgScene = iota + 1
|
||||
// MsgSceneComment 评论
|
||||
MsgSceneComment
|
||||
// MsgSceneForum 论坛
|
||||
MsgSceneForum
|
||||
// MsgSceneSocialLog 社交日志
|
||||
MsgSceneSocialLog
|
||||
)
|
||||
|
||||
// CheckLabel 检查命中标签
|
||||
type CheckLabel int
|
||||
|
||||
func (cl CheckLabel) String() string {
|
||||
switch cl {
|
||||
case 100:
|
||||
return "正常"
|
||||
case 10001:
|
||||
return "广告"
|
||||
case 20001:
|
||||
return "时政"
|
||||
case 20002:
|
||||
return "色情"
|
||||
case 20003:
|
||||
return "辱骂"
|
||||
case 20006:
|
||||
return "违法犯罪"
|
||||
case 20008:
|
||||
return "欺诈"
|
||||
case 20012:
|
||||
return "低俗"
|
||||
case 20013:
|
||||
return "版权"
|
||||
case 21000:
|
||||
return "其他"
|
||||
default:
|
||||
return strconv.Itoa(int(cl))
|
||||
}
|
||||
}
|
||||
|
||||
// MsgCheckRequest 文本检查请求
|
||||
type MsgCheckRequest struct {
|
||||
OpenID string `json:"openid"` // 用户的openid(用户需在近两小时访问过小程序)
|
||||
Scene MsgScene `json:"scene"` // 场景枚举值(1 资料;2 评论;3 论坛;4 社交日志)
|
||||
Content string `json:"content"` // 需检测的文本内容,文本字数的上限为 2500 字,需使用 UTF-8 编码
|
||||
Nickname string `json:"nickname"` // (非必填)用户昵称,需使用UTF-8编码
|
||||
Title string `json:"title"` // (非必填)文本标题,需使用UTF-8编码
|
||||
Signature string `json:"signature"` // (非必填)个性签名,该参数仅在资料类场景有效(scene=1),需使用UTF-8编码
|
||||
}
|
||||
|
||||
// MsgCheckResponse 文本检查响应
|
||||
type MsgCheckResponse struct {
|
||||
util.CommonError
|
||||
TraceID string `json:"trace_id"` // 唯一请求标识
|
||||
Result struct {
|
||||
Suggest CheckSuggest `json:"suggest"` // 建议
|
||||
Label CheckLabel `json:"label"` // 命中标签
|
||||
} `json:"result"` // 综合结果
|
||||
Detail []struct {
|
||||
ErrCode int64 `json:"errcode"` // 错误码,仅当该值为0时,该项结果有效
|
||||
Strategy string `json:"strategy"` // 策略类型
|
||||
Suggest string `json:"suggest"` // 建议
|
||||
Label CheckLabel `json:"label"` // 命中标签
|
||||
Prob uint `json:"prob"` // 置信度。0-100,越高代表越有可能属于当前返回的标签(label)
|
||||
Keyword string `json:"keyword"` // 命中的自定义关键词
|
||||
} `json:"detail"` // 详细检测结果
|
||||
}
|
||||
|
||||
// MsgCheckV1 检查一段文本是否含有违法违规内容
|
||||
// Deprecated
|
||||
// 在2021年9月1日停止更新,请尽快更新至 2.0 接口。建议使用 MsgCheck
|
||||
func (security *Security) MsgCheckV1(content string) (res MsgCheckResponse, err error) {
|
||||
accessToken, err := security.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
req.Content = content
|
||||
|
||||
uri := fmt.Sprintf(msgCheckURL, accessToken)
|
||||
response, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &res, "security.MsgCheckV1")
|
||||
return
|
||||
}
|
||||
|
||||
// MsgCheck 检查一段文本是否含有违法违规内容
|
||||
func (security *Security) MsgCheck(in *MsgCheckRequest) (res MsgCheckResponse, err error) {
|
||||
accessToken, err := security.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
MsgCheckRequest
|
||||
Version uint `json:"version"`
|
||||
}
|
||||
req.MsgCheckRequest = *in
|
||||
req.Version = 2
|
||||
|
||||
uri := fmt.Sprintf(msgCheckURL, accessToken)
|
||||
response, err := util.PostJSON(uri, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
err = util.DecodeWithError(response, &res, "security.MsgCheck")
|
||||
return
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package shortlink
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
generateShortLinkURL = "https://api.weixin.qq.com/wxa/genwxashortlink?access_token=%s"
|
||||
)
|
||||
|
||||
// ShortLink 短链接
|
||||
type ShortLink struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewShortLink 实例
|
||||
func NewShortLink(ctx *context.Context) *ShortLink {
|
||||
return &ShortLink{ctx}
|
||||
}
|
||||
|
||||
// ShortLinker 请求结构体
|
||||
type ShortLinker struct {
|
||||
|
||||
// pageUrl 通过 Short Link 进入的小程序页面路径,必须是已经发布的小程序存在的页面,可携带 query,最大1024个字符
|
||||
PageURL string `json:"page_url"`
|
||||
|
||||
// pageTitle 页面标题,不能包含违法信息,超过20字符会用... 截断代替
|
||||
PageTitle string `json:"page_title"`
|
||||
|
||||
// isPermanent 生成的 Short Link 类型,短期有效:false,永久有效:true
|
||||
IsPermanent bool `json:"is_permanent,omitempty"`
|
||||
}
|
||||
|
||||
// resShortLinker 返回结构体
|
||||
type resShortLinker struct {
|
||||
// 通用错误
|
||||
*util.CommonError
|
||||
|
||||
// 返回的 shortLink
|
||||
Link string `json:"link"`
|
||||
}
|
||||
|
||||
// Generate 生成 shortLink
|
||||
func (shortLink *ShortLink) generate(shortLinkParams ShortLinker) (string, error) {
|
||||
var accessToken string
|
||||
accessToken, err := shortLink.GetAccessToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
urlStr := fmt.Sprintf(generateShortLinkURL, accessToken)
|
||||
response, err := util.PostJSON(urlStr, shortLinkParams)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
var res resShortLinker
|
||||
err = util.DecodeWithError(response, &res, "GenerateShortLink")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return res.Link, nil
|
||||
}
|
||||
|
||||
// GenerateShortLinkPermanent 生成永久shortLink
|
||||
func (shortLink *ShortLink) GenerateShortLinkPermanent(PageURL, pageTitle string) (string, error) {
|
||||
return shortLink.generate(ShortLinker{
|
||||
PageURL: PageURL,
|
||||
PageTitle: pageTitle,
|
||||
IsPermanent: true,
|
||||
})
|
||||
}
|
||||
|
||||
// GenerateShortLinkTemp 生成临时shortLink
|
||||
func (shortLink *ShortLink) GenerateShortLinkTemp(PageURL, pageTitle string) (string, error) {
|
||||
return shortLink.generate(ShortLinker{
|
||||
PageURL: PageURL,
|
||||
PageTitle: pageTitle,
|
||||
IsPermanent: false,
|
||||
})
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
package subscribe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// 发送订阅消息
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html
|
||||
subscribeSendURL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send"
|
||||
|
||||
// 获取当前帐号下的个人模板列表
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.getTemplateList.html
|
||||
getTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/gettemplate"
|
||||
|
||||
// 添加订阅模板
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.addTemplate.html
|
||||
addTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/addtemplate"
|
||||
|
||||
// 删除私有模板
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.deleteTemplate.html
|
||||
delTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/deltemplate"
|
||||
|
||||
// 统一服务消息
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/uniform-message/uniformMessage.send.html
|
||||
uniformMessageSend = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send"
|
||||
)
|
||||
|
||||
// Subscribe 订阅消息
|
||||
type Subscribe struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewSubscribe 实例化
|
||||
func NewSubscribe(ctx *context.Context) *Subscribe {
|
||||
return &Subscribe{Context: ctx}
|
||||
}
|
||||
|
||||
// Message 订阅消息请求参数
|
||||
type Message struct {
|
||||
ToUser string `json:"touser"` // 必选,接收者(用户)的 openid
|
||||
TemplateID string `json:"template_id"` // 必选,所需下发的订阅模板id
|
||||
Page string `json:"page"` // 可选,点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转。
|
||||
Data map[string]*DataItem `json:"data"` // 必选, 模板内容
|
||||
MiniprogramState string `json:"miniprogram_state"` // 可选,跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版
|
||||
Lang string `json:"lang"` // 入小程序查看”的语言类型,支持zh_CN(简体中文)、en_US(英文)、zh_HK(繁体中文)、zh_TW(繁体中文),默认为zh_CN
|
||||
}
|
||||
|
||||
// DataItem 模版内某个 .DATA 的值
|
||||
type DataItem struct {
|
||||
Value interface{} `json:"value"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
// TemplateItem template item
|
||||
type TemplateItem struct {
|
||||
PriTmplID string `json:"priTmplId"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Example string `json:"example"`
|
||||
Type int64 `json:"type"`
|
||||
}
|
||||
|
||||
// TemplateList template list
|
||||
type TemplateList struct {
|
||||
util.CommonError
|
||||
Data []TemplateItem `json:"data"`
|
||||
}
|
||||
|
||||
// Send 发送订阅消息
|
||||
func (s *Subscribe) Send(msg *Message) (err error) {
|
||||
var accessToken string
|
||||
accessToken, err = s.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", subscribeSendURL, accessToken)
|
||||
response, err := util.PostJSON(uri, msg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "Send")
|
||||
}
|
||||
|
||||
// ListTemplates 获取当前帐号下的个人模板列表
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.getTemplateList.html
|
||||
func (s *Subscribe) ListTemplates() (*TemplateList, error) {
|
||||
accessToken, err := s.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", getTemplateURL, accessToken)
|
||||
response, err := util.HTTPGet(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
templateList := TemplateList{}
|
||||
err = util.DecodeWithError(response, &templateList, "ListTemplates")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &templateList, nil
|
||||
}
|
||||
|
||||
// UniformMessage 统一服务消息
|
||||
type UniformMessage struct {
|
||||
ToUser string `json:"touser"`
|
||||
WeappTemplateMsg struct {
|
||||
TemplateID string `json:"template_id"`
|
||||
Page string `json:"page"`
|
||||
FormID string `json:"form_id"`
|
||||
Data map[string]*DataItem `json:"data"`
|
||||
EmphasisKeyword string `json:"emphasis_keyword"`
|
||||
} `json:"weapp_template_msg"`
|
||||
MpTemplateMsg struct {
|
||||
Appid string `json:"appid"`
|
||||
TemplateID string `json:"template_id"`
|
||||
URL string `json:"url"`
|
||||
Miniprogram struct {
|
||||
Appid string `json:"appid"`
|
||||
Pagepath string `json:"page"`
|
||||
} `json:"miniprogram"`
|
||||
Data map[string]*DataItem `json:"data"`
|
||||
} `json:"mp_template_msg"`
|
||||
}
|
||||
|
||||
// UniformSend 发送统一服务消息
|
||||
func (s *Subscribe) UniformSend(msg *UniformMessage) (err error) {
|
||||
var accessToken string
|
||||
accessToken, err = s.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", uniformMessageSend, accessToken)
|
||||
response, err := util.PostJSON(uri, msg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "UniformSend")
|
||||
}
|
||||
|
||||
type resSubscribeAdd struct {
|
||||
util.CommonError
|
||||
|
||||
TemplateID string `json:"priTmplId"`
|
||||
}
|
||||
|
||||
// Add 添加订阅消息模板
|
||||
func (s *Subscribe) Add(ShortID string, kidList []int, sceneDesc string) (templateID string, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = s.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var msg = struct {
|
||||
TemplateIDShort string `json:"tid"`
|
||||
SceneDesc string `json:"sceneDesc"`
|
||||
KidList []int `json:"kidList"`
|
||||
}{TemplateIDShort: ShortID, SceneDesc: sceneDesc, KidList: kidList}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", addTemplateURL, accessToken)
|
||||
var response []byte
|
||||
response, err = util.PostJSON(uri, msg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var result resSubscribeAdd
|
||||
err = util.DecodeWithError(response, &result, "AddSubscribe")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
templateID = result.TemplateID
|
||||
return
|
||||
}
|
||||
|
||||
// Delete 删除私有模板
|
||||
func (s *Subscribe) Delete(templateID string) (err error) {
|
||||
var accessToken string
|
||||
accessToken, err = s.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var msg = struct {
|
||||
TemplateID string `json:"priTmplId"`
|
||||
}{TemplateID: templateID}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", delTemplateURL, accessToken)
|
||||
var response []byte
|
||||
response, err = util.PostJSON(uri, msg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return util.DecodeWithCommonError(response, "DeleteSubscribe")
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package tcb
|
||||
|
||||
import "github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
|
||||
// Tcb Tencent Cloud Base
|
||||
type Tcb struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewTcb new Tencent Cloud Base
|
||||
func NewTcb(context *context.Context) *Tcb {
|
||||
return &Tcb{
|
||||
context,
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package urllink
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
// URLLink 小程序 URL Link
|
||||
type URLLink struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewURLLink 实例化
|
||||
func NewURLLink(ctx *context.Context) *URLLink {
|
||||
return &URLLink{Context: ctx}
|
||||
}
|
||||
|
||||
const generateURL = "https://api.weixin.qq.com/wxa/generate_urllink"
|
||||
|
||||
// TExpireType 失效类型 (指定时间戳/指定间隔)
|
||||
type TExpireType int
|
||||
|
||||
const (
|
||||
// ExpireTypeTime 指定时间戳后失效
|
||||
ExpireTypeTime TExpireType = 0
|
||||
|
||||
// ExpireTypeInterval 间隔指定天数后失效
|
||||
ExpireTypeInterval TExpireType = 1
|
||||
)
|
||||
|
||||
// ULParams 请求参数
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-link/urllink.generate.html#请求参数
|
||||
type ULParams struct {
|
||||
Path string `json:"path"`
|
||||
Query string `json:"query"`
|
||||
// envVersion 要打开的小程序版本。正式版为 "release",体验版为 "trial",开发版为 "develop"
|
||||
EnvVersion string `json:"env_version,omitempty"`
|
||||
IsExpire bool `json:"is_expire"`
|
||||
ExpireType TExpireType `json:"expire_type"`
|
||||
ExpireTime int64 `json:"expire_time"`
|
||||
ExpireInterval int `json:"expire_interval"`
|
||||
}
|
||||
|
||||
// ULResult 返回的结果
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-link/urllink.generate.html#返回值
|
||||
type ULResult struct {
|
||||
util.CommonError
|
||||
|
||||
URLLink string `json:"url_link"`
|
||||
}
|
||||
|
||||
// Generate 生成url link
|
||||
func (u *URLLink) Generate(params *ULParams) (string, error) {
|
||||
accessToken, err := u.GetAccessToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", generateURL, accessToken)
|
||||
response, err := util.PostJSON(uri, params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var resp ULResult
|
||||
err = util.DecodeWithError(response, &resp, "URLLink.Generate")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.URLLink, nil
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package urlscheme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
querySchemeURL = "https://api.weixin.qq.com/wxa/queryscheme?access_token=%s"
|
||||
)
|
||||
|
||||
// QueryScheme 获取小程序访问scheme
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数
|
||||
type QueryScheme struct {
|
||||
// 小程序 scheme 码
|
||||
Scheme string `json:"scheme"`
|
||||
}
|
||||
|
||||
// SchemeInfo scheme 配置
|
||||
type SchemeInfo struct {
|
||||
// 小程序 appid。
|
||||
AppID string `json:"appid"`
|
||||
// 小程序页面路径。
|
||||
Path string `json:"path"`
|
||||
// 小程序页面query。
|
||||
Query string `json:"query"`
|
||||
// 创建时间,为 Unix 时间戳。
|
||||
CreateTime int64 `json:"create_time"`
|
||||
// 到期失效时间,为 Unix 时间戳,0 表示永久生效
|
||||
ExpireTime int64 `json:"expire_time"`
|
||||
// 要打开的小程序版本。正式版为"release",体验版为"trial",开发版为"develop"。
|
||||
EnvVersion EnvVersion `json:"env_version"`
|
||||
}
|
||||
|
||||
// resQueryScheme 返回结构体
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.query.html#参数
|
||||
type resQueryScheme struct {
|
||||
// 通用错误
|
||||
*util.CommonError
|
||||
// scheme 配置
|
||||
SchemeInfo SchemeInfo `json:"scheme_info"`
|
||||
// 访问该链接的openid,没有用户访问过则为空字符串
|
||||
VisitOpenid string `json:"visit_openid"`
|
||||
}
|
||||
|
||||
// QueryScheme 查询小程序 scheme 码
|
||||
func (u *URLScheme) QueryScheme(querySchemeParams QueryScheme) (schemeInfo SchemeInfo, visitOpenid string, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = u.GetAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
urlStr := fmt.Sprintf(querySchemeURL, accessToken)
|
||||
var response []byte
|
||||
response, err = util.PostJSON(urlStr, querySchemeParams)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用通用方法返回错误
|
||||
var res resQueryScheme
|
||||
err = util.DecodeWithError(response, &res, "QueryScheme")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return res.SchemeInfo, res.VisitOpenid, nil
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package urlscheme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
// URLScheme 小程序 URL Scheme
|
||||
type URLScheme struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewURLScheme 实例化
|
||||
func NewURLScheme(ctx *context.Context) *URLScheme {
|
||||
return &URLScheme{Context: ctx}
|
||||
}
|
||||
|
||||
const generateURL = "https://api.weixin.qq.com/wxa/generatescheme"
|
||||
|
||||
// TExpireType 失效类型 (指定时间戳/指定间隔)
|
||||
type TExpireType int
|
||||
|
||||
// EnvVersion 要打开的小程序版本
|
||||
type EnvVersion string
|
||||
|
||||
const (
|
||||
// ExpireTypeTime 指定时间戳后失效
|
||||
ExpireTypeTime TExpireType = 0
|
||||
// ExpireTypeInterval 间隔指定天数后失效
|
||||
ExpireTypeInterval TExpireType = 1
|
||||
|
||||
// EnvVersionRelease 正式版为"release"
|
||||
EnvVersionRelease EnvVersion = "release"
|
||||
// EnvVersionTrial 体验版为"trial"
|
||||
EnvVersionTrial EnvVersion = "trial"
|
||||
// EnvVersionDevelop 开发版为"develop"
|
||||
EnvVersionDevelop EnvVersion = "develop"
|
||||
)
|
||||
|
||||
// JumpWxa 跳转到的目标小程序信息
|
||||
type JumpWxa struct {
|
||||
Path string `json:"path"`
|
||||
Query string `json:"query"`
|
||||
// envVersion 要打开的小程序版本。正式版为 "release",体验版为 "trial",开发版为 "develop"
|
||||
EnvVersion EnvVersion `json:"env_version,omitempty"`
|
||||
}
|
||||
|
||||
// USParams 请求参数
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.generate.html#请求参数
|
||||
type USParams struct {
|
||||
JumpWxa *JumpWxa `json:"jump_wxa"`
|
||||
ExpireType TExpireType `json:"expire_type"`
|
||||
ExpireTime int64 `json:"expire_time"`
|
||||
ExpireInterval int `json:"expire_interval"`
|
||||
}
|
||||
|
||||
// USResult 返回的结果
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.generate.html#返回值
|
||||
type USResult struct {
|
||||
util.CommonError
|
||||
|
||||
OpenLink string `json:"openlink"`
|
||||
}
|
||||
|
||||
// Generate 生成url link
|
||||
func (u *URLScheme) Generate(params *USParams) (string, error) {
|
||||
accessToken, err := u.GetAccessToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("%s?access_token=%s", generateURL, accessToken)
|
||||
response, err := util.PostJSON(uri, params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var resp USResult
|
||||
err = util.DecodeWithError(response, &resp, "URLScheme.Generate")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.OpenLink, nil
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package werun
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||
"github.com/silenceper/wechat/v2/miniprogram/encryptor"
|
||||
)
|
||||
|
||||
// WeRun 微信运动
|
||||
type WeRun struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// Data 微信运动数据
|
||||
type Data struct {
|
||||
StepInfoList []struct {
|
||||
Timestamp int `json:"timestamp"`
|
||||
Step int `json:"step"`
|
||||
} `json:"stepInfoList"`
|
||||
}
|
||||
|
||||
// NewWeRun 实例化
|
||||
func NewWeRun(ctx *context.Context) *WeRun {
|
||||
return &WeRun{Context: ctx}
|
||||
}
|
||||
|
||||
// GetWeRunData 解密数据
|
||||
func (werun *WeRun) GetWeRunData(sessionKey, encryptedData, iv string) (*Data, error) {
|
||||
cipherText, err := encryptor.GetCipherText(sessionKey, encryptedData, iv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var weRunData Data
|
||||
err = json.Unmarshal(cipherText, &weRunData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &weRunData, nil
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
"github.com/silenceper/wechat/context"
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -15,42 +15,42 @@ const (
|
||||
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=%s"
|
||||
userInfoURL = "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN"
|
||||
checkAccessTokenURL = "https://api.weixin.qq.com/sns/auth?access_token=%s&openid=%s"
|
||||
)
|
||||
|
||||
// Oauth 保存用户授权信息
|
||||
//Oauth 保存用户授权信息
|
||||
type Oauth struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewOauth 实例化授权信息
|
||||
//NewOauth 实例化授权信息
|
||||
func NewOauth(context *context.Context) *Oauth {
|
||||
auth := new(Oauth)
|
||||
auth.Context = context
|
||||
return auth
|
||||
}
|
||||
|
||||
// GetRedirectURL 获取跳转的url地址
|
||||
//GetRedirectURL 获取跳转的url地址
|
||||
func (oauth *Oauth) GetRedirectURL(redirectURI, scope, state string) (string, error) {
|
||||
// url encode
|
||||
//url encode
|
||||
urlStr := url.QueryEscape(redirectURI)
|
||||
return fmt.Sprintf(redirectOauthURL, oauth.AppID, urlStr, scope, state), nil
|
||||
}
|
||||
|
||||
// GetWebAppRedirectURL 获取网页应用跳转的url地址
|
||||
//GetWebAppRedirectURL 获取网页应用跳转的url地址
|
||||
func (oauth *Oauth) GetWebAppRedirectURL(redirectURI, scope, state string) (string, error) {
|
||||
urlStr := url.QueryEscape(redirectURI)
|
||||
return fmt.Sprintf(webAppRedirectOauthURL, oauth.AppID, urlStr, scope, state), nil
|
||||
}
|
||||
|
||||
// Redirect 跳转到网页授权
|
||||
//Redirect 跳转到网页授权
|
||||
func (oauth *Oauth) Redirect(writer http.ResponseWriter, req *http.Request, redirectURI, scope, state string) error {
|
||||
location, err := oauth.GetRedirectURL(redirectURI, scope, state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
http.Redirect(writer, req, location, http.StatusFound)
|
||||
http.Redirect(writer, req, location, 302)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ func (oauth *Oauth) GetUserAccessToken(code string) (result ResAccessToken, err
|
||||
return
|
||||
}
|
||||
|
||||
// RefreshAccessToken 刷新access_token
|
||||
//RefreshAccessToken 刷新access_token
|
||||
func (oauth *Oauth) RefreshAccessToken(refreshToken string) (result ResAccessToken, err error) {
|
||||
urlStr := fmt.Sprintf(refreshAccessTokenURL, oauth.AppID, refreshToken)
|
||||
var response []byte
|
||||
@@ -107,7 +107,7 @@ func (oauth *Oauth) RefreshAccessToken(refreshToken string) (result ResAccessTok
|
||||
return
|
||||
}
|
||||
|
||||
// CheckAccessToken 检验access_token是否有效
|
||||
//CheckAccessToken 检验access_token是否有效
|
||||
func (oauth *Oauth) CheckAccessToken(accessToken, openID string) (b bool, err error) {
|
||||
urlStr := fmt.Sprintf(checkAccessTokenURL, accessToken, openID)
|
||||
var response []byte
|
||||
@@ -128,7 +128,7 @@ func (oauth *Oauth) CheckAccessToken(accessToken, openID string) (b bool, err er
|
||||
return
|
||||
}
|
||||
|
||||
// UserInfo 用户授权获取到用户信息
|
||||
//UserInfo 用户授权获取到用户信息
|
||||
type UserInfo struct {
|
||||
util.CommonError
|
||||
|
||||
@@ -143,12 +143,9 @@ type UserInfo struct {
|
||||
Unionid string `json:"unionid"`
|
||||
}
|
||||
|
||||
// GetUserInfo 如果scope为 snsapi_userinfo 则可以通过此方法获取到用户基本信息
|
||||
func (oauth *Oauth) GetUserInfo(accessToken, openID, lang string) (result UserInfo, err error) {
|
||||
if lang == "" {
|
||||
lang = "zh_CN"
|
||||
}
|
||||
urlStr := fmt.Sprintf(userInfoURL, accessToken, openID, lang)
|
||||
//GetUserInfo 如果scope为 snsapi_userinfo 则可以通过此方法获取到用户基本信息
|
||||
func (oauth *Oauth) GetUserInfo(accessToken, openID string) (result UserInfo, err error) {
|
||||
urlStr := fmt.Sprintf(userInfoURL, accessToken, openID)
|
||||
var response []byte
|
||||
response, err = util.HTTPGet(urlStr)
|
||||
if err != nil {
|
||||
95
oauth/qy_oauth.go
Normal file
95
oauth/qy_oauth.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/silenceper/wechat/util"
|
||||
)
|
||||
|
||||
var (
|
||||
qyRedirectOauthURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&agentid=%s&state=%s#wechat_redirect"
|
||||
qyUserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=%s&code=%s"
|
||||
qyUserDetailURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserdetail"
|
||||
)
|
||||
|
||||
//GetQyRedirectURL 获取企业微信跳转的url地址
|
||||
func (oauth *Oauth) GetQyRedirectURL(redirectURI, agentid, scope, state string) (string, error) {
|
||||
//url encode
|
||||
urlStr := url.QueryEscape(redirectURI)
|
||||
return fmt.Sprintf(qyRedirectOauthURL, oauth.AppID, urlStr, scope, agentid, state), nil
|
||||
}
|
||||
|
||||
//QyUserInfo 用户授权获取到用户信息
|
||||
type QyUserInfo struct {
|
||||
util.CommonError
|
||||
|
||||
UserID string `json:"UserId"`
|
||||
DeviceID string `json:"DeviceId"`
|
||||
UserTicket string `json:"user_ticket"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
//GetQyUserInfoByCode 根据code获取企业user_info
|
||||
func (oauth *Oauth) GetQyUserInfoByCode(code string) (result QyUserInfo, err error) {
|
||||
qyAccessToken, e := oauth.GetQyAccessToken()
|
||||
if e != nil {
|
||||
err = e
|
||||
return
|
||||
}
|
||||
urlStr := fmt.Sprintf(qyUserInfoURL, qyAccessToken, code)
|
||||
var response []byte
|
||||
response, err = util.HTTPGet(urlStr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("GetQyUserInfoByCode error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//QyUserDetail 到用户详情
|
||||
type QyUserDetail struct {
|
||||
util.CommonError
|
||||
|
||||
UserID string `json:"UserId"`
|
||||
Name string `json:"name"`
|
||||
Mobile string `json:"mobile"`
|
||||
Gender string `json:"gender"`
|
||||
Email string `json:"email"`
|
||||
Avatar string `json:"avatar"`
|
||||
QrCode string `json:"qr_code"`
|
||||
}
|
||||
|
||||
//GetQyUserDetailUserTicket 根据user_ticket获取到用户详情
|
||||
func (oauth *Oauth) GetQyUserDetailUserTicket(userTicket string) (result QyUserDetail, err error) {
|
||||
var qyAccessToken string
|
||||
qyAccessToken, err = oauth.GetQyAccessToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
uri := fmt.Sprintf("%s?access_token=%s", qyUserDetailURL, qyAccessToken)
|
||||
var response []byte
|
||||
response, err = util.PostJSON(uri, map[string]string{
|
||||
"user_ticket": userTicket,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("GetQyUserDetailUserTicket Error , errcode=%d , errmsg=%s", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# 微信公众号
|
||||
|
||||
[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html)
|
||||
|
||||
## 快速入门
|
||||
@@ -1,84 +0,0 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||
"github.com/silenceper/wechat/v2/util"
|
||||
)
|
||||
|
||||
var (
|
||||
// 获取微信服务器IP地址
|
||||
// 文档:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_the_WeChat_server_IP_address.html
|
||||
getCallbackIPURL = "https://api.weixin.qq.com/cgi-bin/getcallbackip"
|
||||
getAPIDomainIPURL = "https://api.weixin.qq.com/cgi-bin/get_api_domain_ip"
|
||||
|
||||
// 清理接口调用次数
|
||||
clearQuotaURL = "https://api.weixin.qq.com/cgi-bin/clear_quota"
|
||||
)
|
||||
|
||||
// Basic struct
|
||||
type Basic struct {
|
||||
*context.Context
|
||||
}
|
||||
|
||||
// NewBasic 实例
|
||||
func NewBasic(context *context.Context) *Basic {
|
||||
basic := new(Basic)
|
||||
basic.Context = context
|
||||
return basic
|
||||
}
|
||||
|
||||
// IPListRes 获取微信服务器IP地址 返回结果
|
||||
type IPListRes struct {
|
||||
util.CommonError
|
||||
IPList []string `json:"ip_list"`
|
||||
}
|
||||
|
||||
// GetCallbackIP 获取微信callback IP地址
|
||||
func (basic *Basic) GetCallbackIP() ([]string, error) {
|
||||
ak, err := basic.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := fmt.Sprintf("%s?access_token=%s", getCallbackIPURL, ak)
|
||||
data, err := util.HTTPGet(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ipListRes := &IPListRes{}
|
||||
err = util.DecodeWithError(data, ipListRes, "GetCallbackIP")
|
||||
return ipListRes.IPList, err
|
||||
}
|
||||
|
||||
// GetAPIDomainIP 获取微信API接口 IP地址
|
||||
func (basic *Basic) GetAPIDomainIP() ([]string, error) {
|
||||
ak, err := basic.GetAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := fmt.Sprintf("%s?access_token=%s", getAPIDomainIPURL, ak)
|
||||
data, err := util.HTTPGet(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ipListRes := &IPListRes{}
|
||||
err = util.DecodeWithError(data, ipListRes, "GetAPIDomainIP")
|
||||
return ipListRes.IPList, err
|
||||
}
|
||||
|
||||
// ClearQuota 清理接口调用次数
|
||||
func (basic *Basic) ClearQuota() error {
|
||||
ak, err := basic.GetAccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := fmt.Sprintf("%s?access_token=%s", clearQuotaURL, ak)
|
||||
data, err := util.PostJSON(url, map[string]string{
|
||||
"appid": basic.AppID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.DecodeWithCommonError(data, "ClearQuota")
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user