Skip to content

Commit c2b1222

Browse files
committed
完成微信客服SDK初步编写工作
1 parent 5c158c7 commit c2b1222

File tree

18 files changed

+1318
-0
lines changed

18 files changed

+1318
-0
lines changed

account.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package WeChatCustomerServiceSDK
2+
3+
import (
4+
"WeChatCustomerServiceSDK/util"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
)
9+
10+
const (
11+
//添加客服账号
12+
accountAddAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/account/add?access_token=%s"
13+
// 删除客服账号
14+
accountDelAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/account/del?access_token=%s"
15+
// 修改客服账号
16+
accountUpdateAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/account/update?access_token=%s"
17+
// 获取客服账号列表
18+
accountListAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/account/list?access_token=%s"
19+
//获取客服账号链接
20+
addContactWayAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/add_contact_way?access_token=%s"
21+
)
22+
23+
// AccountAddOptions 添加客服账号请求参数
24+
type AccountAddOptions struct {
25+
Name string `json:"name"` // 客服帐号名称, 不多于16个字符
26+
MediaID string `json:"media_id"` // 客服头像临时素材。可以调用上传临时素材接口获取, 不多于128个字节
27+
}
28+
29+
// AccountAddSchema 添加客服账号响应内容
30+
type AccountAddSchema struct {
31+
BaseModel
32+
OpenKFID string `json:"open_kfid"` // 新创建的客服张号ID
33+
}
34+
35+
// AccountAdd 添加客服账号
36+
func (r *Client) AccountAdd(options AccountAddOptions) (info AccessTokenSchema, err error) {
37+
data, err := util.HttpPost(fmt.Sprintf(accountAddAddr, r.accessToken), options)
38+
if err != nil {
39+
return info, err
40+
}
41+
_ = json.Unmarshal(data, &info)
42+
if info.ErrCode != 0 {
43+
return info, errors.New(info.ErrMsg)
44+
}
45+
return info, nil
46+
}
47+
48+
// AccountDelOptions 删除客服账号请求参数
49+
type AccountDelOptions struct {
50+
OpenKFID string `json:"open_kfid"` // 客服帐号ID, 不多于64字节
51+
}
52+
53+
// AccountDel 删除客服账号
54+
func (r *Client) AccountDel(options AccountDelOptions) (info BaseModel, err error) {
55+
data, err := util.HttpPost(fmt.Sprintf(accountDelAddr, r.accessToken), options)
56+
if err != nil {
57+
return info, err
58+
}
59+
_ = json.Unmarshal(data, &info)
60+
if info.ErrCode != 0 {
61+
return info, errors.New(info.ErrMsg)
62+
}
63+
return info, nil
64+
}
65+
66+
// AccountUpdateOptions 修改客服账号请求参数
67+
type AccountUpdateOptions struct {
68+
OpenKFID string `json:"open_kfid"` // 客服帐号ID, 不多于64字节
69+
Name string `json:"name"` // 客服帐号名称, 不多于16个字符
70+
MediaID string `json:"media_id"` // 客服头像临时素材。可以调用上传临时素材接口获取, 不多于128个字节
71+
}
72+
73+
// AccountUpdate 修复客服账号
74+
func (r *Client) AccountUpdate(options AccountUpdateOptions) (info BaseModel, err error) {
75+
data, err := util.HttpPost(fmt.Sprintf(accountUpdateAddr, r.accessToken), options)
76+
if err != nil {
77+
return info, err
78+
}
79+
_ = json.Unmarshal(data, &info)
80+
if info.ErrCode != 0 {
81+
return info, errors.New(info.ErrMsg)
82+
}
83+
return info, nil
84+
}
85+
86+
// AccountInfoSchema 客服详情
87+
type AccountInfoSchema struct {
88+
OpenKFID string `json:"open_kfid"` // 客服帐号ID, 不多于64字节
89+
Name string `json:"name"` // 客服帐号名称, 不多于16个字符
90+
MediaID string `json:"media_id"` // 客服头像临时素材。可以调用上传临时素材接口获取, 不多于128个字节
91+
}
92+
93+
// AccountListSchema 获取客服账号列表响应内容
94+
type AccountListSchema struct {
95+
BaseModel
96+
AccountList []AccountInfoSchema `json:"account_list"` // 客服账号列表
97+
}
98+
99+
// AccountList 获取客服账号列表
100+
func (r *Client) AccountList() (info AccountListSchema, err error) {
101+
data, err := util.HttpGet(fmt.Sprintf(accountListAddr, r.accessToken))
102+
if err != nil {
103+
return info, err
104+
}
105+
_ = json.Unmarshal(data, &info)
106+
if info.ErrCode != 0 {
107+
return info, errors.New(info.ErrMsg)
108+
}
109+
return info, nil
110+
}
111+
112+
// AddContactWayOptions 获取客服账号链接
113+
type AddContactWayOptions struct {
114+
OpenKFID string `json:"open_kfid"` // 客服帐号ID, 不多于64字节
115+
Scene string `json:"scene"` // 场景值,字符串类型,由开发者自定义, 不多于32字节, 字符串取值范围(正则表达式):[0-9a-zA-Z_-]*
116+
}
117+
118+
// AddContactWaySchema 获取客服账号链接响应内容
119+
type AddContactWaySchema struct {
120+
BaseModel
121+
URL string `json:"url"` // 客服链接,开发者可将该链接嵌入到H5页面中,用户点击链接即可向对应的微信客服帐号发起咨询。开发者也可根据该url自行生成需要的二维码图片
122+
}
123+
124+
// AddContactWay 获取客服账号链接
125+
func (r *Client) AddContactWay(options AddContactWayOptions) (info AddContactWaySchema, err error) {
126+
data, err := util.HttpPost(fmt.Sprintf(addContactWayAddr, r.accessToken), options)
127+
if err != nil {
128+
return info, err
129+
}
130+
_ = json.Unmarshal(data, &info)
131+
if info.ErrCode != 0 {
132+
return info, errors.New(info.ErrMsg)
133+
}
134+
return info, nil
135+
}

