1
0
mirror of https://github.com/silenceper/wechat.git synced 2026-02-04 21:02:25 +08:00

Compare commits

..

30 Commits

Author SHA1 Message Date
silenceper
5b578ebd3c Update README.md 2021-08-26 10:40:21 +08:00
hewen
843ed3fa8a material add BatchGetMaterial (#258)
* material add BatchGetMaterial

1. fix material golint error

2. BatchGetMaterial use util.DecodeWithError check error

Co-authored-by: hewen <hewen@liao.com>
2020-05-28 16:20:22 +08:00
silenceper
8762af2441 Merge pull request #252 from silenceper/silenceper-patch-1
Update README.md
2020-05-25 10:24:00 +08:00
silenceper
bfd23056eb Update README.md 2020-05-25 10:23:24 +08:00
silenceper
fedcd371d0 Merge pull request #250 from silenceper/silenceper-patch-1
Update README.md
2020-05-24 22:16:42 +08:00
silenceper
6569d47301 Update README.md 2020-05-24 22:16:06 +08:00
silenceper
05e23e0d88 Merge pull request #237 from silenceper/fix-redis-import
更新依赖
2020-05-22 12:33:50 +08:00
silenceper
6279dadd29 更新依赖 2020-05-22 12:27:26 +08:00
silenceper
a357c82080 Merge pull request #236 from silenceper/revert-232-fix-go.mod
Revert "更新go.mod,redis依赖"
2020-05-22 12:23:56 +08:00
silenceper
ae271960e2 Revert "更新go.mod,redis依赖" 2020-05-22 12:23:44 +08:00
silenceper
45522f003f Merge pull request #235 from silenceper/revert-233-fix-go.mod
Revert "Fix go.mod"
2020-05-22 12:23:11 +08:00
silenceper
a264ce4266 Revert "Fix go.mod" 2020-05-22 12:22:53 +08:00
silenceper
3c881e3885 Merge pull request #233 from silenceper/fix-go.mod
Fix go.mod
2020-05-22 11:16:51 +08:00
silenceper
3740bb55c3 更新redis依赖 2020-05-22 11:12:40 +08:00
silenceper
c42d799367 更新redis依赖 2020-05-22 10:42:45 +08:00
silenceper
798c5b081c 更新redis依赖 2020-05-22 10:39:50 +08:00
silenceper
491ee80136 更新 github.com/gomodule/redigo@1.8.1 2020-05-22 10:28:12 +08:00
silenceper
a54b03a918 更新go.mod依赖 2020-05-22 10:23:30 +08:00
silenceper
eec3233134 Merge pull request #232 from silenceper/fix-go.mod
更新go.mod,redis依赖
2020-05-22 10:13:00 +08:00
silenceper
587ce04b5f 更新go.mod,redis依赖 2020-05-22 10:11:51 +08:00
silenceper
ada9c1ff61 Merge pull request #221 from quxiaolong1504/notify
重写 pay/NotifyResult VerifySign
2020-04-04 16:24:44 +08:00
quxiaolong
9e58e097cb 重写 pay/NotifyResult VerifySign 2020-03-27 10:58:07 +08:00
silenceper
8b6147c3ec Merge pull request #219 from bestony/patch-1
fix: update cloudbase homepage url
2020-03-18 14:54:57 +08:00
白宦成
0071852c75 update cloudbase homepage url 2020-03-18 14:50:42 +08:00
silenceper
f4491193cb Merge pull request #218 from silenceper/cloudbase
add cloudbase
2020-03-18 10:21:44 +08:00
silenceper
576a898c0f 更新文档 2020-03-17 19:47:29 +08:00
silenceper
76fde58ad9 add cloudbase 2020-03-17 19:01:51 +08:00
silenceper
903dadc260 Update README.md 2020-02-26 10:10:28 +08:00
silenceper
1efbf27bde Update README.md 2020-02-22 23:19:29 +08:00
silenceper
99a2eb659c Update .travis.yml 2020-02-16 20:26:48 +08:00
210 changed files with 3735 additions and 13058 deletions

View File

@@ -1,3 +1,2 @@
## 问题及现象
<!-- 描述你的问题现象,报错**贴截图**粘贴或者贴具体信息,提供**必要的代码段**

View File

@@ -1,21 +0,0 @@
---
name: 报告Bug
about: 反馈BUG信息
title: "[BUG]"
labels: bug
assignees: ''
---
**描述**
**如何复现**
步骤:
1、
2、
**关联日志信息**
**使用的版本**
- SDK版本: [比如 v0.0.0]

View File

@@ -1,15 +0,0 @@
---
name: API需求
about: 待实现的API接口SDK的强大离不开社区的帮助欢迎为项目贡献PR
title: "[Feature]"
labels: enhancement
assignees: ''
---
<!--
!!!SDK的强大离不开社区的帮助欢迎为本项目贡献PR!!!
-->
**你想要实现的模块或API**

View File

@@ -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/
-->
**请描述您的问题**

View File

@@ -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,1.17.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.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 ./...

View File

@@ -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
View File

@@ -26,5 +26,4 @@ _testmain.go
.vscode/
vendor
.idea/
example/*
/test
examples/tcb/*

View File

@@ -1,66 +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:
include:
- EXC0002 # disable excluding of issues about comments from golint
exclude-rules:
- linters:
- stylecheck
text: "ST1000:"
# Excluding configuration per-path, per-linter, per-text and per-source
- 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:"

View File

@@ -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
View 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 ./...)

684
README.md
View File

@@ -1,83 +1,655 @@
# WeChat SDK for Go
## 📢 注意: 此分支为v1版本已不再维护更新请切换至 [v2](https://github.com/silenceper/wechat/tree/release-2.0)
![Go](https://github.com/silenceper/wechat/workflows/Go/badge.svg?branch=release-2.0)
# WeChat SDK for Go
[![Build Status](https://travis-ci.org/silenceper/wechat.svg?branch=master)](https://travis-ci.org/silenceper/wechat)
[![Go Report Card](https://goreportcard.com/badge/github.com/silenceper/wechat)](https://goreportcard.com/report/github.com/silenceper/wechat)
[![pkg](https://img.shields.io/badge/dev-reference-007d9c?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/silenceper/wechat/v2?tab=doc)
![version](https://img.shields.io/badge/version-v2-green)
[![GoDoc](http://godoc.org/github.com/silenceper/wechat?status.svg)](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保存access_token也可选择redis或自定义cache
memCache=cache.NewMemcache("127.0.0.1:11211")
//配置微信参数
config := &wechat.Config{
AppID: "xxxx",
AppSecret: "xxxx",
Token: "xxxx",
EncodingAESKey: "xxxx",
Cache: memCache
}
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")
![img](https://silenceper.oss-cn-beijing.aliyuncs.com/qrcode/search_study_program.png)
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
list, err := wc.GetMaterial().BatchGetMaterial(material.PermanentMaterialTypeNews, 0, 10)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(list)
```
## 小程序开发
获取小程序操作对象
``` go
memCache=cache.NewMemcache("127.0.0.1:11211")
config := &wechat.Config{
AppID: "xxx",
AppSecret: "xxx",
Cache: memCache=cache.NewMemcache("127.0.0.1:11211"),
}
wc := wechat.NewWechat(config)
wxa := wc.GetMiniProgram()
```
### 小程序登录凭证校验
``` go
func (wxa *MiniProgram) Code2Session(jsCode string) (result ResCode2Session, err error)
```
### 小程序数据统计
**获取用户访问小程序日留存**
``` go
func (wxa *MiniProgram) GetAnalysisDailyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error)
```
**获取用户访问小程序月留存**
``` go
func (wxa *MiniProgram) GetAnalysisMonthlyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error)
```
**获取用户访问小程序周留存**
``` go
func (wxa *MiniProgram) GetAnalysisWeeklyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error)
```
**获取用户访问小程序数据概况**
``` go
func (wxa *MiniProgram) GetAnalysisDailySummary(beginDate, endDate string) (result ResAnalysisDailySummary, err error)
```
**获取用户访问小程序数据日趋势**
``` go
func (wxa *MiniProgram) GetAnalysisDailyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error)
```
**获取用户访问小程序数据月趋势**
``` go
func (wxa *MiniProgram) GetAnalysisMonthlyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error)
```
**获取用户访问小程序数据周趋势**
``` go
func (wxa *MiniProgram) GetAnalysisWeeklyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error)
```
**获取小程序新增或活跃用户的画像分布数据**
``` go
func (wxa *MiniProgram) GetAnalysisUserPortrait(beginDate, endDate string) (result ResAnalysisUserPortrait, err error)
```
**获取用户小程序访问分布数据**
``` go
func (wxa *MiniProgram) GetAnalysisVisitDistribution(beginDate, endDate string) (result ResAnalysisVisitDistribution, err error)
```
**获取小程序页面访问数据**
``` go
func (wxa *MiniProgram) GetAnalysisVisitPage(beginDate, endDate string) (result ResAnalysisVisitPage, err error)
```
### 小程序二维码生成
**获取小程序二维码,适用于需要的码数量较少的业务场景**
``` go
func (wxa *MiniProgram) CreateWXAQRCode(coderParams QRCoder) (response []byte, err error)
```
**获取小程序码,适用于需要的码数量较少的业务场景**
``` go
func (wxa *MiniProgram) GetWXACode(coderParams QRCoder) (response []byte, err error)
```
**获取小程序码,适用于需要的码数量极多的业务场景**
``` go
func (wxa *MiniProgram) GetWXACodeUnlimit(coderParams QRCoder) (response []byte, err error)
```
更多API使用请参考 godoc
[https://godoc.org/github.com/silenceper/wechat](https://godoc.org/github.com/silenceper/wechat)
## License

View File

@@ -1,5 +0,0 @@
# 智能对话
[官方文档](https://developers.weixin.qq.com/doc/aispeech/platform/INTERFACEDOCUMENT.html)
## 快速入门

2
cache/cache.go vendored
View File

@@ -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
View File

@@ -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)
}

View File

@@ -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
View File

@@ -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
View File

@@ -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
View File

@@ -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
View 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)

View 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 />![image.png](https://cdn.nlark.com/yuque/0/2020/png/748713/1580023609925-93d7ece7-636f-46c8-83b8-be12a41c5f51.png#align=left&display=inline&height=236&name=image.png&originHeight=472&originWidth=746&size=75078&status=done&style=none&width=373)
其中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 />![image.png](https://cdn.nlark.com/yuque/0/2020/png/748713/1580024391966-3cf8aab1-6630-4b5d-b172-150f6b43e53c.png#align=left&display=inline&height=155&name=image.png&originHeight=310&originWidth=1284&size=23217&status=done&style=none&width=642)

View 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用于解析云开发对应的配置参数appkeyapp_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>
```
这样就实现了对文本内容的保存

View 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这样我们就可以在一边修改代码一边对项目进行编译及时发现错误是不是效率提升了呢  :>
![image.png](https://cdn.nlark.com/yuque/0/2020/png/748713/1579680949745-4a9d705e-b2d1-4667-a7a7-b9a5200321c8.png#align=left&display=inline&height=777&name=image.png&originHeight=1554&originWidth=2470&size=400945&status=done&style=none&width=1235)<br />初次build会通过go module自动下载依赖请注意开启go module功能
我们通过访问`127.0.0.1:8080/ping`就可以看到页面上输出`{"message":"pong"}`说明服务启动成功。
<a name="yXtGW"></a>
## 渲染留言页面
我们可以先规划我们的UI是怎么样子
包含两部分:
- 留言框:包含留言内容,附件上传,用户名,提交按钮
- 内容展示:展示留言内容,附件以及留言者和留言日期
界面展示如下:<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/748713/1579681903215-81a613c0-0a08-4196-ba6d-36c8942e107c.png#align=left&display=inline&height=331&name=image.png&originHeight=1312&originWidth=2352&size=99758&status=done&style=none&width=593)
<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)

View 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 />![image.png](https://cdn.nlark.com/yuque/0/2020/png/748713/1580025839031-8efea9fe-3ce0-4a4b-a8cd-0ad2120c1a9e.png#align=left&display=inline&height=666&name=image.png&originHeight=1332&originWidth=2256&size=112605&status=done&style=none&width=1128)
本文中所有代码都上传在 [https://github.com/go-demo/guestbook](https://github.com/go-demo/guestbook)

85
context/access_token.go Normal file
View 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
}

View 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")
}
}

View File

@@ -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
}
@@ -226,36 +189,6 @@ type AuthorizerInfo struct {
}
Alias string `json:"alias"`
QrcodeURL string `json:"qrcode_url"`
MiniProgramInfo *MiniProgramInfo `json:"MiniProgramInfo"`
RegisterType int `json:"register_type"`
AccountStatus int `json:"account_status"`
BasicConfig *AuthorizerBasicConfig `json:"basic_config"`
}
// AuthorizerBasicConfig 授权账号的基础配置结构体
type AuthorizerBasicConfig struct {
IsPhoneConfigured bool `json:"isPhoneConfigured"`
IsEmailConfigured bool `json:"isEmailConfigured"`
}
// MiniProgramInfo 授权账号小程序配置 授权账号为小程序时存在
type MiniProgramInfo struct {
Network struct {
RequestDomain []string `json:"RequestDomain"`
WsRequestDomain []string `json:"WsRequestDomain"`
UploadDomain []string `json:"UploadDomain"`
DownloadDomain []string `json:"DownloadDomain"`
BizDomain []string `json:"BizDomain"`
UDPDomain []string `json:"UDPDomain"`
} `json:"network"`
Categories []CategoriesInfo `json:"categories"`
}
// CategoriesInfo 授权账号小程序配置的类目信息
type CategoriesInfo struct {
First string `wx:"first"`
Second string `wx:"second"`
}
// GetAuthrInfo 获取授权方的帐号基本信息

19
context/component_test.go Normal file
View 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
View 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
}

View 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
View 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
}
}

View File

@@ -1,6 +0,0 @@
package credential
// AccessTokenHandle AccessToken 接口
type AccessTokenHandle interface {
GetAccessToken() (accessToken string, err error)
}

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -1,7 +0,0 @@
package credential
// JsTicketHandle js ticket获取
type JsTicketHandle interface {
// GetTicket 获取ticket
GetTicket(accessToken string) (ticket string, err error)
}

View File

@@ -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 为0product_id 为1不要填写 product_id 字段(会引起不必要错误);
// 当 op_typy 为0product_id 不为1必须填写 product_id 字段;
// 当 op_type 为 1 时,不要填写 product_id 字段。
//当 op_type 为0product_id 为1不要填写 product_id 字段(会引起不必要错误);
//当 op_typy 为0product_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
// 1version 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"`

View File

@@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/v2/util"
"github.com/silenceper/wechat/util"
)
// ReqBind 设备绑定解绑共通实体

View File

@@ -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

View File

@@ -1,6 +1,6 @@
package device
// MsgDevice 设备消息响应
//MsgDevice 设备消息响应
type MsgDevice struct {
DeviceType string
DeviceID string

View File

@@ -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
View File

@@ -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

View File

@@ -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)

View File

@@ -1,3 +0,0 @@
# 智能对话
TODO

View File

@@ -1,3 +0,0 @@
# 小游戏
TODO

View File

@@ -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 |

View File

@@ -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 | YES | (tpl *Subscribe) GetCategory |
| 获取模板中的关键词 | GET | /wxaapi/newtmpl/getpubtemplatekeywords | YES | (tpl *Subscribe) GetPubTplKeyWordsByID |
| 获取类目下的公共模板 | GET | /wxaapi/newtmpl/getpubtemplatetitles | YES | (tpl *Subscribe) GetPublicTemplateTitleList |
| 获取私有模板列表 | 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)
说明:「发表记录」包括群发和发布。
注意:该接口,只能处理 "发布" 相关的信息,无法操作和获取 "群发" 相关内容!![官方回复](https://developers.weixin.qq.com/community/develop/doc/0002a4fb2109d8f7a91d421c556c00)
- 群发:主动推送给粉丝,历史消息可看,被搜一搜收录,可以限定部分的粉丝接收到。
- 发布:不会主动推给粉丝,历史消息列表看不到,但是是公开给所有人的文章。也不会占用群发的次数。每天可以发布多篇内容。可以用于自动回复、自定义菜单、页面模板和话题中,发布成功时会生成一个永久链接。
| 名称 | 请求方式 | 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 |
## 图文消息留言管理
## 用户管理
## 账号管理
## 数据统计
## 微信卡券
## 微信门店
## 智能接口
## 微信设备功能
## 微信“一物一码”
## 微信发票
## 微信非税缴费

View File

@@ -1 +0,0 @@
# 开放平台

View File

@@ -1,74 +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 |
### 客户联系
[官方文档](https://developer.work.weixin.qq.com/document/path/92132/92133)
| 名称 | 请求方式 | URL | 是否已实现 | 使用方法 | 贡献者 |
|:---------------:| -------- | :---------------------------------------| ---------- | ------------------------------- |----------|
| 获取「联系客户统计」数据 | POST | /cgi-bin/externalcontact/get_user_behavior_data | YES | (r *Client) GetUserBehaviorData | MARKWANG |
| 获取「群聊数据统计」数据 (按群主聚合的方式) | POST | /cgi-bin/externalcontact/groupchat/statistic | YES | (r *Client) GetGroupChatStat | MARKWANG |
| 获取「群聊数据统计」数据 (按自然日聚合的方式) | POST | /cgi-bin/externalcontact/groupchat/statistic_group_by_day | YES | (r *Client) GetGroupChatStatByDay | MARKWANG |
## 应用管理
TODO

View File

@@ -1,3 +0,0 @@
# 微信支付
TODO

45
examples/beego/beego.go Normal file
View 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
View 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
View 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)
}
}

27
go.mod
View File

@@ -1,15 +1,22 @@
module github.com/silenceper/wechat/v2
module github.com/silenceper/wechat
go 1.15
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.5
github.com/sirupsen/logrus v1.9.0
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-20220622213112-05595931fe9d
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 v1.8.1
github.com/kr/pretty v0.1.0 // indirect
github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 // indirect
github.com/mattn/go-isatty v0.0.0-20161123143637-30a891c33c7c // indirect
github.com/spf13/cast v1.3.1
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
golang.org/x/net v0.0.0-20191125084936-ffdde1057850 // indirect
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v8 v8.18.1 // indirect
)

165
go.sum
View File

@@ -1,145 +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.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
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.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ=
github.com/onsi/ginkgo/v2 v2.0.0/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.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/gin-gonic/gin v1.1.4 h1:XLaCFbU39SSGRQrEeP7Z7mM3lvRqC4vE5tEaVdLDdSE=
github.com/gin-gonic/gin v1.1.4/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/golang/protobuf v0.0.0-20161117033126-8ee79997227b h1:fE/yi9pibxGEc0gSJuEShcsBXE2d5FW3OudsjE9tKzQ=
github.com/golang/protobuf v0.0.0-20161117033126-8ee79997227b/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gomodule/redigo v1.8.1 h1:Abmo0bI7Xf0IhdIPc7HZQzZcShdnmxeoVuDDtIQp8N8=
github.com/gomodule/redigo v1.8.1/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 h1:ykXz+pRRTibcSjG1yRhpdSHInF8yZY/mfn+Rz2Nd1rE=
github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739/go.mod h1:zUx1mhth20V3VKgL5jbd1BSQcW4Fy6Qs4PZvQwRFwzM=
github.com/mattn/go-isatty v0.0.0-20161123143637-30a891c33c7c h1:YHHK/dEmr2Jo1cWD1VMB2waEeHJhHFp3CEylwWy/VcY=
github.com/mattn/go-isatty v0.0.0-20161123143637-30a891c33c7c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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=
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-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/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 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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-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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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
View 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&timestamp=%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
}

View File

@@ -5,47 +5,45 @@ import (
"errors"
"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 (
addNewsURL = "https://api.weixin.qq.com/cgi-bin/material/add_news"
updateNewsURL = "https://api.weixin.qq.com/cgi-bin/material/update_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"
getMaterialCountURL = "https://api.weixin.qq.com/cgi-bin/material/get_materialcount"
batchGetMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/batchget_material"
)
// PermanentMaterialType 永久素材类型
//PermanentMaterialType 永久素材类型
type PermanentMaterialType string
const (
// PermanentMaterialTypeImage 永久素材图片类型image
//PermanentMaterialTypeImage 永久素材图片类型image
PermanentMaterialTypeImage PermanentMaterialType = "image"
// PermanentMaterialTypeVideo 永久素材视频类型video
//PermanentMaterialTypeVideo 永久素材视频类型video
PermanentMaterialTypeVideo PermanentMaterialType = "video"
// PermanentMaterialTypeVoice 永久素材语音类型 voice
//PermanentMaterialTypeVoice 永久素材语音类型 voice
PermanentMaterialTypeVoice PermanentMaterialType = "voice"
// PermanentMaterialTypeNews 永久素材图文类型news
//PermanentMaterialTypeNews 永久素材图文类型news
PermanentMaterialTypeNews PermanentMaterialType = "news"
)
// Material 素材管理
//Material 素材管理
type Material struct {
*context.Context
}
// NewMaterial init
//NewMaterial init
func NewMaterial(context *context.Context) *Material {
material := new(Material)
material.Context = context
return material
}
// Article 永久图文素材
//Article 永久图文素材
type Article struct {
Title string `json:"title"`
ThumbMediaID string `json:"thumb_media_id"`
@@ -87,19 +85,19 @@ func (material *Material) GetNews(id string) ([]*Article, error) {
return res.NewsItem, nil
}
// reqArticles 永久性图文素材请求信息
//reqArticles 永久性图文素材请求信息
type reqArticles struct {
Articles []*Article `json:"articles"`
}
// resArticles 永久性图文素材返回结果
//resArticles 永久性图文素材返回结果
type resArticles struct {
util.CommonError
MediaID string `json:"media_id"`
}
// AddNews 新增永久图文素材
//AddNews 新增永久图文素材
func (material *Material) AddNews(articles []*Article) (mediaID string, err error) {
req := &reqArticles{articles}
@@ -114,45 +112,17 @@ func (material *Material) AddNews(articles []*Article) (mediaID string, err erro
if err != nil {
return
}
var res resArticles
err = json.Unmarshal(responseBytes, &res)
if err != nil {
return
}
if res.ErrCode != 0 {
return "", fmt.Errorf("errcode=%d,errmsg=%s", res.ErrCode, res.ErrMsg)
}
mediaID = res.MediaID
return
}
// reqUpdateArticle 更新永久性图文素材请求信息
type reqUpdateArticle struct {
MediaID string `json:"media_id"`
Index int64 `json:"index"`
Articles *Article `json:"articles"`
}
// UpdateNews 更新永久图文素材
func (material *Material) UpdateNews(article *Article, mediaID string, index int64) (err error) {
req := &reqUpdateArticle{mediaID, index, article}
var accessToken string
accessToken, err = material.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s", updateNewsURL, accessToken)
var response []byte
response, err = util.PostJSON(uri, req)
if err != nil {
return
}
return util.DecodeWithCommonError(response, "UpdateNews")
}
// resAddMaterial 永久性素材上传返回的结果
//resAddMaterial 永久性素材上传返回的结果
type resAddMaterial struct {
util.CommonError
@@ -160,7 +130,7 @@ type resAddMaterial struct {
URL string `json:"url"`
}
// AddMaterial 上传永久性素材(处理视频需要单独上传)
//AddMaterial 上传永久性素材(处理视频需要单独上传)
func (material *Material) AddMaterial(mediaType MediaType, filename string) (mediaID string, url string, err error) {
if mediaType == MediaTypeVideo {
err = errors.New("永久视频素材上传使用 AddVideo 方法")
@@ -197,7 +167,7 @@ type reqVideo struct {
Introduction string `json:"introduction"`
}
// AddVideo 永久视频素材文件上传
//AddVideo 永久视频素材文件上传
func (material *Material) AddVideo(filename, title, introduction string) (mediaID string, url string, err error) {
var accessToken string
accessToken, err = material.GetAccessToken()
@@ -254,7 +224,7 @@ type reqDeleteMaterial struct {
MediaID string `json:"media_id"`
}
// DeleteMaterial 删除永久素材
//DeleteMaterial 删除永久素材
func (material *Material) DeleteMaterial(mediaID string) error {
accessToken, err := material.GetAccessToken()
if err != nil {
@@ -270,15 +240,14 @@ func (material *Material) DeleteMaterial(mediaID string) error {
return util.DecodeWithCommonError(response, "DeleteMaterial")
}
// ArticleList 永久素材列表
//ArticleList 永久素材列表
type ArticleList struct {
util.CommonError
TotalCount int64 `json:"total_count"`
ItemCount int64 `json:"item_count"`
Item []ArticleListItem `json:"item"`
}
// ArticleListItem 用于ArticleList的item节点
//ArticleListItem 用于ArticleList的item节点
type ArticleListItem struct {
MediaID string `json:"media_id"`
Content ArticleListContent `json:"content"`
@@ -287,14 +256,14 @@ type ArticleListItem struct {
UpdateTime int64 `json:"update_time"`
}
// ArticleListContent 用于ArticleListItem的content节点
//ArticleListContent 用于ArticleListItem的content节点
type ArticleListContent struct {
NewsItem []Article `json:"news_item"`
UpdateTime int64 `json:"update_time"`
CreateTime int64 `json:"create_time"`
}
// reqBatchGetMaterial BatchGetMaterial请求参数
//reqBatchGetMaterial BatchGetMaterial请求参数
type reqBatchGetMaterial struct {
Type PermanentMaterialType `json:"type"`
Count int64 `json:"count"`
@@ -326,29 +295,3 @@ func (material *Material) BatchGetMaterial(permanentMaterialType PermanentMateri
err = util.DecodeWithError(response, &list, "BatchGetMaterial")
return
}
// ResMaterialCount 素材总数
type ResMaterialCount struct {
util.CommonError
VoiceCount int64 `json:"voice_count"` // 语音总数量
VideoCount int64 `json:"video_count"` // 视频总数量
ImageCount int64 `json:"image_count"` // 图片总数量
NewsCount int64 `json:"news_count"` // 图文总数量
}
// GetMaterialCount 获取素材总数.
func (material *Material) GetMaterialCount() (res ResMaterialCount, err error) {
var accessToken string
accessToken, err = material.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s", getMaterialCountURL, accessToken)
var response []byte
response, err = util.HTTPGet(uri)
if err != nil {
return
}
err = util.DecodeWithError(response, &res, "GetMaterialCount")
return
}

View File

@@ -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
View 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
}

View File

@@ -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()

View File

@@ -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 {

View File

@@ -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
View 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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -1,9 +0,0 @@
# 微信小游戏
[官方文档](https://developers.weixin.qq.com/minigame/dev/api-backend/)
## 快速入门
```go
```

View File

@@ -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()
```

View File

@@ -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
}

View File

@@ -1,150 +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
}
// 由于GetPhoneNumberContext需要传入JSON所以HTTPPostContext入参改为[]byte
if response, err = util.HTTPPostContext(ctx, fmt.Sprintf(checkEncryptedDataURL, at), []byte("encrypted_msg_hash="+encryptedMsgHash), nil); 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"` // 数据水印
}
// GetPhoneNumberContext 小程序通过code获取用户手机号
func (auth *Auth) GetPhoneNumberContext(ctx context2.Context, 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,
}
bodyBytes, err := json.Marshal(body)
if err != nil {
return nil, err
}
header := map[string]string{"Content-Type": "application/json;charset=utf-8"}
if response, err = util.HTTPPostContext(ctx, fmt.Sprintf(getPhoneNumber, at), bodyBytes, header); err != nil {
return nil, err
}
var result GetPhoneNumberResponse
if err = util.DecodeWithError(response, &result, "phonenumber.getPhoneNumber"); err != nil {
return nil, err
}
return &result, nil
}
// GetPhoneNumber 小程序通过code获取用户手机号
func (auth *Auth) GetPhoneNumber(code string) (*GetPhoneNumberResponse, error) {
return auth.GetPhoneNumberContext(context2.Background(), code)
}

View File

@@ -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}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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"`
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
View 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
}

View File

@@ -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")
}

View File

@@ -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,
}
}

View File

@@ -1,52 +0,0 @@
package urllink
import (
"fmt"
"github.com/silenceper/wechat/v2/util"
)
const queryURL = "https://api.weixin.qq.com/wxa/query_urllink"
// ULQueryResult 返回的结果
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-link/urllink.query.html 返回值
type ULQueryResult struct {
util.CommonError
URLLinkInfo struct {
Appid string `json:"appid"`
Path string `json:"path"`
Query string `json:"query"`
CreateTime int64 `json:"create_time"`
ExpireTime int64 `json:"expire_time"`
EnvVersion string `json:"env_version"`
CloudBase struct {
Env string `json:"env"`
Domain string `json:"domain"`
Path string `json:"path"`
Query string `json:"query"`
ResourceAppid string `json:"resource_appid"`
} `json:"cloud_base"`
} `json:"url_link_info"`
VisitOpenid string `json:"visit_openid"`
}
// Query 查询小程序 url_link 配置。
func (u *URLLink) Query(urlLink string) (*ULQueryResult, error) {
accessToken, err := u.GetAccessToken()
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s?access_token=%s", queryURL, accessToken)
response, err := util.PostJSON(uri, map[string]string{"url_link": urlLink})
if err != nil {
return nil, err
}
var resp ULQueryResult
err = util.DecodeWithError(response, &resp, "URLLink.Query")
if err != nil {
return nil, err
}
return &resp, nil
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
View 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
}

View File

@@ -1,5 +0,0 @@
# 微信公众号
[官方文档](https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html)
## 快速入门

Some files were not shown because too many files have changed in this diff Show More