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

Compare commits

...

11 Commits

Author SHA1 Message Date
markwang
2dfd2ff608 feat: 微信小程序-登录及用户信息 (#830)
* feat: 微信小程序-登录及用户信息

* feat: 微信小程序-登录及用户信息
2025-04-27 10:09:31 +08:00
litterGuy
23bb10b0c9 fix: ImageUpload 接口报错 'media data missing hint:' (#832) 2025-04-27 10:08:51 +08:00
lizhuang
b639d2235d Add JSSDK context method functionality (#828)
* Add JSSDK context method functionality

* 善JSSDK上下文方法,并添加测试文件

* feat: 完善JSSDK上下文方法,保证协程安全,并添加测试文件

* 修改 import 包分组处理

* feat: 修改测试文件中 fmt.Print -> t.Log

* 删除空行
2025-04-23 14:14:16 +08:00
wwek
26d2093bd7 fix typo (#831) 2025-04-22 19:40:57 +08:00
曹晶
cf42cd8d54 feat: 添加获取成员多次收消息详情API (#824)
* feat(media): add getTempFile api

add getTempFile api

* feat: 添加获取成员多次收消息详情API

- 添加customerAcquisitionGetChatInfoURL常量
- 实现GetChatInfo方法及相关请求/响应结构体
- 支持企业微信获客助手多次收消息功能

---------

Co-authored-by: caojing <jingjing.cao@trustbe.cn>
2025-04-21 10:44:12 +08:00
markwang
85ee45580b feat: 企业微信-打卡-添加打卡记录 (#829) 2025-04-21 10:42:47 +08:00
markwang
208d5c528a feat: 企业微信-打卡-新增返回字段 (#827) 2025-04-18 20:08:21 +08:00
markwang
b5f9a8933e 企业微信-通讯录管理-更新成员接口,支持更新企业邮箱别名 (#826)
* feat: 企业微信-通讯录管理,新增更新成员、更新部门、删除部门方法

* feat: 企业微信-通讯录管理-更新成员接口,支持更新企业邮箱别名
2025-04-18 14:13:26 +08:00
LarryLiu
52fb5596d3 修改最新版本的授权地址 (#823)
* Update accessToken.go

add openplatform refresh_token

* Update accessToken.go

openplatform add refresh_token expire set 10 year

* Update openplatform/context/accessToken.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update accessToken.go

修改最新的授权H5链接地址

* Update accessToken.go

增加新版本授权链接

* Update accessToken.go

增加新版本授权链接

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-03-31 10:51:46 +08:00
LarryLiu
44150c557e 微信开放平台增加refreshtoken缓存 (#822)
* Update accessToken.go

add openplatform refresh_token

* Update accessToken.go

openplatform add refresh_token expire set 10 year

* Update openplatform/context/accessToken.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: houseme <housemecn@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-03-28 11:38:57 +08:00
fearlessfei
635a0c640d feat(auth): getAccessToken with context (#820) 2025-03-25 14:20:12 +08:00
14 changed files with 545 additions and 23 deletions

View File

@@ -1,6 +1,7 @@
package credential
import (
context2 "context"
"encoding/json"
"fmt"
"sync"
@@ -42,6 +43,16 @@ type ResTicket struct {
// GetTicket 获取jsapi_ticket
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中取
jsAPITicketCacheKey := fmt.Sprintf("%s_jsapi_ticket_%s", js.cacheKeyPrefix, js.appID)
if val := js.cache.Get(jsAPITicketCacheKey); val != nil {
@@ -57,7 +68,7 @@ func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err
}
var ticket ResTicket
ticket, err = GetTicketFromServer(accessToken)
ticket, err = GetTicketFromServerContext(ctx, accessToken)
if err != nil {
return
}
@@ -67,11 +78,11 @@ func (js *DefaultJsTicket) GetTicket(accessToken string) (ticketStr string, err
return
}
// GetTicketFromServer 从服务器中获取ticket
func GetTicketFromServer(accessToken string) (ticket ResTicket, err error) {
// GetTicketFromServerContext 从服务器中获取ticket
func GetTicketFromServerContext(ctx context2.Context, accessToken string) (ticket ResTicket, err error) {
var response []byte
url := fmt.Sprintf(getTicketURL, accessToken)
response, err = util.HTTPGet(url)
response, err = util.HTTPGetContext(ctx, url)
if err != nil {
return
}

View 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")
}

View File

@@ -1,7 +1,15 @@
package credential
import context2 "context"
// JsTicketHandle js ticket获取
type JsTicketHandle interface {
// GetTicket 获取ticket
GetTicket(accessToken string) (ticket string, err error)
}
// JsTicketContextHandle js ticket获取
type JsTicketContextHandle interface {
JsTicketHandle
GetTicketContext(ctx context2.Context, accessToken string) (ticket string, err error)
}

View File

@@ -10,11 +10,22 @@ import (
)
const (
// code2SessionURL 小程序登录
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"
// getPhoneNumber 获取手机号
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 登录/用户信息
@@ -65,9 +76,45 @@ func (auth *Auth) Code2SessionContext(ctx context2.Context, jsCode string) (resu
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无需用户授权
func (auth *Auth) GetPaidUnionID() {
// TODO
// see https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/basic-info/getPaidUnionid.html
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天生成的加密数据
@@ -81,7 +128,7 @@ func (auth *Auth) CheckEncryptedDataContext(ctx context2.Context, encryptedMsgHa
var (
at string
)
if at, err = auth.GetAccessToken(); err != nil {
if at, err = auth.GetAccessTokenContext(ctx); err != nil {
return
}
@@ -120,7 +167,7 @@ func (auth *Auth) GetPhoneNumberContext(ctx context2.Context, code string) (*Get
at string
err error
)
if at, err = auth.GetAccessToken(); err != nil {
if at, err = auth.GetAccessTokenContext(ctx); err != nil {
return nil, err
}
body := map[string]interface{}{
@@ -146,3 +193,115 @@ func (auth *Auth) GetPhoneNumberContext(ctx context2.Context, code string) (*Get
func (auth *Auth) GetPhoneNumber(code string) (*GetPhoneNumberResponse, error) {
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
}

View File

@@ -1,6 +1,7 @@
package js
import (
context2 "context"
"fmt"
"github.com/silenceper/wechat/v2/credential"
@@ -39,20 +40,40 @@ func (js *Js) SetJsTicketHandle(ticketHandle credential.JsTicketHandle) {
// GetConfig 获取jssdk需要的配置参数
// uri 为当前网页地址
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
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 {
return
}
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 {
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 = new(Config)
config.AppID = js.AppID
config.NonceStr = nonceStr

View File

@@ -2,7 +2,7 @@ package account
import "github.com/silenceper/wechat/v2/openplatform/context"
// Account 开放平台张哈管理
// Account 开放平台帐号管理
// TODO 实现方法
type Account struct {
*context.Context

View File

@@ -20,6 +20,7 @@ const (
getComponentInfoURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token=%s"
componentLoginURL = "https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=%s&pre_auth_code=%s&redirect_uri=%s&auth_type=%d&biz_appid=%s"
bindComponentURL = "https://mp.weixin.qq.com/safe/bindcomponent?action=bindcomponent&auth_type=%d&no_scan=1&component_appid=%s&pre_auth_code=%s&redirect_uri=%s&biz_appid=%s#wechat_redirect"
bindComponentURLV2 = "https://open.weixin.qq.com/wxaopen/safe/bindcomponent?action=bindcomponent&auth_type=%d&no_scan=1&component_appid=%s&pre_auth_code=%s&redirect_uri=%s&biz_appid=%s#wechat_redirect"
// TODO 获取授权方选项信息
// getComponentConfigURL = "https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_option?component_access_token=%s"
// TODO 获取已授权的账号信息
@@ -137,6 +138,20 @@ func (ctx *Context) GetBindComponentURL(redirectURI string, authType int, bizApp
return ctx.GetBindComponentURLContext(context.Background(), redirectURI, authType, bizAppID)
}
// GetBindComponentURLV2Context 获取新版本第三方公众号授权链接(链接跳转,适用移动端)
func (ctx *Context) GetBindComponentURLV2Context(stdCtx context.Context, redirectURI string, authType int, bizAppID string) (string, error) {
code, err := ctx.GetPreCodeContext(stdCtx)
if err != nil {
return "", err
}
return fmt.Sprintf(bindComponentURLV2, authType, ctx.AppID, code, url.QueryEscape(redirectURI), bizAppID), nil
}
// GetBindComponentURLV2 获取新版本第三方公众号授权链接(链接跳转,适用移动端)
func (ctx *Context) GetBindComponentURLV2(redirectURI string, authType int, bizAppID string) (string, error) {
return ctx.GetBindComponentURLContext(context.Background(), redirectURI, authType, bizAppID)
}
// ID 微信返回接口中各种类型字段
type ID struct {
ID int `json:"id"`
@@ -225,6 +240,10 @@ func (ctx *Context) RefreshAuthrTokenContext(stdCtx context.Context, appid, refr
if err := cache.SetContext(stdCtx, ctx.Cache, authrTokenKey, ret.AccessToken, time.Second*time.Duration(ret.ExpiresIn-30)); err != nil {
return nil, err
}
refreshTokenKey := "authorizer_refresh_token_" + appid
if err := cache.SetContext(stdCtx, ctx.Cache, refreshTokenKey, ret.RefreshToken, 10*365*24*60*60*time.Second); err != nil {
return nil, err
}
return ret, nil
}
@@ -238,8 +257,18 @@ func (ctx *Context) GetAuthrAccessTokenContext(stdCtx context.Context, appid str
authrTokenKey := "authorizer_access_token_" + appid
val := cache.GetContext(stdCtx, ctx.Cache, authrTokenKey)
if val == nil {
return "", fmt.Errorf("cannot get authorizer %s access token", appid)
refreshTokenKey := "authorizer_refresh_token_" + appid
val := cache.GetContext(stdCtx, ctx.Cache, refreshTokenKey)
if val == nil {
return "", fmt.Errorf("cannot get authorizer %s refresh token", appid)
}
token, err := ctx.RefreshAuthrTokenContext(stdCtx, appid, val.(string))
if err != nil {
return "", err
}
return token.AccessToken, nil
}
return val.(string), nil
}

View File

@@ -1,6 +1,7 @@
package js
import (
context2 "context"
"fmt"
"github.com/silenceper/wechat/v2/credential"
@@ -32,14 +33,31 @@ func (js *Js) SetJsTicketHandle(ticketHandle credential.JsTicketHandle) {
// GetConfig 第三方平台 - 获取jssdk需要的配置参数
// uri 为当前网页地址
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
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 {
return
}
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 {
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&timestamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
sigStr := util.Signature(str)
config = new(officialJs.Config)
config.AppID = appid
config.NonceStr = nonceStr
config.Timestamp = timestamp

View 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)
}
})
}

View File

@@ -166,6 +166,7 @@ func PostFile(fieldName, filePath, uri string) ([]byte, error) {
IsFile: true,
Fieldname: fieldName,
FilePath: filePath,
Filename: filePath,
},
}
return PostMultipartForm(fields, uri)

View File

@@ -169,6 +169,7 @@ type UserUpdateRequest struct {
Gender int `json:"gender"`
Email string `json:"email"`
BizMail string `json:"biz_mail"`
BizMailAlias string `json:"biz_mail_alias"`
IsLeaderInDept []int `json:"is_leader_in_dept"`
DirectLeader []string `json:"direct_leader"`
Enable int `json:"enable"`

View File

@@ -21,6 +21,8 @@ const (
clearOptionURL = "https://qyapi.weixin.qq.com/cgi-bin/checkin/clear_checkin_option_array_field?access_token=%s"
// delOptionURL 删除打卡规则
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 为打卡人员排班请求
@@ -140,6 +142,7 @@ type OptionGroupRule struct {
SyncOutCheckin bool `json:"sync_out_checkin,omitempty"`
BukaRemind OptionGroupBukaRemind `json:"buka_remind,omitempty"`
BukaRestriction int64 `json:"buka_restriction,omitempty"`
CheckinMethodType int64 `json:"checkin_method_type,omitempty"`
SpanDayTime int64 `json:"span_day_time,omitempty"`
StandardWorkDuration int64 `json:"standard_work_duration,omitempty"`
}
@@ -385,3 +388,41 @@ func (r *Client) DelOption(req *DelOptionRequest) error {
}
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")
}

View File

@@ -173,9 +173,15 @@ type (
// OtInfo 加班信息
OtInfo struct {
OtStatus int64 `json:"ot_status"`
OtDuration int64 `json:"ot_duration"`
ExceptionDuration []uint64 `json:"exception_duration"`
OtStatus int64 `json:"ot_status"`
OtDuration int64 `json:"ot_duration"`
ExceptionDuration []uint64 `json:"exception_duration"`
WorkdayOverAsVacation int64 `json:"workday_over_as_vacation"`
WorkdayOverAsMoney int64 `json:"workday_over_as_money"`
RestdayOverAsVacation int64 `json:"restday_over_as_vacation"`
RestdayOverAsMoney int64 `json:"restday_over_as_money"`
HolidayOverAsVacation int64 `json:"holiday_over_as_vacation"`
HolidayOverAsMoney int64 `json:"holiday_over_as_money"`
}
)
@@ -237,13 +243,20 @@ type (
RegularDays int64 `json:"regular_days"`
RegularWorkSec int64 `json:"regular_work_sec"`
StandardWorkSec int64 `json:"standard_work_sec"`
RestDays int64 `json:"rest_days"`
}
// OverWorkInfo 加班情况
OverWorkInfo struct {
WorkdayOverSec int64 `json:"workday_over_sec"`
HolidayOverSec int64 `json:"holidays_over_sec"`
RestDayOverSec int64 `json:"restdays_over_sec"`
WorkdayOverSec int64 `json:"workday_over_sec"`
HolidayOverSec int64 `json:"holidays_over_sec"`
RestDayOverSec int64 `json:"restdays_over_sec"`
WorkdaysOverAsVacation int64 `json:"workdays_over_as_vacation"`
WorkdaysOverAsMoney int64 `json:"workdays_over_as_money"`
RestdaysOverAsVacation int64 `json:"restdays_over_as_vacation"`
RestdaysOverAsMoney int64 `json:"restdays_over_as_money"`
HolidaysOverAsVacation int64 `json:"holidays_over_as_vacation"`
HolidaysOverAsMoney int64 `json:"holidays_over_as_money"`
}
)
@@ -304,6 +317,10 @@ type CorpOptionGroup struct {
BukaRestriction int64 `json:"buka_restriction"`
ScheduleList []ScheduleList `json:"schedulelist"`
OffWorkIntervalTime int64 `json:"offwork_interval_time"`
SpanDayTime int64 `json:"span_day_time"`
StandardWorkDuration int64 `json:"standard_work_duration"`
OpenSpCheckin bool `json:"open_sp_checkin"`
CheckinMethodType int64 `json:"checkin_method_type"`
}
// GroupCheckinDate 打卡时间,当规则类型为排班时没有意义
@@ -505,6 +522,7 @@ type OptionInfo struct {
type OptionGroup struct {
GroupType int64 `json:"grouptype"`
GroupID int64 `json:"groupid"`
OpenSpCheckin bool `json:"open_sp_checkin"`
GroupName string `json:"groupname"`
CheckinDate []OptionCheckinDate `json:"checkindate"`
SpeWorkdays []SpeWorkdays `json:"spe_workdays"`
@@ -518,6 +536,10 @@ type OptionGroup struct {
LocInfos []LocInfos `json:"loc_infos"`
ScheduleList []ScheduleList `json:"schedulelist"`
BukaRestriction int64 `json:"buka_restriction"`
SpanDayTime int64 `json:"span_day_time"`
StandardWorkDuration int64 `json:"standard_work_duration"`
OffWorkIntervalTime int64 `json:"offwork_interval_time"`
CheckinMethodType int64 `json:"checkin_method_type"`
}
// OptionCheckinDate 打卡时间配置

View File

@@ -23,6 +23,8 @@ const (
customerAcquisitionQuotaURL = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition_quota?access_token=%s"
// customerAcquisitionStatistic 查询链接使用详情
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 (
@@ -308,3 +310,42 @@ func (r *Client) CustomerAcquisitionStatistic(req *CustomerAcquisitionStatisticR
err = util.DecodeWithError(response, result, "CustomerAcquisitionStatistic")
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
}