mirror of
https://github.com/silenceper/wechat.git
synced 2026-02-23 13:42:25 +08:00
Compare commits
6 Commits
208d5c528a
...
v2.1.9-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dfd2ff608 | ||
|
|
23bb10b0c9 | ||
|
|
b639d2235d | ||
|
|
26d2093bd7 | ||
|
|
cf42cd8d54 | ||
|
|
85ee45580b |
@@ -1,6 +1,7 @@
|
|||||||
package credential
|
package credential
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
context2 "context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -42,6 +43,16 @@ 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) {
|
||||||
|
return js.GetTicketContext(context2.Background(), accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTicketFromServer 从服务器中获取ticket
|
||||||
|
func GetTicketFromServer(accessToken string) (ticket ResTicket, err error) {
|
||||||
|
return GetTicketFromServerContext(context2.Background(), accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTicketContext 获取jsapi_ticket
|
||||||
|
func (js *DefaultJsTicket) GetTicketContext(ctx context2.Context, accessToken string) (ticketStr string, err error) {
|
||||||
// 先从cache中取
|
// 先从cache中取
|
||||||
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", js.cacheKeyPrefix, js.appID)
|
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", js.cacheKeyPrefix, js.appID)
|
||||||
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
|
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
|
||||||
@@ -57,7 +68,7 @@ func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ticket ResTicket
|
var ticket ResTicket
|
||||||
ticket, err = GetTicketFromServer(accessToken)
|
ticket, err = GetTicketFromServerContext(ctx, accessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -67,11 +78,11 @@ func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTicketFromServer 从服务器中获取ticket
|
// GetTicketFromServerContext 从服务器中获取ticket
|
||||||
func GetTicketFromServer(accessToken string) (ticket ResTicket, err error) {
|
func GetTicketFromServerContext(ctx context2.Context, accessToken string) (ticket ResTicket, err error) {
|
||||||
var response []byte
|
var response []byte
|
||||||
url := fmt.Sprintf(getTicketURL, accessToken)
|
url := fmt.Sprintf(getTicketURL, accessToken)
|
||||||
response, err = util.HTTPGet(url)
|
response, err = util.HTTPGetContext(ctx, url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
22
credential/default_js_ticket_test.go
Normal file
22
credential/default_js_ticket_test.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package credential
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"gopkg.in/h2non/gock.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestGetTicketFromServerContext 测试 GetTicketFromServerContext 函数
|
||||||
|
func TestGetTicketFromServerContext(t *testing.T) {
|
||||||
|
defer gock.Off()
|
||||||
|
gock.New(fmt.Sprintf(getTicketURL, "arg-ak")).Reply(200).JSON(&ResTicket{Ticket: "mock-ticket", ExpiresIn: 10})
|
||||||
|
|
||||||
|
ticket, err := GetTicketFromServerContext(context.Background(), "arg-ak")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, int64(0), ticket.ErrCode)
|
||||||
|
assert.Equal(t, "mock-ticket", ticket.Ticket, "they should be equal")
|
||||||
|
assert.Equal(t, int64(10), ticket.ExpiresIn, "they should be equal")
|
||||||
|
}
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
package credential
|
package credential
|
||||||
|
|
||||||
|
import context2 "context"
|
||||||
|
|
||||||
// JsTicketHandle js ticket获取
|
// JsTicketHandle js ticket获取
|
||||||
type JsTicketHandle interface {
|
type JsTicketHandle interface {
|
||||||
// GetTicket 获取ticket
|
// GetTicket 获取ticket
|
||||||
GetTicket(accessToken string) (ticket string, err error)
|
GetTicket(accessToken string) (ticket string, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JsTicketContextHandle js ticket获取
|
||||||
|
type JsTicketContextHandle interface {
|
||||||
|
JsTicketHandle
|
||||||
|
GetTicketContext(ctx context2.Context, accessToken string) (ticket string, err error)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,11 +10,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// code2SessionURL 小程序登录
|
||||||
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 检查加密信息
|
||||||
checkEncryptedDataURL = "https://api.weixin.qq.com/wxa/business/checkencryptedmsg?access_token=%s"
|
checkEncryptedDataURL = "https://api.weixin.qq.com/wxa/business/checkencryptedmsg?access_token=%s"
|
||||||
|
// getPhoneNumber 获取手机号
|
||||||
getPhoneNumber = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s"
|
getPhoneNumber = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s"
|
||||||
|
// checkSessionURL 检验登录态
|
||||||
|
checkSessionURL = "https://api.weixin.qq.com/wxa/checksession?access_token=%s&signature=%s&openid=%s&sig_method=hmac_sha256"
|
||||||
|
// resetUserSessionKeyURL 重置登录态
|
||||||
|
resetUserSessionKeyURL = "https://api.weixin.qq.com/wxa/resetusersessionkey?access_token=%s&signature=%s&openid=%s&sig_method=hmac_sha256"
|
||||||
|
// getPluginOpenPIDURL 获取插件用户openPID
|
||||||
|
getPluginOpenPIDURL = "https://api.weixin.qq.com/wxa/getpluginopenpid?access_token=%s"
|
||||||
|
// getPaidUnionIDURL 支付后获取 UnionID
|
||||||
|
getPaidUnionIDURL = "https://api.weixin.qq.com/wxa/getpaidunionid"
|
||||||
|
// getUserEncryptKeyURL 获取用户encryptKey
|
||||||
|
getUserEncryptKeyURL = "https://api.weixin.qq.com/wxa/business/getuserencryptkey?access_token=%s&signature=%s&openid=%s&sig_method=hmac_sha256"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Auth 登录/用户信息
|
// Auth 登录/用户信息
|
||||||
@@ -65,9 +76,45 @@ func (auth *Auth) Code2SessionContext(ctx context2.Context, jsCode string) (resu
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
// GetPaidUnionIDRequest 支付后获取UnionID请求
|
||||||
|
GetPaidUnionIDRequest struct {
|
||||||
|
OpenID string `json:"openid"`
|
||||||
|
TransactionID string `json:"transaction_id,omitempty"`
|
||||||
|
MchID string `json:"mch_id,omitempty"`
|
||||||
|
OutTradeNo string `json:"out_trade_no,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPaidUnionIDResponse 支付后获取UnionID响应
|
||||||
|
GetPaidUnionIDResponse struct {
|
||||||
|
util.CommonError
|
||||||
|
UnionID string `json:"unionid"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// GetPaidUnionID 用户支付完成后,获取该用户的 UnionId,无需用户授权
|
// GetPaidUnionID 用户支付完成后,获取该用户的 UnionId,无需用户授权
|
||||||
func (auth *Auth) GetPaidUnionID() {
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/basic-info/getPaidUnionid.html
|
||||||
// TODO
|
func (auth *Auth) GetPaidUnionID(req *GetPaidUnionIDRequest) (string, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = auth.GetAccessToken(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var url string
|
||||||
|
if req.TransactionID != "" {
|
||||||
|
url = fmt.Sprintf("%s?access_token=%s&openid=%s&transaction_id=%s", getPaidUnionIDURL, accessToken, req.OpenID, req.TransactionID)
|
||||||
|
} else {
|
||||||
|
url = fmt.Sprintf("%s?access_token=%s&openid=%s&mch_id=%s&out_trade_no=%s", getPaidUnionIDURL, accessToken, req.OpenID, req.MchID, req.OutTradeNo)
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.HTTPGet(url); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
result := &GetPaidUnionIDResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "GetPaidUnionID")
|
||||||
|
return result.UnionID, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckEncryptedData .检查加密信息是否由微信生成(当前只支持手机号加密数据),只能检测最近3天生成的加密数据
|
// CheckEncryptedData .检查加密信息是否由微信生成(当前只支持手机号加密数据),只能检测最近3天生成的加密数据
|
||||||
@@ -146,3 +193,115 @@ func (auth *Auth) GetPhoneNumberContext(ctx context2.Context, code string) (*Get
|
|||||||
func (auth *Auth) GetPhoneNumber(code string) (*GetPhoneNumberResponse, error) {
|
func (auth *Auth) GetPhoneNumber(code string) (*GetPhoneNumberResponse, error) {
|
||||||
return auth.GetPhoneNumberContext(context2.Background(), code)
|
return auth.GetPhoneNumberContext(context2.Background(), code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckSession 检验登录态
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/checkSessionKey.html
|
||||||
|
func (auth *Auth) CheckSession(signature, openID string) error {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = auth.GetAccessToken(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.HTTPGet(fmt.Sprintf(checkSessionURL, accessToken, signature, openID)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return util.DecodeWithCommonError(response, "CheckSession")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetUserSessionKeyResponse 重置登录态响应
|
||||||
|
type ResetUserSessionKeyResponse struct {
|
||||||
|
util.CommonError
|
||||||
|
OpenID string `json:"openid"`
|
||||||
|
SessionKey string `json:"session_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetUserSessionKey 重置登录态
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/ResetUserSessionKey.html
|
||||||
|
func (auth *Auth) ResetUserSessionKey(signature, openID string) (*ResetUserSessionKeyResponse, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = auth.GetAccessToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.HTTPGet(fmt.Sprintf(resetUserSessionKeyURL, accessToken, signature, openID)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &ResetUserSessionKeyResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "ResetUserSessionKey")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
// GetPluginOpenPIDRequest 获取插件用户openPID请求
|
||||||
|
GetPluginOpenPIDRequest struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPluginOpenPIDResponse 获取插件用户openPID响应
|
||||||
|
GetPluginOpenPIDResponse struct {
|
||||||
|
util.CommonError
|
||||||
|
OpenPID string `json:"openpid"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetPluginOpenPID 获取插件用户openPID
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/basic-info/getPluginOpenPId.html
|
||||||
|
func (auth *Auth) GetPluginOpenPID(code string) (string, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = auth.GetAccessToken(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req := &GetPluginOpenPIDRequest{
|
||||||
|
Code: code,
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.PostJSON(fmt.Sprintf(getPluginOpenPIDURL, accessToken), req); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
result := &GetPluginOpenPIDResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "GetPluginOpenPID")
|
||||||
|
return result.OpenPID, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserEncryptKeyResponse 获取用户encryptKey响应
|
||||||
|
type GetUserEncryptKeyResponse struct {
|
||||||
|
util.CommonError
|
||||||
|
KeyInfoList []KeyInfo `json:"key_info_list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyInfo 用户最近三次的加密key
|
||||||
|
type KeyInfo struct {
|
||||||
|
EncryptKey string `json:"encrypt_key"`
|
||||||
|
Version int64 `json:"version"`
|
||||||
|
ExpireIn int64 `json:"expire_in"`
|
||||||
|
Iv string `json:"iv"`
|
||||||
|
CreateTime int64 `json:"create_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserEncryptKey 获取用户encryptKey
|
||||||
|
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/internet/getUserEncryptKey.html
|
||||||
|
func (auth *Auth) GetUserEncryptKey(signature, openID string) (*GetUserEncryptKeyResponse, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = auth.GetAccessToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.HTTPGet(fmt.Sprintf(getUserEncryptKeyURL, accessToken, signature, openID)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &GetUserEncryptKeyResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "GetUserEncryptKey")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package js
|
package js
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
context2 "context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/v2/credential"
|
"github.com/silenceper/wechat/v2/credential"
|
||||||
@@ -39,20 +40,40 @@ func (js *Js) SetJsTicketHandle(ticketHandle credential.JsTicketHandle) {
|
|||||||
// GetConfig 获取jssdk需要的配置参数
|
// GetConfig 获取jssdk需要的配置参数
|
||||||
// uri 为当前网页地址
|
// uri 为当前网页地址
|
||||||
func (js *Js) GetConfig(uri string) (config *Config, err error) {
|
func (js *Js) GetConfig(uri string) (config *Config, err error) {
|
||||||
|
return js.GetConfigContext(context2.Background(), uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigContext 新方法,允许传入上下文,避免协程泄漏
|
||||||
|
func (js *Js) GetConfigContext(ctx context2.Context, uri string) (config *Config, err error) {
|
||||||
var accessToken string
|
var accessToken string
|
||||||
accessToken, err = js.GetAccessToken()
|
// 类型断言,如果断言成功,调用安全的 GetAccessTokenContext 方法
|
||||||
|
if ctxHandle, ok := js.Context.AccessTokenHandle.(credential.AccessTokenContextHandle); ok {
|
||||||
|
accessToken, err = ctxHandle.GetAccessTokenContext(ctx)
|
||||||
|
} else {
|
||||||
|
// 如果没有实现 AccessTokenContextHandle 接口,调用旧的 GetAccessToken 方法
|
||||||
|
accessToken, err = js.Context.GetAccessToken()
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var ticketStr string
|
var ticketStr string
|
||||||
ticketStr, err = js.GetTicket(accessToken)
|
// 类型断言 jsTicket
|
||||||
|
if ticketCtxHandle, ok := js.JsTicketHandle.(credential.JsTicketContextHandle); ok {
|
||||||
|
ticketStr, err = ticketCtxHandle.GetTicketContext(ctx, accessToken)
|
||||||
|
} else {
|
||||||
|
// 如果没有实现 JsTicketContextHandle 接口,调用旧的 GetTicket 方法
|
||||||
|
ticketStr, err = js.GetTicket(accessToken)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nonceStr := util.RandomStr(16)
|
nonceStr := util.RandomStr(16)
|
||||||
timestamp := util.GetCurrTS()
|
timestamp := util.GetCurrTS()
|
||||||
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
||||||
sigStr := util.Signature(str)
|
sigStr := util.Signature(str)
|
||||||
|
|
||||||
config = new(Config)
|
config = new(Config)
|
||||||
config.AppID = js.AppID
|
config.AppID = js.AppID
|
||||||
config.NonceStr = nonceStr
|
config.NonceStr = nonceStr
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package account
|
|||||||
|
|
||||||
import "github.com/silenceper/wechat/v2/openplatform/context"
|
import "github.com/silenceper/wechat/v2/openplatform/context"
|
||||||
|
|
||||||
// Account 开放平台张哈管理
|
// Account 开放平台帐号管理
|
||||||
// TODO 实现方法
|
// TODO 实现方法
|
||||||
type Account struct {
|
type Account struct {
|
||||||
*context.Context
|
*context.Context
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package js
|
package js
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
context2 "context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/silenceper/wechat/v2/credential"
|
"github.com/silenceper/wechat/v2/credential"
|
||||||
@@ -32,14 +33,31 @@ func (js *Js) SetJsTicketHandle(ticketHandle credential.JsTicketHandle) {
|
|||||||
// GetConfig 第三方平台 - 获取jssdk需要的配置参数
|
// GetConfig 第三方平台 - 获取jssdk需要的配置参数
|
||||||
// uri 为当前网页地址
|
// uri 为当前网页地址
|
||||||
func (js *Js) GetConfig(uri, appid string) (config *officialJs.Config, err error) {
|
func (js *Js) GetConfig(uri, appid string) (config *officialJs.Config, err error) {
|
||||||
config = new(officialJs.Config)
|
return js.GetConfigContext(context2.Background(), uri, appid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigContext 新方法,允许传入上下文,避免协程泄漏
|
||||||
|
func (js *Js) GetConfigContext(ctx context2.Context, uri, appid string) (config *officialJs.Config, err error) {
|
||||||
var accessToken string
|
var accessToken string
|
||||||
accessToken, err = js.GetAccessToken()
|
// 类型断言,如果断言成功,调用安全的 GetAccessTokenContext 方法
|
||||||
|
if ctxHandle, ok := js.Context.AccessTokenHandle.(credential.AccessTokenContextHandle); ok {
|
||||||
|
accessToken, err = ctxHandle.GetAccessTokenContext(ctx)
|
||||||
|
} else {
|
||||||
|
// 如果没有实现 AccessTokenContextHandle 接口,调用旧的 GetAccessToken 方法
|
||||||
|
accessToken, err = js.Context.GetAccessToken()
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var ticketStr string
|
var ticketStr string
|
||||||
ticketStr, err = js.GetTicket(accessToken)
|
// 类型断言 jsTicket
|
||||||
|
if ticketCtxHandle, ok := js.JsTicketHandle.(credential.JsTicketContextHandle); ok {
|
||||||
|
ticketStr, err = ticketCtxHandle.GetTicketContext(ctx, accessToken)
|
||||||
|
} else {
|
||||||
|
// 如果没有实现 JsTicketContextHandle 接口,调用旧的 GetTicket 方法
|
||||||
|
ticketStr, err = js.GetTicket(accessToken)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -49,6 +67,7 @@ func (js *Js) GetConfig(uri, appid string) (config *officialJs.Config, err error
|
|||||||
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
||||||
sigStr := util.Signature(str)
|
sigStr := util.Signature(str)
|
||||||
|
|
||||||
|
config = new(officialJs.Config)
|
||||||
config.AppID = appid
|
config.AppID = appid
|
||||||
config.NonceStr = nonceStr
|
config.NonceStr = nonceStr
|
||||||
config.Timestamp = timestamp
|
config.Timestamp = timestamp
|
||||||
|
|||||||
147
openplatform/officialaccount/js/js_test.go
Normal file
147
openplatform/officialaccount/js/js_test.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
// 验证 js.GetConfigContext 是否能正确传递上下文到 HTTP 请求,确保上下文正确传播,防止在获取 JSSDK 配置时发生协程泄露。
|
||||||
|
package js
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
context2 "context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/silenceper/wechat/v2/cache"
|
||||||
|
"github.com/silenceper/wechat/v2/credential"
|
||||||
|
"github.com/silenceper/wechat/v2/officialaccount/config"
|
||||||
|
"github.com/silenceper/wechat/v2/officialaccount/context"
|
||||||
|
"github.com/silenceper/wechat/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockAccessTokenHandle 模拟 AccessTokenHandle
|
||||||
|
type mockAccessTokenHandle struct{}
|
||||||
|
|
||||||
|
func (m *mockAccessTokenHandle) GetAccessToken() (string, error) {
|
||||||
|
return "mock-access-token", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAccessTokenHandle) GetAccessTokenContext(_ context2.Context) (string, error) {
|
||||||
|
return "mock-access-token", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// contextCheckingRoundTripper 自定义 RoundTripper 用于检查 context
|
||||||
|
type contextCheckingRoundTripper struct {
|
||||||
|
originalCtx context2.Context
|
||||||
|
t *testing.T
|
||||||
|
key interface{}
|
||||||
|
expectedVal interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rt *contextCheckingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
// 获取请求中的 context
|
||||||
|
reqCtx := req.Context()
|
||||||
|
|
||||||
|
// 打印 context 比较结果
|
||||||
|
rt.t.Logf("比较上下文的内存地址:\n")
|
||||||
|
if reqCtx == rt.originalCtx {
|
||||||
|
rt.t.Logf("上下文具有相同的内存地址。原始上下文: %p, 请求上下文: %p\n", rt.originalCtx, reqCtx)
|
||||||
|
} else {
|
||||||
|
rt.t.Logf("上下文具有不同的内存地址。原始上下文: %p, 请求上下文: %p\n", rt.originalCtx, reqCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 context 中的键值对
|
||||||
|
if rt.key != nil {
|
||||||
|
value := reqCtx.Value(rt.key)
|
||||||
|
rt.t.Logf("检查请求上下文中的键 %v:\n", rt.key)
|
||||||
|
if value != rt.expectedVal {
|
||||||
|
rt.t.Errorf("上下文键 %v 的值不匹配: 预期 %v, 实际 %v\n", rt.key, rt.expectedVal, value)
|
||||||
|
} else {
|
||||||
|
rt.t.Logf("上下文键 %v 的值匹配: 预期 %v, 实际 %v\n", rt.key, rt.expectedVal, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查上下文是否已取消
|
||||||
|
select {
|
||||||
|
case <-reqCtx.Done():
|
||||||
|
return nil, reqCtx.Err() // 返回上下文取消错误
|
||||||
|
default:
|
||||||
|
// 返回模拟的 HTTP 响应,包含有效的 JSON
|
||||||
|
responseBody := `{"ticket":"mock-ticket","expires_in":7200}`
|
||||||
|
response := &http.Response{
|
||||||
|
Status: "200 OK",
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
ProtoMajor: 1,
|
||||||
|
ProtoMinor: 1,
|
||||||
|
Body: io.NopCloser(bytes.NewReader([]byte(responseBody))),
|
||||||
|
ContentLength: int64(len(responseBody)),
|
||||||
|
Header: make(http.Header),
|
||||||
|
}
|
||||||
|
response.Header.Set("Content-Type", "application/json")
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// contextKey 定义自定义上下文键类型,避免使用内置 string 类型
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
// setupJsInstance 初始化 Js 实例和 HTTP 客户端
|
||||||
|
func setupJsInstance(t *testing.T, ctx context2.Context, key, val interface{}) (*Js, func()) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
AppID: "test-app-id",
|
||||||
|
AppSecret: "test-app-secret",
|
||||||
|
Cache: cache.NewMemory(),
|
||||||
|
}
|
||||||
|
cacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", credential.CacheKeyOfficialAccountPrefix, cfg.AppID)
|
||||||
|
if err := cfg.Cache.Delete(cacheKey); err != nil {
|
||||||
|
t.Fatalf("清除缓存失败: %v", err)
|
||||||
|
}
|
||||||
|
t.Log("清除 jsapi_ticket 的缓存:", cacheKey)
|
||||||
|
|
||||||
|
ctxHandle := &context.Context{Config: cfg, AccessTokenHandle: &mockAccessTokenHandle{}}
|
||||||
|
jsInstance := NewJs(ctxHandle, cfg.AppID)
|
||||||
|
jsInstance.SetJsTicketHandle(credential.NewDefaultJsTicket(cfg.AppID, credential.CacheKeyOfficialAccountPrefix, cfg.Cache))
|
||||||
|
|
||||||
|
originalClient := util.DefaultHTTPClient
|
||||||
|
util.DefaultHTTPClient = &http.Client{
|
||||||
|
Transport: &contextCheckingRoundTripper{originalCtx: ctx, t: t, key: key, expectedVal: val},
|
||||||
|
}
|
||||||
|
return jsInstance, func() { util.DefaultHTTPClient = originalClient }
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetConfigContext 测试GetConfigContext的上下文传递和取消行为。
|
||||||
|
func TestGetConfigContext(t *testing.T) {
|
||||||
|
t.Run("ContextPassing", func(t *testing.T) {
|
||||||
|
ctxKey := contextKey("testKey111") // 使用自定义类型 contextKey
|
||||||
|
ctxValue := "testValue222"
|
||||||
|
ctx := context2.WithValue(context2.Background(), ctxKey, ctxValue)
|
||||||
|
t.Logf("创建的测试上下文: %p, 添加的键值对: %v=%v\n", ctx, ctxKey, ctxValue)
|
||||||
|
|
||||||
|
jsInstance, cleanup := setupJsInstance(t, ctx, ctxKey, ctxValue)
|
||||||
|
defer cleanup()
|
||||||
|
t.Log("调用 GetConfigContext")
|
||||||
|
config2, err := jsInstance.GetConfigContext(ctx, "https://www.baidu.com", "test-app-id")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetConfigContext 失败: %v", err)
|
||||||
|
}
|
||||||
|
if config2.AppID != "test-app-id" {
|
||||||
|
t.Errorf("预期 AppID 为 %s,实际为 %s", "test-app-id", config2.AppID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ContextCancellation", func(t *testing.T) {
|
||||||
|
ctx, cancel := context2.WithCancel(context2.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
jsInstance, cleanup := setupJsInstance(t, ctx, nil, nil)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
t.Log("调用 GetConfigContext(已取消上下文)")
|
||||||
|
_, err := jsInstance.GetConfigContext(ctx, "https://www.baidu.com", "test-app-id")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("预期上下文取消错误,但 GetConfigContext 未返回错误")
|
||||||
|
} else if !errors.Is(err, context2.Canceled) {
|
||||||
|
t.Errorf("预期错误为 context.Canceled,实际为: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -166,6 +166,7 @@ func PostFile(fieldName, filePath, uri string) ([]byte, error) {
|
|||||||
IsFile: true,
|
IsFile: true,
|
||||||
Fieldname: fieldName,
|
Fieldname: fieldName,
|
||||||
FilePath: filePath,
|
FilePath: filePath,
|
||||||
|
Filename: filePath,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return PostMultipartForm(fields, uri)
|
return PostMultipartForm(fields, uri)
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const (
|
|||||||
clearOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/clear_checkin_option_array_field?access_token=%s"
|
clearOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/clear_checkin_option_array_field?access_token=%s"
|
||||||
// delOptionURL 删除打卡规则
|
// delOptionURL 删除打卡规则
|
||||||
delOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/del_checkin_option?access_token=%s"
|
delOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/del_checkin_option?access_token=%s"
|
||||||
|
// addRecordURL 添加打卡记录
|
||||||
|
addRecordURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/add_checkin_record?access_token=%s"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetScheduleListRequest 为打卡人员排班请求
|
// SetScheduleListRequest 为打卡人员排班请求
|
||||||
@@ -386,3 +388,41 @@ func (r *Client) DelOption(req *DelOptionRequest) error {
|
|||||||
}
|
}
|
||||||
return util.DecodeWithCommonError(response, "DelOption")
|
return util.DecodeWithCommonError(response, "DelOption")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddRecordRequest 添加打卡记录请求
|
||||||
|
type AddRecordRequest struct {
|
||||||
|
Records []Record `json:"records"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record 打卡记录
|
||||||
|
type Record struct {
|
||||||
|
UserID string `json:"userid"`
|
||||||
|
CheckinTime int64 `json:"checkin_time"`
|
||||||
|
LocationTitle string `json:"location_title"`
|
||||||
|
LocationDetail string `json:"location_detail"`
|
||||||
|
MediaIDS []string `json:"mediaids"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
DeviceType int `json:"device_type"`
|
||||||
|
Lat int64 `json:"lat"`
|
||||||
|
Lng int64 `json:"lng"`
|
||||||
|
DeviceDetail string `json:"device_detail"`
|
||||||
|
WifiName string `json:"wifiname"`
|
||||||
|
WifiMac string `json:"wifimac"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRecord 添加打卡记录
|
||||||
|
// see https://developer.work.weixin.qq.com/document/path/99647
|
||||||
|
func (r *Client) AddRecord(req *AddRecordRequest) error {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.PostJSON(fmt.Sprintf(addRecordURL, accessToken), req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return util.DecodeWithCommonError(response, "AddRecord")
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ const (
|
|||||||
customerAcquisitionQuotaURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition_quota?access_token=%s"
|
customerAcquisitionQuotaURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition_quota?access_token=%s"
|
||||||
// customerAcquisitionStatistic 查询链接使用详情
|
// customerAcquisitionStatistic 查询链接使用详情
|
||||||
customerAcquisitionStatisticURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/statistic?access_token=%s"
|
customerAcquisitionStatisticURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/statistic?access_token=%s"
|
||||||
|
// customerAcquisitionGetChatInfo 获取成员多次收消息详情
|
||||||
|
customerAcquisitionGetChatInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/get_chat_info?access_token=%s"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -308,3 +310,42 @@ func (r *Client) CustomerAcquisitionStatistic(req *CustomerAcquisitionStatisticR
|
|||||||
err = util.DecodeWithError(response, result, "CustomerAcquisitionStatistic")
|
err = util.DecodeWithError(response, result, "CustomerAcquisitionStatistic")
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
// GetChatInfoRequest 获取成员多次收消息详情请求
|
||||||
|
GetChatInfoRequest struct {
|
||||||
|
ChatKey string `json:"chat_key"`
|
||||||
|
}
|
||||||
|
// GetChatInfoResponse 获取成员多次收消息详情响应
|
||||||
|
GetChatInfoResponse struct {
|
||||||
|
util.CommonError
|
||||||
|
UserID string `json:"userid"`
|
||||||
|
ExternalUserID string `json:"external_userid"`
|
||||||
|
ChatInfo ChatInfo `json:"chat_info"`
|
||||||
|
}
|
||||||
|
// ChatInfo 聊天信息
|
||||||
|
ChatInfo struct {
|
||||||
|
RecvMsgCnt int64 `json:"recv_msg_cnt"` // 成员收到的此客户的消息次数
|
||||||
|
LinkID string `json:"link_id"` // 成员添加客户的获客链接id
|
||||||
|
State string `json:"state"` // 成员添加客户的state
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetChatInfo 获取成员多次收消息详情
|
||||||
|
// see https://developer.work.weixin.qq.com/document/path/100130
|
||||||
|
func (r *Client) GetChatInfo(req *GetChatInfoRequest) (*GetChatInfoResponse, error) {
|
||||||
|
var (
|
||||||
|
accessToken string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if accessToken, err = r.GetAccessToken(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var response []byte
|
||||||
|
if response, err = util.PostJSON(fmt.Sprintf(customerAcquisitionGetChatInfoURL, accessToken), req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := &GetChatInfoResponse{}
|
||||||
|
err = util.DecodeWithError(response, result, "GetChatInfo")
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user