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

规范目录

This commit is contained in:
silenceper
2020-01-20 13:54:04 +08:00
parent 9b06954b10
commit 15ebd71a04
75 changed files with 985 additions and 1871 deletions

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/officialaccount/context"
//Basic struct
type Basic struct {
*context.Context
}
//NewBasic 实例
func NewBasic(context *context.Context) *Basic {
basic := new(Basic)
basic.Context = context
return basic
}

109
officialaccount/basic/qr.go Normal file
View File

@@ -0,0 +1,109 @@
package basic
import (
"encoding/json"
"fmt"
"reflect"
"time"
"github.com/silenceper/wechat/util"
)
const (
qrCreateURL = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s"
getQRImgURL = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=%s"
)
const (
actionID = "QR_SCENE"
actionStr = "QR_STR_SCENE"
actionLimitID = "QR_LIMIT_SCENE"
actionLimitStr = "QR_LIMIT_STR_SCENE"
)
// Request 临时二维码
type Request struct {
ExpireSeconds int64 `json:"expire_seconds,omitempty"`
ActionName string `json:"action_name"`
ActionInfo struct {
Scene struct {
SceneStr string `json:"scene_str,omitempty"`
SceneID int `json:"scene_id,omitempty"`
} `json:"scene"`
} `json:"action_info"`
}
// Ticket 二维码ticket
type Ticket struct {
util.CommonError `json:",inline"`
Ticket string `json:"ticket"`
ExpireSeconds int64 `json:"expire_seconds"`
URL string `json:"url"`
}
// GetQRTicket 获取二维码 Ticket
func (basic *Basic) GetQRTicket(tq *Request) (t *Ticket, err error) {
accessToken, err := basic.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(qrCreateURL, accessToken)
response, err := util.PostJSON(uri, tq)
if err != nil {
err = fmt.Errorf("get qr ticket failed, %s", err)
return
}
t = new(Ticket)
err = json.Unmarshal(response, &t)
if err != nil {
return
}
return
}
// ShowQRCode 通过ticket换取二维码
func ShowQRCode(tk *Ticket) string {
return fmt.Sprintf(getQRImgURL, tk.Ticket)
}
// NewTmpQrRequest 新建临时二维码请求实例
func NewTmpQrRequest(exp time.Duration, scene interface{}) *Request {
tq := &Request{
ExpireSeconds: int64(exp.Seconds()),
}
switch reflect.ValueOf(scene).Kind() {
case reflect.String:
tq.ActionName = actionStr
tq.ActionInfo.Scene.SceneStr = scene.(string)
case reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64:
tq.ActionName = actionID
tq.ActionInfo.Scene.SceneID = scene.(int)
}
return tq
}
// NewLimitQrRequest 新建永久二维码请求实例
func NewLimitQrRequest(scene interface{}) *Request {
tq := &Request{}
switch reflect.ValueOf(scene).Kind() {
case reflect.String:
tq.ActionName = actionLimitStr
tq.ActionInfo.Scene.SceneStr = scene.(string)
case reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64:
tq.ActionName = actionLimitID
tq.ActionInfo.Scene.SceneID = scene.(int)
}
return tq
}

View File

@@ -0,0 +1,14 @@
package config
import (
"github.com/silenceper/wechat/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/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/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

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

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
}

View File

@@ -0,0 +1,60 @@
package device
import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/officialaccount/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
}

View File

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

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
}

109
officialaccount/js/js.go Normal file
View File

