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

Compare commits

..

86 Commits

Author SHA1 Message Date
silenceper
3c881e3885 Merge pull request #233 from silenceper/fix-go.mod
Fix go.mod
2020-05-22 11:16:51 +08:00
silenceper
3740bb55c3 更新redis依赖 2020-05-22 11:12:40 +08:00
silenceper
c42d799367 更新redis依赖 2020-05-22 10:42:45 +08:00
silenceper
798c5b081c 更新redis依赖 2020-05-22 10:39:50 +08:00
silenceper
491ee80136 更新 github.com/gomodule/redigo@1.8.1 2020-05-22 10:28:12 +08:00
silenceper
a54b03a918 更新go.mod依赖 2020-05-22 10:23:30 +08:00
silenceper
eec3233134 Merge pull request #232 from silenceper/fix-go.mod
更新go.mod,redis依赖
2020-05-22 10:13:00 +08:00
silenceper
587ce04b5f 更新go.mod,redis依赖 2020-05-22 10:11:51 +08:00
silenceper
ada9c1ff61 Merge pull request #221 from quxiaolong1504/notify
重写 pay/NotifyResult VerifySign
2020-04-04 16:24:44 +08:00
quxiaolong
9e58e097cb 重写 pay/NotifyResult VerifySign 2020-03-27 10:58:07 +08:00
silenceper
8b6147c3ec Merge pull request #219 from bestony/patch-1
fix: update cloudbase homepage url
2020-03-18 14:54:57 +08:00
白宦成
0071852c75 update cloudbase homepage url 2020-03-18 14:50:42 +08:00
silenceper
f4491193cb Merge pull request #218 from silenceper/cloudbase
add cloudbase
2020-03-18 10:21:44 +08:00
silenceper
576a898c0f 更新文档 2020-03-17 19:47:29 +08:00
silenceper
76fde58ad9 add cloudbase 2020-03-17 19:01:51 +08:00
silenceper
903dadc260 Update README.md 2020-02-26 10:10:28 +08:00
silenceper
1efbf27bde Update README.md 2020-02-22 23:19:29 +08:00
silenceper
99a2eb659c Update .travis.yml 2020-02-16 20:26:48 +08:00
silenceper
9b06954b10 Merge pull request #202 from ckeyer/add_ThumbURL
add ThumbURL
2020-01-14 16:31:43 +08:00
silenceper
431f7d3a9f Update FUNDING.yml 2020-01-10 19:47:44 +08:00
silenceper
8e24b47a70 Update FUNDING.yml 2020-01-10 19:46:35 +08:00
silenceper
83bd282760 Update README.md
增加公众号
2020-01-10 19:43:49 +08:00
Chuanjian Wang
bf167d959c add ThumbURL 2019-12-25 11:23:45 +08:00
silenceper
08b69d9419 Merge pull request #199 from silenceper/develop
fix tcb example
2019-11-26 13:53:48 +08:00
silenceper
ab39ec00d4 fix tcb example 2019-11-26 13:53:08 +08:00
silenceper
cafb84d6da Merge pull request #198 from silenceper/develop
增加小程序云开发http api
2019-11-26 13:49:49 +08:00
silenceper
d46df74eee 1、fix golnt错误
2、将包管理改为go mod
2019-11-26 13:44:55 +08:00
silenceper
96678d2279 增加小程序-云开发http api 2019-11-26 13:09:36 +08:00
silenceper
6e5bb2553d Merge pull request #196 from cielu/master
调整map、验签增加 attach参数
2019-11-21 22:27:54 +08:00
ciel yu
1dbe3f60ea 验签增加 attach 参数 2019-11-20 17:49:09 +08:00
cielu
0f99e2e34a Merge pull request #2 from silenceper/master
update into new
2019-11-20 17:42:04 +08:00
silenceper
7ea817a7c6 1、使用go mod 管理依赖
2、将template模板消息,移入到message文件夹
2019-11-19 22:51:40 +08:00
silenceper
de140f1037 Merge branch 'master' into develop 2019-11-19 18:54:53 +08:00
silenceper
d91e82c183 Merge pull request #194 from silenceper/hotfix/add-cdata
序列化 xml 时添加 cdata 标签
2019-11-18 20:54:38 +08:00
silenceper
3111e12c00 Merge pull request #187 from cielu/master
支付,增加通知回调,签名验证
2019-11-18 20:49:00 +08:00
silenceper
9c8717afba Merge pull request #169 from ckeyer/get_news
增加获取永久素材接口
2019-11-18 20:48:32 +08:00
silenceper
d7f371cb65 序列化 xml 时添加 cdata 标签 2019-11-18 20:45:32 +08:00
silenceper
39e81b45bf Merge pull request #192 from larry-dev/master
增加了关于微信智能硬件的一些方式
2019-11-18 09:35:58 +08:00
larry.liu
733c53a044 格式化代码 2019-11-15 16:32:25 +08:00
larry.liu
5617c9512d 格式化代码样式 2019-11-15 15:56:38 +08:00
larry.liu
d806d1c968 Merge remote-tracking branch 'origin/master' 2019-11-15 15:34:32 +08:00
larry.liu
254ac9d7a6 修改对象类型错误 2019-11-15 15:34:17 +08:00
silenceper
19e3174107 Update .travis.yml 2019-11-15 15:01:03 +08:00
liu larry
0dbc292f44 Merge branch 'master' into master 2019-11-15 14:55:11 +08:00
larry.liu
33d00f45c5 增加设备回调消息实体 2019-11-15 14:46:50 +08:00
larry.liu
c4a361bbf6 完善微信智能硬件接口 2019-11-15 14:46:16 +08:00
larry.liu
f4e58b0712 删除多余代码 2019-11-15 08:05:53 +08:00
larry.liu
5b307df969 增加微信设备管理
1.设备授权方法
2.获取设备二维码方法
2019-11-14 19:30:33 +08:00
silenceper
b16d231a29 Merge pull request #191 from silenceper/revert-184-master
Revert "fix: 序列化 xml 时添加 cdata 标签"
2019-11-12 15:44:13 +08:00
silenceper
c30319c74c Revert "fix: 序列化 xml 时添加 cdata 标签" 2019-11-12 15:05:07 +08:00
silenceper
76c1832798 Merge pull request #184 from chencanxin/master
fix: 序列化 xml 时添加 cdata 标签
2019-11-08 13:42:06 +08:00
陈灿鑫
f6d07aa714 fix: 修复 CDATA.MarshalXML 注释 2019-11-08 11:17:11 +08:00
陈灿鑫
1475417a64 fix: 添加 CDATA.MarshalXML 注释 2019-11-08 11:05:56 +08:00
陈灿鑫
bf456aa77b fix: 添加 CDATA 注释 2019-11-08 10:24:47 +08:00
ciel yu
934ca61b3b 支付 增加 接收通知回调,通知签名验证 2019-11-07 01:42:38 +08:00
cielu
00ca733814 Merge pull request #1 from silenceper/master
同步原仓库
2019-11-07 01:25:40 +08:00
silenceper
2e796b21d3 Merge pull request #186 from huangzhiran/master
修复readme.md里错误的例子
2019-11-06 18:06:12 +08:00
huangzhiran
412c2f0ea9 修复readme.md里错误的例子 2019-11-06 17:52:17 +08:00
silenceper
8cb724ece0 Delete bug_report.md 2019-11-05 14:50:18 +08:00
silenceper
13facb6df8 Create ISSUE_TEMPLATE.md 2019-11-05 14:50:03 +08:00
silenceper
55615762eb Update issue templates 2019-11-05 14:48:27 +08:00
silenceper
1fc4cc70ec Create FUNDING.yml 2019-11-05 14:45:22 +08:00
陈灿鑫
e09031b58c fix: 序列化 xml 时添加 cdata 标签 2019-11-01 23:21:25 +08:00
silenceper
279ff79406 Merge pull request #181 from 4funs/master
修复上传永久视频素材, close #180
2019-10-29 14:27:34 +08:00
kaiiak
7bde39a634 修复上传永久视频素材 2019-10-28 00:53:56 +08:00
silenceper
1711aeb46d Merge pull request #170 from quxiaolong1504/dev
增加内容审核相关 message 字段
2019-10-21 13:57:54 +08:00
quxiaolong
87470f143d 增加内容审核相关 message 字段 2019-10-15 12:24:42 +08:00
Chuanjian Wang
900a54ee06 增加获取永久素材接口 2019-10-15 10:44:54 +08:00
silenceper
7170f6ef32 Merge pull request #168 from cielu/master
支付 新增 BridgeConfig 方法,可获得 prepay ID,及js支付时所需要的参数
2019-10-15 09:02:47 +08:00
ciel yu
0c9dd16f1f 支付 新增 BridgeConfig 方法,可获得 prepay ID,及js支付时所需要的参数
统一下单 新增非必传参数
2019-10-14 19:35:08 +08:00
ciel yu
f68f9d6f5e 支付 新增 BridgeConfig 方法,可获得 prepay ID,及js支付时所需要的参数
统一下单 新增非必传参数
2019-10-14 19:28:28 +08:00
ciel yu
09dabb232d 支付 新增 BridgeConfig 方法,可获得 prepay ID,及js支付时所需要的参数
统一下单 新增非必传参数
2019-10-14 19:14:02 +08:00
ciel yu
3ea624f832 支付 新增 BridgeConfig 方法,可获得 prepay ID,及js支付时所需要的参数
统一下单 新增非必传参数
2019-10-14 19:08:35 +08:00
ciel yu
546dce2396 支付 新增 BridgeConfig 方法,可获得 prepay ID,及js支付时所需要的参数
统一下单 新增非必传参数
2019-10-14 18:59:42 +08:00
ciel yu
abd7f512ba 支付 新增 BridgeConfig 方法,可获得 prepay ID,及js支付时所需要的参数 2019-10-14 18:33:50 +08:00
silenceper
b8239ef9a9 Merge pull request #167 from cielu/master
小程序数据解密,增加手机号解密,兼容之前的用户信息解密
2019-10-14 09:00:37 +08:00
ciel yu
0dffcde475 小程序数据解密,增加手机号解密,兼容之前的用户信息解密 2019-10-12 17:38:41 +08:00
ciel yu
4e6fd625da 小程序数据解密,增加手机号解密,兼容之前的用户信息解密 2019-10-12 16:46:59 +08:00
ciel yu
453089e83e 小程序数据解密,增加手机号解密,兼容之前的用户信息解密 2019-10-12 16:41:26 +08:00
ciel yu
bb97bddc08 Revert "小程序数据解密,增加手机号解密,兼容之前的用户信息解密"
This reverts commit 54e2c82f
2019-10-12 16:27:55 +08:00
cielu
54e2c82fff 小程序数据解密,增加手机号解密,兼容之前的用户信息解密 2019-10-12 16:03:39 +08:00
silenceper
fab09a0bbe Merge pull request #162 from WhisperRain/master
add module to send customer message
2019-10-02 22:22:29 +08:00
WhisperRain
f8ab592606 add(customer message)Add module to send customer message 2019-10-01 20:11:57 +08:00
silenceper
688bca7436 merge master 2019-07-18 16:12:50 +08:00
silenceper
58d6810432 Merge pull request #106 from aikangs/develop
增加批量获取用户信息的功能
2019-04-23 21:11:56 +08:00
song kang
b0f1f71f37 fix code format 2019-03-31 13:40:23 +08:00
35 changed files with 2414 additions and 191 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: https://silenceper.com/img/wechat-pay.jpeg

2
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

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

3
.gitignore vendored
View File