cache/cache.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package cache
2+
3+
import "time"
4+
5+
type Cache interface {
6+
Set(k, v string, expires time.Duration) error
7+
Get(k string) (string, error)
8+
}

cache/redis.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package cache
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/go-redis/redis/v8"
7+
"sync"
8+
"time"
9+
)
10+
11+
const GlobalEvent = "global_event"
12+
13+
type Redis struct {
14+
//订阅服务器实例
15+
Point *redis.Client
16+
//订阅列表
17+
PbFns sync.Map
18+
//读写锁
19+
lock sync.Mutex
20+
}
21+
22+
type RedisOptions struct {
23+
Addr string
24+
Password string
25+
DB int
26+
}
27+
28+
func NewRedis(options RedisOptions) *Redis {
29+
ctx := context.TODO()
30+
instance := Redis{}
31+
//实例化连接池,解决每次重新连接效率低的问题
32+
instance.Point = redis.NewClient(&redis.Options{
33+
Addr: options.Addr,
34+
Password: options.Password,
35+
DB: options.DB,
36+
})
37+
38+
instance.PbFns = sync.Map{}
39+
go func() {
40+
pubSub := instance.Point.Subscribe(ctx, "__keyevent@0__:expired")
41+
for {
42+
msg, err := pubSub.ReceiveMessage(ctx)
43+
if err != nil {
44+
fmt.Println(err)
45+
return
46+
}
47+
if msg.Channel == "__keyevent@0__:expired" {
48+
pbFnList, _ := instance.PbFns.Load(msg.Payload)
49+
if pbFnList != nil {
50+
cbList, ok := pbFnList.([]func(message string))
51+
if ok {
52+
for _, cb := range cbList{
53+
cb(msg.Payload)
54+
}
55+
}
56+
}
57+
//处理全局订阅回调
58+
globalFnList, _ := instance.PbFns.Load(GlobalEvent)
59+
if globalFnList != nil {
60+
cbList, ok := globalFnList.([]func(message string))
61+
if ok {
62+
for _, cb := range cbList{
63+
cb(msg.Payload)
64+
}
65+
}
66+
}
67+
}
68+
}
69+
}()
70+
71+
return &instance
72+
}
73+
74+
// GetOriginPoint 获取原始redis实例
75+
func (r *Redis) GetOriginPoint() *redis.Client {
76+
return r.Point
77+
}
78+
79+
// Subscribe 订阅指定键过期时间,需要redis开启键空间消息通知:config set notify-keyspace-events Ex
80+
func (r *Redis) Subscribe(k string, pb func(message string)) {
81+
var cbList []func(message string)
82+
pbFnList, ok := r.PbFns.Load(k)
83+
if ok {
84+
cbList, ok = pbFnList.([]func(message string))
85+
if ok {
86+
r.lock.Lock()
87+
cbList = append(cbList, pb)
88+
r.lock.Unlock()
89+
}
90+
} else {
91+
cbList = []func(message string){pb}
92+
}
93+
94+
r.PbFns.Store(k, cbList)
95+
}
96+
97+
// SubscribeAllEvents 订阅所有键过期事件
98+
func (r* Redis) SubscribeAllEvents(pb func(message string)) {
99+
var cbList []func(message string)
100+
pbFnList, ok := r.PbFns.Load(GlobalEvent)
101+
if ok {
102+
cbList, ok = pbFnList.([]func(message string))
103+
if ok {
104+
r.lock.Lock()
105+
cbList = append(cbList, pb)
106+
r.lock.Unlock()
107+
}
108+
} else {
109+
cbList = []func(message string){pb}
110+
}
111+
112+
r.PbFns.Store(GlobalEvent, cbList)
113+
}
114+
115+
func (r *Redis) Set(k, v string, expires time.Duration) error {
116+
return r.Point.Set(context.TODO(), k, v, expires * time.Second).Err()
117+
}
118+
119+
func (r *Redis) Get(k string) (string, error) {
120+
con, err := r.Point.Get(context.TODO(), k).Result()
121+
if err != nil && err != redis.Nil {
122+
return "", err
123+
}
124+
return con, nil
125+
}

