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

Compare commits

..

23 Commits

Author SHA1 Message Date
silenceper
340e1a3c5b merge #221 2020-05-23 12:38:35 +08:00
silenceper
165d8f4d96 fix 冲突 2020-05-23 12:23:55 +08:00
silenceper
5557975960 Merge branch 'f-openplatform' into release-2.0 2020-05-23 12:21:41 +08:00
silenceper
7842eed32f Merge branch 'f-openplatform' of https://github.com/silenceper/wechat into f-openplatform 2020-05-23 12:14:16 +08:00
silenceper
d69578bedc fix import 2020-05-23 12:13:22 +08:00
silenceper
fad1b1839b fix import 2020-05-23 12:08:01 +08:00
silenceper
a8abc23893 微信开放平台-代处理小程序 2020-05-23 12:03:44 +08:00
silenceper
c372d7a83c 微信开放平台-代处理小程序 2020-05-23 12:02:55 +08:00
silenceper
85b0a114dc rebase release-2.0 2020-05-23 11:59:46 +08:00
silenceper
ea1b2d92d0 Merge pull request #238 from silenceper/feature-v2
将import path改为v2
2020-05-23 11:47:12 +08:00
silenceper
d91d1dd630 add workflow:memcached service 2020-05-23 11:45:20 +08:00
silenceper
b147370c1d Update go.yml 2020-05-23 11:42:30 +08:00
silenceper
293d47c6fb fix golint 2020-05-23 11:31:14 +08:00
silenceper
aa75de8144 Merge branch 'feature-v2' of https://github.com/silenceper/wechat into feature-v2 2020-05-23 11:28:51 +08:00
silenceper
2423b58855 rebase release-2.0 2020-05-23 11:28:02 +08:00
silenceper
2c7d3aaf3e Merge branch 'release-2.0' of https://github.com/silenceper/wechat into release-2.0 2020-05-23 11:26:52 +08:00
silenceper
ebf85640f7 1、添加log
2、添加workflows
3、添加SkipValidate方法
2020-05-23 11:23:47 +08:00
silenceper
b6bba6ab66 将import path改为v2 2020-05-22 14:37:58 +08:00
silenceper
50588e3d93 Update README.md 2020-05-21 11:49:36 +08:00
silenceper
9084163132 fix 代处理小程序error 2020-01-22 21:23:58 +08:00
silenceper
51cbfab087 待处理小程序业务 2020-01-22 15:45:06 +08:00
silenceper
7e8c94a6c4 开放平台基本框架 2020-01-21 16:36:34 +08:00
silenceper
15ebd71a04 规范目录 2020-01-20 13:54:04 +08:00
90 changed files with 1446 additions and 2479 deletions

50
.github/workflows/go.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: Go
on:
push:
branches: [ master,release-* ]
pull_request:
branches: [ master,release-* ]
jobs:
build:
name: Build
runs-on: ubuntu-latest
services:
redis:
image: redis
ports:
- 6379:6379
options: --entrypoint redis-server
memcached:
image: memcached
ports:
- 11211:11211
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: |
go get -v -t -d ./...
if [ -f Gopkg.toml ]; then
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
dep ensure
fi
- name: Lint Go Code
run: |
export PATH=$PATH:$(go env GOPATH)/bin # temporary fix. See https://github.com/actions/setup-go/issues/14
go get -u golang.org/x/lint/golint
go vet ./...
golint -set_exit_status $(go list ./...)
- name: Test
run: go test -v -race ./...

2
.gitignore vendored
View File

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

View File

@@ -4,6 +4,7 @@ go:
- 1.13.x
- 1.12.x
- 1.11.x
- 1.10.x
services:
- memcached

650
README.md
View File

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

5
aispeech/README.md Normal file
View File

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

View File

@@ -1,53 +0,0 @@
# 小程序-云开发 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

@@ -1,100 +0,0 @@
# 云开发存储:实现留言本附件上传
## 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