@@ -24,5 +24,6 @@ _testmain.go
*.prof
.DS_Store
.vscode/
vendor/*/
vendor
.idea/
examples/tcb/*

View File

@@ -1,9 +1,9 @@
language: go
go:
- 1.13.x
- 1.12.x
- 1.11.x
- 1.10.x
services:
- memcached

View File

@@ -3,7 +3,6 @@
[![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简单、易用。
## 快速开始
@@ -97,6 +96,7 @@ Cache主要用来保存全局access_token以及js-sdk中的ticket
- 获取js-sdk配置
- [素材管理](#素材管理)
- [小程序开发](#小程序开发)
- [小程序-云开发](./tcb)
## 消息管理
@@ -283,8 +283,8 @@ type Reply struct {
#### 回复图片消息
```go
//mediaID 可通过素材管理-上上传多媒体文件获得
image :=message.NewVideo("mediaID")
return &message.Reply{message.MsgTypeVideo, image}
image :=message.NewImage("mediaID")
return &message.Reply{message.MsgTypeImage, image}
```
#### 回复视频消息
```go

53
cloudbase/README.md Normal file
View File

@@ -0,0 +1,53 @@
# 小程序-云开发 SDK
[云开发CloudBase](https://www.cloudbase.net/)是基于Serverless架构构建的一站式后端云服务涵盖函数、数据库、存储、CDN等服务免后端运维支持小程序、Web和APP开发。 其中,小程序·云开发是微信和腾讯云联合推出的云端一体化解决方案,基于云开发可以免鉴权调用微信所有开放能力,在微信开发者工具中即可开通使用。
## 使用说明
**引入依赖**
>推荐使用go module 进行管理
```
go get github.com/silenceper/wechat@v1.2.3
```
**初始化配置**
```golang
//使用memcache保存access_token也可选择redis或自定义cache
memCache=cache.NewMemcache("127.0.0.1:11211")
//配置小程序参数
config := &wechat.Config{
AppID: "your app id",
AppSecret: "your app secret",
Cache: memCache,
}
wc := wechat.NewWechat(config)
wcTcb := wc.GetTcb()
```
### 使用API
#### 触发云函数
```golang
res, err := wcTcb.InvokeCloudFunction("test-xxxx", "add", `{"a":1,"b":2}`)
if err != nil {
panic(err)
}
```
更多使用方法参考[pkg.go.dev](https://pkg.go.dev/github.com/silenceper/wechat@v1.2.3/tcb?tab=doc#Tcb)
## Demo
### 使用wechat sdk开发一个留言板
这是一个使用wechat sdk来完成一个留言板的例子使用到了云开发中的云函数数据库存储API
- [起步:项目搭建](./guestbook-demo/start.md)
- [数据库:调用云开发数据库实现文本保存](./guestbook-demo/database.md)
- [云函数:调用云函数实现文本过滤](./guestbook-demo/cloudfunctions.md)
- [云开发存储:实现留言本附件上传](./guestbook-demo/storage.md)
以上文中的所有代码都上传在 [https://github.com/go-demo/guestbook](https://github.com/go-demo/guestbook)

View File

@@ -0,0 +1,100 @@
# 云开发存储:实现留言本附件上传
## API说明
云开发中云函数[文档说明](https://developers.weixin.qq.com/minigame/dev/wxcloud/reference-http-api/functions/)可以先阅读原始http api需要的参数以及说明
**基本流程:**
1. 创建云函数
1. 通过微信开发者工具编写云函数
1. 利用SDK实现云函数的调用
云函数调用主要使用到了sdk中 `InvokeCloudFunction` 方法的使用:
```go
func (tcb *Tcb) InvokeCloudFunction(env, name, args string) (*InvokeCloudFunctionRes, error)
```
**参数说明:**<br />1、第一个参数为云开发的环境<br />2、第二个参数为云函数名称<br />3、第三个参数为需要传入的参数这里传入一个json方便在云函数中接收并处理函数的返回值也是json<br />**<br />**返回结果:**
```go
type InvokeCloudFunctionRes struct {
util.CommonError
RespData string `json:"resp_data"` //云函数返回的buffer
}
```
> util.CommonError 包含了errcode和errmsg字段因为微信http api中的返回结果都会包含这两个字段所以作为了一个公共的struct
这里演示如何通过云函数实现对文本内容的过滤,比如对关键字的过滤。
<a name="s4cYj"></a>
## 创建一个云函数
打开微信开发者工具在cloudfunctions中创建一个filterText云函数<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/748713/1580023609925-93d7ece7-636f-46c8-83b8-be12a41c5f51.png#align=left&display=inline&height=236&name=image.png&originHeight=472&originWidth=746&size=75078&status=done&style=none&width=373)
其中index.js文本内容实现了对关键字的替换内容如下
```javascript
// 云函数入口文件
//敏感词
var keywords = ["色情"]
// 云函数入口函数
exports.main = async(event, context) => {
let {
text
} = event
keywords.map(word => {
let regExp = new RegExp(word, 'g')
text = text.replace(regExp, "****")
})
return {
text
}
}
```
这里实现了对关键字 `色情` 替换为 `****` 。
<a name="MIN25"></a>
## 调用云函数
在feedbackService中创建FilterText函数实现对云函数的调用传入原始文本内容返回最终过滤之后的内容。
```go
//FilterRes 过滤文件的结果
type FilterRes struct {
Text string `json:"text"`
}
//FilterText 调用云函数过滤文本
func (svc *FeedbackService) FilterText(text string) (string, error) {
res, err := getTcb().InvokeCloudFunction(getConfig().TcbEnv, "filterText", fmt.Sprintf(`{"text":"%s"}`, text))
//返回的是json
filterRes := &FilterRes{}
err = json.Unmarshal([]byte(res.RespData), filterRes)
if err != nil {
return "", nil
}
return filterRes.Text, nil
}
```
这里将云函数调用的返回值保存在FilterRes struct中。
最后再 feedbackService中的 `Save` 对Content内容进行替换
```go
//Save 保存内容
func (svc *FeedbackService) Save(feedback *Feedback) error {
.....
//content 调用云函数过滤
var err error
feedback.Content, err = svc.FilterText(feedback.Content)
if err != nil {
return err
}
....
}
```
最终的效果如下,当我们输入了含有关键字的留言内容最终就会被替换为****<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/748713/1580024391966-3cf8aab1-6630-4b5d-b172-150f6b43e53c.png#align=left&display=inline&height=155&name=image.png&originHeight=310&originWidth=1284&size=23217&status=done&style=none&width=642)

View File

@@ -0,0 +1,306 @@
# 数据库:调用云开发数据库实现文本保存
在这一节,我们主要描述如何利用`wechat sdk`将留言的内容保存在云开发数据库中。
<a name="RjeGP"></a>
## API说明
参考微信云开发文档 [数据库篇](https://developers.weixin.qq.com/minigame/dev/wxcloud/reference-http-api/database/#%E6%95%B0%E6%8D%AE%E5%BA%93)可以先阅读其原始的api提供的方法和说明在[SDK DOC](https://pkg.go.dev/github.com/silenceper/wechat/tcb?tab=doc#Tcb.DatabaseAdd)中都可以找到对应的方法以及参数。
主要利用到[SDK DOC](https://pkg.go.dev/github.com/silenceper/wechat/tcb?tab=doc#Tcb.DatabaseAdd)中的如下方法其他方法可在文档中找到sdk文档中以 `Database` 开头的方法即为数据库相关的方法调用。
```go
func (tcb *Tcb) DatabaseAdd(env, query string) (*DatabaseAddRes, error) //数据库内容保存
func (tcb *Tcb) DatabaseCount(env, query string) (*DatabaseCountRes, error)//数据库计数
func (tcb *Tcb) DatabaseQuery(env, query string) (*DatabaseQueryRes, error)//数据库内容查询
```
返回结果对应字段说明: [`DatabaseAddRes`](https://pkg.go.dev/github.com/silenceper/wechat/tcb?tab=doc#DatabaseAddRes)  [`DatabaseCountRes`](https://pkg.go.dev/github.com/silenceper/wechat/tcb?tab=doc#DatabaseCountRes) , [`DatabaseQueryRes`](https://pkg.go.dev/github.com/silenceper/wechat/tcb?tab=doc#DatabaseQueryRes) 
<a name="VKuDQ"></a>
## 包引入
本例中引入的WeChat sdk版本为v1.2.3版本,通过如下方法引入
```bash
go get github.com/silenceper/wechat@v1.2.3
```
可以在go.mod文件中看到引入的包以及对应的版本
```go
module github.com/go-demo/guestbook
go 1.13
require (
github.com/gin-gonic/gin v1.5.0
github.com/silenceper/wechat v1.2.3 // indirect
)
```
<a name="m7jnm"></a>
## 保存至云开发数据库
<a name="a095b5de"></a>
### WeChat SDK配置
为了方便在其他方法中调用<br />创建config.go用于解析云开发对应的配置参数appkeyapp_secret等
```go
package main
import (
"io/ioutil"
"github.com/silenceper/wechat"
"github.com/silenceper/wechat/cache"
"github.com/silenceper/wechat/tcb"
"gopkg.in/yaml.v2"
)
//Config 配置信息
type Config struct {
TcbEnv string `yaml:"tcb_env"`
AppID string `yaml:"app_id"`
AppSecret string `yaml:"app_secret"`
}
var cfg *Config
var _ = getConfig()
//通过getConfig方法获取配置参数
func getConfig() *Config {
if cfg != nil {
return cfg
}
data, err := ioutil.ReadFile("./config.yaml")
if err != nil {
panic(err)
}
cfg = &Config{}
err = yaml.Unmarshal(data, cfg)
if err != nil {
panic(err)
}
return cfg
}
var wechatTcb *tcb.Tcb
var _ = getTcb()
//通过getTcb获取wechat sdk的配置参数
func getTcb() *tcb.Tcb {
if wechatTcb != nil {
return wechatTcb
}
memCache := cache.NewMemory()
//配置小程序参数
config := &wechat.Config{
AppID: getConfig().AppID,
AppSecret: getConfig().AppSecret,
Cache: memCache,
}
wc := wechat.NewWechat(config)
wechatTcb = wc.GetTcb()
return wechatTcb
}
```
其中config.yaml写入三个配置参数
```yaml
tcb_env: test-6ku2s //云开发环境
app_id: xxxxxx //云开发appid
app_secret: xxxxxxxxx //云开发对应的app secret
```
<a name="VbDg9"></a>
### 调用API
为了方便在其他方法中方便调用sdk中的方法这里新建一个 `feedbackService` struct创建对应的save方法用于保存留言, `feedback.go` :
```go
package main
import (
"fmt"
"time"
)
//FeedbackService service
type FeedbackService struct {
}
//NewFeedbackService new
func NewFeedbackService() *FeedbackService {
return &FeedbackService{}
}
//Feedback 留言记录
type Feedback struct {
Username string `form:"username",json:"username"`
Content string `form:"content",json:"content"`
FilePath string `json:"filePath"`//文件路径
FileID string `json:"fileId"` //存放文件
CreateTime string `json:"createTime"`
}
func (svc *FeedbackService) Save(feedback *Feedback) error {
if feedback.Username == "" || feedback.Content == "" {
return fmt.Errorf("用户名或留言内容不能为空")
}
query := `db.collection(\"%s\").add({
data: [{
username: \"%s\",
content: \"%s\",
filePath: \"%s\",
fileId: \"%s\",
createTime: \"%s\",
}]
})`
feedback.CreateTime = time.Now().Format("2006-01-02 15:04:05")
query = fmt.Sprintf(query, "guestbook", feedback.Username, feedback.Content, feedback.FilePath, feedback.FileID, feedback.CreateTime)
_, err := getTcb().DatabaseAdd(getConfig().TcbEnv, query)
if err != nil {
return err
}
return nil
}
```
其中对于数据库中的query语句可以参考[https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/database/add.html](https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/database/add.html)
这里调用 `DatabaseAdd` 方法实现内容的保存。
<a name="8db0f827"></a>
## 接收表单提交内容
将表单提交的内容提交到 `/feedback` 路由中,并创建 `feedback`方法接收表单提交的参数,
```go
func main() {
r := gin.Default()
//包含html模板
r.LoadHTMLGlob("./template/*")
//渲染留言页面
r.GET("/",index)
//提交留言
r.POST("/feedback", feedback)
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
```
其中feedback方法如下
```go
//接收提交的内容
func feedback(c *gin.Context) {
//1、接收提交参数
feedback := &Feedback{}
err := c.Bind(feedback)
if err != nil {
showError(c, err)
return
}
feedbackService := NewFeedbackService()
//保存内容
err = feedbackService.Save(feedback)
if err != nil {
showError(c, err)
return
}
c.Redirect(http.StatusMovedPermanently, "/")
}
```
这里通过c.Bind方法将form表单中的内容绑定到 `Feedback` stuct中再通过调用feedbackService中的Save方法对文本内容进行保存。
<a name="DlBj4"></a>
## 展示留言内容
留言内容的展示主要分为两步,一先从数据库展示出来,二是将留言内容展示在页面:<br />查询的sql语句为
```go
db.collection("guestbook").orderBy('createTime','desc').skip(0).limit(10).get()
```
`feedback.go` 中新增List方法其中参数传入skip和limit参数用于分页
```go
//List 文本列表
func (svc *FeedbackService) List(skip, limit int) ([]*Feedback, error) {
query := fmt.Sprintf("db.collection(\"guestbook\").orderBy('createTime','desc').skip(%d).limit(%d).get()", skip, limit)
data, err := getTcb().DatabaseQuery(getConfig().TcbEnv, query)
if err != nil {
return nil, err
}
feedbacks := make([]*Feedback, 0, len(data.Data))
for _, v := range data.Data {
feedbackItem := &Feedback{}
err := json.Unmarshal([]byte(v), feedbackItem)
if err != nil {
return nil, err
}
feedbacks = append(feedbacks, feedbackItem)
}
//fmt.Println(data.Pager)
return feedbacks, nil
}
```
这里主要调用 `DatabaseQuery` 方法对db进行查询。
在main.go中的index方法在从数据中获取的数据取出并渲染在index.html中
```go
//首页
func index(c *gin.Context) {
page := c.Query("page")
//获取记录数量
feedbackService := NewFeedbackService()
count, err := feedbackService.Count()
if err != nil {
showError(c, err)
return
}
limit := 10
totalPage := math.Ceil(float64(count) / float64(limit))
totalPageInt := int(totalPage)
pageInt, _ := strconv.Atoi(page)
if pageInt > totalPageInt {
pageInt = totalPageInt
}
if pageInt < 1 {
pageInt = 1
}
//展示留言列表
skip := (pageInt - 1) * limit
list, err := feedbackService.List(skip, limit)
if err != nil {
showError(c, err)
return
}
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "留言板",
"list": list,
"prevPage": pageInt - 1,
"nextPage": pageInt + 1,
"page": pageInt,
"totalPage": totalPageInt,
})
}
```
其中index.html通过go template语法对内容进行渲染
```html
<div class="list-group list-group-flush">
{{range .list}}
<div class="list-group-item">
<div><span><b>{{.Username}} 在 {{.CreateTime}} 说:</b></span></div>
<div><p>{{.Content}}</p></div>
</div>
{{end}}
</div>
<div>
<span>第{{.page}}页</span>
{{if gt .page 1}}<a href="/?page={{.prevPage}}">上一页</a>{{end}}
{{if lt .page .totalPage}} <a href="/?page={{.nextPage}}">下一页</a>{{end}}
</div>
```
这样就实现了对文本内容的保存

View File

@@ -0,0 +1,185 @@
# 起步:项目搭建
<a name="8MeIi"></a>
## 目标
通过完成一个留言板应用来熟悉云开发中go sdk中的使用主要分为以下三个内容
1. 如何利用云开发中的[数据库](https://developers.weixin.qq.com/minigame/dev/wxcloud/reference-http-api/database/#%E6%95%B0%E6%8D%AE%E5%BA%93)进行留言内容的保存
1. 使用[云函数](https://developers.weixin.qq.com/minigame/dev/wxcloud/reference-http-api/functions/)进行文本内容的过滤
1. 使用[云开发存储](https://developers.weixin.qq.com/minigame/dev/wxcloud/reference-http-api/storage/)能力进行附件的保存
<a name="Nq75E"></a>
## 环境介绍
<a name="Kxmk0"></a>
### Golang 1.13
项目中使用Golang 1.13版本进行开发并且使用go module 进行依赖管理
<a name="H0cFe"></a>
### 编辑器Goland
代码编辑工具
<a name="PsxLG"></a>
### 热编译工具Gowatch
Go 程序热编译工具,提升开发效率<br />官网地址: [https://github.com/silenceper/gowatch](https://github.com/silenceper/gowatch)<br />**快速安装:**
```basic
go get -u github.com/silenceper/gowatch
```
<a name="tNyRl"></a>
### web开发框架-Gin
一个web开发框架方便快速构建一个web应用<br />官网: [https://github.com/gin-gonic/gin](https://github.com/gin-gonic/gin)
<a name="MzLXD"></a>
### Wechat SDK For Go
使用Golang对微信公众号小程序云开发等API进行封装使得Go项目中可以方便上手<br />官网: [https://github.com/silenceper/wechat/](https://github.com/silenceper/wechat/) <br />文档:[https://pkg.go.dev/github.com/silenceper/wechat/tcb?tab=doc](https://pkg.go.dev/github.com/silenceper/wechat/tcb?tab=doc)
<a name="pNPPj"></a>
### 云开发
集成数据库,存储,云函数等功能的平台<br />使用文档:[https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/](https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/)<br />在开始开发前,请注册一个小程序获取 `app_id` , `app_secret`参数,并开启云开发功能。
<a name="qyAfI"></a>
## 初始化项目
> 本项目中使用go module进行依赖管理
在工作目录创建一个项目`guestbook`,并使用`go mod init github.com/go-demo/guestbook`进行初始化,后面接的是`import path`
```bash
mkdir guestbook
cd guestbook
go mod init github.com/go-demo/guestbook
```
<a name="JvK7M"></a>
### 编写main.go文件
使用goland编辑器中打开这个项目并创建一个`main.go`文件,内容如下:
```go
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}
```
<a name="q2Vtn"></a>
### 编译并运行
我们可以在Goland编辑器中Terminal面板中进入项目目录使用`gowatch`命令对该项目进行热编译看到图片中的log输出表示已经启动成功
> gowatch会监听项目中文件的变化当进行变化后对项目进行build 和run这样我们就可以在一边修改代码一边对项目进行编译及时发现错误是不是效率提升了呢  :>
![image.png](https://cdn.nlark.com/yuque/0/2020/png/748713/1579680949745-4a9d705e-b2d1-4667-a7a7-b9a5200321c8.png#align=left&display=inline&height=777&name=image.png&originHeight=1554&originWidth=2470&size=400945&status=done&style=none&width=1235)<br />初次build会通过go module自动下载依赖请注意开启go module功能
我们通过访问`127.0.0.1:8080/ping`就可以看到页面上输出`{"message":"pong"}`说明服务启动成功。
<a name="yXtGW"></a>
## 渲染留言页面
我们可以先规划我们的UI是怎么样子
包含两部分:
- 留言框:包含留言内容,附件上传,用户名,提交按钮
- 内容展示:展示留言内容,附件以及留言者和留言日期
界面展示如下:<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/748713/1579681903215-81a613c0-0a08-4196-ba6d-36c8942e107c.png#align=left&display=inline&height=331&name=image.png&originHeight=1312&originWidth=2352&size=99758&status=done&style=none&width=593)
<a name="2vKHj"></a>
### 创建模板文件
对应的html代码如下我们保存在项目中的template/index.html文件中
```html
<!doctype html>
<html>
<head>
<title>留言板</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.4.1/dist/css/bootstrap.min.css"
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body class="container-md">
<h3>留言板</h3>
<div>
<form action="/feedback" method="post" enctype="multipart/form-data">
<div class="form-group">
<textarea class="form-control" name="content" id="content" cols="50" rows="5"></textarea>
</div>
<div class="form-group">
<label for="file">附件</label>
<input type="file" class="form-control-file" name="file" id="">
</div>
<div class="form-group">
<label for="username">名字</label>
<input type="text" name="username" class="form-control"></div>
<div class="form-group"><input type="submit" value="提交" class="btn btn-primary"></div>
</form>
<h2>内容</h2>
<div class="list-group list-group-flush">
<div class="list-group-item">
<div><span><b>silenceper 在 2020-01-21 12:33:45 说:</b></span></div>
<div><p>留言板内容</p></div>
</div>
</div>
<div>
<span>第1页</span>
</div>
</div>
</body>
</html>
```
这里引入了bootstrap样式文件不需要自己写太多前端样式了出来的UI也不会太难看。
<a name="MLcML"></a>
### 通过gin渲染模板
我们想要通过访问`127.0.0.1:8080`直接访问到这个留言页面main.go中代码如下
```go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
//包含html模板
r.LoadHTMLGlob("./template/*")
//渲染留言页面,GET请求通过根路径可以直接访问
//当路径匹配成功后进入index访问进行处理
r.GET("/",index)
r.Run() // listen and serve on 0.0.0.0:8080
}
//渲染留言板首页
func index(c *gin.Context) {
//返回200并渲染index.html页面
c.HTML(http.StatusOK,"index.html",gin.H{
"title":"留言板",
})
}
```
其中`r.LoadHTMLGlob("./template/*") `指定了html模板的位置这样在使用进行`c.HTML`进行渲染的时候就知道到哪个位置进行查找了。
**c.HTML说明**
- 第一个参数http状态码
- 第二个参数:需要渲染的模板
- 第三个参数:需要传递的值(`gin.H`其实是一个`map[string]interface{}`的别名)
这里`c.HTML`渲染了`index.html`,并以`200`状态码输出,第三个参数`gin.H`,传入`key:value` ,就可以在`index.html`页面中使用go-template语法进行值的替换语法格式
`{{.title}}`
这里可以查阅gin文档[如何进行html渲染](https://github.com/gin-gonic/gin#html-rendering)
<a name="XXc0s"></a>
## 代码地址
本文中所有代码都上传在 [https://github.com/go-demo/guestbook](https://github.com/go-demo/guestbook)

View File

@@ -0,0 +1,144 @@
# 云开发存储:实现留言本附件上传
## API说明
官方文档:[微信云开发存储文档](https://developers.weixin.qq.com/minigame/dev/wxcloud/reference-http-api/storage/)主要提供了三个API(上传,下载,删除),可以先分别看下参数
在这一节中主要利用到了sdk中的附件上传和下载的方法方法如下
```go
func (tcb *Tcb) UploadFile(env, path string) (*UploadFileRes, error) {
func (tcb *Tcb) BatchDownloadFile(env string, fileList []*DownloadFile) (*BatchDownloadFileRes, error) {
```
返回对应的返回结果在这里可以查看,[UploadFileRes](https://pkg.go.dev/github.com/silenceper/wechat/tcb?tab=doc#UploadFileRes)[BatchDownloadFileRes](https://pkg.go.dev/github.com/silenceper/wechat/tcb?tab=doc#BatchDownloadFileRes)
<a name="264b81c5"></a>
## 接收附件上传内容
1、保存index.html中form表单中使用的是post方法并且enctype为 `multipart/form-data` :
```html
<form action="/feedback" method="post" enctype="multipart/form-data">
```
2、在main.go中的feedback方法中获取上传的文件
```go
//2、文件上传
fileHeader, err := c.FormFile("file")
if err != nil && err != http.ErrMissingFile {
showError(c, err)
return
}
```
<a name="186268da"></a>
## 将附件保存在云开发中
在feedbackService中创建 `UploadFile` 方法调用sdk中文件上传方法实现对文件的上传<br />UploadFile 第一个参数为云开发环境,第二个参数为需要附件需要保存的路径
> 注意这里path应该为相对路径。不能为绝对路径比如 /guestbook 应该为 guestbook 否则会报错。
```go
//UploadFile 上传文件
func (svc *FeedbackService) UploadFile(path string, file io.Reader) (string, error) {
//获取文件上传链接
uploadRes, err := getTcb().UploadFile(getConfig().TcbEnv, path)
if err != nil {
return "", err
}
data := make(map[string]io.Reader)
data["key"] = strings.NewReader(path)
data["Signature"] = strings.NewReader(uploadRes.Authorization)
data["x-cos-security-token"] = strings.NewReader(uploadRes.Token)
data["x-cos-meta-fileid"] = strings.NewReader(uploadRes.CosFileID)
data["file"] = file
//上传文件
_, err = goutils.PostFormWithFile(&http.Client{}, uploadRes.URL, data)
return uploadRes.FileID, err
}
```
其中 `PostFormWithFile` 方法是对 `mime/multipart` 方法的一个封装用于将附件内容上传到指定的url中`github.com/silenceper/goutils` 包中。
并在main.go中feedback方法调用并将返回的field和Filename保存在db中
```go
//2、文件上传
fileHeader, err := c.FormFile("file")
if err != nil && err != http.ErrMissingFile {
showError(c, err)
return
}
feedbackService := NewFeedbackService()
if fileHeader != nil {
path := fmt.Sprintf("guestbook/%s", fileHeader.Filename)
file, err := fileHeader.Open()
if err != nil {
showError(c, err)
return
}
fileID, err := feedbackService.UploadFile(path, file)
if err != nil {
showError(c, err)
return
}
feedback.FilePath = fileHeader.Filename
feedback.FileID = fileID
}
```
最终附件可以在index方法通过数据库查询在html中展示出来
<a name="786a132e"></a>
## 附件下载
这里创建一个单独的路由用于对附件进行下载通过在get参数中传入fileId定位到附件并打开附件路径
```go
r.GET("/file", downloadFile)
```
```go
//附件下载
func downloadFile(c *gin.Context) {
fileID := c.Query("id")
if fileID == "" {
showError(c, fmt.Errorf("fileID为空"))
return
}
downLoadURL, err := NewFeedbackService().DownloadFile(fileID)
if err != nil {
showError(c, err)
return
}
c.Redirect(http.StatusMovedPermanently, downLoadURL)
}
```
在feedbackService中创建DownloadFile方法返回真实下载路径并跳转:
```go
//DownloadFile 获取下载链接
func (svc *FeedbackService) DownloadFile(id string) (string, error) {
files := []*tcb.DownloadFile{&tcb.DownloadFile{
FileID: id,
MaxAge: 100,
}}
res, err := getTcb().BatchDownloadFile(getConfig().TcbEnv, files)
if err != nil {
return "", err
}
if len(res.FileList) >= 1 {
return res.FileList[0].DownloadURL, nil
}
return "", nil
}
```
这里BatchDownloadFile方法第一个参数是云开发环境第二个参数接收的 `tcb.DownloadFile` 数组指定fileID和下载链接的有效期。
最终完成的效果如下:<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/748713/1580025839031-8efea9fe-3ce0-4a4b-a8cd-0ad2120c1a9e.png#align=left&display=inline&height=666&name=image.png&originHeight=1332&originWidth=2256&size=112605&status=done&style=none&width=1128)
本文中所有代码都上传在 [https://github.com/go-demo/guestbook](https://github.com/go-demo/guestbook)

116
device/authorize.go Normal file
View File

@@ -0,0 +1,116 @@
package device
import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/util"
)
const (
// DeviceAdd 添加设备标识
DeviceAdd = iota
// DeviceUpgrade 更新设备标识
DeviceUpgrade
)
type reqDeviceAuthorize struct {
// 设备id的个数
DeviceNum string `json:"device_num"`
// 设备id的列表json的array格式其size必须等于device_num
DeviceList []ReqDevice `json:"device_list"`
// 请求操作的类型限定取值为0设备授权缺省值为0 1设备更新更新已授权设备的各属性值
OpType string `json:"op_type,omitempty"`
// 设备的产品编号(由微信硬件平台分配)。可在公众号设备功能管理页面查询。
//当 op_type 为0product_id 为1不要填写 product_id 字段(会引起不必要错误);
//当 op_typy 为0product_id 不为1必须填写 product_id 字段;
//当 op_type 为 1 时,不要填写 product_id 字段。
ProductID string `json:"product_id,omitempty"`
}
//ReqDevice 设备授权实体
type ReqDevice struct {
// 设备的 device id
ID string `json:"id"`
// 设备的mac地址 格式采用16进制串的方式长度为12字节
// 不需要0X前缀 1234567890AB
Mac string `json:"mac"`
// 支持以下四种连接协议:
// android classic bluetooth 1
// ios classic bluetooth 2
// ble 3
// wifi -- 4
// 一个设备可以支持多种连接类型,用符号"|"做分割,客户端优先选择靠前的连接方式(优先级按|关系的排序依次降低),举例:
// 1表示设备仅支持andiod classic bluetooth 1|2表示设备支持andiod 和ios 两种classic bluetooth但是客户端优先选择andriod classic bluetooth 协议如果andriod classic bluetooth协议连接失败再选择ios classic bluetooth协议进行连接
// 安卓平台不同时支持BLE和classic类型
ConnectProtocol string `json:"connect_protocol"`
//auth及通信的加密key第三方需要将key烧制在设备上128bit格式采用16进制串的方式长度为32字节不需要0X前缀 1234567890ABCDEF1234567890ABCDEF
AuthKey string `json:"auth_key"`
// 断开策略,目前支持: 1退出公众号页面时即断开连接 2退出公众号之后保持连接不断开
CloseStrategy string `json:"close_strategy"`
//连接策略32位整型按bit位置位目前仅第1bit和第3bit位有效bit置0为无效1为有效第2bit已被废弃且bit位可以按或置位如1|4=5各bit置位含义说明如下
//1第1bit置位在公众号对话页面不停的尝试连接设备
//4第3bit置位处于非公众号页面如主界面等微信自动连接。当用户切换微信到前台时可能尝试去连接设备连上后一定时间会断开
ConnStrategy string `json:"conn_strategy"`
// auth version设备和微信进行auth时会根据该版本号来确认auth buf和auth key的格式各version对应的auth buf及key的具体格式可以参看“客户端蓝牙外设协议”该字段目前支持取值
// 0不加密的version
// 1version 1
AuthVer string `json:"auth_ver"`
// 表示mac地址在厂商广播manufature data里含有mac地址的偏移取值如下
// -1在尾部、
// -2表示不包含mac地址 其他:非法偏移
ManuMacPos string `json:"manu_mac_pos"`
// 表示mac地址在厂商serial number里含有mac地址的偏移取值如下
// -1表示在尾部
// -2表示不包含mac地址 其他:非法偏移
SerMacPost string `json:"ser_mac_post"`
// 精简协议类型取值如下计步设备精简协议1 若该字段填1connect_protocol 必须包括3。非精简协议设备切勿填写该字段
BleSimpleProtocol string `json:"ble_simple_protocol,omitempty"`
}
//ResBaseInfo 授权回调实体
type ResBaseInfo struct {
BaseInfo struct {
DeviceType string `json:"device_type"`
DeviceID string `json:"device_id"`
} `json:"base_info"`
}
// 授权回调根信息
type resDeviceAuthorize struct {
util.CommonError
Resp []ResBaseInfo `json:"resp"`
}
// DeviceAuthorize 设备授权
func (d *Device) DeviceAuthorize(devices []ReqDevice, opType int, product string) (res []ResBaseInfo, err error) {
var accessToken string
accessToken, err = d.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s", uriAuthorize, accessToken)
req := reqDeviceAuthorize{
DeviceNum: fmt.Sprintf("%d", len(devices)),
DeviceList: devices,
OpType: fmt.Sprintf("%d", opType),
ProductID: product,
}
var response []byte
response, err = util.PostJSON(uri, req)
if err != nil {
return nil, err
}
var result resDeviceAuthorize
err = json.Unmarshal(response, &result)
if err != nil {
return
}
if result.ErrCode != 0 {
err = fmt.Errorf("DeviceAuthorize Error , errcode=%d , errmsg=%s", result.ErrCode, result.ErrMsg)
return
}
res = result.Resp
return
}

106
device/bind.go Normal file
View File

@@ -0,0 +1,106 @@
package device
import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/util"
)
// ReqBind 设备绑定解绑共通实体
type ReqBind struct {
Ticket string `json:"ticket,omitempty"`
DeviceID string `json:"device_id"`
OpenID string `json:"openid"`
}
type resBind struct {
BaseResp util.CommonError `json:"base_resp"`
}
// Bind 设备绑定
func (d *Device) Bind(req ReqBind) (err error) {
var accessToken string
if accessToken, err = d.GetAccessToken(); err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s", uriBind, accessToken)
var response []byte
if response, err = util.PostJSON(uri, req); err != nil {
return
}
var result resBind
if err = json.Unmarshal(response, &result); err != nil {
return
}
if result.BaseResp.ErrCode != 0 {
err = fmt.Errorf("DeviceBind Error , errcode=%d , errmsg=%s", result.BaseResp.ErrCode, result.BaseResp.ErrMsg)
return
}
return
}
// Unbind 设备解绑
func (d *Device) Unbind(req ReqBind) (err error) {
var accessToken string
if accessToken, err = d.GetAccessToken(); err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s", uriUnbind, accessToken)
var response []byte
if response, err = util.PostJSON(uri, req); err != nil {
return
}
var result resBind
if err = json.Unmarshal(response, &result); err != nil {
return
}
if result.BaseResp.ErrCode != 0 {
err = fmt.Errorf("DeviceBind Error , errcode=%d , errmsg=%s", result.BaseResp.ErrCode, result.BaseResp.ErrMsg)
return
}
return
}
// CompelBind 强制绑定用户和设备
func (d *Device) CompelBind(req ReqBind) (err error) {
var accessToken string
if accessToken, err = d.GetAccessToken(); err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s", uriCompelBind, accessToken)
var response []byte
if response, err = util.PostJSON(uri, req); err != nil {
return
}
var result resBind
if err = json.Unmarshal(response, &result); err != nil {
return
}
if result.BaseResp.ErrCode != 0 {
err = fmt.Errorf("DeviceBind Error , errcode=%d , errmsg=%s", result.BaseResp.ErrCode, result.BaseResp.ErrMsg)
return
}
return
}
// CompelUnbind 强制解绑用户和设备
func (d *Device) CompelUnbind(req ReqBind) (err error) {
var accessToken string
if accessToken, err = d.GetAccessToken(); err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s", uriCompelUnbind, accessToken)
var response []byte
if response, err = util.PostJSON(uri, req); err != nil {
return
}
var result resBind
if err = json.Unmarshal(response, &result); err != nil {
return
}
if result.BaseResp.ErrCode != 0 {
err = fmt.Errorf("DeviceBind Error , errcode=%d , errmsg=%s", result.BaseResp.ErrCode, result.BaseResp.ErrMsg)
return
}
return
}

60
device/device.go Normal file
View File

@@ -0,0 +1,60 @@
package device
import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/context"
"github.com/silenceper/wechat/util"
)
const (
uriAuthorize = "https://api.weixin.qq.com/device/authorize_device"
uriQRCode = "https://api.weixin.qq.com/device/create_qrcode"
uriVerifyQRCode = "https://api.weixin.qq.com/device/verify_qrcode"
uriBind = "https://api.weixin.qq.com/device/bind"
uriUnbind = "https://api.weixin.qq.com/device/unbind"
uriCompelBind = "https://api.weixin.qq.com/device/compel_bind"
uriCompelUnbind = "https://api.weixin.qq.com/device/compel_unbind"
uriState = "https://api.weixin.qq.com/device/get_stat"
)
//Device struct
type Device struct {
*context.Context
}
//NewDevice 实例
func NewDevice(context *context.Context) *Device {
device := new(Device)
device.Context = context
return device
}
// ResDeviceState 设备状态响应实体
type ResDeviceState struct {
util.CommonError
Status int `json:"status"`
StatusInfo string `json:"status_info"`
}
// State 设备状态查询
func (d *Device) State(device string) (res ResDeviceState, err error) {
var accessToken string
if accessToken, err = d.GetAccessToken(); err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s&device_id=%s", uriState, accessToken, device)
var response []byte
if response, err = util.HTTPGet(uri); err != nil {
return
}
if err = json.Unmarshal(response, &res); err != nil {
return
}
if res.ErrCode != 0 {
err = fmt.Errorf("DeviceState Error , errcode=%d , errmsg=%s", res.ErrCode, res.ErrMsg)
return
}
return
}

9
device/message.go Normal file
View File

@@ -0,0 +1,9 @@
package device
//MsgDevice 设备消息响应
type MsgDevice struct {
DeviceType string
DeviceID string
SessionID string
OpenID string
}

76
device/qrcode.go Normal file
View File

@@ -0,0 +1,76 @@
package device
import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/util"
)
//ResCreateQRCode 获取二维码的返回实体
type ResCreateQRCode struct {
util.CommonError
DeviceNum int `json:"device_num"`
CodeList []struct {
DeviceID string `json:"device_id"`
Ticket string `json:"ticket"`
} `json:"code_list"`
}
// CreateQRCode 获取设备二维码
func (d *Device) CreateQRCode(devices []string) (res ResCreateQRCode, err error) {
var accessToken string
if accessToken, err = d.GetAccessToken(); err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s", uriQRCode, accessToken)
req := map[string]interface{}{
"device_num": len(devices),
"device_id_list": devices,
}
var response []byte
if response, err = util.PostJSON(uri, req); err != nil {
return
}
if err = json.Unmarshal(response, &res); err != nil {
return
}
if res.ErrCode != 0 {
err = fmt.Errorf("DeviceCreateQRCode Error , errcode=%d , errmsg=%s", res.ErrCode, res.ErrMsg)
return
}
return
}
//ResVerifyQRCode 验证授权结果实体
type ResVerifyQRCode struct {
util.CommonError
DeviceType string `json:"device_type"`
DeviceID string `json:"device_id"`
Mac string `json:"mac"`
}
// VerifyQRCode 验证设备二维码
func (d *Device) VerifyQRCode(ticket string) (res ResVerifyQRCode, err error) {
var accessToken string
if accessToken, err = d.GetAccessToken(); err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s", uriVerifyQRCode, accessToken)
req := map[string]interface{}{
"ticket": ticket,
}
fmt.Println(req)
var response []byte
if response, err = util.PostJSON(uri, req); err != nil {
return
}
if err = json.Unmarshal(response, &res); err != nil {
return
}
if res.ErrCode != 0 {
err = fmt.Errorf("DeviceCreateQRCode Error , errcode=%d , errmsg=%s", res.ErrCode, res.ErrMsg)
return
}
return
}

13
go.mod Normal file
View File

@@ -0,0 +1,13 @@
module github.com/silenceper/wechat
go 1.13
require (
github.com/astaxie/beego v1.7.1
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
github.com/fatih/structs v1.1.0
github.com/gin-gonic/gin v1.6.3
github.com/gomodule/redigo v1.8.1 // indirect
github.com/spf13/cast v1.3.1
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
)

75
go.sum Normal file
View File

@@ -0,0 +1,75 @@
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-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-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/gomodule/redigo v0.0.0-20200429221454-e14091dffc1b h1:58st9mvZD0VpNKVxG3QQqcEXatwcZQimIhRq/hQHRBU=
github.com/gomodule/redigo v0.0.0-20200429221454-e14091dffc1b/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/gomodule/redigo v1.8.1 h1:Abmo0bI7Xf0IhdIPc7HZQzZcShdnmxeoVuDDtIQp8N8=
github.com/gomodule/redigo v1.8.1/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
github.com/gomodule/redigo/redis v0.0.0-20200429221454-e14091dffc1b h1:XAV1CHPRk+MUwbuzYHrCgq7mVq0Z0kcKNilDF4A8yys=
github.com/gomodule/redigo/redis v0.0.0-20200429221454-e14091dffc1b/go.mod h1:0zioC1ElIFjAFjnYHA9Y8oeU9SHff86XhZgs2tpx9SU=
github.com/gomodule/redigo/redis v0.0.0-do-not-use/go.mod h1:DAjdM68BqtBsasI8YAQVRAOTlhDyI5EjYykQ4DBKY0I=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-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/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -13,6 +13,7 @@ const (
addNewsURL = "https://api.weixin.qq.com/cgi-bin/material/add_news"
addMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/add_material"
delMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/del_material"
getMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/get_material"
)
//Material 素材管理
@@ -31,11 +32,39 @@ func NewMaterial(context *context.Context) *Material {
type Article struct {
Title string `json:"title"`
ThumbMediaID string `json:"thumb_media_id"`
ThumbURL string `json:"thumb_url"`
Author string `json:"author"`
Digest string `json:"digest"`
ShowCoverPic int `json:"show_cover_pic"`
Content string `json:"content"`
ContentSourceURL string `json:"content_source_url"`
URL string `json:"url"`
DownURL string `json:"down_url"`
}
// GetNews 获取/下载永久素材
func (material *Material) GetNews(id string) ([]*Article, error) {
accessToken, err := material.GetAccessToken()
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s?access_token=%s", getMaterialURL, accessToken)
var req struct {
MediaID string `json:"media_id"`
}
req.MediaID = id
responseBytes, err := util.PostJSON(uri, req)
var res struct {
NewsItem []*Article `json:"news_item"`
}
err = json.Unmarshal(responseBytes, &res)
if err != nil {
return nil, err
}
return res.NewsItem, nil
}
//reqArticles 永久性图文素材请求信息
@@ -138,11 +167,11 @@ func (material *Material) AddVideo(filename, title, introduction string) (mediaI
fields := []util.MultipartFormField{
{
IsFile: true,
Fieldname: "video",
Fieldname: "media",
Filename: filename,
},
{
IsFile: true,
IsFile: false,
Fieldname: "description",
Value: fieldValue,
},

160
message/customer_message.go Normal file
View File

@@ -0,0 +1,160 @@
package message
import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/context"
"github.com/silenceper/wechat/util"
)
const (
customerSendMessage = "https://api.weixin.qq.com/cgi-bin/message/custom/send"
)
//Manager 消息管理者,可以发送消息
type Manager struct {
*context.Context
}
//NewMessageManager 实例化消息管理者
func NewMessageManager(context *context.Context) *Manager {
return &Manager{
context,
}
}
//CustomerMessage 客服消息
type CustomerMessage struct {
ToUser string `json:"touser"` //接受者OpenID
Msgtype MsgType `json:"msgtype"` //客服消息类型
Text *MediaText `json:"text,omitempty"` //可选
Image *MediaResource `json:"image,omitempty"` //可选
Voice *MediaResource `json:"voice,omitempty"` //可选
Video *MediaVideo `json:"video,omitempty"` //可选
Music *MediaMusic `json:"music,omitempty"` //可选
News *MediaNews `json:"news,omitempty"` //可选
Mpnews *MediaResource `json:"mpnews,omitempty"` //可选
Wxcard *MediaWxcard `json:"wxcard,omitempty"` //可选
Msgmenu *MediaMsgmenu `json:"msgmenu,omitempty"` //可选
Miniprogrampage *MediaMiniprogrampage `json:"miniprogrampage,omitempty"` //可选
}
//NewCustomerTextMessage 文本消息结构体构造方法
func NewCustomerTextMessage(toUser, text string) *CustomerMessage {
return &CustomerMessage{
ToUser: toUser,
Msgtype: MsgTypeText,
Text: &MediaText{
text,
},
}
}
//NewCustomerImgMessage 图片消息的构造方法
func NewCustomerImgMessage(toUser, mediaID string) *CustomerMessage {
return &CustomerMessage{
ToUser: toUser,
Msgtype: MsgTypeImage,
Image: &MediaResource{
mediaID,
},
}
}
//NewCustomerVoiceMessage 语音消息的构造方法
func NewCustomerVoiceMessage(toUser, mediaID string) *CustomerMessage {
return &CustomerMessage{
ToUser: toUser,
Msgtype: MsgTypeVoice,
Voice: &MediaResource{
mediaID,
},
}
}
//MediaText 文本消息的文字
type MediaText struct {
Content string `json:"content"`
}
//MediaResource 消息使用的永久素材id
type MediaResource struct {
MediaID string `json:"media_id"`
}
//MediaVideo 视频消息包含的内容
type MediaVideo struct {
MediaID string `json:"media_id"`
ThumbMediaID string `json:"thumb_media_id"`
Title string `json:"title"`
Description string `json:"description"`
}
//MediaMusic 音乐消息包括的内容
type MediaMusic struct {
Title string `json:"title"`
Description string `json:"description"`
Musicurl string `json:"musicurl"`
Hqmusicurl string `json:"hqmusicurl"`
ThumbMediaID string `json:"thumb_media_id"`
}
//MediaNews 图文消息的内容
type MediaNews struct {
Articles []MediaArticles `json:"articles"`
}
//MediaArticles 图文消息的内容的文章列表中的单独一条
type MediaArticles struct {
Title string `json:"title"`
Description string `json:"description"`
URL string `json:"url"`
Picurl string `json:"picurl"`
}
//MediaMsgmenu 菜单消息的内容
type MediaMsgmenu struct {
HeadContent string `json:"head_content"`
List []MsgmenuItem `json:"list"`
TailContent string `json:"tail_content"`
}
//MsgmenuItem 菜单消息的菜单按钮
type MsgmenuItem struct {
ID string `json:"id"`
Content string `json:"content"`
}
//MediaWxcard 卡券的id
type MediaWxcard struct {
CardID string `json:"card_id"`
}
//MediaMiniprogrampage 小程序消息
type MediaMiniprogrampage struct {
Title string `json:"title"`
Appid string `json:"appid"`
Pagepath string `json:"pagepath"`
ThumbMediaID string `json:"thumb_media_id"`
}
//Send 发送客服消息
func (manager *Manager) Send(msg *CustomerMessage) error {
accessToken, err := manager.Context.GetAccessToken()
if err != nil {
return err
}
uri := fmt.Sprintf("%s?access_token=%s", customerSendMessage, accessToken)
response, err := util.PostJSON(uri, msg)
var result util.CommonError
err = json.Unmarshal(response, &result)
if err != nil {
return err
}
if result.ErrCode != 0 {
err = fmt.Errorf("customer msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
return err
}
return nil
}

View File

@@ -1,6 +1,10 @@
package message
import "encoding/xml"
import (
"encoding/xml"
"github.com/silenceper/wechat/device"
)
// MsgType 基本消息类型
type MsgType string
@@ -63,6 +67,8 @@ const (
EventLocationSelect = "location_select"
//EventTemplateSendJobFinish 发送模板消息推送通知
EventTemplateSendJobFinish = "TEMPLATESENDJOBFINISH"
//EventWxaMediaCheck 异步校验图片/音频是否含有违法违规内容推送事件
EventWxaMediaCheck = "wxa_media_check"
)
const (
@@ -144,6 +150,15 @@ type MixMessage struct {
OuterStr string `xml:"OuterStr"`
IsRestoreMemberCard int32 `xml:"IsRestoreMemberCard"`
UnionID string `xml:"UnionId"`
// 内容审核相关
IsRisky bool `xml:"isrisky"`
ExtraInfoJSON string `xml:"extra_info_json"`
TraceID string `xml:"trace_id"`
StatusCode int `xml:"status_code"`
//设备相关
device.MsgDevice
}
//EventPic 发图事件推送
@@ -167,22 +182,32 @@ type ResponseEncryptedXMLMsg struct {
Nonce string `xml:"Nonce" json:"Nonce"`
}
// CDATA 使用该类型,在序列化为 xml 文本时文本会被解析器忽略
type CDATA string
// MarshalXML 实现自己的序列化方法
func (c CDATA) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(struct {
string `xml:",cdata"`
}{string(c)}, start)
}
// CommonToken 消息中通用的结构
type CommonToken struct {
XMLName xml.Name `xml:"xml"`
ToUserName string `xml:"ToUserName"`
FromUserName string `xml:"FromUserName"`
ToUserName CDATA `xml:"ToUserName"`
FromUserName CDATA `xml:"FromUserName"`
CreateTime int64 `xml:"CreateTime"`
MsgType MsgType `xml:"MsgType"`
}
//SetToUserName set ToUserName
func (msg *CommonToken) SetToUserName(toUserName string) {
func (msg *CommonToken) SetToUserName(toUserName CDATA) {
msg.ToUserName = toUserName
}
//SetFromUserName set FromUserName
func (msg *CommonToken) SetFromUserName(fromUserName string) {
func (msg *CommonToken) SetFromUserName(fromUserName CDATA) {
msg.FromUserName = fromUserName
}

View File

@@ -1,4 +1,4 @@
package template
package message
import (
"encoding/json"

View File

@@ -3,12 +3,12 @@ package message
//Text 文本消息
type Text struct {
CommonToken
Content string `xml:"Content"`
Content CDATA `xml:"Content"`
}
//NewText 初始化文本消息
func NewText(content string) *Text {
text := new(Text)
text.Content = content
text.Content = CDATA(content)
return text
}

View File

@@ -36,6 +36,17 @@ type UserInfo struct {
} `json:"watermark"`
}
// PhoneInfo 用户手机号
type PhoneInfo struct {
PhoneNumber string `json:"phoneNumber"`
PurePhoneNumber string `json:"purePhoneNumber"`
CountryCode string `json:"countryCode"`
Watermark struct {
Timestamp int64 `json:"timestamp"`
AppID string `json:"appid"`
} `json:"watermark"`
}
// pkcs7Unpad returns slice of the original data without padding
func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) {
if blockSize <= 0 {
@@ -57,8 +68,8 @@ func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) {
return data[:len(data)-n], nil
}
// Decrypt 解密数据
func (wxa *MiniProgram) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo, error) {
// getCipherText returns slice of the cipher text
func getCipherText(sessionKey, encryptedData, iv string) ([]byte, error) {
aesKey, err := base64.StdEncoding.DecodeString(sessionKey)
if err != nil {
return nil, err
@@ -81,6 +92,15 @@ func (wxa *MiniProgram) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo
if err != nil {
return nil, err
}
return cipherText, nil
}
// Decrypt 解密数据
func (wxa *MiniProgram) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo, error) {
cipherText, err := getCipherText(sessionKey, encryptedData, iv)
if err != nil {
return nil, err
}
var userInfo UserInfo
err = json.Unmarshal(cipherText, &userInfo)
if err != nil {
@@ -91,3 +111,20 @@ func (wxa *MiniProgram) Decrypt(sessionKey, encryptedData, iv string) (*UserInfo
}
return &userInfo, nil
}
// DecryptPhone 解密数据(手机)
func (wxa *MiniProgram) DecryptPhone(sessionKey, encryptedData, iv string) (*PhoneInfo, error) {
cipherText, err := getCipherText(sessionKey, encryptedData, iv)
if err != nil {
return nil, err
}
var phoneInfo PhoneInfo
err = json.Unmarshal(cipherText, &phoneInfo)
if err != nil {
return nil, err
}
if phoneInfo.Watermark.AppID != wxa.AppID {
return nil, ErrAppIDNotMatch
}
return &phoneInfo, nil
}

100
pay/notify_result.go Normal file
View File

@@ -0,0 +1,100 @@
package pay
import (
"fmt"
"github.com/fatih/structs"
"github.com/silenceper/wechat/util"
"github.com/spf13/cast"
"reflect"
"sort"
"strings"
)
// doc: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7&index=8
// NotifyResult 下单回调
type NotifyResult struct {
ReturnCode *string `xml:"return_code"`
ReturnMsg *string `xml:"return_msg"`
AppID *string `xml:"appid" json:"appid"`
MchID *string `xml:"mch_id"`
DeviceInfo *string `xml:"device_info"`
NonceStr *string `xml:"nonce_str"`
Sign *string `xml:"sign"`
SignType *string `xml:"sign_type"`
ResultCode *string `xml:"result_code"`
ErrCode *string `xml:"err_code"`
ErrCodeDes *string `xml:"err_code_des"`
OpenID *string `xml:"openid"`
IsSubscribe *string `xml:"is_subscribe"`
TradeType *string `xml:"trade_type"`
BankType *string `xml:"bank_type"`
TotalFee *int `xml:"total_fee"`
SettlementTotalFee *int `xml:"settlement_total_fee"`
FeeType *string `xml:"fee_type"`
CashFee *string `xml:"cash_fee"`
CashFeeType *string `xml:"cash_fee_type"`
CouponFee *int `xml:"coupon_fee"`
CouponCount *int `xml:"coupon_count"`
// coupon_type_$n 这里只声明 3 个,如果有更多的可以自己组合
CouponType0 *string `xml:"coupon_type_0"`
CouponType1 *string `xml:"coupon_type_1"`
CouponType2 *string `xml:"coupon_type_2"`
CouponID0 *string `xml:"coupon_id_0"`
CouponID1 *string `xml:"coupon_id_1"`
CouponID2 *string `xml:"coupon_id_2"`
CouponFeed0 *string `xml:"coupon_fee_0"`
CouponFeed1 *string `xml:"coupon_fee_1"`
CouponFeed2 *string `xml:"coupon_fee_2"`
TransactionID *string `xml:"transaction_id"`
OutTradeNo *string `xml:"out_trade_no"`
Attach *string `xml:"attach"`
TimeEnd *string `xml:"time_end"`
}
// NotifyResp 消息通知返回
type NotifyResp struct {
ReturnCode string `xml:"return_code"`
ReturnMsg string `xml:"return_msg"`
}
// VerifySign 验签
func (pcf *Pay) VerifySign(notifyRes NotifyResult) bool {
// STEP1, 转换 struct 为 map并对 map keys 做排序
resMap := structs.Map(notifyRes)
sortedKeys := make([]string, 0, len(resMap))
for k := range resMap {
sortedKeys = append(sortedKeys, k)
}
sort.Strings(sortedKeys)
// STEP2, 对key=value的键值对用&连接起来,略过空值 & sign
var signStrings string
for _, k := range sortedKeys {
value := fmt.Sprintf("%v", cast.ToString(resMap[k]))
if value != "" && strings.ToLower(k) != "sign" {
signStrings = signStrings + getTagKeyName(k, &notifyRes) + "=" + value + "&"
}
}
// STEP3, 在键值对的最后加上key=API_KEY
signStrings = signStrings + "key=" + pcf.PayKey
// STEP4, 进行MD5签名并且将所有字符转为大写.
sign := util.MD5Sum(signStrings)
if sign != *notifyRes.Sign {
return false
}
return true
}
func getTagKeyName(key string, notifyRes *NotifyResult) string {
s := reflect.TypeOf(notifyRes).Elem()
f, _ := s.FieldByName(key)
name := f.Tag.Get("xml")
return name
}

View File

@@ -2,10 +2,17 @@ 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"
@@ -27,15 +34,21 @@ type Params struct {
OutTradeNo string
OpenID string
TradeType string
SignType string
Detail string
Attach string
GoodsTag string
NotifyURL string
}
// Config 是传出用于 jsdk 用的参数
// Config 是传出用于 js sdk 用的参数
type Config struct {
Timestamp int64
NonceStr string
PrePayID string
SignType string
Sign string
Timestamp string `json:"timestamp"`
NonceStr string `json:"nonceStr"`
PrePayID string `json:"prePayId"`
SignType string `json:"signType"`
Package string `json:"package"`
PaySign string `json:"paySign"`
}
// PreOrder 是 unifie order 接口的返回
@@ -54,7 +67,7 @@ type PreOrder struct {
ErrCodeDes string `xml:"err_code_des,omitempty"`
}
//payRequest 接口请求参数
// payRequest 接口请求参数
type payRequest struct {
AppID string `xml:"appid"`
MchID string `xml:"mch_id"`
@@ -64,20 +77,20 @@ type payRequest struct {
SignType string `xml:"sign_type,omitempty"`
Body string `xml:"body"`
Detail string `xml:"detail,omitempty"`
Attach string `xml:"attach,omitempty"` //附加数据
OutTradeNo string `xml:"out_trade_no"` //商户订单号
FeeType string `xml:"fee_type,omitempty"` //标价币种
TotalFee string `xml:"total_fee"` //标价金额
SpbillCreateIP string `xml:"spbill_create_ip"` //终端IP
TimeStart string `xml:"time_start,omitempty"` //交易起始时间
TimeExpire string `xml:"time_expire,omitempty"` //交易结束时间
GoodsTag string `xml:"goods_tag,omitempty"` //订单优惠标记
NotifyURL string `xml:"notify_url"` //通知地址
TradeType string `xml:"trade_type"` //交易类型
ProductID string `xml:"product_id,omitempty"` //商品ID
Attach string `xml:"attach,omitempty"` // 附加数据
OutTradeNo string `xml:"out_trade_no"` // 商户订单号
FeeType string `xml:"fee_type,omitempty"` // 标价币种
TotalFee string `xml:"total_fee"` // 标价金额
SpbillCreateIP string `xml:"spbill_create_ip"` // 终端IP
TimeStart string `xml:"time_start,omitempty"` // 交易起始时间
TimeExpire string `xml:"time_expire,omitempty"` // 交易结束时间
GoodsTag string `xml:"goods_tag,omitempty"` // 订单优惠标记
NotifyURL string `xml:"notify_url"` // 通知地址
TradeType string `xml:"trade_type"` // 交易类型
ProductID string `xml:"product_id,omitempty"` // 商品ID
LimitPay string `xml:"limit_pay,omitempty"` //
OpenID string `xml:"openid,omitempty"` //用户标识
SceneInfo string `xml:"scene_info,omitempty"` //场景信息
OpenID string `xml:"openid,omitempty"` // 用户标识
SceneInfo string `xml:"scene_info,omitempty"` // 场景信息
}
// NewPay return an instance of Pay package
@@ -86,20 +99,72 @@ func NewPay(ctx *context.Context) *Pay {
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["notify_url"] = pcf.PayNotifyURL
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)
@@ -113,9 +178,13 @@ func (pcf *Pay) PrePayOrder(p *Params) (payOrder PreOrder, err error) {
OutTradeNo: p.OutTradeNo,
TotalFee: p.TotalFee,
SpbillCreateIP: p.CreateIP,
NotifyURL: pcf.PayNotifyURL,
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 {
@@ -126,7 +195,7 @@ func (pcf *Pay) PrePayOrder(p *Params) (payOrder PreOrder, err error) {
return
}
if payOrder.ReturnCode == "SUCCESS" {
//pay success
// pay success
if payOrder.ResultCode == "SUCCESS" {
err = nil
return

View File

@@ -65,7 +65,9 @@ func (srv *Server) Serve() error {
}
//debug
//fmt.Println("request msg = ", string(srv.requestRawXMLMsg))
if srv.debug {
fmt.Println("request msg = ", string(srv.requestRawXMLMsg))
}
return srv.buildResponse(response)
}

32
tcb/README.md Normal file
View File

@@ -0,0 +1,32 @@
# 小程序-云开发 SDK
Tencent Cloud Base [文档](https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/)
## 使用说明
**初始化配置**
```golang
//使用memcache保存access_token也可选择redis或自定义cache
memCache=cache.NewMemcache("127.0.0.1:11211")
//配置小程序参数
config := &wechat.Config{
AppID: "your app id",
AppSecret: "your app secret",
Cache: memCache,
}
wc := wechat.NewWechat(config)
wcTcb := wc.GetTcb()
```
### 举例
#### 触发云函数
```golang
res, err := wcTcb.InvokeCloudFunction("test-xxxx", "add", `{"a":1,"b":2}`)
if err != nil {
panic(err)
}
```
更多使用方法参考[GODOC](https://godoc.org/github.com/silenceper/wechat/tcb)

35
tcb/cloudfunction.go Normal file
View File

@@ -0,0 +1,35 @@
package tcb
import (
"fmt"
"github.com/silenceper/wechat/util"
)
const (
//触发云函数
invokeCloudFunctionURL = "https://api.weixin.qq.com/tcb/invokecloudfunction"
)
//InvokeCloudFunctionRes 云函数调用返回结果
type InvokeCloudFunctionRes struct {
util.CommonError
RespData string `json:"resp_data"` //云函数返回的buffer
}
//InvokeCloudFunction 云函数调用
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/functions/invokeCloudFunction.html
func (tcb *Tcb) InvokeCloudFunction(env, name, args string) (*InvokeCloudFunctionRes, error) {
accessToken, err := tcb.GetAccessToken()
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s?access_token=%s&env=%s&name=%s", invokeCloudFunctionURL, accessToken, env, name)
response, err := util.HTTPPost(uri, args)
if err != nil {
return nil, err
}
invokeCloudFunctionRes := &InvokeCloudFunctionRes{}
err = util.DecodeWithError(response, invokeCloudFunctionRes, "InvokeCloudFunction")
return invokeCloudFunctionRes, err
}

418
tcb/database.go Normal file
View File

@@ -0,0 +1,418 @@
package tcb
import (
"fmt"
"github.com/silenceper/wechat/util"
)
const (
//数据库导入
databaseMigrateImportURL = "https://api.weixin.qq.com/tcb/databasemigrateimport"
//数据库导出
databaseMigrateExportURL = "https://api.weixin.qq.com/tcb/databasemigrateexport"
//数据库迁移状态查询
databaseMigrateQueryInfoURL = "https://api.weixin.qq.com/tcb/databasemigratequeryinfo"
//变更数据库索引
updateIndexURL = "https://api.weixin.qq.com/tcb/updateindex"
//新增集合
databaseCollectionAddURL = "https://api.weixin.qq.com/tcb/databasecollectionadd"
//删除集合
databaseCollectionDeleteURL = "https://api.weixin.qq.com/tcb/databasecollectiondelete"
//获取特定云环境下集合信息
databaseCollectionGetURL = "https://api.weixin.qq.com/tcb/databasecollectionget"
//数据库插入记录
databaseAddURL = "https://api.weixin.qq.com/tcb/databaseadd"
//数据库删除记录
databaseDeleteURL = "https://api.weixin.qq.com/tcb/databasedelete"
//数据库更新记录
databaseUpdateURL = "https://api.weixin.qq.com/tcb/databaseupdate"
//数据库查询记录
databaseQueryURL = "https://api.weixin.qq.com/tcb/databasequery"
//统计集合记录数或统计查询语句对应的结果记录数
databaseCountURL = "https://api.weixin.qq.com/tcb/databasecount"
//ConflictModeInster 冲突处理模式 插入
ConflictModeInster ConflictMode = 1
//ConflictModeUpsert 冲突处理模式 更新
ConflictModeUpsert ConflictMode = 2
//FileTypeJSON 的合法值 json
FileTypeJSON FileType = 1
//FileTypeCsv 的合法值 csv
FileTypeCsv FileType = 2
)
//ConflictMode 冲突处理模式
type ConflictMode int
//FileType 文件上传和导出的允许文件类型
type FileType int
//ValidDirections 合法的direction值
var ValidDirections = []string{"1", "-1", "2dsphere"}
//DatabaseMigrateExportReq 数据库出 请求参数
type DatabaseMigrateExportReq struct {
Env string `json:"env,omitempty"` //云环境ID
FilePath string `json:"file_path,omitempty"` //导出文件路径(导入文件需先上传到同环境的存储中,可使用开发者工具或 HTTP API的上传文件 API上传
FileType FileType `json:"file_type,omitempty"` //导出文件类型,文件格式参考数据库导入指引中的文件格式部分 1:json 2:csv
Query string `json:"query,omitempty"` //导出条件
}
//DatabaseMigrateExportRes 数据库导出 返回结果
type DatabaseMigrateExportRes struct {
util.CommonError
JobID int64 `json:"job_id"` //导出任务ID可使用数据库迁移进度查询 API 查询导入进度及结果
}
//DatabaseMigrateImportReq 数据库导入 请求参数
type DatabaseMigrateImportReq struct {
Env string `json:"env,omitempty"` //云环境ID
CollectionName string `json:"collection_name,omitempty"` //集合名称
FilePath string `json:"file_path,omitempty"` //导出文件路径(文件会导出到同环境的云存储中,可使用获取下载链接 API 获取下载链接)
FileType FileType `json:"file_type,omitempty"` //导入文件类型,文件格式参考数据库导入指引中的文件格式部分 1:json 2:csv
StopOnError bool `json:"stop_on_error,omitempty"` //是否在遇到错误时停止导入
ConflictMode ConflictMode `json:"conflict_mode,omitempty"` //冲突处理模式 1:inster 2:UPSERT
}
//DatabaseMigrateImportRes 数据库导入 返回结果
type DatabaseMigrateImportRes struct {
util.CommonError
JobID int64 `json:"job_id"` //导入任务ID可使用数据库迁移进度查询 API 查询导入进度及结果
}
//DatabaseMigrateQueryInfoRes 数据库迁移状态查询
type DatabaseMigrateQueryInfoRes struct {
util.CommonError
Status string `json:"status"` //导出状态
RecordSuccess int64 `json:"record_success"` //导出成功记录数
RecordFail int64 `json:"record_fail"` //导出失败记录数
ErrMsg string `json:"err_msg"` //导出错误信息
FileURL string `json:"file_url"` //导出文件下载地址
}
//UpdateIndexReq 变更数据库索引 请求参数
type UpdateIndexReq struct {
Env string `json:"env,omitempty"` //云环境ID
CollectionName string `json:"collection_name,omitempty"` //集合名称
CreateIndexes []CreateIndex `json:"create_indexes,omitempty"` //新增索引
DropIndexes []DropIndex `json:"drop_indexes,omitempty"` //删除索引
}
//CreateIndex 新增索引
type CreateIndex struct {
Name string `json:"name,omitempty"` //索引名
Unique bool `json:"unique,omitempty"` //是否唯一
Keys []CreateIndexKey `json:"keys,omitempty"` //索引字段
}
//CreateIndexKey create index key
type CreateIndexKey struct {
Name string `json:"name,omitempty"` //字段名
Direction string `json:"direction,omitempty"` //字段排序
}
//DropIndex 删除索引
type DropIndex struct {
Name string `json:"name,omitempty"`
}
//DatabaseCollectionReq 新增/删除集合请求参数
type DatabaseCollectionReq struct {
Env string `json:"env,omitempty"` //云环境ID
CollectionName string `json:"collection_name,omitempty"` //集合名称
}
//DatabaseCollectionGetReq 获取特定云环境下集合信息请求
type DatabaseCollectionGetReq struct {
Env string `json:"env,omitempty"` //云环境ID
Limit int64 `json:"limit,omitempty"` //获取数量限制
Offset int64 `json:"offset,omitempty"` //偏移量
}
//DatabaseCollectionGetRes 获取特定云环境下集合信息结果
type DatabaseCollectionGetRes struct {
util.CommonError
Pager struct {
Limit int64 `json:"limit"` //单次查询限制
Offset int64 `json:"offset"` //偏移量
Total int64 `json:"total"` //符合查询条件的记录总数
} `json:"pager"`
Collections []struct {
Name string `json:"name"` //集合名
Count int64 `json:"count"` //表中文档数量
Size int64 `json:"size"` //表的大小(即表中文档总大小),单位:字节
IndexCount int64 `json:"index_count"` //索引数量
IndexSize int64 `json:"index_size"` //索引占用大小,单位:字节
} `json:"collections"`
}
//DatabaseReq 数据库插入/删除/更新/查询/统计记录请求参数
type DatabaseReq struct {
Env string `json:"env,omitempty"` //云环境ID
Query string `json:"query,omitempty"` //数据库操作语句
}
//DatabaseAddRes 数据库插入记录返回结果
type DatabaseAddRes struct {
util.CommonError
IDList []string `json:"id_list"` //插入成功的数据集合主键_id。
}
//DatabaseDeleteRes 数据库删除记录返回结果
type DatabaseDeleteRes struct {
util.CommonError
Deleted int64 `json:"deleted"` //删除记录数量
}
//DatabaseUpdateRes 数据库更新记录返回结果
type DatabaseUpdateRes struct {
util.CommonError
Matched int64 `json:"matched"` //更新条件匹配到的结果数
Modified int64 `json:"modified"` //修改的记录数注意使用set操作新插入的数据不计入修改数目
ID string `json:"id"`
}
//DatabaseQueryRes 数据库查询记录 返回结果
type DatabaseQueryRes struct {
util.CommonError
Pager struct {
Limit int64 `json:"limit"` //单次查询限制
Offset int64 `json:"offset"` //偏移量
Total int64 `json:"total"` //符合查询条件的记录总数
} `json:"pager"`
Data []string `json:"data"`
}
//DatabaseCountRes 统计集合记录数或统计查询语句对应的结果记录数 返回结果
type DatabaseCountRes struct {
util.CommonError
Count int64 `json:"count"` //记录数量
}
//DatabaseMigrateImport 数据库导入
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseMigrateImport.html
func (tcb *Tcb) DatabaseMigrateImport(req *DatabaseMigrateImportReq) (*DatabaseMigrateImportRes, error) {
accessToken, err := tcb.GetAccessToken()
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s?access_token=%s", databaseMigrateImportURL, accessToken)
response, err := util.PostJSON(uri, req)
if err != nil {
return nil, err
}
databaseMigrateImportRes := &DatabaseMigrateImportRes{}
err = util.DecodeWithError(response, databaseMigrateImportRes, "DatabaseMigrateImport")
return databaseMigrateImportRes, err
}
//DatabaseMigrateExport 数据库导出
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseMigrateExport.html
func (tcb *Tcb) DatabaseMigrateExport(req *DatabaseMigrateExportReq) (*DatabaseMigrateExportRes, error) {
accessToken, err := tcb.GetAccessToken()
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s?access_token=%s", databaseMigrateExportURL, accessToken)
response, err := util.PostJSON(uri, req)
if err != nil {
return nil, err
}
databaseMigrateExportRes := &DatabaseMigrateExportRes{}
err = util.DecodeWithError(response, databaseMigrateExportRes, "DatabaseMigrateExport")
return databaseMigrateExportRes, err
}
//DatabaseMigrateQueryInfo 数据库迁移状态查询
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseMigrateQueryInfo.html
func (tcb *Tcb) DatabaseMigrateQueryInfo(env string, jobID int64) (*DatabaseMigrateQueryInfoRes, error) {
accessToken, err := tcb.GetAccessToken()
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s?access_token=%s", databaseMigrateQueryInfoURL, accessToken)
response, err := util.PostJSON(uri, map[string]interface{}{
"env": env,
"job_id": jobID,
})
if err != nil {
return nil, err
}
databaseMigrateQueryInfoRes := &DatabaseMigrateQueryInfoRes{}
err = util.DecodeWithError(response, databaseMigrateQueryInfoRes, "DatabaseMigrateQueryInfo")
return databaseMigrateQueryInfoRes, err
}
//UpdateIndex 变更数据库索引
//https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/updateIndex.html
func (tcb *Tcb) UpdateIndex(req *UpdateIndexReq) error {
accessToken, err := tcb.GetAccessToken()
if err != nil {
return err
}
uri := fmt.Sprintf("%s?access_token=%s", updateIndexURL, accessToken)
response, err := util.PostJSON(uri, req)
if err != nil {
return err
}
return util.DecodeWithCommonError(response, "UpdateIndex")
}
//DatabaseCollectionAdd 新增集合
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCollectionAdd.html
func (tcb *Tcb) DatabaseCollectionAdd(env, collectionName string) error {
accessToken, err := tcb.GetAccessToken()
if err != nil {
return err
}
uri := fmt.Sprintf("%s?access_token=%s", databaseCollectionAddURL, accessToken)
response, err := util.PostJSON(uri, &DatabaseCollectionReq{
Env: env,
CollectionName: collectionName,
})
if err != nil {
return err
}
return util.DecodeWithCommonError(response, "DatabaseCollectionAdd")
}
//DatabaseCollectionDelete 删除集合
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCollectionDelete.html
func (tcb *Tcb) DatabaseCollectionDelete(env, collectionName string) error {
accessToken, err := tcb.GetAccessToken()
if err != nil {
return err
}
uri := fmt.Sprintf("%s?access_token=%s", databaseCollectionDeleteURL, accessToken)
response, err := util.PostJSON(uri, &DatabaseCollectionReq{
Env: env,
CollectionName: collectionName,
})
if err != nil {
return err
}
return util.DecodeWithCommonError(response, "DatabaseCollectionDelete")
}
//DatabaseCollectionGet 获取特定云环境下集合信息
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCollectionGet.html
func (tcb *Tcb) DatabaseCollectionGet(env string, limit, offset int64) (*DatabaseCollectionGetRes, error) {
accessToken, err := tcb.GetAccessToken()
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s?access_token=%s", databaseCollectionGetURL, accessToken)
response, err := util.PostJSON(uri, &DatabaseCollectionGetReq{
Env: env,
Limit: limit,
Offset: offset,
})
if err != nil {
return nil, err
}
databaseCollectionGetRes := &DatabaseCollectionGetRes{}
err = util.DecodeWithError(response, databaseCollectionGetRes, "DatabaseCollectionGet")
return databaseCollectionGetRes, err
}
//DatabaseAdd 数据库插入记录
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseAdd.html
func (tcb *Tcb) DatabaseAdd(env, query string) (*DatabaseAddRes, error) {
accessToken, err := tcb.GetAccessToken()
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s?access_token=%s", databaseAddURL, accessToken)
response, err := util.PostJSON(uri, &DatabaseReq{
Env: env,
Query: query,
})
if err != nil {
return nil, err
}
databaseAddRes := &DatabaseAddRes{}
err = util.DecodeWithError(response, databaseAddRes, "DatabaseAdd")
return databaseAddRes, err
}
//DatabaseDelete 数据库插入记录
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseDelete.html
func (tcb *Tcb) DatabaseDelete(env, query string) (*DatabaseDeleteRes, error) {
accessToken, err := tcb.GetAccessToken()
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s?access_token=%s", databaseDeleteURL, accessToken)
response, err := util.PostJSON(uri, &DatabaseReq{
Env: env,
Query: query,
})
if err != nil {
return nil, err
}
databaseDeleteRes := &DatabaseDeleteRes{}
err = util.DecodeWithError(response, databaseDeleteRes, "DatabaseDelete")
return databaseDeleteRes, err
}
//DatabaseUpdate 数据库插入记录
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseUpdate.html
func (tcb *Tcb) DatabaseUpdate(env, query string) (*DatabaseUpdateRes, error) {
accessToken, err := tcb.GetAccessToken()
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s?access_token=%s", databaseUpdateURL, accessToken)
response, err := util.PostJSON(uri, &DatabaseReq{
Env: env,
Query: query,
})
if err != nil {
return nil, err
}
databaseUpdateRes := &DatabaseUpdateRes{}
err = util.DecodeWithError(response, databaseUpdateRes, "DatabaseUpdate")
return databaseUpdateRes, err
}
//DatabaseQuery 数据库查询记录
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseQuery.html
func (tcb *Tcb) DatabaseQuery(env, query string) (*DatabaseQueryRes, error) {
accessToken, err := tcb.GetAccessToken()
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s?access_token=%s", databaseQueryURL, accessToken)
response, err := util.PostJSON(uri, &DatabaseReq{
Env: env,
Query: query,
})
if err != nil {
return nil, err
}
databaseQueryRes := &DatabaseQueryRes{}
err = util.DecodeWithError(response, databaseQueryRes, "DatabaseQuery")
return databaseQueryRes, err
}
//DatabaseCount 统计集合记录数或统计查询语句对应的结果记录数
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/database/databaseCount.html
func (tcb *Tcb) DatabaseCount(env, query string) (*DatabaseCountRes, error) {
accessToken, err := tcb.GetAccessToken()
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s?access_token=%s", databaseCountURL, accessToken)
response, err := util.PostJSON(uri, &DatabaseReq{
Env: env,
Query: query,
})
if err != nil {
return nil, err
}
databaseCountRes := &DatabaseCountRes{}
err = util.DecodeWithError(response, databaseCountRes, "DatabaseCount")
return databaseCountRes, err
}

134
tcb/file.go Normal file
View File

@@ -0,0 +1,134 @@
package tcb
import (
"fmt"
"github.com/silenceper/wechat/util"
)
const (
//获取文件上传链接
uploadFilePathURL = "https://api.weixin.qq.com/tcb/uploadfile"
//获取文件下载链接
batchDownloadFileURL = "https://api.weixin.qq.com/tcb/batchdownloadfile"
//删除文件链接
batchDeleteFileURL = "https://api.weixin.qq.com/tcb/batchdeletefile"
)
//UploadFileReq 上传文件请求值
type UploadFileReq struct {
Env string `json:"env,omitempty"`
Path string `json:"path,omitempty"`
}
//UploadFileRes 上传文件返回结果
type UploadFileRes struct {
util.CommonError
URL string `json:"url"` //上传url
Token string `json:"token"` //token
Authorization string `json:"authorization"` //authorization
FileID string `json:"file_id"` //文件ID
CosFileID string `json:"cos_file_id"` //cos文件ID
}
//BatchDownloadFileReq 上传文件请求值
type BatchDownloadFileReq struct {
Env string `json:"env,omitempty"`
FileList []*DownloadFile `json:"file_list,omitempty"`
}
//DownloadFile 文件信息
type DownloadFile struct {
FileID string `json:"fileid"` //文件ID
MaxAge int64 `json:"max_age"` //下载链接有效期
}
//BatchDownloadFileRes 上传文件返回结果
type BatchDownloadFileRes struct {
util.CommonError
FileList []struct {
FileID string `json:"file_id"` //文件ID
DownloadURL string `json:"download_url"` //下载链接
Status int64 `json:"status"` //状态码
ErrMsg string `json:"errmsg"` //该文件错误信息
} `json:"file_list"`
}
//BatchDeleteFileReq 批量删除文件请求参数
type BatchDeleteFileReq struct {
Env string `json:"env,omitempty"`
FileIDList []string `json:"fileid_list,omitempty"`
}
//BatchDeleteFileRes 批量删除文件返回结果
type BatchDeleteFileRes struct {
util.CommonError
DeleteList []struct {
FileID string `json:"fileid"`
Status int64 `json:"status"`
ErrMsg string `json:"errmsg"`
} `json:"delete_list"`
}
//UploadFile 上传文件
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/storage/uploadFile.html
func (tcb *Tcb) UploadFile(env, path string) (*UploadFileRes, error) {
accessToken, err := tcb.GetAccessToken()
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s?access_token=%s", uploadFilePathURL, accessToken)
req := &UploadFileReq{
Env: env,
Path: path,
}
response, err := util.PostJSON(uri, req)
if err != nil {
return nil, err
}
uploadFileRes := &UploadFileRes{}
err = util.DecodeWithError(response, uploadFileRes, "UploadFile")
return uploadFileRes, err
}
//BatchDownloadFile 获取文件下载链接
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/storage/batchDownloadFile.html
func (tcb *Tcb) BatchDownloadFile(env string, fileList []*DownloadFile) (*BatchDownloadFileRes, error) {
accessToken, err := tcb.GetAccessToken()
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s?access_token=%s", batchDownloadFileURL, accessToken)
req := &BatchDownloadFileReq{
Env: env,
FileList: fileList,
}
response, err := util.PostJSON(uri, req)
if err != nil {
return nil, err
}
batchDownloadFileRes := &BatchDownloadFileRes{}
err = util.DecodeWithError(response, batchDownloadFileRes, "BatchDownloadFile")
return batchDownloadFileRes, err
}
//BatchDeleteFile 批量删除文件
//reference:https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-http-api/storage/batchDeleteFile.html
func (tcb *Tcb) BatchDeleteFile(env string, fileIDList []string) (*BatchDeleteFileRes, error) {
accessToken, err := tcb.GetAccessToken()
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s?access_token=%s", batchDeleteFileURL, accessToken)
req := &BatchDeleteFileReq{
Env: env,
FileIDList: fileIDList,
}
response, err := util.PostJSON(uri, req)
if err != nil {
return nil, err
}
batchDeleteFileRes := &BatchDeleteFileRes{}
err = util.DecodeWithError(response, batchDeleteFileRes, "BatchDeleteFile")
return batchDeleteFileRes, nil
}

16
tcb/tcb.go Normal file
View File

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

View File

@@ -3,6 +3,7 @@ package util
import (
"encoding/json"
"fmt"
"reflect"
)
// CommonError 微信返回的通用错误json
@@ -23,3 +24,28 @@ func DecodeWithCommonError(response []byte, apiName string) (err error) {
}
return nil
}
// DecodeWithError 将返回值按照解析
func DecodeWithError(response []byte, obj interface{}, apiName string) error {
err := json.Unmarshal(response, obj)
if err != nil {
return fmt.Errorf("json Unmarshal Error, err=%v", err)
}
responseObj := reflect.ValueOf(obj)
if !responseObj.IsValid() {
return fmt.Errorf("obj is invalid")
}
commonError := responseObj.Elem().FieldByName("CommonError")
if !commonError.IsValid() || commonError.Kind() != reflect.Struct {
return fmt.Errorf("commonError is invalid or not struct")
}
errCode := commonError.FieldByName("ErrCode")
errMsg := commonError.FieldByName("ErrMsg")
if !errCode.IsValid() || !errMsg.IsValid() {
return fmt.Errorf("errcode or errmsg is invalid")
}
if errCode.Int() != 0 {
return fmt.Errorf("%s Error , errcode=%d , errmsg=%s", apiName, errCode.Int(), errMsg.String())
}
return nil
}

View File

@@ -7,13 +7,14 @@ import (
"encoding/pem"
"encoding/xml"
"fmt"
"golang.org/x/crypto/pkcs12"
"io"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"os"
"golang.org/x/crypto/pkcs12"
)
//HTTPGet get 请求
@@ -30,17 +31,30 @@ func HTTPGet(uri string) ([]byte, error) {
return ioutil.ReadAll(response.Body)
}
//HTTPPost post 请求
func HTTPPost(uri string, data string) ([]byte, error) {
body := bytes.NewBuffer([]byte(data))
response, err := http.Post(uri, "", body)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("http get error : uri=%v , statusCode=%v", uri, response.StatusCode)
}
return ioutil.ReadAll(response.Body)
}
//PostJSON post json 数据请求
func PostJSON(uri string, obj interface{}) ([]byte, error) {
jsonData, err := json.Marshal(obj)
if err != nil {
return nil, err
}
jsonData = bytes.Replace(jsonData, []byte("\\u003c"), []byte("<"), -1)
jsonData = bytes.Replace(jsonData, []byte("\\u003e"), []byte(">"), -1)
jsonData = bytes.Replace(jsonData, []byte("\\u0026"), []byte("&"), -1)
body := bytes.NewBuffer(jsonData)
response, err := http.Post(uri, "application/json;charset=utf-8", body)
if err != nil {

144
vendor/vendor.json vendored
View File

@@ -1,144 +0,0 @@
{
"comment": "",
"ignore": "test",
"package": [
{
"checksumSHA1": "ZZ4FL7s5f8QK4RysjZObSBYGOLY=",
"path": "github.com/astaxie/beego",
"revision": "2d87d4feafeea0a133d217a82e6e02df0348fed5",
"revisionTime": "2016-09-22T15:18:45Z"
},
{
"checksumSHA1": "LwEiQ/Hyb7Ul28TSlwowN9cpWDY=",
"path": "github.com/astaxie/beego/config",
"revision": "2d87d4feafeea0a133d217a82e6e02df0348fed5",
"revisionTime": "2016-09-22T15:18:45Z"
},
{
"checksumSHA1": "s+gj1rES9SvvCIyF8W2tzlziSPE=",
"path": "github.com/astaxie/beego/context",
"revision": "2d87d4feafeea0a133d217a82e6e02df0348fed5",
"revisionTime": "2016-09-22T15:18:45Z"
},
{
"checksumSHA1": "PDNn16w89zWODshT9zlPzSmWZFA=",
"path": "github.com/astaxie/beego/grace",
"revision": "2d87d4feafeea0a133d217a82e6e02df0348fed5",
"revisionTime": "2016-09-22T15:18:45Z"
},
{
"checksumSHA1": "Iz/p1UTvFNe5HFeohX7cvKEOQW0=",
"path": "github.com/astaxie/beego/logs",
"revision": "2d87d4feafeea0a133d217a82e6e02df0348fed5",
"revisionTime": "2016-09-22T15:18:45Z"
},
{
"checksumSHA1": "R797q1pCbp086SraUETxX1rsJYw=",
"path": "github.com/astaxie/beego/session",
"revision": "2d87d4feafeea0a133d217a82e6e02df0348fed5",
"revisionTime": "2016-09-22T15:18:45Z"
},
{
"checksumSHA1": "rxxln8GHFToVnaEJz4JMv0WbaKc=",
"path": "github.com/astaxie/beego/toolbox",
"revision": "2d87d4feafeea0a133d217a82e6e02df0348fed5",
"revisionTime": "2016-09-22T15:18:45Z"
},
{
"checksumSHA1": "fRJk3RZPPz6ovbautfsfxAk+CrI=",
"path": "github.com/astaxie/beego/utils",
"revision": "2d87d4feafeea0a133d217a82e6e02df0348fed5",
"revisionTime": "2016-09-22T15:18:45Z"
},
{
"checksumSHA1": "fNAC4qgZDqF3kxq74/yyk3PWdy8=",
"path": "github.com/bradfitz/gomemcache/memcache",
"revision": "fb1f79c6b65acda83063cbc69f6bba1522558bfc",
"revisionTime": "2016-01-17T19:21:50Z"
},
{
"checksumSHA1": "RsNwOto8G8aXIiRrlFn4dtU9q/g=",
"path": "github.com/gin-gonic/gin",
"revision": "e2212d40c62a98b388a5eb48ecbdcf88534688ba",
"revisionTime": "2016-12-04T22:13:08Z"
},
{
"checksumSHA1": "UsILDoIB2S7ra+w2fMdb85mX3HM=",
"path": "github.com/gin-gonic/gin/binding",
"revision": "e2212d40c62a98b388a5eb48ecbdcf88534688ba",
"revisionTime": "2016-12-04T22:13:08Z"
},
{
"checksumSHA1": "PHv9FNb7YavJWtAHcY6ZgXmkmHs=",
"path": "github.com/gin-gonic/gin/render",
"revision": "e2212d40c62a98b388a5eb48ecbdcf88534688ba",
"revisionTime": "2016-12-04T22:13:08Z"
},
{
"checksumSHA1": "kBeNcaKk56FguvPSUCEaH6AxpRc=",
"path": "github.com/golang/protobuf/proto",
"revision": "8ee79997227bf9b34611aee7946ae64735e6fd93",
"revisionTime": "2016-11-17T03:31:26Z"
},
{
"checksumSHA1": "w3QCCIYHgZzIXQ+xTl7oLfFrXHs=",
"path": "github.com/gomodule/redigo/internal",
"revision": "2cd21d9966bf7ff9ae091419744f0b3fb0fecace",
"revisionTime": "2018-06-27T14:45:07Z"
},
{
"checksumSHA1": "To/N5YA/FD0Rrs6r2OOmHXgxYwI=",
"path": "github.com/gomodule/redigo/redis",
"revision": "2cd21d9966bf7ff9ae091419744f0b3fb0fecace",
"revisionTime": "2018-06-27T14:45:07Z"
},
{
"checksumSHA1": "b0T0Hzd+zYk+OCDTFMps+jwa/nY=",
"path": "github.com/manucorporat/sse",
"revision": "ee05b128a739a0fb76c7ebd3ae4810c1de808d6d",
"revisionTime": "2016-01-26T18:01:36Z"
},
{
"checksumSHA1": "xZuhljnmBysJPta/lMyYmJdujCg=",
"path": "github.com/mattn/go-isatty",
"revision": "30a891c33c7cde7b02a981314b4228ec99380cca",
"revisionTime": "2016-11-23T14:36:37Z"
},
{
"checksumSHA1": "PJY7uCr3UnX4/Mf/RoWnbieSZ8o=",
"path": "golang.org/x/crypto/pkcs12",
"revision": "9f005a07e0d31d45e6656d241bb5c0f2efd4bc94",
"revisionTime": "2017-09-21T17:41:56Z"
},
{
"checksumSHA1": "iVJcif9M9uefvvqHCNR9VQrjc/s=",
"path": "golang.org/x/crypto/pkcs12/internal/rc2",
"revision": "9f005a07e0d31d45e6656d241bb5c0f2efd4bc94",
"revisionTime": "2017-09-21T17:41:56Z"
},
{
"checksumSHA1": "pancewZW3HwGvpDwfH5Imrbadc4=",
"path": "golang.org/x/net/context",
"revision": ""
},
{
"checksumSHA1": "8fD/im5Kwvy3JgmxulDTambmE8w=",
"path": "golang.org/x/sys/unix",
"revision": "a646d33e2ee3172a661fc09bca23bb4889a41bc8",
"revisionTime": "2016-07-15T05:43:45Z"
},
{
"checksumSHA1": "39V1idWER42Lmcmg2Uy40wMzOlo=",
"path": "gopkg.in/go-playground/validator.v8",
"revision": "5f57d2222ad794d0dffb07e664ea05e2ee07d60c",
"revisionTime": "2016-07-18T13:41:25Z"
},
{
"checksumSHA1": "12GqsW8PiRPnezDDy0v4brZrndM=",
"path": "gopkg.in/yaml.v2",
"revision": "a5b47d31c556af34a302ce5d659e6fea44d90de0",
"revisionTime": "2016-09-28T15:37:09Z"
}
],
"rootPath": "github.com/silenceper/wechat"
}

View File

@@ -6,15 +6,17 @@ import (
"github.com/silenceper/wechat/cache"
"github.com/silenceper/wechat/context"
"github.com/silenceper/wechat/device"
"github.com/silenceper/wechat/js"
"github.com/silenceper/wechat/material"
"github.com/silenceper/wechat/menu"
"github.com/silenceper/wechat/message"
"github.com/silenceper/wechat/miniprogram"
"github.com/silenceper/wechat/oauth"
"github.com/silenceper/wechat/pay"
"github.com/silenceper/wechat/qr"
"github.com/silenceper/wechat/server"
"github.com/silenceper/wechat/template"
"github.com/silenceper/wechat/tcb"
"github.com/silenceper/wechat/user"
)
@@ -93,8 +95,8 @@ func (wc *Wechat) GetUser() *user.User {
}
// GetTemplate 模板消息接口
func (wc *Wechat) GetTemplate() *template.Template {
return template.NewTemplate(wc.Context)
func (wc *Wechat) GetTemplate() *message.Template {
return message.NewTemplate(wc.Context)
}
// GetPay 返回支付消息的实例
@@ -111,3 +113,13 @@ func (wc *Wechat) GetQR() *qr.QR {
func (wc *Wechat) GetMiniProgram() *miniprogram.MiniProgram {
return miniprogram.NewMiniProgram(wc.Context)
}
// GetDevice 获取智能设备的实例
func (wc *Wechat) GetDevice() *device.Device {
return device.NewDevice(wc.Context)
}
// GetTcb 获取小程序-云开发的实例
func (wc *Wechat) GetTcb() *tcb.Tcb {
return tcb.NewTcb(wc.Context)
}