@@ -0,0 +1,109 @@
package js
import (
"encoding/json"
"fmt"
"time"
"github.com/silenceper/wechat/officialaccount/context"
"github.com/silenceper/wechat/util"
)
const getTicketURL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi"
// Js struct
type Js struct {
*context.Context
}
// Config 返回给用户jssdk配置信息
type Config struct {
AppID string `json:"app_id"`
Timestamp int64 `json:"timestamp"`
NonceStr string `json:"nonce_str"`
Signature string `json:"signature"`
}
// resTicket 请求jsapi_tikcet返回结果
type resTicket struct {
util.CommonError
Ticket string `json:"ticket"`
ExpiresIn int64 `json:"expires_in"`
}
//NewJs init
func NewJs(context *context.Context) *Js {
js := new(Js)
js.Context = context
return js
}
//GetConfig 获取jssdk需要的配置参数
//uri 为当前网页地址
func (js *Js) GetConfig(uri string) (config *Config, err error) {
config = new(Config)
var ticketStr string
ticketStr, err = js.GetTicket()
if err != nil {
return
}
nonceStr := util.RandomStr(16)
timestamp := util.GetCurrTs()
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s&timestamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
sigStr := util.Signature(str)
config.AppID = js.AppID
config.NonceStr = nonceStr
config.Timestamp = timestamp
config.Signature = sigStr
return
}
//GetTicket 获取jsapi_ticket
func (js *Js) GetTicket() (ticketStr string, err error) {
js.GetJsAPITicketLock().Lock()
defer js.GetJsAPITicketLock().Unlock()
//先从cache中取
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", context.CacheKeyPrefix, js.AppID)
val := js.Cache.Get(jsAPITicketCacheKey)
if val != nil {
ticketStr = val.(string)
return
}
var ticket resTicket
ticket, err = js.getTicketFromServer()
if err != nil {
return
}
ticketStr = ticket.Ticket
return
}
//getTicketFromServer 强制从服务器中获取ticket
func (js *Js) getTicketFromServer() (ticket resTicket, err error) {
var accessToken string
accessToken, err = js.GetAccessToken()
if err != nil {
return
}
var response []byte
url := fmt.Sprintf(getTicketURL, accessToken)
response, err = util.HTTPGet(url)
err = json.Unmarshal(response, &ticket)
if err != nil {
return
}
if ticket.ErrCode != 0 {
err = fmt.Errorf("getTicket Error : errcode=%d , errmsg=%s", ticket.ErrCode, ticket.ErrMsg)
return
}
jsAPITicketCacheKey := fmt.Sprintf("%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

@@ -0,0 +1,218 @@
package material
import (
"encoding/json"
"errors"
"fmt"
"github.com/silenceper/wechat/officialaccount/context"
"github.com/silenceper/wechat/util"
)
const (
addNewsURL = "https://api.weixin.qq.com/cgi-bin/material/add_news"
addMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/add_material"
delMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/del_material"
getMaterialURL = "https://api.weixin.qq.com/cgi-bin/material/get_material"
)
//Material 素材管理
type Material struct {
*context.Context
}
//NewMaterial init
func NewMaterial(context *context.Context) *Material {
material := new(Material)
material.Context = context
return material
}
//Article 永久图文素材
type Article struct {
Title string `json:"title"`
ThumbMediaID string `json:"thumb_media_id"`
ThumbURL string `json:"thumb_url"`
Author string `json:"author"`
Digest string `json:"digest"`
ShowCoverPic int `json:"show_cover_pic"`
Content string `json:"content"`
ContentSourceURL string `json:"content_source_url"`
URL string `json:"url"`
DownURL string `json:"down_url"`
}
// GetNews 获取/下载永久素材
func (material *Material) GetNews(id string) ([]*Article, error) {
accessToken, err := material.GetAccessToken()
if err != nil {
return nil, err
}
uri := fmt.Sprintf("%s?access_token=%s", getMaterialURL, accessToken)
var req struct {
MediaID string `json:"media_id"`
}
req.MediaID = id
responseBytes, err := util.PostJSON(uri, req)
var res struct {
NewsItem []*Article `json:"news_item"`
}
err = json.Unmarshal(responseBytes, &res)
if err != nil {
return nil, err
}
return res.NewsItem, nil
}
//reqArticles 永久性图文素材请求信息
type reqArticles struct {
Articles []*Article `json:"articles"`
}
//resArticles 永久性图文素材返回结果
type resArticles struct {
util.CommonError
MediaID string `json:"media_id"`
}
//AddNews 新增永久图文素材
func (material *Material) AddNews(articles []*Article) (mediaID string, err error) {
req := &reqArticles{articles}
var accessToken string
accessToken, err = material.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s", addNewsURL, accessToken)
responseBytes, err := util.PostJSON(uri, req)
var res resArticles
err = json.Unmarshal(responseBytes, &res)
if err != nil {
return
}
mediaID = res.MediaID
return
}
//resAddMaterial 永久性素材上传返回的结果
type resAddMaterial struct {
util.CommonError
MediaID string `json:"media_id"`
URL string `json:"url"`
}
//AddMaterial 上传永久性素材(处理视频需要单独上传)
func (material *Material) AddMaterial(mediaType MediaType, filename string) (mediaID string, url string, err error) {
if mediaType == MediaTypeVideo {
err = errors.New("永久视频素材上传使用 AddVideo 方法")
}
var accessToken string
accessToken, err = material.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s&type=%s", addMaterialURL, accessToken, mediaType)
var response []byte
response, err = util.PostFile("media", filename, uri)
if err != nil {
return
}
var resMaterial resAddMaterial
err = json.Unmarshal(response, &resMaterial)
if err != nil {
return
}
if resMaterial.ErrCode != 0 {
err = fmt.Errorf("AddMaterial error : errcode=%v , errmsg=%v", resMaterial.ErrCode, resMaterial.ErrMsg)
return
}
mediaID = resMaterial.MediaID
url = resMaterial.URL
return
}
type reqVideo struct {
Title string `json:"title"`
Introduction string `json:"introduction"`
}
//AddVideo 永久视频素材文件上传
func (material *Material) AddVideo(filename, title, introduction string) (mediaID string, url string, err error) {
var accessToken string
accessToken, err = material.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s&type=video", addMaterialURL, accessToken)
videoDesc := &reqVideo{
Title: title,
Introduction: introduction,
}
var fieldValue []byte
fieldValue, err = json.Marshal(videoDesc)
if err != nil {
return
}
fields := []util.MultipartFormField{
{
IsFile: true,
Fieldname: "media",
Filename: filename,
},
{
IsFile: false,
Fieldname: "description",
Value: fieldValue,
},
}
var response []byte
response, err = util.PostMultipartForm(fields, uri)
if err != nil {
return
}
var resMaterial resAddMaterial
err = json.Unmarshal(response, &resMaterial)
if err != nil {
return
}
if resMaterial.ErrCode != 0 {
err = fmt.Errorf("AddMaterial error : errcode=%v , errmsg=%v", resMaterial.ErrCode, resMaterial.ErrMsg)
return
}
mediaID = resMaterial.MediaID
url = resMaterial.URL
return
}
type reqDeleteMaterial struct {
MediaID string `json:"media_id"`
}
//DeleteMaterial 删除永久素材
func (material *Material) DeleteMaterial(mediaID string) error {
accessToken, err := material.GetAccessToken()
if err != nil {
return err
}
uri := fmt.Sprintf("%s?access_token=%s", delMaterialURL, accessToken)
response, err := util.PostJSON(uri, reqDeleteMaterial{mediaID})
if err != nil {
return err
}
return util.DecodeWithCommonError(response, "DeleteMaterial")
}

View File

@@ -0,0 +1,110 @@
package material
import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/util"
)
//MediaType 媒体文件类型
type MediaType string
const (
//MediaTypeImage 媒体文件:图片
MediaTypeImage MediaType = "image"
//MediaTypeVoice 媒体文件:声音
MediaTypeVoice = "voice"
//MediaTypeVideo 媒体文件:视频
MediaTypeVideo = "video"
//MediaTypeThumb 媒体文件:缩略图
MediaTypeThumb = "thumb"
)
const (
mediaUploadURL = "https://api.weixin.qq.com/cgi-bin/media/upload"
mediaUploadImageURL = "https://api.weixin.qq.com/cgi-bin/media/uploadimg"
mediaGetURL = "https://api.weixin.qq.com/cgi-bin/media/get"
)
//Media 临时素材上传返回信息
type Media struct {
util.CommonError
Type MediaType `json:"type"`
MediaID string `json:"media_id"`
ThumbMediaID string `json:"thumb_media_id"`
CreatedAt int64 `json:"created_at"`
}
//MediaUpload 临时素材上传
func (material *Material) MediaUpload(mediaType MediaType, filename string) (media Media, err error) {
var accessToken string
accessToken, err = material.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s&type=%s", mediaUploadURL, accessToken, mediaType)
var response []byte
response, err = util.PostFile("media", filename, uri)
if err != nil {
return
}
err = json.Unmarshal(response, &media)
if err != nil {
return
}
if media.ErrCode != 0 {
err = fmt.Errorf("MediaUpload error : errcode=%v , errmsg=%v", media.ErrCode, media.ErrMsg)
return
}
return
}
//GetMediaURL 返回临时素材的下载地址供用户自己处理
//NOTICE: URL 不可公开因为含access_token 需要立即另存文件
func (material *Material) GetMediaURL(mediaID string) (mediaURL string, err error) {
var accessToken string
accessToken, err = material.GetAccessToken()
if err != nil {
return
}
mediaURL = fmt.Sprintf("%s?access_token=%s&media_id=%s", mediaGetURL, accessToken, mediaID)
return
}
//resMediaImage 图片上传返回结果
type resMediaImage struct {
util.CommonError
URL string `json:"url"`
}
//ImageUpload 图片上传
func (material *Material) ImageUpload(filename string) (url string, err error) {
var accessToken string
accessToken, err = material.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s", mediaUploadImageURL, accessToken)
var response []byte
response, err = util.PostFile("media", filename, uri)
if err != nil {
return
}
var image resMediaImage
err = json.Unmarshal(response, &image)
if err != nil {
return
}
if image.ErrCode != 0 {
err = fmt.Errorf("UploadImage error : errcode=%v , errmsg=%v", image.ErrCode, image.ErrMsg)
return
}
url = image.URL
return
}

View File

@@ -0,0 +1,143 @@
package menu
//Button 菜单按钮
type Button struct {
Type string `json:"type,omitempty"`
Name string `json:"name,omitempty"`
Key string `json:"key,omitempty"`
URL string `json:"url,omitempty"`
MediaID string `json:"media_id,omitempty"`
AppID string `json:"appid,omitempty"`
PagePath string `json:"pagepath,omitempty"`
SubButtons []*Button `json:"sub_button,omitempty"`
}
//SetSubButton 设置二级菜单
func (btn *Button) SetSubButton(name string, subButtons []*Button) {
btn.Name = name
btn.SubButtons = subButtons
btn.Type = ""
btn.Key = ""
btn.URL = ""
btn.MediaID = ""
}
//SetClickButton btn 为click类型
func (btn *Button) SetClickButton(name, key string) {
btn.Type = "click"
btn.Name = name
btn.Key = key
btn.URL = ""
btn.MediaID = ""
btn.SubButtons = nil
}
//SetViewButton view类型
func (btn *Button) SetViewButton(name, url string) {
btn.Type = "view"
btn.Name = name
btn.URL = url
btn.Key = ""
btn.MediaID = ""
btn.SubButtons = nil
}
// SetScanCodePushButton 扫码推事件
func (btn *Button) SetScanCodePushButton(name, key string) {
btn.Type = "scancode_push"
btn.Name = name
btn.Key = key
btn.URL = ""
btn.MediaID = ""
btn.SubButtons = nil
}
//SetScanCodeWaitMsgButton 设置 扫码推事件且弹出"消息接收中"提示框
func (btn *Button) SetScanCodeWaitMsgButton(name, key string) {
btn.Type = "scancode_waitmsg"
btn.Name = name
btn.Key = key
btn.URL = ""
btn.MediaID = ""
btn.SubButtons = nil
}
//SetPicSysPhotoButton 设置弹出系统拍照发图按钮
func (btn *Button) SetPicSysPhotoButton(name, key string) {
btn.Type = "pic_sysphoto"
btn.Name = name
btn.Key = key
btn.URL = ""
btn.MediaID = ""
btn.SubButtons = nil
}
//SetPicPhotoOrAlbumButton 设置弹出拍照或者相册发图类型按钮
func (btn *Button) SetPicPhotoOrAlbumButton(name, key string) {
btn.Type = "pic_photo_or_album"
btn.Name = name
btn.Key = key
btn.URL = ""
btn.MediaID = ""
btn.SubButtons = nil
}
// SetPicWeixinButton 设置弹出微信相册发图器类型按钮
func (btn *Button) SetPicWeixinButton(name, key string) {
btn.Type = "pic_weixin"
btn.Name = name
btn.Key = key
btn.URL = ""
btn.MediaID = ""
btn.SubButtons = nil
}
// SetLocationSelectButton 设置 弹出地理位置选择器 类型按钮
func (btn *Button) SetLocationSelectButton(name, key string) {
btn.Type = "location_select"
btn.Name = name
btn.Key = key
btn.URL = ""
btn.MediaID = ""
btn.SubButtons = nil
}
//SetMediaIDButton 设置 下发消息(除文本消息) 类型按钮
func (btn *Button) SetMediaIDButton(name, mediaID string) {
btn.Type = "media_id"
btn.Name = name
btn.MediaID = mediaID
btn.Key = ""
btn.URL = ""
btn.SubButtons = nil
}
//SetViewLimitedButton 设置 跳转图文消息URL 类型按钮
func (btn *Button) SetViewLimitedButton(name, mediaID string) {
btn.Type = "view_limited"
btn.Name = name
btn.MediaID = mediaID
btn.Key = ""
btn.URL = ""
btn.SubButtons = nil
}
//SetMiniprogramButton 设置 跳转小程序 类型按钮 (公众号后台必须已经关联小程序)
func (btn *Button) SetMiniprogramButton(name, url, appID, pagePath string) {
btn.Type = "miniprogram"
btn.Name = name
btn.URL = url
btn.AppID = appID
btn.PagePath = pagePath
btn.Key = ""
btn.MediaID = ""
btn.SubButtons = nil
}

View File

@@ -0,0 +1,270 @@
package menu
import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/officialaccount/context"
"github.com/silenceper/wechat/util"
)
const (
menuCreateURL = "https://api.weixin.qq.com/cgi-bin/menu/create"
menuGetURL = "https://api.weixin.qq.com/cgi-bin/menu/get"
menuDeleteURL = "https://api.weixin.qq.com/cgi-bin/menu/delete"
menuAddConditionalURL = "https://api.weixin.qq.com/cgi-bin/menu/addconditional"
menuDeleteConditionalURL = "https://api.weixin.qq.com/cgi-bin/menu/delconditional"
menuTryMatchURL = "https://api.weixin.qq.com/cgi-bin/menu/trymatch"
menuSelfMenuInfoURL = "https://api.weixin.qq.com/cgi-bin/get_current_selfmenu_info"
)
//Menu struct
type Menu struct {
*context.Context
}
//reqMenu 设置菜单请求数据
type reqMenu struct {
Button []*Button `json:"button,omitempty"`
MatchRule *MatchRule `json:"matchrule,omitempty"`
}
//reqDeleteConditional 删除个性化菜单请求数据
type reqDeleteConditional struct {
MenuID int64 `json:"menuid"`
}
//reqMenuTryMatch 菜单匹配请求
type reqMenuTryMatch struct {
UserID string `json:"user_id"`
}
//resConditionalMenu 个性化菜单返回结果
type resConditionalMenu struct {
Button []Button `json:"button"`
MatchRule MatchRule `json:"matchrule"`
MenuID int64 `json:"menuid"`
}
//resMenuTryMatch 菜单匹配请求结果
type resMenuTryMatch struct {
util.CommonError
Button []Button `json:"button"`
}
//ResMenu 查询菜单的返回数据
type ResMenu struct {
util.CommonError
Menu struct {
Button []Button `json:"button"`
MenuID int64 `json:"menuid"`
} `json:"menu"`
Conditionalmenu []resConditionalMenu `json:"conditionalmenu"`
}
//ResSelfMenuInfo 自定义菜单配置返回结果
type ResSelfMenuInfo struct {
util.CommonError
IsMenuOpen int32 `json:"is_menu_open"`
SelfMenuInfo struct {
Button []SelfMenuButton `json:"button"`
} `json:"selfmenu_info"`
}
//SelfMenuButton 自定义菜单配置详情
type SelfMenuButton struct {
Type string `json:"type"`
Name string `json:"name"`
Key string `json:"key"`
URL string `json:"url,omitempty"`
Value string `json:"value,omitempty"`
SubButton struct {
List []SelfMenuButton `json:"list"`
} `json:"sub_button,omitempty"`
NewsInfo struct {
List []ButtonNew `json:"list"`
} `json:"news_info,omitempty"`
}
//ButtonNew 图文消息菜单
type ButtonNew struct {
Title string `json:"title"`
Author string `json:"author"`
Digest string `json:"digest"`
ShowCover int32 `json:"show_cover"`
CoverURL string `json:"cover_url"`
ContentURL string `json:"content_url"`
SourceURL string `json:"source_url"`
}
//MatchRule 个性化菜单规则
type MatchRule struct {
GroupID int32 `json:"group_id,omitempty"`
Sex int32 `json:"sex,omitempty"`
Country string `json:"country,omitempty"`
Province string `json:"province,omitempty"`
City string `json:"city,omitempty"`
ClientPlatformType int32 `json:"client_platform_type,omitempty"`
Language string `json:"language,omitempty"`
}
//NewMenu 实例
func NewMenu(context *context.Context) *Menu {
menu := new(Menu)
menu.Context = context
return menu
}
//SetMenu 设置按钮
func (menu *Menu) SetMenu(buttons []*Button) error {
accessToken, err := menu.GetAccessToken()
if err != nil {
return err
}
uri := fmt.Sprintf("%s?access_token=%s", menuCreateURL, accessToken)
reqMenu := &reqMenu{
Button: buttons,
}
response, err := util.PostJSON(uri, reqMenu)
if err != nil {
return err
}
return util.DecodeWithCommonError(response, "SetMenu")
}
//GetMenu 获取菜单配置
func (menu *Menu) GetMenu() (resMenu ResMenu, err error) {
var accessToken string
accessToken, err = menu.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s", menuGetURL, accessToken)
var response []byte
response, err = util.HTTPGet(uri)
if err != nil {
return
}
err = json.Unmarshal(response, &resMenu)
if err != nil {
return
}
if resMenu.ErrCode != 0 {
err = fmt.Errorf("GetMenu Error , errcode=%d , errmsg=%s", resMenu.ErrCode, resMenu.ErrMsg)
return
}
return
}
//DeleteMenu 删除菜单
func (menu *Menu) DeleteMenu() error {
accessToken, err := menu.GetAccessToken()
if err != nil {
return err
}
uri := fmt.Sprintf("%s?access_token=%s", menuDeleteURL, accessToken)
response, err := util.HTTPGet(uri)
if err != nil {
return err
}
return util.DecodeWithCommonError(response, "GetMenu")
}
//AddConditional 添加个性化菜单
func (menu *Menu) AddConditional(buttons []*Button, matchRule *MatchRule) error {
accessToken, err := menu.GetAccessToken()
if err != nil {
return err
}
uri := fmt.Sprintf("%s?access_token=%s", menuAddConditionalURL, accessToken)
reqMenu := &reqMenu{
Button: buttons,
MatchRule: matchRule,
}
response, err := util.PostJSON(uri, reqMenu)
if err != nil {
return err
}
return util.DecodeWithCommonError(response, "AddConditional")
}
//DeleteConditional 删除个性化菜单
func (menu *Menu) DeleteConditional(menuID int64) error {
accessToken, err := menu.GetAccessToken()
if err != nil {
return err
}
uri := fmt.Sprintf("%s?access_token=%s", menuDeleteConditionalURL, accessToken)
reqDeleteConditional := &reqDeleteConditional{
MenuID: menuID,
}
response, err := util.PostJSON(uri, reqDeleteConditional)
if err != nil {
return err
}
return util.DecodeWithCommonError(response, "DeleteConditional")
}
//MenuTryMatch 菜单匹配
func (menu *Menu) MenuTryMatch(userID string) (buttons []Button, err error) {
var accessToken string
accessToken, err = menu.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s", menuTryMatchURL, accessToken)
reqMenuTryMatch := &reqMenuTryMatch{userID}
var response []byte
response, err = util.PostJSON(uri, reqMenuTryMatch)
if err != nil {
return
}
var resMenuTryMatch resMenuTryMatch
err = json.Unmarshal(response, &resMenuTryMatch)
if err != nil {
return
}
if resMenuTryMatch.ErrCode != 0 {
err = fmt.Errorf("MenuTryMatch Error , errcode=%d , errmsg=%s", resMenuTryMatch.ErrCode, resMenuTryMatch.ErrMsg)
return
}
buttons = resMenuTryMatch.Button
return
}
//GetCurrentSelfMenuInfo 获取自定义菜单配置接口
func (menu *Menu) GetCurrentSelfMenuInfo() (resSelfMenuInfo ResSelfMenuInfo, err error) {
var accessToken string
accessToken, err = menu.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s", menuSelfMenuInfoURL, accessToken)
var response []byte
response, err = util.HTTPGet(uri)
if err != nil {
return
}
err = json.Unmarshal(response, &resSelfMenuInfo)
if err != nil {
return
}
if resSelfMenuInfo.ErrCode != 0 {
err = fmt.Errorf("GetCurrentSelfMenuInfo Error , errcode=%d , errmsg=%s", resSelfMenuInfo.ErrCode, resSelfMenuInfo.ErrMsg)
return
}
return
}

