mirror of
https://github.com/silenceper/wechat.git
synced 2026-02-07 22:22:28 +08:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c021336a3c | ||
|
|
9294950ab5 | ||
|
|
d776f5c400 | ||
|
|
bc9f483c8e | ||
|
|
d3d91b8d29 | ||
|
|
96c1f98944 | ||
|
|
47adf42208 | ||
|
|
39ed108b11 | ||
|
|
f767b72872 | ||
|
|
d3f1a83d46 | ||
|
|
db205405ee | ||
|
|
e1ef3ea160 | ||
|
|
b9f0e8368d | ||
|
|
9335b13807 | ||
|
|
3afe499e70 | ||
|
|
beb2f9506d | ||
|
|
b535cd116a | ||
|
|
3cfa9e6c71 | ||
|
|
80d91d8316 | ||
|
|
c61154105b |
4
.github/workflows/go.yml
vendored
4
.github/workflows/go.yml
vendored
@@ -2,9 +2,9 @@ name: Go
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master,release-* ]
|
branches: [ master,release-*,v2 ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master,release-* ]
|
branches: [ master,release-*,v2 ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
golangci:
|
golangci:
|
||||||
|
|||||||
9
cache/redis.go
vendored
9
cache/redis.go
vendored
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/gomodule/redigo/redis"
|
"github.com/gomodule/redigo/redis"
|
||||||
)
|
)
|
||||||
|
|
||||||
//Redis redis cache
|
// Redis .redis cache
|
||||||
type Redis struct {
|
type Redis struct {
|
||||||
conn *redis.Pool
|
conn *redis.Pool
|
||||||
}
|
}
|
||||||
@@ -23,16 +23,17 @@ type RedisOpts struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewRedis 实例化
|
// NewRedis 实例化
|
||||||
func NewRedis(opts *RedisOpts) *Redis {
|
func NewRedis(opts *RedisOpts, dialOpts ...redis.DialOption) *Redis {
|
||||||
pool := &redis.Pool{
|
pool := &redis.Pool{
|
||||||
MaxActive: opts.MaxActive,
|
MaxActive: opts.MaxActive,
|
||||||
MaxIdle: opts.MaxIdle,
|
MaxIdle: opts.MaxIdle,
|
||||||
IdleTimeout: time.Second * time.Duration(opts.IdleTimeout),
|
IdleTimeout: time.Second * time.Duration(opts.IdleTimeout),
|
||||||
Dial: func() (redis.Conn, error) {
|
Dial: func() (redis.Conn, error) {
|
||||||
return redis.Dial("tcp", opts.Host,
|
dialOpts = append(dialOpts, []redis.DialOption{
|
||||||
redis.DialDatabase(opts.Database),
|
redis.DialDatabase(opts.Database),
|
||||||
redis.DialPassword(opts.Password),
|
redis.DialPassword(opts.Password),
|
||||||
)
|
}...)
|
||||||
|
return redis.Dial("tcp", opts.Host, dialOpts...)
|
||||||
},
|
},
|
||||||
TestOnBorrow: func(conn redis.Conn, t time.Time) error {
|
TestOnBorrow: func(conn redis.Conn, t time.Time) error {
|
||||||
if time.Since(t) < time.Minute {
|
if time.Since(t) < time.Minute {
|
||||||
|
|||||||
@@ -56,15 +56,19 @@ type ResAccessToken struct {
|
|||||||
|
|
||||||
// GetAccessToken 获取access_token,先从cache中获取,没有则从服务端获取
|
// GetAccessToken 获取access_token,先从cache中获取,没有则从服务端获取
|
||||||
func (ak *DefaultAccessToken) GetAccessToken() (accessToken string, err error) {
|
func (ak *DefaultAccessToken) GetAccessToken() (accessToken string, err error) {
|
||||||
|
// 先从cache中取
|
||||||
|
accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", ak.cacheKeyPrefix, ak.appID)
|
||||||
|
if val := ak.cache.Get(accessTokenCacheKey); val != nil {
|
||||||
|
return val.(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
// 加上lock,是为了防止在并发获取token时,cache刚好失效,导致从微信服务器上获取到不同token
|
// 加上lock,是为了防止在并发获取token时,cache刚好失效,导致从微信服务器上获取到不同token
|
||||||
ak.accessTokenLock.Lock()
|
ak.accessTokenLock.Lock()
|
||||||
defer ak.accessTokenLock.Unlock()
|
defer ak.accessTokenLock.Unlock()
|
||||||
|
|
||||||
accessTokenCacheKey := fmt.Sprintf("%s_access_token_%s", ak.cacheKeyPrefix, ak.appID)
|
// 双检,防止重复从微信服务器获取
|
||||||
val := ak.cache.Get(accessTokenCacheKey)
|
if val := ak.cache.Get(accessTokenCacheKey); val != nil {
|
||||||
if val != nil {
|
return val.(string), nil
|
||||||
accessToken = val.(string)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// cache失效,从微信服务器获取
|
// cache失效,从微信服务器获取
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"gopkg.in/h2non/gock.v1"
|
"gopkg.in/h2non/gock.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TestGetTicketFromServer .
|
||||||
func TestGetTicketFromServer(t *testing.T) {
|
func TestGetTicketFromServer(t *testing.T) {
|
||||||
defer gock.Off()
|
defer gock.Off()
|
||||||
gock.New(getTicketURL).Reply(200).JSON(&ResTicket{Ticket: "mock-ticket", ExpiresIn: 10})
|
gock.New(getTicketURL).Reply(200).JSON(&ResTicket{Ticket: "mock-ticket", ExpiresIn: 10})
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/silenceper/wechat/v2/util"
|
"github.com/silenceper/wechat/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
//获取ticket的url
|
// getTicketURL 获取ticket的url
|
||||||
const getTicketURL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi"
|
const getTicketURL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi"
|
||||||
|
|
||||||
// DefaultJsTicket 默认获取js ticket方法
|
// DefaultJsTicket 默认获取js ticket方法
|
||||||
@@ -42,16 +42,20 @@ type ResTicket struct {
|
|||||||
|
|
||||||
// GetTicket 获取jsapi_ticket
|
// GetTicket 获取jsapi_ticket
|
||||||
func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err error) {
|
func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err error) {
|
||||||
|
// 先从cache中取
|
||||||
|
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", js.cacheKeyPrefix, js.appID)
|
||||||
|
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
|
||||||
|
return val.(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
js.jsAPITicketLock.Lock()
|
js.jsAPITicketLock.Lock()
|
||||||
defer js.jsAPITicketLock.Unlock()
|
defer js.jsAPITicketLock.Unlock()
|
||||||
|
|
||||||
//先从cache中取
|
// 双检,防止重复从微信服务器获取
|
||||||
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", js.cacheKeyPrefix, js.appID)
|
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
|
||||||
val := js.cache.Get(jsAPITicketCacheKey)
|
return val.(string), nil
|
||||||
if val != nil {
|
|
||||||
ticketStr = val.(string)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var ticket ResTicket
|
var ticket ResTicket
|
||||||
ticket, err = GetTicketFromServer(accessToken)
|
ticket, err = GetTicketFromServer(accessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -5,7 +5,7 @@ go 1.14
|
|||||||
require (
|
require (
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
|
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
|
||||||
github.com/fatih/structs v1.1.0
|
github.com/fatih/structs v1.1.0
|
||||||
github.com/gomodule/redigo v1.8.4
|
github.com/gomodule/redigo v1.8.5
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||||
github.com/sirupsen/logrus v1.8.1
|
github.com/sirupsen/logrus v1.8.1
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -6,8 +6,8 @@ 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/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 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
github.com/gomodule/redigo v1.8.4 h1:Z5JUg94HMTR1XpwBaSH4vq3+PNSIykBLxMdglbw10gg=
|
github.com/gomodule/redigo v1.8.5 h1:nRAxCa+SVsyjSBrtZmG/cqb6VbTmuRzpg/PoTFlpumc=
|
||||||
github.com/gomodule/redigo v1.8.4/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
|
github.com/gomodule/redigo v1.8.5/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
|
||||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
code2SessionURL = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
|
code2SessionURL = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
|
||||||
|
|
||||||
|
checkEncryptedDataURL = "https://api.weixin.qq.com/wxa/business/checkencryptedmsg?access_token=%s"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Auth 登录/用户信息
|
// Auth 登录/用户信息
|
||||||
@@ -31,16 +33,21 @@ type ResCode2Session struct {
|
|||||||
UnionID string `json:"unionid"` // 用户在开放平台的唯一标识符,在满足UnionID下发条件的情况下会返回
|
UnionID string `json:"unionid"` // 用户在开放平台的唯一标识符,在满足UnionID下发条件的情况下会返回
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RspCheckEncryptedData .
|
||||||
|
type RspCheckEncryptedData struct {
|
||||||
|
util.CommonError
|
||||||
|
|
||||||
|
Vaild bool `json:"vaild"` // 是否是合法的数据
|
||||||
|
CreateTime uint `json:"create_time"` // 加密数据生成的时间戳
|
||||||
|
}
|
||||||
|
|
||||||
// Code2Session 登录凭证校验。
|
// Code2Session 登录凭证校验。
|
||||||
func (auth *Auth) Code2Session(jsCode string) (result ResCode2Session, err error) {
|
func (auth *Auth) Code2Session(jsCode string) (result ResCode2Session, err error) {
|
||||||
urlStr := fmt.Sprintf(code2SessionURL, auth.AppID, auth.AppSecret, jsCode)
|
|
||||||
var response []byte
|
var response []byte
|
||||||
response, err = util.HTTPGet(urlStr)
|
if response, err = util.HTTPGet(fmt.Sprintf(code2SessionURL, auth.AppID, auth.AppSecret, jsCode)); err != nil {
|
||||||
if err != nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(response, &result)
|
if err = json.Unmarshal(response, &result); err != nil {
|
||||||
if err != nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if result.ErrCode != 0 {
|
if result.ErrCode != 0 {
|
||||||
@@ -54,3 +61,21 @@ func (auth *Auth) Code2Session(jsCode string) (result ResCode2Session, err error
|
|||||||
func (auth *Auth) GetPaidUnionID() {
|
func (auth *Auth) GetPaidUnionID() {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckEncryptedData .检查加密信息是否由微信生成(当前只支持手机号加密数据),只能检测最近3天生成的加密数据
|
||||||
|
func (auth *Auth) CheckEncryptedData(encryptedMsgHash string) (result RspCheckEncryptedData, err error) {
|
||||||
|
var response []byte
|
||||||
|
var (
|
||||||
|
at string
|
||||||
|
)
|
||||||
|
if at, err = auth.GetAccessToken(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if response, err = util.HTTPPost(fmt.Sprintf(checkEncryptedDataURL, at), "encrypted_msg_hash="+encryptedMsgHash); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = util.DecodeWithError(response, &result, "CheckEncryptedDataAuth"); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"github.com/silenceper/wechat/v2/cache"
|
"github.com/silenceper/wechat/v2/cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config config for 小程序
|
// Config .config for 小程序
|
||||||
type Config struct {
|
type Config struct {
|
||||||
AppID string `json:"app_id"` // appid
|
AppID string `json:"app_id"` // appid
|
||||||
AppSecret string `json:"app_secret"` // appsecret
|
AppSecret string `json:"app_secret"` // appsecret
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import (
|
|||||||
"github.com/silenceper/wechat/v2/miniprogram/encryptor"
|
"github.com/silenceper/wechat/v2/miniprogram/encryptor"
|
||||||
"github.com/silenceper/wechat/v2/miniprogram/message"
|
"github.com/silenceper/wechat/v2/miniprogram/message"
|
||||||
"github.com/silenceper/wechat/v2/miniprogram/qrcode"
|
"github.com/silenceper/wechat/v2/miniprogram/qrcode"
|
||||||
|
"github.com/silenceper/wechat/v2/miniprogram/shortlink"
|
||||||
"github.com/silenceper/wechat/v2/miniprogram/subscribe"
|
"github.com/silenceper/wechat/v2/miniprogram/subscribe"
|
||||||
"github.com/silenceper/wechat/v2/miniprogram/tcb"
|
"github.com/silenceper/wechat/v2/miniprogram/tcb"
|
||||||
|
"github.com/silenceper/wechat/v2/miniprogram/urllink"
|
||||||
"github.com/silenceper/wechat/v2/miniprogram/werun"
|
"github.com/silenceper/wechat/v2/miniprogram/werun"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -84,3 +86,13 @@ func (miniProgram *MiniProgram) GetWeRun() *werun.WeRun {
|
|||||||
func (miniProgram *MiniProgram) GetContentSecurity() *content.Content {
|
func (miniProgram *MiniProgram) GetContentSecurity() *content.Content {
|
||||||
return content.NewContent(miniProgram.ctx)
|
return content.NewContent(miniProgram.ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetURLLink 小程序URL Link接口
|
||||||
|
func (miniProgram *MiniProgram) GetURLLink() *urllink.URLLink {
|
||||||
|
return urllink.NewURLLink(miniProgram.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetShortLink 小程序短链接口
|
||||||
|
func (miniProgram *MiniProgram) GetShortLink() *shortlink.ShortLink {
|
||||||
|
return shortlink.NewShortLink(miniProgram.ctx)
|
||||||
|
}
|
||||||
|
|||||||
86
miniprogram/shortlink/shortlink.go
Normal file
86
miniprogram/shortlink/shortlink.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package shortlink
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
generateShortLinkURL = "https://api.weixin.qq.com/wxa/genwxashortlink?access_token=%s"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShortLink 短链接
|
||||||
|
type ShortLink struct {
|
||||||
|
*context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewShortLink 实例
|
||||||
|
func NewShortLink(ctx *context.Context) *ShortLink {
|
||||||
|
return &ShortLink{ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShortLinker 请求结构体
|
||||||
|
type ShortLinker struct {
|
||||||
|
|
||||||
|
// pageUrl 通过 Short Link 进入的小程序页面路径,必须是已经发布的小程序存在的页面,可携带 query,最大1024个字符
|
||||||
|
PageURL string `json:"page_url"`
|
||||||
|
|
||||||
|
// pageTitle 页面标题,不能包含违法信息,超过20字符会用... 截断代替
|
||||||
|
PageTitle string `json:"page_title"`
|
||||||
|
|
||||||
|
// isPermanent 生成的 Short Link 类型,短期有效:false,永久有效:true
|
||||||
|
IsPermanent bool `json:"is_permanent,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// resShortLinker 返回结构体
|
||||||
|
type resShortLinker struct {
|
||||||
|
// 通用错误
|
||||||
|
*util.CommonError
|
||||||
|
|
||||||
|
// 返回的 shortLink
|
||||||
|
Link string `json:"link"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate 生成 shortLink
|
||||||
|
func (shortLink *ShortLink) generate(shortLinkParams ShortLinker) (string, error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err := shortLink.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
urlStr := fmt.Sprintf(generateShortLinkURL, accessToken)
|
||||||
|
response, err := util.PostJSON(urlStr, shortLinkParams)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用通用方法返回错误
|
||||||
|
var res resShortLinker
|
||||||
|
err = util.DecodeWithError(response, &res, "GenerateShortLink")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Link, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateShortLinkPermanent 生成永久shortLink
|
||||||
|
func (shortLink *ShortLink) GenerateShortLinkPermanent(PageURL, pageTitle string) (string, error) {
|
||||||
|
return shortLink.generate(ShortLinker{
|
||||||
|
PageURL: PageURL,
|
||||||
|
PageTitle: pageTitle,
|
||||||
|
IsPermanent: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateShortLinkTemp 生成临时shortLink
|
||||||
|
func (shortLink *ShortLink) GenerateShortLinkTemp(PageURL, pageTitle string) (string, error) {
|
||||||
|
return shortLink.generate(ShortLinker{
|
||||||
|
PageURL: PageURL,
|
||||||
|
PageTitle: pageTitle,
|
||||||
|
IsPermanent: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -16,6 +16,14 @@ const (
|
|||||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.getTemplateList.html
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.getTemplateList.html
|
||||||
getTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/gettemplate"
|
getTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/gettemplate"
|
||||||
|
|
||||||
|
// 添加订阅模板
|
||||||
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.addTemplate.html
|
||||||
|
addTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/addtemplate"
|
||||||
|
|
||||||
|
// 删除私有模板
|
||||||
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.deleteTemplate.html
|
||||||
|
delTemplateURL = "https://api.weixin.qq.com/wxaapi/newtmpl/deltemplate"
|
||||||
|
|
||||||
// 统一服务消息
|
// 统一服务消息
|
||||||
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/uniform-message/uniformMessage.send.html
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/uniform-message/uniformMessage.send.html
|
||||||
uniformMessageSend = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send"
|
uniformMessageSend = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send"
|
||||||
@@ -133,3 +141,55 @@ func (s *Subscribe) UniformSend(msg *UniformMessage) (err error) {
|
|||||||
}
|
}
|
||||||
return util.DecodeWithCommonError(response, "UniformSend")
|
return util.DecodeWithCommonError(response, "UniformSend")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type resSubscribeAdd struct {
|
||||||
|
util.CommonError
|
||||||
|
|
||||||
|
TemplateID string `json:"priTmplId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add 添加订阅消息模板
|
||||||
|
func (s *Subscribe) Add(ShortID string, kidList []int, sceneDesc string) (templateID string, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = s.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var msg = struct {
|
||||||
|
TemplateIDShort string `json:"tid"`
|
||||||
|
SceneDesc string `json:"sceneDesc"`
|
||||||
|
KidList []int `json:"kidList"`
|
||||||
|
}{TemplateIDShort: ShortID, SceneDesc: sceneDesc, KidList: kidList}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s", addTemplateURL, accessToken)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, msg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var result resSubscribeAdd
|
||||||
|
err = util.DecodeWithError(response, &result, "AddSubscribe")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
templateID = result.TemplateID
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除私有模板
|
||||||
|
func (s *Subscribe) Delete(templateID string) (err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = s.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var msg = struct {
|
||||||
|
TemplateID string `json:"priTmplId"`
|
||||||
|
}{TemplateID: templateID}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s", delTemplateURL, accessToken)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, msg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return util.DecodeWithCommonError(response, "DeleteSubscribe")
|
||||||
|
}
|
||||||
|
|||||||
70
miniprogram/urllink/urllink.go
Normal file
70
miniprogram/urllink/urllink.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package urllink
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/miniprogram/context"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// URLLink 小程序 URL Link
|
||||||
|
type URLLink struct {
|
||||||
|
*context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewURLLink 实例化
|
||||||
|
func NewURLLink(ctx *context.Context) *URLLink {
|
||||||
|
return &URLLink{Context: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateURL = "https://api.weixin.qq.com/wxa/generate_urllink"
|
||||||
|
|
||||||
|
// TExpireType 失效类型 (指定时间戳/指定间隔)
|
||||||
|
type TExpireType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ExpireTypeTime 指定时间戳后失效
|
||||||
|
ExpireTypeTime TExpireType = 0
|
||||||
|
|
||||||
|
// ExpireTypeInterval 间隔指定天数后失效
|
||||||
|
ExpireTypeInterval TExpireType = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// ULParams 请求参数
|
||||||
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-link/urllink.generate.html#请求参数
|
||||||
|
type ULParams struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Query string `json:"query"`
|
||||||
|
IsExpire bool `json:"is_expire"`
|
||||||
|
ExpireType TExpireType `json:"expire_type"`
|
||||||
|
ExpireTime int64 `json:"expire_time"`
|
||||||
|
ExpireInterval int `json:"expire_interval"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ULResult 返回的结果
|
||||||
|
// https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-link/urllink.generate.html#返回值
|
||||||
|
type ULResult struct {
|
||||||
|
util.CommonError
|
||||||
|
|
||||||
|
URLLink string `json:"url_link"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate 生成url link
|
||||||
|
func (u *URLLink) Generate(params *ULParams) (string, error) {
|
||||||
|
accessToken, err := u.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s", generateURL, accessToken)
|
||||||
|
response, err := util.PostJSON(uri, params)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var resp ULResult
|
||||||
|
err = util.DecodeWithError(response, &resp, "URLLink.Generate")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return resp.URLLink, nil
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"github.com/silenceper/wechat/v2/cache"
|
"github.com/silenceper/wechat/v2/cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config config for 微信公众号
|
// Config .config for 微信公众号
|
||||||
type Config struct {
|
type Config struct {
|
||||||
AppID string `json:"app_id"` // appid
|
AppID string `json:"app_id"` // appid
|
||||||
AppSecret string `json:"app_secret"` // appsecret
|
AppSecret string `json:"app_secret"` // appsecret
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
const (
|
const (
|
||||||
subscribeSendURL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/bizsend"
|
subscribeSendURL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/bizsend"
|
||||||
subscribeTemplateListURL = "https://api.weixin.qq.com/wxaapi/newtmpl/gettemplate"
|
subscribeTemplateListURL = "https://api.weixin.qq.com/wxaapi/newtmpl/gettemplate"
|
||||||
|
subscribeTemplateAddURL = "https://api.weixin.qq.com/wxaapi/newtmpl/addtemplate"
|
||||||
|
subscribeTemplateDelURL = "https://api.weixin.qq.com/wxaapi/newtmpl/deltemplate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Subscribe 订阅消息
|
// Subscribe 订阅消息
|
||||||
@@ -84,10 +86,62 @@ func (tpl *Subscribe) List() (templateList []*PrivateSubscribeItem, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var res resPrivateSubscribeList
|
var res resPrivateSubscribeList
|
||||||
err = util.DecodeWithError(response, &res, "ListSubscription")
|
err = util.DecodeWithError(response, &res, "ListSubscribe")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
templateList = res.SubscriptionList
|
templateList = res.SubscriptionList
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type resSubscribeAdd struct {
|
||||||
|
util.CommonError
|
||||||
|
|
||||||
|
TemplateID string `json:"priTmplId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add 添加订阅消息模板
|
||||||
|
func (tpl *Subscribe) Add(ShortID string, kidList []int, sceneDesc string) (templateID string, err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = tpl.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var msg = struct {
|
||||||
|
TemplateIDShort string `json:"tid"`
|
||||||
|
SceneDesc string `json:"sceneDesc"`
|
||||||
|
KidList []int `json:"kidList"`
|
||||||
|
}{TemplateIDShort: ShortID, SceneDesc: sceneDesc, KidList: kidList}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s", subscribeTemplateAddURL, accessToken)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, msg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var result resSubscribeAdd
|
||||||
|
err = util.DecodeWithError(response, &result, "AddSubscribe")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
templateID = result.TemplateID
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除私有模板
|
||||||
|
func (tpl *Subscribe) Delete(templateID string) (err error) {
|
||||||
|
var accessToken string
|
||||||
|
accessToken, err = tpl.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var msg = struct {
|
||||||
|
TemplateID string `json:"priTmplId"`
|
||||||
|
}{TemplateID: templateID}
|
||||||
|
uri := fmt.Sprintf("%s?access_token=%s", subscribeTemplateDelURL, accessToken)
|
||||||
|
var response []byte
|
||||||
|
response, err = util.PostJSON(uri, msg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return util.DecodeWithCommonError(response, "DeleteSubscribe")
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"github.com/silenceper/wechat/v2/cache"
|
"github.com/silenceper/wechat/v2/cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
//Config config for 微信开放平台
|
// Config .config for 微信开放平台
|
||||||
type Config struct {
|
type Config struct {
|
||||||
AppID string `json:"app_id"` // appid
|
AppID string `json:"app_id"` // appid
|
||||||
AppSecret string `json:"app_secret"` // appsecret
|
AppSecret string `json:"app_secret"` // appsecret
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
// Config config for pay
|
// Config .config for pay
|
||||||
type Config struct {
|
type Config struct {
|
||||||
AppID string `json:"app_id"`
|
AppID string `json:"app_id"`
|
||||||
MchID string `json:"mch_id"`
|
MchID string `json:"mch_id"`
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ type RefundedReqInfo struct {
|
|||||||
SettlementRefundFee *int `xml:"settlement_refund_fee"`
|
SettlementRefundFee *int `xml:"settlement_refund_fee"`
|
||||||
RefundStatus *string `xml:"refund_status"`
|
RefundStatus *string `xml:"refund_status"`
|
||||||
SuccessTime *string `xml:"success_time"`
|
SuccessTime *string `xml:"success_time"`
|
||||||
RefundRecvAccount *string `xml:"refund_recv_account"`
|
RefundRecvAccount *string `xml:"refund_recv_accout"`
|
||||||
RefundAccount *string `xml:"refund_account"`
|
RefundAccount *string `xml:"refund_account"`
|
||||||
RefundRequestSource *string `xml:"refund_request_source"`
|
RefundRequestSource *string `xml:"refund_request_source"`
|
||||||
}
|
}
|
||||||
|
|||||||
98
pay/order/close.go
Normal file
98
pay/order/close.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package order
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_3
|
||||||
|
var closeGateway = "https://api.mch.weixin.qq.com/pay/closeorder"
|
||||||
|
|
||||||
|
// CloseParams 传入的参数
|
||||||
|
type CloseParams struct {
|
||||||
|
OutTradeNo string // 商户订单号
|
||||||
|
SignType string // 签名类型
|
||||||
|
}
|
||||||
|
|
||||||
|
// closeRequest 接口请求参数
|
||||||
|
type closeRequest struct {
|
||||||
|
AppID string `xml:"appid"` // 公众账号ID
|
||||||
|
MchID string `xml:"mch_id"` // 商户号
|
||||||
|
NonceStr string `xml:"nonce_str"` // 随机字符串
|
||||||
|
Sign string `xml:"sign"` // 签名
|
||||||
|
SignType string `xml:"sign_type,omitempty"` // 签名类型
|
||||||
|
OutTradeNo string `xml:"out_trade_no"` // 商户订单号
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseResult 关闭订单返回结果
|
||||||
|
type CloseResult struct {
|
||||||
|
ReturnCode *string `xml:"return_code"`
|
||||||
|
ReturnMsg *string `xml:"return_msg"`
|
||||||
|
|
||||||
|
AppID *string `xml:"appid" json:"appid"`
|
||||||
|
MchID *string `xml:"mch_id"`
|
||||||
|
NonceStr *string `xml:"nonce_str"`
|
||||||
|
Sign *string `xml:"sign"`
|
||||||
|
ResultCode *string `xml:"result_code"`
|
||||||
|
ResultMsg *string `xml:"result_msg"`
|
||||||
|
ErrCode *string `xml:"err_code"`
|
||||||
|
ErrCodeDes *string `xml:"err_code_des"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseOrder 关闭订单
|
||||||
|
func (o *Order) CloseOrder(p *CloseParams) (closeResult CloseResult, err error) {
|
||||||
|
nonceStr := util.RandomStr(32)
|
||||||
|
// 签名类型
|
||||||
|
if p.SignType == "" {
|
||||||
|
p.SignType = "MD5"
|
||||||
|
}
|
||||||
|
|
||||||
|
params := make(map[string]string)
|
||||||
|
params["appid"] = o.AppID
|
||||||
|
params["mch_id"] = o.MchID
|
||||||
|
params["nonce_str"] = nonceStr
|
||||||
|
params["out_trade_no"] = p.OutTradeNo
|
||||||
|
params["sign_type"] = p.SignType
|
||||||
|
|
||||||
|
var (
|
||||||
|
sign string
|
||||||
|
rawRet []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
sign, err = util.ParamSign(params, o.Key)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request := closeRequest{
|
||||||
|
AppID: o.AppID,
|
||||||
|
MchID: o.MchID,
|
||||||
|
NonceStr: nonceStr,
|
||||||
|
Sign: sign,
|
||||||
|
OutTradeNo: p.OutTradeNo,
|
||||||
|
SignType: p.SignType,
|
||||||
|
}
|
||||||
|
|
||||||
|
rawRet, err = util.PostXML(closeGateway, request)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = xml.Unmarshal(rawRet, &closeResult)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if *closeResult.ReturnCode == SUCCESS {
|
||||||
|
// close success
|
||||||
|
if *closeResult.ResultCode == SUCCESS {
|
||||||
|
err = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = errors.New(*closeResult.ErrCode + *closeResult.ErrCodeDes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = errors.New("[msg : xmlUnmarshalError] [rawReturn : " + string(rawRet) + "] [sign : " + sign + "]")
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -172,9 +172,9 @@ func (o *Order) BridgeConfig(p *Params) (cfg Config, err error) {
|
|||||||
// BridgeAppConfig get app bridge config
|
// BridgeAppConfig get app bridge config
|
||||||
func (o *Order) BridgeAppConfig(p *Params) (cfg ConfigForApp, err error) {
|
func (o *Order) BridgeAppConfig(p *Params) (cfg ConfigForApp, err error) {
|
||||||
var (
|
var (
|
||||||
timestamp string = strconv.FormatInt(time.Now().Unix(), 10)
|
timestamp = strconv.FormatInt(time.Now().Unix(), 10)
|
||||||
noncestr string = util.RandomStr(32)
|
noncestr = util.RandomStr(32)
|
||||||
_package string = "Sign=WXPay"
|
_package = "Sign=WXPay"
|
||||||
)
|
)
|
||||||
order, err := o.PrePayOrder(p)
|
order, err := o.PrePayOrder(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -210,6 +210,17 @@ func (o *Order) BridgeAppConfig(p *Params) (cfg ConfigForApp, err error) {
|
|||||||
// PrePayOrder return data for invoke wechat payment
|
// PrePayOrder return data for invoke wechat payment
|
||||||
func (o *Order) PrePayOrder(p *Params) (payOrder PreOrder, err error) {
|
func (o *Order) PrePayOrder(p *Params) (payOrder PreOrder, err error) {
|
||||||
nonceStr := util.RandomStr(32)
|
nonceStr := util.RandomStr(32)
|
||||||
|
|
||||||
|
// 通知地址
|
||||||
|
if len(p.NotifyURL) == 0 {
|
||||||
|
p.NotifyURL = o.NotifyURL // 默认使用order.NotifyURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// 签名类型
|
||||||
|
if p.SignType == "" {
|
||||||
|
p.SignType = util.SignTypeMD5
|
||||||
|
}
|
||||||
|
|
||||||
param := map[string]string{
|
param := map[string]string{
|
||||||
"appid": o.AppID,
|
"appid": o.AppID,
|
||||||
"body": p.Body,
|
"body": p.Body,
|
||||||
@@ -224,15 +235,7 @@ func (o *Order) PrePayOrder(p *Params) (payOrder PreOrder, err error) {
|
|||||||
"detail": p.Detail,
|
"detail": p.Detail,
|
||||||
"attach": p.Attach,
|
"attach": p.Attach,
|
||||||
"goods_tag": p.GoodsTag,
|
"goods_tag": p.GoodsTag,
|
||||||
}
|
"notify_url": p.NotifyURL,
|
||||||
// 签名类型
|
|
||||||
if param["sign_type"] == "" {
|
|
||||||
param["sign_type"] = util.SignTypeMD5
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通知地址
|
|
||||||
if p.NotifyURL != "" {
|
|
||||||
param["notify_url"] = p.NotifyURL
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.TimeExpire != "" {
|
if p.TimeExpire != "" {
|
||||||
|
|||||||
@@ -31,8 +31,18 @@ const (
|
|||||||
SDKAccessTokenExpired Error = "AccessToken 已过期"
|
SDKAccessTokenExpired Error = "AccessToken 已过期"
|
||||||
// SDKApiFreqOutOfLimit 错误码:45009
|
// SDKApiFreqOutOfLimit 错误码:45009
|
||||||
SDKApiFreqOutOfLimit Error = "接口请求次数超频"
|
SDKApiFreqOutOfLimit Error = "接口请求次数超频"
|
||||||
|
// SDKApiForbidden 错误码:48002
|
||||||
|
SDKApiForbidden Error = "API 禁止调用"
|
||||||
|
// SDKInvalidOpenKFID 错误码:95000
|
||||||
|
SDKInvalidOpenKFID Error = "无效的 open_kfid"
|
||||||
|
// SDKOpenKFIDNotExist 错误码:95004
|
||||||
|
SDKOpenKFIDNotExist Error = "open_kfid 不存在"
|
||||||
// SDKWeWorkAlready 错误码:95011
|
// SDKWeWorkAlready 错误码:95011
|
||||||
SDKWeWorkAlready Error = "已在企业微信使用微信客服"
|
SDKWeWorkAlready Error = "已在企业微信使用微信客服"
|
||||||
|
// SDKNotUseInWeCom 错误码:95012
|
||||||
|
SDKNotUseInWeCom Error = "未在企业微信使用微信客服"
|
||||||
|
// SDKApiNotOpen 错误码:95017
|
||||||
|
SDKApiNotOpen Error = "API 功能没有被开启"
|
||||||
)
|
)
|
||||||
|
|
||||||
//Error 输出错误信息
|
//Error 输出错误信息
|
||||||
@@ -63,8 +73,18 @@ func NewSDKErr(code int64, msgList ...string) Error {
|
|||||||
return SDKDecryptMSGFailed
|
return SDKDecryptMSGFailed
|
||||||
case 45009:
|
case 45009:
|
||||||
return SDKApiFreqOutOfLimit
|
return SDKApiFreqOutOfLimit
|
||||||
|
case 48002:
|
||||||
|
return SDKApiForbidden
|
||||||
|
case 95000:
|
||||||
|
return SDKInvalidOpenKFID
|
||||||
|
case 95004:
|
||||||
|
return SDKOpenKFIDNotExist
|
||||||
case 95011:
|
case 95011:
|
||||||
return SDKWeWorkAlready
|
return SDKWeWorkAlready
|
||||||
|
case 95012:
|
||||||
|
return SDKNotUseInWeCom
|
||||||
|
case 95017:
|
||||||
|
return SDKApiNotOpen
|
||||||
default:
|
default:
|
||||||
//返回未知的自定义错误
|
//返回未知的自定义错误
|
||||||
if len(msgList) > 0 {
|
if len(msgList) > 0 {
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ type Menu struct {
|
|||||||
MsgType string `json:"msgtype"` // 消息类型,此时固定为:msgmenu
|
MsgType string `json:"msgtype"` // 消息类型,此时固定为:msgmenu
|
||||||
MsgMenu struct {
|
MsgMenu struct {
|
||||||
HeadContent string `json:"head_content"` // 消息内容,不多于1024字节
|
HeadContent string `json:"head_content"` // 消息内容,不多于1024字节
|
||||||
List []interface{} `json:"list"` // 菜单项配置
|
List []interface{} `json:"list"` // 菜单项配置,不能多余10个
|
||||||
|
TailContent string `json:"tail_content"` // 结束文本, 不多于1024字
|
||||||
} `json:"msgmenu"`
|
} `json:"msgmenu"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
45
work/kf/sendmsgonevent.go
Normal file
45
work/kf/sendmsgonevent.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package kf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 发送事件响应消息
|
||||||
|
sendMsgOnEventAddr = "https://qyapi.weixin.qq.com/cgi-bin/kf/send_msg_on_event?access_token=%s"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SendMsgOnEventSchema 发送事件响应消息
|
||||||
|
type SendMsgOnEventSchema struct {
|
||||||
|
util.CommonError
|
||||||
|
MsgID string `json:"msgid"` // 消息ID。如果请求参数指定了msgid,则原样返回,否则系统自动生成并返回。不多于32字节, 字符串取值范围(正则表达式):[0-9a-zA-Z_-]*
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMsgOnEvent 发送事件响应消息
|
||||||
|
//「进入会话事件」响应消息:
|
||||||
|
// 如果满足通过API下发欢迎语条件(条件为:1. 企业没有在管理端配置了原生欢迎语;2. 用户在过去48小时里未收过欢迎语,且未向该用户发过消息),则用户进入会话事件会额外返回一个welcome_code,开发者以此为凭据调用接口(填到该接口code参数),即可向客户发送客服欢迎语。
|
||||||
|
// 为了保证用户体验以及避免滥用,开发者仅可在收到相关事件后20秒内调用,且只可调用一次。
|
||||||
|
func (r *Client) SendMsgOnEvent(options interface{}) (info SendMsgOnEventSchema, err error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
data []byte
|
||||||
|
)
|
||||||
|
accessToken, err = r.ctx.GetAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err = util.PostJSON(fmt.Sprintf(sendMsgOnEventAddr, accessToken), options)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = json.Unmarshal(data, &info); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if info.ErrCode != 0 {
|
||||||
|
return info, NewSDKErr(info.ErrCode, info.ErrMsg)
|
||||||
|
}
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
55
work/kf/sendmsgonevent/message.go
Normal file
55
work/kf/sendmsgonevent/message.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package sendmsgonevent
|
||||||
|
|
||||||
|
// Message 发送事件响应消息
|
||||||
|
type Message struct {
|
||||||
|
Code string `json:"code"` // 事件响应消息对应的code。通过事件回调下发,仅可使用一次。
|
||||||
|
MsgID string `json:"msgid"` // 消息ID。如果请求参数指定了msgid,则原样返回,否则系统自动生成并返回。不多于32字节,不多于32字节
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text 文本消息
|
||||||
|
type Text struct {
|
||||||
|
Message
|
||||||
|
MsgType string `json:"msgtype"` // 消息类型,此时固定为:text
|
||||||
|
Text struct {
|
||||||
|
Content string `json:"content"` // 消息内容,最长不超过2048个字节
|
||||||
|
} `json:"text"` // 文本消息
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu 发送菜单消息
|
||||||
|
type Menu struct {
|
||||||
|
Message
|
||||||
|
MsgType string `json:"msgtype"` // 消息类型,此时固定为:msgmenu
|
||||||
|
MsgMenu struct {
|
||||||
|
HeadContent string `json:"head_content"` // 消息内容,不多于1024字节
|
||||||
|
List []interface{} `json:"list"` // 菜单项配置,不能多余10个
|
||||||
|
TailContent string `json:"tail_content"` // 结束文本, 不多于1024字
|
||||||
|
} `json:"msgmenu"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuClick 回复菜单
|
||||||
|
type MenuClick struct {
|
||||||
|
Type string `json:"type"` // 菜单类型: click 回复菜单
|
||||||
|
Click struct {
|
||||||
|
ID string `json:"id"` // 菜单ID, 不少于1字节, 不多于64字节
|
||||||
|
Content string `json:"content"` // 菜单显示内容, 不少于1字节, 不多于128字节
|
||||||
|
} `json:"click"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuView 超链接菜单
|
||||||
|
type MenuView struct {
|
||||||
|
Type string `json:"type"` // 菜单类型: view 超链接菜单
|
||||||
|
View struct {
|
||||||
|
URL string `json:"url"` // 点击后跳转的链接, 不少于1字节, 不多于2048字节
|
||||||
|
Content string `json:"content"` // 菜单显示内容, 不少于1字节, 不多于1024字节
|
||||||
|
} `json:"view"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuMiniProgram 小程序菜单
|
||||||
|
type MenuMiniProgram struct {
|
||||||
|
Type string `json:"type"` // 菜单类型: miniprogram 小程序菜单
|
||||||
|
MiniProgram struct {
|
||||||
|
AppID string `json:"appid"` // 小程序appid, 不少于1字节, 不多于32字节
|
||||||
|
PagePath string `json:"pagepath"` // 点击后进入的小程序页面, 不少于1字节, 不多于1024字节
|
||||||
|
Content string `json:"content"` // 菜单显示内容, 不少于1字节, 不多于1024字节
|
||||||
|
} `json:"miniprogram"`
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ const (
|
|||||||
// ReceptionistOptions 添加接待人员请求参数
|
// ReceptionistOptions 添加接待人员请求参数
|
||||||
type ReceptionistOptions struct {
|
type ReceptionistOptions struct {
|
||||||
OpenKFID string `json:"open_kfid"` // 客服帐号ID
|
OpenKFID string `json:"open_kfid"` // 客服帐号ID
|
||||||
UserIDList []string `json:"userid_list"` // 接待人员userid列表
|
UserIDList []string `json:"userid_list"` // 接待人员userid列表。第三方应用填密文userid,即open_userid 可填充个数:1 ~ 100。超过100个需分批调用。
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReceptionistSchema 添加接待人员响应内容
|
// ReceptionistSchema 添加接待人员响应内容
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ type EnterSessionEvent struct {
|
|||||||
ExternalUserID string `json:"external_userid"` // 客户UserID
|
ExternalUserID string `json:"external_userid"` // 客户UserID
|
||||||
Scene string `json:"scene"` // 进入会话的场景值,获取客服帐号链接开发者自定义的场景值
|
Scene string `json:"scene"` // 进入会话的场景值,获取客服帐号链接开发者自定义的场景值
|
||||||
SceneParam string `json:"scene_param"` // 进入会话的自定义参数,获取客服帐号链接返回的url,开发者按规范拼接的scene_param参数
|
SceneParam string `json:"scene_param"` // 进入会话的自定义参数,获取客服帐号链接返回的url,开发者按规范拼接的scene_param参数
|
||||||
|
WelcomeCode string `json:"welcome_code"` // 如果满足发送欢迎语条件(条件为:1. 企业没有在管理端配置了原生欢迎语;2. 用户在过去48小时里未收过欢迎语,且未向该用户发过消息),会返回该字段。可用该welcome_code调用发送事件响应消息接口给客户发送欢迎语。
|
||||||
} `json:"event"` // 事件消息
|
} `json:"event"` // 事件消息
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ type Message struct {
|
|||||||
MsgID string `json:"msgid"` // 消息ID
|
MsgID string `json:"msgid"` // 消息ID
|
||||||
OpenKFID string `json:"open_kfid"` // 客服帐号ID
|
OpenKFID string `json:"open_kfid"` // 客服帐号ID
|
||||||
ExternalUserID string `json:"external_userid"` // 客户UserID
|
ExternalUserID string `json:"external_userid"` // 客户UserID
|
||||||
|
ReceptionistUserID string `json:"servicer_userid"` // 接待客服userID
|
||||||
SendTime uint64 `json:"send_time"` // 消息发送时间
|
SendTime uint64 `json:"send_time"` // 消息发送时间
|
||||||
Origin uint32 `json:"origin"` // 消息来源。3-客户回复的消息 4-系统推送的消 息
|
Origin uint32 `json:"origin"` // 消息来源。3-客户回复的消息 4-系统推送的消 息
|
||||||
MsgType string `json:"msgtype"` // 消息类型
|
MsgType string `json:"msgtype"` // 消息类型
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
企业微信会话存档SDK(基于企业微信C版官方SDK封装),暂时只支持在`linux`环境下使用当前SDK。
|
企业微信会话存档SDK(基于企业微信C版官方SDK封装),暂时只支持在`linux`环境下使用当前SDK。
|
||||||
|
|
||||||
|
|
||||||
### 官方文档地址
|
### 官方文档地址
|
||||||
https://open.work.weixin.qq.com/api/doc/90000/90135/91774
|
https://open.work.weixin.qq.com/api/doc/90000/90135/91774
|
||||||
|
|
||||||
@@ -10,6 +11,10 @@ https://open.work.weixin.qq.com/api/doc/90000/90135/91774
|
|||||||
|
|
||||||
2、从 `github.com/silenceper/wechat/v2/work/msgaudit/lib` 文件夹下复制 `libWeWorkFinanceSdk_C.so` 动态库文件到系统动态链接库默认文件夹下,或者复制到任意文件夹并在当前文件夹下执行 `export LD_LIBRARY_PATH=$(pwd)`命令设置动态链接库检索地址后即可正常使用
|
2、从 `github.com/silenceper/wechat/v2/work/msgaudit/lib` 文件夹下复制 `libWeWorkFinanceSdk_C.so` 动态库文件到系统动态链接库默认文件夹下,或者复制到任意文件夹并在当前文件夹下执行 `export LD_LIBRARY_PATH=$(pwd)`命令设置动态链接库检索地址后即可正常使用
|
||||||
|
|
||||||
|
3、编译要求
|
||||||
|
- 开启CGO: `CGO_ENABLED=1`
|
||||||
|
- 增加tags参数`msgaudit`: `go build -tags msgaudit`或者`go run -tags msgaudit main.go`
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -30,7 +35,7 @@ func main() {
|
|||||||
//初始化客户端
|
//初始化客户端
|
||||||
wechatClient := wechat.NewWechat()
|
wechatClient := wechat.NewWechat()
|
||||||
|
|
||||||
workClient := wechatClient.NewWork(&config.Config{
|
workClient := wechatClient.GetWork(&config.Config{
|
||||||
CorpID: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
CorpID: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||||
CorpSecret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
CorpSecret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||||
RasPrivateKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
RasPrivateKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
//go:build linux && cgo && msgaudit
|
||||||
|
// +build linux,cgo,msgaudit
|
||||||
|
|
||||||
|
// Package msgaudit only for linux
|
||||||
package msgaudit
|
package msgaudit
|
||||||
|
|
||||||
// #cgo LDFLAGS: -L${SRCDIR}/lib -lWeWorkFinanceSdk_C
|
// #cgo LDFLAGS: -L${SRCDIR}/lib -lWeWorkFinanceSdk_C
|
||||||
@@ -7,9 +11,10 @@ package msgaudit
|
|||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/v2/util"
|
"github.com/silenceper/wechat/v2/util"
|
||||||
"github.com/silenceper/wechat/v2/work/config"
|
"github.com/silenceper/wechat/v2/work/config"
|
||||||
"unsafe"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client 会话存档
|
// Client 会话存档
|
||||||
20
work/msgaudit/client_unsupport.go
Normal file
20
work/msgaudit/client_unsupport.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//go:build !linux || !cgo || !msgaudit
|
||||||
|
// +build !linux !cgo !msgaudit
|
||||||
|
|
||||||
|
// Package msgaudit for unsupport platform
|
||||||
|
package msgaudit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/work/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client 会话存档
|
||||||
|
type Client struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient new
|
||||||
|
func NewClient(cfg *config.Config) (*Client, error) {
|
||||||
|
return nil, fmt.Errorf("会话存档功能目前只支持Linux平台运行,并且打开设置CGO_ENABLED=1")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user