client.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package WeChatCustomerServiceSDK
2+
3+
import (
4+
"WeChatCustomerServiceSDK/cache"
5+
"errors"
6+
"sync"
7+
"time"
8+
)
9+
10+
// BaseModel 基础数据
11+
type BaseModel struct {
12+
ErrCode int `json:"errcode"` // 出错返回码,为0表示成功,非0表示调用失败
13+
ErrMsg string `json:"errmsg"` // 返回码提示语
14+
}
15+
16+
// Options 微信客服初始化参数
17+
type Options struct {
18+
CorpID string // 企业ID:企业开通的每个微信客服,都对应唯一的企业ID,企业可在微信客服管理后台的企业信息处查看
19+
Secret string // Secret是微信客服用于校验开发者身份的访问密钥,企业成功注册微信客服后,可在「微信客服管理后台-开发配置」处获取
20+
Token string // 用于生成签名校验回调请求的合法性
21+
EncodingAESKey string // 回调消息加解密参数是AES密钥的Base64编码,用于解密回调消息内容对应的密文
22+
Cache cache.Cache // 数据缓存
23+
ExpireTime time.Duration // 令牌过期时间
24+
}
25+
26+
// Client 微信客服实例
27+
type Client struct {
28+
corpID string // 企业ID:企业开通的每个微信客服,都对应唯一的企业ID,企业可在微信客服管理后台的企业信息处查看
29+
secret string // Secret是微信客服用于校验开发者身份的访问密钥,企业成功注册微信客服后,可在「微信客服管理后台-开发配置」处获取
30+
token string // 用于生成签名校验回调请求的合法性
31+
encodingAESKey string // 回调消息加解密参数是AES密钥的Base64编码,用于解密回调消息内容对应的密文
32+
expireTime time.Duration // 令牌过期时间
33+
cache cache.Cache
34+
eventQueue sync.Map //事件队列
35+
mutex sync.Mutex
36+
accessToken string // 用户访问凭证
37+
}
38+
39+
// New 初始化微信客服实例
40+
func New(options Options) (client *Client, err error) {
41+
if options.Cache == nil {
42+
return nil, errors.New("the cache is nil, please set the cache first")
43+
}
44+
45+
if options.ExpireTime == 0 {
46+
options.ExpireTime = 6000
47+
}
48+
49+
client = &Client{
50+
corpID: options.CorpID,
51+
secret: options.Secret,
52+
token: options.Token,
53+
encodingAESKey: options.EncodingAESKey,
54+
expireTime: options.ExpireTime,
55+
cache: options.Cache,
56+
eventQueue: sync.Map{},
57+
mutex: sync.Mutex{},
58+
}
59+
60+
//判断是否已初始化完成,如果己初始化则直接返回当前实例
61+
token, err := client.getAccessToken()
62+
if err != nil {
63+
return nil, errors.New("cache unavailable")
64+
}
65+
66+
if token == "" {
67+
//初始化AccessToken
68+
tokenInfo, err := client.GetAccessToken()
69+
if err != nil {
70+
return nil, err
71+
}
72+
73+
if err = client.setAccessToken(tokenInfo.AccessToken); err != nil {
74+
return nil, err
75+
}
76+
client.accessToken = tokenInfo.AccessToken
77+
} else {
78+
client.accessToken = token
79+
}
80+
81+
return client, nil
82+
}

crypto.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package WeChatCustomerServiceSDK
2+
3+
import (
4+
"WeChatCustomerServiceSDK/crypto"
5+
"errors"
6+
)
7+
8+
// CryptoOptions 微信服务器验证参数
9+
type CryptoOptions struct {
10+
Signature string `form:"msg_signature"`
11+
TimeStamp string `form:"timestamp"`
12+
Nonce string `form:"nonce"`
13+
EchoStr string `form:"echostr"`
14+
}
15+
16+
// VerifyURL 验证请求参数是否合法
17+
func (r *Client) VerifyURL(options CryptoOptions) (string, error) {
18+
wxCpt := crypto.NewWXBizMsgCrypt(r.token, r.encodingAESKey, r.corpID, crypto.XmlType)
19+
data, err := wxCpt.VerifyURL(options.Signature, options.TimeStamp, options.Nonce, options.EchoStr)
20+
if err != nil {
21+
return "", errors.New(err.ErrMsg)
22+
}
23+
return string(data), nil
24+
}
25+
26+
// DecryptMsg 解密消息
27+
func (r *Client) DecryptMsg(options CryptoOptions, postData []byte) ([]byte, error) {
28+
wxCpt := crypto.NewWXBizMsgCrypt(r.token, r.encodingAESKey, r.corpID, crypto.XmlType)
29+
message, status := wxCpt.DecryptMsg(options.Signature, options.TimeStamp, options.Nonce, postData)
30+
if status != nil && status.ErrCode != 0 {
31+
return nil, errors.New(status.ErrMsg)
32+
}
33+
return message, nil
34+
}

0 commit comments

Comments
 (0)