View File

@@ -0,0 +1,161 @@
package message
import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/officialaccount/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

@@ -0,0 +1,17 @@
package message
//Image 图片消息
type Image struct {
CommonToken
Image struct {
MediaID string `xml:"MediaId"`
} `xml:"Image"`
}
//NewImage 回复图片消息
func NewImage(mediaID string) *Image {
image := new(Image)
image.Image.MediaID = mediaID
return image
}

View File

@@ -0,0 +1,222 @@
package message
import (
"encoding/xml"
"github.com/silenceper/wechat/officialaccount/device"
)
// MsgType 基本消息类型
type MsgType string
// EventType 事件类型
type EventType string
// InfoType 第三方平台授权事件类型
type InfoType string
const (
//MsgTypeText 表示文本消息
MsgTypeText MsgType = "text"
//MsgTypeImage 表示图片消息
MsgTypeImage = "image"
//MsgTypeVoice 表示语音消息
MsgTypeVoice = "voice"
//MsgTypeVideo 表示视频消息
MsgTypeVideo = "video"
//MsgTypeShortVideo 表示短视频消息[限接收]
MsgTypeShortVideo = "shortvideo"
//MsgTypeLocation 表示坐标消息[限接收]
MsgTypeLocation = "location"
//MsgTypeLink 表示链接消息[限接收]
MsgTypeLink = "link"
//MsgTypeMusic 表示音乐消息[限回复]
MsgTypeMusic = "music"
//MsgTypeNews 表示图文消息[限回复]
MsgTypeNews = "news"
//MsgTypeTransfer 表示消息消息转发到客服
MsgTypeTransfer = "transfer_customer_service"
//MsgTypeEvent 表示事件推送消息
MsgTypeEvent = "event"
)
const (
//EventSubscribe 订阅
EventSubscribe EventType = "subscribe"
//EventUnsubscribe 取消订阅
EventUnsubscribe = "unsubscribe"
//EventScan 用户已经关注公众号,则微信会将带场景值扫描事件推送给开发者
EventScan = "SCAN"
//EventLocation 上报地理位置事件
EventLocation = "LOCATION"
//EventClick 点击菜单拉取消息时的事件推送
EventClick = "CLICK"
//EventView 点击菜单跳转链接时的事件推送
EventView = "VIEW"
//EventScancodePush 扫码推事件的事件推送
EventScancodePush = "scancode_push"
//EventScancodeWaitmsg 扫码推事件且弹出“消息接收中”提示框的事件推送
EventScancodeWaitmsg = "scancode_waitmsg"
//EventPicSysphoto 弹出系统拍照发图的事件推送
EventPicSysphoto = "pic_sysphoto"
//EventPicPhotoOrAlbum 弹出拍照或者相册发图的事件推送
EventPicPhotoOrAlbum = "pic_photo_or_album"
//EventPicWeixin 弹出微信相册发图器的事件推送
EventPicWeixin = "pic_weixin"
//EventLocationSelect 弹出地理位置选择器的事件推送
EventLocationSelect = "location_select"
//EventTemplateSendJobFinish 发送模板消息推送通知
EventTemplateSendJobFinish = "TEMPLATESENDJOBFINISH"
//EventWxaMediaCheck 异步校验图片/音频是否含有违法违规内容推送事件
EventWxaMediaCheck = "wxa_media_check"
)
const (
// InfoTypeVerifyTicket 返回ticket
InfoTypeVerifyTicket InfoType = "component_verify_ticket"
// InfoTypeAuthorized 授权
InfoTypeAuthorized = "authorized"
// InfoTypeUnauthorized 取消授权
InfoTypeUnauthorized = "unauthorized"
// InfoTypeUpdateAuthorized 更新授权
InfoTypeUpdateAuthorized = "updateauthorized"
)
//MixMessage 存放所有微信发送过来的消息和事件
type MixMessage struct {
CommonToken
//基本消息
MsgID int64 `xml:"MsgId"`
Content string `xml:"Content"`
Recognition string `xml:"Recognition"`
PicURL string `xml:"PicUrl"`
MediaID string `xml:"MediaId"`
Format string `xml:"Format"`
ThumbMediaID string `xml:"ThumbMediaId"`
LocationX float64 `xml:"Location_X"`
LocationY float64 `xml:"Location_Y"`
Scale float64 `xml:"Scale"`
Label string `xml:"Label"`
Title string `xml:"Title"`
Description string `xml:"Description"`
URL string `xml:"Url"`
//事件相关
Event EventType `xml:"Event"`
EventKey string `xml:"EventKey"`
Ticket string `xml:"Ticket"`
Latitude string `xml:"Latitude"`
Longitude string `xml:"Longitude"`
Precision string `xml:"Precision"`
MenuID string `xml:"MenuId"`
Status string `xml:"Status"`
SessionFrom string `xml:"SessionFrom"`
ScanCodeInfo struct {
ScanType string `xml:"ScanType"`
ScanResult string `xml:"ScanResult"`
} `xml:"ScanCodeInfo"`
SendPicsInfo struct {
Count int32 `xml:"Count"`
PicList []EventPic `xml:"PicList>item"`
} `xml:"SendPicsInfo"`
SendLocationInfo struct {
LocationX float64 `xml:"Location_X"`
LocationY float64 `xml:"Location_Y"`
Scale float64 `xml:"Scale"`
Label string `xml:"Label"`
Poiname string `xml:"Poiname"`
}
// 第三方平台相关
InfoType InfoType `xml:"InfoType"`
AppID string `xml:"AppId"`
ComponentVerifyTicket string `xml:"ComponentVerifyTicket"`
AuthorizerAppid string `xml:"AuthorizerAppid"`
AuthorizationCode string `xml:"AuthorizationCode"`
AuthorizationCodeExpiredTime int64 `xml:"AuthorizationCodeExpiredTime"`
PreAuthCode string `xml:"PreAuthCode"`
// 卡券相关
CardID string `xml:"CardId"`
RefuseReason string `xml:"RefuseReason"`
IsGiveByFriend int32 `xml:"IsGiveByFriend"`
FriendUserName string `xml:"FriendUserName"`
UserCardCode string `xml:"UserCardCode"`
OldUserCardCode string `xml:"OldUserCardCode"`
OuterStr string `xml:"OuterStr"`
IsRestoreMemberCard int32 `xml:"IsRestoreMemberCard"`
UnionID string `xml:"UnionId"`
// 内容审核相关
IsRisky bool `xml:"isrisky"`
ExtraInfoJSON string `xml:"extra_info_json"`
TraceID string `xml:"trace_id"`
StatusCode int `xml:"status_code"`
//设备相关
device.MsgDevice
}
//EventPic 发图事件推送
type EventPic struct {
PicMd5Sum string `xml:"PicMd5Sum"`
}
//EncryptedXMLMsg 安全模式下的消息体
type EncryptedXMLMsg struct {
XMLName struct{} `xml:"xml" json:"-"`
ToUserName string `xml:"ToUserName" json:"ToUserName"`
EncryptedMsg string `xml:"Encrypt" json:"Encrypt"`
}
//ResponseEncryptedXMLMsg 需要返回的消息体
type ResponseEncryptedXMLMsg struct {
XMLName struct{} `xml:"xml" json:"-"`
EncryptedMsg string `xml:"Encrypt" json:"Encrypt"`
MsgSignature string `xml:"MsgSignature" json:"MsgSignature"`
Timestamp int64 `xml:"TimeStamp" json:"TimeStamp"`
Nonce string `xml:"Nonce" json:"Nonce"`
}
// CDATA 使用该类型,在序列化为 xml 文本时文本会被解析器忽略
type CDATA string
// MarshalXML 实现自己的序列化方法
func (c CDATA) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(struct {
string `xml:",cdata"`
}{string(c)}, start)
}
// CommonToken 消息中通用的结构
type CommonToken struct {
XMLName xml.Name `xml:"xml"`
ToUserName CDATA `xml:"ToUserName"`
FromUserName CDATA `xml:"FromUserName"`
CreateTime int64 `xml:"CreateTime"`
MsgType MsgType `xml:"MsgType"`
}
//SetToUserName set ToUserName
func (msg *CommonToken) SetToUserName(toUserName CDATA) {
msg.ToUserName = toUserName
}
//SetFromUserName set FromUserName
func (msg *CommonToken) SetFromUserName(fromUserName CDATA) {
msg.FromUserName = fromUserName
}
//SetCreateTime set createTime
func (msg *CommonToken) SetCreateTime(createTime int64) {
msg.CreateTime = createTime
}
//SetMsgType set MsgType
func (msg *CommonToken) SetMsgType(msgType MsgType) {
msg.MsgType = msgType
}