@@ -1,306 +0,0 @@
# 数据库:调用云开发数据库实现文本保存
在这一节,我们主要描述如何利用`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

@@ -1,185 +0,0 @@
# 起步:项目搭建
<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

@@ -1,144 +0,0 @@
# 云开发存储:实现留言本附件上传
## 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)

View File

@@ -1,30 +0,0 @@
package context
import (
"sync"
"testing"
)
func TestContext_SetCustomAccessTokenFunc(t *testing.T) {
ctx := Context{
accessTokenLock: new(sync.RWMutex),
}
f := func(ctx *Context) (accessToken string, err error) {
return "fake token", nil
}
ctx.SetGetAccessTokenFunc(f)
res, err := ctx.GetAccessToken()
if res != "fake token" || err != nil {
t.Error("expect fake token but error")
}
}
func TestContext_NoSetCustomAccessTokenFunc(t *testing.T) {
ctx := Context{
accessTokenLock: new(sync.RWMutex),
}
if ctx.accessTokenFunc != nil {
t.Error("error accessTokenFunc")
}
}

View File

@@ -1,19 +0,0 @@
package context
import (
"encoding/json"
"testing"
)
var testdata = `{"authorizer_info":{"nick_name":"就爱浪","head_img":"http:\/\/wx.qlogo.cn\/mmopen\/xPKCxELaaj6hiaTZGv19oQPBJibb7hBoKmNOjQibCNOUycE8iaBhiaHOA6eC8hadQSAUZTuHUJl4qCIbCQGjSWialicfzWh4mdxuejY\/0","service_type_info":{"id":1},"verify_type_info":{"id":-1},"user_name":"gh_dcdbaa6f1687","alias":"ckeyer","qrcode_url":"http:\/\/mmbiz.qpic.cn\/mmbiz_jpg\/FribWCoIzQbAX7R1PQ8iaxGonqKp0doYD2ibhC0uhx11LrRcblASiazsbQJTJ4icQnMzfH7G0SUPuKbibTA8Cs4uk5WQ\/0","business_info":{"open_pay":0,"open_shake":0,"open_scan":0,"open_card":0,"open_store":0},"idc":1,"principal_name":"个人","signature":"不折腾会死。"},"authorization_info":{"authorizer_appid":"yyyyy","authorizer_refresh_token":"xxxx","func_info":[{"funcscope_category":{"id":1}},{"funcscope_category":{"id":15}},{"funcscope_category":{"id":4}},{"funcscope_category":{"id":7}},{"funcscope_category":{"id":2}},{"funcscope_category":{"id":3}},{"funcscope_category":{"id":11}},{"funcscope_category":{"id":6}},{"funcscope_category":{"id":5}},{"funcscope_category":{"id":8}},{"funcscope_category":{"id":13}},{"funcscope_category":{"id":9}},{"funcscope_category":{"id":12}},{"funcscope_category":{"id":22}},{"funcscope_category":{"id":23}},{"funcscope_category":{"id":24},"confirm_info":{"need_confirm":0,"already_confirm":0,"can_confirm":0}},{"funcscope_category":{"id":26}},{"funcscope_category":{"id":27},"confirm_info":{"need_confirm":0,"already_confirm":0,"can_confirm":0}},{"funcscope_category":{"id":33},"confirm_info":{"need_confirm":0,"already_confirm":0,"can_confirm":0}},{"funcscope_category":{"id":35}}]}}`
// TestDecode
func TestDecode(t *testing.T) {
var ret struct {
AuthorizerInfo *AuthorizerInfo `json:"authorizer_info"`
AuthorizationInfo *AuthBaseInfo `json:"authorization_info"`
}
json.Unmarshal([]byte(testdata), &ret)
t.Logf("%+v", ret.AuthorizerInfo)
t.Logf("%+v", ret.AuthorizationInfo)
}

View File

@@ -1,58 +0,0 @@
package context
import (
"net/http"
"sync"
"github.com/silenceper/wechat/cache"
)
// Context struct
type Context struct {
AppID string
AppSecret string
Token string
EncodingAESKey string
PayMchID string
PayNotifyURL string
PayKey string
Cache cache.Cache
Writer http.ResponseWriter
Request *http.Request
//accessTokenLock 读写锁 同一个AppID一个
accessTokenLock *sync.RWMutex
//jsAPITicket 读写锁 同一个AppID一个
jsAPITicketLock *sync.RWMutex
//accessTokenFunc 自定义获取 access token 的方法
accessTokenFunc GetAccessTokenFunc
}
// Query returns the keyed url query value if it exists
func (ctx *Context) Query(key string) string {
value, _ := ctx.GetQuery(key)
return value
}
// GetQuery is like Query(), it returns the keyed url query value
func (ctx *Context) GetQuery(key string) (string, bool) {
req := ctx.Request
if values, ok := req.URL.Query()[key]; ok && len(values) > 0 {
return values[0], true
}
return "", false
}
// SetJsAPITicketLock 设置jsAPITicket的lock
func (ctx *Context) SetJsAPITicketLock(lock *sync.RWMutex) {
ctx.jsAPITicketLock = lock
}
// GetJsAPITicketLock 获取jsAPITicket 的lock
func (ctx *Context) GetJsAPITicketLock() *sync.RWMutex {
return ctx.jsAPITicketLock
}

View File

@@ -1,76 +0,0 @@
package context
import (
"encoding/json"
"fmt"
"log"
"sync"
"time"
"github.com/silenceper/wechat/util"
)
const (
//qyAccessTokenURL 获取access_token的接口
qyAccessTokenURL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s"
)
//ResQyAccessToken struct
type ResQyAccessToken struct {
util.CommonError
AccessToken string `json:"access_token"`
ExpiresIn int64 `json:"expires_in"`
}
//SetQyAccessTokenLock 设置读写锁一个appID一个读写锁
func (ctx *Context) SetQyAccessTokenLock(l *sync.RWMutex) {
ctx.accessTokenLock = l
}
//GetQyAccessToken 获取access_token
func (ctx *Context) GetQyAccessToken() (accessToken string, err error) {
ctx.accessTokenLock.Lock()
defer ctx.accessTokenLock.Unlock()
accessTokenCacheKey := fmt.Sprintf("qy_access_token_%s", ctx.AppID)
val := ctx.Cache.Get(accessTokenCacheKey)
if val != nil {
accessToken = val.(string)
return
}
//从微信服务器获取
var resQyAccessToken ResQyAccessToken
resQyAccessToken, err = ctx.GetQyAccessTokenFromServer()
if err != nil {
return
}
accessToken = resQyAccessToken.AccessToken
return
}
//GetQyAccessTokenFromServer 强制从微信服务器获取token
func (ctx *Context) GetQyAccessTokenFromServer() (resQyAccessToken ResQyAccessToken, err error) {
log.Printf("GetQyAccessTokenFromServer")
url := fmt.Sprintf(qyAccessTokenURL, ctx.AppID, ctx.AppSecret)
var body []byte
body, err = util.HTTPGet(url)
if err != nil {
return
}
err = json.Unmarshal(body, &resQyAccessToken)
if err != nil {
return
}
if resQyAccessToken.ErrCode != 0 {
err = fmt.Errorf("get qy_access_token error : errcode=%v , errormsg=%v", resQyAccessToken.ErrCode, resQyAccessToken.ErrMsg)
return
}
qyAccessTokenCacheKey := fmt.Sprintf("qy_access_token_%s", ctx.AppID)
expires := resQyAccessToken.ExpiresIn - 1500
err = ctx.Cache.Set(qyAccessTokenCacheKey, resQyAccessToken.AccessToken, time.Duration(expires)*time.Second)
return
}

View File

@@ -1,43 +0,0 @@
package context
import (
"encoding/xml"
"net/http"
)
var xmlContentType = []string{"application/xml; charset=utf-8"}
var plainContentType = []string{"text/plain; charset=utf-8"}
//Render render from bytes
func (ctx *Context) Render(bytes []byte) {
//debug
//fmt.Println("response msg = ", string(bytes))
ctx.Writer.WriteHeader(200)
_, err := ctx.Writer.Write(bytes)
if err != nil {
panic(err)
}
}
//String render from string
func (ctx *Context) String(str string) {
writeContextType(ctx.Writer, plainContentType)
ctx.Render([]byte(str))
}
//XML render to xml
func (ctx *Context) XML(obj interface{}) {
writeContextType(ctx.Writer, xmlContentType)
bytes, err := xml.Marshal(obj)
if err != nil {
panic(err)
}
ctx.Render(bytes)
}
func writeContextType(w http.ResponseWriter, value []string) {
header := w.Header()
if val := header["Content-Type"]; len(val) == 0 {
header["Content-Type"] = value
}
}

23
doc.go
View File

@@ -3,29 +3,6 @@ Package wechat provide wechat sdk for go
使用Golang开发的微信SDK简单、易用。
以下是一个处理消息接收以及回复的例子:
//配置微信参数
config := &wechat.Config{
AppID: "xxxx",
AppSecret: "xxxx",
Token: "xxxx",
EncodingAESKey: "xxxx",
}
wc := wechat.NewWechat(config)
// 传入request和responseWriter
server := wc.GetServer(request, responseWriter)
server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {
//回复消息:演示回复用户发送的消息
text := message.NewText(msg.Content)
return &message.Reply{message.MsgText, text}
})
server.Serve()
server.Send()
更多信息https://github.com/silenceper/wechat

View File

@@ -1,45 +0,0 @@
package main
import (
"fmt"
"github.com/astaxie/beego"
"github.com/astaxie/beego/context"
"github.com/silenceper/wechat"
"github.com/silenceper/wechat/message"
)
func hello(ctx *context.Context) {
//配置微信参数
config := &wechat.Config{
AppID: "your app id",
AppSecret: "your app secret",
Token: "your token",
EncodingAESKey: "your encoding aes key",
}
wc := wechat.NewWechat(config)
// 传入request和responseWriter
server := wc.GetServer(ctx.Request, ctx.ResponseWriter)
//设置接收消息的处理方法
server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {
//回复消息:演示回复用户发送的消息
text := message.NewText(msg.Content)
return &message.Reply{MsgType: message.MsgTypeText, MsgData: text}
})
//处理消息接收以及回复
err := server.Serve()
if err != nil {
fmt.Println(err)
return
}
//发送回复的消息
server.Send()
}
func main() {
beego.Any("/", hello)
beego.Run(":8001")
}

View File

@@ -1,47 +0,0 @@
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/silenceper/wechat"
"github.com/silenceper/wechat/message"
)
func main() {
router := gin.Default()
router.Any("/", hello)
router.Run(":8001")
}
func hello(c *gin.Context) {
//配置微信参数
config := &wechat.Config{
AppID: "your app id",
AppSecret: "your app secret",
Token: "your token",
EncodingAESKey: "your encoding aes key",
}
wc := wechat.NewWechat(config)
// 传入request和responseWriter
server := wc.GetServer(c.Request, c.Writer)
//设置接收消息的处理方法
server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {
//回复消息:演示回复用户发送的消息
text := message.NewText(msg.Content)
return &message.Reply{MsgType: message.MsgTypeText, MsgData: text}
})
//处理消息接收以及回复
err := server.Serve()
if err != nil {
fmt.Println(err)
return
}
//发送回复的消息
server.Send()
}

View File

@@ -1,48 +0,0 @@
package main
import (
"fmt"
"net/http"
"github.com/silenceper/wechat"
"github.com/silenceper/wechat/message"
)
func hello(rw http.ResponseWriter, req *http.Request) {
//配置微信参数
config := &wechat.Config{
AppID: "your app id",
AppSecret: "your app secret",
Token: "your token",
EncodingAESKey: "your encoding aes key",
}
wc := wechat.NewWechat(config)
// 传入request和responseWriter
server := wc.GetServer(req, rw)
//设置接收消息的处理方法
server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {
//回复消息:演示回复用户发送的消息
text := message.NewText(msg.Content)
return &message.Reply{MsgType: message.MsgTypeText, MsgData: text}
})
//处理消息接收以及回复
err := server.Serve()
if err != nil {
fmt.Println(err)
return
}
//发送回复的消息
server.Send()
}
func main() {
http.HandleFunc("/", hello)
err := http.ListenAndServe(":8001", nil)
if err != nil {
fmt.Printf("start server error , err=%v", err)
}
}

20
go.mod
View File

@@ -1,22 +1,12 @@
module github.com/silenceper/wechat
module github.com/silenceper/wechat/v2
go 1.13
go 1.14
require (
github.com/astaxie/beego v1.7.1
github.com/bradfitz/gomemcache v0.0.0-20160117192205-fb1f79c6b65a
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
github.com/fatih/structs v1.1.0
github.com/gin-gonic/gin v1.1.4
github.com/golang/protobuf v0.0.0-20161117033126-8ee79997227b // indirect
github.com/gomodule/redigo v1.8.1
github.com/kr/pretty v0.1.0 // indirect
github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 // indirect
github.com/mattn/go-isatty v0.0.0-20161123143637-30a891c33c7c // indirect
github.com/sirupsen/logrus v1.6.0
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
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
)

47
go.sum
View File

@@ -1,50 +1,33 @@
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/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/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/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20191125084936-ffdde1057850 h1:Vq85/r8R9IdcUHmZ0/nQlUg1v15rzvQ2sHdnZAj/x7s=
golang.org/x/net v0.0.0-20191125084936-ffdde1057850/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/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=

8
minigame/README.md Normal file
View File

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

20
miniprogram/README.md Normal file
View File

@@ -0,0 +1,20 @@
# 微信小程序
[官方文档](https://developers.weixin.qq.com/miniprogram/dev/framework/)
## 包说明
- analysis 数据分析相关API
## 快速入门
```go
wc := wechat.NewWechat()
memory := cache.NewMemory()
cfg := &miniConfig.Config{
AppID: "xxx",
AppSecret: "xxx",
Cache: memory,
}
miniprogram := wc.GetMiniProgram(cfg)
miniprogram.GetAnalysis().GetAnalysisDailyRetain()
```

View File

@@ -1,10 +1,12 @@
package miniprogram
package analysis
import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/miniprogram/context"
"github.com/silenceper/wechat/v2/util"
)
const (
@@ -30,10 +32,20 @@ const (
getAnalysisVisitPageURL = "https://api.weixin.qq.com/datacube/getweanalysisappidvisitpage?access_token=%s"
)
//Analysis analyis 数据分析
type Analysis struct {
*context.Context
}
//NewAnalysis new
func NewAnalysis(ctx *context.Context) *Analysis {
return &Analysis{ctx}
}
// fetchData 拉取统计数据
func (wxa *MiniProgram) fetchData(urlStr string, body interface{}) (response []byte, err error) {
func (analysis *Analysis) fetchData(urlStr string, body interface{}) (response []byte, err error) {
var accessToken string
accessToken, err = wxa.GetAccessToken()
accessToken, err = analysis.GetAccessToken()
if err != nil {
return
}
@@ -42,8 +54,8 @@ func (wxa *MiniProgram) fetchData(urlStr string, body interface{}) (response []b
return
}
// AnalysisRetainItem 留存项结构
type AnalysisRetainItem struct {
// RetainItem 留存项结构
type RetainItem struct {
Key int `json:"key"` // 标识0开始表示当天1表示1甜后以此类推
Value int `json:"value"` // key对应日期的新增用户数/活跃用户数key=0时或留存用户数k>0时
}
@@ -51,18 +63,18 @@ type AnalysisRetainItem struct {
// ResAnalysisRetain 小程序留存数据返回
type ResAnalysisRetain struct {
util.CommonError
RefDate string `json:"ref_date"` // 日期
VisitUVNew []AnalysisRetainItem `json:"visit_uv_new"` // 新增用户留存
VisitUV []AnalysisRetainItem `json:"visit_uv"` // 活跃用户留存
RefDate string `json:"ref_date"` // 日期
VisitUVNew []RetainItem `json:"visit_uv_new"` // 新增用户留存
VisitUV []RetainItem `json:"visit_uv"` // 活跃用户留存
}
// getAnalysisRetain 获取用户访问小程序留存数据(日、月、周)
func (wxa *MiniProgram) getAnalysisRetain(urlStr string, beginDate, endDate string) (result ResAnalysisRetain, err error) {
func (analysis *Analysis) getAnalysisRetain(urlStr string, beginDate, endDate string) (result ResAnalysisRetain, err error) {
body := map[string]string{
"begin_date": beginDate,
"end_date": endDate,
}
response, err := wxa.fetchData(urlStr, body)
response, err := analysis.fetchData(urlStr, body)
if err != nil {
return
}
@@ -78,18 +90,18 @@ func (wxa *MiniProgram) getAnalysisRetain(urlStr string, beginDate, endDate stri
}
// GetAnalysisDailyRetain 获取用户访问小程序日留存
func (wxa *MiniProgram) GetAnalysisDailyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
return wxa.getAnalysisRetain(getAnalysisDailyRetainURL, beginDate, endDate)
func (analysis *Analysis) GetAnalysisDailyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
return analysis.getAnalysisRetain(getAnalysisDailyRetainURL, beginDate, endDate)
}
// GetAnalysisMonthlyRetain 获取用户访问小程序月留存
func (wxa *MiniProgram) GetAnalysisMonthlyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
return wxa.getAnalysisRetain(getAnalysisMonthlyRetainURL, beginDate, endDate)
func (analysis *Analysis) GetAnalysisMonthlyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
return analysis.getAnalysisRetain(getAnalysisMonthlyRetainURL, beginDate, endDate)
}
// GetAnalysisWeeklyRetain 获取用户访问小程序周留存
func (wxa *MiniProgram) GetAnalysisWeeklyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
return wxa.getAnalysisRetain(getAnalysisWeeklyRetainURL, beginDate, endDate)
func (analysis *Analysis) GetAnalysisWeeklyRetain(beginDate, endDate string) (result ResAnalysisRetain, err error) {
return analysis.getAnalysisRetain(getAnalysisWeeklyRetainURL, beginDate, endDate)
}
// ResAnalysisDailySummary 小程序访问数据概况
@@ -104,12 +116,12 @@ type ResAnalysisDailySummary struct {
}
// GetAnalysisDailySummary 获取用户访问小程序数据概况
func (wxa *MiniProgram) GetAnalysisDailySummary(beginDate, endDate string) (result ResAnalysisDailySummary, err error) {
func (analysis *Analysis) GetAnalysisDailySummary(beginDate, endDate string) (result ResAnalysisDailySummary, err error) {
body := map[string]string{
"begin_date": beginDate,
"end_date": endDate,
}
response, err := wxa.fetchData(getAnalysisDailySummaryURL, body)
response, err := analysis.fetchData(getAnalysisDailySummaryURL, body)
if err != nil {
return
}
@@ -141,12 +153,12 @@ type ResAnalysisVisitTrend struct {
}
// getAnalysisRetain 获取小程序访问数据趋势(日、月、周)
func (wxa *MiniProgram) getAnalysisVisitTrend(urlStr string, beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
func (analysis *Analysis) getAnalysisVisitTrend(urlStr string, beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
body := map[string]string{
"begin_date": beginDate,
"end_date": endDate,
}
response, err := wxa.fetchData(urlStr, body)
response, err := analysis.fetchData(urlStr, body)
if err != nil {
return
}
@@ -162,18 +174,18 @@ func (wxa *MiniProgram) getAnalysisVisitTrend(urlStr string, beginDate, endDate
}
// GetAnalysisDailyVisitTrend 获取用户访问小程序数据日趋势
func (wxa *MiniProgram) GetAnalysisDailyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
return wxa.getAnalysisVisitTrend(getAnalysisDailyVisitTrendURL, beginDate, endDate)
func (analysis *Analysis) GetAnalysisDailyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
return analysis.getAnalysisVisitTrend(getAnalysisDailyVisitTrendURL, beginDate, endDate)
}
// GetAnalysisMonthlyVisitTrend 获取用户访问小程序数据月趋势
func (wxa *MiniProgram) GetAnalysisMonthlyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
return wxa.getAnalysisVisitTrend(getAnalysisMonthlyVisitTrendURL, beginDate, endDate)
func (analysis *Analysis) GetAnalysisMonthlyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
return analysis.getAnalysisVisitTrend(getAnalysisMonthlyVisitTrendURL, beginDate, endDate)
}
// GetAnalysisWeeklyVisitTrend 获取用户访问小程序数据周趋势
func (wxa *MiniProgram) GetAnalysisWeeklyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
return wxa.getAnalysisVisitTrend(getAnalysisWeeklyVisitTrendURL, beginDate, endDate)
func (analysis *Analysis) GetAnalysisWeeklyVisitTrend(beginDate, endDate string) (result ResAnalysisVisitTrend, err error) {
return analysis.getAnalysisVisitTrend(getAnalysisWeeklyVisitTrendURL, beginDate, endDate)
}
// UserPortraitItem 用户画像项目
@@ -203,12 +215,12 @@ type ResAnalysisUserPortrait struct {
}
// GetAnalysisUserPortrait 获取小程序新增或活跃用户的画像分布数据
func (wxa *MiniProgram) GetAnalysisUserPortrait(beginDate, endDate string) (result ResAnalysisUserPortrait, err error) {
func (analysis *Analysis) GetAnalysisUserPortrait(beginDate, endDate string) (result ResAnalysisUserPortrait, err error) {
body := map[string]string{
"begin_date": beginDate,
"end_date": endDate,
}
response, err := wxa.fetchData(getAnalysisUserPortraitURL, body)
response, err := analysis.fetchData(getAnalysisUserPortraitURL, body)
if err != nil {
return
}
@@ -244,12 +256,12 @@ type ResAnalysisVisitDistribution struct {
}
// GetAnalysisVisitDistribution 获取用户小程序访问分布数据
func (wxa *MiniProgram) GetAnalysisVisitDistribution(beginDate, endDate string) (result ResAnalysisVisitDistribution, err error) {
func (analysis *Analysis) GetAnalysisVisitDistribution(beginDate, endDate string) (result ResAnalysisVisitDistribution, err error) {
body := map[string]string{
"begin_date": beginDate,
"end_date": endDate,
}
response, err := wxa.fetchData(getAnalysisVisitDistributionURL, body)
response, err := analysis.fetchData(getAnalysisVisitDistributionURL, body)
if err != nil {
return
}
@@ -284,12 +296,12 @@ type ResAnalysisVisitPage struct {
}
// GetAnalysisVisitPage 获取小程序页面访问数据
func (wxa *MiniProgram) GetAnalysisVisitPage(beginDate, endDate string) (result ResAnalysisVisitPage, err error) {
func (analysis *Analysis) GetAnalysisVisitPage(beginDate, endDate string) (result ResAnalysisVisitPage, err error) {
body := map[string]string{
"begin_date": beginDate,
"end_date": endDate,
}
response, err := wxa.fetchData(getAnalysisVisitPageURL, body)
response, err := analysis.fetchData(getAnalysisVisitPageURL, body)
if err != nil {
return
}

View File

@@ -1,16 +1,27 @@
package miniprogram
package auth
import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/miniprogram/context"
"github.com/silenceper/wechat/v2/util"
)
const (
code2SessionURL = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
)
//Auth 登录/用户信息
type Auth struct {
*context.Context
}
//NewAuth new auth
func NewAuth(ctx *context.Context) *Auth {
return &Auth{ctx}
}
// ResCode2Session 登录凭证校验的返回结果
type ResCode2Session struct {
util.CommonError
@@ -20,9 +31,9 @@ type ResCode2Session struct {
UnionID string `json:"unionid"` // 用户在开放平台的唯一标识符在满足UnionID下发条件的情况下会返回
}
// Code2Session 登录凭证校验
func (wxa *MiniProgram) Code2Session(jsCode string) (result ResCode2Session, err error) {
urlStr := fmt.Sprintf(code2SessionURL, wxa.AppID, wxa.AppSecret, jsCode)
//Code2Session 登录凭证校验
func (auth *Auth) Code2Session(jsCode string) (result ResCode2Session, err error) {
urlStr := fmt.Sprintf(code2SessionURL, auth.AppID, auth.AppSecret, jsCode)
var response []byte
response, err = util.HTTPGet(urlStr)
if err != nil {
@@ -38,3 +49,8 @@ func (wxa *MiniProgram) Code2Session(jsCode string) (result ResCode2Session, err
}
return
}
//GetPaidUnionID 用户支付完成后,获取该用户的 UnionId无需用户授权
func (auth *Auth) GetPaidUnionID() {
//TODO
}

View File

@@ -0,0 +1,15 @@
package basic
import "github.com/silenceper/wechat/v2/miniprogram/context"
//Basic struct
type Basic struct {
*context.Context
}
//NewBasic 实例
func NewBasic(context *context.Context) *Basic {
basic := new(Basic)
basic.Context = context
return basic
}

View File

@@ -1,4 +1,4 @@
package miniprogram
package basic
import (
"crypto/aes"
@@ -96,7 +96,7 @@ func getCipherText(sessionKey, encryptedData, iv string) ([]byte, error) {
}
// Decrypt 解密数据
func (wxa *MiniProgram) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo, error) {
func (basic *Basic) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo, error) {
cipherText, err := getCipherText(sessionKey, encryptedData, iv)
if err != nil {
return nil, err
@@ -106,14 +106,14 @@ func (wxa *MiniProgram) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo
if err != nil {
return nil, err
}
if userInfo.Watermark.AppID != wxa.AppID {
if userInfo.Watermark.AppID != basic.AppID {
return nil, ErrAppIDNotMatch
}
return &userInfo, nil
}
// DecryptPhone 解密数据(手机)
func (wxa *MiniProgram) DecryptPhone(sessionKey, encryptedData, iv string) (*PhoneInfo, error) {
func (basic *Basic) DecryptPhone(sessionKey, encryptedData, iv string) (*PhoneInfo, error) {
cipherText, err := getCipherText(sessionKey, encryptedData, iv)
if err != nil {
return nil, err
@@ -123,7 +123,7 @@ func (wxa *MiniProgram) DecryptPhone(sessionKey, encryptedData, iv string) (*Pho
if err != nil {
return nil, err
}
if phoneInfo.Watermark.AppID != wxa.AppID {
if phoneInfo.Watermark.AppID != basic.AppID {
return nil, ErrAppIDNotMatch
}
return &phoneInfo, nil

View File

@@ -0,0 +1,12 @@
package config
import (
"github.com/silenceper/wechat/v2/cache"
)
//Config config for 小程序
type Config struct {
AppID string `json:"app_id"` //appid
AppSecret string `json:"app_secret"` //appsecret
Cache cache.Cache
}

View File

@@ -6,12 +6,14 @@ import (
"sync"
"time"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/util"
)
const (
//AccessTokenURL 获取access_token的接口
AccessTokenURL = "https://api.weixin.qq.com/cgi-bin/token"
//CacheKeyPrefix cache前缀
CacheKeyPrefix = "gowechat_miniprogram_"
)
//ResAccessToken struct
@@ -43,7 +45,7 @@ func (ctx *Context) GetAccessToken() (accessToken string, err error) {
if ctx.accessTokenFunc != nil {
return ctx.accessTokenFunc(ctx)
}
accessTokenCacheKey := fmt.Sprintf("access_token_%s", ctx.AppID)
accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", CacheKeyPrefix, ctx.AppID)
val := ctx.Cache.Get(accessTokenCacheKey)
if val != nil {
accessToken = val.(string)
@@ -78,7 +80,7 @@ func (ctx *Context) GetAccessTokenFromServer() (resAccessToken ResAccessToken, e
return
}
accessTokenCacheKey := fmt.Sprintf("access_token_%s", ctx.AppID)
accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", CacheKeyPrefix, ctx.AppID)
expires := resAccessToken.ExpiresIn - 1500
err = ctx.Cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
return

View File

@@ -0,0 +1,18 @@
package context
import (
"sync"
"github.com/silenceper/wechat/v2/miniprogram/config"
)
// Context struct
type Context struct {
*config.Config
//accessTokenLock 读写锁 同一个AppID一个
accessTokenLock *sync.RWMutex
//accessTokenFunc 自定义获取 access token 的方法
accessTokenFunc GetAccessTokenFunc
}

View File

@@ -1,17 +1,60 @@
package miniprogram
import (
"github.com/silenceper/wechat/context"
"sync"
"github.com/silenceper/wechat/v2/miniprogram/analysis"
"github.com/silenceper/wechat/v2/miniprogram/auth"
"github.com/silenceper/wechat/v2/miniprogram/basic"
"github.com/silenceper/wechat/v2/miniprogram/config"
"github.com/silenceper/wechat/v2/miniprogram/context"
"github.com/silenceper/wechat/v2/miniprogram/qrcode"
"github.com/silenceper/wechat/v2/miniprogram/tcb"
)
// MiniProgram struct extends context
//MiniProgram 微信小程序相关API
type MiniProgram struct {
*context.Context
ctx *context.Context
}
// NewMiniProgram 实例化小程序接口
func NewMiniProgram(context *context.Context) *MiniProgram {
miniProgram := new(MiniProgram)
miniProgram.Context = context
return miniProgram
//NewMiniProgram 实例化小程序API
func NewMiniProgram(cfg *config.Config) *MiniProgram {
if cfg.Cache == nil {
panic("cache未设置")
}
ctx := &context.Context{
Config: cfg,
}
ctx.SetAccessTokenLock(new(sync.RWMutex))
return &MiniProgram{ctx}
}
// GetContext get Context
func (miniProgram *MiniProgram) GetContext() *context.Context {
return miniProgram.ctx
}
// GetBasic 基础接口(小程序加解密)
func (miniProgram *MiniProgram) GetBasic() *basic.Basic {
return basic.NewBasic(miniProgram.ctx)
}
//GetAuth 登录/用户信息相关接口
func (miniProgram *MiniProgram) GetAuth() *auth.Auth {
return auth.NewAuth(miniProgram.ctx)
}
//GetAnalysis 数据分析
func (miniProgram *MiniProgram) GetAnalysis() *analysis.Analysis {
return analysis.NewAnalysis(miniProgram.ctx)
}
//GetQRCode 小程序码相关API
func (miniProgram *MiniProgram) GetQRCode() *qrcode.QRCode {
return qrcode.NewQRCode(miniProgram.ctx)
}
//GetTcb 小程序云开发API
func (miniProgram *MiniProgram) GetTcb() *tcb.Tcb {
return tcb.NewTcb(miniProgram.ctx)
}

View File

@@ -1,11 +1,12 @@
package miniprogram
package qrcode
import (
"encoding/json"
"fmt"
"strings"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/miniprogram/context"
"github.com/silenceper/wechat/v2/util"
)
const (
@@ -14,6 +15,25 @@ const (
getWXACodeUnlimitURL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%s"
)
//QRCode struct
type QRCode struct {
*context.Context
}
//NewQRCode 实例
func NewQRCode(context *context.Context) *QRCode {
qrCode := new(QRCode)
qrCode.Context = context
return qrCode
}
// Color QRCode color
type Color struct {
R string `json:"r"`
G string `json:"g"`
B string `json:"b"`
}
// QRCoder 小程序码参数
type QRCoder struct {
// page 必须是已经发布的小程序存在的页面,根路径前不要填加 /,不能携带参数参数请放在scene字段里如果不填写这个字段默认跳主页面
@@ -32,17 +52,10 @@ type QRCoder struct {
IsHyaline bool `json:"is_hyaline,omitempty"`
}
// Color QRCode color
type Color struct {
R string `json:"r"`
G string `json:"g"`
B string `json:"b"`
}
// fetchCode 请求并返回二维码二进制数据
func (wxa *MiniProgram) fetchCode(urlStr string, body interface{}) (response []byte, err error) {
func (qrCode *QRCode) fetchCode(urlStr string, body interface{}) (response []byte, err error) {
var accessToken string
accessToken, err = wxa.GetAccessToken()
accessToken, err = qrCode.GetAccessToken()
if err != nil {
return
}
@@ -74,18 +87,18 @@ func (wxa *MiniProgram) fetchCode(urlStr string, body interface{}) (response []b
// CreateWXAQRCode 获取小程序二维码,适用于需要的码数量较少的业务场景
// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/createWXAQRCode.html
func (wxa *MiniProgram) CreateWXAQRCode(coderParams QRCoder) (response []byte, err error) {
return wxa.fetchCode(createWXAQRCodeURL, coderParams)
func (qrCode *QRCode) CreateWXAQRCode(coderParams QRCoder) (response []byte, err error) {
return qrCode.fetchCode(createWXAQRCodeURL, coderParams)
}
// GetWXACode 获取小程序码,适用于需要的码数量较少的业务场景
// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/getWXACode.html
func (wxa *MiniProgram) GetWXACode(coderParams QRCoder) (response []byte, err error) {
return wxa.fetchCode(getWXACodeURL, coderParams)
func (qrCode *QRCode) GetWXACode(coderParams QRCoder) (response []byte, err error) {
return qrCode.fetchCode(getWXACodeURL, coderParams)
}
// GetWXACodeUnlimit 获取小程序码,适用于需要的码数量极多的业务场景
// 文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/getWXACodeUnlimit.html
func (wxa *MiniProgram) GetWXACodeUnlimit(coderParams QRCoder) (response []byte, err error) {
return wxa.fetchCode(getWXACodeUnlimitURL, coderParams)
func (qrCode *QRCode) GetWXACodeUnlimit(coderParams QRCoder) (response []byte, err error) {
return qrCode.fetchCode(getWXACodeUnlimitURL, coderParams)
}

View File

@@ -29,4 +29,4 @@ if err != nil {
}
```
更多使用方法参考[GODOC](https://godoc.org/github.com/silenceper/wechat/tcb)
更多使用方法参考[GODOC](https://godoc.org/github.com/silenceper/wechat/v2/tcb)

View File

@@ -3,7 +3,7 @@ package tcb
import (
"fmt"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/util"
)
const (

View File

@@ -3,7 +3,7 @@ package tcb
import (
"fmt"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/util"
)
const (

View File

@@ -3,7 +3,7 @@ package tcb
import (
"fmt"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/util"
)
const (

15
miniprogram/tcb/tcb.go Normal file
View File

@@ -0,0 +1,15 @@
package tcb
import "github.com/silenceper/wechat/v2/miniprogram/context"
//Tcb Tencent Cloud Base
type Tcb struct {
*context.Context
}
//NewTcb new Tencent Cloud Base
func NewTcb(context *context.Context) *Tcb {
return &Tcb{
context,
}
}

View File

@@ -1,95 +0,0 @@
package oauth
import (
"encoding/json"
"fmt"
"net/url"
"github.com/silenceper/wechat/util"
)
var (
qyRedirectOauthURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&agentid=%s&state=%s#wechat_redirect"
qyUserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=%s&code=%s"
qyUserDetailURL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserdetail"
)
//GetQyRedirectURL 获取企业微信跳转的url地址
func (oauth *Oauth) GetQyRedirectURL(redirectURI, agentid, scope, state string) (string, error) {
//url encode
urlStr := url.QueryEscape(redirectURI)
return fmt.Sprintf(qyRedirectOauthURL, oauth.AppID, urlStr, scope, agentid, state), nil
}
//QyUserInfo 用户授权获取到用户信息
type QyUserInfo struct {
util.CommonError
UserID string `json:"UserId"`
DeviceID string `json:"DeviceId"`
UserTicket string `json:"user_ticket"`
ExpiresIn int64 `json:"expires_in"`
}
//GetQyUserInfoByCode 根据code获取企业user_info
func (oauth *Oauth) GetQyUserInfoByCode(code string) (result QyUserInfo, err error) {
qyAccessToken, e := oauth.GetQyAccessToken()
if e != nil {
err = e
return
}
urlStr := fmt.Sprintf(qyUserInfoURL, qyAccessToken, code)
var response []byte
response, err = util.HTTPGet(urlStr)
if err != nil {
return
}
err = json.Unmarshal(response, &result)
if err != nil {
return
}
if result.ErrCode != 0 {
err = fmt.Errorf("GetQyUserInfoByCode error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
return
}
return
}
//QyUserDetail 到用户详情
type QyUserDetail struct {
util.CommonError
UserID string `json:"UserId"`
Name string `json:"name"`
Mobile string `json:"mobile"`
Gender string `json:"gender"`
Email string `json:"email"`
Avatar string `json:"avatar"`
QrCode string `json:"qr_code"`
}
//GetQyUserDetailUserTicket 根据user_ticket获取到用户详情
func (oauth *Oauth) GetQyUserDetailUserTicket(userTicket string) (result QyUserDetail, err error) {
var qyAccessToken string
qyAccessToken, err = oauth.GetQyAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s", qyUserDetailURL, qyAccessToken)
var response []byte
response, err = util.PostJSON(uri, map[string]string{
"user_ticket": userTicket,
})
if err != nil {
return
}
err = json.Unmarshal(response, &result)
if err != nil {
return
}
if result.ErrCode != 0 {
err = fmt.Errorf("GetQyUserDetailUserTicket Error , errcode=%d , errmsg=%s", result.ErrCode, result.ErrMsg)
return
}
return
}

View File

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

View File

@@ -0,0 +1,15 @@
package basic
import "github.com/silenceper/wechat/v2/officialaccount/context"
//Basic struct
type Basic struct {
*context.Context
}
//NewBasic 实例
func NewBasic(context *context.Context) *Basic {
basic := new(Basic)
basic.Context = context
return basic
}

View File

@@ -1,4 +1,4 @@
package qr
package basic
import (
"encoding/json"
@@ -6,8 +6,7 @@ import (
"reflect"
"time"
"github.com/silenceper/wechat/context"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/util"
)
const (
@@ -23,18 +22,6 @@ const (
actionLimitStr = "QR_LIMIT_STR_SCENE"
)
// QR 二维码
type QR struct {
*context.Context
}
//NewQR 二维码实例
func NewQR(context *context.Context) *QR {
q := new(QR)
q.Context = context
return q
}
// Request 临时二维码
type Request struct {
ExpireSeconds int64 `json:"expire_seconds,omitempty"`
@@ -56,8 +43,8 @@ type Ticket struct {
}
// GetQRTicket 获取二维码 Ticket
func (q *QR) GetQRTicket(tq *Request) (t *Ticket, err error) {
accessToken, err := q.GetAccessToken()
func (basic *Basic) GetQRTicket(tq *Request) (t *Ticket, err error) {
accessToken, err := basic.GetAccessToken()
if err != nil {
return
}

View File

@@ -0,0 +1,14 @@
package config
import (
"github.com/silenceper/wechat/v2/cache"
)
//Config config for 微信公众号
type Config struct {
AppID string `json:"app_id"` //appid
AppSecret string `json:"app_secret"` //appsecret
Token string `json:"token"` //token
EncodingAESKey string `json:"encoding_aes_key"` //EncodingAESKey
Cache cache.Cache
}

View File

@@ -0,0 +1,87 @@
package context
import (
"encoding/json"
"fmt"
"sync"
"time"
"github.com/silenceper/wechat/v2/util"
)
const (
//AccessTokenURL 获取access_token的接口
AccessTokenURL = "https://api.weixin.qq.com/cgi-bin/token"
//CacheKeyPrefix 微信公众号cache key前缀
CacheKeyPrefix = "gowechat_officialaccount_"
)
//ResAccessToken struct
type ResAccessToken struct {
util.CommonError
AccessToken string `json:"access_token"`
ExpiresIn int64 `json:"expires_in"`
}
//GetAccessTokenFunc 获取 access token 的函数签名
type GetAccessTokenFunc func(ctx *Context) (accessToken string, err error)
//SetAccessTokenLock 设置读写锁一个appID一个读写锁
func (ctx *Context) SetAccessTokenLock(l *sync.RWMutex) {
ctx.accessTokenLock = l
}
//SetGetAccessTokenFunc 设置自定义获取accessToken的方式, 需要自己实现缓存
func (ctx *Context) SetGetAccessTokenFunc(f GetAccessTokenFunc) {
ctx.accessTokenFunc = f
}
//GetAccessToken 获取access_token
func (ctx *Context) GetAccessToken() (accessToken string, err error) {
ctx.accessTokenLock.Lock()
defer ctx.accessTokenLock.Unlock()
if ctx.accessTokenFunc != nil {
return ctx.accessTokenFunc(ctx)
}
accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", CacheKeyPrefix, ctx.AppID)
val := ctx.Cache.Get(accessTokenCacheKey)
if val != nil {
accessToken = val.(string)
return
}
//从微信服务器获取
var resAccessToken ResAccessToken
resAccessToken, err = ctx.GetAccessTokenFromServer()
if err != nil {
return
}
accessToken = resAccessToken.AccessToken
return
}
//GetAccessTokenFromServer 强制从微信服务器获取token
func (ctx *Context) GetAccessTokenFromServer() (resAccessToken ResAccessToken, err error) {
url := fmt.Sprintf("%s?grant_type=client_credential&appid=%s&secret=%s", AccessTokenURL, ctx.AppID, ctx.AppSecret)
var body []byte
body, err = util.HTTPGet(url)
if err != nil {
return
}
err = json.Unmarshal(body, &resAccessToken)
if err != nil {
return
}
if resAccessToken.ErrMsg != "" {
err = fmt.Errorf("get access_token error : errcode=%v , errormsg=%v", resAccessToken.ErrCode, resAccessToken.ErrMsg)
return
}
accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", CacheKeyPrefix, ctx.AppID)
expires := resAccessToken.ExpiresIn - 1500
err = ctx.Cache.Set(accessTokenCacheKey, resAccessToken.AccessToken, time.Duration(expires)*time.Second)
return
}

View File

@@ -0,0 +1,31 @@
package context
import (
"sync"
"github.com/silenceper/wechat/v2/officialaccount/config"
)
// Context struct
type Context struct {
*config.Config
//accessTokenLock 读写锁 同一个AppID一个
accessTokenLock *sync.RWMutex
//jsAPITicket 读写锁 同一个AppID一个
jsAPITicketLock *sync.RWMutex
//accessTokenFunc 自定义获取 access token 的方法
accessTokenFunc GetAccessTokenFunc
}
// SetJsAPITicketLock 设置jsAPITicket的lock
func (ctx *Context) SetJsAPITicketLock(lock *sync.RWMutex) {
ctx.jsAPITicketLock = lock
}
// GetJsAPITicketLock 获取jsAPITicket 的lock
func (ctx *Context) GetJsAPITicketLock() *sync.RWMutex {
return ctx.jsAPITicketLock
}

View File

@@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/util"
)
const (

View File

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

View File

@@ -4,8 +4,8 @@ import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/context"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/officialaccount/context"
"github.com/silenceper/wechat/v2/util"
)
const (

View File

@@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/util"
)
//ResCreateQRCode 获取二维码的返回实体

View File

@@ -5,8 +5,8 @@ import (
"fmt"
"time"
"github.com/silenceper/wechat/context"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/officialaccount/context"
"github.com/silenceper/wechat/v2/util"
)
const getTicketURL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi"
@@ -67,7 +67,7 @@ func (js *Js) GetTicket() (ticketStr string, err error) {
defer js.GetJsAPITicketLock().Unlock()
//先从cache中取
jsAPITicketCacheKey := fmt.Sprintf("jsapi_ticket_%s", js.AppID)
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", context.CacheKeyPrefix, js.AppID)
val := js.Cache.Get(jsAPITicketCacheKey)
if val != nil {
ticketStr = val.(string)
@@ -102,7 +102,7 @@ func (js *Js) getTicketFromServer() (ticket resTicket, err error) {
return
}
jsAPITicketCacheKey := fmt.Sprintf("jsapi_ticket_%s", js.AppID)
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", context.CacheKeyPrefix, js.AppID)
expires := ticket.ExpiresIn - 1500
err = js.Cache.Set(jsAPITicketCacheKey, ticket.Ticket, time.Duration(expires)*time.Second)
return

View File

@@ -5,8 +5,8 @@ import (
"errors"
"fmt"
"github.com/silenceper/wechat/context"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/officialaccount/context"
"github.com/silenceper/wechat/v2/util"
)
const (

View File

@@ -4,7 +4,7 @@ import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/util"
)
//MediaType 媒体文件类型

View File

@@ -4,8 +4,8 @@ import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/context"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/officialaccount/context"
"github.com/silenceper/wechat/v2/util"
)
const (

View File

@@ -3,8 +3,9 @@ package message
import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/context"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/officialaccount/context"
"github.com/silenceper/wechat/v2/util"
)
const (

View File

@@ -3,7 +3,7 @@ package message
import (
"encoding/xml"
"github.com/silenceper/wechat/device"
"github.com/silenceper/wechat/v2/officialaccount/device"
)
// MsgType 基本消息类型
@@ -72,6 +72,8 @@ const (
)
const (
//微信开放平台需要用到
// InfoTypeVerifyTicket 返回ticket
InfoTypeVerifyTicket InfoType = "component_verify_ticket"
// InfoTypeAuthorized 授权

View File

@@ -4,8 +4,8 @@ import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/context"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/officialaccount/context"
"github.com/silenceper/wechat/v2/util"
)
const (

View File

@@ -6,8 +6,8 @@ import (
"net/http"
"net/url"
"github.com/silenceper/wechat/context"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/officialaccount/context"
"github.com/silenceper/wechat/v2/util"
)
const (

View File

@@ -0,0 +1,94 @@
package officialaccount
import (
"net/http"
"sync"
"github.com/silenceper/wechat/v2/officialaccount/basic"
"github.com/silenceper/wechat/v2/officialaccount/config"
"github.com/silenceper/wechat/v2/officialaccount/context"
"github.com/silenceper/wechat/v2/officialaccount/device"
"github.com/silenceper/wechat/v2/officialaccount/js"
"github.com/silenceper/wechat/v2/officialaccount/material"
"github.com/silenceper/wechat/v2/officialaccount/menu"
"github.com/silenceper/wechat/v2/officialaccount/message"
"github.com/silenceper/wechat/v2/officialaccount/oauth"
"github.com/silenceper/wechat/v2/officialaccount/server"
"github.com/silenceper/wechat/v2/officialaccount/user"
)
//OfficialAccount 微信公众号相关API
type OfficialAccount struct {
ctx *context.Context
}
//NewOfficialAccount 实例化公众号API
func NewOfficialAccount(cfg *config.Config) *OfficialAccount {
//if cfg.Cache == nil {
// panic("cache未设置")
//}
ctx := &context.Context{
Config: cfg,
}
ctx.SetAccessTokenLock(new(sync.RWMutex))
ctx.SetJsAPITicketLock(new(sync.RWMutex))
return &OfficialAccount{ctx}
}
// GetContext get Context
func (officialAccount *OfficialAccount) GetContext() *context.Context {
return officialAccount.ctx
}
// GetBasic qr/url 相关配置
func (officialAccount *OfficialAccount) GetBasic() *basic.Basic {
return basic.NewBasic(officialAccount.ctx)
}
// GetMenu 菜单管理接口
func (officialAccount *OfficialAccount) GetMenu() *menu.Menu {
return menu.NewMenu(officialAccount.ctx)
}
// GetServer 消息管理
func (officialAccount *OfficialAccount) GetServer(req *http.Request, writer http.ResponseWriter) *server.Server {
srv := server.NewServer(officialAccount.ctx)
srv.Request = req
srv.Writer = writer
return srv
}
//GetAccessToken 获取access_token
func (officialAccount *OfficialAccount) GetAccessToken() (string, error) {
return officialAccount.ctx.GetAccessToken()
}
// GetOauth oauth2网页授权
func (officialAccount *OfficialAccount) GetOauth() *oauth.Oauth {
return oauth.NewOauth(officialAccount.ctx)
}
// GetMaterial 素材管理
func (officialAccount *OfficialAccount) GetMaterial() *material.Material {
return material.NewMaterial(officialAccount.ctx)
}
// GetJs js-sdk配置
func (officialAccount *OfficialAccount) GetJs() *js.Js {
return js.NewJs(officialAccount.ctx)
}
// GetUser 用户管理接口
func (officialAccount *OfficialAccount) GetUser() *user.User {
return user.NewUser(officialAccount.ctx)
}
// GetTemplate 模板消息接口
func (officialAccount *OfficialAccount) GetTemplate() *message.Template {
return message.NewTemplate(officialAccount.ctx)
}
// GetDevice 获取智能设备的实例
func (officialAccount *OfficialAccount) GetDevice() *device.Device {
return device.NewDevice(officialAccount.ctx)
}

View File

@@ -5,20 +5,25 @@ import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"runtime/debug"
"strconv"
"github.com/silenceper/wechat/context"
"github.com/silenceper/wechat/message"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/officialaccount/context"
"github.com/silenceper/wechat/v2/officialaccount/message"
log "github.com/sirupsen/logrus"
"github.com/silenceper/wechat/v2/util"
)
//Server struct
type Server struct {
*context.Context
Writer http.ResponseWriter
Request *http.Request
debug bool
skipValidate bool
openID string
@@ -42,14 +47,15 @@ func NewServer(context *context.Context) *Server {
return srv
}
// SetDebug set debug field
func (srv *Server) SetDebug(debug bool) {
srv.debug = debug
// SkipValidate set skip validate
func (srv *Server) SkipValidate(skip bool) {
srv.skipValidate = skip
}
//Serve 处理微信的请求消息
func (srv *Server) Serve() error {
if !srv.Validate() {
log.Error("Validate Signature Failed.")
return fmt.Errorf("请求校验失败")
}
@@ -64,22 +70,21 @@ func (srv *Server) Serve() error {
return err
}
//debug
if srv.debug {
fmt.Println("request msg = ", string(srv.requestRawXMLMsg))
}
//debug print request msg
log.Debugf("request msg =%s", string(srv.requestRawXMLMsg))
return srv.buildResponse(response)
}
//Validate 校验请求是否合法
func (srv *Server) Validate() bool {
if srv.debug {
if srv.skipValidate {
return true
}
timestamp := srv.Query("timestamp")
nonce := srv.Query("nonce")
signature := srv.Query("signature")
log.Debugf("validate signature, timestamp=%s, nonce=%s", timestamp, nonce)
return signature == util.Signature(srv.Token, timestamp, nonce)
}
@@ -219,6 +224,7 @@ func (srv *Server) buildResponse(reply *message.Reply) (err error) {
//Send 将自定义的消息发送
func (srv *Server) Send() (err error) {
replyMsg := srv.responseMsg
log.Debugf("response msg =%+v", replyMsg)
if srv.isSafeMode {
//安全模式下对消息进行加密
var encryptedMsg []byte

View File

@@ -0,0 +1,58 @@
package server
import (
"encoding/xml"
"net/http"
)
var xmlContentType = []string{"application/xml; charset=utf-8"}
var plainContentType = []string{"text/plain; charset=utf-8"}
func writeContextType(w http.ResponseWriter, value []string) {
header := w.Header()
if val := header["Content-Type"]; len(val) == 0 {
header["Content-Type"] = value
}
}
//Render render from bytes
func (srv *Server) Render(bytes []byte) {
//debug
//fmt.Println("response msg = ", string(bytes))
srv.Writer.WriteHeader(200)
_, err := srv.Writer.Write(bytes)
if err != nil {
panic(err)
}
}
//String render from string
func (srv *Server) String(str string) {
writeContextType(srv.Writer, plainContentType)
srv.Render([]byte(str))
}
//XML render to xml
func (srv *Server) XML(obj interface{}) {
writeContextType(srv.Writer, xmlContentType)
bytes, err := xml.Marshal(obj)
if err != nil {
panic(err)
}
srv.Render(bytes)
}
// Query returns the keyed url query value if it exists
func (srv *Server) Query(key string) string {
value, _ := srv.GetQuery(key)
return value
}
// GetQuery is like Query(), it returns the keyed url query value
func (srv *Server) GetQuery(key string) (string, bool) {
req := srv.Request
if values, ok := req.URL.Query()[key]; ok && len(values) > 0 {
return values[0], true
}
return "", false
}

View File

@@ -5,8 +5,8 @@ import (
"fmt"
"net/url"
"github.com/silenceper/wechat/context"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/officialaccount/context"
"github.com/silenceper/wechat/v2/util"
)
const (

55
openplatform/README.md Normal file
View File

@@ -0,0 +1,55 @@
# 微信开放平台
状态:开发中
[官方文档](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Third_party_platform_appid.html)
## 快速入门
```go
wc := wechat.NewWechat()
memory := cache.NewMemory()
cfg := &openplatform.Config{
AppID: "xxx",
AppSecret: "xxx",
Token: "xxx",
EncodingAESKey: "xxx",
Cache: memory,
}
//授权的第三方公众号的appID
appID := "xxx"
openPlatform := wc.GetOpenPlatform(cfg)
officialAccount := openPlatform.GetOfficialAccount(appID)
// 传入request和responseWriter
server := officialAccount.GetServer(req, rw)
//设置接收消息的处理方法
server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {
if msg.InfoType == message.InfoTypeVerifyTicket {
componentVerifyTicket, err := openPlatform.SetComponentAccessToken(msg.ComponentVerifyTicket)
if err != nil {
log.Println(err)
return nil
}
//debug
fmt.Println(componentVerifyTicket)
rw.Write([]byte("success"))
return nil
}
//handle other message
//
return nil
})
//处理消息接收以及回复
err := server.Serve()
if err != nil {
fmt.Println(err)
return
}
//发送回复的消息
server.Send()
```

View File

@@ -0,0 +1,14 @@
package config
import (
"github.com/silenceper/wechat/v2/cache"
)
//Config config for 微信开放平台
type Config struct {
AppID string `json:"app_id"` //appid
AppSecret string `json:"app_secret"` //appsecret
Token string `json:"token"` //token
EncodingAESKey string `json:"encoding_aes_key"` //EncodingAESKey
Cache cache.Cache
}

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"time"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/util"
)
const (
@@ -52,7 +52,9 @@ func (ctx *Context) SetComponentAccessToken(verifyTicket string) (*ComponentAcce
accessTokenCacheKey := fmt.Sprintf("component_access_token_%s", ctx.AppID)
expires := at.ExpiresIn - 1500
ctx.Cache.Set(accessTokenCacheKey, at.AccessToken, time.Duration(expires)*time.Second)
if err := ctx.Cache.Set(accessTokenCacheKey, at.AccessToken, time.Duration(expires)*time.Second); err != nil {
return nil, nil
}
return at, nil
}
@@ -157,8 +159,9 @@ func (ctx *Context) RefreshAuthrToken(appid, refreshToken string) (*AuthrAccessT
}
authrTokenKey := "authorizer_access_token_" + appid
ctx.Cache.Set(authrTokenKey, ret.AccessToken, time.Minute*80)
if err := ctx.Cache.Set(authrTokenKey, ret.AccessToken, time.Minute*80); err != nil {
return nil, err
}
return ret, nil
}

View File

@@ -0,0 +1,10 @@
package context
import (
"github.com/silenceper/wechat/v2/openplatform/config"
)
// Context struct
type Context struct {
*config.Config
}

View File

@@ -0,0 +1,52 @@
package basic
import (
"fmt"
openContext "github.com/silenceper/wechat/v2/openplatform/context"
"github.com/silenceper/wechat/v2/util"
)
const (
getAccountBasicInfoURL = "https://api.weixin.qq.com/cgi-bin/account/getaccountbasicinfo"
)
//Basic 基础信息设置
type Basic struct {
*openContext.Context
appID string
}
//NewBasic new
func NewBasic(opContext *openContext.Context, appID string) *Basic {
return &Basic{Context: opContext, appID: appID}
}
//AccountBasicInfo 基础信息
type AccountBasicInfo struct {
util.CommonError
}
//GetAccountBasicInfo 获取小程序基础信息
//reference:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Mini_Programs/Mini_Program_Information_Settings.html
func (basic *Basic) GetAccountBasicInfo() (*AccountBasicInfo, error) {
ak, err := basic.GetAuthrAccessToken(basic.AppID)
if err != nil {
return nil, err
}
url := fmt.Sprintf("%s?access_token=%s", getAccountBasicInfoURL, ak)
data, err := util.HTTPGet(url)
if err != nil {
return nil, err
}
result := &AccountBasicInfo{}
if err := util.DecodeWithError(data, result, "account/getaccountbasicinfo"); err != nil {
return nil, err
}
return result, nil
}
//modify_domain设置服务器域名
//TODO
//func (basic *Basic) modifyDomain() {
//}

View File

@@ -0,0 +1,69 @@
package component
import (
"fmt"
openContext "github.com/silenceper/wechat/v2/openplatform/context"
"github.com/silenceper/wechat/v2/util"
)
const (
fastregisterweappURL = "https://api.weixin.qq.com/cgi-bin/component/fastregisterweapp"
)
//Component 快速创建小程序
type Component struct {
*openContext.Context
}
//NewComponent new
func NewComponent(opContext *openContext.Context) *Component {
return &Component{opContext}
}
//RegisterMiniProgramParam 快速注册小程序参数
type RegisterMiniProgramParam struct {
Name string `json:"name"` //企业名
Code string `json:"code"` //企业代码
CodeType string `json:"code_type"` //企业代码类型 1统一社会信用代码18 位) 2组织机构代码9 位 xxxxxxxx-x 3营业执照注册号(15 位)
LegalPersonaWechat string `json:"legal_persona_wechat"` //法人微信号
LegalPersonaName string `json:"legal_persona_name"` //法人姓名(绑定银行卡)
ComponentPhone string `json:"component_phone"` //第三方联系电话(方便法人与第三方联系)
}
//RegisterMiniProgram 快速创建小程
//reference: https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Mini_Programs/Fast_Registration_Interface_document.html
func (component *Component) RegisterMiniProgram(param *RegisterMiniProgramParam) error {
componentAK, err := component.GetComponentAccessToken()
if err != nil {
return nil
}
url := fmt.Sprintf(fastregisterweappURL+"?action=create&component_access_token=%s", componentAK)
data, err := util.PostJSON(url, param)
if err != nil {
return err
}
return util.DecodeWithCommonError(data, "component/fastregisterweapp?action=create")
}
//GetRegistrationStatusParam 查询任务创建状态
type GetRegistrationStatusParam struct {
Name string `json:"name"` //企业名
LegalPersonaWechat string `json:"legal_persona_wechat"` //法人微信号
LegalPersonaName string `json:"legal_persona_name"` //法人姓名(绑定银行卡)
}
//GetRegistrationStatus 查询创建任务状态.
func (component *Component) GetRegistrationStatus(param *GetRegistrationStatusParam) error {
componentAK, err := component.GetComponentAccessToken()
if err != nil {
return nil
}
url := fmt.Sprintf(fastregisterweappURL+"?action=search&component_access_token=%s", componentAK)
data, err := util.PostJSON(url, param)
if err != nil {
return err
}
return util.DecodeWithCommonError(data, "component/fastregisterweapp?action=search")
}

View File

@@ -0,0 +1,32 @@
package miniprogram
import (
openContext "github.com/silenceper/wechat/v2/openplatform/context"
"github.com/silenceper/wechat/v2/openplatform/miniprogram/basic"
"github.com/silenceper/wechat/v2/openplatform/miniprogram/component"
)
//MiniProgram 代小程序实现业务
type MiniProgram struct {
AppID string
openContext *openContext.Context
}
//NewMiniProgram 实例化
func NewMiniProgram(opCtx *openContext.Context, appID string) *MiniProgram {
return &MiniProgram{
openContext: opCtx,
AppID: appID,
}
}
//GetComponent get component
//快速注册小程序相关
func (miniProgram *MiniProgram) GetComponent() *component.Component {
return component.NewComponent(miniProgram.openContext)
}
//GetBasic 基础信息设置
func (miniProgram *MiniProgram) GetBasic() *basic.Basic {
return basic.NewBasic(miniProgram.openContext, miniProgram.AppID)
}

View File

@@ -0,0 +1,33 @@
package officialaccount
import (
"github.com/silenceper/wechat/v2/officialaccount"
offConfig "github.com/silenceper/wechat/v2/officialaccount/config"
offContext "github.com/silenceper/wechat/v2/officialaccount/context"
opContext "github.com/silenceper/wechat/v2/openplatform/context"
)
//OfficialAccount 代公众号实现业务
type OfficialAccount struct {
//授权的公众号的appID
appID string
*officialaccount.OfficialAccount
opContext *opContext.Context
}
//NewOfficialAccount 实例化
//appID :为授权方公众号 APPID非开放平台第三方平台 APPID
func NewOfficialAccount(opCtx *opContext.Context, appID string) *OfficialAccount {
officialAccount := officialaccount.NewOfficialAccount(&offConfig.Config{
AppID: opCtx.AppID,
EncodingAESKey: opCtx.EncodingAESKey,
Token: opCtx.Token,
Cache: opCtx.Cache,
})
//设置获取access_token的函数
officialAccount.GetContext().SetGetAccessTokenFunc(func(offCtx *offContext.Context) (accessToken string, err error) {
// 获取授权方的access_token
return opCtx.GetAuthrAccessToken(appID)
})
return &OfficialAccount{appID: appID, OfficialAccount: officialAccount}
}

View File

@@ -0,0 +1,34 @@
package openplatform
import (
"github.com/silenceper/wechat/v2/openplatform/config"
"github.com/silenceper/wechat/v2/openplatform/context"
"github.com/silenceper/wechat/v2/openplatform/miniprogram"
"github.com/silenceper/wechat/v2/openplatform/officialaccount"
)
//OpenPlatform 微信开放平台相关api
type OpenPlatform struct {
*context.Context
}
//NewOpenPlatform new openplatform
func NewOpenPlatform(cfg *config.Config) *OpenPlatform {
if cfg.Cache == nil {
panic("cache 未设置")
}
ctx := &context.Context{
Config: cfg,
}
return &OpenPlatform{ctx}
}
//GetOfficialAccount 公众号代处理
func (openPlatform *OpenPlatform) GetOfficialAccount(appID string) *officialaccount.OfficialAccount {
return officialaccount.NewOfficialAccount(openPlatform.Context, appID)
}
//GetMiniProgram 小程序代理
func (openPlatform *OpenPlatform) GetMiniProgram(opCtx *context.Context, appID string) *miniprogram.MiniProgram {
return miniprogram.NewMiniProgram(opCtx, appID)
}

5
pay/README.md Normal file
View File

@@ -0,0 +1,5 @@
# 微信支付
[官方文档](https://pay.weixin.qq.com/wiki/doc/api/index.html)
## 快速入门

9
pay/config/config.go Normal file
View File

@@ -0,0 +1,9 @@
package config
//Config config for pay
type Config struct {
AppID string `json:"app_id"`
MchID string `json:"mch_id"`
Key string `json:"key"`
NotifyURL string `json:"notify_url"`
}

15
pay/notify/notify.go Normal file
View File

@@ -0,0 +1,15 @@
package notify
import (
"github.com/silenceper/wechat/v2/pay/config"
)
//Notify 回调
type Notify struct {
*config.Config
}
//NewNotify new
func NewNotify(cfg *config.Config) *Notify {
return &Notify{cfg}
}

View File

@@ -1,19 +1,20 @@
package pay
package notify
import (
"fmt"
"github.com/fatih/structs"
"github.com/silenceper/wechat/util"
"github.com/spf13/cast"
"reflect"
"sort"
"strings"
"github.com/fatih/structs"
"github.com/silenceper/wechat/v2/util"
"github.com/spf13/cast"
)
// doc: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7&index=8
// NotifyResult 下单回调
type NotifyResult struct {
// PaidResult 下单回调
type PaidResult struct {
ReturnCode *string `xml:"return_code"`
ReturnMsg *string `xml:"return_msg"`
@@ -55,14 +56,14 @@ type NotifyResult struct {
TimeEnd *string `xml:"time_end"`
}
// NotifyResp 消息通知返回
type NotifyResp struct {
// PaidResp 消息通知返回
type PaidResp struct {
ReturnCode string `xml:"return_code"`
ReturnMsg string `xml:"return_msg"`
}
// VerifySign 验签
func (pcf *Pay) VerifySign(notifyRes NotifyResult) bool {
// PaidVerifySign 支付成功结果验签
func (notify *Notify) PaidVerifySign(notifyRes PaidResult) bool {
// STEP1, 转换 struct 为 map并对 map keys 做排序
resMap := structs.Map(notifyRes)
@@ -82,7 +83,7 @@ func (pcf *Pay) VerifySign(notifyRes NotifyResult) bool {
}
// STEP3, 在键值对的最后加上key=API_KEY
signStrings = signStrings + "key=" + pcf.PayKey
signStrings = signStrings + "key=" + notify.Key
// STEP4, 进行MD5签名并且将所有字符转为大写.
sign := util.MD5Sum(signStrings)
@@ -92,7 +93,7 @@ func (pcf *Pay) VerifySign(notifyRes NotifyResult) bool {
return true
}
func getTagKeyName(key string, notifyRes *NotifyResult) string {
func getTagKeyName(key string, notifyRes *PaidResult) string {
s := reflect.TypeOf(notifyRes).Elem()
f, _ := s.FieldByName(key)
name := f.Tag.Get("xml")

220
pay/order/pay.go Normal file
View File

@@ -0,0 +1,220 @@
package order
import (
"crypto/hmac"
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"encoding/xml"
"errors"
"hash"
"strconv"
"strings"
"time"
"github.com/silenceper/wechat/v2/pay/config"
"github.com/silenceper/wechat/v2/util"
)
//https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
var payGateway = "https://api.mch.weixin.qq.com/pay/unifiedorder"
// Order struct extends context
type Order struct {
*config.Config
}
// NewOrder return an instance of order package
func NewOrder(cfg *config.Config) *Order {
order := Order{cfg}
return &order
}
// Params was NEEDED when request unifiedorder
// 传入的参数,用于生成 prepay_id 的必需参数
type Params struct {
TotalFee string
CreateIP string
Body string
OutTradeNo string
OpenID string
TradeType string
SignType string
Detail string
Attach string
GoodsTag string
NotifyURL string
}
// Config 是传出用于 js sdk 用的参数
type Config struct {
Timestamp string `json:"timestamp"`
NonceStr string `json:"nonceStr"`
PrePayID string `json:"prePayId"`
SignType string `json:"signType"`
Package string `json:"package"`
PaySign string `json:"paySign"`
}
// PreOrder 是 unifie order 接口的返回
type PreOrder struct {
ReturnCode string `xml:"return_code"`
ReturnMsg string `xml:"return_msg"`
AppID string `xml:"appid,omitempty"`
MchID string `xml:"mch_id,omitempty"`
NonceStr string `xml:"nonce_str,omitempty"`
Sign string `xml:"sign,omitempty"`
ResultCode string `xml:"result_code,omitempty"`
TradeType string `xml:"trade_type,omitempty"`
PrePayID string `xml:"prepay_id,omitempty"`
CodeURL string `xml:"code_url,omitempty"`
ErrCode string `xml:"err_code,omitempty"`
ErrCodeDes string `xml:"err_code_des,omitempty"`
}
// payRequest 接口请求参数
type payRequest struct {
AppID string `xml:"appid"`
MchID string `xml:"mch_id"`
DeviceInfo string `xml:"device_info,omitempty"`
NonceStr string `xml:"nonce_str"`
Sign string `xml:"sign"`
SignType string `xml:"sign_type,omitempty"`
Body string `xml:"body"`
Detail string `xml:"detail,omitempty"`
Attach string `xml:"attach,omitempty"` // 附加数据
OutTradeNo string `xml:"out_trade_no"` // 商户订单号
FeeType string `xml:"fee_type,omitempty"` // 标价币种
TotalFee string `xml:"total_fee"` // 标价金额
SpbillCreateIP string `xml:"spbill_create_ip"` // 终端IP
TimeStart string `xml:"time_start,omitempty"` // 交易起始时间
TimeExpire string `xml:"time_expire,omitempty"` // 交易结束时间
GoodsTag string `xml:"goods_tag,omitempty"` // 订单优惠标记
NotifyURL string `xml:"notify_url"` // 通知地址
TradeType string `xml:"trade_type"` // 交易类型
ProductID string `xml:"product_id,omitempty"` // 商品ID
LimitPay string `xml:"limit_pay,omitempty"` //
OpenID string `xml:"openid,omitempty"` // 用户标识
SceneInfo string `xml:"scene_info,omitempty"` // 场景信息
}
// BridgeConfig get js bridge config
func (o *Order) BridgeConfig(p *Params) (cfg Config, err error) {
var (
buffer strings.Builder
h hash.Hash
timestamp = strconv.FormatInt(time.Now().Unix(), 10)
)
order, err := o.PrePayOrder(p)
if err != nil {
return
}
buffer.WriteString("appId=")
buffer.WriteString(order.AppID)
buffer.WriteString("&nonceStr=")
buffer.WriteString(order.NonceStr)
buffer.WriteString("&package=")
buffer.WriteString("prepay_id=" + order.PrePayID)
buffer.WriteString("&signType=")
buffer.WriteString(p.SignType)
buffer.WriteString("&timeStamp=")
buffer.WriteString(timestamp)
buffer.WriteString("&key=")
buffer.WriteString(o.Key)
if p.SignType == "MD5" {
h = md5.New()
} else {
h = hmac.New(sha256.New, []byte(o.Key))
}
h.Write([]byte(buffer.String()))
// 签名
cfg.PaySign = strings.ToUpper(hex.EncodeToString(h.Sum(nil)))
cfg.NonceStr = order.NonceStr
cfg.Timestamp = timestamp
cfg.PrePayID = order.PrePayID
cfg.SignType = p.SignType
cfg.Package = "prepay_id=" + order.PrePayID
return
}
// PrePayOrder return data for invoke wechat payment
func (o *Order) PrePayOrder(p *Params) (payOrder PreOrder, err error) {
nonceStr := util.RandomStr(32)
notifyURL := o.NotifyURL
// 签名类型
if p.SignType == "" {
p.SignType = "MD5"
}
// 通知地址
if p.NotifyURL != "" {
notifyURL = p.NotifyURL
}
param := make(map[string]interface{})
param["appid"] = o.AppID
param["body"] = p.Body
param["mch_id"] = o.MchID
param["nonce_str"] = nonceStr
param["out_trade_no"] = p.OutTradeNo
param["spbill_create_ip"] = p.CreateIP
param["total_fee"] = p.TotalFee
param["trade_type"] = p.TradeType
param["openid"] = p.OpenID
param["sign_type"] = p.SignType
param["detail"] = p.Detail
param["attach"] = p.Attach
param["goods_tag"] = p.GoodsTag
param["notify_url"] = notifyURL
bizKey := "&key=" + o.Key
str := util.OrderParam(param, bizKey)
sign := util.MD5Sum(str)
request := payRequest{
AppID: o.AppID,
MchID: o.MchID,
NonceStr: nonceStr,
Sign: sign,
Body: p.Body,
OutTradeNo: p.OutTradeNo,
TotalFee: p.TotalFee,
SpbillCreateIP: p.CreateIP,
NotifyURL: notifyURL,
TradeType: p.TradeType,
OpenID: p.OpenID,
SignType: p.SignType,
Detail: p.Detail,
Attach: p.Attach,
GoodsTag: p.GoodsTag,
}
rawRet, err := util.PostXML(payGateway, request)
if err != nil {
return
}
err = xml.Unmarshal(rawRet, &payOrder)
if err != nil {
return
}
if payOrder.ReturnCode == "SUCCESS" {
// pay success
if payOrder.ResultCode == "SUCCESS" {
err = nil
return
}
err = errors.New(payOrder.ErrCode + payOrder.ErrCodeDes)
return
}
err = errors.New("[msg : xmlUnmarshalError] [rawReturn : " + string(rawRet) + "] [params : " + str + "] [sign : " + sign + "]")
return
}
// PrePayID will request wechat merchant api and request for a pre payment order id
func (o *Order) PrePayID(p *Params) (prePayID string, err error) {
order, err := o.PrePayOrder(p)
if err != nil {
return
}
if order.PrePayID == "" {
err = errors.New("empty prepayid")
}
prePayID = order.PrePayID
return
}

View File

@@ -1,281 +1,27 @@
package pay
import (
"bytes"
"crypto/hmac"
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"encoding/xml"
"errors"
"hash"
"sort"
"strconv"
"strings"
"time"
"github.com/silenceper/wechat/context"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/pay/config"
"github.com/silenceper/wechat/v2/pay/notify"
"github.com/silenceper/wechat/v2/pay/order"
)
var payGateway = "https://api.mch.weixin.qq.com/pay/unifiedorder"
// Pay struct extends context
//Pay 微信支付相关API
type Pay struct {
*context.Context
cfg *config.Config
}
// Params was NEEDED when request unifiedorder
// 传入的参数,用于生成 prepay_id 的必需参数
type Params struct {
TotalFee string
CreateIP string
Body string
OutTradeNo string
OpenID string
TradeType string
SignType string
Detail string
Attach string
GoodsTag string
NotifyURL string
//NewPay 实例化微信支付相关API
func NewPay(cfg *config.Config) *Pay {
return &Pay{cfg}
}
// Config 是传出用于 js sdk 用的参数
type Config struct {
Timestamp string `json:"timestamp"`
NonceStr string `json:"nonceStr"`
PrePayID string `json:"prePayId"`
SignType string `json:"signType"`
Package string `json:"package"`
PaySign string `json:"paySign"`
// GetOrder 下单
func (pay *Pay) GetOrder() *order.Order {
return order.NewOrder(pay.cfg)
}
// PreOrder 是 unifie order 接口的返回
type PreOrder struct {
ReturnCode string `xml:"return_code"`
ReturnMsg string `xml:"return_msg"`
AppID string `xml:"appid,omitempty"`
MchID string `xml:"mch_id,omitempty"`
NonceStr string `xml:"nonce_str,omitempty"`
Sign string `xml:"sign,omitempty"`
ResultCode string `xml:"result_code,omitempty"`
TradeType string `xml:"trade_type,omitempty"`
PrePayID string `xml:"prepay_id,omitempty"`
CodeURL string `xml:"code_url,omitempty"`
ErrCode string `xml:"err_code,omitempty"`
ErrCodeDes string `xml:"err_code_des,omitempty"`
}
// payRequest 接口请求参数
type payRequest struct {
AppID string `xml:"appid"`
MchID string `xml:"mch_id"`
DeviceInfo string `xml:"device_info,omitempty"`
NonceStr string `xml:"nonce_str"`
Sign string `xml:"sign"`
SignType string `xml:"sign_type,omitempty"`
Body string `xml:"body"`
Detail string `xml:"detail,omitempty"`
Attach string `xml:"attach,omitempty"` // 附加数据
OutTradeNo string `xml:"out_trade_no"` // 商户订单号
FeeType string `xml:"fee_type,omitempty"` // 标价币种
TotalFee string `xml:"total_fee"` // 标价金额
SpbillCreateIP string `xml:"spbill_create_ip"` // 终端IP
TimeStart string `xml:"time_start,omitempty"` // 交易起始时间
TimeExpire string `xml:"time_expire,omitempty"` // 交易结束时间
GoodsTag string `xml:"goods_tag,omitempty"` // 订单优惠标记
NotifyURL string `xml:"notify_url"` // 通知地址
TradeType string `xml:"trade_type"` // 交易类型
ProductID string `xml:"product_id,omitempty"` // 商品ID
LimitPay string `xml:"limit_pay,omitempty"` //
OpenID string `xml:"openid,omitempty"` // 用户标识
SceneInfo string `xml:"scene_info,omitempty"` // 场景信息
}
// NewPay return an instance of Pay package
func NewPay(ctx *context.Context) *Pay {
pay := Pay{Context: ctx}
return &pay
}
// BridgeConfig get js bridge config
func (pcf *Pay) BridgeConfig(p *Params) (cfg Config, err error) {
var (
buffer strings.Builder
h hash.Hash
timestamp = strconv.FormatInt(time.Now().Unix(), 10)
)
order, err := pcf.PrePayOrder(p)
if err != nil {
return
}
buffer.WriteString("appId=")
buffer.WriteString(order.AppID)
buffer.WriteString("&nonceStr=")
buffer.WriteString(order.NonceStr)
buffer.WriteString("&package=")
buffer.WriteString("prepay_id=" + order.PrePayID)
buffer.WriteString("&signType=")
buffer.WriteString(p.SignType)
buffer.WriteString("&timeStamp=")
buffer.WriteString(timestamp)
buffer.WriteString("&key=")
buffer.WriteString(pcf.PayKey)
if p.SignType == "MD5" {
h = md5.New()
} else {
h = hmac.New(sha256.New, []byte(pcf.PayKey))
}
h.Write([]byte(buffer.String()))
// 签名
cfg.PaySign = strings.ToUpper(hex.EncodeToString(h.Sum(nil)))
cfg.NonceStr = order.NonceStr
cfg.Timestamp = timestamp
cfg.PrePayID = order.PrePayID
cfg.SignType = p.SignType
cfg.Package = "prepay_id=" + order.PrePayID
return
}
// PrePayOrder return data for invoke wechat payment
func (pcf *Pay) PrePayOrder(p *Params) (payOrder PreOrder, err error) {
nonceStr := util.RandomStr(32)
notifyURL := pcf.PayNotifyURL
// 签名类型
if p.SignType == "" {
p.SignType = "MD5"
}
// 通知地址
if p.NotifyURL != "" {
notifyURL = p.NotifyURL
}
param := make(map[string]interface{})
param["appid"] = pcf.AppID
param["body"] = p.Body
param["mch_id"] = pcf.PayMchID
param["nonce_str"] = nonceStr
param["out_trade_no"] = p.OutTradeNo
param["spbill_create_ip"] = p.CreateIP
param["total_fee"] = p.TotalFee
param["trade_type"] = p.TradeType
param["openid"] = p.OpenID
param["sign_type"] = p.SignType
param["detail"] = p.Detail
param["attach"] = p.Attach
param["goods_tag"] = p.GoodsTag
param["notify_url"] = notifyURL
bizKey := "&key=" + pcf.PayKey
str := orderParam(param, bizKey)
sign := util.MD5Sum(str)
request := payRequest{
AppID: pcf.AppID,
MchID: pcf.PayMchID,
NonceStr: nonceStr,
Sign: sign,
Body: p.Body,
OutTradeNo: p.OutTradeNo,
TotalFee: p.TotalFee,
SpbillCreateIP: p.CreateIP,
NotifyURL: notifyURL,
TradeType: p.TradeType,
OpenID: p.OpenID,
SignType: p.SignType,
Detail: p.Detail,
Attach: p.Attach,
GoodsTag: p.GoodsTag,
}
rawRet, err := util.PostXML(payGateway, request)
if err != nil {
return
}
err = xml.Unmarshal(rawRet, &payOrder)
if err != nil {
return
}
if payOrder.ReturnCode == "SUCCESS" {
// pay success
if payOrder.ResultCode == "SUCCESS" {
err = nil
return
}
err = errors.New(payOrder.ErrCode + payOrder.ErrCodeDes)
return
}
err = errors.New("[msg : xmlUnmarshalError] [rawReturn : " + string(rawRet) + "] [params : " + str + "] [sign : " + sign + "]")
return
}
// PrePayID will request wechat merchant api and request for a pre payment order id
func (pcf *Pay) PrePayID(p *Params) (prePayID string, err error) {
order, err := pcf.PrePayOrder(p)
if err != nil {
return
}
if order.PrePayID == "" {
err = errors.New("empty prepayid")
}
prePayID = order.PrePayID
return
}
// order params
func orderParam(source interface{}, bizKey string) (returnStr string) {
switch v := source.(type) {
case map[string]string:
keys := make([]string, 0, len(v))
for k := range v {
if k == "sign" {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
var buf bytes.Buffer
for _, k := range keys {
if v[k] == "" {
continue
}
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(k)
buf.WriteByte('=')
buf.WriteString(v[k])
}
buf.WriteString(bizKey)
returnStr = buf.String()
case map[string]interface{}:
keys := make([]string, 0, len(v))
for k := range v {
if k == "sign" {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
var buf bytes.Buffer
for _, k := range keys {
if v[k] == "" {
continue
}
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(k)
buf.WriteByte('=')
switch vv := v[k].(type) {
case string:
buf.WriteString(vv)
case int:
buf.WriteString(strconv.FormatInt(int64(vv), 10))
default:
panic("params type not supported")
}
}
buf.WriteString(bizKey)
returnStr = buf.String()
}
return
// GetNotify 通知
func (pay *Pay) GetNotify() *notify.Notify {
return notify.NewNotify(pay.cfg)
}

View File

@@ -1,16 +1,28 @@
package pay
package refund
import (
"encoding/xml"
"fmt"
"github.com/silenceper/wechat/util"
"github.com/silenceper/wechat/v2/pay/config"
"github.com/silenceper/wechat/v2/util"
)
var refundGateway = "https://api.mch.weixin.qq.com/secapi/pay/refund"
//RefundParams 调用参数
type RefundParams struct {
// Refund struct extends context
type Refund struct {
*config.Config
}
// NewRefund return an instance of refund package
func NewRefund(cfg *config.Config) *Refund {
refund := Refund{cfg}
return &refund
}
//Params 调用参数
type Params struct {
TransactionID string
OutRefundNo string
TotalFee string
@@ -19,8 +31,8 @@ type RefundParams struct {
RootCa string //ca证书
}
//refundRequest 接口请求参数
type refundRequest struct {
//request 接口请求参数
type request struct {
AppID string `xml:"appid"`
MchID string `xml:"mch_id"`
NonceStr string `xml:"nonce_str"`
@@ -34,8 +46,8 @@ type refundRequest struct {
//NotifyUrl string `xml:"notify_url,omitempty"`
}
//RefundResponse 接口返回
type RefundResponse struct {
//Response 接口返回
type Response struct {
ReturnCode string `xml:"return_code"`
ReturnMsg string `xml:"return_msg"`
AppID string `xml:"appid,omitempty"`
@@ -59,11 +71,11 @@ type RefundResponse struct {
}
//Refund 退款申请
func (pcf *Pay) Refund(p *RefundParams) (rsp RefundResponse, err error) {
func (refund *Refund) Refund(p *Params) (rsp Response, err error) {
nonceStr := util.RandomStr(32)
param := make(map[string]interface{})
param["appid"] = pcf.AppID
param["mch_id"] = pcf.PayMchID
param["appid"] = refund.AppID
param["mch_id"] = refund.MchID
param["nonce_str"] = nonceStr
param["out_refund_no"] = p.OutRefundNo
param["refund_desc"] = p.RefundDesc
@@ -72,12 +84,12 @@ func (pcf *Pay) Refund(p *RefundParams) (rsp RefundResponse, err error) {
param["sign_type"] = "MD5"
param["transaction_id"] = p.TransactionID
bizKey := "&key=" + pcf.PayKey
str := orderParam(param, bizKey)
bizKey := "&key=" + refund.Key
str := util.OrderParam(param, bizKey)
sign := util.MD5Sum(str)
request := refundRequest{
AppID: pcf.AppID,
MchID: pcf.PayMchID,
request := request{
AppID: refund.AppID,
MchID: refund.MchID,
NonceStr: nonceStr,
Sign: sign,
SignType: "MD5",
@@ -87,7 +99,7 @@ func (pcf *Pay) Refund(p *RefundParams) (rsp RefundResponse, err error) {
RefundFee: p.RefundFee,
RefundDesc: p.RefundDesc,
}
rawRet, err := util.PostXMLWithTLS(refundGateway, request, p.RootCa, pcf.PayMchID)
rawRet, err := util.PostXMLWithTLS(refundGateway, request, p.RootCa, refund.MchID)
if err != nil {
return
}

View File

@@ -1,16 +0,0 @@
package tcb
import "github.com/silenceper/wechat/context"
//Tcb Tencent Cloud Base
type Tcb struct{
*context.Context
}
//NewTcb new Tencent Cloud Base
func NewTcb(context *context.Context)*Tcb{
return &Tcb{
context,
}
}

67
util/param.go Normal file
View File

@@ -0,0 +1,67 @@
package util
import (
"bytes"
"sort"
"strconv"
)
// OrderParam order params
func OrderParam(source interface{}, bizKey string) (returnStr string) {
switch v := source.(type) {
case map[string]string:
keys := make([]string, 0, len(v))
for k := range v {
if k == "sign" {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
var buf bytes.Buffer
for _, k := range keys {
if v[k] == "" {
continue
}
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(k)
buf.WriteByte('=')
buf.WriteString(v[k])
}
buf.WriteString(bizKey)
returnStr = buf.String()
case map[string]interface{}:
keys := make([]string, 0, len(v))
for k := range v {
if k == "sign" {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
var buf bytes.Buffer
for _, k := range keys {
if v[k] == "" {
continue
}
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(k)
buf.WriteByte('=')
switch vv := v[k].(type) {
case string:
buf.WriteString(vv)
case int:
buf.WriteString(strconv.FormatInt(int64(vv), 10))
default:
panic("params type not supported")
}
}
buf.WriteString(bizKey)
returnStr = buf.String()
}
return
}

150
wechat.go
View File

@@ -1,125 +1,71 @@
package wechat
import (
"net/http"
"sync"
"os"
"github.com/silenceper/wechat/cache"
"github.com/silenceper/wechat/context"
"github.com/silenceper/wechat/device"
"github.com/silenceper/wechat/js"
"github.com/silenceper/wechat/material"
"github.com/silenceper/wechat/menu"
"github.com/silenceper/wechat/message"
"github.com/silenceper/wechat/miniprogram"
"github.com/silenceper/wechat/oauth"
"github.com/silenceper/wechat/pay"
"github.com/silenceper/wechat/qr"
"github.com/silenceper/wechat/server"
"github.com/silenceper/wechat/tcb"
"github.com/silenceper/wechat/user"
"github.com/silenceper/wechat/v2/cache"
"github.com/silenceper/wechat/v2/miniprogram"
miniConfig "github.com/silenceper/wechat/v2/miniprogram/config"
"github.com/silenceper/wechat/v2/officialaccount"
offConfig "github.com/silenceper/wechat/v2/officialaccount/config"
"github.com/silenceper/wechat/v2/openplatform"
openConfig "github.com/silenceper/wechat/v2/openplatform/config"
"github.com/silenceper/wechat/v2/pay"
payConfig "github.com/silenceper/wechat/v2/pay/config"
log "github.com/sirupsen/logrus"
)
func init() {
// Log as JSON instead of the default ASCII formatter.
log.SetFormatter(&log.TextFormatter{})
// Output to stdout instead of the default stderr
// Can be any io.Writer, see below for File example
log.SetOutput(os.Stdout)
// Only log the warning severity or above.
log.SetLevel(log.DebugLevel)
}
// Wechat struct
type Wechat struct {
Context *context.Context
}
// Config for user
type Config struct {
AppID string
AppSecret string
Token string
EncodingAESKey string
PayMchID string //支付 - 商户 ID
PayNotifyURL string //支付 - 接受微信支付结果通知的接口地址
PayKey string //支付 - 商户后台设置的支付 key
Cache cache.Cache
cache cache.Cache
}
// NewWechat init
func NewWechat(cfg *Config) *Wechat {
context := new(context.Context)
copyConfigToContext(cfg, context)
return &Wechat{context}
func NewWechat() *Wechat {
return &Wechat{}
}
func copyConfigToContext(cfg *Config, context *context.Context) {
context.AppID = cfg.AppID
context.AppSecret = cfg.AppSecret
context.Token = cfg.Token
context.EncodingAESKey = cfg.EncodingAESKey
context.PayMchID = cfg.PayMchID
context.PayKey = cfg.PayKey
context.PayNotifyURL = cfg.PayNotifyURL
context.Cache = cfg.Cache
context.SetAccessTokenLock(new(sync.RWMutex))
context.SetJsAPITicketLock(new(sync.RWMutex))
//SetCache 设置cache
func (wc *Wechat) SetCache(cahce cache.Cache) {
wc.cache = cahce
}
// GetServer 消息管理
func (wc *Wechat) GetServer(req *http.Request, writer http.ResponseWriter) *server.Server {
wc.Context.Request = req
wc.Context.Writer = writer
return server.NewServer(wc.Context)
}
//GetAccessToken 获取access_token
func (wc *Wechat) GetAccessToken() (string, error) {
return wc.Context.GetAccessToken()
}
// GetOauth oauth2网页授权
func (wc *Wechat) GetOauth() *oauth.Oauth {
return oauth.NewOauth(wc.Context)
}
// GetMaterial 素材管理
func (wc *Wechat) GetMaterial() *material.Material {
return material.NewMaterial(wc.Context)
}
// GetJs js-sdk配置
func (wc *Wechat) GetJs() *js.Js {
return js.NewJs(wc.Context)
}
// GetMenu 菜单管理接口
func (wc *Wechat) GetMenu() *menu.Menu {
return menu.NewMenu(wc.Context)
}
// GetUser 用户管理接口
func (wc *Wechat) GetUser() *user.User {
return user.NewUser(wc.Context)
}
// GetTemplate 模板消息接口
func (wc *Wechat) GetTemplate() *message.Template {
return message.NewTemplate(wc.Context)
}
// GetPay 返回支付消息的实例
func (wc *Wechat) GetPay() *pay.Pay {
return pay.NewPay(wc.Context)
}
// GetQR 返回二维码的实例
func (wc *Wechat) GetQR() *qr.QR {
return qr.NewQR(wc.Context)
//GetOfficialAccount 获取微信公众号实例
func (wc *Wechat) GetOfficialAccount(cfg *offConfig.Config) *officialaccount.OfficialAccount {
if cfg.Cache == nil {
cfg.Cache = wc.cache
}
return officialaccount.NewOfficialAccount(cfg)
}
// GetMiniProgram 获取小程序的实例
func (wc *Wechat) GetMiniProgram() *miniprogram.MiniProgram {
return miniprogram.NewMiniProgram(wc.Context)
func (wc *Wechat) GetMiniProgram(cfg *miniConfig.Config) *miniprogram.MiniProgram {
if cfg.Cache == nil {
cfg.Cache = wc.cache
}
return miniprogram.NewMiniProgram(cfg)
}
// GetDevice 获取智能设备的实例
func (wc *Wechat) GetDevice() *device.Device {
return device.NewDevice(wc.Context)
// GetPay 获取微信支付的实例
func (wc *Wechat) GetPay(cfg *payConfig.Config) *pay.Pay {
return pay.NewPay(cfg)
}
// GetTcb 获取小程序-云开发的实例
func (wc *Wechat) GetTcb() *tcb.Tcb {
return tcb.NewTcb(wc.Context)
// GetOpenPlatform 获取微信开放平台的实例
func (wc *Wechat) GetOpenPlatform(cfg *openConfig.Config) *openplatform.OpenPlatform {
return openplatform.NewOpenPlatform(cfg)
}

5
work/README.md Normal file
View File

@@ -0,0 +1,5 @@
# 企业微信
[官方文档](https://work.weixin.qq.com/api/doc)
## 快速入门