View File

@@ -0,0 +1,24 @@
package message
//Music 音乐消息
type Music struct {
CommonToken
Music struct {
Title string `xml:"Title" `
Description string `xml:"Description" `
MusicURL string `xml:"MusicUrl" `
HQMusicURL string `xml:"HQMusicUrl" `
ThumbMediaID string `xml:"ThumbMediaId"`
} `xml:"Music"`
}
//NewMusic 回复音乐消息
func NewMusic(title, description, musicURL, hQMusicURL, thumbMediaID string) *Music {
music := new(Music)
music.Music.Title = title
music.Music.Description = description
music.Music.MusicURL = musicURL
music.Music.ThumbMediaID = thumbMediaID
return music
}

View File

@@ -0,0 +1,35 @@
package message
//News 图文消息
type News struct {
CommonToken
ArticleCount int `xml:"ArticleCount"`
Articles []*Article `xml:"Articles>item,omitempty"`
}
//NewNews 初始化图文消息
func NewNews(articles []*Article) *News {
news := new(News)
news.ArticleCount = len(articles)
news.Articles = articles
return news
}
//Article 单篇文章
type Article struct {
Title string `xml:"Title,omitempty"`
Description string `xml:"Description,omitempty"`
PicURL string `xml:"PicUrl,omitempty"`
URL string `xml:"Url,omitempty"`
}
//NewArticle 初始化文章
func NewArticle(title, description, picURL, url string) *Article {
article := new(Article)
article.Title = title
article.Description = description
article.PicURL = picURL
article.URL = url
return article
}

View File

@@ -0,0 +1,24 @@
package message
//TransferCustomer 转发客服消息
type TransferCustomer struct {
CommonToken
TransInfo *TransInfo `xml:"TransInfo,omitempty"`
}
//TransInfo 转发到指定客服
type TransInfo struct {
KfAccount string `xml:"KfAccount"`
}
//NewTransferCustomer 实例化
func NewTransferCustomer(KfAccount string) *TransferCustomer {
tc := new(TransferCustomer)
if KfAccount != "" {
transInfo := new(TransInfo)
transInfo.KfAccount = KfAccount
tc.TransInfo = transInfo
}
return tc
}

View File

@@ -0,0 +1,15 @@
package message
import "errors"
//ErrInvalidReply 无效的回复
var ErrInvalidReply = errors.New("无效的回复消息")
//ErrUnsupportReply 不支持的回复类型
var ErrUnsupportReply = errors.New("不支持的回复消息")
//Reply 消息回复
type Reply struct {
MsgType MsgType
MsgData interface{}
}

View File

@@ -0,0 +1,74 @@
package message
import (
"encoding/json"
"fmt"
"github.com/silenceper/wechat/officialaccount/context"
"github.com/silenceper/wechat/util"
)
const (
templateSendURL = "https://api.weixin.qq.com/cgi-bin/message/template/send"
)
//Template 模板消息
type Template struct {
*context.Context
}
//NewTemplate 实例化
func NewTemplate(context *context.Context) *Template {
tpl := new(Template)
tpl.Context = context
return tpl
}
//Message 发送的模板消息内容
type Message struct {
ToUser string `json:"touser"` // 必须, 接受者OpenID
TemplateID string `json:"template_id"` // 必须, 模版ID
URL string `json:"url,omitempty"` // 可选, 用户点击后跳转的URL, 该URL必须处于开发者在公众平台网站中设置的域中
Color string `json:"color,omitempty"` // 可选, 整个消息的颜色, 可以不设置
Data map[string]*DataItem `json:"data"` // 必须, 模板数据
MiniProgram struct {
AppID string `json:"appid"` //所需跳转到的小程序appid该小程序appid必须与发模板消息的公众号是绑定关联关系
PagePath string `json:"pagepath"` //所需跳转到小程序的具体页面路径,支持带参数,示例index?foo=bar
} `json:"miniprogram"` //可选,跳转至小程序地址
}
//DataItem 模版内某个 .DATA 的值
type DataItem struct {
Value string `json:"value"`
Color string `json:"color,omitempty"`
}
type resTemplateSend struct {
util.CommonError
MsgID int64 `json:"msgid"`
}
//Send 发送模板消息
func (tpl *Template) Send(msg *Message) (msgID int64, err error) {
var accessToken string
accessToken, err = tpl.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf("%s?access_token=%s", templateSendURL, accessToken)
response, err := util.PostJSON(uri, msg)
var result resTemplateSend
err = json.Unmarshal(response, &result)
if err != nil {
return
}
if result.ErrCode != 0 {
err = fmt.Errorf("template msg send error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
return
}
msgID = result.MsgID
return
}

View File

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

View File

@@ -0,0 +1,21 @@
package message
//Video 视频消息
type Video struct {
CommonToken
Video struct {
MediaID string `xml:"MediaId"`
Title string `xml:"Title,omitempty"`
Description string `xml:"Description,omitempty"`
} `xml:"Video"`
}
//NewVideo 回复图片消息
func NewVideo(mediaID, title, description string) *Video {
video := new(Video)
video.Video.MediaID = mediaID
video.Video.Title = title
video.Video.Description = description
return video
}

View File

@@ -0,0 +1,17 @@
package message
//Voice 语音消息
type Voice struct {
CommonToken
Voice struct {
MediaID string `xml:"MediaId"`
} `xml:"Voice"`
}
//NewVoice 回复语音消息
func NewVoice(mediaID string) *Voice {
voice := new(Voice)
voice.Voice.MediaID = mediaID
return voice
}

View File

@@ -0,0 +1,163 @@
package oauth
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/silenceper/wechat/officialaccount/context"
"github.com/silenceper/wechat/util"
)
const (
redirectOauthURL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect"
webAppRedirectOauthURL = "https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect"
accessTokenURL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"
refreshAccessTokenURL = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%s"
userInfoURL = "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN"
checkAccessTokenURL = "https://api.weixin.qq.com/sns/auth?access_token=%s&openid=%s"
)
//Oauth 保存用户授权信息
type Oauth struct {
*context.Context
}
//NewOauth 实例化授权信息
func NewOauth(context *context.Context) *Oauth {
auth := new(Oauth)
auth.Context = context
return auth
}
//GetRedirectURL 获取跳转的url地址
func (oauth *Oauth) GetRedirectURL(redirectURI, scope, state string) (string, error) {
//url encode
urlStr := url.QueryEscape(redirectURI)
return fmt.Sprintf(redirectOauthURL, oauth.AppID, urlStr, scope, state), nil
}
//GetWebAppRedirectURL 获取网页应用跳转的url地址
func (oauth *Oauth) GetWebAppRedirectURL(redirectURI, scope, state string) (string, error) {
urlStr := url.QueryEscape(redirectURI)
return fmt.Sprintf(webAppRedirectOauthURL, oauth.AppID, urlStr, scope, state), nil
}
//Redirect 跳转到网页授权
func (oauth *Oauth) Redirect(writer http.ResponseWriter, req *http.Request, redirectURI, scope, state string) error {
location, err := oauth.GetRedirectURL(redirectURI, scope, state)
if err != nil {
return err
}
http.Redirect(writer, req, location, 302)
return nil
}
// ResAccessToken 获取用户授权access_token的返回结果
type ResAccessToken struct {
util.CommonError
AccessToken string `json:"access_token"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
OpenID string `json:"openid"`
Scope string `json:"scope"`
// UnionID 只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。
// 公众号文档 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842
UnionID string `json:"unionid"`
}
// GetUserAccessToken 通过网页授权的code 换取access_token(区别于context中的access_token)
func (oauth *Oauth) GetUserAccessToken(code string) (result ResAccessToken, err error) {
urlStr := fmt.Sprintf(accessTokenURL, oauth.AppID, oauth.AppSecret, 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("GetUserAccessToken error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
return
}
return
}
//RefreshAccessToken 刷新access_token
func (oauth *Oauth) RefreshAccessToken(refreshToken string) (result ResAccessToken, err error) {
urlStr := fmt.Sprintf(refreshAccessTokenURL, oauth.AppID, refreshToken)
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("GetUserAccessToken error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
return
}
return
}
//CheckAccessToken 检验access_token是否有效
func (oauth *Oauth) CheckAccessToken(accessToken, openID string) (b bool, err error) {
urlStr := fmt.Sprintf(checkAccessTokenURL, accessToken, openID)
var response []byte
response, err = util.HTTPGet(urlStr)
if err != nil {
return
}
var result util.CommonError
err = json.Unmarshal(response, &result)
if err != nil {
return
}
if result.ErrCode != 0 {
b = false
return
}
b = true
return
}
//UserInfo 用户授权获取到用户信息
type UserInfo struct {
util.CommonError
OpenID string `json:"openid"`
Nickname string `json:"nickname"`
Sex int32 `json:"sex"`
Province string `json:"province"`
City string `json:"city"`
Country string `json:"country"`
HeadImgURL string `json:"headimgurl"`
Privilege []string `json:"privilege"`
Unionid string `json:"unionid"`
}
//GetUserInfo 如果scope为 snsapi_userinfo 则可以通过此方法获取到用户基本信息
func (oauth *Oauth) GetUserInfo(accessToken, openID string) (result UserInfo, err error) {
urlStr := fmt.Sprintf(userInfoURL, accessToken, openID)
var response []byte
response, err = util.HTTPGet(urlStr)
if err != nil {
return
}
err = json.Unmarshal(response, &result)
if err != nil {
return
}
if result.ErrCode != 0 {
err = fmt.Errorf("GetUserInfo error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
return
}
return
}

View File

@@ -0,0 +1,94 @@
package officialaccount
import (
"net/http"
"sync"
"github.com/silenceper/wechat/officialaccount/basic"
"github.com/silenceper/wechat/officialaccount/config"
"github.com/silenceper/wechat/officialaccount/context"
"github.com/silenceper/wechat/officialaccount/device"
"github.com/silenceper/wechat/officialaccount/js"
"github.com/silenceper/wechat/officialaccount/material"
"github.com/silenceper/wechat/officialaccount/menu"
"github.com/silenceper/wechat/officialaccount/message"
"github.com/silenceper/wechat/officialaccount/oauth"
"github.com/silenceper/wechat/officialaccount/server"
"github.com/silenceper/wechat/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

@@ -0,0 +1,248 @@
package server
import (
"encoding/xml"
"errors"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"runtime/debug"
"strconv"
"github.com/silenceper/wechat/officialaccount/context"
"github.com/silenceper/wechat/officialaccount/message"
"github.com/silenceper/wechat/util"
)
//Server struct
type Server struct {
*context.Context
Writer http.ResponseWriter
Request *http.Request
debug bool
openID string
messageHandler func(message.MixMessage) *message.Reply
requestRawXMLMsg []byte
requestMsg message.MixMessage
responseRawXMLMsg []byte
responseMsg interface{}
isSafeMode bool
random []byte
nonce string
timestamp int64
}
//NewServer init
func NewServer(context *context.Context) *Server {
srv := new(Server)
srv.Context = context
return srv
}
// SetDebug set debug field
func (srv *Server) SetDebug(debug bool) {
srv.debug = debug
}
//Serve 处理微信的请求消息
func (srv *Server) Serve() error {
if !srv.Validate() {
return fmt.Errorf("请求校验失败")
}
echostr, exists := srv.GetQuery("echostr")
if exists {
srv.String(echostr)
return nil
}
response, err := srv.handleRequest()
if err != nil {
return err
}
//debug
if srv.debug {
fmt.Println("request msg = ", string(srv.requestRawXMLMsg))
}
return srv.buildResponse(response)
}
//Validate 校验请求是否合法
func (srv *Server) Validate() bool {
if srv.debug {
return true
}
timestamp := srv.Query("timestamp")
nonce := srv.Query("nonce")
signature := srv.Query("signature")
return signature == util.Signature(srv.Token, timestamp, nonce)
}
//HandleRequest 处理微信的请求
func (srv *Server) handleRequest() (reply *message.Reply, err error) {
//set isSafeMode
srv.isSafeMode = false
encryptType := srv.Query("encrypt_type")
if encryptType == "aes" {
srv.isSafeMode = true
}
//set openID
srv.openID = srv.Query("openid")
var msg interface{}
msg, err = srv.getMessage()
if err != nil {
return
}
mixMessage, success := msg.(message.MixMessage)
if !success {
err = errors.New("消息类型转换失败")
}
srv.requestMsg = mixMessage
reply = srv.messageHandler(mixMessage)
return
}
//GetOpenID return openID
func (srv *Server) GetOpenID() string {
return srv.openID
}
//getMessage 解析微信返回的消息
func (srv *Server) getMessage() (interface{}, error) {
var rawXMLMsgBytes []byte
var err error
if srv.isSafeMode {
var encryptedXMLMsg message.EncryptedXMLMsg
if err := xml.NewDecoder(srv.Request.Body).Decode(&encryptedXMLMsg); err != nil {
return nil, fmt.Errorf("从body中解析xml失败,err=%v", err)
}
//验证消息签名
timestamp := srv.Query("timestamp")
srv.timestamp, err = strconv.ParseInt(timestamp, 10, 32)
if err != nil {
return nil, err
}
nonce := srv.Query("nonce")
srv.nonce = nonce
msgSignature := srv.Query("msg_signature")
msgSignatureGen := util.Signature(srv.Token, timestamp, nonce, encryptedXMLMsg.EncryptedMsg)
if msgSignature != msgSignatureGen {
return nil, fmt.Errorf("消息不合法,验证签名失败")
}
//解密
srv.random, rawXMLMsgBytes, err = util.DecryptMsg(srv.AppID, encryptedXMLMsg.EncryptedMsg, srv.EncodingAESKey)
if err != nil {
return nil, fmt.Errorf("消息解密失败, err=%v", err)
}
} else {
rawXMLMsgBytes, err = ioutil.ReadAll(srv.Request.Body)
if err != nil {
return nil, fmt.Errorf("从body中解析xml失败, err=%v", err)
}
}
srv.requestRawXMLMsg = rawXMLMsgBytes
return srv.parseRequestMessage(rawXMLMsgBytes)
}
func (srv *Server) parseRequestMessage(rawXMLMsgBytes []byte) (msg message.MixMessage, err error) {
msg = message.MixMessage{}
err = xml.Unmarshal(rawXMLMsgBytes, &msg)
return
}
//SetMessageHandler 设置用户自定义的回调方法
func (srv *Server) SetMessageHandler(handler func(message.MixMessage) *message.Reply) {
srv.messageHandler = handler
}
func (srv *Server) buildResponse(reply *message.Reply) (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("panic error: %v\n%s", e, debug.Stack())
}
}()
if reply == nil {
//do nothing
return nil
}
msgType := reply.MsgType
switch msgType {
case message.MsgTypeText:
case message.MsgTypeImage:
case message.MsgTypeVoice:
case message.MsgTypeVideo:
case message.MsgTypeMusic:
case message.MsgTypeNews:
case message.MsgTypeTransfer:
default:
err = message.ErrUnsupportReply
return
}
msgData := reply.MsgData
value := reflect.ValueOf(msgData)
//msgData must be a ptr
kind := value.Kind().String()
if "ptr" != kind {
return message.ErrUnsupportReply
}
params := make([]reflect.Value, 1)
params[0] = reflect.ValueOf(srv.requestMsg.FromUserName)
value.MethodByName("SetToUserName").Call(params)
params[0] = reflect.ValueOf(srv.requestMsg.ToUserName)
value.MethodByName("SetFromUserName").Call(params)
params[0] = reflect.ValueOf(msgType)
value.MethodByName("SetMsgType").Call(params)
params[0] = reflect.ValueOf(util.GetCurrTs())
value.MethodByName("SetCreateTime").Call(params)
srv.responseMsg = msgData
srv.responseRawXMLMsg, err = xml.Marshal(msgData)
return
}
//Send 将自定义的消息发送
func (srv *Server) Send() (err error) {
replyMsg := srv.responseMsg
if srv.isSafeMode {
//安全模式下对消息进行加密
var encryptedMsg []byte
encryptedMsg, err = util.EncryptMsg(srv.random, srv.responseRawXMLMsg, srv.AppID, srv.EncodingAESKey)
if err != nil {
return
}
//TODO 如果获取不到timestamp nonce 则自己生成
timestamp := srv.timestamp
timestampStr := strconv.FormatInt(timestamp, 10)
msgSignature := util.Signature(srv.Token, timestampStr, srv.nonce, string(encryptedMsg))
replyMsg = message.ResponseEncryptedXMLMsg{
EncryptedMsg: string(encryptedMsg),
MsgSignature: msgSignature,
Timestamp: timestamp,
Nonce: srv.nonce,
}
}
if replyMsg != nil {
srv.XML(replyMsg)
}
return
}

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

@@ -0,0 +1,154 @@
package user
import (
"encoding/json"
"fmt"
"net/url"
"github.com/silenceper/wechat/officialaccount/context"
"github.com/silenceper/wechat/util"
)
const (
userInfoURL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN"
updateRemarkURL = "https://api.weixin.qq.com/cgi-bin/user/info/updateremark?access_token=%s"
userListURL = "https://api.weixin.qq.com/cgi-bin/user/get"
)
//User 用户管理
type User struct {
*context.Context
}
//NewUser 实例化
func NewUser(context *context.Context) *User {
user := new(User)
user.Context = context
return user
}
//Info 用户基本信息
type Info struct {
util.CommonError
Subscribe int32 `json:"subscribe"`
OpenID string `json:"openid"`
Nickname string `json:"nickname"`
Sex int32 `json:"sex"`
City string `json:"city"`
Country string `json:"country"`
Province string `json:"province"`
Language string `json:"language"`
Headimgurl string `json:"headimgurl"`
SubscribeTime int32 `json:"subscribe_time"`
UnionID string `json:"unionid"`
Remark string `json:"remark"`
GroupID int32 `json:"groupid"`
TagidList []int32 `json:"tagid_list"`
SubscribeScene string `json:"subscribe_scene"`
QrScene int `json:"qr_scene"`
QrSceneStr string `json:"qr_scene_str"`
}
// OpenidList 用户列表
type OpenidList struct {
Total int `json:"total"`
Count int `json:"count"`
Data struct {
OpenIDs []string `json:"openid"`
} `json:"data"`
NextOpenID string `json:"next_openid"`
}
//GetUserInfo 获取用户基本信息
func (user *User) GetUserInfo(openID string) (userInfo *Info, err error) {
var accessToken string
accessToken, err = user.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(userInfoURL, accessToken, openID)
var response []byte
response, err = util.HTTPGet(uri)
if err != nil {
return
}
userInfo = new(Info)
err = json.Unmarshal(response, userInfo)
if err != nil {
return
}
if userInfo.ErrCode != 0 {
err = fmt.Errorf("GetUserInfo Error , errcode=%d , errmsg=%s", userInfo.ErrCode, userInfo.ErrMsg)
return
}
return
}
// UpdateRemark 设置用户备注名
func (user *User) UpdateRemark(openID, remark string) (err error) {
var accessToken string
accessToken, err = user.GetAccessToken()
if err != nil {
return
}
uri := fmt.Sprintf(updateRemarkURL, accessToken)
var response []byte
response, err = util.PostJSON(uri, map[string]string{"openid": openID, "remark": remark})
if err != nil {
return
}
return util.DecodeWithCommonError(response, "UpdateRemark")
}
// ListUserOpenIDs 返回用户列表
func (user *User) ListUserOpenIDs(nextOpenid ...string) (*OpenidList, error) {
accessToken, err := user.GetAccessToken()
if err != nil {
return nil, err
}
uri, _ := url.Parse(userListURL)
q := uri.Query()
q.Set("access_token", accessToken)
if len(nextOpenid) > 0 && nextOpenid[0] != "" {
q.Set("next_openid", nextOpenid[0])
}
uri.RawQuery = q.Encode()
response, err := util.HTTPGet(uri.String())
if err != nil {
return nil, err
}
userlist := new(OpenidList)
err = json.Unmarshal(response, userlist)
if err != nil {
return nil, err
}
return userlist, nil
}
// ListAllUserOpenIDs 返回所有用户OpenID列表
func (user *User) ListAllUserOpenIDs() ([]string, error) {
nextOpenid := ""
openids := []string{}
count := 0
for {
ul, err := user.ListUserOpenIDs(nextOpenid)
if err != nil {
return nil, err
}
openids = append(openids, ul.Data.OpenIDs...)
count += ul.Count
if ul.Total > count {
nextOpenid = ul.NextOpenID
} else {
return openids, nil
}
}
}